mirror of
https://github.com/openai/codex.git
synced 2026-10-24 18:18:41 +00:00
Canonicalize default tools under the functions namespace (#37022)
## What changed - Group top-level function and custom tool definitions into a single `functions` namespace for Responses Lite providers that support namespaced tools. - Normalize missing, empty, and explicit `functions` namespaces to the same tool identity across registration, routing, lifecycle hooks, configuration, and tool search. - Keep default tool names unprefixed in code mode, display output, and dispatch traces while preserving explicit non-default namespaces. ## Testing - Add coverage for Responses Lite serialization, tool search results, namespace normalization and collision handling, routing, lifecycle events, and code-mode namespace policies. GitOrigin-RevId: d48414005b5d22d39b11a198e19c47814d3a19f2
This commit is contained in:
@@ -89,6 +89,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_tools::create_tools_raw_json_for_responses_api;
|
||||
use eventsource_stream::Event;
|
||||
use eventsource_stream::EventStreamError;
|
||||
@@ -859,7 +860,11 @@ impl ModelClient {
|
||||
}
|
||||
}
|
||||
let (instructions, tools) = if model_info.use_responses_lite {
|
||||
let tools = create_tools_json_for_responses_api(&prompt.tools)?;
|
||||
let tools = if self.state.provider.capabilities().namespace_tools {
|
||||
create_tools_json_for_responses_lite(&prompt.tools)?
|
||||
} else {
|
||||
create_tools_json_for_responses_api(&prompt.tools)?
|
||||
};
|
||||
let mut prefix = vec![ResponseItem::AdditionalTools {
|
||||
id: None,
|
||||
role: "developer".to_string(),
|
||||
|
||||
@@ -31,16 +31,17 @@ pub(crate) fn shell_script_for_invocation(invocation: &ToolInvocation) -> Option
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,9 +62,9 @@ pub(crate) fn default_exec_yield_time_override_ms(features: &Features) -> Option
|
||||
.then_some(BUFFERED_EXEC_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)]
|
||||
@@ -339,7 +339,7 @@ async fn call_nested_tool(
|
||||
};
|
||||
|
||||
let call = ToolCall {
|
||||
tool_name,
|
||||
tool_name: tool_name.with_default_namespace(),
|
||||
call_id: format!("{PUBLIC_TOOL_NAME}-{}", uuid::Uuid::new_v4()),
|
||||
payload,
|
||||
encrypted_function_args: None,
|
||||
|
||||
@@ -82,7 +82,8 @@ impl CodeModeWaitHandler {
|
||||
);
|
||||
let result = 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).inspect_err(|_error| {
|
||||
telemetry.finish(/*success*/ false);
|
||||
|
||||
@@ -70,7 +70,7 @@ impl ExecutedToolCallRecorder {
|
||||
) {
|
||||
if matches!(source, ToolCallSource::Direct)
|
||||
&& matches!(tool_mode, ToolMode::CodeMode | ToolMode::CodeModeOnly)
|
||||
&& call.tool_name.namespace.is_none()
|
||||
&& call.tool_name.is_default_namespace()
|
||||
&& matches!(
|
||||
(call.tool_name.name.as_str(), &call.payload),
|
||||
(
|
||||
|
||||
@@ -40,6 +40,10 @@ 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());
|
||||
|
||||
@@ -266,7 +266,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"
|
||||
|
||||
@@ -290,7 +290,8 @@ impl ToolRegistry {
|
||||
runtime: Arc<dyn CoreToolRuntime>,
|
||||
exposure: ToolExposure,
|
||||
) {
|
||||
match self.tools.entry(runtime.tool_name()) {
|
||||
let tool_name = runtime.tool_name().with_default_namespace();
|
||||
match self.tools.entry(tool_name) {
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(RegisteredTool { runtime, exposure });
|
||||
}
|
||||
@@ -302,7 +303,7 @@ impl ToolRegistry {
|
||||
}
|
||||
|
||||
pub(crate) fn prepend_trusted(&mut self, runtime: Arc<dyn CoreToolRuntime>) {
|
||||
let tool_name = runtime.tool_name();
|
||||
let tool_name = runtime.tool_name().with_default_namespace();
|
||||
if self.tools.contains_key(&tool_name) {
|
||||
error_or_panic(format!("tool {tool_name} already registered"));
|
||||
return;
|
||||
@@ -323,8 +324,8 @@ impl ToolRegistry {
|
||||
runtime: Arc<dyn CoreToolRuntime>,
|
||||
exposure: ToolExposure,
|
||||
) -> bool {
|
||||
let tool_name = runtime.tool_name();
|
||||
if tool_name.namespace.is_none() && tool_name.name == "shell_command" {
|
||||
let tool_name = runtime.tool_name().with_default_namespace();
|
||||
if tool_name.is_default_namespace() && tool_name.name == "shell_command" {
|
||||
tracing::warn!(tool_name = %tool_name, "skipping external tool with reserved name");
|
||||
if self.tools.contains_key(&tool_name) {
|
||||
self.record_collision(tool_name);
|
||||
@@ -358,7 +359,9 @@ impl ToolRegistry {
|
||||
}
|
||||
|
||||
pub(crate) fn remove(&mut self, tool_name: &ToolName) -> Option<Arc<dyn CoreToolRuntime>> {
|
||||
self.tools.shift_remove(tool_name).map(|tool| tool.runtime)
|
||||
self.tools
|
||||
.shift_remove(&tool_name.clone().with_default_namespace())
|
||||
.map(|tool| tool.runtime)
|
||||
}
|
||||
|
||||
pub(crate) fn entries(&self) -> impl Iterator<Item = &RegisteredTool> {
|
||||
@@ -372,7 +375,7 @@ impl ToolRegistry {
|
||||
pub(crate) fn deferred_tool_namespaces(&self) -> BTreeMap<String, String> {
|
||||
let mut namespaces = BTreeMap::<String, String>::new();
|
||||
for (name, tool) in &self.tools {
|
||||
if !tool.exposure.is_deferred() {
|
||||
if !tool.exposure.is_deferred() || name.is_default_namespace() {
|
||||
continue;
|
||||
}
|
||||
let Some(namespace) = &name.namespace else {
|
||||
@@ -410,7 +413,9 @@ impl ToolRegistry {
|
||||
}
|
||||
|
||||
pub(crate) fn tool(&self, name: &ToolName) -> Option<Arc<dyn CoreToolRuntime>> {
|
||||
self.tools.get(name).map(|tool| Arc::clone(&tool.runtime))
|
||||
self.tools
|
||||
.get(&name.clone().with_default_namespace())
|
||||
.map(|tool| Arc::clone(&tool.runtime))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -422,7 +427,9 @@ impl ToolRegistry {
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn tool_exposure(&self, name: &ToolName) -> Option<ToolExposure> {
|
||||
self.tools.get(name).map(|tool| tool.exposure)
|
||||
self.tools
|
||||
.get(&name.clone().with_default_namespace())
|
||||
.map(|tool| tool.exposure)
|
||||
}
|
||||
|
||||
pub(crate) fn create_diff_consumer(
|
||||
@@ -433,7 +440,7 @@ impl ToolRegistry {
|
||||
}
|
||||
|
||||
pub(crate) fn supports_parallel_tool_calls(&self, name: &ToolName) -> Option<bool> {
|
||||
let tool = self.tools.get(name)?;
|
||||
let tool = self.tools.get(&name.clone().with_default_namespace())?;
|
||||
Some(tool.exposure != ToolExposure::Hidden && tool.runtime.supports_parallel_tool_calls())
|
||||
}
|
||||
|
||||
@@ -504,7 +511,6 @@ impl ToolRegistry {
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
|
||||
let telemetry_tags = tool.telemetry_tags(&invocation);
|
||||
let mut tool_result_tags =
|
||||
Vec::with_capacity(base_tool_result_tags.len() + telemetry_tags.len() + 1);
|
||||
@@ -773,10 +779,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();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use super::*;
|
||||
use crate::session::session::Session;
|
||||
use crate::session::step_context::StepContext;
|
||||
use codex_protocol::DEFAULT_FUNCTION_NAMESPACE;
|
||||
use futures::future::BoxFuture;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
@@ -80,7 +81,11 @@ impl ToolExecutor<ToolInvocation> for LifecycleTestHandler {
|
||||
test_spec(&self.tool_name)
|
||||
}
|
||||
|
||||
fn handle(&self, _invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_> {
|
||||
fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_> {
|
||||
assert_eq!(
|
||||
invocation.tool_name,
|
||||
self.tool_name.clone().with_default_namespace()
|
||||
);
|
||||
Box::pin(self.handle_call())
|
||||
}
|
||||
}
|
||||
@@ -172,7 +177,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);
|
||||
@@ -187,6 +192,11 @@ fn handler_looks_up_namespaced_aliases_explicitly() {
|
||||
ToolRegistry::from_tools([Arc::clone(&plain_handler), Arc::clone(&namespaced_handler)]);
|
||||
|
||||
let plain = registry.tool(&plain_name);
|
||||
let default_namespaced = registry.tool(&codex_tools::ToolName::namespaced(
|
||||
DEFAULT_FUNCTION_NAMESPACE,
|
||||
tool_name,
|
||||
));
|
||||
let empty_namespaced = registry.tool(&codex_tools::ToolName::namespaced("", tool_name));
|
||||
let namespaced = registry.tool(&namespaced_name);
|
||||
let missing_namespaced = registry.tool(&codex_tools::ToolName::namespaced(
|
||||
"mcp__codex_apps__calendar",
|
||||
@@ -201,6 +211,16 @@ 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!(
|
||||
empty_namespaced
|
||||
.as_ref()
|
||||
.is_some_and(|handler| Arc::ptr_eq(handler, &plain_handler))
|
||||
);
|
||||
assert!(
|
||||
namespaced
|
||||
.as_ref()
|
||||
@@ -208,6 +228,45 @@ fn handler_looks_up_namespaced_aliases_explicitly() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registry_rejects_default_namespace_alias_collisions() {
|
||||
let plain_name = codex_tools::ToolName::plain("lookup");
|
||||
let namespaced_name = codex_tools::ToolName::namespaced(DEFAULT_FUNCTION_NAMESPACE, "lookup");
|
||||
|
||||
for [first_name, duplicate_name] in [
|
||||
[plain_name.clone(), namespaced_name.clone()],
|
||||
[namespaced_name, plain_name],
|
||||
] {
|
||||
let winner = Arc::new(TestHandler {
|
||||
tool_name: first_name.clone(),
|
||||
}) as Arc<dyn CoreToolRuntime>;
|
||||
let mut registry = ToolRegistry::from_tools([Arc::clone(&winner)]);
|
||||
|
||||
assert!(!registry.register_external(Arc::new(TestHandler {
|
||||
tool_name: duplicate_name.clone(),
|
||||
})));
|
||||
assert!(
|
||||
registry
|
||||
.tool(&duplicate_name)
|
||||
.is_some_and(|handler| Arc::ptr_eq(&handler, &winner))
|
||||
);
|
||||
assert_eq!(
|
||||
registry.tool_exposure(&duplicate_name),
|
||||
Some(ToolExposure::Direct)
|
||||
);
|
||||
assert_eq!(
|
||||
registry.supports_parallel_tool_calls(&duplicate_name),
|
||||
Some(false)
|
||||
);
|
||||
assert!(
|
||||
registry
|
||||
.remove(&duplicate_name)
|
||||
.is_some_and(|handler| Arc::ptr_eq(&handler, &winner))
|
||||
);
|
||||
assert!(registry.tool(&first_name).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registry_preserves_external_winners_and_trusted_synthetic_order() {
|
||||
let handler = |tool_name| Arc::new(TestHandler { tool_name }) as Arc<dyn CoreToolRuntime>;
|
||||
@@ -248,6 +307,12 @@ fn reserved_shell_command_rejects_external_runtimes_without_a_builtin() {
|
||||
handler(shell_command_name.clone()),
|
||||
ToolExposure::Direct,
|
||||
));
|
||||
assert!(
|
||||
!registry.register_external(handler(codex_tools::ToolName::namespaced(
|
||||
DEFAULT_FUNCTION_NAMESPACE,
|
||||
"shell_command",
|
||||
)))
|
||||
);
|
||||
assert!(registry.tool(&shell_command_name).is_none());
|
||||
assert_eq!(registry.first_collision(), None);
|
||||
|
||||
@@ -449,6 +514,7 @@ async fn spawn_agent_function_tools_use_agent_matcher_alias() {
|
||||
|
||||
let hook_payloads = [
|
||||
codex_tools::ToolName::plain("spawn_agent"),
|
||||
codex_tools::ToolName::namespaced(DEFAULT_FUNCTION_NAMESPACE, "spawn_agent"),
|
||||
codex_tools::ToolName::namespaced(MULTI_AGENT_V1_NAMESPACE, "spawn_agent"),
|
||||
]
|
||||
.into_iter()
|
||||
@@ -477,6 +543,10 @@ async fn spawn_agent_function_tools_use_agent_matcher_alias() {
|
||||
tool_name: HookToolName::spawn_agent(),
|
||||
tool_input: serde_json::json!({ "message": "inspect this repo" }),
|
||||
}),
|
||||
Some(PreToolUsePayload {
|
||||
tool_name: HookToolName::spawn_agent(),
|
||||
tool_input: serde_json::json!({ "message": "inspect this repo" }),
|
||||
}),
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -565,7 +635,7 @@ fn post_tool_use_feedback_output_keeps_code_mode_result_typed() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dispatch_notifies_tool_lifecycle_contributors() -> anyhow::Result<()> {
|
||||
async fn dispatch_uses_canonical_tool_names_for_lifecycle_contributors() -> anyhow::Result<()> {
|
||||
let (mut session, turn) = crate::session::tests::make_session_and_context().await;
|
||||
let records = Arc::new(std::sync::Mutex::new(Vec::new()));
|
||||
let mut builder = codex_extension_api::ExtensionRegistryBuilder::<crate::config::Config>::new();
|
||||
@@ -575,7 +645,7 @@ async fn dispatch_notifies_tool_lifecycle_contributors() -> anyhow::Result<()> {
|
||||
session.services.extensions = Arc::new(builder.build());
|
||||
|
||||
let ok_tool = codex_tools::ToolName::plain("ok_tool");
|
||||
let failing_tool = codex_tools::ToolName::plain("failing_tool");
|
||||
let failing_tool = codex_tools::ToolName::namespaced("extensions", "failing_tool");
|
||||
let ok_handler = Arc::new(LifecycleTestHandler {
|
||||
tool_name: ok_tool.clone(),
|
||||
result: LifecycleTestResult::Ok { success: false },
|
||||
@@ -594,7 +664,7 @@ async fn dispatch_notifies_tool_lifecycle_contributors() -> anyhow::Result<()> {
|
||||
Arc::clone(&session),
|
||||
Arc::clone(&turn),
|
||||
"ok-call",
|
||||
ok_tool.clone(),
|
||||
codex_tools::ToolName::namespaced(DEFAULT_FUNCTION_NAMESPACE, "ok_tool"),
|
||||
),
|
||||
/*terminal_outcome_reached*/ None,
|
||||
)
|
||||
@@ -619,11 +689,11 @@ async fn dispatch_notifies_tool_lifecycle_contributors() -> anyhow::Result<()> {
|
||||
let expected = vec![
|
||||
RecordedToolLifecycle::Start {
|
||||
call_id: "ok-call".to_string(),
|
||||
tool_name: ok_tool.clone(),
|
||||
tool_name: ok_tool.clone().with_default_namespace(),
|
||||
},
|
||||
RecordedToolLifecycle::Finish {
|
||||
call_id: "ok-call".to_string(),
|
||||
tool_name: ok_tool,
|
||||
tool_name: ok_tool.with_default_namespace(),
|
||||
outcome: codex_extension_api::ToolCallOutcome::Completed { success: false },
|
||||
},
|
||||
RecordedToolLifecycle::Start {
|
||||
|
||||
@@ -161,7 +161,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,
|
||||
@@ -196,7 +196,7 @@ impl ToolRouter {
|
||||
call_id,
|
||||
..
|
||||
} => Ok(Some(ToolCall {
|
||||
tool_name: ToolName::new(namespace, name),
|
||||
tool_name: ToolName::new(namespace, name).with_default_namespace(),
|
||||
call_id,
|
||||
payload: ToolPayload::Custom { input },
|
||||
encrypted_function_args: None,
|
||||
|
||||
@@ -19,6 +19,7 @@ use codex_extension_api::ExtensionRegistryBuilder;
|
||||
use codex_extension_api::ResponsesApiTool;
|
||||
use codex_extension_api::ToolCall as ExtensionToolCall;
|
||||
use codex_extension_api::ToolExecutor;
|
||||
use codex_protocol::DEFAULT_FUNCTION_NAMESPACE;
|
||||
use codex_protocol::dynamic_tools::DynamicToolFunctionSpec;
|
||||
use codex_protocol::dynamic_tools::DynamicToolNamespaceSpec;
|
||||
use codex_protocol::dynamic_tools::DynamicToolNamespaceTool;
|
||||
@@ -268,6 +269,42 @@ async fn build_custom_tool_call_uses_namespace_for_registry_name() -> anyhow::Re
|
||||
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(),
|
||||
encrypted_function_args: None,
|
||||
call_id: "call-function".to_string(),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
})?
|
||||
.expect("function_call should produce a tool call");
|
||||
let custom_call = ToolRouter::build_tool_call(ResponseItem::CustomToolCall {
|
||||
id: None,
|
||||
status: None,
|
||||
call_id: "call-custom".to_string(),
|
||||
name: "apply_patch".to_string(),
|
||||
namespace: namespace.map(str::to_string),
|
||||
input: "patch".to_string(),
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
})?
|
||||
.expect("custom_tool_call should produce a tool call");
|
||||
|
||||
assert_eq!(
|
||||
[function_call.tool_name, custom_call.tool_name],
|
||||
[
|
||||
ToolName::namespaced(DEFAULT_FUNCTION_NAMESPACE, "lookup"),
|
||||
ToolName::namespaced(DEFAULT_FUNCTION_NAMESPACE, "apply_patch"),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mcp_parallel_support_uses_handler_data() -> anyhow::Result<()> {
|
||||
let (_, turn) = make_session_and_context().await;
|
||||
|
||||
@@ -205,6 +205,7 @@ fn apply_mcp_tool_exposure_policy(
|
||||
let Some(omitted_exposures) = omitted_exposures_by_tool.get(&tool_name) else {
|
||||
continue;
|
||||
};
|
||||
let tool_name = tool_name.with_default_namespace();
|
||||
|
||||
let mut exposures = ToolExposures::ALL.difference(*omitted_exposures);
|
||||
if tool_name.namespace.as_ref().is_some_and(|namespace| {
|
||||
@@ -368,6 +369,7 @@ fn apply_direct_model_only_namespace_overrides(
|
||||
let configured = tool
|
||||
.runtime
|
||||
.tool_name()
|
||||
.with_default_namespace()
|
||||
.namespace
|
||||
.as_ref()
|
||||
.is_some_and(|namespace| {
|
||||
@@ -591,6 +593,7 @@ fn is_hidden_by_code_mode_only(
|
||||
}
|
||||
|
||||
fn is_excluded_from_code_mode(turn_context: &TurnContext, tool_name: &ToolName) -> bool {
|
||||
let tool_name = tool_name.clone().with_default_namespace();
|
||||
tool_name.namespace.as_ref().is_some_and(|namespace| {
|
||||
turn_context
|
||||
.config
|
||||
|
||||
@@ -1824,6 +1824,25 @@ async fn code_mode_only_exposes_configured_dynamic_namespace_directly() {
|
||||
assert!(!exec.description.contains("direct_only_lookup(args:"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn code_mode_only_exposes_default_namespace_tools_directly() {
|
||||
let plan = probe(|turn| {
|
||||
set_features(turn, &[Feature::CodeMode, Feature::CodeModeOnly]);
|
||||
update_config(turn, |config| {
|
||||
config.code_mode.direct_only_tool_namespaces = vec!["functions".to_string()];
|
||||
});
|
||||
})
|
||||
.await;
|
||||
|
||||
plan.assert_visible_contains(&["update_plan"]);
|
||||
assert_eq!(plan.exposure("update_plan"), ToolExposure::DirectModelOnly);
|
||||
|
||||
let ToolSpec::Freeform(exec) = plan.visible_spec(codex_code_mode::PUBLIC_TOOL_NAME) else {
|
||||
panic!("expected code mode exec tool");
|
||||
};
|
||||
assert!(!exec.description.contains("update_plan(args:"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn excluded_deferred_namespaces_do_not_enable_nested_tool_guidance() {
|
||||
let plan = probe_with(
|
||||
@@ -1860,6 +1879,26 @@ async fn excluded_deferred_namespaces_do_not_enable_nested_tool_guidance() {
|
||||
]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn code_mode_excludes_default_namespace_tools() {
|
||||
let plan = probe(|turn| {
|
||||
set_feature(turn, Feature::CodeMode, /*enabled*/ true);
|
||||
update_config(turn, |config| {
|
||||
config.code_mode.excluded_tool_namespaces = vec!["functions".to_string()];
|
||||
});
|
||||
})
|
||||
.await;
|
||||
|
||||
plan.assert_visible_contains(&["update_plan"]);
|
||||
plan.assert_registered_contains(&["update_plan"]);
|
||||
assert_eq!(plan.exposure("update_plan"), ToolExposure::Direct);
|
||||
|
||||
let ToolSpec::Freeform(exec) = plan.visible_spec(codex_code_mode::PUBLIC_TOOL_NAME) else {
|
||||
panic!("expected code mode exec tool");
|
||||
};
|
||||
assert!(!exec.description.contains("update_plan(args:"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multi_agent_feature_selects_one_agent_tool_family() {
|
||||
let v1 = probe(|turn| {
|
||||
|
||||
@@ -80,7 +80,12 @@ fn tool_dispatch_invocation(invocation: &ToolInvocation) -> Option<ToolDispatchI
|
||||
codex_turn_id: invocation.turn.sub_id.clone(),
|
||||
tool_call_id: invocation.call_id.clone(),
|
||||
tool_name: invocation.tool_name.name.clone(),
|
||||
tool_namespace: invocation.tool_name.namespace.clone(),
|
||||
tool_namespace: invocation
|
||||
.tool_name
|
||||
.namespace
|
||||
.as_ref()
|
||||
.filter(|_| !invocation.tool_name.is_default_namespace())
|
||||
.cloned(),
|
||||
requester,
|
||||
payload: tool_dispatch_payload(&invocation.payload),
|
||||
})
|
||||
|
||||
@@ -287,18 +287,24 @@ async fn missing_code_mode_wait_traces_only_the_wait_tool_call() -> anyhow::Resu
|
||||
let session = Arc::new(session);
|
||||
let turn = Arc::new(turn);
|
||||
|
||||
let mut invocation = test_invocation(
|
||||
session,
|
||||
turn,
|
||||
"wait-call",
|
||||
WAIT_TOOL_NAME,
|
||||
ToolCallSource::Direct,
|
||||
r#"{"cell_id":"noop","terminate":true}"#,
|
||||
);
|
||||
invocation.tool_name = invocation.tool_name.with_default_namespace();
|
||||
assert!(
|
||||
super::tool_dispatch_invocation(&invocation)
|
||||
.expect("wait calls should produce a trace invocation")
|
||||
.tool_namespace
|
||||
.is_none()
|
||||
);
|
||||
|
||||
registry
|
||||
.dispatch_any_with_terminal_outcome(
|
||||
test_invocation(
|
||||
session,
|
||||
turn,
|
||||
"wait-call",
|
||||
WAIT_TOOL_NAME,
|
||||
ToolCallSource::Direct,
|
||||
r#"{"cell_id":"noop","terminate":true}"#,
|
||||
),
|
||||
/*terminal_outcome_reached*/ None,
|
||||
)
|
||||
.dispatch_any_with_terminal_outcome(invocation, /*terminal_outcome_reached*/ None)
|
||||
.await?;
|
||||
|
||||
let replayed = codex_rollout_trace::replay_bundle(single_bundle_dir(temp.path())?)?;
|
||||
|
||||
@@ -4623,7 +4623,14 @@ text(JSON.stringify({
|
||||
})
|
||||
})
|
||||
.and_then(|item| item["tools"].as_array())
|
||||
.expect("the Responses Lite request should contain its additional tools")
|
||||
.and_then(|tools| {
|
||||
tools.iter().find(|tool| {
|
||||
tool.get("type").and_then(Value::as_str) == Some("namespace")
|
||||
&& tool.get("name").and_then(Value::as_str) == Some("functions")
|
||||
})
|
||||
})
|
||||
.and_then(|namespace| namespace["tools"].as_array())
|
||||
.expect("the Responses Lite request should contain its default-namespace tools")
|
||||
} else {
|
||||
first_body["tools"]
|
||||
.as_array()
|
||||
|
||||
@@ -129,7 +129,19 @@ async fn responses_lite_uses_input_items_for_instructions_and_tools() -> Result<
|
||||
);
|
||||
|
||||
let tools = additional_tools(&body)?;
|
||||
assert!(!tools.is_empty());
|
||||
let functions_namespaces = tools
|
||||
.iter()
|
||||
.filter(|tool| tool["type"] == "namespace" && tool["name"] == "functions")
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(functions_namespaces.len(), 1);
|
||||
assert_eq!(functions_namespaces[0]["description"], "");
|
||||
assert!(has_namespaced_tool(tools, "functions", "wait"));
|
||||
assert!(has_namespaced_tool(tools, "functions", "exec"));
|
||||
assert!(
|
||||
tools
|
||||
.iter()
|
||||
.all(|tool| { !matches!(tool["type"].as_str(), Some("function" | "custom")) })
|
||||
);
|
||||
let client_metadata = body["client_metadata"]
|
||||
.as_object()
|
||||
.context("Responses request should include client metadata")?;
|
||||
|
||||
@@ -55,7 +55,7 @@ use core_test_support::apps_test_server::search_capable_apps_builder as configur
|
||||
use core_test_support::responses::ResponsesRequest;
|
||||
use core_test_support::responses::ev_assistant_message;
|
||||
use core_test_support::responses::ev_completed;
|
||||
use core_test_support::responses::ev_custom_tool_call;
|
||||
use core_test_support::responses::ev_custom_tool_call_with_namespace;
|
||||
use core_test_support::responses::ev_function_call_with_namespace;
|
||||
use core_test_support::responses::ev_response_created;
|
||||
use core_test_support::responses::ev_tool_search_call;
|
||||
@@ -1001,7 +1001,7 @@ async fn tool_search_returns_deferred_custom_tool_and_routes_follow_up_call() ->
|
||||
]),
|
||||
sse(vec![
|
||||
ev_response_created("resp-2"),
|
||||
ev_custom_tool_call("custom-1", "custom_echo", "hello"),
|
||||
ev_custom_tool_call_with_namespace("custom-1", "functions", "custom_echo", "hello"),
|
||||
ev_completed("resp-2"),
|
||||
]),
|
||||
sse(vec![
|
||||
@@ -1035,15 +1035,20 @@ async fn tool_search_returns_deferred_custom_tool_and_routes_follow_up_call() ->
|
||||
assert_eq!(
|
||||
tool_search_output_tools(&requests[1], "search-1"),
|
||||
vec![json!({
|
||||
"type": "custom",
|
||||
"name": "custom_echo",
|
||||
"description": "Echo a custom payload.",
|
||||
"defer_loading": true,
|
||||
"format": {
|
||||
"type": "grammar",
|
||||
"syntax": "lark",
|
||||
"definition": "start: /.+/",
|
||||
},
|
||||
"type": "namespace",
|
||||
"name": "functions",
|
||||
"description": "",
|
||||
"tools": [{
|
||||
"type": "custom",
|
||||
"name": "custom_echo",
|
||||
"description": "Echo a custom payload.",
|
||||
"defer_loading": true,
|
||||
"format": {
|
||||
"type": "grammar",
|
||||
"syntax": "lark",
|
||||
"definition": "start: /.+/",
|
||||
},
|
||||
}],
|
||||
})]
|
||||
);
|
||||
let output = requests[2].custom_tool_call_output("custom-1");
|
||||
@@ -1052,7 +1057,7 @@ async fn tool_search_returns_deferred_custom_tool_and_routes_follow_up_call() ->
|
||||
.as_str()
|
||||
.expect("custom tool output should contain serialized JSON"),
|
||||
)?;
|
||||
assert_eq!(output, json!({ "echo": "hello", "namespace": null }));
|
||||
assert_eq!(output, json!({ "echo": "hello", "namespace": "functions" }));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -372,7 +372,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;
|
||||
|
||||
@@ -9,6 +9,7 @@ pub use agent_path::AgentPath;
|
||||
pub use response_item_id::ResponseItemId;
|
||||
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 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)]
|
||||
@@ -32,11 +35,26 @@ impl ToolName {
|
||||
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 {
|
||||
matches!(
|
||||
self.namespace.as_deref(),
|
||||
None | Some("") | Some(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),
|
||||
}
|
||||
|
||||
@@ -178,6 +178,10 @@ 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)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::augment_tool_spec_for_code_mode;
|
||||
use super::code_mode_name_for_tool_name;
|
||||
use super::tool_spec_to_code_mode_tool_definition;
|
||||
use crate::AdditionalProperties;
|
||||
use crate::FreeformTool;
|
||||
@@ -13,6 +14,21 @@ use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[test]
|
||||
fn code_mode_tool_names_do_not_prefix_the_default_namespace() {
|
||||
for tool_name in [
|
||||
ToolName::plain("apply_patch"),
|
||||
ToolName::namespaced("functions", "apply_patch"),
|
||||
] {
|
||||
assert_eq!(code_mode_name_for_tool_name(&tool_name), "apply_patch");
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
code_mode_name_for_tool_name(&ToolName::namespaced("editor", "apply_patch")),
|
||||
"editor__apply_patch"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn augment_tool_spec_for_code_mode_augments_function_tools() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -106,4 +106,5 @@ 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;
|
||||
pub use tool_spec::create_tools_raw_json_for_responses_api;
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::ToolDefinition;
|
||||
use crate::ToolName;
|
||||
use crate::parse_dynamic_tool;
|
||||
use crate::parse_mcp_tool;
|
||||
use codex_protocol::DEFAULT_FUNCTION_NAMESPACE;
|
||||
use codex_protocol::dynamic_tools::DynamicToolFunctionSpec;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
@@ -46,8 +47,6 @@ pub enum LoadableToolSpec {
|
||||
#[allow(dead_code)]
|
||||
#[serde(rename = "function")]
|
||||
Function(ResponsesApiTool),
|
||||
#[serde(rename = "custom")]
|
||||
Custom(FreeformTool),
|
||||
#[serde(rename = "namespace")]
|
||||
Namespace(ResponsesApiNamespace),
|
||||
}
|
||||
@@ -60,7 +59,11 @@ pub struct ResponsesApiNamespace {
|
||||
}
|
||||
|
||||
pub fn default_namespace_description(namespace_name: &str) -> String {
|
||||
format!("Tools in the {namespace_name} namespace.")
|
||||
if namespace_name == DEFAULT_FUNCTION_NAMESPACE {
|
||||
String::new()
|
||||
} else {
|
||||
format!("Tools in the {namespace_name} namespace.")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq)]
|
||||
@@ -87,8 +90,8 @@ pub fn coalesce_loadable_tool_specs(
|
||||
let mut coalesced_specs = Vec::new();
|
||||
for spec in specs {
|
||||
match spec {
|
||||
tool @ (LoadableToolSpec::Function(_) | LoadableToolSpec::Custom(_)) => {
|
||||
coalesced_specs.push(tool);
|
||||
LoadableToolSpec::Function(tool) => {
|
||||
coalesced_specs.push(LoadableToolSpec::Function(tool));
|
||||
}
|
||||
LoadableToolSpec::Namespace(mut namespace) => {
|
||||
if let Some(existing_namespace) =
|
||||
@@ -98,9 +101,7 @@ pub fn coalesce_loadable_tool_specs(
|
||||
{
|
||||
Some(existing_namespace)
|
||||
}
|
||||
LoadableToolSpec::Function(_)
|
||||
| LoadableToolSpec::Custom(_)
|
||||
| LoadableToolSpec::Namespace(_) => None,
|
||||
LoadableToolSpec::Function(_) | LoadableToolSpec::Namespace(_) => None,
|
||||
})
|
||||
{
|
||||
existing_namespace.tools.append(&mut namespace.tools);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use crate::JsonSchema;
|
||||
use crate::LoadableToolSpec;
|
||||
use crate::ResponsesApiNamespace;
|
||||
use crate::ResponsesApiNamespaceTool;
|
||||
use crate::ResponsesApiTool;
|
||||
use crate::ToolSearchSourceInfo;
|
||||
use crate::ToolSpec;
|
||||
use crate::default_namespace_description;
|
||||
use codex_protocol::DEFAULT_FUNCTION_NAMESPACE;
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct ToolSearchEntry {
|
||||
@@ -36,11 +38,19 @@ 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::Freeform(mut tool) => {
|
||||
tool.defer_loading = Some(true);
|
||||
LoadableToolSpec::Custom(tool)
|
||||
LoadableToolSpec::Namespace(ResponsesApiNamespace {
|
||||
name: DEFAULT_FUNCTION_NAMESPACE.to_string(),
|
||||
description: default_namespace_description(DEFAULT_FUNCTION_NAMESPACE),
|
||||
tools: vec![ResponsesApiNamespaceTool::Custom(tool)],
|
||||
})
|
||||
}
|
||||
ToolSpec::Namespace(mut namespace) => {
|
||||
if namespace.description.trim().is_empty() {
|
||||
|
||||
@@ -2,6 +2,43 @@ use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[test]
|
||||
fn top_level_function_search_results_use_the_default_namespace() {
|
||||
let function_tool = ResponsesApiTool {
|
||||
name: "lookup_order".to_string(),
|
||||
description: "Look up an order".to_string(),
|
||||
strict: false,
|
||||
defer_loading: None,
|
||||
parameters: JsonSchema::object(
|
||||
BTreeMap::new(),
|
||||
/*required*/ None,
|
||||
/*additional_properties*/ None,
|
||||
),
|
||||
output_schema: Some(serde_json::json!({ "type": "object" })),
|
||||
};
|
||||
let search_info = ToolSearchInfo::from_tool_spec(
|
||||
ToolSpec::Function(function_tool.clone()),
|
||||
/*source_info*/ None,
|
||||
)
|
||||
.expect("top-level function should be searchable");
|
||||
|
||||
assert_eq!(
|
||||
(search_info.entry.search_text, search_info.entry.output),
|
||||
(
|
||||
"lookup_order lookup order Look up an order".to_string(),
|
||||
LoadableToolSpec::Namespace(ResponsesApiNamespace {
|
||||
name: "functions".to_string(),
|
||||
description: String::new(),
|
||||
tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool {
|
||||
defer_loading: Some(true),
|
||||
output_schema: None,
|
||||
..function_tool
|
||||
})],
|
||||
}),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn top_level_custom_tools_are_searchable() {
|
||||
let custom_tool = crate::FreeformTool {
|
||||
@@ -24,9 +61,13 @@ fn top_level_custom_tools_are_searchable() {
|
||||
(search_info.entry.search_text, search_info.entry.output),
|
||||
(
|
||||
"apply_patch Apply a patch lark".to_string(),
|
||||
LoadableToolSpec::Custom(crate::FreeformTool {
|
||||
defer_loading: Some(true),
|
||||
..custom_tool
|
||||
LoadableToolSpec::Namespace(ResponsesApiNamespace {
|
||||
name: "functions".to_string(),
|
||||
description: String::new(),
|
||||
tools: vec![ResponsesApiNamespaceTool::Custom(crate::FreeformTool {
|
||||
defer_loading: Some(true),
|
||||
..custom_tool
|
||||
})],
|
||||
}),
|
||||
)
|
||||
);
|
||||
|
||||
@@ -2,7 +2,10 @@ 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::DEFAULT_FUNCTION_NAMESPACE;
|
||||
use codex_protocol::config_types::WebSearchContextSize;
|
||||
use codex_protocol::config_types::WebSearchFilters as ConfigWebSearchFilters;
|
||||
use codex_protocol::config_types::WebSearchUserLocation as ConfigWebSearchUserLocation;
|
||||
@@ -68,7 +71,6 @@ impl From<LoadableToolSpec> for ToolSpec {
|
||||
fn from(value: LoadableToolSpec) -> Self {
|
||||
match value {
|
||||
LoadableToolSpec::Function(tool) => ToolSpec::Function(tool),
|
||||
LoadableToolSpec::Custom(tool) => ToolSpec::Freeform(tool),
|
||||
LoadableToolSpec::Namespace(namespace) => ToolSpec::Namespace(namespace),
|
||||
}
|
||||
}
|
||||
@@ -90,6 +92,55 @@ 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 = ResponsesApiNamespace {
|
||||
name: DEFAULT_FUNCTION_NAMESPACE.to_string(),
|
||||
description: default_namespace_description(DEFAULT_FUNCTION_NAMESPACE),
|
||||
tools: Vec::new(),
|
||||
};
|
||||
let mut functions_index = None;
|
||||
let mut tools_json = Vec::new();
|
||||
|
||||
for tool in tools {
|
||||
match tool {
|
||||
ToolSpec::Function(tool) => {
|
||||
functions
|
||||
.tools
|
||||
.push(ResponsesApiNamespaceTool::Function(tool.clone()));
|
||||
}
|
||||
ToolSpec::Freeform(tool) => {
|
||||
functions
|
||||
.tools
|
||||
.push(ResponsesApiNamespaceTool::Custom(tool.clone()));
|
||||
}
|
||||
ToolSpec::Namespace(namespace) if namespace.name == DEFAULT_FUNCTION_NAMESPACE => {
|
||||
if !namespace.description.trim().is_empty() {
|
||||
functions.description = namespace.description.clone();
|
||||
}
|
||||
functions.tools.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.tools.is_empty()
|
||||
{
|
||||
tools_json.insert(
|
||||
functions_index,
|
||||
serde_json::to_value(ToolSpec::Namespace(functions))?,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(tools_json)
|
||||
}
|
||||
|
||||
/// Returns raw JSON that can be embedded directly in a Responses API request.
|
||||
pub fn create_tools_raw_json_for_responses_api(
|
||||
tools: &[ToolSpec],
|
||||
|
||||
@@ -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 crate::create_tools_raw_json_for_responses_api;
|
||||
use codex_protocol::config_types::WebSearchContextSize;
|
||||
use codex_protocol::config_types::WebSearchFilters as ConfigWebSearchFilters;
|
||||
@@ -145,6 +146,107 @@ fn create_tools_json_for_responses_api_includes_top_level_name() {
|
||||
);
|
||||
}
|
||||
|
||||
fn responses_lite_function(name: &str) -> ResponsesApiTool {
|
||||
ResponsesApiTool {
|
||||
name: name.to_string(),
|
||||
description: format!("{name} tool"),
|
||||
strict: false,
|
||||
defer_loading: None,
|
||||
parameters: JsonSchema::object(
|
||||
BTreeMap::new(),
|
||||
/*required*/ None,
|
||||
/*additional_properties*/ None,
|
||||
),
|
||||
output_schema: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn responses_lite_groups_default_function_and_custom_tools() {
|
||||
let tools = create_tools_json_for_responses_lite(&[
|
||||
ToolSpec::ToolSearch {
|
||||
execution: "client".to_string(),
|
||||
description: "Search tools".to_string(),
|
||||
parameters: JsonSchema::object(
|
||||
BTreeMap::new(),
|
||||
/*required*/ None,
|
||||
/*additional_properties*/ None,
|
||||
),
|
||||
},
|
||||
ToolSpec::Function(responses_lite_function("lookup_order")),
|
||||
ToolSpec::Namespace(ResponsesApiNamespace {
|
||||
name: "editor".to_string(),
|
||||
description: "Editing tools".to_string(),
|
||||
tools: vec![ResponsesApiNamespaceTool::Function(
|
||||
responses_lite_function("edit"),
|
||||
)],
|
||||
}),
|
||||
ToolSpec::Freeform(FreeformTool {
|
||||
name: "exec".to_string(),
|
||||
description: "Run code".to_string(),
|
||||
defer_loading: None,
|
||||
format: FreeformToolFormat {
|
||||
r#type: "grammar".to_string(),
|
||||
syntax: "lark".to_string(),
|
||||
definition: "start: /.+/".to_string(),
|
||||
},
|
||||
}),
|
||||
ToolSpec::Namespace(ResponsesApiNamespace {
|
||||
name: "functions".to_string(),
|
||||
description: "Existing default tools".to_string(),
|
||||
tools: vec![ResponsesApiNamespaceTool::Function(
|
||||
responses_lite_function("existing"),
|
||||
)],
|
||||
}),
|
||||
ToolSpec::Function(responses_lite_function("last")),
|
||||
])
|
||||
.expect("serialize Responses Lite tools");
|
||||
|
||||
assert_eq!(tools.len(), 3);
|
||||
assert_eq!(tools[0]["type"], "tool_search");
|
||||
assert_eq!(tools[2]["type"], "namespace");
|
||||
assert_eq!(tools[2]["name"], "editor");
|
||||
assert_eq!(tools[2]["tools"][0]["name"], "edit");
|
||||
assert_eq!(tools[1]["type"], "namespace");
|
||||
assert_eq!(tools[1]["name"], "functions");
|
||||
assert_eq!(tools[1]["description"], "Existing default tools");
|
||||
assert_eq!(
|
||||
tools[1]["tools"]
|
||||
.as_array()
|
||||
.expect("functions namespace should contain tools")
|
||||
.iter()
|
||||
.map(|tool| (tool["type"].as_str(), tool["name"].as_str()))
|
||||
.collect::<Vec<_>>(),
|
||||
[
|
||||
(Some("function"), Some("lookup_order")),
|
||||
(Some("custom"), Some("exec")),
|
||||
(Some("function"), Some("existing")),
|
||||
(Some("function"), Some("last")),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn responses_lite_preserves_empty_functions_namespace_description() {
|
||||
let tools = create_tools_json_for_responses_lite(&[ToolSpec::Function(
|
||||
responses_lite_function("lookup_order"),
|
||||
)])
|
||||
.expect("serialize Responses Lite tools");
|
||||
assert_eq!(tools[0]["description"], "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn responses_lite_does_not_add_an_empty_functions_namespace() {
|
||||
let tools =
|
||||
create_tools_json_for_responses_lite(&[ToolSpec::Namespace(ResponsesApiNamespace {
|
||||
name: "functions".to_string(),
|
||||
description: "Empty default tools".to_string(),
|
||||
tools: Vec::new(),
|
||||
})])
|
||||
.expect("serialize Responses Lite tools");
|
||||
assert!(tools.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_tool_json_matches_value_encoding() {
|
||||
let specs = vec![ToolSpec::Function(ResponsesApiTool {
|
||||
|
||||
Reference in New Issue
Block a user