1use serde::{Deserialize, Serialize};
2use serde_json::{Map, Value, json};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
6#[serde(rename_all = "lowercase")]
7pub enum ToolSearchAlgorithm {
8 #[default]
10 Regex,
11 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
39pub struct ToolDefinition {
40 #[serde(rename = "type")]
50 pub tool_type: String,
51
52 #[serde(skip_serializing_if = "Option::is_none")]
55 pub function: Option<FunctionDefinition>,
56
57 #[serde(skip_serializing_if = "Option::is_none")]
59 pub allowed_callers: Option<Vec<String>>,
60
61 #[serde(skip_serializing_if = "Option::is_none")]
63 pub input_examples: Option<Vec<Value>>,
64
65 #[serde(skip_serializing_if = "Option::is_none")]
67 pub web_search: Option<Value>,
68
69 #[serde(skip, default)]
72 pub hosted_tool_config: Option<Value>,
73
74 #[serde(skip_serializing_if = "Option::is_none")]
77 pub shell: Option<ShellToolDefinition>,
78
79 #[serde(skip_serializing_if = "Option::is_none")]
81 pub grammar: Option<GrammarDefinition>,
82
83 #[serde(skip_serializing_if = "Option::is_none")]
85 pub strict: Option<bool>,
86
87 #[serde(skip_serializing_if = "Option::is_none")]
90 pub defer_loading: Option<bool>,
91}
92
93#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
96pub struct ShellToolDefinition {
97 pub description: String,
99
100 pub allowed_commands: Vec<String>,
102
103 pub forbidden_patterns: Vec<String>,
105
106 pub timeout_seconds: u32,
108}
109
110#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
112pub struct GrammarDefinition {
113 pub syntax: String,
115
116 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
150pub struct FunctionDefinition {
151 pub name: String,
153
154 pub description: String,
156
157 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 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 pub fn with_strict(mut self, strict: bool) -> Self {
204 self.strict = Some(strict);
205 self
206 }
207
208 pub fn with_allowed_callers(mut self, allowed_callers: Vec<String>) -> Self {
210 self.allowed_callers = Some(allowed_callers);
211 self
212 }
213
214 pub fn with_input_examples(mut self, input_examples: Vec<Value>) -> Self {
216 self.input_examples = Some(input_examples);
217 self
218 }
219
220 pub fn with_defer_loading(mut self, defer: bool) -> Self {
222 self.defer_loading = Some(defer);
223 self
224 }
225
226 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 pub fn hosted_tool_search() -> Self {
258 Self::empty("tool_search")
259 }
260
261 pub fn anthropic_memory() -> Self {
263 Self::empty("memory_20250818")
264 }
265
266 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 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!({}), });
291 tool
292 }
293
294 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 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 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 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 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 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 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 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 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 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 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 pub fn is_anthropic_web_search(&self) -> bool {
402 self.tool_type.starts_with("web_search_")
403 }
404
405 pub fn is_anthropic_code_execution(&self) -> bool {
407 self.tool_type.starts_with("code_execution_")
408 }
409
410 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}