@axlsdk/axl 0.23.3 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -120,6 +120,237 @@ declare function inputText(input: ModelInput): string;
120
120
  /** Safe representation for context summarizers; never includes locators or data. */
121
121
  declare function summarizeModelInput(input: ModelInput): string;
122
122
 
123
+ /**
124
+ * Core accounting — one authoritative record of what a workload spent.
125
+ *
126
+ * This module owns the *measurement* rail. It is deliberately independent of
127
+ * the trace/event rail (`event-utils.ts`): trace retention, trace level,
128
+ * redaction and capture change diagnostics, never the numbers here. Both rails
129
+ * are fed by the same settlement producer (the scoped provider facade and the
130
+ * instrumented tool / memory / transcription boundaries), so turning tracing
131
+ * off cannot change `Accounting`.
132
+ *
133
+ * The three concepts:
134
+ *
135
+ * - **Operation** — one logical paid unit of work (a chat call, a stream, an
136
+ * embedding, a transcription, a tool invocation attempt, a declared external
137
+ * call). It has a stable id and a lifecycle: `admitted` → `dispatched` →
138
+ * `settled` | `denied` | `abandoned`. Transport retries are subordinate to
139
+ * the operation; they never mint a second one.
140
+ * - **Scope** — an `AsyncLocalStorage` frame opened by
141
+ * `AxlRuntime.trackOutcome()`. Scopes nest; an operation settled in a child
142
+ * is recorded exactly once in every open ancestor (deduped by operation id),
143
+ * so a parent total is the sum of disjoint operations and never a double
144
+ * charge.
145
+ * - **Admission** — a synchronous known-spend threshold ({@link
146
+ * AdmissionController}) attached to a scope. It is a threshold, not a
147
+ * reservation system: work already dispatched settles and is counted.
148
+ *
149
+ * Zero is never confused with unknown. A known $0 settles `complete`; anything
150
+ * dispatched whose charge we could not establish settles `unknown` with a
151
+ * {@link AccountingReason}, which makes the scope `incomplete` and makes
152
+ * `knownCost` an explicit lower bound.
153
+ */
154
+ /**
155
+ * How much of the declared/instrumented scope the numbers actually cover.
156
+ *
157
+ * This is coverage of Axl-observable operations, NOT invoice reconciliation.
158
+ *
159
+ * - `'complete'` — every operation in the scope reached a terminal state with a
160
+ * usable charge (including known $0).
161
+ * - `'incomplete'` — at least one operation lacked a usable charge, or the
162
+ * scope detected work from an incompatible loaded copy; `knownCost` is a
163
+ * lower bound and `reasons` says why.
164
+ * - `'unverified'` — reserved for READERS of artifacts that carry no
165
+ * accounting at all (legacy eval results). A live scope never produces it.
166
+ */
167
+ type AccountingCompleteness = 'complete' | 'incomplete' | 'unverified';
168
+ /** Where a settled USD figure came from. Keys of `Accounting.provenance`. */
169
+ type CostProvenance =
170
+ /** The vendor supplied the USD figure itself (e.g. OpenRouter `usage.cost`). */
171
+ 'provider_reported'
172
+ /** An Axl/adapter price table was applied to reported usage. */
173
+ | 'price_table_estimate'
174
+ /** An adapter supplied a cost without declaring its basis. */
175
+ | 'adapter_reported'
176
+ /** `withExternalOperation` / tool / legacy caller value. */
177
+ | 'caller_reported';
178
+ /** Why an operation could not contribute a usable charge. */
179
+ type AccountingReason =
180
+ /** Usage was reported but no usable cost was (undefined / NaN / negative / ∞). */
181
+ 'unpriced_model'
182
+ /** Dispatched, but the terminal outcome carried no usage (failure/abort/stall). */
183
+ | 'usage_missing'
184
+ /** Dispatched and never settled before its scope finalized. */
185
+ | 'abandoned'
186
+ /** An external operation finished without calling `report.setCost()`. */
187
+ | 'external_unreported'
188
+ /**
189
+ * A consumer ran work with no accounting scope available, or another loaded
190
+ * copy attempted work under a scope whose accounting protocol it cannot join.
191
+ */
192
+ | 'uninstrumented';
193
+ /** The instrumented boundaries that can open an operation. */
194
+ type OperationKind = 'chat' | 'stream' | 'embedding' | 'transcription' | 'tool' | 'external';
195
+ /** Whether a scope's operations are generating an answer or judging one. */
196
+ type OperationPurpose = 'generation' | 'judging';
197
+ /**
198
+ * Usage folded across a scope's settled operations.
199
+ *
200
+ * `reasoningTokens` / `cachedTokens` / `cacheWriteTokens` are counted ONCE in
201
+ * their own bucket. `inputTokens` is the provider's already-folded
202
+ * `prompt_tokens` — cached tokens are not re-added to it.
203
+ */
204
+ type AccountingUsage = {
205
+ inputTokens: number;
206
+ outputTokens: number;
207
+ reasoningTokens: number;
208
+ cachedTokens: number;
209
+ cacheWriteTokens: number;
210
+ audioSeconds: number;
211
+ };
212
+ /** The authoritative accounting record for one tracked scope. */
213
+ type Accounting = {
214
+ version: 1;
215
+ currency: 'USD';
216
+ /** Sum of settled, usable, disjoint charges. Never includes caller aggregates. */
217
+ knownCost: number;
218
+ completeness: AccountingCompleteness;
219
+ /** Reason → count of unresolved operations or coverage failures. */
220
+ reasons: Partial<Record<AccountingReason, number>>;
221
+ usage: AccountingUsage;
222
+ operations: {
223
+ /**
224
+ * Operations admitted and opened in this scope (or a descendant). An
225
+ * operation later refused at the dispatch check is retracted from this
226
+ * count and appears under `denied` instead, so
227
+ * `total === settled + unknown` always holds.
228
+ */
229
+ total: number;
230
+ /** Terminal with a usable cost, including a known $0. */
231
+ settled: number;
232
+ /** Terminal or abandoned without a usable cost. Coverage failures can also make `incomplete`. */
233
+ unknown: number;
234
+ /** Refused admission — never dispatched, contributes nothing anywhere. */
235
+ denied: number;
236
+ /** `total` split by kind. Denied operations are not included. */
237
+ byKind: Partial<Record<OperationKind, number>>;
238
+ };
239
+ /** `knownCost` split. `external` is kind `'external'`; the rest by scope purpose. */
240
+ breakdown: {
241
+ generation: number;
242
+ judging: number;
243
+ external: number;
244
+ };
245
+ /** `knownCost` split by provenance. Keys sum to `knownCost`. */
246
+ provenance: Partial<Record<CostProvenance, number>>;
247
+ };
248
+ /**
249
+ * The dispatch-level admission hook.
250
+ *
251
+ * `fetchWithRetry` calls `beforeDispatch` after acquiring a rate-governor
252
+ * permit and immediately before EVERY `fetch` attempt, outside the
253
+ * network-error catch. Throwing an {@link AdmissionDeniedError} from it stops
254
+ * the request before it leaves the process; the permit is still released.
255
+ *
256
+ * A third-party adapter that ignores this hook still gets the
257
+ * operation-open check, but a request already queued behind a governor or
258
+ * sleeping in its own backoff cannot be stopped. See `docs/providers.md`.
259
+ */
260
+ type DispatchAdmission = {
261
+ /** @param attempt 1-indexed transport attempt. Throws to refuse the dispatch. */
262
+ beforeDispatch(attempt: number): void;
263
+ };
264
+ /** Reports a declared external charge for one `withExternalOperation` call. */
265
+ type ExternalOperationReport = {
266
+ /**
267
+ * Record a finite, non-negative, DISJOINT USD charge for this operation.
268
+ * Explicit `0` means known-free. Nested Axl calls account for themselves and
269
+ * must be excluded.
270
+ *
271
+ * @throws AxlError `INVALID_COST_REPORT` on a non-finite/negative amount or a
272
+ * second call.
273
+ */
274
+ setCost(amountUsd: number, usage?: Partial<AccountingUsage>): void;
275
+ };
276
+ /** Descriptor for `withExternalOperation` / {@link externalOperation}. */
277
+ type ExternalOperationDescriptor = {
278
+ name: string;
279
+ };
280
+ /**
281
+ * @internal The settlement channel into a controller.
282
+ *
283
+ * `Symbol.for`, NOT `Symbol()`: this package legitimately loads twice in one
284
+ * process — an ESM consumer whose `runtime.eval()` dynamically imports
285
+ * `@axlsdk/eval`, which resolves its own copy of `@axlsdk/axl`, is the ordinary
286
+ * case, not a pathology. A module-private symbol would make the controller
287
+ * built by copy A invisible to the settlement walk in copy B, and spend would
288
+ * silently stop reaching the budget. The registry symbol is shared across
289
+ * realms, so both copies agree.
290
+ */
291
+ declare const RECORD_SPEND: unique symbol;
292
+ /**
293
+ * A synchronous known-spend threshold for one invocation.
294
+ *
295
+ * Attach it to a scope via `runtime.trackOutcome(fn, { admission })`. Every
296
+ * instrumented operation checks it when it opens, and every built-in transport
297
+ * checks it again immediately before each dispatch.
298
+ *
299
+ * It is a **threshold, not a reservation**: a call admitted before a sibling
300
+ * settles may push known spend past `limit`. `knownOvershoot` reports how far.
301
+ * Never share one controller across unrelated invocations.
302
+ */
303
+ declare class AdmissionController {
304
+ /** The configured USD threshold. Admission closes at `knownSpend >= limit`. */
305
+ readonly limit: number;
306
+ private spend;
307
+ constructor(options: {
308
+ limit: number;
309
+ });
310
+ /** USD settled so far under this controller. Unknown spend is NOT included. */
311
+ get knownSpend(): number;
312
+ /** `true` once `knownSpend >= limit`. A `limit` of 0 is closed from the start. */
313
+ get closed(): boolean;
314
+ get status(): 'open' | 'closed';
315
+ /** `max(0, knownSpend - limit)`, unconditionally — necessarily 0 while open. */
316
+ get knownOvershoot(): number;
317
+ /** Synchronous admission decision. No await between the check and the answer. */
318
+ admit(): {
319
+ admitted: true;
320
+ } | {
321
+ admitted: false;
322
+ limit: number;
323
+ knownSpend: number;
324
+ };
325
+ snapshot(): {
326
+ limit: number;
327
+ status: 'open' | 'closed';
328
+ knownSpend: number;
329
+ knownOvershoot: number;
330
+ };
331
+ /** @internal Applied synchronously at settlement, in the tick it is observed. */
332
+ [RECORD_SPEND](amountUsd: number): void;
333
+ }
334
+ /**
335
+ * Declare a paid unit of work Axl cannot observe — a third-party API call
336
+ * inside a tool, a custom scorer's own model call — so it joins the scope's
337
+ * accounting as an `'external'` operation.
338
+ *
339
+ * Admission is checked BEFORE `fn` runs. The operation finalizes on return,
340
+ * throw or abort; a cost reported before a later throw is kept. Not calling
341
+ * `report.setCost()` makes the scope `incomplete` with reason
342
+ * `'external_unreported'` — never free.
343
+ *
344
+ * The reported amount must be DISJOINT: nested `ctx.ask` / provider calls
345
+ * account for themselves and must be excluded, or the scope double-charges.
346
+ *
347
+ * Outside any accounting scope `fn` still runs, with a report that validates
348
+ * its argument but records nothing.
349
+ *
350
+ * @throws AdmissionDeniedError when the enclosing budget has closed.
351
+ */
352
+ declare function externalOperation<T>(descriptor: ExternalOperationDescriptor, fn: (report: ExternalOperationReport) => Promise<T>): Promise<T>;
353
+
123
354
  type InputModalitySupport = {
124
355
  image?: {
125
356
  sources: readonly InputMediaSource['type'][];
@@ -198,6 +429,14 @@ type ChatOptions = {
198
429
  responseFormat?: ResponseFormat;
199
430
  stop?: string[];
200
431
  signal?: AbortSignal;
432
+ /**
433
+ * @internal Runtime admission hook. Set by the scoped provider facade when an
434
+ * accounting scope with a budget is active. Built-in adapters forward it to
435
+ * `fetchWithRetry`, which calls it immediately before EVERY fetch attempt so a
436
+ * request queued behind the rate governor or sleeping in retry backoff is not
437
+ * dispatched after the budget closes. It is never sent to the vendor.
438
+ */
439
+ dispatchAdmission?: DispatchAdmission;
201
440
  /** @internal Runtime observer for the actual adapter transport lifecycle. */
202
441
  requestLifecycle?: {
203
442
  /** The request was actually dispatched, after limiter queue and retry backoff. */
@@ -281,6 +520,8 @@ type StreamChunk = {
281
520
  };
282
521
  /** Estimated cost in USD for this call, computed the same way as ProviderResponse.cost. */
283
522
  cost?: number;
523
+ /** How `cost` was derived. Field parity with `ProviderResponse.costProvenance`. */
524
+ costProvenance?: 'provider_reported' | 'price_table_estimate';
284
525
  /** Provider-specific opaque metadata (e.g. raw Gemini parts with thought signatures). */
285
526
  providerMetadata?: Record<string, unknown>;
286
527
  /**
@@ -1708,17 +1949,31 @@ type ToolCallMessage = {
1708
1949
  */
1709
1950
  type CallTiming = {
1710
1951
  /**
1711
- * Time parked in Axl's own opt-in `RateLimiter` (concurrency cap plus
1712
- * `minIntervalMs` spacing) before the request was allowed out. `0` when the
1713
- * provider has no `rateLimit` configured. This is self-imposed wait, not
1714
- * provider latency.
1952
+ * Every wait Axl imposed on itself: the first permit (concurrency cap),
1953
+ * `minIntervalMs` spacing, adaptive pacing after a rate-limit 429, a
1954
+ * rate-limit pause on the scope, and the re-acquire after a rate-limit 429.
1955
+ * `0` when nothing waited. This is
1956
+ * self-imposed wait, not provider latency.
1715
1957
  */
1716
1958
  queuedMs: number;
1717
- /** `fetch` attempts made for this call, including the final one (≥ 1). */
1959
+ /**
1960
+ * Requests actually sent for this call, including the final one (≥ 1). A call
1961
+ * held back by a rate-limit pause before sending is not an attempt.
1962
+ */
1718
1963
  attempts: number;
1719
1964
  /**
1720
- * First attempt's dispatch → final attempt's dispatch, i.e. everything spent
1721
- * on failed attempts and their backoff sleeps. `0` for a single attempt.
1965
+ * Rate-limit 429s this call received and retried: how often the provider
1966
+ * throttled it. A 429 that is returned instead (a spend cap, or the last one
1967
+ * once the retry budget is spent) is not counted, nor are 503/529/network
1968
+ * retries. Built-in adapters always set it (`0` when none); optional so a
1969
+ * custom `Provider` may omit it.
1970
+ */
1971
+ rateLimitRetries?: number;
1972
+ /**
1973
+ * First attempt's dispatch → final attempt's dispatch, minus the
1974
+ * self-imposed waits inside that span (which are in `queuedMs`): failed
1975
+ * attempts and their 503/529/network backoff sleeps. Disjoint from
1976
+ * `queuedMs`. `0` for a single attempt.
1722
1977
  */
1723
1978
  retryMs: number;
1724
1979
  /** Final dispatch → response headers. Time to first byte. */
@@ -1763,6 +2018,14 @@ type ProviderResponse = {
1763
2018
  audio_output_tokens?: number;
1764
2019
  };
1765
2020
  cost?: number;
2021
+ /**
2022
+ * How `cost` was derived, when the adapter can say. `'provider_reported'`
2023
+ * means the vendor supplied the USD figure itself; `'price_table_estimate'`
2024
+ * means the adapter priced reported usage from a table. Built-in adapters set
2025
+ * it whenever they return a `cost`; a cost with no provenance is recorded as
2026
+ * `'adapter_reported'`.
2027
+ */
2028
+ costProvenance?: 'provider_reported' | 'price_table_estimate';
1766
2029
  /** Provider-specific opaque metadata that needs to round-trip through conversation history. */
1767
2030
  providerMetadata?: Record<string, unknown>;
1768
2031
  /** Per-call latency breakdown. Absent on providers that don't instrument it. */
@@ -1922,6 +2185,54 @@ declare class BudgetExceededError extends AxlError {
1922
2185
  readonly policy: string;
1923
2186
  constructor(limit: number, spent: number, policy: string);
1924
2187
  }
2188
+ /**
2189
+ * Thrown when an `AdmissionController` refuses to admit a paid operation
2190
+ * because known spend has reached its limit.
2191
+ *
2192
+ * This is a *stop*, not a failure of the provider: it is never wrapped in a
2193
+ * `ProviderError`, never auto-retried by the transport, and never treated as
2194
+ * an abort. It is raised either when the operation opens or at the transport's
2195
+ * pre-dispatch check — in both cases before the request leaves the process, so
2196
+ * it never accompanies a charge.
2197
+ *
2198
+ * Distinct from `BudgetExceededError`, which is `ctx.budget()`'s own
2199
+ * workflow-scoped policy and keeps its existing semantics.
2200
+ */
2201
+ declare class AdmissionDeniedError extends AxlError {
2202
+ /** The controller's configured USD threshold. */
2203
+ readonly limit: number;
2204
+ /** Known settled spend at the moment of refusal. Unknown spend is excluded. */
2205
+ readonly knownSpend: number;
2206
+ /** What was refused. */
2207
+ readonly operation: {
2208
+ kind: string;
2209
+ model?: string;
2210
+ };
2211
+ constructor(options: {
2212
+ limit: number;
2213
+ knownSpend: number;
2214
+ operation: {
2215
+ kind: string;
2216
+ model?: string;
2217
+ };
2218
+ });
2219
+ }
2220
+ /**
2221
+ * Is this an admission denial, regardless of which copy of `@axlsdk/axl` threw it?
2222
+ *
2223
+ * `instanceof AdmissionDeniedError` is the natural check and it is the WRONG one
2224
+ * across a module boundary: `runtime.eval()` dynamically imports `@axlsdk/eval`,
2225
+ * which resolves its own `@axlsdk/axl`, and a dual ESM/CJS consumer can hold two
2226
+ * copies at once. The classes are then structurally identical and referentially
2227
+ * distinct, so `instanceof` returns false and a budget stop gets misreported as
2228
+ * an ordinary workflow failure — the item reads `failed` instead of
2229
+ * `budget_interrupted`, which is a lie about why the run stopped.
2230
+ *
2231
+ * Duck-typing on the stable public identity (`code` + `name`) is realm-proof.
2232
+ * Prefer this over `instanceof` in any consumer that can be loaded separately
2233
+ * from the runtime that threw.
2234
+ */
2235
+ declare function isAdmissionDeniedError(error: unknown): error is AdmissionDeniedError;
1925
2236
  /** Thrown when an agent exceeds its maximum number of tool-calling turns */
1926
2237
  declare class MaxTurnsError extends AxlError {
1927
2238
  readonly maxTurns: number;
@@ -2393,7 +2704,253 @@ declare class AxlEventBus implements AsyncIterable<AxlEvent> {
2393
2704
  }
2394
2705
 
2395
2706
  /**
2396
- * Opt-in client-side rate governor for provider HTTP calls.
2707
+ * Diagnostic artifact storage — the durable side of opt-in request capture.
2708
+ *
2709
+ * Captured request records are far too large to live inside an eval result, so
2710
+ * they live beside it as an **artifact**: an opaque id owned by an eval history
2711
+ * entry, holding a JSONL stream of records plus a small manifest describing
2712
+ * what the capture actually managed to record.
2713
+ *
2714
+ * The lifecycle is deliberately two-phase, because the artifact and the history
2715
+ * row are written to different stores and either write can fail:
2716
+ *
2717
+ * ```
2718
+ * stage(owner) → append(...)* → finalize(status) → [save history] → commit(expiresAt)
2719
+ * ↘ (save threw) → rollback()
2720
+ * ```
2721
+ *
2722
+ * A `staged` artifact is never served and is reclaimed once its lease expires,
2723
+ * so a process that dies mid-run leaves recoverable bytes rather than a
2724
+ * permanent orphan. Only a `committed` artifact whose owner still exists and
2725
+ * whose logical expiry has not passed is readable.
2726
+ *
2727
+ * Deletion is also two-phase: `markDeletePending` records the intent, then
2728
+ * `delete` removes the bytes. A failure between the two leaves the intent
2729
+ * behind, so a later `reconcile` finishes the job without the caller having to
2730
+ * re-supply anything.
2731
+ *
2732
+ * The interface is the extension point: a host running Redis history (whose
2733
+ * server-side TTL cannot notify a local filesystem) supplies its own
2734
+ * implementation. {@link FileDiagnosticArtifactStore} is the only built-in
2735
+ * backend, and Axl never silently falls back to a process temp folder.
2736
+ */
2737
+ /** Who an artifact belongs to. Today only an eval history entry can own one. */
2738
+ type ArtifactOwner = {
2739
+ kind: 'eval';
2740
+ /** The eval history entry id (`EvalHistoryEntry.id`). */
2741
+ id: string;
2742
+ };
2743
+ /** How completely a capture managed to record what it was asked to record. */
2744
+ type ArtifactStatus =
2745
+ /** Every record the run produced was written. */
2746
+ 'complete'
2747
+ /** A byte/queue bound stopped capture partway; `reason` says which. */
2748
+ | 'truncated'
2749
+ /** The writer never finalized (process death). Bytes are readable but partial. */
2750
+ | 'interrupted'
2751
+ /** The sink failed; there may be no usable bytes at all. `reason` says why. */
2752
+ | 'unavailable';
2753
+ /** The versioned descriptor written beside every artifact's records. */
2754
+ type ArtifactManifest = {
2755
+ version: 1;
2756
+ artifactId: string;
2757
+ owner: ArtifactOwner;
2758
+ state: 'staged' | 'committed' | 'delete_pending';
2759
+ /** Records actually written (including truncation stubs). */
2760
+ records: number;
2761
+ /** UTF-8 bytes of the written record lines. */
2762
+ bytes: number;
2763
+ status: ArtifactStatus;
2764
+ reason?: string;
2765
+ /** What the captured requests represent. Never provider wire bytes. */
2766
+ fidelity: 'runtime_request';
2767
+ redaction: 'applied' | 'none';
2768
+ createdAt: number;
2769
+ committedAt?: number;
2770
+ /** Absolute logical expiry, mirrored from the owning history row's retention. */
2771
+ expiresAt?: number;
2772
+ /** While `staged`: the deadline after which an abandoned writer is reclaimed. */
2773
+ leaseUntil?: number;
2774
+ /** Provenance when a rescore copied this artifact from another run's. */
2775
+ copiedFrom?: {
2776
+ artifactId: string;
2777
+ ownerId: string;
2778
+ };
2779
+ };
2780
+ /** A staged artifact, held open by a renewable lease while its writer runs. */
2781
+ type StagedArtifact = {
2782
+ artifactId: string;
2783
+ /** Push the lease deadline out. Call periodically from a long-running writer. */
2784
+ renewLease(): Promise<void>;
2785
+ };
2786
+ /** An opened artifact: its manifest plus a lazily-read stream of JSONL lines. */
2787
+ type OpenedArtifact = {
2788
+ manifest: ArtifactManifest;
2789
+ lines: AsyncIterable<string>;
2790
+ };
2791
+ /**
2792
+ * The outcome of a manifest write that can legitimately find nothing to write.
2793
+ *
2794
+ * `commit`, `markDeletePending` and `refreshExpiry` all name an artifact that a
2795
+ * sweep, an operator, or a crashed sibling process may have already removed.
2796
+ * Returning success in that case is how a history row gets published pointing
2797
+ * at bytes that are not there, still claiming `status: 'complete'` — so the
2798
+ * absence is reported instead of swallowed.
2799
+ */
2800
+ type ArtifactWriteResult = {
2801
+ ok: true;
2802
+ manifest: ArtifactManifest;
2803
+ } | {
2804
+ ok: false;
2805
+ reason: 'missing';
2806
+ };
2807
+ /**
2808
+ * The storage contract for diagnostic artifacts.
2809
+ *
2810
+ * Every method is idempotent where that is meaningful: deleting an unknown id
2811
+ * succeeds, marking delete-pending twice succeeds, and rolling back an
2812
+ * already-gone artifact succeeds. Keys are opaque ids the store mints — a
2813
+ * caller never supplies a path or a URL, which is what keeps an imported
2814
+ * artifact reference from naming an arbitrary file.
2815
+ */
2816
+ interface DiagnosticArtifactStore {
2817
+ /** Reserve a new artifact id for `owner`, held by a lease of `leaseMs`. */
2818
+ stage(owner: ArtifactOwner, opts: {
2819
+ leaseMs: number;
2820
+ }): Promise<StagedArtifact>;
2821
+ /** Append one already-encoded JSONL record. The caller pre-bounds its size. */
2822
+ append(artifactId: string, line: string): Promise<void>;
2823
+ /**
2824
+ * Seal the record stream and record how complete it is.
2825
+ *
2826
+ * `redaction` is what the WRITER applied. The store cannot infer it — the
2827
+ * records arrive already scrubbed — and a manifest that says `'none'` over
2828
+ * redacted bytes is the kind of quiet dishonesty a compliance reader acts on.
2829
+ */
2830
+ finalize(artifactId: string, status: ArtifactStatus, reason?: string, redaction?: 'applied' | 'none'): Promise<ArtifactManifest>;
2831
+ /** Promote a finalized artifact to `committed` once its owner row exists. */
2832
+ commit(artifactId: string, opts: {
2833
+ expiresAt?: number;
2834
+ }): Promise<ArtifactWriteResult>;
2835
+ /** Discard a staged artifact entirely (its owner row was never written). */
2836
+ rollback(artifactId: string): Promise<void>;
2837
+ /** Read a committed (or interrupted) artifact, or `undefined` if absent. */
2838
+ open(artifactId: string): Promise<OpenedArtifact | undefined>;
2839
+ /**
2840
+ * Copy `sourceId`'s records under a new id owned by `owner`, bounded by bytes.
2841
+ *
2842
+ * The result is a STAGED artifact, exactly as `stage` returns one: a copy is
2843
+ * the beginning of the new owner's capture, not the end of it — a rescore
2844
+ * goes on writing its own judge calls into the same artifact. It therefore
2845
+ * carries `renewLease` too, since a copy whose lease nobody holds is swept
2846
+ * out from under the run still filling it.
2847
+ */
2848
+ copy(sourceId: string, owner: ArtifactOwner, opts: {
2849
+ maxBytes: number;
2850
+ leaseMs: number;
2851
+ }): Promise<(StagedArtifact & {
2852
+ truncated: boolean;
2853
+ bytes: number;
2854
+ /** What the SOURCE said about the bytes just copied. Never inferred. */
2855
+ redaction: 'applied' | 'none';
2856
+ }) | undefined>;
2857
+ /** Record an idempotent deletion intent. Reports an artifact already gone. */
2858
+ markDeletePending(artifactId: string): Promise<ArtifactWriteResult>;
2859
+ /** Physically remove an artifact. Idempotent. */
2860
+ delete(artifactId: string): Promise<void>;
2861
+ /** Every manifest the store holds. */
2862
+ list(): Promise<ArtifactManifest[]>;
2863
+ /** Rewrite an artifact's logical expiry (its owner row was re-saved). */
2864
+ refreshExpiry(artifactId: string, expiresAt: number | undefined): Promise<ArtifactWriteResult>;
2865
+ }
2866
+ /**
2867
+ * The built-in filesystem backend: one directory per artifact under `root`,
2868
+ * holding `manifest.json` and `records.jsonl`.
2869
+ *
2870
+ * Manifest writes go through a temp file and a rename, so a crash mid-write
2871
+ * leaves either the previous manifest or the new one — never a half-parsed
2872
+ * JSON document that would make the artifact unreadable and unreclaimable.
2873
+ * Record appends are serialized per artifact through a promise chain so
2874
+ * concurrent writers cannot interleave partial lines.
2875
+ */
2876
+ declare class FileDiagnosticArtifactStore implements DiagnosticArtifactStore {
2877
+ private readonly root;
2878
+ /** Per-artifact serialization chain for appends. */
2879
+ private readonly writeChains;
2880
+ /** Live byte/record counters, folded into the manifest at finalize. */
2881
+ private readonly counters;
2882
+ /**
2883
+ * The most recently deleted ids, newest last.
2884
+ *
2885
+ * Ids are minted, never reused, so a write for one of these is always a
2886
+ * caller bug rather than a legitimate resurrection. Remembering them is what
2887
+ * stops such a write from recreating a directory holding records with no
2888
+ * manifest — a shape `list()` skips and reconciliation therefore never
2889
+ * reclaims.
2890
+ *
2891
+ * Bounded, because a long-lived Studio server deletes an artifact for every
2892
+ * eval it ever expires and an unbounded set would hold every id for the life
2893
+ * of the process. The window that matters is short: a dropped write is one
2894
+ * already in flight when the delete landed, i.e. queued behind it on the same
2895
+ * serializer. Keeping the last {@link DELETED_MEMORY} ids covers that window
2896
+ * by orders of magnitude. Past it the append does not recreate anything —
2897
+ * `appendFile` does not create parent directories, so it fails `ENOENT` — it
2898
+ * degrades that run's capture, which the guard exists to avoid: the channel
2899
+ * reads a rejected write as a dead sink and stops capturing the rest of the
2900
+ * run.
2901
+ */
2902
+ private readonly deleted;
2903
+ constructor(options: {
2904
+ root: string;
2905
+ });
2906
+ private dir;
2907
+ private readManifest;
2908
+ /** Atomic manifest replacement: write a sibling temp file, then rename over. */
2909
+ private writeManifest;
2910
+ /**
2911
+ * Run `work` after everything already queued for this artifact.
2912
+ *
2913
+ * EVERY write to an artifact goes through here — record appends and manifest
2914
+ * mutations alike — because a manifest mutation is a read-modify-write and
2915
+ * two of them interleave badly. The concrete race: the lease renewal reads a
2916
+ * pre-finalize manifest, `finalize` writes `complete/137 records`, and the
2917
+ * renewal's rename lands last and restores `interrupted/0`. `finalize`
2918
+ * returned its in-memory object, so the result would claim 137 records while
2919
+ * the artifact on disk claimed none.
2920
+ */
2921
+ private serialize;
2922
+ private mutate;
2923
+ stage(owner: ArtifactOwner, opts: {
2924
+ leaseMs: number;
2925
+ }): Promise<StagedArtifact>;
2926
+ append(artifactId: string, line: string): Promise<void>;
2927
+ finalize(artifactId: string, status: ArtifactStatus, reason?: string, redaction?: 'applied' | 'none'): Promise<ArtifactManifest>;
2928
+ commit(artifactId: string, opts: {
2929
+ expiresAt?: number;
2930
+ }): Promise<ArtifactWriteResult>;
2931
+ rollback(artifactId: string): Promise<void>;
2932
+ open(artifactId: string): Promise<OpenedArtifact | undefined>;
2933
+ copy(sourceId: string, owner: ArtifactOwner, opts: {
2934
+ maxBytes: number;
2935
+ leaseMs: number;
2936
+ }): Promise<(StagedArtifact & {
2937
+ truncated: boolean;
2938
+ bytes: number;
2939
+ redaction: 'applied' | 'none';
2940
+ }) | undefined>;
2941
+ markDeletePending(artifactId: string): Promise<ArtifactWriteResult>;
2942
+ delete(artifactId: string): Promise<void>;
2943
+ /** Record a deletion, evicting the oldest id once the window is full. */
2944
+ private rememberDeleted;
2945
+ list(): Promise<ArtifactManifest[]>;
2946
+ refreshExpiry(artifactId: string, expiresAt: number | undefined): Promise<ArtifactWriteResult>;
2947
+ }
2948
+
2949
+ /**
2950
+ * Client-side rate governor for provider HTTP calls: the plain, non-adaptive
2951
+ * base. Constructed directly (custom adapters, transcription) it applies only
2952
+ * the configured static caps. The built-in chat adapters use its pooled,
2953
+ * default-on adaptive subclass, `ScopeGovernor` (`governor-pool.ts`).
2397
2954
  *
2398
2955
  * Wraps the single `fetchWithRetry` chokepoint (see `retry.ts`) so a configured
2399
2956
  * provider can bound how many requests it has *in flight* at once and, optionally,
@@ -2412,10 +2969,13 @@ declare class AxlEventBus implements AsyncIterable<AxlEvent> {
2412
2969
  * its whole lifetime. Non-streaming calls still hold it across generation in
2413
2970
  * practice, because a provider sends no headers until the completion is
2414
2971
  * finished. Neither bounds tokens/min.
2415
- * - **Per provider instance.** Providers are singletons per (runtime, provider
2416
- * type), so one governor governs all calls through that adapter — but NOT
2417
- * embedder calls (the embedder is constructed outside the registry) and NOT
2418
- * other processes/runtimes sharing the same API key.
2972
+ * - **One governor per scope, per runtime.** The built-in chat adapters don't
2973
+ * construct this class directly: they resolve a governor per call from the
2974
+ * runtime's `GovernorPool` (`governor-pool.ts`), one per provider family +
2975
+ * base-URL origin + credential source + model. A directly constructed
2976
+ * `RateLimiter` (custom adapters) governs exactly the calls it is passed to.
2977
+ * Neither covers embedder calls (constructed outside the registry) nor other
2978
+ * processes.
2419
2979
  * - **`minIntervalMs` is global spacing, not a burst bucket.** A single
2420
2980
  * last-grant timestamp gates every grant: a permit may be free yet a grant
2421
2981
  * still waits out the interval. There is no accumulated burst allowance.
@@ -2423,8 +2983,9 @@ declare class AxlEventBus implements AsyncIterable<AxlEvent> {
2423
2983
  * never across a nested `ctx.ask`, so an agent-as-tool chain on the same
2424
2984
  * provider under `maxConcurrent: 1` still completes — permits don't stack.
2425
2985
  *
2426
- * `observe(res)` is a no-op seam in v1 for a future adaptive (header-driven)
2427
- * pacing follow-up.
2986
+ * `observe(res)` is a no-op here. The per-scope governor in `governor-pool.ts`
2987
+ * overrides it (quota-header hint) and adds the rate-limit brake; a directly
2988
+ * constructed `RateLimiter` is never adaptive.
2428
2989
  */
2429
2990
  /** Configuration for a provider's {@link RateLimiter}. All fields optional. */
2430
2991
  type RateLimitConfig = {
@@ -2446,23 +3007,67 @@ type RateLimitConfig = {
2446
3007
  * If set, `acquire()` rejects (fail loud) when a caller has queued longer than
2447
3008
  * this many ms, instead of waiting indefinitely — surfaces a misconfigured
2448
3009
  * cap rather than silently hanging the run.
3010
+ *
3011
+ * Bounds only a call's FIRST wait for a permit. On a scope that adapts (see
3012
+ * `adaptive`), a call first waits out any active rate-limit brake and only
3013
+ * then starts this clock; a call already queued when a brake begins keeps
3014
+ * its clock running through it. A retry's re-acquire after a rate-limit 429
3015
+ * is exempt: it is bounded by `maxRateLimitRetries` and the ask's `timeout`.
2449
3016
  */
2450
3017
  acquireTimeoutMs?: number;
3018
+ /**
3019
+ * Feedback-driven pacing on every built-in chat provider. Default `true`.
3020
+ * When on, a rate-limit 429 brakes every call on the scope for the
3021
+ * exponential backoff, lengthened (never shortened) by a longer
3022
+ * `Retry-After` / `retry-after-ms` and clamped at 60 s, and
3023
+ * the call retries on its own budget (`maxRateLimitRetries`), and the scope
3024
+ * then paces its grants adaptively (a rate seeded from recent demand,
3025
+ * recovered linearly on success, dropped once traffic stays well below it;
3026
+ * tuning is internal) until it reopens. Nothing waits before the first 429.
3027
+ *
3028
+ * Only first-party OpenAI (`openai:`, `openai-responses:`) and Anthropic at
3029
+ * the vendor's default endpoint origin read the 429 body: there a spend-cap
3030
+ * 429 fails fast, and a low quota header holds recovery. Everywhere else
3031
+ * (Gemini, OpenAI-compatible presets, a proxy or gateway `baseUrl`) every
3032
+ * 429 is a rate limit, so a spend-cap or daily-quota 429 fails only once
3033
+ * `maxRateLimitRetries` is spent.
3034
+ *
3035
+ * `false` restores the plain behavior: a 429 shares the transient retry
3036
+ * budget, holds up no other call, and nothing is paced. Ignored by a
3037
+ * directly constructed `RateLimiter`, which is never adaptive.
3038
+ */
3039
+ adaptive?: boolean;
3040
+ /**
3041
+ * Retries after a rate-limit 429 on an adaptive scope, separate from the
3042
+ * transient (503/529/network) budget. Default `8`. A non-negative integer;
3043
+ * `0` still brakes the scope but returns the 429. Ignored where
3044
+ * `adaptive` has no effect.
3045
+ */
3046
+ maxRateLimitRetries?: number;
2451
3047
  };
2452
3048
  /**
2453
3049
  * Counting semaphore + FIFO waiter queue. Dependency-free. Not shared across
2454
3050
  * processes — purely an in-process pacing aid.
2455
3051
  */
2456
3052
  declare class RateLimiter {
2457
- private readonly maxConcurrent;
2458
- private readonly minIntervalMs;
2459
- private readonly acquireTimeoutMs?;
3053
+ protected maxConcurrent: number;
3054
+ protected minIntervalMs: number;
3055
+ protected acquireTimeoutMs?: number;
2460
3056
  private active;
2461
3057
  private lastGrantAt;
2462
3058
  private readonly queue;
2463
3059
  private spacingTimer?;
2464
3060
  private warnedQueued;
2465
3061
  constructor(config?: RateLimitConfig);
3062
+ /**
3063
+ * Apply already-sanitized limits (see {@link sanitizeRateLimitConfig}). An
3064
+ * absent field means "no limit". Takes effect for the next grant; a waiter
3065
+ * already queued keeps the `acquireTimeoutMs` timer it was armed with.
3066
+ *
3067
+ * @internal Extension point for Axl's own per-scope governor; not a supported
3068
+ * public contract and may change without notice.
3069
+ */
3070
+ protected applyLimits(limits: RateLimitConfig): void;
2466
3071
  /**
2467
3072
  * Acquire a permit. Resolves when one is granted (respecting `maxConcurrent`
2468
3073
  * and `minIntervalMs`). The caller MUST pair every resolved `acquire()` with
@@ -2477,18 +3082,282 @@ declare class RateLimiter {
2477
3082
  * With `acquireTimeoutMs`, a waiter that sits in the queue too long rejects.
2478
3083
  */
2479
3084
  acquire(signal?: AbortSignal): Promise<void>;
3085
+ /**
3086
+ * Queue for a permit. A `priority` waiter goes ahead of every non-priority
3087
+ * waiter (FIFO among priority waiters) and never warns about queueing;
3088
+ * `timeoutMs` arms the fail-loud queue timeout (`undefined`: none).
3089
+ *
3090
+ * @internal Extension point for Axl's own per-scope governor (a retry's
3091
+ * re-acquire); not a supported public contract and may change without notice.
3092
+ */
3093
+ protected enqueue(signal: AbortSignal | undefined, options: {
3094
+ priority: boolean;
3095
+ timeoutMs: number | undefined;
3096
+ }): Promise<void>;
3097
+ /**
3098
+ * Take a permit synchronously if one is free right now — nobody queued,
3099
+ * capacity available and spacing satisfied — exactly when `acquire()` would
3100
+ * grant without waiting. Returns whether it took one; the caller pairs a
3101
+ * `true` with one {@link release}.
3102
+ *
3103
+ * @internal Extension point for Axl's own per-scope governor.
3104
+ */
3105
+ protected tryGrant(): boolean;
3106
+ /**
3107
+ * The minimum gap between two grants, in ms. Every grant path (the queue
3108
+ * pump, priority waiters and {@link tryGrant}) spaces by it.
3109
+ *
3110
+ * @internal Extension point for Axl's own per-scope governor, which widens it
3111
+ * while adaptively paced; not a supported public contract.
3112
+ */
3113
+ protected grantIntervalMs(): number;
3114
+ /**
3115
+ * Called on every grant, with its timestamp.
3116
+ *
3117
+ * @internal Extension point for Axl's own per-scope governor.
3118
+ */
3119
+ protected onGrant(_at: number): void;
3120
+ /** How long the next grant must wait for spacing; `<= 0` means it may go now. */
3121
+ private spacingWaitMs;
3122
+ /**
3123
+ * Whether a CONFIGURED cap is what stops a grant right now: the concurrency
3124
+ * cap is full, or the configured spacing has not elapsed. Queueing for any
3125
+ * other reason (a brake, adaptive spacing) is not a cap the user can raise,
3126
+ * so it never triggers the queued warning.
3127
+ */
3128
+ private staticCapBinding;
3129
+ /**
3130
+ * Whether any caller is queued for a permit.
3131
+ *
3132
+ * @internal Extension point for Axl's own per-scope governor.
3133
+ */
3134
+ protected hasWaiters(): boolean;
2480
3135
  /** Release a previously acquired permit and wake the next waiter. */
2481
3136
  release(): void;
2482
- /** v1 no-op seam for a future adaptive (rate-limit-header-driven) pacing follow-up. */
3137
+ /** No-op; the per-scope governor overrides it to read quota headers. */
2483
3138
  observe(_res: Response): void;
2484
- /** Drain the queue: grant head waiters while a permit is free and spacing allows. */
2485
- private pump;
3139
+ /**
3140
+ * Drain the queue: grant head waiters while a permit is free and spacing allows.
3141
+ * Protected so the internal per-scope governor can gate grants on its own
3142
+ * state (for example a brake) by overriding it.
3143
+ *
3144
+ * @internal Extension point for Axl's own per-scope governor; not a supported
3145
+ * public contract and may change without notice.
3146
+ */
3147
+ protected pump(): void;
2486
3148
  private grant;
2487
3149
  /** Reject a still-queued waiter (abort or timeout) and remove it cleanly. */
2488
3150
  private settleWaiter;
2489
3151
  private clearWaiterTimers;
2490
3152
  }
2491
3153
 
3154
+ /**
3155
+ * Why a 429 was returned. `'unknown'` means the body did not identify either
3156
+ * cause; the transport treats it as a rate limit, as it does every 429 on a
3157
+ * scope with no dialect.
3158
+ */
3159
+ type QuotaClass = 'rate_limit' | 'spend_cap' | 'unknown';
3160
+ type QuotaDialect = {
3161
+ /**
3162
+ * Minimum `remaining / limit` over the dialect's lanes, in `[0, 1]`, or
3163
+ * `undefined` when no lane is usable. Meant for 2xx responses.
3164
+ *
3165
+ * Total over header *values*. A `Headers` object whose `get` itself throws
3166
+ * is a broken input rather than provider data, so that throw propagates for
3167
+ * the governor's warn-once containment instead of being hidden here.
3168
+ */
3169
+ hint(headers: Headers): number | undefined;
3170
+ /**
3171
+ * Classify a 429 without consuming the original body. Never rejects:
3172
+ * a non-429, a response without `clone()` or a body, a failing read, or a
3173
+ * non-JSON body all yield `'unknown'`.
3174
+ */
3175
+ classify429(res: Response): Promise<QuotaClass>;
3176
+ };
3177
+
3178
+ /**
3179
+ * Per-runtime pool of rate governors, one per **scope**.
3180
+ *
3181
+ * A provider's rate limits belong to the account and model, not to an adapter
3182
+ * instance. Before this pool, every built-in adapter built its own
3183
+ * `RateLimiter`, so `openai:` and `openai-responses:` on one key got two
3184
+ * independent caps (effective concurrency = the sum), and one cap covered every
3185
+ * model the adapter served. Here a scope is
3186
+ *
3187
+ * provider family + base-URL origin + credential source + model
3188
+ *
3189
+ * - **Family**: `openai` for both OpenAI adapters (Chat Completions and
3190
+ * Responses share an account's limits); otherwise the adapter or preset name.
3191
+ * - **Origin**: `new URL(baseUrl).origin`, so a proxy or a self-hosted endpoint
3192
+ * is a different account. The origin carries no userinfo, path or query.
3193
+ * - **Credential source**: a callback `apiKey` by *identity* (a rotating token
3194
+ * callback is one scope), a string by *value* (two tenants' keys never
3195
+ * share). Each source maps to an opaque in-process number; the key itself is
3196
+ * never part of a map key that is logged, never in a warning, never in an
3197
+ * error.
3198
+ * - **Model**: the effective wire model of the call. There is no model-family
3199
+ * table; models that share a vendor bucket are separate scopes.
3200
+ *
3201
+ * Ownership: the runtime's `ProviderRegistry` holds one pool and hands it to the
3202
+ * built-in factories, which join the adapter they construct to it at
3203
+ * construction ({@link bindGovernorPool}). An adapter constructed directly
3204
+ * joins a private pool on its first call, so one instance registered in two runtimes shares its
3205
+ * governors (the documented explicit cross-runtime sharing path). Sharing is
3206
+ * never inferred from equal keys across runtimes.
3207
+ *
3208
+ * Config merge: only when two provider blocks reach one account (for example
3209
+ * `providers.openai` and `providers['openai-responses']` with the same key and
3210
+ * origin). Each explicit field takes the strictest value and the conflict is
3211
+ * warned about once per account. Merging only ever tightens, so a live
3212
+ * governor can adopt the merged limits in place.
3213
+ *
3214
+ * Internal: nothing here is barrel-exported.
3215
+ */
3216
+
3217
+ /**
3218
+ * The governor for one scope. A {@link RateLimiter} whose limits can be
3219
+ * tightened in place when a second provider block reaches the same account.
3220
+ *
3221
+ * Unless `adaptive` is `false` ({@link ScopeGovernor.adapts}), it is also the
3222
+ * scope's **fleet brake**: `fetchWithRetry` calls {@link brake} after a
3223
+ * rate-limit 429, and from then until `brakeUntil` no call on the scope is
3224
+ * granted a permit or dispatches. `braked()`, `awaitClear()` and `pump()`
3225
+ * share one predicate, `Date.now() < brakeUntil`. A scope that does not adapt
3226
+ * never brakes, so it behaves exactly as a plain `RateLimiter`.
3227
+ *
3228
+ * Adapting needs no quota dialect. A dialect (first-party OpenAI and Anthropic
3229
+ * at the vendor origin, see `quota.ts`) adds only two vendor-specific extras:
3230
+ * spend-cap classification of a 429 body, and the 2xx quota hint that holds
3231
+ * recovery. Without one, every 429 is treated as a rate limit and the hint is
3232
+ * never read, so recovery and reopening proceed as if the account were healthy.
3233
+ */
3234
+ declare class ScopeGovernor extends RateLimiter {
3235
+ readonly dialect?: QuotaDialect | undefined;
3236
+ private readonly family;
3237
+ private brakeUntil?;
3238
+ private brakeEndTimer?;
3239
+ private brakeEndAt?;
3240
+ private adaptive;
3241
+ private rateLimitRetries;
3242
+ private warnedHint;
3243
+ private warnedEngaged;
3244
+ /** The last 2xx quota hint (`remaining / limit`, see `quota.ts`); a low one holds growth. */
3245
+ lastHint: number | undefined;
3246
+ /**
3247
+ * Grants per second, or `undefined` while the scope is fully open (its state
3248
+ * until the first rate-limit 429, and again after it reopens). Enforced as a
3249
+ * minimum gap of `1000 / rate` ms between grants, never below `minIntervalMs`.
3250
+ */
3251
+ private rate;
3252
+ /** The rate the last cut set; recovery adds `max(rateAtLastCut, ALPHA_FLOOR_RATE) / R` per second. */
3253
+ private rateAtLastCut;
3254
+ /** When the last cut happened. A 429 cuts only if its request left after this. */
3255
+ private lastCutAt;
3256
+ /**
3257
+ * Recovery accrues from here. Advanced by each 2xx and pushed to the end of
3258
+ * any brake, so braked time never accrues.
3259
+ */
3260
+ private lastProgressAt;
3261
+ /** Since when the reopen condition has held on every 2xx; `undefined` while it doesn't. */
3262
+ private reopenSince;
3263
+ /** Grant timestamps within `WINDOW_MS`, oldest first, from `grantsHead` on. */
3264
+ private grants;
3265
+ private grantsHead;
3266
+ /** Braked spans overlapping the window, oldest first, non-overlapping. */
3267
+ private brakeSpans;
3268
+ /**
3269
+ * @param limits already sanitized (see `sanitizeRateLimitConfig`).
3270
+ * @param dialect the scope's quota dialect, or `undefined` when the vendor
3271
+ * has none (no spend-cap classification, no quota hint).
3272
+ * @param family the provider family, only to name the scope in a warning.
3273
+ */
3274
+ constructor(limits: RateLimitConfig, dialect?: QuotaDialect | undefined, family?: string);
3275
+ /** Adopt the account's merged limits. Merges only tighten (see {@link STRICTEST}). */
3276
+ reconfigure(limits: RateLimitConfig): void;
3277
+ /** Whether this scope brakes and retries rate-limit 429s on their own budget. */
3278
+ get adapts(): boolean;
3279
+ /** The rate-limit retry budget (`maxRateLimitRetries`). */
3280
+ get maxRateLimitRetries(): number;
3281
+ /** The brake predicate shared by `awaitClear`, `pump` and `fetchWithRetry`. */
3282
+ braked(): boolean;
3283
+ /**
3284
+ * On a rate-limit 429: brake every call on the scope for `ms` (already
3285
+ * clamped by the caller to the transport's backoff ceiling), and cut the
3286
+ * adaptive rate. Extends, never shortens, an active brake. `dispatchedAt` is
3287
+ * when the 429'd request left.
3288
+ *
3289
+ * The cut happens at most once per congestion epoch: only when the 429'd
3290
+ * request left after the last cut. The rest of a wave that was already in
3291
+ * flight extends the brake without cutting again.
3292
+ */
3293
+ brake(ms: number, dispatchedAt: number): void;
3294
+ /** The adaptive rate in grants per second; `undefined` while fully open. @internal */
3295
+ get currentRate(): number | undefined;
3296
+ /**
3297
+ * `rate = BETA × min(rate ?? ∞, demand)`, floored at `MIN_RATE`, where demand
3298
+ * is grants in the window over its unbraked elapsed time (floored at 1 s).
3299
+ * Demand is used only when it can be trusted: when no brake overlaps the
3300
+ * window (a burst from a cold scope reads its true count), or once the window
3301
+ * holds `MIN_DEMAND_SPAN_MS` of unbraked time. Right after a long brake the
3302
+ * window holds only a few spaced grants, so the cut uses the current rate.
3303
+ */
3304
+ private cut;
3305
+ /** Advance recovery on a 2xx, then check whether the scope can reopen (plan §4.4, Q11). */
3306
+ private recover;
3307
+ /**
3308
+ * Whether `rate` exceeds `REOPEN_FACTOR ×` the window's peak one-second grant
3309
+ * count. The peak is at least the window's per-second average, so a scope
3310
+ * paced near its demand (the common case, with the largest window) is
3311
+ * answered without the O(window) scan.
3312
+ */
3313
+ private farAbovePeakDemand;
3314
+ /** The most grants in any one-second span of the window (two pointers, O(window)). */
3315
+ private peakOneSecondGrants;
3316
+ /** Total braked time inside `[from, to)`. */
3317
+ private brakedOverlap;
3318
+ private recordBrakeSpan;
3319
+ /** Drop grants and braked spans older than the window; memory stays bounded by it. */
3320
+ private pruneWindow;
3321
+ protected onGrant(at: number): void;
3322
+ /** Adaptive spacing on top of the configured `minIntervalMs`, never below it (RQ5). */
3323
+ protected grantIntervalMs(): number;
3324
+ /**
3325
+ * Resolve once the scope is not braked. Rejects with `signal.reason` if the
3326
+ * signal is already aborted (even when not braked) or aborts while waiting.
3327
+ */
3328
+ awaitClear(signal?: AbortSignal): Promise<void>;
3329
+ /**
3330
+ * Re-acquire a permit for a call that already dispatched (after a
3331
+ * rate-limit 429, or a transient backoff that woke into a brake): queued
3332
+ * ahead of first-time callers and exempt from `acquireTimeoutMs`.
3333
+ */
3334
+ reacquire(signal?: AbortSignal): Promise<void>;
3335
+ /**
3336
+ * Take a permit without waiting, if `acquire()` / `reacquire()` would grant
3337
+ * one at once: not braked and a permit free. Lets the transport skip timing a
3338
+ * wait that never happened, so an unqueued call reports `queuedMs` exactly 0.
3339
+ */
3340
+ tryAcquire(): boolean;
3341
+ /**
3342
+ * On a 2xx: read the quota hint (if the scope has a dialect), then advance
3343
+ * recovery. Total: a throwing dialect warns once and is ignored (the hint is
3344
+ * then treated as healthy). Without a dialect `lastHint` stays `undefined`.
3345
+ */
3346
+ observe(res: Response): void;
3347
+ /**
3348
+ * Grants nothing while braked; re-pumps at the brake's end if anyone is
3349
+ * queued. The brake-end timer lives only as long as its waiters: when the
3350
+ * last one leaves (abort or timeout, both of which re-pump) it is cleared, so
3351
+ * a long brake never holds the event loop open with nothing to wake. It is
3352
+ * deliberately not `unref`'d — while a waiter exists, that timer is the only
3353
+ * thing that will resume its call.
3354
+ */
3355
+ protected pump(): void;
3356
+ private clearBrakeEnd;
3357
+ private armBrakeEnd;
3358
+ private sleepUntilBrakeEnd;
3359
+ }
3360
+
2492
3361
  /**
2493
3362
  * Input modalities a profile declares for the OpenAI-compatible wire format.
2494
3363
  * Declaring a modality is the ONLY way a profile opts in: the engine derives
@@ -2744,8 +3613,20 @@ declare class OpenAICompatibleProvider implements Provider {
2744
3613
  /** A key string, or a resolver invoked per request (expiring tokens). */
2745
3614
  protected readonly apiKeySource: ApiKeySource;
2746
3615
  protected readonly authHeader?: AuthHeader;
2747
- protected readonly governor?: RateLimiter;
3616
+ /**
3617
+ * Namespaced so it cannot collide with a member a downstream subclass
3618
+ * declares. A plain property (not `#private` or a WeakMap keyed by `this`)
3619
+ * so a caller's own Proxy around the adapter still reaches it.
3620
+ */
3621
+ private readonly axlRateGovernors;
2748
3622
  constructor(options: OpenAICompatibleOptions);
3623
+ /**
3624
+ * The rate governor for one call to `model` (the effective wire model), from
3625
+ * the runtime's per-scope pool. Every scope has one (with no `rateLimit` it
3626
+ * caps nothing until a rate-limit 429). Subclasses that issue their own
3627
+ * `fetchWithRetry` pass this as `governor`.
3628
+ */
3629
+ protected governorFor(model: string): ScopeGovernor;
2749
3630
  /** Resolve the API key for one request, validating against allowMissingApiKey. */
2750
3631
  protected resolveKey(): Promise<string>;
2751
3632
  chat(messages: ChatMessage[], options: ChatOptions): Promise<ProviderResponse>;
@@ -2767,6 +3648,13 @@ declare class OpenAICompatibleProvider implements Provider {
2767
3648
  response?: OpenAIModelResponse;
2768
3649
  }): number | undefined;
2769
3650
  protected parseResponse(json: OpenAIChatResponse, model: string, request?: Record<string, unknown>): ProviderResponse;
3651
+ /**
3652
+ * How this profile's `computeCost` derived its figure. `'from-response'`
3653
+ * profiles (OpenRouter, xAI) echo a vendor-supplied USD amount and are
3654
+ * authoritative; table and zero pricing are Axl estimates. Native subclasses
3655
+ * that override `computeCost` override this alongside it.
3656
+ */
3657
+ protected costProvenance(cost: number | undefined): ProviderResponse['costProvenance'];
2770
3658
  private roundTripMetadata;
2771
3659
  /**
2772
3660
  * PRIVATE, and deliberately not an extension point. It was `protected`, but
@@ -2957,12 +3845,53 @@ interface StateStore {
2957
3845
  * deleting an unknown id returns `false` without throwing.
2958
3846
  */
2959
3847
  deleteExecution?(executionId: string): Promise<boolean>;
2960
- /** Save an eval result to history. */
3848
+ /** Save an eval result to history. Creates the row, or replaces it. */
2961
3849
  saveEvalResult?(entry: EvalHistoryEntry): Promise<void>;
3850
+ /**
3851
+ * Replace an eval history row that ALREADY EXISTS, leaving its retention
3852
+ * exactly as it was. Returns `true` iff a row was there and was updated;
3853
+ * **never creates one**.
3854
+ *
3855
+ * This is how the runtime writes a CORRECTION — a diagnostics sweep rewriting
3856
+ * a row whose artifact it just reclaimed, a commit failure downgrading one.
3857
+ * Such a write has no business creating anything: the row may have been
3858
+ * deleted (a right-to-be-forgotten request) or expired between the moment the
3859
+ * correction was computed and the moment it lands, and `saveEvalResult` would
3860
+ * bring it back — permanently on a store with no expiry. Checking first and
3861
+ * then saving does not close that window; only the store can, so the
3862
+ * condition lives here.
3863
+ *
3864
+ * Implement it conditionally and atomically: `SET ... XX KEEPTTL` on Redis,
3865
+ * `UPDATE ... WHERE id = ?` on SQL, a presence check on an in-process map.
3866
+ * A store that does not implement it gets no correction written at all.
3867
+ */
3868
+ updateEvalResult?(entry: EvalHistoryEntry): Promise<boolean>;
2962
3869
  /** List eval history entries (most recent first). */
2963
3870
  listEvalResults?(limit?: number): Promise<EvalHistoryEntry[]>;
2964
3871
  /** Delete an eval history entry by id. Returns true if an entry was deleted. */
2965
3872
  deleteEvalResult?(id: string): Promise<boolean>;
3873
+ /**
3874
+ * Retention capability: does this eval history row still exist, and when does
3875
+ * the store itself expire it?
3876
+ *
3877
+ * Diagnostic artifacts (opt-in captured requests) live OUTSIDE the state store
3878
+ * — typically on a filesystem — and their lifetime must follow the history row
3879
+ * that owns them. A store with server-side expiry (Redis `PTTL`) cannot notify
3880
+ * that filesystem when a key ages out, so the runtime mirrors the absolute
3881
+ * `expiresAt` onto the artifact manifest at commit time and sweeps expired
3882
+ * bytes on a timer and at startup. Physical deletion is therefore EVENTUAL,
3883
+ * not synchronous with the store's expiry.
3884
+ *
3885
+ * Built-ins implement it: `MemoryStore` and `SqliteStore` never expire, so
3886
+ * `expiresAt` is `undefined`; `RedisStore` reports existence and any live TTL.
3887
+ * A custom store WITHOUT this method cannot host managed artifact capture —
3888
+ * `AxlRuntime` rejects that configuration when it is constructed rather than
3889
+ * after a run has spent money.
3890
+ */
3891
+ getEvalRetention?(id: string): Promise<{
3892
+ exists: boolean;
3893
+ expiresAt?: number;
3894
+ }>;
2966
3895
  /** List all session IDs (used by Studio session browser). */
2967
3896
  listSessions?(): Promise<string[]>;
2968
3897
  /**
@@ -3134,12 +4063,18 @@ type ProviderConfig = {
3134
4063
  /** OpenAI-compatible presets only: override the profile auth header shape. */
3135
4064
  authHeader?: AuthHeader;
3136
4065
  /**
3137
- * Opt-in client-side rate governor for this provider's HTTP calls (see
3138
- * {@link RateLimitConfig}). Bounds in-flight request concurrency (and,
3139
- * optionally, request spacing) through the shared `fetchWithRetry` chokepoint.
3140
- * Omitted ⇒ no governor (behavior unchanged). Caveat: governs **chat** calls
3141
- * through this provider instance only — NOT memory-embedder calls (constructed
3142
- * outside the registry) and NOT other processes sharing the same API key.
4066
+ * Client-side rate governor for this provider's chat calls (see
4067
+ * {@link RateLimitConfig}), through the shared `fetchWithRetry` chokepoint.
4068
+ * Omitted ⇒ no static caps (no concurrency bound, no spacing), but the
4069
+ * adaptive brake and pacing still apply after a rate-limit 429; turn them
4070
+ * off with `{ adaptive: false }`. Governors are pooled per runtime, one per
4071
+ * scope: provider family (`openai` covers `openai-responses`) + base-URL
4072
+ * origin + credential source + model. Two blocks reaching one scope use the
4073
+ * strictest value per field. Caveats: the pooled, adaptive governor covers
4074
+ * **chat** calls only. Transcription providers apply the same static caps
4075
+ * through their own per-instance, non-adaptive limiter; memory-embedder
4076
+ * calls, other runtimes (unless they share a provider instance) and other
4077
+ * processes are not governed.
3143
4078
  */
3144
4079
  rateLimit?: RateLimitConfig;
3145
4080
  };
@@ -3247,6 +4182,49 @@ type DiagnosticsConfig = {
3247
4182
  * `AXL_DIAGNOSTICS_SILENT=true`.
3248
4183
  */
3249
4184
  silent?: boolean;
4185
+ /**
4186
+ * Where opt-in captured-request artifacts are stored, and how their lifecycle
4187
+ * is reclaimed. Managed capture (`runEval({ captureRequests: true })`) is
4188
+ * UNAVAILABLE until this is configured — Axl never silently writes captured
4189
+ * prompts to a process temp folder.
4190
+ *
4191
+ * Supply exactly one of `root` (use the built-in
4192
+ * {@link DiagnosticArtifactStore} filesystem backend) or `store` (your own
4193
+ * backend — required when eval history lives in Redis, whose server-side TTL
4194
+ * cannot notify a local filesystem).
4195
+ *
4196
+ * The configured `StateStore` must implement `getEvalRetention` so an
4197
+ * artifact's logical expiry can follow its owning history row; a custom store
4198
+ * without it is rejected when the runtime is constructed, not after a run has
4199
+ * already spent money.
4200
+ */
4201
+ artifacts?: DiagnosticArtifactsConfig;
4202
+ };
4203
+ /** Artifact storage + reclamation settings. See {@link DiagnosticsConfig.artifacts}. */
4204
+ type DiagnosticArtifactsConfig = {
4205
+ /** A host-supplied backend. Takes precedence over `root`. */
4206
+ store?: DiagnosticArtifactStore;
4207
+ /** Directory for the built-in filesystem backend. Ignored when `store` is set. */
4208
+ root?: string;
4209
+ /** Reconciliation sweep period in ms. Default `60_000`. The timer is unref'd
4210
+ * and cleared by `runtime.shutdown()`. */
4211
+ sweepIntervalMs?: number;
4212
+ /** How long a staged (actively written) artifact is protected from the
4213
+ * sweeper before it counts as abandoned. Default `300_000`. */
4214
+ leaseMs?: number;
4215
+ /**
4216
+ * Longest a single artifact's lease is renewed before the runtime lets go.
4217
+ * Default `86_400_000` (24 h).
4218
+ *
4219
+ * The lease renewal is a timer, and a caller that never finalizes or rolls
4220
+ * back its artifact would otherwise pin it forever — a leak with no
4221
+ * self-healing path, because the sweeper only reclaims a lease that expired.
4222
+ * Past this bound the runtime stops renewing, the lease runs out, and the
4223
+ * artifact is reclaimed on the next sweep like any abandoned writer's. Set it
4224
+ * above the longest run you expect; a run that outlives it keeps working and
4225
+ * only loses its captured requests.
4226
+ */
4227
+ maxHoldMs?: number;
3250
4228
  };
3251
4229
 
3252
4230
  /** Full Axl configuration */
@@ -3279,6 +4257,7 @@ type ResolvedProvider = {
3279
4257
  provider: Provider;
3280
4258
  model: string;
3281
4259
  };
4260
+ /** A user factory registered with {@link ProviderRegistry.register}. */
3282
4261
  type ProviderFactory = (config: AxlConfig) => Provider;
3283
4262
  /**
3284
4263
  * Registry for LLM providers. Holds cached provider instances and supports
@@ -3294,6 +4273,11 @@ declare class ProviderRegistry {
3294
4273
  private instances;
3295
4274
  /** Factory functions, keyed by provider name */
3296
4275
  private factories;
4276
+ /**
4277
+ * Rate governors for the adapters built-in factories construct, one per
4278
+ * scope. Per registry, so per runtime: the state dies with the runtime.
4279
+ */
4280
+ private governors;
3297
4281
  /** Fallback provider returned when no factory or instance matches */
3298
4282
  private fallbackInstance?;
3299
4283
  constructor();
@@ -3339,6 +4323,9 @@ declare class ProviderRegistry {
3339
4323
  resolve(uri: string | undefined, config?: AxlConfig): ResolvedProvider;
3340
4324
  /**
3341
4325
  * Clear all cached provider instances. Useful for testing or reconfiguration.
4326
+ * Also starts a fresh governor pool, so adapters rebuilt from a changed
4327
+ * `rateLimit` are not merged with the discarded ones. A call already in
4328
+ * flight finishes on the governor it holds.
3342
4329
  */
3343
4330
  clearCache(): void;
3344
4331
  /**
@@ -3476,11 +4463,6 @@ type RecallResult = {
3476
4463
  data: unknown | VectorResult[] | null;
3477
4464
  usage?: EmbedUsage;
3478
4465
  };
3479
- /**
3480
- * Coordinates key-value memory and vector store for semantic search.
3481
- * All key-value operations delegate to the StateStore.
3482
- * Vector operations use the configured VectorStore + Embedder.
3483
- */
3484
4466
  declare class MemoryManager {
3485
4467
  private vectorStore?;
3486
4468
  private embedder?;
@@ -3591,6 +4573,18 @@ type WorkflowContextInit = {
3591
4573
  metadata?: Record<string, unknown>;
3592
4574
  config: AxlConfig;
3593
4575
  providerRegistry: ProviderRegistry;
4576
+ /**
4577
+ * Internal: resolve a `provider:model` URI to the runtime's SCOPED provider
4578
+ * facade, so every model call the context makes joins the active accounting
4579
+ * scope and honors its budget. Supplied by `AxlRuntime`; when omitted the
4580
+ * context wraps the registry's raw adapter itself, so a directly constructed
4581
+ * context is instrumented too. The registry stays available for everything
4582
+ * else that needs it.
4583
+ */
4584
+ resolveProvider?: (uri: string) => {
4585
+ provider: Provider;
4586
+ model: string;
4587
+ };
3594
4588
  transcriptionProviderRegistry?: TranscriptionProviderRegistry;
3595
4589
  sessionHistory?: ChatMessage[];
3596
4590
  onTrace?: (event: AxlEvent) => void;
@@ -3696,6 +4690,9 @@ declare class WorkflowContext<TInput = unknown> {
3696
4690
  get totalCost(): number;
3697
4691
  private config;
3698
4692
  private providerRegistry;
4693
+ private readonly resolveProviderUri;
4694
+ /** Fallback facade cache, used only when no runtime resolver was supplied. */
4695
+ private readonly ownScopedProviders;
3699
4696
  private transcriptionProviderRegistry?;
3700
4697
  private sessionHistory;
3701
4698
  private onTrace?;
@@ -4081,6 +5078,29 @@ declare class WorkflowContext<TInput = unknown> {
4081
5078
  * @param options - Optional: schema, routerModel, metadata, retries
4082
5079
  */
4083
5080
  delegate<T = string>(agents: Agent[], prompt: ModelInput, options?: DelegateOptions<T>): Promise<T>;
5081
+ /**
5082
+ * Declare a paid unit of work Axl cannot observe — a third-party API called
5083
+ * from a tool, a vendor SDK, a scraper with per-request billing — so it joins
5084
+ * the run's accounting and its budget.
5085
+ *
5086
+ * Admission is checked BEFORE `fn` runs, so a closed budget refuses the call
5087
+ * with an `AdmissionDeniedError` instead of spending. The operation finalizes
5088
+ * on return, throw or abort; a cost reported before a later throw is kept.
5089
+ * Not reporting a cost marks the run's accounting `incomplete` with reason
5090
+ * `'external_unreported'` — silence is never read as free.
5091
+ *
5092
+ * The amount must be DISJOINT: nested `ctx.ask` and other Axl operations
5093
+ * account for themselves, so including them double-charges.
5094
+ *
5095
+ * ```ts
5096
+ * const rows = await ctx.withExternalOperation({ name: 'vendor-search' }, async (report) => {
5097
+ * const res = await vendor.search(query);
5098
+ * report.setCost(res.billedUsd);
5099
+ * return res.rows;
5100
+ * });
5101
+ * ```
5102
+ */
5103
+ withExternalOperation<T>(descriptor: ExternalOperationDescriptor, fn: (report: ExternalOperationReport) => Promise<T>): Promise<T>;
4084
5104
  /** Transcribe recorded audio through the dedicated transcription registry.
4085
5105
  * This operation never enters the chat history or chat-provider path. */
4086
5106
  transcribe(request: TranscriptionRequest): Promise<Transcript>;
@@ -4395,13 +5415,282 @@ declare class Session {
4395
5415
  }): Promise<Session>;
4396
5416
  }
4397
5417
 
5418
+ /**
5419
+ * Opt-in request capture — "what did we actually submit on the turn after the
5420
+ * model's output was rejected?"
5421
+ *
5422
+ * Capture is a **diagnostics** rail, strictly subordinate to the accounting
5423
+ * rail in `accounting.ts`. Nothing here may change what a run costs, whether it
5424
+ * succeeds, or how long a provider call takes:
5425
+ *
5426
+ * - Records are built at the scoped provider facade, by value, BEFORE the
5427
+ * adapter can mutate the request. A caller mutating its own `messages` array
5428
+ * afterwards cannot change what was recorded.
5429
+ * - Every sink write is fire-and-forget into a byte-bounded queue. A sink that
5430
+ * blocks forever exhausts the queue and capture stops; the provider call is
5431
+ * never awaited on it.
5432
+ * - Any failure — a throwing sink, an exhausted bound, an unserializable
5433
+ * payload — degrades the manifest's `status`/`reason` and nothing else.
5434
+ *
5435
+ * Privacy is enforced by projection, not by filtering: only an allowlist of
5436
+ * request options is serialized, `providerOptions` contributes its KEYS only,
5437
+ * and rich media parts become bounded descriptors. Signals, functions and
5438
+ * credentials never enter a record in the first place. When redaction is
5439
+ * configured the projected record additionally passes through
5440
+ * `redactCapturedRequest()` before it reaches the sink.
5441
+ */
5442
+
5443
+ /** One conversation message as captured: text kept, rich media described. */
5444
+ type CapturedMessage = {
5445
+ role: string;
5446
+ /** Text content verbatim, or `null` when the message carried only rich parts. */
5447
+ content: string | null;
5448
+ /** Bounded descriptor for the non-text parts, when there were any. */
5449
+ input?: ModelInputDescriptor;
5450
+ name?: string;
5451
+ tool_calls?: ToolCallMessage[];
5452
+ tool_call_id?: string;
5453
+ };
5454
+ /** The provider-neutral request, snapshotted by value at invocation. */
5455
+ type CapturedRequest = {
5456
+ messages: CapturedMessage[];
5457
+ /** Tool definitions as the model sees them: names + JSON-Schema parameters. */
5458
+ tools?: Array<{
5459
+ name: string;
5460
+ description?: string;
5461
+ parameters: unknown;
5462
+ }>;
5463
+ responseFormat?: ResponseFormat;
5464
+ /** Allowlisted call options only. Never signals, functions or credentials. */
5465
+ options: {
5466
+ model: string;
5467
+ temperature?: number;
5468
+ maxTokens?: number;
5469
+ effort?: string;
5470
+ thinkingBudget?: number;
5471
+ includeThoughts?: boolean;
5472
+ toolChoice?: unknown;
5473
+ stop?: string[];
5474
+ promptCache?: boolean;
5475
+ };
5476
+ /** `providerOptions` KEYS only — the values can carry credentials. */
5477
+ providerOptionKeys?: string[];
5478
+ };
5479
+ /** What came back, projected to the fields that explain a turn. */
5480
+ type CapturedResponse = {
5481
+ content: string;
5482
+ tool_calls?: ToolCallMessage[];
5483
+ usage?: ProviderResponse['usage'];
5484
+ cost?: number;
5485
+ costProvenance?: string;
5486
+ timing?: ProviderResponse['timing'];
5487
+ };
5488
+ /** What went wrong, projected to identity rather than a serialized error. */
5489
+ type CapturedError = {
5490
+ name?: string;
5491
+ message: string;
5492
+ code?: string;
5493
+ status?: number;
5494
+ };
5495
+ /** The gate that rejected the previous turn and the correction fed back in. */
5496
+ type CapturedCorrection = {
5497
+ stage: 'schema' | 'validate' | 'guardrail';
5498
+ reason?: string;
5499
+ /** The exact text appended to the conversation for the next turn. */
5500
+ feedbackMessage: string;
5501
+ };
5502
+ /** Which part of an operation's life a record describes. */
5503
+ type CapturedPhase = 'start' | 'attempt' | 'end';
5504
+ /**
5505
+ * One JSONL line. `v: 1` is the codec version; `@axlsdk/eval` re-exports this
5506
+ * as `RequestRecord` and owns the bundle/import validation around it.
5507
+ *
5508
+ * A `start` record carries the request; `attempt` records mark additional
5509
+ * transport attempts for the SAME logical operation (a 429 retry is not an
5510
+ * output repair); an `end` record carries the response or the error. An
5511
+ * operation that never returns therefore leaves a start record with no end —
5512
+ * which is exactly the call you most want to inspect.
5513
+ */
5514
+ type CapturedRequestRecord = {
5515
+ v: 1;
5516
+ phase: CapturedPhase;
5517
+ operationId: string;
5518
+ kind: 'chat' | 'stream';
5519
+ caseIndex?: number;
5520
+ scorer?: string;
5521
+ executionId?: string;
5522
+ askId?: string;
5523
+ parentAskId?: string;
5524
+ turn?: number;
5525
+ retryReason?: 'schema' | 'validate' | 'guardrail';
5526
+ /** 1-indexed transport attempt this record describes. */
5527
+ transportAttempts: number;
5528
+ provider: string;
5529
+ model: string;
5530
+ request?: CapturedRequest;
5531
+ response?: CapturedResponse;
5532
+ error?: CapturedError;
5533
+ /**
5534
+ * Why an operation ended without a response, when it ended deliberately.
5535
+ *
5536
+ * A stream closed by a stall timeout, a consumer `break`, or an adapter that
5537
+ * simply stopped yielding is NOT a call that never came back. Without this
5538
+ * the two are indistinguishable, and `start_only` — the status reserved for a
5539
+ * genuinely hung call — stops discriminating.
5540
+ */
5541
+ termination?: string;
5542
+ correction?: CapturedCorrection;
5543
+ captured: {
5544
+ fidelity: 'runtime_request';
5545
+ redacted: boolean;
5546
+ /** `true` when the record was replaced by a stub over `maxRecordBytes`. */
5547
+ truncated: boolean;
5548
+ /** What could not be represented, e.g. `['media']`. */
5549
+ omitted: string[];
5550
+ /**
5551
+ * Why this record is a stub, when it is one.
5552
+ *
5553
+ * A stub has two causes and a reader has to be able to tell them apart:
5554
+ * the record exceeded `maxRecordBytes` (its size is on `bytes`), or the
5555
+ * request/response could not be projected at all — a `json_schema.schema`
5556
+ * holding a function, a content part no adapter names. Both keep the
5557
+ * operation's identity; neither is a call that failed.
5558
+ */
5559
+ reason?: string;
5560
+ };
5561
+ /** Encoded size of the record this stub replaced. Present only on stubs. */
5562
+ bytes?: number;
5563
+ };
5564
+ /** A pointer from an eval item / scorer to one captured operation. */
5565
+ type CapturedOperationRef = {
5566
+ operationId: string;
5567
+ kind: 'chat' | 'stream';
5568
+ turn?: number;
5569
+ attempt?: number;
5570
+ status: 'recorded' | 'start_only' | 'truncated' | 'omitted';
5571
+ };
5572
+ /** Where encoded record lines go. One call per JSONL line, already bounded. */
5573
+ type RequestCaptureSink = {
5574
+ append(line: string): Promise<void>;
5575
+ };
5576
+ /** Per-run capture bounds. All finite positive byte counts. */
5577
+ type RequestCaptureOptions = {
5578
+ sink: RequestCaptureSink;
5579
+ /** Encoded bytes above which a record is replaced by a stub. Default 256 KiB. */
5580
+ maxRecordBytes?: number;
5581
+ /** Encoded bytes above which capture stops for the run. Default 16 MiB. */
5582
+ maxRunBytes?: number;
5583
+ /** Pending unflushed bytes above which capture stops. Default 1 MiB. */
5584
+ maxQueueBytes?: number;
5585
+ /** Apply `redactCapturedRequest` before every write. */
5586
+ redact?: boolean;
5587
+ /** Bounded wait for the queue to drain at close. Default 5s. */
5588
+ flushTimeoutMs?: number;
5589
+ /**
5590
+ * Bytes the artifact this channel writes into ALREADY holds.
5591
+ *
5592
+ * A rescore opens its channel over an artifact seeded with its source run's
5593
+ * copied records. `maxRunBytes` is a promise about how large the artifact
5594
+ * gets, not about this channel's share of it, so the carried bytes count
5595
+ * against the same bound.
5596
+ */
5597
+ carriedBytes?: number;
5598
+ };
5599
+ /** Eval-side correlation stamped onto every record produced inside a scope. */
5600
+ type CaptureCorrelation = {
5601
+ caseIndex?: number;
5602
+ scorer?: string;
5603
+ };
5604
+ /** What a finished capture channel has to say about itself. */
5605
+ type RequestCaptureStatus = {
5606
+ status: 'complete' | 'truncated' | 'unavailable';
5607
+ reason?: string;
5608
+ records: number;
5609
+ bytes: number;
5610
+ redaction: 'applied' | 'none';
5611
+ };
5612
+ declare const DEFAULT_MAX_RECORD_BYTES: number;
5613
+ declare const DEFAULT_MAX_RUN_BYTES: number;
5614
+ declare const DEFAULT_MAX_QUEUE_BYTES: number;
5615
+ /**
5616
+ * The bounded, non-blocking pipeline between the facade and a sink.
5617
+ *
5618
+ * One channel per capturing run. It owns the run's byte budget, the pending
5619
+ * queue, and the ledger of which operations produced which records — the
5620
+ * ledger is what lets an eval item point at its own operations without the
5621
+ * runner having to parse the JSONL back.
5622
+ */
5623
+ declare class RequestCaptureChannel {
5624
+ private readonly sink;
5625
+ private readonly maxRecordBytes;
5626
+ private readonly maxRunBytes;
5627
+ private readonly maxQueueBytes;
5628
+ private readonly flushTimeoutMs;
5629
+ readonly redact: boolean;
5630
+ private runBytes;
5631
+ private queueBytes;
5632
+ private records;
5633
+ private stopped;
5634
+ private status;
5635
+ private reason;
5636
+ private drain;
5637
+ private stoppedSignal;
5638
+ private readonly stoppedPromise;
5639
+ /** operationId → the ref an eval item/scorer will point at. */
5640
+ private readonly refs;
5641
+ constructor(options: RequestCaptureOptions);
5642
+ /**
5643
+ * Report a CHANNEL-WIDE capture failure from outside the channel.
5644
+ *
5645
+ * Terminal for the run, so it is reserved for a loss that really is
5646
+ * run-wide: a sink that cannot be written to, a host producer that has lost
5647
+ * its ability to record anything further. A single record that could not be
5648
+ * projected — a `json_schema.schema` holding a function, a content part no
5649
+ * adapter names — is NOT this: it is the truncation case, and the provider
5650
+ * path stubs that one record and keeps capturing. Calling `fail()` for it
5651
+ * would throw away every later record in the run.
5652
+ */
5653
+ fail(reason: string): void;
5654
+ /** Stop capturing for the rest of the run and remember why. */
5655
+ private stop;
5656
+ /**
5657
+ * Enqueue one record. Synchronous and total: it returns immediately whatever
5658
+ * the sink is doing, and it never throws into the provider call path.
5659
+ */
5660
+ write(record: CapturedRequestRecord): void;
5661
+ /** Register (or upgrade) the ref an eval item will point at. */
5662
+ noteOperation(ref: CapturedOperationRef & {
5663
+ caseIndex?: number;
5664
+ scorer?: string;
5665
+ }): void;
5666
+ /** Every operation this run captured, with the correlation it was seen under. */
5667
+ operations(): Array<CapturedOperationRef & {
5668
+ caseIndex?: number;
5669
+ scorer?: string;
5670
+ }>;
5671
+ /**
5672
+ * Wait for the queue to drain (bounded) and report the run's capture status.
5673
+ *
5674
+ * Called after the tracked function has already returned, so it can never
5675
+ * delay a provider call. It is additionally bounded three ways: a stopped
5676
+ * channel abandons its queue immediately, the queue itself is byte-bounded,
5677
+ * and a flush timeout is the final backstop against a sink that never
5678
+ * settles.
5679
+ */
5680
+ close(): Promise<RequestCaptureStatus>;
5681
+ }
5682
+
4398
5683
  /**
4399
5684
  * Function exported from an eval module that overrides the default
4400
5685
  * `runtime.execute(workflow, input)` behavior. The runtime injects itself as
4401
5686
  * the second argument so user code can call `runtime.createContext()` etc.
4402
5687
  *
4403
- * Returns `{ output, cost?, metadata? }` — cost and metadata are optional and
4404
- * fall back to values derived from `runtime.trackExecution()` when omitted.
5688
+ * Returns `{ output, cost?, metadata? }`. Both extras are optional and purely
5689
+ * informational: since 0.24 the eval runner MEASURES spend on the accounting
5690
+ * rail, so a `cost` reported here is recorded as a caller claim
5691
+ * (`EvalItem.callerReport`) and never becomes a run total. Reserved diagnostic
5692
+ * metadata keys (`models`, `tokens`, …) are likewise kept apart from the
5693
+ * runtime's own.
4405
5694
  *
4406
5695
  * Single source of truth for the eval-execution contract; consumed by
4407
5696
  * `registerEval`, `getRegisteredEval`, the `axl-eval` CLI, and
@@ -4466,6 +5755,8 @@ type RuntimeEvalConfigShape = {
4466
5755
  scorerConcurrency?: number;
4467
5756
  budget?: string;
4468
5757
  failOnScorerErrorRate?: number;
5758
+ /** Mirrors `EvalConfig.failOnItemErrorRate` (default `0.05`; `1` disables the gate). */
5759
+ failOnItemErrorRate?: number;
4469
5760
  metadata?: Record<string, unknown>;
4470
5761
  };
4471
5762
  type CreateContextOptions = {
@@ -4483,6 +5774,143 @@ type CreateContextOptions = {
4483
5774
  * 'drop-oldest-non-terminal'`. */
4484
5775
  events?: EventStreamOptions;
4485
5776
  };
5777
+ /**
5778
+ * Event-derived execution metadata returned by {@link AxlRuntime.trackExecution}
5779
+ * and {@link AxlRuntime.trackOutcome}.
5780
+ *
5781
+ * Derived from trace events, so it follows trace configuration — unlike
5782
+ * `Accounting`, which is independent of it.
5783
+ */
5784
+ type TrackExecutionMetadata = {
5785
+ /** Unique effective model URIs observed, in first-seen order. */
5786
+ models: string[];
5787
+ modelCallCounts?: Record<string, number>;
5788
+ /**
5789
+ * Agent token totals only — embedder tokens from semantic memory are a
5790
+ * different category and are excluded. Read `Accounting.usage` for the
5791
+ * folded, category-separated totals.
5792
+ */
5793
+ tokens: {
5794
+ input: number;
5795
+ output: number;
5796
+ reasoning: number;
5797
+ };
5798
+ agentCalls: number;
5799
+ /**
5800
+ * Unique workflow names observed during execution, ordered by first
5801
+ * appearance (outermost first for nested calls). Captured automatically
5802
+ * from `workflow_start` trace events — callers don't need to declare
5803
+ * anything. Parallel mechanism to `models`.
5804
+ */
5805
+ workflows: string[];
5806
+ /** Call counts per workflow, if workflows.length > 0. */
5807
+ workflowCallCounts?: Record<string, number>;
5808
+ };
5809
+ /**
5810
+ * Per-model sums of `agent_call_end.timing`, keyed by the same effective
5811
+ * model URI as `TrackExecutionMetadata.modelCallCounts`. Present only when at
5812
+ * least one tracked call reported timing, so absence means "nothing was
5813
+ * instrumented" rather than "everything took zero ms".
5814
+ *
5815
+ * `calls` counts the SUCCESSFUL timed calls only — it can be lower than the
5816
+ * same model's `modelCallCounts` entry when a provider omits `timing` or a
5817
+ * call failed. Divide a sum by `calls`, never by `modelCallCounts`.
5818
+ *
5819
+ * Failed calls are excluded even though they now report timing: a non-2xx
5820
+ * response is a measured round trip, but a rollup blending answers with
5821
+ * failures describes neither, and a fast 429 would improve a model's
5822
+ * apparent latency. The failures stay on the events themselves.
5823
+ *
5824
+ * `firstTokenMs` is streaming-only and is summed across the calls that
5825
+ * reported it; it is omitted entirely when no call did, so a non-streaming
5826
+ * model never reports a misleading `0`. Its denominator is `firstTokenCalls`,
5827
+ * NOT `calls` — the two travel together and are present or absent together,
5828
+ * so a mixed streaming/non-streaming model still yields an exact mean.
5829
+ *
5830
+ * Sums are per-call totals, so `wireMs` across concurrent calls can exceed
5831
+ * the wall clock of the tracked function — that is expected under fan-out and
5832
+ * is why nothing in the core sums timing at the ask level.
5833
+ */
5834
+ type ModelTimingRollup = Record<string, {
5835
+ calls: number;
5836
+ queuedMs: number;
5837
+ retryMs: number;
5838
+ wireMs: number;
5839
+ firstTokenMs?: number;
5840
+ /** Timed calls that reported a `firstTokenMs` — the denominator for it. */
5841
+ firstTokenCalls?: number;
5842
+ /**
5843
+ * The per-call blocks the sums were built from, in event order.
5844
+ * `length === calls`. Present only under `captureTimingSamples`, because
5845
+ * retaining it is O(calls) in memory and the sums alone are O(models) — a
5846
+ * caller that needs a real DISTRIBUTION (percentiles, min/max) cannot
5847
+ * recover one from sums, and everyone else should not pay for it.
5848
+ */
5849
+ samples?: CallTiming[];
5850
+ }>;
5851
+ /** Options for {@link AxlRuntime.trackOutcome}. */
5852
+ type TrackOutcomeOptions = {
5853
+ /**
5854
+ * How this scope's operations are classified in `Accounting.breakdown`.
5855
+ * Defaults to the enclosing scope's purpose, else `'generation'`.
5856
+ */
5857
+ purpose?: OperationPurpose;
5858
+ /** Stop admitting new paid operations once known spend reaches its limit. */
5859
+ admission?: AdmissionController;
5860
+ /** Collect the raw `AxlEvent[]` observed during the call. */
5861
+ captureTraces?: boolean;
5862
+ /** Retain the per-call `CallTiming` blocks behind the `modelTiming` sums. */
5863
+ captureTimingSamples?: boolean;
5864
+ /**
5865
+ * Opt in to bounded, redaction-aware capture of the provider-neutral REQUESTS
5866
+ * this scope submits, written into `sink` as JSONL.
5867
+ *
5868
+ * Diagnostics only: capture can be truncated, fail outright, or be turned off
5869
+ * entirely and `accounting` is byte-identical either way. The sink is never
5870
+ * awaited by a provider call. Nested scopes inherit the enclosing channel, so
5871
+ * declare it once at the outermost scope of a run.
5872
+ *
5873
+ * The CALLER owns the channel because the caller is the one that has to
5874
+ * `close()` it and read back its operation ledger after the run.
5875
+ */
5876
+ capture?: RequestCaptureChannel;
5877
+ /**
5878
+ * Stamp consumer-side identity onto every record captured inside this scope
5879
+ * (an eval's `caseIndex`, a judge's `scorer` name). Merged over the enclosing
5880
+ * scope's correlation, so a scorer scope keeps its item's case index.
5881
+ */
5882
+ captureCorrelation?: CaptureCorrelation;
5883
+ };
5884
+ /** The result of {@link AxlRuntime.trackOutcome} — never a rejection. */
5885
+ type TrackedOutcome<T> = ({
5886
+ status: 'fulfilled';
5887
+ value: T;
5888
+ }
5889
+ /** The ORIGINAL thrown value: primitives and frozen objects are untouched. */
5890
+ | {
5891
+ status: 'rejected';
5892
+ error: unknown;
5893
+ }) & {
5894
+ /** Authoritative spend. Independent of trace level, capture and redaction. */
5895
+ accounting: Accounting;
5896
+ metadata: TrackExecutionMetadata;
5897
+ modelTiming?: ModelTimingRollup;
5898
+ traces?: AxlEvent[];
5899
+ };
5900
+ /** The result of {@link AxlRuntime.trackExecution}. */
5901
+ type TrackExecutionResult<T> = {
5902
+ result: T;
5903
+ /** Compatibility view of `accounting.knownCost`. */
5904
+ cost: number;
5905
+ /** Compatibility view of `accounting.completeness !== 'complete'`. When true,
5906
+ * `cost` is a LOWER BOUND. */
5907
+ unpriced: boolean;
5908
+ /** The authoritative record `cost` and `unpriced` are derived from. */
5909
+ accounting: Accounting;
5910
+ modelTiming?: ModelTimingRollup;
5911
+ traces?: AxlEvent[];
5912
+ metadata: TrackExecutionMetadata;
5913
+ };
4486
5914
  /**
4487
5915
  * The main entry point for executing Axl workflows.
4488
5916
  * Manages workflow registration, provider resolution, state storage, tracing, MCP servers,
@@ -4495,6 +5923,9 @@ declare class AxlRuntime extends EventEmitter {
4495
5923
  private tools;
4496
5924
  private agents;
4497
5925
  private providerRegistry;
5926
+ /** Raw adapter → its scoped facade. Keyed weakly so a registry that drops an
5927
+ * adapter does not pin it here. */
5928
+ private scopedProviders;
4498
5929
  private transcriptionProviderRegistry;
4499
5930
  private stateStore;
4500
5931
  private executions;
@@ -4547,7 +5978,181 @@ declare class AxlRuntime extends EventEmitter {
4547
5978
  * The Set is mutated by the chain's own `.finally` so entries clear as
4548
5979
  * soon as the work settles. */
4549
5980
  private persistInflight;
5981
+ /** Resolved diagnostic artifact backend, when `diagnostics.artifacts` is set. */
5982
+ private artifactStore?;
5983
+ private artifactLeaseMs;
5984
+ private artifactMaxHoldMs;
5985
+ private artifactSweepTimer?;
5986
+ /**
5987
+ * Eval history ids this process has established are gone.
5988
+ *
5989
+ * Two jobs. It makes a delete started here beat a correction already in
5990
+ * flight, which no store-side condition can do while the delete has not
5991
+ * landed yet. And it keeps the lazy first load from merging a row back in
5992
+ * from a snapshot taken before the eviction. Ids are minted per result and
5993
+ * never reused, so an entry can only ever be right.
5994
+ *
5995
+ * Bounded like the artifact store's deleted-id window, and for the same
5996
+ * reason: a long-lived server with TTL churn would otherwise grow it for the
5997
+ * life of the process. Both jobs are windows measured in the length of one
5998
+ * in-flight write or one lazy first load, so the oldest ids are the ones with
5999
+ * nothing left to protect. Past the window the row is simply re-read from the
6000
+ * store, which by then is the authority anyway.
6001
+ */
6002
+ private readonly deletedEvalIds;
6003
+ /** Lease-renewal timers for artifacts currently being written, by id. */
6004
+ private readonly artifactRenewals;
4550
6005
  constructor(config?: AxlConfig);
6006
+ /**
6007
+ * Resolve the artifact backend and arm reclamation.
6008
+ *
6009
+ * Two configuration errors are raised HERE rather than at capture time,
6010
+ * because both mean "this deployment can never host managed capture" and the
6011
+ * alternative is discovering it after an eval has already spent money:
6012
+ * a `diagnostics.artifacts` block with neither `store` nor `root`, and a
6013
+ * custom `StateStore` that cannot report eval retention.
6014
+ */
6015
+ private setUpDiagnosticArtifacts;
6016
+ /** The configured artifact backend, or `undefined` when capture is off. */
6017
+ getDiagnosticArtifactStore(): DiagnosticArtifactStore | undefined;
6018
+ private requireArtifactStore;
6019
+ /**
6020
+ * Reserve an artifact for `owner` and hand back a sink to write records into.
6021
+ *
6022
+ * Throws `AxlError('DIAGNOSTICS_UNAVAILABLE')` BEFORE any work when capture is
6023
+ * not configured, which is why `runEval` calls it ahead of loading the
6024
+ * dataset. The returned sink renews the staging lease as it writes, so a long
6025
+ * run is never swept out from under itself, while a run that dies leaves a
6026
+ * lease that expires and is reclaimed.
6027
+ */
6028
+ stageDiagnosticArtifact(owner: ArtifactOwner): Promise<{
6029
+ artifactId: string;
6030
+ sink: RequestCaptureSink;
6031
+ }>;
6032
+ /**
6033
+ * Hold a staged artifact's lease for as long as its writer lives, and hand
6034
+ * back the sink that writer appends through.
6035
+ *
6036
+ * The lease belongs to the WRITER's lifetime, not to its write rate. Renewing
6037
+ * from inside `append` looks equivalent and is not: a run that exhausts
6038
+ * `maxRunBytes` at minute two stops appending forever, and a run waiting on a
6039
+ * tool or a human approval may not call a provider for longer than a lease.
6040
+ * Either way the sweeper would delete a live run's evidence, and the run
6041
+ * would then fail to finalize the artifact it had been filling.
6042
+ */
6043
+ private holdStagedArtifact;
6044
+ /** Stop renewing a lease once the writer is done with the artifact. */
6045
+ private stopArtifactRenewal;
6046
+ /** Seal a staged artifact's manifest. Called before the history row is saved. */
6047
+ finalizeDiagnosticArtifact(artifactId: string, status: ArtifactStatus, reason?: string, redaction?: 'applied' | 'none'): Promise<ArtifactManifest>;
6048
+ /** Discard a staged artifact whose owner will never be written. */
6049
+ rollbackDiagnosticArtifact(artifactId: string): Promise<void>;
6050
+ /**
6051
+ * Copy a source run's artifact into a new owner (rescore provenance).
6052
+ *
6053
+ * Bounded by `maxBytes`; a partial copy reports `truncated` so the caller can
6054
+ * finalize the new manifest honestly. Returns `undefined` when the source is
6055
+ * gone — a rescore of a run whose artifact was deleted still produces numbers,
6056
+ * it just reports its diagnostics as `unavailable`.
6057
+ *
6058
+ * The copy comes back STAGED and lease-held, with a sink: the new owner
6059
+ * carries the source records forward AND keeps writing its own calls into the
6060
+ * same artifact, which is what makes one rescore result hold both halves of
6061
+ * its evidence.
6062
+ */
6063
+ copyDiagnosticArtifact(sourceId: string, owner: ArtifactOwner, opts: {
6064
+ maxBytes: number;
6065
+ }): Promise<{
6066
+ artifactId: string;
6067
+ truncated: boolean;
6068
+ bytes: number;
6069
+ /** What the SOURCE manifest said about the copied bytes. Never inferred. */
6070
+ redaction: 'applied' | 'none';
6071
+ sink: RequestCaptureSink;
6072
+ } | undefined>;
6073
+ /**
6074
+ * Read a committed artifact's records, or `undefined`.
6075
+ *
6076
+ * Two logical checks stand between a caller and the bytes, because the bytes
6077
+ * outlive the store that governs them: the owning history row must still
6078
+ * exist, and the mirrored `expiresAt` must not have passed. A Redis row that
6079
+ * aged out while this process was offline therefore reads as unavailable the
6080
+ * moment it is asked for, not whenever the sweeper next runs.
6081
+ */
6082
+ openDiagnosticArtifact(artifactId: string): Promise<OpenedArtifact | undefined>;
6083
+ /**
6084
+ * Reclaim artifacts nothing can legitimately read any more.
6085
+ *
6086
+ * Removed: everything marked `delete_pending` (a delete that did not finish),
6087
+ * every `committed` artifact whose owner row is gone or whose expiry has
6088
+ * passed, and every `staged` artifact whose lease ran out. A staged artifact
6089
+ * with a LIVE lease is never touched — that is an active writer, and sweeping
6090
+ * it would race a run that is still producing evidence.
6091
+ */
6092
+ reconcileDiagnosticArtifacts(): Promise<{
6093
+ removed: string[];
6094
+ }>;
6095
+ /**
6096
+ * Rewrite the history row that owned an artifact the sweep has just removed.
6097
+ *
6098
+ * Best effort by design: reclaiming the bytes is the operation that had to
6099
+ * succeed, and a state store that cannot take the correction must not turn a
6100
+ * routine sweep into a throwing one. The next `saveEvalResult` or artifact
6101
+ * read corrects the row anyway, because both already downgrade a dangling id.
6102
+ */
6103
+ private downgradeReclaimedOwner;
6104
+ /**
6105
+ * Write back a history row this process corrected in memory — but ONLY while
6106
+ * the store still holds it.
6107
+ *
6108
+ * The history cache is process-global, loaded once and evicted only by
6109
+ * `deleteEvalResult`. It therefore outlives whatever the STORE decided: a
6110
+ * Redis TTL that elapsed, a delete by another process, a
6111
+ * right-to-be-forgotten request. A blind `saveEvalResult` of a cached row
6112
+ * resurrects all of it — item inputs, outputs and scores — and on Redis
6113
+ * re-SETs it with a fresh full TTL window, silently extending the retention
6114
+ * the operator configured. Correcting a row must never be able to un-delete
6115
+ * one, so the store is asked first and the stale cache entry is dropped when
6116
+ * the answer is no.
6117
+ *
6118
+ * A store with no retention view cannot answer, and a blind write is exactly
6119
+ * what this exists to prevent, so nothing is written: the cache is corrected
6120
+ * and the store keeps what it decided. In practice the question does not
6121
+ * arise, because configuring artifacts at all requires `getEvalRetention`.
6122
+ */
6123
+ private persistCorrectedRow;
6124
+ /**
6125
+ * Drop a row from the history cache and refuse to re-add it.
6126
+ *
6127
+ * The cache is process-global and loaded once, so a row the STORE has
6128
+ * dropped — expired, deleted here or elsewhere — would otherwise be served
6129
+ * for the life of the process, and an in-flight first load whose snapshot
6130
+ * predates the eviction would merge it straight back in.
6131
+ */
6132
+ private forgetEvalRow;
6133
+ /** The artifact id an eval result carries, when it carries one. */
6134
+ private artifactIdOf;
6135
+ /**
6136
+ * Rewrite an entry's diagnostics to say plainly that its evidence is gone.
6137
+ *
6138
+ * Mutates the entry in place on purpose: the caller usually holds the same
6139
+ * `EvalResult` object it is about to return to a user, and a result that
6140
+ * claims `complete` while storage has nothing is the failure being fixed.
6141
+ * The dangling id is cleared too — `''` is the sentinel every lifecycle path
6142
+ * already skips.
6143
+ */
6144
+ private downgradeDiagnostics;
6145
+ /**
6146
+ * The artifact id `ownerId` actually owns, or `undefined`.
6147
+ *
6148
+ * `diagnostics.artifactId` is data on a result object, and a result can carry
6149
+ * an id that belongs to a DIFFERENT run — a rescore that degraded while
6150
+ * naming its source, or an imported blob. Committing or deleting on the
6151
+ * strength of that field alone lets one entry rewrite or destroy another
6152
+ * entry's evidence, so ownership is confirmed against the manifest before
6153
+ * any lifecycle write.
6154
+ */
6155
+ private ownedArtifactId;
4551
6156
  /**
4552
6157
  * Whether `config.trace.redact` is enabled on this runtime. A narrow
4553
6158
  * boolean getter is preferred over exposing the full config because:
@@ -4663,12 +6268,22 @@ declare class AxlRuntime extends EventEmitter {
4663
6268
  signal?: AbortSignal;
4664
6269
  /**
4665
6270
  * When `true`, populate `EvalItem.traces` on every item (success + failure
4666
- * paths). Forwards to `runEval({ captureTraces: true })`, which wraps each
4667
- * item's execution in `runtime.trackExecution({ captureTraces: true })`.
6271
+ * paths). Forwards to `runEval({ captureTraces: true })`, which opens each
6272
+ * item's accounting scope with `trackOutcome({ captureTraces: true })`.
4668
6273
  * Verbose-mode `agent_call_start.data.messages` snapshots are stripped from
4669
6274
  * captured traces to keep memory bounded.
4670
6275
  */
4671
6276
  captureTraces?: boolean;
6277
+ /**
6278
+ * Forwarded to `runEval({ captureRequests })`. Requires
6279
+ * `config.diagnostics.artifacts`; without it the run fails fast with
6280
+ * `AxlError('DIAGNOSTICS_UNAVAILABLE')` before any provider call.
6281
+ */
6282
+ captureRequests?: boolean | {
6283
+ maxRecordBytes?: number;
6284
+ maxRunBytes?: number;
6285
+ maxQueueBytes?: number;
6286
+ };
4672
6287
  }): Promise<unknown>;
4673
6288
  /** Get all execution info (running + completed + historical). */
4674
6289
  /** Normalize an execution loaded from a `StateStore` so the
@@ -4690,11 +6305,26 @@ declare class AxlRuntime extends EventEmitter {
4690
6305
  registerProvider(name: string, provider: Provider): void;
4691
6306
  /** Register a dedicated transcription adapter. It is never used for chat. */
4692
6307
  registerTranscriptionProvider(name: string, provider: TranscriptionProvider): void;
4693
- /** Resolve a provider:model URI to a Provider instance and model name. */
6308
+ /**
6309
+ * Resolve a `provider:model` URI to a provider and model name.
6310
+ *
6311
+ * **The returned provider is a scoped FACADE, not the registered instance.**
6312
+ * It forwards everything to the adapter — custom properties and accessors,
6313
+ * class private-field methods, property writes, capability methods, ordinary
6314
+ * `instanceof` — while routing `chat`/`stream` through accounting and budget
6315
+ * admission whenever a `trackOutcome` scope is active. Outside a scope it
6316
+ * delegates verbatim.
6317
+ *
6318
+ * Identity is stable per runtime per adapter (repeated calls return the same
6319
+ * facade), but `resolveProvider(uri).provider === registeredInstance` is now
6320
+ * `false`. See `docs/migration/eval-accounting.md`.
6321
+ */
4694
6322
  resolveProvider(uri: string): {
4695
6323
  provider: Provider;
4696
6324
  model: string;
4697
6325
  };
6326
+ /** One facade per raw adapter per runtime, so repeated resolution is stable. */
6327
+ private scopedProvider;
4698
6328
  /** Execute a workflow and return the result. */
4699
6329
  execute(name: string, input: unknown, options?: ExecuteOptions): Promise<unknown>;
4700
6330
  /** Execute a workflow and return a stream. */
@@ -4797,6 +6427,28 @@ declare class AxlRuntime extends EventEmitter {
4797
6427
  deleteEvalResult(id: string): Promise<boolean>;
4798
6428
  /** Get eval result history (most recent first). */
4799
6429
  getEvalHistory(): Promise<EvalHistoryEntry[]>;
6430
+ /**
6431
+ * One eval history entry by id, or `undefined`.
6432
+ *
6433
+ * A read that wants one entry should not have to materialize the whole
6434
+ * history to find it. Studio's diagnostics routes resolve an artifact through
6435
+ * a history id on every request, and `getEvalHistory()` copies the entire
6436
+ * array — every result's full `data` blob — before the caller discards all
6437
+ * but one of them.
6438
+ *
6439
+ * The lazy first load is still whole-history (that is how the cache is
6440
+ * populated); what this avoids is paying for a copy of it per request.
6441
+ *
6442
+ * The row is confirmed against the store's retention view before it is
6443
+ * served, and dropped when the store says it is gone. The cache outlives
6444
+ * whatever the store decided — a Redis TTL that elapsed, a delete elsewhere —
6445
+ * and this is the read a rescore resolves its SOURCE through, copying that
6446
+ * row's items into a brand-new result with a brand-new retention window. An
6447
+ * expired run could otherwise be republished indefinitely, one rescore at a
6448
+ * time. `getEvalHistory()` is still served from the cache unconfirmed; the
6449
+ * per-row check is affordable here because it is one row.
6450
+ */
6451
+ getEvalResult(id: string): Promise<EvalHistoryEntry | undefined>;
4800
6452
  /** List pending human decisions. */
4801
6453
  getPendingDecisions(): Promise<PendingDecision[]>;
4802
6454
  /** Resolve a pending human decision. */
@@ -4818,6 +6470,13 @@ declare class AxlRuntime extends EventEmitter {
4818
6470
  onProgress?: (event: EvalProgressEventShape) => void;
4819
6471
  signal?: AbortSignal;
4820
6472
  captureTraces?: boolean;
6473
+ /** Forwarded to `runEval({ captureRequests })`. See
6474
+ * {@link AxlRuntime.runRegisteredEval}. */
6475
+ captureRequests?: boolean | {
6476
+ maxRecordBytes?: number;
6477
+ maxRunBytes?: number;
6478
+ maxQueueBytes?: number;
6479
+ };
4821
6480
  }): Promise<unknown>;
4822
6481
  /**
4823
6482
  * Compare two evaluation results to detect regressions and improvements.
@@ -4839,26 +6498,55 @@ declare class AxlRuntime extends EventEmitter {
4839
6498
  unpriced: boolean;
4840
6499
  }>;
4841
6500
  /**
4842
- * Track cost and execution metadata across any runtime operations within the given function.
4843
- * Uses AsyncLocalStorage to scope attribution to specific execution IDs,
4844
- * making it correct with concurrent calls.
6501
+ * Run `fn` and ALWAYS return its outcome plus the authoritative
6502
+ * {@link Accounting} for everything paid that happened inside it.
6503
+ *
6504
+ * This is the accounting entry point. Unlike {@link trackExecution} it does
6505
+ * not throw on failure — a workflow that threw after a paid call still
6506
+ * returns that call's charge, which is what makes "what did the failed run
6507
+ * cost?" answerable. The thrown value is handed back verbatim in `error`:
6508
+ * primitives, frozen objects, `AbortError`, `ProviderError` and
6509
+ * `BudgetExceededError` all come back `===` what was thrown.
6510
+ *
6511
+ * Scopes nest. An operation settled inside a child `trackOutcome` is counted
6512
+ * exactly once in every enclosing scope, so a parent total is the sum of
6513
+ * disjoint operations. Concurrent `trackOutcome` calls on one runtime are
6514
+ * isolated: neither sees the other's operations, budget closure or spend.
6515
+ *
6516
+ * Pass `{ admission: new AdmissionController({ limit }) }` to stop admitting
6517
+ * new paid operations once known spend reaches the limit. Already-dispatched
6518
+ * work still settles and is still counted.
6519
+ *
6520
+ * `metadata`, `modelTiming` and `traces` remain EVENT-derived and therefore
6521
+ * depend on trace configuration; `accounting` never does.
6522
+ */
6523
+ trackOutcome<T>(fn: () => Promise<T>, options?: TrackOutcomeOptions): Promise<TrackedOutcome<T>>;
6524
+ /**
6525
+ * Throwing compatibility wrapper over {@link trackOutcome}.
6526
+ *
6527
+ * Returns the same shape it always has, now derived from the accounting rail
6528
+ * rather than summed from trace events: `cost` is `accounting.knownCost` and
6529
+ * `unpriced` is `accounting.completeness !== 'complete'`. For instrumented
6530
+ * paths the numbers are identical; they differ only where the trace rail used
6531
+ * to lose a charge (notably a leaf that never settled). `accounting` is
6532
+ * exposed alongside them so callers can migrate incrementally.
4845
6533
  *
4846
- * Returns cost (same as `trackCost`) plus metadata extracted from trace events:
4847
- * models (unique URIs), tokens (input/output/reasoning sums), and agent call count.
6534
+ * On failure it rethrows the ORIGINAL error and, under `captureTraces`,
6535
+ * attaches the non-enumerable `axlCapturedTraces` side channel — prefer
6536
+ * {@link trackOutcome}, which returns the accounting for a failed run too.
4848
6537
  *
4849
6538
  * ## Cost vs tokens semantics
4850
6539
  *
4851
- * - `cost` is the full aggregate across EVERY event with a top-level
4852
- * `event.cost` set: agent calls, tool calls, semantic memory ops, etc.
4853
- * This is the number to reconcile against your provider bill.
6540
+ * - `cost` covers EVERY settled operation: agent calls, tool calls, semantic
6541
+ * memory embeddings, transcriptions, declared external work. This is the
6542
+ * number to reconcile against your provider bill.
4854
6543
  *
4855
6544
  * - `metadata.tokens` is narrowly scoped to **agent** prompt/completion/
4856
6545
  * reasoning tokens. Embedder tokens from semantic `ctx.remember({embed:true})`
4857
6546
  * / `ctx.recall({query})` are deliberately NOT summed here — they're a
4858
6547
  * different category (input-only, different pricing, different model).
4859
- * Conflating them would make "prompt tokens" misleading in the UI. If you
4860
- * need embedder token counts, subscribe to `runtime.on('trace', ...)` and
4861
- * read `data.usage.tokens` on `memory_remember` / `memory_recall` events.
6548
+ * Conflating them would make "prompt tokens" misleading in the UI. Read
6549
+ * `accounting.usage` for the folded, category-separated totals.
4862
6550
  *
4863
6551
  * Pass `{ captureTraces: true }` to also collect the raw `AxlEvent[]` observed
4864
6552
  * during `fn()`. This is opt-in because it keeps every event in memory for the
@@ -4873,86 +6561,7 @@ declare class AxlRuntime extends EventEmitter {
4873
6561
  trackExecution<T>(fn: () => Promise<T>, options?: {
4874
6562
  captureTraces?: boolean;
4875
6563
  captureTimingSamples?: boolean;
4876
- }): Promise<{
4877
- result: T;
4878
- cost: number;
4879
- /** True when any tracked call was unpriced — `cost` is then a LOWER BOUND.
4880
- * Aggregate counterpart of `ExecutionInfo.unpriced`, via `isUnpricedLeaf`. */
4881
- unpriced: boolean;
4882
- /**
4883
- * Per-model sums of `agent_call_end.timing`, keyed by the same effective
4884
- * model URI as `metadata.modelCallCounts`. Present only when at least one
4885
- * tracked call reported timing, so absence means "nothing was instrumented"
4886
- * rather than "everything took zero ms".
4887
- *
4888
- * `calls` counts the SUCCESSFUL timed calls only — it can be lower than the
4889
- * same model's `modelCallCounts` entry when a provider omits `timing` or a
4890
- * call failed. Divide a sum by `calls`, never by `modelCallCounts`.
4891
- *
4892
- * Failed calls are excluded even though they now report timing: a non-2xx
4893
- * response is a measured round trip, but a rollup blending answers with
4894
- * failures describes neither, and a fast 429 would improve a model's
4895
- * apparent latency. The failures stay on the events themselves.
4896
- *
4897
- * `firstTokenMs` is streaming-only and is summed across the calls that
4898
- * reported it; it is omitted entirely when no call did, so a non-streaming
4899
- * model never reports a misleading `0`. Its denominator is `firstTokenCalls`,
4900
- * NOT `calls` — the two travel together and are present or absent together,
4901
- * so a mixed streaming/non-streaming model still yields an exact mean.
4902
- *
4903
- * Sums are per-call totals, so `wireMs` across concurrent calls can exceed
4904
- * the wall clock of `fn` — that is expected under fan-out and is why
4905
- * nothing in the core sums timing at the ask level.
4906
- *
4907
- * `samples` carries the raw per-call `CallTiming` blocks behind the sums, in
4908
- * `agent_call_end` order, so a caller that needs a DISTRIBUTION (percentiles,
4909
- * min/max) rather than a mean can compute one over real per-call values. It
4910
- * is the same data the sums were built from — `samples.length === calls` —
4911
- * and is deliberately not something callers should persist per item: the
4912
- * sums are the compact form, `samples` is the working form.
4913
- *
4914
- * It is **opt-in** via `captureTimingSamples` and the key is absent
4915
- * otherwise. Retaining it is O(calls) in memory for the lifetime of `fn`,
4916
- * which no caller should pay for a figure it never reads — the sums alone
4917
- * are O(models).
4918
- */
4919
- modelTiming?: Record<string, {
4920
- calls: number;
4921
- queuedMs: number;
4922
- retryMs: number;
4923
- wireMs: number;
4924
- firstTokenMs?: number;
4925
- /** Timed calls that reported a `firstTokenMs` — the denominator for it. */
4926
- firstTokenCalls?: number;
4927
- /** The per-call blocks the sums were built from, in event order.
4928
- * `length === calls`. Present only under `captureTimingSamples`. */
4929
- samples?: CallTiming[];
4930
- }>;
4931
- traces?: AxlEvent[];
4932
- metadata: {
4933
- models: string[];
4934
- modelCallCounts?: Record<string, number>;
4935
- /**
4936
- * Agent token totals only — does not include embedder tokens from
4937
- * semantic memory operations. See the method-level JSDoc above.
4938
- */
4939
- tokens: {
4940
- input: number;
4941
- output: number;
4942
- reasoning: number;
4943
- };
4944
- agentCalls: number;
4945
- /**
4946
- * Unique workflow names observed during execution, ordered by first
4947
- * appearance (outermost first for nested calls). Captured automatically
4948
- * from `workflow_start` trace events — callers don't need to declare
4949
- * anything. Parallel mechanism to `models`.
4950
- */
4951
- workflows: string[];
4952
- /** Call counts per workflow, if workflows.length > 0. */
4953
- workflowCallCounts?: Record<string, number>;
4954
- };
4955
- }>;
6564
+ }): Promise<TrackExecutionResult<T>>;
4956
6565
  /** Register an execution ID with the active cost scope for trackCost() attribution. */
4957
6566
  private registerWithCostScope;
4958
6567
  /**
@@ -5583,6 +7192,26 @@ declare const REDACTION_RULES: {
5583
7192
  declare function redactEvent(event: AxlEvent): AxlEvent;
5584
7193
  /** Redact either persisted schema without reinterpreting its lifecycle. */
5585
7194
  declare function redactHistoricalEvent(event: HistoricalAxlEvent): HistoricalAxlEvent;
7195
+ /**
7196
+ * Scrub a captured request record before it reaches a diagnostics sink.
7197
+ *
7198
+ * The same policy `agent_call_start` / `agent_call_end` / the gate events
7199
+ * already apply, expressed over the capture record's shape: prompts, responses,
7200
+ * gate reasons and correction text are user/LLM content and become
7201
+ * `[redacted]`; roles, tool NAMES, JSON-Schema parameters, option scalars,
7202
+ * usage, cost, timing and the `providerOptions` key list are structural and
7203
+ * survive, because a compliance-mode reader still has to be able to tell a
7204
+ * tool-choice bug from a token-budget bug.
7205
+ *
7206
+ * Rich media never had bytes in the record to begin with (the projection in
7207
+ * `diagnostics/capture.ts` replaces them with descriptors), but a descriptor's
7208
+ * `locator` and `label` can name a file or a URL, so they are scrubbed here
7209
+ * exactly as `redactInputDescriptor` scrubs them on the trace rail.
7210
+ *
7211
+ * Pure, like every other rule in this file: the caller decides whether
7212
+ * redaction is on, and this function unconditionally scrubs when called.
7213
+ */
7214
+ declare function redactCapturedRequest(record: CapturedRequestRecord): CapturedRequestRecord;
5586
7215
 
5587
7216
  /**
5588
7217
  * Typed provider errors + shared HTTP-failure classification.
@@ -5734,13 +7363,20 @@ declare class OpenAIResponsesProvider implements Provider {
5734
7363
  nativeStructuredOutputSupport(): 'schema';
5735
7364
  private baseUrl;
5736
7365
  private apiKeySource;
5737
- private governor?;
7366
+ /**
7367
+ * Namespaced so it cannot collide with a member a downstream subclass
7368
+ * declares. A plain property (not `#private` or a WeakMap keyed by `this`)
7369
+ * so a caller's own Proxy around the adapter still reaches it.
7370
+ */
7371
+ private readonly axlRateGovernors;
5738
7372
  constructor(options?: {
5739
7373
  apiKey?: ApiKeySource;
5740
7374
  baseUrl?: string;
5741
7375
  dangerouslyAllowInsecureHttp?: boolean;
5742
7376
  rateLimit?: RateLimitConfig;
5743
7377
  });
7378
+ /** The rate governor for one call to `model`, from the runtime's per-scope pool. */
7379
+ protected governorFor(model: string): ScopeGovernor;
5744
7380
  /** Resolve the API key for one request (supports an expiring-token callback). */
5745
7381
  private resolveKey;
5746
7382
  /** Build request headers from a resolved key (used by both chat and stream). */
@@ -5802,13 +7438,20 @@ declare class AnthropicProvider implements Provider {
5802
7438
  nativeStructuredOutputSupport(): 'unsupported';
5803
7439
  private baseUrl;
5804
7440
  private apiKeySource;
5805
- private governor?;
7441
+ /**
7442
+ * Namespaced so it cannot collide with a member a downstream subclass
7443
+ * declares. A plain property (not `#private` or a WeakMap keyed by `this`)
7444
+ * so a caller's own Proxy around the adapter still reaches it.
7445
+ */
7446
+ private readonly axlRateGovernors;
5806
7447
  constructor(options?: {
5807
7448
  apiKey?: ApiKeySource;
5808
7449
  baseUrl?: string;
5809
7450
  dangerouslyAllowInsecureHttp?: boolean;
5810
7451
  rateLimit?: RateLimitConfig;
5811
7452
  });
7453
+ /** The rate governor for one call to `model`, from the runtime's per-scope pool. */
7454
+ protected governorFor(model: string): ScopeGovernor;
5812
7455
  /** Resolve the API key for one request (supports an expiring-token callback). */
5813
7456
  private resolveKey;
5814
7457
  chat(messages: ChatMessage[], options: ChatOptions): Promise<ProviderResponse>;
@@ -5882,13 +7525,20 @@ declare class GeminiProvider implements Provider {
5882
7525
  private baseUrl;
5883
7526
  private apiKeySource;
5884
7527
  private callCounter;
5885
- private governor?;
7528
+ /**
7529
+ * Namespaced so it cannot collide with a member a downstream subclass
7530
+ * declares. A plain property (not `#private` or a WeakMap keyed by `this`)
7531
+ * so a caller's own Proxy around the adapter still reaches it.
7532
+ */
7533
+ private readonly axlRateGovernors;
5886
7534
  constructor(options?: {
5887
7535
  apiKey?: ApiKeySource;
5888
7536
  baseUrl?: string;
5889
7537
  dangerouslyAllowInsecureHttp?: boolean;
5890
7538
  rateLimit?: RateLimitConfig;
5891
7539
  });
7540
+ /** The rate governor for one call to `model`, from the runtime's per-scope pool. */
7541
+ protected governorFor(model: string): ScopeGovernor;
5892
7542
  /** Resolve the API key for one request (supports an expiring-token callback). */
5893
7543
  private resolveKey;
5894
7544
  chat(messages: ChatMessage[], options: ChatOptions): Promise<ProviderResponse>;
@@ -6187,8 +7837,15 @@ declare class MemoryStore implements StateStore {
6187
7837
  listExecutions(limit?: number): Promise<HistoricalExecutionInfo[]>;
6188
7838
  deleteExecution(executionId: string): Promise<boolean>;
6189
7839
  saveEvalResult(entry: EvalHistoryEntry): Promise<void>;
7840
+ /** Update-only write: see `StateStore.updateEvalResult`. */
7841
+ updateEvalResult(entry: EvalHistoryEntry): Promise<boolean>;
6190
7842
  listEvalResults(limit?: number): Promise<EvalHistoryEntry[]>;
6191
7843
  deleteEvalResult(id: string): Promise<boolean>;
7844
+ /** In-memory history never expires on its own — only existence is meaningful. */
7845
+ getEvalRetention(id: string): Promise<{
7846
+ exists: boolean;
7847
+ expiresAt?: number;
7848
+ }>;
6192
7849
  listSessions(): Promise<string[]>;
6193
7850
  private streamingEvents;
6194
7851
  appendStreamingEvents(executionId: string, events: AxlEvent[]): Promise<void>;
@@ -6275,8 +7932,20 @@ declare class SQLiteStore implements StateStore {
6275
7932
  listExecutions(limit?: number): Promise<HistoricalExecutionInfo[]>;
6276
7933
  deleteExecution(executionId: string): Promise<boolean>;
6277
7934
  saveEvalResult(entry: EvalHistoryEntry): Promise<void>;
7935
+ /**
7936
+ * Update-only write: see `StateStore.updateEvalResult`. `UPDATE ... WHERE id`
7937
+ * touches nothing when the row is gone, where `INSERT OR REPLACE` would
7938
+ * resurrect it — and SQLite history has no expiry, so a resurrection here is
7939
+ * permanent.
7940
+ */
7941
+ updateEvalResult(entry: EvalHistoryEntry): Promise<boolean>;
6278
7942
  listEvalResults(limit?: number): Promise<EvalHistoryEntry[]>;
6279
7943
  deleteEvalResult(id: string): Promise<boolean>;
7944
+ /** SQLite history has no automatic expiry — only existence is meaningful. */
7945
+ getEvalRetention(id: string): Promise<{
7946
+ exists: boolean;
7947
+ expiresAt?: number;
7948
+ }>;
6280
7949
  listSessions(): Promise<string[]>;
6281
7950
  saveMemory(scope: string, key: string, value: unknown): Promise<void>;
6282
7951
  getMemory(scope: string, key: string): Promise<unknown | null>;
@@ -6505,9 +8174,70 @@ declare class RedisStore implements StateStore {
6505
8174
  getExecution(executionId: string): Promise<HistoricalExecutionInfo | null>;
6506
8175
  listExecutions(limit?: number): Promise<HistoricalExecutionInfo[]>;
6507
8176
  deleteExecution(executionId: string): Promise<boolean>;
8177
+ /**
8178
+ * Write an eval history row.
8179
+ *
8180
+ * **Re-saving an existing eval result never extends its retention.** Only a
8181
+ * genuinely new key gets the configured window; an existing one keeps the
8182
+ * time it had left. A result is re-saved by corrections that have nothing to
8183
+ * do with the operator's retention policy — the diagnostics sweep rewriting a
8184
+ * row whose artifact it just reclaimed, a commit failure downgrading one —
8185
+ * and a plain `SET ... EX` would renew a full window on every one of them,
8186
+ * silently keeping item inputs, outputs and scores alive indefinitely on a
8187
+ * server that corrects rows often enough.
8188
+ *
8189
+ * The remaining window is read with `PTTL` and re-applied as `PX` rather than
8190
+ * using `KEEPTTL`. Both need an existence check the SET cannot share a
8191
+ * transaction with, so both race a key expiring in between — but the failure
8192
+ * modes differ, and only one is safe: `KEEPTTL` on a key that expired in the
8193
+ * gap recreates it with NO expiry at all, an immortal row holding exactly the
8194
+ * data retention exists to age out. Every `PX` branch leaves a bounded
8195
+ * lifetime, so the race costs at most one window's length, never the window
8196
+ * itself. It also carries no Redis version floor, where `KEEPTTL` needs 6.0.
8197
+ *
8198
+ * The index entries carry no TTL and age out lazily, as with `saveExecution`.
8199
+ */
6508
8200
  saveEvalResult(entry: EvalHistoryEntry): Promise<void>;
8201
+ /**
8202
+ * Update-only write: see `StateStore.updateEvalResult`.
8203
+ *
8204
+ * One `SET key value XX KEEPTTL`, which is atomic and therefore has no
8205
+ * check-then-write window at all: `XX` refuses a key that is not there, so a
8206
+ * delete or an expiry racing the correction wins, and `KEEPTTL` leaves the
8207
+ * remaining lifetime untouched, so the correction carries no retention
8208
+ * decision of its own.
8209
+ *
8210
+ * **Requires Redis 6.0 or newer** (`KEEPTTL`). An older server rejects the
8211
+ * option with a syntax error, which is translated into a
8212
+ * `REDIS_VERSION_UNSUPPORTED` {@link AxlError} naming the floor — a
8213
+ * misconfiguration, not a failed write, and the one thing in `RedisStore`
8214
+ * that needs 6.0. Silently resetting the retention window instead is the one
8215
+ * outcome this write exists to rule out.
8216
+ *
8217
+ * The index members are deliberately not re-added: the row already exists, so
8218
+ * they are already there, and writing them for a key that vanished is exactly
8219
+ * the resurrection this avoids.
8220
+ */
8221
+ updateEvalResult(entry: EvalHistoryEntry): Promise<boolean>;
6509
8222
  listEvalResults(limit?: number): Promise<EvalHistoryEntry[]>;
6510
8223
  deleteEvalResult(id: string): Promise<boolean>;
8224
+ /**
8225
+ * Existence + absolute expiry for one eval history row.
8226
+ *
8227
+ * `PTTL` is the authority here rather than the configured `ttls.evalHistory`:
8228
+ * the row may have been written under a different configuration, re-saved, or
8229
+ * had its TTL cleared out of band, and an artifact swept on a stale
8230
+ * assumption is deleted diagnostic evidence the user still owns.
8231
+ *
8232
+ * Redis expires the row server-side without telling anyone, so the absolute
8233
+ * `expiresAt` returned here is mirrored onto the artifact manifest and the
8234
+ * physical bytes are reclaimed by the next sweep (or by startup reconciliation
8235
+ * if the process was down when the key aged out) — eventual, not synchronous.
8236
+ */
8237
+ getEvalRetention(id: string): Promise<{
8238
+ exists: boolean;
8239
+ expiresAt?: number;
8240
+ }>;
6511
8241
  /**
6512
8242
  * Shared list-by-sorted-set implementation. Uses ZREVRANGEBYSCORE +
6513
8243
  * MGET when the ZSET is populated (post-migration). Falls back to the
@@ -6521,7 +8251,15 @@ declare class RedisStore implements StateStore {
6521
8251
  finalizeStreamingEvents(executionId: string): Promise<void>;
6522
8252
  listStreamingExecutions(): Promise<string[]>;
6523
8253
  getStreamingEvents(executionId: string): Promise<HistoricalAxlEvent[]>;
6524
- /** Close the Redis connection. */
8254
+ /**
8255
+ * Close the Redis connection.
8256
+ *
8257
+ * Idempotent: `runtime.shutdown()` closes the state store it was handed, so
8258
+ * a caller that also closes its own store is doing the ordinary thing and
8259
+ * must not get a rejected promise for it. `MemoryStore.close()` and
8260
+ * better-sqlite3's `close()` are both no-ops the second time; node-redis
8261
+ * instead rejects with "The client is closed", so absorb that here.
8262
+ */
6525
8263
  close(): Promise<void>;
6526
8264
  deleteCheckpoints(executionId: string): Promise<void>;
6527
8265
  }
@@ -6593,4 +8331,4 @@ declare class NoopSpanManager implements SpanManager {
6593
8331
  */
6594
8332
  declare function createSpanManager(config?: TelemetryConfig): Promise<SpanManager>;
6595
8333
 
6596
- export { AXL_EVENT_TYPES, AXL_EVENT_TYPES_V2, AXL_TOOL_LIFECYCLE_TYPES_V2, AZURE_PROFILE, type Agent, type AgentCallEndData, type AgentCallInfo, type AgentCallParams, type AgentCallStartData, type AgentConfig, AnthropicProvider, type AnyWorkflow, type ApiKeySource, type AskOptions, type AskScoped, type AuthHeader, type AwaitHumanData, type AwaitHumanOptions, type AwaitHumanResolvedData, type AxlConfig, AxlError, type AxlEvent, type AxlEventBase, AxlEventBus, type AxlEventOf, type AxlEventType, type AxlEventV2, type AxlEventV2Of, AxlRuntime, AxlStream, BEDROCK_PROFILE, BUILTIN_PROFILES, BudgetExceededError, type BudgetOptions, type BudgetResult, CONFIG_CANDIDATES, COST_BEARING_LEAF_TYPES, type CallTiming, type CapabilityFlags, type ChatMessage, type ChatOptions, type CheckpointEventData, type CoalescedPartialObject, type CreateContextOptions, DEEPSEEK_PROFILE, type DecisionCleanupFailedEvent, type DelegateData, type DelegateOptions, type Effort, type EffortResolution, type EmbedResult, type EmbedUsage, type Embedder, type EvalExecuteWorkflow, type EvalHistoryEntry, type EventStreamOptions, EventStreamOverflowError, type ExecuteOptions, type ExecutionInfo, type ExecutionInfoV1, type ExecutionInfoV2, type ExecutionState, GROQ_PROFILE, GeminiProvider, GeminiTranscriptionProvider, type GuardrailBlockHandler, type GuardrailData, GuardrailError, type GuardrailResult, type GuardrailsConfig, type HandoffDescriptor, type HandoffRecord, type HandoffReturnData, type HandoffStartData, type HistoricalAxlEvent, type HistoricalExecutionInfo, type HumanDecision, InMemoryVectorStore, type InputAudioPart, type InputContentPart, type InputGuardrail, type InputImagePart, type InputMediaSource, type InputModalitySupport, type InputTextPart, InvalidModelInputError, InvalidTranscriptionInputError, LLAMACPP_PROFILE, LMSTUDIO_PROFILE, type LegacyAxlEventV1, type LegacyExecutionInfoV1, MAX_INLINE_MODEL_INPUT_BYTES, MAX_INLINE_TRANSCRIPTION_BYTES, MISTRAL_PROFILE, type MapOptions, MaxTurnsError, McpManager, type McpServer, type McpServerConfig, type McpToolDefinition, type McpToolResult, type MemoryConfig, type MemoryEventData, MemoryManager, MemoryStore, type ModelInput, type ModelInputDescriptor, NoConsensus, NoopSpanManager, OLLAMA_PROFILE, OPENAI_PRICING, OPENAI_PROFILE, OPENROUTER_PROFILE, type ObservationStatus, type OpenAICompatibleOptions, OpenAICompatibleProvider, OpenAIEmbedder, OpenAIProvider, OpenAIResponsesProvider, OpenAITranscriptionProvider, OpenRouterTranscriptionProvider, type OutputGuardrail, type OutputValidator, type PendingDecision, type PerModel, type PricingSource, type PricingTable, type ProfileInputModalities, type Provider, type ProviderAdapter, type ProviderDiagnosticData, ProviderError, type ProviderInputValidationRequest, type ProviderInputValidationResult, type ProviderProfile, ProviderRegistry, type ProviderResponse, QuorumNotMet, REDACTED, REDACTION_RULES, type RaceOptions, type RateLimitConfig, RateLimiter, type ReasoningCapture, type ReasoningEmit, type ReasoningProfile, type ReasoningRoundTrip, type RecallOptions, type RecallResult, type RecordedAudioSource, RedisStore, type RememberOptions, type RememberResult, type ResolvedThinkingOptions, type Result, type RetryFeedbackHook, type RetryFeedbackInfo, type RetryFeedbackResult, type RetryPolicy, SGLANG_PROFILE, SQLiteStore, type SchemaCheckData, type SchemaDiagnosticData, type SchemaPromptOption, Session, type SessionOptions, type SpanHandle, type SpanManager, type SpawnOptions, SqliteVectorStore, StallTimeoutError, type StateStore, type StreamChunk, type StringDeltaData, type StringStreamEvent, type StringStreamFilter, type TelemetryConfig, ThinkTagScanner, type TimeoutBreakdown, TimeoutError, type Tool, type ToolApprovalData, type ToolArgumentIssue, type ToolCallCancellation, type ToolCallData, type ToolCallEndData, type ToolCallFailure, type ToolCallMessage, type ToolCallOutcome, type ToolCallRejectedData, type ToolCallRejectedEvent, type ToolCallStartData, type ToolCallStartDataV2, type ToolChoice, type ToolConfig, type ToolDeniedData, type ToolEventError, ToolFailure, type ToolFailureConstructor, type ToolFailureOptions, type ToolHooks, type ToolLifecycleEventV2, type ToolModelOutput, ToolModelOutputError, type Transcript, type TranscriptionCapabilities, TranscriptionOperationError, type TranscriptionProvider, TranscriptionProviderRegistry, type TranscriptionProviderRequest, type TranscriptionProviderResult, type TranscriptionRequest, UnsupportedEventSchemaVersionError, UnsupportedModelInputError, UnsupportedTranscriptionInputError, VLLM_PROFILE, type ValidateData, type ValidateResult, ValidationError, type VectorEntry, type VectorResult, type VectorStore, type VerifyData, VerifyError, type VerifyOptions, type VerifyRetry, type VoteOptions, type Workflow, type WorkflowConfig, WorkflowContext, type WorkflowContextInit, type WorkflowEndData, type WorkflowStartData, XAI_PROFILE, agent, createSpanManager, defineConfig, estimateOpenAICost, eventCostContribution, expandGlob, extractJson, extractThinkTags, findConfig, getEventSchemaVersion, getExecutionEventSchemaVersion, hasPositiveBillableWork, hasPositiveTokens, importModule, inputText, isCostBearingLeaf, isRetryableStatus, isRootLevel, isUnpricedLeaf, isUsableCost, needsTsxLoader, normalizeStoredExecution, openaiReasoningEmit, parsePartialJson, pickDefault, pickExport, priceFromTable, reasoningEffortEmit, reasoningObjectEmit, redactEvent, redactHistoricalEvent, registerConditions, resolveApiKey, resolvePerModel, resolveRuntime, resolveThinkingOptions, stringStreamFromEvents, summarizeModelInput, tool, workflow, zodToJsonSchema };
8334
+ export { AXL_EVENT_TYPES, AXL_EVENT_TYPES_V2, AXL_TOOL_LIFECYCLE_TYPES_V2, AZURE_PROFILE, type Accounting, type AccountingCompleteness, type AccountingReason, type AccountingUsage, AdmissionController, AdmissionDeniedError, type Agent, type AgentCallEndData, type AgentCallInfo, type AgentCallParams, type AgentCallStartData, type AgentConfig, AnthropicProvider, type AnyWorkflow, type ApiKeySource, type ArtifactManifest, type ArtifactOwner, type ArtifactStatus, type ArtifactWriteResult, type AskOptions, type AskScoped, type AuthHeader, type AwaitHumanData, type AwaitHumanOptions, type AwaitHumanResolvedData, type AxlConfig, AxlError, type AxlEvent, type AxlEventBase, AxlEventBus, type AxlEventOf, type AxlEventType, type AxlEventV2, type AxlEventV2Of, AxlRuntime, AxlStream, BEDROCK_PROFILE, BUILTIN_PROFILES, BudgetExceededError, type BudgetOptions, type BudgetResult, CONFIG_CANDIDATES, COST_BEARING_LEAF_TYPES, type CallTiming, type CapabilityFlags, type CaptureCorrelation, type CapturedCorrection, type CapturedError, type CapturedMessage, type CapturedOperationRef, type CapturedPhase, type CapturedRequest, type CapturedRequestRecord, type CapturedResponse, type ChatMessage, type ChatOptions, type CheckpointEventData, type CoalescedPartialObject, type CostProvenance, type CreateContextOptions, DEEPSEEK_PROFILE, DEFAULT_MAX_QUEUE_BYTES, DEFAULT_MAX_RECORD_BYTES, DEFAULT_MAX_RUN_BYTES, type DecisionCleanupFailedEvent, type DelegateData, type DelegateOptions, type DiagnosticArtifactStore, type DiagnosticArtifactsConfig, type DiagnosticsConfig, type DispatchAdmission, type Effort, type EffortResolution, type EmbedResult, type EmbedUsage, type Embedder, type EvalExecuteWorkflow, type EvalHistoryEntry, type EvalProgressEventShape, type EventStreamOptions, EventStreamOverflowError, type ExecuteOptions, type ExecutionInfo, type ExecutionInfoV1, type ExecutionInfoV2, type ExecutionState, type ExternalOperationDescriptor, type ExternalOperationReport, FileDiagnosticArtifactStore, GROQ_PROFILE, GeminiProvider, GeminiTranscriptionProvider, type GuardrailBlockHandler, type GuardrailData, GuardrailError, type GuardrailResult, type GuardrailsConfig, type HandoffDescriptor, type HandoffRecord, type HandoffReturnData, type HandoffStartData, type HistoricalAxlEvent, type HistoricalExecutionInfo, type HumanDecision, InMemoryVectorStore, type InputAudioPart, type InputContentPart, type InputGuardrail, type InputImagePart, type InputMediaSource, type InputModalitySupport, type InputTextPart, InvalidModelInputError, InvalidTranscriptionInputError, LLAMACPP_PROFILE, LMSTUDIO_PROFILE, type LegacyAxlEventV1, type LegacyExecutionInfoV1, MAX_INLINE_MODEL_INPUT_BYTES, MAX_INLINE_TRANSCRIPTION_BYTES, MISTRAL_PROFILE, type MapOptions, MaxTurnsError, McpManager, type McpServer, type McpServerConfig, type McpToolDefinition, type McpToolResult, type MemoryConfig, type MemoryEventData, MemoryManager, MemoryStore, type ModelInput, type ModelInputDescriptor, type ModelTimingRollup, NoConsensus, NoopSpanManager, OLLAMA_PROFILE, OPENAI_PRICING, OPENAI_PROFILE, OPENROUTER_PROFILE, type ObservationStatus, type OpenAICompatibleOptions, OpenAICompatibleProvider, OpenAIEmbedder, OpenAIProvider, OpenAIResponsesProvider, OpenAITranscriptionProvider, OpenRouterTranscriptionProvider, type OpenedArtifact, type OperationKind, type OperationPurpose, type OutputGuardrail, type OutputValidator, type PendingDecision, type PerModel, type PricingSource, type PricingTable, type ProfileInputModalities, type Provider, type ProviderAdapter, type ProviderDiagnosticData, ProviderError, type ProviderInputValidationRequest, type ProviderInputValidationResult, type ProviderProfile, ProviderRegistry, type ProviderResponse, QuorumNotMet, REDACTED, REDACTION_RULES, type RaceOptions, type RateLimitConfig, RateLimiter, type ReasoningCapture, type ReasoningEmit, type ReasoningProfile, type ReasoningRoundTrip, type RecallOptions, type RecallResult, type RecordedAudioSource, RedisStore, type RememberOptions, type RememberResult, RequestCaptureChannel, type RequestCaptureOptions, type RequestCaptureSink, type RequestCaptureStatus, type ResolvedThinkingOptions, type Result, type RetryFeedbackHook, type RetryFeedbackInfo, type RetryFeedbackResult, type RetryPolicy, type RuntimeEvalConfigShape, SGLANG_PROFILE, SQLiteStore, type SchemaCheckData, type SchemaDiagnosticData, type SchemaPromptOption, Session, type SessionOptions, type SpanHandle, type SpanManager, type SpawnOptions, SqliteVectorStore, type StagedArtifact, StallTimeoutError, type StateStore, type StreamChunk, type StringDeltaData, type StringStreamEvent, type StringStreamFilter, type TelemetryConfig, ThinkTagScanner, type TimeoutBreakdown, TimeoutError, type Tool, type ToolApprovalData, type ToolArgumentIssue, type ToolCallCancellation, type ToolCallData, type ToolCallEndData, type ToolCallFailure, type ToolCallMessage, type ToolCallOutcome, type ToolCallRejectedData, type ToolCallRejectedEvent, type ToolCallStartData, type ToolCallStartDataV2, type ToolChoice, type ToolConfig, type ToolDeniedData, type ToolEventError, ToolFailure, type ToolFailureConstructor, type ToolFailureOptions, type ToolHooks, type ToolLifecycleEventV2, type ToolModelOutput, ToolModelOutputError, type TrackExecutionMetadata, type TrackExecutionResult, type TrackOutcomeOptions, type TrackedOutcome, type Transcript, type TranscriptionCapabilities, TranscriptionOperationError, type TranscriptionProvider, TranscriptionProviderRegistry, type TranscriptionProviderRequest, type TranscriptionProviderResult, type TranscriptionRequest, UnsupportedEventSchemaVersionError, UnsupportedModelInputError, UnsupportedTranscriptionInputError, VLLM_PROFILE, type ValidateData, type ValidateResult, ValidationError, type VectorEntry, type VectorResult, type VectorStore, type VerifyData, VerifyError, type VerifyOptions, type VerifyRetry, type VoteOptions, type Workflow, type WorkflowConfig, WorkflowContext, type WorkflowContextInit, type WorkflowEndData, type WorkflowStartData, XAI_PROFILE, agent, createSpanManager, defineConfig, estimateOpenAICost, eventCostContribution, expandGlob, externalOperation, extractJson, extractThinkTags, findConfig, getEventSchemaVersion, getExecutionEventSchemaVersion, hasPositiveBillableWork, hasPositiveTokens, importModule, inputText, isAdmissionDeniedError, isCostBearingLeaf, isRetryableStatus, isRootLevel, isUnpricedLeaf, isUsableCost, needsTsxLoader, normalizeStoredExecution, openaiReasoningEmit, parsePartialJson, pickDefault, pickExport, priceFromTable, reasoningEffortEmit, reasoningObjectEmit, redactCapturedRequest, redactEvent, redactHistoricalEvent, registerConditions, resolveApiKey, resolvePerModel, resolveRuntime, resolveThinkingOptions, stringStreamFromEvents, summarizeModelInput, tool, workflow, zodToJsonSchema };