diff --git a/codex-rs/core/src/context/contextual_user_message_tests.rs b/codex-rs/core/src/context/contextual_user_message_tests.rs index c3f360772a..dae0ff313b 100644 --- a/codex-rs/core/src/context/contextual_user_message_tests.rs +++ b/codex-rs/core/src/context/contextual_user_message_tests.rs @@ -47,6 +47,43 @@ fn detects_internal_model_context_fragment() { })); } +#[test] +fn parses_canonical_internal_model_context_fragment() { + for (source, body) in [ + ("goal", "Keep working toward the user-provided objective."), + ("extension", "\nPreserve body whitespace exactly.\n"), + ] { + let rendered = + InternalModelContextFragment::new(InternalContextSource::from_static(source), body) + .render(); + + let parsed = InternalModelContextFragment::parse_canonical(&rendered) + .expect("canonical internal context should parse"); + + assert_eq!(parsed.source().as_str(), source); + assert_eq!(parsed.body(), body); + } + + assert_eq!( + InternalModelContextFragment::parse_canonical( + "\nbody\n" + ), + None + ); + assert_eq!( + InternalModelContextFragment::parse_canonical( + "\nbody\n" + ), + None + ); + assert_eq!( + InternalModelContextFragment::parse_canonical( + "\nContinue working toward the active thread goal.\n" + ), + None + ); +} + #[test] fn detects_legacy_goal_context_fragment() { assert!(is_contextual_user_fragment(&ContentItem::InputText { diff --git a/codex-rs/core/src/context/internal_model_context.rs b/codex-rs/core/src/context/internal_model_context.rs index cbba3ecd75..2966a11994 100644 --- a/codex-rs/core/src/context/internal_model_context.rs +++ b/codex-rs/core/src/context/internal_model_context.rs @@ -73,6 +73,32 @@ impl InternalModelContextFragment { body: body.into(), } } + + pub(crate) fn parse_canonical(text: &str) -> Option { + let rest = text.strip_prefix(CONTEXT_START_MARKER)?; + let rest = rest.strip_prefix(SOURCE_ATTR_START)?; + let (source, body_and_close) = rest.split_once(SOURCE_ATTR_END)?; + if !is_valid_source(source) { + return None; + } + + let body = body_and_close.strip_suffix(CONTEXT_END_MARKER)?; + let body = body.strip_prefix('\n').unwrap_or(body); + let body = body.strip_suffix('\n').unwrap_or(body); + + Some(Self::new( + InternalContextSource(source.to_string()), + body.to_string(), + )) + } + + pub(crate) fn source(&self) -> &InternalContextSource { + &self.source + } + + pub(crate) fn body(&self) -> &str { + &self.body + } } impl ContextualUserFragment for InternalModelContextFragment { @@ -94,22 +120,12 @@ impl ContextualUserFragment for InternalModelContextFragment { return true; } - let Some(rest) = trimmed.strip_prefix(CONTEXT_START_MARKER) else { - return false; - }; - let Some(rest) = rest.strip_prefix(SOURCE_ATTR_START) else { - return false; - }; - let Some((source, body_and_close)) = rest.split_once(SOURCE_ATTR_END) else { - return false; - }; - - is_valid_source(source) && body_and_close.ends_with(CONTEXT_END_MARKER) + Self::parse_canonical(trimmed).is_some() } fn body(&self) -> String { - let source = self.source.as_str(); - let body = &self.body; + let source = self.source().as_str(); + let body = self.body(); format!(" source=\"{source}\">\n{body}\n") } }