@aragon-agent/cli 0.6.16 → 0.6.17

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.
Files changed (55) hide show
  1. package/README.md +44 -47
  2. package/dist/agent/controller.js +39 -47
  3. package/dist/agent/reducer.js +0 -4
  4. package/dist/cli.js +0 -13
  5. package/dist/commands/builtins.js +3 -17
  6. package/dist/compaction/child.d.ts +10 -8
  7. package/dist/compaction/child.js +23 -25
  8. package/dist/compaction/limits.d.ts +0 -2
  9. package/dist/compaction/limits.js +0 -2
  10. package/dist/config/cli-commands.d.ts +1 -1
  11. package/dist/config/cli-commands.js +16 -15
  12. package/dist/config/load.d.ts +0 -17
  13. package/dist/config/load.js +0 -6
  14. package/dist/config/schema.d.ts +17 -5
  15. package/dist/config/schema.js +14 -11
  16. package/dist/exec/events.d.ts +12 -8
  17. package/dist/exec/events.js +2 -2
  18. package/dist/exec/runner.js +4 -1
  19. package/dist/fast/limits.d.ts +2 -2
  20. package/dist/fast/limits.js +2 -2
  21. package/dist/fast/prompt.d.ts +10 -11
  22. package/dist/fast/prompt.js +19 -20
  23. package/dist/fast/reviewer.d.ts +1 -1
  24. package/dist/fast/reviewer.js +1 -1
  25. package/dist/fast/types.d.ts +0 -2
  26. package/dist/fast/wiring.d.ts +18 -34
  27. package/dist/fast/wiring.js +20 -51
  28. package/dist/session/validate-session.js +3 -1
  29. package/dist/team/child-reviewer.d.ts +105 -0
  30. package/dist/team/child-reviewer.js +117 -0
  31. package/dist/team/normalize.d.ts +7 -26
  32. package/dist/team/normalize.js +9 -22
  33. package/dist/team/prompt.d.ts +0 -11
  34. package/dist/team/prompt.js +3 -12
  35. package/dist/team/report.d.ts +4 -19
  36. package/dist/team/report.js +1 -27
  37. package/dist/team/runtime.d.ts +5 -10
  38. package/dist/team/runtime.js +35 -39
  39. package/dist/team/subagent.d.ts +18 -18
  40. package/dist/team/subagent.js +80 -30
  41. package/dist/team/task-tool.d.ts +6 -26
  42. package/dist/team/task-tool.js +13 -47
  43. package/dist/team/types.d.ts +3 -42
  44. package/dist/ui/App.js +3 -4
  45. package/dist/ui/BottomStatusRow.js +2 -1
  46. package/dist/ui/StatusBar.js +2 -1
  47. package/dist/ui/TeamPanel.js +1 -1
  48. package/dist/ui/entries/TeamCard.js +1 -6
  49. package/dist/ui/layout/status-detail-layout.js +12 -2
  50. package/dist/ui/layout/status-layout.d.ts +4 -2
  51. package/dist/ui/layout/status-layout.js +24 -1
  52. package/dist/ui/overlays/HelpOverlay.js +1 -2
  53. package/package.json +2 -2
  54. package/dist/ui/ModeChip.d.ts +0 -18
  55. package/dist/ui/ModeChip.js +0 -24
@@ -7,8 +7,8 @@
7
7
  * THIS CLASS EXISTS FOR A BUDGET REASON, NOT AN AESTHETIC ONE (C-12 / RV-5).
8
8
  * `CLAUDE.md` caps a source file at 1000 lines and `controller.ts` was at 960
9
9
  * before this feature, which handed it nine new responsibilities. `FastWiring`
10
- * owns the flag pair, the tier cache, the reviewer's lifetime and the
11
- * `resolveTier` closure; `AgentController` keeps one field and four thin
10
+ * owns the flag pair, the tier cache, the reviewer's lifetime and the shared
11
+ * fail-fast review transport; `AgentController` keeps one field and four thin
12
12
  * forwarders. It is still CLI-local and still holds no `ui/` import.
13
13
  *
14
14
  * TWO FLAGS AND ONE LIVE PREDICATE (§3.3), which is the shape every optional
@@ -64,7 +64,6 @@ export function offFastStatus() {
64
64
  // `budgetReached` is CARRIED and callers must never re-derive it.
65
65
  reviewBudget: 0,
66
66
  budgetReached: false,
67
- delegated: 0,
68
67
  usage: { inputTokens: 0, outputTokens: 0 },
69
68
  pricingUnknown: false,
70
69
  inFlight: false,
@@ -86,7 +85,6 @@ export class FastWiring {
86
85
  tier;
87
86
  /** Warned once per TRANSITION, so a broken tier does not shout every turn. */
88
87
  lastReportedReason = null;
89
- delegated = 0;
90
88
  constructor(deps) {
91
89
  this.deps = deps;
92
90
  this.tier = resolveFastTier(deps.getConfig(), deps.hasKey);
@@ -168,26 +166,15 @@ export class FastWiring {
168
166
  * §3.3's live predicate. THE THIRD TERM IS THE POINT - see the header.
169
167
  *
170
168
  * `FastReviewer.isSelfDisabled()` is DELIBERATELY NOT a fourth term. Folding
171
- * it in would look tidier and would break something else: `delegationAvailable()`
172
- * is `available() && config.fast.delegate === true`, so three consecutive
173
- * REVIEW failures would silently switch DELEGATION off as well - turning a
174
- * diagnostic signal into a behaviour change for every user. The two facts are
175
- * reported separately on `FastSnapshot` instead (`live` / `selfDisabled`).
169
+ * it in would look tidier and would break something else: per-child reviewers
170
+ * gate on `available()` too, so three consecutive REVIEW failures would
171
+ * silently stop every child's reviews as well - turning a diagnostic signal
172
+ * into a behaviour change for every dispatch. The two facts are reported
173
+ * separately on `FastSnapshot` instead (`live` / `selfDisabled`).
176
174
  */
177
175
  available() {
178
176
  return this.registered && this.enabled && this.tier.ok;
179
177
  }
180
- /**
181
- * Whether `model: "fast"` on `task` is honoured right now.
182
- *
183
- * Evaluated AT DISPATCH TIME, never snapshot (§3.4 / RV-3). `fast.delegate:
184
- * false` and "the tier stopped resolving twenty minutes ago" are the same
185
- * observable to the model - a downgrade, counted and reported - which is the
186
- * honest reading of both.
187
- */
188
- delegationAvailable() {
189
- return this.available() && this.deps.getConfig().fast.delegate === true;
190
- }
191
178
  setEnabled(enabled) {
192
179
  this.enabled = enabled;
193
180
  this.deps.onPromptChanged();
@@ -233,40 +220,23 @@ export class FastWiring {
233
220
  return describeFastTierProblem(this.tier, fastProviderOf(this.deps.getConfig()));
234
221
  }
235
222
  // -----------------------------------------------------------------------
236
- // Tier resolution for subagents (§3.4)
223
+ // The tier's transport, shared with per-child reviewers (§3.4)
237
224
  // -----------------------------------------------------------------------
238
- /**
239
- * The `ModelRef` and thinking level one subagent tier runs on.
240
- *
241
- * `'fast'` FALLS BACK TO MAIN when delegation is not available, so the two
242
- * guards agree BY CONSTRUCTION: the normalizer can never produce a
243
- * `tier: 'fast'` spec this factory would refuse, and this factory can never
244
- * build a child against a `ModelRef` the tier no longer resolves (RV-3).
245
- */
246
- resolveTier(tier) {
247
- const config = this.deps.getConfig();
248
- if (tier === 'fast' && this.delegationAvailable() && this.tier.ok) {
249
- return { ref: this.tier.ref, thinkingLevel: this.tier.thinkingLevel, role: 'fast' };
250
- }
251
- return {
252
- ref: {
253
- providerId: config.provider,
254
- modelId: config.model,
255
- ...(config.baseUrl ? { baseUrl: config.baseUrl } : {}),
256
- },
257
- thinkingLevel: config.thinkingLevel,
258
- role: 'main',
259
- };
260
- }
261
- /** The resolved fast `ModelRef`, or `null` - for per-tier cost lookup (§3.6). */
225
+ /** The resolved fast `ModelRef`, or `null` - for cost lookups (§3.6). */
262
226
  fastRef() {
263
227
  return this.tier.ok ? this.tier.ref : null;
264
228
  }
265
- /** Count fast-tier children so `/fast status` can answer "is this saving me
266
- * anything?" — the aggregate on the status bar cannot. */
267
- noteDelegated(count) {
268
- if (count > 0)
269
- this.delegated += count;
229
+ /**
230
+ * The fail-fast review transport, exposed so PER-CHILD reviewers share ONE
231
+ * registry instead of building one each - the same adapter-state reuse
232
+ * argument `TeamRuntime` makes about the lead's `ProviderRegistry`.
233
+ *
234
+ * Lazy: a session that never reviews allocates nothing (I-2). The retry
235
+ * policy is `FAST_RETRY_POLICY`, deliberately not the user's `retry` config
236
+ * - see `getFastRegistry()` for the reason.
237
+ */
238
+ completeViaFastRegistry(providerId, request) {
239
+ return this.getFastRegistry().complete(providerId, request);
270
240
  }
271
241
  // -----------------------------------------------------------------------
272
242
  // Reviewer lifetime
@@ -302,7 +272,6 @@ export class FastWiring {
302
272
  reviews: this.reviewer.reviewCount(),
303
273
  reviewBudget: this.reviewer.budgetLimit(),
304
274
  budgetReached: this.reviewer.isBudgetReached(),
305
- delegated: this.delegated,
306
275
  usage: this.reviewer.sessionUsage(),
307
276
  // UNKNOWN PRICING IS NOT ZERO PRICING (C-11 / RV-4). The fast tier is
308
277
  // precisely where an unrecognised model id is likely, and rendering it as
@@ -128,7 +128,9 @@ const todo = shape({
128
128
  });
129
129
  const subagent = shape({
130
130
  label: string, description: string,
131
- // Old team archives predate tier; renderers already treat absence as main.
131
+ // OPTIONAL IN BOTH DIRECTIONS: sessions written since main-agent parity
132
+ // have no tier at all, and older archives may carry `main` or `fast` from
133
+ // the removed per-child fast dispatch. Renderers treat absence as main.
132
134
  tier: optional(enumeration('main', 'fast')),
133
135
  phase: enumeration('queued', 'starting', 'thinking', 'tool', 'waiting', 'done', 'failed', 'aborted'),
134
136
  startedAt: optional(number), endedAt: optional(number), turns: number, toolCalls: number,
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Per-child fast review (main-agent parity).
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
+ * THE LEAD HAS A FAST REVIEWER; A CHILD NOW HAS ONE TOO. The requirement is
8
+ * that "when the main agent pulls up subagents, all subagents are as capable as
9
+ * the main agent", and the fast/slow cooperation is part of what the main agent
10
+ * has: a cheap second model periodically critiques the run and steers a
11
+ * `<fast_review>` block into the conversation. A child runs the lead's own
12
+ * model, and this module gives that run the same second opinion.
13
+ *
14
+ * WHAT IS SHARED, AND WHAT IS PER CHILD:
15
+ * - SHARED: the tier resolution (`resolveFastTier` on the LIVE config), the
16
+ * fail-fast review transport (`FastWiring.completeViaFastRegistry` - one
17
+ * lazy registry for the whole session, the same adapter-state reuse
18
+ * argument `TeamRuntime` makes about the lead's `ProviderRegistry`), and
19
+ * the `fast.*` policy keys. A `/fast off` mid-dispatch therefore stops
20
+ * child reviews at the same instant it stops the lead's.
21
+ * - PER CHILD: one `FastReviewer` instance, its own turn-frame ring, its own
22
+ * session budget, its own goal (the spec's description).
23
+ *
24
+ * THE REVIEWER'S PROTOCOL IS UNCHANGED, AND THAT IS THE POINT. `FastReviewer`
25
+ * steers from exactly one place (the `tool_execution_end` that brings the batch
26
+ * to zero), never steers into a requested abort, and proves delivery at the
27
+ * next `turn_start`. The two guards need host-side facts, and a child supplies
28
+ * them differently than the lead:
29
+ *
30
+ * - `userSteerCount` is ALWAYS ZERO: no human can steer a child. That makes
31
+ * the reviewer's `clearAllQueues` provably safe by construction (D-21).
32
+ * - `isAbortRequested` comes from the HANDLE-level abort flag that
33
+ * `TeamRuntime` sets BEFORE calling `agent.abort()`. Reading the agent
34
+ * alone is not enough: an Esc lands as `abortAll()`, and without the flag
35
+ * the reviewer would race the abort it was told about.
36
+ * - `emit` and `notify` LOG INSTEAD OF RENDERING. A child has no transcript
37
+ * card and no status chip; the team panel row and the dispatch report are
38
+ * where a child's internals belong, and review spend is bounded by the
39
+ * same `fast.reviewMaxPerSession` budget the lead obeys.
40
+ */
41
+ import type { AgentEvent, AssistantMessage, LLMRequest } from '@aragon-agent/core';
42
+ import type { ModelRole } from '../config/model-profiles.js';
43
+ import type { CliConfig } from '../config/schema.js';
44
+ /**
45
+ * The slice of a child `Agent` the reviewer needs beyond what
46
+ * `SubagentAgentLike` already carries. Core's `Agent` satisfies this with its
47
+ * own `steer` / `clearAllQueues`; a test stub satisfies it the same way.
48
+ */
49
+ export interface ChildReviewerAgent {
50
+ subscribe(listener: (event: AgentEvent) => void): () => void;
51
+ steer(text: string): void;
52
+ clearAllQueues(): void;
53
+ }
54
+ /** Everything one child reviewer needs to know about its child. */
55
+ export interface ChildReviewerRequest {
56
+ label: string;
57
+ /** The spec's description: the goal the critique judges the run against. */
58
+ goal: string;
59
+ agent: ChildReviewerAgent;
60
+ /** A live phase, not a terminal one - the lead's `isRunning` equivalent. */
61
+ isRunning: () => boolean;
62
+ /** Set by the HANDLE-level abort, synchronously before `agent.abort()`. */
63
+ isAbortRequested: () => boolean;
64
+ }
65
+ /** Everything the reviewer needs from the session, all of it shared. */
66
+ export interface ChildReviewerDeps {
67
+ /** Read LIVE on every call - a mid-dispatch `/fast off` must reach children. */
68
+ getConfig: () => CliConfig;
69
+ hasKey: (providerId: string, role?: ModelRole) => boolean;
70
+ getApiKey: (providerId: string, role?: ModelRole) => string | undefined;
71
+ /** `FastWiring.completeViaFastRegistry` - the shared fail-fast transport. */
72
+ complete: (providerId: string, request: LLMRequest) => Promise<AssistantMessage>;
73
+ /** `FastWiring.available()` - registered AND enabled AND resolving. */
74
+ available: () => boolean;
75
+ }
76
+ export interface ChildReviewerHandle {
77
+ dispose(): void;
78
+ }
79
+ /**
80
+ * What the controller hands `subagent.ts`, so a child's reviewer is built from
81
+ * the lead's own wiring rather than a second tier resolver.
82
+ *
83
+ * `active` AND `create` ARE SEPARATE ON PURPOSE: the child's system prompt
84
+ * must be composed BEFORE the agent exists, and `create` needs the agent. The
85
+ * two must answer the same question at the same instant, so both derive from
86
+ * `FastWiring.available() && config.fast.review` and the caller reads `active`
87
+ * once per child.
88
+ */
89
+ export interface ChildReviewProvider {
90
+ /** Whether per-child review is live RIGHT NOW (tier live AND `fast.review`). */
91
+ active: () => boolean;
92
+ /** The resolved fast model id, for the child's `<fast_tier>` prompt block. */
93
+ modelId: () => string;
94
+ /** Build one reviewer for a child; `undefined` when not active. */
95
+ create: (req: ChildReviewerRequest) => ChildReviewerHandle | undefined;
96
+ }
97
+ /**
98
+ * Build one reviewer for one child, or `undefined` when the session has no
99
+ * live review to give it.
100
+ *
101
+ * `undefined` RATHER THAN A NO-OP REVIEWER, for the same reason
102
+ * `createChildContextManager` declines: the caller then wires nothing at all,
103
+ * and a session with the fast tier off builds the child it always built.
104
+ */
105
+ export declare function createChildReviewer(req: ChildReviewerRequest, deps: ChildReviewerDeps): ChildReviewerHandle | undefined;
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Per-child fast review (main-agent parity).
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
+ * THE LEAD HAS A FAST REVIEWER; A CHILD NOW HAS ONE TOO. The requirement is
8
+ * that "when the main agent pulls up subagents, all subagents are as capable as
9
+ * the main agent", and the fast/slow cooperation is part of what the main agent
10
+ * has: a cheap second model periodically critiques the run and steers a
11
+ * `<fast_review>` block into the conversation. A child runs the lead's own
12
+ * model, and this module gives that run the same second opinion.
13
+ *
14
+ * WHAT IS SHARED, AND WHAT IS PER CHILD:
15
+ * - SHARED: the tier resolution (`resolveFastTier` on the LIVE config), the
16
+ * fail-fast review transport (`FastWiring.completeViaFastRegistry` - one
17
+ * lazy registry for the whole session, the same adapter-state reuse
18
+ * argument `TeamRuntime` makes about the lead's `ProviderRegistry`), and
19
+ * the `fast.*` policy keys. A `/fast off` mid-dispatch therefore stops
20
+ * child reviews at the same instant it stops the lead's.
21
+ * - PER CHILD: one `FastReviewer` instance, its own turn-frame ring, its own
22
+ * session budget, its own goal (the spec's description).
23
+ *
24
+ * THE REVIEWER'S PROTOCOL IS UNCHANGED, AND THAT IS THE POINT. `FastReviewer`
25
+ * steers from exactly one place (the `tool_execution_end` that brings the batch
26
+ * to zero), never steers into a requested abort, and proves delivery at the
27
+ * next `turn_start`. The two guards need host-side facts, and a child supplies
28
+ * them differently than the lead:
29
+ *
30
+ * - `userSteerCount` is ALWAYS ZERO: no human can steer a child. That makes
31
+ * the reviewer's `clearAllQueues` provably safe by construction (D-21).
32
+ * - `isAbortRequested` comes from the HANDLE-level abort flag that
33
+ * `TeamRuntime` sets BEFORE calling `agent.abort()`. Reading the agent
34
+ * alone is not enough: an Esc lands as `abortAll()`, and without the flag
35
+ * the reviewer would race the abort it was told about.
36
+ * - `emit` and `notify` LOG INSTEAD OF RENDERING. A child has no transcript
37
+ * card and no status chip; the team panel row and the dispatch report are
38
+ * where a child's internals belong, and review spend is bounded by the
39
+ * same `fast.reviewMaxPerSession` budget the lead obeys.
40
+ */
41
+ import { getLogger } from '../logging/logger.js';
42
+ import { resolveFastTier } from '../fast/resolve.js';
43
+ import { FastReviewer } from '../fast/reviewer.js';
44
+ /**
45
+ * A snapshot for the reviewer's self-disable announcement. The lead's
46
+ * `FastWiring.snapshot()` builds the same shape from its own reviewer; a child
47
+ * has no wiring, so this builds it from the reviewer the module itself holds.
48
+ *
49
+ * `pricingUnknown` is FALSE because the child reviewer never prices anything:
50
+ * it logs counts, not currency, and an unpriced model id therefore cannot lie
51
+ * here the way it can on the lead's status line (C-11).
52
+ */
53
+ function buildSnapshot(reviewer, tier) {
54
+ return {
55
+ live: tier.ok,
56
+ selfDisabled: reviewer?.isSelfDisabled() ?? false,
57
+ model: tier.ok ? tier.ref.modelId : '',
58
+ sameAsMain: tier.ok ? tier.sameAsMain : false,
59
+ reviews: reviewer?.reviewCount() ?? 0,
60
+ reviewBudget: reviewer?.budgetLimit() ?? 0,
61
+ budgetReached: reviewer?.isBudgetReached() ?? false,
62
+ usage: reviewer?.sessionUsage() ?? { inputTokens: 0, outputTokens: 0 },
63
+ pricingUnknown: false,
64
+ inFlight: reviewer?.isReviewInFlight() ?? false,
65
+ };
66
+ }
67
+ /**
68
+ * Build one reviewer for one child, or `undefined` when the session has no
69
+ * live review to give it.
70
+ *
71
+ * `undefined` RATHER THAN A NO-OP REVIEWER, for the same reason
72
+ * `createChildContextManager` declines: the caller then wires nothing at all,
73
+ * and a session with the fast tier off builds the child it always built.
74
+ */
75
+ export function createChildReviewer(req, deps) {
76
+ // GATED ON THE SAME THREE FACTS THE LEAD'S REVIEWER IS: the wiring is live
77
+ // (`available()`), the policy allows reviews (`fast.review`), and the tier
78
+ // resolves against the LIVE config. Re-checked, never snapshotted, so a
79
+ // mid-dispatch `/fast off` or a settings-screen model edit reaches the next
80
+ // child even when it cannot stop one already running.
81
+ const tier = resolveFastTier(deps.getConfig(), deps.hasKey);
82
+ if (!deps.available() || !deps.getConfig().fast.review || !tier.ok)
83
+ return undefined;
84
+ const log = getLogger().child('fast');
85
+ let reviewer = null;
86
+ const built = new FastReviewer({
87
+ subscribe: (listener) => req.agent.subscribe(listener),
88
+ complete: deps.complete,
89
+ steer: (text) => req.agent.steer(text),
90
+ isRunning: req.isRunning,
91
+ isAbortRequested: req.isAbortRequested,
92
+ userSteerCount: () => 0,
93
+ clearAllQueues: () => req.agent.clearAllQueues(),
94
+ getTier: () => resolveFastTier(deps.getConfig(), deps.hasKey),
95
+ getConfig: () => deps.getConfig().fast,
96
+ available: () => deps.available() && resolveFastTier(deps.getConfig(), deps.hasKey).ok,
97
+ getApiKey: (providerId) => deps.getApiKey(providerId, 'fast'),
98
+ emit: (event) => {
99
+ log.info('child_review', { label: req.label, type: event.type });
100
+ },
101
+ snapshot: () => buildSnapshot(reviewer, tier),
102
+ // A CHILD HAS NO TRANSCRIPT. The lead's warnings ride `notify` into a card
103
+ // the user reads; the same text from a background worker would claim the
104
+ // user's attention for an agent they cannot see. It is logged instead.
105
+ notify: (level, text) => {
106
+ log.warn('child_review_notice', { label: req.label, level, text });
107
+ },
108
+ });
109
+ reviewer = built;
110
+ built.setGoal(req.goal);
111
+ log.info('child_review_attached', {
112
+ label: req.label,
113
+ model: tier.ref.modelId,
114
+ reviewEveryTurns: deps.getConfig().fast.reviewEveryTurns,
115
+ });
116
+ return { dispose: () => built.dispose() };
117
+ }
@@ -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, opts?: NormalizeOptions): NormalizedSpecs;
45
+ export declare function normalizeSubagentSpecs(raw: unknown, max: number): NormalizedSpecs;
@@ -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, opts = {}) {
86
+ export function normalizeSubagentSpecs(raw, max) {
86
87
  if (!Array.isArray(raw))
87
- return { specs: [], requested: 0, downgraded: 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
- // COUNTED OVER THE SURVIVORS, not over everything the model wrote: a spec the
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
  }
@@ -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
@@ -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. Subagents cannot',
48
- 'dispatch further subagents and cannot ask the user anything.',
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>',
@@ -23,26 +23,11 @@
23
23
  import type { ModelCost } from '@aragon-agent/core';
24
24
  import type { DispatchOutcome, SubagentRun } from './types.js';
25
25
  export interface ReportOptions {
26
- /** The lead's model cost table, so the header can state real spend (R-5). */
27
- cost?: ModelCost;
28
- /**
29
- * The FAST model's cost table (fast-model-tier §3.4 / R-7).
30
- *
31
- * A SECOND TABLE, not a second number derived from the first. Summing
32
- * fast-tier tokens at the lead's price over-reports a Haiku child under a
33
- * Sonnet lead by roughly an order of magnitude, and the whole justification
34
- * for delegating to a cheaper model is the figure on this line.
35
- */
36
- fastCost?: ModelCost;
37
- /**
38
- * The static price table has never heard of the fast model (C-11 / RV-4).
26
+ /** The lead's model cost table, so the header can state real spend (R-5).
39
27
  *
40
- * UNKNOWN PRICING IS NOT ZERO PRICING. `buildRuntimeModel` hands back
41
- * `cost: { input: 0, output: 0 }` for an unrecognised id, and the fast tier is
42
- * precisely where an unrecognised id is LIKELY — so rendering it as `$0.00`
43
- * would make the feature look free while it is spending money.
44
- */
45
- fastPricingUnknown?: boolean;
28
+ * THE ONE TABLE, because every child runs the lead's own model
29
+ * (main-agent parity): one tier, one price, one honest total. */
30
+ cost?: ModelCost;
46
31
  }
47
32
  /**
48
33
  * Cut `text` to at most `maxBytes` WITHOUT splitting a UTF-8 sequence.
@@ -106,28 +106,6 @@ function buildHeader(outcome, options) {
106
106
  `(${counts}) in ${formatDuration(durationMs)}.`);
107
107
  lines.push(`Tokens: in ${formatTokens(outcome.usage.inputTokens)}, out ` +
108
108
  `${formatTokens(outcome.usage.outputTokens)}. Cost: ${formatCost(computeCost(outcome.usage, options.cost))}.`);
109
- // A SECOND LINE, NOT A COMBINED TOTAL (fast-model-tier §3.4). Two tiers with
110
- // two price tables have two answers, and folding them into one figure is the
111
- // misattribution R-7 is about. Present only when a fast child actually ran, so
112
- // an ordinary dispatch's header is byte-identical.
113
- const fastUsage = outcome.fastUsage;
114
- if (fastUsage) {
115
- // `unknown` rather than a currency amount when the model has no price table
116
- // (RV-4): a lower bound rendered as a total is the same class of lie as
117
- // pricing a Haiku child at Sonnet rates.
118
- const fastCostLabel = options.fastPricingUnknown
119
- ? 'unknown (no price table)'
120
- : formatCost(computeCost(fastUsage, options.fastCost));
121
- lines.push(`Fast tier: in ${formatTokens(fastUsage.inputTokens)}, out ` +
122
- `${formatTokens(fastUsage.outputTokens)}. Cost: ${fastCostLabel}.`);
123
- }
124
- // THE LINE THAT KEEPS A DOWNGRADE FROM BEING SILENT (R-6). Without it the
125
- // model asked for a cheap child, got an expensive one, and has no way to learn
126
- // that its cost model is wrong.
127
- if (outcome.downgraded && outcome.downgraded > 0) {
128
- const n = outcome.downgraded;
129
- lines.push(`${n} ${n === 1 ? 'subagent' : 'subagents'} ran on the main model (fast tier off).`);
130
- }
131
109
  for (const conflict of findFileConflicts(outcome.runs)) {
132
110
  const who = conflict.labels.map((l) => `"${l}"`).join(' and ');
133
111
  lines.push(`WARNING: agents ${who} both wrote ${conflict.path} - review before trusting either.`);
@@ -139,10 +117,6 @@ function buildSection(run) {
139
117
  // the only place the lead learns that a child's first attempt died before it
140
118
  // did anything (F-4).
141
119
  const retried = run.retries ? `, retried ${run.retries}x` : '';
142
- // `[fast]` after the LABEL, on the head line, which is never trimmed. Six
143
- // bytes, and it is the only place the lead learns which of its children were
144
- // cheap — the fact the whole second cost line is about.
145
- const tier = run.tier === 'fast' ? ' [fast]' : '';
146
120
  // `compacted N` in the same position `retried Nx` uses, and for the same
147
121
  // reason (context-auto-compaction-hardening §3.4.4 / W3): this is the ONLY
148
122
  // place the lead learns that a child's own history was summarized mid-task, so
@@ -150,7 +124,7 @@ function buildSection(run) {
150
124
  // rather than an unexplained one. Absent when it never happened.
151
125
  const compacted = run.compactions ? `, compacted ${run.compactions}` : '';
152
126
  const head = [
153
- `### ${run.label}${tier} "${run.description}" ${statusBadge(run)} ${elapsed(run)}, ` +
127
+ `### ${run.label} "${run.description}" ${statusBadge(run)} ${elapsed(run)}, ` +
154
128
  `${run.turns} turns, ${run.toolCalls} tools${retried}${compacted}`,
155
129
  ];
156
130
  if (run.filesTouched.length > 0)
@@ -56,11 +56,11 @@ export interface TeamRuntimeDeps {
56
56
  /** Test seam (§8.1): a stub factory keeps the whole scheduler off the network. */
57
57
  agentFactory?: SubagentAgentFactory;
58
58
  /**
59
- * Per-tier model resolution (fast-model-tier §3.4). Passed straight through to
60
- * `SubagentDeps`; absent means every child runs on the session's model, which
61
- * is the pre-feature behaviour.
59
+ * Per-child fast review (main-agent parity), passed straight through to
60
+ * `SubagentDeps`. ABSENT means the session has no fast wiring and children
61
+ * are built exactly as they were before the reviewers existed.
62
62
  */
63
- resolveTier?: SubagentDeps['resolveTier'];
63
+ review?: SubagentDeps['review'];
64
64
  /**
65
65
  * Per-child context compaction (context-auto-compaction-hardening §3.4 / W3).
66
66
  *
@@ -104,12 +104,7 @@ export declare class TeamRuntime {
104
104
  isBusy(): boolean;
105
105
  /** The live roster, for `/team` status. `null` when nothing is dispatching. */
106
106
  snapshot(): TeamSnapshot | null;
107
- dispatch(specs: SubagentSpec[], requested: number, signal?: AbortSignal,
108
- /** Carried through from the normalizer so the report can state it (§3.4 /
109
- * R-6). Optional, so every existing caller compiles unchanged. */
110
- extra?: {
111
- downgraded?: number;
112
- }): Promise<DispatchOutcome>;
107
+ dispatch(specs: SubagentSpec[], requested: number, signal?: AbortSignal): Promise<DispatchOutcome>;
113
108
  /** One slot of the pool: take the next index until the list is exhausted. */
114
109
  private worker;
115
110
  /**