intermediatearchitecture-patternsdesign-patternsarchitectureloop-engineeringpatterns

Loop Engineering Design Patterns

Essential design patterns for loop engineering — retry, circuit breaker, supervisor, pipeline, and state machine patterns for autonomous AI agent loops.

Loop Engineering Design Patterns

Design patterns are reusable solutions to recurring problems in software architecture. In loop engineering, they provide battle-tested structures for building reliable, efficient, and maintainable autonomous AI agent loops. Whether you are building a simple auto-correction cycle or a complex multi-agent orchestration system, understanding these patterns is essential.

This guide covers seven foundational design patterns used in agentic loop systems, complete with ASCII diagrams, code examples, and practical guidance on when to apply each one.

Why Design Patterns Matter for Autonomous AI Loops

Traditional software design patterns (Factory, Observer, Strategy) were designed for deterministic systems. Loop engineering introduces a fundamentally different set of challenges:

  • Non-deterministic outputs: The LLM may produce different code on each iteration
  • External tool dependencies: Loops interact with file systems, shells, and APIs
  • Unbounded execution: A loop can theoretically run forever if convergence fails
  • Context drift: As loops iterate, accumulated context can degrade quality
  • Cascading failures: One failed tool call can destabilize the entire loop

Design patterns for autonomous coding address these challenges by providing structured approaches to common failure modes. They give you a shared vocabulary for discussing loop architectures and a proven starting point for new implementations.

┌─────────────────────────────────────────────────────────┐
│              Loop Engineering Pattern Landscape          │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  Structural Patterns          Control Flow Patterns     │
│  ┌─────────────────┐          ┌──────────────────┐    │
│  │ Pipeline         │          │ Auto-Correction   │    │
│  │ Fan-Out/Fan-In   │          │ Retry w/ Backoff  │    │
│  │ State Machine    │          │ Circuit Breaker    │    │
│  └─────────────────┘          └──────────────────┘    │
│                                                         │
│  Resilience Patterns            Orchestration Patterns  │
│  ┌─────────────────┐          ┌──────────────────┐    │
│  │ Circuit Breaker  │          │ Supervisor        │    │
│  │ Retry            │          │ Pipeline         │    │
│  └─────────────────┘          │ Fan-Out/Fan-In   │    │
│                                └──────────────────┘    │
└─────────────────────────────────────────────────────────┘

Pattern 1: Auto-Correction Loop

The auto-correction loop is the most fundamental pattern in loop engineering. It follows a simple act-verify-fix cycle: the agent performs an action, checks the result against expected outcomes, and corrects any discrepancies.

This pattern is the backbone of tools like Claude Code and Aider, where the AI writes code, runs tests, and fixes failures iteratively.

  ┌──────────┐
  │  START   │
  └────┬─────┘
       │
       ▼
  ┌──────────┐
  │  REASON  │ Analyze task, plan approach
  └────┬─────┘
       │
       ▼
  ┌──────────┐
  │   ACT    │ Write code, modify files
  └────┬─────┘
       │
       ▼
  ┌──────────┐     ┌──────────┐
  │  VERIFY  │────▶│  PASS?   │
  └────┬─────┘     └────┬─────┘
       │                │
       │           YES  │  NO
       │           ┌────┘
       │           ▼
       │      ┌──────────┐
       │      │  ANALYZE │ Identify failure cause
       │      └────┬─────┘
       │           │
       │           ▼
       │      ┌──────────┐
       │      │   FIX    │ Correct the issue
       │      └────┬─────┘
       │           │
       └───────────┘ (back to VERIFY)
       │
       ▼
  ┌──────────┐
  │  RETURN  │ Success
  └──────────┘

TypeScript Implementation

interface LoopResult {
  success: boolean;
  output: string;
  iterations: number;
}

async function autoCorrectionLoop(
  task: string,
  maxIterations: number = 10
): Promise<LoopResult> {
  let context = `Task: ${task}`;

  for (let i = 0; i < maxIterations; i++) {
    // REASON + ACT: Generate or fix code
    const action = await llm.generate(context);

    // ACT: Apply the changes
    const applyResult = await applyChanges(action);

    // VERIFY: Run tests or validation
    const verifyResult = await runTests();

    if (verifyResult.passed) {
      return {
        success: true,
        output: applyResult.diff,
        iterations: i + 1
      };
    }

    // FIX: Analyze failure and update context
    context += `\n\nIteration ${i + 1} failed:\n`;
    context += `Errors: ${verifyResult.errors.join(', ')}\n`;
    context += `Please fix these errors.`;
  }

  return {
    success: false,
    output: 'Max iterations reached',
    iterations: maxIterations
  };
}

When to Use This Pattern

  • Simple code modification tasks with clear pass/fail criteria
  • When you have reliable test suites for verification
  • Single-file or small-scope changes
  • As the inner loop within more complex patterns

Pattern 2: Retry with Exponential Backoff

When an AI agent loop encounters transient failures (rate limits, network timeouts, API errors), a retry strategy with exponential backoff provides resilience without overwhelming the failing service.

This pattern is critical for loop engineering in production environments where external dependencies can be unreliable.

  Request ──▶ Failure?
                │
           YES  │  NO
           ┌────┘
           ▼
  ┌──────────────────────┐
  │ Wait: baseDelay *    │
  │ 2^attemptCount       │
  │ + randomJitter        │
  └──────────┬───────────┘
             │
             ▼
  ┌──────────────────────┐
  │ attemptCount++       │
  │ maxAttempts reached? │
  └──────────┬───────────┘
             │
        YES  │  NO
        ┌────┘
        ▼
  ┌──────────┐
  │  ABORT   │
  └──────────┘

TypeScript Implementation

async function retryWithBackoff<T>(
  fn: () => Promise<T>,
  options: {
    maxAttempts?: number;
    baseDelayMs?: number;
    maxDelayMs?: number;
    jitter?: boolean;
  } = {}
): Promise<T> {
  const {
    maxAttempts = 5,
    baseDelayMs = 1000,
    maxDelayMs = 30000,
    jitter = true
  } = options;

  let lastError: Error | undefined;

  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error as Error;

      // Don't retry on permanent errors
      if (isPermanentError(error)) {
        throw error;
      }

      if (attempt < maxAttempts - 1) {
        const delay = Math.min(
          baseDelayMs * Math.pow(2, attempt),
          maxDelayMs
        );
        const jitterMs = jitter
          ? Math.random() * delay * 0.25
          : 0;
        await sleep(delay + jitterMs);

        console.log(
          `Retry ${attempt + 1}/${maxAttempts} ` +
          `after ${Math.round(delay + jitterMs)}ms`
        );
      }
    }
  }

  throw lastError;
}

When to Use This Pattern

  • API calls to LLM providers (rate limit handling)
  • Network-dependent tool calls (HTTP requests, package installs)
  • File system operations that may have transient locking issues
  • Any external dependency with known transient failure modes

Pattern 3: Circuit Breaker

The circuit breaker pattern prevents a failing component from dragging down the entire system. When failures exceed a threshold, the circuit "opens" and stops attempting the operation, allowing the system to fail fast and potentially use an alternative approach.

In agentic loop systems, circuit breakers prevent runaway loops that burn through tokens and API credits on hopeless tasks.

  ┌─────────────────────────────────────────┐
  │           CIRCUIT BREAKER                │
  │                                         │
  │   ┌────────┐    failure    ┌─────────┐  │
  │   │ CLOSED ├──────────────▶│  OPEN   │  │
  │   │ (pass) │  threshold    │ (block) │  │
  │   └───┬────┘               └────┬────┘  │
  │       ▲                         │        │
  │       │       timeout            │        │
  │       └─────────────────────────┘        │
  │       │                         │        │
  │       ▼                  success │        │
  │   ┌────────┐               ┌────┴────┐    │
  │   │ CLOSED │◀──────────────│HALF-OPEN│    │
  │   │        │  (test probe) │         │    │
  │   └────────┘               └─────────┘    │
  │                                         │
  └─────────────────────────────────────────┘

TypeScript Implementation

enum CircuitState {
  CLOSED = 'CLOSED',
  OPEN = 'OPEN',
  HALF_OPEN = 'HALF_OPEN'
}

class CircuitBreaker<T> {
  private state: CircuitState = CircuitState.CLOSED;
  private failureCount = 0;
  private successCount = 0;
  private lastFailureTime = 0;

  constructor(
    private fn: () => Promise<T>,
    private options: {
      failureThreshold: number;
      recoveryTimeoutMs: number;
      halfOpenMaxAttempts: number;
      onOpen?: () => void;
      onClose?: () => void;
    }
  ) {}

  async execute(): Promise<T> {
    if (this.state === CircuitState.OPEN) {
      if (Date.now() - this.lastFailureTime > this.options.recoveryTimeoutMs) {
        this.state = CircuitState.HALF_OPEN;
        this.successCount = 0;
      } else {
        throw new Error('Circuit breaker is OPEN');
      }
    }

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

  private onSuccess(): void {
    this.failureCount = 0;
    if (this.state === CircuitState.HALF_OPEN) {
      this.successCount++;
      if (this.successCount >= this.options.halfOpenMaxAttempts) {
        this.state = CircuitState.CLOSED;
        this.options.onClose?.();
      }
    }
  }

  private onFailure(): void {
    this.failureCount++;
    this.lastFailureTime = Date.now();
    if (this.failureCount >= this.options.failureThreshold) {
      this.state = CircuitState.OPEN;
      this.options.onOpen?.();
    }
  }
}

// Usage in an agentic loop
const llmCircuit = new CircuitBreaker(
  () => llm.complete(prompt),
  {
    failureThreshold: 3,
    recoveryTimeoutMs: 30000,
    halfOpenMaxAttempts: 2,
    onOpen: () => console.warn('LLM circuit opened — switching to fallback'),
    onClose: () => console.info('LLM circuit closed — resuming normal ops')
  }
);

When to Use This Pattern

  • LLM API calls with rate limits or service degradation
  • Expensive tool operations (builds, deployments)
  • Sub-loops within a multi-agent system
  • Any operation where repeated failures indicate a systemic issue

Pattern 4: Supervisor Pattern

The supervisor pattern monitors one or more child loops and restarts them when they fail. This pattern is essential for long-running autonomous coding systems where individual sub-tasks may crash or hang.

  ┌──────────────────────────────────┐
  │          SUPERVISOR              │
  │                                  │
  │   ┌──────────┐    ┌──────────┐  │
  │   │ Child 1  │    │ Child 2  │  │
  │   │ (Loop A) │    │ (Loop B) │  │
  │   └────┬─────┘    └────┬─────┘  │
  │        │               │        │
  │        │ crash         │ ok      │
  │        ▼               │        │
  │   ┌──────────┐        │        │
  │   │ RESTART  │        │        │
  │   │ max 3x   │        │        │
  │   └────┬─────┘        │        │
  │        │               │        │
  │   3x crash?            │        │
  │   ┌────┴─────┐        │        │
  │   │ ESCALATE │        │        │
  │   │ to human │        │        │
  │   └──────────┘        │        │
  │                        │        │
  │   Monitor health       │        │
  │   Collect metrics      │        │
  │   Enforce timeouts     │        │
  └──────────────────────────────────┘

TypeScript Implementation

interface ChildLoop {
  id: string;
  run: () => Promise<void>;
  maxRestarts: number;
}

class LoopSupervisor {
  private children = new Map<string, {
    loop: ChildLoop;
    restartCount: number;
    status: 'running' | 'stopped' | 'failed';
  }>();

  addChild(loop: ChildLoop): void {
    this.children.set(loop.id, {
      loop,
      restartCount: 0,
      status: 'stopped'
    });
  }

  async startAll(): Promise<void> {
    const promises = Array.from(this.children.values()).map(
      child => this.monitorChild(child)
    );
    await Promise.allSettled(promises);
  }

  private async monitorChild(
    child: typeof this.children extends Map<string, infer V> ? V : never
  ): Promise<void> {
    while (true) {
      try {
        child.status = 'running';
        await Promise.race([
          child.loop.run(),
          timeout(30_000) // per-loop timeout
        ]);
        child.status = 'stopped';
        break; // normal completion
      } catch (error) {
        child.restartCount++;
        if (child.restartCount >= child.loop.maxRestarts) {
          child.status = 'failed';
          console.error(
            `Child ${child.loop.id} exceeded max restarts. Escalating.`
          );
          break;
        }
        console.warn(
          `Restarting child ${child.loop.id} ` +
          `(${child.restartCount}/${child.loop.maxRestarts})`
        );
      }
    }
  }
}

When to Use This Pattern

  • Multi-agent systems with independent sub-loops
  • Long-running batch processing with multiple tasks
  • Systems where individual component failures should not crash the whole
  • Production deployments requiring self-healing behavior

Pattern 5: Pipeline Pattern

The pipeline pattern chains multiple loop stages in sequence, where the output of one stage feeds into the next. Each stage has its own loop logic and convergence criteria.

This is the standard architecture for complex autonomous coding workflows: plan, implement, test, review, deploy.

  ┌─────────┐   ┌─────────────┐   ┌─────────┐   ┌─────────┐
  │  PLAN   │──▶│  IMPLEMENT  │──▶│  TEST   │──▶│ REVIEW  │──▶ DONE
  │  loop   │   │  loop       │   │  loop   │   │  loop   │
  └─────────┘   └─────────────┘   └─────────┘   └─────────┘
       │              │                │              │
       ▼              ▼                ▼              ▼
   ┌────────┐    ┌────────┐      ┌────────┐    ┌────────┐
   │ Plan   │    │ Code   │      │ Test   │    │ Code   │
   │ Output │    │ Changes│      │ Report │    │ Review │
   └────────┘    └────────┘      └────────┘    └────────┘

TypeScript Implementation

interface PipelineStage<TIn, TOut> {
  name: string;
  execute: (input: TIn) => Promise<TOut>;
  validate: (output: TOut) => boolean;
  maxRetries: number;
}

async function runPipeline<T>(
  stages: PipelineStage<unknown, T>[],
  initialInput: unknown
): Promise<T> {
  let currentInput = initialInput;

  for (const stage of stages) {
    let attempts = 0;
    let output: T;

    while (attempts < stage.maxRetries) {
      try {
        output = await stage.execute(currentInput) as T;
        if (stage.validate(output)) {
          currentInput = output;
          break;
        }
        throw new Error(`${stage.name} validation failed`);
      } catch (error) {
        attempts++;
        if (attempts >= stage.maxRetries) {
          throw new Error(
            `Pipeline failed at stage "${stage.name}" ` +
            `after ${attempts} attempts`
          );
        }
      }
    }
  }

  return currentInput as T;
}

// Usage
const codePipeline = runPipeline([
  {
    name: 'plan',
    execute: (task) => llm.generatePlan(task),
    validate: (plan) => plan.steps.length > 0,
    maxRetries: 3
  },
  {
    name: 'implement',
    execute: (plan) => implementCode(plan),
    validate: (code) => code.files.length > 0,
    maxRetries: 5
  },
  {
    name: 'test',
    execute: (code) => runTestSuite(code),
    validate: (results) => results.allPassed,
    maxRetries: 10
  }
], initialTask);

When to Use This Pattern

  • Multi-step coding workflows with distinct phases
  • CI/CD pipelines with AI-driven stages
  • Document processing pipelines
  • Any workflow where stages have clear input/output contracts

Pattern 6: Fan-Out/Fan-In

The fan-out/fan-in pattern distributes work across multiple parallel sub-loops, then aggregates their results. This is essential for multi-agent loop systems where tasks can be parallelized.

            ┌──────────────────┐
            │   ORCHESTRATOR   │
            │   (main loop)    │
            └────────┬─────────┘
                     │
              FAN-OUT│ (split task)
       ┌──────┬──────┼──────┬──────┐
       ▼      ▼      ▼      ▼      ▼
   ┌──────┐┌──────┐┌──────┐┌──────┐┌──────┐
   │Sub 1 ││Sub 2 ││Sub 3 ││Sub 4 ││Sub 5 │
   │Loop  ││Loop  ││Loop  ││Loop  ││Loop  │
   └──┬───┘└──┬───┘└──┬───┘└──┬───┘└──┬───┘
      │       │       │       │       │
       └──────┴──────┼──────┴──────┘
              FAN-IN │ (merge results)
                     ▼
            ┌──────────────────┐
            │   AGGREGATOR     │
            │  (combine +      │
            │   resolve        │
            │   conflicts)     │
            └──────────────────┘

When to Use This Pattern

  • Multi-file refactoring (one sub-loop per file)
  • Parallel API integrations
  • Distributed test execution
  • Any embarrassingly parallel task decomposition

Pattern 7: State Machine Loop

The state machine pattern uses explicit state transitions to control loop behavior. Each state has defined entry/exit actions and transition rules, making the loop's behavior predictable and auditable.

  ┌──────────────────────────────────────────────────────┐
  │               STATE MACHINE LOOP                     │
  │                                                      │
  │    ┌───────────┐  task_ready   ┌──────────────┐     │
  │    │   IDLE    │──────────────▶│   PLANNING    │     │
  │    └───────────┘               └──────┬───────┘     │
  │          ▲                          │               │
  │          │                   plan_ready                │
  │          │                          ▼               │
  │          │                   ┌──────────────┐        │
  │          │     task_done     │  EXECUTING   │        │
  │          │◀──────────────────└──────┬───────┘        │
  │          │                          │                │
  │          │              ┌───────────┼───────────┐    │
  │          │         need_fix│    need_test│  done  │    │
  │          │              ▼           ▼         │     │
  │          │       ┌───────────┐ ┌──────────┐  │     │
  │          │       │ FIXING    │ │ TESTING  │  │     │
  │          │       └─────┬─────┘ └────┬─────┘  │     │
  │          │             │            │        │     │
  │          │         test_fail    all_pass     │     │
  │          │             │            │        │     │
  │          │             └────────────┘────────┘     │
  │          │                   │                       │
  │          │              max_retries                   │
  │          │                   │                       │
  │          │                   ▼                       │
  │          │            ┌───────────┐                  │
  │          └────────────│   FAILED  │                  │
  │                       └───────────┘                  │
  └──────────────────────────────────────────────────────┘

TypeScript Implementation

type LoopState =
  | 'IDLE'
  | 'PLANNING'
  | 'EXECUTING'
  | 'TESTING'
  | 'FIXING'
  | 'COMPLETED'
  | 'FAILED';

type StateTransition = Record<LoopState, LoopState[]>;

const transitions: StateTransition = {
  IDLE:      ['PLANNING'],
  PLANNING:  ['EXECUTING', 'FAILED'],
  EXECUTING: ['TESTING', 'FIXING', 'COMPLETED'],
  TESTING:   ['COMPLETED', 'FIXING', 'FAILED'],
  FIXING:    ['TESTING', 'FAILED'],
  COMPLETED: [],
  FAILED:    []
};

class StateMachineLoop {
  private state: LoopState = 'IDLE';
  private iterationCount = 0;

  async run(task: string): Promise<void> {
    while (this.state !== 'COMPLETED' && this.state !== 'FAILED') {
      this.iterationCount++;

      switch (this.state) {
        case 'IDLE':
          this.transition('PLANNING');
          break;
        case 'PLANNING':
          const plan = await this.plan(task);
          if (plan.valid) {
            this.transition('EXECUTING');
          } else {
            this.transition('FAILED');
          }
          break;
        case 'EXECUTING':
          await this.execute(plan);
          this.transition('TESTING');
          break;
        case 'TESTING':
          const results = await this.test();
          if (results.allPassed) {
            this.transition('COMPLETED');
          } else if (this.iterationCount < 20) {
            this.transition('FIXING');
          } else {
            this.transition('FAILED');
          }
          break;
        case 'FIXING':
          await this.fix(results.errors);
          this.transition('TESTING');
          break;
      }
    }
  }

  private transition(newState: LoopState): void {
    const allowed = transitions[this.state];
    if (!allowed.includes(newState)) {
      throw new Error(
        `Invalid transition: ${this.state} -> ${newState}`
      );
    }
    console.log(`[${this.state}] -> [${newState}]`);
    this.state = newState;
  }
}

Pattern Comparison

PatternComplexityParallelismResilienceBest For
Auto-CorrectionLowNoneMediumSimple fix-and-verify tasks
Retry w/ BackoffLowNoneHighTransient failure handling
Circuit BreakerMediumNoneHighPreventing runaway failure
SupervisorMediumHighVery HighMulti-component systems
PipelineMediumNoneMediumMulti-stage workflows
Fan-Out/Fan-InHighVery HighMediumParallelizable tasks
State MachineHighNoneHighComplex state-dependent logic

Choosing the Right Pattern

Use this decision guide to select the appropriate pattern:

Start: What is your loop doing?
│
├── Fixing a single issue with clear tests?
│   └── Auto-Correction Loop
│
├── Calling unreliable external APIs?
│   ├── Need to prevent runaway? → Circuit Breaker
│   └── Simple retry OK? → Retry with Exponential Backoff
│
├── Running multiple independent tasks?
│   ├── Tasks are parallel? → Fan-Out/Fan-In
│   └── Tasks are sequential? → Pipeline
│
├── Managing multiple sub-loops that may fail?
│   └── Supervisor Pattern
│
└── Complex behavior with many states?
    └── State Machine Loop

Composition: Patterns Within Patterns

In practice, production loop engineering systems compose multiple patterns. For example, a pipeline may use circuit breakers for each stage, and each stage may internally use auto-correction loops. The supervisor pattern may oversee the entire pipeline, with fan-out/fan-in for parallelizable stages.

// Real-world composition example
const robustPipeline = new Supervisor([
  new PipelineStage({
    name: 'plan',
    loop: new CircuitBreaker(planLoop, { failureThreshold: 3 }),
    timeout: 60_000
  }),
  new PipelineStage({
    name: 'implement',
    loop: new FanOutFanIn({
      workers: fileCount,
      workerLoop: new AutoCorrectionLoop(implementFile, { maxIter: 10 })
    }),
    timeout: 300_000
  }),
  new PipelineStage({
    name: 'test',
    loop: new RetryWithBackoff(testLoop, { maxAttempts: 15 }),
    timeout: 120_000
  })
], { maxSystemRestarts: 3 });

Key Takeaways

  • Auto-Correction is the foundational pattern — every loop engineering system uses some form of act-verify-fix
  • Circuit Breakers prevent runaway loops from burning through tokens and API credits on hopeless tasks
  • Retry with Backoff handles transient failures gracefully but must distinguish permanent from transient errors
  • Supervisors provide self-healing for multi-component autonomous systems, with escalation to human operators
  • Pipelines structure complex workflows into discrete stages with clear contracts between them
  • Fan-Out/Fan-In enables parallel execution for tasks that decompose naturally into independent sub-tasks
  • State Machines make complex loop behavior explicit, auditable, and predictable — essential for production systems
  • In practice, compose multiple patterns together: a pipeline of circuit-broken stages, supervised by a state machine orchestrator