Update user instruction message format (#6010)

This commit is contained in:
pakrym-oai
2025-10-30 18:44:02 -07:00
committed by GitHub
parent 9a638dbf4e
commit 2371d771cc
8 changed files with 144 additions and 34 deletions

View File

@@ -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()),

View File

@@ -347,7 +347,8 @@ mod tests {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "<user_instructions>do things</user_instructions>".to_string(),
text: "# AGENTS.md instructions for project\n\n<INSTRUCTIONS>\ndo things\n</INSTRUCTIONS>"
.to_string(),
}],
},
ResponseItem::Message {

View File

@@ -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("<environment_context>") || lowered.starts_with("<user_instructions>")
lowered.starts_with("<environment_context>")
}
fn parse_user_message(message: &[ContentItem]) -> Option<UserMessageItem> {
if UserInstructions::is_user_instructions(message) {
return None;
}
let mut content: Vec<UserInput> = 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: "<user_instructions>test_text</user_instructions>".to_string(),
}],
},
ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "<environment_context>test_text</environment_context>".to_string(),
}],
},
ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "# AGENTS.md instructions for test_directory\n\n<INSTRUCTIONS>\ntest_text\n</INSTRUCTIONS>".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 {

View File

@@ -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 = "<user_instructions>";
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<T: Into<String>>(text: T) -> Self {
Self { text: text.into() }
}
/// Serializes the user instructions to an XML-like tagged block that starts
/// with <user_instructions> 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<UserInstructions> 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<INSTRUCTIONS>\n{contents}\n</INSTRUCTIONS>",
directory = ui.directory,
contents = ui.text
),
}],
}
}
@@ -68,3 +68,51 @@ impl From<DeveloperInstructions> 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<INSTRUCTIONS>\ntest_text\n</INSTRUCTIONS>",
);
}
#[test]
fn test_is_user_instructions() {
assert!(UserInstructions::is_user_instructions(
&[ContentItem::InputText {
text: "# AGENTS.md instructions for test_directory\n\n<INSTRUCTIONS>\ntest_text\n</INSTRUCTIONS>".to_string(),
}]
));
assert!(UserInstructions::is_user_instructions(&[
ContentItem::InputText {
text: "<user_instructions>test_text</user_instructions>".to_string(),
}
]));
assert!(!UserInstructions::is_user_instructions(&[
ContentItem::InputText {
text: "test_text".to_string(),
}
]));
}
}