@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
@@ -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 { Constants, EdgeType } from '@/common';
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 HANDOFF_INSTRUCTIONS_PATTERN = /(?:Instructions?|Context):\s*(.+)/is;
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 handoff edges: Can dynamically route to any handoff destination
36
- * - Agents with ONLY direct edges: Always follow their direct edges
37
- * - Agents with BOTH: Use Command for exclusive routing (handoff OR direct, not both)
38
- * - If handoff occurs: Only the handoff destination executes
39
- * - If no handoff: Direct edges execute (potentially in parallel)
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 delegates (handoff)
42
- * OR continues its workflow (direct edges), but not both simultaneously.
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 directEdges: t.GraphEdge[] = [];
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 direct types
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
- // Default behavior: edges with conditions or explicit 'handoff' type are handoff edges
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 handoff, single-to-multiple are direct
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 direct
95
- this.directEdges.push(edge);
108
+ // Fan-out pattern defaults to sequence
109
+ this.sequenceEdges.push(edge);
96
110
  } else {
97
- // Everything else defaults to handoff
98
- this.handoffEdges.push(edge);
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.directEdges.length} direct (of ${this.edges.length} total)`
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 direct edges from this node
175
- for (const edge of this.directEdges) {
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 (but they don't create parallel groups)
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 handoff tools for agents based on handoff edges only
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 createHandoffTools(): void {
257
- // Group handoff edges by source agent(s)
258
- const handoffsByAgent = new Map<string, t.GraphEdge[]>();
271
+ private createTransferTools(): void {
272
+ const transfersByAgent = new Map<string, t.GraphEdge[]>();
259
273
 
260
- // Only process handoff edges for tool creation
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 (!handoffsByAgent.has(source)) {
265
- handoffsByAgent.set(source, []);
277
+ if (!transfersByAgent.has(source)) {
278
+ transfersByAgent.set(source, []);
266
279
  }
267
- handoffsByAgent.get(source)!.push(edge);
280
+ transfersByAgent.get(source)!.push(edge);
268
281
  });
269
282
  }
270
283
 
271
- // Create handoff tools for each agent
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
- // Create handoff tools for this agent's outgoing edges
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
- handoffTools.push(
281
- ...this.createHandoffToolsForEdge(edge, agentId, sourceAgentName)
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(...handoffTools);
299
+ agentContext.graphTools.push(...transferTools);
289
300
  console.debug(
290
- `[MultiAgentGraph] Handoff tools for "${agentId}": [${handoffTools.map((t) => t.name).join(', ')}]`
301
+ `[MultiAgentGraph] Transfer tools for "${agentId}": [${transferTools.map((t) => t.name).join(', ')}]`
291
302
  );
292
303
  }
293
304
  }
294
305
 
295
306
  /**
296
- * Create handoff tools for an edge (handles multiple destinations)
297
- * @param edge - The graph edge defining the handoff
298
- * @param sourceAgentId - The ID of the agent that will perform the handoff
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 createHandoffToolsForEdge(
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 hasHandoffInput =
328
+ const hasTransferInput =
317
329
  edge.prompt != null && typeof edge.prompt === 'string';
318
- const handoffInputDescription = hasHandoffInput ? edge.prompt : undefined;
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
- hasHandoffInput &&
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: hasHandoffInput
386
+ schema: hasTransferInput
375
387
  ? {
376
388
  type: 'object',
377
389
  properties: {
378
390
  [promptKey]: {
379
391
  type: 'string',
380
- description: handoffInputDescription as string,
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.buildDefaultHandoffDescription(destContext, destination);
409
+ this.buildDefaultTransferDescription(destContext, destination);
398
410
 
399
411
  /** Check if we have a prompt for handoff input */
400
- const hasHandoffInput =
412
+ const hasTransferInput =
401
413
  edge.prompt != null && typeof edge.prompt === 'string';
402
- const handoffInputDescription = hasHandoffInput
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
- hasHandoffInput &&
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: hasHandoffInput
520
+ schema: hasTransferInput
509
521
  ? {
510
522
  type: 'object',
511
523
  properties: {
512
524
  [promptKey]: {
513
525
  type: 'string',
514
- description: handoffInputDescription as string,
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 handoff tool when no explicit
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 buildDefaultHandoffDescription(
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 processHandoffReception(
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 handoffDest = candidateMsg.additional_kwargs.handoff_destination;
611
- destinationAgent = typeof handoffDest === 'string' ? handoffDest : null;
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(HANDOFF_INSTRUCTIONS_PATTERN);
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 dynamic handoffs
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 handoffDestinations = new Set<string>();
891
- const directDestinations = new Set<string>();
1165
+ const transferDestinations = new Set<string>();
1166
+ const sequenceDestinations = new Set<string>();
892
1167
 
893
- // Check handoff edges for destinations
894
- for (const edge of this.handoffEdges) {
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) => handoffDestinations.add(dest));
1173
+ dests.forEach((dest) => transferDestinations.add(dest));
899
1174
  }
900
1175
  }
901
1176
 
902
- // Check direct edges for destinations
903
- for (const edge of this.directEdges) {
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) => directDestinations.add(dest));
1182
+ dests.forEach((dest) => sequenceDestinations.add(dest));
908
1183
  }
909
1184
  }
910
1185
 
911
- /** Check if this agent has BOTH handoff and direct edges */
912
- const hasHandoffEdges = handoffDestinations.size > 0;
913
- const hasDirectEdges = directDestinations.size > 0;
914
- const needsCommandRouting = hasHandoffEdges && hasDirectEdges;
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
- ...handoffDestinations,
919
- ...directDestinations,
1193
+ ...transferDestinations,
1194
+ ...sequenceDestinations,
920
1195
  ]);
921
- if (handoffDestinations.size > 0 || directDestinations.size === 0) {
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 handoff.
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 handoffContext = this.processHandoffReception(
1222
+ const transferContext = this.processTransferReception(
945
1223
  state.messages,
946
1224
  agentId
947
1225
  );
948
1226
 
949
- if (handoffContext !== null) {
1227
+ if (transferContext !== null) {
950
1228
  const {
951
1229
  filteredMessages,
952
1230
  instructions,
953
1231
  sourceAgentName,
954
1232
  parallelSiblings,
955
- } = handoffContext;
1233
+ } = transferContext;
956
1234
  console.debug(
957
- `[MultiAgentGraph] Agent "${agentId}" receiving handoff from "${sourceAgentName}" (instructions: ${instructions != null}, parallelSiblings: ${parallelSiblings.length})`
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 handoff and direct edges, use Command for exclusive routing */
1380
+ /** If agent has both transfer and sequence edges, use Command for exclusive routing */
1103
1381
  if (needsCommandRouting) {
1104
- /** Check if a handoff occurred */
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
- /** Handoff occurred - extract destination and navigate there exclusively */
1115
- const handoffDest = lastMessage.name.replace(
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}" -> handoff to "${handoffDest}" (direct edges skipped: [${Array.from(directDestinations).join(', ')}])`
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(handoffDest)) {
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] Handoff to non-existent agent "${handoffDest}". Available: ${availableAgents}`
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 "${handoffDest}" does not exist. Available agents: ${availableAgents}. Please choose a valid agent to transfer to.`,
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(handoffDest);
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 "${handoffDest}" budget (${receiverBudget} tokens)`
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: handoffDest,
1179
- agentChain: [agentId, handoffDest],
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: handoffDest,
1513
+ goto: transferDest,
1236
1514
  });
1237
1515
  } else {
1238
- /** No handoff - proceed with direct edges */
1516
+ /** No transfer - proceed with sequence edges */
1239
1517
  console.debug(
1240
- `[MultiAgentGraph] Command routing: "${agentId}" -> no handoff, following direct edges: [${Array.from(directDestinations).join(', ')}]`
1518
+ `[MultiAgentGraph] Command routing: "${agentId}" -> no transfer, following sequence edges: [${Array.from(sequenceDestinations).join(', ')}]`
1241
1519
  );
1242
- const directDests = Array.from(directDestinations);
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 direct edges for automatic transitions
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.directEdges) {
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 handoff and direct edges */
1381
- const sourceHandoffEdges = this.handoffEdges.filter((e) => {
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 sourceDirectEdges = this.directEdges.filter((e) => {
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 (sourceHandoffEdges.length > 0 && sourceDirectEdges.length > 0) {
1669
+ if (sourceTransferEdges.length > 0 && sourceSequenceEdges.length > 0) {
1392
1670
  continue;
1393
1671
  }
1394
1672