@sema-agent/core 5.55.0 → 5.56.0
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 +66 -0
- package/dist/agents/send-message-tool.js +48 -2
- package/dist/agents/subagent.js +250 -89
- package/dist/core/auto-compaction.d.ts +17 -4
- package/dist/core/auto-compaction.js +3 -0
- package/dist/core/context-edit.d.ts +55 -6
- package/dist/core/context-edit.js +12 -1
- package/dist/core/hooks.d.ts +293 -11
- package/dist/core/hooks.js +158 -11
- package/dist/core/human-input-projection.d.ts +20 -2
- package/dist/core/human-input-projection.js +9 -0
- package/dist/core/permission-rules.d.ts +23 -15
- package/dist/core/permission-rules.js +40 -31
- package/dist/core/runner/prepare-task.d.ts +8 -0
- package/dist/core/runner/prepare-task.js +34 -23
- package/dist/core/runner/runtask.js +158 -21
- package/dist/core/runner/session-rule-policy.js +5 -5
- package/dist/core/session-reconcile.d.ts +32 -0
- package/dist/core/session-reconcile.js +15 -0
- package/dist/core/task-notification.d.ts +34 -7
- package/dist/core/task-notification.js +11 -1
- package/dist/core/task-registry-agent.d.ts +20 -3
- package/dist/core/task-registry-agent.js +31 -2
- package/dist/core/tool-policy.d.ts +14 -9
- package/dist/core/tool-policy.js +27 -22
- package/dist/core/types.d.ts +37 -11
- package/dist/core/untrusted-text.js +8 -0
- package/dist/engine/compaction/compaction.d.ts +77 -7
- package/dist/engine/compaction/compaction.js +98 -9
- package/dist/engine/compaction/utils.d.ts +4 -0
- package/dist/engine/compaction/utils.js +6 -0
- package/dist/engine/harness/agent-harness.d.ts +84 -0
- package/dist/engine/harness/agent-harness.js +88 -12
- package/dist/engine/harness/messages.d.ts +4 -2
- package/dist/engine/harness/messages.js +7 -2
- package/dist/engine/harness/types.d.ts +11 -5
- package/dist/engine/loop/types.d.ts +7 -0
- package/dist/engine/session/import-validate.js +10 -0
- package/dist/engine/session/session.js +2 -2
- package/dist/orchestration/run-spec.js +8 -1
- package/dist/prompts/default.d.ts +10 -4
- package/package.json +1 -1
|
@@ -14,11 +14,18 @@ export declare const EDIT_FRACTION = 0.7;
|
|
|
14
14
|
* effectiveWindow = autocompactWindow − min(maxOutputTokens, 20000) (CC `Nye`, cap `uMd`)
|
|
15
15
|
* trigger = effectiveWindow − 13000 (CC `dSo`, buffer `rMd`)
|
|
16
16
|
* i.e. trigger = W − 33000 for every model whose max output is ≥ 20k: a 200k window triggers at
|
|
17
|
-
* 167000 (83.5% of nominal), a 1M window at 967000.
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
17
|
+
* 167000 (83.5% of nominal), a 1M window at 967000.
|
|
18
|
+
*
|
|
19
|
+
* 1M DIVERGENCE, stated as fact rather than as an equivalence (anchoring re-check 2026-08-23): CC
|
|
20
|
+
* carries a MODEL-DEFAULT autocompact-window table (2.1.223 `j3u` :242918 — `claude-sonnet-5`
|
|
21
|
+
* default 967000, and since 223 a surface dimension: `remote_cowork` / `local-agent` 500000), so a
|
|
22
|
+
* 1M sonnet-5 gets 967000 as its window and 934000 as its trigger WITHOUT anyone configuring
|
|
23
|
+
* anything. We ship no such table: a model declaring only `contextWindow: 1e6` triggers at 967000
|
|
24
|
+
* here, 33000 LATER than CC. `model.autoCompactTokens = 967000` reproduces CC's number — but that
|
|
25
|
+
* is an EMBEDDER action against CC's product default, not the same posture, and the choice not to
|
|
26
|
+
* follow 934000 is deliberate (design/146 §1.3 argues against living on that thin a cushion). The
|
|
27
|
+
* knob itself is real and unchanged: `autoCompactTokens` lowers only this trigger-side window while
|
|
28
|
+
* the guard and physical request budgeting stay on the 1M window — CC's dual-window shape. We take the
|
|
22
29
|
* 20000 cap branch unconditionally (flat 33000), i.e. the `min(maxOutputTokens, 20000)` branch is
|
|
23
30
|
* deliberately NOT ported: the deduction is exact parity only for models with max output ≥ 20k. A
|
|
24
31
|
* model with a smaller max output would deduct less in CC (trigger LATER); we deduct the full
|
|
@@ -51,11 +58,53 @@ export declare function contextEditFrontier(window: number): number;
|
|
|
51
58
|
* PowerShell is kept for CC fidelity even though sema does not mount it. Overridable per call via
|
|
52
59
|
* {@link ContextEditOptions.compactableTools} for custom-tool-heavy embedders.
|
|
53
60
|
*/
|
|
61
|
+
/**
|
|
62
|
+
* How many most-recent CANDIDATE tool results keep their content when the stale-result pass runs.
|
|
63
|
+
*
|
|
64
|
+
* 3, UNCHANGED — and the fact that CC's corresponding number is 5 (2.1.223 `uAp`, and the same value
|
|
65
|
+
* on CC 88's `timeBasedMCConfig`, so it is stable across both corpus generations) is deliberately NOT
|
|
66
|
+
* adopted here. Two reasons, in order of weight:
|
|
67
|
+
*
|
|
68
|
+
* 1. MEASURED: widening the window removes this pass's only lever in the shape where a terminal
|
|
69
|
+
* parallel batch holds exactly as many large results as the window keeps. Probed on a 200k model
|
|
70
|
+
* with a 140k usage anchor and five 36k-char Bash results: at 3 the pass clears two and the
|
|
71
|
+
* request lands at ~167k, UNDER the 177k request guard; at 5 nothing is clearable, the request
|
|
72
|
+
* stays at 185k, and the guard cannot recover it either — `trimToBudget` must turn-align back to
|
|
73
|
+
* the emitting assistant, so the whole batch is retained. The window is count-based on both
|
|
74
|
+
* sides (CC's `slice(-keepRecent)` too), so this ceiling exists at every value; 5 simply widens
|
|
75
|
+
* the band that reaches it.
|
|
76
|
+
* 2. SEAT: the two numbers do not govern the same machine. CC's keep-recent clear runs ONLY on the
|
|
77
|
+
* `context_hint` rejection leg — gated off by default — and behind a hard "saves ≥ 20000 tokens
|
|
78
|
+
* or don't bother" test, with request REFUSAL as the real backstop. Ours is on by default, is
|
|
79
|
+
* the only reduction between the frontier and the guard, and its backstop drops messages instead
|
|
80
|
+
* of refusing. Copying a constant across that difference is the "same name, different question"
|
|
81
|
+
* mistake, not parity.
|
|
82
|
+
*
|
|
83
|
+
* So the VALUE is an open adjudication (recorded with the probe above), while the two things CC
|
|
84
|
+
* unambiguously answers — the floor and the candidate-scoped window — are followed exactly.
|
|
85
|
+
*/
|
|
86
|
+
export declare const DEFAULT_KEEP_RECENT_TOOL_RESULTS = 3;
|
|
87
|
+
/** Minimum kept results — CC `EUs` :397710 `Math.max(1, keepRecent)`. See
|
|
88
|
+
* {@link ContextEditOptions.keepRecentToolResults} for why both degenerate ends are unusable. */
|
|
89
|
+
export declare const MIN_KEEP_RECENT_TOOL_RESULTS = 1;
|
|
54
90
|
export declare const COMPACTABLE_TOOLS: ReadonlySet<string>;
|
|
55
91
|
export interface ContextEditOptions {
|
|
56
92
|
/** Start clearing once estimated context tokens exceed this. */
|
|
57
93
|
budgetTokens: number;
|
|
58
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* Always keep the content of this many most-recent CANDIDATE tool results (candidates = results
|
|
96
|
+
* from {@link compactableTools}). Default {@link DEFAULT_KEEP_RECENT_TOOL_RESULTS} = 3 — see that
|
|
97
|
+
* constant for why CC's 5 is not adopted here.
|
|
98
|
+
*
|
|
99
|
+
* Floored at 1, which IS CC's rule (`EUs` `Math.max(1, keepRecent)`), whose own source note gives
|
|
100
|
+
* the reason: 0 leaves the model with zero working tool context, and in CC's `slice(-0)` spelling
|
|
101
|
+
* it degenerates the other way into keeping everything — two unusable extremes for one value.
|
|
102
|
+
* Ours degenerated identically (a 0 or negative `keep` cleared EVERY candidate). Values that are
|
|
103
|
+
* not a non-negative safe integer are REFUSED rather than floored: a NaN used to make this whole
|
|
104
|
+
* defense silently inert (`slice(0, NaN)` = clear nothing), which is the one outcome a context
|
|
105
|
+
* defense must never reach quietly, and there is no config door upstream to announce at — this
|
|
106
|
+
* option only ever arrives as a direct argument from an embedder.
|
|
107
|
+
*/
|
|
59
108
|
keepRecentToolResults?: number;
|
|
60
109
|
/**
|
|
61
110
|
* roadmap #6② (CC contentReplacementState parity): when set, a result's FULL TEXT is offloaded to
|
|
@@ -27,6 +27,17 @@ export const COMPACTION_TRIGGER_BUFFER_TOKENS = 13000;
|
|
|
27
27
|
export function contextEditFrontier(window) {
|
|
28
28
|
return Math.max(window - (CONTEXT_OUTPUT_RESERVE_TOKENS + COMPACTION_TRIGGER_BUFFER_TOKENS), Math.floor(window * EDIT_FRACTION));
|
|
29
29
|
}
|
|
30
|
+
export const DEFAULT_KEEP_RECENT_TOOL_RESULTS = 3;
|
|
31
|
+
export const MIN_KEEP_RECENT_TOOL_RESULTS = 1;
|
|
32
|
+
function resolveKeepRecentToolResults(value) {
|
|
33
|
+
if (value === undefined)
|
|
34
|
+
return DEFAULT_KEEP_RECENT_TOOL_RESULTS;
|
|
35
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
36
|
+
throw new TypeError(`keepRecentToolResults must be a non-negative safe integer (got ${String(value)}); ` +
|
|
37
|
+
`omit it for the default ${DEFAULT_KEEP_RECENT_TOOL_RESULTS}`);
|
|
38
|
+
}
|
|
39
|
+
return Math.max(MIN_KEEP_RECENT_TOOL_RESULTS, value);
|
|
40
|
+
}
|
|
30
41
|
export const COMPACTABLE_TOOLS = new Set([
|
|
31
42
|
"Read",
|
|
32
43
|
"Bash",
|
|
@@ -74,7 +85,7 @@ export function clearStaleToolResults(messages, opts) {
|
|
|
74
85
|
if (total <= opts.budgetTokens) {
|
|
75
86
|
return messages;
|
|
76
87
|
}
|
|
77
|
-
const keep = opts.keepRecentToolResults
|
|
88
|
+
const keep = resolveKeepRecentToolResults(opts.keepRecentToolResults);
|
|
78
89
|
const compactable = opts.compactableTools ?? COMPACTABLE_TOOLS;
|
|
79
90
|
const toolResultCandidates = messages.flatMap((m, i) => isToolResult(m) && !isCleared(m) && compactable.has(m.toolName) ? [{ idx: i, target: m }] : []);
|
|
80
91
|
const clearable = toolResultCandidates.slice(0, Math.max(0, toolResultCandidates.length - keep));
|
package/dist/core/hooks.d.ts
CHANGED
|
@@ -25,6 +25,23 @@ import type { WiringLegKind } from "./wiring-manifest.js";
|
|
|
25
25
|
*
|
|
26
26
|
* All feedback the model should see (a block reason, injected context) is wrapped as a
|
|
27
27
|
* `<system-reminder>` via {@link formatHookFeedback} so the model can read and self-correct.
|
|
28
|
+
*
|
|
29
|
+
* **FAILURE POSTURE, and a correction to a widely-quoted anchor (recorded 2026-08-23).** Three repos
|
|
30
|
+
* have reasoned from the sentence "CC fails open on hook failures, exit code 2 excepted". That sentence
|
|
31
|
+
* is TRUE OF ONE LANE and false as a general statement, and the difference lands exactly on this seam.
|
|
32
|
+
* CC runs hooks of seven kinds; the `command` kind is a shell script, and for it a spawn failure, a
|
|
33
|
+
* non-zero exit and a per-hook timeout are all non-blocking — the call proceeds. But CC ALSO has an
|
|
34
|
+
* in-process CALLBACK lane, which is the one structurally equivalent to this interface, and there a
|
|
35
|
+
* PreToolUse callback that throws or times out is FAIL-CLOSED, with its own message stating that the
|
|
36
|
+
* tool call was not executed. (Two further details the short sentence hides: the decision channel is
|
|
37
|
+
* stdout JSON, with the exit code only a fallback — so exit 0 can block and a non-zero non-2 exit with
|
|
38
|
+
* valid JSON does not; and the callback lane's UserPromptSubmit timeout BLOCKS the prompt.)
|
|
39
|
+
*
|
|
40
|
+
* So this seam's fail-closed postures are not a sema invention to be justified against CC — they are
|
|
41
|
+
* the same posture CC takes on the same kind of lane: a `preToolUse` that throws or does not answer
|
|
42
|
+
* refuses the call, a `userPromptSubmit` that throws or does not answer refuses the prompt, and the
|
|
43
|
+
* OBSERVATION seats swallow and disclose because an observer has nothing to fail closed about. Anyone
|
|
44
|
+
* quoting the short sentence to argue this layer "should" fail open is quoting the script lane.
|
|
28
45
|
*/
|
|
29
46
|
export interface Hooks {
|
|
30
47
|
preToolUse?(toolName: string, input: unknown, ctx: HookToolContext): PreToolUseResult | undefined | Promise<PreToolUseResult | undefined>;
|
|
@@ -159,9 +176,179 @@ export interface Hooks {
|
|
|
159
176
|
* in for CC `decision_reason_type`): no `retry` capability (recorded — retry semantics vs our adjudicate
|
|
160
177
|
* chain need their own design), no additionalContext. A throwing callback is swallowed + traced via
|
|
161
178
|
* `onError(phase:"hook")` — an observer bug must never alter the deny outcome (design/134 discipline).
|
|
179
|
+
*
|
|
180
|
+
* **HOW WIDE THIS SEAT IS, stated honestly (anchoring re-check 2026-08-23).** The exclusion sentence
|
|
181
|
+
* above is quoted from the SDK's TYPE-LEVEL documentation, and the whole R5 scoping argument was
|
|
182
|
+
* built on it without ever being checked against a shipped bundle. Against CC 2.1.223's runtime the
|
|
183
|
+
* picture is different in a way that matters for how this comment may be USED: CC emits its
|
|
184
|
+
* PermissionDenied event from exactly ONE site, guarded to the auto-mode CLASSIFIER's refusals —
|
|
185
|
+
* a rule deny, a mode deny and a headless auto-deny emit nothing at all. So "a hook's own deny is
|
|
186
|
+
* excluded" is TRIVIALLY true there (almost everything is excluded), and it must not be read as
|
|
187
|
+
* evidence that this seat's TRIGGER SURFACE matches CC's. It does not: sema fires on four engine
|
|
188
|
+
* emissions with seven `source` values, deliberately WIDER. The width is the useful direction for an
|
|
189
|
+
* observation seat — the blind spot R3 named is real and a narrower seat would not close it — and it
|
|
190
|
+
* is a divergence rather than a parity claim. The exclusion itself stays, on its own merit: a hook's
|
|
191
|
+
* deny is the HOOK's decision, and this seat exists to attribute the ENGINE's.
|
|
162
192
|
*/
|
|
163
193
|
permissionDenied?(payload: PermissionDeniedPayload): void | Promise<void>;
|
|
194
|
+
/**
|
|
195
|
+
* The TIME BOUND every seat on this object runs under, in milliseconds. Absent ⇒
|
|
196
|
+
* {@link DEFAULT_HOOK_TIMEOUT_MS}. It is the analog of the CC hook record's own `timeout` field —
|
|
197
|
+
* declared once per hook record there, once per hook OBJECT here, because this seam's record IS the
|
|
198
|
+
* object. The unit is ms rather than CC's seconds: every other deadline this library takes ends in
|
|
199
|
+
* `Ms`, and one unit per codebase beats one unit per upstream.
|
|
200
|
+
*
|
|
201
|
+
* **Why the seam has a bound at all.** Every callback above is DEPLOYMENT code awaited on the
|
|
202
|
+
* engine's critical path, and one that never returns used to stop the whole run — the tool gate
|
|
203
|
+
* awaited `preToolUse` bare, so a hung screening face left the call unsettled and the task's own
|
|
204
|
+
* timeout/cancel could not end it. The diagnosis was an internal asymmetry rather than a guess: the
|
|
205
|
+
* SAME callback installed as an inherited constraint on a delegated child was already bounded, because
|
|
206
|
+
* that path folds it into a `ToolPolicy` and the policy wait is abort-raced — and the sibling seam in
|
|
207
|
+
* this very file (`reversibilityProbe`) had already been given a default deadline for the identical
|
|
208
|
+
* reason. Policies were bounded, probes were bounded, hooks were not.
|
|
209
|
+
*
|
|
210
|
+
* **What a bound does NOT change.** A seat that answers keeps its answer, byte for byte; a seat that
|
|
211
|
+
* throws keeps its existing crash posture (this member changes nothing about a throw). The bound is
|
|
212
|
+
* consulted only when a seat has not answered yet, and what happens then is per-seat and stated on
|
|
213
|
+
* each member's own contract — decision seats fail CLOSED (`preToolUse` refuses the call the way a
|
|
214
|
+
* crash does; `userPromptSubmit` refuses the prompt), observation seats are ABANDONED and the engine
|
|
215
|
+
* proceeds as if the seat had returned nothing. Either way the deployment is told on its `onError`
|
|
216
|
+
* lane, because a silently-dropped seat is the failure mode this member exists to make visible.
|
|
217
|
+
*
|
|
218
|
+
* **The seat also gets a signal.** An expired wait would otherwise leave the callback running with
|
|
219
|
+
* nobody reading its answer — an orphan of the deployment's own making. So every context carries an
|
|
220
|
+
* optional `signal` that aborts when this bound elapses (or the task aborts), and an implementation
|
|
221
|
+
* that honors it can cancel the read it was waiting on. Honoring it is optional; a seat that ignores
|
|
222
|
+
* it is exactly as bounded as before, the engine simply stops waiting.
|
|
223
|
+
*
|
|
224
|
+
* **WHAT THE BOUND IS ON, stated narrowly because the difference is not academic.** It bounds the
|
|
225
|
+
* engine's WAIT, not the callback's CPU. A hook that blocks the thread SYNCHRONOUSLY — a spin loop, a
|
|
226
|
+
* synchronous exec, a synchronous parse of something enormous — cannot be preempted by this bound or
|
|
227
|
+
* by any other in-process mechanism, because the timer that would fire is queued behind the code that
|
|
228
|
+
* is not yielding; the ordering of the timer's installation makes no difference to that. In-process
|
|
229
|
+
* is the seam's whole premise (it is what makes a hook able to rewrite args and return a verdict),
|
|
230
|
+
* and preemption is what it trades away. A deployment that must survive a hook it does not trust to
|
|
231
|
+
* yield runs that hook somewhere with its own thread — a worker, a process, a service — and awaits
|
|
232
|
+
* the result from here. What this member DOES bound is every asynchronous wait, which is where the
|
|
233
|
+
* failure this was written for lives: a callback that awaits an unreachable host, a lock, a queue.
|
|
234
|
+
*
|
|
235
|
+
* **Value discipline** (bad values are loud, never silently reinterpreted): a non-finite, negative, or
|
|
236
|
+
* above-`setTimeout`-ceiling (2147483647) value is REFUSED to the default and reported on the
|
|
237
|
+
* deployment's error lane — the same treatment, and the same ceiling, the gate's `approvalTimeoutMs`
|
|
238
|
+
* seat gives its own garbage. `0` is honored as written (every seat expires immediately), because a
|
|
239
|
+
* deployment that writes zero has said something, and guessing that it meant "unbounded" would be the
|
|
240
|
+
* one reading this member cannot support.
|
|
241
|
+
*/
|
|
242
|
+
timeoutMs?: number;
|
|
164
243
|
}
|
|
244
|
+
/**
|
|
245
|
+
* The bound a {@link Hooks} seat runs under when the deployment declares none ({@link Hooks.timeoutMs}).
|
|
246
|
+
*
|
|
247
|
+
* The value is CC's own hook default (600s) rather than a number invented here. It is deliberately
|
|
248
|
+
* GENEROUS: the seat bound's job is to make a wedge terminal, not to police how long a legitimate hook
|
|
249
|
+
* may think — the task's own abort signal is what ends a hung DECISION seat promptly on a run that has a
|
|
250
|
+
* deadline, and this ceiling is what ends it on a run that does not. A deployment whose hooks should
|
|
251
|
+
* answer in a second says so with `timeoutMs`.
|
|
252
|
+
*/
|
|
253
|
+
export declare const DEFAULT_HOOK_TIMEOUT_MS = 600000;
|
|
254
|
+
/**
|
|
255
|
+
* How long an observation seat still gets when the task is cancelled `elapsed` ms into its wait — the
|
|
256
|
+
* grace, capped by what is LEFT of the declared deadline.
|
|
257
|
+
*
|
|
258
|
+
* The `remaining` argument, not the whole deadline, is the correction: arming a fresh
|
|
259
|
+
* `min(GRACE, timeoutMs)` from the abort moment restarts the clock, so an abort landing near the
|
|
260
|
+
* deadline pushes the seat's absolute end PAST the number the deployment declared — up to double it for
|
|
261
|
+
* a sub-second bound — on precisely the cancelled-task settlement path the grace exists to shorten. The
|
|
262
|
+
* two contract sentences this file already carries ("never an extension", "a deployment that declared a
|
|
263
|
+
* SHORTER `timeoutMs` keeps its own number") were true only in the aborted-on-entry case, where elapsed
|
|
264
|
+
* is zero and the two formulas coincide. Now they are true everywhere.
|
|
265
|
+
*
|
|
266
|
+
* Clamped at zero: a deadline already spent is no grace at all, not a negative timer.
|
|
267
|
+
*/
|
|
268
|
+
export declare function postAbortGraceMs(remainingMs: number): number;
|
|
269
|
+
/**
|
|
270
|
+
* Read {@link Hooks.timeoutMs} as a usable deadline: the declared value when it is one, the default
|
|
271
|
+
* when it is absent, and the default LOUDLY when it is garbage (bad-value loudness — a knob's bad value
|
|
272
|
+
* must never resolve silently to the default).
|
|
273
|
+
*
|
|
274
|
+
* `owner` is the record the value was read off; passing it bounds the disclosure to once per record
|
|
275
|
+
* (see {@link badHookTimeoutReported}). Omit it where the caller already bounds its own channel.
|
|
276
|
+
*/
|
|
277
|
+
export declare function resolveHookTimeoutMs(supplied: number | undefined, report?: (err: unknown) => void, owner?: object): number;
|
|
278
|
+
/** The ten {@link Hooks} callback seats, named so a bound wait can say WHICH one did not answer. */
|
|
279
|
+
export type HookSeatName = "preToolUse" | "postToolUse" | "postToolUseFailure" | "postToolBatch" | "userPromptSubmit" | "stop" | "stopFailure" | "preCompact" | "postCompact" | "permissionDenied";
|
|
280
|
+
/** What one bounded seat invocation produced: the seat's own value, or the fact that the wait ended
|
|
281
|
+
* first. A seat that THREW is neither — the throw propagates out of {@link runHookSeat} unchanged, so
|
|
282
|
+
* each site's existing crash arm keeps owning it. */
|
|
283
|
+
export type HookSeatOutcome<T> = {
|
|
284
|
+
readonly expired: false;
|
|
285
|
+
readonly value: T;
|
|
286
|
+
} | {
|
|
287
|
+
readonly expired: true;
|
|
288
|
+
readonly cause: "timeout" | "aborted";
|
|
289
|
+
};
|
|
290
|
+
/** How one seat invocation is bounded. */
|
|
291
|
+
export interface HookSeatBound {
|
|
292
|
+
/** The deployment's declared bound ({@link Hooks.timeoutMs}); re-validated here, so a caller that
|
|
293
|
+
* already validated pays nothing and a caller that did not cannot pass garbage through. */
|
|
294
|
+
timeoutMs?: number;
|
|
295
|
+
/** The task abort signal. It is ALWAYS forwarded to the seat (so an implementation can cancel its own
|
|
296
|
+
* underlying wait) and it ALWAYS shortens the wait — the two seat kinds differ only in HOW MUCH.
|
|
297
|
+
* Supply it wherever the caller has one: a seat with no signal is bounded by its deadline alone, and
|
|
298
|
+
* a ten-minute default deadline is not a cancellation story. */
|
|
299
|
+
signal?: AbortSignal;
|
|
300
|
+
/** True for a seat whose answer is MOOT once the task is ending — the DECISION seats. Those settle as
|
|
301
|
+
* `aborted` the instant the task is cancelled. An observation seat leaves this false on purpose:
|
|
302
|
+
* `postToolUseFailure` fires precisely BECAUSE the task aborted (`isInterrupt`), and cancelling it
|
|
303
|
+
* outright would delete the observation the seat exists to deliver — it gets
|
|
304
|
+
* {@link POST_ABORT_GRACE_MS} instead of the full deadline. */
|
|
305
|
+
abortEnds?: boolean;
|
|
306
|
+
/** Where a bad {@link timeoutMs} is disclosed. The EXPIRY itself is not reported here — each site
|
|
307
|
+
* words its own, because the sentence a deployment needs includes what the engine then did. */
|
|
308
|
+
onBadTimeout?: (err: unknown) => void;
|
|
309
|
+
/** The hook record the bound was read off, for once-per-record bad-value disclosure. */
|
|
310
|
+
owner?: object;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Run ONE {@link Hooks} seat invocation under its bound — the single home of "a deployment callback
|
|
314
|
+
* gets a deadline", so the ten seats cannot grow ten dialects of one wait.
|
|
315
|
+
*
|
|
316
|
+
* Shape notes, each one load-bearing:
|
|
317
|
+
* · the seat is invoked SYNCHRONOUSLY, in this turn, exactly where the bare `await` used to be — a
|
|
318
|
+
* callback that reads its own mutable state before its first `await` must see the turn it always saw;
|
|
319
|
+
* · a SYNCHRONOUS throw propagates unchanged (it never reached a deadline, and the site's crash arm is
|
|
320
|
+
* the contract for it), and so does an asynchronous rejection;
|
|
321
|
+
* · the seat's signal fires when — and ONLY when — this function stops waiting, and its promise is then
|
|
322
|
+
* abandoned with a rejection sink attached (an abandoned seat that fails later must not surface as an
|
|
323
|
+
* unhandled rejection somewhere else). The task's own signal is never forwarded raw: for an
|
|
324
|
+
* OBSERVATION seat the task being cancelled is not a reason to stop, it is the reason the seat is
|
|
325
|
+
* firing at all;
|
|
326
|
+
* · the TASK signal ends the wait on both kinds of seat, differently. A DECISION seat
|
|
327
|
+
* ({@link HookSeatBound.abortEnds}) settles as `aborted` the moment the task is cancelled, INCLUDING
|
|
328
|
+
* when it was already cancelled on entry — a verdict about a call that will not run decides nothing,
|
|
329
|
+
* and waiting the full deadline for one holds a cancelled task open for as long as the deadline says.
|
|
330
|
+
* On the entry case the callback is still INVOKED (an audit face keeps seeing the attempt) and its
|
|
331
|
+
* answer is discarded: settling on the cancellation rather than on whichever of the two happened to
|
|
332
|
+
* land first is what makes the arm a rule instead of a race.
|
|
333
|
+
* An OBSERVATION seat is not cancelled by the abort — it fires precisely BECAUSE the task aborted
|
|
334
|
+
* (`isInterrupt`), and racing it against that same abort would delete the observation it exists to
|
|
335
|
+
* deliver — but it does not get the full deadline either: the abort collapses its wait to
|
|
336
|
+
* {@link POST_ABORT_GRACE_MS}. Prompt observers still land; a hung one can no longer hold a cancelled
|
|
337
|
+
* task's settlement for the length of the whole bound.
|
|
338
|
+
*/
|
|
339
|
+
export declare function runHookSeat<T>(seat: HookSeatName, bound: HookSeatBound, call: (signal: AbortSignal) => T | Promise<T>): Promise<HookSeatOutcome<T>>;
|
|
340
|
+
/**
|
|
341
|
+
* The ONE wording home for "the engine stopped waiting for a hook seat", so the ten sites cannot drift
|
|
342
|
+
* into ten sentences about one fact. `consequence` is the half only the site knows: what the engine did
|
|
343
|
+
* instead.
|
|
344
|
+
*
|
|
345
|
+
* The CAUSE is named, not glossed. A deadline that elapsed and a task that was cancelled are different
|
|
346
|
+
* facts to whoever reads this — the first says a hook is too slow or stuck and wants fixing, the second
|
|
347
|
+
* says nothing about the hook at all — and one sentence covering both would have to be false about one
|
|
348
|
+
* of them. (It was: the first version of this said "did not answer within its Nms bound" on every
|
|
349
|
+
* expiry, including the ones where the bound was never reached.)
|
|
350
|
+
*/
|
|
351
|
+
export declare function hookSeatExpiredError(seat: HookSeatName, timeoutMs: number, cause: "timeout" | "aborted", consequence: string): Error;
|
|
165
352
|
/**
|
|
166
353
|
* Where a {@link Hooks.permissionDenied} deny came from — our gate-source enum standing in for CC's
|
|
167
354
|
* `decision_reason_type` ('classifier'|'asyncAgent'|'mode'|'rule'), named after OUR adjudicate-chain
|
|
@@ -170,7 +357,9 @@ export interface Hooks {
|
|
|
170
357
|
* (headless auto-deny / approver said no / approval aborted).
|
|
171
358
|
* - `"hook"` — a PreToolUse hook `ask` (folded allow→ask) resolved to deny, OR the engine's fail-closed
|
|
172
359
|
* interception of a hook that THREW (RB-442). (A hook's own `deny` short-circuit is EXCLUDED from the
|
|
173
|
-
* event
|
|
360
|
+
* event — a decision the hook made; the crash interception is a decision the ENGINE made. The
|
|
361
|
+
* exclusion is ours on that merit; see {@link Hooks.permissionDenied} for why it must NOT be read
|
|
362
|
+
* as a parity claim about this seat's trigger surface.)
|
|
174
363
|
* - `"safety"` — the gate's deterministic egress/irreversibility tighten (design/70 / design/77 §4)
|
|
175
364
|
* raised the `ask` that resolved to deny, from an explicit per-tool mark.
|
|
176
365
|
* - `"shellGate"` — same tighten-deny, but the tool's irreversibility tier was installed by the COARSE
|
|
@@ -205,6 +394,10 @@ export interface PermissionDeniedPayload {
|
|
|
205
394
|
* screen; a policy's direct deny, a hook deny, and the crash/plan-mode/compliance emissions carry
|
|
206
395
|
* none. See {@link import("./tool-policy.js").AskDenyResolution}. */
|
|
207
396
|
resolution?: import("./tool-policy.js").AskDenyResolution;
|
|
397
|
+
/** {@link HookSeatSignal} — this invocation's own abort signal. On an OBSERVATION seat the deny has
|
|
398
|
+
* already happened and nothing this callback does can change it, so the signal says exactly one
|
|
399
|
+
* thing: stop reading, nobody is waiting for your answer any more. */
|
|
400
|
+
signal?: AbortSignal;
|
|
208
401
|
}
|
|
209
402
|
/**
|
|
210
403
|
* 1.256 复审 MED-1 — observe-only payload isolation for {@link Hooks.permissionDenied}: clone the tool
|
|
@@ -227,7 +420,28 @@ export interface StopFailureContext {
|
|
|
227
420
|
errorKind?: string;
|
|
228
421
|
/** Turns completed when the failure ended the run. */
|
|
229
422
|
turns: number;
|
|
423
|
+
/** {@link HookSeatSignal} — this invocation's own abort signal. */
|
|
424
|
+
signal?: AbortSignal;
|
|
230
425
|
}
|
|
426
|
+
/**
|
|
427
|
+
* The abort signal every hook seat's context carries (`ctx.signal`), documented once here and
|
|
428
|
+
* referenced from each seat.
|
|
429
|
+
*
|
|
430
|
+
* It carries exactly one meaning: **the engine has stopped waiting for this invocation.** It is not the
|
|
431
|
+
* task's abort signal and it never fires early on the task's behalf — a DECISION seat's wait ends when
|
|
432
|
+
* the task is cancelled, so its signal fires then; an OBSERVATION seat's wait ends at its deadline or
|
|
433
|
+
* after the post-abort grace, so its signal fires there and NOT at the cancellation itself. The
|
|
434
|
+
* difference is deliberate: a `postToolUseFailure` fires precisely BECAUSE the task aborted, and a
|
|
435
|
+
* signal that went off at the cancellation would tell a cooperative observer to abandon the very
|
|
436
|
+
* observation it was invoked to deliver.
|
|
437
|
+
*
|
|
438
|
+
* Honoring it is optional and it changes nothing about the engine's own behavior — the engine stops
|
|
439
|
+
* waiting either way. What it is for is the deployment's side of that: a seat that was awaiting a
|
|
440
|
+
* network read, a lock or a subprocess can cancel it instead of leaving an orphan running with nobody
|
|
441
|
+
* left to read its answer. Absent when a host drives a seat outside the engine's own invocation (no
|
|
442
|
+
* bound, nothing to signal).
|
|
443
|
+
*/
|
|
444
|
+
export type HookSeatSignal = AbortSignal;
|
|
231
445
|
/** The failed tool execution a PostToolUseFailure callback inspects (design/134). */
|
|
232
446
|
export interface HookToolFailure {
|
|
233
447
|
/** Model-facing error text of the failed call (the error tool result's text content, joined). */
|
|
@@ -273,6 +487,8 @@ export interface PreCompactContext {
|
|
|
273
487
|
trigger: "auto" | "manual" | "forced";
|
|
274
488
|
/** The summarization instructions in effect (spec/deployment-level), when set. */
|
|
275
489
|
customInstructions?: string;
|
|
490
|
+
/** {@link HookSeatSignal} — this invocation's own abort signal. */
|
|
491
|
+
signal?: AbortSignal;
|
|
276
492
|
}
|
|
277
493
|
/** A PreCompact result: skip this compaction (auto/manual only) and/or extend the summary instructions. */
|
|
278
494
|
export interface PreCompactResult {
|
|
@@ -293,6 +509,8 @@ export interface PostCompactContext {
|
|
|
293
509
|
summary: string;
|
|
294
510
|
tokensBefore?: number;
|
|
295
511
|
tokensAfter?: number;
|
|
512
|
+
/** {@link HookSeatSignal} — this invocation's own abort signal. */
|
|
513
|
+
signal?: AbortSignal;
|
|
296
514
|
}
|
|
297
515
|
/** Context for the {@link Hooks.stop} hook (CC `stop_hook_active` parity). */
|
|
298
516
|
export interface StopHookContext {
|
|
@@ -326,6 +544,8 @@ export interface StopHookContext {
|
|
|
326
544
|
* Absent when the runner cannot supply one (a stop gate wired without a session).
|
|
327
545
|
*/
|
|
328
546
|
getBranch?: () => Promise<readonly SessionTreeEntry[]>;
|
|
547
|
+
/** {@link HookSeatSignal} — this invocation's own abort signal. */
|
|
548
|
+
signal?: AbortSignal;
|
|
329
549
|
}
|
|
330
550
|
/** A Stop hook result: block the run from ending, with a model-readable reason. */
|
|
331
551
|
export interface StopHookResult {
|
|
@@ -333,11 +553,20 @@ export interface StopHookResult {
|
|
|
333
553
|
* model gets another turn to address it. */
|
|
334
554
|
block?: string;
|
|
335
555
|
/**
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
556
|
+
* Extra model-readable context injected at the stop point — DECOUPLED from `block`. When present it
|
|
557
|
+
* is injected (as a `<system-reminder>`, neutral framing: "Stop hook additional context: …") whether
|
|
558
|
+
* or not the hook blocked, and an `additionalContext` WITHOUT `block` still continues the run one
|
|
559
|
+
* more turn.
|
|
560
|
+
*
|
|
561
|
+
* **It DOES count toward the consecutive-block cap, and it never resets it.** This sentence used to
|
|
562
|
+
* say the opposite ("the cap is driven by `block` alone", cited to CC 2.1.201), and it was wrong on
|
|
563
|
+
* both halves. Re-read against CC: the `additionalContext` branch pushes into the SAME array as the
|
|
564
|
+
* blocking-error branch, the loop counts that array's length against the cap, and CC never resets
|
|
565
|
+
* mid-run. The engine was fixed to match; the sentence here was not, so the module that DEFINES this
|
|
566
|
+
* seat and the module that IMPLEMENTS it stated opposite behaviors for years. What the cap bounds is
|
|
567
|
+
* "the run did not end when it otherwise would have", and a context-only push-back is exactly that —
|
|
568
|
+
* a hook whose most common failure mode is "no parseable verdict, here is some text" would otherwise
|
|
569
|
+
* run to `maxTurns` looking busy rather than stuck.
|
|
341
570
|
*/
|
|
342
571
|
additionalContext?: string;
|
|
343
572
|
}
|
|
@@ -524,6 +753,28 @@ export interface HookToolContext {
|
|
|
524
753
|
* environment to report). Present ⇒ inspect the individual members: each one is itself optional.
|
|
525
754
|
*/
|
|
526
755
|
env?: HookEnvCapabilities;
|
|
756
|
+
/**
|
|
757
|
+
* The TASK's tracked working directory at this call — the base the hand tools will resolve a RELATIVE
|
|
758
|
+
* path against, read live off the same ref the tool policy reads (so a hook and a policy judging the
|
|
759
|
+
* same call judge it against the same base).
|
|
760
|
+
*
|
|
761
|
+
* It is NOT {@link HookEnvCapabilities.cwd}, and the difference is the reason this member exists: that
|
|
762
|
+
* one is the ENVIRONMENT's own working directory, which never moves when the shell `cd`s or the run
|
|
763
|
+
* enters a worktree. A hook is documented — on the capability face itself — as the seat that judges
|
|
764
|
+
* "is this write inside the workspace", and it was the only seat in the gate that could not resolve a
|
|
765
|
+
* relative target the way the tool would. CC ships its own `cwd` to every hook event for the same
|
|
766
|
+
* reason; this is that field, named for the value it actually carries.
|
|
767
|
+
*
|
|
768
|
+
* ABSENT when the run has no tracked cwd to report — no real write hands (read-only or no shell), a
|
|
769
|
+
* host driving {@link runToolGate} without one, or the delegation-fold twin (which runs inside a
|
|
770
|
+
* DESCENDANT's policy fold and would otherwise report the install site's directory, i.e. the wrong
|
|
771
|
+
* machine's answer). Absence means "this engine cannot tell you", never "the root".
|
|
772
|
+
*/
|
|
773
|
+
cwd?: string;
|
|
774
|
+
/** {@link HookSeatSignal} — this invocation's own abort signal. A FRESH one per screening pass, like
|
|
775
|
+
* the context object it rides: phase 1 and the approval-edit re-screen are two bounded waits, and a
|
|
776
|
+
* shared signal would let the first pass's expiry cancel the second. */
|
|
777
|
+
signal?: AbortSignal;
|
|
527
778
|
}
|
|
528
779
|
/** The executed tool result a PostToolUse hook inspects. */
|
|
529
780
|
export interface HookToolOutput {
|
|
@@ -572,12 +823,16 @@ export interface PostToolUseResult {
|
|
|
572
823
|
export interface UserPromptSubmitContext {
|
|
573
824
|
/** The run/leg identity envelope — see {@link HookInvocationIdentity} for the presence law. */
|
|
574
825
|
identity?: HookInvocationIdentity;
|
|
826
|
+
/** {@link HookSeatSignal} — this invocation's own abort signal. */
|
|
827
|
+
signal?: AbortSignal;
|
|
575
828
|
}
|
|
576
829
|
/** #281 件A — the context of a {@link Hooks.postToolBatch} invocation (third parameter, additive —
|
|
577
830
|
* deliberately NOT folded into `meta`, whose presence already means "the engine injected here"). */
|
|
578
831
|
export interface PostToolBatchContext {
|
|
579
832
|
/** The run/leg identity envelope — see {@link HookInvocationIdentity} for the presence law. */
|
|
580
833
|
identity?: HookInvocationIdentity;
|
|
834
|
+
/** {@link HookSeatSignal} — this invocation's own abort signal. */
|
|
835
|
+
signal?: AbortSignal;
|
|
581
836
|
}
|
|
582
837
|
/** A UserPromptSubmit hook result: block the submission, or inject context ahead of the prompt. */
|
|
583
838
|
export interface UserPromptSubmitResult {
|
|
@@ -822,6 +1077,26 @@ export interface ToolGateInput {
|
|
|
822
1077
|
* system-prompt declaration names. Absent (a host driving the gate directly) ⇒ bare tags. */
|
|
823
1078
|
reminderMark?: string;
|
|
824
1079
|
preToolUse?: Hooks["preToolUse"];
|
|
1080
|
+
/**
|
|
1081
|
+
* The bound the {@link preToolUse} screenings run under — {@link Hooks.timeoutMs}, carried in from
|
|
1082
|
+
* the hook record the caller resolved it off (the gate receives the callback, not the record). Absent
|
|
1083
|
+
* ⇒ {@link DEFAULT_HOOK_TIMEOUT_MS}. A garbage value is refused loudly to that default via
|
|
1084
|
+
* {@link onHookError}, exactly as {@link approvalTimeoutMs} is.
|
|
1085
|
+
*
|
|
1086
|
+
* On elapse the screening FAILS CLOSED — the call does not execute — and the block reason names the
|
|
1087
|
+
* hook and the bound. That direction is not a new posture for this seat: a hook that THROWS is
|
|
1088
|
+
* already the fail-closed deny here, and a hook that never answers has told the gate strictly less
|
|
1089
|
+
* than one that crashed. A face the deployment declared observational is not exempt, for the same
|
|
1090
|
+
* reason its throw is not: the declaration says its VERDICTS do not count, and a wait that never
|
|
1091
|
+
* ends is not a verdict.
|
|
1092
|
+
*/
|
|
1093
|
+
hookTimeoutMs?: number;
|
|
1094
|
+
/** The task's live tracked cwd reader, put on every {@link HookToolContext} this gate call builds
|
|
1095
|
+
* (see {@link HookToolContext.cwd}). A FUNCTION rather than a string for the reason the policy's own
|
|
1096
|
+
* threading gives: capturing the value when the gate's wiring is built would freeze the task's
|
|
1097
|
+
* starting directory and answer for a run that has since `cd`ed. Absent ⇒ the contexts carry no
|
|
1098
|
+
* `cwd`, which is the honest answer for a run with no tracked one. */
|
|
1099
|
+
trackedCwd?: () => string | undefined;
|
|
825
1100
|
/** The read-only env capability face put on every {@link HookToolContext} this gate call
|
|
826
1101
|
* builds ({@link HookEnvCapabilities}). Built ONCE per task by the runner (after the env is minted) and
|
|
827
1102
|
* passed through unchanged; omitted when the deployment wired no execution environment. */
|
|
@@ -941,12 +1216,13 @@ export interface ToolGateInput {
|
|
|
941
1216
|
/**
|
|
942
1217
|
* design/134 R5: observer for the gate's DENY short-circuit ({@link Hooks.permissionDenied}), pre-wrapped
|
|
943
1218
|
* by the caller (swallow + onError) so it never throws. Fired ONCE, at the single deny exit of the
|
|
944
|
-
* adjudicate chain — a policy deny or a resolved-ask deny. NOT fired for a PreToolUse hook deny
|
|
945
|
-
*
|
|
946
|
-
*
|
|
1219
|
+
* adjudicate chain — a policy deny or a resolved-ask deny. NOT fired for a PreToolUse hook deny (the
|
|
1220
|
+
* exclusion on {@link Hooks.permissionDenied}, which also records why that exclusion is not a parity
|
|
1221
|
+
* claim) or a durable suspend (the ask path). runToolGate still try/catches defensively: an observer
|
|
1222
|
+
* must never alter the deny outcome.
|
|
947
1223
|
*
|
|
948
1224
|
* RB-442: ALSO fired for the engine's fail-closed interception of a THROWING PreToolUse hook, with
|
|
949
|
-
* `source:"hook"` — the
|
|
1225
|
+
* `source:"hook"` — the exclusion is the hook's own deny DECISION, not an engine decision.
|
|
950
1226
|
*/
|
|
951
1227
|
permissionDenied?: (payload: PermissionDeniedPayload) => void | Promise<void>;
|
|
952
1228
|
/**
|
|
@@ -1198,7 +1474,13 @@ export interface ToolGateInput {
|
|
|
1198
1474
|
* so a broken screening face is distinguishable from a deliberate refusal, and the folded form has to
|
|
1199
1475
|
* reach the same lane from inside a descendant's fold.
|
|
1200
1476
|
*/
|
|
1201
|
-
export declare function createPreToolUseConstraintPolicy(preToolUse: NonNullable<Hooks["preToolUse"]>, env?: HookEnvCapabilities, onCrash?: (err: unknown) => void
|
|
1477
|
+
export declare function createPreToolUseConstraintPolicy(preToolUse: NonNullable<Hooks["preToolUse"]>, env?: HookEnvCapabilities, onCrash?: (err: unknown) => void,
|
|
1478
|
+
/** The install site's {@link Hooks.timeoutMs}. The folded form was ALREADY bounded by the descendant's
|
|
1479
|
+
* abort race — that asymmetry (bounded as an inherited constraint, unbounded in its own gate) is what
|
|
1480
|
+
* named the hole in the first place — but an abort bound is not a deadline: on a descendant with no
|
|
1481
|
+
* walltime and no cancel, the race never fires. So the same seat bound rides here too, and the two
|
|
1482
|
+
* installations of one callback are now bounded the same way for the same reason. */
|
|
1483
|
+
timeoutMs?: number): ToolPolicy;
|
|
1202
1484
|
/**
|
|
1203
1485
|
* The mandate provenance of one call, judged from the SAME mark inputs the gate is driven with —
|
|
1204
1486
|
* the single source for "could a persisted allow rule clear this ask?". Allow rules silence the
|