4 minute read

第一篇已经提到,本地 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)。

Codex App Server runtime 封面图:TUI、VS Code、remote client 进入 App Server,再由 thread/turn runtime 路由到 core 和 client UI

图 1. App Server 是多端进入同一套 thread/turn runtime 的协议边界,不是模型循环本身。

阅读契约

这篇按四条线读:

  1. initializethread/startturn/start 如何把 client 接入 core?
  2. ServerNotificationServerRequest 为什么要分开?
  3. 多 Agent 为什么要建 child thread 和 mailbox,而不是在 parent prompt 里递归?
  4. rollout trace 为什么能还原图谱,却不承担生产恢复职责?

读完后,应当能把一个事件放回它所在的生命周期:连接状态、thread/turn、core event、client request、child agent edge,或 trace reducer graph。

下图把 App Server、core、多 Agent 和 trace 放在一起。重点不是“多一层服务”,而是所有 client 都先变成 thread/turn lifecycle,再由 core 事件回流。

Codex App Server、多 Agent 和 rollout trace 总览:连接、thread、turn、AgentControl、mailbox、state DB 和 trace graph 的关系

图 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

App Server 协议生命周期图:initialize、thread/start、turn/start、server events、turn done 顺序组成 client 到 core 的状态机

图 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/startturn/start 是多端进入 core 的同一条门

真正进入 core 的大多数请求,都在 CodexMessageProcessor。它持有 ThreadManagerthread_storethread_state_managerthread_watch_manager、command exec manager、workspace settings cache、feedback / log db 等对象,见 CodexMessageProcessor

TurnStart 分支最终进入 turn_start。这里做的事情第一篇讲过一部分:校验 input,检查 thread,映射 v2 input 到 core input,处理 turn overrides,最后构造 Op::UserInputWithTurnContextOp::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::TurnStarted
  • AgentMessageContentDelta -> AgentMessageDelta
  • RequestUserInput -> ServerRequest::ToolRequestUserInput
  • TurnComplete -> TurnCompleted

这个边界在 bespoke_event_handling.rs

为什么 approval 要走 ServerRequest,而不是普通 notification?因为 approval 需要 client 回答。core 这边已经计算出某个 command / patch / MCP tool 需要审批;app-server 把它转成 request,UI 展示给用户,用户决策再回填 core。

Approval ServerRequest 图:core policy 产生 RequestUserInput,App Server 翻译成 ToolRequestUserInput,client UI 收集 allow/deny 后回到 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_notificationhandle_server_request

这一层支持两个能力:

  1. 非 active thread 的事件可以先 buffer。
  2. 切换 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 的新执行实体。

Codex 多 Agent trace edge 示例:parent spawn child、child 完成后返回 parent,并由 reducer 还原 InteractionEdge

图 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。

多 Agent child thread 与 mailbox 图:parent thread spawn child thread,消息进入 mailbox,state DB 持久化 spawn edges,completion envelope 回到 parent

图 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

rollout trace reducer 图:raw events 与 payload refs 写入 trace bundle,离线 reducer 再还原 threads、edges 和 tool evidence graph

图 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、可审计执行系统的关键中间层。

参考