mirror of
https://github.com/openai/codex.git
synced 2026-05-29 15:30:22 +00:00
refactor: extract executable tool contracts into codex-tool-api (#22138)
## Why The tool-extraction work needs one shared executable-tool seam that hosts and tool owners can depend on without reaching into `codex-core`. Landing that seam first makes the later tool-family ports incremental and keeps the reusable contract separate from any one migration. ## What changed - add a new `codex-tool-api` crate and workspace wiring - move the common executable-tool contracts into that crate: `ToolBundle`, `ToolDefinition`, `ToolExecutor`, `ToolCall`, `ToolInput`, `ToolOutput`, `JsonToolOutput`, and `ToolError` - keep host state generic through `ToolBundle<C>` / `ToolCall<C>` so later integrations can provide their own runtime context without baking core types into the API - carry the host signals the runtime will need later, including parallel-call support and mutability probing - leave existing tool families in place for now; this PR only establishes the reusable API surface - add the Bazel target and lockfile updates for the new crate ## Testing - `cargo test -p codex-tool-api`
This commit is contained in:
12
codex-rs/Cargo.lock
generated
12
codex-rs/Cargo.lock
generated
@@ -3706,6 +3706,18 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-tool-api"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"codex-protocol",
|
||||
"codex-tools",
|
||||
"pretty_assertions",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-tools"
|
||||
version = "0.0.0"
|
||||
|
||||
@@ -108,6 +108,7 @@ members = [
|
||||
"test-binary-support",
|
||||
"thread-manager-sample",
|
||||
"thread-store",
|
||||
"tool-api",
|
||||
"uds",
|
||||
"codex-experimental-api-macros",
|
||||
"plugin",
|
||||
@@ -209,6 +210,7 @@ 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" }
|
||||
@@ -472,6 +474,7 @@ unwrap_used = "deny"
|
||||
[workspace.metadata.cargo-shear]
|
||||
ignored = [
|
||||
"codex-agent-graph-store",
|
||||
"codex-tool-api",
|
||||
"icu_provider",
|
||||
"openssl-sys",
|
||||
"codex-v8-poc",
|
||||
|
||||
6
codex-rs/tool-api/BUILD.bazel
Normal file
6
codex-rs/tool-api/BUILD.bazel
Normal file
@@ -0,0 +1,6 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "tool-api",
|
||||
crate_name = "codex_tool_api",
|
||||
)
|
||||
23
codex-rs/tool-api/Cargo.toml
Normal file
23
codex-rs/tool-api/Cargo.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
[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]
|
||||
codex-protocol = { workspace = true }
|
||||
codex-tools = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = { workspace = true }
|
||||
79
codex-rs/tool-api/src/bundle.rs
Normal file
79
codex-rs/tool-api/src/bundle.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolSpec;
|
||||
|
||||
use crate::ToolCall;
|
||||
use crate::ToolError;
|
||||
use crate::ToolOutput;
|
||||
|
||||
/// Future returned by one executable-tool invocation.
|
||||
pub type ToolFuture<'a> =
|
||||
Pin<Box<dyn Future<Output = Result<Box<dyn ToolOutput>, ToolError>> + Send + 'a>>;
|
||||
|
||||
/// Future returned by one mutability probe.
|
||||
pub type BoolFuture<'a> = Pin<Box<dyn Future<Output = bool> + Send + 'a>>;
|
||||
|
||||
/// Model-visible definition plus executable implementation for one tool.
|
||||
#[derive(Clone)]
|
||||
pub struct ToolBundle<C> {
|
||||
definition: ToolDefinition,
|
||||
executor: Arc<dyn ToolExecutor<C>>,
|
||||
}
|
||||
|
||||
impl<C> ToolBundle<C> {
|
||||
/// Creates one executable tool bundle.
|
||||
pub fn new(name: ToolName, spec: ToolSpec, executor: Arc<dyn ToolExecutor<C>>) -> Self {
|
||||
Self {
|
||||
definition: ToolDefinition {
|
||||
name,
|
||||
spec,
|
||||
supports_parallel_tool_calls: false,
|
||||
},
|
||||
executor,
|
||||
}
|
||||
}
|
||||
|
||||
/// Marks this tool as safe for the host to run in parallel with peers.
|
||||
#[must_use]
|
||||
pub fn allow_parallel_calls(mut self) -> Self {
|
||||
self.definition.supports_parallel_tool_calls = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the model-visible tool definition.
|
||||
pub fn definition(&self) -> &ToolDefinition {
|
||||
&self.definition
|
||||
}
|
||||
|
||||
/// Returns the executable implementation.
|
||||
pub fn executor(&self) -> Arc<dyn ToolExecutor<C>> {
|
||||
Arc::clone(&self.executor)
|
||||
}
|
||||
}
|
||||
|
||||
/// Model-visible metadata owned by an executable tool bundle.
|
||||
#[derive(Clone)]
|
||||
pub struct ToolDefinition {
|
||||
pub name: ToolName,
|
||||
pub spec: ToolSpec,
|
||||
pub supports_parallel_tool_calls: bool,
|
||||
}
|
||||
|
||||
/// Executable behavior for one contributed tool.
|
||||
///
|
||||
/// Implementations should keep host-specific needs inside `C`; tool owners that
|
||||
/// do not require host state can implement the trait for any `C`.
|
||||
pub trait ToolExecutor<C>: Send + Sync {
|
||||
fn execute<'a>(&'a self, call: ToolCall<C>) -> ToolFuture<'a>;
|
||||
|
||||
/// Returns whether the call may mutate user state.
|
||||
///
|
||||
/// Hosts can use this conservative signal for serialization or approval
|
||||
/// policy. Context-free read tools should keep the default.
|
||||
fn is_mutating<'a>(&'a self, _call: &'a ToolCall<C>) -> BoolFuture<'a> {
|
||||
Box::pin(async { false })
|
||||
}
|
||||
}
|
||||
14
codex-rs/tool-api/src/call.rs
Normal file
14
codex-rs/tool-api/src/call.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
/// One executable tool call delivered to a contributed tool.
|
||||
pub struct ToolCall<C> {
|
||||
pub context: C,
|
||||
pub call_id: String,
|
||||
pub input: ToolInput,
|
||||
}
|
||||
|
||||
/// Model-supplied input for the executable tool families currently exposed by
|
||||
/// the shared tool seam.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum ToolInput {
|
||||
Function { arguments: String },
|
||||
Freeform { input: String },
|
||||
}
|
||||
22
codex-rs/tool-api/src/error.rs
Normal file
22
codex-rs/tool-api/src/error.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
17
codex-rs/tool-api/src/lib.rs
Normal file
17
codex-rs/tool-api/src/lib.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
//! Reusable executable-tool contracts shared between hosts and tool owners.
|
||||
|
||||
mod bundle;
|
||||
mod call;
|
||||
mod error;
|
||||
mod output;
|
||||
|
||||
pub use bundle::BoolFuture;
|
||||
pub use bundle::ToolBundle;
|
||||
pub use bundle::ToolDefinition;
|
||||
pub use bundle::ToolExecutor;
|
||||
pub use bundle::ToolFuture;
|
||||
pub use call::ToolCall;
|
||||
pub use call::ToolInput;
|
||||
pub use error::ToolError;
|
||||
pub use output::JsonToolOutput;
|
||||
pub use output::ToolOutput;
|
||||
113
codex-rs/tool-api/src/output.rs
Normal file
113
codex-rs/tool-api/src/output.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
use codex_protocol::models::FunctionCallOutputBody;
|
||||
use codex_protocol::models::FunctionCallOutputPayload;
|
||||
use codex_protocol::models::ResponseInputItem;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::ToolError;
|
||||
use crate::ToolInput;
|
||||
|
||||
/// Tool-owned output rendering for each host-facing boundary.
|
||||
pub trait ToolOutput: Send {
|
||||
fn log_preview(&self) -> String;
|
||||
|
||||
fn success_for_logging(&self) -> bool;
|
||||
|
||||
fn to_response_item(&self, call_id: &str, input: &ToolInput) -> ResponseInputItem;
|
||||
|
||||
/// Returns the stable value exposed to post-tool-use hook integration when a
|
||||
/// host chooses to wire that surface for this tool.
|
||||
fn post_tool_use_response(&self, _call_id: &str, _input: &ToolInput) -> Option<Value> {
|
||||
None
|
||||
}
|
||||
|
||||
fn code_mode_result(&self, input: &ToolInput) -> Value;
|
||||
}
|
||||
|
||||
/// Convenience output for ordinary JSON-returning function tools.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct JsonToolOutput {
|
||||
value: Value,
|
||||
}
|
||||
|
||||
impl JsonToolOutput {
|
||||
/// Creates a JSON output from a serializable value.
|
||||
pub fn from_serializable(value: impl Serialize) -> Result<Self, ToolError> {
|
||||
serde_json::to_value(value).map(Self::new).map_err(|err| {
|
||||
ToolError::respond_to_model(format!("failed to serialize output: {err}"))
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a JSON output from an already materialized value.
|
||||
pub fn new(value: Value) -> Self {
|
||||
Self { value }
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolOutput for JsonToolOutput {
|
||||
fn log_preview(&self) -> String {
|
||||
self.value.to_string()
|
||||
}
|
||||
|
||||
fn success_for_logging(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn to_response_item(&self, call_id: &str, _input: &ToolInput) -> 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, _input: &ToolInput) -> Option<Value> {
|
||||
Some(self.value.clone())
|
||||
}
|
||||
|
||||
fn code_mode_result(&self, _input: &ToolInput) -> Value {
|
||||
self.value.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use codex_protocol::models::FunctionCallOutputBody;
|
||||
use codex_protocol::models::FunctionCallOutputPayload;
|
||||
use codex_protocol::models::ResponseInputItem;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
|
||||
use super::JsonToolOutput;
|
||||
use super::ToolOutput;
|
||||
use crate::ToolInput;
|
||||
|
||||
#[test]
|
||||
fn json_tool_output_renders_function_output() {
|
||||
let input = ToolInput::Function {
|
||||
arguments: "{}".to_string(),
|
||||
};
|
||||
let output = JsonToolOutput::from_serializable(json!({ "ok": true }))
|
||||
.expect("serializable value should produce json output");
|
||||
|
||||
assert_eq!(output.log_preview(), "{\"ok\":true}");
|
||||
assert!(output.success_for_logging());
|
||||
assert_eq!(
|
||||
output.to_response_item("call-1", &input),
|
||||
ResponseInputItem::FunctionCallOutput {
|
||||
call_id: "call-1".to_string(),
|
||||
output: FunctionCallOutputPayload {
|
||||
body: FunctionCallOutputBody::Text("{\"ok\":true}".to_string()),
|
||||
success: Some(true),
|
||||
},
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
output.post_tool_use_response("call-1", &input),
|
||||
Some(json!({ "ok": true }))
|
||||
);
|
||||
assert_eq!(output.code_mode_result(&input), json!({ "ok": true }));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user