@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/README.md
CHANGED
|
@@ -68,7 +68,7 @@ Run `/subagents`, `subagent agents`, or the `list-agents` tool to inspect discov
|
|
|
68
68
|
|
|
69
69
|
### Optional starter pack
|
|
70
70
|
|
|
71
|
-
If no agents are found, `/subagents` can offer an optional, opinionated starter pack. It copies
|
|
71
|
+
If no agents are found, `/subagents` can offer an optional, opinionated starter pack. It copies eight agent templates, two example workflow skills, and missing global `subagent` settings. Existing files and configured values are not overwritten.
|
|
72
72
|
|
|
73
73
|
The starter pack is not required. Decline it if you prefer to define agents manually. It fills a missing `claudeRuntime` setting with `cli`; without that setting, the extension default is `sdk`.
|
|
74
74
|
|
package/commands.ts
CHANGED
|
@@ -35,6 +35,12 @@ import {
|
|
|
35
35
|
shouldSummarizeSubagentTask,
|
|
36
36
|
summarizeSubagentDisplayTask,
|
|
37
37
|
} from "./display-task.js";
|
|
38
|
+
import {
|
|
39
|
+
classifySubagentFailure,
|
|
40
|
+
getSubagentRunStatusLabel,
|
|
41
|
+
getSubagentTerminalLabel,
|
|
42
|
+
isSubagentAbort,
|
|
43
|
+
} from "./failure-telemetry.js";
|
|
38
44
|
import {
|
|
39
45
|
AGENT_NAME_PALETTE,
|
|
40
46
|
agentBgIndex,
|
|
@@ -215,10 +221,11 @@ class SubagentHistoryOverlay {
|
|
|
215
221
|
const marker = isSelected ? "▸" : " ";
|
|
216
222
|
|
|
217
223
|
// Status color
|
|
224
|
+
const statusLabel = getSubagentRunStatusLabel(run.status, run.errorClass);
|
|
218
225
|
let statusColor: "success" | "error" | "warning" | "dim" = "dim";
|
|
219
|
-
if (
|
|
220
|
-
else if (
|
|
221
|
-
else if (
|
|
226
|
+
if (statusLabel === "done") statusColor = "success";
|
|
227
|
+
else if (statusLabel === "error") statusColor = "error";
|
|
228
|
+
else if (statusLabel === "running" || statusLabel === "aborted") statusColor = "warning";
|
|
222
229
|
|
|
223
230
|
const timeLabel = new Date(run.startedAt).toLocaleTimeString([], {
|
|
224
231
|
hour: "2-digit",
|
|
@@ -227,7 +234,7 @@ class SubagentHistoryOverlay {
|
|
|
227
234
|
});
|
|
228
235
|
|
|
229
236
|
const removedBadge = run.removed ? theme.fg("dim", " [removed]") : "";
|
|
230
|
-
const statusStr = theme.fg(statusColor, `[${
|
|
237
|
+
const statusStr = theme.fg(statusColor, `[${statusLabel}]`);
|
|
231
238
|
const agentStr = theme.fg("accent", run.agent);
|
|
232
239
|
const taskPreview = run.task
|
|
233
240
|
.replace(/\s*\n+\s*/g, " ")
|
|
@@ -395,7 +402,7 @@ function attachRunResponseToEditor(ctx: ExtensionContext, run: CommandRunState,
|
|
|
395
402
|
async function showRunLatestResponseOverlay(ctx: ExtensionContext, run: CommandRunState): Promise<void> {
|
|
396
403
|
const response = getRunLatestResponse(run);
|
|
397
404
|
const contextLabel = run.contextMode === "main" ? "main" : "sub";
|
|
398
|
-
const subtitle = `#${run.id} · ${run.agent} · ${run.status} · ctx:${contextLabel} · turn:${run.turnCount ?? DEFAULT_TURN_COUNT}`;
|
|
405
|
+
const subtitle = `#${run.id} · ${run.agent} · ${getSubagentRunStatusLabel(run.status, run.errorClass)} · ctx:${contextLabel} · turn:${run.turnCount ?? DEFAULT_TURN_COUNT}`;
|
|
399
406
|
|
|
400
407
|
await ctx.ui.custom(
|
|
401
408
|
(tui, theme, _kb, done) => {
|
|
@@ -520,7 +527,11 @@ function restoreRunsFromSession(store: SubagentStore, ctx: any, pi?: ExtensionAP
|
|
|
520
527
|
try {
|
|
521
528
|
const entries = ctx.sessionManager.getEntries();
|
|
522
529
|
const restoredRuns = new Map<number, CommandRunState>();
|
|
523
|
-
const
|
|
530
|
+
const removedRuns = new Map<
|
|
531
|
+
number,
|
|
532
|
+
{ entryIndex: number; startedAt?: number; aborted: boolean; message?: string }
|
|
533
|
+
>();
|
|
534
|
+
const latestRunEntryIndexes = new Map<number, number>();
|
|
524
535
|
const displayTaskUpdates = new Map<number, { task?: string; displayTask?: string; startedAt?: number }>();
|
|
525
536
|
let maxRunId = 0;
|
|
526
537
|
|
|
@@ -541,13 +552,19 @@ function restoreRunsFromSession(store: SubagentStore, ctx: any, pi?: ExtensionAP
|
|
|
541
552
|
store.currentParentSessionFile = latestParentSessionFile;
|
|
542
553
|
|
|
543
554
|
// First pass: collect removed run IDs and persisted displayTask updates
|
|
544
|
-
for (const entry of entries) {
|
|
555
|
+
for (const [entryIndex, entry] of entries.entries()) {
|
|
545
556
|
if (entry.type === "custom") {
|
|
546
557
|
const ce = entry as any;
|
|
547
558
|
if (ce.customType === "subagent-removed") {
|
|
548
559
|
sawSubagentMarkers = true;
|
|
549
560
|
if (ce.data?.runId != null) {
|
|
550
|
-
|
|
561
|
+
removedRuns.set(ce.data.runId, {
|
|
562
|
+
entryIndex,
|
|
563
|
+
startedAt: toValidTimestampMs(ce.data?.startedAt),
|
|
564
|
+
aborted:
|
|
565
|
+
ce.data?.status === "aborted" || ce.data?.errorClass === "aborted" || ce.data?.stopReason === "aborted",
|
|
566
|
+
message: typeof ce.data?.message === "string" ? ce.data.message : undefined,
|
|
567
|
+
});
|
|
551
568
|
}
|
|
552
569
|
continue;
|
|
553
570
|
}
|
|
@@ -562,7 +579,7 @@ function restoreRunsFromSession(store: SubagentStore, ctx: any, pi?: ExtensionAP
|
|
|
562
579
|
}
|
|
563
580
|
}
|
|
564
581
|
|
|
565
|
-
for (const entry of entries) {
|
|
582
|
+
for (const [entryIndex, entry] of entries.entries()) {
|
|
566
583
|
if (entry.type !== "custom_message") continue;
|
|
567
584
|
const cm = entry as any;
|
|
568
585
|
if (cm.customType !== "subagent-command" && cm.customType !== "subagent-tool") continue;
|
|
@@ -571,6 +588,7 @@ function restoreRunsFromSession(store: SubagentStore, ctx: any, pi?: ExtensionAP
|
|
|
571
588
|
if (!d || typeof d.runId !== "number") continue;
|
|
572
589
|
|
|
573
590
|
const runId = d.runId;
|
|
591
|
+
latestRunEntryIndexes.set(runId, entryIndex);
|
|
574
592
|
if (runId > maxRunId) maxRunId = runId;
|
|
575
593
|
|
|
576
594
|
const existing = restoredRuns.get(runId);
|
|
@@ -584,11 +602,12 @@ function restoreRunsFromSession(store: SubagentStore, ctx: any, pi?: ExtensionAP
|
|
|
584
602
|
|
|
585
603
|
// Determine final status primarily from structured metadata.
|
|
586
604
|
const content = typeof cm.content === "string" ? cm.content : "";
|
|
605
|
+
const firstContentLine = content.split("\n", 1)[0]?.trim() ?? "";
|
|
587
606
|
const statusRaw = typeof d.status === "string" ? d.status.trim().toLowerCase() : "";
|
|
588
607
|
const statusFromDetails: "done" | "error" | null =
|
|
589
608
|
statusRaw === "done" || statusRaw === "completed"
|
|
590
609
|
? "done"
|
|
591
|
-
: statusRaw === "error" || statusRaw === "failed"
|
|
610
|
+
: statusRaw === "error" || statusRaw === "failed" || statusRaw === "aborted"
|
|
592
611
|
? "error"
|
|
593
612
|
: null;
|
|
594
613
|
const statusFromExitCode: "done" | "error" | null =
|
|
@@ -597,13 +616,25 @@ function restoreRunsFromSession(store: SubagentStore, ctx: any, pi?: ExtensionAP
|
|
|
597
616
|
typeof d.error === "string" && d.error.trim() ? "error" : null;
|
|
598
617
|
|
|
599
618
|
// Legacy fallback for old sessions where structured fields are missing.
|
|
600
|
-
const legacyStatusFromContent: "done" | "error" | null =
|
|
619
|
+
const legacyStatusFromContent: "done" | "error" | null = firstContentLine.includes("] completed")
|
|
601
620
|
? "done"
|
|
602
|
-
:
|
|
621
|
+
: firstContentLine.includes("] failed") ||
|
|
622
|
+
firstContentLine.includes("] error") ||
|
|
623
|
+
firstContentLine.includes("] aborted")
|
|
603
624
|
? "error"
|
|
604
625
|
: null;
|
|
605
626
|
|
|
606
627
|
const finalStatus = statusFromDetails ?? statusFromExitCode ?? statusFromErrorField ?? legacyStatusFromContent;
|
|
628
|
+
const hasStructuredTerminalOutcome =
|
|
629
|
+
statusFromDetails !== null ||
|
|
630
|
+
statusFromExitCode !== null ||
|
|
631
|
+
typeof d.stopReason === "string" ||
|
|
632
|
+
typeof d.errorClass === "string";
|
|
633
|
+
const isAbortedCompletion =
|
|
634
|
+
d.stopReason === "aborted" ||
|
|
635
|
+
d.errorClass === "aborted" ||
|
|
636
|
+
statusRaw === "aborted" ||
|
|
637
|
+
(!hasStructuredTerminalOutcome && firstContentLine.includes("] aborted"));
|
|
607
638
|
|
|
608
639
|
// Derive source from customType for backward-compatible restored run metadata.
|
|
609
640
|
const restoredSource: "tool" | "command" = cm.customType === "subagent-tool" ? "tool" : "command";
|
|
@@ -648,7 +679,7 @@ function restoreRunsFromSession(store: SubagentStore, ctx: any, pi?: ExtensionAP
|
|
|
648
679
|
contextMode: d.contextMode ?? existing?.contextMode,
|
|
649
680
|
usage: d.usage ?? existing?.usage,
|
|
650
681
|
model: d.model ?? existing?.model,
|
|
651
|
-
errorClass: d.errorClass ?? existing?.errorClass,
|
|
682
|
+
errorClass: d.errorClass ?? (isAbortedCompletion ? "aborted" : existing?.errorClass),
|
|
652
683
|
peakContextTokens: d.peakContextTokens ?? existing?.peakContextTokens,
|
|
653
684
|
lastToolName: d.lastToolName ?? existing?.lastToolName,
|
|
654
685
|
lastToolOutputChars: d.lastToolOutputChars ?? existing?.lastToolOutputChars,
|
|
@@ -723,8 +754,26 @@ function restoreRunsFromSession(store: SubagentStore, ctx: any, pi?: ExtensionAP
|
|
|
723
754
|
}
|
|
724
755
|
|
|
725
756
|
for (const [id, run] of restoredRuns) {
|
|
726
|
-
|
|
727
|
-
|
|
757
|
+
const removal = removedRuns.get(id);
|
|
758
|
+
const latestRunEntryIndex = latestRunEntryIndexes.get(id) ?? -1;
|
|
759
|
+
const removalApplies = removal
|
|
760
|
+
? removal.startedAt !== undefined
|
|
761
|
+
? removal.startedAt === run.startedAt
|
|
762
|
+
: removal.entryIndex > latestRunEntryIndex
|
|
763
|
+
: false;
|
|
764
|
+
if (removal && removalApplies) {
|
|
765
|
+
store.commandRuns.set(id, {
|
|
766
|
+
...run,
|
|
767
|
+
...(removal.aborted
|
|
768
|
+
? {
|
|
769
|
+
status: "error" as const,
|
|
770
|
+
errorClass: "aborted" as const,
|
|
771
|
+
lastLine: removal.message ?? run.lastLine,
|
|
772
|
+
lastOutput: removal.message ?? run.lastOutput,
|
|
773
|
+
}
|
|
774
|
+
: {}),
|
|
775
|
+
removed: true,
|
|
776
|
+
});
|
|
728
777
|
continue;
|
|
729
778
|
}
|
|
730
779
|
store.commandRuns.set(id, run);
|
|
@@ -1107,6 +1156,13 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): SubagentReg
|
|
|
1107
1156
|
const targetRunId = Number(firstToken);
|
|
1108
1157
|
const targetRun = continuationRun;
|
|
1109
1158
|
|
|
1159
|
+
if (targetRun.abortPending) {
|
|
1160
|
+
ctx.ui.notify(
|
|
1161
|
+
`Subagent #${targetRunId} is still aborting. Wait for cancellation to settle before continuing it.`,
|
|
1162
|
+
"warning",
|
|
1163
|
+
);
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1110
1166
|
if (targetRun.status === "running") {
|
|
1111
1167
|
ctx.ui.notify(`Subagent #${targetRunId} is already running.`, "warning");
|
|
1112
1168
|
return;
|
|
@@ -1274,6 +1330,7 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): SubagentReg
|
|
|
1274
1330
|
runState.lastRetryReason = undefined;
|
|
1275
1331
|
runState.errorClass = undefined;
|
|
1276
1332
|
runState.autoAbortReason = undefined;
|
|
1333
|
+
runState.abortPending = false;
|
|
1277
1334
|
runState.removed = false;
|
|
1278
1335
|
runState.deliveryMode = deliveryMode;
|
|
1279
1336
|
runState.turnCount = Math.max(DEFAULT_TURN_COUNT, runState.turnCount || DEFAULT_TURN_COUNT) + 1;
|
|
@@ -1509,6 +1566,22 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): SubagentReg
|
|
|
1509
1566
|
updateRunFromResult(runState, result);
|
|
1510
1567
|
const isError = result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
|
|
1511
1568
|
runState.status = isError ? "error" : "done";
|
|
1569
|
+
if (isError) {
|
|
1570
|
+
runState.errorClass =
|
|
1571
|
+
result.errorClass ??
|
|
1572
|
+
classifySubagentFailure({
|
|
1573
|
+
failed: true,
|
|
1574
|
+
stopReason: result.stopReason,
|
|
1575
|
+
exitCode: result.exitCode,
|
|
1576
|
+
errorMessage: result.errorMessage,
|
|
1577
|
+
stderr: result.stderr,
|
|
1578
|
+
output: getFinalOutput(result.messages),
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
const terminalLabel = getSubagentTerminalLabel(isError, {
|
|
1582
|
+
stopReason: result.stopReason,
|
|
1583
|
+
errorClass: runState.errorClass,
|
|
1584
|
+
});
|
|
1512
1585
|
runState.elapsedMs = Date.now() - runState.startedAt;
|
|
1513
1586
|
updateCommandRunsWidget(store);
|
|
1514
1587
|
|
|
@@ -1527,7 +1600,7 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): SubagentReg
|
|
|
1527
1600
|
const completionMessage = {
|
|
1528
1601
|
customType: "subagent-command" as const,
|
|
1529
1602
|
content:
|
|
1530
|
-
`[subagent:${selectedAgent}#${runId}] ${
|
|
1603
|
+
`[subagent:${selectedAgent}#${runId}] ${terminalLabel}` +
|
|
1531
1604
|
`\nPrompt: ${truncateLines(taskForDisplay, 2)}` +
|
|
1532
1605
|
(usage ? `\nUsage: ${usage}` : "") +
|
|
1533
1606
|
(runState.retryCount ? `\nRetries: ${runState.retryCount}/${MAX_SUBAGENT_AUTO_RETRIES}` : "") +
|
|
@@ -1548,6 +1621,7 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): SubagentReg
|
|
|
1548
1621
|
elapsedMs: runState.elapsedMs,
|
|
1549
1622
|
lastActivityAt: runState.lastActivityAt,
|
|
1550
1623
|
exitCode: result.exitCode,
|
|
1624
|
+
stopReason: result.stopReason,
|
|
1551
1625
|
usage: result.usage,
|
|
1552
1626
|
model: result.model,
|
|
1553
1627
|
source: result.agentSource,
|
|
@@ -1570,24 +1644,27 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): SubagentReg
|
|
|
1570
1644
|
runId,
|
|
1571
1645
|
`Hidden subagent #${runId} · ${selectedAgent}`,
|
|
1572
1646
|
completionMessage.content,
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
: `hidden subagent #${runId} (${selectedAgent}) completed`,
|
|
1576
|
-
isError ? "error" : "info",
|
|
1647
|
+
`hidden subagent #${runId} (${selectedAgent}) ${terminalLabel}`,
|
|
1648
|
+
terminalLabel === "completed" ? "info" : terminalLabel === "aborted" ? "warning" : "error",
|
|
1577
1649
|
);
|
|
1578
1650
|
} else {
|
|
1579
1651
|
deliverOrQueueCompletion(store, pi, ctx, runId, runState, completionMessage);
|
|
1580
1652
|
ctx.ui.notify(
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
: `subagent #${runId} (${selectedAgent}) completed`,
|
|
1584
|
-
isError ? "error" : "info",
|
|
1653
|
+
`subagent #${runId} (${selectedAgent}) ${terminalLabel}`,
|
|
1654
|
+
terminalLabel === "completed" ? "info" : terminalLabel === "aborted" ? "warning" : "error",
|
|
1585
1655
|
);
|
|
1586
1656
|
}
|
|
1587
1657
|
} catch (error: any) {
|
|
1588
1658
|
if (runState.removed || store.disposed) return;
|
|
1589
1659
|
runState.status = "error";
|
|
1590
|
-
runState.errorClass =
|
|
1660
|
+
runState.errorClass = isSubagentAbort({
|
|
1661
|
+
signal: runState.abortController?.signal,
|
|
1662
|
+
autoAbortReason: runState.autoAbortReason,
|
|
1663
|
+
error,
|
|
1664
|
+
})
|
|
1665
|
+
? "aborted"
|
|
1666
|
+
: "process_error";
|
|
1667
|
+
const terminalLabel = getSubagentTerminalLabel(true, { errorClass: runState.errorClass });
|
|
1591
1668
|
runState.elapsedMs = Date.now() - runState.startedAt;
|
|
1592
1669
|
runState.lastLine =
|
|
1593
1670
|
runState.autoAbortReason ?? (error?.message ? String(error.message) : "Subagent execution failed");
|
|
@@ -1596,7 +1673,7 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): SubagentReg
|
|
|
1596
1673
|
const cmdErrorMessage = {
|
|
1597
1674
|
customType: "subagent-command" as const,
|
|
1598
1675
|
content:
|
|
1599
|
-
`[subagent:${selectedAgent}#${runId}]
|
|
1676
|
+
`[subagent:${selectedAgent}#${runId}] ${terminalLabel}` +
|
|
1600
1677
|
`\nPrompt: ${truncateLines(taskForDisplay, 2)}` +
|
|
1601
1678
|
`\n\n${runState.lastLine}`,
|
|
1602
1679
|
display: true,
|
|
@@ -1615,6 +1692,7 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): SubagentReg
|
|
|
1615
1692
|
lastActivityAt: runState.lastActivityAt,
|
|
1616
1693
|
error: runState.lastLine,
|
|
1617
1694
|
errorClass: runState.errorClass,
|
|
1695
|
+
stopReason: runState.errorClass === "aborted" ? "aborted" : undefined,
|
|
1618
1696
|
peakContextTokens: runState.peakContextTokens,
|
|
1619
1697
|
lastToolName: runState.lastToolName,
|
|
1620
1698
|
lastToolOutputChars: runState.lastToolOutputChars,
|
|
@@ -1632,16 +1710,20 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): SubagentReg
|
|
|
1632
1710
|
runId,
|
|
1633
1711
|
`Hidden subagent #${runId} · ${selectedAgent}`,
|
|
1634
1712
|
cmdErrorMessage.content,
|
|
1635
|
-
`hidden subagent #${runId}
|
|
1636
|
-
"error",
|
|
1713
|
+
`hidden subagent #${runId} ${terminalLabel}: ${runState.lastLine}`,
|
|
1714
|
+
terminalLabel === "aborted" ? "warning" : "error",
|
|
1637
1715
|
);
|
|
1638
1716
|
} else {
|
|
1639
1717
|
deliverOrQueueCompletion(store, pi, ctx, runId, runState, cmdErrorMessage);
|
|
1640
|
-
ctx.ui.notify(
|
|
1718
|
+
ctx.ui.notify(
|
|
1719
|
+
`subagent #${runId} ${terminalLabel}: ${runState.lastLine}`,
|
|
1720
|
+
terminalLabel === "aborted" ? "warning" : "error",
|
|
1721
|
+
);
|
|
1641
1722
|
}
|
|
1642
1723
|
} finally {
|
|
1643
1724
|
clearInterval(tick);
|
|
1644
1725
|
runState.abortController = undefined;
|
|
1726
|
+
runState.abortPending = false;
|
|
1645
1727
|
if (!store.disposed) {
|
|
1646
1728
|
trimCommandRunHistory(store, {
|
|
1647
1729
|
maxRuns: 10,
|
|
@@ -1783,7 +1865,7 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): SubagentReg
|
|
|
1783
1865
|
run.status === "running" ? "(still running; no final output yet)" : run.lastLine || "(no output captured)";
|
|
1784
1866
|
const contextLabel = run.contextMode === "main" ? "main" : "isolated";
|
|
1785
1867
|
const content =
|
|
1786
|
-
`Subagent #${run.id} [${run.status}] ${run.agent} ctx:${contextLabel} turn:${run.turnCount ?? DEFAULT_TURN_COUNT} ${elapsedSec}s tools:${run.toolCalls}` +
|
|
1868
|
+
`Subagent #${run.id} [${getSubagentRunStatusLabel(run.status, run.errorClass)}] ${run.agent} ctx:${contextLabel} turn:${run.turnCount ?? DEFAULT_TURN_COUNT} ${elapsedSec}s tools:${run.toolCalls}` +
|
|
1787
1869
|
`\n${run.task}` +
|
|
1788
1870
|
usageLine +
|
|
1789
1871
|
`\n\n${output || fallback}`;
|
|
@@ -1840,7 +1922,7 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): SubagentReg
|
|
|
1840
1922
|
.replace(/\s*\n+\s*/g, " ")
|
|
1841
1923
|
.trim()
|
|
1842
1924
|
.slice(0, COMMAND_TASK_PREVIEW_CHARS);
|
|
1843
|
-
return `#${r.id} [${r.status}]${removed} ${r.agent}: ${task}`;
|
|
1925
|
+
return `#${r.id} [${getSubagentRunStatusLabel(r.status, r.errorClass)}]${removed} ${r.agent}: ${task}`;
|
|
1844
1926
|
});
|
|
1845
1927
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
1846
1928
|
return;
|
package/failure-telemetry.ts
CHANGED
|
@@ -5,6 +5,7 @@ export type SubagentErrorClass =
|
|
|
5
5
|
| "overloaded"
|
|
6
6
|
| "rate_limit"
|
|
7
7
|
| "tool_error"
|
|
8
|
+
| "network_error"
|
|
8
9
|
| "aborted"
|
|
9
10
|
| "process_error"
|
|
10
11
|
| "unknown";
|
|
@@ -18,6 +19,36 @@ export interface FailureTelemetryInput {
|
|
|
18
19
|
output?: string;
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
export interface AbortTelemetryInput {
|
|
23
|
+
stopReason?: string;
|
|
24
|
+
errorClass?: SubagentErrorClass;
|
|
25
|
+
signal?: AbortSignal;
|
|
26
|
+
autoAbortReason?: string;
|
|
27
|
+
error?: unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isSubagentAbort(input: AbortTelemetryInput): boolean {
|
|
31
|
+
return (
|
|
32
|
+
input.stopReason === "aborted" ||
|
|
33
|
+
input.errorClass === "aborted" ||
|
|
34
|
+
input.signal?.aborted === true ||
|
|
35
|
+
Boolean(input.autoAbortReason) ||
|
|
36
|
+
(input.error instanceof Error && input.error.message === "Subagent was aborted")
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getSubagentTerminalLabel(
|
|
41
|
+
failed: boolean,
|
|
42
|
+
input: AbortTelemetryInput,
|
|
43
|
+
): "completed" | "aborted" | "failed" {
|
|
44
|
+
if (!failed) return "completed";
|
|
45
|
+
return isSubagentAbort(input) ? "aborted" : "failed";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getSubagentRunStatusLabel(status: string, errorClass?: SubagentErrorClass): string {
|
|
49
|
+
return errorClass === "aborted" ? "aborted" : status;
|
|
50
|
+
}
|
|
51
|
+
|
|
21
52
|
const OVERLOADED_PATTERNS = [
|
|
22
53
|
/servers? (?:are|is) currently overloaded/i,
|
|
23
54
|
/server overloaded/i,
|
|
@@ -34,15 +65,26 @@ const TOOL_ERROR_PATTERNS = [
|
|
|
34
65
|
/invalid tool/i,
|
|
35
66
|
];
|
|
36
67
|
|
|
68
|
+
const NETWORK_PATTERNS = [/\bwebsocket\b/i, /\bconnection error\b/i];
|
|
69
|
+
const PROCESS_SIGNAL_TERMINATION_PATTERN = /\bprocess terminated by signal\b/i;
|
|
70
|
+
|
|
37
71
|
export function classifySubagentFailure(input: FailureTelemetryInput): SubagentErrorClass | undefined {
|
|
38
72
|
if (!input.failed) return undefined;
|
|
39
73
|
const text = [input.errorMessage, input.stderr, input.output].filter(Boolean).join("\n");
|
|
40
74
|
|
|
75
|
+
if (input.stopReason === "aborted") return "aborted";
|
|
41
76
|
if (isContextOverflowText(text)) return "context_overflow";
|
|
42
77
|
if (OVERLOADED_PATTERNS.some((pattern) => pattern.test(text))) return "overloaded";
|
|
43
78
|
if (RATE_LIMIT_PATTERNS.some((pattern) => pattern.test(text))) return "rate_limit";
|
|
44
|
-
if (input.stopReason === "aborted") return "aborted";
|
|
45
79
|
if (TOOL_ERROR_PATTERNS.some((pattern) => pattern.test(text))) return "tool_error";
|
|
80
|
+
const signalTerminated = PROCESS_SIGNAL_TERMINATION_PATTERN.test(input.stderr ?? "");
|
|
81
|
+
if (
|
|
82
|
+
!signalTerminated &&
|
|
83
|
+
(NETWORK_PATTERNS.some((pattern) => pattern.test(text)) ||
|
|
84
|
+
/^terminated\.?$/i.test(input.errorMessage?.trim() ?? ""))
|
|
85
|
+
) {
|
|
86
|
+
return "network_error";
|
|
87
|
+
}
|
|
46
88
|
if ((input.exitCode ?? 0) !== 0) return "process_error";
|
|
47
89
|
return "unknown";
|
|
48
90
|
}
|
package/lifecycle.ts
CHANGED
|
@@ -36,6 +36,7 @@ function reconcileRunWithPersistedSession(run: CommandRunState): void {
|
|
|
36
36
|
snapshot.completionMarker?.exitCode ??
|
|
37
37
|
(snapshot.terminalStopReason === "error" || snapshot.terminalStopReason === "aborted" ? 1 : 0);
|
|
38
38
|
run.status = exitCode === 0 ? "done" : "error";
|
|
39
|
+
if (snapshot.terminalStopReason === "aborted") run.errorClass = "aborted";
|
|
39
40
|
if (snapshot.finalOutput) {
|
|
40
41
|
run.lastOutput = snapshot.finalOutput;
|
|
41
42
|
run.lastLine = getLastNonEmptyLine(snapshot.finalOutput) || run.lastLine;
|
|
@@ -90,7 +91,7 @@ export function shutdownSubagentRuns(store: SubagentStore, pi: ExtensionAPI, rea
|
|
|
90
91
|
pi.sendMessage(
|
|
91
92
|
{
|
|
92
93
|
customType: run.source === "tool" ? "subagent-tool" : "subagent-command",
|
|
93
|
-
content: `[subagent:${run.agent}#${runId}]
|
|
94
|
+
content: `[subagent:${run.agent}#${runId}] aborted\n\n${message}`,
|
|
94
95
|
display: false,
|
|
95
96
|
details: {
|
|
96
97
|
runId,
|
|
@@ -100,6 +101,7 @@ export function shutdownSubagentRuns(store: SubagentStore, pi: ExtensionAPI, rea
|
|
|
100
101
|
status: "error",
|
|
101
102
|
error: message,
|
|
102
103
|
errorClass: run.errorClass,
|
|
104
|
+
stopReason: "aborted",
|
|
103
105
|
peakContextTokens: run.peakContextTokens,
|
|
104
106
|
lastToolName: run.lastToolName,
|
|
105
107
|
lastToolOutputChars: run.lastToolOutputChars,
|
|
@@ -155,7 +157,9 @@ export function checkForHungRuns(store: SubagentStore, _pi: ExtensionAPI): void
|
|
|
155
157
|
run.lastLine = reason;
|
|
156
158
|
run.lastOutput = reason;
|
|
157
159
|
run.status = "error";
|
|
160
|
+
run.errorClass = "aborted";
|
|
158
161
|
run.autoAbortReason = reason;
|
|
162
|
+
run.abortPending = Boolean(controller);
|
|
159
163
|
|
|
160
164
|
if (controller) {
|
|
161
165
|
controller.abort({ source: "hang_timeout", runId, idleMs, reason });
|
package/package.json
CHANGED
package/replay.ts
CHANGED
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
TOOL_RESULT_DETAILS_SUMMARY_MAX_CHARS,
|
|
45
45
|
USAGE_EXTRA_ROWS,
|
|
46
46
|
} from "./constants.js";
|
|
47
|
+
import { getSubagentRunStatusLabel } from "./failure-telemetry.js";
|
|
47
48
|
import { formatUsageStats, truncatePlainToWidth } from "./format.js";
|
|
48
49
|
import type { CommandRunState, SessionReplayItem } from "./types.js";
|
|
49
50
|
import { formatDuration, formatDurationBetween } from "./utils/time-utils.js";
|
|
@@ -351,7 +352,7 @@ export class SubagentSessionReplayOverlay {
|
|
|
351
352
|
theme.fg("toolTitle", theme.bold(`#${this.run.id} ${this.run.agent}`)) +
|
|
352
353
|
theme.fg(
|
|
353
354
|
"dim",
|
|
354
|
-
` [${this.run.status}] ctx:${contextLabel} turn:${this.run.turnCount ?? DEFAULT_TURN_COUNT} ${elapsedLabel} tools:${this.run.toolCalls}`,
|
|
355
|
+
` [${getSubagentRunStatusLabel(this.run.status, this.run.errorClass)}] ctx:${contextLabel} turn:${this.run.turnCount ?? DEFAULT_TURN_COUNT} ${elapsedLabel} tools:${this.run.toolCalls}`,
|
|
355
356
|
),
|
|
356
357
|
0,
|
|
357
358
|
0,
|
package/retry.ts
CHANGED
|
@@ -34,9 +34,13 @@ function buildFailureText(result: SingleResult): string {
|
|
|
34
34
|
.join("\n");
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
const PROCESS_SIGNAL_TERMINATION_PATTERN = /\bprocess terminated by signal\b/i;
|
|
38
|
+
|
|
37
39
|
const TRANSIENT_FAILURE_PATTERNS: RegExp[] = [
|
|
38
40
|
/\bnetwork\b/i,
|
|
41
|
+
/\bwebsocket\b/i,
|
|
39
42
|
/\bfetch failed\b/i,
|
|
43
|
+
/\bconnection error\b/i,
|
|
40
44
|
/\bconnection reset\b/i,
|
|
41
45
|
/\bsocket hang up\b/i,
|
|
42
46
|
/\btemporar(?:y|ily) unavailable\b/i,
|
|
@@ -70,12 +74,18 @@ function matchesTransientPattern(text: string): boolean {
|
|
|
70
74
|
|
|
71
75
|
export function diagnoseRetryableResult(result: SingleResult): RetryDecision {
|
|
72
76
|
if (result.stopReason === "aborted") return { retryable: false };
|
|
77
|
+
if (PROCESS_SIGNAL_TERMINATION_PATTERN.test(result.stderr)) return { retryable: false };
|
|
73
78
|
|
|
74
79
|
const finalOutput = getFinalOutput(result.messages).trim();
|
|
75
80
|
if (result.exitCode === 0 && result.stopReason !== "error" && finalOutput) {
|
|
76
81
|
return { retryable: false };
|
|
77
82
|
}
|
|
78
83
|
|
|
84
|
+
const errorMessage = result.errorMessage?.trim();
|
|
85
|
+
if (errorMessage && /^terminated\.?$/i.test(errorMessage)) {
|
|
86
|
+
return { retryable: true, reason: errorMessage };
|
|
87
|
+
}
|
|
88
|
+
|
|
79
89
|
const failureText = buildFailureText(result);
|
|
80
90
|
if (!failureText) return { retryable: false };
|
|
81
91
|
if (!matchesTransientPattern(failureText)) return { retryable: false };
|
package/run-utils.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import { FINISHED_GROUP_TTL_MS, MAX_FINISHED_GROUPS, STATUS_OUTPUT_PREVIEW_MAX_CHARS } from "./constants.js";
|
|
11
|
+
import { getSubagentRunStatusLabel } from "./failure-telemetry.js";
|
|
11
12
|
import type { SubagentStore } from "./store.js";
|
|
12
13
|
import type {
|
|
13
14
|
BatchGroupState,
|
|
@@ -57,7 +58,7 @@ export interface ClearFinishedRunsOptions {
|
|
|
57
58
|
export function formatCommandRunSummary(run: CommandRunState): string {
|
|
58
59
|
const elapsedSec = Math.max(0, Math.round(run.elapsedMs / 1000));
|
|
59
60
|
const contextLabel = run.contextMode === "main" ? "main" : "isolated";
|
|
60
|
-
return `#${run.id} [${run.status}] ${run.agent} ctx:${contextLabel} turn:${run.turnCount ?? 1} ${elapsedSec}s tools:${run.toolCalls}`;
|
|
61
|
+
return `#${run.id} [${getSubagentRunStatusLabel(run.status, run.errorClass)}] ${run.agent} ctx:${contextLabel} turn:${run.turnCount ?? 1} ${elapsedSec}s tools:${run.toolCalls}`;
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
/**
|
|
@@ -95,12 +96,19 @@ export function removeRun(store: SubagentStore, runId: number, options: RemoveRu
|
|
|
95
96
|
const globalEntry = store.globalLiveRuns.get(runId);
|
|
96
97
|
const controller = run.abortController ?? globalEntry?.abortController;
|
|
97
98
|
|
|
98
|
-
if (abortIfRunning && run.status === "running"
|
|
99
|
+
if (abortIfRunning && run.status === "running") {
|
|
99
100
|
const reason = options.reason ?? "Aborting by remove...";
|
|
101
|
+
run.status = "error";
|
|
102
|
+
run.errorClass = "aborted";
|
|
103
|
+
run.elapsedMs = Date.now() - run.startedAt;
|
|
104
|
+
run.lastActivityAt = Date.now();
|
|
100
105
|
run.lastLine = reason;
|
|
101
106
|
run.lastOutput = reason;
|
|
102
|
-
|
|
103
|
-
|
|
107
|
+
run.abortPending = Boolean(controller);
|
|
108
|
+
if (controller) {
|
|
109
|
+
controller.abort({ source: "remove_run", runId, reason, removalReason: options.removalReason });
|
|
110
|
+
aborted = true;
|
|
111
|
+
}
|
|
104
112
|
}
|
|
105
113
|
|
|
106
114
|
run.abortController = undefined;
|
|
@@ -112,6 +120,13 @@ export function removeRun(store: SubagentStore, runId: number, options: RemoveRu
|
|
|
112
120
|
if (persistRemovedEntry && options.pi && run.deliveryMode !== "humanOnly") {
|
|
113
121
|
const payload: Record<string, unknown> = { runId };
|
|
114
122
|
if (options.removalReason) payload.reason = options.removalReason;
|
|
123
|
+
if (run.errorClass === "aborted") {
|
|
124
|
+
payload.startedAt = run.startedAt;
|
|
125
|
+
payload.status = "aborted";
|
|
126
|
+
payload.errorClass = "aborted";
|
|
127
|
+
payload.stopReason = "aborted";
|
|
128
|
+
payload.message = run.lastLine;
|
|
129
|
+
}
|
|
115
130
|
try {
|
|
116
131
|
options.pi.appendEntry("subagent-removed", payload);
|
|
117
132
|
} catch {
|
|
@@ -225,16 +240,19 @@ export function snapshotBatchGroup(
|
|
|
225
240
|
terminalStatus: FinishedGroupSnapshot["terminalStatus"],
|
|
226
241
|
): FinishedGroupSnapshot {
|
|
227
242
|
let failed = 0;
|
|
243
|
+
let aborted = 0;
|
|
228
244
|
const members: FinishedGroupMember[] = batch.runIds.map((runId) => {
|
|
229
245
|
const run = store.commandRuns.get(runId);
|
|
230
246
|
if (!run) {
|
|
231
|
-
if (batch.
|
|
247
|
+
if (batch.abortedRunIds.has(runId)) aborted++;
|
|
248
|
+
else if (batch.failedRunIds.has(runId)) failed++;
|
|
232
249
|
return {
|
|
233
250
|
summaryLine: `#${runId} [gone] (run no longer available)`,
|
|
234
251
|
output: batch.pendingResults.get(runId)?.trim() || "(no output)",
|
|
235
252
|
};
|
|
236
253
|
}
|
|
237
|
-
if (run.
|
|
254
|
+
if (run.errorClass === "aborted" || batch.abortedRunIds.has(runId)) aborted++;
|
|
255
|
+
else if (run.status === "error" || batch.failedRunIds.has(runId)) failed++;
|
|
238
256
|
return { summaryLine: formatCommandRunSummary(run), output: memberOutput(run, batch.pendingResults.get(runId)) };
|
|
239
257
|
});
|
|
240
258
|
return {
|
|
@@ -244,6 +262,7 @@ export function snapshotBatchGroup(
|
|
|
244
262
|
finishedAt: Date.now(),
|
|
245
263
|
total: batch.runIds.length,
|
|
246
264
|
failed,
|
|
265
|
+
aborted,
|
|
247
266
|
members,
|
|
248
267
|
};
|
|
249
268
|
}
|
|
@@ -265,6 +284,7 @@ export function snapshotPipeline(
|
|
|
265
284
|
finishedAt: Date.now(),
|
|
266
285
|
total: pipeline.stepResults.length,
|
|
267
286
|
failed: pipeline.stepResults.filter((step) => step.status === "error").length,
|
|
287
|
+
aborted: pipeline.stepResults.filter((step) => step.status === "aborted").length,
|
|
268
288
|
members,
|
|
269
289
|
};
|
|
270
290
|
}
|
|
@@ -304,9 +324,10 @@ function formatFinishedAge(finishedAt: number, now = Date.now()): string {
|
|
|
304
324
|
export function formatFinishedGroupStatus(snapshot: FinishedGroupSnapshot, detailed: boolean): string {
|
|
305
325
|
const label = snapshot.kind === "batch" ? "subagent-batch" : "subagent-chain";
|
|
306
326
|
const failedSuffix = snapshot.failed > 0 ? `, ${snapshot.failed} failed` : "";
|
|
327
|
+
const abortedSuffix = snapshot.aborted > 0 ? `, ${snapshot.aborted} aborted` : "";
|
|
307
328
|
const header = `[${label}#${snapshot.groupId}] ${snapshot.terminalStatus} · ${snapshot.total} ${
|
|
308
329
|
snapshot.kind === "batch" ? "runs" : "steps"
|
|
309
|
-
}${failedSuffix} · finished ${formatFinishedAge(snapshot.finishedAt)}`;
|
|
330
|
+
}${failedSuffix}${abortedSuffix} · finished ${formatFinishedAge(snapshot.finishedAt)}`;
|
|
310
331
|
const body = snapshot.members
|
|
311
332
|
.map((member) => {
|
|
312
333
|
if (!detailed) return member.summaryLine;
|
package/starter-pack.ts
CHANGED
|
@@ -11,7 +11,6 @@ export const STARTER_AGENT_NAMES = [
|
|
|
11
11
|
"reviewer",
|
|
12
12
|
"searcher",
|
|
13
13
|
"security-auditor",
|
|
14
|
-
"simplifier",
|
|
15
14
|
"verifier",
|
|
16
15
|
"worker",
|
|
17
16
|
] as const;
|
|
@@ -220,7 +219,7 @@ export async function offerStarterPackIfEmpty(
|
|
|
220
219
|
|
|
221
220
|
const accepted = await ctx.ui.confirm(
|
|
222
221
|
"Install starter subagents?",
|
|
223
|
-
"No subagent definitions were found. Install
|
|
222
|
+
"No subagent definitions were found. Install 8 portable English agents, the stress-interview and self-healing skills, and missing subagent settings? Existing files and configured values will not be overwritten.",
|
|
224
223
|
);
|
|
225
224
|
if (!accepted) return { status: "declined", discovery };
|
|
226
225
|
|