@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
|
@@ -25,35 +25,35 @@ import { StandardGraph } from './Graph';
|
|
|
25
25
|
import {
|
|
26
26
|
Constants,
|
|
27
27
|
EdgeType,
|
|
28
|
-
|
|
28
|
+
DEFAULT_HANDOFF_MAX_RESULT_CHARS,
|
|
29
29
|
} from '@/common';
|
|
30
30
|
|
|
31
31
|
/** Pattern to extract instructions from transfer ToolMessage content */
|
|
32
|
-
const
|
|
32
|
+
const TRANSFER_INSTRUCTIONS_PATTERN = /(?:Instructions?|Context):\s*(.+)/is;
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
35
|
* MultiAgentGraph extends StandardGraph to support dynamic multi-agent workflows
|
|
36
36
|
* with handoffs, fan-in/fan-out, and other composable patterns.
|
|
37
37
|
*
|
|
38
38
|
* Key behavior:
|
|
39
|
-
* - Agents with ONLY
|
|
40
|
-
* - Agents with ONLY
|
|
41
|
-
* - Agents with BOTH: Use Command for exclusive routing (
|
|
42
|
-
* - If
|
|
43
|
-
* - If no
|
|
39
|
+
* - Agents with ONLY transfer edges: Can dynamically route to any transfer destination
|
|
40
|
+
* - Agents with ONLY sequence edges: Always follow their sequence edges
|
|
41
|
+
* - Agents with BOTH: Use Command for exclusive routing (transfer OR sequence, not both)
|
|
42
|
+
* - If transfer occurs: Only the transfer destination executes
|
|
43
|
+
* - If no transfer: Sequence edges execute (potentially in parallel)
|
|
44
44
|
*
|
|
45
|
-
* This enables the common pattern where an agent either
|
|
46
|
-
* OR continues its workflow (
|
|
45
|
+
* This enables the common pattern where an agent either transfers (one-way)
|
|
46
|
+
* OR continues its workflow (sequence edges), but not both simultaneously.
|
|
47
47
|
*/
|
|
48
48
|
export class MultiAgentGraph extends StandardGraph {
|
|
49
49
|
private edges: t.GraphEdge[];
|
|
50
50
|
private startingNodes: Set<string> = new Set();
|
|
51
|
-
private
|
|
51
|
+
private sequenceEdges: t.GraphEdge[] = [];
|
|
52
|
+
private transferEdges: t.GraphEdge[] = [];
|
|
52
53
|
private handoffEdges: t.GraphEdge[] = [];
|
|
53
|
-
private delegateEdges: t.GraphEdge[] = [];
|
|
54
54
|
/**
|
|
55
55
|
* Lazily populated registry of compiled subgraphs, keyed by agentId.
|
|
56
|
-
*
|
|
56
|
+
* Handoff tools are created in the constructor but reference subgraphs
|
|
57
57
|
* that are only created in createWorkflow(). This Map bridges that gap —
|
|
58
58
|
* tools capture the Map reference in their closure, and createWorkflow()
|
|
59
59
|
* populates it before any tool invocation occurs.
|
|
@@ -81,42 +81,40 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
81
81
|
this.edges = input.edges;
|
|
82
82
|
this.categorizeEdges();
|
|
83
83
|
this.analyzeGraph();
|
|
84
|
+
this.createTransferTools();
|
|
84
85
|
this.createHandoffTools();
|
|
85
|
-
this.createDelegateTools();
|
|
86
86
|
console.debug(
|
|
87
87
|
`[MultiAgentGraph] Constructor complete: ${this.agentContexts.size} agents, ${this.edges.length} edges`
|
|
88
88
|
);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
/**
|
|
92
|
-
* Categorize edges into handoff and
|
|
92
|
+
* Categorize edges into handoff, transfer, and sequence types
|
|
93
93
|
*/
|
|
94
94
|
private categorizeEdges(): void {
|
|
95
95
|
for (const edge of this.edges) {
|
|
96
|
-
|
|
97
|
-
// Edges with explicit 'direct' type or multi-destination without conditions are direct edges
|
|
98
|
-
if (edge.edgeType === EdgeType.DELEGATE) {
|
|
99
|
-
this.delegateEdges.push(edge);
|
|
100
|
-
} else if (edge.edgeType === EdgeType.DIRECT) {
|
|
101
|
-
this.directEdges.push(edge);
|
|
102
|
-
} else if (edge.edgeType === EdgeType.HANDOFF || edge.condition != null) {
|
|
96
|
+
if (edge.edgeType === EdgeType.HANDOFF) {
|
|
103
97
|
this.handoffEdges.push(edge);
|
|
98
|
+
} else if (edge.edgeType === EdgeType.SEQUENCE) {
|
|
99
|
+
this.sequenceEdges.push(edge);
|
|
100
|
+
} else if (edge.edgeType === EdgeType.TRANSFER || edge.condition != null) {
|
|
101
|
+
this.transferEdges.push(edge);
|
|
104
102
|
} else {
|
|
105
|
-
// Default: single-to-single edges are
|
|
103
|
+
// Default: single-to-single edges are transfer, single-to-multiple are sequence
|
|
106
104
|
const destinations = Array.isArray(edge.to) ? edge.to : [edge.to];
|
|
107
105
|
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
108
106
|
|
|
109
107
|
if (sources.length === 1 && destinations.length > 1) {
|
|
110
|
-
// Fan-out pattern defaults to
|
|
111
|
-
this.
|
|
108
|
+
// Fan-out pattern defaults to sequence
|
|
109
|
+
this.sequenceEdges.push(edge);
|
|
112
110
|
} else {
|
|
113
|
-
// Everything else defaults to
|
|
114
|
-
this.
|
|
111
|
+
// Everything else defaults to transfer
|
|
112
|
+
this.transferEdges.push(edge);
|
|
115
113
|
}
|
|
116
114
|
}
|
|
117
115
|
}
|
|
118
116
|
console.debug(
|
|
119
|
-
`[MultiAgentGraph] Edge categorization: ${this.handoffEdges.length} handoff, ${this.
|
|
117
|
+
`[MultiAgentGraph] Edge categorization: ${this.handoffEdges.length} handoff, ${this.transferEdges.length} transfer, ${this.sequenceEdges.length} sequence (of ${this.edges.length} total)`
|
|
120
118
|
);
|
|
121
119
|
}
|
|
122
120
|
|
|
@@ -187,8 +185,8 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
187
185
|
if (visited.has(current)) continue;
|
|
188
186
|
visited.add(current);
|
|
189
187
|
|
|
190
|
-
// Find
|
|
191
|
-
for (const edge of this.
|
|
188
|
+
// Find sequence edges from this node
|
|
189
|
+
for (const edge of this.sequenceEdges) {
|
|
192
190
|
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
193
191
|
if (!sources.includes(current)) continue;
|
|
194
192
|
|
|
@@ -216,8 +214,8 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
216
214
|
}
|
|
217
215
|
}
|
|
218
216
|
|
|
219
|
-
// Also follow
|
|
220
|
-
for (const edge of [...this.
|
|
217
|
+
// Also follow transfer and handoff edges for traversal (they don't create parallel groups)
|
|
218
|
+
for (const edge of [...this.transferEdges, ...this.handoffEdges]) {
|
|
221
219
|
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
222
220
|
if (!sources.includes(current)) continue;
|
|
223
221
|
|
|
@@ -267,54 +265,52 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
267
265
|
}
|
|
268
266
|
|
|
269
267
|
/**
|
|
270
|
-
* Create
|
|
268
|
+
* Create transfer tools for agents based on transfer edges only.
|
|
269
|
+
* Transfer tools return Command for one-way routing — parent exits, child takes over.
|
|
271
270
|
*/
|
|
272
|
-
private
|
|
273
|
-
|
|
274
|
-
const handoffsByAgent = new Map<string, t.GraphEdge[]>();
|
|
271
|
+
private createTransferTools(): void {
|
|
272
|
+
const transfersByAgent = new Map<string, t.GraphEdge[]>();
|
|
275
273
|
|
|
276
|
-
|
|
277
|
-
for (const edge of this.handoffEdges) {
|
|
274
|
+
for (const edge of this.transferEdges) {
|
|
278
275
|
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
279
276
|
sources.forEach((source) => {
|
|
280
|
-
if (!
|
|
281
|
-
|
|
277
|
+
if (!transfersByAgent.has(source)) {
|
|
278
|
+
transfersByAgent.set(source, []);
|
|
282
279
|
}
|
|
283
|
-
|
|
280
|
+
transfersByAgent.get(source)!.push(edge);
|
|
284
281
|
});
|
|
285
282
|
}
|
|
286
283
|
|
|
287
|
-
|
|
288
|
-
for (const [agentId, edges] of handoffsByAgent) {
|
|
284
|
+
for (const [agentId, edges] of transfersByAgent) {
|
|
289
285
|
const agentContext = this.agentContexts.get(agentId);
|
|
290
286
|
if (!agentContext) continue;
|
|
291
287
|
|
|
292
|
-
|
|
293
|
-
const handoffTools: t.GenericTool[] = [];
|
|
288
|
+
const transferTools: t.GenericTool[] = [];
|
|
294
289
|
const sourceAgentName = agentContext.name ?? agentId;
|
|
295
290
|
for (const edge of edges) {
|
|
296
|
-
|
|
297
|
-
...this.
|
|
291
|
+
transferTools.push(
|
|
292
|
+
...this.createTransferToolsForEdge(edge, agentId, sourceAgentName)
|
|
298
293
|
);
|
|
299
294
|
}
|
|
300
295
|
|
|
301
296
|
if (!agentContext.graphTools) {
|
|
302
297
|
agentContext.graphTools = [];
|
|
303
298
|
}
|
|
304
|
-
agentContext.graphTools.push(...
|
|
299
|
+
agentContext.graphTools.push(...transferTools);
|
|
305
300
|
console.debug(
|
|
306
|
-
`[MultiAgentGraph]
|
|
301
|
+
`[MultiAgentGraph] Transfer tools for "${agentId}": [${transferTools.map((t) => t.name).join(', ')}]`
|
|
307
302
|
);
|
|
308
303
|
}
|
|
309
304
|
}
|
|
310
305
|
|
|
311
306
|
/**
|
|
312
|
-
* Create
|
|
313
|
-
*
|
|
314
|
-
* @param
|
|
307
|
+
* Create transfer tools for an edge (handles multiple destinations).
|
|
308
|
+
* Transfer tools return Command for one-way routing — parent exits, child takes over.
|
|
309
|
+
* @param edge - The graph edge defining the transfer
|
|
310
|
+
* @param sourceAgentId - The ID of the agent that will perform the transfer
|
|
315
311
|
* @param sourceAgentName - The human-readable name of the source agent
|
|
316
312
|
*/
|
|
317
|
-
private
|
|
313
|
+
private createTransferToolsForEdge(
|
|
318
314
|
edge: t.GraphEdge,
|
|
319
315
|
sourceAgentId: string,
|
|
320
316
|
sourceAgentName: string
|
|
@@ -329,9 +325,9 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
329
325
|
edge.description ?? 'Conditionally transfer control based on state';
|
|
330
326
|
|
|
331
327
|
/** Check if we have a prompt for handoff input */
|
|
332
|
-
const
|
|
328
|
+
const hasTransferInput =
|
|
333
329
|
edge.prompt != null && typeof edge.prompt === 'string';
|
|
334
|
-
const
|
|
330
|
+
const transferInputDescription = hasTransferInput ? edge.prompt : undefined;
|
|
335
331
|
const promptKey = edge.promptKey ?? 'instructions';
|
|
336
332
|
|
|
337
333
|
tools.push(
|
|
@@ -360,7 +356,7 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
360
356
|
|
|
361
357
|
let content = `Conditionally transferred to ${destination}`;
|
|
362
358
|
if (
|
|
363
|
-
|
|
359
|
+
hasTransferInput &&
|
|
364
360
|
promptKey in input &&
|
|
365
361
|
input[promptKey] != null
|
|
366
362
|
) {
|
|
@@ -387,13 +383,13 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
387
383
|
},
|
|
388
384
|
{
|
|
389
385
|
name: toolName,
|
|
390
|
-
schema:
|
|
386
|
+
schema: hasTransferInput
|
|
391
387
|
? {
|
|
392
388
|
type: 'object',
|
|
393
389
|
properties: {
|
|
394
390
|
[promptKey]: {
|
|
395
391
|
type: 'string',
|
|
396
|
-
description:
|
|
392
|
+
description: transferInputDescription as string,
|
|
397
393
|
},
|
|
398
394
|
},
|
|
399
395
|
required: [],
|
|
@@ -410,12 +406,12 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
410
406
|
const destContext = this.agentContexts.get(destination);
|
|
411
407
|
const toolDescription =
|
|
412
408
|
edge.description ??
|
|
413
|
-
this.
|
|
409
|
+
this.buildDefaultTransferDescription(destContext, destination);
|
|
414
410
|
|
|
415
411
|
/** Check if we have a prompt for handoff input */
|
|
416
|
-
const
|
|
412
|
+
const hasTransferInput =
|
|
417
413
|
edge.prompt != null && typeof edge.prompt === 'string';
|
|
418
|
-
const
|
|
414
|
+
const transferInputDescription = hasTransferInput
|
|
419
415
|
? edge.prompt
|
|
420
416
|
: undefined;
|
|
421
417
|
const promptKey = edge.promptKey ?? 'instructions';
|
|
@@ -430,7 +426,7 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
430
426
|
|
|
431
427
|
let content = `Successfully transferred to ${destination}`;
|
|
432
428
|
if (
|
|
433
|
-
|
|
429
|
+
hasTransferInput &&
|
|
434
430
|
promptKey in input &&
|
|
435
431
|
input[promptKey] != null
|
|
436
432
|
) {
|
|
@@ -521,13 +517,13 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
521
517
|
},
|
|
522
518
|
{
|
|
523
519
|
name: toolName,
|
|
524
|
-
schema:
|
|
520
|
+
schema: hasTransferInput
|
|
525
521
|
? {
|
|
526
522
|
type: 'object',
|
|
527
523
|
properties: {
|
|
528
524
|
[promptKey]: {
|
|
529
525
|
type: 'string',
|
|
530
|
-
description:
|
|
526
|
+
description: transferInputDescription as string,
|
|
531
527
|
},
|
|
532
528
|
},
|
|
533
529
|
required: [],
|
|
@@ -544,13 +540,13 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
544
540
|
}
|
|
545
541
|
|
|
546
542
|
/**
|
|
547
|
-
* Builds a meaningful default description for a
|
|
543
|
+
* Builds a meaningful default description for a transfer tool when no explicit
|
|
548
544
|
* edge.description is provided. Uses the destination agent's name and description
|
|
549
545
|
* so the LLM can make informed routing decisions.
|
|
550
546
|
* @param destContext - AgentContext of the destination agent (may be undefined)
|
|
551
547
|
* @param destinationId - Raw agent ID (fallback when context unavailable)
|
|
552
548
|
*/
|
|
553
|
-
private
|
|
549
|
+
private buildDefaultTransferDescription(
|
|
554
550
|
destContext: import('@/agents/AgentContext').AgentContext | undefined,
|
|
555
551
|
destinationId: string
|
|
556
552
|
): string {
|
|
@@ -564,72 +560,72 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
564
560
|
}
|
|
565
561
|
|
|
566
562
|
/**
|
|
567
|
-
* Create
|
|
568
|
-
*
|
|
569
|
-
* as a string to the parent agent's context. Unlike
|
|
570
|
-
* return Command for
|
|
571
|
-
*
|
|
563
|
+
* Create handoff tools for agents based on handoff edges.
|
|
564
|
+
* Handoff tools invoke child agent subgraphs inline and return the result
|
|
565
|
+
* as a string to the parent agent's context. Unlike transfer tools (which
|
|
566
|
+
* return Command for one-way routing), handoff tools execute the child,
|
|
567
|
+
* extract the final text, and return it within the parent's agent loop.
|
|
572
568
|
*
|
|
573
569
|
* This enables the supervisor pattern: parent calls child → gets result → thinks → calls another.
|
|
574
570
|
*/
|
|
575
|
-
private
|
|
576
|
-
const
|
|
571
|
+
private createHandoffTools(): void {
|
|
572
|
+
const handoffsByAgent = new Map<string, t.GraphEdge[]>();
|
|
577
573
|
|
|
578
|
-
for (const edge of this.
|
|
574
|
+
for (const edge of this.handoffEdges) {
|
|
579
575
|
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
580
576
|
sources.forEach((source) => {
|
|
581
|
-
if (!
|
|
582
|
-
|
|
577
|
+
if (!handoffsByAgent.has(source)) {
|
|
578
|
+
handoffsByAgent.set(source, []);
|
|
583
579
|
}
|
|
584
|
-
|
|
580
|
+
handoffsByAgent.get(source)!.push(edge);
|
|
585
581
|
});
|
|
586
582
|
}
|
|
587
583
|
|
|
588
|
-
for (const [agentId, edges] of
|
|
584
|
+
for (const [agentId, edges] of handoffsByAgent) {
|
|
589
585
|
const agentContext = this.agentContexts.get(agentId);
|
|
590
586
|
if (!agentContext) continue;
|
|
591
587
|
|
|
592
|
-
const
|
|
588
|
+
const handoffTools: t.GenericTool[] = [];
|
|
593
589
|
for (const edge of edges) {
|
|
594
|
-
|
|
595
|
-
...this.
|
|
590
|
+
handoffTools.push(
|
|
591
|
+
...this.createHandoffToolsForEdge(edge, agentId)
|
|
596
592
|
);
|
|
597
593
|
}
|
|
598
594
|
|
|
599
595
|
if (!agentContext.graphTools) {
|
|
600
596
|
agentContext.graphTools = [];
|
|
601
597
|
}
|
|
602
|
-
agentContext.graphTools.push(...
|
|
598
|
+
agentContext.graphTools.push(...handoffTools);
|
|
603
599
|
console.debug(
|
|
604
|
-
`[MultiAgentGraph]
|
|
600
|
+
`[MultiAgentGraph] Handoff tools for "${agentId}": [${handoffTools.map((t) => t.name).join(', ')}]`
|
|
605
601
|
);
|
|
606
602
|
}
|
|
607
603
|
}
|
|
608
604
|
|
|
609
605
|
/**
|
|
610
|
-
* Create
|
|
611
|
-
* Each
|
|
606
|
+
* Create handoff tools for an edge (handles multiple destinations).
|
|
607
|
+
* Each handoff tool invokes the child agent's compiled subgraph inline,
|
|
612
608
|
* extracts the final AI message text, truncates it, and returns it as
|
|
613
609
|
* a string (which becomes a ToolMessage in the parent's context).
|
|
614
610
|
*
|
|
615
|
-
* @param edge - The graph edge defining the
|
|
611
|
+
* @param edge - The graph edge defining the handoff
|
|
616
612
|
* @param sourceAgentId - The ID of the parent/supervisor agent
|
|
617
613
|
*/
|
|
618
|
-
private
|
|
614
|
+
private createHandoffToolsForEdge(
|
|
619
615
|
edge: t.GraphEdge,
|
|
620
616
|
sourceAgentId: string
|
|
621
617
|
): t.GenericTool[] {
|
|
622
618
|
const tools: t.GenericTool[] = [];
|
|
623
619
|
const destinations = Array.isArray(edge.to) ? edge.to : [edge.to];
|
|
624
620
|
const maxResultChars =
|
|
625
|
-
edge.maxResultChars ??
|
|
621
|
+
edge.maxResultChars ?? DEFAULT_HANDOFF_MAX_RESULT_CHARS;
|
|
626
622
|
|
|
627
623
|
for (const destination of destinations) {
|
|
628
|
-
const toolName = `${Constants.
|
|
624
|
+
const toolName = `${Constants.LC_HANDOFF_TO_}${destination}`;
|
|
629
625
|
const destContext = this.agentContexts.get(destination);
|
|
630
626
|
const toolDescription =
|
|
631
627
|
edge.description ??
|
|
632
|
-
this.
|
|
628
|
+
this.buildDefaultHandoffDescription(destContext, destination);
|
|
633
629
|
|
|
634
630
|
const hasPromptInput =
|
|
635
631
|
edge.prompt != null && typeof edge.prompt === 'string';
|
|
@@ -646,7 +642,7 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
646
642
|
const subgraph = registry.get(destination);
|
|
647
643
|
if (!subgraph) {
|
|
648
644
|
throw new Error(
|
|
649
|
-
`
|
|
645
|
+
`Handoff target "${destination}" subgraph not found in registry. ` +
|
|
650
646
|
'This is a bug: createWorkflow() should have populated the subgraph registry.'
|
|
651
647
|
);
|
|
652
648
|
}
|
|
@@ -671,7 +667,7 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
671
667
|
};
|
|
672
668
|
|
|
673
669
|
console.debug(
|
|
674
|
-
`[MultiAgentGraph]
|
|
670
|
+
`[MultiAgentGraph] Handoff "${sourceAgentId}" -> "${destination}" START ` +
|
|
675
671
|
`(messages: ${childMessages.length})`
|
|
676
672
|
);
|
|
677
673
|
|
|
@@ -683,17 +679,17 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
683
679
|
*/
|
|
684
680
|
const result = await subgraph.invoke(childState, config);
|
|
685
681
|
|
|
686
|
-
const resultText = MultiAgentGraph.
|
|
682
|
+
const resultText = MultiAgentGraph.extractHandoffResult(
|
|
687
683
|
result.messages,
|
|
688
684
|
destination
|
|
689
685
|
);
|
|
690
|
-
const truncatedResult = MultiAgentGraph.
|
|
686
|
+
const truncatedResult = MultiAgentGraph.truncateHandoffResult(
|
|
691
687
|
resultText,
|
|
692
688
|
maxResultChars
|
|
693
689
|
);
|
|
694
690
|
|
|
695
691
|
console.debug(
|
|
696
|
-
`[MultiAgentGraph]
|
|
692
|
+
`[MultiAgentGraph] Handoff "${sourceAgentId}" -> "${destination}" DONE ` +
|
|
697
693
|
`(result: ${resultText.length} chars` +
|
|
698
694
|
`${truncatedResult.length < resultText.length ? `, truncated to ${truncatedResult.length}` : ''})`
|
|
699
695
|
);
|
|
@@ -703,10 +699,10 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
703
699
|
const errorMessage =
|
|
704
700
|
err instanceof Error ? err.message : String(err);
|
|
705
701
|
console.error(
|
|
706
|
-
`[MultiAgentGraph]
|
|
702
|
+
`[MultiAgentGraph] Handoff "${sourceAgentId}" -> "${destination}" ERROR:`,
|
|
707
703
|
errorMessage
|
|
708
704
|
);
|
|
709
|
-
return `[
|
|
705
|
+
return `[Handoff to "${destination}" failed: ${errorMessage}]`;
|
|
710
706
|
}
|
|
711
707
|
},
|
|
712
708
|
{
|
|
@@ -739,7 +735,7 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
739
735
|
* @param messages - The child agent's output messages
|
|
740
736
|
* @param agentId - The child agent ID (for fallback message)
|
|
741
737
|
*/
|
|
742
|
-
static
|
|
738
|
+
static extractHandoffResult(
|
|
743
739
|
messages: BaseMessage[],
|
|
744
740
|
agentId: string
|
|
745
741
|
): string {
|
|
@@ -780,19 +776,19 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
780
776
|
}
|
|
781
777
|
|
|
782
778
|
/**
|
|
783
|
-
* Truncate
|
|
779
|
+
* Truncate handoff result using head/tail strategy (60/40 split).
|
|
784
780
|
* Preserves the beginning (key findings) and end (conclusions).
|
|
785
781
|
* Matches the TaskTool.truncateResult pattern from Ranger.
|
|
786
782
|
* @param result - The full result text
|
|
787
783
|
* @param maxChars - Maximum allowed characters
|
|
788
784
|
*/
|
|
789
|
-
static
|
|
785
|
+
static truncateHandoffResult(result: string, maxChars: number): string {
|
|
790
786
|
if (!result || result.length <= maxChars) {
|
|
791
787
|
return result;
|
|
792
788
|
}
|
|
793
789
|
|
|
794
790
|
const truncationNotice =
|
|
795
|
-
'\n\n[...
|
|
791
|
+
'\n\n[... handoff output truncated — middle section omitted to fit parent context ...]\n\n';
|
|
796
792
|
const available = maxChars - truncationNotice.length;
|
|
797
793
|
if (available <= 0) {
|
|
798
794
|
return result.substring(0, maxChars);
|
|
@@ -809,11 +805,11 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
809
805
|
}
|
|
810
806
|
|
|
811
807
|
/**
|
|
812
|
-
* Build a meaningful default description for a
|
|
808
|
+
* Build a meaningful default description for a handoff tool.
|
|
813
809
|
* @param destContext - AgentContext of the destination agent
|
|
814
810
|
* @param destinationId - Raw agent ID (fallback)
|
|
815
811
|
*/
|
|
816
|
-
private
|
|
812
|
+
private buildDefaultHandoffDescription(
|
|
817
813
|
destContext: import('@/agents/AgentContext').AgentContext | undefined,
|
|
818
814
|
destinationId: string
|
|
819
815
|
): string {
|
|
@@ -821,9 +817,9 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
821
817
|
const agentDescription = destContext?.description;
|
|
822
818
|
|
|
823
819
|
if (agentDescription != null && agentDescription !== '') {
|
|
824
|
-
return `
|
|
820
|
+
return `Hand off task to "${displayName}": ${agentDescription}. The agent will execute and return its result.`;
|
|
825
821
|
}
|
|
826
|
-
return `
|
|
822
|
+
return `Hand off task to "${displayName}" and receive its result.`;
|
|
827
823
|
}
|
|
828
824
|
|
|
829
825
|
/**
|
|
@@ -846,7 +842,7 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
846
842
|
* @param agentId - The agent ID to check for handoff reception
|
|
847
843
|
* @returns Object with filtered messages, extracted instructions, source agent, and parallel siblings
|
|
848
844
|
*/
|
|
849
|
-
private
|
|
845
|
+
private processTransferReception(
|
|
850
846
|
messages: BaseMessage[],
|
|
851
847
|
agentId: string
|
|
852
848
|
): {
|
|
@@ -886,8 +882,8 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
886
882
|
if (isTransferMessage) {
|
|
887
883
|
destinationAgent = toolName.replace(Constants.LC_TRANSFER_TO_, '');
|
|
888
884
|
} else if (isConditionalTransfer) {
|
|
889
|
-
const
|
|
890
|
-
destinationAgent = typeof
|
|
885
|
+
const transferDest = candidateMsg.additional_kwargs.handoff_destination;
|
|
886
|
+
destinationAgent = typeof transferDest === 'string' ? transferDest : null;
|
|
891
887
|
}
|
|
892
888
|
|
|
893
889
|
/** Check if this transfer targets our agent */
|
|
@@ -907,7 +903,7 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
907
903
|
? toolMessage.content
|
|
908
904
|
: JSON.stringify(toolMessage.content);
|
|
909
905
|
|
|
910
|
-
const instructionsMatch = contentStr.match(
|
|
906
|
+
const instructionsMatch = contentStr.match(TRANSFER_INSTRUCTIONS_PATTERN);
|
|
911
907
|
const instructions = instructionsMatch?.[1]?.trim() ?? null;
|
|
912
908
|
|
|
913
909
|
/** Extract source agent name from additional_kwargs */
|
|
@@ -1138,7 +1134,7 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1138
1134
|
}
|
|
1139
1135
|
|
|
1140
1136
|
/**
|
|
1141
|
-
* Create the multi-agent workflow with
|
|
1137
|
+
* Create the multi-agent workflow with handoffs, transfers, and sequences
|
|
1142
1138
|
*/
|
|
1143
1139
|
override createWorkflow(): t.CompiledMultiAgentWorkflow {
|
|
1144
1140
|
const StateAnnotation = Annotation.Root({
|
|
@@ -1166,45 +1162,45 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1166
1162
|
// Add all agents as complete subgraphs
|
|
1167
1163
|
for (const [agentId] of this.agentContexts) {
|
|
1168
1164
|
// Get all possible destinations for this agent
|
|
1169
|
-
const
|
|
1170
|
-
const
|
|
1165
|
+
const transferDestinations = new Set<string>();
|
|
1166
|
+
const sequenceDestinations = new Set<string>();
|
|
1171
1167
|
|
|
1172
|
-
// Check
|
|
1173
|
-
for (const edge of this.
|
|
1168
|
+
// Check transfer edges for destinations
|
|
1169
|
+
for (const edge of this.transferEdges) {
|
|
1174
1170
|
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
1175
1171
|
if (sources.includes(agentId) === true) {
|
|
1176
1172
|
const dests = Array.isArray(edge.to) ? edge.to : [edge.to];
|
|
1177
|
-
dests.forEach((dest) =>
|
|
1173
|
+
dests.forEach((dest) => transferDestinations.add(dest));
|
|
1178
1174
|
}
|
|
1179
1175
|
}
|
|
1180
1176
|
|
|
1181
|
-
// Check
|
|
1182
|
-
for (const edge of this.
|
|
1177
|
+
// Check sequence edges for destinations
|
|
1178
|
+
for (const edge of this.sequenceEdges) {
|
|
1183
1179
|
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
1184
1180
|
if (sources.includes(agentId) === true) {
|
|
1185
1181
|
const dests = Array.isArray(edge.to) ? edge.to : [edge.to];
|
|
1186
|
-
dests.forEach((dest) =>
|
|
1182
|
+
dests.forEach((dest) => sequenceDestinations.add(dest));
|
|
1187
1183
|
}
|
|
1188
1184
|
}
|
|
1189
1185
|
|
|
1190
|
-
/** Check if this agent has BOTH
|
|
1191
|
-
const
|
|
1192
|
-
const
|
|
1193
|
-
const needsCommandRouting =
|
|
1186
|
+
/** Check if this agent has BOTH transfer and sequence edges */
|
|
1187
|
+
const hasTransferEdges = transferDestinations.size > 0;
|
|
1188
|
+
const hasSequenceEdges = sequenceDestinations.size > 0;
|
|
1189
|
+
const needsCommandRouting = hasTransferEdges && hasSequenceEdges;
|
|
1194
1190
|
|
|
1195
1191
|
/** Collect all possible destinations for this agent */
|
|
1196
1192
|
const allDestinations = new Set([
|
|
1197
|
-
...
|
|
1198
|
-
...
|
|
1193
|
+
...transferDestinations,
|
|
1194
|
+
...sequenceDestinations,
|
|
1199
1195
|
]);
|
|
1200
|
-
if (
|
|
1196
|
+
if (transferDestinations.size > 0 || sequenceDestinations.size === 0) {
|
|
1201
1197
|
allDestinations.add(END);
|
|
1202
1198
|
}
|
|
1203
1199
|
|
|
1204
1200
|
/** Agent subgraph (includes agent + tools) */
|
|
1205
1201
|
const agentSubgraph = this.createAgentSubgraph(agentId);
|
|
1206
1202
|
|
|
1207
|
-
/** Register subgraph for
|
|
1203
|
+
/** Register subgraph for handoff tools (lazy reference resolution) */
|
|
1208
1204
|
this.subgraphRegistry.set(agentId, agentSubgraph);
|
|
1209
1205
|
|
|
1210
1206
|
/** Wrapper function that handles agentMessages channel, handoff reception, and conditional routing */
|
|
@@ -1218,25 +1214,25 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1218
1214
|
let result: t.MultiAgentGraphState;
|
|
1219
1215
|
|
|
1220
1216
|
/**
|
|
1221
|
-
* Check if this agent is receiving a
|
|
1217
|
+
* Check if this agent is receiving a transfer.
|
|
1222
1218
|
* If so, filter out the transfer messages and inject instructions as preamble.
|
|
1223
1219
|
* This prevents the receiving agent from seeing the transfer as "completed work"
|
|
1224
1220
|
* and prematurely producing an end token.
|
|
1225
1221
|
*/
|
|
1226
|
-
const
|
|
1222
|
+
const transferContext = this.processTransferReception(
|
|
1227
1223
|
state.messages,
|
|
1228
1224
|
agentId
|
|
1229
1225
|
);
|
|
1230
1226
|
|
|
1231
|
-
if (
|
|
1227
|
+
if (transferContext !== null) {
|
|
1232
1228
|
const {
|
|
1233
1229
|
filteredMessages,
|
|
1234
1230
|
instructions,
|
|
1235
1231
|
sourceAgentName,
|
|
1236
1232
|
parallelSiblings,
|
|
1237
|
-
} =
|
|
1233
|
+
} = transferContext;
|
|
1238
1234
|
console.debug(
|
|
1239
|
-
`[MultiAgentGraph] Agent "${agentId}" receiving
|
|
1235
|
+
`[MultiAgentGraph] Agent "${agentId}" receiving transfer from "${sourceAgentName}" (instructions: ${instructions != null}, parallelSiblings: ${parallelSiblings.length})`
|
|
1240
1236
|
);
|
|
1241
1237
|
|
|
1242
1238
|
/**
|
|
@@ -1381,9 +1377,9 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1381
1377
|
`[MultiAgentGraph] Agent "${agentId}" wrapper EXIT (result messages: ${result.messages.length})`
|
|
1382
1378
|
);
|
|
1383
1379
|
|
|
1384
|
-
/** If agent has both
|
|
1380
|
+
/** If agent has both transfer and sequence edges, use Command for exclusive routing */
|
|
1385
1381
|
if (needsCommandRouting) {
|
|
1386
|
-
/** Check if a
|
|
1382
|
+
/** Check if a transfer occurred */
|
|
1387
1383
|
const lastMessage = result.messages[
|
|
1388
1384
|
result.messages.length - 1
|
|
1389
1385
|
] as BaseMessage | null;
|
|
@@ -1393,26 +1389,26 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1393
1389
|
typeof lastMessage.name === 'string' &&
|
|
1394
1390
|
lastMessage.name.startsWith(Constants.LC_TRANSFER_TO_)
|
|
1395
1391
|
) {
|
|
1396
|
-
/**
|
|
1397
|
-
const
|
|
1392
|
+
/** Transfer occurred - extract destination and navigate there exclusively */
|
|
1393
|
+
const transferDest = lastMessage.name.replace(
|
|
1398
1394
|
Constants.LC_TRANSFER_TO_,
|
|
1399
1395
|
''
|
|
1400
1396
|
);
|
|
1401
1397
|
console.debug(
|
|
1402
|
-
`[MultiAgentGraph] Command routing: "${agentId}" ->
|
|
1398
|
+
`[MultiAgentGraph] Command routing: "${agentId}" -> transfer to "${transferDest}" (sequence edges skipped: [${Array.from(sequenceDestinations).join(', ')}])`
|
|
1403
1399
|
);
|
|
1404
1400
|
|
|
1405
1401
|
/** Validate destination agent exists */
|
|
1406
|
-
if (!this.agentContexts.has(
|
|
1402
|
+
if (!this.agentContexts.has(transferDest)) {
|
|
1407
1403
|
const availableAgents = Array.from(
|
|
1408
1404
|
this.agentContexts.keys()
|
|
1409
1405
|
).join(', ');
|
|
1410
1406
|
console.error(
|
|
1411
|
-
`[MultiAgentGraph]
|
|
1407
|
+
`[MultiAgentGraph] Transfer to non-existent agent "${transferDest}". Available: ${availableAgents}`
|
|
1412
1408
|
);
|
|
1413
1409
|
/** Return error to model so it can self-correct */
|
|
1414
1410
|
const errorMsg = new ToolMessage({
|
|
1415
|
-
content: `Transfer failed: agent "${
|
|
1411
|
+
content: `Transfer failed: agent "${transferDest}" does not exist. Available agents: ${availableAgents}. Please choose a valid agent to transfer to.`,
|
|
1416
1412
|
tool_call_id: (lastMessage as ToolMessage).tool_call_id,
|
|
1417
1413
|
name: lastMessage.name,
|
|
1418
1414
|
});
|
|
@@ -1423,7 +1419,7 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1423
1419
|
}
|
|
1424
1420
|
|
|
1425
1421
|
/** Pre-handoff context compaction: if receiving agent has smaller budget */
|
|
1426
|
-
const receiverContext = this.agentContexts.get(
|
|
1422
|
+
const receiverContext = this.agentContexts.get(transferDest);
|
|
1427
1423
|
const senderContext = this.agentContexts.get(agentId);
|
|
1428
1424
|
if (
|
|
1429
1425
|
receiverContext?.maxContextTokens != null &&
|
|
@@ -1439,7 +1435,7 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1439
1435
|
if (currentSize > receiverBudget * 0.7) {
|
|
1440
1436
|
console.warn(
|
|
1441
1437
|
`[MultiAgentGraph] Pre-handoff compaction: context (${currentSize} tokens) exceeds ` +
|
|
1442
|
-
`70% of receiver "${
|
|
1438
|
+
`70% of receiver "${transferDest}" budget (${receiverBudget} tokens)`
|
|
1443
1439
|
);
|
|
1444
1440
|
|
|
1445
1441
|
/** Generate handoff briefing */
|
|
@@ -1457,8 +1453,8 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1457
1453
|
summaryBudget: Math.floor(receiverBudget * 0.2),
|
|
1458
1454
|
isMultiAgent: true,
|
|
1459
1455
|
agentWorkflowState: {
|
|
1460
|
-
currentAgentId:
|
|
1461
|
-
agentChain: [agentId,
|
|
1456
|
+
currentAgentId: transferDest,
|
|
1457
|
+
agentChain: [agentId, transferDest],
|
|
1462
1458
|
pendingAgents: [],
|
|
1463
1459
|
},
|
|
1464
1460
|
}
|
|
@@ -1514,14 +1510,14 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1514
1510
|
|
|
1515
1511
|
return new Command({
|
|
1516
1512
|
update: result,
|
|
1517
|
-
goto:
|
|
1513
|
+
goto: transferDest,
|
|
1518
1514
|
});
|
|
1519
1515
|
} else {
|
|
1520
|
-
/** No
|
|
1516
|
+
/** No transfer - proceed with sequence edges */
|
|
1521
1517
|
console.debug(
|
|
1522
|
-
`[MultiAgentGraph] Command routing: "${agentId}" -> no
|
|
1518
|
+
`[MultiAgentGraph] Command routing: "${agentId}" -> no transfer, following sequence edges: [${Array.from(sequenceDestinations).join(', ')}]`
|
|
1523
1519
|
);
|
|
1524
|
-
const directDests = Array.from(
|
|
1520
|
+
const directDests = Array.from(sequenceDestinations);
|
|
1525
1521
|
if (directDests.length === 1) {
|
|
1526
1522
|
return new Command({
|
|
1527
1523
|
update: result,
|
|
@@ -1555,12 +1551,12 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1555
1551
|
}
|
|
1556
1552
|
|
|
1557
1553
|
/**
|
|
1558
|
-
* Add
|
|
1554
|
+
* Add sequence edges for automatic transitions
|
|
1559
1555
|
* Group edges by destination to handle fan-in scenarios
|
|
1560
1556
|
*/
|
|
1561
1557
|
const edgesByDestination = new Map<string, t.GraphEdge[]>();
|
|
1562
1558
|
|
|
1563
|
-
for (const edge of this.
|
|
1559
|
+
for (const edge of this.sequenceEdges) {
|
|
1564
1560
|
const destinations = Array.isArray(edge.to) ? edge.to : [edge.to];
|
|
1565
1561
|
for (const destination of destinations) {
|
|
1566
1562
|
if (!edgesByDestination.has(destination)) {
|
|
@@ -1659,18 +1655,18 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1659
1655
|
for (const edge of edges) {
|
|
1660
1656
|
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
1661
1657
|
for (const source of sources) {
|
|
1662
|
-
/** Check if this source node has both
|
|
1663
|
-
const
|
|
1658
|
+
/** Check if this source node has both transfer and sequence edges */
|
|
1659
|
+
const sourceTransferEdges = this.transferEdges.filter((e) => {
|
|
1664
1660
|
const eSources = Array.isArray(e.from) ? e.from : [e.from];
|
|
1665
1661
|
return eSources.includes(source);
|
|
1666
1662
|
});
|
|
1667
|
-
const
|
|
1663
|
+
const sourceSequenceEdges = this.sequenceEdges.filter((e) => {
|
|
1668
1664
|
const eSources = Array.isArray(e.from) ? e.from : [e.from];
|
|
1669
1665
|
return eSources.includes(source);
|
|
1670
1666
|
});
|
|
1671
1667
|
|
|
1672
1668
|
/** Skip adding edge if source uses Command routing (has both types) */
|
|
1673
|
-
if (
|
|
1669
|
+
if (sourceTransferEdges.length > 0 && sourceSequenceEdges.length > 0) {
|
|
1674
1670
|
continue;
|
|
1675
1671
|
}
|
|
1676
1672
|
|