@illuma-ai/agents 1.1.15 → 1.1.16
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/common/enum.cjs +13 -13
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +146 -150
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/main.cjs +2 -2
- package/dist/cjs/types/graph.cjs.map +1 -1
- package/dist/esm/common/enum.mjs +12 -12
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +147 -151
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/main.mjs +1 -1
- package/dist/esm/types/graph.mjs.map +1 -1
- package/dist/types/common/enum.d.ts +11 -11
- package/dist/types/graphs/MultiAgentGraph.d.ts +38 -36
- package/dist/types/types/graph.d.ts +13 -7
- package/package.json +1 -1
- package/src/common/__tests__/enum.test.ts +14 -6
- package/src/common/enum.ts +11 -11
- package/src/graphs/MultiAgentGraph.ts +148 -152
- package/src/graphs/__tests__/multi-agent-delegate.test.ts +44 -44
- package/src/graphs/__tests__/multi-agent-edges.test.ts +83 -85
- package/src/scripts/multi-agent-chain.js +1 -1
- package/src/scripts/multi-agent-chain.ts +1 -1
- package/src/scripts/multi-agent-document-review-chain.js +1 -1
- package/src/scripts/multi-agent-document-review-chain.ts +1 -1
- package/src/scripts/multi-agent-hybrid-flow.js +3 -3
- package/src/scripts/multi-agent-hybrid-flow.ts +3 -3
- package/src/scripts/multi-agent-parallel.js +2 -2
- package/src/scripts/multi-agent-parallel.ts +2 -2
- package/src/scripts/multi-agent-sequence.js +2 -2
- package/src/scripts/multi-agent-sequence.ts +2 -2
- package/src/scripts/multi-agent-supervisor.js +5 -5
- package/src/scripts/multi-agent-supervisor.ts +5 -5
- package/src/scripts/poc-multi-agent-comprehensive.ts +7 -7
- package/src/scripts/sequential-full-metadata-test.js +1 -1
- package/src/scripts/sequential-full-metadata-test.ts +1 -1
- package/src/scripts/test-custom-prompt-key.js +3 -3
- package/src/scripts/test-custom-prompt-key.ts +3 -3
- package/src/scripts/test-handoff-input.js +1 -1
- package/src/scripts/test-handoff-input.ts +1 -1
- package/src/scripts/test-handoff-preamble.js +1 -1
- package/src/scripts/test-handoff-preamble.ts +1 -1
- package/src/scripts/test-handoff-steering.js +3 -3
- package/src/scripts/test-handoff-steering.ts +3 -3
- package/src/scripts/test-multi-agent-list-handoff.js +1 -1
- package/src/scripts/test-multi-agent-list-handoff.ts +1 -1
- package/src/scripts/test-parallel-agent-labeling.js +2 -2
- package/src/scripts/test-parallel-agent-labeling.ts +2 -2
- package/src/scripts/test-parallel-handoffs.js +2 -2
- package/src/scripts/test-parallel-handoffs.ts +2 -2
- package/src/scripts/test-thinking-handoff-bedrock.js +1 -1
- package/src/scripts/test-thinking-handoff-bedrock.ts +1 -1
- package/src/scripts/test-thinking-handoff.js +1 -1
- package/src/scripts/test-thinking-handoff.ts +1 -1
- package/src/scripts/test-thinking-to-thinking-handoff-bedrock.js +1 -1
- package/src/scripts/test-thinking-to-thinking-handoff-bedrock.ts +1 -1
- package/src/scripts/test-tool-before-handoff-role-order.js +1 -1
- package/src/scripts/test-tool-before-handoff-role-order.ts +1 -1
- package/src/scripts/test-tools-before-handoff.js +1 -1
- package/src/scripts/test-tools-before-handoff.ts +1 -1
- package/src/specs/agent-handoffs-bedrock.integration.test.ts +6 -6
- package/src/specs/agent-handoffs.test.ts +35 -35
- package/src/specs/thinking-handoff.test.ts +9 -9
- package/src/types/graph.ts +13 -7
|
@@ -4,37 +4,37 @@ import { ToolMessage, AIMessage, HumanMessage, getBufferString, SystemMessage }
|
|
|
4
4
|
import { getCurrentTaskInput, Command, Annotation, messagesStateReducer, StateGraph, END, START } from '@langchain/langgraph';
|
|
5
5
|
import '../messages/core.mjs';
|
|
6
6
|
import 'nanoid';
|
|
7
|
-
import { EdgeType, Constants,
|
|
7
|
+
import { EdgeType, Constants, DEFAULT_HANDOFF_MAX_RESULT_CHARS } from '../common/enum.mjs';
|
|
8
8
|
import '../tools/approval/constants.mjs';
|
|
9
9
|
import '../utils/toonFormat.mjs';
|
|
10
10
|
import { summarize, createEmergencySummary } from '../messages/summarize.mjs';
|
|
11
11
|
import { StandardGraph } from './Graph.mjs';
|
|
12
12
|
|
|
13
13
|
/** Pattern to extract instructions from transfer ToolMessage content */
|
|
14
|
-
const
|
|
14
|
+
const TRANSFER_INSTRUCTIONS_PATTERN = /(?:Instructions?|Context):\s*(.+)/is;
|
|
15
15
|
/**
|
|
16
16
|
* MultiAgentGraph extends StandardGraph to support dynamic multi-agent workflows
|
|
17
17
|
* with handoffs, fan-in/fan-out, and other composable patterns.
|
|
18
18
|
*
|
|
19
19
|
* Key behavior:
|
|
20
|
-
* - Agents with ONLY
|
|
21
|
-
* - Agents with ONLY
|
|
22
|
-
* - Agents with BOTH: Use Command for exclusive routing (
|
|
23
|
-
* - If
|
|
24
|
-
* - If no
|
|
20
|
+
* - Agents with ONLY transfer edges: Can dynamically route to any transfer destination
|
|
21
|
+
* - Agents with ONLY sequence edges: Always follow their sequence edges
|
|
22
|
+
* - Agents with BOTH: Use Command for exclusive routing (transfer OR sequence, not both)
|
|
23
|
+
* - If transfer occurs: Only the transfer destination executes
|
|
24
|
+
* - If no transfer: Sequence edges execute (potentially in parallel)
|
|
25
25
|
*
|
|
26
|
-
* This enables the common pattern where an agent either
|
|
27
|
-
* OR continues its workflow (
|
|
26
|
+
* This enables the common pattern where an agent either transfers (one-way)
|
|
27
|
+
* OR continues its workflow (sequence edges), but not both simultaneously.
|
|
28
28
|
*/
|
|
29
29
|
class MultiAgentGraph extends StandardGraph {
|
|
30
30
|
edges;
|
|
31
31
|
startingNodes = new Set();
|
|
32
|
-
|
|
32
|
+
sequenceEdges = [];
|
|
33
|
+
transferEdges = [];
|
|
33
34
|
handoffEdges = [];
|
|
34
|
-
delegateEdges = [];
|
|
35
35
|
/**
|
|
36
36
|
* Lazily populated registry of compiled subgraphs, keyed by agentId.
|
|
37
|
-
*
|
|
37
|
+
* Handoff tools are created in the constructor but reference subgraphs
|
|
38
38
|
* that are only created in createWorkflow(). This Map bridges that gap —
|
|
39
39
|
* tools capture the Map reference in their closure, and createWorkflow()
|
|
40
40
|
* populates it before any tool invocation occurs.
|
|
@@ -61,41 +61,39 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
61
61
|
this.edges = input.edges;
|
|
62
62
|
this.categorizeEdges();
|
|
63
63
|
this.analyzeGraph();
|
|
64
|
+
this.createTransferTools();
|
|
64
65
|
this.createHandoffTools();
|
|
65
|
-
this.createDelegateTools();
|
|
66
66
|
console.debug(`[MultiAgentGraph] Constructor complete: ${this.agentContexts.size} agents, ${this.edges.length} edges`);
|
|
67
67
|
}
|
|
68
68
|
/**
|
|
69
|
-
* Categorize edges into handoff and
|
|
69
|
+
* Categorize edges into handoff, transfer, and sequence types
|
|
70
70
|
*/
|
|
71
71
|
categorizeEdges() {
|
|
72
72
|
for (const edge of this.edges) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (edge.edgeType === EdgeType.DELEGATE) {
|
|
76
|
-
this.delegateEdges.push(edge);
|
|
73
|
+
if (edge.edgeType === EdgeType.HANDOFF) {
|
|
74
|
+
this.handoffEdges.push(edge);
|
|
77
75
|
}
|
|
78
|
-
else if (edge.edgeType === EdgeType.
|
|
79
|
-
this.
|
|
76
|
+
else if (edge.edgeType === EdgeType.SEQUENCE) {
|
|
77
|
+
this.sequenceEdges.push(edge);
|
|
80
78
|
}
|
|
81
|
-
else if (edge.edgeType === EdgeType.
|
|
82
|
-
this.
|
|
79
|
+
else if (edge.edgeType === EdgeType.TRANSFER || edge.condition != null) {
|
|
80
|
+
this.transferEdges.push(edge);
|
|
83
81
|
}
|
|
84
82
|
else {
|
|
85
|
-
// Default: single-to-single edges are
|
|
83
|
+
// Default: single-to-single edges are transfer, single-to-multiple are sequence
|
|
86
84
|
const destinations = Array.isArray(edge.to) ? edge.to : [edge.to];
|
|
87
85
|
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
88
86
|
if (sources.length === 1 && destinations.length > 1) {
|
|
89
|
-
// Fan-out pattern defaults to
|
|
90
|
-
this.
|
|
87
|
+
// Fan-out pattern defaults to sequence
|
|
88
|
+
this.sequenceEdges.push(edge);
|
|
91
89
|
}
|
|
92
90
|
else {
|
|
93
|
-
// Everything else defaults to
|
|
94
|
-
this.
|
|
91
|
+
// Everything else defaults to transfer
|
|
92
|
+
this.transferEdges.push(edge);
|
|
95
93
|
}
|
|
96
94
|
}
|
|
97
95
|
}
|
|
98
|
-
console.debug(`[MultiAgentGraph] Edge categorization: ${this.handoffEdges.length} handoff, ${this.
|
|
96
|
+
console.debug(`[MultiAgentGraph] Edge categorization: ${this.handoffEdges.length} handoff, ${this.transferEdges.length} transfer, ${this.sequenceEdges.length} sequence (of ${this.edges.length} total)`);
|
|
99
97
|
}
|
|
100
98
|
/**
|
|
101
99
|
* Analyze graph structure to determine starting nodes and connections
|
|
@@ -153,8 +151,8 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
153
151
|
if (visited.has(current))
|
|
154
152
|
continue;
|
|
155
153
|
visited.add(current);
|
|
156
|
-
// Find
|
|
157
|
-
for (const edge of this.
|
|
154
|
+
// Find sequence edges from this node
|
|
155
|
+
for (const edge of this.sequenceEdges) {
|
|
158
156
|
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
159
157
|
if (!sources.includes(current))
|
|
160
158
|
continue;
|
|
@@ -181,8 +179,8 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
181
179
|
}
|
|
182
180
|
}
|
|
183
181
|
}
|
|
184
|
-
// Also follow
|
|
185
|
-
for (const edge of [...this.
|
|
182
|
+
// Also follow transfer and handoff edges for traversal (they don't create parallel groups)
|
|
183
|
+
for (const edge of [...this.transferEdges, ...this.handoffEdges]) {
|
|
186
184
|
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
187
185
|
if (!sources.includes(current))
|
|
188
186
|
continue;
|
|
@@ -225,46 +223,44 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
225
223
|
return this.agentParallelGroups.get(agentId);
|
|
226
224
|
}
|
|
227
225
|
/**
|
|
228
|
-
* Create
|
|
226
|
+
* Create transfer tools for agents based on transfer edges only.
|
|
227
|
+
* Transfer tools return Command for one-way routing — parent exits, child takes over.
|
|
229
228
|
*/
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const
|
|
233
|
-
// Only process handoff edges for tool creation
|
|
234
|
-
for (const edge of this.handoffEdges) {
|
|
229
|
+
createTransferTools() {
|
|
230
|
+
const transfersByAgent = new Map();
|
|
231
|
+
for (const edge of this.transferEdges) {
|
|
235
232
|
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
236
233
|
sources.forEach((source) => {
|
|
237
|
-
if (!
|
|
238
|
-
|
|
234
|
+
if (!transfersByAgent.has(source)) {
|
|
235
|
+
transfersByAgent.set(source, []);
|
|
239
236
|
}
|
|
240
|
-
|
|
237
|
+
transfersByAgent.get(source).push(edge);
|
|
241
238
|
});
|
|
242
239
|
}
|
|
243
|
-
|
|
244
|
-
for (const [agentId, edges] of handoffsByAgent) {
|
|
240
|
+
for (const [agentId, edges] of transfersByAgent) {
|
|
245
241
|
const agentContext = this.agentContexts.get(agentId);
|
|
246
242
|
if (!agentContext)
|
|
247
243
|
continue;
|
|
248
|
-
|
|
249
|
-
const handoffTools = [];
|
|
244
|
+
const transferTools = [];
|
|
250
245
|
const sourceAgentName = agentContext.name ?? agentId;
|
|
251
246
|
for (const edge of edges) {
|
|
252
|
-
|
|
247
|
+
transferTools.push(...this.createTransferToolsForEdge(edge, agentId, sourceAgentName));
|
|
253
248
|
}
|
|
254
249
|
if (!agentContext.graphTools) {
|
|
255
250
|
agentContext.graphTools = [];
|
|
256
251
|
}
|
|
257
|
-
agentContext.graphTools.push(...
|
|
258
|
-
console.debug(`[MultiAgentGraph]
|
|
252
|
+
agentContext.graphTools.push(...transferTools);
|
|
253
|
+
console.debug(`[MultiAgentGraph] Transfer tools for "${agentId}": [${transferTools.map((t) => t.name).join(', ')}]`);
|
|
259
254
|
}
|
|
260
255
|
}
|
|
261
256
|
/**
|
|
262
|
-
* Create
|
|
263
|
-
*
|
|
264
|
-
* @param
|
|
257
|
+
* Create transfer tools for an edge (handles multiple destinations).
|
|
258
|
+
* Transfer tools return Command for one-way routing — parent exits, child takes over.
|
|
259
|
+
* @param edge - The graph edge defining the transfer
|
|
260
|
+
* @param sourceAgentId - The ID of the agent that will perform the transfer
|
|
265
261
|
* @param sourceAgentName - The human-readable name of the source agent
|
|
266
262
|
*/
|
|
267
|
-
|
|
263
|
+
createTransferToolsForEdge(edge, sourceAgentId, sourceAgentName) {
|
|
268
264
|
const tools = [];
|
|
269
265
|
const destinations = Array.isArray(edge.to) ? edge.to : [edge.to];
|
|
270
266
|
/** If there's a condition, create a single conditional handoff tool */
|
|
@@ -272,8 +268,8 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
272
268
|
const toolName = 'conditional_transfer';
|
|
273
269
|
const toolDescription = edge.description ?? 'Conditionally transfer control based on state';
|
|
274
270
|
/** Check if we have a prompt for handoff input */
|
|
275
|
-
const
|
|
276
|
-
const
|
|
271
|
+
const hasTransferInput = edge.prompt != null && typeof edge.prompt === 'string';
|
|
272
|
+
const transferInputDescription = hasTransferInput ? edge.prompt : undefined;
|
|
277
273
|
const promptKey = edge.promptKey ?? 'instructions';
|
|
278
274
|
tools.push(tool(async (rawInput, config) => {
|
|
279
275
|
const input = rawInput;
|
|
@@ -297,7 +293,7 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
297
293
|
destination = Array.isArray(result) ? result[0] : destinations[0];
|
|
298
294
|
}
|
|
299
295
|
let content = `Conditionally transferred to ${destination}`;
|
|
300
|
-
if (
|
|
296
|
+
if (hasTransferInput &&
|
|
301
297
|
promptKey in input &&
|
|
302
298
|
input[promptKey] != null) {
|
|
303
299
|
content += `\n\n${promptKey.charAt(0).toUpperCase() + promptKey.slice(1)}: ${input[promptKey]}`;
|
|
@@ -320,13 +316,13 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
320
316
|
});
|
|
321
317
|
}, {
|
|
322
318
|
name: toolName,
|
|
323
|
-
schema:
|
|
319
|
+
schema: hasTransferInput
|
|
324
320
|
? {
|
|
325
321
|
type: 'object',
|
|
326
322
|
properties: {
|
|
327
323
|
[promptKey]: {
|
|
328
324
|
type: 'string',
|
|
329
|
-
description:
|
|
325
|
+
description: transferInputDescription,
|
|
330
326
|
},
|
|
331
327
|
},
|
|
332
328
|
required: [],
|
|
@@ -341,10 +337,10 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
341
337
|
const toolName = `${Constants.LC_TRANSFER_TO_}${destination}`;
|
|
342
338
|
const destContext = this.agentContexts.get(destination);
|
|
343
339
|
const toolDescription = edge.description ??
|
|
344
|
-
this.
|
|
340
|
+
this.buildDefaultTransferDescription(destContext, destination);
|
|
345
341
|
/** Check if we have a prompt for handoff input */
|
|
346
|
-
const
|
|
347
|
-
const
|
|
342
|
+
const hasTransferInput = edge.prompt != null && typeof edge.prompt === 'string';
|
|
343
|
+
const transferInputDescription = hasTransferInput
|
|
348
344
|
? edge.prompt
|
|
349
345
|
: undefined;
|
|
350
346
|
const promptKey = edge.promptKey ?? 'instructions';
|
|
@@ -353,7 +349,7 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
353
349
|
const toolCallId = config?.toolCall?.id ??
|
|
354
350
|
'unknown';
|
|
355
351
|
let content = `Successfully transferred to ${destination}`;
|
|
356
|
-
if (
|
|
352
|
+
if (hasTransferInput &&
|
|
357
353
|
promptKey in input &&
|
|
358
354
|
input[promptKey] != null) {
|
|
359
355
|
content += `\n\n${promptKey.charAt(0).toUpperCase() + promptKey.slice(1)}: ${input[promptKey]}`;
|
|
@@ -430,13 +426,13 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
430
426
|
});
|
|
431
427
|
}, {
|
|
432
428
|
name: toolName,
|
|
433
|
-
schema:
|
|
429
|
+
schema: hasTransferInput
|
|
434
430
|
? {
|
|
435
431
|
type: 'object',
|
|
436
432
|
properties: {
|
|
437
433
|
[promptKey]: {
|
|
438
434
|
type: 'string',
|
|
439
|
-
description:
|
|
435
|
+
description: transferInputDescription,
|
|
440
436
|
},
|
|
441
437
|
},
|
|
442
438
|
required: [],
|
|
@@ -449,13 +445,13 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
449
445
|
return tools;
|
|
450
446
|
}
|
|
451
447
|
/**
|
|
452
|
-
* Builds a meaningful default description for a
|
|
448
|
+
* Builds a meaningful default description for a transfer tool when no explicit
|
|
453
449
|
* edge.description is provided. Uses the destination agent's name and description
|
|
454
450
|
* so the LLM can make informed routing decisions.
|
|
455
451
|
* @param destContext - AgentContext of the destination agent (may be undefined)
|
|
456
452
|
* @param destinationId - Raw agent ID (fallback when context unavailable)
|
|
457
453
|
*/
|
|
458
|
-
|
|
454
|
+
buildDefaultTransferDescription(destContext, destinationId) {
|
|
459
455
|
const displayName = destContext?.name ?? destinationId;
|
|
460
456
|
const agentDescription = destContext?.description;
|
|
461
457
|
if (agentDescription != null && agentDescription !== '') {
|
|
@@ -464,58 +460,58 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
464
460
|
return `Transfer control to "${displayName}"`;
|
|
465
461
|
}
|
|
466
462
|
/**
|
|
467
|
-
* Create
|
|
468
|
-
*
|
|
469
|
-
* as a string to the parent agent's context. Unlike
|
|
470
|
-
* return Command for
|
|
471
|
-
*
|
|
463
|
+
* Create handoff tools for agents based on handoff edges.
|
|
464
|
+
* Handoff tools invoke child agent subgraphs inline and return the result
|
|
465
|
+
* as a string to the parent agent's context. Unlike transfer tools (which
|
|
466
|
+
* return Command for one-way routing), handoff tools execute the child,
|
|
467
|
+
* extract the final text, and return it within the parent's agent loop.
|
|
472
468
|
*
|
|
473
469
|
* This enables the supervisor pattern: parent calls child → gets result → thinks → calls another.
|
|
474
470
|
*/
|
|
475
|
-
|
|
476
|
-
const
|
|
477
|
-
for (const edge of this.
|
|
471
|
+
createHandoffTools() {
|
|
472
|
+
const handoffsByAgent = new Map();
|
|
473
|
+
for (const edge of this.handoffEdges) {
|
|
478
474
|
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
479
475
|
sources.forEach((source) => {
|
|
480
|
-
if (!
|
|
481
|
-
|
|
476
|
+
if (!handoffsByAgent.has(source)) {
|
|
477
|
+
handoffsByAgent.set(source, []);
|
|
482
478
|
}
|
|
483
|
-
|
|
479
|
+
handoffsByAgent.get(source).push(edge);
|
|
484
480
|
});
|
|
485
481
|
}
|
|
486
|
-
for (const [agentId, edges] of
|
|
482
|
+
for (const [agentId, edges] of handoffsByAgent) {
|
|
487
483
|
const agentContext = this.agentContexts.get(agentId);
|
|
488
484
|
if (!agentContext)
|
|
489
485
|
continue;
|
|
490
|
-
const
|
|
486
|
+
const handoffTools = [];
|
|
491
487
|
for (const edge of edges) {
|
|
492
|
-
|
|
488
|
+
handoffTools.push(...this.createHandoffToolsForEdge(edge, agentId));
|
|
493
489
|
}
|
|
494
490
|
if (!agentContext.graphTools) {
|
|
495
491
|
agentContext.graphTools = [];
|
|
496
492
|
}
|
|
497
|
-
agentContext.graphTools.push(...
|
|
498
|
-
console.debug(`[MultiAgentGraph]
|
|
493
|
+
agentContext.graphTools.push(...handoffTools);
|
|
494
|
+
console.debug(`[MultiAgentGraph] Handoff tools for "${agentId}": [${handoffTools.map((t) => t.name).join(', ')}]`);
|
|
499
495
|
}
|
|
500
496
|
}
|
|
501
497
|
/**
|
|
502
|
-
* Create
|
|
503
|
-
* Each
|
|
498
|
+
* Create handoff tools for an edge (handles multiple destinations).
|
|
499
|
+
* Each handoff tool invokes the child agent's compiled subgraph inline,
|
|
504
500
|
* extracts the final AI message text, truncates it, and returns it as
|
|
505
501
|
* a string (which becomes a ToolMessage in the parent's context).
|
|
506
502
|
*
|
|
507
|
-
* @param edge - The graph edge defining the
|
|
503
|
+
* @param edge - The graph edge defining the handoff
|
|
508
504
|
* @param sourceAgentId - The ID of the parent/supervisor agent
|
|
509
505
|
*/
|
|
510
|
-
|
|
506
|
+
createHandoffToolsForEdge(edge, sourceAgentId) {
|
|
511
507
|
const tools = [];
|
|
512
508
|
const destinations = Array.isArray(edge.to) ? edge.to : [edge.to];
|
|
513
|
-
const maxResultChars = edge.maxResultChars ??
|
|
509
|
+
const maxResultChars = edge.maxResultChars ?? DEFAULT_HANDOFF_MAX_RESULT_CHARS;
|
|
514
510
|
for (const destination of destinations) {
|
|
515
|
-
const toolName = `${Constants.
|
|
511
|
+
const toolName = `${Constants.LC_HANDOFF_TO_}${destination}`;
|
|
516
512
|
const destContext = this.agentContexts.get(destination);
|
|
517
513
|
const toolDescription = edge.description ??
|
|
518
|
-
this.
|
|
514
|
+
this.buildDefaultHandoffDescription(destContext, destination);
|
|
519
515
|
const hasPromptInput = edge.prompt != null && typeof edge.prompt === 'string';
|
|
520
516
|
const promptInputDescription = hasPromptInput ? edge.prompt : undefined;
|
|
521
517
|
const promptKey = edge.promptKey ?? 'instructions';
|
|
@@ -525,7 +521,7 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
525
521
|
const input = rawInput;
|
|
526
522
|
const subgraph = registry.get(destination);
|
|
527
523
|
if (!subgraph) {
|
|
528
|
-
throw new Error(`
|
|
524
|
+
throw new Error(`Handoff target "${destination}" subgraph not found in registry. ` +
|
|
529
525
|
'This is a bug: createWorkflow() should have populated the subgraph registry.');
|
|
530
526
|
}
|
|
531
527
|
const state = getCurrentTaskInput();
|
|
@@ -542,7 +538,7 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
542
538
|
const childState = {
|
|
543
539
|
messages: childMessages,
|
|
544
540
|
};
|
|
545
|
-
console.debug(`[MultiAgentGraph]
|
|
541
|
+
console.debug(`[MultiAgentGraph] Handoff "${sourceAgentId}" -> "${destination}" START ` +
|
|
546
542
|
`(messages: ${childMessages.length})`);
|
|
547
543
|
try {
|
|
548
544
|
/**
|
|
@@ -551,17 +547,17 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
551
547
|
* and configurable data (thread_id, user_id) to the child.
|
|
552
548
|
*/
|
|
553
549
|
const result = await subgraph.invoke(childState, config);
|
|
554
|
-
const resultText = MultiAgentGraph.
|
|
555
|
-
const truncatedResult = MultiAgentGraph.
|
|
556
|
-
console.debug(`[MultiAgentGraph]
|
|
550
|
+
const resultText = MultiAgentGraph.extractHandoffResult(result.messages, destination);
|
|
551
|
+
const truncatedResult = MultiAgentGraph.truncateHandoffResult(resultText, maxResultChars);
|
|
552
|
+
console.debug(`[MultiAgentGraph] Handoff "${sourceAgentId}" -> "${destination}" DONE ` +
|
|
557
553
|
`(result: ${resultText.length} chars` +
|
|
558
554
|
`${truncatedResult.length < resultText.length ? `, truncated to ${truncatedResult.length}` : ''})`);
|
|
559
555
|
return truncatedResult;
|
|
560
556
|
}
|
|
561
557
|
catch (err) {
|
|
562
558
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
563
|
-
console.error(`[MultiAgentGraph]
|
|
564
|
-
return `[
|
|
559
|
+
console.error(`[MultiAgentGraph] Handoff "${sourceAgentId}" -> "${destination}" ERROR:`, errorMessage);
|
|
560
|
+
return `[Handoff to "${destination}" failed: ${errorMessage}]`;
|
|
565
561
|
}
|
|
566
562
|
}, {
|
|
567
563
|
name: toolName,
|
|
@@ -589,7 +585,7 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
589
585
|
* @param messages - The child agent's output messages
|
|
590
586
|
* @param agentId - The child agent ID (for fallback message)
|
|
591
587
|
*/
|
|
592
|
-
static
|
|
588
|
+
static extractHandoffResult(messages, agentId) {
|
|
593
589
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
594
590
|
const msg = messages[i];
|
|
595
591
|
if (msg.getType() !== 'ai')
|
|
@@ -616,17 +612,17 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
616
612
|
return `[Agent "${agentId}" completed but produced no text output]`;
|
|
617
613
|
}
|
|
618
614
|
/**
|
|
619
|
-
* Truncate
|
|
615
|
+
* Truncate handoff result using head/tail strategy (60/40 split).
|
|
620
616
|
* Preserves the beginning (key findings) and end (conclusions).
|
|
621
617
|
* Matches the TaskTool.truncateResult pattern from Ranger.
|
|
622
618
|
* @param result - The full result text
|
|
623
619
|
* @param maxChars - Maximum allowed characters
|
|
624
620
|
*/
|
|
625
|
-
static
|
|
621
|
+
static truncateHandoffResult(result, maxChars) {
|
|
626
622
|
if (!result || result.length <= maxChars) {
|
|
627
623
|
return result;
|
|
628
624
|
}
|
|
629
|
-
const truncationNotice = '\n\n[...
|
|
625
|
+
const truncationNotice = '\n\n[... handoff output truncated — middle section omitted to fit parent context ...]\n\n';
|
|
630
626
|
const available = maxChars - truncationNotice.length;
|
|
631
627
|
if (available <= 0) {
|
|
632
628
|
return result.substring(0, maxChars);
|
|
@@ -638,17 +634,17 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
638
634
|
result.substring(result.length - tailSize));
|
|
639
635
|
}
|
|
640
636
|
/**
|
|
641
|
-
* Build a meaningful default description for a
|
|
637
|
+
* Build a meaningful default description for a handoff tool.
|
|
642
638
|
* @param destContext - AgentContext of the destination agent
|
|
643
639
|
* @param destinationId - Raw agent ID (fallback)
|
|
644
640
|
*/
|
|
645
|
-
|
|
641
|
+
buildDefaultHandoffDescription(destContext, destinationId) {
|
|
646
642
|
const displayName = destContext?.name ?? destinationId;
|
|
647
643
|
const agentDescription = destContext?.description;
|
|
648
644
|
if (agentDescription != null && agentDescription !== '') {
|
|
649
|
-
return `
|
|
645
|
+
return `Hand off task to "${displayName}": ${agentDescription}. The agent will execute and return its result.`;
|
|
650
646
|
}
|
|
651
|
-
return `
|
|
647
|
+
return `Hand off task to "${displayName}" and receive its result.`;
|
|
652
648
|
}
|
|
653
649
|
/**
|
|
654
650
|
* Create a complete agent subgraph (similar to createReactAgent)
|
|
@@ -669,7 +665,7 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
669
665
|
* @param agentId - The agent ID to check for handoff reception
|
|
670
666
|
* @returns Object with filtered messages, extracted instructions, source agent, and parallel siblings
|
|
671
667
|
*/
|
|
672
|
-
|
|
668
|
+
processTransferReception(messages, agentId) {
|
|
673
669
|
if (messages.length === 0)
|
|
674
670
|
return null;
|
|
675
671
|
/**
|
|
@@ -698,8 +694,8 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
698
694
|
destinationAgent = toolName.replace(Constants.LC_TRANSFER_TO_, '');
|
|
699
695
|
}
|
|
700
696
|
else if (isConditionalTransfer) {
|
|
701
|
-
const
|
|
702
|
-
destinationAgent = typeof
|
|
697
|
+
const transferDest = candidateMsg.additional_kwargs.handoff_destination;
|
|
698
|
+
destinationAgent = typeof transferDest === 'string' ? transferDest : null;
|
|
703
699
|
}
|
|
704
700
|
/** Check if this transfer targets our agent */
|
|
705
701
|
if (destinationAgent === agentId) {
|
|
@@ -715,7 +711,7 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
715
711
|
const contentStr = typeof toolMessage.content === 'string'
|
|
716
712
|
? toolMessage.content
|
|
717
713
|
: JSON.stringify(toolMessage.content);
|
|
718
|
-
const instructionsMatch = contentStr.match(
|
|
714
|
+
const instructionsMatch = contentStr.match(TRANSFER_INSTRUCTIONS_PATTERN);
|
|
719
715
|
const instructions = instructionsMatch?.[1]?.trim() ?? null;
|
|
720
716
|
/** Extract source agent name from additional_kwargs */
|
|
721
717
|
const handoffSourceName = toolMessage.additional_kwargs.handoff_source_name;
|
|
@@ -893,7 +889,7 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
893
889
|
};
|
|
894
890
|
}
|
|
895
891
|
/**
|
|
896
|
-
* Create the multi-agent workflow with
|
|
892
|
+
* Create the multi-agent workflow with handoffs, transfers, and sequences
|
|
897
893
|
*/
|
|
898
894
|
createWorkflow() {
|
|
899
895
|
const StateAnnotation = Annotation.Root({
|
|
@@ -919,54 +915,54 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
919
915
|
// Add all agents as complete subgraphs
|
|
920
916
|
for (const [agentId] of this.agentContexts) {
|
|
921
917
|
// Get all possible destinations for this agent
|
|
922
|
-
const
|
|
923
|
-
const
|
|
924
|
-
// Check
|
|
925
|
-
for (const edge of this.
|
|
918
|
+
const transferDestinations = new Set();
|
|
919
|
+
const sequenceDestinations = new Set();
|
|
920
|
+
// Check transfer edges for destinations
|
|
921
|
+
for (const edge of this.transferEdges) {
|
|
926
922
|
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
927
923
|
if (sources.includes(agentId) === true) {
|
|
928
924
|
const dests = Array.isArray(edge.to) ? edge.to : [edge.to];
|
|
929
|
-
dests.forEach((dest) =>
|
|
925
|
+
dests.forEach((dest) => transferDestinations.add(dest));
|
|
930
926
|
}
|
|
931
927
|
}
|
|
932
|
-
// Check
|
|
933
|
-
for (const edge of this.
|
|
928
|
+
// Check sequence edges for destinations
|
|
929
|
+
for (const edge of this.sequenceEdges) {
|
|
934
930
|
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
935
931
|
if (sources.includes(agentId) === true) {
|
|
936
932
|
const dests = Array.isArray(edge.to) ? edge.to : [edge.to];
|
|
937
|
-
dests.forEach((dest) =>
|
|
933
|
+
dests.forEach((dest) => sequenceDestinations.add(dest));
|
|
938
934
|
}
|
|
939
935
|
}
|
|
940
|
-
/** Check if this agent has BOTH
|
|
941
|
-
const
|
|
942
|
-
const
|
|
943
|
-
const needsCommandRouting =
|
|
936
|
+
/** Check if this agent has BOTH transfer and sequence edges */
|
|
937
|
+
const hasTransferEdges = transferDestinations.size > 0;
|
|
938
|
+
const hasSequenceEdges = sequenceDestinations.size > 0;
|
|
939
|
+
const needsCommandRouting = hasTransferEdges && hasSequenceEdges;
|
|
944
940
|
/** Collect all possible destinations for this agent */
|
|
945
941
|
const allDestinations = new Set([
|
|
946
|
-
...
|
|
947
|
-
...
|
|
942
|
+
...transferDestinations,
|
|
943
|
+
...sequenceDestinations,
|
|
948
944
|
]);
|
|
949
|
-
if (
|
|
945
|
+
if (transferDestinations.size > 0 || sequenceDestinations.size === 0) {
|
|
950
946
|
allDestinations.add(END);
|
|
951
947
|
}
|
|
952
948
|
/** Agent subgraph (includes agent + tools) */
|
|
953
949
|
const agentSubgraph = this.createAgentSubgraph(agentId);
|
|
954
|
-
/** Register subgraph for
|
|
950
|
+
/** Register subgraph for handoff tools (lazy reference resolution) */
|
|
955
951
|
this.subgraphRegistry.set(agentId, agentSubgraph);
|
|
956
952
|
/** Wrapper function that handles agentMessages channel, handoff reception, and conditional routing */
|
|
957
953
|
const agentWrapper = async (state, config) => {
|
|
958
954
|
console.debug(`[MultiAgentGraph] Agent "${agentId}" wrapper ENTRY (messages: ${state.messages.length}, needsCommandRouting: ${needsCommandRouting})`);
|
|
959
955
|
let result;
|
|
960
956
|
/**
|
|
961
|
-
* Check if this agent is receiving a
|
|
957
|
+
* Check if this agent is receiving a transfer.
|
|
962
958
|
* If so, filter out the transfer messages and inject instructions as preamble.
|
|
963
959
|
* This prevents the receiving agent from seeing the transfer as "completed work"
|
|
964
960
|
* and prematurely producing an end token.
|
|
965
961
|
*/
|
|
966
|
-
const
|
|
967
|
-
if (
|
|
968
|
-
const { filteredMessages, instructions, sourceAgentName, parallelSiblings, } =
|
|
969
|
-
console.debug(`[MultiAgentGraph] Agent "${agentId}" receiving
|
|
962
|
+
const transferContext = this.processTransferReception(state.messages, agentId);
|
|
963
|
+
if (transferContext !== null) {
|
|
964
|
+
const { filteredMessages, instructions, sourceAgentName, parallelSiblings, } = transferContext;
|
|
965
|
+
console.debug(`[MultiAgentGraph] Agent "${agentId}" receiving transfer from "${sourceAgentName}" (instructions: ${instructions != null}, parallelSiblings: ${parallelSiblings.length})`);
|
|
970
966
|
/**
|
|
971
967
|
* Set handoff context on the receiving agent.
|
|
972
968
|
* Uses pre-computed graph position for depth and parallel info.
|
|
@@ -1083,24 +1079,24 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
1083
1079
|
/** Track the last agent that produced output for continuation support */
|
|
1084
1080
|
this.lastActiveAgentId = agentId;
|
|
1085
1081
|
console.debug(`[MultiAgentGraph] Agent "${agentId}" wrapper EXIT (result messages: ${result.messages.length})`);
|
|
1086
|
-
/** If agent has both
|
|
1082
|
+
/** If agent has both transfer and sequence edges, use Command for exclusive routing */
|
|
1087
1083
|
if (needsCommandRouting) {
|
|
1088
|
-
/** Check if a
|
|
1084
|
+
/** Check if a transfer occurred */
|
|
1089
1085
|
const lastMessage = result.messages[result.messages.length - 1];
|
|
1090
1086
|
if (lastMessage != null &&
|
|
1091
1087
|
lastMessage.getType() === 'tool' &&
|
|
1092
1088
|
typeof lastMessage.name === 'string' &&
|
|
1093
1089
|
lastMessage.name.startsWith(Constants.LC_TRANSFER_TO_)) {
|
|
1094
|
-
/**
|
|
1095
|
-
const
|
|
1096
|
-
console.debug(`[MultiAgentGraph] Command routing: "${agentId}" ->
|
|
1090
|
+
/** Transfer occurred - extract destination and navigate there exclusively */
|
|
1091
|
+
const transferDest = lastMessage.name.replace(Constants.LC_TRANSFER_TO_, '');
|
|
1092
|
+
console.debug(`[MultiAgentGraph] Command routing: "${agentId}" -> transfer to "${transferDest}" (sequence edges skipped: [${Array.from(sequenceDestinations).join(', ')}])`);
|
|
1097
1093
|
/** Validate destination agent exists */
|
|
1098
|
-
if (!this.agentContexts.has(
|
|
1094
|
+
if (!this.agentContexts.has(transferDest)) {
|
|
1099
1095
|
const availableAgents = Array.from(this.agentContexts.keys()).join(', ');
|
|
1100
|
-
console.error(`[MultiAgentGraph]
|
|
1096
|
+
console.error(`[MultiAgentGraph] Transfer to non-existent agent "${transferDest}". Available: ${availableAgents}`);
|
|
1101
1097
|
/** Return error to model so it can self-correct */
|
|
1102
1098
|
const errorMsg = new ToolMessage({
|
|
1103
|
-
content: `Transfer failed: agent "${
|
|
1099
|
+
content: `Transfer failed: agent "${transferDest}" does not exist. Available agents: ${availableAgents}. Please choose a valid agent to transfer to.`,
|
|
1104
1100
|
tool_call_id: lastMessage.tool_call_id,
|
|
1105
1101
|
name: lastMessage.name,
|
|
1106
1102
|
});
|
|
@@ -1110,7 +1106,7 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
1110
1106
|
};
|
|
1111
1107
|
}
|
|
1112
1108
|
/** Pre-handoff context compaction: if receiving agent has smaller budget */
|
|
1113
|
-
const receiverContext = this.agentContexts.get(
|
|
1109
|
+
const receiverContext = this.agentContexts.get(transferDest);
|
|
1114
1110
|
const senderContext = this.agentContexts.get(agentId);
|
|
1115
1111
|
if (receiverContext?.maxContextTokens != null &&
|
|
1116
1112
|
senderContext?.tokenCounter != null &&
|
|
@@ -1122,7 +1118,7 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
1122
1118
|
const receiverBudget = receiverContext.maxContextTokens;
|
|
1123
1119
|
if (currentSize > receiverBudget * 0.7) {
|
|
1124
1120
|
console.warn(`[MultiAgentGraph] Pre-handoff compaction: context (${currentSize} tokens) exceeds ` +
|
|
1125
|
-
`70% of receiver "${
|
|
1121
|
+
`70% of receiver "${transferDest}" budget (${receiverBudget} tokens)`);
|
|
1126
1122
|
/** Generate handoff briefing */
|
|
1127
1123
|
const senderName = senderContext.name ?? agentId;
|
|
1128
1124
|
if (senderContext.summarizeCallback) {
|
|
@@ -1134,8 +1130,8 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
1134
1130
|
summaryBudget: Math.floor(receiverBudget * 0.2),
|
|
1135
1131
|
isMultiAgent: true,
|
|
1136
1132
|
agentWorkflowState: {
|
|
1137
|
-
currentAgentId:
|
|
1138
|
-
agentChain: [agentId,
|
|
1133
|
+
currentAgentId: transferDest,
|
|
1134
|
+
agentChain: [agentId, transferDest],
|
|
1139
1135
|
pendingAgents: [],
|
|
1140
1136
|
},
|
|
1141
1137
|
});
|
|
@@ -1173,13 +1169,13 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
1173
1169
|
}
|
|
1174
1170
|
return new Command({
|
|
1175
1171
|
update: result,
|
|
1176
|
-
goto:
|
|
1172
|
+
goto: transferDest,
|
|
1177
1173
|
});
|
|
1178
1174
|
}
|
|
1179
1175
|
else {
|
|
1180
|
-
/** No
|
|
1181
|
-
console.debug(`[MultiAgentGraph] Command routing: "${agentId}" -> no
|
|
1182
|
-
const directDests = Array.from(
|
|
1176
|
+
/** No transfer - proceed with sequence edges */
|
|
1177
|
+
console.debug(`[MultiAgentGraph] Command routing: "${agentId}" -> no transfer, following sequence edges: [${Array.from(sequenceDestinations).join(', ')}]`);
|
|
1178
|
+
const directDests = Array.from(sequenceDestinations);
|
|
1183
1179
|
if (directDests.length === 1) {
|
|
1184
1180
|
return new Command({
|
|
1185
1181
|
update: result,
|
|
@@ -1210,11 +1206,11 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
1210
1206
|
builder.addEdge(START, startNode);
|
|
1211
1207
|
}
|
|
1212
1208
|
/**
|
|
1213
|
-
* Add
|
|
1209
|
+
* Add sequence edges for automatic transitions
|
|
1214
1210
|
* Group edges by destination to handle fan-in scenarios
|
|
1215
1211
|
*/
|
|
1216
1212
|
const edgesByDestination = new Map();
|
|
1217
|
-
for (const edge of this.
|
|
1213
|
+
for (const edge of this.sequenceEdges) {
|
|
1218
1214
|
const destinations = Array.isArray(edge.to) ? edge.to : [edge.to];
|
|
1219
1215
|
for (const destination of destinations) {
|
|
1220
1216
|
if (!edgesByDestination.has(destination)) {
|
|
@@ -1303,17 +1299,17 @@ class MultiAgentGraph extends StandardGraph {
|
|
|
1303
1299
|
for (const edge of edges) {
|
|
1304
1300
|
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
1305
1301
|
for (const source of sources) {
|
|
1306
|
-
/** Check if this source node has both
|
|
1307
|
-
const
|
|
1302
|
+
/** Check if this source node has both transfer and sequence edges */
|
|
1303
|
+
const sourceTransferEdges = this.transferEdges.filter((e) => {
|
|
1308
1304
|
const eSources = Array.isArray(e.from) ? e.from : [e.from];
|
|
1309
1305
|
return eSources.includes(source);
|
|
1310
1306
|
});
|
|
1311
|
-
const
|
|
1307
|
+
const sourceSequenceEdges = this.sequenceEdges.filter((e) => {
|
|
1312
1308
|
const eSources = Array.isArray(e.from) ? e.from : [e.from];
|
|
1313
1309
|
return eSources.includes(source);
|
|
1314
1310
|
});
|
|
1315
1311
|
/** Skip adding edge if source uses Command routing (has both types) */
|
|
1316
|
-
if (
|
|
1312
|
+
if (sourceTransferEdges.length > 0 && sourceSequenceEdges.length > 0) {
|
|
1317
1313
|
continue;
|
|
1318
1314
|
}
|
|
1319
1315
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|