mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
2704ecea9a
## Why We're now [discouraging use of `async_trait`](https://github.com/openai/codex/pull/20242). Removing use of `async_trait` from `ToolExecutor` yields a `codex_core` debug test build speedup of ~78% (from 227.5s to 50.3s) on my machine. Stacked on #27299, this PR applies the trait change after the handler bodies have been outlined. ## What Changed `ToolExecutor::handle` to return an explicit boxed `ToolExecutorFuture` instead of using `async_trait`. Updated ToolExecutor implementors to return `Box::pin(...)`, reexported the future alias through `codex-tools` and `codex-extension-api`, and removed `codex-tools` direct `async-trait` dependency.
242 lines
8.1 KiB
Rust
242 lines
8.1 KiB
Rust
use codex_api::ReqwestTransport;
|
|
use codex_api::SearchClient;
|
|
use codex_api::SearchCommands;
|
|
use codex_api::SearchQuery;
|
|
use codex_api::SearchRequest;
|
|
use codex_api::SearchSettings;
|
|
use codex_core::web_search_action_detail;
|
|
use codex_extension_api::ExtensionTurnItem;
|
|
use codex_extension_api::FunctionCallError;
|
|
use codex_extension_api::ResponsesApiTool;
|
|
use codex_extension_api::ToolCall;
|
|
use codex_extension_api::ToolExecutor;
|
|
use codex_extension_api::ToolName;
|
|
use codex_extension_api::ToolOutput;
|
|
use codex_extension_api::ToolSpec;
|
|
use codex_extension_api::parse_tool_input_schema_without_compaction;
|
|
use codex_login::default_client::build_reqwest_client;
|
|
use codex_model_provider::SharedModelProvider;
|
|
use codex_protocol::items::WebSearchItem;
|
|
use codex_protocol::models::WebSearchAction;
|
|
use codex_tools::ResponsesApiNamespace;
|
|
use codex_tools::ResponsesApiNamespaceTool;
|
|
use codex_tools::ToolExposure;
|
|
use codex_tools::default_namespace_description;
|
|
use http::HeaderMap;
|
|
use url::Url;
|
|
|
|
use crate::history::recent_input;
|
|
use crate::output::SearchOutput;
|
|
use crate::schema::commands_schema;
|
|
|
|
pub(crate) const WEB_NAMESPACE: &str = "web";
|
|
pub(crate) const RUN_TOOL_NAME: &str = "run";
|
|
const WEB_RUN_DESCRIPTION: &str = include_str!("../web_run_description.md");
|
|
|
|
pub(crate) struct WebSearchTool {
|
|
pub(crate) session_id: String,
|
|
pub(crate) provider: SharedModelProvider,
|
|
pub(crate) settings: SearchSettings,
|
|
}
|
|
|
|
impl ToolExecutor<ToolCall> for WebSearchTool {
|
|
fn tool_name(&self) -> ToolName {
|
|
ToolName::namespaced(WEB_NAMESPACE, RUN_TOOL_NAME)
|
|
}
|
|
|
|
fn spec(&self) -> ToolSpec {
|
|
// parse schema without compaction that removes field metadata/descriptions to match hosted tool definition
|
|
let parameters = match parse_tool_input_schema_without_compaction(&commands_schema()) {
|
|
Ok(parameters) => parameters,
|
|
Err(err) => panic!("search command schema should parse: {err}"),
|
|
};
|
|
|
|
ToolSpec::Namespace(ResponsesApiNamespace {
|
|
name: WEB_NAMESPACE.to_string(),
|
|
description: default_namespace_description(WEB_NAMESPACE),
|
|
tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool {
|
|
name: RUN_TOOL_NAME.to_string(),
|
|
description: WEB_RUN_DESCRIPTION.to_string(),
|
|
strict: false,
|
|
parameters,
|
|
output_schema: None,
|
|
defer_loading: None,
|
|
})],
|
|
})
|
|
}
|
|
|
|
fn exposure(&self) -> ToolExposure {
|
|
ToolExposure::Direct
|
|
}
|
|
|
|
fn supports_parallel_tool_calls(&self) -> bool {
|
|
true
|
|
}
|
|
|
|
fn handle(&self, call: ToolCall) -> codex_extension_api::ToolExecutorFuture<'_> {
|
|
Box::pin(self.handle_call(call))
|
|
}
|
|
}
|
|
|
|
impl WebSearchTool {
|
|
async fn handle_call(&self, call: ToolCall) -> Result<Box<dyn ToolOutput>, FunctionCallError> {
|
|
let commands = parse_commands(&call)?;
|
|
let command_action = command_action(&commands);
|
|
let provider = self
|
|
.provider
|
|
.api_provider()
|
|
.await
|
|
.map_err(|err| FunctionCallError::Fatal(err.to_string()))?;
|
|
let auth = self
|
|
.provider
|
|
.api_auth()
|
|
.await
|
|
.map_err(|err| FunctionCallError::Fatal(err.to_string()))?;
|
|
let client = SearchClient::new(
|
|
ReqwestTransport::new(build_reqwest_client()),
|
|
provider,
|
|
auth,
|
|
);
|
|
let request = SearchRequest {
|
|
id: self.session_id.clone(),
|
|
model: call.model.clone(),
|
|
reasoning: None,
|
|
input: recent_input(call.conversation_history.items()),
|
|
commands: Some(commands),
|
|
settings: Some(self.settings.clone()),
|
|
max_output_tokens: Some(
|
|
u64::try_from(call.truncation_policy.token_budget()).unwrap_or(u64::MAX),
|
|
),
|
|
};
|
|
call.turn_item_emitter
|
|
.emit_started(web_search_item(&call.call_id, WebSearchAction::Other))
|
|
.await;
|
|
let response = client
|
|
.search(&request, HeaderMap::new())
|
|
.await
|
|
.map_err(|err| FunctionCallError::Fatal(err.to_string()))?;
|
|
call.turn_item_emitter
|
|
.emit_completed(web_search_item(&call.call_id, command_action))
|
|
.await;
|
|
|
|
Ok(Box::new(SearchOutput::new(response.output)))
|
|
}
|
|
}
|
|
|
|
fn parse_commands(call: &ToolCall) -> Result<SearchCommands, FunctionCallError> {
|
|
let arguments = call.function_arguments()?;
|
|
if arguments.trim().is_empty() {
|
|
return Ok(SearchCommands::default());
|
|
}
|
|
|
|
serde_json::from_str(arguments)
|
|
.map_err(|err| FunctionCallError::RespondToModel(err.to_string()))
|
|
}
|
|
|
|
fn command_action(commands: &SearchCommands) -> WebSearchAction {
|
|
commands
|
|
.search_query
|
|
.as_deref()
|
|
.and_then(query_action)
|
|
.or_else(|| commands.image_query.as_deref().and_then(query_action))
|
|
.or_else(|| {
|
|
commands
|
|
.open
|
|
.as_deref()
|
|
.and_then(|operations| operations.first())
|
|
.and_then(|operation| {
|
|
literal_url(&operation.ref_id)
|
|
.map(|url| WebSearchAction::OpenPage { url: Some(url) })
|
|
})
|
|
})
|
|
.or_else(|| {
|
|
commands
|
|
.find
|
|
.as_deref()
|
|
.and_then(|operations| operations.first())
|
|
.map(|operation| WebSearchAction::FindInPage {
|
|
url: literal_url(&operation.ref_id),
|
|
pattern: Some(operation.pattern.clone()),
|
|
})
|
|
})
|
|
.unwrap_or(WebSearchAction::Other)
|
|
}
|
|
|
|
fn query_action(queries: &[SearchQuery]) -> Option<WebSearchAction> {
|
|
match queries {
|
|
[] => None,
|
|
[query] => Some(WebSearchAction::Search {
|
|
query: Some(query.q.clone()),
|
|
queries: None,
|
|
}),
|
|
queries => Some(WebSearchAction::Search {
|
|
query: None,
|
|
queries: Some(queries.iter().map(|query| query.q.clone()).collect()),
|
|
}),
|
|
}
|
|
}
|
|
|
|
fn literal_url(ref_id: &str) -> Option<String> {
|
|
Url::parse(ref_id).is_ok().then(|| ref_id.to_string())
|
|
}
|
|
|
|
fn web_search_item(call_id: &str, action: WebSearchAction) -> ExtensionTurnItem {
|
|
ExtensionTurnItem::WebSearch(WebSearchItem {
|
|
id: call_id.to_string(),
|
|
query: web_search_action_detail(&action),
|
|
action,
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use codex_api::SearchCommands;
|
|
use codex_protocol::models::WebSearchAction;
|
|
use pretty_assertions::assert_eq;
|
|
|
|
use super::command_action;
|
|
|
|
#[test]
|
|
fn command_action_reports_queries_and_navigation_detail() {
|
|
let cases = [
|
|
(
|
|
r#"{"image_query":[{"q":"waterfalls"},{"q":"mountains"}]}"#,
|
|
WebSearchAction::Search {
|
|
query: None,
|
|
queries: Some(vec!["waterfalls".to_string(), "mountains".to_string()]),
|
|
},
|
|
),
|
|
(
|
|
r#"{"open":[{"ref_id":"https://example.com/docs"}]}"#,
|
|
WebSearchAction::OpenPage {
|
|
url: Some("https://example.com/docs".to_string()),
|
|
},
|
|
),
|
|
(
|
|
r#"{"find":[{"ref_id":"https://example.com/docs","pattern":"install"}]}"#,
|
|
WebSearchAction::FindInPage {
|
|
url: Some("https://example.com/docs".to_string()),
|
|
pattern: Some("install".to_string()),
|
|
},
|
|
),
|
|
(
|
|
r#"{"find":[{"ref_id":"turn0search0","pattern":"install"}]}"#,
|
|
WebSearchAction::FindInPage {
|
|
url: None,
|
|
pattern: Some("install".to_string()),
|
|
},
|
|
),
|
|
(
|
|
r#"{"open":[{"ref_id":"turn0search0"}]}"#,
|
|
WebSearchAction::Other,
|
|
),
|
|
];
|
|
|
|
for (arguments, expected) in cases {
|
|
let commands: SearchCommands =
|
|
serde_json::from_str(arguments).expect("valid search command arguments");
|
|
assert_eq!(command_action(&commands), expected);
|
|
}
|
|
}
|
|
}
|