Add web search tool (#2371)

Adds web_search tool, enabling the model to use Responses API web_search
tool.
- Disabled by default, enabled by --search flag
- When --search is passed, exposes web_search_request function tool to
the model, which triggers user approval. When approved, the model can
use the web_search tool for the remainder of the turn
<img width="1033" height="294" alt="image"
src="https://github.com/user-attachments/assets/62ac6563-b946-465c-ba5d-9325af28b28f"
/>

---------

Co-authored-by: easong-openai <easong@openai.com>
This commit is contained in:
Reuben Narad
2025-08-23 22:58:56 -07:00
committed by GitHub
parent 957d44918d
commit 363636f5eb
16 changed files with 158 additions and 26 deletions

View File

@@ -623,6 +623,12 @@ where
Poll::Ready(Some(Ok(ResponseEvent::ReasoningSummaryPartAdded))) => { Poll::Ready(Some(Ok(ResponseEvent::ReasoningSummaryPartAdded))) => {
continue; continue;
} }
Poll::Ready(Some(Ok(ResponseEvent::WebSearchCallBegin { .. }))) => {
return Poll::Ready(Some(Ok(ResponseEvent::WebSearchCallBegin {
call_id: String::new(),
query: None,
})));
}
} }
} }
} }

View File

@@ -149,7 +149,21 @@ impl ModelClient {
let store = prompt.store && auth_mode != Some(AuthMode::ChatGPT); let store = prompt.store && auth_mode != Some(AuthMode::ChatGPT);
let full_instructions = prompt.get_full_instructions(&self.config.model_family); let full_instructions = prompt.get_full_instructions(&self.config.model_family);
let tools_json = create_tools_json_for_responses_api(&prompt.tools)?; 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 reasoning = create_reasoning_param_for_request( let reasoning = create_reasoning_param_for_request(
&self.config.model_family, &self.config.model_family,
self.effort, self.effort,
@@ -466,7 +480,8 @@ async fn process_sse<S>(
} }
}; };
trace!("SSE event: {}", sse.data); let raw = sse.data.clone();
trace!("SSE event: {}", raw);
let event: SseEvent = match serde_json::from_str(&sse.data) { let event: SseEvent = match serde_json::from_str(&sse.data) {
Ok(event) => event, Ok(event) => event,
@@ -580,8 +595,24 @@ async fn process_sse<S>(
| "response.in_progress" | "response.in_progress"
| "response.output_item.added" | "response.output_item.added"
| "response.output_text.done" => { | "response.output_text.done" => {
// Currently, we ignore this event, but we handle it if event.kind == "response.output_item.added"
// separately to skip the logging message in the `other` case. && 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"
{
let call_id = item
.get("id")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let ev = ResponseEvent::WebSearchCallBegin { call_id, query: None };
if tx_event.send(Ok(ev)).await.is_err() {
return;
}
}
}
} }
"response.reasoning_summary_part.added" => { "response.reasoning_summary_part.added" => {
// Boundary between reasoning summary sections (e.g., titles). // Boundary between reasoning summary sections (e.g., titles).
@@ -591,7 +622,7 @@ async fn process_sse<S>(
} }
} }
"response.reasoning_summary_text.done" => {} "response.reasoning_summary_text.done" => {}
other => debug!(other, "sse event"), _ => {}
} }
} }
} }

View File

@@ -93,6 +93,10 @@ pub enum ResponseEvent {
ReasoningSummaryDelta(String), ReasoningSummaryDelta(String),
ReasoningContentDelta(String), ReasoningContentDelta(String),
ReasoningSummaryPartAdded, ReasoningSummaryPartAdded,
WebSearchCallBegin {
call_id: String,
query: Option<String>,
},
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]

View File

@@ -96,6 +96,7 @@ use crate::protocol::StreamErrorEvent;
use crate::protocol::Submission; use crate::protocol::Submission;
use crate::protocol::TaskCompleteEvent; use crate::protocol::TaskCompleteEvent;
use crate::protocol::TurnDiffEvent; use crate::protocol::TurnDiffEvent;
use crate::protocol::WebSearchBeginEvent;
use crate::rollout::RolloutRecorder; use crate::rollout::RolloutRecorder;
use crate::safety::SafetyCheck; use crate::safety::SafetyCheck;
use crate::safety::assess_command_safety; use crate::safety::assess_command_safety;
@@ -511,6 +512,7 @@ impl Session {
sandbox_policy.clone(), sandbox_policy.clone(),
config.include_plan_tool, config.include_plan_tool,
config.include_apply_patch_tool, config.include_apply_patch_tool,
config.tools_web_search_request,
config.use_experimental_streamable_shell_tool, config.use_experimental_streamable_shell_tool,
), ),
user_instructions, user_instructions,
@@ -1096,6 +1098,7 @@ async fn submission_loop(
new_sandbox_policy.clone(), new_sandbox_policy.clone(),
config.include_plan_tool, config.include_plan_tool,
config.include_apply_patch_tool, config.include_apply_patch_tool,
config.tools_web_search_request,
config.use_experimental_streamable_shell_tool, config.use_experimental_streamable_shell_tool,
); );
@@ -1175,6 +1178,7 @@ async fn submission_loop(
sandbox_policy.clone(), sandbox_policy.clone(),
config.include_plan_tool, config.include_plan_tool,
config.include_apply_patch_tool, config.include_apply_patch_tool,
config.tools_web_search_request,
config.use_experimental_streamable_shell_tool, config.use_experimental_streamable_shell_tool,
), ),
user_instructions: turn_context.user_instructions.clone(), user_instructions: turn_context.user_instructions.clone(),
@@ -1687,6 +1691,7 @@ async fn try_run_turn(
let mut stream = turn_context.client.clone().stream(&prompt).await?; let mut stream = turn_context.client.clone().stream(&prompt).await?;
let mut output = Vec::new(); let mut output = Vec::new();
loop { loop {
// Poll the next item from the model stream. We must inspect *both* Ok and Err // Poll the next item from the model stream. We must inspect *both* Ok and Err
// cases so that transient stream failures (e.g., dropped SSE connection before // cases so that transient stream failures (e.g., dropped SSE connection before
@@ -1723,6 +1728,16 @@ async fn try_run_turn(
.await?; .await?;
output.push(ProcessedResponseItem { item, response }); output.push(ProcessedResponseItem { item, response });
} }
ResponseEvent::WebSearchCallBegin { call_id, query } => {
let q = query.unwrap_or_else(|| "Searching Web...".to_string());
let _ = sess
.tx_event
.send(Event {
id: sub_id.to_string(),
msg: EventMsg::WebSearchBegin(WebSearchBeginEvent { call_id, query: q }),
})
.await;
}
ResponseEvent::Completed { ResponseEvent::Completed {
response_id: _, response_id: _,
token_usage, token_usage,

View File

@@ -169,6 +169,8 @@ pub struct Config {
/// model family's default preference. /// model family's default preference.
pub include_apply_patch_tool: bool, pub include_apply_patch_tool: bool,
pub tools_web_search_request: bool,
/// The value for the `originator` header included with Responses API requests. /// The value for the `originator` header included with Responses API requests.
pub responses_originator_header: String, pub responses_originator_header: String,
@@ -480,6 +482,9 @@ pub struct ConfigToml {
/// If set to `true`, the API key will be signed with the `originator` header. /// If set to `true`, the API key will be signed with the `originator` header.
pub preferred_auth_method: Option<AuthMode>, pub preferred_auth_method: Option<AuthMode>,
/// Nested tools section for feature toggles
pub tools: Option<ToolsToml>,
} }
#[derive(Deserialize, Debug, Clone, PartialEq, Eq)] #[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
@@ -487,6 +492,13 @@ pub struct ProjectConfig {
pub trust_level: Option<String>, pub trust_level: Option<String>,
} }
#[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>,
}
impl ConfigToml { impl ConfigToml {
/// Derive the effective sandbox policy from the configuration. /// Derive the effective sandbox policy from the configuration.
fn derive_sandbox_policy(&self, sandbox_mode_override: Option<SandboxMode>) -> SandboxPolicy { fn derive_sandbox_policy(&self, sandbox_mode_override: Option<SandboxMode>) -> SandboxPolicy {
@@ -576,6 +588,7 @@ pub struct ConfigOverrides {
pub include_apply_patch_tool: Option<bool>, pub include_apply_patch_tool: Option<bool>,
pub disable_response_storage: Option<bool>, pub disable_response_storage: Option<bool>,
pub show_raw_agent_reasoning: Option<bool>, pub show_raw_agent_reasoning: Option<bool>,
pub tools_web_search_request: Option<bool>,
} }
impl Config { impl Config {
@@ -602,6 +615,7 @@ impl Config {
include_apply_patch_tool, include_apply_patch_tool,
disable_response_storage, disable_response_storage,
show_raw_agent_reasoning, show_raw_agent_reasoning,
tools_web_search_request: override_tools_web_search_request,
} = overrides; } = overrides;
let config_profile = match config_profile_key.as_ref().or(cfg.profile.as_ref()) { let config_profile = match config_profile_key.as_ref().or(cfg.profile.as_ref()) {
@@ -640,7 +654,7 @@ impl Config {
})? })?
.clone(); .clone();
let shell_environment_policy = cfg.shell_environment_policy.into(); let shell_environment_policy = cfg.shell_environment_policy.clone().into();
let resolved_cwd = { let resolved_cwd = {
use std::env; use std::env;
@@ -661,7 +675,11 @@ impl Config {
} }
}; };
let history = cfg.history.unwrap_or_default(); let history = cfg.history.clone().unwrap_or_default();
let tools_web_search_request = override_tools_web_search_request
.or(cfg.tools.as_ref().and_then(|t| t.web_search))
.unwrap_or(false);
let model = model let model = model
.or(config_profile.model) .or(config_profile.model)
@@ -735,7 +753,7 @@ impl Config {
codex_home, codex_home,
history, history,
file_opener: cfg.file_opener.unwrap_or(UriBasedFileOpener::VsCode), file_opener: cfg.file_opener.unwrap_or(UriBasedFileOpener::VsCode),
tui: cfg.tui.unwrap_or_default(), tui: cfg.tui.clone().unwrap_or_default(),
codex_linux_sandbox_exe, codex_linux_sandbox_exe,
hide_agent_reasoning: cfg.hide_agent_reasoning.unwrap_or(false), hide_agent_reasoning: cfg.hide_agent_reasoning.unwrap_or(false),
@@ -754,12 +772,13 @@ impl Config {
model_verbosity: config_profile.model_verbosity.or(cfg.model_verbosity), model_verbosity: config_profile.model_verbosity.or(cfg.model_verbosity),
chatgpt_base_url: config_profile chatgpt_base_url: config_profile
.chatgpt_base_url .chatgpt_base_url
.or(cfg.chatgpt_base_url) .or(cfg.chatgpt_base_url.clone())
.unwrap_or("https://chatgpt.com/backend-api/".to_string()), .unwrap_or("https://chatgpt.com/backend-api/".to_string()),
experimental_resume, experimental_resume,
include_plan_tool: include_plan_tool.unwrap_or(false), include_plan_tool: include_plan_tool.unwrap_or(false),
include_apply_patch_tool: include_apply_patch_tool.unwrap_or(false), include_apply_patch_tool: include_apply_patch_tool.unwrap_or(false),
tools_web_search_request,
responses_originator_header, responses_originator_header,
preferred_auth_method: cfg.preferred_auth_method.unwrap_or(AuthMode::ChatGPT), preferred_auth_method: cfg.preferred_auth_method.unwrap_or(AuthMode::ChatGPT),
use_experimental_streamable_shell_tool: cfg use_experimental_streamable_shell_tool: cfg
@@ -1129,6 +1148,7 @@ disable_response_storage = true
base_instructions: None, base_instructions: None,
include_plan_tool: false, include_plan_tool: false,
include_apply_patch_tool: false, include_apply_patch_tool: false,
tools_web_search_request: false,
responses_originator_header: "codex_cli_rs".to_string(), responses_originator_header: "codex_cli_rs".to_string(),
preferred_auth_method: AuthMode::ChatGPT, preferred_auth_method: AuthMode::ChatGPT,
use_experimental_streamable_shell_tool: false, use_experimental_streamable_shell_tool: false,
@@ -1184,6 +1204,7 @@ disable_response_storage = true
base_instructions: None, base_instructions: None,
include_plan_tool: false, include_plan_tool: false,
include_apply_patch_tool: false, include_apply_patch_tool: false,
tools_web_search_request: false,
responses_originator_header: "codex_cli_rs".to_string(), responses_originator_header: "codex_cli_rs".to_string(),
preferred_auth_method: AuthMode::ChatGPT, preferred_auth_method: AuthMode::ChatGPT,
use_experimental_streamable_shell_tool: false, use_experimental_streamable_shell_tool: false,
@@ -1254,6 +1275,7 @@ disable_response_storage = true
base_instructions: None, base_instructions: None,
include_plan_tool: false, include_plan_tool: false,
include_apply_patch_tool: false, include_apply_patch_tool: false,
tools_web_search_request: false,
responses_originator_header: "codex_cli_rs".to_string(), responses_originator_header: "codex_cli_rs".to_string(),
preferred_auth_method: AuthMode::ChatGPT, preferred_auth_method: AuthMode::ChatGPT,
use_experimental_streamable_shell_tool: false, use_experimental_streamable_shell_tool: false,

View File

@@ -47,6 +47,8 @@ pub(crate) enum OpenAiTool {
Function(ResponsesApiTool), Function(ResponsesApiTool),
#[serde(rename = "local_shell")] #[serde(rename = "local_shell")]
LocalShell {}, LocalShell {},
#[serde(rename = "web_search")]
WebSearch {},
#[serde(rename = "custom")] #[serde(rename = "custom")]
Freeform(FreeformTool), Freeform(FreeformTool),
} }
@@ -64,6 +66,7 @@ pub struct ToolsConfig {
pub shell_type: ConfigShellToolType, pub shell_type: ConfigShellToolType,
pub plan_tool: bool, pub plan_tool: bool,
pub apply_patch_tool_type: Option<ApplyPatchToolType>, pub apply_patch_tool_type: Option<ApplyPatchToolType>,
pub web_search_request: bool,
} }
impl ToolsConfig { impl ToolsConfig {
@@ -73,6 +76,7 @@ impl ToolsConfig {
sandbox_policy: SandboxPolicy, sandbox_policy: SandboxPolicy,
include_plan_tool: bool, include_plan_tool: bool,
include_apply_patch_tool: bool, include_apply_patch_tool: bool,
include_web_search_request: bool,
use_streamable_shell_tool: bool, use_streamable_shell_tool: bool,
) -> Self { ) -> Self {
let mut shell_type = if use_streamable_shell_tool { let mut shell_type = if use_streamable_shell_tool {
@@ -104,6 +108,7 @@ impl ToolsConfig {
shell_type, shell_type,
plan_tool: include_plan_tool, plan_tool: include_plan_tool,
apply_patch_tool_type, apply_patch_tool_type,
web_search_request: include_web_search_request,
} }
} }
} }
@@ -521,6 +526,10 @@ pub(crate) fn get_openai_tools(
} }
} }
if config.web_search_request {
tools.push(OpenAiTool::WebSearch {});
}
if let Some(mcp_tools) = mcp_tools { if let Some(mcp_tools) = mcp_tools {
for (name, tool) in mcp_tools { for (name, tool) in mcp_tools {
match mcp_tool_to_openai_tool(name.clone(), tool.clone()) { match mcp_tool_to_openai_tool(name.clone(), tool.clone()) {
@@ -549,6 +558,7 @@ mod tests {
.map(|tool| match tool { .map(|tool| match tool {
OpenAiTool::Function(ResponsesApiTool { name, .. }) => name, OpenAiTool::Function(ResponsesApiTool { name, .. }) => name,
OpenAiTool::LocalShell {} => "local_shell", OpenAiTool::LocalShell {} => "local_shell",
OpenAiTool::WebSearch {} => "web_search",
OpenAiTool::Freeform(FreeformTool { name, .. }) => name, OpenAiTool::Freeform(FreeformTool { name, .. }) => name,
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@@ -576,11 +586,12 @@ mod tests {
SandboxPolicy::ReadOnly, SandboxPolicy::ReadOnly,
true, true,
false, false,
true,
/*use_experimental_streamable_shell_tool*/ false, /*use_experimental_streamable_shell_tool*/ false,
); );
let tools = get_openai_tools(&config, Some(HashMap::new())); let tools = get_openai_tools(&config, Some(HashMap::new()));
assert_eq_tool_names(&tools, &["local_shell", "update_plan"]); assert_eq_tool_names(&tools, &["local_shell", "update_plan", "web_search"]);
} }
#[test] #[test]
@@ -592,11 +603,12 @@ mod tests {
SandboxPolicy::ReadOnly, SandboxPolicy::ReadOnly,
true, true,
false, false,
true,
/*use_experimental_streamable_shell_tool*/ false, /*use_experimental_streamable_shell_tool*/ false,
); );
let tools = get_openai_tools(&config, Some(HashMap::new())); let tools = get_openai_tools(&config, Some(HashMap::new()));
assert_eq_tool_names(&tools, &["shell", "update_plan"]); assert_eq_tool_names(&tools, &["shell", "update_plan", "web_search"]);
} }
#[test] #[test]
@@ -608,6 +620,7 @@ mod tests {
SandboxPolicy::ReadOnly, SandboxPolicy::ReadOnly,
false, false,
false, false,
true,
/*use_experimental_streamable_shell_tool*/ false, /*use_experimental_streamable_shell_tool*/ false,
); );
let tools = get_openai_tools( let tools = get_openai_tools(
@@ -631,8 +644,8 @@ mod tests {
"number_property": { "type": "number" }, "number_property": { "type": "number" },
}, },
"required": [ "required": [
"string_property", "string_property".to_string(),
"number_property" "number_property".to_string()
], ],
"additionalProperties": Some(false), "additionalProperties": Some(false),
}, },
@@ -648,10 +661,13 @@ mod tests {
)])), )])),
); );
assert_eq_tool_names(&tools, &["shell", "test_server/do_something_cool"]); assert_eq_tool_names(
&tools,
&["shell", "web_search", "test_server/do_something_cool"],
);
assert_eq!( assert_eq!(
tools[1], tools[2],
OpenAiTool::Function(ResponsesApiTool { OpenAiTool::Function(ResponsesApiTool {
name: "test_server/do_something_cool".to_string(), name: "test_server/do_something_cool".to_string(),
parameters: JsonSchema::Object { parameters: JsonSchema::Object {
@@ -703,6 +719,7 @@ mod tests {
SandboxPolicy::ReadOnly, SandboxPolicy::ReadOnly,
false, false,
false, false,
true,
/*use_experimental_streamable_shell_tool*/ false, /*use_experimental_streamable_shell_tool*/ false,
); );
@@ -729,10 +746,10 @@ mod tests {
)])), )])),
); );
assert_eq_tool_names(&tools, &["shell", "dash/search"]); assert_eq_tool_names(&tools, &["shell", "web_search", "dash/search"]);
assert_eq!( assert_eq!(
tools[1], tools[2],
OpenAiTool::Function(ResponsesApiTool { OpenAiTool::Function(ResponsesApiTool {
name: "dash/search".to_string(), name: "dash/search".to_string(),
parameters: JsonSchema::Object { parameters: JsonSchema::Object {
@@ -760,6 +777,7 @@ mod tests {
SandboxPolicy::ReadOnly, SandboxPolicy::ReadOnly,
false, false,
false, false,
true,
/*use_experimental_streamable_shell_tool*/ false, /*use_experimental_streamable_shell_tool*/ false,
); );
@@ -784,9 +802,9 @@ mod tests {
)])), )])),
); );
assert_eq_tool_names(&tools, &["shell", "dash/paginate"]); assert_eq_tool_names(&tools, &["shell", "web_search", "dash/paginate"]);
assert_eq!( assert_eq!(
tools[1], tools[2],
OpenAiTool::Function(ResponsesApiTool { OpenAiTool::Function(ResponsesApiTool {
name: "dash/paginate".to_string(), name: "dash/paginate".to_string(),
parameters: JsonSchema::Object { parameters: JsonSchema::Object {
@@ -812,6 +830,7 @@ mod tests {
SandboxPolicy::ReadOnly, SandboxPolicy::ReadOnly,
false, false,
false, false,
true,
/*use_experimental_streamable_shell_tool*/ false, /*use_experimental_streamable_shell_tool*/ false,
); );
@@ -836,9 +855,9 @@ mod tests {
)])), )])),
); );
assert_eq_tool_names(&tools, &["shell", "dash/tags"]); assert_eq_tool_names(&tools, &["shell", "web_search", "dash/tags"]);
assert_eq!( assert_eq!(
tools[1], tools[2],
OpenAiTool::Function(ResponsesApiTool { OpenAiTool::Function(ResponsesApiTool {
name: "dash/tags".to_string(), name: "dash/tags".to_string(),
parameters: JsonSchema::Object { parameters: JsonSchema::Object {
@@ -867,6 +886,7 @@ mod tests {
SandboxPolicy::ReadOnly, SandboxPolicy::ReadOnly,
false, false,
false, false,
true,
/*use_experimental_streamable_shell_tool*/ false, /*use_experimental_streamable_shell_tool*/ false,
); );
@@ -891,9 +911,9 @@ mod tests {
)])), )])),
); );
assert_eq_tool_names(&tools, &["shell", "dash/value"]); assert_eq_tool_names(&tools, &["shell", "web_search", "dash/value"]);
assert_eq!( assert_eq!(
tools[1], tools[2],
OpenAiTool::Function(ResponsesApiTool { OpenAiTool::Function(ResponsesApiTool {
name: "dash/value".to_string(), name: "dash/value".to_string(),
parameters: JsonSchema::Object { parameters: JsonSchema::Object {

View File

@@ -24,6 +24,7 @@ use codex_core::protocol::StreamErrorEvent;
use codex_core::protocol::TaskCompleteEvent; use codex_core::protocol::TaskCompleteEvent;
use codex_core::protocol::TurnAbortReason; use codex_core::protocol::TurnAbortReason;
use codex_core::protocol::TurnDiffEvent; use codex_core::protocol::TurnDiffEvent;
use codex_core::protocol::WebSearchBeginEvent;
use owo_colors::OwoColorize; use owo_colors::OwoColorize;
use owo_colors::Style; use owo_colors::Style;
use shlex::try_join; use shlex::try_join;
@@ -361,6 +362,9 @@ impl EventProcessor for EventProcessorWithHumanOutput {
} }
} }
} }
EventMsg::WebSearchBegin(WebSearchBeginEvent { call_id: _, query }) => {
ts_println!(self, "🌐 {query}");
}
EventMsg::PatchApplyBegin(PatchApplyBeginEvent { EventMsg::PatchApplyBegin(PatchApplyBeginEvent {
call_id, call_id,
auto_approved, auto_approved,

View File

@@ -150,6 +150,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
include_apply_patch_tool: None, include_apply_patch_tool: None,
disable_response_storage: oss.then_some(true), disable_response_storage: oss.then_some(true),
show_raw_agent_reasoning: oss.then_some(true), show_raw_agent_reasoning: oss.then_some(true),
tools_web_search_request: None,
}; };
// Parse `-c` overrides. // Parse `-c` overrides.
let cli_kv_overrides = match config_overrides.parse_overrides() { let cli_kv_overrides = match config_overrides.parse_overrides() {

View File

@@ -738,6 +738,7 @@ fn derive_config_from_params(
include_apply_patch_tool, include_apply_patch_tool,
disable_response_storage: None, disable_response_storage: None,
show_raw_agent_reasoning: None, show_raw_agent_reasoning: None,
tools_web_search_request: None,
}; };
let cli_overrides = cli_overrides let cli_overrides = cli_overrides

View File

@@ -163,6 +163,7 @@ impl CodexToolCallParam {
include_apply_patch_tool: None, include_apply_patch_tool: None,
disable_response_storage: None, disable_response_storage: None,
show_raw_agent_reasoning: None, show_raw_agent_reasoning: None,
tools_web_search_request: None,
}; };
let cli_overrides = cli_overrides let cli_overrides = cli_overrides

View File

@@ -272,6 +272,7 @@ async fn run_codex_tool_session_inner(
| EventMsg::PatchApplyBegin(_) | EventMsg::PatchApplyBegin(_)
| EventMsg::PatchApplyEnd(_) | EventMsg::PatchApplyEnd(_)
| EventMsg::TurnDiff(_) | EventMsg::TurnDiff(_)
| EventMsg::WebSearchBegin(_)
| EventMsg::GetHistoryEntryResponse(_) | EventMsg::GetHistoryEntryResponse(_)
| EventMsg::PlanUpdate(_) | EventMsg::PlanUpdate(_)
| EventMsg::TurnAborted(_) | EventMsg::TurnAborted(_)

View File

@@ -437,6 +437,8 @@ pub enum EventMsg {
McpToolCallEnd(McpToolCallEndEvent), McpToolCallEnd(McpToolCallEndEvent),
WebSearchBegin(WebSearchBeginEvent),
/// Notification that the server is about to execute a command. /// Notification that the server is about to execute a command.
ExecCommandBegin(ExecCommandBeginEvent), ExecCommandBegin(ExecCommandBeginEvent),
@@ -658,6 +660,12 @@ impl McpToolCallEndEvent {
} }
} }
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct WebSearchBeginEvent {
pub call_id: String,
pub query: String,
}
/// Response payload for `Op::GetHistory` containing the current session's /// Response payload for `Op::GetHistory` containing the current session's
/// in-memory transcript. /// in-memory transcript.
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]

View File

@@ -28,6 +28,7 @@ use codex_core::protocol::TaskCompleteEvent;
use codex_core::protocol::TokenUsage; use codex_core::protocol::TokenUsage;
use codex_core::protocol::TurnAbortReason; use codex_core::protocol::TurnAbortReason;
use codex_core::protocol::TurnDiffEvent; use codex_core::protocol::TurnDiffEvent;
use codex_core::protocol::WebSearchBeginEvent;
use codex_protocol::parse_command::ParsedCommand; use codex_protocol::parse_command::ParsedCommand;
use crossterm::event::KeyEvent; use crossterm::event::KeyEvent;
use crossterm::event::KeyEventKind; use crossterm::event::KeyEventKind;
@@ -308,6 +309,11 @@ impl ChatWidget {
self.defer_or_handle(|q| q.push_mcp_end(ev), |s| s.handle_mcp_end_now(ev2)); 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) {
self.flush_answer_stream_with_separator();
self.add_to_history(history_cell::new_web_search_call(ev.query));
}
fn on_get_history_entry_response( fn on_get_history_entry_response(
&mut self, &mut self,
event: codex_core::protocol::GetHistoryEntryResponseEvent, event: codex_core::protocol::GetHistoryEntryResponseEvent,
@@ -839,6 +845,7 @@ impl ChatWidget {
EventMsg::ExecCommandEnd(ev) => self.on_exec_command_end(ev), EventMsg::ExecCommandEnd(ev) => self.on_exec_command_end(ev),
EventMsg::McpToolCallBegin(ev) => self.on_mcp_tool_call_begin(ev), EventMsg::McpToolCallBegin(ev) => self.on_mcp_tool_call_begin(ev),
EventMsg::McpToolCallEnd(ev) => self.on_mcp_tool_call_end(ev), EventMsg::McpToolCallEnd(ev) => self.on_mcp_tool_call_end(ev),
EventMsg::WebSearchBegin(ev) => self.on_web_search_begin(ev),
EventMsg::GetHistoryEntryResponse(ev) => self.on_get_history_entry_response(ev), EventMsg::GetHistoryEntryResponse(ev) => self.on_get_history_entry_response(ev),
EventMsg::McpListToolsResponse(ev) => self.on_list_mcp_tools(ev), EventMsg::McpListToolsResponse(ev) => self.on_list_mcp_tools(ev),
EventMsg::ShutdownComplete => self.on_shutdown_complete(), EventMsg::ShutdownComplete => self.on_shutdown_complete(),

View File

@@ -54,6 +54,10 @@ pub struct Cli {
#[clap(long = "cd", short = 'C', value_name = "DIR")] #[clap(long = "cd", short = 'C', value_name = "DIR")]
pub cwd: Option<PathBuf>, pub cwd: Option<PathBuf>,
/// Enable web search (off by default). When enabled, the native Responses `web_search` tool is available to the model (no percall approval).
#[arg(long = "search", default_value_t = false)]
pub web_search: bool,
#[clap(skip)] #[clap(skip)]
pub config_overrides: CliConfigOverrides, pub config_overrides: CliConfigOverrides,
} }

View File

@@ -445,6 +445,12 @@ pub(crate) fn new_active_mcp_tool_call(invocation: McpInvocation) -> PlainHistor
PlainHistoryCell { lines } PlainHistoryCell { lines }
} }
pub(crate) fn new_web_search_call(query: String) -> PlainHistoryCell {
let lines: Vec<Line<'static>> =
vec![Line::from(""), Line::from(vec!["🌐 ".into(), query.into()])];
PlainHistoryCell { lines }
}
/// If the first content is an image, return a new cell with the image. /// If the first content is an image, return a new cell with the image.
/// TODO(rgwood-dd): Handle images properly even if they're not the first result. /// TODO(rgwood-dd): Handle images properly even if they're not the first result.
fn try_new_completed_mcp_tool_call_with_image_output( fn try_new_completed_mcp_tool_call_with_image_output(

View File

@@ -128,10 +128,11 @@ pub async fn run_main(
include_apply_patch_tool: None, include_apply_patch_tool: None,
disable_response_storage: cli.oss.then_some(true), disable_response_storage: cli.oss.then_some(true),
show_raw_agent_reasoning: cli.oss.then_some(true), show_raw_agent_reasoning: cli.oss.then_some(true),
tools_web_search_request: cli.web_search.then_some(true),
}; };
let raw_overrides = cli.config_overrides.raw_overrides.clone();
// Parse `-c` overrides from the CLI. let overrides_cli = codex_common::CliConfigOverrides { raw_overrides };
let cli_kv_overrides = match cli.config_overrides.parse_overrides() { let cli_kv_overrides = match overrides_cli.parse_overrides() {
Ok(v) => v, Ok(v) => v,
#[allow(clippy::print_stderr)] #[allow(clippy::print_stderr)]
Err(e) => { Err(e) => {