Agent Development Kit · Reference
Changelog
What changed, release by release — rendered from the package's own
CHANGELOG.md, so this page cannot disagree with it.
release
Unreleased
Fixed
npm install @animahealth/adkno longer fails withERESOLVE— the optional peer ranges now co-resolve (reactwidened to^18 || ^19,inkpinned to the tested^5,ink-text-inputto^6), and@anthropic-ai/claude-agent-sdkleft the peer list (its published versions requirezod@4; the SDK is bundled into@animahealth/adk/agents/coding/claude-code, so nothing needs it installed).- Importing the ESM main entry (or
/testing) no longer requiresopenaiand@ag-ui/coreto be installed — the build now code-splits, so lazy provider imports stay lazy instead of hoisting their SDKs' static imports to the entry's top level, and/testinguses the SDK-freeopenaimodel descriptor. A packaging gate (verify-package-exports.cjs) now walks each key-free entry's static import graph so this class cannot ship again. - Provider SDKs the core loads lazily (
openai,@google/genai,@anthropic-ai/vertex-sdk,ws) are now declared as optional peers, so package managers surface them instead of the firstimportfailing. - Concurrent first operations on a lazily-opened store no longer construct two instances —
SQLiteStoreand lazy vector providers (sqliteVec,qdrant,voyage) memoize the in-flight open, closing the window where a':memory:'database could silently drop one side's committed writes. sqliteVecfiltered search no longer returns empty when every match ranks beyond its overfetch window — the KNN window now widens untiltopKmatches are found or the collection is exhausted.- Event dedup is now uniform across session stores:
InMemoryStoreskips already-stored event ids like the SQL stores, and the SQLite/Postgres stores no longer assign a duplicateidxwhen a committed batch overlaps stored events (which leftORDER BY idxunspecified). memory(...).close()on a never-used lazy index is a no-op instead of instantiating the provider (and creating its database file) just to close it.PostgresStore.loadScopedStateno longer crashes on string-valued state ('dark') — the JSONB driver already decodes values, and the redundantJSON.parsethrew on any bare string.DynamoDBStore.list()now lists sessions (it was a stub returning[]); Scan-based — see its doc comment before using it on a large table.pgvectorreads (scroll/count/distanceMatrix/get) no longer create the collection's table as a side effect — a missing collection reads as empty instead of failing on a dimension-less table whose index cannot build. Mutations on a missing collection are no-ops.- Every documented store and vector backend now actually runs its shared compliance suite:
DynamoDBStoreandPostgresStorerun the session-store suite andpgvectorthe vector-index suite against service containers in CI (the suite fixtures are now hygienic across tests on shared backends). Theqdrantindex deliberately does not run the vector suite — it is provisioning-based (collections and named-vector sets are fixed at creation viacollectionSpec()), where the suite encodes lazy creation. composeHooks,loggingHook,metricsHook, andcliHookare now exported from the Core entry — only their option types were previously reachable, so a consumer could not construct any built-in hook at all.
Changed
model()in the test kit accepts a plain string as a text reply —model('hello'), symmetric withuser('hi'). Previously a string produced a response with no.text: the adapter emitted nothing and the run "passed" with the reply silently gone.OpenAIAdapteris now exported from@animahealth/adk/openai— the documentednew OpenAIAdapter(endpoints)+adk({ adapters: { openai } })seam for programmatic endpoint injection was previously unreachable (the class was not exported anywhere).OpenAIEndpointgainsdangerouslyAllowBrowser— passed through to the OpenAI/Azure client so a page where the END USER supplies their own key can construct the adapter in a browser. Never set it with a key the user did not type themselves.
Removed
- Experimental surfaces no longer reach the Core entry —
import { DockerExecutor } from '@animahealth/adk'and friends are gone./executors(all exports),/agents/coding(all exports, including the root-onlyclaudeCode/mockCodingAgentaliases — usecreateClaudeCodeAgent/createMockCodingAgentfrom@animahealth/adk/agents/coding), and the knowledge module (provisionClaudeProtocol,renderClaudeMd,renderRule,renderSettings— now internal, no replacement) left the main barrel. A gate (src/index.tier-boundary.test.ts) keeps every fenced module's vocabulary out of the Core entry. - The gateway/process-store surface (
createGateway,GatewayImpl,inMemoryProcessStore,postgresProcessStore,createInProcessExecutor, and their types), the artifact services (InMemoryArtifactService,postgresArtifactService,inferMimeType,createArtifactsProxy), and channels (InMemoryChannel,EventChannel) are internal — removed from the main barrel with no subpath. They are proposals-stage machinery, not public SDK. - SQLite backends —
SQLiteStore/sqliteStore()(/stores/sqlite), thesqliteIndex()vector provider, and thebetter-sqlite3/sqlite-vecoptional peers were dropped during the 0.5.20–0.5.27 line without a changelog entry; documenting here. Both surfaces are restored below (the vector provider returns assqliteVec, notsqliteIndex).
Added
- SQLite session store restored —
SQLiteStore/sqliteStore(dbPath)return at@animahealth/adk/stores/sqliteover the optionalbetter-sqlite3peer (>=11): zero-infrastructure durable sessions for local development, CLIs, and single-process deployments, passing the same store compliance suite as the in-memory and Postgres stores.':memory:'gives an ephemeral store. - SQLite vector memory restored as
sqliteVec({ path })— a config formemory({ index })likeqdrant(…)/pgvector(…), over the optionalbetter-sqlite3+sqlite-vecpeers (vec0 virtual tables, cosine metric). Successor to the removedsqliteIndex(); where the old provider's no-variantscroll/countread only thedefaultvariant,sqliteVecfollows the in-memory reference (each id is one logical point). - VectorIndex compliance suite —
runVectorIndexTests(src/memory/providers/index-compliance.test.ts) now proves every index provider against one contract; the in-memory reference andsqliteVecboth run it.
2026-08-20
0.5.27
Added
- OpenAI explicit prompt caching — configure
OpenAIModel.promptCacheand mark the stable prefix withapp.context.cacheableUser(...); Responses API cache reads and writes are exposed throughModelUsageandUsageSummary.
2026-06-08
0.5.25
Republish of 0.5.24 with a packaging fix — no API or runtime changes.
Fixed
- Published manifest —
@types/nodenow publishes as a concrete range (^22.19.19) instead of the raw pnpmcatalog:token. 0.5.24 shipped"@types/node": "catalog:", which brokepnpm pack/pnpm installfor consumers that vendor the ADK outside the Serenity workspace (e.g. the LiveKit voice agent deploy) withERR_PNPM_CATALOG_ENTRY_NOT_FOUND_FOR_SPEC.
Internal
- Publish pipeline —
adk-publish.ymlnow packs withpnpm pack(which resolvescatalog:/workspace:specifiers) and uploads the resulting tarball withnpm publish, so the published manifest no longer leaks workspace-only specifiers while keeping npm OIDC trusted publishing. Publishing the source directory withnpm publishshippedpackage.jsonverbatim, which is how thecatalog:token reached 0.5.24.
2026-06-04
0.5.24
Workflows: author Claude Code-style .workflow.js files and run them through the ADK, plus a few general additions used to express them.
Added
@animahealth/adk/workflow—runWorkflowFile()runs a CC-style workflow file throughapp.run, bindingagent()to a configurable node runner (defaultapp.ask; aCodingAgentover a provisioned workspace for build attractors).app.ask(prompt, opts)— terse, typed one-shot LLM call (no tools, fresh session); options typed asAskOpts.fanout(thunks, { limit })— capped isolated concurrency; a failed thunk resolves tonull.AnnotationEvent+ctx.note()— generic progress events (phase()/log()are sugar overctx.note()).
Changed
- Voice handler — end-of-invocation hooks (
afterAgent/afterTurn) now run inside LiveKit's shutdown barrier, so completion side effects (e.g.completeCall) run exactly once before the worker exits — even on abnormal teardown (caller disconnect, human transfer, drop). Previously they were skipped if the job was killed before the post-sessionDonepath ran. - Voice handler — fixed
shutdownProcessTimeoutunit bug (60ms →60_000ms / 60s, matching LiveKit's default); the worker was force-killing job processes ~60ms into shutdown, before finalization could complete. - Voice handler —
beforeAgentreturning a string now finalizes through the same shared path as any other call (completion hooks run), replacing a separate early-exit lifecycle.
2026-05-26
0.5.23
Added
- Voice lifecycle diagnostics — added typed voice activity and lifecycle hook events to production/eval voice handlers,
voiceLoggingHook, and voice eval reports. - Voice playout tests — covered
ctx.voice.generateReply()plusreply.waitForPlayout()from inside tool execution, including LiveKit awaitable speech handles.
Changed
- LiveKit voice peers — raised
@livekit/agentsand provider plugin peer floor to^1.4.4, and bumped@livekit/rtc-nodeto^0.13.28, verified with child speech-handle playout waits inside tool execution.
Fixed
- Voice output tools — model-initiated output-tool completion now stores the structured output internally without returning it to the realtime model, preventing final summaries from being spoken as a trailing assistant response.
2026-05-20
0.5.22
Voice output completion is now a visible, typed lifecycle step. Named voice tool forcing is handled inside the ADK without mutating the realtime tool list, so voice agents can reliably collect final structured output while preserving provider prompt caches.
Added
- Voice forced-tool gate —
ctx.voice.generateReply({ toolChoice: { name } })now enforces the named tool internally while sending provider-compatibletoolChoice: "required". - Voice output completion telemetry — added
output_tool_completion_started,output_tool_completion_succeeded,output_tool_completion_failed,forced_tool_correction, andforced_tool_failurevoice events. - Voice diagnostics — eval reports now include forced-tool and output-completion timelines with timestamps relative to case start.
- Voice errors — exported
ForcedToolCallErrorandOutputToolCompletionErrorfrom@animahealth/adk/voice. - Durable intent — added
packages/adk/intent/voice-forced-tool-gating/spec.mdfor cache-stable named tool forcing.
Changed
- Voice
ctx.end()— ending tools now return their tool result before ADK forces the configured output tool and then shuts down the voice lifecycle. - Voice output tools — output-tool completion timeout/failure is no longer treated as silent success; eval paths surface typed failures and production emits diagnostic voice events.
- Voice eval cleanup — teardown now uses bounded waits for tracker flush, LiveKit session close, recorder stop/disconnect, room disconnect, and room deletion.
Fixed
- Voice forced tools — wrong tools are intercepted before tool execution and before app
beforeToolhooks, then corrected after the synthetic wrong-tool result is returned to the provider. - Voice forced tools — required generations that produce no tool call now retry with a generic correction naming
no_tool_calland the intended tool. - Voice shutdown — after-turn hooks, session commit, and call termination now run in a stable order after output finalization.
Migration from 0.5.21
Voice output completion failures
Voice evals can now fail when the configured output tool is not actually completed. This is intentional: missing final structured output is now observable instead of being treated as best effort success. Production cleanup still runs after output completion failure.
Named voice tool choices
Applications no longer need app-level generic "wrong tool redirect" state for voice toolChoice: { name }. Keep domain-specific fallback logic in the application, but let the ADK own generic named-tool enforcement.
2026-05-19
0.5.21
Added
- Voice evals —
app.evaluate.voice.case((control) => case)now exposescontrol.disconnectUser(), letting eval code orchestrate caller disconnects from hooks, tool mocks, or other TypeScript code.
Fixed
- Voice handlers — participant disconnect, inactivity, expiry, and
ctx.end()paths now wait for the output tool to complete before room termination. - Voice evals — transcript hooks now run in the voice harness, and participant-left cases can pass/fail on metrics after cleanup instead of always reporting as terminated.
2026-05-17
0.5.20
Fixed
- Voice handlers — lifecycle hooks (
onInactivity,onExpiry,onDisconnect) now run the active agent hooks together with handler hooks, matching voice eval behavior and allowing agent-owned inactivity prompts in production. - Voice evals — expiry timeouts now run
onExpiryhooks before ending the case, matching production timeout behavior. - Artifact sync — file-watch artifact watchers now perform the documented final sweep on
stop(), so missed filesystem watch events are still collected. - Memory evals — the network-backed Voyage embedding eval now requires
ADK_RUN_VOYAGE_EVALS=1in addition toVOYAGE_API_KEY, keeping default test and publish runs offline.
Internal
- Package publishing — Serenity
packages/adkis now the source for publishing@animahealth/adk, with package metadata pointing at the Serenity monorepo.
2026-05-08
0.5.19
Fixed
- Voice handlers — forced output-tool replies now wait for speech playout before ending the LiveKit room.
2026-05-08
0.5.18
Fixed
- Voice
generateReply()— preserves the entry-reply scheduling yield after capturing LiveKit's synchronous speech handle soonEnterreplies do not race realtime session instruction updates.
2026-05-05
0.5.17
Fixed
- Voice handlers —
ctx.end()from a voice tool now waits for the model-triggered output tool and current playout to finish, then deletes the LiveKit room by default; usecallTermination: falseto leave hangup to the deployment. - Voice
generateReply()— LiveKit speech handles are now captured synchronously, named tool choices use LiveKit's{ type: 'function', function: { name } }shape, andundefinedtool results stayundefinedinstead of being coerced to an empty string.
2026-03-26
0.5.16
Changed
waitForPlayout— added toToolExecutionContextandMockToolContext; removed broken session-levelVoiceSession.waitForPlayout(). Usectx.waitForPlayout?.()in tools,reply.waitForPlayout()in lifecycle hooks.
Fixed
dynamoStore— unusedExpressionAttributeNameson non-create commits causedValidationException.dynamoStore— scoped-state pk separator changed from_to#to prevent collisions whenscopeIdcontains underscores. (unused in production currently)
2026-03-26
0.5.15
Fixed
dynamoStore— support custom key schemas viapartitionKey/sortKeyconfig options (defaults topk/skfor backwards compat).
2026-03-24
0.5.14
Fixed
- Coercion parser — use
_def.typeNameinstead ofinstanceofso coercion works across Zod instances. - Voice tool bridge — pass JSON Schema to LiveKit; validate with coercion in the ADK executor.
{}on optional primitive fields now coerces toundefined.
2026-03-24
0.5.13
Fixed
- Tool arg validation — run args through the coercion parser before
safeParse, so malformed values are coerced instead of silently failing. Applies to all agent tool calls, yielding tools, and the LiveKit voice bridge.
2026-03-21
0.5.12
Fixed
- Handler session key —
resolveSessionusedagent.nameinstead of the app name;app.sessions.get()could never find handler-created sessions.HandlerConfig.appNameis now required (app.handler.*injects it automatically).
Removed
session.truncateAt()— brokecommitSession(cursor divergence). Usesession.forkAt()instead.
2026-03-20
0.5.11
Added
session.truncateAt(eventIndex)— truncate event history in-place; unlikeforkAt, mutates the same session.- Memory
sample()— large candidate pools now bypass the server-side distance matrix and compute diversity locally from raw vectors.
2026-03-20
0.5.10
Fixed
toolInputsSchema()— generated schema fielddata→inputto matchToolInputconsumed byapplyInput().
Added
RestResponse.state— rest handler returns session state whenresponse.stateis enabled.
2026-03-17
0.5.9
Added
- Voice eval reports now include per-model-call cost and token breakdown.
Fixed
- A single voice eval worker crash no longer aborts the entire suite.
ctx.end()from the output tool no longer loops indefinitely.- Orphaned eval workers handle
EPIPE, closed IPC, and upstreamcurrentGenerationthrows gracefully.
2026-03-16
0.5.8
Added
toolMocks— output tools (agent.output) are now intercepted the same as regular tools; unmocked output tools throwEvalToolErrorwhentoolMocksis provided.
2026-03-16
0.5.7
Fixed
- Voice eval —
requireLiveKit()await import()→require()to fix dual-package hazard that silently prevented conversations from starting. - Voice eval report — stale speech end timestamps from previous segments no longer produce backwards time ranges.
2026-03-15
0.5.6
Changed
openai,gemini,claude,voyage,qdrant— import from@animahealth/adk/openai,/gemini,/claude,/voyage,/qdrantinstead of the main entry.
```typescript
// Before
import { adk, openai, voyage, qdrant } from '@animahealth/adk'
// After
import { adk } from '@animahealth/adk'
import { openai } from '@animahealth/adk/openai'
import { voyage } from '@animahealth/adk/voyage'
import { qdrant } from '@animahealth/adk/qdrant'
```
2026-03-14
0.5.5
Changed
- All optional dependencies (
openai,@google/genai,@anthropic-ai/vertex-sdk,pg,@qdrant/js-client-rest,voyageai,ws, etc.) —require()→ asyncimport()so bundlers can tree-shake unused providers. sqliteIndex(dbPath)— now returnsPromise<VectorIndex>; callers mustawait.
2026-03-08
0.5.4
Added
app.hook.voice()— typed entry point for custom voice hooks; acceptsPartial<VoiceHook<S>>and returnsVoiceHook<S>. Mirrorsapp.hook()for standard hooks.VoiceHook.onTranscript— fires for each user or assistant transcript message with full context (session,state,voice,event) andctx.run()for text-mode sub-agent orchestration. Runs in a dedicated queue that never blocks the voice pipeline; drained before session commit so in-flight state mutations are preserved.VoiceSession.turnCount— number of user speech segments (blocks of continuous user speech). Incremented each time the user starts speaking. Usectx.voice.turnCount > 0to determine if the caller has engaged.- Output tool auto-trigger — when an agent has an
outputtool andturnCount > 0, the ADK automatically triggers it viagenerateReply({ toolChoice: 'required' })on lifecycle events (disconnect, inactivity, expiry). Hooks no longer need to manually force the output tool. ctx.end()— synchronous control signal (likectx.output()) that triggers the agent's output tool via the model. Return it from a tool'sexecuteto end the session with model-generated output:return ctx.end(). Voice mode triggersgenerateReply; text mode setsendInvocation.ctx.run()/ctx.spawn()/ctx.dispatch()in voice mode — voice tools can now run text-mode sub-agents inline. Backed byBaseRunner; the sub-agent runs in the same session while the voice session continues.VoiceHandlerConfig.prewarm— optional per-subprocess init callback, called by LiveKit before any job runs (e.g. Sentry, OpenTelemetry setup)- Schema defaults — Zod
.default()values declared instateSchemaare now applied to initial state across all entry points (voice setup, REST/AG-UI handlers,app.run(), test runner, voice evals). Setup functions no longer need to manually mirror defaults. SessionSetup.noiseCancellation— per-session noise cancellation profile set insetup(). Overrides handler-levelsound.noiseCancellationwhen present.app.evaluate.report(options?)— factory that returns(result) => string. Configure once, call with any eval result. Replacesapp.report(result, options).app.evaluate.voice.report(options?)— voice-specific report factory with narrowed types;renderCasereceivesVoiceEvalCaseResultwithrun.transcript,run.timing,run.recording.BaseEvalCaseResult/BaseEvalResult— shared base types for text and voice eval results;EvalCaseResultandVoiceEvalCaseResultboth extendBaseEvalCaseResult.ReportOptions<S, R>— now generic over result typeR;renderCase,sections, andfootercallbacks receive the correct result/case types.MetricResult.data— optionalRecord<string, unknown>for attaching arbitrary structured data (computed values, intermediate measurements, debug info) to metric results.app.evaluate.case()/.cases()/.metric()— identity helpers for type-safe eval config definition; provides schema inference without runtime overhead.app.evaluate.voice.case()/.cases()— identity helpers for voice eval cases.app.initialState()— identity helper for typed multi-scope initial state config.MockToolContext.voice,.output(),.end(),.run()— expanded mock context surface; tool mocks can now test voice state, output signals, end signals, and sub-agent handoffs.Input.initialState/SessionSetup.initialState— multi-scope state seeding via handler input and voice setup; seedssession,user,patient,practice,org,teamscopes in one call.EvalOptions.repeat/VoiceEvalOptions.repeat— run each case N times; results carryrepeatIndexandrepeatTotalmetadata. Reports auto-group repeated cases with pass-rate summaries.BaseEvalCaseResult.repeatIndex/.repeatTotal— present whenrepeat > 1; structured repeat metadata replaces name-mangled[i/n]suffixes.- Voice eval process isolation — when
concurrency > 1, each voice eval case auto-forks into its own child process with an independent event loop and native thread pool. Eliminates WebRTC contention at high concurrency.
Changed
- Generic parameter order —
Agent<TOutput, S>→Agent<S, TOutput>(andAgentConfig,OutputConfig,RunResult,AgentSpec);Agent<unknown, MySchema>simplifies toAgent<MySchema> ctx.output()in voice mode — output signal is ignored whenturnCount === 0(no user engagement). A voice session with no user turn cannot produce meaningful output; the session ends through the lifecycle event (disconnect, inactivity) instead ofcompleted.SoundConfig.noiseCancellation—boolean | unknown→'general' | 'telephony'. Resolves toBackgroundVoiceCancellationorTelephonyBackgroundVoiceCancellationfrom@livekit/noise-cancellation-nodeinternally. Replacetruewith'general'.VoiceEvalOptions.room— now optional; defaults toLIVEKIT_URL/LIVEKIT_API_KEY/LIVEKIT_API_SECRETenv vars.- Voice eval default timeout — 480s → 300s (5 min).
StateChanges<S>— now generic over state schema for typed multi-scope seeding.MetricRun<S>— now generic over state schema;Metric<MetricRun<S>>provides typedsessionaccess.
Removed
app.report()— useapp.evaluate.report()insteadrepeatCases()— useEvalOptions.repeat/VoiceEvalOptions.repeatinsteadparseRepeatName()— repeat metadata is now structured (repeatIndex,repeatTotal) on result objects
Fixed
- Voice thinking sounds — pre-decoded into memory at session start; eliminates ffmpeg subprocess spawn and stutter on first play
- Voice handler cleanup — cancel speech wait and stop thinking sound on session end; prevents dangling timers after disconnect.
- Voice handler lifecycle — state guard before
beforeEndcallback prevents double-end race when multiple lifecycle events fire concurrently. - Voice handler transcript drain — 10s timeout cap prevents hanging when the transcript queue stalls on disconnect.
- Voice eval inactivity timer — now resets on agent activity and fires
onInactivityhooks, mirroring production handler behavior.
Migration from 0.5.3
Generic parameter order: <TOutput, S> → <S, TOutput>
```typescript
// Before
Agent<unknown, typeof schema>
Agent<string, typeof schema>
// After
Agent<typeof schema>
Agent<typeof schema, string>
```
app.report() → app.evaluate.report()
Report is now a factory — configure options up front, call the returned function with a result. This lets you define reports in a separate file without having the result available.
```typescript
// Before
const md = app.report(result, { title: 'My Eval' })
// After
const report = app.evaluate.report({ title: 'My Eval' })
const md = report(result)
// Voice — renderCase receives VoiceEvalCaseResult with full inference
const voiceReport = app.evaluate.voice.report({
renderCase: (r) => r.run.transcript.map((t) => t.text).join('\n'),
})
```
2026-03-05
0.5.3
Voice evaluation framework — run voice agents through real LiveKit rooms with a simulated user agent, collect transcripts and timing data, and measure outcomes with voice-specific metrics.
Added
evaluateVoice(cases, options)— runsVoiceEvalCase[]through LiveKit rooms with real audio; returnsVoiceEvalResultwith per-case transcripts, timing, recordings, and metricsVoiceEvalCase— defines agent under test, simulateduserAgent, optionaltoolMocks, per-casemetrics,retries, andtimeoutVoiceEvalOptions— suite config:room(LiveKit URL + credentials),outputdirectory for per-case reports and recordings,concurrency,stopOnFirstFailure,onCaseprogress callback, and suite-levelmetricsandhooksVoiceRunResult— run output withtranscript: TranscriptEntry[],timing: VoiceTiming,recording: { path },events,session,usage, anddurationMsVoiceTiming— timing data:timeToFirstSpeechMs,responseTimes,silenceGaps,interruptions(count, byAgent, byUser),vadResolutionMsvoiceTimingMetric(config)— metric factory for voice timing measures:time_to_first_speech,response_latency_p50,response_latency_p95,response_latency_max,silence_gap_max,silence_gap_total,interruption_countcreateSpeakerTracker()— tracks agent/user speech segments for transcript and timing computationTranscriptEntry—{ role, text, startMs?, endMs?, turnIndex }ToolContext.waitForPlayout— opt-in helper for tools that need to wait for pending agent speech to finishVoiceHook.onEnter— fires when an agent becomes active (initial entry and after transfer); replaces default auto-speak when defined- Voice handler auto-speak — calls
generateReply({ toolChoice: 'none' })on agent activation when noonEnterhook is defined
Changed
- Voice handler
onEnter— no longer auto-speaks when no hooks are defined; add an explicitonEnterhook to greet the caller VoiceEvalOptions.recording→VoiceEvalOptions.output
Fixed
- Voice thinking sounds — deferred until agent speech playout completes; tool execution itself is not blocked
- Voice inactivity timer — resets on actual speech start instead of transcript arrival; no longer fires while agent is thinking or speaking
Removed
Agent.greeting,GreetingContext— move greeting content into system prompt; useonEnterfor ephemeral instructions
2026-03-02
0.5.2
Internal
- Fix ESM
require()shim — injectcreateRequirebanner in tsup ESM output so lazyrequire()calls resolve correctly (fixes"Dynamic require of X is not supported") - Remove
lodashdependency —isEqual→node:util.isDeepStrictEqual
2026-03-01
0.5.1
Realtime voice agents — OpenAI and Gemini realtime adapters, LiveKit voice handler, native agent yield/resume loops with greeting and timeout support, structured session completion via output tools, and app-owned session management.
Added
realtime()/openai.realtime()/gemini.realtime()— wraps a provider model for realtime use; text-mode agents use native WebSocket adapters, full audio pipeline (withstt+tts) falls through to the standard adapter@animahealth/adk/voicesubpath — LiveKit voice handler;ctx.voice(VoiceSession) available in tools forgenerateReply,waitForPlayout,interrupt;app.handler.voice(config)on the handler namespaceOutputConfigaccepts aFunctionTool— passapp.tool()asagent.outputfor tool-injection output mode; framework injects the tool, validates args, runsexecute, captures output, and manages shutdownVoiceHook— extendsHookwithonInactivity,onExpiry,onDisconnectlifecycle callbacks; defined inside the unifiedhooksarray onVoiceHandlerConfig. Returnfalseto keep session alive,trueto explicitly end. Multiple hooks compose: anyfalsevetoes endVoiceHandlerConfig.timeouts— handler-level{ inactivity?, expiry? }defaults; per-agenttimeoutsoverride them after transfersVoiceSession.generateReply()accepts fullToolChoice— adds'required'and{ name: string }for forcing specific tool calls in lifecycle hooksAgent.yields— yield for user input after terminal model output instead of completing; defaults totruefor realtime models.Agent.maxTurnscaps yield/resume cycles (default: 100; producesstatus: 'max_turns')Agent.greeting— string or(ctx: GreetingContext) => string | Promise<string>; injected as system event on first activation only, not on resumeAgent.timeouts—{ inactivity?, expiry? }in ms; producesstatus: 'inactivity_timeout'orstatus: 'max_duration'ctx.output(value)onToolContext,ToolExecutionContext,StepContext— ends the invocation early; value becomesRunResult.output.valueadk({ store })— optionalSessionStoreon the app factory; defaults to in-memory.app.sessionsexposescreate,get,delete,list,commit,mergepre-bound to the app nameinMemoryStore(),dynamoStore(),sqliteStore(),postgresStore()— store factory functions; class constructors deprecatedUsageSummary.models— per-modelModelUsageEntry[]withcalls, token counts, and per-modelcost.ModelUsagegainsaudioInputTokens,audioOutputTokens,audioCachedTokensfor realtime modelsUserEvent.source,AssistantEvent.source—'text' | 'transcript'distinguishes direct text from audio transcription- Realtime model pricing —
gpt-realtime-*,gpt-4o-realtime,gpt-4o-mini-realtime,gemini-*-native-audio,gemini-live-* @livekit/agents,@livekit/agents-plugin-openai,@livekit/agents-plugin-google— optional peer dependencies
Changed
ctx.call()→ctx.run(),CallResult→SubRunResult,CallResultTransfer→SubRunResultTransfer— old names are deprecated aliases until 0.6.0AgentTimeouts.maxDuration→AgentTimeouts.expiry(deprecated aliasmaxDurationstill works)ModelEndEvent.modelName→ModelUsage.modelName— model name now lives on the usage object; code readingevent.modelNameonmodel_endevents must switch toevent.usage?.modelNameHook.afterAgent— output parameter widenedstring→unknown; typed hooks with explicitstringsignatures need updatingHandlerConfig.sessionService— now optional; handlers auto-inherit the app's session servicecalculateCost(usage, modelName)→calculateCost(usage)— model name read fromusage.modelNameformatCost— sub-dollar amounts usetoFixedinstead oftoPrecisionRunStatus—'inactivity-timeout'→'inactivity_timeout','max-duration'→'max_duration','participant-left'→'participant_left'; added'disconnected'and'participant_left'as first-class statuses (previously collapsed to'aborted')InvocationEndReason— same underscore renames asRunStatus;InvocationEndReasonis now a strict subset ofRunStatusInvocationState— limit end reasons (max_steps,max_turns,max_duration,inactivity_timeout,disconnected,participant_left) map to'completed'instead of their raw reason stringBaseRunner—sessionServiceconstructor option is now optional (defaults to in-memory)computeResumeContext()— multi-turn resume no longer bails early when a prior turn's root invocation is terminal; thefindYieldedNodesscan already handles "nothing to resume"
Deprecated
AgentTimeouts.maxDuration— useexpiryinstead. Will be removed in 0.7.0.sessionService()factory — useadk({ store })andapp.sessionsInMemoryStore,DynamoDBStore,PostgresStore,SQLiteStoreclasses — useinMemoryStore(),dynamoStore(),sqliteStore(),postgresStore()app.session()— useapp.sessions.create()- Standalone
turn()— useapp.handler.turn()
Removed
ModelEndEvent.modelName— useModelUsage.modelNamegemini-2.5-flashhigh-tier pricing bracket (price changed)
2026-02-23
0.5.0
Eval API moves onto the app (app.evaluate, app.report), with full RunResult preserved per case and markdown report generation for persistent, agent readable, git-checkable records.
Added
app.evaluate(cases, options)— runs eval cases (single or array) with tool mocking and metrics; returnsEvalResult<T, S>with app stateSand optional per-case contextTapp.report(result, options)— generates markdown fromEvalResult; optionaltitle,footer(string or function),sections, andrenderCasefor custom contentEvalCaseResult.run— fullRunResult(session, usage, output) preserved for reporting and metrics; convenience accessors.events,.usage,.turnsEvalOptions.onCase— progress callback(result, index, total) => voidcalled after each case completesEvalCase.retries— number of additional attempts for flaky cases; retries onfailed,error, ortimeoutEvalCase.timeout— per-case timeout in ms; producesstatus: 'timeout'on expiryTestOptions.maxTurns— replacesmaxIterationsfor consistency withSimulateOptionsRunResult.state— typedTypedState<S>accessor (same type asresult.session.state); symmetric withinput: { state }on the input sideAssistantEvent.media— optionalMediaPart[]field; eliminates the need to cast when accessing media on assistant eventsSession.boundState(invocationId)— returns invocation-scopedTypedState; useful in custom context renderersSession.onStateChange(callback)— registers a state change observerSession.getSpawnedTaskStatus(id),Session.getRunningSpawnedTasks(),Session.getAllSpawnedTasks(),Session.waitForSpawnedTask(id),Session.waitForAllSpawnedTasks(),Session.hasRunningSpawnedTasks()— spawned task observation from hooks and toolsVectorFilternested filters —must,should,must_notarrays now accept nestedVectorFilterobjects for compound boolean logic (e.g. AND-within-OR for per-slice filtering)mem.slices(names)— subset accessor for searching, sampling, and filtering across a selected set of slices in one call; return type narrows to only the selected slice typesSlicedSubset<TSlices>— type for the object returned bymem.slices()mem.variant.summary.returning('detailed')— cross-variant content: search using one variant's embeddings, return another variant's content
Changed
- Parser structured output — path-scoped visited key (no false circular ref); valid
partialused on parse failure when it passes schema EvalCase— composes shared fields withSimulateOptionsviaPick;initialState+firstMessagereplaced by standardinput(string or{ message, state })EvalCaseResult— nowEvalCaseResult<S>withrun: RunResult<unknown, S>;tokenUsage→usage(typeUsageSummary);eventsandturnspreserved as convenience accessorsMetric.evaluate— signature(events: Event[])→(run: RunResult); events available asrun.session.events; built-in metric factories updated- Eval error shape —
erroronEvalCaseResultis now{ message: string; stack?: string };EvalErrorinterface andphasefield removed - Eval status mapping —
abortedrun status now maps toEvalStatus: 'aborted'instead of falling through to pass/fail; metric name collisions between suite and case level emit a console warning app.handler.rest/app.handler.agui/app.handler.turn— now inherit app-levelhooksanderrorHandlers; handler-level config composes after app-level (app hooks run outer, handler hooks run inner)app.cli— now inherits app-levelhooks,errorHandlers, andappNamefor session creation; CLI-level hooks compose after app-level; respects user-providedrunneroverride- Untyped state scopes return
unknowninstead ofany— accessing properties on scopes without a schema (e.g.state.user.namewhen nouserschema is defined) now requires explicit narrowing mem.variantrenamed frommem.variants- Memory internal key separator
#→_— vector names (model#variant→model_variant), metadata prefixes (_variant#→_variant_,_slice#→_slice_); existing collections must be re-indexed ModelStartEvent—messages: ContextMessageSummary[]→messageCount: number;serializedSchemaremoved. The CLI reconstructs exact context on-demand viasession.forkAt()+buildContext()when the user expands a context block. Eliminates O(n²) storage of repeated context snapshots.SlicedMemory.slices→SlicedMemory.slice— singular accessor for per-slice operations (mem.slice.medication.search());slicesis now the subset methodeventCountMetric/eventSequenceMetric—filtercallback infers narrowed event type fromeventType; casts no longer needed.Metric<S>threads the app's state schema throughrun.state
Internal
- Voice lifecycle state machine (
idle → active → ending → ended) with atomictryEnd()— first caller wins, concurrent shutdown events are safely ignored - ADK-owned inactivity timer replaces LiveKit
userAwayTimeout— supports repeated firings andinactivityCountreset on user speech
Removed
Simulatortype — removed from main package andadk/evalexports (was the literal signature ofapp.simulate)llmJudge/LlmJudgeConfig— removed fromadk/eval; implement theMetricinterface directly with your own judge agent (see migration below)runEval/runEvalSuite/EvalSuiteConfig— useapp.evaluate(cases, options)EvalError— useerror?: { message: string; stack?: string }onEvalCaseResultEvalCaseResult.tokenUsage— useresult.usage(UsageSummary)EvalCase.initialState/EvalCase.firstMessage— use standardinput(stringor{ message, state })TestOptions.maxIterations→TestOptions.maxTurnstoolCallCountMetric— useeventCountMetricwitheventType: 'tool_call'and afilterdurationMetric— usetimingMetricwithmeasure: 'total_duration'modelLatencyMetric— usetimingMetricwithmeasure: 'model_latency_average'timeToFirstResponseMetric— usetimingMetricwithmeasure: 'time_to_first_assistant'
Migration from 0.4.x
EvalCase: initialState / firstMessage → input
EvalCase now uses the standard input field (same as app.run and app.simulate) instead of separate initialState and firstMessage fields.
```typescript
// Before
{ initialState: { session: { orgId: 'org-1' } }, firstMessage: 'Hello' }
// After
{ input: { message: 'Hello', state: { orgId: 'org-1' } } }
// or just: { input: 'Hello' }
```
Custom metrics: (events) → (run)
Metrics receive the full run; events are on run.session.events.
```typescript
// Before
const metric = {
name: 'my_metric',
evaluate: (events: Event[]) => {
/* ... */
},
}
// After
const metric = {
name: 'my_metric',
evaluate: (run: RunResult) => {
const events = [...run.session.events]
// ... same logic, or use run.usage, run.output, run.session.state
},
}
```
llmJudge → custom Metric
llmJudge assumed a fixed transcript format and generic pass/fail schema. Implement Metric directly with your judge agent; the metric now receives RunResult so you can pass session or output into the judge.
```typescript
// Before
import { llmJudge } from '@animahealth/adk/eval'
const metric = llmJudge({
name: 'quality',
prompt: '...',
model: openai('gpt-5-mini'),
passingScore: 0.8,
})
// After
import type { Metric, MetricResult } from '@animahealth/adk/eval'
return {
name: 'quality',
evaluate: async (run): Promise<MetricResult> => {
// build input from run.session.events or run.output, then run judge agent
const { output } = await app.run(judgeAgent, { input: judgeInput })
return {
passed: output.value!.score >= 0.8,
score: output.value!.score,
evidence: [output.value!.reasoning],
}
},
}
```
2026-02-20
0.4.6
Added
handler.turn— shared streaming lifecycle (resolve session, run, stream events, commit, resolve conflict); returnsStreamResult<TurnResult>withinvocationIdon the stream; use for custom projections (Slack, cron, CLI) without duplicating persistenceHook.afterTurn— turn-level lifecycle hook that runs within thehandler.turncommit boundary (after run completes, beforecommitSession); state mutations are included in the commit atomically; receivesTurnContextwith session, result, and runnableTurnContext— context type forafterTurn; provides writable session,RunResult, and the runnableCommitStatus,TurnResult—TurnResultextendsRunResultwithsessionId,invocationId, optionalcommitStatus('committed' | 'merged' | 'skipped' | 'orphaned')RunConfig.invocationId— optional root invocation ID; when set, runner uses it instead of generating one (enables traceability with AG-UIrunId)RunConfig.errorHandlers— per-run error handlers, composed after runner and agent handlers (mirrorsRunConfig.hooks)sqliteIndex()— SQLite vector index provider viasqlite-vecwith auto-provisioningvoyage()sagemakeroption — SageMaker endpoint with automatic fallback to Voyage API; each path retries independentlyVectorCondition.range— string bounds for datetime range filtering across all providersVectorCondition.text— case-insensitive text matching on string metadata fields;containsacceptsstring | string[](array = OR)SearchOptions.contains— shorthand for text matching against stored contentCollectionSpec.textIndexes— payload field names that need a text index for content search (Qdrant)normalizeFilter()— filter shorthand:{ org: 'acme' }expands to{ must: [{ key: 'org', match: { value: 'acme' } }] }; all filter-accepting methods (search,context,tool,sample,scroll,count) accept the shorthand viaFilterInputslicesconfig onmemory()— heterogeneous collections with per-slice typed metadata;records.slices.medication.search()returnsSearchResult<MedicationMeta>,records.search()returns a discriminated union withmatch.kindfor narrowing (renamed torecords.slicein 0.5.0)SlicedMemory,SliceAccessor,SlicedMatchUnion,SlicedSearchResult— types for sliced memoryCollectionSpec.payloadIndexes— auto-populated with_slice#kindwhen slices are declaredMatch.kind— optional; present when the document belongs to a slicebetter-sqlite3,sqlite-vec,@aws-sdk/client-sagemaker-runtime— optional peer dependencies
Changed
handler.agui— delegates toturn; events stream live (no buffering until commit); AG-UIrunIdis the turn’sinvocationId;RUN_FINISHEDresult payload includescommitStatusfor reconciliationhandler.rest— delegates toturninternally; external contract (buffered JSON response) unchangedresolveConflictreturn type —ConflictOutcome→CommitStatus(same values, adds'committed'for happy path)UpsertItem.content— now required; content is stored alongside vectors and returned asMatch.content- Upsert metadata — merge semantics across variants instead of replace
CollectionSpec.textIndexes— Qdrant users should provision text indexes from this field to enableSearchOptions.containsVoyageModel.dimensions— now required;voyage('voyage-4')→voyage('voyage-4', { dimensions: 1024 })MemoryConfig.variant→MemoryConfig.variants— singular string replaced by string array; omit for implicit['default']mem.variant('name')→mem.variants.name(changed tovariantin 0.5.0) — dynamic method replaced by upfront property mapcollectionSpec(config, variants)→collectionSpec(config)— variants now read fromconfig.variants- Internal metadata prefix
_content#→_variant#;_slice#reserved for slices
Removed
createRunId()— useturn(config, input).invocationId(or the root invocation ID from the stream) as AG-UIrunIdConflictOutcome— replaced byCommitStatus(import from handler or runtime types)vectorKey()— no longer public; usecollectionSpec()insteadEmbedResult,Point,VectorMatch,DistanceMatrixPair,DistanceMatrixResult— removed from top-level exports (importable from@animahealth/adk/memoryfor custom providers)MemoryContextConfig,MemoryToolConfig— removed aliases; use inlinemem.context()/mem.tool()config
Migration from 0.4.5
Memory variant API
```typescript
// Before
const mem = memory({ ..., variant: 'questionnaire' });
const full = mem.variant('full');
// After
const mem = memory({ ..., variants: ['questionnaire', 'full'] });
const full = mem.variants.full; // (changed to variant in 0.5.0)
```
Memory collectionSpec signature
```typescript
// Before
collectionSpec({ model, collection }, ['questionnaire', 'full'])
// After
collectionSpec({ model, collection, variants: ['questionnaire', 'full'] })
```
2026-02-15
0.4.5
Added
memory()— composable vector memory; typed metadata via Zod schema, provider-agnosticEmbedder/VectorIndexinterfacesvoyage()— Voyage AI embedding provider with batching (128/request), automaticinputTyperouting, retryqdrant()— Qdrant vector index provider with retrypgvector()— pgvector vector index provider (PostgreSQL) with auto-provisioning, HNSW indexing, retryinMemoryIndex()— in-memory vector index with real cosine similarity for testing and prototypingmem.context()— returnsContextRendererfor deterministic recall before reasoningmem.tool()— returnsFunctionToolfor agent-driven recall via tool callmem.search()— returns typedMatch<TMetadata>[]and computed embedding for downstream forwardingmem.upsert()— batch-aware write acceptingcontent(embeds) or pre-computedembedding; validates dimensionsmem.updateMetadata()— merge metadata without re-embedding;nulldeletes keysmem.variant()— named vector variants sharing collection, schema, and providersmem.sample()— representative sampling via density-weighted farthest-point selection; optional query-focused mode with gravityvectorKey()— exported so provisioning scripts, Terraform generators, and migration jobs can compute the samemodel#variantvector names the ADK uses internallycollectionSpec()— computes collection vector specifications from memory config for provisioningrepresentativeSample(),estimateDensity()— exported sampling utilities for custom workflowsEmbedder,EmbedResult,VectorIndex,Match,Point— exported types for custom provider implementationsvoyageai,@qdrant/js-client-rest,pg— optional peer dependencies
2026-02-11
0.4.4
Added
- Event type guards —
isToolCallEvent,isToolYieldEvent,isToolInputEvent,isToolResultEvent,isAssistantEvent, and 10 more for everyEvent/StreamEventmember SimulateYieldContextexported from main entry point
Changed
SimulateYieldContext,Transform,SimulateOptions— now generic overTArgs(defaults tounknown) soTransformcallbacks can typectx.argswithout casting
2026-02-11
0.4.3
Changed
- Eval, run, test, simulate, CLI, and handlers —
Runnable/Hookat orchestration boundaries widened toRunnable<any>/Hook<any>[]so typed agents and hooks work without casts
2026-02-10
0.4.2
Exports the eval framework as @animahealth/adk/eval and moves simulation termination into the core run loop.
Added
@animahealth/adk/eval— subpath export:runEval,runEvalSuite,interceptTools, metric factories, typesSimulateOptions.maxTurns,.maxDuration,.stateMatches— flat termination fields replacingmaxIterationsRunStatus: 'terminated'withterminationReason: TerminationReasonon the resultSimulateOptions.userAgentis now optional — tool-only flows no longer need a stub
Changed
SimulateOptions.maxIterations→maxTurnsEvalSuiteConfig.parallel: boolean→concurrency: number(defaults toInfinity; use1for sequential)runEval/runEvalSuite— first arg is now aSimulatorfunction (passapp.simulate)
Removed
SimulateOptions.maxIterations— usemaxTurnsEvalSuiteConfig.parallel— useconcurrency
Migration from 0.4.1
Max-iterations status change
maxIterations exceeded previously returned status: 'error'. It now returns status: 'terminated' with terminationReason: 'maxTurns'. Code that checked result.status === 'error' for iteration limits must check 'terminated' instead.
2026-02-09
0.4.1
Added
app.hook()— callable hook namespace for typed custom hooks, mirroringapp.context()createEventId/createCallId— exported from public API
Changed
ToolYieldEvent.preparedArgs→ToolYieldEvent.argsyieldedTools— returnsToolYieldEvent[]instead ofToolCallEvent[]- CLI — pending yields show enriched
tool_yieldargs instead of rawtool_callargs - OpenAI — synthetic call IDs normalized to
fc_prefix at serialization boundary
Migration from 0.4.0
yieldedTools returns ToolYieldEvent[] instead of ToolCallEvent[]
session.yieldedTools, RunResult.yieldedTools, and RestResponse.yieldedTools now return the enriched ToolYieldEvent (with prepare args) instead of the raw ToolCallEvent. Code that accessed yieldedTools[n].args continues to work — the args are now the enriched version from prepare.
2026-02-09
0.4.0
Consolidates the public API with a canonical Input/Output pair — descriptive yield statuses, unified media and tool, namespaced handler payloads, fewer redundant types.
Changed
RenderContext— all fields are nowreadonly; context renderers must return new objects instead of mutatingRunResult.status—'yielded'→'yielded_tool','input_required'→'yielded_message'; each branch of the discriminated union carries only its relevant fieldsapp.run()/app.test()/app.simulate()/ctx.call()(renamed toctx.run()in 0.5.1) /ctx.spawn()— typedAgent<TOutput>overloads that preserve output type through to the resultImageSource/AudioSource/DocumentSource→MediaSource;MessageInput.images/.audio→media: MediaPart[]ToolInput/ResultInput→ToolInput { callId, input };session.input.tool()now handles both tool yields and tool call resultsTestOptions—messages→userHandler,tools→toolHandlers;SimulateOptions/EvalCase—simulator→userAgent,tools→toolAgentsHook | Hook[]→Hook[]— hooks options now only accept an array; wrap a single hook in[hook]CallOptions/SpawnOptions/DispatchOptions→HandoffOptionsFunctionToolHookContext→ToolExecutionContextRunResultOutput<T>→Output<T>— canonical output shape shared byRunResult,Session,CallResult(renamed toSubRunResultin 0.5.1),SpawnResultRunInput/BaseInput→Input— canonical input shape withmessage,tools, andstatefieldsHandlerInput,RunOptions,TestOptions,SimulateOptions— payload fields grouped underinputnamespace;HandlerInput.inputusesInputdirectly;toolInputs→input.tools;messagewidened tostring | MessageInputRestResponse.output— uses canonicalOutputtype; opt-inevents,usageviaHandlerConfig.responseRestResponse.yieldedTools— yielded tools promoted to top-level field (replacestoolCall/toolCalls)session.pendingYieldingCalls→session.yieldedTools;result.pendingCalls→result.yieldedTools;pendingCallIds→yieldedToolIds
Removed
result.response— useresult.output.valueorresult.output.textresult.awaitingInput— checkresult.status === 'yielded_message'ImageSource,AudioSource,DocumentSource,ImageInput,AudioInput— useMediaSource/MediaPart[]ResultInput,session.input.result()— useToolInput/session.input.tool()CallResultOutput,CallOptions,SpawnOptions,DispatchOptions,FunctionToolHookContext,ToolHookContextRunResultOutput— useOutputRunInput,BaseInput— useInputSessionOutputNamespace—session.outputnow returnsOutputdirectlyAdkRunConfig— useRunOptions
Migration from 0.3.x
Immutable RenderContext
```typescript
// Before
app.context((ctx) => {
ctx.events.push(systemEvent)
ctx.allowedTools = ['search']
return ctx
})
// After
app.context((ctx) => ({
...ctx,
events: [...ctx.events, systemEvent],
allowedTools: ['search'],
}))
```
Yield statuses
```typescript
// Before
if (result.status === 'yielded') {
if (result.awaitingInput) {
/* loop */
} else {
/* tool */
}
}
// After
if (result.status === 'yielded_tool') {
session.input.tool({ callId: result.yieldedTools[0].callId, input: data })
}
if (result.status === 'yielded_message') {
session.input.message({ text, invocationId: result.yieldedInvocationId })
}
```
Tool input
```typescript
// Before
session.input.tool({ callId, data: value })
// After
session.input.tool({ callId, input: value })
```
Media input
```typescript
// Before
session.input.message({ text, images: [{ url }], audio: [{ mimeType, data }] })
// After
session.input.message({
text,
media: [
{ type: 'image', source: { type: 'url', url } },
{ type: 'audio', source: { type: 'base64', mimeType, data } },
],
})
```
Orchestration options
```typescript
// Before (ctx.call renamed to ctx.run in 0.5.1)
ctx.call(agent, { message: 'hello', tempState: { key: 'val' } })
// After (ctx.call renamed to ctx.run in 0.5.1)
ctx.call(agent, { input: { message: 'hello', state: { key: 'val' } } })
```
Input / Output types
```typescript
// Before
import type { RunInput, BaseInput, RunResultOutput } from '@animahealth/adk'
const input: RunInput = { message: 'Hello' }
const output: RunResultOutput = result.output
// After
import type { Input, Output } from '@animahealth/adk'
const input: Input = { message: 'Hello' }
const output: Output = result.output
```
Handler Input
```typescript
// Before
handler({ sessionId: 'abc', message: 'Hello', state: { mode: 'debug' } })
handler({
sessionId: 'abc',
toolInputs: [{ callId: 'c1', data: { ok: true } }],
})
// After
handler({
sessionId: 'abc',
input: { message: 'Hello', state: { mode: 'debug' } },
})
handler({
sessionId: 'abc',
input: { tools: [{ callId: 'c1', input: { ok: true } }] },
})
```
Handler Output
```typescript
// Before
response.output // string
response.toolCall // { callId, name, args }
response.toolCalls // Array<{ callId, name, args }>
// After
response.output.text // string
response.yieldedTools // Array<{ callId, name, args }>
```
RunOptions / TestOptions
```typescript
// Before
app.run(agent, { input: 'Hello', state: { mode: 'debug' } })
app.test(agent, { input: 'Start', tools: { ask: [{ answer: 'Blue' }] } })
// After
app.run(agent, { input: { message: 'Hello', state: { mode: 'debug' } } })
app.test(agent, {
input: { message: 'Start', tools: { ask: [{ answer: 'Blue' }] } },
})
```
app.run(agent, 'Hello') string shorthand is unchanged.
Yield Renames
```typescript
// Before
session.pendingYieldingCalls
result.pendingCalls
event.pendingCallIds
// After
session.yieldedTools
result.yieldedTools
event.yieldedToolIds
```
2026-02-07
0.3.1
Fixed
- Yielding tool
safeParsefailure — feed validation errors back astool_resultinstead of silently hanging - Consistent ID prefixes for forked sessions (
session_) and AG-UI runs (run_)
2026-02-07
0.3.0
Introduces the Hook system, pluggable session persistence, protocol handlers, and deterministic testing — replaces middleware, standalone runners, and user primitives.
Added
Hookinterface — unified observation (onEvent,onStep) + interception (before*/after*)app.run()acceptsRunOptionswithstateand call-sitehooksapp.test()— deterministic yield/resume testing (replacesscriptedUser())app.simulate()— LLM-powered eval loop (replacesagentUser())app.hook.logging(),app.hook.metrics()— built-in hook factories.app.handler.rest()/app.handler.agui()— protocol handlers withHandlerInput/HandlerConfigSessionStoreinterface withsessionService(store)factory — pluggable persistence- Stores:
InMemoryStore(main entry),SQLiteStore(/stores/sqlite),DynamoDBStore(/stores/dynamodb),PostgresStore(/stores/postgres) runSessionStoreTests()— shared compliance suite for custom stores- Scoped shared state via
session.scopes,getScopedState()/setScopedState() ConflictError— thrown on OCC version conflict duringcommitSession()
Changed
- **
runner.run()no longer commits sessions** — callers must callsessionService.commitSession()(built-in handlers do this automatically) Middleware/Hooks→ singleHookinterface;onStream→onEventAgent.middleware+Agent.hooks→Agent.hooks: Hook[];AdkConfig.middleware→AdkConfig.hookscomposeMiddleware()→composeHooks();loggingMiddleware()→loggingHook();cliMiddleware()→cliHook()session.versiontype:string→number;SessionStoreSnapshot→StoredSession
Removed
src/users/—scriptedUser(),humanUser(),agentUser(),Userinterface (useapp.test()/app.simulate())src/middleware/— replaced bysrc/hook/InMemorySessionService,LocalSessionService— usesessionService(new InMemoryStore())- Per-scope methods (
getUserState, etc.) — usegetScopedState/setScopedState HandlerInput.toolInput— usetoolInputsarray@animahealth/adk/persistencesubpath — use main entry or store subpaths
Migration from 0.2.x
Session Commit (runner.run callers only)
```typescript
// Before
const result = await runner.run(agent, session)
// After
const result = await runner.run(agent, session)
await sessionService.commitSession(session)
```
Built-in handlers and app.run() commit automatically — no change needed.
Middleware → Hooks
```typescript
// Before
const app = adk({ middleware: [loggingMiddleware()] })
const agent = app.agent({ middleware: [myMw], hooks: { beforeAgent: fn } })
// After
const app = adk({ hooks: [loggingHook()] })
const agent = app.agent({ hooks: [myHook, { beforeAgent: fn }] })
```
Session Stores
```typescript
// Before
import { sessionService, SQLiteStore } from '@animahealth/adk'
// After
import { sessionService, InMemoryStore } from '@animahealth/adk'
import { SQLiteStore } from '@animahealth/adk/stores/sqlite'
import { DynamoDBStore } from '@animahealth/adk/stores/dynamodb'
import { PostgresStore } from '@animahealth/adk/stores/postgres'
```
User Primitives → app.test / app.simulate
```typescript
// Before
await runner.runWithUser(agent, session, {
user: scriptedUser({ tools: { approve: [{ ok: true }] } }),
})
await runner.runWithUser(agent, session, {
user: agentUser({ loop: simAgent, tools: { ask: answerAgent } }),
})
// After
await app.test(agent, { input: 'Start', tools: { approve: [{ ok: true }] } })
await app.simulate(agent, {
input: 'Start',
simulator: simAgent,
tools: { ask: answerAgent },
})
```
2026-02-06
0.2.1
Changed
- Ink 3 / React 17 → Ink 5 / React 18 for CLI terminal UI
- CJS→ESM bridge for
app.cli()— transparentimport()wrapper so CJS consumers work unchanged extractCurrentThoughtBlock,buildInvocationBlocks→ exported from@animahealth/adk/cliinstead of main entryreact,ink,ink-spinner,ink-text-input— optional peer dependencies forapp.cli()consumers
Internal
tsup.config.tsnow includes an esbuild plugin that externalizes../cliin the CJS build, sodist/index.jsemitsrequire("./cli/index.js")instead of inlining the CLI module tree.scripts/postbuild-cli-cjs-wrapper.cjsgenerates a CJS→ESM wrapper atdist/cli/index.jsthat doesimport('./index.mjs')to load Ink in native ESM context.lodashis force-bundled (noExternal) to avoid Node ESM's "Named export not found" error when importing CJS-only packages.- Jest
moduleNameMappermocks added forinkandink-text-inputsince they are ESM-only and cannot berequire()'d in test.
Migration from 0.2.0
**CLI utility imports** — if you import extractCurrentThoughtBlock or buildInvocationBlocks, update the import path:
```typescript
// Before
import { extractCurrentThoughtBlock, buildInvocationBlocks } from '@animahealth/adk'
// After
import { extractCurrentThoughtBlock, buildInvocationBlocks } from '@animahealth/adk/cli'
```
2026-02-04
0.2.0
Introduces the adk() factory — a typed app instance with namespaced methods for agents, tools, context, sessions, and MCP, replacing standalone factories and adding multimodal input/output.
Added
adk()factory — creates typed app instance withnameandschemaapp.*methods for all runnables with automatic type inferenceapp.context.*namespace for context renderersapp.tools.*namespace for built-in tools: -webSearch()— web search via Serper API -fetchPage()— fetches web pages, PDFs, and images as markdown -takeScreenshot()— captures webpage screenshotsapp.mcp.*namespace for MCP server management: -server()— create/get MCP server instance -tools()— aggregated callable tools from all servers -toolDefinitions()— aggregated tool metadata from all servers -resourceDefinitions()— aggregated resource metadata from all servers -promptDefinitions()— aggregated prompt metadata from all serversserver.*instance API: -tools()— callableFunctionTool[]-toolDefinitions()— rawMCPToolInfo[]-resourceDefinitions()—MCPResourceInfo[]-promptDefinitions()—MCPPromptInfo[]-resource(uri)/prompt(name)— context rendererssession.input.*namespace for input operations: -message()— user messages (text and multimodal) -tool()— user input for yielding toolssession.output.*namespace for output operations: -text— last assistant text -items— all assistant events -tool()— provide tool results (superseded bysession.input.result()in 0.3.3)result.output.*namespace with convenient accessors: -text— last assistant message text -value— structured output (if schema configured) -items— all assistant events -media— generated media (images, audio)- Multimodal input via
session.input.message({ text, images, audio, media }) - Multimodal output via
result.output.mediaand tool__mediareturn pattern MediaParttype for image, audio, and document attachmentsImageInputandAudioInputhelpers:{ url }or{ mimeType, data }(base64)- Provider support: Claude, OpenAI, and Gemini handle media in user messages and tool results
spec.*namespace for cross-app reusable specs
Changed
- Standalone factories →
app.*methods (agent()→app.agent(), etc.) - Standalone context renderers →
app.context.*(injectSystemMessage()→app.context.system(), etc.) - Model providers remain standalone:
openai(),gemini(),claude() - Output config simplified:
output: 'key'instead ofoutput: output(schema, 'key') - State API: method-based → property access - Session state is now the default scope:
ctx.state.mode(notctx.state.session.mode) - Other scopes remain explicit:ctx.state.user.theme,ctx.state.patient.id - Session input:
addMessage()→session.input.message() - Session input:
addToolInput()→session.input.tool({ callId, data }) UserEventstructure simplified: -text: string— always the text message -media?: MediaPart[]— optional attachments (images, audio)
Removed
BaseRunner— useapp.run()instead- Standalone factories and context renderers — use
app.*methods - Method-based state API (
get,set,delete,toObject) initialStatefromCreateSessionOptions— usesession.state.update()session.addMessage()— usesession.input.message()insteadsession.addToolInput()— usesession.input.tool()insteadsession.addToolResult()— usesession.output.tool()(superseded bysession.input.result()in 0.3.3)session.append()— usesession.pushEvent()if needed (internal)
Migration from 0.1.0
App Factory Pattern
```typescript
// Before
import { agent, tool, openai, injectSystemMessage, includeHistory, BaseRunner } from '@animahealth/adk';
const myTool = tool({ name: 'greet', schema: z.object({ name: z.string() }), ... });
const assistant = agent({
name: 'assistant',
model: openai('gpt-4o-mini'),
context: [injectSystemMessage('You are helpful'), includeHistory()],
tools: [myTool],
});
await BaseRunner.run(assistant, 'Hello');
// After
import { adk, openai } from '@animahealth/adk';
const app = adk({ schema: { session: { mode: z.string() } } });
const myTool = app.tool({ name: 'greet', schema: z.object({ name: z.string() }), ... });
const assistant = app.agent({
name: 'assistant',
model: openai('gpt-4o-mini'),
context: [app.context.system('You are helpful'), app.context.history()],
tools: [myTool],
});
await app.run(assistant, 'Hello');
```
State API
```typescript
// Before
ctx.state.get('mode')
ctx.state.set('mode', 'triage')
// After
ctx.state.mode
ctx.state.mode = 'triage'
ctx.state.update({ mode: 'triage', count: 42 })
```
Session Input
```typescript
// Before
session.addMessage('Hello')
session.addToolInput(callId, input)
// After
session.input.message('Hello')
session.input.message({ text, invocationId }) // For resuming loops
session.input.tool({ callId, data: input })
```
Output Access
```typescript
// Before
const lastEvent = result.session.events.findLast((e) => e.type === 'assistant')
if (lastEvent && lastEvent.type === 'assistant') {
console.log(lastEvent.text)
}
// After
console.log(result.output.text)
// Structured output
const output = result.output.value
// Provide tool results (superseded by session.input.result() in 0.3.3)
session.output.tool({ callId, result: data })
```
Reusable Specs (Advanced)
```typescript
// For runnables shared across multiple apps:
import { spec } from '@animahealth/adk';
// Stateless (no schema)
const calc = spec.tool()({ name: 'calc', schema: z.object({ expr: z.string() }), ... });
// Stateful (with schema constraint)
const counter = spec.tool({ session: { count: z.number() } })({ name: 'inc', ... });
// Bind to any schema compatible app
const boundTool = app.use(calc);
```
2026-01-21
0.1.0
Added
- Initial release as standalone package ported from anima-service.
Migration from anima-service
```typescript
// Before
import { agent, tool } from '../../../modules/adk'
// After
import { agent, tool } from '@animahealth/adk'
```
For PersistentSessionService (DynamoDB), continue importing from anima-service until that is extracted.