@axlsdk/eval 0.23.3 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { Provider, Effort, AxlEvent, AxlRuntime } from '@axlsdk/axl';
2
+ import { Provider, Effort, ArtifactManifest, CapturedRequestRecord, Accounting, AxlEvent, AxlRuntime } from '@axlsdk/axl';
3
3
 
4
4
  type DatasetItem<TInput, TAnnotations = undefined> = {
5
5
  input: TInput;
@@ -173,6 +173,193 @@ type LlmScorerConfig = {
173
173
  };
174
174
  declare function llmScorer(config: LlmScorerConfig): Scorer;
175
175
 
176
+ /**
177
+ * The eval side of opt-in request capture: the JSONL codec, the manifest an
178
+ * `EvalResult` carries, and the bundle validation that keeps an imported
179
+ * artifact from naming anything but an artifact id.
180
+ *
181
+ * The division of labour with the core is deliberate. `@axlsdk/axl` owns the
182
+ * measurement-adjacent parts — snapshotting the request before dispatch,
183
+ * redaction, byte bounds, the non-blocking queue, and the artifact lifecycle —
184
+ * because all of those must hold for ANY consumer of `trackOutcome`, not just
185
+ * evals. This module owns what is eval-shaped: which run/case/scorer a record
186
+ * belongs to, how a run reports what it managed to capture, and what an
187
+ * exported bundle is allowed to contain.
188
+ *
189
+ * `RequestRecord` is the core record type by alias rather than by copy. Two
190
+ * hand-maintained declarations of a wire format drift, and the drift is
191
+ * invisible until someone tries to read last month's artifact.
192
+ */
193
+
194
+ /** One JSONL line of a captured-request artifact. Codec version 1. */
195
+ type RequestRecord = CapturedRequestRecord;
196
+ /**
197
+ * A pointer from an item or a scorer to one captured operation.
198
+ *
199
+ * `status` is the honest part: `'recorded'` is a complete request/response
200
+ * pair, `'start_only'` is a call that never came back (the one you most want
201
+ * to see), `'truncated'` is a record replaced by a stub because it exceeded the
202
+ * per-record byte bound, and `'omitted'` is an operation capture never reached.
203
+ */
204
+ type OperationRef = {
205
+ operationId: string;
206
+ kind: 'chat' | 'stream';
207
+ turn?: number;
208
+ attempt?: number;
209
+ status: 'recorded' | 'start_only' | 'truncated' | 'omitted';
210
+ };
211
+ /** What an `EvalResult` says about its captured requests. */
212
+ type DiagnosticManifest = {
213
+ version: 1;
214
+ artifactId: string;
215
+ fidelity: 'runtime_request';
216
+ status: ArtifactManifest['status'];
217
+ reason?: string;
218
+ records: number;
219
+ bytes: number;
220
+ redaction: 'applied' | 'none';
221
+ expiresAt?: number;
222
+ };
223
+ /** Per-run capture limits, as `runEval`/`rescore` accept them. */
224
+ type CaptureRequestsOption = boolean | {
225
+ maxRecordBytes?: number;
226
+ maxRunBytes?: number;
227
+ maxQueueBytes?: number;
228
+ };
229
+ /**
230
+ * Default bytes a rescore may copy from the source run's artifact.
231
+ *
232
+ * The same ceiling as a run's own capture budget: a rescore's copied evidence
233
+ * should never be able to dwarf the evidence a fresh run is allowed to produce.
234
+ */
235
+ declare const DEFAULT_COPY_MAX_BYTES: number;
236
+ /** Normalize the option into explicit limits, or `undefined` when capture is off. */
237
+ declare function resolveCaptureLimits(option: CaptureRequestsOption | undefined): {
238
+ maxRecordBytes?: number;
239
+ maxRunBytes?: number;
240
+ maxQueueBytes?: number;
241
+ } | undefined;
242
+ /** Build the result-level manifest from the store's finalized manifest. */
243
+ declare function toDiagnosticManifest(manifest: ArtifactManifest): DiagnosticManifest;
244
+ /** A manifest for a run whose capture could not be produced at all. */
245
+ declare function unavailableManifest(artifactId: string, reason: string): DiagnosticManifest;
246
+ /** Default ceiling on an imported `.requests.jsonl` sidecar. */
247
+ declare const DEFAULT_SIDECAR_MAX_BYTES: number;
248
+ type SidecarValidation = {
249
+ ok: true;
250
+ lines: string[];
251
+ bytes: number;
252
+ } | {
253
+ ok: false;
254
+ reason: string;
255
+ };
256
+ /**
257
+ * Validate an imported JSONL sidecar before a single byte of it is stored.
258
+ *
259
+ * Imports are the one place where a stranger's JSON reaches this subsystem, so
260
+ * the rules are deliberately narrow: every line must parse, must declare `v: 1`,
261
+ * and must carry the identity fields a reader indexes on. Anything else — a
262
+ * `file://` reference, a `path` field, a record for a codec version we cannot
263
+ * interpret — is refused rather than stored and puzzled over later.
264
+ *
265
+ * Note what is NOT here: nothing in a record can name a filesystem path or a
266
+ * URL to fetch, because the reader never dereferences anything. Records are
267
+ * re-staged under a NEW artifact id owned by the NEW history row, so an
268
+ * imported reference can only ever point at bytes this runtime wrote itself.
269
+ */
270
+ declare function validateRequestSidecar(text: unknown, options?: {
271
+ maxBytes?: number;
272
+ maxRecordBytes?: number;
273
+ }): SidecarValidation;
274
+ /** Parse a validated sidecar into records. */
275
+ declare function parseRequestRecords(lines: readonly string[]): RequestRecord[];
276
+ /** Encode records back into a sidecar body. */
277
+ declare function serializeRequestRecords(records: readonly RequestRecord[]): string;
278
+
279
+ /**
280
+ * How one dataset item ended. Absent only on artifacts written before 0.24.
281
+ *
282
+ * The four non-`completed` values are deliberately NOT interchangeable: a
283
+ * model/workflow failure, a caller cancellation, a case never started because
284
+ * the budget had closed, and a case stopped mid-flight by a denied operation
285
+ * are four different facts about the run, and collapsing them makes a
286
+ * budget-truncated run look like a broken model.
287
+ */
288
+ type EvalItemOutcome =
289
+ /** Ran to completion (its scorers may still have been stopped). */
290
+ 'completed'
291
+ /** The workflow threw — including a user `ctx.budget` block, which is user logic. */
292
+ | 'failed'
293
+ /** The caller's `AbortSignal` fired. */
294
+ | 'cancelled'
295
+ /** Never started: the run budget had already closed. No operations, no spend. */
296
+ | 'budget_skipped'
297
+ /** Started and charged, then a further operation was denied by the budget. */
298
+ | 'budget_interrupted';
299
+ /** How one scorer ended for one item. See {@link EvalItemOutcome} for the rationale. */
300
+ type ScorerOutcome =
301
+ /** Produced a valid numeric score. */
302
+ 'scored'
303
+ /** Ran and threw, or returned an out-of-range score. */
304
+ | 'failed'
305
+ /** Its `applies` predicate returned `false` — deliberately not run. */
306
+ | 'skipped'
307
+ /** The caller's `AbortSignal` fired before or during the call. */
308
+ | 'cancelled'
309
+ /** A paid judge that was never started because the budget had closed. */
310
+ | 'budget_skipped'
311
+ /** Started, then hit a denied operation. */
312
+ | 'budget_interrupted';
313
+ /** The run budget's terminal state, persisted on {@link EvalAccounting}. */
314
+ type EvalBudgetStatus = {
315
+ /** The configured USD threshold. */
316
+ limit: number;
317
+ status: 'open' | 'closed';
318
+ /** Known spend observed under the controller — `accounting.knownCost`. */
319
+ knownSpend: number;
320
+ /**
321
+ * `max(0, knownSpend - limit)`. A threshold is not a reservation: work
322
+ * already dispatched when the limit was crossed still settles, so a run can
323
+ * legitimately end above its limit. This says by how much rather than
324
+ * clamping the reported total.
325
+ */
326
+ knownOvershoot: number;
327
+ /** Which scheduling decision first observed the closure. */
328
+ closedBy?: 'case' | 'scorer' | 'operation';
329
+ };
330
+ /**
331
+ * A run's authoritative accounting: the core {@link Accounting} record plus the
332
+ * eval-specific context needed to read it honestly.
333
+ */
334
+ type EvalAccounting = Accounting & {
335
+ /** `'run'` for `runEval`, `'rescore'` for `rescore` (judging-only spend). */
336
+ scope: 'run' | 'rescore';
337
+ /** Present exactly when a budget was configured. */
338
+ budget?: EvalBudgetStatus;
339
+ /**
340
+ * Rescore provenance: the source run and its UNMODIFIED generation
341
+ * accounting, or `null` when the source was a legacy artifact that carried
342
+ * none. The original spend is never added to the rescore's own total.
343
+ */
344
+ source?: {
345
+ runId: string;
346
+ generation: Accounting | null;
347
+ };
348
+ /**
349
+ * Legacy caller-reported values observed during the run. Inspection only —
350
+ * never summed into `knownCost`, which measures Axl-observed operations.
351
+ */
352
+ callerReported?: {
353
+ costItems: number;
354
+ costTotal: number;
355
+ metadataItems: number;
356
+ };
357
+ };
358
+ /** Per-outcome counts for a run. Every key is present, including zeros. */
359
+ type EvalCoverage = {
360
+ items: Record<EvalItemOutcome, number>;
361
+ scorers: Record<string, Record<ScorerOutcome, number>>;
362
+ };
176
363
  type EvalConfig = {
177
364
  workflow: string;
178
365
  dataset: Dataset<unknown, unknown>;
@@ -186,6 +373,17 @@ type EvalConfig = {
186
373
  * so lower `concurrency` if a rate-limited judge model needs a tighter ceiling.
187
374
  */
188
375
  scorerConcurrency?: number;
376
+ /**
377
+ * Known-spend threshold for the whole run, e.g. `'$1'`, `'1'`, `'0.50'`.
378
+ *
379
+ * Validated BEFORE the dataset is loaded — a malformed value throws
380
+ * `AxlError('INVALID_BUDGET')` before any item runs or any provider is
381
+ * called. Admission closes at `knownSpend >= limit` (so `'$0'` admits
382
+ * nothing), which stops new cases, new paid judges, and new instrumented
383
+ * operations inside cases that are already running. Work already dispatched
384
+ * settles and is still counted, so the final total can exceed the limit —
385
+ * `accounting.budget.knownOvershoot` says by how much.
386
+ */
189
387
  budget?: string;
190
388
  /**
191
389
  * Opt-in source-side trust gate. When set (0–1), `runEval` flags the run as
@@ -205,8 +403,45 @@ type EvalConfig = {
205
403
  * other at consume/gate time (refuse to certify a thinned baseline/candidate).
206
404
  */
207
405
  failOnScorerErrorRate?: number;
406
+ /**
407
+ * Source-side coverage gate, **on by default** at `0.05`. `runEval` records
408
+ * `summary.itemErrorRate` whenever an item failed, and marks it `exceeded`
409
+ * when more than this fraction of the attempted items failed; the CLI then
410
+ * exits non-zero. It exists because a run that lost most of its items to
411
+ * throttling or an incident otherwise exits clean and is scored over the
412
+ * survivors.
413
+ *
414
+ * Rate = `coverage.items.failed / (count − cancelled − budget_skipped −
415
+ * budget_interrupted)`: cancelled and budget-stopped items are reported by
416
+ * their own gates and never count twice. Fires on strictly `>`, so `1`
417
+ * disables it (the CLI flag `--max-item-error-rate` overrides this value).
418
+ * A run with nothing attempted never fires.
419
+ *
420
+ * Unlike `failOnScorerErrorRate`, an invalid value (non-number, non-finite,
421
+ * `< 0` or `> 1`) makes `runEval` THROW `AxlError('INVALID_ITEM_ERROR_RATE')`
422
+ * before the dataset loads: warning and skipping would silently switch off a
423
+ * default-on gate.
424
+ */
425
+ failOnItemErrorRate?: number;
208
426
  metadata?: Record<string, unknown>;
209
427
  };
428
+ /**
429
+ * A run's item error rate against its `failOnItemErrorRate` limit, on
430
+ * `EvalSummary.itemErrorRate`. See {@link EvalConfig.failOnItemErrorRate} for
431
+ * the definition.
432
+ */
433
+ type ItemErrorRate = {
434
+ /** Items whose workflow threw — `coverage.items.failed`. */
435
+ failed: number;
436
+ /** `count − cancelled − budget_skipped − budget_interrupted`. */
437
+ attempted: number;
438
+ /** `failed / attempted`; `0` when nothing was attempted. */
439
+ rate: number;
440
+ /** The limit in force for this run (config or CLI flag, default `0.05`). */
441
+ limit: number;
442
+ /** `rate > limit` with at least one attempted item. */
443
+ exceeded: boolean;
444
+ };
210
445
  /**
211
446
  * One scorer whose failure rate tripped `EvalConfig.failOnScorerErrorRate`.
212
447
  * Surfaced on `EvalSummary.degraded` so consumers (CLI exit code, Studio
@@ -252,9 +487,32 @@ type EvalResult = {
252
487
  */
253
488
  metadata: Record<string, unknown>;
254
489
  timestamp: string;
490
+ /**
491
+ * Compatibility view of `accounting.knownCost` — the sum of settled, disjoint
492
+ * charges Axl observed. A caller-reported `cost` is NEVER part of it (read
493
+ * `accounting.callerReported` for those). Lower bound when `unpriced` is set.
494
+ */
255
495
  totalCost: number;
256
- /** True when `totalCost` is a lower bound because at least one item used unpriced work. */
496
+ /** True when `totalCost` is a lower bound — present iff `accounting.completeness !== 'complete'`. */
257
497
  unpriced?: boolean;
498
+ /**
499
+ * The authoritative record `totalCost` / `unpriced` are derived from.
500
+ * Required on results this version produces; optional so a pre-0.24 artifact
501
+ * still types. Read it through `readAccounting()`, which reports an absent
502
+ * record as `'unverified'` instead of silently treating it as complete.
503
+ */
504
+ accounting?: EvalAccounting;
505
+ /**
506
+ * What this run captured of the requests it submitted, when
507
+ * `captureRequests` was on. Present only for a capturing run — the default
508
+ * artifact carries no `diagnostics` key at all.
509
+ *
510
+ * It is a POINTER plus a self-assessment, never content: `artifactId` names
511
+ * bytes owned by the runtime that saved this result, and `status`/`reason`
512
+ * say whether those bytes are complete, truncated, interrupted or
513
+ * unavailable. Numeric results stay readable when the artifact is gone.
514
+ */
515
+ diagnostics?: DiagnosticManifest;
258
516
  duration: number;
259
517
  items: EvalItem[];
260
518
  summary: EvalSummary;
@@ -263,7 +521,24 @@ type ScorerDetail = {
263
521
  score: number | null;
264
522
  metadata?: Record<string, unknown>;
265
523
  duration?: number;
524
+ /**
525
+ * Compatibility view of `accounting.knownCost` for this scorer. Falls back to
526
+ * a scorer-returned `cost` ONLY when the runtime measured nothing at all
527
+ * (no `trackOutcome`); a caller value never overrides a measured one and is
528
+ * never summed into any total.
529
+ */
266
530
  cost?: number;
531
+ /** How this scorer ended for this item. Absent only on pre-0.24 artifacts. */
532
+ outcome?: ScorerOutcome;
533
+ /** This scorer's own operations for this item (judging spend, plus any
534
+ * `externalOperation` it declared). Absent on pre-0.24 artifacts. */
535
+ accounting?: Accounting;
536
+ /** The captured operations this judge performed for this item. Present only
537
+ * under `captureRequests`; references only, resolved through the run's
538
+ * artifact. */
539
+ diagnostics?: {
540
+ operations: OperationRef[];
541
+ };
267
542
  /**
268
543
  * `true` when the scorer's `applies` predicate returned `false` for this item,
269
544
  * so the scorer was deliberately skipped (NOT run). Distinct from a `null`
@@ -320,11 +595,15 @@ type TimingStats = {
320
595
  /**
321
596
  * Per-model latency stats across a run, on `EvalSummary.modelTiming`.
322
597
  *
323
- * Every field here is a distribution over **per-call** values, pooled across all
324
- * of the run's successful items — the same population `calls` counts. One
325
- * provider call is one sample, so an item that makes ten calls weighs ten times
326
- * an item that makes one. That is the right weighting for a model comparison:
327
- * these numbers describe the model's latency, not the item's.
598
+ * Every field here is a distribution over **per-call** values, pooled across
599
+ * every successful provider call the run made — the same population `calls`
600
+ * counts. Since 0.24 that includes the calls made by items that later FAILED or
601
+ * were stopped on budget: those calls really happened and really took that long,
602
+ * and dropping them would bias the latency of exactly the models whose slowness
603
+ * caused the timeouts. One provider call is one sample, so an item that makes
604
+ * ten calls weighs ten times an item that makes one. That is the right
605
+ * weighting for a model comparison: these numbers describe the model's latency,
606
+ * not the item's.
328
607
  *
329
608
  * This is deliberately a different weighting from the wall-clock
330
609
  * `EvalSummary.timing`, which samples once per item because it describes the
@@ -347,6 +626,14 @@ type ModelTimingStats = {
347
626
  * throttled hard on the day of the run shows it here, which is what keeps
348
627
  * `wireMs` an honest comparison. */
349
628
  retryMs: TimingStats;
629
+ /** Total `CallTiming.rateLimitRetries` over the same calls `calls` counts:
630
+ * how many rate-limit 429s this model's calls absorbed and retried. A plain
631
+ * sum, not a distribution; a call that did not report the field adds `0`.
632
+ * Nonzero means the provider throttled the run, so lower `concurrency`
633
+ * before calls start failing. `runEval` always sets it; optional because
634
+ * artifacts written before it existed do not carry it, so treat absence as
635
+ * "not recorded", not as `0`. */
636
+ rateLimitRetries?: number;
350
637
  /** Per-call time to first content delta (ms), over the STREAMING calls that
351
638
  * reported one — non-streaming calls are excluded from the sample rather
352
639
  * than entered as `0`. Absent when no call reported one. This is the
@@ -360,20 +647,72 @@ type ModelTimingStats = {
360
647
  * them. Present exactly when `firstTokenMs` is. */
361
648
  firstTokenCalls?: number;
362
649
  };
650
+ /**
651
+ * Why an item's workflow failed, captured from the thrown value before it is
652
+ * flattened to `EvalItem.error`. Populated on `failed` items only.
653
+ *
654
+ * When a `ProviderError` is found — the thrown value itself or the first one
655
+ * down its `cause` chain — every field comes from it. Otherwise only the thrown
656
+ * value's `name` is recorded. This record never copies `ProviderError.body` or
657
+ * the message: the body can echo prompt text and is redaction-eligible.
658
+ * `EvalItem.error` keeps the error message as before, which for some providers
659
+ * (a non-JSON error response) embeds the response text — that is outside this
660
+ * record's guarantee.
661
+ */
662
+ type EvalItemFailure = {
663
+ /** `'ProviderError'` when one was found, else the thrown value's own `name`. */
664
+ name: string;
665
+ /** Adapter/profile name, e.g. `'openai'`. */
666
+ provider?: string;
667
+ /** HTTP status; `0` for a network-level failure. */
668
+ status?: number;
669
+ /** `ProviderError.retryable` — the semantic failover hint. */
670
+ retryable?: boolean;
671
+ /** Provider request id, when the response carried one. */
672
+ requestId?: string;
673
+ };
363
674
  type EvalItem = {
364
675
  input: unknown;
365
676
  annotations?: unknown;
366
677
  output: unknown;
367
678
  error?: string;
679
+ /**
680
+ * Structured cause of a `failed` item — see {@link EvalItemFailure}. Absent
681
+ * on every other outcome, on pre-0.24 artifacts, and when the thrown value
682
+ * carried no `name` (a thrown string, say).
683
+ */
684
+ failure?: EvalItemFailure;
368
685
  scorerErrors?: string[];
369
686
  scores: Record<string, number | null>;
370
687
  duration?: number;
688
+ /** Compatibility view of `accounting.breakdown.generation` — this item's
689
+ * measured workflow spend. On a pre-0.24 artifact this is whatever the
690
+ * caller reported instead. */
371
691
  cost?: number;
372
692
  /** True when `cost` is a lower bound because this item used unpriced work. */
373
693
  unpriced?: boolean;
694
+ /** Compatibility view of `accounting.breakdown.judging` — this item's judges. */
374
695
  scorerCost?: number;
375
696
  scoreDetails?: Record<string, ScorerDetail>;
376
- /** Tracked metadata merged with user keys (user wins; list-only overrides clear paired counts). */
697
+ /** How this item ended. Absent only on pre-0.24 artifacts. */
698
+ outcome?: EvalItemOutcome;
699
+ /** Generation AND judging spend for this item; `breakdown` splits them.
700
+ * Absent on pre-0.24 artifacts. */
701
+ accounting?: Accounting;
702
+ /**
703
+ * What the `executeWorkflow` callback claimed, kept for inspection and never
704
+ * used as a measurement. `cost` is the callback's own `cost` return;
705
+ * `metadata` holds the reserved keys (`models`, `modelCallCounts`,
706
+ * `workflows`, `workflowCallCounts`, `tokens`, `agentCalls`) that used to
707
+ * overwrite the measured ones. Non-reserved caller keys still merge into
708
+ * `metadata` below.
709
+ */
710
+ callerReport?: {
711
+ cost?: number;
712
+ metadata?: Record<string, unknown>;
713
+ };
714
+ /** Tracked metadata merged with the caller's NON-reserved keys (caller wins
715
+ * on those). Reserved keys stay measured — see {@link EvalItem.callerReport}. */
377
716
  metadata?: Record<string, unknown>;
378
717
  /** Per-model provider-call latency for this item, rolled up from
379
718
  * `agent_call_end.timing`. Absent when the item made no timed provider call
@@ -389,6 +728,11 @@ type EvalItem = {
389
728
  * sums and per-model totals from `item.timing`, but not the distribution.
390
729
  * (`rescore` already reports no timing stats at all, so nothing regresses.) */
391
730
  timing?: Record<string, ItemModelTiming>;
731
+ /** The captured operations this item's workflow performed. Present only under
732
+ * `captureRequests`; references only, resolved through the run's artifact. */
733
+ diagnostics?: {
734
+ operations: OperationRef[];
735
+ };
392
736
  /** Trace events captured during this item's execution. Only populated when
393
737
  * `runEval` was called with `{ captureTraces: true }`. Verbose-mode
394
738
  * `agent_call_start.data.messages` snapshots are stripped to keep memory bounded;
@@ -397,7 +741,18 @@ type EvalItem = {
397
741
  };
398
742
  type EvalSummary = {
399
743
  count: number;
744
+ /**
745
+ * Items carrying an `error` string. UNCHANGED legacy meaning — it therefore
746
+ * still counts budget-stopped and cancelled items, which also carry one. Read
747
+ * {@link EvalSummary.coverage} to tell a model failure from a budget stop.
748
+ */
400
749
  failures: number;
750
+ /**
751
+ * Per-outcome counts for items and for each scorer. Every key of the outcome
752
+ * unions is present, including zeros, so a consumer can render "0 skipped"
753
+ * without inferring it from an absent key. Absent on pre-0.24 artifacts.
754
+ */
755
+ coverage?: EvalCoverage;
401
756
  scorers: Record<string, {
402
757
  mean: number;
403
758
  min: number;
@@ -448,6 +803,15 @@ type EvalSummary = {
448
803
  * tripped (either not configured or all scorers within tolerance).
449
804
  */
450
805
  degraded?: DegradedScorer[];
806
+ /**
807
+ * The item error rate against the run's `failOnItemErrorRate` limit.
808
+ * Present exactly when at least one item `failed` (so a clean run's summary
809
+ * is unchanged); `exceeded` says whether the gate tripped. Like `degraded`,
810
+ * `runEval` never throws on it — the CLI and consumers decide the exit code.
811
+ * Absent on rescores (the source run owns item failures) and on pre-0.24
812
+ * artifacts.
813
+ */
814
+ itemErrorRate?: ItemErrorRate;
451
815
  };
452
816
  /**
453
817
  * Sample-size context for one side of a comparison.
@@ -548,7 +912,20 @@ type EvalComparison = {
548
912
  baselineTotal: number;
549
913
  candidateTotal: number;
550
914
  delta: number;
551
- deltaPercent: number;
915
+ /** `null` when the baseline total is 0 — a percentage change from zero is
916
+ * not a number, and reporting `0` or `Infinity` misleads. */
917
+ deltaPercent: number | null;
918
+ /**
919
+ * `true` only when the two sides are comparable as SPEND: both accountings
920
+ * `complete`, the same `scope`, and the same case/scorer coverage. A
921
+ * partial or budget-truncated run is cheaper because it did less work, so
922
+ * without this an incomplete candidate reads as a saving.
923
+ *
924
+ * Raw totals are reported either way, and quality comparison is unaffected.
925
+ */
926
+ certified: boolean;
927
+ /** Why certification was refused. Present exactly when `certified` is false. */
928
+ reason?: string;
552
929
  };
553
930
  regressions: EvalRegression[];
554
931
  improvements: EvalImprovement[];
@@ -607,6 +984,22 @@ type RunEvalOptions = {
607
984
  * payload, subscribe to `runtime.on('trace', ...)` directly.
608
985
  */
609
986
  captureTraces?: boolean;
987
+ /**
988
+ * Capture the provider-neutral REQUESTS this run submits into a diagnostic
989
+ * artifact, so a failed case can be inspected after the process exits.
990
+ *
991
+ * Off by default and deliberately explicit to turn on: it requires
992
+ * `config.diagnostics.artifacts` on the runtime, and without it the run fails
993
+ * fast with `AxlError('DIAGNOSTICS_UNAVAILABLE')` BEFORE the dataset is
994
+ * loaded — a capture misconfiguration must never be discovered after the run
995
+ * has already spent money.
996
+ *
997
+ * Capture is diagnostics, never measurement: `accounting` is byte-identical
998
+ * with capture on, off, truncated, or failing outright. Pass an object to
999
+ * override the byte bounds (defaults 256 KiB per record, 16 MiB per run,
1000
+ * 1 MiB pending queue).
1001
+ */
1002
+ captureRequests?: CaptureRequestsOption;
610
1003
  };
611
1004
 
612
1005
  declare function defineEval(config: EvalConfig): EvalConfig;
@@ -638,6 +1031,17 @@ declare function evalCompare(baseline: EvalResult | EvalResult[], candidate: Eva
638
1031
  * `evalCompare` used for the means/CI (`baseline/candidateScored/Failed`).
639
1032
  */
640
1033
  declare function evaluateScorerErrorRateGate(comparison: EvalComparison, maxScorerErrorRate: number): string | null;
1034
+ /**
1035
+ * Decide whether a comparison must be REFUSED because a side lost too many
1036
+ * items (`axl-eval compare`, default-on at `0.05`, overridden by
1037
+ * `--max-item-error-rate`; `1` disables it). Pure and testable — returns a
1038
+ * human-readable refusal reason naming coverage and the side, or `null` to
1039
+ * allow. See {@link compareItemErrorRates} for the per-run rule.
1040
+ *
1041
+ * Throws on an invalid limit: the gate is default-on, so a bad value must not
1042
+ * quietly disable it.
1043
+ */
1044
+ declare function evaluateItemErrorRateGate(baseline: EvalResult | EvalResult[], candidate: EvalResult | EvalResult[], maxItemErrorRate?: number): string | null;
641
1045
 
642
1046
  /** A scorer's failure-rate verdict against a tolerance — see {@link evaluateScorerTolerance}. */
643
1047
  type ScorerToleranceVerdict = {
@@ -696,10 +1100,50 @@ type RescoreOptions = {
696
1100
  * be cancelled mid-flight. Also checked between items to short-circuit
697
1101
  * remaining work. Mirrors `RunEvalOptions.signal`. */
698
1102
  signal?: AbortSignal;
1103
+ /**
1104
+ * Known-spend threshold for the NEW judging work only, e.g. `'$0.10'`.
1105
+ *
1106
+ * The source run's spend is history: it is recorded under
1107
+ * `accounting.source.generation` and is NOT counted toward this limit, so a
1108
+ * rescore budget means "spend at most this much re-judging", which is the
1109
+ * only question a rescore can answer. Same validation and `>=` semantics as
1110
+ * `EvalConfig.budget`.
1111
+ */
1112
+ budget?: string;
1113
+ /**
1114
+ * Carry the source run's captured requests forward into this rescore, and
1115
+ * capture the judge calls this rescore makes.
1116
+ *
1117
+ * The source artifact is COPIED rather than referenced. Reference counting
1118
+ * would make deleting the original run either impossible or silently
1119
+ * destructive to every rescore of it; copying costs bytes once and makes each
1120
+ * result independently deletable. The copy preserves the ORIGINAL operation
1121
+ * ids as provenance, so a reader can still line a record up against the run
1122
+ * that produced it — even after that run is gone.
1123
+ *
1124
+ * Both halves land in ONE artifact and share ONE `maxRunBytes` budget: the
1125
+ * copied bytes count against it, so the artifact never grows to twice the
1126
+ * bound the caller asked for. A copy that would exceed it is partial and the
1127
+ * manifest says `truncated`; a source artifact that no longer exists, or a
1128
+ * copy that fails, yields `unavailable` with `artifactId: ''` — never the
1129
+ * source's id, which this result does not own. None of it prevents the
1130
+ * numeric rescore results from being read.
1131
+ */
1132
+ captureRequests?: CaptureRequestsOption;
699
1133
  };
700
1134
  /**
701
1135
  * Re-run scorers on the saved outputs of an existing eval result.
702
1136
  * Preserves original input/output/annotations. Only re-runs scoring.
1137
+ *
1138
+ * ## Accounting
1139
+ *
1140
+ * A rescore opens its OWN scope (`accounting.scope === 'rescore'`) covering
1141
+ * only the judging it performs. The original run's generation spend is never
1142
+ * added to the new total — a rescore that reported the sum would make a $0.20
1143
+ * re-judge of a $0.75 run look like $0.95 of new spend. Instead the source is
1144
+ * recorded verbatim under `accounting.source`, so a reader can add them up
1145
+ * deliberately, and `source.generation` is `null` when the source artifact
1146
+ * predates accounting (never synthesized from `totalCost`).
703
1147
  */
704
1148
  declare function rescore(result: EvalResult, scorers: Scorer[], runtime: AxlRuntime, options?: RescoreOptions): Promise<EvalResult>;
705
1149
 
@@ -714,7 +1158,16 @@ type MultiRunSummary = {
714
1158
  */
715
1159
  workflows: string[];
716
1160
  dataset: string;
1161
+ /** Compatibility view: the sum of each run's `totalCost`. */
717
1162
  totalCost: number;
1163
+ /**
1164
+ * The union of every run's accounting, folded conservatively: known spend and
1165
+ * operation counts sum, incompleteness reasons union, and the group takes the
1166
+ * WORST completeness in it — one legacy run makes the whole group
1167
+ * `'unverified'`, one incomplete run makes it `'incomplete'`. Inheriting
1168
+ * run[0]'s flags would report a contaminated group as clean.
1169
+ */
1170
+ accounting: Accounting;
718
1171
  totalDuration: number;
719
1172
  scorers: Record<string, {
720
1173
  mean: number;
@@ -739,4 +1192,87 @@ type MultiRunSummary = {
739
1192
  */
740
1193
  declare function aggregateRuns(runs: EvalResult[]): MultiRunSummary;
741
1194
 
742
- export { type BootstrapCIResult, type Dataset, type DatasetConfig, type DatasetItem, type DegradedScorer, type EvalCompareOptions, type EvalComparison, type EvalComparisonPartial, type EvalConfig, type EvalImprovement, type EvalItem, type EvalProgressEvent, type EvalRegression, type EvalResult, type EvalSummary, type ExtraKeyPolicy, type ItemModelTiming, type LlmScorerConfig, type ModelTimingStats, type MultiRunSummary, type RescoreOptions, type RunEvalOptions, type Scorer, type ScorerApplies, type ScorerConfig, type ScorerContext, type ScorerDetail, type ScorerFn, type ScorerResult, type ScorerToleranceVerdict, aggregateRuns, dataset, defineEval, evalCompare, evaluateScorerErrorRateGate, evaluateScorerTolerance, llmScorer, normalizeScorerResult, pairedBootstrapCI, rescore, runEval, scorer };
1195
+ /**
1196
+ * Eval-side accounting helpers.
1197
+ *
1198
+ * The core owns measurement (`@axlsdk/axl`'s `Accounting`, `trackOutcome`,
1199
+ * `AdmissionController`). This module owns the three things eval adds on top:
1200
+ *
1201
+ * - **Scope plumbing** — {@link trackScope} opens a core accounting scope when
1202
+ * the runtime has one, and produces an explicitly `uninstrumented` record
1203
+ * when it does not. An eval driven by a hand-rolled runtime therefore reports
1204
+ * "we measured nothing", never a confident `$0`.
1205
+ * - **Reading** — {@link readAccounting} turns any `EvalResult`, including a
1206
+ * pre-0.24 artifact with no `accounting` at all, into an `EvalAccounting`.
1207
+ * An absent record reads `'unverified'` and is NEVER upgraded to `'complete'`.
1208
+ * - **Union** — {@link aggregateAccounting} folds several records
1209
+ * conservatively: sums of known spend, unions of reasons, and the WORST
1210
+ * completeness of any input.
1211
+ *
1212
+ * Everything here is pure except `trackScope`, which only delegates.
1213
+ */
1214
+
1215
+ /**
1216
+ * Read the accounting of any `EvalResult`, including one written before this
1217
+ * field existed.
1218
+ *
1219
+ * A legacy artifact reads `'unverified'`: its `totalCost` is repeated as
1220
+ * `knownCost` for continuity, but no operation counts, no usage and no
1221
+ * generation/judging split are invented, and it is never reported as
1222
+ * `'complete'`. Turning an absent completeness into `'complete'` is exactly the
1223
+ * bug that would let a legacy run certify a cost comparison.
1224
+ *
1225
+ * **The structural identities do not hold on a synthesized record.** For a live
1226
+ * record (`'complete'` or `'incomplete'`) `provenance` sums to `knownCost` and
1227
+ * `breakdown` splits it. A legacy artifact reports only a single total, so the
1228
+ * synthesized record carries `knownCost` with an all-zero `breakdown` and
1229
+ * `provenance` — inventing a generation/judging split from a number of unknown
1230
+ * origin would be a fabrication. Consumers that render a breakdown must treat
1231
+ * `'unverified'` as "no split available", not as "the split is zero"; the
1232
+ * completeness field is what tells them which case they are in.
1233
+ */
1234
+ declare function readAccounting(result: EvalResult): EvalAccounting;
1235
+ /**
1236
+ * How much work the budget actually refused: cases never started or stopped
1237
+ * mid-flight, plus judges that never ran for the same reason.
1238
+ *
1239
+ * This is the quantity that makes a run short. Spend alone does not: a run can
1240
+ * settle exactly on its limit having refused nothing. Missing coverage (a
1241
+ * pre-0.24 artifact) reads `0` — an artifact that never recorded outcomes
1242
+ * cannot be used to assert that work was refused — and so do missing or
1243
+ * negative counts inside a malformed block.
1244
+ */
1245
+ declare function refusedWork(coverage: EvalCoverage | undefined): number;
1246
+ /**
1247
+ * `true` when a run's budget both closed AND refused work — the one reading
1248
+ * that entitles a consumer to call the run truncated.
1249
+ *
1250
+ * A closed controller alone is the NORMAL end state of a run whose final
1251
+ * settlement lands exactly on its limit, which is exactly how `--budget` is
1252
+ * used as a CI threshold. Reporting that as a stop fails a perfect run and
1253
+ * prints a self-contradicting message ("0 never started, 0 stopped mid-flight,
1254
+ * 2 completed … incomplete by design").
1255
+ *
1256
+ * The argument is structural rather than an `EvalResult` because the three
1257
+ * consumers hold different things: the CLI has the result, the Studio server
1258
+ * has a persisted blob it parses itself, and the Studio browser mirror has a
1259
+ * client-side type. All three can produce `{ budget, coverage }`.
1260
+ */
1261
+ declare function isBudgetStopped(summary: {
1262
+ budget?: {
1263
+ status?: string;
1264
+ } | undefined;
1265
+ coverage?: EvalCoverage | undefined;
1266
+ }): boolean;
1267
+ /**
1268
+ * Fold several accounting records into one, conservatively.
1269
+ *
1270
+ * Numbers sum; reasons union with their counts; completeness takes the WORST
1271
+ * of the inputs (`unverified` > `incomplete` > `complete`), so a group
1272
+ * containing one legacy run is `unverified` as a whole and one incomplete run
1273
+ * makes the group incomplete. Inheriting the first input's flags — the bug this
1274
+ * exists to prevent — would report a group as complete because run 1 was.
1275
+ */
1276
+ declare function aggregateAccounting(inputs: readonly Accounting[]): Accounting;
1277
+
1278
+ export { type BootstrapCIResult, type CaptureRequestsOption, DEFAULT_COPY_MAX_BYTES, DEFAULT_SIDECAR_MAX_BYTES, type Dataset, type DatasetConfig, type DatasetItem, type DegradedScorer, type DiagnosticManifest, type EvalAccounting, type EvalBudgetStatus, type EvalCompareOptions, type EvalComparison, type EvalComparisonPartial, type EvalConfig, type EvalCoverage, type EvalImprovement, type EvalItem, type EvalItemFailure, type EvalItemOutcome, type EvalProgressEvent, type EvalRegression, type EvalResult, type EvalSummary, type ExtraKeyPolicy, type ItemErrorRate, type ItemModelTiming, type LlmScorerConfig, type ModelTimingStats, type MultiRunSummary, type OperationRef, type RequestRecord, type RescoreOptions, type RunEvalOptions, type Scorer, type ScorerApplies, type ScorerConfig, type ScorerContext, type ScorerDetail, type ScorerFn, type ScorerOutcome, type ScorerResult, type ScorerToleranceVerdict, type SidecarValidation, aggregateAccounting, aggregateRuns, dataset, defineEval, evalCompare, evaluateItemErrorRateGate, evaluateScorerErrorRateGate, evaluateScorerTolerance, isBudgetStopped, llmScorer, normalizeScorerResult, pairedBootstrapCI, parseRequestRecords, readAccounting, refusedWork, rescore, resolveCaptureLimits, runEval, scorer, serializeRequestRecords, toDiagnosticManifest, unavailableManifest, validateRequestSidecar };