@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.
Files changed (64) hide show
  1. package/dist/cjs/common/enum.cjs +15 -13
  2. package/dist/cjs/common/enum.cjs.map +1 -1
  3. package/dist/cjs/graphs/MultiAgentGraph.cjs +173 -150
  4. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  5. package/dist/cjs/main.cjs +2 -2
  6. package/dist/cjs/types/graph.cjs.map +1 -1
  7. package/dist/esm/common/enum.mjs +14 -12
  8. package/dist/esm/common/enum.mjs.map +1 -1
  9. package/dist/esm/graphs/MultiAgentGraph.mjs +174 -151
  10. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  11. package/dist/esm/main.mjs +1 -1
  12. package/dist/esm/types/graph.mjs.map +1 -1
  13. package/dist/types/common/enum.d.ts +13 -11
  14. package/dist/types/graphs/MultiAgentGraph.d.ts +38 -36
  15. package/dist/types/types/graph.d.ts +22 -7
  16. package/package.json +1 -1
  17. package/src/common/__tests__/enum.test.ts +14 -6
  18. package/src/common/enum.ts +13 -11
  19. package/src/graphs/MultiAgentGraph.ts +190 -152
  20. package/src/graphs/__tests__/multi-agent-delegate.test.ts +44 -44
  21. package/src/graphs/__tests__/multi-agent-edges.test.ts +83 -85
  22. package/src/scripts/multi-agent-chain.js +1 -1
  23. package/src/scripts/multi-agent-chain.ts +1 -1
  24. package/src/scripts/multi-agent-document-review-chain.js +1 -1
  25. package/src/scripts/multi-agent-document-review-chain.ts +1 -1
  26. package/src/scripts/multi-agent-hybrid-flow.js +3 -3
  27. package/src/scripts/multi-agent-hybrid-flow.ts +3 -3
  28. package/src/scripts/multi-agent-parallel.js +2 -2
  29. package/src/scripts/multi-agent-parallel.ts +2 -2
  30. package/src/scripts/multi-agent-sequence.js +2 -2
  31. package/src/scripts/multi-agent-sequence.ts +2 -2
  32. package/src/scripts/multi-agent-supervisor.js +5 -5
  33. package/src/scripts/multi-agent-supervisor.ts +5 -5
  34. package/src/scripts/poc-multi-agent-comprehensive.ts +7 -7
  35. package/src/scripts/sequential-full-metadata-test.js +1 -1
  36. package/src/scripts/sequential-full-metadata-test.ts +1 -1
  37. package/src/scripts/test-custom-prompt-key.js +3 -3
  38. package/src/scripts/test-custom-prompt-key.ts +3 -3
  39. package/src/scripts/test-handoff-input.js +1 -1
  40. package/src/scripts/test-handoff-input.ts +1 -1
  41. package/src/scripts/test-handoff-preamble.js +1 -1
  42. package/src/scripts/test-handoff-preamble.ts +1 -1
  43. package/src/scripts/test-handoff-steering.js +3 -3
  44. package/src/scripts/test-handoff-steering.ts +3 -3
  45. package/src/scripts/test-multi-agent-list-handoff.js +1 -1
  46. package/src/scripts/test-multi-agent-list-handoff.ts +1 -1
  47. package/src/scripts/test-parallel-agent-labeling.js +2 -2
  48. package/src/scripts/test-parallel-agent-labeling.ts +2 -2
  49. package/src/scripts/test-parallel-handoffs.js +2 -2
  50. package/src/scripts/test-parallel-handoffs.ts +2 -2
  51. package/src/scripts/test-thinking-handoff-bedrock.js +1 -1
  52. package/src/scripts/test-thinking-handoff-bedrock.ts +1 -1
  53. package/src/scripts/test-thinking-handoff.js +1 -1
  54. package/src/scripts/test-thinking-handoff.ts +1 -1
  55. package/src/scripts/test-thinking-to-thinking-handoff-bedrock.js +1 -1
  56. package/src/scripts/test-thinking-to-thinking-handoff-bedrock.ts +1 -1
  57. package/src/scripts/test-tool-before-handoff-role-order.js +1 -1
  58. package/src/scripts/test-tool-before-handoff-role-order.ts +1 -1
  59. package/src/scripts/test-tools-before-handoff.js +1 -1
  60. package/src/scripts/test-tools-before-handoff.ts +1 -1
  61. package/src/specs/agent-handoffs-bedrock.integration.test.ts +6 -6
  62. package/src/specs/agent-handoffs.test.ts +35 -35
  63. package/src/specs/thinking-handoff.test.ts +9 -9
  64. package/src/types/graph.ts +23 -7
@@ -25,35 +25,37 @@ import { StandardGraph } from './Graph';
25
25
  import {
26
26
  Constants,
27
27
  EdgeType,
28
- DEFAULT_DELEGATE_MAX_RESULT_CHARS,
28
+ GraphEvents,
29
+ DEFAULT_HANDOFF_MAX_RESULT_CHARS,
29
30
  } from '@/common';
31
+ import { safeDispatchCustomEvent } from '@/utils/events';
30
32
 
31
33
  /** Pattern to extract instructions from transfer ToolMessage content */
32
- const HANDOFF_INSTRUCTIONS_PATTERN = /(?:Instructions?|Context):\s*(.+)/is;
34
+ const TRANSFER_INSTRUCTIONS_PATTERN = /(?:Instructions?|Context):\s*(.+)/is;
33
35
 
34
36
  /**
35
37
  * MultiAgentGraph extends StandardGraph to support dynamic multi-agent workflows
36
38
  * with handoffs, fan-in/fan-out, and other composable patterns.
37
39
  *
38
40
  * Key behavior:
39
- * - Agents with ONLY handoff edges: Can dynamically route to any handoff destination
40
- * - Agents with ONLY direct edges: Always follow their direct edges
41
- * - Agents with BOTH: Use Command for exclusive routing (handoff OR direct, not both)
42
- * - If handoff occurs: Only the handoff destination executes
43
- * - If no handoff: Direct edges execute (potentially in parallel)
41
+ * - Agents with ONLY transfer edges: Can dynamically route to any transfer destination
42
+ * - Agents with ONLY sequence edges: Always follow their sequence edges
43
+ * - Agents with BOTH: Use Command for exclusive routing (transfer OR sequence, not both)
44
+ * - If transfer occurs: Only the transfer destination executes
45
+ * - If no transfer: Sequence edges execute (potentially in parallel)
44
46
  *
45
- * This enables the common pattern where an agent either delegates (handoff)
46
- * OR continues its workflow (direct edges), but not both simultaneously.
47
+ * This enables the common pattern where an agent either transfers (one-way)
48
+ * OR continues its workflow (sequence edges), but not both simultaneously.
47
49
  */
48
50
  export class MultiAgentGraph extends StandardGraph {
49
51
  private edges: t.GraphEdge[];
50
52
  private startingNodes: Set<string> = new Set();
51
- private directEdges: t.GraphEdge[] = [];
53
+ private sequenceEdges: t.GraphEdge[] = [];
54
+ private transferEdges: t.GraphEdge[] = [];
52
55
  private handoffEdges: t.GraphEdge[] = [];
53
- private delegateEdges: t.GraphEdge[] = [];
54
56
  /**
55
57
  * Lazily populated registry of compiled subgraphs, keyed by agentId.
56
- * Delegate tools are created in the constructor but reference subgraphs
58
+ * Handoff tools are created in the constructor but reference subgraphs
57
59
  * that are only created in createWorkflow(). This Map bridges that gap —
58
60
  * tools capture the Map reference in their closure, and createWorkflow()
59
61
  * populates it before any tool invocation occurs.
@@ -81,42 +83,40 @@ export class MultiAgentGraph extends StandardGraph {
81
83
  this.edges = input.edges;
82
84
  this.categorizeEdges();
83
85
  this.analyzeGraph();
86
+ this.createTransferTools();
84
87
  this.createHandoffTools();
85
- this.createDelegateTools();
86
88
  console.debug(
87
89
  `[MultiAgentGraph] Constructor complete: ${this.agentContexts.size} agents, ${this.edges.length} edges`
88
90
  );
89
91
  }
90
92
 
91
93
  /**
92
- * Categorize edges into handoff and direct types
94
+ * Categorize edges into handoff, transfer, and sequence types
93
95
  */
94
96
  private categorizeEdges(): void {
95
97
  for (const edge of this.edges) {
96
- // Default behavior: edges with conditions or explicit 'handoff' type are handoff edges
97
- // Edges with explicit 'direct' type or multi-destination without conditions are direct edges
98
- if (edge.edgeType === EdgeType.DELEGATE) {
99
- this.delegateEdges.push(edge);
100
- } else if (edge.edgeType === EdgeType.DIRECT) {
101
- this.directEdges.push(edge);
102
- } else if (edge.edgeType === EdgeType.HANDOFF || edge.condition != null) {
98
+ if (edge.edgeType === EdgeType.HANDOFF) {
103
99
  this.handoffEdges.push(edge);
100
+ } else if (edge.edgeType === EdgeType.SEQUENCE) {
101
+ this.sequenceEdges.push(edge);
102
+ } else if (edge.edgeType === EdgeType.TRANSFER || edge.condition != null) {
103
+ this.transferEdges.push(edge);
104
104
  } else {
105
- // Default: single-to-single edges are handoff, single-to-multiple are direct
105
+ // Default: single-to-single edges are transfer, single-to-multiple are sequence
106
106
  const destinations = Array.isArray(edge.to) ? edge.to : [edge.to];
107
107
  const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
108
108
 
109
109
  if (sources.length === 1 && destinations.length > 1) {
110
- // Fan-out pattern defaults to direct
111
- this.directEdges.push(edge);
110
+ // Fan-out pattern defaults to sequence
111
+ this.sequenceEdges.push(edge);
112
112
  } else {
113
- // Everything else defaults to handoff
114
- this.handoffEdges.push(edge);
113
+ // Everything else defaults to transfer
114
+ this.transferEdges.push(edge);
115
115
  }
116
116
  }
117
117
  }
118
118
  console.debug(
119
- `[MultiAgentGraph] Edge categorization: ${this.handoffEdges.length} handoff, ${this.directEdges.length} direct, ${this.delegateEdges.length} delegate (of ${this.edges.length} total)`
119
+ `[MultiAgentGraph] Edge categorization: ${this.handoffEdges.length} handoff, ${this.transferEdges.length} transfer, ${this.sequenceEdges.length} sequence (of ${this.edges.length} total)`
120
120
  );
121
121
  }
122
122
 
@@ -187,8 +187,8 @@ export class MultiAgentGraph extends StandardGraph {
187
187
  if (visited.has(current)) continue;
188
188
  visited.add(current);
189
189
 
190
- // Find direct edges from this node
191
- for (const edge of this.directEdges) {
190
+ // Find sequence edges from this node
191
+ for (const edge of this.sequenceEdges) {
192
192
  const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
193
193
  if (!sources.includes(current)) continue;
194
194
 
@@ -216,8 +216,8 @@ export class MultiAgentGraph extends StandardGraph {
216
216
  }
217
217
  }
218
218
 
219
- // Also follow handoff and delegate edges for traversal (they don't create parallel groups)
220
- for (const edge of [...this.handoffEdges, ...this.delegateEdges]) {
219
+ // Also follow transfer and handoff edges for traversal (they don't create parallel groups)
220
+ for (const edge of [...this.transferEdges, ...this.handoffEdges]) {
221
221
  const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
222
222
  if (!sources.includes(current)) continue;
223
223
 
@@ -267,54 +267,52 @@ export class MultiAgentGraph extends StandardGraph {
267
267
  }
268
268
 
269
269
  /**
270
- * Create handoff tools for agents based on handoff edges only
270
+ * Create transfer tools for agents based on transfer edges only.
271
+ * Transfer tools return Command for one-way routing — parent exits, child takes over.
271
272
  */
272
- private createHandoffTools(): void {
273
- // Group handoff edges by source agent(s)
274
- const handoffsByAgent = new Map<string, t.GraphEdge[]>();
273
+ private createTransferTools(): void {
274
+ const transfersByAgent = new Map<string, t.GraphEdge[]>();
275
275
 
276
- // Only process handoff edges for tool creation
277
- for (const edge of this.handoffEdges) {
276
+ for (const edge of this.transferEdges) {
278
277
  const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
279
278
  sources.forEach((source) => {
280
- if (!handoffsByAgent.has(source)) {
281
- handoffsByAgent.set(source, []);
279
+ if (!transfersByAgent.has(source)) {
280
+ transfersByAgent.set(source, []);
282
281
  }
283
- handoffsByAgent.get(source)!.push(edge);
282
+ transfersByAgent.get(source)!.push(edge);
284
283
  });
285
284
  }
286
285
 
287
- // Create handoff tools for each agent
288
- for (const [agentId, edges] of handoffsByAgent) {
286
+ for (const [agentId, edges] of transfersByAgent) {
289
287
  const agentContext = this.agentContexts.get(agentId);
290
288
  if (!agentContext) continue;
291
289
 
292
- // Create handoff tools for this agent's outgoing edges
293
- const handoffTools: t.GenericTool[] = [];
290
+ const transferTools: t.GenericTool[] = [];
294
291
  const sourceAgentName = agentContext.name ?? agentId;
295
292
  for (const edge of edges) {
296
- handoffTools.push(
297
- ...this.createHandoffToolsForEdge(edge, agentId, sourceAgentName)
293
+ transferTools.push(
294
+ ...this.createTransferToolsForEdge(edge, agentId, sourceAgentName)
298
295
  );
299
296
  }
300
297
 
301
298
  if (!agentContext.graphTools) {
302
299
  agentContext.graphTools = [];
303
300
  }
304
- agentContext.graphTools.push(...handoffTools);
301
+ agentContext.graphTools.push(...transferTools);
305
302
  console.debug(
306
- `[MultiAgentGraph] Handoff tools for "${agentId}": [${handoffTools.map((t) => t.name).join(', ')}]`
303
+ `[MultiAgentGraph] Transfer tools for "${agentId}": [${transferTools.map((t) => t.name).join(', ')}]`
307
304
  );
308
305
  }
309
306
  }
310
307
 
311
308
  /**
312
- * Create handoff tools for an edge (handles multiple destinations)
313
- * @param edge - The graph edge defining the handoff
314
- * @param sourceAgentId - The ID of the agent that will perform the handoff
309
+ * Create transfer tools for an edge (handles multiple destinations).
310
+ * Transfer tools return Command for one-way routing parent exits, child takes over.
311
+ * @param edge - The graph edge defining the transfer
312
+ * @param sourceAgentId - The ID of the agent that will perform the transfer
315
313
  * @param sourceAgentName - The human-readable name of the source agent
316
314
  */
317
- private createHandoffToolsForEdge(
315
+ private createTransferToolsForEdge(
318
316
  edge: t.GraphEdge,
319
317
  sourceAgentId: string,
320
318
  sourceAgentName: string
@@ -329,9 +327,9 @@ export class MultiAgentGraph extends StandardGraph {
329
327
  edge.description ?? 'Conditionally transfer control based on state';
330
328
 
331
329
  /** Check if we have a prompt for handoff input */
332
- const hasHandoffInput =
330
+ const hasTransferInput =
333
331
  edge.prompt != null && typeof edge.prompt === 'string';
334
- const handoffInputDescription = hasHandoffInput ? edge.prompt : undefined;
332
+ const transferInputDescription = hasTransferInput ? edge.prompt : undefined;
335
333
  const promptKey = edge.promptKey ?? 'instructions';
336
334
 
337
335
  tools.push(
@@ -360,7 +358,7 @@ export class MultiAgentGraph extends StandardGraph {
360
358
 
361
359
  let content = `Conditionally transferred to ${destination}`;
362
360
  if (
363
- hasHandoffInput &&
361
+ hasTransferInput &&
364
362
  promptKey in input &&
365
363
  input[promptKey] != null
366
364
  ) {
@@ -387,13 +385,13 @@ export class MultiAgentGraph extends StandardGraph {
387
385
  },
388
386
  {
389
387
  name: toolName,
390
- schema: hasHandoffInput
388
+ schema: hasTransferInput
391
389
  ? {
392
390
  type: 'object',
393
391
  properties: {
394
392
  [promptKey]: {
395
393
  type: 'string',
396
- description: handoffInputDescription as string,
394
+ description: transferInputDescription as string,
397
395
  },
398
396
  },
399
397
  required: [],
@@ -410,12 +408,12 @@ export class MultiAgentGraph extends StandardGraph {
410
408
  const destContext = this.agentContexts.get(destination);
411
409
  const toolDescription =
412
410
  edge.description ??
413
- this.buildDefaultHandoffDescription(destContext, destination);
411
+ this.buildDefaultTransferDescription(destContext, destination);
414
412
 
415
413
  /** Check if we have a prompt for handoff input */
416
- const hasHandoffInput =
414
+ const hasTransferInput =
417
415
  edge.prompt != null && typeof edge.prompt === 'string';
418
- const handoffInputDescription = hasHandoffInput
416
+ const transferInputDescription = hasTransferInput
419
417
  ? edge.prompt
420
418
  : undefined;
421
419
  const promptKey = edge.promptKey ?? 'instructions';
@@ -430,7 +428,7 @@ export class MultiAgentGraph extends StandardGraph {
430
428
 
431
429
  let content = `Successfully transferred to ${destination}`;
432
430
  if (
433
- hasHandoffInput &&
431
+ hasTransferInput &&
434
432
  promptKey in input &&
435
433
  input[promptKey] != null
436
434
  ) {
@@ -521,13 +519,13 @@ export class MultiAgentGraph extends StandardGraph {
521
519
  },
522
520
  {
523
521
  name: toolName,
524
- schema: hasHandoffInput
522
+ schema: hasTransferInput
525
523
  ? {
526
524
  type: 'object',
527
525
  properties: {
528
526
  [promptKey]: {
529
527
  type: 'string',
530
- description: handoffInputDescription as string,
528
+ description: transferInputDescription as string,
531
529
  },
532
530
  },
533
531
  required: [],
@@ -544,13 +542,13 @@ export class MultiAgentGraph extends StandardGraph {
544
542
  }
545
543
 
546
544
  /**
547
- * Builds a meaningful default description for a handoff tool when no explicit
545
+ * Builds a meaningful default description for a transfer tool when no explicit
548
546
  * edge.description is provided. Uses the destination agent's name and description
549
547
  * so the LLM can make informed routing decisions.
550
548
  * @param destContext - AgentContext of the destination agent (may be undefined)
551
549
  * @param destinationId - Raw agent ID (fallback when context unavailable)
552
550
  */
553
- private buildDefaultHandoffDescription(
551
+ private buildDefaultTransferDescription(
554
552
  destContext: import('@/agents/AgentContext').AgentContext | undefined,
555
553
  destinationId: string
556
554
  ): string {
@@ -564,72 +562,72 @@ export class MultiAgentGraph extends StandardGraph {
564
562
  }
565
563
 
566
564
  /**
567
- * Create delegate tools for agents based on delegate edges.
568
- * Delegate tools invoke child agent subgraphs inline and return the result
569
- * as a string to the parent agent's context. Unlike handoff tools (which
570
- * return Command for fire-and-forget routing), delegate tools execute the
571
- * child, extract the final text, and return it within the parent's agent loop.
565
+ * Create handoff tools for agents based on handoff edges.
566
+ * Handoff tools invoke child agent subgraphs inline and return the result
567
+ * as a string to the parent agent's context. Unlike transfer tools (which
568
+ * return Command for one-way routing), handoff tools execute the child,
569
+ * extract the final text, and return it within the parent's agent loop.
572
570
  *
573
571
  * This enables the supervisor pattern: parent calls child → gets result → thinks → calls another.
574
572
  */
575
- private createDelegateTools(): void {
576
- const delegatesByAgent = new Map<string, t.GraphEdge[]>();
573
+ private createHandoffTools(): void {
574
+ const handoffsByAgent = new Map<string, t.GraphEdge[]>();
577
575
 
578
- for (const edge of this.delegateEdges) {
576
+ for (const edge of this.handoffEdges) {
579
577
  const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
580
578
  sources.forEach((source) => {
581
- if (!delegatesByAgent.has(source)) {
582
- delegatesByAgent.set(source, []);
579
+ if (!handoffsByAgent.has(source)) {
580
+ handoffsByAgent.set(source, []);
583
581
  }
584
- delegatesByAgent.get(source)!.push(edge);
582
+ handoffsByAgent.get(source)!.push(edge);
585
583
  });
586
584
  }
587
585
 
588
- for (const [agentId, edges] of delegatesByAgent) {
586
+ for (const [agentId, edges] of handoffsByAgent) {
589
587
  const agentContext = this.agentContexts.get(agentId);
590
588
  if (!agentContext) continue;
591
589
 
592
- const delegateTools: t.GenericTool[] = [];
590
+ const handoffTools: t.GenericTool[] = [];
593
591
  for (const edge of edges) {
594
- delegateTools.push(
595
- ...this.createDelegateToolsForEdge(edge, agentId)
592
+ handoffTools.push(
593
+ ...this.createHandoffToolsForEdge(edge, agentId)
596
594
  );
597
595
  }
598
596
 
599
597
  if (!agentContext.graphTools) {
600
598
  agentContext.graphTools = [];
601
599
  }
602
- agentContext.graphTools.push(...delegateTools);
600
+ agentContext.graphTools.push(...handoffTools);
603
601
  console.debug(
604
- `[MultiAgentGraph] Delegate tools for "${agentId}": [${delegateTools.map((t) => t.name).join(', ')}]`
602
+ `[MultiAgentGraph] Handoff tools for "${agentId}": [${handoffTools.map((t) => t.name).join(', ')}]`
605
603
  );
606
604
  }
607
605
  }
608
606
 
609
607
  /**
610
- * Create delegate tools for an edge (handles multiple destinations).
611
- * Each delegate tool invokes the child agent's compiled subgraph inline,
608
+ * Create handoff tools for an edge (handles multiple destinations).
609
+ * Each handoff tool invokes the child agent's compiled subgraph inline,
612
610
  * extracts the final AI message text, truncates it, and returns it as
613
611
  * a string (which becomes a ToolMessage in the parent's context).
614
612
  *
615
- * @param edge - The graph edge defining the delegation
613
+ * @param edge - The graph edge defining the handoff
616
614
  * @param sourceAgentId - The ID of the parent/supervisor agent
617
615
  */
618
- private createDelegateToolsForEdge(
616
+ private createHandoffToolsForEdge(
619
617
  edge: t.GraphEdge,
620
618
  sourceAgentId: string
621
619
  ): t.GenericTool[] {
622
620
  const tools: t.GenericTool[] = [];
623
621
  const destinations = Array.isArray(edge.to) ? edge.to : [edge.to];
624
622
  const maxResultChars =
625
- edge.maxResultChars ?? DEFAULT_DELEGATE_MAX_RESULT_CHARS;
623
+ edge.maxResultChars ?? DEFAULT_HANDOFF_MAX_RESULT_CHARS;
626
624
 
627
625
  for (const destination of destinations) {
628
- const toolName = `${Constants.LC_DELEGATE_TO_}${destination}`;
626
+ const toolName = `${Constants.LC_HANDOFF_TO_}${destination}`;
629
627
  const destContext = this.agentContexts.get(destination);
630
628
  const toolDescription =
631
629
  edge.description ??
632
- this.buildDefaultDelegateDescription(destContext, destination);
630
+ this.buildDefaultHandoffDescription(destContext, destination);
633
631
 
634
632
  const hasPromptInput =
635
633
  edge.prompt != null && typeof edge.prompt === 'string';
@@ -646,7 +644,7 @@ export class MultiAgentGraph extends StandardGraph {
646
644
  const subgraph = registry.get(destination);
647
645
  if (!subgraph) {
648
646
  throw new Error(
649
- `Delegate target "${destination}" subgraph not found in registry. ` +
647
+ `Handoff target "${destination}" subgraph not found in registry. ` +
650
648
  'This is a bug: createWorkflow() should have populated the subgraph registry.'
651
649
  );
652
650
  }
@@ -671,7 +669,7 @@ export class MultiAgentGraph extends StandardGraph {
671
669
  };
672
670
 
673
671
  console.debug(
674
- `[MultiAgentGraph] Delegate "${sourceAgentId}" -> "${destination}" START ` +
672
+ `[MultiAgentGraph] Handoff "${sourceAgentId}" -> "${destination}" START ` +
675
673
  `(messages: ${childMessages.length})`
676
674
  );
677
675
 
@@ -683,30 +681,43 @@ export class MultiAgentGraph extends StandardGraph {
683
681
  */
684
682
  const result = await subgraph.invoke(childState, config);
685
683
 
686
- const resultText = MultiAgentGraph.extractDelegateResult(
684
+ const resultText = MultiAgentGraph.extractHandoffResult(
687
685
  result.messages,
688
686
  destination
689
687
  );
690
- const truncatedResult = MultiAgentGraph.truncateDelegateResult(
688
+ const truncatedResult = MultiAgentGraph.truncateHandoffResult(
691
689
  resultText,
692
690
  maxResultChars
693
691
  );
694
692
 
695
693
  console.debug(
696
- `[MultiAgentGraph] Delegate "${sourceAgentId}" -> "${destination}" DONE ` +
694
+ `[MultiAgentGraph] Handoff "${sourceAgentId}" -> "${destination}" DONE ` +
697
695
  `(result: ${resultText.length} chars` +
698
696
  `${truncatedResult.length < resultText.length ? `, truncated to ${truncatedResult.length}` : ''})`
699
697
  );
700
698
 
699
+ await safeDispatchCustomEvent(
700
+ GraphEvents.ON_AGENT_TRANSITION,
701
+ {
702
+ sourceAgentId: sourceAgentId,
703
+ sourceAgentName: this.agentContexts.get(sourceAgentId)?.name ?? sourceAgentId,
704
+ destinationAgentId: destination,
705
+ destinationAgentName: destContext?.name ?? destination,
706
+ edgeType: EdgeType.HANDOFF,
707
+ timestamp: Date.now(),
708
+ },
709
+ config
710
+ );
711
+
701
712
  return truncatedResult;
702
713
  } catch (err) {
703
714
  const errorMessage =
704
715
  err instanceof Error ? err.message : String(err);
705
716
  console.error(
706
- `[MultiAgentGraph] Delegate "${sourceAgentId}" -> "${destination}" ERROR:`,
717
+ `[MultiAgentGraph] Handoff "${sourceAgentId}" -> "${destination}" ERROR:`,
707
718
  errorMessage
708
719
  );
709
- return `[Delegate to "${destination}" failed: ${errorMessage}]`;
720
+ return `[Handoff to "${destination}" failed: ${errorMessage}]`;
710
721
  }
711
722
  },
712
723
  {
@@ -739,7 +750,7 @@ export class MultiAgentGraph extends StandardGraph {
739
750
  * @param messages - The child agent's output messages
740
751
  * @param agentId - The child agent ID (for fallback message)
741
752
  */
742
- static extractDelegateResult(
753
+ static extractHandoffResult(
743
754
  messages: BaseMessage[],
744
755
  agentId: string
745
756
  ): string {
@@ -780,19 +791,19 @@ export class MultiAgentGraph extends StandardGraph {
780
791
  }
781
792
 
782
793
  /**
783
- * Truncate delegate result using head/tail strategy (60/40 split).
794
+ * Truncate handoff result using head/tail strategy (60/40 split).
784
795
  * Preserves the beginning (key findings) and end (conclusions).
785
796
  * Matches the TaskTool.truncateResult pattern from Ranger.
786
797
  * @param result - The full result text
787
798
  * @param maxChars - Maximum allowed characters
788
799
  */
789
- static truncateDelegateResult(result: string, maxChars: number): string {
800
+ static truncateHandoffResult(result: string, maxChars: number): string {
790
801
  if (!result || result.length <= maxChars) {
791
802
  return result;
792
803
  }
793
804
 
794
805
  const truncationNotice =
795
- '\n\n[... delegate output truncated — middle section omitted to fit parent context ...]\n\n';
806
+ '\n\n[... handoff output truncated — middle section omitted to fit parent context ...]\n\n';
796
807
  const available = maxChars - truncationNotice.length;
797
808
  if (available <= 0) {
798
809
  return result.substring(0, maxChars);
@@ -809,11 +820,11 @@ export class MultiAgentGraph extends StandardGraph {
809
820
  }
810
821
 
811
822
  /**
812
- * Build a meaningful default description for a delegate tool.
823
+ * Build a meaningful default description for a handoff tool.
813
824
  * @param destContext - AgentContext of the destination agent
814
825
  * @param destinationId - Raw agent ID (fallback)
815
826
  */
816
- private buildDefaultDelegateDescription(
827
+ private buildDefaultHandoffDescription(
817
828
  destContext: import('@/agents/AgentContext').AgentContext | undefined,
818
829
  destinationId: string
819
830
  ): string {
@@ -821,9 +832,9 @@ export class MultiAgentGraph extends StandardGraph {
821
832
  const agentDescription = destContext?.description;
822
833
 
823
834
  if (agentDescription != null && agentDescription !== '') {
824
- return `Delegate task to "${displayName}": ${agentDescription}. The agent will execute and return its result.`;
835
+ return `Hand off task to "${displayName}": ${agentDescription}. The agent will execute and return its result.`;
825
836
  }
826
- return `Delegate task to "${displayName}" and receive its result.`;
837
+ return `Hand off task to "${displayName}" and receive its result.`;
827
838
  }
828
839
 
829
840
  /**
@@ -846,7 +857,7 @@ export class MultiAgentGraph extends StandardGraph {
846
857
  * @param agentId - The agent ID to check for handoff reception
847
858
  * @returns Object with filtered messages, extracted instructions, source agent, and parallel siblings
848
859
  */
849
- private processHandoffReception(
860
+ private processTransferReception(
850
861
  messages: BaseMessage[],
851
862
  agentId: string
852
863
  ): {
@@ -886,8 +897,8 @@ export class MultiAgentGraph extends StandardGraph {
886
897
  if (isTransferMessage) {
887
898
  destinationAgent = toolName.replace(Constants.LC_TRANSFER_TO_, '');
888
899
  } else if (isConditionalTransfer) {
889
- const handoffDest = candidateMsg.additional_kwargs.handoff_destination;
890
- destinationAgent = typeof handoffDest === 'string' ? handoffDest : null;
900
+ const transferDest = candidateMsg.additional_kwargs.handoff_destination;
901
+ destinationAgent = typeof transferDest === 'string' ? transferDest : null;
891
902
  }
892
903
 
893
904
  /** Check if this transfer targets our agent */
@@ -907,7 +918,7 @@ export class MultiAgentGraph extends StandardGraph {
907
918
  ? toolMessage.content
908
919
  : JSON.stringify(toolMessage.content);
909
920
 
910
- const instructionsMatch = contentStr.match(HANDOFF_INSTRUCTIONS_PATTERN);
921
+ const instructionsMatch = contentStr.match(TRANSFER_INSTRUCTIONS_PATTERN);
911
922
  const instructions = instructionsMatch?.[1]?.trim() ?? null;
912
923
 
913
924
  /** Extract source agent name from additional_kwargs */
@@ -1138,7 +1149,7 @@ export class MultiAgentGraph extends StandardGraph {
1138
1149
  }
1139
1150
 
1140
1151
  /**
1141
- * Create the multi-agent workflow with dynamic handoffs
1152
+ * Create the multi-agent workflow with handoffs, transfers, and sequences
1142
1153
  */
1143
1154
  override createWorkflow(): t.CompiledMultiAgentWorkflow {
1144
1155
  const StateAnnotation = Annotation.Root({
@@ -1166,45 +1177,45 @@ export class MultiAgentGraph extends StandardGraph {
1166
1177
  // Add all agents as complete subgraphs
1167
1178
  for (const [agentId] of this.agentContexts) {
1168
1179
  // Get all possible destinations for this agent
1169
- const handoffDestinations = new Set<string>();
1170
- const directDestinations = new Set<string>();
1180
+ const transferDestinations = new Set<string>();
1181
+ const sequenceDestinations = new Set<string>();
1171
1182
 
1172
- // Check handoff edges for destinations
1173
- for (const edge of this.handoffEdges) {
1183
+ // Check transfer edges for destinations
1184
+ for (const edge of this.transferEdges) {
1174
1185
  const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
1175
1186
  if (sources.includes(agentId) === true) {
1176
1187
  const dests = Array.isArray(edge.to) ? edge.to : [edge.to];
1177
- dests.forEach((dest) => handoffDestinations.add(dest));
1188
+ dests.forEach((dest) => transferDestinations.add(dest));
1178
1189
  }
1179
1190
  }
1180
1191
 
1181
- // Check direct edges for destinations
1182
- for (const edge of this.directEdges) {
1192
+ // Check sequence edges for destinations
1193
+ for (const edge of this.sequenceEdges) {
1183
1194
  const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
1184
1195
  if (sources.includes(agentId) === true) {
1185
1196
  const dests = Array.isArray(edge.to) ? edge.to : [edge.to];
1186
- dests.forEach((dest) => directDestinations.add(dest));
1197
+ dests.forEach((dest) => sequenceDestinations.add(dest));
1187
1198
  }
1188
1199
  }
1189
1200
 
1190
- /** Check if this agent has BOTH handoff and direct edges */
1191
- const hasHandoffEdges = handoffDestinations.size > 0;
1192
- const hasDirectEdges = directDestinations.size > 0;
1193
- const needsCommandRouting = hasHandoffEdges && hasDirectEdges;
1201
+ /** Check if this agent has BOTH transfer and sequence edges */
1202
+ const hasTransferEdges = transferDestinations.size > 0;
1203
+ const hasSequenceEdges = sequenceDestinations.size > 0;
1204
+ const needsCommandRouting = hasTransferEdges && hasSequenceEdges;
1194
1205
 
1195
1206
  /** Collect all possible destinations for this agent */
1196
1207
  const allDestinations = new Set([
1197
- ...handoffDestinations,
1198
- ...directDestinations,
1208
+ ...transferDestinations,
1209
+ ...sequenceDestinations,
1199
1210
  ]);
1200
- if (handoffDestinations.size > 0 || directDestinations.size === 0) {
1211
+ if (transferDestinations.size > 0 || sequenceDestinations.size === 0) {
1201
1212
  allDestinations.add(END);
1202
1213
  }
1203
1214
 
1204
1215
  /** Agent subgraph (includes agent + tools) */
1205
1216
  const agentSubgraph = this.createAgentSubgraph(agentId);
1206
1217
 
1207
- /** Register subgraph for delegate tools (lazy reference resolution) */
1218
+ /** Register subgraph for handoff tools (lazy reference resolution) */
1208
1219
  this.subgraphRegistry.set(agentId, agentSubgraph);
1209
1220
 
1210
1221
  /** Wrapper function that handles agentMessages channel, handoff reception, and conditional routing */
@@ -1218,25 +1229,25 @@ export class MultiAgentGraph extends StandardGraph {
1218
1229
  let result: t.MultiAgentGraphState;
1219
1230
 
1220
1231
  /**
1221
- * Check if this agent is receiving a handoff.
1232
+ * Check if this agent is receiving a transfer.
1222
1233
  * If so, filter out the transfer messages and inject instructions as preamble.
1223
1234
  * This prevents the receiving agent from seeing the transfer as "completed work"
1224
1235
  * and prematurely producing an end token.
1225
1236
  */
1226
- const handoffContext = this.processHandoffReception(
1237
+ const transferContext = this.processTransferReception(
1227
1238
  state.messages,
1228
1239
  agentId
1229
1240
  );
1230
1241
 
1231
- if (handoffContext !== null) {
1242
+ if (transferContext !== null) {
1232
1243
  const {
1233
1244
  filteredMessages,
1234
1245
  instructions,
1235
1246
  sourceAgentName,
1236
1247
  parallelSiblings,
1237
- } = handoffContext;
1248
+ } = transferContext;
1238
1249
  console.debug(
1239
- `[MultiAgentGraph] Agent "${agentId}" receiving handoff from "${sourceAgentName}" (instructions: ${instructions != null}, parallelSiblings: ${parallelSiblings.length})`
1250
+ `[MultiAgentGraph] Agent "${agentId}" receiving transfer from "${sourceAgentName}" (instructions: ${instructions != null}, parallelSiblings: ${parallelSiblings.length})`
1240
1251
  );
1241
1252
 
1242
1253
  /**
@@ -1381,9 +1392,9 @@ export class MultiAgentGraph extends StandardGraph {
1381
1392
  `[MultiAgentGraph] Agent "${agentId}" wrapper EXIT (result messages: ${result.messages.length})`
1382
1393
  );
1383
1394
 
1384
- /** If agent has both handoff and direct edges, use Command for exclusive routing */
1395
+ /** If agent has both transfer and sequence edges, use Command for exclusive routing */
1385
1396
  if (needsCommandRouting) {
1386
- /** Check if a handoff occurred */
1397
+ /** Check if a transfer occurred */
1387
1398
  const lastMessage = result.messages[
1388
1399
  result.messages.length - 1
1389
1400
  ] as BaseMessage | null;
@@ -1393,26 +1404,26 @@ export class MultiAgentGraph extends StandardGraph {
1393
1404
  typeof lastMessage.name === 'string' &&
1394
1405
  lastMessage.name.startsWith(Constants.LC_TRANSFER_TO_)
1395
1406
  ) {
1396
- /** Handoff occurred - extract destination and navigate there exclusively */
1397
- const handoffDest = lastMessage.name.replace(
1407
+ /** Transfer occurred - extract destination and navigate there exclusively */
1408
+ const transferDest = lastMessage.name.replace(
1398
1409
  Constants.LC_TRANSFER_TO_,
1399
1410
  ''
1400
1411
  );
1401
1412
  console.debug(
1402
- `[MultiAgentGraph] Command routing: "${agentId}" -> handoff to "${handoffDest}" (direct edges skipped: [${Array.from(directDestinations).join(', ')}])`
1413
+ `[MultiAgentGraph] Command routing: "${agentId}" -> transfer to "${transferDest}" (sequence edges skipped: [${Array.from(sequenceDestinations).join(', ')}])`
1403
1414
  );
1404
1415
 
1405
1416
  /** Validate destination agent exists */
1406
- if (!this.agentContexts.has(handoffDest)) {
1417
+ if (!this.agentContexts.has(transferDest)) {
1407
1418
  const availableAgents = Array.from(
1408
1419
  this.agentContexts.keys()
1409
1420
  ).join(', ');
1410
1421
  console.error(
1411
- `[MultiAgentGraph] Handoff to non-existent agent "${handoffDest}". Available: ${availableAgents}`
1422
+ `[MultiAgentGraph] Transfer to non-existent agent "${transferDest}". Available: ${availableAgents}`
1412
1423
  );
1413
1424
  /** Return error to model so it can self-correct */
1414
1425
  const errorMsg = new ToolMessage({
1415
- content: `Transfer failed: agent "${handoffDest}" does not exist. Available agents: ${availableAgents}. Please choose a valid agent to transfer to.`,
1426
+ content: `Transfer failed: agent "${transferDest}" does not exist. Available agents: ${availableAgents}. Please choose a valid agent to transfer to.`,
1416
1427
  tool_call_id: (lastMessage as ToolMessage).tool_call_id,
1417
1428
  name: lastMessage.name,
1418
1429
  });
@@ -1423,7 +1434,7 @@ export class MultiAgentGraph extends StandardGraph {
1423
1434
  }
1424
1435
 
1425
1436
  /** Pre-handoff context compaction: if receiving agent has smaller budget */
1426
- const receiverContext = this.agentContexts.get(handoffDest);
1437
+ const receiverContext = this.agentContexts.get(transferDest);
1427
1438
  const senderContext = this.agentContexts.get(agentId);
1428
1439
  if (
1429
1440
  receiverContext?.maxContextTokens != null &&
@@ -1439,7 +1450,7 @@ export class MultiAgentGraph extends StandardGraph {
1439
1450
  if (currentSize > receiverBudget * 0.7) {
1440
1451
  console.warn(
1441
1452
  `[MultiAgentGraph] Pre-handoff compaction: context (${currentSize} tokens) exceeds ` +
1442
- `70% of receiver "${handoffDest}" budget (${receiverBudget} tokens)`
1453
+ `70% of receiver "${transferDest}" budget (${receiverBudget} tokens)`
1443
1454
  );
1444
1455
 
1445
1456
  /** Generate handoff briefing */
@@ -1457,8 +1468,8 @@ export class MultiAgentGraph extends StandardGraph {
1457
1468
  summaryBudget: Math.floor(receiverBudget * 0.2),
1458
1469
  isMultiAgent: true,
1459
1470
  agentWorkflowState: {
1460
- currentAgentId: handoffDest,
1461
- agentChain: [agentId, handoffDest],
1471
+ currentAgentId: transferDest,
1472
+ agentChain: [agentId, transferDest],
1462
1473
  pendingAgents: [],
1463
1474
  },
1464
1475
  }
@@ -1512,16 +1523,43 @@ export class MultiAgentGraph extends StandardGraph {
1512
1523
  }
1513
1524
  }
1514
1525
 
1526
+ await safeDispatchCustomEvent(
1527
+ GraphEvents.ON_AGENT_TRANSITION,
1528
+ {
1529
+ sourceAgentId: agentId,
1530
+ sourceAgentName: this.agentContexts.get(agentId)?.name ?? agentId,
1531
+ destinationAgentId: transferDest,
1532
+ destinationAgentName: this.agentContexts.get(transferDest)?.name ?? transferDest,
1533
+ edgeType: EdgeType.TRANSFER,
1534
+ timestamp: Date.now(),
1535
+ },
1536
+ config
1537
+ );
1538
+
1515
1539
  return new Command({
1516
1540
  update: result,
1517
- goto: handoffDest,
1541
+ goto: transferDest,
1518
1542
  });
1519
1543
  } else {
1520
- /** No handoff - proceed with direct edges */
1544
+ /** No transfer - proceed with sequence edges */
1521
1545
  console.debug(
1522
- `[MultiAgentGraph] Command routing: "${agentId}" -> no handoff, following direct edges: [${Array.from(directDestinations).join(', ')}]`
1546
+ `[MultiAgentGraph] Command routing: "${agentId}" -> no transfer, following sequence edges: [${Array.from(sequenceDestinations).join(', ')}]`
1523
1547
  );
1524
- const directDests = Array.from(directDestinations);
1548
+ const directDests = Array.from(sequenceDestinations);
1549
+ for (const dest of directDests) {
1550
+ await safeDispatchCustomEvent(
1551
+ GraphEvents.ON_AGENT_TRANSITION,
1552
+ {
1553
+ sourceAgentId: agentId,
1554
+ sourceAgentName: this.agentContexts.get(agentId)?.name ?? agentId,
1555
+ destinationAgentId: dest,
1556
+ destinationAgentName: this.agentContexts.get(dest)?.name ?? dest,
1557
+ edgeType: EdgeType.SEQUENCE,
1558
+ timestamp: Date.now(),
1559
+ },
1560
+ config
1561
+ );
1562
+ }
1525
1563
  if (directDests.length === 1) {
1526
1564
  return new Command({
1527
1565
  update: result,
@@ -1555,12 +1593,12 @@ export class MultiAgentGraph extends StandardGraph {
1555
1593
  }
1556
1594
 
1557
1595
  /**
1558
- * Add direct edges for automatic transitions
1596
+ * Add sequence edges for automatic transitions
1559
1597
  * Group edges by destination to handle fan-in scenarios
1560
1598
  */
1561
1599
  const edgesByDestination = new Map<string, t.GraphEdge[]>();
1562
1600
 
1563
- for (const edge of this.directEdges) {
1601
+ for (const edge of this.sequenceEdges) {
1564
1602
  const destinations = Array.isArray(edge.to) ? edge.to : [edge.to];
1565
1603
  for (const destination of destinations) {
1566
1604
  if (!edgesByDestination.has(destination)) {
@@ -1659,18 +1697,18 @@ export class MultiAgentGraph extends StandardGraph {
1659
1697
  for (const edge of edges) {
1660
1698
  const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
1661
1699
  for (const source of sources) {
1662
- /** Check if this source node has both handoff and direct edges */
1663
- const sourceHandoffEdges = this.handoffEdges.filter((e) => {
1700
+ /** Check if this source node has both transfer and sequence edges */
1701
+ const sourceTransferEdges = this.transferEdges.filter((e) => {
1664
1702
  const eSources = Array.isArray(e.from) ? e.from : [e.from];
1665
1703
  return eSources.includes(source);
1666
1704
  });
1667
- const sourceDirectEdges = this.directEdges.filter((e) => {
1705
+ const sourceSequenceEdges = this.sequenceEdges.filter((e) => {
1668
1706
  const eSources = Array.isArray(e.from) ? e.from : [e.from];
1669
1707
  return eSources.includes(source);
1670
1708
  });
1671
1709
 
1672
1710
  /** Skip adding edge if source uses Command routing (has both types) */
1673
- if (sourceHandoffEdges.length > 0 && sourceDirectEdges.length > 0) {
1711
+ if (sourceTransferEdges.length > 0 && sourceSequenceEdges.length > 0) {
1674
1712
  continue;
1675
1713
  }
1676
1714