Auto-Correction Loop
Self-correction mechanisms, verifier/grader patterns, error classification, and retry strategies.
What Is an Auto-Correction Loop?
An auto-correction loop is a mechanism that enables an AI agent to detect failures in its own output, diagnose the root cause, and automatically retry or adjust its behavior without human intervention. It wraps the core agent loop in a protective layer that catches errors and applies recovery strategies.
The concept is not new in software engineering -- retry logic and circuit breakers are standard patterns. What is new is applying these patterns to LLM-generated outputs, where the failures can be subtle (a logically correct but semantically wrong function call) or dramatic (hallucinated API endpoints that do not exist).
LangChain's framework formalized this as the verification loop -- a grader checks the agent's output against a rubric and, if it fails, sends the result back with feedback. Graders can be deterministic (tests, linters) or agentic (LLM-as-a-Judge). This pattern is now implemented in production tools ranging from Claude Code (github.com/anthropics/claude-code) to SWE-Agent (github.com/princeton-nlp/SWE-Agent, 15K+ stars) and OpenHands (github.com/All-Hands-AI/OpenHands).
The Verifier/Grader Component
The central insight from the LangChain ecosystem is that the core agent loop gets work done, but it does not always produce correct work on the first pass. When consistency matters, wrapping the agent in a verification loop dramatically improves reliability.
How LangChain Implements the Verification Loop
In LangChain (github.com/langchain-ai/langchain), the verification loop follows this flow:
[Agent runs] -> [Grader checks output against rubric]
| |
v v
[Done if pass] [Send feedback and retry if fail]
For their internal documentation agent, the grader runs tests after each attempt: checking that all links resolve, all CI checks pass, and the diff is scoped to what was actually requested. No manual review is needed to catch those classes of error.
LangGraph (github.com/langchain-ai/langgraph) takes this further with graph-based agent orchestration, enabling conditional edges that route agent output through verification nodes. If verification fails, the graph loops back to the agent with feedback -- all as a defined state machine, not ad-hoc retry logic.
The Maker-Checker Split
A critical design principle shared across Claude Code, Codex CLI (github.com/openai/codex), and the broader agent ecosystem: the model that wrote the code should not be the one grading it.
Claude Code's approach applies this directly: after generating code, the agent can invoke tool commands to run tests, linters, or type checkers. The test suite acts as an impartial grader. In Codex CLI, sub-agents defined as TOML files in .codex/agents/ can be assigned different models for different roles -- your security reviewer can be a strong model on high effort while your explorer is a fast, read-only one.
SWE-Agent (github.com/princeton-nlp/SWE-Agent) demonstrates this in research: it operates on real GitHub issues, runs test commands to validate patches, and feeds failure output back to the agent for correction. The test suite is the ground-truth grader; the LLM only judges what tests cannot cover.
Deterministic Checkers vs LLM Judges
The production agent community has converged on a clear hierarchy of verification reliability:
| Checker Type | Reliability | Use Case | Bias Risk |
|---|---|---|---|
| Deterministic (tests, type-check, lint) | Highest | Code correctness, build integrity | None |
| Deterministic execution (run the code) | High | Runtime behavior, API responses | None |
| Separate LLM judge | Medium | Code quality, documentation style | Moderate |
| Same-model self-evaluation | Lowest | Quick triage | High (confirmation bias) |
Deterministic checkers outperform LLM-based self-evaluation in every documented benchmark. Whenever you can replace an LLM judge with a test suite, a type checker, or a build step, do it.
How Claude Code Handles Self-Correction
Claude Code (github.com/anthropics/claude-code, docs at code.claude.com/docs) implements auto-correction at multiple levels within its agent loop.
Tool Error Feedback Loop
When a tool call returns an error, Claude sees the error in its context window and can adjust its next action accordingly. This is the most basic form of self-correction: the agent's inner loop (Perceive, Reason, Plan, Act, Observe) naturally handles tool failures by observing the error and replanning.
For example, if a git commit fails because of a pre-commit hook, Claude reads the hook output, adjusts the files or commit message, and retries -- all within the same conversational turn.
The Test-Fix-Retry Pattern
Claude Code supports a common auto-correction workflow:
- The agent writes code
- Tests run automatically (via hooks or explicit agent action)
- If tests fail, the agent reads the failure output
- The agent diagnoses the issue and writes a fix
- Tests run again
- Repeat until tests pass or a maximum retry count is reached
Aider (github.com/paul-gauthier/aider, 30K+ stars) implements a Git-first version of this pattern. Aider automatically commits after each successful change and can run tests between iterations via its --auto-test flag, feeding failure output back into the LLM context for correction:
aider --model claude-3.5-sonnet --auto-test "pytest tests/"
The Anti-Pattern: Infinite Error Loops
- Exception handling failures -- uncaught errors crash the loop without recovery
- Blind retries -- retrying without diagnosing the root cause, burning tokens
- Context overflow -- accumulating error history until the context window fills
- Infinite loops -- the agent oscillates between fixes without converging
Specific anti-patterns include: fixing a bug and introducing a new one repeatedly, deleting tests to make them pass, and redefining completion criteria mid-task. Anthropic's official documentation addresses this with a test ratchet instruction: "It is unacceptable to remove or edit tests because this could lead to missing or buggy functionality." This explicit instruction in CLAUDE.md prevents the most common self-correction failure mode.
Error Classification: Recoverable vs Fatal
Not all errors should trigger a retry. Effective auto-correction requires classifying errors and applying the right strategy to each.
Recoverable Errors
These are transient failures where retrying with adjusted parameters has a reasonable chance of success:
| Error Type | Example | Recovery Strategy |
|---|---|---|
| Tool failure | Network timeout, rate limit | Retry with backoff |
| Test failure | Logic error caught by tests | Agent reads error, fixes code, re-runs |
| Type error | TypeScript compilation failure | Agent fixes type annotations |
| Lint error | Code style violation | Agent runs lint --fix or adjusts code |
| Partial success | Some tests pass, others fail | Agent fixes failing subset |
Fatal Errors
These are failures where retrying will not help:
| Error Type | Example | Recovery Strategy |
|---|---|---|
| Authentication failure | Invalid API credentials | Alert human, halt loop |
| Resource not found | Referenced file does not exist | Report error, skip task |
| Permission denied | Agent lacks required access | Alert human |
| Budget exceeded | Token limit reached | Save state, report partial results |
| Contradictory requirements | Task spec is impossible | Alert human for clarification |
Implementation
from enum import Enum
from dataclasses import dataclass
class ErrorSeverity(Enum):
RECOVERABLE = "recoverable"
FATAL = "fatal"
DEGRADED = "degraded"
@dataclass
class ClassifiedError:
message: str
severity: ErrorSeverity
recovery_hint: str | None = None
def classify_error(error: Exception, context: dict) -> ClassifiedError:
"""Classify an error to determine the appropriate recovery strategy."""
error_str = str(error).lower()
# Rate limits and transient failures
if any(kw in error_str for kw in ["rate limit", "timeout", "429", "503"]):
return ClassifiedError(
message=str(error),
severity=ErrorSeverity.RECOVERABLE,
recovery_hint="Retry with exponential backoff"
)
# Test failures -- the agent can fix these
if any(kw in error_str for kw in ["test failed", "assertion", "expected"]):
return ClassifiedError(
message=str(error),
severity=ErrorSeverity.RECOVERABLE,
recovery_hint="Read test output, diagnose, and fix code"
)
# Authentication and permissions -- fatal, need human
if any(kw in error_str for kw in ["unauthorized", "403", "401", "permission"]):
return ClassifiedError(
message=str(error),
severity=ErrorSeverity.FATAL,
recovery_hint="Alert human operator"
)
# Budget exceeded
if any(kw in error_str for kw in ["budget", "quota", "limit exceeded"]):
return ClassifiedError(
message=str(error),
severity=ErrorSeverity.FATAL,
recovery_hint="Save state and report partial results"
)
# Default: assume recoverable with a warning
return ClassifiedError(
message=str(error),
severity=ErrorSeverity.RECOVERABLE,
recovery_hint="Retry once, then escalate if it fails again"
)
Retry with Backoff Patterns
When a recoverable error is detected, the simplest recovery strategy is to retry the operation. However, naive retries can overwhelm failing services. Exponential backoff introduces increasing delays between retries.
The Backoff Formula
delay = min(base_delay * (2 ^ attempt) + jitter, max_delay)
- base_delay: Initial wait time (e.g., 1 second)
- attempt: Retry attempt number (0, 1, 2, ...)
- jitter: Random value to prevent thundering herd problems
- max_delay: Upper bound to prevent excessive waiting
Complete Auto-Correction Loop Implementation
import time
import random
import logging
logger = logging.getLogger(__name__)
class AutoCorrectionLoop:
"""Agent loop with auto-correction, error classification, and backoff."""
def __init__(self, agent, verifier, state_store,
max_retries: int = 5,
base_delay: float = 1.0,
max_delay: float = 60.0):
self.agent = agent
self.verifier = verifier
self.state_store = state_store
self.max_retries = max_retries
self.base_delay = base_delay
self.max_delay = max_delay
def run(self, task):
"""Execute the task with auto-correction."""
state = self.state_store.load(task.id) or {
"attempt": 0,
"best_result": None,
"best_score": 0.0,
"error_history": []
}
for attempt in range(state["attempt"], self.max_retries + 1):
try:
# Agent generates output
result = self.agent.execute(task)
# Verifier checks the output
verification = self.verifier.evaluate(
task, result
)
if verification.passed:
logger.info(
"Task %s passed verification on attempt %d",
task.id, attempt + 1
)
self.state_store.save(task.id, {
"status": "complete",
"result": result,
"attempts": attempt + 1
})
return result
# Verification failed but not fatal
if verification.score > state["best_score"]:
state["best_score"] = verification.score
state["best_result"] = result
# Provide feedback to the agent for the next attempt
state["attempt"] = attempt + 1
state["last_feedback"] = verification.feedback
self.state_store.save(task.id, state)
# Wait before retrying
delay = self._backoff(attempt)
logger.warning(
"Verification failed on attempt %d for task %s: %s. "
"Retrying in %.1fs",
attempt + 1, task.id, verification.feedback, delay
)
time.sleep(delay)
except FatalError as e:
logger.error("Fatal error for task %s: %s", task.id, e)
self.state_store.save(task.id, {
"status": "failed",
"error": str(e),
"severity": "fatal"
})
raise
except RecoverableError as e:
state["attempt"] = attempt + 1
state["error_history"].append(str(e))
state["last_feedback"] = f"Error: {e}"
self.state_store.save(task.id, state)
delay = self._backoff(attempt)
logger.warning(
"Recoverable error on attempt %d for task %s: %s. "
"Retrying in %.1fs",
attempt + 1, task.id, e, delay
)
time.sleep(delay)
# Exhausted retries -- return best result found
logger.warning(
"Max retries (%d) exceeded for task %s. "
"Returning best result (score: %.2f)",
self.max_retries, task.id, state["best_score"]
)
return state["best_result"]
def _backoff(self, attempt: int) -> float:
"""Calculate delay with exponential backoff and jitter."""
delay = min(
self.base_delay * (2 ** attempt),
self.max_delay
)
jitter = random.uniform(0, delay * 0.1)
return delay + jitter
Real-World Examples from Production Tools
SWE-Agent: Research-Grade Verification Loops
SWE-Agent (github.com/princeton-nlp/SWE-Agent, 15K+ stars) from Princeton NLP implements auto-correction as a core research pattern. Given a GitHub issue, SWE-Agent:
- Explores the repository structure
- Identifies relevant files
- Generates a patch
- Runs the project's test suite to verify
- If tests fail, reads the output and generates a corrected patch
- Repeats until tests pass or a maximum iteration limit is reached
The key insight from SWE-Agent's research: structured, executable verification commands (actual test runs) dramatically outperform asking the LLM to self-evaluate its own output.
OpenHands: Autonomous Multi-Step Correction
OpenHands (github.com/All-Hands-AI/OpenHands, formerly OpenDevin) implements auto-correction within its autonomous coding agent platform. The agent operates in a sandboxed environment with full access to a shell, browser, and file system. When code execution fails, OpenHands reads the error, adjusts the approach, and retries -- mimicking how a human developer would debug.
OpenHands supports configurable action limits and browser observation steps, which act as guardrails against infinite correction loops.
Aider: Git-First Correction with Auto-Test
Aider (github.com/paul-gauthier/aider, 30K+ stars) takes a Git-native approach to auto-correction. Every code change is automatically committed, creating a clean history of attempts. The --auto-test flag enables automatic verification:
# Run aider with Claude 3.5 Sonnet and automatic test verification
aider --model claude-3.5-sonnet --auto-test "python -m pytest tests/"
# Or with a specific test file for faster feedback
aider --model claude-3.5-sonnet --auto-test "python -m pytest tests/test_auth.py -v"
When tests fail, the failure output is injected back into the LLM's context. Aider also supports an --auto-commit flag that commits each successful correction, making it easy to roll back if an agent goes down a wrong path.
Cursor: Agent Mode with Multi-File Correction
This "chat first, then agent" pattern is a manual form of the maker-checker split -- you use a conversational, lower-stakes interaction to validate the approach before committing to Agent mode's broader automated corrections.
The Four Verification Architectures
The production agent landscape has converged on four distinct self-correction architectures:
-
Output Scoring (LLM-as-Judge): A separate model scores the output against criteria and provides a pass/fail signal. Used in MetaGPT (github.com/geekan/MetaGPT, 45K+ stars), which runs multiple agent roles (architect, project manager, engineer) with cross-validation between roles.
-
Reflexion Loops: The agent reflects on its own errors and generates a corrective context for the next attempt. CrewAI (github.com/crewAIInc/crewAI) supports this via task delegation where one agent's output is reviewed by another with a critiquing role.
-
Adversarial Debate: Multiple agents argue for/against an output to surface errors through disagreement. AutoGen (github.com/microsoft/autogen) from Microsoft implements group chat patterns where agents can challenge each other's outputs before converging on a solution.
-
Process Verification: Step-by-step checking of the reasoning process, not just the final output. LangGraph (github.com/langchain-ai/langgraph) enables this through conditional nodes that verify intermediate state at each step of the graph.
The most robust production systems combine multiple architectures: deterministic checks (tests, linters) for mechanical correctness, plus an LLM judge for semantic quality, plus human review for judgment-sensitive decisions.
Best Practices
-
Always separate the verifier from the generator: The same model grading its own work is the single most common failure mode. Use separate agents, separate models, or -- ideally -- deterministic tests.
-
Prefer deterministic checkers: Tests, type-checkers, and linters are more reliable than LLM judges. SWE-Agent's research consistently shows that executable verification outperforms self-evaluation.
-
Set a maximum retry limit: Without it, agents burn tokens on impossible problems. Aider's default behavior, Claude Code's configurable action limits, and OpenHands' action constraints all enforce this.
-
Persist error history: Even failed attempts contain valuable diagnostic information. Aider's Git-first approach provides this automatically -- every retry is a commit.
-
Use exponential backoff with jitter: Prevents thundering herd and gives failing services time to recover.
-
Always retain the best result: Even if verification never passes completely, the best attempt may be usable with minor manual fixes.
-
Guard against anti-patterns: Explicit instructions against deleting tests, silently ignoring errors, or redefining completion criteria. Claude Code's test ratchet pattern in
CLAUDE.mdis the canonical example. -
Use a layered model strategy: Fast models for verification triage, strong models for generation. This reduces cost while maintaining correction quality, .
Next Steps
- For the broader verification framework, see Closed Loop Iteration
- For how state persistence supports error recovery, see State Persistence
- For the execution pattern these corrections live inside, see Agent Loop