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.
2026-10-14
0.6.8
Added
VoyageModel.timeoutMsandQdrantConfig.timeoutMs— the limit for each provider HTTP request, including reading the response; a whole number of milliseconds from 1 to 3600000, default 30000. A request that runs out fails, andretrydecides whether to try again. Any other value throws aRangeErrorwhen the provider is created. See vector memory.
Changed
qdrant()— requests time out after 30s by default instead of the client's 300s.voyage()— requests time out after 30s by default instead of the SDK's 60s, which stopped at the response headers, and fail withVoyage request timed out after <n>ms. The SDK no longer retries a 408, 409, 429 or 5xx response itself;retry.maxAttemptsis the number of requests.
2026-10-12
0.6.7
A suite's scorecard is one path, recorded with the commit it measured, so the scorecards of several
repositories can be kept in one place. The scorecard declaration from 0.6.6 changes shape in this
patch release: one suite had adopted it, and it moves with this release.
Added
- Scorecards and
provenancerecordjudges— each distinct model the cases' metrics reported calling, such as the judge metric's model. @animahealth/adk/evalexportsparseScorecardand theScorecardtype — the reader every tool shares.- Scorecards may hold
measures— aggregates that are not one case's pass count, each{ passed, runs }or{ value, n }— withcasesor in place of them. The eval CLI writescasesonly and rejects a scorecard that has none. OpenAIModel.serviceTier: 'flex'— sends OpenAI flex processing requests; a flex request that fails before any output is sent again at the default tier on the same endpoint. Azure endpoints get the default request. See OpenAI.
Changed
MixedEvalOptions.scorecard:{ path, fingerprint }→ a path from the top of the outermost repository that holds the working directory — a suite in a submodule records into the repository around it.- Scorecard files are version 2 —
repo(theowner/nametheoriginremote ends in) andcommitreplacefingerprint; a reader compares the paths it cares about at that commit. Scorecard.casesis optional, and the reader rejects a key the format does not name.- Eval CLI
run --recordneeds the scorecard's directory to exist, anoriginremote and the whole checkout committed, submodules aside, before any case runs, and records nothing whenHEADmoves or the checkout changes during the run. provenance.fingerprintis removed;provenance.dirtycovers the whole checkout, submodules aside, for every suite.
Migration from 0.6.6
Scorecard declaration and file
Replace scorecard: { path, fingerprint } with scorecard: '<path>', from the top of the outermost
repository. A version 1 scorecard at that path is rejected before any case runs, so --record
cannot replace it. Delete the file, commit the deletion and record again with --record; or replace
its fingerprint with repo and the commit whose ids for those paths are the recorded ones, and
set version to 2.
2026-10-08
0.6.6
Added
MixedEvalOptions.scorecard: { path, fingerprint }declares a committed eval CLI baseline with per-case pass counts, Git file IDs, runner, and models. Keep it in the same repository as its fingerprint paths, but outside them. See Eval scorecards.- Eval CLI
run --recordreplaces the declared scorecard with exactly the selected cases. It requires clean fingerprint paths and preserves the scorecard if the run stops early, a case errors, times out or aborts, or fingerprinted files change during the run.result.jsonreportsrecord: { file, written }; exit code 0 means the scorecard was written. - Eval CLI
runcompares with an existing declared scorecard unless--baselineis supplied, reporting its recording date and whether fingerprint paths still match. Scorecards require acasesfield and contain no tool results, socomparison.toolResultsis absent for these comparisons. - Eval CLI
--kind text|voicefilters cases alongside--case. An empty selection exits with code 2. result.jsonincludesprovenance: runner, models, available Git commit and dirty state, and declared fingerprints. Suites without scorecards still run when Git is unavailable.
2026-10-08
0.6.5
Added
Hook.duringModel(ctx, renderCtx, signal)runs alongside the model call. A hook answer that wins the race replaces the model's step and cancels the call. If the hook declines, rejects, or answers late, the model's step stands. The hook's signal aborts when its answer can no longer be used. Model events are buffered until the hook settles or the model step completes. See Hooks.
2026-10-07
0.6.4
Added
app.decide(input, { questions, model?, signal? })answers closed questions (predicate,choice,score) on the model's decisions endpoint, with probabilities. See One-shot calls.DecisionsUnavailableErrorrejects it for a model no decisions endpoint serves; today only OpenAIgpt-6-lunais served. There is no fallback to a model call.ModelAdapter.decideis the optional adapter method behind it;MockAdapterscripts it withdecisionsand fails an unscripted question.- Eval CLI
--baseline <run>(repeatable, pooled) compares a run with earlier ones — each case isregressed,improved,unchangedorunclearby a two-sided Fisher exact test on its pass counts, and tool results that one side returned and the other never did are listed. The comparison follows the summary inreport.mdand iscomparisoninresult.json. - Eval CLI
--casemay repeat. - A text case's
case-<n>.jsonhasmodelInputsandinstructions— the system messages,toolChoiceandallowedToolseach model call was given.
2026-10-06
0.6.3
Added
LiveVoiceHook.onVoiceActivityreports speaking, transcript, backend-run and silence events. Its context exposessinceRunandrunBackend()to process caller speech not yet given to a run. The trigger returnsfalsewhen a run is active, no speech is pending, or the call has ended.- Silence-timeout activity hooks can await before triggering a run. The handler holds the silence timer and waits up to
backendTimeoutMs; a run started during that wait replaces the silence response. Other activity events do not wait for hook promises. - GPT Live supports
sound.backgroundAudio.thinkingwhile delegations are pending, pausing for completed answers and resuming for remaining work. Playback failure leaves the call running without sound. Voice eval callers still hear only the agent's microphone track, not the thinking sound. LiveVoiceDelegation.startedBysays what started a GPT Live backend run:voicefor GPT Live's delegation,handlerfor the silence rule,appfor a hook'srunBackend(). Hooks read it onctx.delegationand onrun_startedandrun_settledactivity, so an app can tell its own trigger's run from the silence rule's without parsing the run's ID.
Fixed
- Package export verification can invoke npm on Windows.
Internal
- The silence rule that runs undelegated caller speech goes through the same trigger as
runBackend(); its behaviour is unchanged. - The ADK guide explains how to keep context prefixes stable for provider prompt caching.
- Stream regression tests cover nested runs, parallel branches, timeouts and aborts.
Migration from 0.6.2
Code that builds a LiveVoiceDelegation itself, such as a test double given to a hook, must set startedBy.
2026-09-30
0.6.2
Added
LiveVoiceControls.untilQuiet()waits for non-interruptible commentary to finish or reach its playout bound. Use it before a transfer that must follow a notice; it does not confirm the caller heard the notice.- GPT Live supports call recording through
recordingandsetup().recordingKey, and caller noise cancellation throughsound.noiseCancellation. app.replyToolandReplyToolConfiglet a tool call serve as the agent's reply without forcing an exit.OpenAIModel.parallelToolCalls: falsedisables parallel tool calls on the Responses API.LiveContentFilterError, exported from/voice, reports moderation stops to GPT LiveonErrorhooks after reconnection. It is recoverable and has no delegation.
Changed
- Non-interruptible GPT Live commentary waits for quiet, not in-flight delegations or earlier commentary to start. Combine a notice and its preceding commentary into one line when their order matters.
- GPT Live groups consecutive caller transcript fragments on each connection, up to the next delegation, into one user event in backend history and voice evals.
liveTranscriptretains raw fragments. - Voice eval evidence uses one run clock through
atMs; simulated callers can delegate without failing the case. OpenAIRealtimeTextAdapteruses the GA Realtime API and no longer sendstemperature. OptionalModelAdapter.endInvocationreleases resources when an invocation ends, including on a tool call.
Fixed
- GPT Live reconnects reuse the in-flight backend result for the same caller turn, preventing duplicate tool actions. Interrupted result and error hooks are not replayed.
- Moderation recovery starts a fresh connection without replaying the blocked conversation and drops commentary interrupted by the disconnect.
- Pending non-interruptible commentary suppresses silence prompts, settles if delivery fails, and no longer delays caller hangup. Invalid noise-cancellation modules fail at call startup.
- GPT Live error logs omit messages and stacks that could contain caller speech.
- Voice eval recording uses the participants' own rooms to avoid missing audio tracks. Evidence preserves safe error fields, omits function-valued state properties, and identifies invalid JSON values by path.
- Output coercion handles repeated union values independently instead of treating them as cycles. Output tools run even when omitted from the agent's
toolslist. - MCP installs process signal handlers only after an MCP server is added, leaving other hosts' shutdown handling untouched otherwise.
- GPT Live logs each provider
errorevent asLive provider errorwith its type and code only, never its message, so moderation stops and other provider failures are visible. - A caller turn GPT Live transcribes but never delegates is run on the backend when the silence timer fires, instead of a silence prompt, so the call no longer ends on silence.
Migration from 0.6.1
Custom implementations and mocks of LiveVoiceControls must implement untilQuiet(): Promise<void>. ADK-provided controls already implement it. Realtime consumers must use GA-compatible models and must not rely on temperature taking effect.
2026-09-28
0.6.1
Adds GPT Live, richer evaluations, self-hosted model adapters and Zod 4 support. Review the migration notes before upgrading from 0.6.0.
Added
- [GPT Live](docs/gpt-live.md) —
openai.live('gpt-live-1', options)connects voice agents to ADK backends, with tools, persisted state, transcripts, lifecycle hooks and cost tracking. Commentary supports interruption control, not guaranteed verbatim speech. - Evaluations —
app.evaluate.cliruns mixed text/voice suites with simulated callers, JSON evidence and pass/fail exit codes;app.evaluate.judgeadds [model-judged metrics](docs/judge-metric.md) with separate judge costs./evalexportsliveTranscriptTurnsfor speaker-turn grouping. - Model adapters —
/chat-completionsand/euroutersupport self-hosted endpoints, streaming, tools and structured output. run.settled— await execution and cleanup before committing or closing a session store.
Changed
- Schema compatibility — supports Zod 3 Classic
^3.25.76and Zod 4 Classic^4.6.5. - Provider compatibility — accepts matching LiveKit agents/plugins from
1.4.4; GPT Live requires Agents and the OpenAI plugin 1.9 or later. OpenAI requires^6.0.0, Google GenAI^1.52.0, and@livekit/rtc-nodeis an explicit optional peer. - Naming — use
app.terminal,/terminal,Terminal*,app.hook.console,consoleHookandConsoleHookOptions. All previous CLI names, imports and types remain deprecated aliases. - Pricing — estimates use LiteLLM's live catalog;
configurePricing(options | false)overrides or disables it. Cost-summary helpers accept a catalog, and unavailable prices leave costs unknown. - Streams, hooks and adapters — include nested runs, notes, yields and state changes consistently; REST and AG-UI filter process-local events. See migration notes for forwarding and hook timing.
- Eval reports — preserve partial costs and running results after failures. Metric data must be JSON-compatible, report cases require
run, and voice configuration moves underoptions.voice. Use returned evidence paths orindex.md.
Fixed
- Execution — correct nested-run status, errors, state callbacks and yielded-tool resumption. Calls after an output or transfer receive error results without running, preventing unanswered-tool errors.
- Voice — silence timers exclude speaking time and expose
user_speech_ended; see the changed inactivity-count semantics below. - Providers and schemas — correct cache/audio/thinking-token costs, preserve EUrouter reasoning and Zod wrapper metadata, support OpenAI
reasoning.effort: 'none'with temperature, and parse wrapped primitive outputs correctly. - Packaging — exclude generated caches, restore standalone test typechecking, and declare the existing
ctx.noteAPI.
Migration from 0.6.0
Stream forwarding and hooks
Run and turn-handler streams now include state_change for every scope. Before forwarding events outside the process, filter with mayLeaveProcess; REST and AG-UI already do so. Annotations are forwarded, so avoid patient data in ctx.note.
Hooks receive parallel events when branches settle and only state changes owned by their run's invocations. They continue observing unfinished spawned/dispatched work after a parent yields or fails. Use the new session.addStateChangeListener for direct session writes or other runs; custom Session implementations must implement it.
REST consumers should group nested events by invocationId and parentInvocationId. AG-UI consumers should use tool results rather than expecting nested agents' text in the conversation.
InvocationContext.onStream and ToolContext.onStream are removed. Return tool data or use ctx.note for annotations.
Metrics and pricing
Return passed: false for a product failure; thrown metric errors now produce status error, not failed. Treat missing cost as unknown, not zero; configure a reachable price map or disable estimation.
Silence count
inactivityCount resets when the agent replies, not on caller sounds or silence prompts. GPT Live requires a separate audio burst, with an 800 ms pause, to count a new reply. If an inactivity hook keeps the call open without speaking, the next reply counts as its prompt unless the handler skipped that hook. Review fixed-count hangup hooks against these semantics; see [GPT Live](docs/gpt-live.md).
2026-09-01
0.6.0
The first public release. It removes untested experimental APIs and restores the supported local storage integrations.
Added
- SQLite storage — restored
SQLiteStore,sqliteStore(), and vector memory throughsqliteVec(). - Built-in hooks — exported
composeHooks,loggingHook,metricsHook, andcliHookfrom the main entry. - Storage contracts — added shared compliance suites for session stores and vector indexes.
Changed
zod— raised the peer floor to^3.25.0and rejected Zod 4 schemas with a clear error.- Model adapters — applied
adk({ adapters })to run, handler, voice, and CLI entry points. - Provider SDKs — kept imports lazy and declared the packages as optional peers.
Fixed
- Stores — fixed concurrent lazy initialization, duplicate events, filtered SQLite vector search, and side effects from read-only
pgvectoroperations. - Postgres and DynamoDB — fixed string-valued Postgres state and implemented
DynamoDBStore.list(). - Optional dependencies — added clear missing-package errors for CLI, SQLite, and web packages.
- Test kit — shipped
/testingmatcher types and allowedmodel('text')replies.
Removed
- Experimental APIs — removed executors, gateway, process-store, artifact, channel, and knowledge APIs.
- Obsolete fields — removed
RunResult.stepEventsandFunctionTool.requiresApproval. Read events fromrun.session.eventsand use yielding tools for approval.
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 anyone consuming the package outside the workspace it is published from, 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. - Voice forced-tool gating — cache-stable named tool forcing is now a specified contract rather than an implementation detail.
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 —
@animahealth/adknow publishes from this package's own source tree, with repository metadata to match.
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 a standalone package, extracted from the service it grew inside.
Migration from the original in-service module
```typescript
// Before
import { agent, tool } from '../../../modules/adk'
// After
import { agent, tool } from '@animahealth/adk'
```
PersistentSessionService (DynamoDB) was not part of this extraction and still came from the original service at the time of this release.