mirror of
https://github.com/openai/codex.git
synced 2026-05-23 12:34:25 +00:00
Strip parent startup context from forked history
This commit is contained in:
@@ -127,6 +127,52 @@ fn keep_forked_rollout_item(item: &RolloutItem) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
fn strip_parent_startup_context_bundle_from_forked_rollout(items: &mut Vec<RolloutItem>) {
|
||||
let Some(turn_context_idx) = items
|
||||
.iter()
|
||||
.position(|item| matches!(item, RolloutItem::TurnContext(_)))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let first_turn_context_is_before_parent_turn = items[..turn_context_idx].iter().all(|item| {
|
||||
!matches!(
|
||||
item,
|
||||
RolloutItem::ResponseItem(response_item)
|
||||
if crate::context_manager::is_user_turn_boundary(response_item)
|
||||
)
|
||||
});
|
||||
if !first_turn_context_is_before_parent_turn {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut context_start = turn_context_idx;
|
||||
while context_start > 0 {
|
||||
let is_startup_context_item = match &items[context_start - 1] {
|
||||
RolloutItem::ResponseItem(ResponseItem::Message { role, .. })
|
||||
if role == "developer" =>
|
||||
{
|
||||
true
|
||||
}
|
||||
RolloutItem::ResponseItem(ResponseItem::Message { role, content, .. })
|
||||
if role == "user"
|
||||
&& crate::event_mapping::is_contextual_user_message_content(content) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
if !is_startup_context_item {
|
||||
break;
|
||||
}
|
||||
|
||||
context_start -= 1;
|
||||
}
|
||||
|
||||
if context_start < turn_context_idx {
|
||||
items.drain(context_start..=turn_context_idx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Control-plane handle for multi-agent operations.
|
||||
/// `AgentControl` is held by each session (via `SessionServices`). It provides capability to
|
||||
/// spawn new agents and the inter-agent communication layer.
|
||||
@@ -396,6 +442,7 @@ impl AgentControl {
|
||||
forked_rollout_items =
|
||||
truncate_rollout_to_last_n_fork_turns(&forked_rollout_items, *last_n_turns);
|
||||
}
|
||||
strip_parent_startup_context_bundle_from_forked_rollout(&mut forked_rollout_items);
|
||||
// MultiAgentV2 root/subagent usage hints are injected as standalone developer
|
||||
// messages at thread start. When forking history, drop hints from the parent
|
||||
// so the child gets a fresh hint that matches its own session source/config.
|
||||
@@ -417,56 +464,19 @@ impl AgentControl {
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let developer_instruction_texts_to_filter = match parent_thread.as_ref() {
|
||||
Some(parent_thread) => parent_thread
|
||||
.codex
|
||||
.session
|
||||
.get_config()
|
||||
.await
|
||||
.developer_instructions
|
||||
.clone(),
|
||||
None => config.developer_instructions.clone(),
|
||||
}
|
||||
.into_iter()
|
||||
.filter(|developer_instructions| !developer_instructions.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
// Parent developer instructions may be one fragment inside a larger startup context
|
||||
// message. Strip only that fragment so parent context updates remain fork-visible.
|
||||
forked_rollout_items = forked_rollout_items
|
||||
.into_iter()
|
||||
.filter_map(|mut item| {
|
||||
if !keep_forked_rollout_item(&item) {
|
||||
return None;
|
||||
}
|
||||
forked_rollout_items.retain(|item| {
|
||||
if let RolloutItem::ResponseItem(ResponseItem::Message { role, content, .. }) = item
|
||||
&& role == "developer"
|
||||
&& let [ContentItem::InputText { text }] = content.as_slice()
|
||||
&& multi_agent_v2_usage_hint_texts_to_filter
|
||||
.iter()
|
||||
.any(|usage_hint_text| usage_hint_text == text)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if let RolloutItem::ResponseItem(ResponseItem::Message { role, content, .. }) =
|
||||
&mut item
|
||||
&& role == "developer"
|
||||
{
|
||||
if let [ContentItem::InputText { text }] = content.as_slice()
|
||||
&& multi_agent_v2_usage_hint_texts_to_filter
|
||||
.iter()
|
||||
.any(|usage_hint_text| usage_hint_text == text)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
content.retain(|content_item| {
|
||||
let ContentItem::InputText { text } = content_item else {
|
||||
return true;
|
||||
};
|
||||
!developer_instruction_texts_to_filter
|
||||
.iter()
|
||||
.any(|developer_instructions| developer_instructions == text)
|
||||
});
|
||||
if content.is_empty() {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
Some(item)
|
||||
})
|
||||
.collect();
|
||||
keep_forked_rollout_item(item)
|
||||
});
|
||||
|
||||
state
|
||||
.fork_thread_with_source(
|
||||
|
||||
@@ -14,17 +14,22 @@ use codex_features::Feature;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_protocol::AgentPath;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::MessagePhase;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::ErrorEvent;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::InterAgentCommunication;
|
||||
use codex_protocol::protocol::RolloutItem;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use codex_protocol::protocol::TurnAbortReason;
|
||||
use codex_protocol::protocol::TurnAbortedEvent;
|
||||
use codex_protocol::protocol::TurnCompleteEvent;
|
||||
use codex_protocol::protocol::TurnContextItem;
|
||||
use codex_protocol::protocol::TurnStartedEvent;
|
||||
use codex_thread_store::ArchiveThreadParams;
|
||||
use codex_thread_store::LocalThreadStore;
|
||||
@@ -83,6 +88,89 @@ fn spawn_agent_call(call_id: &str) -> ResponseItem {
|
||||
}
|
||||
}
|
||||
|
||||
fn turn_context_item_for_test(turn_id: &str) -> TurnContextItem {
|
||||
TurnContextItem {
|
||||
turn_id: Some(turn_id.to_string()),
|
||||
trace_id: None,
|
||||
cwd: std::path::PathBuf::from("/tmp"),
|
||||
current_date: None,
|
||||
timezone: None,
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
sandbox_policy: SandboxPolicy::ReadOnly {
|
||||
network_access: false,
|
||||
},
|
||||
permission_profile: None,
|
||||
network: None,
|
||||
file_system_sandbox_policy: None,
|
||||
model: "gpt-test".to_string(),
|
||||
personality: None,
|
||||
collaboration_mode: None,
|
||||
realtime_active: None,
|
||||
effort: None,
|
||||
summary: ReasoningSummary::Auto,
|
||||
user_instructions: None,
|
||||
developer_instructions: None,
|
||||
final_output_json_schema: None,
|
||||
truncation_policy: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strips_only_parent_startup_context_bundle() {
|
||||
let parent_user_message = ResponseItem::Message {
|
||||
id: None,
|
||||
role: "user".to_string(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "parent seed context".to_string(),
|
||||
}],
|
||||
phase: None,
|
||||
};
|
||||
let parent_later_context_update = ResponseItem::Message {
|
||||
id: None,
|
||||
role: "developer".to_string(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "Parent later context update.".to_string(),
|
||||
}],
|
||||
phase: None,
|
||||
};
|
||||
let mut structural_rollout = vec![
|
||||
RolloutItem::ResponseItem(ResponseItem::Message {
|
||||
id: None,
|
||||
role: "developer".to_string(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "Parent startup context.".to_string(),
|
||||
}],
|
||||
phase: None,
|
||||
}),
|
||||
RolloutItem::ResponseItem(ResponseItem::Message {
|
||||
id: None,
|
||||
role: "user".to_string(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "<environment_context>\n<cwd>/tmp</cwd>\n</environment_context>".to_string(),
|
||||
}],
|
||||
phase: None,
|
||||
}),
|
||||
RolloutItem::TurnContext(turn_context_item_for_test("startup")),
|
||||
RolloutItem::ResponseItem(parent_user_message.clone()),
|
||||
RolloutItem::ResponseItem(parent_later_context_update.clone()),
|
||||
RolloutItem::TurnContext(turn_context_item_for_test("later")),
|
||||
];
|
||||
|
||||
strip_parent_startup_context_bundle_from_forked_rollout(&mut structural_rollout);
|
||||
|
||||
assert_eq!(structural_rollout.len(), 3);
|
||||
assert!(
|
||||
matches!(&structural_rollout[0], RolloutItem::ResponseItem(item) if item == &parent_user_message)
|
||||
);
|
||||
assert!(
|
||||
matches!(&structural_rollout[1], RolloutItem::ResponseItem(item) if item == &parent_later_context_update)
|
||||
);
|
||||
assert!(
|
||||
matches!(&structural_rollout[2], RolloutItem::TurnContext(_)),
|
||||
"later parent context updates should survive startup-bundle stripping"
|
||||
);
|
||||
}
|
||||
|
||||
struct AgentControlHarness {
|
||||
_home: TempDir,
|
||||
config: Config,
|
||||
@@ -628,42 +716,6 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
|
||||
.session
|
||||
.record_context_updates_and_set_reference_context_item(turn_context.as_ref())
|
||||
.await;
|
||||
let mut expected_history = parent_thread
|
||||
.codex
|
||||
.session
|
||||
.clone_history()
|
||||
.await
|
||||
.raw_items()
|
||||
.to_vec();
|
||||
for item in &mut expected_history {
|
||||
if let ResponseItem::Message { role, content, .. } = item
|
||||
&& role == "developer"
|
||||
{
|
||||
content.retain(|content_item| {
|
||||
!matches!(
|
||||
content_item,
|
||||
ContentItem::InputText { text }
|
||||
if text == "Parent developer instructions."
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
expected_history.retain(|item| match item {
|
||||
ResponseItem::Message { role, content, .. } if role == "developer" => {
|
||||
!content.is_empty()
|
||||
&& !matches!(
|
||||
content.as_slice(),
|
||||
[ContentItem::InputText { text }]
|
||||
if text == "Parent root guidance."
|
||||
|| text == "Parent subagent guidance."
|
||||
)
|
||||
}
|
||||
_ => true,
|
||||
});
|
||||
assert!(
|
||||
!expected_history.is_empty(),
|
||||
"test setup should keep parent startup context blocks"
|
||||
);
|
||||
parent_thread
|
||||
.inject_user_message_without_turn("parent seed context".to_string())
|
||||
.await;
|
||||
@@ -736,21 +788,42 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
|
||||
.expect("child thread should be registered");
|
||||
assert_ne!(child_thread_id, parent_thread_id);
|
||||
let history = child_thread.codex.session.clone_history().await;
|
||||
expected_history.extend([
|
||||
let message_summary = |item: &ResponseItem| match item {
|
||||
ResponseItem::Message {
|
||||
id: None,
|
||||
role: "user".to_string(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "parent seed context".to_string(),
|
||||
}],
|
||||
phase: None,
|
||||
},
|
||||
assistant_message("parent final answer", Some(MessagePhase::FinalAnswer)),
|
||||
]);
|
||||
role,
|
||||
content,
|
||||
phase,
|
||||
..
|
||||
} => {
|
||||
let text = content
|
||||
.iter()
|
||||
.map(|content_item| match content_item {
|
||||
ContentItem::InputText { text } | ContentItem::OutputText { text } => {
|
||||
text.as_str()
|
||||
}
|
||||
_ => "",
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
(role.clone(), text, phase.clone())
|
||||
}
|
||||
_ => panic!("expected only message items in forked child history"),
|
||||
};
|
||||
assert_eq!(
|
||||
history.raw_items(),
|
||||
expected_history.as_slice(),
|
||||
"forked child history should keep parent context blocks while removing duplicated setup instructions"
|
||||
history
|
||||
.raw_items()
|
||||
.iter()
|
||||
.map(message_summary)
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
("user".to_string(), "parent seed context".to_string(), None,),
|
||||
(
|
||||
"assistant".to_string(),
|
||||
"parent final answer".to_string(),
|
||||
Some(MessagePhase::FinalAnswer),
|
||||
),
|
||||
],
|
||||
"forked child history should drop parent startup context while keeping parent conversation items"
|
||||
);
|
||||
let child_rollout_path = child_thread
|
||||
.rollout_path()
|
||||
@@ -765,7 +838,6 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
|
||||
!child_rollout.contains("Parent root guidance."),
|
||||
"forked child rollout should not retain parent multi-agent setup guidance"
|
||||
);
|
||||
|
||||
let expected = (
|
||||
child_thread_id,
|
||||
Op::UserInput {
|
||||
|
||||
Reference in New Issue
Block a user