# pi-death-loop-guard

[English](./README.md) | [中文](./README.zh-CN.md)

<p align="center">
<strong>Loop-Guard</strong> — <a href="https://pi.dev">Pi</a> 的防死循环插件
<br />
模型不可能永远循环下去 —— 它会被<em>警告</em>、被<em>拦截</em>、最终被<em>终止</em>。
</p>

---

<p align="center">
<a href="https://www.npmjs.com/package/pi-death-loop-guard"><img src="https://img.shields.io/npm/v/pi-death-loop-guard?style=flat-square" alt="npm version"></a>
<a href="https://github.com/Tyan66666/pi-death-loop-guard/blob/main/LICENSE"><img src="https://img.shields.io/github/license/Tyan66666/pi-death-loop-guard?style=flat-square" alt="license"></a>
<a href="https://github.com/Tyan66666/pi-death-loop-guard"><img src="https://img.shields.io/badge/GitHub-Tyan66666%2Fpi--death--loop--guard-181717?style=flat-square&logo=github" alt="GitHub"></a>
</p>

<p align="center">
<code>pi install npm:pi-death-loop-guard</code>
</p>

---

## 为什么需要

当小模型陷入困境时,它有时会**用完全相同的参数反复调用同一个工具** —— 重复读取同一个文件、重复执行同一条命令、重复更新同一个 todo。每一次重复都在消耗 token、污染对话,而结果从未变化。Pi 本身没有内置的循环检测([#6158](https://github.com/earendil-works/pi-mono/issues/6158),closed no-action):agent 循环会一直持续下去。

**pi-death-loop-guard** 精确识别这种特征 —— 结果无变化的重复相同工具调用 —— 并以分级方式打断:

1. **警告** — 注入一条 steer 消息,提示模型更换策略或收尾
2. **拦截** — 拒绝该工具调用;拦截原因成为喂回模型的 error 消息
3. **终止** — 通过 `ctx.abort()` 终止整个用户请求

合法行为完全不受影响:参数不同的调用,或参数相同但结果持续变化的调用(轮询),都不是循环。

## 安装

```bash
pi install npm:pi-death-loop-guard
```

完成。扩展在下次 Pi 启动时自动加载。无需配置 —— 默认值(3 次警告、5 次拦截、3 次拦截后终止)开箱即用。

锁定版本:

```bash
pi install npm:pi-death-loop-guard@0.1.0
```

或从 GitHub 安装:

```bash
pi install git:github.com/Tyan66666/pi-death-loop-guard
```

## 工作原理

pi-death-loop-guard 监听 `tool_call` / `tool_result` 事件并维护一个 streak 计数器。只有**三者全部相同**才判定为循环:

1. **工具名相同**
2. **参数指纹相同** — 递归 key 排序后 JSON 序列化,字段顺序无关
3. **结果指纹相同** — 文本内容流式 SHA-256(图片只算数量,不比内容)

分级干预(默认值):

| 阶段 | 触发 | 动作 |
|------|------|------|
| 警告 | 连续 3 次相同 | 注入 steer 消息:"请停止重复,换一种策略或给出最终回答" |
| 拦截 | 连续 5 次相同(允许 4 次,拦第 5 次) | 返回 `{ block: true, reason }`;reason 成为模型可见的 error |
| 终止 | 同一用户请求内被拦 3 次 | `ctx.abort()` 终止请求 |

关键细节:

- **跨参数调用永不计数** — 读取不同文件永不触发,即使内容相同。结果对比只作用于重复的同参数调用。
- **合法轮询安全** — 参数相同但结果变化的调用不推进 streak。
- **并行批不推进 streak** — 单次 LLM 响应内的并行调用不计数,只允许批内最后一个结果与上次串行结果比较。
- **拦截后同参数重试会被连续拦截**(streak 不重置),直到请求被终止或模型更换参数。
- **`terminate` 需要 pi ≥ 0.84.1**(PR #7715),默认关闭(`LOOP_GUARD_USE_TERMINATE`);旧版本下插件主体可安全运行。

## 配置

全部通过环境变量配置,无需配置文件。

| 变量 | 默认值 | 说明 |
|------|--------|------|
| `LOOP_GUARD_ENABLED` | `true` | 总开关 |
| `LOOP_GUARD_WARN_THRESHOLD` | `3` | 连续多少次相同后警告 |
| `LOOP_GUARD_BLOCK_THRESHOLD` | `5` | 连续多少次相同后拦截(必须 > warn) |
| `LOOP_GUARD_ABORT_THRESHOLD` | `3` | 同一用户请求内拦截多少次后 `ctx.abort()` |
| `LOOP_GUARD_IGNORE_TOOLS` | *(空)* | 白名单工具(逗号分隔,如 `todo,bash`) |
| `LOOP_GUARD_IGNORE_ERRORS` | `false` | 跳过 `isError=true` 的结果(错误重试不计数) |
| `LOOP_GUARD_USE_TERMINATE` | `false` | 拦截时附带 `terminate: true`(需 pi ≥ 0.84.1) |

示例:

```bash
LOOP_GUARD_WARN_THRESHOLD=5 LOOP_GUARD_BLOCK_THRESHOLD=8 pi
```

若 `warnThreshold >= blockThreshold`,插件会自我禁用并记录错误,而不是异常运行。

## 插件兼容性

pi-death-loop-guard 只拦截 `tool_call`(用于拦截)和 `tool_result`(用于结果指纹)。它**不**触碰 `context` 事件,因此可以与上下文压缩插件(如 billion-context-pi)及任何其他扩展共存。

一点须知:当多个扩展同时 block 同一个 `tool_call` 时,pi 按加载顺序采用第一个 `{ block: true }` 结果。如果你运行多个拦截类扩展,先加载的优先。这是 Pi 扩展模型的固有限制,并非 pi-death-loop-guard 特有。

## 开发

```bash
npm install            # typescript / @types/node / tsx(--ignore-scripts)
npm run check          # tsc 类型检查
npm test               # 单元测试(mock ExtensionAPI 驱动,无需真实 pi)
```

测试覆盖:警告触发、拦截时机(允许 4 次拦第 5 次)、合法轮询不误伤、跨参数重置、abortThreshold 终止、`before_agent_start` 重置、ignoreErrors、并行批、useTerminate、ignoreTools 白名单、配置不变量。

## 已知局限

- **换参数循环**(如轮换读取一组文件)检测不到 —— 通常有进展,危害有限。
- **交错循环**(`read(A) → bash(cmd) → read(A) → bash(cmd)`)不在目标范围。
- **图片只算数量,不比内容** — 两个不同图片 + 相同文本可能误报。

## 许可证

MIT。
