@sema-agent/core 7.9.2 → 7.11.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 +102 -0
- package/dist/agents/child-model-seat.d.ts +71 -14
- package/dist/agents/child-model-seat.js +23 -7
- package/dist/agents/subagent.d.ts +2 -2
- package/dist/agents/subagent.js +8 -5
- package/dist/agents/teacher.js +2 -2
- package/dist/core/ask-unresolvable-notice.d.ts +52 -0
- package/dist/core/ask-unresolvable-notice.js +25 -0
- package/dist/core/auto-mode-defaults.d.ts +15 -3
- package/dist/core/auto-mode-defaults.js +1 -0
- package/dist/core/auto-mode.d.ts +86 -23
- package/dist/core/auto-mode.js +43 -12
- package/dist/core/checkpoint-store.d.ts +14 -0
- package/dist/core/checkpoint-store.js +2 -1
- package/dist/core/engine-notice.d.ts +28 -7
- package/dist/core/gate-fold.js +1 -0
- package/dist/core/gate-lanes.d.ts +6 -1
- package/dist/core/gate-lanes.js +60 -18
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +4 -0
- package/dist/core/hooks.d.ts +37 -1
- package/dist/core/hooks.js +2 -0
- package/dist/core/permission-rule-model.d.ts +53 -16
- package/dist/core/permission-rule-model.js +59 -21
- package/dist/core/permission-rules.d.ts +6 -4
- package/dist/core/permission-rules.js +14 -14
- package/dist/core/persisted-rule-arms.js +4 -3
- package/dist/core/read-only-shell-table.d.ts +87 -0
- package/dist/core/read-only-shell-table.js +485 -0
- package/dist/core/read-only-shell.d.ts +42 -0
- package/dist/core/read-only-shell.js +316 -0
- package/dist/core/roles.d.ts +3 -2
- package/dist/core/runner/contracts.d.ts +26 -2
- package/dist/core/runner/denial-limit-arms.d.ts +14 -3
- package/dist/core/runner/denial-limit-arms.js +15 -5
- package/dist/core/runner/gate-exit.d.ts +5 -0
- package/dist/core/runner/permission-rule-lanes.d.ts +7 -1
- package/dist/core/runner/permission-rule-lanes.js +9 -3
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +1 -1
- package/dist/core/runner/prepare-caps-and-workflow.js +35 -14
- package/dist/core/runner/prepare-gate-stations.d.ts +3 -2
- package/dist/core/runner/prepare-gate-stations.js +12 -0
- package/dist/core/runner/prepare-policy-chain.js +7 -6
- package/dist/core/runner/prepare-task.js +1 -1
- package/dist/core/runner/prepare-turn-wiring.js +1 -1
- package/dist/core/runner/prepare-wiring-manifest.d.ts +1 -1
- package/dist/core/runner/prepare-wiring-manifest.js +8 -1
- package/dist/core/runner/runtask.d.ts +33 -32
- package/dist/core/runner/runtask.js +63 -33
- package/dist/core/runner-deps.d.ts +9 -2
- package/dist/core/shell-lexer.d.ts +18 -0
- package/dist/core/shell-lexer.js +17 -10
- package/dist/core/shell-wrapper-table.js +8 -5
- package/dist/core/swappable-deps.d.ts +90 -0
- package/dist/core/swappable-deps.js +55 -0
- package/dist/core/tool-policy.d.ts +30 -1
- package/dist/core/tool-policy.js +6 -2
- package/dist/core/tools.d.ts +28 -7
- package/dist/core/tools.js +44 -4
- package/dist/core/trace.d.ts +15 -0
- package/dist/core/wiring-manifest.d.ts +15 -1
- package/dist/core/wiring-manifest.js +10 -2
- package/dist/core/workflow-journal-store.d.ts +21 -2
- package/dist/core/workflow-journal-store.js +1 -1
- package/dist/engine/execution-env/node-execution-env.d.ts +2 -0
- package/dist/engine/execution-env/node-execution-env.js +2 -1
- package/dist/engine/harness/agent-harness.d.ts +3 -1
- package/dist/engine/harness/agent-harness.js +1 -1
- package/dist/engine/harness/types.d.ts +15 -2
- package/dist/index.d.ts +8 -4
- package/dist/index.js +7 -3
- package/dist/orchestration/run-workflow-tool.d.ts +22 -2
- package/dist/orchestration/run-workflow-tool.js +14 -1
- package/dist/orchestration/workflow-governance.d.ts +3 -2
- package/dist/orchestration/workflow-observe.d.ts +1 -1
- package/dist/orchestration/workflow-observe.js +2 -0
- package/dist/orchestration/workflow-primitives.d.ts +4 -1
- package/dist/orchestration/workflow-primitives.js +1 -6
- package/dist/orchestration/workflow-types.d.ts +37 -2
- package/dist/orchestration/workflow-types.js +16 -0
- package/dist/orchestration/workflow.d.ts +53 -6
- package/dist/orchestration/workflow.js +319 -51
- package/dist/prompt-assembly/turn-snapshot.d.ts +4 -2
- package/dist/stores/file/workflow-journal-store.js +10 -3
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +85 -5
package/dist/core/shell-lexer.js
CHANGED
|
@@ -13,11 +13,13 @@ const ALL_OPS = [...CONNECTOR_OPS, ...REDIRECT_OPS];
|
|
|
13
13
|
export const MAX_SHELL_READ_CHARS = 10_000;
|
|
14
14
|
export function readShellCommand(source) {
|
|
15
15
|
if (source.length > MAX_SHELL_READ_CHARS)
|
|
16
|
-
return { segments: [{ argv: [], peeled: [], unreadable: `the command is longer than ${MAX_SHELL_READ_CHARS} characters — not read` }], connectors: [], grouped: false };
|
|
16
|
+
return { segments: [{ argv: [], redirections: [], peeled: [], unreadable: `the command is longer than ${MAX_SHELL_READ_CHARS} characters — not read` }], connectors: [], grouped: false, backgrounded: false, strayRedirection: false };
|
|
17
17
|
const segments = [];
|
|
18
18
|
const connectors = [];
|
|
19
19
|
let grouped = false;
|
|
20
|
-
let
|
|
20
|
+
let backgrounded = false;
|
|
21
|
+
let strayRedirection = false;
|
|
22
|
+
let seg = { words: [], redirections: [] };
|
|
21
23
|
let word = freshWord();
|
|
22
24
|
let pendingRedirect;
|
|
23
25
|
let heredocs = [];
|
|
@@ -40,6 +42,7 @@ export function readShellCommand(source) {
|
|
|
40
42
|
badDelimiter = true;
|
|
41
43
|
heredocs.push({ delimiter: word.text, quoted: word.raw !== word.text, dash: pendingRedirect === "<<-" });
|
|
42
44
|
}
|
|
45
|
+
seg.redirections.push({ op: pendingRedirect, target: publicWord(word) });
|
|
43
46
|
pendingRedirect = undefined;
|
|
44
47
|
}
|
|
45
48
|
else {
|
|
@@ -56,17 +59,19 @@ export function readShellCommand(source) {
|
|
|
56
59
|
let built = buildSegment(seg);
|
|
57
60
|
if (built === "keyworded")
|
|
58
61
|
grouped = true;
|
|
62
|
+
if ((built === "empty" || built === "keyworded") && seg.redirections.length > 0)
|
|
63
|
+
strayRedirection = true;
|
|
59
64
|
if (built === "empty" || built === "keyworded") {
|
|
60
65
|
const continuation = connector === "\n" && lastConnector !== undefined && lastConnector !== ";" && lastConnector !== "&" && lastConnector !== "\n";
|
|
61
66
|
const legal = built === "keyworded" || afterBoundary || connector === undefined || connector === "\n" || lastConnector === "\n";
|
|
62
67
|
if (!legal)
|
|
63
|
-
built = { argv: [], peeled: [], unreadable: `an empty command before \`${connector}\` — a syntax error` };
|
|
68
|
+
built = { argv: [], redirections: seg.redirections, peeled: [], unreadable: `an empty command before \`${connector}\` — a syntax error` };
|
|
64
69
|
else {
|
|
65
70
|
if (connector !== undefined && connector !== "\n" && segments.length > 0)
|
|
66
71
|
grouped = true;
|
|
67
72
|
if (connector !== undefined && !continuation)
|
|
68
73
|
lastConnector = connector;
|
|
69
|
-
seg = { words: [] };
|
|
74
|
+
seg = { words: [], redirections: [] };
|
|
70
75
|
afterBoundary = false;
|
|
71
76
|
return;
|
|
72
77
|
}
|
|
@@ -76,7 +81,7 @@ export function readShellCommand(source) {
|
|
|
76
81
|
segments.push(built);
|
|
77
82
|
if (connector !== undefined)
|
|
78
83
|
lastConnector = connector;
|
|
79
|
-
seg = { words: [] };
|
|
84
|
+
seg = { words: [], redirections: [] };
|
|
80
85
|
afterBoundary = false;
|
|
81
86
|
};
|
|
82
87
|
const boundary = () => { finishSegment(undefined); grouped = true; afterBoundary = true; lastConnector = undefined; };
|
|
@@ -187,6 +192,8 @@ export function readShellCommand(source) {
|
|
|
187
192
|
i += op.length;
|
|
188
193
|
continue;
|
|
189
194
|
}
|
|
195
|
+
if (op === "&")
|
|
196
|
+
backgrounded = true;
|
|
190
197
|
finishSegment(op);
|
|
191
198
|
i += op.length;
|
|
192
199
|
continue;
|
|
@@ -271,7 +278,7 @@ export function readShellCommand(source) {
|
|
|
271
278
|
seg.unreadable ??= `the connector \`${lastConnector}\` has no command after it — a syntax error`;
|
|
272
279
|
finishSegment(undefined);
|
|
273
280
|
}
|
|
274
|
-
return { segments, connectors, grouped };
|
|
281
|
+
return { segments, connectors, grouped, backgrounded, strayRedirection };
|
|
275
282
|
}
|
|
276
283
|
const publicWord = ({ text, raw, expands }) => ({ text, raw, expands });
|
|
277
284
|
const SUBSTITUTION_UNREAD = "a command substitution runs a program this reader does not read";
|
|
@@ -281,8 +288,8 @@ export function isFullyReadable(shape) {
|
|
|
281
288
|
}
|
|
282
289
|
function buildSegment(seg) {
|
|
283
290
|
if (seg.unreadable !== undefined)
|
|
284
|
-
return { argv: seg.words.map(publicWord), peeled: [], unreadable: seg.unreadable };
|
|
285
|
-
const carried = () => (seg.substitution === true ? { argv: [], peeled: [], peelUnreadable: SUBSTITUTION_UNREAD } : keyworded ? "keyworded" : "empty");
|
|
291
|
+
return { argv: seg.words.map(publicWord), redirections: seg.redirections, peeled: [], unreadable: seg.unreadable };
|
|
292
|
+
const carried = () => (seg.substitution === true ? { argv: [], redirections: seg.redirections, peeled: [], peelUnreadable: SUBSTITUTION_UNREAD } : keyworded ? "keyworded" : "empty");
|
|
286
293
|
let keyworded = false;
|
|
287
294
|
let k0 = 0;
|
|
288
295
|
while (k0 < seg.words.length && seg.words[k0].expands === false && (PREFIX_KEYWORDS.has(seg.words[k0].text) || CLOSING_KEYWORDS.has(seg.words[k0].text))) {
|
|
@@ -298,7 +305,7 @@ function buildSegment(seg) {
|
|
|
298
305
|
if (head.expands === false && (head.text === "[[" || head.text === "["))
|
|
299
306
|
return carried();
|
|
300
307
|
if (head.expands === false && OPAQUE_KEYWORDS.has(head.text))
|
|
301
|
-
return { argv: words.map(publicWord), peeled: [], unreadable: `a \`${head.text}\` control structure — its commands need a syntax tree` };
|
|
308
|
+
return { argv: words.map(publicWord), redirections: seg.redirections, peeled: [], unreadable: `a \`${head.text}\` control structure — its commands need a syntax tree` };
|
|
302
309
|
const argv = words.map(publicWord);
|
|
303
310
|
const peeled = [];
|
|
304
311
|
let peelUnreadable;
|
|
@@ -338,7 +345,7 @@ function buildSegment(seg) {
|
|
|
338
345
|
}
|
|
339
346
|
if (seg.substitution === true || [argv, ...peeled].some((c) => c[0]?.expands === false && evaluatesOperand(c)))
|
|
340
347
|
peelUnreadable ??= SUBSTITUTION_UNREAD;
|
|
341
|
-
return { argv, peeled, ...(peelUnreadable !== undefined ? { peelUnreadable } : {}) };
|
|
348
|
+
return { argv, redirections: seg.redirections, peeled, ...(peelUnreadable !== undefined ? { peelUnreadable } : {}) };
|
|
342
349
|
}
|
|
343
350
|
function unwrap(words) {
|
|
344
351
|
const head = words[0];
|
|
@@ -50,9 +50,12 @@ export function evaluatesOperand(run) {
|
|
|
50
50
|
return run.some((w) => namesArrayElement(w.text) || namesArrayElement(w.raw) || (integer && /^[A-Za-z_][A-Za-z0-9_]*\+?=.*[A-Za-z_$]/.test(w.text)));
|
|
51
51
|
}
|
|
52
52
|
function namesArrayElement(word) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
for (let open = word.indexOf("["); open > 0; open = word.indexOf("[", open + 1)) {
|
|
54
|
+
if (!/[A-Za-z0-9_]/.test(word[open - 1]))
|
|
55
|
+
continue;
|
|
56
|
+
const close = word.indexOf("]", open);
|
|
57
|
+
if (close > 0 && /[A-Za-z_$]/.test(word.slice(open + 1, close)))
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
58
61
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The deployment seats a Runner may HOT-SWAP after construction — the vocabulary behind the ONE door,
|
|
3
|
+
* `Runner.swapDeps` (runtask.ts).
|
|
4
|
+
*
|
|
5
|
+
* WHY ONE DOOR. Each hot-swappable seat used to be a verb of its own (`swapModels` was the first), and a
|
|
6
|
+
* verb per seat restates the same three laws every time — validate the candidate BEFORE anything is
|
|
7
|
+
* written, write the deps object ONCE (a swap is atomic: a refused candidate leaves the current deps
|
|
8
|
+
* byte-identical), announce the change under a notice code — while the question "which seats may be
|
|
9
|
+
* swapped at all" was answered by the list of verbs, i.e. by nothing a compiler could see. With one door
|
|
10
|
+
* the laws are stated once, the swappable set is THIS closed list, and every seat carries its rows in
|
|
11
|
+
* the tables beside it: the compiler refuses a member without its rows ({@link AssertAllKeysHandled}),
|
|
12
|
+
* the door refuses a key outside the set at run time (never a silent ignore — a hot-config driver
|
|
13
|
+
* handing `{ brain }` must learn that the brain is not a swappable seat, not watch nothing happen).
|
|
14
|
+
*
|
|
15
|
+
* WHAT A SWAP MEANS — the natural snapshot, one sentence for every seat: a running task reads the deps
|
|
16
|
+
* object it was handed at its prepare and holds that reference; a swap replaces the Runner's object, so
|
|
17
|
+
* every LATER prepare reads the new seat and every in-flight leg keeps the value it prepared under.
|
|
18
|
+
* Nothing is re-routed, re-priced or re-fenced mid-run; the divergence window is exactly the in-flight
|
|
19
|
+
* legs' lifetime, by design (the `swapModels` precedent, now the rule for the whole set).
|
|
20
|
+
*
|
|
21
|
+
* KEY PRESENCE IS THE INSTRUCTION: a key present on the candidate is REPLACED (an explicit `undefined`
|
|
22
|
+
* CLEARS an optional seat back to its engine default — `readFace: undefined` ⇒ the deployment declares
|
|
23
|
+
* no read face, `tiers: undefined` ⇒ no tier bindings); a key absent is KEPT. `Object.hasOwn`, never
|
|
24
|
+
* truthiness, so the two spellings cannot be confused. An EMPTY candidate is refused: a swap that names
|
|
25
|
+
* no seat is a caller defect, not a no-op.
|
|
26
|
+
*/
|
|
27
|
+
import type { AssertAllKeysHandled } from "./ask-origin.js";
|
|
28
|
+
import type { RunnerDeps } from "./runner-deps.js";
|
|
29
|
+
/**
|
|
30
|
+
* The closed set of hot-swappable deployment seats.
|
|
31
|
+
* - `models` — the model catalog generation (the same-name redirect, re-pricing and retirement rules
|
|
32
|
+
* of the former `swapModels`: a string ref resolves against the new catalog at the next
|
|
33
|
+
* prepare; a `Model` object a leg already holds is untouched).
|
|
34
|
+
* - `tiers` — the tier bindings, a BINDING OVER a catalog: it may only be swapped together with
|
|
35
|
+
* `models` (the door refuses `tiers` alone — re-expanding bindings over an already
|
|
36
|
+
* expanded catalog would keep the retired tier words as ordinary keys, which is exactly
|
|
37
|
+
* the stale-generation shape the swap exists to end). Omitted beside `models` ⇒ the
|
|
38
|
+
* current bindings are re-applied over the new catalog; present (including `undefined`)
|
|
39
|
+
* ⇒ replaced.
|
|
40
|
+
* - `readFace` — the deployment's read-face declaration ({@link RunnerDeps.readFace}); screened by the
|
|
41
|
+
* same value gate every prepare applies (`assertReadFaceValue`: `"open"` | `"roots"`
|
|
42
|
+
* | absent, anything else refused loudly). The RESOLVED face of a leg is still the
|
|
43
|
+
* resolver's business (task seat, governance, the read-only mount); the swap changes the
|
|
44
|
+
* deployment seat that resolution reads.
|
|
45
|
+
*/
|
|
46
|
+
export declare const SWAPPABLE_DEP_SEATS: readonly ["models", "tiers", "readFace"];
|
|
47
|
+
export type SwappableDepSeat = (typeof SWAPPABLE_DEP_SEATS)[number];
|
|
48
|
+
/** The candidate a swap takes: the swappable seats of {@link RunnerDeps}, each optional (presence = instruction). */
|
|
49
|
+
export type SwappableDeps = Pick<RunnerDeps, SwappableDepSeat>;
|
|
50
|
+
/** Whether a key names a swappable seat. */
|
|
51
|
+
export declare function isSwappableDepSeat(k: unknown): k is SwappableDepSeat;
|
|
52
|
+
/**
|
|
53
|
+
* Which notice a seat's successful swap is announced under — the disposition table over the set (a seat
|
|
54
|
+
* with no row does not compile: {@link SwapNoticeTableCoversEverySeat}). `models` and `tiers` share the
|
|
55
|
+
* catalog-generation line (one swap, one line, both counts); the read face has its own.
|
|
56
|
+
*/
|
|
57
|
+
export declare const SWAP_SEAT_NOTICE: {
|
|
58
|
+
readonly models: "config.models_swapped";
|
|
59
|
+
readonly tiers: "config.models_swapped";
|
|
60
|
+
readonly readFace: "config.read_face_swapped";
|
|
61
|
+
};
|
|
62
|
+
/** The fence over the notice table: `never` while every seat has a row. */
|
|
63
|
+
export type SwapNoticeTableCoversEverySeat = AssertAllKeysHandled<Exclude<SwappableDepSeat, keyof typeof SWAP_SEAT_NOTICE>>;
|
|
64
|
+
/** What the screen hands back: the seats present (set order) and the ONE read of each present seat's value,
|
|
65
|
+
* copied at screen time — the door writes from THIS object and never re-reads the caller's. */
|
|
66
|
+
export interface ScreenedSwappableDeps {
|
|
67
|
+
readonly seats: readonly SwappableDepSeat[];
|
|
68
|
+
/** A fresh plain object: the screened value of every present seat (`models`/`tiers` shallow-copied,
|
|
69
|
+
* `readFace` the screened word; an explicit `undefined` is kept as a PRESENT own key so the door can
|
|
70
|
+
* tell "clear this seat" from "not named"). */
|
|
71
|
+
readonly candidate: SwappableDeps;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Screen a swap candidate BEFORE anything is written: the object shape, the key set (every own key must
|
|
75
|
+
* be a member of {@link SWAPPABLE_DEP_SEATS}; at least one), the `tiers`-rides-with-`models` rule, and
|
|
76
|
+
* each present seat's own value gate. Returns the seats present (set order) and a SNAPSHOT of their values
|
|
77
|
+
* — the one read. Throws on the first defect; the caller has written nothing yet, so the current deps stay
|
|
78
|
+
* in force byte-identical.
|
|
79
|
+
*
|
|
80
|
+
* ONE READ, BY CONSTRUCTION: every seat value is read off the caller's object exactly once, here, and the
|
|
81
|
+
* screened copy is what the door writes and announces. A getter-backed or concurrently-mutated candidate
|
|
82
|
+
* therefore cannot present a legal value to the screen and a different one to the write (or to the
|
|
83
|
+
* announcement, which runs AFTER the write — a throw there would have left a half-announced swap in
|
|
84
|
+
* force). The prepare doors read their deps seats the same way.
|
|
85
|
+
*
|
|
86
|
+
* The per-seat gate is the SAME one the seat's prepare-time reader applies (`readFace` ⇒
|
|
87
|
+
* `assertReadFaceValue` with the deployment-seat name a prepare door uses — one value, one loudness, one
|
|
88
|
+
* seat name on every leg), so a value the door admits is a value every later prepare admits.
|
|
89
|
+
*/
|
|
90
|
+
export declare function screenSwappableDeps(next: unknown): ScreenedSwappableDeps;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { assertReadFaceValue } from "../tools/fs/read-face.js";
|
|
2
|
+
export const SWAPPABLE_DEP_SEATS = ["models", "tiers", "readFace"];
|
|
3
|
+
const SWAPPABLE_DEP_SEAT_SET = new Set(SWAPPABLE_DEP_SEATS);
|
|
4
|
+
export function isSwappableDepSeat(k) {
|
|
5
|
+
return SWAPPABLE_DEP_SEAT_SET.has(k);
|
|
6
|
+
}
|
|
7
|
+
export const SWAP_SEAT_NOTICE = {
|
|
8
|
+
models: "config.models_swapped",
|
|
9
|
+
tiers: "config.models_swapped",
|
|
10
|
+
readFace: "config.read_face_swapped",
|
|
11
|
+
};
|
|
12
|
+
export function screenSwappableDeps(next) {
|
|
13
|
+
if (next === null || typeof next !== "object" || Array.isArray(next)) {
|
|
14
|
+
throw new Error(`swapDeps: the candidate must be a plain object naming the seats to swap (got ${next === null ? "null" : Array.isArray(next) ? "array" : typeof next}) — the current deps stay in force`);
|
|
15
|
+
}
|
|
16
|
+
const keys = Object.keys(next);
|
|
17
|
+
const unknown = keys.filter((k) => !isSwappableDepSeat(k));
|
|
18
|
+
if (unknown.length > 0) {
|
|
19
|
+
throw new Error(`swapDeps: ${unknown.map((k) => JSON.stringify(k)).join(", ")} is not a hot-swappable seat (the closed set is ${SWAPPABLE_DEP_SEATS.join(" | ")}) — the current deps stay in force`);
|
|
20
|
+
}
|
|
21
|
+
const present = SWAPPABLE_DEP_SEATS.filter((seat) => Object.hasOwn(next, seat));
|
|
22
|
+
if (present.length === 0) {
|
|
23
|
+
throw new Error(`swapDeps: the candidate names no seat (an empty swap is a caller defect, not a no-op) — the current deps stay in force`);
|
|
24
|
+
}
|
|
25
|
+
const raw = next;
|
|
26
|
+
const candidate = {};
|
|
27
|
+
for (const seat of present) {
|
|
28
|
+
const v = raw[seat];
|
|
29
|
+
switch (seat) {
|
|
30
|
+
case "models":
|
|
31
|
+
if (v === null || typeof v !== "object" || Array.isArray(v)) {
|
|
32
|
+
throw new Error(`swapDeps: models must be a plain Record<string, Model> (got ${v === null ? "null" : Array.isArray(v) ? "array" : typeof v}) — the current generation stays in force`);
|
|
33
|
+
}
|
|
34
|
+
candidate.models = { ...v };
|
|
35
|
+
break;
|
|
36
|
+
case "tiers":
|
|
37
|
+
if (!present.includes("models")) {
|
|
38
|
+
throw new Error(`swapDeps: tiers is a binding over models — pass the catalog it binds in the same swap (\`{ models, tiers }\`); the current generation stays in force`);
|
|
39
|
+
}
|
|
40
|
+
if (v !== undefined && (v === null || typeof v !== "object" || Array.isArray(v))) {
|
|
41
|
+
throw new Error(`swapDeps: tiers must be a plain Record<string, ModelRef> or undefined (got ${v === null ? "null" : Array.isArray(v) ? "array" : typeof v}) — the current generation stays in force`);
|
|
42
|
+
}
|
|
43
|
+
candidate.tiers = v === undefined ? undefined : { ...v };
|
|
44
|
+
break;
|
|
45
|
+
case "readFace":
|
|
46
|
+
candidate.readFace = assertReadFaceValue(v, "readFace (deployment seat)");
|
|
47
|
+
break;
|
|
48
|
+
default: {
|
|
49
|
+
const _exhaustive = seat;
|
|
50
|
+
throw new Error(`swapDeps: unhandled seat ${String(_exhaustive)}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { seats: present, candidate: candidate };
|
|
55
|
+
}
|
|
@@ -99,7 +99,10 @@ export interface ToolCallRequest {
|
|
|
99
99
|
* - `"org_unavailable"` — an org-governed deployment could not adjudicate against a snapshot, so the
|
|
100
100
|
* whole decision boundary failed closed (see `ORG_UNAVAILABLE_DECISION_REASON`, the single
|
|
101
101
|
* spelling this word is minted from). */
|
|
102
|
-
|
|
102
|
+
/** `read_only` (#619): the shell tool's command was PROVABLY read-only by the engine's transcribed upstream
|
|
103
|
+
* tables (`readOnlyShellVerdict`) and the allow layer let it run without a question — after every
|
|
104
|
+
* deny/ask lane and the person's own allow rules, never over a mandated ask. */
|
|
105
|
+
declare const DECISION_REASONS: readonly ["rule", "mode", "hook", "safety", "classifier", "persisted_rule", "sandbox", "org_rule", "org_unavailable", "read_only"];
|
|
103
106
|
export type DecisionReason = (typeof DECISION_REASONS)[number];
|
|
104
107
|
/** Read the engine-attested settlement off a funneled decision FOR the named call (the gate's exit is the
|
|
105
108
|
* one consumer): an attestation bound to a different call is a replayed object and answers absence.
|
|
@@ -204,6 +207,23 @@ export type PermissionResult = {
|
|
|
204
207
|
* same safe direction as `matchedAskRule`); it cannot state a window of its own, because the
|
|
205
208
|
* route it would be a window for has not been chosen yet. */
|
|
206
209
|
denialLimitFallback?: import("./auto-mode.js").UnarmedDenialLimitFallback;
|
|
210
|
+
/** #616 (additive): the auto-mode classifier was CONSULTED on this ask and could not run — the ask
|
|
211
|
+
* flows the original chain exactly as it would have (routing, origin, bit and members unchanged),
|
|
212
|
+
* carrying the station FACT beside them so a card can say "asked because the classifier was
|
|
213
|
+
* unavailable (timeout)" instead of reading as ordinary hesitation. `cause` is the verdict's own
|
|
214
|
+
* word ({@link import("./auto-mode.js").AutoModeUnavailableCause}: `error` / `timeout` /
|
|
215
|
+
* `breaker_open`). A FACT, not an origin: CC 2.1.250 marks the same condition as a denial KIND on
|
|
216
|
+
* the outcome (`automode-unavailable`) beside the decision's provenance, never in place of it, and
|
|
217
|
+
* this engine's divergence (the ask reaches a person instead of being denied) does not move the
|
|
218
|
+
* fact onto the origin axis either. ENGINE-STAMPED at the classifier stations (the gate's own and
|
|
219
|
+
* the inherited-lane arms) only when the verdict was `unavailable`; `parse_error` stamps nothing
|
|
220
|
+
* (the classifier ran and answered outside its contract — a different sentence). Display metadata:
|
|
221
|
+
* nothing reads it to decide anything, so a policy that self-declares it can only put its own
|
|
222
|
+
* sentence on its own card. Carried onto the approval request and the durable row by the carry
|
|
223
|
+
* stations. */
|
|
224
|
+
classifierUnavailable?: {
|
|
225
|
+
readonly cause: import("./auto-mode.js").AutoModeUnavailableCause;
|
|
226
|
+
};
|
|
207
227
|
/** #144 disclosure (additive): a persisted allow rule MATCHED this call but could not clear the
|
|
208
228
|
* ask, because the ask is MANDATED (operator shellGate:"always", or the tool's own
|
|
209
229
|
* egress/irreversibility marks) rather than a classifier's hesitation — "allow rules silence
|
|
@@ -1191,6 +1211,15 @@ export interface AskRequest {
|
|
|
1191
1211
|
* that forwards a decision's member here does not compile, which is the point: the window is the
|
|
1192
1212
|
* route's fact, and forwarding it silently is how a configured window becomes a wait with none. */
|
|
1193
1213
|
readonly denialLimitFallback?: import("./auto-mode.js").DenialLimitFallback;
|
|
1214
|
+
/** #616 (additive) — present ⇔ the auto-mode classifier was consulted on this ask and could not run (see
|
|
1215
|
+
* the {@link PermissionResult} ask-arm member of the same name): the card's "asked because the
|
|
1216
|
+
* classifier was unavailable" fact, with the verdict's own cause word. Absent on every ask the classifier
|
|
1217
|
+
* answered, was not eligible for, or was not wired for — read presence, never absence. Filled by the
|
|
1218
|
+
* carry stations from the decision, never a caller/worker-settable field; the durable park row carries
|
|
1219
|
+
* the same member (`PendingAction.tool_approval.classifierUnavailable`). */
|
|
1220
|
+
readonly classifierUnavailable?: {
|
|
1221
|
+
readonly cause: import("./auto-mode.js").AutoModeUnavailableCause;
|
|
1222
|
+
};
|
|
1194
1223
|
/** (additive) WHICH AUTHORITY raised this ask — the wire twin of the {@link PermissionResult} ask-arm
|
|
1195
1224
|
* member of the same name, one word from the closed {@link import("./ask-origin.js").AskOrigin} set.
|
|
1196
1225
|
* The gate's own mint station copies the engine-stamped word; the three inherited-lane stations
|
package/dist/core/tool-policy.js
CHANGED
|
@@ -10,7 +10,7 @@ import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY } from "./unt
|
|
|
10
10
|
import { isNamespacedCoveringRuleName, namespacedRuleNameCovers, parsePermissionRule } from "./permission-rules.js";
|
|
11
11
|
import { protocolOf } from "./protocol-table.js";
|
|
12
12
|
import { isAbsolutePathForm, isWinFormPath, writeTargetPath } from "../tools/fs/safety.js";
|
|
13
|
-
const DECISION_REASONS = ["rule", "mode", "hook", "safety", "classifier", "persisted_rule", "sandbox", "org_rule", "org_unavailable"];
|
|
13
|
+
const DECISION_REASONS = ["rule", "mode", "hook", "safety", "classifier", "persisted_rule", "sandbox", "org_rule", "org_unavailable", "read_only"];
|
|
14
14
|
const DECISION_REASON_SET = new Set(DECISION_REASONS);
|
|
15
15
|
function settledByNobody(kind) {
|
|
16
16
|
return { kind, who: { party: "none" }, when: Date.now() };
|
|
@@ -410,6 +410,7 @@ export function combinePolicies(...policies) {
|
|
|
410
410
|
let ruleAskText;
|
|
411
411
|
let probeMandateSeen = false;
|
|
412
412
|
let fallbackSeen;
|
|
413
|
+
let classifierUnavailableSeen;
|
|
413
414
|
for (const p of policies) {
|
|
414
415
|
const d = refuseOutOfContractDecision(await p.check(current, signal));
|
|
415
416
|
if (d.action === "deny") {
|
|
@@ -440,13 +441,16 @@ export function combinePolicies(...policies) {
|
|
|
440
441
|
probeMandateSeen = true;
|
|
441
442
|
if (d.action === "ask" && d.denialLimitFallback !== undefined && fallbackSeen === undefined)
|
|
442
443
|
fallbackSeen = d.denialLimitFallback;
|
|
444
|
+
if (d.action === "ask" && d.classifierUnavailable !== undefined && classifierUnavailableSeen === undefined)
|
|
445
|
+
classifierUnavailableSeen = d.classifierUnavailable;
|
|
443
446
|
}
|
|
444
447
|
if (asked) {
|
|
445
448
|
const merged = rewrite?.updatedInput;
|
|
446
449
|
const withRuleAsk = ruleAskText !== undefined && asked.matchedAskRule === undefined ? { ...asked, matchedAskRule: ruleAskText } : asked;
|
|
447
450
|
const withMark = probeMandateSeen && withRuleAsk.probeMandated !== true ? { ...withRuleAsk, probeMandated: true } : withRuleAsk;
|
|
448
451
|
const withFallback = fallbackSeen !== undefined && withMark.denialLimitFallback === undefined ? { ...withMark, denialLimitFallback: fallbackSeen, requiresRealApproval: true } : withMark;
|
|
449
|
-
|
|
452
|
+
const withFact = classifierUnavailableSeen !== undefined && withFallback.classifierUnavailable === undefined ? { ...withFallback, classifierUnavailable: { cause: classifierUnavailableSeen.cause } } : withFallback;
|
|
453
|
+
return merged !== undefined ? { ...withFact, updatedInput: merged } : withFact;
|
|
450
454
|
}
|
|
451
455
|
const allowed = rewrite ?? ALLOW;
|
|
452
456
|
if (settledAllow === undefined)
|
package/dist/core/tools.d.ts
CHANGED
|
@@ -15,14 +15,34 @@ export declare function errorResult(text: string, details?: unknown): {
|
|
|
15
15
|
isError: true;
|
|
16
16
|
details?: unknown;
|
|
17
17
|
};
|
|
18
|
-
/**
|
|
18
|
+
/** The seat's function: the mount's ctx builder and the very object being mounted (the seal compares it to the stamped one). */
|
|
19
|
+
type DefineToolRebind = (enrich: ToolCtxEnricher, mounted: object) => AgentTool;
|
|
20
|
+
/** True iff `x` is an `AgentTool` this module's `defineTool` itself constructed — see {@link DEFINE_TOOL_BRAND}.
|
|
21
|
+
* An OWN property read: an object that merely INHERITS the brand (`Object.create(product)`, a Proxy over it)
|
|
22
|
+
* is not the product — the rebind seat would rebuild the product and drop whatever the derived object
|
|
23
|
+
* overrode, so such objects are not recognised here and fall to the spec arm's contract (which they do not
|
|
24
|
+
* meet either; see {@link stampDefineToolBrand} for the one supported wrapper form). */
|
|
19
25
|
export declare function isDefineToolProduct(x: unknown): x is AgentTool;
|
|
20
|
-
/** RB-362 类修同源章点 — the ONE place the brand is stamped
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
|
|
26
|
+
/** RB-362 类修同源章点 — the ONE place the brand is stamped, and with it the rebind seat (a branded object
|
|
27
|
+
* ALWAYS carries one; that invariant is what lets the caller mount rebind every product without a
|
|
28
|
+
* "cannot enrich" arm). `defineTool` uses it on its own product; the only OTHER legitimate caller is a
|
|
29
|
+
* wrapper that (a) starts from a branded product, (b) preserves the AgentTool
|
|
30
|
+
* `execute(toolCallId, rawParams, signal, onUpdate)` contract faithfully and (c) hands over a `rebind`
|
|
31
|
+
* that re-wraps the REBOUND inner product the same way (e.g. teacher.ts's logging wrapper) — so a mount
|
|
32
|
+
* rebinding the wrapper gets a wrapper over an enriched product, not an enriched product without the
|
|
33
|
+
* wrapper. A shallow copy that does NOT re-stamp deliberately loses the brand — that is the brand's
|
|
34
|
+
* documented survival contract, not an accident. Internal only. */
|
|
35
|
+
export declare function stampDefineToolBrand<T extends object>(tool: T, rebind: DefineToolRebind): T;
|
|
36
|
+
/**
|
|
37
|
+
* A branded product REBOUND to a mount's ctx builder — the product's own `execute` rebuilt over its own
|
|
38
|
+
* spec with `enrich` applied per call (see {@link ToolCtxEnricher}; a builder the product was built with
|
|
39
|
+
* runs AFTER the mount's, refining the run's trusted seats, and the per-call identity is re-stamped after
|
|
40
|
+
* both). This is how a `defineTool()` product handed to `TaskSpec.tools` receives exactly the ctx a raw
|
|
41
|
+
* `ToolSpec` on the same mount receives: one mount law, whatever object shape the caller handed in.
|
|
42
|
+
* Throws on an unbranded object — the caller checks {@link isDefineToolProduct} first; a branded object
|
|
43
|
+
* without the seat cannot be constructed (both are written at the one stamp site).
|
|
44
|
+
*/
|
|
45
|
+
export declare function rebindDefineToolCtx(product: AgentTool, enrich: ToolCtxEnricher): AgentTool;
|
|
26
46
|
/**
|
|
27
47
|
* RB-409 — a per-call ctx builder a MOUNT hands to {@link defineTool}.
|
|
28
48
|
*
|
|
@@ -58,3 +78,4 @@ export interface DefineToolOptions {
|
|
|
58
78
|
}
|
|
59
79
|
/** Adapt a friendly ToolSpec into the vendored AgentTool the agent loop expects. */
|
|
60
80
|
export declare function defineTool<TParams extends TSchema = TSchema>(spec: ToolSpec<TParams>, options?: DefineToolOptions): AgentTool<TParams>;
|
|
81
|
+
export {};
|
package/dist/core/tools.js
CHANGED
|
@@ -26,13 +26,46 @@ export function errorResult(text, details) {
|
|
|
26
26
|
return details === undefined ? { content: text, isError: true } : { content: text, isError: true, details };
|
|
27
27
|
}
|
|
28
28
|
const DEFINE_TOOL_BRAND = Symbol("sema.core.defineTool.product");
|
|
29
|
+
const DEFINE_TOOL_REBIND = Symbol("sema.core.defineTool.rebind");
|
|
29
30
|
export function isDefineToolProduct(x) {
|
|
30
|
-
return typeof x === "object" && x !== null && x
|
|
31
|
+
return typeof x === "object" && x !== null && Object.getOwnPropertyDescriptor(x, DEFINE_TOOL_BRAND)?.value === true;
|
|
31
32
|
}
|
|
32
|
-
export function stampDefineToolBrand(tool) {
|
|
33
|
+
export function stampDefineToolBrand(tool, rebind) {
|
|
33
34
|
Object.defineProperty(tool, DEFINE_TOOL_BRAND, { value: true, enumerable: false });
|
|
35
|
+
Object.defineProperty(tool, DEFINE_TOOL_REBIND, { value: sealedRebind(tool, rebind), enumerable: false });
|
|
34
36
|
return tool;
|
|
35
37
|
}
|
|
38
|
+
function sealedRebind(stamped, rebuild) {
|
|
39
|
+
const ownExecute = Object.getOwnPropertyDescriptor(stamped, "execute")?.value;
|
|
40
|
+
const stillSealed = () => {
|
|
41
|
+
const d = Object.getOwnPropertyDescriptor(stamped, "execute");
|
|
42
|
+
return d !== undefined && "value" in d && d.value === ownExecute;
|
|
43
|
+
};
|
|
44
|
+
return (enrich, mounted) => {
|
|
45
|
+
if (mounted !== stamped) {
|
|
46
|
+
throw new Error(`defineTool product ${JSON.stringify(stamped.name)}: the object being mounted is not the one this brand was stamped on (a derived object — prototype, proxy or descriptor copy — carrying a borrowed brand), so it cannot be rebound to the run's ctx without dropping its own overrides — wrap the product as a NEW object (stampDefineToolBrand with a rebind that re-wraps the rebound inner product), or author it as a raw ToolSpec`);
|
|
47
|
+
}
|
|
48
|
+
const refuse = () => {
|
|
49
|
+
throw new Error(`defineTool product ${JSON.stringify(stamped.name)}: its execute is not the own data property the factory wrote (replaced, or turned into an accessor, after the factory built it), so it cannot be rebound to the run's ctx without dropping the replacement — wrap the product as a NEW object (stampDefineToolBrand with a rebind that re-wraps the rebound inner product), or author it as a raw ToolSpec`);
|
|
50
|
+
};
|
|
51
|
+
if (!stillSealed())
|
|
52
|
+
refuse();
|
|
53
|
+
const rebound = rebuild(enrich, mounted);
|
|
54
|
+
if (!stillSealed())
|
|
55
|
+
refuse();
|
|
56
|
+
return rebound;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export function rebindDefineToolCtx(product, enrich) {
|
|
60
|
+
const rebind = Object.getOwnPropertyDescriptor(product, DEFINE_TOOL_REBIND)?.value;
|
|
61
|
+
if (typeof rebind !== "function")
|
|
62
|
+
throw new Error(`rebindDefineToolCtx: ${product.name} is not a defineTool product (no rebind seat)`);
|
|
63
|
+
return rebind(enrich, product);
|
|
64
|
+
}
|
|
65
|
+
function stampCallIdentity(enriched, toolCallId, signal) {
|
|
66
|
+
const own = (value) => ({ value, enumerable: true, configurable: true, writable: true });
|
|
67
|
+
return Object.create(Object.getPrototypeOf(enriched), { ...Object.getOwnPropertyDescriptors(enriched), toolCallId: own(toolCallId), signal: own(signal) });
|
|
68
|
+
}
|
|
36
69
|
export function defineTool(spec, options) {
|
|
37
70
|
const executionMode = spec.executionMode ?? (spec.effect === "read" ? "parallel" : "sequential");
|
|
38
71
|
const tool = {
|
|
@@ -77,7 +110,8 @@ export function defineTool(spec, options) {
|
|
|
77
110
|
let ret;
|
|
78
111
|
try {
|
|
79
112
|
const baseCtx = { toolCallId, signal };
|
|
80
|
-
|
|
113
|
+
const ctx = options?.enrichCtx ? stampCallIdentity(options.enrichCtx(baseCtx), toolCallId, signal) : baseCtx;
|
|
114
|
+
ret = await spec.execute(params, ctx);
|
|
81
115
|
}
|
|
82
116
|
catch (err) {
|
|
83
117
|
const wrapped = new Error(formatToolError(err));
|
|
@@ -105,6 +139,12 @@ export function defineTool(spec, options) {
|
|
|
105
139
|
...(spec.aliases && spec.aliases.length > 0 ? { aliases: spec.aliases } : {}),
|
|
106
140
|
});
|
|
107
141
|
}
|
|
108
|
-
stampDefineToolBrand(tool)
|
|
142
|
+
stampDefineToolBrand(tool, (enrich) => {
|
|
143
|
+
const composed = options?.enrichCtx === undefined ? enrich : (base) => options.enrichCtx(enrich(base));
|
|
144
|
+
const rebuilt = defineTool(spec, { ...options, enrichCtx: composed });
|
|
145
|
+
const { [DEFINE_TOOL_BRAND]: _brand, [DEFINE_TOOL_REBIND]: _seat, ...face } = Object.getOwnPropertyDescriptors(tool);
|
|
146
|
+
const copy = Object.create(Object.getPrototypeOf(tool), { ...face, execute: { value: rebuilt.execute, enumerable: true, configurable: true, writable: true } });
|
|
147
|
+
return stampDefineToolBrand(copy, (again) => rebindDefineToolCtx(rebuilt, again));
|
|
148
|
+
});
|
|
109
149
|
return tool;
|
|
110
150
|
}
|
package/dist/core/trace.d.ts
CHANGED
|
@@ -425,6 +425,21 @@ export type TraceEvent = {
|
|
|
425
425
|
toolCallId: string;
|
|
426
426
|
rules: readonly string[];
|
|
427
427
|
ts: number;
|
|
428
|
+
} | {
|
|
429
|
+
/**
|
|
430
|
+
* #619 — the READ-ONLY reader cleared a shell call, so no person and no classifier was asked: the
|
|
431
|
+
* allow layer's second attribution channel beside `permission.persisted_rule_allowed`. `command` is
|
|
432
|
+
* the FINAL command the gate judged (a policy rewrite included) — model-authored text, carried
|
|
433
|
+
* verbatim as the audit fact this frame exists for (a consumer rendering it applies its own
|
|
434
|
+
* display sanitizer, as it does for `tool_start` args).
|
|
435
|
+
*/
|
|
436
|
+
kind: "permission.read_only_allowed";
|
|
437
|
+
version: 1;
|
|
438
|
+
taskId: string;
|
|
439
|
+
toolName: string;
|
|
440
|
+
toolCallId: string;
|
|
441
|
+
command: string;
|
|
442
|
+
ts: number;
|
|
428
443
|
} | {
|
|
429
444
|
/**
|
|
430
445
|
* design/179 — the persisted allow-rule store could not be read, so this call was adjudicated with
|
|
@@ -244,10 +244,21 @@ export interface WiringManifest {
|
|
|
244
244
|
* classifier decider was minted for this leg), and `reason` names the first arm that failed
|
|
245
245
|
* otherwise — see {@link AUTO_MODE_ARM_REASONS}. A serving layer that used to infer the mode from
|
|
246
246
|
* spec shape reads it here instead.
|
|
247
|
+
*
|
|
248
|
+
* `breaker` (#616) — the SESSION-level breaker read face: the most recent one-way breaker trip recorded
|
|
249
|
+
* for this session on this Runner (an EARLIER leg's decider — the latch is a per-run fact and a leg-start
|
|
250
|
+
* manifest's own decider is closed by construction, which is why there is no `open` boolean here: it
|
|
251
|
+
* would read `false` on every mint; the reason vocabulary reserves `latch_open` for a mid-leg re-read
|
|
252
|
+
* face nothing mints today). Present ⇔ a trip was recorded (never tripped, evicted or a standalone
|
|
253
|
+
* prepare ⇒ absent). `{ openedAtMs, lastCause, failures, runId }` — see
|
|
254
|
+
* {@link import("./auto-mode.js").AutoModeBreakerTrip}. A per-leg observation like `mcp`, so it is NOT
|
|
255
|
+
* part of {@link configFingerprint}: legs of one assembly must fingerprint alike whether or not a
|
|
256
|
+
* classifier was down.
|
|
247
257
|
*/
|
|
248
258
|
autoMode?: {
|
|
249
259
|
armed: boolean;
|
|
250
260
|
reason: AutoModeArmReason;
|
|
261
|
+
breaker?: import("./auto-mode.js").AutoModeBreakerTrip;
|
|
251
262
|
};
|
|
252
263
|
/**
|
|
253
264
|
* EFFECTIVE half only, and ALWAYS present on an engine-minted effective manifest (the `autoMode`
|
|
@@ -336,10 +347,13 @@ export interface WiringFacts {
|
|
|
336
347
|
restore: string;
|
|
337
348
|
};
|
|
338
349
|
/** Effective half only — see {@link WiringManifest.autoMode}; the static half has no leg to arm.
|
|
339
|
-
* `armed` must agree with `reason` (`armed ⇔ reason === "armed"`); a contradicting pair is refused.
|
|
350
|
+
* `armed` must agree with `reason` (`armed ⇔ reason === "armed"`); a contradicting pair is refused.
|
|
351
|
+
* `breaker` (#616) is the session's most recent recorded trip, or absent; a trip whose cause is outside
|
|
352
|
+
* the closed set or whose numbers are not finite is refused. */
|
|
340
353
|
autoMode?: {
|
|
341
354
|
armed: boolean;
|
|
342
355
|
reason: AutoModeArmReason;
|
|
356
|
+
breaker?: import("./auto-mode.js").AutoModeBreakerTrip;
|
|
343
357
|
};
|
|
344
358
|
/** Effective half only — see {@link WiringManifest.mcp}; the static half materializes nothing.
|
|
345
359
|
* Copied entry-wise onto the manifest (the caller's array is never aliased). */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { AUTO_MODE_BREAKER_CAUSES, isAutoModeBreakerCause } from "./auto-mode.js";
|
|
2
3
|
import { canonicalize } from "./canonical-json.js";
|
|
3
4
|
import { resolveCheckpointStore } from "./checkpoint-store.js";
|
|
4
5
|
import { isLiveQuestionFace } from "./ask-question.js";
|
|
@@ -71,7 +72,13 @@ function readAutoModeFact(fact) {
|
|
|
71
72
|
if (!AUTO_MODE_ARM_REASONS.includes(fact.reason) || fact.armed !== (fact.reason === "armed")) {
|
|
72
73
|
throw new Error(`the auto-mode wiring fact is inconsistent (armed=${String(fact.armed)}, reason=${JSON.stringify(fact.reason)}) — armed must hold exactly when reason is "armed", and reason must be one of ${AUTO_MODE_ARM_REASONS.join("|")}`);
|
|
73
74
|
}
|
|
74
|
-
|
|
75
|
+
const b = fact.breaker;
|
|
76
|
+
if (b === undefined)
|
|
77
|
+
return { armed: fact.armed, reason: fact.reason };
|
|
78
|
+
if (!isAutoModeBreakerCause(b.lastCause) || !Number.isFinite(b.openedAtMs) || !Number.isInteger(b.failures) || b.failures < 1 || typeof b.runId !== "string" || b.runId === "") {
|
|
79
|
+
throw new Error(`the auto-mode breaker fact is malformed (${JSON.stringify(b)}) — lastCause must be one of ${AUTO_MODE_BREAKER_CAUSES.join("|")}, openedAtMs finite, failures a positive integer, runId a non-empty string`);
|
|
80
|
+
}
|
|
81
|
+
return { armed: fact.armed, reason: fact.reason, breaker: { openedAtMs: b.openedAtMs, lastCause: b.lastCause, failures: b.failures, runId: b.runId } };
|
|
75
82
|
}
|
|
76
83
|
export function deriveWiringManifest(facts) {
|
|
77
84
|
if (facts.half === "static" && facts.leg !== undefined) {
|
|
@@ -133,8 +140,9 @@ export function deriveWiringManifest(facts) {
|
|
|
133
140
|
const { leg: _leg, mcp: _mcp, tools: _tools, ...assembly } = manifest;
|
|
134
141
|
const { provenance: _askSeat, ...askForHash } = assembly.ask;
|
|
135
142
|
const { provenance: _questionSeat, ...questionForHash } = assembly.question;
|
|
143
|
+
const { breaker: _breaker, ...autoModeForHash } = assembly.autoMode ?? {};
|
|
136
144
|
manifest.configFingerprint = createHash("sha256")
|
|
137
|
-
.update(canonicalize({ ...assembly, ask: askForHash, question: questionForHash }))
|
|
145
|
+
.update(canonicalize({ ...assembly, ask: askForHash, question: questionForHash, ...(assembly.autoMode !== undefined ? { autoMode: autoModeForHash } : {}) }))
|
|
138
146
|
.digest("hex")
|
|
139
147
|
.slice(0, 16);
|
|
140
148
|
}
|
|
@@ -5,12 +5,31 @@ import type { TaskResult } from "./types.js";
|
|
|
5
5
|
* longest unchanged PREFIX of these (keyed by the deterministic {@link workflowAgentCallKey}) and runs only the
|
|
6
6
|
* first changed/new call + everything after it live. Same script + same args → 100% cache hit.
|
|
7
7
|
*/
|
|
8
|
-
export
|
|
8
|
+
export type WorkflowJournalEntry = {
|
|
9
9
|
/** The agent's deterministic call key (`ordinal:specIdentityHash`) — the replay match key. */
|
|
10
10
|
callKey: string;
|
|
11
11
|
/** The agent's TaskResult, replayed verbatim when a resume's call key matches at the same ordinal. */
|
|
12
12
|
result: TaskResult;
|
|
13
|
-
|
|
13
|
+
parked?: never;
|
|
14
|
+
} | {
|
|
15
|
+
callKey: string;
|
|
16
|
+
result?: never;
|
|
17
|
+
/**
|
|
18
|
+
* #642 — the ordinal's leg is PARKED at a durable approval gate: the child's own paused `TaskResult`
|
|
19
|
+
* (its `terminal` carries the checkpoint token + gate, its `sessionId` the pinned session). A resume
|
|
20
|
+
* reaching this ordinal with a matching call key does NOT run the call live (the parked child's
|
|
21
|
+
* session is pinned under a pending checkpoint; a fresh spawn would duplicate it) — it drives the
|
|
22
|
+
* parked child's resume when the caller hands it a decision for the token, else re-parks. Deliberately
|
|
23
|
+
* a DIFFERENT key from `result`: a journal reader from before this entry kind existed finds no
|
|
24
|
+
* `result.terminal` and refuses the whole resume (`WorkflowJournalIncompatibleError`) instead of
|
|
25
|
+
* admitting a paused cause it would re-run live — one-way, loud, like every journal schema step.
|
|
26
|
+
*/
|
|
27
|
+
parked: TaskResult & {
|
|
28
|
+
terminal: Extract<TaskResult["terminal"], {
|
|
29
|
+
kind: "paused";
|
|
30
|
+
}>;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
14
33
|
/** REF-D5: the resume-claim key triple — ONE shape for the interface pair and every implementation
|
|
15
34
|
* (it was re-inlined seven times; a key-field rename must red every leg at once). */
|
|
16
35
|
export interface ResumeClaimArgs {
|
|
@@ -65,7 +65,7 @@ export class InMemoryWorkflowJournalStore {
|
|
|
65
65
|
return [...rec.byOrdinal.entries()].sort((a, b) => a[0] - b[0]).map(([, e]) => snapshot(e));
|
|
66
66
|
}
|
|
67
67
|
async append(runId, scope, entry) {
|
|
68
|
-
if (oversizeJournalResult(JSON.stringify(entry.result)))
|
|
68
|
+
if (oversizeJournalResult(JSON.stringify(entry.parked !== undefined ? entry.parked : entry.result)))
|
|
69
69
|
return;
|
|
70
70
|
let rec = this.runs.get(runId);
|
|
71
71
|
if (!rec) {
|
|
@@ -79,6 +79,8 @@ export declare function openSpoolPair(base: string): {
|
|
|
79
79
|
};
|
|
80
80
|
export declare class NodeExecutionEnv implements ExecutionEnv, BackgroundShellCapability, SchedulerCapability {
|
|
81
81
|
cwd: string;
|
|
82
|
+
/** #644 — this adapter runs on the engine host, so the host's home IS the environment's (`ExecutionEnv.homeDir`). */
|
|
83
|
+
readonly homeDir: string;
|
|
82
84
|
private shellPath?;
|
|
83
85
|
private shellEnv?;
|
|
84
86
|
private inheritEnv;
|