雨天小六

读懂 Codex(9.3):Session 主循环与活动 Turn 所有权

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

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

具体问题与边界

Session 怎样一边运行多 Step Turn,一边继续响应 Interrupt,并保证只发一个终态?

Session 主循环只拥有 Operation 分派;独立活动 Task 拥有 Turn 执行;_run_owned_turn 独占 Completed/Aborted 终态。 本节不是把 Rust 改写成 Python;它从锁定提交的字段、调用顺序和测试行为提炼实现合同,再检查 Mini Codex 是否以 Python 的并发原语保持同一条不变量。

协议、类型与状态所有权

对象创建/所有者生命周期与作用域是否持久化
_runner_taskSession.start/close跨多个 Turn
_active_turn_taskSession 主循环创建,owned wrapper 清理一个活动 Turn
CancellationToken每个 Turn 新建主循环 cancel,各层检查
terminal_sent_run_owned_turn 栈帧只服务一个 Turn

正常路径

Session 主循环与活动 Turn 所有权正常路径图
图 9.3-1:Session 怎样一边运行多 Step Turn,一边继续响应 Interrupt,并保证只发一个终态?
  1. 主循环不能直接 await _execute_turn:那会在模型或进程等待期间停止读取 Interrupt。
  2. 收到 UserInput 且没有活动 Turn时,新建 CancellationToken,并把 _run_owned_turn 作为独立 Task。
  3. 第二个 UserInput 在活动 Turn 中得到 RuntimeErrorEvent。Mini Codex 不把它暗中排队,也不实现 Steer。
  4. Interrupt 只 cancel() 当前 Token;模型传输、工具和进程管理器在各自等待点观察同一信号。
  5. owned wrapper 把正常、TurnCancelled、普通异常和防御兜底四条路径统一成恰好一个终态,随后清理活动指针并 set idle。

机制调用链

1. 主循环不能直接 `await _execute_turn`:那会在模型或进程等待期间停止读取 Interrupt
→ 2. 收到 UserInput 且没有活动 Turn时,新建 CancellationToken,并把 `_run_owned_turn` 作为独立 Task
→ 3. 第二个 UserInput 在活动 Turn 中得到 RuntimeErrorEvent
→ 4. Interrupt 只 `cancel()` 当前 Token;模型传输、工具和进程管理器在各自等待点观察同一信号
→ 5. owned wrapper 把正常、TurnCancelled、普通异常和防御兜底四条路径统一成恰好一个终态,随后清理活动指针并 set idle

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

Python 风格伪代码

async def submission_loop():
    while True:
        sub = await input_queue.get()
        match sub.operation:
            case Shutdown():
                cancel_active()
                await join_active()
                return
            case Interrupt():
                if active_token: active_token.cancel()
                else: await emit_error(sub, "no active turn")
            case UserInput() as op:
                if active_task:
                    await emit_error(sub, "another turn is active")
                else:
                    token = CancellationToken()
                    active_task = create_task(run_owned(sub, op, token))

async def run_owned(sub, op, token):
    terminal = False
    try:
        await execute_turn(sub, op, token)
        terminal = True              # execute_turn emitted Completed
    except TurnCancelled as exc:
        await persist_aborted(exc)
        await emit_aborted(exc)
        terminal = True
    except Exception as exc:
        await emit_error_and_aborted(exc)
        terminal = True
    finally:
        if not terminal: await emit_defensive_aborted()
        clear_active_and_set_idle()

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

失败、取消与恢复

Session 主循环与活动 Turn 所有权失败路径图
图 9.3-2:失败必须回到实际状态所有者,不能用一条通用异常吞掉协议差异。
故障或错误设计会留下什么Mini Codex 的处理
直接 await 完整 TurnInterrupt 无法被主循环消费活动 Turn 使用独立 Task
多个位置各自发终态异常竞态产生 Completed + Aborted终态集中到 owned wrapper
取消只 cancel asyncio Task子进程/HTTP 可能没有清理机会共享协作式 Token 贯穿边界
异常后不清 active 指针后续输入永远被判定为 busyfinally 清理并 set idle

必须保持的不变量

一个 Session 同时至多有一个活动 Turn;一个 Turn 对客户端恰好产生一个 Completed 或 Aborted。

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

设计取舍

独立任务使中断可达,却引入 active 指针、idle 和关闭 join 的状态同步。Mini Codex 省略官方 Steer、Review 与 Realtime 等任务变体。

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

测试与复现实验

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

本节对应的关键断言:

  • test_interrupt_produces_one_aborted_terminal_event
  • test_single_sample_turn_emits_one_terminal_event

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

官方源码导航

Mini Codex 对照

  • src/mini_codex/runtime/session.py_run_run_owned_turn_execute_turn
  • src/mini_codex/runtime/cancellation.py:共享取消 Token

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

本节边界

已经证明:一个 Session 同时至多有一个活动 Turn;一个 Turn 对客户端恰好产生一个 Completed 或 Aborted。

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

阅读导航

上一节:9.2 · 下一节:9.4

评论


← 返回文章列表