具体问题与边界
长进程为什么不能被一次 communicate 调用表达,session_id、增量输出和清理又怎样协作?
ProcessManager 是持续进程唯一所有者;exec_command 创建并首次 yield;write_stdin 按 session_id 写入或轮询;终态后从 Registry 移除。 本节以官方 Codex 锁定提交为事实基线,以 Mini Codex 的可执行断言检验 Python 设计;二者不是逐行翻译关系。
协议、类型与状态所有权
| 对象 | 创建/所有者 | 生命周期与作用域 | 是否持久化 |
|---|---|---|---|
| process session registry | ProcessManager | 进程运行期间 | 否 |
| session_id | ProcessManager 单调分配 | 直到 final poll | ToolResult 中暴露 |
| stdout/stderr chunk buffers | reader tasks | 两次 poll 之间 | 否 |
| timeout/cancel watcher | ProcessManager | 进程生命周期 | 否 |
| stdin pipe | 子进程/Manager | close_stdin 前 | 否 |
正常路径
exec_command先审批,再由 ProcessManager 以 stdin/stdout/stderr pipe 启动进程。- Manager 为进程注册 session_id,并启动 stdout reader、stderr reader、timeout watcher、cancellation watcher 四类 Task。
- 首次只等待
yield_time_ms。进程未结束就返回 running=true、session_id 与当前增量输出;Tool Turn 可以继续采样。 write_stdin查找 session_id,可写 chars、关闭 stdin 或空写作 poll,然后再次等待一段 yield。- 进程结束时等待 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 导航。
失败、取消与恢复
| 故障点 | 已发生/残留状态 | 处理与模型可见结果 |
|---|---|---|
| 未知 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_stdintest_process_manager_timeout_kills_and_reaps
官方源码导航
- codex-rs/core/src/unified_exec/process_manager.rs:持续进程 Registry 与 session 操作
- codex-rs/core/src/unified_exec/process.rs:子进程状态、I/O 与清理
- codex-rs/core/src/unified_exec/head_tail_buffer.rs:有界输出保留
- codex-rs/core/src/tools/handlers/unified_exec/exec_command.rs:首次执行工具
- codex-rs/core/src/tools/handlers/unified_exec/write_stdin.rs:stdin/poll 工具
Mini Codex 对照
src/mini_codex/execution/process_manager.py:进程 Session、reader/watcher 和 Registrysrc/mini_codex/tools/unified_exec.py:exec_command/write_stdin Handler
本节边界
已经证明:running 结果必须带可继续寻址的 session_id;final 结果必须移除 Registry 并完成 reader/watcher 清理。
评论
登录后即可评论