UNPKG

@langchain/langgraph-sdk

Version:

Client library for interacting with the LangGraph API

207 lines (206 loc) • 9.25 kB
const require_constants = require("../constants.cjs"); const require_message_coercion = require("../message-coercion.cjs"); const require_namespace = require("../namespace.cjs"); //#region src/stream/discovery/namespace-from-history.ts function getTasks(checkpoint) { const tasks = checkpoint.tasks; return Array.isArray(tasks) ? tasks : []; } /** * Phase 1 (preferred): map a `task` tool-call id directly to its * execution namespace via the task's result `ToolMessage`. Unambiguous * — works even when a step mixes subagent and non-subagent tool calls. * * The subgraph checkpoint_ns is `task.name + ":" + task.id` (mirrors * pregel's `taskCheckpointNamespace`), so we derive it from name+id * rather than the always-null completed-task checkpoint. */ function collectDirectTaskMappings(tasks, targets, out) { for (const task of tasks) { if (!Array.isArray(task.path) || task.path[0] !== "__pregel_push" || typeof task.id !== "string" || typeof task.name !== "string") continue; const resultMessages = task.result?.messages; if (!Array.isArray(resultMessages)) continue; for (const msg of resultMessages) { const m = msg; const id = m.tool_call_id; if (m.type === "tool" && typeof id === "string" && targets.has(id) && !out.has(id)) out.set(id, `${task.name}:${task.id}`); } } } /** * Phase 2 (fallback): align still-pending push tasks to the triggering * AI message's tool calls by Send index (`path[1]`), where `path[1]` * indexes into the *full* `tool_calls` array. Only push tasks whose * targeted call is a subagent `task` are mapped, so a sibling * non-subagent tool call in the same message can't capture a subagent's * namespace. Applied only to ids Phase 1 could not resolve so a correct * direct mapping is never overwritten by a positional guess. */ function collectPositionalTaskMappings(checkpoint, targets, out, messagesKey) { const pushTasks = getTasks(checkpoint).filter((t) => Array.isArray(t.path) && t.path[0] === "__pregel_push" && typeof t.path[1] === "number" && t.path.length <= 3 && typeof t.id === "string" && typeof t.name === "string"); if (pushTasks.length === 0) return; const msgs = checkpoint.values?.[messagesKey]; if (!Array.isArray(msgs)) return; let toolCalls; for (let i = msgs.length - 1; i >= 0; i -= 1) { const m = msgs[i]; if (m.type !== "ai") continue; const normalizedToolCalls = require_message_coercion.normalizeAIMessageToolCalls(m).tool_calls; if (Array.isArray(normalizedToolCalls) && normalizedToolCalls.some((tc) => tc.name === "task")) { toolCalls = normalizedToolCalls; break; } } if (toolCalls == null) return; for (const task of pushTasks) { const index = task.path[1]; const tc = toolCalls[index]; if (tc?.name === "task" && tc.id != null && typeof task.id === "string" && typeof task.name === "string" && targets.has(tc.id) && !out.has(tc.id)) out.set(tc.id, `${task.name}:${task.id}`); } } /** Build a `getHistory` `before` cursor from a history entry. */ function beforeCursor(entry) { const checkpointId = entry.checkpoint?.checkpoint_id; if (typeof checkpointId !== "string") return void 0; return { configurable: { checkpoint_id: checkpointId } }; } function applyCollectors(history, targets, out, messagesKey) { for (const checkpoint of history) collectDirectTaskMappings(getTasks(checkpoint), targets, out); const stillUnmapped = () => [...targets].some((id) => !out.has(id)); if (stillUnmapped()) for (const checkpoint of history) { if (!stillUnmapped()) break; collectPositionalTaskMappings(checkpoint, targets, out, messagesKey); } } /** * Synchronous subagent namespace mapping over an already-fetched * history page. Used by {@link resolveSubagentNamespaces} and by the * controller's hydrate-time bulk seed (which shares a single * `getHistory` page with subgraph host detection). */ function mapSubagentNamespaces(history, toolCallIds, messagesKey = require_constants.DEFAULT_MESSAGES_KEY) { const out = /* @__PURE__ */ new Map(); const targets = new Set(toolCallIds); if (targets.size === 0) return out; applyCollectors(history, targets, out, messagesKey); return out; } /** * Resolve execution namespaces for the given subagent `task` tool-call * ids from checkpoint history. * * Bounded and O(1) in calls: one `getHistory` page, plus at most one * `before`-cursor fallback page when the first leaves ids unresolved. * Never fans out per id. * * @returns Map of `toolCallId` → single execution namespace segment * (e.g. `tools:<uuid>`). Unresolved ids are omitted. */ async function resolveSubagentNamespaces(client, threadId, toolCallIds, opts) { const out = /* @__PURE__ */ new Map(); const targets = new Set(toolCallIds); if (targets.size === 0) return out; const limit = opts?.limit ?? 20; const messagesKey = opts?.messagesKey ?? "messages"; const signal = opts?.signal; const page1 = await getHistoryPage(client, threadId, { limit, signal }); applyCollectors(page1, targets, out, messagesKey); if ([...targets].filter((id) => !out.has(id)).length > 0 && page1.length > 0) { const before = beforeCursor(page1[page1.length - 1]); if (before != null) applyCollectors(await getHistoryPage(client, threadId, { limit, before, signal }), targets, out, messagesKey); } return out; } /** * Fetch one bounded history page typed as plain records, sidestepping * the client's `TStateType` generic so the discovery collectors (which * read raw `values`/`tasks`) get a stable {@link AnyCheckpoint} shape. */ function getHistoryPage(client, threadId, options) { return client.threads.getHistory(threadId, options); } function checkpointNsToSegments(checkpointNs) { if (typeof checkpointNs !== "string" || checkpointNs.length === 0) return []; return checkpointNs.split("|").filter((segment) => segment.length > 0); } function isInternalSegment(segment) { return segment.startsWith("tools:") || segment.startsWith("task:"); } /** * Identify subgraph host namespaces from checkpoint history, mirroring the * promotion rule of the live {@link ./subgraphs} `SubgraphDiscovery`: a * namespace is a host iff it is a *strict ancestor* of a deeper namespace, * and inner function-node leaves are never promoted on their own. * * Two host signals are derived from the checkpoint namespaces observed in * history (`state` + `task` `checkpoint_ns`): * * - **Interior hosts** — every strict ancestor of an observed namespace. * Each segment of a nested `checkpoint_ns` wraps a deeper one, so the * parent is always a subgraph (this also recovers a host whose own * checkpoint fell outside the fetched page). * - **Top-level hosts** — every depth-1 observed namespace. A top-level * subgraph is always a host, including the values-only shape supported * by `SubgraphDiscovery.#onValuesEvent`, where the host (e.g. * `research:<uuid>`) appears with no deeper `research:<uuid>|...` key. * The old strict-prefix rule dropped those, so reconnecting waited for * SSE replay instead of hydrating the card immediately. * * A *deeper* observed namespace's own deepest segment is NOT promoted * unless it is itself an ancestor of something deeper — otherwise a plain * inner node (e.g. `worker:<uuid>|inner:<uuid>`) would surface as a * spurious subgraph card that was never present during live streaming. * * Tool/subagent namespaces (`tools:` / `task:`) are excluded — those are * owned by {@link ./subagents}. A namespace nested under a subgraph (e.g. * `research:<uuid>|tools:<uuid>`) still promotes its non-internal * `research:<uuid>` ancestor. */ function collectSubgraphHostNamespaces(history) { const observed = /* @__PURE__ */ new Map(); const record = (segments) => { if (segments.length === 0) return; observed.set(require_namespace.namespaceKey(segments), segments); }; for (const state of history) { record(checkpointNsToSegments(state.checkpoint?.checkpoint_ns)); for (const task of getTasks(state)) record(checkpointNsToSegments(task.checkpoint?.checkpoint_ns)); } const pending = /* @__PURE__ */ new Set(); if (history.length > 0) for (const task of getTasks(history[0])) { const segments = checkpointNsToSegments(task.checkpoint?.checkpoint_ns); if (segments.length > 0) pending.add(require_namespace.namespaceKey(segments)); } const candidates = /* @__PURE__ */ new Map(); for (const segments of observed.values()) { for (let depth = 1; depth < segments.length; depth += 1) { const slice = segments.slice(0, depth); candidates.set(require_namespace.namespaceKey(slice), slice); } if (segments.length === 1) candidates.set(require_namespace.namespaceKey(segments), segments); } const hosts = []; for (const [key, segments] of candidates) { if (segments.some(isInternalSegment)) continue; const prefix = key + "\0"; const running = pending.has(key) || [...pending].some((p) => p.startsWith(prefix)); hosts.push({ namespace: segments, status: running ? "running" : "complete" }); } return hosts; } //#endregion exports.collectSubgraphHostNamespaces = collectSubgraphHostNamespaces; exports.getHistoryPage = getHistoryPage; exports.mapSubagentNamespaces = mapSubagentNamespaces; exports.resolveSubagentNamespaces = resolveSubagentNamespaces; //# sourceMappingURL=namespace-from-history.cjs.map