mirror of
https://github.com/openai/codex.git
synced 2026-04-02 13:31:47 +03:00
Compare commits
1 Commits
pr16496
...
latest-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d7ef3a249 |
@@ -87,7 +87,7 @@ members = [
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "0.0.0"
|
||||
version = "0.119.0-alpha.3"
|
||||
# Track the edition for all workspace crates in one place. Individual
|
||||
# crates can still override this value, but keeping it here means new
|
||||
# crates created with `cargo new -w ...` automatically inherit the 2024
|
||||
|
||||
@@ -2880,6 +2880,7 @@ mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
use rmcp::model::Content;
|
||||
use serde_json::Value as JsonValue;
|
||||
use serde_json::json;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
@@ -156,6 +156,13 @@ fn strip_total_output_header(output: &str) -> Option<(&str, u32)> {
|
||||
Some((remainder, total_lines))
|
||||
}
|
||||
|
||||
pub(crate) mod tools {
|
||||
#[cfg(test)]
|
||||
pub(crate) use codex_tools::ResponsesApiTool;
|
||||
pub(crate) use codex_tools::ToolSearchOutputTool;
|
||||
pub(crate) use codex_tools::ToolSpec;
|
||||
}
|
||||
|
||||
pub struct ResponseStream {
|
||||
pub(crate) rx_event: mpsc::Receiver<Result<ResponseEvent>>,
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ use codex_protocol::models::ResponseInputItem;
|
||||
use serde_json::Value as JsonValue;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::client_common::tools::ToolSpec;
|
||||
use crate::codex::Session;
|
||||
use crate::codex::TurnContext;
|
||||
use crate::function_tool::FunctionCallError;
|
||||
@@ -27,7 +28,6 @@ use crate::tools::router::ToolCallSource;
|
||||
use crate::tools::router::ToolRouterParams;
|
||||
use crate::unified_exec::resolve_max_tokens;
|
||||
use codex_features::Feature;
|
||||
use codex_tools::ToolSpec;
|
||||
use codex_tools::tool_spec_to_code_mode_tool_definition;
|
||||
use codex_utils_output_truncation::TruncationPolicy;
|
||||
use codex_utils_output_truncation::formatted_truncate_text_content_items_with_policy;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::client_common::tools::ToolSearchOutputTool;
|
||||
use crate::codex::Session;
|
||||
use crate::codex::TurnContext;
|
||||
use crate::tools::TELEMETRY_PREVIEW_MAX_BYTES;
|
||||
@@ -13,7 +14,6 @@ use codex_protocol::models::ResponseInputItem;
|
||||
use codex_protocol::models::SearchToolCallParams;
|
||||
use codex_protocol::models::ShellToolCallParams;
|
||||
use codex_protocol::models::function_call_output_content_items_to_text;
|
||||
use codex_tools::ToolSearchOutputTool;
|
||||
use codex_utils_output_truncation::TruncationPolicy;
|
||||
use codex_utils_output_truncation::formatted_truncate_text;
|
||||
use codex_utils_string::take_bytes_at_char_boundary;
|
||||
|
||||
@@ -142,7 +142,7 @@ fn tool_search_payloads_roundtrip_as_tool_search_outputs() {
|
||||
};
|
||||
let response = ToolSearchOutput {
|
||||
tools: vec![ToolSearchOutputTool::Function(
|
||||
codex_tools::ResponsesApiTool {
|
||||
crate::client_common::tools::ResponsesApiTool {
|
||||
name: "create_event".to_string(),
|
||||
description: String::new(),
|
||||
strict: false,
|
||||
|
||||
@@ -47,7 +47,10 @@ pub use request_user_input::RequestUserInputHandler;
|
||||
pub use shell::ShellCommandHandler;
|
||||
pub use shell::ShellHandler;
|
||||
pub use test_sync::TestSyncHandler;
|
||||
pub(crate) use tool_search::DEFAULT_LIMIT as TOOL_SEARCH_DEFAULT_LIMIT;
|
||||
pub(crate) use tool_search::TOOL_SEARCH_TOOL_NAME;
|
||||
pub use tool_search::ToolSearchHandler;
|
||||
pub(crate) use tool_suggest::TOOL_SUGGEST_TOOL_NAME;
|
||||
pub use tool_suggest::ToolSuggestHandler;
|
||||
pub use unified_exec::UnifiedExecHandler;
|
||||
pub use view_image::ViewImageHandler;
|
||||
|
||||
@@ -9,16 +9,20 @@ use async_trait::async_trait;
|
||||
use bm25::Document;
|
||||
use bm25::Language;
|
||||
use bm25::SearchEngineBuilder;
|
||||
use codex_tools::TOOL_SEARCH_DEFAULT_LIMIT;
|
||||
use codex_tools::TOOL_SEARCH_TOOL_NAME;
|
||||
use codex_tools::ToolSearchResultSource;
|
||||
use codex_tools::collect_tool_search_output_tools;
|
||||
use codex_tools::ResponsesApiNamespace;
|
||||
use codex_tools::ResponsesApiNamespaceTool;
|
||||
use codex_tools::ToolSearchOutputTool;
|
||||
use codex_tools::mcp_tool_to_deferred_responses_api_tool;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub struct ToolSearchHandler {
|
||||
tools: HashMap<String, ToolInfo>,
|
||||
}
|
||||
|
||||
pub(crate) const TOOL_SEARCH_TOOL_NAME: &str = "tool_search";
|
||||
pub(crate) const DEFAULT_LIMIT: usize = 8;
|
||||
|
||||
impl ToolSearchHandler {
|
||||
pub fn new(tools: HashMap<String, ToolInfo>) -> Self {
|
||||
Self { tools }
|
||||
@@ -54,7 +58,7 @@ impl ToolHandler for ToolSearchHandler {
|
||||
"query must not be empty".to_string(),
|
||||
));
|
||||
}
|
||||
let limit = args.limit.unwrap_or(TOOL_SEARCH_DEFAULT_LIMIT);
|
||||
let limit = args.limit.unwrap_or(DEFAULT_LIMIT);
|
||||
|
||||
if limit == 0 {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
@@ -78,28 +82,65 @@ impl ToolHandler for ToolSearchHandler {
|
||||
SearchEngineBuilder::<usize>::with_documents(Language::English, documents).build();
|
||||
let results = search_engine.search(query, limit);
|
||||
|
||||
let tools = collect_tool_search_output_tools(
|
||||
results
|
||||
.into_iter()
|
||||
.filter_map(|result| entries.get(result.document.id))
|
||||
.map(|(_name, tool)| ToolSearchResultSource {
|
||||
tool_namespace: tool.tool_namespace.as_str(),
|
||||
tool_name: tool.tool_name.as_str(),
|
||||
tool: &tool.tool,
|
||||
connector_name: tool.connector_name.as_deref(),
|
||||
connector_description: tool.connector_description.as_deref(),
|
||||
}),
|
||||
)
|
||||
.map_err(|err| {
|
||||
FunctionCallError::Fatal(format!(
|
||||
"failed to encode {TOOL_SEARCH_TOOL_NAME} output: {err}"
|
||||
))
|
||||
let matched_entries = results
|
||||
.into_iter()
|
||||
.filter_map(|result| entries.get(result.document.id))
|
||||
.collect::<Vec<_>>();
|
||||
let tools = serialize_tool_search_output_tools(&matched_entries).map_err(|err| {
|
||||
FunctionCallError::Fatal(format!("failed to encode tool_search output: {err}"))
|
||||
})?;
|
||||
|
||||
Ok(ToolSearchOutput { tools })
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_tool_search_output_tools(
|
||||
matched_entries: &[&(String, ToolInfo)],
|
||||
) -> Result<Vec<ToolSearchOutputTool>, serde_json::Error> {
|
||||
let grouped: BTreeMap<String, Vec<ToolInfo>> =
|
||||
matched_entries
|
||||
.iter()
|
||||
.fold(BTreeMap::new(), |mut acc, (_name, tool)| {
|
||||
acc.entry(tool.tool_namespace.clone())
|
||||
.or_default()
|
||||
.push(tool.clone());
|
||||
|
||||
acc
|
||||
});
|
||||
|
||||
let mut results = Vec::with_capacity(grouped.len());
|
||||
for (namespace, tools) in grouped {
|
||||
let Some(first_tool) = tools.first() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let description = first_tool.connector_description.clone().or_else(|| {
|
||||
first_tool
|
||||
.connector_name
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|connector_name| !connector_name.is_empty())
|
||||
.map(|connector_name| format!("Tools for working with {connector_name}."))
|
||||
});
|
||||
|
||||
let tools = tools
|
||||
.iter()
|
||||
.map(|tool| {
|
||||
mcp_tool_to_deferred_responses_api_tool(tool.tool_name.clone(), &tool.tool)
|
||||
.map(ResponsesApiNamespaceTool::Function)
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
results.push(ToolSearchOutputTool::Namespace(ResponsesApiNamespace {
|
||||
name: namespace,
|
||||
description: description.unwrap_or_default(),
|
||||
tools,
|
||||
}));
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
fn build_search_text(name: &str, info: &ToolInfo) -> String {
|
||||
let mut parts = vec![
|
||||
name.to_string(),
|
||||
@@ -142,3 +183,7 @@ fn build_search_text(name: &str, info: &ToolInfo) -> String {
|
||||
|
||||
parts.join(" ")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tool_search_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
199
codex-rs/core/src/tools/handlers/tool_search_tests.rs
Normal file
199
codex-rs/core/src/tools/handlers/tool_search_tests.rs
Normal file
@@ -0,0 +1,199 @@
|
||||
use super::*;
|
||||
use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use codex_tools::ResponsesApiTool;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rmcp::model::JsonObject;
|
||||
use rmcp::model::Tool;
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[test]
|
||||
fn serialize_tool_search_output_tools_groups_results_by_namespace() {
|
||||
let entries = [
|
||||
(
|
||||
"mcp__codex_apps__calendar_create_event".to_string(),
|
||||
ToolInfo {
|
||||
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
tool_name: "_create_event".to_string(),
|
||||
tool_namespace: "mcp__codex_apps__calendar".to_string(),
|
||||
tool: Tool {
|
||||
name: "calendar-create-event".to_string().into(),
|
||||
title: None,
|
||||
description: Some("Create a calendar event.".into()),
|
||||
input_schema: Arc::new(JsonObject::from_iter([(
|
||||
"type".to_string(),
|
||||
json!("object"),
|
||||
)])),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
execution: None,
|
||||
icons: None,
|
||||
meta: None,
|
||||
},
|
||||
connector_id: Some("calendar".to_string()),
|
||||
connector_name: Some("Calendar".to_string()),
|
||||
plugin_display_names: Vec::new(),
|
||||
connector_description: Some("Plan events".to_string()),
|
||||
},
|
||||
),
|
||||
(
|
||||
"mcp__codex_apps__gmail_read_email".to_string(),
|
||||
ToolInfo {
|
||||
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
tool_name: "_read_email".to_string(),
|
||||
tool_namespace: "mcp__codex_apps__gmail".to_string(),
|
||||
tool: Tool {
|
||||
name: "gmail-read-email".to_string().into(),
|
||||
title: None,
|
||||
description: Some("Read an email.".into()),
|
||||
input_schema: Arc::new(JsonObject::from_iter([(
|
||||
"type".to_string(),
|
||||
json!("object"),
|
||||
)])),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
execution: None,
|
||||
icons: None,
|
||||
meta: None,
|
||||
},
|
||||
connector_id: Some("gmail".to_string()),
|
||||
connector_name: Some("Gmail".to_string()),
|
||||
plugin_display_names: Vec::new(),
|
||||
connector_description: Some("Read mail".to_string()),
|
||||
},
|
||||
),
|
||||
(
|
||||
"mcp__codex_apps__calendar_list_events".to_string(),
|
||||
ToolInfo {
|
||||
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
tool_name: "_list_events".to_string(),
|
||||
tool_namespace: "mcp__codex_apps__calendar".to_string(),
|
||||
tool: Tool {
|
||||
name: "calendar-list-events".to_string().into(),
|
||||
title: None,
|
||||
description: Some("List calendar events.".into()),
|
||||
input_schema: Arc::new(JsonObject::from_iter([(
|
||||
"type".to_string(),
|
||||
json!("object"),
|
||||
)])),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
execution: None,
|
||||
icons: None,
|
||||
meta: None,
|
||||
},
|
||||
connector_id: Some("calendar".to_string()),
|
||||
connector_name: Some("Calendar".to_string()),
|
||||
plugin_display_names: Vec::new(),
|
||||
connector_description: Some("Plan events".to_string()),
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
let tools = serialize_tool_search_output_tools(&[&entries[0], &entries[1], &entries[2]])
|
||||
.expect("serialize tool search output");
|
||||
|
||||
assert_eq!(
|
||||
tools,
|
||||
vec![
|
||||
ToolSearchOutputTool::Namespace(ResponsesApiNamespace {
|
||||
name: "mcp__codex_apps__calendar".to_string(),
|
||||
description: "Plan events".to_string(),
|
||||
tools: vec![
|
||||
ResponsesApiNamespaceTool::Function(ResponsesApiTool {
|
||||
name: "_create_event".to_string(),
|
||||
description: "Create a calendar event.".to_string(),
|
||||
strict: false,
|
||||
defer_loading: Some(true),
|
||||
parameters: codex_tools::JsonSchema::Object {
|
||||
properties: Default::default(),
|
||||
required: None,
|
||||
additional_properties: None,
|
||||
},
|
||||
output_schema: None,
|
||||
}),
|
||||
ResponsesApiNamespaceTool::Function(ResponsesApiTool {
|
||||
name: "_list_events".to_string(),
|
||||
description: "List calendar events.".to_string(),
|
||||
strict: false,
|
||||
defer_loading: Some(true),
|
||||
parameters: codex_tools::JsonSchema::Object {
|
||||
properties: Default::default(),
|
||||
required: None,
|
||||
additional_properties: None,
|
||||
},
|
||||
output_schema: None,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
ToolSearchOutputTool::Namespace(ResponsesApiNamespace {
|
||||
name: "mcp__codex_apps__gmail".to_string(),
|
||||
description: "Read mail".to_string(),
|
||||
tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool {
|
||||
name: "_read_email".to_string(),
|
||||
description: "Read an email.".to_string(),
|
||||
strict: false,
|
||||
defer_loading: Some(true),
|
||||
parameters: codex_tools::JsonSchema::Object {
|
||||
properties: Default::default(),
|
||||
required: None,
|
||||
additional_properties: None,
|
||||
},
|
||||
output_schema: None,
|
||||
})],
|
||||
})
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_tool_search_output_tools_falls_back_to_connector_name_description() {
|
||||
let entries = [(
|
||||
"mcp__codex_apps__gmail_batch_read_email".to_string(),
|
||||
ToolInfo {
|
||||
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
tool_name: "_batch_read_email".to_string(),
|
||||
tool_namespace: "mcp__codex_apps__gmail".to_string(),
|
||||
tool: Tool {
|
||||
name: "gmail-batch-read-email".to_string().into(),
|
||||
title: None,
|
||||
description: Some("Read multiple emails.".into()),
|
||||
input_schema: Arc::new(JsonObject::from_iter([(
|
||||
"type".to_string(),
|
||||
json!("object"),
|
||||
)])),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
execution: None,
|
||||
icons: None,
|
||||
meta: None,
|
||||
},
|
||||
connector_id: Some("connector_gmail_456".to_string()),
|
||||
connector_name: Some("Gmail".to_string()),
|
||||
plugin_display_names: Vec::new(),
|
||||
connector_description: None,
|
||||
},
|
||||
)];
|
||||
|
||||
let tools = serialize_tool_search_output_tools(&[&entries[0]]).expect("serialize");
|
||||
|
||||
assert_eq!(
|
||||
tools,
|
||||
vec![ToolSearchOutputTool::Namespace(ResponsesApiNamespace {
|
||||
name: "mcp__codex_apps__gmail".to_string(),
|
||||
description: "Tools for working with Gmail.".to_string(),
|
||||
tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool {
|
||||
name: "_batch_read_email".to_string(),
|
||||
description: "Read multiple emails.".to_string(),
|
||||
strict: false,
|
||||
defer_loading: Some(true),
|
||||
parameters: codex_tools::JsonSchema::Object {
|
||||
properties: Default::default(),
|
||||
required: None,
|
||||
additional_properties: None,
|
||||
},
|
||||
output_schema: None,
|
||||
})],
|
||||
})]
|
||||
);
|
||||
}
|
||||
@@ -11,7 +11,6 @@ use codex_rmcp_client::ElicitationAction;
|
||||
use codex_tools::DiscoverableTool;
|
||||
use codex_tools::DiscoverableToolAction;
|
||||
use codex_tools::DiscoverableToolType;
|
||||
use codex_tools::TOOL_SUGGEST_TOOL_NAME;
|
||||
use codex_tools::filter_tool_suggest_discoverable_tools_for_client;
|
||||
use rmcp::model::RequestId;
|
||||
use serde::Deserialize;
|
||||
@@ -31,6 +30,7 @@ use crate::tools::registry::ToolKind;
|
||||
|
||||
pub struct ToolSuggestHandler;
|
||||
|
||||
pub(crate) const TOOL_SUGGEST_TOOL_NAME: &str = "tool_suggest";
|
||||
const TOOL_SUGGEST_APPROVAL_KIND_VALUE: &str = "tool_suggestion";
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
||||
@@ -31,6 +31,7 @@ use tracing::trace;
|
||||
use tracing::warn;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::client_common::tools::ToolSpec;
|
||||
use crate::codex::Session;
|
||||
use crate::codex::TurnContext;
|
||||
use crate::exec::ExecCapturePolicy;
|
||||
@@ -45,7 +46,6 @@ use codex_sandboxing::SandboxCommand;
|
||||
use codex_sandboxing::SandboxManager;
|
||||
use codex_sandboxing::SandboxTransformRequest;
|
||||
use codex_sandboxing::SandboxablePreference;
|
||||
use codex_tools::ToolSpec;
|
||||
use codex_utils_output_truncation::TruncationPolicy;
|
||||
use codex_utils_output_truncation::truncate_text;
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ use tracing::Instrument;
|
||||
use tracing::instrument;
|
||||
use tracing::trace_span;
|
||||
|
||||
use crate::client_common::tools::ToolSpec;
|
||||
use crate::codex::Session;
|
||||
use crate::codex::TurnContext;
|
||||
use crate::error::CodexErr;
|
||||
@@ -21,7 +22,6 @@ use crate::tools::router::ToolCall;
|
||||
use crate::tools::router::ToolCallSource;
|
||||
use crate::tools::router::ToolRouter;
|
||||
use codex_protocol::models::ResponseInputItem;
|
||||
use codex_tools::ToolSpec;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ToolCallRuntime {
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::client_common::tools::ToolSpec;
|
||||
use crate::function_tool::FunctionCallError;
|
||||
use crate::hook_runtime::record_additional_contexts;
|
||||
use crate::hook_runtime::run_post_tool_use_hooks;
|
||||
@@ -24,7 +25,6 @@ use codex_hooks::HookToolInputLocalShell;
|
||||
use codex_hooks::HookToolKind;
|
||||
use codex_protocol::models::ResponseInputItem;
|
||||
use codex_tools::ConfiguredToolSpec;
|
||||
use codex_tools::ToolSpec;
|
||||
use codex_utils_readiness::Readiness;
|
||||
use serde_json::Value;
|
||||
use tracing::warn;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::client_common::tools::ToolSpec;
|
||||
use crate::codex::Session;
|
||||
use crate::codex::TurnContext;
|
||||
use crate::function_tool::FunctionCallError;
|
||||
@@ -17,7 +18,6 @@ use codex_protocol::models::SearchToolCallParams;
|
||||
use codex_protocol::models::ShellToolCallParams;
|
||||
use codex_tools::ConfiguredToolSpec;
|
||||
use codex_tools::DiscoverableTool;
|
||||
use codex_tools::ToolSpec;
|
||||
use rmcp::model::Tool;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -1,31 +1,32 @@
|
||||
use crate::client_common::tools::ToolSpec;
|
||||
use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;
|
||||
use crate::mcp_connection_manager::ToolInfo;
|
||||
use crate::shell::Shell;
|
||||
use crate::shell::ShellType;
|
||||
use crate::tools::code_mode::PUBLIC_TOOL_NAME;
|
||||
use crate::tools::code_mode::WAIT_TOOL_NAME;
|
||||
use crate::tools::handlers::TOOL_SEARCH_DEFAULT_LIMIT;
|
||||
use crate::tools::handlers::TOOL_SEARCH_TOOL_NAME;
|
||||
use crate::tools::handlers::TOOL_SUGGEST_TOOL_NAME;
|
||||
use crate::tools::handlers::agent_jobs::BatchJobHandler;
|
||||
use crate::tools::handlers::multi_agents_common::DEFAULT_WAIT_TIMEOUT_MS;
|
||||
use crate::tools::handlers::multi_agents_common::MAX_WAIT_TIMEOUT_MS;
|
||||
use crate::tools::handlers::multi_agents_common::MIN_WAIT_TIMEOUT_MS;
|
||||
use crate::tools::registry::ToolRegistryBuilder;
|
||||
use crate::tools::registry::tool_handler_key;
|
||||
use codex_protocol::config_types::WebSearchMode;
|
||||
use codex_protocol::dynamic_tools::DynamicToolSpec;
|
||||
use codex_protocol::openai_models::ApplyPatchToolType;
|
||||
use codex_protocol::openai_models::ConfigShellToolType;
|
||||
use codex_protocol::openai_models::WebSearchToolType;
|
||||
use codex_tools::CommandToolOptions;
|
||||
use codex_tools::DiscoverableTool;
|
||||
use codex_tools::ShellToolOptions;
|
||||
use codex_tools::SpawnAgentToolOptions;
|
||||
use codex_tools::TOOL_SEARCH_DEFAULT_LIMIT;
|
||||
use codex_tools::TOOL_SEARCH_TOOL_NAME;
|
||||
use codex_tools::TOOL_SUGGEST_TOOL_NAME;
|
||||
use codex_tools::ToolSearchAppSource;
|
||||
use codex_tools::ToolSpec;
|
||||
use codex_tools::ToolUserShellType;
|
||||
use codex_tools::ViewImageToolOptions;
|
||||
use codex_tools::WaitAgentTimeoutOptions;
|
||||
use codex_tools::WebSearchToolOptions;
|
||||
use codex_tools::augment_tool_spec_for_code_mode;
|
||||
use codex_tools::collect_tool_search_app_infos;
|
||||
use codex_tools::collect_tool_suggest_entries;
|
||||
@@ -36,14 +37,12 @@ use codex_tools::create_close_agent_tool_v1;
|
||||
use codex_tools::create_close_agent_tool_v2;
|
||||
use codex_tools::create_code_mode_tool;
|
||||
use codex_tools::create_exec_command_tool;
|
||||
use codex_tools::create_image_generation_tool;
|
||||
use codex_tools::create_js_repl_reset_tool;
|
||||
use codex_tools::create_js_repl_tool;
|
||||
use codex_tools::create_list_agents_tool;
|
||||
use codex_tools::create_list_dir_tool;
|
||||
use codex_tools::create_list_mcp_resource_templates_tool;
|
||||
use codex_tools::create_list_mcp_resources_tool;
|
||||
use codex_tools::create_local_shell_tool;
|
||||
use codex_tools::create_read_mcp_resource_tool;
|
||||
use codex_tools::create_report_agent_job_result_tool;
|
||||
use codex_tools::create_request_permissions_tool;
|
||||
@@ -64,7 +63,6 @@ use codex_tools::create_view_image_tool;
|
||||
use codex_tools::create_wait_agent_tool_v1;
|
||||
use codex_tools::create_wait_agent_tool_v2;
|
||||
use codex_tools::create_wait_tool;
|
||||
use codex_tools::create_web_search_tool;
|
||||
use codex_tools::create_write_stdin_tool;
|
||||
use codex_tools::dynamic_tool_to_responses_api_tool;
|
||||
use codex_tools::mcp_tool_to_responses_api_tool;
|
||||
@@ -82,6 +80,8 @@ pub use codex_tools::ZshForkConfig;
|
||||
#[cfg(test)]
|
||||
pub(crate) use codex_tools::mcp_call_tool_result_output_schema;
|
||||
|
||||
const WEB_SEARCH_CONTENT_TYPES: [&str; 2] = ["text", "image"];
|
||||
|
||||
pub(crate) fn tool_user_shell_type(user_shell: &Shell) -> ToolUserShellType {
|
||||
match user_shell.shell_type {
|
||||
ShellType::Zsh => ToolUserShellType::Zsh,
|
||||
@@ -243,7 +243,7 @@ pub(crate) fn build_specs_with_discoverable_tools(
|
||||
ConfigShellToolType::Local => {
|
||||
push_tool_spec(
|
||||
&mut builder,
|
||||
create_local_shell_tool(),
|
||||
ToolSpec::LocalShell {},
|
||||
/*supports_parallel_tool_calls*/ true,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
@@ -453,14 +453,41 @@ pub(crate) fn build_specs_with_discoverable_tools(
|
||||
builder.register_handler("test_sync_tool", test_sync_handler);
|
||||
}
|
||||
|
||||
if let Some(web_search_tool) = create_web_search_tool(WebSearchToolOptions {
|
||||
web_search_mode: config.web_search_mode,
|
||||
web_search_config: config.web_search_config.as_ref(),
|
||||
web_search_tool_type: config.web_search_tool_type,
|
||||
}) {
|
||||
let external_web_access = match config.web_search_mode {
|
||||
Some(WebSearchMode::Cached) => Some(false),
|
||||
Some(WebSearchMode::Live) => Some(true),
|
||||
Some(WebSearchMode::Disabled) | None => None,
|
||||
};
|
||||
|
||||
if let Some(external_web_access) = external_web_access {
|
||||
let search_content_types = match config.web_search_tool_type {
|
||||
WebSearchToolType::Text => None,
|
||||
WebSearchToolType::TextAndImage => Some(
|
||||
WEB_SEARCH_CONTENT_TYPES
|
||||
.into_iter()
|
||||
.map(str::to_string)
|
||||
.collect(),
|
||||
),
|
||||
};
|
||||
|
||||
push_tool_spec(
|
||||
&mut builder,
|
||||
web_search_tool,
|
||||
ToolSpec::WebSearch {
|
||||
external_web_access: Some(external_web_access),
|
||||
filters: config
|
||||
.web_search_config
|
||||
.as_ref()
|
||||
.and_then(|cfg| cfg.filters.clone().map(Into::into)),
|
||||
user_location: config
|
||||
.web_search_config
|
||||
.as_ref()
|
||||
.and_then(|cfg| cfg.user_location.clone().map(Into::into)),
|
||||
search_context_size: config
|
||||
.web_search_config
|
||||
.as_ref()
|
||||
.and_then(|cfg| cfg.search_context_size),
|
||||
search_content_types,
|
||||
},
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
@@ -469,7 +496,9 @@ pub(crate) fn build_specs_with_discoverable_tools(
|
||||
if config.image_gen_tool {
|
||||
push_tool_spec(
|
||||
&mut builder,
|
||||
create_image_generation_tool("png"),
|
||||
ToolSpec::ImageGeneration {
|
||||
output_format: "png".to_string(),
|
||||
},
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
|
||||
@@ -15,7 +15,6 @@ use codex_protocol::models::VIEW_IMAGE_TOOL_NAME;
|
||||
use codex_protocol::openai_models::InputModality;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::openai_models::ModelsResponse;
|
||||
use codex_protocol::openai_models::WebSearchToolType;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
@@ -1278,7 +1277,12 @@ fn web_search_tool_type_text_and_image_sets_search_content_types() {
|
||||
filters: None,
|
||||
user_location: None,
|
||||
search_context_size: None,
|
||||
search_content_types: Some(vec!["text".to_string(), "image".to_string()]),
|
||||
search_content_types: Some(
|
||||
WEB_SEARCH_CONTENT_TYPES
|
||||
.into_iter()
|
||||
.map(str::to_string)
|
||||
.collect()
|
||||
),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -90,15 +90,10 @@ pub use tool_discovery::DiscoverablePluginInfo;
|
||||
pub use tool_discovery::DiscoverableTool;
|
||||
pub use tool_discovery::DiscoverableToolAction;
|
||||
pub use tool_discovery::DiscoverableToolType;
|
||||
pub use tool_discovery::TOOL_SEARCH_DEFAULT_LIMIT;
|
||||
pub use tool_discovery::TOOL_SEARCH_TOOL_NAME;
|
||||
pub use tool_discovery::TOOL_SUGGEST_TOOL_NAME;
|
||||
pub use tool_discovery::ToolSearchAppInfo;
|
||||
pub use tool_discovery::ToolSearchAppSource;
|
||||
pub use tool_discovery::ToolSearchResultSource;
|
||||
pub use tool_discovery::ToolSuggestEntry;
|
||||
pub use tool_discovery::collect_tool_search_app_infos;
|
||||
pub use tool_discovery::collect_tool_search_output_tools;
|
||||
pub use tool_discovery::collect_tool_suggest_entries;
|
||||
pub use tool_discovery::create_tool_search_tool;
|
||||
pub use tool_discovery::create_tool_suggest_tool;
|
||||
@@ -107,11 +102,7 @@ pub use tool_spec::ConfiguredToolSpec;
|
||||
pub use tool_spec::ResponsesApiWebSearchFilters;
|
||||
pub use tool_spec::ResponsesApiWebSearchUserLocation;
|
||||
pub use tool_spec::ToolSpec;
|
||||
pub use tool_spec::WebSearchToolOptions;
|
||||
pub use tool_spec::create_image_generation_tool;
|
||||
pub use tool_spec::create_local_shell_tool;
|
||||
pub use tool_spec::create_tools_json_for_responses_api;
|
||||
pub use tool_spec::create_web_search_tool;
|
||||
pub use utility_tool::create_list_dir_tool;
|
||||
pub use utility_tool::create_test_sync_tool;
|
||||
pub use view_image::ViewImageToolOptions;
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
use crate::JsonSchema;
|
||||
use crate::ResponsesApiNamespace;
|
||||
use crate::ResponsesApiNamespaceTool;
|
||||
use crate::ResponsesApiTool;
|
||||
use crate::ToolSearchOutputTool;
|
||||
use crate::ToolSpec;
|
||||
use crate::mcp_tool_to_deferred_responses_api_tool;
|
||||
use codex_app_server_protocol::AppInfo;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
const TUI_CLIENT_NAME: &str = "codex-tui";
|
||||
pub const TOOL_SEARCH_TOOL_NAME: &str = "tool_search";
|
||||
pub const TOOL_SEARCH_DEFAULT_LIMIT: usize = 8;
|
||||
pub const TOOL_SUGGEST_TOOL_NAME: &str = "tool_suggest";
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ToolSearchAppInfo {
|
||||
@@ -28,15 +21,6 @@ pub struct ToolSearchAppSource<'a> {
|
||||
pub connector_description: Option<&'a str>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct ToolSearchResultSource<'a> {
|
||||
pub tool_namespace: &'a str,
|
||||
pub tool_name: &'a str,
|
||||
pub tool: &'a rmcp::model::Tool,
|
||||
pub connector_name: Option<&'a str>,
|
||||
pub connector_description: Option<&'a str>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DiscoverableToolType {
|
||||
@@ -187,7 +171,7 @@ pub fn create_tool_search_tool(app_tools: &[ToolSearchAppInfo], default_limit: u
|
||||
};
|
||||
|
||||
let description = format!(
|
||||
"# Apps (Connectors) tool discovery\n\nSearches over apps/connectors tool metadata with BM25 and exposes matching tools for the next model call.\n\nYou have access to all the tools of the following apps/connectors:\n{app_descriptions}\nSome of the tools may not have been provided to you upfront, and you should use this tool (`{TOOL_SEARCH_TOOL_NAME}`) to search for the required tools and load them for the apps mentioned above. For the apps mentioned above, always use `{TOOL_SEARCH_TOOL_NAME}` instead of `list_mcp_resources` or `list_mcp_resource_templates` for tool discovery."
|
||||
"# Apps (Connectors) tool discovery\n\nSearches over apps/connectors tool metadata with BM25 and exposes matching tools for the next model call.\n\nYou have access to all the tools of the following apps/connectors:\n{app_descriptions}\nSome of the tools may not have been provided to you upfront, and you should use this tool (`tool_search`) to search for the required tools and load them for the apps mentioned above. For the apps mentioned above, always use `tool_search` instead of `list_mcp_resources` or `list_mcp_resource_templates` for tool discovery."
|
||||
);
|
||||
|
||||
ToolSpec::ToolSearch {
|
||||
@@ -201,52 +185,6 @@ pub fn create_tool_search_tool(app_tools: &[ToolSearchAppInfo], default_limit: u
|
||||
}
|
||||
}
|
||||
|
||||
pub fn collect_tool_search_output_tools<'a>(
|
||||
tool_sources: impl IntoIterator<Item = ToolSearchResultSource<'a>>,
|
||||
) -> Result<Vec<ToolSearchOutputTool>, serde_json::Error> {
|
||||
let grouped = tool_sources.into_iter().fold(
|
||||
BTreeMap::<&'a str, Vec<ToolSearchResultSource<'a>>>::new(),
|
||||
|mut grouped, tool| {
|
||||
grouped.entry(tool.tool_namespace).or_default().push(tool);
|
||||
grouped
|
||||
},
|
||||
);
|
||||
|
||||
let mut results = Vec::with_capacity(grouped.len());
|
||||
for (tool_namespace, tools) in grouped {
|
||||
let Some(first_tool) = tools.first() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let description = first_tool
|
||||
.connector_description
|
||||
.map(str::to_string)
|
||||
.or_else(|| {
|
||||
first_tool
|
||||
.connector_name
|
||||
.map(str::trim)
|
||||
.filter(|connector_name| !connector_name.is_empty())
|
||||
.map(|connector_name| format!("Tools for working with {connector_name}."))
|
||||
});
|
||||
|
||||
let tools = tools
|
||||
.iter()
|
||||
.map(|tool| {
|
||||
mcp_tool_to_deferred_responses_api_tool(tool.tool_name.to_string(), tool.tool)
|
||||
.map(ResponsesApiNamespaceTool::Function)
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
results.push(ToolSearchOutputTool::Namespace(ResponsesApiNamespace {
|
||||
name: tool_namespace.to_string(),
|
||||
description: description.unwrap_or_default(),
|
||||
tools,
|
||||
}));
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
pub fn collect_tool_search_app_infos<'a>(
|
||||
app_tools: impl IntoIterator<Item = ToolSearchAppSource<'a>>,
|
||||
codex_apps_server_name: &str,
|
||||
@@ -315,11 +253,11 @@ pub fn create_tool_suggest_tool(discoverable_tools: &[ToolSuggestEntry]) -> Tool
|
||||
|
||||
let discoverable_tools = format_discoverable_tools(discoverable_tools);
|
||||
let description = format!(
|
||||
"# Tool suggestion discovery\n\nSuggests a missing connector in an installed plugin, or in narrower cases a not installed but discoverable plugin, when the user clearly wants a capability that is not currently available in the active `tools` list.\n\nUse this ONLY when:\n- You've already tried to find a matching available tool for the user's request but couldn't find a good match. This includes `{TOOL_SEARCH_TOOL_NAME}` (if available) and other means.\n- For connectors/apps that are not installed but needed for an installed plugin, suggest to install them if the task requirements match precisely.\n- For plugins that are not installed but discoverable, only suggest discoverable and installable plugins when the user's intent very explicitly and unambiguously matches that plugin itself. Do not suggest a plugin just because one of its connectors or capabilities seems relevant.\n\nTool suggestions should only use the discoverable tools listed here. DO NOT explore or recommend tools that are not on this list.\n\nDiscoverable tools:\n{discoverable_tools}\n\nWorkflow:\n\n1. Ensure all possible means have been exhausted to find an existing available tool but none of them matches the request intent.\n2. Match the user's request against the discoverable tools list above. Apply the stricter explicit-and-unambiguous rule for *discoverable tools* like plugin install suggestions; *missing tools* like connector install suggestions continue to use the normal clear-fit standard.\n3. If one tool clearly fits, call `{TOOL_SUGGEST_TOOL_NAME}` with:\n - `tool_type`: `connector` or `plugin`\n - `action_type`: `install` or `enable`\n - `tool_id`: exact id from the discoverable tools list above\n - `suggest_reason`: concise one-line user-facing reason this tool can help with the current request\n4. After the suggestion flow completes:\n - if the user finished the install or enable flow, continue by searching again or using the newly available tool\n - if the user did not finish, continue without that tool, and don't suggest that tool again unless the user explicitly asks for it."
|
||||
"# Tool suggestion discovery\n\nSuggests a missing connector in an installed plugin, or in narrower cases a not installed but discoverable plugin, when the user clearly wants a capability that is not currently available in the active `tools` list.\n\nUse this ONLY when:\n- You've already tried to find a matching available tool for the user's request but couldn't find a good match. This includes `tool_search` (if available) and other means.\n- For connectors/apps that are not installed but needed for an installed plugin, suggest to install them if the task requirements match precisely.\n- For plugins that are not installed but discoverable, only suggest discoverable and installable plugins when the user's intent very explicitly and unambiguously matches that plugin itself. Do not suggest a plugin just because one of its connectors or capabilities seems relevant.\n\nTool suggestions should only use the discoverable tools listed here. DO NOT explore or recommend tools that are not on this list.\n\nDiscoverable tools:\n{discoverable_tools}\n\nWorkflow:\n\n1. Ensure all possible means have been exhausted to find an existing available tool but none of them matches the request intent.\n2. Match the user's request against the discoverable tools list above. Apply the stricter explicit-and-unambiguous rule for *discoverable tools* like plugin install suggestions; *missing tools* like connector install suggestions continue to use the normal clear-fit standard.\n3. If one tool clearly fits, call `tool_suggest` with:\n - `tool_type`: `connector` or `plugin`\n - `action_type`: `install` or `enable`\n - `tool_id`: exact id from the discoverable tools list above\n - `suggest_reason`: concise one-line user-facing reason this tool can help with the current request\n4. After the suggestion flow completes:\n - if the user finished the install or enable flow, continue by searching again or using the newly available tool\n - if the user did not finish, continue without that tool, and don't suggest that tool again unless the user explicitly asks for it."
|
||||
);
|
||||
|
||||
ToolSpec::Function(ResponsesApiTool {
|
||||
name: TOOL_SUGGEST_TOOL_NAME.to_string(),
|
||||
name: "tool_suggest".to_string(),
|
||||
description,
|
||||
strict: false,
|
||||
defer_loading: None,
|
||||
|
||||
@@ -1,28 +1,8 @@
|
||||
use super::*;
|
||||
use codex_app_server_protocol::AppInfo;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rmcp::model::JsonObject;
|
||||
use rmcp::model::Tool;
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn mcp_tool(name: &str, description: &str) -> Tool {
|
||||
Tool {
|
||||
name: name.to_string().into(),
|
||||
title: None,
|
||||
description: Some(description.to_string().into()),
|
||||
input_schema: Arc::new(JsonObject::from_iter([(
|
||||
"type".to_string(),
|
||||
json!("object"),
|
||||
)])),
|
||||
output_schema: None,
|
||||
annotations: None,
|
||||
execution: None,
|
||||
icons: None,
|
||||
meta: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_tool_search_tool_deduplicates_and_renders_enabled_apps() {
|
||||
@@ -155,124 +135,6 @@ fn create_tool_suggest_tool_uses_plugin_summary_fallback() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collect_tool_search_output_tools_groups_results_by_namespace() {
|
||||
let calendar_create_event = mcp_tool("calendar-create-event", "Create a calendar event.");
|
||||
let gmail_read_email = mcp_tool("gmail-read-email", "Read an email.");
|
||||
let calendar_list_events = mcp_tool("calendar-list-events", "List calendar events.");
|
||||
|
||||
let tools = collect_tool_search_output_tools([
|
||||
ToolSearchResultSource {
|
||||
tool_namespace: "mcp__codex_apps__calendar",
|
||||
tool_name: "_create_event",
|
||||
tool: &calendar_create_event,
|
||||
connector_name: Some("Calendar"),
|
||||
connector_description: Some("Plan events"),
|
||||
},
|
||||
ToolSearchResultSource {
|
||||
tool_namespace: "mcp__codex_apps__gmail",
|
||||
tool_name: "_read_email",
|
||||
tool: &gmail_read_email,
|
||||
connector_name: Some("Gmail"),
|
||||
connector_description: Some("Read mail"),
|
||||
},
|
||||
ToolSearchResultSource {
|
||||
tool_namespace: "mcp__codex_apps__calendar",
|
||||
tool_name: "_list_events",
|
||||
tool: &calendar_list_events,
|
||||
connector_name: Some("Calendar"),
|
||||
connector_description: Some("Plan events"),
|
||||
},
|
||||
])
|
||||
.expect("collect tool search output tools");
|
||||
|
||||
assert_eq!(
|
||||
tools,
|
||||
vec![
|
||||
ToolSearchOutputTool::Namespace(ResponsesApiNamespace {
|
||||
name: "mcp__codex_apps__calendar".to_string(),
|
||||
description: "Plan events".to_string(),
|
||||
tools: vec![
|
||||
ResponsesApiNamespaceTool::Function(ResponsesApiTool {
|
||||
name: "_create_event".to_string(),
|
||||
description: "Create a calendar event.".to_string(),
|
||||
strict: false,
|
||||
defer_loading: Some(true),
|
||||
parameters: JsonSchema::Object {
|
||||
properties: Default::default(),
|
||||
required: None,
|
||||
additional_properties: None,
|
||||
},
|
||||
output_schema: None,
|
||||
}),
|
||||
ResponsesApiNamespaceTool::Function(ResponsesApiTool {
|
||||
name: "_list_events".to_string(),
|
||||
description: "List calendar events.".to_string(),
|
||||
strict: false,
|
||||
defer_loading: Some(true),
|
||||
parameters: JsonSchema::Object {
|
||||
properties: Default::default(),
|
||||
required: None,
|
||||
additional_properties: None,
|
||||
},
|
||||
output_schema: None,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
ToolSearchOutputTool::Namespace(ResponsesApiNamespace {
|
||||
name: "mcp__codex_apps__gmail".to_string(),
|
||||
description: "Read mail".to_string(),
|
||||
tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool {
|
||||
name: "_read_email".to_string(),
|
||||
description: "Read an email.".to_string(),
|
||||
strict: false,
|
||||
defer_loading: Some(true),
|
||||
parameters: JsonSchema::Object {
|
||||
properties: Default::default(),
|
||||
required: None,
|
||||
additional_properties: None,
|
||||
},
|
||||
output_schema: None,
|
||||
})],
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collect_tool_search_output_tools_falls_back_to_connector_name_description() {
|
||||
let gmail_batch_read_email = mcp_tool("gmail-batch-read-email", "Read multiple emails.");
|
||||
|
||||
let tools = collect_tool_search_output_tools([ToolSearchResultSource {
|
||||
tool_namespace: "mcp__codex_apps__gmail",
|
||||
tool_name: "_batch_read_email",
|
||||
tool: &gmail_batch_read_email,
|
||||
connector_name: Some("Gmail"),
|
||||
connector_description: None,
|
||||
}])
|
||||
.expect("collect tool search output tools");
|
||||
|
||||
assert_eq!(
|
||||
tools,
|
||||
vec![ToolSearchOutputTool::Namespace(ResponsesApiNamespace {
|
||||
name: "mcp__codex_apps__gmail".to_string(),
|
||||
description: "Tools for working with Gmail.".to_string(),
|
||||
tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool {
|
||||
name: "_batch_read_email".to_string(),
|
||||
description: "Read multiple emails.".to_string(),
|
||||
strict: false,
|
||||
defer_loading: Some(true),
|
||||
parameters: JsonSchema::Object {
|
||||
properties: Default::default(),
|
||||
required: None,
|
||||
additional_properties: None,
|
||||
},
|
||||
output_schema: None,
|
||||
})],
|
||||
})],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discoverable_tool_enums_use_expected_wire_names() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
use crate::FreeformTool;
|
||||
use crate::JsonSchema;
|
||||
use crate::ResponsesApiTool;
|
||||
use codex_protocol::config_types::WebSearchConfig;
|
||||
use codex_protocol::config_types::WebSearchContextSize;
|
||||
use codex_protocol::config_types::WebSearchFilters as ConfigWebSearchFilters;
|
||||
use codex_protocol::config_types::WebSearchMode;
|
||||
use codex_protocol::config_types::WebSearchUserLocation as ConfigWebSearchUserLocation;
|
||||
use codex_protocol::config_types::WebSearchUserLocationType;
|
||||
use codex_protocol::openai_models::WebSearchToolType;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
|
||||
const WEB_SEARCH_TEXT_AND_IMAGE_CONTENT_TYPES: [&str; 2] = ["text", "image"];
|
||||
|
||||
/// When serialized as JSON, this produces a valid "Tool" in the OpenAI
|
||||
/// Responses API.
|
||||
#[derive(Debug, Clone, Serialize, PartialEq)]
|
||||
@@ -66,54 +61,6 @@ impl ToolSpec {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_local_shell_tool() -> ToolSpec {
|
||||
ToolSpec::LocalShell {}
|
||||
}
|
||||
|
||||
pub fn create_image_generation_tool(output_format: &str) -> ToolSpec {
|
||||
ToolSpec::ImageGeneration {
|
||||
output_format: output_format.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WebSearchToolOptions<'a> {
|
||||
pub web_search_mode: Option<WebSearchMode>,
|
||||
pub web_search_config: Option<&'a WebSearchConfig>,
|
||||
pub web_search_tool_type: WebSearchToolType,
|
||||
}
|
||||
|
||||
pub fn create_web_search_tool(options: WebSearchToolOptions<'_>) -> Option<ToolSpec> {
|
||||
let external_web_access = match options.web_search_mode {
|
||||
Some(WebSearchMode::Cached) => Some(false),
|
||||
Some(WebSearchMode::Live) => Some(true),
|
||||
Some(WebSearchMode::Disabled) | None => None,
|
||||
}?;
|
||||
|
||||
let search_content_types = match options.web_search_tool_type {
|
||||
WebSearchToolType::Text => None,
|
||||
WebSearchToolType::TextAndImage => Some(
|
||||
WEB_SEARCH_TEXT_AND_IMAGE_CONTENT_TYPES
|
||||
.into_iter()
|
||||
.map(str::to_string)
|
||||
.collect(),
|
||||
),
|
||||
};
|
||||
|
||||
Some(ToolSpec::WebSearch {
|
||||
external_web_access: Some(external_web_access),
|
||||
filters: options
|
||||
.web_search_config
|
||||
.and_then(|config| config.filters.clone().map(Into::into)),
|
||||
user_location: options
|
||||
.web_search_config
|
||||
.and_then(|config| config.user_location.clone().map(Into::into)),
|
||||
search_context_size: options
|
||||
.web_search_config
|
||||
.and_then(|config| config.search_context_size),
|
||||
search_content_types,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ConfiguredToolSpec {
|
||||
pub spec: ToolSpec,
|
||||
|
||||
Reference in New Issue
Block a user