@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/team/runtime.js
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,11 +16,104 @@
|
|
|
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 { TeamBus } from './bus.js';
|
|
21
31
|
import { TEAM_LIMITS } from './limits.js';
|
|
32
|
+
import { TeamOverseer, buildReplacementPrompt } from './overseer.js';
|
|
22
33
|
import { shouldRetryColdStart } from './retry.js';
|
|
23
34
|
import { createSubagent, defaultSubagentFactory, } from './subagent.js';
|
|
35
|
+
/**
|
|
36
|
+
* The two soft timers per child (team-overseer §3.2). Timers only — no state,
|
|
37
|
+
* no decisions; the runtime applies what the supervisor decides.
|
|
38
|
+
*
|
|
39
|
+
* Every timer is `unref`'d (the `backoff` argument): a pending supervision
|
|
40
|
+
* look must not hold the process open on exit.
|
|
41
|
+
*/
|
|
42
|
+
class ChildSupervisor {
|
|
43
|
+
deps;
|
|
44
|
+
clocks = new Map();
|
|
45
|
+
silences = new Map();
|
|
46
|
+
/** The interval used when a decision did not name one. */
|
|
47
|
+
defaultCheckMs;
|
|
48
|
+
constructor(deps) {
|
|
49
|
+
this.deps = deps;
|
|
50
|
+
this.defaultCheckMs = Math.max(TEAM_LIMITS.overseerNextCheckMinMs, deps.firstCheckMs);
|
|
51
|
+
}
|
|
52
|
+
/** Arm (or re-arm) both windows for a child starting or restarting an attempt. */
|
|
53
|
+
start(label) {
|
|
54
|
+
this.armClock(label, this.deps.firstCheckMs);
|
|
55
|
+
this.armSilence(label);
|
|
56
|
+
}
|
|
57
|
+
/** Reset the silence window. A no-op for a child nobody is supervising. */
|
|
58
|
+
kick(label) {
|
|
59
|
+
if (this.silences.has(label))
|
|
60
|
+
this.armSilence(label);
|
|
61
|
+
}
|
|
62
|
+
rearmSilence(label) {
|
|
63
|
+
this.armSilence(label);
|
|
64
|
+
}
|
|
65
|
+
rearmClock(label, ms) {
|
|
66
|
+
this.armClock(label, ms);
|
|
67
|
+
}
|
|
68
|
+
clear(label) {
|
|
69
|
+
const clock = this.clocks.get(label);
|
|
70
|
+
if (clock !== undefined)
|
|
71
|
+
clearTimeout(clock);
|
|
72
|
+
const silence = this.silences.get(label);
|
|
73
|
+
if (silence !== undefined)
|
|
74
|
+
clearTimeout(silence);
|
|
75
|
+
this.clocks.delete(label);
|
|
76
|
+
this.silences.delete(label);
|
|
77
|
+
}
|
|
78
|
+
clearAll() {
|
|
79
|
+
for (const label of [...this.clocks.keys()])
|
|
80
|
+
this.clear(label);
|
|
81
|
+
for (const label of [...this.silences.keys()])
|
|
82
|
+
this.clear(label);
|
|
83
|
+
}
|
|
84
|
+
armClock(label, ms) {
|
|
85
|
+
const previous = this.clocks.get(label);
|
|
86
|
+
if (previous !== undefined)
|
|
87
|
+
clearTimeout(previous);
|
|
88
|
+
const timer = setTimeout(() => this.fire(label, 'clock'), ms);
|
|
89
|
+
timer.unref?.();
|
|
90
|
+
this.clocks.set(label, timer);
|
|
91
|
+
}
|
|
92
|
+
armSilence(label) {
|
|
93
|
+
const previous = this.silences.get(label);
|
|
94
|
+
if (previous !== undefined)
|
|
95
|
+
clearTimeout(previous);
|
|
96
|
+
const timer = setTimeout(() => {
|
|
97
|
+
// I-OV3: a child legitimately blocked on `team_wait` or the confirm
|
|
98
|
+
// queue re-arms quietly instead of spending an inspection on a fact the
|
|
99
|
+
// digest would only re-derive.
|
|
100
|
+
if (this.deps.isLegitimatelyBlocked(label)) {
|
|
101
|
+
this.armSilence(label);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
this.fire(label, 'silence');
|
|
105
|
+
}, this.deps.idleMs);
|
|
106
|
+
timer.unref?.();
|
|
107
|
+
this.silences.set(label, timer);
|
|
108
|
+
}
|
|
109
|
+
fire(label, trigger) {
|
|
110
|
+
if (trigger === 'clock')
|
|
111
|
+
this.clocks.delete(label);
|
|
112
|
+
else
|
|
113
|
+
this.silences.delete(label);
|
|
114
|
+
this.deps.onFire(label, trigger);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
24
117
|
export class TeamRuntime {
|
|
25
118
|
deps;
|
|
26
119
|
listeners = new Set();
|
|
@@ -43,6 +136,16 @@ export class TeamRuntime {
|
|
|
43
136
|
* quietly not for the others.
|
|
44
137
|
*/
|
|
45
138
|
pendingBackoffs = new Set();
|
|
139
|
+
/**
|
|
140
|
+
* The dispatch supervisor, LIVE for the current dispatch only (team-overseer).
|
|
141
|
+
* Null when the provider is absent/disabled/off, and reset to null at every
|
|
142
|
+
* dispatch end so nothing supervision-shaped leaks across dispatches.
|
|
143
|
+
*/
|
|
144
|
+
overseer = null;
|
|
145
|
+
/** The soft timers, same lifetime as `overseer`. */
|
|
146
|
+
supervision = null;
|
|
147
|
+
/** Applied interventions for the in-flight dispatch's outcome. */
|
|
148
|
+
interventions = [];
|
|
46
149
|
constructor(deps) {
|
|
47
150
|
this.deps = deps;
|
|
48
151
|
}
|
|
@@ -65,10 +168,7 @@ export class TeamRuntime {
|
|
|
65
168
|
// -------------------------------------------------------------------------
|
|
66
169
|
// Dispatch
|
|
67
170
|
// -------------------------------------------------------------------------
|
|
68
|
-
async dispatch(specs, requested, signal
|
|
69
|
-
/** Carried through from the normalizer so the report can state it (§3.4 /
|
|
70
|
-
* R-6). Optional, so every existing caller compiles unchanged. */
|
|
71
|
-
extra = {}) {
|
|
171
|
+
async dispatch(specs, requested, signal) {
|
|
72
172
|
this.counter += 1;
|
|
73
173
|
const dispatchId = `d${this.counter}`;
|
|
74
174
|
const config = this.deps.getConfig();
|
|
@@ -76,6 +176,30 @@ export class TeamRuntime {
|
|
|
76
176
|
const startedAt = Date.now();
|
|
77
177
|
this.busy = true;
|
|
78
178
|
this.aborted = false;
|
|
179
|
+
this.interventions = [];
|
|
180
|
+
// THE SUPERVISOR IS CREATED PER DISPATCH and read live: provider present,
|
|
181
|
+
// provider active, policy on. `TeamOverseer.isActive()` re-checks the
|
|
182
|
+
// provider per inspection, so a mid-dispatch `/fast off` stops the next
|
|
183
|
+
// look without unwinding anything already applied.
|
|
184
|
+
this.overseer =
|
|
185
|
+
this.deps.overseer !== undefined &&
|
|
186
|
+
this.deps.overseer.active() &&
|
|
187
|
+
team.overseer
|
|
188
|
+
? new TeamOverseer(this.deps.overseer, { startedAt })
|
|
189
|
+
: null;
|
|
190
|
+
this.supervision =
|
|
191
|
+
this.overseer !== null
|
|
192
|
+
? new ChildSupervisor({
|
|
193
|
+
idleMs: config.idleTimeoutMs,
|
|
194
|
+
firstCheckMs: team.subagentTimeoutMs > 0
|
|
195
|
+
? team.subagentTimeoutMs
|
|
196
|
+
: TEAM_LIMITS.overseerDefaultCheckMs,
|
|
197
|
+
isLegitimatelyBlocked: (label) => this.isBlocked(label),
|
|
198
|
+
onFire: (label, trigger) => {
|
|
199
|
+
void this.inspectAndApply(dispatchId, label, trigger);
|
|
200
|
+
},
|
|
201
|
+
})
|
|
202
|
+
: null;
|
|
79
203
|
// DECLARED BEFORE THE BUS, ASSIGNED AFTER THE HANDLES. `canSend` closes over
|
|
80
204
|
// this binding so it reads the LIVE array: F-4 replaces `handles[index]`
|
|
81
205
|
// mid-dispatch, and a captured array or a captured handle would answer for
|
|
@@ -88,7 +212,7 @@ export class TeamRuntime {
|
|
|
88
212
|
* Whether `label` still has a loop that could call `team_send` (F-3 / P0-2).
|
|
89
213
|
*
|
|
90
214
|
* NEGATIVE, over TERMINAL PHASES ONLY. "The child's phase is running" is the
|
|
91
|
-
* natural way to write this and
|
|
215
|
+
* natural way to write this and is wrong on the SHIPPED DEFAULTS: at
|
|
92
216
|
* `maxConcurrent: 3, maxSubagents: 5`, children 4 and 5 sit in `queued` for
|
|
93
217
|
* the first part of every five-way dispatch, and a wait on one of them would
|
|
94
218
|
* be refused with "nobody can answer: a4 already finished". `queued` is not
|
|
@@ -131,7 +255,7 @@ export class TeamRuntime {
|
|
|
131
255
|
confirmTools: config.confirmTools,
|
|
132
256
|
...(this.deps.humanQueue ? { humanQueue: this.deps.humanQueue } : {}),
|
|
133
257
|
agentFactory: this.deps.agentFactory ?? defaultSubagentFactory,
|
|
134
|
-
...(this.deps.
|
|
258
|
+
...(this.deps.review ? { review: this.deps.review } : {}),
|
|
135
259
|
// Site two of two, and the omission is silent in the same way the policy
|
|
136
260
|
// one above is: declaring it on `TeamRuntimeDeps` and forgetting this line
|
|
137
261
|
// type-checks and forwards nothing, so every child would quietly run with
|
|
@@ -139,6 +263,10 @@ export class TeamRuntime {
|
|
|
139
263
|
...(this.deps.contextManagerFor
|
|
140
264
|
? { contextManagerFor: this.deps.contextManagerFor }
|
|
141
265
|
: {}),
|
|
266
|
+
// Same conditional-spread rule: a dispatch without a supervisor builds
|
|
267
|
+
// the exact pre-feature `SubagentDeps`, and the child's idle watchdog
|
|
268
|
+
// keeps its un-lengthened ceiling (team-overseer §3.6).
|
|
269
|
+
...(this.overseer !== null ? { overseerActive: true } : {}),
|
|
142
270
|
};
|
|
143
271
|
// Hoisted into a factory so a retry can build a fresh child with IDENTICAL
|
|
144
272
|
// wiring (F-4). Replacing the child rather than re-prompting it is the only
|
|
@@ -147,11 +275,10 @@ export class TeamRuntime {
|
|
|
147
275
|
// show the child its brief twice.
|
|
148
276
|
const makeHandle = (spec) => createSubagent(spec, subDeps, {
|
|
149
277
|
onUpdate: (run) => this.publish(dispatchId, run),
|
|
150
|
-
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
onUsage: (label, usage) => this.emit({ type: 'usage', dispatchId, label, usage, tier: spec.tier }),
|
|
278
|
+
onUsage: (label, usage) => this.emit({ type: 'usage', dispatchId, label, usage }),
|
|
279
|
+
// ONE KICK PER REAL AGENT EVENT, unthrottled (I-OV3): the silence
|
|
280
|
+
// window resets on evidence, not on a coalesced UI frame.
|
|
281
|
+
onActivity: () => this.supervision?.kick(spec.label),
|
|
155
282
|
});
|
|
156
283
|
// ASSIGNMENT, not a declaration: a `const` here would shadow the `let` above
|
|
157
284
|
// the bus, `canSend` would see the empty array forever, and every peer would
|
|
@@ -179,7 +306,15 @@ export class TeamRuntime {
|
|
|
179
306
|
// so the override only bites because `task` listens to `ctx.signal` — and a
|
|
180
307
|
// second, independent timer means a signal that never arrives still ends the
|
|
181
308
|
// dispatch.
|
|
182
|
-
|
|
309
|
+
//
|
|
310
|
+
// `0` MEANS NO CEILING (main-agent parity): the dispatch lasts as long as
|
|
311
|
+
// its slowest child and only an Esc ends it early. The lead's idle
|
|
312
|
+
// watchdog is paused for the whole dispatch either way, exactly as it is
|
|
313
|
+
// for the lead's own long turns.
|
|
314
|
+
this.dispatchTimer =
|
|
315
|
+
team.dispatchTimeoutMs > 0
|
|
316
|
+
? setTimeout(() => this.abortAll(), team.dispatchTimeoutMs)
|
|
317
|
+
: null;
|
|
183
318
|
const runCtx = {
|
|
184
319
|
dispatchId,
|
|
185
320
|
handles,
|
|
@@ -195,24 +330,16 @@ export class TeamRuntime {
|
|
|
195
330
|
signal?.removeEventListener('abort', onAbort);
|
|
196
331
|
for (const handle of handles)
|
|
197
332
|
handle.unsubscribe();
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
|
|
205
|
-
// existed — has it `undefined`, and `=== 'main'` would silently drop that
|
|
206
|
-
// child's spend from the total. Under-reporting is the one failure this
|
|
207
|
-
// split exists to prevent, so the default direction has to be "counted".
|
|
208
|
-
const sumTier = (tier) => handles
|
|
209
|
-
.filter((h) => (tier === 'fast' ? h.run.tier === 'fast' : h.run.tier !== 'fast'))
|
|
210
|
-
.reduce((acc, h) => ({
|
|
333
|
+
this.supervision?.clearAll();
|
|
334
|
+
this.supervision = null;
|
|
335
|
+
this.overseer = null;
|
|
336
|
+
// ONE TOTAL, AT THE LEAD'S TABLE. Every child runs the lead's own model
|
|
337
|
+
// (main-agent parity), so there is exactly one tier to sum and one price to
|
|
338
|
+
// sum it at; a split would be two answers to one question.
|
|
339
|
+
const usage = handles.reduce((acc, h) => ({
|
|
211
340
|
inputTokens: acc.inputTokens + h.run.usage.inputTokens,
|
|
212
341
|
outputTokens: acc.outputTokens + h.run.usage.outputTokens,
|
|
213
342
|
}), { inputTokens: 0, outputTokens: 0 });
|
|
214
|
-
const fastUsage = sumTier('fast');
|
|
215
|
-
const ranFast = handles.some((h) => h.run.tier === 'fast');
|
|
216
343
|
const outcome = {
|
|
217
344
|
dispatchId,
|
|
218
345
|
runs: handles.map((h) => ({ ...h.run, filesTouched: [...h.run.filesTouched] })),
|
|
@@ -221,11 +348,8 @@ export class TeamRuntime {
|
|
|
221
348
|
endedAt: Date.now(),
|
|
222
349
|
aborted: this.aborted,
|
|
223
350
|
leadMail: bus.leadMail(),
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
// outcome shape byte-identical to the pre-feature one.
|
|
227
|
-
...(ranFast ? { fastUsage } : {}),
|
|
228
|
-
...(extra.downgraded ? { downgraded: extra.downgraded } : {}),
|
|
351
|
+
interventions: this.interventions,
|
|
352
|
+
usage,
|
|
229
353
|
};
|
|
230
354
|
this.live = [];
|
|
231
355
|
this.bus = null;
|
|
@@ -255,21 +379,41 @@ export class TeamRuntime {
|
|
|
255
379
|
}
|
|
256
380
|
/**
|
|
257
381
|
* Run one child to a terminal phase, replacing it once if its very first
|
|
258
|
-
* request died on a transport error before it did anything at all (F-4)
|
|
382
|
+
* request died on a transport error before it did anything at all (F-4), and
|
|
383
|
+
* replacing it when the SUPERVISOR decides to (team-overseer §3.4).
|
|
384
|
+
*
|
|
385
|
+
* ONE LOOP, ALL LIFECYCLE: the cold-start replacement, the supervisor's
|
|
386
|
+
* replace and the post-mortem replace all swap `handles[index]` and continue
|
|
387
|
+
* THIS loop, because the await below is the only place a child is driven and
|
|
388
|
+
* a handle swapped anywhere else would never be (I-OV1).
|
|
259
389
|
*/
|
|
260
390
|
async runOne(ctx, index) {
|
|
261
391
|
const { dispatchId, handles, makeHandle, subagentTimeoutMs } = ctx;
|
|
262
|
-
|
|
392
|
+
let attempt = 0;
|
|
393
|
+
for (;;) {
|
|
394
|
+
attempt += 1;
|
|
263
395
|
const handle = handles[index];
|
|
264
396
|
const { run, agent } = handle;
|
|
397
|
+
// ABORTS GO THROUGH THE HANDLE (never `agent.abort()` directly): the
|
|
398
|
+
// handle sets the synchronous `abortRequested` flag the child's fast
|
|
399
|
+
// reviewer reads as its never-steer-into-a-requested-abort fact.
|
|
265
400
|
run.phase = 'starting';
|
|
266
401
|
run.startedAt = Date.now();
|
|
267
402
|
this.publish(dispatchId, run);
|
|
403
|
+
// WITH A SUPERVISOR there is no hard wall clock on the child: the
|
|
404
|
+
// supervisor looks and decides, and the child's own (lengthened) idle
|
|
405
|
+
// watchdog is the silence backstop. WITHOUT one, the legacy hard timer —
|
|
406
|
+
// armed exactly as before, `0` meaning no ceiling.
|
|
407
|
+
const supervised = this.overseer !== null;
|
|
408
|
+
if (supervised)
|
|
409
|
+
this.supervision?.start(run.label);
|
|
268
410
|
let timedOut = false;
|
|
269
|
-
const timer =
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
411
|
+
const timer = !supervised && subagentTimeoutMs > 0
|
|
412
|
+
? setTimeout(() => {
|
|
413
|
+
timedOut = true;
|
|
414
|
+
handle.abort();
|
|
415
|
+
}, subagentTimeoutMs)
|
|
416
|
+
: null;
|
|
273
417
|
try {
|
|
274
418
|
await agent.prompt(handle.spec.prompt);
|
|
275
419
|
}
|
|
@@ -281,23 +425,49 @@ export class TeamRuntime {
|
|
|
281
425
|
run.error = err instanceof Error ? err.message : String(err);
|
|
282
426
|
}
|
|
283
427
|
finally {
|
|
284
|
-
|
|
428
|
+
if (timer)
|
|
429
|
+
clearTimeout(timer);
|
|
285
430
|
}
|
|
286
|
-
|
|
431
|
+
const verdict = handle.overseerVerdict ?? null;
|
|
432
|
+
// THE SUPERVISOR'S REBUILD. Same loop, fresh body, amended brief: the
|
|
433
|
+
// verdict was set and the child aborted by `inspectAndApply`; here is
|
|
434
|
+
// where the replacement is actually built and driven. `attempt` resets so
|
|
435
|
+
// the fresh body gets its own cold-start budget.
|
|
436
|
+
if (verdict !== null && verdict.action === 'replace' && !this.aborted) {
|
|
437
|
+
this.replaceChild(dispatchId, handles, index, makeHandle, handle, verdict);
|
|
438
|
+
attempt = 0;
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
// THE ABORT EXCLUSIONS THAT MAKE THE PREDICATE SAFE. Every abort in
|
|
287
442
|
// this system reaches the provider as an aborted fetch, and `core` marks
|
|
288
443
|
// an `AbortError` retryable - so Esc, `ctx.signal`, the dispatch timeout
|
|
289
|
-
// (all three via `this.aborted`)
|
|
290
|
-
// are excluded HERE, before
|
|
291
|
-
|
|
444
|
+
// (all three via `this.aborted`), the per-child timeout (`timedOut`) and
|
|
445
|
+
// now the SUPERVISOR's verdict are excluded HERE, before
|
|
446
|
+
// `shouldRetryColdStart` is ever consulted.
|
|
447
|
+
const retryable = !this.aborted &&
|
|
448
|
+
!timedOut &&
|
|
449
|
+
verdict === null &&
|
|
450
|
+
shouldRetryColdStart(run, attempt);
|
|
292
451
|
if (!retryable) {
|
|
293
452
|
run.endedAt = Date.now();
|
|
453
|
+
// The supervisor's abandon is an error like any other, with the
|
|
454
|
+
// supervisor's own reason so the report explains WHO stopped it and why.
|
|
455
|
+
if (verdict !== null && verdict.action === 'abandon') {
|
|
456
|
+
run.error = run.error ?? `overseer abandoned: ${verdict.reason}`;
|
|
457
|
+
}
|
|
294
458
|
this.settlePhase(run, timedOut, subagentTimeoutMs);
|
|
295
459
|
this.publish(dispatchId, run);
|
|
460
|
+
// ONE post-mortem look at a child that died silently (team-overseer
|
|
461
|
+
// §3.4): a watchdog death is the "timed out" case the requirement most
|
|
462
|
+
// wants diagnosed, and replace-after-death is exactly re-assignment.
|
|
463
|
+
await this.maybePostMortem(ctx, index, handle, verdict);
|
|
464
|
+
this.supervision?.clear(run.label);
|
|
296
465
|
return;
|
|
297
466
|
}
|
|
298
|
-
|
|
467
|
+
this.replaceChild(dispatchId, handles, index, makeHandle, handle, null);
|
|
299
468
|
await this.backoff(TEAM_LIMITS.retryBackoffMs);
|
|
300
469
|
if (this.aborted) {
|
|
470
|
+
const fresh = handles[index];
|
|
301
471
|
fresh.run.phase = 'aborted';
|
|
302
472
|
fresh.run.endedAt = Date.now();
|
|
303
473
|
this.publish(dispatchId, fresh.run);
|
|
@@ -306,35 +476,214 @@ export class TeamRuntime {
|
|
|
306
476
|
}
|
|
307
477
|
}
|
|
308
478
|
/**
|
|
309
|
-
*
|
|
479
|
+
* One post-mortem inspection, and possibly ONE replacement driven by a
|
|
480
|
+
* recursive `runOne` — the worker that called us has already moved on to the
|
|
481
|
+
* next index, so nobody else will drive the fresh child (I-OV1 holds here
|
|
482
|
+
* too, one frame deeper). Recursion depth is bounded by the replacement
|
|
483
|
+
* budget, which is 1.
|
|
484
|
+
*/
|
|
485
|
+
async maybePostMortem(ctx, index, handle, verdict) {
|
|
486
|
+
const overseer = this.overseer;
|
|
487
|
+
if (overseer === null || this.aborted || verdict !== null)
|
|
488
|
+
return;
|
|
489
|
+
const { run, agent } = handle;
|
|
490
|
+
if (run.phase !== 'failed' || run.summary !== undefined)
|
|
491
|
+
return;
|
|
492
|
+
if (!overseer.replacementsLeft(run.label))
|
|
493
|
+
return;
|
|
494
|
+
const decision = await overseer.inspect({
|
|
495
|
+
run: { ...run },
|
|
496
|
+
messages: agent.state.messages,
|
|
497
|
+
trigger: 'postmortem',
|
|
498
|
+
childAlive: false,
|
|
499
|
+
});
|
|
500
|
+
if (decision === null || this.aborted)
|
|
501
|
+
return;
|
|
502
|
+
this.recordIntervention(ctx.dispatchId, run.label, 'postmortem', decision);
|
|
503
|
+
this.emit({
|
|
504
|
+
type: 'overseer',
|
|
505
|
+
dispatchId: ctx.dispatchId,
|
|
506
|
+
label: run.label,
|
|
507
|
+
decision,
|
|
508
|
+
trigger: 'postmortem',
|
|
509
|
+
});
|
|
510
|
+
if (decision.action === 'replace' && overseer.replacementsLeft(run.label)) {
|
|
511
|
+
this.replaceChild(ctx.dispatchId, ctx.handles, index, ctx.makeHandle, handle, {
|
|
512
|
+
action: 'replace',
|
|
513
|
+
reason: decision.reason,
|
|
514
|
+
...(decision.guidance !== undefined ? { guidance: decision.guidance } : {}),
|
|
515
|
+
});
|
|
516
|
+
await this.runOne(ctx, index);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Swap one child for a fresh one, carrying only what must survive.
|
|
310
521
|
*
|
|
311
522
|
* `this.live` IS `handles`, so the single assignment below also makes
|
|
312
523
|
* `abortAll()` abort the NEW child and makes the outcome carry the new run.
|
|
313
524
|
* Anyone tempted to "clean up" that aliasing into a defensive copy must update
|
|
314
525
|
* both places.
|
|
526
|
+
*
|
|
527
|
+
* `verdict === null` is the COLD-START retry (F-4): identical spec, `retries`
|
|
528
|
+
* carried. A verdict is the SUPERVISOR's replace: an amended brief
|
|
529
|
+
* (`buildReplacementPrompt`), `replacements` carried, and the supervisor's
|
|
530
|
+
* budget spent HERE, at the moment the rebuild is real.
|
|
315
531
|
*/
|
|
316
|
-
|
|
317
|
-
const handle = handles[index];
|
|
532
|
+
replaceChild(dispatchId, handles, index, makeHandle, handle, verdict) {
|
|
318
533
|
const carried = { ...handle.run.usage };
|
|
319
|
-
const retries = (handle.run.retries ?? 0) + 1;
|
|
534
|
+
const retries = (handle.run.retries ?? 0) + (verdict === null ? 1 : 0);
|
|
535
|
+
const replacements = (handle.run.replacements ?? 0) + (verdict !== null ? 1 : 0);
|
|
536
|
+
const interventions = handle.run.interventions;
|
|
320
537
|
handle.unsubscribe();
|
|
321
538
|
try {
|
|
322
|
-
handle.
|
|
539
|
+
handle.abort();
|
|
323
540
|
}
|
|
324
541
|
catch {
|
|
325
542
|
// Already gone; this must not stop the retry.
|
|
326
543
|
}
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
544
|
+
const spec = verdict !== null
|
|
545
|
+
? {
|
|
546
|
+
...handle.spec,
|
|
547
|
+
prompt: buildReplacementPrompt(handle.spec, {
|
|
548
|
+
action: 'replace',
|
|
549
|
+
reason: verdict.reason,
|
|
550
|
+
...(verdict.guidance !== undefined ? { guidance: verdict.guidance } : {}),
|
|
551
|
+
}, handle.run),
|
|
552
|
+
}
|
|
553
|
+
: handle.spec;
|
|
554
|
+
const fresh = makeHandle(spec);
|
|
555
|
+
// Provably {0,0} under the cold-start predicate above (no turn completed,
|
|
556
|
+
// so no `turn_end` and no usage event). Carried anyway so that a future
|
|
557
|
+
// relaxation of the predicate cannot silently under-report spend - the most
|
|
558
|
+
// misleading failure this feature can produce.
|
|
332
559
|
fresh.run.usage = carried;
|
|
333
560
|
fresh.run.retries = retries;
|
|
561
|
+
if (replacements > 0)
|
|
562
|
+
fresh.run.replacements = replacements;
|
|
563
|
+
if (interventions !== undefined)
|
|
564
|
+
fresh.run.interventions = interventions;
|
|
565
|
+
if (verdict !== null)
|
|
566
|
+
this.overseer?.noteReplacement(handle.run.label);
|
|
334
567
|
handles[index] = fresh;
|
|
335
568
|
this.publish(dispatchId, fresh.run);
|
|
336
569
|
return fresh;
|
|
337
570
|
}
|
|
571
|
+
// -------------------------------------------------------------------------
|
|
572
|
+
// Supervision (team-overseer)
|
|
573
|
+
// -------------------------------------------------------------------------
|
|
574
|
+
/**
|
|
575
|
+
* Whether a child's silence is a legitimate wait rather than a stall:
|
|
576
|
+
* `team_wait` (the comm tool already pauses the child's own watchdog for
|
|
577
|
+
* exactly this wait) or a confirmation queued on the one human slot.
|
|
578
|
+
*/
|
|
579
|
+
isBlocked(label) {
|
|
580
|
+
const handle = this.live.find((h) => h.run.label === label);
|
|
581
|
+
if (handle !== undefined && handle.run.phase === 'waiting')
|
|
582
|
+
return true;
|
|
583
|
+
return this.deps.humanQueue?.isWaiting(label) === true;
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* One trigger handled: look, then apply. Fired from a supervision timer and
|
|
587
|
+
* therefore async-and-forgotten; nothing here may throw.
|
|
588
|
+
*
|
|
589
|
+
* QUIET (`decision === null` — provider gone, budget spent) CLEARS the
|
|
590
|
+
* child's timers: supervision that has nothing left to say should stop
|
|
591
|
+
* waking up, and the child's own hard ceilings take over.
|
|
592
|
+
*/
|
|
593
|
+
async inspectAndApply(dispatchId, label, trigger) {
|
|
594
|
+
const overseer = this.overseer;
|
|
595
|
+
const supervision = this.supervision;
|
|
596
|
+
if (overseer === null || supervision === null || this.aborted)
|
|
597
|
+
return;
|
|
598
|
+
const handle = this.live.find((h) => h.run.label === label);
|
|
599
|
+
if (handle === undefined)
|
|
600
|
+
return;
|
|
601
|
+
const phase = handle.run.phase;
|
|
602
|
+
if (phase === 'done' || phase === 'failed' || phase === 'aborted')
|
|
603
|
+
return;
|
|
604
|
+
try {
|
|
605
|
+
const decision = await overseer.inspect({
|
|
606
|
+
run: handle.run,
|
|
607
|
+
messages: handle.agent.state.messages,
|
|
608
|
+
trigger,
|
|
609
|
+
childAlive: true,
|
|
610
|
+
});
|
|
611
|
+
if (decision === null || this.aborted) {
|
|
612
|
+
supervision.clear(label);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
// The child may have settled while the call was in flight; an
|
|
616
|
+
// intervention is still recorded (it happened), but nothing is applied.
|
|
617
|
+
const settled = handle.run.phase === 'done' ||
|
|
618
|
+
handle.run.phase === 'failed' ||
|
|
619
|
+
handle.run.phase === 'aborted';
|
|
620
|
+
this.recordIntervention(dispatchId, label, trigger, decision);
|
|
621
|
+
this.emit({ type: 'overseer', dispatchId, label, decision, trigger });
|
|
622
|
+
if (settled) {
|
|
623
|
+
supervision.clear(label);
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
this.applyDecision(supervision, overseer, handle, label, decision);
|
|
627
|
+
}
|
|
628
|
+
catch {
|
|
629
|
+
// Defence in depth around an async timer callback: an unexpected throw
|
|
630
|
+
// must not take the dispatch down. The child keeps its timers.
|
|
631
|
+
supervision.rearmSilence(label);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
applyDecision(supervision, overseer, handle, label, decision) {
|
|
635
|
+
switch (decision.action) {
|
|
636
|
+
case 'wait':
|
|
637
|
+
supervision.rearmClock(label, decision.nextCheckMs ?? supervision.defaultCheckMs);
|
|
638
|
+
supervision.rearmSilence(label);
|
|
639
|
+
break;
|
|
640
|
+
case 'nudge':
|
|
641
|
+
// I-OV4: never steer into an abort already requested — the same
|
|
642
|
+
// guard-1 fact the child's fast reviewer reads. Queued steering is
|
|
643
|
+
// drained at the safe top-of-loop checkpoint by core.
|
|
644
|
+
if (!handle.isAborted() && decision.guidance !== undefined) {
|
|
645
|
+
handle.agent.steer(decision.guidance);
|
|
646
|
+
}
|
|
647
|
+
supervision.rearmClock(label, supervision.defaultCheckMs);
|
|
648
|
+
supervision.rearmSilence(label);
|
|
649
|
+
break;
|
|
650
|
+
case 'replace': {
|
|
651
|
+
// Budget checked BEFORE the verdict is set, so two near-simultaneous
|
|
652
|
+
// replace decisions cannot both rebuild (the second becomes an
|
|
653
|
+
// abandon with the budget stated in the reason).
|
|
654
|
+
handle.overseerVerdict = overseer.replacementsLeft(label)
|
|
655
|
+
? {
|
|
656
|
+
action: 'replace',
|
|
657
|
+
reason: decision.reason,
|
|
658
|
+
...(decision.guidance !== undefined ? { guidance: decision.guidance } : {}),
|
|
659
|
+
}
|
|
660
|
+
: { action: 'abandon', reason: `replacement budget exhausted; ${decision.reason}` };
|
|
661
|
+
supervision.clear(label);
|
|
662
|
+
handle.abort();
|
|
663
|
+
break;
|
|
664
|
+
}
|
|
665
|
+
case 'abandon':
|
|
666
|
+
handle.overseerVerdict = { action: 'abandon', reason: decision.reason };
|
|
667
|
+
supervision.clear(label);
|
|
668
|
+
handle.abort();
|
|
669
|
+
break;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
/** Record one applied decision on the outcome, the run and the event stream. */
|
|
673
|
+
recordIntervention(dispatchId, label, trigger, decision) {
|
|
674
|
+
this.interventions.push({
|
|
675
|
+
label,
|
|
676
|
+
at: Date.now(),
|
|
677
|
+
action: decision.action,
|
|
678
|
+
reason: decision.reason,
|
|
679
|
+
trigger,
|
|
680
|
+
});
|
|
681
|
+
const handle = this.live.find((h) => h.run.label === label);
|
|
682
|
+
if (handle !== undefined) {
|
|
683
|
+
handle.run.interventions = (handle.run.interventions ?? 0) + 1;
|
|
684
|
+
this.publish(dispatchId, handle.run);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
338
687
|
/**
|
|
339
688
|
* The final phase of an attempt, in a FIXED ORDER: aborted, then timed out,
|
|
340
689
|
* then error, then no-summary, then done.
|
|
@@ -346,7 +695,7 @@ export class TeamRuntime {
|
|
|
346
695
|
if (this.aborted) {
|
|
347
696
|
run.phase = 'aborted';
|
|
348
697
|
}
|
|
349
|
-
else if (timedOut) {
|
|
698
|
+
else if (timedOut && subagentTimeoutMs > 0) {
|
|
350
699
|
run.phase = 'failed';
|
|
351
700
|
run.error = run.error ?? `subagent exceeded ${Math.round(subagentTimeoutMs / 1000)}s`;
|
|
352
701
|
}
|
|
@@ -398,13 +747,14 @@ export class TeamRuntime {
|
|
|
398
747
|
this.aborted = true;
|
|
399
748
|
for (const handle of this.live) {
|
|
400
749
|
try {
|
|
401
|
-
handle.
|
|
750
|
+
handle.abort();
|
|
402
751
|
}
|
|
403
752
|
catch {
|
|
404
753
|
// A child that is already gone must not stop the others being aborted.
|
|
405
754
|
}
|
|
406
755
|
}
|
|
407
756
|
this.bus?.cancelAllWaits();
|
|
757
|
+
this.supervision?.clearAll();
|
|
408
758
|
// Every in-flight retry backoff, not just the last one registered: a
|
|
409
759
|
// provider-wide failure puts up to `maxConcurrent` children here at once.
|
|
410
760
|
for (const release of [...this.pendingBackoffs])
|
|
@@ -416,6 +766,9 @@ export class TeamRuntime {
|
|
|
416
766
|
if (this.dispatchTimer)
|
|
417
767
|
clearTimeout(this.dispatchTimer);
|
|
418
768
|
this.dispatchTimer = null;
|
|
769
|
+
this.supervision?.clearAll();
|
|
770
|
+
this.supervision = null;
|
|
771
|
+
this.overseer = null;
|
|
419
772
|
this.listeners.clear();
|
|
420
773
|
}
|
|
421
774
|
// -------------------------------------------------------------------------
|