@aragon-agent/cli 0.6.20 → 0.6.22
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 +32 -12
- package/dist/agent/controller.js +13 -0
- package/dist/commands/builtins.js +2 -1
- package/dist/config/cli-commands.d.ts +1 -1
- package/dist/config/cli-commands.js +3 -0
- package/dist/config/schema.d.ts +28 -12
- package/dist/config/schema.js +4 -0
- package/dist/team/limits.d.ts +73 -13
- package/dist/team/limits.js +82 -15
- package/dist/team/normalize.js +7 -2
- package/dist/team/overseer.d.ts +154 -16
- package/dist/team/overseer.js +342 -46
- package/dist/team/report.d.ts +9 -0
- package/dist/team/report.js +32 -4
- package/dist/team/runtime.d.ts +51 -9
- package/dist/team/runtime.js +478 -71
- package/dist/team/task-tool.d.ts +7 -0
- package/dist/team/task-tool.js +2 -0
- package/dist/team/types.d.ts +43 -2
- package/dist/ui/App.js +80 -2
- package/dist/ui/Composer.d.ts +14 -1
- package/dist/ui/Composer.js +6 -3
- package/dist/ui/PromptInput.d.ts +10 -1
- package/dist/ui/PromptInput.js +11 -1
- package/dist/ui/TeamPanel.d.ts +22 -0
- package/dist/ui/TeamPanel.js +71 -2
- package/dist/ui/editor-reducer.js +10 -5
- package/dist/ui/interaction-copy.d.ts +1 -0
- package/dist/ui/interaction-copy.js +1 -0
- package/dist/ui/interaction-hints.d.ts +2 -0
- package/dist/ui/interaction-hints.js +1 -1
- package/dist/ui/layout/status-layout.js +1 -0
- package/dist/ui/layout/team-panel.js +4 -1
- package/dist/ui/overlays/HelpOverlay.js +4 -1
- package/dist/ui/overlays/ModelProfilePicker.js +12 -1
- package/dist/ui/status-feedback.d.ts +3 -1
- package/dist/ui/status-feedback.js +3 -1
- package/package.json +2 -2
package/dist/team/overseer.js
CHANGED
|
@@ -1,34 +1,57 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The dispatch supervisor (team-overseer).
|
|
2
|
+
* The dispatch supervisor (team-overseer, upgraded by subagent-overseer-v2).
|
|
3
3
|
*
|
|
4
4
|
* ASCII ONLY: `src/team/**` is inside the glyph scanner's scope
|
|
5
5
|
* (`glyphs.test.ts`), so no literal in this tree may hold a non-ASCII byte.
|
|
6
6
|
*
|
|
7
7
|
* A TIME CEILING ON A CHILD IS AN INSPECTION TRIGGER, NOT A DEATH SENTENCE.
|
|
8
|
-
* When a child stalls (event silence) or
|
|
9
|
-
*
|
|
8
|
+
* When a child stalls (event silence) or a cadence tick comes due, the
|
|
9
|
+
* supervisor looks at it through ONE bounded fast-tier call and decides:
|
|
10
10
|
* keep waiting, steer a nudge in to help it recover, rebuild it with an
|
|
11
11
|
* amended brief, or abandon it with a recorded reason. Only the supervisor
|
|
12
12
|
* "decides"; the RUNTIME applies (see `runtime.ts`), because every child
|
|
13
13
|
* lifecycle change must funnel through the worker loop that awaits it
|
|
14
14
|
* (I-OV1).
|
|
15
15
|
*
|
|
16
|
+
* THE SUPERVISOR IS A NO-LOOP, BOUNDED, REMEMBERED CALLEE - NOT AN AGENT
|
|
17
|
+
* (subagent-overseer-v2 D-1). The requirement says "a dedicated supervising
|
|
18
|
+
* subagent on the fast model"; it is satisfied by a per-dispatch, remembered,
|
|
19
|
+
* cadence-woken caller on the fast tier rather than a real `Agent.prompt()`
|
|
20
|
+
* loop, because a supervisor that can itself wedge needs a second supervisor.
|
|
21
|
+
*
|
|
22
|
+
* TWO INSPECTION SHAPES (D-3). Event silence triggers a SINGLE-child look
|
|
23
|
+
* (fast reaction to a suspected stall, unchanged from round 1); the cadence
|
|
24
|
+
* ladder triggers a BATCH look that carries every live child in one digest
|
|
25
|
+
* and returns one JSON array of per-child decisions. The batch call carries
|
|
26
|
+
* its own output budget (`overseerBatchOutputTokens`, R-P0-1): a full
|
|
27
|
+
* ten-child reply does not physically fit in the single-inspection 512
|
|
28
|
+
* tokens, and a truncated reply would parse to nothing - all wait, exactly
|
|
29
|
+
* when help is most needed.
|
|
30
|
+
*
|
|
16
31
|
* THE TRANSPORT IS THE REVIEWER'S, DELIBERATELY. One `complete` call, no
|
|
17
|
-
* tools, thinking off, temperature 0, output capped
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
32
|
+
* tools, thinking off, temperature 0, output capped (512 single /
|
|
33
|
+
* 2048 batch) and wall clock at `FAST_LIMITS.reviewTimeoutMs` — a
|
|
34
|
+
* supervision call that could itself hang would need a second supervisor,
|
|
35
|
+
* which is the regress this design refuses.
|
|
21
36
|
*
|
|
22
37
|
* REPAIR, NEVER REJECT (the `normalizeSubagentSpecs` rule). An unparseable
|
|
23
38
|
* or out-of-budget decision becomes `wait` with the default interval: the
|
|
24
39
|
* failure mode of a supervisor must be "looked and let it run", never
|
|
25
|
-
* "looked and killed it" (I-OV2).
|
|
40
|
+
* "looked and killed it" (I-OV2). In a batch, one bad ENTRY degrades only
|
|
41
|
+
* that entry; a wholly unparseable reply degrades the whole batch to wait
|
|
42
|
+
* with zero action side effects (R-P0-1 / AC-13).
|
|
26
43
|
*/
|
|
27
44
|
import { resolveFastTier } from '../fast/resolve.js';
|
|
28
|
-
import { assistantText } from '../fast/review-call.js';
|
|
45
|
+
import { accumulateUsage, assistantText, usageOf } from '../fast/review-call.js';
|
|
29
46
|
import { FAST_LIMITS } from '../fast/limits.js';
|
|
30
47
|
import { truncateBytes } from './report.js';
|
|
31
48
|
import { TEAM_LIMITS } from './limits.js';
|
|
49
|
+
/**
|
|
50
|
+
* Bumped whenever either supervisor protocol prompt changes (D-9), so a
|
|
51
|
+
* behaviour report can be tied to a prompt revision with one grep.
|
|
52
|
+
* `TEAM_BLOCK_VERSION` covers only the LEAD's `<team>` block, not these.
|
|
53
|
+
*/
|
|
54
|
+
export const OVERSEER_PROMPT_VERSION = 'v2-2026-10';
|
|
32
55
|
/** How many trailing history messages the digest carries. */
|
|
33
56
|
const DIGEST_TAIL_MESSAGES = 8;
|
|
34
57
|
/** Per-message text clamp inside the digest. */
|
|
@@ -94,6 +117,51 @@ export function buildChildDigest(input) {
|
|
|
94
117
|
}
|
|
95
118
|
return truncateBytes(lines.join('\n'), TEAM_LIMITS.overseerDigestBytes);
|
|
96
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* The per-child sections of one batch payload (D-3).
|
|
122
|
+
*
|
|
123
|
+
* SORTED BY LABEL so the same roster produces the byte-identical payload -
|
|
124
|
+
* the batch is the one place where ordering is observable to the model and
|
|
125
|
+
* to every test that asserts on it. Each child's digest is clamped to
|
|
126
|
+
* `overseerPerChildDigestBytes` (facts first, history tail last - R-P2-5).
|
|
127
|
+
* The whole-batch ceiling is applied by the joiner in the provider request.
|
|
128
|
+
*/
|
|
129
|
+
export function buildBatchSections(children) {
|
|
130
|
+
return [...children]
|
|
131
|
+
.sort((a, b) => a.run.label.localeCompare(b.run.label, 'en'))
|
|
132
|
+
.map((child) => ({
|
|
133
|
+
label: child.run.label,
|
|
134
|
+
goal: child.run.description,
|
|
135
|
+
digest: truncateBytes(buildChildDigest(child), TEAM_LIMITS.overseerPerChildDigestBytes),
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
// Cadence ladder (D-2) - pure math, so tests assert arithmetic, not timers
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
/**
|
|
142
|
+
* The FIRST cadence check, from the D-8 priority: `overseerIntervalMs` when
|
|
143
|
+
* the user named one; otherwise a positive `subagentTimeoutMs` (compat with
|
|
144
|
+
* users who already treated it as a trigger); otherwise the structural
|
|
145
|
+
* default. Read-only derivation - the user's stored config is never
|
|
146
|
+
* rewritten to "resolve" this.
|
|
147
|
+
*/
|
|
148
|
+
export function resolveOverseerFirstCheckMs(team) {
|
|
149
|
+
if (team.overseerIntervalMs > 0)
|
|
150
|
+
return team.overseerIntervalMs;
|
|
151
|
+
if (team.subagentTimeoutMs > 0)
|
|
152
|
+
return team.subagentTimeoutMs;
|
|
153
|
+
return TEAM_LIMITS.overseerDefaultCheckMs;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* The next ladder step after one that produced no action: grow by
|
|
157
|
+
* `overseerCadenceRatio`, clamped to the structural range. With the shipped
|
|
158
|
+
* 300 s base the checks land at 300 / 780 / 1548 / 2448 / ... seconds - a
|
|
159
|
+
* healthy slow child is looked at logarithmically in its run time (AC-11).
|
|
160
|
+
*/
|
|
161
|
+
export function nextOverseerLadderStepMs(previousMs) {
|
|
162
|
+
const grown = Math.round(previousMs * TEAM_LIMITS.overseerCadenceRatio);
|
|
163
|
+
return Math.min(TEAM_LIMITS.overseerNextCheckMaxMs, Math.max(TEAM_LIMITS.overseerNextCheckMinMs, grown));
|
|
164
|
+
}
|
|
97
165
|
// ---------------------------------------------------------------------------
|
|
98
166
|
// Decision normalization
|
|
99
167
|
// ---------------------------------------------------------------------------
|
|
@@ -119,8 +187,39 @@ function waitDecision(reason, nextCheckMs) {
|
|
|
119
187
|
* replaced or abandoned, never waited on or nudged, so those normalize to
|
|
120
188
|
* `abandon` rather than being rejected — a post-mortem answer the protocol
|
|
121
189
|
* cannot honour must still produce a decision the runtime can apply.
|
|
190
|
+
*
|
|
191
|
+
* The single-inspection char limits (200/1200) are the defaults here; the
|
|
192
|
+
* batch path calls `normalizeWithLimits` directly with the tighter protocol
|
|
193
|
+
* limits (R-P0-1).
|
|
122
194
|
*/
|
|
123
195
|
export function normalizeOverseerDecision(raw, childAlive) {
|
|
196
|
+
return normalizeWithLimits(raw, childAlive, {
|
|
197
|
+
reasonChars: TEAM_LIMITS.overseerReasonChars,
|
|
198
|
+
guidanceChars: TEAM_LIMITS.overseerGuidanceChars,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Parse the model's answer text as one JSON decision object.
|
|
203
|
+
*
|
|
204
|
+
* Forgiving on PURPOSE: models wrap JSON in prose and fences. The FIRST `{`
|
|
205
|
+
* to the LAST `}` is the only span tried, and anything that does not parse is
|
|
206
|
+
* `wait` — the caller never sees a throw from parsing.
|
|
207
|
+
*/
|
|
208
|
+
export function parseOverseerDecisionText(text, childAlive) {
|
|
209
|
+
const first = text.indexOf('{');
|
|
210
|
+
const last = text.lastIndexOf('}');
|
|
211
|
+
if (first >= 0 && last > first) {
|
|
212
|
+
try {
|
|
213
|
+
return normalizeOverseerDecision(JSON.parse(text.slice(first, last + 1)), childAlive);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
// Fall through to the safe default.
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return waitDecision('decision unparseable; defaulting to wait');
|
|
220
|
+
}
|
|
221
|
+
/** Normalize with EXPLICIT limits so the two protocols stay independently tunable. */
|
|
222
|
+
function normalizeWithLimits(raw, childAlive, limits) {
|
|
124
223
|
const obj = (raw && typeof raw === 'object' ? raw : {});
|
|
125
224
|
const action = typeof obj.action === 'string' && VALID_ACTIONS.has(obj.action)
|
|
126
225
|
? obj.action
|
|
@@ -129,7 +228,7 @@ export function normalizeOverseerDecision(raw, childAlive) {
|
|
|
129
228
|
? obj.reason.trim()
|
|
130
229
|
: 'no reason given';
|
|
131
230
|
const guidance = typeof obj.guidance === 'string' && obj.guidance.trim().length > 0
|
|
132
|
-
? clampChars(obj.guidance.trim(),
|
|
231
|
+
? clampChars(obj.guidance.trim(), limits.guidanceChars)
|
|
133
232
|
: undefined;
|
|
134
233
|
const nextCheckMs = clampNextCheckMs(obj.nextCheckMs);
|
|
135
234
|
if (!childAlive && (action === 'wait' || action === 'nudge')) {
|
|
@@ -140,30 +239,34 @@ export function normalizeOverseerDecision(raw, childAlive) {
|
|
|
140
239
|
}
|
|
141
240
|
return {
|
|
142
241
|
action,
|
|
143
|
-
reason: clampChars(reason,
|
|
242
|
+
reason: clampChars(reason, limits.reasonChars),
|
|
144
243
|
...(guidance !== undefined && action === 'nudge' ? { guidance } : {}),
|
|
145
244
|
...(action === 'wait' && nextCheckMs !== undefined ? { nextCheckMs } : {}),
|
|
146
245
|
};
|
|
147
246
|
}
|
|
148
247
|
/**
|
|
149
|
-
* Parse the model's answer text as
|
|
248
|
+
* Parse the model's answer text as a JSON ARRAY of per-child decisions (D-3).
|
|
150
249
|
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
250
|
+
* Same forgiving span rule as the single path - first `[` to last `]` - and
|
|
251
|
+
* the same failure semantics: a reply that is not an array (prose, a single
|
|
252
|
+
* object, or TRUNCATED output with no closing `]`) is `[]`, which the caller
|
|
253
|
+
* turns into all-wait with zero action side effects (R-P0-1 / AC-13). Never
|
|
254
|
+
* throws.
|
|
154
255
|
*/
|
|
155
|
-
export function
|
|
156
|
-
const first = text.indexOf('
|
|
157
|
-
const last = text.lastIndexOf('
|
|
256
|
+
export function parseOverseerBatchText(text) {
|
|
257
|
+
const first = text.indexOf('[');
|
|
258
|
+
const last = text.lastIndexOf(']');
|
|
158
259
|
if (first >= 0 && last > first) {
|
|
159
260
|
try {
|
|
160
|
-
|
|
261
|
+
const parsed = JSON.parse(text.slice(first, last + 1));
|
|
262
|
+
if (Array.isArray(parsed))
|
|
263
|
+
return parsed;
|
|
161
264
|
}
|
|
162
265
|
catch {
|
|
163
|
-
// Fall through to the
|
|
266
|
+
// Fall through to the empty batch.
|
|
164
267
|
}
|
|
165
268
|
}
|
|
166
|
-
return
|
|
269
|
+
return [];
|
|
167
270
|
}
|
|
168
271
|
const OVERSEER_SYSTEM_PROMPT = [
|
|
169
272
|
'You are the supervisor of a team of subagents. You are shown ONE subagent',
|
|
@@ -183,6 +286,30 @@ const OVERSEER_SYSTEM_PROMPT = [
|
|
|
183
286
|
'A nudge is only delivered after the child finishes its current tool call;',
|
|
184
287
|
'a long-running tool is not a stall. Prefer the cheapest action that works.',
|
|
185
288
|
].join('\n');
|
|
289
|
+
const OVERSEER_BATCH_SYSTEM_PROMPT = [
|
|
290
|
+
'You are the supervisor of a team of subagents. You are shown EVERY live',
|
|
291
|
+
'subagent of one dispatch, separated by --- lines, on a periodic check.',
|
|
292
|
+
'Decide ONCE PER SUBAGENT. Answer with ONE JSON array and nothing else -',
|
|
293
|
+
'one object per subagent, using exactly the label shown:',
|
|
294
|
+
'[{"label": "<label>", "action": "wait" | "nudge" | "replace" | "abandon",',
|
|
295
|
+
' "reason": "<= 80 chars", "guidance": "<= 400 chars, nudge only",',
|
|
296
|
+
' "nextCheckMs": <ms, wait only>}, ...]',
|
|
297
|
+
'Keep every reason and guidance SHORT: the whole array must fit a small',
|
|
298
|
+
'output budget and a truncated array helps nobody.',
|
|
299
|
+
'wait: the child looks healthy or is legitimately blocked (API backoff,',
|
|
300
|
+
' waiting on a teammate or a human confirmation) - keep waiting, pick the',
|
|
301
|
+
' next check delay.',
|
|
302
|
+
'nudge: the child is stuck in a way a hint can fix - write the hint as',
|
|
303
|
+
' guidance.',
|
|
304
|
+
'replace: the child cannot recover - a fresh child will be built with your',
|
|
305
|
+
' reason and guidance prepended to its brief.',
|
|
306
|
+
'abandon: the task is not worth more attempts - the child is stopped and',
|
|
307
|
+
' your reason is reported to the lead.',
|
|
308
|
+
'You may use whole-team judgement: a child far behind healthy peers on the',
|
|
309
|
+
'same brief is a different signal than one slow child alone.',
|
|
310
|
+
'A nudge is only delivered after the child finishes its current tool call;',
|
|
311
|
+
'a long-running tool is not a stall. Prefer the cheapest action that works.',
|
|
312
|
+
].join('\n');
|
|
186
313
|
function buildInspectionRequest(params) {
|
|
187
314
|
return {
|
|
188
315
|
model: params.modelId,
|
|
@@ -196,44 +323,94 @@ function buildInspectionRequest(params) {
|
|
|
196
323
|
signal: params.signal,
|
|
197
324
|
};
|
|
198
325
|
}
|
|
326
|
+
function buildBatchRequest(params) {
|
|
327
|
+
return {
|
|
328
|
+
model: params.modelId,
|
|
329
|
+
...(params.baseUrl ? { baseUrl: params.baseUrl } : {}),
|
|
330
|
+
apiKey: params.apiKey,
|
|
331
|
+
systemPrompt: OVERSEER_BATCH_SYSTEM_PROMPT,
|
|
332
|
+
messages: [{ role: 'user', content: params.digest, timestamp: Date.now() }],
|
|
333
|
+
// NOT FAST_LIMITS.reviewOutputTokens: a ten-child array does not fit 512
|
|
334
|
+
// tokens, and a truncated reply parses to all-wait (R-P0-1 / AC-13).
|
|
335
|
+
maxTokens: TEAM_LIMITS.overseerBatchOutputTokens,
|
|
336
|
+
thinkingLevel: 'off',
|
|
337
|
+
temperature: 0,
|
|
338
|
+
signal: params.signal,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
const NO_USAGE = { inputTokens: 0, outputTokens: 0 };
|
|
199
342
|
/**
|
|
200
343
|
* The fast-tier-backed provider, mirroring `createChildReviewer`'s shape so
|
|
201
344
|
* the controller wires it the same way: shared transport, live predicates,
|
|
202
345
|
* fail-soft (any failure resolves to `wait` — I-OV2).
|
|
346
|
+
*
|
|
347
|
+
* USAGE IS ACCUMULATED HERE (D-7), off every `AssistantMessage` the shared
|
|
348
|
+
* transport returns, along the same `usageOf` / `accumulateUsage` path the
|
|
349
|
+
* fast reviewer already proved. A reply without a usage block contributes
|
|
350
|
+
* nothing rather than a fabricated zero-cost total.
|
|
203
351
|
*/
|
|
204
352
|
export function createFastOverseerProvider(deps) {
|
|
205
353
|
const tier = () => resolveFastTier(deps.getConfig(), deps.hasKey);
|
|
354
|
+
let totalUsage = { ...NO_USAGE };
|
|
355
|
+
const call = async (build) => {
|
|
356
|
+
const resolved = tier();
|
|
357
|
+
if (!deps.available() || !resolved.ok)
|
|
358
|
+
return null;
|
|
359
|
+
const apiKey = deps.getApiKey(resolved.ref.providerId, 'fast');
|
|
360
|
+
if (apiKey === undefined)
|
|
361
|
+
return null;
|
|
362
|
+
const controller = new AbortController();
|
|
363
|
+
const timer = setTimeout(() => controller.abort(), FAST_LIMITS.reviewTimeoutMs);
|
|
364
|
+
try {
|
|
365
|
+
const message = await deps.complete(resolved.ref.providerId, build(resolved.ref, apiKey, controller.signal));
|
|
366
|
+
totalUsage = accumulateUsage(totalUsage, usageOf(message).usage);
|
|
367
|
+
return assistantText(message);
|
|
368
|
+
}
|
|
369
|
+
finally {
|
|
370
|
+
clearTimeout(timer);
|
|
371
|
+
}
|
|
372
|
+
};
|
|
206
373
|
return {
|
|
207
374
|
active: () => deps.available() && tier().ok,
|
|
208
375
|
async inspect(req) {
|
|
209
|
-
const resolved = tier();
|
|
210
|
-
if (!deps.available() || !resolved.ok) {
|
|
211
|
-
return waitDecision('fast tier unavailable; defaulting to wait');
|
|
212
|
-
}
|
|
213
|
-
const apiKey = deps.getApiKey(resolved.ref.providerId, 'fast');
|
|
214
|
-
if (apiKey === undefined) {
|
|
215
|
-
return waitDecision('no fast-tier key; defaulting to wait');
|
|
216
|
-
}
|
|
217
|
-
const controller = new AbortController();
|
|
218
|
-
const timer = setTimeout(() => controller.abort(), FAST_LIMITS.reviewTimeoutMs);
|
|
219
376
|
try {
|
|
220
|
-
const
|
|
377
|
+
const text = await call((ref, apiKey, signal) => buildInspectionRequest({
|
|
221
378
|
digest: req.digest,
|
|
222
|
-
modelId:
|
|
223
|
-
...(
|
|
379
|
+
modelId: ref.modelId,
|
|
380
|
+
...(ref.baseUrl ? { baseUrl: ref.baseUrl } : {}),
|
|
224
381
|
apiKey,
|
|
225
|
-
signal
|
|
382
|
+
signal,
|
|
226
383
|
}));
|
|
227
|
-
|
|
384
|
+
if (text === null)
|
|
385
|
+
return waitDecision('fast tier unavailable; defaulting to wait');
|
|
386
|
+
return parseOverseerDecisionText(text, true);
|
|
228
387
|
}
|
|
229
388
|
catch {
|
|
230
389
|
// INCLUDING a timeout abort: supervision failing is wait, never kill.
|
|
231
390
|
return waitDecision('inspection call failed; defaulting to wait');
|
|
232
391
|
}
|
|
233
|
-
|
|
234
|
-
|
|
392
|
+
},
|
|
393
|
+
async inspectBatch(req) {
|
|
394
|
+
try {
|
|
395
|
+
// The joiner owns the WHOLE-BATCH ceiling (D-3): sections arrive
|
|
396
|
+
// per-child-clamped and label-sorted; `hardMaxSubagents` means the
|
|
397
|
+
// clamp is a guard, not a squeeze.
|
|
398
|
+
const digest = truncateBytes(req.map((r) => `### ${r.label}\n${r.digest}`).join('\n---\n'), TEAM_LIMITS.overseerBatchDigestBytes);
|
|
399
|
+
const text = await call((ref, apiKey, signal) => buildBatchRequest({
|
|
400
|
+
digest,
|
|
401
|
+
modelId: ref.modelId,
|
|
402
|
+
...(ref.baseUrl ? { baseUrl: ref.baseUrl } : {}),
|
|
403
|
+
apiKey,
|
|
404
|
+
signal,
|
|
405
|
+
}));
|
|
406
|
+
return text ?? '';
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
// INCLUDING a timeout abort: supervision failing is wait, never kill.
|
|
410
|
+
return '';
|
|
235
411
|
}
|
|
236
412
|
},
|
|
413
|
+
usage: () => ({ ...totalUsage }),
|
|
237
414
|
};
|
|
238
415
|
}
|
|
239
416
|
export const OVERSEER_DEFAULT_NEXT_CHECK_MS = TEAM_LIMITS.overseerDefaultCheckMs;
|
|
@@ -243,14 +420,27 @@ export const OVERSEER_DEFAULT_NEXT_CHECK_MS = TEAM_LIMITS.overseerDefaultCheckMs
|
|
|
243
420
|
* THE CLASS NEVER TOUCHES A CHILD (I-OV1). It looks and decides; `runtime.ts`
|
|
244
421
|
* steers, flags, aborts and rebuilds, because only the worker loop can apply
|
|
245
422
|
* a lifecycle change without orphaning the replacement.
|
|
423
|
+
*
|
|
424
|
+
* LOOKS AND ACTIONS ARE SEPARATE LEDGERS (D-5). A LOOK is cost: every
|
|
425
|
+
* inspection of a child spends one, including a call that failed softly -
|
|
426
|
+
* which is what closes G-6 (a flaky transport used to burn the budget the
|
|
427
|
+
* same as a decision, and the supervisor went quiet exactly when the team
|
|
428
|
+
* was hardest to see). An ACTION is a mutation (nudge / replace / abandon)
|
|
429
|
+
* and is counted only when the runtime APPLIES it, through the `note*`
|
|
430
|
+
* methods below. A label skipped because an inspection is already in flight
|
|
431
|
+
* spends NOTHING (R-P1-4).
|
|
246
432
|
*/
|
|
247
433
|
export class TeamOverseer {
|
|
248
434
|
provider;
|
|
249
435
|
options;
|
|
250
436
|
memory = new Map();
|
|
251
|
-
|
|
437
|
+
looks = new Map();
|
|
438
|
+
nudges = new Map();
|
|
252
439
|
replacements = new Map();
|
|
440
|
+
abandons = new Map();
|
|
253
441
|
inFlight = new Set();
|
|
442
|
+
looksTotal = 0;
|
|
443
|
+
callCount = 0;
|
|
254
444
|
constructor(provider, options) {
|
|
255
445
|
this.provider = provider;
|
|
256
446
|
this.options = options;
|
|
@@ -264,24 +454,62 @@ export class TeamOverseer {
|
|
|
264
454
|
return false;
|
|
265
455
|
}
|
|
266
456
|
}
|
|
267
|
-
|
|
268
|
-
|
|
457
|
+
/** Whether ANY inspection of `label` is in flight (single or batch). */
|
|
458
|
+
isInFlight(label) {
|
|
459
|
+
return this.inFlight.has(label);
|
|
460
|
+
}
|
|
461
|
+
/** Per-child look budget spent (D-5). */
|
|
462
|
+
looksExhausted(label) {
|
|
463
|
+
return ((this.looks.get(label) ?? 0) >= TEAM_LIMITS.overseerMaxLooksPerChild ||
|
|
464
|
+
this.dispatchLooksExhausted());
|
|
465
|
+
}
|
|
466
|
+
/** The fool-proof dispatch-wide total (R-P1-6). */
|
|
467
|
+
dispatchLooksExhausted() {
|
|
468
|
+
return this.looksTotal >= TEAM_LIMITS.overseerMaxLooksPerDispatch;
|
|
469
|
+
}
|
|
470
|
+
nudgesLeft(label) {
|
|
471
|
+
return (this.nudges.get(label) ?? 0) < TEAM_LIMITS.overseerMaxNudgesPerChild;
|
|
269
472
|
}
|
|
270
473
|
replacementsLeft(label) {
|
|
271
474
|
return (this.replacements.get(label) ?? 0) < TEAM_LIMITS.overseerMaxReplacementsPerChild;
|
|
272
475
|
}
|
|
476
|
+
abandonsLeft(label) {
|
|
477
|
+
return (this.abandons.get(label) ?? 0) < TEAM_LIMITS.overseerMaxAbandonsPerChild;
|
|
478
|
+
}
|
|
479
|
+
/** Called by the runtime when it APPLIES a nudge, not when it merely reads one. */
|
|
480
|
+
noteNudge(label) {
|
|
481
|
+
this.nudges.set(label, (this.nudges.get(label) ?? 0) + 1);
|
|
482
|
+
}
|
|
273
483
|
/** Called by the runtime when it APPLIES a replace, not when it merely reads one. */
|
|
274
484
|
noteReplacement(label) {
|
|
275
485
|
this.replacements.set(label, (this.replacements.get(label) ?? 0) + 1);
|
|
276
486
|
}
|
|
487
|
+
/** Called by the runtime when it APPLIES an abandon, not when it merely reads one. */
|
|
488
|
+
noteAbandon(label) {
|
|
489
|
+
this.abandons.set(label, (this.abandons.get(label) ?? 0) + 1);
|
|
490
|
+
}
|
|
491
|
+
/** The provider's own accumulated inspection spend (D-7), zero when unreadable. */
|
|
492
|
+
usage() {
|
|
493
|
+
try {
|
|
494
|
+
return this.provider.usage();
|
|
495
|
+
}
|
|
496
|
+
catch {
|
|
497
|
+
return { ...NO_USAGE };
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
/** Provider invocations, including ones that failed softly (D-7). */
|
|
501
|
+
calls() {
|
|
502
|
+
return this.callCount;
|
|
503
|
+
}
|
|
277
504
|
/**
|
|
278
|
-
* ONE inspection
|
|
279
|
-
*
|
|
505
|
+
* ONE single-child inspection (the silence-triggered path, round-1
|
|
506
|
+
* behaviour preserved). `null` means QUIET — not active, no budget left, or
|
|
507
|
+
* one already in flight for this child — and the caller then does nothing,
|
|
280
508
|
* which is exactly what quiet should do.
|
|
281
509
|
*/
|
|
282
510
|
async inspect(req) {
|
|
283
511
|
const label = req.run.label;
|
|
284
|
-
if (!this.isActive() || this.
|
|
512
|
+
if (!this.isActive() || this.looksExhausted(label) || this.inFlight.has(label)) {
|
|
285
513
|
return null;
|
|
286
514
|
}
|
|
287
515
|
this.inFlight.add(label);
|
|
@@ -295,6 +523,7 @@ export class TeamOverseer {
|
|
|
295
523
|
elapsedSec: Math.max(0, (now - (req.run.startedAt ?? now)) / 1000),
|
|
296
524
|
});
|
|
297
525
|
let decision;
|
|
526
|
+
this.callCount += 1;
|
|
298
527
|
try {
|
|
299
528
|
decision = await this.provider.inspect({
|
|
300
529
|
label,
|
|
@@ -308,7 +537,75 @@ export class TeamOverseer {
|
|
|
308
537
|
finally {
|
|
309
538
|
this.inFlight.delete(label);
|
|
310
539
|
}
|
|
311
|
-
this.
|
|
540
|
+
this.spendLook(label);
|
|
541
|
+
this.remember(label, now, decision);
|
|
542
|
+
return decision;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* ONE batch inspection over every requested child (the cadence path, D-3):
|
|
546
|
+
* a single fast-tier call, a JSON array back, one decision per child.
|
|
547
|
+
*
|
|
548
|
+
* SKIPS FIRST, CALLS SECOND (R-P1-4): a label whose single inspection is
|
|
549
|
+
* still in flight, or whose look budget is spent, simply gets NO entry -
|
|
550
|
+
* no decision, no look charged, ladder untouched by the caller. A label
|
|
551
|
+
* that IS carried but whose reply entry is missing or garbage gets `wait`
|
|
552
|
+
* (repair, never reject). A wholly unparseable or truncated reply is ALL
|
|
553
|
+
* wait with zero action side effects (R-P0-1 / AC-13).
|
|
554
|
+
*/
|
|
555
|
+
async inspectBatch(reqs) {
|
|
556
|
+
const eligible = reqs.filter((req) => !this.inFlight.has(req.run.label) && !this.looksExhausted(req.run.label));
|
|
557
|
+
if (eligible.length === 0 || !this.isActive())
|
|
558
|
+
return new Map();
|
|
559
|
+
const now = this.options.now?.() ?? Date.now();
|
|
560
|
+
const inputs = eligible.map((req) => ({
|
|
561
|
+
run: req.run,
|
|
562
|
+
childAlive: req.childAlive,
|
|
563
|
+
trigger: 'clock',
|
|
564
|
+
messages: req.messages,
|
|
565
|
+
memory: this.memory.get(req.run.label) ?? [],
|
|
566
|
+
elapsedSec: Math.max(0, (now - (req.run.startedAt ?? now)) / 1000),
|
|
567
|
+
}));
|
|
568
|
+
const sections = buildBatchSections(inputs);
|
|
569
|
+
for (const req of eligible)
|
|
570
|
+
this.inFlight.add(req.run.label);
|
|
571
|
+
this.callCount += 1;
|
|
572
|
+
let raw;
|
|
573
|
+
try {
|
|
574
|
+
raw = await this.provider.inspectBatch(sections);
|
|
575
|
+
}
|
|
576
|
+
catch {
|
|
577
|
+
raw = [];
|
|
578
|
+
}
|
|
579
|
+
finally {
|
|
580
|
+
for (const req of eligible)
|
|
581
|
+
this.inFlight.delete(req.run.label);
|
|
582
|
+
}
|
|
583
|
+
const entries = typeof raw === 'string' ? parseOverseerBatchText(raw) : Array.isArray(raw) ? raw : [];
|
|
584
|
+
const byLabel = new Map();
|
|
585
|
+
for (const entry of entries) {
|
|
586
|
+
const label = entry?.label;
|
|
587
|
+
if (typeof label === 'string' && label.length > 0 && !byLabel.has(label)) {
|
|
588
|
+
byLabel.set(label, entry);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
const decisions = new Map();
|
|
592
|
+
for (const input of inputs) {
|
|
593
|
+
const label = input.run.label;
|
|
594
|
+
const decision = normalizeWithLimits(byLabel.get(label), input.childAlive, {
|
|
595
|
+
reasonChars: TEAM_LIMITS.overseerBatchReasonChars,
|
|
596
|
+
guidanceChars: TEAM_LIMITS.overseerBatchGuidanceChars,
|
|
597
|
+
});
|
|
598
|
+
this.spendLook(label);
|
|
599
|
+
this.remember(label, now, decision);
|
|
600
|
+
decisions.set(label, decision);
|
|
601
|
+
}
|
|
602
|
+
return decisions;
|
|
603
|
+
}
|
|
604
|
+
spendLook(label) {
|
|
605
|
+
this.looks.set(label, (this.looks.get(label) ?? 0) + 1);
|
|
606
|
+
this.looksTotal += 1;
|
|
607
|
+
}
|
|
608
|
+
remember(label, now, decision) {
|
|
312
609
|
const entries = this.memory.get(label) ?? [];
|
|
313
610
|
entries.push({
|
|
314
611
|
atSec: Math.max(0, Math.round((now - this.options.startedAt) / 1000)),
|
|
@@ -316,7 +613,6 @@ export class TeamOverseer {
|
|
|
316
613
|
reason: decision.reason,
|
|
317
614
|
});
|
|
318
615
|
this.memory.set(label, entries);
|
|
319
|
-
return decision;
|
|
320
616
|
}
|
|
321
617
|
}
|
|
322
618
|
// ---------------------------------------------------------------------------
|
package/dist/team/report.d.ts
CHANGED
|
@@ -28,6 +28,15 @@ export interface ReportOptions {
|
|
|
28
28
|
* THE ONE TABLE, because every child runs the lead's own model
|
|
29
29
|
* (main-agent parity): one tier, one price, one honest total. */
|
|
30
30
|
cost?: ModelCost;
|
|
31
|
+
/**
|
|
32
|
+
* The FAST tier's cost table, so the supervisor's own spend line can
|
|
33
|
+
* state real spend (subagent-overseer-v2 D-7 / R-P1-2). Supplied by
|
|
34
|
+
* `TaskToolDeps.fastModelCost()`; ABSENT when the fast model is not
|
|
35
|
+
* priced, in which case the spend line says `pricing unknown` rather
|
|
36
|
+
* than `$0.00` - unknown pricing is not zero pricing (the C-11 / RV-4
|
|
37
|
+
* rule the fast tier already follows everywhere else).
|
|
38
|
+
*/
|
|
39
|
+
fastCost?: ModelCost;
|
|
31
40
|
}
|
|
32
41
|
/**
|
|
33
42
|
* Cut `text` to at most `maxBytes` WITHOUT splitting a UTF-8 sequence.
|
package/dist/team/report.js
CHANGED
|
@@ -21,7 +21,9 @@
|
|
|
21
21
|
* because of one HTTP 429.
|
|
22
22
|
*/
|
|
23
23
|
import { computeCost, formatCost, formatDuration, formatTokens } from '../agent/usage.js';
|
|
24
|
-
import { TEAM_LIMITS } from './limits.js';
|
|
24
|
+
import { TEAM_AGGREGATE_LABEL, TEAM_LIMITS } from './limits.js';
|
|
25
|
+
/** The pseudo-label aggregate notices use; reserved at normalization. */
|
|
26
|
+
const SUPERVISOR_TEAM_LABEL = TEAM_AGGREGATE_LABEL;
|
|
25
27
|
function byteLength(text) {
|
|
26
28
|
return Buffer.byteLength(text, 'utf8');
|
|
27
29
|
}
|
|
@@ -147,16 +149,42 @@ function buildSection(run) {
|
|
|
147
149
|
* so the lead can see who was nudged or replaced, when, and why. Budgeted by
|
|
148
150
|
* the same `reportMaxBytes` ladder as everything else - it renders as a block,
|
|
149
151
|
* so trimming it means trimming the blocks, which the ladder already does.
|
|
152
|
+
*
|
|
153
|
+
* subagent-overseer-v2 appends three aggregates, each AT MOST ONCE (D-7 /
|
|
154
|
+
* D-10): the fast-tier spend line, a degraded-cadence note and a
|
|
155
|
+
* went-quiet note. Aggregates, never per-tick rows: a degraded dispatch
|
|
156
|
+
* ticks for hours and the report's job is the fact, not the log.
|
|
150
157
|
*/
|
|
151
|
-
function buildOverseer(outcome) {
|
|
158
|
+
function buildOverseer(outcome, options) {
|
|
152
159
|
const list = outcome.interventions ?? [];
|
|
153
|
-
|
|
160
|
+
const calls = outcome.overseerCalls ?? 0;
|
|
161
|
+
const degraded = outcome.overseerDegraded === true;
|
|
162
|
+
// The pseudo-label aggregate (`team`) is a note, not a child's ledger row.
|
|
163
|
+
const quietChildren = list.filter((item) => item.trigger === 'quiet' && item.label !== SUPERVISOR_TEAM_LABEL).length;
|
|
164
|
+
if (list.length === 0 && calls === 0 && !degraded)
|
|
154
165
|
return [];
|
|
155
166
|
const lines = ['### Supervisor'];
|
|
156
167
|
for (const item of list) {
|
|
157
168
|
const at = Math.max(0, Math.round((item.at - outcome.startedAt) / 1000));
|
|
158
169
|
lines.push(`[${item.trigger} ${at}s] ${item.label}: ${item.action} - ${item.reason}`);
|
|
159
170
|
}
|
|
171
|
+
// Honest accounting (D-7): rendered only when calls were actually made,
|
|
172
|
+
// priced at the FAST table, `pricing unknown` when that table is absent.
|
|
173
|
+
if (calls > 0 && outcome.overseerUsage !== undefined) {
|
|
174
|
+
const price = options.fastCost
|
|
175
|
+
? formatCost(computeCost(outcome.overseerUsage, options.fastCost))
|
|
176
|
+
: 'pricing unknown';
|
|
177
|
+
lines.push(`supervisor: ${calls} call${calls === 1 ? '' : 's'}, ${formatTokens(outcome.overseerUsage.inputTokens)} in / ${formatTokens(outcome.overseerUsage.outputTokens)} out tokens` +
|
|
178
|
+
` (fast tier), ${price}.`);
|
|
179
|
+
}
|
|
180
|
+
if (degraded) {
|
|
181
|
+
lines.push(`note: the fast tier was unavailable for ${outcome.overseerDegradedTicks ?? 1}` +
|
|
182
|
+
` cadence check(s); children waited unassisted and none were killed.`);
|
|
183
|
+
}
|
|
184
|
+
if (quietChildren > 0) {
|
|
185
|
+
lines.push(`note: supervision went quiet on ${quietChildren}` +
|
|
186
|
+
` child${quietChildren === 1 ? '' : 'ren'} after its look budget.`);
|
|
187
|
+
}
|
|
160
188
|
return lines;
|
|
161
189
|
}
|
|
162
190
|
function buildLeadMail(outcome) {
|
|
@@ -184,7 +212,7 @@ export function buildDispatchReport(outcome, options = {}) {
|
|
|
184
212
|
const header = buildHeader(outcome, options);
|
|
185
213
|
const sections = outcome.runs.map(buildSection);
|
|
186
214
|
const mail = buildLeadMail(outcome);
|
|
187
|
-
const overseer = buildOverseer(outcome);
|
|
215
|
+
const overseer = buildOverseer(outcome, options);
|
|
188
216
|
const summaries = sections.map((s) => truncateBytes(s.summary, TEAM_LIMITS.summaryMaxBytes));
|
|
189
217
|
const trimmed = summaries.map((s, i) => s.length < sections[i].summary.length);
|
|
190
218
|
const render = (bodies) => {
|