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