@illuma-ai/agents 1.1.25 → 1.3.0
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/cjs/agents/AgentContext.cjs +20 -3
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/common/spawnPath.cjs +104 -0
- package/dist/cjs/common/spawnPath.cjs.map +1 -0
- package/dist/cjs/graphs/Graph.cjs +87 -31
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/HandoffRegistry.cjs +143 -0
- package/dist/cjs/graphs/HandoffRegistry.cjs.map +1 -0
- package/dist/cjs/graphs/MultiAgentGraph.cjs +587 -184
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/graphs/phases/flushLoop.cjs +214 -0
- package/dist/cjs/graphs/phases/flushLoop.cjs.map +1 -0
- package/dist/cjs/graphs/phases/memoryFlushPhase.cjs +102 -0
- package/dist/cjs/graphs/phases/memoryFlushPhase.cjs.map +1 -0
- package/dist/cjs/llm/bedrock/index.cjs +4 -3
- package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +115 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/memory/citations.cjs +69 -0
- package/dist/cjs/memory/citations.cjs.map +1 -0
- package/dist/cjs/memory/compositeBackend.cjs +60 -0
- package/dist/cjs/memory/compositeBackend.cjs.map +1 -0
- package/dist/cjs/memory/constants.cjs +232 -0
- package/dist/cjs/memory/constants.cjs.map +1 -0
- package/dist/cjs/memory/embeddings.cjs +151 -0
- package/dist/cjs/memory/embeddings.cjs.map +1 -0
- package/dist/cjs/memory/factory.cjs +95 -0
- package/dist/cjs/memory/factory.cjs.map +1 -0
- package/dist/cjs/memory/migrate.cjs +81 -0
- package/dist/cjs/memory/migrate.cjs.map +1 -0
- package/dist/cjs/memory/mmr.cjs +138 -0
- package/dist/cjs/memory/mmr.cjs.map +1 -0
- package/dist/cjs/memory/paths.cjs +217 -0
- package/dist/cjs/memory/paths.cjs.map +1 -0
- package/dist/cjs/memory/pgvectorStore.cjs +225 -0
- package/dist/cjs/memory/pgvectorStore.cjs.map +1 -0
- package/dist/cjs/memory/recallTracking.cjs +98 -0
- package/dist/cjs/memory/recallTracking.cjs.map +1 -0
- package/dist/cjs/memory/schema.sql +51 -0
- package/dist/cjs/memory/temporalDecay.cjs +118 -0
- package/dist/cjs/memory/temporalDecay.cjs.map +1 -0
- package/dist/cjs/nodes/ApprovalGateNode.cjs +1 -1
- package/dist/cjs/nodes/ApprovalGateNode.cjs.map +1 -1
- package/dist/cjs/prompts/memoryFlushPrompt.cjs +49 -0
- package/dist/cjs/prompts/memoryFlushPrompt.cjs.map +1 -0
- package/dist/cjs/run.cjs +16 -3
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/stream.cjs +4 -4
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/AskUser.cjs +6 -1
- package/dist/cjs/tools/AskUser.cjs.map +1 -1
- package/dist/cjs/tools/BrowserTools.cjs +1 -1
- package/dist/cjs/tools/BrowserTools.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +127 -10
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/approval/constants.cjs +2 -2
- package/dist/cjs/tools/approval/constants.cjs.map +1 -1
- package/dist/cjs/tools/memory/index.cjs +58 -0
- package/dist/cjs/tools/memory/index.cjs.map +1 -0
- package/dist/cjs/tools/memory/memoryAppendTool.cjs +69 -0
- package/dist/cjs/tools/memory/memoryAppendTool.cjs.map +1 -0
- package/dist/cjs/tools/memory/memoryGetTool.cjs +49 -0
- package/dist/cjs/tools/memory/memoryGetTool.cjs.map +1 -0
- package/dist/cjs/tools/memory/memorySearchTool.cjs +65 -0
- package/dist/cjs/tools/memory/memorySearchTool.cjs.map +1 -0
- package/dist/cjs/tools/memory/shared.cjs +106 -0
- package/dist/cjs/tools/memory/shared.cjs.map +1 -0
- package/dist/cjs/types/graph.cjs.map +1 -1
- package/dist/cjs/utils/childAgentContext.cjs +242 -0
- package/dist/cjs/utils/childAgentContext.cjs.map +1 -0
- package/dist/cjs/utils/events.cjs +36 -4
- package/dist/cjs/utils/events.cjs.map +1 -1
- package/dist/cjs/utils/finishReasons.cjs +44 -0
- package/dist/cjs/utils/finishReasons.cjs.map +1 -0
- package/dist/cjs/utils/llm.cjs.map +1 -1
- package/dist/cjs/utils/logging.cjs +34 -0
- package/dist/cjs/utils/logging.cjs.map +1 -0
- package/dist/cjs/utils/toolCallNormalization.cjs +250 -0
- package/dist/cjs/utils/toolCallNormalization.cjs.map +1 -0
- package/dist/esm/agents/AgentContext.mjs +20 -3
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/common/spawnPath.mjs +95 -0
- package/dist/esm/common/spawnPath.mjs.map +1 -0
- package/dist/esm/graphs/Graph.mjs +87 -31
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/HandoffRegistry.mjs +141 -0
- package/dist/esm/graphs/HandoffRegistry.mjs.map +1 -0
- package/dist/esm/graphs/MultiAgentGraph.mjs +587 -184
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/graphs/phases/flushLoop.mjs +209 -0
- package/dist/esm/graphs/phases/flushLoop.mjs.map +1 -0
- package/dist/esm/graphs/phases/memoryFlushPhase.mjs +99 -0
- package/dist/esm/graphs/phases/memoryFlushPhase.mjs.map +1 -0
- package/dist/esm/llm/bedrock/index.mjs +4 -3
- package/dist/esm/llm/bedrock/index.mjs.map +1 -1
- package/dist/esm/main.mjs +21 -0
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/memory/citations.mjs +64 -0
- package/dist/esm/memory/citations.mjs.map +1 -0
- package/dist/esm/memory/compositeBackend.mjs +58 -0
- package/dist/esm/memory/compositeBackend.mjs.map +1 -0
- package/dist/esm/memory/constants.mjs +198 -0
- package/dist/esm/memory/constants.mjs.map +1 -0
- package/dist/esm/memory/embeddings.mjs +148 -0
- package/dist/esm/memory/embeddings.mjs.map +1 -0
- package/dist/esm/memory/factory.mjs +93 -0
- package/dist/esm/memory/factory.mjs.map +1 -0
- package/dist/esm/memory/migrate.mjs +78 -0
- package/dist/esm/memory/migrate.mjs.map +1 -0
- package/dist/esm/memory/mmr.mjs +130 -0
- package/dist/esm/memory/mmr.mjs.map +1 -0
- package/dist/esm/memory/paths.mjs +207 -0
- package/dist/esm/memory/paths.mjs.map +1 -0
- package/dist/esm/memory/pgvectorStore.mjs +223 -0
- package/dist/esm/memory/pgvectorStore.mjs.map +1 -0
- package/dist/esm/memory/recallTracking.mjs +94 -0
- package/dist/esm/memory/recallTracking.mjs.map +1 -0
- package/dist/esm/memory/schema.sql +51 -0
- package/dist/esm/memory/temporalDecay.mjs +110 -0
- package/dist/esm/memory/temporalDecay.mjs.map +1 -0
- package/dist/esm/nodes/ApprovalGateNode.mjs +1 -1
- package/dist/esm/nodes/ApprovalGateNode.mjs.map +1 -1
- package/dist/esm/prompts/memoryFlushPrompt.mjs +44 -0
- package/dist/esm/prompts/memoryFlushPrompt.mjs.map +1 -0
- package/dist/esm/run.mjs +16 -3
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/stream.mjs +4 -4
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/AskUser.mjs +6 -1
- package/dist/esm/tools/AskUser.mjs.map +1 -1
- package/dist/esm/tools/BrowserTools.mjs +1 -1
- package/dist/esm/tools/BrowserTools.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +128 -11
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/approval/constants.mjs +2 -2
- package/dist/esm/tools/approval/constants.mjs.map +1 -1
- package/dist/esm/tools/memory/index.mjs +46 -0
- package/dist/esm/tools/memory/index.mjs.map +1 -0
- package/dist/esm/tools/memory/memoryAppendTool.mjs +67 -0
- package/dist/esm/tools/memory/memoryAppendTool.mjs.map +1 -0
- package/dist/esm/tools/memory/memoryGetTool.mjs +47 -0
- package/dist/esm/tools/memory/memoryGetTool.mjs.map +1 -0
- package/dist/esm/tools/memory/memorySearchTool.mjs +63 -0
- package/dist/esm/tools/memory/memorySearchTool.mjs.map +1 -0
- package/dist/esm/tools/memory/shared.mjs +98 -0
- package/dist/esm/tools/memory/shared.mjs.map +1 -0
- package/dist/esm/types/graph.mjs.map +1 -1
- package/dist/esm/utils/childAgentContext.mjs +237 -0
- package/dist/esm/utils/childAgentContext.mjs.map +1 -0
- package/dist/esm/utils/events.mjs +36 -5
- package/dist/esm/utils/events.mjs.map +1 -1
- package/dist/esm/utils/finishReasons.mjs +41 -0
- package/dist/esm/utils/finishReasons.mjs.map +1 -0
- package/dist/esm/utils/llm.mjs.map +1 -1
- package/dist/esm/utils/logging.mjs +31 -0
- package/dist/esm/utils/logging.mjs.map +1 -0
- package/dist/esm/utils/toolCallNormalization.mjs +247 -0
- package/dist/esm/utils/toolCallNormalization.mjs.map +1 -0
- package/dist/types/common/index.d.ts +1 -0
- package/dist/types/common/spawnPath.d.ts +59 -0
- package/dist/types/graphs/HandoffRegistry.d.ts +97 -0
- package/dist/types/graphs/MultiAgentGraph.d.ts +58 -18
- package/dist/types/graphs/index.d.ts +1 -0
- package/dist/types/graphs/phases/flushLoop.d.ts +106 -0
- package/dist/types/graphs/phases/memoryFlushPhase.d.ts +100 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/memory/__tests__/mockBackend.d.ts +40 -0
- package/dist/types/memory/citations.d.ts +39 -0
- package/dist/types/memory/compositeBackend.d.ts +30 -0
- package/dist/types/memory/constants.d.ts +121 -0
- package/dist/types/memory/embeddings.d.ts +15 -0
- package/dist/types/memory/factory.d.ts +23 -0
- package/dist/types/memory/index.d.ts +21 -0
- package/dist/types/memory/migrate.d.ts +14 -0
- package/dist/types/memory/mmr.d.ts +50 -0
- package/dist/types/memory/paths.d.ts +107 -0
- package/dist/types/memory/pgvectorStore.d.ts +56 -0
- package/dist/types/memory/recallTracking.d.ts +30 -0
- package/dist/types/memory/temporalDecay.d.ts +53 -0
- package/dist/types/memory/types.d.ts +182 -0
- package/dist/types/prompts/memoryFlushPrompt.d.ts +54 -0
- package/dist/types/run.d.ts +1 -0
- package/dist/types/tools/AskUser.d.ts +1 -1
- package/dist/types/tools/BrowserTools.d.ts +2 -2
- package/dist/types/tools/approval/constants.d.ts +2 -2
- package/dist/types/tools/memory/index.d.ts +39 -0
- package/dist/types/tools/memory/memoryAppendTool.d.ts +27 -0
- package/dist/types/tools/memory/memoryGetTool.d.ts +22 -0
- package/dist/types/tools/memory/memorySearchTool.d.ts +22 -0
- package/dist/types/tools/memory/shared.d.ts +106 -0
- package/dist/types/types/graph.d.ts +16 -3
- package/dist/types/utils/childAgentContext.d.ts +99 -0
- package/dist/types/utils/events.d.ts +21 -0
- package/dist/types/utils/finishReasons.d.ts +32 -0
- package/dist/types/utils/logging.d.ts +2 -0
- package/dist/types/utils/toolCallNormalization.d.ts +44 -0
- package/package.json +6 -4
- package/src/agents/AgentContext.ts +26 -3
- package/src/common/__tests__/enum.test.ts +4 -2
- package/src/common/__tests__/spawnPath.test.ts +110 -0
- package/src/common/index.ts +1 -0
- package/src/common/spawnPath.ts +101 -0
- package/src/graphs/Graph.ts +94 -43
- package/src/graphs/HandoffRegistry.ts +199 -0
- package/src/graphs/MultiAgentGraph.ts +694 -226
- package/src/graphs/__tests__/HandoffRegistry.test.ts +410 -0
- package/src/graphs/__tests__/multi-agent-delegate.test.ts +61 -16
- package/src/graphs/__tests__/multi-agent-edges.test.ts +4 -2
- package/src/graphs/__tests__/multi-agent-nested-subgraph.test.ts +221 -0
- package/src/graphs/__tests__/structured-output.integration.test.ts +212 -118
- package/src/graphs/contextManagement.e2e.test.ts +1 -1
- package/src/graphs/index.ts +1 -0
- package/src/graphs/phases/__tests__/flushLoop.test.ts +264 -0
- package/src/graphs/phases/__tests__/memoryFlushPhase.test.ts +37 -0
- package/src/graphs/phases/__tests__/runMemoryFlush.test.ts +150 -0
- package/src/graphs/phases/flushLoop.ts +303 -0
- package/src/graphs/phases/memoryFlushPhase.ts +209 -0
- package/src/index.ts +30 -1
- package/src/llm/bedrock/index.ts +4 -5
- package/src/memory/__tests__/citations.test.ts +61 -0
- package/src/memory/__tests__/compositeBackend.test.ts +79 -0
- package/src/memory/__tests__/isolation.test.ts +206 -0
- package/src/memory/__tests__/mmr.test.ts +148 -0
- package/src/memory/__tests__/mockBackend.ts +161 -0
- package/src/memory/__tests__/paths.test.ts +168 -0
- package/src/memory/__tests__/recallTracking.test.ts +96 -0
- package/src/memory/__tests__/temporalDecay.test.ts +151 -0
- package/src/memory/citations.ts +80 -0
- package/src/memory/compositeBackend.ts +99 -0
- package/src/memory/constants.ts +229 -0
- package/src/memory/embeddings.ts +188 -0
- package/src/memory/factory.ts +111 -0
- package/src/memory/index.ts +46 -0
- package/src/memory/migrate.ts +116 -0
- package/src/memory/mmr.ts +161 -0
- package/src/memory/paths.ts +258 -0
- package/src/memory/pgvectorStore.ts +324 -0
- package/src/memory/recallTracking.ts +127 -0
- package/src/memory/schema.sql +51 -0
- package/src/memory/temporalDecay.ts +134 -0
- package/src/memory/types.ts +185 -0
- package/src/nodes/ApprovalGateNode.ts +4 -10
- package/src/nodes/__tests__/ApprovalGateNode.test.ts +11 -20
- package/src/prompts/memoryFlushPrompt.ts +78 -0
- package/src/run.ts +17 -6
- package/src/scripts/test-bedrock-handoff-autonomous.ts +56 -20
- package/src/specs/agent-handoffs-bedrock.integration.test.ts +8 -5
- package/src/specs/agent-handoffs.test.ts +8 -2
- package/src/stream.ts +4 -6
- package/src/tools/AskUser.ts +7 -2
- package/src/tools/BrowserTools.ts +3 -5
- package/src/tools/ToolNode.ts +150 -13
- package/src/tools/__tests__/ToolApproval.test.ts +22 -9
- package/src/tools/approval/__tests__/constants.test.ts +4 -4
- package/src/tools/approval/constants.ts +2 -2
- package/src/tools/memory/__tests__/memoryTools.test.ts +205 -0
- package/src/tools/memory/index.ts +96 -0
- package/src/tools/memory/memoryAppendTool.ts +101 -0
- package/src/tools/memory/memoryGetTool.ts +53 -0
- package/src/tools/memory/memorySearchTool.ts +80 -0
- package/src/tools/memory/shared.ts +169 -0
- package/src/tools/search/search.test.ts +6 -1
- package/src/types/graph.ts +16 -3
- package/src/utils/__tests__/childAgentContext.test.ts +217 -0
- package/src/utils/__tests__/finishReasons.test.ts +55 -0
- package/src/utils/__tests__/toolCallNormalization.test.ts +181 -0
- package/src/utils/childAgentContext.ts +259 -0
- package/src/utils/events.ts +37 -4
- package/src/utils/finishReasons.ts +40 -0
- package/src/utils/llm.ts +0 -1
- package/src/utils/logging.ts +45 -8
- package/src/utils/toolCallNormalization.ts +271 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* spawnPath — hierarchical invocation identity for nested multi-agent orchestration.
|
|
3
|
+
*
|
|
4
|
+
* A spawnPath is a slash-separated chain of spawnKeys from the root of the current
|
|
5
|
+
* agent invocation to the current spawn. The root agent has an empty spawnPath;
|
|
6
|
+
* each handoff/transfer/sequence that spawns a new subgraph appends a new spawnKey.
|
|
7
|
+
*
|
|
8
|
+
* Examples:
|
|
9
|
+
* "" → primary agent (no spawn)
|
|
10
|
+
* "call_abc" → first-level handoff child
|
|
11
|
+
* "call_abc/call_def" → grandchild (depth 2)
|
|
12
|
+
* "call_abc/call_def/call_ghi" → depth 3
|
|
13
|
+
*
|
|
14
|
+
* These utilities are the single source of truth for path manipulation across:
|
|
15
|
+
* - @illuma-ai/agents (MultiAgentGraph, HandoffRegistry, callbacks)
|
|
16
|
+
* - host api (initialize.js, callbacks.js, ExecutionTrace writes)
|
|
17
|
+
* - host client (subagent store, sidebar rendering)
|
|
18
|
+
*
|
|
19
|
+
* See docs/multi-agent-nesting-architecture.md for the full design.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** Separator between spawnKeys in a spawnPath. Chosen so that the path looks
|
|
23
|
+
* like a filesystem/URL path, which makes it easy to read in logs and traces. */
|
|
24
|
+
export const SPAWN_PATH_SEP = '/';
|
|
25
|
+
|
|
26
|
+
/** Hard cap on nested multi-agent invocations. Prevents runaway recursion.
|
|
27
|
+
* Can be overridden at the host api layer via MULTI_AGENT_MAX_NESTING_DEPTH. */
|
|
28
|
+
export const MAX_NESTING_DEPTH = 5;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Append a spawnKey to a parent spawnPath.
|
|
32
|
+
*
|
|
33
|
+
* @param parent - Parent spawnPath (may be undefined/null/empty for root)
|
|
34
|
+
* @param key - spawnKey to append
|
|
35
|
+
* @returns New spawnPath string
|
|
36
|
+
*/
|
|
37
|
+
export function buildSpawnPath(
|
|
38
|
+
parent: string | undefined | null,
|
|
39
|
+
key: string
|
|
40
|
+
): string {
|
|
41
|
+
if (!key) {
|
|
42
|
+
throw new Error('[spawnPath] buildSpawnPath called with empty key');
|
|
43
|
+
}
|
|
44
|
+
if (parent == null || parent === '') return key;
|
|
45
|
+
return `${parent}${SPAWN_PATH_SEP}${key}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Compute the depth of a spawnPath.
|
|
50
|
+
* Root (empty) → 0; single-segment → 1; etc.
|
|
51
|
+
*/
|
|
52
|
+
export function spawnPathDepth(path: string | undefined | null): number {
|
|
53
|
+
if (path == null || path === '') return 0;
|
|
54
|
+
return path.split(SPAWN_PATH_SEP).filter(Boolean).length;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Return the parent spawnPath, or null if the input is already root.
|
|
59
|
+
*
|
|
60
|
+
* - parentSpawnPath("a/b/c") === "a/b"
|
|
61
|
+
* - parentSpawnPath("a") === ""
|
|
62
|
+
* - parentSpawnPath("") === null
|
|
63
|
+
*/
|
|
64
|
+
export function parentSpawnPath(
|
|
65
|
+
path: string | undefined | null
|
|
66
|
+
): string | null {
|
|
67
|
+
if (path == null || path === '') return null;
|
|
68
|
+
const parts = path.split(SPAWN_PATH_SEP).filter(Boolean);
|
|
69
|
+
if (parts.length <= 1) return '';
|
|
70
|
+
return parts.slice(0, -1).join(SPAWN_PATH_SEP);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Split a spawnPath into its constituent spawnKey segments. */
|
|
74
|
+
export function spawnPathParts(path: string | undefined | null): string[] {
|
|
75
|
+
if (path == null || path === '') return [];
|
|
76
|
+
return path.split(SPAWN_PATH_SEP).filter(Boolean);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Return the last spawnKey in a spawnPath (the "current" spawn).
|
|
81
|
+
* Returns null for root.
|
|
82
|
+
*/
|
|
83
|
+
export function leafSpawnKey(path: string | undefined | null): string | null {
|
|
84
|
+
const parts = spawnPathParts(path);
|
|
85
|
+
return parts.length === 0 ? null : parts[parts.length - 1];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* True if `ancestor` is a strict ancestor of `descendant`. Root ("") is
|
|
90
|
+
* ancestor of everything except itself.
|
|
91
|
+
*/
|
|
92
|
+
export function isAncestorSpawnPath(
|
|
93
|
+
ancestor: string | undefined | null,
|
|
94
|
+
descendant: string | undefined | null
|
|
95
|
+
): boolean {
|
|
96
|
+
const a = ancestor ?? '';
|
|
97
|
+
const d = descendant ?? '';
|
|
98
|
+
if (a === d) return false;
|
|
99
|
+
if (a === '') return d !== '';
|
|
100
|
+
return d.startsWith(a + SPAWN_PATH_SEP);
|
|
101
|
+
}
|
package/src/graphs/Graph.ts
CHANGED
|
@@ -79,6 +79,9 @@ import { getChatModelClass, manualToolStreamProviders } from '@/llm/providers';
|
|
|
79
79
|
import { ToolNode as CustomToolNode, toolsCondition } from '@/tools/ToolNode';
|
|
80
80
|
import { ChatOpenAI, AzureChatOpenAI } from '@/llm/openai';
|
|
81
81
|
import { safeDispatchCustomEvent } from '@/utils/events';
|
|
82
|
+
import { mlog, mwarn } from '@/utils/logging';
|
|
83
|
+
import { normalizeMessageToolCalls } from '@/utils/toolCallNormalization';
|
|
84
|
+
import { isTruncationReason } from '@/utils/finishReasons';
|
|
82
85
|
import {
|
|
83
86
|
detectDocuments,
|
|
84
87
|
shouldInjectMultiDocHint,
|
|
@@ -1144,10 +1147,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1144
1147
|
const resolved = agentContext.resolveStructuredOutputMode();
|
|
1145
1148
|
method = resolved.method;
|
|
1146
1149
|
if (resolved.warnings.length > 0) {
|
|
1147
|
-
|
|
1148
|
-
'[Graph] Structured output mode warnings:',
|
|
1149
|
-
resolved.warnings
|
|
1150
|
-
);
|
|
1150
|
+
mwarn('[Graph] Structured output mode warnings:', resolved.warnings);
|
|
1151
1151
|
}
|
|
1152
1152
|
} else {
|
|
1153
1153
|
// Legacy fallback: use the old mode-based resolution
|
|
@@ -1172,7 +1172,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1172
1172
|
);
|
|
1173
1173
|
preparedSchema = prepared;
|
|
1174
1174
|
if (warnings.length > 0) {
|
|
1175
|
-
|
|
1175
|
+
mwarn('[Graph] Schema preparation warnings:', warnings);
|
|
1176
1176
|
}
|
|
1177
1177
|
}
|
|
1178
1178
|
|
|
@@ -1264,7 +1264,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1264
1264
|
? handleErrors
|
|
1265
1265
|
: `The response did not match the expected schema. Error: ${lastError.message}. Please try again with a valid response.`;
|
|
1266
1266
|
|
|
1267
|
-
|
|
1267
|
+
mwarn(
|
|
1268
1268
|
`[Graph] Structured output attempt ${attempts} failed: ${lastError.message}. Retrying...`
|
|
1269
1269
|
);
|
|
1270
1270
|
|
|
@@ -1467,7 +1467,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1467
1467
|
this._toolDiscoveryCache.getNewDiscoveries(messages);
|
|
1468
1468
|
if (cachedDiscoveries.length > 0) {
|
|
1469
1469
|
agentContext.markToolsAsDiscovered(cachedDiscoveries);
|
|
1470
|
-
|
|
1470
|
+
mlog(
|
|
1471
1471
|
`[Graph:ToolDiscovery] Cached ${cachedDiscoveries.length} new tools (total: ${this._toolDiscoveryCache.size})`
|
|
1472
1472
|
);
|
|
1473
1473
|
}
|
|
@@ -1497,6 +1497,14 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1497
1497
|
clientOptions: effectiveClientOptions,
|
|
1498
1498
|
});
|
|
1499
1499
|
|
|
1500
|
+
// DEBUG: Log which model and tools each agent uses during handoff
|
|
1501
|
+
mlog(
|
|
1502
|
+
`[createCallModel] Agent "${agentId}" invoking LLM | provider=${agentContext.provider} | ` +
|
|
1503
|
+
`model=${(effectiveClientOptions as Record<string, unknown>).model ?? 'default'} | ` +
|
|
1504
|
+
`toolsForBinding=${toolsForBinding?.length ?? 0} | ` +
|
|
1505
|
+
`toolNames=[${(toolsForBinding ?? []).map((t) => (t as { name?: string }).name ?? 'unknown').join(', ')}]`
|
|
1506
|
+
);
|
|
1507
|
+
|
|
1500
1508
|
if (agentContext.systemRunnable) {
|
|
1501
1509
|
model = agentContext.systemRunnable.pipe(model as Runnable);
|
|
1502
1510
|
}
|
|
@@ -1507,7 +1515,15 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1507
1515
|
if (!config.signal) {
|
|
1508
1516
|
config.signal = this.signal;
|
|
1509
1517
|
}
|
|
1510
|
-
this.config
|
|
1518
|
+
// First-writer-wins: `this.config` is used ONLY as a "has a run started"
|
|
1519
|
+
// existence flag by the dispatch* methods (they never read its value —
|
|
1520
|
+
// they read the current RunnableConfig from LangChain AsyncLocalStorage).
|
|
1521
|
+
// Unconditionally reassigning here races across concurrent child
|
|
1522
|
+
// subgraph.invoke() calls under parallel multi-agent handoffs; the last
|
|
1523
|
+
// writer wins, and any dispatch firing between writes would historically
|
|
1524
|
+
// have been tagged with the wrong child's metadata. Keeping the first
|
|
1525
|
+
// write pinned makes this a true flag, eliminating the race.
|
|
1526
|
+
this.config ??= config;
|
|
1511
1527
|
|
|
1512
1528
|
let messagesToUse = messages;
|
|
1513
1529
|
|
|
@@ -1611,7 +1627,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1611
1627
|
|
|
1612
1628
|
if (oldMessages.length > 0) {
|
|
1613
1629
|
this._summaryInFlight = true;
|
|
1614
|
-
|
|
1630
|
+
mlog(
|
|
1615
1631
|
`[Graph:ProactiveSummary] Context at ${utilization.toFixed(1)}% (threshold ${threshold}%) — summarizing ${oldMessages.length} older msgs in background`
|
|
1616
1632
|
);
|
|
1617
1633
|
|
|
@@ -1620,7 +1636,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1620
1636
|
.then((updated) => {
|
|
1621
1637
|
if (updated != null && updated !== '') {
|
|
1622
1638
|
this._cachedRunSummary = updated;
|
|
1623
|
-
|
|
1639
|
+
mlog(
|
|
1624
1640
|
`[Graph:ProactiveSummary] Background summary ready (len=${updated.length})`
|
|
1625
1641
|
);
|
|
1626
1642
|
}
|
|
@@ -1830,7 +1846,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1830
1846
|
}
|
|
1831
1847
|
agentContext.indexTokenCountMap = viewTokenMap;
|
|
1832
1848
|
|
|
1833
|
-
|
|
1849
|
+
mlog(
|
|
1834
1850
|
`[Graph:Compaction] ${messages.length}→${viewParts.length} msgs | ` +
|
|
1835
1851
|
`compacted=${compactedMessages.length} window=${recentMessages.length} | ` +
|
|
1836
1852
|
`summary=${summarySource} | budget=${usedTokens}/${recentBudget}` +
|
|
@@ -1854,7 +1870,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1854
1870
|
if (shouldSummarize) {
|
|
1855
1871
|
if (this._summaryInFlight) {
|
|
1856
1872
|
this._pendingMessagesToRefine.push(...compactedMessages);
|
|
1857
|
-
|
|
1873
|
+
mlog(
|
|
1858
1874
|
`[Graph:Compaction] Summary in-flight, queued ${compactedMessages.length} msgs (pending=${this._pendingMessagesToRefine.length})`
|
|
1859
1875
|
);
|
|
1860
1876
|
} else {
|
|
@@ -1907,7 +1923,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1907
1923
|
deduplicateSystemMessages(messagesToUse);
|
|
1908
1924
|
if (removedCount > 0) {
|
|
1909
1925
|
messagesToUse = dedupedMessages;
|
|
1910
|
-
|
|
1926
|
+
mlog(
|
|
1911
1927
|
`[Graph:Dedup] Removed ${removedCount} duplicate system message(s)`
|
|
1912
1928
|
);
|
|
1913
1929
|
}
|
|
@@ -2031,7 +2047,6 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
2031
2047
|
);
|
|
2032
2048
|
}
|
|
2033
2049
|
|
|
2034
|
-
|
|
2035
2050
|
// Get model info for analytics
|
|
2036
2051
|
const bedrockOpts = agentContext.clientOptions as
|
|
2037
2052
|
| t.BedrockAnthropicClientOptions
|
|
@@ -2145,11 +2160,11 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
2145
2160
|
|
|
2146
2161
|
// Log when we detect the error
|
|
2147
2162
|
if (isInputTooLongError) {
|
|
2148
|
-
|
|
2163
|
+
mwarn(
|
|
2149
2164
|
'[Graph] Detected input too long error:',
|
|
2150
2165
|
errorMessage.substring(0, 200)
|
|
2151
2166
|
);
|
|
2152
|
-
|
|
2167
|
+
mwarn('[Graph] Checking emergency pruning conditions:', {
|
|
2153
2168
|
hasPruneMessages: !!agentContext.pruneMessages,
|
|
2154
2169
|
hasTokenCounter: !!agentContext.tokenCounter,
|
|
2155
2170
|
maxContextTokens: agentContext.maxContextTokens,
|
|
@@ -2174,7 +2189,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
2174
2189
|
const reducedMaxTokens = Math.floor(
|
|
2175
2190
|
agentContext.maxContextTokens! * reductionFactor
|
|
2176
2191
|
);
|
|
2177
|
-
|
|
2192
|
+
mwarn(
|
|
2178
2193
|
`[Graph] Input too long. Retrying with ${reductionFactor * 100}% context (${reducedMaxTokens} tokens)...`
|
|
2179
2194
|
);
|
|
2180
2195
|
|
|
@@ -2182,7 +2197,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
2182
2197
|
// This is needed when messages were dynamically added without updating the token map
|
|
2183
2198
|
let tokenMapForPruning = agentContext.indexTokenCountMap;
|
|
2184
2199
|
if (Object.keys(tokenMapForPruning).length < messages.length) {
|
|
2185
|
-
|
|
2200
|
+
mwarn(
|
|
2186
2201
|
'[Graph] Building fresh token count map for emergency pruning...'
|
|
2187
2202
|
);
|
|
2188
2203
|
tokenMapForPruning = {};
|
|
@@ -2207,7 +2222,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
2207
2222
|
|
|
2208
2223
|
// Skip if we can't fit any messages
|
|
2209
2224
|
if (reducedMessages.length === 0) {
|
|
2210
|
-
|
|
2225
|
+
mwarn(
|
|
2211
2226
|
`[Graph] Cannot fit any messages at ${reductionFactor * 100}% reduction, trying next level...`
|
|
2212
2227
|
);
|
|
2213
2228
|
continue;
|
|
@@ -2290,7 +2305,7 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
|
|
|
2290
2305
|
retryErrorMsg.includes('validationexception');
|
|
2291
2306
|
|
|
2292
2307
|
if (stillTooLong && reductionFactor > 0.1) {
|
|
2293
|
-
|
|
2308
|
+
mwarn(
|
|
2294
2309
|
`[Graph] Still too long at ${reductionFactor * 100}%, trying more aggressive pruning...`
|
|
2295
2310
|
);
|
|
2296
2311
|
} else {
|
|
@@ -2362,6 +2377,27 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
|
|
|
2362
2377
|
* handled everything — both paths become no-ops.
|
|
2363
2378
|
*/
|
|
2364
2379
|
const responseMessage = result.messages?.[0];
|
|
2380
|
+
|
|
2381
|
+
// Tool-call name normalization — catches LLM output that names tools
|
|
2382
|
+
// with wrong delimiters (outlook/operations), prefixes
|
|
2383
|
+
// (functions.outlook_operations), case drift, counter suffixes, or
|
|
2384
|
+
// empty names recoverable from the tool_call id. Mutates in place so
|
|
2385
|
+
// the downstream ToolNode dispatch sees the corrected names.
|
|
2386
|
+
if (responseMessage && agentContext.toolMap) {
|
|
2387
|
+
const allowedNames = new Set(Object.keys(agentContext.toolMap));
|
|
2388
|
+
if (allowedNames.size > 0) {
|
|
2389
|
+
const rewrote = normalizeMessageToolCalls(
|
|
2390
|
+
responseMessage,
|
|
2391
|
+
allowedNames
|
|
2392
|
+
);
|
|
2393
|
+
if (rewrote) {
|
|
2394
|
+
mlog(
|
|
2395
|
+
`[Graph] normalized tool_call names on agent "${agentId}" response`
|
|
2396
|
+
);
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2365
2401
|
const toolCalls = (responseMessage as AIMessageChunk | undefined)
|
|
2366
2402
|
?.tool_calls;
|
|
2367
2403
|
const hasToolCalls = Array.isArray(toolCalls) && toolCalls.length > 0;
|
|
@@ -2477,12 +2513,22 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
|
|
|
2477
2513
|
const messageStop = meta.messageStop as
|
|
2478
2514
|
| Record<string, unknown>
|
|
2479
2515
|
| undefined;
|
|
2480
|
-
|
|
2516
|
+
const nextReason =
|
|
2481
2517
|
(meta.finish_reason as string | undefined) ?? // OpenAI/Azure
|
|
2482
2518
|
(meta.stop_reason as string | undefined) ?? // Anthropic direct API
|
|
2483
2519
|
(meta.stopReason as string | undefined) ?? // Bedrock invoke (non-streaming)
|
|
2484
2520
|
(messageStop?.stopReason as string | undefined) ?? // Bedrock streaming
|
|
2485
2521
|
(meta.finishReason as string | undefined); // VertexAI/Google
|
|
2522
|
+
|
|
2523
|
+
// Sticky on truncation: a single Graph instance is reused across
|
|
2524
|
+
// every scoped-subgraph inner node invocation (see MultiAgentGraph
|
|
2525
|
+
// buildScopedSubgraph). If an earlier inner node hit max_tokens
|
|
2526
|
+
// but a later inner node finished cleanly, the host's continuation layer
|
|
2527
|
+
// would miss the truncation signal unless we preserve it. Keep the
|
|
2528
|
+
// truncation reason pinned so the outer caller can retry.
|
|
2529
|
+
if (!isTruncationReason(this.lastFinishReason)) {
|
|
2530
|
+
this.lastFinishReason = nextReason;
|
|
2531
|
+
}
|
|
2486
2532
|
}
|
|
2487
2533
|
|
|
2488
2534
|
this.cleanupSignalListener();
|
|
@@ -2545,7 +2591,7 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
|
|
|
2545
2591
|
'[Graph] Deferred structured output failed after successful tool use:',
|
|
2546
2592
|
structuredError
|
|
2547
2593
|
);
|
|
2548
|
-
|
|
2594
|
+
mwarn(
|
|
2549
2595
|
'[Graph] Falling back to unstructured response from tool-use phase'
|
|
2550
2596
|
);
|
|
2551
2597
|
return result;
|
|
@@ -2570,7 +2616,10 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
|
|
|
2570
2616
|
state: t.BaseGraphState,
|
|
2571
2617
|
config?: RunnableConfig
|
|
2572
2618
|
): string => {
|
|
2573
|
-
this.config
|
|
2619
|
+
// First-writer-wins — see note in createCallModel. `this.config` is an
|
|
2620
|
+
// existence flag only; assigning unconditionally would race under
|
|
2621
|
+
// parallel child subgraph.invoke().
|
|
2622
|
+
this.config ??= config;
|
|
2574
2623
|
return toolsCondition(state, toolNode, this.invokedToolIds);
|
|
2575
2624
|
};
|
|
2576
2625
|
|
|
@@ -2615,10 +2664,16 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
|
|
|
2615
2664
|
default: () => [],
|
|
2616
2665
|
}),
|
|
2617
2666
|
});
|
|
2667
|
+
// Pass compileOptions (including the HITL checkpointer) to the OUTER
|
|
2668
|
+
// workflow — not just the inner agent subgraph. hasInterrupts() calls
|
|
2669
|
+
// getState() on the outer compiled graph; without a checkpointer here,
|
|
2670
|
+
// getState reports zero tasks and the HITL interrupt/resume loop breaks
|
|
2671
|
+
// out immediately even though interrupt() fired correctly inside the
|
|
2672
|
+
// agent subgraph.
|
|
2618
2673
|
const workflow = new StateGraph(StateAnnotation)
|
|
2619
2674
|
.addNode(this.defaultAgentId, agentNode, { ends: [END] })
|
|
2620
2675
|
.addEdge(START, this.defaultAgentId)
|
|
2621
|
-
.compile();
|
|
2676
|
+
.compile(this.compileOptions as unknown as never);
|
|
2622
2677
|
|
|
2623
2678
|
return workflow;
|
|
2624
2679
|
}
|
|
@@ -2701,7 +2756,7 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
|
|
|
2701
2756
|
}
|
|
2702
2757
|
} catch (_e) {
|
|
2703
2758
|
/** If we can't get agent context, that's okay - agentId remains undefined */
|
|
2704
|
-
|
|
2759
|
+
mlog(
|
|
2705
2760
|
`[dispatchRunStep] Could not resolve agentId from metadata.langgraph_node="${(metadata as Record<string, unknown>).langgraph_node}": ${(_e as Error).message}`
|
|
2706
2761
|
);
|
|
2707
2762
|
}
|
|
@@ -2709,11 +2764,11 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
|
|
|
2709
2764
|
|
|
2710
2765
|
this.contentData.push(runStep);
|
|
2711
2766
|
this.contentIndexMap.set(stepId, runStep.index);
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
);
|
|
2767
|
+
// Pass undefined so safeDispatchCustomEvent resolves the runnable config
|
|
2768
|
+
// from LangChain's AsyncLocalStorage. Using the shared `this.config` would
|
|
2769
|
+
// race across concurrent child subgraph.invoke calls under parallel
|
|
2770
|
+
// multi-agent handoffs and tag events with the wrong child's spawnKey.
|
|
2771
|
+
await safeDispatchCustomEvent(GraphEvents.ON_RUN_STEP, runStep);
|
|
2717
2772
|
return stepId;
|
|
2718
2773
|
}
|
|
2719
2774
|
|
|
@@ -2854,7 +2909,7 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
|
|
|
2854
2909
|
}
|
|
2855
2910
|
|
|
2856
2911
|
if (!data.id) {
|
|
2857
|
-
|
|
2912
|
+
mwarn('No Tool ID provided for Tool Error');
|
|
2858
2913
|
return;
|
|
2859
2914
|
}
|
|
2860
2915
|
|
|
@@ -2919,11 +2974,10 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
|
|
|
2919
2974
|
id,
|
|
2920
2975
|
delta,
|
|
2921
2976
|
};
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
);
|
|
2977
|
+
// See dispatchRunStep note: do not pass `this.config`. The implicit
|
|
2978
|
+
// AsyncLocalStorage config is the correct per-async-branch source under
|
|
2979
|
+
// parallel handoffs.
|
|
2980
|
+
await safeDispatchCustomEvent(GraphEvents.ON_RUN_STEP_DELTA, runStepDelta);
|
|
2927
2981
|
}
|
|
2928
2982
|
|
|
2929
2983
|
async dispatchMessageDelta(id: string, delta: t.MessageDelta): Promise<void> {
|
|
@@ -2934,11 +2988,8 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
|
|
|
2934
2988
|
id,
|
|
2935
2989
|
delta,
|
|
2936
2990
|
};
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
messageDelta,
|
|
2940
|
-
this.config
|
|
2941
|
-
);
|
|
2991
|
+
// See dispatchRunStep note.
|
|
2992
|
+
await safeDispatchCustomEvent(GraphEvents.ON_MESSAGE_DELTA, messageDelta);
|
|
2942
2993
|
}
|
|
2943
2994
|
|
|
2944
2995
|
dispatchReasoningDelta = async (
|
|
@@ -2952,10 +3003,10 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
|
|
|
2952
3003
|
id: stepId,
|
|
2953
3004
|
delta,
|
|
2954
3005
|
};
|
|
3006
|
+
// See dispatchRunStep note.
|
|
2955
3007
|
await safeDispatchCustomEvent(
|
|
2956
3008
|
GraphEvents.ON_REASONING_DELTA,
|
|
2957
|
-
reasoningDelta
|
|
2958
|
-
this.config
|
|
3009
|
+
reasoningDelta
|
|
2959
3010
|
);
|
|
2960
3011
|
};
|
|
2961
3012
|
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type { BaseMessage } from '@langchain/core/messages';
|
|
2
|
+
import type * as t from '@/types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Tracks the lifecycle of a spawned handoff child agent.
|
|
6
|
+
*/
|
|
7
|
+
export type HandoffRecord = {
|
|
8
|
+
/** Agent identity (destination agentId) — stable across multiple spawns of the same agent */
|
|
9
|
+
id: string;
|
|
10
|
+
/** Unique internal key for this specific spawn (agentId + monotonic counter) */
|
|
11
|
+
spawnKey: string;
|
|
12
|
+
/** Display name of the child agent */
|
|
13
|
+
name: string;
|
|
14
|
+
/** Task description / instructions passed to child */
|
|
15
|
+
task: string;
|
|
16
|
+
/** When the handoff was spawned */
|
|
17
|
+
spawnedAt: number;
|
|
18
|
+
/** Current status */
|
|
19
|
+
status: 'pending' | 'running' | 'completed' | 'failed';
|
|
20
|
+
/** The background promise executing the child subgraph */
|
|
21
|
+
promise: Promise<t.BaseGraphState>;
|
|
22
|
+
/** Resolved result text (populated on completion) */
|
|
23
|
+
resultText?: string;
|
|
24
|
+
/** Error message (populated on failure) */
|
|
25
|
+
error?: string;
|
|
26
|
+
/** Duration in ms (populated on completion/failure) */
|
|
27
|
+
durationMs?: number;
|
|
28
|
+
/** Number of messages in child's output */
|
|
29
|
+
resultMessageCount?: number;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Registry for async handoff execution.
|
|
34
|
+
*
|
|
35
|
+
* Enables the autonomous orchestration pattern:
|
|
36
|
+
* 1. Orchestrator spawns children (non-blocking)
|
|
37
|
+
* 2. Orchestrator stays alive to reason, spawn more, or check status
|
|
38
|
+
* 3. Orchestrator collects results when ready
|
|
39
|
+
*
|
|
40
|
+
* Scoped per MultiAgentGraph instance — each orchestrator graph gets its own registry.
|
|
41
|
+
*/
|
|
42
|
+
export class HandoffRegistry {
|
|
43
|
+
private records: Map<string, HandoffRecord> = new Map();
|
|
44
|
+
/** Monotonically increasing counter for unique spawn IDs */
|
|
45
|
+
private spawnCounter = 0;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Register a spawned handoff child.
|
|
49
|
+
* The promise runs in the background — not awaited here.
|
|
50
|
+
* Uses a unique key per spawn so the same agent can be spawned multiple times
|
|
51
|
+
* across rounds without overwriting prior records.
|
|
52
|
+
*/
|
|
53
|
+
spawn(params: {
|
|
54
|
+
id: string;
|
|
55
|
+
name: string;
|
|
56
|
+
task: string;
|
|
57
|
+
promise: Promise<t.BaseGraphState>;
|
|
58
|
+
extractResult: (messages: BaseMessage[], agentId: string) => string;
|
|
59
|
+
truncateResult: (text: string, maxChars: number) => string;
|
|
60
|
+
maxResultChars: number;
|
|
61
|
+
/** Callback when child completes (for SSE events) */
|
|
62
|
+
onComplete?: (record: HandoffRecord) => void;
|
|
63
|
+
}): void {
|
|
64
|
+
// Unique internal key: agentId + counter to support multiple spawns of same agent.
|
|
65
|
+
// record.id stays as the agent identity so callers/observability can attribute
|
|
66
|
+
// results to the agent regardless of which spawn round they came from.
|
|
67
|
+
const spawnKey = `${params.id}__${this.spawnCounter++}`;
|
|
68
|
+
const record: HandoffRecord = {
|
|
69
|
+
id: params.id,
|
|
70
|
+
spawnKey,
|
|
71
|
+
name: params.name,
|
|
72
|
+
task: params.task,
|
|
73
|
+
spawnedAt: Date.now(),
|
|
74
|
+
status: 'running',
|
|
75
|
+
promise: params.promise,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Wire up the promise to update the record on completion
|
|
79
|
+
params.promise
|
|
80
|
+
.then((result) => {
|
|
81
|
+
const resultText = params.extractResult(result.messages, params.id);
|
|
82
|
+
const truncated = params.truncateResult(
|
|
83
|
+
resultText,
|
|
84
|
+
params.maxResultChars
|
|
85
|
+
);
|
|
86
|
+
record.status = 'completed';
|
|
87
|
+
record.resultText = truncated;
|
|
88
|
+
record.durationMs = Date.now() - record.spawnedAt;
|
|
89
|
+
record.resultMessageCount = result.messages.length;
|
|
90
|
+
params.onComplete?.(record);
|
|
91
|
+
})
|
|
92
|
+
.catch((err) => {
|
|
93
|
+
record.status = 'failed';
|
|
94
|
+
record.error = err instanceof Error ? err.message : String(err);
|
|
95
|
+
record.durationMs = Date.now() - record.spawnedAt;
|
|
96
|
+
params.onComplete?.(record);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
this.records.set(spawnKey, record);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** List all pending (running) handoffs */
|
|
103
|
+
listPending(): HandoffRecord[] {
|
|
104
|
+
return Array.from(this.records.values()).filter(
|
|
105
|
+
(r) => r.status === 'running'
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** List all completed handoffs (not yet collected) */
|
|
110
|
+
listCompleted(): HandoffRecord[] {
|
|
111
|
+
return Array.from(this.records.values()).filter(
|
|
112
|
+
(r) => r.status === 'completed' || r.status === 'failed'
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** List all handoffs regardless of status */
|
|
117
|
+
listAll(): HandoffRecord[] {
|
|
118
|
+
return Array.from(this.records.values());
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get a handoff record by either its unique spawnKey or by agentId.
|
|
123
|
+
* - Exact spawnKey match wins (O(1)).
|
|
124
|
+
* - Falls back to the most recently spawned record for that agentId — matching
|
|
125
|
+
* lookups by callers that only know the agent identity, not the spawn round.
|
|
126
|
+
*/
|
|
127
|
+
get(idOrSpawnKey: string): HandoffRecord | undefined {
|
|
128
|
+
const direct = this.records.get(idOrSpawnKey);
|
|
129
|
+
if (direct) return direct;
|
|
130
|
+
let latest: HandoffRecord | undefined;
|
|
131
|
+
for (const record of this.records.values()) {
|
|
132
|
+
if (record.id !== idOrSpawnKey) continue;
|
|
133
|
+
if (!latest || record.spawnedAt >= latest.spawnedAt) latest = record;
|
|
134
|
+
}
|
|
135
|
+
return latest;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Check if any handoffs are still running */
|
|
139
|
+
hasPending(): boolean {
|
|
140
|
+
return this.listPending().length > 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Wait for ALL pending handoffs to complete and return all records.
|
|
145
|
+
* Records are NOT auto-cleared — caller removes collected records via remove().
|
|
146
|
+
*/
|
|
147
|
+
async waitForAll(): Promise<HandoffRecord[]> {
|
|
148
|
+
const pending = this.listPending();
|
|
149
|
+
if (pending.length > 0) {
|
|
150
|
+
await Promise.allSettled(pending.map((r) => r.promise));
|
|
151
|
+
}
|
|
152
|
+
const results = this.listAll();
|
|
153
|
+
return results;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Wait for ANY pending handoff to complete.
|
|
158
|
+
* Returns the newly completed record(s).
|
|
159
|
+
*/
|
|
160
|
+
async waitForAny(): Promise<HandoffRecord[]> {
|
|
161
|
+
const pending = this.listPending();
|
|
162
|
+
if (pending.length === 0) {
|
|
163
|
+
return this.listCompleted();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Race all pending promises — at least one will resolve
|
|
167
|
+
await Promise.race(
|
|
168
|
+
pending.map((r) => r.promise.then(() => r).catch(() => r))
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
// Small yield to let promise handlers update records
|
|
172
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
173
|
+
|
|
174
|
+
return this.listCompleted();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Remove record(s) by spawnKey or agentId.
|
|
179
|
+
* - Exact spawnKey match removes only that record.
|
|
180
|
+
* - agentId match removes ALL records for that agent (covers callers that
|
|
181
|
+
* want to forget everything tied to a given agent).
|
|
182
|
+
*/
|
|
183
|
+
remove(idOrSpawnKey: string): void {
|
|
184
|
+
if (this.records.delete(idOrSpawnKey)) return;
|
|
185
|
+
for (const [key, record] of this.records) {
|
|
186
|
+
if (record.id === idOrSpawnKey) this.records.delete(key);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Clear all records (for cleanup between graph invocations) */
|
|
191
|
+
clear(): void {
|
|
192
|
+
this.records.clear();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Number of total tracked handoffs */
|
|
196
|
+
get size(): number {
|
|
197
|
+
return this.records.size;
|
|
198
|
+
}
|
|
199
|
+
}
|