@sema-agent/core 7.11.0 → 7.11.1

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 CHANGED
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## 7.11.1 — 2026-09-09
4
+
5
+ ### Fixes — a read the DEPLOYMENT'S READ BOUNDARY demoted is a mandated ask; the read-only shell arm cannot clear it (B-057; security axis; @server 7.69.0 six cells @test @cli)
6
+ - **The regression (7.11.0 #619).** `bashReversibilityProbe` minted `mandated` only for an operand OUTSIDE the roots; a deny-listed operand (`grep needle secrets/app.txt` under `readDenyPatterns:["**/secrets/**"]`) and a recursive walk under a wired deny judge (`grep -r needle sub`, `du`) came back as a bare `reversible:false` — an "ordinary" classify-tier ask — and the new read-only arm (a reader of command text, blind to paths) retired it and the command ran, with `decisionReason:"read_only"`. Fail-open; measured red first.
7
+ - **The rule, ONE arm (`boundaryGate`):** a demotion the read boundary raised is structural — outside the roots, on the deny judge (new structured `CompoundReadonlyVerdict.readDenied`, stamped where the deny arm demotes), or a recursive walk under the deny judge — and neither a stored allow rule nor the read-only arm may retire it (`probe_mandate`, the same word #502 minted). Rules do not grow: the mandate's trigger widens from "outside the roots" to "the boundary spoke". No wire change; `ReversibilityVerdict.mandated` unchanged in shape.
8
+ - The same rule covers a recursion the classifier cannot bound even with NO deny pattern: `find . -name x && pwd` under `shellGate:"classify"` asks once again (the 7.11.0 reading that let the read-only arm clear it was the same fail-open, pinned in `test/backlog482-compound-readonly-e2e.test.ts`).
9
+ - Observable: with `readFace:"roots"`, `shellGate:"classify"` and a deny pattern, those three commands ask ONCE (`origin:"shell_gate_tighten"`), no `permission.read_only_allowed` trace, and run only after the person answers; with no pattern (or one that matches nothing) the same readers are reversible at the fold and ask nothing. Pins: `test/backlog-b057-read-boundary-mandate.test.ts`.
10
+
11
+ ### Fixes — a `spec.tools` entry the ToolSpec arm cannot rebuild is refused by name with a stable code (#666 ③; test [6794] G4.b; @server @test)
12
+ - The caller mount rebuilds a raw ToolSpec by spreading it, so an entry whose `name` / `execute` live on a prototype (a class instance, `Object.create(spec)`, `Object.create(product)`), a function carrying the brand, or a Proxy lying about its own keys mounted with `name: undefined` and failed on an unrelated roster read (`undefined.startsWith`). ONE predicate — can the spec arm construct this entry (own enumerable string `name` + own function `execute`)? — now refuses the whole family with terminal code **`config.tool_mount_denied`** (registered, terminal) and a remedy sentence shared with the rebind seal (plain-object ToolSpec / the product itself / the supported wrapper form via `stampDefineToolBrand`). The 7.11.0-era brand-inheritance arm (`inheritsDefineToolBrand`) is retired — it closed only the branded subset. Pins: the four shapes through a real Runner + the own-property control.
13
+
14
+ ### Internal — design/393 S4: the TaskStream façade leaves `runtask.ts` as five `stream-*.ts` lanes (byte-invariant; zero behavior surface)
15
+ - **What moved.** `Runner.runTaskStream`'s T3–T7 — the settle backstop (the `run.catch` handler: checkpoint reopen compensation, terminal resume-failure unpin/destroy, the `failed` terminal mint, the backstop `task.end`, the owed delegation terminal, the drained `done` push), the suspended-run reap (`reapSuspended`), and the eight verb closures (`steer`; `notify` / `optOutMemoryCapture` / `compact` / `detach` / `interrupt`; `halt` / `destroy`) — moved whole to `stream-settle-backstop.ts`, `stream-reap.ts`, `stream-steer-verb.ts`, `stream-lifecycle-verbs.ts`, `stream-halt-verbs.ts` (layer 3, run lanes), each a factory behind ONE Input (`Stream…Input` / `Stream…Result`, 43 seats in all) that hands back the closure(s) the stream object carries under the same names. The driver keeps the seats (T1), the run IIFE (T2) and the eleven-name stream object; it mints the lanes' live view (`TaskStreamLiveSeat` — getters over `resultValue` / `handle` / `reapHandle`, one setter for the backstop's mint) and threads the Runner's registries and live deps seat as borrowed seats. Six seat types (`LiveHandle`, `TaskIdRef`, `ManualCompactRef`, `NotifyRef`, `CaptureOptOutRef`, `TaskStreamLiveSeat`) went down to `contracts.ts` from the driver's inline annotations. `steerChain` and `destroyOnce` moved with the only verb that read each.
16
+ - **Byte-invariant.** dist `runtask.js` differs from the previous release by import lines, the live-seat mint, five factory calls and eight property names only (−510 / +34); the five new files are the only additions; no other runtime file changed; the export surface is unchanged. Every await inside a moved closure sits on the tick it did: the lanes return the same functions the driver still installs in the same positions (`run.catch(onRunRejected)`, the stream object's properties), and every pre-await host read (`live.resultValue`, `live.handle`, the options bags) is the same read on the same tick. `runtask.ts` 7 565 → 6 594.
17
+ - **Doors that moved with it.** module-size ratchet (runtask banked; five lane entries; contracts.ts +70 with reason), `gate:phase-api` floor 728 → 771 and the stream- row's pins, `gate:layering` `streamLanesNote`, the run-lane residents pin, the rb466 / fail-open / retired-key per-file lists (+5 each), INTEGRATION-CORE's prepare-throw-mint coordinates. `@server @cli @test`: nothing to pick up — no wire key, no closed set, no export moved.
18
+
19
+ ### Fixes — the auto-mode classifier's reasoning seat: a model that cannot switch thinking off is asked to think `low`, not `off` (#661 ⑤ re-form; B-056; ruling: "关不掉思考的模型,席位思考改 low"; @cli 1.0.106 @test @server)
20
+ - **The ruling, as one predicate.** The classifier request's seat is `thinkingOffExpressible(model) ? { reasoning:"off", maxTokens: 256 } : { reasoning:"low", maxTokens: 256 + 2048 }` — a wire that can say off pays for the verdict alone (unchanged since #618); a wire that cannot (`reasoning:true` on an effort-only wire with no declared off spelling, the anthropic adaptive family) is asked for `low` (the request word; what lands on the wire follows the catalog's level mapping — see the wire note and the residual below) under a FIXED 2304-token output cap. Before, such a model was sent `off` with NO cap and kept its own budget (7.11.0 and earlier) — a request the wire could not honor, paid for open-endedly. A model that spends the whole cap on thinking still answers nothing: that is one `parse_error` per ask, and three of them trip the breaker as before.
21
+ - **The re-ask is the same seat.** 7.11.0's one re-ask of an empty-at-cap reply (text `""` at `stopReason:"length"` / `errorKind:"length_empty"`) stays, but the second request is now the `low` seat (`reasoning:"low"`, 256 + 2048) instead of `off` at the widened cap: a catalog entry that says it does not reason and then answers nothing at the `off` cap has just said it thinks anyway, and is re-asked as a model known to think would have been asked. A seat already on `low` has no second request. Everything else holds: one decision per ask, no new breaker-cause word, a second empty reply / a verdict-free reply / an empty clean stop is the `parse_error` it was.
22
+ - **The constant.** `AUTO_MODE_CLASSIFIER_THINKING_ALLOWANCE_TOKENS` (7.11.0, module-internal) is renamed `AUTO_MODE_CLASSIFIER_LOW_REASONING_BUDGET_TOKENS` = 2048: CC 2.1.250 `E4`'s allowance for its `rejects_disabled_thinking` family, and 2 × `MIN_THINKING_TOKENS` — so on the anthropic BUDGET path a HARD cap of 256 + 2048 clears the cap-wins skip and hosts a legal `low` budget. Still not on the index face.
23
+ - **Wire note (`@test`).** `reasoning:"low"` reaches the wire only where the catalog declares `reasoning:true` (the appliers' entry gate `reasoningRequestCarried`); on a catalog entry with no `reasoning` flag the re-ask's `low` puts no thinking key on the wire — the model thinks at its own default and the widened cap is what makes the re-ask succeed. On a declared-reasoning wire: anthropic budget form `thinking:{type:"enabled", budget_tokens ∈ [1024, 2303]}`, adaptive `output_config.effort:"low"`, openai effort wires `reasoning_effort:"low"` (clamped to the endpoint's declared levels), binary enable wires `enable_thinking:true`.
24
+ - **Known residual (review r1).** "Cannot say off" does not imply `low` reaches the wire: an anthropic adaptive entry with NO `compat.effortLevels` declared sends no `output_config.effort` (the shipped `fable5Model` preset declares them; a hand-written catalog entry may not), and an openai entry with `supportsReasoningEffort:false` or `thinkingLevelMap.low:null` sends no effort value — those models think at their DEFAULT tier inside the 2304 cap, where they used to be uncapped, and the `low` seat is never re-asked. Declare the effort levels / the `low` spelling so the seat's word lands; judging "is `low` spellable" inside the seat would be a second copy of the appliers' per-wire knowledge (`reasoning.ts` is the one source), so the seat reads the one capability predicate the engine has (`thinkingOffExpressible`) and this cell is disclosed rather than special-cased.
25
+ - **Other engine-side seats.** `rg 'reasoning: "off"' src/` finds ONE seat — the classifier's; the titler / summarize / compaction legs do not pin a level (they inherit), so nothing else moves.
26
+ - `@cli` 1.0.106: pick up this version with 7.11.0 — a thinking-locked classifier model is now asked for `low` inside a 2304-token cap instead of being sent an off it ignores (where the catalog maps `low` onto the wire, it thinks at that tier; where it does not, at its default — see the residual); a model that exhausts the cap still fails once per ask; nothing to render. `@test`: known cannot-off (a catalog entry `reasoning:true` on the openai wire with no `thinkingLevelMap.off`, or anthropic adaptive) ⇒ the FIRST classifier request carries `reasoning:"low"` and `max_tokens` 2304, one request; an unknown entry (no `reasoning`) answering empty `finish_reason:"length"` then `<block>no</block>` ⇒ two requests, the second `low` at 2304; a can-off entry (deepseek format / declared off spelling) ⇒ `off` at 256, one request. `@server`: zero surface.
27
+ - Pins: `test/backlog661-classifier-cap-retry.test.ts` (the seat predicate — cannot-off adaptive and stock-openai first request `low` + 2304; can-off `off` + 256 ×3; the re-ask's second request `low`; cannot-off empty ⇒ one request); `test/backlog618-classifier-request-shape.test.ts` re-pinned (the no-cap seat is gone: cannot-off ⇒ `low` + 2304).
28
+
3
29
  ## 7.11.0 — 2026-09-09
4
30
 
5
31
  ### Fixes — the auto-mode classifier reads its verdict the way CC 2.1.250 does, and re-asks once when a capped reply came back empty (#661 ① ⑤; B-056; @cli 1.0.106 @test @server)
@@ -52,7 +78,7 @@
52
78
  - **Widening half — EMPTY** (no allow gets wider: the allow arm cannot be constructed). Pins: `test/backlog657-tailed-bare-path-rule-depth.test.ts`; the #654 file's anchored example moved from `dist/**` to `dist/**/x`.
53
79
 
54
80
  ### Narrowing — a cwd-relative BARE path rule reads at ANY depth, the upstream file-rule (gitignore) anchoring (#654; @cli @server @test)
55
- - **The rule** (`resolvePathPattern`, contract `path_rule.bare_name`): a cwd-relative pattern body with NO `/` — `Edit(.env)`, `Read(secret.txt)`, `Read(*.env)` — names that segment at any depth under the working directory (`<cwd>/**/<body>`); a body carrying a `/` (`dist/**`, `a/b`) stays anchored at cwd; `./x` is the same pattern as `x` (the `./` marker only spells the cwd word); the `~/x` / `/x` / `//x` forms are unchanged (anchored at home / root / absolute). Anchored on CC 2.1.250's matcher: the base-relative pattern is handed to a gitignore engine, so a slash-less pattern matches at every depth. `ruleBasesNeeded` is unchanged — a bare name still names `cwd` (the root stands in when cwd is absent).
81
+ - **The rule** (`resolvePathPattern`, contract `path_rule.bare_name`): a cwd-relative pattern body with NO `/` — `Edit(.env)`, `Read(secret.txt)`, `Read(*.env)` — names that segment at any depth under the working directory (`<cwd>/**/<body>`); a body carrying a `/` BEFORE any `/**` tail (`dist/sub/**`, `a/b`) stays anchored at cwd (erratum, test [6794] G1: a ONE-segment `dist/**` is the peeled form of #657 above and reads at any depth for deny/ask); `./x` is the same pattern as `x` (the `./` marker only spells the cwd word); the `~/x` / `/x` / `//x` forms are unchanged (anchored at home / root / absolute). Anchored on CC 2.1.250's matcher: the base-relative pattern is handed to a gitignore engine, so a slash-less pattern matches at every depth. `ruleBasesNeeded` is unchanged — a bare name still names `cwd` (the root stands in when cwd is absent).
56
82
  - **Narrowing half (deny/ask get WIDER reach = stricter):** a migrated `deny: ["Edit(.env)"]` now refuses `nested/.env` too; a persisted `Read(secret.txt)` deny/ask row reaches every `secret.txt` under the working directory. ONE reader (`pathRuleReachOf`) serves the persisted store lane, the content-rule policy lane and its compile step, so all move together. `@cli`: the `/permissions` editor's breadth wording for slash-less path rules can drop the root-only caveat; `@server`: task-settings content rules ride the same lane, zero code change; `@test`: black-box `deny:["Edit(.env)"]` ⇒ `nested/.env` refused, `Read(*.env)` ask reaches `a/b/prod.env`, `Write(dist/**/x)` still only `<cwd>/dist/…/x` (the `dist/**` tail form is #657's), `./x` ≡ `x`.
57
83
  - **Widening half — EMPTY:** an allow in the pattern form has no rule form on any lane (persisted parse `invalid.grammar` / `unsupported.tool`, policy compile `unsupported.file_glob`; only the `Read(//abs/**)` directory form is a standing approval), so no allow gets wider — pinned.
58
84
  - The `/**`-tailed form (`secrets/**`) is the #657 entry below — the residual this entry once carried is closed there.
@@ -24,16 +24,20 @@ export declare const AUTO_MODE_DEFAULT_WINDOW_MAX_CHARS = 2000;
24
24
  */
25
25
  export declare const AUTO_MODE_CLASSIFIER_MAX_TOKENS = 256;
26
26
  /**
27
- * CC 2.1.250 `E4`: for a model in the `rejects_disabled_thinking` family (`BHe`) the classifier request
28
- * adds 2048 output tokens on top of the verdict cap the thinking ALLOWANCE for a model that reasons
29
- * whether or not it is told to. The engine has no per-model declaration of that fact (a gateway model
30
- * with no `reasoning`/`compat` in the catalog reads as "off is expressible"), so it learns it from the
31
- * reply instead: a capped classify that comes back EMPTY at the cap (the output went to reasoning) is
32
- * re-asked ONCE with the cap raised by this allowance, thinking still off (CC `cEt` re-asks such a
33
- * reply up to `maxRetries` = 4 times at the same cap; one re-ask at the allowance-widened cap is the
34
- * subset the engine spends). A second empty reply is the contract failure it always was.
27
+ * The output-budget ALLOWANCE for a classifier request that must think: added to the verdict cap when the
28
+ * seat asks for `reasoning:"low"` instead of `"off"`a model whose wire cannot say off (`reasoning:true`
29
+ * with no off spelling on an effort-only wire, or the anthropic adaptive family), or an unknown model whose
30
+ * capped off-request came back EMPTY at the cap (it reasoned anyway). The seat then pays for the lowest
31
+ * reasoning tier plus the verdict, never for an open-ended budget.
32
+ *
33
+ * 2048 is anchored twice: CC 2.1.250 `E4` adds exactly this many tokens to the verdict cap for a model in
34
+ * its `rejects_disabled_thinking` family (`BHe`); and it is 2 × `MIN_THINKING_TOKENS` (1024), so on the
35
+ * anthropic BUDGET path a HARD cap of 256 + 2048 clears the cap-wins skip (`budgetCapSkipsThinking`) and
36
+ * hosts a legal `low` budget (`1024 ≤ budget_tokens < max_tokens`) without the brain having to raise a cap
37
+ * it refuses to raise. A model that spends more than this on thinking still answers nothing — that reply
38
+ * is the contract failure it always was.
35
39
  */
36
- export declare const AUTO_MODE_CLASSIFIER_THINKING_ALLOWANCE_TOKENS = 2048;
40
+ export declare const AUTO_MODE_CLASSIFIER_LOW_REASONING_BUDGET_TOKENS = 2048;
37
41
  /** The sentinel a deployment puts INSIDE a paired rule list to splice the CC default rules back in at
38
42
  * that position (CC `XYt = "$defaults"`). It lives here, beside the other defaults, because the #503
39
43
  * recipe canonicalizer needs its VALUE and must not load the assembly face (and its SHA-locked assets)
@@ -3,7 +3,7 @@ export const AUTO_MODE_DEFAULT_FAILURE_THRESHOLD = 3;
3
3
  export const AUTO_MODE_DEFAULT_WINDOW_MAX_ENTRIES = 40;
4
4
  export const AUTO_MODE_DEFAULT_WINDOW_MAX_CHARS = 2_000;
5
5
  export const AUTO_MODE_CLASSIFIER_MAX_TOKENS = 256;
6
- export const AUTO_MODE_CLASSIFIER_THINKING_ALLOWANCE_TOKENS = 2048;
6
+ export const AUTO_MODE_CLASSIFIER_LOW_REASONING_BUDGET_TOKENS = 2048;
7
7
  export const AUTO_MODE_DEFAULTS_SENTINEL = "$defaults";
8
8
  export const AUTO_MODE_DENIAL_LIMIT_DEFAULTS = Object.freeze({ maxConsecutive: 3, maxTotal: 20 });
9
9
  export const AUTO_MODE_DENIAL_AUTO_DENY_DEFAULT_MS = 120_000;
@@ -12,6 +12,7 @@ export declare const GOVERNANCE_CODES: {
12
12
  readonly "config.compliance_required": "transient";
13
13
  readonly "config.compliance_denied": "terminal";
14
14
  readonly "config.locked_key": "terminal";
15
+ readonly "config.tool_mount_denied": "terminal";
15
16
  readonly "memory.capture_optout_denied": "terminal";
16
17
  };
17
18
  export type GovernanceCode = keyof typeof GOVERNANCE_CODES;
@@ -5,6 +5,7 @@ export const GOVERNANCE_CODES = {
5
5
  "config.compliance_required": "transient",
6
6
  "config.compliance_denied": "terminal",
7
7
  "config.locked_key": "terminal",
8
+ "config.tool_mount_denied": "terminal",
8
9
  "memory.capture_optout_denied": "terminal",
9
10
  };
10
11
  export const NON_GOVERNANCE_MEMORY_CODES = new Set([
@@ -2424,3 +2424,73 @@ export interface RunnerSelfSeat {
2424
2424
  /** Streaming form of {@link RunnerSelfSeat.resume}: the pre-CAS guards and the CAS run first, then the live stream is returned. */
2425
2425
  resumeStream(token: CheckpointToken, outcome: ResumeOutcome, taskConfig: ResumeTaskConfig, internals?: RunInternals): Promise<TaskStream>;
2426
2426
  }
2427
+ /** The live-task handle `runLocked` publishes once the harness exists (design/47): the harness + abort
2428
+ * controller, the loop-liveness latch (`ended` flips when the single `harness.prompt` settles; `userInterrupted`
2429
+ * / `userHalted` are the interrupt and halt verbs' attribution seats), the run's reminder mark, its session and
2430
+ * engine-minted run id, and the hook bound + identity envelope the steer entrance screen runs under. */
2431
+ export interface LiveHandle {
2432
+ harness: AgentHarness;
2433
+ abortController: AbortController;
2434
+ loop: {
2435
+ ended: boolean;
2436
+ userInterrupted: boolean;
2437
+ userHalted: boolean;
2438
+ };
2439
+ reminderMark: string;
2440
+ sessionId: string;
2441
+ /** #499 — the run body's engine-minted run id, so the stream-layer verbs can name the run their disclosures are about. */
2442
+ runId: string;
2443
+ hookTimeoutMs: number;
2444
+ hookIdentity: HookInvocationIdentity;
2445
+ }
2446
+ /** The run body's backstop CARRIER (F-05/W8/件①/#327/#499/#281 r2-D2): the effective ids and the post-prepare
2447
+ * observations the run body publishes as they are minted, so the stream layer's failure backstop names the
2448
+ * same run, session and observations the frames the run body already emitted did. */
2449
+ export interface TaskIdRef {
2450
+ current?: string;
2451
+ sessionId?: string;
2452
+ runId?: string;
2453
+ effectiveMemoryScopes?: TaskResult["effectiveMemoryScopes"];
2454
+ effectiveReasoning?: TaskResult["effectiveReasoning"];
2455
+ delegationTerminalOwed?: HookInvocationIdentity;
2456
+ editedFiles?: () => TaskResult["editedFiles"];
2457
+ }
2458
+ /** design/99 MF-18 — the manual `/compact` request seat: the `requested` flag, the parked waiters (each with its
2459
+ * caller's cancel signal and per-call instructions), the run body's mooted-frame channel and the registration
2460
+ * gate the run-end backstop closes before its final drain. */
2461
+ export interface ManualCompactRef {
2462
+ requested: boolean;
2463
+ waiters: Array<{
2464
+ resolve: (outcome: CompactOutcome) => void;
2465
+ signal?: AbortSignal;
2466
+ instructions?: string;
2467
+ }>;
2468
+ emitMooted?: (reason: string) => void;
2469
+ closed?: boolean;
2470
+ }
2471
+ /** design/144 §2 — the `notify()` bridge: `runLocked` binds `inject` the moment the task-notification lane exists. */
2472
+ export interface NotifyRef {
2473
+ inject?: (n: TaskNotificationPayload, opts?: {
2474
+ priority?: SystemInjectionPriority;
2475
+ }) => void;
2476
+ }
2477
+ /** design/383 §2.1 — the capture opt-out flip verb's binding; bound only when the run mounted a memory session. */
2478
+ export interface CaptureOptOutRef {
2479
+ flip?: (reason?: string) => Promise<{
2480
+ outcome: "created" | "existed";
2481
+ }>;
2482
+ }
2483
+ /**
2484
+ * The stream's LIVE state as the verb lanes read it — a view over the driver's own locals (getters, and one
2485
+ * setter), never a copy: `resultValue` is written by the run body's `setResult` callback and by the failure
2486
+ * backstop, `handle` by the run body's `onReady`, `reapHandle` by its `onSuspend`; every lane reads each of them
2487
+ * at the instant of its own read, exactly as the one-function façade read the closure variables.
2488
+ */
2489
+ export interface TaskStreamLiveSeat {
2490
+ /** The assembled result once the run settled (the run body's `setResult`, or the failure backstop's mint). */
2491
+ resultValue: TaskResult | undefined;
2492
+ /** The live-task handle once `runLocked` published it; `undefined` before, and forever when prepare threw. */
2493
+ readonly handle: LiveHandle | undefined;
2494
+ /** design/51 — what `destroy()` reaps when the run SUSPENDED; unset for every other terminal state. */
2495
+ readonly reapHandle: SuspendReap | undefined;
2496
+ }
@@ -7,7 +7,7 @@ import { isSelfOrchestrationActive } from "../../orchestration/workflow-script-r
7
7
  import { resolveWorkflowSizeGuideline } from "../../orchestration/workflow-size-guideline.js";
8
8
  import { thinkingOffExpressible } from "../../brain/reasoning.js";
9
9
  import { autoModeArmingRecipeOf } from "../auto-mode-arming.js";
10
- import { AUTO_MODE_CLASSIFIER_MAX_TOKENS, AUTO_MODE_CLASSIFIER_THINKING_ALLOWANCE_TOKENS } from "../auto-mode-defaults.js";
10
+ import { AUTO_MODE_CLASSIFIER_LOW_REASONING_BUDGET_TOKENS, AUTO_MODE_CLASSIFIER_MAX_TOKENS } from "../auto-mode-defaults.js";
11
11
  import { buildAutoModePrompt, renderAutoModeAction, renderAutoModeWindow } from "../auto-mode-prompt.js";
12
12
  import { createAutoModeDecider, createAutoModeDenialTracker } from "../auto-mode.js";
13
13
  import { resolveCheckpointStore } from "../checkpoint-store.js";
@@ -18,6 +18,7 @@ import { brainToRuntime } from "../runtime.js";
18
18
  import { defaultTaskRegistry } from "../task-registry.js";
19
19
  import { emitTrace } from "../trace.js";
20
20
  import { defineTool, isDefineToolProduct, rebindDefineToolCtx } from "../tools.js";
21
+ const MOUNT_REMEDY = "author it as a plain-object ToolSpec (own `name` and `execute`), hand the defineTool product itself, or wrap a product through the supported wrapper form (a fresh object re-stamped with `stampDefineToolBrand` whose rebind re-wraps the rebound product)";
21
22
  import { derivedRouteFallsBack } from "./derived-route-fallback.js";
22
23
  import { REPORT_FINDINGS_TOOL_NAME, createReportBlockedTool, createReportFindingsTool } from "./synthetic-tools.js";
23
24
  function classifierReplyText(reply) {
@@ -82,9 +83,22 @@ export async function prepareCapsAndWorkflow(input) {
82
83
  const mounted = mountCallerTool(t);
83
84
  roster.mount(mounted, { source: callerMountSource(mounted), mountedBy: "scenario" });
84
85
  }
86
+ function specArmCanConstruct(t) {
87
+ if (typeof t !== "object" || t === null)
88
+ return false;
89
+ const name = Object.getOwnPropertyDescriptor(t, "name");
90
+ const execute = Object.getOwnPropertyDescriptor(t, "execute");
91
+ return name !== undefined && "value" in name && typeof name.value === "string" && name.enumerable === true && execute !== undefined && "value" in execute && typeof execute.value === "function" && execute.enumerable === true;
92
+ }
85
93
  function mountCallerTool(t) {
86
94
  if (isDefineToolProduct(t))
87
95
  return maybeOffload(rebindDefineToolCtx(t, enrichSpecToolCtx), t);
96
+ if (!specArmCanConstruct(t)) {
97
+ const shown = Object.getOwnPropertyDescriptor(t, "name")?.value;
98
+ const e = new Error(`spec.tools entry ${typeof shown === "string" ? JSON.stringify(shown) : "(no own name)"}: the ToolSpec arm rebuilds a caller tool from its OWN enumerable members, and this entry does not carry a string \`name\` and a function \`execute\` as own properties (a class instance, a prototype child of a spec or of a defineTool product, a function, or a Proxy over one) — ${MOUNT_REMEDY}`);
99
+ e.code = "config.tool_mount_denied";
100
+ throw e;
101
+ }
88
102
  return maybeOffload(defineTool({
89
103
  ...t,
90
104
  execute: (args, ctx) => t.execute(args, enrichSpecToolCtx(ctx)),
@@ -175,7 +189,9 @@ export async function prepareCapsAndWorkflow(input) {
175
189
  const classifierLaneRule = peerLaneActive && peerSendMessageBuiltIn;
176
190
  const classifierSystemPrompt = buildAutoModePrompt(classifierLaneRule ? { ...am, crossSessionMessagesRule: CROSS_SESSION_CLASSIFIER_RULE } : am);
177
191
  const classifierRuntime = brainToRuntime(deps.brain);
178
- const classifierCap = thinkingOffExpressible(classifierModel) ? { maxTokens: AUTO_MODE_CLASSIFIER_MAX_TOKENS } : {};
192
+ const classifierOffSeat = { reasoning: "off", maxTokens: AUTO_MODE_CLASSIFIER_MAX_TOKENS };
193
+ const classifierLowSeat = { reasoning: "low", maxTokens: AUTO_MODE_CLASSIFIER_MAX_TOKENS + AUTO_MODE_CLASSIFIER_LOW_REASONING_BUDGET_TOKENS };
194
+ const classifierSeat = thinkingOffExpressible(classifierModel) ? classifierOffSeat : classifierLowSeat;
179
195
  autoModeDenialTracking = createAutoModeDenialTracker(am.denialLimit);
180
196
  autoModeDecider = createAutoModeDecider({
181
197
  ...(am.timeoutMs !== undefined ? { timeoutMs: am.timeoutMs } : {}),
@@ -201,16 +217,15 @@ export async function prepareCapsAndWorkflow(input) {
201
217
  (m.role === "user" || m.role === "assistant" || m.role === "toolResult"));
202
218
  const userPrompt = renderAutoModeWindow(known, am.window) + renderAutoModeAction(input);
203
219
  const classifierAuth = await spec.getApiKeyAndHeaders?.(classifierModel);
204
- const request = (cap) => classifierRuntime.completeSimple(classifierModel, { systemPrompt: classifierSystemPrompt, messages: [{ role: "user", content: userPrompt, timestamp: Date.now() }] }, {
220
+ const request = (seat) => classifierRuntime.completeSimple(classifierModel, { systemPrompt: classifierSystemPrompt, messages: [{ role: "user", content: userPrompt, timestamp: Date.now() }] }, {
205
221
  signal,
206
- reasoning: "off",
207
- ...cap,
222
+ ...seat,
208
223
  ...(classifierAuth?.apiKey !== undefined ? { apiKey: classifierAuth.apiKey } : {}),
209
224
  ...(classifierAuth?.headers !== undefined ? { headers: classifierAuth.headers } : {}),
210
225
  });
211
- let response = await request(classifierCap);
212
- if (classifierCap.maxTokens !== undefined && classifierReplyEmptyAtCap(response)) {
213
- response = await request({ maxTokens: classifierCap.maxTokens + AUTO_MODE_CLASSIFIER_THINKING_ALLOWANCE_TOKENS });
226
+ let response = await request(classifierSeat);
227
+ if (classifierSeat === classifierOffSeat && classifierReplyEmptyAtCap(response)) {
228
+ response = await request(classifierLowSeat);
214
229
  }
215
230
  return classifierReplyText(response);
216
231
  },