@langchain/langgraph-sdk 1.9.13 → 1.9.15

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.
@@ -108,7 +108,7 @@ var SubgraphDiscovery = class {
108
108
  const id = require_namespace.namespaceKey(namespace);
109
109
  const nodeName = parseNodeName(namespace[namespace.length - 1] ?? "");
110
110
  const entry = this.#ensureShadow(id, namespace, nodeName);
111
- entry.status = "running";
111
+ if (entry.status !== "complete" && entry.status !== "error") entry.status = "running";
112
112
  if (!this.#promoted.has(id)) this.#promoted.add(id);
113
113
  this.#commit();
114
114
  }
@@ -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\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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"}
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 // A `values` snapshot is a discovery/promotion signal, not a status\n // transition — terminal status is owned by `lifecycle` events. The\n // content pump (which carries `values`) and the lifecycle watcher\n // (which carries `lifecycle`) are independent streams deduped through\n // `onEvent`, so a host namespace's final `values` snapshot can be\n // observed AFTER its terminal `completed`/`failed` lifecycle event.\n // Unconditionally writing \"running\" here would resurrect a finished\n // subgraph and strand it as perpetually running in the UI. New\n // entries are already created as \"running\" by `#ensureShadow`, so we\n // only need to avoid downgrading an entry that already reached a\n // terminal state.\n if (entry.status !== \"complete\" && entry.status !== \"error\") {\n entry.status = \"running\";\n }\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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"}
@@ -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,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
+ {"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,IAwHR,QAAA,CAAA,GAAY,WAAA;EAAA,IAIZ,cAAA,CAAA,GAAkB,iBAAA;;;;;EAQtB,KAAA,CAAA;AAAA"}
@@ -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,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
+ {"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,IAwHR,QAAA,CAAA,GAAY,WAAA;EAAA,IAIZ,cAAA,CAAA,GAAkB,iBAAA;;;;;EAQtB,KAAA,CAAA;AAAA"}
@@ -108,7 +108,7 @@ var SubgraphDiscovery = class {
108
108
  const id = namespaceKey(namespace);
109
109
  const nodeName = parseNodeName(namespace[namespace.length - 1] ?? "");
110
110
  const entry = this.#ensureShadow(id, namespace, nodeName);
111
- entry.status = "running";
111
+ if (entry.status !== "complete" && entry.status !== "error") entry.status = "running";
112
112
  if (!this.#promoted.has(id)) this.#promoted.add(id);
113
113
  this.#commit();
114
114
  }
@@ -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\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":";;;;AA2DA,MAAM,qCAAkC,IAAI,KAAK;AACjD,MAAM,6CAAgD,IAAI,KAAK;;;;;;;AAiB/D,SAAS,cAAc,SAAyB;CAC9C,MAAM,QAAQ,QAAQ,QAAQ,IAAI;AAClC,QAAO,UAAU,KAAK,UAAU,QAAQ,MAAM,GAAG,MAAM;;AAGzD,IAAa,oBAAb,MAA+B;CAC7B,QAAiB,IAAI,4BAAyB,IAAI,KAAK,CAAC;CACxD,cAAuB,IAAI,4BAA+B,IAAI,KAAK,CAAC;;;;;;;CAQpE,0BAAU,IAAI,KAA8B;;;;;;;CAQ5C,4BAAY,IAAI,KAAa;;CAG7B,KAAK,OAAoB;AACvB,MAAI,MAAM,WAAW,UAAU;AAC7B,SAAA,cAAoB,MAAqB;AACzC;;AAEF,MAAI,MAAM,WAAW,YAAa;EAClC,MAAM,YAAY;EAClB,MAAM,YAAY,UAAU,OAAO;AAGnC,MAAI,gBAAgB,UAAU,CAAE;EAChC,MAAM,KAAK,aAAa,UAAU;EAClC,MAAM,OAAO,UAAU,OAAO;EAE9B,MAAM,WAAW,cADG,UAAU,UAAU,SAAS,MAAM,GACZ;EAE3C,IAAI,UAAU;AAKd,OAAK,IAAI,QAAQ,GAAG,QAAQ,UAAU,QAAQ,SAAS,GAAG;GACxD,MAAM,aAAa,aAAa,UAAU,MAAM,GAAG,MAAM,CAAC;AAC1D,OAAI,CAAC,MAAA,SAAe,IAAI,WAAW,EAAE;AACnC,UAAA,SAAe,IAAI,WAAW;AAC9B,QAAI,MAAA,OAAa,IAAI,WAAW,CAAE,WAAU;;;AAKhD,MAAI,KAAK,UAAU;OACb,CAAC,MAAA,OAAa,IAAI,GAAG,EAAE;AACzB,UAAA,OAAa,IAAI,IAAI;KACnB;KACA,WAAW,CAAC,GAAG,UAAU;KACzB;KACA,QAAQ;KACR,2BAAW,IAAI,MAAM;KACrB,aAAa;KACd,CAAC;AACF,QAAI,MAAA,SAAe,IAAI,GAAG,CAAE,WAAU;;aAGxC,KAAK,UAAU,eACf,KAAK,UAAU,iBACf,KAAK,UAAU,UACf;GAGA,MAAM,QAAQ,MAAA,aAAmB,IAAI,WAAW,SAAS;AACzD,OAAI,KAAK,UAAU,SACjB,OAAM,SAAS;OAEf,OAAM,SAAS;AAEjB,SAAM,8BAAc,IAAI,MAAM;AAC9B,OAAI,MAAA,SAAe,IAAI,GAAG,CAAE,WAAU;;AAGxC,MAAI,QAAS,OAAA,QAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6B7B,eAAe,OAA0B;EACvC,MAAM,YAAY,MAAM,OAAO;AAC/B,MAAI,gBAAgB,UAAU,CAAE;AAChC,MAAI,wBAAwB,UAAU,CAAE;EACxC,MAAM,OAAO,MAAM,OAAO;AAC1B,MAAI,QAAQ,QAAQ,OAAO,SAAS,YAAY,MAAM,QAAQ,KAAK,CAAE;EAErE,MAAM,KAAK,aAAa,UAAU;EAElC,MAAM,WAAW,cADG,UAAU,UAAU,SAAS,MAAM,GACZ;EAC3C,MAAM,QAAQ,MAAA,aAAmB,IAAI,WAAW,SAAS;AACzD,QAAM,SAAS;AAEf,MAAI,CAAC,MAAA,SAAe,IAAI,GAAG,CACzB,OAAA,SAAe,IAAI,GAAG;AAExB,QAAA,QAAc;;CAGhB,IAAI,WAAwB;AAC1B,SAAO,KAAK,MAAM,aAAa;;CAGjC,IAAI,iBAAoC;AACtC,SAAO,KAAK,YAAY,aAAa;;;;;;CAOvC,QAAc;AACZ,QAAA,OAAa,OAAO;AACpB,QAAA,SAAe,OAAO;AACtB,OAAK,MAAM,SAAS,mBAAmB;AACvC,OAAK,YAAY,SAAS,2BAA2B;;CAGvD,cACE,IACA,WACA,UACiB;EACjB,IAAI,QAAQ,MAAA,OAAa,IAAI,GAAG;AAChC,MAAI,SAAS,MAAM;AACjB,WAAQ;IACN;IACA,WAAW,CAAC,GAAG,UAAU;IACzB;IACA,QAAQ;IACR,2BAAW,IAAI,MAAM;IACrB,aAAa;IACd;AACD,SAAA,OAAa,IAAI,IAAI,MAAM;;AAE7B,SAAO;;CAGT,UAAgB;EACd,MAAM,YAAyC,EAAE;AACjD,OAAK,MAAM,MAAM,MAAA,UAAgB;GAC/B,MAAM,QAAQ,MAAA,OAAa,IAAI,GAAG;AAKlC,OAAI,SAAS,KAAM;AACnB,aAAU,KAAK,EAAE,GAAG,OAAO,CAAC;;AAG9B,OAAK,MAAM,SAAS,IAAI,IAAI,UAAU,KAAK,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;EAE7D,MAAM,yBAAS,IAAI,KAA0C;AAC7D,OAAK,MAAM,QAAQ,WAAW;GAC5B,MAAM,SAAS,OAAO,IAAI,KAAK,SAAS;AACxC,OAAI,UAAU,KAAM,QAAO,IAAI,KAAK,UAAU,CAAC,KAAK,CAAC;OAChD,QAAO,KAAK,KAAK;;AAExB,OAAK,YAAY,SAAS,OAAO"}
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 // A `values` snapshot is a discovery/promotion signal, not a status\n // transition — terminal status is owned by `lifecycle` events. The\n // content pump (which carries `values`) and the lifecycle watcher\n // (which carries `lifecycle`) are independent streams deduped through\n // `onEvent`, so a host namespace's final `values` snapshot can be\n // observed AFTER its terminal `completed`/`failed` lifecycle event.\n // Unconditionally writing \"running\" here would resurrect a finished\n // subgraph and strand it as perpetually running in the UI. New\n // entries are already created as \"running\" by `#ensureShadow`, so we\n // only need to avoid downgrading an entry that already reached a\n // terminal state.\n if (entry.status !== \"complete\" && entry.status !== \"error\") {\n entry.status = \"running\";\n }\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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"}
@@ -200,7 +200,6 @@ var SubmitCoordinator = class {
200
200
  if (currentThreadId == null) return;
201
201
  const thread = this.#ensureThread(currentThreadId, wasSelfCreated);
202
202
  const activeThreadId = currentThreadId;
203
- await this.#waitForRootPumpReady();
204
203
  const strategy = options?.multitaskStrategy ?? "rollback";
205
204
  const hasActiveRun = !wasSelfCreated && this.#runAbort != null && !this.#runAbort.signal.aborted;
206
205
  if (hasActiveRun && strategy === "reject") throw new Error("submit() rejected: a run is already in flight and multitaskStrategy is 'reject'.");
@@ -218,6 +217,7 @@ var SubmitCoordinator = class {
218
217
  error: void 0,
219
218
  isLoading: true
220
219
  }));
220
+ await this.#waitForRootPumpReady();
221
221
  const boundConfig = bindThreadConfig(options?.config, currentThreadId);
222
222
  const terminalPromise = this.#awaitNextTerminal(abort.signal);
223
223
  this.#onRunStart();
@@ -1 +1 @@
1
- {"version":3,"file":"submit-coordinator.cjs","names":["#options","#rootStore","#queueStore","#getDisposed","#getCurrentThreadId","#setCurrentThreadId","#rememberSelfCreatedThreadId","#forgetSelfCreatedThreadId","#hydrate","#ensureThread","#startDeferredRootPump","#abandonDeferredRootPump","#waitForRootPumpReady","#awaitNextTerminal","#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()`) 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 /** 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 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.#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 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 *\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 * To resume a pending interrupt, use {@link StreamController.respond}\n * instead of `submit()`.\n *\n * @param input - Input payload for the run.\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 // 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 (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 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 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 * Client-side only — server-side cancel is handled by\n * {@link StreamController.stop} before this is invoked.\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","#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()`) 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 /** 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 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.#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 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 *\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 * To resume a pending interrupt, use {@link StreamController.respond}\n * instead of `submit()`.\n *\n * @param input - Input payload for the run.\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\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 // Claim the in-flight slot before awaiting the root pump so\n // concurrent `enqueue` submits in the same tick observe\n // `hasActiveRun` and land in {@link queueStore}.\n this.#rootStore.setState((s) => ({\n ...s,\n interrupts: [],\n interrupt: undefined,\n error: undefined,\n isLoading: true,\n }));\n\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 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 (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 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 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 * Client-side only — server-side cancel is handled by\n * {@link StreamController.stop} before this is invoked.\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2EA,SAAS,eAAe,OAAoD;AAC1E,KAAI,UAAU,YAAa,QAAO;AAClC,KAAI,UAAU,SAAU,QAAO;AAC/B,KAAI,UAAU,cAAe,QAAO;AACpC,QAAO;;;;;;;;;AAqCT,MAAa,cAA8C,OAAO,OAAO,EAAE,CAAC;;;;;;;;;;;;;;;;;;;AAoB5E,IAAa,oBAAb,MAIE;;CAEA;;CAEA;;CAEA;;CAEA;;CAEA;;CAEA;;CAEA;;CAEA;;CAEA;;CAEA;;CAKA;;CAEA;;CAEA;;CAEA;;CAEA;;CAEA;;CAEA;;CAEA;;CAKA;;;;;;;;CASA;CAEA,YAAY,QAoBT;AACD,QAAA,UAAgB,OAAO;AACvB,QAAA,YAAkB,OAAO;AACzB,QAAA,aAAmB,OAAO;AAC1B,QAAA,cAAoB,OAAO;AAC3B,QAAA,qBAA2B,OAAO;AAClC,QAAA,qBAA2B,OAAO;AAClC,QAAA,8BAAoC,OAAO;AAC3C,QAAA,4BAAkC,OAAO;AACzC,QAAA,UAAgB,OAAO;AACvB,QAAA,eAAqB,OAAO;AAC5B,QAAA,wBAA8B,OAAO;AACrC,QAAA,0BAAgC,OAAO;AACvC,QAAA,uBAA6B,OAAO;AACpC,QAAA,oBAA0B,OAAO;AACjC,QAAA,gBAAsB,OAAO,wBAAwB,KAAA;AACrD,QAAA,aAAmB,OAAO,qBAAqB,KAAA;AAC/C,QAAA,eAAqB,OAAO,uBAAuB,KAAA;AACnD,QAAA,iBAAuB,OAAO,yBAAyB,KAAA;AACvD,QAAA,WAAiB,OAAO,mBAAmB,KAAA;;;;;;;;;;;;;;;;;;;;;;;;;;CA2B7C,MAAM,OACJ,OACA,SACe;AACf,MAAI,MAAA,aAAmB,CAAE;AACzB,QAAA,eAAqB;EAIrB,MAAM,mBAAmB,SAAS;AAClC,MACE,qBAAqB,KAAA,KACrB,qBAAqB,MAAA,oBAA0B,CAE/C,OAAM,MAAA,QAAc,iBAAiB;EAMvC,MAAM,iBAAiB,MAAA,oBAA0B,IAAI;AACrD,MAAI,gBAAgB;GAClB,MAAM,YAAA,GAAA,KAAA,KAAmB;AACzB,SAAA,mBAAyB,SAAS;AAClC,SAAA,4BAAkC,SAAS;AAC3C,SAAA,QAAc,aAAa,SAAS;AACpC,SAAA,UAAgB,UAAU,OAAO;IAC/B,GAAG;IACH;IACD,EAAE;;EAGL,MAAM,kBAAkB,MAAA,oBAA0B;AAClD,MAAI,mBAAmB,KAAM;EAQ7B,MAAM,SAAS,MAAA,aAAmB,iBAAiB,eAAe;EAClE,MAAM,iBAAiB;EAEvB,MAAM,WAAW,SAAS,qBAAqB;EAU/C,MAAM,eACJ,CAAC,kBACD,MAAA,YAAkB,QAClB,CAAC,MAAA,SAAe,OAAO;AACzB,MAAI,gBAAgB,aAAa,SAC/B,OAAM,IAAI,MACR,mFACD;AAEH,MAAI,gBAAgB,aAAa,WAAW;AAC1C,SAAA,kBAAwB,OAAO,QAAQ;AACvC;;AAIF,QAAA,UAAgB,OAAO;EACvB,MAAM,QAAQ,IAAI,iBAAiB;AACnC,QAAA,WAAiB;AAKjB,QAAA,UAAgB,UAAU,OAAO;GAC/B,GAAG;GACH,YAAY,EAAE;GACd,WAAW,KAAA;GACX,OAAO,KAAA;GACP,WAAW;GACZ,EAAE;AAKH,QAAM,MAAA,sBAA4B;EAElC,MAAM,cAAc,iBAAiB,SAAS,QAAQ,gBAAgB;EAGtE,MAAM,kBAAkB,MAAA,kBAAwB,MAAM,OAAO;AAC7D,QAAA,YAAkB;EAElB,IAAI,kBAAkB;EACtB,IAAI;EACJ,IAAI;EACJ,IAAI,qBAAqB;EACzB,MAAM,oBAAoB,WAAqC;AAC7D,OAAI,mBAAoB;AACxB,OAAI,gBAAgB,MAAM;AACxB,8BAA0B;AAC1B;;AAEF,wBAAqB;AACrB,SAAA,eAAqB,QAAQ,aAAa;;EAE5C,MAAM,eAAe,UAAyB;AAC5C,OAAI,MAAM,OAAO,QAAS;AAC1B,SAAA,UAAgB,UAAU,OAAO;IAAE,GAAG;IAAG;IAAO,EAAE;AAClD,OAAI;AACF,aAAS,UAAU,MAAM;WACnB;;AAKV,MAAI;GACF,IAAI;GAEJ,MAAM,iBAAiB,OAAO,UAAU;IACtC,OAAO,SAAS;IAChB,QAAQ;IACR,UAAW,SAAS,YAAY,KAAA;IAChC,UAAU,SAAS;IACnB,mBACE,SAAS,sBAAsB,YAC3B,YACA,SAAS;IAChB,CAAC;AAYG,kBAAe,WACZ;AACJ,UAAA,uBAA6B;AAC7B,UAAA,0BAAgC,eAAe;YAE3C;AAQJ,QAAI,gBAAgB;AAClB,WAAA,yBAA+B;AAC/B,WAAA,0BAAgC,eAAe;;KAGpD;GACD,MAAM,iBAAiB,WAAiC;AACtD,QAAI,OAAO,OAAO,WAAW,SAAU;AACvC,mBAAe,OAAO;AACtB,UAAA,aAAmB,aAAa;AAChC,QAAI,2BAA2B,KAC7B,kBAAiB,wBAAwB;;GAG7C,MAAM,QAAQ,MAAM,QAAQ,KAAK,CAC/B,gBAAgB,MAAM,WAAW;IAC/B,MAAM;IACN;IACD,EAAE,EACH,eAAe,MACZ,YAAY;IAAE,MAAM;IAAoB;IAAQ,IAChD,WAAW;IAAE,MAAM;IAAkB;IAAO,EAC9C,CACF,CAAC;AACF,OAAI,MAAM,SAAS,QAAS,OAAM,MAAM;AACxC,OAAI,MAAM,SAAS,UACjB,eAAc,MAAM,OAAO;QACtB;AAIL,eAAW,MAAM;AACjB,sBAAkB;AACb,mBAAe,KAAK,cAAc,CAAC,OAAO,UAAU;AACvD,SAAI,CAAC,gBAAiB,aAAY,MAAM;MACxC;;AAGJ,gBAAa,MAAM;AACnB,qBAAkB;AAClB,OAAI,SAAS,UAAU,YAAY,CAAC,MAAM,OAAO,SAAS;IACxD,MAAM,WAAW,IAAI,MACnB,SAAS,SAAS,mCACnB;AACD,UAAA,UAAgB,UAAU,OAAO;KAAE,GAAG;KAAG,OAAO;KAAU,EAAE;AAC5D,QAAI;AACF,cAAS,UAAU,SAAS;YACtB;;AAIV,oBAAiB,eAAe,SAAS,MAAM,CAAC;WACzC,OAAO;AACd,eAAY,MAAM;YACV;AAIR,SAAA,UAAgB,UAAU,OAAO;IAAE,GAAG;IAAG,WAAW;IAAO,EAAE;AAC7D,OAAI,MAAA,aAAmB,MAAO,OAAA,WAAiB,KAAA;AAC/C,SAAA,UAAgB;AAChB,oBAAiB,MAAA,YAAkB,EAAE,EAAE;;;;;;;;;CAU3C,MAAM,OAAsB;AAC1B,OAAK,gBAAgB;AACrB,QAAA,UAAgB,UAAU,OAAO;GAAE,GAAG;GAAG,WAAW;GAAO,EAAE;;;;;;;;;CAU/D,iBAAuB;AACrB,QAAA,UAAgB,OAAO;AACvB,QAAA,WAAiB,KAAA;;;;;;;;CASnB,MAAM,aAAa,IAA8B;EAC/C,MAAM,UAAU,MAAA,WAAiB,aAAa;EAC9C,MAAM,OAAO,QAAQ,QAAQ,UAAU,MAAM,OAAO,GAAG;AACvD,MAAI,KAAK,WAAW,QAAQ,OAAQ,QAAO;AAC3C,QAAA,WAAiB,eAAe,KAAK;AACrC,SAAO;;;;;CAMT,MAAM,aAA4B;AAChC,QAAA,WAAiB,eACT,YACP;;;;;;;;CASH,mBACE,OACA,SACM;EACN,MAAM,QAAyC;GAC7C,KAAA,GAAA,KAAA,KAAY;GACZ,QAAS,SAAS,KAAA;GACT;GACT,2BAAW,IAAI,MAAM;GACtB;AACD,QAAA,WAAiB,UAAU,YAAY,CAAC,GAAG,SAAS,MAAM,CAAC;;;;;;;;;;CAW7D,cAAoB;AAClB,MAAI,MAAA,aAAmB,CAAE;AACzB,MAAI,MAAA,YAAkB,QAAQ,CAAC,MAAA,SAAe,OAAO,QAAS;EAC9D,MAAM,UAAU,MAAA,WAAiB,aAAa;AAC9C,MAAI,QAAQ,WAAW,EAAG;EAC1B,MAAM,CAAC,MAAM,GAAG,QAAQ;AACxB,QAAA,WAAiB,eAAe,KAAK;EACrC,MAAM,cAAgE;GACpE,GAAK,KAAK,WAAW,EAAE;GAIvB,mBAAmB,KAAA;GACpB;AACI,OAAK,OAAO,KAAK,QAAQ,YAAY,CAAC,YAAY,GAIrD;;;;;;;;;;;AAYN,SAAS,iBACP,QACA,UACyB;CACzB,MAAM,OACJ,UAAU,QAAQ,OAAO,WAAW,WAC/B,SACD,EAAE;CACR,MAAM,eACJ,KAAK,gBAAgB,QAAQ,OAAO,KAAK,iBAAiB,WACrD,KAAK,eACN,EAAE;AACR,QAAO;EACL,GAAG;EACH,cAAc;GACZ,GAAG;GACH,WAAW;GACZ;EACF"}
@@ -199,7 +199,6 @@ var SubmitCoordinator = class {
199
199
  if (currentThreadId == null) return;
200
200
  const thread = this.#ensureThread(currentThreadId, wasSelfCreated);
201
201
  const activeThreadId = currentThreadId;
202
- await this.#waitForRootPumpReady();
203
202
  const strategy = options?.multitaskStrategy ?? "rollback";
204
203
  const hasActiveRun = !wasSelfCreated && this.#runAbort != null && !this.#runAbort.signal.aborted;
205
204
  if (hasActiveRun && strategy === "reject") throw new Error("submit() rejected: a run is already in flight and multitaskStrategy is 'reject'.");
@@ -217,6 +216,7 @@ var SubmitCoordinator = class {
217
216
  error: void 0,
218
217
  isLoading: true
219
218
  }));
219
+ await this.#waitForRootPumpReady();
220
220
  const boundConfig = bindThreadConfig(options?.config, currentThreadId);
221
221
  const terminalPromise = this.#awaitNextTerminal(abort.signal);
222
222
  this.#onRunStart();
@@ -1 +1 @@
1
- {"version":3,"file":"submit-coordinator.js","names":["#options","#rootStore","#queueStore","#getDisposed","#getCurrentThreadId","#setCurrentThreadId","#rememberSelfCreatedThreadId","#forgetSelfCreatedThreadId","#hydrate","#ensureThread","#startDeferredRootPump","#abandonDeferredRootPump","#waitForRootPumpReady","#awaitNextTerminal","#onSubmitStart","#onRunStart","#onRunCreated","#onRunCompleted","#onRunEnd","uuidv7","#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()`) 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 /** 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 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.#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 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 *\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 * To resume a pending interrupt, use {@link StreamController.respond}\n * instead of `submit()`.\n *\n * @param input - Input payload for the run.\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 // 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 (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 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 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 * Client-side only — server-side cancel is handled by\n * {@link StreamController.stop} before this is invoked.\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.js","names":["#options","#rootStore","#queueStore","#getDisposed","#getCurrentThreadId","#setCurrentThreadId","#rememberSelfCreatedThreadId","#forgetSelfCreatedThreadId","#hydrate","#ensureThread","#startDeferredRootPump","#abandonDeferredRootPump","#waitForRootPumpReady","#awaitNextTerminal","#onSubmitStart","#onRunStart","#onRunCreated","#onRunCompleted","#onRunEnd","uuidv7","#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()`) 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 /** 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 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.#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 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 *\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 * To resume a pending interrupt, use {@link StreamController.respond}\n * instead of `submit()`.\n *\n * @param input - Input payload for the run.\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\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 // Claim the in-flight slot before awaiting the root pump so\n // concurrent `enqueue` submits in the same tick observe\n // `hasActiveRun` and land in {@link queueStore}.\n this.#rootStore.setState((s) => ({\n ...s,\n interrupts: [],\n interrupt: undefined,\n error: undefined,\n isLoading: true,\n }));\n\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 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 (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 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 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 * Client-side only — server-side cancel is handled by\n * {@link StreamController.stop} before this is invoked.\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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@langchain/langgraph-sdk",
3
- "version": "1.9.13",
3
+ "version": "1.9.15",
4
4
  "description": "Client library for interacting with the LangGraph API",
5
5
  "type": "module",
6
6
  "repository": {
@@ -21,17 +21,17 @@
21
21
  "@langchain/scripts": "^0.1.4",
22
22
  "@tsconfig/recommended": "^1.0.2",
23
23
  "@types/node": "^18.15.11",
24
- "@types/react": "^19.2.14",
24
+ "@types/react": "^19.2.16",
25
25
  "@types/react-dom": "^19.2.3",
26
26
  "@types/uuid": "^9.0.1",
27
27
  "deepagents": "^1.8.3",
28
28
  "langchain": "^1.3.5",
29
- "react": "^19.2.4",
30
- "react-dom": "^19.2.4",
31
- "svelte": "^5.55.7",
29
+ "react": "^19.2.7",
30
+ "react-dom": "^19.2.7",
31
+ "svelte": "^5.56.1",
32
32
  "typescript": "^4.9.5 || ^5.4.5",
33
33
  "vitest": "^3.2.4",
34
- "vue": "^3.5.33",
34
+ "vue": "^3.5.35",
35
35
  "zod": "^4.3.5"
36
36
  },
37
37
  "peerDependencies": {