pi-subagents 0.13.4 → 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 +11 -0
- package/README.md +32 -11
- 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 -73
- package/chain-clarify.ts +45 -156
- package/chain-execution.ts +23 -63
- package/execution.ts +53 -48
- package/index.ts +1 -1
- package/model-fallback.ts +8 -2
- package/package.json +1 -1
- package/schemas.ts +1 -1
- package/settings.ts +6 -4
- package/skills.ts +165 -75
- package/subagent-executor.ts +43 -9
- package/subagent-runner.ts +167 -50
- package/types.ts +64 -13
- package/utils.ts +5 -10
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,
|
|
@@ -123,32 +125,44 @@ function emptyUsage(): Usage {
|
|
|
123
125
|
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
124
126
|
}
|
|
125
127
|
|
|
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
|
-
|
|
151
|
-
|
|
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;
|
|
152
166
|
}
|
|
153
167
|
|
|
154
168
|
function runPiStreaming(
|
|
@@ -159,7 +173,8 @@ function runPiStreaming(
|
|
|
159
173
|
piPackageRoot?: string,
|
|
160
174
|
piArgv1?: string,
|
|
161
175
|
maxSubagentDepth?: number,
|
|
162
|
-
|
|
176
|
+
childEventContext?: ChildEventContext,
|
|
177
|
+
): Promise<RunPiStreamingResult> {
|
|
163
178
|
return new Promise((resolve) => {
|
|
164
179
|
const outputStream = fs.createWriteStream(outputFile, { flags: "w" });
|
|
165
180
|
const spawnEnv = { ...process.env, ...(env ?? {}), ...getSubagentDepthEnv(maxSubagentDepth) };
|
|
@@ -168,29 +183,119 @@ function runPiStreaming(
|
|
|
168
183
|
...(piArgv1 ? { argv1: piArgv1 } : {}),
|
|
169
184
|
});
|
|
170
185
|
const child = spawn(spawnSpec.command, spawnSpec.args, { cwd, stdio: ["ignore", "pipe", "pipe"], env: spawnEnv });
|
|
171
|
-
let stdout = "";
|
|
172
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
|
+
};
|
|
173
273
|
|
|
174
274
|
child.stdout.on("data", (chunk: Buffer) => {
|
|
175
275
|
const text = chunk.toString();
|
|
176
|
-
|
|
177
|
-
|
|
276
|
+
stdoutBuf += text;
|
|
277
|
+
const lines = stdoutBuf.split("\n");
|
|
278
|
+
stdoutBuf = lines.pop() || "";
|
|
279
|
+
for (const line of lines) processStdoutLine(line);
|
|
178
280
|
});
|
|
179
281
|
|
|
180
282
|
child.stderr.on("data", (chunk: Buffer) => {
|
|
181
|
-
|
|
182
|
-
stderr += text;
|
|
183
|
-
outputStream.write(text);
|
|
283
|
+
processStderrText(chunk.toString());
|
|
184
284
|
});
|
|
185
285
|
|
|
186
286
|
child.on("close", (exitCode) => {
|
|
287
|
+
if (stdoutBuf.trim()) processStdoutLine(stdoutBuf);
|
|
288
|
+
if (stderrBuf.trim()) appendChildLine("subagent.child.stderr", stderrBuf);
|
|
187
289
|
outputStream.end();
|
|
188
|
-
|
|
290
|
+
const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
|
|
291
|
+
resolve({ stderr, exitCode, messages, usage, model, error, finalOutput });
|
|
189
292
|
});
|
|
190
293
|
|
|
191
|
-
child.on("error", () => {
|
|
294
|
+
child.on("error", (spawnError) => {
|
|
192
295
|
outputStream.end();
|
|
193
|
-
|
|
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 });
|
|
194
299
|
});
|
|
195
300
|
});
|
|
196
301
|
}
|
|
@@ -386,14 +491,13 @@ async function runSingleStep(
|
|
|
386
491
|
const attemptedModels: string[] = [];
|
|
387
492
|
const modelAttempts: ModelAttempt[] = [];
|
|
388
493
|
const attemptNotes: string[] = [];
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
| undefined;
|
|
494
|
+
const eventsPath = path.join(path.dirname(ctx.outputFile), "events.jsonl");
|
|
495
|
+
let finalResult: RunPiStreamingResult | undefined;
|
|
392
496
|
|
|
393
497
|
for (let index = 0; index < candidates.length; index++) {
|
|
394
498
|
const candidate = candidates[index];
|
|
395
499
|
const { args, env, tempDir } = buildPiArgs({
|
|
396
|
-
baseArgs: ["-p"],
|
|
500
|
+
baseArgs: ["--mode", "json", "-p"],
|
|
397
501
|
task,
|
|
398
502
|
sessionEnabled,
|
|
399
503
|
sessionDir,
|
|
@@ -406,28 +510,41 @@ async function runSingleStep(
|
|
|
406
510
|
mcpDirectTools: step.mcpDirectTools,
|
|
407
511
|
promptFileStem: step.agent,
|
|
408
512
|
});
|
|
409
|
-
const
|
|
410
|
-
|
|
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
|
+
);
|
|
411
523
|
cleanupTempDir(tempDir);
|
|
412
524
|
|
|
413
|
-
const
|
|
414
|
-
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);
|
|
415
532
|
const attempt: ModelAttempt = {
|
|
416
|
-
model: candidate ??
|
|
417
|
-
success:
|
|
418
|
-
exitCode:
|
|
533
|
+
model: candidate ?? run.model ?? step.model ?? "default",
|
|
534
|
+
success: effectiveExitCode === 0 && !error,
|
|
535
|
+
exitCode: effectiveExitCode,
|
|
419
536
|
error,
|
|
420
|
-
usage:
|
|
537
|
+
usage: run.usage,
|
|
421
538
|
};
|
|
422
539
|
modelAttempts.push(attempt);
|
|
423
540
|
if (candidate) attemptedModels.push(candidate);
|
|
424
|
-
finalResult = { ...run,
|
|
541
|
+
finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error };
|
|
425
542
|
if (attempt.success) break;
|
|
426
543
|
if (!isRetryableModelFailure(error) || index === candidates.length - 1) break;
|
|
427
544
|
attemptNotes.push(formatModelAttemptNote(attempt, candidates[index + 1]));
|
|
428
545
|
}
|
|
429
546
|
|
|
430
|
-
const rawOutput =
|
|
547
|
+
const rawOutput = finalResult?.finalOutput ?? "";
|
|
431
548
|
const resolvedOutput = step.outputPath && finalResult?.exitCode === 0
|
|
432
549
|
? resolveSingleOutput(step.outputPath, rawOutput, outputSnapshot)
|
|
433
550
|
: { fullOutput: rawOutput };
|
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");
|