@tanstack/ai-persistence 0.6.4 → 0.6.7

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.
@@ -1,4 +1,5 @@
1
1
  import { validateReconstructChatStores } from "./types.js";
2
+ import { storedSubagentInfo } from "./subagent-runs.js";
2
3
  import { modelMessagesToUIMessages } from "@tanstack/ai";
3
4
  //#region src/reconstruct.ts
4
5
  var MAX_PAGE_SIZE = 500;
@@ -76,7 +77,7 @@ async function reconstructChat(persistence, request, options) {
76
77
  before
77
78
  }) : windowFromMessagePage(stored, pageSize);
78
79
  const body = {
79
- messages: transcript.messages,
80
+ messages: await attachSubagentCards(transcript.messages, persistence.stores.runs, messageStore, threadId, pending),
80
81
  activeRun: active ? { runId: active.runId } : null,
81
82
  interrupts: firstPending ? {
82
83
  runId: firstPending.runId,
@@ -89,6 +90,94 @@ async function reconstructChat(persistence, request, options) {
89
90
  "cache-control": "no-store"
90
91
  } });
91
92
  }
93
+ function messageRunId(message) {
94
+ const metadata = message.metadata;
95
+ if (!metadata || typeof metadata !== "object") return;
96
+ const tanstack = metadata.tanstack;
97
+ if (!tanstack || typeof tanstack !== "object") return;
98
+ const runId = tanstack.runId;
99
+ return typeof runId === "string" && runId !== "" ? runId : void 0;
100
+ }
101
+ /** Rebuild one child card from its run record and stored transcript. */
102
+ async function childCard(child, runs, messageStore, pending, depth) {
103
+ const subagentRunId = child.subagentRunId ?? child.runId;
104
+ const stored = await messageStore.loadThread(child.threadId);
105
+ const info = storedSubagentInfo(stored);
106
+ const messages = modelMessagesToUIMessages(stored.filter((message) => storedSubagentInfo([message])?.placeholder !== true));
107
+ const nested = runs.listByParentRun && depth < 8 ? await runs.listByParentRun(subagentRunId) : [];
108
+ if (nested.length > 0) {
109
+ const cards = await Promise.all(nested.map((run) => childCard(run, runs, messageStore, pending, depth + 1)));
110
+ const last = messages.findLastIndex((m) => m.role === "assistant");
111
+ if (last === -1) messages.push({
112
+ id: `child-cards:${subagentRunId}`,
113
+ role: "assistant",
114
+ parts: cards
115
+ });
116
+ else {
117
+ const host = messages[last];
118
+ if (host) messages[last] = {
119
+ ...host,
120
+ parts: [...host.parts, ...cards]
121
+ };
122
+ }
123
+ }
124
+ const failed = child.status === "failed" || child.status === "aborted";
125
+ const interruptIds = pending.filter((record) => record.payload.subagentRunId === subagentRunId).map((record) => record.interruptId);
126
+ return {
127
+ type: "subagent",
128
+ subagent: {
129
+ id: subagentRunId,
130
+ name: child.name ?? info?.name ?? "subagent",
131
+ status: failed ? "error" : child.status === "running" ? "running" : child.status === "interrupted" ? "suspended" : "finished",
132
+ ...child.parentRunId !== void 0 && { parentRunId: child.parentRunId },
133
+ ...info?.parentToolCallId !== void 0 && { parentToolCallId: info.parentToolCallId },
134
+ ...interruptIds.length > 0 && { interruptIds },
135
+ ...info?.metadata !== void 0 && { metadata: info.metadata },
136
+ messages,
137
+ ...failed && child.error ? { error: child.error } : {}
138
+ }
139
+ };
140
+ }
141
+ /**
142
+ * Put stored subagent cards back on the transcript. A routed child sits on
143
+ * the parent assistant message of its run. A child that a tool call started
144
+ * sits on the message that holds that tool call.
145
+ */
146
+ async function attachSubagentCards(messages, runs, messageStore, threadId, pending) {
147
+ if (!runs?.listByParentRun) return messages;
148
+ const parentRunIds = /* @__PURE__ */ new Set();
149
+ for (const message of messages) {
150
+ const runId = messageRunId(message);
151
+ if (runId) parentRunIds.add(runId);
152
+ }
153
+ if (messages.some((message) => message.parts.some((part) => part.type === "tool-call")) && runs.listByThread && threadId !== "") for (const run of await runs.listByThread(threadId)) parentRunIds.add(run.runId);
154
+ const cardsByRun = /* @__PURE__ */ new Map();
155
+ const cardsByToolCall = /* @__PURE__ */ new Map();
156
+ for (const runId of parentRunIds) for (const child of await runs.listByParentRun(runId)) {
157
+ const card = await childCard(child, runs, messageStore, pending, 0);
158
+ const toolCallId = card.subagent.parentToolCallId;
159
+ const target = toolCallId === void 0 ? cardsByRun : cardsByToolCall;
160
+ const key = toolCallId ?? runId;
161
+ target.set(key, [...target.get(key) ?? [], card]);
162
+ }
163
+ if (cardsByRun.size === 0 && cardsByToolCall.size === 0) return messages;
164
+ return messages.map((message) => {
165
+ if (message.role !== "assistant") return message;
166
+ const runId = messageRunId(message);
167
+ const routed = runId !== void 0 ? cardsByRun.get(runId) : void 0;
168
+ const started = message.parts.flatMap((part) => part.type === "tool-call" ? cardsByToolCall.get(part.id) ?? [] : []);
169
+ if (!routed && started.length === 0) return message;
170
+ const parts = routed ? message.parts.filter((part) => part.type !== "text") : message.parts;
171
+ return {
172
+ ...message,
173
+ parts: [
174
+ ...routed ?? [],
175
+ ...parts,
176
+ ...started
177
+ ]
178
+ };
179
+ });
180
+ }
92
181
  function parsePageSize(raw) {
93
182
  if (raw == null) return;
94
183
  const pageSize = Number(raw);
@@ -1 +1 @@
1
- {"version":3,"file":"reconstruct.js","names":[],"sources":["../../src/reconstruct.ts"],"sourcesContent":["import { modelMessagesToUIMessages } from '@tanstack/ai'\nimport type { ModelMessage, UIMessage } from '@tanstack/ai'\nimport { validateReconstructChatStores } from './types'\nimport type {\n AIPersistence,\n ChatTranscriptStores,\n MessagePage,\n MessageStore,\n} from './types'\n\nconst MAX_PAGE_SIZE = 500\n\n/**\n * The JSON body `reconstructChat` returns and a server-authoritative client\n * hydrates from on mount.\n *\n * `messages` is the stored transcript as UI messages (ready to paint).\n * `activeRun` is a cursor to a run still generating for the thread, or `null` —\n * resolved from the STABLE thread id via `stores.runs.findActiveRun`, so the\n * client learns \"there is a live run to tail\" without ever handling a run id.\n * `interrupts` is the thread's pending human-in-the-loop interrupts (tool\n * approvals, client-tool/generic waits) and the run they paused, or `null` —\n * so a reload (or another device) re-prompts the approval from the SERVER, not\n * from client storage. Resolved via `stores.interrupts.listPending`.\n * `page` is set only when the GET included a valid `limit`. `truncated` is true\n * when older UI messages exist. `cursor` is the opaque `before` token for the\n * next older window.\n */\nexport interface ReconstructedChat {\n messages: Array<UIMessage>\n activeRun: { runId: string } | null\n interrupts: {\n runId: string\n pending: Array<Record<string, unknown>>\n } | null\n page?: { truncated: false } | { truncated: true; cursor: string }\n}\n\nexport interface ReconstructChatOptions {\n /** Query parameter carrying the thread id. Defaults to `threadId`. */\n param?: string\n /**\n * Authorize access to the requested thread before loading history.\n *\n * ⚠️ Without this, any caller who knows or guesses `?threadId=` receives the\n * full transcript. Multi-user / multi-tenant deployments **must** supply\n * an authorization check (session → owned threads) or resolve a validated\n * thread id in the route and pass it via a custom `param` that only your\n * server sets.\n *\n * Return:\n * - `true` to allow the load\n * - `false` for a default `403` response\n * - a `Response` to return as-is (e.g. `401` with a body)\n */\n authorize?: (\n threadId: string,\n request: Request,\n ) => boolean | Response | Promise<boolean | Response>\n}\n\n/**\n * Build the JSON `Response` a server-authoritative client hydrates from on load\n * (see the client-persistence guide). Reads the thread id from the request query\n * (`?threadId=` by default) and returns `{ messages, activeRun, interrupts }`\n * ({@link ReconstructedChat}):\n *\n * - `messages` — the stored transcript as UI messages.\n * - `activeRun` — `{ runId }` if a run is still generating for the thread (so the\n * client tails it via the durability stream), else `null`. Resolved via the\n * required `stores.runs.findActiveRun`; `null` when the `runs` store is absent.\n * - `interrupts` — `{ runId, pending }` if the thread has pending human-in-the-loop\n * interrupts (a paused approval / wait) and the run they paused, else `null`, so\n * a reload re-prompts the decision from the server. Resolved via the optional\n * `stores.interrupts.listPending`; `null` when that store is absent.\n *\n * Paging is opt-in. A valid `?limit=` (positive integer, capped at 500) returns\n * the newest window of UI messages plus `page`. `?before=` walks to an older\n * window. Invalid `limit` (`0`, negative, NaN) is ignored and the full\n * transcript is returned. `activeRun` and `interrupts` are never paged.\n *\n * Requires `stores.messages`. Returns an empty transcript with no active run\n * and no interrupts when the thread id is missing or the thread is unknown, so\n * the caller never has to special-case a first load.\n *\n * This helper does **not** enforce tenancy by itself. Pass\n * {@link ReconstructChatOptions.authorize} (or wrap the call in your own\n * session gate) before exposing it on a public route.\n *\n * ```ts\n * export async function GET(request: Request) {\n * return reconstructChat(persistence, request, {\n * authorize: async (threadId, req) => {\n * const userId = await getSessionUserId(req)\n * return userId != null && (await userOwnsThread(userId, threadId))\n * },\n * })\n * }\n * ```\n */\nexport async function reconstructChat(\n persistence: AIPersistence<ChatTranscriptStores>,\n request: Request,\n options?: ReconstructChatOptions,\n): Promise<Response> {\n validateReconstructChatStores(persistence)\n const messageStore = persistence.stores.messages\n if (!messageStore) {\n // validateReconstructChatStores already throws; this narrows for TypeScript.\n throw new Error('reconstructChat requires stores.messages.')\n }\n\n const requestUrl = new URL(request.url)\n const param = options?.param ?? 'threadId'\n const threadId = requestUrl.searchParams.get(param) ?? ''\n const pageSize = parsePageSize(requestUrl.searchParams.get('limit'))\n const before = parseBefore(requestUrl.searchParams.get('before'))\n\n if (threadId && options?.authorize) {\n const decision = await options.authorize(threadId, request)\n if (decision instanceof Response) {\n return decision\n }\n if (!decision) {\n return new Response(JSON.stringify({ error: 'Forbidden' }), {\n status: 403,\n headers: {\n 'content-type': 'application/json',\n 'cache-control': 'no-store',\n },\n })\n }\n }\n\n // Resolve the active run BEFORE reading the transcript. `withPersistence`\n // persists the final transcript BEFORE marking a run complete, so observing\n // \"no active run\" here guarantees the transcript read below is the FINAL one.\n // Reading them in the other order opens a finish-window race: a fast run that\n // completes between the two reads would return a stale streaming snapshot with\n // `activeRun: null`, leaving the client stuck on the partial (no run to tail).\n const active = threadId\n ? await persistence.stores.runs?.findActiveRun(threadId)\n : null\n const stored =\n threadId === ''\n ? []\n : pageSize === undefined\n ? await messageStore.loadThread(threadId)\n : await messageStore.loadThread(threadId, {\n limit: pageSize + 1,\n ...(before === undefined ? {} : { before }),\n })\n // Pending interrupts for the thread, so a reload re-prompts the approval from\n // the server. Each stored `payload` is the full interrupt descriptor the\n // client hydrates; they share the run they paused.\n const pending = threadId\n ? ((await persistence.stores.interrupts?.listPending(threadId)) ?? [])\n : []\n const firstPending = pending[0]\n const isPaging = pageSize !== undefined && threadId !== ''\n const transcript = !isPaging\n ? {\n messages: modelMessagesToUIMessages(threadMessages(stored)),\n }\n : Array.isArray(stored)\n ? await windowFromArray({\n stored,\n messageStore,\n threadId,\n pageSize,\n before,\n })\n : windowFromMessagePage(stored, pageSize)\n const body: ReconstructedChat = {\n messages: transcript.messages,\n activeRun: active ? { runId: active.runId } : null,\n interrupts: firstPending\n ? {\n runId: firstPending.runId,\n pending: pending.map((record) => record.payload),\n }\n : null,\n ...('page' in transcript ? { page: transcript.page } : {}),\n }\n return new Response(JSON.stringify(body), {\n headers: {\n 'content-type': 'application/json',\n 'cache-control': 'no-store',\n },\n })\n}\n\nfunction parsePageSize(raw: string | null) {\n if (raw == null) return\n const pageSize = Number(raw)\n const isValidPageSize = Number.isInteger(pageSize) && pageSize > 0\n if (!isValidPageSize) return\n return Math.min(pageSize, MAX_PAGE_SIZE)\n}\n\nfunction parseBefore(raw: string | null) {\n if (raw == null || raw === '') return\n return raw\n}\n\nfunction threadMessages(\n loaded: Array<ModelMessage> | MessagePage,\n): Array<ModelMessage> {\n return Array.isArray(loaded) ? loaded : loaded.messages\n}\n\nfunction completePage() {\n return { truncated: false as const }\n}\n\nfunction truncatedPage(cursor: string) {\n return { truncated: true as const, cursor }\n}\n\nfunction pageFromCursor(cursor: string | undefined) {\n if (cursor === undefined || cursor === '') {\n return completePage()\n }\n return truncatedPage(cursor)\n}\n\nfunction newestUiWindow(messages: Array<UIMessage>, pageSize: number) {\n const truncated = messages.length > pageSize\n if (!truncated) {\n return { messages, page: completePage() }\n }\n const uiWindow = messages.slice(messages.length - pageSize)\n return {\n messages: uiWindow,\n page: pageFromCursor(uiWindow[0]?.id),\n }\n}\n\nfunction uiBeforeCursor(messages: Array<UIMessage>, cursor: string) {\n const cut = messages.findIndex((message) => message.id === cursor)\n if (cut === -1) return\n return messages.slice(0, cut)\n}\n\nfunction windowFromMessagePage(page: MessagePage, pageSize: number) {\n const ui = modelMessagesToUIMessages(page.messages)\n if (ui.length > pageSize) {\n // Extra slice uses a library-minted cursor. Keeping the adapter cursor\n // after dropping the oldest row would skip that row on the next GET.\n return newestUiWindow(ui, pageSize)\n }\n if (page.truncated) {\n return {\n messages: ui,\n page: pageFromCursor(page.cursor),\n }\n }\n return { messages: ui, page: completePage() }\n}\n\nasync function windowFromArray(input: {\n stored: Array<ModelMessage>\n messageStore: MessageStore\n threadId: string\n pageSize: number\n before: string | undefined\n}) {\n const { stored, messageStore, threadId, pageSize, before } = input\n if (before === undefined) {\n return newestUiWindow(modelMessagesToUIMessages(stored), pageSize)\n }\n // Array adapters own no cursor. Apply `before` to the full transcript so an\n // adapter that ignored the hint cannot return the same newest page forever.\n const full = threadMessages(await messageStore.loadThread(threadId))\n const older = uiBeforeCursor(modelMessagesToUIMessages(full), before)\n if (older === undefined) {\n return { messages: [], page: truncatedPage(before) }\n }\n return newestUiWindow(older, pageSize)\n}\n"],"mappings":";;;AAUA,IAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0FtB,eAAsB,gBACpB,aACA,SACA,SACmB;CACnB,8BAA8B,WAAW;CACzC,MAAM,eAAe,YAAY,OAAO;CACxC,IAAI,CAAC,cAEH,MAAM,IAAI,MAAM,2CAA2C;CAG7D,MAAM,aAAa,IAAI,IAAI,QAAQ,GAAG;CACtC,MAAM,QAAQ,SAAS,SAAS;CAChC,MAAM,WAAW,WAAW,aAAa,IAAI,KAAK,KAAK;CACvD,MAAM,WAAW,cAAc,WAAW,aAAa,IAAI,OAAO,CAAC;CACnE,MAAM,SAAS,YAAY,WAAW,aAAa,IAAI,QAAQ,CAAC;CAEhE,IAAI,YAAY,SAAS,WAAW;EAClC,MAAM,WAAW,MAAM,QAAQ,UAAU,UAAU,OAAO;EAC1D,IAAI,oBAAoB,UACtB,OAAO;EAET,IAAI,CAAC,UACH,OAAO,IAAI,SAAS,KAAK,UAAU,EAAE,OAAO,YAAY,CAAC,GAAG;GAC1D,QAAQ;GACR,SAAS;IACP,gBAAgB;IAChB,iBAAiB;GACnB;EACF,CAAC;CAEL;CAQA,MAAM,SAAS,WACX,MAAM,YAAY,OAAO,MAAM,cAAc,QAAQ,IACrD;CACJ,MAAM,SACJ,aAAa,KACT,CAAC,IACD,aAAa,KAAA,IACX,MAAM,aAAa,WAAW,QAAQ,IACtC,MAAM,aAAa,WAAW,UAAU;EACtC,OAAO,WAAW;EAClB,GAAI,WAAW,KAAA,IAAY,CAAC,IAAI,EAAE,OAAO;CAC3C,CAAC;CAIT,MAAM,UAAU,WACV,MAAM,YAAY,OAAO,YAAY,YAAY,QAAQ,KAAM,CAAC,IAClE,CAAC;CACL,MAAM,eAAe,QAAQ;CAE7B,MAAM,aAAa,EADF,aAAa,KAAA,KAAa,aAAa,MAEpD,EACE,UAAU,0BAA0B,eAAe,MAAM,CAAC,EAC5D,IACA,MAAM,QAAQ,MAAM,IAClB,MAAM,gBAAgB;EACpB;EACA;EACA;EACA;EACA;CACF,CAAC,IACD,sBAAsB,QAAQ,QAAQ;CAC5C,MAAM,OAA0B;EAC9B,UAAU,WAAW;EACrB,WAAW,SAAS,EAAE,OAAO,OAAO,MAAM,IAAI;EAC9C,YAAY,eACR;GACE,OAAO,aAAa;GACpB,SAAS,QAAQ,KAAK,WAAW,OAAO,OAAO;EACjD,IACA;EACJ,GAAI,UAAU,aAAa,EAAE,MAAM,WAAW,KAAK,IAAI,CAAC;CAC1D;CACA,OAAO,IAAI,SAAS,KAAK,UAAU,IAAI,GAAG,EACxC,SAAS;EACP,gBAAgB;EAChB,iBAAiB;CACnB,EACF,CAAC;AACH;AAEA,SAAS,cAAc,KAAoB;CACzC,IAAI,OAAO,MAAM;CACjB,MAAM,WAAW,OAAO,GAAG;CAE3B,IAAI,EADoB,OAAO,UAAU,QAAQ,KAAK,WAAW,IAC3C;CACtB,OAAO,KAAK,IAAI,UAAU,aAAa;AACzC;AAEA,SAAS,YAAY,KAAoB;CACvC,IAAI,OAAO,QAAQ,QAAQ,IAAI;CAC/B,OAAO;AACT;AAEA,SAAS,eACP,QACqB;CACrB,OAAO,MAAM,QAAQ,MAAM,IAAI,SAAS,OAAO;AACjD;AAEA,SAAS,eAAe;CACtB,OAAO,EAAE,WAAW,MAAe;AACrC;AAEA,SAAS,cAAc,QAAgB;CACrC,OAAO;EAAE,WAAW;EAAe;CAAO;AAC5C;AAEA,SAAS,eAAe,QAA4B;CAClD,IAAI,WAAW,KAAA,KAAa,WAAW,IACrC,OAAO,aAAa;CAEtB,OAAO,cAAc,MAAM;AAC7B;AAEA,SAAS,eAAe,UAA4B,UAAkB;CAEpE,IAAI,EADc,SAAS,SAAS,WAElC,OAAO;EAAE;EAAU,MAAM,aAAa;CAAE;CAE1C,MAAM,WAAW,SAAS,MAAM,SAAS,SAAS,QAAQ;CAC1D,OAAO;EACL,UAAU;EACV,MAAM,eAAe,SAAS,EAAE,EAAE,EAAE;CACtC;AACF;AAEA,SAAS,eAAe,UAA4B,QAAgB;CAClE,MAAM,MAAM,SAAS,WAAW,YAAY,QAAQ,OAAO,MAAM;CACjE,IAAI,QAAQ,IAAI;CAChB,OAAO,SAAS,MAAM,GAAG,GAAG;AAC9B;AAEA,SAAS,sBAAsB,MAAmB,UAAkB;CAClE,MAAM,KAAK,0BAA0B,KAAK,QAAQ;CAClD,IAAI,GAAG,SAAS,UAGd,OAAO,eAAe,IAAI,QAAQ;CAEpC,IAAI,KAAK,WACP,OAAO;EACL,UAAU;EACV,MAAM,eAAe,KAAK,MAAM;CAClC;CAEF,OAAO;EAAE,UAAU;EAAI,MAAM,aAAa;CAAE;AAC9C;AAEA,eAAe,gBAAgB,OAM5B;CACD,MAAM,EAAE,QAAQ,cAAc,UAAU,UAAU,WAAW;CAC7D,IAAI,WAAW,KAAA,GACb,OAAO,eAAe,0BAA0B,MAAM,GAAG,QAAQ;CAInE,MAAM,OAAO,eAAe,MAAM,aAAa,WAAW,QAAQ,CAAC;CACnE,MAAM,QAAQ,eAAe,0BAA0B,IAAI,GAAG,MAAM;CACpE,IAAI,UAAU,KAAA,GACZ,OAAO;EAAE,UAAU,CAAC;EAAG,MAAM,cAAc,MAAM;CAAE;CAErD,OAAO,eAAe,OAAO,QAAQ;AACvC"}
1
+ {"version":3,"file":"reconstruct.js","names":[],"sources":["../../src/reconstruct.ts"],"sourcesContent":["import { modelMessagesToUIMessages } from '@tanstack/ai'\nimport type {\n ModelMessage,\n RunRecord,\n SubagentPart,\n UIMessage,\n} from '@tanstack/ai'\nimport { storedSubagentInfo } from './subagent-runs'\nimport { validateReconstructChatStores } from './types'\nimport type {\n AIPersistence,\n ChatTranscriptStores,\n InterruptRecord,\n MessagePage,\n MessageStore,\n} from './types'\n\nconst MAX_PAGE_SIZE = 500\n\n/**\n * The JSON body `reconstructChat` returns and a server-authoritative client\n * hydrates from on mount.\n *\n * `messages` is the stored transcript as UI messages (ready to paint).\n * `activeRun` is a cursor to a run still generating for the thread, or `null` —\n * resolved from the STABLE thread id via `stores.runs.findActiveRun`, so the\n * client learns \"there is a live run to tail\" without ever handling a run id.\n * `interrupts` is the thread's pending human-in-the-loop interrupts (tool\n * approvals, client-tool/generic waits) and the run they paused, or `null` —\n * so a reload (or another device) re-prompts the approval from the SERVER, not\n * from client storage. Resolved via `stores.interrupts.listPending`.\n * `page` is set only when the GET included a valid `limit`. `truncated` is true\n * when older UI messages exist. `cursor` is the opaque `before` token for the\n * next older window.\n */\nexport interface ReconstructedChat {\n messages: Array<UIMessage>\n activeRun: { runId: string } | null\n interrupts: {\n runId: string\n pending: Array<Record<string, unknown>>\n } | null\n page?: { truncated: false } | { truncated: true; cursor: string }\n}\n\nexport interface ReconstructChatOptions {\n /** Query parameter carrying the thread id. Defaults to `threadId`. */\n param?: string\n /**\n * Authorize access to the requested thread before loading history.\n *\n * ⚠️ Without this, any caller who knows or guesses `?threadId=` receives the\n * full transcript. Multi-user / multi-tenant deployments **must** supply\n * an authorization check (session → owned threads) or resolve a validated\n * thread id in the route and pass it via a custom `param` that only your\n * server sets.\n *\n * Return:\n * - `true` to allow the load\n * - `false` for a default `403` response\n * - a `Response` to return as-is (e.g. `401` with a body)\n */\n authorize?: (\n threadId: string,\n request: Request,\n ) => boolean | Response | Promise<boolean | Response>\n}\n\n/**\n * Build the JSON `Response` a server-authoritative client hydrates from on load\n * (see the client-persistence guide). Reads the thread id from the request query\n * (`?threadId=` by default) and returns `{ messages, activeRun, interrupts }`\n * ({@link ReconstructedChat}):\n *\n * - `messages` — the stored transcript as UI messages.\n * - `activeRun` — `{ runId }` if a run is still generating for the thread (so the\n * client tails it via the durability stream), else `null`. Resolved via the\n * required `stores.runs.findActiveRun`; `null` when the `runs` store is absent.\n * - `interrupts` — `{ runId, pending }` if the thread has pending human-in-the-loop\n * interrupts (a paused approval / wait) and the run they paused, else `null`, so\n * a reload re-prompts the decision from the server. Resolved via the optional\n * `stores.interrupts.listPending`; `null` when that store is absent.\n *\n * Paging is opt-in. A valid `?limit=` (positive integer, capped at 500) returns\n * the newest window of UI messages plus `page`. `?before=` walks to an older\n * window. Invalid `limit` (`0`, negative, NaN) is ignored and the full\n * transcript is returned. `activeRun` and `interrupts` are never paged.\n *\n * Requires `stores.messages`. Returns an empty transcript with no active run\n * and no interrupts when the thread id is missing or the thread is unknown, so\n * the caller never has to special-case a first load.\n *\n * This helper does **not** enforce tenancy by itself. Pass\n * {@link ReconstructChatOptions.authorize} (or wrap the call in your own\n * session gate) before exposing it on a public route.\n *\n * ```ts\n * export async function GET(request: Request) {\n * return reconstructChat(persistence, request, {\n * authorize: async (threadId, req) => {\n * const userId = await getSessionUserId(req)\n * return userId != null && (await userOwnsThread(userId, threadId))\n * },\n * })\n * }\n * ```\n */\nexport async function reconstructChat(\n persistence: AIPersistence<ChatTranscriptStores>,\n request: Request,\n options?: ReconstructChatOptions,\n): Promise<Response> {\n validateReconstructChatStores(persistence)\n const messageStore = persistence.stores.messages\n if (!messageStore) {\n // validateReconstructChatStores already throws; this narrows for TypeScript.\n throw new Error('reconstructChat requires stores.messages.')\n }\n\n const requestUrl = new URL(request.url)\n const param = options?.param ?? 'threadId'\n const threadId = requestUrl.searchParams.get(param) ?? ''\n const pageSize = parsePageSize(requestUrl.searchParams.get('limit'))\n const before = parseBefore(requestUrl.searchParams.get('before'))\n\n if (threadId && options?.authorize) {\n const decision = await options.authorize(threadId, request)\n if (decision instanceof Response) {\n return decision\n }\n if (!decision) {\n return new Response(JSON.stringify({ error: 'Forbidden' }), {\n status: 403,\n headers: {\n 'content-type': 'application/json',\n 'cache-control': 'no-store',\n },\n })\n }\n }\n\n // Resolve the active run BEFORE reading the transcript. `withPersistence`\n // persists the final transcript BEFORE marking a run complete, so observing\n // \"no active run\" here guarantees the transcript read below is the FINAL one.\n // Reading them in the other order opens a finish-window race: a fast run that\n // completes between the two reads would return a stale streaming snapshot with\n // `activeRun: null`, leaving the client stuck on the partial (no run to tail).\n const active = threadId\n ? await persistence.stores.runs?.findActiveRun(threadId)\n : null\n const stored =\n threadId === ''\n ? []\n : pageSize === undefined\n ? await messageStore.loadThread(threadId)\n : await messageStore.loadThread(threadId, {\n limit: pageSize + 1,\n ...(before === undefined ? {} : { before }),\n })\n // Pending interrupts for the thread, so a reload re-prompts the approval from\n // the server. Each stored `payload` is the full interrupt descriptor the\n // client hydrates; they share the run they paused.\n const pending = threadId\n ? ((await persistence.stores.interrupts?.listPending(threadId)) ?? [])\n : []\n const firstPending = pending[0]\n const isPaging = pageSize !== undefined && threadId !== ''\n const transcript = !isPaging\n ? {\n messages: modelMessagesToUIMessages(threadMessages(stored)),\n }\n : Array.isArray(stored)\n ? await windowFromArray({\n stored,\n messageStore,\n threadId,\n pageSize,\n before,\n })\n : windowFromMessagePage(stored, pageSize)\n const messages = await attachSubagentCards(\n transcript.messages,\n persistence.stores.runs,\n messageStore,\n threadId,\n pending,\n )\n const body: ReconstructedChat = {\n messages,\n activeRun: active ? { runId: active.runId } : null,\n interrupts: firstPending\n ? {\n runId: firstPending.runId,\n pending: pending.map((record) => record.payload),\n }\n : null,\n ...('page' in transcript ? { page: transcript.page } : {}),\n }\n return new Response(JSON.stringify(body), {\n headers: {\n 'content-type': 'application/json',\n 'cache-control': 'no-store',\n },\n })\n}\n\nfunction messageRunId(message: UIMessage) {\n const metadata = message.metadata\n if (!metadata || typeof metadata !== 'object') return\n const tanstack = metadata.tanstack\n if (!tanstack || typeof tanstack !== 'object') return\n const runId = (tanstack as { runId?: unknown }).runId\n return typeof runId === 'string' && runId !== '' ? runId : undefined\n}\n\ntype Runs = NonNullable<ChatTranscriptStores['runs']>\n\n/** Rebuild one child card from its run record and stored transcript. */\nasync function childCard(\n child: RunRecord,\n runs: Runs,\n messageStore: MessageStore,\n pending: ReadonlyArray<InterruptRecord>,\n depth: number,\n): Promise<SubagentPart> {\n const subagentRunId = child.subagentRunId ?? child.runId\n const stored = await messageStore.loadThread(child.threadId)\n const info = storedSubagentInfo(stored)\n const messages = modelMessagesToUIMessages(\n stored.filter(\n (message) => storedSubagentInfo([message])?.placeholder !== true,\n ),\n )\n const nested =\n runs.listByParentRun && depth < 8\n ? await runs.listByParentRun(subagentRunId)\n : []\n if (nested.length > 0) {\n const cards = await Promise.all(\n nested.map((run) =>\n childCard(run, runs, messageStore, pending, depth + 1),\n ),\n )\n const last = messages.findLastIndex((m) => m.role === 'assistant')\n if (last === -1) {\n messages.push({\n id: `child-cards:${subagentRunId}`,\n role: 'assistant',\n parts: cards,\n })\n } else {\n const host = messages[last]\n if (host) messages[last] = { ...host, parts: [...host.parts, ...cards] }\n }\n }\n const failed = child.status === 'failed' || child.status === 'aborted'\n const interruptIds = pending\n .filter((record) => record.payload.subagentRunId === subagentRunId)\n .map((record) => record.interruptId)\n return {\n type: 'subagent',\n subagent: {\n id: subagentRunId,\n name: child.name ?? info?.name ?? 'subagent',\n status: failed\n ? 'error'\n : child.status === 'running'\n ? 'running'\n : child.status === 'interrupted'\n ? 'suspended'\n : 'finished',\n ...(child.parentRunId !== undefined && {\n parentRunId: child.parentRunId,\n }),\n ...(info?.parentToolCallId !== undefined && {\n parentToolCallId: info.parentToolCallId,\n }),\n ...(interruptIds.length > 0 && { interruptIds }),\n ...(info?.metadata !== undefined && { metadata: info.metadata }),\n messages,\n ...(failed && child.error ? { error: child.error } : {}),\n },\n }\n}\n\n/**\n * Put stored subagent cards back on the transcript. A routed child sits on\n * the parent assistant message of its run. A child that a tool call started\n * sits on the message that holds that tool call.\n */\nasync function attachSubagentCards(\n messages: Array<UIMessage>,\n runs: ChatTranscriptStores['runs'],\n messageStore: MessageStore,\n threadId: string,\n pending: ReadonlyArray<InterruptRecord>,\n) {\n if (!runs?.listByParentRun) return messages\n const parentRunIds = new Set<string>()\n for (const message of messages) {\n const runId = messageRunId(message)\n if (runId) parentRunIds.add(runId)\n }\n const hasToolCalls = messages.some((message) =>\n message.parts.some((part) => part.type === 'tool-call'),\n )\n if (hasToolCalls && runs.listByThread && threadId !== '') {\n for (const run of await runs.listByThread(threadId)) {\n parentRunIds.add(run.runId)\n }\n }\n\n const cardsByRun = new Map<string, Array<SubagentPart>>()\n const cardsByToolCall = new Map<string, Array<SubagentPart>>()\n for (const runId of parentRunIds) {\n for (const child of await runs.listByParentRun(runId)) {\n const card = await childCard(child, runs, messageStore, pending, 0)\n const toolCallId = card.subagent.parentToolCallId\n const target = toolCallId === undefined ? cardsByRun : cardsByToolCall\n const key = toolCallId ?? runId\n target.set(key, [...(target.get(key) ?? []), card])\n }\n }\n if (cardsByRun.size === 0 && cardsByToolCall.size === 0) return messages\n\n return messages.map((message) => {\n if (message.role !== 'assistant') return message\n const runId = messageRunId(message)\n const routed = runId !== undefined ? cardsByRun.get(runId) : undefined\n const started = message.parts.flatMap((part) =>\n part.type === 'tool-call' ? (cardsByToolCall.get(part.id) ?? []) : [],\n )\n if (!routed && started.length === 0) return message\n // A routed parent message holds only the children's text. The cards\n // replace it.\n const parts = routed\n ? message.parts.filter((part) => part.type !== 'text')\n : message.parts\n return {\n ...message,\n parts: [...(routed ?? []), ...parts, ...started],\n }\n })\n}\n\nfunction parsePageSize(raw: string | null) {\n if (raw == null) return\n const pageSize = Number(raw)\n const isValidPageSize = Number.isInteger(pageSize) && pageSize > 0\n if (!isValidPageSize) return\n return Math.min(pageSize, MAX_PAGE_SIZE)\n}\n\nfunction parseBefore(raw: string | null) {\n if (raw == null || raw === '') return\n return raw\n}\n\nfunction threadMessages(\n loaded: Array<ModelMessage> | MessagePage,\n): Array<ModelMessage> {\n return Array.isArray(loaded) ? loaded : loaded.messages\n}\n\nfunction completePage() {\n return { truncated: false as const }\n}\n\nfunction truncatedPage(cursor: string) {\n return { truncated: true as const, cursor }\n}\n\nfunction pageFromCursor(cursor: string | undefined) {\n if (cursor === undefined || cursor === '') {\n return completePage()\n }\n return truncatedPage(cursor)\n}\n\nfunction newestUiWindow(messages: Array<UIMessage>, pageSize: number) {\n const truncated = messages.length > pageSize\n if (!truncated) {\n return { messages, page: completePage() }\n }\n const uiWindow = messages.slice(messages.length - pageSize)\n return {\n messages: uiWindow,\n page: pageFromCursor(uiWindow[0]?.id),\n }\n}\n\nfunction uiBeforeCursor(messages: Array<UIMessage>, cursor: string) {\n const cut = messages.findIndex((message) => message.id === cursor)\n if (cut === -1) return\n return messages.slice(0, cut)\n}\n\nfunction windowFromMessagePage(page: MessagePage, pageSize: number) {\n const ui = modelMessagesToUIMessages(page.messages)\n if (ui.length > pageSize) {\n // Extra slice uses a library-minted cursor. Keeping the adapter cursor\n // after dropping the oldest row would skip that row on the next GET.\n return newestUiWindow(ui, pageSize)\n }\n if (page.truncated) {\n return {\n messages: ui,\n page: pageFromCursor(page.cursor),\n }\n }\n return { messages: ui, page: completePage() }\n}\n\nasync function windowFromArray(input: {\n stored: Array<ModelMessage>\n messageStore: MessageStore\n threadId: string\n pageSize: number\n before: string | undefined\n}) {\n const { stored, messageStore, threadId, pageSize, before } = input\n if (before === undefined) {\n return newestUiWindow(modelMessagesToUIMessages(stored), pageSize)\n }\n // Array adapters own no cursor. Apply `before` to the full transcript so an\n // adapter that ignored the hint cannot return the same newest page forever.\n const full = threadMessages(await messageStore.loadThread(threadId))\n const older = uiBeforeCursor(modelMessagesToUIMessages(full), before)\n if (older === undefined) {\n return { messages: [], page: truncatedPage(before) }\n }\n return newestUiWindow(older, pageSize)\n}\n"],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ import { Interrupt, ModelMessage, RunAgentResumeItem, RunStore, StreamChunk, SubagentWireInfo, UIMessage } from '@tanstack/ai';
2
+ import { InterruptStore, MessageStore } from './types.js';
3
+ /**
4
+ * Card data for a stored child transcript. It rides on the first message, in
5
+ * `metadata.tanstack.subagent`, the same shape the wire uses.
6
+ */
7
+ export declare function storedSubagentInfo(messages: ReadonlyArray<ModelMessage>): SubagentWireInfo | undefined;
8
+ export declare function createSubagentRunRecorder(stores: {
9
+ messages: MessageStore;
10
+ runs?: RunStore;
11
+ interrupts?: InterruptStore;
12
+ /** Minimum milliseconds between writes while a child streams. */
13
+ intervalMs?: number;
14
+ }): {
15
+ start(input: {
16
+ threadId: string;
17
+ runId: string;
18
+ messages: ReadonlyArray<UIMessage | ModelMessage>;
19
+ resume?: ReadonlyArray<RunAgentResumeItem>;
20
+ }): Promise<void>;
21
+ chunk(input: {
22
+ threadId: string;
23
+ runId: string;
24
+ chunk: StreamChunk;
25
+ }): Promise<void>;
26
+ suspend(input: {
27
+ threadId: string;
28
+ runId: string;
29
+ interrupts: ReadonlyArray<Interrupt>;
30
+ }): Promise<void>;
31
+ finish(input: {
32
+ threadId: string;
33
+ runId: string;
34
+ }): Promise<void>;
35
+ abort(input: {
36
+ threadId: string;
37
+ runId: string;
38
+ error?: unknown;
39
+ }): Promise<void>;
40
+ };
@@ -0,0 +1,366 @@
1
+ import { mergeStoredMessages } from "./merge-stored.js";
2
+ import { StreamProcessor, convertMessagesToModelMessages, modelMessagesToUIMessages, subagentHostMessageId, wireSubagentInfo } from "@tanstack/ai";
3
+ //#region src/subagent-runs.ts
4
+ function withSubagentInfo(metadata, info) {
5
+ const source = metadata != null && typeof metadata === "object" ? metadata : {};
6
+ const tanstack = source.tanstack != null && typeof source.tanstack === "object" ? source.tanstack : {};
7
+ return {
8
+ ...source,
9
+ tanstack: {
10
+ ...tanstack,
11
+ subagent: info
12
+ }
13
+ };
14
+ }
15
+ function childStoreId(subagentRunId) {
16
+ return `subagent:${subagentRunId}`;
17
+ }
18
+ function readSubagentRunId(chunk) {
19
+ if (!("subagentRunId" in chunk)) return;
20
+ const id = chunk.subagentRunId;
21
+ return typeof id === "string" && id !== "" ? id : void 0;
22
+ }
23
+ function assistantId(runId) {
24
+ return subagentHostMessageId(runId);
25
+ }
26
+ function messageText(message) {
27
+ return typeof message.content === "string" ? message.content : "";
28
+ }
29
+ function readModelRunId(message) {
30
+ const metadata = message.metadata;
31
+ if (metadata == null || typeof metadata !== "object") return;
32
+ if (!("tanstack" in metadata)) return;
33
+ const tanstack = metadata.tanstack;
34
+ if (tanstack == null || typeof tanstack !== "object") return;
35
+ if (!("runId" in tanstack)) return;
36
+ const runId = tanstack.runId;
37
+ return typeof runId === "string" && runId !== "" ? runId : void 0;
38
+ }
39
+ function withRunId(message, runId) {
40
+ const metadata = message.metadata;
41
+ const tanstack = metadata != null && typeof metadata === "object" && "tanstack" in metadata && metadata.tanstack != null && typeof metadata.tanstack === "object" ? metadata.tanstack : {};
42
+ return {
43
+ ...message,
44
+ metadata: {
45
+ ...metadata,
46
+ tanstack: {
47
+ ...tanstack,
48
+ runId
49
+ }
50
+ }
51
+ };
52
+ }
53
+ function sameChildText(stored, incoming) {
54
+ if (stored === "" || incoming === "") return false;
55
+ if (stored === incoming) return true;
56
+ const blocks = stored.split("\n\n").filter((block) => block.includes(":\n"));
57
+ if (blocks.length === 0) return false;
58
+ return blocks.every((block) => incoming.includes(block));
59
+ }
60
+ function keepSubagentRunIds(stored, merged) {
61
+ const dropped = stored.filter((message) => {
62
+ if (readModelRunId(message) === void 0) return false;
63
+ return !merged.some((item) => item.id !== void 0 && item.id === message.id);
64
+ });
65
+ const turnOf = (list, index) => list.slice(0, index).findLast((message) => message.role === "user")?.id;
66
+ const used = /* @__PURE__ */ new Set();
67
+ for (const previous of dropped) {
68
+ const runId = readModelRunId(previous);
69
+ if (runId === void 0) continue;
70
+ if (merged.some((message) => readModelRunId(message) === runId)) continue;
71
+ const previousText = messageText(previous);
72
+ const previousTurn = turnOf(stored, stored.indexOf(previous));
73
+ const index = merged.findIndex((message, messageIndex) => {
74
+ if (used.has(messageIndex)) return false;
75
+ if (message.role !== "assistant") return false;
76
+ if (readModelRunId(message) !== void 0) return false;
77
+ if (previousText === "") return previousTurn !== void 0 && turnOf(merged, messageIndex) === previousTurn && merged[messageIndex - 1]?.role === "user";
78
+ return sameChildText(previousText, messageText(message));
79
+ });
80
+ if (index === -1) continue;
81
+ const host = merged[index];
82
+ if (!host) continue;
83
+ used.add(index);
84
+ merged[index] = withRunId(host, runId);
85
+ }
86
+ return merged;
87
+ }
88
+ function childText(messages) {
89
+ return messages.flatMap((message) => message.role === "assistant" ? message.parts.flatMap((part) => part.type === "text" && part.content.trim() !== "" ? [part.content.trim()] : []) : []).join("\n\n");
90
+ }
91
+ function withoutCards(messages) {
92
+ return messages.map((message) => ({
93
+ ...message,
94
+ parts: message.parts.filter((part) => part.type !== "subagent")
95
+ }));
96
+ }
97
+ /**
98
+ * Card data for a stored child transcript. It rides on the first message, in
99
+ * `metadata.tanstack.subagent`, the same shape the wire uses.
100
+ */
101
+ function storedSubagentInfo(messages) {
102
+ return wireSubagentInfo(messages[0]);
103
+ }
104
+ function createSubagentRunRecorder(stores) {
105
+ const children = /* @__PURE__ */ new Map();
106
+ const intervalMs = stores.intervalMs ?? 1e3;
107
+ const answered = /* @__PURE__ */ new Map();
108
+ const parentSavedAt = /* @__PURE__ */ new Map();
109
+ async function loadMessages(threadId) {
110
+ return stores.messages.loadThread(threadId);
111
+ }
112
+ function lineage(subagentRunId) {
113
+ const notes = [];
114
+ let id = subagentRunId;
115
+ for (let depth = 0; id !== void 0 && depth < 64; depth++) {
116
+ const note = children.get(id);
117
+ if (!note) break;
118
+ notes.push(note);
119
+ id = note.parentSubagentRunId;
120
+ }
121
+ return notes;
122
+ }
123
+ async function saveChild(subagentRunId) {
124
+ const note = children.get(subagentRunId);
125
+ if (!note) return;
126
+ note.savedAt = Date.now();
127
+ const info = {
128
+ name: note.name,
129
+ status: note.status,
130
+ ...note.parentSubagentRunId !== void 0 && { parentSubagentRunId: note.parentSubagentRunId },
131
+ ...note.parentToolCallId !== void 0 && { parentToolCallId: note.parentToolCallId },
132
+ ...note.interruptIds !== void 0 && { interruptIds: note.interruptIds },
133
+ ...note.error !== void 0 && { error: note.error },
134
+ ...note.metadata !== void 0 && { metadata: note.metadata }
135
+ };
136
+ const [first, ...rest] = convertMessagesToModelMessages(withoutCards(note.processor.getMessages()));
137
+ const head = first ? {
138
+ ...first,
139
+ metadata: withSubagentInfo(first.metadata, info)
140
+ } : {
141
+ id: `child:${subagentRunId}`,
142
+ role: "assistant",
143
+ content: "",
144
+ metadata: withSubagentInfo(void 0, {
145
+ ...info,
146
+ placeholder: true
147
+ })
148
+ };
149
+ await stores.messages.saveThread(childStoreId(subagentRunId), [head, ...rest]);
150
+ }
151
+ async function saveParent(threadId, runId) {
152
+ const notes = [...children.values()].filter((note) => note.parentRunId === runId && note.parentSubagentRunId === void 0 && note.parentToolCallId === void 0);
153
+ if (notes.length === 0) return;
154
+ const content = notes.map((note) => {
155
+ const text = childText(note.processor.getMessages());
156
+ return text === "" ? "" : `${note.name}:\n${text}`;
157
+ }).filter((block) => block !== "").join("\n\n");
158
+ const stored = await loadMessages(threadId);
159
+ const index = stored.findIndex((message) => readModelRunId(message) === runId);
160
+ const host = stored[index];
161
+ if (host) {
162
+ const next = [...stored];
163
+ next[index] = {
164
+ ...host,
165
+ content
166
+ };
167
+ await stores.messages.saveThread(threadId, next);
168
+ return;
169
+ }
170
+ const assistant = {
171
+ id: assistantId(runId),
172
+ role: "assistant",
173
+ content,
174
+ metadata: { tanstack: { runId } }
175
+ };
176
+ await stores.messages.saveThread(threadId, [...stored, assistant]);
177
+ }
178
+ async function startChild(input, chunk) {
179
+ const id = chunk.subagentRunId;
180
+ const record = await stores.runs?.createOrResume({
181
+ runId: id,
182
+ threadId: childStoreId(id),
183
+ startedAt: Date.now(),
184
+ parentRunId: chunk.parentSubagentRunId ?? input.runId,
185
+ subagentRunId: id,
186
+ name: chunk.name
187
+ });
188
+ const existing = children.get(id);
189
+ if (existing) {
190
+ existing.threadId = input.threadId;
191
+ existing.runId = input.runId;
192
+ existing.status = "running";
193
+ delete existing.interruptIds;
194
+ if (chunk.metadata !== void 0) existing.metadata = chunk.metadata;
195
+ } else {
196
+ const stored = (await loadMessages(childStoreId(id))).filter((message) => storedSubagentInfo([message])?.placeholder !== true);
197
+ children.set(id, {
198
+ name: chunk.name,
199
+ threadId: input.threadId,
200
+ runId: input.runId,
201
+ savedAt: 0,
202
+ parentRunId: record?.parentRunId ?? chunk.parentSubagentRunId ?? input.runId,
203
+ ...chunk.parentSubagentRunId !== void 0 && { parentSubagentRunId: chunk.parentSubagentRunId },
204
+ ...chunk.parentToolCallId !== void 0 && { parentToolCallId: chunk.parentToolCallId },
205
+ ...chunk.metadata !== void 0 && { metadata: chunk.metadata },
206
+ processor: new StreamProcessor({
207
+ subagentRunId: id,
208
+ initialMessages: modelMessagesToUIMessages(stored)
209
+ }),
210
+ status: "running"
211
+ });
212
+ }
213
+ if (record && record.status !== "running") await stores.runs?.update(id, { status: "running" });
214
+ if (chunk.parentSubagentRunId !== void 0) children.get(chunk.parentSubagentRunId)?.processor.processChunk(chunk);
215
+ await saveChild(id);
216
+ }
217
+ async function settleChild(chunk) {
218
+ const id = chunk.subagentRunId;
219
+ const note = children.get(id);
220
+ if (!note) return;
221
+ try {
222
+ await writeSettled(note, id, chunk);
223
+ } finally {
224
+ if (note.parentSubagentRunId !== void 0 || note.parentToolCallId !== void 0) children.delete(id);
225
+ }
226
+ }
227
+ async function writeSettled(note, id, chunk) {
228
+ note.processor.finalizeStream();
229
+ if (note.parentSubagentRunId !== void 0) children.get(note.parentSubagentRunId)?.processor.processChunk(chunk);
230
+ if (chunk.type === "SUBAGENT_ERROR") {
231
+ const stopped = chunk.message === "Stopped";
232
+ note.status = "error";
233
+ note.error = { message: chunk.message };
234
+ await saveChild(id);
235
+ await stores.runs?.update(id, {
236
+ status: stopped ? "aborted" : "failed",
237
+ finishedAt: Date.now(),
238
+ ...!stopped ? { error: { message: chunk.message } } : {}
239
+ });
240
+ return;
241
+ }
242
+ if (chunk.outcome?.type === "suspended") {
243
+ note.status = "suspended";
244
+ note.interruptIds = chunk.outcome.interruptIds ?? [];
245
+ await saveChild(id);
246
+ await stores.runs?.update(id, { status: "interrupted" });
247
+ return;
248
+ }
249
+ note.status = "finished";
250
+ await saveChild(id);
251
+ await stores.runs?.update(id, {
252
+ status: "completed",
253
+ finishedAt: Date.now()
254
+ });
255
+ }
256
+ async function commitAnswers(runId) {
257
+ const entries = answered.get(runId) ?? [];
258
+ answered.delete(runId);
259
+ for (const entry of entries) if (entry.status === "cancelled") await stores.interrupts?.cancel(entry.interruptId);
260
+ else await stores.interrupts?.resolve(entry.interruptId, entry.payload);
261
+ }
262
+ /** The notes a run fed. */
263
+ function notesOf(runId) {
264
+ return [...children].filter(([, note]) => note.runId === runId);
265
+ }
266
+ function forget(runId) {
267
+ for (const [id] of notesOf(runId)) children.delete(id);
268
+ parentSavedAt.delete(runId);
269
+ }
270
+ async function settleOpenChildren(runId, status, error) {
271
+ for (const [subagentRunId] of notesOf(runId)) {
272
+ await saveChild(subagentRunId);
273
+ const current = await stores.runs?.get(subagentRunId);
274
+ if (current && current.status !== "running") continue;
275
+ await stores.runs?.update(subagentRunId, {
276
+ status,
277
+ finishedAt: Date.now(),
278
+ ...error ? { error } : {}
279
+ });
280
+ }
281
+ }
282
+ /** Write the parent messages of this thread's routed children. */
283
+ async function saveParents(threadId) {
284
+ const runIds = new Set([...children.values()].filter((note) => note.threadId === threadId).map((note) => note.parentRunId));
285
+ for (const runId of runIds) await saveParent(threadId, runId);
286
+ }
287
+ return {
288
+ async start(input) {
289
+ await stores.runs?.createOrResume({
290
+ runId: input.runId,
291
+ threadId: input.threadId,
292
+ startedAt: Date.now()
293
+ });
294
+ if (input.resume?.length) answered.set(input.runId, [...input.resume]);
295
+ const incoming = convertMessagesToModelMessages([...input.messages]);
296
+ const stored = await loadMessages(input.threadId);
297
+ const merged = mergeStoredMessages(stored, incoming);
298
+ await stores.messages.saveThread(input.threadId, keepSubagentRunIds(stored, merged));
299
+ },
300
+ async chunk(input) {
301
+ const chunk = input.chunk;
302
+ if (chunk.type === "SUBAGENT_STARTED") {
303
+ await startChild(input, chunk);
304
+ return;
305
+ }
306
+ if (chunk.type === "SUBAGENT_FINISHED" || chunk.type === "SUBAGENT_ERROR") {
307
+ await settleChild(chunk);
308
+ await saveParents(input.threadId);
309
+ return;
310
+ }
311
+ const subagentRunId = readSubagentRunId(chunk);
312
+ if (!subagentRunId) return;
313
+ const notes = lineage(subagentRunId);
314
+ if (notes.length === 0) return;
315
+ for (const note of notes) note.processor.processChunk(chunk);
316
+ const now = Date.now();
317
+ const note = notes[0];
318
+ if (note && now - note.savedAt >= intervalMs) await saveChild(subagentRunId);
319
+ if (chunk.type === "TEXT_MESSAGE_CONTENT" && now - (parentSavedAt.get(input.runId) ?? 0) >= intervalMs) {
320
+ parentSavedAt.set(input.runId, now);
321
+ await saveParents(input.threadId);
322
+ }
323
+ },
324
+ async suspend(input) {
325
+ await commitAnswers(input.runId);
326
+ await saveParents(input.threadId);
327
+ for (const [subagentRunId] of notesOf(input.runId)) await saveChild(subagentRunId);
328
+ for (const interrupt of input.interrupts) await stores.interrupts?.create({
329
+ interruptId: interrupt.id,
330
+ runId: input.runId,
331
+ threadId: input.threadId,
332
+ requestedAt: Date.now(),
333
+ payload: { ...interrupt }
334
+ });
335
+ await stores.runs?.update(input.runId, { status: "interrupted" });
336
+ forget(input.runId);
337
+ },
338
+ async finish(input) {
339
+ await commitAnswers(input.runId);
340
+ await saveParents(input.threadId);
341
+ await settleOpenChildren(input.runId, "completed");
342
+ await stores.runs?.update(input.runId, {
343
+ status: "completed",
344
+ finishedAt: Date.now()
345
+ });
346
+ forget(input.runId);
347
+ },
348
+ async abort(input) {
349
+ const aborted = input.error instanceof Error && input.error.name === "AbortError";
350
+ const message = input.error instanceof Error ? input.error.message : "Run failed";
351
+ await settleOpenChildren(input.runId, aborted ? "aborted" : "failed", aborted ? void 0 : { message });
352
+ await saveParents(input.threadId);
353
+ await stores.runs?.update(input.runId, {
354
+ status: aborted ? "aborted" : "failed",
355
+ finishedAt: Date.now(),
356
+ ...!aborted ? { error: { message } } : {}
357
+ });
358
+ answered.delete(input.runId);
359
+ forget(input.runId);
360
+ }
361
+ };
362
+ }
363
+ //#endregion
364
+ export { createSubagentRunRecorder, storedSubagentInfo };
365
+
366
+ //# sourceMappingURL=subagent-runs.js.map