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