@aragon-agent/cli 0.6.4 → 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 +37 -0
- package/README.md +108 -11
- package/dist/agent/controller.d.ts +39 -2
- package/dist/agent/controller.js +125 -0
- package/dist/agent/reducer.d.ts +33 -19
- package/dist/agent/reducer.js +62 -71
- package/dist/agent/usage.d.ts +34 -0
- package/dist/agent/usage.js +43 -0
- package/dist/cli.js +48 -1
- package/dist/commands/builtins.js +9 -0
- package/dist/compaction/compactor.d.ts +60 -24
- package/dist/compaction/compactor.js +89 -61
- 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 +41 -0
- package/dist/compaction/wiring.d.ts +47 -0
- package/dist/compaction/wiring.js +88 -27
- package/dist/config/cli-commands.js +5 -0
- package/dist/config/env.js +15 -1
- package/dist/config/load.d.ts +17 -0
- package/dist/config/load.js +45 -1
- package/dist/config/schema.d.ts +44 -0
- package/dist/config/schema.js +36 -0
- package/dist/config/store.js +27 -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/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/ui/App.js +75 -63
- 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 +3 -1
- package/dist/ui/Composer.js +2 -2
- package/dist/ui/PromptInput.d.ts +4 -1
- package/dist/ui/PromptInput.js +19 -5
- package/dist/ui/StatusBar.d.ts +21 -15
- package/dist/ui/StatusBar.js +48 -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/UpdateLine.d.ts +1 -1
- package/dist/ui/capabilities.js +5 -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/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/transcript-text.js +6 -2
- package/package.json +2 -2
package/dist/config/store.js
CHANGED
|
@@ -14,7 +14,7 @@ import { dirname } from 'node:path';
|
|
|
14
14
|
import process from 'node:process';
|
|
15
15
|
import { getConfigPath, getSessionsDir } from './app-paths.js';
|
|
16
16
|
import { registerSecretsFrom } from '../logging/secret-registry.js';
|
|
17
|
-
import { clampBashConfig, CONFIG_VERSION, DEFAULT_CONFIG, clampCompactionConfig, clampFastConfig, clampLogConfig, clampMaxTokens, clampRetryConfig, clampSkillsConfig, clampTeamConfig, clampTodoConfig, clampUpdateConfig, isAutoToken, stripLegacyStateKeys, } from './schema.js';
|
|
17
|
+
import { clampBashConfig, CONFIG_VERSION, DEFAULT_CONFIG, clampCompactionConfig, clampFastConfig, clampLogConfig, clampMaxTokens, clampRetryConfig, clampSkillsConfig, clampTeamConfig, clampTodoConfig, clampContextWindow, clampUpdateConfig, isAutoToken, stripLegacyStateKeys, } from './schema.js';
|
|
18
18
|
/**
|
|
19
19
|
* Normalize `maxTokens` for both directions of the config file.
|
|
20
20
|
*
|
|
@@ -31,6 +31,22 @@ function normalizeMaxTokens(v) {
|
|
|
31
31
|
return null;
|
|
32
32
|
return clampMaxTokens(v) ?? null;
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Normalize `contextWindow` for both directions of the config file.
|
|
36
|
+
*
|
|
37
|
+
* THE SAME TRI-STATE DISCIPLINE AS `maxTokens` ABOVE, and for the same reason:
|
|
38
|
+
* hardening only the read path leaves a bad value on disk that reverts on every
|
|
39
|
+
* launch and presents as "my setting won't stick". `null` is AUTO and survives
|
|
40
|
+
* untouched; `auto` / `0` / an empty string mean AUTO too; a number is clamped;
|
|
41
|
+
* anything else on a hand-edited file becomes AUTO rather than a silent default.
|
|
42
|
+
*/
|
|
43
|
+
function normalizeContextWindow(v) {
|
|
44
|
+
if (v === undefined)
|
|
45
|
+
return DEFAULT_CONFIG.contextWindow;
|
|
46
|
+
if (v === null || isAutoToken(v))
|
|
47
|
+
return null;
|
|
48
|
+
return clampContextWindow(v, null);
|
|
49
|
+
}
|
|
34
50
|
// Re-exported so the many existing importers of `store.js` keep working; the
|
|
35
51
|
// definitions live in `app-paths.ts` because they are pure path arithmetic.
|
|
36
52
|
export { getConfigPath, getSessionsDir };
|
|
@@ -114,6 +130,11 @@ export function loadPersistedConfig() {
|
|
|
114
130
|
maxTokens: Object.prototype.hasOwnProperty.call(partial, 'maxTokens')
|
|
115
131
|
? normalizeMaxTokens(partial.maxTokens)
|
|
116
132
|
: DEFAULT_CONFIG.maxTokens,
|
|
133
|
+
// Own-property aware for the reason its neighbour records: `null` is AUTO
|
|
134
|
+
// and a spread cannot tell it from an absent key.
|
|
135
|
+
contextWindow: Object.prototype.hasOwnProperty.call(partial, 'contextWindow')
|
|
136
|
+
? normalizeContextWindow(partial.contextWindow)
|
|
137
|
+
: DEFAULT_CONFIG.contextWindow,
|
|
117
138
|
apiKeys: { ...DEFAULT_CONFIG.apiKeys, ...(partial.apiKeys ?? {}) },
|
|
118
139
|
// `skills` is the second nested object in this file and needs the same
|
|
119
140
|
// treatment `apiKeys` gets — see the note on updatePersistedConfig below.
|
|
@@ -198,6 +219,11 @@ export function updatePersistedConfig(patch) {
|
|
|
198
219
|
maxTokens: Object.prototype.hasOwnProperty.call(patch, 'maxTokens')
|
|
199
220
|
? normalizeMaxTokens(patch.maxTokens)
|
|
200
221
|
: current.maxTokens,
|
|
222
|
+
// The write half of the same gate: without it `config set contextWindow
|
|
223
|
+
// 99999999` puts a number on disk that the read path silently rewrites.
|
|
224
|
+
contextWindow: Object.prototype.hasOwnProperty.call(patch, 'contextWindow')
|
|
225
|
+
? normalizeContextWindow(patch.contextWindow)
|
|
226
|
+
: current.contextWindow,
|
|
201
227
|
apiKeys: { ...current.apiKeys, ...(patch.apiKeys ?? {}) },
|
|
202
228
|
skills: clampSkillsConfig({ ...current.skills, ...(patch.skills ?? {}) }),
|
|
203
229
|
// Without this line, changing the log level from the settings screen would
|
package/dist/exec/events.d.ts
CHANGED
|
@@ -71,6 +71,43 @@ export interface ExecSystemInitEvent {
|
|
|
71
71
|
permissionMode: ExecPermissionMode;
|
|
72
72
|
tools: string[];
|
|
73
73
|
resumed: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Capabilities of THIS build that a caller has to know about before using
|
|
76
|
+
* them. Optional so older consumers are unaffected, and additive so
|
|
77
|
+
* `EXEC_SCHEMA_VERSION` does not move (see the file header).
|
|
78
|
+
*
|
|
79
|
+
* `'interrupt'` means the stdin `{"type":"interrupt"}` frame really does run
|
|
80
|
+
* the full settle -> persist -> `result` sequence ON EVERY PLATFORM. Builds
|
|
81
|
+
* before that fix routed it through a self-directed SIGINT, which on Windows
|
|
82
|
+
* terminates the process outright with exit code 1 - no persistence, no
|
|
83
|
+
* `result`. A wrapper cannot tell the two apart by observation, and guessing
|
|
84
|
+
* wrong costs the user their conversation, so this build states it.
|
|
85
|
+
*/
|
|
86
|
+
capabilities?: string[];
|
|
87
|
+
/**
|
|
88
|
+
* Absolute path of the session file THIS run reads and writes; `null` under
|
|
89
|
+
* `--no-save-session` (web-use-durable-control-and-tier-accountability W2).
|
|
90
|
+
*
|
|
91
|
+
* Optional and additive, so `EXEC_SCHEMA_VERSION` does not move (file header).
|
|
92
|
+
* Stamped by `buildInitEvent`, never set by a caller - the same discipline
|
|
93
|
+
* `capabilities` follows.
|
|
94
|
+
*
|
|
95
|
+
* ## Why a wrapper cannot work this out for itself
|
|
96
|
+
*
|
|
97
|
+
* Deriving it host-side means re-implementing `getSessionsDir()`
|
|
98
|
+
* (`envPaths('aragon-agent')` plus its environment override) in a second
|
|
99
|
+
* codebase that has no way to notice when this one changes. The failure mode
|
|
100
|
+
* of a drifted copy is a path that does not exist, which a wrapper reads as
|
|
101
|
+
* "that conversation is gone" - identical, on screen, to the session really
|
|
102
|
+
* being gone. The run that is writing the file is the only party that can say
|
|
103
|
+
* where it is without guessing.
|
|
104
|
+
*
|
|
105
|
+
* It is the id THIS RUN writes back under, not the string the caller typed:
|
|
106
|
+
* `--resume` accepts `<id|path>`, and `resumedSessionId()` decides what the
|
|
107
|
+
* file is actually called. A wrapper asking "can I reattach later" needs the
|
|
108
|
+
* latter.
|
|
109
|
+
*/
|
|
110
|
+
sessionFile?: string | null;
|
|
74
111
|
}
|
|
75
112
|
export interface ExecUserEvent {
|
|
76
113
|
type: 'user';
|
|
@@ -84,6 +121,22 @@ export interface ExecUserEvent {
|
|
|
84
121
|
*/
|
|
85
122
|
source: 'caller' | 'todo_continue';
|
|
86
123
|
}
|
|
124
|
+
/** One caller input and its complete automatic follow-through chain. */
|
|
125
|
+
export interface ExecTurnStateEvent {
|
|
126
|
+
type: 'turn_state';
|
|
127
|
+
sessionId: string;
|
|
128
|
+
requestSeq: number;
|
|
129
|
+
phase: 'started' | 'completed' | 'failed' | 'cancelled';
|
|
130
|
+
}
|
|
131
|
+
/** Semantic source progress; never includes model or tool content. */
|
|
132
|
+
export interface ExecProgressEvent {
|
|
133
|
+
type: 'execution_progress';
|
|
134
|
+
sessionId: string;
|
|
135
|
+
requestSeq: number;
|
|
136
|
+
progressSeq: number;
|
|
137
|
+
phase: 'model' | 'thinking' | 'tool_arguments' | 'tool' | 'compaction' | 'retry';
|
|
138
|
+
retryDelayMs?: number;
|
|
139
|
+
}
|
|
87
140
|
export interface ExecTextDeltaEvent {
|
|
88
141
|
type: 'text_delta';
|
|
89
142
|
sessionId: string;
|
|
@@ -157,6 +210,57 @@ export interface ExecFastReviewEvent {
|
|
|
157
210
|
model: string;
|
|
158
211
|
text: string;
|
|
159
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* What the fast tier is ACTUALLY doing, as opposed to what it was configured to
|
|
215
|
+
* do (web-use-tier-cooperation-and-control-closure W2).
|
|
216
|
+
*
|
|
217
|
+
* ONE ADDITIVE EVENT TYPE, AND `EXEC_SCHEMA_VERSION` DOES NOT MOVE — the same
|
|
218
|
+
* rule `ExecCompactionEvent` states. A consumer that ignores event types it does
|
|
219
|
+
* not know is unaffected.
|
|
220
|
+
*
|
|
221
|
+
* THE PROBLEM IT SOLVES. `fast_review` only fires when a review produced TEXT,
|
|
222
|
+
* so every other outcome — a review that failed, a tier that switched itself off
|
|
223
|
+
* after three failures, a budget that ran out, a delegation that did or did not
|
|
224
|
+
* happen — was invisible to a wrapper. The single most likely and least
|
|
225
|
+
* self-diagnosable failure ("it runs but not one review card ever arrives") had
|
|
226
|
+
* no signal at all on this stream.
|
|
227
|
+
*
|
|
228
|
+
* IT IS A STATE EVENT, NOT A LOG LINE. It fires on `tier_changed` and on every
|
|
229
|
+
* `review_end`, which in a session with reviews is a handful per hundred turns.
|
|
230
|
+
*/
|
|
231
|
+
export interface ExecFastTierEvent {
|
|
232
|
+
type: 'fast_tier';
|
|
233
|
+
sessionId: string;
|
|
234
|
+
/** Taken from the runner's own turn counter — `tier_changed` carries none. */
|
|
235
|
+
turn: number;
|
|
236
|
+
/** The tier resolved AND the live switch is on. */
|
|
237
|
+
live: boolean;
|
|
238
|
+
/**
|
|
239
|
+
* The CLI switched the fast REVIEWS off by itself after three consecutive
|
|
240
|
+
* non-transient failures.
|
|
241
|
+
*
|
|
242
|
+
* ORTHOGONAL TO `live` AND NOT DERIVABLE FROM IT: the self-disable leaves
|
|
243
|
+
* `live` at `true`. A consumer reporting "the reviews stopped" MUST read this
|
|
244
|
+
* field; `live === false && reviews > 0` is a predicate that can never be true.
|
|
245
|
+
*/
|
|
246
|
+
selfDisabled: boolean;
|
|
247
|
+
model: string;
|
|
248
|
+
sameAsMain: boolean;
|
|
249
|
+
reviews: number;
|
|
250
|
+
reviewBudget: number;
|
|
251
|
+
budgetReached: boolean;
|
|
252
|
+
/** Fast-tier children dispatched — the only evidence delegation is working. */
|
|
253
|
+
delegated: number;
|
|
254
|
+
inFlight: boolean;
|
|
255
|
+
/** Present only on the `review_end` branch. */
|
|
256
|
+
review?: {
|
|
257
|
+
index: number;
|
|
258
|
+
kind: 'ok' | 'advice' | 'empty' | 'failed' | 'dropped';
|
|
259
|
+
detail?: string;
|
|
260
|
+
durationMs: number;
|
|
261
|
+
injected: boolean;
|
|
262
|
+
};
|
|
263
|
+
}
|
|
160
264
|
/**
|
|
161
265
|
* Context compaction (context-auto-compaction §4.6).
|
|
162
266
|
*
|
|
@@ -251,7 +355,7 @@ export interface ExecResultEvent {
|
|
|
251
355
|
todos: ExecTodoCounts | null;
|
|
252
356
|
error: ExecErrorInfo | null;
|
|
253
357
|
}
|
|
254
|
-
export type ExecEvent = ExecSystemInitEvent | ExecUserEvent | ExecTextDeltaEvent | ExecThinkingEvent | ExecAssistantEvent | ExecToolCallEvent | ExecToolResultEvent | ExecTodoEvent | ExecTeamEvent | ExecFastReviewEvent | ExecCompactionEvent | ExecRetryEvent | ExecErrorEvent | ExecResultEvent;
|
|
358
|
+
export type ExecEvent = ExecSystemInitEvent | ExecUserEvent | ExecTurnStateEvent | ExecProgressEvent | ExecTextDeltaEvent | ExecThinkingEvent | ExecAssistantEvent | ExecToolCallEvent | ExecToolResultEvent | ExecTodoEvent | ExecTeamEvent | ExecFastReviewEvent | ExecFastTierEvent | ExecCompactionEvent | ExecRetryEvent | ExecErrorEvent | ExecResultEvent;
|
|
255
359
|
/** Everything `system/init` needs that the runner cannot derive for itself. */
|
|
256
360
|
export interface ExecInitParams {
|
|
257
361
|
sessionId: string;
|
|
@@ -266,7 +370,40 @@ export interface ExecInitParams {
|
|
|
266
370
|
permissionMode: ExecPermissionMode;
|
|
267
371
|
tools: string[];
|
|
268
372
|
resumed: boolean;
|
|
373
|
+
/**
|
|
374
|
+
* Absolute path of the session file this run reads and writes; `null` under
|
|
375
|
+
* `--no-save-session`. See `ExecSystemInitEvent.sessionFile`.
|
|
376
|
+
*/
|
|
377
|
+
sessionFile?: string | null;
|
|
269
378
|
}
|
|
379
|
+
/**
|
|
380
|
+
* What this build announces in `system/init`.
|
|
381
|
+
*
|
|
382
|
+
* Stamped by `buildInitEvent`, never by a caller - the same discipline as
|
|
383
|
+
* `schemaVersion`. A caller that could set it could claim a capability the
|
|
384
|
+
* binary does not have, which is precisely the mistake the field exists to
|
|
385
|
+
* prevent.
|
|
386
|
+
*/
|
|
387
|
+
/**
|
|
388
|
+
* APPEND ONLY, NEVER REORDER. Consumers use `includes()`, but the next one may
|
|
389
|
+
* not; inserting ahead of `'interrupt'` costs nothing and makes a diff look
|
|
390
|
+
* bigger than it is.
|
|
391
|
+
*
|
|
392
|
+
* · `'interrupt'` — `{"type":"interrupt"}` on stdin settles the turn
|
|
393
|
+
* without killing the process.
|
|
394
|
+
* · `'fast-policy'` — this build understands `--fast-delegate` and
|
|
395
|
+
* `--fast-review`. FOR AFTER-THE-FACT DISPLAY ONLY:
|
|
396
|
+
* argv is fixed before `system/init` arrives, so it
|
|
397
|
+
* cannot gate whether the flags are passed.
|
|
398
|
+
* · `'fast-tier-events'` — this build emits `fast_tier`. Its ABSENCE is what
|
|
399
|
+
* lets a wrapper say "this CLI does not report" and
|
|
400
|
+
* not "the fast tier did nothing" — two very
|
|
401
|
+
* different sentences to a user.
|
|
402
|
+
* · `'session-file'` — `system/init` carries `sessionFile`, so a wrapper
|
|
403
|
+
* can check on disk whether a stopped conversation
|
|
404
|
+
* is still resumable instead of guessing the path.
|
|
405
|
+
*/
|
|
406
|
+
export declare const EXEC_CAPABILITIES: readonly string[];
|
|
270
407
|
export declare function buildInitEvent(params: ExecInitParams): ExecSystemInitEvent;
|
|
271
408
|
/** Everything `result` needs. `schemaVersion` is stamped here, never by a caller. */
|
|
272
409
|
export type ExecResultParams = Omit<ExecResultEvent, 'type' | 'schemaVersion'>;
|
package/dist/exec/events.js
CHANGED
|
@@ -19,6 +19,41 @@
|
|
|
19
19
|
* additive change and every wrapper would pin an exact number forever.
|
|
20
20
|
*/
|
|
21
21
|
export const EXEC_SCHEMA_VERSION = 1;
|
|
22
|
+
/**
|
|
23
|
+
* What this build announces in `system/init`.
|
|
24
|
+
*
|
|
25
|
+
* Stamped by `buildInitEvent`, never by a caller - the same discipline as
|
|
26
|
+
* `schemaVersion`. A caller that could set it could claim a capability the
|
|
27
|
+
* binary does not have, which is precisely the mistake the field exists to
|
|
28
|
+
* prevent.
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* APPEND ONLY, NEVER REORDER. Consumers use `includes()`, but the next one may
|
|
32
|
+
* not; inserting ahead of `'interrupt'` costs nothing and makes a diff look
|
|
33
|
+
* bigger than it is.
|
|
34
|
+
*
|
|
35
|
+
* · `'interrupt'` — `{"type":"interrupt"}` on stdin settles the turn
|
|
36
|
+
* without killing the process.
|
|
37
|
+
* · `'fast-policy'` — this build understands `--fast-delegate` and
|
|
38
|
+
* `--fast-review`. FOR AFTER-THE-FACT DISPLAY ONLY:
|
|
39
|
+
* argv is fixed before `system/init` arrives, so it
|
|
40
|
+
* cannot gate whether the flags are passed.
|
|
41
|
+
* · `'fast-tier-events'` — this build emits `fast_tier`. Its ABSENCE is what
|
|
42
|
+
* lets a wrapper say "this CLI does not report" and
|
|
43
|
+
* not "the fast tier did nothing" — two very
|
|
44
|
+
* different sentences to a user.
|
|
45
|
+
* · `'session-file'` — `system/init` carries `sessionFile`, so a wrapper
|
|
46
|
+
* can check on disk whether a stopped conversation
|
|
47
|
+
* is still resumable instead of guessing the path.
|
|
48
|
+
*/
|
|
49
|
+
export const EXEC_CAPABILITIES = [
|
|
50
|
+
'interrupt',
|
|
51
|
+
'fast-policy',
|
|
52
|
+
'fast-tier-events',
|
|
53
|
+
'session-file',
|
|
54
|
+
'turn-lifecycle',
|
|
55
|
+
'execution-progress',
|
|
56
|
+
];
|
|
22
57
|
export function buildInitEvent(params) {
|
|
23
58
|
return {
|
|
24
59
|
type: 'system',
|
|
@@ -32,6 +67,13 @@ export function buildInitEvent(params) {
|
|
|
32
67
|
permissionMode: params.permissionMode,
|
|
33
68
|
tools: params.tools,
|
|
34
69
|
resumed: params.resumed,
|
|
70
|
+
capabilities: [...EXEC_CAPABILITIES],
|
|
71
|
+
// `?? null` rather than a conditional spread: the field's contract is
|
|
72
|
+
// three-valued for a CONSUMER (`undefined` = an old build that cannot say,
|
|
73
|
+
// `null` = this run saves nothing, a string = here it is), and omitting it
|
|
74
|
+
// when a caller passed nothing would make this build indistinguishable from
|
|
75
|
+
// one that predates the field.
|
|
76
|
+
sessionFile: params.sessionFile ?? null,
|
|
35
77
|
};
|
|
36
78
|
}
|
|
37
79
|
export function buildResultEvent(params) {
|
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;
|