@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/normalize.d.ts
CHANGED
|
@@ -17,32 +17,18 @@
|
|
|
17
17
|
* whether an optional install step succeeded.
|
|
18
18
|
*
|
|
19
19
|
* NEVER THROWS. Zero survivors is the caller's single hard failure.
|
|
20
|
+
*
|
|
21
|
+
* EVERY CHILD RUNS THE LEAD'S MODEL (main-agent parity): the former
|
|
22
|
+
* `model: "fast"` per-child tier was removed, so a spec that still carries a
|
|
23
|
+
* `model` key simply ignores it - there is no tier to downgrade TO, and the
|
|
24
|
+
* honest reading of a stale key is silence, not a report line about a
|
|
25
|
+
* capability that no longer exists.
|
|
20
26
|
*/
|
|
21
27
|
import type { SubagentSpec } from './types.js';
|
|
22
28
|
export interface NormalizedSpecs {
|
|
23
29
|
specs: SubagentSpec[];
|
|
24
30
|
/** How many entries the model asked for, before dropping and capping. */
|
|
25
31
|
requested: number;
|
|
26
|
-
/**
|
|
27
|
-
* Specs that asked for `model:"fast"` and were downgraded to `main`
|
|
28
|
-
* (fast-model-tier §3.4 / R-6).
|
|
29
|
-
*
|
|
30
|
-
* ADDITIVE: `NormalizedSpecs` GAINS a field, it does not change one, so every
|
|
31
|
-
* existing caller and test compiles unchanged.
|
|
32
|
-
*/
|
|
33
|
-
downgraded: number;
|
|
34
|
-
}
|
|
35
|
-
/** Options for the fast tier. Optional for the reason `normalizeSubagentSpecs`
|
|
36
|
-
* documents below (RV-14). */
|
|
37
|
-
export interface NormalizeOptions {
|
|
38
|
-
/**
|
|
39
|
-
* §3.3's LIVE predicate, evaluated at DISPATCH time (RV-3).
|
|
40
|
-
*
|
|
41
|
-
* `fast.delegate: false` and "the tier stopped resolving twenty minutes ago"
|
|
42
|
-
* are the same observable to the model — a downgrade, counted and reported —
|
|
43
|
-
* which is the honest reading of both.
|
|
44
|
-
*/
|
|
45
|
-
fastAvailable?: boolean;
|
|
46
32
|
}
|
|
47
33
|
/**
|
|
48
34
|
* Repair and bound a raw `subagents` array.
|
|
@@ -55,10 +41,5 @@ export interface NormalizeOptions {
|
|
|
55
41
|
* @param max The resolved `team.maxSubagents`; further clamped to
|
|
56
42
|
* `TEAM_LIMITS.hardMaxSubagents` here so a hand-edited config file
|
|
57
43
|
* cannot raise the requirement's ceiling (R-g).
|
|
58
|
-
* @param opts Fast-tier availability. OPTIONAL, DEFAULTING TO `false` (RV-14):
|
|
59
|
-
* this function and `NormalizedSpecs` are both exported and both
|
|
60
|
-
* have existing callers and tests, so a required third parameter
|
|
61
|
-
* would be a breaking edit that buys nothing — and an omitted one
|
|
62
|
-
* now means exactly what a pre-feature build meant.
|
|
63
44
|
*/
|
|
64
|
-
export declare function normalizeSubagentSpecs(raw: unknown, max: number
|
|
45
|
+
export declare function normalizeSubagentSpecs(raw: unknown, max: number): NormalizedSpecs;
|
package/dist/team/normalize.js
CHANGED
|
@@ -17,6 +17,12 @@
|
|
|
17
17
|
* whether an optional install step succeeded.
|
|
18
18
|
*
|
|
19
19
|
* NEVER THROWS. Zero survivors is the caller's single hard failure.
|
|
20
|
+
*
|
|
21
|
+
* EVERY CHILD RUNS THE LEAD'S MODEL (main-agent parity): the former
|
|
22
|
+
* `model: "fast"` per-child tier was removed, so a spec that still carries a
|
|
23
|
+
* `model` key simply ignores it - there is no tier to downgrade TO, and the
|
|
24
|
+
* honest reading of a stale key is silence, not a report line about a
|
|
25
|
+
* capability that no longer exists.
|
|
20
26
|
*/
|
|
21
27
|
import { TEAM_LIMITS } from './limits.js';
|
|
22
28
|
function asString(value) {
|
|
@@ -76,21 +82,13 @@ function dedupeLabel(label, taken) {
|
|
|
76
82
|
* @param max The resolved `team.maxSubagents`; further clamped to
|
|
77
83
|
* `TEAM_LIMITS.hardMaxSubagents` here so a hand-edited config file
|
|
78
84
|
* cannot raise the requirement's ceiling (R-g).
|
|
79
|
-
* @param opts Fast-tier availability. OPTIONAL, DEFAULTING TO `false` (RV-14):
|
|
80
|
-
* this function and `NormalizedSpecs` are both exported and both
|
|
81
|
-
* have existing callers and tests, so a required third parameter
|
|
82
|
-
* would be a breaking edit that buys nothing — and an omitted one
|
|
83
|
-
* now means exactly what a pre-feature build meant.
|
|
84
85
|
*/
|
|
85
|
-
export function normalizeSubagentSpecs(raw, max
|
|
86
|
+
export function normalizeSubagentSpecs(raw, max) {
|
|
86
87
|
if (!Array.isArray(raw))
|
|
87
|
-
return { specs: [], requested: 0
|
|
88
|
+
return { specs: [], requested: 0 };
|
|
88
89
|
const requested = raw.length;
|
|
89
|
-
const fastAvailable = opts.fastAvailable === true;
|
|
90
90
|
const kept = [];
|
|
91
91
|
const taken = new Set();
|
|
92
|
-
/** Parallel to `kept`: did entry `i` ask for the fast tier? */
|
|
93
|
-
const wantedFast = [];
|
|
94
92
|
for (let i = 0; i < raw.length; i += 1) {
|
|
95
93
|
const item = raw[i];
|
|
96
94
|
if (!item || typeof item !== 'object')
|
|
@@ -102,25 +100,14 @@ export function normalizeSubagentSpecs(raw, max, opts = {}) {
|
|
|
102
100
|
const prompt = clamp(asString(src.prompt), TEAM_LIMITS.promptChars);
|
|
103
101
|
if (prompt.length === 0)
|
|
104
102
|
continue;
|
|
105
|
-
// REPAIR, NEVER REJECT — this file's own contract. `'fast'` without
|
|
106
|
-
// availability becomes `'main'` AND IS COUNTED; anything else (a typo, a
|
|
107
|
-
// model id, `undefined`) is simply `'main'` and is NOT counted, because the
|
|
108
|
-
// model did not ask for the fast tier and has nothing to be told about.
|
|
109
|
-
const wantsFast = src.model === 'fast';
|
|
110
|
-
wantedFast.push(wantsFast);
|
|
111
103
|
kept.push({
|
|
112
104
|
label: dedupeLabel(slugLabel(src.label, kept.length), taken),
|
|
113
105
|
description,
|
|
114
106
|
prompt,
|
|
115
107
|
readOnly: src.readOnly === true,
|
|
116
|
-
tier: wantsFast && fastAvailable ? 'fast' : 'main',
|
|
117
108
|
});
|
|
118
109
|
}
|
|
119
110
|
const ceiling = Math.max(1, Math.min(Math.floor(max) || 1, TEAM_LIMITS.hardMaxSubagents));
|
|
120
111
|
const specs = kept.slice(0, ceiling);
|
|
121
|
-
|
|
122
|
-
// cap dropped never ran at all, so reporting it as "ran on the main model"
|
|
123
|
-
// would be a second, different lie in a line that exists to stop the first.
|
|
124
|
-
const downgraded = specs.filter((spec, i) => spec.tier === 'main' && wantedFast[i] === true).length;
|
|
125
|
-
return { specs, requested, downgraded };
|
|
112
|
+
return { specs, requested };
|
|
126
113
|
}
|
|
@@ -0,0 +1,157 @@
|
|
|
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 type { AssistantMessage, LLMRequest, Message } from '@aragon-agent/core';
|
|
28
|
+
import type { ModelRole } from '../config/model-profiles.js';
|
|
29
|
+
import type { CliConfig } from '../config/schema.js';
|
|
30
|
+
import type { OverseerAction, OverseerDecision, SubagentRun, SubagentSpec } from './types.js';
|
|
31
|
+
/** One remembered decision, so the second look knows the first happened. */
|
|
32
|
+
export interface OverseerMemoryEntry {
|
|
33
|
+
/** Seconds into the dispatch. */
|
|
34
|
+
atSec: number;
|
|
35
|
+
action: OverseerAction;
|
|
36
|
+
reason: string;
|
|
37
|
+
}
|
|
38
|
+
/** Everything one digest is built from. Structural, so tests stub it whole. */
|
|
39
|
+
export interface ChildDigestInput {
|
|
40
|
+
run: SubagentRun;
|
|
41
|
+
/** False only in the post-mortem inspection, after the child settled. */
|
|
42
|
+
childAlive: boolean;
|
|
43
|
+
trigger: 'silence' | 'clock' | 'postmortem';
|
|
44
|
+
/** The child's LIVE history (`SubagentAgentLike.state.messages`). */
|
|
45
|
+
messages: readonly Message[];
|
|
46
|
+
memory: readonly OverseerMemoryEntry[];
|
|
47
|
+
/** Seconds the child has been running. */
|
|
48
|
+
elapsedSec: number;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Build the bounded text one inspection reads.
|
|
52
|
+
*
|
|
53
|
+
* FACTS FIRST, HISTORY SECOND: the byte clamp (`overseerDigestBytes`, via the
|
|
54
|
+
* same `truncateBytes` the report uses so a CJK tail cannot be split mid
|
|
55
|
+
* sequence) bites the tail, and the tail is the least decisive part — a
|
|
56
|
+
* supervisor that cannot see the phase, the retry state and its own memory
|
|
57
|
+
* is worse than one missing the fifth message back.
|
|
58
|
+
*/
|
|
59
|
+
export declare function buildChildDigest(input: ChildDigestInput): string;
|
|
60
|
+
/** Clamp a model-chosen next check into the structural range. */
|
|
61
|
+
export declare function clampNextCheckMs(ms: unknown): number | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* Normalize a raw (JSON-parsed or garbage) decision into a safe one.
|
|
64
|
+
*
|
|
65
|
+
* `childAlive === false` PHYSICALLY REMOVES two actions: a dead child can be
|
|
66
|
+
* replaced or abandoned, never waited on or nudged, so those normalize to
|
|
67
|
+
* `abandon` rather than being rejected — a post-mortem answer the protocol
|
|
68
|
+
* cannot honour must still produce a decision the runtime can apply.
|
|
69
|
+
*/
|
|
70
|
+
export declare function normalizeOverseerDecision(raw: unknown, childAlive: boolean): OverseerDecision;
|
|
71
|
+
/**
|
|
72
|
+
* Parse the model's answer text as one JSON decision object.
|
|
73
|
+
*
|
|
74
|
+
* Forgiving on PURPOSE: models wrap JSON in prose and fences. The FIRST `{`
|
|
75
|
+
* to the LAST `}` is the only span tried, and anything that does not parse is
|
|
76
|
+
* `wait` — the caller never sees a throw from parsing.
|
|
77
|
+
*/
|
|
78
|
+
export declare function parseOverseerDecisionText(text: string, childAlive: boolean): OverseerDecision;
|
|
79
|
+
/** The slice the runtime needs: "can you look?" and "decide". */
|
|
80
|
+
export interface OverseerProvider {
|
|
81
|
+
/** Live predicate, re-read per dispatch AND per inspection. */
|
|
82
|
+
active(): boolean;
|
|
83
|
+
/** One bounded decision call. Implementations must not throw. */
|
|
84
|
+
inspect(req: {
|
|
85
|
+
label: string;
|
|
86
|
+
goal: string;
|
|
87
|
+
digest: string;
|
|
88
|
+
}): Promise<OverseerDecision>;
|
|
89
|
+
}
|
|
90
|
+
/** Everything the fast-tier-backed provider needs from the session. */
|
|
91
|
+
export interface FastOverseerDeps {
|
|
92
|
+
/** Read LIVE: a mid-dispatch `/fast off` must reach the next inspection. */
|
|
93
|
+
getConfig: () => CliConfig;
|
|
94
|
+
hasKey: (providerId: string, role?: ModelRole) => boolean;
|
|
95
|
+
getApiKey: (providerId: string, role?: ModelRole) => string | undefined;
|
|
96
|
+
/** `FastWiring.completeViaFastRegistry` — the shared fail-fast transport. */
|
|
97
|
+
complete: (providerId: string, request: LLMRequest) => Promise<AssistantMessage>;
|
|
98
|
+
/** `FastWiring.available()`. */
|
|
99
|
+
available: () => boolean;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The fast-tier-backed provider, mirroring `createChildReviewer`'s shape so
|
|
103
|
+
* the controller wires it the same way: shared transport, live predicates,
|
|
104
|
+
* fail-soft (any failure resolves to `wait` — I-OV2).
|
|
105
|
+
*/
|
|
106
|
+
export declare function createFastOverseerProvider(deps: FastOverseerDeps): OverseerProvider;
|
|
107
|
+
/** One inspection request, as the runtime builds it. */
|
|
108
|
+
export interface OverseerInspectRequest {
|
|
109
|
+
run: SubagentRun;
|
|
110
|
+
messages: readonly Message[];
|
|
111
|
+
trigger: 'silence' | 'clock' | 'postmortem';
|
|
112
|
+
childAlive: boolean;
|
|
113
|
+
}
|
|
114
|
+
export interface TeamOverseerOptions {
|
|
115
|
+
/** The dispatch's start, so memory entries read as "N s in". */
|
|
116
|
+
startedAt: number;
|
|
117
|
+
now?: () => number;
|
|
118
|
+
}
|
|
119
|
+
export declare const OVERSEER_DEFAULT_NEXT_CHECK_MS: 300000;
|
|
120
|
+
/**
|
|
121
|
+
* Per-dispatch supervisor state: memory, budgets and the single-flight guard.
|
|
122
|
+
*
|
|
123
|
+
* THE CLASS NEVER TOUCHES A CHILD (I-OV1). It looks and decides; `runtime.ts`
|
|
124
|
+
* steers, flags, aborts and rebuilds, because only the worker loop can apply
|
|
125
|
+
* a lifecycle change without orphaning the replacement.
|
|
126
|
+
*/
|
|
127
|
+
export declare class TeamOverseer {
|
|
128
|
+
private readonly provider;
|
|
129
|
+
private readonly options;
|
|
130
|
+
private readonly memory;
|
|
131
|
+
private readonly inspections;
|
|
132
|
+
private readonly replacements;
|
|
133
|
+
private readonly inFlight;
|
|
134
|
+
constructor(provider: OverseerProvider, options: TeamOverseerOptions);
|
|
135
|
+
/** Never throws: a provider bug must not take the dispatch down. */
|
|
136
|
+
isActive(): boolean;
|
|
137
|
+
inspectionsExhausted(label: string): boolean;
|
|
138
|
+
replacementsLeft(label: string): boolean;
|
|
139
|
+
/** Called by the runtime when it APPLIES a replace, not when it merely reads one. */
|
|
140
|
+
noteReplacement(label: string): void;
|
|
141
|
+
/**
|
|
142
|
+
* ONE inspection. `null` means QUIET — not active, no budget left, or one
|
|
143
|
+
* already in flight for this child — and the caller then does nothing,
|
|
144
|
+
* which is exactly what quiet should do.
|
|
145
|
+
*/
|
|
146
|
+
inspect(req: OverseerInspectRequest): Promise<OverseerDecision | null>;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Prepend the supervisor's note to a spec's prompt, bounding the TOTAL to
|
|
150
|
+
* `TEAM_LIMITS.promptChars` so the replacement fits exactly what a
|
|
151
|
+
* model-supplied spec would have been allowed to carry.
|
|
152
|
+
*
|
|
153
|
+
* THE NOTE TELLS THE FRESH CHILD WHAT THE DEAD ONE DID (`filesTouched` is
|
|
154
|
+
* best-effort, the same caveat the report's conflict warning carries): a
|
|
155
|
+
* replacement that rewrites a half-written file blind is not a recovery.
|
|
156
|
+
*/
|
|
157
|
+
export declare function buildReplacementPrompt(spec: SubagentSpec, decision: OverseerDecision, previousRun: SubagentRun): string;
|
|
@@ -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/prompt.d.ts
CHANGED
|
@@ -18,17 +18,6 @@ export { TEAM_BLOCK_VERSION };
|
|
|
18
18
|
export interface TeamBlockParams {
|
|
19
19
|
maxSubagents: number;
|
|
20
20
|
maxConcurrent: number;
|
|
21
|
-
/**
|
|
22
|
-
* Whether `model:"fast"` is honoured on `task` right now (fast-model-tier
|
|
23
|
-
* §3.8).
|
|
24
|
-
*
|
|
25
|
-
* ONE SENTENCE, AND ONLY WHEN IT IS TRUE. `<fast_tier>` carries the full
|
|
26
|
-
* guidance; this is the cross-reference that stops a model reading
|
|
27
|
-
* `<team_mode>` in isolation and concluding every child costs the same. With
|
|
28
|
-
* it absent the block is BYTE-IDENTICAL to the pre-feature output, which is
|
|
29
|
-
* what `--no-fast` and every existing snapshot depend on.
|
|
30
|
-
*/
|
|
31
|
-
fastDelegation?: boolean;
|
|
32
21
|
}
|
|
33
22
|
/**
|
|
34
23
|
* The lead's `<team_mode>` block. Under 1200 characters on purpose: it is paid
|
package/dist/team/prompt.js
CHANGED
|
@@ -44,19 +44,10 @@ export function buildTeamBlock(params) {
|
|
|
44
44
|
'',
|
|
45
45
|
'Each subagent starts with no memory of this conversation, so its prompt must',
|
|
46
46
|
'carry everything it needs: the goal, the files or areas it owns, and what to',
|
|
47
|
-
'report back. Give each one a disjoint set of files to write.
|
|
48
|
-
'
|
|
47
|
+
'report back. Give each one a disjoint set of files to write. Every subagent',
|
|
48
|
+
'runs on your own model. Subagents cannot dispatch further subagents and',
|
|
49
|
+
'cannot ask the user anything.',
|
|
49
50
|
'',
|
|
50
|
-
// THE TRAILING BLANK IS INSIDE THE CONDITIONAL, not outside it. An
|
|
51
|
-
// unconditional `''` here would add a line to EVERY team-enabled prompt and
|
|
52
|
-
// silently break the byte-identity `--no-fast` depends on (I-2).
|
|
53
|
-
...(params.fastDelegation
|
|
54
|
-
? [
|
|
55
|
-
'A subagent doing mechanical, high-volume work can run on the cheaper fast',
|
|
56
|
-
'model - see <fast_tier>.',
|
|
57
|
-
'',
|
|
58
|
-
]
|
|
59
|
-
: []),
|
|
60
51
|
'You get one combined report when they all finish. Read it before deciding what',
|
|
61
52
|
'to do next, and tell the user what the team found in your own words.',
|
|
62
53
|
'</team_mode>',
|