mirror of
https://github.com/openai/codex.git
synced 2026-11-06 00:30:29 +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:
1 parent
569ff6a1c4
commit
95bfea847d
9 files changed
+289
No files matched your search
Generated
+12
@@ -3706,6 +3706,18 @@ dependencies = [
|
|||||||
"uuid",
|
"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]]
|
[[package]]
|
||||||
name = "codex-tools"
|
name = "codex-tools"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ members = [
|
|||||||
"test-binary-support",
|
"test-binary-support",
|
||||||
"thread-manager-sample",
|
"thread-manager-sample",
|
||||||
"thread-store",
|
"thread-store",
|
||||||
|
"tool-api",
|
||||||
"uds",
|
"uds",
|
||||||
"codex-experimental-api-macros",
|
"codex-experimental-api-macros",
|
||||||
"plugin",
|
"plugin",
|
||||||
@@ -209,6 +210,7 @@ codex-stdio-to-uds = { path = "stdio-to-uds" }
|
|||||||
codex-terminal-detection = { path = "terminal-detection" }
|
codex-terminal-detection = { path = "terminal-detection" }
|
||||||
codex-test-binary-support = { path = "test-binary-support" }
|
codex-test-binary-support = { path = "test-binary-support" }
|
||||||
codex-thread-store = { path = "thread-store" }
|
codex-thread-store = { path = "thread-store" }
|
||||||
|
codex-tool-api = { path = "tool-api" }
|
||||||
codex-tools = { path = "tools" }
|
codex-tools = { path = "tools" }
|
||||||
codex-tui = { path = "tui" }
|
codex-tui = { path = "tui" }
|
||||||
codex-uds = { path = "uds" }
|
codex-uds = { path = "uds" }
|
||||||
@@ -472,6 +474,7 @@ unwrap_used = "deny"
|
|||||||
[workspace.metadata.cargo-shear]
|
[workspace.metadata.cargo-shear]
|
||||||
ignored = [
|
ignored = [
|
||||||
"codex-agent-graph-store",
|
"codex-agent-graph-store",
|
||||||
|
"codex-tool-api",
|
||||||
"icu_provider",
|
"icu_provider",
|
||||||
"openssl-sys",
|
"openssl-sys",
|
||||||
"codex-v8-poc",
|
"codex-v8-poc",
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
load("//:defs.bzl", "codex_rust_crate")
|
||||||
|
|
||||||
|
codex_rust_crate(
|
||||||
|
name = "tool-api",
|
||||||
|
crate_name = "codex_tool_api",
|
||||||
|
)
|
||||||
@@ -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 }
|
||||||
@@ -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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 },
|
||||||
|
}
|
||||||
@@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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