@aragon-agent/cli 0.6.17 → 0.6.19
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 +121 -51
- package/README.md +48 -2
- package/dist/agent/controller.js +13 -0
- package/dist/agent/headless.js +7 -0
- package/dist/commands/builtins.js +18 -3
- package/dist/commands/index-build.d.ts +39 -0
- package/dist/commands/index-build.js +61 -0
- package/dist/config/cli-commands.d.ts +1 -1
- package/dist/config/cli-commands.js +3 -0
- package/dist/config/schema.d.ts +24 -4
- package/dist/config/schema.js +4 -0
- package/dist/exec/events.d.ts +4 -1
- package/dist/exec/runner.js +13 -0
- 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 +19 -1
- package/dist/logging/install.d.ts +9 -0
- package/dist/logging/install.js +11 -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/overseer.d.ts +157 -0
- package/dist/team/overseer.js +348 -0
- package/dist/team/report.js +26 -1
- package/dist/team/runtime.d.ts +71 -6
- package/dist/team/runtime.js +380 -23
- package/dist/team/subagent.d.ts +30 -1
- package/dist/team/subagent.js +8 -1
- package/dist/team/task-tool.js +4 -1
- package/dist/team/types.d.ts +63 -0
- package/dist/ui/App.d.ts +1 -1
- package/dist/ui/App.js +61 -0
- package/dist/ui/Header.js +13 -2
- package/dist/ui/PromptInput.js +11 -1
- package/dist/ui/overlays/HelpOverlay.js +1 -0
- 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
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dispatch supervisor (team-overseer).
|
|
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
|
+
* A TIME CEILING ON A CHILD IS AN INSPECTION TRIGGER, NOT A DEATH SENTENCE.
|
|
8
|
+
* When a child stalls (event silence) or simply runs long (the wall clock),
|
|
9
|
+
* the supervisor looks at it through ONE bounded fast-tier call and decides:
|
|
10
|
+
* keep waiting, steer a nudge in to help it recover, rebuild it with an
|
|
11
|
+
* amended brief, or abandon it with a recorded reason. Only the supervisor
|
|
12
|
+
* "decides"; the RUNTIME applies (see `runtime.ts`), because every child
|
|
13
|
+
* lifecycle change must funnel through the worker loop that awaits it
|
|
14
|
+
* (I-OV1).
|
|
15
|
+
*
|
|
16
|
+
* THE TRANSPORT IS THE REVIEWER'S, DELIBERATELY. One `complete` call, no
|
|
17
|
+
* tools, thinking off, temperature 0, output capped at
|
|
18
|
+
* `FAST_LIMITS.reviewOutputTokens` and wall clock at
|
|
19
|
+
* `FAST_LIMITS.reviewTimeoutMs` — a supervision call that could itself hang
|
|
20
|
+
* would need a second supervisor, which is the regress this design refuses.
|
|
21
|
+
*
|
|
22
|
+
* REPAIR, NEVER REJECT (the `normalizeSubagentSpecs` rule). An unparseable
|
|
23
|
+
* or out-of-budget decision becomes `wait` with the default interval: the
|
|
24
|
+
* failure mode of a supervisor must be "looked and let it run", never
|
|
25
|
+
* "looked and killed it" (I-OV2).
|
|
26
|
+
*/
|
|
27
|
+
import { resolveFastTier } from '../fast/resolve.js';
|
|
28
|
+
import { assistantText } from '../fast/review-call.js';
|
|
29
|
+
import { FAST_LIMITS } from '../fast/limits.js';
|
|
30
|
+
import { truncateBytes } from './report.js';
|
|
31
|
+
import { TEAM_LIMITS } from './limits.js';
|
|
32
|
+
/** How many trailing history messages the digest carries. */
|
|
33
|
+
const DIGEST_TAIL_MESSAGES = 8;
|
|
34
|
+
/** Per-message text clamp inside the digest. */
|
|
35
|
+
const DIGEST_MSG_CHARS = 400;
|
|
36
|
+
function messageText(message) {
|
|
37
|
+
if (message.role === 'assistant') {
|
|
38
|
+
const parts = [];
|
|
39
|
+
for (const block of message.content) {
|
|
40
|
+
if (block.type === 'text')
|
|
41
|
+
parts.push(block.text);
|
|
42
|
+
if (block.type === 'tool_call')
|
|
43
|
+
parts.push(`(tool_call ${block.toolName})`);
|
|
44
|
+
}
|
|
45
|
+
return parts.join(' ').trim();
|
|
46
|
+
}
|
|
47
|
+
if (typeof message.content === 'string')
|
|
48
|
+
return message.content;
|
|
49
|
+
return message.content
|
|
50
|
+
.filter((p) => p.type === 'text')
|
|
51
|
+
.map((p) => (p.type === 'text' ? p.text : ''))
|
|
52
|
+
.join(' ')
|
|
53
|
+
.trim();
|
|
54
|
+
}
|
|
55
|
+
function clampChars(text, max) {
|
|
56
|
+
return text.length <= max ? text : `${text.slice(0, Math.max(0, max - 3))}...`;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Build the bounded text one inspection reads.
|
|
60
|
+
*
|
|
61
|
+
* FACTS FIRST, HISTORY SECOND: the byte clamp (`overseerDigestBytes`, via the
|
|
62
|
+
* same `truncateBytes` the report uses so a CJK tail cannot be split mid
|
|
63
|
+
* sequence) bites the tail, and the tail is the least decisive part — a
|
|
64
|
+
* supervisor that cannot see the phase, the retry state and its own memory
|
|
65
|
+
* is worse than one missing the fifth message back.
|
|
66
|
+
*/
|
|
67
|
+
export function buildChildDigest(input) {
|
|
68
|
+
const { run, childAlive, trigger, messages, memory, elapsedSec } = input;
|
|
69
|
+
const lines = [];
|
|
70
|
+
lines.push(`child: ${run.label} "${run.description}"`);
|
|
71
|
+
lines.push(`trigger: ${trigger === 'silence' ? 'event silence (suspected stall)' : trigger === 'clock' ? 'wall-clock check' : 'post-mortem (child already ended)'}`);
|
|
72
|
+
lines.push(`elapsed: ${Math.round(elapsedSec)}s, alive: ${childAlive ? 'yes' : 'no'}`);
|
|
73
|
+
lines.push(`phase: ${run.phase}, turns: ${run.turns}, toolCalls: ${run.toolCalls}` +
|
|
74
|
+
(run.lastTool ? `, lastTool: ${run.lastTool}` : ''));
|
|
75
|
+
if (run.retry)
|
|
76
|
+
lines.push(`api-retry: attempt ${run.retry.attempt}/${run.retry.maxRetries} (legitimate backoff, not a stall)`);
|
|
77
|
+
if (run.error)
|
|
78
|
+
lines.push(`error: ${clampChars(run.error, DIGEST_MSG_CHARS)}`);
|
|
79
|
+
if (run.filesTouched.length > 0)
|
|
80
|
+
lines.push(`files: ${run.filesTouched.join(', ')}`);
|
|
81
|
+
if (memory.length > 0) {
|
|
82
|
+
lines.push('previous supervisor decisions:');
|
|
83
|
+
for (const entry of memory) {
|
|
84
|
+
lines.push(` [${entry.atSec}s] ${entry.action}: ${clampChars(entry.reason, 120)}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const tail = messages.slice(-DIGEST_TAIL_MESSAGES);
|
|
88
|
+
if (tail.length > 0) {
|
|
89
|
+
lines.push('history tail:');
|
|
90
|
+
for (const message of tail) {
|
|
91
|
+
const text = clampChars(messageText(message), DIGEST_MSG_CHARS);
|
|
92
|
+
lines.push(` ${message.role}: ${text.length > 0 ? text : '(no text)'}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return truncateBytes(lines.join('\n'), TEAM_LIMITS.overseerDigestBytes);
|
|
96
|
+
}
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Decision normalization
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
const VALID_ACTIONS = new Set(['wait', 'nudge', 'replace', 'abandon']);
|
|
101
|
+
/** Clamp a model-chosen next check into the structural range. */
|
|
102
|
+
export function clampNextCheckMs(ms) {
|
|
103
|
+
const n = typeof ms === 'number' ? ms : Number.NaN;
|
|
104
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
105
|
+
return undefined;
|
|
106
|
+
return Math.min(TEAM_LIMITS.overseerNextCheckMaxMs, Math.max(TEAM_LIMITS.overseerNextCheckMinMs, Math.round(n)));
|
|
107
|
+
}
|
|
108
|
+
function waitDecision(reason, nextCheckMs) {
|
|
109
|
+
return {
|
|
110
|
+
action: 'wait',
|
|
111
|
+
reason: clampChars(reason, TEAM_LIMITS.overseerReasonChars),
|
|
112
|
+
...(nextCheckMs !== undefined ? { nextCheckMs } : {}),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Normalize a raw (JSON-parsed or garbage) decision into a safe one.
|
|
117
|
+
*
|
|
118
|
+
* `childAlive === false` PHYSICALLY REMOVES two actions: a dead child can be
|
|
119
|
+
* replaced or abandoned, never waited on or nudged, so those normalize to
|
|
120
|
+
* `abandon` rather than being rejected — a post-mortem answer the protocol
|
|
121
|
+
* cannot honour must still produce a decision the runtime can apply.
|
|
122
|
+
*/
|
|
123
|
+
export function normalizeOverseerDecision(raw, childAlive) {
|
|
124
|
+
const obj = (raw && typeof raw === 'object' ? raw : {});
|
|
125
|
+
const action = typeof obj.action === 'string' && VALID_ACTIONS.has(obj.action)
|
|
126
|
+
? obj.action
|
|
127
|
+
: 'wait';
|
|
128
|
+
const reason = typeof obj.reason === 'string' && obj.reason.trim().length > 0
|
|
129
|
+
? obj.reason.trim()
|
|
130
|
+
: 'no reason given';
|
|
131
|
+
const guidance = typeof obj.guidance === 'string' && obj.guidance.trim().length > 0
|
|
132
|
+
? clampChars(obj.guidance.trim(), TEAM_LIMITS.overseerGuidanceChars)
|
|
133
|
+
: undefined;
|
|
134
|
+
const nextCheckMs = clampNextCheckMs(obj.nextCheckMs);
|
|
135
|
+
if (!childAlive && (action === 'wait' || action === 'nudge')) {
|
|
136
|
+
return {
|
|
137
|
+
action: 'abandon',
|
|
138
|
+
reason: clampChars(`child already ended; supervisor answered ${action}: ${reason}`, TEAM_LIMITS.overseerReasonChars),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
action,
|
|
143
|
+
reason: clampChars(reason, TEAM_LIMITS.overseerReasonChars),
|
|
144
|
+
...(guidance !== undefined && action === 'nudge' ? { guidance } : {}),
|
|
145
|
+
...(action === 'wait' && nextCheckMs !== undefined ? { nextCheckMs } : {}),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Parse the model's answer text as one JSON decision object.
|
|
150
|
+
*
|
|
151
|
+
* Forgiving on PURPOSE: models wrap JSON in prose and fences. The FIRST `{`
|
|
152
|
+
* to the LAST `}` is the only span tried, and anything that does not parse is
|
|
153
|
+
* `wait` — the caller never sees a throw from parsing.
|
|
154
|
+
*/
|
|
155
|
+
export function parseOverseerDecisionText(text, childAlive) {
|
|
156
|
+
const first = text.indexOf('{');
|
|
157
|
+
const last = text.lastIndexOf('}');
|
|
158
|
+
if (first >= 0 && last > first) {
|
|
159
|
+
try {
|
|
160
|
+
return normalizeOverseerDecision(JSON.parse(text.slice(first, last + 1)), childAlive);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
// Fall through to the safe default.
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return waitDecision('decision unparseable; defaulting to wait');
|
|
167
|
+
}
|
|
168
|
+
const OVERSEER_SYSTEM_PROMPT = [
|
|
169
|
+
'You are the supervisor of a team of subagents. You are shown ONE subagent',
|
|
170
|
+
'that crossed a time trigger (event silence or a wall-clock check) and must',
|
|
171
|
+
'decide what to do with it. Answer with ONE JSON object and nothing else:',
|
|
172
|
+
'{"action": "wait" | "nudge" | "replace" | "abandon", "reason": "<= 200 chars",',
|
|
173
|
+
' "guidance": "<= 1200 chars, nudge only", "nextCheckMs": <ms, wait only>}.',
|
|
174
|
+
'wait: the child looks healthy or is legitimately blocked (API backoff,',
|
|
175
|
+
' waiting on a teammate or a human confirmation) - keep waiting, pick the',
|
|
176
|
+
' next check delay.',
|
|
177
|
+
'nudge: the child is stuck in a way a hint can fix (wrong path, retrying a',
|
|
178
|
+
' failing command, circling) - write the hint as guidance.',
|
|
179
|
+
'replace: the child cannot recover - a fresh child will be built with your',
|
|
180
|
+
' reason and guidance prepended to its brief.',
|
|
181
|
+
'abandon: the task is not worth more attempts - the child is stopped and',
|
|
182
|
+
' your reason is reported to the lead.',
|
|
183
|
+
'A nudge is only delivered after the child finishes its current tool call;',
|
|
184
|
+
'a long-running tool is not a stall. Prefer the cheapest action that works.',
|
|
185
|
+
].join('\n');
|
|
186
|
+
function buildInspectionRequest(params) {
|
|
187
|
+
return {
|
|
188
|
+
model: params.modelId,
|
|
189
|
+
...(params.baseUrl ? { baseUrl: params.baseUrl } : {}),
|
|
190
|
+
apiKey: params.apiKey,
|
|
191
|
+
systemPrompt: OVERSEER_SYSTEM_PROMPT,
|
|
192
|
+
messages: [{ role: 'user', content: params.digest, timestamp: Date.now() }],
|
|
193
|
+
maxTokens: FAST_LIMITS.reviewOutputTokens,
|
|
194
|
+
thinkingLevel: 'off',
|
|
195
|
+
temperature: 0,
|
|
196
|
+
signal: params.signal,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* The fast-tier-backed provider, mirroring `createChildReviewer`'s shape so
|
|
201
|
+
* the controller wires it the same way: shared transport, live predicates,
|
|
202
|
+
* fail-soft (any failure resolves to `wait` — I-OV2).
|
|
203
|
+
*/
|
|
204
|
+
export function createFastOverseerProvider(deps) {
|
|
205
|
+
const tier = () => resolveFastTier(deps.getConfig(), deps.hasKey);
|
|
206
|
+
return {
|
|
207
|
+
active: () => deps.available() && tier().ok,
|
|
208
|
+
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
|
+
try {
|
|
220
|
+
const message = await deps.complete(resolved.ref.providerId, buildInspectionRequest({
|
|
221
|
+
digest: req.digest,
|
|
222
|
+
modelId: resolved.ref.modelId,
|
|
223
|
+
...(resolved.ref.baseUrl ? { baseUrl: resolved.ref.baseUrl } : {}),
|
|
224
|
+
apiKey,
|
|
225
|
+
signal: controller.signal,
|
|
226
|
+
}));
|
|
227
|
+
return parseOverseerDecisionText(assistantText(message), true);
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
// INCLUDING a timeout abort: supervision failing is wait, never kill.
|
|
231
|
+
return waitDecision('inspection call failed; defaulting to wait');
|
|
232
|
+
}
|
|
233
|
+
finally {
|
|
234
|
+
clearTimeout(timer);
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
export const OVERSEER_DEFAULT_NEXT_CHECK_MS = TEAM_LIMITS.overseerDefaultCheckMs;
|
|
240
|
+
/**
|
|
241
|
+
* Per-dispatch supervisor state: memory, budgets and the single-flight guard.
|
|
242
|
+
*
|
|
243
|
+
* THE CLASS NEVER TOUCHES A CHILD (I-OV1). It looks and decides; `runtime.ts`
|
|
244
|
+
* steers, flags, aborts and rebuilds, because only the worker loop can apply
|
|
245
|
+
* a lifecycle change without orphaning the replacement.
|
|
246
|
+
*/
|
|
247
|
+
export class TeamOverseer {
|
|
248
|
+
provider;
|
|
249
|
+
options;
|
|
250
|
+
memory = new Map();
|
|
251
|
+
inspections = new Map();
|
|
252
|
+
replacements = new Map();
|
|
253
|
+
inFlight = new Set();
|
|
254
|
+
constructor(provider, options) {
|
|
255
|
+
this.provider = provider;
|
|
256
|
+
this.options = options;
|
|
257
|
+
}
|
|
258
|
+
/** Never throws: a provider bug must not take the dispatch down. */
|
|
259
|
+
isActive() {
|
|
260
|
+
try {
|
|
261
|
+
return this.provider.active();
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
inspectionsExhausted(label) {
|
|
268
|
+
return (this.inspections.get(label) ?? 0) >= TEAM_LIMITS.overseerMaxInspectionsPerChild;
|
|
269
|
+
}
|
|
270
|
+
replacementsLeft(label) {
|
|
271
|
+
return (this.replacements.get(label) ?? 0) < TEAM_LIMITS.overseerMaxReplacementsPerChild;
|
|
272
|
+
}
|
|
273
|
+
/** Called by the runtime when it APPLIES a replace, not when it merely reads one. */
|
|
274
|
+
noteReplacement(label) {
|
|
275
|
+
this.replacements.set(label, (this.replacements.get(label) ?? 0) + 1);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* ONE inspection. `null` means QUIET — not active, no budget left, or one
|
|
279
|
+
* already in flight for this child — and the caller then does nothing,
|
|
280
|
+
* which is exactly what quiet should do.
|
|
281
|
+
*/
|
|
282
|
+
async inspect(req) {
|
|
283
|
+
const label = req.run.label;
|
|
284
|
+
if (!this.isActive() || this.inspectionsExhausted(label) || this.inFlight.has(label)) {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
this.inFlight.add(label);
|
|
288
|
+
const now = this.options.now?.() ?? Date.now();
|
|
289
|
+
const digest = buildChildDigest({
|
|
290
|
+
run: req.run,
|
|
291
|
+
childAlive: req.childAlive,
|
|
292
|
+
trigger: req.trigger,
|
|
293
|
+
messages: req.messages,
|
|
294
|
+
memory: this.memory.get(label) ?? [],
|
|
295
|
+
elapsedSec: Math.max(0, (now - (req.run.startedAt ?? now)) / 1000),
|
|
296
|
+
});
|
|
297
|
+
let decision;
|
|
298
|
+
try {
|
|
299
|
+
decision = await this.provider.inspect({
|
|
300
|
+
label,
|
|
301
|
+
goal: req.run.description,
|
|
302
|
+
digest,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
decision = waitDecision('inspection threw; defaulting to wait');
|
|
307
|
+
}
|
|
308
|
+
finally {
|
|
309
|
+
this.inFlight.delete(label);
|
|
310
|
+
}
|
|
311
|
+
this.inspections.set(label, (this.inspections.get(label) ?? 0) + 1);
|
|
312
|
+
const entries = this.memory.get(label) ?? [];
|
|
313
|
+
entries.push({
|
|
314
|
+
atSec: Math.max(0, Math.round((now - this.options.startedAt) / 1000)),
|
|
315
|
+
action: decision.action,
|
|
316
|
+
reason: decision.reason,
|
|
317
|
+
});
|
|
318
|
+
this.memory.set(label, entries);
|
|
319
|
+
return decision;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// ---------------------------------------------------------------------------
|
|
323
|
+
// Replacement brief
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
/** Headroom the amended brief keeps over the original prompt's clamp. */
|
|
326
|
+
const REPLACEMENT_NOTE_CHARS = 800;
|
|
327
|
+
/**
|
|
328
|
+
* Prepend the supervisor's note to a spec's prompt, bounding the TOTAL to
|
|
329
|
+
* `TEAM_LIMITS.promptChars` so the replacement fits exactly what a
|
|
330
|
+
* model-supplied spec would have been allowed to carry.
|
|
331
|
+
*
|
|
332
|
+
* THE NOTE TELLS THE FRESH CHILD WHAT THE DEAD ONE DID (`filesTouched` is
|
|
333
|
+
* best-effort, the same caveat the report's conflict warning carries): a
|
|
334
|
+
* replacement that rewrites a half-written file blind is not a recovery.
|
|
335
|
+
*/
|
|
336
|
+
export function buildReplacementPrompt(spec, decision, previousRun) {
|
|
337
|
+
const files = previousRun.filesTouched.length > 0
|
|
338
|
+
? ` It already wrote to: ${previousRun.filesTouched.join(', ')}.`
|
|
339
|
+
: '';
|
|
340
|
+
const guidance = decision.action === 'replace' && decision.guidance !== undefined
|
|
341
|
+
? ` Guidance: ${decision.guidance}`
|
|
342
|
+
: '';
|
|
343
|
+
const note = clampChars(`[supervisor note] A previous attempt at this task ran ${previousRun.turns} turns ` +
|
|
344
|
+
`and was replaced. Reason: ${decision.reason}.${files}${guidance} ` +
|
|
345
|
+
`Verify rather than assume prior work is complete.\n\n`, REPLACEMENT_NOTE_CHARS);
|
|
346
|
+
const originalBudget = Math.max(0, TEAM_LIMITS.promptChars - note.length);
|
|
347
|
+
return `${note}${spec.prompt.slice(0, originalBudget)}`;
|
|
348
|
+
}
|
package/dist/team/report.js
CHANGED
|
@@ -117,6 +117,11 @@ function buildSection(run) {
|
|
|
117
117
|
// the only place the lead learns that a child's first attempt died before it
|
|
118
118
|
// did anything (F-4).
|
|
119
119
|
const retried = run.retries ? `, retried ${run.retries}x` : '';
|
|
120
|
+
// Same head-line position as `retried Nx`, for the same reason: the ONLY
|
|
121
|
+
// place the lead learns that a supervisor looked at this child (N times) or
|
|
122
|
+
// rebuilt it (team-overseer §3.7).
|
|
123
|
+
const overseen = run.interventions ? `, overseer ${run.interventions}` : '';
|
|
124
|
+
const replaced = run.replacements ? `, replaced ${run.replacements}x` : '';
|
|
120
125
|
// `compacted N` in the same position `retried Nx` uses, and for the same
|
|
121
126
|
// reason (context-auto-compaction-hardening §3.4.4 / W3): this is the ONLY
|
|
122
127
|
// place the lead learns that a child's own history was summarized mid-task, so
|
|
@@ -125,7 +130,7 @@ function buildSection(run) {
|
|
|
125
130
|
const compacted = run.compactions ? `, compacted ${run.compactions}` : '';
|
|
126
131
|
const head = [
|
|
127
132
|
`### ${run.label} "${run.description}" ${statusBadge(run)} ${elapsed(run)}, ` +
|
|
128
|
-
`${run.turns} turns, ${run.toolCalls} tools${retried}${compacted}`,
|
|
133
|
+
`${run.turns} turns, ${run.toolCalls} tools${retried}${compacted}${overseen}${replaced}`,
|
|
129
134
|
];
|
|
130
135
|
if (run.filesTouched.length > 0)
|
|
131
136
|
head.push(`files: ${run.filesTouched.join(', ')}`);
|
|
@@ -137,6 +142,23 @@ function buildSection(run) {
|
|
|
137
142
|
const summary = (run.summary ?? '').trim();
|
|
138
143
|
return { head, summary: summary.length > 0 ? summary : '(no summary)' };
|
|
139
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* The supervisor's ledger (team-overseer §3.7): one line per APPLIED decision,
|
|
147
|
+
* so the lead can see who was nudged or replaced, when, and why. Budgeted by
|
|
148
|
+
* the same `reportMaxBytes` ladder as everything else - it renders as a block,
|
|
149
|
+
* so trimming it means trimming the blocks, which the ladder already does.
|
|
150
|
+
*/
|
|
151
|
+
function buildOverseer(outcome) {
|
|
152
|
+
const list = outcome.interventions ?? [];
|
|
153
|
+
if (list.length === 0)
|
|
154
|
+
return [];
|
|
155
|
+
const lines = ['### Supervisor'];
|
|
156
|
+
for (const item of list) {
|
|
157
|
+
const at = Math.max(0, Math.round((item.at - outcome.startedAt) / 1000));
|
|
158
|
+
lines.push(`[${item.trigger} ${at}s] ${item.label}: ${item.action} - ${item.reason}`);
|
|
159
|
+
}
|
|
160
|
+
return lines;
|
|
161
|
+
}
|
|
140
162
|
function buildLeadMail(outcome) {
|
|
141
163
|
if (outcome.leadMail.length === 0)
|
|
142
164
|
return [];
|
|
@@ -162,6 +184,7 @@ export function buildDispatchReport(outcome, options = {}) {
|
|
|
162
184
|
const header = buildHeader(outcome, options);
|
|
163
185
|
const sections = outcome.runs.map(buildSection);
|
|
164
186
|
const mail = buildLeadMail(outcome);
|
|
187
|
+
const overseer = buildOverseer(outcome);
|
|
165
188
|
const summaries = sections.map((s) => truncateBytes(s.summary, TEAM_LIMITS.summaryMaxBytes));
|
|
166
189
|
const trimmed = summaries.map((s, i) => s.length < sections[i].summary.length);
|
|
167
190
|
const render = (bodies) => {
|
|
@@ -174,6 +197,8 @@ export function buildDispatchReport(outcome, options = {}) {
|
|
|
174
197
|
}
|
|
175
198
|
blocks.push(lines.join('\n'));
|
|
176
199
|
});
|
|
200
|
+
if (overseer.length > 0)
|
|
201
|
+
blocks.push(overseer.join('\n'));
|
|
177
202
|
if (mail.length > 0)
|
|
178
203
|
blocks.push(mail.join('\n'));
|
|
179
204
|
return blocks.join('\n\n');
|
package/dist/team/runtime.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TeamRuntime — dispatch scheduling, budgets, the `TeamEvent` stream, abort and
|
|
3
|
-
* disposal (team-subagents §3.2 / §3.5 / §3.8).
|
|
3
|
+
* disposal (team-subagents §3.2 / §3.5 / §3.8; team-overseer §3.4).
|
|
4
4
|
*
|
|
5
5
|
* ASCII ONLY: `src/team/**` is inside the glyph scanner's scope.
|
|
6
6
|
*
|
|
@@ -16,6 +16,16 @@
|
|
|
16
16
|
* worker loops, awaited with `Promise.allSettled`. No worker abstraction, no
|
|
17
17
|
* queue class, no event-loop trickery — every path is observable from the event
|
|
18
18
|
* stream.
|
|
19
|
+
*
|
|
20
|
+
* THE DISPATCH SUPERVISOR (team-overseer) — a time ceiling is an inspection
|
|
21
|
+
* trigger, not a death sentence. When a supervisor is live for the dispatch,
|
|
22
|
+
* two SOFT timers per child (wall clock and event silence) fire an inspection;
|
|
23
|
+
* the supervisor decides wait / nudge / replace / abandon; THIS module applies.
|
|
24
|
+
* Lifecycle changes are funnelled exclusively through `runOne`, the only place
|
|
25
|
+
* a child's `prompt()` is awaited (I-OV1): the supervisor may steer, flag and
|
|
26
|
+
* abort, but a replacement is built by the worker loop or it is built for
|
|
27
|
+
* nobody. Without a supervisor, every path below is byte for byte the
|
|
28
|
+
* pre-feature behaviour.
|
|
19
29
|
*/
|
|
20
30
|
import type { ModelRole } from '../config/model-profiles.js';
|
|
21
31
|
import type { ProviderRegistry, SkillRegistry, ToolPolicyDecision, ToolPolicyVerdict } from '@aragon-agent/core';
|
|
@@ -23,6 +33,7 @@ import type { AgentMode } from '../agent/agent-mode.js';
|
|
|
23
33
|
import type { CliConfig } from '../config/schema.js';
|
|
24
34
|
import type { ToolPermission } from '../exec/permission.js';
|
|
25
35
|
import type { TeamHumanQueue } from './human-queue.js';
|
|
36
|
+
import { type OverseerProvider } from './overseer.js';
|
|
26
37
|
import { type SubagentAgentFactory, type SubagentDeps } from './subagent.js';
|
|
27
38
|
import type { DispatchOutcome, SubagentSpec, TeamEventListener, TeamSnapshot } from './types.js';
|
|
28
39
|
export interface TeamRuntimeDeps {
|
|
@@ -67,10 +78,17 @@ export interface TeamRuntimeDeps {
|
|
|
67
78
|
* Passed straight through to `SubagentDeps`; absent means children get none,
|
|
68
79
|
* which is the pre-hardening behaviour exactly. `AgentController` supplies it
|
|
69
80
|
* from `CompactionWiring.childFactory()`, and the closure returns `undefined`
|
|
70
|
-
* when compaction is unregistered or `compaction.subagents` is false - which
|
|
71
|
-
* what lets `subagent.ts` spread nothing at all.
|
|
81
|
+
* when compaction is unregistered or `compaction.subagents` is false - which
|
|
82
|
+
* is what lets `subagent.ts` spread nothing at all.
|
|
72
83
|
*/
|
|
73
84
|
contextManagerFor?: SubagentDeps['contextManagerFor'];
|
|
85
|
+
/**
|
|
86
|
+
* The dispatch supervisor (team-overseer). ABSENT means the session has no
|
|
87
|
+
* supervision, and every path below is the pre-feature behaviour exactly.
|
|
88
|
+
* `active()` is read once per dispatch AND once per inspection (inside
|
|
89
|
+
* `TeamOverseer`), so a mid-dispatch `/fast off` stops the next look.
|
|
90
|
+
*/
|
|
91
|
+
overseer?: OverseerProvider;
|
|
74
92
|
}
|
|
75
93
|
export declare class TeamRuntime {
|
|
76
94
|
private readonly deps;
|
|
@@ -94,6 +112,16 @@ export declare class TeamRuntime {
|
|
|
94
112
|
* quietly not for the others.
|
|
95
113
|
*/
|
|
96
114
|
private readonly pendingBackoffs;
|
|
115
|
+
/**
|
|
116
|
+
* The dispatch supervisor, LIVE for the current dispatch only (team-overseer).
|
|
117
|
+
* Null when the provider is absent/disabled/off, and reset to null at every
|
|
118
|
+
* dispatch end so nothing supervision-shaped leaks across dispatches.
|
|
119
|
+
*/
|
|
120
|
+
private overseer;
|
|
121
|
+
/** The soft timers, same lifetime as `overseer`. */
|
|
122
|
+
private supervision;
|
|
123
|
+
/** Applied interventions for the in-flight dispatch's outcome. */
|
|
124
|
+
private interventions;
|
|
97
125
|
constructor(deps: TeamRuntimeDeps);
|
|
98
126
|
subscribe(listener: TeamEventListener): () => void;
|
|
99
127
|
/**
|
|
@@ -109,18 +137,55 @@ export declare class TeamRuntime {
|
|
|
109
137
|
private worker;
|
|
110
138
|
/**
|
|
111
139
|
* Run one child to a terminal phase, replacing it once if its very first
|
|
112
|
-
* request died on a transport error before it did anything at all (F-4)
|
|
140
|
+
* request died on a transport error before it did anything at all (F-4), and
|
|
141
|
+
* replacing it when the SUPERVISOR decides to (team-overseer §3.4).
|
|
142
|
+
*
|
|
143
|
+
* ONE LOOP, ALL LIFECYCLE: the cold-start replacement, the supervisor's
|
|
144
|
+
* replace and the post-mortem replace all swap `handles[index]` and continue
|
|
145
|
+
* THIS loop, because the await below is the only place a child is driven and
|
|
146
|
+
* a handle swapped anywhere else would never be (I-OV1).
|
|
113
147
|
*/
|
|
114
148
|
private runOne;
|
|
115
149
|
/**
|
|
116
|
-
*
|
|
150
|
+
* One post-mortem inspection, and possibly ONE replacement driven by a
|
|
151
|
+
* recursive `runOne` — the worker that called us has already moved on to the
|
|
152
|
+
* next index, so nobody else will drive the fresh child (I-OV1 holds here
|
|
153
|
+
* too, one frame deeper). Recursion depth is bounded by the replacement
|
|
154
|
+
* budget, which is 1.
|
|
155
|
+
*/
|
|
156
|
+
private maybePostMortem;
|
|
157
|
+
/**
|
|
158
|
+
* Swap one child for a fresh one, carrying only what must survive.
|
|
117
159
|
*
|
|
118
160
|
* `this.live` IS `handles`, so the single assignment below also makes
|
|
119
161
|
* `abortAll()` abort the NEW child and makes the outcome carry the new run.
|
|
120
162
|
* Anyone tempted to "clean up" that aliasing into a defensive copy must update
|
|
121
163
|
* both places.
|
|
164
|
+
*
|
|
165
|
+
* `verdict === null` is the COLD-START retry (F-4): identical spec, `retries`
|
|
166
|
+
* carried. A verdict is the SUPERVISOR's replace: an amended brief
|
|
167
|
+
* (`buildReplacementPrompt`), `replacements` carried, and the supervisor's
|
|
168
|
+
* budget spent HERE, at the moment the rebuild is real.
|
|
169
|
+
*/
|
|
170
|
+
private replaceChild;
|
|
171
|
+
/**
|
|
172
|
+
* Whether a child's silence is a legitimate wait rather than a stall:
|
|
173
|
+
* `team_wait` (the comm tool already pauses the child's own watchdog for
|
|
174
|
+
* exactly this wait) or a confirmation queued on the one human slot.
|
|
175
|
+
*/
|
|
176
|
+
private isBlocked;
|
|
177
|
+
/**
|
|
178
|
+
* One trigger handled: look, then apply. Fired from a supervision timer and
|
|
179
|
+
* therefore async-and-forgotten; nothing here may throw.
|
|
180
|
+
*
|
|
181
|
+
* QUIET (`decision === null` — provider gone, budget spent) CLEARS the
|
|
182
|
+
* child's timers: supervision that has nothing left to say should stop
|
|
183
|
+
* waking up, and the child's own hard ceilings take over.
|
|
122
184
|
*/
|
|
123
|
-
private
|
|
185
|
+
private inspectAndApply;
|
|
186
|
+
private applyDecision;
|
|
187
|
+
/** Record one applied decision on the outcome, the run and the event stream. */
|
|
188
|
+
private recordIntervention;
|
|
124
189
|
/**
|
|
125
190
|
* The final phase of an attempt, in a FIXED ORDER: aborted, then timed out,
|
|
126
191
|
* then error, then no-summary, then done.
|