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.timeoutMs and QdrantConfig.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, and retry decides whether to try again. Any other value throws a RangeError when 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 with Voyage request timed out after <n>ms. The SDK no longer retries a 408, 409, 429 or 5xx response itself; retry.maxAttempts is 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 provenance record judges — each distinct model the cases' metrics reported calling, such as the judge metric's model.
  • @animahealth/adk/eval exports parseScorecard and the Scorecard type — the reader every tool shares.
  • Scorecards may hold measures — aggregates that are not one case's pass count, each { passed, runs } or { value, n } — with cases or in place of them. The eval CLI writes cases only 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 (the owner/name the origin remote ends in) and commit replace fingerprint; a reader compares the paths it cares about at that commit.
  • Scorecard.cases is optional, and the reader rejects a key the format does not name.
  • Eval CLI run --record needs the scorecard's directory to exist, an origin remote and the whole checkout committed, submodules aside, before any case runs, and records nothing when HEAD moves or the checkout changes during the run.
  • provenance.fingerprint is removed; provenance.dirty covers 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 --record replaces 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.json reports record: { file, written }; exit code 0 means the scorecard was written.
  • Eval CLI run compares with an existing declared scorecard unless --baseline is supplied, reporting its recording date and whether fingerprint paths still match. Scorecards require a cases field and contain no tool results, so comparison.toolResults is absent for these comparisons.
  • Eval CLI --kind text|voice filters cases alongside --case. An empty selection exits with code 2.
  • result.json includes provenance: 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.
  • DecisionsUnavailableError rejects it for a model no decisions endpoint serves; today only OpenAI gpt-6-luna is served. There is no fallback to a model call.
  • ModelAdapter.decide is the optional adapter method behind it; MockAdapter scripts it with decisions and fails an unscripted question.
  • Eval CLI --baseline <run> (repeatable, pooled) compares a run with earlier ones — each case is regressed, improved, unchanged or unclear by 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 in report.md and is comparison in result.json.
  • Eval CLI --case may repeat.
  • A text case's case-<n>.json has modelInputs and instructions — the system messages, toolChoice and allowedTools each model call was given.

2026-10-06

0.6.3

Added

  • LiveVoiceHook.onVoiceActivity reports speaking, transcript, backend-run and silence events. Its context exposes sinceRun and runBackend() to process caller speech not yet given to a run. The trigger returns false when 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.thinking while 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.startedBy says what started a GPT Live backend run: voice for GPT Live's delegation, handler for the silence rule, app for a hook's runBackend(). Hooks read it on ctx.delegation and on run_started and run_settled activity, 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 recording and setup().recordingKey, and caller noise cancellation through sound.noiseCancellation.
  • app.replyTool and ReplyToolConfig let a tool call serve as the agent's reply without forcing an exit.
  • OpenAIModel.parallelToolCalls: false disables parallel tool calls on the Responses API.
  • LiveContentFilterError, exported from /voice, reports moderation stops to GPT Live onError hooks 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. liveTranscript retains raw fragments.
  • Voice eval evidence uses one run clock through atMs; simulated callers can delegate without failing the case.
  • OpenAIRealtimeTextAdapter uses the GA Realtime API and no longer sends temperature. Optional ModelAdapter.endInvocation releases 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 tools list.
  • MCP installs process signal handlers only after an MCP server is added, leaving other hosts' shutdown handling untouched otherwise.
  • GPT Live logs each provider error event as Live provider error with 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.cli runs mixed text/voice suites with simulated callers, JSON evidence and pass/fail exit codes; app.evaluate.judge adds [model-judged metrics](docs/judge-metric.md) with separate judge costs. /eval exports liveTranscriptTurns for speaker-turn grouping.
  • Model adapters — /chat-completions and /eurouter support 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.76 and 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-node is an explicit optional peer.
  • Naming — use app.terminal, /terminal, Terminal*, app.hook.console, consoleHook and ConsoleHookOptions. 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 under options.voice. Use returned evidence paths or index.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.note API.

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 through sqliteVec().
  • Built-in hooks — exported composeHooks, loggingHook, metricsHook, and cliHook from the main entry.
  • Storage contracts — added shared compliance suites for session stores and vector indexes.

Changed

  • zod — raised the peer floor to ^3.25.0 and 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 pgvector operations.
  • 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 /testing matcher types and allowed model('text') replies.

Removed

  • Experimental APIs — removed executors, gateway, process-store, artifact, channel, and knowledge APIs.
  • Obsolete fields — removed RunResult.stepEvents and FunctionTool.requiresApproval. Read events from run.session.events and use yielding tools for approval.

2026-08-20

0.5.27

Added

  • OpenAI explicit prompt caching — configure OpenAIModel.promptCache and mark the stable prefix with app.context.cacheableUser(...); Responses API cache reads and writes are exposed through ModelUsage and UsageSummary.

2026-06-08

0.5.25

Republish of 0.5.24 with a packaging fix — no API or runtime changes.

Fixed

  • Published manifest — @types/node now publishes as a concrete range (^22.19.19) instead of the raw pnpm catalog: token. 0.5.24 shipped "@types/node": "catalog:", which broke pnpm pack / pnpm install for anyone consuming the package outside the workspace it is published from, with ERR_PNPM_CATALOG_ENTRY_NOT_FOUND_FOR_SPEC.

Internal

  • Publish pipeline — adk-publish.yml now packs with pnpm pack (which resolves catalog: / workspace: specifiers) and uploads the resulting tarball with npm publish, so the published manifest no longer leaks workspace-only specifiers while keeping npm OIDC trusted publishing. Publishing the source directory with npm publish shipped package.json verbatim, which is how the catalog: 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 through app.run, binding agent() to a configurable node runner (default app.ask; a CodingAgent over a provisioned workspace for build attractors).
  • app.ask(prompt, opts) — terse, typed one-shot LLM call (no tools, fresh session); options typed as AskOpts.
  • fanout(thunks, { limit }) — capped isolated concurrency; a failed thunk resolves to null.
  • AnnotationEvent + ctx.note() — generic progress events (phase() / log() are sugar over ctx.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-sessionDone path ran.
  • Voice handler — fixed shutdownProcessTimeout unit 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 — beforeAgent returning 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() plus reply.waitForPlayout() from inside tool execution, including LiveKit awaitable speech handles.

Changed

  • LiveKit voice peers — raised @livekit/agents and provider plugin peer floor to ^1.4.4, and bumped @livekit/rtc-node to ^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-compatible toolChoice: "required".
  • Voice output completion telemetry — added output_tool_completion_started, output_tool_completion_succeeded, output_tool_completion_failed, forced_tool_correction, and forced_tool_failure voice events.
  • Voice diagnostics — eval reports now include forced-tool and output-completion timelines with timestamps relative to case start.
  • Voice errors — exported ForcedToolCallError and OutputToolCompletionError from @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 beforeTool hooks, 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_call and 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 exposes control.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 onExpiry hooks 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=1 in addition to VOYAGE_API_KEY, keeping default test and publish runs offline.

Internal

  • Package publishing — @animahealth/adk now 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 so onEnter replies 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; use callTermination: false to 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, and undefined tool results stay undefined instead of being coerced to an empty string.

2026-03-26

0.5.16

Changed

  • waitForPlayout — added to ToolExecutionContext and MockToolContext; removed broken session-level VoiceSession.waitForPlayout(). Use ctx.waitForPlayout?.() in tools, reply.waitForPlayout() in lifecycle hooks.

Fixed

  • dynamoStore — unused ExpressionAttributeNames on non-create commits caused ValidationException.
  • dynamoStore — scoped-state pk separator changed from _ to # to prevent collisions when scopeId contains underscores. (unused in production currently)

2026-03-26

0.5.15

Fixed

  • dynamoStore — support custom key schemas via partitionKey / sortKey config options (defaults to pk / sk for backwards compat).

2026-03-24

0.5.14

Fixed

  • Coercion parser — use _def.typeName instead of instanceof so 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 to undefined.

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 — resolveSession used agent.name instead of the app name; app.sessions.get() could never find handler-created sessions. HandlerConfig.appName is now required (app.handler.* injects it automatically).

Removed

  • session.truncateAt() — broke commitSession (cursor divergence). Use session.forkAt() instead.

2026-03-20

0.5.11

Added

  • session.truncateAt(eventIndex) — truncate event history in-place; unlike forkAt, 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 field data → input to match ToolInput consumed by applyInput().

Added

  • RestResponse.state — rest handler returns session state when response.state is 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 upstream currentGeneration throws gracefully.

2026-03-16

0.5.8

Added

  • toolMocks — output tools (agent.output) are now intercepted the same as regular tools; unmocked output tools throw EvalToolError when toolMocks is 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, /qdrant instead 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() → async import() so bundlers can tree-shake unused providers.
  • sqliteIndex(dbPath) — now returns Promise<VectorIndex>; callers must await.

2026-03-08

0.5.4

Added

  • app.hook.voice() — typed entry point for custom voice hooks; accepts Partial<VoiceHook<S>> and returns VoiceHook<S>. Mirrors app.hook() for standard hooks.
  • VoiceHook.onTranscript — fires for each user or assistant transcript message with full context (session, state, voice, event) and ctx.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. Use ctx.voice.turnCount > 0 to determine if the caller has engaged.
  • Output tool auto-trigger — when an agent has an output tool and turnCount > 0, the ADK automatically triggers it via generateReply({ toolChoice: 'required' }) on lifecycle events (disconnect, inactivity, expiry). Hooks no longer need to manually force the output tool.
  • ctx.end() — synchronous control signal (like ctx.output()) that triggers the agent's output tool via the model. Return it from a tool's execute to end the session with model-generated output: return ctx.end(). Voice mode triggers generateReply; text mode sets endInvocation.
  • ctx.run() / ctx.spawn() / ctx.dispatch() in voice mode — voice tools can now run text-mode sub-agents inline. Backed by BaseRunner; 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 in stateSchema are 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 in setup(). Overrides handler-level sound.noiseCancellation when present.
  • app.evaluate.report(options?) — factory that returns (result) => string. Configure once, call with any eval result. Replaces app.report(result, options).
  • app.evaluate.voice.report(options?) — voice-specific report factory with narrowed types; renderCase receives VoiceEvalCaseResult with run.transcript, run.timing, run.recording.
  • BaseEvalCaseResult / BaseEvalResult — shared base types for text and voice eval results; EvalCaseResult and VoiceEvalCaseResult both extend BaseEvalCaseResult.
  • ReportOptions<S, R> — now generic over result type R; renderCase, sections, and footer callbacks receive the correct result/case types.
  • MetricResult.data — optional Record<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; seeds session, user, patient, practice, org, team scopes in one call.
  • EvalOptions.repeat / VoiceEvalOptions.repeat — run each case N times; results carry repeatIndex and repeatTotal metadata. Reports auto-group repeated cases with pass-rate summaries.
  • BaseEvalCaseResult.repeatIndex / .repeatTotal — present when repeat > 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> (and AgentConfig, OutputConfig, RunResult, AgentSpec); Agent<unknown, MySchema> simplifies to Agent<MySchema>
  • ctx.output() in voice mode — output signal is ignored when turnCount === 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 of completed.
  • SoundConfig.noiseCancellation — boolean | unknown → 'general' | 'telephony'. Resolves to BackgroundVoiceCancellation or TelephonyBackgroundVoiceCancellation from @livekit/noise-cancellation-node internally. Replace true with 'general'.
  • VoiceEvalOptions.room — now optional; defaults to LIVEKIT_URL / LIVEKIT_API_KEY / LIVEKIT_API_SECRET env 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 typed session access.

Removed

  • app.report() — use app.evaluate.report() instead
  • repeatCases() — use EvalOptions.repeat / VoiceEvalOptions.repeat instead
  • parseRepeatName() — 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 beforeEnd callback 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 onInactivity hooks, 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) — runs VoiceEvalCase[] through LiveKit rooms with real audio; returns VoiceEvalResult with per-case transcripts, timing, recordings, and metrics
  • VoiceEvalCase — defines agent under test, simulated userAgent, optional toolMocks, per-case metrics, retries, and timeout
  • VoiceEvalOptions — suite config: room (LiveKit URL + credentials), output directory for per-case reports and recordings, concurrency, stopOnFirstFailure, onCase progress callback, and suite-level metrics and hooks
  • VoiceRunResult — run output with transcript: TranscriptEntry[], timing: VoiceTiming, recording: { path }, events, session, usage, and durationMs
  • VoiceTiming — timing data: timeToFirstSpeechMs, responseTimes, silenceGaps, interruptions (count, byAgent, byUser), vadResolutionMs
  • voiceTimingMetric(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_count
  • createSpeakerTracker() — tracks agent/user speech segments for transcript and timing computation
  • TranscriptEntry — { role, text, startMs?, endMs?, turnIndex }
  • ToolContext.waitForPlayout — opt-in helper for tools that need to wait for pending agent speech to finish
  • VoiceHook.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 no onEnter hook is defined

Changed

  • Voice handler onEnter — no longer auto-speaks when no hooks are defined; add an explicit onEnter hook 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; use onEnter for ephemeral instructions

2026-03-02

0.5.2

Internal

  • Fix ESM require() shim — inject createRequire banner in tsup ESM output so lazy require() calls resolve correctly (fixes "Dynamic require of X is not supported")
  • Remove lodash dependency — 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 (with stt + tts) falls through to the standard adapter
  • @animahealth/adk/voice subpath — LiveKit voice handler; ctx.voice (VoiceSession) available in tools for generateReply, waitForPlayout, interrupt; app.handler.voice(config) on the handler namespace
  • OutputConfig accepts a FunctionTool — pass app.tool() as agent.output for tool-injection output mode; framework injects the tool, validates args, runs execute, captures output, and manages shutdown
  • VoiceHook — extends Hook with onInactivity, onExpiry, onDisconnect lifecycle callbacks; defined inside the unified hooks array on VoiceHandlerConfig. Return false to keep session alive, true to explicitly end. Multiple hooks compose: any false vetoes end
  • VoiceHandlerConfig.timeouts — handler-level { inactivity?, expiry? } defaults; per-agent timeouts override them after transfers
  • VoiceSession.generateReply() accepts full ToolChoice — adds 'required' and { name: string } for forcing specific tool calls in lifecycle hooks
  • Agent.yields — yield for user input after terminal model output instead of completing; defaults to true for realtime models. Agent.maxTurns caps yield/resume cycles (default: 100; produces status: 'max_turns')
  • Agent.greeting — string or (ctx: GreetingContext) => string | Promise<string>; injected as system event on first activation only, not on resume
  • Agent.timeouts — { inactivity?, expiry? } in ms; produces status: 'inactivity_timeout' or status: 'max_duration'
  • ctx.output(value) on ToolContext, ToolExecutionContext, StepContext — ends the invocation early; value becomes RunResult.output.value
  • adk({ store }) — optional SessionStore on the app factory; defaults to in-memory. app.sessions exposes create, get, delete, list, commit, merge pre-bound to the app name
  • inMemoryStore(), dynamoStore(), sqliteStore(), postgresStore() — store factory functions; class constructors deprecated
  • UsageSummary.models — per-model ModelUsageEntry[] with calls, token counts, and per-model cost. ModelUsage gains audioInputTokens, audioOutputTokens, audioCachedTokens for realtime models
  • UserEvent.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.0
  • AgentTimeouts.maxDuration → AgentTimeouts.expiry (deprecated alias maxDuration still works)
  • ModelEndEvent.modelName → ModelUsage.modelName — model name now lives on the usage object; code reading event.modelName on model_end events must switch to event.usage?.modelName
  • Hook.afterAgent — output parameter widened string → unknown; typed hooks with explicit string signatures need updating
  • HandlerConfig.sessionService — now optional; handlers auto-inherit the app's session service
  • calculateCost(usage, modelName) → calculateCost(usage) — model name read from usage.modelName
  • formatCost — sub-dollar amounts use toFixed instead of toPrecision
  • RunStatus — '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 as RunStatus; InvocationEndReason is now a strict subset of RunStatus
  • InvocationState — limit end reasons (max_steps, max_turns, max_duration, inactivity_timeout, disconnected, participant_left) map to 'completed' instead of their raw reason string
  • BaseRunner — sessionService constructor 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; the findYieldedNodes scan already handles "nothing to resume"

Deprecated

  • AgentTimeouts.maxDuration — use expiry instead. Will be removed in 0.7.0.
  • sessionService() factory — use adk({ store }) and app.sessions
  • InMemoryStore, DynamoDBStore, PostgresStore, SQLiteStore classes — use inMemoryStore(), dynamoStore(), sqliteStore(), postgresStore()
  • app.session() — use app.sessions.create()
  • Standalone turn() — use app.handler.turn()

Removed

  • ModelEndEvent.modelName — use ModelUsage.modelName
  • gemini-2.5-flash high-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; returns EvalResult<T, S> with app state S and optional per-case context T
  • app.report(result, options) — generates markdown from EvalResult; optional title, footer (string or function), sections, and renderCase for custom content
  • EvalCaseResult.run — full RunResult (session, usage, output) preserved for reporting and metrics; convenience accessors .events, .usage, .turns
  • EvalOptions.onCase — progress callback (result, index, total) => void called after each case completes
  • EvalCase.retries — number of additional attempts for flaky cases; retries on failed, error, or timeout
  • EvalCase.timeout — per-case timeout in ms; produces status: 'timeout' on expiry
  • TestOptions.maxTurns — replaces maxIterations for consistency with SimulateOptions
  • RunResult.state — typed TypedState<S> accessor (same type as result.session.state); symmetric with input: { state } on the input side
  • AssistantEvent.media — optional MediaPart[] field; eliminates the need to cast when accessing media on assistant events
  • Session.boundState(invocationId) — returns invocation-scoped TypedState; useful in custom context renderers
  • Session.onStateChange(callback) — registers a state change observer
  • Session.getSpawnedTaskStatus(id), Session.getRunningSpawnedTasks(), Session.getAllSpawnedTasks(), Session.waitForSpawnedTask(id), Session.waitForAllSpawnedTasks(), Session.hasRunningSpawnedTasks() — spawned task observation from hooks and tools
  • VectorFilter nested filters — must, should, must_not arrays now accept nested VectorFilter objects 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 types
  • SlicedSubset<TSlices> — type for the object returned by mem.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 partial used on parse failure when it passes schema
  • EvalCase — composes shared fields with SimulateOptions via Pick; initialState + firstMessage replaced by standard input (string or { message, state })
  • EvalCaseResult — now EvalCaseResult<S> with run: RunResult<unknown, S>; tokenUsage → usage (type UsageSummary); events and turns preserved as convenience accessors
  • Metric.evaluate — signature (events: Event[]) → (run: RunResult); events available as run.session.events; built-in metric factories updated
  • Eval error shape — error on EvalCaseResult is now { message: string; stack?: string }; EvalError interface and phase field removed
  • Eval status mapping — aborted run status now maps to EvalStatus: '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-level hooks and errorHandlers; handler-level config composes after app-level (app hooks run outer, handler hooks run inner)
  • app.cli — now inherits app-level hooks, errorHandlers, and appName for session creation; CLI-level hooks compose after app-level; respects user-provided runner override
  • Untyped state scopes return unknown instead of any — accessing properties on scopes without a schema (e.g. state.user.name when no user schema is defined) now requires explicit narrowing
  • mem.variant renamed from mem.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; serializedSchema removed. The CLI reconstructs exact context on-demand via session.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()); slices is now the subset method
  • eventCountMetric / eventSequenceMetric — filter callback infers narrowed event type from eventType; casts no longer needed. Metric<S> threads the app's state schema through run.state

Internal

  • Voice lifecycle state machine (idle → active → ending → ended) with atomic tryEnd() — first caller wins, concurrent shutdown events are safely ignored
  • ADK-owned inactivity timer replaces LiveKit userAwayTimeout — supports repeated firings and inactivityCount reset on user speech

Removed

  • Simulator type — removed from main package and adk/eval exports (was the literal signature of app.simulate)
  • llmJudge / LlmJudgeConfig — removed from adk/eval; implement the Metric interface directly with your own judge agent (see migration below)
  • runEval / runEvalSuite / EvalSuiteConfig — use app.evaluate(cases, options)
  • EvalError — use error?: { message: string; stack?: string } on EvalCaseResult
  • EvalCaseResult.tokenUsage — use result.usage (UsageSummary)
  • EvalCase.initialState / EvalCase.firstMessage — use standard input (string or { message, state })
  • TestOptions.maxIterations → TestOptions.maxTurns
  • toolCallCountMetric — use eventCountMetric with eventType: 'tool_call' and a filter
  • durationMetric — use timingMetric with measure: 'total_duration'
  • modelLatencyMetric — use timingMetric with measure: 'model_latency_average'
  • timeToFirstResponseMetric — use timingMetric with measure: '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); returns StreamResult<TurnResult> with invocationId on the stream; use for custom projections (Slack, cron, CLI) without duplicating persistence
  • Hook.afterTurn — turn-level lifecycle hook that runs within the handler.turn commit boundary (after run completes, before commitSession); state mutations are included in the commit atomically; receives TurnContext with session, result, and runnable
  • TurnContext — context type for afterTurn; provides writable session, RunResult, and the runnable
  • CommitStatus, TurnResult — TurnResult extends RunResult with sessionId, invocationId, optional commitStatus ('committed' | 'merged' | 'skipped' | 'orphaned')
  • RunConfig.invocationId — optional root invocation ID; when set, runner uses it instead of generating one (enables traceability with AG-UI runId)
  • RunConfig.errorHandlers — per-run error handlers, composed after runner and agent handlers (mirrors RunConfig.hooks)
  • sqliteIndex() — SQLite vector index provider via sqlite-vec with auto-provisioning
  • voyage() sagemaker option — SageMaker endpoint with automatic fallback to Voyage API; each path retries independently
  • VectorCondition.range — string bounds for datetime range filtering across all providers
  • VectorCondition.text — case-insensitive text matching on string metadata fields; contains accepts string | string[] (array = OR)
  • SearchOptions.contains — shorthand for text matching against stored content
  • CollectionSpec.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 via FilterInput
  • slices config on memory() — heterogeneous collections with per-slice typed metadata; records.slices.medication.search() returns SearchResult<MedicationMeta>, records.search() returns a discriminated union with match.kind for narrowing (renamed to records.slice in 0.5.0)
  • SlicedMemory, SliceAccessor, SlicedMatchUnion, SlicedSearchResult — types for sliced memory
  • CollectionSpec.payloadIndexes — auto-populated with _slice#kind when slices are declared
  • Match.kind — optional; present when the document belongs to a slice
  • better-sqlite3, sqlite-vec, @aws-sdk/client-sagemaker-runtime — optional peer dependencies

Changed

  • handler.agui — delegates to turn; events stream live (no buffering until commit); AG-UI runId is the turn’s invocationId; RUN_FINISHED result payload includes commitStatus for reconciliation
  • handler.rest — delegates to turn internally; external contract (buffered JSON response) unchanged
  • resolveConflict return type — ConflictOutcome → CommitStatus (same values, adds 'committed' for happy path)
  • UpsertItem.content — now required; content is stored alongside vectors and returned as Match.content
  • Upsert metadata — merge semantics across variants instead of replace
  • CollectionSpec.textIndexes — Qdrant users should provision text indexes from this field to enable SearchOptions.contains
  • VoyageModel.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 to variant in 0.5.0) — dynamic method replaced by upfront property map
  • collectionSpec(config, variants) → collectionSpec(config) — variants now read from config.variants
  • Internal metadata prefix _content# → _variant#; _slice# reserved for slices

Removed

  • createRunId() — use turn(config, input).invocationId (or the root invocation ID from the stream) as AG-UI runId
  • ConflictOutcome — replaced by CommitStatus (import from handler or runtime types)
  • vectorKey() — no longer public; use collectionSpec() instead
  • EmbedResult, Point, VectorMatch, DistanceMatrixPair, DistanceMatrixResult — removed from top-level exports (importable from @animahealth/adk/memory for custom providers)
  • MemoryContextConfig, MemoryToolConfig — removed aliases; use inline mem.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-agnostic Embedder / VectorIndex interfaces
  • voyage() — Voyage AI embedding provider with batching (128/request), automatic inputType routing, retry
  • qdrant() — Qdrant vector index provider with retry
  • pgvector() — pgvector vector index provider (PostgreSQL) with auto-provisioning, HNSW indexing, retry
  • inMemoryIndex() — in-memory vector index with real cosine similarity for testing and prototyping
  • mem.context() — returns ContextRenderer for deterministic recall before reasoning
  • mem.tool() — returns FunctionTool for agent-driven recall via tool call
  • mem.search() — returns typed Match<TMetadata>[] and computed embedding for downstream forwarding
  • mem.upsert() — batch-aware write accepting content (embeds) or pre-computed embedding; validates dimensions
  • mem.updateMetadata() — merge metadata without re-embedding; null deletes keys
  • mem.variant() — named vector variants sharing collection, schema, and providers
  • mem.sample() — representative sampling via density-weighted farthest-point selection; optional query-focused mode with gravity
  • vectorKey() — exported so provisioning scripts, Terraform generators, and migration jobs can compute the same model#variant vector names the ADK uses internally
  • collectionSpec() — computes collection vector specifications from memory config for provisioning
  • representativeSample(), estimateDensity() — exported sampling utilities for custom workflows
  • Embedder, EmbedResult, VectorIndex, Match, Point — exported types for custom provider implementations
  • voyageai, @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 every Event/StreamEvent member
  • SimulateYieldContext exported from main entry point

Changed

  • SimulateYieldContext, Transform, SimulateOptions — now generic over TArgs (defaults to unknown) so Transform callbacks can type ctx.args without casting

2026-02-11

0.4.3

Changed

  • Eval, run, test, simulate, CLI, and handlers — Runnable/Hook at orchestration boundaries widened to Runnable<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, types
  • SimulateOptions.maxTurns, .maxDuration, .stateMatches — flat termination fields replacing maxIterations
  • RunStatus: 'terminated' with terminationReason: TerminationReason on the result
  • SimulateOptions.userAgent is now optional — tool-only flows no longer need a stub

Changed

  • SimulateOptions.maxIterations → maxTurns
  • EvalSuiteConfig.parallel: boolean → concurrency: number (defaults to Infinity; use 1 for sequential)
  • runEval / runEvalSuite — first arg is now a Simulator function (pass app.simulate)

Removed

  • SimulateOptions.maxIterations — use maxTurns
  • EvalSuiteConfig.parallel — use concurrency

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, mirroring app.context()
  • createEventId / createCallId — exported from public API

Changed

  • ToolYieldEvent.preparedArgs → ToolYieldEvent.args
  • yieldedTools — returns ToolYieldEvent[] instead of ToolCallEvent[]
  • CLI — pending yields show enriched tool_yield args instead of raw tool_call args
  • 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 now readonly; context renderers must return new objects instead of mutating
  • RunResult.status — 'yielded' → 'yielded_tool', 'input_required' → 'yielded_message'; each branch of the discriminated union carries only its relevant fields
  • app.run() / app.test() / app.simulate() / ctx.call() (renamed to ctx.run() in 0.5.1) / ctx.spawn() — typed Agent<TOutput> overloads that preserve output type through to the result
  • ImageSource / AudioSource / DocumentSource → MediaSource; MessageInput.images / .audio → media: MediaPart[]
  • ToolInput / ResultInput → ToolInput { callId, input }; session.input.tool() now handles both tool yields and tool call results
  • TestOptions — messages → userHandler, tools → toolHandlers; SimulateOptions / EvalCase — simulator → userAgent, tools → toolAgents
  • Hook | Hook[] → Hook[] — hooks options now only accept an array; wrap a single hook in [hook]
  • CallOptions / SpawnOptions / DispatchOptions → HandoffOptions
  • FunctionToolHookContext → ToolExecutionContext
  • RunResultOutput<T> → Output<T> — canonical output shape shared by RunResult, Session, CallResult (renamed to SubRunResult in 0.5.1), SpawnResult
  • RunInput / BaseInput → Input — canonical input shape with message, tools, and state fields
  • HandlerInput, RunOptions, TestOptions, SimulateOptions — payload fields grouped under input namespace; HandlerInput.input uses Input directly; toolInputs → input.tools; message widened to string | MessageInput
  • RestResponse.output — uses canonical Output type; opt-in events, usage via HandlerConfig.response
  • RestResponse.yieldedTools — yielded tools promoted to top-level field (replaces toolCall / toolCalls)
  • session.pendingYieldingCalls → session.yieldedTools; result.pendingCalls → result.yieldedTools; pendingCallIds → yieldedToolIds

Removed

  • result.response — use result.output.value or result.output.text
  • result.awaitingInput — check result.status === 'yielded_message'
  • ImageSource, AudioSource, DocumentSource, ImageInput, AudioInput — use MediaSource / MediaPart[]
  • ResultInput, session.input.result() — use ToolInput / session.input.tool()
  • CallResultOutput, CallOptions, SpawnOptions, DispatchOptions, FunctionToolHookContext, ToolHookContext
  • RunResultOutput — use Output
  • RunInput, BaseInput — use Input
  • SessionOutputNamespace — session.output now returns Output directly
  • AdkRunConfig — use RunOptions

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 safeParse failure — feed validation errors back as tool_result instead 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

  • Hook interface — unified observation (onEvent, onStep) + interception (before*/after*)
  • app.run() accepts RunOptions with state and call-site hooks
  • app.test() — deterministic yield/resume testing (replaces scriptedUser())
  • app.simulate() — LLM-powered eval loop (replaces agentUser())
  • app.hook.logging(), app.hook.metrics() — built-in hook factories.
  • app.handler.rest() / app.handler.agui() — protocol handlers with HandlerInput / HandlerConfig
  • SessionStore interface with sessionService(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 during commitSession()

Changed

  • **runner.run() no longer commits sessions** — callers must call sessionService.commitSession() (built-in handlers do this automatically)
  • Middleware / Hooks → single Hook interface; onStream → onEvent
  • Agent.middleware + Agent.hooks → Agent.hooks: Hook[]; AdkConfig.middleware → AdkConfig.hooks
  • composeMiddleware() → composeHooks(); loggingMiddleware() → loggingHook(); cliMiddleware() → cliHook()
  • session.version type: string → number; SessionStoreSnapshot → StoredSession

Removed

  • src/users/ — scriptedUser(), humanUser(), agentUser(), User interface (use app.test() / app.simulate())
  • src/middleware/ — replaced by src/hook/
  • InMemorySessionService, LocalSessionService — use sessionService(new InMemoryStore())
  • Per-scope methods (getUserState, etc.) — use getScopedState / setScopedState
  • HandlerInput.toolInput — use toolInputs array
  • @animahealth/adk/persistence subpath — 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() — transparent import() wrapper so CJS consumers work unchanged
  • extractCurrentThoughtBlock, buildInvocationBlocks → exported from @animahealth/adk/cli instead of main entry
  • react, ink, ink-spinner, ink-text-input — optional peer dependencies for app.cli() consumers

Internal

  • tsup.config.ts now includes an esbuild plugin that externalizes ./cli in the CJS build, so dist/index.js emits require("./cli/index.js") instead of inlining the CLI module tree.
  • scripts/postbuild-cli-cjs-wrapper.cjs generates a CJS→ESM wrapper at dist/cli/index.js that does import('./index.mjs') to load Ink in native ESM context.
  • lodash is force-bundled (noExternal) to avoid Node ESM's "Named export not found" error when importing CJS-only packages.
  • Jest moduleNameMapper mocks added for ink and ink-text-input since they are ESM-only and cannot be require()'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 with name and schema
  • app.* methods for all runnables with automatic type inference
  • app.context.* namespace for context renderers
  • app.tools.* namespace for built-in tools: - webSearch() — web search via Serper API - fetchPage() — fetches web pages, PDFs, and images as markdown - takeScreenshot() — captures webpage screenshots
  • app.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 servers
  • server.* instance API: - tools() — callable FunctionTool[] - toolDefinitions() — raw MCPToolInfo[] - resourceDefinitions() — MCPResourceInfo[] - promptDefinitions() — MCPPromptInfo[] - resource(uri) / prompt(name) — context renderers
  • session.input.* namespace for input operations: - message() — user messages (text and multimodal) - tool() — user input for yielding tools
  • session.output.* namespace for output operations: - text — last assistant text - items — all assistant events - tool() — provide tool results (superseded by session.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.media and tool __media return pattern
  • MediaPart type for image, audio, and document attachments
  • ImageInput and AudioInput helpers: { 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 of output: output(schema, 'key')
  • State API: method-based → property access - Session state is now the default scope: ctx.state.mode (not ctx.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 })
  • UserEvent structure simplified: - text: string — always the text message - media?: MediaPart[] — optional attachments (images, audio)

Removed

  • BaseRunner — use app.run() instead
  • Standalone factories and context renderers — use app.* methods
  • Method-based state API (get, set, delete, toObject)
  • initialState from CreateSessionOptions — use session.state.update()
  • session.addMessage() — use session.input.message() instead
  • session.addToolInput() — use session.input.tool() instead
  • session.addToolResult() — use session.output.tool() (superseded by session.input.result() in 0.3.3)
  • session.append() — use session.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.