@illuma-ai/agents 1.1.14 → 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 +14 -3
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +304 -106
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/main.cjs +2 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/types/graph.cjs.map +1 -1
- package/dist/esm/common/enum.mjs +12 -4
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +306 -108
- 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 -3
- package/dist/types/graphs/MultiAgentGraph.d.ts +72 -18
- package/dist/types/types/graph.d.ts +17 -5
- package/package.json +1 -1
- package/src/common/__tests__/enum.test.ts +15 -7
- package/src/common/enum.ts +13 -3
- package/src/graphs/MultiAgentGraph.ts +385 -107
- package/src/graphs/__tests__/multi-agent-delegate.test.ts +208 -0
- package/src/graphs/__tests__/multi-agent-edges.test.ts +98 -61
- 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/tools/search/search.test.ts +173 -0
- package/src/types/graph.ts +17 -5
|
@@ -13,26 +13,35 @@ 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 = [];
|
|
37
|
+
/**
|
|
38
|
+
* Lazily populated registry of compiled subgraphs, keyed by agentId.
|
|
39
|
+
* Handoff tools are created in the constructor but reference subgraphs
|
|
40
|
+
* that are only created in createWorkflow(). This Map bridges that gap —
|
|
41
|
+
* tools capture the Map reference in their closure, and createWorkflow()
|
|
42
|
+
* populates it before any tool invocation occurs.
|
|
43
|
+
*/
|
|
44
|
+
subgraphRegistry = new Map();
|
|
36
45
|
/**
|
|
37
46
|
* Map of agentId to parallel group info.
|
|
38
47
|
* Contains groupId (incrementing number reflecting execution order) for agents in parallel groups.
|
|
@@ -54,37 +63,39 @@ class MultiAgentGraph extends Graph.StandardGraph {
|
|
|
54
63
|
this.edges = input.edges;
|
|
55
64
|
this.categorizeEdges();
|
|
56
65
|
this.analyzeGraph();
|
|
66
|
+
this.createTransferTools();
|
|
57
67
|
this.createHandoffTools();
|
|
58
68
|
console.debug(`[MultiAgentGraph] Constructor complete: ${this.agentContexts.size} agents, ${this.edges.length} edges`);
|
|
59
69
|
}
|
|
60
70
|
/**
|
|
61
|
-
* Categorize edges into handoff and
|
|
71
|
+
* Categorize edges into handoff, transfer, and sequence types
|
|
62
72
|
*/
|
|
63
73
|
categorizeEdges() {
|
|
64
74
|
for (const edge of this.edges) {
|
|
65
|
-
|
|
66
|
-
// Edges with explicit 'direct' type or multi-destination without conditions are direct edges
|
|
67
|
-
if (edge.edgeType === _enum.EdgeType.DIRECT) {
|
|
68
|
-
this.directEdges.push(edge);
|
|
69
|
-
}
|
|
70
|
-
else if (edge.edgeType === _enum.EdgeType.HANDOFF || edge.condition != null) {
|
|
75
|
+
if (edge.edgeType === _enum.EdgeType.HANDOFF) {
|
|
71
76
|
this.handoffEdges.push(edge);
|
|
72
77
|
}
|
|
78
|
+
else if (edge.edgeType === _enum.EdgeType.SEQUENCE) {
|
|
79
|
+
this.sequenceEdges.push(edge);
|
|
80
|
+
}
|
|
81
|
+
else if (edge.edgeType === _enum.EdgeType.TRANSFER || edge.condition != null) {
|
|
82
|
+
this.transferEdges.push(edge);
|
|
83
|
+
}
|
|
73
84
|
else {
|
|
74
|
-
// Default: single-to-single edges are
|
|
85
|
+
// Default: single-to-single edges are transfer, single-to-multiple are sequence
|
|
75
86
|
const destinations = Array.isArray(edge.to) ? edge.to : [edge.to];
|
|
76
87
|
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
77
88
|
if (sources.length === 1 && destinations.length > 1) {
|
|
78
|
-
// Fan-out pattern defaults to
|
|
79
|
-
this.
|
|
89
|
+
// Fan-out pattern defaults to sequence
|
|
90
|
+
this.sequenceEdges.push(edge);
|
|
80
91
|
}
|
|
81
92
|
else {
|
|
82
|
-
// Everything else defaults to
|
|
83
|
-
this.
|
|
93
|
+
// Everything else defaults to transfer
|
|
94
|
+
this.transferEdges.push(edge);
|
|
84
95
|
}
|
|
85
96
|
}
|
|
86
97
|
}
|
|
87
|
-
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)`);
|
|
88
99
|
}
|
|
89
100
|
/**
|
|
90
101
|
* Analyze graph structure to determine starting nodes and connections
|
|
@@ -142,8 +153,8 @@ class MultiAgentGraph extends Graph.StandardGraph {
|
|
|
142
153
|
if (visited.has(current))
|
|
143
154
|
continue;
|
|
144
155
|
visited.add(current);
|
|
145
|
-
// Find
|
|
146
|
-
for (const edge of this.
|
|
156
|
+
// Find sequence edges from this node
|
|
157
|
+
for (const edge of this.sequenceEdges) {
|
|
147
158
|
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
148
159
|
if (!sources.includes(current))
|
|
149
160
|
continue;
|
|
@@ -170,8 +181,8 @@ class MultiAgentGraph extends Graph.StandardGraph {
|
|
|
170
181
|
}
|
|
171
182
|
}
|
|
172
183
|
}
|
|
173
|
-
// Also follow handoff edges for traversal (
|
|
174
|
-
for (const edge of this.handoffEdges) {
|
|
184
|
+
// Also follow transfer and handoff edges for traversal (they don't create parallel groups)
|
|
185
|
+
for (const edge of [...this.transferEdges, ...this.handoffEdges]) {
|
|
175
186
|
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
176
187
|
if (!sources.includes(current))
|
|
177
188
|
continue;
|
|
@@ -214,46 +225,44 @@ class MultiAgentGraph extends Graph.StandardGraph {
|
|
|
214
225
|
return this.agentParallelGroups.get(agentId);
|
|
215
226
|
}
|
|
216
227
|
/**
|
|
217
|
-
* 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.
|
|
218
230
|
*/
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const
|
|
222
|
-
// Only process handoff edges for tool creation
|
|
223
|
-
for (const edge of this.handoffEdges) {
|
|
231
|
+
createTransferTools() {
|
|
232
|
+
const transfersByAgent = new Map();
|
|
233
|
+
for (const edge of this.transferEdges) {
|
|
224
234
|
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
225
235
|
sources.forEach((source) => {
|
|
226
|
-
if (!
|
|
227
|
-
|
|
236
|
+
if (!transfersByAgent.has(source)) {
|
|
237
|
+
transfersByAgent.set(source, []);
|
|
228
238
|
}
|
|
229
|
-
|
|
239
|
+
transfersByAgent.get(source).push(edge);
|
|
230
240
|
});
|
|
231
241
|
}
|
|
232
|
-
|
|
233
|
-
for (const [agentId, edges] of handoffsByAgent) {
|
|
242
|
+
for (const [agentId, edges] of transfersByAgent) {
|
|
234
243
|
const agentContext = this.agentContexts.get(agentId);
|
|
235
244
|
if (!agentContext)
|
|
236
245
|
continue;
|
|
237
|
-
|
|
238
|
-
const handoffTools = [];
|
|
246
|
+
const transferTools = [];
|
|
239
247
|
const sourceAgentName = agentContext.name ?? agentId;
|
|
240
248
|
for (const edge of edges) {
|
|
241
|
-
|
|
249
|
+
transferTools.push(...this.createTransferToolsForEdge(edge, agentId, sourceAgentName));
|
|
242
250
|
}
|
|
243
251
|
if (!agentContext.graphTools) {
|
|
244
252
|
agentContext.graphTools = [];
|
|
245
253
|
}
|
|
246
|
-
agentContext.graphTools.push(...
|
|
247
|
-
console.debug(`[MultiAgentGraph]
|
|
254
|
+
agentContext.graphTools.push(...transferTools);
|
|
255
|
+
console.debug(`[MultiAgentGraph] Transfer tools for "${agentId}": [${transferTools.map((t) => t.name).join(', ')}]`);
|
|
248
256
|
}
|
|
249
257
|
}
|
|
250
258
|
/**
|
|
251
|
-
* Create
|
|
252
|
-
*
|
|
253
|
-
* @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
|
|
254
263
|
* @param sourceAgentName - The human-readable name of the source agent
|
|
255
264
|
*/
|
|
256
|
-
|
|
265
|
+
createTransferToolsForEdge(edge, sourceAgentId, sourceAgentName) {
|
|
257
266
|
const tools$1 = [];
|
|
258
267
|
const destinations = Array.isArray(edge.to) ? edge.to : [edge.to];
|
|
259
268
|
/** If there's a condition, create a single conditional handoff tool */
|
|
@@ -261,8 +270,8 @@ class MultiAgentGraph extends Graph.StandardGraph {
|
|
|
261
270
|
const toolName = 'conditional_transfer';
|
|
262
271
|
const toolDescription = edge.description ?? 'Conditionally transfer control based on state';
|
|
263
272
|
/** Check if we have a prompt for handoff input */
|
|
264
|
-
const
|
|
265
|
-
const
|
|
273
|
+
const hasTransferInput = edge.prompt != null && typeof edge.prompt === 'string';
|
|
274
|
+
const transferInputDescription = hasTransferInput ? edge.prompt : undefined;
|
|
266
275
|
const promptKey = edge.promptKey ?? 'instructions';
|
|
267
276
|
tools$1.push(tools.tool(async (rawInput, config) => {
|
|
268
277
|
const input = rawInput;
|
|
@@ -286,7 +295,7 @@ class MultiAgentGraph extends Graph.StandardGraph {
|
|
|
286
295
|
destination = Array.isArray(result) ? result[0] : destinations[0];
|
|
287
296
|
}
|
|
288
297
|
let content = `Conditionally transferred to ${destination}`;
|
|
289
|
-
if (
|
|
298
|
+
if (hasTransferInput &&
|
|
290
299
|
promptKey in input &&
|
|
291
300
|
input[promptKey] != null) {
|
|
292
301
|
content += `\n\n${promptKey.charAt(0).toUpperCase() + promptKey.slice(1)}: ${input[promptKey]}`;
|
|
@@ -309,13 +318,13 @@ class MultiAgentGraph extends Graph.StandardGraph {
|
|
|
309
318
|
});
|
|
310
319
|
}, {
|
|
311
320
|
name: toolName,
|
|
312
|
-
schema:
|
|
321
|
+
schema: hasTransferInput
|
|
313
322
|
? {
|
|
314
323
|
type: 'object',
|
|
315
324
|
properties: {
|
|
316
325
|
[promptKey]: {
|
|
317
326
|
type: 'string',
|
|
318
|
-
description:
|
|
327
|
+
description: transferInputDescription,
|
|
319
328
|
},
|
|
320
329
|
},
|
|
321
330
|
required: [],
|
|
@@ -330,10 +339,10 @@ class MultiAgentGraph extends Graph.StandardGraph {
|
|
|
330
339
|
const toolName = `${_enum.Constants.LC_TRANSFER_TO_}${destination}`;
|
|
331
340
|
const destContext = this.agentContexts.get(destination);
|
|
332
341
|
const toolDescription = edge.description ??
|
|
333
|
-
this.
|
|
342
|
+
this.buildDefaultTransferDescription(destContext, destination);
|
|
334
343
|
/** Check if we have a prompt for handoff input */
|
|
335
|
-
const
|
|
336
|
-
const
|
|
344
|
+
const hasTransferInput = edge.prompt != null && typeof edge.prompt === 'string';
|
|
345
|
+
const transferInputDescription = hasTransferInput
|
|
337
346
|
? edge.prompt
|
|
338
347
|
: undefined;
|
|
339
348
|
const promptKey = edge.promptKey ?? 'instructions';
|
|
@@ -342,7 +351,7 @@ class MultiAgentGraph extends Graph.StandardGraph {
|
|
|
342
351
|
const toolCallId = config?.toolCall?.id ??
|
|
343
352
|
'unknown';
|
|
344
353
|
let content = `Successfully transferred to ${destination}`;
|
|
345
|
-
if (
|
|
354
|
+
if (hasTransferInput &&
|
|
346
355
|
promptKey in input &&
|
|
347
356
|
input[promptKey] != null) {
|
|
348
357
|
content += `\n\n${promptKey.charAt(0).toUpperCase() + promptKey.slice(1)}: ${input[promptKey]}`;
|
|
@@ -419,13 +428,13 @@ class MultiAgentGraph extends Graph.StandardGraph {
|
|
|
419
428
|
});
|
|
420
429
|
}, {
|
|
421
430
|
name: toolName,
|
|
422
|
-
schema:
|
|
431
|
+
schema: hasTransferInput
|
|
423
432
|
? {
|
|
424
433
|
type: 'object',
|
|
425
434
|
properties: {
|
|
426
435
|
[promptKey]: {
|
|
427
436
|
type: 'string',
|
|
428
|
-
description:
|
|
437
|
+
description: transferInputDescription,
|
|
429
438
|
},
|
|
430
439
|
},
|
|
431
440
|
required: [],
|
|
@@ -438,13 +447,13 @@ class MultiAgentGraph extends Graph.StandardGraph {
|
|
|
438
447
|
return tools$1;
|
|
439
448
|
}
|
|
440
449
|
/**
|
|
441
|
-
* Builds a meaningful default description for a
|
|
450
|
+
* Builds a meaningful default description for a transfer tool when no explicit
|
|
442
451
|
* edge.description is provided. Uses the destination agent's name and description
|
|
443
452
|
* so the LLM can make informed routing decisions.
|
|
444
453
|
* @param destContext - AgentContext of the destination agent (may be undefined)
|
|
445
454
|
* @param destinationId - Raw agent ID (fallback when context unavailable)
|
|
446
455
|
*/
|
|
447
|
-
|
|
456
|
+
buildDefaultTransferDescription(destContext, destinationId) {
|
|
448
457
|
const displayName = destContext?.name ?? destinationId;
|
|
449
458
|
const agentDescription = destContext?.description;
|
|
450
459
|
if (agentDescription != null && agentDescription !== '') {
|
|
@@ -452,6 +461,193 @@ class MultiAgentGraph extends Graph.StandardGraph {
|
|
|
452
461
|
}
|
|
453
462
|
return `Transfer control to "${displayName}"`;
|
|
454
463
|
}
|
|
464
|
+
/**
|
|
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.
|
|
470
|
+
*
|
|
471
|
+
* This enables the supervisor pattern: parent calls child → gets result → thinks → calls another.
|
|
472
|
+
*/
|
|
473
|
+
createHandoffTools() {
|
|
474
|
+
const handoffsByAgent = new Map();
|
|
475
|
+
for (const edge of this.handoffEdges) {
|
|
476
|
+
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
477
|
+
sources.forEach((source) => {
|
|
478
|
+
if (!handoffsByAgent.has(source)) {
|
|
479
|
+
handoffsByAgent.set(source, []);
|
|
480
|
+
}
|
|
481
|
+
handoffsByAgent.get(source).push(edge);
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
for (const [agentId, edges] of handoffsByAgent) {
|
|
485
|
+
const agentContext = this.agentContexts.get(agentId);
|
|
486
|
+
if (!agentContext)
|
|
487
|
+
continue;
|
|
488
|
+
const handoffTools = [];
|
|
489
|
+
for (const edge of edges) {
|
|
490
|
+
handoffTools.push(...this.createHandoffToolsForEdge(edge, agentId));
|
|
491
|
+
}
|
|
492
|
+
if (!agentContext.graphTools) {
|
|
493
|
+
agentContext.graphTools = [];
|
|
494
|
+
}
|
|
495
|
+
agentContext.graphTools.push(...handoffTools);
|
|
496
|
+
console.debug(`[MultiAgentGraph] Handoff tools for "${agentId}": [${handoffTools.map((t) => t.name).join(', ')}]`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Create handoff tools for an edge (handles multiple destinations).
|
|
501
|
+
* Each handoff tool invokes the child agent's compiled subgraph inline,
|
|
502
|
+
* extracts the final AI message text, truncates it, and returns it as
|
|
503
|
+
* a string (which becomes a ToolMessage in the parent's context).
|
|
504
|
+
*
|
|
505
|
+
* @param edge - The graph edge defining the handoff
|
|
506
|
+
* @param sourceAgentId - The ID of the parent/supervisor agent
|
|
507
|
+
*/
|
|
508
|
+
createHandoffToolsForEdge(edge, sourceAgentId) {
|
|
509
|
+
const tools$1 = [];
|
|
510
|
+
const destinations = Array.isArray(edge.to) ? edge.to : [edge.to];
|
|
511
|
+
const maxResultChars = edge.maxResultChars ?? _enum.DEFAULT_HANDOFF_MAX_RESULT_CHARS;
|
|
512
|
+
for (const destination of destinations) {
|
|
513
|
+
const toolName = `${_enum.Constants.LC_HANDOFF_TO_}${destination}`;
|
|
514
|
+
const destContext = this.agentContexts.get(destination);
|
|
515
|
+
const toolDescription = edge.description ??
|
|
516
|
+
this.buildDefaultHandoffDescription(destContext, destination);
|
|
517
|
+
const hasPromptInput = edge.prompt != null && typeof edge.prompt === 'string';
|
|
518
|
+
const promptInputDescription = hasPromptInput ? edge.prompt : undefined;
|
|
519
|
+
const promptKey = edge.promptKey ?? 'instructions';
|
|
520
|
+
/** Capture registry reference — Map populated in createWorkflow() */
|
|
521
|
+
const registry = this.subgraphRegistry;
|
|
522
|
+
tools$1.push(tools.tool(async (rawInput, config) => {
|
|
523
|
+
const input = rawInput;
|
|
524
|
+
const subgraph = registry.get(destination);
|
|
525
|
+
if (!subgraph) {
|
|
526
|
+
throw new Error(`Handoff target "${destination}" subgraph not found in registry. ` +
|
|
527
|
+
'This is a bug: createWorkflow() should have populated the subgraph registry.');
|
|
528
|
+
}
|
|
529
|
+
const state = langgraph.getCurrentTaskInput();
|
|
530
|
+
let childMessages = [...state.messages];
|
|
531
|
+
/** Inject instructions as HumanMessage if provided by the parent LLM */
|
|
532
|
+
if (hasPromptInput &&
|
|
533
|
+
promptKey in input &&
|
|
534
|
+
input[promptKey] != null) {
|
|
535
|
+
childMessages = [
|
|
536
|
+
...childMessages,
|
|
537
|
+
new messages.HumanMessage(String(input[promptKey])),
|
|
538
|
+
];
|
|
539
|
+
}
|
|
540
|
+
const childState = {
|
|
541
|
+
messages: childMessages,
|
|
542
|
+
};
|
|
543
|
+
console.debug(`[MultiAgentGraph] Handoff "${sourceAgentId}" -> "${destination}" START ` +
|
|
544
|
+
`(messages: ${childMessages.length})`);
|
|
545
|
+
try {
|
|
546
|
+
/**
|
|
547
|
+
* Invoke the child subgraph with config propagation.
|
|
548
|
+
* Config carries callbacks (for SSE streaming), abort signal,
|
|
549
|
+
* and configurable data (thread_id, user_id) to the child.
|
|
550
|
+
*/
|
|
551
|
+
const result = await subgraph.invoke(childState, config);
|
|
552
|
+
const resultText = MultiAgentGraph.extractHandoffResult(result.messages, destination);
|
|
553
|
+
const truncatedResult = MultiAgentGraph.truncateHandoffResult(resultText, maxResultChars);
|
|
554
|
+
console.debug(`[MultiAgentGraph] Handoff "${sourceAgentId}" -> "${destination}" DONE ` +
|
|
555
|
+
`(result: ${resultText.length} chars` +
|
|
556
|
+
`${truncatedResult.length < resultText.length ? `, truncated to ${truncatedResult.length}` : ''})`);
|
|
557
|
+
return truncatedResult;
|
|
558
|
+
}
|
|
559
|
+
catch (err) {
|
|
560
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
561
|
+
console.error(`[MultiAgentGraph] Handoff "${sourceAgentId}" -> "${destination}" ERROR:`, errorMessage);
|
|
562
|
+
return `[Handoff to "${destination}" failed: ${errorMessage}]`;
|
|
563
|
+
}
|
|
564
|
+
}, {
|
|
565
|
+
name: toolName,
|
|
566
|
+
schema: hasPromptInput
|
|
567
|
+
? {
|
|
568
|
+
type: 'object',
|
|
569
|
+
properties: {
|
|
570
|
+
[promptKey]: {
|
|
571
|
+
type: 'string',
|
|
572
|
+
description: promptInputDescription,
|
|
573
|
+
},
|
|
574
|
+
},
|
|
575
|
+
required: [],
|
|
576
|
+
}
|
|
577
|
+
: { type: 'object', properties: {}, required: [] },
|
|
578
|
+
description: toolDescription,
|
|
579
|
+
}));
|
|
580
|
+
}
|
|
581
|
+
return tools$1;
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Extract the final text result from a child agent's output messages.
|
|
585
|
+
* Walks backwards to find the last AIMessage with text content.
|
|
586
|
+
* Handles both string content and array content (multi-modal messages).
|
|
587
|
+
* @param messages - The child agent's output messages
|
|
588
|
+
* @param agentId - The child agent ID (for fallback message)
|
|
589
|
+
*/
|
|
590
|
+
static extractHandoffResult(messages, agentId) {
|
|
591
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
592
|
+
const msg = messages[i];
|
|
593
|
+
if (msg.getType() !== 'ai')
|
|
594
|
+
continue;
|
|
595
|
+
const content = msg.content;
|
|
596
|
+
if (typeof content === 'string' && content.trim()) {
|
|
597
|
+
return content.trim();
|
|
598
|
+
}
|
|
599
|
+
/** Handle array content (multi-modal messages with text blocks) */
|
|
600
|
+
if (Array.isArray(content)) {
|
|
601
|
+
const textParts = content
|
|
602
|
+
.filter((block) => typeof block === 'object' &&
|
|
603
|
+
block !== null &&
|
|
604
|
+
'type' in block &&
|
|
605
|
+
block.type === 'text' &&
|
|
606
|
+
'text' in block &&
|
|
607
|
+
typeof block.text === 'string')
|
|
608
|
+
.map((block) => block.text);
|
|
609
|
+
const text = textParts.join('\n').trim();
|
|
610
|
+
if (text)
|
|
611
|
+
return text;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return `[Agent "${agentId}" completed but produced no text output]`;
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Truncate handoff result using head/tail strategy (60/40 split).
|
|
618
|
+
* Preserves the beginning (key findings) and end (conclusions).
|
|
619
|
+
* Matches the TaskTool.truncateResult pattern from Ranger.
|
|
620
|
+
* @param result - The full result text
|
|
621
|
+
* @param maxChars - Maximum allowed characters
|
|
622
|
+
*/
|
|
623
|
+
static truncateHandoffResult(result, maxChars) {
|
|
624
|
+
if (!result || result.length <= maxChars) {
|
|
625
|
+
return result;
|
|
626
|
+
}
|
|
627
|
+
const truncationNotice = '\n\n[... handoff output truncated — middle section omitted to fit parent context ...]\n\n';
|
|
628
|
+
const available = maxChars - truncationNotice.length;
|
|
629
|
+
if (available <= 0) {
|
|
630
|
+
return result.substring(0, maxChars);
|
|
631
|
+
}
|
|
632
|
+
const headSize = Math.floor(available * 0.6);
|
|
633
|
+
const tailSize = available - headSize;
|
|
634
|
+
return (result.substring(0, headSize) +
|
|
635
|
+
truncationNotice +
|
|
636
|
+
result.substring(result.length - tailSize));
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Build a meaningful default description for a handoff tool.
|
|
640
|
+
* @param destContext - AgentContext of the destination agent
|
|
641
|
+
* @param destinationId - Raw agent ID (fallback)
|
|
642
|
+
*/
|
|
643
|
+
buildDefaultHandoffDescription(destContext, destinationId) {
|
|
644
|
+
const displayName = destContext?.name ?? destinationId;
|
|
645
|
+
const agentDescription = destContext?.description;
|
|
646
|
+
if (agentDescription != null && agentDescription !== '') {
|
|
647
|
+
return `Hand off task to "${displayName}": ${agentDescription}. The agent will execute and return its result.`;
|
|
648
|
+
}
|
|
649
|
+
return `Hand off task to "${displayName}" and receive its result.`;
|
|
650
|
+
}
|
|
455
651
|
/**
|
|
456
652
|
* Create a complete agent subgraph (similar to createReactAgent)
|
|
457
653
|
*/
|
|
@@ -471,7 +667,7 @@ class MultiAgentGraph extends Graph.StandardGraph {
|
|
|
471
667
|
* @param agentId - The agent ID to check for handoff reception
|
|
472
668
|
* @returns Object with filtered messages, extracted instructions, source agent, and parallel siblings
|
|
473
669
|
*/
|
|
474
|
-
|
|
670
|
+
processTransferReception(messages$1, agentId) {
|
|
475
671
|
if (messages$1.length === 0)
|
|
476
672
|
return null;
|
|
477
673
|
/**
|
|
@@ -500,8 +696,8 @@ class MultiAgentGraph extends Graph.StandardGraph {
|
|
|
500
696
|
destinationAgent = toolName.replace(_enum.Constants.LC_TRANSFER_TO_, '');
|
|
501
697
|
}
|
|
502
698
|
else if (isConditionalTransfer) {
|
|
503
|
-
const
|
|
504
|
-
destinationAgent = typeof
|
|
699
|
+
const transferDest = candidateMsg.additional_kwargs.handoff_destination;
|
|
700
|
+
destinationAgent = typeof transferDest === 'string' ? transferDest : null;
|
|
505
701
|
}
|
|
506
702
|
/** Check if this transfer targets our agent */
|
|
507
703
|
if (destinationAgent === agentId) {
|
|
@@ -517,7 +713,7 @@ class MultiAgentGraph extends Graph.StandardGraph {
|
|
|
517
713
|
const contentStr = typeof toolMessage.content === 'string'
|
|
518
714
|
? toolMessage.content
|
|
519
715
|
: JSON.stringify(toolMessage.content);
|
|
520
|
-
const instructionsMatch = contentStr.match(
|
|
716
|
+
const instructionsMatch = contentStr.match(TRANSFER_INSTRUCTIONS_PATTERN);
|
|
521
717
|
const instructions = instructionsMatch?.[1]?.trim() ?? null;
|
|
522
718
|
/** Extract source agent name from additional_kwargs */
|
|
523
719
|
const handoffSourceName = toolMessage.additional_kwargs.handoff_source_name;
|
|
@@ -695,7 +891,7 @@ class MultiAgentGraph extends Graph.StandardGraph {
|
|
|
695
891
|
};
|
|
696
892
|
}
|
|
697
893
|
/**
|
|
698
|
-
* Create the multi-agent workflow with
|
|
894
|
+
* Create the multi-agent workflow with handoffs, transfers, and sequences
|
|
699
895
|
*/
|
|
700
896
|
createWorkflow() {
|
|
701
897
|
const StateAnnotation = langgraph.Annotation.Root({
|
|
@@ -721,52 +917,54 @@ class MultiAgentGraph extends Graph.StandardGraph {
|
|
|
721
917
|
// Add all agents as complete subgraphs
|
|
722
918
|
for (const [agentId] of this.agentContexts) {
|
|
723
919
|
// Get all possible destinations for this agent
|
|
724
|
-
const
|
|
725
|
-
const
|
|
726
|
-
// Check
|
|
727
|
-
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) {
|
|
728
924
|
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
729
925
|
if (sources.includes(agentId) === true) {
|
|
730
926
|
const dests = Array.isArray(edge.to) ? edge.to : [edge.to];
|
|
731
|
-
dests.forEach((dest) =>
|
|
927
|
+
dests.forEach((dest) => transferDestinations.add(dest));
|
|
732
928
|
}
|
|
733
929
|
}
|
|
734
|
-
// Check
|
|
735
|
-
for (const edge of this.
|
|
930
|
+
// Check sequence edges for destinations
|
|
931
|
+
for (const edge of this.sequenceEdges) {
|
|
736
932
|
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
737
933
|
if (sources.includes(agentId) === true) {
|
|
738
934
|
const dests = Array.isArray(edge.to) ? edge.to : [edge.to];
|
|
739
|
-
dests.forEach((dest) =>
|
|
935
|
+
dests.forEach((dest) => sequenceDestinations.add(dest));
|
|
740
936
|
}
|
|
741
937
|
}
|
|
742
|
-
/** Check if this agent has BOTH
|
|
743
|
-
const
|
|
744
|
-
const
|
|
745
|
-
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;
|
|
746
942
|
/** Collect all possible destinations for this agent */
|
|
747
943
|
const allDestinations = new Set([
|
|
748
|
-
...
|
|
749
|
-
...
|
|
944
|
+
...transferDestinations,
|
|
945
|
+
...sequenceDestinations,
|
|
750
946
|
]);
|
|
751
|
-
if (
|
|
947
|
+
if (transferDestinations.size > 0 || sequenceDestinations.size === 0) {
|
|
752
948
|
allDestinations.add(langgraph.END);
|
|
753
949
|
}
|
|
754
950
|
/** Agent subgraph (includes agent + tools) */
|
|
755
951
|
const agentSubgraph = this.createAgentSubgraph(agentId);
|
|
952
|
+
/** Register subgraph for handoff tools (lazy reference resolution) */
|
|
953
|
+
this.subgraphRegistry.set(agentId, agentSubgraph);
|
|
756
954
|
/** Wrapper function that handles agentMessages channel, handoff reception, and conditional routing */
|
|
757
955
|
const agentWrapper = async (state, config) => {
|
|
758
956
|
console.debug(`[MultiAgentGraph] Agent "${agentId}" wrapper ENTRY (messages: ${state.messages.length}, needsCommandRouting: ${needsCommandRouting})`);
|
|
759
957
|
let result;
|
|
760
958
|
/**
|
|
761
|
-
* Check if this agent is receiving a
|
|
959
|
+
* Check if this agent is receiving a transfer.
|
|
762
960
|
* If so, filter out the transfer messages and inject instructions as preamble.
|
|
763
961
|
* This prevents the receiving agent from seeing the transfer as "completed work"
|
|
764
962
|
* and prematurely producing an end token.
|
|
765
963
|
*/
|
|
766
|
-
const
|
|
767
|
-
if (
|
|
768
|
-
const { filteredMessages, instructions, sourceAgentName, parallelSiblings, } =
|
|
769
|
-
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})`);
|
|
770
968
|
/**
|
|
771
969
|
* Set handoff context on the receiving agent.
|
|
772
970
|
* Uses pre-computed graph position for depth and parallel info.
|
|
@@ -883,24 +1081,24 @@ class MultiAgentGraph extends Graph.StandardGraph {
|
|
|
883
1081
|
/** Track the last agent that produced output for continuation support */
|
|
884
1082
|
this.lastActiveAgentId = agentId;
|
|
885
1083
|
console.debug(`[MultiAgentGraph] Agent "${agentId}" wrapper EXIT (result messages: ${result.messages.length})`);
|
|
886
|
-
/** If agent has both
|
|
1084
|
+
/** If agent has both transfer and sequence edges, use Command for exclusive routing */
|
|
887
1085
|
if (needsCommandRouting) {
|
|
888
|
-
/** Check if a
|
|
1086
|
+
/** Check if a transfer occurred */
|
|
889
1087
|
const lastMessage = result.messages[result.messages.length - 1];
|
|
890
1088
|
if (lastMessage != null &&
|
|
891
1089
|
lastMessage.getType() === 'tool' &&
|
|
892
1090
|
typeof lastMessage.name === 'string' &&
|
|
893
1091
|
lastMessage.name.startsWith(_enum.Constants.LC_TRANSFER_TO_)) {
|
|
894
|
-
/**
|
|
895
|
-
const
|
|
896
|
-
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(', ')}])`);
|
|
897
1095
|
/** Validate destination agent exists */
|
|
898
|
-
if (!this.agentContexts.has(
|
|
1096
|
+
if (!this.agentContexts.has(transferDest)) {
|
|
899
1097
|
const availableAgents = Array.from(this.agentContexts.keys()).join(', ');
|
|
900
|
-
console.error(`[MultiAgentGraph]
|
|
1098
|
+
console.error(`[MultiAgentGraph] Transfer to non-existent agent "${transferDest}". Available: ${availableAgents}`);
|
|
901
1099
|
/** Return error to model so it can self-correct */
|
|
902
1100
|
const errorMsg = new messages.ToolMessage({
|
|
903
|
-
content: `Transfer failed: agent "${
|
|
1101
|
+
content: `Transfer failed: agent "${transferDest}" does not exist. Available agents: ${availableAgents}. Please choose a valid agent to transfer to.`,
|
|
904
1102
|
tool_call_id: lastMessage.tool_call_id,
|
|
905
1103
|
name: lastMessage.name,
|
|
906
1104
|
});
|
|
@@ -910,7 +1108,7 @@ class MultiAgentGraph extends Graph.StandardGraph {
|
|
|
910
1108
|
};
|
|
911
1109
|
}
|
|
912
1110
|
/** Pre-handoff context compaction: if receiving agent has smaller budget */
|
|
913
|
-
const receiverContext = this.agentContexts.get(
|
|
1111
|
+
const receiverContext = this.agentContexts.get(transferDest);
|
|
914
1112
|
const senderContext = this.agentContexts.get(agentId);
|
|
915
1113
|
if (receiverContext?.maxContextTokens != null &&
|
|
916
1114
|
senderContext?.tokenCounter != null &&
|
|
@@ -922,7 +1120,7 @@ class MultiAgentGraph extends Graph.StandardGraph {
|
|
|
922
1120
|
const receiverBudget = receiverContext.maxContextTokens;
|
|
923
1121
|
if (currentSize > receiverBudget * 0.7) {
|
|
924
1122
|
console.warn(`[MultiAgentGraph] Pre-handoff compaction: context (${currentSize} tokens) exceeds ` +
|
|
925
|
-
`70% of receiver "${
|
|
1123
|
+
`70% of receiver "${transferDest}" budget (${receiverBudget} tokens)`);
|
|
926
1124
|
/** Generate handoff briefing */
|
|
927
1125
|
const senderName = senderContext.name ?? agentId;
|
|
928
1126
|
if (senderContext.summarizeCallback) {
|
|
@@ -934,8 +1132,8 @@ class MultiAgentGraph extends Graph.StandardGraph {
|
|
|
934
1132
|
summaryBudget: Math.floor(receiverBudget * 0.2),
|
|
935
1133
|
isMultiAgent: true,
|
|
936
1134
|
agentWorkflowState: {
|
|
937
|
-
currentAgentId:
|
|
938
|
-
agentChain: [agentId,
|
|
1135
|
+
currentAgentId: transferDest,
|
|
1136
|
+
agentChain: [agentId, transferDest],
|
|
939
1137
|
pendingAgents: [],
|
|
940
1138
|
},
|
|
941
1139
|
});
|
|
@@ -973,13 +1171,13 @@ class MultiAgentGraph extends Graph.StandardGraph {
|
|
|
973
1171
|
}
|
|
974
1172
|
return new langgraph.Command({
|
|
975
1173
|
update: result,
|
|
976
|
-
goto:
|
|
1174
|
+
goto: transferDest,
|
|
977
1175
|
});
|
|
978
1176
|
}
|
|
979
1177
|
else {
|
|
980
|
-
/** No
|
|
981
|
-
console.debug(`[MultiAgentGraph] Command routing: "${agentId}" -> no
|
|
982
|
-
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);
|
|
983
1181
|
if (directDests.length === 1) {
|
|
984
1182
|
return new langgraph.Command({
|
|
985
1183
|
update: result,
|
|
@@ -1010,11 +1208,11 @@ class MultiAgentGraph extends Graph.StandardGraph {
|
|
|
1010
1208
|
builder.addEdge(langgraph.START, startNode);
|
|
1011
1209
|
}
|
|
1012
1210
|
/**
|
|
1013
|
-
* Add
|
|
1211
|
+
* Add sequence edges for automatic transitions
|
|
1014
1212
|
* Group edges by destination to handle fan-in scenarios
|
|
1015
1213
|
*/
|
|
1016
1214
|
const edgesByDestination = new Map();
|
|
1017
|
-
for (const edge of this.
|
|
1215
|
+
for (const edge of this.sequenceEdges) {
|
|
1018
1216
|
const destinations = Array.isArray(edge.to) ? edge.to : [edge.to];
|
|
1019
1217
|
for (const destination of destinations) {
|
|
1020
1218
|
if (!edgesByDestination.has(destination)) {
|
|
@@ -1103,17 +1301,17 @@ class MultiAgentGraph extends Graph.StandardGraph {
|
|
|
1103
1301
|
for (const edge of edges) {
|
|
1104
1302
|
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
1105
1303
|
for (const source of sources) {
|
|
1106
|
-
/** Check if this source node has both
|
|
1107
|
-
const
|
|
1304
|
+
/** Check if this source node has both transfer and sequence edges */
|
|
1305
|
+
const sourceTransferEdges = this.transferEdges.filter((e) => {
|
|
1108
1306
|
const eSources = Array.isArray(e.from) ? e.from : [e.from];
|
|
1109
1307
|
return eSources.includes(source);
|
|
1110
1308
|
});
|
|
1111
|
-
const
|
|
1309
|
+
const sourceSequenceEdges = this.sequenceEdges.filter((e) => {
|
|
1112
1310
|
const eSources = Array.isArray(e.from) ? e.from : [e.from];
|
|
1113
1311
|
return eSources.includes(source);
|
|
1114
1312
|
});
|
|
1115
1313
|
/** Skip adding edge if source uses Command routing (has both types) */
|
|
1116
|
-
if (
|
|
1314
|
+
if (sourceTransferEdges.length > 0 && sourceSequenceEdges.length > 0) {
|
|
1117
1315
|
continue;
|
|
1118
1316
|
}
|
|
1119
1317
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|