mirror of
https://github.com/openai/codex.git
synced 2026-04-28 16:45:54 +00:00
core: bundle settings diff updates into one dev/user envelope (#12417)
## Summary
- bundle contextual prompt injection into at most one developer message
plus one contextual user message in both:
- per-turn settings updates
- initial context insertion
- preserve `<model_switch>` across compaction by rebuilding it through
canonical initial-context injection, instead of relying on
strip/reattach hacks
- centralize contextual user fragment detection in one shared definition
table and reuse it for parsing/compaction logic
- keep `AGENTS.md` in its natural serialized format:
- `# AGENTS.md instructions for {dirname}`
- `<INSTRUCTIONS>...</INSTRUCTIONS>`
- simplify related tests/helpers and accept the expected snapshot/layout
updates from bundled multi-part messages
## Why
The goal is to converge toward a simpler, more intentional prompt shape
where contextual updates are consistently represented as one developer
envelope plus one contextual user envelope, while keeping parsing and
compaction behavior aligned with that representation.
## Notable details
- the temporary `SettingsUpdateEnvelope` wrapper was removed; these
paths now return `Vec<ResponseItem>` directly
- local/remote compaction no longer rely on model-switch strip/restore
helpers
- contextual user detection is now driven by shared fragment definitions
instead of ad hoc matcher assembly
- AGENTS/user instructions are still the same logical context; only the
synthetic `<user_instructions>` wrapper was replaced by the natural
AGENTS text format
## Testing
- `just fmt`
- `cargo test -p codex-app-server
codex_message_processor::tests::extract_conversation_summary_prefers_plain_user_messages
-- --exact`
- `cargo test -p codex-core
compact::tests::collect_user_messages_filters_session_prefix_entries
--lib -- --exact`
- `cargo test -p codex-core --test all
'suite::compact::snapshot_request_shape_pre_turn_compaction_strips_incoming_model_switch'
-- --exact`
- `cargo test -p codex-core --test all
'suite::compact_remote::snapshot_request_shape_remote_pre_turn_compaction_strips_incoming_model_switch'
-- --exact`
- `cargo test -p codex-core --test all
'suite::client::includes_apps_guidance_as_developer_message_when_enabled'
-- --exact`
- `cargo test -p codex-core --test all
'suite::client::includes_developer_instructions_message_in_request' --
--exact`
- `cargo test -p codex-core --test all
'suite::client::includes_user_instructions_message_in_request' --
--exact`
- `cargo test -p codex-core --test all
'suite::client::resume_includes_initial_messages_and_sends_prior_items'
-- --exact`
- `cargo test -p codex-core --test all
'suite::review::review_input_isolated_from_parent_history' -- --exact`
- `cargo test -p codex-exec --test all
'suite::resume::exec_resume_last_respects_cwd_filter_and_all_flag' --
--exact`
- `cargo test -p core_test_support
context_snapshot::tests::full_text_mode_preserves_unredacted_text --
--exact`
## Notes
- I also ran several targeted `compact`, `compact_remote`,
`prompt_caching`, `model_visible_layout`, and `event_mapping` tests
while iterating on prompt-shape changes.
- I have not claimed a clean full-workspace `cargo test` from this
environment because local sandbox/resource conditions have previously
produced unrelated failures in large workspace runs.
This commit is contained in:
committed by
GitHub
parent
28bfbb8f2b
commit
07aefffb1f
@@ -30,10 +30,17 @@ use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn text_user_input(text: String) -> serde_json::Value {
|
||||
text_user_input_parts(vec![text])
|
||||
}
|
||||
|
||||
fn text_user_input_parts(texts: Vec<String>) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"type": "message",
|
||||
"role": "user",
|
||||
"content": [ { "type": "input_text", "text": text } ]
|
||||
"content": texts
|
||||
.into_iter()
|
||||
.map(|text| serde_json::json!({ "type": "input_text", "text": text }))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -297,8 +304,8 @@ async fn prefixes_context_and_instructions_once_and_consistently_across_requests
|
||||
let input1 = body1["input"].as_array().expect("input array");
|
||||
assert_eq!(
|
||||
input1.len(),
|
||||
4,
|
||||
"expected permissions + cached prefix + env + user msg"
|
||||
3,
|
||||
"expected permissions + cached contextual user prefix + user msg"
|
||||
);
|
||||
|
||||
let ui_text = input1[1]["content"][0]["text"]
|
||||
@@ -313,11 +320,11 @@ async fn prefixes_context_and_instructions_once_and_consistently_across_requests
|
||||
let cwd_str = config.cwd.to_string_lossy();
|
||||
let expected_env_text = default_env_context_str(&cwd_str, &shell);
|
||||
assert_eq!(
|
||||
input1[2],
|
||||
text_user_input(expected_env_text),
|
||||
"expected environment context after UI message"
|
||||
input1[1]["content"][1]["text"].as_str(),
|
||||
Some(expected_env_text.as_str()),
|
||||
"expected environment context bundled after UI message in cached contextual message"
|
||||
);
|
||||
assert_eq!(input1[3], text_user_input("hello 1".to_string()));
|
||||
assert_eq!(input1[2], text_user_input("hello 1".to_string()));
|
||||
|
||||
let body2 = req2.single_request().body_json();
|
||||
let input2 = body2["input"].as_array().expect("input array");
|
||||
@@ -402,8 +409,10 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> an
|
||||
.await?;
|
||||
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
|
||||
|
||||
let body1 = req1.single_request().body_json();
|
||||
let body2 = req2.single_request().body_json();
|
||||
let request1 = req1.single_request();
|
||||
let request2 = req2.single_request();
|
||||
let body1 = request1.body_json();
|
||||
let body2 = request2.body_json();
|
||||
// prompt_cache_key should remain constant across overrides
|
||||
assert_eq!(
|
||||
body1["prompt_cache_key"], body2["prompt_cache_key"],
|
||||
@@ -500,11 +509,14 @@ async fn override_before_first_turn_emits_environment_context() -> anyhow::Resul
|
||||
let env_texts: Vec<&str> = input
|
||||
.iter()
|
||||
.filter_map(|msg| {
|
||||
msg["content"]
|
||||
.as_array()
|
||||
.and_then(|content| content.first())
|
||||
.and_then(|item| item["text"].as_str())
|
||||
msg["content"].as_array().map(|content| {
|
||||
content
|
||||
.iter()
|
||||
.filter_map(|item| item["text"].as_str())
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
})
|
||||
.flatten()
|
||||
.filter(|text| text.starts_with(ENVIRONMENT_CONTEXT_OPEN_TAG))
|
||||
.collect();
|
||||
assert!(
|
||||
@@ -559,11 +571,14 @@ async fn override_before_first_turn_emits_environment_context() -> anyhow::Resul
|
||||
let user_texts: Vec<&str> = input
|
||||
.iter()
|
||||
.filter_map(|msg| {
|
||||
msg["content"]
|
||||
.as_array()
|
||||
.and_then(|content| content.first())
|
||||
.and_then(|item| item["text"].as_str())
|
||||
msg["content"].as_array().map(|content| {
|
||||
content
|
||||
.iter()
|
||||
.filter_map(|item| item["text"].as_str())
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
})
|
||||
.flatten()
|
||||
.collect();
|
||||
assert!(
|
||||
user_texts.contains(&"first message"),
|
||||
@@ -639,8 +654,10 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res
|
||||
.await?;
|
||||
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
|
||||
|
||||
let body1 = req1.single_request().body_json();
|
||||
let body2 = req2.single_request().body_json();
|
||||
let request1 = req1.single_request();
|
||||
let request2 = req2.single_request();
|
||||
let body1 = request1.body_json();
|
||||
let body2 = request2.body_json();
|
||||
|
||||
// prompt_cache_key should remain constant across per-turn overrides
|
||||
assert_eq!(
|
||||
@@ -672,26 +689,24 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res
|
||||
});
|
||||
let expected_permissions_msg = body1["input"][0].clone();
|
||||
let body1_input = body1["input"].as_array().expect("input array");
|
||||
let expected_model_switch_msg = body2["input"][body1_input.len()].clone();
|
||||
let expected_settings_update_msg = body2["input"][body1_input.len()].clone();
|
||||
assert_ne!(
|
||||
expected_settings_update_msg, expected_permissions_msg,
|
||||
"expected updated permissions message after per-turn override"
|
||||
);
|
||||
assert_eq!(
|
||||
expected_model_switch_msg["role"].as_str(),
|
||||
expected_settings_update_msg["role"].as_str(),
|
||||
Some("developer")
|
||||
);
|
||||
assert!(
|
||||
expected_model_switch_msg["content"][0]["text"]
|
||||
.as_str()
|
||||
.is_some_and(|text| text.contains("<model_switch>")),
|
||||
"expected model switch message after model override: {expected_model_switch_msg:?}"
|
||||
);
|
||||
let expected_permissions_msg_2 = body2["input"][body1_input.len() + 2].clone();
|
||||
assert_ne!(
|
||||
expected_permissions_msg_2, expected_permissions_msg,
|
||||
"expected updated permissions message after per-turn override"
|
||||
request2.has_message_with_input_texts("developer", |texts| {
|
||||
texts.iter().any(|text| text.contains("<model_switch>"))
|
||||
}),
|
||||
"expected model switch section after model override: {expected_settings_update_msg:?}"
|
||||
);
|
||||
let mut expected_body2 = body1_input.to_vec();
|
||||
expected_body2.push(expected_model_switch_msg);
|
||||
expected_body2.push(expected_settings_update_msg);
|
||||
expected_body2.push(expected_env_msg_2);
|
||||
expected_body2.push(expected_permissions_msg_2);
|
||||
expected_body2.push(expected_user_message_2);
|
||||
assert_eq!(body2["input"], serde_json::Value::Array(expected_body2));
|
||||
|
||||
@@ -773,8 +788,10 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a
|
||||
.await?;
|
||||
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
|
||||
|
||||
let body1 = req1.single_request().body_json();
|
||||
let body2 = req2.single_request().body_json();
|
||||
let request1 = req1.single_request();
|
||||
let request2 = req2.single_request();
|
||||
let body1 = request1.body_json();
|
||||
let body2 = request2.body_json();
|
||||
|
||||
let expected_permissions_msg = body1["input"][0].clone();
|
||||
let expected_ui_msg = body1["input"][1].clone();
|
||||
@@ -782,13 +799,18 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a
|
||||
let shell = default_user_shell();
|
||||
let default_cwd_lossy = default_cwd.to_string_lossy();
|
||||
|
||||
let expected_env_msg_1 = text_user_input(default_env_context_str(&default_cwd_lossy, &shell));
|
||||
let expected_contextual_user_msg_1 = text_user_input_parts(vec![
|
||||
expected_ui_msg["content"][0]["text"]
|
||||
.as_str()
|
||||
.expect("cached user instructions text")
|
||||
.to_string(),
|
||||
default_env_context_str(&default_cwd_lossy, &shell),
|
||||
]);
|
||||
let expected_user_message_1 = text_user_input("hello 1".to_string());
|
||||
|
||||
let expected_input_1 = serde_json::Value::Array(vec![
|
||||
expected_permissions_msg.clone(),
|
||||
expected_ui_msg.clone(),
|
||||
expected_env_msg_1.clone(),
|
||||
expected_contextual_user_msg_1.clone(),
|
||||
expected_user_message_1.clone(),
|
||||
]);
|
||||
assert_eq!(body1["input"], expected_input_1);
|
||||
@@ -796,8 +818,7 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a
|
||||
let expected_user_message_2 = text_user_input("hello 2".to_string());
|
||||
let expected_input_2 = serde_json::Value::Array(vec![
|
||||
expected_permissions_msg,
|
||||
expected_ui_msg,
|
||||
expected_env_msg_1,
|
||||
expected_contextual_user_msg_1,
|
||||
expected_user_message_1,
|
||||
expected_user_message_2,
|
||||
]);
|
||||
@@ -881,49 +902,53 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu
|
||||
.await?;
|
||||
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
|
||||
|
||||
let body1 = req1.single_request().body_json();
|
||||
let body2 = req2.single_request().body_json();
|
||||
let request1 = req1.single_request();
|
||||
let request2 = req2.single_request();
|
||||
let body1 = request1.body_json();
|
||||
let body2 = request2.body_json();
|
||||
|
||||
let expected_permissions_msg = body1["input"][0].clone();
|
||||
let expected_ui_msg = body1["input"][1].clone();
|
||||
|
||||
let shell = default_user_shell();
|
||||
let expected_env_text_1 = default_env_context_str(&default_cwd.to_string_lossy(), &shell);
|
||||
let expected_env_msg_1 = text_user_input(expected_env_text_1);
|
||||
let expected_contextual_user_msg_1 = text_user_input_parts(vec![
|
||||
expected_ui_msg["content"][0]["text"]
|
||||
.as_str()
|
||||
.expect("cached user instructions text")
|
||||
.to_string(),
|
||||
expected_env_text_1,
|
||||
]);
|
||||
let expected_user_message_1 = text_user_input("hello 1".to_string());
|
||||
let expected_input_1 = serde_json::Value::Array(vec![
|
||||
expected_permissions_msg.clone(),
|
||||
expected_ui_msg.clone(),
|
||||
expected_env_msg_1.clone(),
|
||||
expected_contextual_user_msg_1.clone(),
|
||||
expected_user_message_1.clone(),
|
||||
]);
|
||||
assert_eq!(body1["input"], expected_input_1);
|
||||
|
||||
let body1_input = body1["input"].as_array().expect("input array");
|
||||
let expected_model_switch_msg = body2["input"][body1_input.len()].clone();
|
||||
let expected_settings_update_msg = body2["input"][body1_input.len()].clone();
|
||||
assert_ne!(
|
||||
expected_settings_update_msg, expected_permissions_msg,
|
||||
"expected updated permissions message after policy change"
|
||||
);
|
||||
assert_eq!(
|
||||
expected_model_switch_msg["role"].as_str(),
|
||||
expected_settings_update_msg["role"].as_str(),
|
||||
Some("developer")
|
||||
);
|
||||
assert!(
|
||||
expected_model_switch_msg["content"][0]["text"]
|
||||
.as_str()
|
||||
.is_some_and(|text| text.contains("<model_switch>")),
|
||||
"expected model switch message after model override: {expected_model_switch_msg:?}"
|
||||
);
|
||||
let expected_permissions_msg_2 = body2["input"][body1_input.len() + 1].clone();
|
||||
assert_ne!(
|
||||
expected_permissions_msg_2, expected_permissions_msg,
|
||||
"expected updated permissions message after policy change"
|
||||
request2.has_message_with_input_texts("developer", |texts| {
|
||||
texts.iter().any(|text| text.contains("<model_switch>"))
|
||||
}),
|
||||
"expected model switch section after model override: {expected_settings_update_msg:?}"
|
||||
);
|
||||
let expected_user_message_2 = text_user_input("hello 2".to_string());
|
||||
let expected_input_2 = serde_json::Value::Array(vec![
|
||||
expected_permissions_msg,
|
||||
expected_ui_msg,
|
||||
expected_env_msg_1,
|
||||
expected_contextual_user_msg_1,
|
||||
expected_user_message_1,
|
||||
expected_model_switch_msg,
|
||||
expected_permissions_msg_2,
|
||||
expected_settings_update_msg,
|
||||
expected_user_message_2,
|
||||
]);
|
||||
assert_eq!(body2["input"], expected_input_2);
|
||||
|
||||
Reference in New Issue
Block a user