UNPKG

@langchain/langgraph-sdk

Version:

Client library for interacting with the LangGraph API

1 lines • 17.7 kB
{"version":3,"file":"namespace-from-history.cjs","names":["normalizeAIMessageToolCalls","DEFAULT_MESSAGES_KEY","namespaceKey"],"sources":["../../../src/stream/discovery/namespace-from-history.ts"],"sourcesContent":["/**\n * Reconstruct discovery namespaces from checkpoint history.\n *\n * On reconnect the always-on SSE replay eventually re-derives every\n * subagent execution namespace and every subgraph host, but that costs\n * a full depth-1 replay. This module derives the same information from\n * a single bounded `getHistory()` read so the controller can promote\n * namespaces immediately on `hydrate()` (subgraph hosts + subagent\n * execution namespaces) and lazily for a single opened subagent.\n *\n * The logic mirrors the live discovery state machines so a\n * history-derived namespace and an SSE-derived one cannot disagree:\n * - subagent mapping ports {@link ../../ui/manager} `fetchSubagentHistory`\n * (direct task-result mapping, then positional Send-index fallback);\n * - subgraph host detection ports the strict-prefix promotion rule in\n * {@link ./subgraphs}.\n */\nimport type { Client } from \"../../client/index.js\";\nimport type {\n Checkpoint,\n Config,\n Metadata,\n ThreadState,\n} from \"../../schema.js\";\nimport { NAMESPACE_SEPARATOR, DEFAULT_MESSAGES_KEY } from \"../constants.js\";\nimport { namespaceKey } from \"../namespace.js\";\nimport { normalizeAIMessageToolCalls } from \"../message-coercion.js\";\n\ntype AnyCheckpoint = ThreadState<Record<string, unknown>>;\n\ninterface HistoryTask {\n id?: unknown;\n name?: unknown;\n path?: unknown;\n result?: { messages?: unknown[] };\n checkpoint?: { checkpoint_ns?: unknown } | null;\n}\n\nfunction getTasks(checkpoint: AnyCheckpoint): HistoryTask[] {\n const tasks = (checkpoint as { tasks?: unknown }).tasks;\n return Array.isArray(tasks) ? (tasks as HistoryTask[]) : [];\n}\n\n/**\n * Phase 1 (preferred): map a `task` tool-call id directly to its\n * execution namespace via the task's result `ToolMessage`. Unambiguous\n * — works even when a step mixes subagent and non-subagent tool calls.\n *\n * The subgraph checkpoint_ns is `task.name + \":\" + task.id` (mirrors\n * pregel's `taskCheckpointNamespace`), so we derive it from name+id\n * rather than the always-null completed-task checkpoint.\n */\nexport function collectDirectTaskMappings(\n tasks: HistoryTask[],\n targets: Set<string>,\n out: Map<string, string>\n): void {\n for (const task of tasks) {\n if (\n !Array.isArray(task.path) ||\n task.path[0] !== \"__pregel_push\" ||\n typeof task.id !== \"string\" ||\n typeof task.name !== \"string\"\n ) {\n continue;\n }\n const resultMessages = task.result?.messages;\n if (!Array.isArray(resultMessages)) continue;\n for (const msg of resultMessages) {\n const m = msg as Record<string, unknown>;\n const id = m.tool_call_id;\n if (\n m.type === \"tool\" &&\n typeof id === \"string\" &&\n targets.has(id) &&\n !out.has(id)\n ) {\n out.set(id, `${task.name}:${task.id}`);\n }\n }\n }\n}\n\n/**\n * Phase 2 (fallback): align still-pending push tasks to the triggering\n * AI message's tool calls by Send index (`path[1]`), where `path[1]`\n * indexes into the *full* `tool_calls` array. Only push tasks whose\n * targeted call is a subagent `task` are mapped, so a sibling\n * non-subagent tool call in the same message can't capture a subagent's\n * namespace. Applied only to ids Phase 1 could not resolve so a correct\n * direct mapping is never overwritten by a positional guess.\n */\nexport function collectPositionalTaskMappings(\n checkpoint: AnyCheckpoint,\n targets: Set<string>,\n out: Map<string, string>,\n messagesKey: string\n): void {\n const pushTasks = getTasks(checkpoint).filter(\n (t) =>\n Array.isArray(t.path) &&\n t.path[0] === \"__pregel_push\" &&\n typeof t.path[1] === \"number\" &&\n // Send tasks are `[PUSH, i]` (legacy JS servers) or `[PUSH, i, false]`\n // (aligned with Python); longer paths are error handlers of failed\n // Sends (`[PUSH, i, false, \"node_error_handler\", false]`) and must not\n // be mistaken for a second Send at the same index.\n t.path.length <= 3 &&\n typeof t.id === \"string\" &&\n typeof t.name === \"string\"\n );\n if (pushTasks.length === 0) return;\n\n const msgs = (checkpoint.values as Record<string, unknown> | undefined)?.[\n messagesKey\n ];\n if (!Array.isArray(msgs)) return;\n\n let toolCalls: Array<{ id?: string; name?: string }> | undefined;\n for (let i = msgs.length - 1; i >= 0; i -= 1) {\n const m = msgs[i] as Record<string, unknown>;\n if (m.type !== \"ai\") continue;\n const normalized = normalizeAIMessageToolCalls(\n m as unknown as Parameters<typeof normalizeAIMessageToolCalls>[0]\n ) as Record<string, unknown>;\n const normalizedToolCalls = normalized.tool_calls;\n if (\n Array.isArray(normalizedToolCalls) &&\n (normalizedToolCalls as Array<{ name?: string }>).some(\n (tc) => tc.name === \"task\"\n )\n ) {\n toolCalls = normalizedToolCalls as Array<{\n id?: string;\n name?: string;\n }>;\n break;\n }\n }\n if (toolCalls == null) return;\n\n // `path[1]` is the Send index into the *full* `tool_calls` array, not the\n // subagent-only subset. Resolve each push task against that array and map\n // it only when the targeted call is itself a `task`, so a sibling\n // non-subagent tool call cannot capture a subagent's namespace.\n for (const task of pushTasks) {\n const index = (task.path as unknown[])[1] as number;\n const tc = toolCalls[index];\n if (\n tc?.name === \"task\" &&\n tc.id != null &&\n typeof task.id === \"string\" &&\n typeof task.name === \"string\" &&\n targets.has(tc.id) &&\n !out.has(tc.id)\n ) {\n out.set(tc.id, `${task.name}:${task.id}`);\n }\n }\n}\n\n/** Build a `getHistory` `before` cursor from a history entry. */\nfunction beforeCursor(entry: AnyCheckpoint): Config | undefined {\n const checkpointId = entry.checkpoint?.checkpoint_id;\n if (typeof checkpointId !== \"string\") return undefined;\n return { configurable: { checkpoint_id: checkpointId } };\n}\n\nfunction applyCollectors(\n history: AnyCheckpoint[],\n targets: Set<string>,\n out: Map<string, string>,\n messagesKey: string\n): void {\n for (const checkpoint of history) {\n collectDirectTaskMappings(getTasks(checkpoint), targets, out);\n }\n const stillUnmapped = () => [...targets].some((id) => !out.has(id));\n if (stillUnmapped()) {\n for (const checkpoint of history) {\n if (!stillUnmapped()) break;\n collectPositionalTaskMappings(checkpoint, targets, out, messagesKey);\n }\n }\n}\n\n/**\n * Synchronous subagent namespace mapping over an already-fetched\n * history page. Used by {@link resolveSubagentNamespaces} and by the\n * controller's hydrate-time bulk seed (which shares a single\n * `getHistory` page with subgraph host detection).\n */\nexport function mapSubagentNamespaces(\n history: AnyCheckpoint[],\n toolCallIds: string[],\n messagesKey = DEFAULT_MESSAGES_KEY\n): Map<string, string> {\n const out = new Map<string, string>();\n const targets = new Set(toolCallIds);\n if (targets.size === 0) return out;\n applyCollectors(history, targets, out, messagesKey);\n return out;\n}\n\n/**\n * Resolve execution namespaces for the given subagent `task` tool-call\n * ids from checkpoint history.\n *\n * Bounded and O(1) in calls: one `getHistory` page, plus at most one\n * `before`-cursor fallback page when the first leaves ids unresolved.\n * Never fans out per id.\n *\n * @returns Map of `toolCallId` → single execution namespace segment\n * (e.g. `tools:<uuid>`). Unresolved ids are omitted.\n */\nexport async function resolveSubagentNamespaces<TStateType>(\n client: Client<TStateType>,\n threadId: string,\n toolCallIds: string[],\n opts?: { limit?: number; messagesKey?: string; signal?: AbortSignal }\n): Promise<Map<string, string>> {\n const out = new Map<string, string>();\n const targets = new Set(toolCallIds);\n if (targets.size === 0) return out;\n\n const limit = opts?.limit ?? 20;\n const messagesKey = opts?.messagesKey ?? DEFAULT_MESSAGES_KEY;\n const signal = opts?.signal;\n\n const page1 = await getHistoryPage(client, threadId, { limit, signal });\n applyCollectors(page1, targets, out, messagesKey);\n\n const unresolved = [...targets].filter((id) => !out.has(id));\n if (unresolved.length > 0 && page1.length > 0) {\n const before = beforeCursor(page1[page1.length - 1]);\n if (before != null) {\n const page2 = await getHistoryPage(client, threadId, {\n limit,\n before,\n signal,\n });\n applyCollectors(page2, targets, out, messagesKey);\n }\n }\n\n return out;\n}\n\n/**\n * Fetch one bounded history page typed as plain records, sidestepping\n * the client's `TStateType` generic so the discovery collectors (which\n * read raw `values`/`tasks`) get a stable {@link AnyCheckpoint} shape.\n */\nexport function getHistoryPage<TStateType>(\n client: Client<TStateType>,\n threadId: string,\n options: {\n limit?: number;\n before?: Config;\n checkpoint?: Partial<Omit<Checkpoint, \"thread_id\">>;\n metadata?: Metadata;\n signal?: AbortSignal;\n }\n): Promise<AnyCheckpoint[]> {\n return client.threads.getHistory<Record<string, unknown>>(threadId, options);\n}\n\nexport interface SubgraphHost {\n namespace: string[];\n status: \"running\" | \"complete\" | \"error\";\n}\n\nfunction checkpointNsToSegments(checkpointNs: unknown): string[] {\n if (typeof checkpointNs !== \"string\" || checkpointNs.length === 0) return [];\n return checkpointNs.split(\"|\").filter((segment) => segment.length > 0);\n}\n\nfunction isInternalSegment(segment: string): boolean {\n return segment.startsWith(\"tools:\") || segment.startsWith(\"task:\");\n}\n\n/**\n * Identify subgraph host namespaces from checkpoint history, mirroring the\n * promotion rule of the live {@link ./subgraphs} `SubgraphDiscovery`: a\n * namespace is a host iff it is a *strict ancestor* of a deeper namespace,\n * and inner function-node leaves are never promoted on their own.\n *\n * Two host signals are derived from the checkpoint namespaces observed in\n * history (`state` + `task` `checkpoint_ns`):\n *\n * - **Interior hosts** — every strict ancestor of an observed namespace.\n * Each segment of a nested `checkpoint_ns` wraps a deeper one, so the\n * parent is always a subgraph (this also recovers a host whose own\n * checkpoint fell outside the fetched page).\n * - **Top-level hosts** — every depth-1 observed namespace. A top-level\n * subgraph is always a host, including the values-only shape supported\n * by `SubgraphDiscovery.#onValuesEvent`, where the host (e.g.\n * `research:<uuid>`) appears with no deeper `research:<uuid>|...` key.\n * The old strict-prefix rule dropped those, so reconnecting waited for\n * SSE replay instead of hydrating the card immediately.\n *\n * A *deeper* observed namespace's own deepest segment is NOT promoted\n * unless it is itself an ancestor of something deeper — otherwise a plain\n * inner node (e.g. `worker:<uuid>|inner:<uuid>`) would surface as a\n * spurious subgraph card that was never present during live streaming.\n *\n * Tool/subagent namespaces (`tools:` / `task:`) are excluded — those are\n * owned by {@link ./subagents}. A namespace nested under a subgraph (e.g.\n * `research:<uuid>|tools:<uuid>`) still promotes its non-internal\n * `research:<uuid>` ancestor.\n */\nexport function collectSubgraphHostNamespaces(\n history: AnyCheckpoint[]\n): SubgraphHost[] {\n // Directly observed namespace tuples (state + task checkpoint_ns), with\n // NO prefix synthesis — host promotion is derived below so that a deeper\n // namespace's leaf segment is not mistaken for a host.\n const observed = new Map<string, string[]>();\n const record = (segments: string[]) => {\n if (segments.length === 0) return;\n observed.set(namespaceKey(segments), segments);\n };\n for (const state of history) {\n record(checkpointNsToSegments(state.checkpoint?.checkpoint_ns));\n for (const task of getTasks(state)) {\n record(checkpointNsToSegments(task.checkpoint?.checkpoint_ns));\n }\n }\n\n // Pending namespaces of the newest checkpoint → still running.\n const pending = new Set<string>();\n if (history.length > 0) {\n for (const task of getTasks(history[0])) {\n const segments = checkpointNsToSegments(task.checkpoint?.checkpoint_ns);\n if (segments.length > 0) pending.add(namespaceKey(segments));\n }\n }\n\n // Host candidates = strict ancestors of observed (interior) + depth-1\n // observed (top-level). Insertion order: shallow ancestors first so the\n // emitted host list keeps parents ahead of children.\n const candidates = new Map<string, string[]>();\n for (const segments of observed.values()) {\n for (let depth = 1; depth < segments.length; depth += 1) {\n const slice = segments.slice(0, depth);\n candidates.set(namespaceKey(slice), slice);\n }\n if (segments.length === 1) {\n candidates.set(namespaceKey(segments), segments);\n }\n }\n\n const hosts: SubgraphHost[] = [];\n for (const [key, segments] of candidates) {\n if (segments.some(isInternalSegment)) continue;\n const prefix = key + NAMESPACE_SEPARATOR;\n const running =\n pending.has(key) || [...pending].some((p) => p.startsWith(prefix));\n hosts.push({\n namespace: segments,\n status: running ? \"running\" : \"complete\",\n });\n }\n return hosts;\n}\n"],"mappings":"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"}