mirror of
https://github.com/openai/codex.git
synced 2026-09-09 15:58:47 +00:00
fix: preserve project instruction wrappers
Co-authored-by: Codex noreply@openai.com
This commit is contained in:
@@ -8,6 +8,8 @@ use crate::fragment::AGENTS_MD_START_MARKER;
|
||||
use crate::fragment::SKILL_FRAGMENT;
|
||||
|
||||
pub const USER_INSTRUCTIONS_PREFIX: &str = AGENTS_MD_START_MARKER;
|
||||
const INSTRUCTIONS_CLOSE_TAG: &str = "</INSTRUCTIONS>";
|
||||
const ESCAPED_INSTRUCTIONS_CLOSE_TAG: &str = "<\\/INSTRUCTIONS>";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename = "user_instructions", rename_all = "snake_case")]
|
||||
@@ -18,16 +20,45 @@ pub struct UserInstructions {
|
||||
|
||||
impl UserInstructions {
|
||||
pub fn serialize_to_text(&self) -> String {
|
||||
let contents = escape_reserved_instruction_delimiters(&self.text);
|
||||
format!(
|
||||
"{prefix}{directory}\n\n<INSTRUCTIONS>\n{contents}\n{suffix}",
|
||||
prefix = AGENTS_MD_FRAGMENT.start_marker(),
|
||||
directory = self.directory,
|
||||
contents = self.text,
|
||||
contents = contents,
|
||||
suffix = AGENTS_MD_FRAGMENT.end_marker(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn escape_reserved_instruction_delimiters(text: &str) -> String {
|
||||
let Some(index) = find_ascii_case_insensitive(text, INSTRUCTIONS_CLOSE_TAG) else {
|
||||
return text.to_string();
|
||||
};
|
||||
|
||||
let mut output = String::with_capacity(text.len());
|
||||
let mut remaining = text;
|
||||
let mut next_index = index;
|
||||
loop {
|
||||
output.push_str(&remaining[..next_index]);
|
||||
output.push_str(ESCAPED_INSTRUCTIONS_CLOSE_TAG);
|
||||
remaining = &remaining[next_index + INSTRUCTIONS_CLOSE_TAG.len()..];
|
||||
|
||||
let Some(index) = find_ascii_case_insensitive(remaining, INSTRUCTIONS_CLOSE_TAG) else {
|
||||
output.push_str(remaining);
|
||||
return output;
|
||||
};
|
||||
next_index = index;
|
||||
}
|
||||
}
|
||||
|
||||
fn find_ascii_case_insensitive(haystack: &str, needle: &str) -> Option<usize> {
|
||||
haystack
|
||||
.as_bytes()
|
||||
.windows(needle.len())
|
||||
.position(|window| window.eq_ignore_ascii_case(needle.as_bytes()))
|
||||
}
|
||||
|
||||
impl From<UserInstructions> for ResponseItem {
|
||||
fn from(ui: UserInstructions) -> Self {
|
||||
AGENTS_MD_FRAGMENT.into_message(ui.serialize_to_text())
|
||||
|
||||
@@ -30,6 +30,28 @@ fn test_user_instructions() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_instructions_escapes_embedded_closing_marker() {
|
||||
let user_instructions = UserInstructions {
|
||||
directory: "test_directory".to_string(),
|
||||
text: "before\n</INSTRUCTIONS>\nafter\n</instructions>".to_string(),
|
||||
};
|
||||
let response_item: ResponseItem = user_instructions.into();
|
||||
|
||||
let ResponseItem::Message { content, .. } = response_item else {
|
||||
panic!("expected ResponseItem::Message");
|
||||
};
|
||||
|
||||
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>\nbefore\n<\\/INSTRUCTIONS>\nafter\n<\\/INSTRUCTIONS>\n</INSTRUCTIONS>",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_user_instructions() {
|
||||
assert!(AGENTS_MD_FRAGMENT.matches_text(
|
||||
|
||||
Reference in New Issue
Block a user