pi-auto-goal
v0.7.0
Published
Pi stop guard: judges premature agent stops with a second model and continues the task
Maintainers
Readme
pi-auto-goal
Pi 的停止守卫:agent 停下来时,用第二个模型判断这次停止是否属于「擅自早停」,命中就用 system 提示把 agent 赶回去继续干活。
它做什么
一个多步任务只做了一半,agent 却改完一个文件、汇报了进度就结束——这在 Pi 里很常见。pi-auto-goal 监听 agent_settled(Pi 确定不会自己继续的时刻),然后用第二个模型只回答一个问题:此刻停下算合理,还是还有活没干完?
当结论是「还有活没干完」且置信度足够高时,扩展会注入一条 system 继续指令,点明缺口并要求 agent 继续(不会伪装成你自己发的一条用户消息)。否则保持安静,本轮正常结束。
安装与使用
pi install npm:pi-auto-goal安装后执行 /reload;用 /config:auto-goal 改的配置立即生效,手动改配置文件才需要 /reload。
除非 enabled 为 false,每次完全停止都会判定一次,所以一句普通问答也会多花一次判定调用。判定输入包括:
- 用户请求:当前分支上最后一条真实用户输入。本扩展注入的催促是
custom角色的消息,不是用户输入,因此连续干预时判定的始终是你最初的请求。 - 用户回答:agent 用
ask_user_question问过你时,你当时的回答(会话里存成工具结果而不是用户消息)。它是本轮的用户输入,判定提示词规定它优先于原始请求:你借回答缩小范围、选定方案或要求先停下时,agent 停下来属于正常结束。单条回答按maxUserAnswerChars(默认2000)截断。 - 最后输出:agent 本轮最后一段文本输出。
- 工具轨迹(可选,默认关闭:
includeToolTrace):该用户输入之后发生的工具调用,每条压缩成一行;默认不发,判定只看上面几段文本。
判定模型看不到你其它的会话分支。
安全边界
- 干预次数有上限:
maxAutoContinues(默认2)限制同一条用户请求的自动干预次数;你自己发新消息会重置计数。到达上限只提示一次,之后不再干预。 - 不打断你的输入:判定期间如果你开了新一轮、有排队消息,或会话分支已经变化,判定结果直接丢弃。
- 要求落地的改动指令不能只复述:用户请求是「去掉 X」「统一用 Y」「A 应该是 B」这类改动指令(祈使句或目标状态描述)时,agent 只复述规则、列出改动后的样子、回一句「明白了」而没有落地迹象,判定为提前停止;只有请求本身是提问、讨论或征求意见时才按「答完就能停」处理。
- 在等后台任务时判为正常等待:agent 的最后输出说明它在等后台任务结果(例如刚启动
subagent、subagent_resume或workflow,并说明结果会自动送回来)时,判定提示词要求判「可以停止」——后台任务跑完会自己唤醒会话,此时催促只会多打一轮;这一条优先于其它判定条目。判断依据就是 agent 的最后输出,不依赖工具轨迹。 - 失败必须报告:判定、鉴权、超时和发送失败都会在 UI 里报错,判定失败绝不会被当成「可以停止」。
- 只在持续会话中生效:TUI 与 RPC 模式下才会判定;print/json 模式在 agent 停止后就收尾,催促不可能执行,因此直接跳过。
配置
文件:<pi-agent-dir>/extensions/pi-auto-goal/config.json,遵守 PI_CODING_AGENT_DIR。可从 config.example.json 开始。
{
"enabled": true,
"model": "",
"maxAutoContinues": 2,
"confidenceThreshold": 0.6,
"timeoutSeconds": 30,
"includeToolTrace": false,
"maxUserRequestChars": 2000,
"maxFinalOutputChars": 4000,
"maxToolTraceEntries": 20,
"maxUserAnswerChars": 2000,
"notifyOnStopDecision": false,
"showVerdictNotice": true,
"judgeMaxTokens": 2000,
"continueMessageTemplate": "",
"forcedDecision": "auto"
}| 字段 | 默认值 | 含义 |
| --- | --- | --- |
| enabled | true | 总开关;关闭后不产生任何判定调用。 |
| model | "" | 判定模型,格式 provider/modelId;留空则复用当前会话模型。可直接在 /config:auto-goal 菜单里选,或用 /config:auto-goal model <provider/modelId> 指定。 |
| maxAutoContinues | 2 | 同一条用户请求允许的自动干预次数;0 表示不限制。 |
| confidenceThreshold | 0.6 | 触发干预所需的最低置信度。 |
| timeoutSeconds | 30 | 判定请求超时;超时只报告,不当成可以停止。 |
| includeToolTrace | false | 是否把本轮工具调用轨迹交给判定模型。默认关闭:判定应当基于用户请求与 agent 的最后输出。 |
| maxUserRequestChars | 2000 | 用户请求截断长度。 |
| maxFinalOutputChars | 4000 | agent 最后输出截断长度。 |
| maxToolTraceEntries | 20 | 工具轨迹最大条数。 |
| maxUserAnswerChars | 2000 | 单条用户回答(ask_user_question 的答案)截断长度。 |
| notifyOnStopDecision | false | 已废弃:每轮只发一条结论块,这个开关不再起作用(保留字段以免旧配置报错)。 |
| showVerdictNotice | true | 把最近一次判定结论写进会话区(落在消息下方,带底色的消息块;细节按 Ctrl+O 展开)。 |
| judgeMaxTokens | 2000 | 单次判定调用的输出 token 上限,同时会被收敛到模型自身的输出上限。 |
| continueMessageTemplate | "" | 覆盖内置催促文案(作为 system 指令注入),支持 {reason} 占位。 |
| forcedDecision | "auto" | 受控实验开关:auto(正常判定)、continue(强制判定为提前停止)、stop(强制判定为可停止)。 |
未知字段与非法值会明确报错,不会被静默忽略。
怎么知道它到底有没有触发
每轮结束后,会话区里(消息下方)会出现一条带底色的 [auto-goal] 消息块,内容是判定结论(由 showVerdictNotice 控制,默认开):
| 结论行 | 颜色 | 含义 |
| --- | --- | --- |
| ⚖️ 判定可停止 · 置信度 92% | 绿 | 判定为正常结束,没有干预;理由收在展开里(Ctrl+O,全屏模式下直接点这条提示)。 |
| ⚖️ 判定该继续 · 已催促 1/2 | 黄 | 判定为提前停止,已自动注入 system 催促(1/2 = 已催 1 次,上限 2 次)。 |
| ⚖️ 催促次数用尽(2/2)· 不再干预 | 灰 | 本轮干预次数用尽,不再干预。 |
| ⚖️ 已打断 · 未判定 | 灰 | 你按 Esc 打断了这一轮,判定主动让路。 |
| ⚖️ 本轮未正常结束 · 未判定 | 灰 | 这一轮以失败或残缺结束(不是 agent 自己停下)。 |
| ⚖️ 判定失败 | 红 | 判定调用失败。 |
正文只占一行;判定理由、已注入的催促文本、失败原因、结束原因等细节收在展开里:按 Ctrl+O 展开工具输出时会一起展开,全屏模式下也可以直接点这条提示块切换展开,平时不占地方。
判定结论不进 LLM 上下文,也不会写到页脚状态栏;它是本包写进会话的一条本地条目,重新打开会话时仍会照原样显示。
什么轮次会被判定
只判定「正常跑完」的轮次:最后一条 assistant 消息的结束原因是 stop(agent 自己结束本轮)或 length(输出被长度上限截断)。
用户按 Esc 打断时,结束原因是 aborted 或 error、内容为空——这是你的决定,不是 agent 的停止决定。以前这种轮次也会被当成「提前停止」去催,结果是刚按完 Esc 就被自动复活,看起来像 Esc 失效;现在这类轮次直接不判定,只写一行「已打断 · 未判定」。
如果你看到的是旧行为(按 Esc 后又被催),先确认当前会话的进程开在修复之后:本包在运行中的进程里不会热更新,需要
/reload或重开会话。
两件事同时成立时才算「这一轮没有判定」:页脚还停在上一轮的结论,且没有任何新提示。常见原因是当前不是 tui/rpc 模式、你已经在打字,或上面这两种不判定的轮次。
同一轮的结论只发一条消息块(正文一行 + Ctrl+O 展开的细节),不再另外弹提示;notifyOnStopDecision 已废弃(保留字段以免旧配置报错,但不再起作用)。颜色只在 TUI 下添加,其他模式不会出现 ANSI 乱码。
判定调用为什么不会“空响应”
判定只需要一个 JSON 结论,但推理型模型会先把输出预算花在思考上。历史上 judgeMaxTokens 固定为 400,一旦思考把预算吃光,响应里就只剩思考块、没有任何文本,用户看到的是含糊的「无法解析的响应:(空响应)」。现在:
- 判定调用固定使用最低思考强度(Pi 会收敛到模型支持的最低档,不支持关闭思考的模型也不报错);
- 输出上限默认 2000,并可用
judgeMaxTokens调整; - 若仍然被截断且没有文本,自动用翻倍预算重试一次;
- 仍失败时,错误文案带上
stopReason与内容块摘要(如结束原因=length,内容块=thinking:400),不再只说「空响应」。
命令
/config:auto-goal— 打开 TUI 配置面板(别名:/auto-goal、/pi-auto-goal-config);面板是设置列表:左边字段名、右边当前值,选中项下面给说明,回车改一项并立即存盘生效,包含启用判定、判定模型、干预上限、置信度阈值、判定输出上限、判定结论写入会话区。字段的完整清单用status看。/config:auto-goal enable|disable|status|reset— 非交互式写法;status显示生效配置与本会话已干预次数。配置命令改完立即生效。/config:auto-goal model看当前判定模型;/config:auto-goal model provider/modelId指定判定模型;/config:auto-goal model default改回复用当前会话模型。
开发
cd packages/pi-auto-goal
npm run typecheck
npm test
npm run check测试是确定性的:判定调用可以注入替身,不需要 API key 或真实模型。
