当我们打开终端输入 codex,AI 编码助手便开始运行。表面上看,只是接收你的指令、调用工具、返回结果。但在底层,Codex 运行着一套精密的多 Agent 协作运行时,一个会话可以从单 Agent 扩展为一棵 Agent 树,每个子 Agent拥有独立的执行线程,却共享同一个身份与控制面;它们通过邮箱异步通信,将审批请求级联委托给父 Agent,在保证安全的前提下实现分工协作。

进程通信协议

用户在 CLI 或 VSCode 中启动 Codex 时,客户端首先与服务端建立连接。

Codex 支持四种传输方式:stdio(JSONL over stdin/stdout,CLI 默认使用)、WebSocket over TCP、Unix Socket(VSCode 扩展使用)、以及In-Process(同进程内通过内存通道通信)。无论哪种方式,底层都是 JSON-RPC 2.0 协议。

如果采用 stdio 作为传输方式,通常由父进程负责拉起AppServer进程(子进程),在创建AppServer进程时,父进程会同时建立并持有与子进程 stdin / stdout 相连的管道端点,因此父进程可以直接向子进程的标准输入写入字节流,而AppServer进程则从 stdin 按流读取数据。

就这种传输机制而言,并不存在类似 TCP connect 的显式建连过程;通信通道在进程创建与句柄继承/重定向完成时就已经建立好了。

但这只解决了传输层通路问题,并不意味着应用层会话已经成立。对于 Codex app-server 而言,客户端仍必须先发送 initialize 请求完成握手,这一步的本质,是在既有的 stdio 字节流之上,完成客户端与服务端之间的 JSON-RPC 应用层会话协商,从而确认连接状态、客户端能力、通知策略以及相关身份元数据。

会话创建流程原理

  1. 客户端发送会话创建指令

客户端创建新会话的本质,就是由客户端父进程向AppServer子进程的stdin写入创建会话的指令数据流(指令 thread/start), 服务端收到 thread/start 指令后,进入会话创建流程。例如:

{ 
  "method": "thread/start", "id": 10, "params": {
  "model": "gpt-5.4",  ## 模型
  "cwd": "/Users/me/project",  ## 工作区
  "approvalPolicy": "never",   ## 审批策略
  "sandbox": "workspaceWrite", ## 沙箱配置
  "personality": "friendly",   ## 对话风格
  "serviceName": "my_app_server_client" ## 服务名称
} }
  1. ThreadManager 线程运行时的协调者,而不是底层执行内核

服务端处理 thread/start 时,核心工作是构建一个完整的会话实体。从架构层来说,App-server并不会去创建会话线程,而是app-server 解析 thread/start 的 JSON-RPC 参数把 modelcwdapprovalPolicysandboxpersonality 等请求参数解析成线程配置覆盖项,然后调用核心层的 Codex::spawn_internal来真正创建一个会话实体,其最终进入 ThreadManager::spawn_thread_with_source(...),由 core 负责真正创建线程运行时。

这里需要注意下,codex中对于thread的定义并不是操作系统线程,而是逻辑上的线程对象,是指: 用户与Codex代理之间的对话,每个线程包含多个Turn

ThreadManager是Codex 的对话 thread 对象的协调者,它负责决定“这个 thread 是复用已有运行时,还是新建一套运行时”,并在运行时创建完成后把它注册到内存中的 threads 映射表里,但它并不亲自完成会话内核的初始化,真正的构造工作是交给 Codex::spawn(...)Session::new(...) 去做的

  • threads: 哈希表HashMap<ThreadId, Arc<CodexThread>>,也就是:thread_id -> 运行中的 thread 句柄

  • thread_store:用来读写 thread 的持久化数据

  • models_manager:用于读写模型的信息,因为不同的线程可以使用不同的模型

  • environment_manager

  • auth_manager

  • skills_manager

  • plugins_manager

  • mcp_manager

ThreadManager是线程对象的协调者,但它并不做真正的线程创建动作,它核心完成以下事件

(1)先判断是否复用已有运行时 thread

spawn_thread_with_source(...)是创建 thread 的通用入口,因此无论是 newfork,还是 resume,最后都会走到这里。

但这里首先要处理一个特殊情况:如果当前操作是 resume,并且目标 thread_id 对应的 CodexThread 已经存在于内存里且仍在运行,那么就直接复用现有运行时,而不是再创建一套新的 Session/CodexThread

所以这里的本质不是泛泛地“判断是否恢复线程”,而是更准确地说:对于 resume 场景,先检查是否已经有同一个 thread 的活跃运行时,避免重复创建。

let is_resumed_thread = matches!(&initial_history, InitialHistory::Resumed(_));

(2)准备创建上下文

如果不是恢复线程,而是创建一个新的线程对象则会去解析环境相关信息、thread 父子关系、继承信息、多代理版本等。

也就是说,ThreadManager 会在创建 thread 前,先决定这个 thread 是否继承父 thread 的某些上下文关系。例如:它是不是从父 thread spawn 出来的?它是不是 fork 来的?

(4)真正把创建工作交给 Codex::spawn(...)

这里开始,真正的“会话运行时构造”才发生。也就是说,ThreadManager 负责协调,Codex::spawn(...) 负责落地构造。

let CodexSpawnOk { codex, thread_id, .. } = Box::pin(Codex::spawn(CodexSpawnArgs { ... })).await?;

(5)等待初始化完成并注册

Codex::spawn(...) 返回后,ThreadManager 不会立刻把它注册成可用 thread,而是要先等待这个新运行时发出的1个核心事件:SessionConfigured

收到这个事件后,ThreadManager 才会把底层 Codex 包装成 CodexThread,并插入 threads: HashMap<ThreadId, Arc<CodexThread>> 注册表中。

  pub struct CodexThread {
      codex: Codex,                          // 内层会话实体
      session_source: SessionSource,         // 身份来源标记
      session_configured: SessionConfiguredEvent, // 会话初始化完成事件快照
      rollout_path: Option<PathBuf>,         // 持久化路径
      out_of_band_elicitation_count: Mutex<u64>, // 带外请求计数器
  }
  1. 会话运行时构造器

spawn_internal 就是整个会话机制的"出生入口",它不是简单地创建一个对象,而是完成了一条从"无"到"有"的完整构建链路。

(1)创建SQ\EQ核心通道

首先core创建两个异步通道:

  • Submission Queue (SQ):提交通道 这是一个有界通道,容量固定为 512。外部通过它向会话提交 Submission,其中包含 Op::UserInputOp::InterruptOp::Shutdown 等操作。 有界的意义在于:当消费端处理不过来时,提交方会受到反压,避免无穷堆积。

  • Event Queue (EQ):事件通道 这是一个无界通道,会话通过它向外发出事件,例如 SessionConfiguredTurnStartedTurnComplete 等。 更准确地说,这里的设计重点不是“绝对不能丢”,而是:事件生产端不希望因为固定容量而被反压阻塞,因此选择了无界通道,让消费者按序持续读取。

let (tx_sub, rx_sub) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY);
let (tx_event, rx_event) = async_channel::unbounded();
(2)读取和整理共享会话信息

接下来开始组装“这个会话运行时启动时需要知道的全部前置信息”,包括:

- 用户指令来源,例如 AGENTS.md / 指令文件加载结果
- 执行策略 ExecPolicy
- 默认模型和模型信息
- base instructions
- dynamic tools
- collaboration mode 等
(3)组装 SessionConfiguration

所有解析结果被组装成一个 SessionConfiguration 对象。这个对象可以理解为会话的蓝图,它包含了会话运行所需的全部静态信息,但是此时还没有创建会话对象。

这个配置一旦组装完成,在会话生命周期中基本不变(少数字段可以通过 /thread/settings 动态调整)。

(4)创建会话Session

SessionConfiguration 只是静态蓝图,真正的会话实体是 Session。Session::new 是一个巨大的异步构造函数,核心处理以下内容:

  • model / collaboration mode

  • service tier

  • developer instructions / user instructions

  • personality

  • approval policy

  • permission profile

  • cwd / environments / workspace roots

  • session source / parent_thread_id / forked_from_thread_id / thread_source

  • dynamic tools 等

(5)分配身份 ThreadId/SessionId

生成唯一的 ThreadId,并根据是否为SubAgent决定 SessionId,根Agent的 SessionId 就是自己的 ThreadId,子Agent的 SessionId 则继承根Agent的,即所有子Agent共享同一个 SessionId,但各自有独立的 ThreadId。

如果是resume,则直接使用已有历史里的 conversation_id

let thread_id = match &initial_history {
    InitialHistory::New | InitialHistory::Cleared | InitialHistory::Forked(_) => {
        ThreadId::default()
    }
    InitialHistory::Resumed(resumed_history) => resumed_history.conversation_id,
};
let session_id = if session_configuration.session_source.is_non_root_agent() {
    agent_control.session_id()
} else {
    SessionId::from(thread_id)
};
(6)构建服务层、状态层...

此时会创建 SessionServices,包含所有长生命周期的基础设施对象,通俗的说,就是把模型客户端、MCP、hooks、plugins、thread store、telemetry、network proxy 等都挂进去。

- 认证管理器(auth_manager)——处理API密钥和OAuth

- 模型管理器(models_manager)——模型列表查询和缓存

- MCP连接管理器——与外部MCP服务器通信

- 执行策略管理器——命令审批规则

- Shell管理器——终端环境

- Hooks系统——生命周期钩子

- AgentControl——多Agent控制面(所有Agent共享同一个实例)

- 模型客户端(model_client)——实际调用AI模型API的客户端

- 网络代理——受控网络访问

- 环境管理器——执行环境(沙箱容器等)

注意,子Agent会继承父Agent的大部分服务对象(通过 Arc 共享指针),而不是重新创建。这意味着同一个进程中的所有Agent共享同一套认证、模型查询、MCP连接等基础设施,避免了重复初始化。

创建 SessionState——这是会话的"记忆",包含:

- 对话历史(ContextManager)——与模型的所有交互记录

- 速率限制信息——API调用配额追踪

- 自动压缩窗口——上下文长度管理

- 已授权权限记录——按环境ID追踪的权限授予

- 附加上下文存储——客户端提供的额外信息片段

可以通俗的理解为,组装最终实体,将所有部件组合成 Session 结构体:身份与事件通道、运行状态控制、并发与基础设施控制等等

(7) 启动后台 submission loop

最后一步是 tokio::spawn 启动 submission_loop——这是会话的"心跳"。它是一个无限循环的异步任务,不断从 SQ(提交通道)读取操作指令,然后根据指令类型分派处理。这里而已理解类似于Java中创建一个线程Loop处理。Rust 这里启动的是一个 Tokio 异步任务,它可能运行在 Tokio 线程池中的某个工作线程上,但逻辑上承担的是该 Session 的命令事件循环。

  while let Ok(sub) = rx_sub.recv().await {
      match sub.op {
          UserInput => 开始一个新Turn或向当前Turn注入输入
          Interrupt => 中断当前Turn
          InterAgentCommunication => 处理跨Agent消息
          ExecApproval => 处理审批回复
          Shutdown => 关闭会话
          ...其他操作类型
      }
  }
(8)封装成 Codex

最后,这整套运行时会被封装成Codex,因此 Codex 可以理解为对外暴露的会话运行时壳对象。统一打包后,提供给 ThreadManager 和上层调用者使用。

let codex = Codex {
    tx_sub,
    rx_event,
    agent_status: agent_status_rx,
    session,
    session_loop_termination: ...
};

更多推荐