codex: reuse history normalization for tool search (#30618)

This commit is contained in:
Anton Panasenko
2026-06-29 16:20:51 -07:00
parent a47c406a39
commit 92a3568f3d
4 changed files with 97 additions and 99 deletions
+1
View File
@@ -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;
+95 -1
View File
@@ -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<Vec<ResponseItem>> {
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<ResponseItem>) {
let mut function_output_ids = HashSet::new();
let mut tool_search_output_ids = HashSet::new();
+1 -2
View File
@@ -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(),
);
@@ -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<Vec<ResponseItem>> {
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 })
}