diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index 754262a736..e9b749fea9 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -12,7 +12,6 @@ mod client_common; mod realtime_context; mod realtime_conversation; mod realtime_prompt; -mod response_item_history; mod responses_metadata; mod responses_retry; pub(crate) mod session; diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 7f7d1bbc7a..cf793326de 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -208,6 +208,7 @@ mod input_queue; mod mcp; mod mcp_runtime; pub(crate) mod multi_agents; +mod response_item_history; mod review; mod rollout_budget; mod rollout_reconstruction; @@ -2709,24 +2710,28 @@ impl Session { } } - /// Records conversation items: append to history, persist to rollout, and - /// notify clients observing raw response items. - pub(crate) fn prepare_conversation_items_for_history<'a>( + /// Prepares the history and raw-response views used when recording conversation items. + fn prepare_conversation_items_for_recording<'a>( &self, turn_context: &TurnContext, items: &'a [ResponseItem], - ) -> Cow<'a, [ResponseItem]> { + ) -> (Cow<'a, [ResponseItem]>, Option>) { let mut items = Cow::Borrowed(items); prepare_response_items(items.to_mut()); // Most response items get their passthrough turn ID at the durable history boundary. for item in items.to_mut() { item.set_turn_id_if_missing(&turn_context.sub_id); } - if turn_context.config.features.enabled(Feature::ItemIds) { + let raw_response_items = if turn_context.config.features.enabled(Feature::ItemIds) { Self::assign_missing_response_item_ids(items) } else { items - } + }; + let history_items = response_item_history::canonicalize_for_durable_history( + &self.thread_id, + raw_response_items.as_ref(), + ); + (raw_response_items, history_items) } fn assign_missing_response_item_ids(items: Cow<'_, [ResponseItem]>) -> Cow<'_, [ResponseItem]> { @@ -2777,23 +2782,20 @@ impl Session { turn_context: &TurnContext, items: &[ResponseItem], ) { - let raw_items = self.prepare_conversation_items_for_history(turn_context, items); - let history_items = crate::response_item_history::prepare_for_durable_history( - &self.thread_id, - raw_items.as_ref(), - ); + let (raw_items, history_items) = + self.prepare_conversation_items_for_recording(turn_context, items); + let history_items = history_items.as_deref().unwrap_or(raw_items.as_ref()); { let mut state = self.state.lock().await; state .current_time_reminder - .note_recorded_items(history_items.as_ref()); + .note_recorded_items(history_items); state.record_items( history_items.iter(), turn_context.model_info.truncation_policy.into(), ); } - self.persist_rollout_response_items(history_items.as_ref()) - .await; + self.persist_rollout_response_items(history_items).await; self.send_raw_response_items(turn_context, raw_items.as_ref()) .await; } @@ -2885,11 +2887,11 @@ impl Session { ) { communication.set_turn_id_if_missing(&turn_context.sub_id); let response_item = communication.to_model_input_item(); - let items = self.prepare_conversation_items_for_history( + let (raw_items, history_items) = self.prepare_conversation_items_for_recording( turn_context, std::slice::from_ref(&response_item), ); - let items = items.as_ref(); + let items = history_items.as_deref().unwrap_or(raw_items.as_ref()); let response_item = items[0].clone(); { let mut state = self.state.lock().await; @@ -2906,7 +2908,8 @@ impl Session { RolloutItem::ResponseItem(response_item), ]) .await; - self.send_raw_response_items(turn_context, items).await; + self.send_raw_response_items(turn_context, raw_items.as_ref()) + .await; } async fn maybe_warn_on_server_model_mismatch( diff --git a/codex-rs/core/src/response_item_history.rs b/codex-rs/core/src/session/response_item_history.rs similarity index 86% rename from codex-rs/core/src/response_item_history.rs rename to codex-rs/core/src/session/response_item_history.rs index 2a964f0c0b..4bf5f8c372 100644 --- a/codex-rs/core/src/response_item_history.rs +++ b/codex-rs/core/src/session/response_item_history.rs @@ -2,26 +2,29 @@ use codex_protocol::ThreadId; use codex_protocol::models::ResponseItem; use codex_protocol::models::SearchToolCallParams; use serde::Deserialize; -use std::borrow::Cow; use tracing::warn; const INVALID_TOOL_SEARCH_QUERY: &str = "[invalid tool_search arguments omitted]"; -/// Applies response-item-specific normalization before model-visible history and persistence. -pub(super) fn prepare_for_durable_history<'a>( +/// Returns a canonical durable-history view when an item must differ from its raw event. +/// +/// Client-executed tool search is the only case handled here because its arguments have a fixed +/// schema that can be safely deserialized and reserialized. Function-call arguments and custom +/// tool input are intentionally preserved as schema-specific or freeform strings. +pub(super) fn canonicalize_for_durable_history( thread_id: &ThreadId, - items: &'a [ResponseItem], -) -> Cow<'a, [ResponseItem]> { + items: &[ResponseItem], +) -> Option> { if !items.iter().any(|item| { matches!( item, ResponseItem::ToolSearchCall { execution, .. } if execution == "client" ) }) { - return Cow::Borrowed(items); + return None; } - Cow::Owned( + Some( items .iter() .filter_map(|item| {