@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/runtime.js
CHANGED
|
@@ -28,30 +28,48 @@
|
|
|
28
28
|
* pre-feature behaviour.
|
|
29
29
|
*/
|
|
30
30
|
import { TeamBus } from './bus.js';
|
|
31
|
-
import { TEAM_LIMITS } from './limits.js';
|
|
32
|
-
import { TeamOverseer, buildReplacementPrompt } from './overseer.js';
|
|
31
|
+
import { TEAM_AGGREGATE_LABEL, TEAM_LIMITS } from './limits.js';
|
|
32
|
+
import { TeamOverseer, buildReplacementPrompt, nextOverseerLadderStepMs, resolveOverseerFirstCheckMs, } from './overseer.js';
|
|
33
33
|
import { shouldRetryColdStart } from './retry.js';
|
|
34
34
|
import { createSubagent, defaultSubagentFactory, } from './subagent.js';
|
|
35
35
|
/**
|
|
36
|
-
* The
|
|
37
|
-
*
|
|
36
|
+
* The pseudo-label aggregate supervisor notices travel under (D-5 / R-P1-6):
|
|
37
|
+
* `TEAM_AGGREGATE_LABEL`, RESERVED at normalization so no child slug can be
|
|
38
|
+
* `team`. The reservation is what makes this label a safe namespace - a
|
|
39
|
+
* colliding child would have the aggregate notice attributed to it in the
|
|
40
|
+
* ledger and its own quiet note swallowed by the report's aggregate count.
|
|
41
|
+
*/
|
|
42
|
+
const MONITOR_TEAM_LABEL = TEAM_AGGREGATE_LABEL;
|
|
43
|
+
const MONITOR_EXHAUSTED_DECISION = {
|
|
44
|
+
action: 'wait',
|
|
45
|
+
reason: 'dispatch look budget exhausted; supervision quiet for the rest of the dispatch',
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* The wait a DEGRADED tick announces per child (D-4 / D-10). One shared
|
|
49
|
+
* object, not a fresh literal per event: it is the same fact every time and
|
|
50
|
+
* identity makes that obvious to any consumer that looks.
|
|
51
|
+
*/
|
|
52
|
+
const DEGRADED_WAIT_DECISION = {
|
|
53
|
+
action: 'wait',
|
|
54
|
+
reason: 'fast tier unavailable; waiting unassisted',
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* The per-child SILENCE windows (team-overseer §3.2; subagent-overseer-v2
|
|
58
|
+
* keeps only this half). Timers only — no state, no decisions; the runtime
|
|
59
|
+
* applies what the supervisor decides. The wall-clock half moved to
|
|
60
|
+
* `MonitorLoop`, which owns the cadence ladder for the whole roster.
|
|
38
61
|
*
|
|
39
62
|
* Every timer is `unref`'d (the `backoff` argument): a pending supervision
|
|
40
63
|
* look must not hold the process open on exit.
|
|
41
64
|
*/
|
|
42
65
|
class ChildSupervisor {
|
|
43
66
|
deps;
|
|
44
|
-
clocks = new Map();
|
|
45
67
|
silences = new Map();
|
|
46
|
-
/** The interval used when a decision did not name one. */
|
|
47
|
-
defaultCheckMs;
|
|
48
68
|
constructor(deps) {
|
|
49
69
|
this.deps = deps;
|
|
50
|
-
this.defaultCheckMs = Math.max(TEAM_LIMITS.overseerNextCheckMinMs, deps.firstCheckMs);
|
|
51
70
|
}
|
|
52
|
-
/** Arm (or re-arm)
|
|
71
|
+
/** Arm (or re-arm) the silence window for a child starting an attempt. */
|
|
53
72
|
start(label) {
|
|
54
|
-
this.armClock(label, this.deps.firstCheckMs);
|
|
55
73
|
this.armSilence(label);
|
|
56
74
|
}
|
|
57
75
|
/** Reset the silence window. A no-op for a child nobody is supervising. */
|
|
@@ -62,33 +80,16 @@ class ChildSupervisor {
|
|
|
62
80
|
rearmSilence(label) {
|
|
63
81
|
this.armSilence(label);
|
|
64
82
|
}
|
|
65
|
-
rearmClock(label, ms) {
|
|
66
|
-
this.armClock(label, ms);
|
|
67
|
-
}
|
|
68
83
|
clear(label) {
|
|
69
|
-
const clock = this.clocks.get(label);
|
|
70
|
-
if (clock !== undefined)
|
|
71
|
-
clearTimeout(clock);
|
|
72
84
|
const silence = this.silences.get(label);
|
|
73
85
|
if (silence !== undefined)
|
|
74
86
|
clearTimeout(silence);
|
|
75
|
-
this.clocks.delete(label);
|
|
76
87
|
this.silences.delete(label);
|
|
77
88
|
}
|
|
78
89
|
clearAll() {
|
|
79
|
-
for (const label of [...this.clocks.keys()])
|
|
80
|
-
this.clear(label);
|
|
81
90
|
for (const label of [...this.silences.keys()])
|
|
82
91
|
this.clear(label);
|
|
83
92
|
}
|
|
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
93
|
armSilence(label) {
|
|
93
94
|
const previous = this.silences.get(label);
|
|
94
95
|
if (previous !== undefined)
|
|
@@ -101,17 +102,191 @@ class ChildSupervisor {
|
|
|
101
102
|
this.armSilence(label);
|
|
102
103
|
return;
|
|
103
104
|
}
|
|
104
|
-
this.
|
|
105
|
+
this.silences.delete(label);
|
|
106
|
+
this.deps.onFire(label, 'silence');
|
|
105
107
|
}, this.deps.idleMs);
|
|
106
108
|
timer.unref?.();
|
|
107
109
|
this.silences.set(label, timer);
|
|
108
110
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The cadence ladder (subagent-overseer-v2 D-2): one timer for the whole
|
|
114
|
+
* roster, per-child `nextAt` / `ladderStep` entries armed at each child's
|
|
115
|
+
* ACTUAL start (R-P1-5), and a strict one-batch-in-flight rule.
|
|
116
|
+
*
|
|
117
|
+
* A SKIPPED LABEL RE-SCHEULES WITHOUT TOUCHING ITS LADDER STEP: the
|
|
118
|
+
* single-flight guard (R-P1-4) can leave a due `nextAt` in the past while a
|
|
119
|
+
* silence-triggered inspection of that child is still settling, and a timer
|
|
120
|
+
* that re-fired at `max(0, due - now)` would busy-loop until it settles. The
|
|
121
|
+
* recheck delay is invisible to the protocol - no look is spent, the ladder
|
|
122
|
+
* value does not move - it only keeps the event loop idle.
|
|
123
|
+
*/
|
|
124
|
+
const MONITOR_RECHECK_MS = 500;
|
|
125
|
+
class MonitorLoop {
|
|
126
|
+
deps;
|
|
127
|
+
nextAt = new Map();
|
|
128
|
+
ladderStep = new Map();
|
|
129
|
+
timer = null;
|
|
130
|
+
ticking = false;
|
|
131
|
+
stopped = false;
|
|
132
|
+
constructor(deps) {
|
|
133
|
+
this.deps = deps;
|
|
134
|
+
}
|
|
135
|
+
/** Arm the FIRST rung for a child that just started (R-P1-5). */
|
|
136
|
+
arm(label) {
|
|
137
|
+
const now = this.deps.now();
|
|
138
|
+
this.nextAt.set(label, now + this.deps.firstCheckMs);
|
|
139
|
+
this.ladderStep.set(label, this.deps.firstCheckMs);
|
|
140
|
+
this.schedule();
|
|
141
|
+
}
|
|
142
|
+
/** A wait decision's `nextCheckMs` OVERRIDES the rung and becomes it (D-2). */
|
|
143
|
+
override(label, ms) {
|
|
144
|
+
const now = this.deps.now();
|
|
145
|
+
this.nextAt.set(label, now + ms);
|
|
146
|
+
this.ladderStep.set(label, ms);
|
|
147
|
+
this.schedule();
|
|
148
|
+
}
|
|
149
|
+
/** Any ACTION resets that child's ladder to the base rung (D-2). */
|
|
150
|
+
reset(label) {
|
|
151
|
+
this.arm(label);
|
|
152
|
+
}
|
|
153
|
+
/** Grow the rung and schedule the next visit (D-2). */
|
|
154
|
+
advance(label) {
|
|
155
|
+
const now = this.deps.now();
|
|
156
|
+
const step = nextOverseerLadderStepMs(this.ladderStep.get(label) ?? this.deps.firstCheckMs);
|
|
157
|
+
this.ladderStep.set(label, step);
|
|
158
|
+
this.nextAt.set(label, now + step);
|
|
159
|
+
this.schedule();
|
|
160
|
+
}
|
|
161
|
+
/** Stop visiting one child (settled, quieted, or its verdict applied). */
|
|
162
|
+
clear(label) {
|
|
163
|
+
this.nextAt.delete(label);
|
|
164
|
+
this.ladderStep.delete(label);
|
|
165
|
+
this.schedule();
|
|
166
|
+
}
|
|
167
|
+
/** Stop the whole loop; the dispatch is over or the total budget is spent. */
|
|
168
|
+
stop() {
|
|
169
|
+
this.stopped = true;
|
|
170
|
+
if (this.timer !== null)
|
|
171
|
+
clearTimeout(this.timer);
|
|
172
|
+
this.timer = null;
|
|
173
|
+
this.nextAt.clear();
|
|
174
|
+
this.ladderStep.clear();
|
|
175
|
+
}
|
|
176
|
+
dispose() {
|
|
177
|
+
this.stop();
|
|
178
|
+
}
|
|
179
|
+
/** The earliest due time among live labels, or null when nothing is armed. */
|
|
180
|
+
earliestDue() {
|
|
181
|
+
let earliest = null;
|
|
182
|
+
for (const [label, at] of this.nextAt) {
|
|
183
|
+
if (!this.deps.isLive(label))
|
|
184
|
+
continue;
|
|
185
|
+
earliest = earliest === null ? at : Math.min(earliest, at);
|
|
186
|
+
}
|
|
187
|
+
return earliest;
|
|
188
|
+
}
|
|
189
|
+
schedule() {
|
|
190
|
+
if (this.timer !== null) {
|
|
191
|
+
clearTimeout(this.timer);
|
|
192
|
+
this.timer = null;
|
|
193
|
+
}
|
|
194
|
+
if (this.stopped || this.ticking)
|
|
195
|
+
return;
|
|
196
|
+
const earliest = this.earliestDue();
|
|
197
|
+
if (earliest === null)
|
|
198
|
+
return;
|
|
199
|
+
const delay = Math.max(0, earliest - this.deps.now());
|
|
200
|
+
const timer = setTimeout(() => {
|
|
201
|
+
this.timer = null;
|
|
202
|
+
void this.tick();
|
|
203
|
+
}, delay);
|
|
204
|
+
timer.unref?.();
|
|
205
|
+
this.timer = timer;
|
|
206
|
+
}
|
|
207
|
+
async tick() {
|
|
208
|
+
if (this.ticking || this.stopped)
|
|
209
|
+
return;
|
|
210
|
+
const now = this.deps.now();
|
|
211
|
+
const due = [];
|
|
212
|
+
for (const [label, at] of [...this.nextAt]) {
|
|
213
|
+
if (!this.deps.isLive(label)) {
|
|
214
|
+
// Settled while armed; R-P1-5 also means a re-queued replacement body
|
|
215
|
+
// re-arms itself at its own start.
|
|
216
|
+
this.nextAt.delete(label);
|
|
217
|
+
this.ladderStep.delete(label);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (now >= at)
|
|
221
|
+
due.push(label);
|
|
222
|
+
}
|
|
223
|
+
if (due.length === 0) {
|
|
224
|
+
this.schedule();
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
// DEGRADED TICK (D-4 / D-10): no fast tier right now. The children wait
|
|
228
|
+
// unassisted - announced per child, never killed - and the cadence keeps
|
|
229
|
+
// running so a `/fast on` restores assistance at the next rung.
|
|
230
|
+
if (!this.deps.isActive()) {
|
|
231
|
+
this.deps.onDegraded(due);
|
|
232
|
+
for (const label of due)
|
|
233
|
+
this.advance(label);
|
|
234
|
+
this.schedule();
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
// PER-CHILD QUIET (D-5): budget spent says so once, then stop visiting.
|
|
238
|
+
const inspectable = [];
|
|
239
|
+
for (const label of due) {
|
|
240
|
+
if (this.deps.isExhausted(label)) {
|
|
241
|
+
this.clear(label);
|
|
242
|
+
this.deps.onQuiet(label);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
inspectable.push(label);
|
|
246
|
+
}
|
|
247
|
+
if (inspectable.length === 0) {
|
|
248
|
+
this.schedule();
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
this.ticking = true;
|
|
252
|
+
let decisions;
|
|
253
|
+
try {
|
|
254
|
+
decisions = await this.deps.inspectBatch(inspectable);
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
// Defence in depth: `inspectBatch` is fail-soft by contract. Treat the
|
|
258
|
+
// tick as if every child had answered wait.
|
|
259
|
+
decisions = new Map();
|
|
260
|
+
}
|
|
261
|
+
finally {
|
|
262
|
+
this.ticking = false;
|
|
263
|
+
}
|
|
264
|
+
const after = this.deps.now();
|
|
265
|
+
for (const label of inspectable) {
|
|
266
|
+
const decision = decisions.get(label);
|
|
267
|
+
if (decision === undefined) {
|
|
268
|
+
// Skipped for single-flight (R-P1-4): no decision, no look, and the
|
|
269
|
+
// ladder does not move - only the timer must not busy-loop.
|
|
270
|
+
this.nextAt.set(label, after + MONITOR_RECHECK_MS);
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (decision.action === 'wait' && decision.nextCheckMs !== undefined) {
|
|
274
|
+
this.override(label, decision.nextCheckMs);
|
|
275
|
+
}
|
|
276
|
+
else if (decision.action === 'wait') {
|
|
277
|
+
this.advance(label);
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
this.reset(label);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (this.deps.dispatchExhausted()) {
|
|
284
|
+
// The fool-proof total (R-P1-6). Stop the whole loop, once, loudly.
|
|
285
|
+
this.stop();
|
|
286
|
+
this.deps.onExhausted();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
this.schedule();
|
|
115
290
|
}
|
|
116
291
|
}
|
|
117
292
|
export class TeamRuntime {
|
|
@@ -137,15 +312,30 @@ export class TeamRuntime {
|
|
|
137
312
|
*/
|
|
138
313
|
pendingBackoffs = new Set();
|
|
139
314
|
/**
|
|
140
|
-
* The dispatch supervisor, LIVE for the current dispatch only
|
|
141
|
-
* Null when the provider is
|
|
142
|
-
*
|
|
315
|
+
* The dispatch supervisor, LIVE for the current dispatch only
|
|
316
|
+
* (team-overseer / subagent-overseer-v2). Null when the provider is not
|
|
317
|
+
* wired or the policy is off, and reset to null at every dispatch end so
|
|
318
|
+
* nothing supervision-shaped leaks across dispatches.
|
|
319
|
+
*
|
|
320
|
+
* THE GATE IS "PROVIDER PRESENT AND POLICY ON" (R-P0-2), deliberately NOT
|
|
321
|
+
* the provider's live `active()`: a dispatch that STARTS without a fast
|
|
322
|
+
* tier must still get a MonitorLoop, because the promise this feature
|
|
323
|
+
* makes is "timeout is never a death sentence" - an unavailable tier
|
|
324
|
+
* degrades the tick to an announced unassisted wait rather than falling
|
|
325
|
+
* back to the legacy hard abort. `active()` is re-read every tick.
|
|
143
326
|
*/
|
|
144
327
|
overseer = null;
|
|
145
|
-
/** The
|
|
328
|
+
/** The per-child silence windows, same lifetime as `overseer`. */
|
|
146
329
|
supervision = null;
|
|
330
|
+
/** The cadence ladder (subagent-overseer-v2 D-2), same lifetime. */
|
|
331
|
+
monitor = null;
|
|
147
332
|
/** Applied interventions for the in-flight dispatch's outcome. */
|
|
148
333
|
interventions = [];
|
|
334
|
+
/** Labels already told the supervisor went quiet on them (D-5, once). */
|
|
335
|
+
quietNotified = new Set();
|
|
336
|
+
/** Edge-latched "a tick ran unassisted" (D-10), per dispatch. */
|
|
337
|
+
overseerDegraded = false;
|
|
338
|
+
overseerDegradedTicks = 0;
|
|
149
339
|
constructor(deps) {
|
|
150
340
|
this.deps = deps;
|
|
151
341
|
}
|
|
@@ -177,29 +367,48 @@ export class TeamRuntime {
|
|
|
177
367
|
this.busy = true;
|
|
178
368
|
this.aborted = false;
|
|
179
369
|
this.interventions = [];
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
//
|
|
370
|
+
this.quietNotified.clear();
|
|
371
|
+
this.overseerDegraded = false;
|
|
372
|
+
this.overseerDegradedTicks = 0;
|
|
373
|
+
// THE SUPERVISOR IS CREATED PER DISPATCH (R-P0-2): provider wired AND
|
|
374
|
+
// policy on. `TeamOverseer.isActive()` re-checks the provider per
|
|
375
|
+
// inspection and per tick, so a mid-dispatch `/fast off` degrades the
|
|
376
|
+
// NEXT look to an unassisted wait instead of unwinding anything already
|
|
377
|
+
// applied - and a dispatch that started without the tier is still
|
|
378
|
+
// supervised, in that degraded, never-kill sense.
|
|
379
|
+
const supervised = this.deps.overseer !== undefined && team.overseer;
|
|
184
380
|
this.overseer =
|
|
185
|
-
this.deps.overseer !== undefined
|
|
186
|
-
this.deps.overseer.active() &&
|
|
187
|
-
team.overseer
|
|
381
|
+
supervised && this.deps.overseer !== undefined
|
|
188
382
|
? new TeamOverseer(this.deps.overseer, { startedAt })
|
|
189
383
|
: null;
|
|
190
384
|
this.supervision =
|
|
191
385
|
this.overseer !== null
|
|
192
386
|
? new ChildSupervisor({
|
|
193
387
|
idleMs: config.idleTimeoutMs,
|
|
194
|
-
firstCheckMs: team.subagentTimeoutMs > 0
|
|
195
|
-
? team.subagentTimeoutMs
|
|
196
|
-
: TEAM_LIMITS.overseerDefaultCheckMs,
|
|
197
388
|
isLegitimatelyBlocked: (label) => this.isBlocked(label),
|
|
198
389
|
onFire: (label, trigger) => {
|
|
199
390
|
void this.inspectAndApply(dispatchId, label, trigger);
|
|
200
391
|
},
|
|
201
392
|
})
|
|
202
393
|
: null;
|
|
394
|
+
this.monitor =
|
|
395
|
+
this.overseer !== null
|
|
396
|
+
? new MonitorLoop({
|
|
397
|
+
// D-8: `overseerIntervalMs` wins, then a positive
|
|
398
|
+
// `subagentTimeoutMs` (compat), then the structural default.
|
|
399
|
+
firstCheckMs: resolveOverseerFirstCheckMs(team),
|
|
400
|
+
now: () => Date.now(),
|
|
401
|
+
// R-P1-5: only STARTED, UNSETTLED children tick.
|
|
402
|
+
isLive: (label) => this.isMonitorLive(label),
|
|
403
|
+
isActive: () => this.overseer?.isActive() === true,
|
|
404
|
+
isExhausted: (label) => this.overseer?.looksExhausted(label) === true,
|
|
405
|
+
dispatchExhausted: () => this.overseer?.dispatchLooksExhausted() === true,
|
|
406
|
+
inspectBatch: (labels) => this.monitorInspectBatch(dispatchId, labels),
|
|
407
|
+
onDegraded: (labels) => this.noteDegradedTick(dispatchId, labels),
|
|
408
|
+
onQuiet: (label) => this.noteQuiet(dispatchId, label),
|
|
409
|
+
onExhausted: () => this.noteDispatchExhausted(dispatchId),
|
|
410
|
+
})
|
|
411
|
+
: null;
|
|
203
412
|
// DECLARED BEFORE THE BUS, ASSIGNED AFTER THE HANDLES. `canSend` closes over
|
|
204
413
|
// this binding so it reads the LIVE array: F-4 replaces `handles[index]`
|
|
205
414
|
// mid-dispatch, and a captured array or a captured handle would answer for
|
|
@@ -319,7 +528,11 @@ export class TeamRuntime {
|
|
|
319
528
|
dispatchId,
|
|
320
529
|
handles,
|
|
321
530
|
makeHandle,
|
|
322
|
-
|
|
531
|
+
// D-4: with the supervisor gate open, `subagentTimeoutMs` NEVER arms a
|
|
532
|
+
// hard abort - 0 here arms no timer, so the key's only remaining role
|
|
533
|
+
// is the ladder's compat first-check (D-8). The legacy hard abort
|
|
534
|
+
// exists ONLY under `team.overseer: false`.
|
|
535
|
+
subagentTimeoutMs: team.overseer ? 0 : team.subagentTimeoutMs,
|
|
323
536
|
};
|
|
324
537
|
const cursor = { next: 0 };
|
|
325
538
|
const workers = Array.from({ length: Math.max(1, Math.min(team.maxConcurrent, handles.length)) }, () => this.worker(runCtx, cursor));
|
|
@@ -330,8 +543,17 @@ export class TeamRuntime {
|
|
|
330
543
|
signal?.removeEventListener('abort', onAbort);
|
|
331
544
|
for (const handle of handles)
|
|
332
545
|
handle.unsubscribe();
|
|
546
|
+
this.monitor?.dispose();
|
|
547
|
+
this.monitor = null;
|
|
333
548
|
this.supervision?.clearAll();
|
|
334
549
|
this.supervision = null;
|
|
550
|
+
// CAPTURED BEFORE THE FIELD IS NULLED: the outcome carries what the
|
|
551
|
+
// supervisor spent (D-7) and whether any tick ran unassisted (D-10),
|
|
552
|
+
// both read off the per-dispatch state being torn down here.
|
|
553
|
+
const overseerUsage = this.overseer?.usage();
|
|
554
|
+
const overseerCalls = this.overseer?.calls();
|
|
555
|
+
const overseerDegraded = this.overseerDegraded;
|
|
556
|
+
const overseerDegradedTicks = this.overseerDegradedTicks;
|
|
335
557
|
this.overseer = null;
|
|
336
558
|
// ONE TOTAL, AT THE LEAD'S TABLE. Every child runs the lead's own model
|
|
337
559
|
// (main-agent parity), so there is exactly one tier to sum and one price to
|
|
@@ -350,6 +572,13 @@ export class TeamRuntime {
|
|
|
350
572
|
leadMail: bus.leadMail(),
|
|
351
573
|
interventions: this.interventions,
|
|
352
574
|
usage,
|
|
575
|
+
// Honest supervision accounting (D-7 / D-10). `overseerCalls > 0`
|
|
576
|
+
// with zero usage is a provider that never reported usage - the
|
|
577
|
+
// report then omits the spend line rather than pricing nothing.
|
|
578
|
+
...(overseerUsage !== undefined ? { overseerUsage } : {}),
|
|
579
|
+
...(overseerCalls !== undefined ? { overseerCalls } : {}),
|
|
580
|
+
...(overseerDegraded ? { overseerDegraded: true } : {}),
|
|
581
|
+
...(overseerDegradedTicks > 0 ? { overseerDegradedTicks } : {}),
|
|
353
582
|
};
|
|
354
583
|
this.live = [];
|
|
355
584
|
this.bus = null;
|
|
@@ -400,13 +629,20 @@ export class TeamRuntime {
|
|
|
400
629
|
run.phase = 'starting';
|
|
401
630
|
run.startedAt = Date.now();
|
|
402
631
|
this.publish(dispatchId, run);
|
|
403
|
-
// WITH
|
|
404
|
-
//
|
|
405
|
-
// watchdog is the silence backstop.
|
|
406
|
-
//
|
|
632
|
+
// WITH THE SUPERVISOR GATE OPEN there is no hard wall clock on the
|
|
633
|
+
// child at all (D-4): the cadence ladder looks and decides, and the
|
|
634
|
+
// child's own (lengthened) idle watchdog is the silence backstop. The
|
|
635
|
+
// rung and the silence window are both armed HERE, at the attempt's
|
|
636
|
+
// actual start (R-P1-5) - a `queued` child has neither. WITHOUT the
|
|
637
|
+
// gate - `team.overseer: false`, the legacy regime - the hard timer is
|
|
638
|
+
// armed exactly as before, `0` meaning no ceiling. A provider that is
|
|
639
|
+
// wired but INACTIVE arms no timer either: the tick degrades to an
|
|
640
|
+
// announced unassisted wait, never a kill (R-P0-2).
|
|
407
641
|
const supervised = this.overseer !== null;
|
|
408
|
-
if (supervised)
|
|
642
|
+
if (supervised) {
|
|
409
643
|
this.supervision?.start(run.label);
|
|
644
|
+
this.monitor?.arm(run.label);
|
|
645
|
+
}
|
|
410
646
|
let timedOut = false;
|
|
411
647
|
const timer = !supervised && subagentTimeoutMs > 0
|
|
412
648
|
? setTimeout(() => {
|
|
@@ -462,6 +698,7 @@ export class TeamRuntime {
|
|
|
462
698
|
// wants diagnosed, and replace-after-death is exactly re-assignment.
|
|
463
699
|
await this.maybePostMortem(ctx, index, handle, verdict);
|
|
464
700
|
this.supervision?.clear(run.label);
|
|
701
|
+
this.monitor?.clear(run.label);
|
|
465
702
|
return;
|
|
466
703
|
}
|
|
467
704
|
this.replaceChild(dispatchId, handles, index, makeHandle, handle, null);
|
|
@@ -534,6 +771,7 @@ export class TeamRuntime {
|
|
|
534
771
|
const retries = (handle.run.retries ?? 0) + (verdict === null ? 1 : 0);
|
|
535
772
|
const replacements = (handle.run.replacements ?? 0) + (verdict !== null ? 1 : 0);
|
|
536
773
|
const interventions = handle.run.interventions;
|
|
774
|
+
const lastIntervention = handle.run.lastIntervention;
|
|
537
775
|
handle.unsubscribe();
|
|
538
776
|
try {
|
|
539
777
|
handle.abort();
|
|
@@ -562,6 +800,10 @@ export class TeamRuntime {
|
|
|
562
800
|
fresh.run.replacements = replacements;
|
|
563
801
|
if (interventions !== undefined)
|
|
564
802
|
fresh.run.interventions = interventions;
|
|
803
|
+
// The badge survives a rebuild (`replaced 5s`) - the panel row does not
|
|
804
|
+
// reset just because the body under it did.
|
|
805
|
+
if (lastIntervention !== undefined)
|
|
806
|
+
fresh.run.lastIntervention = lastIntervention;
|
|
565
807
|
if (verdict !== null)
|
|
566
808
|
this.overseer?.noteReplacement(handle.run.label);
|
|
567
809
|
handles[index] = fresh;
|
|
@@ -583,12 +825,14 @@ export class TeamRuntime {
|
|
|
583
825
|
return this.deps.humanQueue?.isWaiting(label) === true;
|
|
584
826
|
}
|
|
585
827
|
/**
|
|
586
|
-
* One trigger handled: look, then apply. Fired from a supervision
|
|
587
|
-
* therefore async-and-forgotten; nothing here may throw.
|
|
828
|
+
* One SILENCE trigger handled: look, then apply. Fired from a supervision
|
|
829
|
+
* timer and therefore async-and-forgotten; nothing here may throw.
|
|
588
830
|
*
|
|
589
|
-
*
|
|
590
|
-
*
|
|
591
|
-
*
|
|
831
|
+
* A NULL DECISION HAS THREE DISTINCT QUIETS: the look budget is spent ->
|
|
832
|
+
* say so ONCE and stop supervising this child (D-5); an inspection of this
|
|
833
|
+
* child is already in flight -> re-arm and let it settle (R-P1-4); the
|
|
834
|
+
* tier is gone -> clear quietly, because the cadence tick - not the
|
|
835
|
+
* silence path - owns the degraded announcements (D-4).
|
|
592
836
|
*/
|
|
593
837
|
async inspectAndApply(dispatchId, label, trigger) {
|
|
594
838
|
const overseer = this.overseer;
|
|
@@ -609,7 +853,17 @@ export class TeamRuntime {
|
|
|
609
853
|
childAlive: true,
|
|
610
854
|
});
|
|
611
855
|
if (decision === null || this.aborted) {
|
|
612
|
-
|
|
856
|
+
if (!this.aborted && overseer.looksExhausted(label)) {
|
|
857
|
+
this.noteQuiet(dispatchId, label);
|
|
858
|
+
supervision.clear(label);
|
|
859
|
+
this.monitor?.clear(label);
|
|
860
|
+
}
|
|
861
|
+
else if (!this.aborted && overseer.isInFlight(label)) {
|
|
862
|
+
supervision.rearmSilence(label);
|
|
863
|
+
}
|
|
864
|
+
else {
|
|
865
|
+
supervision.clear(label);
|
|
866
|
+
}
|
|
613
867
|
return;
|
|
614
868
|
}
|
|
615
869
|
// The child may have settled while the call was in flight; an
|
|
@@ -621,9 +875,10 @@ export class TeamRuntime {
|
|
|
621
875
|
this.emit({ type: 'overseer', dispatchId, label, decision, trigger });
|
|
622
876
|
if (settled) {
|
|
623
877
|
supervision.clear(label);
|
|
878
|
+
this.monitor?.clear(label);
|
|
624
879
|
return;
|
|
625
880
|
}
|
|
626
|
-
this.applyDecision(
|
|
881
|
+
this.applyDecision(handle, label, decision);
|
|
627
882
|
}
|
|
628
883
|
catch {
|
|
629
884
|
// Defence in depth around an async timer callback: an unexpected throw
|
|
@@ -631,49 +886,88 @@ export class TeamRuntime {
|
|
|
631
886
|
supervision.rearmSilence(label);
|
|
632
887
|
}
|
|
633
888
|
}
|
|
634
|
-
applyDecision(
|
|
889
|
+
applyDecision(handle, label, decision) {
|
|
890
|
+
const supervision = this.supervision;
|
|
891
|
+
const overseer = this.overseer;
|
|
892
|
+
if (supervision === null || overseer === null)
|
|
893
|
+
return;
|
|
635
894
|
switch (decision.action) {
|
|
636
895
|
case 'wait':
|
|
637
|
-
|
|
896
|
+
// A model-chosen interval OVERRIDES the rung and becomes it (D-2);
|
|
897
|
+
// without one the rung is untouched - the silence window is the only
|
|
898
|
+
// clock this path re-arms.
|
|
899
|
+
if (decision.nextCheckMs !== undefined) {
|
|
900
|
+
this.monitor?.override(label, decision.nextCheckMs);
|
|
901
|
+
}
|
|
638
902
|
supervision.rearmSilence(label);
|
|
639
903
|
break;
|
|
640
|
-
case 'nudge':
|
|
904
|
+
case 'nudge': {
|
|
905
|
+
// THE NUDGE BUDGET IS CHECKED BEFORE STEERING (D-5): a fourth nudge
|
|
906
|
+
// is not advice, it is the supervisor refusing to decide - it waits.
|
|
907
|
+
if (!overseer.nudgesLeft(label)) {
|
|
908
|
+
supervision.rearmSilence(label);
|
|
909
|
+
break;
|
|
910
|
+
}
|
|
641
911
|
// I-OV4: never steer into an abort already requested — the same
|
|
642
912
|
// guard-1 fact the child's fast reviewer reads. Queued steering is
|
|
643
913
|
// drained at the safe top-of-loop checkpoint by core.
|
|
644
914
|
if (!handle.isAborted() && decision.guidance !== undefined) {
|
|
645
915
|
handle.agent.steer(decision.guidance);
|
|
916
|
+
overseer.noteNudge(label);
|
|
646
917
|
}
|
|
647
|
-
|
|
918
|
+
this.monitor?.reset(label);
|
|
648
919
|
supervision.rearmSilence(label);
|
|
649
920
|
break;
|
|
921
|
+
}
|
|
650
922
|
case 'replace': {
|
|
651
|
-
//
|
|
923
|
+
// Budgets checked BEFORE the verdict is set, so two near-simultaneous
|
|
652
924
|
// replace decisions cannot both rebuild (the second becomes an
|
|
653
925
|
// abandon with the budget stated in the reason).
|
|
654
|
-
|
|
655
|
-
|
|
926
|
+
if (overseer.replacementsLeft(label)) {
|
|
927
|
+
handle.overseerVerdict = {
|
|
656
928
|
action: 'replace',
|
|
657
929
|
reason: decision.reason,
|
|
658
930
|
...(decision.guidance !== undefined ? { guidance: decision.guidance } : {}),
|
|
659
|
-
}
|
|
660
|
-
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
else if (overseer.abandonsLeft(label)) {
|
|
934
|
+
handle.overseerVerdict = {
|
|
935
|
+
action: 'abandon',
|
|
936
|
+
reason: `replacement budget exhausted; ${decision.reason}`,
|
|
937
|
+
};
|
|
938
|
+
overseer.noteAbandon(label);
|
|
939
|
+
}
|
|
940
|
+
else {
|
|
941
|
+
// Both mutation budgets spent: the honest reading is wait (I-OV2).
|
|
942
|
+
supervision.rearmSilence(label);
|
|
943
|
+
this.monitor?.advance(label);
|
|
944
|
+
break;
|
|
945
|
+
}
|
|
661
946
|
supervision.clear(label);
|
|
947
|
+
this.monitor?.clear(label);
|
|
662
948
|
handle.abort();
|
|
663
949
|
break;
|
|
664
950
|
}
|
|
665
951
|
case 'abandon':
|
|
952
|
+
if (!overseer.abandonsLeft(label)) {
|
|
953
|
+
supervision.rearmSilence(label);
|
|
954
|
+
this.monitor?.advance(label);
|
|
955
|
+
break;
|
|
956
|
+
}
|
|
666
957
|
handle.overseerVerdict = { action: 'abandon', reason: decision.reason };
|
|
958
|
+
overseer.noteAbandon(label);
|
|
667
959
|
supervision.clear(label);
|
|
960
|
+
this.monitor?.clear(label);
|
|
668
961
|
handle.abort();
|
|
669
962
|
break;
|
|
670
963
|
}
|
|
671
964
|
}
|
|
672
965
|
/** Record one applied decision on the outcome, the run and the event stream. */
|
|
673
966
|
recordIntervention(dispatchId, label, trigger, decision) {
|
|
967
|
+
const at = Date.now();
|
|
674
968
|
this.interventions.push({
|
|
675
969
|
label,
|
|
676
|
-
at
|
|
970
|
+
at,
|
|
677
971
|
action: decision.action,
|
|
678
972
|
reason: decision.reason,
|
|
679
973
|
trigger,
|
|
@@ -681,9 +975,116 @@ export class TeamRuntime {
|
|
|
681
975
|
const handle = this.live.find((h) => h.run.label === label);
|
|
682
976
|
if (handle !== undefined) {
|
|
683
977
|
handle.run.interventions = (handle.run.interventions ?? 0) + 1;
|
|
978
|
+
// The panel's badge (AC-5): ABSOLUTE `at`, same units as `startedAt`
|
|
979
|
+
// (R-P2-3), so rendering is one subtraction.
|
|
980
|
+
handle.run.lastIntervention = {
|
|
981
|
+
action: decision.action,
|
|
982
|
+
at,
|
|
983
|
+
reasonHead: decision.reason.slice(0, 60),
|
|
984
|
+
};
|
|
684
985
|
this.publish(dispatchId, handle.run);
|
|
685
986
|
}
|
|
686
987
|
}
|
|
988
|
+
// -------------------------------------------------------------------------
|
|
989
|
+
// The cadence loop's callbacks (subagent-overseer-v2 D-2 / D-3 / D-4)
|
|
990
|
+
// -------------------------------------------------------------------------
|
|
991
|
+
/** R-P1-5: a label ticks only once STARTED and not yet settled. */
|
|
992
|
+
isMonitorLive(label) {
|
|
993
|
+
const handle = this.live.find((h) => h.run.label === label);
|
|
994
|
+
if (handle === undefined || handle.run.startedAt === undefined)
|
|
995
|
+
return false;
|
|
996
|
+
const phase = handle.run.phase;
|
|
997
|
+
return phase !== 'done' && phase !== 'failed' && phase !== 'aborted';
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* One ASSISTED cadence tick: build the batch over the due labels, apply
|
|
1001
|
+
* every returned decision through the same `applyDecision` the silence
|
|
1002
|
+
* path uses (D-3's single application path), and hand the map back so the
|
|
1003
|
+
* loop can move the ladders. Labels the batch skipped (single-flight,
|
|
1004
|
+
* R-P1-4) are absent from the map.
|
|
1005
|
+
*/
|
|
1006
|
+
async monitorInspectBatch(dispatchId, labels) {
|
|
1007
|
+
const overseer = this.overseer;
|
|
1008
|
+
if (overseer === null || this.aborted)
|
|
1009
|
+
return new Map();
|
|
1010
|
+
const reqs = [];
|
|
1011
|
+
for (const label of labels) {
|
|
1012
|
+
const handle = this.live.find((h) => h.run.label === label);
|
|
1013
|
+
if (handle === undefined)
|
|
1014
|
+
continue;
|
|
1015
|
+
reqs.push({
|
|
1016
|
+
run: handle.run,
|
|
1017
|
+
messages: handle.agent.state.messages,
|
|
1018
|
+
trigger: 'clock',
|
|
1019
|
+
childAlive: true,
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
const decisions = await overseer.inspectBatch(reqs);
|
|
1023
|
+
for (const [label, decision] of decisions) {
|
|
1024
|
+
if (this.aborted)
|
|
1025
|
+
break;
|
|
1026
|
+
const handle = this.live.find((h) => h.run.label === label);
|
|
1027
|
+
const settled = handle === undefined ||
|
|
1028
|
+
handle.run.phase === 'done' ||
|
|
1029
|
+
handle.run.phase === 'failed' ||
|
|
1030
|
+
handle.run.phase === 'aborted';
|
|
1031
|
+
this.recordIntervention(dispatchId, label, 'clock', decision);
|
|
1032
|
+
this.emit({ type: 'overseer', dispatchId, label, decision, trigger: 'clock' });
|
|
1033
|
+
if (settled) {
|
|
1034
|
+
this.monitor?.clear(label);
|
|
1035
|
+
continue;
|
|
1036
|
+
}
|
|
1037
|
+
this.applyDecision(handle, label, decision);
|
|
1038
|
+
}
|
|
1039
|
+
return decisions;
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* One DEGRADED cadence tick (D-4 / D-10): the fast tier is missing right
|
|
1043
|
+
* now. The children wait unassisted - announced per child through the same
|
|
1044
|
+
* event shape, never killed - and the fact is edge-latched here, NOT in
|
|
1045
|
+
* the interventions ledger.
|
|
1046
|
+
*/
|
|
1047
|
+
noteDegradedTick(dispatchId, labels) {
|
|
1048
|
+
this.overseerDegraded = true;
|
|
1049
|
+
this.overseerDegradedTicks += 1;
|
|
1050
|
+
for (const label of labels) {
|
|
1051
|
+
this.emit({
|
|
1052
|
+
type: 'overseer',
|
|
1053
|
+
dispatchId,
|
|
1054
|
+
label,
|
|
1055
|
+
decision: DEGRADED_WAIT_DECISION,
|
|
1056
|
+
trigger: 'clock',
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
/** The supervisor goes quiet on one child, ONCE (D-5 / D-10). */
|
|
1061
|
+
noteQuiet(dispatchId, label) {
|
|
1062
|
+
if (this.quietNotified.has(label))
|
|
1063
|
+
return;
|
|
1064
|
+
this.quietNotified.add(label);
|
|
1065
|
+
const decision = {
|
|
1066
|
+
action: 'wait',
|
|
1067
|
+
reason: 'look budget exhausted; supervision quiet for this child',
|
|
1068
|
+
};
|
|
1069
|
+
this.recordIntervention(dispatchId, label, 'quiet', decision);
|
|
1070
|
+
this.emit({ type: 'overseer', dispatchId, label, decision, trigger: 'quiet' });
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* The dispatch-wide fool-proof total is spent (R-P1-6): stop the whole
|
|
1074
|
+
* loop and emit ONE aggregate notice under a pseudo-label that is never a
|
|
1075
|
+
* child slug.
|
|
1076
|
+
*/
|
|
1077
|
+
noteDispatchExhausted(dispatchId) {
|
|
1078
|
+
this.monitor?.stop();
|
|
1079
|
+
this.recordIntervention(dispatchId, MONITOR_TEAM_LABEL, 'quiet', MONITOR_EXHAUSTED_DECISION);
|
|
1080
|
+
this.emit({
|
|
1081
|
+
type: 'overseer',
|
|
1082
|
+
dispatchId,
|
|
1083
|
+
label: MONITOR_TEAM_LABEL,
|
|
1084
|
+
decision: MONITOR_EXHAUSTED_DECISION,
|
|
1085
|
+
trigger: 'quiet',
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
687
1088
|
/**
|
|
688
1089
|
* The final phase of an attempt, in a FIXED ORDER: aborted, then timed out,
|
|
689
1090
|
* then error, then no-summary, then done.
|
|
@@ -755,6 +1156,10 @@ export class TeamRuntime {
|
|
|
755
1156
|
}
|
|
756
1157
|
this.bus?.cancelAllWaits();
|
|
757
1158
|
this.supervision?.clearAll();
|
|
1159
|
+
// The cadence loop too: every child is being aborted, so no tick has
|
|
1160
|
+
// anything left to look at (an in-flight batch's decisions are dropped
|
|
1161
|
+
// by the aborted guard in `monitorInspectBatch`).
|
|
1162
|
+
this.monitor?.stop();
|
|
758
1163
|
// Every in-flight retry backoff, not just the last one registered: a
|
|
759
1164
|
// provider-wide failure puts up to `maxConcurrent` children here at once.
|
|
760
1165
|
for (const release of [...this.pendingBackoffs])
|
|
@@ -766,6 +1171,8 @@ export class TeamRuntime {
|
|
|
766
1171
|
if (this.dispatchTimer)
|
|
767
1172
|
clearTimeout(this.dispatchTimer);
|
|
768
1173
|
this.dispatchTimer = null;
|
|
1174
|
+
this.monitor?.dispose();
|
|
1175
|
+
this.monitor = null;
|
|
769
1176
|
this.supervision?.clearAll();
|
|
770
1177
|
this.supervision = null;
|
|
771
1178
|
this.overseer = null;
|