mirror of
https://github.com/openai/codex.git
synced 2026-04-28 08:34:54 +00:00
20 KiB
20 KiB
PR #2852: Following up on #2371 post commit feedback
- URL: https://github.com/openai/codex/pull/2852
- Author: aibrahim-oai
- Created: 2025-08-28 20:35:14 UTC
- Updated: 2025-08-29 02:24:46 UTC
- Changes: +89/-49, Files changed: 13, Commits: 5
Description
- Introduce websearch end to complement the begin
- Moves the logic of adding the sebsearch tool to create_tools_json_for_responses_api
- Making it the client responsibility to toggle the tool on or off
- Other misc in #2371 post commit feedback
- Show the query:
Full Diff
diff --git a/codex-rs/core/src/chat_completions.rs b/codex-rs/core/src/chat_completions.rs
index 641574074e..6eca119f3e 100644
--- a/codex-rs/core/src/chat_completions.rs
+++ b/codex-rs/core/src/chat_completions.rs
@@ -129,7 +129,9 @@ pub(crate) async fn stream_chat_completions(
"content": output,
}));
}
- ResponseItem::Reasoning { .. } | ResponseItem::Other => {
+ ResponseItem::Reasoning { .. }
+ | ResponseItem::WebSearchCall { .. }
+ | ResponseItem::Other => {
// Omit these items from the conversation history.
continue;
}
@@ -623,11 +625,8 @@ where
Poll::Ready(Some(Ok(ResponseEvent::ReasoningSummaryPartAdded))) => {
continue;
}
- Poll::Ready(Some(Ok(ResponseEvent::WebSearchCallBegin { .. }))) => {
- return Poll::Ready(Some(Ok(ResponseEvent::WebSearchCallBegin {
- call_id: String::new(),
- query: None,
- })));
+ Poll::Ready(Some(Ok(ResponseEvent::WebSearchCallBegin { call_id }))) => {
+ return Poll::Ready(Some(Ok(ResponseEvent::WebSearchCallBegin { call_id })));
}
}
}
diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs
index e0ab334fcf..b7bfeaa7ca 100644
--- a/codex-rs/core/src/client.rs
+++ b/codex-rs/core/src/client.rs
@@ -160,21 +160,7 @@ impl ModelClient {
let store = prompt.store && auth_mode != Some(AuthMode::ChatGPT);
let full_instructions = prompt.get_full_instructions(&self.config.model_family);
- let mut tools_json = create_tools_json_for_responses_api(&prompt.tools)?;
- // ChatGPT backend expects the preview name for web search.
- if auth_mode == Some(AuthMode::ChatGPT) {
- for tool in &mut tools_json {
- if let Some(map) = tool.as_object_mut()
- && map.get("type").and_then(|v| v.as_str()) == Some("web_search")
- {
- map.insert(
- "type".to_string(),
- serde_json::Value::String("web_search_preview".to_string()),
- );
- }
- }
- }
-
+ let tools_json = create_tools_json_for_responses_api(&prompt.tools)?;
let reasoning = create_reasoning_param_for_request(
&self.config.model_family,
self.effort,
@@ -607,11 +593,9 @@ async fn process_sse<S>(
| "response.custom_tool_call_input.delta"
| "response.custom_tool_call_input.done" // also emitted as response.output_item.done
| "response.in_progress"
- | "response.output_item.added"
- | "response.output_text.done" => {
- if event.kind == "response.output_item.added"
- && let Some(item) = event.item.as_ref()
- {
+ | "response.output_text.done" => {}
+ "response.output_item.added" => {
+ if let Some(item) = event.item.as_ref() {
// Detect web_search_call begin and forward a synthetic event upstream.
if let Some(ty) = item.get("type").and_then(|v| v.as_str())
&& ty == "web_search_call"
@@ -621,7 +605,7 @@ async fn process_sse<S>(
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
- let ev = ResponseEvent::WebSearchCallBegin { call_id, query: None };
+ let ev = ResponseEvent::WebSearchCallBegin { call_id };
if tx_event.send(Ok(ev)).await.is_err() {
return;
}
diff --git a/codex-rs/core/src/client_common.rs b/codex-rs/core/src/client_common.rs
index e2c191f4a5..2740105100 100644
--- a/codex-rs/core/src/client_common.rs
+++ b/codex-rs/core/src/client_common.rs
@@ -95,7 +95,6 @@ pub enum ResponseEvent {
ReasoningSummaryPartAdded,
WebSearchCallBegin {
call_id: String,
- query: Option<String>,
},
}
diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs
index 365969ac02..3df69c8590 100644
--- a/codex-rs/core/src/codex.rs
+++ b/codex-rs/core/src/codex.rs
@@ -100,6 +100,7 @@ use crate::protocol::Submission;
use crate::protocol::TaskCompleteEvent;
use crate::protocol::TurnDiffEvent;
use crate::protocol::WebSearchBeginEvent;
+use crate::protocol::WebSearchEndEvent;
use crate::rollout::RolloutRecorder;
use crate::safety::SafetyCheck;
use crate::safety::assess_command_safety;
@@ -118,6 +119,7 @@ use codex_protocol::models::ReasoningItemReasoningSummary;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::models::ShellToolCallParams;
+use codex_protocol::models::WebSearchAction;
// A convenience extension trait for acquiring mutex locks where poisoning is
// unrecoverable and should abort the program. This avoids scattered `.unwrap()`
@@ -1746,13 +1748,12 @@ async fn try_run_turn(
.await?;
output.push(ProcessedResponseItem { item, response });
}
- ResponseEvent::WebSearchCallBegin { call_id, query } => {
- let q = query.unwrap_or_else(|| "Searching Web...".to_string());
+ ResponseEvent::WebSearchCallBegin { call_id } => {
let _ = sess
.tx_event
.send(Event {
id: sub_id.to_string(),
- msg: EventMsg::WebSearchBegin(WebSearchBeginEvent { call_id, query: q }),
+ msg: EventMsg::WebSearchBegin(WebSearchBeginEvent { call_id }),
})
.await;
}
@@ -2048,6 +2049,17 @@ async fn handle_response_item(
debug!("unexpected CustomToolCallOutput from stream");
None
}
+ ResponseItem::WebSearchCall { id, action, .. } => {
+ if let WebSearchAction::Search { query } = action {
+ let call_id = id.unwrap_or_else(|| "".to_string());
+ let event = Event {
+ id: sub_id.to_string(),
+ msg: EventMsg::WebSearchEnd(WebSearchEndEvent { call_id, query }),
+ };
+ sess.tx_event.send(event).await.ok();
+ }
+ None
+ }
ResponseItem::Other => None,
};
Ok(output)
diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs
index 4d623c3e5b..a39756a9cf 100644
--- a/codex-rs/core/src/config.rs
+++ b/codex-rs/core/src/config.rs
@@ -497,7 +497,6 @@ pub struct ProjectConfig {
#[derive(Deserialize, Debug, Clone, Default)]
pub struct ToolsToml {
- // Renamed from `web_search_request`; keep alias for backwards compatibility.
#[serde(default, alias = "web_search_request")]
pub web_search: Option<bool>,
@@ -663,7 +662,7 @@ impl Config {
})?
.clone();
- let shell_environment_policy = cfg.shell_environment_policy.clone().into();
+ let shell_environment_policy = cfg.shell_environment_policy.into();
let resolved_cwd = {
use std::env;
@@ -684,7 +683,7 @@ impl Config {
}
};
- let history = cfg.history.clone().unwrap_or_default();
+ let history = cfg.history.unwrap_or_default();
let tools_web_search_request = override_tools_web_search_request
.or(cfg.tools.as_ref().and_then(|t| t.web_search))
@@ -766,7 +765,7 @@ impl Config {
codex_home,
history,
file_opener: cfg.file_opener.unwrap_or(UriBasedFileOpener::VsCode),
- tui: cfg.tui.clone().unwrap_or_default(),
+ tui: cfg.tui.unwrap_or_default(),
codex_linux_sandbox_exe,
hide_agent_reasoning: cfg.hide_agent_reasoning.unwrap_or(false),
@@ -785,7 +784,7 @@ impl Config {
model_verbosity: config_profile.model_verbosity.or(cfg.model_verbosity),
chatgpt_base_url: config_profile
.chatgpt_base_url
- .or(cfg.chatgpt_base_url.clone())
+ .or(cfg.chatgpt_base_url)
.unwrap_or("https://chatgpt.com/backend-api/".to_string()),
experimental_resume,
diff --git a/codex-rs/core/src/conversation_history.rs b/codex-rs/core/src/conversation_history.rs
index a8995f57f7..ed15bd4155 100644
--- a/codex-rs/core/src/conversation_history.rs
+++ b/codex-rs/core/src/conversation_history.rs
@@ -72,7 +72,7 @@ fn is_api_message(message: &ResponseItem) -> bool {
| ResponseItem::CustomToolCallOutput { .. }
| ResponseItem::LocalShellCall { .. }
| ResponseItem::Reasoning { .. } => true,
- ResponseItem::Other => false,
+ ResponseItem::WebSearchCall { .. } | ResponseItem::Other => false,
}
}
diff --git a/codex-rs/core/src/openai_tools.rs b/codex-rs/core/src/openai_tools.rs
index f74188162c..59ca7a8361 100644
--- a/codex-rs/core/src/openai_tools.rs
+++ b/codex-rs/core/src/openai_tools.rs
@@ -47,7 +47,9 @@ pub(crate) enum OpenAiTool {
Function(ResponsesApiTool),
#[serde(rename = "local_shell")]
LocalShell {},
- #[serde(rename = "web_search")]
+ // TODO: Understand why we get an error on web_search although the API docs say it's supported.
+ // https://platform.openai.com/docs/guides/tools-web-search?api-mode=responses#:~:text=%7B%20type%3A%20%22web_search%22%20%7D%2C
+ #[serde(rename = "web_search_preview")]
WebSearch {},
#[serde(rename = "custom")]
Freeform(FreeformTool),
@@ -335,12 +337,12 @@ pub fn create_tools_json_for_responses_api(
let mut tools_json = Vec::new();
for tool in tools {
- tools_json.push(serde_json::to_value(tool)?);
+ let json = serde_json::to_value(tool)?;
+ tools_json.push(json);
}
Ok(tools_json)
}
-
/// Returns JSON values that are compatible with Function Calling in the
/// Chat Completions API:
/// https://platform.openai.com/docs/guides/function-calling?api-mode=chat
@@ -702,8 +704,8 @@ mod tests {
"number_property": { "type": "number" },
},
"required": [
- "string_property".to_string(),
- "number_property".to_string()
+ "string_property",
+ "number_property",
],
"additionalProperties": Some(false),
},
diff --git a/codex-rs/core/src/rollout.rs b/codex-rs/core/src/rollout.rs
index 46098c1637..3c7162e883 100644
--- a/codex-rs/core/src/rollout.rs
+++ b/codex-rs/core/src/rollout.rs
@@ -135,7 +135,7 @@ impl RolloutRecorder {
| ResponseItem::CustomToolCall { .. }
| ResponseItem::CustomToolCallOutput { .. }
| ResponseItem::Reasoning { .. } => filtered.push(item.clone()),
- ResponseItem::Other => {
+ ResponseItem::WebSearchCall { .. } | ResponseItem::Other => {
// These should never be serialized.
continue;
}
@@ -199,7 +199,7 @@ impl RolloutRecorder {
| ResponseItem::CustomToolCall { .. }
| ResponseItem::CustomToolCallOutput { .. }
| ResponseItem::Reasoning { .. } => items.push(item),
- ResponseItem::Other => {}
+ ResponseItem::WebSearchCall { .. } | ResponseItem::Other => {}
},
Err(e) => {
warn!("failed to parse item: {v:?}, error: {e}");
@@ -326,7 +326,7 @@ async fn rollout_writer(
| ResponseItem::Reasoning { .. } => {
writer.write_line(&item).await?;
}
- ResponseItem::Other => {}
+ ResponseItem::WebSearchCall { .. } | ResponseItem::Other => {}
}
}
}
diff --git a/codex-rs/exec/src/event_processor_with_human_output.rs b/codex-rs/exec/src/event_processor_with_human_output.rs
index a3558f7407..6b680ddea9 100644
--- a/codex-rs/exec/src/event_processor_with_human_output.rs
+++ b/codex-rs/exec/src/event_processor_with_human_output.rs
@@ -25,6 +25,7 @@ use codex_core::protocol::TaskCompleteEvent;
use codex_core::protocol::TurnAbortReason;
use codex_core::protocol::TurnDiffEvent;
use codex_core::protocol::WebSearchBeginEvent;
+use codex_core::protocol::WebSearchEndEvent;
use owo_colors::OwoColorize;
use owo_colors::Style;
use shlex::try_join;
@@ -362,8 +363,9 @@ impl EventProcessor for EventProcessorWithHumanOutput {
}
}
}
- EventMsg::WebSearchBegin(WebSearchBeginEvent { call_id: _, query }) => {
- ts_println!(self, "🌐 {query}");
+ EventMsg::WebSearchBegin(WebSearchBeginEvent { call_id: _ }) => {}
+ EventMsg::WebSearchEnd(WebSearchEndEvent { call_id: _, query }) => {
+ ts_println!(self, "🌐 Searched: {query}");
}
EventMsg::PatchApplyBegin(PatchApplyBeginEvent {
call_id,
diff --git a/codex-rs/mcp-server/src/codex_tool_runner.rs b/codex-rs/mcp-server/src/codex_tool_runner.rs
index 3a26c26cd3..2f2245cc63 100644
--- a/codex-rs/mcp-server/src/codex_tool_runner.rs
+++ b/codex-rs/mcp-server/src/codex_tool_runner.rs
@@ -273,6 +273,7 @@ async fn run_codex_tool_session_inner(
| EventMsg::PatchApplyEnd(_)
| EventMsg::TurnDiff(_)
| EventMsg::WebSearchBegin(_)
+ | EventMsg::WebSearchEnd(_)
| EventMsg::GetHistoryEntryResponse(_)
| EventMsg::PlanUpdate(_)
| EventMsg::TurnAborted(_)
diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs
index 31e61d8be3..6cdfd933b1 100644
--- a/codex-rs/protocol/src/models.rs
+++ b/codex-rs/protocol/src/models.rs
@@ -95,6 +95,22 @@ pub enum ResponseItem {
call_id: String,
output: String,
},
+ // Emitted by the Responses API when the agent triggers a web search.
+ // Example payload (from SSE `response.output_item.done`):
+ // {
+ // "id":"ws_...",
+ // "type":"web_search_call",
+ // "status":"completed",
+ // "action": {"type":"search","query":"weather: San Francisco, CA"}
+ // }
+ WebSearchCall {
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ id: Option<String>,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ status: Option<String>,
+ action: WebSearchAction,
+ },
+
#[serde(other)]
Other,
}
@@ -162,6 +178,16 @@ pub struct LocalShellExecAction {
pub user: Option<String>,
}
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
+#[serde(tag = "type", rename_all = "snake_case")]
+pub enum WebSearchAction {
+ Search {
+ query: String,
+ },
+ #[serde(other)]
+ Other,
+}
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ReasoningItemReasoningSummary {
diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs
index 7f317bbfba..464dcf274c 100644
--- a/codex-rs/protocol/src/protocol.rs
+++ b/codex-rs/protocol/src/protocol.rs
@@ -439,6 +439,8 @@ pub enum EventMsg {
WebSearchBegin(WebSearchBeginEvent),
+ WebSearchEnd(WebSearchEndEvent),
+
/// Notification that the server is about to execute a command.
ExecCommandBegin(ExecCommandBeginEvent),
@@ -668,6 +670,11 @@ impl McpToolCallEndEvent {
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct WebSearchBeginEvent {
pub call_id: String,
+}
+
+#[derive(Debug, Clone, Deserialize, Serialize)]
+pub struct WebSearchEndEvent {
+ pub call_id: String,
pub query: String,
}
diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs
index e687fc038f..2fb09900ed 100644
--- a/codex-rs/tui/src/chatwidget.rs
+++ b/codex-rs/tui/src/chatwidget.rs
@@ -30,6 +30,7 @@ use codex_core::protocol::TokenUsage;
use codex_core::protocol::TurnAbortReason;
use codex_core::protocol::TurnDiffEvent;
use codex_core::protocol::WebSearchBeginEvent;
+use codex_core::protocol::WebSearchEndEvent;
use codex_protocol::parse_command::ParsedCommand;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
@@ -355,9 +356,16 @@ impl ChatWidget {
self.defer_or_handle(|q| q.push_mcp_end(ev), |s| s.handle_mcp_end_now(ev2));
}
- fn on_web_search_begin(&mut self, ev: WebSearchBeginEvent) {
+ fn on_web_search_begin(&mut self, _ev: WebSearchBeginEvent) {
self.flush_answer_stream_with_separator();
- self.add_to_history(history_cell::new_web_search_call(ev.query));
+ }
+
+ fn on_web_search_end(&mut self, ev: WebSearchEndEvent) {
+ self.flush_answer_stream_with_separator();
+ self.add_to_history(history_cell::new_web_search_call(format!(
+ "Searched: {}",
+ ev.query
+ )));
}
fn on_get_history_entry_response(
@@ -969,6 +977,7 @@ impl ChatWidget {
EventMsg::McpToolCallBegin(ev) => self.on_mcp_tool_call_begin(ev),
EventMsg::McpToolCallEnd(ev) => self.on_mcp_tool_call_end(ev),
EventMsg::WebSearchBegin(ev) => self.on_web_search_begin(ev),
+ EventMsg::WebSearchEnd(ev) => self.on_web_search_end(ev),
EventMsg::GetHistoryEntryResponse(ev) => self.on_get_history_entry_response(ev),
EventMsg::McpListToolsResponse(ev) => self.on_list_mcp_tools(ev),
EventMsg::ShutdownComplete => self.on_shutdown_complete(),
Review Comments
codex-rs/core/src/client_common.rs
- Created: 2025-08-29 02:05:21 UTC | Link: https://github.com/openai/codex/pull/2852#discussion_r2308939873
@@ -95,7 +95,6 @@ pub enum ResponseEvent {
ReasoningSummaryPartAdded,
WebSearchCallBegin {
call_id: String,
- query: Option<String>,
Should we keep this around? Might the UI want this?
codex-rs/core/src/codex.rs
- Created: 2025-08-29 02:06:25 UTC | Link: https://github.com/openai/codex/pull/2852#discussion_r2308940855
@@ -2048,6 +2049,17 @@ async fn handle_response_item(
debug!("unexpected CustomToolCallOutput from stream");
None
}
+ ResponseItem::WebSearchCall { id, action, .. } => {
+ if let WebSearchAction::Search { query } = action {
+ let call_id = id.unwrap_or_else(|| "".to_string());
Is this OK or a logical error? Can we make this a stronger check?
codex-rs/protocol/src/models.rs
- Created: 2025-08-29 02:07:54 UTC | Link: https://github.com/openai/codex/pull/2852#discussion_r2308942364
@@ -95,6 +95,22 @@ pub enum ResponseItem {
call_id: String,
output: String,
},
+ // Emitted by the Responses API when the agent triggers a web search.
+ // Example payload (from SSE `response.output_item.done`):
+ // {
+ // "id":"ws_...",
+ // "type":"web_search_call",
+ // "status":"completed",
+ // "action": {"type":"search","query":"weather: San Francisco, CA"}
+ // }
+ WebSearchCall {
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ id: Option<String>,
So this is not reliably supplied?