@sema-agent/core 7.17.6 → 7.18.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.
Files changed (164) hide show
  1. package/CHANGELOG.md +165 -13
  2. package/dist/agents/send-message-tool.js +1 -0
  3. package/dist/agents/subagent.d.ts +1 -0
  4. package/dist/agents/subagent.js +5 -0
  5. package/dist/agents/teacher.js +1 -0
  6. package/dist/agents/verify.js +3 -0
  7. package/dist/brain/route-adjudicator.d.ts +18 -0
  8. package/dist/brain/route-adjudicator.js +16 -0
  9. package/dist/core/agent-definition.d.ts +10 -0
  10. package/dist/core/artifact-host.d.ts +41 -4
  11. package/dist/core/artifact-host.js +18 -2
  12. package/dist/core/auto-mode.d.ts +141 -45
  13. package/dist/core/auto-mode.js +109 -3
  14. package/dist/core/checkpoint-store.d.ts +16 -2
  15. package/dist/core/engine-notice.d.ts +47 -1
  16. package/dist/core/engine-notice.js +7 -0
  17. package/dist/core/gate-lanes.js +2 -1
  18. package/dist/core/gate-outcome.d.ts +17 -2
  19. package/dist/core/gate-outcome.js +16 -0
  20. package/dist/core/governance-codes.d.ts +1 -1
  21. package/dist/core/governance-codes.js +8 -0
  22. package/dist/core/hook-seats.d.ts +66 -0
  23. package/dist/core/hook-seats.js +37 -0
  24. package/dist/core/runner/advertised-writable-dirs.d.ts +8 -3
  25. package/dist/core/runner/assemble-result.d.ts +2 -1
  26. package/dist/core/runner/assemble-result.js +1 -1
  27. package/dist/core/runner/classifier-ladder.d.ts +89 -0
  28. package/dist/core/runner/classifier-ladder.js +65 -0
  29. package/dist/core/runner/contracts.d.ts +69 -13
  30. package/dist/core/runner/decide-continuation.d.ts +1 -3
  31. package/dist/core/runner/decide-continuation.js +1 -7
  32. package/dist/core/runner/deferral-census.d.ts +52 -0
  33. package/dist/core/runner/deferral-census.js +44 -0
  34. package/dist/core/runner/derived-route-fallback.d.ts +8 -4
  35. package/dist/core/runner/derived-route-fallback.js +8 -3
  36. package/dist/core/runner/gate-exit.d.ts +12 -0
  37. package/dist/core/runner/gate-exit.js +6 -3
  38. package/dist/core/runner/prepare-artifact.d.ts +10 -2
  39. package/dist/core/runner/prepare-artifact.js +5 -4
  40. package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -2
  41. package/dist/core/runner/prepare-caps-and-workflow.js +30 -31
  42. package/dist/core/runner/prepare-config-doors.d.ts +1 -1
  43. package/dist/core/runner/prepare-config-doors.js +5 -3
  44. package/dist/core/runner/prepare-defer-classify.d.ts +13 -19
  45. package/dist/core/runner/prepare-defer-classify.js +8 -3
  46. package/dist/core/runner/prepare-gate-stations.d.ts +26 -0
  47. package/dist/core/runner/prepare-gate-stations.js +1 -1
  48. package/dist/core/runner/prepare-hands-readface.d.ts +24 -7
  49. package/dist/core/runner/prepare-hands-readface.js +60 -24
  50. package/dist/core/runner/prepare-inherited-gate.d.ts +17 -4
  51. package/dist/core/runner/prepare-inherited-gate.js +10 -2
  52. package/dist/core/runner/prepare-lsp.d.ts +2 -2
  53. package/dist/core/runner/prepare-lsp.js +1 -1
  54. package/dist/core/runner/prepare-policy-chain.js +2 -0
  55. package/dist/core/runner/prepare-project-context.js +4 -2
  56. package/dist/core/runner/prepare-prompt-inputs.d.ts +4 -2
  57. package/dist/core/runner/prepare-prompt-inputs.js +4 -1
  58. package/dist/core/runner/prepare-run-refs.d.ts +9 -1
  59. package/dist/core/runner/prepare-run-refs.js +3 -2
  60. package/dist/core/runner/prepare-suspend-saga.d.ts +5 -0
  61. package/dist/core/runner/prepare-suspend-saga.js +5 -2
  62. package/dist/core/runner/prepare-task.js +18 -13
  63. package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +5 -0
  64. package/dist/core/runner/prepare-tool-disclosure-mount.js +10 -2
  65. package/dist/core/runner/prepare-turn-wiring.d.ts +8 -4
  66. package/dist/core/runner/prepare-turn-wiring.js +3 -2
  67. package/dist/core/runner/prepare-wiring-manifest.d.ts +5 -5
  68. package/dist/core/runner/prepare-wiring-manifest.js +5 -2
  69. package/dist/core/runner/resume-checkpoint-screen.js +7 -0
  70. package/dist/core/runner/run-harness-handlers.js +4 -1
  71. package/dist/core/runner/run-identity-wiring.js +6 -2
  72. package/dist/core/runner/run-leg.d.ts +2 -0
  73. package/dist/core/runner/run-leg.js +35 -41
  74. package/dist/core/runner/run-notification-lane.js +55 -27
  75. package/dist/core/runner/run-settle-and-teardown.js +1 -0
  76. package/dist/core/runner/run-terminal-adoption.js +16 -13
  77. package/dist/core/runner/run-turn-boundary.js +3 -1
  78. package/dist/core/runner/runtask.d.ts +6 -5
  79. package/dist/core/runner/runtask.js +26 -14
  80. package/dist/core/runner/skill-base-dir.d.ts +54 -0
  81. package/dist/core/runner/skill-base-dir.js +42 -0
  82. package/dist/core/runner/synthetic-tools.d.ts +16 -19
  83. package/dist/core/runner/synthetic-tools.js +18 -10
  84. package/dist/core/runner/tool-defer-gate.d.ts +22 -9
  85. package/dist/core/runner/tool-defer-gate.js +15 -5
  86. package/dist/core/runner/tool-disclosure.d.ts +5 -0
  87. package/dist/core/runner/tool-disclosure.js +2 -0
  88. package/dist/core/runner/tool-progress-lane.d.ts +42 -0
  89. package/dist/core/runner/tool-progress-lane.js +61 -0
  90. package/dist/core/runner-deps.d.ts +48 -7
  91. package/dist/core/swappable-deps.d.ts +9 -7
  92. package/dist/core/swappable-deps.js +7 -3
  93. package/dist/core/task-event.d.ts +96 -29
  94. package/dist/core/task-notification.d.ts +142 -16
  95. package/dist/core/task-notification.js +106 -0
  96. package/dist/core/task-result.d.ts +9 -1
  97. package/dist/core/task-spec.d.ts +20 -3
  98. package/dist/core/tighten-task-spec.js +7 -5
  99. package/dist/core/tool-model-gate.d.ts +1 -1
  100. package/dist/core/tool-search-setting.d.ts +78 -0
  101. package/dist/core/tool-search-setting.js +90 -0
  102. package/dist/core/tool-spec.d.ts +8 -0
  103. package/dist/core/types.d.ts +1 -1
  104. package/dist/core/types.js +1 -1
  105. package/dist/core/wiring-manifest.d.ts +46 -0
  106. package/dist/core/wiring-manifest.js +9 -1
  107. package/dist/engine/execution-env/node-execution-env.js +10 -4
  108. package/dist/engine/harness/agent-harness.d.ts +42 -0
  109. package/dist/engine/harness/agent-harness.js +61 -29
  110. package/dist/engine/loop/agent-loop.d.ts +68 -3
  111. package/dist/engine/loop/agent-loop.js +109 -16
  112. package/dist/index.d.ts +10 -5
  113. package/dist/index.js +9 -5
  114. package/dist/internal/harness.d.ts +1 -1
  115. package/dist/orchestration/run-workflow-tool.d.ts +3 -0
  116. package/dist/orchestration/run-workflow-tool.js +6 -1
  117. package/dist/orchestration/workflow-governance.d.ts +7 -1
  118. package/dist/orchestration/workflow-governance.js +9 -5
  119. package/dist/orchestration/workflow-primitives.d.ts +3 -0
  120. package/dist/orchestration/workflow-primitives.js +6 -1
  121. package/dist/orchestration/workflow-types.d.ts +48 -13
  122. package/dist/orchestration/workflow-types.js +1 -1
  123. package/dist/orchestration/workflow.js +45 -17
  124. package/dist/prompt-assembly/epoch.js +2 -0
  125. package/dist/prompt-assembly/packs/sema-default.js +2 -2
  126. package/dist/prompt-assembly/section-usage.d.ts +26 -0
  127. package/dist/prompt-assembly/section-usage.js +10 -0
  128. package/dist/prompt-assembly/types.d.ts +8 -6
  129. package/dist/prompts/default.d.ts +13 -0
  130. package/dist/prompts/default.js +16 -8
  131. package/dist/tools/artifact/artifact-text.d.ts +10 -1
  132. package/dist/tools/artifact/artifact-text.js +6 -3
  133. package/dist/tools/artifact/artifact-tool.d.ts +12 -2
  134. package/dist/tools/artifact/artifact-tool.js +203 -70
  135. package/dist/tools/artifact/conformance.d.ts +56 -0
  136. package/dist/tools/artifact/conformance.js +349 -0
  137. package/dist/tools/artifact/local-stub.js +1 -1
  138. package/dist/tools/artifact/reject-breaker.d.ts +28 -0
  139. package/dist/tools/artifact/reject-breaker.js +42 -0
  140. package/dist/tools/fs/bash-lexer.d.ts +132 -0
  141. package/dist/tools/fs/bash-lexer.js +563 -0
  142. package/dist/tools/fs/bash-program-position.d.ts +142 -0
  143. package/dist/tools/fs/bash-program-position.js +125 -0
  144. package/dist/tools/fs/bash-readonly-classifier.d.ts +54 -16
  145. package/dist/tools/fs/bash-readonly-classifier.js +375 -530
  146. package/dist/tools/fs/containment-face.d.ts +89 -0
  147. package/dist/tools/fs/containment-face.js +81 -0
  148. package/dist/tools/fs/fs-bash.d.ts +46 -1
  149. package/dist/tools/fs/fs-bash.js +158 -22
  150. package/dist/tools/fs/fs-read.d.ts +1 -1
  151. package/dist/tools/fs/fs-search-tools.d.ts +3 -3
  152. package/dist/tools/fs/fs-write.d.ts +4 -3
  153. package/dist/tools/fs/fs-write.js +9 -9
  154. package/dist/tools/fs/index.d.ts +24 -5
  155. package/dist/tools/fs/index.js +18 -6
  156. package/dist/tools/fs/repo-map.d.ts +2 -2
  157. package/dist/tools/fs/safety.d.ts +3 -3
  158. package/dist/tools/fs/safety.js +4 -4
  159. package/package.json +1 -1
  160. package/test/export-surface.snapshot.json +85 -9
  161. package/wire-changes/core@7.17.6.json +81 -0
  162. package/wire-changes/core@7.17.7.json +49 -0
  163. package/dist/tools/fs/read-face.d.ts +0 -68
  164. package/dist/tools/fs/read-face.js +0 -47
@@ -85,9 +85,12 @@ export interface EngineNotice {
85
85
  * The SEAT is announced, not a leg's resolved face: resolution (task seat, governance, the
86
86
  * read-only mount) still runs per prepare. In-flight legs keep the face they prepared under;
87
87
  * every later prepare resolves against the new declaration. A candidate outside the value gate
88
- * (`assertReadFaceValue`) throws atomically and mints nothing. Own `occurrenceId` for the same
88
+ * (`assertContainmentFaceValue`) throws atomically and mints nothing. Own `occurrenceId` for the same
89
89
  * reason as the catalog line (no run, no session, a two-word payload). Dedup key:
90
90
  * (code, occurrenceId).
91
+ * - `"config.write_face_swapped"` — the same line for the WRITE seat (`RunnerDeps.writeFace`),
92
+ * same payload, same laws. A SEPARATE code rather than a face field on the line above: the audience
93
+ * table and every consumer table are keyed by code.
91
94
  * - `"route.fallback_to_primary"` (key↔URL pairing, `src/brain/route-adjudicator.ts`) — a
92
95
  * DERIVED-leg model (role/tier/system-default resolution, never a caller-explicit one) failed
93
96
  * the pairing pre-flight and the seat fell back to the primary model instead of sinking the
@@ -100,6 +103,21 @@ export interface EngineNotice {
100
103
  * session falling back on the same seat used to mint byte-identical lines; `runId` is what
101
104
  * tells one task falling back twice apart from two tasks falling back once each.
102
105
  * Explicitly-named models never mint this: they refuse at the brain's request gate instead.
106
+ * - `"route.classifier_unroutable"` (same pre-flight, the other POSTURE) — the derived classifier seat
107
+ * failed the same pairing pre-flight while the deployment had declared NO fallback
108
+ * (`RunnerDeps.autoMode.classifierFallback: "none"`), so the seat was KEPT and its calls are refused at
109
+ * the request gate rather than served by the primary model. The line is why "no swap" is still not "no
110
+ * word". `detail: { seat, from, cause, fixHint, action: "refused", sessionId?, runId? }` — the twin
111
+ * above's keys MINUS `to` (nothing moved, and a `to` naming a model that was not used is the one lie a
112
+ * consumer cannot detect); `action` is the discriminator, so a reader never infers the posture from that
113
+ * absence. Audience `operator`, same entitlement reading as its twin. Dedup key: (code, seat) per run.
114
+ * - `"classifier.fallback"` (the classifier's candidate ladder, `src/core/runner/classifier-ladder.ts`)
115
+ * — a gated call's classification did NOT rule on the classifier seat and was re-asked on the model
116
+ * behind it. Sibling of `route.fallback_to_primary` one layer later: that one fires when the derived
117
+ * model is not ROUTABLE at all, this one when it is routable and still does not answer.
118
+ * `detail: { from, to, cause, sessionId?, runId? }` — `cause` is the `AutoModeFallbackCause` word, the
119
+ * two ids are the rungs. Dedup key: (code, from, cause) PER RUN; a second CAUSE on the same rung is
120
+ * its own line.
103
121
  * - `"route.base_url_changed_key_unchanged"` (key↔URL pairing) — `Runner.swapDeps` moved a
104
122
  * same-name entry's `baseUrl` while its Model-visible credential half (auth-bearing headers)
105
123
  * did not change: legal (a provider changing domains), but worth one loud line — if the
@@ -120,6 +138,9 @@ export interface EngineNotice {
120
138
  * a library-direct `createHandsToolkit` mount announces at toolkit creation (one per mount,
121
139
  * through the band-local `HandsToolkitOptions.onNotice` seat, absent ⇒ `console.warn`);
122
140
  * `detail: { seat, declared, inForce, cause }`.
141
+ * - `"config.write_face_deployment_clamped"` — the write seat's twin of the line above, with the
142
+ * same stations, the same once-per-sink ledger (its OWN ledger row) and the same detail shape. The
143
+ * cause differs in prose only: that mount carries no write faces at all.
123
144
  * - `"config.tool_model_gate_removed"` (design/277) — the model gate trimmed default-mounted
124
145
  * scaffold entries from a task's roster. One notice per (model, class), de-duplicated once per
125
146
  * `onNotice` SINK on the (modelId, class, canonical sorted removed-name set) line (same unit
@@ -629,6 +650,31 @@ export declare function deliverEngineNotice(onNotice: ((notice: EngineNotice) =>
629
650
  * one repeated disclosure about one run. The account is per-RUN by its own contract sentence ("at most
630
651
  * once per family, per run"), so it must carry the run's own id. Same omit-never-fabricate rule.
631
652
  */
653
+ /**
654
+ * What this leg still OWES a person, one count per family — the input to {@link undrainedUserInputNotices}.
655
+ * TWO lanes accept caller input and either can be holding some when a run ends: the harness's live queue
656
+ * (`held`, the at-agent_end snapshot, minus what a durable park migrated onto the new checkpoint) and a resumed
657
+ * leg's undelivered continuation frames (`owedContinuation` — the checkpoint's parked steers and the caller's
658
+ * wake message, minus what the same migration carried). They are ONE book: a frame is owed for the same reason
659
+ * on either lane — accepted, and never in the transcript — and counting the live queue alone is what let a leg
660
+ * that committed no pause drop a person's instruction in silence (the original row is already spent by then).
661
+ */
662
+ export declare function owedUserInputCounts(held: {
663
+ steer: number;
664
+ followUp: number;
665
+ } | undefined, migrated: {
666
+ steer: number;
667
+ followUp: number;
668
+ }, continuation: {
669
+ frames: ReadonlyArray<{
670
+ seq?: number;
671
+ }>;
672
+ carried: number;
673
+ rowMayReturn: boolean;
674
+ }): {
675
+ steer: number;
676
+ followUp: number;
677
+ };
632
678
  export declare function undrainedUserInputNotices(counts: {
633
679
  steer: number;
634
680
  followUp: number;
@@ -31,6 +31,13 @@ export function deliverEngineNotice(onNotice, notice) {
31
31
  }
32
32
  console.warn(notice.message);
33
33
  }
34
+ export function owedUserInputCounts(held, migrated, continuation) {
35
+ const owedContinuation = (continuation.rowMayReturn ? continuation.frames.filter((f) => f.seq === undefined).length : continuation.frames.length) - continuation.carried;
36
+ return {
37
+ steer: Math.max(0, (held?.steer ?? 0) - migrated.steer) + Math.max(0, owedContinuation),
38
+ followUp: Math.max(0, (held?.followUp ?? 0) - migrated.followUp),
39
+ };
40
+ }
34
41
  export function undrainedUserInputNotices(counts, taskId, sessionId, runId) {
35
42
  const tid = taskId !== undefined ? { taskId } : {};
36
43
  const sid = sessionId !== undefined ? { sessionId } : {};
@@ -369,6 +369,7 @@ export async function runGateLanes(pass) {
369
369
  const verdict = await input.autoMode.decider
370
370
  .decide({ req: pass.req, askMessage: decisionText(pass.decision) }, input.abortSignal)
371
371
  .catch(() => ({ kind: "unavailable", cause: "error" }));
372
+ pass.classifierRound = verdict.classifier;
372
373
  if (verdict.kind === "allow") {
373
374
  input.autoMode.denialTracking?.recordAllow();
374
375
  pass.decision = {
@@ -381,7 +382,7 @@ export async function runGateLanes(pass) {
381
382
  else if (verdict.kind === "unavailable") {
382
383
  pass.decision = attestClassifierDenyCause({
383
384
  action: "deny",
384
- message: classifierUnavailableDenyMessage(toolName, verdict.cause),
385
+ message: classifierUnavailableDenyMessage(toolName, verdict.cause, verdict.classifier),
385
386
  decisionReason: "classifier",
386
387
  }, "unavailable", pass.req);
387
388
  }
@@ -8,6 +8,7 @@
8
8
  * verdict ended.
9
9
  */
10
10
  import { type AskOrigin, type AssertAllKeysHandled } from "./ask-origin.js";
11
+ import { type AutoModeClassifierRound } from "./auto-mode.js";
11
12
  /**
12
13
  * How the wait an ask was in ENDED — the closed set. One word per way a wait can end; the `who` on the
13
14
  * record says which party ended it and is fixed BY the word (see {@link Settlement}).
@@ -189,13 +190,23 @@ export type CauseTableCoversEveryDeniedBy = AssertAllKeysHandled<Exclude<DeniedB
189
190
  /** The final disposition of a gate pass. `cause` is present exactly when the deny came out of an auto-mode
190
191
  * classifier round that did not RULE ({@link ClassifierDenyCause}); a ruling block and every non-classifier
191
192
  * deny carry none. Invariant I5 ({@link screenGateOutcome}): a cause stands only beside a layer word in
192
- * {@link DENIED_BY_MAY_CARRY_CAUSE}. */
193
+ * {@link DENIED_BY_MAY_CARRY_CAUSE}.
194
+ *
195
+ * `classifier` — the classification's model attribution, on BOTH arms (a classification ends either way,
196
+ * allow or deny): which model it was for, which one answered, and — only when the ladder moved — the rung it
197
+ * left and why. ABSENT reads UNKNOWN, never "the seat answered" (that is `fallback` absent WITH the member
198
+ * present), and a consumer cannot tell its three causes apart from the record alone: no classification
199
+ * happened; one ran on a decider given no candidate ladder, which can name no model; or one ran on an
200
+ * ANCESTOR's inherited chain, which carries the FORM (`cause`) but has no seat for the attribution yet.
201
+ * Additive. Invariant I6 ({@link screenGateOutcome}) screens its shape. */
193
202
  export type GateDisposition = {
194
203
  kind: "allowed";
204
+ classifier?: AutoModeClassifierRound;
195
205
  } | {
196
206
  kind: "denied";
197
207
  deniedBy: DeniedBy;
198
208
  cause?: ClassifierDenyCause;
209
+ classifier?: AutoModeClassifierRound;
199
210
  };
200
211
  /**
201
212
  * The WHOLE record of one tool-gate pass: the final disposition, plus — when this pass settled an ask —
@@ -212,7 +223,11 @@ export type GateDisposition = {
212
223
  * on the settlement (it is not the approver's refusal);
213
224
  * I4 `disposition.kind === "allowed"` ⇒ `settlement` absent or `human_allowed`;
214
225
  * I5 `disposition.cause` present ⇒ a member of {@link CLASSIFIER_DENY_CAUSES} beside a layer word
215
- * {@link DENIED_BY_MAY_CARRY_CAUSE} admits (the classifier station, or the inherited chain's `policy`).
226
+ * {@link DENIED_BY_MAY_CARRY_CAUSE} admits (the classifier station, or the inherited chain's `policy`);
227
+ * I6 `disposition.classifier` present ⇒ both model ids are non-empty strings, and a `fallback` on it
228
+ * names a `to` that IS `modelUsed` (the ladder landed where the record says) with a cause in
229
+ * {@link import("./auto-mode.js").AUTO_MODE_FALLBACK_CAUSES} — a record whose `to` and `modelUsed`
230
+ * disagree would let a reader answer "which model decided this call?" two ways.
216
231
  * Nothing here is read out of an ABSENCE: an ordinary allow with no ask is `{ disposition: { kind: "allowed" } }`,
217
232
  * a direct policy deny is `{ disposition: { kind: "denied", deniedBy: "policy" } }`, and neither carries a
218
233
  * settlement because neither settled one.
@@ -1,4 +1,5 @@
1
1
  import { isAskOrigin } from "./ask-origin.js";
2
+ import { isAutoModeFallbackCause } from "./auto-mode.js";
2
3
  export const SETTLEMENT_KINDS = [
3
4
  "human_allowed",
4
5
  "human_refused",
@@ -89,5 +90,20 @@ export function screenGateOutcome(g) {
89
90
  else if (isDeniedBy(d.deniedBy) && !DENIED_BY_MAY_CARRY_CAUSE[d.deniedBy])
90
91
  defects.push(`I5: a classifier cause cannot stand beside deniedBy "${d.deniedBy}"`);
91
92
  }
93
+ const attribution = d.classifier;
94
+ if (attribution !== undefined) {
95
+ const named = (v) => typeof v === "string" && v !== "";
96
+ if (!named(attribution.modelRequested) || !named(attribution.modelUsed))
97
+ defects.push("I6: disposition.classifier must name both the model the round was for and the model that answered");
98
+ const fb = attribution.fallback;
99
+ if (fb !== undefined) {
100
+ if (!named(fb.from) || !named(fb.to))
101
+ defects.push("I6: disposition.classifier.fallback must name the rung it left and the rung it arrived at");
102
+ else if (fb.to !== attribution.modelUsed)
103
+ defects.push(`I6: the fallback landed on "${fb.to}" but the record names "${attribution.modelUsed}" as the model that answered`);
104
+ if (!isAutoModeFallbackCause(fb.cause))
105
+ defects.push(`I6: fallback cause "${String(fb.cause)}" is outside the closed set`);
106
+ }
107
+ }
92
108
  return defects;
93
109
  }
@@ -104,7 +104,7 @@ export type NoticeAudience = "user" | "operator";
104
104
  * src/ for notice mint shapes and names any code that is minted but unregistered, or registered but
105
105
  * no longer minted.
106
106
  */
107
- export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_swapped", "config.read_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.artifact_host_invalid", "config.tool_card_undeclared", "config.tool_face_undeclared", "config.tool_face_invalid", "config.durable_gate_unavailable", "config.peer_admission_out_of_range", "config.peer_lane_unmounted", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "checkpoint.execution_outcome_unrecorded", "delegation.transcript_integrity", "delegation.ask_unresolvable", "mcp.injection_dropped", "mcp.revocation_probe_failed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "route.fallback_to_primary", "route.base_url_changed_key_unchanged", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "task.interrupt_unconsumed", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "tool_result.offload_put_failed"];
107
+ export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_swapped", "config.read_face_deployment_clamped", "config.write_face_swapped", "config.write_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.artifact_host_invalid", "config.tool_card_undeclared", "config.tool_face_undeclared", "config.tool_face_invalid", "config.durable_gate_unavailable", "config.peer_admission_out_of_range", "config.peer_lane_unmounted", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "classifier.fallback", "checkpoint.execution_outcome_unrecorded", "delegation.transcript_integrity", "delegation.ask_unresolvable", "mcp.injection_dropped", "mcp.revocation_probe_failed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "route.fallback_to_primary", "route.classifier_unroutable", "route.base_url_changed_key_unchanged", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "task.interrupt_unconsumed", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "tool_result.offload_put_failed"];
108
108
  /** A code this engine mints (see {@link ENGINE_NOTICE_CODES}). NOT the type of
109
109
  * `EngineNotice.code`, which stays `string` — a host forwarding its own notices through the same
110
110
  * sink is a supported shape, and narrowing that field would break it. */
@@ -102,6 +102,8 @@ export const ENGINE_NOTICE_CODES = [
102
102
  "config.models_swapped",
103
103
  "config.read_face_swapped",
104
104
  "config.read_face_deployment_clamped",
105
+ "config.write_face_swapped",
106
+ "config.write_face_deployment_clamped",
105
107
  "config.tool_model_gate_removed",
106
108
  "config.tool_model_gate_unknown_class",
107
109
  "config.tool_model_gate_env_invalid",
@@ -116,6 +118,7 @@ export const ENGINE_NOTICE_CODES = [
116
118
  "peer.held_settled",
117
119
  "peer.idle_subscription",
118
120
  "classifier.denial_limit",
121
+ "classifier.fallback",
119
122
  "checkpoint.execution_outcome_unrecorded",
120
123
  "delegation.transcript_integrity",
121
124
  "delegation.ask_unresolvable",
@@ -137,6 +140,7 @@ export const ENGINE_NOTICE_CODES = [
137
140
  "memory.consolidation_refused",
138
141
  "memory.consolidation_withheld",
139
142
  "route.fallback_to_primary",
143
+ "route.classifier_unroutable",
140
144
  "route.base_url_changed_key_unchanged",
141
145
  "task.user_steer_undrained",
142
146
  "task.user_followup_undrained",
@@ -159,6 +163,7 @@ const NOTICE_AUDIENCE_TABLE = {
159
163
  "task.user_steer_undrained": "user",
160
164
  "task.user_followup_undrained": "user",
161
165
  "classifier.denial_limit": "user",
166
+ "classifier.fallback": "operator",
162
167
  "task.turn_interrupted": "user",
163
168
  "steering.parked_input_blocked": "user",
164
169
  "task.halt_unconsumed": "user",
@@ -175,6 +180,8 @@ const NOTICE_AUDIENCE_TABLE = {
175
180
  "config.models_swapped": "operator",
176
181
  "config.read_face_swapped": "operator",
177
182
  "config.read_face_deployment_clamped": "operator",
183
+ "config.write_face_swapped": "operator",
184
+ "config.write_face_deployment_clamped": "operator",
178
185
  "config.tool_model_gate_removed": "operator",
179
186
  "config.tool_model_gate_unknown_class": "operator",
180
187
  "config.tool_model_gate_env_invalid": "operator",
@@ -200,6 +207,7 @@ const NOTICE_AUDIENCE_TABLE = {
200
207
  "memory.consolidation_incomplete": "operator",
201
208
  "memory.consolidation_refused": "operator",
202
209
  "route.fallback_to_primary": "operator",
210
+ "route.classifier_unroutable": "operator",
203
211
  "route.base_url_changed_key_unchanged": "operator",
204
212
  "tool_result.offload_put_failed": "operator",
205
213
  };
@@ -0,0 +1,66 @@
1
+ /**
2
+ * The hook seam's SEAT discipline and its disclosure vocabulary: which callback seats exist, what a
3
+ * deployment is allowed to put in one, and how a wiring disclosure names the ones that are wired.
4
+ *
5
+ * Kept beside the interface rather than inside it: `hooks.ts` owns the seam's BEHAVIOUR, and this module owns
6
+ * the seat as a CONFIGURATION VALUE — the same split `wiring-manifest.ts` keeps for the ask/question seats.
7
+ */
8
+ import type { Hooks } from "./hooks.js";
9
+ import type { AssertAllKeysHandled } from "./ask-origin.js";
10
+ import type { SeamProvenance } from "./wiring-manifest.js";
11
+ /**
12
+ * The CALLBACK SEATS of {@link Hooks} as a word list — the vocabulary a wiring disclosure names a wired seam
13
+ * by (`WiringManifest.hooks[].family`), in declaration order. `preToolUseObservational` is deliberately NOT a
14
+ * member (it is a DECLARATION ABOUT `preToolUse`); `timeoutMs` is not a seat either.
15
+ */
16
+ export declare const HOOK_FAMILIES: readonly ["preToolUse", "postToolUse", "userPromptSubmit", "stop", "postToolUseFailure", "postToolBatch", "preCompact", "postCompact", "stopFailure", "permissionDenied"];
17
+ /**
18
+ * One hook seat's name.
19
+ *
20
+ * @contract hooks.family_vocabulary — {@link HOOK_FAMILIES} is the only spelling of a hook seat's name on any
21
+ * disclosure face, and {@link HookFamiliesCoverEverySeat} makes a new callback member of {@link Hooks} red at
22
+ * `tsc` until it has a word there — a disclosure that omits a seat under-states what runs on the critical path.
23
+ */
24
+ export type HookFamily = (typeof HOOK_FAMILIES)[number];
25
+ /** Every CALLBACK member of {@link Hooks} — derived from the interface, never a hand-kept second list. */
26
+ type HookCallbackSeat = {
27
+ [K in keyof Hooks]-?: NonNullable<Hooks[K]> extends (...args: never[]) => unknown ? K : never;
28
+ }[keyof Hooks];
29
+ /** The closed-set fence: `never` while every callback seat has a word, the MISSING SEAT'S NAME when one does
30
+ * not (the constraint form names it; a conditional type says only "something changed"). */
31
+ export type HookFamiliesCoverEverySeat = AssertAllKeysHandled<Exclude<HookCallbackSeat, HookFamily>>;
32
+ /** The typed refusal a hook seat holding a present NON-CALLBACK value raises at prepare. */
33
+ export declare const HOOKS_INVALID_CODE = "config.hooks_invalid";
34
+ /**
35
+ * Screen the resolved hook record's seats (`spec.hooks ?? deps.hooks`).
36
+ *
37
+ * @contract hooks.seat_value — a hook seat is a FUNCTION or it is absent. A PRESENT non-function
38
+ * (`preToolUse: null`, a string, a bare object) is a configuration error and refuses the whole prepare with
39
+ * {@link HOOKS_INVALID_CODE}, naming the family and the type received. It is not folded to "unwired": the tool
40
+ * gate arms on PRESENCE (`hooks?.preToolUse !== undefined`) and would invoke the non-function and fail closed
41
+ * on every call, while a callability-based disclosure reported no hooks at all. The sibling seams already
42
+ * refuse the same way (`resolveQuestionSeam` / `resolveElicitSeam`). `undefined` stays exactly what it was:
43
+ * an unwired seat, silent.
44
+ */
45
+ export declare function assertHookSeatsCallable(hooks: Hooks | undefined, provenance: SeamProvenance): void;
46
+ /**
47
+ * One WIRED HOOK SEAT of this leg. `family` is the seat's name; `owner` is WHICH SEAT supplied the record, the
48
+ * `spec > deps` provenance the ask/question seams report. Deliberately no third owner word: core mounts NO
49
+ * engine-owned hooks, and a delegated child's inherited `preToolUse` rides the gate's constraint fold.
50
+ */
51
+ export interface WiringManifestHookEntry {
52
+ family: HookFamily;
53
+ owner: SeamProvenance;
54
+ /** Only on the `preToolUse` entry, and only when the deployment DECLARED the face observational
55
+ * (`Hooks.preToolUseObservational`) — it still runs on every call but is not folded into a delegated
56
+ * child's inherited constraints. Absence means "not declared", never "declared false". */
57
+ observational?: true;
58
+ }
59
+ /**
60
+ * The wiring manifest's hook read face ({@link WiringManifestHookEntry}): one entry per CALLBACK seat the
61
+ * resolved record wires, in the vocabulary's declaration order. The VOCABULARY is the judge — families are
62
+ * walked, never the record's own keys. `owner` is a property of the RECORD, so every entry agrees. Callability
63
+ * and presence cannot diverge here: {@link assertHookSeatsCallable} has already refused the leg where they would.
64
+ */
65
+ export declare function wiredHookEntries(hooks: Hooks | undefined, owner: SeamProvenance, preToolUseObservational: boolean): WiringManifestHookEntry[];
66
+ export {};
@@ -0,0 +1,37 @@
1
+ export const HOOK_FAMILIES = [
2
+ "preToolUse",
3
+ "postToolUse",
4
+ "userPromptSubmit",
5
+ "stop",
6
+ "postToolUseFailure",
7
+ "postToolBatch",
8
+ "preCompact",
9
+ "postCompact",
10
+ "stopFailure",
11
+ "permissionDenied",
12
+ ];
13
+ export const HOOKS_INVALID_CODE = "config.hooks_invalid";
14
+ export function assertHookSeatsCallable(hooks, provenance) {
15
+ if (hooks === undefined)
16
+ return;
17
+ for (const family of HOOK_FAMILIES) {
18
+ const seat = hooks[family];
19
+ if (seat === undefined || typeof seat === "function")
20
+ continue;
21
+ const e = new Error(`the resolved hooks record (${provenance}) holds ${seat === null ? "null" : typeof seat} at \`${family}\` — a hook seat is a callback or it is absent. ` +
22
+ `A present non-callback is not an unwired seat: the tool gate arms on presence and would refuse every call through it. Omit the key, or wire a function.`);
23
+ e.code = HOOKS_INVALID_CODE;
24
+ throw e;
25
+ }
26
+ }
27
+ export function wiredHookEntries(hooks, owner, preToolUseObservational) {
28
+ if (hooks === undefined)
29
+ return [];
30
+ const entries = [];
31
+ for (const family of HOOK_FAMILIES) {
32
+ if (typeof hooks[family] !== "function")
33
+ continue;
34
+ entries.push({ family, owner, ...(family === "preToolUse" && preToolUseObservational ? { observational: true } : {}) });
35
+ }
36
+ return entries;
37
+ }
@@ -21,9 +21,14 @@
21
21
  * no marker and an absent directory narrows.
22
22
  *
23
23
  * Reverse check (the seat's own invariant): nothing enters `roots` except through `admit`, and every
24
- * caller of `admit` is an advertiser deciding its advertisement off the returned verdict — so there is
25
- * no directory the fence opens that the prompt does not advertise, and none the prompt advertises
26
- * that the fence refuses.
24
+ * caller of `admit` is an advertiser deciding its advertisement off the returned verdict — so no
25
+ * directory reaches the ROOTS FENCE through this seat without the prompt advertising it, and none
26
+ * the prompt advertises is refused by that fence.
27
+ *
28
+ * The invariant is about this seat, not about the whole write surface: under an open write face
29
+ * ({@link import("../../tools/fs/containment-face.js").ContainmentFace}) the roots fence judges
30
+ * nothing, so what the prompt advertises is a floor rather than the whole list — the `# Environment`
31
+ * block says so in its own line, and the advertisement stays true (these directories ARE writable).
27
32
  */
28
33
  import type { ExecutionEnv } from "../../internal/harness.js";
29
34
  export interface AdvertisedWritableDir {
@@ -153,11 +153,12 @@ export interface ResultFlags {
153
153
  * distinct codes from arriving as one anonymous `limits.max_turns_exceeded`. Empty/absent ⇒ the field is omitted. */
154
154
  remoteEnvFailures?: TaskResult["remoteEnvFailures"];
155
155
  /** The run's effective read posture (resolved face + normalized deny
156
- * additions), echoed on `TaskResult.effectiveReadFace` / `.effectiveReadDenyPatterns`. Pure
156
+ * additions), echoed on `TaskResult.effectiveReadFace` / `.effectiveWriteFace` / `.effectiveReadDenyPatterns`. Pure
157
157
  * pass-through (assembly neither adds nor filters) and INDEPENDENT of the status/errorCode chain:
158
158
  * the posture is a fact about the leg that ran, whatever terminal it reached — a post-completion
159
159
  * spawner folds it stricter-wins into follow-on legs; every other consumer may ignore it. */
160
160
  effectiveReadFace?: TaskResult["effectiveReadFace"];
161
+ effectiveWriteFace?: TaskResult["effectiveWriteFace"];
161
162
  effectiveReadDenyPatterns?: TaskResult["effectiveReadDenyPatterns"];
162
163
  /** The memory-visibility observation, echoed on
163
164
  * `TaskResult.effectiveMemoryScopes`. Pure pass-through on every terminal, same law as the
@@ -166,5 +166,5 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
166
166
  if (flags.unpricedSpend)
167
167
  delete publicStats.costMicroUsd;
168
168
  const stampHaltedByUser = haltConsumed(flags.haltConsumption);
169
- return { taskId, ...(flags.runId !== undefined ? { runId: flags.runId } : {}), sessionId, terminal, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, ...(apiFailure !== undefined ? { apiFailure } : {}), ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.editedFiles !== undefined && flags.editedFiles.length > 0 ? { editedFiles: flags.editedFiles } : {}), ...(flags.haltedOnUserRejection === true ? { haltedOnUserRejection: true } : {}), ...(stampHaltedByUser ? { haltedByUser: true } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: [...flags.remoteEnvFailures] } : {}), ...(flags.strandedHumanAnswers !== undefined && flags.strandedHumanAnswers.length > 0 ? { strandedHumanAnswers: flags.strandedHumanAnswers } : {}), ...(flags.effectiveReadFace !== undefined ? { effectiveReadFace: flags.effectiveReadFace } : {}), ...(flags.effectiveReadDenyPatterns !== undefined && flags.effectiveReadDenyPatterns.length > 0 ? { effectiveReadDenyPatterns: flags.effectiveReadDenyPatterns } : {}), ...(flags.effectiveMemoryScopes !== undefined ? { effectiveMemoryScopes: flags.effectiveMemoryScopes } : {}), ...(flags.effectiveReasoning !== undefined ? { effectiveReasoning: flags.effectiveReasoning } : {}), stats: publicStats };
169
+ return { taskId, ...(flags.runId !== undefined ? { runId: flags.runId } : {}), sessionId, terminal, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, ...(apiFailure !== undefined ? { apiFailure } : {}), ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.editedFiles !== undefined && flags.editedFiles.length > 0 ? { editedFiles: flags.editedFiles } : {}), ...(flags.haltedOnUserRejection === true ? { haltedOnUserRejection: true } : {}), ...(stampHaltedByUser ? { haltedByUser: true } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: [...flags.remoteEnvFailures] } : {}), ...(flags.strandedHumanAnswers !== undefined && flags.strandedHumanAnswers.length > 0 ? { strandedHumanAnswers: flags.strandedHumanAnswers } : {}), ...(flags.effectiveReadFace !== undefined ? { effectiveReadFace: flags.effectiveReadFace } : {}), ...(flags.effectiveWriteFace !== undefined ? { effectiveWriteFace: flags.effectiveWriteFace } : {}), ...(flags.effectiveReadDenyPatterns !== undefined && flags.effectiveReadDenyPatterns.length > 0 ? { effectiveReadDenyPatterns: flags.effectiveReadDenyPatterns } : {}), ...(flags.effectiveMemoryScopes !== undefined ? { effectiveMemoryScopes: flags.effectiveMemoryScopes } : {}), ...(flags.effectiveReasoning !== undefined ? { effectiveReasoning: flags.effectiveReasoning } : {}), stats: publicStats };
170
170
  }
@@ -0,0 +1,89 @@
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, and the once-per-run disclosure its first move mints.
4
+ *
5
+ * Beside `derived-route-fallback.ts` and in the same family — both are seats the caps phase's classifier
6
+ * arming reaches DOWN for, and both answer "this leg's model did not work out" one layer apart. Which is why
7
+ * the rung comparison here borrows the route adjudicator's own identity law instead of inventing one.
8
+ *
9
+ * THE TWO LAYERS, since one sentence ("the classifier seat did not work out ⇒ use the run's main model") is
10
+ * spelled in two places. They are ordered, and each has its own trigger and its own record face:
11
+ *
12
+ * ① PRE-FLIGHT — `derivedRouteFallsBack`, BEFORE any call. Trigger: the classifier model's route differs
13
+ * from the primary's AND the brain's own route judge finds the credential/URL pairing broken. Effect:
14
+ * the arming SWAPS the seat, so the ladder below is built from the main model and rung 0 IS the main
15
+ * model. Record: the once-per-run operator notice `route.fallback_to_primary` (`detail.{seat, from, to,
16
+ * cause}`) — and only that. `GateDisposition.classifier.modelRequested` reads the MAIN model's id,
17
+ * because the ladder it describes never carried the configured seat; the gate record answers "which
18
+ * model was asked", and "what did the deployment configure, and was it swapped" is the notice's
19
+ * question. `classifierFallback` governs THIS layer too: under `"none"` the pre-flight does not swap —
20
+ * it keeps the declared seat and announces `route.classifier_unroutable`, so the deployment that said
21
+ * "deny rather than fall back" gets the denial (the seat's calls are refused at the request gate)
22
+ * instead of a silent fallback the knob had just declined.
23
+ * ② THE LADDER — here, AFTER a call. Trigger: the rung produced no RULING. Effect: the next rung is asked.
24
+ * Record: `GateDisposition.classifier.fallback` on every classification this run's own gate decided (an
25
+ * inherited-ancestor chain carries the form and no attribution) + the throttled `classifier.fallback`.
26
+ *
27
+ * They FOLD rather than stack: after a ① SWAP the seat and the main model are the same route, so
28
+ * {@link classifierLadderRungs} returns ONE rung and ② cannot fire in the same run (and under `"none"`, where
29
+ * ① does not swap, the ladder is one rung by the knob). The split's cost is the
30
+ * `modelRequested` reading above; the unification (the pre-flight as rung 0 under its own cause word, one
31
+ * record face for both) changes the notice set and is a ticket, not a repair.
32
+ */
33
+ import type { Model } from "../../internal/llm.js";
34
+ import { type AutoModeClassified } from "../auto-mode.js";
35
+ import type { RunnerDeps } from "../types.js";
36
+ /**
37
+ * The rungs a classification walks, in order: the classifier seat, and — under `classifierFallback: "main"` (the
38
+ * default) — the run's main model behind it. The knob is screened by `autoModeClassifierFallbackOf`, here and
39
+ * (idempotently) at the caps phase that reads the same word for the pre-flight's posture, so a value outside
40
+ * the closed set refuses the prepare loudly (`config.auto_mode_classifier_fallback_invalid`) instead of
41
+ * reading as the default.
42
+ *
43
+ * ONE RUNG when the two resolve to the SAME ROUTE. Route identity, not `Model.id`: two catalog entries may
44
+ * legitimately share a wire id while pointing at different endpoints (the route adjudicator's own law, and
45
+ * the split its pre-flight exists for) — an id-only comparison would collapse a healthy main endpoint and a
46
+ * broken classifier endpoint into one rung, turning the ladder off for the deployment that needs it most.
47
+ * The collapse where it does apply is deliberate: re-asking the SAME model is a retry, which is what CC's
48
+ * `unavailableOuterRetries` does and what this ladder is not. A catalog value the identity law cannot
49
+ * normalize (a non-string `baseUrl` — it throws) reads as TWO rungs: being wrong that way costs one extra
50
+ * call on a broken catalog, the other way turns the feature off invisibly, and the bad value still earns its
51
+ * loud refusal at the brain's request gate.
52
+ */
53
+ export declare function classifierLadderRungs(knob: unknown, classifier: Model, main: Model): readonly Model[];
54
+ /**
55
+ * The classifier's REASONING SEAT, read off the model a classification is actually asked on — ONE predicate:
56
+ * can the wire say off? Yes ⇒ `off` + the verdict-sized cap (the request pays for the answer alone); no ⇒
57
+ * `low` + the verdict cap + the low-reasoning budget (a model that thinks regardless is told to think the
58
+ * LEAST its wire can spell, in exactly the room that tier needs). PER RUNG, not once per arming: the second
59
+ * rung is a different model with its own wire, and asking it under the first rung's seat is how a correct
60
+ * fallback comes back with zero text. The `low` seat is also the re-ask seat: an unknown model that answers
61
+ * NOTHING at the `off` cap has just told us it thinks anyway, and is re-asked as one that does.
62
+ */
63
+ export declare const CLASSIFIER_OFF_SEAT: {
64
+ reasoning: "off";
65
+ maxTokens: number;
66
+ };
67
+ export declare const CLASSIFIER_LOW_SEAT: {
68
+ reasoning: "low";
69
+ maxTokens: number;
70
+ };
71
+ export declare function classifierRungSeat(model: Model): typeof CLASSIFIER_OFF_SEAT | typeof CLASSIFIER_LOW_SEAT;
72
+ /**
73
+ * The per-decision READ of a classification, as one seat: the `auto_mode.classified` trace frame (wall time,
74
+ * verdict, cause) and the ladder's throttled `classifier.fallback` disclosure. Both are observers — a
75
+ * throwing sink never reaches the gate (the decider swallows it), and neither changes a verdict.
76
+ *
77
+ * The frame's `model` is the model the classification RAN ON, which is what the field has always meant (its
78
+ * declaration reads "the resolved classifier role, or the main model it fell back to"): the route
79
+ * pre-flight's swap and the ladder's walk are two ways to reach that same sentence. `seatModelId` is the
80
+ * fallback for a classification that carries no attribution at all.
81
+ */
82
+ export declare function classifierRoundObserver(args: {
83
+ tracer: RunnerDeps["tracer"];
84
+ onNotice: RunnerDeps["onNotice"];
85
+ taskId: string;
86
+ seatModelId: string;
87
+ sessionId?: string;
88
+ runId?: string;
89
+ }): (info: AutoModeClassified) => void;
@@ -0,0 +1,65 @@
1
+ import { sameRouteIdentity } from "../../brain/route-adjudicator.js";
2
+ import { thinkingOffExpressible } from "../../brain/reasoning.js";
3
+ import { AUTO_MODE_CLASSIFIER_LOW_REASONING_BUDGET_TOKENS, AUTO_MODE_CLASSIFIER_MAX_TOKENS } from "../auto-mode-defaults.js";
4
+ import { autoModeClassifierFallbackOf } from "../auto-mode.js";
5
+ import { emitTrace } from "../trace.js";
6
+ import { deliverEngineNotice } from "../types.js";
7
+ export function classifierLadderRungs(knob, classifier, main) {
8
+ if (autoModeClassifierFallbackOf(knob) === "none")
9
+ return [classifier];
10
+ let sameRoute;
11
+ try {
12
+ sameRoute = sameRouteIdentity(classifier, main);
13
+ }
14
+ catch {
15
+ sameRoute = false;
16
+ }
17
+ return sameRoute ? [classifier] : [classifier, main];
18
+ }
19
+ function classifierFallbackAnnouncer(args) {
20
+ const announced = new Set();
21
+ return (attribution) => {
22
+ const fallback = attribution?.fallback;
23
+ if (fallback === undefined)
24
+ return;
25
+ const key = `${fallback.from}\u0000${fallback.cause}`;
26
+ if (announced.has(key))
27
+ return;
28
+ announced.add(key);
29
+ deliverEngineNotice(args.onNotice, {
30
+ code: "classifier.fallback",
31
+ message: `the auto-mode classifier model "${fallback.from}" did not rule (${fallback.cause}) — this run re-asks each gated call on "${fallback.to}". ` +
32
+ 'Check that the classifier role names a model this deployment\'s gateway serves; set RunnerDeps.autoMode.classifierFallback to "none" to deny instead of falling back.',
33
+ detail: {
34
+ from: fallback.from,
35
+ to: fallback.to,
36
+ cause: fallback.cause,
37
+ ...(args.sessionId !== undefined ? { sessionId: args.sessionId } : {}),
38
+ ...(args.runId !== undefined ? { runId: args.runId } : {}),
39
+ },
40
+ ...(args.sessionId !== undefined ? { sessionId: args.sessionId } : {}),
41
+ });
42
+ };
43
+ }
44
+ export const CLASSIFIER_OFF_SEAT = { reasoning: "off", maxTokens: AUTO_MODE_CLASSIFIER_MAX_TOKENS };
45
+ export const CLASSIFIER_LOW_SEAT = { reasoning: "low", maxTokens: AUTO_MODE_CLASSIFIER_MAX_TOKENS + AUTO_MODE_CLASSIFIER_LOW_REASONING_BUDGET_TOKENS };
46
+ export function classifierRungSeat(model) {
47
+ return thinkingOffExpressible(model) ? CLASSIFIER_OFF_SEAT : CLASSIFIER_LOW_SEAT;
48
+ }
49
+ export function classifierRoundObserver(args) {
50
+ const announce = classifierFallbackAnnouncer(args);
51
+ return (info) => {
52
+ emitTrace(args.tracer, () => ({
53
+ kind: "auto_mode.classified",
54
+ version: 1,
55
+ taskId: args.taskId,
56
+ toolCallId: info.toolCallId,
57
+ model: info.classifier?.modelUsed ?? args.seatModelId,
58
+ ms: info.ms,
59
+ verdict: info.verdict,
60
+ ...(info.cause !== undefined ? { cause: info.cause } : {}),
61
+ ts: Date.now(),
62
+ }));
63
+ announce(info.classifier);
64
+ };
65
+ }