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