@illuma-ai/agents 1.1.28 → 1.3.1
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.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 +89 -45
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/HandoffRegistry.cjs +47 -8
- package/dist/cjs/graphs/HandoffRegistry.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +493 -267
- 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 +117 -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/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/errors.cjs +113 -0
- package/dist/cjs/utils/errors.cjs.map +1 -0
- package/dist/cjs/utils/events.cjs +36 -7
- 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.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 +89 -45
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/HandoffRegistry.mjs +47 -8
- package/dist/esm/graphs/HandoffRegistry.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +493 -267
- 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/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/errors.mjs +109 -0
- package/dist/esm/utils/errors.mjs.map +1 -0
- package/dist/esm/utils/events.mjs +36 -8
- 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 +24 -7
- package/dist/types/graphs/MultiAgentGraph.d.ts +43 -23
- 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 +10 -3
- package/dist/types/utils/childAgentContext.d.ts +99 -0
- package/dist/types/utils/errors.d.ts +37 -0
- package/dist/types/utils/events.d.ts +21 -0
- package/dist/types/utils/finishReasons.d.ts +32 -0
- package/dist/types/utils/index.d.ts +1 -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 +12 -4
- 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 +95 -61
- package/src/graphs/HandoffRegistry.ts +48 -17
- package/src/graphs/MultiAgentGraph.ts +588 -327
- package/src/graphs/__tests__/HandoffRegistry.test.ts +4 -1
- 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/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/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 +1 -1
- 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 +10 -3
- package/src/utils/__tests__/childAgentContext.test.ts +217 -0
- package/src/utils/__tests__/errors.test.ts +136 -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/errors.ts +115 -0
- package/src/utils/events.ts +37 -7
- package/src/utils/finishReasons.ts +40 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/llm.ts +0 -1
- package/src/utils/logging.ts +45 -8
- package/src/utils/toolCallNormalization.ts +271 -0
|
@@ -27,13 +27,21 @@ import {
|
|
|
27
27
|
EdgeType,
|
|
28
28
|
GraphEvents,
|
|
29
29
|
DEFAULT_HANDOFF_MAX_RESULT_CHARS,
|
|
30
|
+
buildSpawnPath,
|
|
31
|
+
spawnPathDepth,
|
|
30
32
|
} from '@/common';
|
|
31
33
|
import { safeDispatchCustomEvent } from '@/utils/events';
|
|
34
|
+
import { mlog, mwarn } from '@/utils/logging';
|
|
35
|
+
import {
|
|
36
|
+
prepareHandoffMessages as prepareHandoffMessagesUtil,
|
|
37
|
+
prepareIsolatedChildMessages as prepareIsolatedChildMessagesUtil,
|
|
38
|
+
} from '@/utils/childAgentContext';
|
|
32
39
|
import {
|
|
33
40
|
createApprovalGateNode,
|
|
34
41
|
getApprovalGateNodeId,
|
|
35
42
|
} from '@/nodes/ApprovalGateNode';
|
|
36
|
-
|
|
43
|
+
// HandoffRegistry no longer needed — handoff tools use synchronous
|
|
44
|
+
// browser-tool callback pattern (spawn → wait → return result)
|
|
37
45
|
|
|
38
46
|
/** Pattern to extract instructions from transfer ToolMessage content */
|
|
39
47
|
const TRANSFER_INSTRUCTIONS_PATTERN = /(?:Instructions?|Context):\s*(.+)/is;
|
|
@@ -85,10 +93,10 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
85
93
|
|
|
86
94
|
/**
|
|
87
95
|
* Registry for async handoff execution.
|
|
88
|
-
* Enables
|
|
96
|
+
* Enables autonomous orchestration: spawn children non-blocking,
|
|
89
97
|
* orchestrator stays alive to reason and collect results when ready.
|
|
90
98
|
*/
|
|
91
|
-
|
|
99
|
+
// HandoffRegistry removed — handoff tools are synchronous (callback pattern)
|
|
92
100
|
|
|
93
101
|
/**
|
|
94
102
|
* When set, the graph routes START to this agent instead of the default starting nodes.
|
|
@@ -105,7 +113,7 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
105
113
|
this.analyzeGraph();
|
|
106
114
|
this.createTransferTools();
|
|
107
115
|
this.createHandoffTools();
|
|
108
|
-
|
|
116
|
+
mlog(
|
|
109
117
|
`[MultiAgentGraph] Constructor complete: ${this.agentContexts.size} agents, ${this.edges.length} edges`
|
|
110
118
|
);
|
|
111
119
|
}
|
|
@@ -119,7 +127,10 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
119
127
|
this.handoffEdges.push(edge);
|
|
120
128
|
} else if (edge.edgeType === EdgeType.SEQUENCE) {
|
|
121
129
|
this.sequenceEdges.push(edge);
|
|
122
|
-
} else if (
|
|
130
|
+
} else if (
|
|
131
|
+
edge.edgeType === EdgeType.TRANSFER ||
|
|
132
|
+
edge.condition != null
|
|
133
|
+
) {
|
|
123
134
|
this.transferEdges.push(edge);
|
|
124
135
|
} else {
|
|
125
136
|
// Default: single-to-single edges are transfer, single-to-multiple are sequence
|
|
@@ -135,7 +146,7 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
135
146
|
}
|
|
136
147
|
}
|
|
137
148
|
}
|
|
138
|
-
|
|
149
|
+
mlog(
|
|
139
150
|
`[MultiAgentGraph] Edge categorization: ${this.handoffEdges.length} handoff, ${this.transferEdges.length} transfer, ${this.sequenceEdges.length} sequence (of ${this.edges.length} total)`
|
|
140
151
|
);
|
|
141
152
|
}
|
|
@@ -164,7 +175,7 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
164
175
|
this.startingNodes.add(this.agentContexts.keys().next().value!);
|
|
165
176
|
}
|
|
166
177
|
|
|
167
|
-
|
|
178
|
+
mlog(
|
|
168
179
|
`[MultiAgentGraph] Starting nodes identified: [${Array.from(this.startingNodes).join(', ')}]`
|
|
169
180
|
);
|
|
170
181
|
|
|
@@ -319,7 +330,7 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
319
330
|
agentContext.graphTools = [];
|
|
320
331
|
}
|
|
321
332
|
agentContext.graphTools.push(...transferTools);
|
|
322
|
-
|
|
333
|
+
mlog(
|
|
323
334
|
`[MultiAgentGraph] Transfer tools for "${agentId}": [${transferTools.map((t) => t.name).join(', ')}]`
|
|
324
335
|
);
|
|
325
336
|
|
|
@@ -333,7 +344,10 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
333
344
|
return `${name}${desc}`;
|
|
334
345
|
});
|
|
335
346
|
});
|
|
336
|
-
const guidance = this.buildOrchestratorGuidance(
|
|
347
|
+
const guidance = this.buildOrchestratorGuidance(
|
|
348
|
+
childDescs,
|
|
349
|
+
transferTools.length
|
|
350
|
+
);
|
|
337
351
|
const existing = agentContext.additionalInstructions ?? '';
|
|
338
352
|
agentContext.additionalInstructions = existing
|
|
339
353
|
? `${existing}\n\n${guidance}`
|
|
@@ -365,7 +379,9 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
365
379
|
/** Check if we have a prompt for handoff input */
|
|
366
380
|
const hasTransferInput =
|
|
367
381
|
edge.prompt != null && typeof edge.prompt === 'string';
|
|
368
|
-
const transferInputDescription = hasTransferInput
|
|
382
|
+
const transferInputDescription = hasTransferInput
|
|
383
|
+
? edge.prompt
|
|
384
|
+
: undefined;
|
|
369
385
|
const promptKey = edge.promptKey ?? 'instructions';
|
|
370
386
|
|
|
371
387
|
tools.push(
|
|
@@ -581,10 +597,14 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
581
597
|
* Builds orchestration guidance injected into the system message of agents
|
|
582
598
|
* that have handoff or transfer tools (i.e., orchestrator agents).
|
|
583
599
|
*
|
|
584
|
-
*
|
|
585
|
-
* -
|
|
586
|
-
* - Multi-round execution for dependent tasks
|
|
587
|
-
*
|
|
600
|
+
* Implements two orchestration primitives:
|
|
601
|
+
* - Execution bias guidance injected into the system prompt
|
|
602
|
+
* - Multi-round autonomous execution for dependent tasks
|
|
603
|
+
*
|
|
604
|
+
* Handoff tools are synchronous (browser-tool callback pattern): spawn the
|
|
605
|
+
* child, await completion, return the real text as the tool output. Parallel
|
|
606
|
+
* handoff tool calls in one turn run concurrently via LangGraph's ToolNode,
|
|
607
|
+
* so independent children run in parallel without explicit orchestration.
|
|
588
608
|
*
|
|
589
609
|
* @param childDescs - Display names (with optional descriptions) of child agents
|
|
590
610
|
* @param toolCount - Number of handoff/transfer tools available
|
|
@@ -599,20 +619,59 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
599
619
|
`You have ${toolCount} specialist agent(s) available for delegation:`,
|
|
600
620
|
...childDescs.map((d) => `- ${d}`),
|
|
601
621
|
'',
|
|
602
|
-
'
|
|
603
|
-
'
|
|
604
|
-
'
|
|
605
|
-
'
|
|
606
|
-
'
|
|
607
|
-
'
|
|
622
|
+
'## Execution Bias',
|
|
623
|
+
'If the user asks you to do the work, start doing it in the same turn.',
|
|
624
|
+
'Use a real tool call or concrete action first when the task is actionable; do not stop at a plan or promise-to-act reply.',
|
|
625
|
+
'Commentary-only turns are incomplete when tools are available and the next action is clear.',
|
|
626
|
+
'If the work will take multiple steps or a while to finish, send one short progress update before or while acting.',
|
|
627
|
+
'',
|
|
628
|
+
'## How Delegation Works',
|
|
629
|
+
'Each handoff tool call spawns a sub-agent, waits for it to complete, and returns the real result directly — like a function call.',
|
|
630
|
+
'Independent tasks MAY be called in parallel (multiple handoff tool calls in one turn). They run concurrently and all results return together.',
|
|
631
|
+
'Dependent tasks MUST be sequential: call one agent, get the result, then call the next agent using that real data.',
|
|
632
|
+
'',
|
|
633
|
+
'## Agent Isolation',
|
|
634
|
+
"Sub-agents CANNOT see your conversation or the user's original message. They ONLY see what you write in the `instructions` parameter.",
|
|
635
|
+
"When writing instructions, include ALL data the agent needs. Copy exact values from the user's message — email addresses, names, URLs, dates, numbers.",
|
|
636
|
+
'When delegating follow-up work, include the real data from prior agent results directly in the instructions text.',
|
|
637
|
+
'Do NOT re-delegate a task that was already completed. If you have the data, pass it directly.',
|
|
638
|
+
].join('\n');
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Builds subagent context instructions injected into child agents that are
|
|
643
|
+
* handoff destinations. This tells the child agent it is a subagent with
|
|
644
|
+
* a focused task.
|
|
645
|
+
*
|
|
646
|
+
* @param orchestratorName - Display name of the parent orchestrator agent
|
|
647
|
+
*/
|
|
648
|
+
private buildChildAgentContext(orchestratorName: string): string {
|
|
649
|
+
return [
|
|
650
|
+
'# Subagent Context',
|
|
651
|
+
'',
|
|
652
|
+
`You are a **subagent** spawned by the ${orchestratorName} for a specific task.`,
|
|
608
653
|
'',
|
|
609
|
-
'
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
'
|
|
613
|
-
'
|
|
614
|
-
'
|
|
615
|
-
|
|
654
|
+
'## Your Role',
|
|
655
|
+
"- Complete this task. That's your entire purpose.",
|
|
656
|
+
`- You are NOT the ${orchestratorName}. Don't try to be.`,
|
|
657
|
+
'',
|
|
658
|
+
'## Rules',
|
|
659
|
+
'1. **Stay focused** - Do your assigned task, nothing else',
|
|
660
|
+
`2. **Complete the task** - Your final message will be automatically reported to the ${orchestratorName}`,
|
|
661
|
+
"3. **Don't initiate** - No heartbeats, no proactive actions, no side quests",
|
|
662
|
+
"4. **Be ephemeral** - You may be terminated after task completion. That's fine.",
|
|
663
|
+
'',
|
|
664
|
+
'## Output Format',
|
|
665
|
+
'When complete, your final response should include:',
|
|
666
|
+
'- What you accomplished or found',
|
|
667
|
+
`- Any relevant details the ${orchestratorName} should know`,
|
|
668
|
+
'- Keep it concise but informative',
|
|
669
|
+
'',
|
|
670
|
+
"## What You DON'T Do",
|
|
671
|
+
`- NO user conversations (that's ${orchestratorName}'s job)`,
|
|
672
|
+
'- NO external messages (email, tweets, etc.) unless explicitly tasked with a specific recipient/channel',
|
|
673
|
+
'- NO cron jobs or persistent state',
|
|
674
|
+
`- NO pretending to be the ${orchestratorName}`,
|
|
616
675
|
].join('\n');
|
|
617
676
|
}
|
|
618
677
|
|
|
@@ -664,9 +723,7 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
664
723
|
|
|
665
724
|
const handoffTools: t.GenericTool[] = [];
|
|
666
725
|
for (const edge of edges) {
|
|
667
|
-
handoffTools.push(
|
|
668
|
-
...this.createHandoffToolsForEdge(edge, agentId)
|
|
669
|
-
);
|
|
726
|
+
handoffTools.push(...this.createHandoffToolsForEdge(edge, agentId));
|
|
670
727
|
}
|
|
671
728
|
|
|
672
729
|
if (!agentContext.graphTools) {
|
|
@@ -674,94 +731,15 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
674
731
|
}
|
|
675
732
|
agentContext.graphTools.push(...handoffTools);
|
|
676
733
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
* spawn → reason → check/collect → reason → spawn more → synthesize
|
|
681
|
-
*/
|
|
682
|
-
const handoffReg = this.handoffRegistry;
|
|
734
|
+
// No collect_results tool needed — handoff tools use the browser-tool
|
|
735
|
+
// callback pattern: spawn child, wait for completion, return real result.
|
|
736
|
+
// The LLM naturally gets child results as tool return values.
|
|
683
737
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
async () => {
|
|
687
|
-
if (!handoffReg.hasPending() && handoffReg.size === 0) {
|
|
688
|
-
return 'No agents have been spawned yet.';
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
/** Wait for all pending handoffs to complete */
|
|
692
|
-
const records = await handoffReg.waitForAll();
|
|
693
|
-
|
|
694
|
-
const parts: string[] = [];
|
|
695
|
-
for (const record of records) {
|
|
696
|
-
if (record.status === 'completed') {
|
|
697
|
-
parts.push(
|
|
698
|
-
`## ${record.name} (completed in ${record.durationMs}ms)\n${record.resultText}`
|
|
699
|
-
);
|
|
700
|
-
} else if (record.status === 'failed') {
|
|
701
|
-
parts.push(
|
|
702
|
-
`## ${record.name} (FAILED after ${record.durationMs}ms)\nError: ${record.error}`
|
|
703
|
-
);
|
|
704
|
-
} else {
|
|
705
|
-
parts.push(
|
|
706
|
-
`## ${record.name} (still running, ${Date.now() - record.spawnedAt}ms elapsed)`
|
|
707
|
-
);
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
return parts.join('\n\n---\n\n');
|
|
712
|
-
},
|
|
713
|
-
{
|
|
714
|
-
name: 'collect_results',
|
|
715
|
-
schema: { type: 'object', properties: {}, required: [] },
|
|
716
|
-
description:
|
|
717
|
-
'Wait for all spawned agents to complete and collect their results. ' +
|
|
718
|
-
'Call this after spawning one or more agents to get their output.',
|
|
719
|
-
}
|
|
720
|
-
),
|
|
721
|
-
tool(
|
|
722
|
-
async () => {
|
|
723
|
-
const all = handoffReg.listAll();
|
|
724
|
-
if (all.length === 0) {
|
|
725
|
-
return 'No agents tracked.';
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
const lines = all.map((r) => {
|
|
729
|
-
const elapsed = Date.now() - r.spawnedAt;
|
|
730
|
-
if (r.status === 'running') {
|
|
731
|
-
return `- **${r.name}**: running (${elapsed}ms elapsed) — task: ${r.task.substring(0, 100)}`;
|
|
732
|
-
} else if (r.status === 'completed') {
|
|
733
|
-
return `- **${r.name}**: completed (${r.durationMs}ms, ${r.resultText?.length ?? 0} chars)`;
|
|
734
|
-
} else {
|
|
735
|
-
return `- **${r.name}**: failed (${r.durationMs}ms) — ${r.error}`;
|
|
736
|
-
}
|
|
737
|
-
});
|
|
738
|
-
|
|
739
|
-
const pending = all.filter((r) => r.status === 'running').length;
|
|
740
|
-
const completed = all.filter((r) => r.status === 'completed').length;
|
|
741
|
-
const failed = all.filter((r) => r.status === 'failed').length;
|
|
742
|
-
|
|
743
|
-
return [
|
|
744
|
-
`**Agent Status**: ${pending} running, ${completed} completed, ${failed} failed`,
|
|
745
|
-
'',
|
|
746
|
-
...lines,
|
|
747
|
-
].join('\n');
|
|
748
|
-
},
|
|
749
|
-
{
|
|
750
|
-
name: 'check_agents',
|
|
751
|
-
schema: { type: 'object', properties: {}, required: [] },
|
|
752
|
-
description:
|
|
753
|
-
'Check the status of all spawned agents without waiting. ' +
|
|
754
|
-
'Shows which agents are running, completed, or failed.',
|
|
755
|
-
}
|
|
756
|
-
)
|
|
757
|
-
);
|
|
758
|
-
|
|
759
|
-
console.debug(
|
|
760
|
-
`[MultiAgentGraph] Handoff tools for "${agentId}": [${handoffTools.map((t) => t.name).join(', ')}, collect_results, check_agents]`
|
|
738
|
+
mlog(
|
|
739
|
+
`[MultiAgentGraph] Handoff tools for "${agentId}": [${handoffTools.map((t) => t.name).join(', ')}]`
|
|
761
740
|
);
|
|
762
741
|
|
|
763
742
|
// Inject autonomous orchestration guidance for agents with handoff tools.
|
|
764
|
-
// Modeled after OpenClaw's battle-tested subagent orchestration patterns.
|
|
765
743
|
const childDescs = edges.flatMap((e) => {
|
|
766
744
|
const dests = Array.isArray(e.to) ? e.to : [e.to];
|
|
767
745
|
return dests.map((d) => {
|
|
@@ -780,6 +758,25 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
780
758
|
agentContext.additionalInstructions = existing
|
|
781
759
|
? `${existing}\n\n${orchestrationGuidance}`
|
|
782
760
|
: orchestrationGuidance;
|
|
761
|
+
|
|
762
|
+
// Inject subagent context into each child/destination agent.
|
|
763
|
+
// This tells child agents they are subagents with a focused task — stay focused,
|
|
764
|
+
// execute (don't plan), and return results to the orchestrator.
|
|
765
|
+
const orchestratorName = agentContext.name ?? agentId;
|
|
766
|
+
const childAgentContext = this.buildChildAgentContext(orchestratorName);
|
|
767
|
+
for (const edge of edges) {
|
|
768
|
+
const dests = Array.isArray(edge.to) ? edge.to : [edge.to];
|
|
769
|
+
for (const destId of dests) {
|
|
770
|
+
const destCtx = this.agentContexts.get(destId);
|
|
771
|
+
if (!destCtx) continue;
|
|
772
|
+
const existingChild = destCtx.additionalInstructions ?? '';
|
|
773
|
+
// Avoid duplicate injection if agent is destination of multiple edges
|
|
774
|
+
if (existingChild.includes('# Subagent Context')) continue;
|
|
775
|
+
destCtx.additionalInstructions = existingChild
|
|
776
|
+
? `${existingChild}\n\n${childAgentContext}`
|
|
777
|
+
: childAgentContext;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
783
780
|
}
|
|
784
781
|
}
|
|
785
782
|
|
|
@@ -787,8 +784,8 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
787
784
|
* Create handoff tools for an edge (handles multiple destinations).
|
|
788
785
|
* Each handoff tool spawns the child agent's compiled subgraph asynchronously
|
|
789
786
|
* and returns immediately. The orchestrator uses `collect_results` to retrieve
|
|
790
|
-
* outputs and `check_agents` to monitor status —
|
|
791
|
-
*
|
|
787
|
+
* outputs and `check_agents` to monitor status — a push-based autonomous
|
|
788
|
+
* orchestration pattern.
|
|
792
789
|
*
|
|
793
790
|
* @param edge - The graph edge defining the handoff
|
|
794
791
|
* @param sourceAgentId - The ID of the parent/supervisor agent
|
|
@@ -809,14 +806,21 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
809
806
|
edge.description ??
|
|
810
807
|
this.buildDefaultHandoffDescription(destContext, destination);
|
|
811
808
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
809
|
+
/**
|
|
810
|
+
* Always include an instructions parameter so the orchestrator can
|
|
811
|
+
* pass scoped task descriptions to each child agent. Without this,
|
|
812
|
+
* the child gets no context about what to do.
|
|
813
|
+
*/
|
|
815
814
|
const promptKey = edge.promptKey ?? 'instructions';
|
|
816
|
-
|
|
817
|
-
|
|
815
|
+
const destDesc = destContext?.description;
|
|
816
|
+
const promptInputDescription =
|
|
817
|
+
edge.prompt ??
|
|
818
|
+
(destDesc
|
|
819
|
+
? `Task instructions for this agent (${destDesc}). Describe exactly what it should do.`
|
|
820
|
+
: 'Specific task instructions for this agent. Describe exactly what it should do and what data to use.');
|
|
821
|
+
|
|
822
|
+
/** Capture registry reference — Map populated in createWorkflow() */
|
|
818
823
|
const subgraphReg = this.subgraphRegistry;
|
|
819
|
-
const handoffReg = this.handoffRegistry;
|
|
820
824
|
|
|
821
825
|
tools.push(
|
|
822
826
|
tool(
|
|
@@ -830,19 +834,72 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
830
834
|
);
|
|
831
835
|
}
|
|
832
836
|
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
+
/**
|
|
838
|
+
* Per-spawn unique key = the orchestrator's tool_call.id.
|
|
839
|
+
* LangChain's ToolNode passes `config.toolCall.id` to the tool
|
|
840
|
+
* function; this is the same id the frontend sees on the parent's
|
|
841
|
+
* handoff content part, so the UI can match each AgentHandoff
|
|
842
|
+
* indicator to its own sidebar task without collision when the
|
|
843
|
+
* same agent is invoked multiple times.
|
|
844
|
+
*/
|
|
845
|
+
const toolCallCfg = (
|
|
846
|
+
config as { toolCall?: { id?: string } } | undefined
|
|
847
|
+
)?.toolCall;
|
|
848
|
+
const spawnKey =
|
|
849
|
+
toolCallCfg?.id ??
|
|
850
|
+
`${destination}_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Hierarchical spawnPath: parent's spawnPath (from metadata) + this spawnKey.
|
|
854
|
+
* Root invocations have empty parentSpawnPath. Threaded through childConfig
|
|
855
|
+
* so nested handoffs/sequences inherit the full ancestry.
|
|
856
|
+
* See docs/multi-agent-nesting-architecture.md §4.
|
|
857
|
+
*/
|
|
858
|
+
const parentMetadata = (
|
|
859
|
+
config as { metadata?: Record<string, unknown> } | undefined
|
|
860
|
+
)?.metadata;
|
|
861
|
+
const parentSpawnPath =
|
|
862
|
+
typeof parentMetadata?.spawnPath === 'string'
|
|
863
|
+
? parentMetadata.spawnPath
|
|
864
|
+
: '';
|
|
865
|
+
const childSpawnPath = buildSpawnPath(parentSpawnPath, spawnKey);
|
|
866
|
+
const childDepth = spawnPathDepth(childSpawnPath);
|
|
837
867
|
|
|
838
|
-
/**
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
868
|
+
/**
|
|
869
|
+
* Child agent message construction — three modes:
|
|
870
|
+
*
|
|
871
|
+
* 1. Default (isolated-session pattern): Child gets ONLY the orchestrator's
|
|
872
|
+
* task instructions as a single HumanMessage. No parent conversation
|
|
873
|
+
* leaks. Orchestrator controls exactly what context the child sees.
|
|
874
|
+
*
|
|
875
|
+
* 2. Passthrough (edge.passthrough = true): Child gets the full parent
|
|
876
|
+
* conversation + orchestrator's instructions appended. Use this when
|
|
877
|
+
* the child needs the full user context (e.g., a transfer-like handoff).
|
|
878
|
+
*
|
|
879
|
+
* 3. Fallback: If no instructions provided AND not passthrough, child
|
|
880
|
+
* gets the agent's description as its task.
|
|
881
|
+
*/
|
|
882
|
+
const taskDescription =
|
|
883
|
+
promptKey in input && input[promptKey] != null
|
|
884
|
+
? String(input[promptKey])
|
|
885
|
+
: '';
|
|
886
|
+
|
|
887
|
+
let childMessages: BaseMessage[];
|
|
888
|
+
if (edge.passthrough) {
|
|
889
|
+
// Passthrough: full parent context + instructions appended
|
|
890
|
+
const state = getCurrentTaskInput() as t.BaseGraphState;
|
|
891
|
+
childMessages = MultiAgentGraph.prepareHandoffMessages([
|
|
892
|
+
...state.messages,
|
|
893
|
+
]);
|
|
894
|
+
if (taskDescription) {
|
|
895
|
+
childMessages.push(new HumanMessage(taskDescription));
|
|
896
|
+
}
|
|
897
|
+
} else {
|
|
898
|
+
// Default: isolated — only orchestrator's instructions
|
|
899
|
+
const fallbackTask =
|
|
900
|
+
destContext?.description ?? 'Complete your assigned task.';
|
|
843
901
|
childMessages = [
|
|
844
|
-
|
|
845
|
-
new HumanMessage(taskDescription),
|
|
902
|
+
new HumanMessage(taskDescription || fallbackTask),
|
|
846
903
|
];
|
|
847
904
|
}
|
|
848
905
|
|
|
@@ -852,7 +909,7 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
852
909
|
|
|
853
910
|
const childContext = this.agentContexts.get(destination);
|
|
854
911
|
const destName = destContext?.name ?? destination;
|
|
855
|
-
|
|
912
|
+
mlog(
|
|
856
913
|
`[MultiAgentGraph] Handoff "${sourceAgentId}" -> "${destination}" SPAWN (async)\n` +
|
|
857
914
|
` messages: ${childMessages.length}\n` +
|
|
858
915
|
` childTools: ${childContext?.tools?.length ?? 0} instances\n` +
|
|
@@ -862,83 +919,192 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
862
919
|
/**
|
|
863
920
|
* Dispatch transition BEFORE spawning the child subgraph so that
|
|
864
921
|
* callbacks.js sets multiAgentTrace.isMultiAgent = true before the
|
|
865
|
-
* child's ON_RUN_STEP events fire.
|
|
922
|
+
* child's ON_RUN_STEP events fire. spawnKey lets the UI create a
|
|
923
|
+
* distinct sidebar task for this specific invocation.
|
|
866
924
|
*/
|
|
925
|
+
mlog(
|
|
926
|
+
`[MultiAgentGraph] Handoff SPAWN "${sourceAgentId}" -> "${destination}" spawnKey=${spawnKey}`
|
|
927
|
+
);
|
|
867
928
|
await safeDispatchCustomEvent(
|
|
868
929
|
GraphEvents.ON_AGENT_TRANSITION,
|
|
869
930
|
{
|
|
870
931
|
sourceAgentId: sourceAgentId,
|
|
871
|
-
sourceAgentName:
|
|
932
|
+
sourceAgentName:
|
|
933
|
+
this.agentContexts.get(sourceAgentId)?.name ?? sourceAgentId,
|
|
872
934
|
destinationAgentId: destination,
|
|
873
935
|
destinationAgentName: destName,
|
|
874
936
|
edgeType: EdgeType.HANDOFF,
|
|
875
937
|
timestamp: Date.now(),
|
|
938
|
+
spawnKey,
|
|
939
|
+
spawnPath: childSpawnPath,
|
|
940
|
+
parentSpawnPath: parentSpawnPath || null,
|
|
941
|
+
spawnDepth: childDepth,
|
|
876
942
|
},
|
|
877
943
|
config
|
|
878
944
|
);
|
|
879
945
|
|
|
880
946
|
/**
|
|
881
|
-
*
|
|
882
|
-
*
|
|
883
|
-
*
|
|
947
|
+
* Child events need to carry spawnKey so callbacks.js can route
|
|
948
|
+
* them to the correct child aggregator. LangChain propagates
|
|
949
|
+
* `metadata` and `tags` from the parent config to all descendants,
|
|
950
|
+
* so everything dispatched inside subgraph.invoke will have
|
|
951
|
+
* metadata.spawnKey populated on the event's runtime metadata.
|
|
952
|
+
*/
|
|
953
|
+
const childConfig = {
|
|
954
|
+
...(config ?? {}),
|
|
955
|
+
metadata: {
|
|
956
|
+
...((
|
|
957
|
+
config as { metadata?: Record<string, unknown> } | undefined
|
|
958
|
+
)?.metadata ?? {}),
|
|
959
|
+
spawnKey,
|
|
960
|
+
spawnAgentId: destination,
|
|
961
|
+
/** Hierarchical identity — see spawnPath.ts */
|
|
962
|
+
spawnPath: childSpawnPath,
|
|
963
|
+
parentSpawnPath: parentSpawnPath || null,
|
|
964
|
+
spawnDepth: childDepth,
|
|
965
|
+
},
|
|
966
|
+
tags: [
|
|
967
|
+
...((config as { tags?: string[] } | undefined)?.tags ?? []),
|
|
968
|
+
`spawn:${spawnKey}`,
|
|
969
|
+
`depth:${childDepth}`,
|
|
970
|
+
],
|
|
971
|
+
};
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* Callback pattern: spawn child, WAIT for completion, return real
|
|
975
|
+
* result. The parent naturally sees child results in its tool
|
|
976
|
+
* return, so no manual "collect_results" step is needed.
|
|
884
977
|
*
|
|
885
|
-
*
|
|
886
|
-
*
|
|
978
|
+
* Parallelism still works: when the LLM emits multiple handoff
|
|
979
|
+
* tool calls in one response, LangGraph runs all tool functions
|
|
980
|
+
* concurrently. Each waits for its child. All results land in
|
|
981
|
+
* the LLM's next turn together.
|
|
887
982
|
*/
|
|
888
|
-
const
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
983
|
+
const spawnedAt = Date.now();
|
|
984
|
+
|
|
985
|
+
try {
|
|
986
|
+
const result = await subgraph.invoke(childState, childConfig);
|
|
987
|
+
const durationMs = Date.now() - spawnedAt;
|
|
988
|
+
|
|
989
|
+
const resultText = MultiAgentGraph.extractHandoffResult(
|
|
990
|
+
result.messages,
|
|
991
|
+
destination
|
|
992
|
+
);
|
|
993
|
+
const truncated = MultiAgentGraph.truncateHandoffResult(
|
|
994
|
+
resultText,
|
|
995
|
+
maxResultChars
|
|
996
|
+
);
|
|
997
|
+
|
|
998
|
+
mlog(
|
|
999
|
+
`[MultiAgentGraph] Handoff COMPLETED "${sourceAgentId}" -> "${destination}" ` +
|
|
1000
|
+
`spawnKey=${spawnKey} (${durationMs}ms, ${truncated.length} chars)`
|
|
1001
|
+
);
|
|
1002
|
+
|
|
1003
|
+
/** Dispatch completion event for UI update — carries spawnKey so
|
|
1004
|
+
* the frontend can mark the correct sidebar task as completed. */
|
|
1005
|
+
safeDispatchCustomEvent(
|
|
1006
|
+
GraphEvents.ON_AGENT_TRANSITION,
|
|
1007
|
+
{
|
|
1008
|
+
sourceAgentId: destination,
|
|
1009
|
+
sourceAgentName: destName,
|
|
1010
|
+
destinationAgentId: sourceAgentId,
|
|
1011
|
+
destinationAgentName:
|
|
1012
|
+
this.agentContexts.get(sourceAgentId)?.name ??
|
|
1013
|
+
sourceAgentId,
|
|
1014
|
+
edgeType: EdgeType.HANDOFF,
|
|
1015
|
+
timestamp: Date.now(),
|
|
1016
|
+
isCompletion: true,
|
|
1017
|
+
durationMs,
|
|
1018
|
+
resultLength: truncated.length,
|
|
1019
|
+
spawnKey,
|
|
1020
|
+
spawnPath: childSpawnPath,
|
|
1021
|
+
parentSpawnPath: parentSpawnPath || null,
|
|
1022
|
+
spawnDepth: childDepth,
|
|
1023
|
+
},
|
|
1024
|
+
config
|
|
1025
|
+
).catch(() => {
|
|
1026
|
+
/* best-effort event dispatch */
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
return truncated;
|
|
1030
|
+
} catch (err: unknown) {
|
|
1031
|
+
const durationMs = Date.now() - spawnedAt;
|
|
1032
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1033
|
+
|
|
1034
|
+
// EPIPE from console.debug is non-fatal
|
|
1035
|
+
if (errMsg.includes('EPIPE')) {
|
|
1036
|
+
mwarn(
|
|
1037
|
+
`[MultiAgentGraph] Child "${destination}" hit EPIPE (non-fatal) spawnKey=${spawnKey}`
|
|
906
1038
|
);
|
|
907
|
-
/** Dispatch completion event for UI update */
|
|
908
1039
|
safeDispatchCustomEvent(
|
|
909
1040
|
GraphEvents.ON_AGENT_TRANSITION,
|
|
910
1041
|
{
|
|
911
1042
|
sourceAgentId: destination,
|
|
912
1043
|
sourceAgentName: destName,
|
|
913
|
-
destinationAgentId:
|
|
914
|
-
destinationAgentName:
|
|
1044
|
+
destinationAgentId: sourceAgentId,
|
|
1045
|
+
destinationAgentName:
|
|
1046
|
+
this.agentContexts.get(sourceAgentId)?.name ??
|
|
1047
|
+
sourceAgentId,
|
|
915
1048
|
edgeType: EdgeType.HANDOFF,
|
|
916
1049
|
timestamp: Date.now(),
|
|
917
1050
|
isCompletion: true,
|
|
918
|
-
durationMs
|
|
919
|
-
|
|
1051
|
+
durationMs,
|
|
1052
|
+
spawnKey,
|
|
1053
|
+
spawnPath: childSpawnPath,
|
|
1054
|
+
parentSpawnPath: parentSpawnPath || null,
|
|
1055
|
+
spawnDepth: childDepth,
|
|
920
1056
|
},
|
|
921
|
-
|
|
922
|
-
).catch(() => {
|
|
923
|
-
|
|
924
|
-
|
|
1057
|
+
config
|
|
1058
|
+
).catch(() => {
|
|
1059
|
+
/* best-effort */
|
|
1060
|
+
});
|
|
1061
|
+
return `Agent "${destName}" completed but output was lost due to stream closure.`;
|
|
1062
|
+
}
|
|
925
1063
|
|
|
926
|
-
|
|
1064
|
+
console.error(
|
|
1065
|
+
`[MultiAgentGraph] Handoff FAILED "${sourceAgentId}" -> "${destination}" ` +
|
|
1066
|
+
`spawnKey=${spawnKey} (${durationMs}ms): ${errMsg}`
|
|
1067
|
+
);
|
|
1068
|
+
|
|
1069
|
+
safeDispatchCustomEvent(
|
|
1070
|
+
GraphEvents.ON_AGENT_TRANSITION,
|
|
1071
|
+
{
|
|
1072
|
+
sourceAgentId: destination,
|
|
1073
|
+
sourceAgentName: destName,
|
|
1074
|
+
destinationAgentId: sourceAgentId,
|
|
1075
|
+
destinationAgentName:
|
|
1076
|
+
this.agentContexts.get(sourceAgentId)?.name ??
|
|
1077
|
+
sourceAgentId,
|
|
1078
|
+
edgeType: EdgeType.HANDOFF,
|
|
1079
|
+
timestamp: Date.now(),
|
|
1080
|
+
isCompletion: true,
|
|
1081
|
+
durationMs,
|
|
1082
|
+
spawnKey,
|
|
1083
|
+
spawnPath: childSpawnPath,
|
|
1084
|
+
parentSpawnPath: parentSpawnPath || null,
|
|
1085
|
+
spawnDepth: childDepth,
|
|
1086
|
+
error: errMsg,
|
|
1087
|
+
},
|
|
1088
|
+
config
|
|
1089
|
+
).catch(() => {
|
|
1090
|
+
/* best-effort */
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
return `Agent "${destName}" failed after ${durationMs}ms: ${errMsg}`;
|
|
1094
|
+
}
|
|
927
1095
|
},
|
|
928
1096
|
{
|
|
929
1097
|
name: toolName,
|
|
930
|
-
schema:
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
}
|
|
941
|
-
: { type: 'object', properties: {}, required: [] },
|
|
1098
|
+
schema: {
|
|
1099
|
+
type: 'object',
|
|
1100
|
+
properties: {
|
|
1101
|
+
[promptKey]: {
|
|
1102
|
+
type: 'string',
|
|
1103
|
+
description: promptInputDescription,
|
|
1104
|
+
},
|
|
1105
|
+
},
|
|
1106
|
+
required: [promptKey],
|
|
1107
|
+
},
|
|
942
1108
|
description: toolDescription,
|
|
943
1109
|
}
|
|
944
1110
|
)
|
|
@@ -998,7 +1164,7 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
998
1164
|
/**
|
|
999
1165
|
* Truncate handoff result using head/tail strategy (60/40 split).
|
|
1000
1166
|
* Preserves the beginning (key findings) and end (conclusions).
|
|
1001
|
-
* Matches the TaskTool.truncateResult pattern
|
|
1167
|
+
* Matches the TaskTool.truncateResult pattern used by host orchestrators.
|
|
1002
1168
|
* @param result - The full result text
|
|
1003
1169
|
* @param maxChars - Maximum allowed characters
|
|
1004
1170
|
*/
|
|
@@ -1046,144 +1212,186 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1046
1212
|
* Create a complete agent subgraph (similar to createReactAgent)
|
|
1047
1213
|
*/
|
|
1048
1214
|
private createAgentSubgraph(agentId: string): t.CompiledAgentWorfklow {
|
|
1049
|
-
/**
|
|
1050
|
-
|
|
1215
|
+
/**
|
|
1216
|
+
* Scoped subgraph build for handoff targets.
|
|
1217
|
+
*
|
|
1218
|
+
* If the handoff target has outgoing sequence/transfer edges (e.g. a
|
|
1219
|
+
* "researcher" agent with its own sequence `[researcher → prod_assistant]`),
|
|
1220
|
+
* we compile a mini-StateGraph containing the agent and all agents reachable
|
|
1221
|
+
* from it via non-handoff edges. This way, when the parent hands off to
|
|
1222
|
+
* researcher via `subgraph.invoke()`, the nested sequence runs to completion
|
|
1223
|
+
* before the result is returned to the parent.
|
|
1224
|
+
*
|
|
1225
|
+
* Fast path: if no downstream agents are reachable, fall back to the
|
|
1226
|
+
* previous single-node behavior (`createAgentNode`).
|
|
1227
|
+
*
|
|
1228
|
+
* See docs/multi-agent-nesting-architecture.md §6.
|
|
1229
|
+
*/
|
|
1230
|
+
const reachable = this.computeReachableViaNonHandoff(agentId);
|
|
1231
|
+
if (reachable.size === 1) {
|
|
1232
|
+
return this.createAgentNode(agentId);
|
|
1233
|
+
}
|
|
1234
|
+
mlog(
|
|
1235
|
+
`[MultiAgentGraph] Scoped subgraph for "${agentId}": ${reachable.size} nodes [${Array.from(reachable).join(', ')}]`
|
|
1236
|
+
);
|
|
1237
|
+
return this.buildScopedSubgraph(agentId, reachable);
|
|
1051
1238
|
}
|
|
1052
1239
|
|
|
1053
1240
|
/**
|
|
1054
|
-
*
|
|
1055
|
-
* Returns
|
|
1056
|
-
* source agent, and parallel sibling information extracted from the transfer.
|
|
1057
|
-
*
|
|
1058
|
-
* Supports both single handoffs (last message is the transfer) and parallel handoffs
|
|
1059
|
-
* (multiple transfer ToolMessages, need to find the one targeting this agent).
|
|
1060
|
-
*
|
|
1061
|
-
* @param messages - Current state messages
|
|
1062
|
-
* @param agentId - The agent ID to check for handoff reception
|
|
1063
|
-
* @returns Object with filtered messages, extracted instructions, source agent, and parallel siblings
|
|
1241
|
+
* BFS from `rootAgentId` across sequence + transfer edges (NOT handoff edges).
|
|
1242
|
+
* Returns the set of agents reachable in this agent's "local workflow".
|
|
1064
1243
|
*/
|
|
1244
|
+
private computeReachableViaNonHandoff(rootAgentId: string): Set<string> {
|
|
1245
|
+
const reachable = new Set<string>([rootAgentId]);
|
|
1246
|
+
const queue: string[] = [rootAgentId];
|
|
1247
|
+
const localEdges = [...this.sequenceEdges, ...this.transferEdges];
|
|
1248
|
+
while (queue.length > 0) {
|
|
1249
|
+
const current = queue.shift()!;
|
|
1250
|
+
for (const edge of localEdges) {
|
|
1251
|
+
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
1252
|
+
if (!sources.includes(current)) continue;
|
|
1253
|
+
const dests = Array.isArray(edge.to) ? edge.to : [edge.to];
|
|
1254
|
+
for (const dest of dests) {
|
|
1255
|
+
if (!reachable.has(dest) && this.agentContexts.has(dest)) {
|
|
1256
|
+
reachable.add(dest);
|
|
1257
|
+
queue.push(dest);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
return reachable;
|
|
1263
|
+
}
|
|
1065
1264
|
|
|
1066
1265
|
/**
|
|
1067
|
-
*
|
|
1266
|
+
* Build a compiled scoped StateGraph containing `agentIds` as nodes, rooted
|
|
1267
|
+
* at `rootAgentId`. Linear sequence edges where both endpoints are in scope
|
|
1268
|
+
* are wired directly; nodes with no outgoing in-scope edges route to END.
|
|
1068
1269
|
*
|
|
1069
|
-
*
|
|
1070
|
-
*
|
|
1071
|
-
* for the handoff tool itself, with no matching `tool_result`. Providers
|
|
1072
|
-
* (Bedrock/Anthropic) reject this.
|
|
1073
|
-
* 2. **Paired tool_use/tool_result in history**: The child may not have the same
|
|
1074
|
-
* tools as the parent. Bedrock requires `toolConfig` when tool_use/tool_result
|
|
1075
|
-
* blocks exist in the message history. Compacting these into text summaries
|
|
1076
|
-
* avoids the requirement and reduces context bloat.
|
|
1077
|
-
*
|
|
1078
|
-
* Strategy:
|
|
1079
|
-
* - Remove orphaned tool_use blocks (no matching tool_result)
|
|
1080
|
-
* - Compact paired tool_use/tool_result interactions into text summaries
|
|
1270
|
+
* Each node is wrapped around the per-agent `createAgentNode` compiled
|
|
1271
|
+
* workflow (agent + tools loop) to preserve isolated tool context.
|
|
1081
1272
|
*/
|
|
1082
|
-
|
|
1083
|
-
|
|
1273
|
+
private buildScopedSubgraph(
|
|
1274
|
+
rootAgentId: string,
|
|
1275
|
+
agentIds: Set<string>
|
|
1276
|
+
): t.CompiledAgentWorfklow {
|
|
1277
|
+
const StateAnnotation = Annotation.Root({
|
|
1278
|
+
messages: Annotation<BaseMessage[]>({
|
|
1279
|
+
reducer: messagesStateReducer,
|
|
1280
|
+
default: () => [],
|
|
1281
|
+
}),
|
|
1282
|
+
});
|
|
1283
|
+
const builder = new StateGraph(StateAnnotation);
|
|
1084
1284
|
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1285
|
+
// Precompile each scoped agent's inner workflow and wrap as a node.
|
|
1286
|
+
//
|
|
1287
|
+
// Two different isolation strategies depending on position:
|
|
1288
|
+
//
|
|
1289
|
+
// • ROOT node (the handoff target itself): receives the parent
|
|
1290
|
+
// orchestrator's handoff frame. Use `prepareHandoffMessages` — drops
|
|
1291
|
+
// orphaned tool_use, compacts paired tool calls, guarantees trailing
|
|
1292
|
+
// HumanMessage for Bedrock/VertexAI compatibility. The root needs
|
|
1293
|
+
// orchestrator context because it's responding to the handoff.
|
|
1294
|
+
//
|
|
1295
|
+
// • DOWNSTREAM nodes (sequence targets of the root): run as FULLY
|
|
1296
|
+
// ISOLATED child sessions. They receive only:
|
|
1297
|
+
// [original user request, synthetic HumanMessage describing what
|
|
1298
|
+
// the upstream agent produced and asking them to act]
|
|
1299
|
+
// No raw tool_use / tool_result blocks from the upstream agent —
|
|
1300
|
+
// prevents schema confusion when a downstream agent sees noisy
|
|
1301
|
+
// upstream context and produces malformed tool_use JSON.
|
|
1302
|
+
//
|
|
1303
|
+
// Each wrapper returns only the DELTA (new messages produced by the
|
|
1304
|
+
// inner invoke), not the prepared input — otherwise messagesStateReducer
|
|
1305
|
+
// would double-append the synthetic instruction into the scoped state.
|
|
1306
|
+
for (const aid of agentIds) {
|
|
1307
|
+
const inner = this.createAgentNode(aid);
|
|
1308
|
+
const isRoot = aid === rootAgentId;
|
|
1309
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1310
|
+
builder.addNode(aid as any, async (state: t.BaseGraphState, config) => {
|
|
1311
|
+
const prepared = isRoot
|
|
1312
|
+
? MultiAgentGraph.prepareHandoffMessages(state.messages)
|
|
1313
|
+
: MultiAgentGraph.prepareIsolatedChildMessages(state.messages);
|
|
1314
|
+
mlog(
|
|
1315
|
+
`[MultiAgentGraph] scoped node "${aid}" entering (isRoot=${isRoot}, stateMessages=${state.messages.length}, prepared=${prepared.length})`
|
|
1316
|
+
);
|
|
1317
|
+
const result = await inner.invoke(
|
|
1318
|
+
{ ...state, messages: prepared },
|
|
1319
|
+
config
|
|
1320
|
+
);
|
|
1321
|
+
// Return only the messages the inner node appended beyond its input,
|
|
1322
|
+
// so messagesStateReducer doesn't duplicate the synthetic wrapper
|
|
1323
|
+
// prompt into the scoped state.
|
|
1324
|
+
const delta =
|
|
1325
|
+
result.messages.length > prepared.length
|
|
1326
|
+
? result.messages.slice(prepared.length)
|
|
1327
|
+
: result.messages;
|
|
1328
|
+
return { messages: delta };
|
|
1329
|
+
});
|
|
1094
1330
|
}
|
|
1095
1331
|
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
const cleaned: BaseMessage[] = [];
|
|
1101
|
-
|
|
1102
|
-
for (const msg of messages) {
|
|
1103
|
-
/** Skip all ToolMessages — paired ones will be compacted in pass 2 */
|
|
1104
|
-
if (msg.getType() === 'tool') {
|
|
1105
|
-
continue;
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
if (msg.getType() !== 'ai') {
|
|
1109
|
-
cleaned.push(msg);
|
|
1110
|
-
continue;
|
|
1111
|
-
}
|
|
1332
|
+
// START → root
|
|
1333
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
1334
|
+
// @ts-ignore — LangGraph string typing is too strict for dynamic agent ids
|
|
1335
|
+
builder.addEdge(START, rootAgentId);
|
|
1112
1336
|
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
? (aiMsg.content as { type?: string; text?: string }[])
|
|
1127
|
-
.filter((b) => b.type === 'text' && 'text' in b)
|
|
1128
|
-
.map((b) => b.text ?? '')
|
|
1129
|
-
.join('\n')
|
|
1130
|
-
: '';
|
|
1131
|
-
|
|
1132
|
-
/** Build text summaries of paired tool calls */
|
|
1133
|
-
const toolSummaries: string[] = [];
|
|
1134
|
-
for (const tc of toolCalls) {
|
|
1135
|
-
if (tc.id != null && pairedToolCallIds.has(tc.id)) {
|
|
1136
|
-
/** Find the matching ToolMessage result */
|
|
1137
|
-
const toolResult = messages.find(
|
|
1138
|
-
(m) => m.getType() === 'tool' && (m as ToolMessage).tool_call_id === tc.id
|
|
1139
|
-
) as ToolMessage | undefined;
|
|
1140
|
-
|
|
1141
|
-
const resultContent = toolResult
|
|
1142
|
-
? typeof toolResult.content === 'string'
|
|
1143
|
-
? toolResult.content.slice(0, 500)
|
|
1144
|
-
: '[complex result]'
|
|
1145
|
-
: '[no result]';
|
|
1146
|
-
|
|
1147
|
-
toolSummaries.push(`[Tool "${tc.name}": ${resultContent}]`);
|
|
1337
|
+
// Wire sequence edges in scope (linear chain support)
|
|
1338
|
+
const hasOutgoing = new Set<string>();
|
|
1339
|
+
for (const edge of this.sequenceEdges) {
|
|
1340
|
+
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
1341
|
+
const dests = Array.isArray(edge.to) ? edge.to : [edge.to];
|
|
1342
|
+
for (const source of sources) {
|
|
1343
|
+
if (!agentIds.has(source)) continue;
|
|
1344
|
+
for (const dest of dests) {
|
|
1345
|
+
if (!agentIds.has(dest)) continue;
|
|
1346
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
1347
|
+
// @ts-ignore
|
|
1348
|
+
builder.addEdge(source, dest);
|
|
1349
|
+
hasOutgoing.add(source);
|
|
1148
1350
|
}
|
|
1149
|
-
// Orphaned tool_use blocks (no matching result) are silently dropped
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
/** Reconstruct as plain text AI message (no tool_calls) */
|
|
1153
|
-
const parts = [textContent, ...toolSummaries].filter(Boolean);
|
|
1154
|
-
if (parts.length > 0) {
|
|
1155
|
-
cleaned.push(
|
|
1156
|
-
new AIMessage({
|
|
1157
|
-
content: parts.join('\n\n'),
|
|
1158
|
-
id: aiMsg.id,
|
|
1159
|
-
})
|
|
1160
|
-
);
|
|
1161
1351
|
}
|
|
1162
1352
|
}
|
|
1163
1353
|
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
* context (e.g., compacted tool summaries) while satisfying the API requirement.
|
|
1171
|
-
*/
|
|
1172
|
-
if (cleaned.length > 0 && cleaned[cleaned.length - 1].getType() === 'ai') {
|
|
1173
|
-
const lastAI = cleaned[cleaned.length - 1];
|
|
1174
|
-
const content = typeof lastAI.content === 'string'
|
|
1175
|
-
? lastAI.content
|
|
1176
|
-
: '';
|
|
1177
|
-
if (content.trim()) {
|
|
1178
|
-
cleaned[cleaned.length - 1] = new HumanMessage(
|
|
1179
|
-
`[Context from orchestrator]: ${content}`
|
|
1180
|
-
);
|
|
1181
|
-
} else {
|
|
1182
|
-
cleaned.pop();
|
|
1354
|
+
// Leaves (no outgoing in-scope edges) route to END
|
|
1355
|
+
for (const aid of agentIds) {
|
|
1356
|
+
if (!hasOutgoing.has(aid)) {
|
|
1357
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
1358
|
+
// @ts-ignore
|
|
1359
|
+
builder.addEdge(aid, END);
|
|
1183
1360
|
}
|
|
1184
1361
|
}
|
|
1185
1362
|
|
|
1186
|
-
return
|
|
1363
|
+
return builder.compile(this.compileOptions as unknown as never);
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
/**
|
|
1367
|
+
* Detects if the current agent is receiving a handoff and processes the messages accordingly.
|
|
1368
|
+
* Returns filtered messages with the transfer tool call/message removed, plus any instructions,
|
|
1369
|
+
* source agent, and parallel sibling information extracted from the transfer.
|
|
1370
|
+
*
|
|
1371
|
+
* Supports both single handoffs (last message is the transfer) and parallel handoffs
|
|
1372
|
+
* (multiple transfer ToolMessages, need to find the one targeting this agent).
|
|
1373
|
+
*
|
|
1374
|
+
* @param messages - Current state messages
|
|
1375
|
+
* @param agentId - The agent ID to check for handoff reception
|
|
1376
|
+
* @returns Object with filtered messages, extracted instructions, source agent, and parallel siblings
|
|
1377
|
+
*/
|
|
1378
|
+
|
|
1379
|
+
/**
|
|
1380
|
+
* Prepare messages for a handoff child agent. See
|
|
1381
|
+
* {@link prepareHandoffMessagesUtil} for the full implementation and
|
|
1382
|
+
* semantics — this static method is a thin delegate preserved for
|
|
1383
|
+
* backward compatibility with existing call sites and unit tests.
|
|
1384
|
+
*/
|
|
1385
|
+
static prepareHandoffMessages(messages: BaseMessage[]): BaseMessage[] {
|
|
1386
|
+
return prepareHandoffMessagesUtil(messages);
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
/**
|
|
1390
|
+
* Build an isolated message context for a downstream scoped-subgraph
|
|
1391
|
+
* node. See {@link prepareIsolatedChildMessagesUtil} for details.
|
|
1392
|
+
*/
|
|
1393
|
+
static prepareIsolatedChildMessages(messages: BaseMessage[]): BaseMessage[] {
|
|
1394
|
+
return prepareIsolatedChildMessagesUtil(messages);
|
|
1187
1395
|
}
|
|
1188
1396
|
|
|
1189
1397
|
private processTransferReception(
|
|
@@ -1227,7 +1435,8 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1227
1435
|
destinationAgent = toolName.replace(Constants.LC_TRANSFER_TO_, '');
|
|
1228
1436
|
} else if (isConditionalTransfer) {
|
|
1229
1437
|
const transferDest = candidateMsg.additional_kwargs.handoff_destination;
|
|
1230
|
-
destinationAgent =
|
|
1438
|
+
destinationAgent =
|
|
1439
|
+
typeof transferDest === 'string' ? transferDest : null;
|
|
1231
1440
|
}
|
|
1232
1441
|
|
|
1233
1442
|
/** Check if this transfer targets our agent */
|
|
@@ -1529,8 +1738,36 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1529
1738
|
handoffOnlyDestinations.delete(startNode);
|
|
1530
1739
|
}
|
|
1531
1740
|
|
|
1741
|
+
/**
|
|
1742
|
+
* Nested-sequence expansion: for each handoff-only target, its downstream
|
|
1743
|
+
* sequence/transfer agents MUST also become handoff-only — they exist only
|
|
1744
|
+
* inside the target's scoped subgraph, not at top level. Without this,
|
|
1745
|
+
* those downstream nodes would be added as top-level orphans and LangGraph
|
|
1746
|
+
* would fail compilation (UNREACHABLE_NODE).
|
|
1747
|
+
*
|
|
1748
|
+
* See docs/multi-agent-nesting-architecture.md §6.
|
|
1749
|
+
*/
|
|
1750
|
+
const nestedHandoffOnly = new Set<string>();
|
|
1751
|
+
for (const target of handoffOnlyDestinations) {
|
|
1752
|
+
const reachable = this.computeReachableViaNonHandoff(target);
|
|
1753
|
+
for (const agent of reachable) {
|
|
1754
|
+
if (agent === target) continue;
|
|
1755
|
+
// Skip if this agent is legitimately a top-level starting node
|
|
1756
|
+
if (this.startingNodes.has(agent)) continue;
|
|
1757
|
+
nestedHandoffOnly.add(agent);
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
for (const agent of nestedHandoffOnly) {
|
|
1761
|
+
handoffOnlyDestinations.add(agent);
|
|
1762
|
+
}
|
|
1763
|
+
if (nestedHandoffOnly.size > 0) {
|
|
1764
|
+
mlog(
|
|
1765
|
+
`[MultiAgentGraph] Nested handoff-only (scoped subgraph downstream): [${Array.from(nestedHandoffOnly).join(', ')}]`
|
|
1766
|
+
);
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1532
1769
|
if (handoffOnlyDestinations.size > 0) {
|
|
1533
|
-
|
|
1770
|
+
mlog(
|
|
1534
1771
|
`[MultiAgentGraph] Handoff-only children (subgraph only, no top-level node): [${Array.from(handoffOnlyDestinations).join(', ')}]`
|
|
1535
1772
|
);
|
|
1536
1773
|
}
|
|
@@ -1593,7 +1830,7 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1593
1830
|
state: t.MultiAgentGraphState,
|
|
1594
1831
|
config?: LangGraphRunnableConfig
|
|
1595
1832
|
): Promise<t.MultiAgentGraphState | Command> => {
|
|
1596
|
-
|
|
1833
|
+
mlog(
|
|
1597
1834
|
`[MultiAgentGraph] Agent "${agentId}" wrapper ENTRY (messages: ${state.messages.length}, needsCommandRouting: ${needsCommandRouting})`
|
|
1598
1835
|
);
|
|
1599
1836
|
let result: t.MultiAgentGraphState;
|
|
@@ -1616,7 +1853,7 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1616
1853
|
sourceAgentName,
|
|
1617
1854
|
parallelSiblings,
|
|
1618
1855
|
} = transferContext;
|
|
1619
|
-
|
|
1856
|
+
mlog(
|
|
1620
1857
|
`[MultiAgentGraph] Agent "${agentId}" receiving transfer from "${sourceAgentName}" (instructions: ${instructions != null}, parallelSiblings: ${parallelSiblings.length})`
|
|
1621
1858
|
);
|
|
1622
1859
|
|
|
@@ -1758,7 +1995,7 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1758
1995
|
/** Track the last agent that produced output for continuation support */
|
|
1759
1996
|
this.lastActiveAgentId = agentId;
|
|
1760
1997
|
|
|
1761
|
-
|
|
1998
|
+
mlog(
|
|
1762
1999
|
`[MultiAgentGraph] Agent "${agentId}" wrapper EXIT (result messages: ${result.messages.length})`
|
|
1763
2000
|
);
|
|
1764
2001
|
|
|
@@ -1779,7 +2016,7 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1779
2016
|
Constants.LC_TRANSFER_TO_,
|
|
1780
2017
|
''
|
|
1781
2018
|
);
|
|
1782
|
-
|
|
2019
|
+
mlog(
|
|
1783
2020
|
`[MultiAgentGraph] Command routing: "${agentId}" -> transfer to "${transferDest}" (sequence edges skipped: [${Array.from(sequenceDestinations).join(', ')}])`
|
|
1784
2021
|
);
|
|
1785
2022
|
|
|
@@ -1818,7 +2055,7 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1818
2055
|
const receiverBudget = receiverContext.maxContextTokens;
|
|
1819
2056
|
|
|
1820
2057
|
if (currentSize > receiverBudget * 0.7) {
|
|
1821
|
-
|
|
2058
|
+
mwarn(
|
|
1822
2059
|
`[MultiAgentGraph] Pre-handoff compaction: context (${currentSize} tokens) exceeds ` +
|
|
1823
2060
|
`70% of receiver "${transferDest}" budget (${receiverBudget} tokens)`
|
|
1824
2061
|
);
|
|
@@ -1897,9 +2134,11 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1897
2134
|
GraphEvents.ON_AGENT_TRANSITION,
|
|
1898
2135
|
{
|
|
1899
2136
|
sourceAgentId: agentId,
|
|
1900
|
-
sourceAgentName:
|
|
2137
|
+
sourceAgentName:
|
|
2138
|
+
this.agentContexts.get(agentId)?.name ?? agentId,
|
|
1901
2139
|
destinationAgentId: transferDest,
|
|
1902
|
-
destinationAgentName:
|
|
2140
|
+
destinationAgentName:
|
|
2141
|
+
this.agentContexts.get(transferDest)?.name ?? transferDest,
|
|
1903
2142
|
edgeType: EdgeType.TRANSFER,
|
|
1904
2143
|
timestamp: Date.now(),
|
|
1905
2144
|
},
|
|
@@ -1912,7 +2151,7 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1912
2151
|
});
|
|
1913
2152
|
} else {
|
|
1914
2153
|
/** No transfer - proceed with sequence edges */
|
|
1915
|
-
|
|
2154
|
+
mlog(
|
|
1916
2155
|
`[MultiAgentGraph] Command routing: "${agentId}" -> no transfer, following sequence edges: [${Array.from(sequenceDestinations).join(', ')}]`
|
|
1917
2156
|
);
|
|
1918
2157
|
const directDests = Array.from(sequenceDestinations);
|
|
@@ -1921,9 +2160,11 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1921
2160
|
GraphEvents.ON_AGENT_TRANSITION,
|
|
1922
2161
|
{
|
|
1923
2162
|
sourceAgentId: agentId,
|
|
1924
|
-
sourceAgentName:
|
|
2163
|
+
sourceAgentName:
|
|
2164
|
+
this.agentContexts.get(agentId)?.name ?? agentId,
|
|
1925
2165
|
destinationAgentId: dest,
|
|
1926
|
-
destinationAgentName:
|
|
2166
|
+
destinationAgentName:
|
|
2167
|
+
this.agentContexts.get(dest)?.name ?? dest,
|
|
1927
2168
|
edgeType: EdgeType.SEQUENCE,
|
|
1928
2169
|
timestamp: Date.now(),
|
|
1929
2170
|
},
|
|
@@ -1950,17 +2191,24 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1950
2191
|
* destinations so callbacks.js can register child agents for event
|
|
1951
2192
|
* isolation BEFORE they start streaming.
|
|
1952
2193
|
*/
|
|
1953
|
-
const allDests = new Set([
|
|
2194
|
+
const allDests = new Set([
|
|
2195
|
+
...transferDestinations,
|
|
2196
|
+
...sequenceDestinations,
|
|
2197
|
+
]);
|
|
1954
2198
|
if (allDests.size > 0) {
|
|
1955
|
-
const edgeType = hasTransferEdges
|
|
2199
|
+
const edgeType = hasTransferEdges
|
|
2200
|
+
? EdgeType.TRANSFER
|
|
2201
|
+
: EdgeType.SEQUENCE;
|
|
1956
2202
|
for (const dest of allDests) {
|
|
1957
2203
|
await safeDispatchCustomEvent(
|
|
1958
2204
|
GraphEvents.ON_AGENT_TRANSITION,
|
|
1959
2205
|
{
|
|
1960
2206
|
sourceAgentId: agentId,
|
|
1961
|
-
sourceAgentName:
|
|
2207
|
+
sourceAgentName:
|
|
2208
|
+
this.agentContexts.get(agentId)?.name ?? agentId,
|
|
1962
2209
|
destinationAgentId: dest,
|
|
1963
|
-
destinationAgentName:
|
|
2210
|
+
destinationAgentName:
|
|
2211
|
+
this.agentContexts.get(dest)?.name ?? dest,
|
|
1964
2212
|
edgeType,
|
|
1965
2213
|
timestamp: Date.now(),
|
|
1966
2214
|
},
|
|
@@ -1994,7 +2242,7 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1994
2242
|
|
|
1995
2243
|
if (validResumeAgent) {
|
|
1996
2244
|
const resumeAgentId = this.resumeFromAgentId!;
|
|
1997
|
-
|
|
2245
|
+
mlog(
|
|
1998
2246
|
`[MultiAgentGraph] Multi-turn resumption: routing START → "${resumeAgentId}" (skipping default starting nodes: [${Array.from(this.startingNodes).join(', ')}])`
|
|
1999
2247
|
);
|
|
2000
2248
|
|
|
@@ -2003,10 +2251,7 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
2003
2251
|
* nodes. This is required by LangGraph — all possible destinations must
|
|
2004
2252
|
* be declared even if the router always picks one.
|
|
2005
2253
|
*/
|
|
2006
|
-
const allPossibleStarts = new Set([
|
|
2007
|
-
...this.startingNodes,
|
|
2008
|
-
resumeAgentId,
|
|
2009
|
-
]);
|
|
2254
|
+
const allPossibleStarts = new Set([...this.startingNodes, resumeAgentId]);
|
|
2010
2255
|
const routeMap: Record<string, string> = {};
|
|
2011
2256
|
for (const nodeId of allPossibleStarts) {
|
|
2012
2257
|
routeMap[nodeId] = nodeId;
|
|
@@ -2019,7 +2264,7 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
2019
2264
|
);
|
|
2020
2265
|
} else {
|
|
2021
2266
|
if (this.resumeFromAgentId != null) {
|
|
2022
|
-
|
|
2267
|
+
mwarn(
|
|
2023
2268
|
`[MultiAgentGraph] resumeFromAgentId "${this.resumeFromAgentId}" not found in graph — falling back to default starting nodes`
|
|
2024
2269
|
);
|
|
2025
2270
|
}
|
|
@@ -2054,7 +2299,7 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
2054
2299
|
const gateNode = createApprovalGateNode(
|
|
2055
2300
|
edge.approvalGate,
|
|
2056
2301
|
source,
|
|
2057
|
-
dest
|
|
2302
|
+
dest
|
|
2058
2303
|
);
|
|
2059
2304
|
|
|
2060
2305
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
@@ -2099,8 +2344,21 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
2099
2344
|
if (gatedEdges.has(edge)) {
|
|
2100
2345
|
continue;
|
|
2101
2346
|
}
|
|
2102
|
-
|
|
2103
|
-
|
|
2347
|
+
/**
|
|
2348
|
+
* Skip sequence edges where either endpoint lives only inside a scoped
|
|
2349
|
+
* handoff subgraph. Those edges are wired inside `buildScopedSubgraph`,
|
|
2350
|
+
* not at the top level — adding them here would reference non-existent
|
|
2351
|
+
* top-level nodes and fail compilation.
|
|
2352
|
+
*/
|
|
2353
|
+
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
2354
|
+
const dests = Array.isArray(edge.to) ? edge.to : [edge.to];
|
|
2355
|
+
const anyEndpointHandoffOnly = [...sources, ...dests].some((n) =>
|
|
2356
|
+
handoffOnlyDestinations.has(n)
|
|
2357
|
+
);
|
|
2358
|
+
if (anyEndpointHandoffOnly) {
|
|
2359
|
+
continue;
|
|
2360
|
+
}
|
|
2361
|
+
for (const destination of dests) {
|
|
2104
2362
|
if (!edgesByDestination.has(destination)) {
|
|
2105
2363
|
edgesByDestination.set(destination, []);
|
|
2106
2364
|
}
|
|
@@ -2208,7 +2466,10 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
2208
2466
|
});
|
|
2209
2467
|
|
|
2210
2468
|
/** Skip adding edge if source uses Command routing (has both types) */
|
|
2211
|
-
if (
|
|
2469
|
+
if (
|
|
2470
|
+
sourceTransferEdges.length > 0 &&
|
|
2471
|
+
sourceSequenceEdges.length > 0
|
|
2472
|
+
) {
|
|
2212
2473
|
continue;
|
|
2213
2474
|
}
|
|
2214
2475
|
|