@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.

Packages

Package details

extension

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

English | 中文



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:

  1. Warn — injects a steering message telling the model to change strategy or wrap up
  2. Block — refuses the tool call; the block reason becomes an error message fed back to the model
  3. 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:

  1. Same tool name
  2. Same argument fingerprint — recursively key-sorted JSON; field order is irrelevant
  3. 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 countread of 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.
  • terminate needs 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. read of 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.