@aragon-agent/cli 0.6.16 → 0.6.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +107 -51
- package/README.md +91 -48
- package/dist/agent/controller.js +52 -47
- package/dist/agent/headless.js +7 -0
- package/dist/agent/reducer.js +0 -4
- package/dist/cli.js +0 -13
- package/dist/commands/builtins.js +21 -20
- package/dist/commands/index-build.d.ts +39 -0
- package/dist/commands/index-build.js +61 -0
- package/dist/compaction/child.d.ts +10 -8
- package/dist/compaction/child.js +23 -25
- package/dist/compaction/limits.d.ts +0 -2
- package/dist/compaction/limits.js +0 -2
- package/dist/config/cli-commands.d.ts +2 -2
- package/dist/config/cli-commands.js +19 -15
- package/dist/config/load.d.ts +0 -17
- package/dist/config/load.js +0 -6
- package/dist/config/schema.d.ts +41 -9
- package/dist/config/schema.js +18 -11
- package/dist/exec/events.d.ts +16 -9
- package/dist/exec/events.js +2 -2
- package/dist/exec/runner.js +17 -1
- package/dist/fast/limits.d.ts +2 -2
- package/dist/fast/limits.js +2 -2
- package/dist/fast/prompt.d.ts +10 -11
- package/dist/fast/prompt.js +19 -20
- package/dist/fast/reviewer.d.ts +1 -1
- package/dist/fast/reviewer.js +1 -1
- package/dist/fast/types.d.ts +0 -2
- package/dist/fast/wiring.d.ts +18 -34
- package/dist/fast/wiring.js +20 -51
- package/dist/input/csiu-keys.js +13 -2
- package/dist/input/limits.d.ts +15 -0
- package/dist/input/limits.js +15 -0
- package/dist/input/win32-input-mode.js +7 -1
- package/dist/logging/install.d.ts +9 -0
- package/dist/logging/install.js +11 -0
- package/dist/session/validate-session.js +3 -1
- package/dist/team/child-reviewer.d.ts +105 -0
- package/dist/team/child-reviewer.js +117 -0
- package/dist/team/human-queue.d.ts +11 -0
- package/dist/team/human-queue.js +18 -1
- package/dist/team/limits.d.ts +40 -0
- package/dist/team/limits.js +41 -0
- package/dist/team/normalize.d.ts +7 -26
- package/dist/team/normalize.js +9 -22
- package/dist/team/overseer.d.ts +157 -0
- package/dist/team/overseer.js +348 -0
- package/dist/team/prompt.d.ts +0 -11
- package/dist/team/prompt.js +3 -12
- package/dist/team/report.d.ts +4 -19
- package/dist/team/report.js +27 -28
- package/dist/team/runtime.d.ts +76 -16
- package/dist/team/runtime.js +410 -57
- package/dist/team/subagent.d.ts +48 -19
- package/dist/team/subagent.js +88 -31
- package/dist/team/task-tool.d.ts +6 -26
- package/dist/team/task-tool.js +17 -48
- package/dist/team/types.d.ts +65 -41
- package/dist/ui/App.d.ts +1 -1
- package/dist/ui/App.js +64 -4
- package/dist/ui/BottomStatusRow.js +2 -1
- package/dist/ui/Header.js +13 -2
- package/dist/ui/PromptInput.js +11 -1
- package/dist/ui/StatusBar.js +2 -1
- package/dist/ui/TeamPanel.js +1 -1
- package/dist/ui/entries/TeamCard.js +1 -6
- package/dist/ui/layout/status-detail-layout.js +12 -2
- package/dist/ui/layout/status-layout.d.ts +4 -2
- package/dist/ui/layout/status-layout.js +24 -1
- package/dist/ui/overlays/HelpOverlay.js +2 -2
- package/package.json +2 -2
- package/skills/project-indexer/SKILL.md +163 -0
- package/skills/project-indexer/templates/cleancode-overrides.md +46 -0
- package/skills/project-indexer/templates/cleancode-template.md +94 -0
- package/skills/project-indexer/templates/config-template.md +69 -0
- package/skills/project-indexer/templates/index-template.md +129 -0
- package/dist/ui/ModeChip.d.ts +0 -18
- package/dist/ui/ModeChip.js +0 -24
package/dist/fast/wiring.js
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* THIS CLASS EXISTS FOR A BUDGET REASON, NOT AN AESTHETIC ONE (C-12 / RV-5).
|
|
8
8
|
* `CLAUDE.md` caps a source file at 1000 lines and `controller.ts` was at 960
|
|
9
9
|
* before this feature, which handed it nine new responsibilities. `FastWiring`
|
|
10
|
-
* owns the flag pair, the tier cache, the reviewer's lifetime and the
|
|
11
|
-
*
|
|
10
|
+
* owns the flag pair, the tier cache, the reviewer's lifetime and the shared
|
|
11
|
+
* fail-fast review transport; `AgentController` keeps one field and four thin
|
|
12
12
|
* forwarders. It is still CLI-local and still holds no `ui/` import.
|
|
13
13
|
*
|
|
14
14
|
* TWO FLAGS AND ONE LIVE PREDICATE (§3.3), which is the shape every optional
|
|
@@ -64,7 +64,6 @@ export function offFastStatus() {
|
|
|
64
64
|
// `budgetReached` is CARRIED and callers must never re-derive it.
|
|
65
65
|
reviewBudget: 0,
|
|
66
66
|
budgetReached: false,
|
|
67
|
-
delegated: 0,
|
|
68
67
|
usage: { inputTokens: 0, outputTokens: 0 },
|
|
69
68
|
pricingUnknown: false,
|
|
70
69
|
inFlight: false,
|
|
@@ -86,7 +85,6 @@ export class FastWiring {
|
|
|
86
85
|
tier;
|
|
87
86
|
/** Warned once per TRANSITION, so a broken tier does not shout every turn. */
|
|
88
87
|
lastReportedReason = null;
|
|
89
|
-
delegated = 0;
|
|
90
88
|
constructor(deps) {
|
|
91
89
|
this.deps = deps;
|
|
92
90
|
this.tier = resolveFastTier(deps.getConfig(), deps.hasKey);
|
|
@@ -168,26 +166,15 @@ export class FastWiring {
|
|
|
168
166
|
* §3.3's live predicate. THE THIRD TERM IS THE POINT - see the header.
|
|
169
167
|
*
|
|
170
168
|
* `FastReviewer.isSelfDisabled()` is DELIBERATELY NOT a fourth term. Folding
|
|
171
|
-
* it in would look tidier and would break something else:
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
169
|
+
* it in would look tidier and would break something else: per-child reviewers
|
|
170
|
+
* gate on `available()` too, so three consecutive REVIEW failures would
|
|
171
|
+
* silently stop every child's reviews as well - turning a diagnostic signal
|
|
172
|
+
* into a behaviour change for every dispatch. The two facts are reported
|
|
173
|
+
* separately on `FastSnapshot` instead (`live` / `selfDisabled`).
|
|
176
174
|
*/
|
|
177
175
|
available() {
|
|
178
176
|
return this.registered && this.enabled && this.tier.ok;
|
|
179
177
|
}
|
|
180
|
-
/**
|
|
181
|
-
* Whether `model: "fast"` on `task` is honoured right now.
|
|
182
|
-
*
|
|
183
|
-
* Evaluated AT DISPATCH TIME, never snapshot (§3.4 / RV-3). `fast.delegate:
|
|
184
|
-
* false` and "the tier stopped resolving twenty minutes ago" are the same
|
|
185
|
-
* observable to the model - a downgrade, counted and reported - which is the
|
|
186
|
-
* honest reading of both.
|
|
187
|
-
*/
|
|
188
|
-
delegationAvailable() {
|
|
189
|
-
return this.available() && this.deps.getConfig().fast.delegate === true;
|
|
190
|
-
}
|
|
191
178
|
setEnabled(enabled) {
|
|
192
179
|
this.enabled = enabled;
|
|
193
180
|
this.deps.onPromptChanged();
|
|
@@ -233,40 +220,23 @@ export class FastWiring {
|
|
|
233
220
|
return describeFastTierProblem(this.tier, fastProviderOf(this.deps.getConfig()));
|
|
234
221
|
}
|
|
235
222
|
// -----------------------------------------------------------------------
|
|
236
|
-
//
|
|
223
|
+
// The tier's transport, shared with per-child reviewers (§3.4)
|
|
237
224
|
// -----------------------------------------------------------------------
|
|
238
|
-
/**
|
|
239
|
-
* The `ModelRef` and thinking level one subagent tier runs on.
|
|
240
|
-
*
|
|
241
|
-
* `'fast'` FALLS BACK TO MAIN when delegation is not available, so the two
|
|
242
|
-
* guards agree BY CONSTRUCTION: the normalizer can never produce a
|
|
243
|
-
* `tier: 'fast'` spec this factory would refuse, and this factory can never
|
|
244
|
-
* build a child against a `ModelRef` the tier no longer resolves (RV-3).
|
|
245
|
-
*/
|
|
246
|
-
resolveTier(tier) {
|
|
247
|
-
const config = this.deps.getConfig();
|
|
248
|
-
if (tier === 'fast' && this.delegationAvailable() && this.tier.ok) {
|
|
249
|
-
return { ref: this.tier.ref, thinkingLevel: this.tier.thinkingLevel, role: 'fast' };
|
|
250
|
-
}
|
|
251
|
-
return {
|
|
252
|
-
ref: {
|
|
253
|
-
providerId: config.provider,
|
|
254
|
-
modelId: config.model,
|
|
255
|
-
...(config.baseUrl ? { baseUrl: config.baseUrl } : {}),
|
|
256
|
-
},
|
|
257
|
-
thinkingLevel: config.thinkingLevel,
|
|
258
|
-
role: 'main',
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
/** The resolved fast `ModelRef`, or `null` - for per-tier cost lookup (§3.6). */
|
|
225
|
+
/** The resolved fast `ModelRef`, or `null` - for cost lookups (§3.6). */
|
|
262
226
|
fastRef() {
|
|
263
227
|
return this.tier.ok ? this.tier.ref : null;
|
|
264
228
|
}
|
|
265
|
-
/**
|
|
266
|
-
*
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
229
|
+
/**
|
|
230
|
+
* The fail-fast review transport, exposed so PER-CHILD reviewers share ONE
|
|
231
|
+
* registry instead of building one each - the same adapter-state reuse
|
|
232
|
+
* argument `TeamRuntime` makes about the lead's `ProviderRegistry`.
|
|
233
|
+
*
|
|
234
|
+
* Lazy: a session that never reviews allocates nothing (I-2). The retry
|
|
235
|
+
* policy is `FAST_RETRY_POLICY`, deliberately not the user's `retry` config
|
|
236
|
+
* - see `getFastRegistry()` for the reason.
|
|
237
|
+
*/
|
|
238
|
+
completeViaFastRegistry(providerId, request) {
|
|
239
|
+
return this.getFastRegistry().complete(providerId, request);
|
|
270
240
|
}
|
|
271
241
|
// -----------------------------------------------------------------------
|
|
272
242
|
// Reviewer lifetime
|
|
@@ -302,7 +272,6 @@ export class FastWiring {
|
|
|
302
272
|
reviews: this.reviewer.reviewCount(),
|
|
303
273
|
reviewBudget: this.reviewer.budgetLimit(),
|
|
304
274
|
budgetReached: this.reviewer.isBudgetReached(),
|
|
305
|
-
delegated: this.delegated,
|
|
306
275
|
usage: this.reviewer.sessionUsage(),
|
|
307
276
|
// UNKNOWN PRICING IS NOT ZERO PRICING (C-11 / RV-4). The fast tier is
|
|
308
277
|
// precisely where an unrecognised model id is likely, and rendering it as
|
package/dist/input/csiu-keys.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* legacy bytes, with the modified-Enter family becoming `ENTER_NEWLINE_FRAME`.
|
|
25
25
|
* Pure: no node/React imports.
|
|
26
26
|
*/
|
|
27
|
-
import { ENTER_NEWLINE_FRAME } from './limits.js';
|
|
27
|
+
import { ENTER_NEWLINE_FRAME, INDEX_KEY_FRAME } from './limits.js';
|
|
28
28
|
/** Enable / disable the two CSI-u report modes (posix side). */
|
|
29
29
|
export const CSI_U_ENABLE = '\x1b[>1u\x1b[>4;2m';
|
|
30
30
|
export const CSI_U_DISABLE = '\x1b[<u\x1b[>4m';
|
|
@@ -111,8 +111,19 @@ export function translateCsiUKey(key) {
|
|
|
111
111
|
return m === 0 ? '\r' : ENTER_NEWLINE_FRAME;
|
|
112
112
|
if (key.code === 27)
|
|
113
113
|
return alt ? '\x1b\x1b' : '\x1b';
|
|
114
|
-
if (key.code === 9)
|
|
114
|
+
if (key.code === 9) {
|
|
115
|
+
// A modified Tab codepoint (modifyOtherKeys shape) mirrors the VK_TAB
|
|
116
|
+
// decision in win32-input-mode.ts: ctrl (without shift/alt) is the
|
|
117
|
+
// project-indexer trigger frame, shift stays Shift+Tab.
|
|
118
|
+
if (ctrl && !shift && !alt)
|
|
119
|
+
return INDEX_KEY_FRAME;
|
|
115
120
|
return shift ? '\x1b[Z' : '\t';
|
|
121
|
+
}
|
|
122
|
+
// Ctrl+I under kitty disambiguate arrives as codepoint 105 with the ctrl
|
|
123
|
+
// modifier; the generic printable branch below would fold it back onto the
|
|
124
|
+
// legacy Tab byte, which is exactly the ambiguity the frame exists to break.
|
|
125
|
+
if (key.code === 105 && ctrl && !shift && !alt)
|
|
126
|
+
return INDEX_KEY_FRAME;
|
|
116
127
|
if (key.code === 127) {
|
|
117
128
|
// POST-normalisation bytes, same rationale as VK_BACK in win32-input-mode.
|
|
118
129
|
if (alt)
|
package/dist/input/limits.d.ts
CHANGED
|
@@ -64,6 +64,21 @@ export declare const PASTE_CLOSE = "\0]";
|
|
|
64
64
|
* no payload, so one constant replaces the OPEN/CLOSE pair.
|
|
65
65
|
*/
|
|
66
66
|
export declare const ENTER_NEWLINE_FRAME = "\0n";
|
|
67
|
+
/**
|
|
68
|
+
* The inline frame that carries ONE "Ctrl+I was pressed" intent from the stdin
|
|
69
|
+
* filter to `App` (project-indexer Ctrl+I trigger).
|
|
70
|
+
*
|
|
71
|
+
* WHY A FRAME AND NOT `key.ctrl && input === 'i'`. Legacy terminals encode
|
|
72
|
+
* Ctrl+I as the same byte as Tab (0x09), so by the time the stream reaches Ink
|
|
73
|
+
* the two are indistinguishable. Under win32-input-mode (`?9001h`) and kitty
|
|
74
|
+
* CSI-u the modifiers ARE known at translation time, and both translators emit
|
|
75
|
+
* this frame instead of the legacy Tab byte. Same NUL-delimiter guarantee as
|
|
76
|
+
* `ENTER_NEWLINE_FRAME`: no terminal delivers NUL for a key and pasted text is
|
|
77
|
+
* stripped of NUL, so a payload can never forge it. Terminals without keyboard
|
|
78
|
+
* enhancement keep the legacy collapse — Ctrl+I there is Tab, documented in the
|
|
79
|
+
* README keybindings table.
|
|
80
|
+
*/
|
|
81
|
+
export declare const INDEX_KEY_FRAME = "\0i";
|
|
67
82
|
/**
|
|
68
83
|
* The one channel from the stdin filter to `App`'s notice dispatch (P1-3 / I-15).
|
|
69
84
|
*
|
package/dist/input/limits.js
CHANGED
|
@@ -64,6 +64,21 @@ export const PASTE_CLOSE = '\u0000]';
|
|
|
64
64
|
* no payload, so one constant replaces the OPEN/CLOSE pair.
|
|
65
65
|
*/
|
|
66
66
|
export const ENTER_NEWLINE_FRAME = '\u0000n';
|
|
67
|
+
/**
|
|
68
|
+
* The inline frame that carries ONE "Ctrl+I was pressed" intent from the stdin
|
|
69
|
+
* filter to `App` (project-indexer Ctrl+I trigger).
|
|
70
|
+
*
|
|
71
|
+
* WHY A FRAME AND NOT `key.ctrl && input === 'i'`. Legacy terminals encode
|
|
72
|
+
* Ctrl+I as the same byte as Tab (0x09), so by the time the stream reaches Ink
|
|
73
|
+
* the two are indistinguishable. Under win32-input-mode (`?9001h`) and kitty
|
|
74
|
+
* CSI-u the modifiers ARE known at translation time, and both translators emit
|
|
75
|
+
* this frame instead of the legacy Tab byte. Same NUL-delimiter guarantee as
|
|
76
|
+
* `ENTER_NEWLINE_FRAME`: no terminal delivers NUL for a key and pasted text is
|
|
77
|
+
* stripped of NUL, so a payload can never forge it. Terminals without keyboard
|
|
78
|
+
* enhancement keep the legacy collapse — Ctrl+I there is Tab, documented in the
|
|
79
|
+
* README keybindings table.
|
|
80
|
+
*/
|
|
81
|
+
export const INDEX_KEY_FRAME = '\u0000i';
|
|
67
82
|
/** Human-readable byte size for the refusal toast (`4.1 MB`, `812.0 KB`, `7 B`). */
|
|
68
83
|
export function formatPasteSize(bytes) {
|
|
69
84
|
if (bytes >= 1024 * 1024)
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
* Pure: no node/React imports, callable from any test environment.
|
|
22
22
|
*/
|
|
23
|
-
import { ENTER_NEWLINE_FRAME } from './limits.js';
|
|
23
|
+
import { ENTER_NEWLINE_FRAME, INDEX_KEY_FRAME } from './limits.js';
|
|
24
24
|
/** Enable / disable win32-input-mode (private mode 9001). */
|
|
25
25
|
export const WIN32_INPUT_ENABLE = '\x1b[?9001h';
|
|
26
26
|
export const WIN32_INPUT_DISABLE = '\x1b[?9001l';
|
|
@@ -190,6 +190,12 @@ export function translateWin32Key(record) {
|
|
|
190
190
|
// A modified Enter NEVER submits -- same rule as `ENTER_SEQUENCES`.
|
|
191
191
|
return m === 0 ? '\r' : ENTER_NEWLINE_FRAME;
|
|
192
192
|
case VK_TAB:
|
|
193
|
+
// Ctrl+I is the project-indexer trigger, and the ONLY place the two are
|
|
194
|
+
// tell-apart is right here: legacy encoding collapses it to the Tab
|
|
195
|
+
// byte. Shift keeps ESC [ Z (Shift+Tab) so the mode toggle stays intact,
|
|
196
|
+
// and Ctrl+Shift+I stays on that Shift branch.
|
|
197
|
+
if (ctrl && !shift && !alt)
|
|
198
|
+
return INDEX_KEY_FRAME;
|
|
193
199
|
return shift ? '\x1b[Z' : '\t';
|
|
194
200
|
case VK_ESCAPE:
|
|
195
201
|
return alt ? '\x1b\x1b' : '\x1b';
|
|
@@ -292,6 +292,15 @@ export type TeamLogEvent = {
|
|
|
292
292
|
subject?: string;
|
|
293
293
|
body?: string;
|
|
294
294
|
};
|
|
295
|
+
} | {
|
|
296
|
+
type: 'overseer';
|
|
297
|
+
dispatchId: string;
|
|
298
|
+
label?: string;
|
|
299
|
+
decision?: {
|
|
300
|
+
action?: string;
|
|
301
|
+
reason?: string;
|
|
302
|
+
};
|
|
303
|
+
trigger?: string;
|
|
295
304
|
} | {
|
|
296
305
|
type: 'dispatch_end';
|
|
297
306
|
dispatchId: string;
|
package/dist/logging/install.js
CHANGED
|
@@ -510,6 +510,17 @@ export function attachTeamEvents(logger, controller) {
|
|
|
510
510
|
subject: event.message?.subject,
|
|
511
511
|
});
|
|
512
512
|
break;
|
|
513
|
+
case 'overseer':
|
|
514
|
+
// REASON ONLY, and at debug: interventions are routine supervision,
|
|
515
|
+
// not failures; the report and the panel carry them to the user.
|
|
516
|
+
logger.debug('agent', 'team_overseer', {
|
|
517
|
+
dispatchId: event.dispatchId,
|
|
518
|
+
label: event.label,
|
|
519
|
+
action: event.decision?.action,
|
|
520
|
+
trigger: event.trigger,
|
|
521
|
+
reason: event.decision?.reason,
|
|
522
|
+
});
|
|
523
|
+
break;
|
|
513
524
|
case 'dispatch_end': {
|
|
514
525
|
const outcome = event.outcome;
|
|
515
526
|
const runs = outcome?.runs ?? [];
|
|
@@ -128,7 +128,9 @@ const todo = shape({
|
|
|
128
128
|
});
|
|
129
129
|
const subagent = shape({
|
|
130
130
|
label: string, description: string,
|
|
131
|
-
//
|
|
131
|
+
// OPTIONAL IN BOTH DIRECTIONS: sessions written since main-agent parity
|
|
132
|
+
// have no tier at all, and older archives may carry `main` or `fast` from
|
|
133
|
+
// the removed per-child fast dispatch. Renderers treat absence as main.
|
|
132
134
|
tier: optional(enumeration('main', 'fast')),
|
|
133
135
|
phase: enumeration('queued', 'starting', 'thinking', 'tool', 'waiting', 'done', 'failed', 'aborted'),
|
|
134
136
|
startedAt: optional(number), endedAt: optional(number), turns: number, toolCalls: number,
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-child fast review (main-agent parity).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY: `src/team/**` is inside the glyph scanner's scope
|
|
5
|
+
* (`glyphs.test.ts`), so no literal in this tree may hold a non-ASCII byte.
|
|
6
|
+
*
|
|
7
|
+
* THE LEAD HAS A FAST REVIEWER; A CHILD NOW HAS ONE TOO. The requirement is
|
|
8
|
+
* that "when the main agent pulls up subagents, all subagents are as capable as
|
|
9
|
+
* the main agent", and the fast/slow cooperation is part of what the main agent
|
|
10
|
+
* has: a cheap second model periodically critiques the run and steers a
|
|
11
|
+
* `<fast_review>` block into the conversation. A child runs the lead's own
|
|
12
|
+
* model, and this module gives that run the same second opinion.
|
|
13
|
+
*
|
|
14
|
+
* WHAT IS SHARED, AND WHAT IS PER CHILD:
|
|
15
|
+
* - SHARED: the tier resolution (`resolveFastTier` on the LIVE config), the
|
|
16
|
+
* fail-fast review transport (`FastWiring.completeViaFastRegistry` - one
|
|
17
|
+
* lazy registry for the whole session, the same adapter-state reuse
|
|
18
|
+
* argument `TeamRuntime` makes about the lead's `ProviderRegistry`), and
|
|
19
|
+
* the `fast.*` policy keys. A `/fast off` mid-dispatch therefore stops
|
|
20
|
+
* child reviews at the same instant it stops the lead's.
|
|
21
|
+
* - PER CHILD: one `FastReviewer` instance, its own turn-frame ring, its own
|
|
22
|
+
* session budget, its own goal (the spec's description).
|
|
23
|
+
*
|
|
24
|
+
* THE REVIEWER'S PROTOCOL IS UNCHANGED, AND THAT IS THE POINT. `FastReviewer`
|
|
25
|
+
* steers from exactly one place (the `tool_execution_end` that brings the batch
|
|
26
|
+
* to zero), never steers into a requested abort, and proves delivery at the
|
|
27
|
+
* next `turn_start`. The two guards need host-side facts, and a child supplies
|
|
28
|
+
* them differently than the lead:
|
|
29
|
+
*
|
|
30
|
+
* - `userSteerCount` is ALWAYS ZERO: no human can steer a child. That makes
|
|
31
|
+
* the reviewer's `clearAllQueues` provably safe by construction (D-21).
|
|
32
|
+
* - `isAbortRequested` comes from the HANDLE-level abort flag that
|
|
33
|
+
* `TeamRuntime` sets BEFORE calling `agent.abort()`. Reading the agent
|
|
34
|
+
* alone is not enough: an Esc lands as `abortAll()`, and without the flag
|
|
35
|
+
* the reviewer would race the abort it was told about.
|
|
36
|
+
* - `emit` and `notify` LOG INSTEAD OF RENDERING. A child has no transcript
|
|
37
|
+
* card and no status chip; the team panel row and the dispatch report are
|
|
38
|
+
* where a child's internals belong, and review spend is bounded by the
|
|
39
|
+
* same `fast.reviewMaxPerSession` budget the lead obeys.
|
|
40
|
+
*/
|
|
41
|
+
import type { AgentEvent, AssistantMessage, LLMRequest } from '@aragon-agent/core';
|
|
42
|
+
import type { ModelRole } from '../config/model-profiles.js';
|
|
43
|
+
import type { CliConfig } from '../config/schema.js';
|
|
44
|
+
/**
|
|
45
|
+
* The slice of a child `Agent` the reviewer needs beyond what
|
|
46
|
+
* `SubagentAgentLike` already carries. Core's `Agent` satisfies this with its
|
|
47
|
+
* own `steer` / `clearAllQueues`; a test stub satisfies it the same way.
|
|
48
|
+
*/
|
|
49
|
+
export interface ChildReviewerAgent {
|
|
50
|
+
subscribe(listener: (event: AgentEvent) => void): () => void;
|
|
51
|
+
steer(text: string): void;
|
|
52
|
+
clearAllQueues(): void;
|
|
53
|
+
}
|
|
54
|
+
/** Everything one child reviewer needs to know about its child. */
|
|
55
|
+
export interface ChildReviewerRequest {
|
|
56
|
+
label: string;
|
|
57
|
+
/** The spec's description: the goal the critique judges the run against. */
|
|
58
|
+
goal: string;
|
|
59
|
+
agent: ChildReviewerAgent;
|
|
60
|
+
/** A live phase, not a terminal one - the lead's `isRunning` equivalent. */
|
|
61
|
+
isRunning: () => boolean;
|
|
62
|
+
/** Set by the HANDLE-level abort, synchronously before `agent.abort()`. */
|
|
63
|
+
isAbortRequested: () => boolean;
|
|
64
|
+
}
|
|
65
|
+
/** Everything the reviewer needs from the session, all of it shared. */
|
|
66
|
+
export interface ChildReviewerDeps {
|
|
67
|
+
/** Read LIVE on every call - a mid-dispatch `/fast off` must reach children. */
|
|
68
|
+
getConfig: () => CliConfig;
|
|
69
|
+
hasKey: (providerId: string, role?: ModelRole) => boolean;
|
|
70
|
+
getApiKey: (providerId: string, role?: ModelRole) => string | undefined;
|
|
71
|
+
/** `FastWiring.completeViaFastRegistry` - the shared fail-fast transport. */
|
|
72
|
+
complete: (providerId: string, request: LLMRequest) => Promise<AssistantMessage>;
|
|
73
|
+
/** `FastWiring.available()` - registered AND enabled AND resolving. */
|
|
74
|
+
available: () => boolean;
|
|
75
|
+
}
|
|
76
|
+
export interface ChildReviewerHandle {
|
|
77
|
+
dispose(): void;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* What the controller hands `subagent.ts`, so a child's reviewer is built from
|
|
81
|
+
* the lead's own wiring rather than a second tier resolver.
|
|
82
|
+
*
|
|
83
|
+
* `active` AND `create` ARE SEPARATE ON PURPOSE: the child's system prompt
|
|
84
|
+
* must be composed BEFORE the agent exists, and `create` needs the agent. The
|
|
85
|
+
* two must answer the same question at the same instant, so both derive from
|
|
86
|
+
* `FastWiring.available() && config.fast.review` and the caller reads `active`
|
|
87
|
+
* once per child.
|
|
88
|
+
*/
|
|
89
|
+
export interface ChildReviewProvider {
|
|
90
|
+
/** Whether per-child review is live RIGHT NOW (tier live AND `fast.review`). */
|
|
91
|
+
active: () => boolean;
|
|
92
|
+
/** The resolved fast model id, for the child's `<fast_tier>` prompt block. */
|
|
93
|
+
modelId: () => string;
|
|
94
|
+
/** Build one reviewer for a child; `undefined` when not active. */
|
|
95
|
+
create: (req: ChildReviewerRequest) => ChildReviewerHandle | undefined;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Build one reviewer for one child, or `undefined` when the session has no
|
|
99
|
+
* live review to give it.
|
|
100
|
+
*
|
|
101
|
+
* `undefined` RATHER THAN A NO-OP REVIEWER, for the same reason
|
|
102
|
+
* `createChildContextManager` declines: the caller then wires nothing at all,
|
|
103
|
+
* and a session with the fast tier off builds the child it always built.
|
|
104
|
+
*/
|
|
105
|
+
export declare function createChildReviewer(req: ChildReviewerRequest, deps: ChildReviewerDeps): ChildReviewerHandle | undefined;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-child fast review (main-agent parity).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY: `src/team/**` is inside the glyph scanner's scope
|
|
5
|
+
* (`glyphs.test.ts`), so no literal in this tree may hold a non-ASCII byte.
|
|
6
|
+
*
|
|
7
|
+
* THE LEAD HAS A FAST REVIEWER; A CHILD NOW HAS ONE TOO. The requirement is
|
|
8
|
+
* that "when the main agent pulls up subagents, all subagents are as capable as
|
|
9
|
+
* the main agent", and the fast/slow cooperation is part of what the main agent
|
|
10
|
+
* has: a cheap second model periodically critiques the run and steers a
|
|
11
|
+
* `<fast_review>` block into the conversation. A child runs the lead's own
|
|
12
|
+
* model, and this module gives that run the same second opinion.
|
|
13
|
+
*
|
|
14
|
+
* WHAT IS SHARED, AND WHAT IS PER CHILD:
|
|
15
|
+
* - SHARED: the tier resolution (`resolveFastTier` on the LIVE config), the
|
|
16
|
+
* fail-fast review transport (`FastWiring.completeViaFastRegistry` - one
|
|
17
|
+
* lazy registry for the whole session, the same adapter-state reuse
|
|
18
|
+
* argument `TeamRuntime` makes about the lead's `ProviderRegistry`), and
|
|
19
|
+
* the `fast.*` policy keys. A `/fast off` mid-dispatch therefore stops
|
|
20
|
+
* child reviews at the same instant it stops the lead's.
|
|
21
|
+
* - PER CHILD: one `FastReviewer` instance, its own turn-frame ring, its own
|
|
22
|
+
* session budget, its own goal (the spec's description).
|
|
23
|
+
*
|
|
24
|
+
* THE REVIEWER'S PROTOCOL IS UNCHANGED, AND THAT IS THE POINT. `FastReviewer`
|
|
25
|
+
* steers from exactly one place (the `tool_execution_end` that brings the batch
|
|
26
|
+
* to zero), never steers into a requested abort, and proves delivery at the
|
|
27
|
+
* next `turn_start`. The two guards need host-side facts, and a child supplies
|
|
28
|
+
* them differently than the lead:
|
|
29
|
+
*
|
|
30
|
+
* - `userSteerCount` is ALWAYS ZERO: no human can steer a child. That makes
|
|
31
|
+
* the reviewer's `clearAllQueues` provably safe by construction (D-21).
|
|
32
|
+
* - `isAbortRequested` comes from the HANDLE-level abort flag that
|
|
33
|
+
* `TeamRuntime` sets BEFORE calling `agent.abort()`. Reading the agent
|
|
34
|
+
* alone is not enough: an Esc lands as `abortAll()`, and without the flag
|
|
35
|
+
* the reviewer would race the abort it was told about.
|
|
36
|
+
* - `emit` and `notify` LOG INSTEAD OF RENDERING. A child has no transcript
|
|
37
|
+
* card and no status chip; the team panel row and the dispatch report are
|
|
38
|
+
* where a child's internals belong, and review spend is bounded by the
|
|
39
|
+
* same `fast.reviewMaxPerSession` budget the lead obeys.
|
|
40
|
+
*/
|
|
41
|
+
import { getLogger } from '../logging/logger.js';
|
|
42
|
+
import { resolveFastTier } from '../fast/resolve.js';
|
|
43
|
+
import { FastReviewer } from '../fast/reviewer.js';
|
|
44
|
+
/**
|
|
45
|
+
* A snapshot for the reviewer's self-disable announcement. The lead's
|
|
46
|
+
* `FastWiring.snapshot()` builds the same shape from its own reviewer; a child
|
|
47
|
+
* has no wiring, so this builds it from the reviewer the module itself holds.
|
|
48
|
+
*
|
|
49
|
+
* `pricingUnknown` is FALSE because the child reviewer never prices anything:
|
|
50
|
+
* it logs counts, not currency, and an unpriced model id therefore cannot lie
|
|
51
|
+
* here the way it can on the lead's status line (C-11).
|
|
52
|
+
*/
|
|
53
|
+
function buildSnapshot(reviewer, tier) {
|
|
54
|
+
return {
|
|
55
|
+
live: tier.ok,
|
|
56
|
+
selfDisabled: reviewer?.isSelfDisabled() ?? false,
|
|
57
|
+
model: tier.ok ? tier.ref.modelId : '',
|
|
58
|
+
sameAsMain: tier.ok ? tier.sameAsMain : false,
|
|
59
|
+
reviews: reviewer?.reviewCount() ?? 0,
|
|
60
|
+
reviewBudget: reviewer?.budgetLimit() ?? 0,
|
|
61
|
+
budgetReached: reviewer?.isBudgetReached() ?? false,
|
|
62
|
+
usage: reviewer?.sessionUsage() ?? { inputTokens: 0, outputTokens: 0 },
|
|
63
|
+
pricingUnknown: false,
|
|
64
|
+
inFlight: reviewer?.isReviewInFlight() ?? false,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Build one reviewer for one child, or `undefined` when the session has no
|
|
69
|
+
* live review to give it.
|
|
70
|
+
*
|
|
71
|
+
* `undefined` RATHER THAN A NO-OP REVIEWER, for the same reason
|
|
72
|
+
* `createChildContextManager` declines: the caller then wires nothing at all,
|
|
73
|
+
* and a session with the fast tier off builds the child it always built.
|
|
74
|
+
*/
|
|
75
|
+
export function createChildReviewer(req, deps) {
|
|
76
|
+
// GATED ON THE SAME THREE FACTS THE LEAD'S REVIEWER IS: the wiring is live
|
|
77
|
+
// (`available()`), the policy allows reviews (`fast.review`), and the tier
|
|
78
|
+
// resolves against the LIVE config. Re-checked, never snapshotted, so a
|
|
79
|
+
// mid-dispatch `/fast off` or a settings-screen model edit reaches the next
|
|
80
|
+
// child even when it cannot stop one already running.
|
|
81
|
+
const tier = resolveFastTier(deps.getConfig(), deps.hasKey);
|
|
82
|
+
if (!deps.available() || !deps.getConfig().fast.review || !tier.ok)
|
|
83
|
+
return undefined;
|
|
84
|
+
const log = getLogger().child('fast');
|
|
85
|
+
let reviewer = null;
|
|
86
|
+
const built = new FastReviewer({
|
|
87
|
+
subscribe: (listener) => req.agent.subscribe(listener),
|
|
88
|
+
complete: deps.complete,
|
|
89
|
+
steer: (text) => req.agent.steer(text),
|
|
90
|
+
isRunning: req.isRunning,
|
|
91
|
+
isAbortRequested: req.isAbortRequested,
|
|
92
|
+
userSteerCount: () => 0,
|
|
93
|
+
clearAllQueues: () => req.agent.clearAllQueues(),
|
|
94
|
+
getTier: () => resolveFastTier(deps.getConfig(), deps.hasKey),
|
|
95
|
+
getConfig: () => deps.getConfig().fast,
|
|
96
|
+
available: () => deps.available() && resolveFastTier(deps.getConfig(), deps.hasKey).ok,
|
|
97
|
+
getApiKey: (providerId) => deps.getApiKey(providerId, 'fast'),
|
|
98
|
+
emit: (event) => {
|
|
99
|
+
log.info('child_review', { label: req.label, type: event.type });
|
|
100
|
+
},
|
|
101
|
+
snapshot: () => buildSnapshot(reviewer, tier),
|
|
102
|
+
// A CHILD HAS NO TRANSCRIPT. The lead's warnings ride `notify` into a card
|
|
103
|
+
// the user reads; the same text from a background worker would claim the
|
|
104
|
+
// user's attention for an agent they cannot see. It is logged instead.
|
|
105
|
+
notify: (level, text) => {
|
|
106
|
+
log.warn('child_review_notice', { label: req.label, level, text });
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
reviewer = built;
|
|
110
|
+
built.setGoal(req.goal);
|
|
111
|
+
log.info('child_review_attached', {
|
|
112
|
+
label: req.label,
|
|
113
|
+
model: tier.ref.modelId,
|
|
114
|
+
reviewEveryTurns: deps.getConfig().fast.reviewEveryTurns,
|
|
115
|
+
});
|
|
116
|
+
return { dispose: () => built.dispose() };
|
|
117
|
+
}
|
|
@@ -33,7 +33,18 @@ export declare class TeamHumanQueue {
|
|
|
33
33
|
private readonly confirm;
|
|
34
34
|
/** The chain. Never rejects: a failed request resolves `false` (deny). */
|
|
35
35
|
private tail;
|
|
36
|
+
/**
|
|
37
|
+
* Labels with a request chained but not yet answered (team-overseer I-OV3).
|
|
38
|
+
*
|
|
39
|
+
* The queue pauses the CHILD's own watchdog on enqueue; the supervisor's
|
|
40
|
+
* soft silence timer needs the same fact one level up, or a user who takes
|
|
41
|
+
* four minutes over the first dialog burns inspection budget on siblings
|
|
42
|
+
* that are third in the FIFO and provably fine.
|
|
43
|
+
*/
|
|
44
|
+
private readonly waiting;
|
|
36
45
|
constructor(confirm: (req: ConfirmRequest) => Promise<boolean>);
|
|
46
|
+
/** Whether `label` has a confirmation queued or displayed right now. */
|
|
47
|
+
isWaiting(label: string): boolean;
|
|
37
48
|
/**
|
|
38
49
|
* Ask the human about `req` on behalf of `label`, one at a time.
|
|
39
50
|
*
|
package/dist/team/human-queue.js
CHANGED
|
@@ -27,9 +27,22 @@ export class TeamHumanQueue {
|
|
|
27
27
|
confirm;
|
|
28
28
|
/** The chain. Never rejects: a failed request resolves `false` (deny). */
|
|
29
29
|
tail = Promise.resolve();
|
|
30
|
+
/**
|
|
31
|
+
* Labels with a request chained but not yet answered (team-overseer I-OV3).
|
|
32
|
+
*
|
|
33
|
+
* The queue pauses the CHILD's own watchdog on enqueue; the supervisor's
|
|
34
|
+
* soft silence timer needs the same fact one level up, or a user who takes
|
|
35
|
+
* four minutes over the first dialog burns inspection budget on siblings
|
|
36
|
+
* that are third in the FIFO and provably fine.
|
|
37
|
+
*/
|
|
38
|
+
waiting = new Set();
|
|
30
39
|
constructor(confirm) {
|
|
31
40
|
this.confirm = confirm;
|
|
32
41
|
}
|
|
42
|
+
/** Whether `label` has a confirmation queued or displayed right now. */
|
|
43
|
+
isWaiting(label) {
|
|
44
|
+
return this.waiting.has(label);
|
|
45
|
+
}
|
|
33
46
|
/**
|
|
34
47
|
* Ask the human about `req` on behalf of `label`, one at a time.
|
|
35
48
|
*
|
|
@@ -41,13 +54,17 @@ export class TeamHumanQueue {
|
|
|
41
54
|
*/
|
|
42
55
|
request(child, label, req, signal) {
|
|
43
56
|
child?.pauseIdleWatchdog();
|
|
57
|
+
this.waiting.add(label);
|
|
44
58
|
const queued = this.enqueue(async () => {
|
|
45
59
|
if (signal?.aborted)
|
|
46
60
|
return false;
|
|
47
61
|
return this.confirm({ ...req, summary: `[${label}] ${req.summary}` });
|
|
48
62
|
});
|
|
49
63
|
const raced = signal ? Promise.race([queued, abortsToFalse(signal)]) : queued;
|
|
50
|
-
return raced.finally(() =>
|
|
64
|
+
return raced.finally(() => {
|
|
65
|
+
this.waiting.delete(label);
|
|
66
|
+
child?.resumeIdleWatchdog();
|
|
67
|
+
});
|
|
51
68
|
}
|
|
52
69
|
/**
|
|
53
70
|
* Chain `run` after everything already queued.
|
package/dist/team/limits.d.ts
CHANGED
|
@@ -101,6 +101,46 @@ export declare const TEAM_LIMITS: {
|
|
|
101
101
|
* nothing a re-dispatch by the lead would not buy better. */
|
|
102
102
|
readonly maxColdStartRetries: 1;
|
|
103
103
|
readonly retryBackoffMs: 2000;
|
|
104
|
+
/**
|
|
105
|
+
* Wall-clock budget of everything one inspection may carry. The digest holds
|
|
106
|
+
* run facts, the supervisor's own memory and the tail of the child's live
|
|
107
|
+
* history; unbounded, one look at a child that read five big files would
|
|
108
|
+
* make the "cheap" supervision call cost more than the child (the argument
|
|
109
|
+
* FAST_LIMITS.digestMaxChars makes, one level up).
|
|
110
|
+
*/
|
|
111
|
+
readonly overseerDigestBytes: 8000;
|
|
112
|
+
/** Clamp on a `nudge` guidance text. Advice, not a second brief. */
|
|
113
|
+
readonly overseerGuidanceChars: 1200;
|
|
114
|
+
/** Clamp on the decision `reason` recorded in the report. */
|
|
115
|
+
readonly overseerReasonChars: 200;
|
|
116
|
+
/**
|
|
117
|
+
* Inspections PER CHILD per dispatch. An indecisive supervisor must stop
|
|
118
|
+
* burning fast-tier calls; past the budget the child simply runs under its
|
|
119
|
+
* hard ceilings and the supervisor goes quiet.
|
|
120
|
+
*/
|
|
121
|
+
readonly overseerMaxInspectionsPerChild: 4;
|
|
122
|
+
/**
|
|
123
|
+
* Rebuilds PER CHILD per dispatch. One: a replacement that also stalls dies
|
|
124
|
+
* on its own watchdog rather than ping-ponging fresh children at a task the
|
|
125
|
+
* supervisor already failed to rescue once.
|
|
126
|
+
*/
|
|
127
|
+
readonly overseerMaxReplacementsPerChild: 1;
|
|
128
|
+
/**
|
|
129
|
+
* First wall-clock check when `team.subagentTimeoutMs === 0` (the shipped
|
|
130
|
+
* default): the supervisor still looks even when no ceiling was configured.
|
|
131
|
+
*/
|
|
132
|
+
readonly overseerDefaultCheckMs: 300000;
|
|
133
|
+
/** Clamp on the model-chosen `nextCheckMs`, lower bound. */
|
|
134
|
+
readonly overseerNextCheckMinMs: 60000;
|
|
135
|
+
/** Clamp on the model-chosen `nextCheckMs`, upper bound. */
|
|
136
|
+
readonly overseerNextCheckMaxMs: 900000;
|
|
137
|
+
/**
|
|
138
|
+
* A child's OWN idle watchdog, multiplied by this while the supervisor is
|
|
139
|
+
* active, so the soft silence inspection at `idleTimeoutMs` always precedes
|
|
140
|
+
* the hard abort. The watchdog stays as the backstop for a child that
|
|
141
|
+
* wedges past every inspection.
|
|
142
|
+
*/
|
|
143
|
+
readonly overseerWatchdogFactor: 4;
|
|
104
144
|
};
|
|
105
145
|
/**
|
|
106
146
|
* The two comm tools, which are POLICY-EXEMPT inside a child (§3.11).
|
package/dist/team/limits.js
CHANGED
|
@@ -106,6 +106,47 @@ export const TEAM_LIMITS = {
|
|
|
106
106
|
* nothing a re-dispatch by the lead would not buy better. */
|
|
107
107
|
maxColdStartRetries: 1,
|
|
108
108
|
retryBackoffMs: 2_000,
|
|
109
|
+
// --- The dispatch supervisor (team-overseer) ---
|
|
110
|
+
/**
|
|
111
|
+
* Wall-clock budget of everything one inspection may carry. The digest holds
|
|
112
|
+
* run facts, the supervisor's own memory and the tail of the child's live
|
|
113
|
+
* history; unbounded, one look at a child that read five big files would
|
|
114
|
+
* make the "cheap" supervision call cost more than the child (the argument
|
|
115
|
+
* FAST_LIMITS.digestMaxChars makes, one level up).
|
|
116
|
+
*/
|
|
117
|
+
overseerDigestBytes: 8_000,
|
|
118
|
+
/** Clamp on a `nudge` guidance text. Advice, not a second brief. */
|
|
119
|
+
overseerGuidanceChars: 1_200,
|
|
120
|
+
/** Clamp on the decision `reason` recorded in the report. */
|
|
121
|
+
overseerReasonChars: 200,
|
|
122
|
+
/**
|
|
123
|
+
* Inspections PER CHILD per dispatch. An indecisive supervisor must stop
|
|
124
|
+
* burning fast-tier calls; past the budget the child simply runs under its
|
|
125
|
+
* hard ceilings and the supervisor goes quiet.
|
|
126
|
+
*/
|
|
127
|
+
overseerMaxInspectionsPerChild: 4,
|
|
128
|
+
/**
|
|
129
|
+
* Rebuilds PER CHILD per dispatch. One: a replacement that also stalls dies
|
|
130
|
+
* on its own watchdog rather than ping-ponging fresh children at a task the
|
|
131
|
+
* supervisor already failed to rescue once.
|
|
132
|
+
*/
|
|
133
|
+
overseerMaxReplacementsPerChild: 1,
|
|
134
|
+
/**
|
|
135
|
+
* First wall-clock check when `team.subagentTimeoutMs === 0` (the shipped
|
|
136
|
+
* default): the supervisor still looks even when no ceiling was configured.
|
|
137
|
+
*/
|
|
138
|
+
overseerDefaultCheckMs: 300_000,
|
|
139
|
+
/** Clamp on the model-chosen `nextCheckMs`, lower bound. */
|
|
140
|
+
overseerNextCheckMinMs: 60_000,
|
|
141
|
+
/** Clamp on the model-chosen `nextCheckMs`, upper bound. */
|
|
142
|
+
overseerNextCheckMaxMs: 900_000,
|
|
143
|
+
/**
|
|
144
|
+
* A child's OWN idle watchdog, multiplied by this while the supervisor is
|
|
145
|
+
* active, so the soft silence inspection at `idleTimeoutMs` always precedes
|
|
146
|
+
* the hard abort. The watchdog stays as the backstop for a child that
|
|
147
|
+
* wedges past every inspection.
|
|
148
|
+
*/
|
|
149
|
+
overseerWatchdogFactor: 4,
|
|
109
150
|
};
|
|
110
151
|
/**
|
|
111
152
|
* The two comm tools, which are POLICY-EXEMPT inside a child (§3.11).
|