@pi-archimedes/subagent 0.3.2 → 0.4.0
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 +2 -2
- package/src/compact.ts +44 -28
- package/src/execute.ts +18 -11
- package/src/expanded.ts +2 -0
- package/src/format.ts +7 -6
- package/src/handlers.ts +20 -14
- package/src/index.ts +12 -7
- package/src/render.ts +2 -2
- package/src/spawn.ts +11 -3
- package/src/types.ts +14 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-archimedes/subagent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package"
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
],
|
|
12
12
|
"main": "./src/index.ts",
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@pi-archimedes/core": "0.
|
|
14
|
+
"@pi-archimedes/core": "0.4.0"
|
|
15
15
|
},
|
|
16
16
|
"peerDependencies": {
|
|
17
17
|
"@earendil-works/pi-coding-agent": ">=0.1.0",
|
package/src/compact.ts
CHANGED
|
@@ -9,12 +9,12 @@ type RenderContext = { state: Record<string, unknown>; invalidate: () => void };
|
|
|
9
9
|
|
|
10
10
|
export function buildActivityLine(
|
|
11
11
|
progress: {
|
|
12
|
-
currentTool
|
|
13
|
-
currentToolArgs
|
|
14
|
-
currentToolStartedAt
|
|
15
|
-
finalOutput
|
|
16
|
-
status
|
|
17
|
-
error
|
|
12
|
+
currentTool: string | undefined;
|
|
13
|
+
currentToolArgs: string | undefined;
|
|
14
|
+
currentToolStartedAt: number | undefined;
|
|
15
|
+
finalOutput: string | undefined;
|
|
16
|
+
status: "running" | "completed" | "failed" | undefined;
|
|
17
|
+
error: string | undefined;
|
|
18
18
|
},
|
|
19
19
|
theme: Theme,
|
|
20
20
|
): string {
|
|
@@ -26,7 +26,7 @@ export function buildActivityLine(
|
|
|
26
26
|
|
|
27
27
|
if (progress.currentTool) {
|
|
28
28
|
const argsPreview = progress.currentToolArgs
|
|
29
|
-
? truncLine(progress.currentToolArgs, 60)
|
|
29
|
+
? truncLine(progress.currentToolArgs ?? "", 60)
|
|
30
30
|
: "";
|
|
31
31
|
const durationPart = progress.currentToolStartedAt
|
|
32
32
|
? " | " + formatDuration(Date.now() - progress.currentToolStartedAt)
|
|
@@ -51,6 +51,8 @@ export function buildActivityLine(
|
|
|
51
51
|
|
|
52
52
|
// ── Spinner helper ──────────────────────────────────────────────────────────
|
|
53
53
|
|
|
54
|
+
const SPINNER_INTERVAL_MS = 80;
|
|
55
|
+
|
|
54
56
|
function getSpinnerGlyph(agentName: string, isRunning: boolean, status: string, context: RenderContext): string {
|
|
55
57
|
if (isRunning) {
|
|
56
58
|
const timerKey = "_subagentTimer_" + agentName;
|
|
@@ -60,24 +62,33 @@ function getSpinnerGlyph(agentName: string, isRunning: boolean, status: string,
|
|
|
60
62
|
const timer = setInterval(() => {
|
|
61
63
|
context.state[frameKey] = ((context.state[frameKey] as number) + 1) % SPINNER_FRAMES.length;
|
|
62
64
|
context.invalidate();
|
|
63
|
-
},
|
|
65
|
+
}, SPINNER_INTERVAL_MS);
|
|
64
66
|
context.state[timerKey] = timer;
|
|
65
67
|
}
|
|
66
|
-
return SPINNER_FRAMES[context.state[frameKey] as number]
|
|
68
|
+
return SPINNER_FRAMES[context.state[frameKey] as number]!;
|
|
67
69
|
}
|
|
68
70
|
return status === "completed" ? "✓" : "✗";
|
|
69
71
|
}
|
|
70
72
|
|
|
71
|
-
function cleanupTimer(agentName: string, context: RenderContext) {
|
|
73
|
+
function cleanupTimer(agentName: string, context: RenderContext): void {
|
|
72
74
|
const timerKey = "_subagentTimer_" + agentName;
|
|
73
75
|
const frameKey = "_subagentFrame_" + agentName;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
const timer = context.state[timerKey] as ReturnType<typeof setInterval> | undefined;
|
|
77
|
+
if (timer) {
|
|
78
|
+
clearInterval(timer);
|
|
76
79
|
delete context.state[timerKey];
|
|
77
80
|
}
|
|
78
81
|
delete context.state[frameKey];
|
|
79
82
|
}
|
|
80
83
|
|
|
84
|
+
/** Cleanup ALL timers for a given agent — idempotent. */
|
|
85
|
+
function cleanupAllTimers(agentName: string, context: RenderContext): void {
|
|
86
|
+
cleanupTimer(agentName, context);
|
|
87
|
+
// Also clean up start time tracker
|
|
88
|
+
const timeKey = "_subagentStartTime_" + agentName;
|
|
89
|
+
delete context.state[timeKey];
|
|
90
|
+
}
|
|
91
|
+
|
|
81
92
|
// ── Compact single agent ────────────────────────────────────────────────────
|
|
82
93
|
|
|
83
94
|
export function renderCompactSingle(
|
|
@@ -92,23 +103,20 @@ export function renderCompactSingle(
|
|
|
92
103
|
const isRunning = progress?.status === "running";
|
|
93
104
|
const status = isRunning ? "running" : (result.exitCode === 0 ? "completed" : "failed");
|
|
94
105
|
|
|
95
|
-
// Manage timer for spinner
|
|
106
|
+
// Manage timer for spinner
|
|
96
107
|
if (isRunning) {
|
|
97
108
|
getSpinnerGlyph(agentName, true, "running", context);
|
|
98
109
|
} else {
|
|
99
|
-
|
|
110
|
+
cleanupAllTimers(agentName, context);
|
|
100
111
|
}
|
|
101
112
|
|
|
102
113
|
// Track start time for live duration
|
|
103
114
|
const timeKey = "_subagentStartTime_" + agentName;
|
|
104
|
-
if (isRunning &&
|
|
115
|
+
if (isRunning && context.state[timeKey] === undefined) {
|
|
105
116
|
// Estimate start time from current duration
|
|
106
117
|
const currentDuration = summary.durationMs || (progress?.durationMs ?? 0);
|
|
107
118
|
context.state[timeKey] = Date.now() - currentDuration;
|
|
108
119
|
}
|
|
109
|
-
if (!isRunning) {
|
|
110
|
-
delete context.state[timeKey];
|
|
111
|
-
}
|
|
112
120
|
const liveDuration = isRunning && context.state[timeKey]
|
|
113
121
|
? Date.now() - (context.state[timeKey] as number)
|
|
114
122
|
: summary.durationMs;
|
|
@@ -128,10 +136,10 @@ export function renderCompactSingle(
|
|
|
128
136
|
let activityLine: string;
|
|
129
137
|
if (isRunning) {
|
|
130
138
|
const spinner = SPINNER_FRAMES[(context.state["_subagentFrame_" + agentName] as number) ?? 0];
|
|
131
|
-
const spinnerColored = theme.fg("muted", spinner);
|
|
139
|
+
const spinnerColored = theme.fg("muted", spinner ?? "");
|
|
132
140
|
if (progress?.currentTool) {
|
|
133
141
|
const argsPreview = progress.currentToolArgs
|
|
134
|
-
? truncLine(progress.currentToolArgs, 60)
|
|
142
|
+
? truncLine(progress.currentToolArgs ?? "", 60)
|
|
135
143
|
: "";
|
|
136
144
|
const durationPart = progress.currentToolStartedAt
|
|
137
145
|
? " | " + formatDuration(Date.now() - progress.currentToolStartedAt)
|
|
@@ -146,7 +154,7 @@ export function renderCompactSingle(
|
|
|
146
154
|
activityLine = spinnerColored + " " + line;
|
|
147
155
|
} else if (progress?.toolCalls && progress.toolCalls.length > 0) {
|
|
148
156
|
const lastCall = progress.toolCalls[progress.toolCalls.length - 1];
|
|
149
|
-
activityLine = theme.fg("dim", " ⎿ ") + theme.fg("muted", lastCall);
|
|
157
|
+
activityLine = theme.fg("dim", " ⎿ ") + theme.fg("muted", lastCall ?? "");
|
|
150
158
|
} else {
|
|
151
159
|
activityLine = theme.fg("muted", " ⎿ Working...");
|
|
152
160
|
}
|
|
@@ -182,9 +190,17 @@ export function renderCompactParallel(
|
|
|
182
190
|
const progress = details.progress?.[i];
|
|
183
191
|
const agentName = result.agent ?? "agent-" + i;
|
|
184
192
|
const summary = result.progressSummary ?? { toolCount: 0, tokens: 0, durationMs: 0 };
|
|
193
|
+
const isRunning = progress?.status === "running";
|
|
185
194
|
const status = progress?.status ?? (result.exitCode === 0 ? "completed" : "failed");
|
|
186
195
|
|
|
187
|
-
|
|
196
|
+
// Use animated spinner for running agents
|
|
197
|
+
let glyph: string;
|
|
198
|
+
if (isRunning) {
|
|
199
|
+
glyph = getSpinnerGlyph(agentName, true, "running", context);
|
|
200
|
+
} else {
|
|
201
|
+
cleanupTimer(agentName, context);
|
|
202
|
+
glyph = status === "completed" ? "✓" : "✗";
|
|
203
|
+
}
|
|
188
204
|
const glyphColored = status === "completed"
|
|
189
205
|
? theme.fg("success", glyph)
|
|
190
206
|
: status === "failed"
|
|
@@ -243,13 +259,13 @@ export function renderCompactProgress(
|
|
|
243
259
|
|
|
244
260
|
// Track start time for live duration
|
|
245
261
|
const timeKey = "_subagentStartTime_" + agentName;
|
|
246
|
-
if (isRunning &&
|
|
262
|
+
if (isRunning && context.state[timeKey] === undefined) {
|
|
247
263
|
context.state[timeKey] = Date.now() - (progress.durationMs ?? 0);
|
|
248
264
|
}
|
|
249
265
|
if (!isRunning) {
|
|
250
|
-
|
|
266
|
+
cleanupAllTimers(agentName, context);
|
|
251
267
|
}
|
|
252
|
-
const liveDuration = isRunning && context.state[timeKey]
|
|
268
|
+
const liveDuration = isRunning && context.state[timeKey] !== undefined
|
|
253
269
|
? Date.now() - (context.state[timeKey] as number)
|
|
254
270
|
: progress.durationMs;
|
|
255
271
|
|
|
@@ -266,10 +282,10 @@ export function renderCompactProgress(
|
|
|
266
282
|
let activityLine: string;
|
|
267
283
|
if (isRunning) {
|
|
268
284
|
const spinner = SPINNER_FRAMES[(context.state["_subagentFrame_" + agentName] as number) ?? 0];
|
|
269
|
-
const spinnerColored = theme.fg("muted", spinner);
|
|
285
|
+
const spinnerColored = theme.fg("muted", spinner ?? "");
|
|
270
286
|
if (progress.currentTool) {
|
|
271
287
|
const argsPreview = progress.currentToolArgs
|
|
272
|
-
? truncLine(progress.currentToolArgs, 60)
|
|
288
|
+
? truncLine(progress.currentToolArgs ?? "", 60)
|
|
273
289
|
: "";
|
|
274
290
|
const durationPart = progress.currentToolStartedAt
|
|
275
291
|
? " | " + formatDuration(Date.now() - progress.currentToolStartedAt)
|
|
@@ -284,7 +300,7 @@ export function renderCompactProgress(
|
|
|
284
300
|
activityLine = spinnerColored + " " + line;
|
|
285
301
|
} else if (progress.toolCalls && progress.toolCalls.length > 0) {
|
|
286
302
|
const lastCall = progress.toolCalls[progress.toolCalls.length - 1];
|
|
287
|
-
activityLine = spinnerColored + " " + theme.fg("muted", lastCall);
|
|
303
|
+
activityLine = spinnerColored + " " + theme.fg("muted", lastCall ?? "");
|
|
288
304
|
} else {
|
|
289
305
|
activityLine = spinnerColored + " " + theme.fg("muted", "Working...");
|
|
290
306
|
}
|
package/src/execute.ts
CHANGED
|
@@ -4,12 +4,12 @@ import { emitCostUpdate } from "./cost.js";
|
|
|
4
4
|
import type { SubagentProgress, SubagentResult, SubagentUsage } from "./types.js";
|
|
5
5
|
|
|
6
6
|
export interface ExecuteOptions {
|
|
7
|
-
agent
|
|
7
|
+
agent: string | undefined;
|
|
8
8
|
task: string;
|
|
9
|
-
model
|
|
10
|
-
cwd
|
|
11
|
-
signal
|
|
12
|
-
onUpdate
|
|
9
|
+
model: string | undefined;
|
|
10
|
+
cwd: string | undefined;
|
|
11
|
+
signal: AbortSignal | undefined;
|
|
12
|
+
onUpdate: ((progress: SubagentProgress) => void) | undefined;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/**
|
|
@@ -80,7 +80,10 @@ export async function executeSubagent(options: ExecuteOptions): Promise<Subagent
|
|
|
80
80
|
cost: 0,
|
|
81
81
|
turns: 0,
|
|
82
82
|
} as SubagentUsage,
|
|
83
|
+
model: undefined,
|
|
84
|
+
finalOutput: undefined,
|
|
83
85
|
error: err instanceof Error ? err.message : String(err),
|
|
86
|
+
progress: undefined,
|
|
84
87
|
progressSummary: { toolCount: 0, tokens: 0, durationMs: Date.now() - startTime },
|
|
85
88
|
};
|
|
86
89
|
}
|
|
@@ -90,19 +93,23 @@ export async function executeSubagent(options: ExecuteOptions): Promise<Subagent
|
|
|
90
93
|
* Execute multiple subagents in parallel.
|
|
91
94
|
*/
|
|
92
95
|
export async function executeParallel(options: {
|
|
93
|
-
tasks: Array<{ agent
|
|
94
|
-
signal
|
|
95
|
-
onUpdate
|
|
96
|
+
tasks: Array<{ agent: string | undefined; task: string; model: string | undefined; cwd: string | undefined }>;
|
|
97
|
+
signal: AbortSignal | undefined;
|
|
98
|
+
onUpdate: ((progress: SubagentProgress[]) => void) | undefined;
|
|
96
99
|
}): Promise<SubagentResult[]> {
|
|
100
|
+
// Collect latest progress per agent for aggregated reporting
|
|
101
|
+
const latestProgress = new Map<string, SubagentProgress>();
|
|
102
|
+
|
|
97
103
|
const results = await Promise.all(
|
|
98
104
|
options.tasks.map((taskDef) =>
|
|
99
105
|
executeSubagent({
|
|
100
106
|
...taskDef,
|
|
101
107
|
signal: options.signal,
|
|
102
108
|
onUpdate: (progress: SubagentProgress) => {
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
109
|
+
// Store latest progress keyed by agent name
|
|
110
|
+
latestProgress.set(progress.agent, progress);
|
|
111
|
+
// Emit aggregated progress across ALL agents
|
|
112
|
+
options.onUpdate?.([...latestProgress.values()]);
|
|
106
113
|
},
|
|
107
114
|
}),
|
|
108
115
|
),
|
package/src/expanded.ts
CHANGED
|
@@ -88,6 +88,7 @@ export function renderProgressExpanded(
|
|
|
88
88
|
// Stats line (same as compact view)
|
|
89
89
|
const modelLabel = progress.model ? theme.fg("accent", progress.model) : "";
|
|
90
90
|
const statsLine = buildStatsLine({
|
|
91
|
+
turns: undefined,
|
|
91
92
|
toolCount: progress.toolCount,
|
|
92
93
|
tokens: progress.tokens,
|
|
93
94
|
durationMs: progress.durationMs,
|
|
@@ -160,6 +161,7 @@ export function buildProgressExpandedText(
|
|
|
160
161
|
// Stats line (same as compact view)
|
|
161
162
|
const modelLabel = progress.model ? theme.fg("accent", progress.model) : "";
|
|
162
163
|
const statsLine = buildStatsLine({
|
|
164
|
+
turns: undefined,
|
|
163
165
|
toolCount: progress.toolCount,
|
|
164
166
|
tokens: progress.tokens,
|
|
165
167
|
durationMs: progress.durationMs,
|
package/src/format.ts
CHANGED
|
@@ -9,7 +9,8 @@ export function formatTokens(n: number): string {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export function formatDuration(ms: number): string {
|
|
12
|
-
if (ms <
|
|
12
|
+
if (ms < 100) return "<0.1s";
|
|
13
|
+
if (ms < 1000) return (ms / 1000).toFixed(1) + "s";
|
|
13
14
|
const seconds = Math.floor(ms / 1000);
|
|
14
15
|
if (seconds < 60) return seconds + "s";
|
|
15
16
|
const minutes = Math.floor(seconds / 60);
|
|
@@ -28,11 +29,11 @@ export function truncLine(text: string, width: number): string {
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
export interface StatsData {
|
|
31
|
-
turns
|
|
32
|
-
toolCount
|
|
33
|
-
tokens
|
|
34
|
-
durationMs
|
|
35
|
-
cost
|
|
32
|
+
turns: number | undefined;
|
|
33
|
+
toolCount: number | undefined;
|
|
34
|
+
tokens: number | undefined;
|
|
35
|
+
durationMs: number | undefined;
|
|
36
|
+
cost: number | undefined;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
export interface StatsTheme {
|
package/src/handlers.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { StreamState } from "./types.js";
|
|
2
2
|
|
|
3
|
+
// Truncation limits for previews
|
|
4
|
+
const ARGS_PREVIEW_MAX = 120;
|
|
5
|
+
const TOOL_CALLS_MAX = 50;
|
|
6
|
+
const RECENT_OUTPUT_MAX = 50;
|
|
7
|
+
|
|
3
8
|
export interface JsonEvent {
|
|
4
9
|
type: string;
|
|
5
10
|
[key: string]: unknown;
|
|
@@ -10,14 +15,14 @@ export interface JsonEvent {
|
|
|
10
15
|
* Generic: no hardcoded tool names.
|
|
11
16
|
*/
|
|
12
17
|
export function extractArgsPreview(args: unknown): string {
|
|
13
|
-
if (typeof args === "string") return args.slice(0,
|
|
18
|
+
if (typeof args === "string") return args.slice(0, ARGS_PREVIEW_MAX);
|
|
14
19
|
if (args && typeof args === "object" && !Array.isArray(args)) {
|
|
15
20
|
const obj = args as Record<string, unknown>;
|
|
16
21
|
const keys = Object.keys(obj);
|
|
17
22
|
// Single-key object: just show the value
|
|
18
23
|
if (keys.length === 1) {
|
|
19
|
-
const v = obj[keys[0]];
|
|
20
|
-
if (typeof v === "string") return v.slice(0,
|
|
24
|
+
const v = obj[keys[0]!];
|
|
25
|
+
if (typeof v === "string") return v.slice(0, ARGS_PREVIEW_MAX);
|
|
21
26
|
if (typeof v === "number" || typeof v === "boolean") return String(v);
|
|
22
27
|
}
|
|
23
28
|
// Multi-key: find the longest string value (likely the main payload)
|
|
@@ -27,9 +32,10 @@ export function extractArgsPreview(args: unknown): string {
|
|
|
27
32
|
best = v;
|
|
28
33
|
}
|
|
29
34
|
}
|
|
30
|
-
if (best) return best.slice(0,
|
|
35
|
+
if (best) return best.slice(0, ARGS_PREVIEW_MAX);
|
|
31
36
|
}
|
|
32
|
-
|
|
37
|
+
const serialized = JSON.stringify(args);
|
|
38
|
+
return serialized?.slice(0, ARGS_PREVIEW_MAX) ?? "";
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
/**
|
|
@@ -43,8 +49,8 @@ export function handleToolStart(state: StreamState, event: JsonEvent): void {
|
|
|
43
49
|
// Record tool call with args preview
|
|
44
50
|
const argsPreview = extractArgsPreview(event.args);
|
|
45
51
|
state.toolCalls.push(`${state.currentTool}: ${argsPreview}`);
|
|
46
|
-
if (state.toolCalls.length >
|
|
47
|
-
state.toolCalls.splice(0, state.toolCalls.length -
|
|
52
|
+
if (state.toolCalls.length > TOOL_CALLS_MAX) {
|
|
53
|
+
state.toolCalls.splice(0, state.toolCalls.length - TOOL_CALLS_MAX);
|
|
48
54
|
}
|
|
49
55
|
}
|
|
50
56
|
|
|
@@ -69,20 +75,20 @@ export function handleToolResult(state: StreamState, event: JsonEvent): void {
|
|
|
69
75
|
|
|
70
76
|
if (typeof toolContent === "string" && toolContent.trim()) {
|
|
71
77
|
const lines = toolContent.split("\n").filter((l) => l.trim());
|
|
72
|
-
state.recentOutput.push(`[${toolName}] ${lines[0]?.slice(0,
|
|
78
|
+
state.recentOutput.push(`[${toolName}] ${lines[0]?.slice(0, ARGS_PREVIEW_MAX)}`);
|
|
73
79
|
} else if (Array.isArray(toolContent)) {
|
|
74
80
|
for (const part of toolContent) {
|
|
75
81
|
if (part.type === "text" && (part.text as string)?.trim()) {
|
|
76
82
|
const text = part.text as string;
|
|
77
83
|
const lines = text.split("\n").filter((l) => l.trim());
|
|
78
|
-
state.recentOutput.push(`[${toolName}] ${lines[0]?.slice(0,
|
|
84
|
+
state.recentOutput.push(`[${toolName}] ${lines[0]?.slice(0, ARGS_PREVIEW_MAX)}`);
|
|
79
85
|
break;
|
|
80
86
|
}
|
|
81
87
|
}
|
|
82
88
|
}
|
|
83
89
|
|
|
84
|
-
if (state.recentOutput.length >
|
|
85
|
-
state.recentOutput.splice(0, state.recentOutput.length -
|
|
90
|
+
if (state.recentOutput.length > RECENT_OUTPUT_MAX) {
|
|
91
|
+
state.recentOutput.splice(0, state.recentOutput.length - RECENT_OUTPUT_MAX);
|
|
86
92
|
}
|
|
87
93
|
}
|
|
88
94
|
|
|
@@ -115,9 +121,9 @@ export function handleMessageEnd(state: StreamState, event: JsonEvent): void {
|
|
|
115
121
|
}
|
|
116
122
|
}
|
|
117
123
|
|
|
118
|
-
// Cap recentOutput
|
|
119
|
-
if (state.recentOutput.length >
|
|
120
|
-
state.recentOutput.splice(0, state.recentOutput.length -
|
|
124
|
+
// Cap recentOutput
|
|
125
|
+
if (state.recentOutput.length > RECENT_OUTPUT_MAX) {
|
|
126
|
+
state.recentOutput.splice(0, state.recentOutput.length - RECENT_OUTPUT_MAX);
|
|
121
127
|
}
|
|
122
128
|
|
|
123
129
|
// Extract usage (turnCount tracked via turn_start in stream.ts)
|
package/src/index.ts
CHANGED
|
@@ -15,7 +15,6 @@ import type {
|
|
|
15
15
|
const TaskItem = Type.Object({
|
|
16
16
|
agent: Type.Optional(Type.String()),
|
|
17
17
|
task: Type.String(),
|
|
18
|
-
count: Type.Optional(Type.Number()),
|
|
19
18
|
model: Type.Optional(Type.String()),
|
|
20
19
|
cwd: Type.Optional(Type.String()),
|
|
21
20
|
});
|
|
@@ -75,8 +74,13 @@ export function registerSubagent(pi: ExtensionAPI): void {
|
|
|
75
74
|
// Parallel mode
|
|
76
75
|
if (params.tasks && params.tasks.length > 0) {
|
|
77
76
|
const results: SubagentResult[] = await executeParallel({
|
|
78
|
-
tasks: params.tasks
|
|
79
|
-
|
|
77
|
+
tasks: params.tasks.map((t) => ({
|
|
78
|
+
agent: t.agent ?? undefined,
|
|
79
|
+
task: t.task,
|
|
80
|
+
model: t.model ?? undefined,
|
|
81
|
+
cwd: t.cwd ?? undefined,
|
|
82
|
+
})),
|
|
83
|
+
signal: signal ?? undefined,
|
|
80
84
|
onUpdate: (progress: SubagentProgress[]) => {
|
|
81
85
|
onUpdate?.({
|
|
82
86
|
content: [],
|
|
@@ -102,11 +106,11 @@ export function registerSubagent(pi: ExtensionAPI): void {
|
|
|
102
106
|
// Single mode
|
|
103
107
|
if (params.task) {
|
|
104
108
|
const result: SubagentResult = await executeSubagent({
|
|
105
|
-
agent: params.agent,
|
|
109
|
+
agent: params.agent ?? undefined,
|
|
106
110
|
task: params.task,
|
|
107
|
-
model: params.model,
|
|
108
|
-
cwd: params.cwd,
|
|
109
|
-
signal,
|
|
111
|
+
model: params.model ?? undefined,
|
|
112
|
+
cwd: params.cwd ?? undefined,
|
|
113
|
+
signal: signal ?? undefined,
|
|
110
114
|
onUpdate: (progress: SubagentProgress) => {
|
|
111
115
|
onUpdate?.({
|
|
112
116
|
content: [],
|
|
@@ -135,6 +139,7 @@ export function registerSubagent(pi: ExtensionAPI): void {
|
|
|
135
139
|
details: {
|
|
136
140
|
mode: "single",
|
|
137
141
|
results: [],
|
|
142
|
+
progress: undefined,
|
|
138
143
|
},
|
|
139
144
|
isError: true,
|
|
140
145
|
};
|
package/src/render.ts
CHANGED
|
@@ -40,7 +40,7 @@ export function renderSubagentResult(
|
|
|
40
40
|
// ── Streaming progress (results empty but progress has data) ──────────
|
|
41
41
|
if (details.results.length === 0 && details.progress && details.progress.length > 0) {
|
|
42
42
|
if (details.progress.length === 1) {
|
|
43
|
-
return renderProgressUpdate(text, details.progress[0]
|
|
43
|
+
return renderProgressUpdate(text, details.progress[0]!, expanded, theme, context);
|
|
44
44
|
}
|
|
45
45
|
return renderProgressUpdatesParallel(text, details, expanded, theme, context);
|
|
46
46
|
}
|
|
@@ -52,7 +52,7 @@ export function renderSubagentResult(
|
|
|
52
52
|
|
|
53
53
|
// ── Single agent ────────────────────────────────────────────────────────
|
|
54
54
|
if (details.mode === "single" || details.results.length === 1) {
|
|
55
|
-
return renderSingleAgent(text, details.results[0]
|
|
55
|
+
return renderSingleAgent(text, details.results[0]!, details.progress?.[0], expanded, theme, context);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
// ── Parallel agents ─────────────────────────────────────────────────────
|
package/src/spawn.ts
CHANGED
|
@@ -4,9 +4,9 @@ import { existsSync } from "node:fs";
|
|
|
4
4
|
|
|
5
5
|
export interface SpawnOptions {
|
|
6
6
|
task: string;
|
|
7
|
-
model
|
|
8
|
-
cwd
|
|
9
|
-
signal
|
|
7
|
+
model: string | undefined;
|
|
8
|
+
cwd: string | undefined;
|
|
9
|
+
signal: AbortSignal | undefined;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -69,5 +69,13 @@ export function spawnSubagent(options: SpawnOptions): ChildProcess {
|
|
|
69
69
|
options.signal.addEventListener("abort", cleanup, { once: true });
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
// Clean up on normal exit to prevent forceKill firing after process ends
|
|
73
|
+
const exitCleanup = (): void => {
|
|
74
|
+
child.removeListener("exit", exitCleanup);
|
|
75
|
+
child.removeListener("error", exitCleanup);
|
|
76
|
+
};
|
|
77
|
+
child.on("exit", exitCleanup);
|
|
78
|
+
child.on("error", exitCleanup);
|
|
79
|
+
|
|
72
80
|
return child;
|
|
73
81
|
}
|
package/src/types.ts
CHANGED
|
@@ -11,24 +11,24 @@ export interface SubagentProgress {
|
|
|
11
11
|
agent: string;
|
|
12
12
|
status: "running" | "completed" | "failed";
|
|
13
13
|
task: string;
|
|
14
|
-
currentTool
|
|
15
|
-
currentToolArgs
|
|
16
|
-
currentToolStartedAt
|
|
14
|
+
currentTool: string | undefined;
|
|
15
|
+
currentToolArgs: string | undefined;
|
|
16
|
+
currentToolStartedAt: number | undefined;
|
|
17
17
|
toolCount: number;
|
|
18
18
|
inputTokens: number;
|
|
19
19
|
outputTokens: number;
|
|
20
20
|
tokens: number;
|
|
21
21
|
cost: number;
|
|
22
22
|
durationMs: number;
|
|
23
|
-
error
|
|
23
|
+
error: string | undefined;
|
|
24
24
|
/** Model used by the subagent */
|
|
25
|
-
model
|
|
25
|
+
model: string | undefined;
|
|
26
26
|
/** Accumulated assistant text output during streaming */
|
|
27
|
-
output
|
|
27
|
+
output: string | undefined;
|
|
28
28
|
/** Last N lines of assistant text for live display */
|
|
29
|
-
recentOutput
|
|
29
|
+
recentOutput: string[] | undefined;
|
|
30
30
|
/** History of tool calls: "toolName: args_preview" */
|
|
31
|
-
toolCalls
|
|
31
|
+
toolCalls: string[] | undefined;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
export interface SubagentResult {
|
|
@@ -36,11 +36,11 @@ export interface SubagentResult {
|
|
|
36
36
|
task: string;
|
|
37
37
|
exitCode: number;
|
|
38
38
|
usage: SubagentUsage;
|
|
39
|
-
model
|
|
40
|
-
finalOutput
|
|
41
|
-
error
|
|
42
|
-
progress
|
|
43
|
-
progressSummary
|
|
39
|
+
model: string | undefined;
|
|
40
|
+
finalOutput: string | undefined;
|
|
41
|
+
error: string | undefined;
|
|
42
|
+
progress: SubagentProgress | undefined;
|
|
43
|
+
progressSummary: { toolCount: number; tokens: number; durationMs: number } | undefined;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
export interface SubagentToolResult {
|
|
@@ -52,7 +52,7 @@ export interface SubagentToolResult {
|
|
|
52
52
|
export interface SubagentDetails {
|
|
53
53
|
mode: "single" | "parallel";
|
|
54
54
|
results: SubagentResult[];
|
|
55
|
-
progress
|
|
55
|
+
progress: SubagentProgress[] | undefined;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/** Mutable state during streaming — shared between stream.ts and handlers.ts */
|