@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
@@ -622,18 +622,33 @@ export class AgentContext {
622
622
  const isParallel = parallelSiblings.length > 0;
623
623
 
624
624
  const lines: string[] = [];
625
- lines.push('## Multi-Agent Workflow');
625
+ lines.push('## Subagent Context');
626
+ lines.push('');
626
627
  lines.push(
627
- `You are "${displayName}", transferred from "${sourceAgentName}".`
628
+ `You are "${displayName}", a subagent spawned by "${sourceAgentName}" for a specific task.`
628
629
  );
629
630
 
630
631
  if (isParallel) {
631
632
  lines.push(`Running in parallel with: ${parallelSiblings.join(', ')}.`);
632
633
  }
633
634
 
634
- lines.push(
635
- 'Execute only tasks relevant to your role. Routing is already handled if requested, unless you can route further.'
636
- );
635
+ lines.push('');
636
+ lines.push('### Your Role');
637
+ lines.push('- Complete your assigned task. That is your entire purpose.');
638
+ lines.push(`- You are NOT "${sourceAgentName}". Do not try to be.`);
639
+ lines.push('');
640
+ lines.push('### Rules');
641
+ lines.push('1. **Stay focused** — Do your assigned task, nothing else.');
642
+ lines.push('2. **Complete the task** — Your final message will be automatically reported back.');
643
+ lines.push('3. **Be autonomous** — Execute directly without asking for user confirmation. Use your tools and best judgment.');
644
+ lines.push('4. **No placeholders** — Never generate fake or placeholder data. If you cannot retrieve real data, say so.');
645
+ lines.push('5. **No side effects** — Do not send messages, emails, or notifications unless explicitly tasked to do so.');
646
+ lines.push('');
647
+ lines.push('### Output');
648
+ lines.push('When complete, your final response should include:');
649
+ lines.push('- What you accomplished or found');
650
+ lines.push('- Any relevant details the orchestrator should know');
651
+ lines.push('- Keep it concise but informative');
637
652
 
638
653
  return lines.join('\n');
639
654
  }
@@ -1497,6 +1497,14 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1497
1497
  clientOptions: effectiveClientOptions,
1498
1498
  });
1499
1499
 
1500
+ // DEBUG: Log which model and tools each agent uses during handoff
1501
+ console.debug(
1502
+ `[createCallModel] Agent "${agentId}" invoking LLM | provider=${agentContext.provider} | ` +
1503
+ `model=${(effectiveClientOptions as Record<string, unknown>)?.model ?? 'default'} | ` +
1504
+ `toolsForBinding=${toolsForBinding?.length ?? 0} | ` +
1505
+ `toolNames=[${(toolsForBinding ?? []).map((t) => (t as { name?: string }).name ?? 'unknown').join(', ')}]`,
1506
+ );
1507
+
1500
1508
  if (agentContext.systemRunnable) {
1501
1509
  model = agentContext.systemRunnable.pipe(model as Runnable);
1502
1510
  }
@@ -0,0 +1,168 @@
1
+ import type { BaseMessage } from '@langchain/core/messages';
2
+ import type * as t from '@/types';
3
+
4
+ /**
5
+ * Tracks the lifecycle of a spawned handoff child agent.
6
+ * Mirrors OpenClaw's SubagentRunRecord pattern.
7
+ */
8
+ export type HandoffRecord = {
9
+ /** Unique handoff ID (destination agentId) */
10
+ id: string;
11
+ /** Display name of the child agent */
12
+ name: string;
13
+ /** Task description / instructions passed to child */
14
+ task: string;
15
+ /** When the handoff was spawned */
16
+ spawnedAt: number;
17
+ /** Current status */
18
+ status: 'pending' | 'running' | 'completed' | 'failed';
19
+ /** The background promise executing the child subgraph */
20
+ promise: Promise<t.BaseGraphState>;
21
+ /** Resolved result text (populated on completion) */
22
+ resultText?: string;
23
+ /** Error message (populated on failure) */
24
+ error?: string;
25
+ /** Duration in ms (populated on completion/failure) */
26
+ durationMs?: number;
27
+ /** Number of messages in child's output */
28
+ resultMessageCount?: number;
29
+ };
30
+
31
+ /**
32
+ * Registry for async handoff execution.
33
+ *
34
+ * Enables the OpenClaw-style autonomous orchestration pattern:
35
+ * 1. Orchestrator spawns children (non-blocking)
36
+ * 2. Orchestrator stays alive to reason, spawn more, or check status
37
+ * 3. Orchestrator collects results when ready
38
+ *
39
+ * Scoped per MultiAgentGraph instance — each orchestrator graph gets its own registry.
40
+ */
41
+ export class HandoffRegistry {
42
+ private records: Map<string, HandoffRecord> = new Map();
43
+
44
+ /**
45
+ * Register a spawned handoff child.
46
+ * The promise runs in the background — not awaited here.
47
+ */
48
+ spawn(params: {
49
+ id: string;
50
+ name: string;
51
+ task: string;
52
+ promise: Promise<t.BaseGraphState>;
53
+ extractResult: (messages: BaseMessage[], agentId: string) => string;
54
+ truncateResult: (text: string, maxChars: number) => string;
55
+ maxResultChars: number;
56
+ /** Callback when child completes (for SSE events) */
57
+ onComplete?: (record: HandoffRecord) => void;
58
+ }): void {
59
+ const record: HandoffRecord = {
60
+ id: params.id,
61
+ name: params.name,
62
+ task: params.task,
63
+ spawnedAt: Date.now(),
64
+ status: 'running',
65
+ promise: params.promise,
66
+ };
67
+
68
+ // Wire up the promise to update the record on completion
69
+ params.promise
70
+ .then((result) => {
71
+ const resultText = params.extractResult(
72
+ result.messages,
73
+ params.id
74
+ );
75
+ const truncated = params.truncateResult(
76
+ resultText,
77
+ params.maxResultChars
78
+ );
79
+ record.status = 'completed';
80
+ record.resultText = truncated;
81
+ record.durationMs = Date.now() - record.spawnedAt;
82
+ record.resultMessageCount = result.messages.length;
83
+ params.onComplete?.(record);
84
+ })
85
+ .catch((err) => {
86
+ record.status = 'failed';
87
+ record.error = err instanceof Error ? err.message : String(err);
88
+ record.durationMs = Date.now() - record.spawnedAt;
89
+ params.onComplete?.(record);
90
+ });
91
+
92
+ this.records.set(params.id, record);
93
+ }
94
+
95
+ /** List all pending (running) handoffs */
96
+ listPending(): HandoffRecord[] {
97
+ return Array.from(this.records.values()).filter(
98
+ (r) => r.status === 'running'
99
+ );
100
+ }
101
+
102
+ /** List all completed handoffs (not yet collected) */
103
+ listCompleted(): HandoffRecord[] {
104
+ return Array.from(this.records.values()).filter(
105
+ (r) => r.status === 'completed' || r.status === 'failed'
106
+ );
107
+ }
108
+
109
+ /** List all handoffs regardless of status */
110
+ listAll(): HandoffRecord[] {
111
+ return Array.from(this.records.values());
112
+ }
113
+
114
+ /** Get a specific handoff by ID */
115
+ get(id: string): HandoffRecord | undefined {
116
+ return this.records.get(id);
117
+ }
118
+
119
+ /** Check if any handoffs are still running */
120
+ hasPending(): boolean {
121
+ return this.listPending().length > 0;
122
+ }
123
+
124
+ /**
125
+ * Wait for ALL pending handoffs to complete.
126
+ * Returns all completed records (including previously completed ones).
127
+ */
128
+ async waitForAll(): Promise<HandoffRecord[]> {
129
+ const pending = this.listPending();
130
+ if (pending.length > 0) {
131
+ await Promise.allSettled(pending.map((r) => r.promise));
132
+ }
133
+ return this.listAll();
134
+ }
135
+
136
+ /**
137
+ * Wait for ANY pending handoff to complete.
138
+ * Returns the newly completed record(s).
139
+ */
140
+ async waitForAny(): Promise<HandoffRecord[]> {
141
+ const pending = this.listPending();
142
+ if (pending.length === 0) {
143
+ return this.listCompleted();
144
+ }
145
+
146
+ // Race all pending promises — at least one will resolve
147
+ await Promise.race(
148
+ pending.map((r) =>
149
+ r.promise.then(() => r).catch(() => r)
150
+ )
151
+ );
152
+
153
+ // Small yield to let promise handlers update records
154
+ await new Promise((resolve) => setTimeout(resolve, 0));
155
+
156
+ return this.listCompleted();
157
+ }
158
+
159
+ /** Clear all records (for cleanup between graph invocations) */
160
+ clear(): void {
161
+ this.records.clear();
162
+ }
163
+
164
+ /** Number of total tracked handoffs */
165
+ get size(): number {
166
+ return this.records.size;
167
+ }
168
+ }
@@ -33,6 +33,7 @@ import {
33
33
  createApprovalGateNode,
34
34
  getApprovalGateNodeId,
35
35
  } from '@/nodes/ApprovalGateNode';
36
+ import { HandoffRegistry } from './HandoffRegistry';
36
37
 
37
38
  /** Pattern to extract instructions from transfer ToolMessage content */
38
39
  const TRANSFER_INSTRUCTIONS_PATTERN = /(?:Instructions?|Context):\s*(.+)/is;
@@ -82,6 +83,13 @@ export class MultiAgentGraph extends StandardGraph {
82
83
  */
83
84
  private lastActiveAgentId: string | undefined;
84
85
 
86
+ /**
87
+ * Registry for async handoff execution.
88
+ * Enables OpenClaw-style autonomous orchestration: spawn children non-blocking,
89
+ * orchestrator stays alive to reason and collect results when ready.
90
+ */
91
+ private handoffRegistry: HandoffRegistry = new HandoffRegistry();
92
+
85
93
  /**
86
94
  * When set, the graph routes START to this agent instead of the default starting nodes.
87
95
  * Enables multi-turn resumption: follow-up messages go to the agent that last handled
@@ -314,6 +322,22 @@ export class MultiAgentGraph extends StandardGraph {
314
322
  console.debug(
315
323
  `[MultiAgentGraph] Transfer tools for "${agentId}": [${transferTools.map((t) => t.name).join(', ')}]`
316
324
  );
325
+
326
+ // Inject orchestration guidance for agents with transfer tools
327
+ const childDescs = edges.flatMap((e) => {
328
+ const dests = Array.isArray(e.to) ? e.to : [e.to];
329
+ return dests.map((d) => {
330
+ const ctx = this.agentContexts.get(d);
331
+ const name = ctx?.name ?? d;
332
+ const desc = ctx?.description ? ` — ${ctx.description}` : '';
333
+ return `${name}${desc}`;
334
+ });
335
+ });
336
+ const guidance = this.buildOrchestratorGuidance(childDescs, transferTools.length);
337
+ const existing = agentContext.additionalInstructions ?? '';
338
+ agentContext.additionalInstructions = existing
339
+ ? `${existing}\n\n${guidance}`
340
+ : guidance;
317
341
  }
318
342
  }
319
343
 
@@ -553,6 +577,45 @@ export class MultiAgentGraph extends StandardGraph {
553
577
  return tools;
554
578
  }
555
579
 
580
+ /**
581
+ * Builds orchestration guidance injected into the system message of agents
582
+ * that have handoff or transfer tools (i.e., orchestrator agents).
583
+ *
584
+ * Modeled after OpenClaw's battle-tested subagent orchestration patterns:
585
+ * - Push-based completion (results auto-return from child agents)
586
+ * - Multi-round execution for dependent tasks
587
+ * - Explicit rules against hallucinating data or acting on unavailable context
588
+ *
589
+ * @param childDescs - Display names (with optional descriptions) of child agents
590
+ * @param toolCount - Number of handoff/transfer tools available
591
+ */
592
+ private buildOrchestratorGuidance(
593
+ childDescs: string[],
594
+ toolCount: number
595
+ ): string {
596
+ return [
597
+ '## Agent Orchestration',
598
+ '',
599
+ `You have ${toolCount} specialist agent(s) available for delegation:`,
600
+ ...childDescs.map((d) => `- ${d}`),
601
+ '',
602
+ '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.',
603
+ 'Use `check_agents` to check status without waiting. Use `collect_results` to wait for and retrieve agent outputs.',
604
+ 'Default workflow: spawn work, continue reasoning, and call `collect_results` when ready.',
605
+ 'Coordinate agent work and synthesize results before responding to the user.',
606
+ 'For non-trivial multi-step work, keep a short plan updated for the user.',
607
+ 'Do not poll `check_agents` in a loop; only check status on-demand (for debugging or when explicitly asked).',
608
+ '',
609
+ '### Delegation Rules',
610
+ '- Delegate one clear, specific task per agent call.',
611
+ '- Independent tasks MAY be spawned in parallel (multiple calls in one turn, then one `collect_results`).',
612
+ '- Dependent tasks MUST be spawned in separate rounds — spawn, collect, analyze, then spawn the next with REAL data from prior results.',
613
+ '- NEVER fabricate, guess, or use placeholder data. Only pass real data from collected agent results.',
614
+ '- After collecting results, analyze them before proceeding. Explain what the agent found and what you will do next.',
615
+ '- If an agent fails, analyze the error and retry with clearer instructions before reporting failure.',
616
+ ].join('\n');
617
+ }
618
+
556
619
  /**
557
620
  * Builds a meaningful default description for a transfer tool when no explicit
558
621
  * edge.description is provided. Uses the destination agent's name and description
@@ -610,17 +673,122 @@ export class MultiAgentGraph extends StandardGraph {
610
673
  agentContext.graphTools = [];
611
674
  }
612
675
  agentContext.graphTools.push(...handoffTools);
676
+
677
+ /**
678
+ * Add orchestrator coordination tools: collect_results and check_agents.
679
+ * These enable the OpenClaw-style autonomous loop:
680
+ * spawn → reason → check/collect → reason → spawn more → synthesize
681
+ */
682
+ const handoffReg = this.handoffRegistry;
683
+
684
+ agentContext.graphTools.push(
685
+ tool(
686
+ async () => {
687
+ if (!handoffReg.hasPending() && handoffReg.size === 0) {
688
+ return 'No agents have been spawned yet.';
689
+ }
690
+
691
+ /** Wait for all pending handoffs to complete */
692
+ const records = await handoffReg.waitForAll();
693
+
694
+ const parts: string[] = [];
695
+ for (const record of records) {
696
+ if (record.status === 'completed') {
697
+ parts.push(
698
+ `## ${record.name} (completed in ${record.durationMs}ms)\n${record.resultText}`
699
+ );
700
+ } else if (record.status === 'failed') {
701
+ parts.push(
702
+ `## ${record.name} (FAILED after ${record.durationMs}ms)\nError: ${record.error}`
703
+ );
704
+ } else {
705
+ parts.push(
706
+ `## ${record.name} (still running, ${Date.now() - record.spawnedAt}ms elapsed)`
707
+ );
708
+ }
709
+ }
710
+
711
+ return parts.join('\n\n---\n\n');
712
+ },
713
+ {
714
+ name: 'collect_results',
715
+ schema: { type: 'object', properties: {}, required: [] },
716
+ description:
717
+ 'Wait for all spawned agents to complete and collect their results. ' +
718
+ 'Call this after spawning one or more agents to get their output.',
719
+ }
720
+ ),
721
+ tool(
722
+ async () => {
723
+ const all = handoffReg.listAll();
724
+ if (all.length === 0) {
725
+ return 'No agents tracked.';
726
+ }
727
+
728
+ const lines = all.map((r) => {
729
+ const elapsed = Date.now() - r.spawnedAt;
730
+ if (r.status === 'running') {
731
+ return `- **${r.name}**: running (${elapsed}ms elapsed) — task: ${r.task.substring(0, 100)}`;
732
+ } else if (r.status === 'completed') {
733
+ return `- **${r.name}**: completed (${r.durationMs}ms, ${r.resultText?.length ?? 0} chars)`;
734
+ } else {
735
+ return `- **${r.name}**: failed (${r.durationMs}ms) — ${r.error}`;
736
+ }
737
+ });
738
+
739
+ const pending = all.filter((r) => r.status === 'running').length;
740
+ const completed = all.filter((r) => r.status === 'completed').length;
741
+ const failed = all.filter((r) => r.status === 'failed').length;
742
+
743
+ return [
744
+ `**Agent Status**: ${pending} running, ${completed} completed, ${failed} failed`,
745
+ '',
746
+ ...lines,
747
+ ].join('\n');
748
+ },
749
+ {
750
+ name: 'check_agents',
751
+ schema: { type: 'object', properties: {}, required: [] },
752
+ description:
753
+ 'Check the status of all spawned agents without waiting. ' +
754
+ 'Shows which agents are running, completed, or failed.',
755
+ }
756
+ )
757
+ );
758
+
613
759
  console.debug(
614
- `[MultiAgentGraph] Handoff tools for "${agentId}": [${handoffTools.map((t) => t.name).join(', ')}]`
760
+ `[MultiAgentGraph] Handoff tools for "${agentId}": [${handoffTools.map((t) => t.name).join(', ')}, collect_results, check_agents]`
615
761
  );
762
+
763
+ // Inject autonomous orchestration guidance for agents with handoff tools.
764
+ // Modeled after OpenClaw's battle-tested subagent orchestration patterns.
765
+ const childDescs = edges.flatMap((e) => {
766
+ const dests = Array.isArray(e.to) ? e.to : [e.to];
767
+ return dests.map((d) => {
768
+ const ctx = this.agentContexts.get(d);
769
+ const name = ctx?.name ?? d;
770
+ const desc = ctx?.description ? ` — ${ctx.description}` : '';
771
+ return `${name}${desc}`;
772
+ });
773
+ });
774
+ const orchestrationGuidance = this.buildOrchestratorGuidance(
775
+ childDescs,
776
+ handoffTools.length
777
+ );
778
+
779
+ const existing = agentContext.additionalInstructions ?? '';
780
+ agentContext.additionalInstructions = existing
781
+ ? `${existing}\n\n${orchestrationGuidance}`
782
+ : orchestrationGuidance;
616
783
  }
617
784
  }
618
785
 
619
786
  /**
620
787
  * Create handoff tools for an edge (handles multiple destinations).
621
- * Each handoff tool invokes the child agent's compiled subgraph inline,
622
- * extracts the final AI message text, truncates it, and returns it as
623
- * a string (which becomes a ToolMessage in the parent's context).
788
+ * Each handoff tool spawns the child agent's compiled subgraph asynchronously
789
+ * and returns immediately. The orchestrator uses `collect_results` to retrieve
790
+ * outputs and `check_agents` to monitor status matching OpenClaw's
791
+ * push-based autonomous orchestration pattern.
624
792
  *
625
793
  * @param edge - The graph edge defining the handoff
626
794
  * @param sourceAgentId - The ID of the parent/supervisor agent
@@ -646,14 +814,15 @@ export class MultiAgentGraph extends StandardGraph {
646
814
  const promptInputDescription = hasPromptInput ? edge.prompt : undefined;
647
815
  const promptKey = edge.promptKey ?? 'instructions';
648
816
 
649
- /** Capture registry reference — Map populated in createWorkflow() */
650
- const registry = this.subgraphRegistry;
817
+ /** Capture registry references — Map populated in createWorkflow() */
818
+ const subgraphReg = this.subgraphRegistry;
819
+ const handoffReg = this.handoffRegistry;
651
820
 
652
821
  tools.push(
653
822
  tool(
654
823
  async (rawInput, config) => {
655
824
  const input = rawInput as Record<string, unknown>;
656
- const subgraph = registry.get(destination);
825
+ const subgraph = subgraphReg.get(destination);
657
826
  if (!subgraph) {
658
827
  throw new Error(
659
828
  `Handoff target "${destination}" subgraph not found in registry. ` +
@@ -667,79 +836,94 @@ export class MultiAgentGraph extends StandardGraph {
667
836
  );
668
837
 
669
838
  /** Inject instructions as HumanMessage if provided by the parent LLM */
670
- if (
671
- hasPromptInput &&
672
- promptKey in input &&
673
- input[promptKey] != null
674
- ) {
839
+ const taskDescription = (hasPromptInput && promptKey in input && input[promptKey] != null)
840
+ ? String(input[promptKey])
841
+ : '';
842
+ if (taskDescription) {
675
843
  childMessages = [
676
844
  ...childMessages,
677
- new HumanMessage(String(input[promptKey])),
845
+ new HumanMessage(taskDescription),
678
846
  ];
679
847
  }
680
848
 
681
-
682
849
  const childState: t.BaseGraphState = {
683
850
  messages: childMessages,
684
851
  };
685
852
 
853
+ const childContext = this.agentContexts.get(destination);
854
+ const destName = destContext?.name ?? destination;
686
855
  console.debug(
687
- `[MultiAgentGraph] Handoff "${sourceAgentId}" -> "${destination}" START ` +
688
- `(messages: ${childMessages.length})`
856
+ `[MultiAgentGraph] Handoff "${sourceAgentId}" -> "${destination}" SPAWN (async)\n` +
857
+ ` messages: ${childMessages.length}\n` +
858
+ ` childTools: ${childContext?.tools?.length ?? 0} instances\n` +
859
+ ` childToolDefs: ${childContext?.toolDefinitions?.length ?? 0} definitions`
689
860
  );
690
861
 
691
- try {
692
- /**
693
- * Dispatch transition BEFORE invoking the child subgraph so that
694
- * callbacks.js sets multiAgentTrace.isMultiAgent = true before the
695
- * child's ON_RUN_STEP events fire. This ensures child tool calls
696
- * are attributed to the correct agent in admin traces.
697
- */
698
- await safeDispatchCustomEvent(
699
- GraphEvents.ON_AGENT_TRANSITION,
700
- {
701
- sourceAgentId: sourceAgentId,
702
- sourceAgentName: this.agentContexts.get(sourceAgentId)?.name ?? sourceAgentId,
703
- destinationAgentId: destination,
704
- destinationAgentName: destContext?.name ?? destination,
705
- edgeType: EdgeType.HANDOFF,
706
- timestamp: Date.now(),
707
- },
708
- config
709
- );
710
-
711
- /**
712
- * Invoke the child subgraph with config propagation.
713
- * Config carries callbacks (for SSE streaming), abort signal,
714
- * and configurable data (thread_id, user_id) to the child.
715
- */
716
- const result = await subgraph.invoke(childState, config);
717
-
718
- const resultText = MultiAgentGraph.extractHandoffResult(
719
- result.messages,
720
- destination
721
- );
722
- const truncatedResult = MultiAgentGraph.truncateHandoffResult(
723
- resultText,
724
- maxResultChars
725
- );
862
+ /**
863
+ * Dispatch transition BEFORE spawning the child subgraph so that
864
+ * callbacks.js sets multiAgentTrace.isMultiAgent = true before the
865
+ * child's ON_RUN_STEP events fire.
866
+ */
867
+ await safeDispatchCustomEvent(
868
+ GraphEvents.ON_AGENT_TRANSITION,
869
+ {
870
+ sourceAgentId: sourceAgentId,
871
+ sourceAgentName: this.agentContexts.get(sourceAgentId)?.name ?? sourceAgentId,
872
+ destinationAgentId: destination,
873
+ destinationAgentName: destName,
874
+ edgeType: EdgeType.HANDOFF,
875
+ timestamp: Date.now(),
876
+ },
877
+ config
878
+ );
726
879
 
727
- console.debug(
728
- `[MultiAgentGraph] Handoff "${sourceAgentId}" -> "${destination}" DONE ` +
729
- `(result: ${resultText.length} chars` +
730
- `${truncatedResult.length < resultText.length ? `, truncated to ${truncatedResult.length}` : ''})`
731
- );
880
+ /**
881
+ * Spawn child execution as a background promise (non-blocking).
882
+ * The orchestrator gets an immediate response and can reason,
883
+ * spawn more agents, or call collect_results when ready.
884
+ *
885
+ * The config is passed through so SSE callbacks, abort signal,
886
+ * and configurable data still propagate to the child.
887
+ */
888
+ const childPromise = subgraph.invoke(childState, config);
889
+
890
+ const capturedConfig = config;
891
+ const parentAgentId = sourceAgentId;
892
+ const parentCtx = this.agentContexts.get(sourceAgentId);
893
+
894
+ handoffReg.spawn({
895
+ id: destination,
896
+ name: destName,
897
+ task: taskDescription || '(no explicit instructions)',
898
+ promise: childPromise,
899
+ extractResult: MultiAgentGraph.extractHandoffResult,
900
+ truncateResult: MultiAgentGraph.truncateHandoffResult,
901
+ maxResultChars,
902
+ onComplete: (record) => {
903
+ console.debug(
904
+ `[MultiAgentGraph] Handoff "${parentAgentId}" -> "${destination}" ${record.status.toUpperCase()} ` +
905
+ `(${record.durationMs}ms, ${record.resultText?.length ?? 0} chars)`
906
+ );
907
+ /** Dispatch completion event for UI update */
908
+ safeDispatchCustomEvent(
909
+ GraphEvents.ON_AGENT_TRANSITION,
910
+ {
911
+ sourceAgentId: destination,
912
+ sourceAgentName: destName,
913
+ destinationAgentId: parentAgentId,
914
+ destinationAgentName: parentCtx?.name ?? parentAgentId,
915
+ edgeType: EdgeType.HANDOFF,
916
+ timestamp: Date.now(),
917
+ isCompletion: true,
918
+ durationMs: record.durationMs,
919
+ resultLength: record.resultText?.length ?? 0,
920
+ },
921
+ capturedConfig
922
+ ).catch(() => { /* best-effort event dispatch */ });
923
+ },
924
+ });
732
925
 
733
- return truncatedResult;
734
- } catch (err) {
735
- const errorMessage =
736
- err instanceof Error ? err.message : String(err);
737
- console.error(
738
- `[MultiAgentGraph] Handoff "${sourceAgentId}" -> "${destination}" ERROR:`,
739
- errorMessage
740
- );
741
- return `[Handoff to "${destination}" failed: ${errorMessage}]`;
742
- }
926
+ return `[Agent "${destName}" spawned. Use collect_results to get the output when ready.]`;
743
927
  },
744
928
  {
745
929
  name: toolName,
@@ -1761,7 +1945,30 @@ export class MultiAgentGraph extends StandardGraph {
1761
1945
  }
1762
1946
  }
1763
1947
 
1764
- /** No special routing needed - return state normally */
1948
+ /**
1949
+ * No Command routing needed — dispatch ON_AGENT_TRANSITION for all
1950
+ * destinations so callbacks.js can register child agents for event
1951
+ * isolation BEFORE they start streaming.
1952
+ */
1953
+ const allDests = new Set([...transferDestinations, ...sequenceDestinations]);
1954
+ if (allDests.size > 0) {
1955
+ const edgeType = hasTransferEdges ? EdgeType.TRANSFER : EdgeType.SEQUENCE;
1956
+ for (const dest of allDests) {
1957
+ await safeDispatchCustomEvent(
1958
+ GraphEvents.ON_AGENT_TRANSITION,
1959
+ {
1960
+ sourceAgentId: agentId,
1961
+ sourceAgentName: this.agentContexts.get(agentId)?.name ?? agentId,
1962
+ destinationAgentId: dest,
1963
+ destinationAgentName: this.agentContexts.get(dest)?.name ?? dest,
1964
+ edgeType,
1965
+ timestamp: Date.now(),
1966
+ },
1967
+ config
1968
+ );
1969
+ }
1970
+ }
1971
+
1765
1972
  return result;
1766
1973
  };
1767
1974