@langchain/langgraph-sdk 1.9.5 → 1.9.6

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 (55) hide show
  1. package/dist/client/stream/index.cjs +11 -3
  2. package/dist/client/stream/index.cjs.map +1 -1
  3. package/dist/client/stream/index.d.cts.map +1 -1
  4. package/dist/client/stream/index.d.ts.map +1 -1
  5. package/dist/client/stream/index.js +11 -3
  6. package/dist/client/stream/index.js.map +1 -1
  7. package/dist/headless-tools.cjs +128 -4
  8. package/dist/headless-tools.cjs.map +1 -1
  9. package/dist/headless-tools.d.cts +9 -1
  10. package/dist/headless-tools.d.cts.map +1 -1
  11. package/dist/headless-tools.d.ts +9 -1
  12. package/dist/headless-tools.d.ts.map +1 -1
  13. package/dist/headless-tools.js +126 -5
  14. package/dist/headless-tools.js.map +1 -1
  15. package/dist/index.cjs +1 -0
  16. package/dist/index.d.cts +2 -2
  17. package/dist/index.d.ts +2 -2
  18. package/dist/index.js +2 -2
  19. package/dist/stream/controller.cjs +26 -15
  20. package/dist/stream/controller.cjs.map +1 -1
  21. package/dist/stream/controller.d.cts +3 -1
  22. package/dist/stream/controller.d.cts.map +1 -1
  23. package/dist/stream/controller.d.ts +3 -1
  24. package/dist/stream/controller.d.ts.map +1 -1
  25. package/dist/stream/controller.js +27 -16
  26. package/dist/stream/controller.js.map +1 -1
  27. package/dist/stream/discovery/subagents.cjs +13 -0
  28. package/dist/stream/discovery/subagents.cjs.map +1 -1
  29. package/dist/stream/discovery/subagents.d.cts +5 -0
  30. package/dist/stream/discovery/subagents.d.cts.map +1 -1
  31. package/dist/stream/discovery/subagents.d.ts +5 -0
  32. package/dist/stream/discovery/subagents.d.ts.map +1 -1
  33. package/dist/stream/discovery/subagents.js +13 -0
  34. package/dist/stream/discovery/subagents.js.map +1 -1
  35. package/dist/stream/discovery/subgraphs.cjs +13 -0
  36. package/dist/stream/discovery/subgraphs.cjs.map +1 -1
  37. package/dist/stream/discovery/subgraphs.d.cts +5 -0
  38. package/dist/stream/discovery/subgraphs.d.cts.map +1 -1
  39. package/dist/stream/discovery/subgraphs.d.ts +5 -0
  40. package/dist/stream/discovery/subgraphs.d.ts.map +1 -1
  41. package/dist/stream/discovery/subgraphs.js +13 -0
  42. package/dist/stream/discovery/subgraphs.js.map +1 -1
  43. package/dist/stream/submit-coordinator.cjs +10 -12
  44. package/dist/stream/submit-coordinator.cjs.map +1 -1
  45. package/dist/stream/submit-coordinator.d.cts.map +1 -1
  46. package/dist/stream/submit-coordinator.d.ts.map +1 -1
  47. package/dist/stream/submit-coordinator.js +10 -12
  48. package/dist/stream/submit-coordinator.js.map +1 -1
  49. package/dist/stream/tool-calls.cjs +39 -2
  50. package/dist/stream/tool-calls.cjs.map +1 -1
  51. package/dist/stream/tool-calls.js +38 -3
  52. package/dist/stream/tool-calls.js.map +1 -1
  53. package/dist/stream/types.d.cts +2 -2
  54. package/dist/stream/types.d.ts +2 -2
  55. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"subagents.js","names":["#onToolEvent","#onValuesEvent","#upsertTaskToolCall","#commit","#map","#bindNamespaceByTaskInput","#recordObservedWorkNamespace","#recordTaskNamespaceCandidate","#observedOwnNamespaces","#toolCallIdByTaskInput","#taskIdByObservedNamespace"],"sources":["../../../src/stream/discovery/subagents.ts"],"sourcesContent":["/**\n * Root-scoped subagent discovery.\n *\n * Populates a `Map<callId, SubagentDiscoverySnapshot>` by watching\n * `task` tool calls on the root subscription. No content channels\n * (subagent messages, tool calls, extensions) are opened here — that\n * layer is driven by selector hooks via the\n * {@link ChannelRegistry}, keyed on `SubagentDiscoverySnapshot.namespace`.\n *\n * Discovery data this runner populates per subagent:\n * - id, name, namespace, parentId, depth\n * - status (`running` | `complete` | `error`)\n * - taskInput / output / error / startedAt / completedAt\n *\n * The runner is fed events by the {@link StreamController}'s root\n * subscription; it does not open subscriptions of its own.\n */\nimport type { Event, ToolsEvent, ValuesEvent } from \"@langchain/protocol\";\nimport { StreamStore } from \"../store.js\";\nimport type { SubagentDiscoverySnapshot } from \"../types.js\";\nimport {\n isConcreteToolNamespace,\n isRootNamespace,\n isToolNamespaceSegment,\n namespaceKey,\n} from \"../namespace.js\";\n\nexport type SubagentMap = ReadonlyMap<string, SubagentDiscoverySnapshot>;\n\ninterface MutableSubagent {\n id: string;\n name: string;\n namespace: readonly string[];\n parentId: string | null;\n depth: number;\n status: \"running\" | \"complete\" | \"error\";\n taskInput: string | undefined;\n output: unknown;\n error: string | undefined;\n startedAt: Date;\n completedAt: Date | null;\n}\n\nexport class SubagentDiscovery {\n readonly store = new StreamStore<SubagentMap>(new Map());\n #map = new Map<string, MutableSubagent>();\n #taskIdByObservedNamespace = new Map<string, string>();\n #observedOwnNamespaces = new Set<string>();\n // Index from `taskInput` (the `description` arg) to a FIFO queue of\n // pending parent `tool_call_id`s. Bridges the wire's missing link\n // between a deepagents `task` dispatch and its subagent execution\n // namespace — the server seeds the subagent's first HumanMessage\n // with `taskInput` verbatim, so an exact-equality lookup is\n // deterministic. The queue (not a single value) handles the case\n // where the coordinator dispatches N task calls with identical\n // descriptions; pregel preserves dispatch order across executions,\n // so FIFO pop attributes them correctly.\n #toolCallIdByTaskInput = new Map<string, string[]>();\n\n /** Feed a single root event. Non-discovery events are ignored. */\n push(event: Event): void {\n if (event.method === \"tools\") {\n this.#onToolEvent(event as ToolsEvent);\n } else if (event.method === \"values\") {\n this.#onValuesEvent(event as ValuesEvent);\n }\n }\n\n /** Current snapshot map. */\n get snapshot(): SubagentMap {\n return this.store.getSnapshot();\n }\n\n discoverFromMessage(message: unknown, namespace: readonly string[]): void {\n let changed = false;\n for (const toolCall of getTaskToolCalls(message)) {\n changed =\n this.#upsertTaskToolCall(toolCall.id, toolCall.input, namespace) ||\n changed;\n }\n if (changed) this.#commit();\n }\n\n #commit(): void {\n // Rebuild as a fresh Map so React / useSyncExternalStore sees a\n // new reference on every change.\n this.store.setValue(\n new Map(\n [...this.#map.values()].map((entry) => [entry.id, toSnapshot(entry)])\n )\n );\n }\n\n #onToolEvent(event: ToolsEvent): void {\n const data = event.params.data;\n const toolCallId = (data as { tool_call_id?: string }).tool_call_id;\n const toolName = (data as { tool_name?: string }).tool_name;\n\n if (data.event === \"tool-started\" && toolName === \"task\") {\n const input = parseTaskInput((data as { input?: unknown }).input);\n if (toolCallId == null) return;\n this.#upsertTaskToolCall(toolCallId, input, event.params.namespace);\n this.#commit();\n return;\n }\n\n if (toolCallId == null) return;\n const entry = this.#map.get(toolCallId);\n if (entry == null) return;\n\n if (data.event === \"tool-finished\") {\n entry.status = \"complete\";\n entry.output = (data as { output?: unknown }).output;\n entry.completedAt = new Date();\n this.#commit();\n return;\n }\n\n if (data.event === \"tool-error\") {\n entry.status = \"error\";\n entry.error = (data as { message?: string }).message ?? \"Subagent failed\";\n entry.completedAt = new Date();\n this.#commit();\n }\n }\n\n #onValuesEvent(event: ValuesEvent): void {\n const data = event.params.data;\n if (data == null || typeof data !== \"object\" || Array.isArray(data)) return;\n const messages = (data as { messages?: unknown }).messages;\n if (!Array.isArray(messages)) return;\n\n // If a `tools:<id>` namespace's first HumanMessage matches a known\n // taskInput, that's the subagent's execution scope. Record the\n // binding so `#recordObservedWorkNamespace` can promote it.\n this.#bindNamespaceByTaskInput(event.params.namespace, messages);\n\n let changed = this.#recordObservedWorkNamespace(event.params.namespace);\n for (const message of messages) {\n for (const toolCall of getTaskToolCalls(message)) {\n changed =\n this.#upsertTaskToolCall(\n toolCall.id,\n toolCall.input,\n event.params.namespace\n ) || changed;\n }\n\n const toolCallId = getToolMessageCallId(message);\n if (toolCallId == null) continue;\n const existing = this.#map.get(toolCallId);\n if (existing == null) continue;\n existing.status = \"complete\";\n existing.output = message;\n existing.completedAt = new Date();\n changed = true;\n }\n if (changed) this.#commit();\n }\n\n #upsertTaskToolCall(\n toolCallId: string,\n input: { description?: string; subagent_type?: string },\n eventNamespace: readonly string[]\n ): boolean {\n const namespace = taskWorkNamespace(toolCallId, eventNamespace);\n const existing = this.#map.get(toolCallId);\n if (existing != null) {\n let changed = false;\n this.#recordTaskNamespaceCandidate(toolCallId, eventNamespace);\n const nextName = input.subagent_type ?? existing.name;\n const nextTaskInput = input.description ?? existing.taskInput;\n if (existing.name !== nextName) {\n existing.name = nextName;\n changed = true;\n }\n if (existing.taskInput !== nextTaskInput) {\n existing.taskInput = nextTaskInput;\n changed = true;\n }\n const namespaceKeyed = namespaceKey(existing.namespace);\n const ownNamespaceKey = `tools:${toolCallId}`;\n const nextNamespaceKey = namespaceKey(namespace);\n if (\n isConcreteToolNamespace(eventNamespace) ||\n namespaceKeyed === ownNamespaceKey\n ) {\n // A wrapper task tool event can arrive under an execution namespace\n // like `tools:<uuid>`, while the subagent's actual message state is\n // under `tools:<tool_call_id>`. Once discovery has observed the own\n // namespace carrying state, do not demote it back to the wrapper\n // namespace.\n if (\n namespaceKeyed === ownNamespaceKey &&\n nextNamespaceKey !== ownNamespaceKey &&\n this.#observedOwnNamespaces.has(toolCallId)\n ) {\n return changed;\n }\n if (namespaceKeyed !== nextNamespaceKey) {\n existing.namespace = namespace;\n changed = true;\n }\n }\n if (existing.status !== \"complete\" && existing.status !== \"error\") {\n if (existing.status !== \"running\") {\n existing.status = \"running\";\n changed = true;\n }\n }\n return changed;\n }\n\n // Prefer the namespace where the task is first observed. Later\n // observations may move it between wrapper execution namespaces\n // and `[\"tools:<toolCallId>\"]`, depending on where the stream proves\n // the worker's scoped message/tool state exists.\n const { parentId, depth } = lineageFromNamespace(eventNamespace);\n this.#recordTaskNamespaceCandidate(toolCallId, eventNamespace);\n if (input.description != null) {\n const queue = this.#toolCallIdByTaskInput.get(input.description) ?? [];\n queue.push(toolCallId);\n this.#toolCallIdByTaskInput.set(input.description, queue);\n }\n this.#map.set(toolCallId, {\n id: toolCallId,\n name: input.subagent_type ?? \"unknown\",\n namespace,\n parentId,\n depth,\n status: \"running\",\n taskInput: input.description,\n output: undefined,\n error: undefined,\n startedAt: new Date(),\n completedAt: null,\n });\n return true;\n }\n\n #recordObservedWorkNamespace(namespace: readonly string[]): boolean {\n if (!isConcreteToolNamespace(namespace)) return false;\n const last = namespace.at(-1);\n if (last == null) return false;\n const namespaceKeyed = namespaceKey(namespace);\n const toolCallId =\n this.#taskIdByObservedNamespace.get(namespaceKeyed) ??\n last.slice(\"tools:\".length);\n const existing = this.#map.get(toolCallId);\n if (existing == null) return false;\n\n const ownNamespaceKey = `tools:${toolCallId}`;\n if (namespaceKeyed === ownNamespaceKey) {\n this.#observedOwnNamespaces.add(toolCallId);\n } else if (\n this.#observedOwnNamespaces.has(toolCallId) ||\n (!this.#taskIdByObservedNamespace.has(namespaceKeyed) &&\n !shouldPromoteToObservedNamespace(existing))\n ) {\n return false;\n }\n\n if (namespaceKey(existing.namespace) === namespaceKeyed) return false;\n existing.namespace = [...namespace];\n return true;\n }\n\n #recordTaskNamespaceCandidate(\n toolCallId: string,\n namespace: readonly string[]\n ): void {\n if (!isConcreteToolNamespace(namespace)) return;\n this.#taskIdByObservedNamespace.set(namespaceKey(namespace), toolCallId);\n }\n\n /**\n * Bind a `tools:<id>` namespace to a registered subagent by looking\n * up the first HumanMessage content in the `taskInput` index.\n */\n #bindNamespaceByTaskInput(\n namespace: readonly string[],\n messages: unknown[]\n ): void {\n if (!isConcreteToolNamespace(namespace)) return;\n const namespaceKeyed = namespaceKey(namespace);\n if (this.#taskIdByObservedNamespace.has(namespaceKeyed)) return;\n\n const text = getFirstHumanMessageText(messages);\n if (text == null) return;\n const toolCallId = this.#toolCallIdByTaskInput.get(text)?.shift();\n if (toolCallId == null) return;\n this.#taskIdByObservedNamespace.set(namespaceKeyed, toolCallId);\n }\n}\n\nfunction getFirstHumanMessageText(messages: unknown[]): string | null {\n for (const message of messages) {\n if (message == null || typeof message !== \"object\") continue;\n const record = message as {\n type?: unknown;\n role?: unknown;\n content?: unknown;\n kwargs?: { type?: unknown; content?: unknown };\n lc_kwargs?: { type?: unknown; content?: unknown };\n };\n const type =\n record.type ??\n record.role ??\n record.kwargs?.type ??\n record.lc_kwargs?.type;\n if (type !== \"human\") continue;\n const content =\n record.content ?? record.kwargs?.content ?? record.lc_kwargs?.content;\n return typeof content === \"string\" && content.length > 0 ? content : null;\n }\n return null;\n}\n\nfunction shouldPromoteToObservedNamespace(entry: MutableSubagent): boolean {\n return (\n entry.name === \"fanout-worker\" ||\n /^Worker worker-\\d+/i.test(entry.taskInput ?? \"\")\n );\n}\n\nfunction taskWorkNamespace(\n toolCallId: string,\n eventNamespace: readonly string[]\n): readonly string[] {\n const last = eventNamespace.at(-1);\n if (last != null && isToolNamespaceSegment(last)) return [...eventNamespace];\n return [`tools:${toolCallId}`];\n}\n\nfunction toSnapshot(entry: MutableSubagent): SubagentDiscoverySnapshot {\n return {\n id: entry.id,\n name: entry.name,\n namespace: entry.namespace,\n parentId: entry.parentId,\n depth: entry.depth,\n status: entry.status,\n taskInput: entry.taskInput,\n output: entry.output,\n error: entry.error,\n startedAt: entry.startedAt,\n completedAt: entry.completedAt,\n };\n}\n\nfunction parseTaskInput(raw: unknown): {\n description?: string;\n subagent_type?: string;\n} {\n if (raw == null) return {};\n if (typeof raw === \"string\") {\n try {\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n return {\n description:\n typeof parsed.description === \"string\"\n ? parsed.description\n : undefined,\n subagent_type:\n typeof parsed.subagent_type === \"string\"\n ? parsed.subagent_type\n : undefined,\n };\n } catch {\n return {};\n }\n }\n if (typeof raw === \"object\" && !Array.isArray(raw)) {\n const obj = raw as Record<string, unknown>;\n return {\n description:\n typeof obj.description === \"string\" ? obj.description : undefined,\n subagent_type:\n typeof obj.subagent_type === \"string\" ? obj.subagent_type : undefined,\n };\n }\n return {};\n}\n\nfunction getTaskToolCalls(message: unknown): Array<{\n id: string;\n input: { description?: string; subagent_type?: string };\n}> {\n if (\n message == null ||\n typeof message !== \"object\" ||\n Array.isArray(message)\n ) {\n return [];\n }\n const record = message as {\n tool_calls?: unknown;\n kwargs?: { tool_calls?: unknown };\n lc_kwargs?: { tool_calls?: unknown };\n };\n const toolCalls =\n record.tool_calls ??\n record.kwargs?.tool_calls ??\n record.lc_kwargs?.tool_calls;\n if (!Array.isArray(toolCalls)) return [];\n\n const result: Array<{\n id: string;\n input: { description?: string; subagent_type?: string };\n }> = [];\n for (const toolCall of toolCalls) {\n if (\n toolCall == null ||\n typeof toolCall !== \"object\" ||\n Array.isArray(toolCall)\n ) {\n continue;\n }\n const record = toolCall as {\n id?: unknown;\n name?: unknown;\n args?: unknown;\n };\n if (typeof record.id !== \"string\" || record.name !== \"task\") continue;\n result.push({ id: record.id, input: parseTaskInput(record.args) });\n }\n return result;\n}\n\nfunction getToolMessageCallId(message: unknown): string | undefined {\n if (\n message == null ||\n typeof message !== \"object\" ||\n Array.isArray(message)\n ) {\n return undefined;\n }\n const record = message as {\n tool_call_id?: unknown;\n kwargs?: { tool_call_id?: unknown };\n lc_kwargs?: { tool_call_id?: unknown };\n };\n const id =\n record.tool_call_id ??\n record.kwargs?.tool_call_id ??\n record.lc_kwargs?.tool_call_id;\n return typeof id === \"string\" && id.length > 0 ? id : undefined;\n}\n\n/**\n * Derive (parentId, depth) from a namespace like\n * `[\"subagents:abc:def\"]`. Namespaces form a rooted tree; the last\n * `:` segment of the deepest namespace element is the current node's\n * call-id and the one before it is the parent.\n */\nfunction lineageFromNamespace(namespace: readonly string[]): {\n parentId: string | null;\n depth: number;\n} {\n if (isRootNamespace(namespace)) return { parentId: null, depth: 1 };\n const last = namespace[namespace.length - 1];\n if (last == null) return { parentId: null, depth: 1 };\n // Namespace segments typically look like\n // subagents:<parentCallId>:<thisCallId>\n // but the protocol doesn't mandate that shape; we best-effort.\n const parts = last.split(\":\").filter((part) => part.length > 0);\n const trimmed = parts.slice(1); // drop the leading \"subagents\" prefix\n const depth = Math.max(1, trimmed.length);\n const parentId = trimmed.length >= 2 ? trimmed[trimmed.length - 2] : null;\n return { parentId: parentId ?? null, depth };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"subagents.js","names":["#onToolEvent","#onValuesEvent","#map","#taskIdByObservedNamespace","#observedOwnNamespaces","#toolCallIdByTaskInput","#upsertTaskToolCall","#commit","#bindNamespaceByTaskInput","#recordObservedWorkNamespace","#recordTaskNamespaceCandidate"],"sources":["../../../src/stream/discovery/subagents.ts"],"sourcesContent":["/**\n * Root-scoped subagent discovery.\n *\n * Populates a `Map<callId, SubagentDiscoverySnapshot>` by watching\n * `task` tool calls on the root subscription. No content channels\n * (subagent messages, tool calls, extensions) are opened here — that\n * layer is driven by selector hooks via the\n * {@link ChannelRegistry}, keyed on `SubagentDiscoverySnapshot.namespace`.\n *\n * Discovery data this runner populates per subagent:\n * - id, name, namespace, parentId, depth\n * - status (`running` | `complete` | `error`)\n * - taskInput / output / error / startedAt / completedAt\n *\n * The runner is fed events by the {@link StreamController}'s root\n * subscription; it does not open subscriptions of its own.\n */\nimport type { Event, ToolsEvent, ValuesEvent } from \"@langchain/protocol\";\nimport { StreamStore } from \"../store.js\";\nimport type { SubagentDiscoverySnapshot } from \"../types.js\";\nimport {\n isConcreteToolNamespace,\n isRootNamespace,\n isToolNamespaceSegment,\n namespaceKey,\n} from \"../namespace.js\";\n\nexport type SubagentMap = ReadonlyMap<string, SubagentDiscoverySnapshot>;\n\n/** Stable empty map — reused on {@link SubagentDiscovery.reset}. */\nconst EMPTY_SUBAGENT_MAP: SubagentMap = new Map();\n\ninterface MutableSubagent {\n id: string;\n name: string;\n namespace: readonly string[];\n parentId: string | null;\n depth: number;\n status: \"running\" | \"complete\" | \"error\";\n taskInput: string | undefined;\n output: unknown;\n error: string | undefined;\n startedAt: Date;\n completedAt: Date | null;\n}\n\nexport class SubagentDiscovery {\n readonly store = new StreamStore<SubagentMap>(new Map());\n #map = new Map<string, MutableSubagent>();\n #taskIdByObservedNamespace = new Map<string, string>();\n #observedOwnNamespaces = new Set<string>();\n // Index from `taskInput` (the `description` arg) to a FIFO queue of\n // pending parent `tool_call_id`s. Bridges the wire's missing link\n // between a deepagents `task` dispatch and its subagent execution\n // namespace — the server seeds the subagent's first HumanMessage\n // with `taskInput` verbatim, so an exact-equality lookup is\n // deterministic. The queue (not a single value) handles the case\n // where the coordinator dispatches N task calls with identical\n // descriptions; pregel preserves dispatch order across executions,\n // so FIFO pop attributes them correctly.\n #toolCallIdByTaskInput = new Map<string, string[]>();\n\n /** Feed a single root event. Non-discovery events are ignored. */\n push(event: Event): void {\n if (event.method === \"tools\") {\n this.#onToolEvent(event as ToolsEvent);\n } else if (event.method === \"values\") {\n this.#onValuesEvent(event as ValuesEvent);\n }\n }\n\n /** Current snapshot map. */\n get snapshot(): SubagentMap {\n return this.store.getSnapshot();\n }\n\n /**\n * Drop all discovery state. Called on thread rebind / dispose so a\n * new thread's subagents cannot bleed into the previous UI.\n */\n reset(): void {\n this.#map.clear();\n this.#taskIdByObservedNamespace.clear();\n this.#observedOwnNamespaces.clear();\n this.#toolCallIdByTaskInput.clear();\n this.store.setValue(EMPTY_SUBAGENT_MAP);\n }\n\n discoverFromMessage(message: unknown, namespace: readonly string[]): void {\n let changed = false;\n for (const toolCall of getTaskToolCalls(message)) {\n changed =\n this.#upsertTaskToolCall(toolCall.id, toolCall.input, namespace) ||\n changed;\n }\n if (changed) this.#commit();\n }\n\n #commit(): void {\n // Rebuild as a fresh Map so React / useSyncExternalStore sees a\n // new reference on every change.\n this.store.setValue(\n new Map(\n [...this.#map.values()].map((entry) => [entry.id, toSnapshot(entry)])\n )\n );\n }\n\n #onToolEvent(event: ToolsEvent): void {\n const data = event.params.data;\n const toolCallId = (data as { tool_call_id?: string }).tool_call_id;\n const toolName = (data as { tool_name?: string }).tool_name;\n\n if (data.event === \"tool-started\" && toolName === \"task\") {\n const input = parseTaskInput((data as { input?: unknown }).input);\n if (toolCallId == null) return;\n this.#upsertTaskToolCall(toolCallId, input, event.params.namespace);\n this.#commit();\n return;\n }\n\n if (toolCallId == null) return;\n const entry = this.#map.get(toolCallId);\n if (entry == null) return;\n\n if (data.event === \"tool-finished\") {\n entry.status = \"complete\";\n entry.output = (data as { output?: unknown }).output;\n entry.completedAt = new Date();\n this.#commit();\n return;\n }\n\n if (data.event === \"tool-error\") {\n entry.status = \"error\";\n entry.error = (data as { message?: string }).message ?? \"Subagent failed\";\n entry.completedAt = new Date();\n this.#commit();\n }\n }\n\n #onValuesEvent(event: ValuesEvent): void {\n const data = event.params.data;\n if (data == null || typeof data !== \"object\" || Array.isArray(data)) return;\n const messages = (data as { messages?: unknown }).messages;\n if (!Array.isArray(messages)) return;\n\n // If a `tools:<id>` namespace's first HumanMessage matches a known\n // taskInput, that's the subagent's execution scope. Record the\n // binding so `#recordObservedWorkNamespace` can promote it.\n this.#bindNamespaceByTaskInput(event.params.namespace, messages);\n\n let changed = this.#recordObservedWorkNamespace(event.params.namespace);\n for (const message of messages) {\n for (const toolCall of getTaskToolCalls(message)) {\n changed =\n this.#upsertTaskToolCall(\n toolCall.id,\n toolCall.input,\n event.params.namespace\n ) || changed;\n }\n\n const toolCallId = getToolMessageCallId(message);\n if (toolCallId == null) continue;\n const existing = this.#map.get(toolCallId);\n if (existing == null) continue;\n existing.status = \"complete\";\n existing.output = message;\n existing.completedAt = new Date();\n changed = true;\n }\n if (changed) this.#commit();\n }\n\n #upsertTaskToolCall(\n toolCallId: string,\n input: { description?: string; subagent_type?: string },\n eventNamespace: readonly string[]\n ): boolean {\n const namespace = taskWorkNamespace(toolCallId, eventNamespace);\n const existing = this.#map.get(toolCallId);\n if (existing != null) {\n let changed = false;\n this.#recordTaskNamespaceCandidate(toolCallId, eventNamespace);\n const nextName = input.subagent_type ?? existing.name;\n const nextTaskInput = input.description ?? existing.taskInput;\n if (existing.name !== nextName) {\n existing.name = nextName;\n changed = true;\n }\n if (existing.taskInput !== nextTaskInput) {\n existing.taskInput = nextTaskInput;\n changed = true;\n }\n const namespaceKeyed = namespaceKey(existing.namespace);\n const ownNamespaceKey = `tools:${toolCallId}`;\n const nextNamespaceKey = namespaceKey(namespace);\n if (\n isConcreteToolNamespace(eventNamespace) ||\n namespaceKeyed === ownNamespaceKey\n ) {\n // A wrapper task tool event can arrive under an execution namespace\n // like `tools:<uuid>`, while the subagent's actual message state is\n // under `tools:<tool_call_id>`. Once discovery has observed the own\n // namespace carrying state, do not demote it back to the wrapper\n // namespace.\n if (\n namespaceKeyed === ownNamespaceKey &&\n nextNamespaceKey !== ownNamespaceKey &&\n this.#observedOwnNamespaces.has(toolCallId)\n ) {\n return changed;\n }\n if (namespaceKeyed !== nextNamespaceKey) {\n existing.namespace = namespace;\n changed = true;\n }\n }\n if (existing.status !== \"complete\" && existing.status !== \"error\") {\n if (existing.status !== \"running\") {\n existing.status = \"running\";\n changed = true;\n }\n }\n return changed;\n }\n\n // Prefer the namespace where the task is first observed. Later\n // observations may move it between wrapper execution namespaces\n // and `[\"tools:<toolCallId>\"]`, depending on where the stream proves\n // the worker's scoped message/tool state exists.\n const { parentId, depth } = lineageFromNamespace(eventNamespace);\n this.#recordTaskNamespaceCandidate(toolCallId, eventNamespace);\n if (input.description != null) {\n const queue = this.#toolCallIdByTaskInput.get(input.description) ?? [];\n queue.push(toolCallId);\n this.#toolCallIdByTaskInput.set(input.description, queue);\n }\n this.#map.set(toolCallId, {\n id: toolCallId,\n name: input.subagent_type ?? \"unknown\",\n namespace,\n parentId,\n depth,\n status: \"running\",\n taskInput: input.description,\n output: undefined,\n error: undefined,\n startedAt: new Date(),\n completedAt: null,\n });\n return true;\n }\n\n #recordObservedWorkNamespace(namespace: readonly string[]): boolean {\n if (!isConcreteToolNamespace(namespace)) return false;\n const last = namespace.at(-1);\n if (last == null) return false;\n const namespaceKeyed = namespaceKey(namespace);\n const toolCallId =\n this.#taskIdByObservedNamespace.get(namespaceKeyed) ??\n last.slice(\"tools:\".length);\n const existing = this.#map.get(toolCallId);\n if (existing == null) return false;\n\n const ownNamespaceKey = `tools:${toolCallId}`;\n if (namespaceKeyed === ownNamespaceKey) {\n this.#observedOwnNamespaces.add(toolCallId);\n } else if (\n this.#observedOwnNamespaces.has(toolCallId) ||\n (!this.#taskIdByObservedNamespace.has(namespaceKeyed) &&\n !shouldPromoteToObservedNamespace(existing))\n ) {\n return false;\n }\n\n if (namespaceKey(existing.namespace) === namespaceKeyed) return false;\n existing.namespace = [...namespace];\n return true;\n }\n\n #recordTaskNamespaceCandidate(\n toolCallId: string,\n namespace: readonly string[]\n ): void {\n if (!isConcreteToolNamespace(namespace)) return;\n this.#taskIdByObservedNamespace.set(namespaceKey(namespace), toolCallId);\n }\n\n /**\n * Bind a `tools:<id>` namespace to a registered subagent by looking\n * up the first HumanMessage content in the `taskInput` index.\n */\n #bindNamespaceByTaskInput(\n namespace: readonly string[],\n messages: unknown[]\n ): void {\n if (!isConcreteToolNamespace(namespace)) return;\n const namespaceKeyed = namespaceKey(namespace);\n if (this.#taskIdByObservedNamespace.has(namespaceKeyed)) return;\n\n const text = getFirstHumanMessageText(messages);\n if (text == null) return;\n const toolCallId = this.#toolCallIdByTaskInput.get(text)?.shift();\n if (toolCallId == null) return;\n this.#taskIdByObservedNamespace.set(namespaceKeyed, toolCallId);\n }\n}\n\nfunction getFirstHumanMessageText(messages: unknown[]): string | null {\n for (const message of messages) {\n if (message == null || typeof message !== \"object\") continue;\n const record = message as {\n type?: unknown;\n role?: unknown;\n content?: unknown;\n kwargs?: { type?: unknown; content?: unknown };\n lc_kwargs?: { type?: unknown; content?: unknown };\n };\n const type =\n record.type ??\n record.role ??\n record.kwargs?.type ??\n record.lc_kwargs?.type;\n if (type !== \"human\") continue;\n const content =\n record.content ?? record.kwargs?.content ?? record.lc_kwargs?.content;\n return typeof content === \"string\" && content.length > 0 ? content : null;\n }\n return null;\n}\n\nfunction shouldPromoteToObservedNamespace(entry: MutableSubagent): boolean {\n return (\n entry.name === \"fanout-worker\" ||\n /^Worker worker-\\d+/i.test(entry.taskInput ?? \"\")\n );\n}\n\nfunction taskWorkNamespace(\n toolCallId: string,\n eventNamespace: readonly string[]\n): readonly string[] {\n const last = eventNamespace.at(-1);\n if (last != null && isToolNamespaceSegment(last)) return [...eventNamespace];\n return [`tools:${toolCallId}`];\n}\n\nfunction toSnapshot(entry: MutableSubagent): SubagentDiscoverySnapshot {\n return {\n id: entry.id,\n name: entry.name,\n namespace: entry.namespace,\n parentId: entry.parentId,\n depth: entry.depth,\n status: entry.status,\n taskInput: entry.taskInput,\n output: entry.output,\n error: entry.error,\n startedAt: entry.startedAt,\n completedAt: entry.completedAt,\n };\n}\n\nfunction parseTaskInput(raw: unknown): {\n description?: string;\n subagent_type?: string;\n} {\n if (raw == null) return {};\n if (typeof raw === \"string\") {\n try {\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n return {\n description:\n typeof parsed.description === \"string\"\n ? parsed.description\n : undefined,\n subagent_type:\n typeof parsed.subagent_type === \"string\"\n ? parsed.subagent_type\n : undefined,\n };\n } catch {\n return {};\n }\n }\n if (typeof raw === \"object\" && !Array.isArray(raw)) {\n const obj = raw as Record<string, unknown>;\n return {\n description:\n typeof obj.description === \"string\" ? obj.description : undefined,\n subagent_type:\n typeof obj.subagent_type === \"string\" ? obj.subagent_type : undefined,\n };\n }\n return {};\n}\n\nfunction getTaskToolCalls(message: unknown): Array<{\n id: string;\n input: { description?: string; subagent_type?: string };\n}> {\n if (\n message == null ||\n typeof message !== \"object\" ||\n Array.isArray(message)\n ) {\n return [];\n }\n const record = message as {\n tool_calls?: unknown;\n kwargs?: { tool_calls?: unknown };\n lc_kwargs?: { tool_calls?: unknown };\n };\n const toolCalls =\n record.tool_calls ??\n record.kwargs?.tool_calls ??\n record.lc_kwargs?.tool_calls;\n if (!Array.isArray(toolCalls)) return [];\n\n const result: Array<{\n id: string;\n input: { description?: string; subagent_type?: string };\n }> = [];\n for (const toolCall of toolCalls) {\n if (\n toolCall == null ||\n typeof toolCall !== \"object\" ||\n Array.isArray(toolCall)\n ) {\n continue;\n }\n const record = toolCall as {\n id?: unknown;\n name?: unknown;\n args?: unknown;\n };\n if (typeof record.id !== \"string\" || record.name !== \"task\") continue;\n result.push({ id: record.id, input: parseTaskInput(record.args) });\n }\n return result;\n}\n\nfunction getToolMessageCallId(message: unknown): string | undefined {\n if (\n message == null ||\n typeof message !== \"object\" ||\n Array.isArray(message)\n ) {\n return undefined;\n }\n const record = message as {\n tool_call_id?: unknown;\n kwargs?: { tool_call_id?: unknown };\n lc_kwargs?: { tool_call_id?: unknown };\n };\n const id =\n record.tool_call_id ??\n record.kwargs?.tool_call_id ??\n record.lc_kwargs?.tool_call_id;\n return typeof id === \"string\" && id.length > 0 ? id : undefined;\n}\n\n/**\n * Derive (parentId, depth) from a namespace like\n * `[\"subagents:abc:def\"]`. Namespaces form a rooted tree; the last\n * `:` segment of the deepest namespace element is the current node's\n * call-id and the one before it is the parent.\n */\nfunction lineageFromNamespace(namespace: readonly string[]): {\n parentId: string | null;\n depth: number;\n} {\n if (isRootNamespace(namespace)) return { parentId: null, depth: 1 };\n const last = namespace[namespace.length - 1];\n if (last == null) return { parentId: null, depth: 1 };\n // Namespace segments typically look like\n // subagents:<parentCallId>:<thisCallId>\n // but the protocol doesn't mandate that shape; we best-effort.\n const parts = last.split(\":\").filter((part) => part.length > 0);\n const trimmed = parts.slice(1); // drop the leading \"subagents\" prefix\n const depth = Math.max(1, trimmed.length);\n const parentId = trimmed.length >= 2 ? trimmed[trimmed.length - 2] : null;\n return { parentId: parentId ?? null, depth };\n}\n"],"mappings":";;;;AA8BA,MAAM,qCAAkC,IAAI,KAAK;AAgBjD,IAAa,oBAAb,MAA+B;CAC7B,QAAiB,IAAI,4BAAyB,IAAI,KAAK,CAAC;CACxD,uBAAO,IAAI,KAA8B;CACzC,6CAA6B,IAAI,KAAqB;CACtD,yCAAyB,IAAI,KAAa;CAU1C,yCAAyB,IAAI,KAAuB;;CAGpD,KAAK,OAAoB;AACvB,MAAI,MAAM,WAAW,QACnB,OAAA,YAAkB,MAAoB;WAC7B,MAAM,WAAW,SAC1B,OAAA,cAAoB,MAAqB;;;CAK7C,IAAI,WAAwB;AAC1B,SAAO,KAAK,MAAM,aAAa;;;;;;CAOjC,QAAc;AACZ,QAAA,IAAU,OAAO;AACjB,QAAA,0BAAgC,OAAO;AACvC,QAAA,sBAA4B,OAAO;AACnC,QAAA,sBAA4B,OAAO;AACnC,OAAK,MAAM,SAAS,mBAAmB;;CAGzC,oBAAoB,SAAkB,WAAoC;EACxE,IAAI,UAAU;AACd,OAAK,MAAM,YAAY,iBAAiB,QAAQ,CAC9C,WACE,MAAA,mBAAyB,SAAS,IAAI,SAAS,OAAO,UAAU,IAChE;AAEJ,MAAI,QAAS,OAAA,QAAc;;CAG7B,UAAgB;AAGd,OAAK,MAAM,SACT,IAAI,IACF,CAAC,GAAG,MAAA,IAAU,QAAQ,CAAC,CAAC,KAAK,UAAU,CAAC,MAAM,IAAI,WAAW,MAAM,CAAC,CAAC,CACtE,CACF;;CAGH,aAAa,OAAyB;EACpC,MAAM,OAAO,MAAM,OAAO;EAC1B,MAAM,aAAc,KAAmC;EACvD,MAAM,WAAY,KAAgC;AAElD,MAAI,KAAK,UAAU,kBAAkB,aAAa,QAAQ;GACxD,MAAM,QAAQ,eAAgB,KAA6B,MAAM;AACjE,OAAI,cAAc,KAAM;AACxB,SAAA,mBAAyB,YAAY,OAAO,MAAM,OAAO,UAAU;AACnE,SAAA,QAAc;AACd;;AAGF,MAAI,cAAc,KAAM;EACxB,MAAM,QAAQ,MAAA,IAAU,IAAI,WAAW;AACvC,MAAI,SAAS,KAAM;AAEnB,MAAI,KAAK,UAAU,iBAAiB;AAClC,SAAM,SAAS;AACf,SAAM,SAAU,KAA8B;AAC9C,SAAM,8BAAc,IAAI,MAAM;AAC9B,SAAA,QAAc;AACd;;AAGF,MAAI,KAAK,UAAU,cAAc;AAC/B,SAAM,SAAS;AACf,SAAM,QAAS,KAA8B,WAAW;AACxD,SAAM,8BAAc,IAAI,MAAM;AAC9B,SAAA,QAAc;;;CAIlB,eAAe,OAA0B;EACvC,MAAM,OAAO,MAAM,OAAO;AAC1B,MAAI,QAAQ,QAAQ,OAAO,SAAS,YAAY,MAAM,QAAQ,KAAK,CAAE;EACrE,MAAM,WAAY,KAAgC;AAClD,MAAI,CAAC,MAAM,QAAQ,SAAS,CAAE;AAK9B,QAAA,yBAA+B,MAAM,OAAO,WAAW,SAAS;EAEhE,IAAI,UAAU,MAAA,4BAAkC,MAAM,OAAO,UAAU;AACvE,OAAK,MAAM,WAAW,UAAU;AAC9B,QAAK,MAAM,YAAY,iBAAiB,QAAQ,CAC9C,WACE,MAAA,mBACE,SAAS,IACT,SAAS,OACT,MAAM,OAAO,UACd,IAAI;GAGT,MAAM,aAAa,qBAAqB,QAAQ;AAChD,OAAI,cAAc,KAAM;GACxB,MAAM,WAAW,MAAA,IAAU,IAAI,WAAW;AAC1C,OAAI,YAAY,KAAM;AACtB,YAAS,SAAS;AAClB,YAAS,SAAS;AAClB,YAAS,8BAAc,IAAI,MAAM;AACjC,aAAU;;AAEZ,MAAI,QAAS,OAAA,QAAc;;CAG7B,oBACE,YACA,OACA,gBACS;EACT,MAAM,YAAY,kBAAkB,YAAY,eAAe;EAC/D,MAAM,WAAW,MAAA,IAAU,IAAI,WAAW;AAC1C,MAAI,YAAY,MAAM;GACpB,IAAI,UAAU;AACd,SAAA,6BAAmC,YAAY,eAAe;GAC9D,MAAM,WAAW,MAAM,iBAAiB,SAAS;GACjD,MAAM,gBAAgB,MAAM,eAAe,SAAS;AACpD,OAAI,SAAS,SAAS,UAAU;AAC9B,aAAS,OAAO;AAChB,cAAU;;AAEZ,OAAI,SAAS,cAAc,eAAe;AACxC,aAAS,YAAY;AACrB,cAAU;;GAEZ,MAAM,iBAAiB,aAAa,SAAS,UAAU;GACvD,MAAM,kBAAkB,SAAS;GACjC,MAAM,mBAAmB,aAAa,UAAU;AAChD,OACE,wBAAwB,eAAe,IACvC,mBAAmB,iBACnB;AAMA,QACE,mBAAmB,mBACnB,qBAAqB,mBACrB,MAAA,sBAA4B,IAAI,WAAW,CAE3C,QAAO;AAET,QAAI,mBAAmB,kBAAkB;AACvC,cAAS,YAAY;AACrB,eAAU;;;AAGd,OAAI,SAAS,WAAW,cAAc,SAAS,WAAW;QACpD,SAAS,WAAW,WAAW;AACjC,cAAS,SAAS;AAClB,eAAU;;;AAGd,UAAO;;EAOT,MAAM,EAAE,UAAU,UAAU,qBAAqB,eAAe;AAChE,QAAA,6BAAmC,YAAY,eAAe;AAC9D,MAAI,MAAM,eAAe,MAAM;GAC7B,MAAM,QAAQ,MAAA,sBAA4B,IAAI,MAAM,YAAY,IAAI,EAAE;AACtE,SAAM,KAAK,WAAW;AACtB,SAAA,sBAA4B,IAAI,MAAM,aAAa,MAAM;;AAE3D,QAAA,IAAU,IAAI,YAAY;GACxB,IAAI;GACJ,MAAM,MAAM,iBAAiB;GAC7B;GACA;GACA;GACA,QAAQ;GACR,WAAW,MAAM;GACjB,QAAQ,KAAA;GACR,OAAO,KAAA;GACP,2BAAW,IAAI,MAAM;GACrB,aAAa;GACd,CAAC;AACF,SAAO;;CAGT,6BAA6B,WAAuC;AAClE,MAAI,CAAC,wBAAwB,UAAU,CAAE,QAAO;EAChD,MAAM,OAAO,UAAU,GAAG,GAAG;AAC7B,MAAI,QAAQ,KAAM,QAAO;EACzB,MAAM,iBAAiB,aAAa,UAAU;EAC9C,MAAM,aACJ,MAAA,0BAAgC,IAAI,eAAe,IACnD,KAAK,MAAM,EAAgB;EAC7B,MAAM,WAAW,MAAA,IAAU,IAAI,WAAW;AAC1C,MAAI,YAAY,KAAM,QAAO;AAG7B,MAAI,mBADoB,SAAS,aAE/B,OAAA,sBAA4B,IAAI,WAAW;WAE3C,MAAA,sBAA4B,IAAI,WAAW,IAC1C,CAAC,MAAA,0BAAgC,IAAI,eAAe,IACnD,CAAC,iCAAiC,SAAS,CAE7C,QAAO;AAGT,MAAI,aAAa,SAAS,UAAU,KAAK,eAAgB,QAAO;AAChE,WAAS,YAAY,CAAC,GAAG,UAAU;AACnC,SAAO;;CAGT,8BACE,YACA,WACM;AACN,MAAI,CAAC,wBAAwB,UAAU,CAAE;AACzC,QAAA,0BAAgC,IAAI,aAAa,UAAU,EAAE,WAAW;;;;;;CAO1E,0BACE,WACA,UACM;AACN,MAAI,CAAC,wBAAwB,UAAU,CAAE;EACzC,MAAM,iBAAiB,aAAa,UAAU;AAC9C,MAAI,MAAA,0BAAgC,IAAI,eAAe,CAAE;EAEzD,MAAM,OAAO,yBAAyB,SAAS;AAC/C,MAAI,QAAQ,KAAM;EAClB,MAAM,aAAa,MAAA,sBAA4B,IAAI,KAAK,EAAE,OAAO;AACjE,MAAI,cAAc,KAAM;AACxB,QAAA,0BAAgC,IAAI,gBAAgB,WAAW;;;AAInE,SAAS,yBAAyB,UAAoC;AACpE,MAAK,MAAM,WAAW,UAAU;AAC9B,MAAI,WAAW,QAAQ,OAAO,YAAY,SAAU;EACpD,MAAM,SAAS;AAYf,OAJE,OAAO,QACP,OAAO,QACP,OAAO,QAAQ,QACf,OAAO,WAAW,UACP,QAAS;EACtB,MAAM,UACJ,OAAO,WAAW,OAAO,QAAQ,WAAW,OAAO,WAAW;AAChE,SAAO,OAAO,YAAY,YAAY,QAAQ,SAAS,IAAI,UAAU;;AAEvE,QAAO;;AAGT,SAAS,iCAAiC,OAAiC;AACzE,QACE,MAAM,SAAS,mBACf,sBAAsB,KAAK,MAAM,aAAa,GAAG;;AAIrD,SAAS,kBACP,YACA,gBACmB;CACnB,MAAM,OAAO,eAAe,GAAG,GAAG;AAClC,KAAI,QAAQ,QAAQ,uBAAuB,KAAK,CAAE,QAAO,CAAC,GAAG,eAAe;AAC5E,QAAO,CAAC,SAAS,aAAa;;AAGhC,SAAS,WAAW,OAAmD;AACrE,QAAO;EACL,IAAI,MAAM;EACV,MAAM,MAAM;EACZ,WAAW,MAAM;EACjB,UAAU,MAAM;EAChB,OAAO,MAAM;EACb,QAAQ,MAAM;EACd,WAAW,MAAM;EACjB,QAAQ,MAAM;EACd,OAAO,MAAM;EACb,WAAW,MAAM;EACjB,aAAa,MAAM;EACpB;;AAGH,SAAS,eAAe,KAGtB;AACA,KAAI,OAAO,KAAM,QAAO,EAAE;AAC1B,KAAI,OAAO,QAAQ,SACjB,KAAI;EACF,MAAM,SAAS,KAAK,MAAM,IAAI;AAC9B,SAAO;GACL,aACE,OAAO,OAAO,gBAAgB,WAC1B,OAAO,cACP,KAAA;GACN,eACE,OAAO,OAAO,kBAAkB,WAC5B,OAAO,gBACP,KAAA;GACP;SACK;AACN,SAAO,EAAE;;AAGb,KAAI,OAAO,QAAQ,YAAY,CAAC,MAAM,QAAQ,IAAI,EAAE;EAClD,MAAM,MAAM;AACZ,SAAO;GACL,aACE,OAAO,IAAI,gBAAgB,WAAW,IAAI,cAAc,KAAA;GAC1D,eACE,OAAO,IAAI,kBAAkB,WAAW,IAAI,gBAAgB,KAAA;GAC/D;;AAEH,QAAO,EAAE;;AAGX,SAAS,iBAAiB,SAGvB;AACD,KACE,WAAW,QACX,OAAO,YAAY,YACnB,MAAM,QAAQ,QAAQ,CAEtB,QAAO,EAAE;CAEX,MAAM,SAAS;CAKf,MAAM,YACJ,OAAO,cACP,OAAO,QAAQ,cACf,OAAO,WAAW;AACpB,KAAI,CAAC,MAAM,QAAQ,UAAU,CAAE,QAAO,EAAE;CAExC,MAAM,SAGD,EAAE;AACP,MAAK,MAAM,YAAY,WAAW;AAChC,MACE,YAAY,QACZ,OAAO,aAAa,YACpB,MAAM,QAAQ,SAAS,CAEvB;EAEF,MAAM,SAAS;AAKf,MAAI,OAAO,OAAO,OAAO,YAAY,OAAO,SAAS,OAAQ;AAC7D,SAAO,KAAK;GAAE,IAAI,OAAO;GAAI,OAAO,eAAe,OAAO,KAAK;GAAE,CAAC;;AAEpE,QAAO;;AAGT,SAAS,qBAAqB,SAAsC;AAClE,KACE,WAAW,QACX,OAAO,YAAY,YACnB,MAAM,QAAQ,QAAQ,CAEtB;CAEF,MAAM,SAAS;CAKf,MAAM,KACJ,OAAO,gBACP,OAAO,QAAQ,gBACf,OAAO,WAAW;AACpB,QAAO,OAAO,OAAO,YAAY,GAAG,SAAS,IAAI,KAAK,KAAA;;;;;;;;AASxD,SAAS,qBAAqB,WAG5B;AACA,KAAI,gBAAgB,UAAU,CAAE,QAAO;EAAE,UAAU;EAAM,OAAO;EAAG;CACnE,MAAM,OAAO,UAAU,UAAU,SAAS;AAC1C,KAAI,QAAQ,KAAM,QAAO;EAAE,UAAU;EAAM,OAAO;EAAG;CAKrD,MAAM,UADQ,KAAK,MAAM,IAAI,CAAC,QAAQ,SAAS,KAAK,SAAS,EAAE,CACzC,MAAM,EAAE;CAC9B,MAAM,QAAQ,KAAK,IAAI,GAAG,QAAQ,OAAO;AAEzC,QAAO;EAAE,WADQ,QAAQ,UAAU,IAAI,QAAQ,QAAQ,SAAS,KAAK,SACtC;EAAM;EAAO"}
@@ -1,6 +1,9 @@
1
1
  const require_store = require("../store.cjs");
2
2
  const require_namespace = require("../namespace.cjs");
3
3
  //#region src/stream/discovery/subgraphs.ts
4
+ /** Stable empty maps — reused on {@link SubgraphDiscovery.reset}. */
5
+ const EMPTY_SUBGRAPH_MAP = /* @__PURE__ */ new Map();
6
+ const EMPTY_SUBGRAPH_BY_NODE_MAP = /* @__PURE__ */ new Map();
4
7
  /**
5
8
  * LangGraph namespaces a node invocation as `<node_name>:<uuid>`
6
9
  * (parallel fan-outs share `<node_name>` as a prefix but each get a
@@ -115,6 +118,16 @@ var SubgraphDiscovery = class {
115
118
  get byNodeSnapshot() {
116
119
  return this.byNodeStore.getSnapshot();
117
120
  }
121
+ /**
122
+ * Drop all discovery state. Called on thread rebind / dispose so a
123
+ * new thread's subgraphs cannot bleed into the previous UI.
124
+ */
125
+ reset() {
126
+ this.#shadow.clear();
127
+ this.#promoted.clear();
128
+ this.store.setValue(EMPTY_SUBGRAPH_MAP);
129
+ this.byNodeStore.setValue(EMPTY_SUBGRAPH_BY_NODE_MAP);
130
+ }
118
131
  #ensureShadow(id, namespace, nodeName) {
119
132
  let entry = this.#shadow.get(id);
120
133
  if (entry == null) {
@@ -1 +1 @@
1
- {"version":3,"file":"subgraphs.cjs","names":["StreamStore","#onValuesEvent","isRootNamespace","namespaceKey","#promoted","#shadow","#ensureShadow","#commit","isInternalWorkNamespace"],"sources":["../../../src/stream/discovery/subgraphs.ts"],"sourcesContent":["/**\n * Root-scoped subgraph discovery.\n *\n * Watches namespaced `lifecycle` events on the root subscription and\n * assembles two views of the subgraph set:\n *\n * - {@link SubgraphMap}: `Map<namespaceKey, SubgraphDiscoverySnapshot>`,\n * the canonical identity-keyed view consumed by the channel\n * registry and selector hooks.\n * - {@link SubgraphByNodeMap}: `Map<nodeName, readonly\n * SubgraphDiscoverySnapshot[]>`, a convenience index so callers\n * can look up subgraphs by the graph node that produced them\n * (`addNode(\"visualizer_0\", …)`) without parsing namespaces.\n * Arrays preserve insertion order, which matters for parallel\n * fan-outs that share a node name.\n *\n * # What counts as a subgraph\n *\n * The server emits a namespaced `lifecycle` event for every node\n * invocation — a plain function node (`orchestrator`) and a subgraph\n * host (`research`) look identical on the wire. We classify a\n * namespace as a subgraph iff at least one strictly-deeper namespace\n * has been observed with it as a prefix. Concretely, given a stream\n * whose lifecycle events hit the namespaces\n *\n * `[\"orchestrator:u1\"]`\n * `[\"research:u2\"]`\n * `[\"research:u2\", \"researcher:u3\"]`\n * `[\"research:u2\", \"tools:u4\"]`\n * `[\"writer:u5\"]`\n *\n * only `[\"research:u2\"]` is promoted — it's the only namespace that\n * hosts deeper executions. `orchestrator` and `writer` are plain\n * function-node leaves; the `researcher` / `tools` entries are the\n * subgraph's internal nodes, not subgraphs in their own right.\n *\n * Promotion is monotonic (a namespace never loses subgraph status)\n * and retroactive: a namespace whose own `started` event arrived\n * before any descendant is promoted later when the first descendant\n * event lands. Latency is bounded by the gap between a parent node\n * entering and its first inner node materializing — typically tens\n * of milliseconds.\n */\nimport type { Event, LifecycleEvent, ValuesEvent } from \"@langchain/protocol\";\nimport { StreamStore } from \"../store.js\";\nimport type { SubgraphDiscoverySnapshot } from \"../types.js\";\nimport {\n isInternalWorkNamespace,\n isRootNamespace,\n namespaceKey,\n} from \"../namespace.js\";\n\nexport type SubgraphMap = ReadonlyMap<string, SubgraphDiscoverySnapshot>;\nexport type SubgraphByNodeMap = ReadonlyMap<\n string,\n readonly SubgraphDiscoverySnapshot[]\n>;\n\ninterface MutableSubgraph {\n id: string;\n namespace: readonly string[];\n nodeName: string;\n status: \"running\" | \"complete\" | \"error\";\n startedAt: Date;\n completedAt: Date | null;\n}\n\n/**\n * LangGraph namespaces a node invocation as `<node_name>:<uuid>`\n * (parallel fan-outs share `<node_name>` as a prefix but each get a\n * fresh uuid). Extract the node-name half so callers can key\n * discovery lookups on names they wrote in `addNode(...)`.\n */\nfunction parseNodeName(segment: string): string {\n const colon = segment.indexOf(\":\");\n return colon === -1 ? segment : segment.slice(0, colon);\n}\n\nexport class SubgraphDiscovery {\n readonly store = new StreamStore<SubgraphMap>(new Map());\n readonly byNodeStore = new StreamStore<SubgraphByNodeMap>(new Map());\n\n /**\n * Latest known status for every namespaced lifecycle event we have\n * ever observed. A shadow entry is NOT necessarily a subgraph —\n * it is only projected into the committed stores once the same\n * namespace also appears in {@link #promoted}.\n */\n #shadow = new Map<string, MutableSubgraph>();\n\n /**\n * Namespaces that have been observed as a strict prefix of a\n * deeper namespace and are therefore confirmed subgraph hosts.\n * Insertion order is preserved and becomes the iteration order\n * of the committed snapshot maps.\n */\n #promoted = new Set<string>();\n\n /** Feed a single root event. Non-discovery events are ignored. */\n push(event: Event): void {\n if (event.method === \"values\") {\n this.#onValuesEvent(event as ValuesEvent);\n return;\n }\n if (event.method !== \"lifecycle\") return;\n const lifecycle = event as LifecycleEvent;\n const namespace = lifecycle.params.namespace;\n // Root lifecycle events describe the main run; subgraph discovery\n // only cares about namespaced lifecycle events.\n if (isRootNamespace(namespace)) return;\n const id = namespaceKey(namespace);\n const data = lifecycle.params.data as { event?: string };\n const lastSegment = namespace[namespace.length - 1] ?? \"\";\n const nodeName = parseNodeName(lastSegment);\n\n let touched = false;\n\n // Promote every strict ancestor the first time we see it as a\n // prefix. The ancestor may or may not yet have a shadow entry;\n // #commit() tolerates either case.\n for (let depth = 1; depth < namespace.length; depth += 1) {\n const ancestorId = namespaceKey(namespace.slice(0, depth));\n if (!this.#promoted.has(ancestorId)) {\n this.#promoted.add(ancestorId);\n if (this.#shadow.has(ancestorId)) touched = true;\n }\n }\n\n // Update shadow status for this namespace itself.\n if (data.event === \"started\") {\n if (!this.#shadow.has(id)) {\n this.#shadow.set(id, {\n id,\n namespace: [...namespace],\n nodeName,\n status: \"running\",\n startedAt: new Date(),\n completedAt: null,\n });\n if (this.#promoted.has(id)) touched = true;\n }\n } else if (\n data.event === \"completed\" ||\n data.event === \"interrupted\" ||\n data.event === \"failed\"\n ) {\n // Synthesize a shadow entry if we missed the `started` event\n // (common when a late subscription attaches to a running run).\n const entry = this.#ensureShadow(id, namespace, nodeName);\n if (data.event === \"failed\") {\n entry.status = \"error\";\n } else {\n entry.status = \"complete\";\n }\n entry.completedAt = new Date();\n if (this.#promoted.has(id)) touched = true;\n }\n\n if (touched) this.#commit();\n }\n\n /**\n * Promote subgraph host namespaces from namespaced `values` snapshots.\n *\n * Older protocol streams exposed subgraph structure primarily through\n * nested `lifecycle` events: a host namespace such as\n * `[\"research:<uuid>\"]` was promoted once a deeper namespace like\n * `[\"research:<uuid>\", \"inner:<uuid>\"]` appeared. Some runtimes now\n * emit the useful subgraph signal as `values` snapshots scoped directly\n * to the host namespace instead, without forwarding inner lifecycle\n * events to the client. In that shape, the namespace on the `values`\n * event is already the selector target that `useMessages(stream,\n * subgraph)` should subscribe to, so we create/promote the shadow\n * subgraph entry from that namespace.\n *\n * Root `values` events are ignored because they represent the parent\n * thread state, not a subgraph. Tool/subagent namespaces are also\n * ignored because deep-agent task tools emit their own namespaced\n * `values` snapshots under `tools:*` / `task:*`; those are discovered\n * by `SubagentDiscovery` and must not be duplicated as subgraphs.\n *\n * A `values` event does not carry lifecycle terminal status, so the\n * entry remains marked `running` until a matching lifecycle terminal\n * event arrives. If no terminal arrives, the discovery map still\n * contains the host namespace, which is the important invariant for\n * scoped selectors.\n */\n #onValuesEvent(event: ValuesEvent): void {\n const namespace = event.params.namespace;\n if (isRootNamespace(namespace)) return;\n if (isInternalWorkNamespace(namespace)) return;\n const data = event.params.data;\n if (data == null || typeof data !== \"object\" || Array.isArray(data)) return;\n\n const id = namespaceKey(namespace);\n const lastSegment = namespace[namespace.length - 1] ?? \"\";\n const nodeName = parseNodeName(lastSegment);\n const entry = this.#ensureShadow(id, namespace, nodeName);\n entry.status = \"running\";\n\n if (!this.#promoted.has(id)) {\n this.#promoted.add(id);\n }\n this.#commit();\n }\n\n get snapshot(): SubgraphMap {\n return this.store.getSnapshot();\n }\n\n get byNodeSnapshot(): SubgraphByNodeMap {\n return this.byNodeStore.getSnapshot();\n }\n\n #ensureShadow(\n id: string,\n namespace: readonly string[],\n nodeName: string\n ): MutableSubgraph {\n let entry = this.#shadow.get(id);\n if (entry == null) {\n entry = {\n id,\n namespace: [...namespace],\n nodeName,\n status: \"running\",\n startedAt: new Date(),\n completedAt: null,\n };\n this.#shadow.set(id, entry);\n }\n return entry;\n }\n\n #commit(): void {\n const snapshots: SubgraphDiscoverySnapshot[] = [];\n for (const id of this.#promoted) {\n const entry = this.#shadow.get(id);\n // A namespace can be promoted before its own lifecycle event\n // arrives if descendant events outpace the prefix event. Skip\n // until the shadow entry lands; the next push() promoting or\n // updating this namespace will re-commit.\n if (entry == null) continue;\n snapshots.push({ ...entry });\n }\n\n this.store.setValue(new Map(snapshots.map((s) => [s.id, s])));\n\n const byNode = new Map<string, SubgraphDiscoverySnapshot[]>();\n for (const snap of snapshots) {\n const bucket = byNode.get(snap.nodeName);\n if (bucket == null) byNode.set(snap.nodeName, [snap]);\n else bucket.push(snap);\n }\n this.byNodeStore.setValue(byNode);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"subgraphs.cjs","names":["StreamStore","#onValuesEvent","isRootNamespace","namespaceKey","#promoted","#shadow","#ensureShadow","#commit","isInternalWorkNamespace"],"sources":["../../../src/stream/discovery/subgraphs.ts"],"sourcesContent":["/**\n * Root-scoped subgraph discovery.\n *\n * Watches namespaced `lifecycle` events on the root subscription and\n * assembles two views of the subgraph set:\n *\n * - {@link SubgraphMap}: `Map<namespaceKey, SubgraphDiscoverySnapshot>`,\n * the canonical identity-keyed view consumed by the channel\n * registry and selector hooks.\n * - {@link SubgraphByNodeMap}: `Map<nodeName, readonly\n * SubgraphDiscoverySnapshot[]>`, a convenience index so callers\n * can look up subgraphs by the graph node that produced them\n * (`addNode(\"visualizer_0\", …)`) without parsing namespaces.\n * Arrays preserve insertion order, which matters for parallel\n * fan-outs that share a node name.\n *\n * # What counts as a subgraph\n *\n * The server emits a namespaced `lifecycle` event for every node\n * invocation — a plain function node (`orchestrator`) and a subgraph\n * host (`research`) look identical on the wire. We classify a\n * namespace as a subgraph iff at least one strictly-deeper namespace\n * has been observed with it as a prefix. Concretely, given a stream\n * whose lifecycle events hit the namespaces\n *\n * `[\"orchestrator:u1\"]`\n * `[\"research:u2\"]`\n * `[\"research:u2\", \"researcher:u3\"]`\n * `[\"research:u2\", \"tools:u4\"]`\n * `[\"writer:u5\"]`\n *\n * only `[\"research:u2\"]` is promoted — it's the only namespace that\n * hosts deeper executions. `orchestrator` and `writer` are plain\n * function-node leaves; the `researcher` / `tools` entries are the\n * subgraph's internal nodes, not subgraphs in their own right.\n *\n * Promotion is monotonic (a namespace never loses subgraph status)\n * and retroactive: a namespace whose own `started` event arrived\n * before any descendant is promoted later when the first descendant\n * event lands. Latency is bounded by the gap between a parent node\n * entering and its first inner node materializing — typically tens\n * of milliseconds.\n */\nimport type { Event, LifecycleEvent, ValuesEvent } from \"@langchain/protocol\";\nimport { StreamStore } from \"../store.js\";\nimport type { SubgraphDiscoverySnapshot } from \"../types.js\";\nimport {\n isInternalWorkNamespace,\n isRootNamespace,\n namespaceKey,\n} from \"../namespace.js\";\n\nexport type SubgraphMap = ReadonlyMap<string, SubgraphDiscoverySnapshot>;\nexport type SubgraphByNodeMap = ReadonlyMap<\n string,\n readonly SubgraphDiscoverySnapshot[]\n>;\n\n/** Stable empty maps — reused on {@link SubgraphDiscovery.reset}. */\nconst EMPTY_SUBGRAPH_MAP: SubgraphMap = new Map();\nconst EMPTY_SUBGRAPH_BY_NODE_MAP: SubgraphByNodeMap = new Map();\n\ninterface MutableSubgraph {\n id: string;\n namespace: readonly string[];\n nodeName: string;\n status: \"running\" | \"complete\" | \"error\";\n startedAt: Date;\n completedAt: Date | null;\n}\n\n/**\n * LangGraph namespaces a node invocation as `<node_name>:<uuid>`\n * (parallel fan-outs share `<node_name>` as a prefix but each get a\n * fresh uuid). Extract the node-name half so callers can key\n * discovery lookups on names they wrote in `addNode(...)`.\n */\nfunction parseNodeName(segment: string): string {\n const colon = segment.indexOf(\":\");\n return colon === -1 ? segment : segment.slice(0, colon);\n}\n\nexport class SubgraphDiscovery {\n readonly store = new StreamStore<SubgraphMap>(new Map());\n readonly byNodeStore = new StreamStore<SubgraphByNodeMap>(new Map());\n\n /**\n * Latest known status for every namespaced lifecycle event we have\n * ever observed. A shadow entry is NOT necessarily a subgraph —\n * it is only projected into the committed stores once the same\n * namespace also appears in {@link #promoted}.\n */\n #shadow = new Map<string, MutableSubgraph>();\n\n /**\n * Namespaces that have been observed as a strict prefix of a\n * deeper namespace and are therefore confirmed subgraph hosts.\n * Insertion order is preserved and becomes the iteration order\n * of the committed snapshot maps.\n */\n #promoted = new Set<string>();\n\n /** Feed a single root event. Non-discovery events are ignored. */\n push(event: Event): void {\n if (event.method === \"values\") {\n this.#onValuesEvent(event as ValuesEvent);\n return;\n }\n if (event.method !== \"lifecycle\") return;\n const lifecycle = event as LifecycleEvent;\n const namespace = lifecycle.params.namespace;\n // Root lifecycle events describe the main run; subgraph discovery\n // only cares about namespaced lifecycle events.\n if (isRootNamespace(namespace)) return;\n const id = namespaceKey(namespace);\n const data = lifecycle.params.data as { event?: string };\n const lastSegment = namespace[namespace.length - 1] ?? \"\";\n const nodeName = parseNodeName(lastSegment);\n\n let touched = false;\n\n // Promote every strict ancestor the first time we see it as a\n // prefix. The ancestor may or may not yet have a shadow entry;\n // #commit() tolerates either case.\n for (let depth = 1; depth < namespace.length; depth += 1) {\n const ancestorId = namespaceKey(namespace.slice(0, depth));\n if (!this.#promoted.has(ancestorId)) {\n this.#promoted.add(ancestorId);\n if (this.#shadow.has(ancestorId)) touched = true;\n }\n }\n\n // Update shadow status for this namespace itself.\n if (data.event === \"started\") {\n if (!this.#shadow.has(id)) {\n this.#shadow.set(id, {\n id,\n namespace: [...namespace],\n nodeName,\n status: \"running\",\n startedAt: new Date(),\n completedAt: null,\n });\n if (this.#promoted.has(id)) touched = true;\n }\n } else if (\n data.event === \"completed\" ||\n data.event === \"interrupted\" ||\n data.event === \"failed\"\n ) {\n // Synthesize a shadow entry if we missed the `started` event\n // (common when a late subscription attaches to a running run).\n const entry = this.#ensureShadow(id, namespace, nodeName);\n if (data.event === \"failed\") {\n entry.status = \"error\";\n } else {\n entry.status = \"complete\";\n }\n entry.completedAt = new Date();\n if (this.#promoted.has(id)) touched = true;\n }\n\n if (touched) this.#commit();\n }\n\n /**\n * Promote subgraph host namespaces from namespaced `values` snapshots.\n *\n * Older protocol streams exposed subgraph structure primarily through\n * nested `lifecycle` events: a host namespace such as\n * `[\"research:<uuid>\"]` was promoted once a deeper namespace like\n * `[\"research:<uuid>\", \"inner:<uuid>\"]` appeared. Some runtimes now\n * emit the useful subgraph signal as `values` snapshots scoped directly\n * to the host namespace instead, without forwarding inner lifecycle\n * events to the client. In that shape, the namespace on the `values`\n * event is already the selector target that `useMessages(stream,\n * subgraph)` should subscribe to, so we create/promote the shadow\n * subgraph entry from that namespace.\n *\n * Root `values` events are ignored because they represent the parent\n * thread state, not a subgraph. Tool/subagent namespaces are also\n * ignored because deep-agent task tools emit their own namespaced\n * `values` snapshots under `tools:*` / `task:*`; those are discovered\n * by `SubagentDiscovery` and must not be duplicated as subgraphs.\n *\n * A `values` event does not carry lifecycle terminal status, so the\n * entry remains marked `running` until a matching lifecycle terminal\n * event arrives. If no terminal arrives, the discovery map still\n * contains the host namespace, which is the important invariant for\n * scoped selectors.\n */\n #onValuesEvent(event: ValuesEvent): void {\n const namespace = event.params.namespace;\n if (isRootNamespace(namespace)) return;\n if (isInternalWorkNamespace(namespace)) return;\n const data = event.params.data;\n if (data == null || typeof data !== \"object\" || Array.isArray(data)) return;\n\n const id = namespaceKey(namespace);\n const lastSegment = namespace[namespace.length - 1] ?? \"\";\n const nodeName = parseNodeName(lastSegment);\n const entry = this.#ensureShadow(id, namespace, nodeName);\n entry.status = \"running\";\n\n if (!this.#promoted.has(id)) {\n this.#promoted.add(id);\n }\n this.#commit();\n }\n\n get snapshot(): SubgraphMap {\n return this.store.getSnapshot();\n }\n\n get byNodeSnapshot(): SubgraphByNodeMap {\n return this.byNodeStore.getSnapshot();\n }\n\n /**\n * Drop all discovery state. Called on thread rebind / dispose so a\n * new thread's subgraphs cannot bleed into the previous UI.\n */\n reset(): void {\n this.#shadow.clear();\n this.#promoted.clear();\n this.store.setValue(EMPTY_SUBGRAPH_MAP);\n this.byNodeStore.setValue(EMPTY_SUBGRAPH_BY_NODE_MAP);\n }\n\n #ensureShadow(\n id: string,\n namespace: readonly string[],\n nodeName: string\n ): MutableSubgraph {\n let entry = this.#shadow.get(id);\n if (entry == null) {\n entry = {\n id,\n namespace: [...namespace],\n nodeName,\n status: \"running\",\n startedAt: new Date(),\n completedAt: null,\n };\n this.#shadow.set(id, entry);\n }\n return entry;\n }\n\n #commit(): void {\n const snapshots: SubgraphDiscoverySnapshot[] = [];\n for (const id of this.#promoted) {\n const entry = this.#shadow.get(id);\n // A namespace can be promoted before its own lifecycle event\n // arrives if descendant events outpace the prefix event. Skip\n // until the shadow entry lands; the next push() promoting or\n // updating this namespace will re-commit.\n if (entry == null) continue;\n snapshots.push({ ...entry });\n }\n\n this.store.setValue(new Map(snapshots.map((s) => [s.id, s])));\n\n const byNode = new Map<string, SubgraphDiscoverySnapshot[]>();\n for (const snap of snapshots) {\n const bucket = byNode.get(snap.nodeName);\n if (bucket == null) byNode.set(snap.nodeName, [snap]);\n else bucket.push(snap);\n }\n this.byNodeStore.setValue(byNode);\n }\n}\n"],"mappings":"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"}
@@ -13,6 +13,11 @@ declare class SubgraphDiscovery {
13
13
  push(event: Event): void;
14
14
  get snapshot(): SubgraphMap;
15
15
  get byNodeSnapshot(): SubgraphByNodeMap;
16
+ /**
17
+ * Drop all discovery state. Called on thread rebind / dispose so a
18
+ * new thread's subgraphs cannot bleed into the previous UI.
19
+ */
20
+ reset(): void;
16
21
  }
17
22
  //#endregion
18
23
  export { SubgraphByNodeMap, SubgraphDiscovery, SubgraphMap };
@@ -1 +1 @@
1
- {"version":3,"file":"subgraphs.d.cts","names":[],"sources":["../../../src/stream/discovery/subgraphs.ts"],"mappings":";;;;;KAoDY,WAAA,GAAc,WAAA,SAAoB,yBAAA;AAAA,KAClC,iBAAA,GAAoB,WAAA,kBAErB,yBAAA;AAAA,cAuBE,iBAAA;EAAA;WACF,KAAA,EAAK,WAAA,CAAA,WAAA;EAAA,SACL,WAAA,EAAW,WAAA,CAAA,iBAAA;;EAmBpB,IAAA,CAAK,KAAA,EAAO,KAAA;EAAA,IA2GR,QAAA,CAAA,GAAY,WAAA;EAAA,IAIZ,cAAA,CAAA,GAAkB,iBAAA;AAAA"}
1
+ {"version":3,"file":"subgraphs.d.cts","names":[],"sources":["../../../src/stream/discovery/subgraphs.ts"],"mappings":";;;;;KAoDY,WAAA,GAAc,WAAA,SAAoB,yBAAA;AAAA,KAClC,iBAAA,GAAoB,WAAA,kBAErB,yBAAA;AAAA,cA2BE,iBAAA;EAAA;WACF,KAAA,EAAK,WAAA,CAAA,WAAA;EAAA,SACL,WAAA,EAAW,WAAA,CAAA,iBAAA;;EAmBpB,IAAA,CAAK,KAAA,EAAO,KAAA;EAAA,IA2GR,QAAA,CAAA,GAAY,WAAA;EAAA,IAIZ,cAAA,CAAA,GAAkB,iBAAA;;;;;EAQtB,KAAA,CAAA;AAAA"}
@@ -13,6 +13,11 @@ declare class SubgraphDiscovery {
13
13
  push(event: Event): void;
14
14
  get snapshot(): SubgraphMap;
15
15
  get byNodeSnapshot(): SubgraphByNodeMap;
16
+ /**
17
+ * Drop all discovery state. Called on thread rebind / dispose so a
18
+ * new thread's subgraphs cannot bleed into the previous UI.
19
+ */
20
+ reset(): void;
16
21
  }
17
22
  //#endregion
18
23
  export { SubgraphByNodeMap, SubgraphDiscovery, SubgraphMap };
@@ -1 +1 @@
1
- {"version":3,"file":"subgraphs.d.ts","names":[],"sources":["../../../src/stream/discovery/subgraphs.ts"],"mappings":";;;;;KAoDY,WAAA,GAAc,WAAA,SAAoB,yBAAA;AAAA,KAClC,iBAAA,GAAoB,WAAA,kBAErB,yBAAA;AAAA,cAuBE,iBAAA;EAAA;WACF,KAAA,EAAK,WAAA,CAAA,WAAA;EAAA,SACL,WAAA,EAAW,WAAA,CAAA,iBAAA;;EAmBpB,IAAA,CAAK,KAAA,EAAO,KAAA;EAAA,IA2GR,QAAA,CAAA,GAAY,WAAA;EAAA,IAIZ,cAAA,CAAA,GAAkB,iBAAA;AAAA"}
1
+ {"version":3,"file":"subgraphs.d.ts","names":[],"sources":["../../../src/stream/discovery/subgraphs.ts"],"mappings":";;;;;KAoDY,WAAA,GAAc,WAAA,SAAoB,yBAAA;AAAA,KAClC,iBAAA,GAAoB,WAAA,kBAErB,yBAAA;AAAA,cA2BE,iBAAA;EAAA;WACF,KAAA,EAAK,WAAA,CAAA,WAAA;EAAA,SACL,WAAA,EAAW,WAAA,CAAA,iBAAA;;EAmBpB,IAAA,CAAK,KAAA,EAAO,KAAA;EAAA,IA2GR,QAAA,CAAA,GAAY,WAAA;EAAA,IAIZ,cAAA,CAAA,GAAkB,iBAAA;;;;;EAQtB,KAAA,CAAA;AAAA"}
@@ -1,6 +1,9 @@
1
1
  import { StreamStore } from "../store.js";
2
2
  import { isInternalWorkNamespace, isRootNamespace, namespaceKey } from "../namespace.js";
3
3
  //#region src/stream/discovery/subgraphs.ts
4
+ /** Stable empty maps — reused on {@link SubgraphDiscovery.reset}. */
5
+ const EMPTY_SUBGRAPH_MAP = /* @__PURE__ */ new Map();
6
+ const EMPTY_SUBGRAPH_BY_NODE_MAP = /* @__PURE__ */ new Map();
4
7
  /**
5
8
  * LangGraph namespaces a node invocation as `<node_name>:<uuid>`
6
9
  * (parallel fan-outs share `<node_name>` as a prefix but each get a
@@ -115,6 +118,16 @@ var SubgraphDiscovery = class {
115
118
  get byNodeSnapshot() {
116
119
  return this.byNodeStore.getSnapshot();
117
120
  }
121
+ /**
122
+ * Drop all discovery state. Called on thread rebind / dispose so a
123
+ * new thread's subgraphs cannot bleed into the previous UI.
124
+ */
125
+ reset() {
126
+ this.#shadow.clear();
127
+ this.#promoted.clear();
128
+ this.store.setValue(EMPTY_SUBGRAPH_MAP);
129
+ this.byNodeStore.setValue(EMPTY_SUBGRAPH_BY_NODE_MAP);
130
+ }
118
131
  #ensureShadow(id, namespace, nodeName) {
119
132
  let entry = this.#shadow.get(id);
120
133
  if (entry == null) {
@@ -1 +1 @@
1
- {"version":3,"file":"subgraphs.js","names":["#onValuesEvent","#promoted","#shadow","#ensureShadow","#commit"],"sources":["../../../src/stream/discovery/subgraphs.ts"],"sourcesContent":["/**\n * Root-scoped subgraph discovery.\n *\n * Watches namespaced `lifecycle` events on the root subscription and\n * assembles two views of the subgraph set:\n *\n * - {@link SubgraphMap}: `Map<namespaceKey, SubgraphDiscoverySnapshot>`,\n * the canonical identity-keyed view consumed by the channel\n * registry and selector hooks.\n * - {@link SubgraphByNodeMap}: `Map<nodeName, readonly\n * SubgraphDiscoverySnapshot[]>`, a convenience index so callers\n * can look up subgraphs by the graph node that produced them\n * (`addNode(\"visualizer_0\", …)`) without parsing namespaces.\n * Arrays preserve insertion order, which matters for parallel\n * fan-outs that share a node name.\n *\n * # What counts as a subgraph\n *\n * The server emits a namespaced `lifecycle` event for every node\n * invocation — a plain function node (`orchestrator`) and a subgraph\n * host (`research`) look identical on the wire. We classify a\n * namespace as a subgraph iff at least one strictly-deeper namespace\n * has been observed with it as a prefix. Concretely, given a stream\n * whose lifecycle events hit the namespaces\n *\n * `[\"orchestrator:u1\"]`\n * `[\"research:u2\"]`\n * `[\"research:u2\", \"researcher:u3\"]`\n * `[\"research:u2\", \"tools:u4\"]`\n * `[\"writer:u5\"]`\n *\n * only `[\"research:u2\"]` is promoted — it's the only namespace that\n * hosts deeper executions. `orchestrator` and `writer` are plain\n * function-node leaves; the `researcher` / `tools` entries are the\n * subgraph's internal nodes, not subgraphs in their own right.\n *\n * Promotion is monotonic (a namespace never loses subgraph status)\n * and retroactive: a namespace whose own `started` event arrived\n * before any descendant is promoted later when the first descendant\n * event lands. Latency is bounded by the gap between a parent node\n * entering and its first inner node materializing — typically tens\n * of milliseconds.\n */\nimport type { Event, LifecycleEvent, ValuesEvent } from \"@langchain/protocol\";\nimport { StreamStore } from \"../store.js\";\nimport type { SubgraphDiscoverySnapshot } from \"../types.js\";\nimport {\n isInternalWorkNamespace,\n isRootNamespace,\n namespaceKey,\n} from \"../namespace.js\";\n\nexport type SubgraphMap = ReadonlyMap<string, SubgraphDiscoverySnapshot>;\nexport type SubgraphByNodeMap = ReadonlyMap<\n string,\n readonly SubgraphDiscoverySnapshot[]\n>;\n\ninterface MutableSubgraph {\n id: string;\n namespace: readonly string[];\n nodeName: string;\n status: \"running\" | \"complete\" | \"error\";\n startedAt: Date;\n completedAt: Date | null;\n}\n\n/**\n * LangGraph namespaces a node invocation as `<node_name>:<uuid>`\n * (parallel fan-outs share `<node_name>` as a prefix but each get a\n * fresh uuid). Extract the node-name half so callers can key\n * discovery lookups on names they wrote in `addNode(...)`.\n */\nfunction parseNodeName(segment: string): string {\n const colon = segment.indexOf(\":\");\n return colon === -1 ? segment : segment.slice(0, colon);\n}\n\nexport class SubgraphDiscovery {\n readonly store = new StreamStore<SubgraphMap>(new Map());\n readonly byNodeStore = new StreamStore<SubgraphByNodeMap>(new Map());\n\n /**\n * Latest known status for every namespaced lifecycle event we have\n * ever observed. A shadow entry is NOT necessarily a subgraph —\n * it is only projected into the committed stores once the same\n * namespace also appears in {@link #promoted}.\n */\n #shadow = new Map<string, MutableSubgraph>();\n\n /**\n * Namespaces that have been observed as a strict prefix of a\n * deeper namespace and are therefore confirmed subgraph hosts.\n * Insertion order is preserved and becomes the iteration order\n * of the committed snapshot maps.\n */\n #promoted = new Set<string>();\n\n /** Feed a single root event. Non-discovery events are ignored. */\n push(event: Event): void {\n if (event.method === \"values\") {\n this.#onValuesEvent(event as ValuesEvent);\n return;\n }\n if (event.method !== \"lifecycle\") return;\n const lifecycle = event as LifecycleEvent;\n const namespace = lifecycle.params.namespace;\n // Root lifecycle events describe the main run; subgraph discovery\n // only cares about namespaced lifecycle events.\n if (isRootNamespace(namespace)) return;\n const id = namespaceKey(namespace);\n const data = lifecycle.params.data as { event?: string };\n const lastSegment = namespace[namespace.length - 1] ?? \"\";\n const nodeName = parseNodeName(lastSegment);\n\n let touched = false;\n\n // Promote every strict ancestor the first time we see it as a\n // prefix. The ancestor may or may not yet have a shadow entry;\n // #commit() tolerates either case.\n for (let depth = 1; depth < namespace.length; depth += 1) {\n const ancestorId = namespaceKey(namespace.slice(0, depth));\n if (!this.#promoted.has(ancestorId)) {\n this.#promoted.add(ancestorId);\n if (this.#shadow.has(ancestorId)) touched = true;\n }\n }\n\n // Update shadow status for this namespace itself.\n if (data.event === \"started\") {\n if (!this.#shadow.has(id)) {\n this.#shadow.set(id, {\n id,\n namespace: [...namespace],\n nodeName,\n status: \"running\",\n startedAt: new Date(),\n completedAt: null,\n });\n if (this.#promoted.has(id)) touched = true;\n }\n } else if (\n data.event === \"completed\" ||\n data.event === \"interrupted\" ||\n data.event === \"failed\"\n ) {\n // Synthesize a shadow entry if we missed the `started` event\n // (common when a late subscription attaches to a running run).\n const entry = this.#ensureShadow(id, namespace, nodeName);\n if (data.event === \"failed\") {\n entry.status = \"error\";\n } else {\n entry.status = \"complete\";\n }\n entry.completedAt = new Date();\n if (this.#promoted.has(id)) touched = true;\n }\n\n if (touched) this.#commit();\n }\n\n /**\n * Promote subgraph host namespaces from namespaced `values` snapshots.\n *\n * Older protocol streams exposed subgraph structure primarily through\n * nested `lifecycle` events: a host namespace such as\n * `[\"research:<uuid>\"]` was promoted once a deeper namespace like\n * `[\"research:<uuid>\", \"inner:<uuid>\"]` appeared. Some runtimes now\n * emit the useful subgraph signal as `values` snapshots scoped directly\n * to the host namespace instead, without forwarding inner lifecycle\n * events to the client. In that shape, the namespace on the `values`\n * event is already the selector target that `useMessages(stream,\n * subgraph)` should subscribe to, so we create/promote the shadow\n * subgraph entry from that namespace.\n *\n * Root `values` events are ignored because they represent the parent\n * thread state, not a subgraph. Tool/subagent namespaces are also\n * ignored because deep-agent task tools emit their own namespaced\n * `values` snapshots under `tools:*` / `task:*`; those are discovered\n * by `SubagentDiscovery` and must not be duplicated as subgraphs.\n *\n * A `values` event does not carry lifecycle terminal status, so the\n * entry remains marked `running` until a matching lifecycle terminal\n * event arrives. If no terminal arrives, the discovery map still\n * contains the host namespace, which is the important invariant for\n * scoped selectors.\n */\n #onValuesEvent(event: ValuesEvent): void {\n const namespace = event.params.namespace;\n if (isRootNamespace(namespace)) return;\n if (isInternalWorkNamespace(namespace)) return;\n const data = event.params.data;\n if (data == null || typeof data !== \"object\" || Array.isArray(data)) return;\n\n const id = namespaceKey(namespace);\n const lastSegment = namespace[namespace.length - 1] ?? \"\";\n const nodeName = parseNodeName(lastSegment);\n const entry = this.#ensureShadow(id, namespace, nodeName);\n entry.status = \"running\";\n\n if (!this.#promoted.has(id)) {\n this.#promoted.add(id);\n }\n this.#commit();\n }\n\n get snapshot(): SubgraphMap {\n return this.store.getSnapshot();\n }\n\n get byNodeSnapshot(): SubgraphByNodeMap {\n return this.byNodeStore.getSnapshot();\n }\n\n #ensureShadow(\n id: string,\n namespace: readonly string[],\n nodeName: string\n ): MutableSubgraph {\n let entry = this.#shadow.get(id);\n if (entry == null) {\n entry = {\n id,\n namespace: [...namespace],\n nodeName,\n status: \"running\",\n startedAt: new Date(),\n completedAt: null,\n };\n this.#shadow.set(id, entry);\n }\n return entry;\n }\n\n #commit(): void {\n const snapshots: SubgraphDiscoverySnapshot[] = [];\n for (const id of this.#promoted) {\n const entry = this.#shadow.get(id);\n // A namespace can be promoted before its own lifecycle event\n // arrives if descendant events outpace the prefix event. Skip\n // until the shadow entry lands; the next push() promoting or\n // updating this namespace will re-commit.\n if (entry == null) continue;\n snapshots.push({ ...entry });\n }\n\n this.store.setValue(new Map(snapshots.map((s) => [s.id, s])));\n\n const byNode = new Map<string, SubgraphDiscoverySnapshot[]>();\n for (const snap of snapshots) {\n const bucket = byNode.get(snap.nodeName);\n if (bucket == null) byNode.set(snap.nodeName, [snap]);\n else bucket.push(snap);\n }\n this.byNodeStore.setValue(byNode);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"subgraphs.js","names":["#onValuesEvent","#promoted","#shadow","#ensureShadow","#commit"],"sources":["../../../src/stream/discovery/subgraphs.ts"],"sourcesContent":["/**\n * Root-scoped subgraph discovery.\n *\n * Watches namespaced `lifecycle` events on the root subscription and\n * assembles two views of the subgraph set:\n *\n * - {@link SubgraphMap}: `Map<namespaceKey, SubgraphDiscoverySnapshot>`,\n * the canonical identity-keyed view consumed by the channel\n * registry and selector hooks.\n * - {@link SubgraphByNodeMap}: `Map<nodeName, readonly\n * SubgraphDiscoverySnapshot[]>`, a convenience index so callers\n * can look up subgraphs by the graph node that produced them\n * (`addNode(\"visualizer_0\", …)`) without parsing namespaces.\n * Arrays preserve insertion order, which matters for parallel\n * fan-outs that share a node name.\n *\n * # What counts as a subgraph\n *\n * The server emits a namespaced `lifecycle` event for every node\n * invocation — a plain function node (`orchestrator`) and a subgraph\n * host (`research`) look identical on the wire. We classify a\n * namespace as a subgraph iff at least one strictly-deeper namespace\n * has been observed with it as a prefix. Concretely, given a stream\n * whose lifecycle events hit the namespaces\n *\n * `[\"orchestrator:u1\"]`\n * `[\"research:u2\"]`\n * `[\"research:u2\", \"researcher:u3\"]`\n * `[\"research:u2\", \"tools:u4\"]`\n * `[\"writer:u5\"]`\n *\n * only `[\"research:u2\"]` is promoted — it's the only namespace that\n * hosts deeper executions. `orchestrator` and `writer` are plain\n * function-node leaves; the `researcher` / `tools` entries are the\n * subgraph's internal nodes, not subgraphs in their own right.\n *\n * Promotion is monotonic (a namespace never loses subgraph status)\n * and retroactive: a namespace whose own `started` event arrived\n * before any descendant is promoted later when the first descendant\n * event lands. Latency is bounded by the gap between a parent node\n * entering and its first inner node materializing — typically tens\n * of milliseconds.\n */\nimport type { Event, LifecycleEvent, ValuesEvent } from \"@langchain/protocol\";\nimport { StreamStore } from \"../store.js\";\nimport type { SubgraphDiscoverySnapshot } from \"../types.js\";\nimport {\n isInternalWorkNamespace,\n isRootNamespace,\n namespaceKey,\n} from \"../namespace.js\";\n\nexport type SubgraphMap = ReadonlyMap<string, SubgraphDiscoverySnapshot>;\nexport type SubgraphByNodeMap = ReadonlyMap<\n string,\n readonly SubgraphDiscoverySnapshot[]\n>;\n\n/** Stable empty maps — reused on {@link SubgraphDiscovery.reset}. */\nconst EMPTY_SUBGRAPH_MAP: SubgraphMap = new Map();\nconst EMPTY_SUBGRAPH_BY_NODE_MAP: SubgraphByNodeMap = new Map();\n\ninterface MutableSubgraph {\n id: string;\n namespace: readonly string[];\n nodeName: string;\n status: \"running\" | \"complete\" | \"error\";\n startedAt: Date;\n completedAt: Date | null;\n}\n\n/**\n * LangGraph namespaces a node invocation as `<node_name>:<uuid>`\n * (parallel fan-outs share `<node_name>` as a prefix but each get a\n * fresh uuid). Extract the node-name half so callers can key\n * discovery lookups on names they wrote in `addNode(...)`.\n */\nfunction parseNodeName(segment: string): string {\n const colon = segment.indexOf(\":\");\n return colon === -1 ? segment : segment.slice(0, colon);\n}\n\nexport class SubgraphDiscovery {\n readonly store = new StreamStore<SubgraphMap>(new Map());\n readonly byNodeStore = new StreamStore<SubgraphByNodeMap>(new Map());\n\n /**\n * Latest known status for every namespaced lifecycle event we have\n * ever observed. A shadow entry is NOT necessarily a subgraph —\n * it is only projected into the committed stores once the same\n * namespace also appears in {@link #promoted}.\n */\n #shadow = new Map<string, MutableSubgraph>();\n\n /**\n * Namespaces that have been observed as a strict prefix of a\n * deeper namespace and are therefore confirmed subgraph hosts.\n * Insertion order is preserved and becomes the iteration order\n * of the committed snapshot maps.\n */\n #promoted = new Set<string>();\n\n /** Feed a single root event. Non-discovery events are ignored. */\n push(event: Event): void {\n if (event.method === \"values\") {\n this.#onValuesEvent(event as ValuesEvent);\n return;\n }\n if (event.method !== \"lifecycle\") return;\n const lifecycle = event as LifecycleEvent;\n const namespace = lifecycle.params.namespace;\n // Root lifecycle events describe the main run; subgraph discovery\n // only cares about namespaced lifecycle events.\n if (isRootNamespace(namespace)) return;\n const id = namespaceKey(namespace);\n const data = lifecycle.params.data as { event?: string };\n const lastSegment = namespace[namespace.length - 1] ?? \"\";\n const nodeName = parseNodeName(lastSegment);\n\n let touched = false;\n\n // Promote every strict ancestor the first time we see it as a\n // prefix. The ancestor may or may not yet have a shadow entry;\n // #commit() tolerates either case.\n for (let depth = 1; depth < namespace.length; depth += 1) {\n const ancestorId = namespaceKey(namespace.slice(0, depth));\n if (!this.#promoted.has(ancestorId)) {\n this.#promoted.add(ancestorId);\n if (this.#shadow.has(ancestorId)) touched = true;\n }\n }\n\n // Update shadow status for this namespace itself.\n if (data.event === \"started\") {\n if (!this.#shadow.has(id)) {\n this.#shadow.set(id, {\n id,\n namespace: [...namespace],\n nodeName,\n status: \"running\",\n startedAt: new Date(),\n completedAt: null,\n });\n if (this.#promoted.has(id)) touched = true;\n }\n } else if (\n data.event === \"completed\" ||\n data.event === \"interrupted\" ||\n data.event === \"failed\"\n ) {\n // Synthesize a shadow entry if we missed the `started` event\n // (common when a late subscription attaches to a running run).\n const entry = this.#ensureShadow(id, namespace, nodeName);\n if (data.event === \"failed\") {\n entry.status = \"error\";\n } else {\n entry.status = \"complete\";\n }\n entry.completedAt = new Date();\n if (this.#promoted.has(id)) touched = true;\n }\n\n if (touched) this.#commit();\n }\n\n /**\n * Promote subgraph host namespaces from namespaced `values` snapshots.\n *\n * Older protocol streams exposed subgraph structure primarily through\n * nested `lifecycle` events: a host namespace such as\n * `[\"research:<uuid>\"]` was promoted once a deeper namespace like\n * `[\"research:<uuid>\", \"inner:<uuid>\"]` appeared. Some runtimes now\n * emit the useful subgraph signal as `values` snapshots scoped directly\n * to the host namespace instead, without forwarding inner lifecycle\n * events to the client. In that shape, the namespace on the `values`\n * event is already the selector target that `useMessages(stream,\n * subgraph)` should subscribe to, so we create/promote the shadow\n * subgraph entry from that namespace.\n *\n * Root `values` events are ignored because they represent the parent\n * thread state, not a subgraph. Tool/subagent namespaces are also\n * ignored because deep-agent task tools emit their own namespaced\n * `values` snapshots under `tools:*` / `task:*`; those are discovered\n * by `SubagentDiscovery` and must not be duplicated as subgraphs.\n *\n * A `values` event does not carry lifecycle terminal status, so the\n * entry remains marked `running` until a matching lifecycle terminal\n * event arrives. If no terminal arrives, the discovery map still\n * contains the host namespace, which is the important invariant for\n * scoped selectors.\n */\n #onValuesEvent(event: ValuesEvent): void {\n const namespace = event.params.namespace;\n if (isRootNamespace(namespace)) return;\n if (isInternalWorkNamespace(namespace)) return;\n const data = event.params.data;\n if (data == null || typeof data !== \"object\" || Array.isArray(data)) return;\n\n const id = namespaceKey(namespace);\n const lastSegment = namespace[namespace.length - 1] ?? \"\";\n const nodeName = parseNodeName(lastSegment);\n const entry = this.#ensureShadow(id, namespace, nodeName);\n entry.status = \"running\";\n\n if (!this.#promoted.has(id)) {\n this.#promoted.add(id);\n }\n this.#commit();\n }\n\n get snapshot(): SubgraphMap {\n return this.store.getSnapshot();\n }\n\n get byNodeSnapshot(): SubgraphByNodeMap {\n return this.byNodeStore.getSnapshot();\n }\n\n /**\n * Drop all discovery state. Called on thread rebind / dispose so a\n * new thread's subgraphs cannot bleed into the previous UI.\n */\n reset(): void {\n this.#shadow.clear();\n this.#promoted.clear();\n this.store.setValue(EMPTY_SUBGRAPH_MAP);\n this.byNodeStore.setValue(EMPTY_SUBGRAPH_BY_NODE_MAP);\n }\n\n #ensureShadow(\n id: string,\n namespace: readonly string[],\n nodeName: string\n ): MutableSubgraph {\n let entry = this.#shadow.get(id);\n if (entry == null) {\n entry = {\n id,\n namespace: [...namespace],\n nodeName,\n status: \"running\",\n startedAt: new Date(),\n completedAt: null,\n };\n this.#shadow.set(id, entry);\n }\n return entry;\n }\n\n #commit(): void {\n const snapshots: SubgraphDiscoverySnapshot[] = [];\n for (const id of this.#promoted) {\n const entry = this.#shadow.get(id);\n // A namespace can be promoted before its own lifecycle event\n // arrives if descendant events outpace the prefix event. Skip\n // until the shadow entry lands; the next push() promoting or\n // updating this namespace will re-commit.\n if (entry == null) continue;\n snapshots.push({ ...entry });\n }\n\n this.store.setValue(new Map(snapshots.map((s) => [s.id, s])));\n\n const byNode = new Map<string, SubgraphDiscoverySnapshot[]>();\n for (const snap of snapshots) {\n const bucket = byNode.get(snap.nodeName);\n if (bucket == null) byNode.set(snap.nodeName, [snap]);\n else bucket.push(snap);\n }\n this.byNodeStore.setValue(byNode);\n }\n}\n"],"mappings":"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"}
@@ -23,7 +23,7 @@ let uuid = require("uuid");
23
23
  * 4. Apply the {@link StreamSubmitOptions.multitaskStrategy} to
24
24
  * decide whether to abort, enqueue, reject, or proceed.
25
25
  * 5. Race the dispatch promise (`thread.submitRun()` or
26
- * `thread.respondInput()` for resumes) against the next root
26
+ * `thread.submitRun()` for resumes) against the next root
27
27
  * terminal lifecycle event.
28
28
  * 6. Settle the resulting state (loading flag, error slot) and
29
29
  * drain the next queued submission, if any.
@@ -118,8 +118,8 @@ var SubmitCoordinator = class {
118
118
  #waitForRootPumpReady;
119
119
  /** Resolves on the next root terminal lifecycle (or on abort). */
120
120
  #awaitNextTerminal;
121
- /** Returns the most recent unresolved root interrupt, for resumes. */
122
- #latestUnresolvedInterrupt;
121
+ /** Builds interrupt-id keyed `run.start` input from `command.resume`. */
122
+ #buildResumeRunInput;
123
123
  /** Marks an interrupt id as resolved so it isn't re-targeted. */
124
124
  #markInterruptResolved;
125
125
  /** Called once at the start of every {@link submit} invocation. */
@@ -155,7 +155,7 @@ var SubmitCoordinator = class {
155
155
  this.#abandonDeferredRootPump = params.abandonDeferredRootPump;
156
156
  this.#waitForRootPumpReady = params.waitForRootPumpReady;
157
157
  this.#awaitNextTerminal = params.awaitNextTerminal;
158
- this.#latestUnresolvedInterrupt = params.latestUnresolvedInterrupt;
158
+ this.#buildResumeRunInput = params.buildResumeRunInput;
159
159
  this.#markInterruptResolved = params.markInterruptResolved;
160
160
  this.#onSubmitStart = params.onSubmitStart ?? (() => void 0);
161
161
  this.#onRunStart = params.onRunStart ?? (() => void 0);
@@ -255,17 +255,15 @@ var SubmitCoordinator = class {
255
255
  try {
256
256
  let terminal;
257
257
  if (isResume) {
258
- const target = this.#latestUnresolvedInterrupt();
259
- if (target == null) throw new Error("submit({ command: { resume } }) called but no pending protocol interrupt is available.");
260
- const commandPromise = thread.respondInput({
261
- namespace: target.namespace,
262
- interrupt_id: target.interruptId,
263
- response: resumeCommand,
258
+ const resumeInput = this.#buildResumeRunInput(resumeCommand);
259
+ if (resumeInput == null) throw new Error("submit({ command: { resume } }) called but no pending protocol interrupt is available.");
260
+ const commandPromise = thread.submitRun({
261
+ input: resumeInput,
264
262
  config: boundConfig,
265
- metadata: options?.metadata
263
+ metadata: options?.metadata ?? void 0
266
264
  });
267
265
  commandPromise.then(() => this.#startDeferredRootPump(), () => {});
268
- this.#markInterruptResolved(target.interruptId);
266
+ for (const interruptId of Object.keys(resumeInput)) this.#markInterruptResolved(interruptId);
269
267
  const first = await Promise.race([terminalPromise.then((value) => ({
270
268
  type: "terminal",
271
269
  value
@@ -1 +1 @@
1
- {"version":3,"file":"submit-coordinator.cjs","names":["#options","#rootStore","#queueStore","#getDisposed","#getCurrentThreadId","#setCurrentThreadId","#rememberSelfCreatedThreadId","#forgetSelfCreatedThreadId","#hydrate","#ensureThread","#startDeferredRootPump","#abandonDeferredRootPump","#waitForRootPumpReady","#awaitNextTerminal","#latestUnresolvedInterrupt","#markInterruptResolved","#onSubmitStart","#onRunStart","#onRunCreated","#onRunCompleted","#onRunEnd","#runAbort","#enqueueSubmission","#drainQueue"],"sources":["../../src/stream/submit-coordinator.ts"],"sourcesContent":["/**\n * Owns the run-submission lifecycle for a single\n * {@link StreamController}.\n *\n * # What this module is\n *\n * The {@link SubmitCoordinator} is the piece of the controller that\n * dispatches runs (`submit()`), enforces multitask strategies, queues\n * deferred submissions, races dispatch against terminal lifecycle\n * events, and surfaces errors back through the per-submit `onError`\n * callback and the root snapshot.\n *\n * Conceptually a submit looks like:\n *\n * 1. Optionally rebind to a different thread (`options.threadId`).\n * 2. Mint a thread id if one isn't bound yet.\n * 3. Wait for the controller's root pump to be ready (so the\n * transport is subscribed before the run is dispatched —\n * otherwise we could miss replayed events).\n * 4. Apply the {@link StreamSubmitOptions.multitaskStrategy} to\n * decide whether to abort, enqueue, reject, or proceed.\n * 5. Race the dispatch promise (`thread.submitRun()` or\n * `thread.respondInput()` for resumes) against the next root\n * terminal lifecycle event.\n * 6. Settle the resulting state (loading flag, error slot) and\n * drain the next queued submission, if any.\n *\n * # Why it lives in its own class\n *\n * The submit lifecycle is the most state-heavy part of the\n * controller — six promises, an abort controller, a queue, a\n * terminal-vs-command race, and bidirectional callback wiring with\n * the controller. Splitting it out keeps `controller.ts` focused on\n * subscription / projection wiring while letting the submit logic\n * evolve independently.\n *\n * # Why we race \"command\" against \"terminal\"\n *\n * For fast runs, the server's terminal lifecycle event can arrive\n * *before* the dispatch HTTP response has resolved. Racing the two\n * lets us detect terminal early and not block waiting for a now-stale\n * dispatch response. The dispatch response is still consumed (via\n * `.then(notifyCreated).catch(reportError)`) so `onCreated` still\n * fires and dispatch errors still surface through `onError`.\n *\n * # Queue semantics (`multitaskStrategy: \"enqueue\"`)\n *\n * When a run is already in flight, an `\"enqueue\"` submit is recorded\n * into {@link queueStore} and the call returns immediately. After the\n * active run terminates, `#drainQueue` schedules the head of the\n * queue as a fresh submit on the next macrotask. Each drained\n * submission has its own `multitaskStrategy` cleared so it doesn't\n * recursively re-enqueue.\n *\n * @see StreamController - The owner; injects every collaborator dep.\n */\nimport { v7 as uuidv7 } from \"uuid\";\nimport type { ThreadStream } from \"../client/stream/index.js\";\nimport { StreamStore } from \"./store.js\";\nimport type {\n RootSnapshot,\n RunExecutionReason,\n StreamControllerOptions,\n StreamSubmitOptions,\n} from \"./types.js\";\n\n/**\n * Pointer to a pending root protocol interrupt. Used to target\n * `respondInput` for resume submissions.\n */\ninterface ResolvedInterrupt {\n interruptId: string;\n namespace: string[];\n}\n\n/**\n * Result of awaiting the next root terminal lifecycle event. Mirrors\n * the three terminal lifecycle states the protocol surfaces, plus a\n * synthetic `\"aborted\"` for client-side cancellation.\n */\ntype TerminalResult = {\n event: \"completed\" | \"failed\" | \"interrupted\" | \"aborted\";\n error?: string;\n};\n\nfunction terminalReason(event: TerminalResult[\"event\"]): RunExecutionReason {\n if (event === \"completed\") return \"success\";\n if (event === \"failed\") return \"error\";\n if (event === \"interrupted\") return \"interrupt\";\n return \"stopped\";\n}\n\n/**\n * Queued submission entry mirrored from the server-side run queue.\n *\n * Surfaces the deferred submission to UI consumers via\n * {@link StreamController.queueStore}.\n */\nexport interface SubmissionQueueEntry<\n StateType extends object = Record<string, unknown>,\n> {\n /** Stable id minted on enqueue (uuidv7 — sortable by creation time). */\n readonly id: string;\n /** Original submit input, narrowed to the partial state shape. */\n readonly values: Partial<StateType> | null | undefined;\n /** Original submit options, minus the strategy slot which is reset on drain. */\n readonly options?: StreamSubmitOptions<StateType>;\n /** Wall-clock timestamp at enqueue. */\n readonly createdAt: Date;\n}\n\n/**\n * Read-only snapshot of the queue. The queue store hands this out\n * directly; consumers must not mutate the array.\n */\nexport type SubmissionQueueSnapshot<\n StateType extends object = Record<string, unknown>,\n> = ReadonlyArray<SubmissionQueueEntry<StateType>>;\n\n/**\n * Frozen empty queue value used as the initial / cleared snapshot.\n *\n * Reusing one frozen reference keeps store identity stable across\n * empty resets, so React's `useSyncExternalStore` doesn't think the\n * queue changed when it actually didn't.\n */\nexport const EMPTY_QUEUE: SubmissionQueueSnapshot<never> = Object.freeze([]);\n\n/**\n * Coordinates one controller's run-submission lifecycle.\n *\n * The constructor takes a bag of callbacks rather than a reference to\n * the parent {@link StreamController} on purpose:\n *\n * - It keeps the dependency surface explicit and testable — every\n * piece of controller state the submit lifecycle touches is one\n * of these closures.\n * - It avoids a cyclic dependency between controller and coordinator.\n * - Tests can construct one with stub callbacks and assert behavior\n * without mocking the entire controller.\n *\n * @typeParam StateType - Root state shape.\n * @typeParam InterruptType - Root interrupt payload shape.\n * @typeParam ConfigurableType - `config.configurable` shape accepted\n * by submit (usually `Record<string, unknown>`).\n */\nexport class SubmitCoordinator<\n StateType extends object = Record<string, unknown>,\n InterruptType = unknown,\n ConfigurableType extends object = Record<string, unknown>,\n> {\n /** Controller-level options forwarded into `submitRun` / callbacks. */\n readonly #options: StreamControllerOptions<StateType>;\n /** Root snapshot store; written for `isLoading`, `error`, `interrupts`. */\n readonly #rootStore: StreamStore<RootSnapshot<StateType, InterruptType>>;\n /** Pending submissions awaiting the active run to terminate. */\n readonly #queueStore: StreamStore<SubmissionQueueSnapshot<StateType>>;\n /** Probes the controller's `disposed` flag from deferred work. */\n readonly #getDisposed: () => boolean;\n /** Reads the controller's currently-bound thread id. */\n readonly #getCurrentThreadId: () => string | null;\n /** Updates the controller's thread id (used when minting a new id). */\n readonly #setCurrentThreadId: (threadId: string | null) => void;\n /** Records a thread id we created client-side so hydrate can skip a 404 round-trip. */\n readonly #rememberSelfCreatedThreadId: (threadId: string) => void;\n /** Drops a thread id from the self-created set once it's committed server-side. */\n readonly #forgetSelfCreatedThreadId: (threadId: string) => void;\n /** Triggers a hydrate on the controller (used by `options.threadId` rebinds). */\n readonly #hydrate: (threadId?: string | null) => Promise<void>;\n /** Lazily creates / returns the active {@link ThreadStream}. */\n readonly #ensureThread: (\n threadId: string,\n deferRootPump?: boolean\n ) => ThreadStream;\n /** Starts the previously-deferred root pump after a self-created thread commits. */\n readonly #startDeferredRootPump: () => void;\n /** Abandons a deferred root pump after a self-created dispatch fails. */\n readonly #abandonDeferredRootPump: () => void;\n /** Resolves once the controller's root subscription pump is up. */\n readonly #waitForRootPumpReady: () => Promise<void> | undefined;\n /** Resolves on the next root terminal lifecycle (or on abort). */\n readonly #awaitNextTerminal: (signal: AbortSignal) => Promise<TerminalResult>;\n /** Returns the most recent unresolved root interrupt, for resumes. */\n readonly #latestUnresolvedInterrupt: () => ResolvedInterrupt | null;\n /** Marks an interrupt id as resolved so it isn't re-targeted. */\n readonly #markInterruptResolved: (interruptId: string) => void;\n /** Called once at the start of every {@link submit} invocation. */\n readonly #onSubmitStart: () => void;\n /** Marks that a local run dispatch is now active. */\n readonly #onRunStart: () => void;\n /** Records a server-accepted local run id and fires `onCreated`. */\n readonly #onRunCreated: (runId: string) => void;\n /** Fires `onCompleted` for the local run lifecycle. */\n readonly #onRunCompleted: (\n reason: RunExecutionReason,\n runId?: string\n ) => void;\n /** Marks the local run dispatch lifecycle as settled. */\n readonly #onRunEnd: () => void;\n\n /**\n * Active submission's abort controller. `undefined` between submits.\n *\n * Used both for `multitaskStrategy: \"rollback\"` (abort the previous\n * controller's signal) and `stop()` (abort the current one without\n * starting a new one).\n */\n #runAbort: AbortController | undefined;\n\n constructor(params: {\n options: StreamControllerOptions<StateType>;\n rootStore: StreamStore<RootSnapshot<StateType, InterruptType>>;\n queueStore: StreamStore<SubmissionQueueSnapshot<StateType>>;\n getDisposed: () => boolean;\n getCurrentThreadId: () => string | null;\n setCurrentThreadId: (threadId: string | null) => void;\n rememberSelfCreatedThreadId: (threadId: string) => void;\n forgetSelfCreatedThreadId: (threadId: string) => void;\n hydrate: (threadId?: string | null) => Promise<void>;\n ensureThread: (threadId: string, deferRootPump?: boolean) => ThreadStream;\n startDeferredRootPump: () => void;\n abandonDeferredRootPump: () => void;\n waitForRootPumpReady: () => Promise<void> | undefined;\n awaitNextTerminal: (signal: AbortSignal) => Promise<TerminalResult>;\n latestUnresolvedInterrupt: () => ResolvedInterrupt | null;\n markInterruptResolved: (interruptId: string) => void;\n onSubmitStart?: () => void;\n onRunStart?: () => void;\n onRunCreated?: (runId: string) => void;\n onRunCompleted?: (reason: RunExecutionReason, runId?: string) => void;\n onRunEnd?: () => void;\n }) {\n this.#options = params.options;\n this.#rootStore = params.rootStore;\n this.#queueStore = params.queueStore;\n this.#getDisposed = params.getDisposed;\n this.#getCurrentThreadId = params.getCurrentThreadId;\n this.#setCurrentThreadId = params.setCurrentThreadId;\n this.#rememberSelfCreatedThreadId = params.rememberSelfCreatedThreadId;\n this.#forgetSelfCreatedThreadId = params.forgetSelfCreatedThreadId;\n this.#hydrate = params.hydrate;\n this.#ensureThread = params.ensureThread;\n this.#startDeferredRootPump = params.startDeferredRootPump;\n this.#abandonDeferredRootPump = params.abandonDeferredRootPump;\n this.#waitForRootPumpReady = params.waitForRootPumpReady;\n this.#awaitNextTerminal = params.awaitNextTerminal;\n this.#latestUnresolvedInterrupt = params.latestUnresolvedInterrupt;\n this.#markInterruptResolved = params.markInterruptResolved;\n this.#onSubmitStart = params.onSubmitStart ?? (() => undefined);\n this.#onRunStart = params.onRunStart ?? (() => undefined);\n this.#onRunCreated = params.onRunCreated ?? (() => undefined);\n this.#onRunCompleted = params.onRunCompleted ?? (() => undefined);\n this.#onRunEnd = params.onRunEnd ?? (() => undefined);\n }\n\n /**\n * Submit input or a resume command to the active thread.\n *\n * Honours {@link StreamSubmitOptions.multitaskStrategy}:\n *\n * - `\"rollback\"` (default) — aborts any in-flight run and\n * dispatches immediately.\n * - `\"reject\"` — throws synchronously when a run is\n * already in flight.\n * - `\"enqueue\"` — defers via {@link #enqueueSubmission};\n * the call returns without dispatching.\n * - `\"interrupt\"` — falls through to the default path\n * (server-side cancellation lands with roadmap A0.3).\n *\n * Errors are routed through both the per-submit `onError` callback\n * and `rootStore.error`. Aborts (controller dispose / rollback) are\n * silently dropped.\n *\n * @param input - Input payload, or `null`/`undefined` for no input\n * (typical for resume commands).\n * @param options - Per-submit options (config, metadata, callbacks,\n * strategy, etc).\n */\n async submit(\n input: unknown,\n options?: StreamSubmitOptions<StateType, ConfigurableType>\n ): Promise<void> {\n if (this.#getDisposed()) return;\n this.#onSubmitStart();\n\n // Per-submit thread override: rebind first so the rest of the\n // submit operates against the new thread.\n const overrideThreadId = options?.threadId;\n if (\n overrideThreadId !== undefined &&\n overrideThreadId !== this.#getCurrentThreadId()\n ) {\n await this.#hydrate(overrideThreadId);\n }\n\n // Self-created thread id path: mint client-side so the controller\n // (and Suspense boundaries) get a stable id even before the run\n // is dispatched.\n const wasSelfCreated = this.#getCurrentThreadId() == null;\n if (wasSelfCreated) {\n const threadId = uuidv7();\n this.#setCurrentThreadId(threadId);\n this.#rememberSelfCreatedThreadId(threadId);\n this.#options.onThreadId?.(threadId);\n this.#rootStore.setState((s) => ({\n ...s,\n threadId,\n }));\n }\n\n const currentThreadId = this.#getCurrentThreadId();\n if (currentThreadId == null) return;\n // For client-self-created threads we defer the persistent root SSE\n // pump until after `submitRun` / `respondInput` commits the thread\n // server-side. Opening the pump's `subscription.subscribe` against\n // a not-yet-existent thread row produces a `404: Thread not found`\n // protocol error that strands lifecycle / messages events for the\n // first run. The deferred path starts the pump after dispatch\n // returns (see `#startDeferredRootPump` calls below).\n const thread = this.#ensureThread(currentThreadId, wasSelfCreated);\n const activeThreadId = currentThreadId;\n // Wait for the root subscription to be live; otherwise the\n // dispatch could resolve before we're listening for events and\n // we'd miss the terminal that ends the run.\n await this.#waitForRootPumpReady();\n\n const strategy = options?.multitaskStrategy ?? \"rollback\";\n // `wasSelfCreated` short-circuit: when this submit just minted a\n // brand-new thread id (the user clicked \"New Thread\"), the\n // strategy check shouldn't see a run on the *previous* thread as\n // a reason to enqueue. The previous run is on a thread the user\n // navigated away from; abandoning its client-side abort tracking\n // is correct (the server-side run continues independently).\n // Without this, `enqueue` would trap the new submission and\n // `submitRun` never fires for the new thread — leaving a freshly-\n // minted thread id committed to the URL but never to the server.\n const hasActiveRun =\n !wasSelfCreated &&\n this.#runAbort != null &&\n !this.#runAbort.signal.aborted;\n if (hasActiveRun && strategy === \"reject\") {\n throw new Error(\n \"submit() rejected: a run is already in flight and multitaskStrategy is 'reject'.\"\n );\n }\n if (hasActiveRun && strategy === \"enqueue\") {\n this.#enqueueSubmission(input, options);\n return;\n }\n\n // Rollback: abort the previous run before starting a new one.\n this.#runAbort?.abort();\n const abort = new AbortController();\n this.#runAbort = abort;\n\n const resumeCommand = options?.command?.resume;\n const isResume = resumeCommand !== undefined;\n\n // Optimistically clear interrupts/error and flip loading. The\n // root pump's lifecycle listener will re-flip these as the run\n // terminates.\n this.#rootStore.setState((s) => ({\n ...s,\n interrupts: [],\n interrupt: undefined,\n error: undefined,\n isLoading: true,\n }));\n\n const boundConfig = bindThreadConfig(options?.config, currentThreadId);\n // Subscribe to the next terminal *before* dispatching so a fast\n // run's terminal can't race us.\n const terminalPromise = this.#awaitNextTerminal(abort.signal);\n this.#onRunStart();\n\n let terminalSettled = false;\n let createdRunId: string | undefined;\n let pendingCompletionReason: RunExecutionReason | undefined;\n let completionNotified = false;\n const notifyCompletion = (reason: RunExecutionReason): void => {\n if (completionNotified) return;\n if (!isResume && createdRunId == null) {\n pendingCompletionReason = reason;\n return;\n }\n completionNotified = true;\n this.#onRunCompleted(reason, createdRunId);\n };\n const reportError = (error: unknown): void => {\n if (abort.signal.aborted) return;\n this.#rootStore.setState((s) => ({ ...s, error }));\n try {\n options?.onError?.(error);\n } catch {\n /* caller-supplied callback errors must not crash the submit */\n }\n };\n\n try {\n let terminal: TerminalResult | undefined;\n\n if (isResume) {\n const target = this.#latestUnresolvedInterrupt();\n if (target == null) {\n throw new Error(\n \"submit({ command: { resume } }) called but no pending protocol interrupt is available.\"\n );\n }\n const commandPromise = thread.respondInput({\n namespace: target.namespace,\n interrupt_id: target.interruptId,\n response: resumeCommand,\n config: boundConfig,\n metadata: options?.metadata,\n });\n // Defer the pump start until the dispatch HTTP response\n // lands — see the analogous block in the non-resume path\n // below for the rationale (thread row not committed\n // synchronously). For a resume the thread exists already\n // (it must, since `latestUnresolvedInterrupt()` was non-null),\n // so `#startDeferredRootPump` is typically a no-op here, but\n // we keep the same shape to avoid a future regression.\n //\n // Asymmetry with the non-resume path: we don't call\n // `#forgetSelfCreatedThreadId` because a resume implies the\n // thread already committed, so the self-created marker was\n // already cleared on the original submit. Same reason we\n // don't call `#abandonDeferredRootPump` on failure here.\n void commandPromise.then(\n () => this.#startDeferredRootPump(),\n () => {}\n );\n // Mark resolved synchronously: even if the response races and\n // the command settles after the terminal, we don't want to\n // re-target this same interrupt on the next submit.\n this.#markInterruptResolved(target.interruptId);\n const first = await Promise.race([\n terminalPromise.then((value) => ({\n type: \"terminal\" as const,\n value,\n })),\n commandPromise.then(\n () => ({ type: \"command\" as const }),\n (error) => ({ type: \"error\" as const, error })\n ),\n ]);\n if (first.type === \"error\") throw first.error;\n if (first.type === \"terminal\") {\n terminal = first.value;\n terminalSettled = true;\n // Stale command response — surface as error only if it\n // arrives with a real failure (not just our own abort).\n void commandPromise.catch((error) => {\n if (!terminalSettled) reportError(error);\n });\n }\n } else {\n const commandPromise = thread.submitRun({\n input: input ?? null,\n config: boundConfig,\n metadata: (options?.metadata ?? undefined) as Record<string, unknown>,\n forkFrom: options?.forkFrom,\n multitaskStrategy:\n options?.multitaskStrategy === \"enqueue\"\n ? \"enqueue\"\n : options?.multitaskStrategy,\n });\n // Start the deferred root pump *after* the dispatch HTTP\n // response lands — that's when the thread row exists server-\n // side. Doing it synchronously here would race the response\n // and the pump's `subscription.subscribe` would 404. Same\n // reason we drop the self-created flag only after dispatch:\n // future hydrates need the thread to exist before they fetch\n // state.\n //\n // Fire-and-forget: we don't want to gate Promise.race on this,\n // and `commandPromise.catch` is already handled below. A\n // dispatch failure means there's no thread to pump anyway.\n void commandPromise.then(\n () => {\n this.#startDeferredRootPump();\n this.#forgetSelfCreatedThreadId(activeThreadId);\n },\n () => {\n // Dispatch failed. Without abandoning, `#rootPumpDeferred`\n // stays armed and `selfCreatedThreadIds` still holds this\n // id — a retry submit would see `wasSelfCreated=false`\n // (currentThreadId is no longer null), `#ensureThread`\n // would early-return because `#thread != null`, and the\n // root pump would never start. Tear down so the next\n // submit re-runs `#ensureThread` from scratch.\n if (wasSelfCreated) {\n this.#abandonDeferredRootPump();\n this.#forgetSelfCreatedThreadId(activeThreadId);\n }\n }\n );\n const notifyCreated = (result: { run_id?: unknown }) => {\n if (typeof result.run_id !== \"string\") return;\n createdRunId = result.run_id;\n this.#onRunCreated(createdRunId);\n if (pendingCompletionReason != null) {\n notifyCompletion(pendingCompletionReason);\n }\n };\n const first = await Promise.race([\n terminalPromise.then((value) => ({\n type: \"terminal\" as const,\n value,\n })),\n commandPromise.then(\n (result) => ({ type: \"command\" as const, result }),\n (error) => ({ type: \"error\" as const, error })\n ),\n ]);\n if (first.type === \"error\") throw first.error;\n if (first.type === \"command\") {\n notifyCreated(first.result);\n } else {\n // Terminal landed first (very fast runs). Wait for the\n // dispatch response in the background so onCreated fires\n // and dispatch errors still surface.\n terminal = first.value;\n terminalSettled = true;\n void commandPromise.then(notifyCreated).catch((error) => {\n if (!terminalSettled) reportError(error);\n });\n }\n }\n\n terminal ??= await terminalPromise;\n terminalSettled = true;\n if (terminal.event === \"failed\" && !abort.signal.aborted) {\n const runError = new Error(\n terminal.error ?? \"Run failed with no error message\"\n );\n this.#rootStore.setState((s) => ({ ...s, error: runError }));\n try {\n options?.onError?.(runError);\n } catch {\n /* caller-supplied callback errors must not crash the submit */\n }\n }\n notifyCompletion(terminalReason(terminal.event));\n } catch (error) {\n reportError(error);\n } finally {\n // Always settle loading and clear our slot of the abort\n // controller. Schedule queue drain on the next macrotask so any\n // late state updates from this run finish flushing first.\n this.#rootStore.setState((s) => ({ ...s, isLoading: false }));\n if (this.#runAbort === abort) this.#runAbort = undefined;\n this.#onRunEnd();\n setTimeout(() => this.#drainQueue(), 0);\n }\n }\n\n /**\n * Abort the current run (if any) and force `isLoading=false`.\n *\n * Does NOT issue a server-side cancel — that lands with roadmap\n * A0.3. Today this is a client-side stop only: subsequent events\n * for the aborted run are ignored by the controller's pump because\n * the abort signal is the same one `#awaitNextTerminal` is wired\n * to.\n */\n async stop(): Promise<void> {\n this.abortActiveRun();\n this.#rootStore.setState((s) => ({ ...s, isLoading: false }));\n }\n\n /**\n * Abort the current run without forcing the loading flag down.\n *\n * Used by {@link StreamController.dispose}: disposal already tears\n * down the root store, so flipping `isLoading` here is unnecessary\n * and would race the dispose path.\n */\n abortActiveRun(): void {\n this.#runAbort?.abort();\n this.#runAbort = undefined;\n }\n\n /**\n * Cancel a queued submission by id.\n *\n * @param id - Client-side queue entry id to remove.\n * @returns `true` when the entry was found and dropped, `false` otherwise.\n */\n async cancelQueued(id: string): Promise<boolean> {\n const current = this.#queueStore.getSnapshot();\n const next = current.filter((entry) => entry.id !== id);\n if (next.length === current.length) return false;\n this.#queueStore.setState(() => next);\n return true;\n }\n\n /**\n * Drop every queued submission. Server-side cancel arrives with A0.3.\n */\n async clearQueue(): Promise<void> {\n this.#queueStore.setState(\n () => EMPTY_QUEUE as SubmissionQueueSnapshot<StateType>\n );\n }\n\n /**\n * Append a submission to the queue without dispatching.\n *\n * The drained submission is later run via {@link #drainQueue} after\n * the active run terminates.\n */\n #enqueueSubmission(\n input: unknown,\n options?: StreamSubmitOptions<StateType, ConfigurableType>\n ): void {\n const entry: SubmissionQueueEntry<StateType> = {\n id: uuidv7(),\n values: (input ?? undefined) as Partial<StateType> | null | undefined,\n options: options as StreamSubmitOptions<StateType> | undefined,\n createdAt: new Date(),\n };\n this.#queueStore.setState((current) => [...current, entry]);\n }\n\n /**\n * Drain the head of the queue if no run is active.\n *\n * Called from the `finally` block of `submit()` on the next\n * macrotask (so the just-finished run's state flushes first).\n * Strips the strategy off the dequeued options to prevent infinite\n * re-enqueueing.\n */\n #drainQueue(): void {\n if (this.#getDisposed()) return;\n if (this.#runAbort != null && !this.#runAbort.signal.aborted) return;\n const current = this.#queueStore.getSnapshot();\n if (current.length === 0) return;\n const [next, ...rest] = current;\n this.#queueStore.setState(() => rest);\n const nextOptions: StreamSubmitOptions<StateType, ConfigurableType> = {\n ...((next.options ?? {}) as StreamSubmitOptions<\n StateType,\n ConfigurableType\n >),\n multitaskStrategy: undefined,\n };\n void this.submit(next.values, nextOptions).catch(() => {\n /* submit() already routes errors through the per-submit onError\n * hook and the root store; swallow here so a failing drain does\n * not surface as an unhandled rejection. */\n });\n }\n}\n\n/**\n * Merge `thread_id` into a user-supplied `config.configurable` blob.\n *\n * The platform expects `config.configurable.thread_id` on every run\n * dispatch; we set it last so user-supplied values can't accidentally\n * override the active thread id (which would route the run to a\n * different thread).\n */\nfunction bindThreadConfig(\n config: unknown,\n threadId: string\n): Record<string, unknown> {\n const base =\n config != null && typeof config === \"object\"\n ? (config as Record<string, unknown>)\n : {};\n const configurable =\n base.configurable != null && typeof base.configurable === \"object\"\n ? (base.configurable as Record<string, unknown>)\n : {};\n return {\n ...base,\n configurable: {\n ...configurable,\n thread_id: threadId,\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"submit-coordinator.cjs","names":["#options","#rootStore","#queueStore","#getDisposed","#getCurrentThreadId","#setCurrentThreadId","#rememberSelfCreatedThreadId","#forgetSelfCreatedThreadId","#hydrate","#ensureThread","#startDeferredRootPump","#abandonDeferredRootPump","#waitForRootPumpReady","#awaitNextTerminal","#buildResumeRunInput","#markInterruptResolved","#onSubmitStart","#onRunStart","#onRunCreated","#onRunCompleted","#onRunEnd","#runAbort","#enqueueSubmission","#drainQueue"],"sources":["../../src/stream/submit-coordinator.ts"],"sourcesContent":["/**\n * Owns the run-submission lifecycle for a single\n * {@link StreamController}.\n *\n * # What this module is\n *\n * The {@link SubmitCoordinator} is the piece of the controller that\n * dispatches runs (`submit()`), enforces multitask strategies, queues\n * deferred submissions, races dispatch against terminal lifecycle\n * events, and surfaces errors back through the per-submit `onError`\n * callback and the root snapshot.\n *\n * Conceptually a submit looks like:\n *\n * 1. Optionally rebind to a different thread (`options.threadId`).\n * 2. Mint a thread id if one isn't bound yet.\n * 3. Wait for the controller's root pump to be ready (so the\n * transport is subscribed before the run is dispatched —\n * otherwise we could miss replayed events).\n * 4. Apply the {@link StreamSubmitOptions.multitaskStrategy} to\n * decide whether to abort, enqueue, reject, or proceed.\n * 5. Race the dispatch promise (`thread.submitRun()` or\n * `thread.submitRun()` for resumes) against the next root\n * terminal lifecycle event.\n * 6. Settle the resulting state (loading flag, error slot) and\n * drain the next queued submission, if any.\n *\n * # Why it lives in its own class\n *\n * The submit lifecycle is the most state-heavy part of the\n * controller — six promises, an abort controller, a queue, a\n * terminal-vs-command race, and bidirectional callback wiring with\n * the controller. Splitting it out keeps `controller.ts` focused on\n * subscription / projection wiring while letting the submit logic\n * evolve independently.\n *\n * # Why we race \"command\" against \"terminal\"\n *\n * For fast runs, the server's terminal lifecycle event can arrive\n * *before* the dispatch HTTP response has resolved. Racing the two\n * lets us detect terminal early and not block waiting for a now-stale\n * dispatch response. The dispatch response is still consumed (via\n * `.then(notifyCreated).catch(reportError)`) so `onCreated` still\n * fires and dispatch errors still surface through `onError`.\n *\n * # Queue semantics (`multitaskStrategy: \"enqueue\"`)\n *\n * When a run is already in flight, an `\"enqueue\"` submit is recorded\n * into {@link queueStore} and the call returns immediately. After the\n * active run terminates, `#drainQueue` schedules the head of the\n * queue as a fresh submit on the next macrotask. Each drained\n * submission has its own `multitaskStrategy` cleared so it doesn't\n * recursively re-enqueue.\n *\n * @see StreamController - The owner; injects every collaborator dep.\n */\nimport { v7 as uuidv7 } from \"uuid\";\nimport type { ThreadStream } from \"../client/stream/index.js\";\nimport { StreamStore } from \"./store.js\";\nimport type {\n RootSnapshot,\n RunExecutionReason,\n StreamControllerOptions,\n StreamSubmitOptions,\n} from \"./types.js\";\n\n/**\n * Result of awaiting the next root terminal lifecycle event. Mirrors\n * the three terminal lifecycle states the protocol surfaces, plus a\n * synthetic `\"aborted\"` for client-side cancellation.\n */\ntype TerminalResult = {\n event: \"completed\" | \"failed\" | \"interrupted\" | \"aborted\";\n error?: string;\n};\n\nfunction terminalReason(event: TerminalResult[\"event\"]): RunExecutionReason {\n if (event === \"completed\") return \"success\";\n if (event === \"failed\") return \"error\";\n if (event === \"interrupted\") return \"interrupt\";\n return \"stopped\";\n}\n\n/**\n * Queued submission entry mirrored from the server-side run queue.\n *\n * Surfaces the deferred submission to UI consumers via\n * {@link StreamController.queueStore}.\n */\nexport interface SubmissionQueueEntry<\n StateType extends object = Record<string, unknown>,\n> {\n /** Stable id minted on enqueue (uuidv7 — sortable by creation time). */\n readonly id: string;\n /** Original submit input, narrowed to the partial state shape. */\n readonly values: Partial<StateType> | null | undefined;\n /** Original submit options, minus the strategy slot which is reset on drain. */\n readonly options?: StreamSubmitOptions<StateType>;\n /** Wall-clock timestamp at enqueue. */\n readonly createdAt: Date;\n}\n\n/**\n * Read-only snapshot of the queue. The queue store hands this out\n * directly; consumers must not mutate the array.\n */\nexport type SubmissionQueueSnapshot<\n StateType extends object = Record<string, unknown>,\n> = ReadonlyArray<SubmissionQueueEntry<StateType>>;\n\n/**\n * Frozen empty queue value used as the initial / cleared snapshot.\n *\n * Reusing one frozen reference keeps store identity stable across\n * empty resets, so React's `useSyncExternalStore` doesn't think the\n * queue changed when it actually didn't.\n */\nexport const EMPTY_QUEUE: SubmissionQueueSnapshot<never> = Object.freeze([]);\n\n/**\n * Coordinates one controller's run-submission lifecycle.\n *\n * The constructor takes a bag of callbacks rather than a reference to\n * the parent {@link StreamController} on purpose:\n *\n * - It keeps the dependency surface explicit and testable — every\n * piece of controller state the submit lifecycle touches is one\n * of these closures.\n * - It avoids a cyclic dependency between controller and coordinator.\n * - Tests can construct one with stub callbacks and assert behavior\n * without mocking the entire controller.\n *\n * @typeParam StateType - Root state shape.\n * @typeParam InterruptType - Root interrupt payload shape.\n * @typeParam ConfigurableType - `config.configurable` shape accepted\n * by submit (usually `Record<string, unknown>`).\n */\nexport class SubmitCoordinator<\n StateType extends object = Record<string, unknown>,\n InterruptType = unknown,\n ConfigurableType extends object = Record<string, unknown>,\n> {\n /** Controller-level options forwarded into `submitRun` / callbacks. */\n readonly #options: StreamControllerOptions<StateType>;\n /** Root snapshot store; written for `isLoading`, `error`, `interrupts`. */\n readonly #rootStore: StreamStore<RootSnapshot<StateType, InterruptType>>;\n /** Pending submissions awaiting the active run to terminate. */\n readonly #queueStore: StreamStore<SubmissionQueueSnapshot<StateType>>;\n /** Probes the controller's `disposed` flag from deferred work. */\n readonly #getDisposed: () => boolean;\n /** Reads the controller's currently-bound thread id. */\n readonly #getCurrentThreadId: () => string | null;\n /** Updates the controller's thread id (used when minting a new id). */\n readonly #setCurrentThreadId: (threadId: string | null) => void;\n /** Records a thread id we created client-side so hydrate can skip a 404 round-trip. */\n readonly #rememberSelfCreatedThreadId: (threadId: string) => void;\n /** Drops a thread id from the self-created set once it's committed server-side. */\n readonly #forgetSelfCreatedThreadId: (threadId: string) => void;\n /** Triggers a hydrate on the controller (used by `options.threadId` rebinds). */\n readonly #hydrate: (threadId?: string | null) => Promise<void>;\n /** Lazily creates / returns the active {@link ThreadStream}. */\n readonly #ensureThread: (\n threadId: string,\n deferRootPump?: boolean\n ) => ThreadStream;\n /** Starts the previously-deferred root pump after a self-created thread commits. */\n readonly #startDeferredRootPump: () => void;\n /** Abandons a deferred root pump after a self-created dispatch fails. */\n readonly #abandonDeferredRootPump: () => void;\n /** Resolves once the controller's root subscription pump is up. */\n readonly #waitForRootPumpReady: () => Promise<void> | undefined;\n /** Resolves on the next root terminal lifecycle (or on abort). */\n readonly #awaitNextTerminal: (signal: AbortSignal) => Promise<TerminalResult>;\n /** Builds interrupt-id keyed `run.start` input from `command.resume`. */\n readonly #buildResumeRunInput: (\n resume: unknown\n ) => Record<string, unknown> | null;\n /** Marks an interrupt id as resolved so it isn't re-targeted. */\n readonly #markInterruptResolved: (interruptId: string) => void;\n /** Called once at the start of every {@link submit} invocation. */\n readonly #onSubmitStart: () => void;\n /** Marks that a local run dispatch is now active. */\n readonly #onRunStart: () => void;\n /** Records a server-accepted local run id and fires `onCreated`. */\n readonly #onRunCreated: (runId: string) => void;\n /** Fires `onCompleted` for the local run lifecycle. */\n readonly #onRunCompleted: (\n reason: RunExecutionReason,\n runId?: string\n ) => void;\n /** Marks the local run dispatch lifecycle as settled. */\n readonly #onRunEnd: () => void;\n\n /**\n * Active submission's abort controller. `undefined` between submits.\n *\n * Used both for `multitaskStrategy: \"rollback\"` (abort the previous\n * controller's signal) and `stop()` (abort the current one without\n * starting a new one).\n */\n #runAbort: AbortController | undefined;\n\n constructor(params: {\n options: StreamControllerOptions<StateType>;\n rootStore: StreamStore<RootSnapshot<StateType, InterruptType>>;\n queueStore: StreamStore<SubmissionQueueSnapshot<StateType>>;\n getDisposed: () => boolean;\n getCurrentThreadId: () => string | null;\n setCurrentThreadId: (threadId: string | null) => void;\n rememberSelfCreatedThreadId: (threadId: string) => void;\n forgetSelfCreatedThreadId: (threadId: string) => void;\n hydrate: (threadId?: string | null) => Promise<void>;\n ensureThread: (threadId: string, deferRootPump?: boolean) => ThreadStream;\n startDeferredRootPump: () => void;\n abandonDeferredRootPump: () => void;\n waitForRootPumpReady: () => Promise<void> | undefined;\n awaitNextTerminal: (signal: AbortSignal) => Promise<TerminalResult>;\n buildResumeRunInput: (resume: unknown) => Record<string, unknown> | null;\n markInterruptResolved: (interruptId: string) => void;\n onSubmitStart?: () => void;\n onRunStart?: () => void;\n onRunCreated?: (runId: string) => void;\n onRunCompleted?: (reason: RunExecutionReason, runId?: string) => void;\n onRunEnd?: () => void;\n }) {\n this.#options = params.options;\n this.#rootStore = params.rootStore;\n this.#queueStore = params.queueStore;\n this.#getDisposed = params.getDisposed;\n this.#getCurrentThreadId = params.getCurrentThreadId;\n this.#setCurrentThreadId = params.setCurrentThreadId;\n this.#rememberSelfCreatedThreadId = params.rememberSelfCreatedThreadId;\n this.#forgetSelfCreatedThreadId = params.forgetSelfCreatedThreadId;\n this.#hydrate = params.hydrate;\n this.#ensureThread = params.ensureThread;\n this.#startDeferredRootPump = params.startDeferredRootPump;\n this.#abandonDeferredRootPump = params.abandonDeferredRootPump;\n this.#waitForRootPumpReady = params.waitForRootPumpReady;\n this.#awaitNextTerminal = params.awaitNextTerminal;\n this.#buildResumeRunInput = params.buildResumeRunInput;\n this.#markInterruptResolved = params.markInterruptResolved;\n this.#onSubmitStart = params.onSubmitStart ?? (() => undefined);\n this.#onRunStart = params.onRunStart ?? (() => undefined);\n this.#onRunCreated = params.onRunCreated ?? (() => undefined);\n this.#onRunCompleted = params.onRunCompleted ?? (() => undefined);\n this.#onRunEnd = params.onRunEnd ?? (() => undefined);\n }\n\n /**\n * Submit input or a resume command to the active thread.\n *\n * Honours {@link StreamSubmitOptions.multitaskStrategy}:\n *\n * - `\"rollback\"` (default) — aborts any in-flight run and\n * dispatches immediately.\n * - `\"reject\"` — throws synchronously when a run is\n * already in flight.\n * - `\"enqueue\"` — defers via {@link #enqueueSubmission};\n * the call returns without dispatching.\n * - `\"interrupt\"` — falls through to the default path\n * (server-side cancellation lands with roadmap A0.3).\n *\n * Errors are routed through both the per-submit `onError` callback\n * and `rootStore.error`. Aborts (controller dispose / rollback) are\n * silently dropped.\n *\n * @param input - Input payload, or `null`/`undefined` for no input\n * (typical for resume commands).\n * @param options - Per-submit options (config, metadata, callbacks,\n * strategy, etc).\n */\n async submit(\n input: unknown,\n options?: StreamSubmitOptions<StateType, ConfigurableType>\n ): Promise<void> {\n if (this.#getDisposed()) return;\n this.#onSubmitStart();\n\n // Per-submit thread override: rebind first so the rest of the\n // submit operates against the new thread.\n const overrideThreadId = options?.threadId;\n if (\n overrideThreadId !== undefined &&\n overrideThreadId !== this.#getCurrentThreadId()\n ) {\n await this.#hydrate(overrideThreadId);\n }\n\n // Self-created thread id path: mint client-side so the controller\n // (and Suspense boundaries) get a stable id even before the run\n // is dispatched.\n const wasSelfCreated = this.#getCurrentThreadId() == null;\n if (wasSelfCreated) {\n const threadId = uuidv7();\n this.#setCurrentThreadId(threadId);\n this.#rememberSelfCreatedThreadId(threadId);\n this.#options.onThreadId?.(threadId);\n this.#rootStore.setState((s) => ({\n ...s,\n threadId,\n }));\n }\n\n const currentThreadId = this.#getCurrentThreadId();\n if (currentThreadId == null) return;\n // For client-self-created threads we defer the persistent root SSE\n // pump until after `submitRun` / `respondInput` commits the thread\n // server-side. Opening the pump's `subscription.subscribe` against\n // a not-yet-existent thread row produces a `404: Thread not found`\n // protocol error that strands lifecycle / messages events for the\n // first run. The deferred path starts the pump after dispatch\n // returns (see `#startDeferredRootPump` calls below).\n const thread = this.#ensureThread(currentThreadId, wasSelfCreated);\n const activeThreadId = currentThreadId;\n // Wait for the root subscription to be live; otherwise the\n // dispatch could resolve before we're listening for events and\n // we'd miss the terminal that ends the run.\n await this.#waitForRootPumpReady();\n\n const strategy = options?.multitaskStrategy ?? \"rollback\";\n // `wasSelfCreated` short-circuit: when this submit just minted a\n // brand-new thread id (the user clicked \"New Thread\"), the\n // strategy check shouldn't see a run on the *previous* thread as\n // a reason to enqueue. The previous run is on a thread the user\n // navigated away from; abandoning its client-side abort tracking\n // is correct (the server-side run continues independently).\n // Without this, `enqueue` would trap the new submission and\n // `submitRun` never fires for the new thread — leaving a freshly-\n // minted thread id committed to the URL but never to the server.\n const hasActiveRun =\n !wasSelfCreated &&\n this.#runAbort != null &&\n !this.#runAbort.signal.aborted;\n if (hasActiveRun && strategy === \"reject\") {\n throw new Error(\n \"submit() rejected: a run is already in flight and multitaskStrategy is 'reject'.\"\n );\n }\n if (hasActiveRun && strategy === \"enqueue\") {\n this.#enqueueSubmission(input, options);\n return;\n }\n\n // Rollback: abort the previous run before starting a new one.\n this.#runAbort?.abort();\n const abort = new AbortController();\n this.#runAbort = abort;\n\n const resumeCommand = options?.command?.resume;\n const isResume = resumeCommand !== undefined;\n\n // Optimistically clear interrupts/error and flip loading. The\n // root pump's lifecycle listener will re-flip these as the run\n // terminates.\n this.#rootStore.setState((s) => ({\n ...s,\n interrupts: [],\n interrupt: undefined,\n error: undefined,\n isLoading: true,\n }));\n\n const boundConfig = bindThreadConfig(options?.config, currentThreadId);\n // Subscribe to the next terminal *before* dispatching so a fast\n // run's terminal can't race us.\n const terminalPromise = this.#awaitNextTerminal(abort.signal);\n this.#onRunStart();\n\n let terminalSettled = false;\n let createdRunId: string | undefined;\n let pendingCompletionReason: RunExecutionReason | undefined;\n let completionNotified = false;\n const notifyCompletion = (reason: RunExecutionReason): void => {\n if (completionNotified) return;\n if (!isResume && createdRunId == null) {\n pendingCompletionReason = reason;\n return;\n }\n completionNotified = true;\n this.#onRunCompleted(reason, createdRunId);\n };\n const reportError = (error: unknown): void => {\n if (abort.signal.aborted) return;\n this.#rootStore.setState((s) => ({ ...s, error }));\n try {\n options?.onError?.(error);\n } catch {\n /* caller-supplied callback errors must not crash the submit */\n }\n };\n\n try {\n let terminal: TerminalResult | undefined;\n\n if (isResume) {\n const resumeInput = this.#buildResumeRunInput(resumeCommand);\n if (resumeInput == null) {\n throw new Error(\n \"submit({ command: { resume } }) called but no pending protocol interrupt is available.\"\n );\n }\n const commandPromise = thread.submitRun({\n input: resumeInput,\n config: boundConfig,\n metadata: (options?.metadata ?? undefined) as Record<string, unknown>,\n });\n // Defer the pump start until the dispatch HTTP response\n // lands — see the analogous block in the non-resume path\n // below for the rationale (thread row not committed\n // synchronously). For a resume the thread exists already\n // (it must, since `buildResumeRunInput()` was non-null),\n // so `#startDeferredRootPump` is typically a no-op here, but\n // we keep the same shape to avoid a future regression.\n //\n // Asymmetry with the non-resume path: we don't call\n // `#forgetSelfCreatedThreadId` because a resume implies the\n // thread already committed, so the self-created marker was\n // already cleared on the original submit. Same reason we\n // don't call `#abandonDeferredRootPump` on failure here.\n void commandPromise.then(\n () => this.#startDeferredRootPump(),\n () => {}\n );\n // Mark resolved synchronously: even if the response races and\n // the command settles after the terminal, we don't want to\n // re-target these interrupts on the next submit.\n for (const interruptId of Object.keys(resumeInput)) {\n this.#markInterruptResolved(interruptId);\n }\n const first = await Promise.race([\n terminalPromise.then((value) => ({\n type: \"terminal\" as const,\n value,\n })),\n commandPromise.then(\n () => ({ type: \"command\" as const }),\n (error) => ({ type: \"error\" as const, error })\n ),\n ]);\n if (first.type === \"error\") throw first.error;\n if (first.type === \"terminal\") {\n terminal = first.value;\n terminalSettled = true;\n // Stale command response — surface as error only if it\n // arrives with a real failure (not just our own abort).\n void commandPromise.catch((error) => {\n if (!terminalSettled) reportError(error);\n });\n }\n } else {\n const commandPromise = thread.submitRun({\n input: input ?? null,\n config: boundConfig,\n metadata: (options?.metadata ?? undefined) as Record<string, unknown>,\n forkFrom: options?.forkFrom,\n multitaskStrategy:\n options?.multitaskStrategy === \"enqueue\"\n ? \"enqueue\"\n : options?.multitaskStrategy,\n });\n // Start the deferred root pump *after* the dispatch HTTP\n // response lands — that's when the thread row exists server-\n // side. Doing it synchronously here would race the response\n // and the pump's `subscription.subscribe` would 404. Same\n // reason we drop the self-created flag only after dispatch:\n // future hydrates need the thread to exist before they fetch\n // state.\n //\n // Fire-and-forget: we don't want to gate Promise.race on this,\n // and `commandPromise.catch` is already handled below. A\n // dispatch failure means there's no thread to pump anyway.\n void commandPromise.then(\n () => {\n this.#startDeferredRootPump();\n this.#forgetSelfCreatedThreadId(activeThreadId);\n },\n () => {\n // Dispatch failed. Without abandoning, `#rootPumpDeferred`\n // stays armed and `selfCreatedThreadIds` still holds this\n // id — a retry submit would see `wasSelfCreated=false`\n // (currentThreadId is no longer null), `#ensureThread`\n // would early-return because `#thread != null`, and the\n // root pump would never start. Tear down so the next\n // submit re-runs `#ensureThread` from scratch.\n if (wasSelfCreated) {\n this.#abandonDeferredRootPump();\n this.#forgetSelfCreatedThreadId(activeThreadId);\n }\n }\n );\n const notifyCreated = (result: { run_id?: unknown }) => {\n if (typeof result.run_id !== \"string\") return;\n createdRunId = result.run_id;\n this.#onRunCreated(createdRunId);\n if (pendingCompletionReason != null) {\n notifyCompletion(pendingCompletionReason);\n }\n };\n const first = await Promise.race([\n terminalPromise.then((value) => ({\n type: \"terminal\" as const,\n value,\n })),\n commandPromise.then(\n (result) => ({ type: \"command\" as const, result }),\n (error) => ({ type: \"error\" as const, error })\n ),\n ]);\n if (first.type === \"error\") throw first.error;\n if (first.type === \"command\") {\n notifyCreated(first.result);\n } else {\n // Terminal landed first (very fast runs). Wait for the\n // dispatch response in the background so onCreated fires\n // and dispatch errors still surface.\n terminal = first.value;\n terminalSettled = true;\n void commandPromise.then(notifyCreated).catch((error) => {\n if (!terminalSettled) reportError(error);\n });\n }\n }\n\n terminal ??= await terminalPromise;\n terminalSettled = true;\n if (terminal.event === \"failed\" && !abort.signal.aborted) {\n const runError = new Error(\n terminal.error ?? \"Run failed with no error message\"\n );\n this.#rootStore.setState((s) => ({ ...s, error: runError }));\n try {\n options?.onError?.(runError);\n } catch {\n /* caller-supplied callback errors must not crash the submit */\n }\n }\n notifyCompletion(terminalReason(terminal.event));\n } catch (error) {\n reportError(error);\n } finally {\n // Always settle loading and clear our slot of the abort\n // controller. Schedule queue drain on the next macrotask so any\n // late state updates from this run finish flushing first.\n this.#rootStore.setState((s) => ({ ...s, isLoading: false }));\n if (this.#runAbort === abort) this.#runAbort = undefined;\n this.#onRunEnd();\n setTimeout(() => this.#drainQueue(), 0);\n }\n }\n\n /**\n * Abort the current run (if any) and force `isLoading=false`.\n *\n * Does NOT issue a server-side cancel — that lands with roadmap\n * A0.3. Today this is a client-side stop only: subsequent events\n * for the aborted run are ignored by the controller's pump because\n * the abort signal is the same one `#awaitNextTerminal` is wired\n * to.\n */\n async stop(): Promise<void> {\n this.abortActiveRun();\n this.#rootStore.setState((s) => ({ ...s, isLoading: false }));\n }\n\n /**\n * Abort the current run without forcing the loading flag down.\n *\n * Used by {@link StreamController.dispose}: disposal already tears\n * down the root store, so flipping `isLoading` here is unnecessary\n * and would race the dispose path.\n */\n abortActiveRun(): void {\n this.#runAbort?.abort();\n this.#runAbort = undefined;\n }\n\n /**\n * Cancel a queued submission by id.\n *\n * @param id - Client-side queue entry id to remove.\n * @returns `true` when the entry was found and dropped, `false` otherwise.\n */\n async cancelQueued(id: string): Promise<boolean> {\n const current = this.#queueStore.getSnapshot();\n const next = current.filter((entry) => entry.id !== id);\n if (next.length === current.length) return false;\n this.#queueStore.setState(() => next);\n return true;\n }\n\n /**\n * Drop every queued submission. Server-side cancel arrives with A0.3.\n */\n async clearQueue(): Promise<void> {\n this.#queueStore.setState(\n () => EMPTY_QUEUE as SubmissionQueueSnapshot<StateType>\n );\n }\n\n /**\n * Append a submission to the queue without dispatching.\n *\n * The drained submission is later run via {@link #drainQueue} after\n * the active run terminates.\n */\n #enqueueSubmission(\n input: unknown,\n options?: StreamSubmitOptions<StateType, ConfigurableType>\n ): void {\n const entry: SubmissionQueueEntry<StateType> = {\n id: uuidv7(),\n values: (input ?? undefined) as Partial<StateType> | null | undefined,\n options: options as StreamSubmitOptions<StateType> | undefined,\n createdAt: new Date(),\n };\n this.#queueStore.setState((current) => [...current, entry]);\n }\n\n /**\n * Drain the head of the queue if no run is active.\n *\n * Called from the `finally` block of `submit()` on the next\n * macrotask (so the just-finished run's state flushes first).\n * Strips the strategy off the dequeued options to prevent infinite\n * re-enqueueing.\n */\n #drainQueue(): void {\n if (this.#getDisposed()) return;\n if (this.#runAbort != null && !this.#runAbort.signal.aborted) return;\n const current = this.#queueStore.getSnapshot();\n if (current.length === 0) return;\n const [next, ...rest] = current;\n this.#queueStore.setState(() => rest);\n const nextOptions: StreamSubmitOptions<StateType, ConfigurableType> = {\n ...((next.options ?? {}) as StreamSubmitOptions<\n StateType,\n ConfigurableType\n >),\n multitaskStrategy: undefined,\n };\n void this.submit(next.values, nextOptions).catch(() => {\n /* submit() already routes errors through the per-submit onError\n * hook and the root store; swallow here so a failing drain does\n * not surface as an unhandled rejection. */\n });\n }\n}\n\n/**\n * Merge `thread_id` into a user-supplied `config.configurable` blob.\n *\n * The platform expects `config.configurable.thread_id` on every run\n * dispatch; we set it last so user-supplied values can't accidentally\n * override the active thread id (which would route the run to a\n * different thread).\n */\nfunction bindThreadConfig(\n config: unknown,\n threadId: string\n): Record<string, unknown> {\n const base =\n config != null && typeof config === \"object\"\n ? (config as Record<string, unknown>)\n : {};\n const configurable =\n base.configurable != null && typeof base.configurable === \"object\"\n ? (base.configurable as Record<string, unknown>)\n : {};\n return {\n ...base,\n configurable: {\n ...configurable,\n thread_id: threadId,\n },\n };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"submit-coordinator.d.cts","names":[],"sources":["../../src/stream/submit-coordinator.ts"],"mappings":";;;;;;;;;UAkGiB,oBAAA,4BACY,MAAA;EAgBjB;EAAA,SAbD,EAAA;EAawB;EAAA,SAXxB,MAAA,EAAQ,OAAA,CAAQ,SAAA;EAaY;EAAA,SAX5B,OAAA,GAAU,mBAAA,CAAoB,SAAA;EAWrC;EAAA,SATO,SAAA,EAAW,IAAA;AAAA;;;;;KAOV,uBAAA,4BACiB,MAAA,qBACzB,aAAA,CAAc,oBAAA,CAAqB,SAAA"}
1
+ {"version":3,"file":"submit-coordinator.d.cts","names":[],"sources":["../../src/stream/submit-coordinator.ts"],"mappings":";;;;;;;;;UAyFiB,oBAAA,4BACY,MAAA;EAKF;EAAA,SAFhB,EAAA;EAIU;EAAA,SAFV,MAAA,EAAQ,OAAA,CAAQ,SAAA;EAIhB;EAAA,SAFA,OAAA,GAAU,mBAAA,CAAoB,SAAA;EAEf;EAAA,SAAf,SAAA,EAAW,IAAA;AAAA;;;;;KAOV,uBAAA,4BACiB,MAAA,qBACzB,aAAA,CAAc,oBAAA,CAAqB,SAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"submit-coordinator.d.ts","names":[],"sources":["../../src/stream/submit-coordinator.ts"],"mappings":";;;;;;;;;UAkGiB,oBAAA,4BACY,MAAA;EASH;EAAA,SANf,EAAA;EAawB;EAAA,SAXxB,MAAA,EAAQ,OAAA,CAAQ,SAAA;EAYE;EAAA,SAVlB,OAAA,GAAU,mBAAA,CAAoB,SAAA;EAWvB;EAAA,SATP,SAAA,EAAW,IAAA;AAAA;;;;;KAOV,uBAAA,4BACiB,MAAA,qBACzB,aAAA,CAAc,oBAAA,CAAqB,SAAA"}
1
+ {"version":3,"file":"submit-coordinator.d.ts","names":[],"sources":["../../src/stream/submit-coordinator.ts"],"mappings":";;;;;;;;;UAyFiB,oBAAA,4BACY,MAAA;EAKV;EAAA,SAFR,EAAA;EAIA;EAAA,SAFA,MAAA,EAAQ,OAAA,CAAQ,SAAA;EAEc;EAAA,SAA9B,OAAA,GAAU,mBAAA,CAAoB,SAAA;EAEnB;EAAA,SAAX,SAAA,EAAW,IAAA;AAAA;AAOtB;;;;AAAA,KAAY,uBAAA,4BACiB,MAAA,qBACzB,aAAA,CAAc,oBAAA,CAAqB,SAAA"}