@langchain/langgraph-sdk 1.9.15 → 1.9.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/base.cjs +70 -4
- package/dist/client/base.cjs.map +1 -1
- package/dist/client/base.d.cts +3 -0
- package/dist/client/base.d.cts.map +1 -1
- package/dist/client/base.d.ts +3 -0
- package/dist/client/base.d.ts.map +1 -1
- package/dist/client/base.js +70 -4
- package/dist/client/base.js.map +1 -1
- package/dist/client/threads/index.cjs +4 -2
- package/dist/client/threads/index.cjs.map +1 -1
- package/dist/client/threads/index.d.cts.map +1 -1
- package/dist/client/threads/index.d.ts.map +1 -1
- package/dist/client/threads/index.js +4 -2
- package/dist/client/threads/index.js.map +1 -1
- package/dist/stream/controller.cjs +496 -46
- package/dist/stream/controller.cjs.map +1 -1
- package/dist/stream/controller.d.cts +15 -0
- package/dist/stream/controller.d.cts.map +1 -1
- package/dist/stream/controller.d.ts +15 -0
- package/dist/stream/controller.d.ts.map +1 -1
- package/dist/stream/controller.js +517 -46
- package/dist/stream/controller.js.map +1 -1
- package/dist/stream/discovery/index.cjs +2 -0
- package/dist/stream/discovery/index.js +3 -0
- package/dist/stream/discovery/namespace-from-history.cjs +207 -0
- package/dist/stream/discovery/namespace-from-history.cjs.map +1 -0
- package/dist/stream/discovery/namespace-from-history.js +204 -0
- package/dist/stream/discovery/namespace-from-history.js.map +1 -0
- package/dist/stream/discovery/subagents.cjs +56 -1
- package/dist/stream/discovery/subagents.cjs.map +1 -1
- package/dist/stream/discovery/subagents.d.cts +31 -0
- package/dist/stream/discovery/subagents.d.cts.map +1 -1
- package/dist/stream/discovery/subagents.d.ts +31 -0
- package/dist/stream/discovery/subagents.d.ts.map +1 -1
- package/dist/stream/discovery/subagents.js +56 -1
- package/dist/stream/discovery/subagents.js.map +1 -1
- package/dist/stream/discovery/subgraphs.cjs +24 -0
- package/dist/stream/discovery/subgraphs.cjs.map +1 -1
- package/dist/stream/discovery/subgraphs.d.cts +13 -0
- package/dist/stream/discovery/subgraphs.d.cts.map +1 -1
- package/dist/stream/discovery/subgraphs.d.ts +13 -0
- package/dist/stream/discovery/subgraphs.d.ts.map +1 -1
- package/dist/stream/discovery/subgraphs.js +24 -0
- package/dist/stream/discovery/subgraphs.js.map +1 -1
- package/dist/stream/index.cjs +1 -0
- package/dist/stream/index.js +1 -0
- package/dist/stream/message-coercion.cjs +101 -0
- package/dist/stream/message-coercion.cjs.map +1 -0
- package/dist/stream/message-coercion.d.ts +1 -0
- package/dist/stream/message-coercion.js +98 -0
- package/dist/stream/message-coercion.js.map +1 -0
- package/dist/stream/message-metadata-tracker.cjs +92 -0
- package/dist/stream/message-metadata-tracker.cjs.map +1 -1
- package/dist/stream/message-metadata-tracker.d.cts +23 -0
- package/dist/stream/message-metadata-tracker.d.cts.map +1 -1
- package/dist/stream/message-metadata-tracker.d.ts +23 -0
- package/dist/stream/message-metadata-tracker.d.ts.map +1 -1
- package/dist/stream/message-metadata-tracker.js +92 -0
- package/dist/stream/message-metadata-tracker.js.map +1 -1
- package/dist/stream/message-reconciliation.cjs +2 -2
- package/dist/stream/message-reconciliation.cjs.map +1 -1
- package/dist/stream/message-reconciliation.js +2 -2
- package/dist/stream/message-reconciliation.js.map +1 -1
- package/dist/stream/optimistic-input.cjs +86 -0
- package/dist/stream/optimistic-input.cjs.map +1 -0
- package/dist/stream/optimistic-input.d.ts +1 -0
- package/dist/stream/optimistic-input.js +86 -0
- package/dist/stream/optimistic-input.js.map +1 -0
- package/dist/stream/projections/messages.cjs +24 -14
- package/dist/stream/projections/messages.cjs.map +1 -1
- package/dist/stream/projections/messages.js +21 -11
- package/dist/stream/projections/messages.js.map +1 -1
- package/dist/stream/projections/tool-calls.cjs +22 -10
- package/dist/stream/projections/tool-calls.cjs.map +1 -1
- package/dist/stream/projections/tool-calls.js +22 -10
- package/dist/stream/projections/tool-calls.js.map +1 -1
- package/dist/stream/projections/values.cjs +2 -2
- package/dist/stream/projections/values.cjs.map +1 -1
- package/dist/stream/projections/values.js +1 -1
- package/dist/stream/projections/values.js.map +1 -1
- package/dist/stream/root-message-projection.cjs +130 -3
- package/dist/stream/root-message-projection.cjs.map +1 -1
- package/dist/stream/root-message-projection.js +130 -3
- package/dist/stream/root-message-projection.js.map +1 -1
- package/dist/stream/submit-coordinator.cjs +100 -6
- package/dist/stream/submit-coordinator.cjs.map +1 -1
- package/dist/stream/submit-coordinator.d.cts.map +1 -1
- package/dist/stream/submit-coordinator.d.ts +0 -1
- package/dist/stream/submit-coordinator.d.ts.map +1 -1
- package/dist/stream/submit-coordinator.js +100 -6
- package/dist/stream/submit-coordinator.js.map +1 -1
- package/dist/stream/tool-calls.cjs +32 -0
- package/dist/stream/tool-calls.cjs.map +1 -1
- package/dist/stream/tool-calls.js +32 -1
- package/dist/stream/tool-calls.js.map +1 -1
- package/dist/stream/types.d.cts +43 -0
- package/dist/stream/types.d.cts.map +1 -1
- package/dist/stream/types.d.ts +43 -0
- package/dist/stream/types.d.ts.map +1 -1
- package/dist/ui/index.d.cts +1 -1
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/messages.cjs +4 -50
- package/dist/ui/messages.cjs.map +1 -1
- package/dist/ui/messages.d.cts.map +1 -1
- package/dist/ui/messages.d.ts.map +1 -1
- package/dist/ui/messages.js +3 -48
- package/dist/ui/messages.js.map +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
const require_message_coercion = require("../message-coercion.cjs");
|
|
2
|
+
require("../constants.cjs");
|
|
3
|
+
const require_namespace = require("../namespace.cjs");
|
|
4
|
+
//#region src/stream/discovery/namespace-from-history.ts
|
|
5
|
+
function getTasks(checkpoint) {
|
|
6
|
+
const tasks = checkpoint.tasks;
|
|
7
|
+
return Array.isArray(tasks) ? tasks : [];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Phase 1 (preferred): map a `task` tool-call id directly to its
|
|
11
|
+
* execution namespace via the task's result `ToolMessage`. Unambiguous
|
|
12
|
+
* — works even when a step mixes subagent and non-subagent tool calls.
|
|
13
|
+
*
|
|
14
|
+
* The subgraph checkpoint_ns is `task.name + ":" + task.id` (mirrors
|
|
15
|
+
* pregel's `taskCheckpointNamespace`), so we derive it from name+id
|
|
16
|
+
* rather than the always-null completed-task checkpoint.
|
|
17
|
+
*/
|
|
18
|
+
function collectDirectTaskMappings(tasks, targets, out) {
|
|
19
|
+
for (const task of tasks) {
|
|
20
|
+
if (!Array.isArray(task.path) || task.path[0] !== "__pregel_push" || typeof task.id !== "string" || typeof task.name !== "string") continue;
|
|
21
|
+
const resultMessages = task.result?.messages;
|
|
22
|
+
if (!Array.isArray(resultMessages)) continue;
|
|
23
|
+
for (const msg of resultMessages) {
|
|
24
|
+
const m = msg;
|
|
25
|
+
const id = m.tool_call_id;
|
|
26
|
+
if (m.type === "tool" && typeof id === "string" && targets.has(id) && !out.has(id)) out.set(id, `${task.name}:${task.id}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Phase 2 (fallback): align still-pending push tasks to the triggering
|
|
32
|
+
* AI message's tool calls by Send index (`path[1]`), where `path[1]`
|
|
33
|
+
* indexes into the *full* `tool_calls` array. Only push tasks whose
|
|
34
|
+
* targeted call is a subagent `task` are mapped, so a sibling
|
|
35
|
+
* non-subagent tool call in the same message can't capture a subagent's
|
|
36
|
+
* namespace. Applied only to ids Phase 1 could not resolve so a correct
|
|
37
|
+
* direct mapping is never overwritten by a positional guess.
|
|
38
|
+
*/
|
|
39
|
+
function collectPositionalTaskMappings(checkpoint, targets, out, messagesKey) {
|
|
40
|
+
const pushTasks = getTasks(checkpoint).filter((t) => Array.isArray(t.path) && t.path[0] === "__pregel_push" && typeof t.path[1] === "number" && typeof t.id === "string" && typeof t.name === "string");
|
|
41
|
+
if (pushTasks.length === 0) return;
|
|
42
|
+
const msgs = checkpoint.values?.[messagesKey];
|
|
43
|
+
if (!Array.isArray(msgs)) return;
|
|
44
|
+
let toolCalls;
|
|
45
|
+
for (let i = msgs.length - 1; i >= 0; i -= 1) {
|
|
46
|
+
const m = msgs[i];
|
|
47
|
+
if (m.type !== "ai") continue;
|
|
48
|
+
const normalizedToolCalls = require_message_coercion.normalizeAIMessageToolCalls(m).tool_calls;
|
|
49
|
+
if (Array.isArray(normalizedToolCalls) && normalizedToolCalls.some((tc) => tc.name === "task")) {
|
|
50
|
+
toolCalls = normalizedToolCalls;
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (toolCalls == null) return;
|
|
55
|
+
for (const task of pushTasks) {
|
|
56
|
+
const index = task.path[1];
|
|
57
|
+
const tc = toolCalls[index];
|
|
58
|
+
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}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** Build a `getHistory` `before` cursor from a history entry. */
|
|
62
|
+
function beforeCursor(entry) {
|
|
63
|
+
const checkpointId = entry.checkpoint?.checkpoint_id;
|
|
64
|
+
if (typeof checkpointId !== "string") return void 0;
|
|
65
|
+
return { configurable: { checkpoint_id: checkpointId } };
|
|
66
|
+
}
|
|
67
|
+
function applyCollectors(history, targets, out, messagesKey) {
|
|
68
|
+
for (const checkpoint of history) collectDirectTaskMappings(getTasks(checkpoint), targets, out);
|
|
69
|
+
const stillUnmapped = () => [...targets].some((id) => !out.has(id));
|
|
70
|
+
if (stillUnmapped()) for (const checkpoint of history) {
|
|
71
|
+
if (!stillUnmapped()) break;
|
|
72
|
+
collectPositionalTaskMappings(checkpoint, targets, out, messagesKey);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Synchronous subagent namespace mapping over an already-fetched
|
|
77
|
+
* history page. Used by {@link resolveSubagentNamespaces} and by the
|
|
78
|
+
* controller's hydrate-time bulk seed (which shares a single
|
|
79
|
+
* `getHistory` page with subgraph host detection).
|
|
80
|
+
*/
|
|
81
|
+
function mapSubagentNamespaces(history, toolCallIds, messagesKey = "messages") {
|
|
82
|
+
const out = /* @__PURE__ */ new Map();
|
|
83
|
+
const targets = new Set(toolCallIds);
|
|
84
|
+
if (targets.size === 0) return out;
|
|
85
|
+
applyCollectors(history, targets, out, messagesKey);
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Resolve execution namespaces for the given subagent `task` tool-call
|
|
90
|
+
* ids from checkpoint history.
|
|
91
|
+
*
|
|
92
|
+
* Bounded and O(1) in calls: one `getHistory` page, plus at most one
|
|
93
|
+
* `before`-cursor fallback page when the first leaves ids unresolved.
|
|
94
|
+
* Never fans out per id.
|
|
95
|
+
*
|
|
96
|
+
* @returns Map of `toolCallId` → single execution namespace segment
|
|
97
|
+
* (e.g. `tools:<uuid>`). Unresolved ids are omitted.
|
|
98
|
+
*/
|
|
99
|
+
async function resolveSubagentNamespaces(client, threadId, toolCallIds, opts) {
|
|
100
|
+
const out = /* @__PURE__ */ new Map();
|
|
101
|
+
const targets = new Set(toolCallIds);
|
|
102
|
+
if (targets.size === 0) return out;
|
|
103
|
+
const limit = opts?.limit ?? 20;
|
|
104
|
+
const messagesKey = opts?.messagesKey ?? "messages";
|
|
105
|
+
const signal = opts?.signal;
|
|
106
|
+
const page1 = await getHistoryPage(client, threadId, {
|
|
107
|
+
limit,
|
|
108
|
+
signal
|
|
109
|
+
});
|
|
110
|
+
applyCollectors(page1, targets, out, messagesKey);
|
|
111
|
+
if ([...targets].filter((id) => !out.has(id)).length > 0 && page1.length > 0) {
|
|
112
|
+
const before = beforeCursor(page1[page1.length - 1]);
|
|
113
|
+
if (before != null) applyCollectors(await getHistoryPage(client, threadId, {
|
|
114
|
+
limit,
|
|
115
|
+
before,
|
|
116
|
+
signal
|
|
117
|
+
}), targets, out, messagesKey);
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Fetch one bounded history page typed as plain records, sidestepping
|
|
123
|
+
* the client's `TStateType` generic so the discovery collectors (which
|
|
124
|
+
* read raw `values`/`tasks`) get a stable {@link AnyCheckpoint} shape.
|
|
125
|
+
*/
|
|
126
|
+
function getHistoryPage(client, threadId, options) {
|
|
127
|
+
return client.threads.getHistory(threadId, options);
|
|
128
|
+
}
|
|
129
|
+
function checkpointNsToSegments(checkpointNs) {
|
|
130
|
+
if (typeof checkpointNs !== "string" || checkpointNs.length === 0) return [];
|
|
131
|
+
return checkpointNs.split("|").filter((segment) => segment.length > 0);
|
|
132
|
+
}
|
|
133
|
+
function isInternalSegment(segment) {
|
|
134
|
+
return segment.startsWith("tools:") || segment.startsWith("task:");
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Identify subgraph host namespaces from checkpoint history, mirroring the
|
|
138
|
+
* promotion rule of the live {@link ./subgraphs} `SubgraphDiscovery`: a
|
|
139
|
+
* namespace is a host iff it is a *strict ancestor* of a deeper namespace,
|
|
140
|
+
* and inner function-node leaves are never promoted on their own.
|
|
141
|
+
*
|
|
142
|
+
* Two host signals are derived from the checkpoint namespaces observed in
|
|
143
|
+
* history (`state` + `task` `checkpoint_ns`):
|
|
144
|
+
*
|
|
145
|
+
* - **Interior hosts** — every strict ancestor of an observed namespace.
|
|
146
|
+
* Each segment of a nested `checkpoint_ns` wraps a deeper one, so the
|
|
147
|
+
* parent is always a subgraph (this also recovers a host whose own
|
|
148
|
+
* checkpoint fell outside the fetched page).
|
|
149
|
+
* - **Top-level hosts** — every depth-1 observed namespace. A top-level
|
|
150
|
+
* subgraph is always a host, including the values-only shape supported
|
|
151
|
+
* by `SubgraphDiscovery.#onValuesEvent`, where the host (e.g.
|
|
152
|
+
* `research:<uuid>`) appears with no deeper `research:<uuid>|...` key.
|
|
153
|
+
* The old strict-prefix rule dropped those, so reconnecting waited for
|
|
154
|
+
* SSE replay instead of hydrating the card immediately.
|
|
155
|
+
*
|
|
156
|
+
* A *deeper* observed namespace's own deepest segment is NOT promoted
|
|
157
|
+
* unless it is itself an ancestor of something deeper — otherwise a plain
|
|
158
|
+
* inner node (e.g. `worker:<uuid>|inner:<uuid>`) would surface as a
|
|
159
|
+
* spurious subgraph card that was never present during live streaming.
|
|
160
|
+
*
|
|
161
|
+
* Tool/subagent namespaces (`tools:` / `task:`) are excluded — those are
|
|
162
|
+
* owned by {@link ./subagents}. A namespace nested under a subgraph (e.g.
|
|
163
|
+
* `research:<uuid>|tools:<uuid>`) still promotes its non-internal
|
|
164
|
+
* `research:<uuid>` ancestor.
|
|
165
|
+
*/
|
|
166
|
+
function collectSubgraphHostNamespaces(history) {
|
|
167
|
+
const observed = /* @__PURE__ */ new Map();
|
|
168
|
+
const record = (segments) => {
|
|
169
|
+
if (segments.length === 0) return;
|
|
170
|
+
observed.set(require_namespace.namespaceKey(segments), segments);
|
|
171
|
+
};
|
|
172
|
+
for (const state of history) {
|
|
173
|
+
record(checkpointNsToSegments(state.checkpoint?.checkpoint_ns));
|
|
174
|
+
for (const task of getTasks(state)) record(checkpointNsToSegments(task.checkpoint?.checkpoint_ns));
|
|
175
|
+
}
|
|
176
|
+
const pending = /* @__PURE__ */ new Set();
|
|
177
|
+
if (history.length > 0) for (const task of getTasks(history[0])) {
|
|
178
|
+
const segments = checkpointNsToSegments(task.checkpoint?.checkpoint_ns);
|
|
179
|
+
if (segments.length > 0) pending.add(require_namespace.namespaceKey(segments));
|
|
180
|
+
}
|
|
181
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
182
|
+
for (const segments of observed.values()) {
|
|
183
|
+
for (let depth = 1; depth < segments.length; depth += 1) {
|
|
184
|
+
const slice = segments.slice(0, depth);
|
|
185
|
+
candidates.set(require_namespace.namespaceKey(slice), slice);
|
|
186
|
+
}
|
|
187
|
+
if (segments.length === 1) candidates.set(require_namespace.namespaceKey(segments), segments);
|
|
188
|
+
}
|
|
189
|
+
const hosts = [];
|
|
190
|
+
for (const [key, segments] of candidates) {
|
|
191
|
+
if (segments.some(isInternalSegment)) continue;
|
|
192
|
+
const prefix = key + "\0";
|
|
193
|
+
const running = pending.has(key) || [...pending].some((p) => p.startsWith(prefix));
|
|
194
|
+
hosts.push({
|
|
195
|
+
namespace: segments,
|
|
196
|
+
status: running ? "running" : "complete"
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
return hosts;
|
|
200
|
+
}
|
|
201
|
+
//#endregion
|
|
202
|
+
exports.collectSubgraphHostNamespaces = collectSubgraphHostNamespaces;
|
|
203
|
+
exports.getHistoryPage = getHistoryPage;
|
|
204
|
+
exports.mapSubagentNamespaces = mapSubagentNamespaces;
|
|
205
|
+
exports.resolveSubagentNamespaces = resolveSubagentNamespaces;
|
|
206
|
+
|
|
207
|
+
//# sourceMappingURL=namespace-from-history.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"namespace-from-history.cjs","names":["normalizeAIMessageToolCalls","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 } 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 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 = \"messages\"\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 ?? \"messages\";\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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"}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { normalizeAIMessageToolCalls } from "../message-coercion.js";
|
|
2
|
+
import "../constants.js";
|
|
3
|
+
import { namespaceKey } from "../namespace.js";
|
|
4
|
+
//#region src/stream/discovery/namespace-from-history.ts
|
|
5
|
+
function getTasks(checkpoint) {
|
|
6
|
+
const tasks = checkpoint.tasks;
|
|
7
|
+
return Array.isArray(tasks) ? tasks : [];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Phase 1 (preferred): map a `task` tool-call id directly to its
|
|
11
|
+
* execution namespace via the task's result `ToolMessage`. Unambiguous
|
|
12
|
+
* — works even when a step mixes subagent and non-subagent tool calls.
|
|
13
|
+
*
|
|
14
|
+
* The subgraph checkpoint_ns is `task.name + ":" + task.id` (mirrors
|
|
15
|
+
* pregel's `taskCheckpointNamespace`), so we derive it from name+id
|
|
16
|
+
* rather than the always-null completed-task checkpoint.
|
|
17
|
+
*/
|
|
18
|
+
function collectDirectTaskMappings(tasks, targets, out) {
|
|
19
|
+
for (const task of tasks) {
|
|
20
|
+
if (!Array.isArray(task.path) || task.path[0] !== "__pregel_push" || typeof task.id !== "string" || typeof task.name !== "string") continue;
|
|
21
|
+
const resultMessages = task.result?.messages;
|
|
22
|
+
if (!Array.isArray(resultMessages)) continue;
|
|
23
|
+
for (const msg of resultMessages) {
|
|
24
|
+
const m = msg;
|
|
25
|
+
const id = m.tool_call_id;
|
|
26
|
+
if (m.type === "tool" && typeof id === "string" && targets.has(id) && !out.has(id)) out.set(id, `${task.name}:${task.id}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Phase 2 (fallback): align still-pending push tasks to the triggering
|
|
32
|
+
* AI message's tool calls by Send index (`path[1]`), where `path[1]`
|
|
33
|
+
* indexes into the *full* `tool_calls` array. Only push tasks whose
|
|
34
|
+
* targeted call is a subagent `task` are mapped, so a sibling
|
|
35
|
+
* non-subagent tool call in the same message can't capture a subagent's
|
|
36
|
+
* namespace. Applied only to ids Phase 1 could not resolve so a correct
|
|
37
|
+
* direct mapping is never overwritten by a positional guess.
|
|
38
|
+
*/
|
|
39
|
+
function collectPositionalTaskMappings(checkpoint, targets, out, messagesKey) {
|
|
40
|
+
const pushTasks = getTasks(checkpoint).filter((t) => Array.isArray(t.path) && t.path[0] === "__pregel_push" && typeof t.path[1] === "number" && typeof t.id === "string" && typeof t.name === "string");
|
|
41
|
+
if (pushTasks.length === 0) return;
|
|
42
|
+
const msgs = checkpoint.values?.[messagesKey];
|
|
43
|
+
if (!Array.isArray(msgs)) return;
|
|
44
|
+
let toolCalls;
|
|
45
|
+
for (let i = msgs.length - 1; i >= 0; i -= 1) {
|
|
46
|
+
const m = msgs[i];
|
|
47
|
+
if (m.type !== "ai") continue;
|
|
48
|
+
const normalizedToolCalls = normalizeAIMessageToolCalls(m).tool_calls;
|
|
49
|
+
if (Array.isArray(normalizedToolCalls) && normalizedToolCalls.some((tc) => tc.name === "task")) {
|
|
50
|
+
toolCalls = normalizedToolCalls;
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (toolCalls == null) return;
|
|
55
|
+
for (const task of pushTasks) {
|
|
56
|
+
const index = task.path[1];
|
|
57
|
+
const tc = toolCalls[index];
|
|
58
|
+
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}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** Build a `getHistory` `before` cursor from a history entry. */
|
|
62
|
+
function beforeCursor(entry) {
|
|
63
|
+
const checkpointId = entry.checkpoint?.checkpoint_id;
|
|
64
|
+
if (typeof checkpointId !== "string") return void 0;
|
|
65
|
+
return { configurable: { checkpoint_id: checkpointId } };
|
|
66
|
+
}
|
|
67
|
+
function applyCollectors(history, targets, out, messagesKey) {
|
|
68
|
+
for (const checkpoint of history) collectDirectTaskMappings(getTasks(checkpoint), targets, out);
|
|
69
|
+
const stillUnmapped = () => [...targets].some((id) => !out.has(id));
|
|
70
|
+
if (stillUnmapped()) for (const checkpoint of history) {
|
|
71
|
+
if (!stillUnmapped()) break;
|
|
72
|
+
collectPositionalTaskMappings(checkpoint, targets, out, messagesKey);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Synchronous subagent namespace mapping over an already-fetched
|
|
77
|
+
* history page. Used by {@link resolveSubagentNamespaces} and by the
|
|
78
|
+
* controller's hydrate-time bulk seed (which shares a single
|
|
79
|
+
* `getHistory` page with subgraph host detection).
|
|
80
|
+
*/
|
|
81
|
+
function mapSubagentNamespaces(history, toolCallIds, messagesKey = "messages") {
|
|
82
|
+
const out = /* @__PURE__ */ new Map();
|
|
83
|
+
const targets = new Set(toolCallIds);
|
|
84
|
+
if (targets.size === 0) return out;
|
|
85
|
+
applyCollectors(history, targets, out, messagesKey);
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Resolve execution namespaces for the given subagent `task` tool-call
|
|
90
|
+
* ids from checkpoint history.
|
|
91
|
+
*
|
|
92
|
+
* Bounded and O(1) in calls: one `getHistory` page, plus at most one
|
|
93
|
+
* `before`-cursor fallback page when the first leaves ids unresolved.
|
|
94
|
+
* Never fans out per id.
|
|
95
|
+
*
|
|
96
|
+
* @returns Map of `toolCallId` → single execution namespace segment
|
|
97
|
+
* (e.g. `tools:<uuid>`). Unresolved ids are omitted.
|
|
98
|
+
*/
|
|
99
|
+
async function resolveSubagentNamespaces(client, threadId, toolCallIds, opts) {
|
|
100
|
+
const out = /* @__PURE__ */ new Map();
|
|
101
|
+
const targets = new Set(toolCallIds);
|
|
102
|
+
if (targets.size === 0) return out;
|
|
103
|
+
const limit = opts?.limit ?? 20;
|
|
104
|
+
const messagesKey = opts?.messagesKey ?? "messages";
|
|
105
|
+
const signal = opts?.signal;
|
|
106
|
+
const page1 = await getHistoryPage(client, threadId, {
|
|
107
|
+
limit,
|
|
108
|
+
signal
|
|
109
|
+
});
|
|
110
|
+
applyCollectors(page1, targets, out, messagesKey);
|
|
111
|
+
if ([...targets].filter((id) => !out.has(id)).length > 0 && page1.length > 0) {
|
|
112
|
+
const before = beforeCursor(page1[page1.length - 1]);
|
|
113
|
+
if (before != null) applyCollectors(await getHistoryPage(client, threadId, {
|
|
114
|
+
limit,
|
|
115
|
+
before,
|
|
116
|
+
signal
|
|
117
|
+
}), targets, out, messagesKey);
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Fetch one bounded history page typed as plain records, sidestepping
|
|
123
|
+
* the client's `TStateType` generic so the discovery collectors (which
|
|
124
|
+
* read raw `values`/`tasks`) get a stable {@link AnyCheckpoint} shape.
|
|
125
|
+
*/
|
|
126
|
+
function getHistoryPage(client, threadId, options) {
|
|
127
|
+
return client.threads.getHistory(threadId, options);
|
|
128
|
+
}
|
|
129
|
+
function checkpointNsToSegments(checkpointNs) {
|
|
130
|
+
if (typeof checkpointNs !== "string" || checkpointNs.length === 0) return [];
|
|
131
|
+
return checkpointNs.split("|").filter((segment) => segment.length > 0);
|
|
132
|
+
}
|
|
133
|
+
function isInternalSegment(segment) {
|
|
134
|
+
return segment.startsWith("tools:") || segment.startsWith("task:");
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Identify subgraph host namespaces from checkpoint history, mirroring the
|
|
138
|
+
* promotion rule of the live {@link ./subgraphs} `SubgraphDiscovery`: a
|
|
139
|
+
* namespace is a host iff it is a *strict ancestor* of a deeper namespace,
|
|
140
|
+
* and inner function-node leaves are never promoted on their own.
|
|
141
|
+
*
|
|
142
|
+
* Two host signals are derived from the checkpoint namespaces observed in
|
|
143
|
+
* history (`state` + `task` `checkpoint_ns`):
|
|
144
|
+
*
|
|
145
|
+
* - **Interior hosts** — every strict ancestor of an observed namespace.
|
|
146
|
+
* Each segment of a nested `checkpoint_ns` wraps a deeper one, so the
|
|
147
|
+
* parent is always a subgraph (this also recovers a host whose own
|
|
148
|
+
* checkpoint fell outside the fetched page).
|
|
149
|
+
* - **Top-level hosts** — every depth-1 observed namespace. A top-level
|
|
150
|
+
* subgraph is always a host, including the values-only shape supported
|
|
151
|
+
* by `SubgraphDiscovery.#onValuesEvent`, where the host (e.g.
|
|
152
|
+
* `research:<uuid>`) appears with no deeper `research:<uuid>|...` key.
|
|
153
|
+
* The old strict-prefix rule dropped those, so reconnecting waited for
|
|
154
|
+
* SSE replay instead of hydrating the card immediately.
|
|
155
|
+
*
|
|
156
|
+
* A *deeper* observed namespace's own deepest segment is NOT promoted
|
|
157
|
+
* unless it is itself an ancestor of something deeper — otherwise a plain
|
|
158
|
+
* inner node (e.g. `worker:<uuid>|inner:<uuid>`) would surface as a
|
|
159
|
+
* spurious subgraph card that was never present during live streaming.
|
|
160
|
+
*
|
|
161
|
+
* Tool/subagent namespaces (`tools:` / `task:`) are excluded — those are
|
|
162
|
+
* owned by {@link ./subagents}. A namespace nested under a subgraph (e.g.
|
|
163
|
+
* `research:<uuid>|tools:<uuid>`) still promotes its non-internal
|
|
164
|
+
* `research:<uuid>` ancestor.
|
|
165
|
+
*/
|
|
166
|
+
function collectSubgraphHostNamespaces(history) {
|
|
167
|
+
const observed = /* @__PURE__ */ new Map();
|
|
168
|
+
const record = (segments) => {
|
|
169
|
+
if (segments.length === 0) return;
|
|
170
|
+
observed.set(namespaceKey(segments), segments);
|
|
171
|
+
};
|
|
172
|
+
for (const state of history) {
|
|
173
|
+
record(checkpointNsToSegments(state.checkpoint?.checkpoint_ns));
|
|
174
|
+
for (const task of getTasks(state)) record(checkpointNsToSegments(task.checkpoint?.checkpoint_ns));
|
|
175
|
+
}
|
|
176
|
+
const pending = /* @__PURE__ */ new Set();
|
|
177
|
+
if (history.length > 0) for (const task of getTasks(history[0])) {
|
|
178
|
+
const segments = checkpointNsToSegments(task.checkpoint?.checkpoint_ns);
|
|
179
|
+
if (segments.length > 0) pending.add(namespaceKey(segments));
|
|
180
|
+
}
|
|
181
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
182
|
+
for (const segments of observed.values()) {
|
|
183
|
+
for (let depth = 1; depth < segments.length; depth += 1) {
|
|
184
|
+
const slice = segments.slice(0, depth);
|
|
185
|
+
candidates.set(namespaceKey(slice), slice);
|
|
186
|
+
}
|
|
187
|
+
if (segments.length === 1) candidates.set(namespaceKey(segments), segments);
|
|
188
|
+
}
|
|
189
|
+
const hosts = [];
|
|
190
|
+
for (const [key, segments] of candidates) {
|
|
191
|
+
if (segments.some(isInternalSegment)) continue;
|
|
192
|
+
const prefix = key + "\0";
|
|
193
|
+
const running = pending.has(key) || [...pending].some((p) => p.startsWith(prefix));
|
|
194
|
+
hosts.push({
|
|
195
|
+
namespace: segments,
|
|
196
|
+
status: running ? "running" : "complete"
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
return hosts;
|
|
200
|
+
}
|
|
201
|
+
//#endregion
|
|
202
|
+
export { collectSubgraphHostNamespaces, getHistoryPage, mapSubagentNamespaces, resolveSubagentNamespaces };
|
|
203
|
+
|
|
204
|
+
//# sourceMappingURL=namespace-from-history.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"namespace-from-history.js","names":[],"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 } 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 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 = \"messages\"\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 ?? \"messages\";\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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"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const require_message_coercion = require("../message-coercion.cjs");
|
|
1
2
|
const require_store = require("../store.cjs");
|
|
2
3
|
const require_namespace = require("../namespace.cjs");
|
|
3
4
|
//#region src/stream/discovery/subagents.ts
|
|
@@ -14,6 +15,60 @@ var SubagentDiscovery = class {
|
|
|
14
15
|
if (event.method === "tools") this.#onToolEvent(event);
|
|
15
16
|
else if (event.method === "values") this.#onValuesEvent(event);
|
|
16
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Seed discovery from a checkpoint's root `messages` array (as
|
|
20
|
+
* returned by `client.threads.getState().values.messages`) so deep
|
|
21
|
+
* agent cards render on thread refresh without waiting for SSE
|
|
22
|
+
* replay.
|
|
23
|
+
*
|
|
24
|
+
* Drives the existing root `values` path via a synthetic event so it
|
|
25
|
+
* reuses task discovery + completion marking with no new parsing
|
|
26
|
+
* logic. Root namespace `[]` keeps namespaces at the default
|
|
27
|
+
* `tools:<toolCallId>`; the always-on root pump (and {@link
|
|
28
|
+
* applyExecutionNamespace}) promote them to the execution namespace
|
|
29
|
+
* later. Idempotent by construction: re-driving root values for
|
|
30
|
+
* already-discovered tasks is a no-op (the FIFO `taskInput` queue is
|
|
31
|
+
* only populated on first discovery), so no `snapshot.size` guard is
|
|
32
|
+
* needed.
|
|
33
|
+
*/
|
|
34
|
+
seedFromCheckpointMessages(messages) {
|
|
35
|
+
if (!Array.isArray(messages) || messages.length === 0) return;
|
|
36
|
+
this.push({
|
|
37
|
+
type: "event",
|
|
38
|
+
method: "values",
|
|
39
|
+
params: {
|
|
40
|
+
namespace: [],
|
|
41
|
+
timestamp: Date.now(),
|
|
42
|
+
data: { messages }
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Promote a discovered subagent to its execution namespace, derived
|
|
48
|
+
* from checkpoint history (see `namespace-from-history`).
|
|
49
|
+
*
|
|
50
|
+
* Routes through the same guarded promotion machinery the live SSE
|
|
51
|
+
* replay uses ({@link #recordTaskNamespaceCandidate} + the
|
|
52
|
+
* `#observedOwnNamespaces` no-demote rule) so a getHistory-derived
|
|
53
|
+
* namespace and an SSE-derived one cannot disagree. A no-op when the
|
|
54
|
+
* subagent is unknown or already sits on the target namespace.
|
|
55
|
+
*
|
|
56
|
+
* @param toolCallId - Parent `task` tool-call id (the subagent's discovery key).
|
|
57
|
+
* @param executionSegment - Single execution namespace segment, e.g. `tools:<uuid>`.
|
|
58
|
+
*/
|
|
59
|
+
applyExecutionNamespace(toolCallId, executionSegment) {
|
|
60
|
+
if (typeof executionSegment !== "string" || executionSegment.length === 0) return;
|
|
61
|
+
const entry = this.#map.get(toolCallId);
|
|
62
|
+
if (entry == null) return;
|
|
63
|
+
const namespace = [executionSegment];
|
|
64
|
+
const namespaceKeyed = require_namespace.namespaceKey(namespace);
|
|
65
|
+
const ownNamespaceKey = `tools:${toolCallId}`;
|
|
66
|
+
this.#recordTaskNamespaceCandidate(toolCallId, namespace);
|
|
67
|
+
if (namespaceKeyed !== ownNamespaceKey && this.#observedOwnNamespaces.has(toolCallId)) return;
|
|
68
|
+
if (require_namespace.namespaceKey(entry.namespace) === namespaceKeyed) return;
|
|
69
|
+
entry.namespace = namespace;
|
|
70
|
+
this.#commit();
|
|
71
|
+
}
|
|
17
72
|
/** Current snapshot map. */
|
|
18
73
|
get snapshot() {
|
|
19
74
|
return this.store.getSnapshot();
|
|
@@ -229,7 +284,7 @@ function parseTaskInput(raw) {
|
|
|
229
284
|
}
|
|
230
285
|
function getTaskToolCalls(message) {
|
|
231
286
|
if (message == null || typeof message !== "object" || Array.isArray(message)) return [];
|
|
232
|
-
const record = message;
|
|
287
|
+
const record = require_message_coercion.normalizeAIMessageToolCalls(message);
|
|
233
288
|
const toolCalls = record.tool_calls ?? record.kwargs?.tool_calls ?? record.lc_kwargs?.tool_calls;
|
|
234
289
|
if (!Array.isArray(toolCalls)) return [];
|
|
235
290
|
const result = [];
|