@aragon-agent/cli 0.6.3 → 0.6.4
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 +105 -0
- package/README.md +95 -2
- package/dist/agent/controller.d.ts +119 -0
- package/dist/agent/controller.js +275 -1
- package/dist/agent/reducer.d.ts +69 -0
- package/dist/agent/reducer.js +100 -0
- package/dist/agent/system-prompt.d.ts +14 -0
- package/dist/agent/system-prompt.js +2 -0
- package/dist/cli.js +84 -22
- package/dist/commands/builtins.js +92 -1
- package/dist/compaction/child.js +5 -0
- package/dist/compaction/command.js +14 -1
- package/dist/compaction/compactor.d.ts +34 -0
- package/dist/compaction/compactor.js +158 -37
- package/dist/compaction/types.d.ts +36 -0
- package/dist/compaction/wiring.d.ts +9 -0
- package/dist/compaction/wiring.js +22 -1
- package/dist/config/env.d.ts +1 -1
- package/dist/config/env.js +48 -1
- package/dist/config/load.d.ts +11 -0
- package/dist/config/load.js +39 -1
- package/dist/config/schema.d.ts +85 -0
- package/dist/config/schema.js +33 -0
- package/dist/config/store.js +21 -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/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 +256 -7
- package/dist/ui/Composer.d.ts +54 -1
- package/dist/ui/Composer.js +50 -6
- package/dist/ui/PromptInput.d.ts +49 -3
- package/dist/ui/PromptInput.js +184 -52
- package/dist/ui/StatusBar.d.ts +19 -0
- package/dist/ui/StatusBar.js +5 -2
- package/dist/ui/Transcript.js +29 -1
- 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/layout/budget.d.ts +34 -6
- package/dist/ui/layout/budget.js +32 -7
- 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 +49 -0
- 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,6 +20,11 @@ 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';
|
|
@@ -33,6 +38,15 @@ import { createNodeSkillHost } from '../skills/node-host.js';
|
|
|
33
38
|
import { SkillService } from '../skills/service.js';
|
|
34
39
|
import { createSkillTools } from '../skills/tools.js';
|
|
35
40
|
import { buildSystemPrompt } from './system-prompt.js';
|
|
41
|
+
/**
|
|
42
|
+
* Extra budget, on top of `PROC_LIMITS.killGraceMs`, that `prompt()` allows a
|
|
43
|
+
* force-stopped engine to unwind in before it starts a new run anyway (I-8).
|
|
44
|
+
*
|
|
45
|
+
* `bash`'s own kill grace is what a wedged foreground command costs; this covers
|
|
46
|
+
* the loop's own unwinding on top of it. Waiting FOREVER is not an option - a
|
|
47
|
+
* tool that never settles is exactly why rung two exists.
|
|
48
|
+
*/
|
|
49
|
+
const ENGINE_UNWIND_GRACE_MS = 3000;
|
|
36
50
|
/** Approval gate used whenever there is provably no human to ask (headless). */
|
|
37
51
|
export const DENY_ALL_APPROVAL = {
|
|
38
52
|
canPrompt: () => false,
|
|
@@ -172,6 +186,48 @@ export class AgentController {
|
|
|
172
186
|
// to subscribe to the Agent, and this class cannot do both in one step.
|
|
173
187
|
// -----------------------------------------------------------------------
|
|
174
188
|
compaction;
|
|
189
|
+
// -----------------------------------------------------------------------
|
|
190
|
+
// Background services (background-service-supervision §3.3 / §3.6)
|
|
191
|
+
//
|
|
192
|
+
// CONSTRUCTED UNCONDITIONALLY, and that is the one thing about this field that
|
|
193
|
+
// is easy to get wrong (P1-6 / D-9). `cfg.bash.background` gates the SERVICE
|
|
194
|
+
// half - the `background` tool param, `bash_output`/`bash_kill`, the prompt
|
|
195
|
+
// block, the status chip - and NEVER the foreground half, because G2 ("bash
|
|
196
|
+
// always settles") and G3 ("Esc, Esc always works, and the session stays
|
|
197
|
+
// usable") are promised unconditionally. Gating the whole supervisor on the
|
|
198
|
+
// flag would make rung two of the Esc ladder a silent no-op for every user who
|
|
199
|
+
// turned background services off.
|
|
200
|
+
//
|
|
201
|
+
// With the flag off it holds foreground pids, nothing else, and allocates no
|
|
202
|
+
// timers.
|
|
203
|
+
// -----------------------------------------------------------------------
|
|
204
|
+
procs;
|
|
205
|
+
/**
|
|
206
|
+
* The host's notice channel, kept so `prompt()`'s totality has somewhere to
|
|
207
|
+
* report to (I-8). Every other consumer reads `deps.notify` through a closure
|
|
208
|
+
* built in the constructor; this one is called from a method, which is why it
|
|
209
|
+
* needs a field.
|
|
210
|
+
*/
|
|
211
|
+
notifyHostFn;
|
|
212
|
+
/** Whether the SERVICE half is on: tools, prompt block, `background` param. */
|
|
213
|
+
bashBackgroundRegistered;
|
|
214
|
+
/** Release the exit reaper. Called by `dispose()`. */
|
|
215
|
+
releaseSignalHook;
|
|
216
|
+
/**
|
|
217
|
+
* Bumped by `forceStop()`; captured by `App` when it subscribes to a run.
|
|
218
|
+
*
|
|
219
|
+
* THE VIEW AND THE ENGINE DELIBERATELY DISAGREE AFTER A FORCE-STOP (D-10 /
|
|
220
|
+
* I-7). Rung two dispatches `runEnd` locally so the composer is usable
|
|
221
|
+
* immediately, but the engine keeps unwinding and keeps EMITTING -
|
|
222
|
+
* `turn_start`, `tool_call_start`, `turn_end`, `agent_end` - and
|
|
223
|
+
* `runStart`/`turnStart` would set `status: 'running'` again, bouncing the
|
|
224
|
+
* view straight back out of the `idle` the user was just given and appending
|
|
225
|
+
* entries to a transcript they believe is finished. `App` drops every event
|
|
226
|
+
* whose generation is stale, which is the same shape as `abortRequested`
|
|
227
|
+
* above and exists for the same reason: the loop legitimately emits after an
|
|
228
|
+
* abort lands.
|
|
229
|
+
*/
|
|
230
|
+
runGen = 0;
|
|
175
231
|
/**
|
|
176
232
|
* Set SYNCHRONOUSLY by `abort()` before `agent.abort()` (§3.5.4a guard 1).
|
|
177
233
|
* Cleared at the next `agent_start`.
|
|
@@ -290,6 +346,19 @@ export class AgentController {
|
|
|
290
346
|
// reason the field's own comment gives: the binding below is spread
|
|
291
347
|
// conditionally on it (§3.1.3 / P1-6).
|
|
292
348
|
this.toolOutputs = config.liveToolOutput ? createToolOutputStore() : null;
|
|
349
|
+
// ALSO ABOVE `createBuiltinTools`, and UNCONDITIONAL - see the field's own
|
|
350
|
+
// comment. `readyTimeoutMs` is read through a closure rather than captured,
|
|
351
|
+
// so a live config change reaches the next service rather than the next
|
|
352
|
+
// launch of the CLI.
|
|
353
|
+
this.procs = new ProcSupervisor({ readyTimeoutMs: () => this.config.bash.readyTimeoutMs });
|
|
354
|
+
this.notifyHostFn = deps.notify;
|
|
355
|
+
this.bashBackgroundRegistered = config.bash.background;
|
|
356
|
+
// THE EXIT REAPER (G5 / P1-2 / R-10). An APPEND-ONLY hook rather than a
|
|
357
|
+
// second `setSignalTerminator`: that is a single slot `cli.tsx` already owns
|
|
358
|
+
// for the alternate-screen restore, and replacing it is the documented bug
|
|
359
|
+
// that leaves the user staring at a blank alternate screen. `reapSync` is
|
|
360
|
+
// the only entry point it calls, and it is synchronous end to end (I-9).
|
|
361
|
+
this.releaseSignalHook = addSignalHook(() => this.procs.reapSync());
|
|
293
362
|
this.tools = createBuiltinTools({
|
|
294
363
|
getCwd: () => this.cwd,
|
|
295
364
|
// OWNED HERE, NOT IN `cli.tsx` (D-18): `createBuiltinTools` is called from
|
|
@@ -306,6 +375,28 @@ export class AgentController {
|
|
|
306
375
|
...(this.toolOutputs
|
|
307
376
|
? { recordOutput: (id, chunk) => this.emitToolOutput('lead', id, chunk) }
|
|
308
377
|
: {}),
|
|
378
|
+
// THE SERVICE HALF, AND ONLY THE SERVICE HALF (P1-6 / I-2). With
|
|
379
|
+
// `bash.background: false` the options bag has no `procs` key at all, so
|
|
380
|
+
// `createBuiltinTools` adds no property to `deps`, `bash` gets its default
|
|
381
|
+
// `{}` second argument, and the tool is the pre-feature one: no
|
|
382
|
+
// `background` in its schema, the pre-feature description string, no
|
|
383
|
+
// supervisor call. The FOREGROUND half of `this.procs` is still live, and
|
|
384
|
+
// `forceStop()` still reaches it.
|
|
385
|
+
...(this.bashBackgroundRegistered
|
|
386
|
+
? {
|
|
387
|
+
procs: this.procs,
|
|
388
|
+
// Read LIVE for the reason every other closure in this bag is:
|
|
389
|
+
// `aragon config set bash.autoBackground false` must take effect in
|
|
390
|
+
// the running session.
|
|
391
|
+
autoBackground: () => this.config.bash.autoBackground,
|
|
392
|
+
startupSettleMs: () => this.config.bash.startupSettleMs,
|
|
393
|
+
// D-15 / P1-8. `planInteractive` is the SAME signal `planTools`
|
|
394
|
+
// already uses to decide that no overlay can be rendered, which is
|
|
395
|
+
// exactly the question being asked here: can a card be drawn, and is
|
|
396
|
+
// there a user who could keep a service alive?
|
|
397
|
+
interactive: this.planInteractive,
|
|
398
|
+
}
|
|
399
|
+
: {}),
|
|
309
400
|
confirmTools: config.confirmTools,
|
|
310
401
|
// Routed through the same watchdog pause as the plan tools. This is a
|
|
311
402
|
// pre-existing bug being fixed in passing: a `--confirm` user who took
|
|
@@ -345,6 +436,12 @@ export class AgentController {
|
|
|
345
436
|
}),
|
|
346
437
|
]
|
|
347
438
|
: [],
|
|
439
|
+
// THROUGH THE FACTORY for the reason `teamTools` records below (P0-2), and
|
|
440
|
+
// empty when background services were off at construction - which leaves
|
|
441
|
+
// the array byte-identical to the pre-feature build (I-2).
|
|
442
|
+
procTools: this.bashBackgroundRegistered
|
|
443
|
+
? [makeBashOutput({ supervisor: this.procs }), makeBashKill({ supervisor: this.procs })]
|
|
444
|
+
: [],
|
|
348
445
|
// THROUGH THE FACTORY, NEVER APPENDED AFTERWARDS (D-15 / P0-1).
|
|
349
446
|
// `tools.test.ts::C7` asserts `HOST_TOOL_NAMES` equals what
|
|
350
447
|
// `createBuiltinTools` produces, so appending `task` here would turn that
|
|
@@ -612,6 +709,12 @@ export class AgentController {
|
|
|
612
709
|
review: this.config.fast.review,
|
|
613
710
|
})
|
|
614
711
|
: '',
|
|
712
|
+
// ONE FLAG, unlike team/todo's two: there is no live on/off switch for
|
|
713
|
+
// background services - `bash.background` is read once at construction,
|
|
714
|
+
// and the tools it registers cannot be unregistered (the `team.enabled` /
|
|
715
|
+
// `todo.enabled` rule). So the block is present exactly when the tools are,
|
|
716
|
+
// and can never advertise a capability the model does not have.
|
|
717
|
+
backgroundBlock: this.bashBackgroundRegistered ? buildBackgroundServicesBlock() : '',
|
|
615
718
|
// Read from the FIELD on every rebuild, for the reason the field's own
|
|
616
719
|
// comment gives. Empty everywhere but `aragon exec`, and `buildSystemPrompt`
|
|
617
720
|
// splices it conditionally, so every existing prompt is byte-identical.
|
|
@@ -700,7 +803,64 @@ export class AgentController {
|
|
|
700
803
|
* underlying promise so headless mode can await completion. `agent.prompt()`
|
|
701
804
|
* never rejects, so callers must not treat resolution as success.
|
|
702
805
|
*/
|
|
703
|
-
prompt(text) {
|
|
806
|
+
async prompt(text) {
|
|
807
|
+
// I-8 - THIS METHOD NEVER REJECTS, AND THAT HAS TO BE A PROPERTY OF THE
|
|
808
|
+
// METHOD RATHER THAN A COMMENT ABOVE IT (P0-1).
|
|
809
|
+
//
|
|
810
|
+
// `App` calls this as `void controller.prompt(...)`, and
|
|
811
|
+
// `logging/install.ts` routes an `unhandledRejection` into `handleFatal`,
|
|
812
|
+
// which EXITS THE PROCESS. `Agent.prompt()` throws SYNCHRONOUSLY while
|
|
813
|
+
// `Agent.running` is true - so before the force-stop ladder existed this was
|
|
814
|
+
// merely nearly-true (the view was only ever idle when the engine was), and
|
|
815
|
+
// rung two makes the view idle while the engine is still unwinding. Without
|
|
816
|
+
// the wait and the guard below, "Esc, Esc, then type" would kill the CLI on
|
|
817
|
+
// the very screen this feature exists to rescue.
|
|
818
|
+
//
|
|
819
|
+
// TWO PARTS, BOTH REQUIRED. First WAIT the engine out, bounded, so the
|
|
820
|
+
// common case (the engine unwinds in a few hundred ms) starts a real run
|
|
821
|
+
// rather than reporting a failure. Then swallow anything that still escapes
|
|
822
|
+
// and surface it through the existing notify channel, because a message that
|
|
823
|
+
// cannot be started must produce a visible sentence - never a crash, and
|
|
824
|
+
// never silence.
|
|
825
|
+
if (this.agent.state.isRunning) {
|
|
826
|
+
this.abort();
|
|
827
|
+
await this.waitForEngineIdle();
|
|
828
|
+
}
|
|
829
|
+
try {
|
|
830
|
+
await this.startPrompt(text);
|
|
831
|
+
}
|
|
832
|
+
catch (err) {
|
|
833
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
834
|
+
getLogger().warn('agent', 'prompt_failed', { reason });
|
|
835
|
+
this.notifyHostFn?.('error', `Could not start the run: ${reason}`);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* Wait for the engine to unwind, bounded.
|
|
840
|
+
*
|
|
841
|
+
* BOUNDED BECAUSE THE UNBOUNDED CASE IS THE ONE THIS FEATURE IS ABOUT: a tool
|
|
842
|
+
* that never settles is exactly why rung two exists, and `Agent.waitForIdle()`
|
|
843
|
+
* alone would hang here for as long as it hangs there. The budget is the two
|
|
844
|
+
* graces a wedged `bash` can legitimately spend, plus the executor's own.
|
|
845
|
+
*/
|
|
846
|
+
waitForEngineIdle() {
|
|
847
|
+
const budget = PROC_LIMITS.killGraceMs + ENGINE_UNWIND_GRACE_MS;
|
|
848
|
+
return new Promise((resolve) => {
|
|
849
|
+
let done = false;
|
|
850
|
+
const finish = () => {
|
|
851
|
+
if (done)
|
|
852
|
+
return;
|
|
853
|
+
done = true;
|
|
854
|
+
clearTimeout(timer);
|
|
855
|
+
resolve();
|
|
856
|
+
};
|
|
857
|
+
const timer = setTimeout(finish, budget);
|
|
858
|
+
timer.unref?.();
|
|
859
|
+
void this.agent.waitForIdle().then(finish, finish);
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
/** The pre-round body of `prompt`, unchanged apart from being private. */
|
|
863
|
+
startPrompt(text) {
|
|
704
864
|
// A new user message is a new intent, so the previous turn's tool ceiling
|
|
705
865
|
// expires here and anything a slash command queued is promoted (D-G1 / D-G2).
|
|
706
866
|
// This and `steer()` below are the ONLY two places a user message enters the
|
|
@@ -746,6 +906,115 @@ export class AgentController {
|
|
|
746
906
|
// `App.tsx` records for `cancelPendingHuman()` on `agent_end`.
|
|
747
907
|
this.teamRuntime?.abortAll();
|
|
748
908
|
}
|
|
909
|
+
/**
|
|
910
|
+
* Rung two of the Esc ladder: stop the AGENT, whatever it is blocked on
|
|
911
|
+
* (§3.6 / G3).
|
|
912
|
+
*
|
|
913
|
+
* FOUR STEPS, AND THE FOURTH IS AN ABSENCE.
|
|
914
|
+
*
|
|
915
|
+
* 1. `abort()` - everything it does today, unchanged.
|
|
916
|
+
* 2. `killForeground('force')` - hard-kill every tracked foreground `bash`
|
|
917
|
+
* child. THIS is what actually unblocks a wedged `await tool.execute`;
|
|
918
|
+
* an abort signal a tool ignores is advisory, and rung two exists for
|
|
919
|
+
* precisely the tools that ignore it.
|
|
920
|
+
* 3. `runGen += 1` - so `App` can drop the events the engine will keep
|
|
921
|
+
* emitting while it unwinds (I-7). Without it the view bounces straight
|
|
922
|
+
* back out of the `idle` the user was just given.
|
|
923
|
+
* 4. IT DOES NOT TOUCH BACKGROUND SERVICES. `Esc` interrupts the AGENT;
|
|
924
|
+
* `Ctrl+C` stops the SERVICES. That asymmetry is the user's own two
|
|
925
|
+
* sentences and it is exactly the kind of thing a later reader will
|
|
926
|
+
* "fix" by making this kill everything - which would mean a user who
|
|
927
|
+
* pressed Esc twice to stop a runaway turn also lost the dev server they
|
|
928
|
+
* had been working against for an hour. It is invariant I-4.
|
|
929
|
+
*
|
|
930
|
+
* UNCONDITIONAL ON `bash.background` (P1-6 / D-9). The supervisor is always
|
|
931
|
+
* constructed; with services off its foreground registry is all there is, and
|
|
932
|
+
* that is the half this method uses.
|
|
933
|
+
*/
|
|
934
|
+
forceStop() {
|
|
935
|
+
this.abort();
|
|
936
|
+
const killed = this.procs.killForeground('force');
|
|
937
|
+
this.runGen += 1;
|
|
938
|
+
getLogger().info('agent', 'force_stop', { generation: this.runGen, killed });
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* The current run generation (I-7).
|
|
942
|
+
*
|
|
943
|
+
* `App` captures it when it subscribes to a run and drops every engine event
|
|
944
|
+
* whose generation is stale. Same shape as the `abortRequested` guard the fast
|
|
945
|
+
* reviewer already uses, and for the same reason: the loop legitimately emits
|
|
946
|
+
* after an abort lands.
|
|
947
|
+
*/
|
|
948
|
+
get runGeneration() {
|
|
949
|
+
return this.runGen;
|
|
950
|
+
}
|
|
951
|
+
// -----------------------------------------------------------------------
|
|
952
|
+
// Background services (background-service-supervision §3.5 / §3.6)
|
|
953
|
+
// -----------------------------------------------------------------------
|
|
954
|
+
/** Whether `bash_output` / `bash_kill` are in `this.tools`. Fixed at construction. */
|
|
955
|
+
isBackgroundRegistered() {
|
|
956
|
+
return this.bashBackgroundRegistered;
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Subscribe to the CLI-local `ProcEvent` stream.
|
|
960
|
+
*
|
|
961
|
+
* Returns a live unsubscribe even when services are off, because the
|
|
962
|
+
* supervisor exists either way - the contract `subscribeTeam` /
|
|
963
|
+
* `subscribeTodos` / `subscribeFast` / `subscribeToolOutput` already set. With
|
|
964
|
+
* services off nothing is ever emitted, so an unconditional subscription in
|
|
965
|
+
* `App` costs one `Set` entry.
|
|
966
|
+
*/
|
|
967
|
+
subscribeProc(listener) {
|
|
968
|
+
return this.procs.subscribe(listener);
|
|
969
|
+
}
|
|
970
|
+
getServiceSnapshot(id) {
|
|
971
|
+
return this.procs.get(id);
|
|
972
|
+
}
|
|
973
|
+
listServices() {
|
|
974
|
+
return this.procs.list();
|
|
975
|
+
}
|
|
976
|
+
/** `starting | ready | running`. Drives the status chip and the Ctrl+C rung. */
|
|
977
|
+
liveServiceCount() {
|
|
978
|
+
return this.procs.liveCount();
|
|
979
|
+
}
|
|
980
|
+
readServiceLog(id, since) {
|
|
981
|
+
return this.procs.read(id, since);
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Stop one service, or every service when `id` is `'all'`.
|
|
985
|
+
*
|
|
986
|
+
* The GRACEFUL ladder (`SIGTERM` -> `SIGKILL` after `stopGraceMs`), because
|
|
987
|
+
* this path has a live event loop to run it on. `reapServicesSync` is the
|
|
988
|
+
* other one, and it is the only one an exit path may use.
|
|
989
|
+
*/
|
|
990
|
+
stopService(id) {
|
|
991
|
+
return this.procs.stop(id);
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* Rung one of `Ctrl+C`: stop every live service (§3.6 / G4).
|
|
995
|
+
*
|
|
996
|
+
* `force: true` means `reapSync()` SEMANTICS - immediate `SIGKILL`, no
|
|
997
|
+
* `stopGraceMs` - and is what `doExit` uses, because `doExit` awaits nothing
|
|
998
|
+
* and must not start (§3.6 step 3). The graceful ladder belongs to `Ctrl+C`,
|
|
999
|
+
* `bash_kill` and `/bg stop`.
|
|
1000
|
+
*/
|
|
1001
|
+
stopAllServices(opts = {}) {
|
|
1002
|
+
if (opts.force) {
|
|
1003
|
+
this.procs.reapSync();
|
|
1004
|
+
return Promise.resolve(this.procs.list());
|
|
1005
|
+
}
|
|
1006
|
+
return this.procs.stop('all');
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* The synchronous reaper (I-9).
|
|
1010
|
+
*
|
|
1011
|
+
* Exposed so `doExit` can call it without awaiting anything. It is also what
|
|
1012
|
+
* the signal hook registered in the constructor calls, and it is idempotent -
|
|
1013
|
+
* which it has to be, since a normal quit reaches it twice.
|
|
1014
|
+
*/
|
|
1015
|
+
reapServicesSync() {
|
|
1016
|
+
this.procs.reapSync();
|
|
1017
|
+
}
|
|
749
1018
|
steer(text) {
|
|
750
1019
|
// Mid-run interjection: promote the queue, keep what is already in force
|
|
751
1020
|
// (D-G3). Clearing here would make "ask a follow-up" the way around the
|
|
@@ -938,6 +1207,11 @@ export class AgentController {
|
|
|
938
1207
|
this.teamRuntime?.dispose();
|
|
939
1208
|
this.fast?.dispose();
|
|
940
1209
|
this.compaction?.dispose();
|
|
1210
|
+
// REAP BEFORE RELEASING THE HOOK, in that order: disposal is a teardown, and
|
|
1211
|
+
// a service that outlives it has nothing left that could ever stop it.
|
|
1212
|
+
this.procs.reapSync();
|
|
1213
|
+
this.releaseSignalHook();
|
|
1214
|
+
this.procs.dispose();
|
|
941
1215
|
}
|
|
942
1216
|
// -----------------------------------------------------------------------
|
|
943
1217
|
// Todo planning (todo-plan-execution §3.6a)
|
package/dist/agent/reducer.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type { DispatchOutcome, SubagentRun, SubagentSpec, TeamSnapshot } from '.
|
|
|
16
16
|
import type { FastReview, FastSnapshot } from '../fast/types.js';
|
|
17
17
|
import type { CompactionMode, CompactionRecord, CompactionSnapshot, CompactionUiTrigger } from '../compaction/types.js';
|
|
18
18
|
import type { TodoItem, TodoSnapshot } from '../todo/types.js';
|
|
19
|
+
import type { ServiceSnapshot, ServiceStatus } from '../proc/types.js';
|
|
19
20
|
import type { RetryPhase } from './retry-view.js';
|
|
20
21
|
import type { FilePatch } from '../tools/patch.js';
|
|
21
22
|
export type NoticeLevel = 'info' | 'warn' | 'error';
|
|
@@ -274,6 +275,56 @@ export type Entry = {
|
|
|
274
275
|
charsRemoved: number;
|
|
275
276
|
};
|
|
276
277
|
live: boolean;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* One supervised background service (background-service-supervision §3.10).
|
|
281
|
+
*
|
|
282
|
+
* JSON-SERIALIZABLE by construction, so `/save` needs no format change.
|
|
283
|
+
*
|
|
284
|
+
* IT IS THE ONE LIVE CARD THAT DOES **NOT** BLOCK THE SETTLED BOUNDARY ONCE IT
|
|
285
|
+
* IS UP (D-11 / P0-4). Every other live clause in
|
|
286
|
+
* `Transcript.tsx::computeSettledCount` is bounded by an OPERATION — a
|
|
287
|
+
* dispatch, a turn, one LLM call, one countdown — and each carries a comment
|
|
288
|
+
* saying that an entry which never settles is re-rendered on every frame for
|
|
289
|
+
* the rest of the session. A service is bounded by the USER'S INTENT: a dev
|
|
290
|
+
* server left up for an hour is the normal case. So only `starting` blocks
|
|
291
|
+
* (bounded by `readyTimeoutMs`), a `ready`/`running` card commits its body to
|
|
292
|
+
* `<Static>`, and a later terminal transition appends a NEW one-row entry
|
|
293
|
+
* rather than trying to rewrite a card `<Static>` has already printed.
|
|
294
|
+
*
|
|
295
|
+
* `terminal: true` marks that second entry. `session/persist.ts` rewrites any
|
|
296
|
+
* restored non-terminal card to `stopped`, next to `team.active`, `todo.live`,
|
|
297
|
+
* the retry phase, `fast.live` and `compaction.live`, and for the identical
|
|
298
|
+
* reason.
|
|
299
|
+
*/
|
|
300
|
+
| {
|
|
301
|
+
id: string;
|
|
302
|
+
kind: 'service';
|
|
303
|
+
/** `s1`, `s2`, ... — the id the model quotes back. */
|
|
304
|
+
serviceId: string;
|
|
305
|
+
command: string;
|
|
306
|
+
status: ServiceStatus;
|
|
307
|
+
url?: string;
|
|
308
|
+
port?: number;
|
|
309
|
+
exitCode: number | null;
|
|
310
|
+
startedAt: number;
|
|
311
|
+
readyAt?: number;
|
|
312
|
+
endedAt?: number;
|
|
313
|
+
rows: readonly string[];
|
|
314
|
+
/**
|
|
315
|
+
* MONOTONIC row cursor — the revision term, NEVER `rows.length` (P0-3).
|
|
316
|
+
*
|
|
317
|
+
* The tail is a FIXED-SIZE RING: it evicts its oldest row while appending
|
|
318
|
+
* a new one, so the joined length is unchanged while the content is not.
|
|
319
|
+
* That is verbatim the non-append mutation the `tool` branch's `liveSeq`
|
|
320
|
+
* comment was written for, and a length term would go on matching while
|
|
321
|
+
* the card changed underneath it.
|
|
322
|
+
*/
|
|
323
|
+
rowsSeen: number;
|
|
324
|
+
/** The one-row entry a terminal transition appends (D-11). */
|
|
325
|
+
terminal?: boolean;
|
|
326
|
+
/** The stop reported the pid still alive afterwards (R-2 / P2-6). */
|
|
327
|
+
killIncomplete?: boolean;
|
|
277
328
|
};
|
|
278
329
|
export interface UsageTotal {
|
|
279
330
|
inputTokens: number;
|
|
@@ -606,6 +657,24 @@ export type ViewAction = {
|
|
|
606
657
|
| {
|
|
607
658
|
type: 'contextTokensEstimated';
|
|
608
659
|
tokens: number;
|
|
660
|
+
} | {
|
|
661
|
+
type: 'serviceStart';
|
|
662
|
+
service: ServiceSnapshot;
|
|
663
|
+
} | {
|
|
664
|
+
type: 'serviceUpdate';
|
|
665
|
+
service: ServiceSnapshot;
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* A service reached a terminal state.
|
|
669
|
+
*
|
|
670
|
+
* IT APPENDS A NEW ONE-ROW ENTRY rather than rewriting the card (D-11). The
|
|
671
|
+
* card may already have been printed into `<Static>`, and `<Static>` cannot
|
|
672
|
+
* un-print — so any design that rewrites a printed card is wrong whatever it
|
|
673
|
+
* claims. A second event at a second time is also simply more honest.
|
|
674
|
+
*/
|
|
675
|
+
| {
|
|
676
|
+
type: 'serviceEnd';
|
|
677
|
+
service: ServiceSnapshot;
|
|
609
678
|
};
|
|
610
679
|
export declare const STORED_PREVIEW_CHARS = 8000;
|
|
611
680
|
export declare const STORED_PREVIEW_LINES = 200;
|
package/dist/agent/reducer.js
CHANGED
|
@@ -1005,6 +1005,12 @@ export function viewReducer(state, action) {
|
|
|
1005
1005
|
case 'fastTier':
|
|
1006
1006
|
return { ...state, fast: action.snapshot };
|
|
1007
1007
|
// --- Context compaction (context-auto-compaction §5.5) ----------------
|
|
1008
|
+
// NOT A TOTAL FUNCTION OVER COMPACTION ATTEMPTS (quiet-noop §3.1). A
|
|
1009
|
+
// `pressure` compaction that declines before calling a model or changing the
|
|
1010
|
+
// history dispatches NOTHING - no `compactionStart`, no `compactionEnd`, no
|
|
1011
|
+
// entry - exactly as a checkpoint below the threshold dispatches nothing.
|
|
1012
|
+
// Counting cards here counts compactions that RAN, never checkpoints that
|
|
1013
|
+
// fired; `/compact status` and the diagnostic log are where the attempts are.
|
|
1008
1014
|
case 'compactionStart': {
|
|
1009
1015
|
const { id, seq } = nextId(state);
|
|
1010
1016
|
let entries = state.entries;
|
|
@@ -1317,10 +1323,104 @@ export function viewReducer(state, action) {
|
|
|
1317
1323
|
fastEntryId: undefined,
|
|
1318
1324
|
};
|
|
1319
1325
|
}
|
|
1326
|
+
// --- Background services (§3.10 / D-11) -----------------------------
|
|
1327
|
+
case 'serviceStart': {
|
|
1328
|
+
const { id, seq } = nextId(state);
|
|
1329
|
+
const entry = { id, ...serviceFields(action.service) };
|
|
1330
|
+
const appended = appendEntry(state, state.entries, entry);
|
|
1331
|
+
return {
|
|
1332
|
+
...state,
|
|
1333
|
+
seq,
|
|
1334
|
+
entries: appended.entries,
|
|
1335
|
+
droppedEntries: appended.droppedEntries,
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Rewrite the LIVE card in place.
|
|
1340
|
+
*
|
|
1341
|
+
* SAFE ONLY BECAUSE OF D-11'S OTHER HALF: a `ready` / `running` card has
|
|
1342
|
+
* reached `<Static>` and cannot be re-printed, so `serviceEnd` appends a new
|
|
1343
|
+
* entry instead of routing through here. What this handles is the window in
|
|
1344
|
+
* which the card is still in the live region — `starting`, and the one
|
|
1345
|
+
* transition out of it — plus the output tail while it is there.
|
|
1346
|
+
*
|
|
1347
|
+
* The lookup is by `serviceId` and SKIPS terminal entries, so a stray update
|
|
1348
|
+
* cannot rewrite the one-row record `serviceEnd` appended.
|
|
1349
|
+
*/
|
|
1350
|
+
case 'serviceUpdate': {
|
|
1351
|
+
const target = findServiceEntryId(state, action.service.id);
|
|
1352
|
+
if (!target)
|
|
1353
|
+
return state;
|
|
1354
|
+
return {
|
|
1355
|
+
...state,
|
|
1356
|
+
entries: mapEntry(state.entries, target, (e) => e.kind === 'service' ? { ...e, ...serviceFields(action.service) } : e),
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
/**
|
|
1360
|
+
* A service reached a terminal state.
|
|
1361
|
+
*
|
|
1362
|
+
* TWO WRITES, AND BOTH ARE REQUIRED. The live card is settled in place so
|
|
1363
|
+
* `computeSettledCount` can move past it and `estimateEntryRows` stops
|
|
1364
|
+
* charging for a tail nothing will add to; the NEW one-row entry is what the
|
|
1365
|
+
* user actually reads, because the card may already be printed into
|
|
1366
|
+
* `<Static>` and `<Static>` cannot un-print (D-11 / R-13).
|
|
1367
|
+
*/
|
|
1368
|
+
case 'serviceEnd': {
|
|
1369
|
+
const target = findServiceEntryId(state, action.service.id);
|
|
1370
|
+
const settled = target
|
|
1371
|
+
? mapEntry(state.entries, target, (e) => e.kind === 'service' ? { ...e, ...serviceFields(action.service) } : e)
|
|
1372
|
+
: state.entries;
|
|
1373
|
+
const { id, seq } = nextId(state);
|
|
1374
|
+
const appended = appendEntry(state, settled, {
|
|
1375
|
+
id,
|
|
1376
|
+
...serviceFields(action.service),
|
|
1377
|
+
terminal: true,
|
|
1378
|
+
});
|
|
1379
|
+
return {
|
|
1380
|
+
...state,
|
|
1381
|
+
seq,
|
|
1382
|
+
entries: appended.entries,
|
|
1383
|
+
droppedEntries: appended.droppedEntries,
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1320
1386
|
default:
|
|
1321
1387
|
return state;
|
|
1322
1388
|
}
|
|
1323
1389
|
}
|
|
1390
|
+
/**
|
|
1391
|
+
* Project a supervisor snapshot onto the entry's own fields.
|
|
1392
|
+
*
|
|
1393
|
+
* ONE PROJECTION FOR ALL THREE ACTIONS, so a field that reaches the card through
|
|
1394
|
+
* `serviceStart` can never fail to reach it through `serviceUpdate` — the kind of
|
|
1395
|
+
* asymmetry that produces a card frozen at its first values with nothing
|
|
1396
|
+
* reporting it.
|
|
1397
|
+
*/
|
|
1398
|
+
function serviceFields(service) {
|
|
1399
|
+
return {
|
|
1400
|
+
kind: 'service',
|
|
1401
|
+
serviceId: service.id,
|
|
1402
|
+
command: service.command,
|
|
1403
|
+
status: service.status,
|
|
1404
|
+
...(service.url !== undefined ? { url: service.url } : {}),
|
|
1405
|
+
...(service.port !== undefined ? { port: service.port } : {}),
|
|
1406
|
+
exitCode: service.exitCode,
|
|
1407
|
+
startedAt: service.startedAt,
|
|
1408
|
+
...(service.readyAt !== undefined ? { readyAt: service.readyAt } : {}),
|
|
1409
|
+
...(service.endedAt !== undefined ? { endedAt: service.endedAt } : {}),
|
|
1410
|
+
rows: service.rows,
|
|
1411
|
+
rowsSeen: service.rowsSeen,
|
|
1412
|
+
...(service.killIncomplete ? { killIncomplete: true } : {}),
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
/** The LIVE card for a service id, newest first; terminal records are skipped. */
|
|
1416
|
+
function findServiceEntryId(state, serviceId) {
|
|
1417
|
+
for (let i = state.entries.length - 1; i >= 0; i -= 1) {
|
|
1418
|
+
const e = state.entries[i];
|
|
1419
|
+
if (e && e.kind === 'service' && e.serviceId === serviceId && !e.terminal)
|
|
1420
|
+
return e.id;
|
|
1421
|
+
}
|
|
1422
|
+
return undefined;
|
|
1423
|
+
}
|
|
1324
1424
|
function safeStringify(value) {
|
|
1325
1425
|
try {
|
|
1326
1426
|
return JSON.stringify(value);
|
|
@@ -88,6 +88,20 @@ export interface SystemPromptParams {
|
|
|
88
88
|
* for a fixed `tools` array. Every existing caller passes nothing.
|
|
89
89
|
*/
|
|
90
90
|
appendSystemPrompt?: string;
|
|
91
|
+
/**
|
|
92
|
+
* The rendered `<background_services>` block, or `''`
|
|
93
|
+
* (background-service-supervision §5.2).
|
|
94
|
+
*
|
|
95
|
+
* Spliced conditionally exactly as `skillsBlock`, the plan block, `teamBlock`,
|
|
96
|
+
* `todoBlock` and `fastBlock` are, which is what keeps I-2 true:
|
|
97
|
+
* `bash.background: false` produces the pre-feature prompt BYTE FOR BYTE for a
|
|
98
|
+
* fixed tool array.
|
|
99
|
+
*
|
|
100
|
+
* A CHILD'S PROMPT NEVER CARRIES IT: subagents build tools from the same
|
|
101
|
+
* factory but are given no supervisor, so telling one about `bash_output` /
|
|
102
|
+
* `bash_kill` would describe tools it does not have.
|
|
103
|
+
*/
|
|
104
|
+
backgroundBlock?: string;
|
|
91
105
|
}
|
|
92
106
|
/** Describe the active shell so the model does not emit incompatible syntax. */
|
|
93
107
|
export declare function describeShell(): {
|
|
@@ -41,6 +41,7 @@ export function buildSystemPrompt(params) {
|
|
|
41
41
|
const subagentBlock = params.subagentBlock ?? '';
|
|
42
42
|
const todoBlock = params.todoBlock ?? '';
|
|
43
43
|
const fastBlock = params.fastBlock ?? '';
|
|
44
|
+
const backgroundBlock = params.backgroundBlock ?? '';
|
|
44
45
|
const appended = (params.appendSystemPrompt ?? '').trim();
|
|
45
46
|
return [
|
|
46
47
|
'You are AragonAgent, an autonomous coding assistant operating inside a terminal (TUI).',
|
|
@@ -59,6 +60,7 @@ export function buildSystemPrompt(params) {
|
|
|
59
60
|
...(subagentBlock ? ['', subagentBlock] : []),
|
|
60
61
|
...(todoBlock ? ['', todoBlock] : []),
|
|
61
62
|
...(fastBlock ? ['', fastBlock] : []),
|
|
63
|
+
...(backgroundBlock ? ['', backgroundBlock] : []),
|
|
62
64
|
'',
|
|
63
65
|
'Operating guidance:',
|
|
64
66
|
'- Prefer reading files and inspecting the workspace before making changes.',
|