Skip to content

ContextFragments模型

从Fragment协议追踪渲染、角色分流、消息合并、状态去重、UI过滤与回滚清理。

基于rust-v0.150.0
CodexRustContextFragment

ContextFragments模型 ​

ContextualUserFragment 的名字容易造成两个误解:它既不只生成 user 消息,也不负责全部上下文管理。 在当前实现中,它是一个很薄的传输协议:实现者声明 role、marker、body 和消息隔离要求,默认方法 负责渲染并转换成 ResponseItem。合并由 context_manager::updates 完成,去重由具体状态所有者或 WorldState snapshot 完成,UI 过滤和 rollback 清理又各有独立识别器。

本文面向已经理解 ContextItem角色语义、 ContextHistory读写 和 Context变更语义 的读者。本文分析 fragment 从来源对象到模型请求、 事件映射和回滚边界的完整路径,不重复讲每种 AGENTS、权限或插件片段的业务内容。读完后应能实现一个 新 fragment,并判断它是否需要 marker、应使用哪个 role、怎样避免重复注入,以及为什么它可能对模型 可见却不应显示成用户消息。

1. 协议边界 ​

一个 fragment 从产生到被消费,会经过多个职责不同的层。trait 只覆盖其中第一层。

这条链解释了为什么“实现了 matches_text”不等于“自动去重”。matches_text 主要服务后续识别;是否 生成新 fragment,必须由来源状态、WorldState section 或专用 store 决定。

2. 渲染契约 ​

trait 位于独立的 codex-context-fragments crate,而不是 core 内部。这样 extension 和 core 可以共享同一 消息协议,不必依赖整个 Session 实现。

源码位置:codex-rs/context-fragments/src/fragment.rs :: ContextualUserFragment。

rust
/// Context payload that is injected as a message fragment.
///
/// Implementations own the response role and provide the exact fragment body.
/// Marked fragments also provide start/end markers used to recognize injected
/// context later. `render()` concatenates markers and body without adding
/// separators, so implementations should include any whitespace they need
/// between tags in `body()`. Unmarked fragments should leave both markers empty,
/// in which case the default helpers render only the body and never match
/// arbitrary text.
pub trait ContextualUserFragment {
    fn role(&self) -> &'static str;

    fn content_kind(&self) -> ContentItemKind;

    /// Whether this fragment must be recorded as its own response item.
    fn requires_separate_message(&self) -> bool {
        false
    }

    fn markers(&self) -> (&'static str, &'static str);

    fn body(&self) -> String;

    fn type_markers() -> (&'static str, &'static str)
    where
        Self: Sized;

    fn matches_text(text: &str) -> bool
    where
        Self: Sized,
    {
        let (start_marker, end_marker) = Self::type_markers();
        matches_marked_text(start_marker, end_marker, text)
    }

    fn render_fragment(&self) -> RenderedFragment {
        RenderedFragment::new(
            self.role(),
            AnnotatedContent::input_text(self.render(), self.content_kind()),
        )
    }

这些方法分别回答不同问题:role 决定请求角色,content_kind 为内容附加稳定分类,markers 与 body 决定实例文本,type_markers 让静态识别器无需构造实例,requires_separate_message 决定能否与相邻同 role fragment 合并,render_fragment 则把 role、文本和分类封装为 RenderedFragment。名称中的 “User”是历史命名,实际实现可以返回 developer。

源码位置:codex-rs/context-fragments/src/fragment.rs :: render、into、into_response_input_item。

rust
fn render(&self) -> String {
    let (start_marker, end_marker) = self.markers();
    let body = self.body();
    if start_marker.is_empty() && end_marker.is_empty() {
        return body;
    }

    format!("{start_marker}{body}{end_marker}")
}

fn into(self) -> ResponseItem
where
    Self: Sized,
{
    ResponseItem::Message {
        id: None,
        role: self.role().to_string(),
        content: vec![ContentItem::InputText {
            text: self.render(),
        }],
        phase: None,
        internal_chat_message_metadata_passthrough: None,
    }
}

fn into_response_input_item(self) -> ResponseInputItem
where
    Self: Sized,
{
    ResponseInputItem::Message {
        role: self.role().to_string(),
        content: vec![ContentItem::InputText {
            text: self.render(),
        }],
        phase: None,
    }
}

render 不自动插入换行;片段若需要 marker 内换行,必须在 body 自己提供。into 生成可记录的 ResponseItem,into_response_input_item 则用于还没进入历史的输入管线。两者共享同一渲染结果,但生命周期 不同。

3. 标记识别 ​

默认识别只检查首尾 marker,并容忍前后空白与 ASCII 大小写差异。

源码位置:codex-rs/context-fragments/src/fragment.rs :: matches_marked_text。

rust
pub(crate) fn matches_marked_text(start_marker: &str, end_marker: &str, text: &str) -> bool {
    if start_marker.is_empty() || end_marker.is_empty() {
        return false;
    }

    let trimmed = text.trim_start();
    let starts_with_marker = trimmed
        .get(..start_marker.len())
        .is_some_and(|candidate| candidate.eq_ignore_ascii_case(start_marker));
    let trimmed = trimmed.trim_end();
    let ends_with_marker = trimmed
        .get(trimmed.len().saturating_sub(end_marker.len())..)
        .is_some_and(|candidate| candidate.eq_ignore_ascii_case(end_marker));
    starts_with_marker && ends_with_marker
}

空 marker 必须返回 false,否则任意普通文本都会被识别成上下文。动态 marker 不能直接使用这个默认算法, 例如 additional context 的 key 是标签名的一部分,需要覆盖 matches_text。

源码位置:codex-rs/context-fragments/src/additional_context.rs :: AdditionalContextUserFragment::matches_text。

rust
fn matches_text(text: &str) -> bool {
    let trimmed = text.trim();
    let Some(rest) = trimmed.strip_prefix(ADDITIONAL_CONTEXT_START_MARKER_PREFIX) else {
        return false;
    };
    let Some((key, value_and_close)) = rest.split_once(ADDITIONAL_CONTEXT_END_MARKER_SUFFIX)
    else {
        return false;
    };

    value_and_close.ends_with(&format!("</external_{key}>"))
}

这段逻辑要求开始标签中的 key 与结束标签一致。单独的 <external_api> 没有成对结束标签,因此仍是普通 用户文本;测试专门固定了这个边界。

4. 角色分流 ​

同一个 trait 同时承载 user 和 developer fragment。最清楚的例子是 additional context:不受信任的外部 状态使用 user role,并包裹成 <external_key>;应用可信状态使用 developer role,并保持普通 <key>。

源码位置:codex-rs/context-fragments/src/additional_context.rs :: AdditionalContextUserFragment、AdditionalContextDeveloperFragment。

rust
impl ContextualUserFragment for AdditionalContextUserFragment {
    fn role(&self) -> &'static str {
        "user"
    }

    fn markers(&self) -> (&'static str, &'static str) {
        Self::type_markers()
    }

    fn type_markers() -> (&'static str, &'static str) {
        (
            ADDITIONAL_CONTEXT_START_MARKER_PREFIX,
            ADDITIONAL_CONTEXT_END_MARKER_SUFFIX,
        )
    }

    fn body(&self) -> String {
        additional_context_body(&self.key, &self.value)
    }
}

impl ContextualUserFragment for AdditionalContextDeveloperFragment {
    fn role(&self) -> &'static str {
        "developer"
    }

    fn markers(&self) -> (&'static str, &'static str) {
        Self::type_markers()
    }

    fn type_markers() -> (&'static str, &'static str) {
        ("", "")
    }

    fn body(&self) -> String {
        additional_context_developer_body(&self.key, &self.value)
    }
}

这里的 role 不是展示属性,而是模型请求中的权限层级。Application 数据进入 developer message, Untrusted 数据只能作为 user context。是否信任由协议入口的 AdditionalContextKind 决定,不能通过 key 名称猜测。

5. 消息合并 ​

WorldState diff 可能一次产生多个 Box<dyn ContextualUserFragment>。merge_contextual_fragments 只合并 相邻、同 role、且双方都允许合并的 fragment;不同 role 或 standalone fragment 会切断分组。

源码位置:codex-rs/core/src/context_manager/updates.rs :: merge_contextual_fragments。

rust
#[derive(Clone, Copy, PartialEq, Eq)]
enum MessageGroup {
    Standalone,
    Mergeable,
}

pub(crate) fn merge_contextual_fragments(
    fragments: Vec<Box<dyn ContextualUserFragment>>,
) -> Vec<ResponseItem> {
    let mut messages: Vec<(&str, MessageGroup, Vec<RenderedFragment>)> = Vec::with_capacity(fragments.len());
    for fragment in fragments {
        let group = if fragment.requires_separate_message() {
            MessageGroup::Standalone
        } else {
            MessageGroup::Mergeable
        };
        let rendered = fragment.render_fragment();
        let role = rendered.role();
        match messages.last_mut() {
            Some((previous_role, previous_group, text_sections))
                if *previous_role == role
                    && *previous_group == MessageGroup::Mergeable
                    && group == MessageGroup::Mergeable =>
            {
                text_sections.push(rendered);
            }
            _ => messages.push((role, group, vec![rendered])),
        }
    }
    messages
        .into_iter()
        .filter_map(|(role, _, text_sections)| build_text_message(role, text_sections))
        .collect()
}

“合并”不是把字符串连接成一个 InputText。每个 fragment 仍成为独立的 ContentItem::InputText,并保留 自己的 ContentItemKind;这些 content item 被放进同一个顶层 ResponseItem::Message,由 RenderedFragment 携带 role 与标注一起进入 builder。

源码位置:codex-rs/core/src/context_manager/updates.rs :: build_text_message。

rust
fn build_text_message(role: &str, text_sections: Vec<String>) -> Option<ResponseItem> {
    if text_sections.is_empty() {
        return None;
    }

    let content = text_sections
        .into_iter()
        .map(|text| ContentItem::InputText { text })
        .collect();

    Some(ResponseItem::Message {
        id: None,
        role: role.to_string(),
        content,
        phase: None,
        internal_chat_message_metadata_passthrough: None,
    })
}

ModelSwitchInstructions、PersonalitySpecInstructions 和 ImageResizeNotice 等实现会返回 requires_separate_message() == true。这保证模型切换、人格变更或图像缩放说明拥有独立消息边界,而不是 被挤进相邻设置更新。

6. 初始拼装 ​

full context 构造不是简单调用 merge_contextual_fragments。它还处理模型切换必须前置、无 marker 的 standalone developer section、MultiAgent mode 独立 item,以及 Guardian policy 隔离等顺序约束。

源码位置:codex-rs/core/src/session/mod.rs :: build_initial_context_with_world_state。

rust
// Render the active mode after the usage hint so it can override that hint.
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 =>
        {
            // New-model instructions must precede the rest of the developer context.
            developer_sections.insert(0, fragment.render());
        }
        "developer" if fragment.markers().0 == MULTI_AGENT_MODE_OPEN_TAG => {
            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(developer_message) =
    crate::context_manager::updates::build_developer_update_item(developer_sections)
{
    items.push(developer_message);
}
for section in separate_developer_sections {
    if let Some(developer_message) =
        crate::context_manager::updates::build_developer_update_item(vec![section])
    {
        items.push(developer_message);
    }
}
if let Some(initial_multi_agent_mode) = initial_multi_agent_mode {
    items.push(initial_multi_agent_mode.into_boxed_response_item());
}
if let Some(contextual_user_message) =
    crate::context_manager::updates::build_contextual_user_message(contextual_user_sections)
{
    items.push(contextual_user_message);
}

因此 fragment 在 full context 和 steady-state diff 中可能经过不同的分组入口,但最终都变成有明确 role 和 content 边界的 ResponseItem。新窗口或 compaction 会把 full context items 直接安装进 replacement history,所以构造结束还会为缺少 turn ID 的 item 补上当前 sub_id。

7. 状态去重 ​

trait 没有缓存,也不知道“上一轮发过什么”。WorldState fragment 的去重发生在 section snapshot 与 render_diff 之间。每个 section 只把比较所需字段写入 snapshot,并根据 Absent、Unknown 或 Known 决定是否生成 fragment。

源码位置:codex-rs/core/src/context/world_state/mod.rs :: WorldStateSection、PreviousSectionState。

rust
/// What is known about a section's previously model-visible state.
pub(crate) enum PreviousSectionState<'a, T> {
    /// No persisted snapshot or matching fragment exists in retained history.
    Absent,
    /// Retained history contains the section, but its typed snapshot is unavailable.
    Unknown,
    /// The exact persisted snapshot is available.
    Known(&'a T),
}

pub(crate) trait WorldStateSection: Send + Sync + 'static {
    const ID: &'static str;
    type Snapshot: DeserializeOwned + Serialize;

    fn snapshot(&self) -> Self::Snapshot;

    fn should_persist(&self) -> bool {
        true
    }

    fn matches_legacy_fragment(_role: &str, _text: &str) -> bool {
        false
    }

    fn matches_current_legacy_fragment(&self, role: &str, text: &str) -> bool {
        Self::matches_legacy_fragment(role, text)
    }

    fn has_retained_fragment_matcher() -> bool {
        false
    }

    fn matches_retained_fragment(_role: &str, _text: &str) -> bool {
        false
    }

    fn render_diff(
        &self,
        previous: PreviousSectionState<'_, Self::Snapshot>,
    ) -> Option<Box<dyn ContextualUserFragment>>;
}

有 snapshot 时使用精确比较;只有旧历史没有 typed snapshot 时,才通过 legacy marker 判断为 Unknown。 如果 section 声明历史中的 fragment 必须仍然存在,snapshot 存在但 fragment 已被裁掉时,会退回 Absent 并重新注入。

源码位置:codex-rs/core/src/context/world_state/mod.rs :: render_history_diff。

rust
pub(crate) fn render_history_diff(
    &self,
    previous: Option<&WorldStateSnapshot>,
    items: &[ResponseItem],
) -> Vec<Box<dyn ContextualUserFragment>> {
    self.render_with(|id, section| {
        if let Some(previous) = previous.and_then(|previous| previous.sections.get(id)) {
            if section.has_retained_fragment_matcher() && !has_retained_fragment(items, section)
            {
                PreviousSectionState::Absent
            } else {
                PreviousSectionState::Known(previous)
            }
        } else if has_legacy_fragment(items, section) {
            PreviousSectionState::Unknown
        } else {
            PreviousSectionState::Absent
        }
    })
}

steady-state 的完整顺序是:构造新 WorldState → 对前一 snapshot 生成 fragment → 合并 fragment → 写入历史 → 更新 baseline → 持久化 merge patch。patch 在模型可见历史之后记录,避免 rollout 声称状态已经推进, 但对应上下文还没有写入。

源码位置:codex-rs/core/src/session/mod.rs :: record_step_world_state_if_changed。

rust
let world_state = Arc::new(self.build_world_state_for_step(step_context).await?);
let previous_snapshot = previous_world_state.snapshot();
let world_state_snapshot = world_state.snapshot();
let world_state_item = world_state_snapshot
    .merge_patch_from(&previous_snapshot)
    .map(WorldStateItem::patch);
let items = crate::context_manager::updates::merge_contextual_fragments(
    world_state.render_diff(&previous_snapshot),
);
if !items.is_empty() {
    self.record_conversation_items(turn_context, &items).await;
}

self.state
    .lock()
    .await
    .history
    .set_world_state_baseline(world_state_snapshot);
if let Some(world_state_item) = world_state_item {
    self.persist_rollout_items(&[RolloutItem::WorldState(world_state_item)])
        .await;
}

8. 外部上下文 ​

Additional context 没有进入 WorldState,而是由 AdditionalContextStore 按 key 和完整 entry 去重。新输入 只为新增或变化的键生成 fragment;随后 store 用本次完整 map 替换旧值,因此移除的键不会生成“删除 fragment”,只是后续不再新增该值。

源码位置:codex-rs/core/src/state/additional_context.rs :: AdditionalContextStore::merge。

rust
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct AdditionalContextStore {
    values: BTreeMap<String, AdditionalContextEntry>,
}

impl AdditionalContextStore {
    pub(crate) fn merge(
        &mut self,
        values: BTreeMap<String, AdditionalContextEntry>,
    ) -> Vec<ResponseInputItem> {
        let fragments = values
            .iter()
            .filter(|(key, value)| self.values.get(*key) != Some(*value))
            .map(|(key, entry)| match entry.kind {
                AdditionalContextKind::Untrusted => {
                    AdditionalContextUserFragment::new(key.clone(), entry.value.clone())
                        .into_response_input_item()
                }
                AdditionalContextKind::Application => {
                    AdditionalContextDeveloperFragment::new(key.clone(), entry.value.clone())
                        .into_response_input_item()
                }
            })
            .collect();
        self.values = values;
        fragments
    }
}

无论新 Turn 还是 steer,都先调用同一个 store,再把产生的 ResponseInputItem 转成 pending input,并放在 真实用户输入之前。相同 entry 在连续 Turn 中不会再次插入,但第一次写入的历史条目仍会随历史保留,所以 模型继续看得到它。

9. 展示过滤 ​

模型可见不等于 UI 应把它显示为用户发言。user fragment 使用显式 matcher 注册表;只有已知 fragment 或 合法 hook prompt 会被判定为 contextual user content。

源码位置:codex-rs/core/src/context/contextual_user_message.rs :: matcher registry、is_contextual_user_fragment。

rust
const CONTEXTUAL_USER_FRAGMENT_MATCHERS: &[fn(&str) -> bool] = &[
    UserInstructions::matches_text,
    EnvironmentsState::matches_text,
    AdditionalContextUserFragment::matches_text,
    SkillInstructions::matches_text,
    UserShellCommand::matches_text,
    TurnAborted::matches_text,
    SubagentNotification::matches_text,
    InternalModelContextFragment::matches_text,
    RecommendedPluginsInstructions::matches_text,
    LegacyUnifiedExecProcessLimitWarning::matches_text,
    LegacyApplyPatchExecCommandWarning::matches_text,
    LegacyModelMismatchWarning::matches_text,
];

fn is_standard_contextual_user_text(text: &str) -> bool {
    CONTEXTUAL_USER_FRAGMENT_MATCHERS
        .iter()
        .any(|matches_text| matches_text(text))
}

pub(crate) fn is_contextual_user_fragment(content_item: &ContentItem) -> bool {
    let ContentItem::InputText { text } = content_item else {
        return false;
    };
    parse_hook_prompt_fragment(text).is_some() || is_standard_contextual_user_text(text)
}

注册表是保守的:任意 <project_context> 不会因为“长得像 XML”就被隐藏。新增 user-role fragment 如果 需要从普通 TurnItem::UserMessage 中排除,必须同步加入 matcher;否则模型仍能看到它,但 UI 会误认为 这是用户输入。

源码位置:codex-rs/core/src/event_mapping.rs :: parse_user_message。

rust
fn parse_user_message(message: &[ContentItem]) -> Option<UserMessageItem> {
    if is_contextual_user_message_content(message) {
        return None;
    }

    let mut content: Vec<UserInput> = Vec::new();

    for (idx, content_item) in message.iter().enumerate() {
        match content_item {
            ContentItem::InputText { text } => {
                let is_image_label = ((is_local_image_open_tag_text(text)
                    || is_image_open_tag_text(text))
                    && matches!(message.get(idx + 1), Some(ContentItem::InputImage { .. })))
                    || (idx > 0
                        && (is_local_image_close_tag_text(text) || is_image_close_tag_text(text))
                        && matches!(message.get(idx - 1), Some(ContentItem::InputImage { .. })));
                let is_audio_label = ((is_local_audio_open_tag_text(text)
                    || is_audio_open_tag_text(text))
                    && matches!(message.get(idx + 1), Some(ContentItem::InputAudio { .. })))
                    || (idx > 0
                        && (is_local_audio_close_tag_text(text) || is_audio_close_tag_text(text))
                        && matches!(message.get(idx - 1), Some(ContentItem::InputAudio { .. })));
                if is_image_label || is_audio_label {
                    continue;
                }

只要一个 message 的任一 content item 被识别为 contextual user fragment,整个 message 就不解析成普通 user item。这也是合并边界必须谨慎的原因:不能把真实用户输入和隐藏 fragment 随意塞进同一消息。

10. Hook例外 ​

Hook prompt 同样不作为普通用户消息展示,但它不是完全隐藏。解析器只允许一个 message 包含合法 hook fragment 和其他已知 contextual fragment;出现任意普通文本就拒绝整体解析。

源码位置:codex-rs/core/src/context/contextual_user_message.rs :: parse_visible_hook_prompt_message。

rust
pub(crate) fn parse_visible_hook_prompt_message(
    id: Option<&str>,
    content: &[ContentItem],
) -> Option<HookPromptItem> {
    let mut fragments = Vec::new();

    for content_item in content {
        let ContentItem::InputText { text } = content_item else {
            return None;
        };
        if let Some(fragment) = parse_hook_prompt_fragment(text) {
            fragments.push(fragment);
            continue;
        }
        if is_standard_contextual_user_text(text) {
            continue;
        }
        return None;
    }

    if fragments.is_empty() {
        return None;
    }

    Some(HookPromptItem::from_fragments(id, fragments))
}

因此 Hook prompt 是“可见的结构化 TurnItem”,其他 contextual user fragment 是“模型可见、事件层隐藏”的 脚手架。两者共用识别入口,却有不同展示结果。

11. 回滚清理 ​

rollback 按普通用户 Turn 切历史时,还要向前删除紧贴该 Turn 的上下文更新,否则撤销了用户输入却保留 了专属于该输入的权限、环境或外部上下文。developer 和 user fragment 使用两套识别器。

源码位置:codex-rs/core/src/context_manager/history.rs :: trim_pre_turn_context_updates。

rust
fn trim_pre_turn_context_updates(
    &mut self,
    snapshot: &[ResponseItem],
    first_instruction_turn_idx: usize,
    mut cut_idx: usize,
) -> usize {
    while cut_idx > first_instruction_turn_idx {
        match &snapshot[cut_idx - 1] {
            ResponseItem::Message { role, content, .. }
                if role == "developer" && is_contextual_dev_message_content(content) =>
            {
                if has_non_contextual_dev_message_content(content) {
                    // Mixed `build_initial_context` bundles are not reconstructible from
                    // steady-state diffs once trimmed, so the next real turn must fully
                    // reinject context instead of diffing against a stale baseline.
                    self.reference_context_item = None;
                }
                cut_idx -= 1;
            }
            ResponseItem::Message { role, content, .. }
                if role == "user" && is_contextual_user_message_content(content) =>
            {
                cut_idx -= 1;
            }
            _ => break,
        }
    }
    cut_idx
}

最关键的失败恢复是 mixed developer bundle:初始 context 可能把可回滚 fragment 和持久 developer 文本 放在同一 message。整体删除后无法从 steady-state diff 重建其中的持久部分,所以必须把 reference_context_item 清空,让下一真实 Turn 执行 full reinjection。

developer 识别不是调用所有 trait matcher,而是维护 rollback 可删除的稳定前缀集合。这包括权限、模型 切换、apps、协作模式、工具、token/window 和 rollout budget 等;没有列入集合的 developer 文本被视为 非 contextual 内容。

源码位置:codex-rs/core/src/event_mapping.rs :: CONTEXTUAL_DEVELOPER_PREFIXES、is_contextual_dev_fragment。

rust
const CONTEXTUAL_DEVELOPER_PREFIXES: &[&str] = &[
    "<permissions instructions>",
    APPROVED_COMMAND_PREFIX_SAVED_MESSAGE_PREFIX,
    "<model_switch>",
    APPS_INSTRUCTIONS_OPEN_TAG,
    COLLABORATION_MODE_OPEN_TAG,
    MULTI_AGENT_MODE_OPEN_TAG,
    ENVIRONMENTS_INSTRUCTIONS_OPEN_TAG,
    "<git_attribution>",
    PLUGINS_INSTRUCTIONS_OPEN_TAG,
    REALTIME_CONVERSATION_OPEN_TAG,
    SKILLS_INSTRUCTIONS_OPEN_TAG,
    TOOLS_OPEN_TAG,
    "<personality_spec>",
    // Keep recognizing token-budget wrappers persisted by older versions.
    "<token_budget>",
    CONTEXT_WINDOW_OPEN_TAG,
    CONTEXT_WINDOW_GUIDANCE_OPEN_TAG,
    "<rollout_budget>",
];

fn is_contextual_dev_fragment(content_item: &ContentItem) -> bool {
    let ContentItem::InputText { text } = content_item else {
        return false;
    };

    let trimmed = text.trim_start();
    CONTEXTUAL_DEVELOPER_PREFIXES.iter().any(|prefix| {
        trimmed
            .get(..prefix.len())
            .is_some_and(|candidate| candidate.eq_ignore_ascii_case(prefix))
    })
}

12. 扩展入口 ​

extension contributor 返回的是 Box<dyn ContextualUserFragment>,Session 使用 object-safe 的 into_boxed_response_item 转换。这说明扩展可以贡献新类型,却仍必须服从 role、marker、消息边界和后续 识别规则。

源码位置:codex-rs/core/src/session/turn.rs :: build_turn_context_contribution_items。

rust
let mut items = Vec::new();
for contributor in contributors {
    let contributed_fragments = contributor
        .contribute(
            input.clone(),
            Some(Arc::clone(&extension_metrics)),
            &sess.services.session_extension_data,
            &sess.services.thread_extension_data,
            turn_context.extension_data.as_ref(),
        )
        .or_cancel(cancellation_token)
        .await
        .ok()?;
    items.extend(
        contributed_fragments
            .into_iter()
            .map(ContextualUserFragment::into_boxed_response_item),
    );
}

Some(items)

如果扩展使用 user role 且希望 UI 隐藏,就需要 core 认识它的 marker,或使用已有的 InternalModelContextFragment。后者限制 source 为 [a-z][a-z0-9_]*,避免把未转义属性注入 marker, 同时保留可追踪的来源标签。

13. Fragment测试 ​

contextual_user_message_tests 覆盖三组反向边界:已知环境、AGENTS、子代理和 internal context 能被识别; 任意 <project_context> 不能被隐藏;非法大写 internal source 也不能伪装成内部上下文。Hook roundtrip 测试 使用包含引号、& 和尖括号的文本,断言解析后恢复原值。它证明 marker 识别与转义往返,不证明 fragment 正文可信。

additional_context_is_model_visible_but_not_a_user_message_item 输入一个 Untrusted browser entry、一个 Application automation entry 和真实用户文本,断言模型请求分别出现 user-role <external_browser_info>、developer-role <automation_info>,事件中的 UserMessage 只保留真实输入。 external_context_like_user_text_remains_a_user_message_item 则输入未闭合的 <external_api>,证明前缀相似 不足以触发隐藏。

additional_context_is_deduplicated_between_turns_while_retained 连续两 Turn 提交相同 map,断言第二次请求 只有第一次的 context history,没有新增重复 fragment。它证明 store 的 entry 去重与历史保留,不证明旧值 会被物理删除;历史清理仍由 compaction、rollback 等机制决定。

drop_last_n_user_turns_clears_reference_context_for_mixed_developer_context_bundles 构造包含 contextual 权限和 持久 plugin 文本的混合 developer message,回滚后断言 message 被删除且 reference_context_item 变为 None。这证明下一 Turn 必须 full reinjection,避免对已经不存在的 bundle 做增量 diff。

14. 接入清单 ​

实现新 fragment 时,可以沿下面的实际依赖顺序检查:

  1. 在 ContextualUserFragment 实现中选择 role、marker、body 和是否 standalone。
  2. 确认来源状态在哪里比较旧值;不要把去重塞进无状态的 render。
  3. 若属于 WorldState,实现稳定 section ID、最小 snapshot 和三态 render_diff。
  4. 若是 user-role 隐藏上下文,补充 matcher 和事件映射测试。
  5. 若应随 rollback 删除,补充 developer prefix 或 user matcher,并测试 mixed bundle 恢复。
  6. 用真实请求断言 role、消息顺序、重复 Turn 和删除/回滚行为。

可以从以下只读搜索开始复核这条链:

bash
rg -n "ContextualUserFragment|merge_contextual_fragments|render_history_diff|AdditionalContextStore" \
  codex-rs/context-fragments codex-rs/core/src/context codex-rs/core/src/context_manager \
  codex-rs/core/src/state

如果一个新 fragment 只通过了 render() 单元测试,还不能认为接入完成。至少还要证明它进入正确 role、 遵守消息隔离、不会在稳定状态重复、不会冒充真实用户输入,并在 rollback 或历史缺失后恢复到正确基线。