diff --git a/codex-rs/app-server/tests/suite/send_message.rs b/codex-rs/app-server/tests/suite/send_message.rs index 8793caaf..8d2b36af 100644 --- a/codex-rs/app-server/tests/suite/send_message.rs +++ b/codex-rs/app-server/tests/suite/send_message.rs @@ -313,10 +313,11 @@ fn assert_instructions_message(item: &ResponseItem) { ResponseItem::Message { role, content, .. } => { assert_eq!(role, "user"); let texts = content_texts(content); + let is_instructions = texts + .iter() + .any(|text| text.starts_with("# AGENTS.md instructions for ")); assert!( - texts - .iter() - .any(|text| text.contains("")), + is_instructions, "expected instructions message, got {texts:?}" ); } diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index f7a5d92b..1199c6ca 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -1003,7 +1003,13 @@ impl Session { items.push(DeveloperInstructions::new(developer_instructions.to_string()).into()); } if let Some(user_instructions) = turn_context.user_instructions.as_deref() { - items.push(UserInstructions::new(user_instructions.to_string()).into()); + items.push( + UserInstructions { + text: user_instructions.to_string(), + directory: turn_context.cwd.to_string_lossy().into_owned(), + } + .into(), + ); } items.push(ResponseItem::from(EnvironmentContext::new( Some(turn_context.cwd.clone()), diff --git a/codex-rs/core/src/codex/compact.rs b/codex-rs/core/src/codex/compact.rs index eba9ebe2..a2fade96 100644 --- a/codex-rs/core/src/codex/compact.rs +++ b/codex-rs/core/src/codex/compact.rs @@ -347,7 +347,8 @@ mod tests { id: None, role: "user".to_string(), content: vec![ContentItem::InputText { - text: "do things".to_string(), + text: "# AGENTS.md instructions for project\n\n\ndo things\n" + .to_string(), }], }, ResponseItem::Message { diff --git a/codex-rs/core/src/event_mapping.rs b/codex-rs/core/src/event_mapping.rs index 73c44fd4..c9edd540 100644 --- a/codex-rs/core/src/event_mapping.rs +++ b/codex-rs/core/src/event_mapping.rs @@ -13,13 +13,19 @@ use codex_protocol::user_input::UserInput; use tracing::warn; use uuid::Uuid; +use crate::user_instructions::UserInstructions; + fn is_session_prefix(text: &str) -> bool { let trimmed = text.trim_start(); let lowered = trimmed.to_ascii_lowercase(); - lowered.starts_with("") || lowered.starts_with("") + lowered.starts_with("") } fn parse_user_message(message: &[ContentItem]) -> Option { + if UserInstructions::is_user_instructions(message) { + return None; + } + let mut content: Vec = Vec::new(); for content_item in message.iter() { @@ -167,6 +173,38 @@ mod tests { } } + #[test] + fn skips_user_instructions_and_env() { + let items = vec![ + ResponseItem::Message { + id: None, + role: "user".to_string(), + content: vec![ContentItem::InputText { + text: "test_text".to_string(), + }], + }, + ResponseItem::Message { + id: None, + role: "user".to_string(), + content: vec![ContentItem::InputText { + text: "test_text".to_string(), + }], + }, + ResponseItem::Message { + id: None, + role: "user".to_string(), + content: vec![ContentItem::InputText { + text: "# AGENTS.md instructions for test_directory\n\n\ntest_text\n".to_string(), + }], + }, + ]; + + for item in items { + let turn_item = parse_turn_item(&item); + assert!(turn_item.is_none(), "expected none, got {turn_item:?}"); + } + } + #[test] fn parses_agent_message() { let item = ResponseItem::Message { diff --git a/codex-rs/core/src/user_instructions.rs b/codex-rs/core/src/user_instructions.rs index f4798461..61f8d7fd 100644 --- a/codex-rs/core/src/user_instructions.rs +++ b/codex-rs/core/src/user_instructions.rs @@ -3,29 +3,25 @@ use serde::Serialize; use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseItem; -use codex_protocol::protocol::USER_INSTRUCTIONS_CLOSE_TAG; -use codex_protocol::protocol::USER_INSTRUCTIONS_OPEN_TAG; -/// Wraps user instructions in a tag so the model can classify them easily. +pub const USER_INSTRUCTIONS_OPEN_TAG_LEGACY: &str = ""; +pub const USER_INSTRUCTIONS_PREFIX: &str = "# AGENTS.md instructions for "; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename = "user_instructions", rename_all = "snake_case")] pub(crate) struct UserInstructions { - text: String, + pub directory: String, + pub text: String, } impl UserInstructions { - pub fn new>(text: T) -> Self { - Self { text: text.into() } - } - - /// Serializes the user instructions to an XML-like tagged block that starts - /// with so clients can classify it. - pub fn serialize_to_xml(self) -> String { - format!( - "{USER_INSTRUCTIONS_OPEN_TAG}\n\n{}\n\n{USER_INSTRUCTIONS_CLOSE_TAG}", - self.text - ) + pub fn is_user_instructions(message: &[ContentItem]) -> bool { + if let [ContentItem::InputText { text }] = message { + text.starts_with(USER_INSTRUCTIONS_PREFIX) + || text.starts_with(USER_INSTRUCTIONS_OPEN_TAG_LEGACY) + } else { + false + } } } @@ -35,7 +31,11 @@ impl From for ResponseItem { id: None, role: "user".to_string(), content: vec![ContentItem::InputText { - text: ui.serialize_to_xml(), + text: format!( + "{USER_INSTRUCTIONS_PREFIX}{directory}\n\n\n{contents}\n", + directory = ui.directory, + contents = ui.text + ), }], } } @@ -68,3 +68,51 @@ impl From for ResponseItem { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_user_instructions() { + let user_instructions = UserInstructions { + directory: "test_directory".to_string(), + text: "test_text".to_string(), + }; + let response_item: ResponseItem = user_instructions.into(); + + let ResponseItem::Message { role, content, .. } = response_item else { + panic!("expected ResponseItem::Message"); + }; + + assert_eq!(role, "user"); + + let [ContentItem::InputText { text }] = content.as_slice() else { + panic!("expected one InputText content item"); + }; + + assert_eq!( + text, + "# AGENTS.md instructions for test_directory\n\n\ntest_text\n", + ); + } + + #[test] + fn test_is_user_instructions() { + assert!(UserInstructions::is_user_instructions( + &[ContentItem::InputText { + text: "# AGENTS.md instructions for test_directory\n\n\ntest_text\n".to_string(), + }] + )); + assert!(UserInstructions::is_user_instructions(&[ + ContentItem::InputText { + text: "test_text".to_string(), + } + ])); + assert!(!UserInstructions::is_user_instructions(&[ + ContentItem::InputText { + text: "test_text".to_string(), + } + ])); + } +} diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index 07bee704..c2c5048d 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -613,8 +613,13 @@ async fn includes_user_instructions_message_in_request() { .contains("be nice") ); assert_message_role(&request_body["input"][0], "user"); - assert_message_starts_with(&request_body["input"][0], ""); - assert_message_ends_with(&request_body["input"][0], ""); + assert_message_starts_with(&request_body["input"][0], "# AGENTS.md instructions for "); + assert_message_ends_with(&request_body["input"][0], ""); + let ui_text = request_body["input"][0]["content"][0]["text"] + .as_str() + .expect("invalid message content"); + assert!(ui_text.contains("")); + assert!(ui_text.contains("be nice")); assert_message_role(&request_body["input"][1], "user"); assert_message_starts_with(&request_body["input"][1], ""); assert_message_ends_with(&request_body["input"][1], ""); @@ -671,8 +676,13 @@ async fn includes_developer_instructions_message_in_request() { assert_message_role(&request_body["input"][0], "developer"); assert_message_equals(&request_body["input"][0], "be useful"); assert_message_role(&request_body["input"][1], "user"); - assert_message_starts_with(&request_body["input"][1], ""); - assert_message_ends_with(&request_body["input"][1], ""); + assert_message_starts_with(&request_body["input"][1], "# AGENTS.md instructions for "); + assert_message_ends_with(&request_body["input"][1], ""); + let ui_text = request_body["input"][1]["content"][0]["text"] + .as_str() + .expect("invalid message content"); + assert!(ui_text.contains("")); + assert!(ui_text.contains("be nice")); assert_message_role(&request_body["input"][2], "user"); assert_message_starts_with(&request_body["input"][2], ""); assert_message_ends_with(&request_body["input"][2], ""); diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs index 1bf12eb9..fee57784 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -354,8 +354,10 @@ async fn prefixes_context_and_instructions_once_and_consistently_across_requests None => String::new(), } ); - let expected_ui_text = - "\n\nbe consistent and helpful\n\n"; + let expected_ui_text = format!( + "# AGENTS.md instructions for {}\n\n\nbe consistent and helpful\n", + cwd.path().to_string_lossy() + ); let expected_env_msg = serde_json::json!({ "type": "message", @@ -734,9 +736,11 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() { let body2 = requests[1].body_json::().unwrap(); let shell = default_user_shell().await; - let expected_ui_text = - "\n\nbe consistent and helpful\n\n"; - let expected_ui_msg = text_user_input(expected_ui_text.to_string()); + let expected_ui_text = format!( + "# AGENTS.md instructions for {}\n\n\nbe consistent and helpful\n", + default_cwd.to_string_lossy() + ); + let expected_ui_msg = text_user_input(expected_ui_text); let expected_env_msg_1 = text_user_input(default_env_context_str( &cwd.path().to_string_lossy(), @@ -848,8 +852,10 @@ async fn send_user_turn_with_changes_sends_environment_context() { let body2 = requests[1].body_json::().unwrap(); let shell = default_user_shell().await; - let expected_ui_text = - "\n\nbe consistent and helpful\n\n"; + let expected_ui_text = format!( + "# AGENTS.md instructions for {}\n\n\nbe consistent and helpful\n", + default_cwd.to_string_lossy() + ); let expected_ui_msg = serde_json::json!({ "type": "message", "role": "user", diff --git a/codex-rs/tui/src/resume_picker.rs b/codex-rs/tui/src/resume_picker.rs index 3104b327..83e3a1ae 100644 --- a/codex-rs/tui/src/resume_picker.rs +++ b/codex-rs/tui/src/resume_picker.rs @@ -989,7 +989,7 @@ mod tests { "type": "message", "role": "user", "content": [ - { "type": "input_text", "text": "hi" }, + { "type": "input_text", "text": "# AGENTS.md instructions for project\n\n\nhi\n" }, ] }), json!({