intermediatecoreauto-correctionverifiergradererror-handling

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 TypeReliabilityUse CaseBias Risk
Deterministic (tests, type-check, lint)HighestCode correctness, build integrityNone
Deterministic execution (run the code)HighRuntime behavior, API responsesNone
Separate LLM judgeMediumCode quality, documentation styleModerate
Same-model self-evaluationLowestQuick triageHigh (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:

  1. The agent writes code
  2. Tests run automatically (via hooks or explicit agent action)
  3. If tests fail, the agent reads the failure output
  4. The agent diagnoses the issue and writes a fix
  5. Tests run again
  6. 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

  1. Exception handling failures -- uncaught errors crash the loop without recovery
  2. Blind retries -- retrying without diagnosing the root cause, burning tokens
  3. Context overflow -- accumulating error history until the context window fills
  4. 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 TypeExampleRecovery Strategy
Tool failureNetwork timeout, rate limitRetry with backoff
Test failureLogic error caught by testsAgent reads error, fixes code, re-runs
Type errorTypeScript compilation failureAgent fixes type annotations
Lint errorCode style violationAgent runs lint --fix or adjusts code
Partial successSome tests pass, others failAgent fixes failing subset

Fatal Errors

These are failures where retrying will not help:

Error TypeExampleRecovery Strategy
Authentication failureInvalid API credentialsAlert human, halt loop
Resource not foundReferenced file does not existReport error, skip task
Permission deniedAgent lacks required accessAlert human
Budget exceededToken limit reachedSave state, report partial results
Contradictory requirementsTask spec is impossibleAlert 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:

  1. Explores the repository structure
  2. Identifies relevant files
  3. Generates a patch
  4. Runs the project's test suite to verify
  5. If tests fail, reads the output and generates a corrected patch
  6. 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:

  1. 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.

  2. 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.

  3. 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.

  4. 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

  1. 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.

  2. 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.

  3. 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.

  4. Persist error history: Even failed attempts contain valuable diagnostic information. Aider's Git-first approach provides this automatically -- every retry is a commit.

  5. Use exponential backoff with jitter: Prevents thundering herd and gives failing services time to recover.

  6. Always retain the best result: Even if verification never passes completely, the best attempt may be usable with minor manual fixes.

  7. Guard against anti-patterns: Explicit instructions against deleting tests, silently ignoring errors, or redefining completion criteria. Claude Code's test ratchet pattern in CLAUDE.md is the canonical example.

  8. Use a layered model strategy: Fast models for verification triage, strong models for generation. This reduces cost while maintaining correction quality, .

Next Steps