mirror of
https://github.com/openai/codex.git
synced 2026-11-05 20:10:33 +00:00
Canonicalize default tool namespaces
This commit is contained in:
@@ -84,6 +84,7 @@ use codex_rollout_trace::CompactionTraceContext;
|
||||
use codex_rollout_trace::InferenceTraceAttempt;
|
||||
use codex_rollout_trace::InferenceTraceContext;
|
||||
use codex_tools::create_tools_json_for_responses_api;
|
||||
use codex_tools::create_tools_json_for_responses_lite;
|
||||
use codex_utils_output_truncation::approx_tokens_from_byte_count;
|
||||
use eventsource_stream::Event;
|
||||
use eventsource_stream::EventStreamError;
|
||||
@@ -785,7 +786,11 @@ impl ModelClient {
|
||||
if !self.state.provider.info().is_openai() {
|
||||
input.iter_mut().for_each(ResponseItem::clear_metadata);
|
||||
}
|
||||
let tools = create_tools_json_for_responses_api(&prompt.tools)?;
|
||||
let tools = if model_info.use_responses_lite {
|
||||
create_tools_json_for_responses_lite(&prompt.tools)?
|
||||
} else {
|
||||
create_tools_json_for_responses_api(&prompt.tools)?
|
||||
};
|
||||
let (instructions, tools) = if model_info.use_responses_lite {
|
||||
let additional_tools = ResponseItem::AdditionalTools {
|
||||
id: None,
|
||||
|
||||
@@ -31,16 +31,17 @@ fn shell_script_for_invocation(invocation: &ToolInvocation) -> Option<String> {
|
||||
return None;
|
||||
};
|
||||
|
||||
match (
|
||||
invocation.tool_name.namespace.as_deref(),
|
||||
invocation.tool_name.name.as_str(),
|
||||
) {
|
||||
(None, "shell_command") => serde_json::from_str::<ShellCommandToolCallParams>(arguments)
|
||||
if !invocation.tool_name.is_default_namespace() {
|
||||
return None;
|
||||
}
|
||||
|
||||
match invocation.tool_name.name.as_str() {
|
||||
"shell_command" => serde_json::from_str::<ShellCommandToolCallParams>(arguments)
|
||||
.ok()
|
||||
.map(|params| params.command),
|
||||
(None, "exec_command") => serde_json::from_str::<ExecCommandArgs>(arguments)
|
||||
"exec_command" => serde_json::from_str::<ExecCommandArgs>(arguments)
|
||||
.ok()
|
||||
.map(|params| params.cmd),
|
||||
(Some(_), _) | (None, _) => None,
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2113,7 +2113,7 @@ async fn try_run_sampling_request(
|
||||
}
|
||||
ResponseEvent::OutputItemAdded(item) => {
|
||||
if let ResponseItem::CustomToolCall { call_id, name, .. } = &item {
|
||||
let tool_name = ToolName::plain(name.as_str());
|
||||
let tool_name = ToolName::function(name.as_str());
|
||||
active_tool_argument_diff_consumer = tool_runtime
|
||||
.create_diff_consumer(&tool_name)
|
||||
.map(|consumer| (call_id.clone(), consumer));
|
||||
|
||||
@@ -47,9 +47,9 @@ pub(crate) const PUBLIC_TOOL_NAME: &str = codex_code_mode::PUBLIC_TOOL_NAME;
|
||||
pub(crate) const WAIT_TOOL_NAME: &str = codex_code_mode::WAIT_TOOL_NAME;
|
||||
pub(crate) const DEFAULT_WAIT_YIELD_TIME_MS: u64 = codex_code_mode::DEFAULT_WAIT_YIELD_TIME_MS;
|
||||
|
||||
/// Returns true for the un-namespaced code-mode `exec` tool.
|
||||
/// Returns true for the code-mode `exec` tool in the default namespace.
|
||||
pub(crate) fn is_exec_tool_name(tool_name: &ToolName) -> bool {
|
||||
tool_name.namespace.is_none() && tool_name.name == PUBLIC_TOOL_NAME
|
||||
tool_name.is_default_namespace() && tool_name.name == PUBLIC_TOOL_NAME
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
||||
@@ -73,7 +73,8 @@ impl CodeModeWaitHandler {
|
||||
|
||||
match payload {
|
||||
ToolPayload::Function { arguments }
|
||||
if tool_name.namespace.is_none() && tool_name.name.as_str() == WAIT_TOOL_NAME =>
|
||||
if tool_name.is_default_namespace()
|
||||
&& tool_name.name.as_str() == WAIT_TOOL_NAME =>
|
||||
{
|
||||
let args: ExecWaitArgs = parse_arguments(&arguments)?;
|
||||
let exec = ExecContext { session, turn };
|
||||
|
||||
@@ -37,6 +37,9 @@ pub(crate) const TELEMETRY_PREVIEW_TRUNCATION_NOTICE: &str =
|
||||
/// names still require a single flattened string. Keep comparisons and sorting
|
||||
/// on `ToolName` itself; use this only when crossing those boundaries.
|
||||
pub(crate) fn flat_tool_name(tool_name: &ToolName) -> Cow<'_, str> {
|
||||
if tool_name.is_default_namespace() {
|
||||
return Cow::Borrowed(tool_name.name.as_str());
|
||||
}
|
||||
match tool_name.namespace.as_deref() {
|
||||
Some(namespace) => {
|
||||
let mut name = String::with_capacity(namespace.len() + tool_name.name.len());
|
||||
|
||||
@@ -222,7 +222,7 @@ impl ToolCallRuntime {
|
||||
}
|
||||
|
||||
fn abort_message(call: &ToolCall, secs: f32) -> String {
|
||||
if call.tool_name.namespace.is_none()
|
||||
if call.tool_name.is_default_namespace()
|
||||
&& matches!(
|
||||
call.tool_name.name.as_str(),
|
||||
"shell_command" | "unified_exec"
|
||||
|
||||
@@ -325,6 +325,10 @@ pub struct ToolRegistry {
|
||||
|
||||
impl ToolRegistry {
|
||||
fn new(tools: HashMap<ToolName, Arc<dyn CoreToolRuntime>>) -> Self {
|
||||
let tools = tools
|
||||
.into_iter()
|
||||
.map(|(name, tool)| (name.with_default_namespace(), tool))
|
||||
.collect();
|
||||
Self { tools }
|
||||
}
|
||||
|
||||
@@ -332,7 +336,7 @@ impl ToolRegistry {
|
||||
pub(crate) fn from_tools(tools: impl IntoIterator<Item = Arc<dyn CoreToolRuntime>>) -> Self {
|
||||
let mut tools_by_name = HashMap::new();
|
||||
for tool in tools {
|
||||
let name = tool.tool_name();
|
||||
let name = tool.tool_name().with_default_namespace();
|
||||
if tools_by_name.contains_key(&name) {
|
||||
error_or_panic(format!("tool {name} already registered"));
|
||||
continue;
|
||||
@@ -357,7 +361,9 @@ impl ToolRegistry {
|
||||
}
|
||||
|
||||
fn tool(&self, name: &ToolName) -> Option<Arc<dyn CoreToolRuntime>> {
|
||||
self.tools.get(name).map(Arc::clone)
|
||||
self.tools
|
||||
.get(&name.clone().with_default_namespace())
|
||||
.map(Arc::clone)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -369,7 +375,7 @@ impl ToolRegistry {
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn tool_exposure(&self, name: &ToolName) -> Option<ToolExposure> {
|
||||
self.tools.get(name).map(|tool| tool.exposure())
|
||||
self.tool(name).map(|tool| tool.exposure())
|
||||
}
|
||||
|
||||
pub(crate) fn create_diff_consumer(
|
||||
@@ -712,10 +718,8 @@ async fn handle_any_tool(
|
||||
|
||||
fn function_hook_tool_name(invocation: &ToolInvocation) -> HookToolName {
|
||||
if invocation.tool_name.name == "spawn_agent"
|
||||
&& matches!(
|
||||
invocation.tool_name.namespace.as_deref(),
|
||||
None | Some(MULTI_AGENT_V1_NAMESPACE)
|
||||
)
|
||||
&& (invocation.tool_name.is_default_namespace()
|
||||
|| invocation.tool_name.namespace.as_deref() == Some(MULTI_AGENT_V1_NAMESPACE))
|
||||
{
|
||||
return HookToolName::spawn_agent();
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ impl codex_extension_api::ToolLifecycleContributor for ToolLifecycleRecorder {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handler_looks_up_namespaced_aliases_explicitly() {
|
||||
fn handler_normalizes_only_the_default_namespace() {
|
||||
let namespace = "mcp__codex_apps__gmail";
|
||||
let tool_name = "gmail_get_recent_emails";
|
||||
let plain_name = codex_tools::ToolName::plain(tool_name);
|
||||
@@ -157,6 +157,7 @@ fn handler_looks_up_namespaced_aliases_explicitly() {
|
||||
]));
|
||||
|
||||
let plain = registry.tool(&plain_name);
|
||||
let default_namespaced = registry.tool(&codex_tools::ToolName::function(tool_name));
|
||||
let namespaced = registry.tool(&namespaced_name);
|
||||
let missing_namespaced = registry.tool(&codex_tools::ToolName::namespaced(
|
||||
"mcp__codex_apps__calendar",
|
||||
@@ -164,6 +165,7 @@ fn handler_looks_up_namespaced_aliases_explicitly() {
|
||||
));
|
||||
|
||||
assert_eq!(plain.is_some(), true);
|
||||
assert_eq!(default_namespaced.is_some(), true);
|
||||
assert_eq!(namespaced.is_some(), true);
|
||||
assert_eq!(missing_namespaced.is_none(), true);
|
||||
assert!(
|
||||
@@ -171,6 +173,11 @@ fn handler_looks_up_namespaced_aliases_explicitly() {
|
||||
.as_ref()
|
||||
.is_some_and(|handler| Arc::ptr_eq(handler, &plain_handler))
|
||||
);
|
||||
assert!(
|
||||
default_namespaced
|
||||
.as_ref()
|
||||
.is_some_and(|handler| Arc::ptr_eq(handler, &plain_handler))
|
||||
);
|
||||
assert!(
|
||||
namespaced
|
||||
.as_ref()
|
||||
|
||||
@@ -120,7 +120,7 @@ impl ToolRouter {
|
||||
call_id,
|
||||
..
|
||||
} => {
|
||||
let tool_name = ToolName::new(namespace, name);
|
||||
let tool_name = ToolName::new(namespace, name).with_default_namespace();
|
||||
Ok(Some(ToolCall {
|
||||
tool_name,
|
||||
call_id,
|
||||
@@ -152,7 +152,7 @@ impl ToolRouter {
|
||||
call_id,
|
||||
..
|
||||
} => Ok(Some(ToolCall {
|
||||
tool_name: ToolName::plain(name),
|
||||
tool_name: ToolName::function(name),
|
||||
call_id,
|
||||
payload: ToolPayload::Custom { input },
|
||||
})),
|
||||
|
||||
@@ -18,6 +18,7 @@ use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::FunctionCallOutputBody;
|
||||
use codex_protocol::models::ResponseInputItem;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_tools::DEFAULT_FUNCTION_NAMESPACE;
|
||||
use codex_tools::ResponsesApiNamespace;
|
||||
use codex_tools::ResponsesApiNamespaceTool;
|
||||
use codex_tools::ToolName;
|
||||
@@ -176,6 +177,39 @@ async fn build_tool_call_uses_namespace_for_registry_name() -> anyhow::Result<()
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_tool_call_normalizes_default_function_and_custom_namespaces() -> anyhow::Result<()> {
|
||||
for namespace in [None, Some(""), Some(DEFAULT_FUNCTION_NAMESPACE)] {
|
||||
let function_call = ToolRouter::build_tool_call(ResponseItem::FunctionCall {
|
||||
id: None,
|
||||
name: "lookup".to_string(),
|
||||
namespace: namespace.map(str::to_string),
|
||||
arguments: "{}".to_string(),
|
||||
call_id: "call-function".to_string(),
|
||||
})?
|
||||
.expect("function_call should produce a tool call");
|
||||
assert_eq!(
|
||||
function_call.tool_name,
|
||||
ToolName::namespaced(DEFAULT_FUNCTION_NAMESPACE, "lookup")
|
||||
);
|
||||
}
|
||||
|
||||
let custom_call = ToolRouter::build_tool_call(ResponseItem::CustomToolCall {
|
||||
id: None,
|
||||
status: None,
|
||||
call_id: "call-custom".to_string(),
|
||||
name: "apply_patch".to_string(),
|
||||
input: "*** Begin Patch\n*** End Patch".to_string(),
|
||||
})?
|
||||
.expect("custom_tool_call should produce a tool call");
|
||||
assert_eq!(
|
||||
custom_call.tool_name,
|
||||
ToolName::namespaced(DEFAULT_FUNCTION_NAMESPACE, "apply_patch")
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_tool_call_uses_canonical_tool_search_name() -> anyhow::Result<()> {
|
||||
let call = ToolRouter::build_tool_call(ResponseItem::ToolSearchCall {
|
||||
@@ -443,9 +477,7 @@ fn namespace_function_names(specs: &[ToolSpec], namespace_name: &str) -> Vec<Str
|
||||
namespace
|
||||
.tools
|
||||
.iter()
|
||||
.map(|tool| match tool {
|
||||
ResponsesApiNamespaceTool::Function(tool) => tool.name.clone(),
|
||||
})
|
||||
.map(|tool| tool.name().to_string())
|
||||
.collect(),
|
||||
),
|
||||
ToolSpec::Function(_)
|
||||
|
||||
@@ -241,7 +241,7 @@ fn build_model_visible_specs_and_registry(
|
||||
let mut specs = Vec::new();
|
||||
let mut seen_tool_names = HashSet::new();
|
||||
for runtime in &runtimes {
|
||||
let tool_name = runtime.tool_name();
|
||||
let tool_name = runtime.tool_name().with_default_namespace();
|
||||
if !seen_tool_names.insert(tool_name.clone()) {
|
||||
continue;
|
||||
}
|
||||
@@ -464,6 +464,9 @@ fn is_hidden_by_code_mode_only(
|
||||
}
|
||||
|
||||
fn is_excluded_from_code_mode(turn_context: &TurnContext, tool_name: &ToolName) -> bool {
|
||||
if tool_name.is_default_namespace() {
|
||||
return false;
|
||||
}
|
||||
tool_name.namespace.as_ref().is_some_and(|namespace| {
|
||||
turn_context
|
||||
.config
|
||||
@@ -565,12 +568,9 @@ 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| left.name().cmp(right.name()));
|
||||
|
||||
if namespace.description.trim().is_empty() {
|
||||
namespace.description = default_namespace_description(&namespace.name);
|
||||
@@ -985,12 +985,12 @@ fn append_extension_tool_executors(
|
||||
let mut reserved_tool_names = planned_tools
|
||||
.runtimes()
|
||||
.iter()
|
||||
.map(|executor| executor.tool_name())
|
||||
.map(|executor| executor.tool_name().with_default_namespace())
|
||||
.collect::<HashSet<_>>();
|
||||
let tool_mode = effective_tool_mode(turn_context);
|
||||
if matches!(tool_mode, ToolMode::CodeMode | ToolMode::CodeModeOnly) {
|
||||
reserved_tool_names.insert(ToolName::plain(codex_code_mode::PUBLIC_TOOL_NAME));
|
||||
reserved_tool_names.insert(ToolName::plain(codex_code_mode::WAIT_TOOL_NAME));
|
||||
reserved_tool_names.insert(ToolName::function(codex_code_mode::PUBLIC_TOOL_NAME));
|
||||
reserved_tool_names.insert(ToolName::function(codex_code_mode::WAIT_TOOL_NAME));
|
||||
}
|
||||
if search_tool_enabled(turn_context)
|
||||
&& namespace_tools_enabled(turn_context)
|
||||
@@ -999,7 +999,7 @@ fn append_extension_tool_executors(
|
||||
.iter()
|
||||
.any(|executor| executor.exposure() == ToolExposure::Deferred)
|
||||
{
|
||||
reserved_tool_names.insert(ToolName::plain(TOOL_SEARCH_TOOL_NAME));
|
||||
reserved_tool_names.insert(ToolName::function(TOOL_SEARCH_TOOL_NAME));
|
||||
reserved_tool_names.insert(tool_search_tool_name());
|
||||
}
|
||||
|
||||
@@ -1007,7 +1007,7 @@ fn append_extension_tool_executors(
|
||||
let web_search_mode_on = turn_context.config.web_search_mode.value() != WebSearchMode::Disabled;
|
||||
|
||||
for executor in executors.iter().cloned() {
|
||||
let tool_name = executor.tool_name();
|
||||
let tool_name = executor.tool_name().with_default_namespace();
|
||||
if tool_name == ToolName::namespaced("web", "run")
|
||||
&& (!standalone_web_search_enabled || !web_search_mode_on)
|
||||
{
|
||||
|
||||
@@ -72,9 +72,7 @@ impl ToolPlanProbe {
|
||||
namespace
|
||||
.tools
|
||||
.iter()
|
||||
.map(|tool| match tool {
|
||||
ResponsesApiNamespaceTool::Function(tool) => tool.name.clone(),
|
||||
})
|
||||
.map(|tool| tool.name().to_string())
|
||||
.collect::<Vec<_>>(),
|
||||
)),
|
||||
ToolSpec::Function(_)
|
||||
|
||||
@@ -120,6 +120,13 @@ async fn responses_lite_uses_input_items_for_instructions_and_tools() -> Result<
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn functions_namespace(tools: &[Value]) -> Option<&Value> {
|
||||
tools.iter().find(|tool| {
|
||||
tool.get("type").and_then(Value::as_str) == Some("namespace")
|
||||
&& tool.get("name").and_then(Value::as_str) == Some("functions")
|
||||
})
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn responses_lite_rejects_oversized_additional_tools_item() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
@@ -272,6 +279,17 @@ async fn responses_lite_uses_standalone_web_search_and_image_generation() -> Res
|
||||
assert!(body.get("tools").is_none());
|
||||
let tools = additional_tools(&body)?;
|
||||
assert!(!tools.is_empty());
|
||||
let function_tools = functions_namespace(tools)
|
||||
.and_then(|namespace| namespace.get("tools"))
|
||||
.and_then(Value::as_array)
|
||||
.context("Responses Lite should group default tools under functions")?;
|
||||
assert!(!function_tools.is_empty());
|
||||
assert!(tools.iter().all(|tool| {
|
||||
!matches!(
|
||||
tool.get("type").and_then(Value::as_str),
|
||||
Some("function" | "custom")
|
||||
)
|
||||
}));
|
||||
assert!(!has_hosted_tool(tools, "web_search"));
|
||||
assert!(!has_hosted_tool(tools, "image_generation"));
|
||||
|
||||
|
||||
@@ -896,7 +896,7 @@ async fn tool_search_returns_deferred_dynamic_tool_and_routes_follow_up_call() -
|
||||
"item": {
|
||||
"type": "function_call",
|
||||
"call_id": dynamic_call_id,
|
||||
"namespace": "codex_app",
|
||||
"namespace": "functions",
|
||||
"name": tool_name,
|
||||
"arguments": tool_call_arguments,
|
||||
}
|
||||
@@ -920,17 +920,11 @@ async fn tool_search_returns_deferred_dynamic_tool_and_routes_follow_up_call() -
|
||||
"required": ["mode"],
|
||||
"additionalProperties": false,
|
||||
});
|
||||
let dynamic_tool = DynamicToolSpec::Namespace(DynamicToolNamespaceSpec {
|
||||
name: "codex_app".to_string(),
|
||||
description: "Automation tools.".to_string(),
|
||||
tools: vec![DynamicToolNamespaceTool::Function(
|
||||
DynamicToolFunctionSpec {
|
||||
name: tool_name.to_string(),
|
||||
description: tool_description.to_string(),
|
||||
input_schema: input_schema.clone(),
|
||||
defer_loading: true,
|
||||
},
|
||||
)],
|
||||
let dynamic_tool = DynamicToolSpec::Function(DynamicToolFunctionSpec {
|
||||
name: tool_name.to_string(),
|
||||
description: tool_description.to_string(),
|
||||
input_schema: input_schema.clone(),
|
||||
defer_loading: true,
|
||||
});
|
||||
|
||||
let mut builder = test_codex().with_config(configure_search_capable_model);
|
||||
@@ -964,7 +958,7 @@ async fn tool_search_returns_deferred_dynamic_tool_and_routes_follow_up_call() -
|
||||
unreachable!("event guard guarantees DynamicToolCallRequest");
|
||||
};
|
||||
assert_eq!(request.call_id, dynamic_call_id);
|
||||
assert_eq!(request.namespace.as_deref(), Some("codex_app"));
|
||||
assert_eq!(request.namespace, None);
|
||||
assert_eq!(request.tool, tool_name);
|
||||
assert_eq!(request.arguments, tool_args);
|
||||
|
||||
@@ -1006,8 +1000,8 @@ async fn tool_search_returns_deferred_dynamic_tool_and_routes_follow_up_call() -
|
||||
tools,
|
||||
vec![json!({
|
||||
"type": "namespace",
|
||||
"name": "codex_app",
|
||||
"description": "Automation tools.",
|
||||
"name": "functions",
|
||||
"description": "Tools in the functions namespace.",
|
||||
"tools": [{
|
||||
"type": "function",
|
||||
"name": tool_name,
|
||||
|
||||
@@ -19,6 +19,7 @@ use core_test_support::responses::ev_apply_patch_custom_tool_call;
|
||||
use core_test_support::responses::ev_assistant_message;
|
||||
use core_test_support::responses::ev_completed;
|
||||
use core_test_support::responses::ev_function_call;
|
||||
use core_test_support::responses::ev_function_call_with_namespace;
|
||||
use core_test_support::responses::ev_response_created;
|
||||
use core_test_support::responses::sse;
|
||||
use core_test_support::responses::start_mock_server;
|
||||
@@ -29,6 +30,7 @@ use core_test_support::test_codex::turn_permission_fields;
|
||||
use core_test_support::wait_for_event;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
use test_case::test_case;
|
||||
fn call_output(req: &ResponsesRequest, call_id: &str) -> (String, Option<bool>) {
|
||||
let raw = req.function_call_output(call_id);
|
||||
assert_eq!(
|
||||
@@ -134,8 +136,11 @@ async fn shell_command_tool_executes_command_and_streams_output() -> anyhow::Res
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(None; "omitted_namespace")]
|
||||
#[test_case(Some(""); "empty_namespace")]
|
||||
#[test_case(Some("functions"); "explicit_functions_namespace")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn update_plan_tool_emits_plan_update_event() -> anyhow::Result<()> {
|
||||
async fn update_plan_tool_emits_plan_update_event(namespace: Option<&str>) -> anyhow::Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
@@ -158,9 +163,15 @@ async fn update_plan_tool_emits_plan_update_event() -> anyhow::Result<()> {
|
||||
})
|
||||
.to_string();
|
||||
|
||||
let function_call = match namespace {
|
||||
Some(namespace) => {
|
||||
ev_function_call_with_namespace(call_id, namespace, "update_plan", &plan_args)
|
||||
}
|
||||
None => ev_function_call(call_id, "update_plan", &plan_args),
|
||||
};
|
||||
let first_response = sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_function_call(call_id, "update_plan", &plan_args),
|
||||
function_call,
|
||||
ev_completed("resp-1"),
|
||||
]);
|
||||
responses::mount_sse_once(&server, first_response).await;
|
||||
@@ -350,9 +361,11 @@ async fn apply_patch_tool_executes_and_emits_patch_events() -> anyhow::Result<()
|
||||
*** End Patch"#
|
||||
);
|
||||
|
||||
let mut custom_tool_call = ev_apply_patch_custom_tool_call(call_id, &patch_content);
|
||||
custom_tool_call["namespace"] = json!("functions");
|
||||
let first_response = sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_apply_patch_custom_tool_call(call_id, &patch_content),
|
||||
custom_tool_call,
|
||||
ev_completed("resp-1"),
|
||||
]);
|
||||
responses::mount_sse_once(&server, first_response).await;
|
||||
|
||||
@@ -363,7 +363,7 @@ where
|
||||
};
|
||||
let should_count_for_goal_progress = runtime.is_enabled()
|
||||
&& tool_attempt_counts_for_goal_progress(input.outcome)
|
||||
&& !(input.tool_name.namespace.is_none()
|
||||
&& !(input.tool_name.is_default_namespace()
|
||||
&& input.tool_name.name == UPDATE_GOAL_TOOL_NAME);
|
||||
if !should_count_for_goal_progress {
|
||||
return;
|
||||
|
||||
@@ -33,7 +33,9 @@ fn uses_reserved_image_gen_namespace() {
|
||||
panic!("imagegen should advertise a namespace tool");
|
||||
};
|
||||
assert_eq!(spec.name, IMAGE_GEN_NAMESPACE);
|
||||
let ResponsesApiNamespaceTool::Function(function) = &spec.tools[0];
|
||||
let ResponsesApiNamespaceTool::Function(function) = &spec.tools[0] else {
|
||||
panic!("imagegen should advertise a function tool");
|
||||
};
|
||||
assert_eq!(function.name, IMAGEGEN_TOOL_NAME);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ mod tool_name;
|
||||
pub use agent_path::AgentPath;
|
||||
pub use session_id::SessionId;
|
||||
pub use thread_id::ThreadId;
|
||||
pub use tool_name::DEFAULT_FUNCTION_NAMESPACE;
|
||||
pub use tool_name::ToolName;
|
||||
pub mod approvals;
|
||||
pub mod capabilities;
|
||||
|
||||
@@ -3,6 +3,9 @@ use serde::Serialize;
|
||||
use std::cmp::Ordering;
|
||||
use std::fmt;
|
||||
|
||||
/// Namespace used internally for top-level function and custom tools.
|
||||
pub const DEFAULT_FUNCTION_NAMESPACE: &str = "functions";
|
||||
|
||||
/// Identifies a callable tool, preserving the namespace split when the model
|
||||
/// provides one.
|
||||
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
|
||||
@@ -26,17 +29,35 @@ impl ToolName {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn function(name: impl Into<String>) -> Self {
|
||||
Self::namespaced(DEFAULT_FUNCTION_NAMESPACE, name)
|
||||
}
|
||||
|
||||
pub fn namespaced(namespace: impl Into<String>, name: impl Into<String>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
namespace: Some(namespace.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_default_namespace(mut self) -> Self {
|
||||
if self.namespace.as_deref().is_none_or(str::is_empty) {
|
||||
self.namespace = Some(DEFAULT_FUNCTION_NAMESPACE.to_string());
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn is_default_namespace(&self) -> bool {
|
||||
self.namespace
|
||||
.as_deref()
|
||||
.is_none_or(|namespace| namespace.is_empty() || namespace == DEFAULT_FUNCTION_NAMESPACE)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ToolName {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match &self.namespace {
|
||||
Some(_) if self.is_default_namespace() => f.write_str(&self.name),
|
||||
Some(namespace) => write!(f, "{namespace}{}", self.name),
|
||||
None => f.write_str(&self.name),
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
use std::fmt::Display;
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_protocol::DEFAULT_FUNCTION_NAMESPACE;
|
||||
use codex_protocol::models::AdditionalPermissionProfile;
|
||||
use codex_protocol::models::ResponseInputItem;
|
||||
use codex_protocol::models::SandboxPermissions;
|
||||
@@ -193,7 +194,10 @@ impl ToolDispatchTraceContext {
|
||||
|
||||
fn suppresses_tool_dispatch_trace(invocation: &ToolDispatchInvocation) -> bool {
|
||||
matches!(invocation.payload, ToolDispatchPayload::Custom { .. })
|
||||
&& invocation.tool_namespace.is_none()
|
||||
&& invocation
|
||||
.tool_namespace
|
||||
.as_deref()
|
||||
.is_none_or(|namespace| namespace.is_empty() || namespace == DEFAULT_FUNCTION_NAMESPACE)
|
||||
&& invocation.tool_name == codex_code_mode::PUBLIC_TOOL_NAME
|
||||
}
|
||||
|
||||
@@ -393,16 +397,18 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn suppresses_only_noncanonical_dispatch_boundaries() {
|
||||
assert!(suppresses_tool_dispatch_trace(&invocation(
|
||||
codex_code_mode::PUBLIC_TOOL_NAME,
|
||||
/*tool_namespace*/ None,
|
||||
ToolDispatchRequester::Model {
|
||||
model_visible_call_id: "call-exec".to_string(),
|
||||
},
|
||||
ToolDispatchPayload::Custom {
|
||||
input: "1 + 1".to_string(),
|
||||
},
|
||||
)));
|
||||
for namespace in [None, Some(""), Some(DEFAULT_FUNCTION_NAMESPACE)] {
|
||||
assert!(suppresses_tool_dispatch_trace(&invocation(
|
||||
codex_code_mode::PUBLIC_TOOL_NAME,
|
||||
namespace.map(str::to_string),
|
||||
ToolDispatchRequester::Model {
|
||||
model_visible_call_id: "call-exec".to_string(),
|
||||
},
|
||||
ToolDispatchPayload::Custom {
|
||||
input: "1 + 1".to_string(),
|
||||
},
|
||||
)));
|
||||
}
|
||||
assert!(!suppresses_tool_dispatch_trace(&invocation(
|
||||
"custom_tool",
|
||||
/*tool_namespace*/ None,
|
||||
|
||||
@@ -42,6 +42,20 @@ pub fn augment_tool_spec_for_code_mode(spec: ToolSpec) -> ToolSpec {
|
||||
tool.description =
|
||||
codex_code_mode::augment_tool_definition(definition).description;
|
||||
}
|
||||
ResponsesApiNamespaceTool::Freeform(tool) => {
|
||||
let tool_name =
|
||||
ToolName::namespaced(namespace.name.clone(), tool.name.clone());
|
||||
let definition = CodeModeToolDefinition {
|
||||
name: code_mode_name_for_tool_name(&tool_name),
|
||||
tool_name,
|
||||
description: tool.description.clone(),
|
||||
kind: CodeModeToolKind::Freeform,
|
||||
input_schema: None,
|
||||
output_schema: None,
|
||||
};
|
||||
tool.description =
|
||||
codex_code_mode::augment_tool_definition(definition).description;
|
||||
}
|
||||
}
|
||||
}
|
||||
ToolSpec::Namespace(namespace)
|
||||
@@ -146,6 +160,17 @@ fn code_mode_tool_definitions_for_spec(spec: &ToolSpec) -> Vec<CodeModeToolDefin
|
||||
output_schema: tool.output_schema.clone(),
|
||||
}
|
||||
}
|
||||
ResponsesApiNamespaceTool::Freeform(tool) => {
|
||||
let tool_name = ToolName::namespaced(namespace.name.clone(), tool.name.clone());
|
||||
CodeModeToolDefinition {
|
||||
name: code_mode_name_for_tool_name(&tool_name),
|
||||
tool_name,
|
||||
description: tool.description.clone(),
|
||||
kind: CodeModeToolKind::Freeform,
|
||||
input_schema: None,
|
||||
output_schema: None,
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
ToolSpec::ImageGeneration { .. }
|
||||
@@ -155,6 +180,9 @@ fn code_mode_tool_definitions_for_spec(spec: &ToolSpec) -> Vec<CodeModeToolDefin
|
||||
}
|
||||
|
||||
pub fn code_mode_name_for_tool_name(tool_name: &ToolName) -> String {
|
||||
if tool_name.is_default_namespace() {
|
||||
return tool_name.name.clone();
|
||||
}
|
||||
match tool_name.namespace.as_deref() {
|
||||
Some(namespace) if namespace.ends_with('_') || tool_name.name.starts_with('_') => {
|
||||
format!("{namespace}{}", tool_name.name)
|
||||
|
||||
@@ -25,6 +25,7 @@ pub use code_mode::code_mode_name_for_tool_name;
|
||||
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::DEFAULT_FUNCTION_NAMESPACE;
|
||||
pub use codex_protocol::ToolName;
|
||||
pub use dynamic_tool::parse_dynamic_tool;
|
||||
pub use function_call_error::FunctionCallError;
|
||||
@@ -106,3 +107,4 @@ pub use tool_spec::ResponsesApiWebSearchFilters;
|
||||
pub use tool_spec::ResponsesApiWebSearchUserLocation;
|
||||
pub use tool_spec::ToolSpec;
|
||||
pub use tool_spec::create_tools_json_for_responses_api;
|
||||
pub use tool_spec::create_tools_json_for_responses_lite;
|
||||
|
||||
@@ -61,9 +61,21 @@ pub fn default_namespace_description(namespace_name: &str) -> String {
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq)]
|
||||
#[serde(tag = "type")]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum ResponsesApiNamespaceTool {
|
||||
#[serde(rename = "function")]
|
||||
Function(ResponsesApiTool),
|
||||
#[serde(rename = "custom")]
|
||||
Freeform(FreeformTool),
|
||||
}
|
||||
|
||||
impl ResponsesApiNamespaceTool {
|
||||
pub fn name(&self) -> &str {
|
||||
match self {
|
||||
Self::Function(tool) => &tool.name,
|
||||
Self::Freeform(tool) => &tool.name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dynamic_tool_to_responses_api_tool(
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use crate::DEFAULT_FUNCTION_NAMESPACE;
|
||||
use crate::JsonSchema;
|
||||
use crate::LoadableToolSpec;
|
||||
use crate::ResponsesApiNamespace;
|
||||
use crate::ResponsesApiNamespaceTool;
|
||||
use crate::ResponsesApiTool;
|
||||
use crate::ToolSearchSourceInfo;
|
||||
@@ -36,16 +38,21 @@ impl ToolSearchInfo {
|
||||
ToolSpec::Function(mut tool) => {
|
||||
tool.defer_loading = Some(true);
|
||||
tool.output_schema = None;
|
||||
LoadableToolSpec::Function(tool)
|
||||
LoadableToolSpec::Namespace(ResponsesApiNamespace {
|
||||
name: DEFAULT_FUNCTION_NAMESPACE.to_string(),
|
||||
description: default_namespace_description(DEFAULT_FUNCTION_NAMESPACE),
|
||||
tools: vec![ResponsesApiNamespaceTool::Function(tool)],
|
||||
})
|
||||
}
|
||||
ToolSpec::Namespace(mut namespace) => {
|
||||
if namespace.description.trim().is_empty() {
|
||||
namespace.description = default_namespace_description(&namespace.name);
|
||||
}
|
||||
for tool in &mut namespace.tools {
|
||||
let ResponsesApiNamespaceTool::Function(tool) = tool;
|
||||
tool.defer_loading = Some(true);
|
||||
tool.output_schema = None;
|
||||
if let ResponsesApiNamespaceTool::Function(tool) = tool {
|
||||
tool.defer_loading = Some(true);
|
||||
tool.output_schema = None;
|
||||
}
|
||||
}
|
||||
LoadableToolSpec::Namespace(namespace)
|
||||
}
|
||||
@@ -74,8 +81,16 @@ fn default_tool_search_text(spec: &ToolSpec) -> String {
|
||||
push_search_part(&mut parts, namespace.name.clone());
|
||||
push_search_part(&mut parts, namespace.description.clone());
|
||||
for tool in &namespace.tools {
|
||||
let ResponsesApiNamespaceTool::Function(tool) = tool;
|
||||
append_function_search_text(tool, &mut parts);
|
||||
match tool {
|
||||
ResponsesApiNamespaceTool::Function(tool) => {
|
||||
append_function_search_text(tool, &mut parts);
|
||||
}
|
||||
ResponsesApiNamespaceTool::Freeform(tool) => {
|
||||
push_search_part(&mut parts, tool.name.clone());
|
||||
push_search_part(&mut parts, tool.description.clone());
|
||||
push_search_part(&mut parts, tool.format.syntax.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ToolSpec::ToolSearch { description, .. } => {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
use crate::DEFAULT_FUNCTION_NAMESPACE;
|
||||
use crate::FreeformTool;
|
||||
use crate::JsonSchema;
|
||||
use crate::LoadableToolSpec;
|
||||
use crate::ResponsesApiNamespace;
|
||||
use crate::ResponsesApiNamespaceTool;
|
||||
use crate::ResponsesApiTool;
|
||||
use crate::default_namespace_description;
|
||||
use codex_protocol::config_types::WebSearchContextSize;
|
||||
use codex_protocol::config_types::WebSearchFilters as ConfigWebSearchFilters;
|
||||
use codex_protocol::config_types::WebSearchUserLocation as ConfigWebSearchUserLocation;
|
||||
@@ -90,6 +93,53 @@ pub fn create_tools_json_for_responses_api(
|
||||
Ok(tools_json)
|
||||
}
|
||||
|
||||
pub fn create_tools_json_for_responses_lite(
|
||||
tools: &[ToolSpec],
|
||||
) -> Result<Vec<Value>, serde_json::Error> {
|
||||
let mut functions = Vec::new();
|
||||
let mut functions_description = None;
|
||||
let mut functions_index = None;
|
||||
let mut tools_json = Vec::new();
|
||||
|
||||
for tool in tools {
|
||||
match tool {
|
||||
ToolSpec::Function(tool) => {
|
||||
functions.push(ResponsesApiNamespaceTool::Function(tool.clone()));
|
||||
}
|
||||
ToolSpec::Freeform(tool) => {
|
||||
functions.push(ResponsesApiNamespaceTool::Freeform(tool.clone()));
|
||||
}
|
||||
ToolSpec::Namespace(namespace) if namespace.name == DEFAULT_FUNCTION_NAMESPACE => {
|
||||
if !namespace.description.trim().is_empty() {
|
||||
functions_description = Some(namespace.description.clone());
|
||||
}
|
||||
functions.extend(namespace.tools.clone());
|
||||
}
|
||||
tool => {
|
||||
tools_json.push(serde_json::to_value(tool)?);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
functions_index.get_or_insert(tools_json.len());
|
||||
}
|
||||
|
||||
if let Some(functions_index) = functions_index
|
||||
&& !functions.is_empty()
|
||||
{
|
||||
tools_json.insert(
|
||||
functions_index,
|
||||
serde_json::to_value(ToolSpec::Namespace(ResponsesApiNamespace {
|
||||
name: DEFAULT_FUNCTION_NAMESPACE.to_string(),
|
||||
description: functions_description
|
||||
.unwrap_or_else(|| default_namespace_description(DEFAULT_FUNCTION_NAMESPACE)),
|
||||
tools: functions,
|
||||
}))?,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(tools_json)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq)]
|
||||
pub struct ResponsesApiWebSearchFilters {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
|
||||
@@ -9,6 +9,7 @@ use crate::JsonSchema;
|
||||
use crate::ResponsesApiNamespaceTool;
|
||||
use crate::ResponsesApiTool;
|
||||
use crate::create_tools_json_for_responses_api;
|
||||
use crate::create_tools_json_for_responses_lite;
|
||||
use codex_protocol::config_types::WebSearchContextSize;
|
||||
use codex_protocol::config_types::WebSearchFilters as ConfigWebSearchFilters;
|
||||
use codex_protocol::config_types::WebSearchUserLocation as ConfigWebSearchUserLocation;
|
||||
@@ -150,6 +151,66 @@ fn create_tools_json_for_responses_api_includes_top_level_name() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn responses_lite_groups_default_function_and_custom_tools() {
|
||||
let function = |name: &str| ResponsesApiTool {
|
||||
name: name.to_string(),
|
||||
description: String::new(),
|
||||
strict: false,
|
||||
defer_loading: None,
|
||||
parameters: JsonSchema::default(),
|
||||
output_schema: None,
|
||||
};
|
||||
let tools = create_tools_json_for_responses_lite(&[
|
||||
ToolSpec::ToolSearch {
|
||||
execution: "client".to_string(),
|
||||
description: "Search tools".to_string(),
|
||||
parameters: JsonSchema::default(),
|
||||
},
|
||||
ToolSpec::Function(function("lookup_order")),
|
||||
ToolSpec::Freeform(FreeformTool {
|
||||
name: "exec".to_string(),
|
||||
description: String::new(),
|
||||
format: FreeformToolFormat {
|
||||
r#type: "grammar".to_string(),
|
||||
syntax: "lark".to_string(),
|
||||
definition: "start: /.+/".to_string(),
|
||||
},
|
||||
}),
|
||||
ToolSpec::Namespace(ResponsesApiNamespace {
|
||||
name: "functions".to_string(),
|
||||
description: "Default tools".to_string(),
|
||||
tools: vec![ResponsesApiNamespaceTool::Function(function("existing"))],
|
||||
}),
|
||||
])
|
||||
.expect("serialize Responses Lite tools");
|
||||
|
||||
assert_eq!(tools.len(), 2);
|
||||
assert_eq!(tools[0]["type"], "tool_search");
|
||||
assert_eq!(tools[1]["type"], "namespace");
|
||||
assert_eq!(tools[1]["name"], "functions");
|
||||
assert_eq!(tools[1]["description"], "Default tools");
|
||||
let nested_tools = tools[1]["tools"]
|
||||
.as_array()
|
||||
.expect("functions namespace should contain tools");
|
||||
assert_eq!(
|
||||
nested_tools
|
||||
.iter()
|
||||
.map(|tool| {
|
||||
(
|
||||
tool["type"].as_str().expect("tool type should be a string"),
|
||||
tool["name"].as_str().expect("tool name should be a string"),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
("function", "lookup_order"),
|
||||
("custom", "exec"),
|
||||
("function", "existing"),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn namespace_tool_spec_serializes_expected_wire_shape() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user