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:
parent
a4d884c767
commit
12ee9eb6e0
4 changed files with 699 additions and 79 deletions
|
|
@ -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),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
388
codex-rs/core/src/tools/code_mode_description.rs
Normal file
388
codex-rs/core/src/tools/code_mode_description.rs
Normal 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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
pub mod code_mode;
|
||||
pub(crate) mod code_mode_description;
|
||||
pub mod context;
|
||||
pub mod events;
|
||||
pub(crate) mod handlers;
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue