雨天小六

读懂 Codex(2.7):Op 协议的类别、所有者和分派入口

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

#Codex#Agent Runtime#软件架构#Protocol#Op

Codex 的上层 Surface 不直接调用“运行模型”“执行命令”之类的 Session 内部函数,而是提交 Op。但 Op 不是 RPC method 的机械镜像,也不是所有动作都会启动一个新 Turn。它更像 Core 的命令代数:把 用户输入、审批回答、Thread 管理、Realtime 和生命周期控制放在一组可排序的内部请求中。

理解 Op 的关键,是同时看 Submission 信封和 submission_loop 分派,而不是只读 enum 名称。

Submission 承担关联,Op 承担意图

Submission 包含:

字段作用
id关联该请求触发的 Event;Turn-start 请求也会被 App Server 投影成公开 Turn ID
opCore 要执行的操作
client_user_message_idUserInput 的客户端消息身份
trace跨 Surface 传播的 W3C trace
parent_turn_id多 Agent/派生调用中的父 Turn 关联

同一个 Op 值放进不同 Submission,可以拥有不同的追踪与父子关系。反过来,不能把 Submission ID 当作 所有内部工具 Call 的 ID;工具、审批和 ResponseItem 有各自标识。

当前 Op 可以按所有者分成五组

Op 按 Turn 创建、在途回答、Thread 状态、生命周期控制和 Realtime 五类划分
图 2.7-1:分类依据不是 enum 排列,而是操作影响的状态所有者与是否建立模型 Turn。

Turn 创建或模型工作

  • UserInput:普通用户输入,可能启动 RegularTask,也可能 steer 当前 Turn;
  • InterAgentCommunication:把 Agent 间消息按正常 submission 生命周期记录,必要时触发 Turn;
  • Compact:启动 CompactTask;
  • Review:启动 Review Task;
  • RunUserShellCommand:执行用户 !cmd,用 Turn 事件报告生命周期。

回答一个在途等待点

  • ExecApprovalPatchApproval
  • UserInputAnswerRequestPermissionsResponse
  • DynamicToolResponse
  • ResolveElicitation
  • ApproveGuardianDeniedAction

它们通常根据 request/call ID 唤醒 Store 中的等待者,而不是发起第二次模型请求。若等待点已不存在, handler 要么忽略并记录,要么发错误,不能把迟到回答套到另一个 Call。

Thread 级状态操作

  • ThreadSettings:只更新持久 Thread settings,不启动 Turn;
  • ThreadRollback:删除内存/持久历史中的最后 N 个 user turn;明确不撤销工作区文件;
  • SetThreadMemoryMode:修改未来 memory generation eligibility;
  • RefreshMcpServersReloadUserConfig:刷新 Runtime 投影。

控制与关闭

  • Interrupt:中止当前任务,但保留后台终端;
  • CleanBackgroundTerminals:单独终止长时间运行的后台 shell;
  • Shutdown:关闭整个 Session。

把 Interrupt 和 CleanBackgroundTerminals 分开,是为了让用户中止一次模型 Turn 时,不误杀明确希望保留 的 dev server。

Realtime 会话

Start、Audio、Text、Speech、Close、ListVoices 共享 Thread 的 submission 排序入口,但交给 Realtime Conversation Manager 处理。它们的事件族也独立于普通 AgentMessage。

non_exhaustive 是演进合同

Op 标记为 non_exhaustive。外部 crate 的 match 必须保留兜底,Core submission loop 也有忽略未知 Op 的分支。这允许协议增加 variant,而不要求旧消费者立即理解所有新动作。

代价是“未知操作”不一定自动生成错误事件;做协议桥接的 Surface 应在自己的版本边界先验证 method 与 参数,不能依赖 Core 兜底替它报告所有兼容问题。

submission_loop 是单入口,不是单任务执行器

Loop 顺序从 Receiver 取 Submission,再按 Op 调 handler。这个顺序保证 ThreadSettings 与紧随其后的 UserInput 能按调用顺序进入分派。但 handler 可能 spawn 后台 Task,所以“分派顺序”不等于“所有业务 工作直到完成都串行”。活动 Turn 约束由 Session task control 维护。

Submission loop 顺序接收请求并将不同 Op 分派到 Turn、等待点、Thread 状态、Realtime 或关闭处理器
图 2.7-2:队列提供入口排序,handler 决定是同步更新、唤醒等待者还是启动后台 Task。
async def submission_loop(session, queue):
    saw_shutdown = False
    async for submission in queue:
        with trace_span(submission):
            should_exit = await dispatch(session, submission)
        if should_exit:
            saw_shutdown = True
            break

    if not saw_shutdown:
        await teardown_after_all_senders_closed(session)

Channel 关闭也是终止路径。即使没有显式 Shutdown,loop 仍关闭 Runtime、发 session-end lifecycle,并 shutdown LiveThread persistence,避免“Sender 都没了,资源却永远活着”。

UserInput 的真实语义是 steer-or-start

UserInput 载荷不仅有文本/图片等 items,还包含:

  • final output JSON Schema;
  • Responses API client metadata;
  • 按 opaque source key 组织的 additional context;
  • 必须先应用的 ThreadSettingsOverrides。

Handler 先更新 Thread settings 与 schema,创建 TurnContext,然后尝试把输入 steer 进活动 Turn。只有 返回 NoActiveTurn 时才把输入转成模型 Item 并 spawn RegularTask。活动 Turn 若是 Review/Compact, 则返回 ActiveTurnNotSteerable,而不是悄悄新开并发 Turn。

async def handle_user_input(session, submission):
    op = expect_user_input(submission.op)
    await session.apply_thread_settings(op.thread_settings)
    turn = await session.new_turn_context(output_schema=op.schema)

    result = await session.steer_input(
        items=op.items,
        expected_turn_id=None,
        metadata=op.responses_metadata,
    )
    if result.ok:
        return
    if result.error == NoActiveTurn:
        model_input = build_regular_task_input(op, submission)
        await session.spawn_task(RegularTask(model_input), turn)
        return
    await session.emit_error(submission.id, result.error)

因此,客户端连续发送两个 UserInput 时,第二个可能成为同 Turn steering,而非稳定地创建第二个 Turn。 App Server 的 turn/start/turn/steer 会进一步施加更明确的公开 API 语义,见 2.13。

ThreadSettings 为什么也走同一队列

如果设置更新绕过 Submission Queue,set model=A 和紧接的 UserInput 可能因任务调度先后而让新 Turn 仍使用旧模型。Op::ThreadSettings 明确注释为通过同一队列维持 caller order;应用成功发 ThreadSettingsApplied,约束失败发 Error。

这是一种命令排序,而不是大锁:真正的 SessionConfiguration 更新仍在 handler 内持有必要的状态锁。

回答型 Op 的所有权在等待 Store

执行审批、patch 审批、request_user_input、request_permissions 和 dynamic tool 都先把等待句柄以 ID 注册到 Session service。回答型 Op 查找并完成对应 waiter。核心不变式是“一次请求只被正确 ID 的一次 回答消费”。

审批 Op 带原始请求 ID,而 Submission 自己还有新的 ID。这两个 ID 不能交换:前者定位等待点,后者只 关联“这次回答命令”的处理事件。

Compact、Review、Rollback 不是 UserInput 参数

它们有独立 Op,是因为状态机与失败条件不同:

  • Compact 运行专用 Task,限制 steering,成功后替换/追加摘要历史;
  • Review 临时切入 Review mode,可能使用不同 instructions/model context;
  • Rollback 要求 num_turns > 0、当前无活动 Turn、且存在 persistence,然后重放截断历史;
  • UserShell 不请求模型,直接启动 shell Task,但仍用 Exec 与 Turn 事件表达进度。

Rollback 的注释特别声明“不尝试恢复本地文件”。历史回退和工作区回退是两类操作,客户端需要自行 提供 git/undo 功能。

Shutdown 是唯一让正常 loop 退出的 Op

大多数分支返回 false,Shutdown handler 返回是否应该退出。Shutdown 过程中会中止任务、关闭后台资源、 发送 ShutdownComplete 等收尾。Loop 退出后,共享 termination future 才 resolve。

未知 Op 的兜底返回 false,防止旧 Runtime 因一个新 variant 直接终止整个 Thread。

失败矩阵

Op/条件处理是否新建模型 Turn
UserInput,无活动 Turnspawn RegularTask
UserInput,有可 steer Turn注入当前 Turn
UserInput,活动 Review/CompactError: not steerable
ThreadSettings 违反受管约束Error
审批回答 ID 不存在/已消费忽略或错误记录
Rollback 为 0/Turn 活动/无持久历史ThreadRollbackFailed
Interrupt中止 task,保留后台 terminal
CleanBackgroundTerminals只清后台 terminal
Shutdownteardown 并退出 loop
未知未来 Op旧 loop 忽略

测试焦点

async def test_settings_and_input_preserve_order(thread):
    await thread.submit(Op.ThreadSettings(model="new-model"))
    sid = await thread.submit(Op.UserInput(items=[text("hello")]))
    started = await wait_event(sid, "turn_started")
    assert started.model == "new-model"


async def test_second_input_steers_active_turn(thread):
    first = await start_blocked_turn(thread)
    await thread.submit(Op.UserInput(items=[text("also inspect tests")]))
    assert first.received_steering_input


async def test_channel_close_still_tears_down(session):
    drop_all_submission_senders(session)
    await session.loop_termination
    assert session.persistence.is_shutdown

Op 协议真正提供的是“单 Thread 内可排序的意图入口”。至于该意图是否创建 Turn、唤醒工具、修改配置 或终止 Runtime,必须由具体 handler 和它拥有的状态来解释。

评论


← 返回文章列表