@tyanyin/pi-loop-guard
Anti-death-loop guard extension for pi-coding-agent: detects repeated identical tool calls with unchanged results, warns, blocks, and aborts.
Package details
Install @tyanyin/pi-loop-guard from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@tyanyin/pi-loop-guard- Package
@tyanyin/pi-loop-guard- Version
0.1.0- Published
- Aug 9, 2026
- Downloads
- 117/mo · 7/wk
- Author
- tyanyin
- License
- MIT
- Types
- extension
- Size
- 23.2 KB
- Dependencies
- 0 dependencies · 1 peer
Pi manifest JSON
{
"extensions": [
"./extensions"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-loop-guard
Why?
When a small model gets stuck, it sometimes calls the same tool with the same arguments over and over — re-reading the same file, re-running the same command, re-updating the same todo. Each repetition costs tokens, pollutes the conversation, and the results never change. Pi has no built-in loop detection (#6158, closed no-action): the agent loop just keeps going.
pi-loop-guard watches for this exact signature — repeated identical tool calls with unchanged results — and interrupts with an escalating response:
- Warn — injects a steering message telling the model to change strategy or wrap up
- Block — refuses the tool call; the block reason becomes an error message fed back to the model
- Abort — terminates the whole user request via
ctx.abort()
Legitimate behavior is never touched: calls with different arguments, or identical calls whose results keep changing (polling), are not loops.
Install
pi install git:github.com/Tyan66666/pi-loop-guard
That's it. The extension auto-loads on next Pi startup. No configuration needed — the defaults (warn at 3, block at 5, abort after 3 blocks) work out of the box.
To pin a version:
pi install git:github.com/Tyan66666/pi-loop-guard@v0.1.0
How it works
pi-loop-guard listens to tool_call / tool_result events and maintains a streak counter. A loop is only declared when all three match:
- Same tool name
- Same argument fingerprint — recursively key-sorted JSON; field order is irrelevant
- Same result fingerprint — streaming SHA-256 over text content (images count only, not content)
Escalation (defaults):
| Stage | Trigger | Action |
|---|---|---|
| Warn | 3 consecutive matches | Inject a steer message: "change strategy or give a final answer" |
| Block | 5 consecutive matches (4 allowed, 5th refused) | Return { block: true, reason }; reason becomes an error the model sees |
| Abort | 3 blocks within the same user request | ctx.abort() ends the request |
Key details:
- Cross-argument calls never count —
readof different files never triggers, even with identical content. Result comparison only applies to repeated same-argument calls. - Legitimate polling is safe — identical calls whose results change don't advance the streak.
- Parallel batches don't advance the streak — parallel calls inside a single LLM response are not counted; only the last result of a batch compares against the previous serial result.
- After a block, same-argument retries are blocked every time (the streak is not reset), until the request is aborted or the model changes arguments.
terminateneeds pi ≥ 0.84.1 (PR #7715) and is off by default (LOOP_GUARD_USE_TERMINATE); the plugin core runs safely on older versions.
Configuration
All configuration is via environment variables. No config file needed.
| Variable | Default | Description |
|---|---|---|
LOOP_GUARD_ENABLED |
true |
Master switch |
LOOP_GUARD_WARN_THRESHOLD |
3 |
Consecutive matches before warning |
LOOP_GUARD_BLOCK_THRESHOLD |
5 |
Consecutive matches before blocking (must be > warn) |
LOOP_GUARD_ABORT_THRESHOLD |
3 |
Blocks within one user request before ctx.abort() |
LOOP_GUARD_IGNORE_TOOLS |
(empty) | Comma-separated tool allowlist, e.g. todo,bash |
LOOP_GUARD_IGNORE_ERRORS |
false |
Skip isError=true results (error retries don't count) |
LOOP_GUARD_USE_TERMINATE |
false |
Also return terminate: true on block (needs pi ≥ 0.84.1) |
Example:
LOOP_GUARD_WARN_THRESHOLD=5 LOOP_GUARD_BLOCK_THRESHOLD=8 pi
If warnThreshold >= blockThreshold, the plugin disables itself and logs an error instead of misbehaving.
Plugin compatibility
pi-loop-guard only intercepts tool_call (to block) and tool_result (to fingerprint results). It does not touch the context event, so it coexists with context-compression plugins (e.g. billion-context-pi) and any other extension.
One thing to know: when multiple extensions block the same tool_call, pi honors the first { block: true } result in load order. If you run several blocking extensions, whichever loads first has priority. This is a Pi extension-model limitation, not specific to pi-loop-guard.
Development
npm install # typescript / @types/node / tsx (--ignore-scripts)
npm run check # tsc type check
npm test # unit tests with a mocked ExtensionAPI, no real pi needed
Tests cover: warning trigger, block timing (4 allowed, 5th blocked), safe polling, cross-argument reset, abort threshold, before_agent_start reset, ignoreErrors, parallel batches, useTerminate, ignoreTools, and config invariants.
Known limitations
- Argument-switching loops (e.g.
readof a rotating set of files) are not detected — usually they make progress, so harm is limited. - Interleaved loops (
read(A) → bash(cmd) → read(A) → bash(cmd)) are out of scope. - Images count, not content — two different images plus identical text can false-positive.
License
MIT.