Refactor extension tools onto shared ToolExecutor (#22369)

## Why

Extension tools were split across two public runtime contracts:
`codex-tool-api` exposed `ToolBundle` plus its own call/spec/error
types, while core native tools used `codex_tools::ToolExecutor`. That
made contributed tool specs and execution behavior easy to drift apart
and added another crate boundary for what should be one executable-tool
seam.

This PR makes `ToolExecutor` the single runtime contract and keeps
extension-specific pinning in `codex-extension-api`.

## Remaining todo

https://github.com/openai/codex/pull/22369/changes#diff-b935ea8245c3ce568a30cff660175fa6390b66b872ae409e1e2e965738250741R5
Either generic `Invocation` or sub-extract the `ToolCall` and clean
`ToolInvocation`

## What changed

- Removed the `codex-tool-api` workspace crate and its dependencies from
core and `codex-extension-api`.
- Made `codex_tools::ToolExecutor` object-safe with `async_trait` so
extension contributors can return a dyn executor.
- Added the extension-facing aliases under
`ext/extension-api/src/contributors/tools.rs`, including
`ExtensionToolExecutor = dyn ToolExecutor<ToolCall, Output =
ExtensionToolOutput>`.
- Changed `ToolContributor::tools` to return extension executors
directly instead of `ToolBundle`s.
- Updated core’s extension tool handler/registry/router path to adapt
those extension executors into the existing native `ToolInvocation`
runtime path.
- Added focused coverage for extension tools being registered,
model-visible, dispatchable, and not replacing built-in tools.

## Verification

- `cargo test -p codex-tools`
- `cargo test -p codex-extension-api`
This commit is contained in:
jif-oai
2026-05-13 12:12:06 +02:00
committed by GitHub
Unverified
parent 1824685a00
commit 9c5dfa7b1a
30 changed files with 349 additions and 450 deletions
+1 -11
View File
@@ -2518,7 +2518,6 @@ dependencies = [
"codex-terminal-detection",
"codex-test-binary-support",
"codex-thread-store",
"codex-tool-api",
"codex-tools",
"codex-utils-absolute-path",
"codex-utils-cache",
@@ -2820,7 +2819,7 @@ name = "codex-extension-api"
version = "0.0.0"
dependencies = [
"codex-protocol",
"codex-tool-api",
"codex-tools",
]
[[package]]
@@ -3710,15 +3709,6 @@ dependencies = [
"uuid",
]
[[package]]
name = "codex-tool-api"
version = "0.0.0"
dependencies = [
"pretty_assertions",
"serde_json",
"thiserror 2.0.18",
]
[[package]]
name = "codex-tools"
version = "0.0.0"
-2
View File
@@ -108,7 +108,6 @@ members = [
"test-binary-support",
"thread-manager-sample",
"thread-store",
"tool-api",
"uds",
"codex-experimental-api-macros",
"plugin",
@@ -209,7 +208,6 @@ codex-stdio-to-uds = { path = "stdio-to-uds" }
codex-terminal-detection = { path = "terminal-detection" }
codex-test-binary-support = { path = "test-binary-support" }
codex-thread-store = { path = "thread-store" }
codex-tool-api = { path = "tool-api" }
codex-tools = { path = "tools" }
codex-tui = { path = "tui" }
codex-uds = { path = "uds" }
-1
View File
@@ -60,7 +60,6 @@ codex-sandboxing = { workspace = true }
codex-state = { workspace = true }
codex-terminal-detection = { workspace = true }
codex-thread-store = { workspace = true }
codex-tool-api = { workspace = true }
codex-tools = { workspace = true }
codex-utils-absolute-path = { workspace = true }
codex-utils-cache = { workspace = true }
+2 -2
View File
@@ -543,7 +543,7 @@ fn test_tool_runtime(session: Arc<Session>, turn_context: Arc<TurnContext>) -> T
mcp_tools: None,
deferred_mcp_tools: None,
discoverable_tools: None,
extension_tool_bundles: Vec::new(),
extension_tool_executors: Vec::new(),
dynamic_tools: turn_context.dynamic_tools.as_slice(),
},
));
@@ -8607,7 +8607,7 @@ async fn fatal_tool_error_stops_turn_and_reports_error() {
deferred_mcp_tools,
mcp_tools: Some(tools),
discoverable_tools: None,
extension_tool_bundles: Vec::new(),
extension_tool_executors: Vec::new(),
dynamic_tools: turn_context.dynamic_tools.as_slice(),
},
);
+2 -2
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_executors;
use crate::turn_diff_tracker::TurnDiffTracker;
use crate::turn_timing::record_turn_ttft_metric;
use crate::util::backoff;
@@ -1250,7 +1250,7 @@ pub(crate) async fn built_tools(
mcp_tools,
deferred_mcp_tools,
discoverable_tools,
extension_tool_bundles: extension_tool_bundles(sess),
extension_tool_executors: extension_tool_executors(sess),
dynamic_tools: turn_context.dynamic_tools.as_slice(),
},
)))
@@ -1,9 +1,8 @@
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::ToolError as ExtensionToolError;
use codex_tools::ResponsesApiTool;
use std::sync::Arc;
use codex_extension_api::ExtensionToolExecutor;
use codex_extension_api::ExtensionToolOutput;
use codex_tools::ToolCall as ExtensionToolCall;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use serde_json::Value;
@@ -19,46 +18,13 @@ use crate::tools::registry::PreToolUsePayload;
use crate::tools::registry::ToolExecutor;
use crate::tools::registry::ToolHandler;
pub(crate) struct BundledToolOutput {
value: Value,
pub(crate) struct ExtensionToolHandler {
executor: Arc<dyn ExtensionToolExecutor>,
}
impl ToolOutput for BundledToolOutput {
fn log_preview(&self) -> String {
self.value.to_string()
}
fn success_for_logging(&self) -> bool {
true
}
fn to_response_item(&self, call_id: &str, _payload: &ToolPayload) -> ResponseInputItem {
ResponseInputItem::FunctionCallOutput {
call_id: call_id.to_string(),
output: FunctionCallOutputPayload {
body: FunctionCallOutputBody::Text(self.value.to_string()),
success: Some(true),
},
}
}
fn post_tool_use_response(&self, _call_id: &str, _payload: &ToolPayload) -> Option<Value> {
Some(self.value.clone())
}
fn code_mode_result(&self, _payload: &ToolPayload) -> Value {
self.value.clone()
}
}
pub(crate) struct BundledToolHandler {
bundle: ExtensionToolBundle,
spec: ToolSpec,
}
impl BundledToolHandler {
pub(crate) fn new(bundle: ExtensionToolBundle, spec: ToolSpec) -> Self {
Self { bundle, spec }
impl ExtensionToolHandler {
pub(crate) fn new(executor: Arc<dyn ExtensionToolExecutor>) -> Self {
Self { executor }
}
fn arguments_from_payload<'a>(&self, payload: &'a ToolPayload) -> Option<&'a str> {
@@ -69,41 +35,23 @@ impl BundledToolHandler {
}
}
impl ToolExecutor<ToolInvocation> for BundledToolHandler {
type Output = BundledToolOutput;
impl ToolExecutor<ToolInvocation> for ExtensionToolHandler {
type Output = ExtensionToolOutput;
fn tool_name(&self) -> ToolName {
ToolName::plain(self.bundle.tool_name())
self.executor.tool_name()
}
fn spec(&self) -> Option<ToolSpec> {
Some(self.spec.clone())
self.executor.spec()
}
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 })
self.executor.handle(to_extension_call(&invocation)).await
}
}
impl ToolHandler for BundledToolHandler {
impl ToolHandler for ExtensionToolHandler {
fn matches_kind(&self, payload: &ToolPayload) -> bool {
self.arguments_from_payload(payload).is_some()
}
@@ -132,23 +80,11 @@ impl ToolHandler for BundledToolHandler {
}
}
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),
ExtensionToolError::Fatal(message) => FunctionCallError::Fatal(message),
fn to_extension_call(invocation: &ToolInvocation) -> ExtensionToolCall {
ExtensionToolCall {
call_id: invocation.call_id.clone(),
tool_name: invocation.tool_name.clone(),
payload: invocation.payload.clone(),
}
}
@@ -167,9 +103,7 @@ 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 crate::tools::context::ToolCallSource;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
@@ -181,32 +115,43 @@ mod tests {
struct StubExtensionExecutor;
impl codex_tool_api::ToolExecutor for StubExtensionExecutor {
fn execute(&self, _call: codex_tool_api::ToolCall) -> codex_tool_api::ToolFuture<'_> {
Box::pin(async { Ok(json!({ "ok": true })) })
impl codex_extension_api::ExtensionToolExecutor for StubExtensionExecutor {
fn tool_name(&self) -> codex_tools::ToolName {
codex_tools::ToolName::plain("extension_echo")
}
fn spec(&self) -> Option<codex_tools::ToolSpec> {
Some(codex_tools::ToolSpec::Function(
codex_tools::ResponsesApiTool {
name: "extension_echo".to_string(),
description: "Echoes arguments.".to_string(),
strict: true,
parameters: codex_tools::parse_tool_input_schema(&json!({
"type": "object",
"properties": {
"message": { "type": "string" },
},
"required": ["message"],
"additionalProperties": false,
}))
.expect("extension schema should parse"),
output_schema: None,
defer_loading: None,
},
))
}
fn handle(
&self,
_call: codex_tools::ToolCall,
) -> codex_extension_api::ExtensionToolFuture<'_> {
Box::pin(async { Ok(codex_tools::JsonToolOutput::new(json!({ "ok": true }))) })
}
}
#[tokio::test]
async fn exposes_generic_hook_payloads() {
let bundle = codex_tool_api::ToolBundle::new(
codex_tool_api::FunctionToolSpec {
name: "extension_echo".to_string(),
description: "Echoes arguments.".to_string(),
strict: true,
parameters: json!({
"type": "object",
"properties": {
"message": { "type": "string" },
},
"required": ["message"],
"additionalProperties": false,
}),
},
Arc::new(StubExtensionExecutor),
);
let spec = extension_tool_spec(bundle.spec()).expect("extension spec should convert");
let handler = BundledToolHandler::new(bundle, spec);
let handler = ExtensionToolHandler::new(Arc::new(StubExtensionExecutor));
let (session, turn) = crate::session::tests::make_session_and_context().await;
let invocation = ToolInvocation {
session: session.into(),
@@ -220,9 +165,7 @@ mod tests {
arguments: json!({ "message": "hello" }).to_string(),
},
};
let output = BundledToolOutput {
value: json!({ "ok": true }),
};
let output = codex_tools::JsonToolOutput::new(json!({ "ok": true }));
assert_eq!(
ToolHandler::pre_tool_use_payload(&handler, &invocation),
+8 -16
View File
@@ -18,15 +18,14 @@ 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::handlers::extension_tools::ExtensionToolHandler;
use crate::tools::hook_names::HookToolName;
use crate::tools::tool_dispatch_trace::ToolDispatchTrace;
use crate::tools::tool_search_entry::ToolSearchInfo;
use crate::util::error_or_panic;
use codex_extension_api::ExtensionToolExecutor;
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 futures::future::BoxFuture;
@@ -584,25 +583,18 @@ impl ToolRegistryBuilder {
self.handlers.insert(name, handler);
}
pub fn register_tool_bundle(&mut self, bundle: ExtensionToolBundle) {
let tool_name = ToolName::plain(bundle.tool_name());
pub fn register_extension_tool_executor(&mut self, executor: Arc<dyn ExtensionToolExecutor>) {
let tool_name = executor.tool_name();
if self.handlers.contains_key(&tool_name) {
warn!("Skipping extension tool `{tool_name}`: handler already registered");
return;
}
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());
if let Some(spec) = executor.spec() {
self.push_spec(spec);
}
let handler: Arc<dyn AnyToolHandler> = Arc::new(BundledToolHandler::new(bundle, spec));
let handler: Arc<dyn AnyToolHandler> = Arc::new(ExtensionToolHandler::new(executor));
self.handlers.insert(tool_name, handler);
}
+5 -5
View File
@@ -9,13 +9,13 @@ use crate::tools::registry::AnyToolResult;
use crate::tools::registry::ToolArgumentDiffConsumer;
use crate::tools::registry::ToolRegistry;
use crate::tools::spec::build_specs_with_discoverable_tools;
use codex_extension_api::ExtensionToolExecutor;
use codex_mcp::ToolInfo;
use codex_protocol::dynamic_tools::DynamicToolSpec;
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_tools::DiscoverableTool;
use codex_tools::ResponsesApiNamespaceTool;
use codex_tools::ToolName;
@@ -44,7 +44,7 @@ pub(crate) struct ToolRouterParams<'a> {
pub(crate) mcp_tools: Option<Vec<ToolInfo>>,
pub(crate) deferred_mcp_tools: Option<Vec<ToolInfo>>,
pub(crate) discoverable_tools: Option<Vec<DiscoverableTool>>,
pub(crate) extension_tool_bundles: Vec<ExtensionToolBundle>,
pub(crate) extension_tool_executors: Vec<Arc<dyn ExtensionToolExecutor>>,
pub(crate) dynamic_tools: &'a [DynamicToolSpec],
}
@@ -54,7 +54,7 @@ impl ToolRouter {
mcp_tools,
deferred_mcp_tools,
discoverable_tools,
extension_tool_bundles,
extension_tool_executors,
dynamic_tools,
} = params;
let builder = build_specs_with_discoverable_tools(
@@ -62,7 +62,7 @@ impl ToolRouter {
mcp_tools,
deferred_mcp_tools,
discoverable_tools,
&extension_tool_bundles,
&extension_tool_executors,
dynamic_tools,
);
let (specs, registry) = builder.build();
@@ -217,7 +217,7 @@ impl ToolRouter {
}
}
pub(crate) fn extension_tool_bundles(session: &Session) -> Vec<ExtensionToolBundle> {
pub(crate) fn extension_tool_executors(session: &Session) -> Vec<Arc<dyn ExtensionToolExecutor>> {
session
.services
.extensions
+42 -35
View File
@@ -7,15 +7,14 @@ use crate::turn_diff_tracker::TurnDiffTracker;
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::ToolExecutor;
use codex_extension_api::ToolFuture;
use codex_extension_api::ExtensionToolExecutor;
use codex_extension_api::ExtensionToolOutput;
use codex_extension_api::ResponsesApiTool;
use codex_extension_api::ToolCall as ExtensionToolCall;
use codex_protocol::dynamic_tools::DynamicToolSpec;
use codex_protocol::models::FunctionCallOutputBody;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::models::ResponseItem;
use codex_tool_api::ToolCall as ExtensionToolCall;
use codex_tools::ResponsesApiNamespaceTool;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
@@ -27,7 +26,7 @@ use super::ToolCall;
use super::ToolCallSource;
use super::ToolRouter;
use super::ToolRouterParams;
use super::extension_tool_bundles;
use super::extension_tool_executors;
struct ExtensionEchoContributor;
@@ -36,38 +35,46 @@ impl codex_extension_api::ToolContributor for ExtensionEchoContributor {
&self,
_session_store: &ExtensionData,
_thread_store: &ExtensionData,
) -> Vec<ToolBundle> {
vec![ToolBundle::new(
FunctionToolSpec {
name: "extension_echo".to_string(),
description: "Echoes arguments through an extension tool.".to_string(),
strict: true,
parameters: json!({
"type": "object",
"properties": {
"message": { "type": "string" },
},
"required": ["message"],
"additionalProperties": false,
}),
},
Arc::new(ExtensionEchoExecutor),
)]
) -> Vec<Arc<dyn ExtensionToolExecutor>> {
vec![Arc::new(ExtensionEchoExecutor)]
}
}
struct ExtensionEchoExecutor;
impl ToolExecutor for ExtensionEchoExecutor {
fn execute<'a>(&'a self, call: ExtensionToolCall) -> ToolFuture<'a> {
impl ExtensionToolExecutor for ExtensionEchoExecutor {
fn tool_name(&self) -> ToolName {
ToolName::plain("extension_echo")
}
fn spec(&self) -> Option<ToolSpec> {
Some(ToolSpec::Function(ResponsesApiTool {
name: "extension_echo".to_string(),
description: "Echoes arguments through an extension tool.".to_string(),
strict: true,
parameters: codex_extension_api::parse_tool_input_schema(&json!({
"type": "object",
"properties": {
"message": { "type": "string" },
},
"required": ["message"],
"additionalProperties": false,
}))
.expect("extension schema should parse"),
output_schema: None,
defer_loading: None,
}))
}
fn handle(&self, call: ExtensionToolCall) -> codex_extension_api::ExtensionToolFuture<'_> {
Box::pin(async move {
let arguments: serde_json::Value =
serde_json::from_str(&call.arguments).expect("test arguments should parse");
Ok(json!({
let arguments: serde_json::Value = serde_json::from_str(call.function_arguments()?)
.expect("test arguments should parse");
Ok(ExtensionToolOutput::new(json!({
"arguments": arguments,
"callId": call.call_id.clone(),
"ok": true,
}))
})))
})
}
}
@@ -98,7 +105,7 @@ async fn parallel_support_does_not_match_namespaced_local_tool_names() -> anyhow
deferred_mcp_tools: None,
mcp_tools: Some(mcp_tools),
discoverable_tools: None,
extension_tool_bundles: Vec::new(),
extension_tool_executors: Vec::new(),
dynamic_tools: turn.dynamic_tools.as_slice(),
},
);
@@ -177,7 +184,7 @@ async fn mcp_parallel_support_uses_handler_data() -> anyhow::Result<()> {
),
]),
discoverable_tools: None,
extension_tool_bundles: Vec::new(),
extension_tool_executors: Vec::new(),
dynamic_tools: turn.dynamic_tools.as_slice(),
},
);
@@ -212,7 +219,7 @@ async fn tools_without_handlers_do_not_support_parallel() -> anyhow::Result<()>
deferred_mcp_tools: None,
mcp_tools: None,
discoverable_tools: None,
extension_tool_bundles: Vec::new(),
extension_tool_executors: Vec::new(),
dynamic_tools: turn.dynamic_tools.as_slice(),
},
);
@@ -264,7 +271,7 @@ async fn specs_filter_deferred_dynamic_tools() -> anyhow::Result<()> {
deferred_mcp_tools: None,
mcp_tools: None,
discoverable_tools: None,
extension_tool_bundles: Vec::new(),
extension_tool_executors: Vec::new(),
dynamic_tools: &dynamic_tools,
},
);
@@ -310,7 +317,7 @@ fn mcp_tool_info(
}
#[tokio::test]
async fn extension_tool_bundles_are_model_visible_and_dispatchable() -> anyhow::Result<()> {
async fn extension_tool_executors_are_model_visible_and_dispatchable() -> anyhow::Result<()> {
let (mut session, turn) = make_session_and_context().await;
session.services.extensions = extension_tool_test_registry();
@@ -320,7 +327,7 @@ async fn extension_tool_bundles_are_model_visible_and_dispatchable() -> anyhow::
deferred_mcp_tools: None,
mcp_tools: None,
discoverable_tools: None,
extension_tool_bundles: extension_tool_bundles(&session),
extension_tool_executors: extension_tool_executors(&session),
dynamic_tools: turn.dynamic_tools.as_slice(),
},
);
+4 -3
View File
@@ -7,12 +7,13 @@ use crate::tools::handlers::multi_agents_spec::WaitAgentTimeoutOptions;
use crate::tools::registry::ToolRegistryBuilder;
use crate::tools::spec_plan::build_tool_registry_builder;
use crate::tools::spec_plan_types::ToolRegistryBuildParams;
use codex_extension_api::ExtensionToolExecutor;
use codex_mcp::ToolInfo;
use codex_protocol::dynamic_tools::DynamicToolSpec;
use codex_tool_api::ToolBundle as ExtensionToolBundle;
use codex_tools::DiscoverableTool;
use codex_tools::ToolUserShellType;
use codex_tools::ToolsConfig;
use std::sync::Arc;
pub(crate) fn tool_user_shell_type(user_shell: &Shell) -> ToolUserShellType {
match user_shell.shell_type {
@@ -29,7 +30,7 @@ pub(crate) fn build_specs_with_discoverable_tools(
mcp_tools: Option<Vec<ToolInfo>>,
deferred_mcp_tools: Option<Vec<ToolInfo>>,
discoverable_tools: Option<Vec<DiscoverableTool>>,
extension_tool_bundles: &[ExtensionToolBundle],
extension_tool_executors: &[Arc<dyn ExtensionToolExecutor>],
dynamic_tools: &[DynamicToolSpec],
) -> ToolRegistryBuilder {
let default_agent_type_description =
@@ -50,7 +51,7 @@ pub(crate) fn build_specs_with_discoverable_tools(
mcp_tools: mcp_tools.as_deref(),
deferred_mcp_tools: deferred_mcp_tools.as_deref(),
discoverable_tools: discoverable_tools.as_deref(),
extension_tool_bundles,
extension_tool_executors,
dynamic_tools,
default_agent_type_description: &default_agent_type_description,
wait_agent_timeouts: WaitAgentTimeoutOptions {
+7 -7
View File
@@ -27,7 +27,6 @@ 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::extension_tool_spec;
use crate::tools::handlers::multi_agents::CloseAgentHandler;
use crate::tools::handlers::multi_agents::ResumeAgentHandler;
use crate::tools::handlers::multi_agents::SendInputHandler;
@@ -49,6 +48,7 @@ use crate::tools::registry::AnyToolHandler;
use crate::tools::registry::ToolRegistryBuilder;
use crate::tools::spec_plan_types::ToolRegistryBuildParams;
use crate::tools::spec_plan_types::agent_type_description;
use codex_extension_api::ExtensionToolExecutor;
use codex_protocol::openai_models::ConfigShellToolType;
use codex_tools::ResponsesApiNamespaceTool;
use codex_tools::ToolEnvironmentMode;
@@ -84,7 +84,7 @@ pub fn build_tool_registry_builder(
for handler in build_code_mode_handlers(
config,
&handlers,
params.extension_tool_bundles,
params.extension_tool_executors,
config.search_tool && !all_deferred_tools.is_empty(),
) {
builder.register_any_handler(handler);
@@ -134,8 +134,8 @@ pub fn build_tool_registry_builder(
builder.register_handler(Arc::new(ToolSearchHandler::new(deferred_search_infos)));
}
for bundle in params.extension_tool_bundles.iter().cloned() {
builder.register_tool_bundle(bundle);
for executor in params.extension_tool_executors.iter().cloned() {
builder.register_extension_tool_executor(executor);
}
builder
@@ -144,7 +144,7 @@ pub fn build_tool_registry_builder(
fn build_code_mode_handlers(
config: &ToolsConfig,
handlers: &[Arc<dyn AnyToolHandler>],
extension_tool_bundles: &[codex_tool_api::ToolBundle],
extension_tool_executors: &[Arc<dyn ExtensionToolExecutor>],
deferred_tools_available: bool,
) -> Vec<Arc<dyn AnyToolHandler>> {
if !config.code_mode_enabled {
@@ -156,9 +156,9 @@ fn build_code_mode_handlers(
.filter_map(|handler| handler.spec())
.collect::<Vec<_>>();
code_mode_nested_tool_specs.extend(
extension_tool_bundles
extension_tool_executors
.iter()
.filter_map(|bundle| extension_tool_spec(bundle.spec()).ok()),
.filter_map(|executor| executor.spec()),
);
let namespace_descriptions = code_mode_namespace_descriptions(&code_mode_nested_tool_specs);
let mut enabled_tools =
+49 -38
View File
@@ -26,6 +26,8 @@ use crate::tools::handlers::view_image_spec::ViewImageToolOptions;
use crate::tools::handlers::view_image_spec::create_view_image_tool;
use crate::tools::registry::ToolRegistry;
use codex_app_server_protocol::AppInfo;
use codex_extension_api::ExtensionToolExecutor;
use codex_extension_api::ToolCall as ExtensionToolCall;
use codex_features::Feature;
use codex_features::Features;
use codex_mcp::ToolInfo;
@@ -41,10 +43,6 @@ use codex_protocol::openai_models::ModelInfo;
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::ToolExecutor;
use codex_tool_api::ToolFuture;
use codex_tools::AdditionalProperties;
use codex_tools::DiscoverablePluginInfo;
use codex_tools::DiscoverableTool;
@@ -67,6 +65,7 @@ use pretty_assertions::assert_eq;
use serde_json::json;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::sync::Arc;
const CODEX_APPS_MCP_SERVER_NAME: &str = "codex_apps";
const DEFAULT_AGENT_TYPE_DESCRIPTION: &str = "Test agent type description.";
@@ -74,32 +73,44 @@ const DEFAULT_WAIT_TIMEOUT_MS: i64 = 30_000;
const MIN_WAIT_TIMEOUT_MS: i64 = 10_000;
const MAX_WAIT_TIMEOUT_MS: i64 = 3_600_000;
struct UnusedExtensionExecutor;
impl ToolExecutor for UnusedExtensionExecutor {
fn execute<'a>(&'a self, _call: codex_tool_api::ToolCall) -> ToolFuture<'a> {
Box::pin(async { panic!("spec planning should not execute extension tools") })
fn extension_tool_executor(name: &str, description: &str) -> Arc<dyn ExtensionToolExecutor> {
struct SpecOnlyExtensionExecutor {
name: String,
description: String,
}
}
fn extension_tool_bundle(name: &str, description: &str) -> ExtensionToolBundle {
ExtensionToolBundle::new(
FunctionToolSpec {
name: name.to_string(),
description: description.to_string(),
strict: true,
parameters: serde_json::to_value(JsonSchema::object(
BTreeMap::from([(
"message".to_string(),
JsonSchema::string(/*description*/ None),
)]),
Some(vec!["message".to_string()]),
Some(false.into()),
))
.expect("extension schema should serialize"),
},
std::sync::Arc::new(UnusedExtensionExecutor),
)
impl ExtensionToolExecutor for SpecOnlyExtensionExecutor {
fn tool_name(&self) -> ToolName {
ToolName::plain(self.name.as_str())
}
fn spec(&self) -> Option<ToolSpec> {
Some(ToolSpec::Function(ResponsesApiTool {
name: self.name.clone(),
description: self.description.clone(),
strict: true,
parameters: JsonSchema::object(
BTreeMap::from([(
"message".to_string(),
JsonSchema::string(/*description*/ None),
)]),
Some(vec!["message".to_string()]),
Some(false.into()),
),
output_schema: None,
defer_loading: None,
}))
}
fn handle(&self, _call: ExtensionToolCall) -> codex_extension_api::ExtensionToolFuture<'_> {
Box::pin(async { panic!("spec planning should not execute extension tools") })
}
}
Arc::new(SpecOnlyExtensionExecutor {
name: name.to_string(),
description: description.to_string(),
})
}
#[test]
@@ -116,7 +127,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_executors = vec![extension_tool_executor(
"update_plan",
"Extension attempt to replace a built-in tool.",
)];
@@ -125,7 +136,7 @@ fn extension_tools_do_not_replace_builtin_tools() {
/*mcp_tools*/ None,
/*deferred_mcp_tools*/ None,
/*discoverable_tools*/ None,
&extension_tool_bundles,
&extension_tool_executors,
&[],
);
@@ -1880,7 +1891,7 @@ fn request_plugin_install_is_not_registered_without_feature_flag() {
"Google Calendar",
"Plan events and schedules.",
)]),
/*extension_tool_bundles*/ &[],
/*extension_tool_executors*/ &[],
&[],
);
@@ -1921,7 +1932,7 @@ fn request_plugin_install_can_be_registered_without_search_tool() {
"Google Calendar",
"Plan events and schedules.",
)]),
/*extension_tool_bundles*/ &[],
/*extension_tool_executors*/ &[],
&[],
);
@@ -1986,7 +1997,7 @@ fn request_plugin_install_description_lists_discoverable_tools() {
/*mcp_tools*/ None,
/*deferred_mcp_tools*/ None,
Some(discoverable_tools),
/*extension_tool_bundles*/ &[],
/*extension_tool_executors*/ &[],
&[],
);
assert!(registry.has_handler(&ToolName::plain(REQUEST_PLUGIN_INSTALL_TOOL_NAME)));
@@ -2279,7 +2290,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_executors = vec![extension_tool_executor(
"extension_echo",
"Echoes arguments through an extension tool.",
)];
@@ -2288,7 +2299,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_executors,
&[],
);
let ToolSpec::Freeform(FreeformTool { description, .. }) = find_tool(&tools, "exec") else {
@@ -2386,7 +2397,7 @@ fn build_specs(
mcp_tools,
deferred_mcp_tools,
/*discoverable_tools*/ None,
/*extension_tool_bundles*/ &[],
/*extension_tool_executors*/ &[],
dynamic_tools,
)
}
@@ -2396,7 +2407,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_executors: &[Arc<dyn ExtensionToolExecutor>],
dynamic_tools: &[DynamicToolSpec],
) -> (Vec<ToolSpec>, ToolRegistry) {
let mcp_tool_inputs = mcp_tools.as_ref().map(|mcp_tools| {
@@ -2411,7 +2422,7 @@ fn build_specs_with_discoverable_tools(
mcp_tools: mcp_tool_inputs.as_deref(),
deferred_mcp_tools: deferred_mcp_tools.as_deref(),
discoverable_tools: discoverable_tools.as_deref(),
extension_tool_bundles,
extension_tool_executors,
dynamic_tools,
default_agent_type_description: DEFAULT_AGENT_TYPE_DESCRIPTION,
wait_agent_timeouts: wait_agent_timeout_options(),
+3 -2
View File
@@ -1,16 +1,17 @@
use crate::tools::handlers::multi_agents_spec::WaitAgentTimeoutOptions;
use codex_extension_api::ExtensionToolExecutor;
use codex_mcp::ToolInfo;
use codex_protocol::dynamic_tools::DynamicToolSpec;
use codex_tool_api::ToolBundle as ExtensionToolBundle;
use codex_tools::DiscoverableTool;
use codex_tools::ToolsConfig;
use std::sync::Arc;
#[derive(Clone, Copy)]
pub struct ToolRegistryBuildParams<'a> {
pub mcp_tools: Option<&'a [ToolInfo]>,
pub deferred_mcp_tools: Option<&'a [ToolInfo]>,
pub discoverable_tools: Option<&'a [DiscoverableTool]>,
pub extension_tool_bundles: &'a [ExtensionToolBundle],
pub extension_tool_executors: &'a [Arc<dyn ExtensionToolExecutor>],
pub dynamic_tools: &'a [DynamicToolSpec],
pub default_agent_type_description: &'a str,
pub wait_agent_timeouts: WaitAgentTimeoutOptions,
+3 -3
View File
@@ -275,7 +275,7 @@ fn build_specs(
mcp_tools,
deferred_mcp_tools,
/*discoverable_tools*/ None,
/*extension_tool_bundles*/ &[],
/*extension_tool_executors*/ &[],
dynamic_tools,
)
}
@@ -335,7 +335,7 @@ async fn assert_model_tools(
mcp_tools: None,
deferred_mcp_tools: None,
discoverable_tools: None,
extension_tool_bundles: Vec::new(),
extension_tool_executors: Vec::new(),
dynamic_tools: &[],
},
);
@@ -804,7 +804,7 @@ async fn request_plugin_install_requires_apps_and_plugins_features() {
/*mcp_tools*/ None,
/*deferred_mcp_tools*/ None,
discoverable_tools.clone(),
/*extension_tool_bundles*/ &[],
/*extension_tool_executors*/ &[],
&[],
)
.build();
+1 -1
View File
@@ -14,5 +14,5 @@ doctest = false
workspace = true
[dependencies]
codex-tool-api = { workspace = true }
codex-protocol = { workspace = true }
codex-tools = { workspace = true }
+10 -3
View File
@@ -1,16 +1,20 @@
use std::future::Future;
use std::sync::Arc;
use codex_protocol::ThreadId;
use codex_protocol::items::TurnItem;
use codex_protocol::protocol::ReviewDecision;
use codex_tool_api::ToolBundle;
use crate::ExtensionData;
mod prompt;
mod tools;
pub use prompt::PromptFragment;
pub use prompt::PromptSlot;
pub use tools::ExtensionToolExecutor;
pub use tools::ExtensionToolFuture;
pub use tools::ExtensionToolOutput;
/// Contributor that receives the live thread id and host-owned thread-start
/// input before later contributors read from extension stores.
@@ -36,8 +40,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<Arc<dyn ExtensionToolExecutor>>;
}
/// Future returned by one claimed approval-review contribution.
@@ -0,0 +1,32 @@
use std::future::Future;
use std::pin::Pin;
use codex_tools::FunctionCallError;
use codex_tools::JsonToolOutput;
use codex_tools::ToolCall;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
/// Model-facing output returned by extension-owned tools.
pub type ExtensionToolOutput = JsonToolOutput;
/// Future returned by extension-owned tool execution.
pub type ExtensionToolFuture<'a> =
Pin<Box<dyn Future<Output = Result<ExtensionToolOutput, FunctionCallError>> + Send + 'a>>;
/// Object-safe runtime contract for extension-owned model-visible tools.
///
/// Implementations keep an extension tool's model-visible spec attached to the
/// executable runtime that handles calls for that tool.
pub trait ExtensionToolExecutor: Send + Sync {
/// The concrete tool name handled by this extension runtime.
fn tool_name(&self) -> ToolName;
/// The model-visible spec for this extension tool.
fn spec(&self) -> Option<ToolSpec> {
None
}
/// Execute one extension tool invocation.
fn handle(&self, call: ToolCall) -> ExtensionToolFuture<'_>;
}
+11 -6
View File
@@ -5,15 +5,20 @@ mod state;
pub use capabilities::AgentSpawnFuture;
pub use capabilities::AgentSpawner;
pub use codex_tool_api::FunctionToolSpec;
pub use codex_tool_api::ToolBundle;
pub use codex_tool_api::ToolCall;
pub use codex_tool_api::ToolError;
pub use codex_tool_api::ToolExecutor;
pub use codex_tool_api::ToolFuture;
pub use codex_tools::FunctionCallError;
pub use codex_tools::JsonToolOutput;
pub use codex_tools::ResponsesApiTool;
pub use codex_tools::ToolCall;
pub use codex_tools::ToolName;
pub use codex_tools::ToolPayload;
pub use codex_tools::ToolSpec;
pub use codex_tools::parse_tool_input_schema;
pub use contributors::ApprovalReviewContributor;
pub use contributors::ApprovalReviewFuture;
pub use contributors::ContextContributor;
pub use contributors::ExtensionToolExecutor;
pub use contributors::ExtensionToolFuture;
pub use contributors::ExtensionToolOutput;
pub use contributors::PromptFragment;
pub use contributors::PromptSlot;
pub use contributors::ThreadStartContributor;
-6
View File
@@ -1,6 +0,0 @@
load("//:defs.bzl", "codex_rust_crate")
codex_rust_crate(
name = "tool-api",
crate_name = "codex_tool_api",
)
-20
View File
@@ -1,20 +0,0 @@
[package]
edition.workspace = true
license.workspace = true
name = "codex-tool-api"
version.workspace = true
[lib]
name = "codex_tool_api"
path = "src/lib.rs"
doctest = false
[lints]
workspace = true
[dependencies]
serde_json = { workspace = true }
thiserror = { workspace = true }
[dev-dependencies]
pretty_assertions = { workspace = true }
-29
View File
@@ -1,29 +0,0 @@
# codex-tool-api
`codex-tool-api` is the minimal extension-facing contract for contributed
function tools that can be injected into Codex without making `codex-core`
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`,
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 intended dependency direction is:
```text
tool-owning extension crate --> codex-tool-api <-- codex-core
```
`codex-tools` has a different job. It remains the host-side owner of Responses
API tool models, schema parsing, namespaces, discovery, MCP/dynamic conversion,
code-mode shaping, and other aggregate host concerns. A crate that only wants
to contribute one ordinary function tool through an extension should not need
to depend on `codex-tools`.
-87
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");
}
}
-5
View File
@@ -1,5 +0,0 @@
/// One contributed function-tool call.
pub struct ToolCall {
pub call_id: String,
pub arguments: String,
}
-22
View File
@@ -1,22 +0,0 @@
use thiserror::Error;
/// Error returned by a contributed executable tool.
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum ToolError {
#[error("{0}")]
RespondToModel(String),
#[error("fatal tool error: {0}")]
Fatal(String),
}
impl ToolError {
/// Creates a model-visible tool error.
pub fn respond_to_model(message: impl Into<String>) -> Self {
Self::RespondToModel(message.into())
}
/// Creates a host-fatal tool error.
pub fn fatal(message: impl Into<String>) -> Self {
Self::Fatal(message.into())
}
}
-14
View File
@@ -1,14 +0,0 @@
//! Minimal function-tool contracts shared between hosts and extension-owned
//! tool crates.
mod bundle;
mod call;
mod error;
mod spec;
pub use bundle::ToolBundle;
pub use bundle::ToolExecutor;
pub use bundle::ToolFuture;
pub use call::ToolCall;
pub use error::ToolError;
pub use spec::FunctionToolSpec;
-10
View File
@@ -1,10 +0,0 @@
use serde_json::Value;
/// Model-visible definition for one contributed function tool.
#[derive(Debug, Clone, PartialEq)]
pub struct FunctionToolSpec {
pub name: String,
pub description: String,
pub strict: bool,
pub parameters: Value,
}
+5 -8
View File
@@ -1,11 +1,7 @@
# codex-tools
`codex-tools` is the host-side support crate for building, adapting, and
planning tool sets outside `codex-core`.
It is deliberately not the API that external tool owners should depend on.
Crates that contribute ordinary function tools through extensions should use
`codex-tool-api`, which owns only that small executable-tool seam.
`codex-tools` is the shared support crate for building, adapting, planning, and
executing model-visible tool sets outside `codex-core`.
Today this crate owns the host-facing tool models and helpers that no longer
need to live in `core/src/tools/spec.rs` or `core/src/client_common.rs`:
@@ -17,6 +13,8 @@ need to live in `core/src/tools/spec.rs` or `core/src/client_common.rs`:
`ToolsConfig`, discoverable-tool models, and request-plugin-install helpers
- host adapters such as schema sanitization, MCP/dynamic conversion, code-mode
augmentation, and image-detail normalization
- shared executable-tool contracts such as `ToolExecutor`, `ToolCall`, and
`ToolOutput`
That extraction is the first step in a longer migration. The goal is not to
move all of `core/src/tools` into this crate in one shot. Instead, the plan is
@@ -37,7 +35,6 @@ multiple consumers, for example:
The corresponding non-goals are just as important:
- do not become the extension-facing authoring API for executable tools
- do not move `codex-core` orchestration here prematurely
- do not pull `Session` / `TurnContext` / approval flow / runtime execution
logic into this crate unless those dependencies have first been split into
@@ -48,7 +45,7 @@ The corresponding non-goals are just as important:
The expected migration shape is:
1. Keep ordinary contributed function-tool authoring in `codex-tool-api`.
1. Keep extension-owned executable-tool authoring in `codex-extension-api`.
2. Move host-side planning/adaptation helpers here when they no longer need to
stay coupled to `codex-core`.
3. Leave compatibility-sensitive adapters in `codex-core` while downstream
+3
View File
@@ -9,6 +9,7 @@ mod json_schema;
mod mcp_tool;
mod request_plugin_install;
mod responses_api;
mod tool_call;
mod tool_config;
mod tool_definition;
mod tool_discovery;
@@ -56,6 +57,7 @@ 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_responses_api_tool;
pub use tool_call::ToolCall;
pub use tool_config::ShellCommandBackendConfig;
pub use tool_config::ToolEnvironmentMode;
pub use tool_config::ToolUserShellType;
@@ -77,6 +79,7 @@ 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::JsonToolOutput;
pub use tool_output::ToolOutput;
pub use tool_payload::ToolPayload;
pub use tool_spec::ResponsesApiWebSearchFilters;
+23
View File
@@ -0,0 +1,23 @@
use crate::FunctionCallError;
use crate::ToolName;
use crate::ToolPayload;
// TODO: this is temporary and will disappear in the next PR (as we make codex-extension-api generic on Invocation.
#[derive(Clone, Debug)]
pub struct ToolCall {
pub call_id: String,
pub tool_name: ToolName,
pub payload: ToolPayload,
}
impl ToolCall {
pub fn function_arguments(&self) -> Result<&str, FunctionCallError> {
match &self.payload {
ToolPayload::Function { arguments } => Ok(arguments),
_ => Err(FunctionCallError::Fatal(format!(
"tool {} invoked with incompatible payload",
self.tool_name
))),
}
}
}
+83
View File
@@ -1,6 +1,7 @@
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_utils_string::take_bytes_at_char_boundary;
use serde_json::Value as JsonValue;
@@ -35,6 +36,88 @@ pub trait ToolOutput: Send {
}
}
impl<T> ToolOutput for Box<T>
where
T: ToolOutput + ?Sized,
{
fn log_preview(&self) -> String {
(**self).log_preview()
}
fn success_for_logging(&self) -> bool {
(**self).success_for_logging()
}
fn to_response_item(&self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem {
(**self).to_response_item(call_id, payload)
}
fn post_tool_use_response(&self, call_id: &str, payload: &ToolPayload) -> Option<JsonValue> {
(**self).post_tool_use_response(call_id, payload)
}
fn code_mode_result(&self, payload: &ToolPayload) -> JsonValue {
(**self).code_mode_result(payload)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct JsonToolOutput {
value: JsonValue,
success: Option<bool>,
}
impl JsonToolOutput {
pub fn new(value: JsonValue) -> Self {
Self {
value,
success: Some(true),
}
}
pub fn with_success(value: JsonValue, success: Option<bool>) -> Self {
Self { value, success }
}
}
impl ToolOutput for JsonToolOutput {
fn log_preview(&self) -> String {
telemetry_preview(&self.value.to_string())
}
fn success_for_logging(&self) -> bool {
self.success.unwrap_or(true)
}
fn to_response_item(&self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem {
let output = FunctionCallOutputPayload {
body: FunctionCallOutputBody::Text(self.value.to_string()),
success: self.success,
};
if matches!(payload, ToolPayload::Custom { .. }) {
return ResponseInputItem::CustomToolCallOutput {
call_id: call_id.to_string(),
name: None,
output,
};
}
ResponseInputItem::FunctionCallOutput {
call_id: call_id.to_string(),
output,
}
}
fn post_tool_use_response(&self, _call_id: &str, _payload: &ToolPayload) -> Option<JsonValue> {
Some(self.value.clone())
}
fn code_mode_result(&self, _payload: &ToolPayload) -> JsonValue {
self.value.clone()
}
}
impl ToolOutput for codex_protocol::mcp::CallToolResult {
fn log_preview(&self) -> String {
let output = self.as_function_call_output_payload();