@sema-agent/core 5.48.0 → 5.50.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 (61) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/dist/agents/agent-transcript-tool.d.ts +1 -1
  3. package/dist/agents/agent-transcript-tool.js +1 -1
  4. package/dist/agents/roster-store.js +4 -1
  5. package/dist/agents/send-message-tool.d.ts +2 -2
  6. package/dist/agents/send-message-tool.js +2 -2
  7. package/dist/agents/subagent.d.ts +6 -0
  8. package/dist/agents/subagent.js +126 -1
  9. package/dist/agents/teacher.d.ts +25 -1
  10. package/dist/agents/teacher.js +89 -13
  11. package/dist/brain/anthropic.js +11 -20
  12. package/dist/brain/open-responses.js +6 -14
  13. package/dist/brain/openai.js +6 -18
  14. package/dist/brain/reasoning.d.ts +100 -8
  15. package/dist/brain/reasoning.js +39 -15
  16. package/dist/brain/request-params.d.ts +37 -1
  17. package/dist/brain/request-params.js +40 -2
  18. package/dist/core/background-agent-store.d.ts +1 -1
  19. package/dist/core/background-agent-store.js +5 -4
  20. package/dist/core/mcp.d.ts +7 -1
  21. package/dist/core/mcp.js +64 -8
  22. package/dist/core/memory-engine/delegation-settlement.d.ts +27 -0
  23. package/dist/core/memory-engine/delegation-settlement.js +31 -4
  24. package/dist/core/memory-engine/dual-root.js +11 -0
  25. package/dist/core/memory-engine/engine.d.ts +36 -2
  26. package/dist/core/memory-engine/engine.js +354 -38
  27. package/dist/core/memory-engine/layout.d.ts +43 -0
  28. package/dist/core/memory-engine/layout.js +59 -0
  29. package/dist/core/memory-engine/memory-backend-contract.js +120 -0
  30. package/dist/core/memory-engine/origin-clearance.d.ts +19 -0
  31. package/dist/core/memory-engine/origin-clearance.js +10 -0
  32. package/dist/core/memory-engine/provenance-wording.d.ts +15 -1
  33. package/dist/core/memory-engine/provenance-wording.js +1 -0
  34. package/dist/core/memory-engine/tools.js +6 -4
  35. package/dist/core/memory-engine/types.d.ts +13 -1
  36. package/dist/core/runner/prepare-task.js +22 -8
  37. package/dist/core/runner/runtask.d.ts +26 -1
  38. package/dist/core/runner/runtask.js +18 -2
  39. package/dist/core/strategy-store.d.ts +180 -3
  40. package/dist/core/strategy-store.js +172 -23
  41. package/dist/core/task-registry-agent.js +6 -0
  42. package/dist/core/types.d.ts +24 -1
  43. package/dist/index.d.ts +2 -2
  44. package/dist/index.js +2 -2
  45. package/dist/orchestration/run-workflow-tool.d.ts +12 -0
  46. package/dist/orchestration/run-workflow-tool.js +1 -1
  47. package/dist/orchestration/workflow-governance.d.ts +27 -0
  48. package/dist/orchestration/workflow-governance.js +13 -0
  49. package/dist/orchestration/workflow-primitives.d.ts +8 -1
  50. package/dist/orchestration/workflow-primitives.js +11 -3
  51. package/dist/stores/file/file-snapshot-store.js +7 -1
  52. package/dist/stores/file/index.d.ts +8 -0
  53. package/dist/stores/file/index.js +12 -0
  54. package/dist/stores/file/session-policy-store.d.ts +0 -13
  55. package/dist/stores/file/session-policy-store.js +7 -1
  56. package/dist/stores/file/session-store.d.ts +4 -1
  57. package/dist/stores/file/session-store.js +7 -1
  58. package/dist/stores/file/strategy-store.d.ts +97 -0
  59. package/dist/stores/file/strategy-store.js +340 -0
  60. package/package.json +1 -1
  61. package/test/export-surface.snapshot.json +8 -1
@@ -5,8 +5,8 @@ import { createRepetitionPoll, parseStreamedToolArgs } from "./stream-shared.js"
5
5
  import { mintFallbackToolCallId } from "./tool-call-id.js";
6
6
  import { emitBrainTelemetry } from "./status-sink.js";
7
7
  import { errorResultMediaNote, IMAGE_OMITTED_NO_VISION, imagesOmittedNoVisionNote, modelSupportsVision, sendableImages } from "./media-degrade.js";
8
- import { OUTPUT_CAP_KEYS, RESPONSES_RESERVED, applyExtraBody, effectiveOutputCap, stripAuthHeaders } from "./request-params.js";
9
- import { DEFAULT_EFFORT_LEVELS, isThinkingLevel, resolveEffort } from "./reasoning.js";
8
+ import { OUTPUT_CAP_KEYS, RESPONSES_RESERVED, applyExtraBody, effectiveOutputCap, lockHeader, mergeHeaders, stripAuthHeaders } from "./request-params.js";
9
+ import { mintEffortWireValue, reasoningRequestCarried } from "./reasoning.js";
10
10
  import { runStreamingBrain } from "./stream-engine.js";
11
11
  const DEGENERATE_POLL_CHARS = 64;
12
12
  const MALFORMED_SAMPLE_CHARS = 160;
@@ -151,16 +151,12 @@ function toResponsesTools(ctx) {
151
151
  }));
152
152
  }
153
153
  function resolveWireEffort(model, reasoning) {
154
- if (!model.reasoning || !isThinkingLevel(reasoning) || reasoning === "off")
154
+ if (!reasoningRequestCarried(model, reasoning))
155
155
  return undefined;
156
156
  const compat = responsesCompat(model);
157
157
  if (compat.supportsReasoningEffort === false)
158
158
  return undefined;
159
- const effort = resolveEffort(reasoning, compat.reasoningEffortLevels ?? DEFAULT_EFFORT_LEVELS).effective;
160
- const mapped = model.thinkingLevelMap?.[effort];
161
- if (mapped === null)
162
- return undefined;
163
- return mapped ?? effort;
159
+ return mintEffortWireValue(reasoning, model, compat.reasoningEffortLevels).wireValue;
164
160
  }
165
161
  function computeUsage(model, raw) {
166
162
  const input = raw?.input_tokens ?? 0;
@@ -268,14 +264,10 @@ export function createOpenResponsesBrain(config = {}) {
268
264
  }
269
265
  if (effort !== undefined)
270
266
  body.reasoning = { effort };
271
- const headers = {
272
- ...model.headers,
273
- ...config.headers,
274
- ...options?.headers,
275
- };
267
+ const headers = mergeHeaders(model.headers, config.headers, options?.headers);
276
268
  if (options?.apiKey !== undefined)
277
269
  stripAuthHeaders(headers);
278
- headers["content-type"] = "application/json";
270
+ lockHeader(headers, "content-type", "application/json");
279
271
  if (apiKey)
280
272
  headers["authorization"] = `Bearer ${apiKey}`;
281
273
  const wire = applyExtraBody(body, model.extraBody, RESPONSES_RESERVED);
@@ -5,8 +5,8 @@ import { createRepetitionPoll, parseStreamedToolArgs } from "./stream-shared.js"
5
5
  import { mintFallbackToolCallId } from "./tool-call-id.js";
6
6
  import { emitBrainTelemetry } from "./status-sink.js";
7
7
  import { errorResultMediaNote, IMAGE_OMITTED_NO_VISION, imagesOmittedNoVisionNote, modelSupportsVision, sendableImages } from "./media-degrade.js";
8
- import { OPENAI_RESERVED, OUTPUT_CAP_KEYS, applyExtraBody, effectiveOutputCap, stripAuthHeaders } from "./request-params.js";
9
- import { DEFAULT_EFFORT_LEVELS, isThinkingLevel, resolveEffort } from "./reasoning.js";
8
+ import { OPENAI_RESERVED, OUTPUT_CAP_KEYS, applyExtraBody, effectiveOutputCap, lockHeader, mergeHeaders, stripAuthHeaders } from "./request-params.js";
9
+ import { mintEffortWireValue, reasoningRequestCarried } from "./reasoning.js";
10
10
  import { runStreamingBrain } from "./stream-engine.js";
11
11
  function closeToolCallAccum(acc) {
12
12
  if (acc.closedTc)
@@ -86,19 +86,11 @@ function applyThinking(body, model, reasoning) {
86
86
  }
87
87
  return;
88
88
  }
89
- if (!model.reasoning || !isThinkingLevel(reasoning) || reasoning === "off")
89
+ if (!reasoningRequestCarried(model, reasoning))
90
90
  return;
91
91
  const compat = thinkingCompat(model);
92
92
  const supportsEffort = compat.supportsReasoningEffort ?? true;
93
- const effort = resolveEffort(reasoning, compat.reasoningEffortLevels ?? DEFAULT_EFFORT_LEVELS).effective;
94
- let effortWire = effort;
95
- if (model.thinkingLevelMap) {
96
- const mapped = model.thinkingLevelMap[effort];
97
- if (mapped === null)
98
- effortWire = undefined;
99
- else if (mapped !== undefined)
100
- effortWire = mapped;
101
- }
93
+ const effortWire = mintEffortWireValue(reasoning, model, compat.reasoningEffortLevels).wireValue;
102
94
  const format = compat.thinkingFormat ?? "openai";
103
95
  switch (format) {
104
96
  case "openai":
@@ -311,14 +303,10 @@ export function createOpenAIBrain(config = {}) {
311
303
  if (options?.stop)
312
304
  body.stop = options.stop;
313
305
  applyThinking(body, model, options?.reasoning);
314
- const headers = {
315
- ...model.headers,
316
- ...config.headers,
317
- ...options?.headers,
318
- };
306
+ const headers = mergeHeaders(model.headers, config.headers, options?.headers);
319
307
  if (options?.apiKey !== undefined)
320
308
  stripAuthHeaders(headers);
321
- headers["content-type"] = "application/json";
309
+ lockHeader(headers, "content-type", "application/json");
322
310
  if (apiKey)
323
311
  headers["authorization"] = `Bearer ${apiKey}`;
324
312
  const wire = applyExtraBody(body, model.extraBody, OPENAI_RESERVED);
@@ -16,6 +16,29 @@ import type { ThinkingLevel } from "../internal/harness-types.js";
16
16
  export type ReasoningIntensity = ThinkingLevel;
17
17
  /** Type guard: is `v` one of the 7 {@link ThinkingLevel} tiers? (A legacy/unknown reasoning string is not.) */
18
18
  export declare function isThinkingLevel(v: unknown): v is ThinkingLevel;
19
+ /**
20
+ * THE thinking-request ENTRY predicate — does this request enter an applier's emission path at all?
21
+ * One conjunction, three arms: the model declares reasoning (TRUTHINESS — the adapters' own
22
+ * judgment, see the r5 note in {@link resolveReasoning}), the requested value is a real
23
+ * {@link ThinkingLevel} (an out-of-contract value — a typo'd tier, a caller's own enum — reads as
24
+ * ABSENCE, never as a declared tier), and it is not the explicit `"off"`.
25
+ *
26
+ * ENTRY gate, deliberately NOT an emitted-a-key guarantee: past this gate the per-FORMAT arms still
27
+ * decide what (if anything) lands on the wire — `supportsReasoningEffort:false` on a format whose
28
+ * only carrier is the effort key, a `null` levelmap entry, the binary enable keys. Those arms have
29
+ * their own reporting shape (the `graded:false` intent-echo family, each pinned with its rationale
30
+ * where it lives); this predicate only closes the gate the three appliers used to re-spell.
31
+ *
32
+ * Single-sourced here because every wire applier (openai.ts `applyThinking`, anthropic.ts's thinking
33
+ * + effort arms, open-responses.ts `resolveWireEffort`) used to re-spell the same three conjuncts —
34
+ * and the REPORTING resolver ({@link resolveReasoning}) mirrored only ONE of them (`!model.reasoning`),
35
+ * so a garbage tier put ZERO parameters on the wire while the resolution claimed a clamped-to-minimal
36
+ * gradient (or echoed the garbage string as `effective` on binary formats). A predicate the appliers
37
+ * and the reporter both call cannot drift.
38
+ */
39
+ export declare function reasoningRequestCarried(model: {
40
+ reasoning?: boolean;
41
+ }, reasoning: unknown): reasoning is ThinkingLevel;
19
42
  /** Ordinal rank of a level (`off`=0 … `max`=6). */
20
43
  export declare function rankOf(level: ThinkingLevel): number;
21
44
  /**
@@ -65,6 +88,66 @@ export interface ReasoningResolution {
65
88
  /** True when {@link effective} differs from {@link requested} (the request couldn't be honored exactly). */
66
89
  clamped: boolean;
67
90
  }
91
+ /** Anthropic's hard floor for an extended-thinking budget. Lives HERE (not the anthropic brain) so
92
+ * the cap-wins predicate below and the brain's budget-window math read ONE constant. */
93
+ export declare const MIN_THINKING_TOKENS = 1024;
94
+ /**
95
+ * The anthropic BUDGET path's cap-wins arm (#346, single-sourced; design/119 #2 review codex H4):
96
+ * a HARD per-request output cap (an engine-imposed override or the caller's explicit
97
+ * `options.maxTokens` — the two lanes the brain refuses to raise) too small to host a legal thinking
98
+ * budget (≥ {@link MIN_THINKING_TOKENS}) plus answer room means the CAP WINS and thinking is skipped
99
+ * for the request. Shared by the wire arm (anthropic.ts, which acts on it) and
100
+ * {@link resolveReasoning}'s budget arm (which mirrors it when the caller supplies the request
101
+ * facts), so the skip decision and its report are one predicate — the reporter's budget arm used to
102
+ * claim an unconditional `graded:true` gradient while this arm deleted the thinking block from the
103
+ * very request it described. A soft (model/config-sourced) cap never skips: the brain raises it to
104
+ * host the budget instead (`hardCap === false`).
105
+ */
106
+ export declare function budgetCapSkipsThinking(outputCapTokens: number, hardCap: boolean): boolean;
107
+ /**
108
+ * OPTIONAL per-request facts for {@link resolveReasoning} — what the wire's budget arm knows at
109
+ * request build that a per-leg eager resolution cannot: the resolved output cap and whether it is a
110
+ * HARD bound. Supplied ⇒ the anthropic budget arm mirrors the wire's cap-wins skip
111
+ * ({@link budgetCapSkipsThinking}); absent ⇒ the budget arm reports the cap-blind gradient it always
112
+ * did (the eager per-leg trace/result mint has no request facts — a capped request's per-attempt skip
113
+ * is visible only to a caller that passes them).
114
+ */
115
+ export interface ReasoningWireFacts {
116
+ /** The request's resolved output cap (the wire `max_tokens` at the moment the thinking arm judges). */
117
+ outputCapTokens: number;
118
+ /** True when the cap is HARD (engine override / caller `options.maxTokens`) — the lanes the brain
119
+ * refuses to raise; a soft model/config cap is raised to host the budget instead. */
120
+ hardOutputCap: boolean;
121
+ }
122
+ /**
123
+ * The anthropic-`effortLevels` PRESENCE/SHAPE gate, shared by the wire arm (anthropic.ts) and the
124
+ * reporting dispatch below so the two cannot disagree about which family a request rides (#335): the
125
+ * old twin predicates were `x && x.length > 0` on both sides, so a truthy NON-ARRAY (a string —
126
+ * `.length > 0` holds) entered the wire arm and threw a bare TypeError from its `.filter` pre-clean
127
+ * BEFORE {@link resolveEffort}'s centralized non-array fallback could read it as undeclared — while
128
+ * the reporter happily described an effort resolution for the same config. Only the container shape
129
+ * is judged here; MEMBER validity stays {@link resolveEffort}'s job (its element-level sanitization
130
+ * is the single bad-value seat, which is also why the returned array is not member-checked — the cast
131
+ * is checked at runtime by every consumer's `resolveEffort` call).
132
+ */
133
+ export declare function declaredEffortLevels(v: unknown): readonly ThinkingLevel[] | undefined;
134
+ /**
135
+ * The effort-lane WIRE-VALUE mint — clamp ({@link resolveEffort}) + the `Model.thinkingLevelMap`
136
+ * translation in ONE place, consumed by both completions-family appliers (openai.ts `applyThinking`,
137
+ * open-responses.ts `resolveWireEffort`) AND the reporting dispatch ({@link resolveReasoning}'s
138
+ * arms), so the value that reaches the wire and the resolution a trace/result face claims are two
139
+ * reads of one computation, never parallel re-derivations. Map semantics (the field's contract): a
140
+ * MISSING key ⇒ provider default (the clamped tier name as-is); a STRING ⇒ that provider-specific
141
+ * spelling (same tier, still honored); `null` ⇒ the tier is UNSUPPORTED on this model —
142
+ * `wireValue: undefined`, no effort value on the wire (thinking still enables via a format's own
143
+ * enable key where one exists), which is exactly the tier-not-honored shape the reporter must echo.
144
+ */
145
+ export declare function mintEffortWireValue(requested: ThinkingLevel, model: {
146
+ thinkingLevelMap?: Readonly<Partial<Record<ThinkingLevel, string | null>>>;
147
+ }, allowed: readonly ThinkingLevel[] | undefined): {
148
+ resolution: ReasoningResolution;
149
+ wireValue: string | undefined;
150
+ };
68
151
  /**
69
152
  * A {@link ReasoningResolution} enriched with the endpoint discriminant, for observability (design/96 S6).
70
153
  * The brain consumes only the {@link ReasoningResolution} fields to shape the request; `format`/`endpoint`
@@ -77,9 +160,12 @@ export interface ResolvedReasoning extends ReasoningResolution {
77
160
  /** A coarse endpoint label for the trace (`model.api` — e.g. `openai-completions`, `anthropic-messages`). */
78
161
  endpoint: string;
79
162
  /**
80
- * Present (true) only when the model declares NO reasoning capability (`Model.reasoning` falsy): every
81
- * brain early-returns on that flag (openai.ts applyThinking / anthropic.ts / open-responses.ts
82
- * resolveWireEffort), so NO thinking
163
+ * Present (true) only when the request never CARRIES at all: {@link reasoningRequestCarried} false
164
+ * on a non-"off" request the model declares NO reasoning capability (`Model.reasoning` falsy),
165
+ * OR the requested value is not a valid {@link ThinkingLevel} (an out-of-contract tier every wire
166
+ * applier reads as ABSENCE — the same three-conjunct gate all of them share) — or, with
167
+ * {@link ReasoningWireFacts} supplied, the anthropic budget path's cap-wins skip
168
+ * ({@link budgetCapSkipsThinking}), so NO thinking
83
169
  * parameter reaches the wire at all — the requested tier is DROPPED entirely, not clamped or
84
170
  * downgraded-to-binary. `effective:"off"` here states the ENGINE side of that fact (nothing was
85
171
  * requested), NOT a measured gateway state: on the binary enable-only formats (qwen / zai /
@@ -101,7 +187,10 @@ export interface ResolvedReasoning extends ReasoningResolution {
101
187
  * brains keep calling {@link resolveEffort}/{@link resolveBinary}/{@link reasoningBudgetShare} on the hot path.
102
188
  *
103
189
  * - Anthropic (`api === "anthropic-messages"`) → budget-based: the tier sets a budget share, so it's a real
104
- * gradient (`graded:true`) and never tier-clamped (`clamped:false`); reported as `format:"budget"`.
190
+ * gradient (`graded:true`) and never tier-clamped (`clamped:false`); reported as `format:"budget"`. With
191
+ * the optional {@link ReasoningWireFacts} the arm additionally mirrors the wire's cap-wins skip
192
+ * ({@link budgetCapSkipsThinking}: a hard per-request output cap < 2·{@link MIN_THINKING_TOKENS} deletes
193
+ * the thinking block) as the drop shape — facts absent keeps the historic cap-blind gradient.
105
194
  * - Binary enable-only formats (qwen / zai / qwen-chat-template) → `graded:false` (tier not honored).
106
195
  * - An effort endpoint with `supportsReasoningEffort:false` → `graded:false` (enable key only, no effort tier).
107
196
  * - Otherwise effort-based → clamp DOWN to the endpoint's `reasoningEffortLevels` (default minimal|low|medium|high).
@@ -111,11 +200,14 @@ export interface ResolvedReasoning extends ReasoningResolution {
111
200
  * honored. Previously this resolver never read the map and reported such a request as exactly honored
112
201
  * (`graded:true`, `clamped:false`) while the wire dropped the value — trace/result-face drift.
113
202
  *
114
- * - A model whose `reasoning` capability flag is FALSY drops the request ENTIRELY (neither brain emits any
115
- * thinking parameter, whatever the format) `effective:"off"`, `graded:false`, `clamped:true`,
203
+ * - A request that never CARRIES ({@link reasoningRequestCarried} false on a non-"off" value: the model's
204
+ * `reasoning` capability flag is FALSY, or the requested value is not a valid tier — no brain emits any
205
+ * thinking parameter for either, whatever the format) → `effective:"off"`, `graded:false`, `clamped:true`,
116
206
  * `dropped:true` — the loud-drop arm. Previously this resolver described the capability dispatch for such
117
207
  * a model (a resolution the request never carried), and the runner's trace guard skipped the frame — the
118
- * one arm where the request evaporates was the one arm with no disclosure.
208
+ * one arm where the request evaporates was the one arm with no disclosure; and an out-of-contract tier
209
+ * was worse still — reported as a clamped-to-minimal gradient (or echoed verbatim as `effective` on the
210
+ * binary formats) while the wire carried nothing.
119
211
  *
120
212
  * `off`/falsy never enables thinking, so it resolves trivially (no clamp, graded:true) — the caller decides
121
213
  * whether to emit at all.
@@ -125,7 +217,7 @@ export declare function resolveReasoning(requested: ThinkingLevel, model: {
125
217
  reasoning?: boolean;
126
218
  compat?: unknown;
127
219
  thinkingLevelMap?: Readonly<Partial<Record<ThinkingLevel, string | null>>>;
128
- }): ResolvedReasoning;
220
+ }, facts?: ReasoningWireFacts): ResolvedReasoning;
129
221
  /**
130
222
  * Resolve a requested intensity for an effort-based endpoint (`reasoning_effort` / `reasoning.effort`). Picks
131
223
  * the requested tier when supported; otherwise the highest supported tier ≤ requested (clamp DOWN, never
@@ -10,28 +10,52 @@ const RANK = {
10
10
  export function isThinkingLevel(v) {
11
11
  return typeof v === "string" && Object.prototype.hasOwnProperty.call(RANK, v);
12
12
  }
13
+ export function reasoningRequestCarried(model, reasoning) {
14
+ return !!model.reasoning && isThinkingLevel(reasoning) && reasoning !== "off";
15
+ }
13
16
  export function rankOf(level) {
14
17
  return RANK[level];
15
18
  }
16
19
  export const DEFAULT_EFFORT_LEVELS = ["minimal", "low", "medium", "high"];
17
20
  const BINARY_FORMATS = new Set(["qwen", "zai", "qwen-chat-template"]);
18
21
  export const RESPONSES_APIS = new Set(["openai-responses", "azure-openai-responses", "openai-chatgpt-responses"]);
19
- export function resolveReasoning(requested, model) {
20
- const resolved = dispatchReasoning(requested, model);
21
- if (requested !== "off" && !model.reasoning) {
22
+ export const MIN_THINKING_TOKENS = 1024;
23
+ export function budgetCapSkipsThinking(outputCapTokens, hardCap) {
24
+ return hardCap && outputCapTokens < MIN_THINKING_TOKENS * 2;
25
+ }
26
+ export function declaredEffortLevels(v) {
27
+ return Array.isArray(v) && v.length > 0 ? v : undefined;
28
+ }
29
+ export function mintEffortWireValue(requested, model, allowed) {
30
+ const resolution = resolveEffort(requested, allowed ?? DEFAULT_EFFORT_LEVELS);
31
+ let wireValue = resolution.effective;
32
+ const mapped = model.thinkingLevelMap?.[resolution.effective];
33
+ if (mapped === null)
34
+ wireValue = undefined;
35
+ else if (mapped !== undefined)
36
+ wireValue = mapped;
37
+ return { resolution, wireValue };
38
+ }
39
+ export function resolveReasoning(requested, model, facts) {
40
+ const resolved = dispatchReasoning(requested, model, facts);
41
+ if (requested !== "off" && !reasoningRequestCarried(model, requested)) {
22
42
  return { requested, effective: "off", graded: false, clamped: true, format: resolved.format, endpoint: resolved.endpoint, dropped: true };
23
43
  }
24
44
  return resolved;
25
45
  }
26
- function effortTierUnmapped(requested, effective, model) {
27
- return requested !== "off" && model.thinkingLevelMap?.[effective] === null;
28
- }
29
- function dispatchReasoning(requested, model) {
46
+ function dispatchReasoning(requested, model, facts) {
30
47
  const endpoint = model.api ?? "unknown";
31
48
  const compat = (model.compat ?? {});
32
49
  if (model.api === "anthropic-messages") {
33
- if (compat.effortLevels && compat.effortLevels.length > 0) {
34
- return { ...resolveEffort(requested, compat.effortLevels), format: "effort", endpoint };
50
+ const declaredAnthropic = declaredEffortLevels(compat.effortLevels);
51
+ if (declaredAnthropic !== undefined) {
52
+ return { ...resolveEffort(requested, declaredAnthropic), format: "effort", endpoint };
53
+ }
54
+ if (requested !== "off" &&
55
+ compat.thinkingMode !== "adaptive" &&
56
+ facts !== undefined &&
57
+ budgetCapSkipsThinking(facts.outputCapTokens, facts.hardOutputCap)) {
58
+ return { requested, effective: "off", graded: false, clamped: true, format: "budget", endpoint, dropped: true };
35
59
  }
36
60
  return { requested, effective: requested, graded: true, clamped: false, format: "budget", endpoint };
37
61
  }
@@ -39,11 +63,11 @@ function dispatchReasoning(requested, model) {
39
63
  if (compat.supportsReasoningEffort === false) {
40
64
  return { requested, effective: requested, graded: false, clamped: false, format: "responses", endpoint };
41
65
  }
42
- const responsesResolved = resolveEffort(requested, compat.reasoningEffortLevels ?? DEFAULT_EFFORT_LEVELS);
43
- if (effortTierUnmapped(requested, responsesResolved.effective, model)) {
66
+ const responsesMint = mintEffortWireValue(requested, model, compat.reasoningEffortLevels);
67
+ if (requested !== "off" && responsesMint.wireValue === undefined) {
44
68
  return { requested, effective: requested, graded: false, clamped: false, format: "responses", endpoint };
45
69
  }
46
- return { ...responsesResolved, format: "responses", endpoint };
70
+ return { ...responsesMint.resolution, format: "responses", endpoint };
47
71
  }
48
72
  const format = compat.thinkingFormat ?? "openai";
49
73
  if (BINARY_FORMATS.has(format)) {
@@ -53,11 +77,11 @@ function dispatchReasoning(requested, model) {
53
77
  if (!supportsEffort && format !== "openrouter") {
54
78
  return { requested, effective: requested, graded: false, clamped: false, format, endpoint };
55
79
  }
56
- const resolved = resolveEffort(requested, compat.reasoningEffortLevels ?? DEFAULT_EFFORT_LEVELS);
57
- if (effortTierUnmapped(requested, resolved.effective, model)) {
80
+ const mint = mintEffortWireValue(requested, model, compat.reasoningEffortLevels);
81
+ if (requested !== "off" && mint.wireValue === undefined) {
58
82
  return { requested, effective: requested, graded: false, clamped: false, format, endpoint };
59
83
  }
60
- return { ...resolved, format, endpoint };
84
+ return { ...mint.resolution, format, endpoint };
61
85
  }
62
86
  export function resolveEffort(requested, allowed = DEFAULT_EFFORT_LEVELS) {
63
87
  const declared = Array.isArray(allowed) ? allowed.filter((lvl) => isThinkingLevel(lvl) && lvl !== "off") : [];
@@ -23,7 +23,7 @@ export declare function reservedFor(api: string): ReadonlySet<string>;
23
23
  */
24
24
  export declare function applyExtraBody(body: Record<string, unknown>, extraBody: Record<string, unknown> | undefined, reserved: ReadonlySet<string>): Record<string, unknown>;
25
25
  /**
26
- * [1282] per-call auth REPLACES construction-time auth: drop every auth-bearing header
26
+ * Per-call auth REPLACES construction-time auth: drop every auth-bearing header
27
27
  * (case-insensitive `authorization` / `x-api-key`) from an already-merged header bag. Called by a
28
28
  * brain's buildRequest ONLY when a per-call `options.apiKey` is present — the brain then re-emits
29
29
  * the credential in its own wire posture (anthropic `x-api-key`, openai `Bearer`), making the
@@ -33,6 +33,42 @@ export declare function applyExtraBody(body: Record<string, unknown>, extraBody:
33
33
  * folds duplicates into one comma-joined value — broken auth both ways).
34
34
  */
35
35
  export declare function stripAuthHeaders(headers: Record<string, string>): void;
36
+ /**
37
+ * #343 — the shared USER-HEADER merge layer (`model.headers` → construction `config.headers` →
38
+ * per-call `options.headers`, later bag wins), CASE-FOLD deduplicated: HTTP header field names are
39
+ * case-insensitive (RFC 9110), but the plain-object spread the three brains used
40
+ * (`{...model.headers, ...config.headers, ...options.headers}`) keyed by exact spelling — a
41
+ * `X-Tenant` in one bag and `x-tenant` in another BOTH survived and both went on the wire, where
42
+ * fetch's Headers folds them into one comma-joined value ("a, b"): neither writer's value, and the
43
+ * later layer's documented override silently defeated. Now a later bag's entry replaces an earlier
44
+ * case-variant; the WINNER'S spelling and value survive (a single-spelling config — every existing
45
+ * deployment — is byte-identical on the wire).
46
+ *
47
+ * EXEMPT: the auth carriers (`authorization` / `x-api-key`, any case) pass through with the exact
48
+ * legacy spread semantics (same-spelling override only, no case-fold dedup) — their case handling is
49
+ * {@link stripAuthHeaders}' pinned jurisdiction (the per-call-replaces flow and the
50
+ * header-only ANTHROPIC_AUTH_TOKEN shape, which must survive under its own capital-A spelling), and
51
+ * this layer must not become a second, subtly different auth authority.
52
+ */
53
+ /**
54
+ * #343 (review r4) — assign a STRUCTURAL locked header under its canonical lowercase name, deleting
55
+ * every case-variant spelling first. The brains hard-lock `content-type` / `anthropic-version` AFTER
56
+ * the user-bag merge precisely so they "can NEVER be overridden" (council design/40) — but a valid
57
+ * user bag carrying `Content-Type: text/plain` survived BESIDE the lowercase lock, and the platform
58
+ * `Headers` fold turns the pair into `text/plain, application/json` on the wire: the lock decided
59
+ * nothing. Auth carriers are deliberately NOT routed through here (per-call replacement + the
60
+ * header-only boot flow are {@link stripAuthHeaders}' pinned jurisdiction).
61
+ */
62
+ export declare function lockHeader(headers: Record<string, string>, lowerName: string, value: string): void;
63
+ /**
64
+ * #343 (review r4) — case-fold READ-AND-CLAIM for an AUGMENTABLE structural header (`anthropic-beta`):
65
+ * returns the current value under whatever spelling the user bag carried and deletes that spelling,
66
+ * so the caller's canonical lowercase write REPLACES it instead of duplicating beside it (the
67
+ * read-modify-write used to key the read by the exact lowercase name and miss `Anthropic-Beta`,
68
+ * losing the user's betas from the merge AND double-sending the header).
69
+ */
70
+ export declare function takeHeaderCasefold(headers: Record<string, string>, lowerName: string): string | undefined;
71
+ export declare function mergeHeaders(...bags: Array<Record<string, string> | undefined>): Record<string, string>;
36
72
  /**
37
73
  * The output-cap key(s) each lane's wire form uses. A lane's set is exactly the keys THAT lane's
38
74
  * endpoint reads — a stray cap key belonging to another wire form is inert there and must not be
@@ -54,13 +54,51 @@ export function applyExtraBody(body, extraBody, reserved) {
54
54
  }
55
55
  return { ...passthrough, ...body };
56
56
  }
57
+ const AUTH_CARRIER_NAMES = new Set(["authorization", "x-api-key"]);
57
58
  export function stripAuthHeaders(headers) {
58
59
  for (const k of Object.keys(headers)) {
59
- const lower = k.toLowerCase();
60
- if (lower === "authorization" || lower === "x-api-key")
60
+ if (AUTH_CARRIER_NAMES.has(k.toLowerCase()))
61
61
  delete headers[k];
62
62
  }
63
63
  }
64
+ export function lockHeader(headers, lowerName, value) {
65
+ for (const k of Object.keys(headers)) {
66
+ if (k !== lowerName && k.toLowerCase() === lowerName)
67
+ delete headers[k];
68
+ }
69
+ headers[lowerName] = value;
70
+ }
71
+ export function takeHeaderCasefold(headers, lowerName) {
72
+ for (const k of Object.keys(headers)) {
73
+ if (k.toLowerCase() === lowerName) {
74
+ const v = headers[k];
75
+ delete headers[k];
76
+ return v;
77
+ }
78
+ }
79
+ return undefined;
80
+ }
81
+ export function mergeHeaders(...bags) {
82
+ const out = {};
83
+ const spellingByFold = new Map();
84
+ for (const bag of bags) {
85
+ if (!bag)
86
+ continue;
87
+ for (const [name, value] of Object.entries(bag)) {
88
+ const fold = name.toLowerCase();
89
+ if (AUTH_CARRIER_NAMES.has(fold)) {
90
+ out[name] = value;
91
+ continue;
92
+ }
93
+ const prior = spellingByFold.get(fold);
94
+ if (prior !== undefined && prior !== name)
95
+ delete out[prior];
96
+ spellingByFold.set(fold, name);
97
+ out[name] = value;
98
+ }
99
+ }
100
+ return out;
101
+ }
64
102
  export const OUTPUT_CAP_KEYS = {
65
103
  openai: ["max_tokens", "max_completion_tokens"],
66
104
  anthropic: ["max_tokens"],
@@ -170,7 +170,7 @@ export interface BackgroundAgentRecord {
170
170
  export declare const REVIVED_ROW_CLEARED_FIELDS: readonly ["settledAt", "stoppedBy", "completionId", "finalOutput", "finalOutputFull", "error", "errorCode", "errorRetryable", "errorKind", "errorRetryAfterMs", "resultIsPartial", "summary", "recentSteps", "editedFiles", "usage"];
171
171
  /** Erase {@link REVIVED_ROW_CLEARED_FIELDS} from a record a revival is about to write back. */
172
172
  export declare function clearRevivedRowTerminalPayload(record: BackgroundAgentRecord): void;
173
- export declare function announceTranscriptIntegrityGapOnce(handle: string, sink: ((handle: string) => void) | undefined): void;
173
+ export declare function announceTranscriptIntegrityGapOnce(handle: string, scope: string | undefined, sink: ((handle: string, scope: string | undefined) => void) | undefined): void;
174
174
  /** Content-free projection for list reads (design/151 HIGH-1: `summary`/`finalOutput`/`recentSteps`
175
175
  * and friends NEVER ride a list — content is get-by-handle only, behind the full predicate). */
176
176
  export interface BackgroundAgentRowSummary {
@@ -22,14 +22,15 @@ export function clearRevivedRowTerminalPayload(record) {
22
22
  delete record[field];
23
23
  }
24
24
  const transcriptIntegrityAnnounced = new Set();
25
- export function announceTranscriptIntegrityGapOnce(handle, sink) {
25
+ export function announceTranscriptIntegrityGapOnce(handle, scope, sink) {
26
26
  if (sink === undefined)
27
27
  return;
28
- if (transcriptIntegrityAnnounced.has(handle))
28
+ const key = `${scope ?? ""}\u0000${handle}`;
29
+ if (transcriptIntegrityAnnounced.has(key))
29
30
  return;
30
- transcriptIntegrityAnnounced.add(handle);
31
+ transcriptIntegrityAnnounced.add(key);
31
32
  try {
32
- sink(handle);
33
+ sink(handle, scope);
33
34
  }
34
35
  catch {
35
36
  }
@@ -193,7 +193,10 @@ export interface McpRefreshResult {
193
193
  * DOMAIN for the swap (review F4: a prefix domain is self-healing and decoupled from the diff
194
194
  * baseline, which advances even when a consumer skips a swap). Present on every entry. */
195
195
  prefix: string;
196
- status: "refreshed" | "not_connected" | "failed";
196
+ /** `"revoked"` (design/338): the host ledger marks this server revoked — the refresh did NOT
197
+ * contact it (no tools/list round trip; the engine must not hand a severed server a request
198
+ * channel or splice its post-revocation text into the model catalog). */
199
+ status: "refreshed" | "not_connected" | "failed" | "revoked";
197
200
  toolCount: number;
198
201
  added: string[];
199
202
  removed: string[];
@@ -467,6 +470,9 @@ export declare function materializeMcpTools(specs: McpServerSpec[], principal?:
467
470
  reminderDisclosure?: {
468
471
  reminderMark?: string;
469
472
  counts?: ReminderDisclosureCounts;
473
+ }, mcpRevocations?: {
474
+ isRevoked(serverName: string): boolean;
475
+ onProbeFailure?: (error: unknown) => void;
470
476
  }): Promise<MaterializedMcp>;
471
477
  /**
472
478
  * Fold the caller's AUTHORITATIVE per-tool override (design F: caller = trust root) over the server-hint axis.