Skip to main content

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
By the time you notice, the bill is real. loopDetection on ToolExecutor catches this within a single run.

Configuration

import { ToolExecutor } from "@agentium/core";

const executor = new ToolExecutor(tools, {
  loopDetection: {
    maxRepeats: 3,
    action: "abort", // or "hint"
  },
});
FieldTypeMeaning
maxRepeatsnumberThreshold 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:
import { ToolLoopError } from "@agentium/core";

try {
  const result = await agent.run(input);
} catch (err) {
  if (err instanceof ToolLoopError) {
    console.error(`Loop detected: ${err.toolName} called ${err.repeats} times`);
    // Decide what to do — fall back to a different model? Ask the user for help?
  }
}
ToolLoopError properties:
class ToolLoopError extends Error {
  readonly name = "ToolLoopError";
  readonly toolName: string;
  readonly repeats: number;
  readonly message: string;  // "Tool "X" was called N times with identical arguments - aborting to prevent a loop"
}

action: "hint"

Instead of throwing, the executor returns a synthetic tool result back to the LLM:
[loop-detected] Tool "fetchData" has now been called 4 times with identical arguments.
Consider trying a different approach, changing the arguments, or finishing the response.
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.
const result = await executor.executeAll([call], ctx);
if (result[0].error === "loop-detected") {
  // The model is getting stuck. Surface this in your UI / metrics.
}

How the counter works

The executor maintains a Map<string, number> per ToolExecutor instance. The key is:
${toolName}::${JSON.stringify(arguments)}
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.
Use caseSetting
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:
const executor = new ToolExecutor(tools, {
  // Only abort after many repeats — pollResult will hit this legitimately.
  loopDetection: { maxRepeats: 30, action: "hint" },
});
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