@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/exec/index.js
CHANGED
|
@@ -58,7 +58,18 @@ export async function runExec(flags, raw, promptArg, ctx) {
|
|
|
58
58
|
const plan = planSession(options, cwd, err);
|
|
59
59
|
if (!plan)
|
|
60
60
|
return 2;
|
|
61
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Late-bound interrupt hook (see `resolvePromptSource` and `execute`).
|
|
63
|
+
*
|
|
64
|
+
* The stdin stream is built - and already reading - about eighty lines before
|
|
65
|
+
* the runner exists, so the frame handler cannot close over the runner
|
|
66
|
+
* directly. The box starts as a no-op and is filled in AFTER
|
|
67
|
+
* `runner.attach(controller)`; during that gap an interrupt does nothing at
|
|
68
|
+
* all, which is the honest outcome (the user simply presses it again). See
|
|
69
|
+
* `execute` for why "does nothing" beats "does half of it".
|
|
70
|
+
*/
|
|
71
|
+
const interrupt = { fire: () => { } };
|
|
72
|
+
const prompts = resolvePromptSource(options, promptArg, ctx, err, interrupt);
|
|
62
73
|
if (!prompts)
|
|
63
74
|
return 2;
|
|
64
75
|
const lock = plan.saving ? tryAcquireSessionLock(plan.id) : null;
|
|
@@ -68,14 +79,14 @@ export async function runExec(flags, raw, promptArg, ctx) {
|
|
|
68
79
|
return 2;
|
|
69
80
|
}
|
|
70
81
|
try {
|
|
71
|
-
return await execute({ flags, options, plan, prompts, ctx, out, err, now, appended });
|
|
82
|
+
return await execute({ flags, options, plan, prompts, ctx, out, err, now, appended, interrupt });
|
|
72
83
|
}
|
|
73
84
|
finally {
|
|
74
85
|
lock?.release();
|
|
75
86
|
}
|
|
76
87
|
}
|
|
77
88
|
async function execute(input) {
|
|
78
|
-
const { options, plan, ctx, out, err, now } = input;
|
|
89
|
+
const { options, plan, ctx, out, err, now, interrupt } = input;
|
|
79
90
|
const startedAt = now();
|
|
80
91
|
const permission = resolvePermission({
|
|
81
92
|
mode: options.permissionMode,
|
|
@@ -117,8 +128,29 @@ async function execute(input) {
|
|
|
117
128
|
permissionMode: options.permissionMode,
|
|
118
129
|
tools: controller.listTools().map((t) => t.name),
|
|
119
130
|
resumed: plan.saved !== null,
|
|
131
|
+
// W2 - where this run's conversation lives, so a wrapper can check on disk
|
|
132
|
+
// whether it is still resumable rather than re-deriving the sessions dir.
|
|
133
|
+
//
|
|
134
|
+
// `plan.id`, NEVER `options.resume`. The flag accepts `<id|path>`, and
|
|
135
|
+
// `planSession` runs the caller's string through `resumedSessionId()` to
|
|
136
|
+
// decide what the file is actually named - so "the file the caller pointed
|
|
137
|
+
// at" and "the file this run writes" are routinely two different paths. The
|
|
138
|
+
// question a wrapper is asking ("what can I reattach to next time") is
|
|
139
|
+
// answered by the second one.
|
|
140
|
+
//
|
|
141
|
+
// `!plan.saving` is `--no-save-session`: nothing is written, so there will
|
|
142
|
+
// be nothing to reattach to, and `null` says that outright.
|
|
143
|
+
sessionFile: plan.saving ? sessionPathFor(plan.id) : null,
|
|
120
144
|
});
|
|
121
145
|
runner.attach(controller);
|
|
146
|
+
// AFTER `attach`, never before. The terminator closure ends on
|
|
147
|
+
// `this.controller?.abort()`, so before `attach` the controller is still
|
|
148
|
+
// `undefined` and that last statement is a silent no-op - while the SAME
|
|
149
|
+
// closure has already written `lastSignalAt` and `stopReason = 'interrupted'`.
|
|
150
|
+
// A real interrupt arriving within the next two seconds would then be judged a
|
|
151
|
+
// SECOND signal and take `signals.exit(128 + signo)`: a hard exit, no persist,
|
|
152
|
+
// no `result` - exactly the outcome the caller was trying to avoid.
|
|
153
|
+
interrupt.fire = () => runner.requestInterrupt();
|
|
122
154
|
const pre = controller.preflight();
|
|
123
155
|
if (!pre.ok) {
|
|
124
156
|
runner.detach();
|
|
@@ -261,7 +293,7 @@ function failedResult(sessionId, config, message, code, durationMs) {
|
|
|
261
293
|
* Returns `null` after writing the reason to stderr, so the caller can exit 2
|
|
262
294
|
* without a second error vocabulary.
|
|
263
295
|
*/
|
|
264
|
-
function resolvePromptSource(options, promptArg, ctx, err) {
|
|
296
|
+
function resolvePromptSource(options, promptArg, ctx, err, interrupt) {
|
|
265
297
|
const stdin = ctx.stdin ?? process.stdin;
|
|
266
298
|
const opening = [];
|
|
267
299
|
const positional = promptArg?.trim();
|
|
@@ -282,8 +314,13 @@ function resolvePromptSource(options, promptArg, ctx, err) {
|
|
|
282
314
|
// line is skipped either way, which is the property AC-20 pins.
|
|
283
315
|
onBadLine: (message) => err.write(`${message}\n`),
|
|
284
316
|
// The runner installs the signal terminator; a caller-requested
|
|
285
|
-
// interrupt is the same intent arriving through a different channel
|
|
286
|
-
|
|
317
|
+
// interrupt is the same intent arriving through a different channel -
|
|
318
|
+
// so it now calls the runner DIRECTLY rather than raising a signal at
|
|
319
|
+
// ourselves. `process.kill(process.pid, 'SIGINT')` does not reach this
|
|
320
|
+
// process's own handlers on Windows: it terminates with exit code 1,
|
|
321
|
+
// skipping the settle path, `persist()` and the `result` event. See
|
|
322
|
+
// `ExecRunner.requestInterrupt`.
|
|
323
|
+
onInterrupt: () => interrupt.fire(),
|
|
287
324
|
}));
|
|
288
325
|
}
|
|
289
326
|
if (opening.length === 0 && !stdinIsTty(stdin)) {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ExecProgressEvent } from './events.js';
|
|
2
|
+
/** Request-scoped, one-shot aggregation of real semantic events. */
|
|
3
|
+
export declare class ExecProgressReporter {
|
|
4
|
+
private readonly sessionId;
|
|
5
|
+
private readonly emit;
|
|
6
|
+
private readonly now;
|
|
7
|
+
private requestSeq;
|
|
8
|
+
private progressSeq;
|
|
9
|
+
private sentSeq;
|
|
10
|
+
private phase;
|
|
11
|
+
private retryDelayMs;
|
|
12
|
+
private lastSentAt;
|
|
13
|
+
private timer;
|
|
14
|
+
private enabled;
|
|
15
|
+
constructor(sessionId: string, emit: (event: ExecProgressEvent) => void, now?: () => number);
|
|
16
|
+
begin(requestSeq: number): void;
|
|
17
|
+
/** Called only after the started event has reached the emitter. */
|
|
18
|
+
started(requestSeq: number): void;
|
|
19
|
+
record(phase: ExecProgressEvent['phase'], retryDelayMs?: number): void;
|
|
20
|
+
flush(): void;
|
|
21
|
+
finish(requestSeq: number): void;
|
|
22
|
+
dispose(): void;
|
|
23
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const PROGRESS_INTERVAL_MS = 1_000;
|
|
2
|
+
/** Request-scoped, one-shot aggregation of real semantic events. */
|
|
3
|
+
export class ExecProgressReporter {
|
|
4
|
+
sessionId;
|
|
5
|
+
emit;
|
|
6
|
+
now;
|
|
7
|
+
requestSeq = null;
|
|
8
|
+
progressSeq = 0;
|
|
9
|
+
sentSeq = 0;
|
|
10
|
+
phase = 'model';
|
|
11
|
+
retryDelayMs;
|
|
12
|
+
lastSentAt = -Infinity;
|
|
13
|
+
timer = null;
|
|
14
|
+
enabled = false;
|
|
15
|
+
constructor(sessionId, emit, now = () => performance.now()) {
|
|
16
|
+
this.sessionId = sessionId;
|
|
17
|
+
this.emit = emit;
|
|
18
|
+
this.now = now;
|
|
19
|
+
}
|
|
20
|
+
begin(requestSeq) {
|
|
21
|
+
this.dispose();
|
|
22
|
+
this.requestSeq = requestSeq;
|
|
23
|
+
this.progressSeq = 0;
|
|
24
|
+
this.sentSeq = 0;
|
|
25
|
+
this.phase = 'model';
|
|
26
|
+
this.retryDelayMs = undefined;
|
|
27
|
+
this.lastSentAt = -Infinity;
|
|
28
|
+
}
|
|
29
|
+
/** Called only after the started event has reached the emitter. */
|
|
30
|
+
started(requestSeq) {
|
|
31
|
+
if (this.requestSeq === requestSeq)
|
|
32
|
+
this.enabled = true;
|
|
33
|
+
}
|
|
34
|
+
record(phase, retryDelayMs) {
|
|
35
|
+
if (!this.enabled || this.requestSeq === null)
|
|
36
|
+
return;
|
|
37
|
+
const changed = phase !== this.phase;
|
|
38
|
+
if (changed)
|
|
39
|
+
this.flush();
|
|
40
|
+
this.phase = phase;
|
|
41
|
+
this.retryDelayMs = phase === 'retry' && Number.isFinite(retryDelayMs)
|
|
42
|
+
? Math.min(120_000, Math.max(0, retryDelayMs)) : undefined;
|
|
43
|
+
this.progressSeq += 1;
|
|
44
|
+
if (changed || this.now() - this.lastSentAt >= PROGRESS_INTERVAL_MS) {
|
|
45
|
+
this.flush();
|
|
46
|
+
}
|
|
47
|
+
else if (!this.timer) {
|
|
48
|
+
const requestSeq = this.requestSeq;
|
|
49
|
+
this.timer = setTimeout(() => {
|
|
50
|
+
this.timer = null;
|
|
51
|
+
if (this.requestSeq === requestSeq && this.enabled)
|
|
52
|
+
this.flush();
|
|
53
|
+
}, Math.max(0, PROGRESS_INTERVAL_MS - (this.now() - this.lastSentAt)));
|
|
54
|
+
this.timer.unref?.();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
flush() {
|
|
58
|
+
if (this.timer)
|
|
59
|
+
clearTimeout(this.timer);
|
|
60
|
+
this.timer = null;
|
|
61
|
+
if (!this.enabled || this.requestSeq === null || this.sentSeq === this.progressSeq)
|
|
62
|
+
return;
|
|
63
|
+
this.sentSeq = this.progressSeq;
|
|
64
|
+
this.lastSentAt = this.now();
|
|
65
|
+
this.emit({
|
|
66
|
+
type: 'execution_progress', sessionId: this.sessionId,
|
|
67
|
+
requestSeq: this.requestSeq, progressSeq: this.progressSeq, phase: this.phase,
|
|
68
|
+
...(this.retryDelayMs !== undefined ? { retryDelayMs: this.retryDelayMs } : {}),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
finish(requestSeq) {
|
|
72
|
+
if (this.requestSeq !== requestSeq)
|
|
73
|
+
return;
|
|
74
|
+
try {
|
|
75
|
+
this.flush();
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
this.dispose();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
dispose() {
|
|
82
|
+
if (this.timer)
|
|
83
|
+
clearTimeout(this.timer);
|
|
84
|
+
this.timer = null;
|
|
85
|
+
this.requestSeq = null;
|
|
86
|
+
this.enabled = false;
|
|
87
|
+
}
|
|
88
|
+
}
|
package/dist/exec/runner.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ import type { TokenUsage } from '@aragon-agent/core';
|
|
|
18
18
|
import type { HeadlessController } from '../agent/headless.js';
|
|
19
19
|
import { type FollowThroughMode } from '../todo/follow-through.js';
|
|
20
20
|
import type { ExecEmitter } from './emitter.js';
|
|
21
|
+
import type { FastSnapshot } from '../fast/types.js';
|
|
21
22
|
import type { ExecStopReason } from './events.js';
|
|
22
23
|
/**
|
|
23
24
|
* The controller surface the runner needs.
|
|
@@ -34,6 +35,16 @@ export interface ExecRunnerController extends HeadlessController {
|
|
|
34
35
|
providerId: string;
|
|
35
36
|
modelId: string;
|
|
36
37
|
}): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* The fast tier's current snapshot, or `null` when this session has no tier
|
|
40
|
+
* (web-use-tier-cooperation-and-control-closure §4.2.1).
|
|
41
|
+
*
|
|
42
|
+
* OPTIONAL, for the identical reason `subscribeFast` is: this interface is
|
|
43
|
+
* minimal so a test can satisfy it with an object literal, and a controller
|
|
44
|
+
* with no tier has nothing to report. Called with `?.()`, so a stub that omits
|
|
45
|
+
* it simply emits no `fast_tier` events.
|
|
46
|
+
*/
|
|
47
|
+
fastSnapshot?(): FastSnapshot | null;
|
|
37
48
|
}
|
|
38
49
|
/** Where the caller's messages come from. One turn, or an NDJSON stdin stream. */
|
|
39
50
|
export interface ExecPromptSource {
|
|
@@ -77,6 +88,7 @@ export declare class ExecRunner {
|
|
|
77
88
|
private readonly opts;
|
|
78
89
|
private readonly signals;
|
|
79
90
|
private readonly now;
|
|
91
|
+
private readonly progress;
|
|
80
92
|
private controller;
|
|
81
93
|
private unsubscribers;
|
|
82
94
|
private turns;
|
|
@@ -131,6 +143,9 @@ export declare class ExecRunner {
|
|
|
131
143
|
* the two faces cannot drift about what "unfinished" means.
|
|
132
144
|
*/
|
|
133
145
|
run(source: ExecPromptSource): Promise<void>;
|
|
146
|
+
private requestSeq;
|
|
147
|
+
private requestHadError;
|
|
148
|
+
private isRequestCancelled;
|
|
134
149
|
/** One caller message plus every auto-continuation it produces. */
|
|
135
150
|
private runFollowThroughChain;
|
|
136
151
|
/**
|
|
@@ -192,6 +207,25 @@ export declare class ExecRunner {
|
|
|
192
207
|
* this and add the getter.
|
|
193
208
|
*/
|
|
194
209
|
private installTerminator;
|
|
210
|
+
/**
|
|
211
|
+
* Exactly synonymous with a SIGINT, only it does not travel through a signal.
|
|
212
|
+
*
|
|
213
|
+
* The stdin `{"type":"interrupt"}` frame routes here. It used to route through
|
|
214
|
+
* `process.kill(process.pid, 'SIGINT')`, which is fine on POSIX and WRONG on
|
|
215
|
+
* Windows: Node does not deliver a self-directed SIGINT to its own handlers
|
|
216
|
+
* there, so the process terminates with exit code 1 having run neither the
|
|
217
|
+
* terminator below, nor the settle path, nor `persist()`, nor the `result`
|
|
218
|
+
* event. A wrapper that interrupts a turn and respawns on the same
|
|
219
|
+
* `--session-id` would silently get a conversation missing everything since the
|
|
220
|
+
* last save - and `exec/index.ts` was already designed for exactly that caller
|
|
221
|
+
* ("PERSIST FIRST, ANNOUNCE SECOND").
|
|
222
|
+
*
|
|
223
|
+
* POSIX behaviour is byte-for-byte unchanged: the same closure runs, with
|
|
224
|
+
* `signalNumber = 2`, `exitCode() = 130`, and the two-second second-signal
|
|
225
|
+
* escape hatch intact.
|
|
226
|
+
*/
|
|
227
|
+
requestInterrupt(): void;
|
|
228
|
+
private onSignal;
|
|
195
229
|
private emit;
|
|
196
230
|
/** Caller-facing prose. STDERR ALWAYS - stdout carries nothing but the schema. */
|
|
197
231
|
private note;
|
package/dist/exec/runner.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { formatStreamError } from '../agent/reducer.js';
|
|
18
18
|
import { setSignalTerminator } from '../logging/install.js';
|
|
19
19
|
import { advanceBudget, decideFollowThrough, emptyBudget, } from '../todo/follow-through.js';
|
|
20
|
+
import { ExecProgressReporter } from './progress-reporter.js';
|
|
20
21
|
export function singlePrompt(text) {
|
|
21
22
|
let served = false;
|
|
22
23
|
return {
|
|
@@ -40,6 +41,7 @@ export class ExecRunner {
|
|
|
40
41
|
opts;
|
|
41
42
|
signals;
|
|
42
43
|
now;
|
|
44
|
+
progress;
|
|
43
45
|
controller = null;
|
|
44
46
|
unsubscribers = [];
|
|
45
47
|
turns = 0;
|
|
@@ -62,6 +64,7 @@ export class ExecRunner {
|
|
|
62
64
|
this.opts = options;
|
|
63
65
|
this.signals = options.signals ?? DEFAULT_SIGNAL_PORT;
|
|
64
66
|
this.now = options.now ?? Date.now;
|
|
67
|
+
this.progress = new ExecProgressReporter(this.sessionId, (event) => this.emit(event));
|
|
65
68
|
}
|
|
66
69
|
// -----------------------------------------------------------------------
|
|
67
70
|
// Attach / detach
|
|
@@ -103,20 +106,65 @@ export class ExecRunner {
|
|
|
103
106
|
});
|
|
104
107
|
if (todos)
|
|
105
108
|
this.unsubscribers.push(todos);
|
|
109
|
+
// FOUR BRANCHES, and the split is the whole of W2
|
|
110
|
+
// (web-use-tier-cooperation-and-control-closure §4.2.1):
|
|
111
|
+
//
|
|
112
|
+
// · `usage` — folded into the run's totals (unchanged).
|
|
113
|
+
// · `review_end` — the EXISTING `fast_review` still fires only when the
|
|
114
|
+
// critique has text, byte-for-byte as before; a
|
|
115
|
+
// `fast_tier` now fires UNCONDITIONALLY beside it, so a
|
|
116
|
+
// failed / empty / dropped review is finally visible.
|
|
117
|
+
// · `tier_changed` — a `fast_tier` with no `review` sub-object. This is the
|
|
118
|
+
// one that carries `selfDisabled: true`, because the
|
|
119
|
+
// reviewer emits it right after switching itself off.
|
|
120
|
+
// · `review_start` — DROPPED. `inFlight` on the snapshot already says it,
|
|
121
|
+
// and once every N turns it is pure noise.
|
|
106
122
|
const fast = controller.subscribeFast?.((event) => {
|
|
107
123
|
if (event.type === 'usage') {
|
|
108
124
|
this.usage.inputTokens += event.usage.inputTokens;
|
|
109
125
|
this.usage.outputTokens += event.usage.outputTokens;
|
|
110
126
|
return;
|
|
111
127
|
}
|
|
112
|
-
if (event.type
|
|
128
|
+
if (event.type === 'review_start')
|
|
129
|
+
return;
|
|
130
|
+
if (event.type === 'review_end' && event.review.text) {
|
|
131
|
+
this.emit({
|
|
132
|
+
type: 'fast_review',
|
|
133
|
+
sessionId: this.sessionId,
|
|
134
|
+
turn: event.review.turn,
|
|
135
|
+
model: event.review.model,
|
|
136
|
+
text: event.review.text,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
// `null` means this session never registered a tier, and a session with no
|
|
140
|
+
// tier has no business appearing on this channel at all.
|
|
141
|
+
const snapshot = controller.fastSnapshot?.();
|
|
142
|
+
if (!snapshot)
|
|
113
143
|
return;
|
|
114
144
|
this.emit({
|
|
115
|
-
type: '
|
|
145
|
+
type: 'fast_tier',
|
|
116
146
|
sessionId: this.sessionId,
|
|
117
|
-
turn:
|
|
118
|
-
|
|
119
|
-
|
|
147
|
+
turn: this.turns,
|
|
148
|
+
live: snapshot.live,
|
|
149
|
+
selfDisabled: snapshot.selfDisabled,
|
|
150
|
+
model: snapshot.model,
|
|
151
|
+
sameAsMain: snapshot.sameAsMain,
|
|
152
|
+
reviews: snapshot.reviews,
|
|
153
|
+
reviewBudget: snapshot.reviewBudget,
|
|
154
|
+
budgetReached: snapshot.budgetReached,
|
|
155
|
+
delegated: snapshot.delegated,
|
|
156
|
+
inFlight: snapshot.inFlight,
|
|
157
|
+
...(event.type === 'review_end'
|
|
158
|
+
? {
|
|
159
|
+
review: {
|
|
160
|
+
index: event.review.index,
|
|
161
|
+
kind: event.review.kind,
|
|
162
|
+
...(event.review.detail !== undefined ? { detail: event.review.detail } : {}),
|
|
163
|
+
durationMs: event.review.durationMs,
|
|
164
|
+
injected: event.review.injected,
|
|
165
|
+
},
|
|
166
|
+
}
|
|
167
|
+
: {}),
|
|
120
168
|
});
|
|
121
169
|
});
|
|
122
170
|
if (fast)
|
|
@@ -132,6 +180,7 @@ export class ExecRunner {
|
|
|
132
180
|
return;
|
|
133
181
|
}
|
|
134
182
|
if (event.type === 'compaction_start') {
|
|
183
|
+
this.progress.record('compaction');
|
|
135
184
|
this.emit({
|
|
136
185
|
type: 'compaction',
|
|
137
186
|
sessionId: this.sessionId,
|
|
@@ -143,6 +192,7 @@ export class ExecRunner {
|
|
|
143
192
|
}
|
|
144
193
|
if (event.type !== 'compaction_end')
|
|
145
194
|
return;
|
|
195
|
+
this.progress.record('model');
|
|
146
196
|
const r = event.record;
|
|
147
197
|
this.emit({
|
|
148
198
|
type: 'compaction',
|
|
@@ -177,6 +227,7 @@ export class ExecRunner {
|
|
|
177
227
|
if (this.detached)
|
|
178
228
|
return;
|
|
179
229
|
this.detached = true;
|
|
230
|
+
this.progress.dispose();
|
|
180
231
|
for (const off of this.unsubscribers.splice(0)) {
|
|
181
232
|
try {
|
|
182
233
|
off();
|
|
@@ -241,15 +292,42 @@ export class ExecRunner {
|
|
|
241
292
|
// nothing more to say exits 0 with `end_turn`, not 3 with `max_turns`.
|
|
242
293
|
if (next === null)
|
|
243
294
|
break;
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
295
|
+
const requestSeq = ++this.requestSeq;
|
|
296
|
+
this.requestHadError = false;
|
|
297
|
+
this.progress.begin(requestSeq);
|
|
298
|
+
this.emit({ type: 'turn_state', sessionId: this.sessionId, requestSeq, phase: 'started' });
|
|
299
|
+
this.progress.started(requestSeq);
|
|
300
|
+
try {
|
|
301
|
+
if (this.isRequestCancelled())
|
|
302
|
+
break;
|
|
303
|
+
if (this.turnBudgetExhausted()) {
|
|
304
|
+
this.stopReason = 'max_turns';
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
const finished = await this.runFollowThroughChain(controller, next, followThrough);
|
|
308
|
+
if (finished)
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
this.errored = true;
|
|
313
|
+
this.recordError('agent_error', err instanceof Error ? err.message : String(err));
|
|
314
|
+
throw err;
|
|
315
|
+
}
|
|
316
|
+
finally {
|
|
317
|
+
this.progress.finish(requestSeq);
|
|
318
|
+
const phase = this.isRequestCancelled()
|
|
319
|
+
? 'cancelled'
|
|
320
|
+
: this.requestHadError ? 'failed' : 'completed';
|
|
321
|
+
this.emit({ type: 'turn_state', sessionId: this.sessionId, requestSeq, phase });
|
|
247
322
|
}
|
|
248
|
-
const finished = await this.runFollowThroughChain(controller, next, followThrough);
|
|
249
|
-
if (finished)
|
|
250
|
-
break;
|
|
251
323
|
}
|
|
252
324
|
}
|
|
325
|
+
requestSeq = 0;
|
|
326
|
+
requestHadError = false;
|
|
327
|
+
isRequestCancelled() {
|
|
328
|
+
return this.stopReason === 'interrupted' || this.stopReason === 'timeout' ||
|
|
329
|
+
this.stopReason === 'max_turns';
|
|
330
|
+
}
|
|
253
331
|
/** One caller message plus every auto-continuation it produces. */
|
|
254
332
|
async runFollowThroughChain(controller, initial, followThrough) {
|
|
255
333
|
let budget = emptyBudget();
|
|
@@ -281,7 +359,7 @@ export class ExecRunner {
|
|
|
281
359
|
// `aborted` is false here for the same structural reason `-p` gives: an
|
|
282
360
|
// interrupt sets `stopReason` and returns above, so this branch is only
|
|
283
361
|
// reached by a run that ended on its own terms.
|
|
284
|
-
runEnd: { aborted: false, errored: this.
|
|
362
|
+
runEnd: { aborted: false, errored: this.requestHadError },
|
|
285
363
|
budget,
|
|
286
364
|
interactive: false,
|
|
287
365
|
});
|
|
@@ -394,18 +472,39 @@ export class ExecRunner {
|
|
|
394
472
|
* this and add the getter.
|
|
395
473
|
*/
|
|
396
474
|
installTerminator() {
|
|
397
|
-
this.signals.setTerminator((signo) =>
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
475
|
+
this.signals.setTerminator((signo) => this.onSignal(signo));
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Exactly synonymous with a SIGINT, only it does not travel through a signal.
|
|
479
|
+
*
|
|
480
|
+
* The stdin `{"type":"interrupt"}` frame routes here. It used to route through
|
|
481
|
+
* `process.kill(process.pid, 'SIGINT')`, which is fine on POSIX and WRONG on
|
|
482
|
+
* Windows: Node does not deliver a self-directed SIGINT to its own handlers
|
|
483
|
+
* there, so the process terminates with exit code 1 having run neither the
|
|
484
|
+
* terminator below, nor the settle path, nor `persist()`, nor the `result`
|
|
485
|
+
* event. A wrapper that interrupts a turn and respawns on the same
|
|
486
|
+
* `--session-id` would silently get a conversation missing everything since the
|
|
487
|
+
* last save - and `exec/index.ts` was already designed for exactly that caller
|
|
488
|
+
* ("PERSIST FIRST, ANNOUNCE SECOND").
|
|
489
|
+
*
|
|
490
|
+
* POSIX behaviour is byte-for-byte unchanged: the same closure runs, with
|
|
491
|
+
* `signalNumber = 2`, `exitCode() = 130`, and the two-second second-signal
|
|
492
|
+
* escape hatch intact.
|
|
493
|
+
*/
|
|
494
|
+
requestInterrupt() {
|
|
495
|
+
this.onSignal(2);
|
|
496
|
+
}
|
|
497
|
+
onSignal(signo) {
|
|
498
|
+
const at = this.now();
|
|
499
|
+
const secondSignal = at - this.lastSignalAt <= SECOND_SIGNAL_WINDOW_MS;
|
|
500
|
+
if (this.detached || (this.lastSignalAt > 0 && secondSignal)) {
|
|
501
|
+
this.signals.exit(128 + signo);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
this.lastSignalAt = at;
|
|
505
|
+
this.signalNumber = signo;
|
|
506
|
+
this.stopReason = 'interrupted';
|
|
507
|
+
this.controller?.abort();
|
|
409
508
|
}
|
|
410
509
|
// -----------------------------------------------------------------------
|
|
411
510
|
// Event translation
|
|
@@ -431,6 +530,7 @@ export class ExecRunner {
|
|
|
431
530
|
this.checkTurnBudget();
|
|
432
531
|
break;
|
|
433
532
|
case 'tool_execution_start':
|
|
533
|
+
this.progress.record('tool');
|
|
434
534
|
this.emit({
|
|
435
535
|
type: 'tool_call',
|
|
436
536
|
sessionId: this.sessionId,
|
|
@@ -441,6 +541,7 @@ export class ExecRunner {
|
|
|
441
541
|
});
|
|
442
542
|
break;
|
|
443
543
|
case 'tool_execution_end':
|
|
544
|
+
this.progress.record('model');
|
|
444
545
|
this.emit({
|
|
445
546
|
type: 'tool_result',
|
|
446
547
|
sessionId: this.sessionId,
|
|
@@ -457,7 +558,7 @@ export class ExecRunner {
|
|
|
457
558
|
break;
|
|
458
559
|
case 'agent_end':
|
|
459
560
|
// A swallowed throw (missing key / empty stream) produces no `turn_end`.
|
|
460
|
-
if (!this.sawTurnEnd && !this.
|
|
561
|
+
if (!this.sawTurnEnd && !this.requestHadError && !this.isRequestCancelled()) {
|
|
461
562
|
this.errored = true;
|
|
462
563
|
this.recordError('agent_error', 'The run produced no assistant turn.');
|
|
463
564
|
}
|
|
@@ -467,6 +568,16 @@ export class ExecRunner {
|
|
|
467
568
|
}
|
|
468
569
|
}
|
|
469
570
|
onStreamEvent(se) {
|
|
571
|
+
if (se.type === 'text_delta' && se.delta)
|
|
572
|
+
this.progress.record('model');
|
|
573
|
+
if (se.type === 'thinking_delta' && se.delta)
|
|
574
|
+
this.progress.record('thinking');
|
|
575
|
+
if (se.type === 'tool_call_delta' && se.argsDelta)
|
|
576
|
+
this.progress.record('tool_arguments');
|
|
577
|
+
if (se.type === 'retry_scheduled')
|
|
578
|
+
this.progress.record('retry', se.delayMs);
|
|
579
|
+
if (se.type === 'retry_attempt')
|
|
580
|
+
this.progress.record('model');
|
|
470
581
|
if (se.type === 'text_delta') {
|
|
471
582
|
this.flushThinking();
|
|
472
583
|
if (this.opts.partialMessages) {
|
|
@@ -527,6 +638,7 @@ export class ExecRunner {
|
|
|
527
638
|
this.emit({ type: 'thinking', sessionId: this.sessionId, turn: this.turns + 1, text });
|
|
528
639
|
}
|
|
529
640
|
recordError(code, message) {
|
|
641
|
+
this.requestHadError = true;
|
|
530
642
|
if (this.errorCode)
|
|
531
643
|
return;
|
|
532
644
|
this.errorCode = code;
|
package/dist/fast/reviewer.d.ts
CHANGED
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
import type { AgentEvent, AssistantMessage, LLMRequest, TokenUsage } from '@aragon-agent/core';
|
|
60
60
|
import type { FastConfig } from '../config/schema.js';
|
|
61
61
|
import type { NoticeLevel } from '../agent/reducer.js';
|
|
62
|
-
import type { FastEvent, FastTier } from './types.js';
|
|
62
|
+
import type { FastEvent, FastSnapshot, FastTier } from './types.js';
|
|
63
63
|
/**
|
|
64
64
|
* Everything the reviewer needs from the rest of the world.
|
|
65
65
|
*
|
|
@@ -93,6 +93,14 @@ export interface FastReviewerDeps {
|
|
|
93
93
|
available(): boolean;
|
|
94
94
|
getApiKey(providerId: string): string | undefined;
|
|
95
95
|
emit(event: FastEvent): void;
|
|
96
|
+
/**
|
|
97
|
+
* `FastWiring.snapshot()`, so the self-disable can announce itself.
|
|
98
|
+
*
|
|
99
|
+
* The reviewer cannot build a `FastSnapshot` on its own — `live`,
|
|
100
|
+
* `sameAsMain`, `delegated` and `pricingUnknown` all belong to the wiring —
|
|
101
|
+
* and the two existing `tier_changed` emitters read it the same way.
|
|
102
|
+
*/
|
|
103
|
+
snapshot(): FastSnapshot;
|
|
96
104
|
notify(level: NoticeLevel, text: string): void;
|
|
97
105
|
/** Injected clock. A test that reads the wall clock fails on a slow machine
|
|
98
106
|
* and nowhere else - the reason `TodoStore` and `TeamPanel` take one. */
|
|
@@ -155,6 +163,16 @@ export declare class FastReviewer {
|
|
|
155
163
|
isBudgetReached(): boolean;
|
|
156
164
|
sessionUsage(): TokenUsage;
|
|
157
165
|
isReviewInFlight(): boolean;
|
|
166
|
+
/**
|
|
167
|
+
* Whether this reviewer switched itself off after
|
|
168
|
+
* `FAST_LIMITS.maxConsecutiveFailures` non-transient failures in a row.
|
|
169
|
+
*
|
|
170
|
+
* Carried onto `FastSnapshot` so a wrapper can say "the fast reviews stopped,
|
|
171
|
+
* and here is why" instead of showing a tier that reads as healthy while not
|
|
172
|
+
* one review ever arrives. Same shape as `reviewCount()` / `isBudgetReached()`
|
|
173
|
+
* — a read-only accessor, no side effects.
|
|
174
|
+
*/
|
|
175
|
+
isSelfDisabled(): boolean;
|
|
158
176
|
/**
|
|
159
177
|
* `AgentController.abort()`.
|
|
160
178
|
*
|
package/dist/fast/reviewer.js
CHANGED
|
@@ -141,6 +141,18 @@ export class FastReviewer {
|
|
|
141
141
|
isReviewInFlight() {
|
|
142
142
|
return this.inFlight;
|
|
143
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Whether this reviewer switched itself off after
|
|
146
|
+
* `FAST_LIMITS.maxConsecutiveFailures` non-transient failures in a row.
|
|
147
|
+
*
|
|
148
|
+
* Carried onto `FastSnapshot` so a wrapper can say "the fast reviews stopped,
|
|
149
|
+
* and here is why" instead of showing a tier that reads as healthy while not
|
|
150
|
+
* one review ever arrives. Same shape as `reviewCount()` / `isBudgetReached()`
|
|
151
|
+
* — a read-only accessor, no side effects.
|
|
152
|
+
*/
|
|
153
|
+
isSelfDisabled() {
|
|
154
|
+
return this.selfDisabled;
|
|
155
|
+
}
|
|
144
156
|
/**
|
|
145
157
|
* `AgentController.abort()`.
|
|
146
158
|
*
|
|
@@ -561,6 +573,16 @@ export class FastReviewer {
|
|
|
561
573
|
this.selfDisabled = true;
|
|
562
574
|
this.deps.notify('warn', `Fast review disabled for this session after ${FAST_LIMITS.maxConsecutiveFailures} ` +
|
|
563
575
|
`failures. Last error: ${errorText(err)}`);
|
|
576
|
+
// THE ONE LINE THAT MAKES THE FLAG OBSERVABLE OUTSIDE THIS PROCESS.
|
|
577
|
+
//
|
|
578
|
+
// `emitReview({kind:'failed'})` above already fired, carrying a snapshot
|
|
579
|
+
// in which `selfDisabled` was still `false`, and after this point the
|
|
580
|
+
// reviewer emits NOTHING EVER AGAIN (`shouldReview()` opens with
|
|
581
|
+
// `if (this.selfDisabled) return false;`). Without this extra
|
|
582
|
+
// `tier_changed`, the field exists, the accessor exists, the snapshot line
|
|
583
|
+
// exists — and no consumer can ever see it as `true`. The `App` chip also
|
|
584
|
+
// consumes `tier_changed`; one more refresh is free.
|
|
585
|
+
this.deps.emit({ type: 'tier_changed', snapshot: this.deps.snapshot() });
|
|
564
586
|
}
|
|
565
587
|
}
|
|
566
588
|
// -----------------------------------------------------------------------
|
package/dist/fast/types.d.ts
CHANGED
|
@@ -78,6 +78,21 @@ export interface FastReview {
|
|
|
78
78
|
export interface FastSnapshot {
|
|
79
79
|
/** The tier resolved AND the live switch is on. */
|
|
80
80
|
live: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* The reviewer switched ITSELF off after `maxConsecutiveFailures` in a row
|
|
83
|
+
* (`FastReviewer.onReviewFailure`).
|
|
84
|
+
*
|
|
85
|
+
* ORTHOGONAL TO `live`, AND NEITHER IMPLIES THE OTHER. `live` answers "did
|
|
86
|
+
* this tier resolve at all" and is `registered && enabled && tier.ok`; the
|
|
87
|
+
* self-disable touches none of those three, so `live` stays `true` after it.
|
|
88
|
+
* A consumer that wants to report "the fast reviews stopped happening" MUST
|
|
89
|
+
* read this field: deriving it as `live === false` is a predicate that can
|
|
90
|
+
* never be true, and a hand-written fixture makes its test green for ever.
|
|
91
|
+
*
|
|
92
|
+
* `available()` is deliberately NOT widened to include it — see the note on
|
|
93
|
+
* `FastWiring.available`.
|
|
94
|
+
*/
|
|
95
|
+
selfDisabled: boolean;
|
|
81
96
|
model: string;
|
|
82
97
|
sameAsMain: boolean;
|
|
83
98
|
reviews: number;
|
package/dist/fast/wiring.d.ts
CHANGED
|
@@ -115,7 +115,16 @@ export declare class FastWiring {
|
|
|
115
115
|
private getFastRegistry;
|
|
116
116
|
isRegistered(): boolean;
|
|
117
117
|
isEnabled(): boolean;
|
|
118
|
-
/**
|
|
118
|
+
/**
|
|
119
|
+
* §3.3's live predicate. THE THIRD TERM IS THE POINT - see the header.
|
|
120
|
+
*
|
|
121
|
+
* `FastReviewer.isSelfDisabled()` is DELIBERATELY NOT a fourth term. Folding
|
|
122
|
+
* it in would look tidier and would break something else: `delegationAvailable()`
|
|
123
|
+
* is `available() && config.fast.delegate === true`, so three consecutive
|
|
124
|
+
* REVIEW failures would silently switch DELEGATION off as well - turning a
|
|
125
|
+
* diagnostic signal into a behaviour change for every user. The two facts are
|
|
126
|
+
* reported separately on `FastSnapshot` instead (`live` / `selfDisabled`).
|
|
127
|
+
*/
|
|
119
128
|
available(): boolean;
|
|
120
129
|
/**
|
|
121
130
|
* Whether `model: "fast"` on `task` is honoured right now.
|