@sema-agent/core 7.18.0 → 7.20.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 +276 -2
- package/dist/agents/subagent.js +12 -20
- package/dist/agents/teacher.js +2 -5
- package/dist/agents/verify.js +21 -10
- package/dist/brain/anthropic.js +8 -3
- package/dist/brain/open-responses.js +12 -4
- package/dist/brain/openai.js +7 -2
- package/dist/brain/reasoning.d.ts +13 -13
- package/dist/brain/reasoning.js +2 -2
- package/dist/brain/stream-engine.d.ts +4 -0
- package/dist/brain/stream-engine.js +6 -1
- package/dist/brain/stream-shared.d.ts +3 -0
- package/dist/brain/stream-shared.js +1 -0
- package/dist/core/ask-origin.d.ts +16 -0
- package/dist/core/ask-origin.js +8 -0
- package/dist/core/auto-mode-arming.d.ts +6 -0
- package/dist/core/auto-mode-arming.js +5 -5
- package/dist/core/auto-mode-defaults.d.ts +43 -15
- package/dist/core/auto-mode-defaults.js +4 -1
- package/dist/core/auto-mode.d.ts +74 -26
- package/dist/core/auto-mode.js +80 -11
- package/dist/core/checkpoint-store.d.ts +7 -0
- package/dist/core/checkpoint-store.js +2 -2
- package/dist/core/child-clamp-seats.d.ts +79 -0
- package/dist/core/child-clamp-seats.js +29 -0
- package/dist/core/engine-notice.d.ts +89 -41
- package/dist/core/fs-write-gate-policy.d.ts +17 -9
- package/dist/core/fs-write-gate-policy.js +51 -4
- package/dist/core/gate-bounded-await.d.ts +38 -0
- package/dist/core/gate-bounded-await.js +53 -0
- package/dist/core/gate-fold.js +20 -48
- package/dist/core/gate-lanes.js +17 -9
- package/dist/core/gate-outcome.d.ts +34 -6
- package/dist/core/gate-outcome.js +7 -0
- package/dist/core/governance-codes.d.ts +16 -1
- package/dist/core/governance-codes.js +18 -0
- package/dist/core/hooks.d.ts +8 -8
- package/dist/core/memory-engine/engine.d.ts +14 -9
- package/dist/core/memory-engine/engine.js +21 -8
- package/dist/core/memory-engine/layout.d.ts +31 -0
- package/dist/core/memory-engine/layout.js +68 -3
- package/dist/core/memory-engine/types.d.ts +24 -0
- package/dist/core/memory.d.ts +19 -0
- package/dist/core/memory.js +12 -0
- package/dist/core/read-root-candidate.d.ts +12 -0
- package/dist/core/read-root-candidate.js +24 -0
- package/dist/core/remote-env.d.ts +32 -0
- package/dist/core/remote-env.js +4 -1
- package/dist/core/roles.d.ts +9 -1
- package/dist/core/roles.js +4 -0
- package/dist/core/runner/advertised-writable-dirs.d.ts +59 -1
- package/dist/core/runner/advertised-writable-dirs.js +51 -3
- package/dist/core/runner/announce-once-ledger.d.ts +15 -0
- package/dist/core/runner/announce-once-ledger.js +8 -0
- package/dist/core/runner/classifier-ladder.d.ts +154 -11
- package/dist/core/runner/classifier-ladder.js +170 -10
- package/dist/core/runner/contracts.d.ts +46 -1
- package/dist/core/runner/denial-limit-arms.d.ts +14 -1
- package/dist/core/runner/denial-limit-arms.js +7 -6
- package/dist/core/runner/env-capability-census.d.ts +30 -0
- package/dist/core/runner/env-capability-census.js +38 -0
- package/dist/core/runner/forward-sink.d.ts +32 -0
- package/dist/core/runner/forward-sink.js +41 -0
- package/dist/core/runner/gate-exit.d.ts +4 -0
- package/dist/core/runner/gate-exit.js +4 -1
- package/dist/core/runner/git-status-frame.d.ts +0 -9
- package/dist/core/runner/listing-fold.d.ts +57 -0
- package/dist/core/runner/listing-fold.js +75 -0
- package/dist/core/runner/prepare-ask-lane.js +1 -0
- package/dist/core/runner/prepare-caps-and-workflow.js +20 -31
- package/dist/core/runner/prepare-config-doors.d.ts +6 -0
- package/dist/core/runner/prepare-config-doors.js +8 -10
- package/dist/core/runner/prepare-execution-env.d.ts +1 -1
- package/dist/core/runner/prepare-execution-env.js +2 -0
- package/dist/core/runner/prepare-gate-stations.d.ts +8 -0
- package/dist/core/runner/prepare-gate-stations.js +6 -2
- package/dist/core/runner/prepare-hands-readface.d.ts +4 -0
- package/dist/core/runner/prepare-hands-readface.js +3 -5
- package/dist/core/runner/prepare-listings.d.ts +3 -0
- package/dist/core/runner/prepare-listings.js +4 -2
- package/dist/core/runner/prepare-memory.js +7 -1
- package/dist/core/runner/prepare-policy-chain.js +5 -5
- package/dist/core/runner/prepare-project-context.d.ts +5 -1
- package/dist/core/runner/prepare-project-context.js +3 -2
- package/dist/core/runner/prepare-prompt-inputs.d.ts +2 -2
- package/dist/core/runner/prepare-run-refs.d.ts +4 -2
- package/dist/core/runner/prepare-run-refs.js +5 -4
- package/dist/core/runner/prepare-task.js +8 -6
- package/dist/core/runner/resume-apply.js +3 -3
- package/dist/core/runner/run-clock-and-content.js +3 -7
- package/dist/core/runner/run-harness-handlers.js +10 -10
- package/dist/core/runner/run-leg.js +31 -16
- package/dist/core/runner/run-settle-and-teardown.js +2 -5
- package/dist/core/runner/run-turn-boundary.js +44 -25
- package/dist/core/runner/runtask.js +2 -1
- package/dist/core/runner/synthetic-tools.d.ts +41 -29
- package/dist/core/runner/synthetic-tools.js +25 -40
- package/dist/core/runner/task-root-canonical.d.ts +45 -0
- package/dist/core/runner/task-root-canonical.js +18 -0
- package/dist/core/runner/turn-attachments.d.ts +160 -53
- package/dist/core/runner/turn-attachments.js +153 -51
- package/dist/core/safe-notify.d.ts +5 -0
- package/dist/core/safe-notify.js +24 -2
- package/dist/core/sensitive-path-policy.d.ts +84 -17
- package/dist/core/sensitive-path-policy.js +37 -19
- package/dist/core/skill-listing-budget.d.ts +106 -0
- package/dist/core/skill-listing-budget.js +57 -0
- package/dist/core/skills-directory.js +17 -10
- package/dist/core/task-registry.js +2 -1
- package/dist/core/task-spec.d.ts +29 -2
- package/dist/core/tool-policy.d.ts +31 -5
- package/dist/core/tool-policy.js +45 -32
- package/dist/core/tool-spec.d.ts +37 -4
- package/dist/core/types.d.ts +1 -1
- package/dist/core/write-protect.d.ts +105 -20
- package/dist/core/write-protect.js +89 -32
- package/dist/index.d.ts +8 -7
- package/dist/index.js +6 -5
- package/dist/orchestration/workflow-primitives.js +10 -20
- package/dist/stores/file/memory-store.d.ts +5 -2
- package/dist/stores/file/memory-store.js +8 -4
- package/dist/tools/fs/bash-readonly-classifier.d.ts +40 -2
- package/dist/tools/fs/bash-readonly-classifier.js +376 -41
- package/dist/tools/fs/fs-bash.d.ts +1 -1
- package/dist/tools/fs/fs-bash.js +19 -28
- package/dist/tools/fs/fs-shared.d.ts +22 -0
- package/dist/tools/fs/fs-shared.js +30 -0
- package/dist/tools/fs/index.d.ts +1 -1
- package/dist/tools/fs/index.js +1 -1
- package/dist/tools/fs/read-deny.d.ts +4 -3
- package/dist/tools/fs/safety.d.ts +15 -0
- package/dist/tools/fs/safety.js +7 -0
- package/package.json +2 -1
- package/test/export-surface.snapshot.json +31 -5
- package/wire-changes/core@7.18.0.json +1273 -0
- package/wire-changes/core@7.19.0.json +457 -0
- package/wire-changes/core@7.20.0.json +568 -0
package/dist/core/memory.d.ts
CHANGED
|
@@ -275,6 +275,25 @@ export declare class MemoryGateError extends Error {
|
|
|
275
275
|
readonly code: string;
|
|
276
276
|
constructor(code: string, detail?: string);
|
|
277
277
|
}
|
|
278
|
+
/**
|
|
279
|
+
* The same SHAPE as {@link MemoryGateError} — a stable `code` a caller switches on — for the opposite
|
|
280
|
+
* PHASE of a memory write, and deliberately NOT that class: a gate error means the write was REFUSED
|
|
281
|
+
* and never reached the backend, so a host branching on it would take exactly the wrong action here.
|
|
282
|
+
*
|
|
283
|
+
* @contract memory.projection_failure_is_post_durable — a store throws this ONLY after the note's own
|
|
284
|
+
* event is durably appended and fsync'd, so `committed` is always true and the note survives a restart:
|
|
285
|
+
* a caller that retries the write on this error stores the note a second time. Only the derived
|
|
286
|
+
* projection is missing, and the next mutation of the same scope rewrites it whole.
|
|
287
|
+
*/
|
|
288
|
+
export declare class MemoryProjectionError extends Error {
|
|
289
|
+
/** Stable, switchable — the phase this failure belongs to. */
|
|
290
|
+
readonly code = "memory.projection_failed";
|
|
291
|
+
/** Always true: this error only exists AFTER the durable append was fsync'd. Never retry the write. */
|
|
292
|
+
readonly committed = true;
|
|
293
|
+
/** The projection file that could not be rewritten. */
|
|
294
|
+
readonly path: string;
|
|
295
|
+
constructor(path: string, cause: unknown);
|
|
296
|
+
}
|
|
278
297
|
/**
|
|
279
298
|
* Scan `text` for a high-confidence credential (CC-parity P0-2). Returns the matched pattern's stable
|
|
280
299
|
* `label` (NEVER the matched secret value — labels are safe to log/surface), or `null` when clean.
|
package/dist/core/memory.js
CHANGED
|
@@ -22,6 +22,18 @@ export class MemoryGateError extends Error {
|
|
|
22
22
|
this.code = code;
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
+
export class MemoryProjectionError extends Error {
|
|
26
|
+
code = "memory.projection_failed";
|
|
27
|
+
committed = true;
|
|
28
|
+
path;
|
|
29
|
+
constructor(path, cause) {
|
|
30
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
31
|
+
super(`memory.projection_failed: the note IS durably committed — only the derived projection ${path} could not be rewritten: ${detail}. ` +
|
|
32
|
+
`Do not retry the write (it would store the note twice); the projection is rebuilt by the next mutation of this scope.`, cause !== undefined ? { cause } : undefined);
|
|
33
|
+
this.name = "MemoryProjectionError";
|
|
34
|
+
this.path = path;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
25
37
|
function hasPromotableMarker(note) {
|
|
26
38
|
return note.outcomeStats !== undefined;
|
|
27
39
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** The display bound on a grant candidate's directory: generous next to a real `PATH_MAX`, tight enough
|
|
2
|
+
* that the seat cannot become an unbounded prose channel onto an approval card. */
|
|
3
|
+
export declare const READ_ROOT_CANDIDATE_DIR_MAX = 1024;
|
|
4
|
+
/**
|
|
5
|
+
* The validator for a probe's {@link import("./tool-spec.js").ReversibilityVerdict.readRootCandidate}: the
|
|
6
|
+
* errand `normalizeProbeCause` does for the cause, fail-closed on the disclosure alone — a malformed value
|
|
7
|
+
* yields `undefined`, the ask stands, the card carries no offer. It REFUSES where the cause NEUTRALIZES,
|
|
8
|
+
* per `shell.read_boundary.grant_candidate_is_the_grant`: the cause is prose, so folding a separator out of
|
|
9
|
+
* it keeps it true, while this string IS the grant, and a card showing one spelling while the deployment
|
|
10
|
+
* applies another is the failure the seat prevents.
|
|
11
|
+
*/
|
|
12
|
+
export declare function normalizeReadRootCandidate(value: unknown): import("./tool-spec.js").ReadRootGrantCandidate | undefined;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { isPlainRecord, safeDataValue } from "./checkpoint-store.js";
|
|
2
|
+
import { hasWin32AliasedComponent, isAbsolutePathForm, normalizeAbsPathLexically, pathFamilyOf, win32NamespaceScreen } from "../tools/fs/safety.js";
|
|
3
|
+
export const READ_ROOT_CANDIDATE_DIR_MAX = 1024;
|
|
4
|
+
export function normalizeReadRootCandidate(value) {
|
|
5
|
+
if (!isPlainRecord(value))
|
|
6
|
+
return undefined;
|
|
7
|
+
if (safeDataValue(value, "clearsThisAsk") !== true)
|
|
8
|
+
return undefined;
|
|
9
|
+
const dir = safeDataValue(value, "dir");
|
|
10
|
+
if (typeof dir !== "string" || dir.length === 0 || dir.length > READ_ROOT_CANDIDATE_DIR_MAX)
|
|
11
|
+
return undefined;
|
|
12
|
+
if (/[\u0000-\u001f\u007f\u2028\u2029]/.test(dir))
|
|
13
|
+
return undefined;
|
|
14
|
+
if (!isAbsolutePathForm(dir))
|
|
15
|
+
return undefined;
|
|
16
|
+
const folded = normalizeAbsPathLexically(dir);
|
|
17
|
+
if (folded !== dir || normalizeAbsPathLexically(`${dir}/..`) === dir)
|
|
18
|
+
return undefined;
|
|
19
|
+
if (hasWin32AliasedComponent(dir))
|
|
20
|
+
return undefined;
|
|
21
|
+
if (!win32NamespaceScreen(dir, pathFamilyOf({ root: dir })).ok)
|
|
22
|
+
return undefined;
|
|
23
|
+
return { dir, clearsThisAsk: true };
|
|
24
|
+
}
|
|
@@ -316,6 +316,16 @@ export interface RemoteExecutionEnv extends ExecutionEnv {
|
|
|
316
316
|
* without durable suspend instead (the human gate refuses it), or extend capabilities with an
|
|
317
317
|
* explicit workspace-durability flag before building one. */
|
|
318
318
|
suspendable: boolean;
|
|
319
|
+
/** True iff this adapter's {@link FileSystem.canonicalPath} is the AUTHORITY on where a path lands —
|
|
320
|
+
* it answers for the very filesystem the commands run on, and a failure to answer is a failure of
|
|
321
|
+
* the resolver rather than a namespace it cannot see into. Optional, and absent is not a
|
|
322
|
+
* declaration: an adapter that says nothing keeps the reading every adapter has had, in which a
|
|
323
|
+
* chain nothing could be established about leaves the lexical verdict standing. Declaring it says
|
|
324
|
+
* the opposite — that an unanswerable chain is a question, not an absence — and the read
|
|
325
|
+
* boundary's execute-time recheck then refuses rather than passing the written spelling through.
|
|
326
|
+
* Read STRICTLY through {@link canonicalPathIsAuthoritative}, like every other capability whose
|
|
327
|
+
* `true` changes what a containment leg does; a wrong-SHAPE value reads absent and is announced. */
|
|
328
|
+
canonicalPathAuthoritative?: boolean;
|
|
319
329
|
};
|
|
320
330
|
/** Identity of the connected workspace (design/48 §5 gap 1). Synchronous: it is data the env already holds. */
|
|
321
331
|
workspaceHandle(): WorkspaceHandle;
|
|
@@ -507,6 +517,9 @@ export declare function missingRestoreSurface(env: ExecutionEnv): readonly ("res
|
|
|
507
517
|
*
|
|
508
518
|
* A composite type guard (structural AND capability) so a `suspendVM` call site narrowed by THIS predicate
|
|
509
519
|
* gets the {@link RemoteExecutionEnv} type without a cast — `true` always implies the structural check too.
|
|
520
|
+
* The capability is read STRICTLY, like the containment ones: a non-boolean reads as NOT suspendable and
|
|
521
|
+
* the census announces it. This took anything truthy, so `suspendable: "false"` was snapshotted — the one
|
|
522
|
+
* key of the three whose malformed reading fell on the unsafe side.
|
|
510
523
|
*
|
|
511
524
|
* It also requires the RESTORE surface ({@link missingRestoreSurface}). A suspend is only durable
|
|
512
525
|
* if something can undo it; an adapter that offers `suspendVM` but no `resumeVM` used to pass this guard,
|
|
@@ -549,3 +562,22 @@ export declare function isIsolated(env: ExecutionEnv): boolean;
|
|
|
549
562
|
* answers exactly, and reading the shape as containment made that deployment's boundary lexical-only.
|
|
550
563
|
*/
|
|
551
564
|
export declare function boundaryIsTheSandbox(env: ExecutionEnv): boolean;
|
|
565
|
+
/**
|
|
566
|
+
* Does this env DECLARE that its `canonicalPath` is the authority on where a path lands?
|
|
567
|
+
*
|
|
568
|
+
* The question the execute-time read boundary asks about its own resolver's SILENCE. Resolving a
|
|
569
|
+
* spelling walks it from the deepest answerable ancestor; when no ancestor answers at all, two
|
|
570
|
+
* different worlds produce the same `undefined`: a resolver that is authoritative and is FAILING (an
|
|
571
|
+
* RPC into the machine the command will run on, a transport that dropped), and an env that simply does
|
|
572
|
+
* not answer path questions (an in-memory double, an adapter with no resolution surface). The first is
|
|
573
|
+
* a question and the second is an absence, and reading both as the absence is what leaves a containment
|
|
574
|
+
* leg silently open on an RPC-backed lane.
|
|
575
|
+
*
|
|
576
|
+
* So the adapter says which it is, and says it the way {@link isIsolated} is said — strictly, because a
|
|
577
|
+
* `true` here makes a containment leg REFUSE where it used to pass. ABSENT is the reading every adapter
|
|
578
|
+
* has had, so no existing env moves; a PRESENT non-boolean reads absent HERE too but is announced at the
|
|
579
|
+
* env's admission point, once per key — this reader does not validate. A non-remote env keeps the same
|
|
580
|
+
* reading, which is why the local lane is unchanged: its resolver answers `/`, so the arm this gates
|
|
581
|
+
* has no case to fire on.
|
|
582
|
+
*/
|
|
583
|
+
export declare function canonicalPathIsAuthoritative(env: ExecutionEnv): boolean;
|
package/dist/core/remote-env.js
CHANGED
|
@@ -37,7 +37,7 @@ export function missingRestoreSurface(env) {
|
|
|
37
37
|
return missing;
|
|
38
38
|
}
|
|
39
39
|
export function isSuspendable(env) {
|
|
40
|
-
return isRemoteExecutionEnv(env) && env.capabilities.suspendable && missingRestoreSurface(env).length === 0;
|
|
40
|
+
return isRemoteExecutionEnv(env) && env.capabilities.suspendable === true && missingRestoreSurface(env).length === 0;
|
|
41
41
|
}
|
|
42
42
|
export function isIsolated(env) {
|
|
43
43
|
return isRemoteExecutionEnv(env) ? env.capabilities.isolation === true : false;
|
|
@@ -45,3 +45,6 @@ export function isIsolated(env) {
|
|
|
45
45
|
export function boundaryIsTheSandbox(env) {
|
|
46
46
|
return isIsolated(env);
|
|
47
47
|
}
|
|
48
|
+
export function canonicalPathIsAuthoritative(env) {
|
|
49
|
+
return isRemoteExecutionEnv(env) ? env.capabilities.canonicalPathAuthoritative === true : false;
|
|
50
|
+
}
|
package/dist/core/roles.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ThinkingLevel } from "../internal/harness-types.js";
|
|
2
2
|
import type { Model } from "../internal/llm.js";
|
|
3
|
-
import type { ModelRef, ModelRole, ModelRoles } from "./types.js";
|
|
3
|
+
import type { ModelRef, ModelRole, ModelRoles, RoleSpec } from "./types.js";
|
|
4
4
|
export type { ModelRole, RoleSpec, ModelRoles } from "./types.js";
|
|
5
5
|
/** Resolve a string ModelRef against the catalog (a Model object passes through). */
|
|
6
6
|
export declare function resolveModel(ref: ModelRef, models?: Record<string, Model>): Model;
|
|
@@ -87,6 +87,14 @@ export declare function parseModelMention(text: string, allowedNames: Iterable<s
|
|
|
87
87
|
* (即使 catalog 恰有裸模型名 `pro`/`flash` 也不入档位路由——INERT 靠配置成立,不靠键名运气)。
|
|
88
88
|
*/
|
|
89
89
|
export declare const ROLE_TIER_DEFAULTS: Readonly<Partial<Record<ModelRole, string>>>;
|
|
90
|
+
/**
|
|
91
|
+
* Whether a RoleSpec NAMES a model — a string ref, a Model object, or a config object carrying `model` or
|
|
92
|
+
* `select` — as opposed to a preset-only object (`{ thinking }`, `{ systemPrompt }`) that shapes the role's
|
|
93
|
+
* request but leaves its model to the defaults. The classifier seat's own default (the tier below the run's
|
|
94
|
+
* main model, `classifier-ladder.ts`) applies exactly when this is false: a `{select}` keeps its selection, a
|
|
95
|
+
* preset-only role does not disable the relative pick. One reader, the same parts {@link specParts} reads.
|
|
96
|
+
*/
|
|
97
|
+
export declare function roleSpecNamesModel(spec: RoleSpec | undefined): boolean;
|
|
90
98
|
export interface ResolvedRole {
|
|
91
99
|
model: Model;
|
|
92
100
|
/** The effective thinking level: explicit `spec.thinking` wins, else the role's default. */
|
package/dist/core/roles.js
CHANGED
|
@@ -130,6 +130,10 @@ function specParts(s) {
|
|
|
130
130
|
const cfg = s;
|
|
131
131
|
return { model: cfg.model, select: cfg.select, thinking: cfg.thinking, systemPrompt: cfg.systemPrompt };
|
|
132
132
|
}
|
|
133
|
+
export function roleSpecNamesModel(spec) {
|
|
134
|
+
const parts = specParts(spec);
|
|
135
|
+
return parts.model !== undefined || parts.select !== undefined;
|
|
136
|
+
}
|
|
133
137
|
function toConfig(s) {
|
|
134
138
|
if (s == null) {
|
|
135
139
|
return undefined;
|
|
@@ -20,6 +20,11 @@
|
|
|
20
20
|
* planted path is never written through); an advertiser whose directory is created elsewhere passes
|
|
21
21
|
* no marker and an absent directory narrows.
|
|
22
22
|
*
|
|
23
|
+
* A directory the fence already holds is never admitted as a HOLE in it: a candidate that resolves onto
|
|
24
|
+
* a root — or above one — would turn that whole root into the advertised, write-gate-exempt domain, so
|
|
25
|
+
* an ordinary source write inside the workspace would stop asking. One predicate
|
|
26
|
+
* ({@link swallowedFenceRoot}) states that, and the write gate's configured domains apply the same one.
|
|
27
|
+
*
|
|
23
28
|
* Reverse check (the seat's own invariant): nothing enters `roots` except through `admit`, and every
|
|
24
29
|
* caller of `admit` is an advertiser deciding its advertisement off the returned verdict — so no
|
|
25
30
|
* directory reaches the ROOTS FENCE through this seat without the prompt advertising it, and none
|
|
@@ -55,12 +60,65 @@ export interface AdvertisedWritableDir {
|
|
|
55
60
|
}
|
|
56
61
|
/** The skip announcement — the one loud outlet for a directory that was advertised but could not be admitted. */
|
|
57
62
|
export type AdvertisedWritableDirSkipped = (dir: AdvertisedWritableDir, reason: string) => void;
|
|
63
|
+
/**
|
|
64
|
+
* ONE rule for both halves of the auto-approved write surface: a directory offered as a WRITABLE DOMAIN
|
|
65
|
+
* (an advertised writable directory here; a configured `exemptDirs`/`acceptDirs` entry at the write
|
|
66
|
+
* gate) must not SWALLOW a root the fence already holds. Returns the first root the candidate equals or
|
|
67
|
+
* is an ancestor of — the one to name in the refusal — else `undefined`.
|
|
68
|
+
*
|
|
69
|
+
* Why a domain that contains a root is not a domain: the point of the domain is to exempt a small area
|
|
70
|
+
* the prompt (or the deployment) named. A candidate resolving onto the task root — a symlinked
|
|
71
|
+
* scratchpad or memory home is all it takes, and the resolution is what makes it invisible to the
|
|
72
|
+
* spelling — exempts the ENTIRE workspace instead, turning every ordinary source write from `ask` into
|
|
73
|
+
* `allow`. Refusing the admission is the fix; a special case inside the gate ("ignore this domain when
|
|
74
|
+
* it contains the root") would be the same rule written twice, in the place where it is easiest to
|
|
75
|
+
* forget. Both arguments must already be CANONICAL — this compares spellings, it does not resolve them.
|
|
76
|
+
*/
|
|
77
|
+
export declare function swallowedFenceRoot(domainCanonical: string, fenceRootsCanonical: readonly string[]): string | undefined;
|
|
58
78
|
export declare class AdvertisedWritableDirs {
|
|
59
79
|
private readonly env;
|
|
60
80
|
private readonly onSkipped;
|
|
81
|
+
/** The extra fence roots the SPEC declares (`additionalDirectories` / `additionalReadDirectories`), as
|
|
82
|
+
* spelled. Resolved by this seat rather than awaited from the hands phase — the memory advertiser is
|
|
83
|
+
* admitted BEFORE that phase, so a list that only grew there made the rule depend on phase order. An
|
|
84
|
+
* entry that does not resolve is not a fence root at all (the hands phase reads the SAME memoized
|
|
85
|
+
* answer and skips it too, announcing it there), so there is nothing here for it to guard. */
|
|
86
|
+
private readonly declaredRootSpellings;
|
|
61
87
|
private readonly admitted;
|
|
62
88
|
private readonly verdicts;
|
|
63
|
-
|
|
89
|
+
/** The CANONICAL roots the fence holds: the task root at construction, plus every root the spec
|
|
90
|
+
* DECLARES, resolved by this seat before it judges anything (see {@link resolveDeclaredRoots}) —
|
|
91
|
+
* so which advertiser runs first cannot decide what the rule sees. */
|
|
92
|
+
private readonly fenceRoots;
|
|
93
|
+
/** Latched after the declared spellings have been resolved once (the resolution is per seat, not per call). */
|
|
94
|
+
private declaredRootsResolved;
|
|
95
|
+
/** ONE answer per declared spelling, for this seat AND for the hands phase that folds the same entries
|
|
96
|
+
* into the allowlist ({@link canonicalizeDeclaredRoot}): two independent lookups of one path can
|
|
97
|
+
* DISAGREE, and a root outside this rule's list while inside the fence is exactly the gap. */
|
|
98
|
+
private readonly declaredRootAnswers;
|
|
99
|
+
constructor(env: ExecutionEnv, onSkipped: AdvertisedWritableDirSkipped,
|
|
100
|
+
/** The run's task root, CANONICAL (the driver's one resolution — see `task-root-canonical.ts`). */
|
|
101
|
+
taskRootCanonical: string,
|
|
102
|
+
/** The extra fence roots the SPEC declares (`additionalDirectories` / `additionalReadDirectories`), as
|
|
103
|
+
* spelled. Resolved by this seat rather than awaited from the hands phase — the memory advertiser is
|
|
104
|
+
* admitted BEFORE that phase, so a list that only grew there made the rule depend on phase order. An
|
|
105
|
+
* entry that does not resolve is not a fence root at all (the hands phase reads the SAME memoized
|
|
106
|
+
* answer and skips it too, announcing it there), so there is nothing here for it to guard. */
|
|
107
|
+
declaredRootSpellings?: readonly string[]);
|
|
108
|
+
/** Declare one more CANONICAL root the fence holds (an `additionalDirectories` /
|
|
109
|
+
* `additionalReadDirectories` entry, once the hands phase has resolved it). Declaring is
|
|
110
|
+
* idempotent and order-free; a root declared after an advertiser was judged does not re-open it. */
|
|
111
|
+
declareFenceRoot(canonical: string): void;
|
|
112
|
+
/**
|
|
113
|
+
* The canonical form of a DECLARED fence-root spelling — resolved at most once per spelling and
|
|
114
|
+
* served from the memo thereafter. The hands phase calls this instead of canonicalizing the same
|
|
115
|
+
* entries again, so the list this rule judges against and the allowlist the fence ends up holding
|
|
116
|
+
* are built from ONE answer per path: a lookup that fails for this seat fails for that phase too,
|
|
117
|
+
* and a root cannot be outside the rule's view while being inside the fence.
|
|
118
|
+
*/
|
|
119
|
+
canonicalizeDeclaredRoot(spelled: string): Promise<Awaited<ReturnType<ExecutionEnv["canonicalPath"]>>>;
|
|
120
|
+
/** Resolve the spec-declared root spellings into {@link fenceRoots}, exactly once per seat. */
|
|
121
|
+
private resolveDeclaredRoots;
|
|
64
122
|
/** The admitted roots, CANONICAL, in admission order, deduplicated. The hands phase folds this list into the
|
|
65
123
|
* write-capable containment allowlist before the toolkit compiles; it is not a live view after that. */
|
|
66
124
|
get roots(): readonly string[];
|
|
@@ -1,11 +1,52 @@
|
|
|
1
|
+
import { isWithin } from "./session-rule-policy.js";
|
|
2
|
+
export function swallowedFenceRoot(domainCanonical, fenceRootsCanonical) {
|
|
3
|
+
for (const root of fenceRootsCanonical) {
|
|
4
|
+
if (root && isWithin(domainCanonical, root))
|
|
5
|
+
return root;
|
|
6
|
+
}
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
1
9
|
export class AdvertisedWritableDirs {
|
|
2
10
|
env;
|
|
3
11
|
onSkipped;
|
|
12
|
+
declaredRootSpellings;
|
|
4
13
|
admitted = [];
|
|
5
14
|
verdicts = new Map();
|
|
6
|
-
|
|
15
|
+
fenceRoots;
|
|
16
|
+
declaredRootsResolved;
|
|
17
|
+
declaredRootAnswers = new Map();
|
|
18
|
+
constructor(env, onSkipped, taskRootCanonical, declaredRootSpellings = []) {
|
|
7
19
|
this.env = env;
|
|
8
20
|
this.onSkipped = onSkipped;
|
|
21
|
+
this.declaredRootSpellings = declaredRootSpellings;
|
|
22
|
+
this.fenceRoots = [taskRootCanonical];
|
|
23
|
+
}
|
|
24
|
+
declareFenceRoot(canonical) {
|
|
25
|
+
if (canonical && !this.fenceRoots.includes(canonical))
|
|
26
|
+
this.fenceRoots.push(canonical);
|
|
27
|
+
}
|
|
28
|
+
async canonicalizeDeclaredRoot(spelled) {
|
|
29
|
+
const memo = this.declaredRootAnswers.get(spelled);
|
|
30
|
+
if (memo !== undefined)
|
|
31
|
+
return memo;
|
|
32
|
+
const c = await this.env.canonicalPath(spelled);
|
|
33
|
+
const first = this.declaredRootAnswers.get(spelled);
|
|
34
|
+
if (first !== undefined)
|
|
35
|
+
return first;
|
|
36
|
+
this.declaredRootAnswers.set(spelled, c);
|
|
37
|
+
if (c.ok)
|
|
38
|
+
this.declareFenceRoot(c.value);
|
|
39
|
+
return c;
|
|
40
|
+
}
|
|
41
|
+
resolveDeclaredRoots() {
|
|
42
|
+
this.declaredRootsResolved ??= (async () => {
|
|
43
|
+
for (const spelled of this.declaredRootSpellings) {
|
|
44
|
+
if (!spelled || !spelled.trim())
|
|
45
|
+
continue;
|
|
46
|
+
await this.canonicalizeDeclaredRoot(spelled);
|
|
47
|
+
}
|
|
48
|
+
})();
|
|
49
|
+
return this.declaredRootsResolved;
|
|
9
50
|
}
|
|
10
51
|
get roots() {
|
|
11
52
|
return this.admitted;
|
|
@@ -41,8 +82,15 @@ export class AdvertisedWritableDirs {
|
|
|
41
82
|
this.onSkipped(dir, `the execution environment resolves it to ${c.value} while the advertiser's gates key on ${dir.expectCanonical} — a fence hole those gates cannot see is not opened`);
|
|
42
83
|
return false;
|
|
43
84
|
}
|
|
44
|
-
if (
|
|
45
|
-
|
|
85
|
+
if (this.admitted.includes(c.value))
|
|
86
|
+
return true;
|
|
87
|
+
await this.resolveDeclaredRoots();
|
|
88
|
+
const swallowed = swallowedFenceRoot(c.value, [...this.fenceRoots, ...this.admitted]);
|
|
89
|
+
if (swallowed !== undefined) {
|
|
90
|
+
this.onSkipped(dir, `it resolves to ${c.value}, which covers ${swallowed === this.fenceRoots[0] ? "task root" : "root"} ${swallowed} — a directory that holds a root is not a hole in the fence but the whole of it, and admitting it would auto-approve every write under that root`);
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
this.admitted.push(c.value);
|
|
46
94
|
return true;
|
|
47
95
|
}
|
|
48
96
|
}
|
|
@@ -27,6 +27,21 @@ export interface AnnounceOnceSession {
|
|
|
27
27
|
getAnnouncedListing(): Promise<AnnouncedListingSnapshot | undefined>;
|
|
28
28
|
appendAnnouncedListing(snapshot: AnnouncedListingSnapshotInput): Promise<string>;
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Flush the PARKED announced-listing snapshot write (`FrameReceiptLedger.listingSnapshotOwed`, set by a
|
|
32
|
+
* frame receipt's commit inside the `message_end` walk) at a serialization point — the boundary's end,
|
|
33
|
+
* the prompt's settle. Best-effort: a lost append is a duplicate announcement next run. Never while
|
|
34
|
+
* prepare's branch read failed: a whole-snapshot write would replace the branch's record with this
|
|
35
|
+
* leg's partial view (the flag is cleared either way — nothing is owed on such a leg).
|
|
36
|
+
*/
|
|
37
|
+
export declare function flushOwedAnnouncedSnapshot(prepared: {
|
|
38
|
+
frameReceipts: {
|
|
39
|
+
listingSnapshotOwed: boolean;
|
|
40
|
+
};
|
|
41
|
+
announcedSnapshotRecovered: boolean;
|
|
42
|
+
announcedListingsRef: AnnouncedListingSnapshotInput;
|
|
43
|
+
session: Pick<AnnounceOnceSession, "appendAnnouncedListing">;
|
|
44
|
+
}): Promise<void>;
|
|
30
45
|
/** The run-local ledger — see the module header for the two commit disciplines it serves. */
|
|
31
46
|
export interface AnnounceOnceLedger {
|
|
32
47
|
/** The nearest announced snapshot on the branch as read at prepare (undefined = pre-entry
|
|
@@ -29,6 +29,14 @@ export function withAnnounceDigests(existing, add) {
|
|
|
29
29
|
function sameFailure(a, b) {
|
|
30
30
|
return a.name === b.name && a.error === b.error;
|
|
31
31
|
}
|
|
32
|
+
export async function flushOwedAnnouncedSnapshot(prepared) {
|
|
33
|
+
if (!prepared.frameReceipts.listingSnapshotOwed)
|
|
34
|
+
return;
|
|
35
|
+
prepared.frameReceipts.listingSnapshotOwed = false;
|
|
36
|
+
if (!prepared.announcedSnapshotRecovered)
|
|
37
|
+
return;
|
|
38
|
+
await prepared.session.appendAnnouncedListing(prepared.announcedListingsRef).catch(() => undefined);
|
|
39
|
+
}
|
|
32
40
|
export async function loadAnnounceOnceLedger(session, checkpointMirror, onError) {
|
|
33
41
|
let recovered = true;
|
|
34
42
|
const entry = await session.getAnnouncedListing().catch(() => {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The auto-mode classifier's CANDIDATE LADDER, as one module: which models a classification may be asked on
|
|
3
|
-
* its per-rung reasoning seat
|
|
2
|
+
* The auto-mode classifier's CANDIDATE LADDER, as one module: which models a classification may be asked on
|
|
3
|
+
* (and which model stands in when no `classifier` role names one), its per-rung reasoning seat and the
|
|
4
|
+
* per-attempt user-message tail, how a rung's reply is read for a re-ask or a failure, and the once-per-run
|
|
5
|
+
* disclosures its first move and its broken contract mint.
|
|
4
6
|
*
|
|
5
7
|
* Beside `derived-route-fallback.ts` and in the same family — both are seats the caps phase's classifier
|
|
6
8
|
* arming reaches DOWN for, and both answer "this leg's model did not work out" one layer apart. Which is why
|
|
@@ -30,7 +32,7 @@
|
|
|
30
32
|
* `modelRequested` reading above; the unification (the pre-flight as rung 0 under its own cause word, one
|
|
31
33
|
* record face for both) changes the notice set and is a ticket, not a repair.
|
|
32
34
|
*/
|
|
33
|
-
import type { Model } from "../../internal/llm.js";
|
|
35
|
+
import type { AssistantMessage, Model } from "../../internal/llm.js";
|
|
34
36
|
import { type AutoModeClassified } from "../auto-mode.js";
|
|
35
37
|
import type { RunnerDeps } from "../types.js";
|
|
36
38
|
/**
|
|
@@ -51,14 +53,74 @@ import type { RunnerDeps } from "../types.js";
|
|
|
51
53
|
* loud refusal at the brain's request gate.
|
|
52
54
|
*/
|
|
53
55
|
export declare function classifierLadderRungs(knob: unknown, classifier: Model, main: Model): readonly Model[];
|
|
56
|
+
/**
|
|
57
|
+
* WHY the classifier seat is the model it is when no `classifier` role names one — the closed set behind the
|
|
58
|
+
* `classifier.role_defaulted` notice's `reason`. `tier_below_main` = the nearest directly-bound tier below the
|
|
59
|
+
* run's main model; `tier_table` = the static `flash` row (a main bound to no tier); `role_fallback` = the
|
|
60
|
+
* `summarize → default` chain (no tiers at all); `main_only` = the main model is the only model there is (a
|
|
61
|
+
* single-model catalog, or nothing bound below the main's tier); `role_unresolvable` = a `classifier` role
|
|
62
|
+
* NAMED a model the catalog cannot resolve (a stale or misspelled reference), so the main model stands in —
|
|
63
|
+
* the pre-#805 leg fell back the same way, silently.
|
|
64
|
+
*/
|
|
65
|
+
export declare const CLASSIFIER_ROLE_DEFAULT_REASONS: readonly ["tier_below_main", "tier_table", "role_fallback", "main_only", "role_unresolvable"];
|
|
66
|
+
export type ClassifierRoleDefaultReason = (typeof CLASSIFIER_ROLE_DEFAULT_REASONS)[number];
|
|
67
|
+
/** What {@link classifierSeatModel} returns: the model, and — when no role NAMED it — why it is that one. */
|
|
68
|
+
export type ClassifierSeat = {
|
|
69
|
+
readonly model: Model;
|
|
70
|
+
readonly defaulted?: ClassifierRoleDefaultReason;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* The model the classifier SEAT resolves to for this run — ONE resolver for "which model classifies":
|
|
74
|
+
* · a `classifier` role that NAMES a model (`roleSpecNamesModel`: a string ref, a Model object, `{model}`,
|
|
75
|
+
* `{select}`) ⇒ the role resolver's answer, exactly as before (a preset-only role — `{thinking}` alone —
|
|
76
|
+
* does not name one and takes the default below);
|
|
77
|
+
* · else, with tier bindings declared (`deps.tiers`) and a main model on a tier ⇒ the nearest tier strictly
|
|
78
|
+
* BELOW the main's whose route differs from the main's — `tier_below_main`; the main's tier is the LOWEST
|
|
79
|
+
* tier its route sits on (a route bound as both `max` and `flash` is a `flash` main: deterministic, and
|
|
80
|
+
* the cheaper reading); nothing below ⇒ the main itself, `main_only`. Read off the expanded catalog's
|
|
81
|
+
* tier keys, the Runner's own resolution of a tier word: a key synthesised for an unbound tier falls DOWN
|
|
82
|
+
* to the next bound tier, which is the model this walk reaches anyway, and a catalog key spelled like a
|
|
83
|
+
* tier shadows the binding here as it does everywhere. No thinking preference: whether a model's
|
|
84
|
+
* thinking can be turned off is the SEAT's business ({@link classifierRungSeat}), not routing's;
|
|
85
|
+
* · else the role resolver's own fallback (the static tier row `classifier → flash` when tiers are active,
|
|
86
|
+
* `tier_table`; the `summarize → default` chain otherwise, `role_fallback`); a catalog with no roles at
|
|
87
|
+
* all ⇒ the main model, `main_only`.
|
|
88
|
+
* The relative pick follows the run's MAIN model on purpose: a side channel that resolved without it is the
|
|
89
|
+
* misfit the ticket names (a `flash` classifier above a `lite` main; two tiers below a `max` main).
|
|
90
|
+
*
|
|
91
|
+
* @contract classifier.seat.one_resolver — this function is the ONE answer to "which model judges this run's
|
|
92
|
+
* gated calls". The engine's arming calls it and a host that DISPLAYS the seat (a capabilities face, an
|
|
93
|
+
* operator pane) calls the same exported function rather than re-deriving the pick from catalog, roles and
|
|
94
|
+
* tiers: a second derivation is a second answer, and the two drift the moment either side's rules move.
|
|
95
|
+
*/
|
|
96
|
+
export declare function classifierSeatModel(args: {
|
|
97
|
+
deps: Pick<RunnerDeps, "models" | "roles" | "tiers">;
|
|
98
|
+
main: Model;
|
|
99
|
+
}): ClassifierSeat;
|
|
100
|
+
/**
|
|
101
|
+
* The `classifier.role_defaulted` line — minted ONCE per run by the arming, AFTER the route pre-flight, so it
|
|
102
|
+
* names the seat actually asked (a pre-flight swap to the main model is its own line,
|
|
103
|
+
* `route.fallback_to_primary`, same run). Silent when a role named the model: nothing was defaulted.
|
|
104
|
+
*/
|
|
105
|
+
export declare function announceClassifierRoleDefaulted(args: {
|
|
106
|
+
onNotice: RunnerDeps["onNotice"];
|
|
107
|
+
seat: ClassifierSeat;
|
|
108
|
+
sessionId?: string;
|
|
109
|
+
runId?: string;
|
|
110
|
+
}): void;
|
|
54
111
|
/**
|
|
55
112
|
* The classifier's REASONING SEAT, read off the model a classification is actually asked on — ONE predicate:
|
|
56
|
-
*
|
|
57
|
-
* `low` + the verdict cap + the low
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
113
|
+
* does the wire SPELL off ({@link thinkingOffSpelled})? Yes ⇒ `off` + the verdict-sized cap (the request pays
|
|
114
|
+
* for the answer alone); no ⇒ `low` + the verdict cap + the low allowance (a model whose thinking the wire
|
|
115
|
+
* cannot turn off is asked for the least it can grade, in the room that costs — CC 2.1.259 `MU`: spell off,
|
|
116
|
+
* or leave thinking to the provider and pay 2048). A model that declares no reasoning at all is on the `low`
|
|
117
|
+
* seat too: its wire carries nothing either way, so the request is the gateway's default, and a default-on
|
|
118
|
+
* gateway thinks — the arm that used to read it as "never thinks" is the one the field report was about.
|
|
119
|
+
* PER RUNG, not once per arming: the second rung is a different model with its own wire.
|
|
120
|
+
*
|
|
121
|
+
* The THINK seat is the RE-ASK seat ({@link classifierReplyAsksAgain}): a first attempt that produced no
|
|
122
|
+
* verdict is asked once more with thinking allowed the room of CC's stage-2 classifier (8192) — an unknown
|
|
123
|
+
* model that answered NOTHING at its cap has just told us it thinks past it, and is re-asked as one that does.
|
|
62
124
|
*/
|
|
63
125
|
export declare const CLASSIFIER_OFF_SEAT: {
|
|
64
126
|
reasoning: "off";
|
|
@@ -68,11 +130,92 @@ export declare const CLASSIFIER_LOW_SEAT: {
|
|
|
68
130
|
reasoning: "low";
|
|
69
131
|
maxTokens: number;
|
|
70
132
|
};
|
|
133
|
+
export declare const CLASSIFIER_THINK_SEAT: {
|
|
134
|
+
reasoning: "low";
|
|
135
|
+
maxTokens: number;
|
|
136
|
+
};
|
|
137
|
+
export type ClassifierSeatRequest = typeof CLASSIFIER_OFF_SEAT | typeof CLASSIFIER_LOW_SEAT | typeof CLASSIFIER_THINK_SEAT;
|
|
138
|
+
/**
|
|
139
|
+
* The seat for one rung's FIRST attempt — see the seat table above.
|
|
140
|
+
*
|
|
141
|
+
* @contract classifier.budget_is_room — every classifier output cap is ROOM the request asks for, never a
|
|
142
|
+
* bound the engine can vouch for: a gateway may exclude reasoning tokens from `max_tokens` (a vLLM-fronted
|
|
143
|
+
* Qwen gateway answered a cap of 128 with 938 output tokens and `stop`), so an over-long think is cut by the
|
|
144
|
+
* round-trip timeout (`unavailable` / `timeout`), not by the cap (`parse_error` on an empty reply), and the
|
|
145
|
+
* two are attested as the two different facts they are.
|
|
146
|
+
*/
|
|
71
147
|
export declare function classifierRungSeat(model: Model): typeof CLASSIFIER_OFF_SEAT | typeof CLASSIFIER_LOW_SEAT;
|
|
148
|
+
/**
|
|
149
|
+
* The user-message TAIL of each attempt — CC 2.1.259's classifier suffixes, verbatim: the FIRST attempt
|
|
150
|
+
* carries the fast-mode suffix (`$ns`: the answer must begin with `<block>`, no preamble), the RE-ASK carries
|
|
151
|
+
* the thinking-stage suffix (`Uns`: think in `<thinking>` first, then answer). The base prompt (2.1.250 `KRg`)
|
|
152
|
+
* already says "no preamble" once; upstream repeats it at the END of the user message, and it is the repeat
|
|
153
|
+
* that lands — live 2026-09-15, a Qwen 3.8 with thinking off went from 70% tagged replies (1.2–3.9k chars of
|
|
154
|
+
* "Looking at this action…" prose) to 90% at 8 output tokens with the first suffix, and the re-ask suffix
|
|
155
|
+
* had it write its reasoning inside `<thinking>` (stripped by the reader) before the tag.
|
|
156
|
+
*/
|
|
157
|
+
export declare const CLASSIFIER_ATTEMPT_SUFFIX: {
|
|
158
|
+
readonly first: "\nErr on the side of blocking. Your ENTIRE response MUST begin with <block>. Do NOT output any analysis, reasoning, or commentary before <block>. No \"Looking at...\" or similar preamble.";
|
|
159
|
+
readonly reask: "\nReview the classification process and follow it carefully, making sure you deny actions that should be blocked. As a reminder, explicit (not suggestive or implicit) user confirmation is required to override blocks. Use <thinking> before responding with <block>. Think longer on ambiguous or borderline actions; keep reasoning brief for clear-cut ones.";
|
|
160
|
+
};
|
|
161
|
+
/** The classifier reply's answer text — every text block joined; thinking blocks are not the verdict. */
|
|
162
|
+
export declare function classifierReplyText(reply: AssistantMessage): string;
|
|
163
|
+
/**
|
|
164
|
+
* CC 2.1.259 `$Tt`: an EMPTY reply that stopped at the output cap is "unparseable" — a reply to ask for again,
|
|
165
|
+
* not a refusal and not a verdict. The engine's brains report that cut two ways: `stopReason:"length"` (an
|
|
166
|
+
* engine-shrunk cap) or `stopReason:"error"` with `errorKind:"length_empty"` (a static cap — the classifier's
|
|
167
|
+
* own). An empty reply with any OTHER stop is CC's `policy_refusal` arm: not re-asked.
|
|
168
|
+
*/
|
|
169
|
+
export declare function classifierReplyEmptyAtCap(reply: AssistantMessage): boolean;
|
|
170
|
+
/**
|
|
171
|
+
* A reply that may answer in the TAG form only: its text is not a complete answer of its own. Output cut by the cap
|
|
172
|
+
* (`stopReason: "length"`), a dropped connection or a stalled stream (`partialFinalized`, with a synthesised
|
|
173
|
+
* `stop`), and a reply that CALLED A TOOL (`stopReason: "toolUse"`, or a tool-call block in the content — the
|
|
174
|
+
* classifier is given no tools, and the openai brain reports a length cut with a surviving call as `toolUse`):
|
|
175
|
+
* whatever text arrived is a prefix or a fragment — a `No` on its way to `No, this must be blocked` — so it may
|
|
176
|
+
* carry a verdict only through a `<block>` tag, which reads a `<block>no` prefix as CC does; the bare-answer arm
|
|
177
|
+
* never sees it.
|
|
178
|
+
*/
|
|
179
|
+
export declare function classifierReplyTagOnly(reply: AssistantMessage): boolean;
|
|
180
|
+
/**
|
|
181
|
+
* The text the DECIDER reads for its verdict: the reply's answer text, except that a tag-only reply
|
|
182
|
+
* ({@link classifierReplyTagOnly}) whose text carries NO tagged answer reads as EMPTY — a cut prefix must not
|
|
183
|
+
* reach the bare-answer arm as if it were the whole reply (adversarial passes 2–3: a connection cut after `No`,
|
|
184
|
+
* and a scratch `<block>` mention before a cut `No`, executed the gated tool). Judged by the reader itself with
|
|
185
|
+
* its bare arm closed, so "no tagged verdict" and "no verdict" cannot drift. Only the MISSING form is emptied:
|
|
186
|
+
* a tag-only reply that carries a CONFLICT keeps its text, so the decider records the conflict and the leg
|
|
187
|
+
* does not re-ask it (adversarial pass 4: emptying every parse failure had turned a cut conflict into a
|
|
188
|
+
* missing reply, and the re-ask into an allow). The decider then records `parse_error` / `missing` for the
|
|
189
|
+
* emptied text, the same word an empty reply gets.
|
|
190
|
+
*/
|
|
191
|
+
export declare function classifierReplyVerdictText(reply: AssistantMessage): string;
|
|
192
|
+
/**
|
|
193
|
+
* Whether a first attempt is asked AGAIN on the THINK seat — ONE rule: the reply is MISSING a verdict, either
|
|
194
|
+
* empty at the cap ({@link classifierReplyEmptyAtCap}), cut or fragmentary without a tagged verdict
|
|
195
|
+
* ({@link classifierReplyTagOnly}), or text in which the reader finds no answer at all (`parse_error` of form
|
|
196
|
+
* `missing`: prose, a thinking-only reply). Not a CONFLICT (two `<block>` answers — it carries a `yes`, and a second attempt that answered `no`
|
|
197
|
+
* would erase it: the fail-closed `parse_error` it is stands) and not an empty reply with a non-cap stop
|
|
198
|
+
* (CC's refusal arm). CC 2.1.259 `Vrr` re-asks an unparseable reply up to `maxRetries` times; sema re-asks
|
|
199
|
+
* ONCE, on the stage-2 room, so a rung costs at most two calls. A failed reply ({@link classifierReplyFailure})
|
|
200
|
+
* is judged before this and never reaches it.
|
|
201
|
+
*/
|
|
202
|
+
export declare function classifierReplyAsksAgain(reply: AssistantMessage): boolean;
|
|
203
|
+
/**
|
|
204
|
+
* A reply that is a LEG FAILURE rather than an answer — the brains RETURN a provider error as a message
|
|
205
|
+
* (`stopReason: "error"` with the provider's text on `errorMessage`: an HTTP 4xx/5xx, a quota refusal, an
|
|
206
|
+
* overloaded gateway) and a cancelled request as `"aborted"`; neither is a reply the reader may judge. Read as
|
|
207
|
+
* text they were `parse_error` — a card saying "the classifier's reply carried no verdict" about a model that
|
|
208
|
+
* never answered, a ladder record blaming `parse_error` instead of `error`. The leg THROWS what this returns
|
|
209
|
+
* and the decider maps the throw to `unavailable` (`error`), as it maps every leg failure; the sibling
|
|
210
|
+
* side-channel legs (compaction, consolidation) already read an error stop this way. The cap-cut form
|
|
211
|
+
* (`errorKind: "length_empty"`) is not a failure: it is the empty-at-cap reply the re-ask owns.
|
|
212
|
+
*/
|
|
213
|
+
export declare function classifierReplyFailure(reply: AssistantMessage): Error | undefined;
|
|
72
214
|
/**
|
|
73
215
|
* The per-decision READ of a classification, as one seat: the `auto_mode.classified` trace frame (wall time,
|
|
74
|
-
* verdict, cause)
|
|
75
|
-
* throwing sink never reaches the gate (the decider
|
|
216
|
+
* verdict, cause), the ladder's throttled `classifier.fallback` disclosure and the per-model
|
|
217
|
+
* `classifier.contract_broken` streak. All are observers — a throwing sink never reaches the gate (the decider
|
|
218
|
+
* swallows it), and none changes a verdict.
|
|
76
219
|
*
|
|
77
220
|
* The frame's `model` is the model the classification RAN ON, which is what the field has always meant (its
|
|
78
221
|
* declaration reads "the resolved classifier role, or the main model it fell back to"): the route
|