具体问题与边界
怎样把调用方意图、运行时通知和模型历史拆成三种不会互相污染的数据层?
输入是 UserInput、Interrupt、Shutdown;状态所有者是 Session;输出既包括瞬时 AgentEvent,也包括可进入 History/Rollout 的完整 ResponseItem。 本节不是把 Rust 改写成 Python;它从锁定提交的字段、调用顺序和测试行为提炼实现合同,再检查 Mini Codex 是否以 Python 的并发原语保持同一条不变量。
协议、类型与状态所有权
| 对象 | 创建/所有者 | 生命周期与作用域 | 是否持久化 |
|---|---|---|---|
| Operation / Submission | AgentThread/Session.submit | 排队前创建;Submission ID 只标识一次提交 | 否,Turn 相关语义另行记录 |
| AgentEvent | Session._emit | Event Queue 中短暂存在 | 否 |
| ResponseItem | 模型累积器或 ToolRouter | HistoryManager 持有 | 是 |
| turn_id / call_id | Turn 与模型工具协议 | 跨 Step;call_id 配对 Call/Result | 随 Item 持久化 |
正常路径
- 调用方只提交领域 Operation,不直接调用
_execute_turn,因此输入协议与内部任务结构解耦。 Submission.create()在入队前生成 submission ID;事件外层的AgentEvent沿用这个 ID,客户端可区分并发提交或错误归属。UserInputOp启动 Turn,InterruptOp只触发活动取消 Token,ShutdownOp进入有序关闭分支;三者不会伪装成聊天消息。- 模型的 Message/ToolCall 与工具的 ToolResult 都实现为完整 ResponseItem。文本 Delta 只形成
TextDelta事件,不能直接写进规范历史。 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 沙箱。
失败、取消与恢复
| 故障或错误设计 | 会留下什么 | 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_eventtest_tool_result_is_paired_before_follow_up_sample
全量离线基线为 27 项通过;真实 Responses 测试需要显式环境变量,默认跳过。单项测试名用于定位,不代替对断言内容的解释。
官方源码导航
- codex-rs/protocol/src/protocol.rs:
Op与EventMsg的生产协议枚举 - codex-rs/core/src/codex_thread.rs:
CodexThread::submit、submit_with_id、next_event双向窄腰
Mini Codex 对照
src/mini_codex/protocol.py:Operation、Submission、ResponseItem、AgentEventsrc/mini_codex/runtime/thread.py:调用方句柄src/mini_codex/runtime/session.py:Operation 分派和 Event 发射
Mini Codex 保留本节的状态所有权、顺序和失败反馈;省略的产品能力会在边界处明确列出,不能由测试通过外推为生产等价。
本节边界
已经证明:瞬时事件不能冒充规范历史;控制操作不能冒充模型消息;每个已接收 ToolCall 必须能在模型视图中找到同 call_id 的结果。
尚未覆盖的生产问题由后续单元继续展开;公开版保留官方源码链接和可运行测试合同。
评论
登录后即可评论