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