雨天小六

读懂 Codex(8.24):不完整 Turn 和 Prompt-only Call/Output 修复

· 更新于 2026-08-03 · 专栏:读懂 Codex

#Codex#Agent Runtime#持久化#Rollout#软件架构

崩溃可能留下只有 Call 或只有 Output 的尾部;Codex 在生成模型 Prompt 的副本上补 aborted Output、删除孤儿 Output并剥离不支持媒体,但不把这些推断反写成真实 Rollout。

本节解决的不是“把对象存一下”这种抽象问题,而是把问题限定在:研究 ContextManager::for_prompt 的规范化顺序与 stable synthetic ID。输入:恢复后的原始 Context History 与目标模型 input modalities;状态所有者:ContextManager clone 的 Prompt view;成功结果:满足模型协议配对且能力兼容的 ResponseItem 数组。先把这些边界钉住,后面的顺序、失败和恢复才不会混成一句“持久化失败后重试”。

状态边界

问题本节答案
输入恢复后的原始 Context History 与目标模型 input modalities
状态所有者ContextManager clone 的 Prompt view
成功产物满足模型协议配对且能力兼容的 ResponseItem 数组
研究范围研究 ContextManager::for_prompt 的规范化顺序与 stable synthetic ID

正常路径:先看顺序点

不完整 Turn 和 Prompt-only Call/Output 修复的正常路径时序图,展示clone ContextManager snapshot、为缺失 Output 的 Call 生成占位、删除无 Call 的客户端 Output、按模型能力替换 image/audio、仅把规范化副本交给模型
图 8.24-1:clone ContextManager snapshot → 为缺失 Output 的 Call 生成占位 → 删除无 Call 的客户端 Output → 按模型能力替换 image/audio → 仅把规范化副本交给模型。这张图标出本节的实际顺序点与状态所有者。

这条路径可以压缩成五步:

  1. clone ContextManager snapshot。
  2. 为缺失 Output 的 Call 生成占位。
  3. 删除无 Call 的客户端 Output。
  4. 按模型能力替换 image/audio。
  5. 仅把规范化副本交给模型。

图中的箭头不是“可能调用”的依赖图,而是源码中决定可见性和所有权转移的先后关系。前一步没有确认时,后一步不能替它作出更强的成功承诺。

源码机制拆解

先补缺失 Output 再删孤儿

第一遍收集已有 output IDs;Function/Custom/ToolSearch/LocalShell Call 缺结果时,在 Call 后插入合成结果。第二遍收集 Call IDs,删除没有匹配调用的 Output。顺序保证新插入项不会被误删。

不同调用类型有兼容输出形状

Function 与 LocalShell 使用 FunctionCallOutput(“aborted”);Custom 使用 CustomToolCallOutput;客户端 ToolSearch 生成 completed/empty tools 的 ToolSearchOutput。server-executed search output 可以没有本地 Call,策略保留。

合成 ID 必须跨重试稳定

源 Call 有 item id 时,用固定 UUID v5 namespace 和 prefix:item_id 派生 ResponseItemId。重复 for_prompt/Resume 不会制造不同 ID,从而保护 prompt cache key;旧 Call 无 id 则保持 None 兼容。

媒体能力也是 Prompt-only 变换

目标模型不支持 Image/Audio 时,消息和工具 Output 中相应内容替换为文字占位;ImageGeneration result 清空。原始 Context History/Rollout 不被永久降级,之后切回多模态模型仍能读取原记录。

修复协议,不伪造执行事实

“aborted” 表示该调用没有可用结果,使下一请求的 call/output 结构合法;它不意味着工具真的执行过一次,也不应被 append 到 canonical Rollout。

Python 风格伪代码

下面的伪代码只保留设计职责、状态和失败顺序;它不逐行翻译 Rust,也不借 Python 语法虚构源码中不存在的事务:

def history_for_prompt(raw_history, modalities):
    items = deep_copy(raw_history.items)

    output_ids = collect_output_call_ids(items)
    insertions = []
    for index, item in enumerate(items):
        if is_call(item) and item.call_id not in output_ids:
            insertions.append((index + 1, aborted_output_for(item)))
    for index, output in reversed(insertions):
        items.insert(index, output)  # reverse order avoids shifting indexes

    call_ids = collect_compatible_call_ids(items)
    items = [item for item in items if not is_orphan_client_output(item, call_ids)]
    replace_unsupported_images_with_text(items, modalities)
    replace_unsupported_audio_with_text(items, modalities)
    return items

def aborted_output_for(call):
    stable_id = uuid5(FIXED_NAMESPACE, f'{kind_prefix(call)}:{call.item_id}')         if call.item_id else None
    return compatible_output(call, text='aborted', item_id=stable_id)

阅读时要特别看三处:哪个对象拥有可变状态,哪一个 await 是可观察屏障,以及失败后保留的是已提交前缀、未提交后缀,还是完全独立的外部副作用。

失败、取消与恢复

不完整 Turn 和 Prompt-only Call/Output 修复的失败路径图,区分FunctionCall 尾部无 Output、Output 没有匹配 Call、重复规范化产生随机 ID、文本模型收到旧图片
图 8.24-2:同一操作在不同故障点留下的状态不同,恢复责任必须回到拥有该状态的组件。
故障点已留下的状态可观察结果恢复责任
FunctionCall 尾部无 Output模型协议不完整插入 aborted FunctionCallOutput仅存在于 prompt clone
Output 没有匹配 Call可能来自截断/旧 bug客户端 output 被删除server search 特例保留
重复规范化产生随机 IDPrompt cache key 每次变化缓存命中下降固定 namespace 的 UUIDv5
文本模型收到旧图片provider 不支持 modality请求失败或行为未定义用稳定占位文本替换副本

这里没有统一的“回滚一切”。内存状态、日志行、SQLite 投影、父子拓扑和工具造成的文件/网络变化分别有自己的提交点。恢复代码只能根据已经存在的权威证据继续,不能用较弱的投影替较强的事实背书。

必须保持的不变量

  • 最终 prompt 中每个客户端 Call 有兼容 Output
  • 每个客户端 Output 有兼容 Call
  • 规范化不修改 canonical Rollout
  • 同一带 item ID 的缺失 Output 每次派生相同 synthetic ID

这些不变量比“最终能 Resume”更严格:正常路径要成立,Writer 竞争、任务取消、坏尾行、投影落后和旧格式兼容时也必须成立。

设计取舍

Prompt-only 修复让不完整历史继续可用,却承认它是兼容性推断;把推断限制在请求副本能保住审计真实性。

源码可以直接证明字段、分支、调用顺序和测试期望;“为什么这样设计”的表述是基于这些事实作出的工程归纳,不把它包装成未公开的产品承诺。

Mini Codex 复刻

normalize(copy(history)),绝不 normalize-and-save;为四种 call family 建配对表,并用固定 namespace 派生 synthetic output ID。

复刻时先验证协议不变量,再补性能优化。一个能在故障注入下说明“留下了什么”的小实现,比一个只在正常路径调用 save() 的演示更接近真实 Runtime。

源码导航

相邻测试也很重要:

本节结论

崩溃可能留下只有 Call 或只有 Output 的尾部;Codex 在生成模型 Prompt 的副本上补 aborted Output、删除孤儿 Output并剥离不支持媒体,但不把这些推断反写成真实 Rollout。

阅读导航

上一节:8.23 · 下一节:8.25

评论


← 返回文章列表