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