Your Agent Called the Same Tool Three Times in a Row. Here's What the Loop Is Telling You.

A repeating agent isn't being stubborn — it's being consistent. Identical tool calls mean the context barely changed between turns, and there are only four reasons that happens.

Your Agent Called the Same Tool Three Times in a Row. Here's What the Loop Is Telling You.

You ask a scheduling assistant to move tomorrow's afternoon meetings so you have a clear block after lunch. It calls search_calendar with the same arguments three times in a row, then gives up and apologises. Nothing crashed. No error appeared. The agent simply did the identical thing again and again.

This is one of the most common failures a new agent builder hits, and it is also the fastest way to actually understand how the agent loop works. Identical repetition is not randomness or stubbornness. It is a signal, and it points at exactly one thing.

🔍 The loop only has one moving part

Strip an agent down and you get four steps that cycle:

  1. The model reads everything in its context.
  2. It decides to call a tool.
  3. The tool runs and produces a result.
  4. That result gets appended to the context, and the cycle repeats.

Step 4 is the only thing that changes between one turn and the next. The system prompt is fixed. The user's request is fixed. The model's weights are fixed. The observation — the text that comes back from the tool — is the entire mechanism by which the next turn differs from this one.

So when an agent produces byte-identical tool calls on consecutive turns, the honest reading is: its input barely changed. The model is not being obstinate. It is being consistent. Given nearly the same context, it produced nearly the same output, which is what a language model is supposed to do.

That reframes the debugging question. Stop asking "why won't it move on?" and start asking "what did turn two see that turn one didn't?"

The four things that actually cause it

Once you look at repetition as an input problem, the causes sort into a short list.

1. The result never reached the context. The tool ran, returned something, and your framework dropped it — a serialisation failure, an exception swallowed in the tool wrapper, a result keyed to a call id that didn't match. The agent is on turn three with a context that still looks like turn one. This is the most common cause and the easiest to miss, because logs on the tool side show a successful call.

2. The result reached the context but said nothing. search_calendar returned []. Or {"status": "ok", "results": []}. An empty result is technically an observation, but it carries no information about why it was empty — wrong date format, wrong calendar, no permission, or genuinely no meetings. Faced with a blank, the model's most plausible next action is the one it just tried.

3. The agent can't see that it already tried. If your loop trims history aggressively, or rebuilds the prompt each turn from a summary, the previous attempt may have been edited out. The agent isn't repeating itself; from its point of view it's trying for the first time, every time.

4. Nothing defines "done". Sometimes the tool works, the result is fine, and the agent re-checks because it has no stopping condition. It was told to clear the afternoon, and nothing in the loop tells it what a cleared afternoon looks like, so it keeps looking.

Diagnosing it in under five minutes

Print the raw message list the model receives on each turn. Not your framework's pretty log — the actual array of messages going over the wire.

Then compare turn N and turn N+1:

  • The arrays are identical → cause 1 or 3. The observation isn't landing. Check your tool-result plumbing before you touch the prompt.
  • A new tool result is present but empty or opaque → cause 2. Fix the tool, not the model.
  • A new result is present and informative, and the agent re-calls anyway → cause 4. Fix the instructions and the stopping condition.

💡 Most people reach for a prompt fix first, because prompts are easy to edit. Three of the four causes here are not prompt problems. Editing the system prompt when the tool result is being dropped just adds tokens to a loop that was never going to progress.

Fixing cause 2, since it's the one you control best

Empty results are where a small change in the tool pays off the most. Compare:

# Before
search_calendar(date="tomorrow", after="13:00")
-> []

# After
search_calendar(date="tomorrow", after="13:00")
-> {
     "results": [],
     "searched": "2026-09-12, 13:00-23:59, calendar 'work'",
     "note": "0 events. Calendar 'personal' was not searched."
   }

The second version gives the next turn something to work with. The agent can now try the other calendar, or conclude the afternoon is already clear and stop. The first version gives it a blank wall to walk into again.

The general rule: a tool result should say what was actually done, not just what was found. Echo the interpreted parameters back. Name the scope you searched. Distinguish "no matches" from "couldn't run". Every one of those details is a handle the next turn can grab.

The backstop you still need

Even with good observations, put a hard guard in the loop: if the same tool is called with the same arguments twice, don't run it a third time. Return a synthetic observation instead — something like "This exact call was already made and returned 0 results. Try a different approach or report that you are stuck."

That single guard converts an expensive silent spin into either a new strategy or an honest failure. Both are better outcomes than a loop burning tokens until it hits the step limit.

The takeaway

An agent that repeats itself is telling you its context stopped changing. The model is downstream of that; it is not the bug. Before you rewrite a prompt, prove that turn three saw something turn two didn't — and if it didn't, you've found your problem without having touched the model at all.

Keep reading

Similar posts

Matched on shared tags and category — the more bars, the stronger the overlap with what you just read.