/** * Provider budgets — the runtime's half. * * See docs/proposals/providers-and-budgets.md. A session runs * `provider:model` and is charged to that provider. This module turns the * database's admission verdict into something the turn loop can act on, and * nothing more: the DECISION belongs to `cms_provider_check_turn`, because * the counters it judges are in that database. Splitting "read the numbers" * from "judge the numbers" across a process boundary would buy a pure * function and pay for it with two implementations that can disagree about * whether a session may run. * * What IS here is presentation and scheduling: how long a paused session * sleeps, what its wait says, and the pure window arithmetic the portal * needs to render a meter without asking the server. * * WHERE THE GATE RUNS. Inside the `runTurn` ACTIVITY, before the model is * touched. Activity bodies are not replay-frozen, so the check itself needs * no orchestration version. The POST-turn half does — see * schedulePostTurnContinuation in orchestration/turn.ts, and 1.0.69. * * FAILURE POSTURE: fail open. If the database cannot answer, the turn runs. * The counters are down too, so refusing would trade availability for the * enforcement of numbers nobody can currently read. */ export type BudgetPeriod = "day" | "week" | "month"; /** Why a session is waiting. The remedy differs for each, so the words must. */ export type BudgetPauseKind = "limit" | "allowance" | "hold" | "no_provider"; export interface BudgetRuleState { ruleId: string; providerName: string; period: BudgetPeriod; modelQualified: string | null; limitTokens: number; /** What the provider has spent this window, across everyone. */ usedTokens: number; /** The viewer's ceiling, or null when the allowance is full. */ ceilingTokens: number | null; /** The viewer's own spend this window, or null when there is no owner. */ yourUsedTokens: number | null; windowStartUtc: string; resetsAtUtc: string; } export interface BudgetPause { kind: BudgetPauseKind; provider: string | null; modelRef?: string | null; ruleId?: string; period?: BudgetPeriod; modelQualified?: string | null; limitTokens?: number; usedTokens?: number; ceilingTokens?: number; yourUsedTokens?: number; /** When the pause lifts on its own. Null for a hold with no end. */ resetsAtUtc?: string | null; } export interface TurnAdmission { verdict: "clear" | "paused" | "no_provider"; /** The provider that pays, resolved in the session owner's namespace. */ providerName: string | null; modelQualified: string | null; /** System sessions: never paused, still charged. */ exempt: boolean; pause: BudgetPause | null; rules: BudgetRuleState[]; } /** * A paused session sleeps until its reason expires — but never longer than * this. The durable timer is only the BACKSTOP: what normally wakes a * session is the change itself (a limit raised, an allowance widened, a hold * released, a missing provider created), which fires an immediate wake. The * cap exists for the case where that wake is missed, and for the two pauses * that have no natural expiry at all — an indefinite hold, and a provider * that does not exist. Six hours means a session can never be stranded for * more than a quarter of a day by a lost signal, at the cost of four cheap * re-checks a day: a re-check that still blocks costs no tokens, because the * gate runs before the model. */ export declare const MAX_BUDGET_WAIT_SECONDS: number; /** Below this, sleeping is pointless — the window is about to turn over. */ export declare const MIN_BUDGET_WAIT_SECONDS = 5; /** Deterministic small-integer seed from a session id. */ export declare function budgetJitterSeed(sessionId: string): number; /** * Spread wake-ups so every session waiting on the same limit does not * stampede the database at the same instant. Scales to the wait: five * minutes suits a daily reset and would oversleep a short window many times * over. The seed must be deterministic for the caller, because the * orchestration replays — the same inputs must produce the same delay. */ export declare function budgetWakeJitterMs(waitMs: number, seed: number): number; /** * How long to sleep. A pause with no natural end (an indefinite hold, a * provider that does not exist) sleeps the full backstop; everything else * sleeps until it expires, capped. */ export declare function budgetWaitSeconds(pause: BudgetPause, nowMs: number, sessionId?: string): number; /** * The sentence a person reads on a waiting session. It names the control * that would release it, because the remedy is different for each kind: a * limit needs raising, an allowance needs widening, a hold needs releasing, * and a missing provider needs creating (or the session switching model). */ export declare function budgetWaitReason(pause: BudgetPause): string; /** Normalize whatever the database returned into the runtime's shape. */ export declare function toTurnAdmission(row: any): TurnAdmission; /** * The gate's answer, as the turn loop wants it: either nothing (run the * turn) or the `wait` TurnResult that parks the session. A `wait` is what * the orchestration already knows how to make durable — a timer, a waiting * status, a wait_started event, and a resume that re-enters this same gate. */ export declare function admissionToWait(admission: TurnAdmission, sessionId: string, nowMs: number): { type: "wait"; seconds: number; reason: string; budget: true; } | null; /** * What a woken session is told. It is not a message from a person, and the * transcript should not read as though somebody typed it: the change that * released the session already happened, and all this does is get the * orchestration to ask the gate again. */ export declare const PROVIDER_BUDGET_WAKE_PROMPT: string; //# sourceMappingURL=provider-budgets.d.ts.map