use anyhow::{bail, Result};
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::OnceLock;
use super::read_only::read_only_prompt;
use super::env_identity::{DEFAULT_PROBE_TIMEOUT, help_defines_flag, run_bounded};
use super::RunOpts;
use crate::types::*;
const TERMINAL_EXECUTOR_ERROR: &str = "agent executor error:";
pub struct AntigravityAgent;
impl super::Agent for AntigravityAgent {
fn kind(&self) -> AgentKind {
AgentKind::Antigravity
}
fn streaming(&self) -> bool {
false
}
fn accepts_interactive_input(&self) -> bool {
false
}
fn needs_pty(&self) -> bool {
false
}
fn validate_cli(&self, model: Option<&str>, run: &super::CliCommandRunner<'_>) -> Result<()> {
let Some(model) = model else { return Ok(()) };
let output = match run("agy", &["--help"]) {
Ok(output) if output.success => output,
_ => {
aid_warn!("[aid] could not inspect agy flags; skipping model check");
return Ok(());
}
};
if !parse_agy_capabilities(&format!("{}{}", output.stdout, output.stderr)).has_model_flag {
bail!(
"agy {} does not support --model; cannot run requested model {model}. Omit --model or upgrade agy",
agy_version_string().unwrap_or_else(|| "(unknown version)".into())
);
}
Ok(())
}
fn build_command(&self, prompt: &str, opts: &RunOpts) -> Result<Command> {
let caps = agy_capabilities();
let plan_flag = caps.plan_mode_flag;
let allow_result = super::read_only::allow_result_file_write(opts);
let use_plan = opts.read_only && plan_flag.is_some() && !allow_result;
let effective_prompt = if opts.read_only && !use_plan {
if plan_flag.is_none() {
aid_warn!("[aid] agy read-only does not sandbox the agent; aid fails the task if the Git run directory changes.");
}
read_only_prompt(prompt, opts)
} else {
prompt.to_string()
};
let mut cmd = Command::new("agy");
if use_plan && let Some(flag) = plan_flag {
cmd.args([flag, "plan"]);
}
if let Some(ref model) = opts.model {
if caps.has_model_flag {
cmd.args(["--model", model]);
} else {
aid_warn!(
"[aid] agy {} has no model flag; ignoring --model {model}",
agy_version_string().unwrap_or_else(|| "1.0".into())
);
}
}
cmd.arg("-p");
cmd.arg(&effective_prompt);
cmd.args(["--print-timeout", "24h"]);
cmd.arg("--dangerously-skip-permissions");
let run_dir = opts
.dir
.as_deref()
.filter(|dir| !dir.is_empty())
.and_then(|dir| absolute_dir(Path::new(dir)));
for dir in agy_include_directories(run_dir.as_deref(), &opts.context_files) {
cmd.args(["--add-dir", &dir]);
}
if let Some(log_file) = super::agent_log_from_opts(opts) {
if let Some(parent) = std::path::Path::new(log_file).parent() {
let _ = std::fs::create_dir_all(parent);
}
cmd.args(["--log-file", log_file]);
}
if let Some(ref dir) = run_dir {
cmd.current_dir(dir);
}
Ok(cmd)
}
fn parse_event(&self, _task_id: &TaskId, _line: &str) -> Option<TaskEvent> {
None
}
fn diagnostics_report_terminal_failure(&self, output: &str) -> bool {
output
.lines()
.any(|line| line.contains(TERMINAL_EXECUTOR_ERROR))
}
fn served_models(&self) -> Result<Option<Vec<String>>> {
let Some(output) = super::model_validation::run_probe_cmd("agy", &["models"]) else {
return Ok(None);
};
let models = parse_agy_models_output(&output.stdout);
Ok(if models.is_empty() { None } else { Some(models) })
}
}
pub(crate) fn parse_agy_models_output(output: &str) -> Vec<String> {
let mut models = Vec::new();
for line in output.lines() {
let trimmed = line.trim();
if trimmed.is_empty()
|| trimmed.starts_with('#')
|| trimmed.starts_with("Available")
|| trimmed.starts_with("Fetching")
|| trimmed.starts_with("ERROR")
|| trimmed.starts_with("error")
|| trimmed.starts_with("[ERROR]")
|| trimmed.starts_with("[WARN]")
{
continue;
}
let first = trimmed.split_whitespace().next().unwrap_or("");
let clean = first.trim_matches(|c: char| !c.is_alphanumeric() && c != '-' && c != '.');
if !clean.is_empty() && !models.contains(&clean.to_string()) {
models.push(clean.to_string());
}
}
models
}
#[derive(Debug, Clone, Default)]
struct AgyCapabilities {
plan_mode_flag: Option<&'static str>,
has_model_flag: bool,
}
fn agy_capabilities() -> &'static AgyCapabilities {
static CAPS: OnceLock<AgyCapabilities> = OnceLock::new();
CAPS.get_or_init(|| match probe_agy_capabilities() {
Some(caps) => caps,
None => {
aid_warn!("[aid] failed to probe agy capabilities; assuming no optional agy flags");
AgyCapabilities::default()
}
})
}
#[cfg(not(test))]
fn probe_agy_capabilities() -> Option<AgyCapabilities> {
let output = run_bounded("agy", &["--help"], DEFAULT_PROBE_TIMEOUT).ok()?;
let help = format!("{}{}", output.stdout, output.stderr);
Some(parse_agy_capabilities(&help))
}
fn parse_agy_capabilities(help: &str) -> AgyCapabilities {
let plan_mode_flag = if help_defines_flag(help, "--approval-mode") {
Some("--approval-mode")
} else if help_defines_flag(help, "--mode") {
Some("--mode")
} else {
None
};
AgyCapabilities {
plan_mode_flag,
has_model_flag: help_defines_flag(help, "--model"),
}
}
#[cfg(test)]
fn probe_agy_capabilities() -> Option<AgyCapabilities> {
None
}
fn agy_version_string() -> Option<String> {
let output = run_bounded("agy", &["--version"], DEFAULT_PROBE_TIMEOUT).ok()?;
let s = output.stdout.trim().to_string();
if s.is_empty() { None } else { Some(s) }
}
fn agy_include_directories(run_dir: Option<&Path>, context_files: &[String]) -> Vec<String> {
let mut directories = BTreeSet::new();
if let Some(run_dir) = run_dir {
directories.insert(run_dir.to_string_lossy().into_owned());
}
for file in context_files {
if let Some(include_dir) = context_include_directory(run_dir, file) {
directories.insert(include_dir);
}
}
directories.into_iter().collect()
}
fn context_include_directory(run_dir: Option<&Path>, context_file: &str) -> Option<String> {
if context_file.is_empty() {
return run_dir.map(|run_dir| run_dir.to_string_lossy().into_owned());
}
let path = Path::new(context_file);
let include_path = if path.is_dir() {
path.to_path_buf()
} else if let Some(parent) = path.parent().filter(|parent| !parent.as_os_str().is_empty()) {
parent.to_path_buf()
} else {
PathBuf::from(".")
};
let include_path = match (include_path.is_absolute(), run_dir) {
(false, Some(run_dir)) => run_dir.join(include_path),
_ => include_path,
};
absolute_dir(&include_path).map(|path| path.to_string_lossy().into_owned())
}
fn absolute_dir(path: &Path) -> Option<PathBuf> {
std::fs::canonicalize(path)
.ok()
.or_else(|| std::path::absolute(path).ok())
}
#[cfg(test)]
#[path = "antigravity_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "antigravity_model_tests.rs"]
mod model_tests;
#[cfg(test)]
#[path = "antigravity_preflight_tests.rs"]
mod preflight_tests;