1use serde::{Deserialize, Serialize};
2use serde_json::{Map, Value, json};
3
4pub use vtcode_config::ToolSearchAlgorithm;
6
7#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
16pub struct ToolNamespace {
17 pub name: String,
19 pub description: String,
21}
22
23#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
26pub struct ToolDefinition {
27 #[serde(rename = "type")]
37 pub tool_type: String,
38
39 #[serde(skip_serializing_if = "Option::is_none")]
42 pub function: Option<FunctionDefinition>,
43
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub(crate) allowed_callers: Option<Vec<String>>,
47
48 #[serde(skip_serializing_if = "Option::is_none")]
50 pub(crate) input_examples: Option<Vec<Value>>,
51
52 #[serde(skip_serializing_if = "Option::is_none")]
54 pub web_search: Option<Value>,
55
56 #[serde(skip, default)]
59 pub(crate) hosted_tool_config: Option<Value>,
60
61 #[serde(skip_serializing_if = "Option::is_none")]
64 pub(crate) shell: Option<ShellToolDefinition>,
65
66 #[serde(skip_serializing_if = "Option::is_none")]
68 pub(crate) grammar: Option<GrammarDefinition>,
69
70 #[serde(skip_serializing_if = "Option::is_none")]
72 pub(crate) strict: Option<bool>,
73
74 #[serde(skip_serializing_if = "Option::is_none")]
77 pub defer_loading: Option<bool>,
78
79 #[serde(skip_serializing_if = "Option::is_none")]
83 pub namespace: Option<ToolNamespace>,
84
85 #[serde(skip_serializing_if = "Option::is_none")]
89 pub(crate) advisor: Option<Value>,
90}
91
92#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
95pub struct ShellToolDefinition {
96 description: String,
98
99 allowed_commands: Vec<String>,
101
102 forbidden_patterns: Vec<String>,
104
105 timeout_seconds: u32,
107}
108
109#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
111pub struct GrammarDefinition {
112 pub(crate) syntax: String,
114
115 pub(crate) definition: String,
117}
118
119impl Default for GrammarDefinition {
120 fn default() -> Self {
121 Self { syntax: "lark".into(), definition: String::new() }
122 }
123}
124
125impl Default for ShellToolDefinition {
126 fn default() -> Self {
127 Self {
128 description: "Execute shell commands in the workspace".into(),
129 allowed_commands: vec![
130 "ls".into(),
131 "find".into(),
132 "grep".into(),
133 "cargo".into(),
134 "git".into(),
135 "python".into(),
136 "node".into(),
137 ],
138 forbidden_patterns: vec!["rm -rf".into(), "sudo".into(), "passwd".into()],
139 timeout_seconds: 30,
140 }
141 }
142}
143
144#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
146pub struct FunctionDefinition {
147 pub name: String,
149
150 pub description: String,
152
153 pub parameters: Value,
155}
156
157pub(crate) fn sanitize_tool_description(description: &str) -> String {
158 let mut result = String::with_capacity(description.len());
159 let mut first = true;
160 for line in description.lines() {
161 if !first {
162 result.push('\n');
163 }
164 result.push_str(line.trim_end());
165 first = false;
166 }
167 result.trim().to_owned()
168}
169
170impl ToolDefinition {
171 fn empty(tool_type: impl Into<String>) -> Self {
172 Self {
173 tool_type: tool_type.into(),
174 function: None,
175 allowed_callers: None,
176 input_examples: None,
177 web_search: None,
178 hosted_tool_config: None,
179 shell: None,
180 grammar: None,
181 strict: None,
182 defer_loading: None,
183 namespace: None,
184 advisor: None,
185 }
186 }
187
188 pub fn function(name: String, description: String, parameters: Value) -> Self {
190 let sanitized_description = sanitize_tool_description(&description);
191 let mut tool = Self::empty("function");
192 tool.function = Some(FunctionDefinition {
193 name,
194 description: sanitized_description,
195 parameters,
196 });
197 tool
198 }
199
200 pub fn with_strict(mut self, strict: bool) -> Self {
202 self.strict = Some(strict);
203 self
204 }
205
206 pub(crate) fn with_allowed_callers(mut self, allowed_callers: Vec<String>) -> Self {
208 self.allowed_callers = Some(allowed_callers);
209 self
210 }
211
212 pub(crate) fn with_input_examples(mut self, input_examples: Vec<Value>) -> Self {
214 self.input_examples = Some(input_examples);
215 self
216 }
217
218 pub fn with_defer_loading(mut self, defer: bool) -> Self {
220 self.defer_loading = Some(defer);
221 self
222 }
223
224 pub fn with_namespace(mut self, namespace: ToolNamespace) -> Self {
228 self.namespace = Some(namespace);
229 self
230 }
231
232 pub fn tool_search(algorithm: ToolSearchAlgorithm) -> Self {
235 let (tool_type, name) = match algorithm {
236 ToolSearchAlgorithm::Regex => ("tool_search_tool_regex_20251119", "tool_search_tool_regex"),
237 ToolSearchAlgorithm::Bm25 => ("tool_search_tool_bm25_20251119", "tool_search_tool_bm25"),
238 ToolSearchAlgorithm::Unknown => ("tool_search_tool_regex_20251119", "tool_search_tool_regex"),
240 };
241
242 let mut tool = Self::empty(tool_type);
243 tool.function = Some(FunctionDefinition {
244 name: name.to_owned(),
245 description: "Search for tools by name, description, or parameters".to_owned(),
246 parameters: json!({
247 "type": "object",
248 "properties": {
249 "query": {
250 "type": "string",
251 "description": "Search query (regex pattern for regex variant, natural language for bm25)"
252 }
253 },
254 "required": ["query"]
255 }),
256 });
257 tool
258 }
259
260 pub fn hosted_tool_search() -> Self {
262 Self::empty("tool_search")
263 }
264
265 pub fn anthropic_memory() -> Self {
267 Self::empty("memory_20250818")
268 }
269
270 pub fn apply_patch(description: String) -> Self {
273 let sanitized_description = sanitize_tool_description(&description);
274 let mut tool = Self::empty("apply_patch");
275 tool.function = Some(FunctionDefinition {
276 name: "apply_patch".to_owned(),
277 description: sanitized_description,
278 parameters: apply_patch_schema(vtcode_utility_tool_specs::DEFAULT_APPLY_PATCH_INPUT_DESCRIPTION),
279 });
280 tool
281 }
282
283 pub fn custom(name: String, description: String) -> Self {
286 let sanitized_description = sanitize_tool_description(&description);
287 let mut tool = Self::empty("custom");
288 tool.function = Some(FunctionDefinition {
289 name,
290 description: sanitized_description,
291 parameters: json!({}), });
293 tool
294 }
295
296 pub fn grammar(syntax: String, definition: String) -> Self {
299 let mut tool = Self::empty("grammar");
300 tool.grammar = Some(GrammarDefinition { syntax, definition });
301 tool
302 }
303
304 pub fn web_search(config: Value) -> Self {
306 let mut tool = Self::empty("web_search");
307 tool.web_search = Some(config);
308 tool
309 }
310
311 pub fn google_maps(config: Value) -> Self {
313 let mut tool = Self::empty("google_maps");
314 tool.hosted_tool_config = Some(config);
315 tool
316 }
317
318 pub fn url_context(config: Value) -> Self {
320 let mut tool = Self::empty("url_context");
321 tool.hosted_tool_config = Some(config);
322 tool
323 }
324
325 pub(crate) fn file_search(config: Value) -> Self {
327 let mut tool = Self::empty("file_search");
328 tool.hosted_tool_config = Some(config);
329 tool
330 }
331
332 pub(crate) fn code_execution(config: Value) -> Self {
334 let mut tool = Self::empty("code_execution");
335 tool.hosted_tool_config = Some(config);
336 tool
337 }
338
339 pub(crate) fn mcp(config: Value) -> Self {
341 let mut tool = Self::empty("mcp");
342 tool.hosted_tool_config = Some(config);
343 tool
344 }
345
346 pub fn function_name(&self) -> &str {
348 if self.is_anthropic_memory_tool() {
349 "memory"
350 } else if let Some(func) = &self.function {
351 &func.name
352 } else {
353 &self.tool_type
354 }
355 }
356
357 pub fn description(&self) -> &str {
359 if let Some(func) = &self.function {
360 &func.description
361 } else if let Some(shell) = &self.shell {
362 &shell.description
363 } else {
364 ""
365 }
366 }
367
368 pub fn validate(&self) -> Result<(), String> {
370 match self.tool_type.as_str() {
371 "function" => self.validate_function(),
372 "apply_patch" => self.validate_apply_patch(),
373 "shell" => self.validate_shell(),
374 "custom" => self.validate_custom(),
375 "grammar" => self.validate_grammar(),
376 "web_search" => self.validate_web_search(),
377 "google_maps" | "url_context" | "file_search" | "mcp" | "code_execution" => {
378 self.validate_hosted_tool_config()
379 }
380 "tool_search" => Ok(()),
381 "tool_search_tool_regex_20251119" | "tool_search_tool_bm25_20251119" => self.validate_function(),
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: {other}. 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 )),
388 }
389 }
390
391 pub fn is_tool_search(&self) -> bool {
393 matches!(
394 self.tool_type.as_str(),
395 "tool_search" | "tool_search_tool_regex_20251119" | "tool_search_tool_bm25_20251119"
396 )
397 }
398
399 pub fn is_anthropic_web_search(&self) -> bool {
401 self.tool_type.starts_with("web_search_")
402 }
403
404 pub(crate) fn is_anthropic_code_execution(&self) -> bool {
406 self.tool_type.starts_with("code_execution_")
407 }
408
409 pub(crate) fn is_anthropic_memory_tool(&self) -> bool {
411 self.tool_type.starts_with("memory_")
412 }
413
414 fn validate_function(&self) -> Result<(), String> {
415 if let Some(func) = &self.function {
416 if func.name.is_empty() {
417 return Err("Function name cannot be empty".to_owned());
418 }
419 if func.description.is_empty() {
420 return Err("Function description cannot be empty".to_owned());
421 }
422 if !func.parameters.is_object() {
423 return Err("Function parameters must be a JSON object".to_owned());
424 }
425 Ok(())
426 } else {
427 Err("Function tool missing function definition".to_owned())
428 }
429 }
430
431 fn validate_apply_patch(&self) -> Result<(), String> {
432 if let Some(func) = &self.function {
433 if func.name != "apply_patch" {
434 return Err(format!("apply_patch tool must have name 'apply_patch', got: {}", func.name));
435 }
436 if func.description.is_empty() {
437 return Err("apply_patch description cannot be empty".to_owned());
438 }
439 Ok(())
440 } else {
441 Err("apply_patch tool missing function definition".to_owned())
442 }
443 }
444
445 fn validate_shell(&self) -> Result<(), String> {
446 if let Some(shell) = &self.shell {
447 if shell.description.is_empty() {
448 return Err("Shell tool description cannot be empty".to_owned());
449 }
450 if shell.timeout_seconds == 0 {
451 return Err("Shell tool timeout must be greater than 0".to_owned());
452 }
453 Ok(())
454 } else {
455 Err("Shell tool missing shell definition".to_owned())
456 }
457 }
458
459 fn validate_custom(&self) -> Result<(), String> {
460 if let Some(func) = &self.function {
461 if func.name.is_empty() {
462 return Err("Custom tool name cannot be empty".to_owned());
463 }
464 if func.description.is_empty() {
465 return Err("Custom tool description cannot be empty".to_owned());
466 }
467 Ok(())
468 } else {
469 Err("Custom tool missing function definition".to_owned())
470 }
471 }
472
473 fn validate_grammar(&self) -> Result<(), String> {
474 if let Some(grammar) = &self.grammar {
475 if !["lark", "regex"].contains(&grammar.syntax.as_str()) {
476 return Err("Grammar syntax must be 'lark' or 'regex'".to_owned());
477 }
478 if grammar.definition.is_empty() {
479 return Err("Grammar definition cannot be empty".to_owned());
480 }
481 Ok(())
482 } else {
483 Err("Grammar tool missing grammar definition".to_owned())
484 }
485 }
486
487 fn validate_web_search(&self) -> Result<(), String> {
488 self.web_search_config_object(true).map(|_| ())
489 }
490
491 fn validate_anthropic_web_search(&self) -> Result<(), String> {
492 let Some(config) = self.web_search_config_object(false)? else {
493 return Ok(());
494 };
495
496 if config.contains_key("allowed_domains") && config.contains_key("blocked_domains") {
497 return Err("anthropic web_search tools cannot set both allowed_domains and blocked_domains".to_owned());
498 }
499
500 Ok(())
501 }
502
503 fn validate_hosted_tool_config(&self) -> Result<(), String> {
504 match self.hosted_tool_config.as_ref() {
505 Some(Value::Object(_)) => Ok(()),
506 Some(_) => Err(format!("{} tool configuration must be a JSON object", self.tool_type)),
507 None => Err(format!("{} tool missing configuration", self.tool_type)),
508 }
509 }
510
511 fn web_search_config_object(&self, required: bool) -> Result<Option<&Map<String, Value>>, String> {
512 match self.web_search.as_ref() {
513 Some(Value::Object(config)) => Ok(Some(config)),
514 Some(_) => Err(format!("{} tool configuration must be a JSON object", self.tool_type)),
515 None if required => Err(format!("{} tool missing web_search configuration", self.tool_type)),
516 None => Ok(None),
517 }
518 }
519}
520
521fn apply_patch_schema(description: &str) -> Value {
525 json!({
526 "type": "object",
527 "properties": {
528 "patch": {
529 "type": "string",
530 "description": description
531 }
532 },
533 "required": ["patch"]
534 })
535}
536
537#[cfg(test)]
538mod tests {
539 use super::ToolDefinition;
540
541 #[test]
542 fn apply_patch_parameter_schema_requires_workspace_relative_paths() {
543 let tool = ToolDefinition::apply_patch("Apply patches".to_owned());
544 let function = tool.function.expect("apply_patch function definition");
545 let description = function.parameters["properties"]["patch"]["description"]
546 .as_str()
547 .expect("patch parameter description");
548
549 assert!(description.contains("workspace-relative"));
550 assert!(description.contains("absolute paths"));
551 assert!(description.contains("`..`"));
552 assert!(description.contains("traversal-like forms"));
553 }
554}