@sema-agent/core 7.11.2 → 7.13.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 +86 -13
- package/dist/core/ask-origin.d.ts +55 -0
- package/dist/core/ask-origin.js +21 -0
- package/dist/core/auto-mode-arming.d.ts +10 -14
- package/dist/core/auto-mode-arming.js +3 -9
- package/dist/core/auto-mode-defaults.d.ts +0 -2
- package/dist/core/auto-mode-defaults.js +0 -1
- package/dist/core/auto-mode-rebuild.d.ts +6 -13
- package/dist/core/auto-mode-rebuild.js +0 -2
- package/dist/core/auto-mode.d.ts +30 -89
- package/dist/core/auto-mode.js +12 -59
- package/dist/core/checkpoint-store.d.ts +1 -3
- package/dist/core/engine-notice.d.ts +18 -0
- package/dist/core/gate-fold.js +1 -9
- package/dist/core/gate-lanes.d.ts +0 -40
- package/dist/core/gate-lanes.js +18 -26
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +2 -0
- package/dist/core/hooks.d.ts +7 -1
- package/dist/core/hooks.js +1 -1
- package/dist/core/runner/assemble-result.d.ts +45 -1
- package/dist/core/runner/assemble-result.js +4 -1
- package/dist/core/runner/compaction-seams.d.ts +42 -0
- package/dist/core/runner/compaction-seams.js +80 -0
- package/dist/core/runner/contracts.d.ts +85 -15
- package/dist/core/runner/denial-limit-arms.d.ts +10 -13
- package/dist/core/runner/denial-limit-arms.js +12 -9
- package/dist/core/runner/gate-exit.js +9 -1
- package/dist/core/runner/permission-rule-lanes.d.ts +2 -1
- package/dist/core/runner/permission-rule-lanes.js +2 -1
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +1 -1
- package/dist/core/runner/prepare-caps-and-workflow.js +0 -5
- package/dist/core/runner/prepare-policy-chain.js +3 -2
- package/dist/core/runner/prepare-protocol-tools.js +5 -0
- package/dist/core/runner/prepare-safety-scan.js +11 -16
- package/dist/core/runner/prepare-suspend-saga.d.ts +0 -2
- package/dist/core/runner/prepare-suspend-saga.js +2 -10
- package/dist/core/runner/prepare-task.d.ts +9 -1
- package/dist/core/runner/prepare-task.js +7 -2
- package/dist/core/runner/prepare-wiring-manifest.d.ts +1 -1
- package/dist/core/runner/prepare-wiring-manifest.js +1 -8
- package/dist/core/runner/resume-claim.d.ts +2 -2
- package/dist/core/runner/resume-preflight.d.ts +2 -2
- package/dist/core/runner/run-attachment-seats.d.ts +6 -4
- package/dist/core/runner/run-attachment-seats.js +2 -2
- package/dist/core/runner/run-git-lane.d.ts +1 -1
- package/dist/core/runner/run-harness-handlers.js +2 -0
- package/dist/core/runner/run-identity-wiring.d.ts +21 -33
- package/dist/core/runner/run-identity-wiring.js +6 -3
- package/dist/core/runner/run-leg.d.ts +108 -0
- package/dist/core/runner/run-leg.js +713 -0
- package/dist/core/runner/run-notification-lane.d.ts +56 -0
- package/dist/core/runner/run-notification-lane.js +128 -0
- package/dist/core/runner/run-reasoning-seat.d.ts +5 -5
- package/dist/core/runner/run-reasoning-seat.js +7 -7
- package/dist/core/runner/run-recovery-lanes.d.ts +4 -17
- package/dist/core/runner/run-recovery-lanes.js +5 -4
- package/dist/core/runner/run-settle-and-teardown.d.ts +113 -0
- package/dist/core/runner/run-settle-and-teardown.js +432 -0
- package/dist/core/runner/run-stop-and-final-verify.d.ts +2 -2
- package/dist/core/runner/run-terminal-adoption.d.ts +99 -0
- package/dist/core/runner/run-terminal-adoption.js +200 -0
- package/dist/core/runner/run-turn-boundary.js +5 -4
- package/dist/core/runner/runtask.d.ts +10 -79
- package/dist/core/runner/runtask.js +83 -1557
- package/dist/core/runner/stream-lifecycle-verbs.js +10 -1
- package/dist/core/runner-deps.d.ts +4 -14
- package/dist/core/store-contracts/workflow-journal-store-contract.d.ts +7 -0
- package/dist/core/store-contracts/workflow-journal-store-contract.js +85 -0
- package/dist/core/task-stream.d.ts +10 -1
- package/dist/core/tool-face.d.ts +8 -0
- package/dist/core/tool-face.js +1 -0
- package/dist/core/tool-policy.d.ts +42 -96
- package/dist/core/tool-policy.js +1 -11
- package/dist/core/tool-registry.d.ts +11 -3
- package/dist/core/tool-registry.js +7 -1
- package/dist/core/tool-roster.d.ts +26 -0
- package/dist/core/tool-roster.js +38 -7
- package/dist/core/trace.d.ts +6 -7
- package/dist/core/wiring-manifest.d.ts +5 -22
- package/dist/core/wiring-manifest.js +3 -11
- package/dist/core/workflow-journal-store.d.ts +35 -4
- package/dist/core/workflow-journal-store.js +19 -2
- package/dist/index.d.ts +6 -5
- package/dist/index.js +6 -5
- package/dist/orchestration/workflow.js +2 -0
- package/dist/server/http.js +2 -1
- package/dist/stores/file/workflow-journal-store.d.ts +7 -10
- package/dist/stores/file/workflow-journal-store.js +2 -4
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +21 -11
package/dist/core/auto-mode.d.ts
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import type { ToolCallRequest } from "./tool-policy.js";
|
|
2
2
|
/**
|
|
3
|
-
* WHY an `unavailable` verdict could not run — the closed set behind the verdict arm, the
|
|
4
|
-
* `classifierUnavailable` fact (the
|
|
5
|
-
*
|
|
3
|
+
* WHY an `unavailable` verdict could not run — the closed set behind the verdict arm, the deny's
|
|
4
|
+
* `classifierUnavailable` fact (the deny observer's word and the model-facing sentence's parenthetical) and
|
|
5
|
+
* the `auto_mode.classified` trace frame's `cause`, so the three spell one word. `error` = the model leg
|
|
6
6
|
* threw or rejected (a route failure at classify time reads here too — the derived-route pre-flight fell
|
|
7
|
-
* back BEFORE any decide, so there is no separate word for it); `timeout` = the round-trip cap
|
|
8
|
-
* `breaker_open` = the session latch already tripped and the round was short-circuited.
|
|
7
|
+
* back BEFORE any decide, so there is no separate word for it); `timeout` = the round-trip cap.
|
|
9
8
|
*/
|
|
10
|
-
export declare const AUTO_MODE_UNAVAILABLE_CAUSES: readonly ["error", "timeout"
|
|
9
|
+
export declare const AUTO_MODE_UNAVAILABLE_CAUSES: readonly ["error", "timeout"];
|
|
11
10
|
export type AutoModeUnavailableCause = (typeof AUTO_MODE_UNAVAILABLE_CAUSES)[number];
|
|
12
11
|
/** Whether a value is a member of the unavailable-cause set — the screen a row reader applies to a stored word. */
|
|
13
12
|
export declare function isAutoModeUnavailableCause(v: unknown): v is AutoModeUnavailableCause;
|
|
@@ -21,26 +20,30 @@ export type AutoModeVerdict = {
|
|
|
21
20
|
category: string;
|
|
22
21
|
reason: string;
|
|
23
22
|
}
|
|
24
|
-
/** The classifier could not run: model
|
|
23
|
+
/** The classifier could not run: the model leg threw / rejected, or ran past the cap. NOT a decision — the
|
|
24
|
+
* gate denies and says so ({@link classifierUnavailableDenyMessage}). */
|
|
25
25
|
| {
|
|
26
26
|
kind: "unavailable";
|
|
27
27
|
cause: AutoModeUnavailableCause;
|
|
28
28
|
}
|
|
29
|
-
/** The model responded but not in the `<block>…` contract shape.
|
|
29
|
+
/** The model responded but not in the `<block>…` contract shape. Handled as a BLOCK with the CC
|
|
30
|
+
* parse-failure sentence ({@link CLASSIFIER_PARSE_FAILURE_DENY_MESSAGE}), counted by the denial limit. */
|
|
30
31
|
| {
|
|
31
32
|
kind: "parse_error";
|
|
32
33
|
raw: string;
|
|
33
34
|
};
|
|
34
35
|
/**
|
|
35
|
-
* The
|
|
36
|
-
* the
|
|
37
|
-
*
|
|
38
|
-
*
|
|
36
|
+
* The model-facing deny text for an `unavailable` verdict — CC 2.1.250 `x1t`, with the parenthetical `EIt`
|
|
37
|
+
* spells for the causes this engine distinguishes (a wall-clock / connection timeout reads " (timed out)";
|
|
38
|
+
* an error of unknown kind reads nothing — CC spells HTTP statuses it has, and this decider does not). The
|
|
39
|
+
* subject is the classifier itself rather than a model id: the decider does not know which model answered,
|
|
40
|
+
* and the sentence must not name one it cannot vouch for. One writer for every deny site (the gate's own
|
|
41
|
+
* station and the inherited-lane arms).
|
|
39
42
|
*/
|
|
40
|
-
export declare
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
export declare
|
|
43
|
+
export declare function classifierUnavailableDenyMessage(toolName: string, cause: AutoModeUnavailableCause): string;
|
|
44
|
+
/** The model-facing deny text for a `parse_error` verdict — CC 2.1.250 `Ure` over the R3t stem: the classifier
|
|
45
|
+
* answered, but not with a verdict, and auto mode blocks what it could not evaluate. */
|
|
46
|
+
export declare const CLASSIFIER_PARSE_FAILURE_DENY_MESSAGE = "Auto mode could not evaluate this action and is blocking it for safety \u2014 the classifier's reply carried no verdict.";
|
|
44
47
|
/**
|
|
45
48
|
* Parse the classifier's output contract — the CC 2.1.250 verdict reader (`wCe`, its thinking strip
|
|
46
49
|
* `xCe`, its reason reader `yCe`) ported as is:
|
|
@@ -65,11 +68,12 @@ export declare function isAutoModeBreakerCause(v: unknown): v is AutoModeBreaker
|
|
|
65
68
|
* stripped text, the tag spelled as the prompt spells it, trimmed) are carried when present and `""`
|
|
66
69
|
* when absent; the consumers render their
|
|
67
70
|
* own fallback line from the tool name in that case. The block intent is unambiguous either way, and
|
|
68
|
-
* downgrading a stated block to `parse_error` (
|
|
69
|
-
* would
|
|
71
|
+
* downgrading a stated block to `parse_error` (a block with the generic parse-failure sentence instead
|
|
72
|
+
* of the classifier's own reason) would lose the classifier's explicit verdict on the exact calls it flagged.
|
|
70
73
|
*
|
|
71
|
-
* `parse_error` is fail-closed: the
|
|
72
|
-
*
|
|
74
|
+
* `parse_error` is fail-closed: the gate handles it as a BLOCK (CC 2.1.250's own outcome for a reply with no
|
|
75
|
+
* verdict) that the denial limit counts like any other — never a silent allow, and never a question handed
|
|
76
|
+
* to a person on the classifier's behalf.
|
|
73
77
|
*/
|
|
74
78
|
export declare function parseAutoModeResponse(text: string): AutoModeVerdict;
|
|
75
79
|
/** What the decider hands the deployment's `classify` hook (the assembled prompt is the hook's job
|
|
@@ -88,21 +92,11 @@ export interface AutoModeDeciderOptions {
|
|
|
88
92
|
/** Hard cap on one classification round-trip. Default 15_000 ms (sema 裁量 — CC's constant is not
|
|
89
93
|
* established; a permission gate must not stall the whole run on a slow classifier). */
|
|
90
94
|
timeoutMs?: number;
|
|
91
|
-
/** Consecutive-failure threshold that opens the session breaker (default 3 "连续 N 失败").
|
|
92
|
-
* Failures = unavailable(error|timeout) + parse_error. A successful round (allow/block) resets it. */
|
|
93
|
-
failureThreshold?: number;
|
|
94
|
-
/** Fired ONCE when the breaker opens ("本 session 退回非 auto + 一次性告警"). `lastCause` is the failure that tripped it
|
|
95
|
-
* ({@link AutoModeBreakerCause}). */
|
|
96
|
-
onBreakerOpen?: (info: {
|
|
97
|
-
consecutiveFailures: number;
|
|
98
|
-
lastCause: AutoModeBreakerCause;
|
|
99
|
-
}) => void;
|
|
100
95
|
/**
|
|
101
96
|
* Fired once per `decide` call with the verdict the gate is about to act on and the wall time it
|
|
102
|
-
* waited (`ms`, integer ≥ 0; a timed-out round reads the deadline
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* need not implement it. Like the breaker alarm, a throwing hook never breaks the gate.
|
|
97
|
+
* waited (`ms`, integer ≥ 0; a timed-out round reads the deadline). `cause` is present iff the verdict
|
|
98
|
+
* is `unavailable`. The engine's own wiring turns this into the `auto_mode.classified` trace frame; a
|
|
99
|
+
* hand-built decider need not implement it. A throwing hook never breaks the gate.
|
|
106
100
|
*/
|
|
107
101
|
onClassified?: (info: AutoModeClassified) => void;
|
|
108
102
|
}
|
|
@@ -120,66 +114,13 @@ export interface AutoModeClassified {
|
|
|
120
114
|
export interface AutoModeDecider {
|
|
121
115
|
/** Never rejects. Any internal failure surfaces as `unavailable`/`parse_error` (fail-closed). */
|
|
122
116
|
decide(input: AutoModeClassifyInput, signal?: AbortSignal): Promise<AutoModeVerdict>;
|
|
123
|
-
/** True once the session breaker has opened (it never half-opens: is a SESSION fallback
|
|
124
|
-
* to non-auto, not a retry window — a flapping classifier must not oscillate the permission mode). */
|
|
125
|
-
breakerOpen(): boolean;
|
|
126
|
-
/**
|
|
127
|
-
* The CURRENT consecutive-failure streak — the part of the breaker's state that `breakerOpen()` alone
|
|
128
|
-
* cannot express. A latch that has not tripped yet still carries how much budget is left before it
|
|
129
|
-
* does, and #503 needs exactly that: an arming recorded for cross-process rebuild starts a FRESH
|
|
130
|
-
* decider, so recording one from a decider that is already 2 failures into a threshold of 3 would hand
|
|
131
|
-
* the redemption more tolerance for a failing classifier than the ancestor had left — the redeemed leg
|
|
132
|
-
* stays auto-classified through failures that would have dropped the ancestor back to non-auto.
|
|
133
|
-
* {@link import("./tool-policy.js").constraintChainEntryOfLayer} therefore records an arming only at a
|
|
134
|
-
* ZERO streak, and reads a decider that does not implement this member as unknown (⇒ records nothing).
|
|
135
|
-
*
|
|
136
|
-
* OPTIONAL so that hand-built deciders keep type-checking; the engine's own
|
|
137
|
-
* {@link createAutoModeDecider} always implements it. A deployment that hand-rolls a decider and wants
|
|
138
|
-
* the persisted-arming path must implement it too — the conservative reading of absence is deliberate.
|
|
139
|
-
*/
|
|
140
|
-
consecutiveFailures?(): number;
|
|
141
117
|
}
|
|
142
118
|
/**
|
|
143
|
-
*
|
|
144
|
-
*
|
|
119
|
+
* The per-run decider: timeout + fail-closed error mapping, and nothing carried between rounds. One instance
|
|
120
|
+
* per run (it binds the run's model leg and assembled prompt); a round that failed says nothing about the
|
|
121
|
+
* next one, which asks the model again.
|
|
145
122
|
*/
|
|
146
123
|
export declare function createAutoModeDecider(opts: AutoModeDeciderOptions): AutoModeDecider;
|
|
147
|
-
/**
|
|
148
|
-
* ONE breaker trip as the session-level read face records it (`WiringManifest.autoMode.breaker`): when the
|
|
149
|
-
* latch opened, what tripped it, how many consecutive failures it took, and which run's decider it was.
|
|
150
|
-
* A decider is minted per run (its latch is a RUN fact), so a trip names one leg; the ledger below carries
|
|
151
|
-
* the most recent one forward per session, which is how the session's NEXT leg can say "auto mode fell
|
|
152
|
-
* back to asking on this session, at T, because the classifier timed out three times" — the sentence a
|
|
153
|
-
* shell's doctor line and a capabilities read face owe a person who wonders why auto mode is asking.
|
|
154
|
-
*/
|
|
155
|
-
export interface AutoModeBreakerTrip {
|
|
156
|
-
/** Epoch ms at which the latch opened. */
|
|
157
|
-
readonly openedAtMs: number;
|
|
158
|
-
/** The failure that tripped it — the streak's last failure ({@link AutoModeBreakerCause}). */
|
|
159
|
-
readonly lastCause: AutoModeBreakerCause;
|
|
160
|
-
/** The consecutive-failure count at the trip (the threshold, or more under concurrent rounds). */
|
|
161
|
-
readonly failures: number;
|
|
162
|
-
/** The run whose decider tripped. */
|
|
163
|
-
readonly runId: string;
|
|
164
|
-
}
|
|
165
|
-
/**
|
|
166
|
-
* The Runner-lived, per-session record of the most recent breaker trip — the ONE writer is the engine's
|
|
167
|
-
* own `onBreakerOpen` wrap at the arming site (the deployment's alarm hook, when wired, is called after
|
|
168
|
-
* the record lands), the ONE reader the wiring-manifest phase of a later leg. Never persisted (a trip is
|
|
169
|
-
* process-local, like the decider it describes); bounded FIFO so a long-lived Runner cannot grow it
|
|
170
|
-
* without limit — an evicted session simply reads as "no trip recorded", the same as a session that
|
|
171
|
-
* never tripped. Threaded through the trusted `RunInternals` channel exactly as the per-session read-file
|
|
172
|
-
* seats are (the Runner sets it on every prepare it drives; a standalone prepare has none).
|
|
173
|
-
*/
|
|
174
|
-
export declare class AutoModeBreakerLedger {
|
|
175
|
-
private readonly cap;
|
|
176
|
-
private readonly trips;
|
|
177
|
-
constructor(cap?: number);
|
|
178
|
-
/** Record THIS session's most recent trip (replacing an earlier one). */
|
|
179
|
-
record(sessionId: string, trip: AutoModeBreakerTrip): void;
|
|
180
|
-
/** The most recent trip recorded for this session, or undefined (never tripped, or evicted). */
|
|
181
|
-
lastTrip(sessionId: string): AutoModeBreakerTrip | undefined;
|
|
182
|
-
}
|
|
183
124
|
/** The deployment's bounds for the denial limit (`RunnerDeps.autoMode.denialLimit`). Every member
|
|
184
125
|
* optional; an omitted member takes its CC default. A present member with a bad value is REFUSED
|
|
185
126
|
* loudly at construction (never clamped, never read as the default). */
|
package/dist/core/auto-mode.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export const AUTO_MODE_UNAVAILABLE_CAUSES = ["error", "timeout"
|
|
1
|
+
import { AUTO_MODE_DEFAULT_TIMEOUT_MS, AUTO_MODE_DENIAL_AUTO_DENY_DEFAULT_MS, AUTO_MODE_DENIAL_LIMIT_DEFAULTS } from "./auto-mode-defaults.js";
|
|
2
|
+
export const AUTO_MODE_UNAVAILABLE_CAUSES = ["error", "timeout"];
|
|
3
3
|
const AUTO_MODE_UNAVAILABLE_CAUSE_SET = new Set(AUTO_MODE_UNAVAILABLE_CAUSES);
|
|
4
4
|
export function isAutoModeUnavailableCause(v) {
|
|
5
5
|
return AUTO_MODE_UNAVAILABLE_CAUSE_SET.has(v);
|
|
6
6
|
}
|
|
7
|
-
export
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
export function classifierUnavailableDenyMessage(toolName, cause) {
|
|
8
|
+
const parenthetical = cause === "timeout" ? " (timed out)" : "";
|
|
9
|
+
return (`The auto-mode classifier is temporarily unavailable${parenthetical}, so auto mode cannot determine the safety of ${toolName} right now. ` +
|
|
10
|
+
"Wait a moment and then try this action again. " +
|
|
11
|
+
"If it keeps failing, continue with other tasks that don't require this action and come back to it later. " +
|
|
12
|
+
"Note: reading files, searching code, and other read-only operations do not require the classifier and can still be used.");
|
|
11
13
|
}
|
|
14
|
+
export const CLASSIFIER_PARSE_FAILURE_DENY_MESSAGE = "Auto mode could not evaluate this action and is blocking it for safety — the classifier's reply carried no verdict.";
|
|
12
15
|
export function parseAutoModeResponse(text) {
|
|
13
16
|
const rawAnswers = new Set([...text.matchAll(/<block>(yes|no)\b/gi)].map((m) => m[1].toLowerCase()));
|
|
14
17
|
if (rawAnswers.size > 1)
|
|
@@ -28,20 +31,6 @@ export function parseAutoModeResponse(text) {
|
|
|
28
31
|
}
|
|
29
32
|
export function createAutoModeDecider(opts) {
|
|
30
33
|
const timeoutMs = opts.timeoutMs ?? AUTO_MODE_DEFAULT_TIMEOUT_MS;
|
|
31
|
-
const threshold = Math.max(1, Math.floor(opts.failureThreshold ?? AUTO_MODE_DEFAULT_FAILURE_THRESHOLD));
|
|
32
|
-
let consecutiveFailures = 0;
|
|
33
|
-
let open = false;
|
|
34
|
-
const recordFailure = (cause) => {
|
|
35
|
-
consecutiveFailures++;
|
|
36
|
-
if (!open && consecutiveFailures >= threshold) {
|
|
37
|
-
open = true;
|
|
38
|
-
try {
|
|
39
|
-
opts.onBreakerOpen?.({ consecutiveFailures, lastCause: cause });
|
|
40
|
-
}
|
|
41
|
-
catch {
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
34
|
const decide = async (input, signal) => {
|
|
46
35
|
const startedAt = performance.now();
|
|
47
36
|
const verdict = await decideUnreported(input, signal);
|
|
@@ -59,14 +48,8 @@ export function createAutoModeDecider(opts) {
|
|
|
59
48
|
}
|
|
60
49
|
return verdict;
|
|
61
50
|
};
|
|
62
|
-
return {
|
|
63
|
-
breakerOpen: () => open,
|
|
64
|
-
consecutiveFailures: () => consecutiveFailures,
|
|
65
|
-
decide,
|
|
66
|
-
};
|
|
51
|
+
return { decide };
|
|
67
52
|
async function decideUnreported(input, signal) {
|
|
68
|
-
if (open)
|
|
69
|
-
return { kind: "unavailable", cause: "breaker_open" };
|
|
70
53
|
let timer;
|
|
71
54
|
const inner = new AbortController();
|
|
72
55
|
const onOuterAbort = () => inner.abort();
|
|
@@ -98,21 +81,12 @@ export function createAutoModeDecider(opts) {
|
|
|
98
81
|
}
|
|
99
82
|
});
|
|
100
83
|
});
|
|
101
|
-
|
|
102
|
-
return { kind: "unavailable", cause: "breaker_open" };
|
|
103
|
-
const verdict = parseAutoModeResponse(raw);
|
|
104
|
-
if (verdict.kind === "parse_error")
|
|
105
|
-
recordFailure("parse_error");
|
|
106
|
-
else
|
|
107
|
-
consecutiveFailures = 0;
|
|
108
|
-
return verdict;
|
|
84
|
+
return parseAutoModeResponse(raw);
|
|
109
85
|
}
|
|
110
86
|
catch (e) {
|
|
111
87
|
if (signal?.aborted)
|
|
112
88
|
return { kind: "unavailable", cause: "error" };
|
|
113
|
-
|
|
114
|
-
recordFailure(timedOut ? "timeout" : "error");
|
|
115
|
-
return { kind: "unavailable", cause: timedOut ? "timeout" : "error" };
|
|
89
|
+
return { kind: "unavailable", cause: e instanceof AutoModeTimeout ? "timeout" : "error" };
|
|
116
90
|
}
|
|
117
91
|
finally {
|
|
118
92
|
if (timer !== undefined)
|
|
@@ -127,27 +101,6 @@ class AutoModeTimeout extends Error {
|
|
|
127
101
|
super("auto-mode classify timeout");
|
|
128
102
|
}
|
|
129
103
|
}
|
|
130
|
-
export class AutoModeBreakerLedger {
|
|
131
|
-
cap;
|
|
132
|
-
trips = new Map();
|
|
133
|
-
constructor(cap = 1024) {
|
|
134
|
-
this.cap = cap;
|
|
135
|
-
}
|
|
136
|
-
record(sessionId, trip) {
|
|
137
|
-
if (this.trips.has(sessionId))
|
|
138
|
-
this.trips.delete(sessionId);
|
|
139
|
-
this.trips.set(sessionId, trip);
|
|
140
|
-
while (this.trips.size > this.cap) {
|
|
141
|
-
const oldest = this.trips.keys().next().value;
|
|
142
|
-
if (oldest === undefined)
|
|
143
|
-
break;
|
|
144
|
-
this.trips.delete(oldest);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
lastTrip(sessionId) {
|
|
148
|
-
return this.trips.get(sessionId);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
104
|
export function readDenialLimitFallback(v) {
|
|
152
105
|
if (typeof v !== "object" || v === null)
|
|
153
106
|
return undefined;
|
|
@@ -1123,9 +1123,7 @@ export interface CheckpointState {
|
|
|
1123
1123
|
* suspend leg did. It is a MEMORY of intent, never an authorization: the resuming deployment's
|
|
1124
1124
|
* face (`RunnerDeps.autoMode`) and the resuming principal's deny bit (`RuntimeCaps.autoMode`)
|
|
1125
1125
|
* are judged afresh on every leg — a bit on the row cannot arm where the redeeming deployment
|
|
1126
|
-
* would not.
|
|
1127
|
-
* is untripped and untouched (a session that fell back to non-auto hands nothing forward); a leg
|
|
1128
|
-
* never armed here carries the memory as is. Absent on older checkpoints and on non-auto tasks
|
|
1126
|
+
* would not. A leg never armed here carries the memory as is. Absent on older checkpoints and on non-auto tasks
|
|
1129
1127
|
* (byte-identical to the pre-bit row); an older worker that ignores it resumes un-armed, the
|
|
1130
1128
|
* narrower direction. */
|
|
1131
1129
|
autoModeRequested?: true;
|
|
@@ -235,6 +235,24 @@ export interface EngineNotice {
|
|
|
235
235
|
* (whoever answered the card is the one entitled to hear the answer ran nothing); a host may
|
|
236
236
|
* forward it on its own wire.
|
|
237
237
|
*
|
|
238
|
+
* - `"task.interrupt_unconsumed"` — a HARD stop ({@link TaskStream.interrupt}, the task's `signal`,
|
|
239
|
+
* the walltime or the turns limit) landed AFTER the run's last turn had already given its answer
|
|
240
|
+
* (`stopReason:"stop"`, not partial, and no steer / follow-up of the person's still pending — a
|
|
241
|
+
* queued continuation makes that "stop" work in progress, and a stop that cuts it stays the
|
|
242
|
+
* ordinary abort with its undrained-input account) and before the loop's end — what it aborted was the run's
|
|
243
|
+
* tail (a final-boundary compaction is mooted, a stop hook's wait is cut), never its work. The
|
|
244
|
+
* answer stands: the run ends `completed` with it as `result`, no `[Request interrupted by user]`
|
|
245
|
+
* marker is written and no `limits.*` terminal is minted for the axis that fired; this notice is
|
|
246
|
+
* the stop's only trace. Minted once per such run, after the terminal arbitration (so it never
|
|
247
|
+
* claims a completed ending the terminal did not give — a budget ceiling crossed by the last
|
|
248
|
+
* turn's own spend still fails the run and mints nothing here); `detail: { sessionId, runId,
|
|
249
|
+
* taskId?, origin }` with `origin` ∈ `"interrupt"` (the verb) | `"walltime"` | `"turns"` |
|
|
250
|
+
* `"signal"` (every other run-level abort: the task's own `signal`, a parent's cancel, a
|
|
251
|
+
* `destroy()`, a refused preempt — the leg names the three it can tell apart and files the rest
|
|
252
|
+
* here). Audience `"user"`. A stop that arrives
|
|
253
|
+
* AFTER the loop's end is refused typed (`steering.not_running`, every verb alike) and mints
|
|
254
|
+
* nothing: the refusal itself is the loud answer.
|
|
255
|
+
*
|
|
238
256
|
* - `"checkpoint.execution_outcome_unrecorded"` — the resumed leg's pending-call resolver disposed
|
|
239
257
|
* the decided action (executed it, delivered the refusal, or vetoed the person's yes) and filed
|
|
240
258
|
* the frame's record through `CheckpointStore.recordExecutionOutcome`, and the store answered
|
package/dist/core/gate-fold.js
CHANGED
|
@@ -128,15 +128,7 @@ export async function runGateFold(pass) {
|
|
|
128
128
|
pass.decision = { ...pass.decision, probeMandated: true };
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
|
-
|
|
132
|
-
try {
|
|
133
|
-
return d.breakerOpen();
|
|
134
|
-
}
|
|
135
|
-
catch {
|
|
136
|
-
return false;
|
|
137
|
-
}
|
|
138
|
-
};
|
|
139
|
-
if (input.peerMessage === true && pass.decision.action === "allow" && input.autoMode !== undefined && !breakerKnownOpen(input.autoMode.decider)) {
|
|
131
|
+
if (input.peerMessage === true && pass.decision.action === "allow" && input.autoMode !== undefined) {
|
|
140
132
|
pass.decision = {
|
|
141
133
|
action: "ask",
|
|
142
134
|
message: `tool "${toolName}" sends a message to another agent — routed for classifier review in auto mode`,
|
|
@@ -48,46 +48,6 @@ export declare function normalizePersistedRuleHit(hit: PersistedRuleAnswer): {
|
|
|
48
48
|
reason?: string;
|
|
49
49
|
coverage?: readonly import("./permission-rule-model.js").SegmentCoverage[];
|
|
50
50
|
};
|
|
51
|
-
/**
|
|
52
|
-
* The mandate provenance of one call, judged from the SAME mark inputs the gate is driven with —
|
|
53
|
-
* the single source for "could a persisted allow rule clear this ask?". Allow rules silence the
|
|
54
|
-
* classifier's questions, never a mandated one, and this predicate is the mandated-family half of
|
|
55
|
-
* that boundary (the real-approval/governance half rides the decision's own `requiresRealApproval`
|
|
56
|
-
* bit, which the org layer stamps):
|
|
57
|
-
* · `probeMandated` — this CALL's own reversibility probe declared its demotion STRUCTURAL
|
|
58
|
-
* (`"probe_mandate"`), judged FIRST because it is the only per-call member here: the three below
|
|
59
|
-
* are properties of the TOOL and are true of every call on the seat, so a mandate that is true of
|
|
60
|
-
* this one call must not be shadowed by the tier that happens to carry it (#502: the built-in
|
|
61
|
-
* shell probe raises it for a listed reader naming a path outside the session's roots — a boundary
|
|
62
|
-
* the deployment declared, which is exactly what the classify tier alone cannot say);
|
|
63
|
-
* · `egress` — the tool's own external-write mark, judged next: it is the tool's declaration even
|
|
64
|
-
* when the coarse doctrine also installed a shell tier on the same seat;
|
|
65
|
-
* · `shellGated` + tier `"always"` — the operator's per-call confirmation doctrine
|
|
66
|
-
* (`"operator_always"`); the classify doctrine installs `"maybe"`, and THOSE asks stay the rule
|
|
67
|
-
* lane's home turf (`undefined`) — that is the don't-ask-again main case, and the per-call member
|
|
68
|
-
* above is deliberately the ONLY thing that carves a mandate out of it;
|
|
69
|
-
* · a tool's OWN `"always"`/`"maybe"` irreversibility tier without the doctrine (`"tool_marks"`).
|
|
70
|
-
*
|
|
71
|
-
* Two consumers, one derivation: the gate's silencing arm (a matching rule is disclosed as shadowed
|
|
72
|
-
* instead of clearing the ask) and the runner's suggestion factory (a mandated ask offers no
|
|
73
|
-
* "stop asking me this" option — a rule minted from it would never clear it). A drift between the
|
|
74
|
-
* two would let a card offer a rule the lane then refuses to honor. The per-call member reaches both
|
|
75
|
-
* the same way every other per-call fact does: the gate stamps it on the surviving ask, the
|
|
76
|
-
* synchronous sites read it off the decision they spread, and the park leg threads its own parameter.
|
|
77
|
-
*/
|
|
78
|
-
export declare function persistedRuleMandateOf(marks: {
|
|
79
|
-
egress?: boolean;
|
|
80
|
-
shellGated?: boolean;
|
|
81
|
-
irreversibility?: "never" | "maybe" | "always";
|
|
82
|
-
/** #502: the surviving ask's engine-stamped `probeMandated` — see
|
|
83
|
-
* {@link import("./types.js").ReversibilityVerdict.mandated}. */
|
|
84
|
-
probeMandated?: boolean;
|
|
85
|
-
/** The pass's own record that the probe was consulted and did not answer (`GatePass.probeUnanswered`):
|
|
86
|
-
* the boundary is unknown, which no standing yes may stand in for. The runner's suggestion factory does
|
|
87
|
-
* not see the pass and never passes it — a card may offer a rule the lane then discloses as shadowed
|
|
88
|
-
* (loud, never a silent override), the same drift window the `matchedAskRule` conjunct already has. */
|
|
89
|
-
probeUnanswered?: boolean;
|
|
90
|
-
}): "operator_always" | "tool_marks" | "probe_mandate" | "probe_unanswered" | undefined;
|
|
91
51
|
/**
|
|
92
52
|
* Run the lanes over the pass: the org layer over the fold's verdict, then — for a surviving ask — the rewrite
|
|
93
53
|
* merge, the rule lane, the stamp, the classifier, the sandbox leg, the parks, the content lane, the resolution
|
package/dist/core/gate-lanes.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { decisionText, describeThrown } from "./tool-policy.js";
|
|
2
|
-
import { askOriginOf, classifierMayAnswer } from "./ask-origin.js";
|
|
3
|
-
import { denialLimitFallbackMessage, unarmedWindow } from "./auto-mode.js";
|
|
2
|
+
import { askOriginOf, classifierMayAnswer, persistedRuleMandateOf, probeBoundaryMandate } from "./ask-origin.js";
|
|
3
|
+
import { CLASSIFIER_PARSE_FAILURE_DENY_MESSAGE, classifierUnavailableDenyMessage, denialLimitFallbackMessage, unarmedWindow } from "./auto-mode.js";
|
|
4
4
|
import { inlineUntrusted } from "./untrusted-text.js";
|
|
5
5
|
import { isRuleBehavior } from "./permission-rule-model.js";
|
|
6
6
|
import { ASK_USER_QUESTION_TOOL_NAME } from "./ask-question.js";
|
|
@@ -144,21 +144,6 @@ export function normalizePersistedRuleHit(hit) {
|
|
|
144
144
|
return {};
|
|
145
145
|
return { hit: { behavior, rules: members.map((m) => ({ rule: m.rule, ...copiedUsableDots(m.dots) })) } };
|
|
146
146
|
}
|
|
147
|
-
export function persistedRuleMandateOf(marks) {
|
|
148
|
-
return marks.probeMandated === true
|
|
149
|
-
? "probe_mandate"
|
|
150
|
-
: marks.probeUnanswered === true
|
|
151
|
-
? "probe_unanswered"
|
|
152
|
-
: marks.egress === true
|
|
153
|
-
? "tool_marks"
|
|
154
|
-
: marks.shellGated === true
|
|
155
|
-
? marks.irreversibility === "always"
|
|
156
|
-
? "operator_always"
|
|
157
|
-
: undefined
|
|
158
|
-
: marks.irreversibility === "always" || marks.irreversibility === "maybe"
|
|
159
|
-
? "tool_marks"
|
|
160
|
-
: undefined;
|
|
161
|
-
}
|
|
162
147
|
function parkWith(suspendAsk, parkArgs, carry) {
|
|
163
148
|
return suspendAsk(...parkArgs, carry);
|
|
164
149
|
}
|
|
@@ -375,7 +360,7 @@ export async function runGateLanes(pass) {
|
|
|
375
360
|
}
|
|
376
361
|
if (pass.decision.action === "ask")
|
|
377
362
|
ledger.supersede();
|
|
378
|
-
if (input.autoMode && pass.decision.action === "ask" && pass.decision.origin !== undefined && classifierMayAnswer(pass.decision.origin)) {
|
|
363
|
+
if (input.autoMode && pass.decision.action === "ask" && pass.decision.origin !== undefined && classifierMayAnswer(pass.decision.origin) && !probeBoundaryMandate(persistedRuleMandate)) {
|
|
379
364
|
const verdict = await input.autoMode.decider
|
|
380
365
|
.decide({ req: pass.req, askMessage: decisionText(pass.decision) }, input.abortSignal)
|
|
381
366
|
.catch(() => ({ kind: "unavailable", cause: "error" }));
|
|
@@ -388,9 +373,17 @@ export async function runGateLanes(pass) {
|
|
|
388
373
|
...(pass.policyRewrite !== undefined ? { updatedInput: pass.policyRewrite } : {}),
|
|
389
374
|
};
|
|
390
375
|
}
|
|
391
|
-
else if (verdict.kind === "
|
|
392
|
-
|
|
393
|
-
|
|
376
|
+
else if (verdict.kind === "unavailable") {
|
|
377
|
+
pass.decision = {
|
|
378
|
+
action: "deny",
|
|
379
|
+
message: classifierUnavailableDenyMessage(toolName, verdict.cause),
|
|
380
|
+
decisionReason: "classifier",
|
|
381
|
+
classifierUnavailable: { cause: verdict.cause },
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
const reason = verdict.kind === "block" ? (verdict.reason ? inlineUntrusted(verdict.reason) : "") : CLASSIFIER_PARSE_FAILURE_DENY_MESSAGE;
|
|
386
|
+
const category = verdict.kind === "block" && verdict.category ? inlineUntrusted(verdict.category) : "";
|
|
394
387
|
const tracked = input.autoMode.denialTracking?.recordBlock();
|
|
395
388
|
if (tracked?.limitReached === true) {
|
|
396
389
|
const fallbackAsk = {
|
|
@@ -405,15 +398,13 @@ export async function runGateLanes(pass) {
|
|
|
405
398
|
else {
|
|
406
399
|
pass.decision = {
|
|
407
400
|
action: "deny",
|
|
408
|
-
message: `auto-mode classifier blocked this call${reason ? `: ${reason}` : category ? `: [${category}]` : ""}
|
|
401
|
+
message: verdict.kind === "block" ? `auto-mode classifier blocked this call${reason ? `: ${reason}` : category ? `: [${category}]` : ""}` : reason,
|
|
409
402
|
decisionReason: "classifier",
|
|
410
403
|
};
|
|
411
|
-
pass.deniedBy = "classifier";
|
|
412
404
|
}
|
|
413
405
|
}
|
|
414
|
-
|
|
415
|
-
pass.
|
|
416
|
-
}
|
|
406
|
+
if (pass.decision.action === "deny")
|
|
407
|
+
pass.deniedBy = "classifier";
|
|
417
408
|
}
|
|
418
409
|
if (input.sandboxAdmission !== undefined &&
|
|
419
410
|
pass.decision.action === "ask" &&
|
|
@@ -425,6 +416,7 @@ export async function runGateLanes(pass) {
|
|
|
425
416
|
pass.hookAsk === undefined &&
|
|
426
417
|
input.egress !== true &&
|
|
427
418
|
input.peerMessage !== true &&
|
|
419
|
+
!probeBoundaryMandate(persistedRuleMandate) &&
|
|
428
420
|
input.sandboxAdmission.boundaryCapable !== true) {
|
|
429
421
|
const recorded = input.sandboxAdmission.askClassesOf(toolCallId);
|
|
430
422
|
const admissible = recorded !== undefined && recorded.length > 0 && recorded.every((r) => r.cls === "sandbox_local");
|
|
@@ -104,7 +104,7 @@ export type NoticeAudience = "user" | "operator";
|
|
|
104
104
|
* src/ for notice mint shapes and names any code that is minted but unregistered, or registered but
|
|
105
105
|
* no longer minted.
|
|
106
106
|
*/
|
|
107
|
-
export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_swapped", "config.read_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.tool_card_undeclared", "config.tool_face_undeclared", "config.tool_face_invalid", "config.durable_gate_unavailable", "config.peer_admission_out_of_range", "config.peer_lane_unmounted", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "checkpoint.execution_outcome_unrecorded", "delegation.transcript_integrity", "delegation.ask_unresolvable", "mcp.injection_dropped", "mcp.revocation_probe_failed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "route.fallback_to_primary", "route.base_url_changed_key_unchanged", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "tool_result.offload_put_failed"];
|
|
107
|
+
export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_swapped", "config.read_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.tool_card_undeclared", "config.tool_face_undeclared", "config.tool_face_invalid", "config.durable_gate_unavailable", "config.peer_admission_out_of_range", "config.peer_lane_unmounted", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "checkpoint.execution_outcome_unrecorded", "delegation.transcript_integrity", "delegation.ask_unresolvable", "mcp.injection_dropped", "mcp.revocation_probe_failed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "route.fallback_to_primary", "route.base_url_changed_key_unchanged", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "task.interrupt_unconsumed", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "tool_result.offload_put_failed"];
|
|
108
108
|
/** A code this engine mints (see {@link ENGINE_NOTICE_CODES}). NOT the type of
|
|
109
109
|
* `EngineNotice.code`, which stays `string` — a host forwarding its own notices through the same
|
|
110
110
|
* sink is a supported shape, and narrowing that field would break it. */
|
|
@@ -141,6 +141,7 @@ export const ENGINE_NOTICE_CODES = [
|
|
|
141
141
|
"task.turn_interrupted",
|
|
142
142
|
"task.halt_unconsumed",
|
|
143
143
|
"task.late_approval",
|
|
144
|
+
"task.interrupt_unconsumed",
|
|
144
145
|
"memory.capture_opted_out",
|
|
145
146
|
"memory.capture_optout_unpersisted",
|
|
146
147
|
"tool_result.offload_put_failed",
|
|
@@ -159,6 +160,7 @@ const NOTICE_AUDIENCE_TABLE = {
|
|
|
159
160
|
"steering.parked_input_blocked": "user",
|
|
160
161
|
"task.halt_unconsumed": "user",
|
|
161
162
|
"task.late_approval": "user",
|
|
163
|
+
"task.interrupt_unconsumed": "user",
|
|
162
164
|
"config.durable_gate_unavailable": "user",
|
|
163
165
|
"memory.capture_opted_out": "user",
|
|
164
166
|
"memory.capture_optout_unpersisted": "user",
|
package/dist/core/hooks.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { ActorAssertion, DocumentContent, ImageContent, TextContent } from
|
|
|
2
2
|
import type { ExecutionEnv, FileError, Result, SessionTreeEntry } from "../internal/harness-types.js";
|
|
3
3
|
import type { DecisionReason, PermissionResult, ResolvedAsk, ToolCallFace, ToolCallRequest, ToolPolicy } from "./tool-policy.js";
|
|
4
4
|
import type { GateOutcome } from "./gate-outcome.js";
|
|
5
|
-
export { normalizeOrgGateVerdict, normalizePersistedRuleHit
|
|
5
|
+
export { normalizeOrgGateVerdict, normalizePersistedRuleHit } from "./gate-lanes.js";
|
|
6
6
|
export { cloneObserverInput } from "./runner/gate-exit.js";
|
|
7
7
|
import { type AskClass } from "./ask-class.js";
|
|
8
8
|
import { type AutoModeDenialTracker } from "./auto-mode.js";
|
|
@@ -411,6 +411,12 @@ export interface PermissionDeniedPayload {
|
|
|
411
411
|
* settled none.
|
|
412
412
|
*/
|
|
413
413
|
gate: GateOutcome;
|
|
414
|
+
/** #661 (additive): present ⇔ this deny is the auto-mode classifier's UNAVAILABILITY (`deniedBy: "classifier"`
|
|
415
|
+
* with the verdict's cause word) — the structured half of the deny text's "the classifier is temporarily
|
|
416
|
+
* unavailable" sentence. A classifier BLOCK or parse-failure deny carries no member here. */
|
|
417
|
+
classifierUnavailable?: {
|
|
418
|
+
readonly cause: import("./auto-mode.js").AutoModeUnavailableCause;
|
|
419
|
+
};
|
|
414
420
|
/** {@link HookSeatSignal} — this invocation's own abort signal. On an OBSERVATION seat the deny has
|
|
415
421
|
* already happened and nothing this callback does can change it, so the signal says exactly one
|
|
416
422
|
* thing: stop reading, nobody is waiting for your answer any more. */
|
package/dist/core/hooks.js
CHANGED
|
@@ -2,7 +2,7 @@ import { decisionText, refuseOutOfContractDecision } from "./tool-policy.js";
|
|
|
2
2
|
import { createSettlementLedger, engineFailClosedExit, hookDenyExit, traceHookCrash } from "./runner/gate-exit.js";
|
|
3
3
|
import { runGateFold } from "./gate-fold.js";
|
|
4
4
|
import { runGateLanes } from "./gate-lanes.js";
|
|
5
|
-
export { normalizeOrgGateVerdict, normalizePersistedRuleHit
|
|
5
|
+
export { normalizeOrgGateVerdict, normalizePersistedRuleHit } from "./gate-lanes.js";
|
|
6
6
|
export { cloneObserverInput } from "./runner/gate-exit.js";
|
|
7
7
|
import { brandPolicyAskClass } from "./ask-class.js";
|
|
8
8
|
import { isAskOrigin } from "./ask-origin.js";
|
|
@@ -178,6 +178,10 @@ export interface ResultFlags {
|
|
|
178
178
|
* abort (no synthetic stopReason:"aborted" assistant is appended), `final` is the last REAL assistant
|
|
179
179
|
* message — this flag is what keeps an interrupted run from being mistaken for a completed one. */
|
|
180
180
|
abortedLive?: boolean;
|
|
181
|
+
/** The leg's ONE evaluation of {@link answerSettled} (a clean `"stop"` final with no queued continuation
|
|
182
|
+
* lost): a live abort that found this true landed on the run's tail, not on work — slot 9 stands down
|
|
183
|
+
* and the run takes the completed floor. Absent/false = the abort's ordinary reading. */
|
|
184
|
+
answerSettled?: boolean;
|
|
181
185
|
budgetHit?: "exceeded" | "precall";
|
|
182
186
|
/** design/164 — which budget ceiling the hit belongs to. Set together with {@link budgetHit} at every
|
|
183
187
|
* site that records one; it picks the terminal code (`limits.max_tokens_exceeded` vs
|
|
@@ -225,6 +229,43 @@ export declare function errorCodeOf(err: unknown): string | undefined;
|
|
|
225
229
|
* in-presence rule the usage-window wait hint states for itself), and it is not part of the cause's
|
|
226
230
|
* public projection.
|
|
227
231
|
*/
|
|
232
|
+
/**
|
|
233
|
+
* Has the run's ANSWER settled — is `final` the model's own clean end of its work? A run whose last turn
|
|
234
|
+
* ended `stopReason:"stop"` has said everything it was going to say; whatever ran after that turn (the
|
|
235
|
+
* final boundary's compaction, a stop hook, the teardown) is the run's tail, not its work. A hard stop
|
|
236
|
+
* that lands in that tail — `TaskStream.interrupt()`, the task's `signal`, the walltime or turns limit —
|
|
237
|
+
* cuts nothing the person or the caller wanted, so the run is NOT cancellable there: it ends `completed`
|
|
238
|
+
* with the answer as `result` (slot 9 yields to the completed floor), no interruption marker is written
|
|
239
|
+
* for it, and the end-of-task hook seats keep their full deadline. One predicate, read by every site that
|
|
240
|
+
* used to decide the question for itself; there is no second spelling.
|
|
241
|
+
*
|
|
242
|
+
* What does NOT count as settled — each is "the model was still working" and stays cancellable:
|
|
243
|
+
* - `stopReason:"length"` — the output was truncated; the auto-continue chain owns what follows;
|
|
244
|
+
* - `stopReason:"toolUse"` — a batch was in flight (the orphan reconcile owns the abort);
|
|
245
|
+
* - `stopReason:"aborted"` / `"error"` — the turn itself was cut or failed;
|
|
246
|
+
* - a `partialFinalized` `"stop"` — a mid-stream loss promoted to a final; the loop's bounded
|
|
247
|
+
* continue nudge owns it, so the stop the caller sees is a salvage, not the model's own end;
|
|
248
|
+
* - no `final` at all;
|
|
249
|
+
* - `continuationCut` — the run was cut with a continuation still owed: a steer / follow-up the person
|
|
250
|
+
* had already queued was still pending (queued, or drained at the final boundary and never served —
|
|
251
|
+
* the harness's agent_end account, which also mints `task.user_steer_undrained` /
|
|
252
|
+
* `task.user_followup_undrained`), or the loop itself had decided to run another turn (a drained
|
|
253
|
+
* frame, a truncated-output / thinking-only / degenerate-output nudge) and the abort caught it before
|
|
254
|
+
* the dispatch (`Prepared.workCutRef.continuation`, from the loop's own trace steps) — in both, a
|
|
255
|
+
* stop landing between the decision and the next stream returns the PREVIOUS clean answer as
|
|
256
|
+
* `final` (the loop exits at its pre-dispatch guard), so `final` alone cannot tell, and a
|
|
257
|
+
* thinking-only "stop" the loop was about to nudge into a visible answer is an empty result, not
|
|
258
|
+
* an answer; or the abort was already in force when the last turn ENDED
|
|
259
|
+
* (`Prepared.workCutRef.abortedBeforeTurnEnd`) — a brain that ignored its signal and delivered a
|
|
260
|
+
* full "stop" through the abort still answered a run somebody had stopped mid-work.
|
|
261
|
+
* Three axes, one law: the `abortedLive` flag alone decides that a stop landed; this decides whether it
|
|
262
|
+
* landed on work or on the tail. Evaluated ONCE per leg (the terminal-adoption lane, where both facts
|
|
263
|
+
* are in hand) and handed on as a value — {@link ResultFlags.answerSettled} — so the arbitration, the
|
|
264
|
+
* marker gate, the hook-seat grace, the teardown attribution and the notice read the same verdict.
|
|
265
|
+
* (A limit that is NOT a stop — a budget ceiling crossed by the last turn's own spend — is arbitrated
|
|
266
|
+
* above slot 9 and is unaffected.)
|
|
267
|
+
*/
|
|
268
|
+
export declare function answerSettled(final: AssistantMessage | undefined, continuationCut?: boolean): boolean;
|
|
228
269
|
export declare function terminalCauseOf(final: AssistantMessage | undefined, stats: Pick<Stats, "humanReview">, flags: ResultFlags): {
|
|
229
270
|
terminal: TerminalCause;
|
|
230
271
|
apiFailure: TaskResult["apiFailure"];
|
|
@@ -252,7 +293,10 @@ export declare function terminalCauseOf(final: AssistantMessage | undefined, sta
|
|
|
252
293
|
* 9. `abortedLive` | stopReason aborted → `"limits.max_walltime_exceeded"` | `"limits.max_turns_exceeded"` | undefined
|
|
253
294
|
* (audit A-1: hoisted ABOVE no-final — the loop exits cleanly on abort, so `final` may be a
|
|
254
295
|
* normal message or absent; a plain user interrupt carries NO errorCode — switch on `status`),
|
|
255
|
-
* EXCEPT when the aborted `final` is the bare halt's OWN cut artifact (`haltOwnsAbortedFinal`)
|
|
296
|
+
* EXCEPT when the aborted `final` is the bare halt's OWN cut artifact (`haltOwnsAbortedFinal`),
|
|
297
|
+
* and EXCEPT when the answer had already settled (`answerSettled`: a clean `"stop"` final, not
|
|
298
|
+
* partial) — a stop landing on the run's tail cuts no work, so the run falls through to 12
|
|
299
|
+
* (`completed`, the answer as `result`, no `limits.*` code for the axis; three axes, one law)
|
|
256
300
|
* 9.7 `userHalted` + (no `final` | the halt's own aborted `final`) → status `"completed"` (#504: a
|
|
257
301
|
* bare user halt — the verb working, not a defect; below the aborts on purpose, a real
|
|
258
302
|
* abort/limit that also fired owns the terminal and the halt rides as the pass-through seat)
|