chore: extract context fragments into dedicated crate (#26122)

## Why

`codex-core` currently owns the generic contextual-fragment trait and
several reusable fragment implementations. That makes it harder for
other crates to share the same host-owned model-input abstraction
without depending on all of `codex-core`.

This change extracts the reusable fragment machinery into a small
`codex-context-fragments` crate so future extension and skills work can
depend on the fragment abstraction directly.

## What Changed

- Added the `codex-context-fragments` crate with:
  - `ContextualUserFragment`
  - `FragmentRegistration` / `FragmentRegistrationProxy`
  - additional-context fragment types
- Moved `SkillInstructions` into `codex-core-skills`, since
skill-specific rendering belongs with skills rather than generic core
context machinery.
- Kept `codex-core` re-exporting the fragment types it still uses
internally, so existing call sites keep the same shape.
- Updated Cargo and Bazel workspace metadata for the new crate.

## Verification

- `cargo metadata --locked --format-version 1 --no-deps`
- `just bazel-lock-update`
- `just bazel-lock-check`
This commit is contained in:
jif
2026-06-03 12:25:21 +02:00
committed by GitHub
parent 668703c23f
commit ac67905fc4
15 changed files with 99 additions and 47 deletions
-114
View File
@@ -1,114 +0,0 @@
use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::models::ResponseItem;
use std::marker::PhantomData;
/// Type-erased registration for a contextual user fragment.
///
/// Implementations are used by context filtering code to recognize injected
/// fragments without constructing the concrete context payload.
pub(crate) trait FragmentRegistration: Sync {
fn matches_text(&self, text: &str) -> bool;
}
pub(crate) struct FragmentRegistrationProxy<T> {
_marker: PhantomData<fn() -> T>,
}
impl<T> FragmentRegistrationProxy<T> {
pub(crate) const fn new() -> Self {
Self {
_marker: PhantomData,
}
}
}
impl<T: ContextualUserFragment> FragmentRegistration for FragmentRegistrationProxy<T> {
fn matches_text(&self, text: &str) -> bool {
T::matches_text(text)
}
}
/// 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() -> &'static str
where
Self: Sized;
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(&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,
}
}
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,
}
}
}
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
}
-92
View File
@@ -1,92 +0,0 @@
use super::ContextualUserFragment;
use codex_utils_string::truncate_middle_with_token_budget;
const MAX_ADDITIONAL_CONTEXT_VALUE_TOKENS: usize = 1_000;
const ADDITIONAL_CONTEXT_END_MARKER_SUFFIX: &str = ">";
const ADDITIONAL_CONTEXT_START_MARKER_PREFIX: &str = "<external_";
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct AdditionalContextUserFragment {
key: String,
value: String,
}
impl AdditionalContextUserFragment {
pub(crate) fn new(key: String, value: String) -> Self {
Self { key, value }
}
}
impl ContextualUserFragment for AdditionalContextUserFragment {
fn role() -> &'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 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}>"))
}
fn body(&self) -> String {
additional_context_body(&self.key, &self.value)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct AdditionalContextDeveloperFragment {
key: String,
value: String,
}
impl AdditionalContextDeveloperFragment {
pub(crate) fn new(key: String, value: String) -> Self {
Self { key, value }
}
}
impl ContextualUserFragment for AdditionalContextDeveloperFragment {
fn role() -> &'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)
}
}
fn additional_context_body(key: &str, value: &str) -> String {
let value = truncate_middle_with_token_budget(value, MAX_ADDITIONAL_CONTEXT_VALUE_TOKENS).0;
format!("{key}>{value}</external_{key}")
}
fn additional_context_developer_body(key: &str, value: &str) -> String {
let value = truncate_middle_with_token_budget(value, MAX_ADDITIONAL_CONTEXT_VALUE_TOKENS).0;
format!("<{key}>{value}</{key}>")
}
+6 -9
View File
@@ -7,8 +7,6 @@ mod available_skills_instructions;
mod collaboration_mode_instructions;
mod contextual_user_message;
mod environment_context;
mod fragment;
mod fragments;
mod guardian_followup_review_reminder;
mod hook_additional_context;
mod image_generation_instructions;
@@ -24,7 +22,6 @@ mod plugin_instructions;
mod realtime_end_instructions;
mod realtime_start_instructions;
mod realtime_start_with_instructions;
mod skill_instructions;
mod subagent_notification;
mod turn_aborted;
mod user_instructions;
@@ -34,15 +31,16 @@ pub(crate) use approved_command_prefix_saved::ApprovedCommandPrefixSaved;
pub(crate) use apps_instructions::AppsInstructions;
pub(crate) use available_plugins_instructions::AvailablePluginsInstructions;
pub(crate) use available_skills_instructions::AvailableSkillsInstructions;
pub(crate) use codex_context_fragments::AdditionalContextDeveloperFragment;
pub(crate) use codex_context_fragments::AdditionalContextUserFragment;
pub use codex_context_fragments::ContextualUserFragment;
pub(crate) use codex_context_fragments::FragmentRegistration;
pub(crate) use codex_context_fragments::FragmentRegistrationProxy;
pub(crate) use codex_core_skills::SkillInstructions;
pub(crate) use collaboration_mode_instructions::CollaborationModeInstructions;
pub(crate) use contextual_user_message::is_contextual_user_fragment;
pub(crate) use contextual_user_message::parse_visible_hook_prompt_message;
pub(crate) use environment_context::EnvironmentContext;
pub use fragment::ContextualUserFragment;
pub(crate) use fragment::FragmentRegistration;
pub(crate) use fragment::FragmentRegistrationProxy;
pub(crate) use fragments::AdditionalContextDeveloperFragment;
pub(crate) use fragments::AdditionalContextUserFragment;
pub(crate) use guardian_followup_review_reminder::GuardianFollowupReviewReminder;
pub(crate) use hook_additional_context::HookAdditionalContext;
pub(crate) use image_generation_instructions::ImageGenerationInstructions;
@@ -60,7 +58,6 @@ pub(crate) use plugin_instructions::PluginInstructions;
pub(crate) use realtime_end_instructions::RealtimeEndInstructions;
pub(crate) use realtime_start_instructions::RealtimeStartInstructions;
pub(crate) use realtime_start_with_instructions::RealtimeStartWithInstructions;
pub(crate) use skill_instructions::SkillInstructions;
pub(crate) use subagent_notification::SubagentNotification;
pub(crate) use turn_aborted::TurnAborted;
pub(crate) use user_instructions::UserInstructions;
@@ -1,21 +1 @@
use super::ContextualUserFragment;
pub use codex_prompts::PermissionsInstructions;
impl ContextualUserFragment for PermissionsInstructions {
fn role() -> &'static str {
"developer"
}
fn markers(&self) -> (&'static str, &'static str) {
Self::type_markers()
}
fn type_markers() -> (&'static str, &'static str) {
("<permissions instructions>", "</permissions instructions>")
}
fn body(&self) -> String {
PermissionsInstructions::body(self)
}
}
@@ -1,41 +0,0 @@
use codex_core_skills::injection::SkillInjection;
use super::ContextualUserFragment;
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct SkillInstructions {
pub(crate) name: String,
pub(crate) path: String,
pub(crate) contents: String,
}
impl From<&SkillInjection> for SkillInstructions {
fn from(skill: &SkillInjection) -> Self {
Self {
name: skill.name.clone(),
path: skill.path.clone(),
contents: skill.contents.clone(),
}
}
}
impl ContextualUserFragment for SkillInstructions {
fn role() -> &'static str {
"user"
}
fn markers(&self) -> (&'static str, &'static str) {
Self::type_markers()
}
fn type_markers() -> (&'static str, &'static str) {
("<skill>", "</skill>")
}
fn body(&self) -> String {
format!(
"\n<name>{}</name>\n<path>{}</path>\n{}\n",
self.name, self.path, self.contents
)
}
}