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