Codex 源码剖析:004. App Server、多 Agent 与 Trace
第一篇已经提到,本地 TUI 默认也走 in-process app-server。这一篇继续往下看:App Server 不只是把 UI 请求转给 core,它还把 thread、turn、多 Agent、审批、远程 client、事件回放、trace 全部放到一个协议化生命周期里。
这里最值得看的不是某个具体 JSON-RPC method,而是整体形态:
Codex 正在把“一个 CLI agent”拆成“一个可被多个 client 驱动的 thread/turn runtime”。
OpenAI 单独写过 App Server 的文章:client 和 server 先 initialize,协商能力、协议版本、feature flags,然后 client 创建 thread、启动 turn,server 回 progress notifications 和 approval requests(见 Unlocking the Codex harness)。

图 1. App Server 是多端进入同一套 thread/turn runtime 的协议边界,不是模型循环本身。
阅读契约
这篇按四条线读:
initialize、thread/start、turn/start如何把 client 接入 core?ServerNotification和ServerRequest为什么要分开?- 多 Agent 为什么要建 child thread 和 mailbox,而不是在 parent prompt 里递归?
- rollout trace 为什么能还原图谱,却不承担生产恢复职责?
读完后,应当能把一个事件放回它所在的生命周期:连接状态、thread/turn、core event、client request、child agent edge,或 trace reducer graph。
下图把 App Server、core、多 Agent 和 trace 放在一起。重点不是“多一层服务”,而是所有 client 都先变成 thread/turn lifecycle,再由 core 事件回流。

图 2. App Server 接住 client 请求,thread/turn 把请求送进 core,多 Agent 通过 child session 和 mailbox 形成线程树,trace 再把运行证据还原成图。
一、App Server 是协议边界
1.1 MessageProcessor 管连接、初始化和请求分发
App Server 的外层事件循环在 app-server/src/lib.rs。它维护 connection map,监听 transport events,并在 connection 打开时创建 ConnectionSessionState。
每个连接需要先完成 initialize。connection state 里有 origin、rpc gate、initialized state、experimental API 是否 enabled、opted-out notifications、client name / version 等字段,见 ConnectionSessionState。

图 3. 协议生命周期先于模型循环。连接能力、thread 状态、turn 输入和 server events 都要先在协议层站稳。
请求进来以后,MessageProcessor::process_request 负责把 JSON-RPC request 分给不同 API:
- config API
- device key API
- fs API
- external agent config API
- Codex thread/turn API
MessageProcessor 自身持有 Codex thread/turn API、ConfigApi、FsApi、AuthManager、FsWatchManager、request serialization queues 等对象,见 MessageProcessor。
这层有点像应用网关。它不是模型循环,也不是 UI,但它决定了哪些请求可以进来、怎么被序列化、哪些 connection 能收到哪些 notification。
1.2 Protocol v2 是内部类型和外部 API 的翻译层
App Server protocol v2 里有不少“翻译层”代码。比如 v2_enum_from_core! 宏会把 core enum 映射成 camelCase API v2 enum,见 v2_enum_from_core。
错误类型也是类似。CodexErrorInfo 在 v2 里用 camelCase,并保留 HTTP status code、response stream connection/disconnection、active turn not steerable 等细节,见 CodexErrorInfo。
为什么要这么做?因为 core 内部类型要服务 Rust 运行时,protocol 类型要服务外部 client。两边如果直接共用所有结构,短期省事,长期会让内部重构和外部兼容绑死。Codex 用 app-server-protocol crate 把这层明确拆出来。
所以源码里很多看似重复的类型转换不是纯 boilerplate,而是给“内部可变、外部稳定”留空间。
1.3 thread/start 与 turn/start 是多端进入 core 的同一条门
真正进入 core 的大多数请求,都在 CodexMessageProcessor。它持有 ThreadManager、thread_store、thread_state_manager、thread_watch_manager、command exec manager、workspace settings cache、feedback / log db 等对象,见 CodexMessageProcessor。
TurnStart 分支最终进入 turn_start。这里做的事情第一篇讲过一部分:校验 input,检查 thread,映射 v2 input 到 core input,处理 turn overrides,最后构造 Op::UserInputWithTurnContext 或 Op::UserInput。源码在 turn_start。
更重要的是,这条门不只服务 TUI。VS Code、remote control、其他 client 只要实现 protocol,就可以用同样的 thread/turn 生命周期驱动 core。
不过这里要保留迁移边界:TUI 侧的 app_server_adapter.rs 明确说它是 hybrid migration period 的临时层,TUI 还保留 direct-core 行为。本篇讨论的是 app-server-backed flows 和它代表的演进方向。
二、事件路由:core 只吐事件,client 负责呈现
2.1 EventMsg 翻译成 notification / request
core 发出来的是 EventMsg。App Server 不能原样扔给 client,因为 client 需要的是 protocol v2 里的 notification/request。
翻译发生在 apply_bespoke_event_handling。它会把不同事件转成对应 app-server 事件,例如:
TurnStarted->ServerNotification::TurnStartedAgentMessageContentDelta->AgentMessageDeltaRequestUserInput->ServerRequest::ToolRequestUserInputTurnComplete->TurnCompleted
这个边界在 bespoke_event_handling.rs。
为什么 approval 要走 ServerRequest,而不是普通 notification?因为 approval 需要 client 回答。core 这边已经计算出某个 command / patch / MCP tool 需要审批;app-server 把它转成 request,UI 展示给用户,用户决策再回填 core。

图 4. Notification 只能通知;approval 需要 client 回答。UI 负责呈现风险和收集决策,不重新实现权限判断。
这点对安全很关键。UI 只是呈现和收集决策,不应该重新实现一遍权限判断。
2.2 TUI 按 thread id 分流事件
TUI 收 app-server event 的入口是 handle_app_server_event。它根据 thread id 把 notification/request 投递到对应 thread channel。active thread 的事件再进入 handle_thread_event_now,最后交给 ChatWidget::handle_server_notification 或 handle_server_request。
这一层支持两个能力:
- 非 active thread 的事件可以先 buffer。
- 切换 thread 后,UI 可以把该 thread 的 buffered events 重新消费。
这就是 thread/turn protocol 的价值:UI 不需要知道 core 内部 Session 怎么调度,只需要维护每个 thread 的事件流。
三、多 Agent 不是函数递归,而是线程树
3.1 child agent 是新的 child thread
Codex 的 multi-agent v2 不是简单在当前 prompt 里塞“你现在是子 agent”。它更接近创建一个新的 child thread。
控制面是 AgentControl,它是 root session tree 共享的控制对象,内部持有 AgentRegistry,并通过 weak reference 回到 ThreadManagerState 避免循环引用,见 AgentControl。
线程创建 / 恢复在 ThreadManagerState。fresh child 会继承父 trace context;resumed child 不继承,避免重复 ThreadStarted 事件。这个逻辑在 thread_manager.rs。
v2 identity 用 AgentPath。路径形如 /root/task_name,只允许小写、数字、下划线等安全字符,见 AgentPath。这给多 Agent 树一个稳定、可显示、可路由的身份。
spawn_agent v2 handler 会解析 task、role、model、fork 配置,构造 child config,生成 SessionSource::SubAgent(ThreadSpawn),再交给 AgentControl::spawn_agent_with_metadata,见 multi_agents_v2/spawn.rs。
这里真正改变系统形态的是“child thread source”。它不是在 parent history 里假装换了一个角色,而是创建一个可路由、可恢复、可 trace 的新执行实体。

图 5. 普通 transcript 只能看到“某段结果文本出现了”;trace graph 则能追到这段结果来自哪个 child thread,以及它是通过 spawn/result 边回到 parent 的。
把 v2 spawn 压成控制流:
parse spawn_agent args
resolve role / model / fork_context
build child SessionConfig
create child thread source = SubAgent(ThreadSpawn)
AgentControl.spawn_agent_with_metadata(child)
return child identity to parent turn
3.2 Mailbox 让父子消息保持来源边界
父子 agent 通信走 InterAgentCommunication。这类消息先进入 mailbox,不是直接写对方 conversation history。mailbox drain 时再变成 assistant commentary message。

图 6. 多 Agent 是线程树,不是函数递归。mailbox 保留来源边界,state DB 保留可恢复的父子关系。
protocol 里的结构在 InterAgentCommunication。Session 侧 drain / enqueue 逻辑可以从 session/mod.rs 的 mailbox 路径 看。
直接写对方 history 虽然简单,但会让信息来源、turn 边界、trace replay 都变得模糊。mailbox 相当于一个跨线程消息缓冲层:消息可以触发新 turn,也可以等目标 idle;消息来源可以被标记成 agent communication;trace 可以记录 spawn、send、followup、result 的边;UI 也可以展示 subagent notification,而不是把它当普通用户消息。
child 完成后,v2 会向 parent 投递 completion envelope,并在 trace 里记录 AgentResultObserved edge,见 child completion。
3.3 State DB 保存可恢复关系
多 Agent 如果只存在内存里,就没法可靠 close descendant、resume tree、恢复父子关系。Codex state DB 有 thread_spawn_edges,用于记录 thread 树关系,见 state/src/runtime/threads.rs。
这说明多 Agent 不是“并行起几个 task”这么简单。只要希望它们可恢复、可关闭、可观察,就需要把父子边持久化。
四、rollout trace 把运行时还原成图
rollout-trace/README.md 的设计原则是 “observe first, interpret later”。运行时记录 raw events 和 payload references,离线 reducer 再还原成 semantic graph,见 rollout-trace README。

图 7. Trace graph 很适合诊断交错运行证据;生产恢复仍依赖 rollout JSONL。
这个 graph 不只是 transcript。它包含 threads / turns、conversation_items、inference_calls、tool_calls、code_cells、terminal operations、compactions、interaction_edges、raw_payload refs。
对多 Agent 来说,最关键的是 interaction edges。README 里有一段专门讲 multi-agent v2:child threads 和 root thread 共享 trace writer,一个 root bundle 可以 reduce 成包含 parent、child threads 和边的图,见 rollout trace multi-agent v2。
trace reducer 还会把 spawn/send/followup/close/result 转成 InteractionEdge,尽量指向 recipient 侧真实 conversation item。如果 child 还没形成 task message,spawn edge fallback 到 thread。这个逻辑在 rollout-trace reducer agents.rs。
这就是 trace 的价值:当模型、工具、终端、子 agent、UI notification 交错发生时,普通 transcript 只能告诉你“最后看到了什么文本”,trace graph 才能告诉你“这段文本从哪个 tool call、哪个 child thread、哪个 runtime object 流过来”。
五、和云端 coding agent 的关系
OpenAI 在 Codex 发布文章里强调过 cloud-based task 在独立 sandbox 环境里并行执行,并提供 terminal logs 和 test outputs 作为可验证证据。GitHub Copilot cloud agent 的官方文档也描述了类似方向:agent 可以在 GitHub Actions-powered ephemeral development environment 里研究仓库、制定计划、改分支、运行测试,再让用户 review diff / PR(见 GitHub Copilot cloud agent)。
这说明一个趋势:coding agent 正在从“IDE 里一个聊天助手”演进成“有独立执行环境、有生命周期、有日志、有分支、有审批、有多人协作入口的软件执行系统”。
Codex app-server / thread / turn / trace 这套结构,正是在为这种形态服务。它让本地配对和远程委托逐步收敛到同一个 core runtime 的不同 client 和不同部署方式,而不是永远维护两套互不相干的 agent。
六、源码阅读规则
| 读到的模块 | 先问什么 | 正确边界 |
|---|---|---|
| App Server | 它是在跑模型,还是在守协议? | 它是 protocol boundary,不是模型循环。 |
| protocol v2 | 为什么有类型转换? | 内部 Rust 类型和外部 client API 要解耦。 |
ServerRequest |
为什么不是 notification? | 因为 approval 需要 client 回答。 |
| TUI event channel | 为什么按 thread id 分流? | UI 要能 buffer 非 active thread 事件。 |
spawn_agent |
它是不是函数递归? | 不是。它创建 child thread。 |
| mailbox | 为什么不直接写 history? | 要保留来源、turn 边界和 trace replay。 |
| rollout trace | 它是不是生产恢复路径? | 不是。它是 opt-in 诊断图谱。 |
小结
App Server 是 Codex harness 的协议边界,不只是 TUI 到 core 的 adapter。它用 initialize 管 client 能力和协议状态,用 thread/turn lifecycle 把多端请求统一进入 core,用 ServerNotification / ServerRequest 把 core 事件和审批请求翻译给 client,用 thread event buffer 支持 UI 的 thread 切换和事件回放,用 child thread、mailbox、state DB 把多 Agent 做成可恢复线程树,再用 rollout trace 把运行时证据还原成 graph。
所以 Codex 的 App Server 不是“多了一层服务端”。它是从单机 CLI agent 走向多端、多线程、多 Agent、可审计执行系统的关键中间层。