雨天小六

读懂 Codex(9.10):Unified Exec 的持续进程和 stdin 续写

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

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

具体问题与边界

长进程为什么不能被一次 communicate 调用表达,session_id、增量输出和清理又怎样协作?

ProcessManager 是持续进程唯一所有者;exec_command 创建并首次 yield;write_stdin 按 session_id 写入或轮询;终态后从 Registry 移除。 本节以官方 Codex 锁定提交为事实基线,以 Mini Codex 的可执行断言检验 Python 设计;二者不是逐行翻译关系。

协议、类型与状态所有权

对象创建/所有者生命周期与作用域是否持久化
process session registryProcessManager进程运行期间
session_idProcessManager 单调分配直到 final pollToolResult 中暴露
stdout/stderr chunk buffersreader tasks两次 poll 之间
timeout/cancel watcherProcessManager进程生命周期
stdin pipe子进程/Managerclose_stdin 前

正常路径

Unified Exec 的持续进程和 stdin 续写正常路径图
图 9.10-1:长进程为什么不能被一次 communicate 调用表达,session_id、增量输出和清理又怎样协作?
  1. exec_command 先审批,再由 ProcessManager 以 stdin/stdout/stderr pipe 启动进程。
  2. Manager 为进程注册 session_id,并启动 stdout reader、stderr reader、timeout watcher、cancellation watcher 四类 Task。
  3. 首次只等待 yield_time_ms。进程未结束就返回 running=true、session_id 与当前增量输出;Tool Turn 可以继续采样。
  4. write_stdin 查找 session_id,可写 chars、关闭 stdin 或空写作 poll,然后再次等待一段 yield。
  5. 进程结束时等待 reader drain,取消 watcher,返回 final exit/output,并从 registry 删除;超时或取消同样必须 kill 与 reap。

调用链与等待点

1. `exec_command` 先审批,再由 ProcessManager 以 stdin/stdout/stderr pipe 启动进程
→ 2. Manager 为进程注册 session_id,并启动 stdout reader、stderr reader、timeout watcher、cancellation watcher 四类 Task
→ 3. 首次只等待 `yield_time_ms`
→ 4. `write_stdin` 查找 session_id,可写 chars、关闭 stdin 或空写作 poll,然后再次等待一段 yield
→ 5. 进程结束时等待 reader drain,取消 watcher,返回 final exit/output,并从 registry 删除;超时或取消同样必须 kill 与 reap

每个 await 都是状态可被取消、外部副作用可能已经发生或其他任务能够推进的边界。正文因此同时写清输入形状、所有者、成功产物和失败残留,而不是只列方法名。

Python 风格伪代码

async def exec_command(argv, cwd, yield_ms, timeout_ms, token):
    process = await spawn_with_pipes(argv, cwd)
    sid = registry.reserve(process)
    session = ProcessSession(
        process=process,
        stdout_reader=create_task(read_chunks(process.stdout)),
        stderr_reader=create_task(read_chunks(process.stderr)),
        timeout=create_task(kill_after(timeout_ms)),
        cancel=create_task(kill_on(token)),
    )
    registry[sid] = session
    return await poll(sid, yield_ms)

async def write_stdin(sid, chars="", close=False, yield_ms=250):
    session = registry.require(sid)
    if chars: write_and_drain(session.process.stdin, chars)
    if close: close_stdin(session.process.stdin)
    return await poll(sid, yield_ms)

async def poll(sid, yield_ms):
    wait_for_process_or_yield()
    output = drain_incremental_buffers()
    if still_running: return ProcessPoll(sid, running=True, output=output)
    await readers; cancel_watchers(); registry.remove(sid)
    return ProcessPoll(None, running=False, exit_code=returncode, output=output)

伪代码表达顺序和责任。真实可运行实现没有把万能调用当作未解释黑箱;对应模块见 Mini 导航。

失败、取消与恢复

Unified Exec 的持续进程和 stdin 续写失败路径图
图 9.10-2:失败分支按实际状态所有者收口,模型可见失败与 Runtime fatal 分开。
故障点已发生/残留状态处理与模型可见结果
未知 session_id无法定位进程write_stdin 返回模型可见错误
stdin 已关闭后写入pipe 状态不允许RuntimeError 转 ToolResult
模型不再 poll进程仍占容量timeout watcher 最终清理;Host shutdown 可全杀
timeout/Interrupt外部进程可能已产生副作用kill/reap,但不声称回滚副作用
输出洪泛chunk buffer 膨胀每次 drain 做 head/tail 截断

必须保持的不变量

running 结果必须带可继续寻址的 session_id;final 结果必须移除 Registry 并完成 reader/watcher 清理。

设计取舍

持续进程把一次 ToolCall 扩成跨多 Step 资源,必须新增容量、超时和 shutdown 责任。Mini 不实现 PTY、Windows ConPTY、zsh fork backend 或后台进程上限提示。

“为什么”的表述是从源码状态机、调用顺序和测试反推的工程解释;源码未声明的动机不写成官方承诺。

测试与复现实验

cd examples/mini-codex
uv run pytest -q -k 'test_process_manager_yields_session_and_accepts_stdin or test_process_manager_timeout_kills_and_reaps'
uv run mypy src

关键断言:

  • test_process_manager_yields_session_and_accepts_stdin
  • test_process_manager_timeout_kills_and_reaps

官方源码导航

Mini Codex 对照

  • src/mini_codex/execution/process_manager.py:进程 Session、reader/watcher 和 Registry
  • src/mini_codex/tools/unified_exec.py:exec_command/write_stdin Handler

本节边界

已经证明:running 结果必须带可继续寻址的 session_id;final 结果必须移除 Registry 并完成 reader/watcher 清理。

阅读导航

上一节:9.9 · 下一节:9.11

评论


← 返回文章列表