Stop your tool-using AI agent from getting stuck in loops. A tiny, zero-dependency TypeScript guardrail that detects the three ways autonomous LLM agents spin their wheels — identical repeated tool calls, cyclic tool-call patterns, and idle status-check loops — and hands back a corrective nudge you can feed straight into the model.
Give an LLM a set of tools and a long-running task and it will, sooner or later, get stuck. Three failure modes dominate:
- Identical repeated calls — it calls
read_file("config.json")over and over, expecting a different answer. - Cyclic tool-call patterns — it oscillates between the same two or three tools every turn without making progress (the classic "submit proposal batch #265… submit proposal batch #265… forever" death spiral).
- Idle status-check loops — it keeps calling
git_status,check_credits,list_goalsand never actually does anything.
agent-loop-detector catches all three with a small amount of per-agent state and returns a plain-English reason you can inject as the next tool result so the model corrects course (or calls task_done). It escalates the cyclic case from a warning on the third repeat to a hard enforcement block if the agent ignores the warning.
npm i github:Princeu3/agent-loop-detectornpm package coming soon. Until then, install straight from GitHub (the command above).
import { LoopDetector } from "agent-loop-detector";
const detector = new LoopDetector({
maxIdenticalCalls: 3, // block after N identical calls in a row
maxIdleOnlyTurns: 3, // nudge after N turns of only status tools
windowSize: 10, // how many recent turns/calls to remember
});
// On every tool call the agent makes:
function onToolCall(name: string, argsJson: string) {
const check = detector.recordToolCall(name, argsJson);
if (check.blocked) {
// feed check.reason back to the model instead of executing the tool
return { role: "tool", content: check.reason };
}
// ...actually run the tool...
}
// At the end of each agent turn:
function onTurnEnd() {
const check = detector.endTurn();
if (check.reason) {
// inject check.reason as a system/tool nudge (warning, enforcement, or idle-loop)
}
}
// Starting a brand-new task? Wipe the state:
detector.reset();The notion of an "idle / status-only" tool is configurable. Pass a Set or a predicate:
import { LoopDetector, DEFAULT_IDLE_ONLY_TOOLS } from "agent-loop-detector";
new LoopDetector({
idleOnlyTools: new Set([...DEFAULT_IDLE_ONLY_TOOLS, "get_dashboard"]),
});
new LoopDetector({
idleOnlyTools: (name) => name.startsWith("status_") || name.endsWith("_info"),
});If you omit it, DEFAULT_IDLE_ONLY_TOOLS is used.
| option | default | meaning |
|---|---|---|
maxIdenticalCalls |
3 |
Block after this many identical (same name and same args) calls in a row. |
maxIdleOnlyTurns |
3 |
Nudge after this many consecutive turns that used only idle/status tools. |
windowSize |
10 |
How many recent turn-patterns / calls to keep in memory. |
idleOnlyTools |
DEFAULT_IDLE_ONLY_TOOLS |
Set<string> or (name) => boolean deciding which tools are "idle/status-only". |
Call for every tool invocation. Returns { blocked, reason }. When blocked is true, don't run the tool — feed reason back to the model. Detects the identical repeated call loop.
Call at the end of each agent turn. Detects cyclic turn-patterns (warn on the 3rd repeat, then blocked: true enforcement if ignored) and idle-only turn loops (a non-blocking nudge). A non-empty reason with blocked: false is advisory — inject it, but let the turn proceed.
Clears all internal state. Call between independent tasks.
interface LoopCheckResult {
blocked: boolean; // true => do not execute; feed `reason` back to the model
reason: string; // human-readable nudge, or "" when nothing to say
}Also exported: DEFAULT_IDLE_ONLY_TOOLS: ReadonlySet<string> and simpleHash(str): string.
Pairs well with react-agent-loop (the agent loop itself) and agent-policy-engine (spend / action policy) — drop this in as the loop-safety layer.
Extracted, refactored, and decoupled by Prince Upadhyay (@Princeu3) from the MIT-licensed Conway-Research/automaton project. The idle-tool coupling was inlined and made configurable. See NOTICE for details.
ai-agents, llm-agents, loop-detection, agent-loops, react-agent, tool-use, infinite-loop, guardrails, autonomous-agents, agent-safety, typescript, llm