Skip to content

Thread与Turn概念模型

从运行时所有权、agent-tree 身份、活动任务和客户端投影四个层次区分 Codex 的 Thread、Session、Turn 与 Task。

基于rust-v0.150.0
CodexRustThreadTurn

Thread与Turn概念模型 ​

Codex 源码中的 Thread、Session、Turn 和 Task 不是四个彼此嵌套的业务实体。CodexThread 是运行 handle,Session 是一个已加载 thread 的运行时所有者,session_id 却标识整棵 agent tree;Core 没有保存历史 Turn 列表的 Turn 聚合对象,一次活动 Turn 由 context、active slot、task 和 mutable state 协作表达;客户端看到的 Thread 与 Turn 又是 App Server reducer 生成的快照。

如果忽略这些层次,很容易产生几种错误实现:从 manager map 删除 thread 就认为 Session 已销毁, 用 session_id 查找具体运行 handle,把历史 Turn 当成仍存在的 Tokio task,或者为了修改一个客户端 Turn 字段直接把 Core 内部状态暴露到协议层。

阅读前先用 Core运行时架构总览 建立职责域;需要完整对象地图时可对照 核心数据对象关系。本文只解释概念语义,不逐字段展开锁和 同步原语;进入具体 Session 字段后应转到 Session核心数据结构。

1. 实体投影 ​

名称具体类型所属层生命周期是否直接持久化
Thread 身份ThreadIdprotocol一个具体 thread 的整个寿命写入 metadata/rollout
Thread 运行 handleCodexThreadCorethread 加载到关闭否
Thread 运行时SessionCore与已加载 thread 基本同寿命否
Thread 客户端快照v2::ThreadApp Server protocol一次响应/通知从 store/live state 重建
Agent-tree 身份SessionIdprotocol / AgentControlroot 与 descendants 共享写入 SessionMeta
Turn 身份submission ID / TurnContext.sub_idCore一次 Turn写入 TurnContext/event
Turn 活动槽位ActiveTurnCoreSession 当前活动阶段否
Turn 执行RunningTask + SessionTaskCoretask spawn 到 terminal否
Turn 可变交互TurnStateCoreactive Turn 内否
Turn 客户端快照v2::TurnApp Server protocol一次投影从 events/items 重建

这里最反直觉的关系是:一个 Core Session 只服务一个 loaded Thread,但一个 SessionId 可以对应多 个 Core Session。Rust 类型名与协议字段名相似,语义却不在同一轴上。

2. Thread形态 ​

2.1 ThreadId ​

ThreadId 包装 Uuid,new() 使用 UUIDv7。新建、清空和 fork history 会分配新 ID;resume 使用 原 history 中的 conversation/thread ID。它是 manager map、rollout metadata、App Server thread ID 和 agent graph 的关联键。

ThreadId 不是进程 ID,也不是模型 response ID。一个 thread 内可以产生多个 Turn、多个模型 response 和多个命令进程,这些 ID 不能互换。

2.2 CodexThread ​

ThreadManagerState 的 active map 保存 ThreadId → Arc<CodexThread>。CodexThread 本身组合:

  • Arc<Session>:运行状态和 thread-scoped services;
  • SessionIo:submission sender、event receiver、status receiver 和 termination signal;
  • SessionSource、首次 SessionConfiguredEvent 与 rollout path;
  • 少量 handle 级 out-of-band elicitation 状态。

它的核心职责是让宿主提交 Op、读取 Event、查询运行状态和关闭 Session。它不是可序列化的完整 Thread,也不保存历史 Turn 列表。

源码位置:codex-rs/core/src/codex_thread.rs :: CodexThread

rust
pub struct CodexThread {
    // Session 拥有状态与服务,SessionIo 拥有双向消息端点。
    pub(crate) session: Arc<Session>,
    pub(crate) io: SessionIo,
    pub(crate) session_source: SessionSource,
    // 创建握手快照保存在 handle 上,宿主无需重放首事件才能读取。
    session_configured: SessionConfiguredEvent,
    rollout_path: Option<PathBuf>,
    out_of_band_elicitations: Mutex<OutOfBandElicitations>,
    _diagnostics_guard: GaugeGuard,
}

impl CodexThread {
    pub(crate) fn new(
        session: Arc<Session>,
        io: SessionIo,
        session_configured: SessionConfiguredEvent,
        rollout_path: Option<PathBuf>,
        session_source: SessionSource,
    ) -> Self {
        Self {
            session,
            io,
            session_source,
            session_configured,
            rollout_path,
            // 带外 elicitation 独立加锁,不污染 SessionState 或历史模型。
            out_of_band_elicitations: Mutex::new(OutOfBandElicitations::default()),
        }
    }

    // handle 只委托消息与关闭操作,不复制 Session 的控制状态。
    pub async fn submit(&self, op: Op) -> CodexResult<String> {
        self.io.submit(op).await
    }

    pub async fn shutdown_and_wait(&self) -> CodexResult<()> {
        self.io.shutdown_and_wait().await
    }

    pub fn session_telemetry(&self) -> SessionTelemetry {
        self.session.services.session_telemetry.clone()
    }

    pub async fn wait_until_terminated(&self) {
        self.io.session_loop_termination.clone().await;
    }
}

session_configured 和 rollout_path 是 Thread 级快照,活动 Turn 的 task、waiter 与状态不在这个结构中; 这进一步说明 Thread 不是 Turn 集合容器。

2.3 StoredThread ​

Thread 从 manager map 移除或进程退出后,rollout、SQLite metadata 和 ThreadStore 仍可描述该 thread。 App Server 的 thread/list、thread/read 可以返回未加载记录,其 ThreadStatus 为 NotLoaded。

因此:

  • loaded thread 一定有 Arc<CodexThread>;
  • stored thread 不一定 loaded;
  • ephemeral thread 可能 loaded 却不 materialize 到磁盘;
  • map remove 只解除 manager 所有权,其他 Arc 持有者仍可使用 handle。

2.4 Thread 产品快照 ​

App Server Thread 包含字符串 id/session_id、fork/parent 关系、preview、ephemeral、section、model provider、时间戳、status、path、cwd、source、Git metadata 和可选 turns。大多数 notification 中 turns 为空;只有 resume、rollback、fork、read 且请求 includeTurns 等路径才填充历史。

它不是 CodexThread 的 serde 版本。协议快照可以由 live state、ThreadStore metadata 和 rollout history 合成,而运行 handle 包含 channel、锁、Tokio task 和不可序列化 service。

3. Session ​

Core Session 的注释称其为 initialized model agent context,并声明同一时刻最多一个 running task。 它持有一个具体 thread_id、SessionState、ActiveTurn、InputQueue、SessionServices、realtime manager 和 预热/刷新控制。

SessionId 则是 agent control session 的身份。AgentControl 注释明确要求:每个 root thread/session tree 最多创建一份 control,它随后被所有 sub-agent 共享,所以共同 root 的 descendants 使用相同 session_id。

活动 Turn 在 Core 中是一个很小的拥有关系,而不是历史集合:ActiveTurn 只保存可选的运行任务和共享 的 TurnState。因此“当前有 Turn”与“当前已经 spawn 了 task”是两个可分别判断的事实;Steer、取消和 空闲恢复都必须在持有 active_turn 锁时观察这两个字段。

源码位置:codex-rs/core/src/state/turn.rs 的 ActiveTurn。

rust
pub(crate) struct ActiveTurn {
    pub(crate) task: Option<RunningTask>,
    pub(crate) turn_state: Arc<Mutex<TurnState>>,
}

源码位置:codex-rs/core/src/state/turn.rs 的 MailboxDeliveryPhase。 同一文件还用 MailboxDeliveryPhase 区分当前 Turn 是否仍可吸收 mailbox 消息:

rust
/// Mailbox 是否还能并入当前 Turn 的下一次模型请求。
/// CurrentTurn 允许消费排队消息;NextTurn 把迟到消息留给下一 Turn。
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub(crate) enum MailboxDeliveryPhase {
    /// 当前 Turn 仍可消费 mailbox。
    #[default]
    CurrentTurn,
    /// 已产生可见终态,迟到消息不得延长答案。
    NextTurn,
}

impl Default for ActiveTurn {
    fn default() -> Self {
        Self {
            task: None,
            turn_state: Arc::new(Mutex::new(TurnState::default())),
        }
    }
}

这个结构证明的是活动槽位的所有权边界,不证明 TurnState 内部字段的语义;后者由 Session 状态与 TurnContext 专题分别展开。

3.1 ID层级 ​

Session 初始化按下面的优先级解析 session_id:

  1. resume history 若有 RolloutItem::SessionMeta,优先恢复其中的 session_id;
  2. non-root agent 没有可用 persisted ID 时,继承传入 AgentControl.session_id();
  3. root thread 使用 SessionId::from(thread_id);
  4. legacy subagent rollout 中等于自身 thread ID 的 session ID 会被过滤,再改为继承 agent tree ID。

SessionId 和 ThreadId 都包装 UUID,并提供双向 From 转换。这只表示 root 可以复用相同 UUID 值, 不表示二者语义相同。

源码位置:codex-rs/protocol/src/thread_id.rs :: ThreadId

rust
pub struct ThreadId {
    pub(crate) uuid: Uuid,
}

impl ThreadId {
    // 新 Thread 使用 UUIDv7,使身份唯一且保留时间有序特征。
    pub fn new() -> Self {
        Self { uuid: Uuid::now_v7() }
    }

    pub fn from_string(s: &str) -> Result<Self, uuid::Error> {
        Ok(Self { uuid: Uuid::parse_str(s)? })
    }
}

pub struct SessionId {
    pub(crate) uuid: Uuid,
}

// 转换只复用底层 UUID;Rust 类型仍阻止调用方混淆两种作用域。
impl From<ThreadId> for SessionId {
    fn from(value: ThreadId) -> Self {
        Self { uuid: value.uuid }
    }
}

impl From<SessionId> for ThreadId {
    fn from(value: SessionId) -> Self {
        ThreadId { uuid: value.uuid }
    }
}

两种 ID 可以无损转换,是因为底层表示相同;类型仍分开,是为了让函数签名表达“一个 Thread”还是 “整棵 agent tree”。如果只把它们当字符串使用,编译器就无法阻止把 child ThreadId 误当 tree ID。

图中的 SessionId → AgentControl 是身份归属,不是内存容器关系。AgentControl 通过 weak manager handle 和自己的 registry 管理 agent tree,避免形成 ThreadManagerState → CodexThread → Session → AgentControl → ThreadManagerState 强引用环。

4. Turn聚合 ​

App Server 定义了真正的客户端 Turn struct:id、items、items_view、status、error、 started/completed 时间和 duration。Core 运行时没有与它一一对应、长期保存的 struct。

一次活动 Turn 由四个对象协作表达:

对象回答的问题是否跨 Turn
TurnContext本 Turn 使用什么模型、权限、环境、指令和 metadata?否
ActiveTurnSession 当前是否有活动工作?否
RunningTask哪个算法正在运行,如何取消和等待?否
TurnState哪些审批、输入、elicitation 和动态工具 response 尚未返回?否

4.1 Turn身份 ​

SessionIo::submit() 生成 submission ID。启动用户 Turn 时,该 ID 进入 TurnContext.sub_id,随后成为 TurnStartedEvent.turn_id、Event.id 关联值和 App Server Turn ID。 因此源码中经常把 sub_id 当作当前 Turn 的身份。

不是每个 submission 都创建 Turn。审批回复、MCP response、配置更新和 shutdown 也有 submission ID,但只是 Session 控制消息。只有进入 task/Turn lifecycle 的操作才形成客户端 Turn。

Core 的双向信封直接保留这条关联,而不是把它隐藏在 channel 元数据中:

源码位置:codex-rs/protocol/src/protocol.rs :: Submission, Event

rust
pub struct Submission {
    // 用户 Turn 和控制提交都用该 ID;是否形成 Turn 由 Op 分支决定。
    pub id: String,
    pub op: Op,
    pub client_user_message_id: Option<String>,
    pub trace: Option<W3cTraceContext>,
    pub parent_turn_id: Option<String>,
}

pub struct Event {
    // 事件通过同一 ID 回到所属 submission/Turn。
    pub id: String,
    pub msg: EventMsg,
}

pub enum EventMsg {
    Error(ErrorEvent),
    Warning(WarningEvent),
    GuardianWarning(WarningEvent),
    RealtimeConversationStarted(RealtimeConversationStartedEvent),
    RealtimeConversationRealtime(RealtimeConversationRealtimeEvent),
    RealtimeConversationClosed(RealtimeConversationClosedEvent),
    // ...
}

EventMsg 是开放增长的协议面,Event.id 才是跨事件类型稳定的关联键。调试时若只按 event variant 聚合,会把同一 Turn 的 warning、item 和 terminal event 拆散。

4.2 TurnContext ​

TurnContext 保存模型、provider、reasoning、SessionSource、history mode、parent thread、originator、 environment、cwd、日期时区、developer instructions、mode、network、sandbox level、dynamic tools、输出 schema,以及 timing/metadata state handles。

task、模型 step 和工具通过 Arc<TurnContext> 共享同一 Turn 语义。下一次 Turn 可以改变 model 或 permission,但当前 Turn 不应在执行中读取一份不断变化的全局 Config 来重解释已广告的工具和策略。

4.3 ActiveTurn单活 ​

Session.active_turn 是 Mutex<Option<ActiveTurn>>。None 表示空闲;Some 表示已有 Turn state, 其中 task: Option<RunningTask> 可以在生命周期切换中暂时为空。判断“是否正在执行”时应检查具体 方法的语义,不能看到 Some(ActiveTurn) 就推断模型正在采样。

4.4 TurnState ​

TurnState 维护以 call/request ID 为键的 oneshot sender:approval、request permissions、 request_user_input、MCP elicitation 和 dynamic tool response。它还保存 pending input、当前 Turn 授予 的环境权限、tool-call count 与 token baseline。

这些 sender 只对当前进程中的 waiter 有意义。Turn 终止时必须 clear;resume 只能从 rollout 重建 用户可见 item 和上下文,不能恢复原 oneshot 或继续旧 Tokio future。

5. Task 执行策略 ​

SessionTask trait 负责四件事:返回粗粒度 TaskKind、给 tracing 提供 span name、执行 async run(),以及可选的 abort() 清理。Session::spawn_task() 统一安装 cancellation token、Tokio handle、完成通知、TurnContext 与 terminal handling。

当前生产 task 的关系如下:

Task 实现TaskKind是否走普通模型循环主要终态语义
RegularTaskRegular是,调用 run_turn()assistant complete/error/abort
CompactTaskCompact专用 compaction 流程更新 history/window
ReviewTaskReview启动受限 review conversationexited review item
UserShellCommandTaskRegular否,直接 execcommand item + TurnComplete

UserShellCommandTask 返回 TaskKind::Regular 是理解边界的关键:TaskKind 不是 task 实现的一一枚举, 而是状态查询、取消处理和遥测使用的粗粒度类别。若业务逻辑用 TaskKind 反推具体 Rust 类型,会把 user shell 当作普通模型 Turn。

同一个 Turn 通常由一个 task 驱动,但两个概念仍不可互换:

  • Turn 有对外 ID,task 没有独立业务 ID;
  • TurnContext 描述语义快照,task 描述执行算法;
  • task 返回后,on_task_finished() 才产生 completed/failed/aborted 终态;
  • interrupt/replace 可以终止 task,但必须另行记录 TurnAborted;
  • 历史重放重建 Turn,不会重新实例化原 task。

6. Turn生命周期 ​

客户端 Turn 状态不是通过读取 RunningTask 生成的。Core 发出 lifecycle event,App Server 的 ThreadHistoryBuilder 归约事件和 rollout item:

TurnStatus 只有 Completed、Interrupted、Failed、InProgress。ThreadHistoryBuilder 收到 TurnStarted 时打开 PendingTurn,item event 更新其中的 items,TurnComplete 或 TurnAborted 闭合。 Error 可以记录到 current Turn,最终 status 还要结合 terminal event。

缺少 terminal event 的 rollout 可能留下 incomplete Turn,reducer 在 finish 或恢复逻辑中需要给出 可显示快照。这也是为什么持久化顺序重要:interrupt path 会先把 aborted marker/相关 item flush,再 发 TurnAborted,尽量保证客户端实时结果与恢复结果一致。

同一生命周期按参与者展开后,可以看出 Turn ID 如何从 submission 一直传到客户端 reducer。下面的 时序图只画身份与终态,不展开模型和工具内部循环。

SessionTask 没有自己的业务 ID;它借用 TurnContext.sub_id 产生 lifecycle event。reducer 依据该 ID 归并 item 和终态,所以控制类 submission 即使也有 ID,也不会自动形成一个 Turn。

7. Thread与Turn状态 ​

这两个枚举位于 App Server v2 协议中,源码没有建立一一转换:

源码位置:codex-rs/app-server-protocol/src/protocol/v2/turn.rs :: TurnStatus

rust
// Turn 状态只描述一轮工作的闭合结果。
pub enum TurnStatus {
    Completed,
    Interrupted,
    Failed,
    InProgress,
}

// Thread 状态描述 live runtime,并可携带等待外部交互的细分标记。
pub enum ThreadStatus {
    NotLoaded,
    Idle,
    SystemError,
    Active { active_flags: Vec<ThreadActiveFlag> },
}

pub enum ThreadActiveFlag {
    WaitingOnApproval,
    WaitingOnUserInput,
}

ThreadStatus::Active 还能表达等待哪类外部交互,而 TurnStatus::InProgress 只说明 Turn 尚未闭合;这正是 源码中的状态来源也说明它们不能相互推导。

状态含义来源
ThreadStatus::NotLoaded只有 stored metadata,没有 live handleThreadStore/list/read
ThreadStatus::Idleloaded,但无活动工作live Session 状态
ThreadStatus::Active当前有活动行为,可带 waiting flagsactive task / pending interaction
ThreadStatus::SystemErrorthread runtime 进入系统错误状态App Server live state
TurnStatus::InProgress某个 Turn 已开始未闭合ThreadHistoryBuilder
TurnStatus::Completed收到正常终态TurnComplete
TurnStatus::Interrupted收到中断终态TurnAborted
TurnStatus::FailedTurn 有失败结果error/terminal projection

一个 Thread 可以在历史中拥有多个 completed Turn,同时当前为 Idle;也可以 Thread Active,但历史 最后一个 Turn 的 items 尚未完整 materialize。Thread 状态不能通过 turns.last().status 简单计算, 因为 loaded runtime 还有 approval/user-input wait 和后台状态。

8. ID 关系 ​

ID作用域典型载体用途
SessionIdagent treeSessionMeta、Thread.sessionId归并 root 与 descendants
ThreadId一个 threadmanager map、rollout、Thread.id查 handle、store 与 agent node
Turn/submission ID一次 Turn/控制提交TurnContext.sub_id、Event.id、Turn.id串联 task 与客户端事件
model response ID一次采样ResponseEvent/metadata模型连续性与遥测
tool call ID一次工具调用call + output item配对模型请求和工具结果
process ID一个执行进程exec begin/end/stdin写入、resize、signal、terminate

排查 child agent 未显示时,先比对 session_id 和 parent ThreadId;排查一个 Turn 卡住时,查 Turn/submission ID 与 active task;排查工具结果未回灌时,查 call ID;排查终端命令残留时,查 process ID。只拿 ThreadId 搜索所有日志会得到大量同 thread 的无关采样和工具事件。

9. 常见错误推断 ​

错误推断为什么不成立正确判断
一个 SessionId 对应一个 Sessiondescendants 共享 tree identity用 ThreadId 找具体 Session
一个 Thread 永远有 CodexThreadstored thread 可以 NotLoaded区分 live map 与 ThreadStore
Core Session 保存 Vec<Turn>history 保存 ResponseItem,active_turn 只有一个历史 Turn 由 reducer 重建
ActiveTurn::Some 必然正在模型采样task 可暂为空,也可能等待审批检查 RunningTask 和 wait flags
一个 task kind 对应一个 task 类UserShell 复用 Regular看具体 SessionTask 实现/span
task 结束等于历史已持久化flush 可失败并重试同时检查 terminal event 和 rollout
TurnComplete 包含全部 UI itemdelta/item 由独立事件产生用 ThreadHistoryBuilder 归约完整序列
remove thread 等于 shutdown其他 Arc 仍可持有显式关闭并等待 loop 终止

10. 概念模型测试入口 ​

概念关系应由跨层测试反证,而不是只看 struct:

  • core/src/session/tests.rs:root/resumed/subagent session_id、Session 初始化和 active Turn;
  • core/src/tasks/mod_tests.rs:task spawn、abort、terminal event 与 lifecycle;
  • core/tests/suite/turn_state.rs:活动 Turn 的交互状态;
  • core/tests/suite/pending_input.rs:同一 Turn 的 queued/steered input;
  • core/tests/suite/resume.rs、fork_thread.rs:loaded/stored Thread 和新旧 ID;
  • app-server-protocol/src/protocol/thread_history.rs tests:事件/rollout 到 v2 Turn 的 reducer;
  • app-server/tests/suite/thread_*.rs 与 turn tests:公共 JSON-RPC 快照和 status。

一个完整验证案例至少应同时断言两个层次。例如 resume subagent 不仅要检查 Core session.session_id(),还应检查 SessionConfiguredEvent.session_id 或 App Server Thread.sessionId; interrupt 不仅要看到 task token 取消,还要看到 TurnAborted 和恢复后的 TurnStatus::Interrupted。

概念层的几个关键反例已经有直接 arrange/assert:steer_input_requires_active_turn 在没有 active Turn 时提交输入,断言 NoActiveTurn 且不创建 reservation; try_start_turn_if_idle_rejects_pending_trigger_turn_without_injecting 先放入 trigger-turn mailbox, 断言返回 PendingTriggerTurn、原输入原样退回且 active slot 仍为空; queue_only_mailbox_mail_waits_for_next_turn_after_answer_boundary 先把 mailbox 相位切到 NextTurn, 断言 queue-only mail 不会污染当前 Turn;tool_calls_reopen_mailbox_delivery_for_current_turn 则断言 工具调用重新打开当前 Turn 的投递相位。这些测试证明对象层级和 mailbox 相位,不证明任意调度交错都具有相同顺序。

同一个 ThreadId 是否仍是同一个运行对象,应由指针身份而不是 ID 值推断:

源码位置:codex-rs/core/src/thread_manager_tests.rs

rust
// :: resume_stopped_thread_from_rollout_spawns_new_thread(核心断言)
assert_eq!(resumed.thread_id, source.thread_id);
// stopped resume保留业务身份,但创建新的CodexThread/Session owner。
assert!(!Arc::ptr_eq(&resumed.thread, &source.thread));

最终可将概念模型压缩为四句话:ThreadId 标识具体 thread,CodexThread/Session 表达其 loaded runtime;SessionId 标识共享 root 的 agent tree;一次活动 Turn 由 context、slot、task 和 state 协作完成;客户端 Thread/Turn 是 live events 与 persisted history 的投影,而不是 Core 对象的直接 序列化。

继续追创建与恢复生命周期时,分别阅读 ThreadManager创建、 ThreadManager恢复 和 ThreadManager分叉。

可以用下面的只读搜索把本文的 Thread and Turn state 主线落回源码:

bash
rg -n "struct Thread|struct Turn|TurnContext|TurnState|ActiveTurn" codex-rs/core/src