move plugin/skill instructions into dev msg and reorder (#14609)

Move the general `Apps`, `Skills` and `Plugins` instructions blocks out
of `user_instructions` and into the developer message, with new `Apps ->
Skills -> Plugins` order for better clarity.

Also wrap those sections in stable XML-style instruction tags (like
other sections) and update prompt-layout tests/snapshots. This makes the
tests less brittle in snapshot output (we can parse the sections), and
it consolidates the capability instructions in one place.

#### Tests
Updated snapshots, added tests.

`<AGENTS_MD>` disappearing in snapshots is expected: before this change,
the wrapped user-instructions message was kept alive by `Skills`
content. Now that `Skills` and `Plugins` are in the developer message,
that wrapper only appears when there is real
project-doc/user-instructions content.

---------

Co-authored-by: Charley Cunningham <ccunningham@openai.com>
This commit is contained in:
sayan-oai
2026-03-13 20:51:01 -07:00
committed by GitHub
parent 7f571396c8
commit d272f45058
44 changed files with 344 additions and 362 deletions

View File

@@ -1,6 +1,11 @@
use regex_lite::Regex;
use serde_json::Value;
use std::sync::OnceLock;
use crate::responses::ResponsesRequest;
use codex_protocol::protocol::APPS_INSTRUCTIONS_OPEN_TAG;
use codex_protocol::protocol::PLUGINS_INSTRUCTIONS_OPEN_TAG;
use codex_protocol::protocol::SKILLS_INSTRUCTIONS_OPEN_TAG;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum ContextSnapshotRenderMode {
@@ -16,12 +21,14 @@ pub enum ContextSnapshotRenderMode {
#[derive(Debug, Clone)]
pub struct ContextSnapshotOptions {
render_mode: ContextSnapshotRenderMode,
strip_capability_instructions: bool,
}
impl Default for ContextSnapshotOptions {
fn default() -> Self {
Self {
render_mode: ContextSnapshotRenderMode::RedactedText,
strip_capability_instructions: false,
}
}
}
@@ -31,6 +38,11 @@ impl ContextSnapshotOptions {
self.render_mode = render_mode;
self
}
pub fn strip_capability_instructions(mut self) -> Self {
self.strip_capability_instructions = true;
self
}
}
pub fn format_request_input_snapshot(
@@ -68,17 +80,23 @@ pub fn format_response_items_snapshot(items: &[Value], options: &ContextSnapshot
.map(|content| {
content
.iter()
.map(|entry| {
.filter_map(|entry| {
if let Some(text) = entry.get("text").and_then(Value::as_str) {
return format_snapshot_text(text, options);
if options.strip_capability_instructions
&& role == "developer"
&& is_capability_instruction_text(text)
{
return None;
}
return Some(format_snapshot_text(text, options));
}
let Some(content_type) =
entry.get("type").and_then(Value::as_str)
else {
return "<UNKNOWN_CONTENT_ITEM>".to_string();
return Some("<UNKNOWN_CONTENT_ITEM>".to_string());
};
let Some(content_object) = entry.as_object() else {
return format!("<{content_type}>");
return Some(format!("<{content_type}>"));
};
let mut extra_keys = content_object
.keys()
@@ -86,11 +104,11 @@ pub fn format_response_items_snapshot(items: &[Value], options: &ContextSnapshot
.cloned()
.collect::<Vec<String>>();
extra_keys.sort();
if extra_keys.is_empty() {
Some(if extra_keys.is_empty() {
format!("<{content_type}>")
} else {
format!("<{content_type}:{}>", extra_keys.join(","))
}
})
})
.collect::<Vec<String>>()
})
@@ -241,6 +259,15 @@ fn canonicalize_snapshot_text(text: &str) -> String {
if text.starts_with("<permissions instructions>") {
return "<PERMISSIONS_INSTRUCTIONS>".to_string();
}
if text.starts_with(APPS_INSTRUCTIONS_OPEN_TAG) {
return "<APPS_INSTRUCTIONS>".to_string();
}
if text.starts_with(SKILLS_INSTRUCTIONS_OPEN_TAG) {
return "<SKILLS_INSTRUCTIONS>".to_string();
}
if text.starts_with(PLUGINS_INSTRUCTIONS_OPEN_TAG) {
return "<PLUGINS_INSTRUCTIONS>".to_string();
}
if text.starts_with("# AGENTS.md instructions for ") {
return "<AGENTS_MD>".to_string();
}
@@ -282,7 +309,24 @@ fn canonicalize_snapshot_text(text: &str) -> String {
{
return format!("<COMPACTION_SUMMARY>\n{summary}");
}
text.to_string()
normalize_dynamic_snapshot_paths(text)
}
fn is_capability_instruction_text(text: &str) -> bool {
text.starts_with(APPS_INSTRUCTIONS_OPEN_TAG)
|| text.starts_with(SKILLS_INSTRUCTIONS_OPEN_TAG)
|| text.starts_with(PLUGINS_INSTRUCTIONS_OPEN_TAG)
}
fn normalize_dynamic_snapshot_paths(text: &str) -> String {
static SYSTEM_SKILL_PATH_RE: OnceLock<Regex> = OnceLock::new();
let system_skill_path_re = SYSTEM_SKILL_PATH_RE.get_or_init(|| {
Regex::new(r"/[^)\n]*/skills/\.system/([^/\n]+)/SKILL\.md")
.expect("system skill path regex should compile")
});
system_skill_path_re
.replace_all(text, "<SYSTEM_SKILLS_ROOT>/$1/SKILL.md")
.into_owned()
}
#[cfg(test)]
@@ -353,6 +397,60 @@ mod tests {
assert_eq!(rendered, "00:message/user:<AGENTS_MD>");
}
#[test]
fn redacted_text_mode_keeps_capability_instruction_placeholders() {
let items = vec![json!({
"type": "message",
"role": "developer",
"content": [
{
"type": "input_text",
"text": "<apps_instructions>\n## Apps\nbody\n</apps_instructions>"
},
{
"type": "input_text",
"text": "<skills_instructions>\n## Skills\nbody\n</skills_instructions>"
},
{
"type": "input_text",
"text": "<plugins_instructions>\n## Plugins\nbody\n</plugins_instructions>"
}
]
})];
let rendered = format_response_items_snapshot(
&items,
&ContextSnapshotOptions::default().render_mode(ContextSnapshotRenderMode::RedactedText),
);
assert_eq!(
rendered,
"00:message/developer[3]:\n [01] <APPS_INSTRUCTIONS>\n [02] <SKILLS_INSTRUCTIONS>\n [03] <PLUGINS_INSTRUCTIONS>"
);
}
#[test]
fn strip_capability_instructions_omits_capability_parts_from_developer_messages() {
let items = vec![json!({
"type": "message",
"role": "developer",
"content": [
{ "type": "input_text", "text": "<permissions instructions>\n...</permissions instructions>" },
{ "type": "input_text", "text": "<skills_instructions>\n## Skills\n...</skills_instructions>" },
{ "type": "input_text", "text": "<plugins_instructions>\n## Plugins\n...</plugins_instructions>" }
]
})];
let rendered = format_response_items_snapshot(
&items,
&ContextSnapshotOptions::default()
.render_mode(ContextSnapshotRenderMode::RedactedText)
.strip_capability_instructions(),
);
assert_eq!(rendered, "00:message/developer:<PERMISSIONS_INSTRUCTIONS>");
}
#[test]
fn redacted_text_mode_normalizes_environment_context_with_subagents() {
let items = vec![json!({
@@ -442,4 +540,23 @@ mod tests {
"00:message/user[3]:\n [01] <image>\n [02] <input_image:image_url>\n [03] </image>"
);
}
#[test]
fn redacted_text_mode_normalizes_system_skill_temp_paths() {
let items = vec![json!({
"type": "message",
"role": "developer",
"content": [{
"type": "input_text",
"text": "## Skills\n- openai-docs: helper (file: /private/var/folders/yk/p4jp9nzs79s5q84csslkgqtm0000gn/T/.tmpAnGVww/skills/.system/openai-docs/SKILL.md)"
}]
})];
let rendered = format_response_items_snapshot(&items, &ContextSnapshotOptions::default());
assert_eq!(
rendered,
"00:message/developer:## Skills\\n- openai-docs: helper (file: <SYSTEM_SKILLS_ROOT>/openai-docs/SKILL.md)"
);
}
}