@ryan_nookpi/pi-extension-subagent 0.5.4 → 0.5.6
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 +1 -1
- package/commands.ts +113 -31
- package/failure-telemetry.ts +43 -1
- package/lifecycle.ts +5 -1
- package/package.json +1 -1
- package/replay.ts +2 -1
- package/retry.ts +10 -0
- package/run-utils.ts +28 -7
- package/starter-pack.ts +1 -2
- package/tool-execute.ts +203 -89
- package/types.ts +6 -2
- package/widget.ts +6 -2
- package/seeds/agents/simplifier.md +0 -68
package/tool-execute.ts
CHANGED
|
@@ -32,7 +32,13 @@ import {
|
|
|
32
32
|
summarizeSubagentDisplayTask,
|
|
33
33
|
} from "./display-task.js";
|
|
34
34
|
import { ESCALATION_EXIT_CODE, readAndConsumeEscalation } from "./escalation.js";
|
|
35
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
classifySubagentFailure,
|
|
37
|
+
getSubagentRunStatusLabel,
|
|
38
|
+
getSubagentTerminalLabel,
|
|
39
|
+
isSubagentAbort,
|
|
40
|
+
type SubagentErrorClass,
|
|
41
|
+
} from "./failure-telemetry.js";
|
|
36
42
|
import {
|
|
37
43
|
formatContextUsageBar,
|
|
38
44
|
formatUsageStats,
|
|
@@ -43,6 +49,7 @@ import {
|
|
|
43
49
|
import { clearPendingGroupCompletion, upsertPendingGroupCompletion } from "./group-pending.js";
|
|
44
50
|
import { enqueueSubagentInvocation } from "./invocation-queue.js";
|
|
45
51
|
import { appendDisplayTaskUpdate, getSessionFileSize } from "./persisted-session.js";
|
|
52
|
+
import { invokeWithAutoRetry, MAX_SUBAGENT_AUTO_RETRIES } from "./retry.js";
|
|
46
53
|
import {
|
|
47
54
|
clearFinishedRuns,
|
|
48
55
|
evictStaleFinishedGroups,
|
|
@@ -299,6 +306,9 @@ export function diagnoseResultFailure(result: SingleResult): ResultFailureDiagno
|
|
|
299
306
|
output: finalOutput,
|
|
300
307
|
});
|
|
301
308
|
|
|
309
|
+
if (result.stopReason === "aborted") {
|
|
310
|
+
return { failed: true, errorClass: "aborted", reason: "Subagent execution was aborted." };
|
|
311
|
+
}
|
|
302
312
|
if (result.exitCode !== 0 || result.stopReason === "error") {
|
|
303
313
|
const overflowText = result.errorMessage || result.stderr || finalOutput;
|
|
304
314
|
if (isContextOverflowText(overflowText)) {
|
|
@@ -324,8 +334,6 @@ export function diagnoseResultFailure(result: SingleResult): ResultFailureDiagno
|
|
|
324
334
|
}
|
|
325
335
|
if (result.stopReason === "error")
|
|
326
336
|
return { failed: true, errorClass, reason: result.errorMessage || "Subagent reported stopReason=error." };
|
|
327
|
-
if (result.stopReason === "aborted")
|
|
328
|
-
return { failed: true, errorClass: errorClass ?? "aborted", reason: "Subagent execution was aborted." };
|
|
329
337
|
|
|
330
338
|
const hasAssistantText = finalOutput.length > 0;
|
|
331
339
|
if (hasAssistantText) return { failed: false };
|
|
@@ -469,13 +477,42 @@ function buildRunStartMessage(runState: CommandRunState, status: "started" | "re
|
|
|
469
477
|
};
|
|
470
478
|
}
|
|
471
479
|
|
|
480
|
+
function getFinalizedRunLabel(finalized: FinalizedRun): "completed" | "aborted" | "failed" {
|
|
481
|
+
return getSubagentTerminalLabel(finalized.isError, {
|
|
482
|
+
stopReason: finalized.result?.stopReason,
|
|
483
|
+
errorClass: finalized.runState.errorClass,
|
|
484
|
+
autoAbortReason: finalized.runState.autoAbortReason,
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function getFinalizedRunStatus(finalized: FinalizedRun): PipelineStepResult["status"] {
|
|
489
|
+
const label = getFinalizedRunLabel(finalized);
|
|
490
|
+
return label === "completed" ? "done" : label === "aborted" ? "aborted" : "error";
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function resolveGroupTerminalStatus(failed: number, aborted: number): "completed" | "error" | "aborted" {
|
|
494
|
+
if (failed > 0) return "error";
|
|
495
|
+
if (aborted > 0) return "aborted";
|
|
496
|
+
return "completed";
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function recordBatchResult(batch: BatchGroupState, finalized: FinalizedRun): void {
|
|
500
|
+
const runId = finalized.runState.id;
|
|
501
|
+
const status = finalized.runState.removed ? "aborted" : getFinalizedRunStatus(finalized);
|
|
502
|
+
batch.completedRunIds.add(runId);
|
|
503
|
+
if (status === "aborted") batch.abortedRunIds.add(runId);
|
|
504
|
+
else if (status === "error") batch.failedRunIds.add(runId);
|
|
505
|
+
batch.pendingResults.set(runId, finalized.rawOutput);
|
|
506
|
+
}
|
|
507
|
+
|
|
472
508
|
function buildRunCompletionMessage(finalized: FinalizedRun, options?: { display?: boolean }) {
|
|
473
|
-
const { runState, result,
|
|
509
|
+
const { runState, result, rawOutput } = finalized;
|
|
474
510
|
const usage = result ? formatUsageStats(result.usage, result.model) : "";
|
|
511
|
+
const terminalLabel = getFinalizedRunLabel(finalized);
|
|
475
512
|
return {
|
|
476
513
|
customType: "subagent-tool" as const,
|
|
477
514
|
content:
|
|
478
|
-
`[subagent:${runState.agent}#${runState.id}] ${
|
|
515
|
+
`[subagent:${runState.agent}#${runState.id}] ${terminalLabel}` +
|
|
479
516
|
`\nPrompt: ${truncateLines(runState.task, 2)}` +
|
|
480
517
|
(usage ? `\nUsage: ${usage}` : "") +
|
|
481
518
|
(runState.thoughtText ? `\nThought: ${runState.thoughtText}` : "") +
|
|
@@ -495,6 +532,7 @@ function buildRunCompletionMessage(finalized: FinalizedRun, options?: { display?
|
|
|
495
532
|
elapsedMs: runState.elapsedMs,
|
|
496
533
|
lastActivityAt: runState.lastActivityAt,
|
|
497
534
|
exitCode: result?.exitCode,
|
|
535
|
+
stopReason: result?.stopReason ?? (runState.errorClass === "aborted" ? "aborted" : undefined),
|
|
498
536
|
usage: result?.usage,
|
|
499
537
|
model: result?.model,
|
|
500
538
|
source: result?.agentSource,
|
|
@@ -560,6 +598,7 @@ function finalizeRunState(runState: CommandRunState, result: SingleResult): Fina
|
|
|
560
598
|
|
|
561
599
|
if (runState.autoAbortReason) {
|
|
562
600
|
runState.status = "error";
|
|
601
|
+
runState.errorClass = "aborted";
|
|
563
602
|
runState.elapsedMs = Date.now() - runState.startedAt;
|
|
564
603
|
runState.lastOutput = runState.autoAbortReason;
|
|
565
604
|
runState.lastLine = runState.autoAbortReason;
|
|
@@ -606,29 +645,46 @@ function finalizeRunState(runState: CommandRunState, result: SingleResult): Fina
|
|
|
606
645
|
return { runState, result, isError, rawOutput };
|
|
607
646
|
}
|
|
608
647
|
|
|
609
|
-
function
|
|
610
|
-
|
|
648
|
+
function formatOutcomeCounts(failed: number, aborted: number): string {
|
|
649
|
+
return [failed > 0 ? `${failed} failed` : "", aborted > 0 ? `${aborted} aborted` : ""].filter(Boolean).join(", ");
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function formatBatchSummary(
|
|
653
|
+
batchId: string,
|
|
654
|
+
runs: CommandRunState[],
|
|
655
|
+
terminalStatus: "completed" | "error" | "aborted",
|
|
656
|
+
): string {
|
|
657
|
+
const statuses = runs.map((run) => getSubagentRunStatusLabel(run.status, run.errorClass));
|
|
658
|
+
const headerStatus = runs.map((run, index) => `#${run.id} ${statuses[index]}`).join(", ");
|
|
659
|
+
const outcomeCounts = formatOutcomeCounts(
|
|
660
|
+
statuses.filter((status) => status === "error").length,
|
|
661
|
+
statuses.filter((status) => status === "aborted").length,
|
|
662
|
+
);
|
|
611
663
|
const body = runs
|
|
612
664
|
.map((run) => {
|
|
613
665
|
const summary = run.lastOutput?.trim() || run.lastLine?.trim() || "(no output)";
|
|
614
666
|
return `#${run.id} ${run.agent}\n- ${summary}`;
|
|
615
667
|
})
|
|
616
668
|
.join("\n\n");
|
|
617
|
-
return `[subagent-batch#${batchId}] ${terminalStatus}\nRuns: ${headerStatus}\n\n${body}`;
|
|
669
|
+
return `[subagent-batch#${batchId}] ${terminalStatus}\nRuns: ${headerStatus}${outcomeCounts ? `\nOutcomes: ${outcomeCounts}` : ""}\n\n${body}`;
|
|
618
670
|
}
|
|
619
671
|
|
|
620
672
|
function formatPipelineSummary(
|
|
621
673
|
pipelineId: string,
|
|
622
674
|
stepResults: PipelineStepResult[],
|
|
623
|
-
terminalStatus: "completed" | "stopped" | "error",
|
|
675
|
+
terminalStatus: "completed" | "stopped" | "error" | "aborted",
|
|
624
676
|
): string {
|
|
677
|
+
const outcomeCounts = formatOutcomeCounts(
|
|
678
|
+
stepResults.filter((step) => step.status === "error").length,
|
|
679
|
+
stepResults.filter((step) => step.status === "aborted").length,
|
|
680
|
+
);
|
|
625
681
|
const steps = stepResults
|
|
626
682
|
.map(
|
|
627
683
|
(step, index) =>
|
|
628
684
|
`Step ${index + 1} · #${step.runId} ${step.agent} · ${step.status}\nTask: ${step.task}\n${step.output}`,
|
|
629
685
|
)
|
|
630
686
|
.join("\n\n");
|
|
631
|
-
return `[subagent-chain#${pipelineId}] ${terminalStatus}\n\n${steps}`;
|
|
687
|
+
return `[subagent-chain#${pipelineId}] ${terminalStatus}${outcomeCounts ? `\nOutcomes: ${outcomeCounts}` : ""}\n\n${steps}`;
|
|
632
688
|
}
|
|
633
689
|
|
|
634
690
|
function previewOutput(output: string): string {
|
|
@@ -642,9 +698,10 @@ function formatBatchGroupStatus(store: SubagentStore, batch: BatchGroupState, de
|
|
|
642
698
|
const total = batch.runIds.length;
|
|
643
699
|
const done = batch.completedRunIds.size;
|
|
644
700
|
const failed = batch.failedRunIds.size;
|
|
701
|
+
const aborted = batch.abortedRunIds.size;
|
|
645
702
|
const header = `[subagent-batch#${batch.batchId}] running · ${done}/${total} finished${
|
|
646
703
|
failed > 0 ? `, ${failed} failed` : ""
|
|
647
|
-
}`;
|
|
704
|
+
}${aborted > 0 ? `, ${aborted} aborted` : ""}`;
|
|
648
705
|
const body = batch.runIds
|
|
649
706
|
.map((runId) => {
|
|
650
707
|
const run = store.commandRuns.get(runId);
|
|
@@ -735,7 +792,13 @@ function isInteractiveTuiContext(ctx: SubagentToolExecuteContext): boolean {
|
|
|
735
792
|
|
|
736
793
|
function finalizeRunError(runState: CommandRunState, error: unknown): FinalizedRun {
|
|
737
794
|
runState.status = "error";
|
|
738
|
-
runState.errorClass =
|
|
795
|
+
runState.errorClass = isSubagentAbort({
|
|
796
|
+
signal: runState.abortController?.signal,
|
|
797
|
+
autoAbortReason: runState.autoAbortReason,
|
|
798
|
+
error,
|
|
799
|
+
})
|
|
800
|
+
? "aborted"
|
|
801
|
+
: "process_error";
|
|
739
802
|
runState.elapsedMs = Date.now() - runState.startedAt;
|
|
740
803
|
runState.lastLine =
|
|
741
804
|
runState.autoAbortReason ?? (error instanceof Error ? error.message : "Subagent execution failed");
|
|
@@ -1266,6 +1329,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1266
1329
|
function clearRunAbortState(runState: CommandRunState): void {
|
|
1267
1330
|
clearParentAbortListener(runState.id);
|
|
1268
1331
|
runState.abortController = undefined;
|
|
1332
|
+
runState.abortPending = false;
|
|
1269
1333
|
}
|
|
1270
1334
|
|
|
1271
1335
|
function registerRunLaunch(config: RunLaunchConfig): CommandRunState {
|
|
@@ -1289,6 +1353,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1289
1353
|
runState.lastRetryReason = undefined;
|
|
1290
1354
|
runState.errorClass = undefined;
|
|
1291
1355
|
runState.autoAbortReason = undefined;
|
|
1356
|
+
runState.abortPending = false;
|
|
1292
1357
|
runState.removed = false;
|
|
1293
1358
|
runState.turnCount = Math.max(DEFAULT_TURN_COUNT, runState.turnCount || DEFAULT_TURN_COUNT) + 1;
|
|
1294
1359
|
runState.contextMode = runState.contextMode ?? (config.inheritMainContext ? "main" : "sub");
|
|
@@ -1350,40 +1415,62 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1350
1415
|
|
|
1351
1416
|
function launchRunInBackground(runState: CommandRunState, taskForAgent: string): Promise<FinalizedRun> {
|
|
1352
1417
|
let claudeCheckpointSent = !!runState.claudeSessionId;
|
|
1418
|
+
const abortSignal = runState.abortController?.signal;
|
|
1353
1419
|
const recordActivity = createRunActivityRecorder(pi, runState, (model) => resolveContextWindow(ctx, model));
|
|
1354
|
-
return
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
runState.
|
|
1359
|
-
|
|
1360
|
-
runState.
|
|
1361
|
-
runState.
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
if (
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1420
|
+
return invokeWithAutoRetry({
|
|
1421
|
+
maxRetries: MAX_SUBAGENT_AUTO_RETRIES,
|
|
1422
|
+
signal: abortSignal,
|
|
1423
|
+
onRetryScheduled: ({ retryIndex, maxRetries, delayMs, reason }) => {
|
|
1424
|
+
runState.retryCount = retryIndex;
|
|
1425
|
+
runState.lastRetryReason = reason;
|
|
1426
|
+
runState.lastActivityAt = Date.now();
|
|
1427
|
+
runState.lastLine = `Auto-retrying ${retryIndex}/${maxRetries} in ${Math.ceil(delayMs / 1000)}s: ${reason}`;
|
|
1428
|
+
runState.lastOutput = runState.lastLine;
|
|
1429
|
+
updateCommandRunsWidget(store);
|
|
1430
|
+
},
|
|
1431
|
+
invoke: () => {
|
|
1432
|
+
runState.persistedSessionBaseOffset = getSessionFileSize(runState.sessionFile);
|
|
1433
|
+
return enqueueSubagentInvocation(async () => {
|
|
1434
|
+
if (runState.removed || abortSignal?.aborted) throw new Error("Subagent was aborted");
|
|
1435
|
+
return runSingleAgent(
|
|
1436
|
+
ctx.cwd,
|
|
1437
|
+
agents,
|
|
1438
|
+
runState.agent,
|
|
1439
|
+
taskForAgent,
|
|
1440
|
+
runState.pipelineStepIndex,
|
|
1441
|
+
abortSignal,
|
|
1442
|
+
(partial) => {
|
|
1443
|
+
if (runState.removed || store.disposed) return;
|
|
1444
|
+
const current = partial.details?.results?.[0];
|
|
1445
|
+
if (!current) return;
|
|
1446
|
+
updateRunFromResult(runState, current);
|
|
1447
|
+
recordActivity();
|
|
1448
|
+
if (shouldRunAsync && !claudeCheckpointSent && runState.claudeSessionId) {
|
|
1449
|
+
claudeCheckpointSent = true;
|
|
1450
|
+
pi.sendMessage(buildRunStartMessage(runState, "started"), {
|
|
1451
|
+
deliverAs: "followUp",
|
|
1452
|
+
triggerTurn: false,
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
updateCommandRunsWidget(store);
|
|
1456
|
+
},
|
|
1457
|
+
(results) => makeDetails(mode, results),
|
|
1458
|
+
{
|
|
1459
|
+
sessionFile: runState.sessionFile,
|
|
1460
|
+
resumeSessionId: runState.claudeSessionId,
|
|
1461
|
+
sidecarSessionFile: runState.sessionFile,
|
|
1462
|
+
persistedSessionBaseOffset: runState.persistedSessionBaseOffset,
|
|
1463
|
+
onDiagnostic: createRunDiagnosticSink(pi, runState),
|
|
1464
|
+
},
|
|
1465
|
+
);
|
|
1466
|
+
});
|
|
1467
|
+
},
|
|
1468
|
+
}).then(({ result, retryCount }) => {
|
|
1469
|
+
runState.retryCount = retryCount;
|
|
1470
|
+
return runState.removed || abortSignal?.aborted
|
|
1471
|
+
? finalizeRunError(runState, new Error("Subagent was aborted"))
|
|
1472
|
+
: finalizeRunState(runState, result);
|
|
1473
|
+
});
|
|
1387
1474
|
}
|
|
1388
1475
|
|
|
1389
1476
|
if (hasSingle) {
|
|
@@ -1405,6 +1492,20 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1405
1492
|
isError: true,
|
|
1406
1493
|
};
|
|
1407
1494
|
}
|
|
1495
|
+
if (continueFromRun.abortPending) {
|
|
1496
|
+
return {
|
|
1497
|
+
content: [
|
|
1498
|
+
{
|
|
1499
|
+
type: "text",
|
|
1500
|
+
text: withIdleRunWarning(
|
|
1501
|
+
`Subagent #${continuationRunId} is still aborting. Wait for cancellation to settle before continuing it.`,
|
|
1502
|
+
),
|
|
1503
|
+
},
|
|
1504
|
+
],
|
|
1505
|
+
details: makeDetails("single"),
|
|
1506
|
+
isError: true,
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1408
1509
|
if (continueFromRun.status === "running") {
|
|
1409
1510
|
return {
|
|
1410
1511
|
content: [
|
|
@@ -1505,6 +1606,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1505
1606
|
};
|
|
1506
1607
|
}
|
|
1507
1608
|
|
|
1609
|
+
// No run-start sendMessage here: mid-turn triggerTurn:false messages land between tool call and result (pi #8537).
|
|
1508
1610
|
ctx.ui?.notify?.(
|
|
1509
1611
|
`${continueFromRun ? `Resumed subagent #${runState.id}` : `Started subagent #${runState.id}`}: ${resolvedAgent}`,
|
|
1510
1612
|
"info",
|
|
@@ -1529,6 +1631,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1529
1631
|
return;
|
|
1530
1632
|
}
|
|
1531
1633
|
|
|
1634
|
+
const terminalLabel = getFinalizedRunLabel(finalized);
|
|
1532
1635
|
const completionMessage = buildRunCompletionMessage(finalized);
|
|
1533
1636
|
if (isInOriginSession(ctx, originSessionFile)) {
|
|
1534
1637
|
pi.sendMessage(completionMessage, { deliverAs: "followUp", triggerTurn: true });
|
|
@@ -1539,14 +1642,13 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1539
1642
|
}
|
|
1540
1643
|
|
|
1541
1644
|
ctx.ui?.notify?.(
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
: `subagent tool run #${runState.id} (${resolvedAgent}) completed`,
|
|
1545
|
-
finalized.isError ? "error" : "info",
|
|
1645
|
+
`subagent tool run #${runState.id} (${resolvedAgent}) ${terminalLabel}`,
|
|
1646
|
+
terminalLabel === "completed" ? "info" : terminalLabel === "aborted" ? "warning" : "error",
|
|
1546
1647
|
);
|
|
1547
1648
|
} catch (error: unknown) {
|
|
1548
1649
|
if (runState.removed || store.disposed) return;
|
|
1549
1650
|
const finalized = finalizeRunError(runState, error);
|
|
1651
|
+
const terminalLabel = getFinalizedRunLabel(finalized);
|
|
1550
1652
|
const errorMessage = buildRunCompletionMessage(finalized);
|
|
1551
1653
|
if (isInOriginSession(ctx, originSessionFile)) {
|
|
1552
1654
|
pi.sendMessage(errorMessage, { deliverAs: "followUp", triggerTurn: true });
|
|
@@ -1555,7 +1657,10 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1555
1657
|
const entry = store.globalLiveRuns.get(runState.id);
|
|
1556
1658
|
if (entry) entry.pendingCompletion = makePendingCompletion(errorMessage, true);
|
|
1557
1659
|
}
|
|
1558
|
-
ctx.ui?.notify?.(
|
|
1660
|
+
ctx.ui?.notify?.(
|
|
1661
|
+
`subagent tool run #${runState.id} ${terminalLabel}: ${runState.lastLine}`,
|
|
1662
|
+
terminalLabel === "aborted" ? "warning" : "error",
|
|
1663
|
+
);
|
|
1559
1664
|
updateCommandRunsWidget(store);
|
|
1560
1665
|
} finally {
|
|
1561
1666
|
clearRunAbortState(runState);
|
|
@@ -1626,6 +1731,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1626
1731
|
runIds: runStates.map(({ runState }) => runState.id),
|
|
1627
1732
|
completedRunIds: new Set(),
|
|
1628
1733
|
failedRunIds: new Set(),
|
|
1734
|
+
abortedRunIds: new Set(),
|
|
1629
1735
|
originSessionFile,
|
|
1630
1736
|
createdAt: Date.now(),
|
|
1631
1737
|
pendingResults: new Map(),
|
|
@@ -1645,11 +1751,18 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1645
1751
|
}),
|
|
1646
1752
|
);
|
|
1647
1753
|
const orderedRuns = runStates.map(({ runState }) => runState);
|
|
1648
|
-
const hasError = finalizedRuns.some((finalized) => finalized.isError);
|
|
1649
|
-
const content = formatBatchSummary(batchId, orderedRuns, hasError ? "error" : "completed");
|
|
1650
1754
|
const batchForSnapshot = store.batchGroups.get(batchId);
|
|
1651
|
-
if (batchForSnapshot)
|
|
1652
|
-
|
|
1755
|
+
if (batchForSnapshot) {
|
|
1756
|
+
for (const finalized of finalizedRuns) recordBatchResult(batchForSnapshot, finalized);
|
|
1757
|
+
}
|
|
1758
|
+
const terminalStatus = resolveGroupTerminalStatus(
|
|
1759
|
+
batchForSnapshot?.failedRunIds.size ?? 0,
|
|
1760
|
+
batchForSnapshot?.abortedRunIds.size ?? 0,
|
|
1761
|
+
);
|
|
1762
|
+
const content = formatBatchSummary(batchId, orderedRuns, terminalStatus);
|
|
1763
|
+
if (batchForSnapshot) {
|
|
1764
|
+
retireFinishedGroup(store, snapshotBatchGroup(store, batchForSnapshot, terminalStatus));
|
|
1765
|
+
}
|
|
1653
1766
|
for (const { runState } of runStates) cleanupRunAfterFinalDelivery(runState.id);
|
|
1654
1767
|
clearPendingGroupCompletion("batch", batchId);
|
|
1655
1768
|
store.batchGroups.delete(batchId);
|
|
@@ -1671,7 +1784,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1671
1784
|
.filter((result): result is SingleResult => Boolean(result)),
|
|
1672
1785
|
runStates.map(({ runState }) => toLaunchSummary(runState, "batch")),
|
|
1673
1786
|
),
|
|
1674
|
-
isError:
|
|
1787
|
+
isError: terminalStatus !== "completed",
|
|
1675
1788
|
};
|
|
1676
1789
|
}
|
|
1677
1790
|
|
|
@@ -1682,16 +1795,14 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1682
1795
|
|
|
1683
1796
|
const batch = store.batchGroups.get(batchId);
|
|
1684
1797
|
if (!batch) return;
|
|
1685
|
-
batch
|
|
1686
|
-
if (finalized.isError) batch.failedRunIds.add(runState.id);
|
|
1687
|
-
batch.pendingResults.set(runState.id, finalized.rawOutput);
|
|
1798
|
+
recordBatchResult(batch, finalized);
|
|
1688
1799
|
updateCommandRunsWidget(store);
|
|
1689
1800
|
|
|
1690
1801
|
if (batch.completedRunIds.size === batch.runIds.length) {
|
|
1691
1802
|
const orderedRuns = batch.runIds
|
|
1692
1803
|
.map((runId) => store.commandRuns.get(runId))
|
|
1693
1804
|
.filter((run): run is CommandRunState => Boolean(run));
|
|
1694
|
-
const batchTerminalStatus = batch.failedRunIds.size
|
|
1805
|
+
const batchTerminalStatus = resolveGroupTerminalStatus(batch.failedRunIds.size, batch.abortedRunIds.size);
|
|
1695
1806
|
const content = formatBatchSummary(batchId, orderedRuns, batchTerminalStatus);
|
|
1696
1807
|
const message = {
|
|
1697
1808
|
customType: "subagent-tool" as const,
|
|
@@ -1700,7 +1811,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1700
1811
|
details: {
|
|
1701
1812
|
batchId,
|
|
1702
1813
|
runIds: batch.runIds,
|
|
1703
|
-
status:
|
|
1814
|
+
status: batchTerminalStatus === "completed" ? "done" : batchTerminalStatus,
|
|
1704
1815
|
runSummaries: orderedRuns.map((run) => buildRunAnalyticsSummary(run)),
|
|
1705
1816
|
},
|
|
1706
1817
|
};
|
|
@@ -1728,38 +1839,36 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1728
1839
|
removalReason: "trim",
|
|
1729
1840
|
});
|
|
1730
1841
|
ctx.ui?.notify?.(
|
|
1731
|
-
|
|
1842
|
+
batchTerminalStatus === "error"
|
|
1732
1843
|
? `subagent batch ${batchId} finished with errors`
|
|
1733
|
-
:
|
|
1734
|
-
|
|
1844
|
+
: batchTerminalStatus === "aborted"
|
|
1845
|
+
? `subagent batch ${batchId} aborted`
|
|
1846
|
+
: `subagent batch ${batchId} completed`,
|
|
1847
|
+
batchTerminalStatus === "error" ? "error" : batchTerminalStatus === "aborted" ? "warning" : "info",
|
|
1735
1848
|
);
|
|
1736
1849
|
}
|
|
1737
1850
|
} catch (error: unknown) {
|
|
1738
|
-
|
|
1739
|
-
runState.elapsedMs = Date.now() - runState.startedAt;
|
|
1740
|
-
runState.lastLine = error instanceof Error ? error.message : "Subagent execution failed";
|
|
1741
|
-
runState.lastOutput = runState.lastLine;
|
|
1851
|
+
const finalized = finalizeRunError(runState, error);
|
|
1742
1852
|
const batch = store.batchGroups.get(batchId);
|
|
1743
1853
|
if (!batch) return;
|
|
1744
|
-
batch
|
|
1745
|
-
batch.failedRunIds.add(runState.id);
|
|
1746
|
-
batch.pendingResults.set(runState.id, runState.lastLine);
|
|
1854
|
+
recordBatchResult(batch, finalized);
|
|
1747
1855
|
if (batch.completedRunIds.size === batch.runIds.length) {
|
|
1748
1856
|
const orderedRuns = batch.runIds
|
|
1749
1857
|
.map((runId) => store.commandRuns.get(runId))
|
|
1750
1858
|
.filter((run): run is CommandRunState => Boolean(run));
|
|
1859
|
+
const batchTerminalStatus = resolveGroupTerminalStatus(batch.failedRunIds.size, batch.abortedRunIds.size);
|
|
1751
1860
|
const message = {
|
|
1752
1861
|
customType: "subagent-tool" as const,
|
|
1753
|
-
content: formatBatchSummary(batchId, orderedRuns,
|
|
1862
|
+
content: formatBatchSummary(batchId, orderedRuns, batchTerminalStatus),
|
|
1754
1863
|
display: true,
|
|
1755
1864
|
details: {
|
|
1756
1865
|
batchId,
|
|
1757
1866
|
runIds: batch.runIds,
|
|
1758
|
-
status:
|
|
1867
|
+
status: batchTerminalStatus,
|
|
1759
1868
|
runSummaries: orderedRuns.map((run) => buildRunAnalyticsSummary(run)),
|
|
1760
1869
|
},
|
|
1761
1870
|
};
|
|
1762
|
-
retireFinishedGroup(store, snapshotBatchGroup(store, batch,
|
|
1871
|
+
retireFinishedGroup(store, snapshotBatchGroup(store, batch, batchTerminalStatus));
|
|
1763
1872
|
if (isInOriginSession(ctx, batch.originSessionFile)) {
|
|
1764
1873
|
pi.sendMessage(message, { deliverAs: "followUp", triggerTurn: true });
|
|
1765
1874
|
clearPendingGroupCompletion("batch", batchId);
|
|
@@ -1777,6 +1886,8 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1777
1886
|
}
|
|
1778
1887
|
}
|
|
1779
1888
|
updateCommandRunsWidget(store);
|
|
1889
|
+
} finally {
|
|
1890
|
+
clearRunAbortState(runState);
|
|
1780
1891
|
}
|
|
1781
1892
|
})();
|
|
1782
1893
|
}
|
|
@@ -1828,7 +1939,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1828
1939
|
|
|
1829
1940
|
if (!shouldRunAsync) {
|
|
1830
1941
|
let previousOutput = "";
|
|
1831
|
-
let terminalStatus: "completed" | "stopped" | "error" = "completed";
|
|
1942
|
+
let terminalStatus: "completed" | "stopped" | "error" | "aborted" = "completed";
|
|
1832
1943
|
const finalizedRuns: FinalizedRun[] = [];
|
|
1833
1944
|
const pipeline = store.pipelines.get(pipelineId);
|
|
1834
1945
|
if (!pipeline) {
|
|
@@ -1895,29 +2006,30 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1895
2006
|
finalizedRuns.push(finalized);
|
|
1896
2007
|
|
|
1897
2008
|
if (runState.removed) {
|
|
1898
|
-
terminalStatus =
|
|
2009
|
+
terminalStatus = "aborted";
|
|
1899
2010
|
pipeline.stepResults.push({
|
|
1900
2011
|
runId: runState.id,
|
|
1901
2012
|
agent: runState.agent,
|
|
1902
2013
|
task: step.task,
|
|
1903
2014
|
output: finalized.rawOutput || "Run removed before pipeline completion.",
|
|
1904
|
-
status: "
|
|
2015
|
+
status: "aborted",
|
|
1905
2016
|
});
|
|
1906
2017
|
break;
|
|
1907
2018
|
}
|
|
1908
2019
|
|
|
2020
|
+
const stepStatus = getFinalizedRunStatus(finalized);
|
|
1909
2021
|
pipeline.stepResults.push({
|
|
1910
2022
|
runId: runState.id,
|
|
1911
2023
|
agent: runState.agent,
|
|
1912
2024
|
task: step.task,
|
|
1913
2025
|
output: finalized.rawOutput,
|
|
1914
|
-
status:
|
|
2026
|
+
status: stepStatus,
|
|
1915
2027
|
});
|
|
1916
2028
|
previousOutput = finalized.rawOutput;
|
|
1917
2029
|
updateCommandRunsWidget(store);
|
|
1918
2030
|
|
|
1919
|
-
if (
|
|
1920
|
-
terminalStatus =
|
|
2031
|
+
if (stepStatus !== "done") {
|
|
2032
|
+
terminalStatus = stepStatus;
|
|
1921
2033
|
break;
|
|
1922
2034
|
}
|
|
1923
2035
|
}
|
|
@@ -1933,7 +2045,9 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1933
2045
|
}
|
|
1934
2046
|
|
|
1935
2047
|
const hasError = pipeline.stepResults.some((step) => step.status === "error");
|
|
1936
|
-
|
|
2048
|
+
const hasAborted = pipeline.stepResults.some((step) => step.status === "aborted");
|
|
2049
|
+
if (hasError) terminalStatus = "error";
|
|
2050
|
+
else if (terminalStatus === "completed" && hasAborted) terminalStatus = "aborted";
|
|
1937
2051
|
const content = formatPipelineSummary(pipelineId, pipeline.stepResults, terminalStatus);
|
|
1938
2052
|
retireFinishedGroup(store, snapshotPipeline(pipeline, terminalStatus));
|
|
1939
2053
|
for (const runId of pipeline.stepRunIds) cleanupRunAfterFinalDelivery(runId);
|
|
@@ -1963,7 +2077,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1963
2077
|
|
|
1964
2078
|
void (async () => {
|
|
1965
2079
|
let previousOutput = "";
|
|
1966
|
-
let terminalStatus: "completed" | "stopped" | "error" = "completed";
|
|
2080
|
+
let terminalStatus: "completed" | "stopped" | "error" | "aborted" = "completed";
|
|
1967
2081
|
try {
|
|
1968
2082
|
for (let index = 0; index < steps.length; index++) {
|
|
1969
2083
|
const pipeline = store.pipelines.get(pipelineId);
|
|
@@ -2021,13 +2135,13 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
2021
2135
|
clearRunAbortState(runState);
|
|
2022
2136
|
}
|
|
2023
2137
|
if (runState.removed) {
|
|
2024
|
-
terminalStatus =
|
|
2138
|
+
terminalStatus = "aborted";
|
|
2025
2139
|
pipeline.stepResults.push({
|
|
2026
2140
|
runId: runState.id,
|
|
2027
2141
|
agent: runState.agent,
|
|
2028
2142
|
task: step.task,
|
|
2029
2143
|
output: finalized.rawOutput || "Run removed before pipeline completion.",
|
|
2030
|
-
status: "
|
|
2144
|
+
status: "aborted",
|
|
2031
2145
|
});
|
|
2032
2146
|
break;
|
|
2033
2147
|
}
|
|
@@ -2037,14 +2151,14 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
2037
2151
|
agent: runState.agent,
|
|
2038
2152
|
task: step.task,
|
|
2039
2153
|
output: finalized.rawOutput,
|
|
2040
|
-
status: finalized
|
|
2154
|
+
status: getFinalizedRunStatus(finalized),
|
|
2041
2155
|
};
|
|
2042
2156
|
pipeline.stepResults.push(stepResult);
|
|
2043
2157
|
previousOutput = finalized.rawOutput;
|
|
2044
2158
|
updateCommandRunsWidget(store);
|
|
2045
2159
|
|
|
2046
|
-
if (
|
|
2047
|
-
terminalStatus =
|
|
2160
|
+
if (stepResult.status !== "done") {
|
|
2161
|
+
terminalStatus = stepResult.status;
|
|
2048
2162
|
break;
|
|
2049
2163
|
}
|
|
2050
2164
|
}
|
|
@@ -2064,9 +2178,9 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
2064
2178
|
const pipeline = store.pipelines.get(pipelineId);
|
|
2065
2179
|
if (pipeline) {
|
|
2066
2180
|
const hasError = pipeline.stepResults.some((step) => step.status === "error");
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2181
|
+
const hasAborted = pipeline.stepResults.some((step) => step.status === "aborted");
|
|
2182
|
+
if (hasError) terminalStatus = "error";
|
|
2183
|
+
else if (terminalStatus === "completed" && hasAborted) terminalStatus = "aborted";
|
|
2070
2184
|
const orderedRuns = pipeline.stepRunIds
|
|
2071
2185
|
.map((runId) => store.commandRuns.get(runId))
|
|
2072
2186
|
.filter((run): run is CommandRunState => Boolean(run));
|
package/types.ts
CHANGED
|
@@ -106,6 +106,8 @@ export interface CommandRunState {
|
|
|
106
106
|
lastToolOutputChars?: number;
|
|
107
107
|
/** Hang detector reason preserved until the normal finalizer emits the sole completion. */
|
|
108
108
|
autoAbortReason?: string;
|
|
109
|
+
/** True while a removed invocation is still settling; blocks same-ID continuation races. */
|
|
110
|
+
abortPending?: boolean;
|
|
109
111
|
runtime?: AgentRuntime;
|
|
110
112
|
claudeSessionId?: string;
|
|
111
113
|
claudeProjectDir?: string;
|
|
@@ -172,6 +174,7 @@ export interface BatchGroupState {
|
|
|
172
174
|
runIds: number[];
|
|
173
175
|
completedRunIds: Set<number>;
|
|
174
176
|
failedRunIds: Set<number>;
|
|
177
|
+
abortedRunIds: Set<number>;
|
|
175
178
|
originSessionFile: string;
|
|
176
179
|
createdAt: number;
|
|
177
180
|
pendingResults: Map<number, string>;
|
|
@@ -183,7 +186,7 @@ export interface PipelineStepResult {
|
|
|
183
186
|
agent: string;
|
|
184
187
|
task: string;
|
|
185
188
|
output: string;
|
|
186
|
-
status: "done" | "error";
|
|
189
|
+
status: "done" | "error" | "aborted";
|
|
187
190
|
}
|
|
188
191
|
|
|
189
192
|
export interface PipelineState {
|
|
@@ -213,10 +216,11 @@ export interface FinishedGroupMember {
|
|
|
213
216
|
export interface FinishedGroupSnapshot {
|
|
214
217
|
groupId: string;
|
|
215
218
|
kind: "batch" | "chain";
|
|
216
|
-
terminalStatus: "completed" | "error" | "stopped";
|
|
219
|
+
terminalStatus: "completed" | "error" | "aborted" | "stopped";
|
|
217
220
|
finishedAt: number;
|
|
218
221
|
total: number;
|
|
219
222
|
failed: number;
|
|
223
|
+
aborted: number;
|
|
220
224
|
members: FinishedGroupMember[];
|
|
221
225
|
}
|
|
222
226
|
|
package/widget.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { Box, Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
7
7
|
import { HANG_WARNING_IDLE_MS, PARENT_HINT } from "./constants.js";
|
|
8
|
+
import { getSubagentRunStatusLabel } from "./failure-telemetry.js";
|
|
8
9
|
import {
|
|
9
10
|
AGENT_NAME_PALETTE,
|
|
10
11
|
agentBgIndex,
|
|
@@ -79,9 +80,12 @@ export function cleanupCommandRunsWidgetTimer(): void {
|
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
function getStatusVisual(run: CommandRunState): { statusColor: ThemeColor; statusIcon: string } {
|
|
82
|
-
const
|
|
83
|
+
const statusLabel = getSubagentRunStatusLabel(run.status, run.errorClass);
|
|
84
|
+
const statusColor: ThemeColor =
|
|
85
|
+
statusLabel === "running" || statusLabel === "aborted" ? "warning" : statusLabel === "done" ? "success" : "error";
|
|
83
86
|
const spinnerFrame = SPINNER_FRAMES[Math.floor(Date.now() / SPINNER_INTERVAL_MS) % SPINNER_FRAMES.length];
|
|
84
|
-
const statusIcon =
|
|
87
|
+
const statusIcon =
|
|
88
|
+
statusLabel === "running" ? spinnerFrame : statusLabel === "done" ? "✓" : statusLabel === "aborted" ? "■" : "✗";
|
|
85
89
|
return { statusColor, statusIcon };
|
|
86
90
|
}
|
|
87
91
|
|