Skip to main content

vtcode_core/llm/provider/
tool.rs

1use serde::{Deserialize, Serialize};
2use serde_json::{Map, Value, json};
3
4/// Tool search algorithm for Anthropic's advanced-tool-use beta
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
6#[serde(rename_all = "lowercase")]
7pub enum ToolSearchAlgorithm {
8    /// Regex-based search using Python re.search() syntax
9    #[default]
10    Regex,
11    /// BM25-based natural language search
12    Bm25,
13}
14
15impl std::fmt::Display for ToolSearchAlgorithm {
16    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17        match self {
18            Self::Regex => write!(f, "regex"),
19            Self::Bm25 => write!(f, "bm25"),
20        }
21    }
22}
23
24impl std::str::FromStr for ToolSearchAlgorithm {
25    type Err = String;
26
27    fn from_str(s: &str) -> Result<Self, Self::Err> {
28        match s.to_lowercase().as_str() {
29            "regex" => Ok(Self::Regex),
30            "bm25" => Ok(Self::Bm25),
31            _ => Err(format!("Unknown tool search algorithm: {}", s)),
32        }
33    }
34}
35
36/// Universal tool definition that matches OpenAI/Anthropic/Gemini specifications
37/// Based on official API documentation from Context7
38#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
39pub struct ToolDefinition {
40    /// The type of tool: "function", "apply_patch" (GPT-5.1), "shell" (GPT-5.1), or "custom" (GPT-5 freeform)
41    /// Also supports provider-native and hosted tool types like:
42    /// - "tool_search" (OpenAI hosted tool search)
43    /// - "file_search" and "mcp" (OpenAI Responses hosted tools)
44    /// - Anthropic tool search revisions:
45    /// - "tool_search_tool_regex_20251119", "tool_search_tool_bm25_20251119"
46    /// - "web_search_20260209" (and other web_search_* revisions)
47    /// - "code_execution_20250825" (and other code_execution_* revisions)
48    /// - "memory_20250818" (and other memory_* revisions)
49    #[serde(rename = "type")]
50    pub tool_type: String,
51
52    /// Function definition containing name, description, and parameters
53    /// Used for "function", "apply_patch", and "custom" types
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub function: Option<FunctionDefinition>,
56
57    /// Restricts which Anthropic callers can invoke this tool programmatically.
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub allowed_callers: Option<Vec<String>>,
60
61    /// Anthropic tool use examples used to teach complex tool behavior.
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub input_examples: Option<Vec<Value>>,
64
65    /// Provider-native web search configuration payload (e.g. Z.AI `web_search` tool).
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub web_search: Option<Value>,
68
69    /// Provider-hosted Responses tool configuration for tool types like
70    /// `file_search` and `mcp`.
71    #[serde(skip, default)]
72    pub hosted_tool_config: Option<Value>,
73
74    /// Shell tool configuration (GPT-5.1 specific)
75    /// Describes shell command capabilities and constraints
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub shell: Option<ShellToolDefinition>,
78
79    /// Grammar definition for context-free grammar constraints (GPT-5 specific)
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub grammar: Option<GrammarDefinition>,
82
83    /// When true and using Anthropic, mark the tool as strict for structured tool use validation
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub strict: Option<bool>,
86
87    /// When true, the tool is deferred and only loaded when discovered via tool search (Anthropic advanced-tool-use beta)
88    /// This enables dynamic tool discovery for large tool catalogs (10k+ tools)
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub defer_loading: Option<bool>,
91}
92
93/// Shell tool definition for GPT-5.1 shell tool type
94/// Allows controlled command-line interface interactions
95#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
96pub struct ShellToolDefinition {
97    /// Description of shell tool capabilities
98    pub description: String,
99
100    /// List of allowed commands (whitelist for safety)
101    pub allowed_commands: Vec<String>,
102
103    /// List of forbidden commands (blacklist for safety)
104    pub forbidden_patterns: Vec<String>,
105
106    /// Maximum command timeout in seconds
107    pub timeout_seconds: u32,
108}
109
110/// Grammar definition for GPT-5 context-free grammar (CFG) constraints
111#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
112pub struct GrammarDefinition {
113    /// The syntax of the grammar: "lark" or "regex"
114    pub syntax: String,
115
116    /// The grammar definition in the specified syntax
117    pub definition: String,
118}
119
120impl Default for GrammarDefinition {
121    fn default() -> Self {
122        Self {
123            syntax: "lark".into(),
124            definition: String::new(),
125        }
126    }
127}
128
129impl Default for ShellToolDefinition {
130    fn default() -> Self {
131        Self {
132            description: "Execute shell commands in the workspace".into(),
133            allowed_commands: vec![
134                "ls".into(),
135                "find".into(),
136                "grep".into(),
137                "cargo".into(),
138                "git".into(),
139                "python".into(),
140                "node".into(),
141            ],
142            forbidden_patterns: vec!["rm -rf".into(), "sudo".into(), "passwd".into()],
143            timeout_seconds: 30,
144        }
145    }
146}
147
148/// Function definition within a tool
149#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
150pub struct FunctionDefinition {
151    /// The name of the function to be called
152    pub name: String,
153
154    /// A description of what the function does
155    pub description: String,
156
157    /// The parameters the function accepts, described as a JSON Schema object
158    pub parameters: Value,
159}
160
161pub(crate) fn sanitize_tool_description(description: &str) -> String {
162    let mut result = String::with_capacity(description.len());
163    let mut first = true;
164    for line in description.lines() {
165        if !first {
166            result.push('\n');
167        }
168        result.push_str(line.trim_end());
169        first = false;
170    }
171    result.trim().to_owned()
172}
173
174impl ToolDefinition {
175    fn empty(tool_type: impl Into<String>) -> Self {
176        Self {
177            tool_type: tool_type.into(),
178            function: None,
179            allowed_callers: None,
180            input_examples: None,
181            web_search: None,
182            hosted_tool_config: None,
183            shell: None,
184            grammar: None,
185            strict: None,
186            defer_loading: None,
187        }
188    }
189
190    /// Create a new tool definition with function type
191    pub fn function(name: String, description: String, parameters: Value) -> Self {
192        let sanitized_description = sanitize_tool_description(&description);
193        let mut tool = Self::empty("function");
194        tool.function = Some(FunctionDefinition {
195            name,
196            description: sanitized_description,
197            parameters,
198        });
199        tool
200    }
201
202    /// Set whether the tool should be considered strict (Anthropic structured tool use)
203    pub fn with_strict(mut self, strict: bool) -> Self {
204        self.strict = Some(strict);
205        self
206    }
207
208    /// Restrict which Anthropic callers can invoke this tool programmatically.
209    pub fn with_allowed_callers(mut self, allowed_callers: Vec<String>) -> Self {
210        self.allowed_callers = Some(allowed_callers);
211        self
212    }
213
214    /// Attach Anthropic tool use examples for better tool selection and argument shaping.
215    pub fn with_input_examples(mut self, input_examples: Vec<Value>) -> Self {
216        self.input_examples = Some(input_examples);
217        self
218    }
219
220    /// Set whether the tool should be deferred (Anthropic tool search)
221    pub fn with_defer_loading(mut self, defer: bool) -> Self {
222        self.defer_loading = Some(defer);
223        self
224    }
225
226    /// Create a tool search tool definition for Anthropic's advanced-tool-use beta
227    /// Supports regex and bm25 search algorithms
228    pub fn tool_search(algorithm: ToolSearchAlgorithm) -> Self {
229        let (tool_type, name) = match algorithm {
230            ToolSearchAlgorithm::Regex => {
231                ("tool_search_tool_regex_20251119", "tool_search_tool_regex")
232            }
233            ToolSearchAlgorithm::Bm25 => {
234                ("tool_search_tool_bm25_20251119", "tool_search_tool_bm25")
235            }
236        };
237
238        let mut tool = Self::empty(tool_type);
239        tool.function = Some(FunctionDefinition {
240            name: name.to_owned(),
241            description: "Search for tools by name, description, or parameters".to_owned(),
242            parameters: json!({
243                "type": "object",
244                "properties": {
245                    "query": {
246                        "type": "string",
247                        "description": "Search query (regex pattern for regex variant, natural language for bm25)"
248                    }
249                },
250                "required": ["query"]
251            }),
252        });
253        tool
254    }
255
256    /// Create an OpenAI hosted tool search definition.
257    pub fn hosted_tool_search() -> Self {
258        Self::empty("tool_search")
259    }
260
261    /// Create an Anthropic native memory tool definition.
262    pub fn anthropic_memory() -> Self {
263        Self::empty("memory_20250818")
264    }
265
266    /// Create a new apply_patch tool definition (GPT-5.1 specific)
267    /// The apply_patch tool lets models create, update, and delete files using VT Code structured diffs
268    pub fn apply_patch(description: String) -> Self {
269        let sanitized_description = sanitize_tool_description(&description);
270        let mut tool = Self::empty("apply_patch");
271        tool.function = Some(FunctionDefinition {
272            name: "apply_patch".to_owned(),
273            description: sanitized_description,
274            parameters: crate::tools::apply_patch::parameter_schema(
275                "Patch in VT Code format. MUST use *** Begin Patch, *** Update File: path, @@ context, -/+ lines, *** End Patch. Do NOT use unified diff (---/+++) format.",
276            ),
277        });
278        tool
279    }
280
281    /// Create a new custom tool definition for freeform function calling (GPT-5 specific)
282    /// Allows raw text payloads without JSON wrapping
283    pub fn custom(name: String, description: String) -> Self {
284        let sanitized_description = sanitize_tool_description(&description);
285        let mut tool = Self::empty("custom");
286        tool.function = Some(FunctionDefinition {
287            name,
288            description: sanitized_description,
289            parameters: json!({}), // Custom tools may not need parameters
290        });
291        tool
292    }
293
294    /// Create a new grammar tool definition for context-free grammar constraints (GPT-5 specific)
295    /// Ensures model output matches predefined syntax
296    pub fn grammar(syntax: String, definition: String) -> Self {
297        let mut tool = Self::empty("grammar");
298        tool.grammar = Some(GrammarDefinition { syntax, definition });
299        tool
300    }
301
302    /// Create a provider-native web search tool definition.
303    pub fn web_search(config: Value) -> Self {
304        let mut tool = Self::empty("web_search");
305        tool.web_search = Some(config);
306        tool
307    }
308
309    /// Create a Gemini Google Maps grounding tool definition.
310    pub fn google_maps(config: Value) -> Self {
311        let mut tool = Self::empty("google_maps");
312        tool.hosted_tool_config = Some(config);
313        tool
314    }
315
316    /// Create a Gemini URL Context tool definition.
317    pub fn url_context(config: Value) -> Self {
318        let mut tool = Self::empty("url_context");
319        tool.hosted_tool_config = Some(config);
320        tool
321    }
322
323    /// Create an OpenAI Responses file search tool definition.
324    pub fn file_search(config: Value) -> Self {
325        let mut tool = Self::empty("file_search");
326        tool.hosted_tool_config = Some(config);
327        tool
328    }
329
330    /// Create a Gemini Code Execution tool definition.
331    pub fn code_execution(config: Value) -> Self {
332        let mut tool = Self::empty("code_execution");
333        tool.hosted_tool_config = Some(config);
334        tool
335    }
336
337    /// Create an OpenAI Responses remote MCP tool definition.
338    pub fn mcp(config: Value) -> Self {
339        let mut tool = Self::empty("mcp");
340        tool.hosted_tool_config = Some(config);
341        tool
342    }
343
344    /// Get the function name for easy access
345    pub fn function_name(&self) -> &str {
346        if self.is_anthropic_memory_tool() {
347            "memory"
348        } else if let Some(func) = &self.function {
349            &func.name
350        } else {
351            &self.tool_type
352        }
353    }
354
355    /// Get the description for easy access
356    pub fn description(&self) -> &str {
357        if let Some(func) = &self.function {
358            &func.description
359        } else if let Some(shell) = &self.shell {
360            &shell.description
361        } else {
362            ""
363        }
364    }
365
366    /// Validate that this tool definition is properly formed
367    pub fn validate(&self) -> Result<(), String> {
368        match self.tool_type.as_str() {
369            "function" => self.validate_function(),
370            "apply_patch" => self.validate_apply_patch(),
371            "shell" => self.validate_shell(),
372            "custom" => self.validate_custom(),
373            "grammar" => self.validate_grammar(),
374            "web_search" => self.validate_web_search(),
375            "google_maps" | "url_context" | "file_search" | "mcp" | "code_execution" => {
376                self.validate_hosted_tool_config()
377            }
378            "tool_search" => Ok(()),
379            "tool_search_tool_regex_20251119" | "tool_search_tool_bm25_20251119" => {
380                self.validate_function()
381            }
382            other if other.starts_with("web_search_") => self.validate_anthropic_web_search(),
383            other if other.starts_with("code_execution_") => Ok(()),
384            other if other.starts_with("memory_") => Ok(()),
385            other => Err(format!(
386                "Unsupported tool type: {}. Supported types: function, apply_patch, shell, custom, grammar, web_search, google_maps, url_context, file_search, mcp, code_execution, tool_search, tool_search_tool_*, web_search_*, code_execution_*, memory_*",
387                other
388            )),
389        }
390    }
391
392    /// Returns true if this is a tool search tool type
393    pub fn is_tool_search(&self) -> bool {
394        matches!(
395            self.tool_type.as_str(),
396            "tool_search" | "tool_search_tool_regex_20251119" | "tool_search_tool_bm25_20251119"
397        )
398    }
399
400    /// Returns true when the tool is an Anthropic native web search tool revision.
401    pub fn is_anthropic_web_search(&self) -> bool {
402        self.tool_type.starts_with("web_search_")
403    }
404
405    /// Returns true when the tool is an Anthropic native code execution tool revision.
406    pub fn is_anthropic_code_execution(&self) -> bool {
407        self.tool_type.starts_with("code_execution_")
408    }
409
410    /// Returns true when the tool is an Anthropic native memory tool revision.
411    pub fn is_anthropic_memory_tool(&self) -> bool {
412        self.tool_type.starts_with("memory_")
413    }
414
415    fn validate_function(&self) -> Result<(), String> {
416        if let Some(func) = &self.function {
417            if func.name.is_empty() {
418                return Err("Function name cannot be empty".to_owned());
419            }
420            if func.description.is_empty() {
421                return Err("Function description cannot be empty".to_owned());
422            }
423            if !func.parameters.is_object() {
424                return Err("Function parameters must be a JSON object".to_owned());
425            }
426            Ok(())
427        } else {
428            Err("Function tool missing function definition".to_owned())
429        }
430    }
431
432    fn validate_apply_patch(&self) -> Result<(), String> {
433        if let Some(func) = &self.function {
434            if func.name != "apply_patch" {
435                return Err(format!(
436                    "apply_patch tool must have name 'apply_patch', got: {}",
437                    func.name
438                ));
439            }
440            if func.description.is_empty() {
441                return Err("apply_patch description cannot be empty".to_owned());
442            }
443            Ok(())
444        } else {
445            Err("apply_patch tool missing function definition".to_owned())
446        }
447    }
448
449    fn validate_shell(&self) -> Result<(), String> {
450        if let Some(shell) = &self.shell {
451            if shell.description.is_empty() {
452                return Err("Shell tool description cannot be empty".to_owned());
453            }
454            if shell.timeout_seconds == 0 {
455                return Err("Shell tool timeout must be greater than 0".to_owned());
456            }
457            Ok(())
458        } else {
459            Err("Shell tool missing shell definition".to_owned())
460        }
461    }
462
463    fn validate_custom(&self) -> Result<(), String> {
464        if let Some(func) = &self.function {
465            if func.name.is_empty() {
466                return Err("Custom tool name cannot be empty".to_owned());
467            }
468            if func.description.is_empty() {
469                return Err("Custom tool description cannot be empty".to_owned());
470            }
471            Ok(())
472        } else {
473            Err("Custom tool missing function definition".to_owned())
474        }
475    }
476
477    fn validate_grammar(&self) -> Result<(), String> {
478        if let Some(grammar) = &self.grammar {
479            if !["lark", "regex"].contains(&grammar.syntax.as_str()) {
480                return Err("Grammar syntax must be 'lark' or 'regex'".to_owned());
481            }
482            if grammar.definition.is_empty() {
483                return Err("Grammar definition cannot be empty".to_owned());
484            }
485            Ok(())
486        } else {
487            Err("Grammar tool missing grammar definition".to_owned())
488        }
489    }
490
491    fn validate_web_search(&self) -> Result<(), String> {
492        self.web_search_config_object(true).map(|_| ())
493    }
494
495    fn validate_anthropic_web_search(&self) -> Result<(), String> {
496        let Some(config) = self.web_search_config_object(false)? else {
497            return Ok(());
498        };
499
500        if config.contains_key("allowed_domains") && config.contains_key("blocked_domains") {
501            return Err(
502                "anthropic web_search tools cannot set both allowed_domains and blocked_domains"
503                    .to_owned(),
504            );
505        }
506
507        Ok(())
508    }
509
510    fn validate_hosted_tool_config(&self) -> Result<(), String> {
511        match self.hosted_tool_config.as_ref() {
512            Some(Value::Object(_)) => Ok(()),
513            Some(_) => Err(format!(
514                "{} tool configuration must be a JSON object",
515                self.tool_type
516            )),
517            None => Err(format!("{} tool missing configuration", self.tool_type)),
518        }
519    }
520
521    fn web_search_config_object(
522        &self,
523        required: bool,
524    ) -> Result<Option<&Map<String, Value>>, String> {
525        match self.web_search.as_ref() {
526            Some(Value::Object(config)) => Ok(Some(config)),
527            Some(_) => Err(format!(
528                "{} tool configuration must be a JSON object",
529                self.tool_type
530            )),
531            None if required => Err(format!(
532                "{} tool missing web_search configuration",
533                self.tool_type
534            )),
535            None => Ok(None),
536        }
537    }
538}