@illuma-ai/agents 1.1.25 → 1.1.28

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 (45) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +20 -3
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/graphs/Graph.cjs +5 -0
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/graphs/HandoffRegistry.cjs +104 -0
  6. package/dist/cjs/graphs/HandoffRegistry.cjs.map +1 -0
  7. package/dist/cjs/graphs/MultiAgentGraph.cjs +224 -47
  8. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  9. package/dist/cjs/main.cjs +2 -0
  10. package/dist/cjs/main.cjs.map +1 -1
  11. package/dist/cjs/stream.cjs +4 -4
  12. package/dist/cjs/stream.cjs.map +1 -1
  13. package/dist/cjs/types/graph.cjs.map +1 -1
  14. package/dist/cjs/utils/events.cjs +3 -0
  15. package/dist/cjs/utils/events.cjs.map +1 -1
  16. package/dist/esm/agents/AgentContext.mjs +20 -3
  17. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  18. package/dist/esm/graphs/Graph.mjs +5 -0
  19. package/dist/esm/graphs/Graph.mjs.map +1 -1
  20. package/dist/esm/graphs/HandoffRegistry.mjs +102 -0
  21. package/dist/esm/graphs/HandoffRegistry.mjs.map +1 -0
  22. package/dist/esm/graphs/MultiAgentGraph.mjs +224 -47
  23. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  24. package/dist/esm/main.mjs +1 -0
  25. package/dist/esm/main.mjs.map +1 -1
  26. package/dist/esm/stream.mjs +4 -4
  27. package/dist/esm/stream.mjs.map +1 -1
  28. package/dist/esm/types/graph.mjs.map +1 -1
  29. package/dist/esm/utils/events.mjs +3 -0
  30. package/dist/esm/utils/events.mjs.map +1 -1
  31. package/dist/types/graphs/HandoffRegistry.d.ts +80 -0
  32. package/dist/types/graphs/MultiAgentGraph.d.ts +23 -3
  33. package/dist/types/graphs/index.d.ts +1 -0
  34. package/dist/types/types/graph.d.ts +6 -0
  35. package/package.json +1 -1
  36. package/src/agents/AgentContext.ts +20 -5
  37. package/src/graphs/Graph.ts +8 -0
  38. package/src/graphs/HandoffRegistry.ts +168 -0
  39. package/src/graphs/MultiAgentGraph.ts +274 -67
  40. package/src/graphs/__tests__/HandoffRegistry.test.ts +407 -0
  41. package/src/graphs/index.ts +1 -0
  42. package/src/stream.ts +4 -6
  43. package/src/tools/approval/__tests__/constants.test.ts +3 -3
  44. package/src/types/graph.ts +6 -0
  45. package/src/utils/events.ts +3 -0
@@ -0,0 +1,104 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Registry for async handoff execution.
5
+ *
6
+ * Enables the OpenClaw-style autonomous orchestration pattern:
7
+ * 1. Orchestrator spawns children (non-blocking)
8
+ * 2. Orchestrator stays alive to reason, spawn more, or check status
9
+ * 3. Orchestrator collects results when ready
10
+ *
11
+ * Scoped per MultiAgentGraph instance — each orchestrator graph gets its own registry.
12
+ */
13
+ class HandoffRegistry {
14
+ records = new Map();
15
+ /**
16
+ * Register a spawned handoff child.
17
+ * The promise runs in the background — not awaited here.
18
+ */
19
+ spawn(params) {
20
+ const record = {
21
+ id: params.id,
22
+ name: params.name,
23
+ task: params.task,
24
+ spawnedAt: Date.now(),
25
+ status: 'running',
26
+ promise: params.promise,
27
+ };
28
+ // Wire up the promise to update the record on completion
29
+ params.promise
30
+ .then((result) => {
31
+ const resultText = params.extractResult(result.messages, params.id);
32
+ const truncated = params.truncateResult(resultText, params.maxResultChars);
33
+ record.status = 'completed';
34
+ record.resultText = truncated;
35
+ record.durationMs = Date.now() - record.spawnedAt;
36
+ record.resultMessageCount = result.messages.length;
37
+ params.onComplete?.(record);
38
+ })
39
+ .catch((err) => {
40
+ record.status = 'failed';
41
+ record.error = err instanceof Error ? err.message : String(err);
42
+ record.durationMs = Date.now() - record.spawnedAt;
43
+ params.onComplete?.(record);
44
+ });
45
+ this.records.set(params.id, record);
46
+ }
47
+ /** List all pending (running) handoffs */
48
+ listPending() {
49
+ return Array.from(this.records.values()).filter((r) => r.status === 'running');
50
+ }
51
+ /** List all completed handoffs (not yet collected) */
52
+ listCompleted() {
53
+ return Array.from(this.records.values()).filter((r) => r.status === 'completed' || r.status === 'failed');
54
+ }
55
+ /** List all handoffs regardless of status */
56
+ listAll() {
57
+ return Array.from(this.records.values());
58
+ }
59
+ /** Get a specific handoff by ID */
60
+ get(id) {
61
+ return this.records.get(id);
62
+ }
63
+ /** Check if any handoffs are still running */
64
+ hasPending() {
65
+ return this.listPending().length > 0;
66
+ }
67
+ /**
68
+ * Wait for ALL pending handoffs to complete.
69
+ * Returns all completed records (including previously completed ones).
70
+ */
71
+ async waitForAll() {
72
+ const pending = this.listPending();
73
+ if (pending.length > 0) {
74
+ await Promise.allSettled(pending.map((r) => r.promise));
75
+ }
76
+ return this.listAll();
77
+ }
78
+ /**
79
+ * Wait for ANY pending handoff to complete.
80
+ * Returns the newly completed record(s).
81
+ */
82
+ async waitForAny() {
83
+ const pending = this.listPending();
84
+ if (pending.length === 0) {
85
+ return this.listCompleted();
86
+ }
87
+ // Race all pending promises — at least one will resolve
88
+ await Promise.race(pending.map((r) => r.promise.then(() => r).catch(() => r)));
89
+ // Small yield to let promise handlers update records
90
+ await new Promise((resolve) => setTimeout(resolve, 0));
91
+ return this.listCompleted();
92
+ }
93
+ /** Clear all records (for cleanup between graph invocations) */
94
+ clear() {
95
+ this.records.clear();
96
+ }
97
+ /** Number of total tracked handoffs */
98
+ get size() {
99
+ return this.records.size;
100
+ }
101
+ }
102
+
103
+ exports.HandoffRegistry = HandoffRegistry;
104
+ //# sourceMappingURL=HandoffRegistry.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HandoffRegistry.cjs","sources":["../../../src/graphs/HandoffRegistry.ts"],"sourcesContent":["import type { BaseMessage } from '@langchain/core/messages';\nimport type * as t from '@/types';\n\n/**\n * Tracks the lifecycle of a spawned handoff child agent.\n * Mirrors OpenClaw's SubagentRunRecord pattern.\n */\nexport type HandoffRecord = {\n /** Unique handoff ID (destination agentId) */\n id: string;\n /** Display name of the child agent */\n name: string;\n /** Task description / instructions passed to child */\n task: string;\n /** When the handoff was spawned */\n spawnedAt: number;\n /** Current status */\n status: 'pending' | 'running' | 'completed' | 'failed';\n /** The background promise executing the child subgraph */\n promise: Promise<t.BaseGraphState>;\n /** Resolved result text (populated on completion) */\n resultText?: string;\n /** Error message (populated on failure) */\n error?: string;\n /** Duration in ms (populated on completion/failure) */\n durationMs?: number;\n /** Number of messages in child's output */\n resultMessageCount?: number;\n};\n\n/**\n * Registry for async handoff execution.\n *\n * Enables the OpenClaw-style autonomous orchestration pattern:\n * 1. Orchestrator spawns children (non-blocking)\n * 2. Orchestrator stays alive to reason, spawn more, or check status\n * 3. Orchestrator collects results when ready\n *\n * Scoped per MultiAgentGraph instance — each orchestrator graph gets its own registry.\n */\nexport class HandoffRegistry {\n private records: Map<string, HandoffRecord> = new Map();\n\n /**\n * Register a spawned handoff child.\n * The promise runs in the background — not awaited here.\n */\n spawn(params: {\n id: string;\n name: string;\n task: string;\n promise: Promise<t.BaseGraphState>;\n extractResult: (messages: BaseMessage[], agentId: string) => string;\n truncateResult: (text: string, maxChars: number) => string;\n maxResultChars: number;\n /** Callback when child completes (for SSE events) */\n onComplete?: (record: HandoffRecord) => void;\n }): void {\n const record: HandoffRecord = {\n id: params.id,\n name: params.name,\n task: params.task,\n spawnedAt: Date.now(),\n status: 'running',\n promise: params.promise,\n };\n\n // Wire up the promise to update the record on completion\n params.promise\n .then((result) => {\n const resultText = params.extractResult(\n result.messages,\n params.id\n );\n const truncated = params.truncateResult(\n resultText,\n params.maxResultChars\n );\n record.status = 'completed';\n record.resultText = truncated;\n record.durationMs = Date.now() - record.spawnedAt;\n record.resultMessageCount = result.messages.length;\n params.onComplete?.(record);\n })\n .catch((err) => {\n record.status = 'failed';\n record.error = err instanceof Error ? err.message : String(err);\n record.durationMs = Date.now() - record.spawnedAt;\n params.onComplete?.(record);\n });\n\n this.records.set(params.id, record);\n }\n\n /** List all pending (running) handoffs */\n listPending(): HandoffRecord[] {\n return Array.from(this.records.values()).filter(\n (r) => r.status === 'running'\n );\n }\n\n /** List all completed handoffs (not yet collected) */\n listCompleted(): HandoffRecord[] {\n return Array.from(this.records.values()).filter(\n (r) => r.status === 'completed' || r.status === 'failed'\n );\n }\n\n /** List all handoffs regardless of status */\n listAll(): HandoffRecord[] {\n return Array.from(this.records.values());\n }\n\n /** Get a specific handoff by ID */\n get(id: string): HandoffRecord | undefined {\n return this.records.get(id);\n }\n\n /** Check if any handoffs are still running */\n hasPending(): boolean {\n return this.listPending().length > 0;\n }\n\n /**\n * Wait for ALL pending handoffs to complete.\n * Returns all completed records (including previously completed ones).\n */\n async waitForAll(): Promise<HandoffRecord[]> {\n const pending = this.listPending();\n if (pending.length > 0) {\n await Promise.allSettled(pending.map((r) => r.promise));\n }\n return this.listAll();\n }\n\n /**\n * Wait for ANY pending handoff to complete.\n * Returns the newly completed record(s).\n */\n async waitForAny(): Promise<HandoffRecord[]> {\n const pending = this.listPending();\n if (pending.length === 0) {\n return this.listCompleted();\n }\n\n // Race all pending promises — at least one will resolve\n await Promise.race(\n pending.map((r) =>\n r.promise.then(() => r).catch(() => r)\n )\n );\n\n // Small yield to let promise handlers update records\n await new Promise((resolve) => setTimeout(resolve, 0));\n\n return this.listCompleted();\n }\n\n /** Clear all records (for cleanup between graph invocations) */\n clear(): void {\n this.records.clear();\n }\n\n /** Number of total tracked handoffs */\n get size(): number {\n return this.records.size;\n }\n}\n"],"names":[],"mappings":"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"}
@@ -13,6 +13,7 @@ var summarize = require('../messages/summarize.cjs');
13
13
  var Graph = require('./Graph.cjs');
14
14
  var events = require('../utils/events.cjs');
15
15
  var ApprovalGateNode = require('../nodes/ApprovalGateNode.cjs');
16
+ var HandoffRegistry = require('./HandoffRegistry.cjs');
16
17
 
17
18
  /** Pattern to extract instructions from transfer ToolMessage content */
18
19
  const TRANSFER_INSTRUCTIONS_PATTERN = /(?:Instructions?|Context):\s*(.+)/is;
@@ -60,6 +61,12 @@ class MultiAgentGraph extends Graph.StandardGraph {
60
61
  * Used by auto-continuation to know which agent's context to preserve after handoff.
61
62
  */
62
63
  lastActiveAgentId;
64
+ /**
65
+ * Registry for async handoff execution.
66
+ * Enables OpenClaw-style autonomous orchestration: spawn children non-blocking,
67
+ * orchestrator stays alive to reason and collect results when ready.
68
+ */
69
+ handoffRegistry = new HandoffRegistry.HandoffRegistry();
63
70
  /**
64
71
  * When set, the graph routes START to this agent instead of the default starting nodes.
65
72
  * Enables multi-turn resumption: follow-up messages go to the agent that last handled
@@ -262,6 +269,21 @@ class MultiAgentGraph extends Graph.StandardGraph {
262
269
  }
263
270
  agentContext.graphTools.push(...transferTools);
264
271
  console.debug(`[MultiAgentGraph] Transfer tools for "${agentId}": [${transferTools.map((t) => t.name).join(', ')}]`);
272
+ // Inject orchestration guidance for agents with transfer tools
273
+ const childDescs = edges.flatMap((e) => {
274
+ const dests = Array.isArray(e.to) ? e.to : [e.to];
275
+ return dests.map((d) => {
276
+ const ctx = this.agentContexts.get(d);
277
+ const name = ctx?.name ?? d;
278
+ const desc = ctx?.description ? ` — ${ctx.description}` : '';
279
+ return `${name}${desc}`;
280
+ });
281
+ });
282
+ const guidance = this.buildOrchestratorGuidance(childDescs, transferTools.length);
283
+ const existing = agentContext.additionalInstructions ?? '';
284
+ agentContext.additionalInstructions = existing
285
+ ? `${existing}\n\n${guidance}`
286
+ : guidance;
265
287
  }
266
288
  }
267
289
  /**
@@ -455,6 +477,41 @@ class MultiAgentGraph extends Graph.StandardGraph {
455
477
  }
456
478
  return tools$1;
457
479
  }
480
+ /**
481
+ * Builds orchestration guidance injected into the system message of agents
482
+ * that have handoff or transfer tools (i.e., orchestrator agents).
483
+ *
484
+ * Modeled after OpenClaw's battle-tested subagent orchestration patterns:
485
+ * - Push-based completion (results auto-return from child agents)
486
+ * - Multi-round execution for dependent tasks
487
+ * - Explicit rules against hallucinating data or acting on unavailable context
488
+ *
489
+ * @param childDescs - Display names (with optional descriptions) of child agents
490
+ * @param toolCount - Number of handoff/transfer tools available
491
+ */
492
+ buildOrchestratorGuidance(childDescs, toolCount) {
493
+ return [
494
+ '## Agent Orchestration',
495
+ '',
496
+ `You have ${toolCount} specialist agent(s) available for delegation:`,
497
+ ...childDescs.map((d) => `- ${d}`),
498
+ '',
499
+ 'If a task is more complex or takes longer, delegate it to a specialist agent. Completion is push-based: it will auto-return its result when done.',
500
+ 'Use `check_agents` to check status without waiting. Use `collect_results` to wait for and retrieve agent outputs.',
501
+ 'Default workflow: spawn work, continue reasoning, and call `collect_results` when ready.',
502
+ 'Coordinate agent work and synthesize results before responding to the user.',
503
+ 'For non-trivial multi-step work, keep a short plan updated for the user.',
504
+ 'Do not poll `check_agents` in a loop; only check status on-demand (for debugging or when explicitly asked).',
505
+ '',
506
+ '### Delegation Rules',
507
+ '- Delegate one clear, specific task per agent call.',
508
+ '- Independent tasks MAY be spawned in parallel (multiple calls in one turn, then one `collect_results`).',
509
+ '- Dependent tasks MUST be spawned in separate rounds — spawn, collect, analyze, then spawn the next with REAL data from prior results.',
510
+ '- NEVER fabricate, guess, or use placeholder data. Only pass real data from collected agent results.',
511
+ '- After collecting results, analyze them before proceeding. Explain what the agent found and what you will do next.',
512
+ '- If an agent fails, analyze the error and retry with clearer instructions before reporting failure.',
513
+ ].join('\n');
514
+ }
458
515
  /**
459
516
  * Builds a meaningful default description for a transfer tool when no explicit
460
517
  * edge.description is provided. Uses the destination agent's name and description
@@ -502,14 +559,92 @@ class MultiAgentGraph extends Graph.StandardGraph {
502
559
  agentContext.graphTools = [];
503
560
  }
504
561
  agentContext.graphTools.push(...handoffTools);
505
- console.debug(`[MultiAgentGraph] Handoff tools for "${agentId}": [${handoffTools.map((t) => t.name).join(', ')}]`);
562
+ /**
563
+ * Add orchestrator coordination tools: collect_results and check_agents.
564
+ * These enable the OpenClaw-style autonomous loop:
565
+ * spawn → reason → check/collect → reason → spawn more → synthesize
566
+ */
567
+ const handoffReg = this.handoffRegistry;
568
+ agentContext.graphTools.push(tools.tool(async () => {
569
+ if (!handoffReg.hasPending() && handoffReg.size === 0) {
570
+ return 'No agents have been spawned yet.';
571
+ }
572
+ /** Wait for all pending handoffs to complete */
573
+ const records = await handoffReg.waitForAll();
574
+ const parts = [];
575
+ for (const record of records) {
576
+ if (record.status === 'completed') {
577
+ parts.push(`## ${record.name} (completed in ${record.durationMs}ms)\n${record.resultText}`);
578
+ }
579
+ else if (record.status === 'failed') {
580
+ parts.push(`## ${record.name} (FAILED after ${record.durationMs}ms)\nError: ${record.error}`);
581
+ }
582
+ else {
583
+ parts.push(`## ${record.name} (still running, ${Date.now() - record.spawnedAt}ms elapsed)`);
584
+ }
585
+ }
586
+ return parts.join('\n\n---\n\n');
587
+ }, {
588
+ name: 'collect_results',
589
+ schema: { type: 'object', properties: {}, required: [] },
590
+ description: 'Wait for all spawned agents to complete and collect their results. ' +
591
+ 'Call this after spawning one or more agents to get their output.',
592
+ }), tools.tool(async () => {
593
+ const all = handoffReg.listAll();
594
+ if (all.length === 0) {
595
+ return 'No agents tracked.';
596
+ }
597
+ const lines = all.map((r) => {
598
+ const elapsed = Date.now() - r.spawnedAt;
599
+ if (r.status === 'running') {
600
+ return `- **${r.name}**: running (${elapsed}ms elapsed) — task: ${r.task.substring(0, 100)}`;
601
+ }
602
+ else if (r.status === 'completed') {
603
+ return `- **${r.name}**: completed (${r.durationMs}ms, ${r.resultText?.length ?? 0} chars)`;
604
+ }
605
+ else {
606
+ return `- **${r.name}**: failed (${r.durationMs}ms) — ${r.error}`;
607
+ }
608
+ });
609
+ const pending = all.filter((r) => r.status === 'running').length;
610
+ const completed = all.filter((r) => r.status === 'completed').length;
611
+ const failed = all.filter((r) => r.status === 'failed').length;
612
+ return [
613
+ `**Agent Status**: ${pending} running, ${completed} completed, ${failed} failed`,
614
+ '',
615
+ ...lines,
616
+ ].join('\n');
617
+ }, {
618
+ name: 'check_agents',
619
+ schema: { type: 'object', properties: {}, required: [] },
620
+ description: 'Check the status of all spawned agents without waiting. ' +
621
+ 'Shows which agents are running, completed, or failed.',
622
+ }));
623
+ console.debug(`[MultiAgentGraph] Handoff tools for "${agentId}": [${handoffTools.map((t) => t.name).join(', ')}, collect_results, check_agents]`);
624
+ // Inject autonomous orchestration guidance for agents with handoff tools.
625
+ // Modeled after OpenClaw's battle-tested subagent orchestration patterns.
626
+ const childDescs = edges.flatMap((e) => {
627
+ const dests = Array.isArray(e.to) ? e.to : [e.to];
628
+ return dests.map((d) => {
629
+ const ctx = this.agentContexts.get(d);
630
+ const name = ctx?.name ?? d;
631
+ const desc = ctx?.description ? ` — ${ctx.description}` : '';
632
+ return `${name}${desc}`;
633
+ });
634
+ });
635
+ const orchestrationGuidance = this.buildOrchestratorGuidance(childDescs, handoffTools.length);
636
+ const existing = agentContext.additionalInstructions ?? '';
637
+ agentContext.additionalInstructions = existing
638
+ ? `${existing}\n\n${orchestrationGuidance}`
639
+ : orchestrationGuidance;
506
640
  }
507
641
  }
508
642
  /**
509
643
  * Create handoff tools for an edge (handles multiple destinations).
510
- * Each handoff tool invokes the child agent's compiled subgraph inline,
511
- * extracts the final AI message text, truncates it, and returns it as
512
- * a string (which becomes a ToolMessage in the parent's context).
644
+ * Each handoff tool spawns the child agent's compiled subgraph asynchronously
645
+ * and returns immediately. The orchestrator uses `collect_results` to retrieve
646
+ * outputs and `check_agents` to monitor status matching OpenClaw's
647
+ * push-based autonomous orchestration pattern.
513
648
  *
514
649
  * @param edge - The graph edge defining the handoff
515
650
  * @param sourceAgentId - The ID of the parent/supervisor agent
@@ -526,11 +661,12 @@ class MultiAgentGraph extends Graph.StandardGraph {
526
661
  const hasPromptInput = edge.prompt != null && typeof edge.prompt === 'string';
527
662
  const promptInputDescription = hasPromptInput ? edge.prompt : undefined;
528
663
  const promptKey = edge.promptKey ?? 'instructions';
529
- /** Capture registry reference — Map populated in createWorkflow() */
530
- const registry = this.subgraphRegistry;
664
+ /** Capture registry references — Map populated in createWorkflow() */
665
+ const subgraphReg = this.subgraphRegistry;
666
+ const handoffReg = this.handoffRegistry;
531
667
  tools$1.push(tools.tool(async (rawInput, config) => {
532
668
  const input = rawInput;
533
- const subgraph = registry.get(destination);
669
+ const subgraph = subgraphReg.get(destination);
534
670
  if (!subgraph) {
535
671
  throw new Error(`Handoff target "${destination}" subgraph not found in registry. ` +
536
672
  'This is a bug: createWorkflow() should have populated the subgraph registry.');
@@ -538,52 +674,75 @@ class MultiAgentGraph extends Graph.StandardGraph {
538
674
  const state = langgraph.getCurrentTaskInput();
539
675
  let childMessages = MultiAgentGraph.prepareHandoffMessages([...state.messages]);
540
676
  /** Inject instructions as HumanMessage if provided by the parent LLM */
541
- if (hasPromptInput &&
542
- promptKey in input &&
543
- input[promptKey] != null) {
677
+ const taskDescription = (hasPromptInput && promptKey in input && input[promptKey] != null)
678
+ ? String(input[promptKey])
679
+ : '';
680
+ if (taskDescription) {
544
681
  childMessages = [
545
682
  ...childMessages,
546
- new messages.HumanMessage(String(input[promptKey])),
683
+ new messages.HumanMessage(taskDescription),
547
684
  ];
548
685
  }
549
686
  const childState = {
550
687
  messages: childMessages,
551
688
  };
552
- console.debug(`[MultiAgentGraph] Handoff "${sourceAgentId}" -> "${destination}" START ` +
553
- `(messages: ${childMessages.length})`);
554
- try {
555
- /**
556
- * Dispatch transition BEFORE invoking the child subgraph so that
557
- * callbacks.js sets multiAgentTrace.isMultiAgent = true before the
558
- * child's ON_RUN_STEP events fire. This ensures child tool calls
559
- * are attributed to the correct agent in admin traces.
560
- */
561
- await events.safeDispatchCustomEvent(_enum.GraphEvents.ON_AGENT_TRANSITION, {
562
- sourceAgentId: sourceAgentId,
563
- sourceAgentName: this.agentContexts.get(sourceAgentId)?.name ?? sourceAgentId,
564
- destinationAgentId: destination,
565
- destinationAgentName: destContext?.name ?? destination,
566
- edgeType: _enum.EdgeType.HANDOFF,
567
- timestamp: Date.now(),
568
- }, config);
569
- /**
570
- * Invoke the child subgraph with config propagation.
571
- * Config carries callbacks (for SSE streaming), abort signal,
572
- * and configurable data (thread_id, user_id) to the child.
573
- */
574
- const result = await subgraph.invoke(childState, config);
575
- const resultText = MultiAgentGraph.extractHandoffResult(result.messages, destination);
576
- const truncatedResult = MultiAgentGraph.truncateHandoffResult(resultText, maxResultChars);
577
- console.debug(`[MultiAgentGraph] Handoff "${sourceAgentId}" -> "${destination}" DONE ` +
578
- `(result: ${resultText.length} chars` +
579
- `${truncatedResult.length < resultText.length ? `, truncated to ${truncatedResult.length}` : ''})`);
580
- return truncatedResult;
581
- }
582
- catch (err) {
583
- const errorMessage = err instanceof Error ? err.message : String(err);
584
- console.error(`[MultiAgentGraph] Handoff "${sourceAgentId}" -> "${destination}" ERROR:`, errorMessage);
585
- return `[Handoff to "${destination}" failed: ${errorMessage}]`;
586
- }
689
+ const childContext = this.agentContexts.get(destination);
690
+ const destName = destContext?.name ?? destination;
691
+ console.debug(`[MultiAgentGraph] Handoff "${sourceAgentId}" -> "${destination}" SPAWN (async)\n` +
692
+ ` messages: ${childMessages.length}\n` +
693
+ ` childTools: ${childContext?.tools?.length ?? 0} instances\n` +
694
+ ` childToolDefs: ${childContext?.toolDefinitions?.length ?? 0} definitions`);
695
+ /**
696
+ * Dispatch transition BEFORE spawning the child subgraph so that
697
+ * callbacks.js sets multiAgentTrace.isMultiAgent = true before the
698
+ * child's ON_RUN_STEP events fire.
699
+ */
700
+ await events.safeDispatchCustomEvent(_enum.GraphEvents.ON_AGENT_TRANSITION, {
701
+ sourceAgentId: sourceAgentId,
702
+ sourceAgentName: this.agentContexts.get(sourceAgentId)?.name ?? sourceAgentId,
703
+ destinationAgentId: destination,
704
+ destinationAgentName: destName,
705
+ edgeType: _enum.EdgeType.HANDOFF,
706
+ timestamp: Date.now(),
707
+ }, config);
708
+ /**
709
+ * Spawn child execution as a background promise (non-blocking).
710
+ * The orchestrator gets an immediate response and can reason,
711
+ * spawn more agents, or call collect_results when ready.
712
+ *
713
+ * The config is passed through so SSE callbacks, abort signal,
714
+ * and configurable data still propagate to the child.
715
+ */
716
+ const childPromise = subgraph.invoke(childState, config);
717
+ const capturedConfig = config;
718
+ const parentAgentId = sourceAgentId;
719
+ const parentCtx = this.agentContexts.get(sourceAgentId);
720
+ handoffReg.spawn({
721
+ id: destination,
722
+ name: destName,
723
+ task: taskDescription || '(no explicit instructions)',
724
+ promise: childPromise,
725
+ extractResult: MultiAgentGraph.extractHandoffResult,
726
+ truncateResult: MultiAgentGraph.truncateHandoffResult,
727
+ maxResultChars,
728
+ onComplete: (record) => {
729
+ console.debug(`[MultiAgentGraph] Handoff "${parentAgentId}" -> "${destination}" ${record.status.toUpperCase()} ` +
730
+ `(${record.durationMs}ms, ${record.resultText?.length ?? 0} chars)`);
731
+ /** Dispatch completion event for UI update */
732
+ events.safeDispatchCustomEvent(_enum.GraphEvents.ON_AGENT_TRANSITION, {
733
+ sourceAgentId: destination,
734
+ sourceAgentName: destName,
735
+ destinationAgentId: parentAgentId,
736
+ destinationAgentName: parentCtx?.name ?? parentAgentId,
737
+ edgeType: _enum.EdgeType.HANDOFF,
738
+ timestamp: Date.now(),
739
+ isCompletion: true,
740
+ durationMs: record.durationMs,
741
+ resultLength: record.resultText?.length ?? 0,
742
+ }, capturedConfig).catch(() => { });
743
+ },
744
+ });
745
+ return `[Agent "${destName}" spawned. Use collect_results to get the output when ready.]`;
587
746
  }, {
588
747
  name: toolName,
589
748
  schema: hasPromptInput
@@ -1373,7 +1532,25 @@ class MultiAgentGraph extends Graph.StandardGraph {
1373
1532
  }
1374
1533
  }
1375
1534
  }
1376
- /** No special routing needed - return state normally */
1535
+ /**
1536
+ * No Command routing needed — dispatch ON_AGENT_TRANSITION for all
1537
+ * destinations so callbacks.js can register child agents for event
1538
+ * isolation BEFORE they start streaming.
1539
+ */
1540
+ const allDests = new Set([...transferDestinations, ...sequenceDestinations]);
1541
+ if (allDests.size > 0) {
1542
+ const edgeType = hasTransferEdges ? _enum.EdgeType.TRANSFER : _enum.EdgeType.SEQUENCE;
1543
+ for (const dest of allDests) {
1544
+ await events.safeDispatchCustomEvent(_enum.GraphEvents.ON_AGENT_TRANSITION, {
1545
+ sourceAgentId: agentId,
1546
+ sourceAgentName: this.agentContexts.get(agentId)?.name ?? agentId,
1547
+ destinationAgentId: dest,
1548
+ destinationAgentName: this.agentContexts.get(dest)?.name ?? dest,
1549
+ edgeType,
1550
+ timestamp: Date.now(),
1551
+ }, config);
1552
+ }
1553
+ }
1377
1554
  return result;
1378
1555
  };
1379
1556
  /** Wrapped agent as a node with its possible destinations */