Skip to content

TurnContext字段

逐字段解释 TurnContext 的来源、所有权、生效时机、派生值和持久化投影,并厘清它与 Session、StepContext 的边界。

基于rust-v0.150.0
CodexRustRuntimeTurnContext

TurnContext字段 ​

TurnContext 是一次 Turn 的执行快照:它把 Session 中可更新的设置、模型目录、环境选择、权限、扩展数据 和遥测句柄收拢到一个 Arc 中,交给当前 Task 及其多次模型采样共同使用。它不是历史记录,也不是每次 sampling 都重新生成的请求对象。

建议先阅读 Thread与Turn概念模型,确认业务层的 Thread 与 Turn;再阅读 Session核心数据结构 和 Session设置约束,理解 Session 如何持有可变配置以及哪些设置受约束。本文承担 字段/API 参考角色:逐项回答字段从哪里来、谁消费、何时生效,不重复讲 Turn 主循环和 Step 工具装配。

读完后,应能判断一个异常值应该在 SessionConfiguration、TurnContext 还是请求级 StepContext 中排查, 并能解释为什么“修改 Session 设置”不会改写已经启动的 Turn。

1. TurnContext快照 ​

先把四个生命周期层级分开。Session 配置可以被后续操作更新;TurnContext 固定一次 Turn 的选择;StepContext 固定一次 sampling 的动态依赖;TurnContextItem 则只是写入 rollout 的可恢复投影。

这里有三个容易混淆的不变量:

  1. Arc<TurnContext> 共享的是同一 Turn 的快照,不表示其中 37 个字段都能随时变化;
  2. StepContext.turn 仍指向原 TurnContext,但 MCP binding、ToolRouter、AGENTS.md 等按请求重新捕获;
  3. rollout 不序列化整个 Rust 结构体,只持久化协议类型 TurnContextItem 中恢复真正需要的字段。

源码位置:codex-rs/core/src/session/step_context.rs,符号 StepContext。

rust
/// Request-scoped state that may change between model sampling requests.
pub(crate) struct StepContext {
    // Arc 保证同一个 step 与所属 Turn 共享同一份 TurnContext,而不是复制一套设置。
    pub(crate) turn: Arc<TurnContext>,
    // readiness 可以在 Turn 选择不变的前提下刷新,所以环境状态属于请求级视图。
    pub(crate) environments: TurnEnvironmentSnapshot,
    pub(crate) selected_capability_roots: Vec<ResolvedSelectedCapabilityRoot>,
    pub(crate) executor_capability_discovery:
        Option<Arc<ExecutorCapabilityDiscoverySnapshot>>,
    // MCP binding 与工具路由必须成对冻结,避免“声明旧工具、调用新runtime”。
    pub(crate) mcp: Arc<McpBinding>,
    pub(crate) tool_router: Arc<ToolRouter>,
    pub(crate) loaded_agents_md: Option<Arc<LoadedAgentsMd>>,
}

因此,“某值在下一次模型请求是否会刷新”不能只看它是否出现在 TurnContext。应先问它属于 Turn 选择还是 Step 物化结果:模型、权限、协作模式通常等到新 Turn;MCP binding、工具清单和 AGENTS.md 则可以在同一 Turn 的后续 step 重新捕获。

2. TurnContext 快照 ​

new_turn_with_sub_id 是带本次设置更新的构造入口。它先在 Session 状态锁内验证并提交 SessionSettingsUpdate,再做 MCP/网络副作用,最后才构造 TurnContext。也就是说,构造不是从原始 Config 直接复制字段,而是从已经通过约束的 SessionConfiguration 生成有效视图。

源码位置:codex-rs/core/src/session/turn_context.rs,符号 Session::new_turn_with_sub_id。

rust
pub(crate) async fn new_turn_with_sub_id(
    &self,
    sub_id: String,
    updates: SessionSettingsUpdate,
) -> CodexResult<Arc<TurnContext>> {
    let notify_config_contributors =
        !self.services.extensions.config_contributors().is_empty();
    let update_result: CodexResult<_> = {
        // 先锁住SessionState,使“验证设置”和“发布下一份配置”成为同一临界区。
        let mut state = self.state.lock().await;
        match state.session_configuration.clone().apply(&updates) {
            Ok(next) => {
                let mcp_inputs_changed =
                    state.session_configuration.mcp_inputs_differ(&next);
                let previous_permission_profile =
                    state.session_configuration.permission_profile();
                let next_permission_profile = next.permission_profile();
                let permission_profile_changed =
                    previous_permission_profile != next_permission_profile;
                let previous_config = notify_config_contributors.then(|| {
                    Self::build_effective_session_config(&state.session_configuration)
                });
                let new_config = notify_config_contributors
                    .then(|| Self::build_effective_session_config(&next));

                let environment_config = next.environment_config();
                if updates.environments.is_some() {
                    // 显式环境更新同时改变选择与环境配置;已有Turn仍持有旧snapshot。
                    self.services.turn_environments.update_selections(
                        next.environment_selections(),
                        &environment_config,
                    );
                } else if state.session_configuration.environment_config()
                    != environment_config
                {
                    self.services
                        .turn_environments
                        .update_environment_configs(&environment_config);
                }
                if mcp_inputs_changed {
                    // 这里只标脏;锁外再调度预热,避免在状态锁内等待外部服务。
                    self.mark_mcp_runtime_dirty();
                }
                state.session_configuration = next.clone();
                Ok((
                    next,
                    mcp_inputs_changed,
                    permission_profile_changed,
                    previous_config,
                    new_config,
                ))
            }
            Err(err) => Err(CodexErr::InvalidRequest(err.to_string())),
        }
    };

    let (
        session_configuration,
        mcp_inputs_changed,
        permission_profile_changed,
        previous_config,
        new_config,
    ) = match update_result {
            Ok(update) => update,
            Err(err) => {
                let message = err.to_string();
                // 约束失败时没有半成品TurnContext;调用方先收到同sub_id的BadRequest事件。
                self.send_event_raw(Event {
                    id: sub_id.clone(),
                    msg: EventMsg::Error(ErrorEvent {
                        message: message.clone(),
                        codex_error_info: Some(CodexErrorInfo::BadRequest),
                    }),
                })
                .await;
                return Err(CodexErr::InvalidRequest(message));
            }
        };
    self.emit_config_changed_contributors(previous_config.as_ref(), new_config.as_ref());

    if mcp_inputs_changed {
        self.schedule_mcp_prewarm();
    }
    if permission_profile_changed {
        self.refresh_managed_network_proxy_for_current_permission_profile()
            .await;
    }
    Ok(self
        .new_turn_from_configuration(sub_id, session_configuration,
            updates.final_output_json_schema)
        .await)
}

源码中的 config contributor 通知也保留在摘录中。关键顺序是:约束失败发生在 Session 配置提交之前; contributor 通知、MCP 和网络刷新发生在锁外;只有这些步骤结束后才进入 TurnContext 构造。因而本次更新的权限、环境和模型设置在“新 Turn”生效,不会原地修改正在运行的旧 Turn。

3. 字段所有权 ​

字段清单以源码中的结构体为准。下面先保留完整定义,再按来源与消费者分组;表中的“快照”表示字段值或 Arc 指针在 Turn 创建时确定,不表示它指向的服务永远没有内部状态。

源码位置:codex-rs/core/src/session/turn_context.rs,符号 TurnContext。

rust
/// The context needed for a single turn of the thread.
#[derive(Debug)]
pub struct TurnContext {
    // 第一组把事件、rollout和trace归到同一Turn。
    pub(crate) sub_id: String,
    pub(crate) trace_id: Option<String>,
    pub(crate) realtime_active: bool,
    pub(crate) code_mode_available: bool,

    // Arc<Config>是已物化的per-turn配置,不是Session中可继续更新的配置槽。
    pub config: Arc<Config>,
    pub(crate) auth_manager: Option<Arc<AuthManager>>,
    /// Legacy turn model; step-scoped execution should use `StepContext::model_info`.
    pub(crate) model_info: Arc<ModelInfo>,
    pub(crate) session_telemetry: SessionTelemetry,
    pub(crate) provider: SharedModelProvider,
    /// Legacy turn effort; step-scoped execution should use `StepContext::reasoning_effort`.
    pub(crate) reasoning_effort: Option<ReasoningEffortConfig>,
    /// Legacy turn summary; step-scoped execution should use `StepContext::reasoning_summary`.
    pub(crate) reasoning_summary: ReasoningSummaryConfig,

    // lineage字段描述来源与历史策略;它们不拥有父Thread或历史内容。
    pub(crate) session_source: SessionSource,
    pub(crate) history_mode: ThreadHistoryMode,
    pub(crate) parent_thread_id: Option<ThreadId>,
    pub(crate) originator: String,

    // environments是权威环境选择;cwd仅保留本地兼容投影,已经标记deprecated。
    pub(crate) environments: TurnEnvironmentSnapshot,
    #[deprecated(note = "use the selected turn environment cwd instead")]
    pub(crate) cwd: AbsolutePathBuf,
    pub(crate) current_date: Option<String>,
    pub(crate) timezone: Option<String>,

    pub(crate) app_server_client_name: Option<String>,
    pub(crate) developer_instructions: Option<String>,
    pub(crate) mode: ModeKind,
    pub(crate) collaboration_mode_developer_instructions: Option<String>,
    pub(crate) multi_agent_version: MultiAgentVersion,
    pub(crate) personality: Option<Personality>,

    pub(crate) network: Option<NetworkProxy>,
    pub(crate) windows_sandbox_level: WindowsSandboxLevel,
    pub(crate) available_models: Vec<ModelPreset>,
    pub(crate) unified_exec_shell_mode: UnifiedExecShellMode,
    pub(crate) final_output_json_schema: Option<Value>,
    pub(crate) dynamic_tools: Vec<DynamicToolSpec>,

    // 这四个Arc允许同一Turn的任务共享内部状态;共享不等于跨Turn复用。
    pub(crate) turn_metadata_state: Arc<TurnMetadataState>,
    pub(crate) extension_data: Arc<codex_extension_api::ExtensionData>,
    pub(crate) turn_timing_state: Arc<TurnTimingState>,
    pub(crate) terminal_error: Arc<Mutex<Option<ErrorEvent>>>,

    // 两个AtomicBool是本Turn的一次性发射门闩,避免重复warning/event。
    pub(crate) server_model_warning_emitted: AtomicBool,
    pub(crate) model_verification_emitted: AtomicBool,
}

3.1 身份与运行状态 ​

字段构造来源主要消费者生效与所有权
sub_id调用方传入或 next_internal_sub_id()Event ID、rollout item、扩展 turn store每 Turn 唯一;不是 ThreadId
trace_idcurrent_span_trace_id()TurnStarted、tracing捕获构造时当前 span;可能为 None
realtime_active初值 false,随后查询 conversation running stateworld state、上下文差异构造结束前定值
code_mode_available初值 true,随后查询 CodeModeService模型 warning、工具模式表示该 Turn 构造时服务是否可用
session_sourceSessionConfiguration多代理提示、遥测、权限元数据值快照;不持有来源 Session
history_modeSessionConfiguration历史记录策略值快照
parent_thread_idSessionConfigurationlineage、metadata只保存 ID,不拥有父 Thread
originatorSessionConfigurationMCP、遥测字符串快照

3.2 配置、权限与环境 ​

字段构造来源主要消费者生效与所有权
configbuild_per_turn_config 后包装为 Arc工具、prompt、权限、MCP、压缩当前 Turn 的有效配置
auth_managerSession serviceApps 开关等认证判断共享服务句柄;认证内部状态可变
providerSessionConfiguration模型请求、压缩兼容判断clone 的 provider handle
environmentsTurnEnvironmentManager snapshotStep readiness、cwd、sandbox固定选择,readiness 可在 Step 刷新
cwdprimary 本地环境,否则 Session cwdlegacy sandbox、持久化兼容已弃用;foreign PathUri 不能投影时回退
network当前权限允许时取 managed proxy工具执行/MCPNone 不等于网络策略不存在
windows_sandbox_levelSessionConfigurationfilesystem context、执行器值快照
unified_exec_shell_modefeature、user shell、zsh/wrapper路径联合计算unified exec构造期派生快照
current_date本机时钟环境上下文、rollout构造时的日期,不在 Turn 内自动跨日刷新
timezoneIANA timezone,失败回退 Etc/UTC环境上下文、rollout与 current_date 同时捕获

3.3 模型与工具表面 ​

字段构造来源主要消费者生效与所有权
model_infoModelsManager 按协作模式模型解析context window、prompt、请求参数模型能力的 Turn 快照
reasoning_effortCollaborationMode模型请求、多代理默认值显式值可为空,方法再回退模型默认
reasoning_summarySession 值或 model_info.default_reasoning_summary普通请求、压缩请求构造期解析为非空枚举
available_modelsModelsManager 当前缓存目录spawn-agent 工具说明、review选择普通构造不为目录缺失阻断 Turn
developer_instructionsSessionConfigurationinitial context可选字符串快照
modeCollaborationMode.kindPlan/Default行为、事件与模型/effort共同组成协作模式
collaboration_mode_developer_instructionsCollaborationMode.settingscollaboration_mode() 投影区别于顶层 developer instructions
multi_agent_versionSession固定值或模型默认解析多代理工具与提示正式 Turn 可写回 Session 选择
personalitySessionConfiguration模型指令与 rollout可选值快照
app_server_client_nameSessionConfigurationplugin recommendation只保存名称,不保存完整客户端对象
final_output_json_schema本次设置更新的三态字段sampling request output_schemaNone 表示无schema;更新可显式清除
dynamic_toolsSessionConfigurationToolRegistry规格快照;运行中响应在 TurnState 管理

“预算”不是独立字段。上下文预算由 model_info 的 context window 与有效百分比派生,自动压缩和 token budget 配置则位于 config。因此排查“可用 token 数不对”时,应同时检查 model_info 和 config,不能搜索一个 名为 budget 的 TurnContext 字段。

3.4 遥测与扩展 ​

字段构造来源主要消费者生效与所有权
session_telemetrySessionTelemetry 绑定当前请求/解析模型prompt、token、错误指标clone 后带本 Turn 模型标签
turn_metadata_statethread/session/lineage/cwd/权限创建Responses metadata、MCP metadata本 Turn 共享;可异步补 Git 信息
extension_data以 sub_id 新建并插入 skills/plugin rootsextensions、工具Turn 私有的类型化存储
turn_timing_state每 Turn 新建 defaultsampling、tool、compaction、终态事件内部同步,Task 间共享
terminal_error每 Turn新建 Mutex(None)错误记录、终态选择take() 后只能消费一次
server_model_warning_emittedfalseserver model warningAtomicBool,一次性门闩
model_verification_emittedfalsemodel verification eventAtomicBool,一次性门闩

后四项解释了为什么 TurnContext 不能简单实现 Clone:其中既有需要共享的累计状态,也有应在派生副本中 重新创建的原子门闩。源码只提供语义更窄的 with_model(),明确规定哪些字段共享、替换或重建。

4. Config字段 ​

build_per_turn_config 先克隆原始配置,再用已经解析过的 SessionConfiguration 覆盖 cwd、workspace roots、 权限、推理参数、service tier、personality 与 reviewer,最后按权限和 provider capabilities 解析 web search。 因此 turn_context.config 是“本 Turn 的有效配置”,不是用户配置文件的原样副本。

源码位置:codex-rs/core/src/session/turn_context.rs,符号 Session::build_per_turn_config。

rust
pub(crate) fn build_per_turn_config(
    session_configuration: &SessionConfiguration,
    cwd: AbsolutePathBuf,
) -> Config {
    // 从原配置开始是为了保留大量未迁移字段;下面的覆盖值才是Session权威结果。
    let config = session_configuration.original_config_do_not_use.clone();
    let mut per_turn_config = (*config).clone();
    per_turn_config.cwd = cwd;
    per_turn_config.permissions.approval_policy =
        session_configuration.approval_policy.clone();

    let workspace_roots = session_configuration.primary_workspace_roots();
    per_turn_config.workspace_roots = workspace_roots.clone();
    per_turn_config
        .permissions
        .set_workspace_roots(workspace_roots);

    per_turn_config.model_reasoning_effort =
        session_configuration.collaboration_mode.reasoning_effort();
    per_turn_config.model_reasoning_summary =
        session_configuration.model_reasoning_summary;
    per_turn_config.service_tier = session_configuration.service_tier.clone();
    per_turn_config.personality = session_configuration.personality;
    per_turn_config.approvals_reviewer = session_configuration.approvals_reviewer;

    // profile先物化到permissions,后续消费者无需再猜active profile如何投影。
    session_configuration
        .apply_permission_profile_to_permissions(&mut per_turn_config.permissions);
    let permission_profile = session_configuration.permission_profile();
    let resolved_web_search_mode = resolve_web_search_mode_for_turn(
        &per_turn_config.web_search_mode,
        &permission_profile,
        session_configuration.provider.capabilities(),
    );
    if let Err(err) = per_turn_config.web_search_mode.set(resolved_web_search_mode) {
        // managed requirement拒绝新值时保留受约束值;不会绕过管理策略。
        let fallback_value = per_turn_config.web_search_mode.value();
        tracing::warn!(
            error = %err,
            ?resolved_web_search_mode,
            ?fallback_value,
            "resolved web_search_mode is disallowed by requirements; keeping constrained value"
        );
    }
    per_turn_config.features = config.features.clone();
    per_turn_config
}

注意最后一行重新使用原 config.features。这说明 feature set 仍来自 Session 起始配置,并非本次 SessionSettingsUpdate 任意改写的字段。权限相关 getter 也不是缓存第二份 policy,而是始终从 config.permissions 派生,避免 approval_policy、legacy sandbox 与 split permission profile 漂移。

源码位置:codex-rs/core/src/session/turn_context.rs,符号 TurnContext 的权限与预算派生方法。

rust
pub(crate) fn approval_policy(&self) -> AskForApproval {
    // 返回约束容器中的有效值,而不是原始用户候选值。
    self.config.permissions.approval_policy.value()
}

pub(crate) fn permission_profile(&self) -> PermissionProfile {
    self.config.permissions.effective_permission_profile()
}

pub(crate) fn file_system_sandbox_policy(&self) -> FileSystemSandboxPolicy {
    self.config.permissions.file_system_sandbox_policy()
}

pub(crate) fn network_sandbox_policy(&self) -> NetworkSandboxPolicy {
    self.config.permissions.network_sandbox_policy()
}

pub(crate) fn effective_reasoning_effort(&self) -> Option<ReasoningEffortConfig> {
    // 用户/协作模式未指定时,模型目录默认值在消费时才成为effective effort。
    self.reasoning_effort
        .clone()
        .or_else(|| self.model_info.default_reasoning_level.clone())
}

pub(crate) fn model_context_window(&self) -> Option<i64> {
    let percent = self.model_info.effective_context_window_percent;
    // saturating_mul防止乘百分比时溢出;None继续表示模型窗口未知。
    self.model_info
        .resolved_context_window()
        .map(|window| window.saturating_mul(percent) / 100)
}

这组方法也给出调试顺序:先看 config.permissions 的有效结果,再看原始配置层;先看 model_context_window() 的派生值,再分别检查模型窗口与百分比。绕过这些 getter 直接比较原始输入,常会把 约束生效或模型默认误判为字段丢失。

5. 环境与legacy cwd ​

环境是当前版本最容易读错的一组字段。TurnEnvironmentSnapshot 可以包含本地或 foreign 环境,每个环境 自带 PathUri cwd、workspace roots、shell 和权限配置;旧 cwd: AbsolutePathBuf 只能表达本机绝对路径, 所以它已被标记为 deprecated。

源码位置:codex-rs/core/src/session/turn_context.rs,符号 Session::new_turn_context_from_configuration。

rust
let turn_environments = self.services.turn_environments.snapshot().await;
let primary_turn_environment = turn_environments.primary();

// 只有primary环境的PathUri能转成本地绝对路径时,legacy cwd才跟随该环境。
let cwd = primary_turn_environment
    .as_ref()
    .and_then(|turn_environment| turn_environment.cwd().to_abs_path().ok())
    // foreign环境或空选择回退Session cwd;这不改变environments中的权威选择。
    .unwrap_or_else(|| session_configuration.cwd().clone());

let per_turn_config =
    Self::build_per_turn_config(&session_configuration, cwd.clone());
let model_info = self
    .services
    .models_manager
    .get_model_info(
        session_configuration.collaboration_mode.model(),
        &per_turn_config.to_models_manager_config(),
    )
    .await;

// plugins与skills都基于这份per-turn config和primary filesystem生成Turn快照。
let plugins_input = per_turn_config.plugins_config_input();
let plugin_outcome = self
    .services
    .plugins_manager
    .plugins_for_config(&plugins_input)
    .await;
let effective_skill_roots = plugin_outcome.effective_plugin_skill_roots();
let plugin_skill_snapshots = self
    .services
    .plugins_manager
    .plugin_skill_snapshots_for_config(&plugins_input);
let skills_input = skills_load_input_from_config(
    &per_turn_config,
    effective_skill_roots,
)
    .with_plugin_skill_snapshots(plugin_skill_snapshots);
let fs = primary_turn_environment
    .map(|turn_environment| turn_environment.environment.get_filesystem());
let skills_snapshot = self
    .services
    .skills_service
    .snapshot_for_config(&skills_input, fs)
    .await;

let mut turn_context = Self::make_turn_context(
    // ...
    per_turn_config,
    model_info,
    // ...
    turn_environments,
    cwd,
    sub_id,
    skills_snapshot,
);

所以 turn_context.cwd == turn_context.config.cwd 只能说明 legacy 本地投影一致,不能说明 environments.primary().cwd() 一定是本地路径。执行器和新代码应优先消费选中环境;只有兼容旧 sandbox、 rollout schema 或尚未迁移的调用方才读取 deprecated cwd。

6. TurnContext构造 ​

make_turn_context 负责纯粹的字段装配,但部分值必须等 Session 服务查询完成后覆盖。把这两段代码分开读, 才能理解 false/true/None 哪些是最终语义,哪些只是构造默认值。

源码位置:codex-rs/core/src/session/turn_context.rs,符号 Session::make_turn_context。

rust
let reasoning_effort = collaboration_mode.reasoning_effort();
let reasoning_summary = session_configuration
    .model_reasoning_summary
    // Session没有显式值时,在构造期解析成模型默认,不把None留给消费者重复判断。
    .unwrap_or(model_info.default_reasoning_summary);
let available_models = models_manager.try_list_models().unwrap_or_default();
let unified_exec_shell_mode = UnifiedExecShellMode::for_session(
    codex_tools::unified_exec_feature_mode_for_features(per_turn_config.features.get()),
    crate::tools::tool_user_shell_type(user_shell),
    shell_zsh_path,
    main_execve_wrapper_exe,
);

let mut per_turn_config = per_turn_config;
super::token_budget::apply_model_defaults(&mut per_turn_config, &model_info);
per_turn_config.service_tier = get_service_tier(
    per_turn_config.service_tier,
    per_turn_config.features.enabled(Feature::FastMode),
    &model_info,
);
let permission_profile = per_turn_config.permissions.effective_permission_profile();
let per_turn_config = Arc::new(per_turn_config);

// metadata、extension、timing与terminal error都是新的Turn所有者,不跨Turn复用。
let turn_metadata_state = Arc::new(TurnMetadataState::new(
    session_id.to_string(),
    thread_id.to_string(),
    session_configuration.forked_from_thread_id,
    session_configuration.parent_thread_id,
    &session_configuration.session_source,
    session_configuration.thread_source.clone(),
    sub_id.clone(),
    cwd.clone(),
    &permission_profile,
    session_configuration.windows_sandbox_level,
    network.is_some(),
));
let extension_data = Arc::new(
    codex_extension_api::ExtensionData::new(sub_id.clone())
);
extension_data.insert(skills_snapshot);

TurnContext {
    sub_id,
    trace_id: current_span_trace_id(),
    realtime_active: false,
    code_mode_available: true,
    config: per_turn_config,
    // ...
    available_models,
    unified_exec_shell_mode,
    final_output_json_schema: None,
    dynamic_tools: session_configuration.dynamic_tools.clone(),
    turn_metadata_state,
    extension_data,
    turn_timing_state: Arc::new(TurnTimingState::default()),
    terminal_error: Arc::new(Mutex::new(None)),
    server_model_warning_emitted: AtomicBool::new(false),
    model_verification_emitted: AtomicBool::new(false),
}

源码位置:codex-rs/core/src/session/turn_context.rs,符号 Session::new_turn_context_from_configuration 的后置赋值。

rust
let mut turn_context: TurnContext = Self::make_turn_context(
    // ...前置身份、service与配置参数
    turn_environments,
    cwd,
    sub_id,
    skills_snapshot,
);

// true只是make_turn_context的乐观默认;最终值来自实际CodeModeService。
turn_context.code_mode_available = self.services.code_mode_service.is_available();
turn_context.extension_data.insert(trusted_plugin_roots);
// realtime状态在返回Arc之前读取,保证Task拿到的是构造时一致值。
turn_context.realtime_active = self.conversation.running_state().await.is_some();

if let Some(final_schema) = final_output_json_schema {
    // Option<Option<Value>>允许“未更新”和“显式清除schema”使用不同语义。
    turn_context.final_output_json_schema = final_schema;
}
let turn_context = Arc::new(turn_context);

if git_enrichment_policy == GitEnrichmentPolicy::Fresh
    && turn_context.environments.single_local_environment_cwd().is_some()
{
    // enrichment可异步写入TurnMetadataState,不延迟TurnContext本体发布。
    turn_context.turn_metadata_state.spawn_git_enrichment_task();
}
turn_context

available_models 采用 try_list_models().unwrap_or_default(),说明模型目录缓存缺失不是 Turn 构造失败条件; 真正用于本 Turn 的 model_info 则通过异步 get_model_info() 解析。二者用途不同:前者主要给多代理/审核选择 展示候选,后者决定当前请求能力、context window 与默认 reasoning。

7. with_model派生 ​

预采样压缩可能需要用旧模型处理已有上下文。源码没有修改当前 Arc<TurnContext>,而是调用 with_model() 构造一个派生对象:更新 config/model/telemetry/reasoning,保留同一 Turn 身份和共享累计状态。

源码位置:codex-rs/core/src/session/turn_context.rs,符号 TurnContext::with_model。

rust
pub(crate) async fn with_model(
    &self,
    model: String,
    models_manager: &SharedModelsManager,
) -> Self {
    let mut config = (*self.config).clone();
    config.model = Some(model.clone());
    let model_info = models_manager
        .get_model_info(model.as_str(), &config.to_models_manager_config())
        .await;

    let supported_reasoning_levels = model_info
        .supported_reasoning_levels
        .iter()
        .map(|preset| preset.effort.clone())
        .collect::<Vec<_>>();
    let reasoning_effort = if let Some(current_reasoning_effort) = self.reasoning_effort.clone() {
        if supported_reasoning_levels.contains(&current_reasoning_effort) {
            Some(current_reasoning_effort)
        } else {
            // 旧effort不被新模型支持时选择中位preset,再回退模型默认,避免发送非法参数。
            supported_reasoning_levels
                .get(supported_reasoning_levels.len().saturating_sub(1) / 2)
                .cloned()
                .or_else(|| model_info.default_reasoning_level.clone())
        }
    } else {
        supported_reasoning_levels
            .get(supported_reasoning_levels.len().saturating_sub(1) / 2)
            .cloned()
            .or_else(|| model_info.default_reasoning_level.clone())
    };
    config.model_reasoning_effort = reasoning_effort.clone();

    let available_models = models_manager
        .list_models(
            RefreshStrategy::OnlineIfUncached,
            config.http_client_factory(),
        )
        .await;

    Self {
        sub_id: self.sub_id.clone(),
        trace_id: self.trace_id.clone(),
        realtime_active: self.realtime_active,
        code_mode_available: self.code_mode_available,
        config: Arc::new(config),
        auth_manager: self.auth_manager.clone(),
        model_info: model_info.clone(),
        session_telemetry: self
            .session_telemetry
            .clone()
            .with_model(model.as_str(), model_info.slug.as_str()),
        provider: self.provider.clone(),
        reasoning_effort,
        reasoning_summary: self.reasoning_summary,
        session_source: self.session_source.clone(),
        history_mode: self.history_mode,
        parent_thread_id: self.parent_thread_id,
        originator: self.originator.clone(),
        environments: self.environments.clone(),
        #[allow(deprecated)]
        cwd: self.cwd.clone(),
        current_date: self.current_date.clone(),
        timezone: self.timezone.clone(),
        app_server_client_name: self.app_server_client_name.clone(),
        developer_instructions: self.developer_instructions.clone(),
        mode: self.mode,
        collaboration_mode_developer_instructions: self
            .collaboration_mode_developer_instructions
            .clone(),
        multi_agent_version: self.multi_agent_version,
        personality: self.personality,
        network: self.network.clone(),
        windows_sandbox_level: self.windows_sandbox_level,
        available_models,
        unified_exec_shell_mode: self.unified_exec_shell_mode.clone(),
        final_output_json_schema: self.final_output_json_schema.clone(),
        dynamic_tools: self.dynamic_tools.clone(),
        // 这些Arc继续累计同一Turn的元数据、计时和终态错误。
        turn_metadata_state: self.turn_metadata_state.clone(),
        extension_data: Arc::clone(&self.extension_data),
        turn_timing_state: Arc::clone(&self.turn_timing_state),
        terminal_error: Arc::clone(&self.terminal_error),
        // AtomicBool不能直接clone,只复制当前值到新的原子变量。
        server_model_warning_emitted: AtomicBool::new(
            self.server_model_warning_emitted.load(Ordering::Relaxed)),
        model_verification_emitted: AtomicBool::new(
            self.model_verification_emitted.load(Ordering::Relaxed)),
    }
}

这不是“切换当前 Turn 的模型”通用 API,而是生成保持 Turn 身份的模型特化视图。若仅修改 model_info 而不 同步 config.model、telemetry 与 reasoning effort,模型请求、事件标签和协作模式会互相矛盾;该函数集中 维护了这组不变量。

8. 持久化快照 ​

to_turn_context_item() 将 37 个运行时字段压缩为协议层快照。服务句柄、插件根、timing、terminal error、 动态工具运行状态都不会被序列化;恢复只需要重建“当时模型看到的设置基线”,运行时资源由新 Session 重新装配。

这张关系图表达的是数据基数,不是状态机:一个 Session 配置会产生多个 TurnContext;一个 Turn 可以有多次 sampling;每次需要建立 durable baseline 时,当前 Turn 投影成一条 TurnContextItem。

源码位置:codex-rs/core/src/session/turn_context.rs,符号 TurnContext::to_turn_context_item。

rust
pub(crate) fn to_turn_context_item(&self) -> TurnContextItem {
    let workspace_roots = self.config.effective_workspace_roots();
    #[allow(deprecated)]
    let cwd = self.cwd.clone();
    TurnContextItem {
        turn_id: Some(self.sub_id.clone()),
        cwd,
        workspace_roots: (!workspace_roots.is_empty()).then_some(workspace_roots),
        current_date: self.current_date.clone(),
        timezone: self.timezone.clone(),
        approval_policy: self.approval_policy(),
        approvals_reviewer: Some(self.config.approvals_reviewer),
        sandbox_policy: self.sandbox_policy(),
        permission_profile: Some(self.permission_profile()),
        network: self.turn_context_network_item(),
        // split policy只有在不能由legacy policy等价重建时才额外持久化,保持payload稳定。
        file_system_sandbox_policy: self.non_legacy_file_system_sandbox_policy(),
        model: self.model_info.slug.clone(),
        comp_hash: self.model_info.comp_hash.clone(),
        personality: self.personality,
        collaboration_mode: Some(self.collaboration_mode()),
        multi_agent_version: Some(self.multi_agent_version),
        multi_agent_mode: None,
        realtime_active: Some(self.realtime_active),
        effort: self.reasoning_effort.clone(),
        // 当前协议投影固定为Auto;运行时reasoning_summary仍用于实际模型请求。
        summary: ReasoningSummaryConfig::Auto,
    }
}

持久化逻辑还区分“模型可见上下文变化”和“只需要推进恢复基线”。即使没有向 conversation history 添加 diff,也要写入新的 TurnContextItem,否则 resume 会错误沿用前一 Turn 的设置。

源码位置:codex-rs/core/src/session/mod.rs,符号 Session::record_context_updates_and_set_reference_context_item。

rust
// ...上文已生成turn_context_item、context_items、world_state_item与world_state
let only_world_state_changed = !turn_context_changed && context_items.is_empty();
if only_world_state_changed && world_state_item.is_none() {
    return Ok(world_state);
}
if !context_items.is_empty() {
    // 先记录由快照变化生成的模型可见消息,再推进持久化baseline。
    self.record_conversation_items(turn_context, &context_items)
        .await;
}
if let Some(world_state_item) = world_state_item {
    self.persist_rollout_items(&[RolloutItem::WorldState(world_state_item)])
        .await;
}
if only_world_state_changed {
    return Ok(world_state);
}

// 即使没有模型可见diff,每个真实用户Turn仍持久化最新设置快照。
self.persist_rollout_items(&[RolloutItem::TurnContext(turn_context_item.clone())])
    .await;

let mut state = self.state.lock().await;
state.set_reference_context_item(Some(turn_context_item));

顺序也有意义:先把由新状态生成的模型可见 context 写入历史,再持久化 WorldState/TurnContext baseline, 最后推进内存 reference。若先推进 reference 而后续持久化失败,恢复逻辑会以为新基线已经可靠落盘。

9. TurnContext测试 ​

字段参考不能只停留在“结构体里有这个字段”,还要说明快照何时变化、哪些值必须一起变化、持久化是否保留。

9.1 旧Turn权限 ​

源码位置:codex-rs/core/src/session/tests.rs,测试 permission_profile_updates_apply_to_next_turn_environment。

rust
let (session, active_turn) = make_session_and_context().await;
let active_environment_config = active_turn
    .environments
    .primary()
    .expect("active turn environment")
    .config
    .clone();

let profile_root = active_turn.config.cwd.join("profile-root");
let active_profile = ActivePermissionProfile::read_only();
let updates = SessionSettingsUpdate {
    permission_profile: Some(PermissionProfile::read_only()),
    active_permission_profile: Some(active_profile.clone()),
    profile_workspace_roots: Some(vec![profile_root.clone()]),
    ..Default::default()
};
// ...测试还以update_settings入口运行同一组断言
let next_turn = session
    .new_turn_with_sub_id("permission-profile-update".to_string(), updates)
    .await
    .expect("turn permission profile update should succeed");

let next_environment = next_turn
    .environments
    .primary()
    .expect("next turn environment");
let mut expected_environment_config = active_environment_config.clone();
expected_environment_config.permission_profile =
    PermissionProfileSnapshot::active_with_profile_workspace_roots(
        PermissionProfile::read_only(),
        active_profile,
        vec![profile_root],
    );

// 新Turn拿到read-only环境配置。
assert_eq!(next_environment.config, expected_environment_config);
// 旧Arc<TurnContext>仍保存原配置,说明更新边界是next Turn而非即时覆盖。
assert_eq!(
    active_turn
        .environments
        .primary()
        .expect("active turn environment")
        .config,
    active_environment_config
);

测试同时覆盖 update_settings 后再创建默认 Turn 和在 new_turn_with_sub_id 中直接更新两种入口;两种路径 都只影响下一份快照。这是定位“权限为什么没有在当前运行中立刻改变”的直接证据。

9.2 模型派生保持关联 ​

源码位置:codex-rs/core/src/session/tests.rs,测试 turn_context_with_model_updates_model_fields。

rust
let (session, mut turn_context) = make_session_and_context().await;
turn_context.reasoning_effort = Some(ReasoningEffortConfig::Minimal);
let updated = turn_context
    .with_model("gpt-5.4".to_string(), &session.services.models_manager)
    .await;

// 断言不是只看model_info,而是检查Config、协作模式和effort三条消费路径一致。
assert_eq!(updated.config.model.as_deref(), Some("gpt-5.4"));
assert_eq!(updated.collaboration_mode().model(), "gpt-5.4");
assert_eq!(updated.model_info, expected_model_info);
assert_eq!(updated.reasoning_effort,
    Some(ReasoningEffortConfig::Medium));
assert_eq!(updated.collaboration_mode().reasoning_effort(),
    Some(ReasoningEffortConfig::Medium));
assert_eq!(updated.config.model_reasoning_effort,
    Some(ReasoningEffortConfig::Medium));

输入故意给出新模型不支持的 Minimal,最终断言为 Medium。它说明 with_model 会重新约束 effort,而非 盲目复制旧值;也说明模型切换必须原子地更新多个关联字段。

9.3 Durable推进 ​

源码位置:codex-rs/core/src/session/tests.rs,测试 record_context_updates_and_set_reference_context_item_persists_baseline_without_emitting_diffs。

rust
let (mut session, turn_context) = make_session_and_context().await;
let previous_context_item = turn_context.to_turn_context_item();
let previous_context = Arc::new(turn_context);
let world_state = build_world_state_from_turn_context(&session, &previous_context).await;
let retained_world_state = world_state
    .render_full()
    .into_iter()
    .map(ContextualUserFragment::into_boxed_response_item)
    .collect::<Vec<_>>();
session
    .replace_history(
        retained_world_state.clone(),
        Some(previous_context_item.clone()),
    )
    .await;

let mut turn_context = Arc::try_unwrap(previous_context)
    .unwrap_or_else(|_| panic!("previous turn context should have no remaining references"));
turn_context.sub_id = format!("{}-next", turn_context.sub_id);
{
    let mut state = session.state.lock().await;
    state
        .history
        .set_world_state_baseline(world_state.snapshot());
}
let rollout_path = attach_thread_persistence(&mut session).await;

let turn_context = Arc::new(turn_context);
let step_context = StepContext::for_test(Arc::clone(&turn_context));

session
    .record_context_updates_and_set_reference_context_item(&step_context)
    .await
    .expect("world state should build");

// 没有环境或prompt差异,因此conversation history保持原样。
assert_eq!(
    session.clone_history().await.raw_items().to_vec(),
    retained_world_state
);
// 内存reference必须前进到新turn_id,即使没有新增模型可见消息。
assert_eq!(
    serde_json::to_value(session.reference_context_item().await)
        .expect("serialize current context item"),
    serde_json::to_value(Some(turn_context.to_turn_context_item()))
        .expect("serialize expected context item")
);
session.ensure_rollout_materialized().await;
session.flush_rollout().await.expect("rollout should flush");

let InitialHistory::Resumed(resumed) = RolloutRecorder::get_rollout_history(&rollout_path)
    .await
    .expect("read rollout history")
else {
    panic!("expected resumed rollout history");
};
let persisted_turn_context = resumed.history.iter().find_map(|item| match item {
    RolloutItem::TurnContext(ctx) => Some(ctx.clone()),
    _ => None,
});
// rollout中的durable snapshot也必须等于当前Turn投影。
assert_eq!(
    serde_json::to_value(persisted_turn_context)
        .expect("serialize persisted turn context item"),
    serde_json::to_value(Some(turn_context.to_turn_context_item()))
        .expect("serialize expected turn context item")
);

这个测试防止一种隐蔽错误:把“没有要发给模型的新文本”等同于“没有状态变化”。新 Turn 即使设置值完全 相同,turn_id 仍变化,durable reference 必须前进。

10. 字段层级定位 ​

还有三条具体失败语义需要保留:

  • SessionConfiguration::apply 违反 managed constraints 时,返回 InvalidRequest 并发送 BadRequest ErrorEvent;此时没有 TurnContext,也没有“部分字段已生效”的对象。
  • IANA timezone 获取失败时,日期改用 UTC 且 timezone 为 Etc/UTC;这是明确降级,不会阻止 Turn。
  • 模型目录找不到精确 metadata 时,Turn 可使用 fallback ModelInfo,随后发送 Warning;字段存在不代表能力 信息一定来自精确目录项。

可以用下面两个只读练习检验理解:从 new_turn_with_sub_id 开始,复述权限更新到旧/新 Turn 环境快照的 分界;再任选 final_output_json_schema 或 dynamic_tools,沿构造字段追到 sampling request 或 ToolRegistry 消费者。若问题落在每次模型请求重新捕获的 MCP、AGENTS.md 和 ToolRouter,应停止在本文继续扩展字段表, 转入 StepContext模型请求;它解释请求级快照如何同时约束 Prompt 与工具 执行。

可以用下面的只读搜索把本文的 TurnContext snapshot 主线落回源码:

bash
rg -n "TurnContext|make_turn_context|durable|history" codex-rs/core/src