Compare commits

...

5 Commits

Author SHA1 Message Date
jif-oai
f247d7e7fd fixes 2026-05-12 14:15:50 +01:00
jif-oai
8667f50b42 fmt 2026-05-12 14:11:49 +01:00
jif-oai
1199207c89 fmt 2026-05-12 13:59:18 +01:00
jif-oai
6adb23b821 feat: tools 2 2026-05-12 13:55:41 +01:00
jif-oai
074b7692d8 feat: tools 1 2026-05-12 13:09:51 +01:00
79 changed files with 1502 additions and 1261 deletions

2
codex-rs/Cargo.lock generated
View File

@@ -3689,6 +3689,7 @@ dependencies = [
name = "codex-tool-api"
version = "0.0.0"
dependencies = [
"codex-protocol",
"pretty_assertions",
"serde_json",
"thiserror 2.0.18",
@@ -3702,6 +3703,7 @@ dependencies = [
"codex-code-mode",
"codex-features",
"codex-protocol",
"codex-tool-api",
"codex-utils-absolute-path",
"codex-utils-pty",
"pretty_assertions",

View File

@@ -544,7 +544,7 @@ fn test_tool_runtime(session: Arc<Session>, turn_context: Arc<TurnContext>) -> T
deferred_mcp_tools: None,
unavailable_called_tools: Vec::new(),
discoverable_tools: None,
extension_tool_bundles: Vec::new(),
extension_tool_definitions: Vec::new(),
dynamic_tools: turn_context.dynamic_tools.as_slice(),
},
));
@@ -8613,7 +8613,7 @@ async fn fatal_tool_error_stops_turn_and_reports_error() {
mcp_tools: Some(tools),
unavailable_called_tools: Vec::new(),
discoverable_tools: None,
extension_tool_bundles: Vec::new(),
extension_tool_definitions: Vec::new(),
dynamic_tools: turn_context.dynamic_tools.as_slice(),
},
);
@@ -9118,7 +9118,7 @@ async fn unified_exec_rejects_escalated_permissions_when_policy_not_on_request()
let turn_context = Arc::new(turn_context_raw);
let tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new()));
let handler = ExecCommandHandler::default();
let handler = ExecCommandHandler;
let resp = handler
.handle(ToolInvocation {
session: Arc::clone(&session),

View File

@@ -498,7 +498,7 @@ async fn guardian_allows_unified_exec_additional_permissions_requests_past_polic
let turn_context = Arc::new(turn_context_raw);
let tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new()));
let handler = ExecCommandHandler::default();
let handler = ExecCommandHandler;
let resp = handler
.handle(ToolInvocation {
session: Arc::clone(&session),

View File

@@ -55,7 +55,7 @@ use crate::tools::context::SharedTurnDiffTracker;
use crate::tools::parallel::ToolCallRuntime;
use crate::tools::registry::ToolArgumentDiffConsumer;
use crate::tools::router::ToolRouterParams;
use crate::tools::router::extension_tool_bundles;
use crate::tools::router::extension_tool_definitions;
use crate::turn_diff_tracker::TurnDiffTracker;
use crate::turn_timing::record_turn_ttft_metric;
use crate::unavailable_tool::collect_unavailable_called_tools;
@@ -1267,7 +1267,7 @@ pub(crate) async fn built_tools(
deferred_mcp_tools,
unavailable_called_tools,
discoverable_tools,
extension_tool_bundles: extension_tool_bundles(sess),
extension_tool_definitions: extension_tool_definitions(sess),
dynamic_tools: turn_context.dynamic_tools.as_slice(),
},
)))

View File

@@ -3,22 +3,37 @@ use crate::tools::context::FunctionToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::registry::ToolHandler;
use crate::tools::runtime_definition::RuntimeToolDefinition;
use crate::tools::runtime_definition::runtime_tool_definition;
use codex_code_mode::ToolDefinition as CodeModeToolDefinition;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use std::collections::BTreeMap;
use super::ExecContext;
use super::PUBLIC_TOOL_NAME;
use super::build_enabled_tools;
use super::execute_spec::create_code_mode_tool;
use super::handle_runtime_response;
use super::is_exec_tool_name;
pub struct CodeModeExecuteHandler {
spec: ToolSpec,
}
pub struct CodeModeExecuteHandler;
impl CodeModeExecuteHandler {
pub(crate) fn new(spec: ToolSpec) -> Self {
Self { spec }
pub(crate) fn definition(
enabled_tools: &[CodeModeToolDefinition],
namespace_descriptions: &BTreeMap<String, codex_code_mode::ToolNamespaceDescription>,
code_mode_only: bool,
deferred_tools_available: bool,
) -> RuntimeToolDefinition {
runtime_tool_definition(
Self,
create_code_mode_tool(
enabled_tools,
namespace_descriptions,
code_mode_only,
deferred_tools_available,
),
)
}
async fn execute(
@@ -89,10 +104,6 @@ impl ToolHandler for CodeModeExecuteHandler {
ToolName::plain(PUBLIC_TOOL_NAME)
}
fn spec(&self) -> Option<ToolSpec> {
Some(self.spec.clone())
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Custom { .. })
}

View File

@@ -29,7 +29,7 @@ use crate::tools::parallel::ToolCallRuntime;
use crate::tools::router::ToolCall;
use crate::tools::router::ToolCallSource;
use crate::tools::router::ToolRouterParams;
use crate::tools::router::extension_tool_bundles;
use crate::tools::router::extension_tool_definitions;
use crate::unified_exec::resolve_max_tokens;
use codex_features::Feature;
use codex_tools::ToolName;
@@ -284,7 +284,7 @@ async fn build_nested_router(exec: &ExecContext) -> ToolRouter {
mcp_tools: Some(listed_mcp_tools),
unavailable_called_tools: Vec::new(),
discoverable_tools: None,
extension_tool_bundles: extension_tool_bundles(exec.session.as_ref()),
extension_tool_definitions: extension_tool_definitions(exec.session.as_ref()),
dynamic_tools: exec.turn.dynamic_tools.as_slice(),
},
)

View File

@@ -5,17 +5,24 @@ use crate::tools::context::FunctionToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::registry::ToolHandler;
use crate::tools::runtime_definition::RuntimeToolDefinition;
use crate::tools::runtime_definition::runtime_tool_definition;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use super::DEFAULT_WAIT_YIELD_TIME_MS;
use super::ExecContext;
use super::WAIT_TOOL_NAME;
use super::handle_runtime_response;
use super::wait_spec::create_wait_tool;
use super::handle_runtime_response;
pub struct CodeModeWaitHandler;
impl CodeModeWaitHandler {
pub(crate) fn definition() -> RuntimeToolDefinition {
runtime_tool_definition(Self, create_wait_tool())
}
}
#[derive(Debug, Deserialize)]
struct ExecWaitArgs {
cell_id: String,
@@ -47,10 +54,6 @@ impl ToolHandler for CodeModeWaitHandler {
ToolName::plain(WAIT_TOOL_NAME)
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_wait_tool())
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let ToolInvocation {
session,

View File

@@ -4,13 +4,20 @@ use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::agent_jobs_spec::create_report_agent_job_result_tool;
use crate::tools::registry::ToolHandler;
use crate::tools::runtime_definition::RuntimeToolDefinition;
use crate::tools::runtime_definition::runtime_tool_definition;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use super::*;
pub struct ReportAgentJobResultHandler;
impl ReportAgentJobResultHandler {
pub(crate) fn definition() -> RuntimeToolDefinition {
runtime_tool_definition(Self, create_report_agent_job_result_tool())
}
}
impl ToolHandler for ReportAgentJobResultHandler {
type Output = FunctionToolOutput;
@@ -18,10 +25,6 @@ impl ToolHandler for ReportAgentJobResultHandler {
ToolName::plain("report_agent_job_result")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_report_agent_job_result_tool())
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}

View File

@@ -4,13 +4,20 @@ use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::agent_jobs_spec::create_spawn_agents_on_csv_tool;
use crate::tools::registry::ToolHandler;
use crate::tools::runtime_definition::RuntimeToolDefinition;
use crate::tools::runtime_definition::runtime_tool_definition;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use super::*;
pub struct SpawnAgentsOnCsvHandler;
impl SpawnAgentsOnCsvHandler {
pub(crate) fn definition() -> RuntimeToolDefinition {
runtime_tool_definition(Self, create_spawn_agents_on_csv_tool())
}
}
impl ToolHandler for SpawnAgentsOnCsvHandler {
type Output = FunctionToolOutput;
@@ -18,10 +25,6 @@ impl ToolHandler for SpawnAgentsOnCsvHandler {
ToolName::plain("spawn_agents_on_csv")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_spawn_agents_on_csv_tool())
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}

View File

@@ -31,6 +31,8 @@ use crate::tools::registry::PostToolUsePayload;
use crate::tools::registry::PreToolUsePayload;
use crate::tools::registry::ToolArgumentDiffConsumer;
use crate::tools::registry::ToolHandler;
use crate::tools::runtime_definition::RuntimeToolDefinition;
use crate::tools::runtime_definition::runtime_tool_definition;
use crate::tools::runtimes::apply_patch::ApplyPatchRequest;
use crate::tools::runtimes::apply_patch::ApplyPatchRuntime;
use crate::tools::sandboxing::ToolCtx;
@@ -49,7 +51,6 @@ use codex_sandboxing::policy_transforms::effective_file_system_sandbox_policy;
use codex_sandboxing::policy_transforms::merge_permission_profiles;
use codex_sandboxing::policy_transforms::normalize_additional_permissions;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use codex_utils_absolute_path::AbsolutePathBuf;
const APPLY_PATCH_ARGUMENT_DIFF_BUFFER_INTERVAL: Duration = Duration::from_millis(500);
@@ -64,6 +65,13 @@ impl ApplyPatchHandler {
pub(crate) fn new(multi_environment: bool) -> Self {
Self { multi_environment }
}
pub(crate) fn definition(multi_environment: bool) -> RuntimeToolDefinition {
runtime_tool_definition(
Self::new(multi_environment),
create_apply_patch_freeform_tool(multi_environment),
)
}
}
#[derive(Default)]
@@ -303,10 +311,6 @@ impl ToolHandler for ApplyPatchHandler {
ToolName::plain("apply_patch")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_apply_patch_freeform_tool(self.multi_environment))
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Custom { .. })
}

View File

@@ -1,12 +1,12 @@
use codex_protocol::models::FunctionCallOutputBody;
use codex_protocol::models::FunctionCallOutputPayload;
use codex_protocol::models::ResponseInputItem;
use codex_tool_api::ToolBundle as ExtensionToolBundle;
use codex_tool_api::ToolDefinition;
use codex_tool_api::ToolError as ExtensionToolError;
use codex_tools::ResponsesApiTool;
use codex_tool_api::ToolExecutor;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use serde_json::Value;
use std::sync::Arc;
use crate::function_tool::FunctionCallError;
use crate::tools::context::ToolInvocation;
@@ -18,11 +18,11 @@ use crate::tools::registry::PostToolUsePayload;
use crate::tools::registry::PreToolUsePayload;
use crate::tools::registry::ToolHandler;
pub(crate) struct BundledToolOutput {
pub(crate) struct ExtensionToolOutput {
value: Value,
}
impl ToolOutput for BundledToolOutput {
impl ToolOutput for ExtensionToolOutput {
fn log_preview(&self) -> String {
self.value.to_string()
}
@@ -50,14 +50,13 @@ impl ToolOutput for BundledToolOutput {
}
}
pub(crate) struct BundledToolHandler {
bundle: ExtensionToolBundle,
spec: ToolSpec,
pub(crate) struct ExtensionToolHandler {
definition: ToolDefinition<Arc<dyn ToolExecutor>>,
}
impl BundledToolHandler {
pub(crate) fn new(bundle: ExtensionToolBundle, spec: ToolSpec) -> Self {
Self { bundle, spec }
impl ExtensionToolHandler {
pub(crate) fn new(definition: ToolDefinition<Arc<dyn ToolExecutor>>) -> Self {
Self { definition }
}
fn arguments_from_payload<'a>(&self, payload: &'a ToolPayload) -> Option<&'a str> {
@@ -68,15 +67,11 @@ impl BundledToolHandler {
}
}
impl ToolHandler for BundledToolHandler {
type Output = BundledToolOutput;
impl ToolHandler for ExtensionToolHandler {
type Output = ExtensionToolOutput;
fn tool_name(&self) -> ToolName {
ToolName::plain(self.bundle.tool_name())
}
fn spec(&self) -> Option<ToolSpec> {
Some(self.spec.clone())
self.definition.tool_name().clone()
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
@@ -116,36 +111,23 @@ impl ToolHandler for BundledToolHandler {
.ok_or_else(|| {
FunctionCallError::Fatal(format!(
"tool {} invoked with incompatible payload",
self.bundle.tool_name()
self.definition.tool_name()
))
})?
.to_string();
let value = self
.bundle
.executor()
.definition
.runtime()
.execute(codex_tool_api::ToolCall {
call_id: invocation.call_id,
arguments,
})
.await
.map_err(map_extension_tool_error)?;
Ok(BundledToolOutput { value })
Ok(ExtensionToolOutput { value })
}
}
pub(crate) fn extension_tool_spec(
spec: &codex_tool_api::FunctionToolSpec,
) -> Result<ToolSpec, serde_json::Error> {
Ok(ToolSpec::Function(ResponsesApiTool {
name: spec.name.clone(),
description: spec.description.clone(),
strict: spec.strict,
defer_loading: None,
parameters: codex_tools::parse_tool_input_schema(&spec.parameters)?,
output_schema: None,
}))
}
fn map_extension_tool_error(error: ExtensionToolError) -> FunctionCallError {
match error {
ExtensionToolError::RespondToModel(message) => FunctionCallError::RespondToModel(message),
@@ -168,9 +150,8 @@ mod tests {
use pretty_assertions::assert_eq;
use serde_json::json;
use super::BundledToolHandler;
use super::BundledToolOutput;
use super::extension_tool_spec;
use super::ExtensionToolHandler;
use super::ExtensionToolOutput;
use crate::tools::context::ToolCallSource;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
@@ -190,7 +171,7 @@ mod tests {
#[tokio::test]
async fn exposes_generic_hook_payloads_and_is_conservatively_mutating() {
let bundle = codex_tool_api::ToolBundle::new(
let definition = codex_tool_api::ToolDefinition::from_function_spec(
codex_tool_api::FunctionToolSpec {
name: "extension_echo".to_string(),
description: "Echoes arguments.".to_string(),
@@ -204,10 +185,9 @@ mod tests {
"additionalProperties": false,
}),
},
Arc::new(StubExtensionExecutor),
Arc::new(StubExtensionExecutor) as Arc<dyn codex_tool_api::ToolExecutor>,
);
let spec = extension_tool_spec(bundle.spec()).expect("extension spec should convert");
let handler = BundledToolHandler::new(bundle, spec);
let handler = ExtensionToolHandler::new(definition);
let (session, turn) = crate::session::tests::make_session_and_context().await;
let invocation = ToolInvocation {
session: session.into(),
@@ -221,7 +201,7 @@ mod tests {
arguments: json!({ "message": "hello" }).to_string(),
},
};
let output = BundledToolOutput {
let output = ExtensionToolOutput {
value: json!({ "ok": true }),
};

View File

@@ -3,12 +3,13 @@ use crate::goals::CreateGoalRequest;
use crate::tools::context::FunctionToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::goal_spec::CREATE_GOAL_TOOL_NAME;
use crate::tools::handlers::goal_spec::create_create_goal_tool;
use crate::tools::handlers::goal_spec::CREATE_GOAL_TOOL_NAME;
use crate::tools::handlers::parse_arguments;
use crate::tools::registry::ToolHandler;
use crate::tools::runtime_definition::RuntimeToolDefinition;
use crate::tools::runtime_definition::runtime_tool_definition;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use super::CompletionBudgetReport;
use super::CreateGoalArgs;
@@ -17,6 +18,12 @@ use super::goal_response;
pub struct CreateGoalHandler;
impl CreateGoalHandler {
pub(crate) fn definition() -> RuntimeToolDefinition {
runtime_tool_definition(Self, create_create_goal_tool())
}
}
impl ToolHandler for CreateGoalHandler {
type Output = FunctionToolOutput;
@@ -24,10 +31,6 @@ impl ToolHandler for CreateGoalHandler {
ToolName::plain(CREATE_GOAL_TOOL_NAME)
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_create_goal_tool())
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let ToolInvocation {
session,

View File

@@ -2,11 +2,12 @@ use crate::function_tool::FunctionCallError;
use crate::tools::context::FunctionToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::goal_spec::GET_GOAL_TOOL_NAME;
use crate::tools::handlers::goal_spec::create_get_goal_tool;
use crate::tools::handlers::goal_spec::GET_GOAL_TOOL_NAME;
use crate::tools::registry::ToolHandler;
use crate::tools::runtime_definition::RuntimeToolDefinition;
use crate::tools::runtime_definition::runtime_tool_definition;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use super::CompletionBudgetReport;
use super::format_goal_error;
@@ -14,6 +15,12 @@ use super::goal_response;
pub struct GetGoalHandler;
impl GetGoalHandler {
pub(crate) fn definition() -> RuntimeToolDefinition {
runtime_tool_definition(Self, create_get_goal_tool())
}
}
impl ToolHandler for GetGoalHandler {
type Output = FunctionToolOutput;
@@ -21,10 +28,6 @@ impl ToolHandler for GetGoalHandler {
ToolName::plain(GET_GOAL_TOOL_NAME)
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_get_goal_tool())
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let ToolInvocation {
session, payload, ..

View File

@@ -4,13 +4,14 @@ use crate::goals::SetGoalRequest;
use crate::tools::context::FunctionToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::goal_spec::UPDATE_GOAL_TOOL_NAME;
use crate::tools::handlers::goal_spec::create_update_goal_tool;
use crate::tools::handlers::goal_spec::UPDATE_GOAL_TOOL_NAME;
use crate::tools::handlers::parse_arguments;
use crate::tools::registry::ToolHandler;
use crate::tools::runtime_definition::RuntimeToolDefinition;
use crate::tools::runtime_definition::runtime_tool_definition;
use codex_protocol::protocol::ThreadGoalStatus;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use super::CompletionBudgetReport;
use super::UpdateGoalArgs;
@@ -19,6 +20,12 @@ use super::goal_response;
pub struct UpdateGoalHandler;
impl UpdateGoalHandler {
pub(crate) fn definition() -> RuntimeToolDefinition {
runtime_tool_definition(Self, create_update_goal_tool())
}
}
impl ToolHandler for UpdateGoalHandler {
type Output = FunctionToolOutput;
@@ -26,10 +33,6 @@ impl ToolHandler for UpdateGoalHandler {
ToolName::plain(UPDATE_GOAL_TOOL_NAME)
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_update_goal_tool())
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let ToolInvocation {
session,

View File

@@ -6,10 +6,11 @@ use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::mcp_resource_spec::create_list_mcp_resource_templates_tool;
use crate::tools::registry::ToolHandler;
use crate::tools::runtime_definition::RuntimeToolDefinition;
use crate::tools::runtime_definition::runtime_tool_definition;
use codex_protocol::models::function_call_output_content_items_to_text;
use codex_protocol::protocol::McpInvocation;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use rmcp::model::PaginatedRequestParams;
@@ -25,6 +26,12 @@ use super::serialize_function_output;
pub struct ListMcpResourceTemplatesHandler;
impl ListMcpResourceTemplatesHandler {
pub(crate) fn definition() -> RuntimeToolDefinition {
runtime_tool_definition(Self, create_list_mcp_resource_templates_tool())
}
}
impl ToolHandler for ListMcpResourceTemplatesHandler {
type Output = FunctionToolOutput;
@@ -32,10 +39,6 @@ impl ToolHandler for ListMcpResourceTemplatesHandler {
ToolName::plain("list_mcp_resource_templates")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_list_mcp_resource_templates_tool())
}
fn supports_parallel_tool_calls(&self) -> bool {
true
}

View File

@@ -6,10 +6,11 @@ use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::mcp_resource_spec::create_list_mcp_resources_tool;
use crate::tools::registry::ToolHandler;
use crate::tools::runtime_definition::RuntimeToolDefinition;
use crate::tools::runtime_definition::runtime_tool_definition;
use codex_protocol::models::function_call_output_content_items_to_text;
use codex_protocol::protocol::McpInvocation;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use rmcp::model::PaginatedRequestParams;
@@ -25,6 +26,12 @@ use super::serialize_function_output;
pub struct ListMcpResourcesHandler;
impl ListMcpResourcesHandler {
pub(crate) fn definition() -> RuntimeToolDefinition {
runtime_tool_definition(Self, create_list_mcp_resources_tool())
}
}
impl ToolHandler for ListMcpResourcesHandler {
type Output = FunctionToolOutput;
@@ -32,10 +39,6 @@ impl ToolHandler for ListMcpResourcesHandler {
ToolName::plain("list_mcp_resources")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_list_mcp_resources_tool())
}
fn supports_parallel_tool_calls(&self) -> bool {
true
}

View File

@@ -6,10 +6,11 @@ use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::mcp_resource_spec::create_read_mcp_resource_tool;
use crate::tools::registry::ToolHandler;
use crate::tools::runtime_definition::RuntimeToolDefinition;
use crate::tools::runtime_definition::runtime_tool_definition;
use codex_protocol::models::function_call_output_content_items_to_text;
use codex_protocol::protocol::McpInvocation;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use rmcp::model::ReadResourceRequestParams;
@@ -25,6 +26,12 @@ use super::serialize_function_output;
pub struct ReadMcpResourceHandler;
impl ReadMcpResourceHandler {
pub(crate) fn definition() -> RuntimeToolDefinition {
runtime_tool_definition(Self, create_read_mcp_resource_tool())
}
}
impl ToolHandler for ReadMcpResourceHandler {
type Output = FunctionToolOutput;
@@ -32,10 +39,6 @@ impl ToolHandler for ReadMcpResourceHandler {
ToolName::plain("read_mcp_resource")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_read_mcp_resource_tool())
}
fn supports_parallel_tool_calls(&self) -> bool {
true
}

View File

@@ -66,14 +66,11 @@ pub use request_user_input::RequestUserInputHandler;
pub use shell::ContainerExecHandler;
pub use shell::LocalShellHandler;
pub use shell::ShellCommandHandler;
pub(crate) use shell::ShellCommandHandlerOptions;
pub use shell::ShellHandler;
pub use test_sync::TestSyncHandler;
pub use tool_search::ToolSearchHandler;
pub use unavailable_tool::UnavailableToolHandler;
pub(crate) use unavailable_tool::unavailable_tool_message;
pub use unified_exec::ExecCommandHandler;
pub(crate) use unified_exec::ExecCommandHandlerOptions;
pub use unified_exec::WriteStdinHandler;
pub use view_image::ViewImageHandler;

View File

@@ -14,8 +14,17 @@ use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput;
use crate::tools::context::ToolPayload;
pub(crate) use crate::tools::handlers::multi_agents_common::*;
use crate::tools::handlers::multi_agents_spec::SpawnAgentToolOptions;
use crate::tools::handlers::multi_agents_spec::WaitAgentTimeoutOptions;
use crate::tools::handlers::multi_agents_spec::create_close_agent_tool_v1;
use crate::tools::handlers::multi_agents_spec::create_resume_agent_tool;
use crate::tools::handlers::multi_agents_spec::create_send_input_tool_v1;
use crate::tools::handlers::multi_agents_spec::create_spawn_agent_tool_v1;
use crate::tools::handlers::multi_agents_spec::create_wait_agent_tool_v1;
use crate::tools::handlers::parse_arguments;
use crate::tools::registry::ToolHandler;
use crate::tools::runtime_definition::RuntimeToolDefinition;
use crate::tools::runtime_definition::runtime_tool_definition;
use codex_protocol::ThreadId;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::openai_models::ReasoningEffort;

View File

@@ -1,10 +1,14 @@
use super::*;
use crate::tools::handlers::multi_agents_spec::create_close_agent_tool_v1;
use crate::turn_timing::now_unix_timestamp_ms;
use codex_tools::ToolSpec;
pub(crate) struct Handler;
impl Handler {
pub(crate) fn definition() -> RuntimeToolDefinition {
runtime_tool_definition(Self, create_close_agent_tool_v1())
}
}
impl ToolHandler for Handler {
type Output = CloseAgentResult;
@@ -12,10 +16,6 @@ impl ToolHandler for Handler {
ToolName::plain("close_agent")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_close_agent_tool_v1())
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}

View File

@@ -1,12 +1,16 @@
use super::*;
use crate::agent::next_thread_spawn_depth;
use crate::tools::handlers::multi_agents_spec::create_resume_agent_tool;
use crate::turn_timing::now_unix_timestamp_ms;
use codex_tools::ToolSpec;
use std::sync::Arc;
pub(crate) struct Handler;
impl Handler {
pub(crate) fn definition() -> RuntimeToolDefinition {
runtime_tool_definition(Self, create_resume_agent_tool())
}
}
impl ToolHandler for Handler {
type Output = ResumeAgentResult;
@@ -14,10 +18,6 @@ impl ToolHandler for Handler {
ToolName::plain("resume_agent")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_resume_agent_tool())
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}

View File

@@ -1,11 +1,15 @@
use super::*;
use crate::agent::control::render_input_preview;
use crate::tools::handlers::multi_agents_spec::create_send_input_tool_v1;
use crate::turn_timing::now_unix_timestamp_ms;
use codex_tools::ToolSpec;
pub(crate) struct Handler;
impl Handler {
pub(crate) fn definition() -> RuntimeToolDefinition {
runtime_tool_definition(Self, create_send_input_tool_v1())
}
}
impl ToolHandler for Handler {
type Output = SendInputResult;
@@ -13,10 +17,6 @@ impl ToolHandler for Handler {
ToolName::plain("send_input")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_send_input_tool_v1())
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}

View File

@@ -6,19 +6,14 @@ use crate::agent::exceeds_thread_spawn_depth_limit;
use crate::agent::next_thread_spawn_depth;
use crate::agent::role::DEFAULT_ROLE_NAME;
use crate::agent::role::apply_role_to_config;
use crate::tools::handlers::multi_agents_spec::SpawnAgentToolOptions;
use crate::tools::handlers::multi_agents_spec::create_spawn_agent_tool_v1;
use crate::turn_timing::now_unix_timestamp_ms;
use codex_tools::ToolSpec;
#[derive(Default)]
pub(crate) struct Handler {
options: SpawnAgentToolOptions,
}
pub(crate) struct Handler;
impl Handler {
pub(crate) fn new(options: SpawnAgentToolOptions) -> Self {
Self { options }
pub(crate) fn definition(options: SpawnAgentToolOptions) -> RuntimeToolDefinition {
runtime_tool_definition(Self, create_spawn_agent_tool_v1(options))
}
}
@@ -29,10 +24,6 @@ impl ToolHandler for Handler {
ToolName::plain("spawn_agent")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_spawn_agent_tool_v1(self.options.clone()))
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}

View File

@@ -1,10 +1,7 @@
use super::*;
use crate::agent::status::is_final;
use crate::tools::handlers::multi_agents_spec::WaitAgentTimeoutOptions;
use crate::tools::handlers::multi_agents_spec::create_wait_agent_tool_v1;
use crate::turn_timing::now_unix_timestamp_ms;
use codex_protocol::error::CodexErr;
use codex_tools::ToolSpec;
use futures::FutureExt;
use futures::StreamExt;
use futures::stream::FuturesUnordered;
@@ -17,13 +14,11 @@ use tokio::time::Instant;
use tokio::time::timeout_at;
#[derive(Default)]
pub(crate) struct Handler {
options: WaitAgentTimeoutOptions,
}
pub(crate) struct Handler;
impl Handler {
pub(crate) fn new(options: WaitAgentTimeoutOptions) -> Self {
Self { options }
pub(crate) fn definition(options: WaitAgentTimeoutOptions) -> RuntimeToolDefinition {
runtime_tool_definition(Self, create_wait_agent_tool_v1(options))
}
}
@@ -34,10 +29,6 @@ impl ToolHandler for Handler {
ToolName::plain("wait_agent")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_wait_agent_tool_v1(self.options))
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}

View File

@@ -181,7 +181,7 @@ async fn handler_rejects_non_function_payloads() {
input: "hello".to_string(),
},
);
let Err(err) = SpawnAgentHandler::default().handle(invocation).await else {
let Err(err) = SpawnAgentHandler.handle(invocation).await else {
panic!("payload should be rejected");
};
assert_eq!(
@@ -201,7 +201,7 @@ async fn spawn_agent_rejects_empty_message() {
"spawn_agent",
function_payload(json!({"message": " "})),
);
let Err(err) = SpawnAgentHandler::default().handle(invocation).await else {
let Err(err) = SpawnAgentHandler.handle(invocation).await else {
panic!("empty message should be rejected");
};
assert_eq!(
@@ -222,7 +222,7 @@ async fn spawn_agent_rejects_when_message_and_items_are_both_set() {
"items": [{"type": "mention", "name": "drive", "path": "app://drive"}]
})),
);
let Err(err) = SpawnAgentHandler::default().handle(invocation).await else {
let Err(err) = SpawnAgentHandler.handle(invocation).await else {
panic!("message+items should be rejected");
};
assert_eq!(
@@ -269,7 +269,7 @@ async fn spawn_agent_uses_explorer_role_and_preserves_approval_policy() {
"agent_type": "explorer"
})),
);
let output = SpawnAgentHandler::default()
let output = SpawnAgentHandler
.handle(invocation)
.await
.expect("spawn_agent should succeed");
@@ -304,7 +304,7 @@ async fn spawn_agent_fork_context_rejects_agent_type_override() {
.expect("root thread should start");
session.services.agent_control = manager.agent_control();
session.conversation_id = root.thread_id;
let err = SpawnAgentHandler::default()
let err = SpawnAgentHandler
.handle(invocation(
Arc::new(session),
Arc::new(turn),
@@ -337,7 +337,7 @@ async fn spawn_agent_fork_context_rejects_child_model_overrides() {
session.services.agent_control = manager.agent_control();
session.conversation_id = root.thread_id;
let err = SpawnAgentHandler::default()
let err = SpawnAgentHandler
.handle(invocation(
Arc::new(session),
Arc::new(turn),
@@ -381,7 +381,7 @@ async fn multi_agent_v2_spawn_fork_turns_all_rejects_agent_type_override() {
..turn
};
let err = SpawnAgentHandlerV2::default()
let err = SpawnAgentHandlerV2
.handle(invocation(
Arc::new(session),
Arc::new(turn),
@@ -421,7 +421,7 @@ async fn multi_agent_v2_spawn_defaults_to_full_fork_and_rejects_child_model_over
.expect("test config should allow feature update");
turn.config = Arc::new(config);
let err = SpawnAgentHandlerV2::default()
let err = SpawnAgentHandlerV2
.handle(invocation(
Arc::new(session),
Arc::new(turn),
@@ -465,7 +465,7 @@ async fn multi_agent_v2_spawn_partial_fork_turns_allows_agent_type_override() {
..turn
};
let output = SpawnAgentHandlerV2::default()
let output = SpawnAgentHandlerV2
.handle(invocation(
Arc::new(session),
Arc::new(turn),
@@ -507,7 +507,7 @@ async fn spawn_agent_returns_agent_id_without_task_name() {
let manager = thread_manager();
session.services.agent_control = manager.agent_control();
let output = SpawnAgentHandler::default()
let output = SpawnAgentHandler
.handle(invocation(
Arc::new(session),
Arc::new(turn),
@@ -553,7 +553,7 @@ async fn multi_agent_v2_spawn_requires_task_name() {
"message": "inspect this repo"
})),
);
let Err(err) = SpawnAgentHandlerV2::default().handle(invocation).await else {
let Err(err) = SpawnAgentHandlerV2.handle(invocation).await else {
panic!("missing task_name should be rejected");
};
let FunctionCallError::RespondToModel(message) = err else {
@@ -589,7 +589,7 @@ async fn multi_agent_v2_spawn_rejects_legacy_items_field() {
"task_name": "worker"
})),
);
let Err(err) = SpawnAgentHandlerV2::default().handle(invocation).await else {
let Err(err) = SpawnAgentHandlerV2.handle(invocation).await else {
panic!("legacy items field should be rejected");
};
let FunctionCallError::RespondToModel(message) = err else {
@@ -607,7 +607,7 @@ async fn spawn_agent_errors_when_manager_dropped() {
"spawn_agent",
function_payload(json!({"message": "hello"})),
);
let Err(err) = SpawnAgentHandler::default().handle(invocation).await else {
let Err(err) = SpawnAgentHandler.handle(invocation).await else {
panic!("spawn should fail without a manager");
};
assert_eq!(
@@ -641,7 +641,7 @@ async fn multi_agent_v2_spawn_returns_path_and_send_message_accepts_relative_pat
let session = Arc::new(session);
let turn = Arc::new(turn);
let spawn_output = SpawnAgentHandlerV2::default()
let spawn_output = SpawnAgentHandlerV2
.handle(invocation(
session.clone(),
turn.clone(),
@@ -736,7 +736,7 @@ async fn multi_agent_v2_spawn_rejects_legacy_fork_context() {
.expect("test config should allow feature update");
turn.config = Arc::new(config);
let err = SpawnAgentHandlerV2::default()
let err = SpawnAgentHandlerV2
.handle(invocation(
Arc::new(session),
Arc::new(turn),
@@ -775,7 +775,7 @@ async fn multi_agent_v2_spawn_rejects_invalid_fork_turns_string() {
.expect("test config should allow feature update");
turn.config = Arc::new(config);
let err = SpawnAgentHandlerV2::default()
let err = SpawnAgentHandlerV2
.handle(invocation(
Arc::new(session),
Arc::new(turn),
@@ -814,7 +814,7 @@ async fn multi_agent_v2_spawn_rejects_zero_fork_turns() {
.expect("test config should allow feature update");
turn.config = Arc::new(config);
let err = SpawnAgentHandlerV2::default()
let err = SpawnAgentHandlerV2
.handle(invocation(
Arc::new(session),
Arc::new(turn),
@@ -1009,7 +1009,7 @@ async fn multi_agent_v2_list_agents_returns_completed_status_and_last_task_messa
let session = Arc::new(session);
let turn = Arc::new(turn);
let spawn_output = SpawnAgentHandlerV2::default()
let spawn_output = SpawnAgentHandlerV2
.handle(invocation(
session.clone(),
turn.clone(),
@@ -1190,7 +1190,7 @@ async fn multi_agent_v2_list_agents_omits_closed_agents() {
let session = Arc::new(session);
let turn = Arc::new(turn);
let spawn_output = SpawnAgentHandlerV2::default()
let spawn_output = SpawnAgentHandlerV2
.handle(invocation(
session.clone(),
turn.clone(),
@@ -1254,7 +1254,7 @@ async fn multi_agent_v2_send_message_rejects_legacy_items_field() {
let session = Arc::new(session);
let turn = Arc::new(turn);
SpawnAgentHandlerV2::default()
SpawnAgentHandlerV2
.handle(invocation(
session.clone(),
turn.clone(),
@@ -1310,7 +1310,7 @@ async fn multi_agent_v2_send_message_rejects_interrupt_parameter() {
let session = Arc::new(session);
let turn = Arc::new(turn);
SpawnAgentHandlerV2::default()
SpawnAgentHandlerV2
.handle(invocation(
session.clone(),
turn.clone(),
@@ -1383,7 +1383,7 @@ async fn multi_agent_v2_followup_task_completion_notifies_parent_on_every_turn()
let session = Arc::new(session);
let turn = Arc::new(turn);
SpawnAgentHandlerV2::default()
SpawnAgentHandlerV2
.handle(invocation(
session.clone(),
turn.clone(),
@@ -1518,7 +1518,7 @@ async fn multi_agent_v2_followup_task_rejects_legacy_items_field() {
let session = Arc::new(session);
let turn = Arc::new(turn);
SpawnAgentHandlerV2::default()
SpawnAgentHandlerV2
.handle(invocation(
session.clone(),
turn.clone(),
@@ -1571,7 +1571,7 @@ async fn multi_agent_v2_interrupted_turn_does_not_notify_parent() {
let session = Arc::new(session);
let turn = Arc::new(turn);
SpawnAgentHandlerV2::default()
SpawnAgentHandlerV2
.handle(invocation(
session.clone(),
turn.clone(),
@@ -1649,7 +1649,7 @@ async fn multi_agent_v2_spawn_omits_agent_id_when_named() {
.expect("test config should allow feature update");
turn.config = Arc::new(config);
let output = SpawnAgentHandlerV2::default()
let output = SpawnAgentHandlerV2
.handle(invocation(
Arc::new(session),
Arc::new(turn),
@@ -1697,7 +1697,7 @@ async fn multi_agent_v2_spawn_surfaces_task_name_validation_errors() {
"task_name": "BadName"
})),
);
let Err(err) = SpawnAgentHandlerV2::default().handle(invocation).await else {
let Err(err) = SpawnAgentHandlerV2.handle(invocation).await else {
panic!("invalid agent name should be rejected");
};
assert_eq!(
@@ -1755,7 +1755,7 @@ async fn spawn_agent_reapplies_runtime_sandbox_after_role_config() {
"agent_type": "explorer"
})),
);
let output = SpawnAgentHandler::default()
let output = SpawnAgentHandler
.handle(invocation)
.await
.expect("spawn_agent should succeed");
@@ -1816,7 +1816,7 @@ async fn spawn_agent_rejects_when_depth_limit_exceeded() {
"spawn_agent",
function_payload(json!({"message": "hello"})),
);
let Err(err) = SpawnAgentHandler::default().handle(invocation).await else {
let Err(err) = SpawnAgentHandler.handle(invocation).await else {
panic!("spawn should fail when depth limit exceeded");
};
assert_eq!(
@@ -1856,7 +1856,7 @@ async fn spawn_agent_allows_depth_up_to_configured_max_depth() {
"spawn_agent",
function_payload(json!({"message": "hello"})),
);
let output = SpawnAgentHandler::default()
let output = SpawnAgentHandler
.handle(invocation)
.await
.expect("spawn should succeed within configured depth");
@@ -1915,7 +1915,7 @@ async fn multi_agent_v2_spawn_agent_ignores_configured_max_depth() {
"fork_turns": "none"
})),
);
let output = SpawnAgentHandlerV2::default()
let output = SpawnAgentHandlerV2
.handle(invocation)
.await
.expect("multi-agent v2 spawn should ignore max depth");
@@ -2307,7 +2307,7 @@ async fn wait_agent_rejects_non_positive_timeout() {
"timeout_ms": 0
})),
);
let Err(err) = WaitAgentHandler::default().handle(invocation).await else {
let Err(err) = WaitAgentHandler.handle(invocation).await else {
panic!("non-positive timeout should be rejected");
};
assert_eq!(
@@ -2325,7 +2325,7 @@ async fn wait_agent_rejects_invalid_target() {
"wait_agent",
function_payload(json!({"targets": ["invalid"]})),
);
let Err(err) = WaitAgentHandler::default().handle(invocation).await else {
let Err(err) = WaitAgentHandler.handle(invocation).await else {
panic!("invalid id should be rejected");
};
let FunctionCallError::RespondToModel(msg) = err else {
@@ -2343,7 +2343,7 @@ async fn wait_agent_rejects_empty_targets() {
"wait_agent",
function_payload(json!({"targets": []})),
);
let Err(err) = WaitAgentHandler::default().handle(invocation).await else {
let Err(err) = WaitAgentHandler.handle(invocation).await else {
panic!("empty ids should be rejected");
};
assert_eq!(
@@ -2371,7 +2371,7 @@ async fn multi_agent_v2_wait_agent_accepts_timeout_only_argument() {
let session = Arc::new(session);
let turn = Arc::new(turn);
SpawnAgentHandlerV2::default()
SpawnAgentHandlerV2
.handle(invocation(
session.clone(),
turn.clone(),
@@ -2401,7 +2401,7 @@ async fn multi_agent_v2_wait_agent_accepts_timeout_only_argument() {
let session = session.clone();
let turn = turn.clone();
async move {
WaitAgentHandlerV2::default()
WaitAgentHandlerV2
.handle(invocation(
session,
turn,
@@ -2453,7 +2453,7 @@ async fn multi_agent_v2_wait_agent_uses_configured_min_timeout() {
let early = timeout(
Duration::from_millis(/*millis*/ 20),
WaitAgentHandlerV2::default().handle(invocation(
WaitAgentHandlerV2.handle(invocation(
session.clone(),
turn.clone(),
"wait_agent",
@@ -2468,7 +2468,7 @@ async fn multi_agent_v2_wait_agent_uses_configured_min_timeout() {
let output = timeout(
Duration::from_secs(/*secs*/ 1),
WaitAgentHandlerV2::default().handle(invocation(
WaitAgentHandlerV2.handle(invocation(
session,
turn,
"wait_agent",
@@ -2507,7 +2507,7 @@ async fn wait_agent_returns_not_found_for_missing_agents() {
"timeout_ms": 1000
})),
);
let output = WaitAgentHandler::default()
let output = WaitAgentHandler
.handle(invocation)
.await
.expect("wait_agent should succeed");
@@ -2547,7 +2547,7 @@ async fn wait_agent_times_out_when_status_is_not_final() {
"timeout_ms": MIN_WAIT_TIMEOUT_MS
})),
);
let output = WaitAgentHandler::default()
let output = WaitAgentHandler
.handle(invocation)
.await
.expect("wait_agent should succeed");
@@ -2593,7 +2593,7 @@ async fn wait_agent_clamps_short_timeouts_to_minimum() {
let early = timeout(
Duration::from_millis(50),
WaitAgentHandler::default().handle(invocation),
WaitAgentHandler.handle(invocation),
)
.await;
assert!(
@@ -2643,7 +2643,7 @@ async fn wait_agent_returns_final_status_without_timeout() {
"timeout_ms": 1000
})),
);
let output = WaitAgentHandler::default()
let output = WaitAgentHandler
.handle(invocation)
.await
.expect("wait_agent should succeed");
@@ -2679,7 +2679,7 @@ async fn multi_agent_v2_wait_agent_returns_summary_for_mailbox_activity() {
let session = Arc::new(session);
let turn = Arc::new(turn);
let spawn_output = SpawnAgentHandlerV2::default()
let spawn_output = SpawnAgentHandlerV2
.handle(invocation(
session.clone(),
turn.clone(),
@@ -2714,7 +2714,7 @@ async fn multi_agent_v2_wait_agent_returns_summary_for_mailbox_activity() {
let session = session.clone();
let turn = turn.clone();
async move {
WaitAgentHandlerV2::default()
WaitAgentHandlerV2
.handle(invocation(
session,
turn,
@@ -2770,7 +2770,7 @@ async fn multi_agent_v2_wait_agent_returns_for_already_queued_mail() {
let session = Arc::new(session);
let turn = Arc::new(turn);
SpawnAgentHandlerV2::default()
SpawnAgentHandlerV2
.handle(invocation(
session.clone(),
turn.clone(),
@@ -2806,7 +2806,7 @@ async fn multi_agent_v2_wait_agent_returns_for_already_queued_mail() {
let output = timeout(
Duration::from_millis(500),
WaitAgentHandlerV2::default().handle(invocation(
WaitAgentHandlerV2.handle(invocation(
session,
turn,
"wait_agent",
@@ -2849,7 +2849,7 @@ async fn multi_agent_v2_wait_agent_wakes_on_any_mailbox_notification() {
let turn = Arc::new(turn);
for task_name in ["worker_a", "worker_b"] {
SpawnAgentHandlerV2::default()
SpawnAgentHandlerV2
.handle(invocation(
session.clone(),
turn.clone(),
@@ -2880,7 +2880,7 @@ async fn multi_agent_v2_wait_agent_wakes_on_any_mailbox_notification() {
let session = session.clone();
let turn = turn.clone();
async move {
WaitAgentHandlerV2::default()
WaitAgentHandlerV2
.handle(invocation(
session,
turn,
@@ -2936,7 +2936,7 @@ async fn multi_agent_v2_wait_agent_does_not_return_completed_content() {
let session = Arc::new(session);
let turn = Arc::new(turn);
SpawnAgentHandlerV2::default()
SpawnAgentHandlerV2
.handle(invocation(
session.clone(),
turn.clone(),
@@ -2965,7 +2965,7 @@ async fn multi_agent_v2_wait_agent_does_not_return_completed_content() {
let session = session.clone();
let turn = turn.clone();
async move {
WaitAgentHandlerV2::default()
WaitAgentHandlerV2
.handle(invocation(
session,
turn,
@@ -3022,7 +3022,7 @@ async fn multi_agent_v2_close_agent_accepts_task_name_target() {
let session = Arc::new(session);
let turn = Arc::new(turn);
SpawnAgentHandlerV2::default()
SpawnAgentHandlerV2
.handle(invocation(
session.clone(),
turn.clone(),
@@ -3179,7 +3179,7 @@ async fn tool_handlers_cascade_close_and_resume_and_keep_explicitly_closed_subtr
let parent_thread_id = parent.thread_id;
let parent_session = parent.thread.codex.session.clone();
let child_spawn_output = SpawnAgentHandler::default()
let child_spawn_output = SpawnAgentHandler
.handle(invocation(
parent_session.clone(),
parent_session.new_default_turn().await,
@@ -3204,7 +3204,7 @@ async fn tool_handlers_cascade_close_and_resume_and_keep_explicitly_closed_subtr
.await
.expect("child thread should exist");
let child_session = child_thread.codex.session.clone();
let grandchild_spawn_output = SpawnAgentHandler::default()
let grandchild_spawn_output = SpawnAgentHandler
.handle(invocation(
child_session.clone(),
child_session.new_default_turn().await,

View File

@@ -7,8 +7,18 @@ use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::multi_agents_common::*;
use crate::tools::handlers::multi_agents_spec::SpawnAgentToolOptions;
use crate::tools::handlers::multi_agents_spec::WaitAgentTimeoutOptions;
use crate::tools::handlers::multi_agents_spec::create_close_agent_tool_v2;
use crate::tools::handlers::multi_agents_spec::create_followup_task_tool;
use crate::tools::handlers::multi_agents_spec::create_list_agents_tool;
use crate::tools::handlers::multi_agents_spec::create_send_message_tool;
use crate::tools::handlers::multi_agents_spec::create_spawn_agent_tool_v2;
use crate::tools::handlers::multi_agents_spec::create_wait_agent_tool_v2;
use crate::tools::handlers::parse_arguments;
use crate::tools::registry::ToolHandler;
use crate::tools::runtime_definition::RuntimeToolDefinition;
use crate::tools::runtime_definition::runtime_tool_definition;
use codex_protocol::AgentPath;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::openai_models::ReasoningEffort;

View File

@@ -1,10 +1,14 @@
use super::*;
use crate::tools::handlers::multi_agents_spec::create_close_agent_tool_v2;
use crate::turn_timing::now_unix_timestamp_ms;
use codex_tools::ToolSpec;
pub(crate) struct Handler;
impl Handler {
pub(crate) fn definition() -> RuntimeToolDefinition {
runtime_tool_definition(Self, create_close_agent_tool_v2())
}
}
impl ToolHandler for Handler {
type Output = CloseAgentResult;
@@ -12,10 +16,6 @@ impl ToolHandler for Handler {
ToolName::plain("close_agent")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_close_agent_tool_v2())
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}

View File

@@ -3,11 +3,15 @@ use super::message_tool::MessageDeliveryMode;
use super::message_tool::handle_message_string_tool;
use super::*;
use crate::tools::context::FunctionToolOutput;
use crate::tools::handlers::multi_agents_spec::create_followup_task_tool;
use codex_tools::ToolSpec;
pub(crate) struct Handler;
impl Handler {
pub(crate) fn definition() -> RuntimeToolDefinition {
runtime_tool_definition(Self, create_followup_task_tool())
}
}
impl ToolHandler for Handler {
type Output = FunctionToolOutput;
@@ -15,10 +19,6 @@ impl ToolHandler for Handler {
ToolName::plain("followup_task")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_followup_task_tool())
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}

View File

@@ -1,10 +1,14 @@
use super::*;
use crate::agent::control::ListedAgent;
use crate::tools::handlers::multi_agents_spec::create_list_agents_tool;
use codex_tools::ToolSpec;
pub(crate) struct Handler;
impl Handler {
pub(crate) fn definition() -> RuntimeToolDefinition {
runtime_tool_definition(Self, create_list_agents_tool())
}
}
impl ToolHandler for Handler {
type Output = ListAgentsResult;
@@ -12,10 +16,6 @@ impl ToolHandler for Handler {
ToolName::plain("list_agents")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_list_agents_tool())
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}

View File

@@ -3,11 +3,15 @@ use super::message_tool::SendMessageArgs;
use super::message_tool::handle_message_string_tool;
use super::*;
use crate::tools::context::FunctionToolOutput;
use crate::tools::handlers::multi_agents_spec::create_send_message_tool;
use codex_tools::ToolSpec;
pub(crate) struct Handler;
impl Handler {
pub(crate) fn definition() -> RuntimeToolDefinition {
runtime_tool_definition(Self, create_send_message_tool())
}
}
impl ToolHandler for Handler {
type Output = FunctionToolOutput;
@@ -15,10 +19,6 @@ impl ToolHandler for Handler {
ToolName::plain("send_message")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_send_message_tool())
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}

View File

@@ -5,22 +5,17 @@ use crate::agent::control::render_input_preview;
use crate::agent::next_thread_spawn_depth;
use crate::agent::role::DEFAULT_ROLE_NAME;
use crate::agent::role::apply_role_to_config;
use crate::tools::handlers::multi_agents_spec::SpawnAgentToolOptions;
use crate::tools::handlers::multi_agents_spec::create_spawn_agent_tool_v2;
use crate::turn_timing::now_unix_timestamp_ms;
use codex_protocol::AgentPath;
use codex_protocol::protocol::InterAgentCommunication;
use codex_protocol::protocol::Op;
use codex_tools::ToolSpec;
#[derive(Default)]
pub(crate) struct Handler {
options: SpawnAgentToolOptions,
}
pub(crate) struct Handler;
impl Handler {
pub(crate) fn new(options: SpawnAgentToolOptions) -> Self {
Self { options }
pub(crate) fn definition(options: SpawnAgentToolOptions) -> RuntimeToolDefinition {
runtime_tool_definition(Self, create_spawn_agent_tool_v2(options))
}
}
@@ -31,10 +26,6 @@ impl ToolHandler for Handler {
ToolName::plain("spawn_agent")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_spawn_agent_tool_v2(self.options.clone()))
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}

View File

@@ -1,21 +1,16 @@
use super::*;
use crate::tools::handlers::multi_agents_spec::WaitAgentTimeoutOptions;
use crate::tools::handlers::multi_agents_spec::create_wait_agent_tool_v2;
use crate::turn_timing::now_unix_timestamp_ms;
use codex_tools::ToolSpec;
use std::collections::HashMap;
use std::time::Duration;
use tokio::time::Instant;
use tokio::time::timeout_at;
#[derive(Default)]
pub(crate) struct Handler {
options: WaitAgentTimeoutOptions,
}
pub(crate) struct Handler;
impl Handler {
pub(crate) fn new(options: WaitAgentTimeoutOptions) -> Self {
Self { options }
pub(crate) fn definition(options: WaitAgentTimeoutOptions) -> RuntimeToolDefinition {
runtime_tool_definition(Self, create_wait_agent_tool_v2(options))
}
}
@@ -26,10 +21,6 @@ impl ToolHandler for Handler {
ToolName::plain("wait_agent")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_wait_agent_tool_v2(self.options))
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}

View File

@@ -4,17 +4,24 @@ use crate::tools::context::ToolOutput;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::plan_spec::create_update_plan_tool;
use crate::tools::registry::ToolHandler;
use crate::tools::runtime_definition::RuntimeToolDefinition;
use crate::tools::runtime_definition::runtime_tool_definition;
use codex_protocol::config_types::ModeKind;
use codex_protocol::models::FunctionCallOutputPayload;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::plan_tool::UpdatePlanArgs;
use codex_protocol::protocol::EventMsg;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use serde_json::Value as JsonValue;
pub struct PlanHandler;
impl PlanHandler {
pub(crate) fn definition() -> RuntimeToolDefinition {
runtime_tool_definition(Self, create_update_plan_tool())
}
}
pub struct PlanToolOutput;
const PLAN_UPDATED_MESSAGE: &str = "Plan updated";
@@ -50,10 +57,6 @@ impl ToolHandler for PlanHandler {
ToolName::plain("update_plan")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_update_plan_tool())
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let ToolInvocation {
session,

View File

@@ -9,11 +9,21 @@ use crate::tools::handlers::parse_arguments_with_base_path;
use crate::tools::handlers::shell_spec::create_request_permissions_tool;
use crate::tools::handlers::shell_spec::request_permissions_tool_description;
use crate::tools::registry::ToolHandler;
use crate::tools::runtime_definition::RuntimeToolDefinition;
use crate::tools::runtime_definition::runtime_tool_definition;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
pub struct RequestPermissionsHandler;
impl RequestPermissionsHandler {
pub(crate) fn definition() -> RuntimeToolDefinition {
runtime_tool_definition(
Self,
create_request_permissions_tool(request_permissions_tool_description()),
)
}
}
impl ToolHandler for RequestPermissionsHandler {
type Output = FunctionToolOutput;
@@ -21,12 +31,6 @@ impl ToolHandler for RequestPermissionsHandler {
ToolName::plain("request_permissions")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_request_permissions_tool(
request_permissions_tool_description(),
))
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let ToolInvocation {
session,

View File

@@ -11,14 +11,12 @@ use codex_tools::DiscoverableToolType;
use codex_tools::REQUEST_PLUGIN_INSTALL_PERSIST_ALWAYS_VALUE;
use codex_tools::REQUEST_PLUGIN_INSTALL_PERSIST_KEY;
use codex_tools::REQUEST_PLUGIN_INSTALL_TOOL_NAME;
use codex_tools::RequestPluginInstallArgs;
use codex_tools::RequestPluginInstallEntry;
use codex_tools::RequestPluginInstallArgs;
use codex_tools::RequestPluginInstallResult;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use codex_tools::all_requested_connectors_picked_up;
use codex_tools::build_request_plugin_install_elicitation_request;
use codex_tools::collect_request_plugin_install_entries;
use codex_tools::filter_request_plugin_install_discoverable_tools_for_client;
use codex_tools::verified_connector_install_completed;
use rmcp::model::RequestId;
@@ -35,17 +33,20 @@ use crate::tools::context::ToolPayload;
use crate::tools::handlers::parse_arguments;
use crate::tools::handlers::request_plugin_install_spec::create_request_plugin_install_tool;
use crate::tools::registry::ToolHandler;
use crate::tools::runtime_definition::RuntimeToolDefinition;
use crate::tools::runtime_definition::runtime_tool_definition;
#[derive(Default)]
pub struct RequestPluginInstallHandler {
discoverable_tools: Vec<RequestPluginInstallEntry>,
}
pub struct RequestPluginInstallHandler;
impl RequestPluginInstallHandler {
pub(crate) fn new(discoverable_tools: &[DiscoverableTool]) -> Self {
Self {
discoverable_tools: collect_request_plugin_install_entries(discoverable_tools),
}
pub(crate) fn definition(
discoverable_tools: &[RequestPluginInstallEntry],
) -> RuntimeToolDefinition {
runtime_tool_definition(
Self,
create_request_plugin_install_tool(discoverable_tools),
)
}
}
@@ -56,10 +57,6 @@ impl ToolHandler for RequestPluginInstallHandler {
ToolName::plain(REQUEST_PLUGIN_INSTALL_TOOL_NAME)
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_request_plugin_install_tool(&self.discoverable_tools))
}
fn supports_parallel_tool_calls(&self) -> bool {
true
}

View File

@@ -9,15 +9,26 @@ use crate::tools::handlers::request_user_input_spec::normalize_request_user_inpu
use crate::tools::handlers::request_user_input_spec::request_user_input_tool_description;
use crate::tools::handlers::request_user_input_spec::request_user_input_unavailable_message;
use crate::tools::registry::ToolHandler;
use crate::tools::runtime_definition::RuntimeToolDefinition;
use crate::tools::runtime_definition::runtime_tool_definition;
use codex_protocol::config_types::ModeKind;
use codex_protocol::request_user_input::RequestUserInputArgs;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
pub struct RequestUserInputHandler {
pub available_modes: Vec<ModeKind>,
}
impl RequestUserInputHandler {
pub(crate) fn definition(available_modes: Vec<ModeKind>) -> RuntimeToolDefinition {
let description = request_user_input_tool_description(&available_modes);
runtime_tool_definition(
Self { available_modes },
create_request_user_input_tool(description),
)
}
}
impl ToolHandler for RequestUserInputHandler {
type Output = FunctionToolOutput;
@@ -25,12 +36,6 @@ impl ToolHandler for RequestUserInputHandler {
ToolName::plain(REQUEST_USER_INPUT_TOOL_NAME)
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_request_user_input_tool(
request_user_input_tool_description(&self.available_modes),
))
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let ToolInvocation {
session,

View File

@@ -41,7 +41,6 @@ mod shell_handler;
pub use container_exec::ContainerExecHandler;
pub use local_shell::LocalShellHandler;
pub use shell_command::ShellCommandHandler;
pub(crate) use shell_command::ShellCommandHandlerOptions;
pub use shell_handler::ShellHandler;
fn shell_function_payload_command(payload: &ToolPayload) -> Option<String> {

View File

@@ -1,6 +1,10 @@
use codex_shell_command::is_safe_command::is_known_safe_command;
use codex_tools::ToolName;
use super::RunExecLikeArgs;
use super::local_shell_payload_command;
use super::run_exec_like;
use super::shell_handler::ShellHandler;
use crate::function_tool::FunctionCallError;
use crate::tools::context::FunctionToolOutput;
use crate::tools::context::ToolInvocation;
@@ -11,23 +15,25 @@ use crate::tools::hook_names::HookToolName;
use crate::tools::registry::PostToolUsePayload;
use crate::tools::registry::PreToolUsePayload;
use crate::tools::registry::ToolHandler;
use crate::tools::runtime_definition::RuntimeToolDefinition;
use crate::tools::runtime_definition::runtime_tool_definition;
use crate::tools::runtimes::shell::ShellRuntimeBackend;
use codex_tools::ToolSpec;
use super::super::shell_spec::create_local_shell_tool;
use super::RunExecLikeArgs;
use super::local_shell_payload_command;
use super::run_exec_like;
use super::shell_handler::ShellHandler;
use crate::tools::handlers::shell_spec::create_local_shell_tool;
#[derive(Default)]
pub struct LocalShellHandler {
include_spec: bool,
supports_parallel_tool_calls: bool,
}
impl LocalShellHandler {
pub(crate) fn new() -> Self {
Self { include_spec: true }
Self {
supports_parallel_tool_calls: true,
}
}
pub(crate) fn definition() -> RuntimeToolDefinition {
runtime_tool_definition(Self::new(), create_local_shell_tool())
}
}
@@ -38,12 +44,8 @@ impl ToolHandler for LocalShellHandler {
ToolName::plain("local_shell")
}
fn spec(&self) -> Option<ToolSpec> {
self.include_spec.then(create_local_shell_tool)
}
fn supports_parallel_tool_calls(&self) -> bool {
self.include_spec
self.supports_parallel_tool_calls
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {

View File

@@ -4,6 +4,9 @@ use codex_shell_command::is_safe_command::is_known_safe_command;
use codex_tools::ShellCommandBackendConfig;
use codex_tools::ToolName;
use super::RunExecLikeArgs;
use super::run_exec_like;
use super::shell_command_payload_command;
use crate::exec::ExecCapturePolicy;
use crate::exec::ExecParams;
use crate::exec_env::create_env;
@@ -23,14 +26,11 @@ use crate::tools::hook_names::HookToolName;
use crate::tools::registry::PostToolUsePayload;
use crate::tools::registry::PreToolUsePayload;
use crate::tools::registry::ToolHandler;
use crate::tools::runtime_definition::RuntimeToolDefinition;
use crate::tools::runtime_definition::runtime_tool_definition;
use crate::tools::runtimes::shell::ShellRuntimeBackend;
use codex_tools::ToolSpec;
use super::super::shell_spec::CommandToolOptions;
use super::super::shell_spec::create_shell_command_tool;
use super::RunExecLikeArgs;
use super::run_exec_like;
use super::shell_command_payload_command;
use crate::tools::handlers::shell_spec::CommandToolOptions;
use crate::tools::handlers::shell_spec::create_shell_command_tool;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum ShellCommandBackend {
@@ -40,24 +40,24 @@ enum ShellCommandBackend {
pub struct ShellCommandHandler {
backend: ShellCommandBackend,
options: Option<ShellCommandHandlerOptions>,
}
#[derive(Clone, Copy)]
pub(crate) struct ShellCommandHandlerOptions {
pub(crate) backend_config: ShellCommandBackendConfig,
pub(crate) allow_login_shell: bool,
pub(crate) exec_permission_approvals_enabled: bool,
supports_parallel_tool_calls: bool,
}
impl ShellCommandHandler {
pub(crate) fn new(options: ShellCommandHandlerOptions) -> Self {
pub(crate) fn new(backend_config: ShellCommandBackendConfig) -> Self {
Self {
options: Some(options),
..Self::from(options.backend_config)
supports_parallel_tool_calls: true,
..Self::from(backend_config)
}
}
pub(crate) fn definition(
backend_config: ShellCommandBackendConfig,
options: CommandToolOptions,
) -> RuntimeToolDefinition {
runtime_tool_definition(Self::new(backend_config), create_shell_command_tool(options))
}
fn shell_runtime_backend(&self) -> ShellRuntimeBackend {
match self.backend {
ShellCommandBackend::Classic => ShellRuntimeBackend::ShellCommandClassic,
@@ -120,7 +120,7 @@ impl From<ShellCommandBackendConfig> for ShellCommandHandler {
};
Self {
backend,
options: None,
supports_parallel_tool_calls: false,
}
}
}
@@ -132,17 +132,8 @@ impl ToolHandler for ShellCommandHandler {
ToolName::plain("shell_command")
}
fn spec(&self) -> Option<ToolSpec> {
self.options.map(|options| {
create_shell_command_tool(CommandToolOptions {
allow_login_shell: options.allow_login_shell,
exec_permission_approvals_enabled: options.exec_permission_approvals_enabled,
})
})
}
fn supports_parallel_tool_calls(&self) -> bool {
self.options.is_some()
self.supports_parallel_tool_calls
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {

View File

@@ -3,6 +3,11 @@ use codex_protocol::models::ShellToolCallParams;
use codex_shell_command::is_safe_command::is_known_safe_command;
use codex_tools::ToolName;
use super::RunExecLikeArgs;
use super::rewrite_shell_function_updated_hook_input;
use super::run_exec_like;
use super::shell_function_post_tool_use_payload;
use super::shell_function_pre_tool_use_payload;
use crate::exec::ExecCapturePolicy;
use crate::exec::ExecParams;
use crate::exec_env::create_env;
@@ -16,29 +21,28 @@ use crate::tools::handlers::resolve_workdir_base_path;
use crate::tools::registry::PostToolUsePayload;
use crate::tools::registry::PreToolUsePayload;
use crate::tools::registry::ToolHandler;
use crate::tools::runtime_definition::RuntimeToolDefinition;
use crate::tools::runtime_definition::runtime_tool_definition;
use crate::tools::runtimes::shell::ShellRuntimeBackend;
use codex_tools::ToolSpec;
use super::super::shell_spec::ShellToolOptions;
use super::super::shell_spec::create_shell_tool;
use super::RunExecLikeArgs;
use super::rewrite_shell_function_updated_hook_input;
use super::run_exec_like;
use super::shell_function_post_tool_use_payload;
use super::shell_function_pre_tool_use_payload;
use crate::tools::handlers::shell_spec::ShellToolOptions;
use crate::tools::handlers::shell_spec::create_shell_tool;
#[derive(Default)]
pub struct ShellHandler {
options: Option<ShellToolOptions>,
supports_parallel_tool_calls: bool,
}
impl ShellHandler {
pub(crate) fn new(options: ShellToolOptions) -> Self {
pub(crate) fn new() -> Self {
Self {
options: Some(options),
supports_parallel_tool_calls: true,
}
}
pub(crate) fn definition(options: ShellToolOptions) -> RuntimeToolDefinition {
runtime_tool_definition(Self::new(), create_shell_tool(options))
}
pub(super) fn to_exec_params(
params: &ShellToolCallParams,
turn_context: &TurnContext,
@@ -70,12 +74,8 @@ impl ToolHandler for ShellHandler {
ToolName::plain("shell")
}
fn spec(&self) -> Option<ToolSpec> {
self.options.map(create_shell_tool)
}
fn supports_parallel_tool_calls(&self) -> bool {
self.options.is_some()
self.supports_parallel_tool_calls
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {

View File

@@ -15,11 +15,18 @@ use crate::tools::context::ToolPayload;
use crate::tools::handlers::parse_arguments;
use crate::tools::handlers::test_sync_spec::create_test_sync_tool;
use crate::tools::registry::ToolHandler;
use crate::tools::runtime_definition::RuntimeToolDefinition;
use crate::tools::runtime_definition::runtime_tool_definition;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
pub struct TestSyncHandler;
impl TestSyncHandler {
pub(crate) fn definition() -> RuntimeToolDefinition {
runtime_tool_definition(Self, create_test_sync_tool())
}
}
const DEFAULT_TIMEOUT_MS: u64 = 1_000;
static BARRIERS: OnceLock<tokio::sync::Mutex<HashMap<String, BarrierState>>> = OnceLock::new();
@@ -62,10 +69,6 @@ impl ToolHandler for TestSyncHandler {
ToolName::plain("test_sync_tool")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_test_sync_tool())
}
fn supports_parallel_tool_calls(&self) -> bool {
true
}

View File

@@ -2,9 +2,11 @@ use crate::function_tool::FunctionCallError;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::context::ToolSearchOutput;
use crate::tools::handlers::tool_search_spec::create_tool_search_tool;
use crate::tools::registry::ToolHandler;
use crate::tools::runtime_definition::RuntimeToolDefinition;
use crate::tools::runtime_definition::runtime_tool_definition;
use crate::tools::tool_search_entry::ToolSearchEntry;
use crate::tools::handlers::tool_search_spec::create_tool_search_tool;
use bm25::Document;
use bm25::Language;
use bm25::SearchEngine;
@@ -13,8 +15,6 @@ use codex_tools::LoadableToolSpec;
use codex_tools::TOOL_SEARCH_DEFAULT_LIMIT;
use codex_tools::TOOL_SEARCH_TOOL_NAME;
use codex_tools::ToolName;
use codex_tools::ToolSearchSourceInfo;
use codex_tools::ToolSpec;
use codex_tools::coalesce_loadable_tool_specs;
use std::collections::HashMap;
@@ -23,15 +23,11 @@ const COMPUTER_USE_TOOL_SEARCH_LIMIT: usize = 20;
pub struct ToolSearchHandler {
entries: Vec<ToolSearchEntry>,
search_source_infos: Vec<ToolSearchSourceInfo>,
search_engine: SearchEngine<usize>,
}
impl ToolSearchHandler {
pub(crate) fn new(
entries: Vec<ToolSearchEntry>,
search_source_infos: Vec<ToolSearchSourceInfo>,
) -> Self {
pub(crate) fn new(entries: Vec<ToolSearchEntry>) -> Self {
let documents: Vec<Document<usize>> = entries
.iter()
.map(|entry| entry.search_text.clone())
@@ -43,10 +39,19 @@ impl ToolSearchHandler {
Self {
entries,
search_source_infos,
search_engine,
}
}
pub(crate) fn definition(
entries: Vec<ToolSearchEntry>,
source_infos: Vec<codex_tools::ToolSearchSourceInfo>,
) -> RuntimeToolDefinition {
runtime_tool_definition(
Self::new(entries),
create_tool_search_tool(&source_infos, TOOL_SEARCH_DEFAULT_LIMIT),
)
}
}
impl ToolHandler for ToolSearchHandler {
@@ -56,13 +61,6 @@ impl ToolHandler for ToolSearchHandler {
ToolName::plain(TOOL_SEARCH_TOOL_NAME)
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_tool_search_tool(
&self.search_source_infos,
TOOL_SEARCH_DEFAULT_LIMIT,
))
}
fn supports_parallel_tool_calls(&self) -> bool {
true
}
@@ -431,9 +429,6 @@ mod tests {
mcp_tools: Option<&[ToolInfo]>,
dynamic_tools: &[DynamicToolSpec],
) -> ToolSearchHandler {
ToolSearchHandler::new(
build_tool_search_entries(mcp_tools, dynamic_tools),
Vec::new(),
)
ToolSearchHandler::new(build_tool_search_entries(mcp_tools, dynamic_tools))
}
}

View File

@@ -2,28 +2,43 @@ use crate::function_tool::FunctionCallError;
use crate::tools::context::FunctionToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::flat_tool_name;
use crate::tools::registry::ToolHandler;
use crate::tools::runtime_definition::RuntimeToolDefinition;
use crate::tools::runtime_definition::runtime_tool_definition;
use codex_tools::AdditionalProperties;
use codex_tools::JsonSchema;
use codex_tools::ResponsesApiTool;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
pub struct UnavailableToolHandler {
tool_name: ToolName,
spec: Option<ToolSpec>,
}
impl UnavailableToolHandler {
pub fn new(tool_name: ToolName, spec: ToolSpec) -> Self {
Self {
tool_name,
spec: Some(spec),
}
pub fn new(tool_name: ToolName) -> Self {
Self { tool_name }
}
pub fn without_spec(tool_name: ToolName) -> Self {
Self {
tool_name,
spec: None,
}
pub(crate) fn definition(tool_name: ToolName) -> RuntimeToolDefinition {
let name = flat_tool_name(&tool_name).into_owned();
let spec = ToolSpec::Function(ResponsesApiTool {
name: name.clone(),
description: unavailable_tool_message(
&name,
"Calling this placeholder returns an error explaining that the tool is unavailable.",
),
strict: false,
parameters: JsonSchema::object(
Default::default(),
/*required*/ None,
Some(AdditionalProperties::Boolean(false)),
),
output_schema: None,
defer_loading: None,
});
runtime_tool_definition(Self::new(tool_name), spec)
}
}
@@ -43,10 +58,6 @@ impl ToolHandler for UnavailableToolHandler {
self.tool_name.clone()
}
fn spec(&self) -> Option<ToolSpec> {
self.spec.clone()
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let ToolInvocation { payload, .. } = invocation;

View File

@@ -22,7 +22,6 @@ mod exec_command;
mod write_stdin;
pub use exec_command::ExecCommandHandler;
pub(crate) use exec_command::ExecCommandHandlerOptions;
pub use write_stdin::WriteStdinHandler;
#[derive(Debug, Deserialize)]

View File

@@ -18,6 +18,8 @@ use crate::tools::hook_names::HookToolName;
use crate::tools::registry::PostToolUsePayload;
use crate::tools::registry::PreToolUsePayload;
use crate::tools::registry::ToolHandler;
use crate::tools::runtime_definition::RuntimeToolDefinition;
use crate::tools::runtime_definition::runtime_tool_definition;
use crate::unified_exec::ExecCommandRequest;
use crate::unified_exec::UnifiedExecContext;
use crate::unified_exec::UnifiedExecError;
@@ -28,43 +30,28 @@ use codex_otel::SessionTelemetry;
use codex_otel::TOOL_CALL_UNIFIED_EXEC_METRIC;
use codex_shell_command::is_safe_command::is_known_safe_command;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use codex_utils_output_truncation::approx_token_count;
use super::super::shell_spec::CommandToolOptions;
use super::super::shell_spec::create_exec_command_tool_with_environment_id;
use super::ExecCommandArgs;
use super::ExecCommandEnvironmentArgs;
use super::effective_max_output_tokens;
use super::get_command;
use super::post_unified_exec_tool_use_payload;
use crate::tools::handlers::shell_spec::CommandToolOptions;
use crate::tools::handlers::shell_spec::create_exec_command_tool_with_environment_id;
#[derive(Clone, Copy)]
pub(crate) struct ExecCommandHandlerOptions {
pub(crate) allow_login_shell: bool,
pub(crate) exec_permission_approvals_enabled: bool,
pub(crate) include_environment_id: bool,
}
pub struct ExecCommandHandler {
options: ExecCommandHandlerOptions,
}
impl Default for ExecCommandHandler {
fn default() -> Self {
Self {
options: ExecCommandHandlerOptions {
allow_login_shell: false,
exec_permission_approvals_enabled: false,
include_environment_id: false,
},
}
}
}
#[derive(Default)]
pub struct ExecCommandHandler;
impl ExecCommandHandler {
pub(crate) fn new(options: ExecCommandHandlerOptions) -> Self {
Self { options }
pub(crate) fn definition(
options: CommandToolOptions,
include_environment_id: bool,
) -> RuntimeToolDefinition {
runtime_tool_definition(
Self,
create_exec_command_tool_with_environment_id(options, include_environment_id),
)
}
}
@@ -75,16 +62,6 @@ impl ToolHandler for ExecCommandHandler {
ToolName::plain("exec_command")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_exec_command_tool_with_environment_id(
CommandToolOptions {
allow_login_shell: self.options.allow_login_shell,
exec_permission_approvals_enabled: self.options.exec_permission_approvals_enabled,
},
self.options.include_environment_id,
))
}
fn supports_parallel_tool_calls(&self) -> bool {
true
}

View File

@@ -3,16 +3,17 @@ use crate::tools::context::ExecCommandToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::parse_arguments;
use crate::tools::handlers::shell_spec::create_write_stdin_tool;
use crate::tools::registry::PostToolUsePayload;
use crate::tools::registry::ToolHandler;
use crate::tools::runtime_definition::RuntimeToolDefinition;
use crate::tools::runtime_definition::runtime_tool_definition;
use crate::unified_exec::WriteStdinRequest;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::TerminalInteractionEvent;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use serde::Deserialize;
use super::super::shell_spec::create_write_stdin_tool;
use super::effective_max_output_tokens;
use super::post_unified_exec_tool_use_payload;
@@ -30,6 +31,12 @@ struct WriteStdinArgs {
pub struct WriteStdinHandler;
impl WriteStdinHandler {
pub(crate) fn definition() -> RuntimeToolDefinition {
runtime_tool_definition(Self, create_write_stdin_tool())
}
}
impl ToolHandler for WriteStdinHandler {
type Output = ExecCommandToolOutput;
@@ -37,10 +44,6 @@ impl ToolHandler for WriteStdinHandler {
ToolName::plain("write_stdin")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_write_stdin_tool())
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}

View File

@@ -184,7 +184,7 @@ async fn exec_command_pre_tool_use_payload_uses_raw_command() {
arguments: serde_json::json!({ "cmd": "printf exec command" }).to_string(),
};
let (session, turn) = make_session_and_context().await;
let handler = ExecCommandHandler::default();
let handler = ExecCommandHandler;
assert_eq!(
handler.pre_tool_use_payload(&ToolInvocation {
@@ -244,7 +244,7 @@ async fn exec_command_post_tool_use_payload_uses_output_for_noninteractive_one_s
hook_command: Some("echo three".to_string()),
};
let invocation = invocation_for_payload("exec_command", "call-43", payload).await;
let handler = ExecCommandHandler::default();
let handler = ExecCommandHandler;
assert_eq!(
handler.post_tool_use_payload(&invocation, &output),
Some(crate::tools::registry::PostToolUsePayload {
@@ -273,7 +273,7 @@ async fn exec_command_post_tool_use_payload_uses_output_for_interactive_completi
hook_command: Some("echo three".to_string()),
};
let invocation = invocation_for_payload("exec_command", "call-44", payload).await;
let handler = ExecCommandHandler::default();
let handler = ExecCommandHandler;
assert_eq!(
handler.post_tool_use_payload(&invocation, &output),
@@ -303,7 +303,7 @@ async fn exec_command_post_tool_use_payload_skips_running_sessions() {
hook_command: Some("echo three".to_string()),
};
let invocation = invocation_for_payload("exec_command", "call-45", payload).await;
let handler = ExecCommandHandler::default();
let handler = ExecCommandHandler;
assert_eq!(handler.post_tool_use_payload(&invocation, &output), None);
}

View File

@@ -21,27 +21,16 @@ use crate::tools::handlers::resolve_tool_environment;
use crate::tools::handlers::view_image_spec::ViewImageToolOptions;
use crate::tools::handlers::view_image_spec::create_view_image_tool;
use crate::tools::registry::ToolHandler;
use crate::tools::runtime_definition::RuntimeToolDefinition;
use crate::tools::runtime_definition::runtime_tool_definition;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
pub struct ViewImageHandler {
options: ViewImageToolOptions,
}
impl Default for ViewImageHandler {
fn default() -> Self {
Self {
options: ViewImageToolOptions {
can_request_original_image_detail: false,
include_environment_id: false,
},
}
}
}
#[derive(Default)]
pub struct ViewImageHandler;
impl ViewImageHandler {
pub(crate) fn new(options: ViewImageToolOptions) -> Self {
Self { options }
pub(crate) fn definition(options: ViewImageToolOptions) -> RuntimeToolDefinition {
runtime_tool_definition(Self, create_view_image_tool(options))
}
}
@@ -68,10 +57,6 @@ impl ToolHandler for ViewImageHandler {
ToolName::plain("view_image")
}
fn spec(&self) -> Option<ToolSpec> {
Some(create_view_image_tool(self.options))
}
fn supports_parallel_tool_calls(&self) -> bool {
true
}
@@ -280,7 +265,7 @@ mod tests {
std::fs::write(image_path.as_path(), b"not a real image").expect("write test image");
turn.permission_profile = PermissionProfile::read_only();
let result = ViewImageHandler::default()
let result = ViewImageHandler
.handle(ToolInvocation {
session: Arc::new(session),
turn: Arc::new(turn),

View File

@@ -9,6 +9,7 @@ pub(crate) mod orchestrator;
pub(crate) mod parallel;
pub(crate) mod registry;
pub(crate) mod router;
pub(crate) mod runtime_definition;
pub(crate) mod runtimes;
pub(crate) mod sandboxing;
pub(crate) mod spec;

View File

@@ -17,14 +17,11 @@ use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput;
use crate::tools::context::ToolPayload;
use crate::tools::flat_tool_name;
use crate::tools::handlers::extension_tools::BundledToolHandler;
use crate::tools::handlers::extension_tools::extension_tool_spec;
use crate::tools::hook_names::HookToolName;
use crate::tools::tool_dispatch_trace::ToolDispatchTrace;
use crate::util::error_or_panic;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::protocol::EventMsg;
use codex_tool_api::ToolBundle as ExtensionToolBundle;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use codex_utils_readiness::Readiness;
@@ -40,10 +37,6 @@ pub trait ToolHandler: Send + Sync {
/// The concrete tool name handled by this handler instance.
fn tool_name(&self) -> ToolName;
fn spec(&self) -> Option<ToolSpec> {
None
}
fn supports_parallel_tool_calls(&self) -> bool {
false
}
@@ -180,7 +173,7 @@ pub(crate) struct PostToolUsePayload {
pub(crate) tool_response: Value,
}
trait AnyToolHandler: Send + Sync {
pub(crate) trait AnyToolHandler: Send + Sync {
fn supports_parallel_tool_calls(&self) -> bool;
fn matches_kind(&self, payload: &ToolPayload) -> bool;
@@ -585,34 +578,22 @@ impl ToolRegistryBuilder {
return;
}
if let Some(spec) = handler.spec() {
self.push_spec(spec);
}
let handler: Arc<dyn AnyToolHandler> = handler;
self.handlers.insert(name, handler);
}
pub fn register_tool_bundle(&mut self, bundle: ExtensionToolBundle) {
let tool_name = ToolName::plain(bundle.tool_name());
pub(crate) fn register_erased_handler(
&mut self,
tool_name: ToolName,
handler: Arc<dyn AnyToolHandler>,
) -> bool {
if self.handlers.contains_key(&tool_name) {
warn!("Skipping extension tool `{tool_name}`: handler already registered");
return;
warn!("Skipping tool handler `{tool_name}`: handler already registered");
return false;
}
let spec = match extension_tool_spec(bundle.spec()) {
Ok(spec) => spec,
Err(error) => {
error_or_panic(format!(
"failed to convert extension tool `{tool_name}` to a host spec: {error}"
));
return;
}
};
self.push_spec(spec.clone());
let handler: Arc<dyn AnyToolHandler> = Arc::new(BundledToolHandler::new(bundle, spec));
self.handlers.insert(tool_name, handler);
true
}
pub(crate) fn specs(&self) -> &[ToolSpec] {

View File

@@ -63,9 +63,10 @@ fn handler_looks_up_namespaced_aliases_explicitly() {
}
#[test]
fn register_handler_adds_handler_and_augments_specs_for_code_mode() {
fn handlers_and_specs_are_registered_explicitly() {
let mut builder = ToolRegistryBuilder::new(/*code_mode_enabled*/ true);
builder.register_handler(Arc::new(GetGoalHandler));
builder.push_spec(create_get_goal_tool());
let (specs, registry) = builder.build();

View File

@@ -15,13 +15,13 @@ use codex_protocol::models::LocalShellAction;
use codex_protocol::models::ResponseItem;
use codex_protocol::models::SearchToolCallParams;
use codex_protocol::models::ShellToolCallParams;
use codex_tool_api::ToolBundle as ExtensionToolBundle;
use codex_tool_api::ToolDefinition;
use codex_tool_api::ToolExecutor;
use codex_tools::DiscoverableTool;
use codex_tools::ResponsesApiNamespaceTool;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use codex_tools::ToolsConfig;
use std::collections::HashSet;
use std::sync::Arc;
use tokio_util::sync::CancellationToken;
use tracing::instrument;
@@ -46,7 +46,7 @@ pub(crate) struct ToolRouterParams<'a> {
pub(crate) deferred_mcp_tools: Option<Vec<ToolInfo>>,
pub(crate) unavailable_called_tools: Vec<ToolName>,
pub(crate) discoverable_tools: Option<Vec<DiscoverableTool>>,
pub(crate) extension_tool_bundles: Vec<ExtensionToolBundle>,
pub(crate) extension_tool_definitions: Vec<ToolDefinition<Arc<dyn ToolExecutor>>>,
pub(crate) dynamic_tools: &'a [DynamicToolSpec],
}
@@ -57,7 +57,7 @@ impl ToolRouter {
deferred_mcp_tools,
unavailable_called_tools,
discoverable_tools,
extension_tool_bundles,
extension_tool_definitions,
dynamic_tools,
} = params;
let builder = build_specs_with_discoverable_tools(
@@ -66,15 +66,10 @@ impl ToolRouter {
deferred_mcp_tools,
unavailable_called_tools,
discoverable_tools,
&extension_tool_bundles,
&extension_tool_definitions,
dynamic_tools,
);
let (specs, registry) = builder.build();
let deferred_dynamic_tools = dynamic_tools
.iter()
.filter(|tool| tool.defer_loading)
.map(|tool| ToolName::new(tool.namespace.clone(), tool.name.clone()))
.collect::<HashSet<_>>();
let model_visible_specs = specs
.iter()
.filter_map(|spec| {
@@ -84,7 +79,7 @@ impl ToolRouter {
return None;
}
filter_deferred_dynamic_tool_spec(spec.clone(), &deferred_dynamic_tools)
filter_deferred_tool_spec(spec.clone())
})
.collect();
@@ -251,7 +246,9 @@ impl ToolRouter {
}
}
pub(crate) fn extension_tool_bundles(session: &Session) -> Vec<ExtensionToolBundle> {
pub(crate) fn extension_tool_definitions(
session: &Session,
) -> Vec<ToolDefinition<Arc<dyn ToolExecutor>>> {
session
.services
.extensions
@@ -266,28 +263,18 @@ pub(crate) fn extension_tool_bundles(session: &Session) -> Vec<ExtensionToolBund
.collect()
}
fn filter_deferred_dynamic_tool_spec(
spec: ToolSpec,
deferred_dynamic_tools: &HashSet<ToolName>,
) -> Option<ToolSpec> {
if deferred_dynamic_tools.is_empty() {
return Some(spec);
}
fn filter_deferred_tool_spec(spec: ToolSpec) -> Option<ToolSpec> {
match spec {
ToolSpec::Function(tool) => {
if deferred_dynamic_tools.contains(&ToolName::plain(tool.name.as_str())) {
if tool.defer_loading == Some(true) {
None
} else {
Some(ToolSpec::Function(tool))
}
}
ToolSpec::Namespace(mut namespace) => {
let namespace_name = namespace.name.clone();
namespace.tools.retain(|tool| match tool {
ResponsesApiNamespaceTool::Function(tool) => !deferred_dynamic_tools.contains(
&ToolName::namespaced(namespace_name.as_str(), tool.name.as_str()),
),
ResponsesApiNamespaceTool::Function(tool) => tool.defer_loading != Some(true),
});
if namespace.tools.is_empty() {
None

View File

@@ -8,7 +8,7 @@ use codex_extension_api::ExtensionData;
use codex_extension_api::ExtensionRegistry;
use codex_extension_api::ExtensionRegistryBuilder;
use codex_extension_api::FunctionToolSpec;
use codex_extension_api::ToolBundle;
use codex_extension_api::ToolDefinition;
use codex_extension_api::ToolExecutor;
use codex_extension_api::ToolFuture;
use codex_protocol::dynamic_tools::DynamicToolSpec;
@@ -27,7 +27,7 @@ use super::ToolCall;
use super::ToolCallSource;
use super::ToolRouter;
use super::ToolRouterParams;
use super::extension_tool_bundles;
use super::extension_tool_definitions;
struct ExtensionEchoContributor;
@@ -36,8 +36,8 @@ impl codex_extension_api::ToolContributor for ExtensionEchoContributor {
&self,
_session_store: &ExtensionData,
_thread_store: &ExtensionData,
) -> Vec<ToolBundle> {
vec![ToolBundle::new(
) -> Vec<ToolDefinition<Arc<dyn ToolExecutor>>> {
vec![ToolDefinition::from_function_spec(
FunctionToolSpec {
name: "extension_echo".to_string(),
description: "Echoes arguments through an extension tool.".to_string(),
@@ -99,7 +99,7 @@ async fn parallel_support_does_not_match_namespaced_local_tool_names() -> anyhow
mcp_tools: Some(mcp_tools),
unavailable_called_tools: Vec::new(),
discoverable_tools: None,
extension_tool_bundles: Vec::new(),
extension_tool_definitions: Vec::new(),
dynamic_tools: turn.dynamic_tools.as_slice(),
},
);
@@ -179,7 +179,7 @@ async fn mcp_parallel_support_uses_handler_data() -> anyhow::Result<()> {
]),
unavailable_called_tools: Vec::new(),
discoverable_tools: None,
extension_tool_bundles: Vec::new(),
extension_tool_definitions: Vec::new(),
dynamic_tools: turn.dynamic_tools.as_slice(),
},
);
@@ -215,7 +215,7 @@ async fn tools_without_handlers_do_not_support_parallel() -> anyhow::Result<()>
mcp_tools: None,
unavailable_called_tools: Vec::new(),
discoverable_tools: None,
extension_tool_bundles: Vec::new(),
extension_tool_definitions: Vec::new(),
dynamic_tools: turn.dynamic_tools.as_slice(),
},
);
@@ -268,7 +268,7 @@ async fn model_visible_specs_filter_deferred_dynamic_tools() -> anyhow::Result<(
mcp_tools: None,
unavailable_called_tools: Vec::new(),
discoverable_tools: None,
extension_tool_bundles: Vec::new(),
extension_tool_definitions: Vec::new(),
dynamic_tools: &dynamic_tools,
},
);
@@ -323,7 +323,7 @@ fn mcp_tool_info(
}
#[tokio::test]
async fn extension_tool_bundles_are_model_visible_and_dispatchable() -> anyhow::Result<()> {
async fn extension_tool_definitions_are_model_visible_and_dispatchable() -> anyhow::Result<()> {
let (mut session, turn) = make_session_and_context().await;
session.services.extensions = extension_tool_test_registry();
@@ -334,7 +334,7 @@ async fn extension_tool_bundles_are_model_visible_and_dispatchable() -> anyhow::
mcp_tools: None,
unavailable_called_tools: Vec::new(),
discoverable_tools: None,
extension_tool_bundles: extension_tool_bundles(&session),
extension_tool_definitions: extension_tool_definitions(&session),
dynamic_tools: turn.dynamic_tools.as_slice(),
},
);

View File

@@ -0,0 +1,22 @@
use std::sync::Arc;
use codex_tool_api::ToolDefinition;
use codex_tools::ToolSpec;
use crate::tools::registry::AnyToolHandler;
use crate::tools::registry::ToolHandler;
pub(crate) type RuntimeToolDefinition = ToolDefinition<Arc<dyn AnyToolHandler>, ToolSpec>;
pub(crate) fn runtime_tool_definition<H>(
handler: H,
spec: ToolSpec,
) -> RuntimeToolDefinition
where
H: ToolHandler + 'static,
{
let handler = Arc::new(handler);
let tool_name = handler.tool_name();
let runtime: Arc<dyn AnyToolHandler> = handler;
ToolDefinition::new(tool_name, spec, runtime)
}

View File

@@ -11,11 +11,9 @@ use crate::tools::spec_plan_types::ToolNamespace;
use crate::tools::spec_plan_types::ToolRegistryBuildParams;
use codex_mcp::ToolInfo;
use codex_protocol::dynamic_tools::DynamicToolSpec;
use codex_tool_api::ToolBundle as ExtensionToolBundle;
use codex_tools::AdditionalProperties;
use codex_tool_api::ToolDefinition;
use codex_tool_api::ToolExecutor;
use codex_tools::DiscoverableTool;
use codex_tools::JsonSchema;
use codex_tools::ResponsesApiTool;
use codex_tools::ToolName;
use codex_tools::ToolUserShellType;
use codex_tools::ToolsConfig;
@@ -62,13 +60,10 @@ pub(crate) fn build_specs_with_discoverable_tools(
deferred_mcp_tools: Option<Vec<ToolInfo>>,
unavailable_called_tools: Vec<ToolName>,
discoverable_tools: Option<Vec<DiscoverableTool>>,
extension_tool_bundles: &[ExtensionToolBundle],
extension_tool_definitions: &[ToolDefinition<Arc<dyn ToolExecutor>>],
dynamic_tools: &[DynamicToolSpec],
) -> ToolRegistryBuilder {
use crate::tools::handlers::UnavailableToolHandler;
use crate::tools::handlers::unavailable_tool_message;
use crate::tools::tool_search_entry::build_tool_search_entries_for_config;
let mcp_tool_plan_inputs = mcp_tools.as_deref().map(map_mcp_tools_for_plan);
let deferred_mcp_tool_sources = deferred_mcp_tools.as_deref();
let default_agent_type_description =
@@ -83,16 +78,6 @@ pub(crate) fn build_specs_with_discoverable_tools(
};
let default_wait_timeout_ms =
DEFAULT_WAIT_TIMEOUT_MS.clamp(min_wait_timeout_ms, MAX_WAIT_TIMEOUT_MS);
let deferred_dynamic_tools = dynamic_tools
.iter()
.filter(|tool| tool.defer_loading && (config.namespace_tools || tool.namespace.is_none()))
.cloned()
.collect::<Vec<_>>();
let tool_search_entries = build_tool_search_entries_for_config(
config,
deferred_mcp_tool_sources,
&deferred_dynamic_tools,
);
let mut builder = build_tool_registry_builder(
config,
ToolRegistryBuildParams {
@@ -104,7 +89,7 @@ pub(crate) fn build_specs_with_discoverable_tools(
.as_ref()
.map(|inputs| &inputs.tool_namespaces),
discoverable_tools: discoverable_tools.as_deref(),
extension_tool_bundles,
extension_tool_definitions,
dynamic_tools,
default_agent_type_description: &default_agent_type_description,
wait_agent_timeouts: WaitAgentTimeoutOptions {
@@ -112,7 +97,6 @@ pub(crate) fn build_specs_with_discoverable_tools(
min_timeout_ms: min_wait_timeout_ms,
max_timeout_ms: MAX_WAIT_TIMEOUT_MS,
},
tool_search_entries: &tool_search_entries,
},
);
let mut existing_spec_names = builder
@@ -124,29 +108,15 @@ pub(crate) fn build_specs_with_discoverable_tools(
for unavailable_tool in unavailable_called_tools {
let tool_name = flat_tool_name(&unavailable_tool).into_owned();
if existing_spec_names.insert(tool_name.clone()) {
let spec = codex_tools::ToolSpec::Function(ResponsesApiTool {
name: tool_name.clone(),
description: unavailable_tool_message(
&tool_name,
"Calling this placeholder returns an error explaining that the tool is unavailable.",
),
strict: false,
parameters: JsonSchema::object(
Default::default(),
/*required*/ None,
Some(AdditionalProperties::Boolean(false)),
),
output_schema: None,
defer_loading: None,
});
builder.register_handler(Arc::new(UnavailableToolHandler::new(
unavailable_tool,
spec,
)));
let definition = UnavailableToolHandler::definition(unavailable_tool);
if builder.register_erased_handler(
definition.tool_name().clone(),
Arc::clone(definition.runtime()),
) {
builder.push_spec(definition.spec().clone());
}
} else {
builder.register_handler(Arc::new(UnavailableToolHandler::without_spec(
unavailable_tool,
)));
builder.register_handler(Arc::new(UnavailableToolHandler::new(unavailable_tool)));
}
}
builder

View File

@@ -1,4 +1,3 @@
use crate::tools::code_mode::execute_spec::create_code_mode_tool;
use crate::tools::handlers::ApplyPatchHandler;
use crate::tools::handlers::CodeModeExecuteHandler;
use crate::tools::handlers::CodeModeWaitHandler;
@@ -6,7 +5,6 @@ use crate::tools::handlers::ContainerExecHandler;
use crate::tools::handlers::CreateGoalHandler;
use crate::tools::handlers::DynamicToolHandler;
use crate::tools::handlers::ExecCommandHandler;
use crate::tools::handlers::ExecCommandHandlerOptions;
use crate::tools::handlers::GetGoalHandler;
use crate::tools::handlers::ListMcpResourceTemplatesHandler;
use crate::tools::handlers::ListMcpResourcesHandler;
@@ -18,7 +16,6 @@ use crate::tools::handlers::RequestPermissionsHandler;
use crate::tools::handlers::RequestPluginInstallHandler;
use crate::tools::handlers::RequestUserInputHandler;
use crate::tools::handlers::ShellCommandHandler;
use crate::tools::handlers::ShellCommandHandlerOptions;
use crate::tools::handlers::ShellHandler;
use crate::tools::handlers::TestSyncHandler;
use crate::tools::handlers::ToolSearchHandler;
@@ -27,6 +24,7 @@ use crate::tools::handlers::ViewImageHandler;
use crate::tools::handlers::WriteStdinHandler;
use crate::tools::handlers::agent_jobs::ReportAgentJobResultHandler;
use crate::tools::handlers::agent_jobs::SpawnAgentsOnCsvHandler;
use crate::tools::handlers::extension_tools::ExtensionToolHandler;
use crate::tools::handlers::multi_agents::CloseAgentHandler;
use crate::tools::handlers::multi_agents::ResumeAgentHandler;
use crate::tools::handlers::multi_agents::SendInputHandler;
@@ -39,30 +37,39 @@ use crate::tools::handlers::multi_agents_v2::ListAgentsHandler as ListAgentsHand
use crate::tools::handlers::multi_agents_v2::SendMessageHandler as SendMessageHandlerV2;
use crate::tools::handlers::multi_agents_v2::SpawnAgentHandler as SpawnAgentHandlerV2;
use crate::tools::handlers::multi_agents_v2::WaitAgentHandler as WaitAgentHandlerV2;
use crate::tools::handlers::shell_spec::CommandToolOptions;
use crate::tools::handlers::shell_spec::ShellToolOptions;
use crate::tools::handlers::view_image_spec::ViewImageToolOptions;
use crate::tools::hosted_spec::WebSearchToolOptions;
use crate::tools::hosted_spec::create_image_generation_tool;
use crate::tools::hosted_spec::create_web_search_tool;
use crate::tools::registry::AnyToolHandler;
use crate::tools::registry::ToolRegistryBuilder;
use crate::tools::runtime_definition::RuntimeToolDefinition;
use crate::tools::spec_plan_types::ToolNamespace;
use crate::tools::spec_plan_types::ToolRegistryBuildParams;
use crate::tools::spec_plan_types::agent_type_description;
use crate::tools::tool_search_entry::ToolSearchEntry;
use crate::tools::tool_search_entry::dynamic_tool_search_entry;
use crate::tools::tool_search_entry::mcp_tool_search_entry;
use codex_mcp::ToolInfo;
use codex_protocol::openai_models::ConfigShellToolType;
use codex_tools::ResponsesApiNamespace;
use codex_tools::ResponsesApiNamespaceTool;
use codex_tool_api::ToolDefinition;
use codex_tool_api::ToolExecutor;
use codex_tool_api::ToolExposure;
use codex_tools::ToolEnvironmentMode;
use codex_tools::ToolName;
use codex_tools::ToolSearchSource;
use codex_tools::ToolSearchSourceInfo;
use codex_tools::ToolSpec;
use codex_tools::ToolsConfig;
use codex_tools::coalesce_loadable_tool_specs;
use codex_tools::collect_code_mode_exec_prompt_tool_definitions;
use codex_tools::collect_request_plugin_install_entries;
use codex_tools::collect_tool_search_source_infos;
use codex_tools::default_namespace_description;
use codex_tools::dynamic_tool_to_loadable_tool_spec;
use codex_tools::mcp_tool_to_responses_api_tool;
use codex_tools::mcp_tool_definition;
use codex_tools::parse_dynamic_tool;
use codex_tools::tool_definition_to_loadable_tool_spec;
use std::collections::BTreeMap;
use std::collections::HashSet;
use std::sync::Arc;
@@ -73,6 +80,16 @@ pub fn build_tool_registry_builder(
) -> ToolRegistryBuilder {
let mut builder = ToolRegistryBuilder::new(config.code_mode_enabled);
let exec_permission_approvals_enabled = config.exec_permission_approvals_enabled;
let mcp_definitions = mcp_tool_definitions(params.mcp_tools);
let dynamic_definitions = dynamic_tool_definitions(params.dynamic_tools);
let deferred_mcp_definitions = deferred_mcp_tool_definitions(params.deferred_mcp_tools);
let tool_search_plan = build_tool_search_plan(
config,
params.deferred_mcp_tools,
&deferred_mcp_definitions,
params.dynamic_tools,
&dynamic_definitions,
);
if config.code_mode_enabled {
let namespace_descriptions = params
@@ -101,18 +118,21 @@ pub fn build_tool_registry_builder(
collect_code_mode_exec_prompt_tool_definitions(nested_builder.specs().iter());
enabled_tools
.sort_by(|left, right| compare_code_mode_tools(left, right, &namespace_descriptions));
builder.register_handler(Arc::new(CodeModeExecuteHandler::new(
create_code_mode_tool(
register_and_publish_tool_definition(
&mut builder,
config,
CodeModeExecuteHandler::definition(
&enabled_tools,
&namespace_descriptions,
config.code_mode_only_enabled,
config.search_tool
&& params
.deferred_mcp_tools
.is_some_and(|tools| !tools.is_empty()),
tool_search_plan.deferred_tools_available(),
),
)));
builder.register_handler(Arc::new(CodeModeWaitHandler));
);
register_and_publish_tool_definition(
&mut builder,
config,
CodeModeWaitHandler::definition(),
);
}
if config.environment_mode.has_environment() {
@@ -120,32 +140,54 @@ pub fn build_tool_registry_builder(
matches!(config.environment_mode, ToolEnvironmentMode::Multiple);
match &config.shell_type {
ConfigShellToolType::Default => {
builder.register_handler(Arc::new(ShellHandler::new(ShellToolOptions {
let shell_options = ShellToolOptions {
exec_permission_approvals_enabled,
})));
};
register_and_publish_tool_definition(
&mut builder,
config,
ShellHandler::definition(shell_options),
);
}
ConfigShellToolType::Local => {
builder.register_handler(Arc::new(LocalShellHandler::new()));
register_and_publish_tool_definition(
&mut builder,
config,
LocalShellHandler::definition(),
);
}
ConfigShellToolType::UnifiedExec => {
builder.register_handler(Arc::new(ExecCommandHandler::new(
ExecCommandHandlerOptions {
allow_login_shell: config.allow_login_shell,
exec_permission_approvals_enabled,
register_and_publish_tool_definition(
&mut builder,
config,
ExecCommandHandler::definition(
CommandToolOptions {
allow_login_shell: config.allow_login_shell,
exec_permission_approvals_enabled,
},
include_environment_id,
},
)));
builder.register_handler(Arc::new(WriteStdinHandler));
),
);
register_and_publish_tool_definition(
&mut builder,
config,
WriteStdinHandler::definition(),
);
}
ConfigShellToolType::Disabled => {}
ConfigShellToolType::ShellCommand => {
builder.register_handler(Arc::new(ShellCommandHandler::new(
ShellCommandHandlerOptions {
backend_config: config.shell_command_backend,
allow_login_shell: config.allow_login_shell,
exec_permission_approvals_enabled,
},
)));
let shell_command_options = CommandToolOptions {
allow_login_shell: config.allow_login_shell,
exec_permission_approvals_enabled,
};
register_and_publish_tool_definition(
&mut builder,
config,
ShellCommandHandler::definition(
config.shell_command_backend,
shell_command_options,
),
);
}
}
}
@@ -186,78 +228,94 @@ pub fn build_tool_registry_builder(
}
if params.mcp_tools.is_some() {
builder.register_handler(Arc::new(ListMcpResourcesHandler));
builder.register_handler(Arc::new(ListMcpResourceTemplatesHandler));
builder.register_handler(Arc::new(ReadMcpResourceHandler));
register_and_publish_tool_definition(
&mut builder,
config,
ListMcpResourcesHandler::definition(),
);
register_and_publish_tool_definition(
&mut builder,
config,
ListMcpResourceTemplatesHandler::definition(),
);
register_and_publish_tool_definition(
&mut builder,
config,
ReadMcpResourceHandler::definition(),
);
}
builder.register_handler(Arc::new(PlanHandler));
register_and_publish_tool_definition(
&mut builder,
config,
PlanHandler::definition(),
);
if config.goal_tools {
builder.register_handler(Arc::new(GetGoalHandler));
builder.register_handler(Arc::new(CreateGoalHandler));
builder.register_handler(Arc::new(UpdateGoalHandler));
register_and_publish_tool_definition(
&mut builder,
config,
GetGoalHandler::definition(),
);
register_and_publish_tool_definition(
&mut builder,
config,
CreateGoalHandler::definition(),
);
register_and_publish_tool_definition(
&mut builder,
config,
UpdateGoalHandler::definition(),
);
}
builder.register_handler(Arc::new(RequestUserInputHandler {
available_modes: config.request_user_input_available_modes.clone(),
}));
let available_modes = config.request_user_input_available_modes.clone();
register_and_publish_tool_definition(
&mut builder,
config,
RequestUserInputHandler::definition(available_modes),
);
if config.request_permissions_tool_enabled {
builder.register_handler(Arc::new(RequestPermissionsHandler));
register_and_publish_tool_definition(
&mut builder,
config,
RequestPermissionsHandler::definition(),
);
}
let deferred_dynamic_tools = params
.dynamic_tools
.iter()
.filter(|tool| tool.defer_loading && (config.namespace_tools || tool.namespace.is_none()))
.collect::<Vec<_>>();
let deferred_mcp_tools_for_search = if config.namespace_tools {
params.deferred_mcp_tools
} else {
None
};
if config.search_tool
&& (deferred_mcp_tools_for_search.is_some() || !deferred_dynamic_tools.is_empty())
{
let mut search_source_infos = deferred_mcp_tools_for_search
.map(|deferred_mcp_tools| {
collect_tool_search_source_infos(deferred_mcp_tools.iter().map(|tool| {
ToolSearchSource {
server_name: tool.server_name.as_str(),
connector_name: tool.connector_name.as_deref(),
description: tool.namespace_description.as_deref(),
}
}))
})
.unwrap_or_default();
if !deferred_dynamic_tools.is_empty() {
search_source_infos.push(ToolSearchSourceInfo {
name: "Dynamic tools".to_string(),
description: Some("Tools provided by the current Codex thread.".to_string()),
});
}
builder.register_handler(Arc::new(ToolSearchHandler::new(
params.tool_search_entries.to_vec(),
search_source_infos,
)));
if tool_search_plan.should_register() {
let ToolSearchPlan {
entries,
source_infos,
..
} = tool_search_plan;
register_and_publish_tool_definition(
&mut builder,
config,
ToolSearchHandler::definition(entries, source_infos),
);
}
if config.tool_suggest
&& let Some(discoverable_tools) =
params.discoverable_tools.filter(|tools| !tools.is_empty())
{
builder.register_handler(Arc::new(RequestPluginInstallHandler::new(
discoverable_tools,
)));
let discoverable_tool_entries = collect_request_plugin_install_entries(discoverable_tools);
register_and_publish_tool_definition(
&mut builder,
config,
RequestPluginInstallHandler::definition(&discoverable_tool_entries),
);
}
if config.environment_mode.has_environment() && config.apply_patch_tool_type.is_some() {
let include_environment_id =
matches!(config.environment_mode, ToolEnvironmentMode::Multiple);
builder.register_handler(Arc::new(ApplyPatchHandler::new(include_environment_id)));
register_and_publish_tool_definition(
&mut builder,
config,
ApplyPatchHandler::definition(include_environment_id),
);
}
if config
@@ -265,7 +323,11 @@ pub fn build_tool_registry_builder(
.iter()
.any(|tool| tool == "test_sync_tool")
{
builder.register_handler(Arc::new(TestSyncHandler));
register_and_publish_tool_definition(
&mut builder,
config,
TestSyncHandler::definition(),
);
}
if let Some(web_search_tool) = create_web_search_tool(WebSearchToolOptions {
@@ -283,158 +345,422 @@ pub fn build_tool_registry_builder(
if config.environment_mode.has_environment() {
let include_environment_id =
matches!(config.environment_mode, ToolEnvironmentMode::Multiple);
builder.register_handler(Arc::new(ViewImageHandler::new(ViewImageToolOptions {
let view_image_options = ViewImageToolOptions {
can_request_original_image_detail: config.can_request_original_image_detail,
include_environment_id,
})));
};
register_and_publish_tool_definition(
&mut builder,
config,
ViewImageHandler::definition(view_image_options),
);
}
if config.collab_tools {
if config.multi_agent_v2 {
let agent_type_description =
agent_type_description(config, params.default_agent_type_description);
builder.register_handler(Arc::new(SpawnAgentHandlerV2::new(SpawnAgentToolOptions {
let spawn_agent_options = SpawnAgentToolOptions {
available_models: config.available_models.clone(),
agent_type_description,
hide_agent_type_model_reasoning: config.hide_spawn_agent_metadata,
include_usage_hint: config.spawn_agent_usage_hint,
usage_hint_text: config.spawn_agent_usage_hint_text.clone(),
max_concurrent_threads_per_session: config.max_concurrent_threads_per_session,
})));
builder.register_handler(Arc::new(SendMessageHandlerV2));
builder.register_handler(Arc::new(FollowupTaskHandlerV2));
builder.register_handler(Arc::new(WaitAgentHandlerV2::new(
params.wait_agent_timeouts,
)));
builder.register_handler(Arc::new(CloseAgentHandlerV2));
builder.register_handler(Arc::new(ListAgentsHandlerV2));
};
register_and_publish_tool_definition(
&mut builder,
config,
SpawnAgentHandlerV2::definition(spawn_agent_options),
);
register_and_publish_tool_definition(
&mut builder,
config,
SendMessageHandlerV2::definition(),
);
register_and_publish_tool_definition(
&mut builder,
config,
FollowupTaskHandlerV2::definition(),
);
register_and_publish_tool_definition(
&mut builder,
config,
WaitAgentHandlerV2::definition(params.wait_agent_timeouts),
);
register_and_publish_tool_definition(
&mut builder,
config,
CloseAgentHandlerV2::definition(),
);
register_and_publish_tool_definition(
&mut builder,
config,
ListAgentsHandlerV2::definition(),
);
} else {
let agent_type_description =
agent_type_description(config, params.default_agent_type_description);
builder.register_handler(Arc::new(SpawnAgentHandler::new(SpawnAgentToolOptions {
let spawn_agent_options = SpawnAgentToolOptions {
available_models: config.available_models.clone(),
agent_type_description,
hide_agent_type_model_reasoning: config.hide_spawn_agent_metadata,
include_usage_hint: config.spawn_agent_usage_hint,
usage_hint_text: config.spawn_agent_usage_hint_text.clone(),
max_concurrent_threads_per_session: config.max_concurrent_threads_per_session,
})));
builder.register_handler(Arc::new(SendInputHandler));
builder.register_handler(Arc::new(ResumeAgentHandler));
builder.register_handler(Arc::new(WaitAgentHandler::new(params.wait_agent_timeouts)));
builder.register_handler(Arc::new(CloseAgentHandler));
};
register_and_publish_tool_definition(
&mut builder,
config,
SpawnAgentHandler::definition(spawn_agent_options),
);
register_and_publish_tool_definition(
&mut builder,
config,
SendInputHandler::definition(),
);
register_and_publish_tool_definition(
&mut builder,
config,
ResumeAgentHandler::definition(),
);
register_and_publish_tool_definition(
&mut builder,
config,
WaitAgentHandler::definition(params.wait_agent_timeouts),
);
register_and_publish_tool_definition(
&mut builder,
config,
CloseAgentHandler::definition(),
);
}
}
if config.agent_jobs_tools {
builder.register_handler(Arc::new(SpawnAgentsOnCsvHandler));
register_and_publish_tool_definition(
&mut builder,
config,
SpawnAgentsOnCsvHandler::definition(),
);
if config.agent_jobs_worker_tools {
builder.register_handler(Arc::new(ReportAgentJobResultHandler));
register_and_publish_tool_definition(
&mut builder,
config,
ReportAgentJobResultHandler::definition(),
);
}
}
if let Some(mcp_tools) = params.mcp_tools {
let mut entries = mcp_tools
.iter()
.map(|tool| (tool.canonical_tool_name(), tool))
.collect::<Vec<_>>();
entries.sort_by(|(left, _), (right, _)| left.cmp(right));
let mut namespace_entries = BTreeMap::new();
register_and_publish_function_tool_definitions(
&mut builder,
config,
params.tool_namespaces,
mcp_definitions
.into_iter()
.chain(dynamic_definitions)
.chain(extension_tool_definitions(
params.extension_tool_definitions,
)),
);
for (tool_name, tool) in entries {
let Some(namespace) = tool_name.namespace.as_ref() else {
tracing::error!("Skipping MCP tool `{tool_name}`: MCP tools must be namespaced");
continue;
};
namespace_entries
.entry(namespace.clone())
.or_insert_with(Vec::new)
.push((tool_name, tool));
}
for (namespace, mut entries) in namespace_entries {
entries.sort_by_key(|(tool_name, _)| tool_name.name.clone());
let tool_namespace = params
.tool_namespaces
.and_then(|namespaces| namespaces.get(&namespace));
let description = tool_namespace
.and_then(|namespace| namespace.description.as_deref())
.map(str::trim)
.filter(|description| !description.is_empty())
.map(str::to_string)
.unwrap_or_else(|| {
let namespace_name = tool_namespace
.map(|namespace| namespace.name.as_str())
.unwrap_or(namespace.as_str());
default_namespace_description(namespace_name)
});
let mut tools = Vec::new();
for (tool_name, tool) in entries {
match mcp_tool_to_responses_api_tool(&tool_name, &tool.tool) {
Ok(converted_tool) => {
tools.push(ResponsesApiNamespaceTool::Function(converted_tool));
builder.register_handler(Arc::new(McpHandler::new(tool.clone())));
}
Err(error) => {
tracing::error!(
"Failed to convert `{tool_name}` MCP tool to OpenAI tool: {error:?}"
);
}
}
}
if config.namespace_tools && !tools.is_empty() {
builder.push_spec(ToolSpec::Namespace(ResponsesApiNamespace {
name: namespace,
description,
tools,
}));
}
}
}
let mut dynamic_tool_specs = Vec::new();
for tool in params.dynamic_tools {
match dynamic_tool_to_loadable_tool_spec(tool) {
Ok(loadable_tool) => {
let handler_name = ToolName::new(tool.namespace.clone(), tool.name.clone());
dynamic_tool_specs.push(loadable_tool);
builder.register_handler(Arc::new(DynamicToolHandler::new(handler_name)));
}
Err(error) => {
tracing::error!(
"Failed to convert dynamic tool {:?} to OpenAI tool: {error:?}",
tool.name
);
}
}
}
for spec in coalesce_loadable_tool_specs(dynamic_tool_specs) {
let spec = spec.into();
if config.namespace_tools || !matches!(spec, ToolSpec::Namespace(_)) {
builder.push_spec(spec);
}
}
if let Some(deferred_mcp_tools) = params.deferred_mcp_tools {
if !deferred_mcp_definitions.is_empty() {
let directly_registered_mcp_tools = params
.mcp_tools
.into_iter()
.flatten()
.map(ToolInfo::canonical_tool_name)
.collect::<HashSet<_>>();
for tool in deferred_mcp_tools {
if !directly_registered_mcp_tools.contains(&tool.canonical_tool_name()) {
builder.register_handler(Arc::new(McpHandler::new(tool.clone())));
for definition in deferred_mcp_definitions {
if !directly_registered_mcp_tools.contains(definition.tool_name()) {
register_tool_definition_handler(&mut builder, &definition);
}
}
}
for bundle in params.extension_tool_bundles.iter().cloned() {
builder.register_tool_bundle(bundle);
builder
}
type FunctionRuntimeToolDefinition = ToolDefinition<Arc<dyn AnyToolHandler>>;
struct ToolSearchPlan {
register: bool,
entries: Vec<ToolSearchEntry>,
source_infos: Vec<ToolSearchSourceInfo>,
}
impl ToolSearchPlan {
fn should_register(&self) -> bool {
self.register
}
builder
fn deferred_tools_available(&self) -> bool {
self.register && !self.entries.is_empty()
}
}
fn build_tool_search_plan(
config: &ToolsConfig,
deferred_mcp_tools: Option<&[ToolInfo]>,
deferred_mcp_definitions: &[FunctionRuntimeToolDefinition],
dynamic_tools: &[codex_protocol::dynamic_tools::DynamicToolSpec],
dynamic_definitions: &[FunctionRuntimeToolDefinition],
) -> ToolSearchPlan {
debug_assert_eq!(
deferred_mcp_tools.map_or(0, <[codex_mcp::ToolInfo]>::len),
deferred_mcp_definitions.len()
);
debug_assert_eq!(dynamic_tools.len(), dynamic_definitions.len());
let mut entries = Vec::new();
let mcp_search_enabled = config.namespace_tools && deferred_mcp_tools.is_some();
let deferred_mcp_tools = deferred_mcp_tools.unwrap_or_default();
if config.namespace_tools {
let mut searchable_mcp_tools = deferred_mcp_tools
.iter()
.zip(deferred_mcp_definitions.iter())
.collect::<Vec<_>>();
searchable_mcp_tools.sort_by_key(|(tool, _)| tool.canonical_tool_name());
for (tool, definition) in searchable_mcp_tools {
match mcp_tool_search_entry(tool, definition) {
Ok(entry) => entries.push(entry),
Err(error) => {
let tool_name = tool.canonical_tool_name();
tracing::error!(
"Failed to convert deferred MCP tool `{tool_name}` to OpenAI tool: {error:?}"
);
}
}
}
}
let mut searchable_dynamic_tools = dynamic_tools
.iter()
.zip(dynamic_definitions.iter())
.filter(|(_, definition)| {
matches!(definition.exposure(), ToolExposure::Deferred)
&& (config.namespace_tools || definition.tool_name().namespace.is_none())
})
.collect::<Vec<_>>();
let has_searchable_dynamic_tools = !searchable_dynamic_tools.is_empty();
searchable_dynamic_tools.sort_by(|(left, _), (right, _)| {
left.namespace
.cmp(&right.namespace)
.then(left.name.cmp(&right.name))
});
for (tool, definition) in searchable_dynamic_tools {
match dynamic_tool_search_entry(tool, definition) {
Ok(entry) => entries.push(entry),
Err(error) => {
tracing::error!(
"Failed to convert deferred dynamic tool {:?} to OpenAI tool: {error:?}",
tool.name
);
}
}
}
let mut source_infos = if config.namespace_tools {
collect_tool_search_source_infos(deferred_mcp_tools.iter().map(|tool| ToolSearchSource {
server_name: tool.server_name.as_str(),
connector_name: tool.connector_name.as_deref(),
description: tool.namespace_description.as_deref(),
}))
} else {
Vec::new()
};
if has_searchable_dynamic_tools {
source_infos.push(ToolSearchSourceInfo {
name: "Dynamic tools".to_string(),
description: Some("Tools provided by the current Codex thread.".to_string()),
});
}
ToolSearchPlan {
register: config.search_tool && (mcp_search_enabled || has_searchable_dynamic_tools),
entries,
source_infos,
}
}
fn register_and_publish_tool_definition(
builder: &mut ToolRegistryBuilder,
config: &ToolsConfig,
definition: RuntimeToolDefinition,
) {
register_and_publish_tool_definitions(builder, config, std::iter::once(definition));
}
fn register_and_publish_tool_definitions(
builder: &mut ToolRegistryBuilder,
config: &ToolsConfig,
definitions: impl IntoIterator<Item = RuntimeToolDefinition>,
) {
let mut specs = Vec::new();
for definition in definitions {
if register_tool_definition_handler(builder, &definition) {
specs.push(definition.spec().clone());
}
}
for spec in coalesce_tool_specs(specs) {
if config.namespace_tools || !matches!(spec, ToolSpec::Namespace(_)) {
builder.push_spec(spec);
}
}
}
fn register_and_publish_function_tool_definitions(
builder: &mut ToolRegistryBuilder,
config: &ToolsConfig,
tool_namespaces: Option<&std::collections::HashMap<String, ToolNamespace>>,
definitions: impl IntoIterator<Item = FunctionRuntimeToolDefinition>,
) {
register_and_publish_tool_definitions(
builder,
config,
definitions
.into_iter()
.filter_map(|definition| render_function_tool_definition(definition, tool_namespaces)),
);
}
fn render_function_tool_definition(
definition: FunctionRuntimeToolDefinition,
tool_namespaces: Option<&std::collections::HashMap<String, ToolNamespace>>,
) -> Option<RuntimeToolDefinition> {
let tool_name = definition.tool_name().clone();
let namespace_description = namespace_description_for_tool(&tool_name, tool_namespaces);
let spec = match tool_definition_to_loadable_tool_spec(&definition, namespace_description) {
Ok(spec) => ToolSpec::from(spec),
Err(error) => {
tracing::error!("Failed to convert tool `{tool_name}` to OpenAI tool: {error:?}");
return None;
}
};
let exposure = definition.exposure();
let mut rendered = ToolDefinition::new(tool_name, spec, Arc::clone(definition.runtime()));
if matches!(exposure, ToolExposure::Deferred) {
rendered = rendered.deferred();
}
Some(rendered)
}
fn coalesce_tool_specs(specs: Vec<ToolSpec>) -> Vec<ToolSpec> {
let mut coalesced_specs = Vec::new();
for spec in specs {
match spec {
ToolSpec::Namespace(mut namespace) => {
if let Some(existing_namespace) =
coalesced_specs.iter_mut().find_map(|spec| match spec {
ToolSpec::Namespace(existing_namespace)
if existing_namespace.name == namespace.name =>
{
Some(existing_namespace)
}
_ => None,
})
{
existing_namespace.tools.append(&mut namespace.tools);
} else {
coalesced_specs.push(ToolSpec::Namespace(namespace));
}
}
spec => coalesced_specs.push(spec),
}
}
coalesced_specs
}
fn register_tool_definition_handler<S>(
builder: &mut ToolRegistryBuilder,
definition: &ToolDefinition<Arc<dyn AnyToolHandler>, S>,
) -> bool {
builder.register_erased_handler(
definition.tool_name().clone(),
Arc::clone(definition.runtime()),
)
}
fn mcp_tool_definitions(mcp_tools: Option<&[ToolInfo]>) -> Vec<FunctionRuntimeToolDefinition> {
let mut tools = mcp_tools.into_iter().flatten().collect::<Vec<_>>();
tools.sort_by_key(|tool| tool.canonical_tool_name());
tools
.into_iter()
.filter_map(|tool| {
let tool_name = tool.canonical_tool_name();
if tool_name.namespace.is_none() {
tracing::error!("Skipping MCP tool `{tool_name}`: MCP tools must be namespaced");
return None;
}
Some(
mcp_tool_definition(tool_name, &tool.tool)
.with_runtime(
Arc::new(McpHandler::new(tool.clone())) as Arc<dyn AnyToolHandler>
),
)
})
.collect()
}
fn deferred_mcp_tool_definitions(
mcp_tools: Option<&[ToolInfo]>,
) -> Vec<FunctionRuntimeToolDefinition> {
mcp_tools
.into_iter()
.flatten()
.map(|tool| {
mcp_tool_definition(tool.canonical_tool_name(), &tool.tool)
.deferred()
.with_runtime(Arc::new(McpHandler::new(tool.clone())) as Arc<dyn AnyToolHandler>)
})
.collect()
}
fn dynamic_tool_definitions(
dynamic_tools: &[codex_protocol::dynamic_tools::DynamicToolSpec],
) -> Vec<FunctionRuntimeToolDefinition> {
dynamic_tools
.iter()
.map(|tool| {
let definition = parse_dynamic_tool(tool);
let handler = Arc::new(DynamicToolHandler::new(definition.tool_name().clone()))
as Arc<dyn AnyToolHandler>;
definition.with_runtime(handler)
})
.collect()
}
fn extension_tool_definitions(
definitions: &[ToolDefinition<Arc<dyn ToolExecutor>>],
) -> Vec<FunctionRuntimeToolDefinition> {
definitions
.iter()
.map(|definition| {
let handler =
Arc::new(ExtensionToolHandler::new(definition.clone())) as Arc<dyn AnyToolHandler>;
definition.clone().with_runtime(handler)
})
.collect()
}
fn namespace_description_for_tool(
tool_name: &ToolName,
tool_namespaces: Option<&std::collections::HashMap<String, ToolNamespace>>,
) -> Option<String> {
let namespace = tool_name.namespace.as_ref()?;
let tool_namespace = tool_namespaces.and_then(|namespaces| namespaces.get(namespace));
tool_namespace.map(|tool_namespace| {
tool_namespace
.description
.as_deref()
.map(str::trim)
.filter(|description| !description.is_empty())
.map(str::to_string)
.unwrap_or_else(|| default_namespace_description(tool_namespace.name.as_str()))
})
}
fn compare_code_mode_tools(

View File

@@ -43,7 +43,7 @@ use codex_protocol::openai_models::WebSearchToolType;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SubAgentSource;
use codex_tool_api::FunctionToolSpec;
use codex_tool_api::ToolBundle as ExtensionToolBundle;
use codex_tool_api::ToolDefinition;
use codex_tool_api::ToolExecutor;
use codex_tool_api::ToolFuture;
use codex_tools::AdditionalProperties;
@@ -83,8 +83,11 @@ impl ToolExecutor for UnusedExtensionExecutor {
}
}
fn extension_tool_bundle(name: &str, description: &str) -> ExtensionToolBundle {
ExtensionToolBundle::new(
fn extension_tool_definition(
name: &str,
description: &str,
) -> ToolDefinition<std::sync::Arc<dyn ToolExecutor>> {
ToolDefinition::from_function_spec(
FunctionToolSpec {
name: name.to_string(),
description: description.to_string(),
@@ -117,7 +120,7 @@ fn extension_tools_do_not_replace_builtin_tools() {
permission_profile: &PermissionProfile::Disabled,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
});
let extension_tool_bundles = vec![extension_tool_bundle(
let extension_tool_definitions = vec![extension_tool_definition(
"update_plan",
"Extension attempt to replace a built-in tool.",
)];
@@ -126,7 +129,7 @@ fn extension_tools_do_not_replace_builtin_tools() {
/*mcp_tools*/ None,
/*deferred_mcp_tools*/ None,
/*discoverable_tools*/ None,
&extension_tool_bundles,
&extension_tool_definitions,
&[],
);
@@ -1908,7 +1911,7 @@ fn request_plugin_install_is_not_registered_without_feature_flag() {
"Google Calendar",
"Plan events and schedules.",
)]),
/*extension_tool_bundles*/ &[],
/*extension_tool_definitions*/ &[],
&[],
);
@@ -1949,7 +1952,7 @@ fn request_plugin_install_can_be_registered_without_search_tool() {
"Google Calendar",
"Plan events and schedules.",
)]),
/*extension_tool_bundles*/ &[],
/*extension_tool_definitions*/ &[],
&[],
);
@@ -2014,7 +2017,7 @@ fn request_plugin_install_description_lists_discoverable_tools() {
/*mcp_tools*/ None,
/*deferred_mcp_tools*/ None,
Some(discoverable_tools),
/*extension_tool_bundles*/ &[],
/*extension_tool_definitions*/ &[],
&[],
);
assert!(registry.has_handler(&ToolName::plain(REQUEST_PLUGIN_INSTALL_TOOL_NAME)));
@@ -2307,7 +2310,7 @@ fn code_mode_only_exec_description_includes_extension_tool_details() {
windows_sandbox_level: WindowsSandboxLevel::Disabled,
});
let extension_tool_bundles = vec![extension_tool_bundle(
let extension_tool_definitions = vec![extension_tool_definition(
"extension_echo",
"Echoes arguments through an extension tool.",
)];
@@ -2316,7 +2319,7 @@ fn code_mode_only_exec_description_includes_extension_tool_details() {
/*mcp_tools*/ None,
/*deferred_mcp_tools*/ None,
/*discoverable_tools*/ None,
&extension_tool_bundles,
&extension_tool_definitions,
&[],
);
let ToolSpec::Freeform(FreeformTool { description, .. }) = find_tool(&tools, "exec") else {
@@ -2414,7 +2417,7 @@ fn build_specs(
mcp_tools,
deferred_mcp_tools,
/*discoverable_tools*/ None,
/*extension_tool_bundles*/ &[],
/*extension_tool_definitions*/ &[],
dynamic_tools,
)
}
@@ -2424,7 +2427,7 @@ fn build_specs_with_discoverable_tools(
mcp_tools: Option<HashMap<ToolName, rmcp::model::Tool>>,
deferred_mcp_tools: Option<Vec<ToolInfo>>,
discoverable_tools: Option<Vec<DiscoverableTool>>,
extension_tool_bundles: &[codex_tool_api::ToolBundle],
extension_tool_definitions: &[ToolDefinition<std::sync::Arc<dyn ToolExecutor>>],
dynamic_tools: &[DynamicToolSpec],
) -> (Vec<ToolSpec>, ToolRegistry) {
build_specs_with_optional_tool_namespaces(
@@ -2433,7 +2436,7 @@ fn build_specs_with_discoverable_tools(
deferred_mcp_tools,
/*tool_namespaces*/ None,
discoverable_tools,
extension_tool_bundles,
extension_tool_definitions,
dynamic_tools,
)
}
@@ -2444,7 +2447,7 @@ fn build_specs_with_optional_tool_namespaces(
deferred_mcp_tools: Option<Vec<ToolInfo>>,
tool_namespaces: Option<HashMap<String, ToolNamespace>>,
discoverable_tools: Option<Vec<DiscoverableTool>>,
extension_tool_bundles: &[codex_tool_api::ToolBundle],
extension_tool_definitions: &[ToolDefinition<std::sync::Arc<dyn ToolExecutor>>],
dynamic_tools: &[DynamicToolSpec],
) -> (Vec<ToolSpec>, ToolRegistry) {
let mcp_tool_inputs = mcp_tools.as_ref().map(|mcp_tools| {
@@ -2460,11 +2463,10 @@ fn build_specs_with_optional_tool_namespaces(
deferred_mcp_tools: deferred_mcp_tools.as_deref(),
tool_namespaces: tool_namespaces.as_ref(),
discoverable_tools: discoverable_tools.as_deref(),
extension_tool_bundles,
extension_tool_definitions,
dynamic_tools,
default_agent_type_description: DEFAULT_AGENT_TYPE_DESCRIPTION,
wait_agent_timeouts: wait_agent_timeout_options(),
tool_search_entries: &[],
},
);
builder.build()

View File

@@ -1,10 +1,12 @@
use crate::tools::handlers::multi_agents_spec::WaitAgentTimeoutOptions;
use codex_mcp::ToolInfo;
use codex_protocol::dynamic_tools::DynamicToolSpec;
use codex_tool_api::ToolBundle as ExtensionToolBundle;
use codex_tool_api::ToolDefinition;
use codex_tool_api::ToolExecutor;
use codex_tools::DiscoverableTool;
use codex_tools::ToolsConfig;
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Clone, Copy)]
pub struct ToolRegistryBuildParams<'a> {
@@ -12,11 +14,10 @@ pub struct ToolRegistryBuildParams<'a> {
pub deferred_mcp_tools: Option<&'a [ToolInfo]>,
pub tool_namespaces: Option<&'a HashMap<String, ToolNamespace>>,
pub discoverable_tools: Option<&'a [DiscoverableTool]>,
pub extension_tool_bundles: &'a [ExtensionToolBundle],
pub extension_tool_definitions: &'a [ToolDefinition<Arc<dyn ToolExecutor>>],
pub dynamic_tools: &'a [DynamicToolSpec],
pub default_agent_type_description: &'a str,
pub wait_agent_timeouts: WaitAgentTimeoutOptions,
pub tool_search_entries: &'a [crate::tools::tool_search_entry::ToolSearchEntry],
}
#[derive(Debug, Clone, PartialEq, Eq)]

View File

@@ -19,7 +19,6 @@ use codex_protocol::protocol::SessionSource;
use codex_tools::AdditionalProperties;
use codex_tools::DiscoverableTool;
use codex_tools::JsonSchema;
use codex_tools::LoadableToolSpec;
use codex_tools::REQUEST_PLUGIN_INSTALL_TOOL_NAME;
use codex_tools::ResponsesApiNamespaceTool;
use codex_tools::ResponsesApiTool;
@@ -40,7 +39,6 @@ use std::collections::BTreeMap;
use std::path::PathBuf;
use super::*;
use crate::tools::tool_search_entry::build_tool_search_entries_for_config;
fn mcp_tool(name: &str, description: &str, input_schema: serde_json::Value) -> rmcp::model::Tool {
rmcp::model::Tool {
@@ -295,7 +293,7 @@ fn build_specs_with_unavailable_tools(
deferred_mcp_tools,
unavailable_called_tools,
/*discoverable_tools*/ None,
/*extension_tool_bundles*/ &[],
/*extension_tool_definitions*/ &[],
dynamic_tools,
)
}
@@ -356,7 +354,7 @@ async fn assert_model_tools(
deferred_mcp_tools: None,
unavailable_called_tools: Vec::new(),
discoverable_tools: None,
extension_tool_bundles: Vec::new(),
extension_tool_definitions: Vec::new(),
dynamic_tools: &[],
},
);
@@ -826,7 +824,7 @@ async fn request_plugin_install_requires_apps_and_plugins_features() {
/*deferred_mcp_tools*/ None,
Vec::new(),
discoverable_tools.clone(),
/*extension_tool_bundles*/ &[],
/*extension_tool_definitions*/ &[],
&[],
)
.build();
@@ -1002,59 +1000,6 @@ async fn search_tool_registers_namespaced_mcp_tool_aliases() {
assert!(registry.has_handler(&mcp_alias));
}
#[tokio::test]
async fn tool_search_entries_skip_namespace_outputs_when_namespace_tools_are_disabled() {
let model_info = search_capable_model_info().await;
let mut features = Features::with_defaults();
features.enable(Feature::ToolSearch);
let available_models = Vec::new();
let mut tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
available_models: &available_models,
features: &features,
image_generation_tool_auth_allowed: true,
web_search_mode: Some(WebSearchMode::Cached),
session_source: SessionSource::Cli,
permission_profile: &PermissionProfile::Disabled,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
});
tools_config.namespace_tools = false;
let mcp_tools = vec![mcp_tool_info(mcp_tool(
"echo",
"Echo",
serde_json::json!({"type": "object"}),
))];
let dynamic_tools = vec![
DynamicToolSpec {
namespace: Some("codex_app".to_string()),
name: "automation_update".to_string(),
description: "Create or update automations.".to_string(),
input_schema: serde_json::json!({"type": "object", "properties": {}}),
defer_loading: true,
},
DynamicToolSpec {
namespace: None,
name: "plain_dynamic".to_string(),
description: "Plain dynamic tool.".to_string(),
input_schema: serde_json::json!({"type": "object", "properties": {}}),
defer_loading: true,
},
];
let entries =
build_tool_search_entries_for_config(&tools_config, Some(&mcp_tools), &dynamic_tools);
let outputs = entries
.into_iter()
.map(|entry| entry.output)
.collect::<Vec<_>>();
assert_eq!(outputs.len(), 1);
match &outputs[0] {
LoadableToolSpec::Function(tool) => assert_eq!(tool.name, "plain_dynamic"),
LoadableToolSpec::Namespace(_) => panic!("namespace tool_search output should be hidden"),
}
}
#[tokio::test]
async fn direct_mcp_tools_register_namespaced_handlers() {
let config = test_config().await;

View File

@@ -1,11 +1,9 @@
use crate::tools::flat_tool_name;
use codex_mcp::ToolInfo;
use codex_protocol::dynamic_tools::DynamicToolSpec;
use codex_tool_api::ToolDefinition;
use codex_tools::LoadableToolSpec;
use codex_tools::ToolSearchResultSource;
use codex_tools::ToolsConfig;
use codex_tools::dynamic_tool_to_loadable_tool_spec;
use codex_tools::tool_search_result_source_to_loadable_tool_spec;
use codex_tools::tool_definition_to_loadable_tool_spec;
#[derive(Clone)]
pub(crate) struct ToolSearchEntry {
@@ -14,6 +12,7 @@ pub(crate) struct ToolSearchEntry {
pub(crate) limit_bucket: Option<String>,
}
#[cfg(test)]
pub(crate) fn build_tool_search_entries(
mcp_tools: Option<&[ToolInfo]>,
dynamic_tools: &[DynamicToolSpec],
@@ -25,7 +24,9 @@ pub(crate) fn build_tool_search_entries(
.unwrap_or_default();
mcp_tools.sort_by_key(|info| info.canonical_tool_name());
for info in mcp_tools {
match mcp_tool_search_entry(info) {
let definition =
codex_tools::mcp_tool_definition(info.canonical_tool_name(), &info.tool).deferred();
match mcp_tool_search_entry(info, &definition) {
Ok(entry) => entries.push(entry),
Err(error) => {
let tool_name = info.canonical_tool_name();
@@ -39,7 +40,8 @@ pub(crate) fn build_tool_search_entries(
let mut dynamic_tools = dynamic_tools.iter().collect::<Vec<_>>();
dynamic_tools.sort_by(|a, b| a.namespace.cmp(&b.namespace).then(a.name.cmp(&b.name)));
for tool in dynamic_tools {
match dynamic_tool_search_entry(tool) {
let definition = codex_tools::parse_dynamic_tool(tool);
match dynamic_tool_search_entry(tool, &definition) {
Ok(entry) => entries.push(entry),
Err(error) => {
tracing::error!(
@@ -53,47 +55,48 @@ pub(crate) fn build_tool_search_entries(
entries
}
pub(crate) fn build_tool_search_entries_for_config(
config: &ToolsConfig,
mcp_tools: Option<&[ToolInfo]>,
dynamic_tools: &[DynamicToolSpec],
) -> Vec<ToolSearchEntry> {
let mcp_tools = if config.namespace_tools {
mcp_tools
} else {
None
};
let dynamic_tools = dynamic_tools
.iter()
.filter(|tool| config.namespace_tools || tool.namespace.is_none())
.cloned()
.collect::<Vec<_>>();
build_tool_search_entries(mcp_tools, &dynamic_tools)
}
fn mcp_tool_search_entry(info: &ToolInfo) -> Result<ToolSearchEntry, serde_json::Error> {
pub(crate) fn mcp_tool_search_entry<R>(
info: &ToolInfo,
definition: &ToolDefinition<R>,
) -> Result<ToolSearchEntry, serde_json::Error> {
Ok(ToolSearchEntry {
search_text: build_mcp_search_text(info),
output: tool_search_result_source_to_loadable_tool_spec(ToolSearchResultSource {
server_name: info.server_name.as_str(),
tool_namespace: info.callable_namespace.as_str(),
tool_name: info.callable_name.as_str(),
tool: &info.tool,
connector_name: info.connector_name.as_deref(),
description: info.namespace_description.as_deref(),
})?,
output: tool_definition_to_loadable_tool_spec(
definition,
mcp_tool_search_namespace_description(info),
)?,
limit_bucket: Some(info.server_name.clone()),
})
}
fn dynamic_tool_search_entry(tool: &DynamicToolSpec) -> Result<ToolSearchEntry, serde_json::Error> {
pub(crate) fn dynamic_tool_search_entry<R>(
tool: &DynamicToolSpec,
definition: &ToolDefinition<R>,
) -> Result<ToolSearchEntry, serde_json::Error> {
Ok(ToolSearchEntry {
search_text: build_dynamic_search_text(tool),
output: dynamic_tool_to_loadable_tool_spec(tool)?,
output: tool_definition_to_loadable_tool_spec(
definition, /*namespace_description*/ None,
)?,
limit_bucket: None,
})
}
fn mcp_tool_search_namespace_description(info: &ToolInfo) -> Option<String> {
info.namespace_description
.as_deref()
.map(str::trim)
.filter(|description| !description.is_empty())
.map(str::to_string)
.or_else(|| {
info.connector_name
.as_deref()
.map(str::trim)
.filter(|connector_name| !connector_name.is_empty())
.map(|connector_name| format!("Tools for working with {connector_name}."))
})
}
fn build_mcp_search_text(info: &ToolInfo) -> String {
let tool_name = info.canonical_tool_name();
let mut parts = vec![

View File

@@ -1,7 +1,9 @@
use std::future::Future;
use std::sync::Arc;
use codex_protocol::items::TurnItem;
use codex_tool_api::ToolBundle;
use codex_tool_api::ToolDefinition;
use codex_tool_api::ToolExecutor;
use crate::ExtensionData;
@@ -28,8 +30,11 @@ pub trait ContextContributor: Send + Sync {
/// Extension contribution that exposes native tools owned by a feature.
pub trait ToolContributor: Send + Sync {
/// Returns the native tools visible for the supplied extension stores.
fn tools(&self, session_store: &ExtensionData, thread_store: &ExtensionData)
-> Vec<ToolBundle>;
fn tools(
&self,
session_store: &ExtensionData,
thread_store: &ExtensionData,
) -> Vec<ToolDefinition<Arc<dyn ToolExecutor>>>;
}
/// Future returned by one ordered turn-item contribution.

View File

@@ -3,8 +3,8 @@ mod registry;
mod state;
pub use codex_tool_api::FunctionToolSpec;
pub use codex_tool_api::ToolBundle;
pub use codex_tool_api::ToolCall;
pub use codex_tool_api::ToolDefinition;
pub use codex_tool_api::ToolError;
pub use codex_tool_api::ToolExecutor;
pub use codex_tool_api::ToolFuture;

View File

@@ -13,6 +13,7 @@ doctest = false
workspace = true
[dependencies]
codex-protocol = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }

View File

@@ -6,15 +6,22 @@ depend on the tool owner's crate.
Crates that define contributed tools should depend on this crate. It owns:
- the executable bundle contract: `ToolBundle`, `ToolExecutor`, `ToolCall`,
- the shared definition envelope: `ToolDefinition`, `ToolExposure`, and
`ToolName`
- the executable runtime contract: `ToolExecutor`, `ToolCall`, `ToolFuture`,
and `ToolError`
- the one model-visible spec an extension may contribute directly:
`FunctionToolSpec`
The contract is intentionally narrow: contributed tools receive a call id plus
raw JSON arguments and return a JSON value. If a feature needs richer host
integration, its extension is expected to do that wiring before exposing the
tool rather than widening this crate around the hardest native tools.
The contract is intentionally narrow: a definition keeps one tool's canonical
name, model metadata, exposure mode, and opaque runtime together. Hosts may use
that envelope with richer internal metadata, while contributed extension tools
use `FunctionToolSpec`. Contributed tools receive a call id plus raw JSON
arguments and return a JSON value. If a feature needs richer host integration,
its extension is expected to do that wiring before exposing the tool rather than
widening this crate around the hardest native tools. For ordinary flat function
tools, `ToolDefinition::from_function_spec(...)` derives the canonical tool
name from the contributed spec and keeps construction terse.
The intended dependency direction is:

View File

@@ -1,87 +0,0 @@
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use serde_json::Value;
use crate::FunctionToolSpec;
use crate::ToolCall;
use crate::ToolError;
/// Future returned by one contributed function-tool invocation.
pub type ToolFuture<'a> = Pin<Box<dyn Future<Output = Result<Value, ToolError>> + Send + 'a>>;
/// Model-visible definition plus executable implementation for one contributed
/// function tool.
#[derive(Clone)]
pub struct ToolBundle {
spec: FunctionToolSpec,
executor: Arc<dyn ToolExecutor>,
}
impl ToolBundle {
/// Creates one contributed function-tool bundle.
pub fn new(spec: FunctionToolSpec, executor: Arc<dyn ToolExecutor>) -> Self {
Self { spec, executor }
}
/// Returns the contributed function-tool spec.
pub fn spec(&self) -> &FunctionToolSpec {
&self.spec
}
/// Returns the contributed function-tool name.
pub fn tool_name(&self) -> &str {
self.spec.name.as_str()
}
/// Returns the executable implementation.
pub fn executor(&self) -> Arc<dyn ToolExecutor> {
Arc::clone(&self.executor)
}
}
/// Executable behavior for one contributed function tool.
///
/// Implementations receive the model-supplied call id and JSON arguments and
/// return the JSON value that should be exposed to the model.
pub trait ToolExecutor: Send + Sync {
fn execute<'a>(&'a self, call: ToolCall) -> ToolFuture<'a>;
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use pretty_assertions::assert_eq;
use serde_json::json;
use super::ToolBundle;
use super::ToolExecutor;
use super::ToolFuture;
use crate::FunctionToolSpec;
use crate::ToolCall;
struct StubExecutor;
impl ToolExecutor for StubExecutor {
fn execute<'a>(&'a self, _call: ToolCall) -> ToolFuture<'a> {
Box::pin(async { Ok(json!({ "ok": true })) })
}
}
#[test]
fn bundle_derives_name_from_function_spec() {
let bundle = ToolBundle::new(
FunctionToolSpec {
name: "echo".to_string(),
description: "Echo arguments.".to_string(),
strict: false,
parameters: json!({ "type": "object" }),
},
Arc::new(StubExecutor),
);
assert_eq!(bundle.tool_name(), "echo");
}
}

View File

@@ -0,0 +1,154 @@
use codex_protocol::ToolName;
use serde_json::Value;
use crate::FunctionToolSpec;
/// One callable function tool, its exposure mode, and the runtime object that
/// executes it.
#[derive(Clone, Debug, PartialEq)]
pub struct ToolDefinition<R, S = FunctionToolSpec> {
tool_name: ToolName,
spec: S,
output_schema: Option<Value>,
exposure: ToolExposure,
runtime: R,
}
/// Whether a tool is advertised immediately or marked for deferred loading.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum ToolExposure {
#[default]
Published,
Deferred,
}
impl<R, S> ToolDefinition<R, S> {
/// Creates one immediately-published tool definition.
pub fn new(tool_name: ToolName, spec: S, runtime: R) -> Self {
Self {
tool_name,
spec,
output_schema: None,
exposure: ToolExposure::Published,
runtime,
}
}
/// Returns the callable tool name, including any namespace.
pub fn tool_name(&self) -> &ToolName {
&self.tool_name
}
/// Returns the model-visible metadata bound to this definition.
pub fn spec(&self) -> &S {
&self.spec
}
/// Returns the optional tool-output schema kept alongside the model spec.
pub fn output_schema(&self) -> Option<&Value> {
self.output_schema.as_ref()
}
/// Returns how this tool should be exposed to the model.
pub fn exposure(&self) -> ToolExposure {
self.exposure
}
/// Returns the runtime object bound to this tool definition.
pub fn runtime(&self) -> &R {
&self.runtime
}
/// Rebinds the same tool definition to a different runtime object.
pub fn with_runtime<T>(self, runtime: T) -> ToolDefinition<T, S> {
ToolDefinition {
tool_name: self.tool_name,
spec: self.spec,
output_schema: self.output_schema,
exposure: self.exposure,
runtime,
}
}
/// Marks this tool as deferred. Deferred tools intentionally omit output
/// schema metadata until they are loaded.
pub fn deferred(mut self) -> Self {
self.exposure = ToolExposure::Deferred;
self.output_schema = None;
self
}
}
impl<R> ToolDefinition<R> {
/// Creates one immediately-published flat function-tool definition and
/// derives its callable name from the function spec.
pub fn from_function_spec(spec: FunctionToolSpec, runtime: R) -> Self {
let tool_name = ToolName::plain(spec.name.clone());
Self::new(tool_name, spec, runtime)
}
/// Attaches a tool-output schema to an ordinary function definition.
pub fn with_output_schema(mut self, output_schema: Value) -> Self {
self.output_schema = Some(output_schema);
self
}
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use serde_json::json;
use super::ToolDefinition;
use super::ToolExposure;
use crate::FunctionToolSpec;
use crate::ToolName;
fn definition() -> ToolDefinition<()> {
ToolDefinition::new(
ToolName::namespaced("mcp__calendar", "create_event"),
FunctionToolSpec {
name: "create_event".to_string(),
description: "Create an event.".to_string(),
strict: false,
parameters: json!({ "type": "object" }),
},
(),
)
.with_output_schema(json!({ "type": "object" }))
}
#[test]
fn deferred_tools_drop_output_schema() {
let definition = definition().deferred();
assert_eq!(definition.exposure(), ToolExposure::Deferred);
assert_eq!(definition.output_schema(), None);
}
#[test]
fn runtime_can_be_rebound_without_rebuilding_metadata() {
let definition = definition().with_runtime("handler");
assert_eq!(
definition.tool_name(),
&ToolName::namespaced("mcp__calendar", "create_event")
);
assert_eq!(definition.runtime(), &"handler");
}
#[test]
fn flat_function_definitions_derive_their_tool_name_from_the_spec() {
let definition = ToolDefinition::from_function_spec(
FunctionToolSpec {
name: "echo".to_string(),
description: "Echo arguments.".to_string(),
strict: false,
parameters: json!({ "type": "object" }),
},
(),
);
assert_eq!(definition.tool_name(), &ToolName::plain("echo"));
}
}

View File

@@ -0,0 +1,18 @@
use std::future::Future;
use std::pin::Pin;
use serde_json::Value;
use crate::ToolCall;
use crate::ToolError;
/// Future returned by one contributed function-tool invocation.
pub type ToolFuture<'a> = Pin<Box<dyn Future<Output = Result<Value, ToolError>> + Send + 'a>>;
/// Executable behavior for one contributed function tool.
///
/// Implementations receive the model-supplied call id and JSON arguments and
/// return the JSON value that should be exposed to the model.
pub trait ToolExecutor: Send + Sync {
fn execute<'a>(&'a self, call: ToolCall) -> ToolFuture<'a>;
}

View File

@@ -1,14 +1,17 @@
//! Minimal function-tool contracts shared between hosts and extension-owned
//! tool crates.
mod bundle;
mod call;
mod definition;
mod error;
mod executor;
mod spec;
pub use bundle::ToolBundle;
pub use bundle::ToolExecutor;
pub use bundle::ToolFuture;
pub use call::ToolCall;
pub use codex_protocol::ToolName;
pub use definition::ToolDefinition;
pub use definition::ToolExposure;
pub use error::ToolError;
pub use executor::ToolExecutor;
pub use executor::ToolFuture;
pub use spec::FunctionToolSpec;

View File

@@ -12,6 +12,7 @@ codex-app-server-protocol = { workspace = true }
codex-code-mode = { workspace = true }
codex-features = { workspace = true }
codex-protocol = { workspace = true }
codex-tool-api = { workspace = true }
codex-utils-absolute-path = { workspace = true }
codex-utils-pty = { workspace = true }
rmcp = { workspace = true, default-features = false, features = [

View File

@@ -1,15 +1,25 @@
use crate::ToolDefinition;
use crate::parse_tool_input_schema;
use codex_protocol::dynamic_tools::DynamicToolSpec;
use codex_tool_api::FunctionToolSpec;
use codex_tool_api::ToolDefinition;
use codex_tool_api::ToolName;
pub fn parse_dynamic_tool(tool: &DynamicToolSpec) -> Result<ToolDefinition, serde_json::Error> {
Ok(ToolDefinition {
name: tool.name.clone(),
description: tool.description.clone(),
input_schema: parse_tool_input_schema(&tool.input_schema)?,
output_schema: None,
defer_loading: tool.defer_loading,
})
pub fn parse_dynamic_tool(tool: &DynamicToolSpec) -> ToolDefinition<()> {
let definition = ToolDefinition::new(
ToolName::new(tool.namespace.clone(), tool.name.clone()),
FunctionToolSpec {
name: tool.name.clone(),
description: tool.description.clone(),
strict: false,
parameters: tool.input_schema.clone(),
},
(),
);
if tool.defer_loading {
definition.deferred()
} else {
definition
}
}
#[cfg(test)]

View File

@@ -1,12 +1,13 @@
use super::parse_dynamic_tool;
use crate::JsonSchema;
use crate::ToolDefinition;
use crate::ToolExposure;
use crate::ToolName;
use codex_protocol::dynamic_tools::DynamicToolSpec;
use codex_tool_api::FunctionToolSpec;
use pretty_assertions::assert_eq;
use std::collections::BTreeMap;
#[test]
fn parse_dynamic_tool_sanitizes_input_schema() {
fn parse_dynamic_tool_preserves_definition_metadata() {
let tool = DynamicToolSpec {
namespace: None,
name: "lookup_ticket".to_string(),
@@ -22,21 +23,23 @@ fn parse_dynamic_tool_sanitizes_input_schema() {
};
assert_eq!(
parse_dynamic_tool(&tool).expect("parse dynamic tool"),
ToolDefinition {
name: "lookup_ticket".to_string(),
description: "Fetch a ticket".to_string(),
input_schema: JsonSchema::object(
BTreeMap::from([(
"id".to_string(),
JsonSchema::string(Some("Ticket identifier".to_string()),),
)]),
/*required*/ None,
/*additional_properties*/ None
),
output_schema: None,
defer_loading: false,
}
parse_dynamic_tool(&tool),
ToolDefinition::new(
ToolName::plain("lookup_ticket"),
FunctionToolSpec {
name: "lookup_ticket".to_string(),
description: "Fetch a ticket".to_string(),
strict: false,
parameters: serde_json::json!({
"properties": {
"id": {
"description": "Ticket identifier"
}
}
}),
},
(),
)
);
}
@@ -53,18 +56,8 @@ fn parse_dynamic_tool_preserves_defer_loading() {
defer_loading: true,
};
assert_eq!(
parse_dynamic_tool(&tool).expect("parse dynamic tool"),
ToolDefinition {
name: "lookup_ticket".to_string(),
description: "Fetch a ticket".to_string(),
input_schema: JsonSchema::object(
BTreeMap::new(),
/*required*/ None,
/*additional_properties*/ None
),
output_schema: None,
defer_loading: true,
}
);
let definition = parse_dynamic_tool(&tool);
assert_eq!(definition.exposure(), ToolExposure::Deferred);
assert_eq!(definition.tool_name(), &ToolName::plain("lookup_ticket"));
}

View File

@@ -9,7 +9,6 @@ mod mcp_tool;
mod request_plugin_install;
mod responses_api;
mod tool_config;
mod tool_definition;
mod tool_discovery;
mod tool_spec;
@@ -19,6 +18,8 @@ pub use code_mode::collect_code_mode_exec_prompt_tool_definitions;
pub use code_mode::collect_code_mode_tool_definitions;
pub use code_mode::tool_spec_to_code_mode_tool_definition;
pub use codex_protocol::ToolName;
pub use codex_tool_api::ToolDefinition;
pub use codex_tool_api::ToolExposure;
pub use dynamic_tool::parse_dynamic_tool;
pub use image_detail::can_request_original_image_detail;
pub use image_detail::normalize_output_image_detail;
@@ -29,7 +30,7 @@ pub use json_schema::JsonSchemaPrimitiveType;
pub use json_schema::JsonSchemaType;
pub use json_schema::parse_tool_input_schema;
pub use mcp_tool::mcp_call_tool_result_output_schema;
pub use mcp_tool::parse_mcp_tool;
pub use mcp_tool::mcp_tool_definition;
pub use request_plugin_install::REQUEST_PLUGIN_INSTALL_APPROVAL_KIND_VALUE;
pub use request_plugin_install::REQUEST_PLUGIN_INSTALL_PERSIST_ALWAYS_VALUE;
pub use request_plugin_install::REQUEST_PLUGIN_INSTALL_PERSIST_KEY;
@@ -50,7 +51,7 @@ pub use responses_api::default_namespace_description;
pub use responses_api::dynamic_tool_to_loadable_tool_spec;
pub use responses_api::dynamic_tool_to_responses_api_tool;
pub use responses_api::mcp_tool_to_deferred_responses_api_tool;
pub use responses_api::mcp_tool_to_responses_api_tool;
pub use responses_api::tool_definition_to_loadable_tool_spec;
pub use responses_api::tool_definition_to_responses_api_tool;
pub use tool_config::ShellCommandBackendConfig;
pub use tool_config::ToolEnvironmentMode;
@@ -60,7 +61,6 @@ pub use tool_config::ToolsConfigParams;
pub use tool_config::UnifiedExecShellMode;
pub use tool_config::ZshForkConfig;
pub use tool_config::request_user_input_available_modes;
pub use tool_definition::ToolDefinition;
pub use tool_discovery::DiscoverablePluginInfo;
pub use tool_discovery::DiscoverableTool;
pub use tool_discovery::DiscoverableToolAction;
@@ -69,13 +69,11 @@ pub use tool_discovery::REQUEST_PLUGIN_INSTALL_TOOL_NAME;
pub use tool_discovery::RequestPluginInstallEntry;
pub use tool_discovery::TOOL_SEARCH_DEFAULT_LIMIT;
pub use tool_discovery::TOOL_SEARCH_TOOL_NAME;
pub use tool_discovery::ToolSearchResultSource;
pub use tool_discovery::ToolSearchSource;
pub use tool_discovery::ToolSearchSourceInfo;
pub use tool_discovery::collect_request_plugin_install_entries;
pub use tool_discovery::collect_tool_search_source_infos;
pub use tool_discovery::filter_request_plugin_install_discoverable_tools_for_client;
pub use tool_discovery::tool_search_result_source_to_loadable_tool_spec;
pub use tool_spec::ResponsesApiWebSearchFilters;
pub use tool_spec::ResponsesApiWebSearchUserLocation;
pub use tool_spec::ToolSpec;

View File

@@ -1,9 +1,10 @@
use crate::ToolDefinition;
use crate::parse_tool_input_schema;
use codex_tool_api::FunctionToolSpec;
use codex_tool_api::ToolDefinition;
use codex_tool_api::ToolName;
use serde_json::Value as JsonValue;
use serde_json::json;
pub fn parse_mcp_tool(tool: &rmcp::model::Tool) -> Result<ToolDefinition, serde_json::Error> {
pub fn mcp_tool_definition(tool_name: ToolName, tool: &rmcp::model::Tool) -> ToolDefinition<()> {
let mut serialized_input_schema = serde_json::Value::Object(tool.input_schema.as_ref().clone());
// OpenAI models mandate the "properties" field in the schema. Some MCP
@@ -18,22 +19,25 @@ pub fn parse_mcp_tool(tool: &rmcp::model::Tool) -> Result<ToolDefinition, serde_
);
}
let input_schema = parse_tool_input_schema(&serialized_input_schema)?;
let structured_content_schema = tool
.output_schema
.as_ref()
.map(|output_schema| serde_json::Value::Object(output_schema.as_ref().clone()))
.unwrap_or_else(|| JsonValue::Object(serde_json::Map::new()));
Ok(ToolDefinition {
name: tool.name.to_string(),
description: tool.description.clone().map(Into::into).unwrap_or_default(),
input_schema,
output_schema: Some(mcp_call_tool_result_output_schema(
structured_content_schema,
)),
defer_loading: false,
})
ToolDefinition::new(
tool_name.clone(),
FunctionToolSpec {
name: tool_name.name,
description: tool.description.clone().map(Into::into).unwrap_or_default(),
strict: false,
parameters: serialized_input_schema,
},
(),
)
.with_output_schema(mcp_call_tool_result_output_schema(
structured_content_schema,
))
}
pub fn mcp_call_tool_result_output_schema(structured_content_schema: JsonValue) -> JsonValue {

View File

@@ -1,9 +1,9 @@
use super::mcp_call_tool_result_output_schema;
use super::parse_mcp_tool;
use crate::JsonSchema;
use super::mcp_tool_definition;
use crate::ToolDefinition;
use crate::ToolName;
use codex_tool_api::FunctionToolSpec;
use pretty_assertions::assert_eq;
use std::collections::BTreeMap;
fn mcp_tool(name: &str, description: &str, input_schema: serde_json::Value) -> rmcp::model::Tool {
rmcp::model::Tool {
@@ -30,21 +30,42 @@ fn parse_mcp_tool_inserts_empty_properties() {
);
assert_eq!(
parse_mcp_tool(&tool).expect("parse MCP tool"),
ToolDefinition {
name: "no_props".to_string(),
description: "No properties".to_string(),
input_schema: JsonSchema::object(
BTreeMap::new(),
/*required*/ None,
/*additional_properties*/ None
),
output_schema: Some(mcp_call_tool_result_output_schema(serde_json::json!({}))),
defer_loading: false,
}
mcp_tool_definition(ToolName::plain("no_props"), &tool),
ToolDefinition::new(
ToolName::plain("no_props"),
FunctionToolSpec {
name: "no_props".to_string(),
description: "No properties".to_string(),
strict: false,
parameters: serde_json::json!({
"type": "object",
"properties": {}
}),
},
(),
)
.with_output_schema(mcp_call_tool_result_output_schema(serde_json::json!({})))
);
}
#[test]
fn mcp_tool_definition_uses_callable_name() {
let tool = mcp_tool(
"calendar_create_event",
"Create an event",
serde_json::json!({
"type": "object"
}),
);
let definition = mcp_tool_definition(
ToolName::namespaced("mcp__codex_apps__calendar", "_create_event"),
&tool,
);
assert_eq!(definition.spec().name, "_create_event");
}
#[test]
fn parse_mcp_tool_preserves_top_level_output_schema() {
let mut tool = mcp_tool(
@@ -68,27 +89,30 @@ fn parse_mcp_tool_preserves_top_level_output_schema() {
)));
assert_eq!(
parse_mcp_tool(&tool).expect("parse MCP tool"),
ToolDefinition {
name: "with_output".to_string(),
description: "Has output schema".to_string(),
input_schema: JsonSchema::object(
BTreeMap::new(),
/*required*/ None,
/*additional_properties*/ None
),
output_schema: Some(mcp_call_tool_result_output_schema(serde_json::json!({
"properties": {
"result": {
"properties": {
"nested": {}
}
mcp_tool_definition(ToolName::plain("with_output"), &tool),
ToolDefinition::new(
ToolName::plain("with_output"),
FunctionToolSpec {
name: "with_output".to_string(),
description: "Has output schema".to_string(),
strict: false,
parameters: serde_json::json!({
"type": "object",
"properties": {}
}),
},
(),
)
.with_output_schema(mcp_call_tool_result_output_schema(serde_json::json!({
"properties": {
"result": {
"properties": {
"nested": {}
}
},
"required": ["result"]
}))),
defer_loading: false,
}
}
},
"required": ["result"]
})))
);
}
@@ -108,19 +132,22 @@ fn parse_mcp_tool_preserves_output_schema_without_inferred_type() {
)));
assert_eq!(
parse_mcp_tool(&tool).expect("parse MCP tool"),
ToolDefinition {
name: "with_enum_output".to_string(),
description: "Has enum output schema".to_string(),
input_schema: JsonSchema::object(
BTreeMap::new(),
/*required*/ None,
/*additional_properties*/ None
),
output_schema: Some(mcp_call_tool_result_output_schema(serde_json::json!({
"enum": ["ok", "error"]
}))),
defer_loading: false,
}
mcp_tool_definition(ToolName::plain("with_enum_output"), &tool),
ToolDefinition::new(
ToolName::plain("with_enum_output"),
FunctionToolSpec {
name: "with_enum_output".to_string(),
description: "Has enum output schema".to_string(),
strict: false,
parameters: serde_json::json!({
"type": "object",
"properties": {}
}),
},
(),
)
.with_output_schema(mcp_call_tool_result_output_schema(serde_json::json!({
"enum": ["ok", "error"]
})))
);
}

View File

@@ -1,9 +1,11 @@
use crate::JsonSchema;
use crate::ToolDefinition;
use crate::ToolName;
use crate::mcp_tool_definition;
use crate::parse_dynamic_tool;
use crate::parse_mcp_tool;
use crate::parse_tool_input_schema;
use codex_protocol::dynamic_tools::DynamicToolSpec;
use codex_tool_api::ToolDefinition;
use codex_tool_api::ToolExposure;
use serde::Deserialize;
use serde::Serialize;
use serde_json::Value;
@@ -69,24 +71,16 @@ pub enum ResponsesApiNamespaceTool {
pub fn dynamic_tool_to_responses_api_tool(
tool: &DynamicToolSpec,
) -> Result<ResponsesApiTool, serde_json::Error> {
Ok(tool_definition_to_responses_api_tool(parse_dynamic_tool(
tool,
)?))
tool_definition_to_responses_api_tool(&parse_dynamic_tool(tool))
}
pub fn dynamic_tool_to_loadable_tool_spec(
tool: &DynamicToolSpec,
) -> Result<LoadableToolSpec, serde_json::Error> {
let output_tool = dynamic_tool_to_responses_api_tool(tool)?;
Ok(match tool.namespace.as_ref() {
Some(namespace) => LoadableToolSpec::Namespace(ResponsesApiNamespace {
name: namespace.clone(),
// the user doesn't provide a description for dynamic tools, so we use the default
description: default_namespace_description(namespace),
tools: vec![ResponsesApiNamespaceTool::Function(output_tool)],
}),
None => LoadableToolSpec::Function(output_tool),
})
tool_definition_to_loadable_tool_spec(
&parse_dynamic_tool(tool),
/*namespace_description*/ None,
)
}
pub fn coalesce_loadable_tool_specs(
@@ -119,35 +113,46 @@ pub fn coalesce_loadable_tool_specs(
coalesced_specs
}
pub fn mcp_tool_to_responses_api_tool(
tool_name: &ToolName,
tool: &rmcp::model::Tool,
) -> Result<ResponsesApiTool, serde_json::Error> {
Ok(tool_definition_to_responses_api_tool(
parse_mcp_tool(tool)?.renamed(tool_name.name.clone()),
))
}
pub fn mcp_tool_to_deferred_responses_api_tool(
tool_name: &ToolName,
tool: &rmcp::model::Tool,
) -> Result<ResponsesApiTool, serde_json::Error> {
Ok(tool_definition_to_responses_api_tool(
parse_mcp_tool(tool)?
.renamed(tool_name.name.clone())
.into_deferred(),
))
tool_definition_to_responses_api_tool(&mcp_tool_definition(tool_name.clone(), tool).deferred())
}
pub fn tool_definition_to_responses_api_tool(tool_definition: ToolDefinition) -> ResponsesApiTool {
ResponsesApiTool {
name: tool_definition.name,
description: tool_definition.description,
strict: false,
defer_loading: tool_definition.defer_loading.then_some(true),
parameters: tool_definition.input_schema,
output_schema: tool_definition.output_schema,
}
pub fn tool_definition_to_responses_api_tool<R>(
tool_definition: &ToolDefinition<R>,
) -> Result<ResponsesApiTool, serde_json::Error> {
let spec = tool_definition.spec();
debug_assert_eq!(
tool_definition.tool_name().name,
spec.name,
"tool definition name must match its function spec name"
);
Ok(ResponsesApiTool {
name: tool_definition.tool_name().name.clone(),
description: spec.description.clone(),
strict: spec.strict,
defer_loading: matches!(tool_definition.exposure(), ToolExposure::Deferred).then_some(true),
parameters: parse_tool_input_schema(&spec.parameters)?,
output_schema: tool_definition.output_schema().cloned(),
})
}
pub fn tool_definition_to_loadable_tool_spec<R>(
tool_definition: &ToolDefinition<R>,
namespace_description: Option<String>,
) -> Result<LoadableToolSpec, serde_json::Error> {
let output_tool = tool_definition_to_responses_api_tool(tool_definition)?;
Ok(match tool_definition.tool_name().namespace.as_ref() {
Some(namespace) => LoadableToolSpec::Namespace(ResponsesApiNamespace {
name: namespace.clone(),
description: namespace_description
.unwrap_or_else(|| default_namespace_description(namespace)),
tools: vec![ResponsesApiNamespaceTool::Function(output_tool)],
}),
None => LoadableToolSpec::Function(output_tool),
})
}
#[cfg(test)]

View File

@@ -9,6 +9,7 @@ use crate::JsonSchema;
use crate::ToolDefinition;
use crate::ToolName;
use codex_protocol::dynamic_tools::DynamicToolSpec;
use codex_tool_api::FunctionToolSpec;
use pretty_assertions::assert_eq;
use serde_json::json;
use std::collections::BTreeMap;
@@ -16,20 +17,27 @@ use std::collections::BTreeMap;
#[test]
fn tool_definition_to_responses_api_tool_omits_false_defer_loading() {
assert_eq!(
tool_definition_to_responses_api_tool(ToolDefinition {
name: "lookup_order".to_string(),
description: "Look up an order".to_string(),
input_schema: JsonSchema::object(
BTreeMap::from([(
"order_id".to_string(),
JsonSchema::string(/*description*/ None),
)]),
Some(vec!["order_id".to_string()]),
Some(false.into())
),
output_schema: Some(json!({"type": "object"})),
defer_loading: false,
}),
tool_definition_to_responses_api_tool(
&ToolDefinition::new(
ToolName::plain("lookup_order"),
FunctionToolSpec {
name: "lookup_order".to_string(),
description: "Look up an order".to_string(),
strict: false,
parameters: json!({
"type": "object",
"properties": {
"order_id": {"type": "string"}
},
"required": ["order_id"],
"additionalProperties": false,
}),
},
(),
)
.with_output_schema(json!({"type": "object"})),
)
.expect("convert definition"),
ResponsesApiTool {
name: "lookup_order".to_string(),
description: "Look up an order".to_string(),

View File

@@ -1,30 +0,0 @@
use crate::JsonSchema;
use serde_json::Value as JsonValue;
/// Tool metadata and schemas that downstream crates can adapt into higher-level
/// tool specs.
#[derive(Debug, PartialEq)]
pub struct ToolDefinition {
pub name: String,
pub description: String,
pub input_schema: JsonSchema,
pub output_schema: Option<JsonValue>,
pub defer_loading: bool,
}
impl ToolDefinition {
pub fn renamed(mut self, name: String) -> Self {
self.name = name;
self
}
pub fn into_deferred(mut self) -> Self {
self.output_schema = None;
self.defer_loading = true;
self
}
}
#[cfg(test)]
#[path = "tool_definition_tests.rs"]
mod tests;

View File

@@ -1,43 +0,0 @@
use super::ToolDefinition;
use crate::JsonSchema;
use pretty_assertions::assert_eq;
use std::collections::BTreeMap;
fn tool_definition() -> ToolDefinition {
ToolDefinition {
name: "lookup_order".to_string(),
description: "Look up an order".to_string(),
input_schema: JsonSchema::object(
BTreeMap::new(),
/*required*/ None,
/*additional_properties*/ None,
),
output_schema: Some(serde_json::json!({
"type": "object",
})),
defer_loading: false,
}
}
#[test]
fn renamed_overrides_name_only() {
assert_eq!(
tool_definition().renamed("mcp__orders__lookup_order".to_string()),
ToolDefinition {
name: "mcp__orders__lookup_order".to_string(),
..tool_definition()
}
);
}
#[test]
fn into_deferred_drops_output_schema_and_sets_defer_loading() {
assert_eq!(
tool_definition().into_deferred(),
ToolDefinition {
output_schema: None,
defer_loading: true,
..tool_definition()
}
);
}

View File

@@ -1,9 +1,3 @@
use crate::LoadableToolSpec;
use crate::ResponsesApiNamespace;
use crate::ResponsesApiNamespaceTool;
use crate::ToolName;
use crate::default_namespace_description;
use crate::mcp_tool_to_deferred_responses_api_tool;
use codex_app_server_protocol::AppInfo;
use serde::Deserialize;
use serde::Serialize;
@@ -26,16 +20,6 @@ pub struct ToolSearchSource<'a> {
pub description: Option<&'a str>,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ToolSearchResultSource<'a> {
pub server_name: &'a str,
pub tool_namespace: &'a str,
pub tool_name: &'a str,
pub tool: &'a rmcp::model::Tool,
pub connector_name: Option<&'a str>,
pub description: Option<&'a str>,
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum DiscoverableToolType {
@@ -133,40 +117,6 @@ pub struct RequestPluginInstallEntry {
pub app_connector_ids: Vec<String>,
}
pub fn tool_search_result_source_to_loadable_tool_spec(
source: ToolSearchResultSource<'_>,
) -> Result<LoadableToolSpec, serde_json::Error> {
Ok(LoadableToolSpec::Namespace(ResponsesApiNamespace {
name: source.tool_namespace.to_string(),
description: tool_search_result_source_namespace_description(source),
tools: vec![tool_search_result_source_to_namespace_tool(source)?],
}))
}
fn tool_search_result_source_namespace_description(source: ToolSearchResultSource<'_>) -> String {
source
.description
.map(str::trim)
.filter(|description| !description.is_empty())
.map(str::to_string)
.or_else(|| {
source
.connector_name
.map(str::trim)
.filter(|connector_name| !connector_name.is_empty())
.map(|connector_name| format!("Tools for working with {connector_name}."))
})
.unwrap_or_else(|| default_namespace_description(source.tool_namespace))
}
fn tool_search_result_source_to_namespace_tool(
source: ToolSearchResultSource<'_>,
) -> Result<ResponsesApiNamespaceTool, serde_json::Error> {
let tool_name = ToolName::namespaced(source.tool_namespace, source.tool_name);
mcp_tool_to_deferred_responses_api_tool(&tool_name, source.tool)
.map(ResponsesApiNamespaceTool::Function)
}
pub fn collect_tool_search_source_infos<'a>(
searchable_tools: impl IntoIterator<Item = ToolSearchSource<'a>>,
) -> Vec<ToolSearchSourceInfo> {