use serde::{Deserialize, Serialize};
use typed_builder::TypedBuilder;
use super::DEFAULT_IMAGE_GENERATION_MODEL;
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum BuiltinTools {
#[serde(rename = "list_directory")]
ListDir,
#[serde(rename = "search_directory")]
SearchDir,
#[serde(rename = "find_file")]
FindFile,
#[serde(rename = "view_file")]
ViewFile,
#[serde(rename = "create_file")]
CreateFile,
#[serde(rename = "edit_file")]
EditFile,
#[serde(rename = "run_command")]
RunCommand,
#[serde(rename = "ask_question")]
AskQuestion,
#[serde(rename = "start_subagent")]
StartSubagent,
#[serde(rename = "generate_image")]
GenerateImage,
#[serde(rename = "search_web")]
SearchWeb,
#[serde(rename = "read_url_content")]
ReadUrlContent,
#[serde(rename = "finish")]
Finish,
}
impl BuiltinTools {
#[must_use]
pub const fn read_only() -> &'static [Self] {
&[
Self::ListDir,
Self::SearchDir,
Self::FindFile,
Self::ViewFile,
Self::ReadUrlContent,
Self::Finish,
]
}
#[must_use]
pub const fn nondestructive() -> &'static [Self] {
&[
Self::ListDir,
Self::SearchDir,
Self::FindFile,
Self::ViewFile,
Self::CreateFile,
Self::EditFile,
Self::AskQuestion,
Self::StartSubagent,
Self::GenerateImage,
Self::SearchWeb,
Self::ReadUrlContent,
Self::Finish,
]
}
#[must_use]
pub const fn all_tools() -> &'static [Self] {
&[
Self::ListDir,
Self::SearchDir,
Self::FindFile,
Self::ViewFile,
Self::CreateFile,
Self::EditFile,
Self::RunCommand,
Self::AskQuestion,
Self::StartSubagent,
Self::GenerateImage,
Self::SearchWeb,
Self::ReadUrlContent,
Self::Finish,
]
}
#[must_use]
pub const fn file_tools() -> &'static [Self] {
&[Self::ViewFile, Self::CreateFile, Self::EditFile]
}
#[must_use]
pub const fn none() -> &'static [Self] {
&[]
}
#[must_use]
pub const fn as_sdk_name(&self) -> &'static str {
match self {
Self::ListDir => "list_directory",
Self::SearchDir => "search_directory",
Self::FindFile => "find_file",
Self::ViewFile => "view_file",
Self::CreateFile => "create_file",
Self::EditFile => "edit_file",
Self::RunCommand => "run_command",
Self::AskQuestion => "ask_question",
Self::StartSubagent => "start_subagent",
Self::GenerateImage => "generate_image",
Self::SearchWeb => "search_web",
Self::ReadUrlContent => "read_url_content",
Self::Finish => "finish",
}
}
#[must_use]
pub const fn description(&self) -> &'static str {
match self {
Self::ListDir => "List files and subdirectories.",
Self::SearchDir => "Regex search within directory contents.",
Self::FindFile => "Find files by name pattern.",
Self::ViewFile => "Read file contents.",
Self::CreateFile => "Create a new file.",
Self::EditFile => "Edit an existing file.",
Self::RunCommand => "Execute a shell command.",
Self::AskQuestion => "Ask the user a question.",
Self::StartSubagent => "Spawn a subagent.",
Self::GenerateImage => "Generate images from text prompts.",
Self::SearchWeb => "Search the web for information.",
Self::ReadUrlContent => "Fetch content from a URL via HTTP request.",
Self::Finish => "Signal task completion.",
}
}
}
impl std::fmt::Display for BuiltinTools {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_sdk_name())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum AgentBehavior {
#[default]
#[serde(alias = "AUTONOMOUS", alias = "autonomous")]
Autonomous,
#[serde(alias = "INTERACTIVE", alias = "interactive")]
Interactive,
}
impl std::fmt::Display for AgentBehavior {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Autonomous => f.write_str("autonomous"),
Self::Interactive => f.write_str("interactive"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, TypedBuilder)]
pub struct CapabilitiesConfig {
#[serde(default = "super::default_true")]
#[builder(default = true)]
pub enable_subagents: bool,
#[serde(default)]
#[builder(default)]
pub agent_behavior: AgentBehavior,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[builder(default, setter(strip_option))]
pub max_subagent_depth: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[builder(default, setter(strip_option))]
pub allowed_subagents: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[builder(default, setter(strip_option))]
pub command_timeout_ms: Option<u64>,
#[serde(default)]
#[builder(default, setter(strip_option))]
pub enabled_tools: Option<Vec<BuiltinTools>>,
#[serde(default)]
#[builder(default, setter(strip_option))]
pub disabled_tools: Option<Vec<BuiltinTools>>,
#[builder(default, setter(strip_option))]
pub compaction_threshold: Option<usize>,
#[serde(default = "super::default_image_model")]
#[builder(default = DEFAULT_IMAGE_GENERATION_MODEL.to_owned(), setter(into))]
pub image_model: String,
#[serde(default)]
#[builder(default, setter(into, strip_option))]
pub finish_tool_schema_json: Option<String>,
}
impl CapabilitiesConfig {
#[must_use]
pub fn with_tools(tools: Vec<BuiltinTools>) -> Self {
Self {
enabled_tools: Some(tools),
..Self::default()
}
}
#[must_use]
pub fn full() -> Self {
Self::default()
}
#[must_use]
pub fn read_only() -> Self {
Self {
enabled_tools: Some(BuiltinTools::read_only().to_vec()),
..Self::default()
}
}
#[must_use]
pub fn custom_tools_only() -> Self {
Self {
enabled_tools: Some(vec![]),
..Self::default()
}
}
pub const fn validate(&self) -> Result<(), &'static str> {
if self.enabled_tools.is_some() && self.disabled_tools.is_some() {
return Err("enabled_tools and disabled_tools are mutually exclusive");
}
Ok(())
}
}
impl Default for CapabilitiesConfig {
fn default() -> Self {
Self::builder().build()
}
}
#[cfg(test)]
mod tests {
#[cfg(feature = "python")]
use pyo3::types::PyAnyMethods;
use super::*;
#[test]
fn test_builtin_tools() {
let read_only = BuiltinTools::read_only();
assert_eq!(read_only.len(), 6);
assert!(read_only.contains(&BuiltinTools::ListDir));
assert!(!read_only.contains(&BuiltinTools::SearchWeb));
assert!(read_only.contains(&BuiltinTools::ReadUrlContent));
assert!(read_only.contains(&BuiltinTools::Finish));
assert!(!read_only.contains(&BuiltinTools::CreateFile));
let all = BuiltinTools::all_tools();
assert_eq!(all.len(), 13);
assert!(all.contains(&BuiltinTools::CreateFile));
assert!(all.contains(&BuiltinTools::Finish));
assert!(all.contains(&BuiltinTools::SearchWeb));
assert!(all.contains(&BuiltinTools::ReadUrlContent));
assert_eq!(BuiltinTools::ListDir.as_sdk_name(), "list_directory");
assert_eq!(BuiltinTools::SearchWeb.as_sdk_name(), "search_web");
assert_eq!(
BuiltinTools::ReadUrlContent.as_sdk_name(),
"read_url_content"
);
}
#[test]
fn test_capabilities_validation() {
let mut caps = CapabilitiesConfig {
enable_subagents: true,
enabled_tools: Some(vec![BuiltinTools::ListDir]),
..CapabilitiesConfig::default()
};
assert!(caps.validate().is_ok());
caps.disabled_tools = Some(vec![BuiltinTools::SearchDir]);
assert!(caps.validate().is_err());
}
#[test]
fn builtin_tools_serde_roundtrip_all_variants() {
let all = BuiltinTools::all_tools();
for tool in all {
let json = serde_json::to_string(tool).unwrap();
let parsed: BuiltinTools = serde_json::from_str(&json).unwrap();
assert_eq!(&parsed, tool, "Failed roundtrip for {tool:?}");
}
}
#[test]
fn builtin_tools_python_str_covers_all_variants() {
let expected = [
(BuiltinTools::ListDir, "list_directory"),
(BuiltinTools::SearchDir, "search_directory"),
(BuiltinTools::FindFile, "find_file"),
(BuiltinTools::ViewFile, "view_file"),
(BuiltinTools::CreateFile, "create_file"),
(BuiltinTools::EditFile, "edit_file"),
(BuiltinTools::RunCommand, "run_command"),
(BuiltinTools::AskQuestion, "ask_question"),
(BuiltinTools::StartSubagent, "start_subagent"),
(BuiltinTools::GenerateImage, "generate_image"),
(BuiltinTools::SearchWeb, "search_web"),
(BuiltinTools::ReadUrlContent, "read_url_content"),
(BuiltinTools::Finish, "finish"),
];
for (variant, py_str) in expected {
assert_eq!(
variant.as_sdk_name(),
py_str,
"Python str mismatch for {variant:?}"
);
}
}
#[test]
fn builtin_tools_read_only_is_subset_of_all() {
let all = BuiltinTools::all_tools();
let read_only = BuiltinTools::read_only();
for tool in read_only {
assert!(
all.contains(tool),
"{tool:?} in read_only but not in all_tools"
);
}
}
#[test]
fn builtin_tools_read_only_excludes_write_tools() {
let read_only = BuiltinTools::read_only();
assert!(!read_only.contains(&BuiltinTools::CreateFile));
assert!(!read_only.contains(&BuiltinTools::EditFile));
assert!(!read_only.contains(&BuiltinTools::RunCommand));
assert!(!read_only.contains(&BuiltinTools::StartSubagent));
assert!(!read_only.contains(&BuiltinTools::GenerateImage));
assert!(!read_only.contains(&BuiltinTools::AskQuestion));
}
#[test]
fn capabilities_config_both_none_is_valid() {
let caps = CapabilitiesConfig::default();
assert!(caps.validate().is_ok());
}
#[test]
fn capabilities_config_only_disabled_is_valid() {
let caps = CapabilitiesConfig {
disabled_tools: Some(vec![BuiltinTools::RunCommand]),
compaction_threshold: Some(2000),
..CapabilitiesConfig::default()
};
assert!(caps.validate().is_ok());
}
#[test]
fn capabilities_config_serde_roundtrip() {
let caps = CapabilitiesConfig {
enable_subagents: true,
enabled_tools: Some(vec![BuiltinTools::ViewFile, BuiltinTools::ListDir]),
compaction_threshold: Some(8000),
..CapabilitiesConfig::default()
};
let json = serde_json::to_string(&caps).unwrap();
let parsed: CapabilitiesConfig = serde_json::from_str(&json).unwrap();
assert!(parsed.enable_subagents);
assert_eq!(parsed.enabled_tools.as_ref().unwrap().len(), 2);
assert_eq!(parsed.compaction_threshold, Some(8000));
}
#[test]
fn builtin_tools_snake_case_serde() {
let tool = BuiltinTools::StartSubagent;
let json = serde_json::to_string(&tool).unwrap();
assert_eq!(json, "\"start_subagent\"");
let tool = BuiltinTools::GenerateImage;
let json = serde_json::to_string(&tool).unwrap();
assert_eq!(json, "\"generate_image\"");
}
#[test]
fn capabilities_config_empty_enabled_list_vs_none() {
let caps_empty = CapabilitiesConfig {
enabled_tools: Some(vec![]),
..CapabilitiesConfig::default()
};
assert!(caps_empty.validate().is_ok());
assert!(caps_empty.enabled_tools.as_ref().unwrap().is_empty());
let caps_none = CapabilitiesConfig::default();
assert!(caps_none.enabled_tools.is_none());
}
#[test]
fn capabilities_default_enables_subagents() {
let caps = CapabilitiesConfig::default();
assert!(
caps.enable_subagents,
"enable_subagents should default to true, matching the SDK"
);
}
#[test]
fn capabilities_serde_missing_enable_subagents_defaults_true() {
let json = r#"{"enabled_tools": ["view_file"]}"#;
let caps: CapabilitiesConfig = serde_json::from_str(json).unwrap();
assert!(
caps.enable_subagents,
"Missing enable_subagents in JSON should deserialize to true"
);
}
#[test]
fn capabilities_serde_explicit_false_is_respected() {
let json = r#"{"enable_subagents": false}"#;
let caps: CapabilitiesConfig = serde_json::from_str(json).unwrap();
assert!(!caps.enable_subagents, "Explicit false should be preserved");
}
#[test]
fn capabilities_with_tools_enables_subagents() {
let caps = CapabilitiesConfig::with_tools(vec![
BuiltinTools::ViewFile,
BuiltinTools::StartSubagent,
]);
assert!(caps.enable_subagents);
assert_eq!(caps.enabled_tools.as_ref().unwrap().len(), 2);
}
#[test]
fn capabilities_full_enables_subagents() {
let caps = CapabilitiesConfig::full();
assert!(caps.enable_subagents);
assert!(caps.enabled_tools.is_none()); }
#[test]
fn capabilities_read_only_enables_subagents_but_no_start_subagent() {
let caps = CapabilitiesConfig::read_only();
assert!(caps.enable_subagents);
let tools = caps.enabled_tools.as_ref().unwrap();
assert!(
!tools.contains(&BuiltinTools::StartSubagent),
"read_only should not include StartSubagent in enabled_tools"
);
}
#[test]
fn capabilities_custom_tools_only_enables_subagents() {
let caps = CapabilitiesConfig::custom_tools_only();
assert!(caps.enable_subagents);
assert!(caps.enabled_tools.as_ref().unwrap().is_empty());
}
#[test]
fn start_subagent_in_all_tools_and_nondestructive() {
let all = BuiltinTools::all_tools();
assert!(
all.contains(&BuiltinTools::StartSubagent),
"all_tools() must include StartSubagent"
);
let nondestructive = BuiltinTools::nondestructive();
assert!(
nondestructive.contains(&BuiltinTools::StartSubagent),
"nondestructive() must include StartSubagent"
);
let read_only = BuiltinTools::read_only();
assert!(
!read_only.contains(&BuiltinTools::StartSubagent),
"read_only() must NOT include StartSubagent"
);
}
#[test]
#[cfg(feature = "python")]
fn builtin_tools_match_python_sdk() {
pyo3::Python::initialize();
pyo3::Python::attach(|py| {
crate::runtime::venv::configure_python_sys_path(py)
.unwrap_or_else(|e| panic!("Failed to configure python sys.path: {e}"));
let types_mod =
crate::runtime::py_scripts::import_serialized(py, "google.antigravity.types")
.expect("Failed to import google.antigravity.types");
let bt = types_mod
.getattr("BuiltinTools")
.expect("Failed to get BuiltinTools");
let builtins = py.import("builtins").expect("Failed to import builtins");
let members = builtins
.getattr("list")
.expect("Failed to get list")
.call1((bt,))
.expect("Failed to call list(BuiltinTools)");
let py_tools: Vec<String> = members
.try_iter()
.expect("Failed to iter members")
.map(|item| {
item.and_then(|v| v.getattr("value"))
.and_then(|v| v.extract::<String>())
})
.collect::<pyo3::PyResult<Vec<String>>>()
.expect("Failed to extract tool values");
let rust_tools: Vec<String> = BuiltinTools::all_tools()
.iter()
.map(|t| t.as_sdk_name().to_owned())
.collect();
assert_eq!(
rust_tools.len(),
py_tools.len(),
"Tool count mismatch: Rust has {}, Python has {}.\nRust: {rust_tools:?}\nPython: {py_tools:?}",
rust_tools.len(),
py_tools.len(),
);
for py_name in &py_tools {
assert!(
rust_tools.contains(py_name),
"Python SDK has tool '{py_name}' but Rust BuiltinTools does not"
);
}
for rust_name in &rust_tools {
assert!(
py_tools.contains(rust_name),
"Rust BuiltinTools has '{rust_name}' but Python SDK does not"
);
}
});
}
#[test]
fn capabilities_validate_rejects_both_enabled_and_disabled() {
let caps = CapabilitiesConfig {
enabled_tools: Some(vec![BuiltinTools::ViewFile]),
disabled_tools: Some(vec![BuiltinTools::RunCommand]),
..CapabilitiesConfig::default()
};
assert!(caps.validate().is_err());
}
#[test]
fn test_agent_behavior_roundtrip_and_display() {
assert_eq!(AgentBehavior::Autonomous.to_string(), "autonomous");
assert_eq!(AgentBehavior::Interactive.to_string(), "interactive");
let json = serde_json::to_string(&AgentBehavior::Autonomous).unwrap();
assert_eq!(json, "\"autonomous\"");
let parsed: AgentBehavior = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, AgentBehavior::Autonomous);
let parsed_upper: AgentBehavior = serde_json::from_str("\"AUTONOMOUS\"").unwrap();
assert_eq!(parsed_upper, AgentBehavior::Autonomous);
}
#[test]
fn test_capabilities_new_fields_builder_and_serde() {
let caps = CapabilitiesConfig::builder()
.agent_behavior(AgentBehavior::Interactive)
.max_subagent_depth(3)
.allowed_subagents(vec!["sub1".to_string(), "sub2".to_string()])
.command_timeout_ms(60_000)
.build();
assert_eq!(caps.agent_behavior, AgentBehavior::Interactive);
assert_eq!(caps.max_subagent_depth, Some(3));
assert_eq!(
caps.allowed_subagents,
Some(vec!["sub1".to_string(), "sub2".to_string()])
);
assert_eq!(caps.command_timeout_ms, Some(60_000));
let json = serde_json::to_string(&caps).unwrap();
let parsed: CapabilitiesConfig = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.agent_behavior, AgentBehavior::Interactive);
assert_eq!(parsed.max_subagent_depth, Some(3));
assert_eq!(
parsed.allowed_subagents,
Some(vec!["sub1".to_string(), "sub2".to_string()])
);
assert_eq!(parsed.command_timeout_ms, Some(60_000));
}
}