@langchain/langgraph-sdk
Version:
Client library for interacting with the LangGraph API
207 lines (206 loc) • 9.25 kB
JavaScript
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