@pi-archimedes/subagent 0.4.0 → 0.6.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 +15 -67
- package/src/execute.ts +4 -1
- package/src/expanded.ts +3 -3
- package/src/format.ts +0 -2
- package/src/index.ts +38 -5
- package/src/spawn.ts +67 -6
- package/src/stream.ts +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-archimedes/subagent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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.6.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 };
|
|
@@ -49,46 +49,13 @@ export function buildActivityLine(
|
|
|
49
49
|
return "";
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
// ──
|
|
52
|
+
// ── Status glyph ────────────────────────────────────────────────────────────
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
function getSpinnerGlyph(agentName: string, isRunning: boolean, status: string, context: RenderContext): string {
|
|
57
|
-
if (isRunning) {
|
|
58
|
-
const timerKey = "_subagentTimer_" + agentName;
|
|
59
|
-
const frameKey = "_subagentFrame_" + agentName;
|
|
60
|
-
if (!context.state[timerKey]) {
|
|
61
|
-
context.state[frameKey] = 0;
|
|
62
|
-
const timer = setInterval(() => {
|
|
63
|
-
context.state[frameKey] = ((context.state[frameKey] as number) + 1) % SPINNER_FRAMES.length;
|
|
64
|
-
context.invalidate();
|
|
65
|
-
}, SPINNER_INTERVAL_MS);
|
|
66
|
-
context.state[timerKey] = timer;
|
|
67
|
-
}
|
|
68
|
-
return SPINNER_FRAMES[context.state[frameKey] as number]!;
|
|
69
|
-
}
|
|
54
|
+
function statusGlyph(isRunning: boolean, status: string): string {
|
|
55
|
+
if (isRunning) return "↳";
|
|
70
56
|
return status === "completed" ? "✓" : "✗";
|
|
71
57
|
}
|
|
72
58
|
|
|
73
|
-
function cleanupTimer(agentName: string, context: RenderContext): void {
|
|
74
|
-
const timerKey = "_subagentTimer_" + agentName;
|
|
75
|
-
const frameKey = "_subagentFrame_" + agentName;
|
|
76
|
-
const timer = context.state[timerKey] as ReturnType<typeof setInterval> | undefined;
|
|
77
|
-
if (timer) {
|
|
78
|
-
clearInterval(timer);
|
|
79
|
-
delete context.state[timerKey];
|
|
80
|
-
}
|
|
81
|
-
delete context.state[frameKey];
|
|
82
|
-
}
|
|
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
|
-
|
|
92
59
|
// ── Compact single agent ────────────────────────────────────────────────────
|
|
93
60
|
|
|
94
61
|
export function renderCompactSingle(
|
|
@@ -103,13 +70,6 @@ export function renderCompactSingle(
|
|
|
103
70
|
const isRunning = progress?.status === "running";
|
|
104
71
|
const status = isRunning ? "running" : (result.exitCode === 0 ? "completed" : "failed");
|
|
105
72
|
|
|
106
|
-
// Manage timer for spinner
|
|
107
|
-
if (isRunning) {
|
|
108
|
-
getSpinnerGlyph(agentName, true, "running", context);
|
|
109
|
-
} else {
|
|
110
|
-
cleanupAllTimers(agentName, context);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
73
|
// Track start time for live duration
|
|
114
74
|
const timeKey = "_subagentStartTime_" + agentName;
|
|
115
75
|
if (isRunning && context.state[timeKey] === undefined) {
|
|
@@ -132,11 +92,10 @@ export function renderCompactSingle(
|
|
|
132
92
|
|
|
133
93
|
const statsPart = statsLine ?? "";
|
|
134
94
|
|
|
135
|
-
// Activity:
|
|
95
|
+
// Activity: arrow + current tool if running, status if finished
|
|
136
96
|
let activityLine: string;
|
|
137
97
|
if (isRunning) {
|
|
138
|
-
const
|
|
139
|
-
const spinnerColored = theme.fg("muted", spinner ?? "");
|
|
98
|
+
const arrow = theme.fg("muted", "↳ ");
|
|
140
99
|
if (progress?.currentTool) {
|
|
141
100
|
const argsPreview = progress.currentToolArgs
|
|
142
101
|
? truncLine(progress.currentToolArgs ?? "", 60)
|
|
@@ -151,7 +110,7 @@ export function renderCompactSingle(
|
|
|
151
110
|
if (durationPart) {
|
|
152
111
|
line += theme.fg("dim", durationPart);
|
|
153
112
|
}
|
|
154
|
-
activityLine =
|
|
113
|
+
activityLine = arrow + line;
|
|
155
114
|
} else if (progress?.toolCalls && progress.toolCalls.length > 0) {
|
|
156
115
|
const lastCall = progress.toolCalls[progress.toolCalls.length - 1];
|
|
157
116
|
activityLine = theme.fg("dim", " ⎿ ") + theme.fg("muted", lastCall ?? "");
|
|
@@ -193,14 +152,7 @@ export function renderCompactParallel(
|
|
|
193
152
|
const isRunning = progress?.status === "running";
|
|
194
153
|
const status = progress?.status ?? (result.exitCode === 0 ? "completed" : "failed");
|
|
195
154
|
|
|
196
|
-
|
|
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
|
-
}
|
|
155
|
+
const glyph = statusGlyph(isRunning, status);
|
|
204
156
|
const glyphColored = status === "completed"
|
|
205
157
|
? theme.fg("success", glyph)
|
|
206
158
|
: status === "failed"
|
|
@@ -250,7 +202,7 @@ export function renderCompactProgress(
|
|
|
250
202
|
const status = progress.status;
|
|
251
203
|
const isRunning = status === "running";
|
|
252
204
|
|
|
253
|
-
|
|
205
|
+
const glyph = statusGlyph(isRunning, status);
|
|
254
206
|
const glyphColored = status === "completed"
|
|
255
207
|
? theme.fg("success", glyph)
|
|
256
208
|
: status === "failed"
|
|
@@ -262,9 +214,6 @@ export function renderCompactProgress(
|
|
|
262
214
|
if (isRunning && context.state[timeKey] === undefined) {
|
|
263
215
|
context.state[timeKey] = Date.now() - (progress.durationMs ?? 0);
|
|
264
216
|
}
|
|
265
|
-
if (!isRunning) {
|
|
266
|
-
cleanupAllTimers(agentName, context);
|
|
267
|
-
}
|
|
268
217
|
const liveDuration = isRunning && context.state[timeKey] !== undefined
|
|
269
218
|
? Date.now() - (context.state[timeKey] as number)
|
|
270
219
|
: progress.durationMs;
|
|
@@ -278,11 +227,10 @@ export function renderCompactProgress(
|
|
|
278
227
|
};
|
|
279
228
|
const statsLine = buildStatsLine(statsData, theme);
|
|
280
229
|
|
|
281
|
-
// Activity: current tool if running, status if finished
|
|
230
|
+
// Activity: arrow + current tool if running, status if finished
|
|
282
231
|
let activityLine: string;
|
|
283
232
|
if (isRunning) {
|
|
284
|
-
const
|
|
285
|
-
const spinnerColored = theme.fg("muted", spinner ?? "");
|
|
233
|
+
const arrow = theme.fg("muted", "↳ ");
|
|
286
234
|
if (progress.currentTool) {
|
|
287
235
|
const argsPreview = progress.currentToolArgs
|
|
288
236
|
? truncLine(progress.currentToolArgs ?? "", 60)
|
|
@@ -297,12 +245,12 @@ export function renderCompactProgress(
|
|
|
297
245
|
if (durationPart) {
|
|
298
246
|
line += theme.fg("dim", durationPart);
|
|
299
247
|
}
|
|
300
|
-
activityLine =
|
|
248
|
+
activityLine = arrow + line;
|
|
301
249
|
} else if (progress.toolCalls && progress.toolCalls.length > 0) {
|
|
302
250
|
const lastCall = progress.toolCalls[progress.toolCalls.length - 1];
|
|
303
|
-
activityLine =
|
|
251
|
+
activityLine = arrow + theme.fg("muted", lastCall ?? "");
|
|
304
252
|
} else {
|
|
305
|
-
activityLine =
|
|
253
|
+
activityLine = arrow + theme.fg("muted", "Working...");
|
|
306
254
|
}
|
|
307
255
|
} else if (progress.error) {
|
|
308
256
|
activityLine = theme.fg("error", "✗ " + truncLine(progress.error, 80));
|
|
@@ -337,7 +285,7 @@ export function renderCompactParallelProgress(
|
|
|
337
285
|
const status = progress.status;
|
|
338
286
|
const isRunning = status === "running";
|
|
339
287
|
|
|
340
|
-
|
|
288
|
+
const glyph = statusGlyph(isRunning, status);
|
|
341
289
|
const glyphColored = status === "completed"
|
|
342
290
|
? theme.fg("success", glyph)
|
|
343
291
|
: status === "failed"
|
package/src/execute.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
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
8
|
agent: string | undefined;
|
|
9
|
+
agentConfig: AgentConfig | undefined;
|
|
8
10
|
task: string;
|
|
9
11
|
model: string | undefined;
|
|
10
12
|
cwd: string | undefined;
|
|
@@ -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
|
|
@@ -93,7 +96,7 @@ export async function executeSubagent(options: ExecuteOptions): Promise<Subagent
|
|
|
93
96
|
* Execute multiple subagents in parallel.
|
|
94
97
|
*/
|
|
95
98
|
export async function executeParallel(options: {
|
|
96
|
-
tasks: Array<{ agent: string | undefined; task: string; model: string | undefined; cwd: string | undefined }>;
|
|
99
|
+
tasks: Array<{ agent: string | undefined; agentConfig: AgentConfig | undefined; task: string; model: string | undefined; cwd: string | undefined }>;
|
|
97
100
|
signal: AbortSignal | undefined;
|
|
98
101
|
onUpdate: ((progress: SubagentProgress[]) => void) | undefined;
|
|
99
102
|
}): Promise<SubagentResult[]> {
|
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
|
|
|
@@ -110,7 +110,7 @@ export function renderProgressExpanded(
|
|
|
110
110
|
if (progress.toolCalls && progress.toolCalls.length > 0) {
|
|
111
111
|
lines.push("");
|
|
112
112
|
for (const call of progress.toolCalls) {
|
|
113
|
-
lines.push(theme.fg("dim", "
|
|
113
|
+
lines.push(theme.fg("dim", "↳ " + call));
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
|
|
@@ -183,7 +183,7 @@ export function buildProgressExpandedText(
|
|
|
183
183
|
if (progress.toolCalls && progress.toolCalls.length > 0) {
|
|
184
184
|
lines.push("");
|
|
185
185
|
for (const call of progress.toolCalls) {
|
|
186
|
-
lines.push(theme.fg("dim", "
|
|
186
|
+
lines.push(theme.fg("dim", "↳ " + call));
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
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";
|
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,
|
|
@@ -24,13 +25,13 @@ const SUBAGENT_PARAMS_SCHEMA = Type.Object({
|
|
|
24
25
|
description: "Agent name/identifier (optional, defaults to 'general')",
|
|
25
26
|
})),
|
|
26
27
|
task: Type.Optional(Type.String({
|
|
27
|
-
description: "Task description for the subagent",
|
|
28
|
+
description: "Task description for the subagent. Required when not using 'tasks' array.",
|
|
28
29
|
})),
|
|
29
30
|
tasks: Type.Optional(Type.Array(TaskItem, {
|
|
30
|
-
description: "Multiple tasks for parallel execution",
|
|
31
|
+
description: "Multiple tasks for parallel execution. Required when not using 'task'.",
|
|
31
32
|
})),
|
|
32
33
|
model: Type.Optional(Type.String({
|
|
33
|
-
description: "Model override
|
|
34
|
+
description: "Model override for the subagent",
|
|
34
35
|
})),
|
|
35
36
|
async: Type.Optional(Type.Boolean({
|
|
36
37
|
description: "Run asynchronously (fire-and-forget)",
|
|
@@ -54,7 +55,7 @@ export function registerSubagent(pi: ExtensionAPI): void {
|
|
|
54
55
|
name: "subagent",
|
|
55
56
|
label: "Subagent",
|
|
56
57
|
description:
|
|
57
|
-
"Delegate tasks to subagents.
|
|
58
|
+
"Delegate tasks to subagents. Provide either 'task' (single) or 'tasks' (parallel). Never omit both. Options: agent, model, cwd.",
|
|
58
59
|
parameters: SUBAGENT_PARAMS_SCHEMA,
|
|
59
60
|
|
|
60
61
|
async execute(
|
|
@@ -69,13 +70,31 @@ export function registerSubagent(pi: ExtensionAPI): void {
|
|
|
69
70
|
},
|
|
70
71
|
signal: AbortSignal | undefined,
|
|
71
72
|
onUpdate: ((update: SubagentToolResult) => void) | undefined,
|
|
72
|
-
|
|
73
|
+
ctx: ExtensionContext,
|
|
73
74
|
): Promise<SubagentToolResult> {
|
|
75
|
+
// Discover available agents
|
|
76
|
+
const agents = discoverAgents(ctx.cwd);
|
|
77
|
+
|
|
74
78
|
// Parallel mode
|
|
75
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
|
+
}
|
|
76
94
|
const results: SubagentResult[] = await executeParallel({
|
|
77
95
|
tasks: params.tasks.map((t) => ({
|
|
78
96
|
agent: t.agent ?? undefined,
|
|
97
|
+
agentConfig: t.agent ? findAgent(agents, t.agent) : undefined,
|
|
79
98
|
task: t.task,
|
|
80
99
|
model: t.model ?? undefined,
|
|
81
100
|
cwd: t.cwd ?? undefined,
|
|
@@ -105,8 +124,22 @@ export function registerSubagent(pi: ExtensionAPI): void {
|
|
|
105
124
|
|
|
106
125
|
// Single mode
|
|
107
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
|
+
}
|
|
108
140
|
const result: SubagentResult = await executeSubagent({
|
|
109
141
|
agent: params.agent ?? undefined,
|
|
142
|
+
agentConfig,
|
|
110
143
|
task: params.task,
|
|
111
144
|
model: params.model ?? undefined,
|
|
112
145
|
cwd: params.cwd ?? undefined,
|
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
10
|
model: string | undefined;
|
|
8
11
|
cwd: string | undefined;
|
|
9
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,13 +123,17 @@ 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
|
|
|
72
|
-
// Clean up
|
|
129
|
+
// Clean up temp files and listeners on exit
|
|
73
130
|
const exitCleanup = (): void => {
|
|
74
131
|
child.removeListener("exit", exitCleanup);
|
|
75
132
|
child.removeListener("error", exitCleanup);
|
|
133
|
+
if (abortHandler && options.signal) {
|
|
134
|
+
options.signal.removeEventListener("abort", abortHandler);
|
|
135
|
+
}
|
|
136
|
+
cleanupTempFiles(tmpDir, tmpPath);
|
|
76
137
|
};
|
|
77
138
|
child.on("exit", exitCleanup);
|
|
78
139
|
child.on("error", exitCleanup);
|
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
|
});
|