Compare commits

...

2 Commits

Author SHA1 Message Date
jif-oai
3121e5745e reduce tests 2026-05-11 19:25:35 +01:00
jif-oai
4e244b254b go further 2026-05-11 19:11:15 +01:00
17 changed files with 483 additions and 170 deletions

3
codex-rs/Cargo.lock generated
View File

@@ -3695,7 +3695,9 @@ dependencies = [
name = "codex-tool-api"
version = "0.0.0"
dependencies = [
"codex-protocol",
"pretty_assertions",
"serde",
"serde_json",
"thiserror 2.0.18",
]
@@ -3708,6 +3710,7 @@ dependencies = [
"codex-code-mode",
"codex-features",
"codex-protocol",
"codex-tool-api",
"codex-utils-absolute-path",
"codex-utils-pty",
"pretty_assertions",

View File

@@ -3,7 +3,6 @@ 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 codex_tools::ToolName;
use codex_tools::ToolSpec;
use serde_json::Value;
@@ -73,7 +72,7 @@ impl ToolHandler for BundledToolHandler {
type Output = BundledToolOutput;
fn tool_name(&self) -> ToolName {
ToolName::plain(self.bundle.tool_name())
self.bundle.tool_name().clone()
}
fn spec(&self) -> Option<ToolSpec> {
@@ -138,19 +137,6 @@ 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),
@@ -175,7 +161,6 @@ mod tests {
use super::BundledToolHandler;
use super::BundledToolOutput;
use super::extension_tool_spec;
use crate::tools::context::ToolCallSource;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
@@ -184,6 +169,7 @@ mod tests {
use crate::tools::registry::PreToolUsePayload;
use crate::tools::registry::ToolHandler;
use crate::turn_diff_tracker::TurnDiffTracker;
use codex_tools::ToolSpec;
struct StubExtensionExecutor;
@@ -196,22 +182,27 @@ mod tests {
#[tokio::test]
async fn exposes_generic_hook_payloads_and_is_conservatively_mutating() {
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,
}),
},
codex_tool_api::ToolName::plain("extension_echo"),
"Echoes arguments.".to_string(),
codex_tool_api::JsonSchema::object(
std::collections::BTreeMap::from([(
"message".to_string(),
codex_tool_api::JsonSchema::string(/*description*/ None),
)]),
Some(vec!["message".to_string()]),
Some(false.into()),
),
Arc::new(StubExtensionExecutor),
);
let spec = extension_tool_spec(bundle.spec()).expect("extension spec should convert");
)
.strict();
let spec = ToolSpec::Function(codex_tools::ResponsesApiTool {
name: bundle.tool_name().name.clone(),
description: bundle.description().to_string(),
strict: bundle.is_strict(),
defer_loading: bundle.defer_loading().then_some(true),
parameters: bundle.input_schema().clone(),
output_schema: bundle.output_schema().cloned(),
});
let handler = BundledToolHandler::new(bundle, spec);
let (session, turn) = crate::session::tests::make_session_and_context().await;
let invocation = ToolInvocation {

View File

@@ -17,7 +17,6 @@ use crate::tools::context::ToolOutput;
use crate::tools::context::ToolPayload;
use crate::tools::flat_tool_name;
use crate::tools::handlers::extension_tools::BundledToolHandler;
use crate::tools::handlers::extension_tools::extension_tool_spec;
use crate::tools::hook_names::HookToolName;
use crate::tools::tool_dispatch_trace::ToolDispatchTrace;
use crate::util::error_or_panic;
@@ -25,8 +24,12 @@ use codex_protocol::models::ResponseInputItem;
use codex_protocol::protocol::EventMsg;
use codex_tool_api::ToolBundle as ExtensionToolBundle;
use codex_tools::ConfiguredToolSpec;
use codex_tools::ResponsesApiNamespace;
use codex_tools::ResponsesApiNamespaceTool;
use codex_tools::ResponsesApiTool;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
use codex_tools::default_namespace_description;
use codex_utils_readiness::Readiness;
use futures::future::BoxFuture;
use serde_json::Value;
@@ -512,12 +515,16 @@ impl ToolRegistryBuilder {
}
}
pub(crate) fn push_spec(&mut self, spec: ToolSpec, supports_parallel_tool_calls: bool) {
let spec = if self.code_mode_enabled {
fn prepare_spec(&self, spec: ToolSpec) -> ToolSpec {
if self.code_mode_enabled {
codex_tools::augment_tool_spec_for_code_mode(spec)
} else {
spec
};
}
}
pub(crate) fn push_spec(&mut self, spec: ToolSpec, supports_parallel_tool_calls: bool) {
let spec = self.prepare_spec(spec);
self.specs
.push(ConfiguredToolSpec::new(spec, supports_parallel_tool_calls));
}
@@ -542,22 +549,64 @@ impl ToolRegistryBuilder {
}
pub fn register_tool_bundle(&mut self, bundle: ExtensionToolBundle) {
let tool_name = ToolName::plain(bundle.tool_name());
let tool_name = bundle.tool_name().clone();
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;
}
let function_tool = ResponsesApiTool {
name: bundle.tool_name().name.clone(),
description: bundle.description().to_string(),
strict: bundle.is_strict(),
defer_loading: bundle.defer_loading().then_some(true),
parameters: bundle.input_schema().clone(),
output_schema: bundle.output_schema().cloned(),
};
self.push_spec(spec.clone(), /*supports_parallel_tool_calls*/ false);
let spec = match bundle.namespace() {
Some(namespace) => ToolSpec::Namespace(ResponsesApiNamespace {
name: namespace.name().to_string(),
description: namespace
.description()
.map(str::to_string)
.unwrap_or_else(|| default_namespace_description(namespace.name())),
tools: vec![ResponsesApiNamespaceTool::Function(function_tool)],
}),
None => ToolSpec::Function(function_tool),
};
match self.prepare_spec(spec.clone()) {
ToolSpec::Namespace(mut namespace) => {
if let Some(existing_namespace) =
self.specs.iter_mut().find_map(|configured_tool| {
match &mut configured_tool.spec {
ToolSpec::Namespace(existing_namespace)
if existing_namespace.name == namespace.name =>
{
Some(existing_namespace)
}
ToolSpec::Function(_)
| ToolSpec::ToolSearch { .. }
| ToolSpec::LocalShell {}
| ToolSpec::ImageGeneration { .. }
| ToolSpec::WebSearch { .. }
| ToolSpec::Freeform(_)
| ToolSpec::Namespace(_) => None,
}
})
{
existing_namespace.tools.append(&mut namespace.tools);
} else {
self.specs.push(ConfiguredToolSpec::new(
ToolSpec::Namespace(namespace),
/*supports_parallel_tool_calls*/ false,
));
}
}
prepared_spec => self.specs.push(ConfiguredToolSpec::new(
prepared_spec,
/*supports_parallel_tool_calls*/ false,
)),
}
let handler: Arc<dyn AnyToolHandler> = Arc::new(BundledToolHandler::new(bundle, spec));
self.handlers.insert(tool_name, handler);

View File

@@ -8,7 +8,6 @@ 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;
@@ -16,6 +15,7 @@ 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::JsonSchema;
use codex_tool_api::ToolCall as ExtensionToolCall;
use codex_tools::ResponsesApiNamespaceTool;
use codex_tools::ToolName;
@@ -38,22 +38,40 @@ impl codex_extension_api::ToolContributor for ExtensionEchoContributor {
_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![
ToolBundle::new(
ToolName::plain("extension_echo"),
"Echoes arguments through an extension tool.".to_string(),
JsonSchema::object(
std::collections::BTreeMap::from([(
"message".to_string(),
JsonSchema::string(/*description*/ None),
)]),
Some(vec!["message".to_string()]),
Some(false.into()),
),
Arc::new(ExtensionEchoExecutor),
)
.strict(),
ToolBundle::new(
ToolName::plain("echo"),
"Echoes arguments through a namespaced extension tool.".to_string(),
JsonSchema::object(
std::collections::BTreeMap::from([(
"message".to_string(),
JsonSchema::string(/*description*/ None),
)]),
Some(vec!["message".to_string()]),
Some(false.into()),
),
Arc::new(ExtensionEchoExecutor),
)
.in_namespace(
codex_extension_api::ToolNamespace::new("extension_tools")
.with_description("Extension-owned tools."),
)
.strict(),
]
}
}
@@ -296,35 +314,57 @@ async fn extension_tool_bundles_are_model_visible_and_dispatchable() -> anyhow::
.any(|spec| spec.name() == "extension_echo"),
"expected extension-provided tool to be visible to the model"
);
assert!(
router
.find_spec(&ToolName::namespaced("extension_tools", "echo"))
.is_some(),
"expected namespaced extension-provided tool spec to be registered"
);
assert_eq!(
namespace_function_names(&router.model_visible_specs(), "extension_tools"),
vec!["echo".to_string()]
);
let session = Arc::new(session);
let turn = Arc::new(turn);
let call = ToolRouter::build_tool_call(
&session,
ResponseItem::FunctionCall {
id: None,
name: "extension_echo".to_string(),
namespace: None,
arguments: json!({ "message": "hello" }).to_string(),
call_id: "call-extension".to_string(),
},
)
.await?
.expect("function_call should produce a tool call");
let result = router
.dispatch_tool_call_with_code_mode_result(
Arc::new(session),
Arc::new(turn),
CancellationToken::new(),
Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new())),
call,
ToolCallSource::Direct,
for (name, namespace, call_id) in [
("extension_echo", None, "call-extension"),
("echo", Some("extension_tools"), "call-extension-namespace"),
] {
let call = ToolRouter::build_tool_call(
session.as_ref(),
ResponseItem::FunctionCall {
id: None,
name: name.to_string(),
namespace: namespace.map(str::to_string),
arguments: json!({ "message": "hello" }).to_string(),
call_id: call_id.to_string(),
},
)
.await?;
.await?
.expect("function_call should produce a tool call");
let response = result.into_response();
let result = router
.dispatch_tool_call_with_code_mode_result(
Arc::clone(&session),
Arc::clone(&turn),
CancellationToken::new(),
Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new())),
call,
ToolCallSource::Direct,
)
.await?;
assert_extension_tool_response(result.into_response(), call_id);
}
Ok(())
}
fn assert_extension_tool_response(response: ResponseInputItem, expected_call_id: &str) {
match response {
ResponseInputItem::FunctionCallOutput { call_id, output } => {
assert_eq!(call_id, "call-extension");
assert_eq!(call_id, expected_call_id);
let FunctionCallOutputBody::Text(text) = output.body else {
panic!("expected text function call output")
};
@@ -334,15 +374,13 @@ async fn extension_tool_bundles_are_model_visible_and_dispatchable() -> anyhow::
value,
json!({
"arguments": { "message": "hello" },
"callId": "call-extension",
"callId": expected_call_id,
"ok": true,
})
);
}
other => panic!("expected function call output, got {other:?}"),
}
Ok(())
}
fn namespace_function_names(specs: &[ToolSpec], namespace_name: &str) -> Vec<String> {

View File

@@ -74,7 +74,7 @@ pub fn build_tool_registry_builder(
let exec_permission_approvals_enabled = config.exec_permission_approvals_enabled;
if config.code_mode_enabled {
let namespace_descriptions = params
let mut namespace_descriptions = params
.tool_namespaces
.into_iter()
.flatten()
@@ -88,6 +88,17 @@ pub fn build_tool_registry_builder(
)
})
.collect::<BTreeMap<_, _>>();
for bundle in params.extension_tool_bundles {
let Some(namespace) = bundle.namespace() else {
continue;
};
namespace_descriptions
.entry(namespace.name().to_string())
.or_insert_with(|| codex_code_mode::ToolNamespaceDescription {
name: namespace.name().to_string(),
description: namespace.description().unwrap_or_default().to_string(),
});
}
let nested_config = config.for_code_mode_nested_tools();
let nested_builder = build_tool_registry_builder(
&nested_config,

View File

@@ -43,7 +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;
@@ -87,22 +86,44 @@ impl ToolExecutor for UnusedExtensionExecutor {
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"),
},
ToolName::plain(name),
description.to_string(),
JsonSchema::object(
BTreeMap::from([(
"message".to_string(),
JsonSchema::string(/*description*/ None),
)]),
Some(vec!["message".to_string()]),
Some(false.into()),
),
std::sync::Arc::new(UnusedExtensionExecutor),
)
.strict()
}
fn namespaced_extension_tool_bundle(
name: &str,
description: &str,
namespace: &str,
namespace_description: &str,
) -> ExtensionToolBundle {
extension_tool_bundle(name, description).in_namespace(
codex_tool_api::ToolNamespace::new(namespace).with_description(namespace_description),
)
}
fn build_specs_for_extension_tools(
config: &ToolsConfig,
extension_tool_bundles: &[ExtensionToolBundle],
) -> (Vec<ConfiguredToolSpec>, ToolRegistry) {
build_specs_with_discoverable_tools(
config,
/*mcp_tools*/ None,
/*deferred_mcp_tools*/ None,
/*discoverable_tools*/ None,
extension_tool_bundles,
&[],
)
}
#[test]
@@ -123,14 +144,7 @@ fn extension_tools_do_not_replace_builtin_tools() {
"update_plan",
"Extension attempt to replace a built-in tool.",
)];
let (tools, _) = build_specs_with_discoverable_tools(
&tools_config,
/*mcp_tools*/ None,
/*deferred_mcp_tools*/ None,
/*discoverable_tools*/ None,
&extension_tool_bundles,
&[],
);
let (tools, _) = build_specs_for_extension_tools(&tools_config, &extension_tool_bundles);
assert_eq!(
find_tool(&tools, "update_plan").spec,
@@ -145,6 +159,46 @@ fn extension_tools_do_not_replace_builtin_tools() {
);
}
#[test]
fn namespaced_extension_tools_coalesce_into_namespace_specs() {
let model_info = model_info();
let available_models = Vec::new();
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
available_models: &available_models,
features: &Features::with_defaults(),
image_generation_tool_auth_allowed: true,
web_search_mode: Some(WebSearchMode::Cached),
session_source: SessionSource::Cli,
permission_profile: &PermissionProfile::Disabled,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
});
let extension_tool_bundles = vec![
namespaced_extension_tool_bundle(
"echo",
"Echoes extension arguments.",
"extension_tools",
"Extension-owned tools.",
),
namespaced_extension_tool_bundle(
"status",
"Returns extension status.",
"extension_tools",
"Extension-owned tools.",
),
];
let (tools, _) = build_specs_for_extension_tools(&tools_config, &extension_tool_bundles);
assert_eq!(
namespace_function_names(&tools, "extension_tools"),
vec!["echo".to_string(), "status".to_string()]
);
let ToolSpec::Namespace(namespace) = &find_tool(&tools, "extension_tools").spec else {
panic!("expected extension namespace tool");
};
assert_eq!(namespace.description, "Extension-owned tools.");
}
#[test]
fn test_full_toolset_specs_for_gpt5_codex_unified_exec_web_search() {
let model_info = model_info();
@@ -2264,7 +2318,7 @@ fn code_mode_only_exec_description_includes_full_nested_tool_details() {
}
#[test]
fn code_mode_only_exec_description_includes_extension_tool_details() {
fn code_mode_only_exec_description_includes_extension_tool_details_and_namespace_guidance() {
let model_info = model_info();
let mut features = Features::with_defaults();
features.enable(Feature::CodeMode);
@@ -2281,18 +2335,19 @@ fn code_mode_only_exec_description_includes_extension_tool_details() {
windows_sandbox_level: WindowsSandboxLevel::Disabled,
});
let extension_tool_bundles = vec![extension_tool_bundle(
"extension_echo",
"Echoes arguments through an extension tool.",
)];
let (tools, _) = build_specs_with_discoverable_tools(
&tools_config,
/*mcp_tools*/ None,
/*deferred_mcp_tools*/ None,
/*discoverable_tools*/ None,
&extension_tool_bundles,
&[],
);
let extension_tool_bundles = vec![
extension_tool_bundle(
"extension_echo",
"Echoes arguments through an extension tool.",
),
namespaced_extension_tool_bundle(
"echo",
"Echoes arguments through a namespaced extension tool.",
"extension_tools",
"Extension-owned tools.",
),
];
let (tools, _) = build_specs_for_extension_tools(&tools_config, &extension_tool_bundles);
let ToolSpec::Freeform(FreeformTool { description, .. }) = &find_tool(&tools, "exec").spec
else {
panic!("expected freeform tool");
@@ -2300,6 +2355,7 @@ fn code_mode_only_exec_description_includes_extension_tool_details() {
assert!(description.contains("### `extension_echo`"));
assert!(description.contains("Echoes arguments through an extension tool."));
assert!(description.contains("## extension_tools\nExtension-owned tools."));
}
#[test]

View File

@@ -2,12 +2,13 @@ mod contributors;
mod registry;
mod state;
pub use codex_tool_api::FunctionToolSpec;
pub use codex_tool_api::ToolBundle;
pub use codex_tool_api::ToolCall;
pub use codex_tool_api::ToolError;
pub use codex_tool_api::ToolExecutor;
pub use codex_tool_api::ToolFuture;
pub use codex_tool_api::ToolName;
pub use codex_tool_api::ToolNamespace;
pub use contributors::ApprovalInterceptorContributor;
pub use contributors::ContextContributor;
pub use contributors::PromptFragment;

View File

@@ -13,6 +13,8 @@ doctest = false
workspace = true
[dependencies]
codex-protocol = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
thiserror = { workspace = true }

View File

@@ -8,8 +8,9 @@ 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 shared model-visible function-tool fields stored directly on
`ToolBundle`
- contributed namespace metadata through `ToolNamespace`
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
@@ -23,7 +24,7 @@ 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`.
API tool models, namespace rendering and aggregation, 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`.

View File

@@ -4,9 +4,11 @@ use std::sync::Arc;
use serde_json::Value;
use crate::FunctionToolSpec;
use crate::JsonSchema;
use crate::ToolCall;
use crate::ToolError;
use crate::ToolName;
use crate::ToolNamespace;
/// Future returned by one contributed function-tool invocation.
pub type ToolFuture<'a> = Pin<Box<dyn Future<Output = Result<Value, ToolError>> + Send + 'a>>;
@@ -15,24 +17,98 @@ pub type ToolFuture<'a> = Pin<Box<dyn Future<Output = Result<Value, ToolError>>
/// function tool.
#[derive(Clone)]
pub struct ToolBundle {
spec: FunctionToolSpec,
tool_name: ToolName,
namespace: Option<ToolNamespace>,
description: String,
input_schema: JsonSchema,
strict: bool,
output_schema: Option<Value>,
defer_loading: bool,
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 }
pub fn new(
tool_name: ToolName,
description: String,
input_schema: JsonSchema,
executor: Arc<dyn ToolExecutor>,
) -> Self {
let namespace = tool_name
.namespace
.as_ref()
.map(|namespace| ToolNamespace::new(namespace.clone()));
Self {
tool_name,
namespace,
description,
input_schema,
strict: false,
output_schema: None,
defer_loading: false,
executor,
}
}
/// Returns the contributed function-tool spec.
pub fn spec(&self) -> &FunctionToolSpec {
&self.spec
/// Enables strict schema handling for this contributed tool.
pub fn strict(mut self) -> Self {
self.strict = true;
self
}
/// Adds a model-visible output schema for this contributed tool.
pub fn with_output_schema(mut self, output_schema: Value) -> Self {
self.output_schema = Some(output_schema);
self
}
/// Marks this contributed tool as loadable on demand.
pub fn deferred(mut self) -> Self {
self.defer_loading = true;
self
}
/// Places this contributed tool in a model-visible namespace.
pub fn in_namespace(mut self, namespace: ToolNamespace) -> Self {
self.tool_name.namespace = Some(namespace.name().to_string());
self.namespace = Some(namespace);
self
}
/// Returns the contributed function-tool name.
pub fn tool_name(&self) -> &str {
self.spec.name.as_str()
pub fn tool_name(&self) -> &ToolName {
&self.tool_name
}
/// Returns the contributed tool namespace, if any.
pub fn namespace(&self) -> Option<&ToolNamespace> {
self.namespace.as_ref()
}
/// Returns the contributed function-tool description.
pub fn description(&self) -> &str {
self.description.as_str()
}
/// Returns the contributed function-tool input schema.
pub fn input_schema(&self) -> &JsonSchema {
&self.input_schema
}
/// Returns whether strict schema handling is enabled.
pub fn is_strict(&self) -> bool {
self.strict
}
/// Returns the optional contributed function-tool output schema.
pub fn output_schema(&self) -> Option<&Value> {
self.output_schema.as_ref()
}
/// Returns whether the contributed function tool should be loadable on demand.
pub fn defer_loading(&self) -> bool {
self.defer_loading
}
/// Returns the executable implementation.
@@ -59,8 +135,10 @@ mod tests {
use super::ToolBundle;
use super::ToolExecutor;
use super::ToolFuture;
use crate::FunctionToolSpec;
use crate::JsonSchema;
use crate::ToolCall;
use crate::ToolName;
use crate::ToolNamespace;
struct StubExecutor;
@@ -71,17 +149,67 @@ mod tests {
}
#[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" }),
},
fn bundle_preserves_input_tool_name_and_namespace_metadata() {
let plain_bundle = ToolBundle::new(
ToolName::plain("echo"),
"Echo arguments.".to_string(),
JsonSchema::object(
std::collections::BTreeMap::new(),
/*required*/ None,
Some(true.into()),
),
Arc::new(StubExecutor),
);
let namespaced_bundle = ToolBundle::new(
ToolName::namespaced("extension_tools/", "echo"),
"Echo arguments.".to_string(),
JsonSchema::object(
std::collections::BTreeMap::new(),
/*required*/ None,
Some(true.into()),
),
Arc::new(StubExecutor),
);
assert_eq!(bundle.tool_name(), "echo");
assert_eq!(plain_bundle.tool_name(), &ToolName::plain("echo"));
assert_eq!(plain_bundle.namespace(), None);
assert_eq!(
namespaced_bundle.tool_name(),
&ToolName::namespaced("extension_tools/", "echo")
);
assert_eq!(
namespaced_bundle.namespace(),
Some(&ToolNamespace::new("extension_tools/"))
);
}
#[test]
fn bundle_in_namespace_updates_tool_name_and_namespace_metadata() {
let bundle = ToolBundle::new(
ToolName::plain("echo"),
"Echo arguments.".to_string(),
JsonSchema::object(
std::collections::BTreeMap::new(),
/*required*/ None,
Some(true.into()),
),
Arc::new(StubExecutor),
)
.in_namespace(
ToolNamespace::new("extension_tools/")
.with_description("Extension-owned function tools."),
);
assert_eq!(
bundle.tool_name(),
&ToolName::namespaced("extension_tools/", "echo")
);
assert_eq!(
bundle.namespace(),
Some(
&ToolNamespace::new("extension_tools/")
.with_description("Extension-owned function tools.")
)
);
}
}

View File

@@ -4,11 +4,18 @@
mod bundle;
mod call;
mod error;
mod spec;
mod json_schema;
mod namespace;
pub use bundle::ToolBundle;
pub use bundle::ToolExecutor;
pub use bundle::ToolFuture;
pub use call::ToolCall;
pub use codex_protocol::ToolName;
pub use error::ToolError;
pub use spec::FunctionToolSpec;
pub use json_schema::AdditionalProperties;
pub use json_schema::JsonSchema;
pub use json_schema::JsonSchemaPrimitiveType;
pub use json_schema::JsonSchemaType;
pub use json_schema::parse_tool_input_schema;
pub use namespace::ToolNamespace;

View File

@@ -0,0 +1,36 @@
/// Model-visible namespace metadata for a contributed tool bundle.
///
/// The namespace name participates in the callable tool identity, while the
/// optional description is host-visible metadata used when rendering namespace
/// specs for the model.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ToolNamespace {
name: String,
description: Option<String>,
}
impl ToolNamespace {
/// Creates namespace metadata with no explicit description.
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
description: None,
}
}
/// Sets the model-visible namespace description.
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
/// Returns the callable namespace name.
pub fn name(&self) -> &str {
self.name.as_str()
}
/// Returns the optional model-visible namespace description.
pub fn description(&self) -> Option<&str> {
self.description.as_deref()
}
}

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,
}

View File

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

View File

@@ -4,7 +4,6 @@
mod code_mode;
mod dynamic_tool;
mod image_detail;
mod json_schema;
mod mcp_tool;
mod request_plugin_install;
mod responses_api;
@@ -19,15 +18,15 @@ pub use code_mode::collect_code_mode_exec_prompt_tool_definitions;
pub use code_mode::collect_code_mode_tool_definitions;
pub use code_mode::tool_spec_to_code_mode_tool_definition;
pub use codex_protocol::ToolName;
pub use codex_tool_api::AdditionalProperties;
pub use codex_tool_api::JsonSchema;
pub use codex_tool_api::JsonSchemaPrimitiveType;
pub use codex_tool_api::JsonSchemaType;
pub use codex_tool_api::parse_tool_input_schema;
pub use dynamic_tool::parse_dynamic_tool;
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;
pub use json_schema::AdditionalProperties;
pub use json_schema::JsonSchema;
pub use json_schema::JsonSchemaPrimitiveType;
pub use json_schema::JsonSchemaType;
pub use json_schema::parse_tool_input_schema;
pub use mcp_tool::mcp_call_tool_result_output_schema;
pub use mcp_tool::parse_mcp_tool;
pub use request_plugin_install::REQUEST_PLUGIN_INSTALL_APPROVAL_KIND_VALUE;