@tanstack/ai 0.57.0 → 0.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/dist/esm/activities/chat/agents/define-agent.d.ts +81 -0
  2. package/dist/esm/activities/chat/agents/define-agent.js +34 -0
  3. package/dist/esm/activities/chat/agents/define-agent.js.map +1 -0
  4. package/dist/esm/activities/chat/agents/route.d.ts +53 -0
  5. package/dist/esm/activities/chat/agents/route.js +59 -0
  6. package/dist/esm/activities/chat/agents/route.js.map +1 -0
  7. package/dist/esm/activities/chat/agents/spawn.d.ts +124 -0
  8. package/dist/esm/activities/chat/agents/spawn.js +490 -0
  9. package/dist/esm/activities/chat/agents/spawn.js.map +1 -0
  10. package/dist/esm/activities/chat/agents/turn.d.ts +36 -0
  11. package/dist/esm/activities/chat/agents/turn.js +78 -0
  12. package/dist/esm/activities/chat/agents/turn.js.map +1 -0
  13. package/dist/esm/activities/chat/index.d.ts +13 -3
  14. package/dist/esm/activities/chat/index.js +345 -19
  15. package/dist/esm/activities/chat/index.js.map +1 -1
  16. package/dist/esm/activities/chat/messages.d.ts +7 -1
  17. package/dist/esm/activities/chat/messages.js +94 -18
  18. package/dist/esm/activities/chat/messages.js.map +1 -1
  19. package/dist/esm/activities/chat/middleware/run-store.d.ts +43 -7
  20. package/dist/esm/activities/chat/middleware/run-store.js +8 -1
  21. package/dist/esm/activities/chat/middleware/run-store.js.map +1 -1
  22. package/dist/esm/activities/chat/middleware/types.d.ts +50 -3
  23. package/dist/esm/activities/chat/middleware/types.js.map +1 -1
  24. package/dist/esm/activities/chat/stream/processor.d.ts +46 -0
  25. package/dist/esm/activities/chat/stream/processor.js +280 -7
  26. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  27. package/dist/esm/activities/chat/tools/tool-calls.d.ts +17 -3
  28. package/dist/esm/activities/chat/tools/tool-calls.js +56 -7
  29. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  30. package/dist/esm/activities/generateAudio/index.js +1 -1
  31. package/dist/esm/activities/generateImage/index.js +1 -1
  32. package/dist/esm/activities/generateLiveVideo/index.js +1 -1
  33. package/dist/esm/activities/generateSpeech/index.js +1 -1
  34. package/dist/esm/activities/generateTranscription/index.js +1 -1
  35. package/dist/esm/activities/generateVoice/index.js +1 -1
  36. package/dist/esm/activities/generateWorld/index.js +1 -1
  37. package/dist/esm/activities/index.d.ts +3 -0
  38. package/dist/esm/activities/index.js +5 -3
  39. package/dist/esm/activities/summarize/index.js +1 -1
  40. package/dist/esm/client.d.ts +5 -36
  41. package/dist/esm/client.js +4 -37
  42. package/dist/esm/client.js.map +1 -1
  43. package/dist/esm/index.d.ts +5 -0
  44. package/dist/esm/index.js +7 -4
  45. package/dist/esm/middlewares/content-guard.js.map +1 -1
  46. package/dist/esm/stream-to-response.js +12 -6
  47. package/dist/esm/stream-to-response.js.map +1 -1
  48. package/dist/esm/strip-to-spec-middleware.js +2 -1
  49. package/dist/esm/strip-to-spec-middleware.js.map +1 -1
  50. package/dist/esm/types.d.ts +128 -89
  51. package/dist/esm/utilities/adapter-yield-chunk.d.ts +5 -1
  52. package/dist/esm/utilities/ag-ui-usage.d.ts +9 -9
  53. package/dist/esm/utilities/ag-ui-usage.js +66 -3
  54. package/dist/esm/utilities/ag-ui-usage.js.map +1 -1
  55. package/dist/esm/utilities/ag-ui-wire.js +56 -0
  56. package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
  57. package/dist/esm/utilities/durability-batch.d.ts +8 -0
  58. package/dist/esm/utilities/durability-batch.js +45 -0
  59. package/dist/esm/utilities/durability-batch.js.map +1 -0
  60. package/dist/esm/utilities/normalize-stream-chunk.js +7 -2
  61. package/dist/esm/utilities/normalize-stream-chunk.js.map +1 -1
  62. package/dist/esm/utilities/spec-event-keys.js +13 -8
  63. package/dist/esm/utilities/spec-event-keys.js.map +1 -1
  64. package/dist/esm/utilities/subagent-wire.d.ts +36 -0
  65. package/dist/esm/utilities/subagent-wire.js +131 -0
  66. package/dist/esm/utilities/subagent-wire.js.map +1 -0
  67. package/package.json +4 -4
  68. package/skills/ai-core/adapter-configuration/references/grok-adapter.md +1 -1
  69. package/skills/ai-core/media-generation/SKILL.md +2 -2
  70. package/skills/ai-core/middleware/SKILL.md +7 -4
  71. package/src/activities/chat/agents/define-agent.ts +121 -0
  72. package/src/activities/chat/agents/route.ts +115 -0
  73. package/src/activities/chat/agents/spawn.ts +806 -0
  74. package/src/activities/chat/agents/turn.ts +151 -0
  75. package/src/activities/chat/index.ts +540 -16
  76. package/src/activities/chat/messages.ts +125 -15
  77. package/src/activities/chat/middleware/run-store.ts +56 -7
  78. package/src/activities/chat/middleware/types.ts +55 -2
  79. package/src/activities/chat/stream/processor.ts +428 -8
  80. package/src/activities/chat/tools/tool-calls.ts +92 -16
  81. package/src/activities/index.ts +15 -0
  82. package/src/client.ts +22 -35
  83. package/src/index.ts +23 -0
  84. package/src/middlewares/content-guard.ts +7 -5
  85. package/src/stream-to-response.ts +16 -6
  86. package/src/strip-to-spec-middleware.ts +2 -1
  87. package/src/types.ts +177 -95
  88. package/src/utilities/adapter-yield-chunk.ts +10 -2
  89. package/src/utilities/ag-ui-usage.test.ts +38 -0
  90. package/src/utilities/ag-ui-usage.ts +98 -11
  91. package/src/utilities/ag-ui-wire.ts +74 -0
  92. package/src/utilities/durability-batch.ts +48 -0
  93. package/src/utilities/normalize-stream-chunk.ts +10 -2
  94. package/src/utilities/spec-event-keys.ts +34 -7
  95. package/src/utilities/subagent-wire.ts +184 -0
@@ -1 +1 @@
1
- {"version":3,"file":"run-store.js","names":[],"sources":["../../../../../src/activities/chat/middleware/run-store.ts"],"sourcesContent":["/**\n * Run lifecycle types — the neutral home for what a \"run\" is.\n *\n * Shared by `@tanstack/ai-persistence` (which exposes a `runs` store through\n * `withPersistence`) and `@tanstack/ai-sandbox` (whose run driver records run\n * status). Living in core is what lets one `RunRecord` per run be shared by\n * both, instead of each package keeping its own and disagreeing. Same rationale\n * as `LockStore` (`packages/ai/src/locks.ts`), which is likewise a\n * coordination primitive that core owns so that no consumer package has to.\n */\nimport { createCapability } from './capabilities'\nimport type { TokenUsage } from '../../../types'\n\n/** A terminal run status: no further events will be appended. */\nexport type TerminalRunStatus = 'completed' | 'failed' | 'aborted'\n\n/**\n * Lifecycle status of one run (one agent turn within a conversation).\n *\n * `interrupted` is a human-in-the-loop PAUSE that interrupt-resume continues\n * from — it is deliberately NOT terminal, and must never be conflated with\n * `aborted` (an explicit cancellation).\n *\n * The two are now written by different hooks and cannot be confused:\n *\n * - `'interrupted'` is written ONLY by `withPersistence`'s `onInterrupt`, and\n * carries NO `finishedAt` (a non-terminal status has not finished).\n * - `'aborted'` is written by `withPersistence`'s `onAbort`, and only for an\n * abort that is an explicit cancel or that is ending the run for good.\n * - A mere client disconnect on a run with durable storage wired writes\n * NEITHER: the record stays `'running'` and gains `detachedSince`, because the\n * agent is still running and a later attach can take it over.\n *\n * Intent is never inferred from the abort itself — see `RUN_CANCEL_REASON` and\n * `requestRunCancel` in `../cancel`.\n */\nexport type RunStatus = 'running' | 'interrupted' | TerminalRunStatus\n\n// A Record keyed by the union is exhaustiveness-checked: adding a member to\n// TerminalRunStatus is a compile error here until this map is updated. A\n// `Set<RunStatus>` would silently answer `false` for the new member instead.\nconst TERMINAL: Record<TerminalRunStatus, true> = {\n completed: true,\n failed: true,\n aborted: true,\n}\n\n// Same exhaustiveness trick over the FULL union, for {@link isRunStatus}.\nconst ALL_STATUSES: Record<RunStatus, true> = {\n running: true,\n interrupted: true,\n completed: true,\n failed: true,\n aborted: true,\n}\n\n/**\n * Whether `value` is a {@link RunStatus} — the guard a backend validates a row\n * with at DESERIALIZATION.\n *\n * `RunStatus` is a compile-time claim about a storage column. A row arrives as\n * JSON out of D1, a Durable Object, or Postgres, and nothing in the type system\n * checked what that column actually held, so a `RunStore` implementation should\n * run its row's `status` through this before handing the record on. The readers\n * downstream act DESTRUCTIVELY on the answer — `@tanstack/ai-sandbox`'s journal\n * sweep DELETES the journal of a run it believes terminal — so a row that lies\n * about its status is not a display bug.\n */\nexport function isRunStatus(value: unknown): value is RunStatus {\n return typeof value === 'string' && Object.hasOwn(ALL_STATUSES, value)\n}\n\n/**\n * Whether `status` means no further events will be appended. Narrows, so a\n * caller inside the guard can pass `status` where a {@link TerminalRunStatus}\n * is required without a cast.\n *\n * `Object.hasOwn`, never `in`: `in` walks the prototype chain, so a row whose\n * `status` column held `'toString'` or `'constructor'` would be reported\n * terminal. `status` is TYPED `RunStatus`, but every value reaching here comes\n * off a user-implemented {@link RunStore} and the type is only a claim (see\n * {@link isRunStatus}). A false `true` deletes a live run's journal\n * (`@tanstack/ai-sandbox`'s journal sweep), fails its attach as `'terminal-run'`\n * (`attach-preflight`), and refuses to drive it (`stream-to-response.ts`).\n */\nexport function isTerminalRunStatus(\n status: RunStatus,\n): status is TerminalRunStatus {\n return Object.hasOwn(TERMINAL, status)\n}\n\n/**\n * Why a run failed.\n *\n * A bare message is an LLM provider's prose: it changes between model\n * versions and cannot be branched on. `code` is what a consumer switches over\n * to decide whether to retry, escalate, or surface a specific UI.\n */\nexport interface RunError {\n message: string\n /** Stable, machine-branchable classification, when the provider supplies one. */\n code?: string\n}\n\n/** Durable bookkeeping for a single run. */\nexport interface RunRecord {\n runId: string\n /**\n * Conversation this run belongs to — the `Scope.threadId`.\n *\n * Generation jobs (a one-shot `generate()` with no conversation) must not\n * reuse this record by faking `threadId = requestId`; they need a separate\n * job store. `withGenerationPersistence` currently does exactly that and\n * labels itself a stopgap — do not copy it.\n */\n threadId: string\n status: RunStatus\n startedAt: number\n finishedAt?: number\n error?: RunError\n usage?: TokenUsage\n /**\n * Compound sandbox key this run was bound to, when it ran in a sandbox.\n * Recorded so a future reclaimer can identify the sandbox to tear down\n * without re-deriving the key. Written by `withSandbox`'s detach path\n * (`onAbort` in `@tanstack/ai-sandbox`'s `middleware.ts`) at the same time as\n * `detachedSince`, when a disconnect leaves the run detached rather than\n * destroying the sandbox. A backend must round-trip this field — see\n * `listReclaimable` below for who eventually reads it.\n */\n sandboxKey?: string\n /**\n * Epoch ms when the last viewer detached; absent while someone is attached.\n * Written by `withSandbox`'s detach path (`onAbort` in `@tanstack/ai-sandbox`'s\n * `middleware.ts`) alongside `sandboxKey`, when a disconnect leaves the\n * agent running rather than tearing the sandbox down. A backend must\n * round-trip this field: `listReclaimable` depends on it, and\n * `@tanstack/ai-sandbox`'s `reapDetachedRuns` sweeps the candidates it\n * surfaces (see that method's doc comment).\n */\n detachedSince?: number\n /**\n * Set by an explicit out-of-band cancel, to be distinguished from a mere\n * client disconnect (the two produce an identical TCP close, so intent is not\n * inferable from the disconnect).\n *\n * Written by `requestRunCancel` and read by `wasCancelRequested` (both in\n * `../cancel`). Deliberately NOT a status: recording intent is not the same as\n * the run having stopped, and only the driver knows when it has.\n */\n cancelRequested?: boolean\n /**\n * Monotonic fencing token for the run's driver. Bumped by each host that\n * successfully claims the run (see `withRunClaim` in `@tanstack/ai-sandbox`),\n * so a superseded host can discover it lost by comparing the stored value\n * against the one it holds.\n *\n * A lock alone cannot provide this: it tells the winner it won, but gives a\n * loser nothing to read. Absent on a run that was never claimed.\n */\n driverEpoch?: number\n}\n\n/**\n * Durable store for run lifecycle records.\n *\n * REQUIRED: `createOrResume`, `update`, `get`, `findActiveRun`. Every backend\n * must implement all four — they are what the persistence middleware calls\n * unconditionally. `findActiveRun` is required rather than feature-detected\n * because a backend that has not implemented it is indistinguishable from one\n * whose answer is legitimately `null`, so reconnect would silently do nothing\n * instead of failing at build time. It was optional for exactly one release\n * cycle and cost precisely that.\n *\n * OPTIONAL: `listByThread`, `listReclaimable`. Each serves one higher-level\n * feature (thread history, reclaim reaping) and callers feature-detect them,\n * degrading gracefully when a backend omits them.\n */\nexport interface RunStore {\n /**\n * Create a run record, or return the existing one unchanged if `runId` is\n * already present.\n *\n * INVARIANT (idempotency): an existing record is returned **unchanged** and\n * the passed `threadId`/`startedAt`/`status` are ignored. This is what makes\n * resuming a run safe. `status` defaults to `'running'` on first creation.\n */\n createOrResume: (\n input: Pick<RunRecord, 'runId' | 'threadId' | 'startedAt'> & {\n status?: RunStatus\n },\n ) => Promise<RunRecord>\n /**\n * Patch a record's mutable fields.\n *\n * INVARIANT: updating an unknown `runId` is a **no-op** — it must not throw\n * and must not create a record.\n */\n update: (\n runId: string,\n patch: Partial<\n Pick<\n RunRecord,\n | 'status'\n | 'finishedAt'\n | 'error'\n | 'usage'\n | 'sandboxKey'\n | 'detachedSince'\n | 'cancelRequested'\n | 'driverEpoch'\n >\n >,\n ) => Promise<void>\n /** Current record, or null when unknown. */\n get: (runId: string) => Promise<RunRecord | null>\n /**\n * Every run in a conversation, ascending by `startedAt`. OPTIONAL: only\n * needed to render a thread's past agent activity. Consumers feature-detect.\n */\n listByThread?: (threadId: string) => Promise<Array<RunRecord>>\n /**\n * Runs that may be reclaimed: ALL THREE of `status === 'running'`,\n * `detachedSince` is set, and `detachedSince <= now - ttlMs`. The cutoff is\n * **inclusive** — a run detached at exactly `now - ttlMs` IS reclaimable.\n *\n * OPTIONAL: only needed by a reaper. Consumers feature-detect.\n *\n * `detachedSince` is populated by `withSandbox`'s detach path (see\n * {@link RunRecord.detachedSince}). The sweep over the candidates this\n * surfaces is `@tanstack/ai-sandbox`'s `reapDetachedRuns`: it finalizes a run\n * whose agent already finished, expires one past its TTL, and reclaims the\n * sandbox. That is a function, not a scheduler — the application invokes it\n * (cron, queue, `alarm()`, `waitUntil`) — and a backend that omits this\n * method cannot be reaped at all.\n */\n listReclaimable?: (opts: {\n now: number\n ttlMs: number\n }) => Promise<Array<RunRecord>>\n /**\n * The most recent `'running'` run for `threadId`, or `null` if none is active.\n *\n * REQUIRED. This resolves \"does this thread have a live run to attach to?\"\n * from the STABLE thread id, which is the durable basis for reconnecting a\n * client (a reload, or the same thread opened on another device) — independent\n * of the ephemeral run id, which a single turn may mint several of. When more\n * than one run is `'running'`, the one with the greatest `startedAt` wins.\n *\n * A backend that stubs this to `null` turns reconnect off silently, because\n * `null` is also the correct answer for an idle thread. A backend with no run\n * lifecycle at all should omit the whole `runs` store instead — capability\n * tiers belong at the store level, not the method level.\n */\n findActiveRun: (threadId: string) => Promise<RunRecord | null>\n}\n\n/**\n * Type a {@link RunStore} implementation inline: pass the object and get\n * autocomplete plus contract checking with no separate annotation. Mirrors\n * `defineLock` / `defineSandboxInstanceStore`.\n *\n * The generic return preserves the argument's own type, so an optional method\n * the implementation actually provides stays known-present on the result\n * instead of collapsing back to `| undefined` on the interface.\n */\nexport function defineRunStore<const T extends RunStore>(store: T): T {\n return store\n}\n\n/**\n * Whether the current run can be DETACHED rather than destroyed when its client\n * disconnects — `true` only when some middleware has both a {@link RunStore} and\n * a durable event log wired (`withSandbox`'s `runs` + `durability.adapter`).\n *\n * Lives in core for the same reason `LockStore` does: it is a coordination fact\n * that two consumer packages must agree on, and neither may depend on the other.\n * `@tanstack/ai-sandbox` provides it; `@tanstack/ai-persistence` reads it to\n * decide whether an abort is terminal (`'aborted'`) or a detach (write nothing).\n * A persistence → sandbox import would be a layering inversion.\n *\n * Consumers read it with `{ optional: true }`: absent means \"not detachable\",\n * which is every app that has not wired durability.\n *\n * Typed `true`, not `boolean`: ABSENCE is the negative, so a published `false`\n * has no meaning — and a consumer that tests PRESENCE rather than the value\n * would read one as \"detachable\". Narrowing the payload makes that\n * unrepresentable instead of merely undocumented.\n */\nexport const DetachableRunCapability =\n createCapability<true>()('detachable-run')\n\n/**\n * Destructured accessors: `getDetachableRun(ctx, { optional: true })` /\n * `provideDetachableRun(ctx, true)`.\n */\nexport const [getDetachableRun, provideDetachableRun] = DetachableRunCapability\n\n/**\n * Whether this run's teardown DID detach — the disconnect was survived, the\n * agent is still working, and a later attach can take the run over.\n *\n * The past-tense counterpart of {@link DetachableRunCapability}, and the two must\n * not be confused:\n *\n * - **detachABLE** is published at `setup`, and only says a disconnect *may* be\n * survived (a `RunStore` and a durable log are wired).\n * - **detachED** is published on the ABORT path, by the middleware that actually\n * makes the call — `withSandbox`'s `onAbort`, which is the only actor that has\n * resolved BOTH out-of-band cancel bands (`AbortInfo.cancelRequested` and\n * `wasCancelRequested` on the record) and `detachOnDisconnect`. An explicit\n * cancel, a non-detachable disconnect, an error, and a normal finish all leave\n * it unpublished.\n *\n * Its consumer is the durable DELIVERY sink in `stream-to-response.ts`: a\n * detached run's log must stay OPEN and un-terminalized so the takeover can\n * continue it (see `wasRunDetached` in `../../../delivery-detach`). Reading it\n * is safe and race-free only because a `for await` over the chat stream awaits\n * the generator's `return()` — and therefore the whole `onAbort` chain — before\n * the sink's own `finally` runs.\n *\n * Read with `{ optional: true }`: absent means \"not detached\", which is every\n * other exit path and every app that has not wired durability.\n *\n * Typed `true`, not `boolean`, for the same reason as\n * {@link DetachableRunCapability}: absence is the only negative, so publishing\n * `false` must not be representable.\n */\nexport const RunDetachedCapability = createCapability<true>()('run-detached')\n\n/**\n * Destructured accessors: `getRunDetached(ctx, { optional: true })` /\n * `provideRunDetached(ctx, true)`.\n */\nexport const [getRunDetached, provideRunDetached] = RunDetachedCapability\n\n/** In-memory {@link RunStore}. Single process only. */\nexport class InMemoryRunStore implements RunStore {\n private readonly runs = new Map<string, RunRecord>()\n\n createOrResume(\n input: Pick<RunRecord, 'runId' | 'threadId' | 'startedAt'> & {\n status?: RunStatus\n },\n ): Promise<RunRecord> {\n const existing = this.runs.get(input.runId)\n if (existing) return Promise.resolve(existing)\n const record: RunRecord = {\n runId: input.runId,\n threadId: input.threadId,\n status: input.status ?? 'running',\n startedAt: input.startedAt,\n }\n this.runs.set(record.runId, record)\n return Promise.resolve(record)\n }\n\n update(\n runId: string,\n patch: Partial<\n Pick<\n RunRecord,\n | 'status'\n | 'finishedAt'\n | 'error'\n | 'usage'\n | 'sandboxKey'\n | 'detachedSince'\n | 'cancelRequested'\n | 'driverEpoch'\n >\n >,\n ): Promise<void> {\n const existing = this.runs.get(runId)\n if (existing) this.runs.set(runId, { ...existing, ...patch })\n return Promise.resolve()\n }\n\n get(runId: string): Promise<RunRecord | null> {\n return Promise.resolve(this.runs.get(runId) ?? null)\n }\n\n listByThread(threadId: string): Promise<Array<RunRecord>> {\n const matching = [...this.runs.values()]\n .filter((run) => run.threadId === threadId)\n .sort((a, b) => a.startedAt - b.startedAt)\n return Promise.resolve(matching)\n }\n\n listReclaimable(opts: {\n now: number\n ttlMs: number\n }): Promise<Array<RunRecord>> {\n const cutoff = opts.now - opts.ttlMs\n const matching = [...this.runs.values()].filter(\n (run) =>\n run.status === 'running' &&\n run.detachedSince !== undefined &&\n run.detachedSince <= cutoff,\n )\n return Promise.resolve(matching)\n }\n\n findActiveRun(threadId: string): Promise<RunRecord | null> {\n let active: RunRecord | null = null\n for (const run of this.runs.values()) {\n if (run.threadId !== threadId || run.status !== 'running') continue\n if (active === null || run.startedAt > active.startedAt) active = run\n }\n return Promise.resolve(active)\n }\n}\n"],"mappings":";;;;;;;;;;;;AAyCA,IAAM,WAA4C;CAChD,WAAW;CACX,QAAQ;CACR,SAAS;AACX;AAGA,IAAM,eAAwC;CAC5C,SAAS;CACT,aAAa;CACb,WAAW;CACX,QAAQ;CACR,SAAS;AACX;;;;;;;;;;;;;AAcA,SAAgB,YAAY,OAAoC;CAC9D,OAAO,OAAO,UAAU,YAAY,OAAO,OAAO,cAAc,KAAK;AACvE;;;;;;;;;;;;;;AAeA,SAAgB,oBACd,QAC6B;CAC7B,OAAO,OAAO,OAAO,UAAU,MAAM;AACvC;;;;;;;;;;AAiLA,SAAgB,eAAyC,OAAa;CACpE,OAAO;AACT;;;;;;;;;;;;;;;;;;;;AAqBA,IAAa,0BACX,iBAAuB,CAAC,CAAC,gBAAgB;;;;;AAM3C,IAAa,CAAC,kBAAkB,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCxD,IAAa,wBAAwB,iBAAuB,CAAC,CAAC,cAAc;;;;;AAM5E,IAAa,CAAC,gBAAgB,sBAAsB;;AAGpD,IAAa,mBAAb,MAAkD;CAChD,uBAAwB,IAAI,IAAuB;CAEnD,eACE,OAGoB;EACpB,MAAM,WAAW,KAAK,KAAK,IAAI,MAAM,KAAK;EAC1C,IAAI,UAAU,OAAO,QAAQ,QAAQ,QAAQ;EAC7C,MAAM,SAAoB;GACxB,OAAO,MAAM;GACb,UAAU,MAAM;GAChB,QAAQ,MAAM,UAAU;GACxB,WAAW,MAAM;EACnB;EACA,KAAK,KAAK,IAAI,OAAO,OAAO,MAAM;EAClC,OAAO,QAAQ,QAAQ,MAAM;CAC/B;CAEA,OACE,OACA,OAae;EACf,MAAM,WAAW,KAAK,KAAK,IAAI,KAAK;EACpC,IAAI,UAAU,KAAK,KAAK,IAAI,OAAO;GAAE,GAAG;GAAU,GAAG;EAAM,CAAC;EAC5D,OAAO,QAAQ,QAAQ;CACzB;CAEA,IAAI,OAA0C;EAC5C,OAAO,QAAQ,QAAQ,KAAK,KAAK,IAAI,KAAK,KAAK,IAAI;CACrD;CAEA,aAAa,UAA6C;EACxD,MAAM,WAAW,CAAC,GAAG,KAAK,KAAK,OAAO,CAAC,CAAC,CACrC,QAAQ,QAAQ,IAAI,aAAa,QAAQ,CAAC,CAC1C,MAAM,GAAG,MAAM,EAAE,YAAY,EAAE,SAAS;EAC3C,OAAO,QAAQ,QAAQ,QAAQ;CACjC;CAEA,gBAAgB,MAGc;EAC5B,MAAM,SAAS,KAAK,MAAM,KAAK;EAC/B,MAAM,WAAW,CAAC,GAAG,KAAK,KAAK,OAAO,CAAC,CAAC,CAAC,QACtC,QACC,IAAI,WAAW,aACf,IAAI,kBAAkB,KAAA,KACtB,IAAI,iBAAiB,MACzB;EACA,OAAO,QAAQ,QAAQ,QAAQ;CACjC;CAEA,cAAc,UAA6C;EACzD,IAAI,SAA2B;EAC/B,KAAK,MAAM,OAAO,KAAK,KAAK,OAAO,GAAG;GACpC,IAAI,IAAI,aAAa,YAAY,IAAI,WAAW,WAAW;GAC3D,IAAI,WAAW,QAAQ,IAAI,YAAY,OAAO,WAAW,SAAS;EACpE;EACA,OAAO,QAAQ,QAAQ,MAAM;CAC/B;AACF"}
1
+ {"version":3,"file":"run-store.js","names":[],"sources":["../../../../../src/activities/chat/middleware/run-store.ts"],"sourcesContent":["/**\n * Run lifecycle types — the neutral home for what a \"run\" is.\n *\n * Shared by `@tanstack/ai-persistence` (which exposes a `runs` store through\n * `withPersistence`) and `@tanstack/ai-sandbox` (whose run driver records run\n * status). Living in core is what lets one `RunRecord` per run be shared by\n * both, instead of each package keeping its own and disagreeing. Same rationale\n * as `LockStore` (`packages/ai/src/locks.ts`), which is likewise a\n * coordination primitive that core owns so that no consumer package has to.\n */\nimport { createCapability } from './capabilities'\nimport type { TokenUsage } from '../../../types'\n\n/** A terminal run status: no further events will be appended. */\nexport type TerminalRunStatus = 'completed' | 'failed' | 'aborted'\n\n/**\n * Lifecycle status of one run (one agent turn within a conversation).\n *\n * `interrupted` is a human-in-the-loop PAUSE that interrupt-resume continues\n * from — it is deliberately NOT terminal, and must never be conflated with\n * `aborted` (an explicit cancellation).\n *\n * The two are now written by different hooks and cannot be confused:\n *\n * - `'interrupted'` is written ONLY by `withPersistence`'s `onInterrupt`, and\n * carries NO `finishedAt` (a non-terminal status has not finished).\n * - `'aborted'` is written by `withPersistence`'s `onAbort`, and only for an\n * abort that is an explicit cancel or that is ending the run for good.\n * - A mere client disconnect on a run with durable storage wired writes\n * NEITHER: the record stays `'running'` and gains `detachedSince`, because the\n * agent is still running and a later attach can take it over.\n *\n * Intent is never inferred from the abort itself — see `RUN_CANCEL_REASON` and\n * `requestRunCancel` in `../cancel`.\n */\nexport type RunStatus = 'running' | 'interrupted' | TerminalRunStatus\n\n// A Record keyed by the union is exhaustiveness-checked: adding a member to\n// TerminalRunStatus is a compile error here until this map is updated. A\n// `Set<RunStatus>` would silently answer `false` for the new member instead.\nconst TERMINAL: Record<TerminalRunStatus, true> = {\n completed: true,\n failed: true,\n aborted: true,\n}\n\n// Same exhaustiveness trick over the FULL union, for {@link isRunStatus}.\nconst ALL_STATUSES: Record<RunStatus, true> = {\n running: true,\n interrupted: true,\n completed: true,\n failed: true,\n aborted: true,\n}\n\n/**\n * Whether `value` is a {@link RunStatus} — the guard a backend validates a row\n * with at DESERIALIZATION.\n *\n * `RunStatus` is a compile-time claim about a storage column. A row arrives as\n * JSON out of D1, a Durable Object, or Postgres, and nothing in the type system\n * checked what that column actually held, so a `RunStore` implementation should\n * run its row's `status` through this before handing the record on. The readers\n * downstream act DESTRUCTIVELY on the answer — `@tanstack/ai-sandbox`'s journal\n * sweep DELETES the journal of a run it believes terminal — so a row that lies\n * about its status is not a display bug.\n */\nexport function isRunStatus(value: unknown): value is RunStatus {\n return typeof value === 'string' && Object.hasOwn(ALL_STATUSES, value)\n}\n\n/**\n * Whether `status` means no further events will be appended. Narrows, so a\n * caller inside the guard can pass `status` where a {@link TerminalRunStatus}\n * is required without a cast.\n *\n * `Object.hasOwn`, never `in`: `in` walks the prototype chain, so a row whose\n * `status` column held `'toString'` or `'constructor'` would be reported\n * terminal. `status` is TYPED `RunStatus`, but every value reaching here comes\n * off a user-implemented {@link RunStore} and the type is only a claim (see\n * {@link isRunStatus}). A false `true` deletes a live run's journal\n * (`@tanstack/ai-sandbox`'s journal sweep), fails its attach as `'terminal-run'`\n * (`attach-preflight`), and refuses to drive it (`stream-to-response.ts`).\n */\nexport function isTerminalRunStatus(\n status: RunStatus,\n): status is TerminalRunStatus {\n return Object.hasOwn(TERMINAL, status)\n}\n\n/**\n * Why a run failed.\n *\n * A bare message is an LLM provider's prose: it changes between model\n * versions and cannot be branched on. `code` is what a consumer switches over\n * to decide whether to retry, escalate, or surface a specific UI.\n */\nexport interface RunError {\n message: string\n /** Stable, machine-branchable classification, when the provider supplies one. */\n code?: string\n}\n\n/** Durable bookkeeping for a single run. */\nexport interface RunRecord {\n runId: string\n /**\n * Conversation this run belongs to — the `Scope.threadId`.\n *\n * Generation jobs (a one-shot `generate()` with no conversation) must not\n * reuse this record by faking `threadId = requestId`; they need a separate\n * job store. `withGenerationPersistence` currently does exactly that and\n * labels itself a stopgap — do not copy it.\n *\n * A subagent child record stores `subagent:<subagentRunId>` here, the key of\n * its own transcript, so `findActiveRun` and `listByThread` on the\n * conversation never return children. Use `listByParentRun`.\n */\n threadId: string\n /**\n * Parent chat run that started this child, when this record is a subagent.\n * Absent on the parent run itself.\n */\n parentRunId?: string\n /**\n * The child's AG-UI subagentRunId, the id on its `SUBAGENT_*` chunks and on\n * every chunk it streams. On a child record this equals `runId`. Absent on\n * the parent run.\n */\n subagentRunId?: string\n /** Agent name (`researcher`, `writer`) when this record is a subagent. */\n name?: string\n status: RunStatus\n startedAt: number\n finishedAt?: number\n error?: RunError\n usage?: TokenUsage\n /**\n * Compound sandbox key this run was bound to, when it ran in a sandbox.\n * Recorded so a future reclaimer can identify the sandbox to tear down\n * without re-deriving the key. Written by `withSandbox`'s detach path\n * (`onAbort` in `@tanstack/ai-sandbox`'s `middleware.ts`) at the same time as\n * `detachedSince`, when a disconnect leaves the run detached rather than\n * destroying the sandbox. A backend must round-trip this field — see\n * `listReclaimable` below for who eventually reads it.\n */\n sandboxKey?: string\n /**\n * Epoch ms when the last viewer detached; absent while someone is attached.\n * Written by `withSandbox`'s detach path (`onAbort` in `@tanstack/ai-sandbox`'s\n * `middleware.ts`) alongside `sandboxKey`, when a disconnect leaves the\n * agent running rather than tearing the sandbox down. A backend must\n * round-trip this field: `listReclaimable` depends on it, and\n * `@tanstack/ai-sandbox`'s `reapDetachedRuns` sweeps the candidates it\n * surfaces (see that method's doc comment).\n */\n detachedSince?: number\n /**\n * Set by an explicit out-of-band cancel, to be distinguished from a mere\n * client disconnect (the two produce an identical TCP close, so intent is not\n * inferable from the disconnect).\n *\n * Written by `requestRunCancel` and read by `wasCancelRequested` (both in\n * `../cancel`). Deliberately NOT a status: recording intent is not the same as\n * the run having stopped, and only the driver knows when it has.\n */\n cancelRequested?: boolean\n /**\n * Monotonic fencing token for the run's driver. Bumped by each host that\n * successfully claims the run (see `withRunClaim` in `@tanstack/ai-sandbox`),\n * so a superseded host can discover it lost by comparing the stored value\n * against the one it holds.\n *\n * A lock alone cannot provide this: it tells the winner it won, but gives a\n * loser nothing to read. Absent on a run that was never claimed.\n */\n driverEpoch?: number\n}\n\n/**\n * Durable store for run lifecycle records.\n *\n * REQUIRED: `createOrResume`, `update`, `get`, `findActiveRun`. Every backend\n * must implement all four — they are what the persistence middleware calls\n * unconditionally. `findActiveRun` is required rather than feature-detected\n * because a backend that has not implemented it is indistinguishable from one\n * whose answer is legitimately `null`, so reconnect would silently do nothing\n * instead of failing at build time. It was optional for exactly one release\n * cycle and cost precisely that.\n *\n * OPTIONAL: `listByThread`, `listByParentRun`, `listReclaimable`. Each serves\n * one higher-level feature (thread history, subagent card reload, reclaim\n * reaping) and callers feature-detect them, degrading when a backend omits\n * them.\n */\nexport interface RunStore {\n /**\n * Create a run record, or return the existing one unchanged if `runId` is\n * already present.\n *\n * INVARIANT (idempotency): an existing record is returned **unchanged** and\n * the passed `threadId`, `startedAt`, `status`, `parentRunId`,\n * `subagentRunId`, and `name` are ignored. This is what makes resuming a\n * run safe. `status` defaults to `'running'` on first creation. The three\n * link fields are copied only on the first insert.\n */\n createOrResume: (\n input: Pick<RunRecord, 'runId' | 'threadId' | 'startedAt'> & {\n status?: RunStatus\n parentRunId?: string\n subagentRunId?: string\n name?: string\n },\n ) => Promise<RunRecord>\n /**\n * Patch a record's mutable fields.\n *\n * INVARIANT: updating an unknown `runId` is a **no-op** — it must not throw\n * and must not create a record.\n */\n update: (\n runId: string,\n patch: Partial<\n Pick<\n RunRecord,\n | 'status'\n | 'finishedAt'\n | 'error'\n | 'usage'\n | 'sandboxKey'\n | 'detachedSince'\n | 'cancelRequested'\n | 'driverEpoch'\n >\n >,\n ) => Promise<void>\n /** Current record, or null when unknown. */\n get: (runId: string) => Promise<RunRecord | null>\n /**\n * Every run in a conversation, ascending by `startedAt`. OPTIONAL.\n * `reconstructChat` calls it to find the parent runs of children that a\n * tool call started. Without it those cards stay absent on reload.\n * Consumers feature-detect.\n */\n listByThread?: (threadId: string) => Promise<Array<RunRecord>>\n /**\n * Child runs started by `parentRunId`, ascending by `startedAt`.\n * OPTIONAL. `reconstructChat` uses this to put subagent cards back\n * on the parent assistant message. A store that omits it reloads the\n * text and not the cards.\n */\n listByParentRun?: (parentRunId: string) => Promise<Array<RunRecord>>\n /**\n * Runs that may be reclaimed: ALL THREE of `status === 'running'`,\n * `detachedSince` is set, and `detachedSince <= now - ttlMs`. The cutoff is\n * **inclusive** — a run detached at exactly `now - ttlMs` IS reclaimable.\n *\n * OPTIONAL: only needed by a reaper. Consumers feature-detect.\n *\n * `detachedSince` is populated by `withSandbox`'s detach path (see\n * {@link RunRecord.detachedSince}). The sweep over the candidates this\n * surfaces is `@tanstack/ai-sandbox`'s `reapDetachedRuns`: it finalizes a run\n * whose agent already finished, expires one past its TTL, and reclaims the\n * sandbox. That is a function, not a scheduler — the application invokes it\n * (cron, queue, `alarm()`, `waitUntil`) — and a backend that omits this\n * method cannot be reaped at all.\n */\n listReclaimable?: (opts: {\n now: number\n ttlMs: number\n }) => Promise<Array<RunRecord>>\n /**\n * The most recent `'running'` run for `threadId`, or `null` if none is active.\n *\n * REQUIRED. This resolves \"does this thread have a live run to attach to?\"\n * from the STABLE thread id, which is the durable basis for reconnecting a\n * client (a reload, or the same thread opened on another device) — independent\n * of the ephemeral run id, which a single turn may mint several of. When more\n * than one run is `'running'`, the one with the greatest `startedAt` wins.\n *\n * A backend that stubs this to `null` turns reconnect off silently, because\n * `null` is also the correct answer for an idle thread. A backend with no run\n * lifecycle at all should omit the whole `runs` store instead — capability\n * tiers belong at the store level, not the method level.\n */\n findActiveRun: (threadId: string) => Promise<RunRecord | null>\n}\n\n/**\n * Type a {@link RunStore} implementation inline: pass the object and get\n * autocomplete plus contract checking with no separate annotation. Mirrors\n * `defineLock` / `defineSandboxInstanceStore`.\n *\n * The generic return preserves the argument's own type, so an optional method\n * the implementation actually provides stays known-present on the result\n * instead of collapsing back to `| undefined` on the interface.\n */\nexport function defineRunStore<const T extends RunStore>(store: T): T {\n return store\n}\n\n/**\n * Whether the current run can be DETACHED rather than destroyed when its client\n * disconnects — `true` only when some middleware has both a {@link RunStore} and\n * a durable event log wired (`withSandbox`'s `runs` + `durability.adapter`).\n *\n * Lives in core for the same reason `LockStore` does: it is a coordination fact\n * that two consumer packages must agree on, and neither may depend on the other.\n * `@tanstack/ai-sandbox` provides it; `@tanstack/ai-persistence` reads it to\n * decide whether an abort is terminal (`'aborted'`) or a detach (write nothing).\n * A persistence → sandbox import would be a layering inversion.\n *\n * Consumers read it with `{ optional: true }`: absent means \"not detachable\",\n * which is every app that has not wired durability.\n *\n * Typed `true`, not `boolean`: ABSENCE is the negative, so a published `false`\n * has no meaning — and a consumer that tests PRESENCE rather than the value\n * would read one as \"detachable\". Narrowing the payload makes that\n * unrepresentable instead of merely undocumented.\n */\nexport const DetachableRunCapability =\n createCapability<true>()('detachable-run')\n\n/**\n * Destructured accessors: `getDetachableRun(ctx, { optional: true })` /\n * `provideDetachableRun(ctx, true)`.\n */\nexport const [getDetachableRun, provideDetachableRun] = DetachableRunCapability\n\n/**\n * Whether this run's teardown DID detach — the disconnect was survived, the\n * agent is still working, and a later attach can take the run over.\n *\n * The past-tense counterpart of {@link DetachableRunCapability}, and the two must\n * not be confused:\n *\n * - **detachABLE** is published at `setup`, and only says a disconnect *may* be\n * survived (a `RunStore` and a durable log are wired).\n * - **detachED** is published on the ABORT path, by the middleware that actually\n * makes the call — `withSandbox`'s `onAbort`, which is the only actor that has\n * resolved BOTH out-of-band cancel bands (`AbortInfo.cancelRequested` and\n * `wasCancelRequested` on the record) and `detachOnDisconnect`. An explicit\n * cancel, a non-detachable disconnect, an error, and a normal finish all leave\n * it unpublished.\n *\n * Its consumer is the durable DELIVERY sink in `stream-to-response.ts`: a\n * detached run's log must stay OPEN and un-terminalized so the takeover can\n * continue it (see `wasRunDetached` in `../../../delivery-detach`). Reading it\n * is safe and race-free only because a `for await` over the chat stream awaits\n * the generator's `return()` — and therefore the whole `onAbort` chain — before\n * the sink's own `finally` runs.\n *\n * Read with `{ optional: true }`: absent means \"not detached\", which is every\n * other exit path and every app that has not wired durability.\n *\n * Typed `true`, not `boolean`, for the same reason as\n * {@link DetachableRunCapability}: absence is the only negative, so publishing\n * `false` must not be representable.\n */\nexport const RunDetachedCapability = createCapability<true>()('run-detached')\n\n/**\n * Destructured accessors: `getRunDetached(ctx, { optional: true })` /\n * `provideRunDetached(ctx, true)`.\n */\nexport const [getRunDetached, provideRunDetached] = RunDetachedCapability\n\n/** In-memory {@link RunStore}. Single process only. */\nexport class InMemoryRunStore implements RunStore {\n private readonly runs = new Map<string, RunRecord>()\n\n createOrResume(\n input: Pick<RunRecord, 'runId' | 'threadId' | 'startedAt'> & {\n status?: RunStatus\n parentRunId?: string\n subagentRunId?: string\n name?: string\n },\n ): Promise<RunRecord> {\n const existing = this.runs.get(input.runId)\n if (existing) return Promise.resolve(existing)\n const record: RunRecord = {\n runId: input.runId,\n threadId: input.threadId,\n status: input.status ?? 'running',\n startedAt: input.startedAt,\n ...(input.parentRunId !== undefined\n ? { parentRunId: input.parentRunId }\n : {}),\n ...(input.subagentRunId !== undefined\n ? { subagentRunId: input.subagentRunId }\n : {}),\n ...(input.name !== undefined ? { name: input.name } : {}),\n }\n this.runs.set(record.runId, record)\n return Promise.resolve(record)\n }\n\n update(\n runId: string,\n patch: Partial<\n Pick<\n RunRecord,\n | 'status'\n | 'finishedAt'\n | 'error'\n | 'usage'\n | 'sandboxKey'\n | 'detachedSince'\n | 'cancelRequested'\n | 'driverEpoch'\n >\n >,\n ): Promise<void> {\n const existing = this.runs.get(runId)\n if (existing) this.runs.set(runId, { ...existing, ...patch })\n return Promise.resolve()\n }\n\n get(runId: string): Promise<RunRecord | null> {\n return Promise.resolve(this.runs.get(runId) ?? null)\n }\n\n listByThread(threadId: string): Promise<Array<RunRecord>> {\n const matching = [...this.runs.values()]\n .filter((run) => run.threadId === threadId)\n .sort((a, b) => a.startedAt - b.startedAt)\n return Promise.resolve(matching)\n }\n\n listByParentRun(parentRunId: string): Promise<Array<RunRecord>> {\n const matching = [...this.runs.values()]\n .filter((run) => run.parentRunId === parentRunId)\n .sort((a, b) => a.startedAt - b.startedAt)\n return Promise.resolve(matching)\n }\n\n listReclaimable(opts: {\n now: number\n ttlMs: number\n }): Promise<Array<RunRecord>> {\n const cutoff = opts.now - opts.ttlMs\n const matching = [...this.runs.values()].filter(\n (run) =>\n run.status === 'running' &&\n run.detachedSince !== undefined &&\n run.detachedSince <= cutoff,\n )\n return Promise.resolve(matching)\n }\n\n findActiveRun(threadId: string): Promise<RunRecord | null> {\n let active: RunRecord | null = null\n for (const run of this.runs.values()) {\n if (run.threadId !== threadId || run.status !== 'running') continue\n if (active === null || run.startedAt > active.startedAt) active = run\n }\n return Promise.resolve(active)\n }\n}\n"],"mappings":";;;;;;;;;;;;AAyCA,IAAM,WAA4C;CAChD,WAAW;CACX,QAAQ;CACR,SAAS;AACX;AAGA,IAAM,eAAwC;CAC5C,SAAS;CACT,aAAa;CACb,WAAW;CACX,QAAQ;CACR,SAAS;AACX;;;;;;;;;;;;;AAcA,SAAgB,YAAY,OAAoC;CAC9D,OAAO,OAAO,UAAU,YAAY,OAAO,OAAO,cAAc,KAAK;AACvE;;;;;;;;;;;;;;AAeA,SAAgB,oBACd,QAC6B;CAC7B,OAAO,OAAO,OAAO,UAAU,MAAM;AACvC;;;;;;;;;;AAiNA,SAAgB,eAAyC,OAAa;CACpE,OAAO;AACT;;;;;;;;;;;;;;;;;;;;AAqBA,IAAa,0BACX,iBAAuB,CAAC,CAAC,gBAAgB;;;;;AAM3C,IAAa,CAAC,kBAAkB,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCxD,IAAa,wBAAwB,iBAAuB,CAAC,CAAC,cAAc;;;;;AAM5E,IAAa,CAAC,gBAAgB,sBAAsB;;AAGpD,IAAa,mBAAb,MAAkD;CAChD,uBAAwB,IAAI,IAAuB;CAEnD,eACE,OAMoB;EACpB,MAAM,WAAW,KAAK,KAAK,IAAI,MAAM,KAAK;EAC1C,IAAI,UAAU,OAAO,QAAQ,QAAQ,QAAQ;EAC7C,MAAM,SAAoB;GACxB,OAAO,MAAM;GACb,UAAU,MAAM;GAChB,QAAQ,MAAM,UAAU;GACxB,WAAW,MAAM;GACjB,GAAI,MAAM,gBAAgB,KAAA,IACtB,EAAE,aAAa,MAAM,YAAY,IACjC,CAAC;GACL,GAAI,MAAM,kBAAkB,KAAA,IACxB,EAAE,eAAe,MAAM,cAAc,IACrC,CAAC;GACL,GAAI,MAAM,SAAS,KAAA,IAAY,EAAE,MAAM,MAAM,KAAK,IAAI,CAAC;EACzD;EACA,KAAK,KAAK,IAAI,OAAO,OAAO,MAAM;EAClC,OAAO,QAAQ,QAAQ,MAAM;CAC/B;CAEA,OACE,OACA,OAae;EACf,MAAM,WAAW,KAAK,KAAK,IAAI,KAAK;EACpC,IAAI,UAAU,KAAK,KAAK,IAAI,OAAO;GAAE,GAAG;GAAU,GAAG;EAAM,CAAC;EAC5D,OAAO,QAAQ,QAAQ;CACzB;CAEA,IAAI,OAA0C;EAC5C,OAAO,QAAQ,QAAQ,KAAK,KAAK,IAAI,KAAK,KAAK,IAAI;CACrD;CAEA,aAAa,UAA6C;EACxD,MAAM,WAAW,CAAC,GAAG,KAAK,KAAK,OAAO,CAAC,CAAC,CACrC,QAAQ,QAAQ,IAAI,aAAa,QAAQ,CAAC,CAC1C,MAAM,GAAG,MAAM,EAAE,YAAY,EAAE,SAAS;EAC3C,OAAO,QAAQ,QAAQ,QAAQ;CACjC;CAEA,gBAAgB,aAAgD;EAC9D,MAAM,WAAW,CAAC,GAAG,KAAK,KAAK,OAAO,CAAC,CAAC,CACrC,QAAQ,QAAQ,IAAI,gBAAgB,WAAW,CAAC,CAChD,MAAM,GAAG,MAAM,EAAE,YAAY,EAAE,SAAS;EAC3C,OAAO,QAAQ,QAAQ,QAAQ;CACjC;CAEA,gBAAgB,MAGc;EAC5B,MAAM,SAAS,KAAK,MAAM,KAAK;EAC/B,MAAM,WAAW,CAAC,GAAG,KAAK,KAAK,OAAO,CAAC,CAAC,CAAC,QACtC,QACC,IAAI,WAAW,aACf,IAAI,kBAAkB,KAAA,KACtB,IAAI,iBAAiB,MACzB;EACA,OAAO,QAAQ,QAAQ,QAAQ;CACjC;CAEA,cAAc,UAA6C;EACzD,IAAI,SAA2B;EAC/B,KAAK,MAAM,OAAO,KAAK,KAAK,OAAO,GAAG;GACpC,IAAI,IAAI,aAAa,YAAY,IAAI,WAAW,WAAW;GAC3D,IAAI,WAAW,QAAQ,IAAI,YAAY,OAAO,WAAW,SAAS;EACpE;EACA,OAAO,QAAQ,QAAQ,MAAM;CAC/B;AACF"}
@@ -1,5 +1,5 @@
1
1
  import { StandardJSONSchemaV1, StandardSchemaV1 } from '@standard-schema/spec';
2
- import { AgentLoopState, JSONSchema, ModelMessage, RunAgentResumeItem, StreamChunk, TokenUsage, Tool, ToolCall } from '../../../types.js';
2
+ import { AgentLoopState, EmitCustomEventOptions, Interrupt, JSONSchema, ModelMessage, UIMessage, RunAgentResumeItem, StreamChunk, TokenUsage, Tool, ToolCall } from '../../../types.js';
3
3
  import { SystemPrompt } from '../../../system-prompts.js';
4
4
  import { ToolApprovalResolution } from '../../../interrupts.js';
5
5
  import { GenericInterruptRequest, InterruptDefinition } from '../../../interrupt-definition.js';
@@ -90,6 +90,11 @@ export interface ChatMiddlewareContext<TContext = unknown> {
90
90
  runId: string;
91
91
  /** Interrupted or parent run correlated with this continuation. */
92
92
  parentRunId?: string;
93
+ /**
94
+ * Set when this run is a subagent. The id on the child's `SUBAGENT_STARTED`
95
+ * and on every chunk it streams. Absent on a top-level run.
96
+ */
97
+ subagentRunId?: string;
93
98
  /**
94
99
  * AG-UI thread identifier — a stable per-conversation ID used to
95
100
  * correlate client and server devtools events. Resolves to the
@@ -116,9 +121,10 @@ export interface ChatMiddlewareContext<TContext = unknown> {
116
121
  /**
117
122
  * Push a `CUSTOM` chunk onto the chat stream immediately.
118
123
  * The engine yields it as soon as it can (including while `onConfig`
119
- * is still awaiting work such as a summarize call).
124
+ * is still awaiting work such as a summarize call). Durability then
125
+ * flushes the event on its own, unless you pass `{ batch: true }`.
120
126
  */
121
- emitCustomEvent: (name: string, value: Record<string, any>) => void;
127
+ emitCustomEvent: (name: string, value: Record<string, any>, options?: EmitCustomEventOptions) => void;
122
128
  /** Runtime context provided by chat() options */
123
129
  context: TContext;
124
130
  /**
@@ -385,6 +391,42 @@ export interface ErrorInfo {
385
391
  /** Duration until error in milliseconds */
386
392
  duration: number;
387
393
  }
394
+ /**
395
+ * Saves subagent runs while a router owns the turn.
396
+ * `withPersistence` sets this. `chat()` calls it. Apps do not.
397
+ */
398
+ export interface RoutedSubagentPersistence {
399
+ start: (input: {
400
+ threadId: string;
401
+ runId: string;
402
+ messages: ReadonlyArray<UIMessage | ModelMessage>;
403
+ /**
404
+ * The run's resume entries: answers to earlier child interrupts, plus any
405
+ * the parent answers itself.
406
+ */
407
+ resume?: ReadonlyArray<RunAgentResumeItem>;
408
+ }) => Promise<void>;
409
+ chunk: (input: {
410
+ threadId: string;
411
+ runId: string;
412
+ chunk: StreamChunk;
413
+ }) => Promise<void>;
414
+ finish: (input: {
415
+ threadId: string;
416
+ runId: string;
417
+ }) => Promise<void>;
418
+ /** The run stopped because a child waits for outside input. */
419
+ suspend?: (input: {
420
+ threadId: string;
421
+ runId: string;
422
+ interrupts: ReadonlyArray<Interrupt>;
423
+ }) => Promise<void>;
424
+ abort: (input: {
425
+ threadId: string;
426
+ runId: string;
427
+ error?: unknown;
428
+ }) => Promise<void>;
429
+ }
388
430
  /**
389
431
  * Chat middleware interface.
390
432
  *
@@ -417,6 +459,11 @@ export interface ErrorInfo {
417
459
  export interface ChatMiddleware<TContext = unknown, TInterruptDefinitions extends AnyInterruptDefinition = never> {
418
460
  /** Optional name for debugging and identification */
419
461
  name?: string;
462
+ /**
463
+ * Present when this middleware stores subagent runs.
464
+ * The router calls it. An app does not set it.
465
+ */
466
+ routedSubagentPersistence?: RoutedSubagentPersistence;
420
467
  /**
421
468
  * Called at a lifecycle boundary. Return interrupt requests to pause the run.
422
469
  * Requests from every middleware in the same boundary form one batch.
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","names":[],"sources":["../../../../../src/activities/chat/middleware/types.ts"],"sourcesContent":["import type {\n StandardJSONSchemaV1,\n StandardSchemaV1,\n} from '@standard-schema/spec'\nimport type {\n AgentLoopState,\n JSONSchema,\n ModelMessage,\n RunAgentResumeItem,\n StreamChunk,\n TokenUsage,\n Tool,\n ToolCall,\n} from '../../../types'\nimport type { SystemPrompt } from '../../../system-prompts'\nimport type { ToolApprovalResolution } from '../../../interrupts'\nimport type {\n GenericInterruptRequest,\n InterruptDefinition,\n} from '../../../interrupt-definition'\nimport type {\n Capability,\n CapabilityHandle,\n CapabilityRegistry,\n} from './capabilities'\n\n/** A file change observed inside a sandbox during a chat run. */\nexport interface SandboxFileEvent {\n type: 'create' | 'change' | 'delete'\n /** Absolute path inside the sandbox (under the workspace root). */\n path: string\n timestamp: number\n}\n\n/** The file event a sandbox hook receives: the serializable {@link SandboxFileEvent}\n * plus lazy, git-backed content accessors. Accessors compute on call, so a hook\n * that only reads `path`/`type` pays nothing. Never present on the serialized\n * `sandbox.file` CUSTOM chunk. */\nexport interface SandboxFileHookEvent extends SandboxFileEvent {\n /** Content at the session baseline (`''` for a new file or non-git workspace). */\n before: () => Promise<string>\n /** Current content (`''` when the event is a delete). */\n after: () => Promise<string>\n /** Unified patch vs the session baseline (synthesized add-patch when non-git). */\n diff: () => Promise<string>\n}\n\n/**\n * Sandbox file-event hooks a chat middleware can declare. Fire server-side for\n * every file create/change/delete observed in the sandbox during the run.\n */\nexport interface ChatSandboxHooks<TContext = unknown> {\n onFile?: (\n ctx: ChatMiddlewareContext<TContext>,\n e: SandboxFileHookEvent,\n ) => void | Promise<void>\n onFileCreate?: (\n ctx: ChatMiddlewareContext<TContext>,\n e: SandboxFileHookEvent,\n ) => void | Promise<void>\n onFileChange?: (\n ctx: ChatMiddlewareContext<TContext>,\n e: SandboxFileHookEvent,\n ) => void | Promise<void>\n onFileDelete?: (\n ctx: ChatMiddlewareContext<TContext>,\n e: SandboxFileHookEvent,\n ) => void | Promise<void>\n}\n\n// ===========================\n// Middleware Context\n// ===========================\n\n/**\n * Phase of the chat middleware lifecycle.\n * - 'init': Initial config transform before the chat engine starts\n * - 'beforeModel': Before each adapter chatStream call (per agent iteration)\n * - 'afterModel': After each adapter chatStream call (per agent iteration)\n * - 'modelStream': During model streaming\n * - 'beforeTools': Before tool execution phase\n * - 'afterTools': After tool execution phase\n * - 'structuredOutput': During the final structured-output adapter call (set\n * for chunks from adapter.structuredOutputStream or the synthesized fallback)\n */\nexport type ChatMiddlewarePhase =\n | 'init'\n | 'beforeModel'\n | 'afterModel'\n | 'modelStream'\n | 'beforeTools'\n | 'afterTools'\n | 'structuredOutput'\n\nexport const INTERRUPT_BOUNDARY_PHASES = [\n 'beforeModel',\n 'afterModel',\n 'beforeTools',\n 'afterTools',\n] as const\n\nexport type InterruptBoundaryPhase = (typeof INTERRUPT_BOUNDARY_PHASES)[number]\n\nexport const INTERRUPT_TOOL_RESUMES = ['continue', 'cancel', 'stop'] as const\n\nexport type InterruptToolResume = (typeof INTERRUPT_TOOL_RESUMES)[number]\n\ntype AnyInterruptDefinition = InterruptDefinition<any, any, any, any>\n\ntype InterruptResponse<TDefinition> =\n TDefinition extends InterruptDefinition<any, any, infer TResponseSchema, any>\n ? TResponseSchema extends StandardSchemaV1<any, infer TResponse>\n ? TResponse\n : TResponseSchema extends StandardJSONSchemaV1<any, infer TResponse>\n ? TResponse\n : unknown\n : unknown\n\nexport type GenericInterruptResolution<\n TDefinition extends AnyInterruptDefinition,\n> = TDefinition extends AnyInterruptDefinition\n ?\n | {\n readonly request: GenericInterruptRequest<TDefinition>\n readonly status: 'resolved'\n readonly response: InterruptResponse<TDefinition>\n }\n | {\n readonly request: GenericInterruptRequest<TDefinition>\n readonly status: 'cancelled'\n readonly response?: never\n }\n : never\n\nexport interface InterruptResolutionCollection<\n TDefinitions extends AnyInterruptDefinition = AnyInterruptDefinition,\n> {\n for: <\n TDefinition extends ([TDefinitions] extends [never]\n ? AnyInterruptDefinition\n : TDefinitions),\n >(\n definition: TDefinition,\n ) => ReadonlyArray<GenericInterruptResolution<TDefinition>>\n all: {\n (): ReadonlyArray<GenericInterruptResolution<TDefinitions>>\n <const TSelected extends ReadonlyArray<TDefinitions>>(\n ...definitions: TSelected\n ): ReadonlyArray<GenericInterruptResolution<TSelected[number]>>\n }\n}\n\ntype BivariantInterruptResolutionHook<\n TContext,\n TDefinitions extends AnyInterruptDefinition,\n> = InterruptResolutionHookSignature<TContext, TDefinitions>['call']\n\ndeclare abstract class InterruptResolutionHookSignature<\n TContext,\n TDefinitions extends AnyInterruptDefinition,\n> {\n abstract call(\n ctx: ChatMiddlewareContext<TContext>,\n resolutions: InterruptResolutionCollection<TDefinitions>,\n ): InterruptResolutionResult | Promise<InterruptResolutionResult>\n}\n\nexport type InterruptBoundaryResult<\n TDefinitions extends AnyInterruptDefinition = AnyInterruptDefinition,\n> =\n | undefined\n | {\n readonly interrupts: ReadonlyArray<GenericInterruptRequest<TDefinitions>>\n }\n\nexport type InterruptResolutionResult = void | {\n readonly toolResume: InterruptToolResume\n}\n\n/**\n * Stable context object passed to all middleware hooks.\n * Created once per chat() invocation and shared across all hooks.\n */\nexport interface ChatMiddlewareContext<TContext = unknown> {\n /** Unique identifier for this chat request */\n requestId: string\n /** Unique identifier for this stream */\n streamId: string\n /** AG-UI run identifier for correlating client and server events */\n runId: string\n /** Interrupted or parent run correlated with this continuation. */\n parentRunId?: string\n /**\n * AG-UI thread identifier — a stable per-conversation ID used to\n * correlate client and server devtools events. Resolves to the\n * caller-provided `threadId` (or legacy `conversationId`), or an\n * auto-generated value when neither is supplied.\n */\n threadId: string\n /**\n * @deprecated Use `threadId` instead. Retained as an alias of\n * `threadId` so middleware written before the AG-UI rename keeps\n * working unchanged. Will be removed in a future major release.\n */\n conversationId?: string\n /** Current lifecycle phase */\n phase: ChatMiddlewarePhase\n /** Current agent loop iteration (0-indexed) */\n iteration: number\n /** Running count of chunks yielded so far */\n chunkIndex: number\n /** Abort signal from the chat request */\n signal?: AbortSignal\n /** Abort the chat run with a reason */\n abort: (reason?: string) => void\n /**\n * Push a `CUSTOM` chunk onto the chat stream immediately.\n * The engine yields it as soon as it can (including while `onConfig`\n * is still awaiting work such as a summarize call).\n */\n emitCustomEvent: (name: string, value: Record<string, any>) => void\n /** Runtime context provided by chat() options */\n context: TContext\n /**\n * Defer a non-blocking side-effect promise.\n * Deferred promises do not block streaming and are awaited\n * after the terminal hook (onFinish/onAbort/onError).\n */\n defer: (promise: Promise<unknown>) => void\n\n // --- Provider / adapter info (immutable for the lifetime of the request) ---\n\n /**\n * Which activity this context describes — always `'chat'`. Present so the\n * chat context structurally satisfies the base `GenerationMiddlewareContext`,\n * letting an observe-only middleware authored against the base (e.g.\n * `otelMiddleware`) run on both chat and media activities.\n */\n activity: 'chat'\n /** Provider name (e.g., 'openai', 'anthropic') */\n provider: string\n /** Model identifier (e.g., 'gpt-5.5') */\n model: string\n /** Source of the chat invocation — always 'server' for server-side chat */\n source: 'client' | 'server'\n /** Whether the chat is streaming */\n streaming: boolean\n\n // --- Config-derived info (may update per-iteration via onConfig) ---\n\n /** System prompts configured for this chat */\n systemPrompts: Array<SystemPrompt>\n /** Names of configured tools, if any */\n toolNames?: Array<string>\n /** Flattened generation options (metadata) */\n options?: Record<string, unknown> | undefined\n /** Provider-specific model options */\n modelOptions?: Record<string, unknown> | undefined\n\n // --- Computed info ---\n\n /** Number of messages at the start of the request */\n messageCount: number\n /** Whether tools are configured */\n hasTools: boolean\n\n // --- Mutable per-iteration state ---\n\n /** Current assistant message ID (changes per iteration) */\n currentMessageId: string | null\n /** Accumulated text content for the current iteration */\n accumulatedContent: string\n\n // --- References ---\n\n /** Current messages array (read-only view) */\n messages: ReadonlyArray<ModelMessage>\n /** Generate a unique ID with the given prefix */\n createId: (prefix: string) => string\n /**\n * Capability bookkeeping for this request. Populated by middleware `setup`\n * hooks (via `provide` accessors) and read by later middleware (via `get`\n * accessors). Prefer the accessors returned by `createCapability` over using\n * this directly. Orthogonal to `context` (the user runtime context).\n */\n capabilities: CapabilityRegistry\n /**\n * Read a provided capability by its handle. Equivalent to the handle's own\n * `get` accessor (`getX(ctx)`); throws if the capability was never provided.\n */\n get: <TValue>(capability: Capability<TValue>) => TValue\n /**\n * Read a capability by its handle, returning `undefined` if it was never\n * provided (never throws).\n */\n getOptional: <TValue>(capability: Capability<TValue>) => TValue | undefined\n /**\n * Provide a capability value. Equivalent to the handle's own `provide`\n * accessor (`provideX(ctx, value)`). Typically called from `setup`.\n */\n provide: <TValue>(capability: Capability<TValue>, value: TValue) => void\n}\n\n// ===========================\n// Config passed to onConfig\n// ===========================\n\n/**\n * Chat configuration that middleware can observe or transform.\n * This is a subset of the chat engine's effective configuration\n * that middleware is allowed to modify.\n */\nexport interface ChatMiddlewareConfig {\n /** Canonical conversation history. Middleware and persistence read this. */\n messages: Array<ModelMessage>\n /** Provider-only context. Defaults to `messages` when it is not set. */\n providerMessages?: Array<ModelMessage> | undefined\n systemPrompts: Array<SystemPrompt>\n tools: Array<Tool>\n resume?: Array<RunAgentResumeItem> | undefined\n resumeToolState?: ChatResumeToolState | undefined\n metadata?: Record<string, unknown> | undefined\n modelOptions?: Record<string, unknown> | undefined\n}\n\n/**\n * Tool decisions reconstructed by server-side middleware from validated resume\n * entries. This lets empty-message interrupt resumes continue tool execution\n * without relying on client message history.\n */\nexport interface ChatResumeToolState {\n approvals?: ReadonlyMap<string, ToolApprovalResolution> | undefined\n clientToolResults?: ReadonlyMap<string, unknown> | undefined\n genericInterrupts?:\n | ReadonlyMap<string, ChatResumeGenericResolution>\n | undefined\n /** Durable generic requests reconstructed by server middleware. */\n genericInterruptRequests?:\n | ReadonlyMap<\n string,\n GenericInterruptRequest<InterruptDefinition<any, any, any, any>>\n >\n | undefined\n deniedToolResults?: ReadonlyMap<string, unknown> | undefined\n cancelledToolCallIds?: ReadonlySet<string> | undefined\n}\n\nexport type ChatResumeGenericResolution =\n | { interruptId: string; status: 'resolved'; payload: unknown }\n | { interruptId: string; status: 'cancelled'; payload?: never }\n\n/**\n * Config passed to onStructuredOutputConfig.\n *\n * Mirrors ChatMiddlewareConfig minus `tools` (the final structured-output call\n * is a single typed-response request, not an agentic loop — tools cannot be\n * forwarded to it), plus the `outputSchema` being sent to the provider.\n * Middleware may transform the schema (e.g., inject $defs, strip\n * vendor-incompatible keywords) by returning a partial that includes\n * `outputSchema`.\n */\nexport interface StructuredOutputMiddlewareConfig extends Omit<\n ChatMiddlewareConfig,\n 'tools'\n> {\n /** JSON Schema being sent to the provider for structured output. */\n outputSchema: JSONSchema\n}\n\n// ===========================\n// Tool Call Hook Context\n// ===========================\n\n/**\n * Context provided to tool call hooks (onBeforeToolCall / onAfterToolCall).\n */\nexport interface ToolCallHookContext {\n /** The tool call being executed */\n toolCall: ToolCall\n /** The resolved tool definition, if found */\n tool: Tool | undefined\n /** Parsed arguments for the tool call */\n args: unknown\n /** Name of the tool */\n toolName: string\n /** ID of the tool call */\n toolCallId: string\n}\n\n/**\n * Decision returned from onBeforeToolCall.\n * - undefined/void: continue with normal execution\n * - { type: 'transformArgs', args }: replace args used for execution\n * - { type: 'skip', result }: skip execution, use provided result\n * - { type: 'abort', reason }: abort the entire chat run\n */\nexport type BeforeToolCallDecision =\n | void\n | undefined\n | null\n | { type: 'transformArgs'; args: unknown }\n | { type: 'skip'; result: unknown }\n | { type: 'abort'; reason?: string }\n\n/**\n * Outcome information provided to onAfterToolCall.\n */\nexport interface AfterToolCallInfo {\n /** The tool call that was executed */\n toolCall: ToolCall\n /** The resolved tool definition */\n tool: Tool | undefined\n /** Name of the tool */\n toolName: string\n /** ID of the tool call */\n toolCallId: string\n /** Whether the execution succeeded */\n ok: boolean\n /** Duration of tool execution in milliseconds */\n duration: number\n /** The result (if ok) or error (if not ok) */\n result?: unknown\n error?: unknown\n}\n\n// ===========================\n// Iteration Info\n// ===========================\n\n/**\n * Information passed to onIteration at the start of each agent loop iteration.\n */\nexport interface IterationInfo {\n /** 0-based iteration index */\n iteration: number\n /** The assistant message ID created for this iteration */\n messageId: string\n}\n\n// ===========================\n// Tool Phase Complete Info\n// ===========================\n\n/**\n * Aggregate information passed to onToolPhaseComplete after all tool calls\n * in an iteration have been processed.\n */\nexport interface ToolPhaseCompleteInfo {\n /** Tool calls that were assigned to the assistant message */\n toolCalls: Array<ToolCall>\n /** Completed tool results */\n results: Array<{\n toolCallId: string\n toolName: string\n result: unknown\n duration?: number\n }>\n /** Tools that need user approval */\n needsApproval: Array<{\n toolCallId: string\n toolName: string\n input: unknown\n approvalId: string\n }>\n /** Tools that need client-side execution */\n needsClientExecution: Array<{\n toolCallId: string\n toolName: string\n input: unknown\n }>\n}\n\n// ===========================\n// Usage Info\n// ===========================\n\n/**\n * Token usage statistics passed to the onUsage hook.\n * Extracted from the RUN_FINISHED chunk when usage data is present.\n *\n * Includes optional provider-reported `cost`/`costDetails` (see {@link TokenUsage}).\n * Kept as an interface extending `TokenUsage` to preserve declaration merging for\n * this publicly exported type.\n */\nexport interface UsageInfo extends TokenUsage {}\n\n// ===========================\n// Terminal Hook Info\n// ===========================\n\n/**\n * Information passed to onFinish.\n */\nexport interface FinishInfo {\n /** The finish reason from the last model response */\n finishReason: string | null\n /** Total duration of the chat run in milliseconds */\n duration: number\n /** Final accumulated text content */\n content: string\n /** Final usage totals, if available (optionally including provider-reported cost) */\n usage?: TokenUsage | undefined\n}\n\n/**\n * Information passed to onAbort.\n */\nexport interface AbortInfo {\n /** The reason for the abort, if provided */\n reason?: string\n /** Duration until abort in milliseconds */\n duration: number\n /**\n * True only when the abort came from an explicit, out-of-band cancel (e.g. a\n * cancel endpoint setting `RunRecord.cancelRequested`), never from a mere\n * client disconnect.\n *\n * A disconnect and a user pressing \"stop\" are the SAME connection close on\n * the wire, so consumers must not infer intent from an abort alone. Middleware\n * that tears down expensive resources reads this to distinguish \"the viewer\n * left, keep going\" from \"the user wants this stopped\". Populated from the\n * abort reason: `true` exactly when the run was aborted with `RUN_CANCEL_REASON`\n * (matched with `===`, so an arbitrary error message can never be read as a\n * deliberate cancel), `false` for every other abort. The durable channel is\n * separate — middleware that must also catch a cancel recorded on a different\n * host reads `RunRecord.cancelRequested` in addition to this flag.\n */\n cancelRequested?: boolean\n}\n\n/**\n * Information passed to onError.\n */\nexport interface ErrorInfo {\n /** The error that caused the failure */\n error: unknown\n /** Duration until error in milliseconds */\n duration: number\n}\n\n// ===========================\n// Middleware Interface\n// ===========================\n\n/**\n * Chat middleware interface.\n *\n * All hooks are optional. Middleware is composed in array order:\n * - `onConfig`: config piped through middlewares in order (first transform influences later)\n * - `onChunk`: each output chunk is fed into the next middleware in order\n *\n * @example Logging middleware\n * ```ts\n * const loggingMiddleware: ChatMiddleware = {\n * name: 'logging',\n * onStart(ctx) { console.log('Chat started', ctx.requestId) },\n * onChunk(ctx, chunk) { console.log('Chunk:', chunk.type) },\n * onFinish(ctx, info) { console.log('Done:', info.duration, 'ms') },\n * }\n * ```\n *\n * @example Redaction middleware\n * ```ts\n * const redactionMiddleware: ChatMiddleware = {\n * name: 'redaction',\n * onChunk(ctx, chunk) {\n * if (chunk.type === 'TEXT_MESSAGE_CONTENT') {\n * return { ...chunk, delta: redact(chunk.delta) }\n * }\n * },\n * }\n * ```\n */\nexport interface ChatMiddleware<\n TContext = unknown,\n TInterruptDefinitions extends AnyInterruptDefinition = never,\n> {\n /** Optional name for debugging and identification */\n name?: string\n\n /**\n * Called at a lifecycle boundary. Return interrupt requests to pause the run.\n * Requests from every middleware in the same boundary form one batch.\n */\n onInterruptBoundary?: (\n ctx: ChatMiddlewareContext<TContext> & { phase: InterruptBoundaryPhase },\n ) =>\n | InterruptBoundaryResult<TInterruptDefinitions>\n | Promise<InterruptBoundaryResult<TInterruptDefinitions>>\n\n /**\n * Called on a continuation run after the client answers registered interrupts.\n * Return `toolResume` to decide whether pending tools continue, cancel, or stop.\n */\n onInterruptResolution?: BivariantInterruptResolutionHook<\n TContext,\n TInterruptDefinitions\n >\n\n /**\n * Capabilities this middleware requires. `chat()` validates that some\n * middleware (or the adapter) provides each one; unsatisfied requirements are\n * a compile-time error (array coverage / builder) and a runtime error before\n * the adapter runs.\n */\n requires?: ReadonlyArray<CapabilityHandle>\n\n /**\n * Capabilities this middleware provides. Each declared capability MUST be\n * provided (via its `provide` accessor) inside `setup`, or `chat()` throws\n * after the setup phase.\n */\n provides?: ReadonlyArray<CapabilityHandle>\n\n /**\n * Capabilities this middleware uses if present but does not require.\n * Non-gating: never causes a validation error. Read with\n * `getX(ctx, { optional: true })`.\n */\n optionalRequires?: ReadonlyArray<CapabilityHandle>\n\n /**\n * Provisioning hook. Runs FIRST — before `onConfig` (init) — across all\n * middleware in array order. Use it to call `provide` accessors so later\n * middleware (`onConfig` onward) can consume the capabilities. Receives the\n * stable context; does NOT receive the mutable config.\n */\n setup?: (ctx: ChatMiddlewareContext<TContext>) => void | Promise<void>\n\n /**\n * Called to observe or transform the chat configuration.\n * Called at init and at the beginning of each agent iteration.\n *\n * Return a partial config to merge with the current config, or void to pass through.\n * Only the fields you return are overwritten — everything else is preserved.\n */\n onConfig?: (\n ctx: ChatMiddlewareContext<TContext>,\n config: ChatMiddlewareConfig,\n ) =>\n | void\n | null\n | Partial<ChatMiddlewareConfig>\n | Promise<void | null | Partial<ChatMiddlewareConfig>>\n\n /**\n * Called at the start of the final structured-output call (when the chat\n * was invoked with outputSchema). Pipes through middleware in order, like\n * onConfig, but with access to the JSON Schema being sent to the provider.\n *\n * Return a partial to shallow-merge into the current config, or void to\n * pass through.\n *\n * Fires BEFORE onConfig at the structured-output boundary. onConfig also\n * re-fires at the same boundary with ctx.phase === 'structuredOutput',\n * receiving the post-onStructuredOutputConfig view of the config (minus\n * outputSchema). Use onConfig for general-purpose transforms that apply\n * to every adapter call; use this hook when you need to transform the\n * outputSchema or apply structured-output-specific behavior.\n */\n onStructuredOutputConfig?: (\n ctx: ChatMiddlewareContext<TContext>,\n config: StructuredOutputMiddlewareConfig,\n ) =>\n | void\n | null\n | Partial<StructuredOutputMiddlewareConfig>\n | Promise<void | null | Partial<StructuredOutputMiddlewareConfig>>\n\n /**\n * Called when the chat run starts (after initial onConfig).\n */\n onStart?: (ctx: ChatMiddlewareContext<TContext>) => void | Promise<void>\n\n /**\n * Called at the start of each agent loop iteration, after a new assistant message ID\n * is created. Use this to observe iteration boundaries.\n */\n onIteration?: (\n ctx: ChatMiddlewareContext<TContext>,\n info: IterationInfo,\n ) => void | Promise<void>\n\n /**\n * Called when the engine is deciding whether to start another agent-loop\n * iteration (after a tool phase or between model turns).\n *\n * Return `false` to stop further iterations. Return `true`, `void`, or\n * `undefined` to allow continuation. Combined with AND semantics across\n * middleware and with `agentLoopStrategy` — any `false` stops the loop.\n *\n * Does not abort the run: the stream finishes normally with the current\n * messages. Use `ctx.abort()` only when you need a hard abort.\n *\n * Receives the same {@link AgentLoopState} passed to strategies\n * (`iterationCount`, `toolCallCount`, `lastTurnToolCallCount`, etc.).\n */\n onShouldContinue?: (\n ctx: ChatMiddlewareContext<TContext>,\n state: AgentLoopState,\n ) => boolean | void | Promise<boolean | void>\n\n /**\n * Called for every chunk yielded by chat().\n * Can observe, transform, expand, or drop chunks.\n *\n * @returns void (pass through), chunk (replace), chunk[] (expand), null (drop)\n */\n onChunk?: (\n ctx: ChatMiddlewareContext<TContext>,\n chunk: StreamChunk,\n ) =>\n | void\n | StreamChunk\n | Array<StreamChunk>\n | null\n | Promise<void | StreamChunk | Array<StreamChunk> | null>\n\n /**\n * Called before a tool is executed.\n * Can observe, transform args, skip execution, or abort the run.\n */\n onBeforeToolCall?: (\n ctx: ChatMiddlewareContext<TContext>,\n hookCtx: ToolCallHookContext,\n ) => BeforeToolCallDecision | Promise<BeforeToolCallDecision>\n\n /**\n * Called after a tool execution completes (success or failure).\n */\n onAfterToolCall?: (\n ctx: ChatMiddlewareContext<TContext>,\n info: AfterToolCallInfo,\n ) => void | Promise<void>\n\n /**\n * Called after all tool calls in an iteration have been processed.\n * Provides aggregate data about tool execution results, approvals, and client tools.\n */\n onToolPhaseComplete?: (\n ctx: ChatMiddlewareContext<TContext>,\n info: ToolPhaseCompleteInfo,\n ) => void | Promise<void>\n\n /**\n * Called when usage data is available from a RUN_FINISHED chunk.\n * Called once per model iteration that reports usage.\n */\n onUsage?: (\n ctx: ChatMiddlewareContext<TContext>,\n usage: UsageInfo,\n ) => void | Promise<void>\n\n /**\n * Called when the chat run completes normally.\n * Exactly one of onFinish/onAbort/onError will be called per run.\n */\n onFinish?: (\n ctx: ChatMiddlewareContext<TContext>,\n info: FinishInfo,\n ) => void | Promise<void>\n\n /**\n * Called when the chat run is aborted.\n * Exactly one of onFinish/onAbort/onError will be called per run.\n */\n onAbort?: (\n ctx: ChatMiddlewareContext<TContext>,\n info: AbortInfo,\n ) => void | Promise<void>\n\n /**\n * Called when the chat run encounters an unhandled error.\n * Exactly one of onFinish/onAbort/onError will be called per run.\n */\n onError?: (\n ctx: ChatMiddlewareContext<TContext>,\n info: ErrorInfo,\n ) => void | Promise<void>\n\n /**\n * Sandbox file-event hooks. Fire when a sandbox provided by `withSandbox` is\n * active during the run and a file is created/changed/deleted. Server-side.\n */\n sandbox?: ChatSandboxHooks<TContext>\n}\n\n/** A `ChatMiddleware` with a permissive context — for use as a constraint. */\n/** A permissive middleware constraint that retains the definition parameter. */\nexport type AnyChatMiddleware = ChatMiddleware<any, any>\n"],"mappings":";AA8FA,IAAa,4BAA4B;CACvC;CACA;CACA;CACA;AACF;AAIA,IAAa,yBAAyB;CAAC;CAAY;CAAU;AAAM"}
1
+ {"version":3,"file":"types.js","names":[],"sources":["../../../../../src/activities/chat/middleware/types.ts"],"sourcesContent":["import type {\n StandardJSONSchemaV1,\n StandardSchemaV1,\n} from '@standard-schema/spec'\nimport type {\n AgentLoopState,\n EmitCustomEventOptions,\n Interrupt,\n JSONSchema,\n ModelMessage,\n UIMessage,\n RunAgentResumeItem,\n StreamChunk,\n TokenUsage,\n Tool,\n ToolCall,\n} from '../../../types'\nimport type { SystemPrompt } from '../../../system-prompts'\nimport type { ToolApprovalResolution } from '../../../interrupts'\nimport type {\n GenericInterruptRequest,\n InterruptDefinition,\n} from '../../../interrupt-definition'\nimport type {\n Capability,\n CapabilityHandle,\n CapabilityRegistry,\n} from './capabilities'\n\n/** A file change observed inside a sandbox during a chat run. */\nexport interface SandboxFileEvent {\n type: 'create' | 'change' | 'delete'\n /** Absolute path inside the sandbox (under the workspace root). */\n path: string\n timestamp: number\n}\n\n/** The file event a sandbox hook receives: the serializable {@link SandboxFileEvent}\n * plus lazy, git-backed content accessors. Accessors compute on call, so a hook\n * that only reads `path`/`type` pays nothing. Never present on the serialized\n * `sandbox.file` CUSTOM chunk. */\nexport interface SandboxFileHookEvent extends SandboxFileEvent {\n /** Content at the session baseline (`''` for a new file or non-git workspace). */\n before: () => Promise<string>\n /** Current content (`''` when the event is a delete). */\n after: () => Promise<string>\n /** Unified patch vs the session baseline (synthesized add-patch when non-git). */\n diff: () => Promise<string>\n}\n\n/**\n * Sandbox file-event hooks a chat middleware can declare. Fire server-side for\n * every file create/change/delete observed in the sandbox during the run.\n */\nexport interface ChatSandboxHooks<TContext = unknown> {\n onFile?: (\n ctx: ChatMiddlewareContext<TContext>,\n e: SandboxFileHookEvent,\n ) => void | Promise<void>\n onFileCreate?: (\n ctx: ChatMiddlewareContext<TContext>,\n e: SandboxFileHookEvent,\n ) => void | Promise<void>\n onFileChange?: (\n ctx: ChatMiddlewareContext<TContext>,\n e: SandboxFileHookEvent,\n ) => void | Promise<void>\n onFileDelete?: (\n ctx: ChatMiddlewareContext<TContext>,\n e: SandboxFileHookEvent,\n ) => void | Promise<void>\n}\n\n// ===========================\n// Middleware Context\n// ===========================\n\n/**\n * Phase of the chat middleware lifecycle.\n * - 'init': Initial config transform before the chat engine starts\n * - 'beforeModel': Before each adapter chatStream call (per agent iteration)\n * - 'afterModel': After each adapter chatStream call (per agent iteration)\n * - 'modelStream': During model streaming\n * - 'beforeTools': Before tool execution phase\n * - 'afterTools': After tool execution phase\n * - 'structuredOutput': During the final structured-output adapter call (set\n * for chunks from adapter.structuredOutputStream or the synthesized fallback)\n */\nexport type ChatMiddlewarePhase =\n | 'init'\n | 'beforeModel'\n | 'afterModel'\n | 'modelStream'\n | 'beforeTools'\n | 'afterTools'\n | 'structuredOutput'\n\nexport const INTERRUPT_BOUNDARY_PHASES = [\n 'beforeModel',\n 'afterModel',\n 'beforeTools',\n 'afterTools',\n] as const\n\nexport type InterruptBoundaryPhase = (typeof INTERRUPT_BOUNDARY_PHASES)[number]\n\nexport const INTERRUPT_TOOL_RESUMES = ['continue', 'cancel', 'stop'] as const\n\nexport type InterruptToolResume = (typeof INTERRUPT_TOOL_RESUMES)[number]\n\ntype AnyInterruptDefinition = InterruptDefinition<any, any, any, any>\n\ntype InterruptResponse<TDefinition> =\n TDefinition extends InterruptDefinition<any, any, infer TResponseSchema, any>\n ? TResponseSchema extends StandardSchemaV1<any, infer TResponse>\n ? TResponse\n : TResponseSchema extends StandardJSONSchemaV1<any, infer TResponse>\n ? TResponse\n : unknown\n : unknown\n\nexport type GenericInterruptResolution<\n TDefinition extends AnyInterruptDefinition,\n> = TDefinition extends AnyInterruptDefinition\n ?\n | {\n readonly request: GenericInterruptRequest<TDefinition>\n readonly status: 'resolved'\n readonly response: InterruptResponse<TDefinition>\n }\n | {\n readonly request: GenericInterruptRequest<TDefinition>\n readonly status: 'cancelled'\n readonly response?: never\n }\n : never\n\nexport interface InterruptResolutionCollection<\n TDefinitions extends AnyInterruptDefinition = AnyInterruptDefinition,\n> {\n for: <\n TDefinition extends ([TDefinitions] extends [never]\n ? AnyInterruptDefinition\n : TDefinitions),\n >(\n definition: TDefinition,\n ) => ReadonlyArray<GenericInterruptResolution<TDefinition>>\n all: {\n (): ReadonlyArray<GenericInterruptResolution<TDefinitions>>\n <const TSelected extends ReadonlyArray<TDefinitions>>(\n ...definitions: TSelected\n ): ReadonlyArray<GenericInterruptResolution<TSelected[number]>>\n }\n}\n\ntype BivariantInterruptResolutionHook<\n TContext,\n TDefinitions extends AnyInterruptDefinition,\n> = InterruptResolutionHookSignature<TContext, TDefinitions>['call']\n\ndeclare abstract class InterruptResolutionHookSignature<\n TContext,\n TDefinitions extends AnyInterruptDefinition,\n> {\n abstract call(\n ctx: ChatMiddlewareContext<TContext>,\n resolutions: InterruptResolutionCollection<TDefinitions>,\n ): InterruptResolutionResult | Promise<InterruptResolutionResult>\n}\n\nexport type InterruptBoundaryResult<\n TDefinitions extends AnyInterruptDefinition = AnyInterruptDefinition,\n> =\n | undefined\n | {\n readonly interrupts: ReadonlyArray<GenericInterruptRequest<TDefinitions>>\n }\n\nexport type InterruptResolutionResult = void | {\n readonly toolResume: InterruptToolResume\n}\n\n/**\n * Stable context object passed to all middleware hooks.\n * Created once per chat() invocation and shared across all hooks.\n */\nexport interface ChatMiddlewareContext<TContext = unknown> {\n /** Unique identifier for this chat request */\n requestId: string\n /** Unique identifier for this stream */\n streamId: string\n /** AG-UI run identifier for correlating client and server events */\n runId: string\n /** Interrupted or parent run correlated with this continuation. */\n parentRunId?: string\n /**\n * Set when this run is a subagent. The id on the child's `SUBAGENT_STARTED`\n * and on every chunk it streams. Absent on a top-level run.\n */\n subagentRunId?: string\n /**\n * AG-UI thread identifier — a stable per-conversation ID used to\n * correlate client and server devtools events. Resolves to the\n * caller-provided `threadId` (or legacy `conversationId`), or an\n * auto-generated value when neither is supplied.\n */\n threadId: string\n /**\n * @deprecated Use `threadId` instead. Retained as an alias of\n * `threadId` so middleware written before the AG-UI rename keeps\n * working unchanged. Will be removed in a future major release.\n */\n conversationId?: string\n /** Current lifecycle phase */\n phase: ChatMiddlewarePhase\n /** Current agent loop iteration (0-indexed) */\n iteration: number\n /** Running count of chunks yielded so far */\n chunkIndex: number\n /** Abort signal from the chat request */\n signal?: AbortSignal\n /** Abort the chat run with a reason */\n abort: (reason?: string) => void\n /**\n * Push a `CUSTOM` chunk onto the chat stream immediately.\n * The engine yields it as soon as it can (including while `onConfig`\n * is still awaiting work such as a summarize call). Durability then\n * flushes the event on its own, unless you pass `{ batch: true }`.\n */\n emitCustomEvent: (\n name: string,\n value: Record<string, any>,\n options?: EmitCustomEventOptions,\n ) => void\n /** Runtime context provided by chat() options */\n context: TContext\n /**\n * Defer a non-blocking side-effect promise.\n * Deferred promises do not block streaming and are awaited\n * after the terminal hook (onFinish/onAbort/onError).\n */\n defer: (promise: Promise<unknown>) => void\n\n // --- Provider / adapter info (immutable for the lifetime of the request) ---\n\n /**\n * Which activity this context describes — always `'chat'`. Present so the\n * chat context structurally satisfies the base `GenerationMiddlewareContext`,\n * letting an observe-only middleware authored against the base (e.g.\n * `otelMiddleware`) run on both chat and media activities.\n */\n activity: 'chat'\n /** Provider name (e.g., 'openai', 'anthropic') */\n provider: string\n /** Model identifier (e.g., 'gpt-5.5') */\n model: string\n /** Source of the chat invocation — always 'server' for server-side chat */\n source: 'client' | 'server'\n /** Whether the chat is streaming */\n streaming: boolean\n\n // --- Config-derived info (may update per-iteration via onConfig) ---\n\n /** System prompts configured for this chat */\n systemPrompts: Array<SystemPrompt>\n /** Names of configured tools, if any */\n toolNames?: Array<string>\n /** Flattened generation options (metadata) */\n options?: Record<string, unknown> | undefined\n /** Provider-specific model options */\n modelOptions?: Record<string, unknown> | undefined\n\n // --- Computed info ---\n\n /** Number of messages at the start of the request */\n messageCount: number\n /** Whether tools are configured */\n hasTools: boolean\n\n // --- Mutable per-iteration state ---\n\n /** Current assistant message ID (changes per iteration) */\n currentMessageId: string | null\n /** Accumulated text content for the current iteration */\n accumulatedContent: string\n\n // --- References ---\n\n /** Current messages array (read-only view) */\n messages: ReadonlyArray<ModelMessage>\n /** Generate a unique ID with the given prefix */\n createId: (prefix: string) => string\n /**\n * Capability bookkeeping for this request. Populated by middleware `setup`\n * hooks (via `provide` accessors) and read by later middleware (via `get`\n * accessors). Prefer the accessors returned by `createCapability` over using\n * this directly. Orthogonal to `context` (the user runtime context).\n */\n capabilities: CapabilityRegistry\n /**\n * Read a provided capability by its handle. Equivalent to the handle's own\n * `get` accessor (`getX(ctx)`); throws if the capability was never provided.\n */\n get: <TValue>(capability: Capability<TValue>) => TValue\n /**\n * Read a capability by its handle, returning `undefined` if it was never\n * provided (never throws).\n */\n getOptional: <TValue>(capability: Capability<TValue>) => TValue | undefined\n /**\n * Provide a capability value. Equivalent to the handle's own `provide`\n * accessor (`provideX(ctx, value)`). Typically called from `setup`.\n */\n provide: <TValue>(capability: Capability<TValue>, value: TValue) => void\n}\n\n// ===========================\n// Config passed to onConfig\n// ===========================\n\n/**\n * Chat configuration that middleware can observe or transform.\n * This is a subset of the chat engine's effective configuration\n * that middleware is allowed to modify.\n */\nexport interface ChatMiddlewareConfig {\n /** Canonical conversation history. Middleware and persistence read this. */\n messages: Array<ModelMessage>\n /** Provider-only context. Defaults to `messages` when it is not set. */\n providerMessages?: Array<ModelMessage> | undefined\n systemPrompts: Array<SystemPrompt>\n tools: Array<Tool>\n resume?: Array<RunAgentResumeItem> | undefined\n resumeToolState?: ChatResumeToolState | undefined\n metadata?: Record<string, unknown> | undefined\n modelOptions?: Record<string, unknown> | undefined\n}\n\n/**\n * Tool decisions reconstructed by server-side middleware from validated resume\n * entries. This lets empty-message interrupt resumes continue tool execution\n * without relying on client message history.\n */\nexport interface ChatResumeToolState {\n approvals?: ReadonlyMap<string, ToolApprovalResolution> | undefined\n clientToolResults?: ReadonlyMap<string, unknown> | undefined\n genericInterrupts?:\n | ReadonlyMap<string, ChatResumeGenericResolution>\n | undefined\n /** Durable generic requests reconstructed by server middleware. */\n genericInterruptRequests?:\n | ReadonlyMap<\n string,\n GenericInterruptRequest<InterruptDefinition<any, any, any, any>>\n >\n | undefined\n deniedToolResults?: ReadonlyMap<string, unknown> | undefined\n cancelledToolCallIds?: ReadonlySet<string> | undefined\n}\n\nexport type ChatResumeGenericResolution =\n | { interruptId: string; status: 'resolved'; payload: unknown }\n | { interruptId: string; status: 'cancelled'; payload?: never }\n\n/**\n * Config passed to onStructuredOutputConfig.\n *\n * Mirrors ChatMiddlewareConfig minus `tools` (the final structured-output call\n * is a single typed-response request, not an agentic loop — tools cannot be\n * forwarded to it), plus the `outputSchema` being sent to the provider.\n * Middleware may transform the schema (e.g., inject $defs, strip\n * vendor-incompatible keywords) by returning a partial that includes\n * `outputSchema`.\n */\nexport interface StructuredOutputMiddlewareConfig extends Omit<\n ChatMiddlewareConfig,\n 'tools'\n> {\n /** JSON Schema being sent to the provider for structured output. */\n outputSchema: JSONSchema\n}\n\n// ===========================\n// Tool Call Hook Context\n// ===========================\n\n/**\n * Context provided to tool call hooks (onBeforeToolCall / onAfterToolCall).\n */\nexport interface ToolCallHookContext {\n /** The tool call being executed */\n toolCall: ToolCall\n /** The resolved tool definition, if found */\n tool: Tool | undefined\n /** Parsed arguments for the tool call */\n args: unknown\n /** Name of the tool */\n toolName: string\n /** ID of the tool call */\n toolCallId: string\n}\n\n/**\n * Decision returned from onBeforeToolCall.\n * - undefined/void: continue with normal execution\n * - { type: 'transformArgs', args }: replace args used for execution\n * - { type: 'skip', result }: skip execution, use provided result\n * - { type: 'abort', reason }: abort the entire chat run\n */\nexport type BeforeToolCallDecision =\n | void\n | undefined\n | null\n | { type: 'transformArgs'; args: unknown }\n | { type: 'skip'; result: unknown }\n | { type: 'abort'; reason?: string }\n\n/**\n * Outcome information provided to onAfterToolCall.\n */\nexport interface AfterToolCallInfo {\n /** The tool call that was executed */\n toolCall: ToolCall\n /** The resolved tool definition */\n tool: Tool | undefined\n /** Name of the tool */\n toolName: string\n /** ID of the tool call */\n toolCallId: string\n /** Whether the execution succeeded */\n ok: boolean\n /** Duration of tool execution in milliseconds */\n duration: number\n /** The result (if ok) or error (if not ok) */\n result?: unknown\n error?: unknown\n}\n\n// ===========================\n// Iteration Info\n// ===========================\n\n/**\n * Information passed to onIteration at the start of each agent loop iteration.\n */\nexport interface IterationInfo {\n /** 0-based iteration index */\n iteration: number\n /** The assistant message ID created for this iteration */\n messageId: string\n}\n\n// ===========================\n// Tool Phase Complete Info\n// ===========================\n\n/**\n * Aggregate information passed to onToolPhaseComplete after all tool calls\n * in an iteration have been processed.\n */\nexport interface ToolPhaseCompleteInfo {\n /** Tool calls that were assigned to the assistant message */\n toolCalls: Array<ToolCall>\n /** Completed tool results */\n results: Array<{\n toolCallId: string\n toolName: string\n result: unknown\n duration?: number\n }>\n /** Tools that need user approval */\n needsApproval: Array<{\n toolCallId: string\n toolName: string\n input: unknown\n approvalId: string\n }>\n /** Tools that need client-side execution */\n needsClientExecution: Array<{\n toolCallId: string\n toolName: string\n input: unknown\n }>\n}\n\n// ===========================\n// Usage Info\n// ===========================\n\n/**\n * Token usage statistics passed to the onUsage hook.\n * Extracted from the RUN_FINISHED chunk when usage data is present.\n *\n * Includes optional provider-reported `cost`/`costDetails` (see {@link TokenUsage}).\n * Kept as an interface extending `TokenUsage` to preserve declaration merging for\n * this publicly exported type.\n */\nexport interface UsageInfo extends TokenUsage {}\n\n// ===========================\n// Terminal Hook Info\n// ===========================\n\n/**\n * Information passed to onFinish.\n */\nexport interface FinishInfo {\n /** The finish reason from the last model response */\n finishReason: string | null\n /** Total duration of the chat run in milliseconds */\n duration: number\n /** Final accumulated text content */\n content: string\n /** Final usage totals, if available (optionally including provider-reported cost) */\n usage?: TokenUsage | undefined\n}\n\n/**\n * Information passed to onAbort.\n */\nexport interface AbortInfo {\n /** The reason for the abort, if provided */\n reason?: string\n /** Duration until abort in milliseconds */\n duration: number\n /**\n * True only when the abort came from an explicit, out-of-band cancel (e.g. a\n * cancel endpoint setting `RunRecord.cancelRequested`), never from a mere\n * client disconnect.\n *\n * A disconnect and a user pressing \"stop\" are the SAME connection close on\n * the wire, so consumers must not infer intent from an abort alone. Middleware\n * that tears down expensive resources reads this to distinguish \"the viewer\n * left, keep going\" from \"the user wants this stopped\". Populated from the\n * abort reason: `true` exactly when the run was aborted with `RUN_CANCEL_REASON`\n * (matched with `===`, so an arbitrary error message can never be read as a\n * deliberate cancel), `false` for every other abort. The durable channel is\n * separate — middleware that must also catch a cancel recorded on a different\n * host reads `RunRecord.cancelRequested` in addition to this flag.\n */\n cancelRequested?: boolean\n}\n\n/**\n * Information passed to onError.\n */\nexport interface ErrorInfo {\n /** The error that caused the failure */\n error: unknown\n /** Duration until error in milliseconds */\n duration: number\n}\n\n/**\n * Saves subagent runs while a router owns the turn.\n * `withPersistence` sets this. `chat()` calls it. Apps do not.\n */\nexport interface RoutedSubagentPersistence {\n start: (input: {\n threadId: string\n runId: string\n messages: ReadonlyArray<UIMessage | ModelMessage>\n /**\n * The run's resume entries: answers to earlier child interrupts, plus any\n * the parent answers itself.\n */\n resume?: ReadonlyArray<RunAgentResumeItem>\n }) => Promise<void>\n chunk: (input: {\n threadId: string\n runId: string\n chunk: StreamChunk\n }) => Promise<void>\n finish: (input: { threadId: string; runId: string }) => Promise<void>\n /** The run stopped because a child waits for outside input. */\n suspend?: (input: {\n threadId: string\n runId: string\n interrupts: ReadonlyArray<Interrupt>\n }) => Promise<void>\n abort: (input: {\n threadId: string\n runId: string\n error?: unknown\n }) => Promise<void>\n}\n\n// ===========================\n// Middleware Interface\n// ===========================\n\n/**\n * Chat middleware interface.\n *\n * All hooks are optional. Middleware is composed in array order:\n * - `onConfig`: config piped through middlewares in order (first transform influences later)\n * - `onChunk`: each output chunk is fed into the next middleware in order\n *\n * @example Logging middleware\n * ```ts\n * const loggingMiddleware: ChatMiddleware = {\n * name: 'logging',\n * onStart(ctx) { console.log('Chat started', ctx.requestId) },\n * onChunk(ctx, chunk) { console.log('Chunk:', chunk.type) },\n * onFinish(ctx, info) { console.log('Done:', info.duration, 'ms') },\n * }\n * ```\n *\n * @example Redaction middleware\n * ```ts\n * const redactionMiddleware: ChatMiddleware = {\n * name: 'redaction',\n * onChunk(ctx, chunk) {\n * if (chunk.type === 'TEXT_MESSAGE_CONTENT') {\n * return { ...chunk, delta: redact(chunk.delta) }\n * }\n * },\n * }\n * ```\n */\nexport interface ChatMiddleware<\n TContext = unknown,\n TInterruptDefinitions extends AnyInterruptDefinition = never,\n> {\n /** Optional name for debugging and identification */\n name?: string\n\n /**\n * Present when this middleware stores subagent runs.\n * The router calls it. An app does not set it.\n */\n routedSubagentPersistence?: RoutedSubagentPersistence\n\n /**\n * Called at a lifecycle boundary. Return interrupt requests to pause the run.\n * Requests from every middleware in the same boundary form one batch.\n */\n onInterruptBoundary?: (\n ctx: ChatMiddlewareContext<TContext> & { phase: InterruptBoundaryPhase },\n ) =>\n | InterruptBoundaryResult<TInterruptDefinitions>\n | Promise<InterruptBoundaryResult<TInterruptDefinitions>>\n\n /**\n * Called on a continuation run after the client answers registered interrupts.\n * Return `toolResume` to decide whether pending tools continue, cancel, or stop.\n */\n onInterruptResolution?: BivariantInterruptResolutionHook<\n TContext,\n TInterruptDefinitions\n >\n\n /**\n * Capabilities this middleware requires. `chat()` validates that some\n * middleware (or the adapter) provides each one; unsatisfied requirements are\n * a compile-time error (array coverage / builder) and a runtime error before\n * the adapter runs.\n */\n requires?: ReadonlyArray<CapabilityHandle>\n\n /**\n * Capabilities this middleware provides. Each declared capability MUST be\n * provided (via its `provide` accessor) inside `setup`, or `chat()` throws\n * after the setup phase.\n */\n provides?: ReadonlyArray<CapabilityHandle>\n\n /**\n * Capabilities this middleware uses if present but does not require.\n * Non-gating: never causes a validation error. Read with\n * `getX(ctx, { optional: true })`.\n */\n optionalRequires?: ReadonlyArray<CapabilityHandle>\n\n /**\n * Provisioning hook. Runs FIRST — before `onConfig` (init) — across all\n * middleware in array order. Use it to call `provide` accessors so later\n * middleware (`onConfig` onward) can consume the capabilities. Receives the\n * stable context; does NOT receive the mutable config.\n */\n setup?: (ctx: ChatMiddlewareContext<TContext>) => void | Promise<void>\n\n /**\n * Called to observe or transform the chat configuration.\n * Called at init and at the beginning of each agent iteration.\n *\n * Return a partial config to merge with the current config, or void to pass through.\n * Only the fields you return are overwritten — everything else is preserved.\n */\n onConfig?: (\n ctx: ChatMiddlewareContext<TContext>,\n config: ChatMiddlewareConfig,\n ) =>\n | void\n | null\n | Partial<ChatMiddlewareConfig>\n | Promise<void | null | Partial<ChatMiddlewareConfig>>\n\n /**\n * Called at the start of the final structured-output call (when the chat\n * was invoked with outputSchema). Pipes through middleware in order, like\n * onConfig, but with access to the JSON Schema being sent to the provider.\n *\n * Return a partial to shallow-merge into the current config, or void to\n * pass through.\n *\n * Fires BEFORE onConfig at the structured-output boundary. onConfig also\n * re-fires at the same boundary with ctx.phase === 'structuredOutput',\n * receiving the post-onStructuredOutputConfig view of the config (minus\n * outputSchema). Use onConfig for general-purpose transforms that apply\n * to every adapter call; use this hook when you need to transform the\n * outputSchema or apply structured-output-specific behavior.\n */\n onStructuredOutputConfig?: (\n ctx: ChatMiddlewareContext<TContext>,\n config: StructuredOutputMiddlewareConfig,\n ) =>\n | void\n | null\n | Partial<StructuredOutputMiddlewareConfig>\n | Promise<void | null | Partial<StructuredOutputMiddlewareConfig>>\n\n /**\n * Called when the chat run starts (after initial onConfig).\n */\n onStart?: (ctx: ChatMiddlewareContext<TContext>) => void | Promise<void>\n\n /**\n * Called at the start of each agent loop iteration, after a new assistant message ID\n * is created. Use this to observe iteration boundaries.\n */\n onIteration?: (\n ctx: ChatMiddlewareContext<TContext>,\n info: IterationInfo,\n ) => void | Promise<void>\n\n /**\n * Called when the engine is deciding whether to start another agent-loop\n * iteration (after a tool phase or between model turns).\n *\n * Return `false` to stop further iterations. Return `true`, `void`, or\n * `undefined` to allow continuation. Combined with AND semantics across\n * middleware and with `agentLoopStrategy` — any `false` stops the loop.\n *\n * Does not abort the run: the stream finishes normally with the current\n * messages. Use `ctx.abort()` only when you need a hard abort.\n *\n * Receives the same {@link AgentLoopState} passed to strategies\n * (`iterationCount`, `toolCallCount`, `lastTurnToolCallCount`, etc.).\n */\n onShouldContinue?: (\n ctx: ChatMiddlewareContext<TContext>,\n state: AgentLoopState,\n ) => boolean | void | Promise<boolean | void>\n\n /**\n * Called for every chunk yielded by chat().\n * Can observe, transform, expand, or drop chunks.\n *\n * @returns void (pass through), chunk (replace), chunk[] (expand), null (drop)\n */\n onChunk?: (\n ctx: ChatMiddlewareContext<TContext>,\n chunk: StreamChunk,\n ) =>\n | void\n | StreamChunk\n | Array<StreamChunk>\n | null\n | Promise<void | StreamChunk | Array<StreamChunk> | null>\n\n /**\n * Called before a tool is executed.\n * Can observe, transform args, skip execution, or abort the run.\n */\n onBeforeToolCall?: (\n ctx: ChatMiddlewareContext<TContext>,\n hookCtx: ToolCallHookContext,\n ) => BeforeToolCallDecision | Promise<BeforeToolCallDecision>\n\n /**\n * Called after a tool execution completes (success or failure).\n */\n onAfterToolCall?: (\n ctx: ChatMiddlewareContext<TContext>,\n info: AfterToolCallInfo,\n ) => void | Promise<void>\n\n /**\n * Called after all tool calls in an iteration have been processed.\n * Provides aggregate data about tool execution results, approvals, and client tools.\n */\n onToolPhaseComplete?: (\n ctx: ChatMiddlewareContext<TContext>,\n info: ToolPhaseCompleteInfo,\n ) => void | Promise<void>\n\n /**\n * Called when usage data is available from a RUN_FINISHED chunk.\n * Called once per model iteration that reports usage.\n */\n onUsage?: (\n ctx: ChatMiddlewareContext<TContext>,\n usage: UsageInfo,\n ) => void | Promise<void>\n\n /**\n * Called when the chat run completes normally.\n * Exactly one of onFinish/onAbort/onError will be called per run.\n */\n onFinish?: (\n ctx: ChatMiddlewareContext<TContext>,\n info: FinishInfo,\n ) => void | Promise<void>\n\n /**\n * Called when the chat run is aborted.\n * Exactly one of onFinish/onAbort/onError will be called per run.\n */\n onAbort?: (\n ctx: ChatMiddlewareContext<TContext>,\n info: AbortInfo,\n ) => void | Promise<void>\n\n /**\n * Called when the chat run encounters an unhandled error.\n * Exactly one of onFinish/onAbort/onError will be called per run.\n */\n onError?: (\n ctx: ChatMiddlewareContext<TContext>,\n info: ErrorInfo,\n ) => void | Promise<void>\n\n /**\n * Sandbox file-event hooks. Fire when a sandbox provided by `withSandbox` is\n * active during the run and a file is created/changed/deleted. Server-side.\n */\n sandbox?: ChatSandboxHooks<TContext>\n}\n\n/** A `ChatMiddleware` with a permissive context — for use as a constraint. */\n/** A permissive middleware constraint that retains the definition parameter. */\nexport type AnyChatMiddleware = ChatMiddleware<any, any>\n"],"mappings":";AAiGA,IAAa,4BAA4B;CACvC;CACA;CACA;CACA;AACF;AAIA,IAAa,yBAAyB;CAAC;CAAY;CAAU;AAAM"}
@@ -51,6 +51,11 @@ export interface StreamProcessorOptions {
51
51
  recording?: boolean;
52
52
  /** Initial messages to populate the processor */
53
53
  initialMessages?: Array<UIMessage>;
54
+ /**
55
+ * Set on the processor of a subagent card. Chunks tagged with this id are
56
+ * the card's own; chunks for an id no card holds are dropped.
57
+ */
58
+ subagentRunId?: string;
54
59
  }
55
60
  /**
56
61
  * StreamProcessor - State machine for processing AI response streams
@@ -82,12 +87,15 @@ export declare class StreamProcessor {
82
87
  private readonly structuredMessageIds;
83
88
  private readonly structuredOutputUpdateBatches;
84
89
  private readonly activeRuns;
90
+ private readonly childProcessors;
91
+ private readonly childToolCalls;
85
92
  private finishReason;
86
93
  private hasError;
87
94
  private isDone;
88
95
  private streamEndEmitted;
89
96
  private recording;
90
97
  private recordingStartTime;
98
+ private readonly subagentRunId?;
91
99
  constructor(options?: StreamProcessorOptions);
92
100
  /**
93
101
  * Set the messages array (e.g., from persisted state)
@@ -235,6 +243,31 @@ export declare class StreamProcessor {
235
243
  * Rebuilds `createdAt` when `tanstack.createdAt` is an ISO string.
236
244
  */
237
245
  private mergeMessageMetadata;
246
+ private findSubagentPart;
247
+ /**
248
+ * The direct child whose card holds `id`: the child itself, or a nested
249
+ * child inside its messages.
250
+ */
251
+ private childOwning;
252
+ /** The direct child whose messages hold this tool call. */
253
+ private childOwningToolCall;
254
+ /**
255
+ * The processor for a direct child. The card's messages are the source of
256
+ * truth. The processor re-reads them when something else replaced them.
257
+ */
258
+ private childProcessor;
259
+ /** Run `update` on a direct child, then copy its messages onto the card. */
260
+ private updateChild;
261
+ private handleSubagentStartedEvent;
262
+ private handleSubagentFinishedEvent;
263
+ private handleSubagentErrorEvent;
264
+ private patchSubagent;
265
+ /**
266
+ * Send a child's chunk to that child's processor, so the card keeps text,
267
+ * reasoning, tool calls, results, and nested children. A tool event without
268
+ * `subagentRunId` follows its `TOOL_CALL_START`.
269
+ */
270
+ private routeToChild;
238
271
  /**
239
272
  * Handle TEXT_MESSAGE_START event
240
273
  */
@@ -247,6 +280,14 @@ export declare class StreamProcessor {
247
280
  * Handle MESSAGES_SNAPSHOT event
248
281
  */
249
282
  private handleMessagesSnapshotEvent;
283
+ /**
284
+ * Put subagent cards back on a snapshot. Child wire messages (tagged with
285
+ * `subagentRunId`) become a card on the nearest assistant message before
286
+ * them, or a new assistant message when there is none. A card that the
287
+ * snapshot does not carry stays on its message, because a server snapshot
288
+ * can hold the parent history only.
289
+ */
290
+ private attachSnapshotSubagents;
250
291
  /**
251
292
  * Reconcile a freshly normalized snapshot with the pre-snapshot message
252
293
  * state so unreconstructable tool-call metadata is preserved.
@@ -364,6 +405,11 @@ export declare class StreamProcessor {
364
405
  * @see docs/chat-architecture.md#adapter-contract — Why RUN_FINISHED is mandatory
365
406
  */
366
407
  private handleRunFinishedEvent;
408
+ /**
409
+ * Apply interrupt state to tool-call parts, then fire the client events.
410
+ * A child's interrupt updates the child's card. `emit` is false there, so
411
+ * the events fire once, from the top processor.
412
+ */
367
413
  private handleInterrupts;
368
414
  private findToolCallName;
369
415
  /**