From 6165ecf724b0a288f4dd6439bf686029a29fe1f1 Mon Sep 17 00:00:00 2001 From: Adam Van Ymeren Date: Mon, 15 Jun 2026 13:42:18 -0700 Subject: [PATCH 1/3] [core] expose rollout history boundaries --- codex-rs/core/src/context_manager/history.rs | 18 +---- codex-rs/core/src/context_manager/mod.rs | 1 - codex-rs/core/src/lib.rs | 1 + codex-rs/core/src/rollout_history.rs | 23 +++++++ codex-rs/core/src/rollout_history_tests.rs | 66 +++++++++++++++++++ codex-rs/core/src/session/handlers.rs | 2 +- .../src/session/rollout_reconstruction.rs | 2 +- .../core/src/thread_rollout_truncation.rs | 2 +- 8 files changed, 94 insertions(+), 21 deletions(-) create mode 100644 codex-rs/core/src/rollout_history.rs create mode 100644 codex-rs/core/src/rollout_history_tests.rs diff --git a/codex-rs/core/src/context_manager/history.rs b/codex-rs/core/src/context_manager/history.rs index defdbeb38976..b8bdcf22bbf7 100644 --- a/codex-rs/core/src/context_manager/history.rs +++ b/codex-rs/core/src/context_manager/history.rs @@ -5,6 +5,7 @@ use crate::context_manager::normalize; use crate::event_mapping::has_non_contextual_dev_message_content; use crate::event_mapping::is_contextual_dev_message_content; use crate::event_mapping::is_contextual_user_message_content; +use crate::rollout_history::is_user_turn_boundary; use crate::session::turn_context::TurnContext; use base64::Engine; use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; @@ -16,7 +17,6 @@ use codex_protocol::models::FunctionCallOutputPayload; use codex_protocol::models::ImageDetail; use codex_protocol::models::ResponseItem; use codex_protocol::openai_models::InputModality; -use codex_protocol::protocol::InterAgentCommunication; use codex_protocol::protocol::TokenUsage; use codex_protocol::protocol::TokenUsageInfo; use codex_protocol::protocol::TurnContextItem; @@ -747,22 +747,6 @@ fn is_model_generated_item(item: &ResponseItem) -> bool { } } -pub(crate) fn is_user_turn_boundary(item: &ResponseItem) -> bool { - if matches!(item, ResponseItem::AgentMessage { .. }) { - return true; - } - let ResponseItem::Message { role, content, .. } = item else { - return false; - }; - - (role == "user" && !is_contextual_user_message_content(content)) - || (role == "assistant" && is_inter_agent_instruction_content(content)) -} - -fn is_inter_agent_instruction_content(content: &[ContentItem]) -> bool { - InterAgentCommunication::is_message_content(content) -} - fn user_message_positions(items: &[ResponseItem]) -> Vec { let mut positions = Vec::new(); for (idx, item) in items.iter().enumerate() { diff --git a/codex-rs/core/src/context_manager/mod.rs b/codex-rs/core/src/context_manager/mod.rs index f811c8b373c9..052bb875e138 100644 --- a/codex-rs/core/src/context_manager/mod.rs +++ b/codex-rs/core/src/context_manager/mod.rs @@ -3,5 +3,4 @@ mod normalize; pub(crate) mod updates; pub(crate) use history::ContextManager; -pub(crate) use history::is_user_turn_boundary; pub(crate) use history::truncate_function_output_payload; diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index ad5f54123fa3..5fd50e304b60 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -136,6 +136,7 @@ pub use agents_md::LOCAL_AGENTS_MD_FILENAME; pub use agents_md::LoadedAgentsMd; mod rollout; mod rollout_budget; +pub mod rollout_history; pub(crate) mod safety; mod session_rollout_init_error; pub mod shell; diff --git a/codex-rs/core/src/rollout_history.rs b/codex-rs/core/src/rollout_history.rs new file mode 100644 index 000000000000..93f8a2f474fb --- /dev/null +++ b/codex-rs/core/src/rollout_history.rs @@ -0,0 +1,23 @@ +use crate::event_mapping::is_contextual_user_message_content; +use codex_protocol::models::ResponseItem; +use codex_protocol::protocol::InterAgentCommunication; + +/// Returns whether a response item starts a user-directed turn in stored history. +/// +/// Context-only user messages do not start turns. Agent messages and structured +/// assistant instructions do, since they represent input that expects a response. +pub fn is_user_turn_boundary(item: &ResponseItem) -> bool { + if matches!(item, ResponseItem::AgentMessage { .. }) { + return true; + } + let ResponseItem::Message { role, content, .. } = item else { + return false; + }; + + (role == "user" && !is_contextual_user_message_content(content)) + || (role == "assistant" && InterAgentCommunication::is_message_content(content)) +} + +#[cfg(test)] +#[path = "rollout_history_tests.rs"] +mod tests; diff --git a/codex-rs/core/src/rollout_history_tests.rs b/codex-rs/core/src/rollout_history_tests.rs new file mode 100644 index 000000000000..c530d72ff2e0 --- /dev/null +++ b/codex-rs/core/src/rollout_history_tests.rs @@ -0,0 +1,66 @@ +use super::is_user_turn_boundary; +use codex_protocol::AgentPath; +use codex_protocol::models::ContentItem; +use codex_protocol::models::ResponseItem; +use codex_protocol::protocol::InterAgentCommunication; + +fn message(role: &str, content: ContentItem) -> ResponseItem { + ResponseItem::Message { + id: None, + role: role.to_string(), + content: vec![content], + phase: None, + } +} + +#[test] +fn classifies_user_directed_turn_boundaries() { + let user_message = message( + "user", + ContentItem::InputText { + text: "hello".to_string(), + }, + ); + let agent_message = ResponseItem::AgentMessage { + author: "/root".to_string(), + recipient: "/root/worker".to_string(), + content: Vec::new(), + }; + let instruction = InterAgentCommunication::new( + AgentPath::root(), + AgentPath::root().join("worker").expect("valid agent path"), + Vec::new(), + "continue".to_string(), + /*trigger_turn*/ true, + ); + let assistant_instruction = message( + "assistant", + ContentItem::OutputText { + text: serde_json::to_string(&instruction).expect("instruction should serialize"), + }, + ); + + assert!(is_user_turn_boundary(&user_message)); + assert!(is_user_turn_boundary(&agent_message)); + assert!(is_user_turn_boundary(&assistant_instruction)); +} + +#[test] +fn excludes_context_and_non_input_items() { + let context_message = message( + "user", + ContentItem::InputText { + text: "/tmp".to_string(), + }, + ); + let assistant_message = message( + "assistant", + ContentItem::OutputText { + text: "done".to_string(), + }, + ); + + assert!(!is_user_turn_boundary(&context_message)); + assert!(!is_user_turn_boundary(&assistant_message)); + assert!(!is_user_turn_boundary(&ResponseItem::Other)); +} diff --git a/codex-rs/core/src/session/handlers.rs b/codex-rs/core/src/session/handlers.rs index 5c68893e7ff9..dbcb238ac744 100644 --- a/codex-rs/core/src/session/handlers.rs +++ b/codex-rs/core/src/session/handlers.rs @@ -49,7 +49,7 @@ use codex_protocol::protocol::WarningEvent; use codex_protocol::request_permissions::RequestPermissionsResponse; use codex_protocol::request_user_input::RequestUserInputResponse; -use crate::context_manager::is_user_turn_boundary; +use crate::rollout_history::is_user_turn_boundary; use codex_protocol::dynamic_tools::DynamicToolResponse; use codex_protocol::mcp::RequestId as ProtocolRequestId; use codex_rmcp_client::ElicitationAction; diff --git a/codex-rs/core/src/session/rollout_reconstruction.rs b/codex-rs/core/src/session/rollout_reconstruction.rs index 0b89caaf0575..8cb17536c8e8 100644 --- a/codex-rs/core/src/session/rollout_reconstruction.rs +++ b/codex-rs/core/src/session/rollout_reconstruction.rs @@ -1,6 +1,6 @@ use super::*; use crate::context::world_state::WorldStateSnapshot; -use crate::context_manager::is_user_turn_boundary; +use crate::rollout_history::is_user_turn_boundary; use codex_protocol::protocol::SessionContextWindow; use uuid::Uuid; diff --git a/codex-rs/core/src/thread_rollout_truncation.rs b/codex-rs/core/src/thread_rollout_truncation.rs index 25263b14b12a..47bcf15bf044 100644 --- a/codex-rs/core/src/thread_rollout_truncation.rs +++ b/codex-rs/core/src/thread_rollout_truncation.rs @@ -3,8 +3,8 @@ //! In core, "user turns" are detected by scanning `ResponseItem::Message` items and //! interpreting them via `event_mapping::parse_turn_item(...)`. -use crate::context_manager::is_user_turn_boundary; use crate::event_mapping; +use crate::rollout_history::is_user_turn_boundary; use codex_protocol::items::TurnItem; use codex_protocol::models::ResponseItem; use codex_protocol::protocol::EventMsg; From 8a1326dbbd1ed443b05508ea5ba43a14cc668a4c Mon Sep 17 00:00:00 2001 From: Adam Van Ymeren Date: Mon, 15 Jun 2026 21:47:45 -0700 Subject: [PATCH 2/3] [core] update rollout history test items --- codex-rs/core/src/rollout_history_tests.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/codex-rs/core/src/rollout_history_tests.rs b/codex-rs/core/src/rollout_history_tests.rs index c530d72ff2e0..70e4d64283c7 100644 --- a/codex-rs/core/src/rollout_history_tests.rs +++ b/codex-rs/core/src/rollout_history_tests.rs @@ -10,6 +10,7 @@ fn message(role: &str, content: ContentItem) -> ResponseItem { role: role.to_string(), content: vec![content], phase: None, + metadata: None, } } @@ -22,9 +23,11 @@ fn classifies_user_directed_turn_boundaries() { }, ); let agent_message = ResponseItem::AgentMessage { + id: None, author: "/root".to_string(), recipient: "/root/worker".to_string(), content: Vec::new(), + metadata: None, }; let instruction = InterAgentCommunication::new( AgentPath::root(), From dd4ce5314b403003b3401fb4186b1d6e52c57184 Mon Sep 17 00:00:00 2001 From: Adam Van Ymeren Date: Thu, 25 Jun 2026 09:05:32 -0700 Subject: [PATCH 3/3] [core] avoid metadata field in rollout history tests --- codex-rs/core/src/rollout_history_tests.rs | 27 +++++++++++----------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/codex-rs/core/src/rollout_history_tests.rs b/codex-rs/core/src/rollout_history_tests.rs index 70e4d64283c7..5031001dfc82 100644 --- a/codex-rs/core/src/rollout_history_tests.rs +++ b/codex-rs/core/src/rollout_history_tests.rs @@ -5,13 +5,12 @@ use codex_protocol::models::ResponseItem; use codex_protocol::protocol::InterAgentCommunication; fn message(role: &str, content: ContentItem) -> ResponseItem { - ResponseItem::Message { - id: None, - role: role.to_string(), - content: vec![content], - phase: None, - metadata: None, - } + serde_json::from_value(serde_json::json!({ + "type": "message", + "role": role, + "content": [content], + })) + .expect("message response item should deserialize") } #[test] @@ -22,13 +21,13 @@ fn classifies_user_directed_turn_boundaries() { text: "hello".to_string(), }, ); - let agent_message = ResponseItem::AgentMessage { - id: None, - author: "/root".to_string(), - recipient: "/root/worker".to_string(), - content: Vec::new(), - metadata: None, - }; + let agent_message = serde_json::from_value(serde_json::json!({ + "type": "agent_message", + "author": "/root", + "recipient": "/root/worker", + "content": [], + })) + .expect("agent message response item should deserialize"); let instruction = InterAgentCommunication::new( AgentPath::root(), AgentPath::root().join("worker").expect("valid agent path"),