mirror of
https://github.com/openai/codex.git
synced 2026-06-01 19:02:59 +00:00
codex-tools: extract discovery tool specs (#16193)
## Why `core/src/tools/spec.rs` still owned the pure `tool_search` and `tool_suggest` spec builders even though that logic no longer needed `codex-core` runtime state. This change continues the `codex-tools` migration by moving the reusable discovery and suggestion spec construction out of `codex-core` so `spec.rs` is left with the core-owned policy decisions about when these tools are exposed and what metadata is available. ## What changed - Added `codex-rs/tools/src/tool_discovery.rs` with the shared `tool_search` and `tool_suggest` spec builders, plus focused unit tests in `tool_discovery_tests.rs`. - Moved the shared `DiscoverableToolAction` and `DiscoverableToolType` declarations into `codex-tools` so the `tool_suggest` handler and the extracted spec builders use the same wire-model enums. - Updated `core/src/tools/spec.rs` to translate `ToolInfo` and `DiscoverableTool` values into neutral `codex-tools` inputs and delegate the actual spec building there. - Removed the old template-based description rendering helpers from `core/src/tools/spec.rs` and deleted the now-dead helper methods in `core/src/tools/discoverable.rs`. - Updated `codex-rs/tools/README.md` to document that discovery and suggestion models/spec builders now live in `codex-tools`. ## Test plan - `cargo test -p codex-tools` - `CARGO_TARGET_DIR=/tmp/codex-core-discovery-specs cargo test -p codex-core --lib tools::spec::` - `CARGO_TARGET_DIR=/tmp/codex-core-discovery-specs cargo test -p codex-core --lib tools::handlers::tool_suggest::` - `just argument-comment-lint` ## References - #16154 - #15923 - #15928 - #15944 - #15953 - #16031 - #16047 - #16129 - #16132 - #16138 - #16141
This commit is contained in:
@@ -1,42 +1,9 @@
|
||||
use crate::plugins::PluginCapabilitySummary;
|
||||
use codex_app_server_protocol::AppInfo;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use codex_tools::DiscoverableToolType;
|
||||
|
||||
const TUI_CLIENT_NAME: &str = "codex-tui";
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub(crate) enum DiscoverableToolType {
|
||||
Connector,
|
||||
Plugin,
|
||||
}
|
||||
|
||||
impl DiscoverableToolType {
|
||||
pub(crate) fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Connector => "connector",
|
||||
Self::Plugin => "plugin",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub(crate) enum DiscoverableToolAction {
|
||||
Install,
|
||||
Enable,
|
||||
}
|
||||
|
||||
impl DiscoverableToolAction {
|
||||
pub(crate) fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Install => "install",
|
||||
Self::Enable => "enable",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) enum DiscoverableTool {
|
||||
Connector(Box<AppInfo>),
|
||||
@@ -65,13 +32,6 @@ impl DiscoverableTool {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn description(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::Connector(connector) => connector.description.as_deref(),
|
||||
Self::Plugin(plugin) => plugin.description.as_deref(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn install_url(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::Connector(connector) => connector.install_url.as_deref(),
|
||||
|
||||
@@ -8,6 +8,8 @@ use codex_app_server_protocol::McpElicitationSchema;
|
||||
use codex_app_server_protocol::McpServerElicitationRequest;
|
||||
use codex_app_server_protocol::McpServerElicitationRequestParams;
|
||||
use codex_rmcp_client::ElicitationAction;
|
||||
use codex_tools::DiscoverableToolAction;
|
||||
use codex_tools::DiscoverableToolType;
|
||||
use rmcp::model::RequestId;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
@@ -21,8 +23,6 @@ use crate::tools::context::FunctionToolOutput;
|
||||
use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::discoverable::DiscoverableTool;
|
||||
use crate::tools::discoverable::DiscoverableToolAction;
|
||||
use crate::tools::discoverable::DiscoverableToolType;
|
||||
use crate::tools::discoverable::filter_tool_suggest_discoverable_tools_for_client;
|
||||
use crate::tools::handlers::parse_arguments;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
|
||||
@@ -8,6 +8,8 @@ use crate::plugins::test_support::write_plugins_feature_config;
|
||||
use crate::tools::discoverable::DiscoverablePluginInfo;
|
||||
use crate::tools::discoverable::filter_tool_suggest_discoverable_tools_for_client;
|
||||
use codex_app_server_protocol::AppInfo;
|
||||
use codex_tools::DiscoverableToolAction;
|
||||
use codex_tools::DiscoverableToolType;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
|
||||
@@ -7,10 +7,7 @@ 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::discoverable::DiscoverablePluginInfo;
|
||||
use crate::tools::discoverable::DiscoverableTool;
|
||||
use crate::tools::discoverable::DiscoverableToolAction;
|
||||
use crate::tools::discoverable::DiscoverableToolType;
|
||||
use crate::tools::handlers::PLAN_TOOL;
|
||||
use crate::tools::handlers::TOOL_SEARCH_DEFAULT_LIMIT;
|
||||
use crate::tools::handlers::TOOL_SEARCH_TOOL_NAME;
|
||||
@@ -41,9 +38,11 @@ use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use codex_tools::CommandToolOptions;
|
||||
use codex_tools::ResponsesApiTool;
|
||||
use codex_tools::DiscoverableToolType;
|
||||
use codex_tools::ShellToolOptions;
|
||||
use codex_tools::SpawnAgentToolOptions;
|
||||
use codex_tools::ToolSearchAppInfo;
|
||||
use codex_tools::ToolSuggestEntry;
|
||||
use codex_tools::ViewImageToolOptions;
|
||||
use codex_tools::WaitAgentTimeoutOptions;
|
||||
use codex_tools::augment_tool_spec_for_code_mode;
|
||||
@@ -71,6 +70,8 @@ use codex_tools::create_spawn_agent_tool_v1;
|
||||
use codex_tools::create_spawn_agent_tool_v2;
|
||||
use codex_tools::create_spawn_agents_on_csv_tool;
|
||||
use codex_tools::create_test_sync_tool;
|
||||
use codex_tools::create_tool_search_tool;
|
||||
use codex_tools::create_tool_suggest_tool;
|
||||
use codex_tools::create_view_image_tool;
|
||||
use codex_tools::create_wait_agent_tool_v1;
|
||||
use codex_tools::create_wait_agent_tool_v2;
|
||||
@@ -80,33 +81,17 @@ use codex_tools::dynamic_tool_to_responses_api_tool;
|
||||
use codex_tools::mcp_tool_to_responses_api_tool;
|
||||
use codex_tools::tool_spec_to_code_mode_tool_definition;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_template::Template;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
pub type JsonSchema = codex_tools::JsonSchema;
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) use codex_tools::mcp_call_tool_result_output_schema;
|
||||
|
||||
const TOOL_SEARCH_DESCRIPTION_TEMPLATE_SOURCE: &str =
|
||||
include_str!("../../templates/search_tool/tool_description.md");
|
||||
const TOOL_SEARCH_DESCRIPTION_TEMPLATE_KEY: &str = "app_descriptions";
|
||||
static TOOL_SEARCH_DESCRIPTION_TEMPLATE: LazyLock<Template> = LazyLock::new(|| {
|
||||
Template::parse(TOOL_SEARCH_DESCRIPTION_TEMPLATE_SOURCE)
|
||||
.unwrap_or_else(|err| panic!("tool_search description template must parse: {err}"))
|
||||
});
|
||||
const TOOL_SUGGEST_DESCRIPTION_TEMPLATE_SOURCE: &str =
|
||||
include_str!("../../templates/search_tool/tool_suggest_description.md");
|
||||
const TOOL_SUGGEST_DESCRIPTION_TEMPLATE_KEY: &str = "discoverable_tools";
|
||||
static TOOL_SUGGEST_DESCRIPTION_TEMPLATE: LazyLock<Template> = LazyLock::new(|| {
|
||||
Template::parse(TOOL_SUGGEST_DESCRIPTION_TEMPLATE_SOURCE)
|
||||
.unwrap_or_else(|err| panic!("tool_suggest description template must parse: {err}"))
|
||||
});
|
||||
const WEB_SEARCH_CONTENT_TYPES: [&str; 2] = ["text", "image"];
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
@@ -373,220 +358,6 @@ fn supports_image_generation(model_info: &ModelInfo) -> bool {
|
||||
model_info.input_modalities.contains(&InputModality::Image)
|
||||
}
|
||||
|
||||
fn create_tool_search_tool(app_tools: &HashMap<String, ToolInfo>) -> ToolSpec {
|
||||
let properties = BTreeMap::from([
|
||||
(
|
||||
"query".to_string(),
|
||||
JsonSchema::String {
|
||||
description: Some("Search query for apps tools.".to_string()),
|
||||
},
|
||||
),
|
||||
(
|
||||
"limit".to_string(),
|
||||
JsonSchema::Number {
|
||||
description: Some(format!(
|
||||
"Maximum number of tools to return (defaults to {TOOL_SEARCH_DEFAULT_LIMIT})."
|
||||
)),
|
||||
},
|
||||
),
|
||||
]);
|
||||
let mut app_descriptions = BTreeMap::new();
|
||||
for tool in app_tools.values() {
|
||||
if tool.server_name != CODEX_APPS_MCP_SERVER_NAME {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(connector_name) = tool
|
||||
.connector_name
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|connector_name| !connector_name.is_empty())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let connector_description = tool
|
||||
.connector_description
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|connector_description| !connector_description.is_empty())
|
||||
.map(str::to_string);
|
||||
|
||||
app_descriptions
|
||||
.entry(connector_name.to_string())
|
||||
.and_modify(|existing: &mut Option<String>| {
|
||||
if existing.is_none() {
|
||||
*existing = connector_description.clone();
|
||||
}
|
||||
})
|
||||
.or_insert(connector_description);
|
||||
}
|
||||
|
||||
let app_descriptions = if app_descriptions.is_empty() {
|
||||
"None currently enabled.".to_string()
|
||||
} else {
|
||||
app_descriptions
|
||||
.into_iter()
|
||||
.map(
|
||||
|(connector_name, connector_description)| match connector_description {
|
||||
Some(connector_description) => {
|
||||
format!("- {connector_name}: {connector_description}")
|
||||
}
|
||||
None => format!("- {connector_name}"),
|
||||
},
|
||||
)
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
};
|
||||
|
||||
let description = TOOL_SEARCH_DESCRIPTION_TEMPLATE
|
||||
.render([(
|
||||
TOOL_SEARCH_DESCRIPTION_TEMPLATE_KEY,
|
||||
app_descriptions.as_str(),
|
||||
)])
|
||||
.unwrap_or_else(|err| panic!("tool_search description template must render: {err}"));
|
||||
|
||||
ToolSpec::ToolSearch {
|
||||
execution: "client".to_string(),
|
||||
description,
|
||||
parameters: JsonSchema::Object {
|
||||
properties,
|
||||
required: Some(vec!["query".to_string()]),
|
||||
additional_properties: Some(false.into()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn create_tool_suggest_tool(discoverable_tools: &[DiscoverableTool]) -> ToolSpec {
|
||||
let discoverable_tool_ids = discoverable_tools
|
||||
.iter()
|
||||
.map(DiscoverableTool::id)
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
let properties = BTreeMap::from([
|
||||
(
|
||||
"tool_type".to_string(),
|
||||
JsonSchema::String {
|
||||
description: Some(
|
||||
"Type of discoverable tool to suggest. Use \"connector\" or \"plugin\"."
|
||||
.to_string(),
|
||||
),
|
||||
},
|
||||
),
|
||||
(
|
||||
"action_type".to_string(),
|
||||
JsonSchema::String {
|
||||
description: Some(
|
||||
"Suggested action for the tool. Use \"install\" or \"enable\".".to_string(),
|
||||
),
|
||||
},
|
||||
),
|
||||
(
|
||||
"tool_id".to_string(),
|
||||
JsonSchema::String {
|
||||
description: Some(format!(
|
||||
"Connector or plugin id to suggest. Must be one of: {discoverable_tool_ids}."
|
||||
)),
|
||||
},
|
||||
),
|
||||
(
|
||||
"suggest_reason".to_string(),
|
||||
JsonSchema::String {
|
||||
description: Some(
|
||||
"Concise one-line user-facing reason why this tool can help with the current request."
|
||||
.to_string(),
|
||||
),
|
||||
},
|
||||
),
|
||||
]);
|
||||
let discoverable_tools = format_discoverable_tools(discoverable_tools);
|
||||
let description = TOOL_SUGGEST_DESCRIPTION_TEMPLATE
|
||||
.render([(
|
||||
TOOL_SUGGEST_DESCRIPTION_TEMPLATE_KEY,
|
||||
discoverable_tools.as_str(),
|
||||
)])
|
||||
.unwrap_or_else(|err| panic!("tool_suggest description template must render: {err}"));
|
||||
|
||||
ToolSpec::Function(ResponsesApiTool {
|
||||
name: TOOL_SUGGEST_TOOL_NAME.to_string(),
|
||||
description,
|
||||
strict: false,
|
||||
defer_loading: None,
|
||||
parameters: JsonSchema::Object {
|
||||
properties,
|
||||
required: Some(vec![
|
||||
"tool_type".to_string(),
|
||||
"action_type".to_string(),
|
||||
"tool_id".to_string(),
|
||||
"suggest_reason".to_string(),
|
||||
]),
|
||||
additional_properties: Some(false.into()),
|
||||
},
|
||||
output_schema: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn format_discoverable_tools(discoverable_tools: &[DiscoverableTool]) -> String {
|
||||
let mut discoverable_tools = discoverable_tools.to_vec();
|
||||
discoverable_tools.sort_by(|left, right| {
|
||||
left.name()
|
||||
.cmp(right.name())
|
||||
.then_with(|| left.id().cmp(right.id()))
|
||||
});
|
||||
|
||||
discoverable_tools
|
||||
.into_iter()
|
||||
.map(|tool| {
|
||||
let description = tool
|
||||
.description()
|
||||
.filter(|description| !description.trim().is_empty())
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or_else(|| match &tool {
|
||||
DiscoverableTool::Connector(_) => "No description provided.".to_string(),
|
||||
DiscoverableTool::Plugin(plugin) => format_plugin_summary(plugin.as_ref()),
|
||||
});
|
||||
let default_action = match tool.tool_type() {
|
||||
DiscoverableToolType::Connector => DiscoverableToolAction::Install,
|
||||
DiscoverableToolType::Plugin => DiscoverableToolAction::Install,
|
||||
};
|
||||
format!(
|
||||
"- {} (id: `{}`, type: {}, action: {}): {}",
|
||||
tool.name(),
|
||||
tool.id(),
|
||||
tool.tool_type().as_str(),
|
||||
default_action.as_str(),
|
||||
description
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
fn format_plugin_summary(plugin: &DiscoverablePluginInfo) -> String {
|
||||
let mut details = Vec::new();
|
||||
if plugin.has_skills {
|
||||
details.push("skills".to_string());
|
||||
}
|
||||
if !plugin.mcp_server_names.is_empty() {
|
||||
details.push(format!(
|
||||
"MCP servers: {}",
|
||||
plugin.mcp_server_names.join(", ")
|
||||
));
|
||||
}
|
||||
if !plugin.app_connector_ids.is_empty() {
|
||||
details.push(format!(
|
||||
"app connectors: {}",
|
||||
plugin.app_connector_ids.join(", ")
|
||||
));
|
||||
}
|
||||
|
||||
if details.is_empty() {
|
||||
"No description provided.".to_string()
|
||||
} else {
|
||||
details.join("; ")
|
||||
}
|
||||
}
|
||||
|
||||
/// TODO(dylan): deprecate once we get rid of json tool
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub(crate) struct ApplyPatchToolArgs {
|
||||
@@ -861,7 +632,10 @@ pub(crate) fn build_specs_with_discoverable_tools(
|
||||
let search_tool_handler = Arc::new(ToolSearchHandler::new(app_tools.clone()));
|
||||
push_tool_spec(
|
||||
&mut builder,
|
||||
create_tool_search_tool(&app_tools),
|
||||
create_tool_search_tool(
|
||||
&tool_search_app_infos(&app_tools),
|
||||
TOOL_SEARCH_DEFAULT_LIMIT,
|
||||
),
|
||||
/*supports_parallel_tool_calls*/ true,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
@@ -881,7 +655,7 @@ pub(crate) fn build_specs_with_discoverable_tools(
|
||||
.filter(|tools| !tools.is_empty())
|
||||
{
|
||||
builder.push_spec_with_parallel_support(
|
||||
create_tool_suggest_tool(discoverable_tools),
|
||||
create_tool_suggest_tool(&tool_suggest_entries(discoverable_tools)),
|
||||
/*supports_parallel_tool_calls*/ true,
|
||||
);
|
||||
builder.register_handler(TOOL_SUGGEST_TOOL_NAME, tool_suggest_handler);
|
||||
@@ -1167,6 +941,54 @@ pub(crate) fn build_specs_with_discoverable_tools(
|
||||
builder
|
||||
}
|
||||
|
||||
fn tool_search_app_infos(app_tools: &HashMap<String, ToolInfo>) -> Vec<ToolSearchAppInfo> {
|
||||
app_tools
|
||||
.values()
|
||||
.filter(|tool| tool.server_name == CODEX_APPS_MCP_SERVER_NAME)
|
||||
.filter_map(|tool| {
|
||||
let name = tool
|
||||
.connector_name
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|connector_name| !connector_name.is_empty())?
|
||||
.to_string();
|
||||
let description = tool
|
||||
.connector_description
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|connector_description| !connector_description.is_empty())
|
||||
.map(str::to_string);
|
||||
Some(ToolSearchAppInfo { name, description })
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn tool_suggest_entries(discoverable_tools: &[DiscoverableTool]) -> Vec<ToolSuggestEntry> {
|
||||
discoverable_tools
|
||||
.iter()
|
||||
.map(|tool| match tool {
|
||||
DiscoverableTool::Connector(connector) => ToolSuggestEntry {
|
||||
id: connector.id.clone(),
|
||||
name: connector.name.clone(),
|
||||
description: connector.description.clone(),
|
||||
tool_type: DiscoverableToolType::Connector,
|
||||
has_skills: false,
|
||||
mcp_server_names: Vec::new(),
|
||||
app_connector_ids: Vec::new(),
|
||||
},
|
||||
DiscoverableTool::Plugin(plugin) => ToolSuggestEntry {
|
||||
id: plugin.id.clone(),
|
||||
name: plugin.name.clone(),
|
||||
description: plugin.description.clone(),
|
||||
tool_type: DiscoverableToolType::Plugin,
|
||||
has_skills: plugin.has_skills,
|
||||
mcp_server_names: plugin.mcp_server_names.clone(),
|
||||
app_connector_ids: plugin.app_connector_ids.clone(),
|
||||
},
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "spec_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::models_manager::model_info::with_config_overrides;
|
||||
use crate::shell::Shell;
|
||||
use crate::shell::ShellType;
|
||||
use crate::tools::ToolRouter;
|
||||
use crate::tools::discoverable::DiscoverablePluginInfo;
|
||||
use crate::tools::router::ToolRouterParams;
|
||||
use codex_app_server_protocol::AppInfo;
|
||||
use codex_protocol::models::VIEW_IMAGE_TOOL_NAME;
|
||||
@@ -14,6 +15,7 @@ use codex_tools::AdditionalProperties;
|
||||
use codex_tools::CommandToolOptions;
|
||||
use codex_tools::ConfiguredToolSpec;
|
||||
use codex_tools::FreeformTool;
|
||||
use codex_tools::ResponsesApiTool;
|
||||
use codex_tools::ResponsesApiWebSearchFilters;
|
||||
use codex_tools::ResponsesApiWebSearchUserLocation;
|
||||
use codex_tools::SpawnAgentToolOptions;
|
||||
|
||||
Reference in New Issue
Block a user