diff --git a/codex-rs/core/src/environment_context.rs b/codex-rs/core/src/environment_context.rs index 6a0e0f26cd..7b524ff198 100644 --- a/codex-rs/core/src/environment_context.rs +++ b/codex-rs/core/src/environment_context.rs @@ -8,11 +8,17 @@ use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseItem; use codex_protocol::protocol::ENVIRONMENT_CONTEXT_CLOSE_TAG; use codex_protocol::protocol::ENVIRONMENT_CONTEXT_OPEN_TAG; +use codex_protocol::protocol::USER_IDE_CONTEXT_CLOSE_TAG; +use codex_protocol::protocol::USER_IDE_CONTEXT_OPEN_TAG; use codex_utils_absolute_path::AbsolutePathBuf; use serde::Deserialize; use serde::Serialize; use std::path::PathBuf; +pub(crate) fn is_user_ide_context(text: &str) -> bool { + text.starts_with(USER_IDE_CONTEXT_OPEN_TAG) && text.ends_with(USER_IDE_CONTEXT_CLOSE_TAG) +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename = "environment_context", rename_all = "snake_case")] pub(crate) struct EnvironmentContext { diff --git a/codex-rs/core/src/event_mapping.rs b/codex-rs/core/src/event_mapping.rs index 6ab6291a4b..113f6e6288 100644 --- a/codex-rs/core/src/event_mapping.rs +++ b/codex-rs/core/src/event_mapping.rs @@ -13,6 +13,7 @@ use codex_protocol::user_input::UserInput; use tracing::warn; use uuid::Uuid; +use crate::environment_context::is_user_ide_context; use crate::user_instructions::SkillInstructions; use crate::user_instructions::UserInstructions; use crate::user_shell_command::is_user_shell_command_text; @@ -38,6 +39,10 @@ fn parse_user_message(message: &[ContentItem]) -> Option { if is_session_prefix(text) || is_user_shell_command_text(text) { return None; } + // user_ide_context is bundled with the user's prompt in its own content item + if is_user_ide_context(text) { + continue; + } content.push(UserInput::Text { text: text.clone() }); } ContentItem::InputImage { image_url } => { @@ -49,12 +54,16 @@ fn parse_user_message(message: &[ContentItem]) -> Option { if is_session_prefix(text) { return None; } - warn!("Output text in user message: {}", text); + warn!("Output text in user message: {text}"); } } } - Some(UserMessageItem::new(&content)) + if content.is_empty() { + None + } else { + Some(UserMessageItem::new(&content)) + } } fn parse_agent_message(id: Option<&String>, message: &[ContentItem]) -> AgentMessageItem { @@ -131,14 +140,26 @@ mod tests { use super::parse_turn_item; use codex_protocol::items::AgentMessageContent; use codex_protocol::items::TurnItem; + use codex_protocol::items::UserMessageItem; use codex_protocol::models::ContentItem; use codex_protocol::models::ReasoningItemContent; use codex_protocol::models::ReasoningItemReasoningSummary; use codex_protocol::models::ResponseItem; use codex_protocol::models::WebSearchAction; + use codex_protocol::protocol::USER_IDE_CONTEXT_CLOSE_TAG; + use codex_protocol::protocol::USER_IDE_CONTEXT_OPEN_TAG; use codex_protocol::user_input::UserInput; use pretty_assertions::assert_eq; + fn assert_eq_user_message_content(actual: TurnItem, expected_content: &[UserInput]) { + match actual { + TurnItem::UserMessage(user) => { + assert_eq!(user.content, expected_content); + } + other => panic!("expected TurnItem::UserMessage, got {other:?}"), + } + } + #[test] fn parses_user_message_with_text_and_two_images() { let img1 = "https://example.com/one.png".to_string(); @@ -162,19 +183,14 @@ mod tests { let turn_item = parse_turn_item(&item).expect("expected user message turn item"); - match turn_item { - TurnItem::UserMessage(user) => { - let expected_content = vec![ - UserInput::Text { - text: "Hello world".to_string(), - }, - UserInput::Image { image_url: img1 }, - UserInput::Image { image_url: img2 }, - ]; - assert_eq!(user.content, expected_content); - } - other => panic!("expected TurnItem::UserMessage, got {other:?}"), - } + let expected_content = vec![ + UserInput::Text { + text: "Hello world".to_string(), + }, + UserInput::Image { image_url: img1 }, + UserInput::Image { image_url: img2 }, + ]; + assert_eq_user_message_content(turn_item, &expected_content); } #[test] @@ -216,6 +232,13 @@ mod tests { text: "echo 42".to_string(), }], }, + ResponseItem::Message { + id: None, + role: "user".to_string(), + content: vec![ContentItem::InputText { + text: "echo 42".to_string(), + }], + }, ]; for item in items { @@ -224,6 +247,33 @@ mod tests { } } + #[test] + fn trims_user_ide_context_item_from_user_message() { + let message = ResponseItem::Message { + id: Some("user-1".to_string()), + role: "user".to_string(), + content: vec![ + ContentItem::InputText { + text: format!( + "{USER_IDE_CONTEXT_OPEN_TAG}some context{USER_IDE_CONTEXT_CLOSE_TAG}" + ), + }, + ContentItem::InputText { + text: "Hello world".to_string(), + }, + ], + }; + + let turn_item = parse_turn_item(&message).expect("expected user message turn item"); + + assert_eq_user_message_content( + turn_item, + &[UserInput::Text { + text: "Hello world".to_string(), + }], + ); + } + #[test] fn parses_agent_message() { let item = ResponseItem::Message { diff --git a/codex-rs/protocol/src/items.rs b/codex-rs/protocol/src/items.rs index 36ee0be07c..3d34a753bb 100644 --- a/codex-rs/protocol/src/items.rs +++ b/codex-rs/protocol/src/items.rs @@ -10,7 +10,7 @@ use serde::Deserialize; use serde::Serialize; use ts_rs::TS; -#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)] +#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq)] #[serde(tag = "type")] #[ts(tag = "type")] pub enum TurnItem { @@ -20,26 +20,26 @@ pub enum TurnItem { WebSearch(WebSearchItem), } -#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)] +#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq)] pub struct UserMessageItem { pub id: String, pub content: Vec, } -#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)] +#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq)] #[serde(tag = "type")] #[ts(tag = "type")] pub enum AgentMessageContent { Text { text: String }, } -#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)] +#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq)] pub struct AgentMessageItem { pub id: String, pub content: Vec, } -#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)] +#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq)] pub struct ReasoningItem { pub id: String, pub summary_text: Vec, @@ -47,7 +47,7 @@ pub struct ReasoningItem { pub raw_content: Vec, } -#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)] +#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq)] pub struct WebSearchItem { pub id: String, pub query: String, diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index e3748bafc6..835e29a1b2 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -49,6 +49,8 @@ pub const USER_INSTRUCTIONS_OPEN_TAG: &str = ""; pub const USER_INSTRUCTIONS_CLOSE_TAG: &str = ""; pub const ENVIRONMENT_CONTEXT_OPEN_TAG: &str = ""; pub const ENVIRONMENT_CONTEXT_CLOSE_TAG: &str = ""; +pub const USER_IDE_CONTEXT_OPEN_TAG: &str = ""; +pub const USER_IDE_CONTEXT_CLOSE_TAG: &str = ""; pub const USER_MESSAGE_BEGIN: &str = "## My request for Codex:"; /// Submission Queue Entry - requests from user