All files / src/sdk circuit-breaker.ts

91.66% Statements 22/24
78.94% Branches 15/19
100% Functions 6/6
100% Lines 21/21

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74      17x                             6x 6x 6x             43x           43x   43x       30x       4x 3x       13x   13x 2x 2x     11x   11x 11x 6x               8x 8x 8x        
import type { ErrorCode } from '../types/index.js';
import { sanitizeErrorMessage } from './errors.js';
 
const DEFAULT_MAX_CONSECUTIVE_PROVIDER_FAILURES = 5;
 
type CircuitBreakerCode = Extract<ErrorCode, 'auth_failed' | 'provider_unavailable' | 'invalid_model_selector'>;
 
export interface CircuitBreakerReason {
  code: CircuitBreakerCode;
  message: string;
}
 
interface ProviderFailureCircuitBreakerOptions {
  maxConsecutiveProviderFailures?: number;
  abortController?: AbortController;
}
 
function providerUnavailableMessage(count: number, lastMessage: string): string {
  const detail = sanitizeErrorMessage(lastMessage).trim();
  const suffix = detail ? ` Last error: ${detail}` : '';
  return `Provider unavailable after ${count} consecutive failures. Warden stopped early.${suffix}`;
}
 
/**
 * Tracks unrecoverable provider failures across a Warden run.
 */
export class ProviderFailureCircuitBreaker {
  private consecutiveProviderFailures = 0;
  private openReason?: CircuitBreakerReason;
  private readonly maxConsecutiveProviderFailures: number;
  private readonly abortController?: AbortController;
 
  constructor(options: ProviderFailureCircuitBreakerOptions = {}) {
    this.maxConsecutiveProviderFailures =
      options.maxConsecutiveProviderFailures ?? DEFAULT_MAX_CONSECUTIVE_PROVIDER_FAILURES;
    this.abortController = options.abortController;
  }
 
  get reason(): CircuitBreakerReason | undefined {
    return this.openReason;
  }
 
  recordSuccess(): void {
    if (this.openReason) return;
    this.consecutiveProviderFailures = 0;
  }
 
  recordFailure(code: ErrorCode, message: string): void {
    Iif (this.openReason) return;
 
    if (code === 'auth_failed' || code === 'invalid_model_selector') {
      this.open({ code, message });
      return;
    }
 
    Iif (code !== 'provider_unavailable') return;
 
    this.consecutiveProviderFailures++;
    if (this.consecutiveProviderFailures >= this.maxConsecutiveProviderFailures) {
      this.open({
        code,
        message: providerUnavailableMessage(this.consecutiveProviderFailures, message),
      });
    }
  }
 
  private open(reason: CircuitBreakerReason): void {
    this.openReason = reason;
    Eif (!this.abortController?.signal.aborted) {
      this.abortController?.abort();
    }
  }
}