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