@pi-archimedes/subagent 0.3.2 → 0.5.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/agents.ts +113 -0
- package/src/compact.ts +29 -65
- package/src/execute.ts +21 -11
- package/src/expanded.ts +5 -3
- package/src/format.ts +7 -8
- package/src/handlers.ts +20 -14
- package/src/index.ts +50 -12
- package/src/render.ts +2 -2
- package/src/spawn.ts +77 -8
- package/src/stream.ts +5 -0
- 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.5.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.5.0"
|
|
15
15
|
},
|
|
16
16
|
"peerDependencies": {
|
|
17
17
|
"@earendil-works/pi-coding-agent": ">=0.1.0",
|
package/src/agents.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent discovery and configuration loading.
|
|
3
|
+
* Reads agent definitions from ~/.pi/agent/agents/*.md and .pi/agents/*.md
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
|
|
10
|
+
export interface AgentConfig {
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
model?: string;
|
|
14
|
+
thinking?: string;
|
|
15
|
+
tools?: string[];
|
|
16
|
+
systemPrompt: string;
|
|
17
|
+
source: "user" | "project";
|
|
18
|
+
filePath: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig[] {
|
|
22
|
+
const agents: AgentConfig[] = [];
|
|
23
|
+
|
|
24
|
+
if (!fs.existsSync(dir)) return agents;
|
|
25
|
+
|
|
26
|
+
let entries: fs.Dirent[];
|
|
27
|
+
try {
|
|
28
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
29
|
+
} catch {
|
|
30
|
+
return agents;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
if (!entry.name.endsWith(".md")) continue;
|
|
35
|
+
if (!entry.isFile() && !entry.isSymbolicLink()) continue;
|
|
36
|
+
|
|
37
|
+
const filePath = path.join(dir, entry.name);
|
|
38
|
+
let content: string;
|
|
39
|
+
try {
|
|
40
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
41
|
+
} catch {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const { frontmatter, body } = parseFrontmatter<Record<string, unknown>>(content);
|
|
46
|
+
|
|
47
|
+
if (!frontmatter.name || !frontmatter.description) continue;
|
|
48
|
+
|
|
49
|
+
const tools = typeof frontmatter.tools === "string"
|
|
50
|
+
? frontmatter.tools.split(",").map((t: string) => t.trim()).filter(Boolean)
|
|
51
|
+
: undefined;
|
|
52
|
+
|
|
53
|
+
const config: AgentConfig = {
|
|
54
|
+
name: frontmatter.name as string,
|
|
55
|
+
description: frontmatter.description as string,
|
|
56
|
+
systemPrompt: body,
|
|
57
|
+
source,
|
|
58
|
+
filePath,
|
|
59
|
+
};
|
|
60
|
+
if (frontmatter.model) config.model = frontmatter.model as string;
|
|
61
|
+
if (frontmatter.thinking) config.thinking = frontmatter.thinking as string;
|
|
62
|
+
if (tools && tools.length > 0) config.tools = tools;
|
|
63
|
+
|
|
64
|
+
agents.push(config);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return agents;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isDirectory(p: string): boolean {
|
|
71
|
+
try {
|
|
72
|
+
return fs.statSync(p).isDirectory();
|
|
73
|
+
} catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function findNearestProjectAgentsDir(cwd: string): string | null {
|
|
79
|
+
let currentDir = cwd;
|
|
80
|
+
while (true) {
|
|
81
|
+
const candidate = path.join(currentDir, ".pi", "agents");
|
|
82
|
+
if (isDirectory(candidate)) return candidate;
|
|
83
|
+
|
|
84
|
+
const parentDir = path.dirname(currentDir);
|
|
85
|
+
if (parentDir === currentDir) return null;
|
|
86
|
+
currentDir = parentDir;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Discover all available agents from user and/or project directories.
|
|
92
|
+
*/
|
|
93
|
+
export function discoverAgents(cwd: string): AgentConfig[] {
|
|
94
|
+
const userDir = path.join(getAgentDir(), "agents");
|
|
95
|
+
const projectDir = findNearestProjectAgentsDir(cwd);
|
|
96
|
+
|
|
97
|
+
const userAgents = loadAgentsFromDir(userDir, "user");
|
|
98
|
+
const projectAgents = projectDir ? loadAgentsFromDir(projectDir, "project") : [];
|
|
99
|
+
|
|
100
|
+
// Project agents override user agents with the same name
|
|
101
|
+
const agentMap = new Map<string, AgentConfig>();
|
|
102
|
+
for (const agent of userAgents) agentMap.set(agent.name, agent);
|
|
103
|
+
for (const agent of projectAgents) agentMap.set(agent.name, agent);
|
|
104
|
+
|
|
105
|
+
return Array.from(agentMap.values());
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Look up an agent config by name.
|
|
110
|
+
*/
|
|
111
|
+
export function findAgent(agents: AgentConfig[], name: string): AgentConfig | undefined {
|
|
112
|
+
return agents.find((a) => a.name === name);
|
|
113
|
+
}
|
package/src/compact.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Text } from "@earendil-works/pi-tui";
|
|
2
2
|
import type { SubagentDetails, SubagentProgress, SubagentResult, SubagentToolResult } from "./types.js";
|
|
3
|
-
import {
|
|
3
|
+
import { formatTokens, formatDuration, formatCost, truncLine, buildStatsLine } from "./format.js";
|
|
4
4
|
|
|
5
5
|
type Theme = { fg: (token: string, text: string) => string; bold: (text: string) => string };
|
|
6
6
|
type RenderContext = { state: Record<string, unknown>; invalidate: () => void };
|
|
@@ -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)
|
|
@@ -49,35 +49,13 @@ export function buildActivityLine(
|
|
|
49
49
|
return "";
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
// ──
|
|
52
|
+
// ── Status glyph ────────────────────────────────────────────────────────────
|
|
53
53
|
|
|
54
|
-
function
|
|
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
|
-
}
|
|
54
|
+
function statusGlyph(isRunning: boolean, status: string): string {
|
|
55
|
+
if (isRunning) return "↳";
|
|
68
56
|
return status === "completed" ? "✓" : "✗";
|
|
69
57
|
}
|
|
70
58
|
|
|
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
59
|
// ── Compact single agent ────────────────────────────────────────────────────
|
|
82
60
|
|
|
83
61
|
export function renderCompactSingle(
|
|
@@ -92,23 +70,13 @@ export function renderCompactSingle(
|
|
|
92
70
|
const isRunning = progress?.status === "running";
|
|
93
71
|
const status = isRunning ? "running" : (result.exitCode === 0 ? "completed" : "failed");
|
|
94
72
|
|
|
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
73
|
// Track start time for live duration
|
|
103
74
|
const timeKey = "_subagentStartTime_" + agentName;
|
|
104
|
-
if (isRunning &&
|
|
75
|
+
if (isRunning && context.state[timeKey] === undefined) {
|
|
105
76
|
// Estimate start time from current duration
|
|
106
77
|
const currentDuration = summary.durationMs || (progress?.durationMs ?? 0);
|
|
107
78
|
context.state[timeKey] = Date.now() - currentDuration;
|
|
108
79
|
}
|
|
109
|
-
if (!isRunning) {
|
|
110
|
-
delete context.state[timeKey];
|
|
111
|
-
}
|
|
112
80
|
const liveDuration = isRunning && context.state[timeKey]
|
|
113
81
|
? Date.now() - (context.state[timeKey] as number)
|
|
114
82
|
: summary.durationMs;
|
|
@@ -124,14 +92,13 @@ export function renderCompactSingle(
|
|
|
124
92
|
|
|
125
93
|
const statsPart = statsLine ?? "";
|
|
126
94
|
|
|
127
|
-
// Activity:
|
|
95
|
+
// Activity: arrow + current tool if running, status if finished
|
|
128
96
|
let activityLine: string;
|
|
129
97
|
if (isRunning) {
|
|
130
|
-
const
|
|
131
|
-
const spinnerColored = theme.fg("muted", spinner);
|
|
98
|
+
const arrow = theme.fg("muted", "↳ ");
|
|
132
99
|
if (progress?.currentTool) {
|
|
133
100
|
const argsPreview = progress.currentToolArgs
|
|
134
|
-
? truncLine(progress.currentToolArgs, 60)
|
|
101
|
+
? truncLine(progress.currentToolArgs ?? "", 60)
|
|
135
102
|
: "";
|
|
136
103
|
const durationPart = progress.currentToolStartedAt
|
|
137
104
|
? " | " + formatDuration(Date.now() - progress.currentToolStartedAt)
|
|
@@ -143,10 +110,10 @@ export function renderCompactSingle(
|
|
|
143
110
|
if (durationPart) {
|
|
144
111
|
line += theme.fg("dim", durationPart);
|
|
145
112
|
}
|
|
146
|
-
activityLine =
|
|
113
|
+
activityLine = arrow + line;
|
|
147
114
|
} else if (progress?.toolCalls && progress.toolCalls.length > 0) {
|
|
148
115
|
const lastCall = progress.toolCalls[progress.toolCalls.length - 1];
|
|
149
|
-
activityLine = theme.fg("dim", " ⎿ ") + theme.fg("muted", lastCall);
|
|
116
|
+
activityLine = theme.fg("dim", " ⎿ ") + theme.fg("muted", lastCall ?? "");
|
|
150
117
|
} else {
|
|
151
118
|
activityLine = theme.fg("muted", " ⎿ Working...");
|
|
152
119
|
}
|
|
@@ -182,9 +149,10 @@ export function renderCompactParallel(
|
|
|
182
149
|
const progress = details.progress?.[i];
|
|
183
150
|
const agentName = result.agent ?? "agent-" + i;
|
|
184
151
|
const summary = result.progressSummary ?? { toolCount: 0, tokens: 0, durationMs: 0 };
|
|
152
|
+
const isRunning = progress?.status === "running";
|
|
185
153
|
const status = progress?.status ?? (result.exitCode === 0 ? "completed" : "failed");
|
|
186
154
|
|
|
187
|
-
|
|
155
|
+
const glyph = statusGlyph(isRunning, status);
|
|
188
156
|
const glyphColored = status === "completed"
|
|
189
157
|
? theme.fg("success", glyph)
|
|
190
158
|
: status === "failed"
|
|
@@ -234,7 +202,7 @@ export function renderCompactProgress(
|
|
|
234
202
|
const status = progress.status;
|
|
235
203
|
const isRunning = status === "running";
|
|
236
204
|
|
|
237
|
-
|
|
205
|
+
const glyph = statusGlyph(isRunning, status);
|
|
238
206
|
const glyphColored = status === "completed"
|
|
239
207
|
? theme.fg("success", glyph)
|
|
240
208
|
: status === "failed"
|
|
@@ -243,13 +211,10 @@ export function renderCompactProgress(
|
|
|
243
211
|
|
|
244
212
|
// Track start time for live duration
|
|
245
213
|
const timeKey = "_subagentStartTime_" + agentName;
|
|
246
|
-
if (isRunning &&
|
|
214
|
+
if (isRunning && context.state[timeKey] === undefined) {
|
|
247
215
|
context.state[timeKey] = Date.now() - (progress.durationMs ?? 0);
|
|
248
216
|
}
|
|
249
|
-
|
|
250
|
-
delete context.state[timeKey];
|
|
251
|
-
}
|
|
252
|
-
const liveDuration = isRunning && context.state[timeKey]
|
|
217
|
+
const liveDuration = isRunning && context.state[timeKey] !== undefined
|
|
253
218
|
? Date.now() - (context.state[timeKey] as number)
|
|
254
219
|
: progress.durationMs;
|
|
255
220
|
|
|
@@ -262,14 +227,13 @@ export function renderCompactProgress(
|
|
|
262
227
|
};
|
|
263
228
|
const statsLine = buildStatsLine(statsData, theme);
|
|
264
229
|
|
|
265
|
-
// Activity: current tool if running, status if finished
|
|
230
|
+
// Activity: arrow + current tool if running, status if finished
|
|
266
231
|
let activityLine: string;
|
|
267
232
|
if (isRunning) {
|
|
268
|
-
const
|
|
269
|
-
const spinnerColored = theme.fg("muted", spinner);
|
|
233
|
+
const arrow = theme.fg("muted", "↳ ");
|
|
270
234
|
if (progress.currentTool) {
|
|
271
235
|
const argsPreview = progress.currentToolArgs
|
|
272
|
-
? truncLine(progress.currentToolArgs, 60)
|
|
236
|
+
? truncLine(progress.currentToolArgs ?? "", 60)
|
|
273
237
|
: "";
|
|
274
238
|
const durationPart = progress.currentToolStartedAt
|
|
275
239
|
? " | " + formatDuration(Date.now() - progress.currentToolStartedAt)
|
|
@@ -281,12 +245,12 @@ export function renderCompactProgress(
|
|
|
281
245
|
if (durationPart) {
|
|
282
246
|
line += theme.fg("dim", durationPart);
|
|
283
247
|
}
|
|
284
|
-
activityLine =
|
|
248
|
+
activityLine = arrow + line;
|
|
285
249
|
} else if (progress.toolCalls && progress.toolCalls.length > 0) {
|
|
286
250
|
const lastCall = progress.toolCalls[progress.toolCalls.length - 1];
|
|
287
|
-
activityLine =
|
|
251
|
+
activityLine = arrow + theme.fg("muted", lastCall ?? "");
|
|
288
252
|
} else {
|
|
289
|
-
activityLine =
|
|
253
|
+
activityLine = arrow + theme.fg("muted", "Working...");
|
|
290
254
|
}
|
|
291
255
|
} else if (progress.error) {
|
|
292
256
|
activityLine = theme.fg("error", "✗ " + truncLine(progress.error, 80));
|
|
@@ -321,7 +285,7 @@ export function renderCompactParallelProgress(
|
|
|
321
285
|
const status = progress.status;
|
|
322
286
|
const isRunning = status === "running";
|
|
323
287
|
|
|
324
|
-
|
|
288
|
+
const glyph = statusGlyph(isRunning, status);
|
|
325
289
|
const glyphColored = status === "completed"
|
|
326
290
|
? theme.fg("success", glyph)
|
|
327
291
|
: status === "failed"
|
package/src/execute.ts
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { spawnSubagent } from "./spawn.js";
|
|
2
2
|
import { streamEvents } from "./stream.js";
|
|
3
3
|
import { emitCostUpdate } from "./cost.js";
|
|
4
|
+
import type { AgentConfig } from "./agents.js";
|
|
4
5
|
import type { SubagentProgress, SubagentResult, SubagentUsage } from "./types.js";
|
|
5
6
|
|
|
6
7
|
export interface ExecuteOptions {
|
|
7
|
-
agent
|
|
8
|
+
agent: string | undefined;
|
|
9
|
+
agentConfig: AgentConfig | undefined;
|
|
8
10
|
task: string;
|
|
9
|
-
model
|
|
10
|
-
cwd
|
|
11
|
-
signal
|
|
12
|
-
onUpdate
|
|
11
|
+
model: string | undefined;
|
|
12
|
+
cwd: string | undefined;
|
|
13
|
+
signal: AbortSignal | undefined;
|
|
14
|
+
onUpdate: ((progress: SubagentProgress) => void) | undefined;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
/**
|
|
@@ -24,6 +26,7 @@ export async function executeSubagent(options: ExecuteOptions): Promise<Subagent
|
|
|
24
26
|
model: options.model,
|
|
25
27
|
cwd: options.cwd,
|
|
26
28
|
signal: options.signal,
|
|
29
|
+
agent: options.agentConfig,
|
|
27
30
|
});
|
|
28
31
|
|
|
29
32
|
// Track previously emitted values to only emit deltas
|
|
@@ -80,7 +83,10 @@ export async function executeSubagent(options: ExecuteOptions): Promise<Subagent
|
|
|
80
83
|
cost: 0,
|
|
81
84
|
turns: 0,
|
|
82
85
|
} as SubagentUsage,
|
|
86
|
+
model: undefined,
|
|
87
|
+
finalOutput: undefined,
|
|
83
88
|
error: err instanceof Error ? err.message : String(err),
|
|
89
|
+
progress: undefined,
|
|
84
90
|
progressSummary: { toolCount: 0, tokens: 0, durationMs: Date.now() - startTime },
|
|
85
91
|
};
|
|
86
92
|
}
|
|
@@ -90,19 +96,23 @@ export async function executeSubagent(options: ExecuteOptions): Promise<Subagent
|
|
|
90
96
|
* Execute multiple subagents in parallel.
|
|
91
97
|
*/
|
|
92
98
|
export async function executeParallel(options: {
|
|
93
|
-
tasks: Array<{ agent
|
|
94
|
-
signal
|
|
95
|
-
onUpdate
|
|
99
|
+
tasks: Array<{ agent: string | undefined; agentConfig: AgentConfig | undefined; task: string; model: string | undefined; cwd: string | undefined }>;
|
|
100
|
+
signal: AbortSignal | undefined;
|
|
101
|
+
onUpdate: ((progress: SubagentProgress[]) => void) | undefined;
|
|
96
102
|
}): Promise<SubagentResult[]> {
|
|
103
|
+
// Collect latest progress per agent for aggregated reporting
|
|
104
|
+
const latestProgress = new Map<string, SubagentProgress>();
|
|
105
|
+
|
|
97
106
|
const results = await Promise.all(
|
|
98
107
|
options.tasks.map((taskDef) =>
|
|
99
108
|
executeSubagent({
|
|
100
109
|
...taskDef,
|
|
101
110
|
signal: options.signal,
|
|
102
111
|
onUpdate: (progress: SubagentProgress) => {
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
112
|
+
// Store latest progress keyed by agent name
|
|
113
|
+
latestProgress.set(progress.agent, progress);
|
|
114
|
+
// Emit aggregated progress across ALL agents
|
|
115
|
+
options.onUpdate?.([...latestProgress.values()]);
|
|
106
116
|
},
|
|
107
117
|
}),
|
|
108
118
|
),
|
package/src/expanded.ts
CHANGED
|
@@ -40,7 +40,7 @@ export function buildExpandedText(
|
|
|
40
40
|
if (toolCalls && toolCalls.length > 0) {
|
|
41
41
|
lines.push("");
|
|
42
42
|
for (const call of toolCalls) {
|
|
43
|
-
lines.push(theme.fg("dim", "
|
|
43
|
+
lines.push(theme.fg("dim", "↳ " + call));
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
@@ -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,
|
|
@@ -109,7 +110,7 @@ export function renderProgressExpanded(
|
|
|
109
110
|
if (progress.toolCalls && progress.toolCalls.length > 0) {
|
|
110
111
|
lines.push("");
|
|
111
112
|
for (const call of progress.toolCalls) {
|
|
112
|
-
lines.push(theme.fg("dim", "
|
|
113
|
+
lines.push(theme.fg("dim", "↳ " + call));
|
|
113
114
|
}
|
|
114
115
|
}
|
|
115
116
|
|
|
@@ -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,
|
|
@@ -181,7 +183,7 @@ export function buildProgressExpandedText(
|
|
|
181
183
|
if (progress.toolCalls && progress.toolCalls.length > 0) {
|
|
182
184
|
lines.push("");
|
|
183
185
|
for (const call of progress.toolCalls) {
|
|
184
|
-
lines.push(theme.fg("dim", "
|
|
186
|
+
lines.push(theme.fg("dim", "↳ " + call));
|
|
185
187
|
}
|
|
186
188
|
}
|
|
187
189
|
|
package/src/format.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { clampLine } from "@pi-archimedes/core/text";
|
|
2
2
|
|
|
3
|
-
export const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
4
|
-
|
|
5
3
|
export function formatTokens(n: number): string {
|
|
6
4
|
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + "M";
|
|
7
5
|
if (n >= 1_000) return Math.round(n / 1_000) + "k";
|
|
@@ -9,7 +7,8 @@ export function formatTokens(n: number): string {
|
|
|
9
7
|
}
|
|
10
8
|
|
|
11
9
|
export function formatDuration(ms: number): string {
|
|
12
|
-
if (ms <
|
|
10
|
+
if (ms < 100) return "<0.1s";
|
|
11
|
+
if (ms < 1000) return (ms / 1000).toFixed(1) + "s";
|
|
13
12
|
const seconds = Math.floor(ms / 1000);
|
|
14
13
|
if (seconds < 60) return seconds + "s";
|
|
15
14
|
const minutes = Math.floor(seconds / 60);
|
|
@@ -28,11 +27,11 @@ export function truncLine(text: string, width: number): string {
|
|
|
28
27
|
}
|
|
29
28
|
|
|
30
29
|
export interface StatsData {
|
|
31
|
-
turns
|
|
32
|
-
toolCount
|
|
33
|
-
tokens
|
|
34
|
-
durationMs
|
|
35
|
-
cost
|
|
30
|
+
turns: number | undefined;
|
|
31
|
+
toolCount: number | undefined;
|
|
32
|
+
tokens: number | undefined;
|
|
33
|
+
durationMs: number | undefined;
|
|
34
|
+
cost: number | undefined;
|
|
36
35
|
}
|
|
37
36
|
|
|
38
37
|
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
|
@@ -3,6 +3,7 @@ import { Text } from "@earendil-works/pi-tui";
|
|
|
3
3
|
import { Type } from "typebox";
|
|
4
4
|
import { executeSubagent, executeParallel } from "./execute.js";
|
|
5
5
|
import { renderSubagentResult } from "./render.js";
|
|
6
|
+
import { discoverAgents, findAgent } from "./agents.js";
|
|
6
7
|
import type {
|
|
7
8
|
SubagentDetails,
|
|
8
9
|
SubagentProgress,
|
|
@@ -15,7 +16,6 @@ import type {
|
|
|
15
16
|
const TaskItem = Type.Object({
|
|
16
17
|
agent: Type.Optional(Type.String()),
|
|
17
18
|
task: Type.String(),
|
|
18
|
-
count: Type.Optional(Type.Number()),
|
|
19
19
|
model: Type.Optional(Type.String()),
|
|
20
20
|
cwd: Type.Optional(Type.String()),
|
|
21
21
|
});
|
|
@@ -25,13 +25,13 @@ const SUBAGENT_PARAMS_SCHEMA = Type.Object({
|
|
|
25
25
|
description: "Agent name/identifier (optional, defaults to 'general')",
|
|
26
26
|
})),
|
|
27
27
|
task: Type.Optional(Type.String({
|
|
28
|
-
description: "Task description for the subagent",
|
|
28
|
+
description: "Task description for the subagent. Required when not using 'tasks' array.",
|
|
29
29
|
})),
|
|
30
30
|
tasks: Type.Optional(Type.Array(TaskItem, {
|
|
31
|
-
description: "Multiple tasks for parallel execution",
|
|
31
|
+
description: "Multiple tasks for parallel execution. Required when not using 'task'.",
|
|
32
32
|
})),
|
|
33
33
|
model: Type.Optional(Type.String({
|
|
34
|
-
description: "Model override
|
|
34
|
+
description: "Model override for the subagent",
|
|
35
35
|
})),
|
|
36
36
|
async: Type.Optional(Type.Boolean({
|
|
37
37
|
description: "Run asynchronously (fire-and-forget)",
|
|
@@ -55,7 +55,7 @@ export function registerSubagent(pi: ExtensionAPI): void {
|
|
|
55
55
|
name: "subagent",
|
|
56
56
|
label: "Subagent",
|
|
57
57
|
description:
|
|
58
|
-
"Delegate tasks to subagents.
|
|
58
|
+
"Delegate tasks to subagents. Provide either 'task' (single) or 'tasks' (parallel). Never omit both. Options: agent, model, cwd.",
|
|
59
59
|
parameters: SUBAGENT_PARAMS_SCHEMA,
|
|
60
60
|
|
|
61
61
|
async execute(
|
|
@@ -70,13 +70,36 @@ export function registerSubagent(pi: ExtensionAPI): void {
|
|
|
70
70
|
},
|
|
71
71
|
signal: AbortSignal | undefined,
|
|
72
72
|
onUpdate: ((update: SubagentToolResult) => void) | undefined,
|
|
73
|
-
|
|
73
|
+
ctx: ExtensionContext,
|
|
74
74
|
): Promise<SubagentToolResult> {
|
|
75
|
+
// Discover available agents
|
|
76
|
+
const agents = discoverAgents(ctx.cwd);
|
|
77
|
+
|
|
75
78
|
// Parallel mode
|
|
76
79
|
if (params.tasks && params.tasks.length > 0) {
|
|
80
|
+
const missingAgents = params.tasks.filter((t) => t.agent && !findAgent(agents, t.agent));
|
|
81
|
+
if (missingAgents.length > 0) {
|
|
82
|
+
const available = agents.map((a) => a.name).join(", ") || "none";
|
|
83
|
+
const unknown = missingAgents.map((t) => `"${t.agent}"`).join(", ");
|
|
84
|
+
return {
|
|
85
|
+
content: [{ type: "text", text: `Unknown agent(s): ${unknown}. Available: ${available}` }],
|
|
86
|
+
details: {
|
|
87
|
+
mode: "parallel",
|
|
88
|
+
results: [],
|
|
89
|
+
progress: undefined,
|
|
90
|
+
},
|
|
91
|
+
isError: true,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
77
94
|
const results: SubagentResult[] = await executeParallel({
|
|
78
|
-
tasks: params.tasks
|
|
79
|
-
|
|
95
|
+
tasks: params.tasks.map((t) => ({
|
|
96
|
+
agent: t.agent ?? undefined,
|
|
97
|
+
agentConfig: t.agent ? findAgent(agents, t.agent) : undefined,
|
|
98
|
+
task: t.task,
|
|
99
|
+
model: t.model ?? undefined,
|
|
100
|
+
cwd: t.cwd ?? undefined,
|
|
101
|
+
})),
|
|
102
|
+
signal: signal ?? undefined,
|
|
80
103
|
onUpdate: (progress: SubagentProgress[]) => {
|
|
81
104
|
onUpdate?.({
|
|
82
105
|
content: [],
|
|
@@ -101,12 +124,26 @@ export function registerSubagent(pi: ExtensionAPI): void {
|
|
|
101
124
|
|
|
102
125
|
// Single mode
|
|
103
126
|
if (params.task) {
|
|
127
|
+
let agentConfig = params.agent ? findAgent(agents, params.agent) : undefined;
|
|
128
|
+
if (params.agent && !agentConfig) {
|
|
129
|
+
const available = agents.map((a) => a.name).join(", ") || "none";
|
|
130
|
+
return {
|
|
131
|
+
content: [{ type: "text", text: `Unknown agent: "${params.agent}". Available: ${available}` }],
|
|
132
|
+
details: {
|
|
133
|
+
mode: "single",
|
|
134
|
+
results: [],
|
|
135
|
+
progress: undefined,
|
|
136
|
+
},
|
|
137
|
+
isError: true,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
104
140
|
const result: SubagentResult = await executeSubagent({
|
|
105
|
-
agent: params.agent,
|
|
141
|
+
agent: params.agent ?? undefined,
|
|
142
|
+
agentConfig,
|
|
106
143
|
task: params.task,
|
|
107
|
-
model: params.model,
|
|
108
|
-
cwd: params.cwd,
|
|
109
|
-
signal,
|
|
144
|
+
model: params.model ?? undefined,
|
|
145
|
+
cwd: params.cwd ?? undefined,
|
|
146
|
+
signal: signal ?? undefined,
|
|
110
147
|
onUpdate: (progress: SubagentProgress) => {
|
|
111
148
|
onUpdate?.({
|
|
112
149
|
content: [],
|
|
@@ -135,6 +172,7 @@ export function registerSubagent(pi: ExtensionAPI): void {
|
|
|
135
172
|
details: {
|
|
136
173
|
mode: "single",
|
|
137
174
|
results: [],
|
|
175
|
+
progress: undefined,
|
|
138
176
|
},
|
|
139
177
|
isError: true,
|
|
140
178
|
};
|
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
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { spawn, type ChildProcess } from "node:child_process";
|
|
2
2
|
import { execSync } from "node:child_process";
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
3
|
+
import { existsSync, writeFileSync, unlinkSync, mkdtempSync, rmdirSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import type { AgentConfig } from "./agents.js";
|
|
4
7
|
|
|
5
8
|
export interface SpawnOptions {
|
|
6
9
|
task: string;
|
|
7
|
-
model
|
|
8
|
-
cwd
|
|
9
|
-
signal
|
|
10
|
+
model: string | undefined;
|
|
11
|
+
cwd: string | undefined;
|
|
12
|
+
signal: AbortSignal | undefined;
|
|
13
|
+
agent: AgentConfig | undefined;
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
/**
|
|
@@ -32,6 +36,29 @@ export function resolvePiCommand(): { command: string; args: string[] } {
|
|
|
32
36
|
return { command: piPath, args: [] };
|
|
33
37
|
}
|
|
34
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Write system prompt to a temp file for --append-system-prompt.
|
|
41
|
+
*/
|
|
42
|
+
function writePromptToFile(agentName: string, prompt: string): { dir: string; filePath: string } {
|
|
43
|
+
const safeName = agentName.replace(/[^\w.-]+/g, "_");
|
|
44
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "pi-subagent-"));
|
|
45
|
+
const filePath = join(tmpDir, `prompt-${safeName}.md`);
|
|
46
|
+
writeFileSync(filePath, prompt, { encoding: "utf-8" });
|
|
47
|
+
return { dir: tmpDir, filePath };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Clean up temp prompt file and directory.
|
|
52
|
+
*/
|
|
53
|
+
function cleanupTempFiles(dir: string | null, filePath: string | null): void {
|
|
54
|
+
if (filePath) {
|
|
55
|
+
try { unlinkSync(filePath); } catch { /* ignore */ }
|
|
56
|
+
}
|
|
57
|
+
if (dir) {
|
|
58
|
+
try { rmdirSync(dir); } catch { /* ignore */ }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
35
62
|
/**
|
|
36
63
|
* Spawn a child `pi` process in JSON mode.
|
|
37
64
|
*/
|
|
@@ -40,13 +67,42 @@ export function spawnSubagent(options: SpawnOptions): ChildProcess {
|
|
|
40
67
|
const args: string[] = [
|
|
41
68
|
...baseArgs,
|
|
42
69
|
"--mode", "json",
|
|
43
|
-
"-p",
|
|
70
|
+
"-p",
|
|
71
|
+
"--no-session",
|
|
44
72
|
];
|
|
45
73
|
|
|
46
|
-
if
|
|
74
|
+
// Use agent config from frontmatter if available
|
|
75
|
+
if (options.agent) {
|
|
76
|
+
const agent = options.agent;
|
|
77
|
+
// Agent's model takes precedence; tool-call model as fallback
|
|
78
|
+
const modelToUse = agent.model ?? options.model;
|
|
79
|
+
if (modelToUse) {
|
|
80
|
+
args.push("--model", modelToUse);
|
|
81
|
+
}
|
|
82
|
+
if (agent.thinking) {
|
|
83
|
+
args.push("--thinking", agent.thinking);
|
|
84
|
+
}
|
|
85
|
+
if (agent.tools && agent.tools.length > 0) {
|
|
86
|
+
args.push("--tools", agent.tools.join(","));
|
|
87
|
+
}
|
|
88
|
+
} else if (options.model) {
|
|
89
|
+
// Fallback: use model from tool call params (legacy)
|
|
47
90
|
args.push("--model", options.model);
|
|
48
91
|
}
|
|
49
92
|
|
|
93
|
+
// Temp files for system prompt
|
|
94
|
+
let tmpDir: string | null = null;
|
|
95
|
+
let tmpPath: string | null = null;
|
|
96
|
+
|
|
97
|
+
if (options.agent && options.agent.systemPrompt.trim()) {
|
|
98
|
+
const tmp = writePromptToFile(options.agent.name, options.agent.systemPrompt);
|
|
99
|
+
tmpDir = tmp.dir;
|
|
100
|
+
tmpPath = tmp.filePath;
|
|
101
|
+
args.push("--append-system-prompt", tmpPath);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
args.push(options.task);
|
|
105
|
+
|
|
50
106
|
const child = spawn(command, args, {
|
|
51
107
|
cwd: options.cwd || process.cwd(),
|
|
52
108
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -54,8 +110,9 @@ export function spawnSubagent(options: SpawnOptions): ChildProcess {
|
|
|
54
110
|
});
|
|
55
111
|
|
|
56
112
|
// Handle abort signal
|
|
113
|
+
let abortHandler: (() => void) | undefined;
|
|
57
114
|
if (options.signal) {
|
|
58
|
-
|
|
115
|
+
abortHandler = () => {
|
|
59
116
|
if (child.pid && !child.killed) {
|
|
60
117
|
child.kill("SIGTERM");
|
|
61
118
|
const forceKill = setTimeout(() => {
|
|
@@ -66,8 +123,20 @@ export function spawnSubagent(options: SpawnOptions): ChildProcess {
|
|
|
66
123
|
forceKill.unref();
|
|
67
124
|
}
|
|
68
125
|
};
|
|
69
|
-
options.signal.addEventListener("abort",
|
|
126
|
+
options.signal.addEventListener("abort", abortHandler, { once: true });
|
|
70
127
|
}
|
|
71
128
|
|
|
129
|
+
// Clean up temp files and listeners on exit
|
|
130
|
+
const exitCleanup = (): void => {
|
|
131
|
+
child.removeListener("exit", exitCleanup);
|
|
132
|
+
child.removeListener("error", exitCleanup);
|
|
133
|
+
if (abortHandler && options.signal) {
|
|
134
|
+
options.signal.removeEventListener("abort", abortHandler);
|
|
135
|
+
}
|
|
136
|
+
cleanupTempFiles(tmpDir, tmpPath);
|
|
137
|
+
};
|
|
138
|
+
child.on("exit", exitCleanup);
|
|
139
|
+
child.on("error", exitCleanup);
|
|
140
|
+
|
|
72
141
|
return child;
|
|
73
142
|
}
|
package/src/stream.ts
CHANGED
|
@@ -74,6 +74,9 @@ export function streamEvents(
|
|
|
74
74
|
callbacks.onProgress?.(buildProgress());
|
|
75
75
|
};
|
|
76
76
|
|
|
77
|
+
// Periodic progress updates for live duration display
|
|
78
|
+
const heartbeat = setInterval(emitProgress, 1000);
|
|
79
|
+
|
|
77
80
|
// Collect stderr
|
|
78
81
|
const stderrParts: string[] = [];
|
|
79
82
|
child.stderr?.on("data", (data: Buffer) => {
|
|
@@ -129,6 +132,7 @@ export function streamEvents(
|
|
|
129
132
|
// Handle process exit
|
|
130
133
|
child.on("close", (code) => {
|
|
131
134
|
clearTimeout(timeout);
|
|
135
|
+
clearInterval(heartbeat);
|
|
132
136
|
const durationMs = Date.now() - startTime;
|
|
133
137
|
const exitCode = code ?? 1;
|
|
134
138
|
|
|
@@ -171,6 +175,7 @@ export function streamEvents(
|
|
|
171
175
|
|
|
172
176
|
child.on("error", (err) => {
|
|
173
177
|
clearTimeout(timeout);
|
|
178
|
+
clearInterval(heartbeat);
|
|
174
179
|
reject(err);
|
|
175
180
|
});
|
|
176
181
|
});
|
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 */
|