pi-subagent-windows 0.0.0-stage → 0.1.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/LICENSE +22 -0
- package/README.md +106 -2
- package/bin/subagent.js +16 -0
- package/index.ts +344 -0
- package/package.json +36 -4
- package/shared.ts +385 -0
- package/skills/subagent/SKILL.md +93 -0
- package/subagent.ts +378 -0
- package/supervisor.ts +155 -0
package/subagent.ts
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { dirname, join, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import {
|
|
8
|
+
assistantText,
|
|
9
|
+
effectiveRunState,
|
|
10
|
+
getRunsDir,
|
|
11
|
+
inboxDir,
|
|
12
|
+
isRunAlive,
|
|
13
|
+
killRun,
|
|
14
|
+
launchRun,
|
|
15
|
+
listRuns,
|
|
16
|
+
readLatestAssistant,
|
|
17
|
+
readMetadata,
|
|
18
|
+
removeRunDir,
|
|
19
|
+
runDisplayName,
|
|
20
|
+
isValidRunName,
|
|
21
|
+
type InboxMessage,
|
|
22
|
+
type RunMetadata,
|
|
23
|
+
updateMetadata,
|
|
24
|
+
waitForRunShutdown,
|
|
25
|
+
writeInboxMessage,
|
|
26
|
+
writeMetadata,
|
|
27
|
+
} from "./shared.ts";
|
|
28
|
+
|
|
29
|
+
const VALID_THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
30
|
+
const extensionPath = join(dirname(fileURLToPath(import.meta.url)), "index.ts");
|
|
31
|
+
|
|
32
|
+
function fail(message: string): never {
|
|
33
|
+
throw new Error(message);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function usage(): never {
|
|
37
|
+
fail(`Usage:
|
|
38
|
+
subagent spawn [--name <name>] [--provider <provider>] [--model <model>] [--thinking <level>]
|
|
39
|
+
[--cwd <dir>] [--tools <names>] [--no-extensions] [--no-skills]
|
|
40
|
+
[--no-prompt-templates] [--no-context-files] (--prompt <text> | --file <path>)...
|
|
41
|
+
subagent status <handle>
|
|
42
|
+
subagent rename <handle> <name>
|
|
43
|
+
subagent send <handle> [--follow-up] <message>
|
|
44
|
+
subagent wait <handle> [--timeout <seconds>]
|
|
45
|
+
subagent stop <handle>
|
|
46
|
+
subagent list`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function valueAfter(args: string[], index: number, option: string): string {
|
|
50
|
+
const value = args[index + 1];
|
|
51
|
+
if (!value || value.startsWith("--")) fail(`${option} requires a value`);
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function normalizeRunName(value: string): string {
|
|
56
|
+
const name = value.trim();
|
|
57
|
+
if (!isValidRunName(name)) fail("Subagent name must be a single line of 1 to 64 characters");
|
|
58
|
+
return name;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Windows children run in RPC mode, which does not accept @file arguments.
|
|
63
|
+
* Inline small text files and point at large or binary ones instead.
|
|
64
|
+
*/
|
|
65
|
+
function buildInitialMessage(prompts: string[], files: string[]): string {
|
|
66
|
+
const parts: string[] = [];
|
|
67
|
+
for (const file of files) {
|
|
68
|
+
try {
|
|
69
|
+
const buffer = readFileSync(file);
|
|
70
|
+
if (buffer.includes(0) || buffer.byteLength > 1_000_000) {
|
|
71
|
+
parts.push(`A file is available at ${file}. Inspect it with the read tool before proceeding.`);
|
|
72
|
+
} else {
|
|
73
|
+
parts.push(`<file name="${file}">\n${buffer.toString("utf8")}\n</file>`);
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
76
|
+
parts.push(`A file was requested at ${file}, but it could not be read.`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (prompts.length > 0) parts.push(`Task:\n${prompts.join("\n\n")}`);
|
|
80
|
+
return parts.join("\n\n");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function runDirForHandle(handle: string): string {
|
|
84
|
+
if (!/^[a-z0-9]+$/.test(handle)) fail(`Invalid subagent handle: ${handle}`);
|
|
85
|
+
return join(getRunsDir(), handle);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getRun(handle: string): RunMetadata {
|
|
89
|
+
const metadata = readMetadata(runDirForHandle(handle));
|
|
90
|
+
if (!metadata) fail(`Unknown subagent: ${handle}`);
|
|
91
|
+
return metadata;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function generateHandle(): string {
|
|
95
|
+
mkdirSync(getRunsDir(), { recursive: true, mode: 0o700 });
|
|
96
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
97
|
+
const handle = randomBytes(3).toString("hex");
|
|
98
|
+
if (!existsSync(runDirForHandle(handle))) return handle;
|
|
99
|
+
}
|
|
100
|
+
fail("Could not allocate a unique subagent handle");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function spawnSubagent(args: string[]): void {
|
|
104
|
+
if (process.env.PI_SUBAGENT_RUN_DIR) fail("Nested subagents are disabled");
|
|
105
|
+
let name: string | undefined;
|
|
106
|
+
let provider = process.env.PI_PROVIDER;
|
|
107
|
+
let model = process.env.PI_MODEL;
|
|
108
|
+
let thinking = process.env.PI_REASONING_LEVEL || "medium";
|
|
109
|
+
let cwd = process.cwd();
|
|
110
|
+
let tools: string | undefined;
|
|
111
|
+
let noExtensions = false;
|
|
112
|
+
let noSkills = false;
|
|
113
|
+
let noPromptTemplates = false;
|
|
114
|
+
let noContextFiles = false;
|
|
115
|
+
const prompts: string[] = [];
|
|
116
|
+
const files: string[] = [];
|
|
117
|
+
|
|
118
|
+
for (let i = 0; i < args.length; i++) {
|
|
119
|
+
const arg = args[i];
|
|
120
|
+
switch (arg) {
|
|
121
|
+
case "--name":
|
|
122
|
+
name = normalizeRunName(valueAfter(args, i, arg));
|
|
123
|
+
i++;
|
|
124
|
+
break;
|
|
125
|
+
case "--provider":
|
|
126
|
+
provider = valueAfter(args, i, arg);
|
|
127
|
+
i++;
|
|
128
|
+
break;
|
|
129
|
+
case "--model":
|
|
130
|
+
model = valueAfter(args, i, arg);
|
|
131
|
+
i++;
|
|
132
|
+
break;
|
|
133
|
+
case "--thinking":
|
|
134
|
+
thinking = valueAfter(args, i, arg);
|
|
135
|
+
i++;
|
|
136
|
+
break;
|
|
137
|
+
case "--cwd":
|
|
138
|
+
cwd = resolve(valueAfter(args, i, arg));
|
|
139
|
+
i++;
|
|
140
|
+
break;
|
|
141
|
+
case "--tools":
|
|
142
|
+
tools = valueAfter(args, i, arg);
|
|
143
|
+
i++;
|
|
144
|
+
break;
|
|
145
|
+
case "--no-extensions":
|
|
146
|
+
noExtensions = true;
|
|
147
|
+
break;
|
|
148
|
+
case "--no-skills":
|
|
149
|
+
noSkills = true;
|
|
150
|
+
break;
|
|
151
|
+
case "--no-prompt-templates":
|
|
152
|
+
noPromptTemplates = true;
|
|
153
|
+
break;
|
|
154
|
+
case "--no-context-files":
|
|
155
|
+
noContextFiles = true;
|
|
156
|
+
break;
|
|
157
|
+
case "--prompt":
|
|
158
|
+
prompts.push(valueAfter(args, i, arg));
|
|
159
|
+
i++;
|
|
160
|
+
break;
|
|
161
|
+
case "--file": {
|
|
162
|
+
const file = resolve(valueAfter(args, i, arg));
|
|
163
|
+
if (!existsSync(file)) fail(`File not found: ${file}`);
|
|
164
|
+
files.push(file);
|
|
165
|
+
i++;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
default:
|
|
169
|
+
fail(`Unknown spawn argument: ${arg}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!provider) fail("No provider specified and PI_PROVIDER is not set");
|
|
174
|
+
if (!model) fail("No model specified and PI_MODEL is not set");
|
|
175
|
+
if (!VALID_THINKING_LEVELS.has(thinking)) fail(`Invalid thinking level: ${thinking}`);
|
|
176
|
+
if (tools !== undefined && !tools.split(",").some((name) => name.trim()))
|
|
177
|
+
fail("--tools requires at least one tool name");
|
|
178
|
+
if (prompts.length === 0 && files.length === 0) fail("spawn requires at least one --prompt or --file");
|
|
179
|
+
if (!existsSync(cwd)) fail(`Working directory not found: ${cwd}`);
|
|
180
|
+
|
|
181
|
+
const handle = generateHandle();
|
|
182
|
+
const runDir = runDirForHandle(handle);
|
|
183
|
+
const sessionFile = join(runDir, "session.jsonl");
|
|
184
|
+
const tmuxSession = `pi-subagent-${handle}`;
|
|
185
|
+
mkdirSync(inboxDir(runDir), { recursive: true, mode: 0o700 });
|
|
186
|
+
writeFileSync(sessionFile, "", { mode: 0o600 });
|
|
187
|
+
|
|
188
|
+
const launchArgs: string[] = [];
|
|
189
|
+
if (tools) launchArgs.push("--tools", tools);
|
|
190
|
+
if (noExtensions) launchArgs.push("--no-extensions", "--extension", extensionPath);
|
|
191
|
+
if (noSkills) launchArgs.push("--no-skills");
|
|
192
|
+
if (noPromptTemplates) launchArgs.push("--no-prompt-templates");
|
|
193
|
+
if (noContextFiles) launchArgs.push("--no-context-files");
|
|
194
|
+
|
|
195
|
+
const now = new Date().toISOString();
|
|
196
|
+
const metadata: RunMetadata = {
|
|
197
|
+
version: 1,
|
|
198
|
+
handle,
|
|
199
|
+
name,
|
|
200
|
+
parentSessionId: process.env.PI_SESSION_ID || undefined,
|
|
201
|
+
parentSessionFile: process.env.PI_SESSION_FILE || undefined,
|
|
202
|
+
tmuxSession,
|
|
203
|
+
runDir,
|
|
204
|
+
sessionFile,
|
|
205
|
+
cwd,
|
|
206
|
+
provider,
|
|
207
|
+
model,
|
|
208
|
+
thinking,
|
|
209
|
+
launchArgs,
|
|
210
|
+
state: "starting",
|
|
211
|
+
hasStarted: false,
|
|
212
|
+
createdAt: now,
|
|
213
|
+
updatedAt: now,
|
|
214
|
+
};
|
|
215
|
+
writeMetadata(metadata);
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
if (process.platform === "win32") {
|
|
219
|
+
writeInboxMessage(runDir, { message: buildInitialMessage(prompts, files), delivery: "auto" });
|
|
220
|
+
launchRun(metadata);
|
|
221
|
+
} else {
|
|
222
|
+
const initialArgs = files.map((file) => `@${file}`);
|
|
223
|
+
if (prompts.length > 0) initialArgs.push(`Task:\n${prompts.join("\n\n")}`);
|
|
224
|
+
launchRun(metadata, initialArgs);
|
|
225
|
+
}
|
|
226
|
+
} catch (error) {
|
|
227
|
+
removeRunDir(runDir);
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const attachLine =
|
|
232
|
+
process.platform === "win32"
|
|
233
|
+
? `Session: ${sessionFile}\n`
|
|
234
|
+
: `Attach: tmux attach -t ${tmuxSession}\n`;
|
|
235
|
+
process.stdout.write(`Spawned ${runDisplayName(metadata)}\nState: busy\n${attachLine}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function statusSubagent(args: string[]): void {
|
|
239
|
+
if (args.length !== 1) usage();
|
|
240
|
+
const metadata = getRun(args[0]);
|
|
241
|
+
const attachLine =
|
|
242
|
+
process.platform === "win32"
|
|
243
|
+
? `Session: ${metadata.sessionFile}\n`
|
|
244
|
+
: `Attach: tmux attach -t ${metadata.tmuxSession}\n`;
|
|
245
|
+
process.stdout.write(
|
|
246
|
+
`${runDisplayName(metadata)}: ${effectiveRunState(metadata)} (${metadata.provider}/${metadata.model}, ${metadata.thinking})\n${attachLine}`,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function renameSubagent(args: string[]): void {
|
|
251
|
+
if (args.length !== 2) usage();
|
|
252
|
+
const metadata = getRun(args[0]);
|
|
253
|
+
const name = normalizeRunName(args[1]);
|
|
254
|
+
const updated = updateMetadata(metadata.runDir, { name });
|
|
255
|
+
if (!updated) fail(`Could not rename subagent: ${metadata.handle}`);
|
|
256
|
+
process.stdout.write(`Renamed ${runDisplayName(updated)}\n`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function sendSubagent(args: string[]): void {
|
|
260
|
+
const handle = args.shift();
|
|
261
|
+
if (!handle) usage();
|
|
262
|
+
let followUp = false;
|
|
263
|
+
const messageParts: string[] = [];
|
|
264
|
+
for (const arg of args) {
|
|
265
|
+
if (arg === "--follow-up") followUp = true;
|
|
266
|
+
else if (arg.startsWith("--")) fail(`Unknown send argument: ${arg}`);
|
|
267
|
+
else messageParts.push(arg);
|
|
268
|
+
}
|
|
269
|
+
const message = messageParts.join(" ").trim();
|
|
270
|
+
if (!message) fail("send requires a message");
|
|
271
|
+
const metadata = getRun(handle);
|
|
272
|
+
if (!isRunAlive(metadata)) fail(`${handle} is not running`);
|
|
273
|
+
|
|
274
|
+
const payload: InboxMessage = { message, delivery: followUp ? "followUp" : "auto" };
|
|
275
|
+
writeInboxMessage(metadata.runDir, payload);
|
|
276
|
+
|
|
277
|
+
const state = effectiveRunState(metadata);
|
|
278
|
+
const verb =
|
|
279
|
+
followUp && state === "busy"
|
|
280
|
+
? "Queued follow-up for"
|
|
281
|
+
: state === "idle" || state === "starting"
|
|
282
|
+
? "Prompted"
|
|
283
|
+
: "Steered";
|
|
284
|
+
process.stdout.write(`${verb} ${runDisplayName(metadata)}\n`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function parseTimeout(args: string[]): number {
|
|
288
|
+
if (args.length === 0) return 1800;
|
|
289
|
+
if (args.length !== 2 || args[0] !== "--timeout") usage();
|
|
290
|
+
const timeout = Number(args[1]);
|
|
291
|
+
if (!Number.isInteger(timeout) || timeout <= 0) fail("--timeout must be a positive integer");
|
|
292
|
+
return timeout;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function waitSubagent(args: string[]): Promise<void> {
|
|
296
|
+
const handle = args.shift();
|
|
297
|
+
if (!handle) usage();
|
|
298
|
+
const timeoutSeconds = parseTimeout(args);
|
|
299
|
+
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
300
|
+
|
|
301
|
+
while (Date.now() < deadline) {
|
|
302
|
+
const metadata = getRun(handle);
|
|
303
|
+
const pending = existsSync(inboxDir(metadata.runDir))
|
|
304
|
+
? readdirSync(inboxDir(metadata.runDir)).some((file) => file.endsWith(".json"))
|
|
305
|
+
: false;
|
|
306
|
+
const state = effectiveRunState(metadata);
|
|
307
|
+
if (state === "error") fail(metadata.error || `${handle} failed`);
|
|
308
|
+
if (state === "exited") fail(`${handle} exited before finishing`);
|
|
309
|
+
if (metadata.hasStarted && state === "idle" && !pending) {
|
|
310
|
+
const message = readLatestAssistant(metadata.sessionFile);
|
|
311
|
+
if (!message) fail(`${handle} finished without an assistant response`);
|
|
312
|
+
process.stdout.write(`${handle} finished\n\n${assistantText(message)}\n`);
|
|
313
|
+
if (message.stopReason === "error" || message.stopReason === "aborted") process.exitCode = 1;
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
await new Promise((resolveDelay) => setTimeout(resolveDelay, 500));
|
|
317
|
+
}
|
|
318
|
+
fail(`Timed out after ${timeoutSeconds}s waiting for ${handle}`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function stopSubagent(args: string[]): Promise<void> {
|
|
322
|
+
if (args.length !== 1) usage();
|
|
323
|
+
const metadata = getRun(args[0]);
|
|
324
|
+
const wasRunning = isRunAlive(metadata);
|
|
325
|
+
killRun(metadata);
|
|
326
|
+
if (wasRunning) await waitForRunShutdown(metadata.runDir);
|
|
327
|
+
removeRunDir(metadata.runDir);
|
|
328
|
+
process.stdout.write(`Stopped ${runDisplayName(metadata)}\n`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function listSubagents(args: string[]): void {
|
|
332
|
+
if (args.length !== 0) usage();
|
|
333
|
+
const runs = listRuns(process.env.PI_SESSION_ID || undefined);
|
|
334
|
+
if (runs.length === 0) {
|
|
335
|
+
process.stdout.write("No subagents\n");
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
for (const metadata of runs) {
|
|
339
|
+
process.stdout.write(
|
|
340
|
+
`${runDisplayName(metadata)} ${effectiveRunState(metadata).padEnd(8)} ${metadata.provider}/${metadata.model} ${metadata.thinking}\n`,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function main(): Promise<void> {
|
|
346
|
+
const args = process.argv.slice(2);
|
|
347
|
+
const command = args.shift();
|
|
348
|
+
switch (command) {
|
|
349
|
+
case "spawn":
|
|
350
|
+
spawnSubagent(args);
|
|
351
|
+
break;
|
|
352
|
+
case "status":
|
|
353
|
+
statusSubagent(args);
|
|
354
|
+
break;
|
|
355
|
+
case "rename":
|
|
356
|
+
renameSubagent(args);
|
|
357
|
+
break;
|
|
358
|
+
case "send":
|
|
359
|
+
sendSubagent(args);
|
|
360
|
+
break;
|
|
361
|
+
case "wait":
|
|
362
|
+
await waitSubagent(args);
|
|
363
|
+
break;
|
|
364
|
+
case "stop":
|
|
365
|
+
await stopSubagent(args);
|
|
366
|
+
break;
|
|
367
|
+
case "list":
|
|
368
|
+
listSubagents(args);
|
|
369
|
+
break;
|
|
370
|
+
default:
|
|
371
|
+
usage();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
main().catch((error: unknown) => {
|
|
376
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
377
|
+
process.exitCode = 1;
|
|
378
|
+
});
|
package/supervisor.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Hidden process host for a Windows subagent.
|
|
4
|
+
*
|
|
5
|
+
* It is spawned detached by `launchRun`, writes its own pid for liveness checks,
|
|
6
|
+
* then owns a `pi --mode rpc` child. Because the supervisor is independent of the
|
|
7
|
+
* parent Pi process, subagents survive parent `/reload`, session switches, and
|
|
8
|
+
* parent crashes. Closing the child's stdin is Pi's documented orderly shutdown.
|
|
9
|
+
*/
|
|
10
|
+
import { appendFileSync, createWriteStream, writeFileSync } from "node:fs";
|
|
11
|
+
import { spawn } from "node:child_process";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import {
|
|
14
|
+
getAgentDir,
|
|
15
|
+
readMetadata,
|
|
16
|
+
resolvePiCliPath,
|
|
17
|
+
supervisorPidPath,
|
|
18
|
+
updateMetadata,
|
|
19
|
+
} from "./shared.ts";
|
|
20
|
+
|
|
21
|
+
const runDir = process.argv[2];
|
|
22
|
+
if (!runDir) {
|
|
23
|
+
process.stderr.write("pi-subagent-windows supervisor: missing run directory\n");
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const metadata = readMetadata(runDir);
|
|
28
|
+
if (!metadata) {
|
|
29
|
+
process.stderr.write(`pi-subagent-windows supervisor: no valid metadata in ${runDir}\n`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const logPath = join(runDir, "supervisor.log");
|
|
34
|
+
const log = (message: string): void => {
|
|
35
|
+
try {
|
|
36
|
+
appendFileSync(logPath, `[${new Date().toISOString()}] ${message}\n`, { encoding: "utf8" });
|
|
37
|
+
} catch {
|
|
38
|
+
// Logging is best effort; never fail the run over it.
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
writeFileSync(supervisorPidPath(runDir), `${process.pid}\n`, { encoding: "utf8", mode: 0o600 });
|
|
43
|
+
|
|
44
|
+
const installRoot = join(getAgentDir(), "install");
|
|
45
|
+
let cliPath: string;
|
|
46
|
+
try {
|
|
47
|
+
cliPath = resolvePiCliPath();
|
|
48
|
+
} catch (error) {
|
|
49
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
50
|
+
log(`could not resolve the Pi CLI: ${message}`);
|
|
51
|
+
updateMetadata(runDir, { state: "error", error: message });
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const args = [
|
|
56
|
+
cliPath,
|
|
57
|
+
"--mode", "rpc",
|
|
58
|
+
"--session", metadata.sessionFile,
|
|
59
|
+
"--provider", metadata.provider,
|
|
60
|
+
"--model", metadata.model,
|
|
61
|
+
"--thinking", metadata.thinking,
|
|
62
|
+
...(metadata.launchArgs ?? []),
|
|
63
|
+
];
|
|
64
|
+
log(`supervisor pid=${process.pid} starting child: ${JSON.stringify(args)}`);
|
|
65
|
+
|
|
66
|
+
const child = spawn(process.execPath, args, {
|
|
67
|
+
cwd: metadata.cwd,
|
|
68
|
+
windowsHide: true,
|
|
69
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
70
|
+
env: {
|
|
71
|
+
...process.env,
|
|
72
|
+
PI_SUBAGENT_RUN_DIR: runDir,
|
|
73
|
+
PI_MANAGED_INSTALL_ROOT: installRoot,
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (!child.pid || !child.stdin || !child.stdout) {
|
|
78
|
+
log("failed to spawn the Pi RPC child");
|
|
79
|
+
updateMetadata(runDir, { state: "error", error: "Failed to spawn the Pi RPC child" });
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
log(`child pid=${child.pid}`);
|
|
84
|
+
child.stderr.pipe(createWriteStream(logPath, { flags: "a" }));
|
|
85
|
+
|
|
86
|
+
/** Answer dialogs that would otherwise block a hidden child forever. */
|
|
87
|
+
function respond(id: string, payload: Record<string, unknown>): void {
|
|
88
|
+
try {
|
|
89
|
+
child.stdin!.write(`${JSON.stringify({ type: "extension_ui_response", id, ...payload })}\n`);
|
|
90
|
+
} catch {
|
|
91
|
+
// The child may have exited between the request and the response.
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let stdoutBuffer = "";
|
|
96
|
+
child.stdout.on("data", (data: Buffer) => {
|
|
97
|
+
stdoutBuffer += data.toString("utf8");
|
|
98
|
+
let index: number;
|
|
99
|
+
while ((index = stdoutBuffer.indexOf("\n")) !== -1) {
|
|
100
|
+
const line = stdoutBuffer.slice(0, index).replace(/\r$/, "");
|
|
101
|
+
stdoutBuffer = stdoutBuffer.slice(index + 1);
|
|
102
|
+
if (!line.trim()) continue;
|
|
103
|
+
let record: { type?: string; id?: string; method?: string };
|
|
104
|
+
try {
|
|
105
|
+
record = JSON.parse(line);
|
|
106
|
+
} catch {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (record.type !== "extension_ui_request" || typeof record.id !== "string") continue;
|
|
110
|
+
switch (record.method) {
|
|
111
|
+
case "confirm":
|
|
112
|
+
log("auto-declined an extension confirm dialog (no interactive UI in hidden subagents)");
|
|
113
|
+
respond(record.id, { confirmed: false });
|
|
114
|
+
break;
|
|
115
|
+
case "select":
|
|
116
|
+
case "input":
|
|
117
|
+
case "editor":
|
|
118
|
+
log(`auto-cancelled an extension ${record.method} dialog (no interactive UI in hidden subagents)`);
|
|
119
|
+
respond(record.id, { cancelled: true });
|
|
120
|
+
break;
|
|
121
|
+
default:
|
|
122
|
+
// notify / setStatus / setWidget / setTitle / set_editor_text need no response.
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
child.on("error", (error) => {
|
|
129
|
+
log(`child process error: ${error.message}`);
|
|
130
|
+
updateMetadata(runDir, { state: "error", error: error.message });
|
|
131
|
+
process.exit(1);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
child.on("exit", (code, signal) => {
|
|
135
|
+
log(`child exited code=${code} signal=${signal}`);
|
|
136
|
+
const current = readMetadata(runDir);
|
|
137
|
+
if (current && current.state !== "exited" && current.state !== "error") {
|
|
138
|
+
updateMetadata(
|
|
139
|
+
runDir,
|
|
140
|
+
code === 0
|
|
141
|
+
? { state: "exited" }
|
|
142
|
+
: { state: "error", error: `Subagent process exited unexpectedly (code=${code}, signal=${signal})` },
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
process.exit(0);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// A graceful shutdown request (process.kill) closes the child's stdin first.
|
|
149
|
+
process.on("SIGTERM", () => {
|
|
150
|
+
try {
|
|
151
|
+
child.stdin!.end();
|
|
152
|
+
} catch {
|
|
153
|
+
// Already closed.
|
|
154
|
+
}
|
|
155
|
+
});
|