@sema-agent/core 5.61.0 → 5.63.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 +107 -0
- package/dist/agents/subagent.d.ts +12 -2
- package/dist/agents/subagent.js +3 -2
- package/dist/brain/open-responses.js +8 -3
- package/dist/brain/openai.js +4 -4
- package/dist/brain/stream-engine.d.ts +13 -2
- package/dist/brain/stream-engine.js +3 -3
- package/dist/core/auto-compaction.d.ts +6 -4
- package/dist/core/auto-compaction.js +3 -0
- package/dist/core/auto-mode-prompt-assets.js +1 -1
- package/dist/core/checkpoint-store.d.ts +36 -4
- package/dist/core/checkpoint-store.js +1 -0
- package/dist/core/context-edit.d.ts +36 -29
- package/dist/core/context-edit.js +3 -3
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +2 -0
- package/dist/core/hooks.d.ts +86 -4
- package/dist/core/hooks.js +3 -3
- package/dist/core/memory-engine/engine.d.ts +11 -0
- package/dist/core/memory-engine/engine.js +29 -3
- package/dist/core/memory-engine/index.d.ts +1 -1
- package/dist/core/memory-engine/origin-clearance.d.ts +28 -0
- package/dist/core/park-selfcheck.js +2 -0
- package/dist/core/pricing.d.ts +24 -0
- package/dist/core/pricing.js +18 -0
- package/dist/core/runner/prepare-config-doors.d.ts +36 -2
- package/dist/core/runner/prepare-config-doors.js +66 -8
- package/dist/core/runner/prepare-task.d.ts +113 -12
- package/dist/core/runner/prepare-task.js +239 -131
- package/dist/core/runner/runtask.d.ts +7 -0
- package/dist/core/runner/runtask.js +325 -95
- package/dist/core/runner/turn-attachments.d.ts +137 -5
- package/dist/core/runner/turn-attachments.js +25 -2
- package/dist/core/store-contracts/checkpoint-store-contract.js +19 -0
- package/dist/core/tool-errors.d.ts +2 -1
- package/dist/core/tool-policy.d.ts +27 -0
- package/dist/core/trace.d.ts +5 -4
- package/dist/core/types.d.ts +163 -29
- package/dist/core/untrusted-text.d.ts +5 -4
- package/dist/core/untrusted-text.js +8 -0
- package/dist/core/usage-window-store.d.ts +109 -8
- package/dist/core/usage-window-store.js +79 -12
- package/dist/engine/harness/agent-harness.js +20 -5
- package/dist/engine/harness/types.d.ts +38 -0
- package/dist/engine/loop/agent-loop.js +20 -1
- package/dist/engine/loop/types.d.ts +41 -1
- package/dist/index.d.ts +1 -1
- package/dist/orchestration/run-workflow-tool.d.ts +2 -2
- package/dist/orchestration/workflow-types.d.ts +48 -1
- package/dist/orchestration/workflow-types.js +12 -4
- package/dist/orchestration/workflow.d.ts +14 -3
- package/dist/orchestration/workflow.js +44 -19
- package/dist/prompt-assembly/event-registry.js +2 -0
- package/dist/prompts/default.js +1 -1
- package/dist/server/http.d.ts +1 -1
- package/dist/stores/file/usage-window-store.d.ts +1 -1
- package/dist/stores/file/usage-window-store.js +27 -6
- package/dist/tools/loop-tick.js +1 -1
- package/dist/tools/scheduler-tools.js +9 -1
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +3 -1
package/dist/core/hooks.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DocumentContent, ImageContent, TextContent } from "../internal/llm.js";
|
|
1
|
+
import type { ActorAssertion, DocumentContent, ImageContent, TextContent } from "../internal/llm.js";
|
|
2
2
|
import type { ExecutionEnv, FileError, Result, SessionTreeEntry } from "../internal/harness-types.js";
|
|
3
3
|
import type { DecisionReason, PermissionResult, ResolvedAsk, ToolCallRequest, ToolPolicy } from "./tool-policy.js";
|
|
4
4
|
import { type AskClass } from "./ask-class.js";
|
|
@@ -85,9 +85,36 @@ export interface Hooks {
|
|
|
85
85
|
*/
|
|
86
86
|
preToolUseObservational?: true;
|
|
87
87
|
postToolUse?(toolName: string, input: unknown, output: HookToolOutput, ctx: HookToolContext): PostToolUseResult | undefined | Promise<PostToolUseResult | undefined>;
|
|
88
|
+
/**
|
|
89
|
+
* design/373 §4.3 (D2) ⚠️ WIDENED INVOCATION SET: this seat now screens EVERY user-lane entrance,
|
|
90
|
+
* not just the objective — a live `TaskStream.steer`, a wake resume's message (pre-CAS, at the
|
|
91
|
+
* verb), and a parked steer's resume redelivery (any draining resume kind) all pass through it, discriminated by
|
|
92
|
+
* {@link UserPromptSubmitContext.source}. A hook that blocks unconditionally therefore starts
|
|
93
|
+
* refusing steers too — branch on `ctx.source` (+ `ctx.actor`) to scope a verdict. The screen's
|
|
94
|
+
* domain is caller/operator PROSE only: engine-authored frames (task notifications, diagnostics,
|
|
95
|
+
* recall) never pass here — external `notify()` text takes the sanitized DATA-frame lane instead
|
|
96
|
+
* (ruled: notification frames are not prompts; see the notify contract). The same input can
|
|
97
|
+
* legally arrive twice under two sources (live + parked redelivery) — idempotency is the hook's,
|
|
98
|
+
* keyed on `ctx.inputId`. Block semantics per entrance: objective ⇒ the task ends `blocked`;
|
|
99
|
+
* live steer ⇒ typed `steering.blocked_by_hook` to the steer caller; resume message ⇒ typed
|
|
100
|
+
* pre-CAS refusal (the checkpoint stays pending); parked redelivery ⇒ the frame is withheld
|
|
101
|
+
* with a `steering.parked_input_blocked` notice + a `delivery:"blocked"` human_input account,
|
|
102
|
+
* and the resume itself proceeds (a blocked instruction must never wedge a wake).
|
|
103
|
+
*
|
|
104
|
+
* ⚠️ RE-ENTRANT within one leg: the steer verb goes live the moment the run's handle publishes —
|
|
105
|
+
* BEFORE the objective seat runs — so a steer screen can overlap the objective screen (and a
|
|
106
|
+
* resume-message screen, which fires pre-CAS at the verb) in flight. A stateful implementation
|
|
107
|
+
* (per-call temp files, a single in-flight latch, a shared accumulator) must tolerate concurrent
|
|
108
|
+
* entry; only steer-vs-steer is serialized. And that serialization has a LATENCY face: queued
|
|
109
|
+
* steers screen strictly FIFO, so each waits behind its predecessor's ENTIRE hook bound (the
|
|
110
|
+
* resolved `timeoutMs`, default 600s) — a slow or wedged screen delays every steer behind it by
|
|
111
|
+
* up to the full bound, which is a reason to declare a short `timeoutMs` on deployments that
|
|
112
|
+
* screen steers.
|
|
113
|
+
*/
|
|
88
114
|
userPromptSubmit?(prompt: string,
|
|
89
115
|
/** #281 件A (additive): the run/leg identity envelope — an implementation declaring only
|
|
90
|
-
* `(prompt)` keeps working. Always supplied on the engine's emission.
|
|
116
|
+
* `(prompt)` keeps working. Always supplied on the engine's emission. design/373: carries the
|
|
117
|
+
* entrance discriminator (`source`/`inputId`/`actor`) — see {@link UserPromptSubmitContext}. */
|
|
91
118
|
ctx?: UserPromptSubmitContext): UserPromptSubmitResult | undefined | Promise<UserPromptSubmitResult | undefined>;
|
|
92
119
|
/**
|
|
93
120
|
* roadmap #5 (CC 198 Stop hook, :473831-): runs when the agent WOULD OTHERWISE END its run (no
|
|
@@ -819,12 +846,58 @@ export interface PostToolUseResult {
|
|
|
819
846
|
additionalContext?: string;
|
|
820
847
|
}
|
|
821
848
|
/** #281 件A — the context of a {@link Hooks.userPromptSubmit} invocation (second parameter,
|
|
822
|
-
* additive).
|
|
849
|
+
* additive). An object so later additions stay additive; design/373 §4.3 (D2) grew the
|
|
850
|
+
* entrance-discrimination trio below. */
|
|
823
851
|
export interface UserPromptSubmitContext {
|
|
824
852
|
/** The run/leg identity envelope — see {@link HookInvocationIdentity} for the presence law. */
|
|
825
853
|
identity?: HookInvocationIdentity;
|
|
826
854
|
/** {@link HookSeatSignal} — this invocation's own abort signal. */
|
|
827
855
|
signal?: AbortSignal;
|
|
856
|
+
/**
|
|
857
|
+
* design/373 §4.3 (D2) — WHICH user-lane entrance this text is entering through. The screen now
|
|
858
|
+
* sits at every user-lane entrance, so a hook that used to see only the objective can (and, if it
|
|
859
|
+
* blocks unconditionally, MUST) branch on this: `"objective"` = the task's opening prompt (the
|
|
860
|
+
* pre-373 only caller); `"steer"` = a live mid-run TaskStream.steer; `"resume_message"` = the
|
|
861
|
+
* message riding a wake resume (screened at the verb, pre-CAS); `"parked_redelivery"` = a parked
|
|
862
|
+
* steer being re-screened on the CURRENT process's hook when a resume redelivers it — ANY resume
|
|
863
|
+
* kind that drains parked steers (policy_ask / dry_run_review / plan_review / resource_limit,
|
|
864
|
+
* wake included), because a park's screening cannot be carried across processes (hooks are
|
|
865
|
+
* process-local config). ⚠️ DOUBLE ARRIVAL IS CONTRACTUAL:
|
|
866
|
+
* the same input can reach a hook twice with different sources (screened live as `"steer"`,
|
|
867
|
+
* parked, re-screened as `"parked_redelivery"`); idempotency is the hook's to hold — a stateful
|
|
868
|
+
* hook keys on {@link inputId}. `"followUp"` is a RESERVED spelling (spec seat only): the Runner
|
|
869
|
+
* face has no user-lane followUp verb today, so no invocation carries it yet — if/when that face
|
|
870
|
+
* lands it arrives screened under this name (never a silent widening).
|
|
871
|
+
* Absent ⇒ an emission from a pre-373 engine (old engines stamped nothing); every current engine
|
|
872
|
+
* seat stamps its name, so treat absence as "version unknown", never as a sixth entrance.
|
|
873
|
+
*/
|
|
874
|
+
source?: "objective" | "steer" | "resume_message" | "followUp" | "parked_redelivery";
|
|
875
|
+
/**
|
|
876
|
+
* design/373 §4.3 (D2) — the screened input's correlation id. Present on EVERY entrance except
|
|
877
|
+
* the objective seat (no id exists on that face — nothing is fabricated there):
|
|
878
|
+
* - `"steer"` — the effective `inputId` (caller-supplied or engine-minted — the SAME id the
|
|
879
|
+
* `human_input` event and any parked copy carry);
|
|
880
|
+
* - `"parked_redelivery"` — the parked entry's STORED id (stable across redeliveries);
|
|
881
|
+
* - `"resume_message"` — always present too: the caller's id verbatim when it supplied one,
|
|
882
|
+
* otherwise MINTED PER CALL by the wake validator. ⚠️ Per call, not per message: a retried wake
|
|
883
|
+
* that supplies no caller id and REACHES this seat re-screens identical text under a FRESH id.
|
|
884
|
+
* A dedup/quota hook keying its ledger here gets cross-attempt stability only when the CALLER
|
|
885
|
+
* supplies the id — a host that retries wakes should mint its own. What the engine guarantees
|
|
886
|
+
* instead (#472) is that the seat is not reached needlessly: it sits BELOW every retryable
|
|
887
|
+
* pre-CAS refusal on the resume ladder, so an attempt the engine was going to refuse anyway
|
|
888
|
+
* (a full governance window most of all) costs ZERO invocations — the retry loop that refusal
|
|
889
|
+
* invites cannot bill this hook per attempt.
|
|
890
|
+
* This is the dedup key for the double-arrival contract on {@link source}: a quota-charging hook
|
|
891
|
+
* that must not double-charge one input keys its ledger here.
|
|
892
|
+
*/
|
|
893
|
+
inputId?: string;
|
|
894
|
+
/**
|
|
895
|
+
* design/373 §4.3 (D2) — the API-boundary actor snapshot attributed to this input (the same
|
|
896
|
+
* snapshot the `human_input` event reports), when the caller supplied one. Attribution only,
|
|
897
|
+
* never authority — a hook wanting to exempt its own supervisor lane branches on this PLUS
|
|
898
|
+
* {@link source}, and absence means "anonymous caller", exactly as on the event face.
|
|
899
|
+
*/
|
|
900
|
+
actor?: ActorAssertion;
|
|
828
901
|
}
|
|
829
902
|
/** #281 件A — the context of a {@link Hooks.postToolBatch} invocation (third parameter, additive —
|
|
830
903
|
* deliberately NOT folded into `meta`, whose presence already means "the engine injected here"). */
|
|
@@ -1199,7 +1272,16 @@ export interface ToolGateInput {
|
|
|
1199
1272
|
/** design/375 §5.2②: the surviving ask's engine-stamped `segmentCoverage` — threaded so the park
|
|
1200
1273
|
* mint's offer factory reads the same coverage snapshot on the durable route as the synchronous
|
|
1201
1274
|
* mints read off the decision (absent ⇒ all segments uncovered ⇒ over-offer, the safe way). */
|
|
1202
|
-
segmentCoverage?: readonly import("./permission-rule-model.js").SegmentCoverage[]
|
|
1275
|
+
segmentCoverage?: readonly import("./permission-rule-model.js").SegmentCoverage[],
|
|
1276
|
+
/** #457 ④: the surviving ask's `matchedAskRule` — threaded for the SAME errand as
|
|
1277
|
+
* `askDecisionReason`: the park mint's rule-suggestion door must know that an EXPLICIT `ask` rule
|
|
1278
|
+
* matched this call, because the persisted-rule lane refuses to clear such an ask (the
|
|
1279
|
+
* `decision.matchedAskRule === undefined` conjunct on its allow arm), so the parked card must not
|
|
1280
|
+
* offer to mint a rule that would never silence it. APPENDED at the tail deliberately: every
|
|
1281
|
+
* parameter here is positional and `probeReason` is also a `string`, so inserting this beside its
|
|
1282
|
+
* logical neighbour would silently re-bind three existing call sites' arguments with no type
|
|
1283
|
+
* error to catch it. */
|
|
1284
|
+
matchedAskRule?: string) => Promise<ToolGateResult["suspend"] | ParkAttemptFailed | undefined>;
|
|
1203
1285
|
/**
|
|
1204
1286
|
* design/174 — route a policy `ask` on the reserved question tool to this run's CONTENT-ask channel
|
|
1205
1287
|
* before it can become a park or a refusal. Called in the `ask` branch with the FINAL post-hook,
|
package/dist/core/hooks.js
CHANGED
|
@@ -841,7 +841,7 @@ export async function runToolGate(input) {
|
|
|
841
841
|
? { origin: orgAskOrigin !== undefined ? `org_${orgAskOrigin}` : "policy" }
|
|
842
842
|
: undefined;
|
|
843
843
|
if (suspendAsk && decision.action === "ask") {
|
|
844
|
-
const suspended = await suspendAsk(req, currentInput, safety, undefined, realApprovalOf(decision), decision.action === "ask" ? decision.persistedRuleShadowed : undefined, decision.action === "ask" ? decision.decisionReason : undefined, decision.action === "ask" ? decision.probeReason : undefined, decision.action === "ask" ? decision.probeCause : undefined, decision.action === "ask" ? decision.segmentCoverage : undefined);
|
|
844
|
+
const suspended = await suspendAsk(req, currentInput, safety, undefined, realApprovalOf(decision), decision.action === "ask" ? decision.persistedRuleShadowed : undefined, decision.action === "ask" ? decision.decisionReason : undefined, decision.action === "ask" ? decision.probeReason : undefined, decision.action === "ask" ? decision.probeCause : undefined, decision.action === "ask" ? decision.segmentCoverage : undefined, decision.action === "ask" ? decision.matchedAskRule : undefined);
|
|
845
845
|
if (suspended) {
|
|
846
846
|
if ("parkFailed" in suspended)
|
|
847
847
|
parkFailed = suspended.parkFailed;
|
|
@@ -860,7 +860,7 @@ export async function runToolGate(input) {
|
|
|
860
860
|
req.args = outcome.presentedInput;
|
|
861
861
|
}
|
|
862
862
|
if (suspendAsk && outcome.parkDeclined && parkFailed === undefined) {
|
|
863
|
-
const suspended = await suspendAsk(req, currentInput, safety, true, realApprovalOf(decision), decision.action === "ask" ? decision.persistedRuleShadowed : undefined, decision.action === "ask" ? decision.decisionReason : undefined, decision.action === "ask" ? decision.probeReason : undefined, decision.action === "ask" ? decision.probeCause : undefined, decision.action === "ask" ? decision.segmentCoverage : undefined);
|
|
863
|
+
const suspended = await suspendAsk(req, currentInput, safety, true, realApprovalOf(decision), decision.action === "ask" ? decision.persistedRuleShadowed : undefined, decision.action === "ask" ? decision.decisionReason : undefined, decision.action === "ask" ? decision.probeReason : undefined, decision.action === "ask" ? decision.probeCause : undefined, decision.action === "ask" ? decision.segmentCoverage : undefined, decision.action === "ask" ? decision.matchedAskRule : undefined);
|
|
864
864
|
if (suspended) {
|
|
865
865
|
if ("parkFailed" in suspended)
|
|
866
866
|
parkFailed = suspended.parkFailed;
|
|
@@ -892,7 +892,7 @@ export async function runToolGate(input) {
|
|
|
892
892
|
askDenyResolution = resolved.resolution;
|
|
893
893
|
decision = resolved;
|
|
894
894
|
if (resolved.action === "deny" && resolved.approverUnavailable === true && suspendAsk && parkFailed === undefined) {
|
|
895
|
-
const suspended = await suspendAsk(req, currentInput, safety, true, realApprovalOf(askBeforeResolve), askBeforeResolve.action === "ask" ? askBeforeResolve.persistedRuleShadowed : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.decisionReason : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.probeReason : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.probeCause : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.segmentCoverage : undefined);
|
|
895
|
+
const suspended = await suspendAsk(req, currentInput, safety, true, realApprovalOf(askBeforeResolve), askBeforeResolve.action === "ask" ? askBeforeResolve.persistedRuleShadowed : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.decisionReason : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.probeReason : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.probeCause : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.segmentCoverage : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.matchedAskRule : undefined);
|
|
896
896
|
if (suspended) {
|
|
897
897
|
if ("parkFailed" in suspended)
|
|
898
898
|
parkFailed = suspended.parkFailed;
|
|
@@ -1152,6 +1152,17 @@ export declare class MemoryEngine {
|
|
|
1152
1152
|
* and side-effect-free per the getByIds contract clause). */
|
|
1153
1153
|
private committedAuditFace;
|
|
1154
1154
|
private static originClearRefusal;
|
|
1155
|
+
/** #468② — the most recent COMPLETED clearance of `entryId` in `rows`, or `undefined` when there is none.
|
|
1156
|
+
* Pure over rows the caller ALREADY read (this opens no second read of the account — the not-marked
|
|
1157
|
+
* refusal it feeds sits below `clearEntryOrigin`'s own resume probe, which read the ledger anyway).
|
|
1158
|
+
* `settledAt` is the row's terminal `done` event (the moment the clear finished), not its opening `at`. */
|
|
1159
|
+
private static lastCompletedClearance;
|
|
1160
|
+
/** C11 (#468② rescan): human-spell a ledger row's `settledAt` for refusal prose WITHOUT letting a
|
|
1161
|
+
* bad value displace the typed refusal — `settledAt` comes from a host-writable ledger FILE, and
|
|
1162
|
+
* a finite-but-out-of-Date-range number (|t| > 8.64e15) makes `toISOString` throw a bare
|
|
1163
|
+
* RangeError that would replace `memory.origin_clear_not_marked`. Bad values degrade to the raw
|
|
1164
|
+
* number spelling; the refusal stays typed on every input. */
|
|
1165
|
+
private static spellSettledAt;
|
|
1155
1166
|
/**
|
|
1156
1167
|
* §13-4① — the VIEW face: every marked entry in the given scopes, each with its marker (what /
|
|
1157
1168
|
* cause / when) and its assembled provenance account (来源委派: the lineage contributors joined
|
|
@@ -3617,11 +3617,34 @@ export class MemoryEngine {
|
|
|
3617
3617
|
const b = this.backend;
|
|
3618
3618
|
return b.restrictedAdoptionView?.({ audit: false }) ?? b.retrievalView?.() ?? this.backend;
|
|
3619
3619
|
}
|
|
3620
|
-
static originClearRefusal(code, message) {
|
|
3620
|
+
static originClearRefusal(code, message, detail) {
|
|
3621
3621
|
const e = new Error(message);
|
|
3622
3622
|
e.code = code;
|
|
3623
|
+
if (detail !== undefined)
|
|
3624
|
+
e.lastClearance = detail.lastClearance;
|
|
3623
3625
|
throw e;
|
|
3624
3626
|
}
|
|
3627
|
+
static lastCompletedClearance(rows, entryId) {
|
|
3628
|
+
let best;
|
|
3629
|
+
for (const r of rows) {
|
|
3630
|
+
if (r.entryId !== entryId || r.status !== "done")
|
|
3631
|
+
continue;
|
|
3632
|
+
const settled = r.events.filter((ev) => ev.to === "done").at(-1);
|
|
3633
|
+
if (settled === undefined)
|
|
3634
|
+
continue;
|
|
3635
|
+
if (best === undefined || settled.at >= best.settledAt)
|
|
3636
|
+
best = { requestId: r.requestId, settledAt: settled.at };
|
|
3637
|
+
}
|
|
3638
|
+
return best;
|
|
3639
|
+
}
|
|
3640
|
+
static spellSettledAt(at) {
|
|
3641
|
+
try {
|
|
3642
|
+
return new Date(at).toISOString();
|
|
3643
|
+
}
|
|
3644
|
+
catch {
|
|
3645
|
+
return String(at);
|
|
3646
|
+
}
|
|
3647
|
+
}
|
|
3625
3648
|
async listExternalOriginEntries(scopes) {
|
|
3626
3649
|
const face = this.committedAuditFace();
|
|
3627
3650
|
const headers = await face.listHeaders(scopes);
|
|
@@ -3648,7 +3671,8 @@ export class MemoryEngine {
|
|
|
3648
3671
|
if (typeof input.reason !== "string" || input.reason.length === 0) {
|
|
3649
3672
|
MemoryEngine.originClearRefusal("memory.origin_clear_invalid", "clearEntryOrigin requires a non-empty reason (the host's stated ground rides the audit row) — refused, never defaulted.");
|
|
3650
3673
|
}
|
|
3651
|
-
const
|
|
3674
|
+
const clearances = readOriginClearances(this.controlDir);
|
|
3675
|
+
const pending = clearances.find((r) => r.entryId === entryId && r.status === "pending");
|
|
3652
3676
|
if (pending !== undefined)
|
|
3653
3677
|
return await this.completeOriginClearance(pending, input.requestId);
|
|
3654
3678
|
const face = this.committedAuditFace();
|
|
@@ -3658,7 +3682,9 @@ export class MemoryEngine {
|
|
|
3658
3682
|
}
|
|
3659
3683
|
const origin = committedOriginOf(committed.frontmatter);
|
|
3660
3684
|
if (origin === undefined) {
|
|
3661
|
-
|
|
3685
|
+
const lastClearance = MemoryEngine.lastCompletedClearance(clearances, entryId);
|
|
3686
|
+
MemoryEngine.originClearRefusal("memory.origin_clear_not_marked", `clearEntryOrigin: entry ${JSON.stringify(entryId.slice(0, 80))} carries no external-origin marker — nothing to clear.` +
|
|
3687
|
+
(lastClearance !== undefined ? ` A clearance for this entry already settled at ${MemoryEngine.spellSettledAt(lastClearance.settledAt)} (requestId ${JSON.stringify(lastClearance.requestId)}) — this reads as a re-send of it.` : ""), lastClearance !== undefined ? { lastClearance } : undefined);
|
|
3662
3688
|
}
|
|
3663
3689
|
if (this.readChallengeExclusions().has(entryId)) {
|
|
3664
3690
|
MemoryEngine.originClearRefusal("memory.origin_clear_challenged", `clearEntryOrigin: entry ${JSON.stringify(entryId.slice(0, 80))} is challenged/latched — adjudicate the challenge first (the clear valve is not a challenge exit).`);
|
|
@@ -11,7 +11,7 @@ export { committedDistilledOf, distilledEquals } from "./frontmatter.js";
|
|
|
11
11
|
export { CONSOLIDATION_DEFAULTS, ConsolidationRefusedError, MEMORY_SEARCH_SUPERSEDED_TAG, consolidationTypeEligible, deriveSupersededSet, memorySupersededNote, readIntentCredentials, supersessionFuseCeiling, type ConsolidationGateRead, type ConsolidationGateRow, type ConsolidationIntent, type ConsolidationIntentCredentialRow, type ConsolidationLeaseSeat, type ConsolidationProductProposal, type ConsolidationProposal, type MemoryConsolidationOptions, CONSOLIDATION_RUN_STOP_REASONS, type ConsolidationRunStopReason, } from "./consolidation.js";
|
|
12
12
|
export { DISTILLER_DEFAULT_MAX_INPUTS_PER_PRODUCT, LLM_DISTILLER_CONTRACT, LLM_DISTILLER_CONTRACT_DL2, LLM_DISTILLER_CONTRACT_DL3, LLM_DISTILLER_CONTRACTS, MEMORY_DISTILLER_CONTRACT_V1, contractGroupingDiff, driveConsolidationToFixpoint, isAliasModelId, llmPlanDistiller, mintExposurePartitionedPlan, mintLlmConsolidationPlan, openAiCompatChatSeat, parseJsonAnswer, planParseRepairs, sanitizeLlmGroups, scheduleUnderFuse, type ConsolidationDistillFn, type ConsolidationDriveCycleRow, type ConsolidationDriveEngine, type ConsolidationDriveResult, type ConsolidationFoldState, type DistillerCandidate, type DistillerChatAnswer, type DistillerChatFn, type DistillerChatRequest, type FuseSchedule, type LlmConsolidationPlan, type LlmConsolidationPlanArm, type LlmConsolidationPlanProduct, type LlmDistillerContract, type MintLlmConsolidationPlanResult, type PlanParseRepairs, type SanitizedLlmGroups, } from "./distiller.js";
|
|
13
13
|
export { CONSOLIDATION_DRIVER_PLANS_DIR, CONSOLIDATION_DRIVER_RUNS_FILE, archiveDistillerPlan, readConsolidationDriverRun, runMemoryConsolidationDriver, type ConsolidationDriverEngine, type ConsolidationDriverRunRow, type ConsolidationRunReceipt, type RunMemoryConsolidationOptions, } from "./consolidation-driver.js";
|
|
14
|
-
export type { OriginClearanceRow, OriginClearanceEvent } from "./origin-clearance.js";
|
|
14
|
+
export type { OriginClearanceRow, OriginClearanceEvent, OriginClearanceShadow } from "./origin-clearance.js";
|
|
15
15
|
export { MEMORY_ORIGIN_CAUSES } from "./types.js";
|
|
16
16
|
export type { MemoryBackend, MemoryEntry, MemoryEntryFrontmatter, MemoryEntryOrigin, MemoryOriginCause, MemoryScopeEnumeration, MemoryEntryDistilled, MemoryEntryDistilledInput, MemoryEntryHeader, ScoredMemoryEntry, NotePatch, PatchReport, MaterializedFile, MemorySessionHandle, HarvestReport, HarvestRejection, HarvestRejectionCode, MemoryAnnouncement, ScanFinding, } from "./types.js";
|
|
17
17
|
export { memoryBackendContract, assertMemoryBackendSearchEquivalence, type MemoryBackendContractHooks, } from "./memory-backend-contract.js";
|
|
@@ -40,6 +40,34 @@ export interface OriginClearanceRow {
|
|
|
40
40
|
tombstonedAt?: number;
|
|
41
41
|
events: OriginClearanceEvent[];
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* #468② — the ACCOUNT SHADOW a `memory.origin_clear_not_marked` refusal carries when the engine already
|
|
45
|
+
* held the answer: the entry's most recent COMPLETED clearance, as who asked for it and when it settled.
|
|
46
|
+
*
|
|
47
|
+
* It exists to discriminate the refusal's two very different causes without a second full read of the
|
|
48
|
+
* account: an entry that was NEVER marked (no shadow — nothing to clear, and nothing ever cleared) versus a
|
|
49
|
+
* RE-SEND of a clear that already succeeded (shadow present — the caller's own earlier call, or another
|
|
50
|
+
* host's, already did this). Absence is honest absence: the engine never opens an extra read to fill it, so
|
|
51
|
+
* a refusal raised on a path that did not need the ledger simply omits it.
|
|
52
|
+
*
|
|
53
|
+
* `requestId` is the OPENER's attribution (the `clearEntryOrigin` caller's own key — what a re-sending
|
|
54
|
+
* caller compares against), while `settledAt` is the row's terminal `done` event, which on a crash-resumed
|
|
55
|
+
* row is a LATER moment than the opening. The full row (reason, origin, custody, every event) stays one
|
|
56
|
+
* {@link OriginClearanceRow} lookup away — this shadow is a discriminator, never a replacement for it.
|
|
57
|
+
*
|
|
58
|
+
* AS-OF SEMANTICS (codex r3): the account is read ONCE per `clearEntryOrigin` call, by the probe that looks
|
|
59
|
+
* for a resumable pending row, and the committed entry is read after it. A clearance opened AND settled by
|
|
60
|
+
* ANOTHER caller inside that window is therefore not in the snapshot this shadow is derived from, so its
|
|
61
|
+
* absence means "no settled clearance as of this call's own read of the account", never "none exists" — and
|
|
62
|
+
* a fresh call answers with the newer account. The caller's OWN earlier clear (the re-send case this exists
|
|
63
|
+
* for) settled long before and is always in the snapshot. Deliberately not closed by a second read: the
|
|
64
|
+
* shadow is a convenience over the authoritative `listOriginClearances`, and buying strictness with an extra
|
|
65
|
+
* ledger read on a refusal path would cost every caller what only a concurrent third party could observe.
|
|
66
|
+
*/
|
|
67
|
+
export interface OriginClearanceShadow {
|
|
68
|
+
requestId: string;
|
|
69
|
+
settledAt: number;
|
|
70
|
+
}
|
|
43
71
|
/** Lock-free strict read of the whole account (host audit face; journal-aware, corrupt = throw). */
|
|
44
72
|
export declare function readOriginClearances(controlDir: string): OriginClearanceRow[];
|
|
45
73
|
/**
|
|
@@ -71,6 +71,8 @@ function syntheticCheckpoint(scope) {
|
|
|
71
71
|
args: { probe: true, nested: { depth: 2, list: [1, 2, 3] } },
|
|
72
72
|
boundInputHash: boundInputHashOf({ probe: true, nested: { depth: 2, list: [1, 2, 3] } }),
|
|
73
73
|
hasBidiControls: true,
|
|
74
|
+
preview: { probe: true, note: "park wiring self-check display stub" },
|
|
75
|
+
previewWithheld: "oversize",
|
|
74
76
|
batchToolCallIds: [`${scope}:call`],
|
|
75
77
|
completedCallIds: [],
|
|
76
78
|
},
|
package/dist/core/pricing.d.ts
CHANGED
|
@@ -37,6 +37,30 @@ export interface TokenCounts {
|
|
|
37
37
|
* pricing the cache subset separately instead of trusting the brain's possibly-overstated cost total.
|
|
38
38
|
*/
|
|
39
39
|
export declare function computeCostMicroUsd(counts: TokenCounts, pricing: ModelPricing): number;
|
|
40
|
+
/**
|
|
41
|
+
* RB-368's predicate, single-sourced: does a price table EXIST for this model — an id-keyed
|
|
42
|
+
* `RunnerDeps.pricing` entry, or the model's own `cost` declaration? When neither does,
|
|
43
|
+
* `modelCostToPricing(undefined)` yields an all-zero table and every computed cost is a fabricated 0,
|
|
44
|
+
* indistinguishable from "declared free". Every consumer that must keep those apart (the disclosure faces
|
|
45
|
+
* that go absent rather than lie; the deployment $ governance window that refuses rather than charge a
|
|
46
|
+
* fabricated 0) asks HERE, so the two can never drift into two different answers.
|
|
47
|
+
*/
|
|
48
|
+
export declare function isModelPriced(model: {
|
|
49
|
+
id: string;
|
|
50
|
+
cost?: unknown;
|
|
51
|
+
}, pricing: Record<string, ModelPricing> | undefined): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* The first UNEVALUABLE member of a price table (`"inputPer1M"`, …), or `undefined` when every declared
|
|
54
|
+
* price is a finite, non-negative number.
|
|
55
|
+
*
|
|
56
|
+
* {@link isModelPriced} answers "does a table EXIST", which is the RB-368 question. It is deliberately not
|
|
57
|
+
* the same question as "can this table price anything": a `NaN` rate silently computes a cost of ZERO (the
|
|
58
|
+
* `price && tokens` guard treats NaN as absent) and a negative rate computes a negative one, so a table of
|
|
59
|
+
* garbage looks priced and prices everything at nothing. Wherever a number from this table becomes a
|
|
60
|
+
* CEILING rather than a report, the caller must ask this too — a bad value has to be loud, never folded
|
|
61
|
+
* into a silent default.
|
|
62
|
+
*/
|
|
63
|
+
export declare function malformedPricingField(pricing: ModelPricing): string | undefined;
|
|
40
64
|
/** Map a vendor `Model.cost` (already per-1M absolute) to `ModelPricing`. We don't emit 1h cache
|
|
41
65
|
* writes, so `cacheWriteLongPer1M` defaults to the 5-min write price (irrelevant while that count is 0). */
|
|
42
66
|
export declare function modelCostToPricing(cost: {
|
package/dist/core/pricing.js
CHANGED
|
@@ -11,6 +11,24 @@ export function computeCostMicroUsd(counts, pricing) {
|
|
|
11
11
|
per1M(counts.outputTokens, pricing.outputPer1M);
|
|
12
12
|
return Math.round(usd * 1e6);
|
|
13
13
|
}
|
|
14
|
+
export function isModelPriced(model, pricing) {
|
|
15
|
+
return pricing?.[model.id] !== undefined || model.cost !== undefined;
|
|
16
|
+
}
|
|
17
|
+
export function malformedPricingField(pricing) {
|
|
18
|
+
for (const f of ["inputPer1M", "outputPer1M"]) {
|
|
19
|
+
const v = pricing[f];
|
|
20
|
+
if (typeof v !== "number" || !Number.isFinite(v) || v < 0)
|
|
21
|
+
return f;
|
|
22
|
+
}
|
|
23
|
+
for (const f of ["cacheReadPer1M", "cacheWritePer1M", "cacheWriteLongPer1M"]) {
|
|
24
|
+
const v = pricing[f];
|
|
25
|
+
if (v === undefined)
|
|
26
|
+
continue;
|
|
27
|
+
if (typeof v !== "number" || !Number.isFinite(v) || v < 0)
|
|
28
|
+
return f;
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
14
32
|
export function modelCostToPricing(cost) {
|
|
15
33
|
return {
|
|
16
34
|
inputPer1M: cost?.input ?? 0,
|
|
@@ -46,6 +46,40 @@ export declare function limitConfigError(code: string, message: string): Error &
|
|
|
46
46
|
* sentinel, and on the budget axes it is an exhausted window (absurd but honest).
|
|
47
47
|
*/
|
|
48
48
|
export declare function resolveTaskLimits(limits: TaskLimits | undefined): TaskLimits | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* RB-318 — validate the MODE-VALUED members of `TaskSpec.attachments` at the same door, and return the
|
|
51
|
+
* config unchanged. Today that is exactly one member, `totalTokensReminderMode`.
|
|
52
|
+
*
|
|
53
|
+
* WHY A DOOR AT ALL (坏值响亮度律): a knob whose bad value silently folds to the default is a knob the
|
|
54
|
+
* deployment believes is armed. `totalTokensReminderMode: "padded_countdown"` (underscore) or
|
|
55
|
+
* `"Countdown"` are the near-misses a hand-written config produces, and both would otherwise read as
|
|
56
|
+
* "absent" and take the default arm — a DIFFERENT readout than the one that was asked for, with nothing
|
|
57
|
+
* anywhere saying so.
|
|
58
|
+
*
|
|
59
|
+
* DELIBERATELY NARROW, twice over:
|
|
60
|
+
* - no unknown-KEY refusal (the `resolveTaskLimits` half that has no counterpart here): a wire caller
|
|
61
|
+
* that sends an attachment flag this engine does not know is asking for a lane that stays off, which
|
|
62
|
+
* is the family's own default posture — not a ceiling silently disarmed. Widening it would also be a
|
|
63
|
+
* behavior change for every existing caller, which this additive lane does not license.
|
|
64
|
+
* - the pre-existing `todoReminderMode` is NOT validated here. It has the same shape and the same
|
|
65
|
+
* weakness, but tightening it would refuse specs that run today; recorded as a follow-up rather than
|
|
66
|
+
* folded into this lane's change.
|
|
67
|
+
*
|
|
68
|
+
* A NON-OBJECT CONTAINER (`attachments: null` off a wire, above all) is neither refused nor crashed on
|
|
69
|
+
* (review finding, verified by reading): every consumer reads this config through `attachmentsCfg?.…`,
|
|
70
|
+
* so a null container already means "no lanes are wired" and the spec RUNS today. An unguarded property
|
|
71
|
+
* read here — the first cut — turned that running spec into an untyped `TypeError` from inside a door
|
|
72
|
+
* that promises a typed `config.attachment_invalid`, and refusing it instead would newly reject a spec
|
|
73
|
+
* that works. There is no container-shape door in this family yet; that is a separate widening, not
|
|
74
|
+
* something this lane gets to invent on the way past.
|
|
75
|
+
*
|
|
76
|
+
* The bypass is narrowed to values that CANNOT supply attachment fields (round-2 review finding): a
|
|
77
|
+
* plain-JS caller can hand this a FUNCTION carrying `totalTokensReminderMode` as a property, and every
|
|
78
|
+
* downstream `attachmentsCfg?.…` read would happily find it — so skipping a function here would fold a
|
|
79
|
+
* typo'd mode to the default through exactly the hole this door exists to close. Functions are
|
|
80
|
+
* property-bearing containers and are validated like objects; only `null` and primitives pass through.
|
|
81
|
+
*/
|
|
82
|
+
export declare function resolveAttachmentsConfig(attachments: TaskSpec["attachments"]): TaskSpec["attachments"];
|
|
49
83
|
/**
|
|
50
84
|
* R2 双形轴(追加令 2026-07-18): CC 2.1.212's fable-variant prompt gate (b9e —
|
|
51
85
|
* `fable_5_mitigations` capability / claude-mythos-5), ORTHOGONAL to the simple/classic profile.
|
|
@@ -124,7 +158,7 @@ export interface PrepareConfigDoorsResult {
|
|
|
124
158
|
* for the door + placement reasoning). The per-run state builder consumes exactly these values —
|
|
125
159
|
* the deps bag is never re-read after this door. */
|
|
126
160
|
microCompactKnob: {
|
|
127
|
-
machine: ContextEditMachine;
|
|
161
|
+
machine: "off" | ContextEditMachine;
|
|
128
162
|
clearOnRejection: boolean;
|
|
129
163
|
};
|
|
130
164
|
/** owned — the resolved role; its `systemPrompt` seat is still read at prompt-input time. */
|
|
@@ -182,7 +216,7 @@ export interface PrepareConfigDoorsResult {
|
|
|
182
216
|
* it must refuse exactly like a malformed field, never fold to the defaults. The RESOLVED values
|
|
183
217
|
* returned here are immutable and are the state builder's ONLY source — nothing re-reads the bag. */
|
|
184
218
|
export declare function resolveMicroCompactKnob(bag: RunnerDeps["microCompact"]): {
|
|
185
|
-
machine: ContextEditMachine;
|
|
219
|
+
machine: "off" | ContextEditMachine;
|
|
186
220
|
clearOnRejection: boolean;
|
|
187
221
|
};
|
|
188
222
|
export declare function prepareConfigDoors(input: PrepareConfigDoorsInput): PrepareConfigDoorsResult;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { resolveBrainCallGuardrailMs } from "../../brain/timeout.js";
|
|
2
2
|
import { assertReadFaceValue } from "../../tools/fs/index.js";
|
|
3
3
|
import { resolveCheckpointStore } from "../checkpoint-store.js";
|
|
4
|
+
import { isModelPriced, malformedPricingField, modelCostToPricing } from "../pricing.js";
|
|
4
5
|
import { preflightLockedConfig } from "../locked-config.js";
|
|
5
6
|
import { assertRetentionCapability } from "../retention.js";
|
|
6
7
|
import { resolveModel, resolveTaskModel, roleModelIfSet } from "../roles.js";
|
|
7
8
|
import { applyToolModelGate, assertRestoreGatedToolsValue, modelIdTail } from "../tool-model-gate.js";
|
|
8
9
|
import { resolveUsageWindows } from "../usage-window-store.js";
|
|
9
10
|
import { deriveAskEffective, resolveAskSeamForm, resolveQuestionSeam } from "../wiring-manifest.js";
|
|
11
|
+
import { TOTAL_TOKENS_REMINDER_MODES } from "./turn-attachments.js";
|
|
10
12
|
const TASK_LIMIT_KEY_DICT = {
|
|
11
13
|
maxTokens: true,
|
|
12
14
|
maxCostUsd: true,
|
|
@@ -31,6 +33,16 @@ export function limitConfigError(code, message) {
|
|
|
31
33
|
e.code = code;
|
|
32
34
|
return e;
|
|
33
35
|
}
|
|
36
|
+
function describeRejectedValue(value) {
|
|
37
|
+
try {
|
|
38
|
+
const quoted = JSON.stringify(value);
|
|
39
|
+
if (quoted !== undefined)
|
|
40
|
+
return quoted;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
}
|
|
44
|
+
return `a ${typeof value} value`;
|
|
45
|
+
}
|
|
34
46
|
export function resolveTaskLimits(limits) {
|
|
35
47
|
if (limits === undefined)
|
|
36
48
|
return undefined;
|
|
@@ -73,6 +85,19 @@ export function resolveTaskLimits(limits) {
|
|
|
73
85
|
}
|
|
74
86
|
return limits;
|
|
75
87
|
}
|
|
88
|
+
export function resolveAttachmentsConfig(attachments) {
|
|
89
|
+
if (attachments === null || (typeof attachments !== "object" && typeof attachments !== "function"))
|
|
90
|
+
return attachments;
|
|
91
|
+
const mode = attachments.totalTokensReminderMode;
|
|
92
|
+
if (mode !== undefined) {
|
|
93
|
+
const legal = TOTAL_TOKENS_REMINDER_MODES;
|
|
94
|
+
if (typeof mode !== "string" || !legal.includes(mode)) {
|
|
95
|
+
throw limitConfigError("config.attachment_invalid", `TaskSpec.attachments.totalTokensReminderMode must be one of ${TOTAL_TOKENS_REMINDER_MODES.map((m) => `"${m}"`).join(", ")} ` +
|
|
96
|
+
`(got ${describeRejectedValue(mode)}) — refused rather than folded to the default, which would publish a different readout than the one the deployment asked for.`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return attachments;
|
|
100
|
+
}
|
|
76
101
|
export function isFableFamilyModelId(id) {
|
|
77
102
|
const tail = modelIdTail(id);
|
|
78
103
|
return /^claude-fable-\d/.test(tail) || /^claude-mythos-5(?!\d)/.test(tail);
|
|
@@ -98,8 +123,8 @@ function microCompactConfigError(field, value, legal) {
|
|
|
98
123
|
}
|
|
99
124
|
const seat = field === undefined ? "microCompact" : `microCompact.${field}`;
|
|
100
125
|
const e = new Error(`${seat} ${shown} is not ${legal} — an unevaluable declaration is refused loudly, ` +
|
|
101
|
-
`never folded to the default: a silently-ignored
|
|
102
|
-
`deployment believes
|
|
126
|
+
`never folded to the default: a silently-ignored declaration would run the DEFAULT machine ` +
|
|
127
|
+
`while the deployment believes its opt-in or opt-out took effect.`);
|
|
103
128
|
e.code = "config.microcompact_invalid";
|
|
104
129
|
return e;
|
|
105
130
|
}
|
|
@@ -108,14 +133,17 @@ export function resolveMicroCompactKnob(bag) {
|
|
|
108
133
|
throw microCompactConfigError(undefined, bag, "an object carrying optional machine/clearOnRejection keys");
|
|
109
134
|
}
|
|
110
135
|
const declaredMachine = bag?.machine;
|
|
111
|
-
if (declaredMachine !== undefined && declaredMachine !== "legacy" && declaredMachine !== "cc") {
|
|
112
|
-
throw microCompactConfigError("machine", declaredMachine, `"legacy" | "cc"`);
|
|
136
|
+
if (declaredMachine !== undefined && declaredMachine !== "off" && declaredMachine !== "legacy" && declaredMachine !== "cc") {
|
|
137
|
+
throw microCompactConfigError("machine", declaredMachine, `"off" | "legacy" | "cc"`);
|
|
113
138
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
139
|
+
return { machine: declaredMachine ?? "cc", clearOnRejection: resolveClearOnRejection(bag) };
|
|
140
|
+
}
|
|
141
|
+
function resolveClearOnRejection(bag) {
|
|
142
|
+
const declared = bag?.clearOnRejection;
|
|
143
|
+
if (declared !== undefined && typeof declared !== "boolean") {
|
|
144
|
+
throw microCompactConfigError("clearOnRejection", declared, "a boolean");
|
|
117
145
|
}
|
|
118
|
-
return
|
|
146
|
+
return declared ?? true;
|
|
119
147
|
}
|
|
120
148
|
export function prepareConfigDoors(input) {
|
|
121
149
|
const { deps, sessions, resume, internals } = input;
|
|
@@ -290,6 +318,7 @@ export function prepareConfigDoors(input) {
|
|
|
290
318
|
}
|
|
291
319
|
}
|
|
292
320
|
resolveTaskLimits(spec.limits);
|
|
321
|
+
resolveAttachmentsConfig(spec.attachments);
|
|
293
322
|
if (spec.resourceSuspend !== undefined) {
|
|
294
323
|
const rsus = spec.resourceSuspend;
|
|
295
324
|
if (typeof rsus.scope !== "string" || rsus.scope === "") {
|
|
@@ -327,6 +356,35 @@ export function prepareConfigDoors(input) {
|
|
|
327
356
|
}
|
|
328
357
|
const resolvedRole = resolveTaskModel(spec, deps);
|
|
329
358
|
const model = resolvedRole.model;
|
|
359
|
+
if (usageWindows !== undefined && deps.usageWindowStore !== undefined) {
|
|
360
|
+
const costed = usageWindows.find((w) => w.maxCostUsd !== undefined);
|
|
361
|
+
const why = costed === undefined
|
|
362
|
+
? undefined
|
|
363
|
+
: !isModelPriced(model, deps.pricing)
|
|
364
|
+
? `model ${JSON.stringify(model.id)} is unpriced — it has neither a RunnerDeps.pricing entry nor a Model.cost declaration, so this run produces NO cost figure to charge the window with (an unpriced run has no cost, which is not the same as a cost of 0). Price the model, or drop maxCostUsd from the window.`
|
|
365
|
+
: (() => {
|
|
366
|
+
const bad = malformedPricingField(deps.pricing?.[model.id] ?? modelCostToPricing(model.cost));
|
|
367
|
+
return bad === undefined
|
|
368
|
+
? undefined
|
|
369
|
+
: `the price table for model ${JSON.stringify(model.id)} declares an unevaluable ${bad} — every rate must be a finite, non-negative number of USD per 1M tokens. A table like this prices every turn at zero (or below), so the ceiling would never fill: refused rather than enforced in name only.`;
|
|
370
|
+
})();
|
|
371
|
+
if (costed !== undefined && why !== undefined) {
|
|
372
|
+
const e = new Error(`RunnerDeps.usageWindows declares a cost ceiling (maxCostUsd ${String(costed.maxCostUsd)} over ${String(costed.windowMs)}ms, ${costed.anchor}) but ${why}`);
|
|
373
|
+
e.code = "config.usage_window_unpriced";
|
|
374
|
+
throw e;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
const dollarCeiling = spec.limits?.maxCostUsd !== undefined
|
|
378
|
+
? "limits.maxCostUsd"
|
|
379
|
+
: spec.resourceSuspend?.totalBudgetUsd !== undefined
|
|
380
|
+
? "resourceSuspend.totalBudgetUsd"
|
|
381
|
+
: undefined;
|
|
382
|
+
if (dollarCeiling !== undefined && isModelPriced(model, deps.pricing)) {
|
|
383
|
+
const bad = malformedPricingField(deps.pricing?.[model.id] ?? modelCostToPricing(model.cost));
|
|
384
|
+
if (bad !== undefined) {
|
|
385
|
+
throw limitConfigError("config.limit_invalid", `TaskSpec.${dollarCeiling} is a money ceiling, but the price table for model ${JSON.stringify(model.id)} declares an unevaluable ${bad} — every rate must be a finite, non-negative number of USD per 1M tokens. A table like this prices every turn at zero (or below), so the ceiling would never trip: refused rather than enforced in name only.`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
330
388
|
const fableMitigations = resolveModelPromptTraits(model, spec, internals).fableMitigations;
|
|
331
389
|
const gateDecision = applyToolModelGate({
|
|
332
390
|
tools: spec.tools,
|