/** * Circuit breaker for the Brain's LLM pool. * * The single-LLM tier walks its whole pool on every decision, giving each * target the full `decisionTimeoutMs`. When the pool is dead — expired key, * provider outage, offline machine — that costs `pool.length × timeout` PER * DECISION, forever, with no memory between calls. With a `fallbackModels` * chain of five and the default 15s timeout, a single Brain question stalls * the caller for over a minute, repeatedly, to reach a conclusion the * previous decision already reached. * * This breaker gives the pool a memory: * * - CLOSED — normal operation; every failure increments a counter. * - OPEN — after `failureThreshold` consecutive failures the tier is * skipped outright for `cooldownMs`, so decisions fall * straight through to the deterministic tiers instead of * paying the timeout again. * - HALF_OPEN — once the cooldown expires, ONE probe decision is allowed * through. Success closes the breaker; failure re-opens it * for another cooldown. * * Per-target health is tracked separately so `orderTargets()` can move a * known-bad model to the back of the pool without removing it — a model that * recovers is still tried, just last. * * @module brain-circuit */ export type BrainCircuitState = 'closed' | 'open' | 'half_open'; export interface BrainCircuitOptions { /** Consecutive failures before the breaker opens. Default 3. 0 disables. */ failureThreshold?: number | undefined; /** How long the breaker stays open before allowing a probe (ms). Default 60_000. */ cooldownMs?: number | undefined; /** Override the clock for deterministic tests. */ now?: (() => number) | undefined; } export interface BrainCircuitSnapshot { state: BrainCircuitState; consecutiveFailures: number; /** When the breaker last opened, or undefined while closed. */ openedAt: number | undefined; /** Per-target consecutive failure counts, worst first. */ unhealthyTargets: Array<{ label: string; failures: number; }>; } /** * Failure memory for the Brain LLM tier. Pure bookkeeping — it performs no * I/O and makes no decisions of its own; callers ask `shouldAttempt()` before * spending a timeout and report back via `recordSuccess` / `recordFailure`. */ export declare class BrainCircuitBreaker { private consecutiveFailures; private openedAt; /** Set while a half-open probe is in flight, so only one is allowed. */ private probeInFlight; private readonly targetFailures; private readonly failureThreshold; private readonly cooldownMs; private readonly now; constructor(opts?: BrainCircuitOptions); /** Disabled breakers never open, so callers can skip the state dance. */ get enabled(): boolean; state(): BrainCircuitState; /** * May the LLM tier be consulted right now? * * Returns false only while the breaker is fully open, or when a half-open * probe is already in flight — concurrent decisions must not all become * probes, or an outage would still cost the full timeout on every one. */ shouldAttempt(): boolean; recordSuccess(targetLabel?: string): void; recordFailure(targetLabel?: string): void; /** * Reorder a pool so targets with recent failures are tried last, preserving * relative order within each group. A bad model is deprioritised, never * dropped — it stays reachable the moment it recovers. */ orderTargets(targets: readonly T[], labelOf: (target: T) => string): T[]; snapshot(): BrainCircuitSnapshot; reset(): void; } //# sourceMappingURL=brain-circuit.d.ts.map