@aragon-agent/cli 0.6.38 → 0.6.39
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 +2523 -2326
- package/README.md +3614 -3173
- package/dist/agent/controller.d.ts +142 -1
- package/dist/agent/controller.js +171 -3
- package/dist/agent/reducer.d.ts +41 -1
- package/dist/agent/reducer.js +25 -0
- package/dist/agent/system-prompt.d.ts +14 -0
- package/dist/agent/system-prompt.js +5 -0
- package/dist/cli.js +85 -27
- package/dist/commands/builtins.js +13 -1
- package/dist/commands/keys-command.d.ts +23 -0
- package/dist/commands/keys-command.js +38 -0
- package/dist/commands/registry.d.ts +39 -0
- package/dist/commands/task-command.d.ts +39 -0
- package/dist/commands/task-command.js +102 -0
- package/dist/config/cli-commands.d.ts +23 -0
- package/dist/config/cli-commands.js +36 -0
- package/dist/config/env.js +17 -4
- package/dist/config/load.js +6 -1
- package/dist/config/schema.d.ts +43 -4
- package/dist/config/schema.js +31 -5
- package/dist/config/store.js +17 -2
- package/dist/diagnostics/doctor.d.ts +9 -0
- package/dist/diagnostics/doctor.js +34 -0
- package/dist/diagnostics/info.d.ts +71 -0
- package/dist/diagnostics/info.js +91 -5
- package/dist/exec/bridge/caller-tools.d.ts +96 -0
- package/dist/exec/bridge/caller-tools.js +213 -0
- package/dist/exec/bridge/decisions.d.ts +78 -0
- package/dist/exec/bridge/decisions.js +116 -0
- package/dist/exec/bridge/human-answers.d.ts +38 -0
- package/dist/exec/bridge/human-answers.js +164 -0
- package/dist/exec/bridge/limits.d.ts +68 -0
- package/dist/exec/bridge/limits.js +68 -0
- package/dist/exec/bridge/ndjson.d.ts +17 -0
- package/dist/exec/bridge/ndjson.js +83 -0
- package/dist/exec/bridge/pending.d.ts +70 -0
- package/dist/exec/bridge/pending.js +132 -0
- package/dist/exec/bridge/protocol.d.ts +346 -0
- package/dist/exec/bridge/protocol.js +256 -0
- package/dist/exec/bridge/server.d.ts +142 -0
- package/dist/exec/bridge/server.js +502 -0
- package/dist/exec/bridge/source.d.ts +58 -0
- package/dist/exec/bridge/source.js +145 -0
- package/dist/exec/bridge/writer.d.ts +26 -0
- package/dist/exec/bridge/writer.js +156 -0
- package/dist/exec/cli-commands.js +30 -2
- package/dist/exec/emitter.d.ts +16 -2
- package/dist/exec/emitter.js +26 -6
- package/dist/exec/events.d.ts +465 -10
- package/dist/exec/events.js +47 -1
- package/dist/exec/hooks/dispatch.d.ts +76 -0
- package/dist/exec/hooks/dispatch.js +169 -0
- package/dist/exec/hooks/index.d.ts +83 -0
- package/dist/exec/hooks/index.js +205 -0
- package/dist/exec/hooks/limits.d.ts +56 -0
- package/dist/exec/hooks/limits.js +49 -0
- package/dist/exec/hooks/registry.d.ts +49 -0
- package/dist/exec/hooks/registry.js +91 -0
- package/dist/exec/hooks/tool-wrapper.d.ts +94 -0
- package/dist/exec/hooks/tool-wrapper.js +154 -0
- package/dist/exec/hooks/turn.d.ts +61 -0
- package/dist/exec/hooks/turn.js +59 -0
- package/dist/exec/hooks/validate.d.ts +85 -0
- package/dist/exec/hooks/validate.js +310 -0
- package/dist/exec/index.d.ts +130 -4
- package/dist/exec/index.js +501 -10
- package/dist/exec/options.d.ts +124 -2
- package/dist/exec/options.js +413 -6
- package/dist/exec/permission.d.ts +9 -1
- package/dist/exec/permission.js +7 -1
- package/dist/exec/request/limits.d.ts +51 -0
- package/dist/exec/request/limits.js +49 -0
- package/dist/exec/request/scope.d.ts +106 -0
- package/dist/exec/request/scope.js +149 -0
- package/dist/exec/result/limits.d.ts +74 -0
- package/dist/exec/result/limits.js +72 -0
- package/dist/exec/result/prompt.d.ts +28 -0
- package/dist/exec/result/prompt.js +74 -0
- package/dist/exec/result/schema.d.ts +99 -0
- package/dist/exec/result/schema.js +464 -0
- package/dist/exec/result/sink.d.ts +56 -0
- package/dist/exec/result/sink.js +50 -0
- package/dist/exec/result/tool.d.ts +37 -0
- package/dist/exec/result/tool.js +118 -0
- package/dist/exec/runner.d.ts +395 -1
- package/dist/exec/runner.js +860 -8
- package/dist/exec/threads/aggregate.d.ts +34 -0
- package/dist/exec/threads/aggregate.js +30 -0
- package/dist/exec/threads/capacity.d.ts +23 -0
- package/dist/exec/threads/capacity.js +38 -0
- package/dist/exec/threads/dispatch-source.d.ts +60 -0
- package/dist/exec/threads/dispatch-source.js +106 -0
- package/dist/exec/threads/execute-threads.d.ts +53 -0
- package/dist/exec/threads/execute-threads.js +234 -0
- package/dist/exec/threads/gate.d.ts +42 -0
- package/dist/exec/threads/gate.js +81 -0
- package/dist/exec/threads/identity.d.ts +36 -0
- package/dist/exec/threads/identity.js +67 -0
- package/dist/exec/threads/interrupt.d.ts +29 -0
- package/dist/exec/threads/interrupt.js +79 -0
- package/dist/exec/threads/limits.d.ts +58 -0
- package/dist/exec/threads/limits.js +35 -0
- package/dist/exec/threads/outcome.d.ts +47 -0
- package/dist/exec/threads/outcome.js +26 -0
- package/dist/exec/threads/registry.d.ts +81 -0
- package/dist/exec/threads/registry.js +427 -0
- package/dist/exec/threads/runtime.d.ts +107 -0
- package/dist/exec/threads/runtime.js +523 -0
- package/dist/exec/threads/thread-source.d.ts +20 -0
- package/dist/exec/threads/thread-source.js +134 -0
- package/dist/fast/limits.d.ts +54 -0
- package/dist/fast/limits.js +54 -0
- package/dist/fast/summary-call.d.ts +72 -0
- package/dist/fast/summary-call.js +133 -0
- package/dist/fast/summary-prompt.d.ts +106 -0
- package/dist/fast/summary-prompt.js +320 -0
- package/dist/fast/task-summary-types.d.ts +66 -0
- package/dist/fast/task-summary-types.js +17 -0
- package/dist/logging/logger.d.ts +1 -1
- package/dist/proc/limits.d.ts +7 -0
- package/dist/proc/limits.js +7 -0
- package/dist/proc/output-streams.d.ts +50 -0
- package/dist/proc/output-streams.js +54 -0
- package/dist/team/runtime.d.ts +9 -0
- package/dist/team/runtime.js +8 -0
- package/dist/team/subagent.d.ts +15 -0
- package/dist/team/subagent.js +45 -1
- package/dist/tools/bash-tool.d.ts +10 -0
- package/dist/tools/bash-tool.js +71 -6
- package/dist/tools/index.d.ts +93 -1
- package/dist/tools/index.js +112 -3
- package/dist/tools/tool-gate.d.ts +100 -0
- package/dist/tools/tool-gate.js +87 -0
- package/dist/ui/App.js +224 -11
- package/dist/ui/Header.d.ts +28 -16
- package/dist/ui/Header.js +21 -19
- package/dist/ui/interaction-hints.js +6 -0
- package/dist/ui/layout/OverlayFrame.d.ts +18 -0
- package/dist/ui/layout/OverlayFrame.js +19 -4
- package/dist/ui/layout/header-hints.d.ts +55 -62
- package/dist/ui/layout/header-hints.js +59 -166
- package/dist/ui/layout/header-left.d.ts +115 -0
- package/dist/ui/layout/header-left.js +206 -0
- package/dist/ui/layout/shortcut-panel-layout.d.ts +191 -0
- package/dist/ui/layout/shortcut-panel-layout.js +321 -0
- package/dist/ui/layout/status-detail-layout.js +5 -1
- package/dist/ui/layout/task-summary-cell.d.ts +61 -0
- package/dist/ui/layout/task-summary-cell.js +65 -0
- package/dist/ui/overlays/HelpOverlay.d.ts +12 -0
- package/dist/ui/overlays/HelpOverlay.js +29 -2
- package/dist/ui/overlays/ShortcutPanel.d.ts +39 -0
- package/dist/ui/overlays/ShortcutPanel.js +106 -0
- package/dist/ui/shortcuts.d.ts +81 -0
- package/dist/ui/shortcuts.js +221 -0
- package/package.json +2 -2
package/dist/team/subagent.js
CHANGED
|
@@ -14,7 +14,7 @@ import { Agent, createSkillFindTool, } from '@aragon-agent/core';
|
|
|
14
14
|
import { formatStreamError } from '../agent/reducer.js';
|
|
15
15
|
import { buildSystemPrompt } from '../agent/system-prompt.js';
|
|
16
16
|
import { buildFastBlock } from '../fast/prompt.js';
|
|
17
|
-
import { createBuiltinTools } from '../tools/index.js';
|
|
17
|
+
import { createBuiltinTools, gatedToolTimeouts, } from '../tools/index.js';
|
|
18
18
|
import { describeToolActivity, pickActivityArgs, sanitizeActivity } from './activity.js';
|
|
19
19
|
import { TEAM_LIMITS, TEAM_SUBAGENT_TOOL_NAMES } from './limits.js';
|
|
20
20
|
import { appendTimeline } from './timeline.js';
|
|
@@ -24,6 +24,26 @@ import { makeTeamSend, makeTeamWait, withMailboxTail } from './comm-tools.js';
|
|
|
24
24
|
import { buildSubagentBlock } from './prompt.js';
|
|
25
25
|
/** The default factory: a real `Agent` from `@aragon-agent/core`. */
|
|
26
26
|
export const defaultSubagentFactory = (config) => new Agent(config);
|
|
27
|
+
/**
|
|
28
|
+
* Hold a child's idle watchdog across a caller wait.
|
|
29
|
+
*
|
|
30
|
+
* The child's watchdog is its own (that is the point of a separate ceiling), so a
|
|
31
|
+
* four-minute approval would otherwise abort the child and take the dispatch with
|
|
32
|
+
* it - R-1, one level down. `finally` is what makes "paused" never outlive the
|
|
33
|
+
* wait, including on an abort.
|
|
34
|
+
*/
|
|
35
|
+
function pauseChildWatchdog(getAgent) {
|
|
36
|
+
return async (fn) => {
|
|
37
|
+
const agent = getAgent();
|
|
38
|
+
agent?.pauseIdleWatchdog();
|
|
39
|
+
try {
|
|
40
|
+
return await fn();
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
agent?.resumeIdleWatchdog();
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
27
47
|
/** The final assistant text of a turn, or `''` when the turn was tool calls only. */
|
|
28
48
|
function assistantText(message) {
|
|
29
49
|
if (!message || !Array.isArray(message.content))
|
|
@@ -59,6 +79,21 @@ export function buildSubagentTools(spec, deps, getAgent) {
|
|
|
59
79
|
const tools = createBuiltinTools({
|
|
60
80
|
getCwd: deps.getCwd,
|
|
61
81
|
confirmTools: deps.confirmTools,
|
|
82
|
+
// THE CHILD ASKS THE CALLER TOO (section 2.3.7). Without this the approval
|
|
83
|
+
// gate would hold for the lead and be bypassed by one `task` call - the hole
|
|
84
|
+
// the `--deny-tool` pass-through above records, one feature later and with a
|
|
85
|
+
// human on the other end of it who believes they are approving everything.
|
|
86
|
+
//
|
|
87
|
+
// The watchdog pause is the CHILD'S OWN, never `TeamHumanQueue`'s: that queue
|
|
88
|
+
// is `--confirm`'s, and nesting a second pause is exactly what
|
|
89
|
+
// `withPausedWatchdog` forbids. `getAgent()` is the same live handle the queue
|
|
90
|
+
// and `team_wait` already use.
|
|
91
|
+
...(deps.toolGateFor
|
|
92
|
+
? {
|
|
93
|
+
toolGate: deps.toolGateFor(spec.label),
|
|
94
|
+
withToolWait: pauseChildWatchdog(getAgent),
|
|
95
|
+
}
|
|
96
|
+
: {}),
|
|
62
97
|
// Queued through the ONE human slot, with this child's watchdog paused for
|
|
63
98
|
// the whole queue wait (§3.11 / I-4 / P0-4).
|
|
64
99
|
...(deps.confirmTools && deps.humanQueue
|
|
@@ -253,6 +288,15 @@ export function createSubagent(spec, deps, hooks) {
|
|
|
253
288
|
...(deps.config.maxTokens !== undefined ? { maxTokens: deps.config.maxTokens } : {}),
|
|
254
289
|
timeouts: {
|
|
255
290
|
toolTimeout: deps.config.toolTimeoutMs,
|
|
291
|
+
// THE SAME TWO PHASES THE LEAD GETS (section 2.3.4). A child that asks the
|
|
292
|
+
// caller and waits is billed for the wait out of its tool's budget unless
|
|
293
|
+
// this is here, and the symptom would be "the approved command was killed
|
|
294
|
+
// mid-run" only inside a fan-out.
|
|
295
|
+
...(deps.toolGateFor && deps.decisionBudgetMs !== undefined
|
|
296
|
+
? {
|
|
297
|
+
toolTimeoutOverrides: gatedToolTimeouts(deps.config.toolTimeoutMs, deps.decisionBudgetMs),
|
|
298
|
+
}
|
|
299
|
+
: {}),
|
|
256
300
|
// A child keeps its OWN idle watchdog, and that is a feature: a wedged
|
|
257
301
|
// child is aborted by its own ceiling without taking the dispatch with it
|
|
258
302
|
// (§3.8). The two places a child legitimately blocks on something other
|
|
@@ -19,12 +19,22 @@
|
|
|
19
19
|
* inheriting the pipe — `Start-Process`, `&`, `nohup`, and every
|
|
20
20
|
* `npm run dev` that re-execs — keeps that pipe open after the shell is
|
|
21
21
|
* long gone. The shell exited; the promise did not.
|
|
22
|
+
*
|
|
22
23
|
* 2. `stdio` defaulted to `'pipe'` for stdin, so a command that PROMPTS —
|
|
23
24
|
* `npm init`, a package manager asking to install something, `sudo`, a git
|
|
24
25
|
* credential prompt — blocked on a read that could never be satisfied.
|
|
25
26
|
* 3. An abort killed the tree and then went on waiting for a `'close'` that a
|
|
26
27
|
* detached grandchild was still holding.
|
|
27
28
|
*
|
|
29
|
+
* WHY THE DRAIN WINDOW IS A BOUND AND NOT A VERDICT. The first build called
|
|
30
|
+
* anything still unsettled when that window closed "a held pipe" and printed
|
|
31
|
+
* the `'exit'` drain note on the strength of it. Under load the two disagree:
|
|
32
|
+
* the release gate caught a command that had printed two lines and exited
|
|
33
|
+
* immediately being reported as holding its output stream, because the timers
|
|
34
|
+
* phase came round before libuv delivered the close notification. The note is a
|
|
35
|
+
* claim about STATE, so it is decided by asking the streams
|
|
36
|
+
* (`proc/output-streams.ts`) instead of guessing from a clock.
|
|
37
|
+
*
|
|
28
38
|
* The happy path is BYTE-IDENTICAL to the pre-change build: `$ cmd\n<output>\n
|
|
29
39
|
* [exit code N]`, same truncation, same footer. That is what keeps the existing
|
|
30
40
|
* `bash` assertions meaningful rather than rewritten.
|
package/dist/tools/bash-tool.js
CHANGED
|
@@ -19,12 +19,22 @@
|
|
|
19
19
|
* inheriting the pipe — `Start-Process`, `&`, `nohup`, and every
|
|
20
20
|
* `npm run dev` that re-execs — keeps that pipe open after the shell is
|
|
21
21
|
* long gone. The shell exited; the promise did not.
|
|
22
|
+
*
|
|
22
23
|
* 2. `stdio` defaulted to `'pipe'` for stdin, so a command that PROMPTS —
|
|
23
24
|
* `npm init`, a package manager asking to install something, `sudo`, a git
|
|
24
25
|
* credential prompt — blocked on a read that could never be satisfied.
|
|
25
26
|
* 3. An abort killed the tree and then went on waiting for a `'close'` that a
|
|
26
27
|
* detached grandchild was still holding.
|
|
27
28
|
*
|
|
29
|
+
* WHY THE DRAIN WINDOW IS A BOUND AND NOT A VERDICT. The first build called
|
|
30
|
+
* anything still unsettled when that window closed "a held pipe" and printed
|
|
31
|
+
* the `'exit'` drain note on the strength of it. Under load the two disagree:
|
|
32
|
+
* the release gate caught a command that had printed two lines and exited
|
|
33
|
+
* immediately being reported as holding its output stream, because the timers
|
|
34
|
+
* phase came round before libuv delivered the close notification. The note is a
|
|
35
|
+
* claim about STATE, so it is decided by asking the streams
|
|
36
|
+
* (`proc/output-streams.ts`) instead of guessing from a clock.
|
|
37
|
+
*
|
|
28
38
|
* The happy path is BYTE-IDENTICAL to the pre-change build: `$ cmd\n<output>\n
|
|
29
39
|
* [exit code N]`, same truncation, same footer. That is what keeps the existing
|
|
30
40
|
* `bash` assertions meaningful rather than rewritten.
|
|
@@ -35,6 +45,7 @@ import { defineTool, errorResult, textResult, } from '@aragon-agent/core';
|
|
|
35
45
|
import { looksLongRunning } from '../proc/classify.js';
|
|
36
46
|
import { killTree } from '../proc/kill-tree.js';
|
|
37
47
|
import { PROC_LIMITS } from '../proc/limits.js';
|
|
48
|
+
import { hasOpenOutputStreams } from '../proc/output-streams.js';
|
|
38
49
|
import { isTerminalStatus } from '../proc/types.js';
|
|
39
50
|
/**
|
|
40
51
|
* The two sentences appended to the tool description when background launches
|
|
@@ -285,6 +296,14 @@ export function makeBash(deps, options = {}) {
|
|
|
285
296
|
* the same choice for its `killedByTimeout` flag, for the same reason.
|
|
286
297
|
*/
|
|
287
298
|
let killReason;
|
|
299
|
+
/**
|
|
300
|
+
* The exit code, remembered the moment `'exit'` reports it.
|
|
301
|
+
*
|
|
302
|
+
* THREE PATHS SETTLE THE ORDINARY CASE NOW - the two streams ending, the
|
|
303
|
+
* `'close'` event, and the drain bound - and all three must build the same
|
|
304
|
+
* body, so none of them may depend on having been handed the code itself.
|
|
305
|
+
*/
|
|
306
|
+
let exitInfo = null;
|
|
288
307
|
/**
|
|
289
308
|
* The result body, from whatever is known at settle time.
|
|
290
309
|
*
|
|
@@ -342,6 +361,25 @@ export function makeBash(deps, options = {}) {
|
|
|
342
361
|
releaseForeground();
|
|
343
362
|
resolvePromise(build());
|
|
344
363
|
};
|
|
364
|
+
/** See `proc/output-streams.ts` for why this is asked and not assumed. */
|
|
365
|
+
const streamsStillOpen = () => hasOpenOutputStreams(child);
|
|
366
|
+
/**
|
|
367
|
+
* THE ORDINARY BODY, built from whatever the streams say at this instant.
|
|
368
|
+
*
|
|
369
|
+
* Without a held pipe it is byte-identical to the `'close'` path - no note -
|
|
370
|
+
* which is what AC-30 measures. With one it is the honest diagnosis, and the
|
|
371
|
+
* sentence is only ever printed while it is TRUE.
|
|
372
|
+
*/
|
|
373
|
+
const settleOrdinary = () => {
|
|
374
|
+
const info = exitInfo;
|
|
375
|
+
if (!info)
|
|
376
|
+
return;
|
|
377
|
+
settle(() => buildBody({
|
|
378
|
+
code: info.code,
|
|
379
|
+
signalName: info.signalName,
|
|
380
|
+
note: streamsStillOpen(),
|
|
381
|
+
}));
|
|
382
|
+
};
|
|
345
383
|
const captured = () => (output.length > 0 ? output : '(no output)');
|
|
346
384
|
/**
|
|
347
385
|
* Arm the post-kill force-settle. AT MOST ONCE.
|
|
@@ -421,16 +459,43 @@ export function makeBash(deps, options = {}) {
|
|
|
421
459
|
settle(() => errorResult(`Failed to run command: ${err.message}`));
|
|
422
460
|
});
|
|
423
461
|
/**
|
|
424
|
-
* DEFENCE 1. `'exit'` fires when the PROCESS ends; `'close'` waits for
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
462
|
+
* DEFENCE 1. `'exit'` fires when the PROCESS ends; `'close'` waits for every
|
|
463
|
+
* stdio stream, which a detached grandchild can hold open forever. So `'exit'`
|
|
464
|
+
* starts a short drain and `'close'` - if it arrives - wins the race and
|
|
465
|
+
* produces the byte-identical happy-path body.
|
|
466
|
+
*
|
|
467
|
+
* AND THE COMMON CASE NO LONGER WAITS FOR THAT RACE AT ALL. Two ended streams
|
|
468
|
+
* after `'exit'` mean not one more byte can arrive, so the body `'close'` would
|
|
469
|
+
* build is already known: settle it there. That removes the only window in which
|
|
470
|
+
* a busy event loop could hand the timer a verdict the pipes had already
|
|
471
|
+
* contradicted - the release-gate failure recorded in `proc/output-streams.ts`.
|
|
428
472
|
*/
|
|
473
|
+
const onStreamSettled = () => {
|
|
474
|
+
if (!exitInfo || settled)
|
|
475
|
+
return;
|
|
476
|
+
if (!streamsStillOpen())
|
|
477
|
+
settleOrdinary();
|
|
478
|
+
};
|
|
479
|
+
child.stdout?.on('end', onStreamSettled);
|
|
480
|
+
child.stderr?.on('end', onStreamSettled);
|
|
429
481
|
child.on('exit', (code, signalName) => {
|
|
430
|
-
|
|
482
|
+
exitInfo = { code, signalName };
|
|
483
|
+
if (settled)
|
|
484
|
+
return;
|
|
485
|
+
if (!streamsStillOpen()) {
|
|
486
|
+
settleOrdinary();
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (drainTimer)
|
|
431
490
|
return;
|
|
432
491
|
drainTimer = setTimeout(() => {
|
|
433
|
-
|
|
492
|
+
// DEFERRED PAST THE POLL PHASE, DELIBERATELY. Asking the streams from
|
|
493
|
+
// inside the timer callback would look at them before the loop has
|
|
494
|
+
// dispatched an `'end'`/`'close'` already queued behind it - which is
|
|
495
|
+
// precisely the starvation that produced the false note. The check phase
|
|
496
|
+
// runs after the poll phase of the same iteration, so by then the answer is
|
|
497
|
+
// observable rather than guessable.
|
|
498
|
+
setImmediate(settleOrdinary);
|
|
434
499
|
}, PROC_LIMITS.drainMs);
|
|
435
500
|
});
|
|
436
501
|
child.on('close', (code, signalName) => {
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ import type { ToolPermission } from '../exec/permission.js';
|
|
|
14
14
|
import type { FilePatch } from './patch.js';
|
|
15
15
|
import type { ProcSupervisorPort } from '../proc/types.js';
|
|
16
16
|
import type { JobNotice } from '../proc/notices.js';
|
|
17
|
+
import { type ToolGate } from './tool-gate.js';
|
|
18
|
+
import { type ToolHookRunner } from '../exec/hooks/tool-wrapper.js';
|
|
17
19
|
export interface ConfirmRequest {
|
|
18
20
|
tool: string;
|
|
19
21
|
summary: string;
|
|
@@ -191,6 +193,22 @@ export interface BuiltinToolsOptions {
|
|
|
191
193
|
* to the pre-feature build.
|
|
192
194
|
*/
|
|
193
195
|
jobTools?: AgentTool[];
|
|
196
|
+
/**
|
|
197
|
+
* `submit_result`, appended verbatim when the caller supplied `--result-schema`
|
|
198
|
+
* (headless-request-scope-and-typed-result section 2.3.9).
|
|
199
|
+
*
|
|
200
|
+
* THROUGH THE FACTORY FOR THE REASON `teamTools`, `todoTools`, `procTools` AND
|
|
201
|
+
* `jobTools` EACH RECORD IN CAPITALS: `tools.test.ts::C7` asserts `HOST_TOOL_NAMES`
|
|
202
|
+
* equals what this factory PRODUCES, so appending `submit_result` in
|
|
203
|
+
* `AgentController` after the factory returned would turn it red with a message
|
|
204
|
+
* about two lists of names that says nothing about structured results.
|
|
205
|
+
*
|
|
206
|
+
* ABSENT ON EVERY OTHER PATH - the TUI, `-p`, every subagent - which leaves their
|
|
207
|
+
* tool array byte-identical to the pre-feature build (I-S2). A subagent must never
|
|
208
|
+
* receive it: its product is a report that goes back through `task`, and a second
|
|
209
|
+
* submitter in one request would compete with the lead's schema.
|
|
210
|
+
*/
|
|
211
|
+
resultTool?: AgentTool;
|
|
194
212
|
/**
|
|
195
213
|
* The completion-notice queue's read side, for the PULL channel (§4.6).
|
|
196
214
|
*
|
|
@@ -260,7 +278,81 @@ export interface BuiltinToolsOptions {
|
|
|
260
278
|
* `ToolPermission.isAllowed` applies for itself.
|
|
261
279
|
*/
|
|
262
280
|
permission?: ToolPermission;
|
|
281
|
+
/**
|
|
282
|
+
* The per-call approval gate (headless-host-bridge §2.3.3). Bridge mode only.
|
|
283
|
+
*
|
|
284
|
+
* ABSENT EVERYWHERE ELSE, which is what keeps every other tool array identical
|
|
285
|
+
* by object identity: the mapping below runs only when this key exists, the same
|
|
286
|
+
* house rule `permission` and `jobNotices` follow.
|
|
287
|
+
*/
|
|
288
|
+
toolGate?: ToolGate;
|
|
289
|
+
/**
|
|
290
|
+
* The caller's lifecycle hooks, wrapping `tool_pre` / `tool_post`
|
|
291
|
+
* (headless-threads-and-lifecycle-hooks section 2.4.9).
|
|
292
|
+
*
|
|
293
|
+
* IT IS PRESENT WHENEVER THE HOOK CHANNEL IS OPEN, not only once a hook is registered:
|
|
294
|
+
* `hook_register` arrives AFTER the controller (and therefore after this factory) was
|
|
295
|
+
* built, so a wrapper installed lazily would never be installed at all. The wrappers
|
|
296
|
+
* read the table live and cost one `chainFor` when it is empty, which is why the
|
|
297
|
+
* observable behaviour with zero hooks is exactly today's.
|
|
298
|
+
*
|
|
299
|
+
* ABSENT FOR THE TUI, `-p`, every subagent and every non-bridge exec run - so their tool
|
|
300
|
+
* arrays stay byte-identical to the pre-feature build (I-H1's spirit, and `tools.test.ts`
|
|
301
|
+
* C7's letter).
|
|
302
|
+
*/
|
|
303
|
+
hookRunner?: ToolHookRunner;
|
|
304
|
+
/**
|
|
305
|
+
* The host's watchdog pause, handed to the gate wrapper.
|
|
306
|
+
*
|
|
307
|
+
* THE SAME FUNCTION the plan tools receive (`withPausedWatchdog`), not a second
|
|
308
|
+
* wrapper around it: that method is documented as NOT reentrant, and nesting two
|
|
309
|
+
* pauses would resume the watchdog one level too early.
|
|
310
|
+
*/
|
|
311
|
+
withToolWait?: <T>(fn: () => Promise<T>) => Promise<T>;
|
|
263
312
|
}
|
|
313
|
+
/**
|
|
314
|
+
* The filesystem-mutating set the `--confirm` wrapper shares.
|
|
315
|
+
*
|
|
316
|
+
* EXPORTED SO `CALLER_GATED_TOOLS` CAN EXTEND IT rather than restating it
|
|
317
|
+
* (headless-host-bridge §2.3.2). `exec/permission.ts` already records the rule:
|
|
318
|
+
* a second copy of a name list is a list that drifts, and the drift is discovered
|
|
319
|
+
* by the tool that is refused in one mode and allowed in the other.
|
|
320
|
+
*/
|
|
321
|
+
export declare const MUTATING_TOOLS: ReadonlySet<string>;
|
|
322
|
+
/**
|
|
323
|
+
* The names `--permission-mode ask` asks the caller about.
|
|
324
|
+
*
|
|
325
|
+
* `MUTATING_TOOLS` is the filesystem set the confirm wrapper already shares.
|
|
326
|
+
* `skill_install` is deliberately ABSENT: it already has its own gate
|
|
327
|
+
* (`ApprovalGate`, `exec/bridge/decisions.ts`) and asking twice reads as "no
|
|
328
|
+
* approval at all" - the same argument that makes `--confirm` and `ask` mutually
|
|
329
|
+
* exclusive in `exec/options.ts`. `skill_create` is PRESENT because it writes
|
|
330
|
+
* SKILL.md plus arbitrary bundled files and has no gate of any kind: without it,
|
|
331
|
+
* `ask` mode would let a headless run create files on disk that the caller never
|
|
332
|
+
* saw.
|
|
333
|
+
*
|
|
334
|
+
* ONE DEFINITION, THREE CONSUMERS: `ToolGate.applies`, the executor's two-phase
|
|
335
|
+
* timeout override in `agent/controller.ts`, and the acks and docs that report
|
|
336
|
+
* which tools are gated. That is `BRIDGE_LIMITS`-style discipline (I-6) for a name
|
|
337
|
+
* set instead of a number.
|
|
338
|
+
*/
|
|
339
|
+
export declare const CALLER_GATED_TOOLS: ReadonlySet<string>;
|
|
340
|
+
/**
|
|
341
|
+
* The executor timeout each gated tool needs while a human may be answering.
|
|
342
|
+
*
|
|
343
|
+
* THE TWO PHASES, ADDED (headless-host-bridge section 2.3.4). `ToolExecutor` arms
|
|
344
|
+
* its timer BEFORE the approval wrapper runs, so a ceiling of `toolTimeoutMs`
|
|
345
|
+
* alone would leave the tool less than its configured budget by exactly the time
|
|
346
|
+
* the caller took to answer. ADMITTED OVER-GRANT: a caller that answers instantly
|
|
347
|
+
* still gets the sum. The opposite trade - charging the wait to the tool - would
|
|
348
|
+
* make "the human thought for two minutes" read as "this command is too slow",
|
|
349
|
+
* and a tool timeout is supposed to mean the latter.
|
|
350
|
+
*
|
|
351
|
+
* IT IS A FUNCTION RATHER THAN A LINE IN THE CONTROLLER so the formula has a name
|
|
352
|
+
* and a test: the numbers here reach a place (`toolTimeoutOverrides`) that no
|
|
353
|
+
* assertion can read back, and an unreadable ceiling is one that quietly drifts.
|
|
354
|
+
*/
|
|
355
|
+
export declare function gatedToolTimeouts(toolTimeoutMs: number, decisionBudgetMs: number): Record<string, number>;
|
|
264
356
|
/**
|
|
265
357
|
* Tools the ceiling always permits (D-G6). The ONLY place that knows which of
|
|
266
358
|
* this host's tools are read-only — core must not (D-G7).
|
|
@@ -309,5 +401,5 @@ export declare const PLAN_MODE_BLOCKED_TOOLS: ReadonlySet<string>;
|
|
|
309
401
|
* declaration MEANS, and that reading must not change with how the CLI was
|
|
310
402
|
* started.
|
|
311
403
|
*/
|
|
312
|
-
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", "job_wait", "job_list", "task"];
|
|
404
|
+
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", "job_wait", "job_list", "submit_result", "task"];
|
|
313
405
|
export declare function createBuiltinTools(options: BuiltinToolsOptions): AgentTool[];
|
package/dist/tools/index.js
CHANGED
|
@@ -14,7 +14,57 @@ import { makeReadFile, makeWriteFile, makeEditFile, makeListDir } from './fs-too
|
|
|
14
14
|
import { makeGlob, makeGrep } from './search-tools.js';
|
|
15
15
|
import { makeBash } from './bash-tool.js';
|
|
16
16
|
import { withJobNoticeTail } from './job-tools.js';
|
|
17
|
-
|
|
17
|
+
import { withToolApproval } from './tool-gate.js';
|
|
18
|
+
// VALUE IMPORTS, AND THE DIRECTION IS SAFE: `exec/hooks/tool-wrapper.ts` imports only Core
|
|
19
|
+
// and its own siblings, so `tools/index.ts -> exec/hooks -> (nothing back here)` is not a
|
|
20
|
+
// cycle. The wrappers are plain `{...tool, execute}` objects, exactly like the four this
|
|
21
|
+
// file already composes.
|
|
22
|
+
import { withToolPostHooks, withToolPreHooks, } from '../exec/hooks/tool-wrapper.js';
|
|
23
|
+
/**
|
|
24
|
+
* The filesystem-mutating set the `--confirm` wrapper shares.
|
|
25
|
+
*
|
|
26
|
+
* EXPORTED SO `CALLER_GATED_TOOLS` CAN EXTEND IT rather than restating it
|
|
27
|
+
* (headless-host-bridge §2.3.2). `exec/permission.ts` already records the rule:
|
|
28
|
+
* a second copy of a name list is a list that drifts, and the drift is discovered
|
|
29
|
+
* by the tool that is refused in one mode and allowed in the other.
|
|
30
|
+
*/
|
|
31
|
+
export const MUTATING_TOOLS = new Set(['write_file', 'edit_file', 'bash']);
|
|
32
|
+
/**
|
|
33
|
+
* The names `--permission-mode ask` asks the caller about.
|
|
34
|
+
*
|
|
35
|
+
* `MUTATING_TOOLS` is the filesystem set the confirm wrapper already shares.
|
|
36
|
+
* `skill_install` is deliberately ABSENT: it already has its own gate
|
|
37
|
+
* (`ApprovalGate`, `exec/bridge/decisions.ts`) and asking twice reads as "no
|
|
38
|
+
* approval at all" - the same argument that makes `--confirm` and `ask` mutually
|
|
39
|
+
* exclusive in `exec/options.ts`. `skill_create` is PRESENT because it writes
|
|
40
|
+
* SKILL.md plus arbitrary bundled files and has no gate of any kind: without it,
|
|
41
|
+
* `ask` mode would let a headless run create files on disk that the caller never
|
|
42
|
+
* saw.
|
|
43
|
+
*
|
|
44
|
+
* ONE DEFINITION, THREE CONSUMERS: `ToolGate.applies`, the executor's two-phase
|
|
45
|
+
* timeout override in `agent/controller.ts`, and the acks and docs that report
|
|
46
|
+
* which tools are gated. That is `BRIDGE_LIMITS`-style discipline (I-6) for a name
|
|
47
|
+
* set instead of a number.
|
|
48
|
+
*/
|
|
49
|
+
export const CALLER_GATED_TOOLS = new Set([...MUTATING_TOOLS, 'skill_create']);
|
|
50
|
+
/**
|
|
51
|
+
* The executor timeout each gated tool needs while a human may be answering.
|
|
52
|
+
*
|
|
53
|
+
* THE TWO PHASES, ADDED (headless-host-bridge section 2.3.4). `ToolExecutor` arms
|
|
54
|
+
* its timer BEFORE the approval wrapper runs, so a ceiling of `toolTimeoutMs`
|
|
55
|
+
* alone would leave the tool less than its configured budget by exactly the time
|
|
56
|
+
* the caller took to answer. ADMITTED OVER-GRANT: a caller that answers instantly
|
|
57
|
+
* still gets the sum. The opposite trade - charging the wait to the tool - would
|
|
58
|
+
* make "the human thought for two minutes" read as "this command is too slow",
|
|
59
|
+
* and a tool timeout is supposed to mean the latter.
|
|
60
|
+
*
|
|
61
|
+
* IT IS A FUNCTION RATHER THAN A LINE IN THE CONTROLLER so the formula has a name
|
|
62
|
+
* and a test: the numbers here reach a place (`toolTimeoutOverrides`) that no
|
|
63
|
+
* assertion can read back, and an unreadable ceiling is one that quietly drifts.
|
|
64
|
+
*/
|
|
65
|
+
export function gatedToolTimeouts(toolTimeoutMs, decisionBudgetMs) {
|
|
66
|
+
return Object.fromEntries([...CALLER_GATED_TOOLS].map((name) => [name, toolTimeoutMs + decisionBudgetMs]));
|
|
67
|
+
}
|
|
18
68
|
/**
|
|
19
69
|
* Tools the ceiling always permits (D-G6). The ONLY place that knows which of
|
|
20
70
|
* this host's tools are read-only — core must not (D-G7).
|
|
@@ -146,6 +196,13 @@ export const HOST_TOOL_NAMES = [
|
|
|
146
196
|
// (long-task-jobs §4.3).
|
|
147
197
|
'job_wait',
|
|
148
198
|
'job_list',
|
|
199
|
+
// `submit_result` (headless-request-scope-and-typed-result section 2.3.9) belongs to
|
|
200
|
+
// the superset for the reason the two job names above do: a skill's
|
|
201
|
+
// `allowed-tools: [submit_result]` must be answerable, and `--allow-tool` /
|
|
202
|
+
// `--deny-tool` validate against THIS list. REGISTRATION is a separate decision -
|
|
203
|
+
// only `--result-schema` registers it, which is why `--deny-tool submit_result`
|
|
204
|
+
// without a schema is warned about rather than obeyed (`exec/options.ts` W-3).
|
|
205
|
+
'submit_result',
|
|
149
206
|
// `team_send` / `team_wait` are DELIBERATELY ABSENT (P2-5). This constant
|
|
150
207
|
// answers "can a skill's `allowed-tools` declaration ever take effect?", and
|
|
151
208
|
// those two are never registered on a lead — so a skill naming them genuinely
|
|
@@ -309,12 +366,53 @@ export function createBuiltinTools(options) {
|
|
|
309
366
|
if (options.jobTools && options.jobTools.length > 0) {
|
|
310
367
|
tools = [...tools, ...options.jobTools];
|
|
311
368
|
}
|
|
369
|
+
// AFTER `jobTools`, BEFORE `teamTools`: `submit_result` is the answer-shaped sibling
|
|
370
|
+
// of the plan tools rather than a team tool, and keeping a lead's `task` last means
|
|
371
|
+
// `HOST_TOOL_NAMES` grows by one insertion instead of a reshuffle
|
|
372
|
+
// (headless-request-scope-and-typed-result section 2.6 row 16).
|
|
373
|
+
//
|
|
374
|
+
// INSIDE THE FACTORY, NEVER APPENDED IN `AgentController` - `tools.test.ts::C7`
|
|
375
|
+
// compares `HOST_TOOL_NAMES` against what this function produces.
|
|
376
|
+
if (options.resultTool) {
|
|
377
|
+
tools = [...tools, options.resultTool];
|
|
378
|
+
}
|
|
312
379
|
// BEFORE both wrappers, so the lead's `task` is covered by the plan gate and
|
|
313
380
|
// the ceiling like everything else, and a child's comm tools are covered by
|
|
314
381
|
// everything except the ceiling (see `policyExempt`).
|
|
315
382
|
if (options.teamTools && options.teamTools.length > 0) {
|
|
316
383
|
tools = [...tools, ...options.teamTools];
|
|
317
384
|
}
|
|
385
|
+
// --- The caller's per-call approval gate (headless-host-bridge §2.3.3). ---
|
|
386
|
+
//
|
|
387
|
+
// AFTER THE LAST APPEND AND BEFORE THE PERMISSION FILTER, for the two structural
|
|
388
|
+
// reasons the `permission` block below records, plus one of its own: the appends
|
|
389
|
+
// above are what put `skill_create` in this array, and that is the single
|
|
390
|
+
// write-capable tool with no other gate. Placing this immediately after
|
|
391
|
+
// `withConfirmation` - where the array still holds only the seven built-ins -
|
|
392
|
+
// would leave it permanently unaskable, so a headless run could create files in
|
|
393
|
+
// the skills directory that the caller never saw.
|
|
394
|
+
//
|
|
395
|
+
// At CALL time this ends up inside the plan gate and inside the skill ceiling
|
|
396
|
+
// (both applied below), which is the order their comments demand: neither needs
|
|
397
|
+
// I/O, so neither may sit behind a human wait.
|
|
398
|
+
if (options.toolGate) {
|
|
399
|
+
const gate = options.toolGate;
|
|
400
|
+
const withToolWait = options.withToolWait;
|
|
401
|
+
tools = tools.map((tool) => gate.applies(tool.name)
|
|
402
|
+
? withToolApproval(tool, gate, withToolWait ? { withHumanWait: withToolWait } : {})
|
|
403
|
+
: tool);
|
|
404
|
+
}
|
|
405
|
+
// --- The caller's lifecycle hooks: `tool_pre` (section 2.4.9). ---
|
|
406
|
+
//
|
|
407
|
+
// IMMEDIATELY AFTER THE APPROVAL GATE, WHICH MAKES IT OUTSIDE IT AT CALL TIME: a human
|
|
408
|
+
// must approve the arguments the hook produced, never the ones it replaced. The two
|
|
409
|
+
// policy wrappers below are applied later and are therefore outside this one, so a tool
|
|
410
|
+
// plan mode already refuses never costs a hook round trip (I-H4b - a recorded trade-off,
|
|
411
|
+
// not an oversight).
|
|
412
|
+
if (options.hookRunner) {
|
|
413
|
+
const hookRunner = options.hookRunner;
|
|
414
|
+
tools = tools.map((tool) => withToolPreHooks(tool, hookRunner));
|
|
415
|
+
}
|
|
318
416
|
// --- The `aragon exec` permission filter (cli-integration-surface §4.2). ---
|
|
319
417
|
//
|
|
320
418
|
// ITS OWN `if`, AFTER THE LAST APPEND AND BEFORE THE `toolPolicy` WRAPPER, and
|
|
@@ -386,8 +484,19 @@ export function createBuiltinTools(options) {
|
|
|
386
484
|
return list;
|
|
387
485
|
return list.map((tool) => withJobNoticeTail(tool, () => queue.drain()));
|
|
388
486
|
};
|
|
487
|
+
// THE OUTERMOST LAYER OF ALL, AND THAT IS THE WHOLE OF I-H13 (section 2.4.9): the
|
|
488
|
+
// `<job_notices>` tail is appended INSIDE this wrapper and carries a background job's
|
|
489
|
+
// last raw output lines, so a redactor placed any deeper would miss the most likely
|
|
490
|
+
// leak. The cost - a `tool_post` `replace` swallows the notice block - is accepted and
|
|
491
|
+
// documented: every notice is also a `job` event the caller already received.
|
|
492
|
+
const withHooksPost = (list) => {
|
|
493
|
+
const hookRunner = options.hookRunner;
|
|
494
|
+
if (!hookRunner)
|
|
495
|
+
return list;
|
|
496
|
+
return list.map((tool) => withToolPostHooks(tool, hookRunner));
|
|
497
|
+
};
|
|
389
498
|
if (!options.agentMode)
|
|
390
|
-
return withJobNotices(tools);
|
|
499
|
+
return withHooksPost(withJobNotices(tools));
|
|
391
500
|
const getMode = options.agentMode;
|
|
392
|
-
return withJobNotices(tools.map((t) => withPlanModeGate(t, getMode)));
|
|
501
|
+
return withHooksPost(withJobNotices(tools.map((t) => withPlanModeGate(t, getMode))));
|
|
393
502
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The per-call approval gate: one wrapper, one shared denial text, no I/O.
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - `src/tools/**` is inside the glyph scanner's scope.
|
|
5
|
+
*
|
|
6
|
+
* WHY A WRAPPER AND NOT A CORE HOOK (headless-host-bridge section 2.3.3). A
|
|
7
|
+
* `beforeExecute` callback on Core's `ToolExecutor` would put a human wait in
|
|
8
|
+
* front of the plan gate's refusal, freeze a new field into the public API that
|
|
9
|
+
* `public-api.test.ts` guards, and teach the engine about a host shape it has no
|
|
10
|
+
* business knowing. The `{...tool, execute}` shape below is the one already used
|
|
11
|
+
* three times in `tools/index.ts` (confirmation, skill ceiling, plan gate), so
|
|
12
|
+
* nothing in the engine has to learn that an approval exists.
|
|
13
|
+
*
|
|
14
|
+
* WHERE IT SITS IN THE STACK IS PART OF THE DESIGN, not an accident of import
|
|
15
|
+
* order: `tools/index.ts` applies it AFTER every append (so `skill_create` is
|
|
16
|
+
* reachable) and BEFORE the `toolPolicy` / plan-gate wrappers are added, so at
|
|
17
|
+
* call time those two refusals - which need no I/O - happen first and a tool that
|
|
18
|
+
* policy would refuse never interrupts a human.
|
|
19
|
+
*/
|
|
20
|
+
import { type AgentTool } from '@aragon-agent/core';
|
|
21
|
+
export interface ToolGateRequest {
|
|
22
|
+
tool: string;
|
|
23
|
+
/** Core's tool-call id, so the caller can pair this with the `tool_call` event. */
|
|
24
|
+
callId: string;
|
|
25
|
+
input: Record<string, unknown>;
|
|
26
|
+
/** One readable line: `Run: npm test`. Comes from the same `summarize()`. */
|
|
27
|
+
summary: string;
|
|
28
|
+
/** The turn this belongs to, when the host can say. */
|
|
29
|
+
turn?: number;
|
|
30
|
+
/**
|
|
31
|
+
* The tool's `ctx.signal`. WITHOUT IT THE WAIT HAS NO CEILING: the idle
|
|
32
|
+
* watchdog is paused for the duration and the executor's own timeout only
|
|
33
|
+
* aborts the context, so an unanswered request would wait for the process to
|
|
34
|
+
* die. Passed through, never invented.
|
|
35
|
+
*/
|
|
36
|
+
signal?: AbortSignal;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* `message` is the model-visible text used VERBATIM when present.
|
|
40
|
+
*
|
|
41
|
+
* The three non-approval outcomes are not variations of "denied": a timeout says
|
|
42
|
+
* how long it waited, an interruption says the turn ended. Collapsing them into
|
|
43
|
+
* one sentence would make the model re-try a command after a one-second user
|
|
44
|
+
* click and would hide a real timeout in a log that reads like a refusal.
|
|
45
|
+
*/
|
|
46
|
+
export type ToolGateVerdict = {
|
|
47
|
+
allow: true;
|
|
48
|
+
} | {
|
|
49
|
+
allow: false;
|
|
50
|
+
reason?: string;
|
|
51
|
+
message?: string;
|
|
52
|
+
};
|
|
53
|
+
export interface ToolGate {
|
|
54
|
+
/**
|
|
55
|
+
* PURE AND NAME-ONLY: `CALLER_GATED_TOOLS.has(name)`.
|
|
56
|
+
*
|
|
57
|
+
* The session-level allow set is deliberately NOT consulted here - it lives in
|
|
58
|
+
* `decide()`. Mixing them would make `applies()` stateful, and the object
|
|
59
|
+
* identity assertions that prove "no gate means no change" would stop being
|
|
60
|
+
* decidable.
|
|
61
|
+
*/
|
|
62
|
+
applies(toolName: string): boolean;
|
|
63
|
+
/** NEVER rejects. Resolves `deny` on abort / timeout / capacity / closure. */
|
|
64
|
+
decide(request: ToolGateRequest): Promise<ToolGateVerdict>;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* THE ONE PLACE A DENIAL IS WORDED. Shared by the wrapper below and by the tests,
|
|
68
|
+
* so "the model is told what happened and what to do instead" is a single string
|
|
69
|
+
* rather than a paragraph repeated in two modules.
|
|
70
|
+
*
|
|
71
|
+
* The last sentence is not politeness. A bare "denied" makes a model retry the
|
|
72
|
+
* same call; the observed failure mode is a paid loop that burns a budget on one
|
|
73
|
+
* forbidden command. Naming the tool and the reason is what lets it route around
|
|
74
|
+
* the refusal instead.
|
|
75
|
+
*/
|
|
76
|
+
export declare function denialMessage(tool: string, reason?: string): string;
|
|
77
|
+
/** The text a timed-out decision produces. Exported for the same reason. */
|
|
78
|
+
export declare function timeoutMessage(tool: string, timeoutMs: number): string;
|
|
79
|
+
/** The text an aborted turn produces, in the middle of a decision wait. */
|
|
80
|
+
export declare function interruptedMessage(tool: string): string;
|
|
81
|
+
export interface WithToolApprovalOptions {
|
|
82
|
+
/**
|
|
83
|
+
* The host's watchdog pause, forwarded straight from the controller.
|
|
84
|
+
*
|
|
85
|
+
* THE WAIT IS THE WHOLE REASON IT EXISTS: the idle watchdog would otherwise fire
|
|
86
|
+
* mid-approval and abort a run the user was in the middle of authorising, which
|
|
87
|
+
* on screen reads as "it randomly stopped". `withPausedWatchdog` is not
|
|
88
|
+
* reentrant, so this is the SAME function the plan tools receive rather than a
|
|
89
|
+
* second wrapper around it.
|
|
90
|
+
*/
|
|
91
|
+
withHumanWait?: <T>(fn: () => Promise<T>) => Promise<T>;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Wrap a tool behind the gate. Same `{...tool, execute}` shape as every other
|
|
95
|
+
* wrapper in this package, and the same `errorResult` discipline: a refusal is a
|
|
96
|
+
* TOOL RESULT, never a throw, because the engine pushes whatever this returns
|
|
97
|
+
* into history - and a `tool_use` with no `tool_result` is an HTTP 400 on the next
|
|
98
|
+
* request.
|
|
99
|
+
*/
|
|
100
|
+
export declare function withToolApproval(tool: AgentTool, gate: ToolGate, options?: WithToolApprovalOptions): AgentTool;
|