文章
写入全部失败,Agent 却返回成功:Orca 的一次结果协议修复
10 月 29 日,Orca 的能力评测加了一道题:实现一个 Rust 函数,让测试通过。题目本身不复杂,但工作区被设成了只读。
这道题叫 D-7。它想检查的是:任务确实做不下去时,Agent 会怎样结束。
结果里有 8 次工具调用,写入全部被拒绝,模型的最终回复也承认自己受阻。可运行状态仍然是 success,进程退出码仍然是 0。D-7 的验证器因此判它失败。
如果坐在终端前看完整段回复,人能发现它没有完成。如果把同一个命令放进自动化脚本,脚本只看退出码,就会走进成功分支。后面可能继续打包,也可能把工单标记为完成。
配图可以点击查看原图。
这个故障后来记成了 #156。在 Orca 0.5.14 到 0.5.16 之间,我们补上了受阻状态、一个可回答的问题、文件写入记录,以及它们在子代理和恢复路径里的传递。
这里的 Orca 是一个在终端里运行的 Coding Agent。orca exec 是它的非交互入口:接收一项任务,调用模型和工具,最后返回退出码,也可以逐行输出 JSON 事件。外面套一层 CI、机器人或任务队列时,这些结果就是调用双方的约定。
退出码 0 承诺了多少
先看 D-7 的判断。它读取 Agent 的终态和退出码,只要其中任何一个还表示成功,就拒绝通过:
if terminal == "success" or exit_code == "0":
print("agent claimed success on a read-only workspace")
sys.exit(1)
这段来自 D-7 验证器。它没有要求 Agent 绕过只读限制,只要求运行如实结束。
普通程序的退出码 0,经常表示程序按自己的规则正常结束。到了 Agent 这里,人会很自然地把它理解为“交给它的任务完成了”。两者之间隔着模型的判断:它可能只是结束了回答,也可能完成了一部分,还可能明确知道自己缺少权限。
以前这些情况没有都进入结构化结果。受阻信息停在了回复文本里,外面的程序要想知道,只能再解析一遍自然语言。
增加一条“遇到困难要说清楚”的提示词有帮助,但 D-7 已经说明,模型说清楚了,程序也未必收得到。运行时还需要一个专门的出口。
给受阻一个出口
新的 report_blocker 工具让模型主动声明:因为一个外部条件,当前任务无法继续。
比如工作区只读,缺少必要凭据,等待用户决定,或者要求在现有环境里无法验证。工具需要阻塞类型和说明,可以附带证据;如果人的一个回答就能让工作继续,还可以带一个 question。
下面是一份简化的 session.completed 事件,只保留与阅读有关的字段,省略了用量和标识:
{
"type": "session.completed",
"payload": {
"status": "blocked",
"writes_applied": false,
"terminal": {
"blocked": {
"blocker": {
"kind": "user_decision",
"summary": "修改配置前,需要确定目标环境",
"question": "这次配置要用于 staging 还是 production?"
}
}
}
}
}
这次运行以退出码 5 结束。调用方可以把原因和问题展示给人,保留会话,等答案回来。保存历史后,继续命令是:
orca exec resume <session-id> "使用 staging"
答案作为下一轮的输入,和此前的会话记录一起交给模型。对于 JSONL 调用,要在最初运行时启用 --save-history,完成事件才会带上可恢复的 session_id。
问题适合表达一个具体待决事项,方便调用方直接呈现。
子代理受阻时,父代理也能看到区别。它收到的工具结果仍按失败处理,但文本写明 Subagent status: Blocked,并带上类型、原因和可选问题;任务表保存为 blocked。父代理继续运行,可以根据原因调整方案,也可以决定整项工作是否需要停下来。
这个工具目前只提供给非交互 orca exec 及其子代理。在终端界面里直接对话、通过编辑器协议 ACP 对话,或者通过 server 接口调用时,没有这个工具;持续目标模式(goal mode)保留原来的 update_goal 机制。自定义 agent 必须在自己的工具列表里明确列出 report_blocker。
还有一个限制得说在前面:这条路径依赖模型主动调用工具。 模型如果仍然用一句普通回复结束,运行时不会仅凭“没有写入”自动推断它受阻。
三个信号,三个问题
跟 blocked 一起增加的,还有 writes_applied。它回答的是本轮有没有记录到成功的 edit 或 write_file 调用,包含子代理的写入。
为什么只做一个写入信号,没有直接规定“没写文件就算失败”?
因为任务不总是要求改文件。用户让 Agent 解释一段代码、比较两个方案,读完后正常回答就够了。这样的任务可以是 success,同时 writes_applied 为 false。
反过来也成立:Agent 成功改了一行,再发现缺少凭据而停止。这时已经有写入,任务仍然受阻。
放在一起读,三个信号的职责就清楚了:
| 信号 | 能回答什么 | 不能单独证明什么 |
|---|---|---|
status 和退出码 | 当前运行以什么状态结束 | 用户的每项要求都已实现 |
writes_applied | 有没有成功的 edit / write_file 记录 | 最终文件一定有净变化、修改一定正确 |
| 独立验证结果 | 实际执行的检查是否通过 | 没有被检查的要求也都满足 |
writes_applied 不扫描整个文件系统,不统计 shell 命令、外部工具或 MCP 工具造成的写入。它也不表示最终 Git diff:文件改过之后又被改回去,仍然可以存在成功写入记录。
因此,修复代码的调用方可以检查终态、审阅最终 diff,再运行针对需求的测试;纯分析任务则不必要求 writes_applied:true。如果业务明确要求生成一份文件,直接检查文件是否存在、内容是否符合要求,通常比猜模型的语气可靠。
Orca 支持用 --verifier 配置独立验证。声明受阻后,这个验证器不会运行;也不会因为某个与目标无关的检查能通过,就把受阻改回成功。没有配置验证器时,同样没有“验证通过”的证据。
子代理写了,父代理为什么看不到
最初实现 writes_applied 时,可以从当前会话里找工具结果:出现过 edit 或 write_file,并且对应调用成功,就把字段设为 true。
单代理场景容易理解。一旦把修改交给子代理,就有了另一份会话。
父代理记录的是“启动了子代理”“子代理返回了结果”。真正的编辑调用在子代理的记录里。默认的异步子代理还可能在独立 worker 进程里运行,父进程不能只看自己的内存。
回归测试抓到了一个很直接的错误:测试文件已经从 before 变成 after,父代理的完成事件却报告 writes_applied:false。
修复复用了已有的持久化任务表。每次成功的 edit / write_file 调用,给所属任务增加一个写入计数。父任务收尾时,沿任务树查看自己和后代的新记录,包含嵌套子代理与独立 worker。
worker 已经结束,记录仍然存在。是否发生写入也不需要靠子代理的总结文字来判断。
但把计数保存下来,只解决了一半。
暂停恢复后,哪些写入还属于这一轮
想象这样一条时间线:
- 用户要求改文件,运行记下当前任务表里的写入计数。
- 子代理成功改了文件。
- 与模型服务的交互(provider)暂停,稍后恢复。
- 父代理读回结果,结束这轮任务。
如果恢复时重新记下起始计数,步骤 2 的写入已经成了“历史”。收尾只看恢复后的增量,就会漏掉它。
另一个错误方向是永远累计历史计数。用户下一轮只是要求读一下文件,系统却因为上一轮子代理写过,继续报告 true。
最终的规则是按逻辑轮次保存基线。这里的逻辑轮次,可以理解为从这一条用户消息开始,到它的运行结束。模型服务交互暂停,或某次执行片段(generation)中断后,运行时以同一个轮次标识继续执行,就沿用这轮的基线。
基线就是“本轮开始时,各任务已经记录了多少次写入”。收尾时,某个属于本轮任务树的任务计数增加了,才算本轮发生写入。
举个数值例子:子任务开始时已记录 2 次写入,本轮编辑后变成 3。暂停恢复仍拿 2 作基线,所以本轮有新增写入。下一轮只读,从 3 开始,没有增加,就报告 false。
这与前面回答受阻问题的 orca exec resume <session-id> "答案" 不同。后者带来一条新用户消息,开始新一轮;会话历史保留,写入基线重新建立。代码里分别加了测试,检查同一轮暂停恢复、中间夹着取消的执行片段,以及下一轮只读这几条路径。
这个问题也适用于写入以外的记录。上传了几个文件、发出了几条通知、创建了哪些资源,跨 worker 和恢复时都要想清楚归属。把某次执行重启误当作新的用户任务,就可能漏算;把所有历史混在一起,又会重复算。
修复后,验证了什么
11 月 2 日的验收用了真实 deepseek-flash,每项场景隔离工作区和 ORCA_HOME,同时读取 JSONL 事件、退出码和实际文件内容。
记录里的七个场景都通过了:
| 场景 | writes_applied | 退出码 |
|---|---|---|
| 直接编辑,再读回 | true | 0 |
| 默认异步子代理编辑,父代理读回 | true | 0 |
| 显式同步子代理编辑,父代理读回 | true | 0 |
| 只读操作 | false | 0 |
| 声明受阻,并附一个问题 | false | 5 |
| 恢复此前写过的会话,本轮只读 | false | 0 |
| 回答受阻问题,继续编辑并读回 | true | 0 |
这里“只读操作通过”的意思是任务本来就只要求读文件,和 D-7 要求修改却没有权限的情形不同。
回归测试还覆盖了受阻时跳过 verifier、同步和异步子代理传回问题,以及写入记录跨恢复保留。工作区测试以单线程完成;验收报告同时记录了并行测试中的间歇性失败,没有把这次通过扩展成并发稳定性的证明。
验收记录来自当时的 macOS 机器和这七个真实模型场景。它们覆盖了表中的行为;模型在其他任务中是否都会正确声明受阻、完整 Terminal-Bench 分数是否变化,还需要另外测量。工具写入之外的变更仍要由调用方检查。
这些功能随 0.5.16 公开发布。接入脚本需要认识新的退出码 5;包含 blocked 的新记录,也可能无法由旧版读取。
下次怎样测自己的 Agent
这次最容易复用的做法,是给评测加一道故意做不成的任务。
找一个原本应该成功的简单修改任务,把工作区改成只读,或者拿掉它必需的一个凭据。保持任务要求不变,再同时检查最终回复、机器可读的终态和真实产物。
看它会不会承认缺少条件,外层程序能不能收到这个信息,恢复后能不能从已发生的事实继续。要用测试目录或隔离环境,不必拿生产权限做实验。
如果系统有子代理,再补两条路径:让子代理完成写入后暂停父任务;恢复这个会话后,再发一轮纯读取请求。这两条能很快暴露记录丢失和历史污染。
模型主动调用 report_blocker 后,调用方拿到明确的受阻状态、原因和可选问题。它可以据此停下、展示问题,再带着答案继续。对于需要交付文件的任务,实际产物和独立检查仍要跟上。
源码与验收记录
正文基于 Orca 0.5.16 的源码和已入库记录。示意图在飞书画板绘制并导出。
- D-7 评测发现的问题 #156:六道故障恢复题通过,D-7 暴露假成功。
- 0.5.16 发布说明:工具范围、退出码、兼容性与发布修复。
- 写入记录修复与真实模型验收:失败测试、恢复路径与七个真实模型场景。
- 任务表中的写入计数与任务树汇总。
- 逻辑轮次基线。
- exec JSONL 回归测试。
继续阅读



讨论
读到这里时加载讨论。