modelrig 0.3.0 → 0.5.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 (111) hide show
  1. package/CHANGELOG.md +269 -0
  2. package/README.md +79 -14
  3. package/dist/adapters/deepseek.d.ts.map +1 -1
  4. package/dist/adapters/deepseek.js +8 -1
  5. package/dist/adapters/gemini.d.ts +45 -0
  6. package/dist/adapters/gemini.d.ts.map +1 -1
  7. package/dist/adapters/gemini.js +67 -6
  8. package/dist/adapters/grok.d.ts +7 -0
  9. package/dist/adapters/grok.d.ts.map +1 -1
  10. package/dist/adapters/grok.js +23 -1
  11. package/dist/adapters/openai-compat.d.ts +7 -0
  12. package/dist/adapters/openai-compat.d.ts.map +1 -1
  13. package/dist/adapters/openai-compat.js +24 -1
  14. package/dist/adapters/openai.d.ts +7 -0
  15. package/dist/adapters/openai.d.ts.map +1 -1
  16. package/dist/adapters/openai.js +23 -1
  17. package/dist/artifacts/artifact.d.ts +3 -2
  18. package/dist/artifacts/artifact.d.ts.map +1 -1
  19. package/dist/artifacts/artifact.js +11 -4
  20. package/dist/artifacts/run-context.d.ts +21 -2
  21. package/dist/artifacts/run-context.d.ts.map +1 -1
  22. package/dist/artifacts/run-context.js +46 -1
  23. package/dist/bakeoff/replay.d.ts.map +1 -1
  24. package/dist/bakeoff/replay.js +17 -1
  25. package/dist/cli.d.ts.map +1 -1
  26. package/dist/cli.js +26 -5
  27. package/dist/config.d.ts +7 -1
  28. package/dist/config.d.ts.map +1 -1
  29. package/dist/index.d.ts +21 -15
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +39 -26
  32. package/dist/init/init-cli.d.ts.map +1 -1
  33. package/dist/init/init-cli.js +2 -1
  34. package/dist/init/observe-cli.d.ts.map +1 -1
  35. package/dist/init/observe-cli.js +2 -1
  36. package/dist/registry/select.d.ts +59 -0
  37. package/dist/registry/select.d.ts.map +1 -1
  38. package/dist/registry/select.js +66 -0
  39. package/dist/routes/loader-warnings.d.ts +28 -0
  40. package/dist/routes/loader-warnings.d.ts.map +1 -0
  41. package/dist/routes/loader-warnings.js +56 -0
  42. package/dist/routes/loader.d.ts +1 -11
  43. package/dist/routes/loader.d.ts.map +1 -1
  44. package/dist/routes/loader.js +44 -26
  45. package/dist/routes/sampling-config.d.ts.map +1 -1
  46. package/dist/routes/sampling-config.js +11 -0
  47. package/dist/run/dispatch.d.ts +14 -2
  48. package/dist/run/dispatch.d.ts.map +1 -1
  49. package/dist/run/dispatch.js +31 -2
  50. package/dist/run/ladder-key.d.ts +40 -0
  51. package/dist/run/ladder-key.d.ts.map +1 -0
  52. package/dist/run/ladder-key.js +39 -0
  53. package/dist/run/raw-defaults.d.ts +52 -0
  54. package/dist/run/raw-defaults.d.ts.map +1 -0
  55. package/dist/run/raw-defaults.js +123 -0
  56. package/dist/run/raw-support.d.ts +123 -0
  57. package/dist/run/raw-support.d.ts.map +1 -0
  58. package/dist/run/raw-support.js +150 -0
  59. package/dist/run/raw.d.ts +53 -11
  60. package/dist/run/raw.d.ts.map +1 -1
  61. package/dist/run/raw.js +157 -31
  62. package/dist/run/rawonly.d.ts +60 -0
  63. package/dist/run/rawonly.d.ts.map +1 -0
  64. package/dist/run/rawonly.js +120 -0
  65. package/dist/run/record-attempt.d.ts +14 -1
  66. package/dist/run/record-attempt.d.ts.map +1 -1
  67. package/dist/run/record-attempt.js +43 -2
  68. package/dist/run/resolve.d.ts +21 -1
  69. package/dist/run/resolve.d.ts.map +1 -1
  70. package/dist/run/resolve.js +29 -6
  71. package/dist/run/routing-docs.d.ts +73 -0
  72. package/dist/run/routing-docs.d.ts.map +1 -0
  73. package/dist/run/routing-docs.js +222 -0
  74. package/dist/run/run.d.ts +7 -1
  75. package/dist/run/run.d.ts.map +1 -1
  76. package/dist/run/run.js +38 -55
  77. package/dist/run/variant.d.ts +43 -0
  78. package/dist/run/variant.d.ts.map +1 -0
  79. package/dist/run/variant.js +86 -0
  80. package/dist/savings/emit.d.ts +33 -0
  81. package/dist/savings/emit.d.ts.map +1 -1
  82. package/dist/savings/emit.js +75 -3
  83. package/dist/telemetry/exporter.d.ts.map +1 -1
  84. package/dist/telemetry/exporter.js +6 -0
  85. package/dist/telemetry/ingest-protocol.d.ts +7 -0
  86. package/dist/telemetry/ingest-protocol.d.ts.map +1 -1
  87. package/dist/telemetry/load-error-report.d.ts.map +1 -1
  88. package/dist/telemetry/load-error-report.js +4 -1
  89. package/dist/telemetry/otel-map.d.ts.map +1 -1
  90. package/dist/telemetry/otel-map.js +8 -0
  91. package/dist/telemetry/store-migrations.d.ts +15 -0
  92. package/dist/telemetry/store-migrations.d.ts.map +1 -0
  93. package/dist/telemetry/store-migrations.js +119 -0
  94. package/dist/telemetry/store.d.ts.map +1 -1
  95. package/dist/telemetry/store.js +26 -93
  96. package/dist/types/failures-teaching.js +1 -1
  97. package/dist/types/provider.d.ts +13 -0
  98. package/dist/types/provider.d.ts.map +1 -1
  99. package/dist/types/route.d.ts +37 -1
  100. package/dist/types/route.d.ts.map +1 -1
  101. package/dist/types/telemetry.d.ts +31 -0
  102. package/dist/types/telemetry.d.ts.map +1 -1
  103. package/dist/validate-cli.d.ts.map +1 -1
  104. package/dist/validate-cli.js +17 -4
  105. package/dist/watch/evaluate.d.ts +27 -1
  106. package/dist/watch/evaluate.d.ts.map +1 -1
  107. package/dist/watch/evaluate.js +13 -0
  108. package/dist/watch/proposals.d.ts +28 -0
  109. package/dist/watch/proposals.d.ts.map +1 -1
  110. package/dist/watch/proposals.js +51 -0
  111. package/package.json +1 -1
package/dist/run/raw.js CHANGED
@@ -29,10 +29,18 @@
29
29
  * Each is ADDITIVE: absent ⇒ byte-identical dispatch. A DECLARED directive is
30
30
  * never silently dropped — `grounding`/`reasoning` on a provider that cannot
31
31
  * honor it are rejected fail-closed PRE-DISPATCH (`assertRawKnobsSupported`),
32
- * with no meter, no provider call, and no telemetry row. v1 support set for
33
- * both = { gemini }. `serviceTier` forwards to every adapter's tier path and
34
- * `meta.requestedTier` now reports the DECLARED tier (an honesty fix — it
35
- * previously hardcoded `"standard"`).
32
+ * with no meter, no provider call, and no telemetry row. `serviceTier`
33
+ * forwards to every adapter's tier path and `meta.requestedTier` now reports
34
+ * the DECLARED tier (an honesty fix — it previously hardcoded `"standard"`).
35
+ * - CE-9: multi-provider parity. `grounding` now also serves grok (xAI Live
36
+ * Search); `reasoning` generalizes beyond gemini WITHOUT a per-model
37
+ * allowlist — openai/deepinfra/fireworks forward `level` → `reasoning_effort`
38
+ * verbatim (provider enforces, rejection classified teachably), grok coarsens
39
+ * (disclosed), gemini stays model-gated, deepseek/anthropic fail closed; a
40
+ * new `responseFormat: "json_object"` knob mime-shapes on the OpenAI-dialect
41
+ * providers + gemini (anthropic fails closed). The provider×knob support set
42
+ * is ONE table (`run/raw-support.ts`) the guard AND the published docs matrix
43
+ * both read — they can never disagree (R5).
36
44
  */
37
45
  Object.defineProperty(exports, "__esModule", { value: true });
38
46
  exports.assertRawKnobsSupported = assertRawKnobsSupported;
@@ -47,11 +55,37 @@ const grok_1 = require("../adapters/grok");
47
55
  const openai_1 = require("../adapters/openai");
48
56
  const openai_compat_1 = require("../adapters/openai-compat");
49
57
  const cost_1 = require("./cost");
58
+ const raw_defaults_1 = require("./raw-defaults");
50
59
  const resolve_1 = require("./resolve");
60
+ const raw_support_1 = require("./raw-support");
51
61
  const failures_1 = require("../types/failures");
52
- /** Absent a route timeout, the raw lane uses a generous default — a passthrough
53
- * should not be shorter-fused than the underlying provider SDK. */
54
- const DEFAULT_RAW_TIMEOUT_MS = 120000;
62
+ const run_context_1 = require("../artifacts/run-context");
63
+ const manifest_1 = require("../routes/manifest");
64
+ /**
65
+ * R1 — resolve the effective raw-lane timeout. An explicit `timeoutMs` always
66
+ * wins; absent, the fallback is TIER-AWARE (`RAW_TIMEOUT_DEFAULT_MS`), because a
67
+ * passthrough should not be shorter-fused than the tier it advertises (a flex
68
+ * grounded prompt needs minutes, not the old 120 s). No pre-dispatch rejection —
69
+ * a short explicit timeout is a choice, and the teaching arrives on the first
70
+ * `timeout` failure (`rawTimeoutFixHint`), which is when the caller is looking.
71
+ */
72
+ function resolveRawTimeoutMs(input) {
73
+ return input.timeoutMs ?? raw_defaults_1.RAW_TIMEOUT_DEFAULT_MS[input.serviceTier ?? "standard"];
74
+ }
75
+ /** R1 — the teaching `fixHint` attached to a `timeout` failure on the raw lane:
76
+ * it names the elapsed budget, the tier, that tier's advertised target, and the
77
+ * two fixes (raise `timeoutMs`, or drop to `standard`). Built from
78
+ * `RAW_TIMEOUT_DEFAULT_MS` so the default it quotes can never drift. */
79
+ function rawTimeoutFixHint(input, tier) {
80
+ const deadlineSec = resolveRawTimeoutMs(input) / 1000;
81
+ const budgetMin = raw_defaults_1.RAW_TIMEOUT_DEFAULT_MS[tier] / 60000;
82
+ const defaultSec = raw_defaults_1.RAW_TIMEOUT_DEFAULT_MS[tier] / 1000;
83
+ // Only flex has a cheaper-latency tier to drop to; at standard/priority the
84
+ // only fix is a longer deadline (never tell a caller to switch to the tier
85
+ // they are already on).
86
+ const tail = tier === "flex" ? ' or use serviceTier: "standard"' : "";
87
+ return `timed out at the ${deadlineSec}s deadline at tier ${tier} — ${tier} targets up to ${budgetMin} min; raise timeoutMs (default ${defaultSec}s)${tail}`;
88
+ }
55
89
  /** Conservative compat-host capability baseline — parity with createRig's
56
90
  * COMPAT_STATIC. Inconsequential for raw calls (outputSchema is always null, so
57
91
  * no JSON shaping fires), kept aligned so a raw and a routed compat call shape
@@ -86,23 +120,29 @@ function buildRawAdapter(provider, apiKey) {
86
120
  }
87
121
  }
88
122
  }
89
- /** Providers that can honor a DECLARED raw-lane directive in v1 (spec §3
90
- * fail-closed matrix). Native grounding and reasoning are both gemini-only
91
- * today; the other adapters would silently ignore the field — exactly the bug
92
- * class CE-2 makes impossible. A new-provider entry here is a deliberate,
93
- * spec-gated widening, never an accident. */
94
- const NATIVE_GROUNDING_PROVIDERS = new Set(["gemini"]);
95
- const REASONING_PROVIDERS = new Set(["gemini"]);
96
123
  /**
97
- * CE-2 fail-closed guard (spec §3 F1/F3). A DECLARED directive is NEVER silently
98
- * dropped: `grounding`/`reasoning` on a provider that cannot honor it — or a
99
- * `grounding.mode` other than the v1-supported `"native"` — throws
100
- * `RigFailureError` `invariant_violation` (the existing TERMINAL class; no new
101
- * FailureClass member, spec §3 rationale) BEFORE any adapter is built, any
102
- * provider call is made, or any telemetry row is written. Exported so the guard
103
- * is unit-testable without a network. Idempotent and side-effect-free.
124
+ * CE-2/CE-9 fail-closed guard (spec §3 F1/F3, CE-9 R1/R2/R4). A DECLARED
125
+ * directive is NEVER silently dropped: a `grounding`/`responseFormat` on a
126
+ * provider that cannot honor it, a `reasoning` on a provider with NO effort
127
+ * control (deepseek/anthropic), or a `grounding.mode` other than the
128
+ * v1-supported `"native"` throws `RigFailureError` `invariant_violation` (the
129
+ * existing TERMINAL class; no new FailureClass member) BEFORE any adapter is
130
+ * built, any provider call is made, or any telemetry row is written.
131
+ *
132
+ * The support facts come from `RAW_KNOB_SUPPORT` (run/raw-support.ts) — the ONE
133
+ * table the published docs matrix also reads, so the guard and the docs can
134
+ * never disagree (R5). What the guard does NOT reject (and why):
135
+ * - reasoning on openai/deepinfra/fireworks is FORWARDED verbatim
136
+ * (`kind: "forward"`) and the provider enforces — a rejection is classified
137
+ * teachably by the adapter (no per-model allowlist, R1).
138
+ * - reasoning on grok is a DISCLOSED coarsening (`kind: "coarsen"`).
139
+ * - reasoning on gemini is model-gated (`kind: "model-gated"`) — the adapter
140
+ * rejects a non-thinking model (`geminiReasoningUnsupported`).
141
+ * Exported so the guard is unit-testable without a network. Idempotent and
142
+ * side-effect-free.
104
143
  */
105
144
  function assertRawKnobsSupported(input) {
145
+ const support = raw_support_1.RAW_KNOB_SUPPORT[input.provider];
106
146
  const groundingMode = input.grounding?.mode;
107
147
  if (groundingMode !== undefined) {
108
148
  if (groundingMode !== "native") {
@@ -114,23 +154,35 @@ function assertRawKnobsSupported(input) {
114
154
  fixHint: 'only { mode: "native" } grounding is supported on the raw lane in v1',
115
155
  });
116
156
  }
117
- if (!NATIVE_GROUNDING_PROVIDERS.has(input.provider)) {
157
+ if (!support.grounding.supported) {
118
158
  throw new failures_1.RigFailureError({
119
159
  class: "invariant_violation",
120
160
  message: `native grounding on the raw lane is not supported for provider "${input.provider}"`,
121
161
  provider: input.provider,
122
162
  model: input.model,
123
- fixHint: "native grounding on the raw lane is gemini-only in v1; remove grounding or switch provider",
163
+ fixHint: `native grounding on the raw lane is supported on ${(0, raw_support_1.groundingSupportedProviders)().join(", ")} only; remove grounding or switch provider`,
124
164
  });
125
165
  }
126
166
  }
127
- if (input.reasoning !== undefined && !REASONING_PROVIDERS.has(input.provider)) {
167
+ if (input.reasoning !== undefined && support.reasoning.kind === "fail-closed") {
128
168
  throw new failures_1.RigFailureError({
129
169
  class: "invariant_violation",
130
170
  message: `reasoning control on the raw lane is not supported for provider "${input.provider}"`,
131
171
  provider: input.provider,
132
172
  model: input.model,
133
- fixHint: "reasoning control on the raw lane is gemini-only in v1; remove reasoning or switch provider",
173
+ // Provider-specific teaching straight from the matrix (deepseek →
174
+ // deepseek-reasoner; anthropic → extended thinking unwired).
175
+ fixHint: support.reasoning.fixHint ??
176
+ "reasoning control is not available for this provider on the raw lane; remove reasoning",
177
+ });
178
+ }
179
+ if (input.responseFormat !== undefined && !support.responseFormat.supported) {
180
+ throw new failures_1.RigFailureError({
181
+ class: "invariant_violation",
182
+ message: `responseFormat on the raw lane is not supported for provider "${input.provider}"`,
183
+ provider: input.provider,
184
+ model: input.model,
185
+ fixHint: `responseFormat: "json_object" on the raw lane is supported on ${(0, raw_support_1.responseFormatSupportedProviders)().join(", ")} only; remove responseFormat or switch provider`,
134
186
  });
135
187
  }
136
188
  }
@@ -154,7 +206,8 @@ function buildRawRequest(candidate, input) {
154
206
  // CE-2 F2: forward the DECLARED tier (default "standard"); the meta/row
155
207
  // honesty fix in finalizeRaw reports this same value, never a hardcode.
156
208
  requestedTier: input.serviceTier ?? "standard",
157
- timeoutMs: input.timeoutMs ?? DEFAULT_RAW_TIMEOUT_MS,
209
+ // R1: tier-aware default (RAW_TIMEOUT_DEFAULT_MS); explicit timeoutMs wins.
210
+ timeoutMs: resolveRawTimeoutMs(input),
158
211
  ...(input.maxOutputTokens !== undefined ? { maxOutputTokens: input.maxOutputTokens } : {}),
159
212
  ...(sampling !== undefined ? { sampling } : {}),
160
213
  // CE-2 F1: `{ mode: "native" }` → the adapter's native-grounding directive.
@@ -163,6 +216,10 @@ function buildRawRequest(candidate, input) {
163
216
  ...(input.grounding?.mode === "native" ? { grounding: true } : {}),
164
217
  // CE-2 F3: forward reasoning verbatim; absent ⇒ no key added (byte-identical).
165
218
  ...(input.reasoning !== undefined ? { reasoning: input.reasoning } : {}),
219
+ // CE-9 R4: forward the json_object directive verbatim; absent ⇒ no key added
220
+ // (byte-identical). An unsupported provider (anthropic) never reaches here —
221
+ // the guard (assertRawKnobsSupported) rejects it pre-dispatch.
222
+ ...(input.responseFormat !== undefined ? { responseFormat: input.responseFormat } : {}),
166
223
  ...(input.tools !== undefined ? { tools: input.tools } : {}),
167
224
  ...(input.toolChoice !== undefined ? { toolChoice: input.toolChoice } : {}),
168
225
  ...(input.cache !== undefined ? { cache: input.cache } : {}),
@@ -193,7 +250,10 @@ function recordRawRow(telemetry, p) {
193
250
  ttfbMs: p.usage?.ttfbMs ?? null,
194
251
  failureClass: p.failureClass,
195
252
  attemptsJson: "{}",
196
- tagsJson: JSON.stringify(p.tags),
253
+ // Rung tag (R1 review, F1): the raw lane is a single primary dispatch. Raw
254
+ // rows carry `lane: "raw"` and are excluded from route rollups, but every row
255
+ // gets a rung for a uniform contract.
256
+ tagsJson: JSON.stringify({ ...p.tags, rung: "primary" }),
197
257
  fingerprintCell: null,
198
258
  servedVariant: null,
199
259
  repairedBy: null,
@@ -203,22 +263,66 @@ function recordRawRow(telemetry, p) {
203
263
  // provenance rule reads any non-null key here as `customer`.
204
264
  cacheKey: p.cacheKey ?? null,
205
265
  prefixFingerprint: null,
266
+ // Ladder key (spec §2.1 R0): raw lane = one row per call, seq 1, index 0.
267
+ callId: p.callId,
268
+ attemptSeq: 1,
269
+ candidateIndex: 0,
206
270
  };
207
271
  telemetry.record(row);
208
272
  }
273
+ /**
274
+ * Read the ambient run context at the ENTRY of a raw call (spec §4) and, if
275
+ * present, resolve the step key `input.stepKey ?? tags.step ?? provider/model#n`
276
+ * (validated) — so `finalizeRaw` records a ground-truth step exactly like the
277
+ * routed lane. Also runs the required-dimension hygiene warning (spec §4),
278
+ * unconditionally, as the routed lane does. Fail-open: no context ⇒ null and no
279
+ * step is ever recorded; the dispatch is byte-identical to pre-G1.
280
+ */
281
+ function prepareRawStep(deps, input) {
282
+ (0, manifest_1.warnMissingRequiredDimensions)(deps.manifest ?? null, input.tags ?? {});
283
+ const runCtx = (0, run_context_1.getActiveRunContext)();
284
+ if (runCtx === null)
285
+ return null;
286
+ const rawModelId = `${input.provider}/${input.model}`;
287
+ const stepKey = (0, run_context_1.resolveStepKey)(input.stepKey, input.tags?.step, () => runCtx.nextStepKey(rawModelId));
288
+ return { runCtx, stepKey, startedAtIso: new Date().toISOString() };
289
+ }
209
290
  /**
210
291
  * Turn an `AdapterResult` into a RunResult, recording exactly one telemetry row.
211
292
  * Shared by the buffered and streaming paths so cost/telemetry are byte-identical.
212
293
  * On a failure result: record the (possibly billed) row, then throw
213
294
  * `RigFailureError` — the server maps it to a status exactly like the slug lane.
295
+ *
296
+ * G1: when a run context was captured at entry (`step`), a ground-truth step is
297
+ * emitted after the telemetry row on BOTH branches (success and failure) —
298
+ * fail-open, so a buffer error there never affects the returned result. No
299
+ * artifact CONTENT is written on the raw lane (spec F3): steps are metadata.
214
300
  */
215
- function finalizeRaw(deps, input, result, latencyMs) {
301
+ function finalizeRaw(deps, input, result, latencyMs, step) {
216
302
  const unpricedRatePerTok = (deps.unpricedRateUsdPerMTok ?? cost_1.DEFAULT_UNPRICED_RATE_USD_PER_MTOK) / 1000000;
217
303
  const rawModelId = `${input.provider}/${input.model}`;
304
+ // One call_id for this raw invocation — shared by the (single) recorded row and
305
+ // the returned meta (spec §2.1 R0).
306
+ const callId = (0, node_crypto_1.randomUUID)();
218
307
  // CE-2 F2: the declared tier is reported truthfully on the row AND the meta.
219
308
  const requestedTier = input.serviceTier ?? "standard";
220
309
  // lane is SDK-owned and cannot be overridden by a caller-supplied tag.
221
310
  const tags = { ...(input.tags ?? {}), lane: "raw" };
311
+ // G1: mirror this raw inference into a ground-truth step (parity with the
312
+ // routed lane's record-attempt). task = tags.step ?? provider/model (spec F1).
313
+ // Fail-open is handled inside emitGroundTruthStep (buffer errors are swallowed).
314
+ const emitStep = (inferenceId, failureClass) => {
315
+ if (step === null)
316
+ return;
317
+ step.runCtx.emitGroundTruthStep({
318
+ stepKey: step.stepKey,
319
+ task: input.tags?.step ?? rawModelId,
320
+ inferenceId,
321
+ startedAt: step.startedAtIso,
322
+ endedAt: new Date().toISOString(),
323
+ failureClass,
324
+ });
325
+ };
222
326
  if (!result.ok) {
223
327
  let costUsd = 0;
224
328
  let pricingMissing = false;
@@ -227,8 +331,9 @@ function finalizeRaw(deps, input, result, latencyMs) {
227
331
  costUsd = settled.costUsd;
228
332
  pricingMissing = settled.pricingMissing;
229
333
  }
334
+ const failId = (0, node_crypto_1.randomUUID)();
230
335
  recordRawRow(deps.telemetry, {
231
- id: (0, node_crypto_1.randomUUID)(),
336
+ id: failId,
232
337
  rawModelId,
233
338
  provider: input.provider,
234
339
  model: input.model,
@@ -239,8 +344,19 @@ function finalizeRaw(deps, input, result, latencyMs) {
239
344
  failureClass: result.failure.class,
240
345
  tags,
241
346
  pricingMissing,
347
+ callId,
242
348
  ...(input.cache !== undefined ? { cacheKey: input.cache.key } : {}),
243
349
  });
350
+ emitStep(failId, result.failure.class);
351
+ // R1: a raw-lane timeout is the #245 footgun — attach the tier-aware teaching
352
+ // hint (the customer is looking at THIS error). Any other failure surfaces
353
+ // unchanged. Overrides any adapter-set hint: the tier + budget is the fix.
354
+ if (result.failure.class === "timeout") {
355
+ throw new failures_1.RigFailureError({
356
+ ...result.failure,
357
+ fixHint: rawTimeoutFixHint(input, requestedTier),
358
+ });
359
+ }
244
360
  throw new failures_1.RigFailureError(result.failure);
245
361
  }
246
362
  const { costUsd, pricingMissing } = (0, cost_1.attemptCost)(input.provider, input.model, result.usage, unpricedRatePerTok);
@@ -257,8 +373,10 @@ function finalizeRaw(deps, input, result, latencyMs) {
257
373
  failureClass: null,
258
374
  tags,
259
375
  pricingMissing,
376
+ callId,
260
377
  ...(input.cache !== undefined ? { cacheKey: input.cache.key } : {}),
261
378
  });
379
+ emitStep(id, null);
262
380
  const toolCalls = result.toolCalls;
263
381
  const isToolTurn = toolCalls !== undefined && toolCalls.length > 0;
264
382
  const meta = {
@@ -278,6 +396,7 @@ function finalizeRaw(deps, input, result, latencyMs) {
278
396
  attemptsByClass: {},
279
397
  servedVariant: null,
280
398
  repairedBy: null,
399
+ callId,
281
400
  ...(isToolTurn ? { toolCalls } : {}),
282
401
  };
283
402
  return { output: isToolTurn ? null : result.text, meta };
@@ -288,11 +407,14 @@ async function runRaw(deps, input) {
288
407
  // CE-2 F1/F3: fail closed on an unsupported declared directive BEFORE building
289
408
  // an adapter — no meter, no provider call, no telemetry row.
290
409
  assertRawKnobsSupported(input);
410
+ // G1: bind the ambient run context at ENTRY (ALS boundary) so a raw call inside
411
+ // a run.start records a ground-truth step (spec §4). Null ⇒ uninstrumented.
412
+ const step = prepareRawStep(deps, input);
291
413
  const adapter = buildRawAdapter(input.provider, input.apiKey);
292
414
  const candidate = (0, resolve_1.mintRawCandidate)(input.provider, input.model);
293
415
  const started = Date.now();
294
416
  const result = await adapter.call(buildRawRequest(candidate, input));
295
- return finalizeRaw(deps, input, result, Date.now() - started);
417
+ return finalizeRaw(deps, input, result, Date.now() - started, step);
296
418
  }
297
419
  function streamEndedFailure(input) {
298
420
  return {
@@ -314,6 +436,10 @@ function streamEndedFailure(input) {
314
436
  * the raw lane has no schema and the streamed bytes ARE the answer.
315
437
  */
316
438
  function runRawStream(deps, input) {
439
+ // G1: read the ambient run context HERE — at the synchronous entry, the ALS
440
+ // boundary — NOT inside the async generator below (which defers to drain time,
441
+ // possibly on a different async context, where getActiveRunContext would miss).
442
+ const step = prepareRawStep(deps, input);
317
443
  let settle;
318
444
  let fail;
319
445
  const result = new Promise((res, rej) => {
@@ -350,7 +476,7 @@ function runRawStream(deps, input) {
350
476
  else {
351
477
  adapterResult = await adapter.call(req);
352
478
  }
353
- const runResult = finalizeRaw(deps, input, adapterResult, Date.now() - started);
479
+ const runResult = finalizeRaw(deps, input, adapterResult, Date.now() - started, step);
354
480
  settle(runResult);
355
481
  yield { type: "final", result: runResult };
356
482
  }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * runRaw-only construction — CE-8 (ruling R1,
3
+ * `docs/ModelRig/customer-efficiency/ce8-runraw-only-rig-spec-plan.md`).
4
+ *
5
+ * A Lane-B customer who only calls `rig.runRaw`/`rig.runRawStream` has no routes.
6
+ * With `routesDir: null`, `createRig` SKIPS route loading, the `rig.yaml`
7
+ * manifest, and the serveable-candidate gate — so a stray route file (or a route
8
+ * whose provider key is absent in this deployment) can no longer down a rig that
9
+ * never serves it. The raw lane is unchanged; every route-dependent surface
10
+ * (`run`/`runStream`/`runBundle`/`replay`/`bakeoff`/`verifySwap`) fails with a
11
+ * TERMINAL `invariant_violation` that teaches the fix — Promise surfaces reject,
12
+ * the two streaming surfaces hand back a handle whose `result`/`events` fail
13
+ * (never a synchronous throw, matching the real `runStream` contract).
14
+ *
15
+ * This module holds the small pieces of that opt-out (the empty store, the
16
+ * teaching failure, the throwing route methods) so `index.ts` stays under the
17
+ * 700-LOC cap — `createRig` remains the ONE construction path (a separate
18
+ * `createRawRig()` factory was considered and REJECTED, spec §2 R1).
19
+ */
20
+ import type { RigConfig } from "../config";
21
+ import { type LoadedRoutes } from "../routes/loader";
22
+ import { RigFailureError } from "../types/failures";
23
+ import type { Rig } from "../index";
24
+ /** The teaching remediation for a route-lane call on a runRaw-only rig (spec §2
25
+ * R1). One constant so the failure and the docs stay in lock-step. */
26
+ export declare const RAW_ONLY_FIX_HINT = "this rig was constructed runRaw-only (routesDir: null); give createRig a routesDir to serve routes";
27
+ /** Build the TERMINAL failure a route-dependent surface throws when the rig was
28
+ * constructed runRaw-only. Reuses `invariant_violation` (already TERMINAL + in
29
+ * the teaching table) — no new FailureClass (spec §3 rationale). */
30
+ export declare function rawOnlyRouteFailure(surface: string): RigFailureError;
31
+ /** Narrow a config's `routesDir` to a string for a route-REQUIRED context (the
32
+ * route-oriented CLI subcommands: bakeoff/watch/swap/init/observe/validate),
33
+ * throwing the teaching failure if the rig was built runRaw-only.
34
+ * `loadConfigFromEnv` always yields a string (R2), so this is unreachable for
35
+ * env-built configs — it guards a hand-built `routesDir: null` reaching a
36
+ * route-only command, and satisfies the `string | null` type at those seams. */
37
+ export declare function requireRoutesDir(config: RigConfig, surface: string): string;
38
+ /** The empty route store for a runRaw-only rig: `list()` is `[]` (so
39
+ * serveability, capture-detection, and the exported route mirror are all no-ops)
40
+ * and `get()` teaches, though no public surface reaches it — every route method
41
+ * is overridden by {@link rawOnlyRouteMethods}. */
42
+ export declare function emptyRouteStore(): LoadedRoutes;
43
+ /** Load routes from a real (string) `routesDir`, mirroring a malformed-bundle
44
+ * diagnosis to the console before re-throwing — the byte-for-byte behaviour the
45
+ * string path had inline in `createRig` before CE-8 extracted it here. */
46
+ export declare function loadRoutesOrReport(config: RigConfig, routesDir: string): LoadedRoutes;
47
+ /** The route-dependent slice of the {@link Rig} surface, replaced wholesale when
48
+ * `routesDir === null` so every one of them throws the teaching failure rather
49
+ * than dereferencing an empty store. Spread LAST over the real methods in
50
+ * `createRig`, so later keys win. */
51
+ type RouteMethods = Pick<Rig, "run" | "runStream" | "runBundle" | "runBundleStream" | "replay" | "bakeoff" | "bakeoffFromEvalCases" | "verifySwap">;
52
+ /** The throwing overrides for a runRaw-only rig (spec §2 R1). Each names the
53
+ * surface + the route/task the caller asked for, so the terminal error points at
54
+ * exactly the call that needs a `routesDir`. Promise-returning surfaces REJECT
55
+ * (matching the routed lane, where errors surface as rejections); the two
56
+ * streaming surfaces return a handle synchronously, so they throw synchronously
57
+ * — the caller never reaches an event to await. */
58
+ export declare function rawOnlyRouteMethods(): RouteMethods;
59
+ export {};
60
+ //# sourceMappingURL=rawonly.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rawonly.d.ts","sourceRoot":"","sources":["../../src/run/rawonly.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAyC,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAE5F,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEpD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAEpC;sEACsE;AACtE,eAAO,MAAM,iBAAiB,uGACwE,CAAC;AAEvG;;oEAEoE;AACpE,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,eAAe,CAMpE;AAsBD;;;;;gFAKgF;AAChF,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAG3E;AAED;;;mDAGmD;AACnD,wBAAgB,eAAe,IAAI,YAAY,CAS9C;AAED;;0EAE0E;AAC1E,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,GAAG,YAAY,CASrF;AAED;;;qCAGqC;AACrC,KAAK,YAAY,GAAG,IAAI,CACtB,GAAG,EACD,KAAK,GACL,WAAW,GACX,WAAW,GACX,iBAAiB,GACjB,QAAQ,GACR,SAAS,GACT,sBAAsB,GACtB,YAAY,CACf,CAAC;AAEF;;;;;mDAKmD;AACnD,wBAAgB,mBAAmB,IAAI,YAAY,CAYlD"}
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ /**
3
+ * runRaw-only construction — CE-8 (ruling R1,
4
+ * `docs/ModelRig/customer-efficiency/ce8-runraw-only-rig-spec-plan.md`).
5
+ *
6
+ * A Lane-B customer who only calls `rig.runRaw`/`rig.runRawStream` has no routes.
7
+ * With `routesDir: null`, `createRig` SKIPS route loading, the `rig.yaml`
8
+ * manifest, and the serveable-candidate gate — so a stray route file (or a route
9
+ * whose provider key is absent in this deployment) can no longer down a rig that
10
+ * never serves it. The raw lane is unchanged; every route-dependent surface
11
+ * (`run`/`runStream`/`runBundle`/`replay`/`bakeoff`/`verifySwap`) fails with a
12
+ * TERMINAL `invariant_violation` that teaches the fix — Promise surfaces reject,
13
+ * the two streaming surfaces hand back a handle whose `result`/`events` fail
14
+ * (never a synchronous throw, matching the real `runStream` contract).
15
+ *
16
+ * This module holds the small pieces of that opt-out (the empty store, the
17
+ * teaching failure, the throwing route methods) so `index.ts` stays under the
18
+ * 700-LOC cap — `createRig` remains the ONE construction path (a separate
19
+ * `createRawRig()` factory was considered and REJECTED, spec §2 R1).
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.RAW_ONLY_FIX_HINT = void 0;
23
+ exports.rawOnlyRouteFailure = rawOnlyRouteFailure;
24
+ exports.requireRoutesDir = requireRoutesDir;
25
+ exports.emptyRouteStore = emptyRouteStore;
26
+ exports.loadRoutesOrReport = loadRoutesOrReport;
27
+ exports.rawOnlyRouteMethods = rawOnlyRouteMethods;
28
+ const loader_1 = require("../routes/loader");
29
+ const load_error_report_1 = require("../telemetry/load-error-report");
30
+ const failures_1 = require("../types/failures");
31
+ /** The teaching remediation for a route-lane call on a runRaw-only rig (spec §2
32
+ * R1). One constant so the failure and the docs stay in lock-step. */
33
+ exports.RAW_ONLY_FIX_HINT = "this rig was constructed runRaw-only (routesDir: null); give createRig a routesDir to serve routes";
34
+ /** Build the TERMINAL failure a route-dependent surface throws when the rig was
35
+ * constructed runRaw-only. Reuses `invariant_violation` (already TERMINAL + in
36
+ * the teaching table) — no new FailureClass (spec §3 rationale). */
37
+ function rawOnlyRouteFailure(surface) {
38
+ return new failures_1.RigFailureError({
39
+ class: "invariant_violation",
40
+ message: `${surface} needs routes, but this rig was constructed runRaw-only (routesDir: null)`,
41
+ fixHint: exports.RAW_ONLY_FIX_HINT,
42
+ });
43
+ }
44
+ /** A `RunStreamHandle` that fails the runRaw-only way: `result` rejects and the
45
+ * `events` iterator throws — matching the real `runStream` contract, which NEVER
46
+ * throws synchronously (a stream surface always hands back a handle, errors ride
47
+ * `result`/`events`). A synchronous throw here would break the caller's
48
+ * `const h = rig.runStream(...); await h.result` shape. */
49
+ function rawOnlyStreamHandle(surface) {
50
+ const err = rawOnlyRouteFailure(surface);
51
+ const result = Promise.reject(err);
52
+ // Keep the rejection handled so an unconsumed handle never becomes an
53
+ // unhandledRejection; the events iterator surfaces the same error.
54
+ result.catch(() => undefined);
55
+ const events = {
56
+ // eslint-disable-next-line @typescript-eslint/require-await
57
+ async *[Symbol.asyncIterator]() {
58
+ throw err;
59
+ },
60
+ };
61
+ return { events, result };
62
+ }
63
+ /** Narrow a config's `routesDir` to a string for a route-REQUIRED context (the
64
+ * route-oriented CLI subcommands: bakeoff/watch/swap/init/observe/validate),
65
+ * throwing the teaching failure if the rig was built runRaw-only.
66
+ * `loadConfigFromEnv` always yields a string (R2), so this is unreachable for
67
+ * env-built configs — it guards a hand-built `routesDir: null` reaching a
68
+ * route-only command, and satisfies the `string | null` type at those seams. */
69
+ function requireRoutesDir(config, surface) {
70
+ if (config.routesDir === null)
71
+ throw rawOnlyRouteFailure(surface);
72
+ return config.routesDir;
73
+ }
74
+ /** The empty route store for a runRaw-only rig: `list()` is `[]` (so
75
+ * serveability, capture-detection, and the exported route mirror are all no-ops)
76
+ * and `get()` teaches, though no public surface reaches it — every route method
77
+ * is overridden by {@link rawOnlyRouteMethods}. */
78
+ function emptyRouteStore() {
79
+ return {
80
+ get(route) {
81
+ throw rawOnlyRouteFailure(`get("${route}")`);
82
+ },
83
+ list() {
84
+ return Object.freeze([]);
85
+ },
86
+ };
87
+ }
88
+ /** Load routes from a real (string) `routesDir`, mirroring a malformed-bundle
89
+ * diagnosis to the console before re-throwing — the byte-for-byte behaviour the
90
+ * string path had inline in `createRig` before CE-8 extracted it here. */
91
+ function loadRoutesOrReport(config, routesDir) {
92
+ try {
93
+ return (0, loader_1.loadRoutesWithExtra)(routesDir, config.extraRoutesDir);
94
+ }
95
+ catch (err) {
96
+ // A malformed bundle is teachable, not just fatal: mirror the diagnosis to
97
+ // the console before re-throwing (the rig still cannot serve).
98
+ if (err instanceof loader_1.RouteConfigError)
99
+ (0, load_error_report_1.reportLoadError)(config, err);
100
+ throw err;
101
+ }
102
+ }
103
+ /** The throwing overrides for a runRaw-only rig (spec §2 R1). Each names the
104
+ * surface + the route/task the caller asked for, so the terminal error points at
105
+ * exactly the call that needs a `routesDir`. Promise-returning surfaces REJECT
106
+ * (matching the routed lane, where errors surface as rejections); the two
107
+ * streaming surfaces return a handle synchronously, so they throw synchronously
108
+ * — the caller never reaches an event to await. */
109
+ function rawOnlyRouteMethods() {
110
+ return {
111
+ run: (route) => Promise.reject(rawOnlyRouteFailure(`run("${route}")`)),
112
+ runStream: (route) => rawOnlyStreamHandle(`runStream("${route}")`),
113
+ runBundle: (bundle) => Promise.reject(rawOnlyRouteFailure(`runBundle("${bundle.route}")`)),
114
+ runBundleStream: (bundle) => rawOnlyStreamHandle(`runBundleStream("${bundle.route}")`),
115
+ replay: (route) => Promise.reject(rawOnlyRouteFailure(`replay("${route}")`)),
116
+ bakeoff: (route) => Promise.reject(rawOnlyRouteFailure(`bakeoff("${route}")`)),
117
+ bakeoffFromEvalCases: (task) => Promise.reject(rawOnlyRouteFailure(`bakeoffFromEvalCases("${task}")`)),
118
+ verifySwap: (route) => Promise.reject(rawOnlyRouteFailure(`verifySwap("${route}")`)),
119
+ };
120
+ }
@@ -13,11 +13,17 @@ import type { AttemptTelemetry } from "./dispatch";
13
13
  import type { ActiveRunContext } from "../artifacts/run-context";
14
14
  import type { RetryLedger } from "./retry";
15
15
  import type { RouteBundle, RunOptions, RunTags } from "../types/route";
16
- import type { TelemetryStore } from "../types/telemetry";
16
+ import type { TelemetryRung, TelemetryStore } from "../types/telemetry";
17
+ import type { LadderKeyMinter } from "./ladder-key";
17
18
  export interface RecordAttemptDeps {
18
19
  readonly telemetry: TelemetryStore;
19
20
  readonly route: RouteBundle;
20
21
  readonly ledger: RetryLedger;
22
+ /** Per-call ladder-key minter (spec §2.1 R0) — every row this recorder writes
23
+ * is stamped with the call's id, its 1-based attempt_seq, and the current
24
+ * candidate index. Shared by reference with the run loop, which advances the
25
+ * candidate index as the iterator moves. */
26
+ readonly ladder: LadderKeyMinter;
21
27
  readonly effectiveTags: RunTags;
22
28
  readonly servedVariant: string | null;
23
29
  /** Active run context (E2 chunk A) or null when the artifact namespace is off. */
@@ -33,6 +39,13 @@ export interface RecordAttemptDeps {
33
39
  * uncached), exactly matching the request builder's suppression. */
34
40
  readonly droppedCacheHandle: ReadonlySet<string>;
35
41
  }
42
+ /**
43
+ * Classify a recorded row's ladder rung (R1 review, F1). `primaryDispatched`
44
+ * carries the candidate indices already given a primary dispatch on this call —
45
+ * mutated here so the next same-index non-repair dispatch reads `retry`.
46
+ * Exported for unit tests; the run loop reaches it only through the recorder.
47
+ */
48
+ export declare function rungOf(attempt: AttemptTelemetry, preDispatch: boolean, candidateIndex: number | null, primaryDispatched: Set<number>): TelemetryRung;
36
49
  /** Build the run loop's `recordAttempt(attempt) => inferenceId` over this run's
37
50
  * state. Records one inference row and, with an active run context, mirrors it
38
51
  * as a ground-truth step (fail-open). */
@@ -1 +1 @@
1
- {"version":3,"file":"record-attempt.d.ts","sourceRoot":"","sources":["../../src/run/record-attempt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AACvE,OAAO,KAAK,EAAgB,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAIvE,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,SAAS,EAAE,cAAc,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,kFAAkF;IAClF,QAAQ,CAAC,MAAM,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACzC,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC;;qFAEiF;IACjF,QAAQ,CAAC,KAAK,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC;IACrC;;;wEAGoE;IACpE,QAAQ,CAAC,kBAAkB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CAClD;AAED;;yCAEyC;AACzC,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,iBAAiB,GAAG,CAAC,OAAO,EAAE,gBAAgB,KAAK,MAAM,CA6ElG"}
1
+ {"version":3,"file":"record-attempt.d.ts","sourceRoot":"","sources":["../../src/run/record-attempt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AACvE,OAAO,KAAK,EAAgB,aAAa,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAGtF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEpD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,SAAS,EAAE,cAAc,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B;;;gDAG4C;IAC5C,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAC;IACjC,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,kFAAkF;IAClF,QAAQ,CAAC,MAAM,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACzC,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC;;qFAEiF;IACjF,QAAQ,CAAC,KAAK,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC;IACrC;;;wEAGoE;IACpE,QAAQ,CAAC,kBAAkB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CAClD;AAED;;;;;GAKG;AACH,wBAAgB,MAAM,CACpB,OAAO,EAAE,gBAAgB,EACzB,WAAW,EAAE,OAAO,EACpB,cAAc,EAAE,MAAM,GAAG,IAAI,EAC7B,iBAAiB,EAAE,GAAG,CAAC,MAAM,CAAC,GAC7B,aAAa,CAUf;AAED;;yCAEyC;AACzC,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,iBAAiB,GAAG,CAAC,OAAO,EAAE,gBAAgB,KAAK,MAAM,CA+FlG"}
@@ -11,17 +11,45 @@
11
11
  * in-flight E4 / capture-write work (plan coordination note).
12
12
  */
13
13
  Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.rungOf = rungOf;
14
15
  exports.createRecordAttempt = createRecordAttempt;
15
16
  const node_crypto_1 = require("node:crypto");
16
17
  const failures_1 = require("../types/failures");
17
18
  const cache_stamp_1 = require("./cache-stamp");
18
19
  const cache_handle_1 = require("./cache-handle");
20
+ /**
21
+ * Classify a recorded row's ladder rung (R1 review, F1). `primaryDispatched`
22
+ * carries the candidate indices already given a primary dispatch on this call —
23
+ * mutated here so the next same-index non-repair dispatch reads `retry`.
24
+ * Exported for unit tests; the run loop reaches it only through the recorder.
25
+ */
26
+ function rungOf(attempt, preDispatch, candidateIndex, primaryDispatched) {
27
+ if (attempt.rung !== undefined)
28
+ return attempt.rung; // explicit (tool-call path)
29
+ if (attempt.repairedBy !== undefined)
30
+ return "repair";
31
+ if (preDispatch)
32
+ return "predispatch_fail";
33
+ // A real primary/retry candidate dispatch. First time this index is dispatched
34
+ // ⇒ primary; any later dispatch of the same index ⇒ retry.
35
+ if (candidateIndex === null)
36
+ return "primary";
37
+ if (primaryDispatched.has(candidateIndex))
38
+ return "retry";
39
+ primaryDispatched.add(candidateIndex);
40
+ return "primary";
41
+ }
19
42
  /** Build the run loop's `recordAttempt(attempt) => inferenceId` over this run's
20
43
  * state. Records one inference row and, with an active run context, mirrors it
21
44
  * as a ground-truth step (fail-open). */
22
45
  function createRecordAttempt(deps) {
23
46
  const { telemetry, route, ledger, effectiveTags, servedVariant, runCtx, autoStepKey } = deps;
24
- const { cache, droppedCacheHandle } = deps;
47
+ const { cache, droppedCacheHandle, ladder } = deps;
48
+ // The candidate indices for which a real primary dispatch has already been
49
+ // recorded on THIS call — so the SECOND non-repair dispatch of a candidate
50
+ // stamps `retry` rather than a second `primary` (R1 review, F1). Held in the
51
+ // per-call closure (the recorder is built once per invocation).
52
+ const primaryDispatched = new Set();
25
53
  return function recordAttempt(attempt) {
26
54
  // Request-path cache substrate (WS-C): derived from the SAME pure gate the
27
55
  // dispatcher stamps the request with, so `cache_key`/`prefix_fingerprint` on
@@ -53,6 +81,16 @@ function createRecordAttempt(deps) {
53
81
  const dropped = droppedCacheHandle.has(`${attempt.candidate.provider} ${attempt.candidate.model}`);
54
82
  const customer = isPrimary && !preDispatch ? (0, cache_handle_1.customerCacheHandle)(cache, attempt.candidate, dropped) : null;
55
83
  const cacheKey = customer?.key ?? cacheStamp?.cacheKey ?? null;
84
+ // Ladder key (spec §2.1 R0): one stamp per row — shared call_id, monotonic
85
+ // attempt_seq, and the current candidate_index (the candidate being repaired
86
+ // for a repair row, since the iterator has not advanced during its turn).
87
+ const ladderKey = ladder.stamp();
88
+ // Rung tag (R1 review, F1): the row's place on the ladder, so the rollup and
89
+ // ladder view never guess. Precedence: an explicit hint from the caller
90
+ // (only the tool-call path sets one) wins; then a repair row, then a
91
+ // pre-dispatch terminal failure, then primary-vs-retry by whether this
92
+ // candidate index was already dispatched on this call.
93
+ const rung = rungOf(attempt, preDispatch, ladderKey.candidateIndex, primaryDispatched);
56
94
  const row = {
57
95
  id: (0, node_crypto_1.randomUUID)(),
58
96
  ts: new Date().toISOString(),
@@ -75,11 +113,14 @@ function createRecordAttempt(deps) {
75
113
  ttfbMs: attempt.ttfbMs ?? null,
76
114
  failureClass: attempt.failure?.class ?? null,
77
115
  attemptsJson: JSON.stringify(ledger.snapshot()),
78
- tagsJson: JSON.stringify(effectiveTags),
116
+ tagsJson: JSON.stringify({ ...effectiveTags, rung }),
79
117
  fingerprintCell: route.fingerprintCell,
80
118
  servedVariant,
81
119
  repairedBy: attempt.repairedBy ?? null,
82
120
  pricingMissing: attempt.pricingMissing ?? false,
121
+ callId: ladderKey.callId,
122
+ attemptSeq: ladderKey.attemptSeq,
123
+ candidateIndex: ladderKey.candidateIndex,
83
124
  };
84
125
  telemetry.record(row);
85
126
  // Mirror this inference into a ground-truth step attempt (fail-open — the