@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
|
@@ -22,30 +22,43 @@ import type { ToolRunnableConfig } from '@langchain/core/tools';
|
|
|
22
22
|
import type * as t from '@/types';
|
|
23
23
|
import { summarize, createEmergencySummary } from '@/messages';
|
|
24
24
|
import { StandardGraph } from './Graph';
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
Constants,
|
|
27
|
+
EdgeType,
|
|
28
|
+
DEFAULT_HANDOFF_MAX_RESULT_CHARS,
|
|
29
|
+
} from '@/common';
|
|
26
30
|
|
|
27
31
|
/** Pattern to extract instructions from transfer ToolMessage content */
|
|
28
|
-
const
|
|
32
|
+
const TRANSFER_INSTRUCTIONS_PATTERN = /(?:Instructions?|Context):\s*(.+)/is;
|
|
29
33
|
|
|
30
34
|
/**
|
|
31
35
|
* MultiAgentGraph extends StandardGraph to support dynamic multi-agent workflows
|
|
32
36
|
* with handoffs, fan-in/fan-out, and other composable patterns.
|
|
33
37
|
*
|
|
34
38
|
* Key behavior:
|
|
35
|
-
* - Agents with ONLY
|
|
36
|
-
* - Agents with ONLY
|
|
37
|
-
* - Agents with BOTH: Use Command for exclusive routing (
|
|
38
|
-
* - If
|
|
39
|
-
* - 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)
|
|
40
44
|
*
|
|
41
|
-
* This enables the common pattern where an agent either
|
|
42
|
-
* 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.
|
|
43
47
|
*/
|
|
44
48
|
export class MultiAgentGraph extends StandardGraph {
|
|
45
49
|
private edges: t.GraphEdge[];
|
|
46
50
|
private startingNodes: Set<string> = new Set();
|
|
47
|
-
private
|
|
51
|
+
private sequenceEdges: t.GraphEdge[] = [];
|
|
52
|
+
private transferEdges: t.GraphEdge[] = [];
|
|
48
53
|
private handoffEdges: t.GraphEdge[] = [];
|
|
54
|
+
/**
|
|
55
|
+
* Lazily populated registry of compiled subgraphs, keyed by agentId.
|
|
56
|
+
* Handoff tools are created in the constructor but reference subgraphs
|
|
57
|
+
* that are only created in createWorkflow(). This Map bridges that gap —
|
|
58
|
+
* tools capture the Map reference in their closure, and createWorkflow()
|
|
59
|
+
* populates it before any tool invocation occurs.
|
|
60
|
+
*/
|
|
61
|
+
private subgraphRegistry: Map<string, t.CompiledAgentWorfklow> = new Map();
|
|
49
62
|
/**
|
|
50
63
|
* Map of agentId to parallel group info.
|
|
51
64
|
* Contains groupId (incrementing number reflecting execution order) for agents in parallel groups.
|
|
@@ -68,6 +81,7 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
68
81
|
this.edges = input.edges;
|
|
69
82
|
this.categorizeEdges();
|
|
70
83
|
this.analyzeGraph();
|
|
84
|
+
this.createTransferTools();
|
|
71
85
|
this.createHandoffTools();
|
|
72
86
|
console.debug(
|
|
73
87
|
`[MultiAgentGraph] Constructor complete: ${this.agentContexts.size} agents, ${this.edges.length} edges`
|
|
@@ -75,32 +89,32 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
75
89
|
}
|
|
76
90
|
|
|
77
91
|
/**
|
|
78
|
-
* Categorize edges into handoff and
|
|
92
|
+
* Categorize edges into handoff, transfer, and sequence types
|
|
79
93
|
*/
|
|
80
94
|
private categorizeEdges(): void {
|
|
81
95
|
for (const edge of this.edges) {
|
|
82
|
-
|
|
83
|
-
// Edges with explicit 'direct' type or multi-destination without conditions are direct edges
|
|
84
|
-
if (edge.edgeType === EdgeType.DIRECT) {
|
|
85
|
-
this.directEdges.push(edge);
|
|
86
|
-
} else if (edge.edgeType === EdgeType.HANDOFF || edge.condition != null) {
|
|
96
|
+
if (edge.edgeType === EdgeType.HANDOFF) {
|
|
87
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);
|
|
88
102
|
} else {
|
|
89
|
-
// Default: single-to-single edges are
|
|
103
|
+
// Default: single-to-single edges are transfer, single-to-multiple are sequence
|
|
90
104
|
const destinations = Array.isArray(edge.to) ? edge.to : [edge.to];
|
|
91
105
|
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
92
106
|
|
|
93
107
|
if (sources.length === 1 && destinations.length > 1) {
|
|
94
|
-
// Fan-out pattern defaults to
|
|
95
|
-
this.
|
|
108
|
+
// Fan-out pattern defaults to sequence
|
|
109
|
+
this.sequenceEdges.push(edge);
|
|
96
110
|
} else {
|
|
97
|
-
// Everything else defaults to
|
|
98
|
-
this.
|
|
111
|
+
// Everything else defaults to transfer
|
|
112
|
+
this.transferEdges.push(edge);
|
|
99
113
|
}
|
|
100
114
|
}
|
|
101
115
|
}
|
|
102
116
|
console.debug(
|
|
103
|
-
`[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)`
|
|
104
118
|
);
|
|
105
119
|
}
|
|
106
120
|
|
|
@@ -171,8 +185,8 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
171
185
|
if (visited.has(current)) continue;
|
|
172
186
|
visited.add(current);
|
|
173
187
|
|
|
174
|
-
// Find
|
|
175
|
-
for (const edge of this.
|
|
188
|
+
// Find sequence edges from this node
|
|
189
|
+
for (const edge of this.sequenceEdges) {
|
|
176
190
|
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
177
191
|
if (!sources.includes(current)) continue;
|
|
178
192
|
|
|
@@ -200,8 +214,8 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
200
214
|
}
|
|
201
215
|
}
|
|
202
216
|
|
|
203
|
-
// Also follow handoff edges for traversal (
|
|
204
|
-
for (const edge of this.handoffEdges) {
|
|
217
|
+
// Also follow transfer and handoff edges for traversal (they don't create parallel groups)
|
|
218
|
+
for (const edge of [...this.transferEdges, ...this.handoffEdges]) {
|
|
205
219
|
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
206
220
|
if (!sources.includes(current)) continue;
|
|
207
221
|
|
|
@@ -251,54 +265,52 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
251
265
|
}
|
|
252
266
|
|
|
253
267
|
/**
|
|
254
|
-
* 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.
|
|
255
270
|
*/
|
|
256
|
-
private
|
|
257
|
-
|
|
258
|
-
const handoffsByAgent = new Map<string, t.GraphEdge[]>();
|
|
271
|
+
private createTransferTools(): void {
|
|
272
|
+
const transfersByAgent = new Map<string, t.GraphEdge[]>();
|
|
259
273
|
|
|
260
|
-
|
|
261
|
-
for (const edge of this.handoffEdges) {
|
|
274
|
+
for (const edge of this.transferEdges) {
|
|
262
275
|
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
263
276
|
sources.forEach((source) => {
|
|
264
|
-
if (!
|
|
265
|
-
|
|
277
|
+
if (!transfersByAgent.has(source)) {
|
|
278
|
+
transfersByAgent.set(source, []);
|
|
266
279
|
}
|
|
267
|
-
|
|
280
|
+
transfersByAgent.get(source)!.push(edge);
|
|
268
281
|
});
|
|
269
282
|
}
|
|
270
283
|
|
|
271
|
-
|
|
272
|
-
for (const [agentId, edges] of handoffsByAgent) {
|
|
284
|
+
for (const [agentId, edges] of transfersByAgent) {
|
|
273
285
|
const agentContext = this.agentContexts.get(agentId);
|
|
274
286
|
if (!agentContext) continue;
|
|
275
287
|
|
|
276
|
-
|
|
277
|
-
const handoffTools: t.GenericTool[] = [];
|
|
288
|
+
const transferTools: t.GenericTool[] = [];
|
|
278
289
|
const sourceAgentName = agentContext.name ?? agentId;
|
|
279
290
|
for (const edge of edges) {
|
|
280
|
-
|
|
281
|
-
...this.
|
|
291
|
+
transferTools.push(
|
|
292
|
+
...this.createTransferToolsForEdge(edge, agentId, sourceAgentName)
|
|
282
293
|
);
|
|
283
294
|
}
|
|
284
295
|
|
|
285
296
|
if (!agentContext.graphTools) {
|
|
286
297
|
agentContext.graphTools = [];
|
|
287
298
|
}
|
|
288
|
-
agentContext.graphTools.push(...
|
|
299
|
+
agentContext.graphTools.push(...transferTools);
|
|
289
300
|
console.debug(
|
|
290
|
-
`[MultiAgentGraph]
|
|
301
|
+
`[MultiAgentGraph] Transfer tools for "${agentId}": [${transferTools.map((t) => t.name).join(', ')}]`
|
|
291
302
|
);
|
|
292
303
|
}
|
|
293
304
|
}
|
|
294
305
|
|
|
295
306
|
/**
|
|
296
|
-
* Create
|
|
297
|
-
*
|
|
298
|
-
* @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
|
|
299
311
|
* @param sourceAgentName - The human-readable name of the source agent
|
|
300
312
|
*/
|
|
301
|
-
private
|
|
313
|
+
private createTransferToolsForEdge(
|
|
302
314
|
edge: t.GraphEdge,
|
|
303
315
|
sourceAgentId: string,
|
|
304
316
|
sourceAgentName: string
|
|
@@ -313,9 +325,9 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
313
325
|
edge.description ?? 'Conditionally transfer control based on state';
|
|
314
326
|
|
|
315
327
|
/** Check if we have a prompt for handoff input */
|
|
316
|
-
const
|
|
328
|
+
const hasTransferInput =
|
|
317
329
|
edge.prompt != null && typeof edge.prompt === 'string';
|
|
318
|
-
const
|
|
330
|
+
const transferInputDescription = hasTransferInput ? edge.prompt : undefined;
|
|
319
331
|
const promptKey = edge.promptKey ?? 'instructions';
|
|
320
332
|
|
|
321
333
|
tools.push(
|
|
@@ -344,7 +356,7 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
344
356
|
|
|
345
357
|
let content = `Conditionally transferred to ${destination}`;
|
|
346
358
|
if (
|
|
347
|
-
|
|
359
|
+
hasTransferInput &&
|
|
348
360
|
promptKey in input &&
|
|
349
361
|
input[promptKey] != null
|
|
350
362
|
) {
|
|
@@ -371,13 +383,13 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
371
383
|
},
|
|
372
384
|
{
|
|
373
385
|
name: toolName,
|
|
374
|
-
schema:
|
|
386
|
+
schema: hasTransferInput
|
|
375
387
|
? {
|
|
376
388
|
type: 'object',
|
|
377
389
|
properties: {
|
|
378
390
|
[promptKey]: {
|
|
379
391
|
type: 'string',
|
|
380
|
-
description:
|
|
392
|
+
description: transferInputDescription as string,
|
|
381
393
|
},
|
|
382
394
|
},
|
|
383
395
|
required: [],
|
|
@@ -394,12 +406,12 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
394
406
|
const destContext = this.agentContexts.get(destination);
|
|
395
407
|
const toolDescription =
|
|
396
408
|
edge.description ??
|
|
397
|
-
this.
|
|
409
|
+
this.buildDefaultTransferDescription(destContext, destination);
|
|
398
410
|
|
|
399
411
|
/** Check if we have a prompt for handoff input */
|
|
400
|
-
const
|
|
412
|
+
const hasTransferInput =
|
|
401
413
|
edge.prompt != null && typeof edge.prompt === 'string';
|
|
402
|
-
const
|
|
414
|
+
const transferInputDescription = hasTransferInput
|
|
403
415
|
? edge.prompt
|
|
404
416
|
: undefined;
|
|
405
417
|
const promptKey = edge.promptKey ?? 'instructions';
|
|
@@ -414,7 +426,7 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
414
426
|
|
|
415
427
|
let content = `Successfully transferred to ${destination}`;
|
|
416
428
|
if (
|
|
417
|
-
|
|
429
|
+
hasTransferInput &&
|
|
418
430
|
promptKey in input &&
|
|
419
431
|
input[promptKey] != null
|
|
420
432
|
) {
|
|
@@ -505,13 +517,13 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
505
517
|
},
|
|
506
518
|
{
|
|
507
519
|
name: toolName,
|
|
508
|
-
schema:
|
|
520
|
+
schema: hasTransferInput
|
|
509
521
|
? {
|
|
510
522
|
type: 'object',
|
|
511
523
|
properties: {
|
|
512
524
|
[promptKey]: {
|
|
513
525
|
type: 'string',
|
|
514
|
-
description:
|
|
526
|
+
description: transferInputDescription as string,
|
|
515
527
|
},
|
|
516
528
|
},
|
|
517
529
|
required: [],
|
|
@@ -528,13 +540,13 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
528
540
|
}
|
|
529
541
|
|
|
530
542
|
/**
|
|
531
|
-
* Builds a meaningful default description for a
|
|
543
|
+
* Builds a meaningful default description for a transfer tool when no explicit
|
|
532
544
|
* edge.description is provided. Uses the destination agent's name and description
|
|
533
545
|
* so the LLM can make informed routing decisions.
|
|
534
546
|
* @param destContext - AgentContext of the destination agent (may be undefined)
|
|
535
547
|
* @param destinationId - Raw agent ID (fallback when context unavailable)
|
|
536
548
|
*/
|
|
537
|
-
private
|
|
549
|
+
private buildDefaultTransferDescription(
|
|
538
550
|
destContext: import('@/agents/AgentContext').AgentContext | undefined,
|
|
539
551
|
destinationId: string
|
|
540
552
|
): string {
|
|
@@ -547,6 +559,269 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
547
559
|
return `Transfer control to "${displayName}"`;
|
|
548
560
|
}
|
|
549
561
|
|
|
562
|
+
/**
|
|
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.
|
|
568
|
+
*
|
|
569
|
+
* This enables the supervisor pattern: parent calls child → gets result → thinks → calls another.
|
|
570
|
+
*/
|
|
571
|
+
private createHandoffTools(): void {
|
|
572
|
+
const handoffsByAgent = new Map<string, t.GraphEdge[]>();
|
|
573
|
+
|
|
574
|
+
for (const edge of this.handoffEdges) {
|
|
575
|
+
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
576
|
+
sources.forEach((source) => {
|
|
577
|
+
if (!handoffsByAgent.has(source)) {
|
|
578
|
+
handoffsByAgent.set(source, []);
|
|
579
|
+
}
|
|
580
|
+
handoffsByAgent.get(source)!.push(edge);
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
for (const [agentId, edges] of handoffsByAgent) {
|
|
585
|
+
const agentContext = this.agentContexts.get(agentId);
|
|
586
|
+
if (!agentContext) continue;
|
|
587
|
+
|
|
588
|
+
const handoffTools: t.GenericTool[] = [];
|
|
589
|
+
for (const edge of edges) {
|
|
590
|
+
handoffTools.push(
|
|
591
|
+
...this.createHandoffToolsForEdge(edge, agentId)
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (!agentContext.graphTools) {
|
|
596
|
+
agentContext.graphTools = [];
|
|
597
|
+
}
|
|
598
|
+
agentContext.graphTools.push(...handoffTools);
|
|
599
|
+
console.debug(
|
|
600
|
+
`[MultiAgentGraph] Handoff tools for "${agentId}": [${handoffTools.map((t) => t.name).join(', ')}]`
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Create handoff tools for an edge (handles multiple destinations).
|
|
607
|
+
* Each handoff tool invokes the child agent's compiled subgraph inline,
|
|
608
|
+
* extracts the final AI message text, truncates it, and returns it as
|
|
609
|
+
* a string (which becomes a ToolMessage in the parent's context).
|
|
610
|
+
*
|
|
611
|
+
* @param edge - The graph edge defining the handoff
|
|
612
|
+
* @param sourceAgentId - The ID of the parent/supervisor agent
|
|
613
|
+
*/
|
|
614
|
+
private createHandoffToolsForEdge(
|
|
615
|
+
edge: t.GraphEdge,
|
|
616
|
+
sourceAgentId: string
|
|
617
|
+
): t.GenericTool[] {
|
|
618
|
+
const tools: t.GenericTool[] = [];
|
|
619
|
+
const destinations = Array.isArray(edge.to) ? edge.to : [edge.to];
|
|
620
|
+
const maxResultChars =
|
|
621
|
+
edge.maxResultChars ?? DEFAULT_HANDOFF_MAX_RESULT_CHARS;
|
|
622
|
+
|
|
623
|
+
for (const destination of destinations) {
|
|
624
|
+
const toolName = `${Constants.LC_HANDOFF_TO_}${destination}`;
|
|
625
|
+
const destContext = this.agentContexts.get(destination);
|
|
626
|
+
const toolDescription =
|
|
627
|
+
edge.description ??
|
|
628
|
+
this.buildDefaultHandoffDescription(destContext, destination);
|
|
629
|
+
|
|
630
|
+
const hasPromptInput =
|
|
631
|
+
edge.prompt != null && typeof edge.prompt === 'string';
|
|
632
|
+
const promptInputDescription = hasPromptInput ? edge.prompt : undefined;
|
|
633
|
+
const promptKey = edge.promptKey ?? 'instructions';
|
|
634
|
+
|
|
635
|
+
/** Capture registry reference — Map populated in createWorkflow() */
|
|
636
|
+
const registry = this.subgraphRegistry;
|
|
637
|
+
|
|
638
|
+
tools.push(
|
|
639
|
+
tool(
|
|
640
|
+
async (rawInput, config) => {
|
|
641
|
+
const input = rawInput as Record<string, unknown>;
|
|
642
|
+
const subgraph = registry.get(destination);
|
|
643
|
+
if (!subgraph) {
|
|
644
|
+
throw new Error(
|
|
645
|
+
`Handoff target "${destination}" subgraph not found in registry. ` +
|
|
646
|
+
'This is a bug: createWorkflow() should have populated the subgraph registry.'
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const state = getCurrentTaskInput() as t.BaseGraphState;
|
|
651
|
+
let childMessages = [...state.messages];
|
|
652
|
+
|
|
653
|
+
/** Inject instructions as HumanMessage if provided by the parent LLM */
|
|
654
|
+
if (
|
|
655
|
+
hasPromptInput &&
|
|
656
|
+
promptKey in input &&
|
|
657
|
+
input[promptKey] != null
|
|
658
|
+
) {
|
|
659
|
+
childMessages = [
|
|
660
|
+
...childMessages,
|
|
661
|
+
new HumanMessage(String(input[promptKey])),
|
|
662
|
+
];
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const childState: t.BaseGraphState = {
|
|
666
|
+
messages: childMessages,
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
console.debug(
|
|
670
|
+
`[MultiAgentGraph] Handoff "${sourceAgentId}" -> "${destination}" START ` +
|
|
671
|
+
`(messages: ${childMessages.length})`
|
|
672
|
+
);
|
|
673
|
+
|
|
674
|
+
try {
|
|
675
|
+
/**
|
|
676
|
+
* Invoke the child subgraph with config propagation.
|
|
677
|
+
* Config carries callbacks (for SSE streaming), abort signal,
|
|
678
|
+
* and configurable data (thread_id, user_id) to the child.
|
|
679
|
+
*/
|
|
680
|
+
const result = await subgraph.invoke(childState, config);
|
|
681
|
+
|
|
682
|
+
const resultText = MultiAgentGraph.extractHandoffResult(
|
|
683
|
+
result.messages,
|
|
684
|
+
destination
|
|
685
|
+
);
|
|
686
|
+
const truncatedResult = MultiAgentGraph.truncateHandoffResult(
|
|
687
|
+
resultText,
|
|
688
|
+
maxResultChars
|
|
689
|
+
);
|
|
690
|
+
|
|
691
|
+
console.debug(
|
|
692
|
+
`[MultiAgentGraph] Handoff "${sourceAgentId}" -> "${destination}" DONE ` +
|
|
693
|
+
`(result: ${resultText.length} chars` +
|
|
694
|
+
`${truncatedResult.length < resultText.length ? `, truncated to ${truncatedResult.length}` : ''})`
|
|
695
|
+
);
|
|
696
|
+
|
|
697
|
+
return truncatedResult;
|
|
698
|
+
} catch (err) {
|
|
699
|
+
const errorMessage =
|
|
700
|
+
err instanceof Error ? err.message : String(err);
|
|
701
|
+
console.error(
|
|
702
|
+
`[MultiAgentGraph] Handoff "${sourceAgentId}" -> "${destination}" ERROR:`,
|
|
703
|
+
errorMessage
|
|
704
|
+
);
|
|
705
|
+
return `[Handoff to "${destination}" failed: ${errorMessage}]`;
|
|
706
|
+
}
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
name: toolName,
|
|
710
|
+
schema: hasPromptInput
|
|
711
|
+
? {
|
|
712
|
+
type: 'object',
|
|
713
|
+
properties: {
|
|
714
|
+
[promptKey]: {
|
|
715
|
+
type: 'string',
|
|
716
|
+
description: promptInputDescription as string,
|
|
717
|
+
},
|
|
718
|
+
},
|
|
719
|
+
required: [],
|
|
720
|
+
}
|
|
721
|
+
: { type: 'object', properties: {}, required: [] },
|
|
722
|
+
description: toolDescription,
|
|
723
|
+
}
|
|
724
|
+
)
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
return tools;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Extract the final text result from a child agent's output messages.
|
|
733
|
+
* Walks backwards to find the last AIMessage with text content.
|
|
734
|
+
* Handles both string content and array content (multi-modal messages).
|
|
735
|
+
* @param messages - The child agent's output messages
|
|
736
|
+
* @param agentId - The child agent ID (for fallback message)
|
|
737
|
+
*/
|
|
738
|
+
static extractHandoffResult(
|
|
739
|
+
messages: BaseMessage[],
|
|
740
|
+
agentId: string
|
|
741
|
+
): string {
|
|
742
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
743
|
+
const msg = messages[i];
|
|
744
|
+
if (msg.getType() !== 'ai') continue;
|
|
745
|
+
|
|
746
|
+
const content = msg.content;
|
|
747
|
+
if (typeof content === 'string' && content.trim()) {
|
|
748
|
+
return content.trim();
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/** Handle array content (multi-modal messages with text blocks) */
|
|
752
|
+
if (Array.isArray(content)) {
|
|
753
|
+
const textParts = content
|
|
754
|
+
.filter(
|
|
755
|
+
(
|
|
756
|
+
block
|
|
757
|
+
): block is {
|
|
758
|
+
type: string;
|
|
759
|
+
text: string;
|
|
760
|
+
} =>
|
|
761
|
+
typeof block === 'object' &&
|
|
762
|
+
block !== null &&
|
|
763
|
+
'type' in block &&
|
|
764
|
+
block.type === 'text' &&
|
|
765
|
+
'text' in block &&
|
|
766
|
+
typeof block.text === 'string'
|
|
767
|
+
)
|
|
768
|
+
.map((block) => block.text);
|
|
769
|
+
|
|
770
|
+
const text = textParts.join('\n').trim();
|
|
771
|
+
if (text) return text;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
return `[Agent "${agentId}" completed but produced no text output]`;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Truncate handoff result using head/tail strategy (60/40 split).
|
|
780
|
+
* Preserves the beginning (key findings) and end (conclusions).
|
|
781
|
+
* Matches the TaskTool.truncateResult pattern from Ranger.
|
|
782
|
+
* @param result - The full result text
|
|
783
|
+
* @param maxChars - Maximum allowed characters
|
|
784
|
+
*/
|
|
785
|
+
static truncateHandoffResult(result: string, maxChars: number): string {
|
|
786
|
+
if (!result || result.length <= maxChars) {
|
|
787
|
+
return result;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const truncationNotice =
|
|
791
|
+
'\n\n[... handoff output truncated — middle section omitted to fit parent context ...]\n\n';
|
|
792
|
+
const available = maxChars - truncationNotice.length;
|
|
793
|
+
if (available <= 0) {
|
|
794
|
+
return result.substring(0, maxChars);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
const headSize = Math.floor(available * 0.6);
|
|
798
|
+
const tailSize = available - headSize;
|
|
799
|
+
|
|
800
|
+
return (
|
|
801
|
+
result.substring(0, headSize) +
|
|
802
|
+
truncationNotice +
|
|
803
|
+
result.substring(result.length - tailSize)
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Build a meaningful default description for a handoff tool.
|
|
809
|
+
* @param destContext - AgentContext of the destination agent
|
|
810
|
+
* @param destinationId - Raw agent ID (fallback)
|
|
811
|
+
*/
|
|
812
|
+
private buildDefaultHandoffDescription(
|
|
813
|
+
destContext: import('@/agents/AgentContext').AgentContext | undefined,
|
|
814
|
+
destinationId: string
|
|
815
|
+
): string {
|
|
816
|
+
const displayName = destContext?.name ?? destinationId;
|
|
817
|
+
const agentDescription = destContext?.description;
|
|
818
|
+
|
|
819
|
+
if (agentDescription != null && agentDescription !== '') {
|
|
820
|
+
return `Hand off task to "${displayName}": ${agentDescription}. The agent will execute and return its result.`;
|
|
821
|
+
}
|
|
822
|
+
return `Hand off task to "${displayName}" and receive its result.`;
|
|
823
|
+
}
|
|
824
|
+
|
|
550
825
|
/**
|
|
551
826
|
* Create a complete agent subgraph (similar to createReactAgent)
|
|
552
827
|
*/
|
|
@@ -567,7 +842,7 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
567
842
|
* @param agentId - The agent ID to check for handoff reception
|
|
568
843
|
* @returns Object with filtered messages, extracted instructions, source agent, and parallel siblings
|
|
569
844
|
*/
|
|
570
|
-
private
|
|
845
|
+
private processTransferReception(
|
|
571
846
|
messages: BaseMessage[],
|
|
572
847
|
agentId: string
|
|
573
848
|
): {
|
|
@@ -607,8 +882,8 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
607
882
|
if (isTransferMessage) {
|
|
608
883
|
destinationAgent = toolName.replace(Constants.LC_TRANSFER_TO_, '');
|
|
609
884
|
} else if (isConditionalTransfer) {
|
|
610
|
-
const
|
|
611
|
-
destinationAgent = typeof
|
|
885
|
+
const transferDest = candidateMsg.additional_kwargs.handoff_destination;
|
|
886
|
+
destinationAgent = typeof transferDest === 'string' ? transferDest : null;
|
|
612
887
|
}
|
|
613
888
|
|
|
614
889
|
/** Check if this transfer targets our agent */
|
|
@@ -628,7 +903,7 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
628
903
|
? toolMessage.content
|
|
629
904
|
: JSON.stringify(toolMessage.content);
|
|
630
905
|
|
|
631
|
-
const instructionsMatch = contentStr.match(
|
|
906
|
+
const instructionsMatch = contentStr.match(TRANSFER_INSTRUCTIONS_PATTERN);
|
|
632
907
|
const instructions = instructionsMatch?.[1]?.trim() ?? null;
|
|
633
908
|
|
|
634
909
|
/** Extract source agent name from additional_kwargs */
|
|
@@ -859,7 +1134,7 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
859
1134
|
}
|
|
860
1135
|
|
|
861
1136
|
/**
|
|
862
|
-
* Create the multi-agent workflow with
|
|
1137
|
+
* Create the multi-agent workflow with handoffs, transfers, and sequences
|
|
863
1138
|
*/
|
|
864
1139
|
override createWorkflow(): t.CompiledMultiAgentWorkflow {
|
|
865
1140
|
const StateAnnotation = Annotation.Root({
|
|
@@ -887,44 +1162,47 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
887
1162
|
// Add all agents as complete subgraphs
|
|
888
1163
|
for (const [agentId] of this.agentContexts) {
|
|
889
1164
|
// Get all possible destinations for this agent
|
|
890
|
-
const
|
|
891
|
-
const
|
|
1165
|
+
const transferDestinations = new Set<string>();
|
|
1166
|
+
const sequenceDestinations = new Set<string>();
|
|
892
1167
|
|
|
893
|
-
// Check
|
|
894
|
-
for (const edge of this.
|
|
1168
|
+
// Check transfer edges for destinations
|
|
1169
|
+
for (const edge of this.transferEdges) {
|
|
895
1170
|
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
896
1171
|
if (sources.includes(agentId) === true) {
|
|
897
1172
|
const dests = Array.isArray(edge.to) ? edge.to : [edge.to];
|
|
898
|
-
dests.forEach((dest) =>
|
|
1173
|
+
dests.forEach((dest) => transferDestinations.add(dest));
|
|
899
1174
|
}
|
|
900
1175
|
}
|
|
901
1176
|
|
|
902
|
-
// Check
|
|
903
|
-
for (const edge of this.
|
|
1177
|
+
// Check sequence edges for destinations
|
|
1178
|
+
for (const edge of this.sequenceEdges) {
|
|
904
1179
|
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
905
1180
|
if (sources.includes(agentId) === true) {
|
|
906
1181
|
const dests = Array.isArray(edge.to) ? edge.to : [edge.to];
|
|
907
|
-
dests.forEach((dest) =>
|
|
1182
|
+
dests.forEach((dest) => sequenceDestinations.add(dest));
|
|
908
1183
|
}
|
|
909
1184
|
}
|
|
910
1185
|
|
|
911
|
-
/** Check if this agent has BOTH
|
|
912
|
-
const
|
|
913
|
-
const
|
|
914
|
-
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;
|
|
915
1190
|
|
|
916
1191
|
/** Collect all possible destinations for this agent */
|
|
917
1192
|
const allDestinations = new Set([
|
|
918
|
-
...
|
|
919
|
-
...
|
|
1193
|
+
...transferDestinations,
|
|
1194
|
+
...sequenceDestinations,
|
|
920
1195
|
]);
|
|
921
|
-
if (
|
|
1196
|
+
if (transferDestinations.size > 0 || sequenceDestinations.size === 0) {
|
|
922
1197
|
allDestinations.add(END);
|
|
923
1198
|
}
|
|
924
1199
|
|
|
925
1200
|
/** Agent subgraph (includes agent + tools) */
|
|
926
1201
|
const agentSubgraph = this.createAgentSubgraph(agentId);
|
|
927
1202
|
|
|
1203
|
+
/** Register subgraph for handoff tools (lazy reference resolution) */
|
|
1204
|
+
this.subgraphRegistry.set(agentId, agentSubgraph);
|
|
1205
|
+
|
|
928
1206
|
/** Wrapper function that handles agentMessages channel, handoff reception, and conditional routing */
|
|
929
1207
|
const agentWrapper = async (
|
|
930
1208
|
state: t.MultiAgentGraphState,
|
|
@@ -936,25 +1214,25 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
936
1214
|
let result: t.MultiAgentGraphState;
|
|
937
1215
|
|
|
938
1216
|
/**
|
|
939
|
-
* Check if this agent is receiving a
|
|
1217
|
+
* Check if this agent is receiving a transfer.
|
|
940
1218
|
* If so, filter out the transfer messages and inject instructions as preamble.
|
|
941
1219
|
* This prevents the receiving agent from seeing the transfer as "completed work"
|
|
942
1220
|
* and prematurely producing an end token.
|
|
943
1221
|
*/
|
|
944
|
-
const
|
|
1222
|
+
const transferContext = this.processTransferReception(
|
|
945
1223
|
state.messages,
|
|
946
1224
|
agentId
|
|
947
1225
|
);
|
|
948
1226
|
|
|
949
|
-
if (
|
|
1227
|
+
if (transferContext !== null) {
|
|
950
1228
|
const {
|
|
951
1229
|
filteredMessages,
|
|
952
1230
|
instructions,
|
|
953
1231
|
sourceAgentName,
|
|
954
1232
|
parallelSiblings,
|
|
955
|
-
} =
|
|
1233
|
+
} = transferContext;
|
|
956
1234
|
console.debug(
|
|
957
|
-
`[MultiAgentGraph] Agent "${agentId}" receiving
|
|
1235
|
+
`[MultiAgentGraph] Agent "${agentId}" receiving transfer from "${sourceAgentName}" (instructions: ${instructions != null}, parallelSiblings: ${parallelSiblings.length})`
|
|
958
1236
|
);
|
|
959
1237
|
|
|
960
1238
|
/**
|
|
@@ -1099,9 +1377,9 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1099
1377
|
`[MultiAgentGraph] Agent "${agentId}" wrapper EXIT (result messages: ${result.messages.length})`
|
|
1100
1378
|
);
|
|
1101
1379
|
|
|
1102
|
-
/** If agent has both
|
|
1380
|
+
/** If agent has both transfer and sequence edges, use Command for exclusive routing */
|
|
1103
1381
|
if (needsCommandRouting) {
|
|
1104
|
-
/** Check if a
|
|
1382
|
+
/** Check if a transfer occurred */
|
|
1105
1383
|
const lastMessage = result.messages[
|
|
1106
1384
|
result.messages.length - 1
|
|
1107
1385
|
] as BaseMessage | null;
|
|
@@ -1111,26 +1389,26 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1111
1389
|
typeof lastMessage.name === 'string' &&
|
|
1112
1390
|
lastMessage.name.startsWith(Constants.LC_TRANSFER_TO_)
|
|
1113
1391
|
) {
|
|
1114
|
-
/**
|
|
1115
|
-
const
|
|
1392
|
+
/** Transfer occurred - extract destination and navigate there exclusively */
|
|
1393
|
+
const transferDest = lastMessage.name.replace(
|
|
1116
1394
|
Constants.LC_TRANSFER_TO_,
|
|
1117
1395
|
''
|
|
1118
1396
|
);
|
|
1119
1397
|
console.debug(
|
|
1120
|
-
`[MultiAgentGraph] Command routing: "${agentId}" ->
|
|
1398
|
+
`[MultiAgentGraph] Command routing: "${agentId}" -> transfer to "${transferDest}" (sequence edges skipped: [${Array.from(sequenceDestinations).join(', ')}])`
|
|
1121
1399
|
);
|
|
1122
1400
|
|
|
1123
1401
|
/** Validate destination agent exists */
|
|
1124
|
-
if (!this.agentContexts.has(
|
|
1402
|
+
if (!this.agentContexts.has(transferDest)) {
|
|
1125
1403
|
const availableAgents = Array.from(
|
|
1126
1404
|
this.agentContexts.keys()
|
|
1127
1405
|
).join(', ');
|
|
1128
1406
|
console.error(
|
|
1129
|
-
`[MultiAgentGraph]
|
|
1407
|
+
`[MultiAgentGraph] Transfer to non-existent agent "${transferDest}". Available: ${availableAgents}`
|
|
1130
1408
|
);
|
|
1131
1409
|
/** Return error to model so it can self-correct */
|
|
1132
1410
|
const errorMsg = new ToolMessage({
|
|
1133
|
-
content: `Transfer failed: agent "${
|
|
1411
|
+
content: `Transfer failed: agent "${transferDest}" does not exist. Available agents: ${availableAgents}. Please choose a valid agent to transfer to.`,
|
|
1134
1412
|
tool_call_id: (lastMessage as ToolMessage).tool_call_id,
|
|
1135
1413
|
name: lastMessage.name,
|
|
1136
1414
|
});
|
|
@@ -1141,7 +1419,7 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1141
1419
|
}
|
|
1142
1420
|
|
|
1143
1421
|
/** Pre-handoff context compaction: if receiving agent has smaller budget */
|
|
1144
|
-
const receiverContext = this.agentContexts.get(
|
|
1422
|
+
const receiverContext = this.agentContexts.get(transferDest);
|
|
1145
1423
|
const senderContext = this.agentContexts.get(agentId);
|
|
1146
1424
|
if (
|
|
1147
1425
|
receiverContext?.maxContextTokens != null &&
|
|
@@ -1157,7 +1435,7 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1157
1435
|
if (currentSize > receiverBudget * 0.7) {
|
|
1158
1436
|
console.warn(
|
|
1159
1437
|
`[MultiAgentGraph] Pre-handoff compaction: context (${currentSize} tokens) exceeds ` +
|
|
1160
|
-
`70% of receiver "${
|
|
1438
|
+
`70% of receiver "${transferDest}" budget (${receiverBudget} tokens)`
|
|
1161
1439
|
);
|
|
1162
1440
|
|
|
1163
1441
|
/** Generate handoff briefing */
|
|
@@ -1175,8 +1453,8 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1175
1453
|
summaryBudget: Math.floor(receiverBudget * 0.2),
|
|
1176
1454
|
isMultiAgent: true,
|
|
1177
1455
|
agentWorkflowState: {
|
|
1178
|
-
currentAgentId:
|
|
1179
|
-
agentChain: [agentId,
|
|
1456
|
+
currentAgentId: transferDest,
|
|
1457
|
+
agentChain: [agentId, transferDest],
|
|
1180
1458
|
pendingAgents: [],
|
|
1181
1459
|
},
|
|
1182
1460
|
}
|
|
@@ -1232,14 +1510,14 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1232
1510
|
|
|
1233
1511
|
return new Command({
|
|
1234
1512
|
update: result,
|
|
1235
|
-
goto:
|
|
1513
|
+
goto: transferDest,
|
|
1236
1514
|
});
|
|
1237
1515
|
} else {
|
|
1238
|
-
/** No
|
|
1516
|
+
/** No transfer - proceed with sequence edges */
|
|
1239
1517
|
console.debug(
|
|
1240
|
-
`[MultiAgentGraph] Command routing: "${agentId}" -> no
|
|
1518
|
+
`[MultiAgentGraph] Command routing: "${agentId}" -> no transfer, following sequence edges: [${Array.from(sequenceDestinations).join(', ')}]`
|
|
1241
1519
|
);
|
|
1242
|
-
const directDests = Array.from(
|
|
1520
|
+
const directDests = Array.from(sequenceDestinations);
|
|
1243
1521
|
if (directDests.length === 1) {
|
|
1244
1522
|
return new Command({
|
|
1245
1523
|
update: result,
|
|
@@ -1273,12 +1551,12 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1273
1551
|
}
|
|
1274
1552
|
|
|
1275
1553
|
/**
|
|
1276
|
-
* Add
|
|
1554
|
+
* Add sequence edges for automatic transitions
|
|
1277
1555
|
* Group edges by destination to handle fan-in scenarios
|
|
1278
1556
|
*/
|
|
1279
1557
|
const edgesByDestination = new Map<string, t.GraphEdge[]>();
|
|
1280
1558
|
|
|
1281
|
-
for (const edge of this.
|
|
1559
|
+
for (const edge of this.sequenceEdges) {
|
|
1282
1560
|
const destinations = Array.isArray(edge.to) ? edge.to : [edge.to];
|
|
1283
1561
|
for (const destination of destinations) {
|
|
1284
1562
|
if (!edgesByDestination.has(destination)) {
|
|
@@ -1377,18 +1655,18 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1377
1655
|
for (const edge of edges) {
|
|
1378
1656
|
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
1379
1657
|
for (const source of sources) {
|
|
1380
|
-
/** Check if this source node has both
|
|
1381
|
-
const
|
|
1658
|
+
/** Check if this source node has both transfer and sequence edges */
|
|
1659
|
+
const sourceTransferEdges = this.transferEdges.filter((e) => {
|
|
1382
1660
|
const eSources = Array.isArray(e.from) ? e.from : [e.from];
|
|
1383
1661
|
return eSources.includes(source);
|
|
1384
1662
|
});
|
|
1385
|
-
const
|
|
1663
|
+
const sourceSequenceEdges = this.sequenceEdges.filter((e) => {
|
|
1386
1664
|
const eSources = Array.isArray(e.from) ? e.from : [e.from];
|
|
1387
1665
|
return eSources.includes(source);
|
|
1388
1666
|
});
|
|
1389
1667
|
|
|
1390
1668
|
/** Skip adding edge if source uses Command routing (has both types) */
|
|
1391
|
-
if (
|
|
1669
|
+
if (sourceTransferEdges.length > 0 && sourceSequenceEdges.length > 0) {
|
|
1392
1670
|
continue;
|
|
1393
1671
|
}
|
|
1394
1672
|
|