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
117impl std::fmt::Display for BuiltinTools {
118 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119 f.write_str(self.as_sdk_name())
120 }
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct CapabilitiesConfig {
126 #[serde(default = "super::default_true")]
128 pub enable_subagents: bool,
129 #[serde(default)]
131 pub enabled_tools: Option<Vec<BuiltinTools>>,
132 #[serde(default)]
134 pub disabled_tools: Option<Vec<BuiltinTools>>,
135 pub compaction_threshold: Option<usize>,
137 #[serde(default = "super::default_image_model")]
143 pub image_model: String,
144 #[serde(default)]
146 pub finish_tool_schema_json: Option<String>,
147}
148
149impl CapabilitiesConfig {
150 #[must_use]
154 pub fn with_tools(tools: Vec<BuiltinTools>) -> Self {
155 Self {
156 enabled_tools: Some(tools),
157 ..Self::default()
158 }
159 }
160
161 #[must_use]
163 pub fn full() -> Self {
164 Self::default()
165 }
166
167 #[must_use]
169 pub fn read_only() -> Self {
170 Self {
171 enabled_tools: Some(BuiltinTools::read_only().to_vec()),
172 ..Self::default()
173 }
174 }
175
176 #[must_use]
180 pub fn custom_tools_only() -> Self {
181 Self {
182 enabled_tools: Some(vec![]),
183 ..Self::default()
184 }
185 }
186
187 pub const fn validate(&self) -> Result<(), &'static str> {
191 if self.enabled_tools.is_some() && self.disabled_tools.is_some() {
192 return Err("enabled_tools and disabled_tools are mutually exclusive");
193 }
194 Ok(())
195 }
196}
197
198impl Default for CapabilitiesConfig {
199 fn default() -> Self {
200 Self {
201 enable_subagents: true,
202 enabled_tools: None,
203 disabled_tools: None,
204 compaction_threshold: None,
205 image_model: DEFAULT_IMAGE_GENERATION_MODEL.to_owned(),
206 finish_tool_schema_json: None,
207 }
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use pyo3::types::PyAnyMethods;
214
215 use super::*;
216
217 #[test]
218 fn test_builtin_tools() {
219 let read_only = BuiltinTools::read_only();
220 assert_eq!(read_only.len(), 5);
221 assert!(read_only.contains(&BuiltinTools::ListDir));
222 assert!(read_only.contains(&BuiltinTools::Finish));
223 assert!(!read_only.contains(&BuiltinTools::CreateFile));
224
225 let all = BuiltinTools::all_tools();
226 assert_eq!(all.len(), 11);
227 assert!(all.contains(&BuiltinTools::CreateFile));
228 assert!(all.contains(&BuiltinTools::Finish));
229
230 assert_eq!(BuiltinTools::ListDir.as_sdk_name(), "list_directory");
231 }
232
233 #[test]
234 fn test_capabilities_validation() {
235 let mut caps = CapabilitiesConfig {
236 enable_subagents: true,
237 enabled_tools: Some(vec![BuiltinTools::ListDir]),
238 ..CapabilitiesConfig::default()
239 };
240 assert!(caps.validate().is_ok());
241
242 caps.disabled_tools = Some(vec![BuiltinTools::SearchDir]);
243 assert!(caps.validate().is_err());
244 }
245
246 #[test]
247
248 fn builtin_tools_serde_roundtrip_all_variants() {
249 let all = BuiltinTools::all_tools();
250 for tool in all {
251 let json = serde_json::to_string(tool).unwrap();
252 let parsed: BuiltinTools = serde_json::from_str(&json).unwrap();
253 assert_eq!(&parsed, tool, "Failed roundtrip for {tool:?}");
254 }
255 }
256
257 #[test]
258 fn builtin_tools_python_str_covers_all_variants() {
259 let expected = [
260 (BuiltinTools::ListDir, "list_directory"),
261 (BuiltinTools::SearchDir, "search_directory"),
262 (BuiltinTools::FindFile, "find_file"),
263 (BuiltinTools::ViewFile, "view_file"),
264 (BuiltinTools::CreateFile, "create_file"),
265 (BuiltinTools::EditFile, "edit_file"),
266 (BuiltinTools::RunCommand, "run_command"),
267 (BuiltinTools::AskQuestion, "ask_question"),
268 (BuiltinTools::StartSubagent, "start_subagent"),
269 (BuiltinTools::GenerateImage, "generate_image"),
270 (BuiltinTools::Finish, "finish"),
271 ];
272 for (variant, py_str) in expected {
273 assert_eq!(
274 variant.as_sdk_name(),
275 py_str,
276 "Python str mismatch for {variant:?}"
277 );
278 }
279 }
280
281 #[test]
282 fn builtin_tools_read_only_is_subset_of_all() {
283 let all = BuiltinTools::all_tools();
284 let read_only = BuiltinTools::read_only();
285 for tool in read_only {
286 assert!(
287 all.contains(tool),
288 "{tool:?} in read_only but not in all_tools"
289 );
290 }
291 }
292
293 #[test]
294 fn builtin_tools_read_only_excludes_write_tools() {
295 let read_only = BuiltinTools::read_only();
296 assert!(!read_only.contains(&BuiltinTools::CreateFile));
297 assert!(!read_only.contains(&BuiltinTools::EditFile));
298 assert!(!read_only.contains(&BuiltinTools::RunCommand));
299 assert!(!read_only.contains(&BuiltinTools::StartSubagent));
300 assert!(!read_only.contains(&BuiltinTools::GenerateImage));
301 assert!(!read_only.contains(&BuiltinTools::AskQuestion));
302 }
303
304 #[test]
305 fn capabilities_config_both_none_is_valid() {
306 let caps = CapabilitiesConfig::default();
307 assert!(caps.validate().is_ok());
308 }
309
310 #[test]
311 fn capabilities_config_only_disabled_is_valid() {
312 let caps = CapabilitiesConfig {
313 disabled_tools: Some(vec![BuiltinTools::RunCommand]),
314 compaction_threshold: Some(2000),
315 ..CapabilitiesConfig::default()
316 };
317 assert!(caps.validate().is_ok());
318 }
319
320 #[test]
321 fn capabilities_config_serde_roundtrip() {
322 let caps = CapabilitiesConfig {
323 enable_subagents: true,
324 enabled_tools: Some(vec![BuiltinTools::ViewFile, BuiltinTools::ListDir]),
325 compaction_threshold: Some(8000),
326 ..CapabilitiesConfig::default()
327 };
328 let json = serde_json::to_string(&caps).unwrap();
329 let parsed: CapabilitiesConfig = serde_json::from_str(&json).unwrap();
330 assert!(parsed.enable_subagents);
331 assert_eq!(parsed.enabled_tools.as_ref().unwrap().len(), 2);
332 assert_eq!(parsed.compaction_threshold, Some(8000));
333 }
334
335 #[test]
336 fn builtin_tools_snake_case_serde() {
337 let tool = BuiltinTools::StartSubagent;
339 let json = serde_json::to_string(&tool).unwrap();
340 assert_eq!(json, "\"start_subagent\"");
341
342 let tool = BuiltinTools::GenerateImage;
343 let json = serde_json::to_string(&tool).unwrap();
344 assert_eq!(json, "\"generate_image\"");
345 }
346
347 #[test]
348 fn capabilities_config_empty_enabled_list_vs_none() {
349 let caps_empty = CapabilitiesConfig {
352 enabled_tools: Some(vec![]),
353 ..CapabilitiesConfig::default()
354 };
355 assert!(caps_empty.validate().is_ok());
356 assert!(caps_empty.enabled_tools.as_ref().unwrap().is_empty());
357
358 let caps_none = CapabilitiesConfig::default();
359 assert!(caps_none.enabled_tools.is_none());
360 }
361
362 #[test]
363 fn capabilities_default_enables_subagents() {
364 let caps = CapabilitiesConfig::default();
366 assert!(
367 caps.enable_subagents,
368 "enable_subagents should default to true, matching the SDK"
369 );
370 }
371
372 #[test]
373 fn capabilities_serde_missing_enable_subagents_defaults_true() {
374 let json = r#"{"enabled_tools": ["view_file"]}"#;
376 let caps: CapabilitiesConfig = serde_json::from_str(json).unwrap();
377 assert!(
378 caps.enable_subagents,
379 "Missing enable_subagents in JSON should deserialize to true"
380 );
381 }
382
383 #[test]
384 fn capabilities_serde_explicit_false_is_respected() {
385 let json = r#"{"enable_subagents": false}"#;
386 let caps: CapabilitiesConfig = serde_json::from_str(json).unwrap();
387 assert!(!caps.enable_subagents, "Explicit false should be preserved");
388 }
389
390 #[test]
391 fn capabilities_with_tools_enables_subagents() {
392 let caps = CapabilitiesConfig::with_tools(vec![
393 BuiltinTools::ViewFile,
394 BuiltinTools::StartSubagent,
395 ]);
396 assert!(caps.enable_subagents);
397 assert_eq!(caps.enabled_tools.as_ref().unwrap().len(), 2);
398 }
399
400 #[test]
401 fn capabilities_full_enables_subagents() {
402 let caps = CapabilitiesConfig::full();
403 assert!(caps.enable_subagents);
404 assert!(caps.enabled_tools.is_none()); }
406
407 #[test]
408 fn capabilities_read_only_enables_subagents_but_no_start_subagent() {
409 let caps = CapabilitiesConfig::read_only();
410 assert!(caps.enable_subagents);
411 let tools = caps.enabled_tools.as_ref().unwrap();
412 assert!(
414 !tools.contains(&BuiltinTools::StartSubagent),
415 "read_only should not include StartSubagent in enabled_tools"
416 );
417 }
418
419 #[test]
420 fn capabilities_custom_tools_only_enables_subagents() {
421 let caps = CapabilitiesConfig::custom_tools_only();
422 assert!(caps.enable_subagents);
423 assert!(caps.enabled_tools.as_ref().unwrap().is_empty());
424 }
425
426 #[test]
427 fn start_subagent_in_all_tools_and_nondestructive() {
428 let all = BuiltinTools::all_tools();
429 assert!(
430 all.contains(&BuiltinTools::StartSubagent),
431 "all_tools() must include StartSubagent"
432 );
433 let nondestructive = BuiltinTools::nondestructive();
434 assert!(
435 nondestructive.contains(&BuiltinTools::StartSubagent),
436 "nondestructive() must include StartSubagent"
437 );
438 let read_only = BuiltinTools::read_only();
439 assert!(
440 !read_only.contains(&BuiltinTools::StartSubagent),
441 "read_only() must NOT include StartSubagent"
442 );
443 }
444
445 #[test]
447 fn builtin_tools_match_python_sdk() {
448 pyo3::Python::initialize();
449 pyo3::Python::attach(|py| {
450 crate::runtime::venv::configure_python_sys_path(py)
451 .unwrap_or_else(|e| panic!("Failed to configure python sys.path: {e}"));
452 let types_mod = py
453 .import("google.antigravity.types")
454 .expect("Failed to import google.antigravity.types");
455 let bt = types_mod
456 .getattr("BuiltinTools")
457 .expect("Failed to get BuiltinTools");
458 let builtins = py.import("builtins").expect("Failed to import builtins");
461 let members = builtins
462 .getattr("list")
463 .expect("Failed to get list")
464 .call1((bt,))
465 .expect("Failed to call list(BuiltinTools)");
466 let py_tools: Vec<String> = members
467 .try_iter()
468 .expect("Failed to iter members")
469 .map(|item| {
470 item.and_then(|v| v.getattr("value"))
471 .and_then(|v| v.extract::<String>())
472 })
473 .collect::<pyo3::PyResult<Vec<String>>>()
474 .expect("Failed to extract tool values");
475
476 let rust_tools: Vec<String> = BuiltinTools::all_tools()
477 .iter()
478 .map(|t| t.as_sdk_name().to_owned())
479 .collect();
480
481 assert_eq!(
482 rust_tools.len(),
483 py_tools.len(),
484 "Tool count mismatch: Rust has {}, Python has {}.\nRust: {rust_tools:?}\nPython: {py_tools:?}",
485 rust_tools.len(),
486 py_tools.len(),
487 );
488
489 for py_name in &py_tools {
490 assert!(
491 rust_tools.contains(py_name),
492 "Python SDK has tool '{py_name}' but Rust BuiltinTools does not"
493 );
494 }
495
496 for rust_name in &rust_tools {
497 assert!(
498 py_tools.contains(rust_name),
499 "Rust BuiltinTools has '{rust_name}' but Python SDK does not"
500 );
501 }
502 });
503 }
504
505 #[test]
507 fn capabilities_validate_rejects_both_enabled_and_disabled() {
508 let caps = CapabilitiesConfig {
509 enabled_tools: Some(vec![BuiltinTools::ViewFile]),
510 disabled_tools: Some(vec![BuiltinTools::RunCommand]),
511 ..CapabilitiesConfig::default()
512 };
513 assert!(caps.validate().is_err());
514 }
515}