mirror of
https://github.com/openai/codex.git
synced 2026-05-21 19:45:26 +00:00
Compare commits
2 Commits
rhan/compa
...
jif/keep-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3121e5745e | ||
|
|
4e244b254b |
3
codex-rs/Cargo.lock
generated
3
codex-rs/Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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.")
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
36
codex-rs/tool-api/src/namespace.rs
Normal file
36
codex-rs/tool-api/src/namespace.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user