From ac1fcb04eb63f7575c3cd49db9bb5748316d65a7 Mon Sep 17 00:00:00 2001 From: Sayan Sisodiya Date: Fri, 22 May 2026 08:28:30 -0700 Subject: [PATCH] Refine standalone web search history handling --- codex-rs/core/src/tools/spec_plan_tests.rs | 15 +++++ codex-rs/ext/web-search/src/history.rs | 68 ++++++++++++++++------ codex-rs/ext/web-search/src/output.rs | 2 + 3 files changed, 67 insertions(+), 18 deletions(-) diff --git a/codex-rs/core/src/tools/spec_plan_tests.rs b/codex-rs/core/src/tools/spec_plan_tests.rs index 3e0abeba14..e2f1345b24 100644 --- a/codex-rs/core/src/tools/spec_plan_tests.rs +++ b/codex-rs/core/src/tools/spec_plan_tests.rs @@ -265,6 +265,21 @@ impl ToolExecutor for WebRunExtensionTool { ToolName::namespaced("web", "run") } + fn spec(&self) -> ToolSpec { + ToolSpec::Namespace(codex_tools::ResponsesApiNamespace { + name: "web".to_string(), + description: "Test web namespace.".to_string(), + tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool { + name: "run".to_string(), + description: "Test standalone web search tool.".to_string(), + strict: false, + defer_loading: None, + parameters: codex_tools::JsonSchema::default(), + output_schema: None, + })], + }) + } + async fn handle( &self, _call: ExtensionToolCall, diff --git a/codex-rs/ext/web-search/src/history.rs b/codex-rs/ext/web-search/src/history.rs index 3413945c77..b7a26d32cf 100644 --- a/codex-rs/ext/web-search/src/history.rs +++ b/codex-rs/ext/web-search/src/history.rs @@ -1,10 +1,14 @@ use codex_api::SearchInput; +use codex_core::parse_turn_item; +use codex_protocol::items::TurnItem; use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseItem; use codex_tools::retain_tail_from_last_n_user_messages; use codex_tools::truncate_assistant_output_text_to_token_budget; const ASSISTANT_CONTEXT_TOKEN_LIMIT: usize = 1_000; +const ASSISTANT_ROLE: &str = "assistant"; +const USER_ROLE: &str = "user"; /// Builds the conversation tail for standalone web search. /// @@ -23,13 +27,17 @@ pub(crate) fn recent_input(items: &[ResponseItem]) -> Option { fn push_visible_message(messages: &mut Vec, item: &ResponseItem) { match item { - ResponseItem::Message { role, .. } if role == "assistant" => messages.push(item.clone()), + ResponseItem::Message { role, .. } if role == ASSISTANT_ROLE => { + messages.push(item.clone()); + } ResponseItem::Message { id, role, content, phase, - } if role == "user" => { + } if role == USER_ROLE + && matches!(parse_turn_item(item), Some(TurnItem::UserMessage(_))) => + { let content = content .iter() .filter(|item| matches!(item, ContentItem::InputText { .. })) @@ -55,13 +63,15 @@ mod tests { use codex_protocol::models::ResponseItem; use pretty_assertions::assert_eq; + use super::ASSISTANT_ROLE; + use super::USER_ROLE; use super::recent_input; fn message(role: &str, text: &str) -> ResponseItem { ResponseItem::Message { id: None, role: role.to_string(), - content: vec![if role == "assistant" { + content: vec![if role == ASSISTANT_ROLE { ContentItem::OutputText { text: text.to_string(), } @@ -78,9 +88,9 @@ mod tests { fn keeps_current_user_and_previous_visible_turn() { let items = vec![ message("system", "system"), - message("user", "old user"), - message("assistant", "old assistant"), - message("user", "previous user"), + message(USER_ROLE, "old user"), + message(ASSISTANT_ROLE, "old assistant"), + message(USER_ROLE, "previous user"), ResponseItem::FunctionCall { id: None, name: "tool".to_string(), @@ -88,18 +98,18 @@ mod tests { arguments: "{}".to_string(), call_id: "call-1".to_string(), }, - message("assistant", "previous assistant"), + message(ASSISTANT_ROLE, "previous assistant"), message("developer", "developer"), - message("user", "current user"), - message("assistant", "current commentary"), + message(USER_ROLE, "current user"), + message(ASSISTANT_ROLE, "current commentary"), ]; assert_eq!( recent_input(&items), Some(SearchInput::Items(vec![ - message("user", "previous user"), - message("assistant", "previous assistant"), - message("user", "current user"), + message(USER_ROLE, "previous user"), + message(ASSISTANT_ROLE, "previous assistant"), + message(USER_ROLE, "current user"), ])) ); } @@ -108,7 +118,7 @@ mod tests { fn keeps_only_text_from_recent_user_messages() { let previous_user = ResponseItem::Message { id: None, - role: "user".to_string(), + role: USER_ROLE.to_string(), content: vec![ ContentItem::InputText { text: "previous user".to_string(), @@ -122,16 +132,38 @@ mod tests { }; let items = vec![ previous_user, - message("assistant", "previous assistant"), - message("user", "current user"), + message(ASSISTANT_ROLE, "previous assistant"), + message(USER_ROLE, "current user"), ]; assert_eq!( recent_input(&items), Some(SearchInput::Items(vec![ - message("user", "previous user"), - message("assistant", "previous assistant"), - message("user", "current user"), + message(USER_ROLE, "previous user"), + message(ASSISTANT_ROLE, "previous assistant"), + message(USER_ROLE, "current user"), + ])) + ); + } + + #[test] + fn ignores_contextual_user_messages_when_selecting_recent_turns() { + let items = vec![ + message(USER_ROLE, "previous user"), + message(ASSISTANT_ROLE, "previous assistant"), + message( + USER_ROLE, + "\n/tmp\n", + ), + message(USER_ROLE, "current user"), + ]; + + assert_eq!( + recent_input(&items), + Some(SearchInput::Items(vec![ + message(USER_ROLE, "previous user"), + message(ASSISTANT_ROLE, "previous assistant"), + message(USER_ROLE, "current user"), ])) ); } diff --git a/codex-rs/ext/web-search/src/output.rs b/codex-rs/ext/web-search/src/output.rs index 96c9bc54d4..124271c216 100644 --- a/codex-rs/ext/web-search/src/output.rs +++ b/codex-rs/ext/web-search/src/output.rs @@ -24,6 +24,8 @@ impl ToolOutput for EncryptedSearchOutput { } fn to_response_item(&self, call_id: &str, _payload: &ToolPayload) -> ResponseInputItem { + // TODO: Make standalone search honor memories.disable_on_external_context, + // as hosted web search does. ResponseInputItem::FunctionCallOutput { call_id: call_id.to_string(), output: FunctionCallOutputPayload::from_content_items(vec![