mirror of
https://github.com/openai/codex.git
synced 2026-11-12 21:20:34 +00:00
Add ranked tool discovery to JavaScript code mode (#51209)
## What changed
- Add the default-disabled `code_mode_tool_search` feature to expose `await tools.tool_search({query: "...", limit: 8})` in code mode when the model supports tool search.
- Return BM25-ranked deferred tools as `{tools: [{name, description}]}`, including callable names and TypeScript declarations for use in later code cells.
- Filter results through the current step's nested-tool policy before applying the limit, preserving namespace restrictions, normalized identities, description prefixes, and schema budgets.
- Add ranked-search guidance while retaining `ALL_TOOLS` catalog discovery and description overrides.
## Testing
Add coverage for feature and model-capability gating, namespace restrictions, argument validation, schema budgets, and discovery followed by deferred MCP and dynamic tool calls. Extend the scenario snapshot to cover search, invocation, yielding, and completion through `wait`.
GitOrigin-RevId: 092a29dd5f0773c518f53a001b9cafb917422f13
This commit is contained in:
committed by
copyberry
parent
5ad6891696
commit
4d15794336
@@ -223,7 +223,38 @@ fn code_mode_tool_definitions_for_spec(
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
ToolSpec::ToolSearch { .. } | ToolSpec::WebSearch { .. } => Vec::new(),
|
||||
ToolSpec::ToolSearch { parameters, .. } => {
|
||||
let name = spec.name().to_string();
|
||||
vec![CodeModeToolDefinition {
|
||||
tool_name: ToolName::plain(name.clone()),
|
||||
name,
|
||||
description: codex_code_mode::TOOL_SEARCH_GUIDANCE.to_string(),
|
||||
kind: CodeModeToolKind::Function,
|
||||
input_schema: serde_json::to_value(parameters).ok(),
|
||||
input_schema_max_bytes: Some(effective_input_schema_max_bytes(
|
||||
parameters.mcp_input_schema_max_bytes,
|
||||
code_mode_input_schema_max_bytes,
|
||||
)),
|
||||
output_schema: Some(serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tools": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"description": { "type": "string" }
|
||||
},
|
||||
"required": ["name", "description"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["tools"]
|
||||
})),
|
||||
}]
|
||||
}
|
||||
ToolSpec::WebSearch { .. } => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -238,17 +238,50 @@ declare const tools: { editor__apply_patch(input: string): Promise<unknown>; };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_spec_to_code_mode_tool_definition_skips_unsupported_variants() {
|
||||
fn tool_search_spec_converts_to_a_callable_code_mode_definition() {
|
||||
let definition = tool_spec_to_code_mode_tool_definition(&ToolSpec::ToolSearch {
|
||||
execution: "client".to_string(),
|
||||
description: "Native tool discovery".to_string(),
|
||||
parameters: JsonSchema::object(
|
||||
BTreeMap::from([(
|
||||
"query".to_string(),
|
||||
JsonSchema::string(/*description*/ None),
|
||||
)]),
|
||||
Some(vec!["query".to_string()]),
|
||||
Some(AdditionalProperties::Boolean(false)),
|
||||
),
|
||||
})
|
||||
.expect("tool search should be callable from Code Mode");
|
||||
assert_eq!(
|
||||
tool_spec_to_code_mode_tool_definition(&ToolSpec::ToolSearch {
|
||||
execution: "sync".to_string(),
|
||||
description: "Search".to_string(),
|
||||
parameters: JsonSchema::object(
|
||||
BTreeMap::new(),
|
||||
/*required*/ None,
|
||||
/*additional_properties*/ None
|
||||
),
|
||||
}),
|
||||
None
|
||||
(
|
||||
&definition.name,
|
||||
&definition.tool_name,
|
||||
&definition.input_schema,
|
||||
definition.input_schema_max_bytes,
|
||||
),
|
||||
(
|
||||
&"tool_search".to_string(),
|
||||
&ToolName::plain("tool_search"),
|
||||
&Some(json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": { "type": "string" },
|
||||
},
|
||||
"required": ["query"],
|
||||
"additionalProperties": false,
|
||||
})),
|
||||
Some(codex_code_mode::DEFAULT_INPUT_SCHEMA_MAX_BYTES),
|
||||
),
|
||||
);
|
||||
assert!(
|
||||
definition
|
||||
.description
|
||||
.contains("tool_search(args: { query: string; })")
|
||||
);
|
||||
assert!(
|
||||
definition
|
||||
.description
|
||||
.contains("tools[result.name](arguments)")
|
||||
);
|
||||
assert!(!definition.description.contains("Native tool discovery"));
|
||||
}
|
||||
|
||||
@@ -83,9 +83,14 @@ impl<'a> IndirectNamespacePrefixes<'a> {
|
||||
return;
|
||||
}
|
||||
for tool in tools {
|
||||
if let Some(prefix) = self.prefix(namespace_name(&tool.tool_name)) {
|
||||
prepend(prefix, &mut tool.description);
|
||||
}
|
||||
self.apply_code_mode_description(&tool.tool_name, &mut tool.description);
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds guidance to one owned description without copying the tool's schemas.
|
||||
pub fn apply_code_mode_description(&self, tool_name: &ToolName, description: &mut String) {
|
||||
if let Some(prefix) = self.prefix(namespace_name(tool_name)) {
|
||||
prepend(prefix, description);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ fn plain_tools_join_an_existing_functions_group_only_with_a_prefix() {
|
||||
codex_code_mode::DEFAULT_EXEC_YIELD_TIME_MS,
|
||||
/*code_mode_only*/ true,
|
||||
codex_code_mode::ImageDetailVisibility::Visible,
|
||||
codex_code_mode::DeferredToolDiscovery::Catalog,
|
||||
/*messages*/ None,
|
||||
)
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::LoadableToolSpec;
|
||||
use crate::ResponsesApiNamespace;
|
||||
use crate::ResponsesApiNamespaceTool;
|
||||
use crate::ResponsesApiTool;
|
||||
use crate::ToolName;
|
||||
use crate::ToolSearchSourceInfo;
|
||||
use crate::ToolSpec;
|
||||
use crate::default_namespace_description;
|
||||
@@ -16,6 +17,27 @@ pub struct ToolSearchEntry {
|
||||
}
|
||||
|
||||
impl ToolSearchEntry {
|
||||
/// Iterate candidate identities without materializing their tool schemas.
|
||||
pub fn tool_names(&self) -> impl Iterator<Item = ToolName> + '_ {
|
||||
let (namespace, name, tools): (&str, Option<&str>, &[ResponsesApiNamespaceTool]) =
|
||||
match self.spec.as_ref() {
|
||||
ToolSpec::Function(tool) => (DEFAULT_FUNCTION_NAMESPACE, Some(&tool.name), &[]),
|
||||
ToolSpec::Freeform(tool) => (DEFAULT_FUNCTION_NAMESPACE, Some(&tool.name), &[]),
|
||||
ToolSpec::Namespace(namespace) => {
|
||||
(&namespace.name, None, namespace.tools.as_slice())
|
||||
}
|
||||
ToolSpec::ToolSearch { .. } | ToolSpec::WebSearch { .. } => {
|
||||
unreachable!("search entries contain only loadable tools");
|
||||
}
|
||||
};
|
||||
name.into_iter()
|
||||
.chain(tools.iter().map(|tool| match tool {
|
||||
ResponsesApiNamespaceTool::Function(tool) => tool.name.as_str(),
|
||||
ResponsesApiNamespaceTool::Custom(tool) => tool.name.as_str(),
|
||||
}))
|
||||
.map(move |name| ToolName::namespaced(namespace, name))
|
||||
}
|
||||
|
||||
/// Materialize only selected results; output schemas remain shared until discarded.
|
||||
pub fn to_loadable_spec(&self) -> LoadableToolSpec {
|
||||
let Some(output) = normalize_search_spec(self.spec.as_ref().clone()) else {
|
||||
|
||||
@@ -26,23 +26,46 @@ fn shared_search_specs_preserve_results_and_release_the_source() {
|
||||
definition: "start: \"patch\"".to_string(),
|
||||
},
|
||||
};
|
||||
for spec in [
|
||||
ToolSpec::Function(function.clone()),
|
||||
ToolSpec::Freeform(custom.clone()),
|
||||
ToolSpec::Namespace(ResponsesApiNamespace {
|
||||
name: "example".to_string(),
|
||||
description: String::new(),
|
||||
tools: vec![
|
||||
ResponsesApiNamespaceTool::Function(function),
|
||||
ResponsesApiNamespaceTool::Custom(custom),
|
||||
for (spec, expected_names) in [
|
||||
(
|
||||
ToolSpec::Function(function.clone()),
|
||||
vec![ToolName::namespaced("functions", "lookup")],
|
||||
),
|
||||
(
|
||||
ToolSpec::Freeform(custom.clone()),
|
||||
vec![ToolName::namespaced("functions", "patch")],
|
||||
),
|
||||
(
|
||||
ToolSpec::Namespace(ResponsesApiNamespace {
|
||||
name: "example".to_string(),
|
||||
description: String::new(),
|
||||
tools: vec![
|
||||
ResponsesApiNamespaceTool::Function(function),
|
||||
ResponsesApiNamespaceTool::Custom(custom),
|
||||
],
|
||||
}),
|
||||
vec![
|
||||
ToolName::namespaced("example", "lookup"),
|
||||
ToolName::namespaced("example", "patch"),
|
||||
],
|
||||
}),
|
||||
),
|
||||
(
|
||||
ToolSpec::Namespace(ResponsesApiNamespace {
|
||||
name: "empty".to_string(),
|
||||
description: String::new(),
|
||||
tools: Vec::new(),
|
||||
}),
|
||||
Vec::new(),
|
||||
),
|
||||
] {
|
||||
let expected =
|
||||
let normalized =
|
||||
ToolSearchInfo::from_spec("query".to_string(), spec.clone(), /*source_info*/ None)
|
||||
.unwrap()
|
||||
.entry
|
||||
.to_loadable_spec();
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
normalized.entry.tool_names().collect::<Vec<_>>(),
|
||||
expected_names
|
||||
);
|
||||
let expected = normalized.entry.to_loadable_spec();
|
||||
let spec = Arc::new(spec);
|
||||
let weak = Arc::downgrade(&spec);
|
||||
let info = ToolSearchInfo::from_shared_spec(
|
||||
@@ -57,6 +80,7 @@ fn shared_search_specs_preserve_results_and_release_the_source() {
|
||||
"search retains the original spec rather than a deep copy"
|
||||
);
|
||||
assert_eq!(info.entry.to_loadable_spec(), expected);
|
||||
assert_eq!(info.entry.tool_names().collect::<Vec<_>>(), expected_names);
|
||||
drop(info);
|
||||
assert!(
|
||||
weak.upgrade().is_none(),
|
||||
|
||||
Reference in New Issue
Block a user