@sema-agent/core 7.5.2 → 7.6.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 +41 -0
- package/dist/agents/cascade.d.ts +2 -2
- package/dist/agents/cascade.js +12 -10
- package/dist/agents/repair-loop.d.ts +5 -3
- package/dist/agents/repair-loop.js +13 -15
- package/dist/agents/subagent.d.ts +24 -42
- package/dist/agents/subagent.js +119 -105
- package/dist/agents/suspend-guard.d.ts +31 -19
- package/dist/agents/suspend-guard.js +14 -8
- package/dist/agents/teacher.js +9 -9
- package/dist/agents/team.d.ts +4 -3
- package/dist/agents/team.js +10 -8
- package/dist/agents/verify.d.ts +3 -3
- package/dist/agents/verify.js +17 -17
- package/dist/core/a2a.js +2 -1
- package/dist/core/ask-origin.d.ts +60 -7
- package/dist/core/ask-origin.js +26 -1
- package/dist/core/checkpoint-store.d.ts +78 -76
- package/dist/core/checkpoint-store.js +17 -1
- package/dist/core/gate-outcome.d.ts +189 -0
- package/dist/core/gate-outcome.js +70 -0
- package/dist/core/hooks.d.ts +18 -92
- package/dist/core/hooks.js +88 -85
- package/dist/core/mcp-failure.d.ts +104 -0
- package/dist/core/mcp-failure.js +128 -0
- package/dist/core/mcp.d.ts +21 -77
- package/dist/core/mcp.js +76 -150
- package/dist/core/pause-registry.d.ts +131 -0
- package/dist/core/pause-registry.js +27 -0
- package/dist/core/runner/assemble-result.d.ts +32 -41
- package/dist/core/runner/assemble-result.js +55 -74
- package/dist/core/runner/contracts.d.ts +46 -64
- package/dist/core/runner/denial-limit-arms.d.ts +1 -1
- package/dist/core/runner/denial-limit-arms.js +3 -3
- package/dist/core/runner/gate-exit.d.ts +74 -0
- package/dist/core/runner/gate-exit.js +55 -0
- package/dist/core/runner/park-commit.d.ts +17 -23
- package/dist/core/runner/park-commit.js +14 -15
- package/dist/core/runner/prepare-ask-lane.d.ts +0 -3
- package/dist/core/runner/prepare-ask-lane.js +3 -5
- package/dist/core/runner/prepare-boundary-parks.d.ts +3 -6
- package/dist/core/runner/prepare-boundary-parks.js +3 -3
- package/dist/core/runner/prepare-caps-and-workflow.js +1 -1
- package/dist/core/runner/prepare-gate-stations.d.ts +4 -7
- package/dist/core/runner/prepare-gate-stations.js +29 -54
- package/dist/core/runner/prepare-inherited-gate.js +1 -1
- package/dist/core/runner/prepare-memory.d.ts +44 -26
- package/dist/core/runner/prepare-park-ask.d.ts +2 -4
- package/dist/core/runner/prepare-park-ask.js +5 -5
- package/dist/core/runner/prepare-task.js +8 -9
- package/dist/core/runner/prepare-wiring-manifest.d.ts +7 -15
- package/dist/core/runner/prepare-wiring-manifest.js +9 -10
- package/dist/core/runner/runtask.d.ts +16 -31
- package/dist/core/runner/runtask.js +109 -120
- package/dist/core/runner/terminal-projection.d.ts +22 -0
- package/dist/core/runner/terminal-projection.js +28 -0
- package/dist/core/store-contracts/checkpoint-store-contract.d.ts +4 -1
- package/dist/core/store-contracts/checkpoint-store-contract.js +8 -2
- package/dist/core/terminal-cause.d.ts +137 -0
- package/dist/core/terminal-cause.js +9 -0
- package/dist/core/tool-policy.d.ts +43 -139
- package/dist/core/tool-policy.js +79 -112
- package/dist/core/types.d.ts +67 -164
- package/dist/core/wiring-manifest.d.ts +6 -3
- package/dist/core/workflow-journal-store.js +3 -4
- package/dist/engine/harness/agent-harness.d.ts +1 -1
- package/dist/index.d.ts +10 -7
- package/dist/index.js +8 -5
- package/dist/orchestration/builtin-workflows.d.ts +2 -2
- package/dist/orchestration/builtin-workflows.js +1 -1
- package/dist/orchestration/goal.js +8 -7
- package/dist/orchestration/run-spec.js +5 -3
- package/dist/orchestration/run-workflow-tool.d.ts +1 -1
- package/dist/orchestration/run-workflow-tool.js +4 -4
- package/dist/orchestration/workflow-governance.d.ts +4 -4
- package/dist/orchestration/workflow-governance.js +4 -2
- package/dist/orchestration/workflow-primitives.d.ts +1 -1
- package/dist/orchestration/workflow-primitives.js +1 -1
- package/dist/orchestration/workflow.d.ts +11 -0
- package/dist/orchestration/workflow.js +64 -39
- package/dist/prompts/supervisor.d.ts +1 -1
- package/dist/prompts/supervisor.js +3 -3
- package/dist/scenarios/scenario-registry.js +1 -1
- package/package.json +3 -1
- package/test/export-surface.snapshot.json +74 -22
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { PAUSE_REGISTRY } from "../pause-registry.js";
|
|
2
|
+
export function terminalProjection(terminal) {
|
|
3
|
+
switch (terminal.kind) {
|
|
4
|
+
case "completed":
|
|
5
|
+
return { status: "completed" };
|
|
6
|
+
case "failed":
|
|
7
|
+
return { status: "failed", errorCode: terminal.code, errorMessage: terminal.message };
|
|
8
|
+
case "blocked":
|
|
9
|
+
return { status: "blocked", blockedReason: terminal.reason };
|
|
10
|
+
case "paused": {
|
|
11
|
+
const status = PAUSE_REGISTRY[terminal.gate.kind].taskStatus;
|
|
12
|
+
return {
|
|
13
|
+
status,
|
|
14
|
+
...(status === "needs_review" ? { errorCode: "review.pending" } : {}),
|
|
15
|
+
checkpointToken: terminal.token,
|
|
16
|
+
checkpointId: terminal.checkpointId,
|
|
17
|
+
checkpointGate: terminal.gate,
|
|
18
|
+
workspaceRestoreMode: terminal.restoreMode,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function amendTerminal(result, code, message) {
|
|
24
|
+
const t = result.terminal;
|
|
25
|
+
if (t.kind !== "failed")
|
|
26
|
+
return;
|
|
27
|
+
result.terminal = { ...t, code: t.code ?? code, message: message(t.message) };
|
|
28
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Checkpoint, type CheckpointStore, type ResumeOutcome } from "../checkpoint-store.js";
|
|
2
|
+
import { type CheckpointFields } from "../pause-registry.js";
|
|
2
3
|
import { type ContractAssertionRunner } from "./contract-harness.js";
|
|
3
4
|
/**
|
|
4
5
|
* design/159 S1 — the cross-backend {@link CheckpointStore} contract, extracted VERBATIM from
|
|
@@ -14,7 +15,7 @@ import { type ContractAssertionRunner } from "./contract-harness.js";
|
|
|
14
15
|
*/
|
|
15
16
|
/** Build a {@link Checkpoint} fixture. `createdAt` is FIXED (not wall-clock) so summary projections
|
|
16
17
|
* are deterministic and cross-backend byte-comparable; override via `over` for ordering tests. */
|
|
17
|
-
export declare function createCheckpointFixture(over?: Partial<
|
|
18
|
+
export declare function createCheckpointFixture(over?: Partial<CheckpointFields>): Checkpoint;
|
|
18
19
|
/** The canonical `policy_ask allow` outcome the contract resolves with. Declared as the NARROW
|
|
19
20
|
* `policy_ask` arm — not the whole union — so field access and spread call sites need no
|
|
20
21
|
* re-assertion (RB-396-d: the wide declaration forced an `as Extract<…>` + a same-value
|
|
@@ -45,6 +46,7 @@ export declare const EXPECTED_LISTBYSCOPE_SUMMARIES: ({
|
|
|
45
46
|
toolCallId: string;
|
|
46
47
|
toolName: string;
|
|
47
48
|
toolInput: string;
|
|
49
|
+
origin: string;
|
|
48
50
|
spentMicroUsd?: undefined;
|
|
49
51
|
} | {
|
|
50
52
|
token: string;
|
|
@@ -60,4 +62,5 @@ export declare const EXPECTED_LISTBYSCOPE_SUMMARIES: ({
|
|
|
60
62
|
toolCallId?: undefined;
|
|
61
63
|
toolName?: undefined;
|
|
62
64
|
toolInput?: undefined;
|
|
65
|
+
origin?: undefined;
|
|
63
66
|
})[];
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { strict as assert } from "node:assert";
|
|
2
2
|
import { MAX_PENDING_STEER_ENTRIES, mintCheckpointToken, readPendingSteerQueue, } from "../checkpoint-store.js";
|
|
3
|
+
import { checkpointFrom } from "../pause-registry.js";
|
|
3
4
|
import { beginContract } from "./contract-harness.js";
|
|
4
5
|
export function createCheckpointFixture(over = {}) {
|
|
5
6
|
const token = over.token ?? mintCheckpointToken();
|
|
6
|
-
return {
|
|
7
|
+
return checkpointFrom({
|
|
7
8
|
token,
|
|
8
9
|
scope: "tenant-a",
|
|
9
10
|
sessionId: "sess-1",
|
|
@@ -11,6 +12,7 @@ export function createCheckpointFixture(over = {}) {
|
|
|
11
12
|
gate: { kind: "human", reason: "approve", toolName: "Write" },
|
|
12
13
|
pendingAction: {
|
|
13
14
|
kind: "tool_approval",
|
|
15
|
+
origin: "policy",
|
|
14
16
|
toolCallId: "call-3",
|
|
15
17
|
toolName: "Write",
|
|
16
18
|
args: { path: "/x", content: "y" },
|
|
@@ -25,13 +27,14 @@ export function createCheckpointFixture(over = {}) {
|
|
|
25
27
|
status: "pending",
|
|
26
28
|
createdAt: 1_700_000_000_000,
|
|
27
29
|
...over,
|
|
28
|
-
};
|
|
30
|
+
});
|
|
29
31
|
}
|
|
30
32
|
export const ALLOW = {
|
|
31
33
|
gate: "policy_ask",
|
|
32
34
|
decision: "allow",
|
|
33
35
|
boundCallId: "call-3",
|
|
34
36
|
boundInputHash: "h0",
|
|
37
|
+
hostDecision: { decidedBy: "person" },
|
|
35
38
|
};
|
|
36
39
|
export async function checkpointStoreContract(make, runAssertion) {
|
|
37
40
|
const { run, settle } = beginContract(runAssertion);
|
|
@@ -193,6 +196,7 @@ export async function checkpointStoreContract(make, runAssertion) {
|
|
|
193
196
|
sessionId: "bidi-bitless",
|
|
194
197
|
pendingAction: {
|
|
195
198
|
kind: "tool_approval",
|
|
199
|
+
origin: "policy",
|
|
196
200
|
toolCallId: "call-3",
|
|
197
201
|
toolName: "Write",
|
|
198
202
|
args: { path: `/x/${RLO}txt.exe` },
|
|
@@ -254,6 +258,7 @@ export async function checkpointListByScopeSummaries(store) {
|
|
|
254
258
|
},
|
|
255
259
|
pendingAction: {
|
|
256
260
|
kind: "tool_approval",
|
|
261
|
+
origin: "policy",
|
|
257
262
|
toolCallId: "call-pr",
|
|
258
263
|
toolName: "open_pr",
|
|
259
264
|
args: { repo: "x" },
|
|
@@ -293,6 +298,7 @@ export const EXPECTED_LISTBYSCOPE_SUMMARIES = [
|
|
|
293
298
|
toolCallId: "call-pr",
|
|
294
299
|
toolName: "open_pr",
|
|
295
300
|
toolInput: '{"repo":"x"}',
|
|
301
|
+
origin: "policy",
|
|
296
302
|
},
|
|
297
303
|
{
|
|
298
304
|
token: "tok-resource",
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The terminal cause — the ONE record of how a run ended (`TaskResult.terminal`), its closed kind set,
|
|
3
|
+
* the one disposition table over that set (which cause a resume journal may replay) and the admission
|
|
4
|
+
* read a journal entry's cause passes through. Layer 0 vocabulary, beside `gate-outcome.ts` (the record
|
|
5
|
+
* of one gate pass) and `pause-registry.ts` (the pause gate × pending-action pairing a paused cause
|
|
6
|
+
* points at): no runtime import above this layer, so any module may read the words.
|
|
7
|
+
*
|
|
8
|
+
* The plane-word projection a face that still speaks `status` needs lives in
|
|
9
|
+
* `runner/terminal-projection.ts` — the one cause→plane derivation.
|
|
10
|
+
*/
|
|
11
|
+
import type { AssertAllKeysHandled } from "./ask-origin.js";
|
|
12
|
+
/**
|
|
13
|
+
* WHY a run ended — ONE tagged cause, and the ONLY terminal record a {@link TaskResult} carries. The run
|
|
14
|
+
* loop's terminal arbitration used to decide a `status` word plus seven optional plane fields one by one
|
|
15
|
+
* across an if/else-if ladder over competing optional flags, and the legal combinations
|
|
16
|
+
* ("`status:"suspended"` ⇔ a token is present", "a review pause and an approval pause are never both set")
|
|
17
|
+
* lived only in that ladder's order. Here they are the SHAPE: a paused run carries its token; a completed
|
|
18
|
+
* run cannot. Branch on `kind`; the five-word {@link TaskStatus} a cause reads as (a `paused` cause reads
|
|
19
|
+
* its gate kind's registry row) is derived by {@link import("./runner/terminal-projection.js").terminalProjection},
|
|
20
|
+
* the one cause→plane derivation, for the faces that still speak the word.
|
|
21
|
+
* - `completed` — the run finished (a person's clean halt included — see `TaskResult.haltedByUser`).
|
|
22
|
+
* - `failed` — a limit, a provider failure, an abort, an invalid output…
|
|
23
|
+
* - `blocked` — the agent could not finish and said why.
|
|
24
|
+
* - `paused` — a durable pause committed a checkpoint and the run is resumable.
|
|
25
|
+
* A `GateOutcome` is the record of ONE gate pass (per call); a `TerminalCause` is the record of ONE run
|
|
26
|
+
* (per leg); a paused run's `gate` and the park row's `gate.kind` are the same word, joined by the token.
|
|
27
|
+
*/
|
|
28
|
+
export type TerminalCause = {
|
|
29
|
+
kind: "completed";
|
|
30
|
+
} | {
|
|
31
|
+
kind: "failed";
|
|
32
|
+
/**
|
|
33
|
+
* Machine-readable failure code when the failure carried one — e.g. a `SessionError.code` such as
|
|
34
|
+
* `"conflict"` (a cross-instance write lost the optimistic lock), or a Node error code. Lets a
|
|
35
|
+
* caller branch programmatically (e.g. evict a stale cache + retry on `"conflict"`) instead of
|
|
36
|
+
* matching `message` strings. Absent only for the failures that never had one (an abort with no
|
|
37
|
+
* limit axis, no assistant message produced).
|
|
38
|
+
*
|
|
39
|
+
* Terminal codes use a **dotted namespace** so a caller can prefix-match a whole class:
|
|
40
|
+
* `"limits.max_tokens_exceeded"` / `"limits.max_cost_exceeded"` / `"limits.max_turns_exceeded"` /
|
|
41
|
+
* `"limits.max_walltime_exceeded"` / `"config.limit_invalid"` / `"config.limit_unknown_key"` /
|
|
42
|
+
* `"config.attachment_invalid"` (a mode-valued `TaskSpec.attachments` member outside its closed
|
|
43
|
+
* set, refused at the same door as the limits) (e.g. `code.startsWith("limits.")`). Brain codes
|
|
44
|
+
* (`auth`/`network`/`rate_limit`/…) and `"conflict"` remain flat.
|
|
45
|
+
*
|
|
46
|
+
* Two codes name the EXTERNAL stop causes — neither is a `limits.` code, because neither is an
|
|
47
|
+
* allowance the task chose, and a caller that retries on `limits.*` should NOT treat these the same:
|
|
48
|
+
* - `"env.lifetime_expired"` — the execution environment's platform lifetime ran out and the run
|
|
49
|
+
* could not be suspended durably. Retrying needs a NEW environment, not a smaller budget.
|
|
50
|
+
* - `"usage.window_exhausted"` — a deployment usage-governance window is full (see
|
|
51
|
+
* `RunnerDeps.usageWindows`). Reported at task ENTRY (nothing ran) or after a suspend was
|
|
52
|
+
* impossible; the wait hint rides the thrown error's `retryAfterMs` (delivered through
|
|
53
|
+
* `RunnerDeps.onError`) and the message text. Retrying before the window frees is refused again.
|
|
54
|
+
* Their config-time siblings are `"config.env_lifetime_invalid"` / `"config.usage_window_invalid"`.
|
|
55
|
+
* A governance window with a MONEY ceiling adds two more, both of which say "the ceiling could not
|
|
56
|
+
* be evaluated" rather than "the ceiling was reached" — neither is retryable without a config change:
|
|
57
|
+
* - `"config.usage_window_unpriced"` — a `UsageWindow.maxCostUsd` over a run with no cost figure
|
|
58
|
+
* (no `RunnerDeps.pricing` entry and no `Model.cost`).
|
|
59
|
+
* - `"usage_window.store_cost_unanswered"` — the wired ledger does not carry the money arm.
|
|
60
|
+
* A NESTED orchestrator that met a durable pause it cannot drive stamps `"unexpected.suspended"` /
|
|
61
|
+
* `"unexpected.needs_review"` and carries the pause on {@link nestedPause}.
|
|
62
|
+
*/
|
|
63
|
+
code?: string;
|
|
64
|
+
/** Human-readable failure text. */
|
|
65
|
+
message?: string;
|
|
66
|
+
/**
|
|
67
|
+
* The durable pause this failure STANDS IN FOR — set only by the nested hard boundary
|
|
68
|
+
* (`mapNestedSuspend`): an orchestrator's nested leg paused on a durable gate, the orchestrator cannot
|
|
69
|
+
* drive a resume from the inside, so at ITS boundary the pause is a failure — but the pause's own
|
|
70
|
+
* cause travels here verbatim so the top-level caller keeps the recovery capability
|
|
71
|
+
* (`nestedPause.token` → `runner.resume`). Absent on every failure the engine itself assembles.
|
|
72
|
+
*/
|
|
73
|
+
nestedPause?: PausedCause;
|
|
74
|
+
} | {
|
|
75
|
+
kind: "blocked";
|
|
76
|
+
/** Why the agent could not finish. */
|
|
77
|
+
reason: string;
|
|
78
|
+
} | {
|
|
79
|
+
kind: "paused";
|
|
80
|
+
/**
|
|
81
|
+
* WHICH pause — who/what must resume (e.g. `{kind:"human", reason, toolName}`), so the caller knows
|
|
82
|
+
* what decision the checkpoint awaits and which resume outcome it takes (a `policy_ask` for an
|
|
83
|
+
* approval gate; a `dry_run_review` on a `needs_review` gate; a `plan_review` on a `plan_review`
|
|
84
|
+
* gate). Its kind's {@link import("./pause-registry.js").PAUSE_REGISTRY} row is what
|
|
85
|
+
* {@link import("./runner/terminal-projection.js").terminalProjection} reads the status word from
|
|
86
|
+
* (`"suspended"` for an approval / a resource slice / a platform pause, `"needs_review"` for a review).
|
|
87
|
+
*/
|
|
88
|
+
gate: import("./checkpoint-store.js").CheckpointGate;
|
|
89
|
+
/**
|
|
90
|
+
* The durable-checkpoint token to resume this task with via `runner.resume(token, outcome)`.
|
|
91
|
+
* Branded `CheckpointToken`; **never log it or put it in a URL** (it is the resume capability, §6).
|
|
92
|
+
*/
|
|
93
|
+
token: import("./checkpoint-store.js").CheckpointToken;
|
|
94
|
+
/**
|
|
95
|
+
* The pause's NON-SECRET stable identity ({@link import("./checkpoint-store.js").Checkpoint.checkpointId})
|
|
96
|
+
* — the display/correlation key a consumer may log or render where the token must not travel.
|
|
97
|
+
* Absent on pre-identity checkpoints.
|
|
98
|
+
*/
|
|
99
|
+
checkpointId?: string;
|
|
100
|
+
/**
|
|
101
|
+
* HOW the paused task's remote workspace will come back, present whenever the pause captured a
|
|
102
|
+
* remote workspace. The two modes bill and behave very differently:
|
|
103
|
+
* - `"snapshot"` — the workspace VM was `suspendVM`-paused into a snapshot. Provider billing
|
|
104
|
+
* typically stops; in-memory process state is captured; resume restores it.
|
|
105
|
+
* - `"park_only"` — the env declared itself non-suspendable (an SSH host / an ADB device), so
|
|
106
|
+
* NOTHING was paused: the machine keeps running (and keeps costing), any in-memory process state
|
|
107
|
+
* is at the target's mercy, and resume simply reconnects to the still-present workspace.
|
|
108
|
+
* Deliberate and honest (core does not fabricate a snapshot) — but a scheduler that assumes
|
|
109
|
+
* "paused ⇒ idle and free" must be able to see it.
|
|
110
|
+
* ABSENT for a process-local pause (a static caller-owned env: no remote workspace was captured).
|
|
111
|
+
* The same discriminant rides {@link import("./checkpoint-store.js").CheckpointSummary} for the
|
|
112
|
+
* inbox/`listByScope` face.
|
|
113
|
+
*/
|
|
114
|
+
restoreMode?: "snapshot" | "park_only";
|
|
115
|
+
};
|
|
116
|
+
/** The paused arm alone — the shape the run's committed-pause holder carries. */
|
|
117
|
+
/**
|
|
118
|
+
* The one disposition table over `TerminalCause["kind"]`: which causes a resume journal may REPLAY
|
|
119
|
+
* instead of re-running. Only a completed cause is a finished leg; every other cause means the leg has
|
|
120
|
+
* work left (or failed) and a resume runs it live. The table is also the admission list for a journal
|
|
121
|
+
* entry's cause at load: a kind absent here (a journal written before `TaskResult.terminal`, a corrupted
|
|
122
|
+
* or version-skewed entry) is refused before anything is dispatched — it is never read as "not
|
|
123
|
+
* completed, so run it again", which would replay the side effects of finished work.
|
|
124
|
+
*/
|
|
125
|
+
export declare const TERMINAL_CAUSE_IS_REPLAYABLE: {
|
|
126
|
+
readonly completed: true;
|
|
127
|
+
readonly failed: false;
|
|
128
|
+
readonly blocked: false;
|
|
129
|
+
readonly paused: false;
|
|
130
|
+
};
|
|
131
|
+
/** Compile-time fence: every `TerminalCause` kind has a row in the replay table (and, via `satisfies`, only those). */
|
|
132
|
+
export type ReplayTableCoversEveryTerminalCause = AssertAllKeysHandled<Exclude<TerminalCause["kind"], keyof typeof TERMINAL_CAUSE_IS_REPLAYABLE>>;
|
|
133
|
+
/** The admission read for a journaled cause: a closed-set member, by table row. */
|
|
134
|
+
export declare function isTerminalCauseKind(kind: unknown): kind is TerminalCause["kind"];
|
|
135
|
+
export type PausedCause = Extract<TerminalCause, {
|
|
136
|
+
kind: "paused";
|
|
137
|
+
}>;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { AskOrigin } from "./ask-origin.js";
|
|
2
|
+
import type { Settlement } from "./gate-outcome.js";
|
|
1
3
|
/** A tool call presented to a policy before it executes. */
|
|
2
4
|
export interface ToolCallRequest {
|
|
3
5
|
toolName: string;
|
|
@@ -80,55 +82,17 @@ export interface ToolCallRequest {
|
|
|
80
82
|
* spelling this word is minted from). */
|
|
81
83
|
declare const DECISION_REASONS: readonly ["rule", "mode", "hook", "safety", "classifier", "persisted_rule", "sandbox", "org_rule", "org_unavailable"];
|
|
82
84
|
export type DecisionReason = (typeof DECISION_REASONS)[number];
|
|
83
|
-
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
* **Which windows `"timeout"` speaks for** (#114①, 2026-08-09 — the promise this note used to make was
|
|
95
|
-
* wider than the code): the engine stamps it at the waits IT owns — `createApprovalPolicy`'s
|
|
96
|
-
* `approvalTimeoutMs` window, the durable park's TTL, and (#548) the denial-limit fallback ask's
|
|
97
|
-
* auto-deny window. The SYNCHRONOUS `onAsk` leg is otherwise not one of them: there the deployment owns
|
|
98
|
-
* the window (the engine starts no timer for a callback it does not schedule), so an unanswered card and
|
|
99
|
-
* a refused one arrive as the same `false` and the engine records
|
|
100
|
-
* `"human"` rather than inventing a cause it did not observe. A host that DOES time its own card out
|
|
101
|
-
* can say so — {@link AskOutcome}'s object arm carries an optional `settledBy` for exactly this — but a
|
|
102
|
-
* host that does not is indistinguishable, by construction. Read an absent `"timeout"` as "no window
|
|
103
|
-
* the engine owns elapsed", never as "nobody's window elapsed".
|
|
104
|
-
*
|
|
105
|
-
* **The one `onAsk` exception** (#548): a classifier DENIAL-LIMIT fallback ask — the ask that carries
|
|
106
|
-
* `denialLimitFallback`, minted when the auto-mode classifier reaches its consecutive/total bound —
|
|
107
|
-
* IS timed by the engine over the synchronous leg, because that ask exists to bound a classifier that
|
|
108
|
-
* would otherwise deny without end, and an unbounded wait would only move the "without end" onto the
|
|
109
|
-
* person. Its window elapsing produces an engine-stamped `settledBy: "timeout"` with
|
|
110
|
-
* `resolution: "window_expired"` and `autoDenied: true` on the deny — that last bit is the
|
|
111
|
-
* discriminator between core's window and a host self-report ({@link AskOutcome}'s object arm has no
|
|
112
|
-
* `autoDenied` seat, so a host cannot claim the word).
|
|
113
|
-
* `autoDenyAfterMs: 0` (or an ask with no fallback member) arms nothing, which is every other ask.
|
|
114
|
-
*
|
|
115
|
-
* The three words are exhaustive and mutually exclusive over the ways an approval can end, and the
|
|
116
|
-
* minimum discrimination a consumer needs — someone refused vs nobody answered — is `"human"` vs the
|
|
117
|
-
* other two.
|
|
118
|
-
*
|
|
119
|
-
* **Fill it AT the settlement site.** Each site knows its own cause and names it; deriving the value
|
|
120
|
-
* afterwards from whatever outcome happens to be at hand is exactly how a window that elapsed comes to
|
|
121
|
-
* be reported as a person's refusal.
|
|
122
|
-
*
|
|
123
|
-
* ABSENT means this settlement named no source: an older caller that predates the field, or a verdict
|
|
124
|
-
* that settled no wait at all (a policy POSTURE — headless auto-deny, a blanket circumvention — where nobody
|
|
125
|
-
* was ever asked). A consumer MUST NOT read a semantic out of the absence; render the text instead.
|
|
126
|
-
*/
|
|
127
|
-
export type ApprovalSettledBy = "human" | "timeout" | "aborted";
|
|
128
|
-
/** The closed set above, for runtime domain checks at the seams that accept a caller-supplied value. */
|
|
129
|
-
export declare const APPROVAL_SETTLED_BY_VALUES: readonly ApprovalSettledBy[];
|
|
130
|
-
/** True iff `v` is one of the three {@link ApprovalSettledBy} words. */
|
|
131
|
-
export declare function isApprovalSettledBy(v: unknown): v is ApprovalSettledBy;
|
|
85
|
+
/** Read the engine-attested settlement off a funneled decision FOR the named call (the gate's exit is the
|
|
86
|
+
* one consumer): an attestation bound to a different call is a replayed object and answers absence.
|
|
87
|
+
* Exported for the gate module only — deliberately NOT re-exported from `src/index.ts` (an internal seam
|
|
88
|
+
* between engine modules, not a facility deployments call). */
|
|
89
|
+
export declare function engineSettlementOf(d: unknown, call: {
|
|
90
|
+
toolCallId: string;
|
|
91
|
+
toolName: string;
|
|
92
|
+
}): {
|
|
93
|
+
settlement: Settlement;
|
|
94
|
+
origin: AskOrigin;
|
|
95
|
+
} | undefined;
|
|
132
96
|
/**
|
|
133
97
|
* design/252 G-7 — how long an approver-attribution identifier may be.
|
|
134
98
|
*
|
|
@@ -176,10 +140,10 @@ export declare function screenApproverAttribution(v: unknown): {
|
|
|
176
140
|
* - `deny` blocks the call.
|
|
177
141
|
* Human-readable text is `message`; read it via {@link decisionText}.
|
|
178
142
|
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
143
|
+
* NO arm carries a settlement: what ended a wait is not a policy's to state. The engine's own settlement
|
|
144
|
+
* sites attest it through a module-private seat ({@link engineSettlementOf}) and the gate's exit mints the
|
|
145
|
+
* one settlement record ({@link import("./gate-outcome.js").GateOutcome}); a policy that puts a `settledBy`
|
|
146
|
+
* or `approver` member on its verdict is putting an unread property on it.
|
|
183
147
|
* RB-479-B① (ruled 2026-08-02): the legacy `reason` field is REMOVED — one name for one thing.
|
|
184
148
|
*
|
|
185
149
|
* How far the TYPE carries that retirement (measured 2026-08-03, correcting this note's earlier claim
|
|
@@ -200,8 +164,6 @@ export type PermissionResult = {
|
|
|
200
164
|
updatedInput?: unknown;
|
|
201
165
|
message?: string;
|
|
202
166
|
decisionReason?: DecisionReason;
|
|
203
|
-
settledBy?: Extract<ApprovalSettledBy, "human">;
|
|
204
|
-
approver?: string;
|
|
205
167
|
} | {
|
|
206
168
|
action: "ask";
|
|
207
169
|
updatedInput?: unknown;
|
|
@@ -297,8 +259,6 @@ export type PermissionResult = {
|
|
|
297
259
|
updatedInput?: unknown;
|
|
298
260
|
message?: string;
|
|
299
261
|
decisionReason?: DecisionReason;
|
|
300
|
-
settledBy?: ApprovalSettledBy;
|
|
301
|
-
approver?: string;
|
|
302
262
|
};
|
|
303
263
|
/**
|
|
304
264
|
* design/252 G-2 — WHY a piece of rule-provenance evidence is not on an ask.
|
|
@@ -1204,7 +1164,7 @@ export interface AskRequest {
|
|
|
1204
1164
|
* {@link PermissionResult} ask-arm member of the same name): the counts that tripped the bound and
|
|
1205
1165
|
* this ask's own auto-deny window. Two readers: a card renders it as a countdown; {@link resolveAsk}
|
|
1206
1166
|
* arms its deadline from `autoDenyAfterMs` (> 0 ⇒ an unanswered function approver auto-denies at
|
|
1207
|
-
* that deadline —
|
|
1167
|
+
* that deadline — the `denial_limit_window_expired` settlement on the gate record). Filled
|
|
1208
1168
|
* by the gate from the decision, never a caller/worker-settable field. This is the ARMED type: the
|
|
1209
1169
|
* decision's member carries the counts alone, and the only two ways to reach this one are the route
|
|
1210
1170
|
* stations — `AutoModeDenialTracker.armTimedWindow` (a hand-out to a live function approver, the one
|
|
@@ -1349,9 +1309,8 @@ export type OnAsk = "deny" | "allow" | ((req: AskRequest, signal?: AbortSignal)
|
|
|
1349
1309
|
* the fallback exists to bound a classifier that would otherwise deny without end. When it elapses
|
|
1350
1310
|
* the approver's promise is DETACHED (design/384: the wait is released, not cancelled — an approver
|
|
1351
1311
|
* that answers afterwards is not consulted, and an `allow` it returns late becomes the
|
|
1352
|
-
* `task.late_approval` notice), and the engine's own deny
|
|
1353
|
-
*
|
|
1354
|
-
* consumer tells that window from the host's.
|
|
1312
|
+
* `task.late_approval` notice), and the engine's own deny settles as `denial_limit_window_expired`
|
|
1313
|
+
* (the host's own elapsed window settles as `approval_window_expired` with `who.party: "host"`).
|
|
1355
1314
|
*
|
|
1356
1315
|
* `reason` — the SYNCHRONOUS leg's seat for a model-readable reason attached to a deny, the exact
|
|
1357
1316
|
* counterpart of the durable leg's `ResumeOutcome` `policy_ask` `reason` ("Model-readable reason
|
|
@@ -1387,7 +1346,7 @@ export type OnAsk = "deny" | "allow" | ((req: AskRequest, signal?: AbortSignal)
|
|
|
1387
1346
|
export type AskOutcome = boolean | "unavailable" | {
|
|
1388
1347
|
allow: boolean;
|
|
1389
1348
|
updatedInput?: unknown;
|
|
1390
|
-
settledBy?:
|
|
1349
|
+
settledBy?: "human" | "timeout";
|
|
1391
1350
|
reason?: string;
|
|
1392
1351
|
approver?: string;
|
|
1393
1352
|
};
|
|
@@ -1473,90 +1432,35 @@ export declare function tryCloneArgs<T>(v: T): {
|
|
|
1473
1432
|
* text they let through. Not part of the package's public surface.
|
|
1474
1433
|
*/
|
|
1475
1434
|
export declare function describeThrown(err: unknown): string;
|
|
1476
|
-
/**
|
|
1477
|
-
* The deny-arm classification a {@link resolveAsk} refusal carries — MINTED at the composing arm
|
|
1478
|
-
* (the minter reports the fact; no consumer re-derives it from message text, which is exactly the
|
|
1479
|
-
* inference this closed set exists to end). One word per family of arms:
|
|
1480
|
-
* - `"human_refused"` — a person answered no (the boolean false fold and the object arm's
|
|
1481
|
-
* allow-false, noted or bare — the note fact rides {@link ResolvedAsk.humanRefusalNote});
|
|
1482
|
-
* - `"window_expired"` — the approver's own window elapsed (the timeout-settled deny);
|
|
1483
|
-
* - `"no_approver"` — headless auto-deny (no approver wired, or the deny posture string);
|
|
1484
|
-
* - `"blanket_allow_refused"` — a blanket allow posture met a `requiresRealApproval` ask;
|
|
1485
|
-
* - `"approver_unavailable"` — the approver answered the ROUTING question "nobody reachable"
|
|
1486
|
-
* (the G1 marker's fail-closed carry — the gate may re-route it to a durable park instead);
|
|
1487
|
-
* - `"task_aborted"` — the wait's abort signal ended it (pre-wait, mid-wait and race arms). The
|
|
1488
|
-
* signal is the run's own end AND, since design/384, any turn-level interrupt composed into the
|
|
1489
|
-
* wait (a bare user halt, a steer-now boundary cut): one abort family, one word — a consumer
|
|
1490
|
-
* that must tell the sources apart reads the run's own terminal facts, not this classification;
|
|
1491
|
-
* - `"presentation_failed"` — the args/edit could not be safely presented or adopted (unclonable);
|
|
1492
|
-
* - `"approver_error"` — the approver callback threw;
|
|
1493
|
-
* - `"approver_contract"` — the approver returned something outside the contract (non-boolean
|
|
1494
|
-
* allow, out-of-vocabulary settlement word, unreadable members, a timeout-settled allow, a
|
|
1495
|
-
* non-string or unreadable reason, an out-of-contract truthy, a refused attribution).
|
|
1496
|
-
*/
|
|
1497
|
-
export type AskDenyResolution = "human_refused" | "window_expired" | "no_approver" | "blanket_allow_refused" | "approver_unavailable" | "task_aborted" | "presentation_failed" | "approver_error" | "approver_contract";
|
|
1498
|
-
/** The closed set above, for runtime domain checks at the seams that accept a caller-supplied value
|
|
1499
|
-
* (the `APPROVAL_SETTLED_BY_VALUES` precedent: the word crosses process boundaries on `tool_end`,
|
|
1500
|
-
* so a consumer enumerating or validating it must not hand-roll the vocabulary). */
|
|
1501
|
-
export declare const ASK_DENY_RESOLUTION_VALUES: readonly AskDenyResolution[];
|
|
1502
|
-
/** Closed-vocabulary guard for {@link AskDenyResolution} — the screen every carrier runs before it
|
|
1503
|
-
* files or forwards the word (a policy layer could self-declare the member on its own deny; an
|
|
1504
|
-
* out-of-vocabulary word is dropped by the carriers, never coerced or forwarded). */
|
|
1505
|
-
export declare function isAskDenyResolution(v: unknown): v is AskDenyResolution;
|
|
1506
|
-
/** #548: the engine-attested auto-deny marker off a funneled decision, bound to the named call like
|
|
1507
|
-
* {@link coreMintedResolutionOf} (absent ⇒ not an attested auto-deny of THIS call). */
|
|
1508
|
-
export declare function coreMintedAutoDeniedOf(d: unknown, call: {
|
|
1509
|
-
toolCallId: string;
|
|
1510
|
-
toolName: string;
|
|
1511
|
-
}): boolean;
|
|
1512
|
-
/** Read the engine-attested resolution off a funneled decision (the gate's single deny exit is the
|
|
1513
|
-
* one consumer), FOR the named call: an attestation bound to a different toolCallId/toolName is a
|
|
1514
|
-
* replayed object, not this call's settlement — the reader answers absence (the safe direction; the
|
|
1515
|
-
* public `settledBy`/message on such an object were always the policy's own to state). A present
|
|
1516
|
-
* word is an engine settlement site's own attestation for THIS object and THIS call — no foreign
|
|
1517
|
-
* policy can reach the sidecar. The vocabulary screen is a belt (the typed stamp is the only
|
|
1518
|
-
* writer). Exported for the gate module only — deliberately NOT re-exported from `src/index.ts`
|
|
1519
|
-
* (the {@link refuseOutOfContractDecision} precedent: an internal seam between engine modules, not
|
|
1520
|
-
* a facility deployments call). */
|
|
1521
|
-
export declare function coreMintedResolutionOf(d: unknown, call: {
|
|
1522
|
-
toolCallId: string;
|
|
1523
|
-
toolName: string;
|
|
1524
|
-
}): AskDenyResolution | undefined;
|
|
1525
1435
|
/**
|
|
1526
1436
|
* A {@link resolveAsk} result: always a TERMINAL `allow`/`deny` (never `ask`). `approverUnavailable`
|
|
1527
1437
|
* is the out-of-band G1 three-value marker: the live approver returned `"unavailable"` for this ask —
|
|
1528
1438
|
* the carried `deny` is the FAIL-CLOSED FALLBACK, and the gate may instead re-route the ask onto the
|
|
1529
1439
|
* durable park leg (the only consumer; everywhere else the result reads as a plain deny).
|
|
1530
1440
|
*/
|
|
1531
|
-
export type ResolvedAsk = PermissionResult
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
*
|
|
1536
|
-
*
|
|
1537
|
-
|
|
1441
|
+
export type ResolvedAsk = (Extract<PermissionResult, {
|
|
1442
|
+
action: "allow";
|
|
1443
|
+
}> & {
|
|
1444
|
+
/** Present exactly when a PERSON approved (`human_allowed`, with the channel's attribution when it
|
|
1445
|
+
* reported one); absent on the posture allows (a blanket `"allow"`, a reused grant) — nobody was
|
|
1446
|
+
* asked, so nothing was settled. */
|
|
1447
|
+
settlement?: Settlement;
|
|
1448
|
+
/** The EXACT data snapshot the approver was shown (the `structuredClone` of the presented args). On
|
|
1449
|
+
* a plain approval (no edit) the gate EXECUTES this snapshot, not the original object: a stateful
|
|
1450
|
+
* getter or an external alias must not be able to make the executed action differ from the
|
|
1451
|
+
* approved one (shown == executed, by construction). Set only on the function-approver path
|
|
1452
|
+
* (string modes present nothing). */
|
|
1538
1453
|
presentedInput?: unknown;
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
*
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
* ask's own window (`AskRequest.denialLimitFallback.autoDenyAfterMs`, CC `AKe`) elapsed with no
|
|
1550
|
-
* answer. Rides beside `settledBy:"timeout"` / `resolution:"window_expired"` (the established approval
|
|
1551
|
-
* factory's deadline arm speaks the same two words, so a consumer classifying on them needs no new
|
|
1552
|
-
* branch) and says WHOSE window it was: core's, not the host's. Stamped only at the composing arm. */
|
|
1553
|
-
autoDenied?: true;
|
|
1554
|
-
/** The deny-arm classification (see {@link AskDenyResolution}) — present on every deny this
|
|
1555
|
-
* resolver composes, absent on every allow. Carried by the gate to its block exit, the
|
|
1556
|
-
* permission-denied observer payload and the settlement sideband (thence the call's `tool_end`
|
|
1557
|
-
* frame), so a consumer classifies a refusal by code instead of parsing its text. */
|
|
1558
|
-
resolution?: AskDenyResolution;
|
|
1559
|
-
};
|
|
1454
|
+
approverUnavailable?: never;
|
|
1455
|
+
}) | (Extract<PermissionResult, {
|
|
1456
|
+
action: "deny";
|
|
1457
|
+
}> & {
|
|
1458
|
+
/** What ended the wait — composed at the arm that ended it, one of the refusal kinds. A deny this
|
|
1459
|
+
* resolver composes ALWAYS names its settlement: there is no unclassified refusal. */
|
|
1460
|
+
settlement: Settlement;
|
|
1461
|
+
presentedInput?: unknown;
|
|
1462
|
+
approverUnavailable?: true;
|
|
1463
|
+
});
|
|
1560
1464
|
/**
|
|
1561
1465
|
* Does any string reachable in `value` carry a {@link BIDI_CONTROL_RE} member? Bounded, cycle-safe,
|
|
1562
1466
|
* and never throwing — every caller is on an approval/projection path, where a scan that failed must
|