[core] expose rollout history boundaries

This commit is contained in:
Adam Van Ymeren
2026-06-24 23:27:07 -07:00
parent 7a321198d7
commit 6165ecf724
8 changed files with 94 additions and 21 deletions
+1 -17
View File
@@ -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<usize> {
let mut positions = Vec::new();
for (idx, item) in items.iter().enumerate() {
-1
View File
@@ -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;
+1
View File
@@ -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;
+23
View File
@@ -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;
@@ -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: "<environment_context><cwd>/tmp</cwd></environment_context>".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));
}
+1 -1
View File
@@ -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;
@@ -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;
@@ -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;