@langchain/langgraph-sdk
Version:
Client library for interacting with the LangGraph API
1 lines • 15.7 kB
Source Map (JSON)
{"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 /**\n * Seed subgraph hosts from checkpoint history (see\n * `namespace-from-history.collectSubgraphHostNamespaces`) so subgraph\n * cards render on thread refresh without waiting for the depth-1 SSE\n * replay. Supplies the host/promotion decision from history instead\n * of the live strict-prefix heuristic, then reuses the same\n * `#ensureShadow` / `#promoted` / `#commit` path. Idempotent: never\n * downgrades an entry that already reached a terminal state.\n */\n seedFromHistory(\n hosts: Array<{\n namespace: string[];\n status: \"running\" | \"complete\" | \"error\";\n }>\n ): void {\n if (hosts.length === 0) return;\n for (const host of hosts) {\n if (isRootNamespace(host.namespace)) continue;\n const id = namespaceKey(host.namespace);\n const lastSegment = host.namespace[host.namespace.length - 1] ?? \"\";\n const entry = this.#ensureShadow(\n id,\n host.namespace,\n parseNodeName(lastSegment)\n );\n if (\n host.status !== \"running\" &&\n entry.status !== \"complete\" &&\n entry.status !== \"error\"\n ) {\n entry.status = host.status;\n entry.completedAt = new Date();\n }\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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"}