Skip to content

Session启动上下文

追踪首轮模型请求如何组合基础指令、上下文前缀、环境、AGENTS.md、扩展片段和恢复历史。

基于rust-v0.150.0
CodexRustSessionContext

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

rust
#[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

rust
#[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(节选)

rust
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

rust
// :: 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(节选)

rust
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

rust
// :: 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

rust
// :: 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

rust
// :: 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 == &current) {
        // 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

rust
// :: 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

rust
// :: 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

rust
// :: 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

rust
// :: 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(首轮装配节选)

rust
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

rust
// :: 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

rust
// :: 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

rust
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

rust
// :: 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

rust
// :: 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 主线落回源码:

bash
rg -n "StepContext|WorldState|TurnContext|reference_context" codex-rs/core/src