Session启动上下文
Codex 的首轮 Prompt 不是一段巨大的字符串。它由两个彼此独立的平面组成:BaseInstructions 通过 Responses API 的顶层 instructions 字段发送;其余 developer、contextual user、真实用户消息、工具调用和 恢复 transcript 则作为 ResponseItem 序列进入 input。工具规格又单独进入 Prompt.tools。
“Session 启动上下文”指的是首个真实 Turn 在已有 transcript 前后建立的模型可见前缀。它不在 Session::spawn() 中一次性写死:Core 会等 turn/start 覆盖合入 SessionConfiguration、环境 readiness 刷新、AGENTS.md 重载和 MCP/tool snapshot 捕获完成后,才决定注入完整上下文还是相对恢复基线发送 diff。
阅读前先看 Session启动预热 区分“提前准备”和“正确性消费点”,并用 Session核心数据结构 理解跨Turn history owner。本文只研究模型可见 上下文的构造与恢复,不展开输入并发和 Op 分派。
1. Prompt三通道
下面的图先拆开最容易混淆的三种输入。蓝色路径是请求级基础指令,绿色路径是有顺序的历史 input, 橙色路径是模型可调用工具;它们最后汇入同一个 Prompt,但不会互相拼接成单一文本。
源码中的 Prompt 直接保留了这些边界:
源码位置:codex-rs/core/src/client_common.rs :: Prompt
#[derive(Debug, Clone)]
pub struct Prompt {
/// Conversation context input items.
// developer、contextual user、真实消息与历史工具记录都在这个有序序列中。
pub input: Vec<ResponseItem>,
/// Tools available to the model, including additional tools sourced from
/// external MCP servers.
// 工具规格不是文本前缀,而是独立的结构化请求字段。
pub(crate) tools: Vec<ToolSpec>,
pub(crate) parallel_tool_calls: bool,
// 基础指令与input分离,后续历史裁剪不会把它当普通ResponseItem删除。
pub base_instructions: BaseInstructions,
pub output_schema: Option<Value>,
pub output_schema_strict: bool,
}因此,看到历史第一条 developer message 并不意味着它就是系统基础指令;看到 user role 也不一定代表 用户发起了新 Turn。AGENTS.md 和 <environment_context> 都使用 user role,但由标记和解析规则识别为 上下文,不形成真实用户边界。
2. 启动上下文
InitialHistory 有 New、Cleared、Resumed 和 Forked 四种形态。New/Cleared 没有旧 transcript;Resumed 读取原 Thread rollout;Forked 接收已经截取过的 rollout items。Core 会在构造尾部调用 record_initial_history(),但所有分支都把“当前首轮上下文”推迟到真实 Turn。
源码位置:codex-rs/protocol/src/protocol.rs :: InitialHistory
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub enum InitialHistory {
New,
Cleared,
// Resume保留原ThreadId、rollout items与可选物理路径。
Resumed(ResumedHistory),
// Fork只携带调用方选择保留的rollout前缀。
Forked(Vec<RolloutItem>),
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct ResumedHistory {
pub conversation_id: ThreadId,
pub history: Arc<Vec<RolloutItem>>,
pub rollout_path: Option<PathBuf>,
}New/Cleared 分支显式保持空历史和空 settings baseline。Resumed/Forked 则先用一个默认 TurnContext 解释 rollout,恢复 transcript、上一轮设置、reference context、WorldState baseline 和 context-window ID;它们 同样不会立即把当前 AGENTS.md 或环境内容追加到历史末尾。
源码位置:codex-rs/core/src/session/mod.rs :: Session::record_initial_history(节选)
match conversation_history {
InitialHistory::New | InitialHistory::Cleared => {
// Defer initial context insertion until the first real turn starts so
// turn/start overrides can be merged before we write model-visible context.
// 此时ContextManager仍为空,reference_context_item也没有建立。
self.set_previous_turn_settings(/*previous_turn_settings*/ None)
.await;
}
InitialHistory::Resumed(resumed_history) => {
let turn_context = self.new_default_turn().await;
let rollout_items = resumed_history.history;
// 重建历史及diff基线,但不在这里生成当前版本的initial context。
let previous_turn_settings = self
.apply_rollout_reconstruction(&turn_context, &rollout_items)
.await;
let curr: &str = turn_context.model_info.slug.as_str();
if let Some(prev) = previous_turn_settings
.as_ref()
.map(|settings| settings.model.as_str())
.filter(|model| *model != curr)
{
// 模型变化先作为客户端warning报告,模型可见的切换指令稍后由WorldState生成。
warn!("resuming session with different model: previous={prev}, current={curr}");
}
if let Some(info) = Self::last_token_info_from_rollout(&rollout_items) {
let mut state = self.state.lock().await;
state.set_token_info(Some(info));
}
if !is_subagent {
let _ = self.flush_rollout().await;
}
}
InitialHistory::Forked(mut rollout_items) => {
let turn_context = self.new_default_turn().await;
// Fork历史缺失ResponseItem ID时先补齐,保证后续持久化和配对稳定。
Self::assign_missing_rollout_response_item_ids(&mut rollout_items);
self.apply_rollout_reconstruction(&turn_context, &rollout_items)
.await;
// 不同ForkPersistence只改变继承记录如何落盘,不改变首轮延迟注入原则。
// 此处省略Referenced、Paginated Copied与普通Copied的持久化分支。
}
}推迟是必要的,因为 App Server 的 turn/start 可以覆盖 model、cwd/environment、approval、sandbox、 collaboration mode、personality 等字段。如果构造时就把环境与权限文本写入历史,首轮请求可能同时包含 旧上下文和新配置。
3. TurnContext 快照
new_turn_with_sub_id() 先在 Session state 锁内把 SessionSettingsUpdate 应用到 configuration;更新环境 selection 或权限时还会同步刷新相关 runtime。随后 new_turn_context_from_configuration() 才解析主环境 cwd、模型元数据、Plugin/Skill snapshot 和网络代理,最终构造不可变的 TurnContext。
源码位置: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();
let cwd = primary_turn_environment
.as_ref()
.and_then(|turn_environment| turn_environment.cwd().to_abs_path().ok())
// 当前兼容路径在foreign cwd无法转为本地绝对路径时回退Session cwd。
.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;
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());
// 每个Turn保存自己的Skill snapshot,启动期warmup并不替代这里的准确读取。
let skills_snapshot = self
.services
.skills_service
.snapshot_for_config(&skills_input, fs)
.await;make_turn_context() 把 Session 级策略物化为 Turn 级字段,并把 Skill snapshot 放进 Turn extension store。 这里的时间与 timezone 是创建 Turn 时的本地快照;真正渲染 environment context 时,当前日期会再次通过 TimeProvider 获取,避免长生命周期 Session 永远显示启动日。
源码位置:codex-rs/core/src/session/turn_context.rs :: Session::make_turn_context(节选)
let collaboration_mode = &session_configuration.collaboration_mode;
let reasoning_effort = collaboration_mode.reasoning_effort();
let reasoning_summary = session_configuration
.model_reasoning_summary
.unwrap_or(model_info.default_reasoning_summary);
let permission_profile = per_turn_config.permissions.effective_permission_profile();
let per_turn_config = Arc::new(per_turn_config);
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 (current_date, timezone) = local_time_context();
let extension_data = Arc::new(ExtensionData::new(sub_id.clone()));
// 后续Skill注入只读取这个Turn捕获的HostSkillsSnapshot。
extension_data.insert(skills_snapshot);
TurnContext {
sub_id,
config: per_turn_config,
model_info,
environments,
#[allow(deprecated)]
cwd,
current_date: Some(current_date),
timezone: Some(timezone),
developer_instructions: session_configuration.developer_instructions.clone(),
mode: collaboration_mode.mode,
collaboration_mode_developer_instructions: collaboration_mode
.settings
.developer_instructions
.clone(),
personality: session_configuration.personality,
dynamic_tools: session_configuration.dynamic_tools.clone(),
turn_metadata_state,
extension_data,
// 其余认证、推理、网络、schema、telemetry与终止状态字段省略。
}下面的对象图强调三个 snapshot 的边界。TurnContext 冻结本轮设置,StepContext 冻结一次模型 step 的动态 依赖,WorldState 则是由同一个 StepContext 计算出的模型可见状态。
恢复时,reference_context_item 与 world_state_baseline 是 ContextManager 的两个独立基线。 基线完整时只发送差异;基线不存在、被 rollback 清除或当前环境身份变化时,build_world_state_for_step() 会重新生成完整 context。恢复历史不是把旧环境文本原样复用,而是结合当前环境和扩展快照重新计算模型可见前缀。
4. StepContext
首轮 run_turn() 不直接拿 TurnContext 构建 Prompt。它先解析用户输入中显式要求的 MCP server,再调用 capture_step_context_with_required_mcp_servers()。该函数刷新环境 readiness 和 AGENTS.md,解析 capability roots、MCP binding 与工具 router,最终把它们放进同一个 StepContext。
源码位置:codex-rs/core/src/session/mod.rs
// :: Session::capture_step_context_with_required_mcp_servers(节选)
// selections在TurnContext中固定;这里仅让异步环境启动状态推进到本step可见值。
let environments = turn_context.environments.refresh_readiness();
self.services
.agents_md_manager
.refresh(&turn_context.config, &environments)
.await;
let loaded_agents_md = self.services.agents_md_manager.get_loaded().await;
let selected_capability_roots = self
.resolve_selected_capability_roots_for_step(&environments)
.await;
let ready_selected_capability_roots =
Self::ready_selected_capability_roots(&selected_capability_roots);
let executor_capability_discovery = self
.executor_capability_discovery_for_step(
&turn_context.config,
&ready_selected_capability_roots,
&environments,
turn_context.windows_sandbox_level,
)
.await;
let (mcp, prepared_recommendations) = async {
tokio::join!(
self.mcp_runtime_for_step(
turn_context.as_ref(),
&selected_capability_roots,
required_servers,
),
turn::prepare_tool_recommendations(self.as_ref(), turn_context.as_ref()),
)
}
.or_cancel(cancellation_token)
.await?;
let tool_router = turn::built_tools(
self.as_ref(),
turn_context.as_ref(),
&environments,
mcp.as_ref(),
&extension_data,
prepared_recommendations,
)
.or_cancel(cancellation_token)
.await??;
Ok(Arc::new(StepContext {
turn: turn_context,
environments,
selected_capability_roots,
executor_capability_discovery,
mcp,
tool_router,
// WorldState中的AGENTS.md与本step工具都来自同一次capture。
loaded_agents_md,
}))这避免了一个隐蔽的不一致:如果环境上下文先读取一遍、工具构建又独立刷新一遍,那么 Prompt 可能宣称 某个 environment 或 Plugin 不可用,却同时暴露来自新状态的工具。StepContext 是一次 sampling step 的 一致性边界;同一 Turn 后续模型/工具循环可以重新 capture 新 StepContext,并通过 WorldState diff 告知变化。
5. WorldState事实
build_world_state_for_step() 不负责基础指令或 transcript。它按稳定 section ID 收集模型、personality、 realtime、AGENTS.md、权限、协作模式、环境、Apps/Plugin 指令、deferred tools、扩展状态和 multi-agent 模式。 每个 section 同时知道如何生成 snapshot,以及相对旧 snapshot 如何渲染更新。
源码位置:codex-rs/core/src/session/world_state.rs
// :: Session::build_world_state_for_step(核心章节节选)
let (previous_model, previous_context, base_instructions) = {
let state = self.state.lock().await;
(
state.previous_turn_settings().map(|previous| previous.model),
state.reference_context_item(),
state.session_configuration.base_instructions.clone(),
)
};
let model_instructions = turn_context
.model_info
.get_model_instructions(turn_context.personality);
let mut world_state = WorldState::default();
world_state.add_section(ModelInstructionsState::new(
&turn_context.model_info.slug,
previous_model.as_deref(),
model_instructions,
));
// AGENTS.md来自StepContext,不在这里再次访问文件系统。
world_state.add_section(AgentsMdState::new(
step_context.loaded_agents_md.as_deref(),
));
if turn_context.config.include_permissions_instructions {
let environment = step_context.environments.primary();
let permission_profile = environment
.map(|environment| {
let workspace_roots = environment
.workspace_roots()
.iter()
.filter_map(|workspace_root| workspace_root.to_abs_path().ok())
.collect::<Vec<_>>();
environment
.permission_profile()
.clone()
// 完整权限章节使用本step主环境的workspace roots物化符号路径。
.materialize_project_roots_with_workspace_roots(&workspace_roots)
})
.unwrap_or_else(|| turn_context.permission_profile());
#[allow(deprecated)]
let cwd = environment
.and_then(|environment| environment.cwd().to_abs_path().ok())
.unwrap_or_else(|| turn_context.cwd.clone());
let model_messages = turn_context.model_info.model_messages.as_ref();
let exec_policy = self.services.exec_policy.current();
world_state.add_section(PermissionsState::new(
&permission_profile,
turn_context.approval_policy(),
ApprovalPromptContext::new(
turn_context.config.approvals_reviewer,
model_messages.and_then(|messages| messages.approvals.as_ref()),
model_messages.and_then(|messages| messages.permissions.as_ref()),
),
exec_policy.as_ref(),
&cwd,
turn_context
.config
.features
.enabled(Feature::ExecPermissionApprovals),
turn_context
.config
.features
.enabled(Feature::RequestPermissionsTool),
));
} else {
let exec_policy = self.services.exec_policy.current();
world_state.add_section(CompactPermissionsState::new(exec_policy.as_ref()));
}
if turn_context.config.include_environment_context {
let current_date = self
.services
.time_provider
.current_time(self.thread_id())
.await
.map_err(|err| CodexErr::Fatal(
format!("failed to read current time: {err:#}"),
))?
.with_timezone(&chrono::Local)
.format("%Y-%m-%d")
.to_string();
// 环境文本使用刷新后的StepContext environments和此刻日期。
world_state.add_section(
EnvironmentsState::from_turn_context_with_environments(
turn_context,
&step_context.environments,
Some(current_date),
)
.with_subagents(environment_subagents),
);
}上面的权限构造参数在源码中由 feature、model messages 和 exec policy 逐项求得;这里按原调用顺序保留 核心数据流。若 TimeProvider 读取失败,函数映射为 Fatal,首轮不会发送一个缺日期但看似完整的环境章节。
5.1 AGENTS.md 和环境
AgentsMdState 把 LoadedAgentsMd 转成 UserInstructions contextual fragment。首次出现直接发送当前内容; 已有未知/旧内容时会附带 replacement notice;删除文件时会发送 removal notice。这让模型能撤销先前上下文, 而不是只能不断追加互相冲突的 AGENTS.md。
源码位置:codex-rs/core/src/context/world_state/agents_md.rs
// :: AgentsMdState::render_diff
fn render_diff(
&self,
previous: PreviousSectionState<'_, Self::Snapshot>,
) -> Option<Box<dyn ContextualUserFragment>> {
let current = self.snapshot();
if matches!(previous, PreviousSectionState::Known(previous) if previous == ¤t) {
// snapshot完全相同时不重复消耗上下文窗口。
return None;
}
let previous_may_contain_instructions = match previous {
PreviousSectionState::Known(previous) => previous.text.is_some(),
PreviousSectionState::Unknown => true,
PreviousSectionState::Absent => false,
};
let instructions = match (&self.instructions, previous_may_contain_instructions) {
(Some(instructions), true) => UserInstructions {
directory: instructions.directory.clone(),
// 明确宣告替换,避免模型继续合并旧规则。
text: format!("{REPLACEMENT_NOTICE}\n\n{}", instructions.text),
},
(Some(instructions), false) => instructions.clone(),
(None, true) => UserInstructions {
directory: None,
text: REMOVAL_NOTICE.to_string(),
},
(None, false) => return None,
};
Some(Box::new(instructions))
}环境同样实现 ContextualUserFragment,使用 <environment_context> markers。单本地环境保持兼容的扁平 格式;多环境会输出带 ID 和 primary 属性的列表;远程 Windows cwd 通过 PathUri 渲染为目标系统路径, 而不是错误地套用宿主 Unix 路径。
源码位置:codex-rs/core/src/context/world_state/environment.rs
// :: EnvironmentsState ContextualUserFragment实现
impl ContextualUserFragment for EnvironmentsState {
fn role(&self) -> &'static str {
// user role表示模型上下文输入,但事件映射不会把marker识别成真实用户意图。
"user"
}
fn markers(&self) -> (&'static str, &'static str) {
Self::type_markers()
}
fn type_markers() -> (&'static str, &'static str) {
environment_context_markers()
}
fn body(&self) -> String {
self.rendered_full().body()
}
}5.2 上下文消费者
build_initial_context_with_world_state() 不按 section 来源逐条生成消息,而是先按 slot 分组,再建立最多 几条 top-level ResponseItem。普通 developer fragments 合并,要求隔离的 developer fragments 各占一条; multi-agent mode 保持独立;contextual user fragments 合并;Guardian policy 最后单独发送。
模型切换指令是排序特例:WorldState render 出 <model_switch> developer fragment 时,builder 将其插入 developer sections 的第一个位置,确保新模型指令先于其他 developer 上下文。
源码位置:codex-rs/core/src/session/mod.rs
// :: Session::build_initial_context_with_world_state(排序节选)
let mut initial_multi_agent_mode = None;
for fragment in world_state.render_full() {
match fragment.role() {
"developer"
if fragment.markers().0 == ModelSwitchInstructions::type_markers().0 =>
{
// 模型切换指令必须位于聚合developer message最前面。
developer_sections.insert(0, fragment.render());
}
"developer" if fragment.markers().0 == MULTI_AGENT_MODE_OPEN_TAG => {
// mode后续独立成一条ResponseItem,不与通用策略揉在一起。
initial_multi_agent_mode = Some(fragment);
}
"developer"
if fragment.requires_separate_message() && fragment.markers().0.is_empty() =>
{
separate_developer_sections.push(fragment.render());
}
"developer" => developer_sections.push(fragment.render()),
"user" => contextual_user_sections.push(fragment.render()),
_ => {}
}
}
let mut items = Vec::with_capacity(4);
if let Some(message) =
crate::context_manager::updates::build_developer_update_item(developer_sections)
{
items.push(message);
}
for section in separate_developer_sections {
if let Some(message) =
crate::context_manager::updates::build_developer_update_item(vec![section])
{
items.push(message);
}
}
if let Some(mode) = initial_multi_agent_mode {
items.push(mode.into_boxed_response_item());
}
if let Some(message) =
crate::context_manager::updates::build_contextual_user_message(contextual_user_sections)
{
items.push(message);
}
// Guardian policy的隔离分支随后追加;最后所有item补齐当前turn_id。
for item in &mut items {
item.set_turn_id_if_missing(&turn_context.sub_id);
}6. Prefix 历史前缀
首轮上下文写入 ContextManager 后位于第一个真实用户消息之前,因此构成 session prefix。Core 还允许 内部调用方通过 inject_user_message_without_turn() 写入 user-role prefix,例如父 Agent 给子 Agent 的任务 上下文;该 API 不创建 Turn 边界,也不触发模型请求。
源码位置:codex-rs/core/src/codex_thread.rs
// :: CodexThread::inject_user_message_without_turn
/// Records a user-role session-prefix message without creating a new user turn boundary.
pub(crate) async fn inject_user_message_without_turn(&self, message: String) {
let item = ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText { text: message }],
phase: None,
internal_chat_message_metadata_passthrough: None,
};
// idle时只记录history;running时进入当前Turn pending input,始终不新建Turn。
self.session
.inject_no_new_turn(vec![item], /*current_turn_context*/ None)
.await;
}真实用户边界不能只按 role == "user" 判断。initial_history_has_prior_user_turns() 会通过 is_user_turn_boundary() 识别 rollout 中真正的用户输入;InterAgentCommunication 也算指令边界,而带 上下文 markers 的 user message 不算。这个结果只用于 first-turn analytics 等生命周期判断,不改变 ResponseItem 在 Prompt 中的角色。
源码位置:codex-rs/core/src/thread_rollout_truncation.rs
// :: initial_history_has_prior_user_turns
pub(crate) fn initial_history_has_prior_user_turns(
conversation_history: &InitialHistory,
) -> bool {
conversation_history.scan_rollout_items(rollout_item_is_user_turn_boundary)
}
fn rollout_item_is_user_turn_boundary(item: &RolloutItem) -> bool {
match item {
RolloutItem::ResponseItem(item) => {
// 解析器会排除environment、AGENTS和其他contextual user wrappers。
is_user_turn_boundary(item)
}
RolloutItem::InterAgentCommunication(_) => true,
_ => false,
}
}首次 run_turn() 的实际写入顺序也很重要:先捕获 step 并记录 initial context/diff,再运行 SessionStart hook,然后处理真实用户输入,最后追加显式 Skill/Plugin 注入项。到第一次 sampling 时, ContextManager::for_prompt() 才生成最终 input。
源码位置:codex-rs/core/src/session/turn.rs :: run_turn(首轮装配节选)
let first_step_context = sess
.capture_step_context_with_required_mcp_servers(
Arc::clone(&turn_context),
&cancellation_token,
&required_servers,
)
.await?;
let (world_state, display_roots) = tokio::join!(
// full context或diff先成为历史prefix,再记录当前真实用户输入。
sess.record_context_updates_and_set_reference_context_item(
first_step_context.as_ref(),
),
turn_diff_display_roots(first_step_context.as_ref()),
);
let mut world_state = world_state?;
let Some((injection_items, explicitly_enabled_connectors)) =
build_skills_and_plugins(
&sess,
first_step_context.as_ref(),
&user_input,
&mentioned_plugins,
&cancellation_token,
)
.await
else {
return Ok(None);
};
// SessionStart hook追加的additional context位于真实用户输入之前。
if run_pending_session_start_hooks(&sess, &turn_context).await {
return Ok(None);
}
if run_hooks_and_record_inputs(&sess, &turn_context, &input).await {
return Ok(None);
}
for response_item in injection_items {
// 显式Skill/Plugin说明紧邻触发它们的用户输入之后。
sess.record_conversation_items(
&turn_context,
std::slice::from_ref(&response_item),
)
.await;
}7. Reference上下文
Core 同时维护两种 baseline:TurnContextItem 记录可恢复的 Turn 设置,WorldStateSnapshot 记录每个动态 章节的比较值。前者为 None 时必须完整注入;存在时只渲染 WorldState diff,并在 TurnContext 改变时追加 extension turn-context fragments。
实现中先计算当前 TurnContext snapshot 和 WorldState。完整路径会建立新的 WorldState baseline;diff 路径 通过 history.update_world_state() 同时得到模型可见 fragments 和持久化 merge patch。只有模型可见项先 进入 history 后,Core 才持久化它们对应的 state records,保证 replay 不会看到“状态已前进但上下文未写入”。
源码位置:codex-rs/core/src/session/mod.rs
// :: Session::record_context_updates_and_set_reference_context_item(节选)
let reference_context_item = {
let state = self.state.lock().await;
state.reference_context_item()
};
let turn_context_item = turn_context.to_turn_context_item();
let turn_context_changed = reference_context_item.as_ref() != Some(&turn_context_item);
let should_inject_full_context = reference_context_item.is_none();
let world_state = Arc::new(self.build_world_state_for_step(step_context).await?);
let (mut context_items, world_state_item) = if should_inject_full_context {
let context_items = self
.build_initial_context_with_world_state(turn_context, world_state.as_ref())
.await;
let snapshot = world_state.snapshot();
self.state
.lock()
.await
.history
// 完整注入建立后续diff的内存基线。
.set_world_state_baseline(snapshot.clone());
(context_items, Some(WorldStateItem::full(snapshot.into_value())))
} else {
let (world_state_items, world_state_item) = {
let mut state = self.state.lock().await;
let (fragments, rollout_item) =
state.history.update_world_state(world_state.as_ref());
(
crate::context_manager::updates::merge_contextual_fragments(fragments),
rollout_item,
)
};
(world_state_items, world_state_item)
};
if !should_inject_full_context && turn_context_changed {
context_items.extend(
self.build_turn_context_contribution_items(step_context).await,
);
}
// snapshot可能改变但既没有TurnContext变化,也不产生模型可见diff。
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() {
// 先写模型可见历史,再持久化描述它的WorldState/TurnContext元数据。
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 {
// 只有WorldState前进时不重复写相同TurnContextItem。
return Ok(world_state);
}
self.persist_rollout_items(&[RolloutItem::TurnContext(
turn_context_item.clone(),
)])
.await;
self.state
.lock()
.await
.set_reference_context_item(Some(turn_context_item));持久化关系可以用下面的 ER 图理解。它不是数据库 schema,而是 replay 所需记录的职责和基数。
Resume 时反向扫描 rollout,以 TurnStarted/TurnComplete 划分 segment,找到最新 surviving TurnContextItem、WorldState full/patch 和 replacement history。compaction 可以清除较旧 baseline;只有后续 TurnContextItem 才能重新建立。解析失败的 WorldState full/patch 会 warning 并把 baseline 置空,下一首轮 自然回到完整注入,而不是拿损坏 snapshot 生成 diff。
8. Prompt
当首轮上下文、hook、用户输入和 Skill/Plugin 注入都进入 ContextManager 后,run_turn() 才调用 for_prompt()。这一步补齐 tool call/output 配对,并按模型 input modalities 移除不支持的图片或音频。 run_sampling_request() 随后读取 Session base instructions,与当前 StepContext router 一起构造 Prompt。
源码位置:codex-rs/core/src/session/turn.rs
// :: run_turn与run_sampling_request(请求封装节选)
let sampling_request_input: Vec<ResponseItem> = sess
.clone_history()
.await
// 复制的history在此规范化,不直接修改Session中的原始记录。
.for_prompt(&turn_context.model_info.input_modalities);
run_sampling_request(
Arc::clone(&sess),
Arc::clone(&step_context),
Arc::clone(&turn_context.extension_data),
Arc::clone(&turn_diff_tracker),
&mut client_session,
&responses_metadata,
sampling_request_input,
cancellation_token.child_token(),
)
.await?;
// 以下两行位于run_sampling_request内部。
let base_instructions = sess.get_base_instructions().await;
let prompt = build_prompt(
prompt_input,
router.as_ref(),
turn_context.as_ref(),
// base instructions在每次sampling时单独读取,不混入ContextManager items。
base_instructions.clone(),
);上一段展示请求前的调用位置,下面是负责保持三个 Prompt 通道分离的封装函数。
源码位置:codex-rs/core/src/session/turn.rs :: build_prompt
pub(crate) fn build_prompt(
input: Vec<ResponseItem>,
router: &ToolRouter,
turn_context: &TurnContext,
base_instructions: BaseInstructions,
) -> Prompt {
Prompt {
input,
// 只暴露当前StepContext router判定为model-visible的结构化工具。
tools: router.model_visible_specs(),
parallel_tool_calls: turn_context.model_info.supports_parallel_tool_calls,
base_instructions,
output_schema: turn_context.final_output_json_schema.clone(),
// Guardian reviewer允许非strict schema,其余Session保持严格校验。
output_schema_strict: !crate::guardian::is_guardian_reviewer_source(
&turn_context.session_source,
),
}
}这里还有一个容易忽略的恢复语义:base instructions 来自当前 SessionConfiguration,而恢复 transcript 保留过去真正发送过的 ResponseItems。Core 不会为了“统一”而回写旧历史;模型切换、AGENTS 更新和环境 变化由新的 WorldState fragment 明确说明。这样既保留历史真实性,又能让当前 Turn 获得最新运行条件。
9. 首轮不变量测试
当前测试重点保护三条边界:新 Session 构造后历史仍为空;恢复历史只在第一次 context update 注入当前 initial context;reference baseline 缺失时完整注入并建立 TurnContextItem,第二次相同更新不能重复追加。
源码位置:codex-rs/core/src/session/tests.rs
// :: record_initial_history_new_defers_initial_context_until_first_turn
session.record_initial_history(InitialHistory::New).await;
let history = session.clone_history().await;
// Session构造完成不等于模型上下文已经落入history。
assert_eq!(history.raw_items().to_vec(), Vec::<ResponseItem>::new());
assert!(session.reference_context_item().await.is_none());
assert_eq!(session.previous_turn_settings().await, None);构造期“延迟”与运行期“只注入一次”是配套不变量:
源码位置:codex-rs/core/src/session/tests.rs
// :: resumed_history_injects_initial_context_on_first_context_update_only(节选)
let history_before_seed = session.state.lock().await.clone_history();
assert_eq!(expected, history_before_seed.raw_items());
session
.record_context_updates_and_set_reference_context_item(&step_context)
.await
.expect("world state should build");
let initial_context = build_initial_context(&session, &turn_context).await;
expected.extend(initial_context);
let history_after_seed = session.clone_history().await;
assert_eq!(
strip_response_item_ids(&expected),
strip_response_item_ids(history_after_seed.raw_items()),
);
session
.record_context_updates_and_set_reference_context_item(&step_context)
.await
.expect("world state should build");
let history_after_second_seed = session.clone_history().await;
// baseline已建立且状态未变时,第二次调用不能复制整段prefix。
assert_eq!(
history_after_seed.raw_items(),
history_after_second_seed.raw_items(),
);排查首轮 Prompt 异常时,可以按三个通道逐层定位:
- 基础行为整体错误:检查
SessionConfiguration.base_instructions和请求中的顶层 instructions,不要只搜 history developer message。 - cwd、日期、权限或 AGENTS.md 过期:检查 TurnContext environment selection、StepContext capture 和对应 WorldState snapshot,而不是启动期 Skill warmup。
- 恢复后重复出现整段环境:检查
reference_context_item是否因旧 compaction、rollback 或损坏的 WorldState replay 被清空;完整 reinjection 可能是正确恢复策略。 - user role 上下文被当成新 Turn:检查 marker 与
is_user_turn_boundary(),不能只按 role 分类。 - 工具与上下文不一致:确认两者是否来自同一个 StepContext;工具 router 不应在 Context build 后独立刷新。
- 首轮覆盖未生效:确认 initial context 是否错误地在
turn/start前写入;正常路径会延迟到第一个record_context_updates_and_set_reference_context_item()。
Session 启动上下文的核心并不是“把所有说明放到最前面”,而是在保持历史原貌的前提下,用 TurnContext 冻结有效配置、用 StepContext 捕获动态依赖、用 WorldState 生成可恢复 diff,最后才把基础指令、历史 input 和工具规格封装成一次一致的模型请求。
输入在sampling边界如何进入同一Turn见 Session输入队列;负责触发这些 更新的 Op handler 见 Session运行时处理。
可以用下面的只读搜索把本文的 context construction 主线落回源码:
rg -n "StepContext|WorldState|TurnContext|reference_context" codex-rs/core/src