@aragon-agent/cli 0.5.8 → 0.5.10
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 +1157 -747
- package/README.md +1552 -1163
- package/dist/agent/coalesce.d.ts +13 -0
- package/dist/agent/coalesce.js +50 -11
- package/dist/agent/controller.d.ts +141 -2
- package/dist/agent/controller.js +396 -4
- package/dist/agent/headless.d.ts +8 -0
- package/dist/agent/headless.js +40 -0
- package/dist/agent/reducer.d.ts +245 -3
- package/dist/agent/reducer.js +490 -17
- package/dist/agent/retry-view.d.ts +68 -0
- package/dist/agent/retry-view.js +95 -0
- package/dist/agent/system-prompt.d.ts +11 -0
- package/dist/agent/system-prompt.js +2 -0
- package/dist/boot/guard.d.ts +135 -0
- package/dist/boot/guard.js +218 -0
- package/dist/boot/rollback.d.ts +86 -0
- package/dist/boot/rollback.js +185 -0
- package/dist/cli.js +368 -3
- package/dist/commands/builtins.d.ts +1 -1
- package/dist/commands/builtins.js +426 -2
- package/dist/commands/registry.d.ts +17 -0
- package/dist/config/app-paths.d.ts +31 -0
- package/dist/config/app-paths.js +37 -0
- package/dist/config/cli-commands.d.ts +79 -2
- package/dist/config/cli-commands.js +185 -3
- package/dist/config/env.d.ts +15 -3
- package/dist/config/env.js +143 -3
- package/dist/config/load.d.ts +66 -0
- package/dist/config/load.js +107 -1
- package/dist/config/schema.d.ts +268 -2
- package/dist/config/schema.js +288 -1
- package/dist/config/store.js +32 -1
- package/dist/fast/critique.d.ts +43 -0
- package/dist/fast/critique.js +62 -0
- package/dist/fast/digest.d.ts +42 -0
- package/dist/fast/digest.js +85 -0
- package/dist/fast/frames.d.ts +99 -0
- package/dist/fast/frames.js +180 -0
- package/dist/fast/limits.d.ts +98 -0
- package/dist/fast/limits.js +98 -0
- package/dist/fast/prompt.d.ts +72 -0
- package/dist/fast/prompt.js +94 -0
- package/dist/fast/resolve.d.ts +43 -0
- package/dist/fast/resolve.js +105 -0
- package/dist/fast/review-call.d.ts +99 -0
- package/dist/fast/review-call.js +130 -0
- package/dist/fast/reviewer.d.ts +248 -0
- package/dist/fast/reviewer.js +664 -0
- package/dist/fast/types.d.ts +128 -0
- package/dist/fast/types.js +14 -0
- package/dist/fast/wiring.d.ts +170 -0
- package/dist/fast/wiring.js +316 -0
- package/dist/launcher.d.ts +14 -0
- package/dist/launcher.js +43 -0
- package/dist/logging/install.d.ts +15 -1
- package/dist/logging/install.js +37 -4
- package/dist/logging/logger.d.ts +1 -1
- package/dist/session/persist.js +57 -1
- package/dist/team/normalize.d.ts +26 -1
- package/dist/team/normalize.js +23 -3
- package/dist/team/prompt.d.ts +11 -0
- package/dist/team/prompt.js +10 -0
- package/dist/team/report.d.ts +18 -0
- package/dist/team/report.js +27 -1
- package/dist/team/runtime.d.ts +13 -2
- package/dist/team/runtime.js +33 -6
- package/dist/team/subagent.d.ts +19 -1
- package/dist/team/subagent.js +72 -10
- package/dist/team/task-tool.d.ts +26 -0
- package/dist/team/task-tool.js +48 -4
- package/dist/team/types.d.ts +80 -1
- package/dist/tools/bash-tool.js +19 -3
- package/dist/tools/diff.d.ts +25 -5
- package/dist/tools/diff.js +72 -41
- package/dist/tools/file-change-store.d.ts +34 -0
- package/dist/tools/file-change-store.js +57 -0
- package/dist/tools/fs-tools.d.ts +26 -0
- package/dist/tools/fs-tools.js +62 -5
- package/dist/tools/index.d.ts +31 -0
- package/dist/tools/index.js +7 -1
- package/dist/tools/patch.d.ts +115 -0
- package/dist/tools/patch.js +453 -0
- package/dist/tools/terminal-output.d.ts +107 -0
- package/dist/tools/terminal-output.js +198 -0
- package/dist/tools/tool-output-store.d.ts +68 -0
- package/dist/tools/tool-output-store.js +102 -0
- package/dist/ui/ActivityLine.d.ts +48 -0
- package/dist/ui/ActivityLine.js +22 -0
- package/dist/ui/App.d.ts +11 -4
- package/dist/ui/App.js +359 -37
- package/dist/ui/BottomStatusRow.d.ts +65 -0
- package/dist/ui/BottomStatusRow.js +17 -0
- package/dist/ui/Composer.d.ts +1 -6
- package/dist/ui/Composer.js +2 -2
- package/dist/ui/PromptInput.d.ts +1 -15
- package/dist/ui/PromptInput.js +1 -38
- package/dist/ui/StatusBar.d.ts +37 -0
- package/dist/ui/StatusBar.js +5 -3
- package/dist/ui/TeamPanel.d.ts +5 -4
- package/dist/ui/TeamPanel.js +1 -1
- package/dist/ui/TodoPanel.d.ts +5 -4
- package/dist/ui/Transcript.d.ts +26 -3
- package/dist/ui/Transcript.js +76 -10
- package/dist/ui/UpdateLine.d.ts +44 -0
- package/dist/ui/UpdateLine.js +129 -0
- package/dist/ui/activity-phrases.d.ts +33 -0
- package/dist/ui/activity-phrases.js +50 -0
- package/dist/ui/entries/AssistantEntry.d.ts +20 -0
- package/dist/ui/entries/AssistantEntry.js +3 -2
- package/dist/ui/entries/DiffView.d.ts +67 -0
- package/dist/ui/entries/DiffView.js +103 -0
- package/dist/ui/entries/FastCard.d.ts +53 -0
- package/dist/ui/entries/FastCard.js +87 -0
- package/dist/ui/entries/RetryCard.d.ts +56 -0
- package/dist/ui/entries/RetryCard.js +60 -0
- package/dist/ui/entries/TeamCard.js +6 -1
- package/dist/ui/entries/ToolCard.d.ts +38 -1
- package/dist/ui/entries/ToolCard.js +68 -5
- package/dist/ui/glyphs.d.ts +9 -0
- package/dist/ui/glyphs.js +2 -0
- package/dist/ui/layout/AppShell.d.ts +13 -22
- package/dist/ui/layout/AppShell.js +3 -38
- package/dist/ui/layout/virtual-window.d.ts +1 -1
- package/dist/ui/layout/virtual-window.js +109 -5
- package/dist/ui/overlays/HelpOverlay.js +16 -2
- package/dist/ui/overlays/SettingsScreen.d.ts +82 -1
- package/dist/ui/overlays/SettingsScreen.js +147 -1
- package/dist/ui/transcript-text.js +27 -0
- package/dist/ui/use-wheel-routing.d.ts +16 -32
- package/dist/ui/use-wheel-routing.js +21 -38
- package/dist/update/classify-failure.d.ts +32 -0
- package/dist/update/classify-failure.js +60 -0
- package/dist/update/install-lock.d.ts +55 -0
- package/dist/update/install-lock.js +184 -0
- package/dist/update/install-source.d.ts +97 -0
- package/dist/update/install-source.js +218 -0
- package/dist/update/installer.d.ts +108 -0
- package/dist/update/installer.js +242 -0
- package/dist/update/limits.d.ts +111 -0
- package/dist/update/limits.js +111 -0
- package/dist/update/npm-view.d.ts +62 -0
- package/dist/update/npm-view.js +133 -0
- package/dist/update/registry.d.ts +52 -0
- package/dist/update/registry.js +161 -0
- package/dist/update/semver.d.ts +89 -0
- package/dist/update/semver.js +295 -0
- package/dist/update/service.d.ts +193 -0
- package/dist/update/service.js +636 -0
- package/dist/update/state.d.ts +103 -0
- package/dist/update/state.js +142 -0
- package/dist/update/types.d.ts +169 -0
- package/dist/update/types.js +68 -0
- package/package.json +3 -3
- package/dist/ui/layout/regions.d.ts +0 -55
- package/dist/ui/layout/regions.js +0 -52
package/dist/agent/controller.js
CHANGED
|
@@ -9,19 +9,25 @@
|
|
|
9
9
|
* from the event stream by the reducer (TUI) and the headless writer.
|
|
10
10
|
*/
|
|
11
11
|
import { Agent, initProviders, ModelRegistry, } from '@aragon-agent/core';
|
|
12
|
-
import { clampTeamConfig, clampTodoConfig, isAdapterProvider } from '../config/schema.js';
|
|
12
|
+
import { clampFastConfig, clampRetryConfig, clampTeamConfig, clampTodoConfig, isAdapterProvider, toRetryPolicy, } from '../config/schema.js';
|
|
13
13
|
import { makeGetApiKey } from '../config/load.js';
|
|
14
14
|
import { updatePersistedConfig } from '../config/store.js';
|
|
15
15
|
import { createBuiltinTools } from '../tools/index.js';
|
|
16
|
+
import { createFileChangeStore } from '../tools/file-change-store.js';
|
|
17
|
+
import { createToolOutputStore, } from '../tools/tool-output-store.js';
|
|
16
18
|
import { TeamRuntime } from '../team/runtime.js';
|
|
17
19
|
import { TeamHumanQueue } from '../team/human-queue.js';
|
|
18
20
|
import { createTaskTool } from '../team/task-tool.js';
|
|
19
21
|
import { buildTeamBlock } from '../team/prompt.js';
|
|
22
|
+
import { buildFastBlock } from '../fast/prompt.js';
|
|
23
|
+
import { describeFastTierProblem, fastProviderOf, resolveFastTier } from '../fast/resolve.js';
|
|
24
|
+
import { FastWiring, offFastStatus } from '../fast/wiring.js';
|
|
20
25
|
import { TodoStore } from '../todo/store.js';
|
|
21
26
|
import { createTodoTool } from '../todo/todo-tool.js';
|
|
22
27
|
import { buildTodoBlock } from '../todo/prompt.js';
|
|
23
28
|
import { createPlanTools } from '../tools/plan-tools.js';
|
|
24
29
|
import { DENY_ALL_HUMAN_INPUT } from '../tools/human-input.js';
|
|
30
|
+
import { getLogger } from '../logging/logger.js';
|
|
25
31
|
import { createNodeSkillHost } from '../skills/node-host.js';
|
|
26
32
|
import { SkillService } from '../skills/service.js';
|
|
27
33
|
import { createSkillTools } from '../skills/tools.js';
|
|
@@ -94,13 +100,67 @@ export class AgentController {
|
|
|
94
100
|
todos;
|
|
95
101
|
/** Static: whether this session could ever render the rail. See the dep. */
|
|
96
102
|
todoPanelCapable;
|
|
103
|
+
/**
|
|
104
|
+
* Structured file diffs on their way to the UI (agent-activity-presentation
|
|
105
|
+
* §3.3.5). A bounded, take-once ring; see `file-change-store.ts` for why each
|
|
106
|
+
* of those three words is load-bearing.
|
|
107
|
+
*
|
|
108
|
+
* DECLARED WITH AN INITIALIZER, so it exists before `createBuiltinTools` runs
|
|
109
|
+
* in the constructor body and the recorder closure it hands over is valid from
|
|
110
|
+
* the first tool call.
|
|
111
|
+
*/
|
|
112
|
+
fileChanges = createFileChangeStore();
|
|
113
|
+
/**
|
|
114
|
+
* A running tool's output on its way to the UI
|
|
115
|
+
* (agent-activity-presentation-live §3.1.3).
|
|
116
|
+
*
|
|
117
|
+
* NULL WHEN THE FEATURE IS OFF (P1-6). `fileChanges` next door is declared
|
|
118
|
+
* with an initializer because it is unconditional; this one cannot be, since a
|
|
119
|
+
* field initializer cannot read a constructor parameter -- and an
|
|
120
|
+
* unconditional initializer would allocate on every construction and make
|
|
121
|
+
* AC-31's "no store is allocated" false by construction. `private readonly
|
|
122
|
+
* fast: FastWiring | null` below is the same declaration shape in the same
|
|
123
|
+
* class.
|
|
124
|
+
*
|
|
125
|
+
* ASSIGNED IN THE CONSTRUCTOR BODY, ABOVE THE `createBuiltinTools` CALL, not
|
|
126
|
+
* near `this.fast` which runs after it. The binding there is spread
|
|
127
|
+
* CONDITIONALLY on this field, so an assignment placed after the call would
|
|
128
|
+
* read `undefined`, add no property, and produce a build in which the feature
|
|
129
|
+
* is silently off with the key on.
|
|
130
|
+
*/
|
|
131
|
+
toolOutputs;
|
|
132
|
+
outputListeners = new Set();
|
|
133
|
+
// -----------------------------------------------------------------------
|
|
134
|
+
// Fast model tier (fast-model-tier §3.1 / §3.3)
|
|
135
|
+
//
|
|
136
|
+
// ONE FIELD AND FOUR THIN FORWARDERS. The flag pair, the tier cache, the
|
|
137
|
+
// reviewer's lifetime and the `resolveTier` closure all live in
|
|
138
|
+
// `fast/wiring.ts`, which is a BUDGET decision rather than an aesthetic one
|
|
139
|
+
// (C-12 / RV-5): `CLAUDE.md` caps a source file at 1000 lines and this one had
|
|
140
|
+
// 40 to spare when the design was written.
|
|
141
|
+
//
|
|
142
|
+
// `null` when the tier could not be resolved at construction, which is the
|
|
143
|
+
// byte-identity branch: no reviewer is subscribed, `task` carries no `model`
|
|
144
|
+
// property, and `<fast_tier>` is never spliced (I-2).
|
|
145
|
+
// -----------------------------------------------------------------------
|
|
146
|
+
fast;
|
|
147
|
+
/**
|
|
148
|
+
* Set SYNCHRONOUSLY by `abort()` before `agent.abort()` (§3.5.4a guard 1).
|
|
149
|
+
* Cleared at the next `agent_start`.
|
|
150
|
+
*/
|
|
151
|
+
abortRequested = false;
|
|
152
|
+
/** User messages queued since the last `turn_start` (§3.5.4a guard 2). */
|
|
153
|
+
userSteerCount = 0;
|
|
97
154
|
constructor(config, deps = {}) {
|
|
98
155
|
this.config = config;
|
|
99
156
|
this.cwd = config.cwd;
|
|
100
157
|
const humanInput = deps.humanInput ?? DENY_ALL_HUMAN_INPUT;
|
|
101
158
|
this.planInteractive = humanInput.neverPrompts !== true;
|
|
102
159
|
this.effectiveMode = config.startInPlanMode ? 'plan' : 'build';
|
|
103
|
-
|
|
160
|
+
// The retry policy is installed HERE and replaced live by `setRetryConfig`
|
|
161
|
+
// (llm-api-retry-backoff §6.3). Subagents inherit it for free: `TeamRuntime`
|
|
162
|
+
// reuses this very registry instance.
|
|
163
|
+
this.providerRegistry = initProviders({ retryPolicy: toRetryPolicy(config.retry) });
|
|
104
164
|
this.modelRegistry = new ModelRegistry(this.providerRegistry);
|
|
105
165
|
this.skillsEnabled = config.skills.enabled;
|
|
106
166
|
this.onSkillsChanged = deps.onSkillsChanged;
|
|
@@ -132,12 +192,33 @@ export class AgentController {
|
|
|
132
192
|
this.todoEnabled = config.todo.enabled;
|
|
133
193
|
this.todoPanelCapable = deps.todoPanelCapable === true;
|
|
134
194
|
this.todos = this.todoRegistered ? new TodoStore() : null;
|
|
195
|
+
// --- Fast model tier ---------------------------------------------------
|
|
196
|
+
//
|
|
197
|
+
// RESOLVED BEFORE THE TOOL ARRAY, because `fastRegistered` decides whether
|
|
198
|
+
// `task`'s schema carries a `model` property at all and the array is built
|
|
199
|
+
// ONCE (C-2). `FastWiring` re-runs the same pure function in its own
|
|
200
|
+
// constructor; the two agree by construction because they read the same
|
|
201
|
+
// config through the same resolver.
|
|
202
|
+
const fastTier = resolveFastTier(config, (id) => this.hasApiKey(id));
|
|
203
|
+
const fastRegistered = fastTier.ok;
|
|
135
204
|
// --- Team mode ---------------------------------------------------------
|
|
136
205
|
this.teamRegistered = config.team.enabled;
|
|
137
206
|
this.teamEnabled = config.team.enabled;
|
|
138
207
|
this.teamRuntime = this.teamRegistered
|
|
139
208
|
? new TeamRuntime({
|
|
140
209
|
getConfig: () => this.config,
|
|
210
|
+
// Read LIVE through the wiring, which does not exist yet — the closure
|
|
211
|
+
// is only ever called from inside a dispatch, long after construction.
|
|
212
|
+
resolveTier: (tier) => this.fast
|
|
213
|
+
? this.fast.resolveTier(tier)
|
|
214
|
+
: {
|
|
215
|
+
ref: {
|
|
216
|
+
providerId: this.config.provider,
|
|
217
|
+
modelId: this.config.model,
|
|
218
|
+
...(this.config.baseUrl ? { baseUrl: this.config.baseUrl } : {}),
|
|
219
|
+
},
|
|
220
|
+
thinkingLevel: this.config.thinkingLevel,
|
|
221
|
+
},
|
|
141
222
|
providerRegistry: this.providerRegistry,
|
|
142
223
|
getCwd: () => this.cwd,
|
|
143
224
|
getMode: () => this.effectiveMode,
|
|
@@ -166,8 +247,26 @@ export class AgentController {
|
|
|
166
247
|
...(confirm ? { humanQueue: new TeamHumanQueue(confirm) } : {}),
|
|
167
248
|
})
|
|
168
249
|
: null;
|
|
250
|
+
// ABOVE `createBuiltinTools`, and the ordering is load-bearing for the
|
|
251
|
+
// reason the field's own comment gives: the binding below is spread
|
|
252
|
+
// conditionally on it (§3.1.3 / P1-6).
|
|
253
|
+
this.toolOutputs = config.liveToolOutput ? createToolOutputStore() : null;
|
|
169
254
|
this.tools = createBuiltinTools({
|
|
170
255
|
getCwd: () => this.cwd,
|
|
256
|
+
// OWNED HERE, NOT IN `cli.tsx` (D-18): `createBuiltinTools` is called from
|
|
257
|
+
// this constructor and from `team/subagent.ts`, never from `cli.tsx`, so
|
|
258
|
+
// this is the one place a recorder can be bound to the toolset that will
|
|
259
|
+
// use it. Pre-bound to `'lead'`, so `fs-tools.ts` never learns what an
|
|
260
|
+
// owner is; subagents get no recorder this round (D-13).
|
|
261
|
+
recordChange: (id, patch) => this.fileChanges.record('lead', id, patch),
|
|
262
|
+
// The live-output recorder, at the identical site and for the identical
|
|
263
|
+
// reason, and spread ONLY WHEN THE STORE EXISTS: with `liveToolOutput` off
|
|
264
|
+
// the options bag has no `recordOutput` property at all, so
|
|
265
|
+
// `createBuiltinTools`'s conditional spread yields today's `deps` byte for
|
|
266
|
+
// byte and `bash` is the pre-round tool (AC-31).
|
|
267
|
+
...(this.toolOutputs
|
|
268
|
+
? { recordOutput: (id, chunk) => this.emitToolOutput('lead', id, chunk) }
|
|
269
|
+
: {}),
|
|
171
270
|
confirmTools: config.confirmTools,
|
|
172
271
|
// Routed through the same watchdog pause as the plan tools. This is a
|
|
173
272
|
// pre-existing bug being fixed in passing: a `--confirm` user who took
|
|
@@ -220,10 +319,23 @@ export class AgentController {
|
|
|
220
319
|
runtime: this.teamRuntime,
|
|
221
320
|
// Read LIVE: `/team off` flips the flag, it cannot unregister.
|
|
222
321
|
isTeamEnabled: () => this.teamEnabled,
|
|
322
|
+
// FROZEN AT CONSTRUCTION (C-2). `/fast on` in a session launched
|
|
323
|
+
// without the tier cannot add a schema property, and it says so
|
|
324
|
+
// rather than advertising one that is not there.
|
|
325
|
+
fastRegistered,
|
|
326
|
+
// Read LIVE at dispatch time (RV-3): the tier can stop resolving
|
|
327
|
+
// mid-session, and the normalizer and the subagent factory must
|
|
328
|
+
// agree about that at the same instant.
|
|
329
|
+
fastAvailable: () => this.fast?.delegationAvailable() === true,
|
|
223
330
|
maxSubagents: () => this.config.team.maxSubagents,
|
|
224
331
|
hasApiKey: () => this.hasApiKey(),
|
|
225
332
|
activeProvider: () => this.config.provider,
|
|
226
333
|
modelCost: () => this.getModelInfo().cost,
|
|
334
|
+
fastModelCost: () => {
|
|
335
|
+
const ref = this.fast?.fastRef();
|
|
336
|
+
return ref ? this.getModelInfoFor(ref).cost : undefined;
|
|
337
|
+
},
|
|
338
|
+
fastPricingUnknown: () => this.fast?.snapshot().pricingUnknown === true,
|
|
227
339
|
withPausedWatchdog: (fn) => this.withPausedWatchdog(fn),
|
|
228
340
|
}),
|
|
229
341
|
]
|
|
@@ -275,6 +387,71 @@ export class AgentController {
|
|
|
275
387
|
},
|
|
276
388
|
},
|
|
277
389
|
});
|
|
390
|
+
// --- Fast tier, part 2: after the Agent exists --------------------------
|
|
391
|
+
//
|
|
392
|
+
// `FastWiring` subscribes to the lead's event stream, so it cannot be built
|
|
393
|
+
// before the `Agent`. The prompt is therefore composed twice at startup:
|
|
394
|
+
// once above without `<fast_tier>` (the wiring is still null) and once here.
|
|
395
|
+
// That is one extra string build at construction and no live path.
|
|
396
|
+
this.fast = fastRegistered
|
|
397
|
+
? new FastWiring({
|
|
398
|
+
getConfig: () => this.config,
|
|
399
|
+
hasKey: (id) => this.hasApiKey(id),
|
|
400
|
+
getApiKey: (id) => this.resolveKey(id),
|
|
401
|
+
isPricedModel: (ref) => this.isPricedModel(ref),
|
|
402
|
+
subscribe: (listener) => this.agent.subscribe(listener),
|
|
403
|
+
complete: (providerId, request) => this.providerRegistry.complete(providerId, request),
|
|
404
|
+
// THE REVIEWER GOES THROUGH THIS METHOD, NOT `agent.steer()` DIRECTLY,
|
|
405
|
+
// so its own block is counted alongside the user's — which is what
|
|
406
|
+
// `userSteerCount` would otherwise mis-report and what would make
|
|
407
|
+
// guard 2 clear a queue it does not own (D-21).
|
|
408
|
+
//
|
|
409
|
+
// It is decremented back out below: the counter must answer "did the
|
|
410
|
+
// USER queue anything", and the reviewer's own message is not that.
|
|
411
|
+
steer: (text) => {
|
|
412
|
+
this.steer(text);
|
|
413
|
+
this.userSteerCount -= 1;
|
|
414
|
+
},
|
|
415
|
+
isRunning: () => this.isRunning(),
|
|
416
|
+
isAbortRequested: () => this.abortRequested,
|
|
417
|
+
userSteerCount: () => this.userSteerCount,
|
|
418
|
+
clearAllQueues: () => this.clearAllQueues(),
|
|
419
|
+
notify: (level, text) => deps.notify?.(level, text),
|
|
420
|
+
onPromptChanged: () => this.rebuildSystemPrompt(),
|
|
421
|
+
})
|
|
422
|
+
: null;
|
|
423
|
+
// The two guards' bookkeeping, and the only reason this class subscribes to
|
|
424
|
+
// its own agent. `turn_start` is where a user steer stops being pending;
|
|
425
|
+
// `agent_start` is where a requested abort stops being in force.
|
|
426
|
+
this.agent.subscribe((event) => {
|
|
427
|
+
if (event.type === 'agent_start')
|
|
428
|
+
this.abortRequested = false;
|
|
429
|
+
if (event.type === 'turn_start')
|
|
430
|
+
this.userSteerCount = 0;
|
|
431
|
+
// The authoritative result arrives with this event, so from this instant
|
|
432
|
+
// the tail is superseded and its slot must be released (§3.1.1). Clearing
|
|
433
|
+
// HERE rather than on read is what keeps the store's size a function of
|
|
434
|
+
// CONCURRENT calls instead of total calls.
|
|
435
|
+
if (event.type === 'tool_execution_end') {
|
|
436
|
+
this.toolOutputs?.clear('lead', event.toolCallId);
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
if (this.fast) {
|
|
440
|
+
this.rebuildSystemPrompt();
|
|
441
|
+
// Fast-tier children are counted from the outcome rather than from the
|
|
442
|
+
// specs, so a downgraded child is not miscounted as a saving (§3.6).
|
|
443
|
+
this.teamRuntime?.subscribe((event) => {
|
|
444
|
+
if (event.type !== 'dispatch_end')
|
|
445
|
+
return;
|
|
446
|
+
this.fast?.noteDelegated(event.outcome.runs.filter((r) => r.tier === 'fast').length);
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
// ONE LINE FOR THE TWO CONFIGURATION MISTAKES THAT ARE OTHERWISE INVISIBLE
|
|
450
|
+
// (§3.3): the tier is on but unnamed, or named against a provider with no
|
|
451
|
+
// key. Nothing else in the session would ever mention either.
|
|
452
|
+
const problem = describeFastTierProblem(fastTier, fastProviderOf(config));
|
|
453
|
+
if (problem)
|
|
454
|
+
deps.notify?.('warn', problem);
|
|
278
455
|
}
|
|
279
456
|
resolveKey(providerId) {
|
|
280
457
|
return makeGetApiKey(this.config)(providerId);
|
|
@@ -324,6 +501,11 @@ export class AgentController {
|
|
|
324
501
|
? buildTeamBlock({
|
|
325
502
|
maxSubagents: this.config.team.maxSubagents,
|
|
326
503
|
maxConcurrent: this.config.team.maxConcurrent,
|
|
504
|
+
// One cross-reference sentence, present only when the capability
|
|
505
|
+
// is (§3.8). `this.fast` is null during the FIRST compose (the
|
|
506
|
+
// wiring needs the Agent), which is why the constructor composes
|
|
507
|
+
// again once it exists.
|
|
508
|
+
fastDelegation: this.fast?.delegationAvailable() === true,
|
|
327
509
|
})
|
|
328
510
|
: '',
|
|
329
511
|
// BOTH FLAGS, exactly as team mode uses both, and for the identical
|
|
@@ -336,6 +518,17 @@ export class AgentController {
|
|
|
336
518
|
panelVisible: this.todoPanelCapable && this.config.todo.panel,
|
|
337
519
|
})
|
|
338
520
|
: '',
|
|
521
|
+
// The SAME two flags one more time, plus the live resolution — all three
|
|
522
|
+
// live inside `FastWiring.available()` (§3.3). `buildFastBlock` itself
|
|
523
|
+
// returns `''` when neither capability is on, so a user who turned both
|
|
524
|
+
// off pays nothing for the tier being technically live.
|
|
525
|
+
fastBlock: this.fast && this.fast.available()
|
|
526
|
+
? buildFastBlock({
|
|
527
|
+
model: this.fast.fastRef()?.modelId ?? '',
|
|
528
|
+
delegate: this.config.fast.delegate,
|
|
529
|
+
review: this.config.fast.review,
|
|
530
|
+
})
|
|
531
|
+
: '',
|
|
339
532
|
});
|
|
340
533
|
}
|
|
341
534
|
rebuildSystemPrompt() {
|
|
@@ -437,9 +630,21 @@ export class AgentController {
|
|
|
437
630
|
// an unfinished one survives "continue" / "now do the rest" but not four
|
|
438
631
|
// unrelated turns.
|
|
439
632
|
this.todos?.beginUserTurn();
|
|
633
|
+
// The fast reviewer is asked whether the run is on track FOR THIS (§3.5.2).
|
|
634
|
+
// Without it the reviewer judges progress without being told the goal, which
|
|
635
|
+
// is the one input it cannot reconstruct from the event stream.
|
|
636
|
+
this.fast?.setGoal(text);
|
|
440
637
|
return this.agent.prompt(text);
|
|
441
638
|
}
|
|
442
639
|
abort() {
|
|
640
|
+
// SET BEFORE `agent.abort()`, SYNCHRONOUSLY — guard 1 of §3.5.4a. The loop
|
|
641
|
+
// still emits the batch's final `tool_execution_end` (`agent-loop.ts:249`)
|
|
642
|
+
// after an Esc lands inside the last tool's `await`, and the reviewer's
|
|
643
|
+
// listener runs synchronously inside that emit; without this flag it would
|
|
644
|
+
// see `isRunning() === true`, steer, and strand a critique in a queue
|
|
645
|
+
// nothing ever clears (RV-1).
|
|
646
|
+
this.abortRequested = true;
|
|
647
|
+
this.fast?.abort();
|
|
443
648
|
this.agent.abort();
|
|
444
649
|
// DELIBERATE REDUNDANCY (§3.8). Aborting the lead already aborts the
|
|
445
650
|
// in-flight `task` call's `ctx.signal`, which the runtime listens to — that
|
|
@@ -456,6 +661,13 @@ export class AgentController {
|
|
|
456
661
|
// routes through `steer` whenever the agent happens to be running.
|
|
457
662
|
if (this.skillsEnabled)
|
|
458
663
|
this.skills.absorbPendingFrames();
|
|
664
|
+
// COUNTED so the reviewer can prove it owns the queue before clearing it
|
|
665
|
+
// (§3.5.4a guard 2 / D-21). This method is the only other writer of
|
|
666
|
+
// `steeringQueue` in the package, and `Agent` exposes no way to remove ONE
|
|
667
|
+
// message — so `userSteerCount === 0` is the only sound licence for
|
|
668
|
+
// `clearAllQueues()`, and with a user message in there the reviewer must
|
|
669
|
+
// leave its own residue rather than destroy something the user typed.
|
|
670
|
+
this.userSteerCount += 1;
|
|
459
671
|
this.agent.steer(text);
|
|
460
672
|
}
|
|
461
673
|
followUp(text) {
|
|
@@ -632,6 +844,7 @@ export class AgentController {
|
|
|
632
844
|
/** Abort every child and clear timers. Idempotent; safe to call twice. */
|
|
633
845
|
dispose() {
|
|
634
846
|
this.teamRuntime?.dispose();
|
|
847
|
+
this.fast?.dispose();
|
|
635
848
|
}
|
|
636
849
|
// -----------------------------------------------------------------------
|
|
637
850
|
// Todo planning (todo-plan-execution §3.6a)
|
|
@@ -708,11 +921,47 @@ export class AgentController {
|
|
|
708
921
|
this.todos?.clear('user');
|
|
709
922
|
}
|
|
710
923
|
// -----------------------------------------------------------------------
|
|
924
|
+
// API retry (llm-api-retry-backoff §6.3)
|
|
925
|
+
//
|
|
926
|
+
// NO `retryRegistered` TWIN of the team / todo flag pairs, and that asymmetry
|
|
927
|
+
// is the point: retry is not a tool, so nothing is decided at construction.
|
|
928
|
+
// The policy lives on the `ProviderRegistry`, which means it can be replaced
|
|
929
|
+
// for a running session — including for children, because `TeamRuntime` holds
|
|
930
|
+
// the same registry instance.
|
|
931
|
+
// -----------------------------------------------------------------------
|
|
932
|
+
getRetryConfig() {
|
|
933
|
+
return this.config.retry;
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* Adopt a new retry config for this session AND PUSH IT INTO THE REGISTRY.
|
|
937
|
+
*
|
|
938
|
+
* THE SECOND CALL IS REQUIRED, NOT A NICETY (§6.3). Without it `/retry off`
|
|
939
|
+
* would persist correctly and change nothing until the next launch — the
|
|
940
|
+
* precise failure `config/schema.ts` warns about twice already, and the one a
|
|
941
|
+
* user reports as "the setting does nothing".
|
|
942
|
+
*
|
|
943
|
+
* Clamped here as well as on the persist path, so a bad value can reach neither
|
|
944
|
+
* the runtime nor the file.
|
|
945
|
+
*/
|
|
946
|
+
setRetryConfig(patch) {
|
|
947
|
+
const retry = clampRetryConfig({ ...this.config.retry, ...patch });
|
|
948
|
+
this.config = { ...this.config, retry };
|
|
949
|
+
this.providerRegistry.setRetryPolicy(toRetryPolicy(retry));
|
|
950
|
+
return retry;
|
|
951
|
+
}
|
|
952
|
+
// -----------------------------------------------------------------------
|
|
711
953
|
// Mutators
|
|
712
954
|
// -----------------------------------------------------------------------
|
|
713
955
|
setModel(provider, model, baseUrl) {
|
|
714
956
|
this.config = { ...this.config, provider, model, baseUrl };
|
|
715
957
|
this.agent.setModel({ providerId: provider, modelId: model, ...(baseUrl ? { baseUrl } : {}) });
|
|
958
|
+
// NOT NEUTRAL TO THE FAST TIER even when the user never touched a `fast.*`
|
|
959
|
+
// key (§3.9 / RV-3): `fast.provider: ''` and `fast.baseUrl: ''` INHERIT from
|
|
960
|
+
// exactly these three fields. Switching the main provider can therefore turn
|
|
961
|
+
// a working tier into `no_key`, or silently re-point an inheriting tier at
|
|
962
|
+
// another vendor's gateway — and `<fast_tier>` would go on naming the model
|
|
963
|
+
// it used to resolve to.
|
|
964
|
+
this.fast?.onConfigChanged();
|
|
716
965
|
}
|
|
717
966
|
setThinkingLevel(level) {
|
|
718
967
|
this.config = { ...this.config, thinkingLevel: level };
|
|
@@ -729,6 +978,9 @@ export class AgentController {
|
|
|
729
978
|
...this.config,
|
|
730
979
|
apiKeys: { ...this.config.apiKeys, [provider]: key },
|
|
731
980
|
};
|
|
981
|
+
// Rule 5 of §3.2 reads `apiKeys`, so a key edit is a tier event: this is the
|
|
982
|
+
// path that turns `no_key` back into a working tier without a relaunch.
|
|
983
|
+
this.fast?.onConfigChanged();
|
|
732
984
|
}
|
|
733
985
|
setCwd(cwd) {
|
|
734
986
|
this.cwd = cwd;
|
|
@@ -744,6 +996,61 @@ export class AgentController {
|
|
|
744
996
|
getCwd() {
|
|
745
997
|
return this.cwd;
|
|
746
998
|
}
|
|
999
|
+
/**
|
|
1000
|
+
* The structured diff a file tool left behind, CONSUMED ONCE.
|
|
1001
|
+
*
|
|
1002
|
+
* Called by `reduceEvent` on `tool_execution_end` and nowhere else, which is
|
|
1003
|
+
* what keeps the store at constant size however many files a session edits.
|
|
1004
|
+
* `undefined` is an ordinary answer — a tool that records nothing, a toolset
|
|
1005
|
+
* built without a recorder, or an entry the 64-write ring has already evicted
|
|
1006
|
+
* — and every one of those falls back to `ToolPreview`.
|
|
1007
|
+
*/
|
|
1008
|
+
takeFilePatch(toolCallId) {
|
|
1009
|
+
return this.fileChanges.take('lead', toolCallId);
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Sanitise a chunk into the store and hand the resulting tail to the view.
|
|
1013
|
+
*
|
|
1014
|
+
* SYNCHRONOUS, INSIDE THE CHILD-PROCESS DATA HANDLER. The alternative (queue
|
|
1015
|
+
* plus timer) adds a second clock to a component that already has one, and
|
|
1016
|
+
* `App` coalesces at the other end anyway (D-34).
|
|
1017
|
+
*
|
|
1018
|
+
* EVERY LISTENER IS WRAPPED, and that is not defensive noise (R-5 / AC-35): a
|
|
1019
|
+
* throw here propagates into `child.stdout.on('data')`, which is the reason a
|
|
1020
|
+
* render-side bug could otherwise kill a ten-minute build. `debug` rather than
|
|
1021
|
+
* `warn` because a listener that throws once throws on every chunk.
|
|
1022
|
+
*/
|
|
1023
|
+
emitToolOutput(owner, toolCallId, chunk) {
|
|
1024
|
+
if (!this.toolOutputs)
|
|
1025
|
+
return;
|
|
1026
|
+
const rows = this.toolOutputs.append(owner, toolCallId, chunk);
|
|
1027
|
+
for (const listener of this.outputListeners) {
|
|
1028
|
+
try {
|
|
1029
|
+
listener({ toolCallId, rows });
|
|
1030
|
+
}
|
|
1031
|
+
catch (err) {
|
|
1032
|
+
getLogger().debug('tool', 'tool_output_listener_threw', {
|
|
1033
|
+
toolCallId,
|
|
1034
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Subscribe to the CLI-local live tool-output stream (§3.1.3).
|
|
1041
|
+
*
|
|
1042
|
+
* Returns a no-op unsubscribe when `liveToolOutput` was off at construction,
|
|
1043
|
+
* so every caller can subscribe unconditionally -- the contract
|
|
1044
|
+
* `subscribeTeam` / `subscribeTodos` / `subscribeFast` already set.
|
|
1045
|
+
*/
|
|
1046
|
+
subscribeToolOutput(listener) {
|
|
1047
|
+
if (!this.toolOutputs)
|
|
1048
|
+
return () => { };
|
|
1049
|
+
this.outputListeners.add(listener);
|
|
1050
|
+
return () => {
|
|
1051
|
+
this.outputListeners.delete(listener);
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
747
1054
|
clearMessages() {
|
|
748
1055
|
this.agent.clearMessages();
|
|
749
1056
|
// A new conversation has no loaded skills, so the "already loaded earlier
|
|
@@ -782,10 +1089,95 @@ export class AgentController {
|
|
|
782
1089
|
}
|
|
783
1090
|
/** Resolve `ModelInfo` for the active model, falling back to a runtime model. */
|
|
784
1091
|
getModelInfo() {
|
|
785
|
-
|
|
1092
|
+
return this.getModelInfoFor({
|
|
1093
|
+
providerId: this.config.provider,
|
|
1094
|
+
modelId: this.config.model,
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Resolve `ModelInfo` for ANY model (fast-model-tier §3.6 / RV-4).
|
|
1099
|
+
*
|
|
1100
|
+
* ONE RESOLUTION PATH, which is the same reason §3.2 has exactly one
|
|
1101
|
+
* `resolveFastTier`: `getModelInfo()` above is now a call into this, so "the
|
|
1102
|
+
* cost table for the model that actually billed" cannot drift from "the cost
|
|
1103
|
+
* table for the session's model" as two separate bodies.
|
|
1104
|
+
*/
|
|
1105
|
+
getModelInfoFor(ref) {
|
|
1106
|
+
const found = this.modelRegistry.getModel(ref.providerId, ref.modelId);
|
|
786
1107
|
if (found)
|
|
787
1108
|
return found;
|
|
788
|
-
return this.modelRegistry.buildRuntimeModel(
|
|
1109
|
+
return this.modelRegistry.buildRuntimeModel(ref.providerId, ref.modelId);
|
|
1110
|
+
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Whether the STATIC table knows this model's price (C-11 / RV-4).
|
|
1113
|
+
*
|
|
1114
|
+
* `buildRuntimeModel` returns `cost: { input: 0, output: 0 }` for a model it
|
|
1115
|
+
* has never seen — a table of zeros, not a missing one — so every consumer
|
|
1116
|
+
* that renders spend needs to be able to tell "free" from "unknown". The fast
|
|
1117
|
+
* tier is precisely where an unrecognised id is likely, and `$0.00` on a
|
|
1118
|
+
* feature that is spending money is the same class of lie as pricing a Haiku
|
|
1119
|
+
* child at Sonnet rates.
|
|
1120
|
+
*/
|
|
1121
|
+
isPricedModel(ref) {
|
|
1122
|
+
return this.modelRegistry.getModel(ref.providerId, ref.modelId) !== undefined;
|
|
1123
|
+
}
|
|
1124
|
+
// -----------------------------------------------------------------------
|
|
1125
|
+
// Fast model tier (fast-model-tier §3.1) — FOUR THIN FORWARDERS
|
|
1126
|
+
// -----------------------------------------------------------------------
|
|
1127
|
+
/** Whether `task`'s schema carries `model`. Fixed at construction (C-2). */
|
|
1128
|
+
isFastRegistered() {
|
|
1129
|
+
return this.fast !== null;
|
|
1130
|
+
}
|
|
1131
|
+
/** Whether the tier will actually be used. Flipped live by `/fast on|off`. */
|
|
1132
|
+
isFastEnabled() {
|
|
1133
|
+
return this.fast?.isEnabled() === true;
|
|
1134
|
+
}
|
|
1135
|
+
/**
|
|
1136
|
+
* Flip the live switch and re-render the prompt.
|
|
1137
|
+
*
|
|
1138
|
+
* Turning it ON in a session that started without the tier deliberately does
|
|
1139
|
+
* NOT register it: the tool array is immutable (C-2), and advertising a schema
|
|
1140
|
+
* field that is not there is the dead end `/team on` already documents.
|
|
1141
|
+
* `/fast` reports that honestly by checking `isFastRegistered()` first.
|
|
1142
|
+
*/
|
|
1143
|
+
setFastEnabled(enabled) {
|
|
1144
|
+
this.fast?.setEnabled(enabled);
|
|
1145
|
+
}
|
|
1146
|
+
getFastConfig() {
|
|
1147
|
+
return this.config.fast;
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* Adopt a new fast config for this session.
|
|
1151
|
+
*
|
|
1152
|
+
* THE RE-RESOLUTION IS NOT OPTIONAL (§3.2 rule 8). `App` reads
|
|
1153
|
+
* `controller.getConfig()` on every render and the block interpolates the
|
|
1154
|
+
* model id, so a persist without this call reports success and changes
|
|
1155
|
+
* nothing until relaunch — the failure `/todo panel` records (P1-2).
|
|
1156
|
+
*/
|
|
1157
|
+
setFastConfig(patch) {
|
|
1158
|
+
const fast = clampFastConfig({ ...this.config.fast, ...patch });
|
|
1159
|
+
this.config = { ...this.config, fast };
|
|
1160
|
+
// `onConfigChanged` rebuilds the prompt; when there is no wiring (the tier
|
|
1161
|
+
// was never registered) nothing has to be rebuilt, because no block is
|
|
1162
|
+
// spliced either way.
|
|
1163
|
+
this.fast?.onConfigChanged();
|
|
1164
|
+
return fast;
|
|
1165
|
+
}
|
|
1166
|
+
/** Everything `/fast status`, the settings line and the chip read. */
|
|
1167
|
+
getFastStatus() {
|
|
1168
|
+
return this.fast ? this.fast.status() : offFastStatus();
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Subscribe to the CLI-local fast event stream.
|
|
1172
|
+
*
|
|
1173
|
+
* Returns a no-op unsubscribe when the tier was off at construction, so every
|
|
1174
|
+
* caller can subscribe unconditionally — the contract `subscribeTeam` and
|
|
1175
|
+
* `subscribeTodos` already set.
|
|
1176
|
+
*/
|
|
1177
|
+
subscribeFast(listener) {
|
|
1178
|
+
if (!this.fast)
|
|
1179
|
+
return () => { };
|
|
1180
|
+
return this.fast.subscribe(listener);
|
|
789
1181
|
}
|
|
790
1182
|
/** List usable tools (name + description) for the /tools command. */
|
|
791
1183
|
listTools() {
|
package/dist/agent/headless.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { AgentEvent, ModelInfo } from '@aragon-agent/core';
|
|
|
8
8
|
import type { PreflightResult } from './controller.js';
|
|
9
9
|
import type { TeamEvent } from '../team/types.js';
|
|
10
10
|
import type { TodoEvent, TodoSnapshot } from '../todo/types.js';
|
|
11
|
+
import type { FastEvent } from '../fast/types.js';
|
|
11
12
|
import { type FollowThroughMode } from '../todo/follow-through.js';
|
|
12
13
|
/** The minimal controller surface headless mode needs (real or stubbed). */
|
|
13
14
|
export interface HeadlessController {
|
|
@@ -35,6 +36,13 @@ export interface HeadlessController {
|
|
|
35
36
|
* keeps a `--no-todo` headless run byte-identical to today's.
|
|
36
37
|
*/
|
|
37
38
|
subscribeTodos?(listener: (event: TodoEvent) => void): () => void;
|
|
39
|
+
/**
|
|
40
|
+
* The CLI-local fast-tier stream — OPTIONAL, for the identical reason the two
|
|
41
|
+
* above are (fast-model-tier §3.9). A controller with no tier has no fast
|
|
42
|
+
* events, and `runHeadless` calls it with `?.`, which keeps a default `-p` run
|
|
43
|
+
* byte-identical to today's.
|
|
44
|
+
*/
|
|
45
|
+
subscribeFast?(listener: (event: FastEvent) => void): () => void;
|
|
38
46
|
/**
|
|
39
47
|
* The current plan — OPTIONAL, for the identical reason the two subscriptions
|
|
40
48
|
* above are (todo-plan-followthrough §3.6 rule 2). ABSENT MEANS THE
|
package/dist/agent/headless.js
CHANGED
|
@@ -21,6 +21,8 @@ export async function runHeadless(controller, prompt, options = {}) {
|
|
|
21
21
|
let sawTurnEnd = false;
|
|
22
22
|
let wroteText = false;
|
|
23
23
|
const total = { inputTokens: 0, outputTokens: 0 };
|
|
24
|
+
/** Retries seen this run, so the recovery line can name a count. */
|
|
25
|
+
let retryCount = 0;
|
|
24
26
|
const unsubscribe = controller.subscribe((event) => {
|
|
25
27
|
switch (event.type) {
|
|
26
28
|
case 'message_update': {
|
|
@@ -33,6 +35,25 @@ export async function runHeadless(controller, prompt, options = {}) {
|
|
|
33
35
|
errored = true;
|
|
34
36
|
err.write(`\n${formatStreamError(se.error)}\n`);
|
|
35
37
|
}
|
|
38
|
+
else if (se.type === 'retry_scheduled') {
|
|
39
|
+
// STDERR, NEVER STDOUT (llm-api-retry-backoff §6.7). stdout is the
|
|
40
|
+
// machine-readable answer and has to stay parseable by
|
|
41
|
+
// `aragon -p | jq`; a retry line on it would corrupt every consumer.
|
|
42
|
+
// Not `--quiet`-gated, unlike the tool lines: a three-minute pause with
|
|
43
|
+
// no explanation anywhere is not a "compact" run, it is a hang.
|
|
44
|
+
retryCount = se.attempt;
|
|
45
|
+
// The em dash is a literal because `agent/headless.ts` is the CLI's one
|
|
46
|
+
// documented glyph-scanner exemption: `aragon -p` does no capability
|
|
47
|
+
// probe, so there is no `Glyphs` set to read here.
|
|
48
|
+
err.write(`\n[retry ${se.attempt}/${se.maxRetries}] ${se.errorType} — ` +
|
|
49
|
+
`waiting ${Math.round(se.delayMs / 1000)}s\n`);
|
|
50
|
+
}
|
|
51
|
+
else if (se.type === 'stream_restart') {
|
|
52
|
+
// The partial answer already on stdout is about to be replayed. Saying so
|
|
53
|
+
// is the only way a piped consumer can tell a duplicated paragraph from
|
|
54
|
+
// a model that repeated itself.
|
|
55
|
+
err.write(`[retry] discarding a partial response and restarting\n`);
|
|
56
|
+
}
|
|
36
57
|
break;
|
|
37
58
|
}
|
|
38
59
|
case 'tool_execution_start':
|
|
@@ -46,6 +67,10 @@ export async function runHeadless(controller, prompt, options = {}) {
|
|
|
46
67
|
break;
|
|
47
68
|
case 'turn_end':
|
|
48
69
|
sawTurnEnd = true;
|
|
70
|
+
if (retryCount > 0) {
|
|
71
|
+
err.write(`[retry] recovered after ${retryCount} ${retryCount === 1 ? 'retry' : 'retries'}\n`);
|
|
72
|
+
retryCount = 0;
|
|
73
|
+
}
|
|
49
74
|
total.inputTokens += event.usage.inputTokens;
|
|
50
75
|
total.outputTokens += event.usage.outputTokens;
|
|
51
76
|
break;
|
|
@@ -128,6 +153,20 @@ export async function runHeadless(controller, prompt, options = {}) {
|
|
|
128
153
|
lastActive = activeIndex;
|
|
129
154
|
err.write(`[todo] ${activeIndex + 1}/${total} ${items[activeIndex]?.content ?? ''}\n`);
|
|
130
155
|
});
|
|
156
|
+
// Fast-tier review spend must reach the `[usage]` footer, or `-p` under-reports
|
|
157
|
+
// by however much the reviewer consumed (§3.6).
|
|
158
|
+
//
|
|
159
|
+
// NOTHING IS WRITTEN TO STDOUT, AND NOTHING TO STDERR EITHER (D-12). stdout is
|
|
160
|
+
// the answer channel and is piped into other tools; stderr in `-p` carries the
|
|
161
|
+
// tool trace, and an unrequested second opinion is not a tool the user asked
|
|
162
|
+
// for. The reviews are in the log records either way, which is where a `-p`
|
|
163
|
+
// user goes to ask what happened.
|
|
164
|
+
const unsubscribeFast = controller.subscribeFast?.((event) => {
|
|
165
|
+
if (event.type !== 'usage')
|
|
166
|
+
return;
|
|
167
|
+
total.inputTokens += event.usage.inputTokens;
|
|
168
|
+
total.outputTokens += event.usage.outputTokens;
|
|
169
|
+
});
|
|
131
170
|
// --- The bounded continuation loop (todo-plan-followthrough §3.6). --------
|
|
132
171
|
//
|
|
133
172
|
// THE THREE `unsubscribe` CALLS STAY OUTSIDE IT (rule 4). The loop wraps the
|
|
@@ -187,6 +226,7 @@ export async function runHeadless(controller, prompt, options = {}) {
|
|
|
187
226
|
unsubscribe();
|
|
188
227
|
unsubscribeTeam?.();
|
|
189
228
|
unsubscribeTodos?.();
|
|
229
|
+
unsubscribeFast?.();
|
|
190
230
|
}
|
|
191
231
|
// Ensure a trailing newline so piped output is well-formed.
|
|
192
232
|
if (wroteText)
|