mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
Sanitize developer context in full-history agent forks (#39641)
## Why Developer messages can contain both inherited agent policy and unrelated context. Full-history forks need to replace the parent policy without dropping the unrelated content that shares its message. ## What changed - Filter fork-specific developer instructions by content item instead of excluding an entire developer message. - Remove inherited multi-agent mode instructions before applying the child agent's current mode. - Preserve unrelated content items and discard developer messages only when filtering leaves them empty. ## Testing Extend agent-control and subagent notification tests to cover compound compacted messages, multi-agent mode instructions, and proactive-to-explicit mode transitions. GitOrigin-RevId: 028834e237d8636c13b17a724574bc8eb09a55ba
This commit is contained in:
@@ -4,6 +4,7 @@ use crate::agent::role::apply_role_to_config;
|
||||
use crate::config::PermissionProfileSnapshot;
|
||||
use crate::context::ContextualUserFragment;
|
||||
use crate::context::CurrentTimeReminder;
|
||||
use crate::context::MultiAgentModeInstructions;
|
||||
use crate::context::MultiAgentRoleInstructions;
|
||||
use crate::session::multi_agents::resolve_usage_hints;
|
||||
use codex_extension_api::ExtensionDataInit;
|
||||
@@ -85,22 +86,27 @@ fn keep_forked_rollout_item(item: &RolloutItem, preserve_reference_context_item:
|
||||
}
|
||||
}
|
||||
|
||||
fn is_fork_excluded_developer_message(item: &ResponseItem, usage_hint_texts: &[String]) -> bool {
|
||||
fn retain_forked_developer_message(item: &mut ResponseItem, usage_hint_texts: &[String]) -> bool {
|
||||
let ResponseItem::Message { role, content, .. } = item else {
|
||||
return false;
|
||||
return true;
|
||||
};
|
||||
if role != "developer" {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
let [ContentItem::InputText { text }] = content.as_slice() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
MultiAgentRoleInstructions::matches_text(text)
|
||||
|| CurrentTimeReminder::matches_text(text)
|
||||
|| usage_hint_texts
|
||||
.iter()
|
||||
.any(|usage_hint_text| usage_hint_text == text)
|
||||
content.retain(|content_item| {
|
||||
let ContentItem::InputText { text } = content_item else {
|
||||
return true;
|
||||
};
|
||||
|
||||
!(MultiAgentRoleInstructions::matches_text(text)
|
||||
|| MultiAgentModeInstructions::matches_text(text)
|
||||
|| CurrentTimeReminder::matches_text(text)
|
||||
|| usage_hint_texts
|
||||
.iter()
|
||||
.any(|usage_hint_text| usage_hint_text == text))
|
||||
});
|
||||
!content.is_empty()
|
||||
}
|
||||
|
||||
async fn load_agent_model_context(
|
||||
@@ -724,7 +730,7 @@ impl AgentControl {
|
||||
if matches!(response_item, ResponseItem::AgentMessage { .. }) {
|
||||
return false;
|
||||
}
|
||||
if is_fork_excluded_developer_message(
|
||||
if !retain_forked_developer_message(
|
||||
response_item,
|
||||
&multi_agent_v2_usage_hint_texts_to_filter,
|
||||
) {
|
||||
|
||||
@@ -1381,6 +1381,10 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
|
||||
text: "Developer context before.\nParent developer instructions.\nDeveloper context after."
|
||||
.to_string(),
|
||||
},
|
||||
ContentItem::InputText {
|
||||
text: "<multi_agent_mode>Proactive multi-agent delegation is active.</multi_agent_mode>"
|
||||
.to_string(),
|
||||
},
|
||||
ContentItem::InputText {
|
||||
text: "Preserved developer context.".to_string(),
|
||||
},
|
||||
@@ -1650,6 +1654,10 @@ async fn spawn_agent_fork_strips_parent_usage_hints_from_compacted_history() {
|
||||
text: "Compacted context before.\nParent developer instructions.\nCompacted context after."
|
||||
.to_string(),
|
||||
},
|
||||
ContentItem::InputText {
|
||||
text: "<multi_agent_mode>Proactive multi-agent delegation is active.</multi_agent_mode>"
|
||||
.to_string(),
|
||||
},
|
||||
ContentItem::InputText {
|
||||
text: "Preserved compacted developer context.".to_string(),
|
||||
},
|
||||
@@ -1742,6 +1750,13 @@ async fn spawn_agent_fork_strips_parent_usage_hints_from_compacted_history() {
|
||||
!history_contains_text(history.raw_items(), "Parent root guidance."),
|
||||
"forked child history should strip stale parent hints from compacted replacement history"
|
||||
);
|
||||
assert!(
|
||||
!history_contains_text(
|
||||
history.raw_items(),
|
||||
"Proactive multi-agent delegation is active."
|
||||
),
|
||||
"forked child history should strip stale policy fragments from compound compacted messages"
|
||||
);
|
||||
assert!(
|
||||
!history_contains_text(history.raw_items(), "Parent developer instructions."),
|
||||
"forked child history should replace parent instructions in compacted replacement history"
|
||||
|
||||
@@ -66,6 +66,7 @@ pub(crate) use legacy_apply_patch_exec_command_warning::LegacyApplyPatchExecComm
|
||||
pub(crate) use legacy_model_mismatch_warning::LegacyModelMismatchWarning;
|
||||
pub(crate) use legacy_unified_exec_process_limit_warning::LegacyUnifiedExecProcessLimitWarning;
|
||||
pub(crate) use model_switch_instructions::ModelSwitchInstructions;
|
||||
pub(crate) use multi_agent_mode_instructions::MultiAgentModeInstructions;
|
||||
pub(crate) use multi_agent_role_instructions::MultiAgentRoleInstructions;
|
||||
pub(crate) use multi_agent_usage_hint::MultiAgentUsageHint;
|
||||
pub(crate) use network_rule_saved::NetworkRuleSaved;
|
||||
|
||||
@@ -7,7 +7,7 @@ const EXPLICIT_REQUEST_ONLY_MULTI_AGENT_MODE_TEXT: &str = "Any earlier instructi
|
||||
const PROACTIVE_MULTI_AGENT_MODE_TEXT: &str = "Proactive multi-agent delegation is active. Any earlier instruction requiring an explicit user request before spawning sub-agents no longer applies. Use sub-agents when parallel work would materially improve speed or quality. This mode remains active until a later multi-agent mode developer message changes it.";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(super) struct MultiAgentModeInstructions {
|
||||
pub(crate) struct MultiAgentModeInstructions {
|
||||
multi_agent_mode: MultiAgentMode,
|
||||
}
|
||||
|
||||
|
||||
@@ -13,9 +13,11 @@ use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::openai_models::MultiAgentMessages;
|
||||
use codex_protocol::openai_models::MultiAgentRoleMessages;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::openai_models::ReasoningEffortPreset;
|
||||
use codex_protocol::protocol::AgentStatus;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::MULTI_AGENT_MODE_OPEN_TAG;
|
||||
use codex_protocol::protocol::Op;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
@@ -81,6 +83,12 @@ const ROLE_REASONING_EFFORT: ReasoningEffort = ReasoningEffort::High;
|
||||
const SUBAGENT_START_CONTEXT: &str = "subagent start context reaches child";
|
||||
const SUBAGENT_STOP_CONTINUATION: &str = "continue only the child";
|
||||
const INTERNAL_SUBAGENT_PROMPT: &str = "internal subagent: review";
|
||||
const FULL_HISTORY_MULTI_AGENT_MODE_HINT: &str = "Delegate independent work to another agent.";
|
||||
const FULL_HISTORY_SHARED_USAGE_HINT: &str = "Shared delegation guidance.";
|
||||
const FULL_HISTORY_PROACTIVE_PROMPT: &str = "switch to proactive delegation";
|
||||
const FULL_HISTORY_EXPLICIT_PROMPT: &str = "restore explicit-only delegation";
|
||||
const FULL_HISTORY_PROACTIVE_POLICY: &str = "Proactive multi-agent delegation is active.";
|
||||
const FULL_HISTORY_EXPLICIT_POLICY: &str = "Do not spawn sub-agents unless the user or applicable AGENTS.md/skill instructions explicitly ask";
|
||||
|
||||
fn body_contains(req: &wiremock::Request, text: &str) -> bool {
|
||||
decoded_body(req)
|
||||
@@ -1050,12 +1058,16 @@ enum FullHistoryV2ModelSelection {
|
||||
ExplicitOverride,
|
||||
WorldStateIdentity,
|
||||
CurrentTimeReminders,
|
||||
MultiAgentModeInstructions,
|
||||
MultiAgentModeTransitions,
|
||||
}
|
||||
|
||||
#[test_case(FullHistoryV2ModelSelection::ConfiguredDefault; "configured default with omitted fork_turns")]
|
||||
#[test_case(FullHistoryV2ModelSelection::ExplicitOverride; "explicit override with fork_turns all")]
|
||||
#[test_case(FullHistoryV2ModelSelection::WorldStateIdentity; "world state appends context window when agent identity changes")]
|
||||
#[test_case(FullHistoryV2ModelSelection::CurrentTimeReminders; "full fork drops inherited current-time reminders")]
|
||||
#[test_case(FullHistoryV2ModelSelection::MultiAgentModeInstructions; "full fork drops inherited multi-agent mode instructions")]
|
||||
#[test_case(FullHistoryV2ModelSelection::MultiAgentModeTransitions; "full fork restores explicit policy after proactive transition")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn spawned_full_history_v2_child_uses_model_precedence_without_dropping_context(
|
||||
selection: FullHistoryV2ModelSelection,
|
||||
@@ -1076,7 +1088,9 @@ async fn spawned_full_history_v2_child_uses_model_precedence_without_dropping_co
|
||||
let (spawn_args, expected_model, expected_reasoning_effort) = match selection {
|
||||
FullHistoryV2ModelSelection::ConfiguredDefault
|
||||
| FullHistoryV2ModelSelection::WorldStateIdentity
|
||||
| FullHistoryV2ModelSelection::CurrentTimeReminders => (
|
||||
| FullHistoryV2ModelSelection::CurrentTimeReminders
|
||||
| FullHistoryV2ModelSelection::MultiAgentModeInstructions
|
||||
| FullHistoryV2ModelSelection::MultiAgentModeTransitions => (
|
||||
json!({
|
||||
"message": CHILD_PROMPT,
|
||||
"task_name": "worker",
|
||||
@@ -1097,6 +1111,35 @@ async fn spawned_full_history_v2_child_uses_model_precedence_without_dropping_co
|
||||
),
|
||||
};
|
||||
let spawn_args = serde_json::to_string(&spawn_args)?;
|
||||
let mode_transition_turns = if matches!(
|
||||
selection,
|
||||
FullHistoryV2ModelSelection::MultiAgentModeTransitions
|
||||
) {
|
||||
Some((
|
||||
mount_sse_once_match(
|
||||
&server,
|
||||
|req: &wiremock::Request| body_contains(req, FULL_HISTORY_PROACTIVE_PROMPT),
|
||||
sse(vec![
|
||||
ev_response_created("resp-proactive-1"),
|
||||
ev_assistant_message("msg-proactive-1", "proactive done"),
|
||||
ev_completed("resp-proactive-1"),
|
||||
]),
|
||||
)
|
||||
.await,
|
||||
mount_sse_once_match(
|
||||
&server,
|
||||
|req: &wiremock::Request| body_contains(req, FULL_HISTORY_EXPLICIT_PROMPT),
|
||||
sse(vec![
|
||||
ev_response_created("resp-explicit-1"),
|
||||
ev_assistant_message("msg-explicit-1", "explicit done"),
|
||||
ev_completed("resp-explicit-1"),
|
||||
]),
|
||||
)
|
||||
.await,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let spawn_turn = mount_sse_once_match(
|
||||
&server,
|
||||
|req: &wiremock::Request| body_contains(req, TURN_1_PROMPT),
|
||||
@@ -1162,6 +1205,18 @@ async fn spawned_full_history_v2_child_uses_model_precedence_without_dropping_co
|
||||
root: Some(format!("{model} root role.")),
|
||||
subagent: Some(format!("{model} subagent role.")),
|
||||
});
|
||||
if matches!(
|
||||
selection,
|
||||
FullHistoryV2ModelSelection::MultiAgentModeTransitions
|
||||
) && model == INHERITED_MODEL
|
||||
{
|
||||
model_info
|
||||
.supported_reasoning_levels
|
||||
.push(ReasoningEffortPreset {
|
||||
effort: ReasoningEffort::Ultra,
|
||||
description: "Ultra".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
if matches!(selection, FullHistoryV2ModelSelection::WorldStateIdentity) {
|
||||
config
|
||||
@@ -1180,6 +1235,22 @@ async fn spawned_full_history_v2_child_uses_model_precedence_without_dropping_co
|
||||
..CurrentTimeReminderConfig::default()
|
||||
});
|
||||
}
|
||||
if matches!(
|
||||
selection,
|
||||
FullHistoryV2ModelSelection::MultiAgentModeInstructions
|
||||
) {
|
||||
config.multi_agent_v2.multi_agent_mode_hint_text =
|
||||
Some(FULL_HISTORY_MULTI_AGENT_MODE_HINT.to_string());
|
||||
}
|
||||
if matches!(
|
||||
selection,
|
||||
FullHistoryV2ModelSelection::MultiAgentModeTransitions
|
||||
) {
|
||||
config.multi_agent_v2.root_agent_usage_hint_text =
|
||||
Some(FULL_HISTORY_SHARED_USAGE_HINT.to_string());
|
||||
config.multi_agent_v2.subagent_usage_hint_text =
|
||||
Some(FULL_HISTORY_SHARED_USAGE_HINT.to_string());
|
||||
}
|
||||
config.model = Some(INHERITED_MODEL.to_string());
|
||||
config.model_reasoning_effort = Some(INHERITED_REASONING_EFFORT);
|
||||
config.agent_default_subagent_model = Some(V2_DEFAULT_MODEL.to_string());
|
||||
@@ -1199,23 +1270,120 @@ async fn spawned_full_history_v2_child_uses_model_precedence_without_dropping_co
|
||||
|
||||
test.submit_turn(TURN_0_FORK_PROMPT).await?;
|
||||
let _ = seed_turn.single_request();
|
||||
if let Some((proactive_turn, explicit_turn)) = mode_transition_turns {
|
||||
for (prompt, effort, approval_policy) in [
|
||||
(
|
||||
FULL_HISTORY_PROACTIVE_PROMPT,
|
||||
ReasoningEffort::Ultra,
|
||||
Some(AskForApproval::OnRequest),
|
||||
),
|
||||
(FULL_HISTORY_EXPLICIT_PROMPT, ReasoningEffort::High, None),
|
||||
] {
|
||||
test.codex
|
||||
.start_or_steer_turn(
|
||||
TurnInputRequest::user_input(vec![UserInput::Text {
|
||||
text: prompt.to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}])
|
||||
.with_thread_settings(ThreadSettingsOverrides {
|
||||
effort: Some(Some(effort)),
|
||||
approval_policy,
|
||||
..Default::default()
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
wait_for_event(&test.codex, |event| {
|
||||
matches!(event, EventMsg::TurnComplete(_))
|
||||
})
|
||||
.await;
|
||||
}
|
||||
let proactive_request = proactive_turn.single_request();
|
||||
let proactive_developer_messages = proactive_request.message_input_text_groups("developer");
|
||||
assert!(
|
||||
proactive_developer_messages.iter().any(|message| {
|
||||
message.len() > 1
|
||||
&& message
|
||||
.iter()
|
||||
.any(|text| text.contains(FULL_HISTORY_PROACTIVE_POLICY))
|
||||
}),
|
||||
"proactive policy should share a developer message with unrelated context: {proactive_developer_messages:?}"
|
||||
);
|
||||
let explicit_request = explicit_turn.single_request();
|
||||
assert!(
|
||||
explicit_request
|
||||
.message_input_texts("developer")
|
||||
.iter()
|
||||
.any(|text| text.contains(FULL_HISTORY_EXPLICIT_POLICY)),
|
||||
"restored parent policy should require an explicit delegation request"
|
||||
);
|
||||
}
|
||||
test.submit_turn(TURN_1_PROMPT).await?;
|
||||
let parent_request = spawn_turn.single_request();
|
||||
|
||||
let child_request = wait_for_request_with_model(&child_request_log, expected_model).await?;
|
||||
assert!(child_request.body_contains_text(TURN_0_FORK_PROMPT));
|
||||
let child_developer_messages = child_request.message_input_texts("developer");
|
||||
assert_eq!(
|
||||
child_developer_messages
|
||||
.iter()
|
||||
.filter(|message| message.contains(&format!("{expected_model} subagent role.")))
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
if !matches!(
|
||||
selection,
|
||||
FullHistoryV2ModelSelection::MultiAgentModeTransitions
|
||||
) {
|
||||
assert_eq!(
|
||||
child_developer_messages
|
||||
.iter()
|
||||
.filter(|message| message.contains(&format!("{expected_model} subagent role.")))
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
}
|
||||
assert!(!child_developer_messages.iter().any(|message| {
|
||||
message.contains(&format!("{INHERITED_MODEL} root role."))
|
||||
|| message.contains(&format!("{INHERITED_MODEL} subagent role."))
|
||||
}));
|
||||
if matches!(
|
||||
selection,
|
||||
FullHistoryV2ModelSelection::MultiAgentModeInstructions
|
||||
) {
|
||||
let mode_instruction_count = |request: &ResponsesRequest| {
|
||||
request
|
||||
.message_input_texts("developer")
|
||||
.into_iter()
|
||||
.filter(|text| text.starts_with(MULTI_AGENT_MODE_OPEN_TAG))
|
||||
.count()
|
||||
};
|
||||
assert_eq!(
|
||||
(
|
||||
mode_instruction_count(&parent_request),
|
||||
mode_instruction_count(&child_request),
|
||||
),
|
||||
(1, 1)
|
||||
);
|
||||
}
|
||||
if matches!(
|
||||
selection,
|
||||
FullHistoryV2ModelSelection::MultiAgentModeTransitions
|
||||
) {
|
||||
assert_eq!(
|
||||
(
|
||||
child_developer_messages
|
||||
.iter()
|
||||
.filter(|message| message.starts_with(MULTI_AGENT_MODE_OPEN_TAG))
|
||||
.count(),
|
||||
child_developer_messages
|
||||
.iter()
|
||||
.filter(|message| message.contains(FULL_HISTORY_EXPLICIT_POLICY))
|
||||
.count(),
|
||||
child_developer_messages
|
||||
.iter()
|
||||
.filter(|message| message.contains(FULL_HISTORY_PROACTIVE_POLICY))
|
||||
.count(),
|
||||
child_developer_messages
|
||||
.iter()
|
||||
.filter(|message| message.contains(FULL_HISTORY_SHARED_USAGE_HINT))
|
||||
.count(),
|
||||
),
|
||||
(1, 1, 0, 1)
|
||||
);
|
||||
}
|
||||
if matches!(selection, FullHistoryV2ModelSelection::CurrentTimeReminders) {
|
||||
let reminder_count = |request: &ResponsesRequest| {
|
||||
request
|
||||
|
||||
Reference in New Issue
Block a user