From 92a3568f3d97dd8c41e1c995c7d76b13a02f31bb Mon Sep 17 00:00:00 2001 From: Anton Panasenko Date: Mon, 29 Jun 2026 16:20:51 -0700 Subject: [PATCH] codex: reuse history normalization for tool search (#30618) --- codex-rs/core/src/context_manager/mod.rs | 1 + .../core/src/context_manager/normalize.rs | 96 ++++++++++++++++++- codex-rs/core/src/session/mod.rs | 3 +- .../core/src/session/response_item_history.rs | 96 ------------------- 4 files changed, 97 insertions(+), 99 deletions(-) delete mode 100644 codex-rs/core/src/session/response_item_history.rs diff --git a/codex-rs/core/src/context_manager/mod.rs b/codex-rs/core/src/context_manager/mod.rs index f811c8b373..e97b5b0fd6 100644 --- a/codex-rs/core/src/context_manager/mod.rs +++ b/codex-rs/core/src/context_manager/mod.rs @@ -5,3 +5,4 @@ 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; +pub(crate) use normalize::canonicalize_for_durable_history; diff --git a/codex-rs/core/src/context_manager/normalize.rs b/codex-rs/core/src/context_manager/normalize.rs index 3fd1a59a8f..6592705111 100644 --- a/codex-rs/core/src/context_manager/normalize.rs +++ b/codex-rs/core/src/context_manager/normalize.rs @@ -1,19 +1,113 @@ +use codex_protocol::ThreadId; use codex_protocol::models::ContentItem; use codex_protocol::models::FunctionCallOutputContentItem; use codex_protocol::models::FunctionCallOutputPayload; use codex_protocol::models::ResponseItem; +use codex_protocol::models::SearchToolCallParams; use codex_protocol::openai_models::InputModality; +use serde::Deserialize; use std::collections::HashSet; +use tracing::info; +use tracing::warn; use uuid::Uuid; use crate::util::error_or_panic; -use tracing::info; const IMAGE_CONTENT_OMITTED_PLACEHOLDER: &str = "image content omitted because you do not support image input"; +const INVALID_TOOL_SEARCH_QUERY: &str = "[invalid tool_search arguments omitted]"; // Changing this value would change model-visible IDs and invalidate prompt caches. const SYNTHETIC_OUTPUT_ID_NAMESPACE: Uuid = Uuid::from_u128(0x90d38d3e_6a5b_4d52_bfe2_2f1e634bfac4); +/// 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(crate) fn canonicalize_for_durable_history( + thread_id: &ThreadId, + items: &[ResponseItem], +) -> Option> { + if !items.iter().any(|item| { + matches!( + item, + ResponseItem::ToolSearchCall { execution, .. } if execution == "client" + ) + }) { + return None; + } + + Some( + items + .iter() + .filter_map(|item| { + let ResponseItem::ToolSearchCall { + id, + call_id, + status, + execution, + arguments, + internal_chat_message_metadata_passthrough, + } = item + else { + return Some(item.clone()); + }; + if execution != "client" { + return Some(item.clone()); + } + + let Some(call_id) = call_id.as_deref().filter(|call_id| !call_id.is_empty()) else { + warn!( + %thread_id, + "dropping client tool_search call with missing call_id from durable history" + ); + return None; + }; + + let canonical_arguments = match SearchToolCallParams::deserialize(arguments) { + Ok(params) => match serde_json::to_value(params) { + Ok(canonical_arguments) => canonical_arguments, + Err(err) => { + warn!( + %thread_id, + call_id, + %err, + "failed to serialize canonical client tool_search arguments" + ); + invalid_tool_search_arguments() + } + }, + Err(err) => { + warn!( + %thread_id, + call_id, + error_category = ?err.classify(), + error_line = err.line(), + error_column = err.column(), + "replacing malformed client tool_search arguments in durable history" + ); + invalid_tool_search_arguments() + } + }; + + Some(ResponseItem::ToolSearchCall { + id: id.clone(), + call_id: Some(call_id.to_string()), + status: status.clone(), + execution: execution.clone(), + arguments: canonical_arguments, + internal_chat_message_metadata_passthrough: + internal_chat_message_metadata_passthrough.clone(), + }) + }) + .collect(), + ) +} + +fn invalid_tool_search_arguments() -> serde_json::Value { + serde_json::json!({ "query": INVALID_TOOL_SEARCH_QUERY }) +} + pub(crate) fn ensure_call_outputs_present(items: &mut Vec) { let mut function_output_ids = HashSet::new(); let mut tool_search_output_ids = HashSet::new(); diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index cf793326de..3ab78aa47c 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -208,7 +208,6 @@ 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; @@ -2727,7 +2726,7 @@ impl Session { } else { items }; - let history_items = response_item_history::canonicalize_for_durable_history( + let history_items = crate::context_manager::canonicalize_for_durable_history( &self.thread_id, raw_response_items.as_ref(), ); diff --git a/codex-rs/core/src/session/response_item_history.rs b/codex-rs/core/src/session/response_item_history.rs deleted file mode 100644 index 4bf5f8c372..0000000000 --- a/codex-rs/core/src/session/response_item_history.rs +++ /dev/null @@ -1,96 +0,0 @@ -use codex_protocol::ThreadId; -use codex_protocol::models::ResponseItem; -use codex_protocol::models::SearchToolCallParams; -use serde::Deserialize; -use tracing::warn; - -const INVALID_TOOL_SEARCH_QUERY: &str = "[invalid tool_search arguments omitted]"; - -/// 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: &[ResponseItem], -) -> Option> { - if !items.iter().any(|item| { - matches!( - item, - ResponseItem::ToolSearchCall { execution, .. } if execution == "client" - ) - }) { - return None; - } - - Some( - items - .iter() - .filter_map(|item| { - let ResponseItem::ToolSearchCall { - id, - call_id, - status, - execution, - arguments, - internal_chat_message_metadata_passthrough, - } = item - else { - return Some(item.clone()); - }; - if execution != "client" { - return Some(item.clone()); - } - - let Some(call_id) = call_id.as_deref().filter(|call_id| !call_id.is_empty()) else { - warn!( - %thread_id, - "dropping client tool_search call with missing call_id from durable history" - ); - return None; - }; - - let canonical_arguments = match SearchToolCallParams::deserialize(arguments) { - Ok(params) => match serde_json::to_value(params) { - Ok(canonical_arguments) => canonical_arguments, - Err(err) => { - warn!( - %thread_id, - call_id, - %err, - "failed to serialize canonical client tool_search arguments" - ); - invalid_tool_search_arguments() - } - }, - Err(err) => { - warn!( - %thread_id, - call_id, - error_category = ?err.classify(), - error_line = err.line(), - error_column = err.column(), - "replacing malformed client tool_search arguments in durable history" - ); - invalid_tool_search_arguments() - } - }; - - Some(ResponseItem::ToolSearchCall { - id: id.clone(), - call_id: Some(call_id.to_string()), - status: status.clone(), - execution: execution.clone(), - arguments: canonical_arguments, - internal_chat_message_metadata_passthrough: - internal_chat_message_metadata_passthrough.clone(), - }) - }) - .collect(), - ) -} - -fn invalid_tool_search_arguments() -> serde_json::Value { - serde_json::json!({ "query": INVALID_TOOL_SEARCH_QUERY }) -}