@aragon-agent/cli 0.6.38 → 0.6.40
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 +263 -0
- package/README.md +544 -11
- package/dist/agent/controller.d.ts +245 -10
- package/dist/agent/controller.js +367 -22
- package/dist/agent/model-profile-settings.js +12 -1
- package/dist/agent/plan-prompt.d.ts +1 -3
- package/dist/agent/plan-prompt.js +13 -7
- package/dist/agent/reducer.d.ts +70 -2
- package/dist/agent/reducer.js +59 -6
- package/dist/agent/skill-triage-prompt.d.ts +42 -0
- package/dist/agent/skill-triage-prompt.js +74 -0
- package/dist/agent/system-prompt.d.ts +28 -2
- package/dist/agent/system-prompt.js +12 -4
- package/dist/cli.js +106 -32
- package/dist/commands/builtins.js +24 -3
- 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/skills.js +86 -1
- package/dist/commands/subagent-command.d.ts +25 -0
- package/dist/commands/subagent-command.js +242 -0
- package/dist/commands/task-command.d.ts +39 -0
- package/dist/commands/task-command.js +102 -0
- package/dist/compaction/command.js +16 -3
- package/dist/compaction/compactor.d.ts +25 -0
- package/dist/compaction/compactor.js +111 -10
- package/dist/compaction/digest.d.ts +24 -1
- package/dist/compaction/digest.js +139 -17
- package/dist/compaction/limits.d.ts +21 -0
- package/dist/compaction/limits.js +21 -0
- package/dist/compaction/reason-text.d.ts +36 -0
- package/dist/compaction/reason-text.js +78 -0
- package/dist/compaction/types.d.ts +82 -14
- package/dist/compaction/wiring.d.ts +13 -8
- package/dist/compaction/wiring.js +1 -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 +55 -3
- package/dist/config/schema.d.ts +125 -10
- package/dist/config/schema.js +138 -14
- package/dist/config/store.js +23 -3
- 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/log-mirror.d.ts +21 -4
- package/dist/proc/log-mirror.js +29 -7
- package/dist/proc/output-streams.d.ts +50 -0
- package/dist/proc/output-streams.js +54 -0
- package/dist/skills/service.d.ts +41 -0
- package/dist/skills/service.js +62 -0
- package/dist/skills/triage.d.ts +194 -0
- package/dist/skills/triage.js +393 -0
- package/dist/team/forced-dispatch.d.ts +95 -0
- package/dist/team/forced-dispatch.js +208 -0
- package/dist/team/limits.d.ts +21 -1
- package/dist/team/limits.js +21 -1
- package/dist/team/normalize.d.ts +15 -0
- package/dist/team/normalize.js +21 -4
- package/dist/team/prompt.d.ts +6 -3
- package/dist/team/prompt.js +9 -5
- 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/team/task-tool.js +11 -7
- package/dist/tools/bash-tool.d.ts +10 -0
- package/dist/tools/bash-tool.js +71 -6
- package/dist/tools/human-input.d.ts +35 -2
- package/dist/tools/human-input.js +38 -0
- package/dist/tools/index.d.ts +93 -1
- package/dist/tools/index.js +112 -3
- package/dist/tools/plan-tools.d.ts +20 -12
- package/dist/tools/plan-tools.js +24 -17
- package/dist/tools/tool-gate.d.ts +100 -0
- package/dist/tools/tool-gate.js +87 -0
- package/dist/ui/App.js +238 -12
- package/dist/ui/Header.d.ts +28 -16
- package/dist/ui/Header.js +21 -19
- package/dist/ui/StatusBar.d.ts +1 -0
- package/dist/ui/StatusBar.js +1 -0
- package/dist/ui/Transcript.js +1 -1
- package/dist/ui/entries/CompactionCard.d.ts +11 -1
- package/dist/ui/entries/CompactionCard.js +65 -4
- 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/status-layout.d.ts +10 -0
- package/dist/ui/layout/status-layout.js +18 -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/SettingsScreen.js +29 -5
- package/dist/ui/overlays/ShortcutPanel.d.ts +39 -0
- package/dist/ui/overlays/ShortcutPanel.js +106 -0
- package/dist/ui/overlays/TeamFanoutPage.d.ts +35 -0
- package/dist/ui/overlays/TeamFanoutPage.js +78 -0
- package/dist/ui/overlays/model-profile-state.d.ts +19 -1
- package/dist/ui/overlays/model-profile-state.js +12 -0
- package/dist/ui/overlays/team-fanout-state.d.ts +73 -0
- package/dist/ui/overlays/team-fanout-state.js +97 -0
- package/dist/ui/shortcuts.d.ts +81 -0
- package/dist/ui/shortcuts.js +221 -0
- package/package.json +2 -2
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One hook round trip: write the request, wait bounded, write exactly one outcome
|
|
3
|
+
* (headless-threads-and-lifecycle-hooks section 2.4.8).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY - `src/exec/**` is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* THREE NON-NEGOTIABLE PROPERTIES, each of which a naive implementation loses:
|
|
8
|
+
*
|
|
9
|
+
* EVERY REQUEST GETS EXACTLY ONE `hook_resolved` (I-H5). Timeout, saturation, abort,
|
|
10
|
+
* an illegal answer and a request for an unknown hook id all settle. A caller that
|
|
11
|
+
* bookkeeps by id can therefore close its own table, which is the only reason the
|
|
12
|
+
* channels are debuggable at all.
|
|
13
|
+
*
|
|
14
|
+
* SATURATION REFUSES INSTEAD OF QUEUEING. A queued hook request makes the host wait
|
|
15
|
+
* for an answer it already knows will time out; refusing is explainable now and the
|
|
16
|
+
* model can route around it immediately.
|
|
17
|
+
*
|
|
18
|
+
* THE TIMEOUT IS THE HOOK'S OWN. `timeoutMs` comes from the spec (clamped by the
|
|
19
|
+
* validator and by `--hook-timeout` at dispatch), not from a global constant, because
|
|
20
|
+
* two hooks on one tool may legitimately need different budgets.
|
|
21
|
+
*/
|
|
22
|
+
import type { BridgeHookAnswerFrame } from '../bridge/protocol.js';
|
|
23
|
+
import { type HookOutcome, type HookPoint, type HookTimeoutPolicy } from './limits.js';
|
|
24
|
+
import type { HookRegistry, HookSpec } from './registry.js';
|
|
25
|
+
import { type HookAnswer } from './validate.js';
|
|
26
|
+
export interface HookDispatchPorts {
|
|
27
|
+
/** One outbound frame through the sealed writer. */
|
|
28
|
+
write(event: Record<string, unknown>): void;
|
|
29
|
+
/** Stamped onto every event; the thread's session id. */
|
|
30
|
+
sessionId: string;
|
|
31
|
+
now?: () => number;
|
|
32
|
+
/** The effective ceiling for one round trip: `min(spec.timeoutMs, --hook-timeout)`. */
|
|
33
|
+
timeoutMs: () => number;
|
|
34
|
+
/** `--on-hook-timeout`; read per request so a signal change is not needed. */
|
|
35
|
+
policy: () => HookTimeoutPolicy;
|
|
36
|
+
onAudit?: (record: {
|
|
37
|
+
hookId: string;
|
|
38
|
+
point: HookPoint;
|
|
39
|
+
outcome: HookOutcome;
|
|
40
|
+
}) => void;
|
|
41
|
+
}
|
|
42
|
+
export interface HookRequestInput {
|
|
43
|
+
hook: HookSpec;
|
|
44
|
+
/** The request this hook fires for. Never empty: `prompt_pre` runs after `begin()`. */
|
|
45
|
+
requestId: string;
|
|
46
|
+
payload: Record<string, unknown>;
|
|
47
|
+
/** The tool call's abort signal, when the host has one. */
|
|
48
|
+
signal?: AbortSignal;
|
|
49
|
+
}
|
|
50
|
+
/** What one hook said, plus why it said nothing when it did not. */
|
|
51
|
+
export interface HookReply {
|
|
52
|
+
outcome: HookOutcome;
|
|
53
|
+
/** Present only for `outcome: 'answered'`. */
|
|
54
|
+
answer?: HookAnswer;
|
|
55
|
+
/** The action string as the caller wrote it, for `hook_resolved.action`. */
|
|
56
|
+
action?: string;
|
|
57
|
+
/** A stable reason for a non-answer: `hook_timeout`, `hook_saturated`, ... */
|
|
58
|
+
code?: string;
|
|
59
|
+
}
|
|
60
|
+
export interface HookDispatcher {
|
|
61
|
+
/** One round trip. NEVER rejects; every failure is an outcome. */
|
|
62
|
+
request(input: HookRequestInput): Promise<HookReply>;
|
|
63
|
+
/** Route a `hook_answer` frame. `false` when it names no live request. */
|
|
64
|
+
answer(frame: BridgeHookAnswerFrame): boolean;
|
|
65
|
+
/** Settle every request in flight as `aborted` (cancel_request, thread close, interrupt). */
|
|
66
|
+
abortAll(code?: string): void;
|
|
67
|
+
inFlight(): number;
|
|
68
|
+
close(): void;
|
|
69
|
+
}
|
|
70
|
+
export interface HookDispatcherOptions {
|
|
71
|
+
registry: HookRegistry;
|
|
72
|
+
ports: HookDispatchPorts;
|
|
73
|
+
}
|
|
74
|
+
export declare function createHookDispatcher(options: HookDispatcherOptions): HookDispatcher;
|
|
75
|
+
/** What a non-answer means, per `--on-hook-timeout` (section 2.4.8's table). */
|
|
76
|
+
export declare function isFailClosed(reply: HookReply): boolean;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { HOOK_LIMITS } from './limits.js';
|
|
2
|
+
import { validateHookAnswer } from './validate.js';
|
|
3
|
+
export function createHookDispatcher(options) {
|
|
4
|
+
const ports = options.ports;
|
|
5
|
+
const now = ports.now ?? Date.now;
|
|
6
|
+
const pending = new Map();
|
|
7
|
+
let sequence = 0;
|
|
8
|
+
let closed = false;
|
|
9
|
+
const resolved = (hook, requestId, outcome, extra = {}) => {
|
|
10
|
+
ports.write({
|
|
11
|
+
type: 'hook_resolved',
|
|
12
|
+
sessionId: ports.sessionId,
|
|
13
|
+
requestId,
|
|
14
|
+
...(extra.hookRequestId !== undefined ? { hookRequestId: extra.hookRequestId } : {}),
|
|
15
|
+
hookId: hook.id,
|
|
16
|
+
point: hook.point,
|
|
17
|
+
outcome,
|
|
18
|
+
...(extra.action !== undefined ? { action: extra.action } : {}),
|
|
19
|
+
...(extra.code !== undefined ? { code: extra.code } : {}),
|
|
20
|
+
});
|
|
21
|
+
ports.onAudit?.({ hookId: hook.id, point: hook.point, outcome });
|
|
22
|
+
};
|
|
23
|
+
/** Remove the entry, clear its timer, drop the abort listener. Idempotent by id. */
|
|
24
|
+
const take = (id) => {
|
|
25
|
+
const entry = pending.get(id);
|
|
26
|
+
if (!entry)
|
|
27
|
+
return undefined;
|
|
28
|
+
pending.delete(id);
|
|
29
|
+
if (entry.timer)
|
|
30
|
+
clearTimeout(entry.timer);
|
|
31
|
+
if (entry.signal && entry.onAbort)
|
|
32
|
+
entry.signal.removeEventListener('abort', entry.onAbort);
|
|
33
|
+
return entry;
|
|
34
|
+
};
|
|
35
|
+
return {
|
|
36
|
+
request(input) {
|
|
37
|
+
const { hook } = input;
|
|
38
|
+
if (closed) {
|
|
39
|
+
resolved(hook, input.requestId, 'aborted', { code: 'hook_closed' });
|
|
40
|
+
return Promise.resolve({ outcome: 'aborted', code: 'hook_closed' });
|
|
41
|
+
}
|
|
42
|
+
if (pending.size >= HOOK_LIMITS.maxConcurrentRequests) {
|
|
43
|
+
// NOT QUEUED: see the file header.
|
|
44
|
+
resolved(hook, input.requestId, 'saturated', { code: 'hook_saturated' });
|
|
45
|
+
return Promise.resolve({ outcome: 'saturated', code: 'hook_saturated' });
|
|
46
|
+
}
|
|
47
|
+
sequence += 1;
|
|
48
|
+
const id = `hx-${sequence}`;
|
|
49
|
+
const timeoutMs = Math.min(ports.timeoutMs(), hook.timeoutMs);
|
|
50
|
+
return new Promise((resolve) => {
|
|
51
|
+
// SETTLED ONCE, AND THE SETTLE DOES NOT RE-ENTER THE TABLE. The first draft routed
|
|
52
|
+
// the settle back through `take()`, which had already removed the entry - so the
|
|
53
|
+
// promise never resolved and every hook looked like it hung. One boolean, right here.
|
|
54
|
+
let settled = false;
|
|
55
|
+
const finish = (reply) => {
|
|
56
|
+
if (settled)
|
|
57
|
+
return;
|
|
58
|
+
settled = true;
|
|
59
|
+
if (entry.timer)
|
|
60
|
+
clearTimeout(entry.timer);
|
|
61
|
+
if (input.signal && entry.onAbort) {
|
|
62
|
+
input.signal.removeEventListener('abort', entry.onAbort);
|
|
63
|
+
}
|
|
64
|
+
resolve(reply);
|
|
65
|
+
};
|
|
66
|
+
const entry = {
|
|
67
|
+
hook,
|
|
68
|
+
point: hook.point,
|
|
69
|
+
requestId: input.requestId,
|
|
70
|
+
timer: null,
|
|
71
|
+
settle: finish,
|
|
72
|
+
...(input.signal ? { signal: input.signal } : {}),
|
|
73
|
+
};
|
|
74
|
+
if (input.signal) {
|
|
75
|
+
const onAbort = () => {
|
|
76
|
+
resolved(hook, input.requestId, 'aborted', { code: 'hook_aborted' });
|
|
77
|
+
pending.delete(id);
|
|
78
|
+
entry.settle({ outcome: 'aborted', code: 'hook_aborted' });
|
|
79
|
+
};
|
|
80
|
+
entry.onAbort = onAbort;
|
|
81
|
+
if (input.signal.aborted) {
|
|
82
|
+
resolved(hook, input.requestId, 'aborted', { code: 'hook_aborted' });
|
|
83
|
+
resolve({ outcome: 'aborted', code: 'hook_aborted' });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
input.signal.addEventListener('abort', onAbort, { once: true });
|
|
87
|
+
}
|
|
88
|
+
pending.set(id, entry);
|
|
89
|
+
entry.timer = setTimeout(() => {
|
|
90
|
+
pending.delete(id);
|
|
91
|
+
// `hookRequestId` IS CARRIED HERE TOO. A timeout is precisely the case where the
|
|
92
|
+
// caller must be able to close its own bookkeeping entry, and an outcome without
|
|
93
|
+
// the round-trip id would leave that entry open forever.
|
|
94
|
+
resolved(hook, input.requestId, 'timeout', {
|
|
95
|
+
code: 'hook_timeout',
|
|
96
|
+
hookRequestId: id,
|
|
97
|
+
});
|
|
98
|
+
entry.settle({ outcome: 'timeout', code: 'hook_timeout' });
|
|
99
|
+
}, timeoutMs);
|
|
100
|
+
entry.timer.unref?.();
|
|
101
|
+
ports.write({
|
|
102
|
+
type: 'hook_request',
|
|
103
|
+
sessionId: ports.sessionId,
|
|
104
|
+
requestId: input.requestId,
|
|
105
|
+
hookRequestId: id,
|
|
106
|
+
hookId: hook.id,
|
|
107
|
+
point: hook.point,
|
|
108
|
+
payload: input.payload,
|
|
109
|
+
createdAt: now(),
|
|
110
|
+
timeoutMs,
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
},
|
|
114
|
+
answer(frame) {
|
|
115
|
+
const entry = take(frame.respondsTo);
|
|
116
|
+
if (!entry)
|
|
117
|
+
return false;
|
|
118
|
+
// AN ILLEGAL ANSWER IS `invalid`, AND THE REQUEST STILL SETTLES (I-H7). The caller
|
|
119
|
+
// is told the exact reason so it can fix its own hook, and the policy table then
|
|
120
|
+
// decides what the host does - never a third, undocumented behaviour.
|
|
121
|
+
const validated = validateHookAnswer(entry.point, frame);
|
|
122
|
+
if (!validated.ok) {
|
|
123
|
+
resolved(entry.hook, entry.requestId, 'invalid', {
|
|
124
|
+
code: validated.code,
|
|
125
|
+
hookRequestId: frame.respondsTo,
|
|
126
|
+
});
|
|
127
|
+
entry.settle({ outcome: 'invalid', code: validated.code });
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
resolved(entry.hook, entry.requestId, 'answered', {
|
|
131
|
+
action: validated.answer.action,
|
|
132
|
+
hookRequestId: frame.respondsTo,
|
|
133
|
+
});
|
|
134
|
+
entry.settle({
|
|
135
|
+
outcome: 'answered',
|
|
136
|
+
answer: validated.answer,
|
|
137
|
+
action: validated.answer.action,
|
|
138
|
+
});
|
|
139
|
+
return true;
|
|
140
|
+
},
|
|
141
|
+
abortAll(code = 'hook_aborted') {
|
|
142
|
+
for (const id of [...pending.keys()]) {
|
|
143
|
+
const entry = take(id);
|
|
144
|
+
if (!entry)
|
|
145
|
+
continue;
|
|
146
|
+
resolved(entry.hook, entry.requestId, 'aborted', { code, hookRequestId: id });
|
|
147
|
+
entry.settle({ outcome: 'aborted', code });
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
inFlight: () => pending.size,
|
|
151
|
+
close() {
|
|
152
|
+
closed = true;
|
|
153
|
+
for (const id of [...pending.keys()]) {
|
|
154
|
+
const entry = take(id);
|
|
155
|
+
if (!entry)
|
|
156
|
+
continue;
|
|
157
|
+
resolved(entry.hook, entry.requestId, 'aborted', {
|
|
158
|
+
code: 'hook_closed',
|
|
159
|
+
hookRequestId: id,
|
|
160
|
+
});
|
|
161
|
+
entry.settle({ outcome: 'aborted', code: 'hook_closed' });
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/** What a non-answer means, per `--on-hook-timeout` (section 2.4.8's table). */
|
|
167
|
+
export function isFailClosed(reply) {
|
|
168
|
+
return reply.outcome !== 'answered';
|
|
169
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The hook channel's one public surface
|
|
3
|
+
* (headless-threads-and-lifecycle-hooks section 2.6 rows 22-27).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY - `src/exec/**` is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* THREE PORTS, ONE OBJECT, AND THEY ARE THE ONLY THINGS THE REST OF THE RUN SEES:
|
|
8
|
+
*
|
|
9
|
+
* `toolRunner` -> `createBuiltinTools({ hookRunner })`, the two wrappers.
|
|
10
|
+
* `promptPre` -> `ExecRunner`'s `hookPort`, between `scope.begin()` and the ack.
|
|
11
|
+
* `turnStart` -> `ExecRunner`'s `beforeTurn`, after the budget, before `prompt()`.
|
|
12
|
+
*
|
|
13
|
+
* THE RUNTIME IS ALWAYS PRESENT IN BRIDGE MODE, EVEN WITH ZERO HOOKS. Hooks are
|
|
14
|
+
* registered after the controller - and therefore after `createBuiltinTools` - exists, so
|
|
15
|
+
* the wrappers have to be installed up front and read the table live. That is the one
|
|
16
|
+
* place this implementation deliberately departs from the design's "no `hookRunner`, no
|
|
17
|
+
* wrapper" reading; the OBSERVABLE contract it protects is unchanged (with no hooks
|
|
18
|
+
* registered, no `hook_request` is emitted and every tool call behaves exactly as it does
|
|
19
|
+
* today), and the departure is recorded in the spec's implementation-notes section.
|
|
20
|
+
*/
|
|
21
|
+
import type { BridgeHookAnswerFrame, BridgeHookRegisterFrame } from '../bridge/protocol.js';
|
|
22
|
+
import type { HookTimeoutPolicy } from './limits.js';
|
|
23
|
+
import { type HookRegistry } from './registry.js';
|
|
24
|
+
import { type ToolHookRunner } from './tool-wrapper.js';
|
|
25
|
+
import { type TurnHook } from './turn.js';
|
|
26
|
+
import type { AgentTool } from '@aragon-agent/core';
|
|
27
|
+
/** What `prompt_pre` decided about one caller message. */
|
|
28
|
+
export type PromptPreOutcome = {
|
|
29
|
+
action: 'allow';
|
|
30
|
+
} | {
|
|
31
|
+
action: 'rewrite';
|
|
32
|
+
text: string;
|
|
33
|
+
} | {
|
|
34
|
+
action: 'deny';
|
|
35
|
+
reason?: string;
|
|
36
|
+
message?: string;
|
|
37
|
+
code: string;
|
|
38
|
+
isError: boolean;
|
|
39
|
+
};
|
|
40
|
+
export interface HookRegisterAck {
|
|
41
|
+
accepted: boolean;
|
|
42
|
+
code?: string;
|
|
43
|
+
message?: string;
|
|
44
|
+
hooks?: string[];
|
|
45
|
+
warnings?: string[];
|
|
46
|
+
}
|
|
47
|
+
export interface HookRuntime {
|
|
48
|
+
readonly registry: HookRegistry;
|
|
49
|
+
readonly toolRunner: ToolHookRunner;
|
|
50
|
+
register(frame: BridgeHookRegisterFrame): HookRegisterAck;
|
|
51
|
+
/** `false` when the frame named no live hook request (`hook_not_registered`). */
|
|
52
|
+
answer(frame: BridgeHookAnswerFrame): boolean;
|
|
53
|
+
promptPre(input: {
|
|
54
|
+
text: string;
|
|
55
|
+
queued: boolean;
|
|
56
|
+
requestId: string;
|
|
57
|
+
signal?: AbortSignal;
|
|
58
|
+
}): Promise<PromptPreOutcome>;
|
|
59
|
+
beforeTurn: TurnHook['beforeTurn'];
|
|
60
|
+
/** Every request in flight settles as `aborted`. */
|
|
61
|
+
abortAll(): void;
|
|
62
|
+
close(): void;
|
|
63
|
+
count(): number;
|
|
64
|
+
/** Reset the per-request counters (injection budget). */
|
|
65
|
+
beginRequest(): void;
|
|
66
|
+
}
|
|
67
|
+
export interface HookRuntimeOptions {
|
|
68
|
+
sessionId: string;
|
|
69
|
+
write(event: Record<string, unknown>): void;
|
|
70
|
+
/** Host tools plus this thread's caller tools; the registration whitelist. */
|
|
71
|
+
knownTools: () => ReadonlySet<string>;
|
|
72
|
+
/** `--hook-timeout`, already clamped. */
|
|
73
|
+
timeoutMs: number;
|
|
74
|
+
policy: () => HookTimeoutPolicy;
|
|
75
|
+
now?: () => number;
|
|
76
|
+
onAudit?: (record: {
|
|
77
|
+
hookId: string;
|
|
78
|
+
point: string;
|
|
79
|
+
outcome: string;
|
|
80
|
+
}) => void;
|
|
81
|
+
}
|
|
82
|
+
export declare function createHookRuntime(options: HookRuntimeOptions): HookRuntime;
|
|
83
|
+
export type { AgentTool };
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { createHookDispatcher, } from './dispatch.js';
|
|
2
|
+
import { createHookRegistry } from './registry.js';
|
|
3
|
+
import { hookBlockedMessage, hookWithheldMessage, revalidateRewrite, } from './tool-wrapper.js';
|
|
4
|
+
import { createTurnHook } from './turn.js';
|
|
5
|
+
export function createHookRuntime(options) {
|
|
6
|
+
const registry = createHookRegistry({ knownTools: options.knownTools });
|
|
7
|
+
const dispatch = createHookDispatcher({
|
|
8
|
+
registry,
|
|
9
|
+
ports: {
|
|
10
|
+
write: options.write,
|
|
11
|
+
sessionId: options.sessionId,
|
|
12
|
+
timeoutMs: () => options.timeoutMs,
|
|
13
|
+
policy: options.policy,
|
|
14
|
+
...(options.now ? { now: options.now } : {}),
|
|
15
|
+
...(options.onAudit
|
|
16
|
+
? { onAudit: (record) => options.onAudit?.(record) }
|
|
17
|
+
: {}),
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
const turn = createTurnHook({ registry, dispatch, policy: options.policy });
|
|
21
|
+
const toolRunner = {
|
|
22
|
+
hasTools(point, tool) {
|
|
23
|
+
return registry.chainFor(point, tool).length > 0;
|
|
24
|
+
},
|
|
25
|
+
async beforeTool(input) {
|
|
26
|
+
const chain = registry.chainFor('tool_pre', input.tool.name);
|
|
27
|
+
let params = input.params;
|
|
28
|
+
let turnNumber = input.turn;
|
|
29
|
+
for (const hook of chain) {
|
|
30
|
+
const reply = await dispatch.request({
|
|
31
|
+
hook,
|
|
32
|
+
requestId: input.callId,
|
|
33
|
+
payload: {
|
|
34
|
+
tool: input.tool.name,
|
|
35
|
+
callId: input.callId,
|
|
36
|
+
input: params,
|
|
37
|
+
summary: input.summary,
|
|
38
|
+
...(turnNumber !== undefined ? { turn: turnNumber } : {}),
|
|
39
|
+
},
|
|
40
|
+
...(input.signal ? { signal: input.signal } : {}),
|
|
41
|
+
});
|
|
42
|
+
if (reply.outcome !== 'answered') {
|
|
43
|
+
if (options.policy() === 'allow') {
|
|
44
|
+
// THE ORIGINAL PARAMETERS, NOT THE PARTIALLY REWRITTEN ONES: `allow` means
|
|
45
|
+
// "a hook that cannot answer must not decide", so the chain is abandoned at
|
|
46
|
+
// the answer it never gave.
|
|
47
|
+
return { allow: true, params: input.params };
|
|
48
|
+
}
|
|
49
|
+
return { allow: false, ...hookBlockedMessage(input.tool.name, reply, options.policy()) };
|
|
50
|
+
}
|
|
51
|
+
if (reply.answer?.action === 'deny') {
|
|
52
|
+
return { allow: false, ...hookBlockedMessage(input.tool.name, reply, options.policy()) };
|
|
53
|
+
}
|
|
54
|
+
if (reply.answer?.action === 'rewrite') {
|
|
55
|
+
const rewritten = reply.answer.input ?? params;
|
|
56
|
+
// EVERY REWRITE IS RE-VALIDATED, because the previous hook's output is this
|
|
57
|
+
// hook's input and a chain of local edits can compose into something no
|
|
58
|
+
// single hook would have written (I-H3).
|
|
59
|
+
const checked = await revalidateRewrite(input.tool, rewritten);
|
|
60
|
+
if (!checked.ok) {
|
|
61
|
+
// `params`, for the same reason as the timeout branch above: the rewrites that
|
|
62
|
+
// DID validate must survive a later hook's illegal edit.
|
|
63
|
+
if (options.policy() === 'allow')
|
|
64
|
+
return { allow: true, params };
|
|
65
|
+
return {
|
|
66
|
+
allow: false,
|
|
67
|
+
...hookBlockedMessage(input.tool.name, { outcome: 'invalid', code: 'hook_invalid_answer' }, options.policy()),
|
|
68
|
+
reason: checked.message,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
params = checked.params;
|
|
72
|
+
turnNumber = undefined;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return { allow: true, params };
|
|
76
|
+
},
|
|
77
|
+
async afterTool(input) {
|
|
78
|
+
const chain = registry.chainFor('tool_post', input.tool.name);
|
|
79
|
+
let result = input.result;
|
|
80
|
+
for (const hook of chain) {
|
|
81
|
+
const reply = await dispatch.request({
|
|
82
|
+
hook,
|
|
83
|
+
requestId: input.callId,
|
|
84
|
+
payload: {
|
|
85
|
+
tool: input.tool.name,
|
|
86
|
+
callId: input.callId,
|
|
87
|
+
content: result.content,
|
|
88
|
+
isError: result.isError === true,
|
|
89
|
+
},
|
|
90
|
+
...(input.signal ? { signal: input.signal } : {}),
|
|
91
|
+
});
|
|
92
|
+
if (reply.outcome !== 'answered') {
|
|
93
|
+
// `result`, NOT `input.result`: under `allow` an unanswered hook must not decide -
|
|
94
|
+
// and it must not REWIND either, or a redaction an earlier hook in this chain
|
|
95
|
+
// already made would be undone by a later hook that said nothing.
|
|
96
|
+
if (options.policy() === 'allow')
|
|
97
|
+
return result;
|
|
98
|
+
// FAIL CLOSED FOR OUTPUT, WHICH IS THE WHOLE POINT: an unanswered redactor must
|
|
99
|
+
// withhold the result rather than leak it.
|
|
100
|
+
return {
|
|
101
|
+
content: [{ type: 'text', text: hookWithheldMessage(input.tool.name, reply) }],
|
|
102
|
+
isError: true,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (reply.answer?.action === 'deny') {
|
|
106
|
+
return {
|
|
107
|
+
content: [{ type: 'text', text: hookWithheldMessage(input.tool.name, reply) }],
|
|
108
|
+
isError: true,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (reply.answer?.action === 'replace') {
|
|
112
|
+
result = {
|
|
113
|
+
content: normaliseContent(reply.answer.content),
|
|
114
|
+
isError: reply.answer.isError === true,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
return {
|
|
122
|
+
registry,
|
|
123
|
+
toolRunner,
|
|
124
|
+
register(frame) {
|
|
125
|
+
const result = registry.register(frame.hooks);
|
|
126
|
+
if (!result.ok)
|
|
127
|
+
return { accepted: false, code: result.code, message: result.message };
|
|
128
|
+
const ids = result.hooks.map((hook) => hook.id);
|
|
129
|
+
return {
|
|
130
|
+
accepted: true,
|
|
131
|
+
hooks: ids,
|
|
132
|
+
...(result.warnings.length > 0 ? { warnings: result.warnings } : {}),
|
|
133
|
+
};
|
|
134
|
+
},
|
|
135
|
+
answer: (frame) => dispatch.answer(frame),
|
|
136
|
+
async promptPre(input) {
|
|
137
|
+
// THE PER-REQUEST BUDGET IS RESET HERE, AND THIS IS THE ONLY PLACE THAT CAN DO IT:
|
|
138
|
+
// `prompt_pre` is the one hook point the runner enters exactly once per request, before
|
|
139
|
+
// any turn can start, so it is the request's opening edge. Without the reset
|
|
140
|
+
// `HOOK_LIMITS.maxInjectionsPerRequest` would in fact be a LIFETIME budget - a
|
|
141
|
+
// long-lived thread would stop injecting after 32 turns and never say why.
|
|
142
|
+
turn.reset();
|
|
143
|
+
const chain = registry.chainFor('prompt_pre');
|
|
144
|
+
let text = input.text;
|
|
145
|
+
for (const hook of chain) {
|
|
146
|
+
const reply = await dispatch.request({
|
|
147
|
+
hook,
|
|
148
|
+
requestId: input.requestId,
|
|
149
|
+
payload: { text, queued: input.queued },
|
|
150
|
+
...(input.signal ? { signal: input.signal } : {}),
|
|
151
|
+
});
|
|
152
|
+
if (reply.outcome !== 'answered') {
|
|
153
|
+
if (options.policy() === 'allow') {
|
|
154
|
+
// THE TEXT THE CHAIN HAS AGREED ON SO FAR. Returning `{action:'allow'}` here would
|
|
155
|
+
// discard an earlier hook's rewrite and send the ORIGINAL prompt - the difference
|
|
156
|
+
// between "one hook stayed silent" and "one hook's work vanished".
|
|
157
|
+
return text === input.text ? { action: 'allow' } : { action: 'rewrite', text };
|
|
158
|
+
}
|
|
159
|
+
return deny(reply.code ?? 'hook_timeout', options.policy() === 'error');
|
|
160
|
+
}
|
|
161
|
+
if (reply.answer?.action === 'deny') {
|
|
162
|
+
return {
|
|
163
|
+
action: 'deny',
|
|
164
|
+
...(reply.answer.reason ? { reason: reply.answer.reason } : {}),
|
|
165
|
+
...(reply.answer.message ? { message: reply.answer.message } : {}),
|
|
166
|
+
code: 'hook_denied',
|
|
167
|
+
isError: true,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
if (reply.answer?.action === 'rewrite')
|
|
171
|
+
text = reply.answer.text ?? text;
|
|
172
|
+
}
|
|
173
|
+
return text === input.text ? { action: 'allow' } : { action: 'rewrite', text };
|
|
174
|
+
},
|
|
175
|
+
beforeTurn: turn.beforeTurn,
|
|
176
|
+
abortAll: () => dispatch.abortAll(),
|
|
177
|
+
close: () => dispatch.close(),
|
|
178
|
+
count: () => registry.count(),
|
|
179
|
+
beginRequest: () => turn.reset(),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* A denial is a FAILED request, not a withdrawn one (section 2.4.2).
|
|
184
|
+
*
|
|
185
|
+
* `writeCancelledRequestEnd` reports `isError: false` because the caller withdrew its own
|
|
186
|
+
* request; a hook denial means the caller did not get what it asked for, and the two must
|
|
187
|
+
* not look alike in `result.isError`.
|
|
188
|
+
*/
|
|
189
|
+
function deny(code, isError) {
|
|
190
|
+
return {
|
|
191
|
+
action: 'deny',
|
|
192
|
+
code,
|
|
193
|
+
isError,
|
|
194
|
+
message: `A caller hook did not allow this prompt (${code}), so the model was not called. ` +
|
|
195
|
+
'Change the input, or ask the host operator to relax the hook.',
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
/** `string` -> one text part; an array is taken as a content-block list. */
|
|
199
|
+
function normaliseContent(content) {
|
|
200
|
+
if (typeof content === 'string')
|
|
201
|
+
return [{ type: 'text', text: content }];
|
|
202
|
+
if (Array.isArray(content))
|
|
203
|
+
return content;
|
|
204
|
+
return [{ type: 'text', text: JSON.stringify(content ?? null) }];
|
|
205
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural bounds for caller lifecycle hooks (headless-threads-and-lifecycle-hooks
|
|
3
|
+
* section 4.3).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY - `src/exec/**` is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* THE ONE NUMBER THAT IS NOT OBVIOUS IS `maxContentChars`. A `tool_post` `replace`
|
|
8
|
+
* becomes the `tool_result` the model reads, so it lands in every later provider
|
|
9
|
+
* request. Without a ceiling a caller could replace a 100 KB file read with 10 MB of
|
|
10
|
+
* its own text and the run would fail with a context overflow the caller cannot
|
|
11
|
+
* attribute to its own hook.
|
|
12
|
+
*/
|
|
13
|
+
export declare const HOOK_LIMITS: {
|
|
14
|
+
/** Registered hooks per thread, all points together. */
|
|
15
|
+
readonly maxHooks: 32;
|
|
16
|
+
/** Registered hooks on one (point x tool) pair. */
|
|
17
|
+
readonly maxHooksPerPoint: 4;
|
|
18
|
+
/** `--hook-timeout`'s lower bound: below this nothing can answer. */
|
|
19
|
+
readonly minTimeoutMs: 100;
|
|
20
|
+
/** `--hook-timeout`'s default. One round trip, not one tool call. */
|
|
21
|
+
readonly defaultTimeoutMs: 5000;
|
|
22
|
+
/** `--hook-timeout`'s upper bound. */
|
|
23
|
+
readonly maxTimeoutMs: 60000;
|
|
24
|
+
/**
|
|
25
|
+
* The ceiling `--on-hook-timeout allow` clamps to (section 2.4.8). A tool may be
|
|
26
|
+
* asked twice (`tool_pre` + `tool_post`), and `ToolExecutor`'s budget is 120 000 ms:
|
|
27
|
+
* two allowed round trips must not be able to consume half of it.
|
|
28
|
+
*/
|
|
29
|
+
readonly allowModeMaxTimeoutMs: 30000;
|
|
30
|
+
/** Hook round trips in flight at once. Beyond this the hook is saturated, not queued. */
|
|
31
|
+
readonly maxConcurrentRequests: 16;
|
|
32
|
+
/** `prompt_pre` rewrite / `turn_start` inject text length. */
|
|
33
|
+
readonly maxInjectChars: 8192;
|
|
34
|
+
/** `tool_post` replacement content length, in characters of its text. */
|
|
35
|
+
readonly maxContentChars: 32768;
|
|
36
|
+
/** Injections per request, so a hook cannot drive the model round in a loop. */
|
|
37
|
+
readonly maxInjectionsPerRequest: 32;
|
|
38
|
+
/** How much of a hook's own `reason`/`message` survives into model-visible text. */
|
|
39
|
+
readonly maxReasonChars: 512;
|
|
40
|
+
};
|
|
41
|
+
export type HookLimits = typeof HOOK_LIMITS;
|
|
42
|
+
/** The four points. A CLOSED SET: the design's section 1.4 forbids a fifth. */
|
|
43
|
+
export type HookPoint = 'prompt_pre' | 'turn_start' | 'tool_pre' | 'tool_post';
|
|
44
|
+
export declare const HOOK_POINTS: readonly HookPoint[];
|
|
45
|
+
export declare function isHookPoint(value: unknown): value is HookPoint;
|
|
46
|
+
/**
|
|
47
|
+
* How a hook round trip ended. `answered` is the only one that carries a value.
|
|
48
|
+
*
|
|
49
|
+
* `saturated` and `unregistered` are separate members because they are separate
|
|
50
|
+
* operational facts - "your hook table is full" and "that hook id does not exist"
|
|
51
|
+
* send an operator to two different places - and collapsing them into `invalid`
|
|
52
|
+
* would leave both indistinguishable in an audit trail.
|
|
53
|
+
*/
|
|
54
|
+
export type HookOutcome = 'answered' | 'timeout' | 'aborted' | 'invalid' | 'saturated' | 'unregistered';
|
|
55
|
+
/** `--on-hook-timeout`'s three policies (section 2.4.8). `deny` is fail-closed. */
|
|
56
|
+
export type HookTimeoutPolicy = 'deny' | 'allow' | 'error';
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural bounds for caller lifecycle hooks (headless-threads-and-lifecycle-hooks
|
|
3
|
+
* section 4.3).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY - `src/exec/**` is inside the glyph scanner's scope.
|
|
6
|
+
*
|
|
7
|
+
* THE ONE NUMBER THAT IS NOT OBVIOUS IS `maxContentChars`. A `tool_post` `replace`
|
|
8
|
+
* becomes the `tool_result` the model reads, so it lands in every later provider
|
|
9
|
+
* request. Without a ceiling a caller could replace a 100 KB file read with 10 MB of
|
|
10
|
+
* its own text and the run would fail with a context overflow the caller cannot
|
|
11
|
+
* attribute to its own hook.
|
|
12
|
+
*/
|
|
13
|
+
export const HOOK_LIMITS = {
|
|
14
|
+
/** Registered hooks per thread, all points together. */
|
|
15
|
+
maxHooks: 32,
|
|
16
|
+
/** Registered hooks on one (point x tool) pair. */
|
|
17
|
+
maxHooksPerPoint: 4,
|
|
18
|
+
/** `--hook-timeout`'s lower bound: below this nothing can answer. */
|
|
19
|
+
minTimeoutMs: 100,
|
|
20
|
+
/** `--hook-timeout`'s default. One round trip, not one tool call. */
|
|
21
|
+
defaultTimeoutMs: 5_000,
|
|
22
|
+
/** `--hook-timeout`'s upper bound. */
|
|
23
|
+
maxTimeoutMs: 60_000,
|
|
24
|
+
/**
|
|
25
|
+
* The ceiling `--on-hook-timeout allow` clamps to (section 2.4.8). A tool may be
|
|
26
|
+
* asked twice (`tool_pre` + `tool_post`), and `ToolExecutor`'s budget is 120 000 ms:
|
|
27
|
+
* two allowed round trips must not be able to consume half of it.
|
|
28
|
+
*/
|
|
29
|
+
allowModeMaxTimeoutMs: 30_000,
|
|
30
|
+
/** Hook round trips in flight at once. Beyond this the hook is saturated, not queued. */
|
|
31
|
+
maxConcurrentRequests: 16,
|
|
32
|
+
/** `prompt_pre` rewrite / `turn_start` inject text length. */
|
|
33
|
+
maxInjectChars: 8_192,
|
|
34
|
+
/** `tool_post` replacement content length, in characters of its text. */
|
|
35
|
+
maxContentChars: 32_768,
|
|
36
|
+
/** Injections per request, so a hook cannot drive the model round in a loop. */
|
|
37
|
+
maxInjectionsPerRequest: 32,
|
|
38
|
+
/** How much of a hook's own `reason`/`message` survives into model-visible text. */
|
|
39
|
+
maxReasonChars: 512,
|
|
40
|
+
};
|
|
41
|
+
export const HOOK_POINTS = [
|
|
42
|
+
'prompt_pre',
|
|
43
|
+
'turn_start',
|
|
44
|
+
'tool_pre',
|
|
45
|
+
'tool_post',
|
|
46
|
+
];
|
|
47
|
+
export function isHookPoint(value) {
|
|
48
|
+
return typeof value === 'string' && HOOK_POINTS.includes(value);
|
|
49
|
+
}
|