UNPKG

@langchain/langgraph-sdk

Version:

Client library for interacting with the LangGraph API

190 lines (189 loc) • 7.27 kB
import { StreamStore } from "../store.js"; import { isInternalWorkNamespace, isRootNamespace, namespaceKey } from "../namespace.js"; //#region src/stream/discovery/subgraphs.ts /** Stable empty maps — reused on {@link SubgraphDiscovery.reset}. */ const EMPTY_SUBGRAPH_MAP = /* @__PURE__ */ new Map(); const EMPTY_SUBGRAPH_BY_NODE_MAP = /* @__PURE__ */ new Map(); /** * LangGraph namespaces a node invocation as `<node_name>:<uuid>` * (parallel fan-outs share `<node_name>` as a prefix but each get a * fresh uuid). Extract the node-name half so callers can key * discovery lookups on names they wrote in `addNode(...)`. */ function parseNodeName(segment) { const colon = segment.indexOf(":"); return colon === -1 ? segment : segment.slice(0, colon); } var SubgraphDiscovery = class { store = new StreamStore(/* @__PURE__ */ new Map()); byNodeStore = new StreamStore(/* @__PURE__ */ new Map()); /** * Latest known status for every namespaced lifecycle event we have * ever observed. A shadow entry is NOT necessarily a subgraph — * it is only projected into the committed stores once the same * namespace also appears in {@link #promoted}. */ #shadow = /* @__PURE__ */ new Map(); /** * Namespaces that have been observed as a strict prefix of a * deeper namespace and are therefore confirmed subgraph hosts. * Insertion order is preserved and becomes the iteration order * of the committed snapshot maps. */ #promoted = /* @__PURE__ */ new Set(); /** Feed a single root event. Non-discovery events are ignored. */ push(event) { if (event.method === "values") { this.#onValuesEvent(event); return; } if (event.method !== "lifecycle") return; const lifecycle = event; const namespace = lifecycle.params.namespace; if (isRootNamespace(namespace)) return; const id = namespaceKey(namespace); const data = lifecycle.params.data; const nodeName = parseNodeName(namespace[namespace.length - 1] ?? ""); let touched = false; for (let depth = 1; depth < namespace.length; depth += 1) { const ancestorId = namespaceKey(namespace.slice(0, depth)); if (!this.#promoted.has(ancestorId)) { this.#promoted.add(ancestorId); if (this.#shadow.has(ancestorId)) touched = true; } } if (data.event === "started") { if (!this.#shadow.has(id)) { this.#shadow.set(id, { id, namespace: [...namespace], nodeName, status: "running", startedAt: /* @__PURE__ */ new Date(), completedAt: null }); if (this.#promoted.has(id)) touched = true; } } else if (data.event === "completed" || data.event === "interrupted" || data.event === "failed") { const entry = this.#ensureShadow(id, namespace, nodeName); if (data.event === "failed") entry.status = "error"; else entry.status = "complete"; entry.completedAt = /* @__PURE__ */ new Date(); if (this.#promoted.has(id)) touched = true; } if (touched) this.#commit(); } /** * Promote subgraph host namespaces from namespaced `values` snapshots. * * Older protocol streams exposed subgraph structure primarily through * nested `lifecycle` events: a host namespace such as * `["research:<uuid>"]` was promoted once a deeper namespace like * `["research:<uuid>", "inner:<uuid>"]` appeared. Some runtimes now * emit the useful subgraph signal as `values` snapshots scoped directly * to the host namespace instead, without forwarding inner lifecycle * events to the client. In that shape, the namespace on the `values` * event is already the selector target that `useMessages(stream, * subgraph)` should subscribe to, so we create/promote the shadow * subgraph entry from that namespace. * * Root `values` events are ignored because they represent the parent * thread state, not a subgraph. Tool/subagent namespaces are also * ignored because deep-agent task tools emit their own namespaced * `values` snapshots under `tools:*` / `task:*`; those are discovered * by `SubagentDiscovery` and must not be duplicated as subgraphs. * * A `values` event does not carry lifecycle terminal status, so the * entry remains marked `running` until a matching lifecycle terminal * event arrives. If no terminal arrives, the discovery map still * contains the host namespace, which is the important invariant for * scoped selectors. */ #onValuesEvent(event) { const namespace = event.params.namespace; if (isRootNamespace(namespace)) return; if (isInternalWorkNamespace(namespace)) return; const data = event.params.data; if (data == null || typeof data !== "object" || Array.isArray(data)) return; const id = namespaceKey(namespace); const nodeName = parseNodeName(namespace[namespace.length - 1] ?? ""); const entry = this.#ensureShadow(id, namespace, nodeName); if (entry.status !== "complete" && entry.status !== "error") entry.status = "running"; if (!this.#promoted.has(id)) this.#promoted.add(id); this.#commit(); } /** * Seed subgraph hosts from checkpoint history (see * `namespace-from-history.collectSubgraphHostNamespaces`) so subgraph * cards render on thread refresh without waiting for the depth-1 SSE * replay. Supplies the host/promotion decision from history instead * of the live strict-prefix heuristic, then reuses the same * `#ensureShadow` / `#promoted` / `#commit` path. Idempotent: never * downgrades an entry that already reached a terminal state. */ seedFromHistory(hosts) { if (hosts.length === 0) return; for (const host of hosts) { if (isRootNamespace(host.namespace)) continue; const id = namespaceKey(host.namespace); const lastSegment = host.namespace[host.namespace.length - 1] ?? ""; const entry = this.#ensureShadow(id, host.namespace, parseNodeName(lastSegment)); if (host.status !== "running" && entry.status !== "complete" && entry.status !== "error") { entry.status = host.status; entry.completedAt = /* @__PURE__ */ new Date(); } this.#promoted.add(id); } this.#commit(); } get snapshot() { return this.store.getSnapshot(); } get byNodeSnapshot() { return this.byNodeStore.getSnapshot(); } /** * Drop all discovery state. Called on thread rebind / dispose so a * new thread's subgraphs cannot bleed into the previous UI. */ reset() { this.#shadow.clear(); this.#promoted.clear(); this.store.setValue(EMPTY_SUBGRAPH_MAP); this.byNodeStore.setValue(EMPTY_SUBGRAPH_BY_NODE_MAP); } #ensureShadow(id, namespace, nodeName) { let entry = this.#shadow.get(id); if (entry == null) { entry = { id, namespace: [...namespace], nodeName, status: "running", startedAt: /* @__PURE__ */ new Date(), completedAt: null }; this.#shadow.set(id, entry); } return entry; } #commit() { const snapshots = []; for (const id of this.#promoted) { const entry = this.#shadow.get(id); if (entry == null) continue; snapshots.push({ ...entry }); } this.store.setValue(new Map(snapshots.map((s) => [s.id, s]))); const byNode = /* @__PURE__ */ new Map(); for (const snap of snapshots) { const bucket = byNode.get(snap.nodeName); if (bucket == null) byNode.set(snap.nodeName, [snap]); else bucket.push(snap); } this.byNodeStore.setValue(byNode); } }; //#endregion export { SubgraphDiscovery }; //# sourceMappingURL=subgraphs.js.map