@aragon-agent/cli 0.6.3 → 0.6.7
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 +142 -0
- package/README.md +203 -13
- package/dist/agent/controller.d.ts +158 -2
- package/dist/agent/controller.js +400 -1
- package/dist/agent/reducer.d.ts +102 -19
- package/dist/agent/reducer.js +162 -71
- package/dist/agent/system-prompt.d.ts +14 -0
- package/dist/agent/system-prompt.js +2 -0
- package/dist/agent/usage.d.ts +34 -0
- package/dist/agent/usage.js +43 -0
- package/dist/cli.js +132 -23
- package/dist/commands/builtins.js +101 -1
- package/dist/compaction/child.js +5 -0
- package/dist/compaction/command.js +14 -1
- package/dist/compaction/compactor.d.ts +94 -24
- package/dist/compaction/compactor.js +247 -98
- package/dist/compaction/context-command.d.ts +33 -0
- package/dist/compaction/context-command.js +166 -0
- package/dist/compaction/meter.d.ts +251 -0
- package/dist/compaction/meter.js +428 -0
- package/dist/compaction/pressure.d.ts +6 -0
- package/dist/compaction/pressure.js +4 -0
- package/dist/compaction/types.d.ts +77 -0
- package/dist/compaction/wiring.d.ts +56 -0
- package/dist/compaction/wiring.js +107 -25
- package/dist/config/cli-commands.js +5 -0
- package/dist/config/env.d.ts +1 -1
- package/dist/config/env.js +63 -2
- package/dist/config/load.d.ts +28 -0
- package/dist/config/load.js +83 -1
- package/dist/config/schema.d.ts +129 -0
- package/dist/config/schema.js +69 -0
- package/dist/config/store.js +47 -1
- package/dist/exec/events.d.ts +138 -1
- package/dist/exec/events.js +42 -0
- package/dist/exec/index.js +43 -6
- package/dist/exec/progress-reporter.d.ts +23 -0
- package/dist/exec/progress-reporter.js +88 -0
- package/dist/exec/runner.d.ts +34 -0
- package/dist/exec/runner.js +137 -25
- package/dist/fast/reviewer.d.ts +19 -1
- package/dist/fast/reviewer.js +22 -0
- package/dist/fast/types.d.ts +15 -0
- package/dist/fast/wiring.d.ts +10 -1
- package/dist/fast/wiring.js +18 -1
- package/dist/input/limits.d.ts +72 -0
- package/dist/input/limits.js +63 -0
- package/dist/input/paste-parse.d.ts +71 -0
- package/dist/input/paste-parse.js +95 -0
- package/dist/input/stdin-filter.d.ts +81 -0
- package/dist/input/stdin-filter.js +379 -0
- package/dist/logging/install.d.ts +2 -0
- package/dist/logging/install.js +53 -1
- package/dist/proc/classify.d.ts +52 -0
- package/dist/proc/classify.js +143 -0
- package/dist/proc/kill-tree.d.ts +42 -0
- package/dist/proc/kill-tree.js +119 -0
- package/dist/proc/limits.d.ts +84 -0
- package/dist/proc/limits.js +84 -0
- package/dist/proc/log-ring.d.ts +68 -0
- package/dist/proc/log-ring.js +102 -0
- package/dist/proc/prompt.d.ts +24 -0
- package/dist/proc/prompt.js +49 -0
- package/dist/proc/readiness.d.ts +102 -0
- package/dist/proc/readiness.js +219 -0
- package/dist/proc/supervisor.d.ts +103 -0
- package/dist/proc/supervisor.js +465 -0
- package/dist/proc/types.d.ts +175 -0
- package/dist/proc/types.js +15 -0
- package/dist/session/persist.js +36 -1
- package/dist/todo/limits.d.ts +4 -17
- package/dist/todo/limits.js +4 -17
- package/dist/todo/panel-rows.d.ts +8 -6
- package/dist/todo/panel-rows.js +19 -6
- package/dist/tools/bash-tool.d.ts +51 -2
- package/dist/tools/bash-tool.js +321 -51
- package/dist/tools/fs-tools.d.ts +17 -0
- package/dist/tools/index.d.ts +55 -2
- package/dist/tools/index.js +39 -1
- package/dist/tools/proc-tools.d.ts +25 -0
- package/dist/tools/proc-tools.js +102 -0
- package/dist/ui/App.d.ts +15 -2
- package/dist/ui/App.js +330 -69
- package/dist/ui/AutocompletePopup.d.ts +3 -1
- package/dist/ui/AutocompletePopup.js +9 -11
- package/dist/ui/BottomStatusRow.d.ts +1 -1
- package/dist/ui/Composer.d.ts +56 -1
- package/dist/ui/Composer.js +50 -6
- package/dist/ui/PromptInput.d.ts +52 -3
- package/dist/ui/PromptInput.js +201 -55
- package/dist/ui/StatusBar.d.ts +40 -15
- package/dist/ui/StatusBar.js +51 -9
- package/dist/ui/TeamPanel.d.ts +3 -1
- package/dist/ui/TeamPanel.js +13 -9
- package/dist/ui/TodoPanel.d.ts +5 -5
- package/dist/ui/TodoPanel.js +56 -24
- package/dist/ui/Transcript.js +29 -1
- package/dist/ui/UpdateLine.d.ts +1 -1
- package/dist/ui/capabilities.js +5 -5
- package/dist/ui/composer-limits.d.ts +31 -0
- package/dist/ui/composer-limits.js +50 -0
- package/dist/ui/composer-rows.d.ts +73 -0
- package/dist/ui/composer-rows.js +201 -0
- package/dist/ui/editor-reducer.d.ts +37 -0
- package/dist/ui/editor-reducer.js +88 -12
- package/dist/ui/entries/ServiceCard.d.ts +66 -0
- package/dist/ui/entries/ServiceCard.js +116 -0
- package/dist/ui/entries/UserEntry.d.ts +17 -3
- package/dist/ui/entries/UserEntry.js +34 -5
- package/dist/ui/exit-snapshot.d.ts +9 -0
- package/dist/ui/layout/ScrollViewport.d.ts +1 -1
- package/dist/ui/layout/autocomplete.d.ts +18 -0
- package/dist/ui/layout/autocomplete.js +26 -0
- package/dist/ui/layout/budget.d.ts +34 -6
- package/dist/ui/layout/budget.js +32 -7
- package/dist/ui/layout/rail.d.ts +4 -53
- package/dist/ui/layout/rail.js +10 -62
- package/dist/ui/layout/team-panel.d.ts +18 -0
- package/dist/ui/layout/team-panel.js +22 -0
- package/dist/ui/layout/todo-layout.d.ts +24 -0
- package/dist/ui/layout/todo-layout.js +27 -0
- package/dist/ui/layout/virtual-window.js +51 -1
- package/dist/ui/overlays/HelpOverlay.js +3 -0
- package/dist/ui/overlays/PlanReviewOverlay.js +10 -2
- package/dist/ui/overlays/QuestionOverlay.js +5 -1
- package/dist/ui/overlays/SettingsScreen.js +14 -1
- package/dist/ui/paste-frames.d.ts +87 -0
- package/dist/ui/paste-frames.js +112 -0
- package/dist/ui/paste-tokens.d.ts +90 -0
- package/dist/ui/paste-tokens.js +147 -0
- package/dist/ui/screen.d.ts +16 -0
- package/dist/ui/screen.js +45 -2
- package/dist/ui/selection/selection-controller.d.ts +1 -1
- package/dist/ui/transcript-text.js +55 -2
- package/dist/ui/use-wheel-routing.d.ts +1 -1
- package/package.json +2 -2
- package/dist/input/stdin-mouse-filter.d.ts +0 -47
- package/dist/input/stdin-mouse-filter.js +0 -153
package/dist/agent/controller.js
CHANGED
|
@@ -20,9 +20,15 @@ import { TeamHumanQueue } from '../team/human-queue.js';
|
|
|
20
20
|
import { createTaskTool } from '../team/task-tool.js';
|
|
21
21
|
import { buildTeamBlock } from '../team/prompt.js';
|
|
22
22
|
import { buildFastBlock } from '../fast/prompt.js';
|
|
23
|
+
import { ProcSupervisor } from '../proc/supervisor.js';
|
|
24
|
+
import { buildBackgroundServicesBlock } from '../proc/prompt.js';
|
|
25
|
+
import { PROC_LIMITS } from '../proc/limits.js';
|
|
26
|
+
import { makeBashKill, makeBashOutput } from '../tools/proc-tools.js';
|
|
27
|
+
import { addSignalHook } from '../logging/install.js';
|
|
23
28
|
import { describeFastTierProblem, fastProviderOf, resolveFastTier } from '../fast/resolve.js';
|
|
24
29
|
import { FastWiring, offFastStatus } from '../fast/wiring.js';
|
|
25
30
|
import { CompactionWiring, offCompactionSnapshot } from '../compaction/wiring.js';
|
|
31
|
+
import { ContextMeter } from '../compaction/meter.js';
|
|
26
32
|
import { TodoStore } from '../todo/store.js';
|
|
27
33
|
import { createTodoTool } from '../todo/todo-tool.js';
|
|
28
34
|
import { buildTodoBlock } from '../todo/prompt.js';
|
|
@@ -33,6 +39,15 @@ import { createNodeSkillHost } from '../skills/node-host.js';
|
|
|
33
39
|
import { SkillService } from '../skills/service.js';
|
|
34
40
|
import { createSkillTools } from '../skills/tools.js';
|
|
35
41
|
import { buildSystemPrompt } from './system-prompt.js';
|
|
42
|
+
/**
|
|
43
|
+
* Extra budget, on top of `PROC_LIMITS.killGraceMs`, that `prompt()` allows a
|
|
44
|
+
* force-stopped engine to unwind in before it starts a new run anyway (I-8).
|
|
45
|
+
*
|
|
46
|
+
* `bash`'s own kill grace is what a wedged foreground command costs; this covers
|
|
47
|
+
* the loop's own unwinding on top of it. Waiting FOREVER is not an option - a
|
|
48
|
+
* tool that never settles is exactly why rung two exists.
|
|
49
|
+
*/
|
|
50
|
+
const ENGINE_UNWIND_GRACE_MS = 3000;
|
|
36
51
|
/** Approval gate used whenever there is provably no human to ask (headless). */
|
|
37
52
|
export const DENY_ALL_APPROVAL = {
|
|
38
53
|
canPrompt: () => false,
|
|
@@ -172,6 +187,65 @@ export class AgentController {
|
|
|
172
187
|
// to subscribe to the Agent, and this class cannot do both in one step.
|
|
173
188
|
// -----------------------------------------------------------------------
|
|
174
189
|
compaction;
|
|
190
|
+
// -----------------------------------------------------------------------
|
|
191
|
+
// Context occupancy (context-usage-gauge-accuracy §3.2 / W1)
|
|
192
|
+
//
|
|
193
|
+
// CONSTRUCTED UNCONDITIONALLY, unlike its neighbour, and that asymmetry IS the
|
|
194
|
+
// feature. `compaction` above is `null` for a session started with
|
|
195
|
+
// `--no-compaction` - and before this field existed, that took the only
|
|
196
|
+
// measuring code in the process with it: such a session's gauge had exactly
|
|
197
|
+
// one sample per turn (the provider's `turn_end` usage) and read `0 %` after a
|
|
198
|
+
// `/resume`. Occupancy is not a compaction concern; it is what compaction
|
|
199
|
+
// READS.
|
|
200
|
+
//
|
|
201
|
+
// IT COSTS A HEADLESS HOST NOTHING (I-11). `aragon exec` builds an
|
|
202
|
+
// `AgentController` too, and nothing there reads this number; the meter arms
|
|
203
|
+
// no timer while it has no subscribers, so the extra object is a few scalars
|
|
204
|
+
// and a `Set`.
|
|
205
|
+
// -----------------------------------------------------------------------
|
|
206
|
+
contextMeter;
|
|
207
|
+
// -----------------------------------------------------------------------
|
|
208
|
+
// Background services (background-service-supervision §3.3 / §3.6)
|
|
209
|
+
//
|
|
210
|
+
// CONSTRUCTED UNCONDITIONALLY, and that is the one thing about this field that
|
|
211
|
+
// is easy to get wrong (P1-6 / D-9). `cfg.bash.background` gates the SERVICE
|
|
212
|
+
// half - the `background` tool param, `bash_output`/`bash_kill`, the prompt
|
|
213
|
+
// block, the status chip - and NEVER the foreground half, because G2 ("bash
|
|
214
|
+
// always settles") and G3 ("Esc, Esc always works, and the session stays
|
|
215
|
+
// usable") are promised unconditionally. Gating the whole supervisor on the
|
|
216
|
+
// flag would make rung two of the Esc ladder a silent no-op for every user who
|
|
217
|
+
// turned background services off.
|
|
218
|
+
//
|
|
219
|
+
// With the flag off it holds foreground pids, nothing else, and allocates no
|
|
220
|
+
// timers.
|
|
221
|
+
// -----------------------------------------------------------------------
|
|
222
|
+
procs;
|
|
223
|
+
/**
|
|
224
|
+
* The host's notice channel, kept so `prompt()`'s totality has somewhere to
|
|
225
|
+
* report to (I-8). Every other consumer reads `deps.notify` through a closure
|
|
226
|
+
* built in the constructor; this one is called from a method, which is why it
|
|
227
|
+
* needs a field.
|
|
228
|
+
*/
|
|
229
|
+
notifyHostFn;
|
|
230
|
+
/** Whether the SERVICE half is on: tools, prompt block, `background` param. */
|
|
231
|
+
bashBackgroundRegistered;
|
|
232
|
+
/** Release the exit reaper. Called by `dispose()`. */
|
|
233
|
+
releaseSignalHook;
|
|
234
|
+
/**
|
|
235
|
+
* Bumped by `forceStop()`; captured by `App` when it subscribes to a run.
|
|
236
|
+
*
|
|
237
|
+
* THE VIEW AND THE ENGINE DELIBERATELY DISAGREE AFTER A FORCE-STOP (D-10 /
|
|
238
|
+
* I-7). Rung two dispatches `runEnd` locally so the composer is usable
|
|
239
|
+
* immediately, but the engine keeps unwinding and keeps EMITTING -
|
|
240
|
+
* `turn_start`, `tool_call_start`, `turn_end`, `agent_end` - and
|
|
241
|
+
* `runStart`/`turnStart` would set `status: 'running'` again, bouncing the
|
|
242
|
+
* view straight back out of the `idle` the user was just given and appending
|
|
243
|
+
* entries to a transcript they believe is finished. `App` drops every event
|
|
244
|
+
* whose generation is stale, which is the same shape as `abortRequested`
|
|
245
|
+
* above and exists for the same reason: the loop legitimately emits after an
|
|
246
|
+
* abort lands.
|
|
247
|
+
*/
|
|
248
|
+
runGen = 0;
|
|
175
249
|
/**
|
|
176
250
|
* Set SYNCHRONOUSLY by `abort()` before `agent.abort()` (§3.5.4a guard 1).
|
|
177
251
|
* Cleared at the next `agent_start`.
|
|
@@ -290,6 +364,19 @@ export class AgentController {
|
|
|
290
364
|
// reason the field's own comment gives: the binding below is spread
|
|
291
365
|
// conditionally on it (§3.1.3 / P1-6).
|
|
292
366
|
this.toolOutputs = config.liveToolOutput ? createToolOutputStore() : null;
|
|
367
|
+
// ALSO ABOVE `createBuiltinTools`, and UNCONDITIONAL - see the field's own
|
|
368
|
+
// comment. `readyTimeoutMs` is read through a closure rather than captured,
|
|
369
|
+
// so a live config change reaches the next service rather than the next
|
|
370
|
+
// launch of the CLI.
|
|
371
|
+
this.procs = new ProcSupervisor({ readyTimeoutMs: () => this.config.bash.readyTimeoutMs });
|
|
372
|
+
this.notifyHostFn = deps.notify;
|
|
373
|
+
this.bashBackgroundRegistered = config.bash.background;
|
|
374
|
+
// THE EXIT REAPER (G5 / P1-2 / R-10). An APPEND-ONLY hook rather than a
|
|
375
|
+
// second `setSignalTerminator`: that is a single slot `cli.tsx` already owns
|
|
376
|
+
// for the alternate-screen restore, and replacing it is the documented bug
|
|
377
|
+
// that leaves the user staring at a blank alternate screen. `reapSync` is
|
|
378
|
+
// the only entry point it calls, and it is synchronous end to end (I-9).
|
|
379
|
+
this.releaseSignalHook = addSignalHook(() => this.procs.reapSync());
|
|
293
380
|
this.tools = createBuiltinTools({
|
|
294
381
|
getCwd: () => this.cwd,
|
|
295
382
|
// OWNED HERE, NOT IN `cli.tsx` (D-18): `createBuiltinTools` is called from
|
|
@@ -306,6 +393,28 @@ export class AgentController {
|
|
|
306
393
|
...(this.toolOutputs
|
|
307
394
|
? { recordOutput: (id, chunk) => this.emitToolOutput('lead', id, chunk) }
|
|
308
395
|
: {}),
|
|
396
|
+
// THE SERVICE HALF, AND ONLY THE SERVICE HALF (P1-6 / I-2). With
|
|
397
|
+
// `bash.background: false` the options bag has no `procs` key at all, so
|
|
398
|
+
// `createBuiltinTools` adds no property to `deps`, `bash` gets its default
|
|
399
|
+
// `{}` second argument, and the tool is the pre-feature one: no
|
|
400
|
+
// `background` in its schema, the pre-feature description string, no
|
|
401
|
+
// supervisor call. The FOREGROUND half of `this.procs` is still live, and
|
|
402
|
+
// `forceStop()` still reaches it.
|
|
403
|
+
...(this.bashBackgroundRegistered
|
|
404
|
+
? {
|
|
405
|
+
procs: this.procs,
|
|
406
|
+
// Read LIVE for the reason every other closure in this bag is:
|
|
407
|
+
// `aragon config set bash.autoBackground false` must take effect in
|
|
408
|
+
// the running session.
|
|
409
|
+
autoBackground: () => this.config.bash.autoBackground,
|
|
410
|
+
startupSettleMs: () => this.config.bash.startupSettleMs,
|
|
411
|
+
// D-15 / P1-8. `planInteractive` is the SAME signal `planTools`
|
|
412
|
+
// already uses to decide that no overlay can be rendered, which is
|
|
413
|
+
// exactly the question being asked here: can a card be drawn, and is
|
|
414
|
+
// there a user who could keep a service alive?
|
|
415
|
+
interactive: this.planInteractive,
|
|
416
|
+
}
|
|
417
|
+
: {}),
|
|
309
418
|
confirmTools: config.confirmTools,
|
|
310
419
|
// Routed through the same watchdog pause as the plan tools. This is a
|
|
311
420
|
// pre-existing bug being fixed in passing: a `--confirm` user who took
|
|
@@ -345,6 +454,12 @@ export class AgentController {
|
|
|
345
454
|
}),
|
|
346
455
|
]
|
|
347
456
|
: [],
|
|
457
|
+
// THROUGH THE FACTORY for the reason `teamTools` records below (P0-2), and
|
|
458
|
+
// empty when background services were off at construction - which leaves
|
|
459
|
+
// the array byte-identical to the pre-feature build (I-2).
|
|
460
|
+
procTools: this.bashBackgroundRegistered
|
|
461
|
+
? [makeBashOutput({ supervisor: this.procs }), makeBashKill({ supervisor: this.procs })]
|
|
462
|
+
: [],
|
|
348
463
|
// THROUGH THE FACTORY, NEVER APPENDED AFTERWARDS (D-15 / P0-1).
|
|
349
464
|
// `tools.test.ts::C7` asserts `HOST_TOOL_NAMES` equals what
|
|
350
465
|
// `createBuiltinTools` produces, so appending `task` here would turn that
|
|
@@ -403,6 +518,28 @@ export class AgentController {
|
|
|
403
518
|
modelId: config.model,
|
|
404
519
|
...(config.baseUrl ? { baseUrl: config.baseUrl } : {}),
|
|
405
520
|
};
|
|
521
|
+
// --- Context occupancy, part 1: BEFORE the Agent, UNCONDITIONALLY ------
|
|
522
|
+
//
|
|
523
|
+
// Built here for the same C-12 reason the wiring is - lazy `getMessages` /
|
|
524
|
+
// `getSystemPrompt` closures, `attach` after `new Agent` - and built for
|
|
525
|
+
// EVERY session, including one with compaction off, because that is the one
|
|
526
|
+
// whose gauge was broken (P1-4 / P0-2).
|
|
527
|
+
this.contextMeter = new ContextMeter({
|
|
528
|
+
getMessages: () => this.agent.state.messages,
|
|
529
|
+
getSystemPrompt: () => this.agent.state.systemPrompt,
|
|
530
|
+
getModelInfo: () => this.getModelInfo(),
|
|
531
|
+
// ONE PREDICATE FOR "IS THE DENOMINATOR REAL". `isPricedModel` is
|
|
532
|
+
// `modelRegistry.getModel(...) !== undefined`, and `getModelInfoFor` falls
|
|
533
|
+
// back to `buildRuntimeModel` - a 128k placeholder AND a zero cost table -
|
|
534
|
+
// for exactly the models it answers `false` for. The two are the same
|
|
535
|
+
// question about the same static table, not a coincidence.
|
|
536
|
+
isWindowKnown: () => this.isPricedModel({
|
|
537
|
+
providerId: this.config.provider,
|
|
538
|
+
modelId: this.config.model,
|
|
539
|
+
}),
|
|
540
|
+
// LIVE, so a settings-screen edit moves the denominator without a relaunch.
|
|
541
|
+
getWindowOverride: () => this.config.contextWindow,
|
|
542
|
+
});
|
|
406
543
|
// --- Context compaction, part 1: BEFORE the Agent ----------------------
|
|
407
544
|
//
|
|
408
545
|
// It needs no agent reference to EXIST — only live config, the key
|
|
@@ -420,6 +557,9 @@ export class AgentController {
|
|
|
420
557
|
getMessages: () => this.agent.state.messages,
|
|
421
558
|
getSystemPrompt: () => this.agent.state.systemPrompt,
|
|
422
559
|
notify: (level, text) => deps.notify?.(level, text),
|
|
560
|
+
// ONE METER PER PROCESS (R-1). The wiring forwards it to the compactor,
|
|
561
|
+
// so the trigger and the gauge cannot read two different numbers.
|
|
562
|
+
meter: this.contextMeter,
|
|
423
563
|
})
|
|
424
564
|
: null;
|
|
425
565
|
this.agent = new Agent({
|
|
@@ -470,6 +610,15 @@ export class AgentController {
|
|
|
470
610
|
// subscribed could not run before `new Agent(...)`, and the port has to be
|
|
471
611
|
// inside its constructor argument (C-12 / P0-2).
|
|
472
612
|
this.compaction?.attach((listener) => this.agent.subscribe(listener));
|
|
613
|
+
// --- Context occupancy, part 2: after the Agent exists -----------------
|
|
614
|
+
//
|
|
615
|
+
// ORDER RELATIVE TO THE LINE ABOVE IS DELIBERATELY IRRELEVANT (I-9 / T7).
|
|
616
|
+
// The append direction is order-free because the meter marks itself dirty
|
|
617
|
+
// and re-measures on read; the replace direction is order-free because the
|
|
618
|
+
// splice is announced from `settlePending`'s own synchronous code rather
|
|
619
|
+
// than from a listener. If a future change makes this ordering matter, the
|
|
620
|
+
// fix is at those two sites, not here.
|
|
621
|
+
this.contextMeter.attach((listener) => this.agent.subscribe(listener));
|
|
473
622
|
// --- Fast tier, part 2: after the Agent exists --------------------------
|
|
474
623
|
//
|
|
475
624
|
// `FastWiring` subscribes to the lead's event stream, so it cannot be built
|
|
@@ -612,6 +761,12 @@ export class AgentController {
|
|
|
612
761
|
review: this.config.fast.review,
|
|
613
762
|
})
|
|
614
763
|
: '',
|
|
764
|
+
// ONE FLAG, unlike team/todo's two: there is no live on/off switch for
|
|
765
|
+
// background services - `bash.background` is read once at construction,
|
|
766
|
+
// and the tools it registers cannot be unregistered (the `team.enabled` /
|
|
767
|
+
// `todo.enabled` rule). So the block is present exactly when the tools are,
|
|
768
|
+
// and can never advertise a capability the model does not have.
|
|
769
|
+
backgroundBlock: this.bashBackgroundRegistered ? buildBackgroundServicesBlock() : '',
|
|
615
770
|
// Read from the FIELD on every rebuild, for the reason the field's own
|
|
616
771
|
// comment gives. Empty everywhere but `aragon exec`, and `buildSystemPrompt`
|
|
617
772
|
// splices it conditionally, so every existing prompt is byte-identical.
|
|
@@ -700,7 +855,64 @@ export class AgentController {
|
|
|
700
855
|
* underlying promise so headless mode can await completion. `agent.prompt()`
|
|
701
856
|
* never rejects, so callers must not treat resolution as success.
|
|
702
857
|
*/
|
|
703
|
-
prompt(text) {
|
|
858
|
+
async prompt(text) {
|
|
859
|
+
// I-8 - THIS METHOD NEVER REJECTS, AND THAT HAS TO BE A PROPERTY OF THE
|
|
860
|
+
// METHOD RATHER THAN A COMMENT ABOVE IT (P0-1).
|
|
861
|
+
//
|
|
862
|
+
// `App` calls this as `void controller.prompt(...)`, and
|
|
863
|
+
// `logging/install.ts` routes an `unhandledRejection` into `handleFatal`,
|
|
864
|
+
// which EXITS THE PROCESS. `Agent.prompt()` throws SYNCHRONOUSLY while
|
|
865
|
+
// `Agent.running` is true - so before the force-stop ladder existed this was
|
|
866
|
+
// merely nearly-true (the view was only ever idle when the engine was), and
|
|
867
|
+
// rung two makes the view idle while the engine is still unwinding. Without
|
|
868
|
+
// the wait and the guard below, "Esc, Esc, then type" would kill the CLI on
|
|
869
|
+
// the very screen this feature exists to rescue.
|
|
870
|
+
//
|
|
871
|
+
// TWO PARTS, BOTH REQUIRED. First WAIT the engine out, bounded, so the
|
|
872
|
+
// common case (the engine unwinds in a few hundred ms) starts a real run
|
|
873
|
+
// rather than reporting a failure. Then swallow anything that still escapes
|
|
874
|
+
// and surface it through the existing notify channel, because a message that
|
|
875
|
+
// cannot be started must produce a visible sentence - never a crash, and
|
|
876
|
+
// never silence.
|
|
877
|
+
if (this.agent.state.isRunning) {
|
|
878
|
+
this.abort();
|
|
879
|
+
await this.waitForEngineIdle();
|
|
880
|
+
}
|
|
881
|
+
try {
|
|
882
|
+
await this.startPrompt(text);
|
|
883
|
+
}
|
|
884
|
+
catch (err) {
|
|
885
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
886
|
+
getLogger().warn('agent', 'prompt_failed', { reason });
|
|
887
|
+
this.notifyHostFn?.('error', `Could not start the run: ${reason}`);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* Wait for the engine to unwind, bounded.
|
|
892
|
+
*
|
|
893
|
+
* BOUNDED BECAUSE THE UNBOUNDED CASE IS THE ONE THIS FEATURE IS ABOUT: a tool
|
|
894
|
+
* that never settles is exactly why rung two exists, and `Agent.waitForIdle()`
|
|
895
|
+
* alone would hang here for as long as it hangs there. The budget is the two
|
|
896
|
+
* graces a wedged `bash` can legitimately spend, plus the executor's own.
|
|
897
|
+
*/
|
|
898
|
+
waitForEngineIdle() {
|
|
899
|
+
const budget = PROC_LIMITS.killGraceMs + ENGINE_UNWIND_GRACE_MS;
|
|
900
|
+
return new Promise((resolve) => {
|
|
901
|
+
let done = false;
|
|
902
|
+
const finish = () => {
|
|
903
|
+
if (done)
|
|
904
|
+
return;
|
|
905
|
+
done = true;
|
|
906
|
+
clearTimeout(timer);
|
|
907
|
+
resolve();
|
|
908
|
+
};
|
|
909
|
+
const timer = setTimeout(finish, budget);
|
|
910
|
+
timer.unref?.();
|
|
911
|
+
void this.agent.waitForIdle().then(finish, finish);
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
/** The pre-round body of `prompt`, unchanged apart from being private. */
|
|
915
|
+
startPrompt(text) {
|
|
704
916
|
// A new user message is a new intent, so the previous turn's tool ceiling
|
|
705
917
|
// expires here and anything a slash command queued is promoted (D-G1 / D-G2).
|
|
706
918
|
// This and `steer()` below are the ONLY two places a user message enters the
|
|
@@ -746,6 +958,115 @@ export class AgentController {
|
|
|
746
958
|
// `App.tsx` records for `cancelPendingHuman()` on `agent_end`.
|
|
747
959
|
this.teamRuntime?.abortAll();
|
|
748
960
|
}
|
|
961
|
+
/**
|
|
962
|
+
* Rung two of the Esc ladder: stop the AGENT, whatever it is blocked on
|
|
963
|
+
* (§3.6 / G3).
|
|
964
|
+
*
|
|
965
|
+
* FOUR STEPS, AND THE FOURTH IS AN ABSENCE.
|
|
966
|
+
*
|
|
967
|
+
* 1. `abort()` - everything it does today, unchanged.
|
|
968
|
+
* 2. `killForeground('force')` - hard-kill every tracked foreground `bash`
|
|
969
|
+
* child. THIS is what actually unblocks a wedged `await tool.execute`;
|
|
970
|
+
* an abort signal a tool ignores is advisory, and rung two exists for
|
|
971
|
+
* precisely the tools that ignore it.
|
|
972
|
+
* 3. `runGen += 1` - so `App` can drop the events the engine will keep
|
|
973
|
+
* emitting while it unwinds (I-7). Without it the view bounces straight
|
|
974
|
+
* back out of the `idle` the user was just given.
|
|
975
|
+
* 4. IT DOES NOT TOUCH BACKGROUND SERVICES. `Esc` interrupts the AGENT;
|
|
976
|
+
* `Ctrl+C` stops the SERVICES. That asymmetry is the user's own two
|
|
977
|
+
* sentences and it is exactly the kind of thing a later reader will
|
|
978
|
+
* "fix" by making this kill everything - which would mean a user who
|
|
979
|
+
* pressed Esc twice to stop a runaway turn also lost the dev server they
|
|
980
|
+
* had been working against for an hour. It is invariant I-4.
|
|
981
|
+
*
|
|
982
|
+
* UNCONDITIONAL ON `bash.background` (P1-6 / D-9). The supervisor is always
|
|
983
|
+
* constructed; with services off its foreground registry is all there is, and
|
|
984
|
+
* that is the half this method uses.
|
|
985
|
+
*/
|
|
986
|
+
forceStop() {
|
|
987
|
+
this.abort();
|
|
988
|
+
const killed = this.procs.killForeground('force');
|
|
989
|
+
this.runGen += 1;
|
|
990
|
+
getLogger().info('agent', 'force_stop', { generation: this.runGen, killed });
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* The current run generation (I-7).
|
|
994
|
+
*
|
|
995
|
+
* `App` captures it when it subscribes to a run and drops every engine event
|
|
996
|
+
* whose generation is stale. Same shape as the `abortRequested` guard the fast
|
|
997
|
+
* reviewer already uses, and for the same reason: the loop legitimately emits
|
|
998
|
+
* after an abort lands.
|
|
999
|
+
*/
|
|
1000
|
+
get runGeneration() {
|
|
1001
|
+
return this.runGen;
|
|
1002
|
+
}
|
|
1003
|
+
// -----------------------------------------------------------------------
|
|
1004
|
+
// Background services (background-service-supervision §3.5 / §3.6)
|
|
1005
|
+
// -----------------------------------------------------------------------
|
|
1006
|
+
/** Whether `bash_output` / `bash_kill` are in `this.tools`. Fixed at construction. */
|
|
1007
|
+
isBackgroundRegistered() {
|
|
1008
|
+
return this.bashBackgroundRegistered;
|
|
1009
|
+
}
|
|
1010
|
+
/**
|
|
1011
|
+
* Subscribe to the CLI-local `ProcEvent` stream.
|
|
1012
|
+
*
|
|
1013
|
+
* Returns a live unsubscribe even when services are off, because the
|
|
1014
|
+
* supervisor exists either way - the contract `subscribeTeam` /
|
|
1015
|
+
* `subscribeTodos` / `subscribeFast` / `subscribeToolOutput` already set. With
|
|
1016
|
+
* services off nothing is ever emitted, so an unconditional subscription in
|
|
1017
|
+
* `App` costs one `Set` entry.
|
|
1018
|
+
*/
|
|
1019
|
+
subscribeProc(listener) {
|
|
1020
|
+
return this.procs.subscribe(listener);
|
|
1021
|
+
}
|
|
1022
|
+
getServiceSnapshot(id) {
|
|
1023
|
+
return this.procs.get(id);
|
|
1024
|
+
}
|
|
1025
|
+
listServices() {
|
|
1026
|
+
return this.procs.list();
|
|
1027
|
+
}
|
|
1028
|
+
/** `starting | ready | running`. Drives the status chip and the Ctrl+C rung. */
|
|
1029
|
+
liveServiceCount() {
|
|
1030
|
+
return this.procs.liveCount();
|
|
1031
|
+
}
|
|
1032
|
+
readServiceLog(id, since) {
|
|
1033
|
+
return this.procs.read(id, since);
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Stop one service, or every service when `id` is `'all'`.
|
|
1037
|
+
*
|
|
1038
|
+
* The GRACEFUL ladder (`SIGTERM` -> `SIGKILL` after `stopGraceMs`), because
|
|
1039
|
+
* this path has a live event loop to run it on. `reapServicesSync` is the
|
|
1040
|
+
* other one, and it is the only one an exit path may use.
|
|
1041
|
+
*/
|
|
1042
|
+
stopService(id) {
|
|
1043
|
+
return this.procs.stop(id);
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Rung one of `Ctrl+C`: stop every live service (§3.6 / G4).
|
|
1047
|
+
*
|
|
1048
|
+
* `force: true` means `reapSync()` SEMANTICS - immediate `SIGKILL`, no
|
|
1049
|
+
* `stopGraceMs` - and is what `doExit` uses, because `doExit` awaits nothing
|
|
1050
|
+
* and must not start (§3.6 step 3). The graceful ladder belongs to `Ctrl+C`,
|
|
1051
|
+
* `bash_kill` and `/bg stop`.
|
|
1052
|
+
*/
|
|
1053
|
+
stopAllServices(opts = {}) {
|
|
1054
|
+
if (opts.force) {
|
|
1055
|
+
this.procs.reapSync();
|
|
1056
|
+
return Promise.resolve(this.procs.list());
|
|
1057
|
+
}
|
|
1058
|
+
return this.procs.stop('all');
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* The synchronous reaper (I-9).
|
|
1062
|
+
*
|
|
1063
|
+
* Exposed so `doExit` can call it without awaiting anything. It is also what
|
|
1064
|
+
* the signal hook registered in the constructor calls, and it is idempotent -
|
|
1065
|
+
* which it has to be, since a normal quit reaches it twice.
|
|
1066
|
+
*/
|
|
1067
|
+
reapServicesSync() {
|
|
1068
|
+
this.procs.reapSync();
|
|
1069
|
+
}
|
|
749
1070
|
steer(text) {
|
|
750
1071
|
// Mid-run interjection: promote the queue, keep what is already in force
|
|
751
1072
|
// (D-G3). Clearing here would make "ask a follow-up" the way around the
|
|
@@ -938,6 +1259,12 @@ export class AgentController {
|
|
|
938
1259
|
this.teamRuntime?.dispose();
|
|
939
1260
|
this.fast?.dispose();
|
|
940
1261
|
this.compaction?.dispose();
|
|
1262
|
+
this.contextMeter.dispose();
|
|
1263
|
+
// REAP BEFORE RELEASING THE HOOK, in that order: disposal is a teardown, and
|
|
1264
|
+
// a service that outlives it has nothing left that could ever stop it.
|
|
1265
|
+
this.procs.reapSync();
|
|
1266
|
+
this.releaseSignalHook();
|
|
1267
|
+
this.procs.dispose();
|
|
941
1268
|
}
|
|
942
1269
|
// -----------------------------------------------------------------------
|
|
943
1270
|
// Todo planning (todo-plan-execution §3.6a)
|
|
@@ -1063,6 +1390,13 @@ export class AgentController {
|
|
|
1063
1390
|
// so the snapshot the chip and `/compact status` read has to be refreshed
|
|
1064
1391
|
// for the same reason and at the same moments (the RV-3 lesson).
|
|
1065
1392
|
this.compaction?.onConfigChanged();
|
|
1393
|
+
// THE DENOMINATOR MOVED, NOT THE HISTORY (context-usage-gauge-accuracy §3.4).
|
|
1394
|
+
// A re-measure, deliberately NOT a reset: the measured base is still a true
|
|
1395
|
+
// statement about the messages, and throwing it away would swap a correct
|
|
1396
|
+
// numerator for a whole-history estimate on a common operation. The
|
|
1397
|
+
// numerator's calibration is one turn stale until the next `turn_end`
|
|
1398
|
+
// (RV-13), which is the trade this call names.
|
|
1399
|
+
this.contextMeter.onWindowChanged();
|
|
1066
1400
|
}
|
|
1067
1401
|
setThinkingLevel(level) {
|
|
1068
1402
|
this.config = { ...this.config, thinkingLevel: level };
|
|
@@ -1161,7 +1495,14 @@ export class AgentController {
|
|
|
1161
1495
|
// INVALIDATION SITE 3 (context-auto-compaction-hardening §3.2.3). The history
|
|
1162
1496
|
// was CLEARED, not appended to, so the prefix length the last measurement
|
|
1163
1497
|
// covered indexes an array that no longer exists.
|
|
1498
|
+
//
|
|
1499
|
+
// BOTH LINES, AND THE SECOND IS NOT REDUNDANT (context-usage-gauge-accuracy
|
|
1500
|
+
// I-9). The wiring is `null` for a `--no-compaction` session, so it is the
|
|
1501
|
+
// meter call that guarantees a re-measure - deleting it "to de-duplicate"
|
|
1502
|
+
// reintroduces P0-2 for exactly the sessions this feature was written for. A
|
|
1503
|
+
// deep reset is idempotent, so both firing costs nothing.
|
|
1164
1504
|
this.compaction?.onHistoryReplaced();
|
|
1505
|
+
this.contextMeter.onHistoryReplaced();
|
|
1165
1506
|
// A new conversation has no loaded skills, so the "already loaded earlier
|
|
1166
1507
|
// in this conversation" hint would otherwise start lying (P2-9).
|
|
1167
1508
|
this.skills.getRegistry().clearActive();
|
|
@@ -1181,7 +1522,15 @@ export class AgentController {
|
|
|
1181
1522
|
// INVALIDATION SITE 4 (§3.2.3) - `/resume`, and any future host-side rewrite.
|
|
1182
1523
|
// `estimateAppendedTokens` bounds-checks as well, so a MISSED site costs
|
|
1183
1524
|
// accuracy and never correctness; that belt does not make this call optional.
|
|
1525
|
+
//
|
|
1526
|
+
// THE SECOND LINE IS THE P0-2 FIX. Before it, `/resume` of a 180k-token
|
|
1527
|
+
// session left the gauge and `/compact status` reading 0 %: nothing
|
|
1528
|
+
// re-measured and nothing published, so the mount-time "empty history"
|
|
1529
|
+
// reading stood until the next completed turn. The DEEP reset also drops
|
|
1530
|
+
// `estimateOffset`, because `/resume` can change the model in the same
|
|
1531
|
+
// breath (I-8).
|
|
1184
1532
|
this.compaction?.onHistoryReplaced();
|
|
1533
|
+
this.contextMeter.onHistoryReplaced();
|
|
1185
1534
|
}
|
|
1186
1535
|
getMessages() {
|
|
1187
1536
|
return this.agent.state.messages;
|
|
@@ -1297,6 +1646,56 @@ export class AgentController {
|
|
|
1297
1646
|
return () => { };
|
|
1298
1647
|
return this.fast.subscribe(listener);
|
|
1299
1648
|
}
|
|
1649
|
+
/**
|
|
1650
|
+
* The FIFTH thin forwarder: the current fast-tier snapshot, or `null` when
|
|
1651
|
+
* this session never registered a tier at all
|
|
1652
|
+
* (web-use-tier-cooperation-and-control-closure §4.2.1).
|
|
1653
|
+
*
|
|
1654
|
+
* `null` and "a snapshot with `live: false`" are different answers and callers
|
|
1655
|
+
* depend on the difference: the first means "there is no tier here", the
|
|
1656
|
+
* second means "there is one and it is not usable right now".
|
|
1657
|
+
*
|
|
1658
|
+
* Deliberately NOT folded into the existing `review_end` event: that shape is
|
|
1659
|
+
* consumed by `App` too, and this round promised to be purely additive.
|
|
1660
|
+
*/
|
|
1661
|
+
fastSnapshot() {
|
|
1662
|
+
return this.fast?.snapshot() ?? null;
|
|
1663
|
+
}
|
|
1664
|
+
// -----------------------------------------------------------------------
|
|
1665
|
+
// Context occupancy (context-usage-gauge-accuracy §4.2) — THREE FORWARDERS
|
|
1666
|
+
//
|
|
1667
|
+
// ALL THREE ARE AVAILABLE UNCONDITIONALLY, which is the whole difference
|
|
1668
|
+
// between this block and the compaction one below it: there is no
|
|
1669
|
+
// `?? offSomething()` fallback here, because the meter always exists.
|
|
1670
|
+
// -----------------------------------------------------------------------
|
|
1671
|
+
/**
|
|
1672
|
+
* Subscribe to occupancy publications. Returns an unsubscribe function.
|
|
1673
|
+
*
|
|
1674
|
+
* THE SOLE UPSTREAM OF `ViewState.context` (I-1). The gauge used to have two
|
|
1675
|
+
* writers - `turnEnd` and a `contextTokensEstimated` dispatch fired from two
|
|
1676
|
+
* places in `App` - which is the entire cause of P0-1: both wrote in the same
|
|
1677
|
+
* synchronous fan-out and the second one won, restoring the pre-compaction
|
|
1678
|
+
* figure one statement after the bar had correctly fallen. One writer makes
|
|
1679
|
+
* that class of bug unrepresentable rather than merely fixed.
|
|
1680
|
+
*/
|
|
1681
|
+
subscribeContextUsage(listener) {
|
|
1682
|
+
return this.contextMeter.subscribe(listener);
|
|
1683
|
+
}
|
|
1684
|
+
/**
|
|
1685
|
+
* Occupancy right now, re-measuring if the history has moved.
|
|
1686
|
+
*
|
|
1687
|
+
* `/context` READS THIS AND NOT `getCompactionSnapshot().pressure` (RV-8). The
|
|
1688
|
+
* latter returns `offCompactionSnapshot()`'s hardcoded zero pressure when
|
|
1689
|
+
* compaction was never registered, so a report built on it would read 0 % for
|
|
1690
|
+
* precisely the sessions this feature exists to fix.
|
|
1691
|
+
*/
|
|
1692
|
+
getContextUsage() {
|
|
1693
|
+
return this.contextMeter.currentUsage();
|
|
1694
|
+
}
|
|
1695
|
+
/** The meter itself, for the wiring's single-instance assertion and tests. */
|
|
1696
|
+
getContextMeter() {
|
|
1697
|
+
return this.contextMeter;
|
|
1698
|
+
}
|
|
1300
1699
|
// -----------------------------------------------------------------------
|
|
1301
1700
|
// Context compaction (context-auto-compaction §3.2.1) — THIN FORWARDERS
|
|
1302
1701
|
// -----------------------------------------------------------------------
|