mirror of
https://github.com/openai/codex.git
synced 2026-10-29 19:08:43 +00:00
## 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
310 lines
11 KiB
Rust
310 lines
11 KiB
Rust
//! Tests catalog exec template rendering and runtime description composition.
|
|
|
|
use super::CodeModeToolKind;
|
|
use super::DEFERRED_NESTED_TOOLS_GUIDANCE;
|
|
use super::DeferredToolDiscovery;
|
|
use super::ImageDetailVisibility;
|
|
use super::LEGACY_IMAGE_HELPER_DESCRIPTION;
|
|
use super::MCP_TYPESCRIPT_PREAMBLE;
|
|
use super::TOOL_SEARCH_GUIDANCE;
|
|
use super::ToolDefinition;
|
|
use super::UNIFIED_IMAGE_HELPER_DESCRIPTION;
|
|
use super::build_exec_tool_description;
|
|
use codex_protocol::ToolName;
|
|
use codex_protocol::openai_models::CodeModeToolMessages;
|
|
use codex_protocol::openai_models::ToolMessage;
|
|
use pretty_assertions::assert_eq;
|
|
use serde_json::json;
|
|
use std::collections::BTreeMap;
|
|
|
|
#[test]
|
|
fn exec_override_renders_only_known_literal_placeholders() {
|
|
for (image_detail_visibility, image_helper) in [
|
|
(
|
|
ImageDetailVisibility::Visible,
|
|
LEGACY_IMAGE_HELPER_DESCRIPTION,
|
|
),
|
|
(
|
|
ImageDetailVisibility::Hidden,
|
|
UNIFIED_IMAGE_HELPER_DESCRIPTION,
|
|
),
|
|
] {
|
|
let description = build_exec_tool_description(
|
|
&[],
|
|
&[],
|
|
&BTreeMap::new(),
|
|
/*default_exec_yield_time_ms*/ 4567,
|
|
/*code_mode_only*/ false,
|
|
image_detail_visibility,
|
|
DeferredToolDiscovery::Catalog,
|
|
Some(&CodeModeToolMessages {
|
|
exec: Some(ToolMessage {
|
|
description: Some(" \nDefaults to 10000 ms. {{ default_exec_yield_time_ms }} ms.\n{{ image_helper }}\n{{ unknown }} {{default_exec_yield_time_ms}}\t ".to_string()),
|
|
..Default::default()
|
|
}),
|
|
..Default::default()
|
|
}),
|
|
);
|
|
|
|
assert_eq!(
|
|
description,
|
|
format!(
|
|
" \nDefaults to 10000 ms. 4567 ms.\n{image_helper}\n{{{{ unknown }}}} {{{{default_exec_yield_time_ms}}}}\t \n\n{DEFERRED_NESTED_TOOLS_GUIDANCE}"
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn exec_override_preserves_empty_and_whitespace_only_text() {
|
|
for description_override in ["", " \n\t "] {
|
|
assert_eq!(
|
|
build_exec_tool_description(
|
|
&[],
|
|
&[],
|
|
&BTreeMap::new(),
|
|
crate::DEFAULT_EXEC_YIELD_TIME_MS,
|
|
/*code_mode_only*/ true,
|
|
ImageDetailVisibility::Visible,
|
|
DeferredToolDiscovery::Catalog,
|
|
Some(&CodeModeToolMessages {
|
|
exec: Some(ToolMessage {
|
|
description: Some(description_override.to_string()),
|
|
..Default::default()
|
|
}),
|
|
deferred_nested_tools_guidance: Some(
|
|
"No deferred tools; omit this.".to_string()
|
|
),
|
|
mcp_typescript_preamble: Some("No MCP tools; omit this.".to_string()),
|
|
..Default::default()
|
|
}),
|
|
),
|
|
[
|
|
description_override,
|
|
"No deferred tools; omit this.",
|
|
"Shared MCP Types:\n```ts\nNo MCP tools; omit this.\n```"
|
|
]
|
|
.into_iter()
|
|
.filter(|section| !section.is_empty())
|
|
.collect::<Vec<_>>()
|
|
.join("\n\n"),
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn exec_override_preserves_runtime_sections() {
|
|
let enabled_tools = [ToolDefinition {
|
|
name: "alpha".to_string(),
|
|
tool_name: ToolName::plain("alpha"),
|
|
description: "First tool".to_string(),
|
|
kind: CodeModeToolKind::Function,
|
|
input_schema: None,
|
|
input_schema_max_bytes: None,
|
|
output_schema: None,
|
|
}];
|
|
let deferred_tools = [ToolDefinition {
|
|
name: "mcp__sample__beta".to_string(),
|
|
tool_name: ToolName::namespaced("mcp__sample__", "beta"),
|
|
description: "Deferred tool".to_string(),
|
|
kind: CodeModeToolKind::Function,
|
|
input_schema: None,
|
|
input_schema_max_bytes: None,
|
|
output_schema: Some(json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"content": { "type": "array", "items": { "type": "object" } },
|
|
"isError": { "type": "boolean" },
|
|
"_meta": { "type": "object" }
|
|
}
|
|
})),
|
|
}];
|
|
let declaration = "### `alpha`\nFirst tool\n\nexec tool declaration:\n```ts\ndeclare const tools: { alpha(args: unknown): Promise<unknown>; };\n```";
|
|
for (code_mode_only, guidance, preamble, expected) in [
|
|
(true, Some(""), Some(""), declaration.to_string()),
|
|
(
|
|
true,
|
|
None,
|
|
Some(""),
|
|
format!("{DEFERRED_NESTED_TOOLS_GUIDANCE}\n\n{declaration}"),
|
|
),
|
|
(
|
|
true,
|
|
Some(""),
|
|
None,
|
|
format!("Shared MCP Types:\n```ts\n{MCP_TYPESCRIPT_PREAMBLE}\n```\n\n{declaration}"),
|
|
),
|
|
(
|
|
true,
|
|
Some(" {{ image_helper }}\n"),
|
|
Some(" {{ default_exec_yield_time_ms }}\n"),
|
|
format!(
|
|
" {{{{ image_helper }}}}\n\n\nShared MCP Types:\n```ts\n {{{{ default_exec_yield_time_ms }}}}\n\n```\n\n{declaration}"
|
|
),
|
|
),
|
|
(
|
|
false,
|
|
Some("Catalog discovery."),
|
|
Some("No types outside Code Mode Only."),
|
|
"Catalog discovery.".to_string(),
|
|
),
|
|
] {
|
|
assert_eq!(
|
|
build_exec_tool_description(
|
|
&enabled_tools,
|
|
&deferred_tools,
|
|
&BTreeMap::new(),
|
|
crate::DEFAULT_EXEC_YIELD_TIME_MS,
|
|
code_mode_only,
|
|
ImageDetailVisibility::Visible,
|
|
DeferredToolDiscovery::Catalog,
|
|
Some(&CodeModeToolMessages {
|
|
exec: Some(ToolMessage {
|
|
description: Some(String::new()),
|
|
..Default::default()
|
|
}),
|
|
deferred_nested_tools_guidance: guidance.map(str::to_string),
|
|
mcp_typescript_preamble: preamble.map(str::to_string),
|
|
..Default::default()
|
|
}),
|
|
),
|
|
expected,
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn ranked_search_guidance_honors_overrides() {
|
|
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,
|
|
}];
|
|
for code_mode_only in [false, true] {
|
|
for (guidance, expected) in [
|
|
(
|
|
None,
|
|
if code_mode_only {
|
|
DEFERRED_NESTED_TOOLS_GUIDANCE.to_string()
|
|
} else {
|
|
format!("{DEFERRED_NESTED_TOOLS_GUIDANCE}\n\n{TOOL_SEARCH_GUIDANCE}")
|
|
},
|
|
),
|
|
(Some(""), String::new()),
|
|
(Some("Catalog discovery."), "Catalog discovery.".to_string()),
|
|
] {
|
|
let description = build_exec_tool_description(
|
|
&[],
|
|
&deferred_tools,
|
|
&BTreeMap::new(),
|
|
crate::DEFAULT_EXEC_YIELD_TIME_MS,
|
|
code_mode_only,
|
|
ImageDetailVisibility::Visible,
|
|
DeferredToolDiscovery::RankedSearch,
|
|
Some(&CodeModeToolMessages {
|
|
exec: Some(ToolMessage {
|
|
description: Some(String::new()),
|
|
..Default::default()
|
|
}),
|
|
deferred_nested_tools_guidance: guidance.map(str::to_string),
|
|
mcp_typescript_preamble: Some(String::new()),
|
|
..Default::default()
|
|
}),
|
|
);
|
|
assert_eq!(description, expected);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn mcp_types_stay_stable_when_a_deferred_tool_changes_its_output_schema() {
|
|
let mut tool = ToolDefinition {
|
|
name: "sample".to_string(),
|
|
tool_name: ToolName::plain("sample"),
|
|
description: "Deferred tool".to_string(),
|
|
kind: CodeModeToolKind::Function,
|
|
input_schema: None,
|
|
input_schema_max_bytes: None,
|
|
output_schema: None,
|
|
};
|
|
let render = |tool: &ToolDefinition, preamble: Option<&str>| {
|
|
build_exec_tool_description(
|
|
&[],
|
|
std::slice::from_ref(tool),
|
|
&BTreeMap::new(),
|
|
crate::DEFAULT_EXEC_YIELD_TIME_MS,
|
|
/*code_mode_only*/ true,
|
|
ImageDetailVisibility::Visible,
|
|
DeferredToolDiscovery::Catalog,
|
|
Some(&CodeModeToolMessages {
|
|
mcp_typescript_preamble: preamble.map(str::to_string),
|
|
..Default::default()
|
|
}),
|
|
)
|
|
};
|
|
// Keep one deferred tool throughout to isolate this from the independent
|
|
// discovery guidance transition when the entire deferred catalog empties.
|
|
for preamble in [None, Some("type CustomMcpResult = string;"), Some("")] {
|
|
tool.output_schema = None;
|
|
let before = render(&tool, preamble);
|
|
tool.output_schema = Some(json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"content": { "type": "array", "items": { "type": "object" } },
|
|
"isError": { "type": "boolean" },
|
|
"_meta": { "type": "object" }
|
|
}
|
|
}));
|
|
assert_eq!(before, render(&tool, preamble));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn nested_guidance_survives_catalog_changes_and_respects_overrides() {
|
|
let deferred_tools = [ToolDefinition {
|
|
name: "deferred_echo".to_string(),
|
|
tool_name: ToolName::plain("deferred_echo"),
|
|
description: "Echo a value.".to_string(),
|
|
kind: CodeModeToolKind::Function,
|
|
input_schema: None,
|
|
input_schema_max_bytes: None,
|
|
output_schema: None,
|
|
}];
|
|
let render = |tools: &[ToolDefinition], code_mode_only, guidance: Option<&str>| {
|
|
build_exec_tool_description(
|
|
&[],
|
|
tools,
|
|
&BTreeMap::new(),
|
|
crate::DEFAULT_EXEC_YIELD_TIME_MS,
|
|
code_mode_only,
|
|
ImageDetailVisibility::Visible,
|
|
DeferredToolDiscovery::Catalog,
|
|
Some(&CodeModeToolMessages {
|
|
exec: Some(ToolMessage {
|
|
description: Some(String::new()),
|
|
..Default::default()
|
|
}),
|
|
deferred_nested_tools_guidance: guidance.map(str::to_string),
|
|
mcp_typescript_preamble: Some(String::new()),
|
|
..Default::default()
|
|
}),
|
|
)
|
|
};
|
|
for code_mode_only in [false, true] {
|
|
for (guidance, expected) in [
|
|
(None, DEFERRED_NESTED_TOOLS_GUIDANCE),
|
|
(Some("Catalog discovery."), "Catalog discovery."),
|
|
(Some(""), ""),
|
|
] {
|
|
let prompt = render(&[], code_mode_only, guidance);
|
|
assert_eq!(prompt, expected);
|
|
assert_eq!(prompt, render(&deferred_tools, code_mode_only, guidance));
|
|
}
|
|
}
|
|
assert!(DEFERRED_NESTED_TOOLS_GUIDANCE.contains("Tool availability can change between calls"));
|
|
}
|