diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 5fc804f2f2..11ac60c33b 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1940,6 +1940,7 @@ dependencies = [ "strum_macros 0.27.2", "sys-locale", "tempfile", + "thiserror 2.0.18", "tracing", "ts-rs", "uuid", diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index d6d4d93b57..ecd87e8f7e 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -201,6 +201,7 @@ use crate::util::backoff; use crate::windows_sandbox::WindowsSandboxLevelExt; use codex_async_utils::OrCancelExt; use codex_otel::OtelManager; +use codex_protocol::artificial_messages::ArtificialMessage; use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::Personality; use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig; @@ -1802,15 +1803,10 @@ impl Session { self.services .otel_manager .counter("codex.model_warning", 1, &[]); - let item = ResponseItem::Message { - id: None, - role: "user".to_string(), - content: vec![ContentItem::InputText { - text: format!("Warning: {}", message.into()), - }], - end_turn: None, - phase: None, + let item = ArtificialMessage::ModelWarning { + body: message.into(), }; + .to_response_item(); self.record_conversation_items(ctx, &[item]).await; } @@ -5573,10 +5569,14 @@ mod tests { match last { ResponseItem::Message { role, content, .. } => { assert_eq!(role, "user"); + let expected_warning = ArtificialMessage::ModelWarning { + body: "too many unified exec processes".to_string(), + } + .render(); assert_eq!( content, &vec![ContentItem::InputText { - text: "Warning: too many unified exec processes".to_string(), + text: expected_warning, }] ); } diff --git a/codex-rs/core/src/context_manager/history_tests.rs b/codex-rs/core/src/context_manager/history_tests.rs index a6eba62f1a..69b4bee814 100644 --- a/codex-rs/core/src/context_manager/history_tests.rs +++ b/codex-rs/core/src/context_manager/history_tests.rs @@ -2,6 +2,7 @@ use super::*; use crate::truncate; use crate::truncate::TruncationPolicy; use codex_git::GhostCommit; +use codex_protocol::artificial_messages::ArtificialMessage; use codex_protocol::models::ContentItem; use codex_protocol::models::FunctionCallOutputContentItem; use codex_protocol::models::FunctionCallOutputPayload; @@ -60,6 +61,15 @@ fn user_input_text_msg(text: &str) -> ResponseItem { } } +fn demo_skill_message() -> String { + ArtificialMessage::Skill { + name: "demo".to_string(), + path: "skills/demo/SKILL.md".to_string(), + body: "body".to_string(), + } + .render() +} + fn function_call_output(call_id: &str, content: &str) -> ResponseItem { ResponseItem::FunctionCallOutput { call_id: call_id.to_string(), @@ -445,9 +455,7 @@ fn drop_last_n_user_turns_ignores_session_prefix_user_messages() { user_input_text_msg( "# AGENTS.md instructions for test_directory\n\n\ntest_text\n", ), - user_input_text_msg( - "\ndemo\nskills/demo/SKILL.md\nbody\n", - ), + user_input_text_msg(&demo_skill_message()), user_input_text_msg("echo 42"), user_input_text_msg("turn 1 user"), assistant_msg("turn 1 assistant"), @@ -464,9 +472,7 @@ fn drop_last_n_user_turns_ignores_session_prefix_user_messages() { user_input_text_msg( "# AGENTS.md instructions for test_directory\n\n\ntest_text\n", ), - user_input_text_msg( - "\ndemo\nskills/demo/SKILL.md\nbody\n", - ), + user_input_text_msg(&demo_skill_message()), user_input_text_msg("echo 42"), user_input_text_msg("turn 1 user"), assistant_msg("turn 1 assistant"), @@ -480,9 +486,7 @@ fn drop_last_n_user_turns_ignores_session_prefix_user_messages() { user_input_text_msg( "# AGENTS.md instructions for test_directory\n\n\ntest_text\n", ), - user_input_text_msg( - "\ndemo\nskills/demo/SKILL.md\nbody\n", - ), + user_input_text_msg(&demo_skill_message()), user_input_text_msg("echo 42"), ]; @@ -492,9 +496,7 @@ fn drop_last_n_user_turns_ignores_session_prefix_user_messages() { user_input_text_msg( "# AGENTS.md instructions for test_directory\n\n\ntest_text\n", ), - user_input_text_msg( - "\ndemo\nskills/demo/SKILL.md\nbody\n", - ), + user_input_text_msg(&demo_skill_message()), user_input_text_msg("echo 42"), user_input_text_msg("turn 1 user"), assistant_msg("turn 1 assistant"), @@ -510,9 +512,7 @@ fn drop_last_n_user_turns_ignores_session_prefix_user_messages() { user_input_text_msg( "# AGENTS.md instructions for test_directory\n\n\ntest_text\n", ), - user_input_text_msg( - "\ndemo\nskills/demo/SKILL.md\nbody\n", - ), + user_input_text_msg(&demo_skill_message()), user_input_text_msg("echo 42"), user_input_text_msg("turn 1 user"), assistant_msg("turn 1 assistant"), diff --git a/codex-rs/core/src/event_mapping.rs b/codex-rs/core/src/event_mapping.rs index 2ad19d3df5..d9b4adafe9 100644 --- a/codex-rs/core/src/event_mapping.rs +++ b/codex-rs/core/src/event_mapping.rs @@ -146,6 +146,7 @@ pub fn parse_turn_item(item: &ResponseItem) -> Option { #[cfg(test)] mod tests { use super::parse_turn_item; + use codex_protocol::artificial_messages::ArtificialMessage; use codex_protocol::items::AgentMessageContent; use codex_protocol::items::TurnItem; use codex_protocol::items::WebSearchItem; @@ -284,6 +285,12 @@ mod tests { #[test] fn skips_user_instructions_and_env() { + let skill_message = ArtificialMessage::Skill { + name: "demo".to_string(), + path: "skills/demo/SKILL.md".to_string(), + body: "body".to_string(), + } + .render(); let items = vec![ ResponseItem::Message { id: None, @@ -316,8 +323,7 @@ mod tests { id: None, role: "user".to_string(), content: vec![ContentItem::InputText { - text: "\ndemo\nskills/demo/SKILL.md\nbody\n" - .to_string(), + text: skill_message, }], end_turn: None, phase: None, diff --git a/codex-rs/core/src/instructions/user_instructions.rs b/codex-rs/core/src/instructions/user_instructions.rs index 525834847e..bb7bea5c69 100644 --- a/codex-rs/core/src/instructions/user_instructions.rs +++ b/codex-rs/core/src/instructions/user_instructions.rs @@ -1,3 +1,4 @@ +use codex_protocol::artificial_messages::ArtificialMessage; use serde::Deserialize; use serde::Serialize; @@ -64,18 +65,12 @@ impl SkillInstructions { impl From for ResponseItem { fn from(si: SkillInstructions) -> Self { - ResponseItem::Message { - id: None, - role: "user".to_string(), - content: vec![ContentItem::InputText { - text: format!( - "\n{}\n{}\n{}\n", - si.name, si.path, si.contents - ), - }], - end_turn: None, - phase: None, + ArtificialMessage::Skill { + name: si.name, + path: si.path, + body: si.contents, } + .to_response_item() } } @@ -84,6 +79,15 @@ mod tests { use super::*; use pretty_assertions::assert_eq; + fn demo_skill_message() -> String { + ArtificialMessage::Skill { + name: "demo-skill".to_string(), + path: "skills/demo/SKILL.md".to_string(), + body: "body".to_string(), + } + .render() + } + #[test] fn test_user_instructions() { let user_instructions = UserInstructions { @@ -146,18 +150,15 @@ mod tests { panic!("expected one InputText content item"); }; - assert_eq!( - text, - "\ndemo-skill\nskills/demo/SKILL.md\nbody\n", - ); + let expected = demo_skill_message(); + assert_eq!(text, &expected); } #[test] fn test_is_skill_instructions() { assert!(SkillInstructions::is_skill_instructions(&[ ContentItem::InputText { - text: "\ndemo-skill\nskills/demo/SKILL.md\nbody\n" - .to_string(), + text: demo_skill_message(), } ])); assert!(!SkillInstructions::is_skill_instructions(&[ diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index d0f2ba1ae7..d815135f07 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -23,6 +23,7 @@ use codex_core::protocol::Op; use codex_core::protocol::SessionSource; use codex_otel::OtelManager; use codex_protocol::ThreadId; +use codex_protocol::artificial_messages::ArtificialMessage; use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::ReasoningSummary; @@ -343,7 +344,13 @@ async fn resume_includes_initial_messages_and_sends_prior_items() { ); let pos_permissions = messages .iter() - .position(|(role, text)| role == "developer" && text.contains("")) + .position(|(role, text)| { + role == "developer" + && matches!( + ArtificialMessage::parse(text), + Ok(ArtificialMessage::Permission { .. }) + ) + }) .expect("permissions message"); let pos_user_instructions = messages .iter() diff --git a/codex-rs/core/tests/suite/collaboration_instructions.rs b/codex-rs/core/tests/suite/collaboration_instructions.rs index 9dcecdf383..90b399de11 100644 --- a/codex-rs/core/tests/suite/collaboration_instructions.rs +++ b/codex-rs/core/tests/suite/collaboration_instructions.rs @@ -3,6 +3,7 @@ use codex_core::protocol::COLLABORATION_MODE_CLOSE_TAG; use codex_core::protocol::COLLABORATION_MODE_OPEN_TAG; use codex_core::protocol::EventMsg; use codex_core::protocol::Op; +use codex_protocol::artificial_messages::ArtificialMessage; use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::Settings; @@ -90,7 +91,10 @@ async fn no_collaboration_instructions_by_default() -> Result<()> { let input = req.single_request().input(); let dev_texts = developer_texts(&input); assert_eq!(dev_texts.len(), 1); - assert!(dev_texts[0].contains("")); + assert!(matches!( + ArtificialMessage::parse(&dev_texts[0]), + Ok(ArtificialMessage::Permission { .. }) + )); Ok(()) } diff --git a/codex-rs/core/tests/suite/permissions_messages.rs b/codex-rs/core/tests/suite/permissions_messages.rs index c54203b4f1..f32b952594 100644 --- a/codex-rs/core/tests/suite/permissions_messages.rs +++ b/codex-rs/core/tests/suite/permissions_messages.rs @@ -5,6 +5,7 @@ use codex_core::protocol::EventMsg; use codex_core::protocol::Op; use codex_core::protocol::SandboxPolicy; use codex_execpolicy::Policy; +use codex_protocol::artificial_messages::ArtificialMessage; use codex_protocol::models::DeveloperInstructions; use codex_protocol::user_input::UserInput; use codex_utils_absolute_path::AbsolutePathBuf; @@ -34,7 +35,10 @@ fn permissions_texts(input: &[serde_json::Value]) -> Vec { .first()? .get("text")? .as_str()?; - if text.contains("") { + if matches!( + ArtificialMessage::parse(text), + Ok(ArtificialMessage::Permission { .. }) + ) { Some(text.to_string()) } else { None diff --git a/codex-rs/core/tests/suite/skills.rs b/codex-rs/core/tests/suite/skills.rs index 54cdba4d21..eea89c5f71 100644 --- a/codex-rs/core/tests/suite/skills.rs +++ b/codex-rs/core/tests/suite/skills.rs @@ -5,6 +5,7 @@ use anyhow::Result; use codex_core::protocol::AskForApproval; use codex_core::protocol::Op; use codex_core::protocol::SandboxPolicy; +use codex_protocol::artificial_messages::ArtificialMessage; use codex_protocol::user_input::UserInput; use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; @@ -91,13 +92,15 @@ async fn user_turn_includes_skill_instructions() -> Result<()> { let request = mock.single_request(); let user_texts = request.message_input_texts("user"); let skill_path_str = skill_path.to_string_lossy(); + let skill_contents = fs::read_to_string(&skill_path)?; + let expected_skill_text = ArtificialMessage::Skill { + name: "demo".to_string(), + path: skill_path_str.to_string(), + body: skill_contents, + } + .render(); assert!( - user_texts.iter().any(|text| { - text.contains("\ndemo") - && text.contains("") - && text.contains(skill_body) - && text.contains(skill_path_str.as_ref()) - }), + user_texts.iter().any(|text| text == &expected_skill_text), "expected skill instructions in user input, got {user_texts:?}" ); diff --git a/codex-rs/protocol/Cargo.toml b/codex-rs/protocol/Cargo.toml index b58393936b..da28849dbb 100644 --- a/codex-rs/protocol/Cargo.toml +++ b/codex-rs/protocol/Cargo.toml @@ -27,6 +27,7 @@ serde_with = { workspace = true, features = ["macros", "base64"] } strum = { workspace = true } strum_macros = { workspace = true } sys-locale = { workspace = true } +thiserror = { workspace = true } tracing = { workspace = true } ts-rs = { workspace = true, features = [ "uuid-impl", diff --git a/codex-rs/protocol/src/artificial_messages.rs b/codex-rs/protocol/src/artificial_messages.rs new file mode 100644 index 0000000000..3b43462bda --- /dev/null +++ b/codex-rs/protocol/src/artificial_messages.rs @@ -0,0 +1,151 @@ +//! Artificial message helpers shared between prompt construction and protocol parsing. +//! +//! The `artificial_messages!` macro invocation in this module defines: +//! +//! - The `ArtificialMessage` enum variants declared below (`Skill`, `ModelWarning`, +//! `Permission`) with their configured string fields. +//! - `ArtificialMessage::tag(&self) -> &'static str`: returns the top-level XML tag for +//! the current variant (for example `skill`). +//! - `ArtificialMessage::role(&self) -> &'static str`: returns the role that should be +//! used when this message is emitted as a `ResponseItem`. +//! - `ArtificialMessage::render(&self) -> String`: serializes the message to the tagged +//! XML-like envelope. +//! - `ArtificialMessage::parse(input: &str) -> Result`: +//! parses an envelope back into an enum variant. +//! - `ArtificialMessage::detect_tag(input: &str) -> Option<&'static str>`: returns the +//! known artificial-message tag if the input is a recognized envelope. +//! - `ArtificialMessage::is_artificial(input: &str) -> bool`: convenience boolean check +//! built on top of `detect_tag`. +//! - `ArtificialMessage::to_response_item(&self) -> ResponseItem`: converts the rendered +//! content into a message-shaped `ResponseItem`. +//! +//! This module also provides `impl From for ResponseItem` as a +//! convenience wrapper around `to_response_item`. +//! +use crate::models::ContentItem; +use crate::models::ResponseItem; +use thiserror::Error; + +#[macro_use] +mod macros; + +pub const TAG_SKILL: &str = "skill"; +pub const TAG_MODEL_WARNING: &str = "warning"; +pub const TAG_USER_SHELL_COMMAND: &str = "user_shell_cmd"; +pub const TAG_PERMISSION: &str = "permissions_instructions"; +pub const TAG_COLLABORATION_MODE: &str = "collaboration_mode"; +pub const TAG_SPEC: &str = "personality_spec"; +pub const TAG_EXEC_POLICY_AMENDMENT: &str = "exec_policy_amendment"; + +artificial_messages! { + Skill { + tag: TAG_SKILL, + role: "user", + fields: { + tagged(name, "name"), + tagged(path, "path"), + raw(body) + } + }, + ModelWarning { + tag: TAG_MODEL_WARNING, + role: "user", + fields: { + raw(body) + } + }, + Permission { + tag: TAG_PERMISSION, + role: "developer", + fields: { + raw(body) + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Error)] +pub enum ArtificialMessageParseError { + #[error("invalid artificial message envelope")] + InvalidEnvelope, + #[error("unknown artificial message tag: {0}")] + UnknownTopLevelTag(String), + #[error("missing field <{field_tag}> in message <{message_tag}>")] + MissingField { + message_tag: &'static str, + field_tag: &'static str, + }, +} + +impl From for ResponseItem { + fn from(value: ArtificialMessage) -> Self { + value.to_response_item() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn render_and_parse_skill_round_trip() { + let message = ArtificialMessage::Skill { + name: "demo".to_string(), + path: "skills/demo/SKILL.md".to_string(), + body: "body".to_string(), + }; + + let rendered = message.render(); + assert_eq!( + rendered, + "\ndemo\nskills/demo/SKILL.md\nbody\n" + ); + + let parsed = ArtificialMessage::parse(&rendered).expect("parse skill"); + assert_eq!(parsed, message); + } + + #[test] + fn render_and_parse_warning_round_trip() { + let message = ArtificialMessage::ModelWarning { + body: "be careful".to_string(), + }; + let rendered = message.render(); + assert_eq!(rendered, "be careful"); + + let parsed = ArtificialMessage::parse(&rendered).expect("parse warning"); + assert_eq!(parsed, message); + } + + #[test] + fn render_and_parse_permission_round_trip() { + let message = ArtificialMessage::Permission { + body: "policy text".to_string(), + }; + let rendered = message.render(); + assert_eq!( + rendered, + "policy text" + ); + + let parsed = ArtificialMessage::parse(&rendered).expect("parse permission"); + assert_eq!(parsed, message); + } + + #[test] + fn detect_tag_returns_known_tag() { + let text = "abc"; + assert_eq!(ArtificialMessage::detect_tag(text), Some(TAG_PERMISSION)); + assert!(ArtificialMessage::is_artificial(text)); + } + + #[test] + fn parse_rejects_unknown_tag() { + let text = "abc"; + let err = ArtificialMessage::parse(text).expect_err("unknown tag should fail"); + assert_eq!( + err, + ArtificialMessageParseError::UnknownTopLevelTag("unknown".to_string()) + ); + } +} diff --git a/codex-rs/protocol/src/artificial_messages/macros.rs b/codex-rs/protocol/src/artificial_messages/macros.rs new file mode 100644 index 0000000000..f500bbc737 --- /dev/null +++ b/codex-rs/protocol/src/artificial_messages/macros.rs @@ -0,0 +1,206 @@ +use crate::artificial_messages::ArtificialMessageParseError; + +macro_rules! render_field { + ($tagged:ident, $raw:ident, tagged, $value:ident, $field_tag:literal) => {{ + $tagged.push_str(&$crate::artificial_messages::macros::xml( + $field_tag, $value, + )); + $tagged.push('\n'); + }}; + ($tagged:ident, $raw:ident, raw, $value:ident) => {{ + $raw = Some($value.as_str()); + }}; +} + +macro_rules! render_fields { + ($($kind:ident($value:ident $(, $field_tag:literal)?)),* $(,)?) => {{ + #[allow(unused_mut, unused_assignments)] + let mut tagged = String::new(); + #[allow(unused_mut, unused_assignments)] + let mut raw = None; + $( + render_field!(tagged, raw, $kind, $value $(, $field_tag)?); + )* + + if tagged.is_empty() { + raw.unwrap_or_default().to_string() + } else { + let mut out = String::new(); + out.push('\n'); + out.push_str(&tagged); + if let Some(raw) = raw { + out.push_str(raw); + out.push('\n'); + } + out + } + }}; +} + +macro_rules! parse_field { + ($remaining:ident, $consumed_tagged:ident, $msg_tag:expr, tagged, $field:ident, $field_tag:literal) => { + let tagged_input = $remaining.strip_prefix('\n').unwrap_or($remaining); + let ($field, rest) = + $crate::artificial_messages::macros::extract_prefixed_tag(tagged_input, $field_tag) + .ok_or_else(|| ArtificialMessageParseError::MissingField { + message_tag: $msg_tag, + field_tag: $field_tag, + })?; + let $field = $field.to_string(); + $remaining = rest; + if !$consumed_tagged { + $consumed_tagged = true; + } + }; + ($remaining:ident, $consumed_tagged:ident, $msg_tag:expr, raw, $field:ident) => { + let mut value = $remaining; + if $consumed_tagged { + value = value.strip_prefix('\n').unwrap_or(value); + value = value.strip_suffix('\n').unwrap_or(value); + } + let $field = value.to_string(); + $remaining = ""; + }; +} + +macro_rules! parse_variant { + ($variant:ident, $inner:expr, $msg_tag:expr, $($kind:ident($field:ident $(, $field_tag:literal)?)),* $(,)?) => {{ + #[allow(unused_mut, unused_assignments)] + let mut remaining = $inner; + #[allow(unused_mut, unused_assignments)] + let mut consumed_tagged = false; + $( + parse_field!( + remaining, + consumed_tagged, + $msg_tag, + $kind, + $field + $(, $field_tag)? + ); + )* + let _ = &remaining; + let _ = consumed_tagged; + Ok(ArtificialMessage::$variant { $( $field ),* }) + }}; +} + +macro_rules! artificial_messages { + ( + $( + $variant:ident { + tag: $tag:path, + role: $role:literal, + fields: { $($kind:ident($field:ident $(, $field_tag:literal)?)),* $(,)? } + } + ),* $(,)? + ) => { + #[derive(Debug, Clone, PartialEq, Eq)] + pub enum ArtificialMessage { + $( + $variant { $( $field: String ),* } + ),* + } + + impl ArtificialMessage { + pub fn tag(&self) -> &'static str { + match self { + $( Self::$variant { .. } => $tag ),* + } + } + + pub fn role(&self) -> &'static str { + match self { + $( Self::$variant { .. } => $role ),* + } + } + + pub fn render(&self) -> String { + match self { + $( + Self::$variant { $( $field ),* } => { + let payload = render_fields!($($kind($field $(, $field_tag)?)),*); + $crate::artificial_messages::macros::xml($tag, payload) + } + ),* + } + } + + pub fn parse(input: &str) -> Result { + let (tag, inner) = $crate::artificial_messages::macros::split_outer_tag(input)?; + match tag { + $( + $tag => parse_variant!($variant, inner, $tag, $($kind($field $(, $field_tag)?)),*), + )* + unknown => Err(ArtificialMessageParseError::UnknownTopLevelTag( + unknown.to_string(), + )), + } + } + + pub fn detect_tag(input: &str) -> Option<&'static str> { + let (tag, _) = $crate::artificial_messages::macros::split_outer_tag(input).ok()?; + match tag { + $( $tag => Some($tag), )* + _ => None, + } + } + + pub fn is_artificial(input: &str) -> bool { + Self::detect_tag(input).is_some() + } + + pub fn to_response_item(&self) -> ResponseItem { + ResponseItem::Message { + role: self.role().to_string(), + id: None, + content: vec![ContentItem::InputText { + text: self.render(), + }], + end_turn: None, + phase: None, + } + } + } + }; +} + +pub(crate) fn split_outer_tag(input: &str) -> Result<(&str, &str), ArtificialMessageParseError> { + let text = input.trim(); + if !text.starts_with('<') { + return Err(ArtificialMessageParseError::InvalidEnvelope); + } + + let Some(open_end) = text.find('>') else { + return Err(ArtificialMessageParseError::InvalidEnvelope); + }; + + let tag = &text[1..open_end]; + if tag.is_empty() || tag.starts_with('/') || tag.chars().any(char::is_whitespace) { + return Err(ArtificialMessageParseError::InvalidEnvelope); + } + + let close = format!(""); + let Some(inner) = text[open_end + 1..].strip_suffix(&close) else { + return Err(ArtificialMessageParseError::InvalidEnvelope); + }; + + Ok((tag, inner)) +} + +pub(crate) fn extract_prefixed_tag<'a>( + text: &'a str, + field_tag: &'static str, +) -> Option<(&'a str, &'a str)> { + let open = format!("<{field_tag}>"); + let close = format!(""); + let after_open = text.strip_prefix(&open)?; + let end = after_open.find(&close)?; + let value = &after_open[..end]; + let rest = &after_open[end + close.len()..]; + Some((value, rest)) +} + +pub(crate) fn xml(tag: &str, payload: impl std::fmt::Display) -> String { + format!("<{tag}>{payload}") +} diff --git a/codex-rs/protocol/src/lib.rs b/codex-rs/protocol/src/lib.rs index 5841b1187e..c5cc919560 100644 --- a/codex-rs/protocol/src/lib.rs +++ b/codex-rs/protocol/src/lib.rs @@ -1,4 +1,5 @@ pub mod account; +pub mod artificial_messages; mod thread_id; pub use thread_id::ThreadId; pub mod approvals; diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index fe63666e38..686e379850 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -8,6 +8,7 @@ use serde::Serialize; use serde::ser::Serializer; use ts_rs::TS; +use crate::artificial_messages::ArtificialMessage; use crate::config_types::CollaborationMode; use crate::config_types::SandboxMode; use crate::protocol::AskForApproval; @@ -336,20 +337,16 @@ impl DeveloperInstructions { request_rule_enabled: bool, writable_roots: Option>, ) -> Self { - let start_tag = DeveloperInstructions::new(""); - let end_tag = DeveloperInstructions::new(""); - start_tag - .concat(DeveloperInstructions::sandbox_text( - sandbox_mode, - network_access, - )) + let body = DeveloperInstructions::sandbox_text(sandbox_mode, network_access) .concat(DeveloperInstructions::from( approval_policy, exec_policy, request_rule_enabled, )) .concat(DeveloperInstructions::from_writable_roots(writable_roots)) - .concat(end_tag) + .into_text(); + + DeveloperInstructions::new(ArtificialMessage::Permission { body }.render()) } fn from_writable_roots(writable_roots: Option>) -> Self {