Documentation Index
Fetch the complete documentation index at: https://docs.agentium.in/llms.txt
Use this file to discover all available pages before exploring further.
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
loopDetection on ToolExecutor catches this within a single run.
Configuration
| Field | Type | Meaning |
|---|---|---|
maxRepeats | number | Threshold above which the action fires. Counter starts at 1 on first call. |
action | "abort" | "hint" | What to do when threshold is hit |
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:
ToolCallResult returned has error: "loop-detected" so you can tell it apart from a real result.
How the counter works
The executor maintains aMap<string, number> per ToolExecutor instance. The key is:
- Identical args bump the counter.
fetchData({q: "hi"})called twice → count 2. - Different args reset.
fetchData({q: "hi"})thenfetchData({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
executeAllcalls within the sameToolExecutorinstance. It’s not per-call.
Recommended settings
| Use case | Setting |
|---|---|
| Most agents | { maxRepeats: 3, action: "hint" } |
| Production pipelines | { maxRepeats: 2, action: "abort" } |
| Exploratory / status-polling tools (cache lookups, etc.) | Don’t set loopDetection, OR set maxRepeats: 10+ |
| Debugging an agent that’s spinning | { maxRepeats: 1, action: "abort" } — first repeat throws, you see the bug fast |
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:
ToolExecutor. (Not built in yet; would be a future addition.)
Telemetry
Loop detection emits atool.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
- Hooks and Guardrails — pre/post tool hooks for finer control
- Async HandleId Pattern — the right way to handle naturally repetitive polling