mirror of
https://github.com/openai/codex.git
synced 2026-10-27 08:01:52 +00:00
Support custom tools in namespaces (#36857)
## What changed - Allow namespace tool specs to contain custom freeform tools alongside function tools. - Include namespaced custom tools in deferred tool search and expose them to code mode with names such as `editor__apply_patch`. - Route custom payloads to matching extension tools while preserving function-only payload validation. ## Testing - Add serialization, tool search, code-mode definition, and end-to-end dispatch coverage for namespaced custom tools. GitOrigin-RevId: be64d35f6ae54685c5a9fcf45a732320742ea7e5
This commit is contained in:
@@ -6,6 +6,7 @@ use codex_protocol::protocol::Event;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_tools::ConversationHistory;
|
||||
use codex_tools::ExtensionTurnItem;
|
||||
use codex_tools::ResponsesApiNamespaceTool;
|
||||
use codex_tools::ToolCall as ExtensionToolCall;
|
||||
use codex_tools::ToolEnvironment;
|
||||
use codex_tools::ToolName;
|
||||
@@ -61,7 +62,23 @@ impl ToolExecutor<ToolInvocation> for ExtensionToolAdapter {
|
||||
|
||||
impl CoreToolRuntime for ExtensionToolAdapter {
|
||||
fn matches_kind(&self, payload: &ToolPayload) -> bool {
|
||||
matches!(payload, ToolPayload::Function { .. })
|
||||
match payload {
|
||||
ToolPayload::Function { .. } => true,
|
||||
ToolPayload::Custom { .. } => match self.0.spec() {
|
||||
ToolSpec::Freeform(_) => true,
|
||||
ToolSpec::Namespace(namespace) => namespace.tools.iter().any(|tool| {
|
||||
matches!(
|
||||
tool,
|
||||
ResponsesApiNamespaceTool::Custom(tool)
|
||||
if tool.name == self.0.tool_name().name
|
||||
)
|
||||
}),
|
||||
ToolSpec::Function(_)
|
||||
| ToolSpec::ToolSearch { .. }
|
||||
| ToolSpec::WebSearch { .. } => false,
|
||||
},
|
||||
ToolPayload::ToolSearch { .. } => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,6 +303,18 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_extensions_reject_custom_payloads() {
|
||||
let handler = ExtensionToolAdapter::new(Arc::new(StubExtensionExecutor));
|
||||
|
||||
assert!(handler.matches_kind(&ToolPayload::Function {
|
||||
arguments: "{}".to_string(),
|
||||
}));
|
||||
assert!(!handler.matches_kind(&ToolPayload::Custom {
|
||||
input: "raw input".to_string(),
|
||||
}));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn exposes_generic_hook_payloads() {
|
||||
let handler = ExtensionToolAdapter::new(Arc::new(StubExtensionExecutor));
|
||||
|
||||
@@ -582,6 +582,7 @@ fn namespace_function_names(specs: &[ToolSpec], namespace_name: &str) -> Vec<Str
|
||||
.iter()
|
||||
.map(|tool| match tool {
|
||||
ResponsesApiNamespaceTool::Function(tool) => tool.name.clone(),
|
||||
ResponsesApiNamespaceTool::Custom(tool) => tool.name.clone(),
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
|
||||
@@ -713,11 +713,16 @@ fn merge_into_namespaces(specs: Vec<ToolSpec>) -> Vec<ToolSpec> {
|
||||
continue;
|
||||
};
|
||||
|
||||
namespace.tools.sort_by(|left, right| match (left, right) {
|
||||
(
|
||||
ResponsesApiNamespaceTool::Function(left),
|
||||
ResponsesApiNamespaceTool::Function(right),
|
||||
) => left.name.cmp(&right.name),
|
||||
namespace.tools.sort_by(|left, right| {
|
||||
let left_name = match left {
|
||||
ResponsesApiNamespaceTool::Function(tool) => &tool.name,
|
||||
ResponsesApiNamespaceTool::Custom(tool) => &tool.name,
|
||||
};
|
||||
let right_name = match right {
|
||||
ResponsesApiNamespaceTool::Function(tool) => &tool.name,
|
||||
ResponsesApiNamespaceTool::Custom(tool) => &tool.name,
|
||||
};
|
||||
left_name.cmp(right_name)
|
||||
});
|
||||
|
||||
if namespace.description.trim().is_empty() {
|
||||
|
||||
@@ -84,6 +84,7 @@ impl ToolPlanProbe {
|
||||
.iter()
|
||||
.map(|tool| match tool {
|
||||
ResponsesApiNamespaceTool::Function(tool) => tool.name.clone(),
|
||||
ResponsesApiNamespaceTool::Custom(tool) => tool.name.clone(),
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)),
|
||||
@@ -1666,7 +1667,9 @@ async fn code_mode_only_exposes_configured_dynamic_namespace_directly() {
|
||||
let ToolSpec::Namespace(namespace) = plan.visible_spec("direct_only") else {
|
||||
panic!("expected direct-only namespace spec");
|
||||
};
|
||||
let ResponsesApiNamespaceTool::Function(tool) = &namespace.tools[0];
|
||||
let ResponsesApiNamespaceTool::Function(tool) = &namespace.tools[0] else {
|
||||
panic!("expected direct-only namespace function tool");
|
||||
};
|
||||
assert_eq!(tool.defer_loading, None);
|
||||
let ToolSpec::Freeform(exec) = plan.visible_spec(codex_code_mode::PUBLIC_TOOL_NAME) else {
|
||||
panic!("expected code mode exec tool");
|
||||
|
||||
Reference in New Issue
Block a user