brl-subagent 2.3.5 → 2.3.7
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/AGENT.md +7 -31
- package/README.md +1 -12
- package/package.json +6 -6
- package/src/index.ts +93 -89
- package/src/logging.ts +39 -15
- package/src/notify-completion.ts +38 -7
- package/src/runner.ts +2 -1
- package/src/session-manager.ts +199 -37
- package/src/state.ts +0 -10
- package/src/templates.ts +1 -1
- package/src/transcript.ts +11 -6
- package/src/tui.ts +13 -79
- package/src/types.ts +143 -17
- package/src/update.ts +0 -109
package/src/types.ts
CHANGED
|
@@ -43,7 +43,14 @@ export type ErrorCategory =
|
|
|
43
43
|
| "aborted"
|
|
44
44
|
| "exit_error"
|
|
45
45
|
| "crash"
|
|
46
|
-
| "unknown"
|
|
46
|
+
| "unknown"
|
|
47
|
+
// Issue #179: additive terminal-reason categories. "truncated" = the model
|
|
48
|
+
// hit its output/context limit (stopReason "length"); "incomplete" = the
|
|
49
|
+
// agent stopped mid-turn without a terminal answer (stopReason
|
|
50
|
+
// "toolUse"/"deferred"/"pending"). Existing consumers treat categories as
|
|
51
|
+
// string keys (metrics/SLA breakdown), so both are additive.
|
|
52
|
+
| "truncated"
|
|
53
|
+
| "incomplete";
|
|
47
54
|
|
|
48
55
|
// Issue #120 (Review round): single source of truth for the user-abort stamp.
|
|
49
56
|
// Used by the abort handler (runner.ts), the stopped/aborted settlement paths
|
|
@@ -58,6 +65,17 @@ export const SUBAGENT_ABORTED_MESSAGE = "Subagent aborted by user";
|
|
|
58
65
|
*/
|
|
59
66
|
export function classifyError(result: SubagentResult): ErrorCategory {
|
|
60
67
|
if (result.stopReason === "aborted") return "aborted";
|
|
68
|
+
// Issue #179: a terminal stopReason with no terminal answer is a failure in
|
|
69
|
+
// its own right — classify it before the message/exitCode fallbacks so the
|
|
70
|
+
// honest category wins (length → "truncated"; mid-turn → "incomplete").
|
|
71
|
+
if (result.stopReason === "length") return "truncated";
|
|
72
|
+
if (
|
|
73
|
+
result.stopReason === "toolUse" ||
|
|
74
|
+
result.stopReason === "deferred" ||
|
|
75
|
+
result.stopReason === "pending"
|
|
76
|
+
) {
|
|
77
|
+
return "incomplete";
|
|
78
|
+
}
|
|
61
79
|
|
|
62
80
|
const msg = (result.errorMessage ?? "").toLowerCase();
|
|
63
81
|
const err = (result.stderr ?? "").toLowerCase();
|
|
@@ -209,8 +227,6 @@ export interface SubagentState {
|
|
|
209
227
|
slaTrackingEnabled: boolean; // E4: SLA tracking toggle
|
|
210
228
|
slaWindowSize: number; // E4: number of recent runs to analyze (10-500)
|
|
211
229
|
lastSLAMetrics?: SLAMetrics; // E4: persisted baseline for degradation comparison
|
|
212
|
-
updateCheckEnabled: boolean; // Version update notifier toggle
|
|
213
|
-
lastUpdateCheck: number; // Epoch ms of last update check
|
|
214
230
|
}
|
|
215
231
|
|
|
216
232
|
/**
|
|
@@ -228,6 +244,27 @@ export interface SubagentRun {
|
|
|
228
244
|
id: string;
|
|
229
245
|
task: string;
|
|
230
246
|
label?: string;
|
|
247
|
+
/**
|
|
248
|
+
* Issue #179 (D1/D2): terminal stopReason of the final assistant turn
|
|
249
|
+
* ("stop" | "error" | "aborted" | "length" | "toolUse" | "deferred" |
|
|
250
|
+
* "pending"). Recorded on the finalized entry so consumers can tell a real
|
|
251
|
+
* success from a resolved-but-failed run.
|
|
252
|
+
*/
|
|
253
|
+
stopReason?: string;
|
|
254
|
+
/**
|
|
255
|
+
* Issue #179 (D1/D2): classified error category at the TOP level. The legacy
|
|
256
|
+
* `originalParams.errorCategory` is kept populated for back-compat (metrics,
|
|
257
|
+
* retry snapshot), but new consumers should prefer this field.
|
|
258
|
+
*/
|
|
259
|
+
errorCategory?: ErrorCategory;
|
|
260
|
+
/**
|
|
261
|
+
* Issue #179 (D3): assistant turns folded from the session usage. 0 means the
|
|
262
|
+
* run burned nothing ("did nothing"); > 0 with `finalTurnError` means it did
|
|
263
|
+
* work and then died.
|
|
264
|
+
*/
|
|
265
|
+
turns?: number;
|
|
266
|
+
/** Issue #179 (D3): whether the final assistant turn was a provider error. */
|
|
267
|
+
finalTurnError?: boolean;
|
|
231
268
|
// Issue #98: background run entries carry the agent description (the label
|
|
232
269
|
// the caller passed at spawn) — kept separate from `label` for symmetry
|
|
233
270
|
// with the background agent record.
|
|
@@ -556,8 +593,6 @@ export const TEMPLATE_PARAM_RE = /\$\{(\w+)\}/g;
|
|
|
556
593
|
|
|
557
594
|
export const DEFAULT_SESSION_COST_LIMIT = 0;
|
|
558
595
|
|
|
559
|
-
export const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
560
|
-
|
|
561
596
|
export const CUSTOM_ENTRY_TYPES = {
|
|
562
597
|
run: "brl-subagent-run",
|
|
563
598
|
state: "brl-subagent-state",
|
|
@@ -589,7 +624,7 @@ export const RESERVED_NAME_PATTERN = /^__.*__$/;
|
|
|
589
624
|
export const RESERVED_COMMAND_NAMES = new Set([
|
|
590
625
|
"model", "thinking", "concurrency", "depth", "history", "monitor",
|
|
591
626
|
"preset", "retry", "reset", "priority", "templates", "dashboard", "approval",
|
|
592
|
-
"costlimit", "historyentries", "sla", "
|
|
627
|
+
"costlimit", "historyentries", "sla", "completionnotify",
|
|
593
628
|
"graph", "sla-stats",
|
|
594
629
|
]);
|
|
595
630
|
|
|
@@ -669,9 +704,108 @@ export function getFinalOutput(messages: Array<Record<string, unknown>>): string
|
|
|
669
704
|
}
|
|
670
705
|
|
|
671
706
|
export function isSubagentError(result: SubagentResult): boolean {
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
707
|
+
if (result.exitCode !== 0) return true;
|
|
708
|
+
// Issue #179: a resolved-but-failed terminal reason is still an error. This
|
|
709
|
+
// is the foreground terminal-status derivation (finalizeRunRecord) — the
|
|
710
|
+
// background path classifies via classifyTerminalOutcome below.
|
|
711
|
+
return result.stopReason === "error"
|
|
712
|
+
|| result.stopReason === "aborted"
|
|
713
|
+
|| result.stopReason === "length"
|
|
714
|
+
|| result.stopReason === "toolUse"
|
|
715
|
+
|| result.stopReason === "deferred"
|
|
716
|
+
|| result.stopReason === "pending";
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Issue #179 (review ratchet): the NARROWER question — was the terminal turn a
|
|
721
|
+
* provider/model error specifically?
|
|
722
|
+
*
|
|
723
|
+
* This is distinct from `isSubagentError` (did the run fail at all): a `length`,
|
|
724
|
+
* `toolUse`, `deferred` or `pending` ending is a FAILED RUN but NOT a provider
|
|
725
|
+
* error, and callers asking this narrower question want only `"error"`.
|
|
726
|
+
*
|
|
727
|
+
* Named so the literal has exactly one home — a consistency ratchet test fails if
|
|
728
|
+
* any other module compares `stopReason` against a failure reason directly, which
|
|
729
|
+
* is how the private copies of the wider predicate silently desynced.
|
|
730
|
+
*/
|
|
731
|
+
export function isProviderError(stopReason: string | undefined): boolean {
|
|
732
|
+
return stopReason === "error";
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Issue #179 (C1 fix): tally subtask outcomes by the SHARED failure predicate.
|
|
737
|
+
* Deriving success from `exitCode === 0` alone mislabels a truncated or
|
|
738
|
+
* mid-turn run (a resolved prompt with stopReason length/toolUse/deferred/
|
|
739
|
+
* pending, exitCode 0) as a success — the exact "failure displayed as a
|
|
740
|
+
* success" defect. Every aggregate (parallel succeeded/failed, chain/graph
|
|
741
|
+
* success, the TUI verdicts) must route through isSubagentError so a future
|
|
742
|
+
* widening of the predicate cannot silently desync a private copy.
|
|
743
|
+
*/
|
|
744
|
+
export function countSubagentOutcomes(
|
|
745
|
+
results: ReadonlyArray<SubagentResult | undefined>,
|
|
746
|
+
): { succeeded: number; failed: number } {
|
|
747
|
+
let succeeded = 0;
|
|
748
|
+
let failed = 0;
|
|
749
|
+
for (const r of results) {
|
|
750
|
+
if (!r) continue;
|
|
751
|
+
if (isSubagentError(r)) failed++;
|
|
752
|
+
else succeeded++;
|
|
753
|
+
}
|
|
754
|
+
return { succeeded, failed };
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// ---------------------------------------------------------------------------
|
|
758
|
+
// Issue #179: terminal outcome classification
|
|
759
|
+
// ---------------------------------------------------------------------------
|
|
760
|
+
|
|
761
|
+
/** Terminal status a settled run can take. */
|
|
762
|
+
export type TerminalStatus = "completed" | "failed" | "stopped";
|
|
763
|
+
|
|
764
|
+
export interface TerminalOutcome {
|
|
765
|
+
status: TerminalStatus;
|
|
766
|
+
errorCategory?: ErrorCategory;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Issue #179 (D1): classify a run's terminal outcome from its final assistant
|
|
771
|
+
* stopReason. This is the settle-time counterpart to classifyError/isSubagentError
|
|
772
|
+
* and REUSES classifyError for every failure reason (single taxonomy).
|
|
773
|
+
*
|
|
774
|
+
* | stopReason | status | errorCategory |
|
|
775
|
+
* |--------------------------|-----------|----------------------------|
|
|
776
|
+
* | stop / undefined | completed | — |
|
|
777
|
+
* | aborted | stopped | aborted |
|
|
778
|
+
* | error | failed | from classifyError |
|
|
779
|
+
* | length | failed | truncated |
|
|
780
|
+
* | toolUse/deferred/pending | failed | incomplete |
|
|
781
|
+
*/
|
|
782
|
+
export function classifyTerminalOutcome(
|
|
783
|
+
stopReason: string | undefined,
|
|
784
|
+
errorMessage?: string,
|
|
785
|
+
extra?: { stderr?: string; exitCode?: number },
|
|
786
|
+
): TerminalOutcome {
|
|
787
|
+
switch (stopReason) {
|
|
788
|
+
case "aborted":
|
|
789
|
+
return { status: "stopped", errorCategory: "aborted" };
|
|
790
|
+
case "error":
|
|
791
|
+
case "length":
|
|
792
|
+
case "toolUse":
|
|
793
|
+
case "deferred":
|
|
794
|
+
case "pending":
|
|
795
|
+
return {
|
|
796
|
+
status: "failed",
|
|
797
|
+
errorCategory: classifyError({
|
|
798
|
+
messages: [],
|
|
799
|
+
usage: { ...EMPTY_USAGE },
|
|
800
|
+
exitCode: extra?.exitCode ?? 0,
|
|
801
|
+
stderr: extra?.stderr ?? "",
|
|
802
|
+
errorMessage,
|
|
803
|
+
stopReason,
|
|
804
|
+
}),
|
|
805
|
+
};
|
|
806
|
+
default:
|
|
807
|
+
return { status: "completed" };
|
|
808
|
+
}
|
|
675
809
|
}
|
|
676
810
|
|
|
677
811
|
export function formatModel(m: { provider: string; id: string } | undefined): string {
|
|
@@ -750,14 +884,6 @@ export function isSubagentStateShape(value: unknown): value is SubagentState {
|
|
|
750
884
|
if (typeof v.slaWindowSize !== "number" || v.slaWindowSize < 10 || v.slaWindowSize > 500) return false;
|
|
751
885
|
}
|
|
752
886
|
|
|
753
|
-
// updateCheckEnabled must be a boolean if present
|
|
754
|
-
if (v.updateCheckEnabled !== undefined && typeof v.updateCheckEnabled !== "boolean") return false;
|
|
755
|
-
|
|
756
|
-
// lastUpdateCheck must be a non-negative number if present
|
|
757
|
-
if (v.lastUpdateCheck !== undefined) {
|
|
758
|
-
if (typeof v.lastUpdateCheck !== "number" || v.lastUpdateCheck < 0) return false;
|
|
759
|
-
}
|
|
760
|
-
|
|
761
887
|
return true;
|
|
762
888
|
}
|
|
763
889
|
|
package/src/update.ts
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* brl-subagent — Version Update Notifier
|
|
3
|
-
*
|
|
4
|
-
* Checks GitHub releases for newer versions of brl-subagent.
|
|
5
|
-
* Runs non-blocking with a 5-second timeout on session start.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { Logger } from "./logging";
|
|
9
|
-
|
|
10
|
-
// ---------------------------------------------------------------------------
|
|
11
|
-
// Types
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
|
|
14
|
-
export interface UpdateCheckResult {
|
|
15
|
-
available: boolean;
|
|
16
|
-
version?: string;
|
|
17
|
-
url?: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
// Semver comparison
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Compare two semver strings (e.g., "2.1.0" vs "2.0.1").
|
|
26
|
-
* Returns 1 if a > b, -1 if a < b, 0 if equal.
|
|
27
|
-
*/
|
|
28
|
-
function compareSemver(a: string, b: string): number {
|
|
29
|
-
const aParts = a.split(".").map(Number);
|
|
30
|
-
const bParts = b.split(".").map(Number);
|
|
31
|
-
const len = Math.max(aParts.length, bParts.length);
|
|
32
|
-
|
|
33
|
-
for (let i = 0; i < len; i++) {
|
|
34
|
-
const aVal = aParts[i] ?? 0;
|
|
35
|
-
const bVal = bParts[i] ?? 0;
|
|
36
|
-
if (aVal > bVal) return 1;
|
|
37
|
-
if (aVal < bVal) return -1;
|
|
38
|
-
}
|
|
39
|
-
return 0;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// ---------------------------------------------------------------------------
|
|
43
|
-
// Main export
|
|
44
|
-
// ---------------------------------------------------------------------------
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Check GitHub for a newer release of brl-subagent.
|
|
48
|
-
*
|
|
49
|
-
* @param currentVersion - The current version string (e.g., "2.0.1")
|
|
50
|
-
* @param log - Optional logger for debug output
|
|
51
|
-
* @returns Update check result, or null on any error (never throws)
|
|
52
|
-
*/
|
|
53
|
-
export async function checkForUpdates(
|
|
54
|
-
currentVersion: string,
|
|
55
|
-
log?: Logger,
|
|
56
|
-
): Promise<UpdateCheckResult | null> {
|
|
57
|
-
try {
|
|
58
|
-
const controller = new AbortController();
|
|
59
|
-
const timeoutId = setTimeout(() => controller.abort(), 5_000);
|
|
60
|
-
|
|
61
|
-
const response = await fetch(
|
|
62
|
-
"https://api.github.com/repos/azmeenafandi/brl-subagent/releases/latest",
|
|
63
|
-
{
|
|
64
|
-
headers: {
|
|
65
|
-
Accept: "application/vnd.github+json",
|
|
66
|
-
"User-Agent": "brl-subagent",
|
|
67
|
-
},
|
|
68
|
-
signal: controller.signal,
|
|
69
|
-
},
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
clearTimeout(timeoutId);
|
|
73
|
-
|
|
74
|
-
if (!response.ok) {
|
|
75
|
-
log?.debug("Update check: non-OK response", { status: response.status });
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const data = (await response.json()) as {
|
|
80
|
-
tag_name?: string;
|
|
81
|
-
html_url?: string;
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
if (!data.tag_name || !data.html_url) {
|
|
85
|
-
log?.debug("Update check: missing tag_name or html_url in response");
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Strip "v" prefix from tag_name (e.g., "v2.1.0" → "2.1.0")
|
|
90
|
-
const latestVersion = data.tag_name.replace(/^v/, "");
|
|
91
|
-
|
|
92
|
-
const cmp = compareSemver(latestVersion, currentVersion);
|
|
93
|
-
|
|
94
|
-
if (cmp > 0) {
|
|
95
|
-
return {
|
|
96
|
-
available: true,
|
|
97
|
-
version: latestVersion,
|
|
98
|
-
url: data.html_url,
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return { available: false };
|
|
103
|
-
} catch (err) {
|
|
104
|
-
log?.debug("Update check failed", {
|
|
105
|
-
error: err instanceof Error ? err.message : String(err),
|
|
106
|
-
});
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
}
|