Detect degenerate loops in LLM agent tool-call trajectories — and break them before they burn tokens.
Autonomous agents get stuck. They call the same tool with the same arguments over and over, bounce between two actions forever, or keep re-running a query that returns the same useless result. loopstop is a tiny, zero-dependency library + CLI that spots these patterns so your runtime can stop early, and so your CI/eval harness can fail a trajectory that loops.
- 🔁 Three detectors — consecutive repeats, A⇄B oscillation cycles, and no-progress (same call → same result).
- 🧩 Format-agnostic — reads OpenAI, Anthropic, and LangChain-style step shapes out of the box.
- 🛡️ Runtime guard — a streaming
LoopGuardyou drop inside your agent loop. - 🚦 CI gate — exits non-zero when a trajectory loops, so it plugs into evals.
- 📦 Zero dependencies, ESM, Node ≥ 18. No API keys, no network, fully offline.
npm install loopstop
# or run the CLI without installing:
npx loopstop trajectory.jsonlPipe in a trajectory (JSON array, JSONL, or an object with a steps/trajectory/messages array):
loopstop examples/stuck.jsonl● LOOPING 4 steps analyzed
verdict: stop severity: █████░░░░░ 0.50
consecutive-repeat
Tool call repeated 4× in a row (indices 0-3).
no-progress
Same call and result observed 4× — the agent is making no progress.
Exit code is 1 when a loop is found and 0 when it's clean — drop it straight into CI:
loopstop run.jsonl --quiet || echo "agent looped!"Get machine-readable output:
cat run.jsonl | loopstop --json| Flag | Default | Description |
|---|---|---|
--consecutive <n> |
3 |
flag N identical calls in a row |
--cycle-repeats <n> |
2 |
flag a cycle repeated N times |
--max-period <n> |
8 |
longest oscillation period to look for |
--no-progress <n> |
3 |
flag same call+result seen N times |
--ignore-args |
off | match on tool name only, ignore arguments |
--json |
off | output the full report as JSON |
--quiet |
off | print nothing; rely on the exit code |
import { analyze } from 'loopstop';
const report = analyze([
{ tool: 'search', args: { q: 'paris' }, result: 'no results' },
{ tool: 'search', args: { q: 'paris' }, result: 'no results' },
{ tool: 'search', args: { q: 'paris' }, result: 'no results' },
]);
report.looping; // true
report.verdict; // 'stop'
report.severity; // 0.50
report.findings; // [{ type: 'consecutive-repeat', ... }, { type: 'no-progress', ... }]LoopGuard keeps a bounded window of recent steps so it stays cheap even on long runs:
import { LoopGuard } from 'loopstop';
const guard = new LoopGuard({ consecutive: 3 });
while (true) {
const call = await model.nextToolCall();
const verdict = guard.push(call); // push the step as it happens
if (verdict.looping) {
console.warn('Loop detected:', verdict.topFinding.message);
break; // stop before the next model call
}
await runTool(call);
}A "step" is one tool call. loopstop reads whichever field names your runtime uses:
- tool name:
tool,name,tool_name,function,action - arguments:
args,arguments,input,parameters,params - result (optional):
result,output,observation,response
Argument objects are compared with stable, key-order-independent hashing, so {a:1, b:2} and {b:2, a:1} count as the same call.
| Detector | Pattern | Example |
|---|---|---|
consecutive-repeat |
the same call N times in a row | search → search → search |
cycle |
a short pattern oscillating at the tail | list → read → list → read → … |
no-progress |
same call and same result repeatedly | a query that keeps returning "no results" |
The no-progress detector only fires when results are present — without a result it has no evidence either way.
Most agent frameworks only offer a blunt max_turns cap. That stops runaways eventually, but wastes every turn until the cap, and tells you nothing about why it stalled. loopstop recognizes the actual loop the moment it forms, names the pattern, and hands you a severity score — useful both as a runtime kill-switch and as an offline trajectory linter.
npm test # runs the node:test suite (no build step)loopstop is free and MIT-licensed. If it saved you some wasted tokens and you'd like to say thanks, an optional crypto tip is always welcome (never expected):
- USDT (Ethereum / ERC-20):
0xad39bdf2df0b8dd6991150fcea0a156150ed19b8 - Verify on Etherscan
Please send only on the Ethereum (ERC-20) network.
MIT © 2026 Ayubjon