Add snippets annotated with types to tools when code mode enabled (#14284)

Main purpose is for code mode to understand the return type.
This commit is contained in:
pakrym-oai 2026-03-10 19:20:15 -07:00 committed by Michael Bolin
parent a4d884c767
commit 12ee9eb6e0
4 changed files with 699 additions and 79 deletions

View file

@ -10,6 +10,7 @@ use crate::exec_env::create_env;
use crate::features::Feature;
use crate::function_tool::FunctionCallError;
use crate::tools::ToolRouter;
use crate::tools::code_mode_description::code_mode_tool_reference;
use crate::tools::context::FunctionToolOutput;
use crate::tools::context::SharedTurnDiffTracker;
use crate::tools::context::ToolPayload;
@ -347,25 +348,6 @@ fn truncate_code_mode_result(
async fn build_enabled_tools(exec: &ExecContext) -> Vec<EnabledTool> {
let router = build_nested_router(exec).await;
let mcp_tool_names = exec
.session
.services
.mcp_connection_manager
.read()
.await
.list_all_tools()
.await
.into_iter()
.map(|(qualified_name, tool_info)| {
(
qualified_name,
(
vec!["mcp".to_string(), tool_info.server_name],
tool_info.tool_name,
),
)
})
.collect::<std::collections::HashMap<_, _>>();
let mut out = Vec::new();
for spec in router.specs() {
let tool_name = spec.name().to_string();
@ -373,16 +355,12 @@ async fn build_enabled_tools(exec: &ExecContext) -> Vec<EnabledTool> {
continue;
}
let (namespace, name) = if let Some((namespace, name)) = mcp_tool_names.get(&tool_name) {
(namespace.clone(), name.clone())
} else {
(Vec::new(), tool_name.clone())
};
let reference = code_mode_tool_reference(&tool_name);
out.push(EnabledTool {
tool_name,
namespace,
name,
namespace: reference.namespace,
name: reference.tool_key,
kind: tool_kind_for_spec(&spec),
});
}

View file

@ -0,0 +1,388 @@
use crate::client_common::tools::ToolSpec;
use crate::mcp::split_qualified_tool_name;
use crate::tools::code_mode::PUBLIC_TOOL_NAME;
use serde_json::Value as JsonValue;
pub(crate) struct CodeModeToolReference {
pub(crate) module_path: String,
pub(crate) namespace: Vec<String>,
pub(crate) tool_key: String,
}
pub(crate) fn code_mode_tool_reference(tool_name: &str) -> CodeModeToolReference {
if let Some((server_name, tool_key)) = split_qualified_tool_name(tool_name) {
let namespace = vec!["mcp".to_string(), server_name];
return CodeModeToolReference {
module_path: format!("tools/{}.js", namespace.join("/")),
namespace,
tool_key,
};
}
CodeModeToolReference {
module_path: "tools.js".to_string(),
namespace: Vec::new(),
tool_key: tool_name.to_string(),
}
}
pub(crate) fn augment_tool_spec_for_code_mode(spec: ToolSpec, code_mode_enabled: bool) -> ToolSpec {
if !code_mode_enabled {
return spec;
}
match spec {
ToolSpec::Function(mut tool) => {
if tool.name != PUBLIC_TOOL_NAME {
tool.description = append_code_mode_sample(
&tool.description,
&tool.name,
"args",
serde_json::to_value(&tool.parameters)
.ok()
.as_ref()
.map(render_json_schema_to_typescript)
.unwrap_or_else(|| "unknown".to_string()),
tool.output_schema
.as_ref()
.map(render_json_schema_to_typescript)
.unwrap_or_else(|| "unknown".to_string()),
);
}
ToolSpec::Function(tool)
}
ToolSpec::Freeform(mut tool) => {
if tool.name != PUBLIC_TOOL_NAME {
tool.description = append_code_mode_sample(
&tool.description,
&tool.name,
"input",
"string".to_string(),
"unknown".to_string(),
);
}
ToolSpec::Freeform(tool)
}
other => other,
}
}
fn append_code_mode_sample(
description: &str,
tool_name: &str,
input_name: &str,
input_type: String,
output_type: String,
) -> String {
let reference = code_mode_tool_reference(tool_name);
let local_name = code_mode_local_name(&reference.tool_key);
format!(
"{description}\n\nCode mode declaration:\n```ts\nimport {{ tools }} from \"{}\";\ndeclare function {local_name}({input_name}: {input_type}): Promise<{output_type}>;\n```",
reference.module_path
)
}
fn code_mode_local_name(tool_key: &str) -> String {
let mut identifier = String::new();
for (index, ch) in tool_key.chars().enumerate() {
let is_valid = if index == 0 {
ch == '_' || ch == '$' || ch.is_ascii_alphabetic()
} else {
ch == '_' || ch == '$' || ch.is_ascii_alphanumeric()
};
if is_valid {
identifier.push(ch);
} else {
identifier.push('_');
}
}
if identifier.is_empty() {
return "tool_call".to_string();
}
if identifier == "tools" {
identifier.push_str("_tool");
}
if identifier
.chars()
.next()
.is_some_and(|ch| ch.is_ascii_digit())
{
identifier.insert(0, '_');
}
identifier
}
fn render_json_schema_to_typescript(schema: &JsonValue) -> String {
render_json_schema_to_typescript_inner(schema, 0)
}
fn render_json_schema_to_typescript_inner(schema: &JsonValue, indent: usize) -> String {
match schema {
JsonValue::Bool(true) => "unknown".to_string(),
JsonValue::Bool(false) => "never".to_string(),
JsonValue::Object(map) => {
if let Some(value) = map.get("const") {
return render_json_schema_literal(value);
}
if let Some(values) = map.get("enum").and_then(serde_json::Value::as_array) {
let rendered = values
.iter()
.map(render_json_schema_literal)
.collect::<Vec<_>>();
if !rendered.is_empty() {
return rendered.join(" | ");
}
}
for key in ["anyOf", "oneOf"] {
if let Some(variants) = map.get(key).and_then(serde_json::Value::as_array) {
let rendered = variants
.iter()
.map(|variant| render_json_schema_to_typescript_inner(variant, indent))
.collect::<Vec<_>>();
if !rendered.is_empty() {
return rendered.join(" | ");
}
}
}
if let Some(variants) = map.get("allOf").and_then(serde_json::Value::as_array) {
let rendered = variants
.iter()
.map(|variant| render_json_schema_to_typescript_inner(variant, indent))
.collect::<Vec<_>>();
if !rendered.is_empty() {
return rendered.join(" & ");
}
}
if let Some(schema_type) = map.get("type") {
if let Some(types) = schema_type.as_array() {
let rendered = types
.iter()
.filter_map(serde_json::Value::as_str)
.map(|schema_type| {
render_json_schema_type_keyword(map, schema_type, indent)
})
.collect::<Vec<_>>();
if !rendered.is_empty() {
return rendered.join(" | ");
}
}
if let Some(schema_type) = schema_type.as_str() {
return render_json_schema_type_keyword(map, schema_type, indent);
}
}
if map.contains_key("properties")
|| map.contains_key("additionalProperties")
|| map.contains_key("required")
{
return render_json_schema_object(map, indent);
}
if map.contains_key("items") || map.contains_key("prefixItems") {
return render_json_schema_array(map, indent);
}
"unknown".to_string()
}
_ => "unknown".to_string(),
}
}
fn render_json_schema_type_keyword(
map: &serde_json::Map<String, JsonValue>,
schema_type: &str,
indent: usize,
) -> String {
match schema_type {
"string" => "string".to_string(),
"number" | "integer" => "number".to_string(),
"boolean" => "boolean".to_string(),
"null" => "null".to_string(),
"array" => render_json_schema_array(map, indent),
"object" => render_json_schema_object(map, indent),
_ => "unknown".to_string(),
}
}
fn render_json_schema_array(map: &serde_json::Map<String, JsonValue>, indent: usize) -> String {
if let Some(items) = map.get("items") {
let item_type = render_json_schema_to_typescript_inner(items, indent + 2);
return format!("Array<{item_type}>");
}
if let Some(items) = map.get("prefixItems").and_then(serde_json::Value::as_array) {
let item_types = items
.iter()
.map(|item| render_json_schema_to_typescript_inner(item, indent + 2))
.collect::<Vec<_>>();
if !item_types.is_empty() {
return format!("[{}]", item_types.join(", "));
}
}
"unknown[]".to_string()
}
fn render_json_schema_object(map: &serde_json::Map<String, JsonValue>, indent: usize) -> String {
let required = map
.get("required")
.and_then(serde_json::Value::as_array)
.map(|items| {
items
.iter()
.filter_map(serde_json::Value::as_str)
.collect::<Vec<_>>()
})
.unwrap_or_default();
let properties = map
.get("properties")
.and_then(serde_json::Value::as_object)
.cloned()
.unwrap_or_default();
let mut sorted_properties = properties.iter().collect::<Vec<_>>();
sorted_properties.sort_unstable_by(|(name_a, _), (name_b, _)| name_a.cmp(name_b));
let mut lines = sorted_properties
.into_iter()
.map(|(name, value)| {
let optional = if required.iter().any(|required_name| required_name == name) {
""
} else {
"?"
};
let property_name = render_json_schema_property_name(name);
let property_type = render_json_schema_to_typescript_inner(value, indent + 2);
format!(
"{}{property_name}{optional}: {property_type};",
" ".repeat(indent + 2)
)
})
.collect::<Vec<_>>();
if let Some(additional_properties) = map.get("additionalProperties") {
let additional_type = match additional_properties {
JsonValue::Bool(true) => Some("unknown".to_string()),
JsonValue::Bool(false) => None,
value => Some(render_json_schema_to_typescript_inner(value, indent + 2)),
};
if let Some(additional_type) = additional_type {
lines.push(format!(
"{}[key: string]: {additional_type};",
" ".repeat(indent + 2)
));
}
} else if properties.is_empty() {
lines.push(format!("{}[key: string]: unknown;", " ".repeat(indent + 2)));
}
if lines.is_empty() {
return "{}".to_string();
}
format!("{{\n{}\n{}}}", lines.join("\n"), " ".repeat(indent))
}
fn render_json_schema_property_name(name: &str) -> String {
if code_mode_local_name(name) == name {
name.to_string()
} else {
serde_json::to_string(name).unwrap_or_else(|_| format!("\"{}\"", name.replace('"', "\\\"")))
}
}
fn render_json_schema_literal(value: &JsonValue) -> String {
serde_json::to_string(value).unwrap_or_else(|_| "unknown".to_string())
}
#[cfg(test)]
mod tests {
use super::render_json_schema_to_typescript;
use pretty_assertions::assert_eq;
use serde_json::json;
#[test]
fn render_json_schema_to_typescript_renders_object_properties() {
let schema = json!({
"type": "object",
"properties": {
"path": {"type": "string"},
"recursive": {"type": "boolean"}
},
"required": ["path"],
"additionalProperties": false
});
assert_eq!(
render_json_schema_to_typescript(&schema),
"{\n path: string;\n recursive?: boolean;\n}"
);
}
#[test]
fn render_json_schema_to_typescript_renders_anyof_unions() {
let schema = json!({
"anyOf": [
{"const": "pending"},
{"const": "done"},
{"type": "number"}
]
});
assert_eq!(
render_json_schema_to_typescript(&schema),
"\"pending\" | \"done\" | number"
);
}
#[test]
fn render_json_schema_to_typescript_renders_additional_properties() {
let schema = json!({
"type": "object",
"properties": {
"tags": {
"type": "array",
"items": {"type": "string"}
}
},
"additionalProperties": {"type": "integer"}
});
assert_eq!(
render_json_schema_to_typescript(&schema),
"{\n tags?: Array<string>;\n [key: string]: number;\n}"
);
}
#[test]
fn render_json_schema_to_typescript_sorts_object_properties() {
let schema = json!({
"type": "object",
"properties": {
"structuredContent": {"type": "string"},
"_meta": {"type": "string"},
"isError": {"type": "boolean"},
"content": {"type": "array", "items": {"type": "string"}}
},
"required": ["content"]
});
assert_eq!(
render_json_schema_to_typescript(&schema),
"{\n _meta?: string;\n content: Array<string>;\n isError?: boolean;\n structuredContent?: string;\n}"
);
}
}

View file

@ -1,4 +1,5 @@
pub mod code_mode;
pub(crate) mod code_mode_description;
pub mod context;
pub mod events;
pub(crate) mod handlers;

View file

@ -8,6 +8,7 @@ use crate::features::Features;
use crate::mcp_connection_manager::ToolInfo;
use crate::models_manager::collaboration_mode_presets::CollaborationModesConfig;
use crate::tools::code_mode::PUBLIC_TOOL_NAME;
use crate::tools::code_mode_description::augment_tool_spec_for_code_mode;
use crate::tools::handlers::PLAN_TOOL;
use crate::tools::handlers::SEARCH_TOOL_BM25_DEFAULT_LIMIT;
use crate::tools::handlers::SEARCH_TOOL_BM25_TOOL_NAME;
@ -1764,6 +1765,20 @@ pub fn create_tools_json_for_responses_api(
Ok(tools_json)
}
fn push_tool_spec(
builder: &mut ToolRegistryBuilder,
spec: ToolSpec,
supports_parallel_tool_calls: bool,
code_mode_enabled: bool,
) {
let spec = augment_tool_spec_for_code_mode(spec, code_mode_enabled);
if supports_parallel_tool_calls {
builder.push_spec_with_parallel_support(spec, true);
} else {
builder.push_spec(spec);
}
}
pub(crate) fn mcp_tool_to_openai_tool(
fully_qualified_name: String,
tool: rmcp::model::Tool,
@ -2031,26 +2046,45 @@ pub(crate) fn build_specs(
.collect::<Vec<_>>();
enabled_tool_names.sort();
enabled_tool_names.dedup();
builder.push_spec(create_code_mode_tool(&enabled_tool_names));
push_tool_spec(
&mut builder,
create_code_mode_tool(&enabled_tool_names),
false,
config.code_mode_enabled,
);
builder.register_handler(PUBLIC_TOOL_NAME, code_mode_handler);
}
match &config.shell_type {
ConfigShellToolType::Default => {
builder.push_spec_with_parallel_support(
push_tool_spec(
&mut builder,
create_shell_tool(request_permission_enabled),
true,
config.code_mode_enabled,
);
}
ConfigShellToolType::Local => {
builder.push_spec_with_parallel_support(ToolSpec::LocalShell {}, true);
push_tool_spec(
&mut builder,
ToolSpec::LocalShell {},
true,
config.code_mode_enabled,
);
}
ConfigShellToolType::UnifiedExec => {
builder.push_spec_with_parallel_support(
push_tool_spec(
&mut builder,
create_exec_command_tool(config.allow_login_shell, request_permission_enabled),
true,
config.code_mode_enabled,
);
push_tool_spec(
&mut builder,
create_write_stdin_tool(),
false,
config.code_mode_enabled,
);
builder.push_spec(create_write_stdin_tool());
builder.register_handler("exec_command", unified_exec_handler.clone());
builder.register_handler("write_stdin", unified_exec_handler);
}
@ -2058,9 +2092,11 @@ pub(crate) fn build_specs(
// Do nothing.
}
ConfigShellToolType::ShellCommand => {
builder.push_spec_with_parallel_support(
push_tool_spec(
&mut builder,
create_shell_command_tool(config.allow_login_shell, request_permission_enabled),
true,
config.code_mode_enabled,
);
}
}
@ -2074,49 +2110,104 @@ pub(crate) fn build_specs(
}
if mcp_tools.is_some() {
builder.push_spec_with_parallel_support(create_list_mcp_resources_tool(), true);
builder.push_spec_with_parallel_support(create_list_mcp_resource_templates_tool(), true);
builder.push_spec_with_parallel_support(create_read_mcp_resource_tool(), true);
push_tool_spec(
&mut builder,
create_list_mcp_resources_tool(),
true,
config.code_mode_enabled,
);
push_tool_spec(
&mut builder,
create_list_mcp_resource_templates_tool(),
true,
config.code_mode_enabled,
);
push_tool_spec(
&mut builder,
create_read_mcp_resource_tool(),
true,
config.code_mode_enabled,
);
builder.register_handler("list_mcp_resources", mcp_resource_handler.clone());
builder.register_handler("list_mcp_resource_templates", mcp_resource_handler.clone());
builder.register_handler("read_mcp_resource", mcp_resource_handler);
}
builder.push_spec(PLAN_TOOL.clone());
push_tool_spec(
&mut builder,
PLAN_TOOL.clone(),
false,
config.code_mode_enabled,
);
builder.register_handler("update_plan", plan_handler);
if config.js_repl_enabled {
builder.push_spec(create_js_repl_tool());
builder.push_spec(create_js_repl_reset_tool());
push_tool_spec(
&mut builder,
create_js_repl_tool(),
false,
config.code_mode_enabled,
);
push_tool_spec(
&mut builder,
create_js_repl_reset_tool(),
false,
config.code_mode_enabled,
);
builder.register_handler("js_repl", js_repl_handler);
builder.register_handler("js_repl_reset", js_repl_reset_handler);
}
if config.request_user_input {
builder.push_spec(create_request_user_input_tool(CollaborationModesConfig {
default_mode_request_user_input: config.default_mode_request_user_input,
}));
push_tool_spec(
&mut builder,
create_request_user_input_tool(CollaborationModesConfig {
default_mode_request_user_input: config.default_mode_request_user_input,
}),
false,
config.code_mode_enabled,
);
builder.register_handler("request_user_input", request_user_input_handler);
}
if config.request_permissions_tool_enabled {
builder.push_spec(create_request_permissions_tool());
push_tool_spec(
&mut builder,
create_request_permissions_tool(),
false,
config.code_mode_enabled,
);
builder.register_handler("request_permissions", request_permissions_handler);
}
if config.search_tool {
let app_tools = app_tools.unwrap_or_default();
builder.push_spec_with_parallel_support(create_search_tool_bm25_tool(&app_tools), true);
push_tool_spec(
&mut builder,
create_search_tool_bm25_tool(&app_tools),
true,
config.code_mode_enabled,
);
builder.register_handler(SEARCH_TOOL_BM25_TOOL_NAME, search_tool_handler);
}
if let Some(apply_patch_tool_type) = &config.apply_patch_tool_type {
match apply_patch_tool_type {
ApplyPatchToolType::Freeform => {
builder.push_spec(create_apply_patch_freeform_tool());
push_tool_spec(
&mut builder,
create_apply_patch_freeform_tool(),
false,
config.code_mode_enabled,
);
}
ApplyPatchToolType::Function => {
builder.push_spec(create_apply_patch_json_tool());
push_tool_spec(
&mut builder,
create_apply_patch_json_tool(),
false,
config.code_mode_enabled,
);
}
}
builder.register_handler("apply_patch", apply_patch_handler);
@ -2127,7 +2218,12 @@ pub(crate) fn build_specs(
.contains(&"grep_files".to_string())
{
let grep_files_handler = Arc::new(GrepFilesHandler);
builder.push_spec_with_parallel_support(create_grep_files_tool(), true);
push_tool_spec(
&mut builder,
create_grep_files_tool(),
true,
config.code_mode_enabled,
);
builder.register_handler("grep_files", grep_files_handler);
}
@ -2136,7 +2232,12 @@ pub(crate) fn build_specs(
.contains(&"read_file".to_string())
{
let read_file_handler = Arc::new(ReadFileHandler);
builder.push_spec_with_parallel_support(create_read_file_tool(), true);
push_tool_spec(
&mut builder,
create_read_file_tool(),
true,
config.code_mode_enabled,
);
builder.register_handler("read_file", read_file_handler);
}
@ -2146,7 +2247,12 @@ pub(crate) fn build_specs(
.any(|tool| tool == "list_dir")
{
let list_dir_handler = Arc::new(ListDirHandler);
builder.push_spec_with_parallel_support(create_list_dir_tool(), true);
push_tool_spec(
&mut builder,
create_list_dir_tool(),
true,
config.code_mode_enabled,
);
builder.register_handler("list_dir", list_dir_handler);
}
@ -2155,7 +2261,12 @@ pub(crate) fn build_specs(
.contains(&"test_sync_tool".to_string())
{
let test_sync_handler = Arc::new(TestSyncHandler);
builder.push_spec_with_parallel_support(create_test_sync_tool(), true);
push_tool_spec(
&mut builder,
create_test_sync_tool(),
true,
config.code_mode_enabled,
);
builder.register_handler("test_sync_tool", test_sync_handler);
}
@ -2176,45 +2287,90 @@ pub(crate) fn build_specs(
),
};
builder.push_spec(ToolSpec::WebSearch {
external_web_access: Some(external_web_access),
filters: config
.web_search_config
.as_ref()
.and_then(|cfg| cfg.filters.clone().map(Into::into)),
user_location: config
.web_search_config
.as_ref()
.and_then(|cfg| cfg.user_location.clone().map(Into::into)),
search_context_size: config
.web_search_config
.as_ref()
.and_then(|cfg| cfg.search_context_size),
search_content_types,
});
push_tool_spec(
&mut builder,
ToolSpec::WebSearch {
external_web_access: Some(external_web_access),
filters: config
.web_search_config
.as_ref()
.and_then(|cfg| cfg.filters.clone().map(Into::into)),
user_location: config
.web_search_config
.as_ref()
.and_then(|cfg| cfg.user_location.clone().map(Into::into)),
search_context_size: config
.web_search_config
.as_ref()
.and_then(|cfg| cfg.search_context_size),
search_content_types,
},
false,
config.code_mode_enabled,
);
}
if config.image_gen_tool {
builder.push_spec(ToolSpec::ImageGeneration {
output_format: "png".to_string(),
});
push_tool_spec(
&mut builder,
ToolSpec::ImageGeneration {
output_format: "png".to_string(),
},
false,
config.code_mode_enabled,
);
}
builder.push_spec_with_parallel_support(create_view_image_tool(), true);
push_tool_spec(
&mut builder,
create_view_image_tool(),
true,
config.code_mode_enabled,
);
builder.register_handler("view_image", view_image_handler);
if config.artifact_tools {
builder.push_spec(create_artifacts_tool());
push_tool_spec(
&mut builder,
create_artifacts_tool(),
false,
config.code_mode_enabled,
);
builder.register_handler("artifacts", artifacts_handler);
}
if config.collab_tools {
let multi_agent_handler = Arc::new(MultiAgentHandler);
builder.push_spec(create_spawn_agent_tool(config));
builder.push_spec(create_send_input_tool());
builder.push_spec(create_resume_agent_tool());
builder.push_spec(create_wait_tool());
builder.push_spec(create_close_agent_tool());
push_tool_spec(
&mut builder,
create_spawn_agent_tool(config),
false,
config.code_mode_enabled,
);
push_tool_spec(
&mut builder,
create_send_input_tool(),
false,
config.code_mode_enabled,
);
push_tool_spec(
&mut builder,
create_resume_agent_tool(),
false,
config.code_mode_enabled,
);
push_tool_spec(
&mut builder,
create_wait_tool(),
false,
config.code_mode_enabled,
);
push_tool_spec(
&mut builder,
create_close_agent_tool(),
false,
config.code_mode_enabled,
);
builder.register_handler("spawn_agent", multi_agent_handler.clone());
builder.register_handler("send_input", multi_agent_handler.clone());
builder.register_handler("resume_agent", multi_agent_handler.clone());
@ -2224,10 +2380,20 @@ pub(crate) fn build_specs(
if config.agent_jobs_tools {
let agent_jobs_handler = Arc::new(BatchJobHandler);
builder.push_spec(create_spawn_agents_on_csv_tool());
push_tool_spec(
&mut builder,
create_spawn_agents_on_csv_tool(),
false,
config.code_mode_enabled,
);
builder.register_handler("spawn_agents_on_csv", agent_jobs_handler.clone());
if config.agent_jobs_worker_tools {
builder.push_spec(create_report_agent_job_result_tool());
push_tool_spec(
&mut builder,
create_report_agent_job_result_tool(),
false,
config.code_mode_enabled,
);
builder.register_handler("report_agent_job_result", agent_jobs_handler);
}
}
@ -2239,7 +2405,12 @@ pub(crate) fn build_specs(
for (name, tool) in entries.into_iter() {
match mcp_tool_to_openai_tool(name.clone(), tool.clone()) {
Ok(converted_tool) => {
builder.push_spec(ToolSpec::Function(converted_tool));
push_tool_spec(
&mut builder,
ToolSpec::Function(converted_tool),
false,
config.code_mode_enabled,
);
builder.register_handler(name, mcp_handler.clone());
}
Err(e) => {
@ -2253,7 +2424,12 @@ pub(crate) fn build_specs(
for tool in dynamic_tools {
match dynamic_tool_to_openai_tool(tool) {
Ok(converted_tool) => {
builder.push_spec(ToolSpec::Function(converted_tool));
push_tool_spec(
&mut builder,
ToolSpec::Function(converted_tool),
false,
config.code_mode_enabled,
);
builder.register_handler(tool.name.clone(), dynamic_tool_handler.clone());
}
Err(e) => {
@ -4179,6 +4355,83 @@ Examples of valid command strings:
);
}
#[test]
fn code_mode_augments_builtin_tool_descriptions_with_typed_sample() {
let config = test_config();
let model_info =
ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::CodeMode);
features.enable(Feature::UnifiedExec);
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
features: &features,
web_search_mode: Some(WebSearchMode::Cached),
session_source: SessionSource::Cli,
});
let (tools, _) = build_specs(&tools_config, None, None, &[]).build();
let ToolSpec::Function(ResponsesApiTool { description, .. }) =
&find_tool(&tools, "view_image").spec
else {
panic!("expected function tool");
};
assert_eq!(
description,
"View a local image from the filesystem (only use if given a full filepath by the user, and the image isn't already attached to the thread context within <image ...> tags).\n\nCode mode declaration:\n```ts\nimport { tools } from \"tools.js\";\ndeclare function view_image(args: {\n path: string;\n}): Promise<unknown>;\n```"
);
}
#[test]
fn code_mode_augments_mcp_tool_descriptions_with_namespaced_sample() {
let config = test_config();
let model_info =
ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::CodeMode);
features.enable(Feature::UnifiedExec);
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
features: &features,
web_search_mode: Some(WebSearchMode::Cached),
session_source: SessionSource::Cli,
});
let (tools, _) = build_specs(
&tools_config,
Some(HashMap::from([(
"mcp__sample__echo".to_string(),
mcp_tool(
"echo",
"Echo text",
serde_json::json!({
"type": "object",
"properties": {
"message": {"type": "string"}
},
"required": ["message"],
"additionalProperties": false
}),
),
)])),
None,
&[],
)
.build();
let ToolSpec::Function(ResponsesApiTool { description, .. }) =
&find_tool(&tools, "mcp__sample__echo").spec
else {
panic!("expected function tool");
};
assert_eq!(
description,
"Echo text\n\nCode mode declaration:\n```ts\nimport { tools } from \"tools/mcp/sample.js\";\ndeclare function echo(args: {\n message: string;\n}): Promise<{\n _meta?: unknown;\n content: Array<unknown>;\n isError?: boolean;\n structuredContent?: unknown;\n}>;\n```"
);
}
#[test]
fn chat_tools_include_top_level_name() {
let properties =