feat: extract shared tool executor interface (#22359)

## Why

Codex still models model-visible tools and executable behavior largely
inside `codex-core`, which makes it harder to evolve the tool system
toward a single reusable abstraction for built-ins, MCP-backed tools,
dynamic tools, and later tools injected from outside core.

This PR takes the next incremental step in that direction by moving the
common execution-facing pieces out of core and separating them from
core-only orchestration. The intent is to let shared tool abstractions
improve in one place, while `codex-core` keeps the parts that are still
inherently host-specific today, such as `ToolInvocation`, dispatch
wiring, and hook integration.

This PR is mostly moving things around. The only interesting piece is
this abstraction:
https://github.com/openai/codex/pull/22359/changes#diff-81af519002548ba51ed102bdaaf77e081d40a1e73a6e5f9b104bbbc96a6f1b3dR13

## What changed

- Added `codex_tools::ToolExecutor<Invocation>` as the shared execution
trait for model-visible tools.
- Moved the reusable execution support types from `codex-core` into
`codex-tools`:
  - `FunctionCallError`
  - `ToolPayload`
  - `ToolOutput`
- Refactored core tool implementations so that execution behavior lives
on `ToolExecutor<ToolInvocation>`, while `ToolHandler` remains the
core-local extension point for hook payloads, telemetry tags, diff
consumers, and other orchestration concerns.
- Kept the registry and dispatch flow behaviorally unchanged while
making the shared/extracted boundary explicit across built-in, MCP,
dynamic, extension-backed, shell, and multi-agent tool handlers.

## Verification

- `cargo test -p codex-tools`
- `just fix -p codex-tools`
- `just fix -p codex-core`
- `cargo test -p codex-core` progressed through the updated tool
surfaces and then hit the existing unrelated multi-agent stack overflow
in
`tools::handlers::multi_agents::tests::tool_handlers_cascade_close_and_resume_and_keep_explicitly_closed_subtrees_closed`.
This commit is contained in:
jif-oai
2026-05-13 11:31:27 +02:00
committed by GitHub
parent 155c04ad40
commit 1824685a00
54 changed files with 770 additions and 575 deletions
+2
View File
@@ -3729,10 +3729,12 @@ dependencies = [
"codex-protocol",
"codex-utils-absolute-path",
"codex-utils-pty",
"codex-utils-string",
"pretty_assertions",
"rmcp",
"serde",
"serde_json",
"thiserror 2.0.18",
"tracing",
]
+1 -11
View File
@@ -1,11 +1 @@
use thiserror::Error;
#[derive(Debug, Error, PartialEq)]
pub enum FunctionCallError {
#[error("{0}")]
RespondToModel(String),
#[error("LocalShellCall without call_id or id")]
MissingLocalShellCallId,
#[error("Fatal error: {0}")]
Fatal(String),
}
pub use codex_tools::FunctionCallError;
+1 -1
View File
@@ -71,7 +71,7 @@ use crate::tools::handlers::CreateGoalHandler;
use crate::tools::handlers::ExecCommandHandler;
use crate::tools::handlers::ShellHandler;
use crate::tools::handlers::UpdateGoalHandler;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolExecutor;
use crate::tools::router::ToolCallSource;
use crate::turn_diff_tracker::TurnDiffTracker;
use codex_app_server_protocol::AppInfo;
@@ -2,6 +2,7 @@ use crate::function_tool::FunctionCallError;
use crate::tools::context::FunctionToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::registry::ToolExecutor;
use crate::tools::registry::ToolHandler;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
@@ -86,7 +87,7 @@ impl CodeModeExecuteHandler {
}
}
impl ToolHandler for CodeModeExecuteHandler {
impl ToolExecutor<ToolInvocation> for CodeModeExecuteHandler {
type Output = FunctionToolOutput;
fn tool_name(&self) -> ToolName {
@@ -97,10 +98,6 @@ impl ToolHandler for CodeModeExecuteHandler {
Some(self.spec.clone())
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Custom { .. })
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let ToolInvocation {
session,
@@ -121,3 +118,9 @@ impl ToolHandler for CodeModeExecuteHandler {
}
}
}
impl ToolHandler for CodeModeExecuteHandler {
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Custom { .. })
}
}
@@ -4,6 +4,7 @@ use crate::function_tool::FunctionCallError;
use crate::tools::context::FunctionToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::registry::ToolExecutor;
use crate::tools::registry::ToolHandler;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
@@ -40,7 +41,7 @@ where
})
}
impl ToolHandler for CodeModeWaitHandler {
impl ToolExecutor<ToolInvocation> for CodeModeWaitHandler {
type Output = FunctionToolOutput;
fn tool_name(&self) -> ToolName {
@@ -105,3 +106,5 @@ impl ToolHandler for CodeModeWaitHandler {
}
}
}
impl ToolHandler for CodeModeWaitHandler {}
+3 -127
View File
@@ -8,13 +8,10 @@ use crate::tools::TELEMETRY_PREVIEW_TRUNCATION_NOTICE;
use crate::turn_diff_tracker::TurnDiffTracker;
use crate::unified_exec::resolve_max_tokens;
use codex_protocol::mcp::CallToolResult;
use codex_protocol::models::DEFAULT_IMAGE_DETAIL;
use codex_protocol::models::FunctionCallOutputBody;
use codex_protocol::models::FunctionCallOutputContentItem;
use codex_protocol::models::FunctionCallOutputPayload;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::models::SearchToolCallParams;
use codex_protocol::models::ShellToolCallParams;
use codex_protocol::models::function_call_output_content_items_to_text;
use codex_tools::LoadableToolSpec;
use codex_tools::ToolName;
@@ -23,12 +20,14 @@ use codex_utils_output_truncation::formatted_truncate_text;
use codex_utils_string::take_bytes_at_char_boundary;
use serde::Serialize;
use serde_json::Value as JsonValue;
use std::borrow::Cow;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Mutex;
use tokio_util::sync::CancellationToken;
pub use codex_tools::ToolOutput;
pub use codex_tools::ToolPayload;
pub type SharedTurnDiffTracker = Arc<Mutex<TurnDiffTracker>>;
#[derive(Clone, Debug, Eq, PartialEq)]
@@ -56,73 +55,6 @@ pub struct ToolInvocation {
pub payload: ToolPayload,
}
#[derive(Clone, Debug)]
pub enum ToolPayload {
Function { arguments: String },
ToolSearch { arguments: SearchToolCallParams },
Custom { input: String },
LocalShell { params: ShellToolCallParams },
}
impl ToolPayload {
pub fn log_payload(&self) -> Cow<'_, str> {
match self {
ToolPayload::Function { arguments } => Cow::Borrowed(arguments),
ToolPayload::ToolSearch { arguments } => Cow::Owned(arguments.query.clone()),
ToolPayload::Custom { input } => Cow::Borrowed(input),
ToolPayload::LocalShell { params } => Cow::Owned(params.command.join(" ")),
}
}
}
pub trait ToolOutput: Send {
fn log_preview(&self) -> String;
fn success_for_logging(&self) -> bool;
fn to_response_item(&self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem;
/// Returns the stable value exposed to `PostToolUse` hooks for this tool output.
///
/// Tool handlers decide whether a tool participates in `PostToolUse`, but
/// this method lets the output type own any conversion from model-facing
/// response content to hook-facing data. Returning `None` means the output
/// should not produce a post-use hook payload, not merely that the tool had
/// empty output.
fn post_tool_use_response(&self, _call_id: &str, _payload: &ToolPayload) -> Option<JsonValue> {
None
}
fn code_mode_result(&self, payload: &ToolPayload) -> JsonValue {
response_input_to_code_mode_result(self.to_response_item("", payload))
}
}
impl ToolOutput for CallToolResult {
fn log_preview(&self) -> String {
let output = self.as_function_call_output_payload();
let preview = output.body.to_text().unwrap_or_else(|| output.to_string());
telemetry_preview(&preview)
}
fn success_for_logging(&self) -> bool {
self.success()
}
fn to_response_item(&self, call_id: &str, _payload: &ToolPayload) -> ResponseInputItem {
ResponseInputItem::McpToolCallOutput {
call_id: call_id.to_string(),
output: self.clone(),
}
}
fn code_mode_result(&self, _payload: &ToolPayload) -> JsonValue {
serde_json::to_value(self).unwrap_or_else(|err| {
JsonValue::String(format!("failed to serialize mcp result: {err}"))
})
}
}
#[derive(Clone, Debug)]
pub struct McpToolOutput {
pub result: CallToolResult,
@@ -469,62 +401,6 @@ impl ExecCommandToolOutput {
}
}
pub(crate) fn response_input_to_code_mode_result(response: ResponseInputItem) -> JsonValue {
match response {
ResponseInputItem::Message { content, .. } => content_items_to_code_mode_result(
&content
.into_iter()
.map(|item| match item {
codex_protocol::models::ContentItem::InputText { text }
| codex_protocol::models::ContentItem::OutputText { text } => {
FunctionCallOutputContentItem::InputText { text }
}
codex_protocol::models::ContentItem::InputImage { image_url, detail } => {
FunctionCallOutputContentItem::InputImage {
image_url,
detail: detail.or(Some(DEFAULT_IMAGE_DETAIL)),
}
}
})
.collect::<Vec<_>>(),
),
ResponseInputItem::FunctionCallOutput { output, .. }
| ResponseInputItem::CustomToolCallOutput { output, .. } => match output.body {
FunctionCallOutputBody::Text(text) => JsonValue::String(text),
FunctionCallOutputBody::ContentItems(items) => {
content_items_to_code_mode_result(&items)
}
},
ResponseInputItem::ToolSearchOutput { tools, .. } => JsonValue::Array(tools),
ResponseInputItem::McpToolCallOutput { output, .. } => {
output.code_mode_result(&ToolPayload::Function {
arguments: String::new(),
})
}
}
}
fn content_items_to_code_mode_result(items: &[FunctionCallOutputContentItem]) -> JsonValue {
JsonValue::String(
items
.iter()
.filter_map(|item| match item {
FunctionCallOutputContentItem::InputText { text } if !text.trim().is_empty() => {
Some(text.clone())
}
FunctionCallOutputContentItem::InputImage { image_url, .. }
if !image_url.trim().is_empty() =>
{
Some(image_url.clone())
}
FunctionCallOutputContentItem::InputText { .. }
| FunctionCallOutputContentItem::InputImage { .. } => None,
})
.collect::<Vec<_>>()
.join("\n"),
)
}
fn function_tool_response(
call_id: &str,
payload: &ToolPayload,
+1
View File
@@ -1,5 +1,6 @@
use super::*;
use codex_protocol::models::DEFAULT_IMAGE_DETAIL;
use codex_protocol::models::SearchToolCallParams;
use core_test_support::assert_regex_match;
use pretty_assertions::assert_eq;
use serde_json::json;
@@ -3,6 +3,7 @@ use crate::tools::context::FunctionToolOutput;
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::ToolExecutor;
use crate::tools::registry::ToolHandler;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
@@ -11,7 +12,7 @@ use super::*;
pub struct ReportAgentJobResultHandler;
impl ToolHandler for ReportAgentJobResultHandler {
impl ToolExecutor<ToolInvocation> for ReportAgentJobResultHandler {
type Output = FunctionToolOutput;
fn tool_name(&self) -> ToolName {
@@ -22,10 +23,6 @@ impl ToolHandler for ReportAgentJobResultHandler {
Some(create_report_agent_job_result_tool())
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let ToolInvocation {
session, payload, ..
@@ -44,6 +41,12 @@ impl ToolHandler for ReportAgentJobResultHandler {
}
}
impl ToolHandler for ReportAgentJobResultHandler {
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
}
pub async fn handle(
session: Arc<Session>,
arguments: String,
@@ -3,6 +3,7 @@ use crate::tools::context::FunctionToolOutput;
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::ToolExecutor;
use crate::tools::registry::ToolHandler;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
@@ -11,7 +12,7 @@ use super::*;
pub struct SpawnAgentsOnCsvHandler;
impl ToolHandler for SpawnAgentsOnCsvHandler {
impl ToolExecutor<ToolInvocation> for SpawnAgentsOnCsvHandler {
type Output = FunctionToolOutput;
fn tool_name(&self) -> ToolName {
@@ -22,10 +23,6 @@ impl ToolHandler for SpawnAgentsOnCsvHandler {
Some(create_spawn_agents_on_csv_tool())
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let ToolInvocation {
session,
@@ -47,6 +44,12 @@ impl ToolHandler for SpawnAgentsOnCsvHandler {
}
}
impl ToolHandler for SpawnAgentsOnCsvHandler {
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
}
/// Create a new agent job from a CSV and run it to completion.
///
/// Each CSV row becomes a job item. The instruction string is a template where `{column}`
+51 -48
View File
@@ -30,6 +30,7 @@ use crate::tools::orchestrator::ToolOrchestrator;
use crate::tools::registry::PostToolUsePayload;
use crate::tools::registry::PreToolUsePayload;
use crate::tools::registry::ToolArgumentDiffConsumer;
use crate::tools::registry::ToolExecutor;
use crate::tools::registry::ToolHandler;
use crate::tools::runtimes::apply_patch::ApplyPatchRequest;
use crate::tools::runtimes::apply_patch::ApplyPatchRuntime;
@@ -296,7 +297,7 @@ async fn effective_patch_permissions(
)
}
impl ToolHandler for ApplyPatchHandler {
impl ToolExecutor<ToolInvocation> for ApplyPatchHandler {
type Output = ApplyPatchToolOutput;
fn tool_name(&self) -> ToolName {
@@ -307,53 +308,6 @@ impl ToolHandler for ApplyPatchHandler {
Some(create_apply_patch_freeform_tool(self.multi_environment))
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Custom { .. })
}
fn create_diff_consumer(&self) -> Option<Box<dyn ToolArgumentDiffConsumer>> {
Some(Box::<ApplyPatchArgumentDiffConsumer>::default())
}
fn pre_tool_use_payload(&self, invocation: &ToolInvocation) -> Option<PreToolUsePayload> {
apply_patch_payload_command(&invocation.payload).map(|command| PreToolUsePayload {
tool_name: HookToolName::apply_patch(),
tool_input: serde_json::json!({ "command": command }),
})
}
fn with_updated_hook_input(
&self,
mut invocation: ToolInvocation,
updated_input: serde_json::Value,
) -> Result<ToolInvocation, FunctionCallError> {
let patch = updated_hook_command(&updated_input)?;
invocation.payload = match invocation.payload {
ToolPayload::Custom { .. } => ToolPayload::Custom {
input: patch.to_string(),
},
payload => payload,
};
Ok(invocation)
}
fn post_tool_use_payload(
&self,
invocation: &ToolInvocation,
result: &Self::Output,
) -> Option<PostToolUsePayload> {
let tool_response =
result.post_tool_use_response(&invocation.call_id, &invocation.payload)?;
Some(PostToolUsePayload {
tool_name: HookToolName::apply_patch(),
tool_use_id: invocation.call_id.clone(),
tool_input: serde_json::json!({
"command": apply_patch_payload_command(&invocation.payload)?,
}),
tool_response,
})
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let ToolInvocation {
session,
@@ -484,6 +438,55 @@ impl ToolHandler for ApplyPatchHandler {
}
}
impl ToolHandler for ApplyPatchHandler {
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Custom { .. })
}
fn create_diff_consumer(&self) -> Option<Box<dyn ToolArgumentDiffConsumer>> {
Some(Box::<ApplyPatchArgumentDiffConsumer>::default())
}
fn pre_tool_use_payload(&self, invocation: &ToolInvocation) -> Option<PreToolUsePayload> {
apply_patch_payload_command(&invocation.payload).map(|command| PreToolUsePayload {
tool_name: HookToolName::apply_patch(),
tool_input: serde_json::json!({ "command": command }),
})
}
fn with_updated_hook_input(
&self,
mut invocation: ToolInvocation,
updated_input: serde_json::Value,
) -> Result<ToolInvocation, FunctionCallError> {
let patch = updated_hook_command(&updated_input)?;
invocation.payload = match invocation.payload {
ToolPayload::Custom { .. } => ToolPayload::Custom {
input: patch.to_string(),
},
payload => payload,
};
Ok(invocation)
}
fn post_tool_use_payload(
&self,
invocation: &ToolInvocation,
result: &Self::Output,
) -> Option<PostToolUsePayload> {
let tool_response =
result.post_tool_use_response(&invocation.call_id, &invocation.payload)?;
Some(PostToolUsePayload {
tool_name: HookToolName::apply_patch(),
tool_use_id: invocation.call_id.clone(),
tool_input: serde_json::json!({
"command": apply_patch_payload_command(&invocation.payload)?,
}),
tool_response,
})
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn intercept_apply_patch(
command: &[String],
+15 -12
View File
@@ -5,6 +5,7 @@ use crate::tools::context::FunctionToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::parse_arguments;
use crate::tools::registry::ToolExecutor;
use crate::tools::registry::ToolHandler;
use crate::tools::tool_search_entry::ToolSearchInfo;
use crate::turn_timing::now_unix_timestamp_ms;
@@ -52,7 +53,7 @@ impl DynamicToolHandler {
}
}
impl ToolHandler for DynamicToolHandler {
impl ToolExecutor<ToolInvocation> for DynamicToolHandler {
type Output = FunctionToolOutput;
fn tool_name(&self) -> ToolName {
@@ -63,17 +64,6 @@ impl ToolHandler for DynamicToolHandler {
self.spec.clone()
}
fn search_info(&self) -> Option<ToolSearchInfo> {
ToolSearchInfo::from_spec(
self.search_text.clone(),
self.spec()?,
Some(ToolSearchSourceInfo {
name: "Dynamic tools".to_string(),
description: Some("Tools provided by the current Codex thread.".to_string()),
}),
)
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let ToolInvocation {
session,
@@ -119,6 +109,19 @@ impl ToolHandler for DynamicToolHandler {
}
}
impl ToolHandler for DynamicToolHandler {
fn search_info(&self) -> Option<ToolSearchInfo> {
ToolSearchInfo::from_spec(
self.search_text.clone(),
self.spec()?,
Some(ToolSearchSourceInfo {
name: "Dynamic tools".to_string(),
description: Some("Tools provided by the current Codex thread.".to_string()),
}),
)
}
}
#[expect(
clippy::await_holding_invalid_type,
reason = "active turn checks and dynamic tool response registration must remain atomic"
@@ -16,6 +16,7 @@ use crate::tools::flat_tool_name;
use crate::tools::hook_names::HookToolName;
use crate::tools::registry::PostToolUsePayload;
use crate::tools::registry::PreToolUsePayload;
use crate::tools::registry::ToolExecutor;
use crate::tools::registry::ToolHandler;
pub(crate) struct BundledToolOutput {
@@ -68,7 +69,7 @@ impl BundledToolHandler {
}
}
impl ToolHandler for BundledToolHandler {
impl ToolExecutor<ToolInvocation> for BundledToolHandler {
type Output = BundledToolOutput;
fn tool_name(&self) -> ToolName {
@@ -79,6 +80,30 @@ impl ToolHandler for BundledToolHandler {
Some(self.spec.clone())
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let arguments = self
.arguments_from_payload(&invocation.payload)
.ok_or_else(|| {
FunctionCallError::Fatal(format!(
"tool {} invoked with incompatible payload",
self.bundle.tool_name()
))
})?
.to_string();
let value = self
.bundle
.executor()
.execute(codex_tool_api::ToolCall {
call_id: invocation.call_id,
arguments,
})
.await
.map_err(map_extension_tool_error)?;
Ok(BundledToolOutput { value })
}
}
impl ToolHandler for BundledToolHandler {
fn matches_kind(&self, payload: &ToolPayload) -> bool {
self.arguments_from_payload(payload).is_some()
}
@@ -105,28 +130,6 @@ impl ToolHandler for BundledToolHandler {
.post_tool_use_response(&invocation.call_id, &invocation.payload)?,
})
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let arguments = self
.arguments_from_payload(&invocation.payload)
.ok_or_else(|| {
FunctionCallError::Fatal(format!(
"tool {} invoked with incompatible payload",
self.bundle.tool_name()
))
})?
.to_string();
let value = self
.bundle
.executor()
.execute(codex_tool_api::ToolCall {
call_id: invocation.call_id,
arguments,
})
.await
.map_err(map_extension_tool_error)?;
Ok(BundledToolOutput { value })
}
}
pub(crate) fn extension_tool_spec(
@@ -6,6 +6,7 @@ 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::parse_arguments;
use crate::tools::registry::ToolExecutor;
use crate::tools::registry::ToolHandler;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
@@ -17,7 +18,7 @@ use super::goal_response;
pub struct CreateGoalHandler;
impl ToolHandler for CreateGoalHandler {
impl ToolExecutor<ToolInvocation> for CreateGoalHandler {
type Output = FunctionToolOutput;
fn tool_name(&self) -> ToolName {
@@ -71,3 +72,5 @@ impl ToolHandler for CreateGoalHandler {
goal_response(Some(goal), CompletionBudgetReport::Omit)
}
}
impl ToolHandler for CreateGoalHandler {}
@@ -4,6 +4,7 @@ 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::registry::ToolExecutor;
use crate::tools::registry::ToolHandler;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
@@ -14,7 +15,7 @@ use super::goal_response;
pub struct GetGoalHandler;
impl ToolHandler for GetGoalHandler {
impl ToolExecutor<ToolInvocation> for GetGoalHandler {
type Output = FunctionToolOutput;
fn tool_name(&self) -> ToolName {
@@ -44,3 +45,5 @@ impl ToolHandler for GetGoalHandler {
}
}
}
impl ToolHandler for GetGoalHandler {}
@@ -7,6 +7,7 @@ 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::parse_arguments;
use crate::tools::registry::ToolExecutor;
use crate::tools::registry::ToolHandler;
use codex_protocol::protocol::ThreadGoalStatus;
use codex_tools::ToolName;
@@ -19,7 +20,7 @@ use super::goal_response;
pub struct UpdateGoalHandler;
impl ToolHandler for UpdateGoalHandler {
impl ToolExecutor<ToolInvocation> for UpdateGoalHandler {
type Output = FunctionToolOutput;
fn tool_name(&self) -> ToolName {
@@ -74,3 +75,5 @@ impl ToolHandler for UpdateGoalHandler {
goal_response(Some(goal), CompletionBudgetReport::Include)
}
}
impl ToolHandler for UpdateGoalHandler {}
+47 -44
View File
@@ -12,6 +12,7 @@ use crate::tools::flat_tool_name;
use crate::tools::hook_names::HookToolName;
use crate::tools::registry::PostToolUsePayload;
use crate::tools::registry::PreToolUsePayload;
use crate::tools::registry::ToolExecutor;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolTelemetryTags;
use crate::tools::tool_search_entry::ToolSearchInfo;
@@ -35,7 +36,7 @@ impl McpHandler {
}
}
impl ToolHandler for McpHandler {
impl ToolExecutor<ToolInvocation> for McpHandler {
type Output = McpToolOutput;
fn tool_name(&self) -> ToolName {
@@ -70,6 +71,51 @@ impl ToolHandler for McpHandler {
}))
}
fn supports_parallel_tool_calls(&self) -> bool {
self.tool_info.supports_parallel_tool_calls
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let ToolInvocation {
session,
turn,
call_id,
payload,
..
} = invocation;
let payload = match payload {
ToolPayload::Function { arguments } => arguments,
_ => {
return Err(FunctionCallError::RespondToModel(
"mcp handler received unsupported payload".to_string(),
));
}
};
let started = Instant::now();
let result = handle_mcp_tool_call(
Arc::clone(&session),
&turn,
call_id.clone(),
self.tool_info.server_name.clone(),
self.tool_info.tool.name.to_string(),
self.tool_name().to_string(),
payload,
)
.await;
Ok(McpToolOutput {
result: result.result,
tool_input: result.tool_input,
wall_time: started.elapsed(),
original_image_detail_supported: can_request_original_image_detail(&turn.model_info),
truncation_policy: turn.truncation_policy,
})
}
}
impl ToolHandler for McpHandler {
fn search_info(&self) -> Option<ToolSearchInfo> {
let source_name = self
.tool_info
@@ -96,10 +142,6 @@ impl ToolHandler for McpHandler {
)
}
fn supports_parallel_tool_calls(&self) -> bool {
self.tool_info.supports_parallel_tool_calls
}
async fn telemetry_tags(&self, _invocation: &ToolInvocation) -> ToolTelemetryTags {
let mut tags = vec![("mcp_server", self.tool_info.server_name.clone())];
if let Some(origin) = &self.tool_info.server_origin {
@@ -159,45 +201,6 @@ impl ToolHandler for McpHandler {
tool_response,
})
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let ToolInvocation {
session,
turn,
call_id,
payload,
..
} = invocation;
let payload = match payload {
ToolPayload::Function { arguments } => arguments,
_ => {
return Err(FunctionCallError::RespondToModel(
"mcp handler received unsupported payload".to_string(),
));
}
};
let started = Instant::now();
let result = handle_mcp_tool_call(
Arc::clone(&session),
&turn,
call_id.clone(),
self.tool_info.server_name.clone(),
self.tool_info.tool.name.to_string(),
self.tool_name().to_string(),
payload,
)
.await;
Ok(McpToolOutput {
result: result.result,
tool_input: result.tool_input,
wall_time: started.elapsed(),
original_image_detail_supported: can_request_original_image_detail(&turn.model_info),
truncation_policy: turn.truncation_policy,
})
}
}
fn mcp_hook_tool_input(raw_arguments: &str) -> Value {
@@ -5,6 +5,7 @@ use crate::tools::context::FunctionToolOutput;
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::ToolExecutor;
use crate::tools::registry::ToolHandler;
use codex_protocol::models::function_call_output_content_items_to_text;
use codex_protocol::protocol::McpInvocation;
@@ -25,7 +26,7 @@ use super::serialize_function_output;
pub struct ListMcpResourceTemplatesHandler;
impl ToolHandler for ListMcpResourceTemplatesHandler {
impl ToolExecutor<ToolInvocation> for ListMcpResourceTemplatesHandler {
type Output = FunctionToolOutput;
fn tool_name(&self) -> ToolName {
@@ -163,3 +164,5 @@ impl ToolHandler for ListMcpResourceTemplatesHandler {
}
}
}
impl ToolHandler for ListMcpResourceTemplatesHandler {}
@@ -5,6 +5,7 @@ use crate::tools::context::FunctionToolOutput;
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::ToolExecutor;
use crate::tools::registry::ToolHandler;
use codex_protocol::models::function_call_output_content_items_to_text;
use codex_protocol::protocol::McpInvocation;
@@ -25,7 +26,7 @@ use super::serialize_function_output;
pub struct ListMcpResourcesHandler;
impl ToolHandler for ListMcpResourcesHandler {
impl ToolExecutor<ToolInvocation> for ListMcpResourcesHandler {
type Output = FunctionToolOutput;
fn tool_name(&self) -> ToolName {
@@ -161,3 +162,5 @@ impl ToolHandler for ListMcpResourcesHandler {
}
}
}
impl ToolHandler for ListMcpResourcesHandler {}
@@ -5,6 +5,7 @@ use crate::tools::context::FunctionToolOutput;
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::ToolExecutor;
use crate::tools::registry::ToolHandler;
use codex_protocol::models::function_call_output_content_items_to_text;
use codex_protocol::protocol::McpInvocation;
@@ -25,7 +26,7 @@ use super::serialize_function_output;
pub struct ReadMcpResourceHandler;
impl ToolHandler for ReadMcpResourceHandler {
impl ToolExecutor<ToolInvocation> for ReadMcpResourceHandler {
type Output = FunctionToolOutput;
fn tool_name(&self) -> ToolName {
@@ -144,3 +145,5 @@ impl ToolHandler for ReadMcpResourceHandler {
}
}
}
impl ToolHandler for ReadMcpResourceHandler {}
@@ -15,6 +15,7 @@ use crate::tools::context::ToolOutput;
use crate::tools::context::ToolPayload;
pub(crate) use crate::tools::handlers::multi_agents_common::*;
use crate::tools::handlers::parse_arguments;
use crate::tools::registry::ToolExecutor;
use crate::tools::registry::ToolHandler;
use codex_protocol::ThreadId;
use codex_protocol::models::ResponseInputItem;
@@ -5,7 +5,7 @@ use codex_tools::ToolSpec;
pub(crate) struct Handler;
impl ToolHandler for Handler {
impl ToolExecutor<ToolInvocation> for Handler {
type Output = CloseAgentResult;
fn tool_name(&self) -> ToolName {
@@ -16,10 +16,6 @@ impl ToolHandler for Handler {
Some(create_close_agent_tool_v1())
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
fn handle(
&self,
invocation: ToolInvocation,
@@ -111,6 +107,12 @@ async fn handle_close_agent(
})
}
impl ToolHandler for Handler {
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
}
#[derive(Debug, Deserialize, Serialize)]
pub(crate) struct CloseAgentResult {
pub(crate) previous_status: AgentStatus,
@@ -7,7 +7,7 @@ use std::sync::Arc;
pub(crate) struct Handler;
impl ToolHandler for Handler {
impl ToolExecutor<ToolInvocation> for Handler {
type Output = ResumeAgentResult;
fn tool_name(&self) -> ToolName {
@@ -18,10 +18,6 @@ impl ToolHandler for Handler {
Some(create_resume_agent_tool())
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
fn handle(
&self,
invocation: ToolInvocation,
@@ -139,6 +135,12 @@ async fn handle_resume_agent(
Ok(ResumeAgentResult { status })
}
impl ToolHandler for Handler {
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
}
#[derive(Debug, Deserialize)]
struct ResumeAgentArgs {
id: String,
@@ -6,7 +6,7 @@ use codex_tools::ToolSpec;
pub(crate) struct Handler;
impl ToolHandler for Handler {
impl ToolExecutor<ToolInvocation> for Handler {
type Output = SendInputResult;
fn tool_name(&self) -> ToolName {
@@ -17,10 +17,6 @@ impl ToolHandler for Handler {
Some(create_send_input_tool_v1())
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let ToolInvocation {
session,
@@ -92,6 +88,12 @@ impl ToolHandler for Handler {
}
}
impl ToolHandler for Handler {
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
}
#[derive(Debug, Deserialize)]
struct SendInputArgs {
target: String,
@@ -22,7 +22,7 @@ impl Handler {
}
}
impl ToolHandler for Handler {
impl ToolExecutor<ToolInvocation> for Handler {
type Output = SpawnAgentResult;
fn tool_name(&self) -> ToolName {
@@ -33,10 +33,6 @@ impl ToolHandler for Handler {
Some(create_spawn_agent_tool_v1(self.options.clone()))
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
fn handle(
&self,
invocation: ToolInvocation,
@@ -197,6 +193,12 @@ async fn handle_spawn_agent(
})
}
impl ToolHandler for Handler {
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
}
#[derive(Debug, Deserialize)]
struct SpawnAgentArgs {
message: Option<String>,
@@ -27,7 +27,7 @@ impl Handler {
}
}
impl ToolHandler for Handler {
impl ToolExecutor<ToolInvocation> for Handler {
type Output = WaitAgentResult;
fn tool_name(&self) -> ToolName {
@@ -38,10 +38,6 @@ impl ToolHandler for Handler {
Some(create_wait_agent_tool_v1(self.options))
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let ToolInvocation {
session,
@@ -204,6 +200,12 @@ impl ToolHandler for Handler {
}
}
impl ToolHandler for Handler {
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
}
#[derive(Debug, Deserialize)]
struct WaitArgs {
#[serde(default)]
@@ -8,6 +8,7 @@ use crate::tools::context::ToolOutput;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::multi_agents_common::*;
use crate::tools::handlers::parse_arguments;
use crate::tools::registry::ToolExecutor;
use crate::tools::registry::ToolHandler;
use codex_protocol::AgentPath;
use codex_protocol::models::ResponseInputItem;
@@ -5,7 +5,7 @@ use codex_tools::ToolSpec;
pub(crate) struct Handler;
impl ToolHandler for Handler {
impl ToolExecutor<ToolInvocation> for Handler {
type Output = CloseAgentResult;
fn tool_name(&self) -> ToolName {
@@ -16,10 +16,6 @@ impl ToolHandler for Handler {
Some(create_close_agent_tool_v2())
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
fn handle(
&self,
invocation: ToolInvocation,
@@ -123,6 +119,12 @@ async fn handle_close_agent(
})
}
impl ToolHandler for Handler {
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct CloseAgentArgs {
@@ -8,7 +8,7 @@ use codex_tools::ToolSpec;
pub(crate) struct Handler;
impl ToolHandler for Handler {
impl ToolExecutor<ToolInvocation> for Handler {
type Output = FunctionToolOutput;
fn tool_name(&self) -> ToolName {
@@ -19,10 +19,6 @@ impl ToolHandler for Handler {
Some(create_followup_task_tool())
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let arguments = function_arguments(invocation.payload.clone())?;
let args: FollowupTaskArgs = parse_arguments(&arguments)?;
@@ -35,3 +31,9 @@ impl ToolHandler for Handler {
.await
}
}
impl ToolHandler for Handler {
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
}
@@ -5,7 +5,7 @@ use codex_tools::ToolSpec;
pub(crate) struct Handler;
impl ToolHandler for Handler {
impl ToolExecutor<ToolInvocation> for Handler {
type Output = ListAgentsResult;
fn tool_name(&self) -> ToolName {
@@ -16,10 +16,6 @@ impl ToolHandler for Handler {
Some(create_list_agents_tool())
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let ToolInvocation {
session,
@@ -44,6 +40,12 @@ impl ToolHandler for Handler {
}
}
impl ToolHandler for Handler {
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct ListAgentsArgs {
@@ -8,7 +8,7 @@ use codex_tools::ToolSpec;
pub(crate) struct Handler;
impl ToolHandler for Handler {
impl ToolExecutor<ToolInvocation> for Handler {
type Output = FunctionToolOutput;
fn tool_name(&self) -> ToolName {
@@ -19,10 +19,6 @@ impl ToolHandler for Handler {
Some(create_send_message_tool())
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let arguments = function_arguments(invocation.payload.clone())?;
let args: SendMessageArgs = parse_arguments(&arguments)?;
@@ -35,3 +31,9 @@ impl ToolHandler for Handler {
.await
}
}
impl ToolHandler for Handler {
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
}
@@ -24,7 +24,7 @@ impl Handler {
}
}
impl ToolHandler for Handler {
impl ToolExecutor<ToolInvocation> for Handler {
type Output = SpawnAgentResult;
fn tool_name(&self) -> ToolName {
@@ -35,10 +35,6 @@ impl ToolHandler for Handler {
Some(create_spawn_agent_tool_v2(self.options.clone()))
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
fn handle(
&self,
invocation: ToolInvocation,
@@ -228,6 +224,12 @@ async fn handle_spawn_agent(
}
}
impl ToolHandler for Handler {
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct SpawnAgentArgs {
@@ -19,7 +19,7 @@ impl Handler {
}
}
impl ToolHandler for Handler {
impl ToolExecutor<ToolInvocation> for Handler {
type Output = WaitAgentResult;
fn tool_name(&self) -> ToolName {
@@ -30,10 +30,6 @@ impl ToolHandler for Handler {
Some(create_wait_agent_tool_v2(self.options))
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let ToolInvocation {
session,
@@ -101,6 +97,12 @@ impl ToolHandler for Handler {
}
}
impl ToolHandler for Handler {
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct WaitArgs {
+4 -1
View File
@@ -3,6 +3,7 @@ use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::plan_spec::create_update_plan_tool;
use crate::tools::registry::ToolExecutor;
use crate::tools::registry::ToolHandler;
use codex_protocol::config_types::ModeKind;
use codex_protocol::models::FunctionCallOutputPayload;
@@ -43,7 +44,7 @@ impl ToolOutput for PlanToolOutput {
}
}
impl ToolHandler for PlanHandler {
impl ToolExecutor<ToolInvocation> for PlanHandler {
type Output = PlanToolOutput;
fn tool_name(&self) -> ToolName {
@@ -87,6 +88,8 @@ impl ToolHandler for PlanHandler {
}
}
impl ToolHandler for PlanHandler {}
fn parse_update_plan_arguments(arguments: &str) -> Result<UpdatePlanArgs, FunctionCallError> {
serde_json::from_str::<UpdatePlanArgs>(arguments).map_err(|e| {
FunctionCallError::RespondToModel(format!("failed to parse function arguments: {e}"))
@@ -8,13 +8,14 @@ use crate::tools::context::ToolPayload;
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::ToolExecutor;
use crate::tools::registry::ToolHandler;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
pub struct RequestPermissionsHandler;
impl ToolHandler for RequestPermissionsHandler {
impl ToolExecutor<ToolInvocation> for RequestPermissionsHandler {
type Output = FunctionToolOutput;
fn tool_name(&self) -> ToolName {
@@ -75,3 +76,5 @@ impl ToolHandler for RequestPermissionsHandler {
Ok(FunctionToolOutput::from_text(content, Some(true)))
}
}
impl ToolHandler for RequestPermissionsHandler {}
@@ -34,6 +34,7 @@ use crate::tools::context::ToolInvocation;
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::ToolExecutor;
use crate::tools::registry::ToolHandler;
#[derive(Default)]
@@ -49,7 +50,7 @@ impl RequestPluginInstallHandler {
}
}
impl ToolHandler for RequestPluginInstallHandler {
impl ToolExecutor<ToolInvocation> for RequestPluginInstallHandler {
type Output = FunctionToolOutput;
fn tool_name(&self) -> ToolName {
@@ -192,6 +193,8 @@ impl ToolHandler for RequestPluginInstallHandler {
}
}
impl ToolHandler for RequestPluginInstallHandler {}
async fn maybe_persist_disabled_install_request(
session: &crate::session::session::Session,
turn: &crate::session::turn_context::TurnContext,
@@ -8,6 +8,7 @@ use crate::tools::handlers::request_user_input_spec::create_request_user_input_t
use crate::tools::handlers::request_user_input_spec::normalize_request_user_input_args;
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::ToolExecutor;
use crate::tools::registry::ToolHandler;
use codex_protocol::config_types::ModeKind;
use codex_protocol::request_user_input::RequestUserInputArgs;
@@ -18,7 +19,7 @@ pub struct RequestUserInputHandler {
pub available_modes: Vec<ModeKind>,
}
impl ToolHandler for RequestUserInputHandler {
impl ToolExecutor<ToolInvocation> for RequestUserInputHandler {
type Output = FunctionToolOutput;
fn tool_name(&self) -> ToolName {
@@ -82,6 +83,8 @@ impl ToolHandler for RequestUserInputHandler {
}
}
impl ToolHandler for RequestUserInputHandler {}
#[cfg(test)]
#[path = "request_user_input_tests.rs"]
mod tests;
@@ -9,6 +9,7 @@ use crate::tools::handlers::parse_arguments_with_base_path;
use crate::tools::handlers::resolve_workdir_base_path;
use crate::tools::registry::PostToolUsePayload;
use crate::tools::registry::PreToolUsePayload;
use crate::tools::registry::ToolExecutor;
use crate::tools::registry::ToolHandler;
use crate::tools::runtimes::shell::ShellRuntimeBackend;
@@ -21,37 +22,13 @@ use super::shell_handler::ShellHandler;
pub struct ContainerExecHandler;
impl ToolHandler for ContainerExecHandler {
impl ToolExecutor<ToolInvocation> for ContainerExecHandler {
type Output = FunctionToolOutput;
fn tool_name(&self) -> ToolName {
ToolName::plain("container.exec")
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
fn pre_tool_use_payload(&self, invocation: &ToolInvocation) -> Option<PreToolUsePayload> {
shell_function_pre_tool_use_payload(invocation)
}
fn with_updated_hook_input(
&self,
invocation: ToolInvocation,
updated_input: serde_json::Value,
) -> Result<ToolInvocation, FunctionCallError> {
rewrite_shell_function_updated_hook_input(invocation, updated_input, "container.exec")
}
fn post_tool_use_payload(
&self,
invocation: &ToolInvocation,
result: &Self::Output,
) -> Option<PostToolUsePayload> {
shell_function_post_tool_use_payload(invocation, result)
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let ToolInvocation {
session,
@@ -92,3 +69,29 @@ impl ToolHandler for ContainerExecHandler {
.await
}
}
impl ToolHandler for ContainerExecHandler {
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
fn pre_tool_use_payload(&self, invocation: &ToolInvocation) -> Option<PreToolUsePayload> {
shell_function_pre_tool_use_payload(invocation)
}
fn with_updated_hook_input(
&self,
invocation: ToolInvocation,
updated_input: serde_json::Value,
) -> Result<ToolInvocation, FunctionCallError> {
rewrite_shell_function_updated_hook_input(invocation, updated_input, "container.exec")
}
fn post_tool_use_payload(
&self,
invocation: &ToolInvocation,
result: &Self::Output,
) -> Option<PostToolUsePayload> {
shell_function_post_tool_use_payload(invocation, result)
}
}
@@ -9,6 +9,7 @@ use crate::tools::handlers::updated_hook_command;
use crate::tools::hook_names::HookToolName;
use crate::tools::registry::PostToolUsePayload;
use crate::tools::registry::PreToolUsePayload;
use crate::tools::registry::ToolExecutor;
use crate::tools::registry::ToolHandler;
use crate::tools::runtimes::shell::ShellRuntimeBackend;
use codex_tools::ToolSpec;
@@ -30,7 +31,7 @@ impl LocalShellHandler {
}
}
impl ToolHandler for LocalShellHandler {
impl ToolExecutor<ToolInvocation> for LocalShellHandler {
type Output = FunctionToolOutput;
fn tool_name(&self) -> ToolName {
@@ -41,14 +42,50 @@ impl ToolHandler for LocalShellHandler {
self.include_spec.then(create_local_shell_tool)
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::LocalShell { .. })
}
fn supports_parallel_tool_calls(&self) -> bool {
self.include_spec
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let ToolInvocation {
session,
turn,
tracker,
call_id,
payload,
..
} = invocation;
let ToolPayload::LocalShell { params } = payload else {
return Err(FunctionCallError::RespondToModel(
"unsupported payload for local_shell handler".to_string(),
));
};
let exec_params =
ShellHandler::to_exec_params(&params, turn.as_ref(), session.conversation_id);
run_exec_like(RunExecLikeArgs {
tool_name: ToolName::plain("local_shell"),
exec_params,
hook_command: codex_shell_command::parse_command::shlex_join(&params.command),
additional_permissions: None,
prefix_rule: None,
session,
turn,
tracker,
call_id,
freeform: false,
shell_runtime_backend: ShellRuntimeBackend::Generic,
})
.await
}
}
impl ToolHandler for LocalShellHandler {
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::LocalShell { .. })
}
fn pre_tool_use_payload(&self, invocation: &ToolInvocation) -> Option<PreToolUsePayload> {
local_shell_payload_command(&invocation.payload).map(|command| PreToolUsePayload {
tool_name: HookToolName::bash(),
@@ -91,38 +128,4 @@ impl ToolHandler for LocalShellHandler {
tool_response,
})
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let ToolInvocation {
session,
turn,
tracker,
call_id,
payload,
..
} = invocation;
let ToolPayload::LocalShell { params } = payload else {
return Err(FunctionCallError::RespondToModel(
"unsupported payload for local_shell handler".to_string(),
));
};
let exec_params =
ShellHandler::to_exec_params(&params, turn.as_ref(), session.conversation_id);
run_exec_like(RunExecLikeArgs {
tool_name: ToolName::plain("local_shell"),
exec_params,
hook_command: codex_shell_command::parse_command::shlex_join(&params.command),
additional_permissions: None,
prefix_rule: None,
session,
turn,
tracker,
call_id,
freeform: false,
shell_runtime_backend: ShellRuntimeBackend::Generic,
})
.await
}
}
@@ -21,6 +21,7 @@ use crate::tools::handlers::updated_hook_command;
use crate::tools::hook_names::HookToolName;
use crate::tools::registry::PostToolUsePayload;
use crate::tools::registry::PreToolUsePayload;
use crate::tools::registry::ToolExecutor;
use crate::tools::registry::ToolHandler;
use crate::tools::runtimes::shell::ShellRuntimeBackend;
use codex_tools::ToolSpec;
@@ -124,7 +125,7 @@ impl From<ShellCommandBackendConfig> for ShellCommandHandler {
}
}
impl ToolHandler for ShellCommandHandler {
impl ToolExecutor<ToolInvocation> for ShellCommandHandler {
type Output = FunctionToolOutput;
fn tool_name(&self) -> ToolName {
@@ -140,58 +141,10 @@ impl ToolHandler for ShellCommandHandler {
})
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
fn supports_parallel_tool_calls(&self) -> bool {
self.options.is_some()
}
fn pre_tool_use_payload(&self, invocation: &ToolInvocation) -> Option<PreToolUsePayload> {
shell_command_payload_command(&invocation.payload).map(|command| PreToolUsePayload {
tool_name: HookToolName::bash(),
tool_input: serde_json::json!({ "command": command }),
})
}
fn with_updated_hook_input(
&self,
mut invocation: ToolInvocation,
updated_input: serde_json::Value,
) -> Result<ToolInvocation, FunctionCallError> {
let ToolPayload::Function { arguments } = invocation.payload else {
return Err(FunctionCallError::RespondToModel(
"hook input rewrite received unsupported shell_command payload".to_string(),
));
};
invocation.payload = ToolPayload::Function {
arguments: rewrite_function_string_argument(
&arguments,
"shell_command",
"command",
updated_hook_command(&updated_input)?,
)?,
};
Ok(invocation)
}
fn post_tool_use_payload(
&self,
invocation: &ToolInvocation,
result: &Self::Output,
) -> Option<PostToolUsePayload> {
let tool_response =
result.post_tool_use_response(&invocation.call_id, &invocation.payload)?;
let command = shell_command_payload_command(&invocation.payload)?;
Some(PostToolUsePayload {
tool_name: HookToolName::bash(),
tool_use_id: invocation.call_id.clone(),
tool_input: serde_json::json!({ "command": command }),
tool_response,
})
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let ToolInvocation {
session,
@@ -243,3 +196,53 @@ impl ToolHandler for ShellCommandHandler {
.await
}
}
impl ToolHandler for ShellCommandHandler {
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
fn pre_tool_use_payload(&self, invocation: &ToolInvocation) -> Option<PreToolUsePayload> {
shell_command_payload_command(&invocation.payload).map(|command| PreToolUsePayload {
tool_name: HookToolName::bash(),
tool_input: serde_json::json!({ "command": command }),
})
}
fn with_updated_hook_input(
&self,
mut invocation: ToolInvocation,
updated_input: serde_json::Value,
) -> Result<ToolInvocation, FunctionCallError> {
let ToolPayload::Function { arguments } = invocation.payload else {
return Err(FunctionCallError::RespondToModel(
"hook input rewrite received unsupported shell_command payload".to_string(),
));
};
invocation.payload = ToolPayload::Function {
arguments: rewrite_function_string_argument(
&arguments,
"shell_command",
"command",
updated_hook_command(&updated_input)?,
)?,
};
Ok(invocation)
}
fn post_tool_use_payload(
&self,
invocation: &ToolInvocation,
result: &Self::Output,
) -> Option<PostToolUsePayload> {
let tool_response =
result.post_tool_use_response(&invocation.call_id, &invocation.payload)?;
let command = shell_command_payload_command(&invocation.payload)?;
Some(PostToolUsePayload {
tool_name: HookToolName::bash(),
tool_use_id: invocation.call_id.clone(),
tool_input: serde_json::json!({ "command": command }),
tool_response,
})
}
}
@@ -14,6 +14,7 @@ use crate::tools::handlers::parse_arguments_with_base_path;
use crate::tools::handlers::resolve_workdir_base_path;
use crate::tools::registry::PostToolUsePayload;
use crate::tools::registry::PreToolUsePayload;
use crate::tools::registry::ToolExecutor;
use crate::tools::registry::ToolHandler;
use crate::tools::runtimes::shell::ShellRuntimeBackend;
use codex_tools::ToolSpec;
@@ -62,7 +63,7 @@ impl ShellHandler {
}
}
impl ToolHandler for ShellHandler {
impl ToolExecutor<ToolInvocation> for ShellHandler {
type Output = FunctionToolOutput;
fn tool_name(&self) -> ToolName {
@@ -73,34 +74,10 @@ impl ToolHandler for ShellHandler {
self.options.map(create_shell_tool)
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
fn supports_parallel_tool_calls(&self) -> bool {
self.options.is_some()
}
fn pre_tool_use_payload(&self, invocation: &ToolInvocation) -> Option<PreToolUsePayload> {
shell_function_pre_tool_use_payload(invocation)
}
fn with_updated_hook_input(
&self,
invocation: ToolInvocation,
updated_input: serde_json::Value,
) -> Result<ToolInvocation, FunctionCallError> {
rewrite_shell_function_updated_hook_input(invocation, updated_input, "shell")
}
fn post_tool_use_payload(
&self,
invocation: &ToolInvocation,
result: &Self::Output,
) -> Option<PostToolUsePayload> {
shell_function_post_tool_use_payload(invocation, result)
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let ToolInvocation {
session,
@@ -141,3 +118,29 @@ impl ToolHandler for ShellHandler {
.await
}
}
impl ToolHandler for ShellHandler {
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
fn pre_tool_use_payload(&self, invocation: &ToolInvocation) -> Option<PreToolUsePayload> {
shell_function_pre_tool_use_payload(invocation)
}
fn with_updated_hook_input(
&self,
invocation: ToolInvocation,
updated_input: serde_json::Value,
) -> Result<ToolInvocation, FunctionCallError> {
rewrite_shell_function_updated_hook_input(invocation, updated_input, "shell")
}
fn post_tool_use_payload(
&self,
invocation: &ToolInvocation,
result: &Self::Output,
) -> Option<PostToolUsePayload> {
shell_function_post_tool_use_payload(invocation, result)
}
}
@@ -14,6 +14,7 @@ use crate::tools::context::ToolInvocation;
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::ToolExecutor;
use crate::tools::registry::ToolHandler;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
@@ -55,7 +56,7 @@ fn barrier_map() -> &'static tokio::sync::Mutex<HashMap<String, BarrierState>> {
BARRIERS.get_or_init(|| tokio::sync::Mutex::new(HashMap::new()))
}
impl ToolHandler for TestSyncHandler {
impl ToolExecutor<ToolInvocation> for TestSyncHandler {
type Output = FunctionToolOutput;
fn tool_name(&self) -> ToolName {
@@ -104,6 +105,8 @@ impl ToolHandler for TestSyncHandler {
}
}
impl ToolHandler for TestSyncHandler {}
async fn wait_on_barrier(args: BarrierArgs) -> Result<(), FunctionCallError> {
if args.participants == 0 {
return Err(FunctionCallError::RespondToModel(
@@ -3,6 +3,7 @@ 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::ToolExecutor;
use crate::tools::registry::ToolHandler;
use crate::tools::tool_search_entry::ToolSearchEntry;
use crate::tools::tool_search_entry::ToolSearchInfo;
@@ -51,7 +52,7 @@ impl ToolSearchHandler {
}
}
impl ToolHandler for ToolSearchHandler {
impl ToolExecutor<ToolInvocation> for ToolSearchHandler {
type Output = ToolSearchOutput;
fn tool_name(&self) -> ToolName {
@@ -108,6 +109,8 @@ impl ToolHandler for ToolSearchHandler {
}
}
impl ToolHandler for ToolSearchHandler {}
impl ToolSearchHandler {
fn search(
&self,
@@ -17,6 +17,7 @@ use crate::tools::handlers::updated_hook_command;
use crate::tools::hook_names::HookToolName;
use crate::tools::registry::PostToolUsePayload;
use crate::tools::registry::PreToolUsePayload;
use crate::tools::registry::ToolExecutor;
use crate::tools::registry::ToolHandler;
use crate::unified_exec::ExecCommandRequest;
use crate::unified_exec::UnifiedExecContext;
@@ -67,7 +68,7 @@ impl ExecCommandHandler {
}
}
impl ToolHandler for ExecCommandHandler {
impl ToolExecutor<ToolInvocation> for ExecCommandHandler {
type Output = ExecCommandToolOutput;
fn tool_name(&self) -> ToolName {
@@ -84,56 +85,10 @@ impl ToolHandler for ExecCommandHandler {
))
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
fn supports_parallel_tool_calls(&self) -> bool {
true
}
fn pre_tool_use_payload(&self, invocation: &ToolInvocation) -> Option<PreToolUsePayload> {
let ToolPayload::Function { arguments } = &invocation.payload else {
return None;
};
parse_arguments::<ExecCommandArgs>(arguments)
.ok()
.map(|args| PreToolUsePayload {
tool_name: HookToolName::bash(),
tool_input: serde_json::json!({ "command": args.cmd }),
})
}
fn with_updated_hook_input(
&self,
mut invocation: ToolInvocation,
updated_input: serde_json::Value,
) -> Result<ToolInvocation, FunctionCallError> {
let ToolPayload::Function { arguments } = invocation.payload else {
return Err(FunctionCallError::RespondToModel(
"hook input rewrite received unsupported exec_command payload".to_string(),
));
};
invocation.payload = ToolPayload::Function {
arguments: rewrite_function_string_argument(
&arguments,
"exec_command",
"cmd",
updated_hook_command(&updated_input)?,
)?,
};
Ok(invocation)
}
fn post_tool_use_payload(
&self,
invocation: &ToolInvocation,
result: &Self::Output,
) -> Option<PostToolUsePayload> {
post_unified_exec_tool_use_payload(invocation, result)
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let ToolInvocation {
session,
@@ -339,6 +294,54 @@ impl ToolHandler for ExecCommandHandler {
}
}
impl ToolHandler for ExecCommandHandler {
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
fn pre_tool_use_payload(&self, invocation: &ToolInvocation) -> Option<PreToolUsePayload> {
let ToolPayload::Function { arguments } = &invocation.payload else {
return None;
};
parse_arguments::<ExecCommandArgs>(arguments)
.ok()
.map(|args| PreToolUsePayload {
tool_name: HookToolName::bash(),
tool_input: serde_json::json!({ "command": args.cmd }),
})
}
fn with_updated_hook_input(
&self,
mut invocation: ToolInvocation,
updated_input: serde_json::Value,
) -> Result<ToolInvocation, FunctionCallError> {
let ToolPayload::Function { arguments } = invocation.payload else {
return Err(FunctionCallError::RespondToModel(
"hook input rewrite received unsupported exec_command payload".to_string(),
));
};
invocation.payload = ToolPayload::Function {
arguments: rewrite_function_string_argument(
&arguments,
"exec_command",
"cmd",
updated_hook_command(&updated_input)?,
)?,
};
Ok(invocation)
}
fn post_tool_use_payload(
&self,
invocation: &ToolInvocation,
result: &Self::Output,
) -> Option<PostToolUsePayload> {
post_unified_exec_tool_use_payload(invocation, result)
}
}
fn emit_unified_exec_tty_metric(session_telemetry: &SessionTelemetry, tty: bool) {
session_telemetry.counter(
TOOL_CALL_UNIFIED_EXEC_METRIC,
@@ -4,6 +4,7 @@ use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::parse_arguments;
use crate::tools::registry::PostToolUsePayload;
use crate::tools::registry::ToolExecutor;
use crate::tools::registry::ToolHandler;
use crate::unified_exec::WriteStdinRequest;
use codex_protocol::protocol::EventMsg;
@@ -30,7 +31,7 @@ struct WriteStdinArgs {
pub struct WriteStdinHandler;
impl ToolHandler for WriteStdinHandler {
impl ToolExecutor<ToolInvocation> for WriteStdinHandler {
type Output = ExecCommandToolOutput;
fn tool_name(&self) -> ToolName {
@@ -41,18 +42,6 @@ impl ToolHandler for WriteStdinHandler {
Some(create_write_stdin_tool())
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
fn post_tool_use_payload(
&self,
invocation: &ToolInvocation,
result: &Self::Output,
) -> Option<PostToolUsePayload> {
post_unified_exec_tool_use_payload(invocation, result)
}
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
let ToolInvocation {
session,
@@ -99,3 +88,17 @@ impl ToolHandler for WriteStdinHandler {
Ok(response)
}
}
impl ToolHandler for WriteStdinHandler {
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
fn post_tool_use_payload(
&self,
invocation: &ToolInvocation,
result: &Self::Output,
) -> Option<PostToolUsePayload> {
post_unified_exec_tool_use_payload(invocation, result)
}
}
@@ -20,6 +20,7 @@ use crate::tools::handlers::parse_arguments;
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::ToolExecutor;
use crate::tools::registry::ToolHandler;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
@@ -61,7 +62,7 @@ enum ViewImageDetail {
Original,
}
impl ToolHandler for ViewImageHandler {
impl ToolExecutor<ToolInvocation> for ViewImageHandler {
type Output = ViewImageOutput;
fn tool_name(&self) -> ToolName {
@@ -201,6 +202,8 @@ impl ToolHandler for ViewImageHandler {
}
}
impl ToolHandler for ViewImageHandler {}
pub struct ViewImageOutput {
image_url: String,
image_detail: Option<ImageDetail>,
+8 -25
View File
@@ -1,4 +1,5 @@
use std::collections::HashMap;
use std::future::Future;
use std::sync::Arc;
use std::time::Duration;
@@ -34,24 +35,13 @@ use tracing::warn;
pub(crate) type ToolTelemetryTags = Vec<(&'static str, String)>;
pub trait ToolHandler: Send + Sync {
type Output: ToolOutput + 'static;
/// The concrete tool name handled by this handler instance.
fn tool_name(&self) -> ToolName;
fn spec(&self) -> Option<ToolSpec> {
None
}
pub use codex_tools::ToolExecutor;
pub trait ToolHandler: ToolExecutor<ToolInvocation> {
fn search_info(&self) -> Option<ToolSearchInfo> {
None
}
fn supports_parallel_tool_calls(&self) -> bool {
false
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(
payload,
@@ -62,7 +52,7 @@ pub trait ToolHandler: Send + Sync {
fn telemetry_tags(
&self,
_invocation: &ToolInvocation,
) -> impl std::future::Future<Output = ToolTelemetryTags> + Send {
) -> impl Future<Output = ToolTelemetryTags> + Send {
async { Vec::new() }
}
@@ -96,13 +86,6 @@ pub trait ToolHandler: Send + Sync {
fn create_diff_consumer(&self) -> Option<Box<dyn ToolArgumentDiffConsumer>> {
None
}
/// Perform the actual [ToolInvocation] and returns a [ToolOutput] containing
/// the final output to return to the model.
fn handle(
&self,
invocation: ToolInvocation,
) -> impl std::future::Future<Output = Result<Self::Output, FunctionCallError>> + Send;
}
/// Consumes streamed argument diffs for a tool call and emits protocol events
@@ -209,11 +192,11 @@ where
T: ToolHandler,
{
fn tool_name(&self) -> ToolName {
ToolHandler::tool_name(self)
ToolExecutor::tool_name(self)
}
fn spec(&self) -> Option<ToolSpec> {
ToolHandler::spec(self)
ToolExecutor::spec(self)
}
fn search_info(&self) -> Option<ToolSearchInfo> {
@@ -221,7 +204,7 @@ where
}
fn supports_parallel_tool_calls(&self) -> bool {
ToolHandler::supports_parallel_tool_calls(self)
ToolExecutor::supports_parallel_tool_calls(self)
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
@@ -257,7 +240,7 @@ where
Box::pin(async move {
let call_id = invocation.call_id.clone();
let payload = invocation.payload.clone();
let output = self.handle(invocation.clone()).await?;
let output = ToolExecutor::handle(self, invocation.clone()).await?;
let post_tool_use_payload =
ToolHandler::post_tool_use_payload(self, &invocation, &output);
Ok(AnyToolResult {
+3 -1
View File
@@ -8,7 +8,7 @@ struct TestHandler {
tool_name: codex_tools::ToolName,
}
impl ToolHandler for TestHandler {
impl ToolExecutor<ToolInvocation> for TestHandler {
type Output = crate::tools::context::FunctionToolOutput;
fn tool_name(&self) -> codex_tools::ToolName {
@@ -23,6 +23,8 @@ impl ToolHandler for TestHandler {
}
}
impl ToolHandler for TestHandler {}
#[test]
fn handler_looks_up_namespaced_aliases_explicitly() {
let namespace = "mcp__codex_apps__gmail";
@@ -21,6 +21,7 @@ use crate::tools::context::FunctionToolOutput;
use crate::tools::context::ToolCallSource;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
use crate::tools::registry::ToolExecutor;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolRegistry;
use crate::turn_diff_tracker::TurnDiffTracker;
@@ -29,7 +30,7 @@ struct TestHandler {
tool_name: codex_tools::ToolName,
}
impl ToolHandler for TestHandler {
impl ToolExecutor<ToolInvocation> for TestHandler {
type Output = FunctionToolOutput;
fn tool_name(&self) -> codex_tools::ToolName {
@@ -41,6 +42,8 @@ impl ToolHandler for TestHandler {
}
}
impl ToolHandler for TestHandler {}
#[tokio::test]
async fn dispatch_lifecycle_trace_records_direct_and_code_mode_requesters() -> anyhow::Result<()> {
let temp = TempDir::new()?;
+2
View File
@@ -14,6 +14,7 @@ codex-features = { workspace = true }
codex-protocol = { workspace = true }
codex-utils-absolute-path = { workspace = true }
codex-utils-pty = { workspace = true }
codex-utils-string = { workspace = true }
rmcp = { workspace = true, default-features = false, features = [
"base64",
"macros",
@@ -22,6 +23,7 @@ rmcp = { workspace = true, default-features = false, features = [
] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
[dev-dependencies]
+12
View File
@@ -0,0 +1,12 @@
use thiserror::Error;
/// Error returned while executing a model-visible tool invocation.
#[derive(Debug, Error, PartialEq)]
pub enum FunctionCallError {
#[error("{0}")]
RespondToModel(String),
#[error("LocalShellCall without call_id or id")]
MissingLocalShellCallId,
#[error("Fatal error: {0}")]
Fatal(String),
}
+8
View File
@@ -3,6 +3,7 @@
mod code_mode;
mod dynamic_tool;
mod function_call_error;
mod image_detail;
mod json_schema;
mod mcp_tool;
@@ -11,6 +12,9 @@ mod responses_api;
mod tool_config;
mod tool_definition;
mod tool_discovery;
mod tool_executor;
mod tool_output;
mod tool_payload;
mod tool_spec;
pub use code_mode::augment_tool_spec_for_code_mode;
@@ -20,6 +24,7 @@ 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 dynamic_tool::parse_dynamic_tool;
pub use function_call_error::FunctionCallError;
pub use image_detail::can_request_original_image_detail;
pub use image_detail::normalize_output_image_detail;
pub use image_detail::sanitize_original_image_detail;
@@ -71,6 +76,9 @@ pub use tool_discovery::TOOL_SEARCH_TOOL_NAME;
pub use tool_discovery::ToolSearchSourceInfo;
pub use tool_discovery::collect_request_plugin_install_entries;
pub use tool_discovery::filter_request_plugin_install_discoverable_tools_for_client;
pub use tool_executor::ToolExecutor;
pub use tool_output::ToolOutput;
pub use tool_payload::ToolPayload;
pub use tool_spec::ResponsesApiWebSearchFilters;
pub use tool_spec::ResponsesApiWebSearchUserLocation;
pub use tool_spec::ToolSpec;
+31
View File
@@ -0,0 +1,31 @@
use std::future::Future;
use crate::FunctionCallError;
use crate::ToolName;
use crate::ToolOutput;
use crate::ToolSpec;
/// Shared runtime contract for model-visible tools.
///
/// Implementations keep the model-visible spec tied to the executable runtime.
/// Host crates can layer routing, hooks, telemetry, or other orchestration on
/// top without reopening the spec/runtime split.
pub trait ToolExecutor<Invocation>: Send + Sync {
type Output: ToolOutput + 'static;
/// The concrete tool name handled by this runtime instance.
fn tool_name(&self) -> ToolName;
fn spec(&self) -> Option<ToolSpec> {
None
}
fn supports_parallel_tool_calls(&self) -> bool {
false
}
fn handle(
&self,
invocation: Invocation,
) -> impl Future<Output = Result<Self::Output, FunctionCallError>> + Send;
}
+156
View File
@@ -0,0 +1,156 @@
use codex_protocol::models::DEFAULT_IMAGE_DETAIL;
use codex_protocol::models::FunctionCallOutputBody;
use codex_protocol::models::FunctionCallOutputContentItem;
use codex_protocol::models::ResponseInputItem;
use codex_utils_string::take_bytes_at_char_boundary;
use serde_json::Value as JsonValue;
use crate::ToolPayload;
const TELEMETRY_PREVIEW_MAX_BYTES: usize = 2 * 1024;
const TELEMETRY_PREVIEW_MAX_LINES: usize = 64;
const TELEMETRY_PREVIEW_TRUNCATION_NOTICE: &str = "[... telemetry preview truncated ...]";
/// Model-facing output contract returned by executable tool runtimes.
pub trait ToolOutput: Send {
fn log_preview(&self) -> String;
fn success_for_logging(&self) -> bool;
fn to_response_item(&self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem;
/// Returns the stable value exposed to `PostToolUse` hooks for this tool output.
///
/// Tool handlers decide whether a tool participates in `PostToolUse`, but
/// this method lets the output type own any conversion from model-facing
/// response content to hook-facing data. Returning `None` means the output
/// should not produce a post-use hook payload, not merely that the tool had
/// empty output.
fn post_tool_use_response(&self, _call_id: &str, _payload: &ToolPayload) -> Option<JsonValue> {
None
}
fn code_mode_result(&self, payload: &ToolPayload) -> JsonValue {
response_input_to_code_mode_result(self.to_response_item("", payload))
}
}
impl ToolOutput for codex_protocol::mcp::CallToolResult {
fn log_preview(&self) -> String {
let output = self.as_function_call_output_payload();
let preview = output.body.to_text().unwrap_or_else(|| output.to_string());
telemetry_preview(&preview)
}
fn success_for_logging(&self) -> bool {
self.success()
}
fn to_response_item(&self, call_id: &str, _payload: &ToolPayload) -> ResponseInputItem {
ResponseInputItem::McpToolCallOutput {
call_id: call_id.to_string(),
output: self.clone(),
}
}
fn code_mode_result(&self, _payload: &ToolPayload) -> JsonValue {
serde_json::to_value(self).unwrap_or_else(|err| {
JsonValue::String(format!("failed to serialize mcp result: {err}"))
})
}
}
fn response_input_to_code_mode_result(response: ResponseInputItem) -> JsonValue {
match response {
ResponseInputItem::Message { content, .. } => content_items_to_code_mode_result(
&content
.into_iter()
.map(|item| match item {
codex_protocol::models::ContentItem::InputText { text }
| codex_protocol::models::ContentItem::OutputText { text } => {
FunctionCallOutputContentItem::InputText { text }
}
codex_protocol::models::ContentItem::InputImage { image_url, detail } => {
FunctionCallOutputContentItem::InputImage {
image_url,
detail: detail.or(Some(DEFAULT_IMAGE_DETAIL)),
}
}
})
.collect::<Vec<_>>(),
),
ResponseInputItem::FunctionCallOutput { output, .. }
| ResponseInputItem::CustomToolCallOutput { output, .. } => match output.body {
FunctionCallOutputBody::Text(text) => JsonValue::String(text),
FunctionCallOutputBody::ContentItems(items) => {
content_items_to_code_mode_result(&items)
}
},
ResponseInputItem::ToolSearchOutput { tools, .. } => JsonValue::Array(tools),
ResponseInputItem::McpToolCallOutput { output, .. } => serde_json::to_value(output)
.unwrap_or_else(|err| {
JsonValue::String(format!("failed to serialize mcp result: {err}"))
}),
}
}
fn content_items_to_code_mode_result(items: &[FunctionCallOutputContentItem]) -> JsonValue {
JsonValue::String(
items
.iter()
.filter_map(|item| match item {
FunctionCallOutputContentItem::InputText { text } if !text.trim().is_empty() => {
Some(text.clone())
}
FunctionCallOutputContentItem::InputImage { image_url, .. }
if !image_url.trim().is_empty() =>
{
Some(image_url.clone())
}
FunctionCallOutputContentItem::InputText { .. }
| FunctionCallOutputContentItem::InputImage { .. } => None,
})
.collect::<Vec<_>>()
.join("\n"),
)
}
fn telemetry_preview(content: &str) -> String {
let truncated_slice = take_bytes_at_char_boundary(content, TELEMETRY_PREVIEW_MAX_BYTES);
let truncated_by_bytes = truncated_slice.len() < content.len();
let mut preview = String::new();
let mut lines_iter = truncated_slice.lines();
for idx in 0..TELEMETRY_PREVIEW_MAX_LINES {
match lines_iter.next() {
Some(line) => {
if idx > 0 {
preview.push('\n');
}
preview.push_str(line);
}
None => break,
}
}
let truncated_by_lines = lines_iter.next().is_some();
if !truncated_by_bytes && !truncated_by_lines {
return content.to_string();
}
if preview.len() < truncated_slice.len()
&& truncated_slice
.as_bytes()
.get(preview.len())
.is_some_and(|byte| *byte == b'\n')
{
preview.push('\n');
}
if !preview.is_empty() && !preview.ends_with('\n') {
preview.push('\n');
}
preview.push_str(TELEMETRY_PREVIEW_TRUNCATION_NOTICE);
preview
}
+24
View File
@@ -0,0 +1,24 @@
use std::borrow::Cow;
use codex_protocol::models::SearchToolCallParams;
use codex_protocol::models::ShellToolCallParams;
/// Canonical payload shapes accepted by model-visible tool runtimes.
#[derive(Clone, Debug)]
pub enum ToolPayload {
Function { arguments: String },
ToolSearch { arguments: SearchToolCallParams },
Custom { input: String },
LocalShell { params: ShellToolCallParams },
}
impl ToolPayload {
pub fn log_payload(&self) -> Cow<'_, str> {
match self {
ToolPayload::Function { arguments } => Cow::Borrowed(arguments),
ToolPayload::ToolSearch { arguments } => Cow::Owned(arguments.query.clone()),
ToolPayload::Custom { input } => Cow::Borrowed(input),
ToolPayload::LocalShell { params } => Cow::Owned(params.command.join(" ")),
}
}
}