@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.
Files changed (79) hide show
  1. package/CHANGELOG.md +107 -51
  2. package/README.md +91 -48
  3. package/dist/agent/controller.js +52 -47
  4. package/dist/agent/headless.js +7 -0
  5. package/dist/agent/reducer.js +0 -4
  6. package/dist/cli.js +0 -13
  7. package/dist/commands/builtins.js +21 -20
  8. package/dist/commands/index-build.d.ts +39 -0
  9. package/dist/commands/index-build.js +61 -0
  10. package/dist/compaction/child.d.ts +10 -8
  11. package/dist/compaction/child.js +23 -25
  12. package/dist/compaction/limits.d.ts +0 -2
  13. package/dist/compaction/limits.js +0 -2
  14. package/dist/config/cli-commands.d.ts +2 -2
  15. package/dist/config/cli-commands.js +19 -15
  16. package/dist/config/load.d.ts +0 -17
  17. package/dist/config/load.js +0 -6
  18. package/dist/config/schema.d.ts +41 -9
  19. package/dist/config/schema.js +18 -11
  20. package/dist/exec/events.d.ts +16 -9
  21. package/dist/exec/events.js +2 -2
  22. package/dist/exec/runner.js +17 -1
  23. package/dist/fast/limits.d.ts +2 -2
  24. package/dist/fast/limits.js +2 -2
  25. package/dist/fast/prompt.d.ts +10 -11
  26. package/dist/fast/prompt.js +19 -20
  27. package/dist/fast/reviewer.d.ts +1 -1
  28. package/dist/fast/reviewer.js +1 -1
  29. package/dist/fast/types.d.ts +0 -2
  30. package/dist/fast/wiring.d.ts +18 -34
  31. package/dist/fast/wiring.js +20 -51
  32. package/dist/input/csiu-keys.js +13 -2
  33. package/dist/input/limits.d.ts +15 -0
  34. package/dist/input/limits.js +15 -0
  35. package/dist/input/win32-input-mode.js +7 -1
  36. package/dist/logging/install.d.ts +9 -0
  37. package/dist/logging/install.js +11 -0
  38. package/dist/session/validate-session.js +3 -1
  39. package/dist/team/child-reviewer.d.ts +105 -0
  40. package/dist/team/child-reviewer.js +117 -0
  41. package/dist/team/human-queue.d.ts +11 -0
  42. package/dist/team/human-queue.js +18 -1
  43. package/dist/team/limits.d.ts +40 -0
  44. package/dist/team/limits.js +41 -0
  45. package/dist/team/normalize.d.ts +7 -26
  46. package/dist/team/normalize.js +9 -22
  47. package/dist/team/overseer.d.ts +157 -0
  48. package/dist/team/overseer.js +348 -0
  49. package/dist/team/prompt.d.ts +0 -11
  50. package/dist/team/prompt.js +3 -12
  51. package/dist/team/report.d.ts +4 -19
  52. package/dist/team/report.js +27 -28
  53. package/dist/team/runtime.d.ts +76 -16
  54. package/dist/team/runtime.js +410 -57
  55. package/dist/team/subagent.d.ts +48 -19
  56. package/dist/team/subagent.js +88 -31
  57. package/dist/team/task-tool.d.ts +6 -26
  58. package/dist/team/task-tool.js +17 -48
  59. package/dist/team/types.d.ts +65 -41
  60. package/dist/ui/App.d.ts +1 -1
  61. package/dist/ui/App.js +64 -4
  62. package/dist/ui/BottomStatusRow.js +2 -1
  63. package/dist/ui/Header.js +13 -2
  64. package/dist/ui/PromptInput.js +11 -1
  65. package/dist/ui/StatusBar.js +2 -1
  66. package/dist/ui/TeamPanel.js +1 -1
  67. package/dist/ui/entries/TeamCard.js +1 -6
  68. package/dist/ui/layout/status-detail-layout.js +12 -2
  69. package/dist/ui/layout/status-layout.d.ts +4 -2
  70. package/dist/ui/layout/status-layout.js +24 -1
  71. package/dist/ui/overlays/HelpOverlay.js +2 -2
  72. package/package.json +2 -2
  73. package/skills/project-indexer/SKILL.md +163 -0
  74. package/skills/project-indexer/templates/cleancode-overrides.md +46 -0
  75. package/skills/project-indexer/templates/cleancode-template.md +94 -0
  76. package/skills/project-indexer/templates/config-template.md +69 -0
  77. package/skills/project-indexer/templates/index-template.md +129 -0
  78. package/dist/ui/ModeChip.d.ts +0 -18
  79. package/dist/ui/ModeChip.js +0 -24
@@ -11,15 +11,15 @@
11
11
  * omission has a reason that is a correctness boundary rather than caution.
12
12
  */
13
13
  import type { ModelRole } from '../config/model-profiles.js';
14
- import { type AgentConfig, type AgentEvent, type AgentTool, type Message, type ModelRef, type ProviderRegistry, type SkillRegistry, type ThinkingLevel, type TokenUsage, type ToolPolicyDecision, type ToolPolicyVerdict } from '@aragon-agent/core';
14
+ import { type AgentConfig, type AgentEvent, type AgentTool, type Message, type ProviderRegistry, type SkillRegistry, type TokenUsage, type ToolPolicyDecision, type ToolPolicyVerdict } from '@aragon-agent/core';
15
15
  import type { AgentMode } from '../agent/agent-mode.js';
16
16
  import type { ChildContextManagerProvider } from '../compaction/child.js';
17
17
  import type { CliConfig } from '../config/schema.js';
18
18
  import type { ToolPermission } from '../exec/permission.js';
19
- import type { FastTierName } from '../fast/types.js';
19
+ import type { ChildReviewProvider } from './child-reviewer.js';
20
20
  import { TeamBus } from './bus.js';
21
21
  import type { TeamHumanQueue, WatchdogPausable } from './human-queue.js';
22
- import type { SubagentRun, SubagentSpec } from './types.js';
22
+ import type { OverseerVerdict, SubagentRun, SubagentSpec } from './types.js';
23
23
  /**
24
24
  * The slice of `Agent` a child is used through.
25
25
  *
@@ -31,6 +31,10 @@ export interface SubagentAgentLike extends WatchdogPausable {
31
31
  prompt(text: string): Promise<void>;
32
32
  abort(): void;
33
33
  subscribe(listener: (event: AgentEvent) => void): () => void;
34
+ /** Steering surface for the child's own fast reviewer (core `Agent.steer`). */
35
+ steer(text: string): void;
36
+ /** Queued-block hygiene on abort, again mirroring the lead (D-21). */
37
+ clearAllQueues(): void;
34
38
  /**
35
39
  * The child's live history, for its own context manager
36
40
  * (context-auto-compaction-hardening §3.4.3 / W3).
@@ -95,23 +99,19 @@ export interface SubagentDeps {
95
99
  humanQueue?: TeamHumanQueue;
96
100
  agentFactory: SubagentAgentFactory;
97
101
  /**
98
- * Which model and thinking level a tier runs on (fast-model-tier §3.4).
99
- *
100
- * OPTIONAL, defaulting to the session's own model, so every existing caller
101
- * and test compiles unchanged and a `--no-fast` session builds children
102
- * exactly as it always did.
103
- *
104
- * `resolveTier('fast')` RETURNS THE MAIN TIER when the fast tier is not
105
- * available, which is what makes the two guards agree BY CONSTRUCTION: the
106
- * normalizer can never produce a `tier: 'fast'` spec this factory would
107
- * refuse, and this factory can never build a child against a `ModelRef` the
108
- * tier stopped resolving twenty minutes ago (RV-3 / R-14).
102
+ * Whether the dispatch supervisor is live for THIS dispatch
103
+ * (team-overseer). Read once per dispatch by the runtime and spread here;
104
+ * when true the child's OWN idle watchdog is lengthened by
105
+ * `TEAM_LIMITS.overseerWatchdogFactor`, so the supervisor's soft silence
106
+ * inspection at `idleTimeoutMs` always precedes the hard abort.
109
107
  */
110
- resolveTier?: (tier: FastTierName) => {
111
- ref: ModelRef;
112
- thinkingLevel: ThinkingLevel;
113
- role?: ModelRole;
114
- };
108
+ overseerActive?: boolean;
109
+ /**
110
+ * Per-child fast review (main-agent parity): the controller's shared
111
+ * provider, built on the lead's `FastWiring`. ABSENT means the session
112
+ * has no fast wiring, and the child is then exactly what it always was.
113
+ */
114
+ review?: ChildReviewProvider;
115
115
  /**
116
116
  * Build this child its own context manager
117
117
  * (context-auto-compaction-hardening §3.4.3 / W3).
@@ -128,12 +128,41 @@ export interface SubagentHooks {
128
128
  /** Coalesced at the source (§5.2); phase transitions always pass. */
129
129
  onUpdate: (run: SubagentRun) => void;
130
130
  onUsage: (label: string, usage: TokenUsage) => void;
131
+ /**
132
+ * UNTHROTTLED per-event callback (team-overseer I-OV3). `onUpdate` is
133
+ * coalesced, so it would lie about silence; the supervisor's soft stall
134
+ * timer needs one kick per REAL agent event. Called after the phase switch
135
+ * so the phase read at fire time is current.
136
+ */
137
+ onActivity?: () => void;
131
138
  }
132
139
  export interface SubagentHandle {
133
140
  spec: SubagentSpec;
134
141
  run: SubagentRun;
135
142
  agent: SubagentAgentLike;
136
143
  tools: AgentTool[];
144
+ /**
145
+ * THE HANDLE-LEVEL ABORT, and the only one `TeamRuntime` may call. It sets a
146
+ * synchronous `abortRequested` flag BEFORE `agent.abort()`, which is what the
147
+ * child's fast reviewer reads as its guard-1 fact: an Esc or a dispatch stop
148
+ * must be visible to the reviewer in the same tick it happens, or the
149
+ * reviewer steers a `<fast_review>` block into a run that is already dying.
150
+ */
151
+ abort: () => void;
152
+ /**
153
+ * Whether `abort()` has been called on this handle (team-overseer
154
+ * I-OV4): the supervisor reads it before steering a nudge, so advice is
155
+ * never queued into a child that is already dying - the same guard-1 fact
156
+ * the child's own fast reviewer reads.
157
+ */
158
+ isAborted: () => boolean;
159
+ /**
160
+ * A terminal supervisor decision awaiting application by the WORKER LOOP
161
+ * (team-overseer I-OV1). Written by `TeamRuntime` immediately before
162
+ * `abort()`; read by `runOne` after `prompt()` resolves. NEVER cleared,
163
+ * so an abort racing the read cannot hide the cause.
164
+ */
165
+ overseerVerdict?: OverseerVerdict;
137
166
  unsubscribe: () => void;
138
167
  }
139
168
  /**
@@ -13,6 +13,7 @@
13
13
  import { Agent, createSkillFindTool, } from '@aragon-agent/core';
14
14
  import { formatStreamError } from '../agent/reducer.js';
15
15
  import { buildSystemPrompt } from '../agent/system-prompt.js';
16
+ import { buildFastBlock } from '../fast/prompt.js';
16
17
  import { createBuiltinTools } from '../tools/index.js';
17
18
  import { pickActivityArgs, sanitizeActivity } from './activity.js';
18
19
  import { TEAM_LIMITS, TEAM_SUBAGENT_TOOL_NAMES } from './limits.js';
@@ -40,11 +41,17 @@ function assistantText(message) {
40
41
  * the security boundary here, so it has to be directly checkable.
41
42
  */
42
43
  export function buildSubagentTools(spec, deps, getAgent) {
44
+ // `team_wait`'s ceiling is the STRUCTURAL clamp when the child itself has no
45
+ // wall clock (`subagentTimeoutMs: 0`): a peer wait must stay bounded even when
46
+ // the child's run is not, or one forgotten wait would hold a slot forever.
47
+ const subagentTimeoutMs = deps.config.team.subagentTimeoutMs;
43
48
  const commTools = [
44
49
  makeTeamSend(deps.bus, spec.label),
45
50
  makeTeamWait(deps.bus, spec.label, {
46
51
  getAgent,
47
- maxWaitMs: deps.config.team.subagentTimeoutMs,
52
+ maxWaitMs: subagentTimeoutMs > 0
53
+ ? subagentTimeoutMs
54
+ : TEAM_LIMITS.waitMaxSeconds * 1000,
48
55
  }),
49
56
  ];
50
57
  const tools = createBuiltinTools({
@@ -141,9 +148,6 @@ export function createSubagent(spec, deps, hooks) {
141
148
  const run = {
142
149
  label: spec.label,
143
150
  description: spec.description,
144
- // FROM THE SPEC, which the normalizer already downgraded when the tier was
145
- // unavailable — so this is what RAN, not what was asked for (§3.4).
146
- tier: spec.tier,
147
151
  phase: 'queued',
148
152
  turns: 0,
149
153
  toolCalls: 0,
@@ -158,37 +162,38 @@ export function createSubagent(spec, deps, hooks) {
158
162
  let agentRef = null;
159
163
  const getAgent = () => agentRef;
160
164
  const tools = buildSubagentTools(spec, deps, getAgent);
161
- // THE TWO HARDCODED CONFIG READS THAT USED TO LIVE HERE NOW ROUTE THROUGH ONE
162
- // RESOLVER (fast-model-tier §3.4). The fallback reproduces them exactly, so a
163
- // caller that passes no `resolveTier` builds a byte-identical child.
165
+ // THE CHILD'S MODEL IS THE LEAD'S MODEL, READ FROM THE DISPATCH-TIME CONFIG
166
+ // (main-agent parity). Same provider, same model id, same base URL, same
167
+ // thinking level: a delegation is never a downgrade, and the only model a
168
+ // child may surprise its lead with is one a settings-screen edit made live
169
+ // between two dispatches.
164
170
  //
165
- // `thinkingLevel` is PER TIER and defaults to `'off'` for the fast tier
166
- // (D-13): a "fast" model asked to think for 32 768 tokens is not fast, and
167
- // inheriting the session's `xhigh` into a mechanical file-scan child would be
168
- // the most expensive possible reading of the word.
169
- //
170
- // `maxTokens` is deliberately NOT per tier (RV-15). A child inherits
171
- // `deps.config.maxTokens` below, which is an ambition sized for the main
172
- // model; the adapter clamps it down to whatever the fast model actually
173
- // accepts (`llm/provider.ts:44-52`), so a large inherited value degrades to
174
- // the right number rather than to an HTTP 400.
175
- const resolved = deps.resolveTier
176
- ? deps.resolveTier(spec.tier)
177
- : {
178
- ref: {
179
- providerId: deps.config.provider,
180
- modelId: deps.config.model,
181
- ...(deps.config.baseUrl ? { baseUrl: deps.config.baseUrl } : {}),
182
- },
183
- thinkingLevel: deps.config.thinkingLevel,
184
- role: 'main',
185
- };
171
+ // `maxTokens` inherits `deps.config.maxTokens` below - the same ambition the
172
+ // lead sends, clamped by the same adapter, so an inherited large value
173
+ // degrades to the right number rather than to an HTTP 400.
174
+ const resolved = {
175
+ ref: {
176
+ providerId: deps.config.provider,
177
+ modelId: deps.config.model,
178
+ ...(deps.config.baseUrl ? { baseUrl: deps.config.baseUrl } : {}),
179
+ },
180
+ thinkingLevel: deps.config.thinkingLevel,
181
+ role: 'main',
182
+ };
186
183
  const model = resolved.ref;
187
184
  // HOISTED TO A LOCAL, AND THAT IS WHAT MAKES W3 POSSIBLE AT ALL. This was built
188
185
  // INLINE inside the `agentFactory({...})` argument, so there was nothing for the
189
186
  // child manager's `getSystemPrompt` accessor to close over - and the accessor
190
187
  // cannot read it back off the agent, because `SubagentAgentLike` is narrowed to
191
188
  // `state.messages` on purpose.
189
+ // THE SAME `<fast_tier>` BLOCK THE LEAD GETS, spliced only when this child
190
+ // will actually have a reviewer. The gating expression is written against
191
+ // `deps.review` here and the reviewer is built from the SAME predicate
192
+ // below, so the prompt can never advertise a capability the child lacks -
193
+ // and a child with a reviewer is never surprised by its first injection.
194
+ const fastBlock = deps.review !== undefined && deps.review.active()
195
+ ? buildFastBlock({ model: deps.review.modelId(), review: true })
196
+ : '';
192
197
  const systemPrompt = buildSystemPrompt({
193
198
  cwd: deps.getCwd(),
194
199
  tools,
@@ -203,6 +208,7 @@ export function createSubagent(spec, deps, hooks) {
203
208
  description: spec.description,
204
209
  peers: deps.bus.peersOf(spec.label),
205
210
  }),
211
+ ...(fastBlock ? { fastBlock } : {}),
206
212
  });
207
213
  // LAZY ACCESSORS OVER `agentRef`, WHICH IS ASSIGNED AFTER THIS RETURNS. A
208
214
  // non-lazy read here is `undefined` at construction and the child's estimate
@@ -235,7 +241,9 @@ export function createSubagent(spec, deps, hooks) {
235
241
  // (§3.8). The two places a child legitimately blocks on something other
236
242
  // than its own LLM or tool — `team_wait` and the confirm queue — pause it
237
243
  // explicitly.
238
- idleTimeout: deps.config.idleTimeoutMs,
244
+ idleTimeout: deps.overseerActive === true
245
+ ? deps.config.idleTimeoutMs * TEAM_LIMITS.overseerWatchdogFactor
246
+ : deps.config.idleTimeoutMs,
239
247
  },
240
248
  // SPREAD CONDITIONALLY, never `contextManager: deps.contextManagerFor?.(...)`.
241
249
  // With the feature off the options bag has no such key, so the loop's gate
@@ -244,6 +252,27 @@ export function createSubagent(spec, deps, hooks) {
244
252
  ...(contextManager ? { contextManager } : {}),
245
253
  });
246
254
  agentRef = agent;
255
+ // PER-CHILD FAST REVIEW (main-agent parity). GATED ON THE PROVIDER'S OWN
256
+ // LIVE PREDICATE, read once here so the prompt and the reviewer CANNOT
257
+ // DISAGREE about whether the child has one: a child told about
258
+ // `<fast_review>` blocks must be a child that actually receives them, and a
259
+ // child with a reviewer must have been told. `abortRequested` is the flag
260
+ // `handle.abort()` sets (see `SubagentHandle`); the reviewer reads it as its
261
+ // never-steer-into-a-requested-abort fact.
262
+ let abortRequested = false;
263
+ const reviewLive = deps.review !== undefined && deps.review.active() === true;
264
+ const reviewer = reviewLive
265
+ ? deps.review?.create({
266
+ label: spec.label,
267
+ goal: spec.description,
268
+ agent,
269
+ isRunning: () => run.phase === 'starting' ||
270
+ run.phase === 'thinking' ||
271
+ run.phase === 'tool' ||
272
+ run.phase === 'waiting',
273
+ isAbortRequested: () => abortRequested,
274
+ })
275
+ : undefined;
247
276
  let lastEmit = 0;
248
277
  let lastPhase = run.phase;
249
278
  /**
@@ -325,8 +354,16 @@ export function createSubagent(spec, deps, hooks) {
325
354
  // an aborted run, which the report renders as partial rather than lost.
326
355
  if (text.length > 0)
327
356
  run.summary = text;
328
- if (run.turns >= deps.config.team.maxTurnsPerSubagent) {
357
+ // `0` MEANS NO CAP (main-agent parity): the lead has no turn ceiling
358
+ // either, and a child that needs forty turns for a forty-turn job is
359
+ // not runaway - it is working.
360
+ const turnCap = deps.config.team.maxTurnsPerSubagent;
361
+ if (turnCap > 0 && run.turns >= turnCap) {
329
362
  run.truncated = true;
363
+ // THE FLAG FIRST, exactly like `handle.abort()`: the child's
364
+ // reviewer reads `abortRequested` as its guard-1 fact, and a
365
+ // turn-cap stop must be visible to it in the same tick.
366
+ abortRequested = true;
330
367
  agent.abort();
331
368
  }
332
369
  break;
@@ -402,7 +439,27 @@ export function createSubagent(spec, deps, hooks) {
402
439
  default:
403
440
  break;
404
441
  }
442
+ // AFTER the switch, so `run.phase` is current when the supervisor's
443
+ // silence timer consults it (team-overseer I-OV3): a `team_wait` start
444
+ // must be observable as `waiting` on the very kick that follows it.
445
+ hooks.onActivity?.();
405
446
  emit();
406
447
  });
407
- return { spec, run, agent, tools, unsubscribe };
448
+ return {
449
+ spec,
450
+ run,
451
+ agent,
452
+ tools,
453
+ // THE HANDLE-LEVEL ABORT (see `SubagentHandle`): the flag first, then the
454
+ // agent, so the reviewer's guard observes the request in the same tick.
455
+ abort: () => {
456
+ abortRequested = true;
457
+ agent.abort();
458
+ },
459
+ isAborted: () => abortRequested,
460
+ unsubscribe: () => {
461
+ reviewer?.dispose();
462
+ unsubscribe();
463
+ },
464
+ };
408
465
  }
@@ -19,6 +19,12 @@
19
19
  * `normalizeSubagentSpecs`, which is deterministic and unit-tested. `maxItems`
20
20
  * in the schema would behave differently depending on whether `ajv`, an OPTIONAL
21
21
  * dependency of core, happened to install.
22
+ *
23
+ * EVERY CHILD RUNS THE LEAD'S OWN MODEL (main-agent parity): same provider,
24
+ * model, thinking level and token ambition, resolved from the LIVE config at
25
+ * dispatch. There is no per-child `model` property and no cheap tier to pick -
26
+ * a delegation is never a downgrade, and the fast tier reaches children the
27
+ * same way it reaches the lead (compaction summaries, per-child reviews).
22
28
  */
23
29
  import { type AgentTool } from '@aragon-agent/core';
24
30
  import type { ModelCost } from '@aragon-agent/core';
@@ -27,38 +33,12 @@ export interface TaskToolDeps {
27
33
  runtime: TeamRuntime;
28
34
  /** Read LIVE, never cached: `/team off` flips this, it does not unregister. */
29
35
  isTeamEnabled: () => boolean;
30
- /**
31
- * Whether the fast tier was resolvable AT CONSTRUCTION (fast-model-tier §3.3 /
32
- * C-2).
33
- *
34
- * DECIDED ONCE, because the tool array is built once and must never be
35
- * rebuilt: `Agent.setTools()` mutates the live `ToolRegistry`, and
36
- * `submit_plan` flips the session mode from INSIDE a tool execution. So the
37
- * `model` property either exists in this schema for the whole session or never
38
- * does — and with it absent, `TASK_DESCRIPTION` and the schema are
39
- * BYTE-IDENTICAL to the pre-feature build (AC-3).
40
- *
41
- * Optional so every existing construction site and test compiles unchanged.
42
- */
43
- fastRegistered?: boolean;
44
- /**
45
- * Whether `model:"fast"` is honoured RIGHT NOW. Read LIVE, at dispatch time
46
- * (RV-3) — the tier can stop resolving mid-session, and a downgrade is the
47
- * honest observable either way.
48
- */
49
- fastAvailable?: () => boolean;
50
36
  maxSubagents: () => number;
51
37
  /** Whether a usable key exists for the active provider right now (P2-3). */
52
38
  hasApiKey: () => boolean;
53
39
  activeProvider: () => string;
54
40
  /** The lead's cost table, so the report header states real spend (R-5). */
55
41
  modelCost: () => ModelCost | undefined;
56
- /** The FAST model's cost table (fast-model-tier §3.4). Absent when the tier is
57
- * off, which is what leaves the report byte-identical. */
58
- fastModelCost?: () => ModelCost | undefined;
59
- /** Whether the static price table has never heard of the fast model (C-11 /
60
- * RV-4). `unknown` is rendered as unknown, NEVER as `$0.00`. */
61
- fastPricingUnknown?: () => boolean;
62
42
  /**
63
43
  * Run the dispatch with the LEAD's idle watchdog paused, `try/finally` inside.
64
44
  *
@@ -19,6 +19,12 @@
19
19
  * `normalizeSubagentSpecs`, which is deterministic and unit-tested. `maxItems`
20
20
  * in the schema would behave differently depending on whether `ajv`, an OPTIONAL
21
21
  * dependency of core, happened to install.
22
+ *
23
+ * EVERY CHILD RUNS THE LEAD'S OWN MODEL (main-agent parity): same provider,
24
+ * model, thinking level and token ambition, resolved from the LIVE config at
25
+ * dispatch. There is no per-child `model` property and no cheap tier to pick -
26
+ * a delegation is never a downgrade, and the fast tier reaches children the
27
+ * same way it reaches the lead (compaction summaries, per-child reviews).
22
28
  */
23
29
  import { errorResult, textResult } from '@aragon-agent/core';
24
30
  import { TEAM_LIMITS } from './limits.js';
@@ -33,45 +39,23 @@ const TEAM_OFF_REFUSAL = 'Team mode is off for this session. Do this work yourse
33
39
  */
34
40
  const TASK_DESCRIPTION = 'Run several independent subagents in parallel and get one combined report. Each ' +
35
41
  'subagent is a fresh agent with the same tools and working directory but NO memory of ' +
36
- 'this conversation - its prompt must be self-contained. Use this when a task splits ' +
42
+ 'this conversation - its prompt must be self-contained. Each runs on the same model ' +
43
+ 'you are running on. Use this when a task splits ' +
37
44
  'into 2 or more parts that do not depend on each other\'s output, especially reading or ' +
38
45
  'searching several areas at once. Do NOT use it when one part needs another part\'s ' +
39
46
  'result (do those yourself, in order), when two parts would edit the same file, or for ' +
40
47
  'a change small enough to just make. Subagents cannot dispatch further subagents and ' +
41
48
  'cannot ask the user anything. Give each a description of at most ' +
42
49
  `${TEAM_LIMITS.descriptionChars} characters and a complete prompt. Extra entries beyond the ` +
43
- 'configured maximum are dropped, so keep within it.';
44
- /**
45
- * The one sentence appended when the fast tier exists (fast-model-tier §4.1).
46
- *
47
- * WHEN to use a tier is POLICY and lives here and in `<fast_tier>`, where the
48
- * model actually reads it; the `enum` below is SHAPE and lives in the schema.
49
- * That split is what keeps behaviour the same whether or not `ajv` — an OPTIONAL
50
- * dependency of core — happened to install (C-8).
51
- */
52
- const TASK_FAST_SENTENCE = ' Set model:"fast" on a subagent whose job is bulk reading, searching or summarizing; ' +
53
- 'leave it out for work that needs judgement.';
54
- /**
55
- * NOT `as const`: `AgentTool.parameters` is a `JSONSchema7`, whose `enum` is a
56
- * MUTABLE `unknown[]`, and a readonly tuple is not assignable to it.
57
- */
58
- const SUBAGENT_MODEL_PROPERTY = {
59
- type: 'string',
60
- enum: ['main', 'fast'],
61
- default: 'main',
62
- description: 'Which tier runs this subagent. "fast" is a cheaper, quicker model - use it for ' +
63
- 'mechanical, high-volume work (reading or searching many files, summarizing long ' +
64
- 'output, mechanical edits). Keep "main" for design, tricky debugging, and anything ' +
65
- 'that must be right the first time.',
66
- };
50
+ 'configured maximum are dropped, so keep within it. A supervisor watches the ' +
51
+ 'fan-out: a child that stalls or runs very long may be nudged with guidance ' +
52
+ 'or replaced by a fresh attempt, and the report\'s Supervisor section ' +
53
+ 'records every intervention.';
67
54
  export function createTaskTool(deps) {
68
- const fastRegistered = deps.fastRegistered === true;
69
55
  return {
70
56
  name: 'task',
71
57
  label: 'Dispatch subagents',
72
- // BYTE-IDENTICAL WITHOUT THE TIER (AC-3): concatenation is conditional, so a
73
- // default session's description is the exact string it always was.
74
- description: fastRegistered ? `${TASK_DESCRIPTION}${TASK_FAST_SENTENCE}` : TASK_DESCRIPTION,
58
+ description: TASK_DESCRIPTION,
75
59
  parameters: {
76
60
  type: 'object',
77
61
  properties: {
@@ -99,11 +83,6 @@ export function createTaskTool(deps) {
99
83
  default: false,
100
84
  description: 'Refuse all writes and shell for this subagent.',
101
85
  },
102
- // Spread rather than a `?:` property, so with the tier off the
103
- // object has exactly the four keys it always had — `undefined`
104
- // survives `JSON.stringify` as an absent key but not as an
105
- // identical object, and AC-3 is asserted on the schema itself.
106
- ...(fastRegistered ? { model: SUBAGENT_MODEL_PROPERTY } : {}),
107
86
  },
108
87
  required: ['description', 'prompt'],
109
88
  },
@@ -131,31 +110,21 @@ export function createTaskTool(deps) {
131
110
  'yourself, or ask the user to set a key.');
132
111
  }
133
112
  // 4. Repair, never reject. Zero survivors is the single hard failure.
134
- // `fastAvailable` is read HERE, at dispatch time, not captured at
135
- // construction (RV-3): the tier can stop resolving mid-session, and the
136
- // normalizer and the subagent factory must agree about that at the same
137
- // instant or a child gets built against a provider with no key.
138
- const { specs, requested, downgraded } = normalizeSubagentSpecs(params.subagents, deps.maxSubagents(), { fastAvailable: deps.fastAvailable?.() === true });
113
+ const { specs, requested } = normalizeSubagentSpecs(params.subagents, deps.maxSubagents());
139
114
  if (specs.length === 0) {
140
115
  return errorResult('No usable subagent specs: each needs a description and a prompt.');
141
116
  }
142
117
  // 5. Pause the lead's watchdog for the whole dispatch (I-3).
143
- const outcome = await deps.withPausedWatchdog(() => deps.runtime.dispatch(specs, requested, ctx.signal, { downgraded }));
118
+ const outcome = await deps.withPausedWatchdog(() => deps.runtime.dispatch(specs, requested, ctx.signal));
144
119
  // 6. ALWAYS A NON-ERROR RESULT, abort and all-children-failed included.
145
120
  // A cancellation or a partial result rendered as a tool FAILURE invites
146
121
  // the model to retry the whole fan-out, which is the most expensive
147
122
  // possible reaction (the same reasoning as `ask_user`'s cancelled
148
- // shape, and D-14 one level up).
149
- // TWO COST TABLES (§3.4 / R-7). Summing fast-tier tokens at the lead's
150
- // price is not a rounding error: for a Haiku child under a Sonnet lead
151
- // it over-reports by roughly an order of magnitude, and the whole
152
- // justification for the feature is a number in this report.
123
+ // shape, and D-14 one level up). Every child ran the lead's own model,
124
+ // so the lead's cost table is the one honest price for the total.
153
125
  const cost = deps.modelCost();
154
- const fastCost = deps.fastModelCost?.();
155
126
  return textResult(buildDispatchReport(outcome, {
156
127
  ...(cost ? { cost } : {}),
157
- ...(fastCost ? { fastCost } : {}),
158
- ...(deps.fastPricingUnknown?.() ? { fastPricingUnknown: true } : {}),
159
128
  }));
160
129
  },
161
130
  };
@@ -11,7 +11,6 @@
11
11
  * changes ZERO files under `packages/core/` (AC-12).
12
12
  */
13
13
  import type { TokenUsage } from '@aragon-agent/core';
14
- import type { FastTierName } from '../fast/types.js';
15
14
  import type { ActivityArgs } from './activity.js';
16
15
  /** One normalized delegation request. Produced only by `normalizeSubagentSpecs`. */
17
16
  export interface SubagentSpec {
@@ -27,34 +26,12 @@ export interface SubagentSpec {
27
26
  * permission than the session it was spawned from.
28
27
  */
29
28
  readOnly: boolean;
30
- /**
31
- * Which model runs this child (fast-model-tier §3.4).
32
- *
33
- * THE WIRE NAME IS `model` AND THE INTERNAL NAME IS `tier`, on purpose: a
34
- * model expects to see `model:"fast"`, while `SubagentSpec.model` would read
35
- * as a model ID at every call site in this package.
36
- *
37
- * DELEGATION IS A MODEL CHOICE, NEVER A PERMISSION BOUNDARY (D-15 / I-4). A
38
- * fast child gets the same tools, the same `--confirm` gate, the same
39
- * plan-mode gate and the same skills ceiling as any other; only the model
40
- * differs. Restricting a fast child's tools would look like security, would
41
- * not be, and would make the report's `[fast]` rows mean two different things.
42
- */
43
- tier: FastTierName;
44
29
  }
45
30
  export type SubagentPhase = 'queued' | 'starting' | 'thinking' | 'tool' | 'waiting' | 'done' | 'failed' | 'aborted';
46
31
  /** Live, mutable record of one child. Projected into the UI and the report. */
47
32
  export interface SubagentRun {
48
33
  label: string;
49
34
  description: string;
50
- /**
51
- * The tier this child actually ran on (fast-model-tier §3.4).
52
- *
53
- * WHAT RAN, NOT WHAT WAS ASKED FOR. A spec the normalizer downgraded arrives
54
- * here as `'main'`, which is what makes the report's `[fast]` annotation and
55
- * its second cost table agree with the bill.
56
- */
57
- tier: FastTierName;
58
35
  phase: SubagentPhase;
59
36
  startedAt?: number;
60
37
  endedAt?: number;
@@ -171,6 +148,57 @@ export interface SubagentRun {
171
148
  attempt: number;
172
149
  maxRetries: number;
173
150
  };
151
+ /**
152
+ * Supervisor inspections this child received (team-overseer). Optional so
153
+ * every existing fixture compiles; absent and `0` read the same.
154
+ */
155
+ interventions?: number;
156
+ /**
157
+ * Supervisor-driven rebuilds this child went through (team-overseer). The
158
+ * run object survives a rebuild with the same label, so this is where the
159
+ * report reads "this row is the Nth body of the same brief".
160
+ */
161
+ replacements?: number;
162
+ }
163
+ /** What the supervisor may do with one inspected child. */
164
+ export type OverseerAction = 'wait' | 'nudge' | 'replace' | 'abandon';
165
+ /**
166
+ * One supervisor decision, already normalized and clamped.
167
+ *
168
+ * Produced ONLY by `normalizeOverseerDecision` (repair, never reject): an
169
+ * unparseable or out-of-budget model answer becomes `wait` with the default
170
+ * interval, because supervision must not kill a healthy child.
171
+ */
172
+ export interface OverseerDecision {
173
+ action: OverseerAction;
174
+ /** <= TEAM_LIMITS.overseerReasonChars. Why, for the report and the log. */
175
+ reason: string;
176
+ /** `nudge` only: <= TEAM_LIMITS.overseerGuidanceChars. Steered into the child. */
177
+ guidance?: string;
178
+ /** Model-chosen next check, clamped to the structural range. */
179
+ nextCheckMs?: number;
180
+ }
181
+ /** One supervisor intervention, as recorded on the outcome and rendered. */
182
+ export interface OverseerIntervention {
183
+ label: string;
184
+ at: number;
185
+ action: OverseerAction;
186
+ reason: string;
187
+ /** What the trigger was: event stall, wall-clock check, or post-mortem. */
188
+ trigger: 'silence' | 'clock' | 'postmortem';
189
+ }
190
+ /**
191
+ * A terminal supervisor decision handed to the WORKER LOOP, which is the only
192
+ * place a child's `prompt()` is awaited and therefore the only place a
193
+ * lifecycle change can be applied without orphaning the replacement (I-OV1).
194
+ *
195
+ * Set by `TeamRuntime` on the handle, then `handle.abort()`; consumed and
196
+ * left in place (never cleared) by `runOne` after the await resolves.
197
+ */
198
+ export interface OverseerVerdict {
199
+ action: 'replace' | 'abandon';
200
+ reason: string;
201
+ guidance?: string;
174
202
  }
175
203
  /** One message on the team bus. */
176
204
  export interface TeamMessage {
@@ -192,26 +220,17 @@ export interface DispatchOutcome {
192
220
  /** Messages addressed to `lead`; surfaced in their own report section. */
193
221
  leadMail: TeamMessage[];
194
222
  /**
195
- * Summed across MAIN-TIER children. Real spend; it must reach the status bar
196
- * (§3.9).
197
- *
198
- * THE MEANING NARROWED WHEN THE FAST TIER LANDED, and the split is not a
199
- * rounding concern: a Haiku child under a Sonnet lead priced at the lead's
200
- * table over-reports by roughly an order of magnitude, and the entire
201
- * justification for delegating is a number in this report (R-7).
223
+ * Every supervisor decision that was applied (team-overseer), in order.
224
+ * Bounded by the per-child inspection budget; rendered as the report's
225
+ * Overseer section so the lead can see WHO was nudged or replaced and why.
202
226
  */
203
- usage: TokenUsage;
204
- /** Summed across FAST-TIER children. Absent when none ran, which is what
205
- * keeps a pre-feature dispatch's report byte-identical. */
206
- fastUsage?: TokenUsage;
227
+ interventions?: OverseerIntervention[];
207
228
  /**
208
- * Specs that asked for `model:"fast"` and ran on `main` anyway (§3.4 / R-6).
209
- *
210
- * REPORTED, NEVER SILENT. A silent downgrade means the model asked for a cheap
211
- * child, got an expensive one, and has no way to learn that its cost model is
212
- * wrong.
229
+ * Summed across all children. Real spend; it must reach the status bar
230
+ * (§3.9). Every child runs the LEAD's model (main-agent parity), so one
231
+ * total priced at the lead's table is the honest accounting.
213
232
  */
214
- downgraded?: number;
233
+ usage: TokenUsage;
215
234
  }
216
235
  /**
217
236
  * What the UI renders: a cheap immutable projection, rebuilt per event.
@@ -246,11 +265,16 @@ export type TeamEvent = {
246
265
  dispatchId: string;
247
266
  label: string;
248
267
  usage: TokenUsage;
249
- tier: FastTierName;
250
268
  } | {
251
269
  type: 'message';
252
270
  dispatchId: string;
253
271
  message: TeamMessage;
272
+ } | {
273
+ type: 'overseer';
274
+ dispatchId: string;
275
+ label: string;
276
+ decision: OverseerDecision;
277
+ trigger: OverseerIntervention['trigger'];
254
278
  } | {
255
279
  type: 'dispatch_end';
256
280
  dispatchId: string;
package/dist/ui/App.d.ts CHANGED
@@ -11,7 +11,7 @@ import type { ConfirmRequest } from '../tools/index.js';
11
11
  import { type Overlay, type ToastLevel } from '../agent/reducer.js';
12
12
  import type { UpdateBridge } from '../update/types.js';
13
13
  import type { MouseSource } from '../input/stdin-filter.js';
14
- import type { PasteBridge } from '../input/limits.js';
14
+ import { type PasteBridge } from '../input/limits.js';
15
15
  import type { SelectionBridge } from './selection/selection-controller.js';
16
16
  import type { HumanInputBridge } from '../tools/human-input.js';
17
17
  export interface ConfirmBridge {