import type { UsageSnapshot } from "@boardwalk-labs/workflow/runtime"; import type { Budget } from "../wire/manifest.js"; /** The three capped budget dimensions. Each parks at the budget gate on breach (SUSPEND_POLICY * Decision 3); `capBreachReason()` reports the first breached one. */ export type BudgetDimension = "usd" | "tokens" | "compute"; /** Per-million-token rates. */ export interface ModelRate { /** USD per million input tokens. */ inputPerMillion: number; /** USD per million output tokens. */ outputPerMillion: number; /** USD per million cache-read tokens (Anthropic). Defaults to inputPerMillion / 10. */ cacheReadPerMillion?: number; /** USD per million cache-write tokens (Anthropic). Defaults to inputPerMillion * 1.25. */ cacheWritePerMillion?: number; } export interface UsageDelta { inputTokens?: number; outputTokens?: number; cacheReadTokens?: number; cacheWriteTokens?: number; } /** * Cumulative usage carried forward from PRIOR worker sessions of the same run. Seeded from the * checkpoint on resume so budget caps bound the WHOLE run, not just the current session — a run * that sleeps / waits on a child / recovers from a crash must not get a fresh budget each time *. Zero for a fresh run. */ export interface PriorUsage { inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheWriteTokens: number; totalUsd: number; /** * Active execution time from prior sessions (ms). Only on-CPU turn time counts toward the * compute cap — sleep/wait pauses don't burn it (a run intentionally parked for a day must * not blow its `max_compute_seconds` budget on resume). */ activeMs: number; } export interface BudgetMeterOptions { budget?: Budget; /** Rate for the model the agent is configured with. */ rate: ModelRate; /** This SESSION's start time (ms). Per-session active duration is measured from here. */ startedAt: number; /** Cumulative usage from prior sessions of this run (resume). Defaults to zero. */ priorUsage?: PriorUsage; /** Injected clock for tests. */ now?: () => number; } export interface BudgetSnapshot { inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheWriteTokens: number; totalTokens: number; totalUsd: number; elapsedMs: number; } export declare class BudgetMeter { /** NOT readonly: the budget gate (docs/SUSPEND_POLICY.md Decision 3) raises a breached cap in * place when a responder approves more spend — on any dimension. The meter lives in the frozen * heap, so the wake mutates this instance and the blocked call proceeds against the new cap. */ private budget; /** Wall-clock ms excluded from the compute cap — time the run spent PARKED at the budget gate * (frozen or held). Parked time burns no `max_compute_seconds` (SUSPEND_POLICY Decision 3.4); * without this exclusion a compute park would instantly re-breach on wake, forever. */ private excludedIdleMs; private readonly rate; private readonly startedAt; private readonly prior; private readonly now; private inputTokens; private outputTokens; private cacheReadTokens; private cacheWriteTokens; private totalUsd; constructor(opts: BudgetMeterOptions); /** * Add a usage delta to the accumulator + recompute USD. `realCostUsd`, when provided, is the EXACT * upstream cost the broker observed for this turn (the managed provider's per-request cost) — used * verbatim so the `max_usd` cap tracks ACTUAL spend (already cache-discounted, model-correct). * Omitted for a BYO turn (no upstream price) or a managed turn whose cost the broker couldn't read, * which fall back to the representative-rate {@link costFor} estimate. Token counts accumulate on * both paths (they drive the `max_tokens` cap + the snapshot); only the USD basis differs. */ addUsage(delta: UsageDelta, realCostUsd?: number): void; /** * THIS SESSION's accumulator snapshot (excludes prior-session usage). Read by per-session token * metering — which reports token deltas to the platform (deferred; not yet wired into the brokered * loop) — and by audit/post-run cost rollups. Cap enforcement uses {@link cumulative} instead. */ snapshot(): BudgetSnapshot; /** Exclude `ms` of wall-clock from the compute cap — the budget gate's park window (mirrors * RuntimeFlusher.excludeIdle for billing). Called by the gate after every park, whichever * substrate: on the snapshot fleet the guest clock resyncs on wake so the measured park spans * the frozen window; on a hold substrate it spans the register-and-poll wait. */ excludeIdle(ms: number): void; /** * RUN-CUMULATIVE snapshot: this session's usage PLUS the prior sessions' usage seeded at * construction. Cap enforcement ({@link assertWithinCaps}) and checkpoint persistence use * this so a run that resumes after a sleep/wait/crash is bounded by its declared caps across * the whole run — not once per session. `snapshot()` stays session-local because * per-session token metering reports token deltas; seeding it would double-report usage. */ cumulative(): BudgetSnapshot; /** * The first breached cap (in the tokens → usd → compute order the assert has always used) with * the numbers + message the `BUDGET_EXCEEDED` error carries, or null while within every cap. * The one probe both {@link assertWithinCaps} and {@link capBreachReason} read from. */ private firstBreach; /** * Throws `AppError(BUDGET_EXCEEDED)` when any cap is breached. Call BETWEEN * turns (after `addUsage` reflects the latest delta) — that way the meter * tears the loop down before another LLM call is dispatched. */ assertWithinCaps(): void; /** * Raise `dimension`'s cap to `value` — the budget gate's approval path (docs/SUSPEND_POLICY.md * Decision 3). Only ever RAISES: a value at or below the current cap is ignored, so an approval * can't silently tighten a cap and re-park the run on the very next call. A no-op when the * workflow declared no budget (nothing to breach). Returns the cap now in force, or null when * there is no budget. */ raiseCap(dimension: BudgetDimension, value: number): number | null; /** The cap in force for `dimension`, or null when unset. The gate prompt reports it beside spend. */ cap(dimension: BudgetDimension): number | null; /** What `dimension` has consumed so far, on the SAME run-cumulative basis cap enforcement uses * (usd = real-cost dollars, tokens = conversation tokens, compute = active seconds). */ spent(dimension: BudgetDimension): number; /** * Predicate variant of {@link assertWithinCaps} for callers that handle the cap-hit path inline * (the budget gate parks on it; the legacy no-gate path throws on it). Returns the first breach * dimension or null. */ capBreachReason(): BudgetDimension | null; /** * Live budget state in the host protocol's `usage.get` shape: every dimension always present * as `{spent, cap, remaining}`, with `cap`/`remaining` null when uncapped. RUN-CUMULATIVE (the * same basis cap enforcement uses), so a program polling it sees the numbers the platform's * budget pause would act on. `tokens.spent` is conversation tokens (input + output) — the same * basis as the `max_tokens` cap. */ usageSnapshot(): UsageSnapshot; /** * USD cost for a single delta — the representative-rate ESTIMATE used only when the broker reports * no real upstream cost for the turn (a BYO-provider turn, or a managed turn whose cost tap missed). * Managed turns instead carry the managed provider's exact per-request cost through to {@link addUsage}. Public so * the loop can stamp the per-step cost without re-deriving the rate table. */ costFor(delta: UsageDelta): number; }