pi-subagents 0.13.3 → 0.14.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/CHANGELOG.md +21 -0
- package/README.md +35 -12
- package/agent-management.ts +15 -6
- package/agent-manager-detail.ts +12 -2
- package/agent-manager-edit.ts +75 -23
- package/agent-manager-list.ts +9 -2
- package/agent-manager.ts +199 -11
- package/agents.ts +315 -20
- package/artifacts.ts +11 -5
- package/async-execution.ts +92 -71
- package/chain-clarify.ts +45 -156
- package/chain-execution.ts +23 -63
- package/execution.ts +54 -49
- package/index.ts +1 -1
- package/intercom-bridge.ts +8 -0
- package/model-fallback.ts +8 -2
- package/package.json +1 -1
- package/schemas.ts +1 -1
- package/settings.ts +6 -4
- package/skills.ts +259 -77
- package/subagent-executor.ts +45 -15
- package/subagent-runner.ts +176 -51
- package/types.ts +64 -13
- package/utils.ts +5 -10
- package/worktree.ts +27 -9
package/subagent-runner.ts
CHANGED
|
@@ -3,6 +3,7 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
|
+
import type { Message } from "@mariozechner/pi-ai";
|
|
6
7
|
import { appendJsonl, getArtifactPaths } from "./artifacts.ts";
|
|
7
8
|
import { getPiSpawnCommand } from "./pi-spawn.ts";
|
|
8
9
|
import { captureSingleOutputSnapshot, resolveSingleOutput } from "./single-output.ts";
|
|
@@ -27,6 +28,7 @@ import {
|
|
|
27
28
|
} from "./parallel-utils.ts";
|
|
28
29
|
import { buildPiArgs, cleanupTempDir } from "./pi-args.ts";
|
|
29
30
|
import { formatModelAttemptNote, isRetryableModelFailure } from "./model-fallback.ts";
|
|
31
|
+
import { detectSubagentError, extractTextFromContent, extractToolArgsPreview, getFinalOutput } from "./utils.ts";
|
|
30
32
|
import {
|
|
31
33
|
cleanupWorktrees,
|
|
32
34
|
createWorktrees,
|
|
@@ -53,6 +55,7 @@ interface SubagentRunConfig {
|
|
|
53
55
|
asyncDir: string;
|
|
54
56
|
sessionId?: string | null;
|
|
55
57
|
piPackageRoot?: string;
|
|
58
|
+
piArgv1?: string;
|
|
56
59
|
worktreeSetupHook?: string;
|
|
57
60
|
worktreeSetupHookTimeoutMs?: number;
|
|
58
61
|
}
|
|
@@ -122,32 +125,44 @@ function emptyUsage(): Usage {
|
|
|
122
125
|
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
123
126
|
}
|
|
124
127
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
128
|
+
interface ChildEventContext {
|
|
129
|
+
eventsPath: string;
|
|
130
|
+
runId: string;
|
|
131
|
+
stepIndex: number;
|
|
132
|
+
agent: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
interface ChildUsage {
|
|
136
|
+
input?: number;
|
|
137
|
+
inputTokens?: number;
|
|
138
|
+
output?: number;
|
|
139
|
+
outputTokens?: number;
|
|
140
|
+
cacheRead?: number;
|
|
141
|
+
cacheWrite?: number;
|
|
142
|
+
cost?: { total?: number };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
type ChildMessage = Message & {
|
|
146
|
+
model?: string;
|
|
147
|
+
errorMessage?: string;
|
|
148
|
+
usage?: ChildUsage;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
interface ChildEvent {
|
|
152
|
+
type?: string;
|
|
153
|
+
message?: ChildMessage;
|
|
154
|
+
toolName?: string;
|
|
155
|
+
args?: Record<string, unknown>;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
interface RunPiStreamingResult {
|
|
159
|
+
stderr: string;
|
|
160
|
+
exitCode: number | null;
|
|
161
|
+
messages: Message[];
|
|
162
|
+
usage: Usage;
|
|
163
|
+
model?: string;
|
|
164
|
+
error?: string;
|
|
165
|
+
finalOutput: string;
|
|
151
166
|
}
|
|
152
167
|
|
|
153
168
|
function runPiStreaming(
|
|
@@ -156,36 +171,131 @@ function runPiStreaming(
|
|
|
156
171
|
outputFile: string,
|
|
157
172
|
env?: Record<string, string | undefined>,
|
|
158
173
|
piPackageRoot?: string,
|
|
174
|
+
piArgv1?: string,
|
|
159
175
|
maxSubagentDepth?: number,
|
|
160
|
-
|
|
176
|
+
childEventContext?: ChildEventContext,
|
|
177
|
+
): Promise<RunPiStreamingResult> {
|
|
161
178
|
return new Promise((resolve) => {
|
|
162
179
|
const outputStream = fs.createWriteStream(outputFile, { flags: "w" });
|
|
163
180
|
const spawnEnv = { ...process.env, ...(env ?? {}), ...getSubagentDepthEnv(maxSubagentDepth) };
|
|
164
|
-
const spawnSpec = getPiSpawnCommand(args,
|
|
181
|
+
const spawnSpec = getPiSpawnCommand(args, {
|
|
182
|
+
...(piPackageRoot ? { piPackageRoot } : {}),
|
|
183
|
+
...(piArgv1 ? { argv1: piArgv1 } : {}),
|
|
184
|
+
});
|
|
165
185
|
const child = spawn(spawnSpec.command, spawnSpec.args, { cwd, stdio: ["ignore", "pipe", "pipe"], env: spawnEnv });
|
|
166
|
-
let stdout = "";
|
|
167
186
|
let stderr = "";
|
|
187
|
+
let stdoutBuf = "";
|
|
188
|
+
let stderrBuf = "";
|
|
189
|
+
const messages: Message[] = [];
|
|
190
|
+
const usage = emptyUsage();
|
|
191
|
+
let model: string | undefined;
|
|
192
|
+
let error: string | undefined;
|
|
193
|
+
const rawStdoutLines: string[] = [];
|
|
194
|
+
|
|
195
|
+
const writeOutputLine = (line: string) => {
|
|
196
|
+
if (!line.trim()) return;
|
|
197
|
+
outputStream.write(`${line}\n`);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const writeOutputText = (text: string) => {
|
|
201
|
+
for (const line of text.split("\n")) {
|
|
202
|
+
writeOutputLine(line);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const appendChildEvent = (event: Record<string, unknown>) => {
|
|
207
|
+
if (!childEventContext) return;
|
|
208
|
+
appendJsonl(childEventContext.eventsPath, JSON.stringify({
|
|
209
|
+
...event,
|
|
210
|
+
subagentSource: "child",
|
|
211
|
+
subagentRunId: childEventContext.runId,
|
|
212
|
+
subagentStepIndex: childEventContext.stepIndex,
|
|
213
|
+
subagentAgent: childEventContext.agent,
|
|
214
|
+
observedAt: Date.now(),
|
|
215
|
+
}));
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const appendChildLine = (type: "subagent.child.stdout" | "subagent.child.stderr", line: string) => {
|
|
219
|
+
appendChildEvent({ type, line });
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const processStdoutLine = (line: string) => {
|
|
223
|
+
if (!line.trim()) return;
|
|
224
|
+
let event: ChildEvent;
|
|
225
|
+
try {
|
|
226
|
+
event = JSON.parse(line) as ChildEvent;
|
|
227
|
+
} catch {
|
|
228
|
+
rawStdoutLines.push(line);
|
|
229
|
+
writeOutputLine(line);
|
|
230
|
+
appendChildLine("subagent.child.stdout", line);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
appendChildEvent(event);
|
|
235
|
+
|
|
236
|
+
if (event.type === "tool_execution_start" && event.toolName) {
|
|
237
|
+
const toolArgs = extractToolArgsPreview(event.args ?? {});
|
|
238
|
+
writeOutputLine(toolArgs ? `${event.toolName}: ${toolArgs}` : event.toolName);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if ((event.type === "message_end" || event.type === "tool_result_end") && event.message) {
|
|
243
|
+
messages.push(event.message);
|
|
244
|
+
const text = extractTextFromContent(event.message.content);
|
|
245
|
+
if (text) writeOutputText(text);
|
|
246
|
+
|
|
247
|
+
if (event.type !== "message_end" || event.message.role !== "assistant") return;
|
|
248
|
+
if (event.message.model) model = event.message.model;
|
|
249
|
+
if (event.message.errorMessage) error = event.message.errorMessage;
|
|
250
|
+
const eventUsage = event.message.usage;
|
|
251
|
+
if (!eventUsage) return;
|
|
252
|
+
usage.turns++;
|
|
253
|
+
usage.input += eventUsage.input ?? eventUsage.inputTokens ?? 0;
|
|
254
|
+
usage.output += eventUsage.output ?? eventUsage.outputTokens ?? 0;
|
|
255
|
+
usage.cacheRead += eventUsage.cacheRead ?? 0;
|
|
256
|
+
usage.cacheWrite += eventUsage.cacheWrite ?? 0;
|
|
257
|
+
usage.cost += eventUsage.cost?.total ?? 0;
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const processStderrText = (text: string) => {
|
|
262
|
+
stderr += text;
|
|
263
|
+
stderrBuf += text;
|
|
264
|
+
outputStream.write(text);
|
|
265
|
+
if (!childEventContext) return;
|
|
266
|
+
const lines = stderrBuf.split("\n");
|
|
267
|
+
stderrBuf = lines.pop() || "";
|
|
268
|
+
for (const line of lines) {
|
|
269
|
+
if (!line.trim()) continue;
|
|
270
|
+
appendChildLine("subagent.child.stderr", line);
|
|
271
|
+
}
|
|
272
|
+
};
|
|
168
273
|
|
|
169
274
|
child.stdout.on("data", (chunk: Buffer) => {
|
|
170
275
|
const text = chunk.toString();
|
|
171
|
-
|
|
172
|
-
|
|
276
|
+
stdoutBuf += text;
|
|
277
|
+
const lines = stdoutBuf.split("\n");
|
|
278
|
+
stdoutBuf = lines.pop() || "";
|
|
279
|
+
for (const line of lines) processStdoutLine(line);
|
|
173
280
|
});
|
|
174
281
|
|
|
175
282
|
child.stderr.on("data", (chunk: Buffer) => {
|
|
176
|
-
|
|
177
|
-
stderr += text;
|
|
178
|
-
outputStream.write(text);
|
|
283
|
+
processStderrText(chunk.toString());
|
|
179
284
|
});
|
|
180
285
|
|
|
181
286
|
child.on("close", (exitCode) => {
|
|
287
|
+
if (stdoutBuf.trim()) processStdoutLine(stdoutBuf);
|
|
288
|
+
if (stderrBuf.trim()) appendChildLine("subagent.child.stderr", stderrBuf);
|
|
182
289
|
outputStream.end();
|
|
183
|
-
|
|
290
|
+
const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
|
|
291
|
+
resolve({ stderr, exitCode, messages, usage, model, error, finalOutput });
|
|
184
292
|
});
|
|
185
293
|
|
|
186
|
-
child.on("error", () => {
|
|
294
|
+
child.on("error", (spawnError) => {
|
|
187
295
|
outputStream.end();
|
|
188
|
-
|
|
296
|
+
const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
|
|
297
|
+
const spawnErrorMessage = spawnError instanceof Error ? spawnError.message : String(spawnError);
|
|
298
|
+
resolve({ stderr, exitCode: 1, messages, usage, model, error: error ?? spawnErrorMessage, finalOutput });
|
|
189
299
|
});
|
|
190
300
|
});
|
|
191
301
|
}
|
|
@@ -340,6 +450,7 @@ interface SingleStepContext {
|
|
|
340
450
|
flatStepCount: number;
|
|
341
451
|
outputFile: string;
|
|
342
452
|
piPackageRoot?: string;
|
|
453
|
+
piArgv1?: string;
|
|
343
454
|
}
|
|
344
455
|
|
|
345
456
|
/** Run a single pi agent step, returning output and metadata */
|
|
@@ -380,14 +491,13 @@ async function runSingleStep(
|
|
|
380
491
|
const attemptedModels: string[] = [];
|
|
381
492
|
const modelAttempts: ModelAttempt[] = [];
|
|
382
493
|
const attemptNotes: string[] = [];
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
| undefined;
|
|
494
|
+
const eventsPath = path.join(path.dirname(ctx.outputFile), "events.jsonl");
|
|
495
|
+
let finalResult: RunPiStreamingResult | undefined;
|
|
386
496
|
|
|
387
497
|
for (let index = 0; index < candidates.length; index++) {
|
|
388
498
|
const candidate = candidates[index];
|
|
389
499
|
const { args, env, tempDir } = buildPiArgs({
|
|
390
|
-
baseArgs: ["-p"],
|
|
500
|
+
baseArgs: ["--mode", "json", "-p"],
|
|
391
501
|
task,
|
|
392
502
|
sessionEnabled,
|
|
393
503
|
sessionDir,
|
|
@@ -400,28 +510,41 @@ async function runSingleStep(
|
|
|
400
510
|
mcpDirectTools: step.mcpDirectTools,
|
|
401
511
|
promptFileStem: step.agent,
|
|
402
512
|
});
|
|
403
|
-
const
|
|
404
|
-
|
|
513
|
+
const run = await runPiStreaming(
|
|
514
|
+
args,
|
|
515
|
+
step.cwd ?? ctx.cwd,
|
|
516
|
+
ctx.outputFile,
|
|
517
|
+
env,
|
|
518
|
+
ctx.piPackageRoot,
|
|
519
|
+
ctx.piArgv1,
|
|
520
|
+
step.maxSubagentDepth,
|
|
521
|
+
{ eventsPath, runId: ctx.id, stepIndex: ctx.flatIndex, agent: step.agent },
|
|
522
|
+
);
|
|
405
523
|
cleanupTempDir(tempDir);
|
|
406
524
|
|
|
407
|
-
const
|
|
408
|
-
const
|
|
525
|
+
const hiddenError = run.exitCode === 0 && !run.error ? detectSubagentError(run.messages) : null;
|
|
526
|
+
const effectiveExitCode = hiddenError?.hasError ? (hiddenError.exitCode ?? 1) : run.exitCode;
|
|
527
|
+
const error = hiddenError?.hasError
|
|
528
|
+
? hiddenError.details
|
|
529
|
+
? `${hiddenError.errorType} failed (exit ${effectiveExitCode}): ${hiddenError.details}`
|
|
530
|
+
: `${hiddenError.errorType} failed with exit code ${effectiveExitCode}`
|
|
531
|
+
: run.error || (run.exitCode !== 0 && run.stderr.trim() ? run.stderr.trim() : undefined);
|
|
409
532
|
const attempt: ModelAttempt = {
|
|
410
|
-
model: candidate ??
|
|
411
|
-
success:
|
|
412
|
-
exitCode:
|
|
533
|
+
model: candidate ?? run.model ?? step.model ?? "default",
|
|
534
|
+
success: effectiveExitCode === 0 && !error,
|
|
535
|
+
exitCode: effectiveExitCode,
|
|
413
536
|
error,
|
|
414
|
-
usage:
|
|
537
|
+
usage: run.usage,
|
|
415
538
|
};
|
|
416
539
|
modelAttempts.push(attempt);
|
|
417
540
|
if (candidate) attemptedModels.push(candidate);
|
|
418
|
-
finalResult = { ...run,
|
|
541
|
+
finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error };
|
|
419
542
|
if (attempt.success) break;
|
|
420
543
|
if (!isRetryableModelFailure(error) || index === candidates.length - 1) break;
|
|
421
544
|
attemptNotes.push(formatModelAttemptNote(attempt, candidates[index + 1]));
|
|
422
545
|
}
|
|
423
546
|
|
|
424
|
-
const rawOutput =
|
|
547
|
+
const rawOutput = finalResult?.finalOutput ?? "";
|
|
425
548
|
const resolvedOutput = step.outputPath && finalResult?.exitCode === 0
|
|
426
549
|
? resolveSingleOutput(step.outputPath, rawOutput, outputSnapshot)
|
|
427
550
|
: { fullOutput: rawOutput };
|
|
@@ -759,6 +882,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
759
882
|
flatIndex: fi, flatStepCount: flatSteps.length,
|
|
760
883
|
outputFile: path.join(asyncDir, `output-${fi}.log`),
|
|
761
884
|
piPackageRoot: config.piPackageRoot,
|
|
885
|
+
piArgv1: config.piArgv1,
|
|
762
886
|
});
|
|
763
887
|
if (task.sessionFile) {
|
|
764
888
|
latestSessionFile = task.sessionFile;
|
|
@@ -879,6 +1003,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
|
|
|
879
1003
|
flatIndex, flatStepCount: flatSteps.length,
|
|
880
1004
|
outputFile: path.join(asyncDir, `output-${flatIndex}.log`),
|
|
881
1005
|
piPackageRoot: config.piPackageRoot,
|
|
1006
|
+
piArgv1: config.piArgv1,
|
|
882
1007
|
});
|
|
883
1008
|
if (seqStep.sessionFile) {
|
|
884
1009
|
latestSessionFile = seqStep.sessionFile;
|
package/types.ts
CHANGED
|
@@ -40,17 +40,6 @@ export interface TokenUsage {
|
|
|
40
40
|
total: number;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
// ============================================================================
|
|
44
|
-
// Skills
|
|
45
|
-
// ============================================================================
|
|
46
|
-
|
|
47
|
-
export interface ResolvedSkill {
|
|
48
|
-
name: string;
|
|
49
|
-
path: string;
|
|
50
|
-
content: string;
|
|
51
|
-
source: "project" | "user";
|
|
52
|
-
}
|
|
53
|
-
|
|
54
43
|
// ============================================================================
|
|
55
44
|
// Progress Tracking
|
|
56
45
|
// ============================================================================
|
|
@@ -271,6 +260,8 @@ export interface RunSyncOptions {
|
|
|
271
260
|
modelOverride?: string;
|
|
272
261
|
/** Registry models available for heuristic bare-model resolution */
|
|
273
262
|
availableModels?: Array<{ provider: string; id: string; fullId: string }>;
|
|
263
|
+
/** Current parent-session provider to prefer for ambiguous bare model ids */
|
|
264
|
+
preferredModelProvider?: string;
|
|
274
265
|
/** Skills to inject (overrides agent default if provided) */
|
|
275
266
|
skills?: string[];
|
|
276
267
|
}
|
|
@@ -309,10 +300,66 @@ export const DEFAULT_ARTIFACT_CONFIG: ArtifactConfig = {
|
|
|
309
300
|
cleanupDays: 7,
|
|
310
301
|
};
|
|
311
302
|
|
|
303
|
+
function sanitizeTempScopeSegment(value: string): string {
|
|
304
|
+
const sanitized = value
|
|
305
|
+
.trim()
|
|
306
|
+
.replace(/[^A-Za-z0-9._-]+/g, "-")
|
|
307
|
+
.replace(/^-+|-+$/g, "");
|
|
308
|
+
return sanitized || "unknown";
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export function resolveTempScopeId(options?: {
|
|
312
|
+
env?: NodeJS.ProcessEnv;
|
|
313
|
+
getuid?: (() => number) | undefined;
|
|
314
|
+
userInfo?: (() => { username?: string | null }) | undefined;
|
|
315
|
+
homedir?: (() => string) | undefined;
|
|
316
|
+
}): string {
|
|
317
|
+
const env = options?.env ?? process.env;
|
|
318
|
+
const getuid = options && Object.hasOwn(options, "getuid")
|
|
319
|
+
? options.getuid
|
|
320
|
+
: process.getuid?.bind(process);
|
|
321
|
+
if (typeof getuid === "function") {
|
|
322
|
+
return `uid-${getuid()}`;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
for (const key of ["USERNAME", "USER", "LOGNAME"] as const) {
|
|
326
|
+
const value = env[key];
|
|
327
|
+
if (value) return `user-${sanitizeTempScopeSegment(value)}`;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const userInfo = options && Object.hasOwn(options, "userInfo")
|
|
331
|
+
? options.userInfo
|
|
332
|
+
: os.userInfo;
|
|
333
|
+
try {
|
|
334
|
+
const username = userInfo?.().username;
|
|
335
|
+
if (username) return `user-${sanitizeTempScopeSegment(username)}`;
|
|
336
|
+
} catch {
|
|
337
|
+
// Fall through to home-directory-based scoping.
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const homedir = env.USERPROFILE ?? env.HOME;
|
|
341
|
+
if (homedir) return `home-${sanitizeTempScopeSegment(homedir)}`;
|
|
342
|
+
|
|
343
|
+
const resolveHomedir = options && Object.hasOwn(options, "homedir")
|
|
344
|
+
? options.homedir
|
|
345
|
+
: os.homedir;
|
|
346
|
+
try {
|
|
347
|
+
const fallbackHomedir = resolveHomedir?.();
|
|
348
|
+
if (fallbackHomedir) return `home-${sanitizeTempScopeSegment(fallbackHomedir)}`;
|
|
349
|
+
} catch {
|
|
350
|
+
// Fall through to the last-resort shared scope.
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return "shared";
|
|
354
|
+
}
|
|
355
|
+
|
|
312
356
|
export const MAX_PARALLEL = 8;
|
|
313
357
|
export const MAX_CONCURRENCY = 4;
|
|
314
|
-
export const
|
|
315
|
-
export const
|
|
358
|
+
export const TEMP_ROOT_DIR = path.join(os.tmpdir(), `pi-subagents-${resolveTempScopeId()}`);
|
|
359
|
+
export const RESULTS_DIR = path.join(TEMP_ROOT_DIR, "async-subagent-results");
|
|
360
|
+
export const ASYNC_DIR = path.join(TEMP_ROOT_DIR, "async-subagent-runs");
|
|
361
|
+
export const CHAIN_RUNS_DIR = path.join(TEMP_ROOT_DIR, "chain-runs");
|
|
362
|
+
export const TEMP_ARTIFACTS_DIR = path.join(TEMP_ROOT_DIR, "artifacts");
|
|
316
363
|
export const WIDGET_KEY = "subagent-async";
|
|
317
364
|
export const SLASH_RESULT_TYPE = "subagent-slash-result";
|
|
318
365
|
export const SLASH_SUBAGENT_REQUEST_EVENT = "subagent:slash:request";
|
|
@@ -329,6 +376,10 @@ export const DEFAULT_FORK_PREAMBLE =
|
|
|
329
376
|
"Your sole job is to execute the task below. Do not continue or respond to the prior conversation " +
|
|
330
377
|
"— focus exclusively on completing this task using your tools.";
|
|
331
378
|
|
|
379
|
+
export function getAsyncConfigPath(suffix: string): string {
|
|
380
|
+
return path.join(TEMP_ROOT_DIR, `async-cfg-${suffix}.json`);
|
|
381
|
+
}
|
|
382
|
+
|
|
332
383
|
export function wrapForkTask(task: string, preamble?: string | false): string {
|
|
333
384
|
if (preamble === false) return task;
|
|
334
385
|
const effectivePreamble = preamble ?? DEFAULT_FORK_PREAMBLE;
|
package/utils.ts
CHANGED
|
@@ -224,15 +224,12 @@ export function getDisplayItems(messages: Message[]): DisplayItem[] {
|
|
|
224
224
|
* Detect errors in subagent execution from messages (only errors with no subsequent success)
|
|
225
225
|
*/
|
|
226
226
|
export function detectSubagentError(messages: Message[]): ErrorInfo {
|
|
227
|
-
// Step 1: Find the last assistant message with text content.
|
|
228
|
-
// If the agent produced a text response after encountering errors,
|
|
229
|
-
// it had a chance to recover — only errors AFTER this point matter.
|
|
230
227
|
let lastAssistantTextIndex = -1;
|
|
231
228
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
232
229
|
const msg = messages[i];
|
|
233
230
|
if (msg.role === "assistant") {
|
|
234
231
|
const hasText = Array.isArray(msg.content) && msg.content.some(
|
|
235
|
-
(c) => c.type === "text" && "text" in c &&
|
|
232
|
+
(c) => c.type === "text" && "text" in c && typeof c.text === "string" && c.text.trim().length > 0,
|
|
236
233
|
);
|
|
237
234
|
if (hasText) {
|
|
238
235
|
lastAssistantTextIndex = i;
|
|
@@ -241,28 +238,26 @@ export function detectSubagentError(messages: Message[]): ErrorInfo {
|
|
|
241
238
|
}
|
|
242
239
|
}
|
|
243
240
|
|
|
244
|
-
// Step 2: Only scan tool results AFTER the last assistant text message.
|
|
245
|
-
// Errors before the agent's final response are implicitly recovered.
|
|
246
241
|
const scanStart = lastAssistantTextIndex >= 0 ? lastAssistantTextIndex + 1 : 0;
|
|
247
242
|
|
|
248
|
-
// Step 3: Check tool results in the post-response window
|
|
249
243
|
for (let i = messages.length - 1; i >= scanStart; i--) {
|
|
250
244
|
const msg = messages[i];
|
|
251
245
|
if (msg.role !== "toolResult") continue;
|
|
246
|
+
const toolName = "toolName" in msg && typeof msg.toolName === "string" ? msg.toolName : undefined;
|
|
247
|
+
const isError = "isError" in msg && msg.isError === true;
|
|
252
248
|
|
|
253
|
-
if (
|
|
249
|
+
if (isError) {
|
|
254
250
|
const text = msg.content.find((c) => c.type === "text");
|
|
255
251
|
const details = text && "text" in text ? text.text : undefined;
|
|
256
252
|
const exitMatch = details?.match(/exit(?:ed)?\s*(?:with\s*)?(?:code|status)?\s*[:\s]?\s*(\d+)/i);
|
|
257
253
|
return {
|
|
258
254
|
hasError: true,
|
|
259
255
|
exitCode: exitMatch ? parseInt(exitMatch[1], 10) : 1,
|
|
260
|
-
errorType:
|
|
256
|
+
errorType: toolName || "tool",
|
|
261
257
|
details: details?.slice(0, 200),
|
|
262
258
|
};
|
|
263
259
|
}
|
|
264
260
|
|
|
265
|
-
const toolName = (msg as any).toolName;
|
|
266
261
|
if (toolName !== "bash") continue;
|
|
267
262
|
|
|
268
263
|
const text = msg.content.find((c) => c.type === "text");
|
package/worktree.ts
CHANGED
|
@@ -106,9 +106,11 @@ function resolveRepoState(cwd: string): RepoState {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
const toplevel = runGitChecked(cwd, ["rev-parse", "--show-toplevel"]).trim();
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
|
|
109
|
+
const rawPrefix = runGitChecked(cwd, ["rev-parse", "--show-prefix"]).trim();
|
|
110
|
+
const normalizedPrefix = rawPrefix
|
|
111
|
+
? path.normalize(rawPrefix.replace(/[\\/]+$/, ""))
|
|
112
|
+
: "";
|
|
113
|
+
const cwdRelative = normalizedPrefix === "." ? "" : normalizedPrefix;
|
|
112
114
|
|
|
113
115
|
const status = runGitChecked(toplevel, ["status", "--porcelain"]);
|
|
114
116
|
if (status.trim().length > 0) {
|
|
@@ -124,6 +126,7 @@ function normalizeComparableCwd(cwd: string): string {
|
|
|
124
126
|
try {
|
|
125
127
|
return fs.realpathSync(resolved);
|
|
126
128
|
} catch {
|
|
129
|
+
// Use the unresolved absolute path when realpath resolution is unavailable.
|
|
127
130
|
return resolved;
|
|
128
131
|
}
|
|
129
132
|
}
|
|
@@ -169,6 +172,7 @@ function linkNodeModulesIfPresent(toplevel: string, worktreePath: string): boole
|
|
|
169
172
|
fs.symlinkSync(nodeModulesPath, nodeModulesLinkPath);
|
|
170
173
|
return true;
|
|
171
174
|
} catch {
|
|
175
|
+
// Symlink creation is optional (e.g., unsupported filesystems on CI runners).
|
|
172
176
|
return false;
|
|
173
177
|
}
|
|
174
178
|
}
|
|
@@ -344,8 +348,12 @@ function createSingleWorktree(
|
|
|
344
348
|
syntheticPaths,
|
|
345
349
|
};
|
|
346
350
|
} catch (error) {
|
|
347
|
-
try { runGitChecked(toplevel, ["worktree", "remove", "--force", worktreePath]); } catch {
|
|
348
|
-
|
|
351
|
+
try { runGitChecked(toplevel, ["worktree", "remove", "--force", worktreePath]); } catch {
|
|
352
|
+
// Best-effort rollback; preserve the original setup failure.
|
|
353
|
+
}
|
|
354
|
+
try { runGitChecked(toplevel, ["branch", "-D", branch]); } catch {
|
|
355
|
+
// Best-effort rollback; preserve the original setup failure.
|
|
356
|
+
}
|
|
349
357
|
throw error;
|
|
350
358
|
}
|
|
351
359
|
}
|
|
@@ -453,12 +461,18 @@ function captureWorktreeDiff(
|
|
|
453
461
|
function writeEmptyPatch(patchPath: string): void {
|
|
454
462
|
try {
|
|
455
463
|
fs.writeFileSync(patchPath, "", "utf-8");
|
|
456
|
-
} catch {
|
|
464
|
+
} catch {
|
|
465
|
+
// Diff artifact writing is best-effort in error paths.
|
|
466
|
+
}
|
|
457
467
|
}
|
|
458
468
|
|
|
459
469
|
function cleanupSingleWorktree(repoCwd: string, worktree: WorktreeInfo): void {
|
|
460
|
-
try { runGitChecked(repoCwd, ["worktree", "remove", "--force", worktree.path]); } catch {
|
|
461
|
-
|
|
470
|
+
try { runGitChecked(repoCwd, ["worktree", "remove", "--force", worktree.path]); } catch {
|
|
471
|
+
// Cleanup is best-effort to avoid masking caller errors.
|
|
472
|
+
}
|
|
473
|
+
try { runGitChecked(repoCwd, ["branch", "-D", worktree.branch]); } catch {
|
|
474
|
+
// Cleanup is best-effort to avoid masking caller errors.
|
|
475
|
+
}
|
|
462
476
|
}
|
|
463
477
|
|
|
464
478
|
function hasWorktreeChanges(diff: WorktreeDiff): boolean {
|
|
@@ -502,6 +516,7 @@ export function diffWorktrees(setup: WorktreeSetup, agents: string[], diffsDir:
|
|
|
502
516
|
try {
|
|
503
517
|
fs.mkdirSync(diffsDir, { recursive: true });
|
|
504
518
|
} catch {
|
|
519
|
+
// Returning no diffs is safer than failing the whole command on artifact-dir issues.
|
|
505
520
|
return [];
|
|
506
521
|
}
|
|
507
522
|
|
|
@@ -513,6 +528,7 @@ export function diffWorktrees(setup: WorktreeSetup, agents: string[], diffsDir:
|
|
|
513
528
|
try {
|
|
514
529
|
diffs.push(captureWorktreeDiff(setup, worktree, agent, patchPath));
|
|
515
530
|
} catch {
|
|
531
|
+
// Preserve execution flow; failed diff capture maps to an empty per-task patch.
|
|
516
532
|
writeEmptyPatch(patchPath);
|
|
517
533
|
diffs.push(emptyDiff(index, agent, worktree.branch, patchPath));
|
|
518
534
|
}
|
|
@@ -525,7 +541,9 @@ export function cleanupWorktrees(setup: WorktreeSetup): void {
|
|
|
525
541
|
for (let index = setup.worktrees.length - 1; index >= 0; index--) {
|
|
526
542
|
cleanupSingleWorktree(setup.cwd, setup.worktrees[index]!);
|
|
527
543
|
}
|
|
528
|
-
try { runGitChecked(setup.cwd, ["worktree", "prune"]); } catch {
|
|
544
|
+
try { runGitChecked(setup.cwd, ["worktree", "prune"]); } catch {
|
|
545
|
+
// Pruning is best-effort cleanup.
|
|
546
|
+
}
|
|
529
547
|
}
|
|
530
548
|
|
|
531
549
|
export function formatWorktreeDiffSummary(diffs: WorktreeDiff[]): string {
|