@illuma-ai/agents 1.1.24 → 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.
- package/dist/cjs/agents/AgentContext.cjs +20 -3
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +6 -0
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/HandoffRegistry.cjs +104 -0
- package/dist/cjs/graphs/HandoffRegistry.cjs.map +1 -0
- package/dist/cjs/graphs/MultiAgentGraph.cjs +224 -47
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/main.cjs +2 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/stream.cjs +4 -4
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/types/graph.cjs.map +1 -1
- package/dist/cjs/utils/events.cjs +3 -0
- package/dist/cjs/utils/events.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +20 -3
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +6 -0
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/HandoffRegistry.mjs +102 -0
- package/dist/esm/graphs/HandoffRegistry.mjs.map +1 -0
- package/dist/esm/graphs/MultiAgentGraph.mjs +224 -47
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/main.mjs +1 -0
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/stream.mjs +4 -4
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/types/graph.mjs.map +1 -1
- package/dist/esm/utils/events.mjs +3 -0
- package/dist/esm/utils/events.mjs.map +1 -1
- package/dist/types/graphs/HandoffRegistry.d.ts +80 -0
- package/dist/types/graphs/MultiAgentGraph.d.ts +23 -3
- package/dist/types/graphs/index.d.ts +1 -0
- package/dist/types/types/graph.d.ts +6 -0
- package/package.json +1 -1
- package/src/agents/AgentContext.ts +20 -5
- package/src/graphs/Graph.ts +11 -0
- package/src/graphs/HandoffRegistry.ts +168 -0
- package/src/graphs/MultiAgentGraph.ts +274 -67
- package/src/graphs/__tests__/HandoffRegistry.test.ts +407 -0
- package/src/graphs/index.ts +1 -0
- package/src/stream.ts +4 -6
- package/src/tools/approval/__tests__/constants.test.ts +3 -3
- package/src/types/graph.ts +6 -0
- 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('##
|
|
625
|
+
lines.push('## Subagent Context');
|
|
626
|
+
lines.push('');
|
|
626
627
|
lines.push(
|
|
627
|
-
`You are "${displayName}",
|
|
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
|
-
|
|
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
|
}
|
package/src/graphs/Graph.ts
CHANGED
|
@@ -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
|
}
|
|
@@ -2701,6 +2709,9 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
|
|
|
2701
2709
|
}
|
|
2702
2710
|
} catch (_e) {
|
|
2703
2711
|
/** If we can't get agent context, that's okay - agentId remains undefined */
|
|
2712
|
+
console.debug(
|
|
2713
|
+
`[dispatchRunStep] Could not resolve agentId from metadata.langgraph_node="${(metadata as Record<string, unknown>).langgraph_node}": ${(_e as Error).message}`
|
|
2714
|
+
);
|
|
2704
2715
|
}
|
|
2705
2716
|
}
|
|
2706
2717
|
|
|
@@ -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
|
|
622
|
-
*
|
|
623
|
-
*
|
|
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
|
|
650
|
-
const
|
|
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 =
|
|
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
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
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(
|
|
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}"
|
|
688
|
-
`
|
|
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
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
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
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
|