Ask anyone building with AI agents where this is heading and you will get the same answer: the future is loops. Not a model you prompt once and read, but an agent that runs in a cycle. It does a little, looks at what came back, decides what to do next, and goes again, until the work is actually done. They are right. The loop is what turns a language model from something that answers questions into something that gets things done, and it is the direction much of today's agent work is heading.

I think they are right about that. I also think almost everyone is wrong about what comes next.

Here is what most teams are actually building. A team ships an agent. It handles the easy cases. On the hard ones it stalls, wanders off, or hands back something that looks right but isn't. So they give the loop more room: more steps, a longer leash, permission to keep going on its own. The cost goes up. The reliability does not. Around week three someone says the quiet part out loud. Maybe the model just isn't smart enough yet.

That guess is almost always wrong, and why it is wrong is the most useful thing I can tell you about loops. First, two quick definitions so nothing trips you up later. A language model is a program that predicts text. Give it tools, which are functions your code lets it call, run it in that act, observe, reason, repeat cycle (the pattern ReAct introduced, first posted in 2022 and published at ICLR 2023), and you have an agent loop. The popular story about why loops are the future is really a story about freedom: longer runs, more steps, fewer humans, until the loop can finish whole jobs alone. On the tasks that matter most, that story has it backwards, and the research has been quietly saying so for two years. What decides whether a loop is worth running is not how freely it runs. It is what happens on each pass, in the small gap between acting and the next step. That gap is what this article is about.

The case in three moves

One. A loop is just a way to spend more effort on a problem. More effort only helps if something can tell good work from bad. The model running the loop usually can't, at least not about the things you most want it to get right.

Two. So what decides whether a loop pays off is not how freely it runs. It is how good the check is on each pass. Call it the verifier ceiling: in practice, a loop is only about as reliable as the checks that guide its steps.

Three. The research strongly points toward point two, and then quietly assumes the check is free and trustworthy. It is neither. The real work, and the rest of this article, starts once you admit the check is often unreliable, expensive, and missing altogether.

Why More Autonomy Makes the Hard Cases Worse

Start with what that instinct gets backwards. Letting a shaky agent run longer does not make it reliable. It makes it fail in a more expensive way, and the reason is simple once you look at what a loop really is.

A loop is a chain. Each step builds on the last one's output. So if one step is right with some probability, the chance a long run of them is all right is that probability multiplied by itself, once per step. Say each step is a very respectable ninety-five percent reliable. Twenty of those in a row, with nothing catching the misses, lands at about thirty-six percent. Forty steps, around thirteen. The agent did not get worse at any single step. The math of chaining caught up with it, and more steps just feed that math.

People quote this number when they want to sound careful about agents, and the curve is real, so take it seriously.

Hold onto that curve, because we are going to knock it down later. For now it makes one honest point: long agents fail because of how the chain works, not because the model is dumb, and running longer only speeds the failure up. There is a deeper reason underneath the math too. In a loop, the model never sees the whole situation at once, only the part right in front of it, and it has to remember the rest. The trouble is that a mistake made early often does not show up until much later, after the agent has already stacked several more steps on top of it, when it is hard to trace and costly to fix. Length is what turns a small early mistake into one you can't undo. So the first hole in the autonomy story is simple: the thing everyone turns up is the thing that makes the failure bigger.

If length is the problem, the obvious question is what would catch the misses before they pile up. That is where the most popular fix comes in, and where the research has its most useful, most ignored finding.

The Result That Should Have Ended the Argument

When an agent gets something wrong, the natural fix is to add a step where the model looks back at its own work and corrects it. A reflection step. It is the first thing almost everyone tries, and on the surface the research backs it up. In 2023, Madaan and colleagues published Self-Refine: a model writes an answer, critiques its own answer, and rewrites it, over and over, with no extra training. Across seven tasks, people preferred the polished versions. Self-correction works, the headline says.

Except a study by Huang and colleagues, first posted in 2023, ran the same idea on reasoning problems and found the opposite, plainly enough to put it in the title: large language models cannot self-correct their reasoning yet. When the model revised its own answers to math and logic problems using only its own judgment, it did not improve, and it often got worse. It would talk itself out of a correct answer, because it had no reliable way to tell which of its answers was right in the first place.

Two careful papers, opposite results. That contradiction is the whole point, and clearing it up gives you the one idea this article rests on. Self-Refine showed that a model critiquing and rewriting its own work can improve results across a range of tasks, especially ones where it can genuinely tell that something reads badly or is messy. Huang's study zeroed in on reasoning, and specifically on the case where the model fixes itself with no outside signal, and there it found self-correction does not reliably help and can even make things worse. So the line that matters is not whether you loop. It is whether the loop's feedback is grounded, meaning it comes from outside the model's own opinion, or introspective, meaning the model is just judging itself. Self-feedback tends to help when the model can actually judge the thing, and fail when being right needs a fact or a calculation it can't confirm just by looking again.

Grounded feedback is a test that actually runs and passes or fails. It is a tool that returns a real value. It is a fact you can find in a trusted source. It is a separate checker that does not share the maker's blind spots. Introspective feedback is the model nodding along to its own work. The first kind turns the loop's extra effort into real reliability. The second kind, whenever the model can't actually tell right from wrong, just turns that effort into more confident wrong answers, which is exactly the pile-up the math warned about.

So what actually matters is not how freely the loop runs. It is how good, and how grounded, the check is on each pass. Let me give that idea a name, and then push back on it, because the name is where the research stops and the real problems start.

The Verifier Ceiling, and the Three Assumptions Inside It

Call it the verifier ceiling: in practice, a loop's reliability is bottlenecked by the quality, independence, and cost of the checks on its steps. It is a rule of thumb, not a theorem, but it holds up remarkably often. The generator, the model doing the work, sets the best the loop could ever do. The verifier, the thing checking the work, sets what it actually does, because it decides which attempts move forward and which get caught. Make the generator better and you raise the best case nobody quite reaches. Make the verifier better and you raise the everyday result you actually ship. In production, the everyday result is the only number that counts.

That way of looking at it is useful, and it is about where the research leaves you. A pile of recent work all points the same way: looping with a real check in it is how you turn extra computing into correct answers. Tree of Thoughts made it vivid. On the Game of 24 puzzle, GPT-4 with plain step-by-step reasoning solved four percent of cases, while Tree of Thoughts reached seventy-four percent by trying several reasoning paths and keeping the ones a scoring step rated best, the same model, just a smarter search. Lightman and colleagues, training reward models on human step-by-step labels, found that grading every step beat grading only the final answer. That was a training-time result, but the design lesson carries over: checking the intermediate steps tells you where things broke, not just that they did. Snell and colleagues found that smart test-time strategies, search and revision steered by a checker, can use compute more efficiently than brute force, and in some matched-compute settings let a smaller model beat a much larger one. And Cemri and colleagues, in a large study of why multi-agent systems fall apart, name weak task verification as one of the main failure categories: cases where a step is ended too early, never checked, or checked wrong before the next step trusts it.

All true. And all of it quietly assumes something that falls apart the moment you try to ship: that the check is reliable, cheap, and available. Usually it is none of the three. This is where you have to part ways with the research a little.

The first assumption is that the check is trustworthy. In the clean studies, the check is often a known right answer or a curated test set. In your system, the check is often another language model, or a test suite the same model wrote, which means it goes blind in the same places as the thing it is checking. A check that fails wherever the maker fails is not a ceiling. It is a mirror. Worse, the moment you push the generator to pass a fixed check, it learns to pass the check without doing the real work: the agent that edits the test file so the tests go green, the answer stuffed with the rubric's keywords. Researchers call this reward hacking when it happens during training, and treat it as a hard, unsolved problem. It does not go away just because you are running the model instead of training it.

The second assumption is that the check is cheap. Checking every step is the strongest result in the research and the hardest to actually get. Lightman's team needed eight hundred thousand human step-by-step judgments to build their checker. Most teams have a deadline and a backlog. The honest truth is that good checking is often more work to build than the agent it guards, which is exactly why teams skip it and reach for the cheap option, more autonomy, instead. The verifier ceiling is not a comforting idea. It is a bill.

The third assumption is the shakiest: that a check exists at all. Code and math are the field's favorite examples for a reason. They come with a cheap, automatic right answer. A test runs or it doesn't. But most real work is not like that. Was this the right strategy memo? Is this support reply actually helpful? Is this summary fair to the source? There is no test to run. The published wins cluster in the checkable areas and then get quoted as if they apply everywhere, and the quiet truth is that most valuable agent work sits in the messy middle, where the one thing that makes loops work is the thing you do not have.

Notice what just happened. The research told us the check is the thing that limits everything, and then handed us a check that is often unreliable, costly, and missing. That is not a dead end. It is the actual job. The whole craft of building good agents is in how you answer those three problems, and the answer is not a smarter model. It is a different way of thinking.

Reliability Is Manufactured, Not Summoned

The autonomy dream is that you summon a smart enough agent, give it room, and reliability just appears. The research says reliability does not appear. You produce it, on purpose, by checking. So borrow the mindset from an industry that has been making reliable things out of unreliable parts for a century: manufacturing. No factory makes a perfect part every time. It builds inspection into the line, so defects get caught and fixed before they reach the next station. An agent loop is a production line. The verifier is the inspection. Once you see it that way, the three assumptions stop being objections and turn into a to-do list. Four moves fall out.

The first move answers the trust problem: keep the checker separate from the maker. A factory does not let the machine that stamps a part be the only thing that inspects it. Your loop should not let the model that wrote the answer be the only thing that judges it. Where you have to use a model to check, use a different one, or at least a different method that fails in different places, so the maker and the checker are not blind to the same things. Independence is not a nice-to-have. It is the difference between a check and a mirror.

The second move answers the cost problem: match the check's cost to the moment. You can't afford the most expensive check on every step, and you don't need it. Run quick, cheap checks all the time, a fast parse, a format check, a simple bounds check, and save the expensive ones, running the full test suite, calling a separate model, pulling sources, for the moments that count: checkpoints, high-stakes steps, anything the cheap checks flagged. That is how a real line works, a light check everywhere and a full teardown at the gates, and it is how you make step-by-step checking cheap enough to actually use.

The third move is the one the research points at but never names, and it answers the missing-check problem sideways: spend effort where the model is unsure, not evenly. Loops fail because the model can't tell its right answers from its wrong ones. Flip that around and it becomes useful. When a model can give a useful signal of how unsure it is, even an imperfect one, you can use that signal to decide where to put your effort. A confident step that passes the cheap checks gets one pass. An unsure or weakly checked step gets the full treatment: try several answers, make a separate checker agree, search instead of guess. This is calibration, a model having a good sense of when it is probably wrong, and it is the most underused idea in the whole area. It turns that scary compounding curve from a threat into a budget. You stop spreading effort evenly and start spending it where the risk actually is.

The fourth move answers the missing-check problem head on: build a check where there wasn't one. When a task has no natural test, the best engineering is often to reshape the task until it does. You can't test "is this memo good," but you can test "does it cite a real source for every claim," "does it stay under the length limit," "does a separate model with a fixed rubric pass it on these five named points." None of those is the whole thing you care about, but each one is a real, grounded check, and a handful of them together catch most of what a single perfect check would have. It is the same trick labs use when they design a task to give a checkable reward, just moved into your own loop. "Unverifiable" is rarely a fact about the task. It usually just means nobody has done the work of making it checkable yet.

Now let us knock down the scary curve from the start, because this mindset changes what it means. That clean $p^n$ collapse assumes every step is a separate coin flip with nothing in between. A well-built loop breaks every part of that. Independent checks catch a share of the errors, so they stop piling up. Tiered checks add gates the chain would otherwise blow straight through. Calibration aims your effort at the steps most likely to be wrong. The real reliability of a good loop is not $p^n$. It is much closer to flat, not because the model got smarter, but because the line was built to catch what the model gets wrong. The compounding curve is not the fate of agents. It is the fate of agents nobody bothered to check.

The Whole Thing, as Code You Can Run

Enough talk. Here is the complete program, not a sketch. It is a loop that writes a Python function and checks it by running real tests, so the check is grounded in execution, not in the model's opinion of itself. With pip install anthropic pytest and your ANTHROPIC_API_KEY set, it runs end to end. I have kept it to one file you can read top to bottom; in a real project you would split it up, but nothing here is left as a stub.

There are four parts: the model and the small bits of data it passes around, the generator that does the work, the checks that judge it, and the loop that ties them together. Start with the foundation.

import os
import re
import subprocess
import sys
import tempfile
import textwrap
from dataclasses import dataclass, field
from pathlib import Path

import anthropic


class Model:
    """Thin Anthropic adapter. Swap the body for your provider; nothing else changes."""

    def __init__(self, api_key: str, model: str = "claude-sonnet-4-6"):
        self._client = anthropic.Anthropic(api_key=api_key)
        self._model = model

    def complete(self, system: str, user: str, max_tokens: int = 1500) -> str:
        resp = self._client.messages.create(
            model=self._model, max_tokens=max_tokens, system=system,
            messages=[{"role": "user", "content": user}])
        return "".join(b.text for b in resp.content if b.type == "text")


@dataclass
class Check:
    ok: bool
    confidence: float          # how sure THIS check is of its verdict, 0..1
    detail: str                # plain feedback, fed back to the generator
    where: str | None = None   # which test or line failed, when known


@dataclass
class State:
    goal: str
    notes: list[str] = field(default_factory=list)   # what went wrong before
    last_confidence: float | None = None
    step: int = 0

    def remember(self, text: str) -> None:
        self.notes.append(text.strip())
        self.notes = self.notes[-8:]


def extract_code(text: str) -> str:
    """Pull the code out of a fenced block and drop the trailing confidence line."""
    match = re.search(r"```(?:\w+)?\n(.*?)```", text, re.DOTALL)
    code = match.group(1) if match else text
    return re.sub(r"(?m)^\s*CONFIDENCE:.*$", "", code).strip()

The generator does the work, and it reports how sure it is, because that confidence is the signal the loop uses to decide when to try harder. It can also produce several different attempts when the loop asks it to search.

class Generator:
    """The model doing the work. Reports a confidence, and can produce several
    different attempts on request."""

    def __init__(self, model: Model, kind: str = "Python function"):
        self._model = model
        self.kind = kind

    def _context(self, state: State) -> str:
        notes = "\n".join(f"- {n}" for n in state.notes[-5:]) or "(nothing yet)"
        return f"GOAL:\n{state.goal}\n\nFIX THESE PROBLEMS FROM EARLIER ATTEMPTS:\n{notes}"

    def _ask(self, state: State, nudge: str = "") -> tuple[str, float]:
        out = self._model.complete(
            system=(f"Write a {self.kind} that satisfies the goal. Put the code in a "
                    "single ```python``` block and nothing else, then on a final line "
                    "write 'CONFIDENCE: x' where x is 0..1, how sure you are it is "
                    "correct and complete." + nudge),
            user=self._context(state))
        m = re.search(r"CONFIDENCE:\s*([01](?:\.\d+)?)", out)
        confidence = float(m.group(1)) if m else 0.5
        return extract_code(out), confidence

    def propose_with_confidence(self, state: State) -> tuple[str, float]:
        return self._ask(state)

    def sample(self, state: State, n: int = 2) -> list[str]:
        return [self._ask(state, " Take a different approach from the obvious one.")[0]
                for _ in range(n)]

Now the checks, the part that actually matters. A cheap one that runs instantly (does it even parse) and an expensive one that runs the code against tests in a temporary subprocess with a timeout. The model that wrote the code is on neither list, which is what keeps the check independent of the maker. One thing to be clear about: this is execution-grounded verification, not a security sandbox. The timeout and temp directory give you isolation against a runaway loop, not against hostile code, so do not run untrusted generated code this way in production without a real sandbox.

def syntax_check(code: str, state: State) -> Check:
    """Cheap, instant, grounded: does it even parse?"""
    try:
        compile(code, "<candidate>", "exec")
        return Check(True, 1.0, "parses")
    except SyntaxError as exc:
        return Check(False, 1.0, f"SyntaxError: {exc.msg} (line {exc.lineno})", f"line {exc.lineno}")


class TestVerifier:
    """The expensive, grounded check: actually run the code against tests in a
    subprocess. The truth comes from execution, not from the model's opinion."""

    def __init__(self, tests: str, timeout_s: float = 15.0):
        self._tests = tests
        self._timeout = timeout_s

    def __call__(self, code: str, state: State) -> Check:
        with tempfile.TemporaryDirectory() as d:
            path = Path(d)
            (path / "solution.py").write_text(code)
            (path / "test_solution.py").write_text(
                "from solution import *\n" + textwrap.dedent(self._tests))
            try:
                proc = subprocess.run(
                    [sys.executable, "-m", "pytest", "-x", "-q", str(path)],
                    cwd=d, capture_output=True, text=True, timeout=self._timeout)
            except subprocess.TimeoutExpired:
                return Check(False, 1.0, "timed out (possible infinite loop)", "runtime")
        passed = proc.returncode == 0
        tail = (proc.stdout + proc.stderr).strip()[-1000:]
        where = None
        if not passed:
            hit = re.search(r"(test_\w+)", tail)
            where = hit.group(1) if hit else None
        return Check(passed, 1.0, "all tests passed" if passed else f"tests failed:\n{tail}", where)


class TieredVerifier:
    """Cheap checks first, fail fast. The expensive check runs when stakes are high
    or the model was unsure. The model that wrote the code is on neither list."""

    def __init__(self, cheap: list, expensive: list):
        self._cheap = cheap
        self._expensive = expensive

    def verify(self, code: str, state: State, *, high_stakes: bool) -> Check:
        for check in self._cheap:
            result = check(code, state)
            if not result.ok:
                return result            # a cheap failure stops everything, cheaply
        unsure = state.last_confidence is not None and state.last_confidence < 0.7
        if high_stakes or unsure:
            for check in self._expensive:
                result = check(code, state)
                if not result.ok:
                    return result
        return Check(True, 0.9, "passed the checks that were run")

And the loop. Confident work gets one attempt; unsure work gets searched across several attempts. Either way, the result has to clear the grounded check before it counts as done, so nothing unverified is ever accepted.

class Loop:
    """Generate, check, keep or fix, repeat. Stops the moment the grounded check
    passes, with failsafes if it stalls or runs too long."""

    def __init__(self, generator: Generator, verifier: TieredVerifier,
                 max_steps: int = 6, patience: int = 3):
        self._gen = generator
        self._verify = verifier
        self._max_steps = max_steps
        self._patience = patience

    def run(self, goal: str, *, high_stakes: bool = False) -> dict:
        state = State(goal=goal)
        last_problem, stalls, candidate = None, 0, ""

        for step in range(1, self._max_steps + 1):
            state.step = step
            code, confidence = self._gen.propose_with_confidence(state)
            state.last_confidence = confidence

            if confidence >= 0.8 and not high_stakes:
                # Confident: one attempt, still run the grounded check before accepting.
                candidate = code
                verdict = self._verify.verify(code, state, high_stakes=True)
            else:
                # Unsure or high-stakes: search several attempts, each grounded, keep a winner.
                attempts = [code] + self._gen.sample(state, n=2)
                scored = [(c, self._verify.verify(c, state, high_stakes=True)) for c in attempts]
                passing = [(c, v) for c, v in scored if v.ok]
                candidate, verdict = passing[0] if passing else scored[0]

            print(f"[step {step}] confidence={confidence:.2f} ok={verdict.ok} where={verdict.where}")
            if verdict.ok:
                return {"success": True, "steps": step, "code": candidate}

            # No-progress failsafe: the same failure twice running means we are stuck.
            problem = (verdict.where, verdict.detail[:80])
            stalls = stalls + 1 if problem == last_problem else 0
            last_problem = problem
            if stalls >= self._patience:
                return {"success": False, "steps": step, "code": candidate, "reason": "no progress"}

            state.remember(f"attempt {step} failed at {verdict.where}: {verdict.detail[:200]}")

        return {"success": False, "steps": self._max_steps, "code": candidate, "reason": "step limit"}

Finally, point it at a real, checkable task and run it.

def main() -> None:
    model = Model(api_key=os.environ["ANTHROPIC_API_KEY"])

    goal = ("Write a Python function `is_prime(n: int) -> bool` that returns True if n "
            "is prime and False otherwise. Treat any n < 2 as not prime.")
    tests = """
        def test_small_primes():
            assert is_prime(2) and is_prime(3) and is_prime(13)
        def test_non_primes():
            assert not is_prime(0) and not is_prime(1) and not is_prime(9)
        def test_negatives():
            assert not is_prime(-7)
        def test_large_prime():
            assert is_prime(7919)
    """

    verifier = TieredVerifier(cheap=[syntax_check], expensive=[TestVerifier(tests)])
    result = Loop(Generator(model), verifier, max_steps=6).run(goal)

    head = (f"SUCCESS in {result['steps']} step(s):" if result["success"]
            else f"GAVE UP after {result['steps']} step(s) ({result['reason']}):")
    print("\n" + head + "\n\n" + result["code"])


if __name__ == "__main__":
    main()

Run it and watch what happens. The model writes a first is_prime. Maybe it forgets that negative numbers are not prime, or it loops all the way up to n and times out on 7919. The check does not ask the model whether the code looks right. It runs the code, the tests fail, and the loop gets back the exact failing test by name. That specific, grounded feedback goes into the next attempt, the model fixes that one thing, and the loop stops the instant the tests pass, on evidence, not after a fixed number of tries. Every attempt is checked before anything builds on it, so a wrong first try never becomes the floor for a wrong second one.

Compare that to the version this article opened with: write the code, ask the same model "does this look right," trust the answer. Both are loops. The difference is that one loop's check can run the code and the other's can only have an opinion. One honest caveat: the design keeps the maker off the verifier list, which encourages independence, but it does not enforce it. Nothing stops you from putting a non-independent model checker on that list, and if you do, you are back to a mirror.

Where This Still Falls Short

I would be doing exactly what I accused the research of if I pretended this closes the problem. It doesn't, and the places it falls short are worth naming, because that is where the real open questions are.

Independence is a matter of degree, not a switch. Two different models trained on overlapping data share more blind spots than you would like, so "use a different checker" gives you less independence than it sounds like, and nobody really knows how much. Calibration, the thing the third move leans on, is still young. Models are getting better at flagging when they are unsure, but you can't fully trust them to know their own doubt yet, and a loop that spends effort based on a bad confidence signal will spend it in the wrong places. Those stacked-up checks catch the stand-ins, not the real goal, and a determined model can pass every one of them and still miss the point, which is the same reward-hacking problem in nicer clothes. And none of this rescues the real judgment call, the kind where smart people disagree and no check stands in for taste. For those, the right design still ends with a human, and pretending otherwise is how you ship confident nonsense at scale.

But notice where all of those open questions sit. Not in the model's raw intelligence. In the checker, in whether the model knows its own uncertainty, in our ability to turn fuzzy goals into concrete checks. The interesting, unsolved problems in agent reliability are checking problems. Models keep getting better at longer tasks (Kwa and colleagues measure that ability roughly doubling every seven months, with caveats about how far the trend generalizes), which means the loops we can build keep reaching further, which puts even more weight on the checker. The future that matters is not a more autonomous agent. It is a better-checked one, and building that checker is in your hands, not the model provider's.

How to Build a Loop That Holds

So here is the part you can act on. Next time an agent keeps failing on the hard cases and the first idea in the room is to give it more steps and more freedom, you will know that is the wrong move. Running a loop longer does not fix it. It just gives the mistakes more room to pile up. And a model checking its own work, with nothing real to test it against, is just the model agreeing with itself. What actually decides whether a loop works is the check you run on each step. That check is hard to build, it costs real money, and for a lot of tasks it does not exist yet. That is not a reason to give up. That is the work.

Think of the loop as a small factory line. Do not let the worker who did the job be the only one who inspects it. Run quick, cheap checks all the time, and save the slow, expensive ones for the steps that matter. Put your effort where the model is unsure, not evenly across every step. And when a task has no obvious test, build one out of smaller checks until it does. None of this needs a smarter model. It just means taking the check seriously, because the check is the part you control, and hoping the agent will catch its own mistakes is not a plan.

The agents that hold up in real use will not be the ones given the most freedom. They will be the ones built, like everything else we rely on, to catch their own mistakes before anyone else has to. The same idea runs through two companion pieces: keeping each step reliable across a long loop is the job of context engineering, and refusing to let unchecked or untrusted input move forward is the same idea applied to safety in defending agents against prompt injection. Correctness, context, and safety all come down to one habit: check before you move on.