AgentStack
SKILL verified MIT Self-run

Agent Reliability

skill-marvinrichter-clarc-agent-reliability · by marvinrichter

Agent Reliability Patterns: retry with exponential backoff and jitter, timeout hierarchies (tool < agent < workflow), fallback chains, circuit breaker for agent calls, cost control (token budgets, model tiering), rate limiting, and observability (what to log per agent call).

No reviews yet
0 installs
17 views
0.0% view→install

Install

$ agentstack add skill-marvinrichter-clarc-agent-reliability

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Agent Reliability? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Agent Reliability

Production patterns for reliable, cost-efficient agent systems.

When to Activate

  • Building agents that call external APIs or other agents
  • Configuring retry logic and timeout hierarchies
  • Implementing fallback chains when primary agents fail
  • Controlling cost with token budgets and model tiering
  • Adding circuit breakers to isolate failing agent dependencies
  • Setting up structured logging for agent observability

Retry with Exponential Backoff + Jitter

interface RetryOptions {
  maxAttempts?: number;
  initialDelayMs?: number;
  maxDelayMs?: number;
  backoffFactor?: number;
  retryableErrors?: (err: Error) => boolean;
}

async function withRetry(
  fn: () => Promise,
  options: RetryOptions = {}
): Promise {
  const {
    maxAttempts = 3,
    initialDelayMs = 1000,
    maxDelayMs = 30000,
    backoffFactor = 2,
    retryableErrors = isRetryable,
  } = options;

  let lastError: Error;

  for (let attempt = 1; attempt  Promise
): Promise {
  // Check if this key was already processed
  const cached = await cache.get(idempotencyKey);
  if (cached) return cached;

  const result = await withRetry(fn);

  // Store result with TTL to prevent duplicate processing
  await cache.setEx(idempotencyKey, 3600, result);
  return result;
}

Timeout Hierarchies

Tool call timeout  (
  tool: () => Promise,
  timeoutMs = 15000
): Promise {
  return Promise.race([
    tool(),
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error(`Tool timeout after ${timeoutMs}ms`)), timeoutMs)
    ),
  ]);
}

// Agent-level timeout (wraps the full agent conversation loop)
async function runAgentWithTimeout(
  agent: () => Promise,
  timeoutMs = 60000
): Promise {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), timeoutMs);

  try {
    return await agent();
  } catch (err) {
    if (controller.signal.aborted) {
      throw new Error(`Agent timeout after ${timeoutMs}ms`);
    }
    throw err;
  } finally {
    clearTimeout(timeout);
  }
}

// Workflow-level timeout (outermost, controls entire pipeline)
const WORKFLOW_TIMEOUT_MS = 10 * 60 * 1000;  // 10 minutes

async function runWorkflow(goal: string): Promise {
  return runAgentWithTimeout(async () => {
    const planResult = await runAgentWithTimeout(planningAgent, 30000);
    const implementResult = await runAgentWithTimeout(implementationAgent, 120000);
    const reviewResult = await runAgentWithTimeout(reviewAgent, 60000);
    return { plan: planResult, implementation: implementResult, review: reviewResult };
  }, WORKFLOW_TIMEOUT_MS);
}

Fallback Chains

type AgentFn = () => Promise;

async function fallbackChain(
  agents: AgentFn[],
  context: string
): Promise {
  const errors: Error[] = [];

  for (const [i, agent] of agents.entries()) {
    try {
      const result = await agent();
      if (i > 0) {
        console.warn(`Used fallback agent #${i} for: ${context}`);
      }
      return result;
    } catch (err) {
      errors.push(err as Error);
      console.warn(`Agent ${i} failed: ${(err as Error).message}`);
    }
  }

  throw new Error(
    `All agents failed for: ${context}\n` +
    errors.map((e, i) => `  [${i}]: ${e.message}`).join('\n')
  );
}

// Example: Opus → Sonnet → Haiku fallback
const analysisWithFallback = () => fallbackChain([
  () => runWithModel('claude-opus-latest', task),      // Best quality
  () => runWithModel('claude-sonnet-latest', task),    // Balanced
  () => runWithModel('claude-haiku-latest', task, { simplified: true }),  // Fast, cheap
], 'code-analysis');

Circuit Breaker

Isolate a failing agent dependency so failures don't cascade.

type CircuitState = 'CLOSED' | 'OPEN' | 'HALF_OPEN';

class CircuitBreaker {
  private state: CircuitState = 'CLOSED';
  private failures = 0;
  private lastFailureTime = 0;

  constructor(
    private readonly failureThreshold = 5,
    private readonly recoveryTimeMs = 60000
  ) {}

  async call(fn: () => Promise): Promise {
    if (this.state === 'OPEN') {
      if (Date.now() - this.lastFailureTime > this.recoveryTimeMs) {
        this.state = 'HALF_OPEN';
        console.log('Circuit breaker: HALF_OPEN — probing recovery');
      } else {
        throw new Error('Circuit breaker OPEN — agent calls rejected');
      }
    }

    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (err) {
      this.onFailure();
      throw err;
    }
  }

  private onSuccess(): void {
    this.failures = 0;
    if (this.state === 'HALF_OPEN') {
      this.state = 'CLOSED';
      console.log('Circuit breaker: CLOSED — agent recovered');
    }
  }

  private onFailure(): void {
    this.failures++;
    this.lastFailureTime = Date.now();
    if (this.failures >= this.failureThreshold) {
      this.state = 'OPEN';
      console.error(`Circuit breaker: OPEN after ${this.failures} failures`);
    }
  }

  getState(): CircuitState { return this.state; }
}

// Usage
const codeReviewBreaker = new CircuitBreaker(5, 60_000);

async function safeCodeReview(code: string): Promise {
  return codeReviewBreaker.call(() => codeReviewAgent.run(code));
}

Observability — What to Log Per Agent Call

interface AgentCallLog {
  // Identity
  traceId: string;
  spanId: string;
  agentName: string;
  model: string;

  // Input/Output summary (never log full content in production)
  inputTokens: number;
  outputTokens: number;
  inputPreview: string;  // First 100 chars only
  outputPreview: string;

  // Performance
  latencyMs: number;
  toolCallCount: number;
  retryCount: number;

  // Outcome
  success: boolean;
  errorType?: string;  // rate_limit | timeout | invalid | etc.
  stopReason: string;  // end_turn | tool_use | max_tokens | stop_sequence
}

function logAgentCall(log: AgentCallLog): void {
  // Structured JSON log for log aggregation tools
  console.log(JSON.stringify({
    type: 'agent_call',
    timestamp: new Date().toISOString(),
    ...log,
  }));

  // Emit metrics for dashboards
  metrics.histogram('agent.latency_ms', log.latencyMs, { agent: log.agentName });
  metrics.increment('agent.calls', { agent: log.agentName, success: String(log.success) });
  metrics.histogram('agent.tokens', log.inputTokens + log.outputTokens, { agent: log.agentName });
}

Rate Limiting

> For general-purpose rate limiting patterns (sliding window, distributed rate limiting with Redis, per-user quotas) — see resilience-patterns. The token bucket below is specific to controlling Anthropic API call rates from agent code.

// Token bucket rate limiter for API calls
class TokenBucket {
  private tokens: number;
  private lastRefill: number;

  constructor(
    private readonly capacity: number,        // Max burst
    private readonly refillRate: number,       // Tokens per ms
  ) {
    this.tokens = capacity;
    this.lastRefill = Date.now();
  }

  async acquire(cost = 1): Promise {
    this.refill();

    if (this.tokens >= cost) {
      this.tokens -= cost;
      return;
    }

    // Wait for enough tokens
    const waitMs = (cost - this.tokens) / this.refillRate;
    await sleep(waitMs);
    this.tokens = 0;
  }

  private refill(): void {
    const now = Date.now();
    const elapsed = now - this.lastRefill;
    this.tokens = Math.min(this.capacity, this.tokens + elapsed * this.refillRate);
    this.lastRefill = now;
  }
}

// 10 requests/sec, burst up to 20
const rateLimiter = new TokenBucket(20, 10 / 1000);

async function rateLimitedAgentCall(fn: () => Promise): Promise {
  await rateLimiter.acquire();
  return fn();
}

Cost Control

Model Tiering Strategy

type TaskComplexity = 'simple' | 'medium' | 'complex';

function selectModel(complexity: TaskComplexity): string {
  switch (complexity) {
    case 'simple':
      return 'claude-haiku-latest';  // ~3x cheaper, fast
    case 'medium':
      return 'claude-sonnet-latest';           // Default for coding
    case 'complex':
      return 'claude-opus-latest';             // Deep reasoning only
  }
}

// Classify complexity before selecting model
async function classifyTaskComplexity(task: string): Promise {
  // Use cheap Haiku to decide if we need expensive Opus
  const response = await client.messages.create({
    model: 'claude-haiku-latest',
    system: 'Classify task complexity: reply with exactly "simple", "medium", or "complex".',
    messages: [{ role: 'user', content: task }],
    max_tokens: 10,
  });
  return response.content[0].text.trim() as TaskComplexity;
}

Token Budget Enforcement

interface TokenBudget {
  maxInputTokens: number;
  maxOutputTokens: number;
  warnAt: number;  // Fraction — e.g. 0.8 = warn at 80%
}

const AGENT_BUDGETS: Record = {
  'code-review': { maxInputTokens: 8000, maxOutputTokens: 2000, warnAt: 0.8 },
  'planning': { maxInputTokens: 4000, maxOutputTokens: 4000, warnAt: 0.8 },
  'summarization': { maxInputTokens: 16000, maxOutputTokens: 500, warnAt: 0.9 },
};

function enforceTokenBudget(
  agentName: string,
  inputTokens: number,
  maxOutputTokens: number
): number {
  const budget = AGENT_BUDGETS[agentName];
  if (!budget) return maxOutputTokens;

  if (inputTokens > budget.maxInputTokens) {
    throw new Error(`Input exceeds budget for ${agentName}: ${inputTokens} > ${budget.maxInputTokens}`);
  }
  if (inputTokens > budget.maxInputTokens * budget.warnAt) {
    console.warn(`Token budget warning for ${agentName}: ${inputTokens}/${budget.maxInputTokens} input tokens`);
  }

  return Math.min(maxOutputTokens, budget.maxOutputTokens);
}

For anti-patterns (retrying non-retryable errors, fixed sleep vs jitter, single timeout for all levels, circuit breaker threshold calibration, model over-selection), see skill agent-reliability-advanced.

Reference

  • multi-agent-patterns — orchestration, routing, parallelization, handoffs
  • observability — OpenTelemetry, distributed tracing, production metrics

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.