brl-subagent 2.3.4
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/LICENSE +21 -0
- package/README.md +257 -0
- package/package.json +49 -0
- package/presets/code-reviewer.md +37 -0
- package/presets/data-analyst.md +33 -0
- package/presets/debugger.md +36 -0
- package/presets/dev-agent.md +30 -0
- package/presets/rapid-prototyper.md +25 -0
- package/presets/refactorer.md +34 -0
- package/presets/security-auditor.md +39 -0
- package/presets/tech-writer.md +33 -0
- package/presets/test-engineer.md +34 -0
- package/src/concurrency.ts +186 -0
- package/src/diff.ts +102 -0
- package/src/event-bus.ts +86 -0
- package/src/git.ts +187 -0
- package/src/history.ts +177 -0
- package/src/index.ts +3824 -0
- package/src/logging.ts +135 -0
- package/src/messaging.ts +164 -0
- package/src/metrics.ts +221 -0
- package/src/model-availability.ts +73 -0
- package/src/notify-completion.ts +207 -0
- package/src/params.ts +225 -0
- package/src/preflight.ts +150 -0
- package/src/presets.ts +425 -0
- package/src/prompt.ts +169 -0
- package/src/reports.ts +325 -0
- package/src/router.ts +133 -0
- package/src/runner.ts +877 -0
- package/src/sanitize.ts +365 -0
- package/src/scheduler.ts +187 -0
- package/src/session-manager.ts +1065 -0
- package/src/state.ts +590 -0
- package/src/templates.ts +469 -0
- package/src/transcript-tail.ts +327 -0
- package/src/transcript.ts +119 -0
- package/src/tui-format.ts +51 -0
- package/src/tui.ts +3103 -0
- package/src/types.ts +803 -0
- package/src/unit-run.ts +269 -0
- package/src/update.ts +109 -0
- package/src/validate.ts +302 -0
- package/templates/analyze-data.md +9 -0
- package/templates/code-review.md +10 -0
- package/templates/debug-issue.md +9 -0
- package/templates/implement-feature.md +9 -0
- package/templates/prototype.md +9 -0
- package/templates/refactor.md +9 -0
- package/templates/security-audit.md +10 -0
- package/templates/write-docs.md +9 -0
- package/templates/write-tests.md +10 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,803 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* brl-subagent — Type Definitions
|
|
3
|
+
*
|
|
4
|
+
* All shared type definitions and constants for the brl-subagent extension.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
9
|
+
import type { TranscriptMessage } from "./transcript-tail";
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Core types
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
export type ApprovalMode = "auto" | "writes" | "always";
|
|
16
|
+
|
|
17
|
+
export const COMPLETION_NOTIFY_MODES = ["all", "failed", "off"] as const;
|
|
18
|
+
export type CompletionNotifyMode = (typeof COMPLETION_NOTIFY_MODES)[number];
|
|
19
|
+
|
|
20
|
+
export const DEFAULT_COMPLETION_NOTIFY: CompletionNotifyMode = "all";
|
|
21
|
+
|
|
22
|
+
export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
23
|
+
|
|
24
|
+
export const THINKING_LEVELS: ThinkingLevel[] = ["off", "minimal", "low", "medium", "high", "xhigh"];
|
|
25
|
+
|
|
26
|
+
export type Priority = "critical" | "high" | "normal" | "low";
|
|
27
|
+
|
|
28
|
+
export const PRIORITY_ORDER: Record<Priority, number> = {
|
|
29
|
+
critical: 0,
|
|
30
|
+
high: 1,
|
|
31
|
+
normal: 2,
|
|
32
|
+
low: 3,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const DEFAULT_PRIORITY: Priority = "normal";
|
|
36
|
+
|
|
37
|
+
export type ErrorCategory =
|
|
38
|
+
| "timeout"
|
|
39
|
+
| "model_unavailable"
|
|
40
|
+
| "tool_error"
|
|
41
|
+
| "permission_denied"
|
|
42
|
+
| "parse_error"
|
|
43
|
+
| "aborted"
|
|
44
|
+
| "exit_error"
|
|
45
|
+
| "crash"
|
|
46
|
+
| "unknown";
|
|
47
|
+
|
|
48
|
+
// Issue #120 (Review round): single source of truth for the user-abort stamp.
|
|
49
|
+
// Used by the abort handler (runner.ts), the stopped/aborted settlement paths
|
|
50
|
+
// and the settle-handler catch-all (session-manager.ts), and matched (via its
|
|
51
|
+
// distinctive "aborted by user" substring) by classifyError below. Keep the
|
|
52
|
+
// literal in sync with classifyError — do NOT reword one without the other.
|
|
53
|
+
export const SUBAGENT_ABORTED_MESSAGE = "Subagent aborted by user";
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Classify a subagent result into an error category based on its errorMessage,
|
|
57
|
+
* stopReason, exitCode, and stderr content. Inspects patterns in priority order.
|
|
58
|
+
*/
|
|
59
|
+
export function classifyError(result: SubagentResult): ErrorCategory {
|
|
60
|
+
if (result.stopReason === "aborted") return "aborted";
|
|
61
|
+
|
|
62
|
+
const msg = (result.errorMessage ?? "").toLowerCase();
|
|
63
|
+
const err = (result.stderr ?? "").toLowerCase();
|
|
64
|
+
|
|
65
|
+
if (msg.includes("timed out")) return "timeout";
|
|
66
|
+
|
|
67
|
+
// Issue #120 (Track 1): an abort stamp written BEFORE a kill (the abort
|
|
68
|
+
// handler) carries the honest source in errorMessage (SUBAGENT_ABORTED_MESSAGE,
|
|
69
|
+
// "Subagent aborted by user"). classifyError can't infer an abort from a dead
|
|
70
|
+
// process — the exitCode is just non-zero — so the stamped message must
|
|
71
|
+
// survive reclassification. Matched on the EXACT stamped substring here so a
|
|
72
|
+
// provider-side "stream aborted" is not misclassified as our user-abort (and
|
|
73
|
+
// skips Track-2 enrichment); keep this substring in sync with
|
|
74
|
+
// SUBAGENT_ABORTED_MESSAGE. The stamped category is preserved so it is not
|
|
75
|
+
// clobbered by the exitError/unknown fallbacks below.
|
|
76
|
+
if (msg.includes("aborted by user")) return "aborted";
|
|
77
|
+
|
|
78
|
+
if (msg.includes("model not found") || msg.includes("model unavailable"))
|
|
79
|
+
return "model_unavailable";
|
|
80
|
+
|
|
81
|
+
if (msg.includes("permission denied") || msg.includes("eacces")) return "permission_denied";
|
|
82
|
+
|
|
83
|
+
if (err.includes("parse error") || err.includes("parse_error") || err.includes("unexpected token"))
|
|
84
|
+
return "parse_error";
|
|
85
|
+
|
|
86
|
+
if (err.includes("crash") || err.includes("panic") || err.includes("segmentation fault"))
|
|
87
|
+
return "crash";
|
|
88
|
+
|
|
89
|
+
if (msg.includes("spawn") || msg.includes("not found") || msg.includes("enoent"))
|
|
90
|
+
return "tool_error";
|
|
91
|
+
|
|
92
|
+
if (result.exitCode !== 0) return "exit_error";
|
|
93
|
+
|
|
94
|
+
if (result.stopReason === "error") return "exit_error";
|
|
95
|
+
|
|
96
|
+
return "unknown";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface SubagentPreset {
|
|
100
|
+
name: string;
|
|
101
|
+
description?: string;
|
|
102
|
+
systemPrompt?: string;
|
|
103
|
+
inheritSystemPrompt?: boolean;
|
|
104
|
+
thinkingLevel?: string;
|
|
105
|
+
model?: string; // "provider/model-id", e.g. "anthropic/claude-opus-4-6"
|
|
106
|
+
outputFile?: string;
|
|
107
|
+
timeout?: number;
|
|
108
|
+
tools?: string[];
|
|
109
|
+
excludeTools?: string[];
|
|
110
|
+
noBuiltinTools?: boolean;
|
|
111
|
+
promptGuideline?: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface TaskTemplate {
|
|
115
|
+
name: string;
|
|
116
|
+
description?: string;
|
|
117
|
+
task: string;
|
|
118
|
+
preset?: string;
|
|
119
|
+
thinkingLevel?: string;
|
|
120
|
+
outputFile?: string;
|
|
121
|
+
timeout?: number;
|
|
122
|
+
tools?: string[];
|
|
123
|
+
excludeTools?: string[];
|
|
124
|
+
noBuiltinTools?: boolean;
|
|
125
|
+
inheritSystemPrompt?: boolean;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface CircuitBreakerState {
|
|
129
|
+
consecutiveFailures: number;
|
|
130
|
+
lastFailureTime: number; // epoch ms
|
|
131
|
+
circuitOpen: boolean;
|
|
132
|
+
degradedThinkingLevel?: ThinkingLevel;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export type GitMode = "branch" | "none";
|
|
136
|
+
|
|
137
|
+
export interface SLAMetrics {
|
|
138
|
+
errorCategoryBreakdown: Record<string, number>;
|
|
139
|
+
totalRuns: number;
|
|
140
|
+
successRate: number;
|
|
141
|
+
failureRate: number;
|
|
142
|
+
averageDurationMs: number;
|
|
143
|
+
p50DurationMs: number;
|
|
144
|
+
p95DurationMs: number;
|
|
145
|
+
p99DurationMs: number;
|
|
146
|
+
totalCost: number;
|
|
147
|
+
averageCost: number;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface DegradationReport {
|
|
151
|
+
degraded: boolean;
|
|
152
|
+
successRateChange: number;
|
|
153
|
+
p95Change: number;
|
|
154
|
+
recommendations: string[];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export const DEFAULT_SLA_WINDOW_SIZE = 50;
|
|
158
|
+
export const MIN_SLA_WINDOW_SIZE = 10;
|
|
159
|
+
export const MAX_SLA_WINDOW_SIZE = 500;
|
|
160
|
+
|
|
161
|
+
// E5: Compliance report types
|
|
162
|
+
export interface FileAccessReport {
|
|
163
|
+
files: Record<string, string[]>;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface SecretsExposureEntry {
|
|
167
|
+
file: string;
|
|
168
|
+
runId: string;
|
|
169
|
+
runLabel?: string;
|
|
170
|
+
severity: "high" | "medium" | "low";
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface SecretsExposureReport {
|
|
174
|
+
exposures: SecretsExposureEntry[];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export interface ComplianceSummary {
|
|
178
|
+
totalRuns: number;
|
|
179
|
+
dateRange: {
|
|
180
|
+
earliest: string;
|
|
181
|
+
latest: string;
|
|
182
|
+
};
|
|
183
|
+
metrics: SLAMetrics;
|
|
184
|
+
filesAccessed: number;
|
|
185
|
+
sensitiveFilesTouched: number;
|
|
186
|
+
highSeverityFindings: number;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface SubagentState {
|
|
190
|
+
model?: { provider: string; id: string };
|
|
191
|
+
maxThinkingLevel: ThinkingLevel;
|
|
192
|
+
maxParallel: number; // 0 = unlimited
|
|
193
|
+
maxSubagentDepth: number; // 0 = no recursion allowed, 1 = one level, etc.
|
|
194
|
+
gitMode: GitMode; // P3: branch-based git workflow
|
|
195
|
+
approvalMode: ApprovalMode; // P4: change approval workflow
|
|
196
|
+
// Issue #147: completion-push wake knob — when a background run reaches a
|
|
197
|
+
// terminal state the conductor is woken (delivery is always-on, minimum
|
|
198
|
+
// nextTurn; wake is the configurable part). "all" wakes on every terminal
|
|
199
|
+
// state, "failed" wakes only on failed/stopped, "off" never wakes (still
|
|
200
|
+
// delivers passively at the next user prompt).
|
|
201
|
+
completionNotify: CompletionNotifyMode;
|
|
202
|
+
maxHistoryEntries: number; // 0 = unlimited
|
|
203
|
+
sessionCostLimit: number; // 0 = unlimited
|
|
204
|
+
perTaskCostEstimate: number; // 0 = no estimate, use default
|
|
205
|
+
seenRunIds: string[];
|
|
206
|
+
presets: SubagentPreset[];
|
|
207
|
+
templates: TaskTemplate[];
|
|
208
|
+
circuitBreaker: CircuitBreakerState;
|
|
209
|
+
slaTrackingEnabled: boolean; // E4: SLA tracking toggle
|
|
210
|
+
slaWindowSize: number; // E4: number of recent runs to analyze (10-500)
|
|
211
|
+
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
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Result shape returned by brl-subagent tool handlers.
|
|
218
|
+
*
|
|
219
|
+
* `AgentToolResult<T>` is the pi SDK contract (details is required). The
|
|
220
|
+
* handlers extend it with the legacy `isError` flag the tool contract and
|
|
221
|
+
* tests pin — the pi runtime ignores `isError` on returned results (v0.84
|
|
222
|
+
* signals errors by throwing), so it is metadata for the repo's own contract,
|
|
223
|
+
* not a pi behavior switch.
|
|
224
|
+
*/
|
|
225
|
+
export type ToolResult<T = unknown> = AgentToolResult<T> & { isError?: boolean };
|
|
226
|
+
|
|
227
|
+
export interface SubagentRun {
|
|
228
|
+
id: string;
|
|
229
|
+
task: string;
|
|
230
|
+
label?: string;
|
|
231
|
+
// Issue #98: background run entries carry the agent description (the label
|
|
232
|
+
// the caller passed at spawn) — kept separate from `label` for symmetry
|
|
233
|
+
// with the background agent record.
|
|
234
|
+
description?: string;
|
|
235
|
+
status: "running" | "done" | "failed";
|
|
236
|
+
model: string;
|
|
237
|
+
thinkingLevel: string;
|
|
238
|
+
// Issue #114: per-unit priority (may be absent — the run's slot priority
|
|
239
|
+
// then fell back to the call-level default, which is not recorded here).
|
|
240
|
+
priority?: string;
|
|
241
|
+
startedAt: string;
|
|
242
|
+
finishedAt?: string;
|
|
243
|
+
durationMs?: number;
|
|
244
|
+
cost?: number;
|
|
245
|
+
tokensIn?: number;
|
|
246
|
+
tokensOut?: number;
|
|
247
|
+
errorMessage?: string;
|
|
248
|
+
outputSummary?: string;
|
|
249
|
+
fullOutput?: string;
|
|
250
|
+
originalParams?: {
|
|
251
|
+
systemPrompt?: string;
|
|
252
|
+
inheritSystemPrompt?: boolean;
|
|
253
|
+
model?: string;
|
|
254
|
+
thinkingLevel?: string;
|
|
255
|
+
// Issue #114: per-unit priority survives retries — the snapshot carries
|
|
256
|
+
// it through so a retried run keeps its priority.
|
|
257
|
+
priority?: string;
|
|
258
|
+
outputFile?: string;
|
|
259
|
+
timeout?: number;
|
|
260
|
+
cwd?: string;
|
|
261
|
+
tools?: string[];
|
|
262
|
+
excludeTools?: string[];
|
|
263
|
+
noBuiltinTools?: boolean;
|
|
264
|
+
preset?: string;
|
|
265
|
+
errorCategory?: ErrorCategory;
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export interface LiveSubagent {
|
|
270
|
+
id: string;
|
|
271
|
+
label?: string;
|
|
272
|
+
task: string;
|
|
273
|
+
model: string;
|
|
274
|
+
thinkingLevel: string;
|
|
275
|
+
// Issue #114: per-unit priority for the drill-in header (`p:critical` etc.).
|
|
276
|
+
priority?: string;
|
|
277
|
+
startedAt: number;
|
|
278
|
+
liveOutput: string;
|
|
279
|
+
usage: { input: number; output: number };
|
|
280
|
+
ctx: ExtensionContext;
|
|
281
|
+
/**
|
|
282
|
+
* Issue #105: live streaming transcript for FOREGROUND runs — captured from
|
|
283
|
+
* the subprocess's `message_update` JSONL events (thinking/text/toolCall
|
|
284
|
+
* deltas) and threaded through the live monitor. Background runs render
|
|
285
|
+
* from `_sessionRef` instead; this field is populated only on the
|
|
286
|
+
* foreground path.
|
|
287
|
+
*/
|
|
288
|
+
transcript?: TranscriptMessage[];
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export interface UsageStats {
|
|
292
|
+
input: number;
|
|
293
|
+
output: number;
|
|
294
|
+
cacheRead: number;
|
|
295
|
+
cacheWrite: number;
|
|
296
|
+
cost: number;
|
|
297
|
+
contextTokens: number;
|
|
298
|
+
turns: number;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export interface SubagentResult {
|
|
302
|
+
messages: Array<Record<string, unknown>>;
|
|
303
|
+
model?: string;
|
|
304
|
+
usage: UsageStats;
|
|
305
|
+
stopReason?: string;
|
|
306
|
+
errorMessage?: string;
|
|
307
|
+
exitCode: number;
|
|
308
|
+
stderr: string;
|
|
309
|
+
label?: string;
|
|
310
|
+
errorCategory?: ErrorCategory;
|
|
311
|
+
/**
|
|
312
|
+
* Issue #105: streaming transcript captured from the subprocess's
|
|
313
|
+
* `message_update` JSONL events (foreground parity with the background
|
|
314
|
+
* drill-in). Populated incrementally while the subprocess runs; the array
|
|
315
|
+
* is a live tail capped at LIVE_TRANSCRIPT_MAX_MESSAGES / 64KB (runner.ts).
|
|
316
|
+
*/
|
|
317
|
+
liveTranscript?: TranscriptMessage[];
|
|
318
|
+
// P3: Git integration fields
|
|
319
|
+
gitBranch?: string;
|
|
320
|
+
gitDiff?: string;
|
|
321
|
+
// P4: Change approval workflow
|
|
322
|
+
approved?: boolean;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export interface SubagentToolOptions {
|
|
326
|
+
tools?: string[];
|
|
327
|
+
excludeTools?: string[];
|
|
328
|
+
noBuiltinTools?: boolean;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
// P1+P2: Task chaining & parallel mode types
|
|
333
|
+
// ---------------------------------------------------------------------------
|
|
334
|
+
|
|
335
|
+
/** One step in a chain or one task in a parallel group. */
|
|
336
|
+
export interface SubTaskParams {
|
|
337
|
+
task: string;
|
|
338
|
+
label?: string;
|
|
339
|
+
/** Model override for this step (provider/model-id, e.g. "anthropic/claude-opus-4-6"). */
|
|
340
|
+
model?: string;
|
|
341
|
+
thinkingLevel?: string;
|
|
342
|
+
/** Issue #114: per-unit concurrency priority for tasks[]/graph[] items. */
|
|
343
|
+
priority?: string;
|
|
344
|
+
systemPrompt?: string;
|
|
345
|
+
inheritSystemPrompt?: boolean;
|
|
346
|
+
cwd?: string;
|
|
347
|
+
timeout?: number;
|
|
348
|
+
outputFile?: string;
|
|
349
|
+
tools?: string[];
|
|
350
|
+
excludeTools?: string[];
|
|
351
|
+
noBuiltinTools?: boolean;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Result of a single subtask execution. */
|
|
355
|
+
export interface SubTaskResult {
|
|
356
|
+
label?: string;
|
|
357
|
+
task: string;
|
|
358
|
+
step?: number;
|
|
359
|
+
exitCode: number;
|
|
360
|
+
messages: Array<Record<string, unknown>>;
|
|
361
|
+
stderr: string;
|
|
362
|
+
usage: UsageStats;
|
|
363
|
+
model?: string;
|
|
364
|
+
stopReason?: string;
|
|
365
|
+
errorMessage?: string;
|
|
366
|
+
errorCategory?: ErrorCategory;
|
|
367
|
+
gitBranch?: string;
|
|
368
|
+
gitDiff?: string;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** Aggregate result for chain and parallel modes. */
|
|
372
|
+
export interface MultiSubagentDetails {
|
|
373
|
+
mode: "chain" | "parallel";
|
|
374
|
+
results: SubTaskResult[];
|
|
375
|
+
totalInput: number;
|
|
376
|
+
totalOutput: number;
|
|
377
|
+
totalCost: number;
|
|
378
|
+
totalTurns: number;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export interface ChainDetails extends MultiSubagentDetails {
|
|
382
|
+
mode: "chain";
|
|
383
|
+
completedSteps: number;
|
|
384
|
+
totalSteps: number;
|
|
385
|
+
stoppedEarly: boolean;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export interface ParallelDetails extends MultiSubagentDetails {
|
|
389
|
+
mode: "parallel";
|
|
390
|
+
succeeded: number;
|
|
391
|
+
failed: number;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ---------------------------------------------------------------------------
|
|
395
|
+
// P10: Dependency graph types
|
|
396
|
+
// ---------------------------------------------------------------------------
|
|
397
|
+
|
|
398
|
+
/** A single node in a dependency graph. */
|
|
399
|
+
export interface GraphTask {
|
|
400
|
+
id: string;
|
|
401
|
+
task: string;
|
|
402
|
+
label?: string;
|
|
403
|
+
/** Model override for this task (provider/model-id, e.g. "anthropic/claude-opus-4-6"). */
|
|
404
|
+
model?: string;
|
|
405
|
+
thinkingLevel?: string;
|
|
406
|
+
/** Issue #114: per-unit concurrency priority for this graph node. */
|
|
407
|
+
priority?: string;
|
|
408
|
+
cwd?: string;
|
|
409
|
+
timeout?: number;
|
|
410
|
+
outputFile?: string;
|
|
411
|
+
tools?: string[];
|
|
412
|
+
excludeTools?: string[];
|
|
413
|
+
noBuiltinTools?: boolean;
|
|
414
|
+
systemPrompt?: string;
|
|
415
|
+
inheritSystemPrompt?: boolean;
|
|
416
|
+
dependsOn: string[];
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** One wave of tasks executed together in a graph. */
|
|
420
|
+
export interface GraphWave {
|
|
421
|
+
waveIndex: number;
|
|
422
|
+
tasks: SubTaskResult[];
|
|
423
|
+
parallel: boolean;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** Aggregate result for graph mode. */
|
|
427
|
+
export interface GraphDetails {
|
|
428
|
+
mode: "graph";
|
|
429
|
+
waves: GraphWave[];
|
|
430
|
+
totalInput: number;
|
|
431
|
+
totalOutput: number;
|
|
432
|
+
totalCost: number;
|
|
433
|
+
totalTurns: number;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Union of every details shape delegate_task can produce — the tool's TDetails
|
|
438
|
+
* generic (issue #117). Single mode yields SubagentResult; chain / parallel /
|
|
439
|
+
* graph modes yield their mode-specific aggregates (GraphDetails does NOT
|
|
440
|
+
* extend MultiSubagentDetails, so it must be a member explicitly); error paths
|
|
441
|
+
* yield undefined.
|
|
442
|
+
*/
|
|
443
|
+
export type DelegateTaskDetails =
|
|
444
|
+
| SubagentResult
|
|
445
|
+
| ChainDetails
|
|
446
|
+
| ParallelDetails
|
|
447
|
+
| GraphDetails
|
|
448
|
+
| undefined;
|
|
449
|
+
|
|
450
|
+
// ---------------------------------------------------------------------------
|
|
451
|
+
// Resolved params (after preset merging + validation)
|
|
452
|
+
// ---------------------------------------------------------------------------
|
|
453
|
+
|
|
454
|
+
// ---------------------------------------------------------------------------
|
|
455
|
+
// Resolved params (after preset merging + validation)
|
|
456
|
+
// ---------------------------------------------------------------------------
|
|
457
|
+
|
|
458
|
+
export interface ResolvedParams {
|
|
459
|
+
task: string;
|
|
460
|
+
label: string | undefined;
|
|
461
|
+
inheritSP: boolean;
|
|
462
|
+
customSP: string | undefined;
|
|
463
|
+
outputFile: string | undefined;
|
|
464
|
+
timeout: number | undefined;
|
|
465
|
+
effectiveCwd: string;
|
|
466
|
+
thinkingLevel: ThinkingLevel;
|
|
467
|
+
toolOptions: SubagentToolOptions | undefined;
|
|
468
|
+
/** Set only when autoRoutePreset chose the preset (not explicitly requested). */
|
|
469
|
+
autoRoutedPreset: SubagentPreset | undefined;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// ---------------------------------------------------------------------------
|
|
473
|
+
// Log levels
|
|
474
|
+
// ---------------------------------------------------------------------------
|
|
475
|
+
|
|
476
|
+
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
477
|
+
|
|
478
|
+
// ---------------------------------------------------------------------------
|
|
479
|
+
// Event bus types
|
|
480
|
+
// ---------------------------------------------------------------------------
|
|
481
|
+
|
|
482
|
+
/** Types of lifecycle events that can be emitted. */
|
|
483
|
+
export type SubagentEventType =
|
|
484
|
+
| "subagent:created"
|
|
485
|
+
| "subagent:started"
|
|
486
|
+
| "subagent:completed"
|
|
487
|
+
| "subagent:failed"
|
|
488
|
+
| "subagent:stopped"
|
|
489
|
+
| "subagent:steered";
|
|
490
|
+
|
|
491
|
+
/** An event emitted during the subagent lifecycle. */
|
|
492
|
+
export interface SubagentEvent {
|
|
493
|
+
type: SubagentEventType;
|
|
494
|
+
agentId: string;
|
|
495
|
+
timestamp: number;
|
|
496
|
+
data: Record<string, unknown>;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** Callback function for event listeners. */
|
|
500
|
+
export type SubagentEventListener = (event: SubagentEvent) => void;
|
|
501
|
+
|
|
502
|
+
// ---------------------------------------------------------------------------
|
|
503
|
+
// Transcript (agent conversation log) types
|
|
504
|
+
// ---------------------------------------------------------------------------
|
|
505
|
+
|
|
506
|
+
/** Entry kinds written to an agent's transcript file. */
|
|
507
|
+
export type TranscriptEntryType = "system" | "user";
|
|
508
|
+
|
|
509
|
+
/** A single line in an agent's transcript file. */
|
|
510
|
+
export interface TranscriptEntry {
|
|
511
|
+
type: TranscriptEntryType;
|
|
512
|
+
timestamp: number;
|
|
513
|
+
content: string;
|
|
514
|
+
metadata?: Record<string, unknown>;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// ---------------------------------------------------------------------------
|
|
518
|
+
// Constants
|
|
519
|
+
// ---------------------------------------------------------------------------
|
|
520
|
+
|
|
521
|
+
export const EMPTY_USAGE: UsageStats = {
|
|
522
|
+
input: 0,
|
|
523
|
+
output: 0,
|
|
524
|
+
cacheRead: 0,
|
|
525
|
+
cacheWrite: 0,
|
|
526
|
+
cost: 0,
|
|
527
|
+
contextTokens: 0,
|
|
528
|
+
turns: 0,
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
export const NAV_FOOTER = "\u2191\u2193 navigate \u2022 enter select \u2022 esc cancel";
|
|
532
|
+
|
|
533
|
+
export const SIGKILL_GRACE_MS = 5000;
|
|
534
|
+
export const STATUS_RESET_DELAY_MS = 3000;
|
|
535
|
+
export const TEMP_FILE_MODE = 0o600;
|
|
536
|
+
export const TASK_PREVIEW_MAX_LENGTH = 80;
|
|
537
|
+
export const COLLAPSED_OUTPUT_LINES = 5;
|
|
538
|
+
export const DEFAULT_OUTPUT_CAP_BYTES = 100 * 1024; // 100KB
|
|
539
|
+
|
|
540
|
+
// P5: Output diffing constants
|
|
541
|
+
export const MAX_HUNKS_PER_FILE = 10;
|
|
542
|
+
export const COLLAPSED_DIFF_FILES_PREVIEW = 5; // max file entries in collapsed view
|
|
543
|
+
export const EXPANDED_HUNKS_PER_FILE = 5; // max hunks shown per file in expanded view
|
|
544
|
+
|
|
545
|
+
export const DEFAULT_MAX_SUBAGENT_DEPTH = 3;
|
|
546
|
+
|
|
547
|
+
export const MAX_RUN_HISTORY_ENTRIES = 500;
|
|
548
|
+
export const MAX_TEMP_DIR_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
549
|
+
|
|
550
|
+
// Circuit breaker constants
|
|
551
|
+
export const MAX_CONSECUTIVE_FAILURES = 5;
|
|
552
|
+
export const CIRCUIT_BREAKER_RESET_MS = 60000; // 1 min auto-recovery
|
|
553
|
+
export const CIRCUIT_DEGRADED_THINKING: ThinkingLevel = "minimal";
|
|
554
|
+
|
|
555
|
+
export const TEMPLATE_PARAM_RE = /\$\{(\w+)\}/g;
|
|
556
|
+
|
|
557
|
+
export const DEFAULT_SESSION_COST_LIMIT = 0;
|
|
558
|
+
|
|
559
|
+
export const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
560
|
+
|
|
561
|
+
export const CUSTOM_ENTRY_TYPES = {
|
|
562
|
+
run: "brl-subagent-run",
|
|
563
|
+
state: "brl-subagent-state",
|
|
564
|
+
} as const;
|
|
565
|
+
|
|
566
|
+
// P5: File-level diff summary for structured diff output
|
|
567
|
+
export interface FileDiff {
|
|
568
|
+
path: string; // relative file path (e.g. "src/logging.ts")
|
|
569
|
+
additions: number; // count of + lines
|
|
570
|
+
deletions: number; // count of - lines
|
|
571
|
+
hunks: string[]; // first 10 hunk texts, capped
|
|
572
|
+
totalHunks: number; // actual total (may be more than hunks.length)
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// ---------------------------------------------------------------------------
|
|
576
|
+
// P1+P2: Chain / parallel constants
|
|
577
|
+
// ---------------------------------------------------------------------------
|
|
578
|
+
|
|
579
|
+
export const MAX_CHAIN_STEPS = 10;
|
|
580
|
+
export const MAX_PARALLEL_TASKS = 8;
|
|
581
|
+
export const MAX_GRAPH_TASKS = 12;
|
|
582
|
+
export const PREVIOUS_OUTPUT_PLACEHOLDER = "{previous}";
|
|
583
|
+
export const GRAPH_OUTPUT_PLACEHOLDER_RE = /\{(\w+)\}/g;
|
|
584
|
+
|
|
585
|
+
/** Pattern matching names starting and ending with double underscores (TUI sentinels). */
|
|
586
|
+
export const RESERVED_NAME_PATTERN = /^__.*__$/;
|
|
587
|
+
|
|
588
|
+
/** Command names that collide with /brl-subagent completions. */
|
|
589
|
+
export const RESERVED_COMMAND_NAMES = new Set([
|
|
590
|
+
"model", "thinking", "concurrency", "depth", "history", "monitor",
|
|
591
|
+
"preset", "retry", "reset", "priority", "templates", "dashboard", "approval",
|
|
592
|
+
"costlimit", "historyentries", "sla", "update-check", "completionnotify",
|
|
593
|
+
"graph", "sla-stats",
|
|
594
|
+
]);
|
|
595
|
+
|
|
596
|
+
// ---------------------------------------------------------------------------
|
|
597
|
+
// Phase 6: Background agent session types
|
|
598
|
+
// ---------------------------------------------------------------------------
|
|
599
|
+
|
|
600
|
+
export type AgentStatus = "pending" | "running" | "steered" | "completed" | "failed" | "stopped";
|
|
601
|
+
|
|
602
|
+
export interface BackgroundAgent {
|
|
603
|
+
id: string;
|
|
604
|
+
sessionId: string;
|
|
605
|
+
type: string;
|
|
606
|
+
description: string;
|
|
607
|
+
status: AgentStatus;
|
|
608
|
+
startedAt: number;
|
|
609
|
+
completedAt?: number;
|
|
610
|
+
task: string;
|
|
611
|
+
model: string;
|
|
612
|
+
thinkingLevel: ThinkingLevel;
|
|
613
|
+
// Issue #114: per-unit priority for the drill-in header (`p:critical` etc.).
|
|
614
|
+
priority?: string;
|
|
615
|
+
error?: string;
|
|
616
|
+
result?: SubagentResult;
|
|
617
|
+
finalOutput?: string;
|
|
618
|
+
/** @internal — session reference for live monitor polling */
|
|
619
|
+
_sessionRef?: import('@earendil-works/pi-coding-agent').AgentSession;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// ---------------------------------------------------------------------------
|
|
623
|
+
// Helper functions
|
|
624
|
+
// ---------------------------------------------------------------------------
|
|
625
|
+
|
|
626
|
+
export function resolveThinkingLevel(
|
|
627
|
+
requested: ThinkingLevel | undefined,
|
|
628
|
+
maxAllowed: ThinkingLevel,
|
|
629
|
+
): ThinkingLevel {
|
|
630
|
+
if (!requested) return maxAllowed;
|
|
631
|
+
const requestedIdx = THINKING_LEVELS.indexOf(requested);
|
|
632
|
+
const maxIdx = THINKING_LEVELS.indexOf(maxAllowed);
|
|
633
|
+
return THINKING_LEVELS[Math.min(requestedIdx, maxIdx)];
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
export function formatTokens(count: number): string {
|
|
637
|
+
if (count < 1000) return count.toString();
|
|
638
|
+
if (count < 10_000) return `${(count / 1000).toFixed(1)}k`;
|
|
639
|
+
if (count < 1_000_000) return `${Math.round(count / 1000)}k`;
|
|
640
|
+
return `${(count / 1_000_000).toFixed(1)}M`;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
export function formatUsageStats(usage: UsageStats, model?: string): string {
|
|
644
|
+
const parts: string[] = [];
|
|
645
|
+
if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
|
|
646
|
+
if (usage.input) parts.push(`↑${formatTokens(usage.input)}`);
|
|
647
|
+
if (usage.output) parts.push(`↓${formatTokens(usage.output)}`);
|
|
648
|
+
if (usage.cacheRead) parts.push(`R${formatTokens(usage.cacheRead)}`);
|
|
649
|
+
if (usage.cacheWrite) parts.push(`W${formatTokens(usage.cacheWrite)}`);
|
|
650
|
+
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
|
651
|
+
if (usage.contextTokens > 0) parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
|
|
652
|
+
if (model) parts.push(model);
|
|
653
|
+
return parts.join(" ");
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
export function getFinalOutput(messages: Array<Record<string, unknown>>): string {
|
|
657
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
658
|
+
const msg = messages[i] as Record<string, unknown>;
|
|
659
|
+
if (msg.role === "assistant") {
|
|
660
|
+
const content = msg.content as Array<Record<string, unknown>> | undefined;
|
|
661
|
+
if (content) {
|
|
662
|
+
for (const part of content) {
|
|
663
|
+
if (part.type === "text") return part.text as string;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return "";
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
export function isSubagentError(result: SubagentResult): boolean {
|
|
672
|
+
return result.exitCode !== 0
|
|
673
|
+
|| result.stopReason === "error"
|
|
674
|
+
|| result.stopReason === "aborted";
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
export function formatModel(m: { provider: string; id: string } | undefined): string {
|
|
678
|
+
return m ? `${m.provider}/${m.id}` : "Not set (will use main agent\u2019s model)";
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
export function formatMaxParallel(n: number): string {
|
|
682
|
+
return n === 0 ? "unlimited" : String(n);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Type guard: checks if a value is a valid state object shape.
|
|
687
|
+
* Used to safely restore persisted state without `as any` casts.
|
|
688
|
+
*/
|
|
689
|
+
export function isSubagentStateShape(value: unknown): value is SubagentState {
|
|
690
|
+
if (!value || typeof value !== "object") return false;
|
|
691
|
+
const v = value as Record<string, unknown>;
|
|
692
|
+
|
|
693
|
+
// model is optional but must be valid if present
|
|
694
|
+
if (v.model !== undefined && v.model !== null) {
|
|
695
|
+
if (typeof v.model !== "object") return false;
|
|
696
|
+
const m = v.model as Record<string, unknown>;
|
|
697
|
+
if (typeof m.provider !== "string" || typeof m.id !== "string") return false;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// maxThinkingLevel must be a valid level if present
|
|
701
|
+
if (v.maxThinkingLevel !== undefined && typeof v.maxThinkingLevel === "string") {
|
|
702
|
+
if (!THINKING_LEVELS.includes(v.maxThinkingLevel as ThinkingLevel)) return false;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// maxParallel must be a non-negative number
|
|
706
|
+
if (v.maxParallel !== undefined) {
|
|
707
|
+
if (typeof v.maxParallel !== "number" || v.maxParallel < 0) return false;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// maxSubagentDepth must be a non-negative number if present
|
|
711
|
+
if (v.maxSubagentDepth !== undefined) {
|
|
712
|
+
if (typeof v.maxSubagentDepth !== "number" || v.maxSubagentDepth < 0) return false;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// gitMode must be "branch" or "none" if present
|
|
716
|
+
if (v.gitMode !== undefined) {
|
|
717
|
+
if (v.gitMode !== "branch" && v.gitMode !== "none") return false;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// approvalMode must be "auto", "writes", or "always" if present
|
|
721
|
+
if (v.approvalMode !== undefined) {
|
|
722
|
+
if (v.approvalMode !== "auto" && v.approvalMode !== "writes" && v.approvalMode !== "always") return false;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// completionNotify must be one of COMPLETION_NOTIFY_MODES if present
|
|
726
|
+
if (v.completionNotify !== undefined) {
|
|
727
|
+
if (!COMPLETION_NOTIFY_MODES.includes(v.completionNotify as CompletionNotifyMode)) return false;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// maxHistoryEntries must be a non-negative number if present
|
|
731
|
+
if (v.maxHistoryEntries !== undefined) {
|
|
732
|
+
if (typeof v.maxHistoryEntries !== "number" || v.maxHistoryEntries < 0) return false;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// sessionCostLimit must be a non-negative number if present
|
|
736
|
+
if (v.sessionCostLimit !== undefined) {
|
|
737
|
+
if (typeof v.sessionCostLimit !== "number" || v.sessionCostLimit < 0) return false;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// perTaskCostEstimate must be a non-negative number if present
|
|
741
|
+
if (v.perTaskCostEstimate !== undefined) {
|
|
742
|
+
if (typeof v.perTaskCostEstimate !== "number" || v.perTaskCostEstimate < 0) return false;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// slaTrackingEnabled must be a boolean if present
|
|
746
|
+
if (v.slaTrackingEnabled !== undefined && typeof v.slaTrackingEnabled !== "boolean") return false;
|
|
747
|
+
|
|
748
|
+
// slaWindowSize must be a number 10-500 if present
|
|
749
|
+
if (v.slaWindowSize !== undefined) {
|
|
750
|
+
if (typeof v.slaWindowSize !== "number" || v.slaWindowSize < 10 || v.slaWindowSize > 500) return false;
|
|
751
|
+
}
|
|
752
|
+
|
|
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
|
+
return true;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Type guard: validates a SubagentRun entry shape.
|
|
766
|
+
*/
|
|
767
|
+
export function isSubagentRunShape(value: unknown): value is SubagentRun {
|
|
768
|
+
if (!value || typeof value !== "object") return false;
|
|
769
|
+
const v = value as Record<string, unknown>;
|
|
770
|
+
if (typeof v.id !== "string") return false;
|
|
771
|
+
if (typeof v.task !== "string") return false;
|
|
772
|
+
if (!["running", "done", "failed"].includes(v.status as string)) return false;
|
|
773
|
+
return true;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Type guard: checks if a value is a valid MultiSubagentDetails shape.
|
|
778
|
+
*/
|
|
779
|
+
export function isMultiSubagentDetails(value: unknown): value is MultiSubagentDetails {
|
|
780
|
+
if (!value || typeof value !== "object") return false;
|
|
781
|
+
const v = value as Record<string, unknown>;
|
|
782
|
+
if (v.mode !== "chain" && v.mode !== "parallel" && v.mode !== "graph") return false;
|
|
783
|
+
if (typeof v.totalInput !== "number") return false;
|
|
784
|
+
if (typeof v.totalOutput !== "number") return false;
|
|
785
|
+
if (typeof v.totalCost !== "number") return false;
|
|
786
|
+
if (typeof v.totalTurns !== "number") return false;
|
|
787
|
+
return true;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/** Type guard for graph mode details. */
|
|
791
|
+
export function isGraphDetails(value: unknown): value is GraphDetails {
|
|
792
|
+
if (!value || typeof value !== "object") return false;
|
|
793
|
+
const v = value as Record<string, unknown>;
|
|
794
|
+
if (v.mode !== "graph") return false;
|
|
795
|
+
if (!Array.isArray(v.waves)) return false;
|
|
796
|
+
if (typeof v.totalInput !== "number") return false;
|
|
797
|
+
if (typeof v.totalOutput !== "number") return false;
|
|
798
|
+
if (typeof v.totalCost !== "number") return false;
|
|
799
|
+
if (typeof v.totalTurns !== "number") return false;
|
|
800
|
+
return true;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// ---------------------------------------------------------------------------
|