@pi-archimedes/subagent 0.3.1
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/package.json +31 -0
- package/src/compact.ts +360 -0
- package/src/cost.ts +8 -0
- package/src/execute.ts +111 -0
- package/src/expanded.ts +221 -0
- package/src/format.ts +60 -0
- package/src/handlers.ts +158 -0
- package/src/index.ts +224 -0
- package/src/render.ts +123 -0
- package/src/spawn.ts +73 -0
- package/src/stream.ts +177 -0
- package/src/types.ts +76 -0
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pi-archimedes/subagent",
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package"
|
|
7
|
+
],
|
|
8
|
+
"description": "Subagent dispatch with live TUI streaming and cost tracking",
|
|
9
|
+
"files": [
|
|
10
|
+
"src"
|
|
11
|
+
],
|
|
12
|
+
"main": "./src/index.ts",
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@pi-archimedes/core": "0.3.1"
|
|
15
|
+
},
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"@earendil-works/pi-coding-agent": ">=0.1.0",
|
|
18
|
+
"@earendil-works/pi-tui": ">=0.1.0",
|
|
19
|
+
"typebox": ">=1.1.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^22.0.0",
|
|
23
|
+
"typebox": "^1.1.38",
|
|
24
|
+
"typescript": "^6.0.0"
|
|
25
|
+
},
|
|
26
|
+
"pi": {
|
|
27
|
+
"extensions": [
|
|
28
|
+
"./src/index.ts"
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/compact.ts
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
2
|
+
import type { SubagentDetails, SubagentProgress, SubagentResult, SubagentToolResult } from "./types.js";
|
|
3
|
+
import { SPINNER_FRAMES, formatTokens, formatDuration, formatCost, truncLine, buildStatsLine } from "./format.js";
|
|
4
|
+
|
|
5
|
+
type Theme = { fg: (token: string, text: string) => string; bold: (text: string) => string };
|
|
6
|
+
type RenderContext = { state: Record<string, unknown>; invalidate: () => void };
|
|
7
|
+
|
|
8
|
+
// ── Activity line builder ───────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export function buildActivityLine(
|
|
11
|
+
progress: {
|
|
12
|
+
currentTool?: string;
|
|
13
|
+
currentToolArgs?: string;
|
|
14
|
+
currentToolStartedAt?: number;
|
|
15
|
+
finalOutput?: string;
|
|
16
|
+
status?: "running" | "completed" | "failed";
|
|
17
|
+
error?: string;
|
|
18
|
+
},
|
|
19
|
+
theme: Theme,
|
|
20
|
+
): string {
|
|
21
|
+
const prefix = theme.fg("dim", " ⎿ ");
|
|
22
|
+
|
|
23
|
+
if (progress.error) {
|
|
24
|
+
return prefix + theme.fg("error", truncLine(progress.error, 80));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (progress.currentTool) {
|
|
28
|
+
const argsPreview = progress.currentToolArgs
|
|
29
|
+
? truncLine(progress.currentToolArgs, 60)
|
|
30
|
+
: "";
|
|
31
|
+
const durationPart = progress.currentToolStartedAt
|
|
32
|
+
? " | " + formatDuration(Date.now() - progress.currentToolStartedAt)
|
|
33
|
+
: "";
|
|
34
|
+
let line = theme.fg("syntaxFunction", progress.currentTool);
|
|
35
|
+
if (argsPreview) {
|
|
36
|
+
line += theme.fg("dim", ": " + argsPreview);
|
|
37
|
+
}
|
|
38
|
+
if (durationPart) {
|
|
39
|
+
line += theme.fg("dim", durationPart);
|
|
40
|
+
}
|
|
41
|
+
return prefix + line;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (progress.finalOutput) {
|
|
45
|
+
const firstLine = progress.finalOutput.split("\n")[0] ?? "";
|
|
46
|
+
return prefix + truncLine(firstLine, 80);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ── Spinner helper ──────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
function getSpinnerGlyph(agentName: string, isRunning: boolean, status: string, context: RenderContext): string {
|
|
55
|
+
if (isRunning) {
|
|
56
|
+
const timerKey = "_subagentTimer_" + agentName;
|
|
57
|
+
const frameKey = "_subagentFrame_" + agentName;
|
|
58
|
+
if (!context.state[timerKey]) {
|
|
59
|
+
context.state[frameKey] = 0;
|
|
60
|
+
const timer = setInterval(() => {
|
|
61
|
+
context.state[frameKey] = ((context.state[frameKey] as number) + 1) % SPINNER_FRAMES.length;
|
|
62
|
+
context.invalidate();
|
|
63
|
+
}, 80);
|
|
64
|
+
context.state[timerKey] = timer;
|
|
65
|
+
}
|
|
66
|
+
return SPINNER_FRAMES[context.state[frameKey] as number];
|
|
67
|
+
}
|
|
68
|
+
return status === "completed" ? "✓" : "✗";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function cleanupTimer(agentName: string, context: RenderContext) {
|
|
72
|
+
const timerKey = "_subagentTimer_" + agentName;
|
|
73
|
+
const frameKey = "_subagentFrame_" + agentName;
|
|
74
|
+
if (context.state[timerKey]) {
|
|
75
|
+
clearInterval(context.state[timerKey] as ReturnType<typeof setInterval>);
|
|
76
|
+
delete context.state[timerKey];
|
|
77
|
+
}
|
|
78
|
+
delete context.state[frameKey];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── Compact single agent ────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
export function renderCompactSingle(
|
|
84
|
+
text: Text,
|
|
85
|
+
result: SubagentResult,
|
|
86
|
+
progress: SubagentProgress | undefined,
|
|
87
|
+
theme: Theme,
|
|
88
|
+
context: RenderContext,
|
|
89
|
+
): Text {
|
|
90
|
+
const agentName = result.agent ?? "subagent";
|
|
91
|
+
const summary = result.progressSummary ?? { toolCount: 0, tokens: 0, durationMs: 0 };
|
|
92
|
+
const isRunning = progress?.status === "running";
|
|
93
|
+
const status = isRunning ? "running" : (result.exitCode === 0 ? "completed" : "failed");
|
|
94
|
+
|
|
95
|
+
// Manage timer for spinner on line 3
|
|
96
|
+
if (isRunning) {
|
|
97
|
+
getSpinnerGlyph(agentName, true, "running", context);
|
|
98
|
+
} else {
|
|
99
|
+
cleanupTimer(agentName, context);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Track start time for live duration
|
|
103
|
+
const timeKey = "_subagentStartTime_" + agentName;
|
|
104
|
+
if (isRunning && !context.state[timeKey]) {
|
|
105
|
+
// Estimate start time from current duration
|
|
106
|
+
const currentDuration = summary.durationMs || (progress?.durationMs ?? 0);
|
|
107
|
+
context.state[timeKey] = Date.now() - currentDuration;
|
|
108
|
+
}
|
|
109
|
+
if (!isRunning) {
|
|
110
|
+
delete context.state[timeKey];
|
|
111
|
+
}
|
|
112
|
+
const liveDuration = isRunning && context.state[timeKey]
|
|
113
|
+
? Date.now() - (context.state[timeKey] as number)
|
|
114
|
+
: summary.durationMs;
|
|
115
|
+
|
|
116
|
+
const statsData = {
|
|
117
|
+
turns: result.usage.turns ?? 0,
|
|
118
|
+
toolCount: summary.toolCount,
|
|
119
|
+
tokens: summary.tokens,
|
|
120
|
+
durationMs: liveDuration,
|
|
121
|
+
cost: result.usage.cost ?? 0,
|
|
122
|
+
};
|
|
123
|
+
const statsLine = buildStatsLine(statsData, theme);
|
|
124
|
+
|
|
125
|
+
const statsPart = statsLine ?? "";
|
|
126
|
+
|
|
127
|
+
// Activity: spinner + current tool if running, status if finished
|
|
128
|
+
let activityLine: string;
|
|
129
|
+
if (isRunning) {
|
|
130
|
+
const spinner = SPINNER_FRAMES[(context.state["_subagentFrame_" + agentName] as number) ?? 0];
|
|
131
|
+
const spinnerColored = theme.fg("muted", spinner);
|
|
132
|
+
if (progress?.currentTool) {
|
|
133
|
+
const argsPreview = progress.currentToolArgs
|
|
134
|
+
? truncLine(progress.currentToolArgs, 60)
|
|
135
|
+
: "";
|
|
136
|
+
const durationPart = progress.currentToolStartedAt
|
|
137
|
+
? " | " + formatDuration(Date.now() - progress.currentToolStartedAt)
|
|
138
|
+
: "";
|
|
139
|
+
let line = theme.fg("syntaxFunction", progress.currentTool);
|
|
140
|
+
if (argsPreview) {
|
|
141
|
+
line += theme.fg("dim", ": " + argsPreview);
|
|
142
|
+
}
|
|
143
|
+
if (durationPart) {
|
|
144
|
+
line += theme.fg("dim", durationPart);
|
|
145
|
+
}
|
|
146
|
+
activityLine = spinnerColored + " " + line;
|
|
147
|
+
} else if (progress?.toolCalls && progress.toolCalls.length > 0) {
|
|
148
|
+
const lastCall = progress.toolCalls[progress.toolCalls.length - 1];
|
|
149
|
+
activityLine = theme.fg("dim", " ⎿ ") + theme.fg("muted", lastCall);
|
|
150
|
+
} else {
|
|
151
|
+
activityLine = theme.fg("muted", " ⎿ Working...");
|
|
152
|
+
}
|
|
153
|
+
} else if (result.error) {
|
|
154
|
+
activityLine = theme.fg("error", "✗ " + truncLine(result.error, 80));
|
|
155
|
+
} else if (status === "completed") {
|
|
156
|
+
activityLine = theme.fg("success", "✓ Done");
|
|
157
|
+
} else {
|
|
158
|
+
activityLine = theme.fg("error", "✗ Failed");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const modelName = progress?.model ?? result.model;
|
|
162
|
+
const modelLabel = modelName
|
|
163
|
+
? theme.fg("accent", modelName)
|
|
164
|
+
: "";
|
|
165
|
+
const expandHint = theme.fg("muted", "(ctrl+o)");
|
|
166
|
+
let output = [modelLabel, statsPart, expandHint].filter(Boolean).join(" ");
|
|
167
|
+
output += "\n" + activityLine;
|
|
168
|
+
|
|
169
|
+
text.setText(output);
|
|
170
|
+
return text;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── Compact parallel agents ─────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
export function renderCompactParallel(
|
|
176
|
+
text: Text,
|
|
177
|
+
details: SubagentDetails,
|
|
178
|
+
theme: Theme,
|
|
179
|
+
context: RenderContext,
|
|
180
|
+
): Text {
|
|
181
|
+
const lines = details.results.map((result, i) => {
|
|
182
|
+
const progress = details.progress?.[i];
|
|
183
|
+
const agentName = result.agent ?? "agent-" + i;
|
|
184
|
+
const summary = result.progressSummary ?? { toolCount: 0, tokens: 0, durationMs: 0 };
|
|
185
|
+
const status = progress?.status ?? (result.exitCode === 0 ? "completed" : "failed");
|
|
186
|
+
|
|
187
|
+
let glyph = status === "completed" ? "✓" : status === "failed" ? "✗" : "⠋";
|
|
188
|
+
const glyphColored = status === "completed"
|
|
189
|
+
? theme.fg("success", glyph)
|
|
190
|
+
: status === "failed"
|
|
191
|
+
? theme.fg("error", glyph)
|
|
192
|
+
: theme.fg("muted", glyph);
|
|
193
|
+
|
|
194
|
+
const statsData = {
|
|
195
|
+
turns: result.usage.turns ?? 0,
|
|
196
|
+
toolCount: summary.toolCount,
|
|
197
|
+
tokens: summary.tokens,
|
|
198
|
+
durationMs: summary.durationMs,
|
|
199
|
+
cost: result.usage.cost ?? 0,
|
|
200
|
+
};
|
|
201
|
+
const statsLine = buildStatsLine(statsData, theme);
|
|
202
|
+
const statsPart = statsLine ? " " + statsLine : "";
|
|
203
|
+
|
|
204
|
+
const activityData = {
|
|
205
|
+
currentTool: progress?.currentTool,
|
|
206
|
+
currentToolArgs: progress?.currentToolArgs,
|
|
207
|
+
currentToolStartedAt: progress?.currentToolStartedAt,
|
|
208
|
+
finalOutput: result.finalOutput,
|
|
209
|
+
status,
|
|
210
|
+
error: result.error,
|
|
211
|
+
};
|
|
212
|
+
const activityLine = buildActivityLine(activityData, theme);
|
|
213
|
+
|
|
214
|
+
let line = `${glyphColored} ${agentName}${statsPart}`;
|
|
215
|
+
if (activityLine) {
|
|
216
|
+
line += "\n" + activityLine;
|
|
217
|
+
}
|
|
218
|
+
return line;
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
text.setText(lines.join("\n"));
|
|
222
|
+
return text;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ── Compact streaming progress ──────────────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
export function renderCompactProgress(
|
|
228
|
+
text: Text,
|
|
229
|
+
progress: SubagentProgress,
|
|
230
|
+
theme: Theme,
|
|
231
|
+
context: RenderContext,
|
|
232
|
+
): Text {
|
|
233
|
+
const agentName = progress.agent ?? "subagent";
|
|
234
|
+
const status = progress.status;
|
|
235
|
+
const isRunning = status === "running";
|
|
236
|
+
|
|
237
|
+
let glyph = getSpinnerGlyph(agentName, isRunning, status, context);
|
|
238
|
+
const glyphColored = status === "completed"
|
|
239
|
+
? theme.fg("success", glyph)
|
|
240
|
+
: status === "failed"
|
|
241
|
+
? theme.fg("error", glyph)
|
|
242
|
+
: theme.fg("muted", glyph);
|
|
243
|
+
|
|
244
|
+
// Track start time for live duration
|
|
245
|
+
const timeKey = "_subagentStartTime_" + agentName;
|
|
246
|
+
if (isRunning && !context.state[timeKey]) {
|
|
247
|
+
context.state[timeKey] = Date.now() - (progress.durationMs ?? 0);
|
|
248
|
+
}
|
|
249
|
+
if (!isRunning) {
|
|
250
|
+
delete context.state[timeKey];
|
|
251
|
+
}
|
|
252
|
+
const liveDuration = isRunning && context.state[timeKey]
|
|
253
|
+
? Date.now() - (context.state[timeKey] as number)
|
|
254
|
+
: progress.durationMs;
|
|
255
|
+
|
|
256
|
+
const statsData = {
|
|
257
|
+
turns: 0,
|
|
258
|
+
toolCount: progress.toolCount,
|
|
259
|
+
tokens: progress.tokens,
|
|
260
|
+
durationMs: liveDuration,
|
|
261
|
+
cost: progress.cost,
|
|
262
|
+
};
|
|
263
|
+
const statsLine = buildStatsLine(statsData, theme);
|
|
264
|
+
|
|
265
|
+
// Activity: current tool if running, status if finished
|
|
266
|
+
let activityLine: string;
|
|
267
|
+
if (isRunning) {
|
|
268
|
+
const spinner = SPINNER_FRAMES[(context.state["_subagentFrame_" + agentName] as number) ?? 0];
|
|
269
|
+
const spinnerColored = theme.fg("muted", spinner);
|
|
270
|
+
if (progress.currentTool) {
|
|
271
|
+
const argsPreview = progress.currentToolArgs
|
|
272
|
+
? truncLine(progress.currentToolArgs, 60)
|
|
273
|
+
: "";
|
|
274
|
+
const durationPart = progress.currentToolStartedAt
|
|
275
|
+
? " | " + formatDuration(Date.now() - progress.currentToolStartedAt)
|
|
276
|
+
: "";
|
|
277
|
+
let line = theme.fg("syntaxFunction", progress.currentTool);
|
|
278
|
+
if (argsPreview) {
|
|
279
|
+
line += theme.fg("dim", ": " + argsPreview);
|
|
280
|
+
}
|
|
281
|
+
if (durationPart) {
|
|
282
|
+
line += theme.fg("dim", durationPart);
|
|
283
|
+
}
|
|
284
|
+
activityLine = spinnerColored + " " + line;
|
|
285
|
+
} else if (progress.toolCalls && progress.toolCalls.length > 0) {
|
|
286
|
+
const lastCall = progress.toolCalls[progress.toolCalls.length - 1];
|
|
287
|
+
activityLine = spinnerColored + " " + theme.fg("muted", lastCall);
|
|
288
|
+
} else {
|
|
289
|
+
activityLine = spinnerColored + " " + theme.fg("muted", "Working...");
|
|
290
|
+
}
|
|
291
|
+
} else if (progress.error) {
|
|
292
|
+
activityLine = theme.fg("error", "✗ " + truncLine(progress.error, 80));
|
|
293
|
+
} else if (status === "completed") {
|
|
294
|
+
activityLine = theme.fg("success", "✓ Done");
|
|
295
|
+
} else {
|
|
296
|
+
activityLine = theme.fg("error", "✗ Failed");
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const modelLabel = progress.model
|
|
300
|
+
? theme.fg("accent", progress.model)
|
|
301
|
+
: "";
|
|
302
|
+
const statsPart = statsLine ?? "";
|
|
303
|
+
const expandHint = theme.fg("muted", "(ctrl+o)");
|
|
304
|
+
let output = [modelLabel, statsPart, expandHint].filter(Boolean).join(" ");
|
|
305
|
+
output += "\n" + activityLine;
|
|
306
|
+
|
|
307
|
+
text.setText(output);
|
|
308
|
+
return text;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ── Compact parallel streaming progress ─────────────────────────────────────
|
|
312
|
+
|
|
313
|
+
export function renderCompactParallelProgress(
|
|
314
|
+
text: Text,
|
|
315
|
+
details: SubagentDetails,
|
|
316
|
+
theme: Theme,
|
|
317
|
+
context: RenderContext,
|
|
318
|
+
): Text {
|
|
319
|
+
const lines = (details.progress ?? []).map((progress, i) => {
|
|
320
|
+
const agentName = progress.agent ?? "agent-" + i;
|
|
321
|
+
const status = progress.status;
|
|
322
|
+
const isRunning = status === "running";
|
|
323
|
+
|
|
324
|
+
let glyph = getSpinnerGlyph(agentName, isRunning, status, context);
|
|
325
|
+
const glyphColored = status === "completed"
|
|
326
|
+
? theme.fg("success", glyph)
|
|
327
|
+
: status === "failed"
|
|
328
|
+
? theme.fg("error", glyph)
|
|
329
|
+
: theme.fg("muted", glyph);
|
|
330
|
+
|
|
331
|
+
const statsData = {
|
|
332
|
+
turns: 0,
|
|
333
|
+
toolCount: progress.toolCount,
|
|
334
|
+
tokens: progress.tokens,
|
|
335
|
+
durationMs: progress.durationMs,
|
|
336
|
+
cost: progress.cost,
|
|
337
|
+
};
|
|
338
|
+
const statsLine = buildStatsLine(statsData, theme);
|
|
339
|
+
const statsPart = statsLine ? " " + statsLine : "";
|
|
340
|
+
|
|
341
|
+
const activityData = {
|
|
342
|
+
currentTool: progress.currentTool,
|
|
343
|
+
currentToolArgs: progress.currentToolArgs,
|
|
344
|
+
currentToolStartedAt: progress.currentToolStartedAt,
|
|
345
|
+
finalOutput: undefined,
|
|
346
|
+
status,
|
|
347
|
+
error: progress.error,
|
|
348
|
+
};
|
|
349
|
+
const activityLine = buildActivityLine(activityData, theme);
|
|
350
|
+
|
|
351
|
+
let line = `${glyphColored} ${agentName}${statsPart}`;
|
|
352
|
+
if (activityLine) {
|
|
353
|
+
line += "\n" + activityLine;
|
|
354
|
+
}
|
|
355
|
+
return line;
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
text.setText(lines.join("\n"));
|
|
359
|
+
return text;
|
|
360
|
+
}
|
package/src/cost.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { getBus, Events } from "@pi-archimedes/core/bus";
|
|
2
|
+
|
|
3
|
+
export function emitCostUpdate(agent: string, usage: { inputTokens?: number; outputTokens?: number; cacheReadTokens?: number; cacheWriteTokens?: number; cost?: number }): void {
|
|
4
|
+
getBus().emit(Events.COST_UPDATE, {
|
|
5
|
+
source: `subagent:${agent}`,
|
|
6
|
+
...usage,
|
|
7
|
+
});
|
|
8
|
+
}
|
package/src/execute.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { spawnSubagent } from "./spawn.js";
|
|
2
|
+
import { streamEvents } from "./stream.js";
|
|
3
|
+
import { emitCostUpdate } from "./cost.js";
|
|
4
|
+
import type { SubagentProgress, SubagentResult, SubagentUsage } from "./types.js";
|
|
5
|
+
|
|
6
|
+
export interface ExecuteOptions {
|
|
7
|
+
agent?: string;
|
|
8
|
+
task: string;
|
|
9
|
+
model?: string;
|
|
10
|
+
cwd?: string;
|
|
11
|
+
signal?: AbortSignal;
|
|
12
|
+
onUpdate?: (progress: SubagentProgress) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Execute a single subagent synchronously — blocks until completion.
|
|
17
|
+
*/
|
|
18
|
+
export async function executeSubagent(options: ExecuteOptions): Promise<SubagentResult> {
|
|
19
|
+
const agentName = options.agent ?? "subagent";
|
|
20
|
+
const startTime = Date.now();
|
|
21
|
+
|
|
22
|
+
const child = spawnSubagent({
|
|
23
|
+
task: options.task,
|
|
24
|
+
model: options.model,
|
|
25
|
+
cwd: options.cwd,
|
|
26
|
+
signal: options.signal,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Track previously emitted values to only emit deltas
|
|
30
|
+
let lastEmittedInput = 0;
|
|
31
|
+
let lastEmittedOutput = 0;
|
|
32
|
+
let lastEmittedCost = 0;
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const result = await streamEvents(child, {
|
|
36
|
+
agent: agentName,
|
|
37
|
+
task: options.task,
|
|
38
|
+
onProgress: (progress: SubagentProgress) => {
|
|
39
|
+
// Emit only deltas to avoid double-counting in CostAccumulator
|
|
40
|
+
const deltaInput = progress.inputTokens - lastEmittedInput;
|
|
41
|
+
const deltaOutput = progress.outputTokens - lastEmittedOutput;
|
|
42
|
+
const deltaCost = progress.cost - lastEmittedCost;
|
|
43
|
+
if (deltaInput > 0 || deltaOutput > 0 || deltaCost > 0) {
|
|
44
|
+
emitCostUpdate(agentName, {
|
|
45
|
+
inputTokens: deltaInput,
|
|
46
|
+
outputTokens: deltaOutput,
|
|
47
|
+
cost: deltaCost,
|
|
48
|
+
});
|
|
49
|
+
lastEmittedInput = progress.inputTokens;
|
|
50
|
+
lastEmittedOutput = progress.outputTokens;
|
|
51
|
+
lastEmittedCost = progress.cost;
|
|
52
|
+
}
|
|
53
|
+
options.onUpdate?.(progress);
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Enrich result with agent name and duration
|
|
58
|
+
const durationMs = Date.now() - startTime;
|
|
59
|
+
return {
|
|
60
|
+
...result,
|
|
61
|
+
agent: agentName,
|
|
62
|
+
task: options.task,
|
|
63
|
+
progress: result.progress
|
|
64
|
+
? { ...result.progress, agent: agentName, durationMs }
|
|
65
|
+
: undefined,
|
|
66
|
+
progressSummary: result.progressSummary
|
|
67
|
+
? { ...result.progressSummary, durationMs }
|
|
68
|
+
: { toolCount: 0, tokens: 0, durationMs },
|
|
69
|
+
};
|
|
70
|
+
} catch (err) {
|
|
71
|
+
return {
|
|
72
|
+
agent: agentName,
|
|
73
|
+
task: options.task,
|
|
74
|
+
exitCode: 1,
|
|
75
|
+
usage: {
|
|
76
|
+
input: 0,
|
|
77
|
+
output: 0,
|
|
78
|
+
cacheRead: 0,
|
|
79
|
+
cacheWrite: 0,
|
|
80
|
+
cost: 0,
|
|
81
|
+
turns: 0,
|
|
82
|
+
} as SubagentUsage,
|
|
83
|
+
error: err instanceof Error ? err.message : String(err),
|
|
84
|
+
progressSummary: { toolCount: 0, tokens: 0, durationMs: Date.now() - startTime },
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Execute multiple subagents in parallel.
|
|
91
|
+
*/
|
|
92
|
+
export async function executeParallel(options: {
|
|
93
|
+
tasks: Array<{ agent?: string; task: string; count?: number; model?: string; cwd?: string }>;
|
|
94
|
+
signal?: AbortSignal;
|
|
95
|
+
onUpdate?: (progress: SubagentProgress[]) => void;
|
|
96
|
+
}): Promise<SubagentResult[]> {
|
|
97
|
+
const results = await Promise.all(
|
|
98
|
+
options.tasks.map((taskDef) =>
|
|
99
|
+
executeSubagent({
|
|
100
|
+
...taskDef,
|
|
101
|
+
signal: options.signal,
|
|
102
|
+
onUpdate: (progress: SubagentProgress) => {
|
|
103
|
+
// Collect all parallel progress updates
|
|
104
|
+
// This is a simplified approach — each task fires independently
|
|
105
|
+
options.onUpdate?.([progress]);
|
|
106
|
+
},
|
|
107
|
+
}),
|
|
108
|
+
),
|
|
109
|
+
);
|
|
110
|
+
return results;
|
|
111
|
+
}
|