/** * Rate State Backend — the one swappable seam of the Play Execution Governor. * * Per-`(org, provider)` rate windows are shared across CJS runner processes. * Everything else the Governor owns stays local to one run attempt and threads * down the lineage via the snapshot. See ADR 0007 and CONTEXT.md. */ /** A single resolved rate rule for a bucket (from rate-limit-definitions). */ export interface PacingRule { readonly ruleId: string; readonly requestsPerWindow: number; readonly windowMs: number; /** Optional learned-rate ceiling in requests per second. */ readonly adaptiveMaxRps?: number | null; /** * Optional simultaneous-in-flight cap for this rule. * * The CJS runtime holds a reliable in-flight count and releases it on * permit.release. `requestsPerWindow`/`windowMs` enforce request rate. */ readonly maxConcurrency: number | null; } /** * Per-tool queue-hint metadata produced by the runtime tool catalog * (`src/lib/plays/runtime-tool-metadata.ts`) and surfaced to the runtime via * `ContextOptions.getToolQueueHints`. It is the raw provider rate-limit metadata * that the runtime maps into one {@link PacingRule} per hint before handing it * to the Governor's pacing resolver. The `provider` field identifies the pacing * bucket; `bucketId`/`operation` are descriptive and used for logging/grouping. */ export interface PlayQueueHint { bucketId: string; provider: string; operation: string; ruleId: string; requestsPerWindow: number; windowMs: number; adaptiveMaxRps?: number | null; maxConcurrency: number | null; } /** Handle returned by acquire(); release frees any concurrency held by the rules. */ export interface PacingPermit { release(): void; } /** * Discriminates the backend so the Governor can adapt its policy to it. Only * `app_runtime_postgres` (the Absurd/Node substrate's DB-backed pacer) is * db-authoritative: it runs the whole token-bucket + AIMD in Postgres, so the * Governor's in-memory adaptive admission becomes a passthrough and defers * pacing to the row. The in-memory backend leaves `kind` undefined. */ export type RateStateBackendKind = 'app_runtime_postgres'; export interface RateStateBackend { /** * When present, tells the Governor this backend owns pacing authoritatively. * Absent on the in-memory backend. */ readonly kind?: RateStateBackendKind; /** * Block until one outbound call is permitted for `bucketId` under all `rules` * (request windows and per-rule `maxConcurrency`), then debit and * return a permit. `bucketId` is `${orgId}:${provider}` so the window is global * per (org, provider). Resolves immediately when `rules` is empty (provider has * no configured limit — pacing is a no-op, the global tool-concurrency backstop * still applies). */ acquire(input: { bucketId: string; rateScopeToken?: string | null; rules: readonly PacingRule[]; signal?: AbortSignal; }): Promise; /** * Feed a server-observed Retry-After back so future acquires for this bucket * back off. Advisory and idempotent; never un-charges an in-flight call. */ penalize(input: { bucketId: string; rateScopeToken?: string | null; cooldownMs: number; }): void | Promise; /** Record confirmed provider success for adaptive recovery. */ observeSuccess?(input: { bucketId: string; rateScopeToken?: string | null; rules: readonly PacingRule[]; }): void; } const NOOP_PERMIT: PacingPermit = { release() {} }; /** Permit used when a bucket has no rules — nothing to debit or release. */ export function noopPacingPermit(): PacingPermit { return NOOP_PERMIT; }