雨天小六

读懂 Codex(9.1):Python 领域协议:Op、Event 与 ResponseItem

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

#Codex#Agent Runtime#Python#软件架构

具体问题与边界

怎样把调用方意图、运行时通知和模型历史拆成三种不会互相污染的数据层?

输入是 UserInput、Interrupt、Shutdown;状态所有者是 Session;输出既包括瞬时 AgentEvent,也包括可进入 History/Rollout 的完整 ResponseItem。 本节不是把 Rust 改写成 Python;它从锁定提交的字段、调用顺序和测试行为提炼实现合同,再检查 Mini Codex 是否以 Python 的并发原语保持同一条不变量。

协议、类型与状态所有权

对象创建/所有者生命周期与作用域是否持久化
Operation / SubmissionAgentThread/Session.submit排队前创建;Submission ID 只标识一次提交否,Turn 相关语义另行记录
AgentEventSession._emitEvent Queue 中短暂存在
ResponseItem模型累积器或 ToolRouterHistoryManager 持有
turn_id / call_idTurn 与模型工具协议跨 Step;call_id 配对 Call/Result随 Item 持久化

正常路径

Python 领域协议:Op、Event 与 ResponseItem正常路径图
图 9.1-1:怎样把调用方意图、运行时通知和模型历史拆成三种不会互相污染的数据层?
  1. 调用方只提交领域 Operation,不直接调用 _execute_turn,因此输入协议与内部任务结构解耦。
  2. Submission.create() 在入队前生成 submission ID;事件外层的 AgentEvent 沿用这个 ID,客户端可区分并发提交或错误归属。
  3. UserInputOp 启动 Turn,InterruptOp 只触发活动取消 Token,ShutdownOp 进入有序关闭分支;三者不会伪装成聊天消息。
  4. 模型的 Message/ToolCall 与工具的 ToolResult 都实现为完整 ResponseItem。文本 Delta 只形成 TextDelta 事件,不能直接写进规范历史。
  5. turn_id 负责把完整 Item 归到 Turn,call_id 负责把工具请求与结果配对;两种 ID 的作用域不可交换。

机制调用链

1. 调用方只提交领域 Operation,不直接调用 `_execute_turn`,因此输入协议与内部任务结构解耦
→ 2. `Submission.create()` 在入队前生成 submission ID;事件外层的 `AgentEvent` 沿用这个 ID,客户端可区分并发提交或错误归属
→ 3. `UserInputOp` 启动 Turn,`InterruptOp` 只触发活动取消 Token,`ShutdownOp` 进入有序关闭分支;三者不会伪装成聊天消息
→ 4. 模型的 Message/ToolCall 与工具的 ToolResult 都实现为完整 ResponseItem
→ 5. `turn_id` 负责把完整 Item 归到 Turn,`call_id` 负责把工具请求与结果配对;两种 ID 的作用域不可交换

这里最重要的不是类名,而是控制权何时转移:创建者决定 ID 和初值,状态所有者决定何时修改,跨越 await 的调用必须明确取消、失败和可见性边界。任何绕过这些边界的“便捷调用”都会让恢复或并发测试失去确定答案。

Python 风格伪代码

type Operation = UserInput | Interrupt | Shutdown
type ResponseItem = Message | ToolCall | ToolResult
type EventPayload = TurnStarted | TextDelta | ItemCompleted | TurnCompleted | TurnAborted | RuntimeError

async def submit(operation):
    submission = Submission(new_submission_id(), operation)
    if isinstance(operation, UserInput):
        idle.clear()                    # close stale-idle race before enqueue
    await input_queue.put(submission)
    return submission.id

async def emit(submission_id, payload):
    await event_queue.put(AgentEvent(submission_id, payload))

def persistable(value):
    return isinstance(value, ResponseItem)  # never TextDelta or Queue/Task

伪代码只保留设计职责;Mini Codex 的可运行版本见下方实现导航。它没有伪造官方源码中不存在的 Python API,也没有把路径策略写成 OS 沙箱。

失败、取消与恢复

Python 领域协议:Op、Event 与 ResponseItem失败路径图
图 9.1-2:失败必须回到实际状态所有者,不能用一条通用异常吞掉协议差异。
故障或错误设计会留下什么Mini Codex 的处理
把 Interrupt 编成 User Message模型可能回答“已中断”,但活动 I/O 不会停止Interrupt 是控制面 Operation,只修改取消状态
把 Delta 直接写 History断流会留下半截 Assistant Message等待完整 Item 和 ModelCompleted
混用 submission_id 与 turn_id错误事件和历史归属错位事件外层用 submission_id,语义项用 turn_id
ToolCall 没有同 call_id Result后续 Prompt 协议不合法Router 或 Resume repair 保证配对

必须保持的不变量

瞬时事件不能冒充规范历史;控制操作不能冒充模型消息;每个已接收 ToolCall 必须能在模型视图中找到同 call_id 的结果。

这条不变量同时约束正常路径、异常路径和恢复路径。只在 happy path 里得到正确输出,不足以证明该模块边界成立。

设计取舍

显式领域类型增加了转换代码,却把 UI 低延迟、模型协议和耐久恢复的失败边界拆开。Mini Codex 合并了官方协议中的大量变体,只证明分层方法,不声称枚举覆盖完整。

源码能够直接证明类型、分支、调用顺序和测试期望;关于工程动机的解释是基于这些事实的设计归纳,不冒充未公开承诺。

测试与复现实验

cd examples/mini-codex
uv run pytest -q -k 'test_single_sample_turn_emits_one_terminal_event' -k 'test_tool_result_is_paired_before_follow_up_sample'
uv run mypy src

本节对应的关键断言:

  • test_single_sample_turn_emits_one_terminal_event
  • test_tool_result_is_paired_before_follow_up_sample

全量离线基线为 27 项通过;真实 Responses 测试需要显式环境变量,默认跳过。单项测试名用于定位,不代替对断言内容的解释。

官方源码导航

Mini Codex 对照

  • src/mini_codex/protocol.py:Operation、Submission、ResponseItem、AgentEvent
  • src/mini_codex/runtime/thread.py:调用方句柄
  • src/mini_codex/runtime/session.py:Operation 分派和 Event 发射

Mini Codex 保留本节的状态所有权、顺序和失败反馈;省略的产品能力会在边界处明确列出,不能由测试通过外推为生产等价。

本节边界

已经证明:瞬时事件不能冒充规范历史;控制操作不能冒充模型消息;每个已接收 ToolCall 必须能在模型视图中找到同 call_id 的结果。

尚未覆盖的生产问题由后续单元继续展开;公开版保留官方源码链接和可运行测试合同。

阅读导航

上一节:第九章总览 · 下一节:9.2

评论


← 返回文章列表