Pi 扩展:激活后无限自我迭代。
循环不会因为模型或进程的任何自身原因停止:模型说「做完了」、反问、请求确认、请求出错、运行被 Esc 中断、扩展重载、进程被强杀,都会自动继续。常规停止方式是执行 /loop-off 或 /loop-off-now;唯一例外是上下文超限,重试只会让上下文更长,此时先压缩恢复,恢复失败才停止并提示。
- 需要无人值守连续推进同一目标:从「安装」开始。
- 了解循环的完整行为:见「循环为什么停不下来」。
- 修改扩展代码:先读「关键认知」。
pi-loop 的主体是单个 TypeScript 文件 index.ts。Pi 内置 TypeScript 加载,不需要编译;@earendil-works/pi-coding-agent 与 @earendil-works/pi-tui 由 Pi 宿主在运行时提供,不需要执行 npm install。
推荐用 Pi 包安装,版本更新可用 pi update --extensions 统一处理:
# 用户级(写入 ~/.pi/agent/settings.json)
pi install git:github.com/jerryshell/pi-loop
# 项目级(写入 .pi/settings.json)
pi install -l git:github.com/jerryshell/pi-loop卸载:
pi remove git:github.com/jerryshell/pi-loop手动安装:从仓库检出目录复制 index.ts。
用户级安装(所有项目可用):
mkdir -p ~/.pi/agent/extensions/pi-loop
cp index.ts ~/.pi/agent/extensions/pi-loop/index.ts项目级安装(只对当前项目生效):
mkdir -p .pi/extensions/pi-loop
cp index.ts .pi/extensions/pi-loop/index.tsWindows 的用户级路径为 %USERPROFILE%\.pi\agent\extensions\pi-loop\index.ts。
放置完成后重启 Pi;已在会话中则执行 /reload。命令列表中出现 /loop 即加载成功。
不改动配置、只试用一次:
pi --extension /path/to/pi-loop
# 简写
pi -e /path/to/pi-loop--extension 接受扩展文件,也接受包含 index.ts 的目录。
git clone https://github.com/jerryshell/pi-loop
cd pi-loop
npm install # 开发依赖:类型检查与代码工具
npm run check # 类型检查 + oxlint + oxfmt 格式检查
pi -e .代码检查用 oxlint(配置 .oxlintrc.json),代码格式化用 oxfmt(配置 .oxfmtrc.json)。日常用 npm run format 就地格式化,提交前用 npm run check 做总检查。
改动 index.ts 后,在 Pi 中执行 /reload 即可生效。Pi 会替换扩展运行时,循环状态从会话记录恢复,继续迭代。
把仓库目录链接到扩展目录,可省去手动复制,之后只需执行 /reload:
# macOS / Linux
ln -s "$(pwd)" ~/.pi/agent/extensions/pi-loop
# Windows(目录联接)
mklink /J "%USERPROFILE%\.pi\agent\extensions\pi-loop" "%CD%"包清单:package.json 的 pi.extensions 声明入口 index.ts,宿主包放入 peerDependencies("*"),devDependencies 只用于编辑器类型与 npm run typecheck。宿主提供的包不要放入 dependencies,避免绕过宿主的模块映射。
| 命令 | 行为 |
|---|---|
/loop [目标] |
激活循环。空闲时立即开始下一轮;运行中则在本轮结束时接管,带参数可随时更新长期目标。 |
/loop-off |
里程碑停止:当前轮自然结束后停止;空闲时立即停止。 |
/loop-off-now |
立即停止:中止当前运行,取消所有待执行的重启,清除循环状态。 |
轮数在同一会话内累计;停止后重新激活不会归零。
| 显示 | 含义 |
|---|---|
∞ loop #12 |
运行中,#12 为已开始的轮数 |
∞ loop #12 · 工具 3 次 2.4s |
本轮已有顶层工具调用;有失败时显示失败次数,耗时满 1 s 才显示 |
∞ loop #12 ↻ |
运行已结束,等待自动重新点火 |
∞ loop #12 ⚠ |
最近连续多轮没有工具调用,迭代指令已附加停滞警告 |
∞ loop #12 ✗ |
上一轮调用了工具但全部失败,迭代指令已附加失败提示 |
∞ loop #12 收尾中… |
已执行 /loop-off,等待当前轮结束 |
| 触发情况 | 处理 |
|---|---|
| 模型自然结束、反问、请求确认 | agent_before_settle 返回 continue: true,注入下一轮迭代指令 |
| 模型请求出错 | 不停止;按 5 s → 10 s → … → 60 s 退避后继续,并要求先诊断错误 |
上下文超限(错误文本命中 context length、too many tokens 等特征) |
不重试:压缩会话后自动续跑;压缩失败或压缩后仍超限则停止循环并提示 |
| 运行被 Esc 中断 | agent_settled 后自动重新点火 |
| 运行异常结束、边界无法继续 | agent_settled 后自动重新点火 |
| 看门狗发现「已激活但空闲」 | 每 15 s 检查一次,立即重新点火 |
| 退避期间用户开始了新运行或有排队消息 | 暂不点火;用户运行结束后在结算点继续循环 |
扩展重载(/reload) |
新运行时从会话记录恢复状态,自动继续 |
| 进程退出、被强杀 | 用 --continue 恢复该会话时自动继续迭代 |
重新点火的延迟从 1 s 起指数增加,封顶 30 s;每次运行成功启动后归零。
每轮开始时向模型注入指令,核心内容:
- 立即开始当前最高价值的下一步工作并直接动手;不征求许可、不复述计划、不以提问结束。
- 本轮必须有真实产出(文件修改、命令执行、验证结果);纯文字回复不算迭代。
- 完成后自行验证(测试、构建、运行),简短记录结果和新发现,然后继续下一轮。
- 当前目标完成后,主动寻找下一个更高价值的改进点继续推进。
- 只有外部指令
/loop-off或/loop-off-now才能结束循环;不自行收尾,也不询问是否继续。
附加提示按情况追加:连续无工具调用时追加停滞警告;上一轮工具调用全部失败时要求先定位失败原因;上一轮出错时要求先诊断错误;上一轮被中断时要求从中断处继续;上下文超限压缩恢复后要求从中断处继续、不重复已完成的工作。
上下文规则:运行期间只追加、不改写历史;压缩成功后清理旧指令;循环关闭后全部移除。指令会随轮数累积到压缩窗口上限,这是保住前缀缓存的取舍(见「关键认知」第 3 条)。
修改代码前先读本节。以下契约是扩展对外行为的依据,违反任意一条都视为回归。
模型的一切自述都不算停止信号:完成、提问、请求确认、报错、被中断都只触发继续。循环只有三种阶段:off(关闭)、running(运行)、stopping(已执行 /loop-off,等待本轮里程碑)。
唯一例外是上下文超限。超限时重试只会把上下文越堆越长,注定失败;此时先压缩会话恢复,压缩成功自动续跑,压缩失败或压缩后仍超限才停止循环并提示。识别依据是最近一次模型请求的错误文本特征(context length、too many tokens 等),并排除限流、配额类错误,不依赖具体服务商文案。
| 机制 | 时机 | 作用 |
|---|---|---|
agent_before_settle |
模型即将自然结束 | 注入下一轮并返回 continue: true |
agent_settled |
运行彻底结束(中断、异常) | 延迟重新点火,带指数退避 |
| 看门狗 | 每 15 s | 阶段为 running 但运行已空闲时重新点火 |
session_start |
会话启动、--continue 恢复 |
读取会话记录恢复状态并续跑 |
四道机制可能同时命中,用 runActive、restartArmed、pendingRestart 三个标志互斥。改动其中一道必须检查其余三道是否会重复点火。重新点火前先检查 ctx.isIdle() 与 ctx.hasPendingMessages():用户输入优先,避免迭代指令以 steer 插进用户的运行。
前缀缓存(Anthropic 的显式断点,OpenAI、Gemini、DeepSeek 的自动前缀缓存)都按 token 前缀精确匹配:删除历史中的任意一条消息,都会让删除点之后的缓存全部失效。按轮删除上一条迭代指令,删除点正好落在历史中段,整段对话会在每轮边界反复重算,循环越久浪费越大。
context 钩子在运行期间不裁剪任何消息,只在两个时机清理:
- 压缩成功后:压缩本身重写历史、缓存必然失效,顺带清理旧指令不产生额外代价。
- 循环关闭后:全部移除,不残留「循环激活」的指令。
代价是指令随轮数累积,上限由压缩窗口约束。不要为「只留一条」恢复按轮删除。
tool_execution_start 置位 usedToolThisRound;一整轮没有任何工具调用时 stallStreak 加一,下一轮指令追加停滞警告。纯文字回复不算迭代。
tool_execution_end 只补充两件事:统计顶层调用的失败次数与耗时用于状态栏;上一轮「有调用但全部失败」时单独追加失败提示(状态栏 ✗)。失败提示不参与 stallStreak 判定,因为调试时运行失败的测试或构建同样是有效动作。嵌套调用(codemode 等工具内部发起,带 parentToolCallId)不计入次数与耗时,避免与其顶层调用的耗时重复累计。
phase、goal、iteration 通过 pi.appendEntry("pi-loop-state", ...) 持久化到会话。跨进程恢复(--continue、强杀后重启)依赖它;只在内存里改状态会导致恢复后行为错乱。
- 模型请求错误:5 s → 10 s → … → 60 s 封顶。
- 上下文超限:不走退避,改为压缩恢复;压缩期间看门狗与其余点火机制全部让位。
- 重新点火:1 s 起,指数增加,30 s 封顶;运行成功启动后归零。
- 会话恢复:延迟 1.5 s 点火,避免抢占用户启动时提交的第一条消息。
只用宿主提供的 @earendil-works/pi-coding-agent 与 @earendil-works/pi-tui;不引入编译步骤,pi -e . 直接运行。新增第三方依赖前先确认宿主的提供清单(Pi 文档 packages.md)。
| 现象 | 先检查 | 处理 |
|---|---|---|
命令列表没有 /loop |
扩展是否加载 | 确认 index.ts 位于用户级或项目级扩展目录,执行 /reload 或重启 Pi |
状态栏没有 ∞ loop |
循环是否已激活 | 执行 /loop;状态栏只在激活后显示 |
状态栏停在 ↻ |
是否处于退避延迟 | 等待重新点火,最长延迟 30 s |
| 连续多轮没有工具调用 | 状态栏是否显示 ⚠ |
无需人工处理,迭代指令会附加停滞警告 |
状态栏显示 ✗ |
上一轮工具全失败 | 无需人工处理,迭代指令会要求模型先定位失败原因 |
| 循环停止并提示上下文超限 | 压缩是否失败 | 压缩失败时执行 /compact 清理会话后再 /loop;压缩成功后循环已自动续跑 |
- 本 README 只写当前事实,不记录变更历史;历史见
git log。 - 代码行为变化必须与本文档在同一次提交内完成,对应关系:
| 改动内容 | 同步更新 |
|---|---|
| 命令、参数、状态栏文案 | 「命令」「底部状态栏」 |
| 点火机制、退避策略、触发条件 | 「循环为什么停不下来」「关键认知」 |
| 迭代指令文案 | 「每轮注入的迭代指令」 |
| 安装方式、加载路径 | 「安装」 |
| 包清单、类型与代码工具配置 | package.json、tsconfig.json、.oxfmtrc.json、.oxlintrc.json |
| 设计契约 | 「关键认知」 |
- 注释与文案统一使用简体中文,遵守项目的中文排版规范:直角引号「」、中西文之间留白、不使用第二人称、不使用破折号。
- 修改代码前先读「关键认知」;新增触发路径时必须同步更新「循环为什么停不下来」。
- Pi 发布新版本后,先把
devDependencies中两个宿主包的版本下限提升到新版本并重新安装,再运行npm run check;peerDependencies保持"*"。 - 提交前运行
npm run check(类型检查、oxlint、oxfmt 格式检查)。
MIT,见 LICENSE。