Register hidden context markers from extensions

This commit is contained in:
Eric Traut
2026-05-27 20:41:07 -07:00
parent f690ea0b6a
commit dcf0f74199
12 changed files with 70 additions and 42 deletions

2
codex-rs/Cargo.lock generated
View File

@@ -2848,6 +2848,7 @@ dependencies = [
"async-trait",
"codex-protocol",
"codex-tools",
"inventory",
]
[[package]]
@@ -2976,6 +2977,7 @@ dependencies = [
"codex-state",
"codex-tools",
"codex-utils-template",
"inventory",
"pretty_assertions",
"serde",
"serde_json",

View File

@@ -6,7 +6,6 @@ use super::AdditionalContextUserFragment;
use super::EnvironmentContext;
use super::FragmentRegistration;
use super::FragmentRegistrationProxy;
use super::GoalContext;
use super::LegacyApplyPatchExecCommandWarning;
use super::LegacyModelMismatchWarning;
use super::LegacyUnifiedExecProcessLimitWarning;
@@ -22,8 +21,6 @@ static ENVIRONMENT_CONTEXT_REGISTRATION: FragmentRegistrationProxy<EnvironmentCo
FragmentRegistrationProxy::new();
static ADDITIONAL_CONTEXT_REGISTRATION: FragmentRegistrationProxy<AdditionalContextUserFragment> =
FragmentRegistrationProxy::new();
static GOAL_CONTEXT_REGISTRATION: FragmentRegistrationProxy<GoalContext> =
FragmentRegistrationProxy::new();
static SKILL_INSTRUCTIONS_REGISTRATION: FragmentRegistrationProxy<SkillInstructions> =
FragmentRegistrationProxy::new();
static USER_SHELL_COMMAND_REGISTRATION: FragmentRegistrationProxy<UserShellCommand> =
@@ -46,7 +43,6 @@ static CONTEXTUAL_USER_FRAGMENTS: &[&dyn FragmentRegistration] = &[
&USER_INSTRUCTIONS_REGISTRATION,
&ENVIRONMENT_CONTEXT_REGISTRATION,
&ADDITIONAL_CONTEXT_REGISTRATION,
&GOAL_CONTEXT_REGISTRATION,
&SKILL_INSTRUCTIONS_REGISTRATION,
&USER_SHELL_COMMAND_REGISTRATION,
&TURN_ABORTED_REGISTRATION,
@@ -62,11 +58,17 @@ fn is_standard_contextual_user_text(text: &str) -> bool {
.any(|fragment| fragment.matches_text(text))
}
fn is_registered_extension_context_text(text: &str) -> bool {
codex_extension_api::registered_hidden_context_markers().any(|marker| marker.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)
parse_hook_prompt_fragment(text).is_some()
|| is_standard_contextual_user_text(text)
|| is_registered_extension_context_text(text)
}
pub(crate) fn parse_visible_hook_prompt_message(
@@ -83,7 +85,7 @@ pub(crate) fn parse_visible_hook_prompt_message(
fragments.push(fragment);
continue;
}
if is_standard_contextual_user_text(text) {
if is_standard_contextual_user_text(text) || is_registered_extension_context_text(text) {
continue;
}
return None;

View File

@@ -14,9 +14,10 @@ fn detects_environment_context_fragment() {
}
#[test]
fn detects_legacy_goal_context_fragment() {
assert!(is_contextual_user_fragment(&ContentItem::InputText {
text: "<goal_context>\nlegacy hidden goal prompt\n</goal_context>".to_string(),
fn does_not_hard_code_extension_context_fragments() {
assert!(!is_contextual_user_fragment(&ContentItem::InputText {
text: "<example_extension_context>\nextension-owned hidden prompt\n</example_extension_context>"
.to_string(),
}));
}

View File

@@ -1,29 +0,0 @@
//! Legacy hidden user-context marker for goal steering prompts.
//!
//! The goal implementation now lives in `codex-goal-extension`, but existing
//! rollouts can still contain this marker. Core keeps recognizing it so old
//! hidden goal prompts do not resurface as normal user messages after resume.
use super::ContextualUserFragment;
/// Marker-only registration for hidden goal steering context.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct GoalContext;
impl ContextualUserFragment for GoalContext {
fn role() -> &'static str {
"user"
}
fn markers(&self) -> (&'static str, &'static str) {
Self::type_markers()
}
fn body(&self) -> String {
String::new()
}
fn type_markers() -> (&'static str, &'static str) {
("<goal_context>", "</goal_context>")
}
}

View File

@@ -9,7 +9,6 @@ mod contextual_user_message;
mod environment_context;
mod fragment;
mod fragments;
mod goal_context;
mod guardian_followup_review_reminder;
mod hook_additional_context;
mod image_generation_instructions;
@@ -43,7 +42,6 @@ pub(crate) use fragment::FragmentRegistration;
pub(crate) use fragment::FragmentRegistrationProxy;
pub(crate) use fragments::AdditionalContextDeveloperFragment;
pub(crate) use fragments::AdditionalContextUserFragment;
pub(crate) use goal_context::GoalContext;
pub(crate) use guardian_followup_review_reminder::GuardianFollowupReviewReminder;
pub(crate) use hook_additional_context::HookAdditionalContext;
pub(crate) use image_generation_instructions::ImageGenerationInstructions;

View File

@@ -252,7 +252,7 @@ mod tests {
let original_len = prompt.len();
let request = ThreadIdleRequest::new(HiddenContext::new(
HiddenContextMarker::new("<goal_context>", "</goal_context>"),
HiddenContextMarker::new("<test_context>", "</test_context>"),
prompt,
));
let TurnInput::ResponseInputItem(ResponseInputItem::Message { content, .. }) =
@@ -264,7 +264,7 @@ mod tests {
panic!("expected one text content item");
};
assert!(text.starts_with("<goal_context>"));
assert!(text.starts_with("<test_context>"));
assert!(text.contains("start"));
assert!(text.contains("end"));
assert!(text.len() < original_len);

View File

@@ -17,3 +17,4 @@ workspace = true
async-trait = { workspace = true }
codex-protocol = { workspace = true }
codex-tools = { workspace = true }
inventory = { workspace = true }

View File

@@ -18,6 +18,40 @@ impl HiddenContextMarker {
end_marker,
}
}
/// Returns true when text is wrapped in this marker pair.
pub fn matches_text(self, text: &str) -> bool {
if self.start_marker.is_empty() || self.end_marker.is_empty() {
return false;
}
let trimmed = text.trim_start();
let starts_with_marker = trimmed
.get(..self.start_marker.len())
.is_some_and(|candidate| candidate.eq_ignore_ascii_case(self.start_marker));
let trimmed = trimmed.trim_end();
let ends_with_marker = trimmed
.get(trimmed.len().saturating_sub(self.end_marker.len())..)
.is_some_and(|candidate| candidate.eq_ignore_ascii_case(self.end_marker));
starts_with_marker && ends_with_marker
}
}
/// Compile-time registration for extension-owned hidden context markers.
///
/// Extensions use this to reserve their hidden context wire tags without adding
/// feature-specific tags to core parsing code.
pub struct HiddenContextMarkerRegistration {
pub marker: HiddenContextMarker,
}
inventory::collect!(HiddenContextMarkerRegistration);
/// Returns all hidden context markers registered by linked extensions.
pub fn registered_hidden_context_markers() -> impl Iterator<Item = HiddenContextMarker> {
inventory::iter::<HiddenContextMarkerRegistration>
.into_iter()
.map(|registration| registration.marker)
}
/// Extension-owned hidden context body with the marker pair used to wrap it.

View File

@@ -27,6 +27,8 @@ pub use codex_tools::parse_tool_input_schema;
pub use codex_tools::parse_tool_input_schema_without_compaction;
pub use context::HiddenContext;
pub use context::HiddenContextMarker;
pub use context::HiddenContextMarkerRegistration;
pub use context::registered_hidden_context_markers;
pub use contributors::ApprovalReviewContributor;
pub use contributors::ConfigContributor;
pub use contributors::ContextContributor;

View File

@@ -21,6 +21,7 @@ codex-protocol = { workspace = true }
codex-state = { workspace = true }
codex-tools = { workspace = true }
codex-utils-template = { workspace = true }
inventory = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
tokio = { workspace = true, features = ["sync"] }

View File

@@ -1,5 +1,6 @@
use codex_extension_api::HiddenContext;
use codex_extension_api::HiddenContextMarker;
use codex_extension_api::HiddenContextMarkerRegistration;
use codex_extension_api::ThreadIdleRequest;
use codex_protocol::protocol::ThreadGoal;
use codex_utils_template::Template;
@@ -8,6 +9,12 @@ use std::sync::LazyLock;
const GOAL_CONTEXT_MARKER: HiddenContextMarker =
HiddenContextMarker::new("<goal_context>", "</goal_context>");
inventory::submit! {
HiddenContextMarkerRegistration {
marker: GOAL_CONTEXT_MARKER,
}
}
static CONTINUATION_TEMPLATE: LazyLock<Template> = LazyLock::new(|| {
parse_embedded_template(
include_str!("../templates/goals/continuation.md"),

View File

@@ -42,6 +42,15 @@ use pretty_assertions::assert_eq;
use serde_json::json;
use tempfile::TempDir;
#[test]
fn goal_context_marker_is_registered_as_hidden_context() {
assert!(
codex_extension_api::registered_hidden_context_markers().any(|marker| {
marker.matches_text("<goal_context>\nhidden goal prompt\n</goal_context>")
})
);
}
#[tokio::test]
async fn installed_goal_tools_create_goal_and_fill_empty_preview() -> anyhow::Result<()> {
let runtime = test_runtime().await?;