@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/tools/bash-tool.js
CHANGED
|
@@ -1,59 +1,150 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* bash tool — run a shell command at full permission (spec §3.6)
|
|
2
|
+
* bash tool — run a shell command at full permission (spec §3.6), and, since
|
|
3
|
+
* background-service-supervision, supervise the ones that do not exit.
|
|
3
4
|
*
|
|
4
5
|
* Shell selection: Windows uses `process.env.ComSpec` (cmd.exe by default);
|
|
5
6
|
* POSIX uses `/bin/sh -c`. The command's stdout+stderr and exit code are
|
|
6
7
|
* captured. The `timeout?` param can only SHRINK within the executor ceiling
|
|
7
8
|
* (the core ToolExecutor owns the real timeout + 100 KB truncation — R4), so
|
|
8
9
|
* this tool does not re-implement an output cap.
|
|
10
|
+
*
|
|
11
|
+
* I-1 — THIS TOOL NEVER LEAVES ITS PROMISE PENDING. Every exit path goes through
|
|
12
|
+
* `settle`, and `settle` is reachable from FIVE places: `'error'`, `'close'`,
|
|
13
|
+
* `'exit'` plus a drain timer, an abort plus a kill grace, and the `timeout`
|
|
14
|
+
* param plus the same grace. That is the whole of W1, and each of the five
|
|
15
|
+
* closes a way the old implementation could hang:
|
|
16
|
+
*
|
|
17
|
+
* 1. It settled on `'close'`, which fires only when the process has exited AND
|
|
18
|
+
* every stdio stream is closed. A child that spawns a detached grandchild
|
|
19
|
+
* inheriting the pipe — `Start-Process`, `&`, `nohup`, and every
|
|
20
|
+
* `npm run dev` that re-execs — keeps that pipe open after the shell is
|
|
21
|
+
* long gone. The shell exited; the promise did not.
|
|
22
|
+
* 2. `stdio` defaulted to `'pipe'` for stdin, so a command that PROMPTS —
|
|
23
|
+
* `npm init`, a package manager asking to install something, `sudo`, a git
|
|
24
|
+
* credential prompt — blocked on a read that could never be satisfied.
|
|
25
|
+
* 3. An abort killed the tree and then went on waiting for a `'close'` that a
|
|
26
|
+
* detached grandchild was still holding.
|
|
27
|
+
*
|
|
28
|
+
* The happy path is BYTE-IDENTICAL to the pre-change build: `$ cmd\n<output>\n
|
|
29
|
+
* [exit code N]`, same truncation, same footer. That is what keeps the existing
|
|
30
|
+
* `bash` assertions meaningful rather than rewritten.
|
|
9
31
|
*/
|
|
10
32
|
import { spawn } from 'node:child_process';
|
|
11
33
|
import process from 'node:process';
|
|
12
34
|
import { defineTool, errorResult, textResult, } from '@aragon-agent/core';
|
|
35
|
+
import { looksLongRunning } from '../proc/classify.js';
|
|
36
|
+
import { killTree } from '../proc/kill-tree.js';
|
|
37
|
+
import { PROC_LIMITS } from '../proc/limits.js';
|
|
13
38
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
39
|
+
* The two sentences appended to the tool description when background launches
|
|
40
|
+
* are available.
|
|
41
|
+
*
|
|
42
|
+
* MODEL-FACING POLICY, so their wording is versioned with the prompt block in
|
|
43
|
+
* `proc/prompt.ts` rather than treated as a comment. They are appended
|
|
44
|
+
* CONDITIONALLY, which is half of what keeps I-2 true: with the feature off the
|
|
45
|
+
* description — and therefore the system prompt's tool list — is the pre-feature
|
|
46
|
+
* string, byte for byte.
|
|
17
47
|
*/
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
48
|
+
const BACKGROUND_DESCRIPTION = ' Set `background: true` for anything that does not exit on its own - dev ' +
|
|
49
|
+
'servers, watchers, `docker compose up`. Never use `Start-Process`, `&`, ' +
|
|
50
|
+
'`nohup` or `screen` to detach a command yourself; the runtime supervises ' +
|
|
51
|
+
'background commands, and a self-detached process cannot be reported on or ' +
|
|
52
|
+
'stopped.';
|
|
53
|
+
/**
|
|
54
|
+
* The settle window used when a supervisor is supplied WITHOUT one.
|
|
55
|
+
*
|
|
56
|
+
* Only reachable from a test that builds the tool by hand: `AgentController`
|
|
57
|
+
* always passes `cfg.bash.startupSettleMs`. It mirrors `DEFAULT_BASH_CONFIG`
|
|
58
|
+
* rather than importing it, because nothing in `tools/` imports `config/` — a
|
|
59
|
+
* tool takes its policy through the closures below, which is what
|
|
60
|
+
* `startupSettleMs` is. It is NOT expressed in terms of `PROC_LIMITS`: those are
|
|
61
|
+
* structural bounds, this is policy, and a multiple of `killGraceMs` that
|
|
62
|
+
* happens to equal the default today would silently move if that bound ever did.
|
|
63
|
+
*/
|
|
64
|
+
const FALLBACK_STARTUP_SETTLE_MS = 4000;
|
|
65
|
+
/**
|
|
66
|
+
* The foreground registry a `bash` child registers with, so `forceStop()` can
|
|
67
|
+
* reach it. `undefined` on the paths that build tools without a supervisor
|
|
68
|
+
* (every existing test), which is what keeps those byte-identical.
|
|
69
|
+
*/
|
|
70
|
+
function trackForeground(supervisor, pid) {
|
|
71
|
+
return supervisor ? supervisor.trackForeground(pid) : () => { };
|
|
72
|
+
}
|
|
73
|
+
/** Render a background launch's result block for the model (§3.5). */
|
|
74
|
+
function renderServiceResult(service, opts) {
|
|
75
|
+
const lines = [`$ ${service.command}`];
|
|
76
|
+
lines.push(`[background] service ${service.id} started (pid ${service.pid ?? '?'})`);
|
|
77
|
+
const elapsed = ((service.readyAt ?? service.endedAt ?? Date.now()) - service.startedAt) / 1000;
|
|
78
|
+
if (service.status === 'ready') {
|
|
79
|
+
const how = service.detectedBy === 'probe' ? 'port probe' : 'output';
|
|
80
|
+
const where = service.url ?? (service.port ? `port ${service.port}` : '');
|
|
81
|
+
lines.push(`[ready] ${where} (detected from ${how} after ${elapsed.toFixed(1)}s)`);
|
|
82
|
+
}
|
|
83
|
+
else if (service.status === 'running') {
|
|
84
|
+
lines.push(`[running] still alive, nothing to connect to (a watcher, most likely)`);
|
|
85
|
+
}
|
|
86
|
+
else if (service.status === 'exited' || service.status === 'failed') {
|
|
87
|
+
const code = service.exitCode === null ? `signal ${service.signal}` : `code ${service.exitCode}`;
|
|
88
|
+
lines.push(`[exited] ${code} after ${elapsed.toFixed(1)}s`);
|
|
33
89
|
}
|
|
34
90
|
else {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
91
|
+
lines.push(`[starting] not ready yet after ${elapsed.toFixed(1)}s - still coming up`);
|
|
92
|
+
}
|
|
93
|
+
if (service.portPreoccupied && service.port !== undefined) {
|
|
94
|
+
lines.push(`[note] port ${service.port} was already in use before start`);
|
|
95
|
+
}
|
|
96
|
+
if (service.rows.length > 0) {
|
|
97
|
+
lines.push('[log tail]');
|
|
98
|
+
for (const row of service.rows)
|
|
99
|
+
lines.push(` ${row}`);
|
|
100
|
+
}
|
|
101
|
+
if (opts.ephemeral) {
|
|
102
|
+
// D-15 / P1-8: headless has no user who could keep it alive, and saying so
|
|
103
|
+
// is the difference between a surprise and a documented behaviour.
|
|
104
|
+
lines.push('[note] this service will be stopped when the run ends.');
|
|
41
105
|
}
|
|
106
|
+
lines.push(`Read more with bash_output({ service: "${service.id}" }); ` +
|
|
107
|
+
`stop it with bash_kill({ service: "${service.id}" }).`);
|
|
108
|
+
const body = lines.join('\n');
|
|
109
|
+
// A service that DIED is an error result, so the model treats it as one: this
|
|
110
|
+
// is the failure the user actually cares about, reported in 400 ms instead of
|
|
111
|
+
// three minutes of stall (G1).
|
|
112
|
+
return service.status === 'exited' && service.exitCode !== 0
|
|
113
|
+
? errorResult(body)
|
|
114
|
+
: service.status === 'failed'
|
|
115
|
+
? errorResult(body)
|
|
116
|
+
: textResult(body);
|
|
42
117
|
}
|
|
43
|
-
export function makeBash(deps) {
|
|
118
|
+
export function makeBash(deps, options = {}) {
|
|
119
|
+
const supervisor = options.supervisor;
|
|
120
|
+
const backgroundAvailable = supervisor !== undefined;
|
|
44
121
|
return defineTool({
|
|
45
122
|
name: 'bash',
|
|
46
123
|
label: 'Shell',
|
|
47
124
|
description: 'Run a shell command at full permission and return its combined ' +
|
|
48
125
|
'stdout+stderr and exit code. `timeout` (ms) may only shrink within the ' +
|
|
49
126
|
'agent tool-timeout ceiling; `cwd` defaults to the working directory. ' +
|
|
50
|
-
'Emit shell syntax compatible with the OS/shell stated in the system prompt.'
|
|
127
|
+
'Emit shell syntax compatible with the OS/shell stated in the system prompt.' +
|
|
128
|
+
(backgroundAvailable ? BACKGROUND_DESCRIPTION : ''),
|
|
51
129
|
parameters: {
|
|
52
130
|
type: 'object',
|
|
53
131
|
properties: {
|
|
54
132
|
command: { type: 'string', description: 'The shell command to run.' },
|
|
55
133
|
timeout: { type: 'number', description: 'Optional timeout in milliseconds (shrinks within the ceiling).' },
|
|
56
134
|
cwd: { type: 'string', description: 'Working directory for this command.' },
|
|
135
|
+
// SPREAD CONDITIONALLY. With the feature off the schema is the object it
|
|
136
|
+
// is today, which is what `buildSystemPrompt`'s byte-identity claim (I-2)
|
|
137
|
+
// is measured against.
|
|
138
|
+
...(backgroundAvailable
|
|
139
|
+
? {
|
|
140
|
+
background: {
|
|
141
|
+
type: 'boolean',
|
|
142
|
+
description: 'true = supervise this command and return immediately with a ' +
|
|
143
|
+
'service id. false = never background it. Omit to let the ' +
|
|
144
|
+
'runtime decide from the command.',
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
: {}),
|
|
57
148
|
},
|
|
58
149
|
required: ['command'],
|
|
59
150
|
},
|
|
@@ -63,6 +154,25 @@ export function makeBash(deps) {
|
|
|
63
154
|
const params = rawParams;
|
|
64
155
|
const command = params.command;
|
|
65
156
|
const cwd = params.cwd ? params.cwd : deps.getCwd();
|
|
157
|
+
// --- The three-valued background decision (D-2 / §3.5) ----------------
|
|
158
|
+
//
|
|
159
|
+
// `true` -> background, always.
|
|
160
|
+
// `false` -> foreground, always. The escape hatch; nothing overrides it.
|
|
161
|
+
// absent -> the classifier, and only where a card can be rendered.
|
|
162
|
+
if (supervisor && params.background !== false) {
|
|
163
|
+
const auto = options.interactive !== false &&
|
|
164
|
+
options.autoBackground?.() !== false &&
|
|
165
|
+
looksLongRunning(command);
|
|
166
|
+
if (params.background === true || auto) {
|
|
167
|
+
return runBackground(supervisor, {
|
|
168
|
+
command,
|
|
169
|
+
cwd,
|
|
170
|
+
toolCallId: id,
|
|
171
|
+
settleMs: options.startupSettleMs?.() ?? FALLBACK_STARTUP_SETTLE_MS,
|
|
172
|
+
ephemeral: options.interactive === false,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
66
176
|
return new Promise((resolvePromise) => {
|
|
67
177
|
let child;
|
|
68
178
|
try {
|
|
@@ -74,40 +184,144 @@ export function makeBash(deps) {
|
|
|
74
184
|
cwd,
|
|
75
185
|
env: process.env,
|
|
76
186
|
windowsHide: true,
|
|
187
|
+
// W1.2 — NO UNANSWERABLE READ. `spawn` defaults every descriptor to
|
|
188
|
+
// a pipe, so a command that prompts blocks on a read nobody will
|
|
189
|
+
// ever satisfy and the promise never settles. `'ignore'` turns that
|
|
190
|
+
// into an EOF the command can react to.
|
|
191
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
77
192
|
});
|
|
78
193
|
}
|
|
79
194
|
catch (err) {
|
|
80
195
|
resolvePromise(errorResult(err instanceof Error ? err.message : String(err)));
|
|
81
196
|
return;
|
|
82
197
|
}
|
|
198
|
+
const releaseForeground = trackForeground(supervisor, child.pid);
|
|
83
199
|
let output = '';
|
|
84
|
-
let killedByTimeout = false;
|
|
85
200
|
let settled = false;
|
|
86
|
-
|
|
201
|
+
let drainTimer;
|
|
202
|
+
let graceTimer;
|
|
203
|
+
let selfTimer;
|
|
204
|
+
/**
|
|
205
|
+
* WHY the child is being killed, when it is.
|
|
206
|
+
*
|
|
207
|
+
* THE FLAG OUTRANKS `'close'`, AND THAT ORDERING IS THE WHOLE POINT.
|
|
208
|
+
* Killing a tree usually DOES produce a prompt `'close'`, so a `'close'`
|
|
209
|
+
* handler that rendered the ordinary `[signal SIGTERM]` body would win
|
|
210
|
+
* the race in the common case and the honest footer would only ever be
|
|
211
|
+
* seen in the rare one - the model would be told "the command was
|
|
212
|
+
* signalled" instead of "the command was still running when the ceiling
|
|
213
|
+
* fired, re-run it in the background", which is precisely the sentence
|
|
214
|
+
* that stops it reaching for `Start-Process`. The pre-change build made
|
|
215
|
+
* the same choice for its `killedByTimeout` flag, for the same reason.
|
|
216
|
+
*/
|
|
217
|
+
let killReason;
|
|
218
|
+
/**
|
|
219
|
+
* The result body, from whatever is known at settle time.
|
|
220
|
+
*
|
|
221
|
+
* ONE BUILDER FOR ALL FIVE SETTLE PATHS, so a footer can never depend on
|
|
222
|
+
* WHICH of them got there first - the property that makes the abort and
|
|
223
|
+
* timeout wordings deterministic rather than a race.
|
|
224
|
+
*/
|
|
225
|
+
const buildBody = (opts) => {
|
|
226
|
+
const body = captured();
|
|
227
|
+
if (killReason === 'timeout') {
|
|
228
|
+
// BYTE-IDENTICAL TO THE PRE-CHANGE BUILD for the `timeout` param.
|
|
229
|
+
return errorResult(`Command timed out after ${params.timeout}ms.\n${body}`);
|
|
230
|
+
}
|
|
231
|
+
if (killReason === 'abort') {
|
|
232
|
+
// P1-5. The captured output is the evidence the model needs, and
|
|
233
|
+
// `ctx.abortCause` is what separates "the ceiling fired" (which earns
|
|
234
|
+
// the advice) from "the user pressed Esc" (which does not - they did
|
|
235
|
+
// not ask for a workaround).
|
|
236
|
+
const footer = ctx.abortCause === 'timeout'
|
|
237
|
+
? '[aborted - the command was still running]\n' +
|
|
238
|
+
'It does not look like this command terminates on its own. ' +
|
|
239
|
+
'Re-run it with `background: true` if it is a server or a watcher.'
|
|
240
|
+
: '[aborted]';
|
|
241
|
+
return errorResult(`$ ${command}\n${body}\n${footer}`);
|
|
242
|
+
}
|
|
243
|
+
const status = opts.code === null ? `signal ${opts.signalName}` : `exit code ${opts.code}`;
|
|
244
|
+
const note = opts.note
|
|
245
|
+
? '[note] the command exited but a background child is still holding its ' +
|
|
246
|
+
'output stream.\n'
|
|
247
|
+
: '';
|
|
248
|
+
const text = `$ ${command}\n${body}\n${note}[${status}]`;
|
|
249
|
+
return opts.code === 0 ? textResult(text) : errorResult(text);
|
|
250
|
+
};
|
|
251
|
+
/**
|
|
252
|
+
* THE ONE FUNNEL. Idempotent, clears every timer, removes the abort
|
|
253
|
+
* listener and releases the foreground registration. Five call sites;
|
|
254
|
+
* see the module header for which ways of hanging each one closes.
|
|
255
|
+
*/
|
|
256
|
+
const settle = (build) => {
|
|
87
257
|
if (settled)
|
|
88
258
|
return;
|
|
89
259
|
settled = true;
|
|
90
|
-
|
|
91
|
-
|
|
260
|
+
if (drainTimer)
|
|
261
|
+
clearTimeout(drainTimer);
|
|
262
|
+
if (graceTimer)
|
|
263
|
+
clearTimeout(graceTimer);
|
|
264
|
+
if (selfTimer)
|
|
265
|
+
clearTimeout(selfTimer);
|
|
266
|
+
ctx.signal?.removeEventListener('abort', onAbort);
|
|
267
|
+
releaseForeground();
|
|
268
|
+
resolvePromise(build());
|
|
269
|
+
};
|
|
270
|
+
const captured = () => (output.length > 0 ? output : '(no output)');
|
|
271
|
+
/**
|
|
272
|
+
* Arm the post-kill force-settle. AT MOST ONCE.
|
|
273
|
+
*
|
|
274
|
+
* Both kill paths reach it and the executor's ceiling aborts through
|
|
275
|
+
* `ctx.signal` a moment after the tool's own timer fires, so a plain
|
|
276
|
+
* assignment would overwrite a pending timer with a second one — and
|
|
277
|
+
* `settle` clears only the reference it can see, leaving the orphan to
|
|
278
|
+
* hold the loop for another `killGraceMs` after the promise resolved.
|
|
279
|
+
*/
|
|
280
|
+
const armGrace = () => {
|
|
281
|
+
if (graceTimer)
|
|
282
|
+
return;
|
|
283
|
+
graceTimer = setTimeout(() => {
|
|
284
|
+
settle(() => buildBody({ code: null, signalName: null }));
|
|
285
|
+
}, PROC_LIMITS.killGraceMs);
|
|
92
286
|
};
|
|
93
287
|
// Self-timer for the `timeout` param (shrinks within the executor ceiling).
|
|
94
|
-
let timer;
|
|
95
288
|
if (params.timeout && params.timeout > 0) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
killTree(child);
|
|
289
|
+
selfTimer = setTimeout(() => {
|
|
290
|
+
killReason = 'timeout';
|
|
291
|
+
killTree(child.pid, 'SIGKILL');
|
|
292
|
+
// THE SAME GRACE THE ABORT PATH USES. Killing is not settling: a
|
|
293
|
+
// detached grandchild can hold the pipe open past the kill, and the
|
|
294
|
+
// old build then waited for a `'close'` that never came.
|
|
295
|
+
armGrace();
|
|
99
296
|
}, params.timeout);
|
|
100
297
|
}
|
|
101
|
-
|
|
298
|
+
/**
|
|
299
|
+
* Cooperative cancellation via the executor/agent AbortSignal.
|
|
300
|
+
*
|
|
301
|
+
* THE FORCE-SETTLED BODY CARRIES THE CAPTURED OUTPUT AND NAMES THE CAUSE
|
|
302
|
+
* (P1-5), and that is not a nicety. Today an abort kills the tree,
|
|
303
|
+
* `'close'` fires, and the model receives `$ cmd`, everything printed,
|
|
304
|
+
* and `[signal SIGTERM]`. The executor's own ceiling
|
|
305
|
+
* (`DEFAULT_TOOL_TIMEOUT_MS`) aborts through this SAME signal — so a bare
|
|
306
|
+
* `Command aborted.` would hand the model three words and no evidence at
|
|
307
|
+
* exactly the moment it needs to learn that the command does not
|
|
308
|
+
* terminate, which is the evidence that makes it reach for
|
|
309
|
+
* `background: true` instead of `Start-Process`.
|
|
310
|
+
*
|
|
311
|
+
* `ctx.abortCause` is what separates the two: `'timeout'` earns the
|
|
312
|
+
* advice, `'external'` (an Esc) does not — the user did not ask the model
|
|
313
|
+
* for a workaround.
|
|
314
|
+
*/
|
|
102
315
|
const onAbort = () => {
|
|
103
|
-
|
|
316
|
+
// NOT OVERWRITTEN when a `timeout` kill is already in force: the
|
|
317
|
+
// executor's ceiling aborts through this same signal a moment after
|
|
318
|
+
// the tool's own timer fired, and the `timeout` wording is the more
|
|
319
|
+
// specific of the two.
|
|
320
|
+
killReason ??= 'abort';
|
|
321
|
+
killTree(child.pid, 'SIGKILL');
|
|
322
|
+
armGrace();
|
|
104
323
|
};
|
|
105
324
|
ctx.signal?.addEventListener('abort', onAbort, { once: true });
|
|
106
|
-
function cleanup() {
|
|
107
|
-
if (timer)
|
|
108
|
-
clearTimeout(timer);
|
|
109
|
-
ctx.signal?.removeEventListener('abort', onAbort);
|
|
110
|
-
}
|
|
111
325
|
// `output` IS UNTOUCHED BY THE RECORDER, and that is what makes AC-30
|
|
112
326
|
// checkable by comparison rather than by inspection: the string the
|
|
113
327
|
// model receives, its truncation and the `[exit code N]` footer are all
|
|
@@ -129,19 +343,75 @@ export function makeBash(deps) {
|
|
|
129
343
|
deps.recordOutput?.(id, s);
|
|
130
344
|
});
|
|
131
345
|
child.on('error', (err) => {
|
|
132
|
-
|
|
346
|
+
settle(() => errorResult(`Failed to run command: ${err.message}`));
|
|
133
347
|
});
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
348
|
+
/**
|
|
349
|
+
* DEFENCE 1. `'exit'` fires when the PROCESS ends; `'close'` waits for
|
|
350
|
+
* every stdio stream, which a detached grandchild can hold open forever.
|
|
351
|
+
* So `'exit'` starts a short drain and `'close'` — if it arrives — wins
|
|
352
|
+
* the race and produces the byte-identical happy-path body.
|
|
353
|
+
*/
|
|
354
|
+
child.on('exit', (code, signalName) => {
|
|
355
|
+
if (settled || drainTimer)
|
|
138
356
|
return;
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
357
|
+
drainTimer = setTimeout(() => {
|
|
358
|
+
settle(() => buildBody({ code, signalName, note: true }));
|
|
359
|
+
}, PROC_LIMITS.drainMs);
|
|
360
|
+
});
|
|
361
|
+
child.on('close', (code, signalName) => {
|
|
362
|
+
settle(() => buildBody({ code, signalName }));
|
|
143
363
|
});
|
|
144
364
|
});
|
|
145
365
|
},
|
|
146
366
|
});
|
|
147
367
|
}
|
|
368
|
+
/**
|
|
369
|
+
* Start a supervised service and return as soon as there is something true to
|
|
370
|
+
* say (D-3).
|
|
371
|
+
*
|
|
372
|
+
* THE FIRST OF THREE: ready, exit, or `startupSettleMs`. Never later. A 400 ms
|
|
373
|
+
* crash is reported in 400 ms with the stack in the tool result, and a slow
|
|
374
|
+
* server never costs more than the settle window of turn time.
|
|
375
|
+
*
|
|
376
|
+
* IT DOES NOT CALL `deps.recordOutput` (P2-9). The tool call settles within the
|
|
377
|
+
* window and `tool_execution_end` then clears the live-output slot, so a tail
|
|
378
|
+
* written there would flash and vanish; the `ServiceCard` owns this service's
|
|
379
|
+
* output for its whole life and is the only surface that can keep it.
|
|
380
|
+
*/
|
|
381
|
+
async function runBackground(supervisor, args) {
|
|
382
|
+
const started = await supervisor.start({
|
|
383
|
+
command: args.command,
|
|
384
|
+
cwd: args.cwd,
|
|
385
|
+
toolCallId: args.toolCallId,
|
|
386
|
+
});
|
|
387
|
+
if (!started.ok || !started.service) {
|
|
388
|
+
return errorResult(started.error ?? 'Could not start the background service.');
|
|
389
|
+
}
|
|
390
|
+
const id = started.service.id;
|
|
391
|
+
const settled = await new Promise((resolve) => {
|
|
392
|
+
let done = false;
|
|
393
|
+
const finish = (snapshot) => {
|
|
394
|
+
if (done)
|
|
395
|
+
return;
|
|
396
|
+
done = true;
|
|
397
|
+
unsubscribe();
|
|
398
|
+
clearTimeout(timer);
|
|
399
|
+
resolve(snapshot);
|
|
400
|
+
};
|
|
401
|
+
const unsubscribe = supervisor.subscribe((event) => {
|
|
402
|
+
if (event.service.id !== id)
|
|
403
|
+
return;
|
|
404
|
+
if (event.type === 'ready' || event.type === 'exited' || event.type === 'stopped') {
|
|
405
|
+
finish(event.service);
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
const timer = setTimeout(() => finish(supervisor.get(id) ?? started.service), args.settleMs);
|
|
409
|
+
timer.unref?.();
|
|
410
|
+
// A service that finished between `start()` and this subscription (a crash
|
|
411
|
+
// inside 1 ms is not hypothetical) would otherwise wait out the whole window.
|
|
412
|
+
const now = supervisor.get(id);
|
|
413
|
+
if (now && now.status !== 'starting')
|
|
414
|
+
finish(now);
|
|
415
|
+
});
|
|
416
|
+
return renderServiceResult(settled, { ephemeral: args.ephemeral });
|
|
417
|
+
}
|
package/dist/tools/fs-tools.d.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { type AgentTool } from '@aragon-agent/core';
|
|
10
10
|
import { type FilePatch } from './patch.js';
|
|
11
|
+
import type { ProcSupervisorPort } from '../proc/types.js';
|
|
11
12
|
export interface ToolDeps {
|
|
12
13
|
getCwd: () => string;
|
|
13
14
|
/**
|
|
@@ -35,6 +36,22 @@ export interface ToolDeps {
|
|
|
35
36
|
* name, so a future long-running tool costs one call site.
|
|
36
37
|
*/
|
|
37
38
|
recordOutput?: (toolCallId: string, chunk: string) => void;
|
|
39
|
+
/**
|
|
40
|
+
* The process supervisor, when background launches are on for this session
|
|
41
|
+
* (background-service-supervision §3.5).
|
|
42
|
+
*
|
|
43
|
+
* THE TWO-INTERFACE RULE ABOVE APPLIES VERBATIM, and this package has already
|
|
44
|
+
* made that mistake twice (`recordChange` in round 1, `recordOutput` in round
|
|
45
|
+
* 2): adding the field HERE alone type-checks and forwards nothing, so `bash`
|
|
46
|
+
* never sees a supervisor, every long-running command runs in the foreground
|
|
47
|
+
* exactly as it does today, and NOTHING ANYWHERE ERRORS. It must be added here
|
|
48
|
+
* AND forwarded in `tools/index.ts`.
|
|
49
|
+
*
|
|
50
|
+
* OPTIONAL: omitting it leaves `bash` byte-identical to the pre-feature build —
|
|
51
|
+
* no `background` property in its schema, no supervisor call, and the
|
|
52
|
+
* pre-feature description string (I-2).
|
|
53
|
+
*/
|
|
54
|
+
procs?: ProcSupervisorPort;
|
|
38
55
|
}
|
|
39
56
|
export declare function makeReadFile(deps: ToolDeps): AgentTool;
|
|
40
57
|
export declare function makeWriteFile(deps: ToolDeps): AgentTool;
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { type AgentTool, type ToolPolicyDecision, type ToolPolicyVerdict } from
|
|
|
12
12
|
import type { AgentMode } from '../agent/agent-mode.js';
|
|
13
13
|
import type { ToolPermission } from '../exec/permission.js';
|
|
14
14
|
import type { FilePatch } from './patch.js';
|
|
15
|
+
import type { ProcSupervisorPort } from '../proc/types.js';
|
|
15
16
|
export interface ConfirmRequest {
|
|
16
17
|
tool: string;
|
|
17
18
|
summary: string;
|
|
@@ -151,6 +152,58 @@ export interface BuiltinToolsOptions {
|
|
|
151
152
|
* `--no-skills` is.
|
|
152
153
|
*/
|
|
153
154
|
teamTools?: AgentTool[];
|
|
155
|
+
/**
|
|
156
|
+
* The process supervisor (background-service-supervision §3.5).
|
|
157
|
+
*
|
|
158
|
+
* THE TWO-INTERFACE RULE: it is declared on `ToolDeps` as well, and adding it
|
|
159
|
+
* to only one of the two type-checks and forwards nothing — `bash` never sees
|
|
160
|
+
* a supervisor and every long-running command keeps hanging, with nothing
|
|
161
|
+
* anywhere erroring. Both, or neither.
|
|
162
|
+
*
|
|
163
|
+
* The conditional spread below is what makes I-2 true at this layer: with
|
|
164
|
+
* background services off the controller passes no key, so no property is
|
|
165
|
+
* added and `deps` is the object it is today.
|
|
166
|
+
*/
|
|
167
|
+
procs?: ProcSupervisorPort;
|
|
168
|
+
/**
|
|
169
|
+
* `bash_output` / `bash_kill`, appended verbatim.
|
|
170
|
+
*
|
|
171
|
+
* THROUGH THE FACTORY FOR THE REASON `teamTools` AND `todoTools` EACH RECORD
|
|
172
|
+
* IN CAPITALS (P0-2): `tools.test.ts::C7` asserts `HOST_TOOL_NAMES` equals
|
|
173
|
+
* what this factory PRODUCES, so appending these two in `AgentController`
|
|
174
|
+
* after the factory returned would turn C7 red with a message about two lists
|
|
175
|
+
* of names that says nothing about background services.
|
|
176
|
+
*
|
|
177
|
+
* An empty array (`bash.background: false`) leaves the tool list
|
|
178
|
+
* byte-identical to the pre-feature build — I-2, provable by object identity
|
|
179
|
+
* exactly as `--no-skills` and `--no-team` are.
|
|
180
|
+
*/
|
|
181
|
+
procTools?: AgentTool[];
|
|
182
|
+
/**
|
|
183
|
+
* `cfg.bash.autoBackground`, READ AT CALL TIME.
|
|
184
|
+
*
|
|
185
|
+
* Never cached: `aragon config set bash.autoBackground false` takes effect in
|
|
186
|
+
* the running session, and a cached `true` would go on backgrounding commands
|
|
187
|
+
* the user just told it not to.
|
|
188
|
+
*/
|
|
189
|
+
autoBackground?: () => boolean;
|
|
190
|
+
/** `cfg.bash.startupSettleMs`, read at call time for the same reason. */
|
|
191
|
+
startupSettleMs?: () => number;
|
|
192
|
+
/**
|
|
193
|
+
* Whether this session can render a service card and keep a service alive
|
|
194
|
+
* (D-15 / P1-8).
|
|
195
|
+
*
|
|
196
|
+
* FALSE UNDER `aragon exec` AND `-p`. It suppresses only the CLASSIFIER: an
|
|
197
|
+
* explicit `background: true` is still honoured, and its result then says the
|
|
198
|
+
* service dies with the run. Without it `aragon -p "npm run dev"` would
|
|
199
|
+
* background a server, return in `startupSettleMs`, and reap it at process
|
|
200
|
+
* exit with nothing anywhere saying so.
|
|
201
|
+
*
|
|
202
|
+
* It is the SAME SIGNAL `planTools` already uses to decide that no overlay can
|
|
203
|
+
* be rendered, so this costs one boolean rather than a new capability probe.
|
|
204
|
+
* Absent means interactive, which is what every existing caller is.
|
|
205
|
+
*/
|
|
206
|
+
interactive?: boolean;
|
|
154
207
|
/**
|
|
155
208
|
* Tool names that skip the CEILING wrapper only (never the plan gate, never
|
|
156
209
|
* confirmation).
|
|
@@ -199,7 +252,7 @@ export interface BuiltinToolsOptions {
|
|
|
199
252
|
* is the whole of what this mechanism is worth, and saying so plainly beats
|
|
200
253
|
* implying it covers more.
|
|
201
254
|
*/
|
|
202
|
-
export declare const SKILL_TOOL_FLOOR: readonly ["read_file", "list_dir", "glob", "grep", "skill", "skill_find", "ask_user", "submit_plan", "task", "todo_write"];
|
|
255
|
+
export declare const SKILL_TOOL_FLOOR: readonly ["read_file", "list_dir", "glob", "grep", "skill", "skill_find", "ask_user", "submit_plan", "task", "todo_write", "bash_output"];
|
|
203
256
|
/**
|
|
204
257
|
* The five tools plan mode refuses: exactly the complement of `SKILL_TOOL_FLOOR`
|
|
205
258
|
* within `HOST_TOOL_NAMES` — "the five that change something", the set this file
|
|
@@ -232,5 +285,5 @@ export declare const PLAN_MODE_BLOCKED_TOOLS: ReadonlySet<string>;
|
|
|
232
285
|
* declaration MEANS, and that reading must not change with how the CLI was
|
|
233
286
|
* started.
|
|
234
287
|
*/
|
|
235
|
-
export declare const HOST_TOOL_NAMES: readonly ["read_file", "write_file", "edit_file", "list_dir", "glob", "grep", "bash", "skill", "skill_find", "skill_install", "skill_create", "ask_user", "submit_plan", "todo_write", "task"];
|
|
288
|
+
export declare const HOST_TOOL_NAMES: readonly ["read_file", "write_file", "edit_file", "list_dir", "glob", "grep", "bash", "skill", "skill_find", "skill_install", "skill_create", "ask_user", "submit_plan", "todo_write", "bash_output", "bash_kill", "task"];
|
|
236
289
|
export declare function createBuiltinTools(options: BuiltinToolsOptions): AgentTool[];
|
package/dist/tools/index.js
CHANGED
|
@@ -59,6 +59,12 @@ export const SKILL_TOOL_FLOOR = [
|
|
|
59
59
|
// inside that skill's frame — the identical dead end that put `ask_user` /
|
|
60
60
|
// `submit_plan` / `task` here.
|
|
61
61
|
'todo_write',
|
|
62
|
+
// `bash_output` joins the FLOOR rather than the blocked set (D-12), and the
|
|
63
|
+
// partition test below is what forces that choice to be conscious. It READS A
|
|
64
|
+
// BUFFER: it cannot write to disk or run a shell, which is the floor's stated
|
|
65
|
+
// membership test. Blocking it in plan mode would also let the agent start a
|
|
66
|
+
// service it then could not read — the dead end the floor exists to prevent.
|
|
67
|
+
'bash_output',
|
|
62
68
|
];
|
|
63
69
|
/**
|
|
64
70
|
* The five tools plan mode refuses: exactly the complement of `SKILL_TOOL_FLOOR`
|
|
@@ -77,6 +83,11 @@ export const PLAN_MODE_BLOCKED_TOOLS = new Set([
|
|
|
77
83
|
'bash',
|
|
78
84
|
'skill_install',
|
|
79
85
|
'skill_create',
|
|
86
|
+
// `bash_kill` joins the BLOCKED set (D-12). It TERMINATES A PROCESS, which is
|
|
87
|
+
// a mutation of the world and the complement's stated membership test. Nothing
|
|
88
|
+
// is dead-ended by it: `Ctrl+C` and `/bg stop` both remain available to the
|
|
89
|
+
// USER in plan mode, so the person can always stop what the agent started.
|
|
90
|
+
'bash_kill',
|
|
80
91
|
]);
|
|
81
92
|
/**
|
|
82
93
|
* Every tool name this host CAN register (7 built-ins + 4 skill tools).
|
|
@@ -113,6 +124,13 @@ export const HOST_TOOL_NAMES = [
|
|
|
113
124
|
'ask_user',
|
|
114
125
|
'submit_plan',
|
|
115
126
|
'todo_write',
|
|
127
|
+
// Both names are here even though `bash.background: false` registers NEITHER,
|
|
128
|
+
// and that is the constant's documented meaning rather than an inconsistency:
|
|
129
|
+
// it is the SUPERSET of what this host CAN register, answering "can a skill's
|
|
130
|
+
// `allowed-tools` declaration ever take effect?" — the same relationship
|
|
131
|
+
// `skills doctor` already has with `--no-skills`.
|
|
132
|
+
'bash_output',
|
|
133
|
+
'bash_kill',
|
|
116
134
|
// `team_send` / `team_wait` are DELIBERATELY ABSENT (P2-5). This constant
|
|
117
135
|
// answers "can a skill's `allowed-tools` declaration ever take effect?", and
|
|
118
136
|
// those two are never registered on a lead — so a skill naming them genuinely
|
|
@@ -221,6 +239,10 @@ export function createBuiltinTools(options) {
|
|
|
221
239
|
...(options.recordChange ? { recordChange: options.recordChange } : {}),
|
|
222
240
|
// Added in the same shape and for the same reason (P1-1).
|
|
223
241
|
...(options.recordOutput ? { recordOutput: options.recordOutput } : {}),
|
|
242
|
+
// And the third, in the same shape and for the same reason. The FORWARD half
|
|
243
|
+
// of the two-interface note on `procs` above: without this line the field is
|
|
244
|
+
// on both interfaces, nothing errors, and `bash` still hangs.
|
|
245
|
+
...(options.procs ? { procs: options.procs } : {}),
|
|
224
246
|
};
|
|
225
247
|
let tools = [
|
|
226
248
|
makeReadFile(deps),
|
|
@@ -229,7 +251,18 @@ export function createBuiltinTools(options) {
|
|
|
229
251
|
makeListDir(deps),
|
|
230
252
|
makeGlob(deps),
|
|
231
253
|
makeGrep(deps),
|
|
232
|
-
|
|
254
|
+
// The second argument is spread CONDITIONALLY on the supervisor being
|
|
255
|
+
// present, so with background services off `makeBash` receives its default
|
|
256
|
+
// `{}` and produces the pre-feature tool: no `background` property in the
|
|
257
|
+
// schema, the pre-feature description string, no supervisor call (I-2).
|
|
258
|
+
makeBash(deps, options.procs
|
|
259
|
+
? {
|
|
260
|
+
supervisor: options.procs,
|
|
261
|
+
...(options.autoBackground ? { autoBackground: options.autoBackground } : {}),
|
|
262
|
+
...(options.startupSettleMs ? { startupSettleMs: options.startupSettleMs } : {}),
|
|
263
|
+
...(options.interactive !== undefined ? { interactive: options.interactive } : {}),
|
|
264
|
+
}
|
|
265
|
+
: {}),
|
|
233
266
|
];
|
|
234
267
|
if (options.confirmTools && options.confirm) {
|
|
235
268
|
const confirm = options.confirm;
|
|
@@ -246,6 +279,11 @@ export function createBuiltinTools(options) {
|
|
|
246
279
|
if (options.todoTools && options.todoTools.length > 0) {
|
|
247
280
|
tools = [...tools, ...options.todoTools];
|
|
248
281
|
}
|
|
282
|
+
// AFTER `todoTools`, BEFORE `teamTools`, so a lead's `task` stays last and
|
|
283
|
+
// `HOST_TOOL_NAMES` changes by two insertions rather than a reshuffle.
|
|
284
|
+
if (options.procTools && options.procTools.length > 0) {
|
|
285
|
+
tools = [...tools, ...options.procTools];
|
|
286
|
+
}
|
|
249
287
|
// BEFORE both wrappers, so the lead's `task` is covered by the plan gate and
|
|
250
288
|
// the ceiling like everything else, and a child's comm tools are covered by
|
|
251
289
|
// everything except the ceiling (see `policyExempt`).
|