Initial commit: pi-loop-guard extension

This commit is contained in:
2026-05-30 11:18:56 -04:00
commit 292253ff41
3 changed files with 160 additions and 0 deletions
+54
View File
@@ -0,0 +1,54 @@
# pi-loop-guard
Prevents infinite tool call loops in pi by counting tool calls and turns, then aborting the agent when a configurable threshold is exceeded.
## Why?
When the LLM encounters repeated errors (database connection failures, missing API keys, etc.), it can get stuck in an infinite loop of tool calls that all return the same error. This extension detects and stops that pattern.
## Installation
### Via pi package (recommended)
```bash
pi install git:unraid.local:3003/pi-dev/pi-loop-guard
```
### Manual
Place in `~/.pi/agent/extensions/` or `.pi/extensions/`:
```bash
cp -r pi-loop-guard ~/.pi/agent/extensions/pi-loop-guard
```
## Configuration
Add to `.pi/settings.json` or `~/.pi/agent/settings.json`:
```json
{
"loopGuard": {
"maxToolCalls": 50,
"maxTurns": 20
}
}
```
| Setting | Default | Description |
|---------|---------|-------------|
| `maxToolCalls` | 50 | Maximum total tool calls before abort |
| `maxTurns` | 20 | Maximum turns (LLM responses) before abort |
## Behavior
- Counts tool calls across all turns in a session
- Counts total turns (LLM responses)
- When either threshold is exceeded, calls `ctx.abort()` to stop the agent
- Displays an error notification with details
## Example
```
Loop guard: stopped after 51 tool calls (threshold: 50). Possible infinite loop detected.
```
+13
View File
@@ -0,0 +1,13 @@
{
"name": "pi-loop-guard",
"version": "1.0.0",
"description": "Prevents infinite tool call loops in pi by counting tool calls and aborting the agent when a threshold is exceeded.",
"keywords": ["pi-package"],
"pi": {
"extensions": ["./src/index.ts"]
},
"dependencies": {},
"devDependencies": {
"@earendil-works/pi-coding-agent": "*"
}
}
+93
View File
@@ -0,0 +1,93 @@
/**
* pi-loop-guard — Infinite loop prevention extension
*
* Counts tool calls across turns and aborts the agent when a configurable
* threshold is exceeded. This prevents the agent from looping indefinitely
* when errors repeat (e.g., database connection failures, missing API keys).
*
* Usage:
* pi install git:unraid.local:3003/pi-dev/pi-loop-guard
*
* Configuration (in .pi/settings.json or ~/.pi/agent/settings.json):
* {
* "loopGuard": {
* "maxToolCalls": 50,
* "maxTurns": 20
* }
* }
*/
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
interface LoopGuardSettings {
maxToolCalls: number;
maxTurns: number;
}
const DEFAULT_SETTINGS: LoopGuardSettings = {
maxToolCalls: 50,
maxTurns: 20,
};
function readSettings(ctx: ExtensionContext): LoopGuardSettings {
try {
const raw = ctx.sessionManager.getEntries();
for (const entry of raw) {
if (entry.type === "custom" && entry.customType === "loopguard-settings") {
const s = entry.data as Partial<LoopGuardSettings>;
return {
maxToolCalls: s.maxToolCalls ?? DEFAULT_SETTINGS.maxToolCalls,
maxTurns: s.maxTurns ?? DEFAULT_SETTINGS.maxTurns,
};
}
}
} catch {
// Ignore errors reading settings
}
return { ...DEFAULT_SETTINGS };
}
export default function (pi: ExtensionAPI) {
let toolCallCount = 0;
let turnCount = 0;
let settings: LoopGuardSettings = { ...DEFAULT_SETTINGS };
// Load settings on session start
pi.on("session_start", async (_event, ctx) => {
settings = readSettings(ctx);
});
// Count tool calls and turns
pi.on("turn_end", async (_event, ctx) => {
const entries = ctx.sessionManager.getEntries();
const lastEntry = entries[entries.length - 1];
if (lastEntry?.type === "message" && lastEntry.message?.role === "assistant") {
const content = lastEntry.message.content as Array<{ type: string }>;
const toolCallsInTurn = content.filter((c) => c.type === "toolCall").length;
toolCallCount += toolCallsInTurn;
}
turnCount++;
if (toolCallCount >= settings.maxToolCalls) {
ctx.abort();
ctx.ui.notify(
`Loop guard: stopped after ${toolCallCount} tool calls (threshold: ${settings.maxToolCalls}). Possible infinite loop detected.`,
"error",
);
} else if (turnCount >= settings.maxTurns) {
ctx.abort();
ctx.ui.notify(
`Loop guard: stopped after ${turnCount} turns (threshold: ${settings.maxTurns}). Possible infinite loop detected.`,
"error",
);
}
});
// Reset counters on session shutdown
pi.on("session_shutdown", async () => {
toolCallCount = 0;
turnCount = 0;
});
}