1use serde::{Deserialize, Serialize};
4
5use super::DEFAULT_IMAGE_GENERATION_MODEL;
6
7#[non_exhaustive]
8#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
9pub enum BuiltinTools {
10 #[serde(rename = "list_directory")]
12 ListDir,
13 #[serde(rename = "search_directory")]
15 SearchDir,
16 #[serde(rename = "find_file")]
18 FindFile,
19 #[serde(rename = "view_file")]
21 ViewFile,
22 #[serde(rename = "create_file")]
24 CreateFile,
25 #[serde(rename = "edit_file")]
27 EditFile,
28 #[serde(rename = "run_command")]
30 RunCommand,
31 #[serde(rename = "ask_question")]
33 AskQuestion,
34 #[serde(rename = "start_subagent")]
36 StartSubagent,
37 #[serde(rename = "generate_image")]
39 GenerateImage,
40 #[serde(rename = "finish")]
42 Finish,
43}
44
45impl BuiltinTools {
46 #[must_use]
47 pub const fn read_only() -> &'static [Self] {
49 &[
50 Self::ListDir,
51 Self::SearchDir,
52 Self::FindFile,
53 Self::ViewFile,
54 Self::Finish,
55 ]
56 }
57
58 #[must_use]
60 pub const fn nondestructive() -> &'static [Self] {
61 &[
62 Self::ListDir,
63 Self::SearchDir,
64 Self::FindFile,
65 Self::ViewFile,
66 Self::CreateFile,
67 Self::EditFile,
68 Self::AskQuestion,
69 Self::StartSubagent,
70 Self::GenerateImage,
71 Self::Finish,
72 ]
73 }
74
75 #[must_use]
77 pub const fn all_tools() -> &'static [Self] {
78 &[
79 Self::ListDir,
80 Self::SearchDir,
81 Self::FindFile,
82 Self::ViewFile,
83 Self::CreateFile,
84 Self::EditFile,
85 Self::RunCommand,
86 Self::AskQuestion,
87 Self::StartSubagent,
88 Self::GenerateImage,
89 Self::Finish,
90 ]
91 }
92
93 #[must_use]
98 pub const fn file_tools() -> &'static [Self] {
99 &[Self::ViewFile, Self::CreateFile, Self::EditFile]
100 }
101
102 #[must_use]
104 pub const fn none() -> &'static [Self] {
105 &[]
106 }
107
108 #[must_use]
109 pub const fn as_sdk_name(&self) -> &'static str {
111 match self {
112 Self::ListDir => "list_directory",
113 Self::SearchDir => "search_directory",
114 Self::FindFile => "find_file",
115 Self::ViewFile => "view_file",
116 Self::CreateFile => "create_file",
117 Self::EditFile => "edit_file",
118 Self::RunCommand => "run_command",
119 Self::AskQuestion => "ask_question",
120 Self::StartSubagent => "start_subagent",
121 Self::GenerateImage => "generate_image",
122 Self::Finish => "finish",
123 }
124 }
125
126 #[must_use]
128 pub const fn description(&self) -> &'static str {
129 match self {
130 Self::ListDir => "List files and subdirectories.",
131 Self::SearchDir => "Regex search within directory contents.",
132 Self::FindFile => "Find files by name pattern.",
133 Self::ViewFile => "Read file contents.",
134 Self::CreateFile => "Create a new file.",
135 Self::EditFile => "Edit an existing file.",
136 Self::RunCommand => "Execute a shell command.",
137 Self::AskQuestion => "Ask the user a question.",
138 Self::StartSubagent => "Spawn a subagent.",
139 Self::GenerateImage => "Generate images from text prompts.",
140 Self::Finish => "Signal task completion.",
141 }
142 }
143}
144
145impl std::fmt::Display for BuiltinTools {
146 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147 f.write_str(self.as_sdk_name())
148 }
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct CapabilitiesConfig {
154 #[serde(default = "super::default_true")]
156 pub enable_subagents: bool,
157 #[serde(default)]
159 pub enabled_tools: Option<Vec<BuiltinTools>>,
160 #[serde(default)]
162 pub disabled_tools: Option<Vec<BuiltinTools>>,
163 pub compaction_threshold: Option<usize>,
165 #[serde(default = "super::default_image_model")]
171 pub image_model: String,
172 #[serde(default)]
174 pub finish_tool_schema_json: Option<String>,
175}
176
177impl CapabilitiesConfig {
178 #[must_use]
182 pub fn with_tools(tools: Vec<BuiltinTools>) -> Self {
183 Self {
184 enabled_tools: Some(tools),
185 ..Self::default()
186 }
187 }
188
189 #[must_use]
191 pub fn full() -> Self {
192 Self::default()
193 }
194
195 #[must_use]
197 pub fn read_only() -> Self {
198 Self {
199 enabled_tools: Some(BuiltinTools::read_only().to_vec()),
200 ..Self::default()
201 }
202 }
203
204 #[must_use]
208 pub fn custom_tools_only() -> Self {
209 Self {
210 enabled_tools: Some(vec![]),
211 ..Self::default()
212 }
213 }
214
215 pub const fn validate(&self) -> Result<(), &'static str> {
219 if self.enabled_tools.is_some() && self.disabled_tools.is_some() {
220 return Err("enabled_tools and disabled_tools are mutually exclusive");
221 }
222 Ok(())
223 }
224}
225
226impl Default for CapabilitiesConfig {
227 fn default() -> Self {
228 Self {
229 enable_subagents: true,
230 enabled_tools: None,
231 disabled_tools: None,
232 compaction_threshold: None,
233 image_model: DEFAULT_IMAGE_GENERATION_MODEL.to_owned(),
234 finish_tool_schema_json: None,
235 }
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use pyo3::types::PyAnyMethods;
242
243 use super::*;
244
245 #[test]
246 fn test_builtin_tools() {
247 let read_only = BuiltinTools::read_only();
248 assert_eq!(read_only.len(), 5);
249 assert!(read_only.contains(&BuiltinTools::ListDir));
250 assert!(read_only.contains(&BuiltinTools::Finish));
251 assert!(!read_only.contains(&BuiltinTools::CreateFile));
252
253 let all = BuiltinTools::all_tools();
254 assert_eq!(all.len(), 11);
255 assert!(all.contains(&BuiltinTools::CreateFile));
256 assert!(all.contains(&BuiltinTools::Finish));
257
258 assert_eq!(BuiltinTools::ListDir.as_sdk_name(), "list_directory");
259 }
260
261 #[test]
262 fn test_capabilities_validation() {
263 let mut caps = CapabilitiesConfig {
264 enable_subagents: true,
265 enabled_tools: Some(vec![BuiltinTools::ListDir]),
266 ..CapabilitiesConfig::default()
267 };
268 assert!(caps.validate().is_ok());
269
270 caps.disabled_tools = Some(vec![BuiltinTools::SearchDir]);
271 assert!(caps.validate().is_err());
272 }
273
274 #[test]
275
276 fn builtin_tools_serde_roundtrip_all_variants() {
277 let all = BuiltinTools::all_tools();
278 for tool in all {
279 let json = serde_json::to_string(tool).unwrap();
280 let parsed: BuiltinTools = serde_json::from_str(&json).unwrap();
281 assert_eq!(&parsed, tool, "Failed roundtrip for {tool:?}");
282 }
283 }
284
285 #[test]
286 fn builtin_tools_python_str_covers_all_variants() {
287 let expected = [
288 (BuiltinTools::ListDir, "list_directory"),
289 (BuiltinTools::SearchDir, "search_directory"),
290 (BuiltinTools::FindFile, "find_file"),
291 (BuiltinTools::ViewFile, "view_file"),
292 (BuiltinTools::CreateFile, "create_file"),
293 (BuiltinTools::EditFile, "edit_file"),
294 (BuiltinTools::RunCommand, "run_command"),
295 (BuiltinTools::AskQuestion, "ask_question"),
296 (BuiltinTools::StartSubagent, "start_subagent"),
297 (BuiltinTools::GenerateImage, "generate_image"),
298 (BuiltinTools::Finish, "finish"),
299 ];
300 for (variant, py_str) in expected {
301 assert_eq!(
302 variant.as_sdk_name(),
303 py_str,
304 "Python str mismatch for {variant:?}"
305 );
306 }
307 }
308
309 #[test]
310 fn builtin_tools_read_only_is_subset_of_all() {
311 let all = BuiltinTools::all_tools();
312 let read_only = BuiltinTools::read_only();
313 for tool in read_only {
314 assert!(
315 all.contains(tool),
316 "{tool:?} in read_only but not in all_tools"
317 );
318 }
319 }
320
321 #[test]
322 fn builtin_tools_read_only_excludes_write_tools() {
323 let read_only = BuiltinTools::read_only();
324 assert!(!read_only.contains(&BuiltinTools::CreateFile));
325 assert!(!read_only.contains(&BuiltinTools::EditFile));
326 assert!(!read_only.contains(&BuiltinTools::RunCommand));
327 assert!(!read_only.contains(&BuiltinTools::StartSubagent));
328 assert!(!read_only.contains(&BuiltinTools::GenerateImage));
329 assert!(!read_only.contains(&BuiltinTools::AskQuestion));
330 }
331
332 #[test]
333 fn capabilities_config_both_none_is_valid() {
334 let caps = CapabilitiesConfig::default();
335 assert!(caps.validate().is_ok());
336 }
337
338 #[test]
339 fn capabilities_config_only_disabled_is_valid() {
340 let caps = CapabilitiesConfig {
341 disabled_tools: Some(vec![BuiltinTools::RunCommand]),
342 compaction_threshold: Some(2000),
343 ..CapabilitiesConfig::default()
344 };
345 assert!(caps.validate().is_ok());
346 }
347
348 #[test]
349 fn capabilities_config_serde_roundtrip() {
350 let caps = CapabilitiesConfig {
351 enable_subagents: true,
352 enabled_tools: Some(vec![BuiltinTools::ViewFile, BuiltinTools::ListDir]),
353 compaction_threshold: Some(8000),
354 ..CapabilitiesConfig::default()
355 };
356 let json = serde_json::to_string(&caps).unwrap();
357 let parsed: CapabilitiesConfig = serde_json::from_str(&json).unwrap();
358 assert!(parsed.enable_subagents);
359 assert_eq!(parsed.enabled_tools.as_ref().unwrap().len(), 2);
360 assert_eq!(parsed.compaction_threshold, Some(8000));
361 }
362
363 #[test]
364 fn builtin_tools_snake_case_serde() {
365 let tool = BuiltinTools::StartSubagent;
367 let json = serde_json::to_string(&tool).unwrap();
368 assert_eq!(json, "\"start_subagent\"");
369
370 let tool = BuiltinTools::GenerateImage;
371 let json = serde_json::to_string(&tool).unwrap();
372 assert_eq!(json, "\"generate_image\"");
373 }
374
375 #[test]
376 fn capabilities_config_empty_enabled_list_vs_none() {
377 let caps_empty = CapabilitiesConfig {
380 enabled_tools: Some(vec![]),
381 ..CapabilitiesConfig::default()
382 };
383 assert!(caps_empty.validate().is_ok());
384 assert!(caps_empty.enabled_tools.as_ref().unwrap().is_empty());
385
386 let caps_none = CapabilitiesConfig::default();
387 assert!(caps_none.enabled_tools.is_none());
388 }
389
390 #[test]
391 fn capabilities_default_enables_subagents() {
392 let caps = CapabilitiesConfig::default();
394 assert!(
395 caps.enable_subagents,
396 "enable_subagents should default to true, matching the SDK"
397 );
398 }
399
400 #[test]
401 fn capabilities_serde_missing_enable_subagents_defaults_true() {
402 let json = r#"{"enabled_tools": ["view_file"]}"#;
404 let caps: CapabilitiesConfig = serde_json::from_str(json).unwrap();
405 assert!(
406 caps.enable_subagents,
407 "Missing enable_subagents in JSON should deserialize to true"
408 );
409 }
410
411 #[test]
412 fn capabilities_serde_explicit_false_is_respected() {
413 let json = r#"{"enable_subagents": false}"#;
414 let caps: CapabilitiesConfig = serde_json::from_str(json).unwrap();
415 assert!(!caps.enable_subagents, "Explicit false should be preserved");
416 }
417
418 #[test]
419 fn capabilities_with_tools_enables_subagents() {
420 let caps = CapabilitiesConfig::with_tools(vec![
421 BuiltinTools::ViewFile,
422 BuiltinTools::StartSubagent,
423 ]);
424 assert!(caps.enable_subagents);
425 assert_eq!(caps.enabled_tools.as_ref().unwrap().len(), 2);
426 }
427
428 #[test]
429 fn capabilities_full_enables_subagents() {
430 let caps = CapabilitiesConfig::full();
431 assert!(caps.enable_subagents);
432 assert!(caps.enabled_tools.is_none()); }
434
435 #[test]
436 fn capabilities_read_only_enables_subagents_but_no_start_subagent() {
437 let caps = CapabilitiesConfig::read_only();
438 assert!(caps.enable_subagents);
439 let tools = caps.enabled_tools.as_ref().unwrap();
440 assert!(
442 !tools.contains(&BuiltinTools::StartSubagent),
443 "read_only should not include StartSubagent in enabled_tools"
444 );
445 }
446
447 #[test]
448 fn capabilities_custom_tools_only_enables_subagents() {
449 let caps = CapabilitiesConfig::custom_tools_only();
450 assert!(caps.enable_subagents);
451 assert!(caps.enabled_tools.as_ref().unwrap().is_empty());
452 }
453
454 #[test]
455 fn start_subagent_in_all_tools_and_nondestructive() {
456 let all = BuiltinTools::all_tools();
457 assert!(
458 all.contains(&BuiltinTools::StartSubagent),
459 "all_tools() must include StartSubagent"
460 );
461 let nondestructive = BuiltinTools::nondestructive();
462 assert!(
463 nondestructive.contains(&BuiltinTools::StartSubagent),
464 "nondestructive() must include StartSubagent"
465 );
466 let read_only = BuiltinTools::read_only();
467 assert!(
468 !read_only.contains(&BuiltinTools::StartSubagent),
469 "read_only() must NOT include StartSubagent"
470 );
471 }
472
473 #[test]
475 fn builtin_tools_match_python_sdk() {
476 pyo3::Python::initialize();
477 pyo3::Python::attach(|py| {
478 crate::runtime::venv::configure_python_sys_path(py)
479 .unwrap_or_else(|e| panic!("Failed to configure python sys.path: {e}"));
480 let types_mod = py
481 .import("google.antigravity.types")
482 .expect("Failed to import google.antigravity.types");
483 let bt = types_mod
484 .getattr("BuiltinTools")
485 .expect("Failed to get BuiltinTools");
486 let builtins = py.import("builtins").expect("Failed to import builtins");
489 let members = builtins
490 .getattr("list")
491 .expect("Failed to get list")
492 .call1((bt,))
493 .expect("Failed to call list(BuiltinTools)");
494 let py_tools: Vec<String> = members
495 .try_iter()
496 .expect("Failed to iter members")
497 .map(|item| {
498 item.and_then(|v| v.getattr("value"))
499 .and_then(|v| v.extract::<String>())
500 })
501 .collect::<pyo3::PyResult<Vec<String>>>()
502 .expect("Failed to extract tool values");
503
504 let rust_tools: Vec<String> = BuiltinTools::all_tools()
505 .iter()
506 .map(|t| t.as_sdk_name().to_owned())
507 .collect();
508
509 assert_eq!(
510 rust_tools.len(),
511 py_tools.len(),
512 "Tool count mismatch: Rust has {}, Python has {}.\nRust: {rust_tools:?}\nPython: {py_tools:?}",
513 rust_tools.len(),
514 py_tools.len(),
515 );
516
517 for py_name in &py_tools {
518 assert!(
519 rust_tools.contains(py_name),
520 "Python SDK has tool '{py_name}' but Rust BuiltinTools does not"
521 );
522 }
523
524 for rust_name in &rust_tools {
525 assert!(
526 py_tools.contains(rust_name),
527 "Rust BuiltinTools has '{rust_name}' but Python SDK does not"
528 );
529 }
530 });
531 }
532
533 #[test]
535 fn capabilities_validate_rejects_both_enabled_and_disabled() {
536 let caps = CapabilitiesConfig {
537 enabled_tools: Some(vec![BuiltinTools::ViewFile]),
538 disabled_tools: Some(vec![BuiltinTools::RunCommand]),
539 ..CapabilitiesConfig::default()
540 };
541 assert!(caps.validate().is_err());
542 }
543}