1use crate::conversation::Conversation;
2use crate::hooks::{DynHook, HookRunner};
3#[cfg(not(target_arch = "wasm32"))]
4use crate::local::LocalConnectionStrategy;
5use crate::policy::{self, Policy, PolicyEnforcer};
6use crate::tools::{DynTool, ToolRunner};
7use crate::triggers::{DynTrigger, TriggerRunner};
8use crate::types::{
9 BuiltinTools, CapabilitiesConfig, ChatResponse, GeminiConfig, SystemInstructions,
10};
11use anyhow::anyhow;
12use std::sync::Arc;
13
14#[derive(Default)]
16pub struct AgentConfig {
17 pub binary_path: Option<String>,
20 pub gemini_config: GeminiConfig,
22 pub capabilities: CapabilitiesConfig,
24 pub system_instructions: Option<SystemInstructions>,
26 pub save_dir: Option<String>,
28 pub workspaces: Option<Vec<String>>,
30 pub skills_paths: Vec<String>,
32 pub policies: Option<Vec<Policy>>,
34 pub hooks: Vec<Arc<dyn DynHook>>,
36 pub triggers: Vec<Arc<dyn DynTrigger>>,
38 pub tools: Vec<Arc<dyn DynTool>>,
40 pub conversation_id: Option<String>,
42 pub app_data_dir: Option<String>,
44 pub response_schema: Option<String>,
46}
47
48impl std::fmt::Debug for AgentConfig {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 f.debug_struct("AgentConfig")
51 .field("binary_path", &self.binary_path)
52 .field("gemini_config", &self.gemini_config)
53 .field("capabilities", &self.capabilities)
54 .field("system_instructions", &self.system_instructions)
55 .field("save_dir", &self.save_dir)
56 .field("workspaces", &self.workspaces)
57 .field("skills_paths", &self.skills_paths)
58 .field("policies", &self.policies)
59 .field("hooks_count", &self.hooks.len())
60 .field("triggers_count", &self.triggers.len())
61 .field("tools_count", &self.tools.len())
62 .field("conversation_id", &self.conversation_id)
63 .field("app_data_dir", &self.app_data_dir)
64 .field("response_schema", &self.response_schema)
65 .finish()
66 }
67}
68
69pub struct Agent {
96 config: AgentConfig,
97 conversation: Option<Arc<Conversation>>,
98 tool_runner: ToolRunner,
99 hook_runner: HookRunner,
100 trigger_runner: Option<TriggerRunner>,
101}
102
103impl std::fmt::Debug for Agent {
104 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105 f.debug_struct("Agent")
106 .field("config", &self.config)
107 .field("conversation", &self.conversation)
108 .field("tool_runner", &self.tool_runner)
109 .field("hook_runner", &self.hook_runner)
110 .field("trigger_runner", &self.trigger_runner)
111 .finish()
112 }
113}
114
115impl Agent {
116 pub fn new(config: AgentConfig) -> Self {
118 Self {
119 config,
120 conversation: None,
121 tool_runner: ToolRunner::new(),
122 hook_runner: HookRunner::new(),
123 trigger_runner: None,
124 }
125 }
126
127 pub fn register_hook(&self, hook: Arc<dyn DynHook>) {
129 let hr = self.hook_runner.clone();
130 crate::spawn_task(async move {
131 hr.register(hook).await;
132 });
133 }
134
135 pub fn register_trigger(&mut self, trigger: Arc<dyn DynTrigger>) -> Result<(), anyhow::Error> {
141 if self.conversation.is_some() {
142 return Err(anyhow!(
143 "Cannot register triggers after the agent has started."
144 ));
145 }
146 self.config.triggers.push(trigger);
147 Ok(())
148 }
149
150 pub fn register_tool(&self, tool: Arc<dyn DynTool>) {
152 let tr = self.tool_runner.clone();
153 crate::spawn_task(async move {
154 tr.register(tool).await;
155 });
156 }
157
158 #[allow(clippy::too_many_lines)]
168 pub async fn start(&mut self) -> Result<(), anyhow::Error> {
169 if self.conversation.is_some() {
170 return Ok(());
171 }
172
173 #[cfg(not(target_arch = "wasm32"))]
175 let binary_path = self.config.binary_path.clone()
176 .or_else(get_default_binary_path)
177 .ok_or_else(|| anyhow!("Could not find default localharness binary. Please specify binary_path explicitly."))?;
178
179 for hook in &self.config.hooks {
181 self.hook_runner.register(hook.clone()).await;
182 }
183
184 let enabled_tools = self.config.capabilities.enabled_tools.clone();
186 let disabled_tools = self.config.capabilities.disabled_tools.clone();
187 if enabled_tools.is_some() && disabled_tools.is_some() {
188 return Err(anyhow!(
189 "enabled_tools and disabled_tools are mutually exclusive"
190 ));
191 }
192
193 let active_tools = enabled_tools.unwrap_or_else(|| {
194 disabled_tools.map_or_else(
195 || {
196 vec![
197 BuiltinTools::CreateFile,
198 BuiltinTools::EditFile,
199 BuiltinTools::FindFile,
200 BuiltinTools::ListDir,
201 BuiltinTools::RunCommand,
202 BuiltinTools::SearchDir,
203 BuiltinTools::ViewFile,
204 BuiltinTools::StartSubagent,
205 BuiltinTools::GenerateImage,
206 BuiltinTools::Finish,
207 ]
208 },
209 |disabled| {
210 let all = vec![
211 BuiltinTools::CreateFile,
212 BuiltinTools::EditFile,
213 BuiltinTools::FindFile,
214 BuiltinTools::ListDir,
215 BuiltinTools::RunCommand,
216 BuiltinTools::SearchDir,
217 BuiltinTools::ViewFile,
218 BuiltinTools::StartSubagent,
219 BuiltinTools::GenerateImage,
220 BuiltinTools::Finish,
221 ];
222 all.into_iter().filter(|t| !disabled.contains(t)).collect()
223 },
224 )
225 });
226
227 let read_only = BuiltinTools::read_only();
228 let has_write_tools = active_tools.iter().any(|t| !read_only.contains(t));
229
230 let mut final_policies = self.config.policies.clone().unwrap_or_else(|| {
232 policy::confirm_run_command(None)
234 });
235
236 let workspaces = self.config.workspaces.clone().unwrap_or_else(|| {
238 std::env::current_dir().map_or_else(
239 |_| Vec::new(),
240 |cwd| vec![cwd.to_string_lossy().into_owned()],
241 )
242 });
243
244 if !workspaces.is_empty() {
245 let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
246 let app_data_dir = self
247 .config
248 .app_data_dir
249 .clone()
250 .unwrap_or_else(|| format!("{home}/.gemini/antigravity"));
251 let mut allowed_paths = workspaces;
252 allowed_paths.push(app_data_dir);
253 let mut ws_policies = policy::workspace_only(allowed_paths);
254 ws_policies.append(&mut final_policies);
255 final_policies = ws_policies;
256 }
257
258 if has_write_tools && final_policies.is_empty() {
260 return Err(anyhow!(
261 "Write tools are enabled without a safety policy. Add policies=[policy.allow_all()] to approve all tool calls, or policies=[policy.deny_all(), policy.allow(\"tool_name\")] to selectively allow specific tools."
262 ));
263 }
264
265 if !final_policies.is_empty() {
266 let enforcer = Arc::new(PolicyEnforcer::new(final_policies));
267 self.hook_runner.register(enforcer).await;
268 }
269
270 for tool in &self.config.tools {
272 self.tool_runner.register(tool.clone()).await;
273 }
274
275 #[cfg(target_arch = "wasm32")]
277 {
278 let mut cap = self.config.capabilities.clone();
279 if let Some(ref schema) = self.config.response_schema {
280 cap.finish_tool_schema_json = Some(schema.clone());
281 }
282
283 let strategy = crate::wasm::WasmConnectionStrategy {
284 gemini_config: self.config.gemini_config.clone(),
285 capabilities_config: cap,
286 system_instructions: self.config.system_instructions.clone(),
287 save_dir: self.config.save_dir.clone(),
288 workspaces: self.config.workspaces.clone().unwrap_or_default(),
289 skills_paths: self.config.skills_paths.clone(),
290 tool_runner: Some(self.tool_runner.clone()),
291 hook_runner: Some(self.hook_runner.clone()),
292 conversation_id: self.config.conversation_id.clone().unwrap_or_default(),
293 };
294
295 let conn = strategy.connect().await?;
296 let conversation = Arc::new(Conversation::new(
297 crate::connection::AnyConnection::Wasm(Arc::new(conn)),
298 None,
299 ));
300 self.conversation = Some(conversation.clone());
301
302 if !self.config.triggers.is_empty() {
304 let runner = TriggerRunner::new(self.config.triggers.clone());
305 runner.start(&conversation.connection());
306 self.trigger_runner = Some(runner);
307 }
308
309 Ok(())
310 }
311
312 #[cfg(not(target_arch = "wasm32"))]
313 {
314 let mut cap = self.config.capabilities.clone();
315 if let Some(ref schema) = self.config.response_schema {
316 cap.finish_tool_schema_json = Some(schema.clone());
317 }
318
319 let strategy = LocalConnectionStrategy::new(
320 binary_path,
321 self.config.gemini_config.clone(),
322 cap,
323 self.config.system_instructions.clone(),
324 self.config.save_dir.clone(),
325 self.config.workspaces.clone().unwrap_or_default(),
326 self.config.skills_paths.clone(),
327 Some(self.tool_runner.clone()),
328 Some(self.hook_runner.clone()),
329 self.config.conversation_id.clone().unwrap_or_default(),
330 );
331
332 let conn = strategy.connect().await?;
333 let conversation = Arc::new(Conversation::new(
334 crate::connection::AnyConnection::Local(Arc::new(conn)),
335 None,
336 ));
337 self.conversation = Some(conversation.clone());
338
339 if !self.config.triggers.is_empty() {
341 let runner = TriggerRunner::new(self.config.triggers.clone());
342 runner.start(&conversation.connection());
343 self.trigger_runner = Some(runner);
344 }
345
346 Ok(())
347 }
348 }
349
350 pub async fn chat(&self, prompt: &str) -> Result<ChatResponse, anyhow::Error> {
356 let conversation = self.conversation()?;
357 conversation.chat_to_completion(prompt).await
358 }
359
360 pub fn conversation(&self) -> Result<Arc<Conversation>, anyhow::Error> {
366 self.conversation
367 .clone()
368 .ok_or_else(|| anyhow!("Agent session not started. Use start() first."))
369 }
370
371 pub fn conversation_id(&self) -> Option<String> {
373 self.conversation
374 .as_ref()
375 .map(|c| c.conversation_id().to_string())
376 }
377
378 pub async fn stop(&mut self) -> Result<(), anyhow::Error> {
384 if let Some(conversation) = self.conversation.take() {
385 conversation.disconnect().await?;
386 }
387 Ok(())
388 }
389}
390
391#[cfg(not(target_arch = "wasm32"))]
392fn get_default_binary_path() -> Option<String> {
393 if let Ok(path) = std::env::var("ANTIGRAVITY_HARNESS_PATH") {
394 return Some(path);
395 }
396 if let Ok(paths) = std::env::var("PATH") {
398 for path in std::env::split_paths(&paths) {
399 let p = path.join("localharness");
400 if p.exists() {
401 return Some(p.to_string_lossy().into_owned());
402 }
403 }
404 }
405 if let Some(output) = std::process::Command::new("python3")
407 .args([
408 "-c",
409 "import site; print('\\n'.join(site.getsitepackages()))",
410 ])
411 .output()
412 .ok()
413 .filter(|o| o.status.success())
414 {
415 let stdout = String::from_utf8_lossy(&output.stdout);
416 for line in stdout.lines() {
417 let p = std::path::Path::new(line.trim())
418 .join("google")
419 .join("antigravity")
420 .join("bin")
421 .join("localharness");
422 if p.exists() {
423 return Some(p.to_string_lossy().into_owned());
424 }
425 }
426 }
427 None
428}