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