DEV Community

Walker Miller
Walker Miller

Posted on Edited on Originally published at loopandretry.github.io

Loop drift: how agents convince themselves they're making progress

Originally published on Loop & Retry — field notes on building LLM agents that survive production.

The agent failures that page you are the easy ones. Something throws, a run dies, an alert fires, you look. The failures that quietly cost you are the ones where the agent keeps working — taking actions, narrating confident progress, burning tokens — without getting any closer to done. I call it loop drift, and it's the failure mode I trust least to announce itself, because from the inside it looks exactly like work.

This is a teardown of one instance: what it looked like, why our checks missed it, and the detection and evals that would have caught it in minutes instead of on the invoice.

The incident

A code-maintenance agent, task: "find and fix the failing test in the billing module." Straightforward — the kind it closed dozens of times a day. This run took 41 steps, spent about 15× a normal run's tokens, and ended by hitting the step cap. No exception. No error in the logs worth the name. The final message was, in part: "I've made significant progress narrowing down the issue and am close to identifying the root cause."

It was not close. Reconstructing the trajectory, here's what actually happened:

  • Steps 1–6: reasonable. Read the test, ran it, found a failing assertion, opened the module under test.
  • Steps 7–15: it read the same three files in slightly different orders. Each step's reasoning was fluent and plausible — "let me check how apply_discount handles the null case" — and each ended with a tool call that gathered information it already had.
  • Steps 16–30: it started running grep variations. grep discount, then grep -i discount, then grep "discount" with quotes. Each returned nearly the same lines. Each time the model treated the result as a fresh clue and declared a step of progress.
  • Steps 31–41: it proposed the same one-line fix three times, each time "verifying" by re-reading the file rather than re-running the test, each time reporting the verification as forward motion, until the step cap ended it.

The fix, when a human took over, was four lines and ten minutes. The agent had all the information it needed by step 6. It spent the next 35 steps convincing itself it was progressing.

Why our checks didn't catch it

We had guardrails. They were the wrong ones.

We had a step cap. It fired — that's how the run ended. But a step cap is a backstop, not a detector: it bounds the damage of drift, it doesn't notice drift. By the time it trips you've already paid for every wasted step. It's the retry cap problem from the retry-budgets post in a different costume — a cap turns unbounded waste into bounded waste, and turns a silent cost problem into a silent correctness problem, because a run that dies at the cap looks a lot like a run that was genuinely hard.

We trusted the model's self-assessment. This is the deep one. Our loop asked the model, each step, whether it was making progress and whether it was done — and the model said yes, it was progressing, right up to the cap. That felt reasonable when we built it. It isn't, and the reason is structural: the model's sense of progress is generated from the same context that's drifting. Every "I'm narrowing it down" was a fluent continuation of a transcript full of fluent continuations. A drifting agent's self-report drifts with it. Asking a stuck agent whether it's stuck is asking the unreliable narrator to review their own reliability.

The context made it worse over time. Each near-duplicate action left its result in the window. By step 30 the context was thick with slightly-varied greps and repeated file reads, and that repetition is itself a prior: the most likely continuation of a transcript full of grep variations is another grep variation. The window was pushing the model toward the drift. (Context that actively degrades behavior is its own topic — a future post — but it's a co-conspirator here, not the root cause.)

The root cause was simpler than any of these: we had no external, model-independent signal for progress. Everything we measured, the model could fake without knowing it was faking.


Continue reading: the rest of this post — including the sections that follow — is on Loop & Retry: read the full article

Top comments (0)