Skip to main content

Tool Loop Detection

The problem

Agents occasionally get stuck. The LLM calls a tool, gets a result, doesn’t like it, and calls the same tool with the same arguments again hoping for a different answer. Repeat 20 times. You burn:
  • ~80 tokens of tool definition × 20 = 1,600 tokens
  • 20 × tool result tokens
  • 20 × LLM completion tokens
  • Real wall-clock time
By the time you notice, the bill is real. loopDetection on ToolExecutor catches this within a single run.

Configuration

Action semantics

action: "abort"

After call number maxRepeats + 1 of the same (toolName, JSON.stringify(arguments)), the executor throws ToolLoopError:
ToolLoopError properties:

action: "hint"

Instead of throwing, the executor returns a synthetic tool result back to the LLM:
The LLM sees this in its next turn and almost always self-corrects. The ToolCallResult returned has error: "loop-detected" so you can tell it apart from a real result.

How the counter works

The executor maintains a Map<string, number> per ToolExecutor instance. The key is:
This means:
  • Identical args bump the counter. fetchData({q: "hi"}) called twice → count 2.
  • Different args reset. fetchData({q: "hi"}) then fetchData({q: "bye"}) → two independent counters of 1 each.
  • Order matters in JSON.stringify. {a:1, b:2} and {b:2, a:1} count separately. (Usually not a problem because LLMs are remarkably consistent about key order.)
  • The counter survives across executeAll calls within the same ToolExecutor instance. It’s not per-call.

Interaction with defineAsyncTool / pollResult

Polling is genuinely repetitive — pollResult({handle: "ah:abc"}) called 5 times in a row is correct behavior, not a loop. If you use both, raise the threshold OR skip loop detection on the executor that holds the poll tool. The simplest pattern:
Better: define your own per-tool exemption logic by wrapping ToolExecutor. (Not built in yet; would be a future addition.)

Telemetry

Loop detection emits a tool.result event on the RunContext.eventBus with the synthetic hint message when action: "hint" fires. Wire your observability layer to count these — a spike in loop-detected results is a strong signal that a particular prompt template is broken.

See also