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:
Alex Zamoshchin
2026-10-06 00:36:25 +00:00
committed by copyberry
parent 5ad6891696
commit 4d15794336
23 changed files with 1125 additions and 248 deletions
+83 -22
View File
@@ -17,6 +17,7 @@ const DEFERRED_NESTED_TOOLS_GUIDANCE: &str = r#"Some deferred nested tools may b
To find one, filter `ALL_TOOLS` by `name` and `description`.
Tool availability can change between calls."#;
pub const TOOL_SEARCH_GUIDANCE: &str = r#"Use `await tools.tool_search({query: "...", limit: 8})` for BM25-ranked discovery of deferred tools (up to `limit` matches; 8 by default). It returns `{tools: [{name, description}]}` with callable names and full TypeScript declarations. Print results with `text()` and inspect unfamiliar declarations before calling `tools[result.name](arguments)` in a later execution."#;
const LEGACY_IMAGE_HELPER_DESCRIPTION: &str = r#"`image(imageUrlOrItem: string | { image_url: string; detail?: "auto" | "low" | "high" | "original" | null } | ImageContent, detail?: "auto" | "low" | "high" | "original" | null)`: Appends an image item. `image_url` should be a base64-encoded `data:` URL. To forward an MCP tool image, pass an individual `ImageContent` block from `result.content`, for example `image(result.content[0])`. MCP image blocks may request detail with `_meta: { "codex/imageDetail": "original" }`. When provided, the second `detail` argument overrides any detail embedded in the first argument."#;
const UNIFIED_IMAGE_HELPER_DESCRIPTION: &str = r#"`image(imageUrlOrItem: string | { image_url: string } | ImageContent)`: Appends an image item. `image_url` should be a base64-encoded `data:` URL. To forward an MCP tool image, pass an individual `ImageContent` block from `result.content`, for example `image(result.content[0])`."#;
const EXEC_DESCRIPTION_TEMPLATE: &str = r#"Run JavaScript code to orchestrate/compose tool calls
@@ -268,6 +269,13 @@ pub enum ImageDetailVisibility {
Hidden,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DeferredToolDiscovery {
Catalog,
RankedSearch,
}
#[allow(clippy::too_many_arguments)]
pub fn build_exec_tool_description(
enabled_tools: &[ToolDefinition],
_deferred_tools: &[ToolDefinition],
@@ -275,6 +283,7 @@ pub fn build_exec_tool_description(
default_exec_yield_time_ms: u64,
code_mode_only: bool,
image_detail_visibility: ImageDetailVisibility,
deferred_tool_discovery: DeferredToolDiscovery,
messages: Option<&CodeModeToolMessages>,
) -> String {
let mut sections = Vec::new();
@@ -294,9 +303,18 @@ pub fn build_exec_tool_description(
if !description.is_empty() {
sections.push(description);
}
let default_guidance = match deferred_tool_discovery {
// In Code Mode Only, the enabled search tool's description is rendered below.
DeferredToolDiscovery::RankedSearch if !code_mode_only => {
format!("{DEFERRED_NESTED_TOOLS_GUIDANCE}\n\n{TOOL_SEARCH_GUIDANCE}")
}
DeferredToolDiscovery::Catalog | DeferredToolDiscovery::RankedSearch => {
DEFERRED_NESTED_TOOLS_GUIDANCE.to_string()
}
};
let guidance = messages
.and_then(|messages| messages.deferred_nested_tools_guidance.as_deref())
.unwrap_or(DEFERRED_NESTED_TOOLS_GUIDANCE);
.unwrap_or(&default_guidance);
if !guidance.is_empty() {
sections.push(guidance.to_string());
}
@@ -529,6 +547,7 @@ mod description_override_tests;
#[cfg(test)]
mod tests {
use super::CodeModeToolKind;
use super::DeferredToolDiscovery;
use super::ImageDetailVisibility;
use super::ParsedExecSource;
use super::ToolDefinition;
@@ -737,6 +756,7 @@ mod tests {
crate::DEFAULT_EXEC_YIELD_TIME_MS,
/*code_mode_only*/ true,
ImageDetailVisibility::Visible,
DeferredToolDiscovery::Catalog,
/*messages*/ None,
);
assert!(description.contains(
@@ -755,6 +775,7 @@ bar"
crate::DEFAULT_EXEC_YIELD_TIME_MS,
/*code_mode_only*/ false,
ImageDetailVisibility::Visible,
DeferredToolDiscovery::Catalog,
/*messages*/ None,
);
assert!(description.contains("`audio(audioUrlOrItem:"));
@@ -813,6 +834,7 @@ bar"
crate::DEFAULT_EXEC_YIELD_TIME_MS,
/*code_mode_only*/ true,
ImageDetailVisibility::Visible,
DeferredToolDiscovery::Catalog,
/*messages*/ None,
);
assert_eq!(description.matches("## mcp__sample").count(), 1);
@@ -857,6 +879,7 @@ bar"
crate::DEFAULT_EXEC_YIELD_TIME_MS,
/*code_mode_only*/ true,
ImageDetailVisibility::Visible,
DeferredToolDiscovery::Catalog,
/*messages*/ None,
);
@@ -963,6 +986,7 @@ bar"
crate::DEFAULT_EXEC_YIELD_TIME_MS,
/*code_mode_only*/ true,
ImageDetailVisibility::Visible,
DeferredToolDiscovery::Catalog,
/*messages*/ None,
);
@@ -1002,6 +1026,7 @@ bar"
crate::DEFAULT_EXEC_YIELD_TIME_MS,
/*code_mode_only*/ true,
ImageDetailVisibility::Visible,
DeferredToolDiscovery::Catalog,
/*messages*/ None,
);
@@ -1011,27 +1036,63 @@ bar"
}
#[test]
fn exec_description_mentions_deferred_nested_tools_when_available() {
let description = build_exec_tool_description(
&[],
&[ToolDefinition {
name: "deferred_tool".to_string(),
tool_name: ToolName::plain("deferred_tool"),
description: "Deferred tool".to_string(),
kind: CodeModeToolKind::Function,
input_schema: None,
input_schema_max_bytes: None,
output_schema: None,
}],
&BTreeMap::new(),
crate::DEFAULT_EXEC_YIELD_TIME_MS,
/*code_mode_only*/ false,
ImageDetailVisibility::Visible,
/*messages*/ None,
);
fn exec_description_preserves_discovery_guidance_across_catalog_changes() {
let deferred_tools = [ToolDefinition {
name: "deferred_tool".to_string(),
tool_name: ToolName::plain("deferred_tool"),
description: "Deferred tool".to_string(),
kind: CodeModeToolKind::Function,
input_schema: None,
input_schema_max_bytes: None,
output_schema: None,
}];
let search_tool = ToolDefinition {
name: "tool_search".to_string(),
tool_name: ToolName::plain("tool_search"),
description: super::TOOL_SEARCH_GUIDANCE.to_string(),
kind: CodeModeToolKind::Function,
input_schema: None,
input_schema_max_bytes: None,
output_schema: None,
};
for code_mode_only in [false, true] {
for discovery in [
DeferredToolDiscovery::Catalog,
DeferredToolDiscovery::RankedSearch,
] {
let enabled_tools = match discovery {
DeferredToolDiscovery::Catalog => Vec::new(),
DeferredToolDiscovery::RankedSearch => vec![search_tool.clone()],
};
let description = build_exec_tool_description(
&enabled_tools,
&deferred_tools,
&BTreeMap::new(),
crate::DEFAULT_EXEC_YIELD_TIME_MS,
code_mode_only,
ImageDetailVisibility::Visible,
discovery,
/*messages*/ None,
);
assert!(description.contains("Some deferred nested tools may be omitted"));
assert!(description.contains("filter `ALL_TOOLS` by `name` and `description`"));
assert_eq!(
description.matches("await tools.tool_search(").count(),
usize::from(discovery == DeferredToolDiscovery::RankedSearch),
);
assert!(description.contains("Some deferred nested tools may be omitted"));
assert!(description.contains("filter `ALL_TOOLS` by `name` and `description`"));
assert!(!description.contains("do not print the full `ALL_TOOLS` array"));
let empty_catalog_description = build_exec_tool_description(
&enabled_tools,
&[],
&BTreeMap::new(),
crate::DEFAULT_EXEC_YIELD_TIME_MS,
code_mode_only,
ImageDetailVisibility::Visible,
discovery,
/*messages*/ None,
);
assert_eq!(description, empty_catalog_description);
}
}
}
}