pi-plans 0.1.2 → 0.3.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/README.md +98 -19
- package/index.ts +147 -66
- package/package.json +7 -1
- package/references/pi-planning-workflow.md +21 -3
- package/references/state-and-config.md +35 -3
- package/scripts/validate.ts +4 -0
- package/src/autocomplete.ts +163 -0
- package/src/code-graph/commands.ts +437 -0
- package/src/code-graph/discovery.ts +118 -0
- package/src/code-graph/git.ts +108 -0
- package/src/code-graph/identity.ts +59 -0
- package/src/code-graph/indexer.ts +281 -0
- package/src/code-graph/materialize.ts +166 -0
- package/src/code-graph/mode.ts +28 -0
- package/src/code-graph/mutations.ts +160 -0
- package/src/code-graph/parser.ts +51 -0
- package/src/code-graph/parsers/javascript.ts +35 -0
- package/src/code-graph/parsers/python.ts +160 -0
- package/src/code-graph/parsers/tree-sitter.ts +316 -0
- package/src/code-graph/paths.ts +85 -0
- package/src/code-graph/prompts.ts +18 -0
- package/src/code-graph/resolver.ts +69 -0
- package/src/code-graph/runtime.ts +158 -0
- package/src/code-graph/schema.ts +135 -0
- package/src/code-graph/screening.ts +82 -0
- package/src/code-graph/store.ts +278 -0
- package/src/code-graph/summary.ts +435 -0
- package/src/code-graph/types.ts +163 -0
- package/src/compaction.ts +1256 -0
- package/src/config-command.ts +326 -0
- package/src/exec.ts +519 -625
- package/src/plan.ts +37 -0
- package/src/query-hook.ts +82 -0
- package/src/refine-prompts.ts +50 -0
- package/src/refine-ui-helpers.ts +142 -0
- package/src/refine-ui-state.ts +144 -0
- package/src/refine-ui.ts +430 -0
- package/src/state.ts +24 -29
- package/src/subagent.ts +299 -70
- package/tests/ask-choice.test.ts +263 -0
- package/tests/autocomplete.test.ts +147 -0
- package/tests/code-graph-apply.test.ts +185 -0
- package/tests/code-graph-commands.test.ts +211 -0
- package/tests/code-graph-db.test.ts +166 -0
- package/tests/code-graph-discovery.test.ts +38 -0
- package/tests/code-graph-git.test.ts +94 -0
- package/tests/code-graph-index.test.ts +175 -0
- package/tests/code-graph-loop.e2e.test.ts +159 -0
- package/tests/code-graph-mutations.test.ts +117 -0
- package/tests/code-graph-parser.test.ts +85 -0
- package/tests/code-graph-rollback.test.ts +100 -0
- package/tests/code-graph-summary-batching.test.ts +518 -0
- package/tests/code-graph-summary.test.ts +148 -0
- package/tests/compaction.test.ts +388 -0
- package/tests/config-command.test.ts +255 -0
- package/tests/exec.test.ts +751 -422
- package/tests/execute-plan.test.ts +65 -0
- package/tests/fixtures/code-graph/sample.js +36 -0
- package/tests/fixtures/code-graph/sample.py +20 -0
- package/tests/fixtures/code-graph/sample.ts +15 -0
- package/tests/graph-aware-file-tools.test.ts +411 -0
- package/tests/plan.test.ts +11 -1
- package/tests/plans.test.ts +6 -5
- package/tests/query-hook.test.ts +82 -0
- package/tests/refine-prompts.test.ts +67 -2
- package/tests/refine-ui.test.ts +392 -0
- package/tests/state.test.ts +12 -15
- package/tests/subagent.test.ts +120 -0
- package/tools/ask-choice.ts +180 -11
- package/tools/code-graph.ts +254 -0
- package/tools/execute-plan.ts +7 -39
- package/tools/graph-aware-file-tools.ts +392 -0
- package/tools/plans.ts +84 -18
- package/tools/refine.ts +180 -80
- package/src/execution-panel.ts +0 -633
- package/tests/execution-panel.test.ts +0 -234
package/src/subagent.ts
CHANGED
|
@@ -9,6 +9,25 @@ import * as fs from "node:fs";
|
|
|
9
9
|
import * as os from "node:os";
|
|
10
10
|
import * as path from "node:path";
|
|
11
11
|
|
|
12
|
+
export type SubagentTranscriptEntryType = "assistant-text" | "thinking" | "tool-call" | "tool-result";
|
|
13
|
+
|
|
14
|
+
export type SubagentProgressEvent =
|
|
15
|
+
| { type: "process"; phase: "started" | "exited"; code?: number }
|
|
16
|
+
| { type: "turn"; phase: "start" | "end"; turnIndex?: number }
|
|
17
|
+
| {
|
|
18
|
+
type: "transcript";
|
|
19
|
+
phase: "start" | "update" | "end";
|
|
20
|
+
entryType: SubagentTranscriptEntryType;
|
|
21
|
+
key: string;
|
|
22
|
+
text: string;
|
|
23
|
+
update: "append" | "replace";
|
|
24
|
+
streaming: boolean;
|
|
25
|
+
toolCallId?: string;
|
|
26
|
+
toolName?: string;
|
|
27
|
+
isError?: boolean;
|
|
28
|
+
}
|
|
29
|
+
| { type: "stderr"; text: string };
|
|
30
|
+
|
|
12
31
|
export interface SubagentOptions {
|
|
13
32
|
systemPrompt: string;
|
|
14
33
|
task: string;
|
|
@@ -19,6 +38,8 @@ export interface SubagentOptions {
|
|
|
19
38
|
tools?: string[];
|
|
20
39
|
signal?: AbortSignal;
|
|
21
40
|
timeoutMs?: number;
|
|
41
|
+
/** Optional normalized progress sink. Exceptions from the sink are ignored. */
|
|
42
|
+
onProgress?: (event: SubagentProgressEvent) => void;
|
|
22
43
|
}
|
|
23
44
|
|
|
24
45
|
export interface SubagentResult {
|
|
@@ -28,6 +49,8 @@ export interface SubagentResult {
|
|
|
28
49
|
errorMessage?: string;
|
|
29
50
|
stderr: string;
|
|
30
51
|
turns: number;
|
|
52
|
+
cancelled?: boolean;
|
|
53
|
+
timedOut?: boolean;
|
|
31
54
|
}
|
|
32
55
|
|
|
33
56
|
/** Strip YAML frontmatter from an agent definition file. */
|
|
@@ -54,43 +77,229 @@ export function getPiInvocation(args: string[]): { command: string; args: string
|
|
|
54
77
|
|
|
55
78
|
interface MessageLike {
|
|
56
79
|
role: string;
|
|
57
|
-
content
|
|
80
|
+
content?: unknown;
|
|
58
81
|
model?: string;
|
|
59
82
|
}
|
|
60
83
|
|
|
84
|
+
interface RawSubagentEvent {
|
|
85
|
+
type?: unknown;
|
|
86
|
+
turnIndex?: unknown;
|
|
87
|
+
message?: MessageLike;
|
|
88
|
+
toolCallId?: unknown;
|
|
89
|
+
toolName?: unknown;
|
|
90
|
+
args?: unknown;
|
|
91
|
+
partialResult?: unknown;
|
|
92
|
+
result?: unknown;
|
|
93
|
+
isError?: unknown;
|
|
94
|
+
assistantMessageEvent?: unknown;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function formatValue(value: unknown): string {
|
|
98
|
+
if (typeof value === "string") return value;
|
|
99
|
+
if (value === undefined) return "";
|
|
100
|
+
if (value && typeof value === "object") {
|
|
101
|
+
const content = (value as { content?: unknown }).content;
|
|
102
|
+
if (Array.isArray(content)) {
|
|
103
|
+
const text = content
|
|
104
|
+
.map((part) => {
|
|
105
|
+
if (part && typeof part === "object" && typeof (part as { text?: unknown }).text === "string") {
|
|
106
|
+
return (part as { text: string }).text;
|
|
107
|
+
}
|
|
108
|
+
return formatValue(part);
|
|
109
|
+
})
|
|
110
|
+
.filter(Boolean)
|
|
111
|
+
.join("\n");
|
|
112
|
+
if (text) return text;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
return JSON.stringify(value, null, 2) ?? String(value);
|
|
117
|
+
} catch {
|
|
118
|
+
return String(value);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function messageText(message: MessageLike | undefined, kind: "text" | "thinking" = "text"): string {
|
|
123
|
+
if (!Array.isArray(message?.content)) return typeof message?.content === "string" && kind === "text" ? message.content : "";
|
|
124
|
+
return message.content
|
|
125
|
+
.filter((part) => part && typeof part === "object" && (part as { type?: unknown }).type === kind)
|
|
126
|
+
.map((part) => {
|
|
127
|
+
const value = part as { text?: unknown; thinking?: unknown };
|
|
128
|
+
return typeof value.text === "string" ? value.text : typeof value.thinking === "string" ? value.thinking : "";
|
|
129
|
+
})
|
|
130
|
+
.join("\n");
|
|
131
|
+
}
|
|
132
|
+
|
|
61
133
|
function finalOutput(messages: MessageLike[]): string {
|
|
62
134
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
63
135
|
const message = messages[i];
|
|
64
136
|
if (message.role === "assistant") {
|
|
65
|
-
const text = message.
|
|
66
|
-
.filter((part) => part.type === "text")
|
|
67
|
-
.map((part) => part.text ?? "")
|
|
68
|
-
.join("\n")
|
|
69
|
-
.trim();
|
|
137
|
+
const text = messageText(message, "text").trim();
|
|
70
138
|
if (text) return text;
|
|
71
139
|
}
|
|
72
140
|
}
|
|
73
141
|
return "";
|
|
74
142
|
}
|
|
75
143
|
|
|
76
|
-
|
|
144
|
+
interface TranscriptEventOptions {
|
|
145
|
+
toolCallId?: string;
|
|
146
|
+
toolName?: string;
|
|
147
|
+
isError?: boolean;
|
|
148
|
+
}
|
|
77
149
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
150
|
+
function transcriptEvent(
|
|
151
|
+
phase: "start" | "update" | "end",
|
|
152
|
+
entryType: SubagentTranscriptEntryType,
|
|
153
|
+
key: string,
|
|
154
|
+
text: string,
|
|
155
|
+
update: "append" | "replace",
|
|
156
|
+
streaming: boolean,
|
|
157
|
+
options: TranscriptEventOptions = {},
|
|
158
|
+
): SubagentProgressEvent {
|
|
159
|
+
return { type: "transcript", phase, entryType, key, text, update, streaming, ...options };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function messageContentEvents(message: MessageLike | undefined, phase: "start" | "end"): SubagentProgressEvent[] {
|
|
163
|
+
if (message?.role !== "assistant" || !Array.isArray(message.content)) return [];
|
|
164
|
+
return message.content.flatMap((part, index) => {
|
|
165
|
+
if (!part || typeof part !== "object") return [];
|
|
166
|
+
const value = part as { type?: unknown; text?: unknown; thinking?: unknown; id?: unknown; name?: unknown; arguments?: unknown };
|
|
167
|
+
if (value.type === "text" && typeof value.text === "string") {
|
|
168
|
+
return [transcriptEvent(phase, "assistant-text", `content:${index}`, value.text, "replace", phase !== "end")];
|
|
169
|
+
}
|
|
170
|
+
if (value.type === "thinking") {
|
|
171
|
+
const text = typeof value.thinking === "string" ? value.thinking : typeof value.text === "string" ? value.text : "";
|
|
172
|
+
return [transcriptEvent(phase, "thinking", `content:${index}`, text, "replace", phase !== "end")];
|
|
173
|
+
}
|
|
174
|
+
if (value.type === "toolCall") {
|
|
175
|
+
return [transcriptEvent(phase, "tool-call", `content:${index}`, formatValue(value.arguments), "replace", phase !== "end", {
|
|
176
|
+
toolCallId: typeof value.id === "string" ? value.id : undefined,
|
|
177
|
+
toolName: typeof value.name === "string" ? value.name : undefined,
|
|
178
|
+
})];
|
|
179
|
+
}
|
|
180
|
+
return [];
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function assistantUpdateEvents(event: RawSubagentEvent): SubagentProgressEvent[] {
|
|
185
|
+
const update = event.assistantMessageEvent;
|
|
186
|
+
if (!update || typeof update !== "object") return messageContentEvents(event.message, "end");
|
|
187
|
+
const value = update as {
|
|
188
|
+
type?: unknown;
|
|
189
|
+
contentIndex?: unknown;
|
|
190
|
+
delta?: unknown;
|
|
191
|
+
content?: unknown;
|
|
192
|
+
id?: unknown;
|
|
193
|
+
toolName?: unknown;
|
|
194
|
+
toolCall?: { id?: unknown; name?: unknown; arguments?: unknown };
|
|
195
|
+
};
|
|
196
|
+
const type = typeof value.type === "string" ? value.type : "";
|
|
197
|
+
const index = typeof value.contentIndex === "number" ? value.contentIndex : 0;
|
|
198
|
+
const key = `content:${index}`;
|
|
199
|
+
switch (type) {
|
|
200
|
+
case "text_start":
|
|
201
|
+
return [transcriptEvent("start", "assistant-text", key, "", "replace", true)];
|
|
202
|
+
case "text_delta":
|
|
203
|
+
return [transcriptEvent("update", "assistant-text", key, typeof value.delta === "string" ? value.delta : "", "append", true)];
|
|
204
|
+
case "text_end":
|
|
205
|
+
return [transcriptEvent("end", "assistant-text", key, typeof value.content === "string" ? value.content : "", "replace", false)];
|
|
206
|
+
case "thinking_start":
|
|
207
|
+
return [transcriptEvent("start", "thinking", key, "", "replace", true)];
|
|
208
|
+
case "thinking_delta":
|
|
209
|
+
return [transcriptEvent("update", "thinking", key, typeof value.delta === "string" ? value.delta : "", "append", true)];
|
|
210
|
+
case "thinking_end":
|
|
211
|
+
return [transcriptEvent("end", "thinking", key, typeof value.content === "string" ? value.content : "", "replace", false)];
|
|
212
|
+
case "toolcall_start":
|
|
213
|
+
return [transcriptEvent("start", "tool-call", key, "", "replace", true, {
|
|
214
|
+
toolCallId: typeof value.id === "string" ? value.id : undefined,
|
|
215
|
+
toolName: typeof value.toolName === "string" ? value.toolName : undefined,
|
|
216
|
+
})];
|
|
217
|
+
case "toolcall_delta":
|
|
218
|
+
return [transcriptEvent("update", "tool-call", key, typeof value.delta === "string" ? value.delta : "", "append", true, {
|
|
219
|
+
toolCallId: typeof value.id === "string" ? value.id : undefined,
|
|
220
|
+
toolName: typeof value.toolName === "string" ? value.toolName : undefined,
|
|
221
|
+
})];
|
|
222
|
+
case "toolcall_end":
|
|
223
|
+
return [transcriptEvent("end", "tool-call", key, formatValue(value.toolCall?.arguments), "replace", false, {
|
|
224
|
+
toolCallId: typeof value.toolCall?.id === "string" ? value.toolCall.id : typeof value.id === "string" ? value.id : undefined,
|
|
225
|
+
toolName: typeof value.toolCall?.name === "string" ? value.toolCall.name : typeof value.toolName === "string" ? value.toolName : undefined,
|
|
226
|
+
})];
|
|
227
|
+
default:
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Normalize the JSONL events emitted by `pi --mode json` into the small
|
|
234
|
+
* protocol consumed by the refinement overlay. Unknown events are ignored so
|
|
235
|
+
* adding progress support cannot make result parsing version-fragile.
|
|
236
|
+
*/
|
|
237
|
+
export function normalizeSubagentEvent(value: unknown): SubagentProgressEvent[] {
|
|
238
|
+
if (!value || typeof value !== "object") return [];
|
|
239
|
+
const event = value as RawSubagentEvent;
|
|
240
|
+
if (typeof event.type !== "string") return [];
|
|
241
|
+
|
|
242
|
+
if (event.type === "turn_start") {
|
|
243
|
+
return [{ type: "turn", phase: "start", ...(typeof event.turnIndex === "number" ? { turnIndex: event.turnIndex } : {}) }];
|
|
244
|
+
}
|
|
245
|
+
if (event.type === "turn_end") {
|
|
246
|
+
return [{ type: "turn", phase: "end", ...(typeof event.turnIndex === "number" ? { turnIndex: event.turnIndex } : {}) }];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (event.type === "message_start") return messageContentEvents(event.message, "start");
|
|
250
|
+
if (event.type === "message_update") return assistantUpdateEvents(event);
|
|
251
|
+
if (event.type === "message_end") return messageContentEvents(event.message, "end");
|
|
252
|
+
|
|
253
|
+
if (event.type === "tool_execution_start") {
|
|
254
|
+
return [transcriptEvent("start", "tool-call", `tool:${String(event.toolCallId ?? "unknown")}`, formatValue(event.args), "replace", true, {
|
|
255
|
+
toolCallId: typeof event.toolCallId === "string" ? event.toolCallId : undefined,
|
|
256
|
+
toolName: typeof event.toolName === "string" ? event.toolName : undefined,
|
|
257
|
+
})];
|
|
258
|
+
}
|
|
259
|
+
if (event.type === "tool_execution_update") {
|
|
260
|
+
return [transcriptEvent("update", "tool-result", `tool:${String(event.toolCallId ?? "unknown")}`, formatValue(event.partialResult), "replace", true, {
|
|
261
|
+
toolCallId: typeof event.toolCallId === "string" ? event.toolCallId : undefined,
|
|
262
|
+
toolName: typeof event.toolName === "string" ? event.toolName : undefined,
|
|
263
|
+
})];
|
|
264
|
+
}
|
|
265
|
+
if (event.type === "tool_execution_end") {
|
|
266
|
+
return [transcriptEvent("end", "tool-result", `tool:${String(event.toolCallId ?? "unknown")}`, formatValue(event.result), "replace", false, {
|
|
267
|
+
toolCallId: typeof event.toolCallId === "string" ? event.toolCallId : undefined,
|
|
268
|
+
toolName: typeof event.toolName === "string" ? event.toolName : undefined,
|
|
269
|
+
isError: typeof event.isError === "boolean" ? event.isError : undefined,
|
|
270
|
+
})];
|
|
271
|
+
}
|
|
83
272
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
args.push("--append-system-prompt", promptFile);
|
|
87
|
-
args.push(`Task: ${options.task}`);
|
|
273
|
+
return [];
|
|
274
|
+
}
|
|
88
275
|
|
|
276
|
+
function emitProgress(options: SubagentOptions, event: SubagentProgressEvent): void {
|
|
277
|
+
try {
|
|
278
|
+
options.onProgress?.(event);
|
|
279
|
+
} catch {
|
|
280
|
+
// A display sink must not be able to fail the child runner.
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const DEFAULT_TIMEOUT_MS = 60 * 60 * 1000;
|
|
285
|
+
|
|
286
|
+
export async function runPiSubagent(options: SubagentOptions): Promise<SubagentResult> {
|
|
287
|
+
const tools = options.tools ?? ["read", "grep", "find", "ls"];
|
|
288
|
+
let tmpDir = "";
|
|
89
289
|
const messages: MessageLike[] = [];
|
|
90
290
|
let stderr = "";
|
|
91
|
-
let
|
|
291
|
+
let termination: "abort" | "timeout" | null = null;
|
|
92
292
|
|
|
93
293
|
try {
|
|
294
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-plans-subagent-"));
|
|
295
|
+
const promptFile = path.join(tmpDir, "system-prompt.md");
|
|
296
|
+
fs.writeFileSync(promptFile, options.systemPrompt, { encoding: "utf8", mode: 0o600 });
|
|
297
|
+
|
|
298
|
+
const args: string[] = ["--mode", "json", "-p", "--no-session", "--tools", tools.join(",")];
|
|
299
|
+
if (options.model) args.push("--model", options.model);
|
|
300
|
+
args.push("--append-system-prompt", promptFile);
|
|
301
|
+
args.push(`Task: ${options.task}`);
|
|
302
|
+
|
|
94
303
|
const exitCode = await new Promise<number>((resolve) => {
|
|
95
304
|
const invocation = getPiInvocation(args);
|
|
96
305
|
const proc = spawn(invocation.command, invocation.args, {
|
|
@@ -99,20 +308,61 @@ export async function runPiSubagent(options: SubagentOptions): Promise<SubagentR
|
|
|
99
308
|
stdio: ["ignore", "pipe", "pipe"],
|
|
100
309
|
});
|
|
101
310
|
let buffer = "";
|
|
311
|
+
let closed = false;
|
|
312
|
+
let settled = false;
|
|
313
|
+
let timeoutTimer: ReturnType<typeof setTimeout> | undefined;
|
|
314
|
+
let forceKillTimer: ReturnType<typeof setTimeout> | undefined;
|
|
315
|
+
|
|
316
|
+
const onAbort = () => {
|
|
317
|
+
if (termination === null) termination = "abort";
|
|
318
|
+
killProc();
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const cleanup = () => {
|
|
322
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
323
|
+
if (forceKillTimer) clearTimeout(forceKillTimer);
|
|
324
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const finish = (code: number) => {
|
|
328
|
+
if (settled) return;
|
|
329
|
+
settled = true;
|
|
330
|
+
closed = true;
|
|
331
|
+
cleanup();
|
|
332
|
+
emitProgress(options, { type: "process", phase: "exited", code });
|
|
333
|
+
resolve(code);
|
|
334
|
+
};
|
|
102
335
|
|
|
103
336
|
const processLine = (line: string) => {
|
|
104
337
|
if (!line.trim()) return;
|
|
105
|
-
let event:
|
|
338
|
+
let event: RawSubagentEvent;
|
|
106
339
|
try {
|
|
107
|
-
event = JSON.parse(line);
|
|
340
|
+
event = JSON.parse(line) as RawSubagentEvent;
|
|
108
341
|
} catch {
|
|
109
342
|
return;
|
|
110
343
|
}
|
|
344
|
+
const progress = normalizeSubagentEvent(event);
|
|
345
|
+
for (const progressEvent of progress) emitProgress(options, progressEvent);
|
|
111
346
|
if ((event.type === "message_end" || event.type === "tool_result_end") && event.message) {
|
|
112
347
|
messages.push(event.message);
|
|
113
348
|
}
|
|
114
349
|
};
|
|
115
350
|
|
|
351
|
+
const killProc = () => {
|
|
352
|
+
if (settled || termination === null) return;
|
|
353
|
+
if (!proc.killed) proc.kill("SIGTERM");
|
|
354
|
+
if (!forceKillTimer) {
|
|
355
|
+
forceKillTimer = setTimeout(() => {
|
|
356
|
+
try {
|
|
357
|
+
if (!closed) proc.kill("SIGKILL");
|
|
358
|
+
} catch {
|
|
359
|
+
// The process already exited.
|
|
360
|
+
}
|
|
361
|
+
}, 5000);
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
emitProgress(options, { type: "process", phase: "started" });
|
|
116
366
|
proc.stdout.on("data", (data) => {
|
|
117
367
|
buffer += data.toString();
|
|
118
368
|
const lines = buffer.split("\n");
|
|
@@ -120,78 +370,57 @@ export async function runPiSubagent(options: SubagentOptions): Promise<SubagentR
|
|
|
120
370
|
for (const line of lines) processLine(line);
|
|
121
371
|
});
|
|
122
372
|
proc.stderr.on("data", (data) => {
|
|
123
|
-
|
|
373
|
+
const text = data.toString();
|
|
374
|
+
stderr += text;
|
|
375
|
+
emitProgress(options, { type: "stderr", text });
|
|
124
376
|
});
|
|
125
377
|
proc.on("close", (code) => {
|
|
126
378
|
if (buffer.trim()) processLine(buffer);
|
|
127
|
-
|
|
379
|
+
finish(code ?? 0);
|
|
380
|
+
});
|
|
381
|
+
proc.on("error", (error) => {
|
|
382
|
+
stderr += error instanceof Error ? error.message : String(error);
|
|
383
|
+
finish(1);
|
|
128
384
|
});
|
|
129
|
-
proc.on("error", () => resolve(1));
|
|
130
|
-
|
|
131
|
-
const killProc = () => {
|
|
132
|
-
wasAborted = true;
|
|
133
|
-
proc.kill("SIGTERM");
|
|
134
|
-
setTimeout(() => {
|
|
135
|
-
try {
|
|
136
|
-
if (!proc.killed) proc.kill("SIGKILL");
|
|
137
|
-
} catch {
|
|
138
|
-
/* already gone */
|
|
139
|
-
}
|
|
140
|
-
}, 5000);
|
|
141
|
-
};
|
|
142
385
|
|
|
143
|
-
|
|
144
|
-
|
|
386
|
+
timeoutTimer = setTimeout(() => {
|
|
387
|
+
if (termination === null) termination = "timeout";
|
|
388
|
+
killProc();
|
|
389
|
+
}, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
145
390
|
if (options.signal) {
|
|
146
|
-
if (options.signal.aborted)
|
|
391
|
+
if (options.signal.aborted) onAbort();
|
|
147
392
|
else options.signal.addEventListener("abort", onAbort, { once: true });
|
|
148
393
|
}
|
|
149
|
-
proc.on("close", () => {
|
|
150
|
-
clearTimeout(timer);
|
|
151
|
-
options.signal?.removeEventListener("abort", onAbort);
|
|
152
|
-
});
|
|
153
394
|
});
|
|
154
395
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
};
|
|
396
|
+
const turns = messages.filter((message) => message.role === "assistant").length;
|
|
397
|
+
const output = finalOutput(messages);
|
|
398
|
+
if (termination === "abort") {
|
|
399
|
+
return { ok: false, output, stderr, turns, cancelled: true, errorMessage: "Subagent was aborted" };
|
|
400
|
+
}
|
|
401
|
+
if (termination === "timeout") {
|
|
402
|
+
return { ok: false, output, stderr, turns, timedOut: true, errorMessage: "Subagent timed out" };
|
|
163
403
|
}
|
|
164
404
|
if (exitCode !== 0) {
|
|
165
|
-
return {
|
|
166
|
-
ok: false,
|
|
167
|
-
output: finalOutput(messages),
|
|
168
|
-
stderr,
|
|
169
|
-
turns: messages.filter((m) => m.role === "assistant").length,
|
|
170
|
-
errorMessage: `pi exited with code ${exitCode}`,
|
|
171
|
-
};
|
|
405
|
+
return { ok: false, output, stderr, turns, errorMessage: `pi exited with code ${exitCode}` };
|
|
172
406
|
}
|
|
173
|
-
const output = finalOutput(messages);
|
|
174
407
|
if (!output) {
|
|
175
|
-
return {
|
|
176
|
-
ok: false,
|
|
177
|
-
output: "",
|
|
178
|
-
stderr,
|
|
179
|
-
turns: messages.filter((m) => m.role === "assistant").length,
|
|
180
|
-
errorMessage: "subagent produced no final output",
|
|
181
|
-
};
|
|
408
|
+
return { ok: false, output: "", stderr, turns, errorMessage: "subagent produced no final output" };
|
|
182
409
|
}
|
|
183
410
|
return {
|
|
184
411
|
ok: true,
|
|
185
412
|
output,
|
|
186
|
-
model: [...messages].reverse().find((
|
|
413
|
+
model: [...messages].reverse().find((message) => message.role === "assistant" && message.model)?.model,
|
|
187
414
|
stderr,
|
|
188
|
-
turns
|
|
415
|
+
turns,
|
|
189
416
|
};
|
|
190
417
|
} finally {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
418
|
+
if (tmpDir) {
|
|
419
|
+
try {
|
|
420
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
421
|
+
} catch {
|
|
422
|
+
/* best effort */
|
|
423
|
+
}
|
|
195
424
|
}
|
|
196
425
|
}
|
|
197
426
|
}
|