@illuma-ai/agents 1.1.28 → 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.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 +84 -33
- 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 +113 -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/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 +84 -33
- 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 +20 -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/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/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 +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 +90 -47
- 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__/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 -7
- 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
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import { tool } from '@langchain/core/tools';
|
|
2
2
|
import { PromptTemplate } from '@langchain/core/prompts';
|
|
3
3
|
import { ToolMessage, AIMessage, HumanMessage, getBufferString, SystemMessage } from '@langchain/core/messages';
|
|
4
|
-
import { getCurrentTaskInput, Command, Annotation, messagesStateReducer, StateGraph,
|
|
4
|
+
import { getCurrentTaskInput, Command, Annotation, messagesStateReducer, StateGraph, START, END } from '@langchain/langgraph';
|
|
5
5
|
import '../messages/core.mjs';
|
|
6
6
|
import 'nanoid';
|
|
7
7
|
import { EdgeType, Constants, DEFAULT_HANDOFF_MAX_RESULT_CHARS, GraphEvents } from '../common/enum.mjs';
|
|
8
|
+
import { buildSpawnPath, spawnPathDepth } from '../common/spawnPath.mjs';
|
|
8
9
|
import '../tools/approval/constants.mjs';
|
|
9
10
|
import '../utils/toonFormat.mjs';
|
|
10
11
|
import { summarize, createEmergencySummary } from '../messages/summarize.mjs';
|
|
11
12
|
import { StandardGraph } from './Graph.mjs';
|
|
12
13
|
import { safeDispatchCustomEvent } from '../utils/events.mjs';
|
|
14
|
+
import { mlog, mwarn } from '../utils/logging.mjs';
|
|
15
|
+
import { prepareHandoffMessages, prepareIsolatedChildMessages } from '../utils/childAgentContext.mjs';
|
|
13
16
|
import { getApprovalGateNodeId, createApprovalGateNode } from '../nodes/ApprovalGateNode.mjs';
|
|
14
|
-
import { HandoffRegistry } from './HandoffRegistry.mjs';
|
|
15
17
|
|
|
18
|
+
// HandoffRegistry no longer needed — handoff tools use synchronous
|
|
19
|
+
// browser-tool callback pattern (spawn → wait → return result)
|
|
16
20
|
/** Pattern to extract instructions from transfer ToolMessage content */
|
|
17
21
|
const TRANSFER_INSTRUCTIONS_PATTERN = /(?:Instructions?|Context):\s*(.+)/is;
|
|
18
22
|
/**
|
|
@@ -61,10 +65,10 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
61
65
|
lastActiveAgentId;
|
|
62
66
|
/**
|
|
63
67
|
* Registry for async handoff execution.
|
|
64
|
-
* Enables
|
|
68
|
+
* Enables autonomous orchestration: spawn children non-blocking,
|
|
65
69
|
* orchestrator stays alive to reason and collect results when ready.
|
|
66
70
|
*/
|
|
67
|
-
|
|
71
|
+
// HandoffRegistry removed — handoff tools are synchronous (callback pattern)
|
|
68
72
|
/**
|
|
69
73
|
* When set, the graph routes START to this agent instead of the default starting nodes.
|
|
70
74
|
* Enables multi-turn resumption: follow-up messages go to the agent that last handled
|
|
@@ -79,7 +83,7 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
79
83
|
this.analyzeGraph();
|
|
80
84
|
this.createTransferTools();
|
|
81
85
|
this.createHandoffTools();
|
|
82
|
-
|
|
86
|
+
mlog(`[MultiAgentGraph] Constructor complete: ${this.agentContexts.size} agents, ${this.edges.length} edges`);
|
|
83
87
|
}
|
|
84
88
|
/**
|
|
85
89
|
* Categorize edges into handoff, transfer, and sequence types
|
|
@@ -92,7 +96,8 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
92
96
|
else if (edge.edgeType === EdgeType.SEQUENCE) {
|
|
93
97
|
this.sequenceEdges.push(edge);
|
|
94
98
|
}
|
|
95
|
-
else if (edge.edgeType === EdgeType.TRANSFER ||
|
|
99
|
+
else if (edge.edgeType === EdgeType.TRANSFER ||
|
|
100
|
+
edge.condition != null) {
|
|
96
101
|
this.transferEdges.push(edge);
|
|
97
102
|
}
|
|
98
103
|
else {
|
|
@@ -109,7 +114,7 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
109
114
|
}
|
|
110
115
|
}
|
|
111
116
|
}
|
|
112
|
-
|
|
117
|
+
mlog(`[MultiAgentGraph] Edge categorization: ${this.handoffEdges.length} handoff, ${this.transferEdges.length} transfer, ${this.sequenceEdges.length} sequence (of ${this.edges.length} total)`);
|
|
113
118
|
}
|
|
114
119
|
/**
|
|
115
120
|
* Analyze graph structure to determine starting nodes and connections
|
|
@@ -131,7 +136,7 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
131
136
|
if (this.startingNodes.size === 0 && this.agentContexts.size > 0) {
|
|
132
137
|
this.startingNodes.add(this.agentContexts.keys().next().value);
|
|
133
138
|
}
|
|
134
|
-
|
|
139
|
+
mlog(`[MultiAgentGraph] Starting nodes identified: [${Array.from(this.startingNodes).join(', ')}]`);
|
|
135
140
|
// Determine if graph has parallel execution capability
|
|
136
141
|
this.computeParallelCapability();
|
|
137
142
|
}
|
|
@@ -266,7 +271,7 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
266
271
|
agentContext.graphTools = [];
|
|
267
272
|
}
|
|
268
273
|
agentContext.graphTools.push(...transferTools);
|
|
269
|
-
|
|
274
|
+
mlog(`[MultiAgentGraph] Transfer tools for "${agentId}": [${transferTools.map((t) => t.name).join(', ')}]`);
|
|
270
275
|
// Inject orchestration guidance for agents with transfer tools
|
|
271
276
|
const childDescs = edges.flatMap((e) => {
|
|
272
277
|
const dests = Array.isArray(e.to) ? e.to : [e.to];
|
|
@@ -300,7 +305,9 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
300
305
|
const toolDescription = edge.description ?? 'Conditionally transfer control based on state';
|
|
301
306
|
/** Check if we have a prompt for handoff input */
|
|
302
307
|
const hasTransferInput = edge.prompt != null && typeof edge.prompt === 'string';
|
|
303
|
-
const transferInputDescription = hasTransferInput
|
|
308
|
+
const transferInputDescription = hasTransferInput
|
|
309
|
+
? edge.prompt
|
|
310
|
+
: undefined;
|
|
304
311
|
const promptKey = edge.promptKey ?? 'instructions';
|
|
305
312
|
tools.push(tool(async (rawInput, config) => {
|
|
306
313
|
const input = rawInput;
|
|
@@ -479,10 +486,14 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
479
486
|
* Builds orchestration guidance injected into the system message of agents
|
|
480
487
|
* that have handoff or transfer tools (i.e., orchestrator agents).
|
|
481
488
|
*
|
|
482
|
-
*
|
|
483
|
-
* -
|
|
484
|
-
* - Multi-round execution for dependent tasks
|
|
485
|
-
*
|
|
489
|
+
* Implements two orchestration primitives:
|
|
490
|
+
* - Execution bias guidance injected into the system prompt
|
|
491
|
+
* - Multi-round autonomous execution for dependent tasks
|
|
492
|
+
*
|
|
493
|
+
* Handoff tools are synchronous (browser-tool callback pattern): spawn the
|
|
494
|
+
* child, await completion, return the real text as the tool output. Parallel
|
|
495
|
+
* handoff tool calls in one turn run concurrently via LangGraph's ToolNode,
|
|
496
|
+
* so independent children run in parallel without explicit orchestration.
|
|
486
497
|
*
|
|
487
498
|
* @param childDescs - Display names (with optional descriptions) of child agents
|
|
488
499
|
* @param toolCount - Number of handoff/transfer tools available
|
|
@@ -494,20 +505,58 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
494
505
|
`You have ${toolCount} specialist agent(s) available for delegation:`,
|
|
495
506
|
...childDescs.map((d) => `- ${d}`),
|
|
496
507
|
'',
|
|
497
|
-
'
|
|
498
|
-
'
|
|
499
|
-
'
|
|
500
|
-
'
|
|
501
|
-
'
|
|
502
|
-
'
|
|
508
|
+
'## Execution Bias',
|
|
509
|
+
'If the user asks you to do the work, start doing it in the same turn.',
|
|
510
|
+
'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.',
|
|
511
|
+
'Commentary-only turns are incomplete when tools are available and the next action is clear.',
|
|
512
|
+
'If the work will take multiple steps or a while to finish, send one short progress update before or while acting.',
|
|
513
|
+
'',
|
|
514
|
+
'## How Delegation Works',
|
|
515
|
+
'Each handoff tool call spawns a sub-agent, waits for it to complete, and returns the real result directly — like a function call.',
|
|
516
|
+
'Independent tasks MAY be called in parallel (multiple handoff tool calls in one turn). They run concurrently and all results return together.',
|
|
517
|
+
'Dependent tasks MUST be sequential: call one agent, get the result, then call the next agent using that real data.',
|
|
518
|
+
'',
|
|
519
|
+
'## Agent Isolation',
|
|
520
|
+
"Sub-agents CANNOT see your conversation or the user's original message. They ONLY see what you write in the `instructions` parameter.",
|
|
521
|
+
"When writing instructions, include ALL data the agent needs. Copy exact values from the user's message — email addresses, names, URLs, dates, numbers.",
|
|
522
|
+
'When delegating follow-up work, include the real data from prior agent results directly in the instructions text.',
|
|
523
|
+
'Do NOT re-delegate a task that was already completed. If you have the data, pass it directly.',
|
|
524
|
+
].join('\n');
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Builds subagent context instructions injected into child agents that are
|
|
528
|
+
* handoff destinations. This tells the child agent it is a subagent with
|
|
529
|
+
* a focused task.
|
|
530
|
+
*
|
|
531
|
+
* @param orchestratorName - Display name of the parent orchestrator agent
|
|
532
|
+
*/
|
|
533
|
+
buildChildAgentContext(orchestratorName) {
|
|
534
|
+
return [
|
|
535
|
+
'# Subagent Context',
|
|
536
|
+
'',
|
|
537
|
+
`You are a **subagent** spawned by the ${orchestratorName} for a specific task.`,
|
|
538
|
+
'',
|
|
539
|
+
'## Your Role',
|
|
540
|
+
"- Complete this task. That's your entire purpose.",
|
|
541
|
+
`- You are NOT the ${orchestratorName}. Don't try to be.`,
|
|
503
542
|
'',
|
|
504
|
-
'
|
|
505
|
-
'-
|
|
506
|
-
|
|
507
|
-
'
|
|
508
|
-
|
|
509
|
-
'
|
|
510
|
-
'
|
|
543
|
+
'## Rules',
|
|
544
|
+
'1. **Stay focused** - Do your assigned task, nothing else',
|
|
545
|
+
`2. **Complete the task** - Your final message will be automatically reported to the ${orchestratorName}`,
|
|
546
|
+
"3. **Don't initiate** - No heartbeats, no proactive actions, no side quests",
|
|
547
|
+
"4. **Be ephemeral** - You may be terminated after task completion. That's fine.",
|
|
548
|
+
'',
|
|
549
|
+
'## Output Format',
|
|
550
|
+
'When complete, your final response should include:',
|
|
551
|
+
'- What you accomplished or found',
|
|
552
|
+
`- Any relevant details the ${orchestratorName} should know`,
|
|
553
|
+
'- Keep it concise but informative',
|
|
554
|
+
'',
|
|
555
|
+
"## What You DON'T Do",
|
|
556
|
+
`- NO user conversations (that's ${orchestratorName}'s job)`,
|
|
557
|
+
'- NO external messages (email, tweets, etc.) unless explicitly tasked with a specific recipient/channel',
|
|
558
|
+
'- NO cron jobs or persistent state',
|
|
559
|
+
`- NO pretending to be the ${orchestratorName}`,
|
|
511
560
|
].join('\n');
|
|
512
561
|
}
|
|
513
562
|
/**
|
|
@@ -557,70 +606,11 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
557
606
|
agentContext.graphTools = [];
|
|
558
607
|
}
|
|
559
608
|
agentContext.graphTools.push(...handoffTools);
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
*/
|
|
565
|
-
const handoffReg = this.handoffRegistry;
|
|
566
|
-
agentContext.graphTools.push(tool(async () => {
|
|
567
|
-
if (!handoffReg.hasPending() && handoffReg.size === 0) {
|
|
568
|
-
return 'No agents have been spawned yet.';
|
|
569
|
-
}
|
|
570
|
-
/** Wait for all pending handoffs to complete */
|
|
571
|
-
const records = await handoffReg.waitForAll();
|
|
572
|
-
const parts = [];
|
|
573
|
-
for (const record of records) {
|
|
574
|
-
if (record.status === 'completed') {
|
|
575
|
-
parts.push(`## ${record.name} (completed in ${record.durationMs}ms)\n${record.resultText}`);
|
|
576
|
-
}
|
|
577
|
-
else if (record.status === 'failed') {
|
|
578
|
-
parts.push(`## ${record.name} (FAILED after ${record.durationMs}ms)\nError: ${record.error}`);
|
|
579
|
-
}
|
|
580
|
-
else {
|
|
581
|
-
parts.push(`## ${record.name} (still running, ${Date.now() - record.spawnedAt}ms elapsed)`);
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
return parts.join('\n\n---\n\n');
|
|
585
|
-
}, {
|
|
586
|
-
name: 'collect_results',
|
|
587
|
-
schema: { type: 'object', properties: {}, required: [] },
|
|
588
|
-
description: 'Wait for all spawned agents to complete and collect their results. ' +
|
|
589
|
-
'Call this after spawning one or more agents to get their output.',
|
|
590
|
-
}), tool(async () => {
|
|
591
|
-
const all = handoffReg.listAll();
|
|
592
|
-
if (all.length === 0) {
|
|
593
|
-
return 'No agents tracked.';
|
|
594
|
-
}
|
|
595
|
-
const lines = all.map((r) => {
|
|
596
|
-
const elapsed = Date.now() - r.spawnedAt;
|
|
597
|
-
if (r.status === 'running') {
|
|
598
|
-
return `- **${r.name}**: running (${elapsed}ms elapsed) — task: ${r.task.substring(0, 100)}`;
|
|
599
|
-
}
|
|
600
|
-
else if (r.status === 'completed') {
|
|
601
|
-
return `- **${r.name}**: completed (${r.durationMs}ms, ${r.resultText?.length ?? 0} chars)`;
|
|
602
|
-
}
|
|
603
|
-
else {
|
|
604
|
-
return `- **${r.name}**: failed (${r.durationMs}ms) — ${r.error}`;
|
|
605
|
-
}
|
|
606
|
-
});
|
|
607
|
-
const pending = all.filter((r) => r.status === 'running').length;
|
|
608
|
-
const completed = all.filter((r) => r.status === 'completed').length;
|
|
609
|
-
const failed = all.filter((r) => r.status === 'failed').length;
|
|
610
|
-
return [
|
|
611
|
-
`**Agent Status**: ${pending} running, ${completed} completed, ${failed} failed`,
|
|
612
|
-
'',
|
|
613
|
-
...lines,
|
|
614
|
-
].join('\n');
|
|
615
|
-
}, {
|
|
616
|
-
name: 'check_agents',
|
|
617
|
-
schema: { type: 'object', properties: {}, required: [] },
|
|
618
|
-
description: 'Check the status of all spawned agents without waiting. ' +
|
|
619
|
-
'Shows which agents are running, completed, or failed.',
|
|
620
|
-
}));
|
|
621
|
-
console.debug(`[MultiAgentGraph] Handoff tools for "${agentId}": [${handoffTools.map((t) => t.name).join(', ')}, collect_results, check_agents]`);
|
|
609
|
+
// No collect_results tool needed — handoff tools use the browser-tool
|
|
610
|
+
// callback pattern: spawn child, wait for completion, return real result.
|
|
611
|
+
// The LLM naturally gets child results as tool return values.
|
|
612
|
+
mlog(`[MultiAgentGraph] Handoff tools for "${agentId}": [${handoffTools.map((t) => t.name).join(', ')}]`);
|
|
622
613
|
// Inject autonomous orchestration guidance for agents with handoff tools.
|
|
623
|
-
// Modeled after OpenClaw's battle-tested subagent orchestration patterns.
|
|
624
614
|
const childDescs = edges.flatMap((e) => {
|
|
625
615
|
const dests = Array.isArray(e.to) ? e.to : [e.to];
|
|
626
616
|
return dests.map((d) => {
|
|
@@ -635,14 +625,34 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
635
625
|
agentContext.additionalInstructions = existing
|
|
636
626
|
? `${existing}\n\n${orchestrationGuidance}`
|
|
637
627
|
: orchestrationGuidance;
|
|
628
|
+
// Inject subagent context into each child/destination agent.
|
|
629
|
+
// This tells child agents they are subagents with a focused task — stay focused,
|
|
630
|
+
// execute (don't plan), and return results to the orchestrator.
|
|
631
|
+
const orchestratorName = agentContext.name ?? agentId;
|
|
632
|
+
const childAgentContext = this.buildChildAgentContext(orchestratorName);
|
|
633
|
+
for (const edge of edges) {
|
|
634
|
+
const dests = Array.isArray(edge.to) ? edge.to : [edge.to];
|
|
635
|
+
for (const destId of dests) {
|
|
636
|
+
const destCtx = this.agentContexts.get(destId);
|
|
637
|
+
if (!destCtx)
|
|
638
|
+
continue;
|
|
639
|
+
const existingChild = destCtx.additionalInstructions ?? '';
|
|
640
|
+
// Avoid duplicate injection if agent is destination of multiple edges
|
|
641
|
+
if (existingChild.includes('# Subagent Context'))
|
|
642
|
+
continue;
|
|
643
|
+
destCtx.additionalInstructions = existingChild
|
|
644
|
+
? `${existingChild}\n\n${childAgentContext}`
|
|
645
|
+
: childAgentContext;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
638
648
|
}
|
|
639
649
|
}
|
|
640
650
|
/**
|
|
641
651
|
* Create handoff tools for an edge (handles multiple destinations).
|
|
642
652
|
* Each handoff tool spawns the child agent's compiled subgraph asynchronously
|
|
643
653
|
* and returns immediately. The orchestrator uses `collect_results` to retrieve
|
|
644
|
-
* outputs and `check_agents` to monitor status —
|
|
645
|
-
*
|
|
654
|
+
* outputs and `check_agents` to monitor status — a push-based autonomous
|
|
655
|
+
* orchestration pattern.
|
|
646
656
|
*
|
|
647
657
|
* @param edge - The graph edge defining the handoff
|
|
648
658
|
* @param sourceAgentId - The ID of the parent/supervisor agent
|
|
@@ -656,12 +666,19 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
656
666
|
const destContext = this.agentContexts.get(destination);
|
|
657
667
|
const toolDescription = edge.description ??
|
|
658
668
|
this.buildDefaultHandoffDescription(destContext, destination);
|
|
659
|
-
|
|
660
|
-
|
|
669
|
+
/**
|
|
670
|
+
* Always include an instructions parameter so the orchestrator can
|
|
671
|
+
* pass scoped task descriptions to each child agent. Without this,
|
|
672
|
+
* the child gets no context about what to do.
|
|
673
|
+
*/
|
|
661
674
|
const promptKey = edge.promptKey ?? 'instructions';
|
|
662
|
-
|
|
675
|
+
const destDesc = destContext?.description;
|
|
676
|
+
const promptInputDescription = edge.prompt ??
|
|
677
|
+
(destDesc
|
|
678
|
+
? `Task instructions for this agent (${destDesc}). Describe exactly what it should do.`
|
|
679
|
+
: 'Specific task instructions for this agent. Describe exactly what it should do and what data to use.');
|
|
680
|
+
/** Capture registry reference — Map populated in createWorkflow() */
|
|
663
681
|
const subgraphReg = this.subgraphRegistry;
|
|
664
|
-
const handoffReg = this.handoffRegistry;
|
|
665
682
|
tools.push(tool(async (rawInput, config) => {
|
|
666
683
|
const input = rawInput;
|
|
667
684
|
const subgraph = subgraphReg.get(destination);
|
|
@@ -669,16 +686,62 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
669
686
|
throw new Error(`Handoff target "${destination}" subgraph not found in registry. ` +
|
|
670
687
|
'This is a bug: createWorkflow() should have populated the subgraph registry.');
|
|
671
688
|
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
689
|
+
/**
|
|
690
|
+
* Per-spawn unique key = the orchestrator's tool_call.id.
|
|
691
|
+
* LangChain's ToolNode passes `config.toolCall.id` to the tool
|
|
692
|
+
* function; this is the same id the frontend sees on the parent's
|
|
693
|
+
* handoff content part, so the UI can match each AgentHandoff
|
|
694
|
+
* indicator to its own sidebar task without collision when the
|
|
695
|
+
* same agent is invoked multiple times.
|
|
696
|
+
*/
|
|
697
|
+
const toolCallCfg = config?.toolCall;
|
|
698
|
+
const spawnKey = toolCallCfg?.id ??
|
|
699
|
+
`${destination}_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
700
|
+
/**
|
|
701
|
+
* Hierarchical spawnPath: parent's spawnPath (from metadata) + this spawnKey.
|
|
702
|
+
* Root invocations have empty parentSpawnPath. Threaded through childConfig
|
|
703
|
+
* so nested handoffs/sequences inherit the full ancestry.
|
|
704
|
+
* See docs/multi-agent-nesting-architecture.md §4.
|
|
705
|
+
*/
|
|
706
|
+
const parentMetadata = config?.metadata;
|
|
707
|
+
const parentSpawnPath = typeof parentMetadata?.spawnPath === 'string'
|
|
708
|
+
? parentMetadata.spawnPath
|
|
709
|
+
: '';
|
|
710
|
+
const childSpawnPath = buildSpawnPath(parentSpawnPath, spawnKey);
|
|
711
|
+
const childDepth = spawnPathDepth(childSpawnPath);
|
|
712
|
+
/**
|
|
713
|
+
* Child agent message construction — three modes:
|
|
714
|
+
*
|
|
715
|
+
* 1. Default (isolated-session pattern): Child gets ONLY the orchestrator's
|
|
716
|
+
* task instructions as a single HumanMessage. No parent conversation
|
|
717
|
+
* leaks. Orchestrator controls exactly what context the child sees.
|
|
718
|
+
*
|
|
719
|
+
* 2. Passthrough (edge.passthrough = true): Child gets the full parent
|
|
720
|
+
* conversation + orchestrator's instructions appended. Use this when
|
|
721
|
+
* the child needs the full user context (e.g., a transfer-like handoff).
|
|
722
|
+
*
|
|
723
|
+
* 3. Fallback: If no instructions provided AND not passthrough, child
|
|
724
|
+
* gets the agent's description as its task.
|
|
725
|
+
*/
|
|
726
|
+
const taskDescription = promptKey in input && input[promptKey] != null
|
|
676
727
|
? String(input[promptKey])
|
|
677
728
|
: '';
|
|
678
|
-
|
|
729
|
+
let childMessages;
|
|
730
|
+
if (edge.passthrough) {
|
|
731
|
+
// Passthrough: full parent context + instructions appended
|
|
732
|
+
const state = getCurrentTaskInput();
|
|
733
|
+
childMessages = MultiAgentGraph.prepareHandoffMessages([
|
|
734
|
+
...state.messages,
|
|
735
|
+
]);
|
|
736
|
+
if (taskDescription) {
|
|
737
|
+
childMessages.push(new HumanMessage(taskDescription));
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
else {
|
|
741
|
+
// Default: isolated — only orchestrator's instructions
|
|
742
|
+
const fallbackTask = destContext?.description ?? 'Complete your assigned task.';
|
|
679
743
|
childMessages = [
|
|
680
|
-
|
|
681
|
-
new HumanMessage(taskDescription),
|
|
744
|
+
new HumanMessage(taskDescription || fallbackTask),
|
|
682
745
|
];
|
|
683
746
|
}
|
|
684
747
|
const childState = {
|
|
@@ -686,15 +749,17 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
686
749
|
};
|
|
687
750
|
const childContext = this.agentContexts.get(destination);
|
|
688
751
|
const destName = destContext?.name ?? destination;
|
|
689
|
-
|
|
752
|
+
mlog(`[MultiAgentGraph] Handoff "${sourceAgentId}" -> "${destination}" SPAWN (async)\n` +
|
|
690
753
|
` messages: ${childMessages.length}\n` +
|
|
691
754
|
` childTools: ${childContext?.tools?.length ?? 0} instances\n` +
|
|
692
755
|
` childToolDefs: ${childContext?.toolDefinitions?.length ?? 0} definitions`);
|
|
693
756
|
/**
|
|
694
757
|
* Dispatch transition BEFORE spawning the child subgraph so that
|
|
695
758
|
* callbacks.js sets multiAgentTrace.isMultiAgent = true before the
|
|
696
|
-
* child's ON_RUN_STEP events fire.
|
|
759
|
+
* child's ON_RUN_STEP events fire. spawnKey lets the UI create a
|
|
760
|
+
* distinct sidebar task for this specific invocation.
|
|
697
761
|
*/
|
|
762
|
+
mlog(`[MultiAgentGraph] Handoff SPAWN "${sourceAgentId}" -> "${destination}" spawnKey=${spawnKey}`);
|
|
698
763
|
await safeDispatchCustomEvent(GraphEvents.ON_AGENT_TRANSITION, {
|
|
699
764
|
sourceAgentId: sourceAgentId,
|
|
700
765
|
sourceAgentName: this.agentContexts.get(sourceAgentId)?.name ?? sourceAgentId,
|
|
@@ -702,59 +767,134 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
702
767
|
destinationAgentName: destName,
|
|
703
768
|
edgeType: EdgeType.HANDOFF,
|
|
704
769
|
timestamp: Date.now(),
|
|
770
|
+
spawnKey,
|
|
771
|
+
spawnPath: childSpawnPath,
|
|
772
|
+
parentSpawnPath: parentSpawnPath || null,
|
|
773
|
+
spawnDepth: childDepth,
|
|
705
774
|
}, config);
|
|
706
775
|
/**
|
|
707
|
-
*
|
|
708
|
-
*
|
|
709
|
-
*
|
|
776
|
+
* Child events need to carry spawnKey so callbacks.js can route
|
|
777
|
+
* them to the correct child aggregator. LangChain propagates
|
|
778
|
+
* `metadata` and `tags` from the parent config to all descendants,
|
|
779
|
+
* so everything dispatched inside subgraph.invoke will have
|
|
780
|
+
* metadata.spawnKey populated on the event's runtime metadata.
|
|
781
|
+
*/
|
|
782
|
+
const childConfig = {
|
|
783
|
+
...(config ?? {}),
|
|
784
|
+
metadata: {
|
|
785
|
+
...(config?.metadata ?? {}),
|
|
786
|
+
spawnKey,
|
|
787
|
+
spawnAgentId: destination,
|
|
788
|
+
/** Hierarchical identity — see spawnPath.ts */
|
|
789
|
+
spawnPath: childSpawnPath,
|
|
790
|
+
parentSpawnPath: parentSpawnPath || null,
|
|
791
|
+
spawnDepth: childDepth,
|
|
792
|
+
},
|
|
793
|
+
tags: [
|
|
794
|
+
...(config?.tags ?? []),
|
|
795
|
+
`spawn:${spawnKey}`,
|
|
796
|
+
`depth:${childDepth}`,
|
|
797
|
+
],
|
|
798
|
+
};
|
|
799
|
+
/**
|
|
800
|
+
* Callback pattern: spawn child, WAIT for completion, return real
|
|
801
|
+
* result. The parent naturally sees child results in its tool
|
|
802
|
+
* return, so no manual "collect_results" step is needed.
|
|
710
803
|
*
|
|
711
|
-
*
|
|
712
|
-
*
|
|
804
|
+
* Parallelism still works: when the LLM emits multiple handoff
|
|
805
|
+
* tool calls in one response, LangGraph runs all tool functions
|
|
806
|
+
* concurrently. Each waits for its child. All results land in
|
|
807
|
+
* the LLM's next turn together.
|
|
713
808
|
*/
|
|
714
|
-
const
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
809
|
+
const spawnedAt = Date.now();
|
|
810
|
+
try {
|
|
811
|
+
const result = await subgraph.invoke(childState, childConfig);
|
|
812
|
+
const durationMs = Date.now() - spawnedAt;
|
|
813
|
+
const resultText = MultiAgentGraph.extractHandoffResult(result.messages, destination);
|
|
814
|
+
const truncated = MultiAgentGraph.truncateHandoffResult(resultText, maxResultChars);
|
|
815
|
+
mlog(`[MultiAgentGraph] Handoff COMPLETED "${sourceAgentId}" -> "${destination}" ` +
|
|
816
|
+
`spawnKey=${spawnKey} (${durationMs}ms, ${truncated.length} chars)`);
|
|
817
|
+
/** Dispatch completion event for UI update — carries spawnKey so
|
|
818
|
+
* the frontend can mark the correct sidebar task as completed. */
|
|
819
|
+
safeDispatchCustomEvent(GraphEvents.ON_AGENT_TRANSITION, {
|
|
820
|
+
sourceAgentId: destination,
|
|
821
|
+
sourceAgentName: destName,
|
|
822
|
+
destinationAgentId: sourceAgentId,
|
|
823
|
+
destinationAgentName: this.agentContexts.get(sourceAgentId)?.name ??
|
|
824
|
+
sourceAgentId,
|
|
825
|
+
edgeType: EdgeType.HANDOFF,
|
|
826
|
+
timestamp: Date.now(),
|
|
827
|
+
isCompletion: true,
|
|
828
|
+
durationMs,
|
|
829
|
+
resultLength: truncated.length,
|
|
830
|
+
spawnKey,
|
|
831
|
+
spawnPath: childSpawnPath,
|
|
832
|
+
parentSpawnPath: parentSpawnPath || null,
|
|
833
|
+
spawnDepth: childDepth,
|
|
834
|
+
}, config).catch(() => {
|
|
835
|
+
/* best-effort event dispatch */
|
|
836
|
+
});
|
|
837
|
+
return truncated;
|
|
838
|
+
}
|
|
839
|
+
catch (err) {
|
|
840
|
+
const durationMs = Date.now() - spawnedAt;
|
|
841
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
842
|
+
// EPIPE from console.debug is non-fatal
|
|
843
|
+
if (errMsg.includes('EPIPE')) {
|
|
844
|
+
mwarn(`[MultiAgentGraph] Child "${destination}" hit EPIPE (non-fatal) spawnKey=${spawnKey}`);
|
|
730
845
|
safeDispatchCustomEvent(GraphEvents.ON_AGENT_TRANSITION, {
|
|
731
846
|
sourceAgentId: destination,
|
|
732
847
|
sourceAgentName: destName,
|
|
733
|
-
destinationAgentId:
|
|
734
|
-
destinationAgentName:
|
|
848
|
+
destinationAgentId: sourceAgentId,
|
|
849
|
+
destinationAgentName: this.agentContexts.get(sourceAgentId)?.name ??
|
|
850
|
+
sourceAgentId,
|
|
735
851
|
edgeType: EdgeType.HANDOFF,
|
|
736
852
|
timestamp: Date.now(),
|
|
737
853
|
isCompletion: true,
|
|
738
|
-
durationMs
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
854
|
+
durationMs,
|
|
855
|
+
spawnKey,
|
|
856
|
+
spawnPath: childSpawnPath,
|
|
857
|
+
parentSpawnPath: parentSpawnPath || null,
|
|
858
|
+
spawnDepth: childDepth,
|
|
859
|
+
}, config).catch(() => {
|
|
860
|
+
/* best-effort */
|
|
861
|
+
});
|
|
862
|
+
return `Agent "${destName}" completed but output was lost due to stream closure.`;
|
|
863
|
+
}
|
|
864
|
+
console.error(`[MultiAgentGraph] Handoff FAILED "${sourceAgentId}" -> "${destination}" ` +
|
|
865
|
+
`spawnKey=${spawnKey} (${durationMs}ms): ${errMsg}`);
|
|
866
|
+
safeDispatchCustomEvent(GraphEvents.ON_AGENT_TRANSITION, {
|
|
867
|
+
sourceAgentId: destination,
|
|
868
|
+
sourceAgentName: destName,
|
|
869
|
+
destinationAgentId: sourceAgentId,
|
|
870
|
+
destinationAgentName: this.agentContexts.get(sourceAgentId)?.name ??
|
|
871
|
+
sourceAgentId,
|
|
872
|
+
edgeType: EdgeType.HANDOFF,
|
|
873
|
+
timestamp: Date.now(),
|
|
874
|
+
isCompletion: true,
|
|
875
|
+
durationMs,
|
|
876
|
+
spawnKey,
|
|
877
|
+
spawnPath: childSpawnPath,
|
|
878
|
+
parentSpawnPath: parentSpawnPath || null,
|
|
879
|
+
spawnDepth: childDepth,
|
|
880
|
+
error: errMsg,
|
|
881
|
+
}, config).catch(() => {
|
|
882
|
+
/* best-effort */
|
|
883
|
+
});
|
|
884
|
+
return `Agent "${destName}" failed after ${durationMs}ms: ${errMsg}`;
|
|
885
|
+
}
|
|
744
886
|
}, {
|
|
745
887
|
name: toolName,
|
|
746
|
-
schema:
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
description: promptInputDescription,
|
|
753
|
-
},
|
|
888
|
+
schema: {
|
|
889
|
+
type: 'object',
|
|
890
|
+
properties: {
|
|
891
|
+
[promptKey]: {
|
|
892
|
+
type: 'string',
|
|
893
|
+
description: promptInputDescription,
|
|
754
894
|
},
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
895
|
+
},
|
|
896
|
+
required: [promptKey],
|
|
897
|
+
},
|
|
758
898
|
description: toolDescription,
|
|
759
899
|
}));
|
|
760
900
|
}
|
|
@@ -796,7 +936,7 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
796
936
|
/**
|
|
797
937
|
* Truncate handoff result using head/tail strategy (60/40 split).
|
|
798
938
|
* Preserves the beginning (key findings) and end (conclusions).
|
|
799
|
-
* Matches the TaskTool.truncateResult pattern
|
|
939
|
+
* Matches the TaskTool.truncateResult pattern used by host orchestrators.
|
|
800
940
|
* @param result - The full result text
|
|
801
941
|
* @param maxChars - Maximum allowed characters
|
|
802
942
|
*/
|
|
@@ -832,8 +972,140 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
832
972
|
* Create a complete agent subgraph (similar to createReactAgent)
|
|
833
973
|
*/
|
|
834
974
|
createAgentSubgraph(agentId) {
|
|
835
|
-
/**
|
|
836
|
-
|
|
975
|
+
/**
|
|
976
|
+
* Scoped subgraph build for handoff targets.
|
|
977
|
+
*
|
|
978
|
+
* If the handoff target has outgoing sequence/transfer edges (e.g. a
|
|
979
|
+
* "researcher" agent with its own sequence `[researcher → prod_assistant]`),
|
|
980
|
+
* we compile a mini-StateGraph containing the agent and all agents reachable
|
|
981
|
+
* from it via non-handoff edges. This way, when the parent hands off to
|
|
982
|
+
* researcher via `subgraph.invoke()`, the nested sequence runs to completion
|
|
983
|
+
* before the result is returned to the parent.
|
|
984
|
+
*
|
|
985
|
+
* Fast path: if no downstream agents are reachable, fall back to the
|
|
986
|
+
* previous single-node behavior (`createAgentNode`).
|
|
987
|
+
*
|
|
988
|
+
* See docs/multi-agent-nesting-architecture.md §6.
|
|
989
|
+
*/
|
|
990
|
+
const reachable = this.computeReachableViaNonHandoff(agentId);
|
|
991
|
+
if (reachable.size === 1) {
|
|
992
|
+
return this.createAgentNode(agentId);
|
|
993
|
+
}
|
|
994
|
+
mlog(`[MultiAgentGraph] Scoped subgraph for "${agentId}": ${reachable.size} nodes [${Array.from(reachable).join(', ')}]`);
|
|
995
|
+
return this.buildScopedSubgraph(agentId, reachable);
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* BFS from `rootAgentId` across sequence + transfer edges (NOT handoff edges).
|
|
999
|
+
* Returns the set of agents reachable in this agent's "local workflow".
|
|
1000
|
+
*/
|
|
1001
|
+
computeReachableViaNonHandoff(rootAgentId) {
|
|
1002
|
+
const reachable = new Set([rootAgentId]);
|
|
1003
|
+
const queue = [rootAgentId];
|
|
1004
|
+
const localEdges = [...this.sequenceEdges, ...this.transferEdges];
|
|
1005
|
+
while (queue.length > 0) {
|
|
1006
|
+
const current = queue.shift();
|
|
1007
|
+
for (const edge of localEdges) {
|
|
1008
|
+
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
1009
|
+
if (!sources.includes(current))
|
|
1010
|
+
continue;
|
|
1011
|
+
const dests = Array.isArray(edge.to) ? edge.to : [edge.to];
|
|
1012
|
+
for (const dest of dests) {
|
|
1013
|
+
if (!reachable.has(dest) && this.agentContexts.has(dest)) {
|
|
1014
|
+
reachable.add(dest);
|
|
1015
|
+
queue.push(dest);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
return reachable;
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Build a compiled scoped StateGraph containing `agentIds` as nodes, rooted
|
|
1024
|
+
* at `rootAgentId`. Linear sequence edges where both endpoints are in scope
|
|
1025
|
+
* are wired directly; nodes with no outgoing in-scope edges route to END.
|
|
1026
|
+
*
|
|
1027
|
+
* Each node is wrapped around the per-agent `createAgentNode` compiled
|
|
1028
|
+
* workflow (agent + tools loop) to preserve isolated tool context.
|
|
1029
|
+
*/
|
|
1030
|
+
buildScopedSubgraph(rootAgentId, agentIds) {
|
|
1031
|
+
const StateAnnotation = Annotation.Root({
|
|
1032
|
+
messages: Annotation({
|
|
1033
|
+
reducer: messagesStateReducer,
|
|
1034
|
+
default: () => [],
|
|
1035
|
+
}),
|
|
1036
|
+
});
|
|
1037
|
+
const builder = new StateGraph(StateAnnotation);
|
|
1038
|
+
// Precompile each scoped agent's inner workflow and wrap as a node.
|
|
1039
|
+
//
|
|
1040
|
+
// Two different isolation strategies depending on position:
|
|
1041
|
+
//
|
|
1042
|
+
// • ROOT node (the handoff target itself): receives the parent
|
|
1043
|
+
// orchestrator's handoff frame. Use `prepareHandoffMessages` — drops
|
|
1044
|
+
// orphaned tool_use, compacts paired tool calls, guarantees trailing
|
|
1045
|
+
// HumanMessage for Bedrock/VertexAI compatibility. The root needs
|
|
1046
|
+
// orchestrator context because it's responding to the handoff.
|
|
1047
|
+
//
|
|
1048
|
+
// • DOWNSTREAM nodes (sequence targets of the root): run as FULLY
|
|
1049
|
+
// ISOLATED child sessions. They receive only:
|
|
1050
|
+
// [original user request, synthetic HumanMessage describing what
|
|
1051
|
+
// the upstream agent produced and asking them to act]
|
|
1052
|
+
// No raw tool_use / tool_result blocks from the upstream agent —
|
|
1053
|
+
// prevents schema confusion when a downstream agent sees noisy
|
|
1054
|
+
// upstream context and produces malformed tool_use JSON.
|
|
1055
|
+
//
|
|
1056
|
+
// Each wrapper returns only the DELTA (new messages produced by the
|
|
1057
|
+
// inner invoke), not the prepared input — otherwise messagesStateReducer
|
|
1058
|
+
// would double-append the synthetic instruction into the scoped state.
|
|
1059
|
+
for (const aid of agentIds) {
|
|
1060
|
+
const inner = this.createAgentNode(aid);
|
|
1061
|
+
const isRoot = aid === rootAgentId;
|
|
1062
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1063
|
+
builder.addNode(aid, async (state, config) => {
|
|
1064
|
+
const prepared = isRoot
|
|
1065
|
+
? MultiAgentGraph.prepareHandoffMessages(state.messages)
|
|
1066
|
+
: MultiAgentGraph.prepareIsolatedChildMessages(state.messages);
|
|
1067
|
+
mlog(`[MultiAgentGraph] scoped node "${aid}" entering (isRoot=${isRoot}, stateMessages=${state.messages.length}, prepared=${prepared.length})`);
|
|
1068
|
+
const result = await inner.invoke({ ...state, messages: prepared }, config);
|
|
1069
|
+
// Return only the messages the inner node appended beyond its input,
|
|
1070
|
+
// so messagesStateReducer doesn't duplicate the synthetic wrapper
|
|
1071
|
+
// prompt into the scoped state.
|
|
1072
|
+
const delta = result.messages.length > prepared.length
|
|
1073
|
+
? result.messages.slice(prepared.length)
|
|
1074
|
+
: result.messages;
|
|
1075
|
+
return { messages: delta };
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
// START → root
|
|
1079
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
1080
|
+
// @ts-ignore — LangGraph string typing is too strict for dynamic agent ids
|
|
1081
|
+
builder.addEdge(START, rootAgentId);
|
|
1082
|
+
// Wire sequence edges in scope (linear chain support)
|
|
1083
|
+
const hasOutgoing = new Set();
|
|
1084
|
+
for (const edge of this.sequenceEdges) {
|
|
1085
|
+
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
1086
|
+
const dests = Array.isArray(edge.to) ? edge.to : [edge.to];
|
|
1087
|
+
for (const source of sources) {
|
|
1088
|
+
if (!agentIds.has(source))
|
|
1089
|
+
continue;
|
|
1090
|
+
for (const dest of dests) {
|
|
1091
|
+
if (!agentIds.has(dest))
|
|
1092
|
+
continue;
|
|
1093
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
1094
|
+
// @ts-ignore
|
|
1095
|
+
builder.addEdge(source, dest);
|
|
1096
|
+
hasOutgoing.add(source);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
// Leaves (no outgoing in-scope edges) route to END
|
|
1101
|
+
for (const aid of agentIds) {
|
|
1102
|
+
if (!hasOutgoing.has(aid)) {
|
|
1103
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
1104
|
+
// @ts-ignore
|
|
1105
|
+
builder.addEdge(aid, END);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
return builder.compile(this.compileOptions);
|
|
837
1109
|
}
|
|
838
1110
|
/**
|
|
839
1111
|
* Detects if the current agent is receiving a handoff and processes the messages accordingly.
|
|
@@ -848,108 +1120,20 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
848
1120
|
* @returns Object with filtered messages, extracted instructions, source agent, and parallel siblings
|
|
849
1121
|
*/
|
|
850
1122
|
/**
|
|
851
|
-
* Prepare messages for a handoff child agent.
|
|
852
|
-
*
|
|
853
|
-
*
|
|
854
|
-
*
|
|
855
|
-
* for the handoff tool itself, with no matching `tool_result`. Providers
|
|
856
|
-
* (Bedrock/Anthropic) reject this.
|
|
857
|
-
* 2. **Paired tool_use/tool_result in history**: The child may not have the same
|
|
858
|
-
* tools as the parent. Bedrock requires `toolConfig` when tool_use/tool_result
|
|
859
|
-
* blocks exist in the message history. Compacting these into text summaries
|
|
860
|
-
* avoids the requirement and reduces context bloat.
|
|
861
|
-
*
|
|
862
|
-
* Strategy:
|
|
863
|
-
* - Remove orphaned tool_use blocks (no matching tool_result)
|
|
864
|
-
* - Compact paired tool_use/tool_result interactions into text summaries
|
|
1123
|
+
* Prepare messages for a handoff child agent. See
|
|
1124
|
+
* {@link prepareHandoffMessagesUtil} for the full implementation and
|
|
1125
|
+
* semantics — this static method is a thin delegate preserved for
|
|
1126
|
+
* backward compatibility with existing call sites and unit tests.
|
|
865
1127
|
*/
|
|
866
1128
|
static prepareHandoffMessages(messages) {
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
pairedToolCallIds.add(tm.tool_call_id);
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
/**
|
|
880
|
-
* Pass 1: Remove orphaned tool_use blocks (no matching tool_result).
|
|
881
|
-
* Also skip ToolMessages since we'll compact paired ones in pass 2.
|
|
882
|
-
*/
|
|
883
|
-
const cleaned = [];
|
|
884
|
-
for (const msg of messages) {
|
|
885
|
-
/** Skip all ToolMessages — paired ones will be compacted in pass 2 */
|
|
886
|
-
if (msg.getType() === 'tool') {
|
|
887
|
-
continue;
|
|
888
|
-
}
|
|
889
|
-
if (msg.getType() !== 'ai') {
|
|
890
|
-
cleaned.push(msg);
|
|
891
|
-
continue;
|
|
892
|
-
}
|
|
893
|
-
const aiMsg = msg;
|
|
894
|
-
const toolCalls = aiMsg.tool_calls ?? [];
|
|
895
|
-
if (toolCalls.length === 0) {
|
|
896
|
-
cleaned.push(msg);
|
|
897
|
-
continue;
|
|
898
|
-
}
|
|
899
|
-
/** Extract text content from the AI message */
|
|
900
|
-
const textContent = typeof aiMsg.content === 'string'
|
|
901
|
-
? aiMsg.content
|
|
902
|
-
: Array.isArray(aiMsg.content)
|
|
903
|
-
? aiMsg.content
|
|
904
|
-
.filter((b) => b.type === 'text' && 'text' in b)
|
|
905
|
-
.map((b) => b.text ?? '')
|
|
906
|
-
.join('\n')
|
|
907
|
-
: '';
|
|
908
|
-
/** Build text summaries of paired tool calls */
|
|
909
|
-
const toolSummaries = [];
|
|
910
|
-
for (const tc of toolCalls) {
|
|
911
|
-
if (tc.id != null && pairedToolCallIds.has(tc.id)) {
|
|
912
|
-
/** Find the matching ToolMessage result */
|
|
913
|
-
const toolResult = messages.find((m) => m.getType() === 'tool' && m.tool_call_id === tc.id);
|
|
914
|
-
const resultContent = toolResult
|
|
915
|
-
? typeof toolResult.content === 'string'
|
|
916
|
-
? toolResult.content.slice(0, 500)
|
|
917
|
-
: '[complex result]'
|
|
918
|
-
: '[no result]';
|
|
919
|
-
toolSummaries.push(`[Tool "${tc.name}": ${resultContent}]`);
|
|
920
|
-
}
|
|
921
|
-
// Orphaned tool_use blocks (no matching result) are silently dropped
|
|
922
|
-
}
|
|
923
|
-
/** Reconstruct as plain text AI message (no tool_calls) */
|
|
924
|
-
const parts = [textContent, ...toolSummaries].filter(Boolean);
|
|
925
|
-
if (parts.length > 0) {
|
|
926
|
-
cleaned.push(new AIMessage({
|
|
927
|
-
content: parts.join('\n\n'),
|
|
928
|
-
id: aiMsg.id,
|
|
929
|
-
}));
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
/**
|
|
933
|
-
* Ensure messages end with a HumanMessage.
|
|
934
|
-
* After stripping tool artifacts, the last message may be an AIMessage
|
|
935
|
-
* (orchestrator's reasoning before the handoff). Some providers (Bedrock,
|
|
936
|
-
* Google/VertexAI) reject conversations ending with an assistant message.
|
|
937
|
-
* Convert the trailing AIMessage to a HumanMessage to preserve any useful
|
|
938
|
-
* context (e.g., compacted tool summaries) while satisfying the API requirement.
|
|
939
|
-
*/
|
|
940
|
-
if (cleaned.length > 0 && cleaned[cleaned.length - 1].getType() === 'ai') {
|
|
941
|
-
const lastAI = cleaned[cleaned.length - 1];
|
|
942
|
-
const content = typeof lastAI.content === 'string'
|
|
943
|
-
? lastAI.content
|
|
944
|
-
: '';
|
|
945
|
-
if (content.trim()) {
|
|
946
|
-
cleaned[cleaned.length - 1] = new HumanMessage(`[Context from orchestrator]: ${content}`);
|
|
947
|
-
}
|
|
948
|
-
else {
|
|
949
|
-
cleaned.pop();
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
return cleaned;
|
|
1129
|
+
return prepareHandoffMessages(messages);
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* Build an isolated message context for a downstream scoped-subgraph
|
|
1133
|
+
* node. See {@link prepareIsolatedChildMessagesUtil} for details.
|
|
1134
|
+
*/
|
|
1135
|
+
static prepareIsolatedChildMessages(messages) {
|
|
1136
|
+
return prepareIsolatedChildMessages(messages);
|
|
953
1137
|
}
|
|
954
1138
|
processTransferReception(messages, agentId) {
|
|
955
1139
|
if (messages.length === 0)
|
|
@@ -981,7 +1165,8 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
981
1165
|
}
|
|
982
1166
|
else if (isConditionalTransfer) {
|
|
983
1167
|
const transferDest = candidateMsg.additional_kwargs.handoff_destination;
|
|
984
|
-
destinationAgent =
|
|
1168
|
+
destinationAgent =
|
|
1169
|
+
typeof transferDest === 'string' ? transferDest : null;
|
|
985
1170
|
}
|
|
986
1171
|
/** Check if this transfer targets our agent */
|
|
987
1172
|
if (destinationAgent === agentId) {
|
|
@@ -1222,8 +1407,35 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
1222
1407
|
for (const startNode of this.startingNodes) {
|
|
1223
1408
|
handoffOnlyDestinations.delete(startNode);
|
|
1224
1409
|
}
|
|
1410
|
+
/**
|
|
1411
|
+
* Nested-sequence expansion: for each handoff-only target, its downstream
|
|
1412
|
+
* sequence/transfer agents MUST also become handoff-only — they exist only
|
|
1413
|
+
* inside the target's scoped subgraph, not at top level. Without this,
|
|
1414
|
+
* those downstream nodes would be added as top-level orphans and LangGraph
|
|
1415
|
+
* would fail compilation (UNREACHABLE_NODE).
|
|
1416
|
+
*
|
|
1417
|
+
* See docs/multi-agent-nesting-architecture.md §6.
|
|
1418
|
+
*/
|
|
1419
|
+
const nestedHandoffOnly = new Set();
|
|
1420
|
+
for (const target of handoffOnlyDestinations) {
|
|
1421
|
+
const reachable = this.computeReachableViaNonHandoff(target);
|
|
1422
|
+
for (const agent of reachable) {
|
|
1423
|
+
if (agent === target)
|
|
1424
|
+
continue;
|
|
1425
|
+
// Skip if this agent is legitimately a top-level starting node
|
|
1426
|
+
if (this.startingNodes.has(agent))
|
|
1427
|
+
continue;
|
|
1428
|
+
nestedHandoffOnly.add(agent);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
for (const agent of nestedHandoffOnly) {
|
|
1432
|
+
handoffOnlyDestinations.add(agent);
|
|
1433
|
+
}
|
|
1434
|
+
if (nestedHandoffOnly.size > 0) {
|
|
1435
|
+
mlog(`[MultiAgentGraph] Nested handoff-only (scoped subgraph downstream): [${Array.from(nestedHandoffOnly).join(', ')}]`);
|
|
1436
|
+
}
|
|
1225
1437
|
if (handoffOnlyDestinations.size > 0) {
|
|
1226
|
-
|
|
1438
|
+
mlog(`[MultiAgentGraph] Handoff-only children (subgraph only, no top-level node): [${Array.from(handoffOnlyDestinations).join(', ')}]`);
|
|
1227
1439
|
}
|
|
1228
1440
|
// Add agents as nodes — skip handoff-only children (they exist as subgraphs only)
|
|
1229
1441
|
for (const [agentId] of this.agentContexts) {
|
|
@@ -1272,7 +1484,7 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
1272
1484
|
}
|
|
1273
1485
|
/** Wrapper function that handles agentMessages channel, handoff reception, and conditional routing */
|
|
1274
1486
|
const agentWrapper = async (state, config) => {
|
|
1275
|
-
|
|
1487
|
+
mlog(`[MultiAgentGraph] Agent "${agentId}" wrapper ENTRY (messages: ${state.messages.length}, needsCommandRouting: ${needsCommandRouting})`);
|
|
1276
1488
|
let result;
|
|
1277
1489
|
/**
|
|
1278
1490
|
* Check if this agent is receiving a transfer.
|
|
@@ -1283,7 +1495,7 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
1283
1495
|
const transferContext = this.processTransferReception(state.messages, agentId);
|
|
1284
1496
|
if (transferContext !== null) {
|
|
1285
1497
|
const { filteredMessages, instructions, sourceAgentName, parallelSiblings, } = transferContext;
|
|
1286
|
-
|
|
1498
|
+
mlog(`[MultiAgentGraph] Agent "${agentId}" receiving transfer from "${sourceAgentName}" (instructions: ${instructions != null}, parallelSiblings: ${parallelSiblings.length})`);
|
|
1287
1499
|
/**
|
|
1288
1500
|
* Set handoff context on the receiving agent.
|
|
1289
1501
|
* Uses pre-computed graph position for depth and parallel info.
|
|
@@ -1399,7 +1611,7 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
1399
1611
|
}
|
|
1400
1612
|
/** Track the last agent that produced output for continuation support */
|
|
1401
1613
|
this.lastActiveAgentId = agentId;
|
|
1402
|
-
|
|
1614
|
+
mlog(`[MultiAgentGraph] Agent "${agentId}" wrapper EXIT (result messages: ${result.messages.length})`);
|
|
1403
1615
|
/** If agent has both transfer and sequence edges, use Command for exclusive routing */
|
|
1404
1616
|
if (needsCommandRouting) {
|
|
1405
1617
|
/** Check if a transfer occurred */
|
|
@@ -1410,7 +1622,7 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
1410
1622
|
lastMessage.name.startsWith(Constants.LC_TRANSFER_TO_)) {
|
|
1411
1623
|
/** Transfer occurred - extract destination and navigate there exclusively */
|
|
1412
1624
|
const transferDest = lastMessage.name.replace(Constants.LC_TRANSFER_TO_, '');
|
|
1413
|
-
|
|
1625
|
+
mlog(`[MultiAgentGraph] Command routing: "${agentId}" -> transfer to "${transferDest}" (sequence edges skipped: [${Array.from(sequenceDestinations).join(', ')}])`);
|
|
1414
1626
|
/** Validate destination agent exists */
|
|
1415
1627
|
if (!this.agentContexts.has(transferDest)) {
|
|
1416
1628
|
const availableAgents = Array.from(this.agentContexts.keys()).join(', ');
|
|
@@ -1438,7 +1650,7 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
1438
1650
|
}
|
|
1439
1651
|
const receiverBudget = receiverContext.maxContextTokens;
|
|
1440
1652
|
if (currentSize > receiverBudget * 0.7) {
|
|
1441
|
-
|
|
1653
|
+
mwarn(`[MultiAgentGraph] Pre-handoff compaction: context (${currentSize} tokens) exceeds ` +
|
|
1442
1654
|
`70% of receiver "${transferDest}" budget (${receiverBudget} tokens)`);
|
|
1443
1655
|
/** Generate handoff briefing */
|
|
1444
1656
|
const senderName = senderContext.name ?? agentId;
|
|
@@ -1503,7 +1715,7 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
1503
1715
|
}
|
|
1504
1716
|
else {
|
|
1505
1717
|
/** No transfer - proceed with sequence edges */
|
|
1506
|
-
|
|
1718
|
+
mlog(`[MultiAgentGraph] Command routing: "${agentId}" -> no transfer, following sequence edges: [${Array.from(sequenceDestinations).join(', ')}]`);
|
|
1507
1719
|
const directDests = Array.from(sequenceDestinations);
|
|
1508
1720
|
for (const dest of directDests) {
|
|
1509
1721
|
await safeDispatchCustomEvent(GraphEvents.ON_AGENT_TRANSITION, {
|
|
@@ -1535,9 +1747,14 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
1535
1747
|
* destinations so callbacks.js can register child agents for event
|
|
1536
1748
|
* isolation BEFORE they start streaming.
|
|
1537
1749
|
*/
|
|
1538
|
-
const allDests = new Set([
|
|
1750
|
+
const allDests = new Set([
|
|
1751
|
+
...transferDestinations,
|
|
1752
|
+
...sequenceDestinations,
|
|
1753
|
+
]);
|
|
1539
1754
|
if (allDests.size > 0) {
|
|
1540
|
-
const edgeType = hasTransferEdges
|
|
1755
|
+
const edgeType = hasTransferEdges
|
|
1756
|
+
? EdgeType.TRANSFER
|
|
1757
|
+
: EdgeType.SEQUENCE;
|
|
1541
1758
|
for (const dest of allDests) {
|
|
1542
1759
|
await safeDispatchCustomEvent(GraphEvents.ON_AGENT_TRANSITION, {
|
|
1543
1760
|
sourceAgentId: agentId,
|
|
@@ -1570,16 +1787,13 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
1570
1787
|
this.agentContexts.has(this.resumeFromAgentId);
|
|
1571
1788
|
if (validResumeAgent) {
|
|
1572
1789
|
const resumeAgentId = this.resumeFromAgentId;
|
|
1573
|
-
|
|
1790
|
+
mlog(`[MultiAgentGraph] Multi-turn resumption: routing START → "${resumeAgentId}" (skipping default starting nodes: [${Array.from(this.startingNodes).join(', ')}])`);
|
|
1574
1791
|
/**
|
|
1575
1792
|
* Build route map containing both the resume agent and default starting
|
|
1576
1793
|
* nodes. This is required by LangGraph — all possible destinations must
|
|
1577
1794
|
* be declared even if the router always picks one.
|
|
1578
1795
|
*/
|
|
1579
|
-
const allPossibleStarts = new Set([
|
|
1580
|
-
...this.startingNodes,
|
|
1581
|
-
resumeAgentId,
|
|
1582
|
-
]);
|
|
1796
|
+
const allPossibleStarts = new Set([...this.startingNodes, resumeAgentId]);
|
|
1583
1797
|
const routeMap = {};
|
|
1584
1798
|
for (const nodeId of allPossibleStarts) {
|
|
1585
1799
|
routeMap[nodeId] = nodeId;
|
|
@@ -1588,7 +1802,7 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
1588
1802
|
}
|
|
1589
1803
|
else {
|
|
1590
1804
|
if (this.resumeFromAgentId != null) {
|
|
1591
|
-
|
|
1805
|
+
mwarn(`[MultiAgentGraph] resumeFromAgentId "${this.resumeFromAgentId}" not found in graph — falling back to default starting nodes`);
|
|
1592
1806
|
}
|
|
1593
1807
|
for (const startNode of this.startingNodes) {
|
|
1594
1808
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
@@ -1652,8 +1866,19 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
1652
1866
|
if (gatedEdges.has(edge)) {
|
|
1653
1867
|
continue;
|
|
1654
1868
|
}
|
|
1655
|
-
|
|
1656
|
-
|
|
1869
|
+
/**
|
|
1870
|
+
* Skip sequence edges where either endpoint lives only inside a scoped
|
|
1871
|
+
* handoff subgraph. Those edges are wired inside `buildScopedSubgraph`,
|
|
1872
|
+
* not at the top level — adding them here would reference non-existent
|
|
1873
|
+
* top-level nodes and fail compilation.
|
|
1874
|
+
*/
|
|
1875
|
+
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
1876
|
+
const dests = Array.isArray(edge.to) ? edge.to : [edge.to];
|
|
1877
|
+
const anyEndpointHandoffOnly = [...sources, ...dests].some((n) => handoffOnlyDestinations.has(n));
|
|
1878
|
+
if (anyEndpointHandoffOnly) {
|
|
1879
|
+
continue;
|
|
1880
|
+
}
|
|
1881
|
+
for (const destination of dests) {
|
|
1657
1882
|
if (!edgesByDestination.has(destination)) {
|
|
1658
1883
|
edgesByDestination.set(destination, []);
|
|
1659
1884
|
}
|
|
@@ -1750,7 +1975,8 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
1750
1975
|
return eSources.includes(source);
|
|
1751
1976
|
});
|
|
1752
1977
|
/** Skip adding edge if source uses Command routing (has both types) */
|
|
1753
|
-
if (sourceTransferEdges.length > 0 &&
|
|
1978
|
+
if (sourceTransferEdges.length > 0 &&
|
|
1979
|
+
sourceSequenceEdges.length > 0) {
|
|
1754
1980
|
continue;
|
|
1755
1981
|
}
|
|
1756
1982
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|