feat: namespace in ext (#22556)

This commit is contained in:
jif-oai authored and GitHub committed 2026-05-14 00:37:48 +02:00
1 parent 23bb524973
commit e6939e3969
10 files changed
+128 -73

No files matched your search

-13
View File
@@ -18,12 +18,10 @@ use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput; use crate::tools::context::ToolOutput;
use crate::tools::context::ToolPayload; use crate::tools::context::ToolPayload;
use crate::tools::flat_tool_name; use crate::tools::flat_tool_name;
use crate::tools::handlers::extension_tools::ExtensionToolHandler;
use crate::tools::hook_names::HookToolName; use crate::tools::hook_names::HookToolName;
use crate::tools::tool_dispatch_trace::ToolDispatchTrace; use crate::tools::tool_dispatch_trace::ToolDispatchTrace;
use crate::tools::tool_search_entry::ToolSearchInfo; use crate::tools::tool_search_entry::ToolSearchInfo;
use crate::util::error_or_panic; use crate::util::error_or_panic;
use codex_extension_api::ExtensionToolExecutor;
use codex_protocol::models::ResponseInputItem; use codex_protocol::models::ResponseInputItem;
use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::EventMsg;
use codex_tools::ToolName; use codex_tools::ToolName;
@@ -671,17 +669,6 @@ impl ToolRegistryBuilder {
self.handlers.insert(name, handler); self.handlers.insert(name, handler);
} }
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 handler: Arc<dyn RegisteredTool> = Arc::new(ExtensionToolHandler::new(executor));
self.register_tool_internal(handler, /*include_spec*/ true);
}
pub fn build(self) -> (Vec<ToolSpec>, ToolRegistry) { pub fn build(self) -> (Vec<ToolSpec>, ToolRegistry) {
let registry = ToolRegistry::new(self.handlers); let registry = ToolRegistry::new(self.handlers);
(self.specs, registry) (self.specs, registry)
+32 -22
View File
@@ -15,9 +15,11 @@ use codex_protocol::dynamic_tools::DynamicToolSpec;
use codex_protocol::models::FunctionCallOutputBody; use codex_protocol::models::FunctionCallOutputBody;
use codex_protocol::models::ResponseInputItem; use codex_protocol::models::ResponseInputItem;
use codex_protocol::models::ResponseItem; use codex_protocol::models::ResponseItem;
use codex_tools::ResponsesApiNamespace;
use codex_tools::ResponsesApiNamespaceTool; use codex_tools::ResponsesApiNamespaceTool;
use codex_tools::ToolName; use codex_tools::ToolName;
use codex_tools::ToolSpec; use codex_tools::ToolSpec;
use codex_tools::default_namespace_description;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use serde_json::json; use serde_json::json;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
@@ -44,25 +46,29 @@ struct ExtensionEchoExecutor;
impl ExtensionToolExecutor for ExtensionEchoExecutor { impl ExtensionToolExecutor for ExtensionEchoExecutor {
fn tool_name(&self) -> ToolName { fn tool_name(&self) -> ToolName {
ToolName::plain("extension_echo") ToolName::namespaced("extension/", "echo")
} }
fn spec(&self) -> Option<ToolSpec> { fn spec(&self) -> Option<ToolSpec> {
Some(ToolSpec::Function(ResponsesApiTool { Some(ToolSpec::Namespace(ResponsesApiNamespace {
name: "extension_echo".to_string(), name: "extension/".to_string(),
description: "Echoes arguments through an extension tool.".to_string(), description: default_namespace_description("extension/"),
strict: true, tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool {
parameters: codex_extension_api::parse_tool_input_schema(&json!({ name: "echo".to_string(),
"type": "object", description: "Echoes arguments through an extension tool.".to_string(),
"properties": { strict: true,
"message": { "type": "string" }, parameters: codex_extension_api::parse_tool_input_schema(&json!({
}, "type": "object",
"required": ["message"], "properties": {
"additionalProperties": false, "message": { "type": "string" },
})) },
.expect("extension schema should parse"), "required": ["message"],
output_schema: None, "additionalProperties": false,
defer_loading: None, }))
.expect("extension schema should parse"),
output_schema: None,
defer_loading: None,
})],
})) }))
} }
@@ -333,17 +339,21 @@ async fn extension_tool_executors_are_model_visible_and_dispatchable() -> anyhow
); );
assert!( assert!(
router router.model_visible_specs().iter().any(
.model_visible_specs() |spec| matches!(spec, ToolSpec::Namespace(namespace)
.iter() if namespace.name == "extension/"
.any(|spec| spec.name() == "extension_echo"), && namespace.tools.iter().any(|tool| matches!(
tool,
ResponsesApiNamespaceTool::Function(tool) if tool.name == "echo"
)))
),
"expected extension-provided tool to be visible to the model" "expected extension-provided tool to be visible to the model"
); );
let call = ToolRouter::build_tool_call(ResponseItem::FunctionCall { let call = ToolRouter::build_tool_call(ResponseItem::FunctionCall {
id: None, id: None,
name: "extension_echo".to_string(), name: "echo".to_string(),
namespace: None, namespace: Some("extension/".to_string()),
arguments: json!({ "message": "hello" }).to_string(), arguments: json!({ "message": "hello" }).to_string(),
call_id: "call-extension".to_string(), call_id: "call-extension".to_string(),
})? })?
+45 -14
View File
@@ -24,6 +24,7 @@ use crate::tools::handlers::ViewImageHandler;
use crate::tools::handlers::WriteStdinHandler; use crate::tools::handlers::WriteStdinHandler;
use crate::tools::handlers::agent_jobs::ReportAgentJobResultHandler; use crate::tools::handlers::agent_jobs::ReportAgentJobResultHandler;
use crate::tools::handlers::agent_jobs::SpawnAgentsOnCsvHandler; use crate::tools::handlers::agent_jobs::SpawnAgentsOnCsvHandler;
use crate::tools::handlers::extension_tools::ExtensionToolHandler;
use crate::tools::handlers::multi_agents::CloseAgentHandler; use crate::tools::handlers::multi_agents::CloseAgentHandler;
use crate::tools::handlers::multi_agents::ResumeAgentHandler; use crate::tools::handlers::multi_agents::ResumeAgentHandler;
use crate::tools::handlers::multi_agents::SendInputHandler; use crate::tools::handlers::multi_agents::SendInputHandler;
@@ -49,13 +50,17 @@ use crate::tools::spec_plan_types::agent_type_description;
use codex_extension_api::ExtensionToolExecutor; use codex_extension_api::ExtensionToolExecutor;
use codex_protocol::openai_models::ConfigShellToolType; use codex_protocol::openai_models::ConfigShellToolType;
use codex_tools::ResponsesApiNamespaceTool; use codex_tools::ResponsesApiNamespaceTool;
use codex_tools::TOOL_SEARCH_TOOL_NAME;
use codex_tools::ToolEnvironmentMode; use codex_tools::ToolEnvironmentMode;
use codex_tools::ToolName;
use codex_tools::ToolSpec; use codex_tools::ToolSpec;
use codex_tools::ToolsConfig; use codex_tools::ToolsConfig;
use codex_tools::collect_code_mode_exec_prompt_tool_definitions; use codex_tools::collect_code_mode_exec_prompt_tool_definitions;
use codex_tools::default_namespace_description; use codex_tools::default_namespace_description;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::collections::HashSet;
use std::sync::Arc; use std::sync::Arc;
use tracing::warn;
pub fn build_tool_registry_builder( pub fn build_tool_registry_builder(
config: &ToolsConfig, config: &ToolsConfig,
@@ -70,7 +75,6 @@ pub fn build_tool_registry_builder(
for handler in build_code_mode_handlers( for handler in build_code_mode_handlers(
config, config,
&handlers, &handlers,
params.extension_tool_executors,
config.search_tool && deferred_tools_available, config.search_tool && deferred_tools_available,
) { ) {
builder.register_tool(handler); builder.register_tool(handler);
@@ -130,39 +134,28 @@ pub fn build_tool_registry_builder(
builder.register_handler(Arc::new(ToolSearchHandler::new(deferred_search_infos))); builder.register_handler(Arc::new(ToolSearchHandler::new(deferred_search_infos)));
} }
for executor in params.extension_tool_executors.iter().cloned() {
builder.register_extension_tool_executor(executor);
}
builder builder
} }
fn build_code_mode_handlers( fn build_code_mode_handlers(
config: &ToolsConfig, config: &ToolsConfig,
handlers: &[Arc<dyn RegisteredTool>], handlers: &[Arc<dyn RegisteredTool>],
extension_tool_executors: &[Arc<dyn ExtensionToolExecutor>],
deferred_tools_available: bool, deferred_tools_available: bool,
) -> Vec<Arc<dyn RegisteredTool>> { ) -> Vec<Arc<dyn RegisteredTool>> {
if !config.code_mode_enabled { if !config.code_mode_enabled {
return vec![]; return vec![];
} }
let mut code_mode_nested_tool_specs = handlers let code_mode_nested_tool_specs = handlers
.iter() .iter()
.filter_map(|handler| { .filter_map(|handler| {
if handler.exposure() == ToolExposure::DirectModelOnly { if handler.exposure() == ToolExposure::DirectModelOnly {
return None; return None;
} }
let spec = handler.spec()?; handler.spec()
Some(spec)
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
code_mode_nested_tool_specs.extend(
extension_tool_executors
.iter()
.filter_map(|executor| executor.spec()),
);
let namespace_descriptions = code_mode_namespace_descriptions(&code_mode_nested_tool_specs); let namespace_descriptions = code_mode_namespace_descriptions(&code_mode_nested_tool_specs);
let mut enabled_tools = let mut enabled_tools =
collect_code_mode_exec_prompt_tool_definitions(code_mode_nested_tool_specs.iter()); collect_code_mode_exec_prompt_tool_definitions(code_mode_nested_tool_specs.iter());
@@ -436,9 +429,47 @@ fn collect_handler_tools(
handlers.push(handler); handlers.push(handler);
} }
append_extension_tool_handlers(config, params.extension_tool_executors, &mut handlers);
handlers handlers
} }
fn append_extension_tool_handlers(
config: &ToolsConfig,
executors: &[Arc<dyn ExtensionToolExecutor>],
handlers: &mut Vec<Arc<dyn RegisteredTool>>,
) {
if executors.is_empty() {
return;
}
let mut reserved_tool_names = handlers
.iter()
.map(|handler| handler.tool_name())
.collect::<HashSet<_>>();
if config.code_mode_enabled {
reserved_tool_names.insert(ToolName::plain(codex_code_mode::PUBLIC_TOOL_NAME));
reserved_tool_names.insert(ToolName::plain(codex_code_mode::WAIT_TOOL_NAME));
}
if config.search_tool
&& config.namespace_tools
&& handlers
.iter()
.any(|handler| handler.exposure() == ToolExposure::Deferred)
{
reserved_tool_names.insert(ToolName::plain(TOOL_SEARCH_TOOL_NAME));
}
for executor in executors.iter().cloned() {
let tool_name = executor.tool_name();
if !reserved_tool_names.insert(tool_name.clone()) {
warn!("Skipping extension tool `{tool_name}`: handler already registered");
continue;
}
handlers.push(Arc::new(ExtensionToolHandler::new(executor)));
}
}
fn multi_agent_v2_handler( fn multi_agent_v2_handler(
handler: impl RegisteredTool + 'static, handler: impl RegisteredTool + 'static,
exposure: ToolExposure, exposure: ToolExposure,
+1 -1
View File
@@ -17,6 +17,7 @@ codex-core = { workspace = true }
codex-extension-api = { workspace = true } codex-extension-api = { workspace = true }
codex-features = { workspace = true } codex-features = { workspace = true }
codex-memories-read = { workspace = true } codex-memories-read = { workspace = true }
codex-tools = { workspace = true }
codex-utils-absolute-path = { workspace = true } codex-utils-absolute-path = { workspace = true }
codex-utils-output-truncation = { workspace = true } codex-utils-output-truncation = { workspace = true }
schemars = { workspace = true } schemars = { workspace = true }
@@ -26,7 +27,6 @@ thiserror = { workspace = true }
tokio = { workspace = true, features = ["fs"] } tokio = { workspace = true, features = ["fs"] }
[dev-dependencies] [dev-dependencies]
codex-tools = { workspace = true }
pretty_assertions = { workspace = true } pretty_assertions = { workspace = true }
tempfile = { workspace = true } tempfile = { workspace = true }
tokio = { workspace = true, features = ["fs", "macros"] } tokio = { workspace = true, features = ["fs", "macros"] }
+4 -3
View File
@@ -12,9 +12,10 @@ pub(crate) const DEFAULT_SEARCH_MAX_RESULTS: usize = 200;
pub(crate) const MAX_SEARCH_RESULTS: usize = 200; pub(crate) const MAX_SEARCH_RESULTS: usize = 200;
pub(crate) const DEFAULT_READ_MAX_TOKENS: usize = 20_000; pub(crate) const DEFAULT_READ_MAX_TOKENS: usize = 20_000;
pub(crate) const LIST_TOOL_NAME: &str = "memory_list"; pub(crate) const MEMORY_TOOLS_NAMESPACE: &str = "memories/";
pub(crate) const READ_TOOL_NAME: &str = "memory_read"; pub(crate) const LIST_TOOL_NAME: &str = "list";
pub(crate) const SEARCH_TOOL_NAME: &str = "memory_search"; pub(crate) const READ_TOOL_NAME: &str = "read";
pub(crate) const SEARCH_TOOL_NAME: &str = "search";
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
+14 -9
View File
@@ -62,15 +62,15 @@ fn tools_are_contributed_when_enabled() {
let tool_names = extension let tool_names = extension
.tools(&ExtensionData::new("session"), &thread_store) .tools(&ExtensionData::new("session"), &thread_store)
.into_iter() .into_iter()
.map(|tool| tool.tool_name().to_string()) .map(|tool| tool.tool_name())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
assert_eq!( assert_eq!(
tool_names, tool_names,
vec![ vec![
"memory_list".to_string(), memory_tool_name(crate::LIST_TOOL_NAME),
"memory_read".to_string(), memory_tool_name(crate::READ_TOOL_NAME),
"memory_search".to_string() memory_tool_name(crate::SEARCH_TOOL_NAME),
] ]
); );
} }
@@ -135,7 +135,7 @@ async fn read_tool_reads_memory_file() {
let output = tool let output = tool
.handle(ToolCall { .handle(ToolCall {
call_id: "call-1".to_string(), call_id: "call-1".to_string(),
tool_name: ToolName::plain(crate::READ_TOOL_NAME), tool_name: memory_tool_name(crate::READ_TOOL_NAME),
payload: payload.clone(), payload: payload.clone(),
}) })
.await .await
@@ -177,7 +177,7 @@ async fn search_tool_accepts_multiple_queries() {
let output = tool let output = tool
.handle(ToolCall { .handle(ToolCall {
call_id: "call-1".to_string(), call_id: "call-1".to_string(),
tool_name: ToolName::plain(crate::SEARCH_TOOL_NAME), tool_name: memory_tool_name(crate::SEARCH_TOOL_NAME),
payload: payload.clone(), payload: payload.clone(),
}) })
.await .await
@@ -245,7 +245,7 @@ async fn search_tool_accepts_windowed_all_match_mode() {
let output = tool let output = tool
.handle(ToolCall { .handle(ToolCall {
call_id: "call-1".to_string(), call_id: "call-1".to_string(),
tool_name: ToolName::plain(crate::SEARCH_TOOL_NAME), tool_name: memory_tool_name(crate::SEARCH_TOOL_NAME),
payload: payload.clone(), payload: payload.clone(),
}) })
.await .await
@@ -293,7 +293,7 @@ async fn search_tool_rejects_legacy_single_query() {
let err = tool let err = tool
.handle(ToolCall { .handle(ToolCall {
call_id: "call-1".to_string(), call_id: "call-1".to_string(),
tool_name: ToolName::plain(crate::SEARCH_TOOL_NAME), tool_name: memory_tool_name(crate::SEARCH_TOOL_NAME),
payload, payload,
}) })
.await .await
@@ -304,8 +304,13 @@ async fn search_tool_rejects_legacy_single_query() {
} }
fn memory_tool(memory_root: &Path, tool_name: &str) -> Arc<dyn ExtensionToolExecutor> { fn memory_tool(memory_root: &Path, tool_name: &str) -> Arc<dyn ExtensionToolExecutor> {
let expected_tool_name = memory_tool_name(tool_name);
crate::tools::memory_tools(LocalMemoriesBackend::from_memory_root(memory_root)) crate::tools::memory_tools(LocalMemoriesBackend::from_memory_root(memory_root))
.into_iter() .into_iter()
.find(|tool| tool.tool_name().to_string() == tool_name) .find(|tool| tool.tool_name() == expected_tool_name)
.unwrap_or_else(|| panic!("{tool_name} tool should be registered")) .unwrap_or_else(|| panic!("{tool_name} tool should be registered"))
} }
fn memory_tool_name(tool_name: &str) -> ToolName {
ToolName::namespaced(crate::MEMORY_TOOLS_NAMESPACE, tool_name)
}
+4 -3
View File
@@ -17,7 +17,8 @@ use crate::backend::MemoriesBackend;
use super::backend_error_to_function_call; use super::backend_error_to_function_call;
use super::clamp_max_results; use super::clamp_max_results;
use super::function_tool; use super::memory_function_tool;
use super::memory_tool_name;
use super::parse_args; use super::parse_args;
#[derive(Deserialize, JsonSchema)] #[derive(Deserialize, JsonSchema)]
@@ -39,11 +40,11 @@ where
B: MemoriesBackend, B: MemoriesBackend,
{ {
fn tool_name(&self) -> ToolName { fn tool_name(&self) -> ToolName {
ToolName::plain(LIST_TOOL_NAME) memory_tool_name(LIST_TOOL_NAME)
} }
fn spec(&self) -> Option<ToolSpec> { fn spec(&self) -> Option<ToolSpec> {
Some(function_tool::<ListArgs, ListMemoriesResponse>( Some(memory_function_tool::<ListArgs, ListMemoriesResponse>(
LIST_TOOL_NAME, LIST_TOOL_NAME,
"List immediate files and directories under a path in the Codex memories store.", "List immediate files and directories under a path in the Codex memories store.",
)) ))
+20 -2
View File
@@ -4,12 +4,17 @@ use codex_extension_api::ExtensionToolExecutor;
use codex_extension_api::FunctionCallError; use codex_extension_api::FunctionCallError;
use codex_extension_api::ResponsesApiTool; use codex_extension_api::ResponsesApiTool;
use codex_extension_api::ToolCall; use codex_extension_api::ToolCall;
use codex_extension_api::ToolName;
use codex_extension_api::ToolSpec; use codex_extension_api::ToolSpec;
use codex_extension_api::parse_tool_input_schema; use codex_extension_api::parse_tool_input_schema;
use codex_tools::ResponsesApiNamespace;
use codex_tools::ResponsesApiNamespaceTool;
use codex_tools::default_namespace_description;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::Deserialize; use serde::Deserialize;
use serde_json::Value; use serde_json::Value;
use crate::MEMORY_TOOLS_NAMESPACE;
use crate::backend::MemoriesBackend; use crate::backend::MemoriesBackend;
use crate::backend::MemoriesBackendError; use crate::backend::MemoriesBackendError;
use crate::schema; use crate::schema;
@@ -33,8 +38,15 @@ where
] ]
} }
fn function_tool<I: JsonSchema, O: JsonSchema>(name: &str, description: &str) -> ToolSpec { pub(super) fn memory_tool_name(name: &str) -> ToolName {
ToolSpec::Function(ResponsesApiTool { ToolName::namespaced(MEMORY_TOOLS_NAMESPACE, name)
}
pub(super) fn memory_function_tool<I: JsonSchema, O: JsonSchema>(
name: &str,
description: &str,
) -> ToolSpec {
let tool = ResponsesApiTool {
name: name.to_string(), name: name.to_string(),
description: description.to_string(), description: description.to_string(),
strict: false, strict: false,
@@ -42,6 +54,12 @@ fn function_tool<I: JsonSchema, O: JsonSchema>(name: &str, description: &str) ->
parameters: parse_tool_input_schema(&schema::input_schema_for::<I>()) parameters: parse_tool_input_schema(&schema::input_schema_for::<I>())
.unwrap_or_else(|err| panic!("generated input schema for {name} should parse: {err}")), .unwrap_or_else(|err| panic!("generated input schema for {name} should parse: {err}")),
output_schema: Some(schema::output_schema_for::<O>()), output_schema: Some(schema::output_schema_for::<O>()),
};
ToolSpec::Namespace(ResponsesApiNamespace {
name: MEMORY_TOOLS_NAMESPACE.to_string(),
description: default_namespace_description(MEMORY_TOOLS_NAMESPACE),
tools: vec![ResponsesApiNamespaceTool::Function(tool)],
}) })
} }
+4 -3
View File
@@ -15,7 +15,8 @@ use crate::backend::ReadMemoryRequest;
use crate::backend::ReadMemoryResponse; use crate::backend::ReadMemoryResponse;
use super::backend_error_to_function_call; use super::backend_error_to_function_call;
use super::function_tool; use super::memory_function_tool;
use super::memory_tool_name;
use super::parse_args; use super::parse_args;
#[derive(Deserialize, JsonSchema)] #[derive(Deserialize, JsonSchema)]
@@ -38,11 +39,11 @@ where
B: MemoriesBackend, B: MemoriesBackend,
{ {
fn tool_name(&self) -> ToolName { fn tool_name(&self) -> ToolName {
ToolName::plain(READ_TOOL_NAME) memory_tool_name(READ_TOOL_NAME)
} }
fn spec(&self) -> Option<ToolSpec> { fn spec(&self) -> Option<ToolSpec> {
Some(function_tool::<ReadArgs, ReadMemoryResponse>( Some(memory_function_tool::<ReadArgs, ReadMemoryResponse>(
READ_TOOL_NAME, READ_TOOL_NAME,
"Read a Codex memory file by relative path, optionally starting at a 1-indexed line offset and limiting the number of lines returned.", "Read a Codex memory file by relative path, optionally starting at a 1-indexed line offset and limiting the number of lines returned.",
)) ))
+4 -3
View File
@@ -18,7 +18,8 @@ use crate::backend::SearchMemoriesResponse;
use super::backend_error_to_function_call; use super::backend_error_to_function_call;
use super::clamp_max_results; use super::clamp_max_results;
use super::function_tool; use super::memory_function_tool;
use super::memory_tool_name;
use super::parse_args; use super::parse_args;
#[derive(Debug, Deserialize, JsonSchema)] #[derive(Debug, Deserialize, JsonSchema)]
@@ -47,11 +48,11 @@ where
B: MemoriesBackend, B: MemoriesBackend,
{ {
fn tool_name(&self) -> ToolName { fn tool_name(&self) -> ToolName {
ToolName::plain(SEARCH_TOOL_NAME) memory_tool_name(SEARCH_TOOL_NAME)
} }
fn spec(&self) -> Option<ToolSpec> { fn spec(&self) -> Option<ToolSpec> {
Some(function_tool::<SearchArgs, SearchMemoriesResponse>( Some(memory_function_tool::<SearchArgs, SearchMemoriesResponse>(
SEARCH_TOOL_NAME, SEARCH_TOOL_NAME,
"Search Codex memory files for substring matches, optionally normalizing separators or requiring all query substrings on the same line or within a line window.", "Search Codex memory files for substring matches, optionally normalizing separators or requiring all query substrings on the same line or within a line window.",
)) ))