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
| Pattern | Complexity | Parallelism | Resilience | Best For |
|---|---|---|---|---|
| Auto-Correction | Low | None | Medium | Simple fix-and-verify tasks |
| Retry w/ Backoff | Low | None | High | Transient failure handling |
| Circuit Breaker | Medium | None | High | Preventing runaway failure |
| Supervisor | Medium | High | Very High | Multi-component systems |
| Pipeline | Medium | None | Medium | Multi-stage workflows |
| Fan-Out/Fan-In | High | Very High | Medium | Parallelizable tasks |
| State Machine | High | None | High | Complex 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