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 的可恢复投影。
这里有三个容易混淆的不变量:
Arc<TurnContext>共享的是同一 Turn 的快照,不表示其中 37 个字段都能随时变化;StepContext.turn仍指向原 TurnContext,但 MCP binding、ToolRouter、AGENTS.md 等按请求重新捕获;- rollout 不序列化整个 Rust 结构体,只持久化协议类型
TurnContextItem中恢复真正需要的字段。
源码位置:codex-rs/core/src/session/step_context.rs,符号 StepContext。
/// 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。
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。
/// 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_id | current_span_trace_id() | TurnStarted、tracing | 捕获构造时当前 span;可能为 None |
realtime_active | 初值 false,随后查询 conversation running state | world state、上下文差异 | 构造结束前定值 |
code_mode_available | 初值 true,随后查询 CodeModeService | 模型 warning、工具模式 | 表示该 Turn 构造时服务是否可用 |
session_source | SessionConfiguration | 多代理提示、遥测、权限元数据 | 值快照;不持有来源 Session |
history_mode | SessionConfiguration | 历史记录策略 | 值快照 |
parent_thread_id | SessionConfiguration | lineage、metadata | 只保存 ID,不拥有父 Thread |
originator | SessionConfiguration | MCP、遥测 | 字符串快照 |
3.2 配置、权限与环境
| 字段 | 构造来源 | 主要消费者 | 生效与所有权 |
|---|---|---|---|
config | build_per_turn_config 后包装为 Arc | 工具、prompt、权限、MCP、压缩 | 当前 Turn 的有效配置 |
auth_manager | Session service | Apps 开关等认证判断 | 共享服务句柄;认证内部状态可变 |
provider | SessionConfiguration | 模型请求、压缩兼容判断 | clone 的 provider handle |
environments | TurnEnvironmentManager snapshot | Step readiness、cwd、sandbox | 固定选择,readiness 可在 Step 刷新 |
cwd | primary 本地环境,否则 Session cwd | legacy sandbox、持久化兼容 | 已弃用;foreign PathUri 不能投影时回退 |
network | 当前权限允许时取 managed proxy | 工具执行/MCP | None 不等于网络策略不存在 |
windows_sandbox_level | SessionConfiguration | filesystem context、执行器 | 值快照 |
unified_exec_shell_mode | feature、user shell、zsh/wrapper路径联合计算 | unified exec | 构造期派生快照 |
current_date | 本机时钟 | 环境上下文、rollout | 构造时的日期,不在 Turn 内自动跨日刷新 |
timezone | IANA timezone,失败回退 Etc/UTC | 环境上下文、rollout | 与 current_date 同时捕获 |
3.3 模型与工具表面
| 字段 | 构造来源 | 主要消费者 | 生效与所有权 |
|---|---|---|---|
model_info | ModelsManager 按协作模式模型解析 | context window、prompt、请求参数 | 模型能力的 Turn 快照 |
reasoning_effort | CollaborationMode | 模型请求、多代理默认值 | 显式值可为空,方法再回退模型默认 |
reasoning_summary | Session 值或 model_info.default_reasoning_summary | 普通请求、压缩请求 | 构造期解析为非空枚举 |
available_models | ModelsManager 当前缓存目录 | spawn-agent 工具说明、review选择 | 普通构造不为目录缺失阻断 Turn |
developer_instructions | SessionConfiguration | initial context | 可选字符串快照 |
mode | CollaborationMode.kind | Plan/Default行为、事件 | 与模型/effort共同组成协作模式 |
collaboration_mode_developer_instructions | CollaborationMode.settings | collaboration_mode() 投影 | 区别于顶层 developer instructions |
multi_agent_version | Session固定值或模型默认解析 | 多代理工具与提示 | 正式 Turn 可写回 Session 选择 |
personality | SessionConfiguration | 模型指令与 rollout | 可选值快照 |
app_server_client_name | SessionConfiguration | plugin recommendation | 只保存名称,不保存完整客户端对象 |
final_output_json_schema | 本次设置更新的三态字段 | sampling request output_schema | None 表示无schema;更新可显式清除 |
dynamic_tools | SessionConfiguration | ToolRegistry | 规格快照;运行中响应在 TurnState 管理 |
“预算”不是独立字段。上下文预算由 model_info 的 context window 与有效百分比派生,自动压缩和 token budget 配置则位于 config。因此排查“可用 token 数不对”时,应同时检查 model_info 和 config,不能搜索一个 名为 budget 的 TurnContext 字段。
3.4 遥测与扩展
| 字段 | 构造来源 | 主要消费者 | 生效与所有权 |
|---|---|---|---|
session_telemetry | SessionTelemetry 绑定当前请求/解析模型 | prompt、token、错误指标 | clone 后带本 Turn 模型标签 |
turn_metadata_state | thread/session/lineage/cwd/权限创建 | Responses metadata、MCP metadata | 本 Turn 共享;可异步补 Git 信息 |
extension_data | 以 sub_id 新建并插入 skills/plugin roots | extensions、工具 | Turn 私有的类型化存储 |
turn_timing_state | 每 Turn 新建 default | sampling、tool、compaction、终态事件 | 内部同步,Task 间共享 |
terminal_error | 每 Turn新建 Mutex(None) | 错误记录、终态选择 | take() 后只能消费一次 |
server_model_warning_emitted | false | server model warning | AtomicBool,一次性门闩 |
model_verification_emitted | false | model verification event | AtomicBool,一次性门闩 |
后四项解释了为什么 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。
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 的权限与预算派生方法。
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。
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。
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 的后置赋值。
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_contextavailable_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。
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(¤t_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。
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。
// ...上文已生成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。
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。
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。
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并发送BadRequestErrorEvent;此时没有 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 主线落回源码:
rg -n "TurnContext|make_turn_context|durable|history" codex-rs/core/src