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 futures_util::future::BoxFuture;
13use std::sync::Arc;
14
15#[derive(Default)]
17pub struct AgentConfig {
18 pub binary_path: Option<String>,
21 pub gemini_config: GeminiConfig,
23 pub capabilities: CapabilitiesConfig,
25 pub system_instructions: Option<SystemInstructions>,
27 pub save_dir: Option<String>,
29 pub workspaces: Option<Vec<String>>,
31 pub skills_paths: Vec<String>,
33 pub policies: Option<Vec<Policy>>,
35 pub hooks: Vec<Arc<dyn DynHook>>,
37 pub triggers: Vec<Arc<dyn DynTrigger>>,
39 pub tools: Vec<Arc<dyn DynTool>>,
41 pub conversation_id: Option<String>,
43 pub app_data_dir: Option<String>,
45 pub response_schema: Option<String>,
47}
48
49impl std::fmt::Debug for AgentConfig {
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 f.debug_struct("AgentConfig")
52 .field("binary_path", &self.binary_path)
53 .field("gemini_config", &self.gemini_config)
54 .field("capabilities", &self.capabilities)
55 .field("system_instructions", &self.system_instructions)
56 .field("save_dir", &self.save_dir)
57 .field("workspaces", &self.workspaces)
58 .field("skills_paths", &self.skills_paths)
59 .field("policies", &self.policies)
60 .field("hooks_count", &self.hooks.len())
61 .field("triggers_count", &self.triggers.len())
62 .field("tools_count", &self.tools.len())
63 .field("conversation_id", &self.conversation_id)
64 .field("app_data_dir", &self.app_data_dir)
65 .field("response_schema", &self.response_schema)
66 .finish()
67 }
68}
69
70pub trait AgentLifecycle: Send + Sync + std::fmt::Debug {}
96
97#[derive(Debug)]
99pub struct Unstarted;
100impl AgentLifecycle for Unstarted {}
101
102pub struct Started {
104 pub(crate) conversation: Arc<Conversation>,
105 pub(crate) trigger_runner: Option<TriggerRunner>,
106}
107
108impl AgentLifecycle for Started {}
109
110impl std::fmt::Debug for Started {
111 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112 f.debug_struct("Started")
113 .field("conversation", &self.conversation)
114 .field("trigger_runner", &self.trigger_runner)
115 .finish()
116 }
117}
118
119pub struct Agent<S: AgentLifecycle = Unstarted> {
120 config: AgentConfig,
121 tool_runner: ToolRunner,
122 hook_runner: HookRunner,
123 state: S,
124}
125
126impl<S: AgentLifecycle> std::fmt::Debug for Agent<S> {
127 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128 f.debug_struct("Agent")
129 .field("config", &self.config)
130 .field("tool_runner", &self.tool_runner)
131 .field("hook_runner", &self.hook_runner)
132 .field("state", &self.state)
133 .finish()
134 }
135}
136
137impl Agent<Unstarted> {
138 pub fn new(config: AgentConfig) -> Self {
140 Self {
141 config,
142 tool_runner: ToolRunner::new(),
143 hook_runner: HookRunner::new(),
144 state: Unstarted,
145 }
146 }
147
148 pub fn builder() -> AgentBuilder<NoPolicies> {
150 AgentBuilder::new()
151 }
152
153 pub fn register_hook(&mut self, hook: Arc<dyn DynHook>) {
155 self.config.hooks.push(hook);
156 }
157
158 pub fn register_trigger(&mut self, trigger: Arc<dyn DynTrigger>) -> Result<(), anyhow::Error> {
160 self.config.triggers.push(trigger);
161 Ok(())
162 }
163
164 pub fn register_tool(&mut self, tool: Arc<dyn DynTool>) {
166 self.config.tools.push(tool);
167 }
168
169 #[allow(clippy::too_many_lines)]
179 pub fn start(self) -> BoxFuture<'static, Result<Agent<Started>, anyhow::Error>> {
180 Box::pin(async move {
181 #[cfg(not(target_arch = "wasm32"))]
183 let binary_path = self.config.binary_path.clone()
184 .or_else(get_default_binary_path)
185 .ok_or_else(|| anyhow!("Could not find default localharness binary. Please specify binary_path explicitly."))?;
186
187 for hook in &self.config.hooks {
189 self.hook_runner.register(hook.clone()).await;
190 }
191
192 let enabled_tools = self.config.capabilities.enabled_tools.clone();
194 let disabled_tools = self.config.capabilities.disabled_tools.clone();
195 if enabled_tools.is_some() && disabled_tools.is_some() {
196 return Err(anyhow!(
197 "enabled_tools and disabled_tools are mutually exclusive"
198 ));
199 }
200
201 let active_tools = enabled_tools.unwrap_or_else(|| {
202 disabled_tools.map_or_else(
203 || {
204 vec![
205 BuiltinTools::CreateFile,
206 BuiltinTools::EditFile,
207 BuiltinTools::FindFile,
208 BuiltinTools::ListDir,
209 BuiltinTools::RunCommand,
210 BuiltinTools::SearchDir,
211 BuiltinTools::ViewFile,
212 BuiltinTools::StartSubagent,
213 BuiltinTools::GenerateImage,
214 BuiltinTools::Finish,
215 ]
216 },
217 |disabled| {
218 let all = vec![
219 BuiltinTools::CreateFile,
220 BuiltinTools::EditFile,
221 BuiltinTools::FindFile,
222 BuiltinTools::ListDir,
223 BuiltinTools::RunCommand,
224 BuiltinTools::SearchDir,
225 BuiltinTools::ViewFile,
226 BuiltinTools::StartSubagent,
227 BuiltinTools::GenerateImage,
228 BuiltinTools::Finish,
229 ];
230 all.into_iter().filter(|t| !disabled.contains(t)).collect()
231 },
232 )
233 });
234
235 let read_only = BuiltinTools::read_only();
236 let has_write_tools = active_tools.iter().any(|t| !read_only.contains(t));
237
238 let mut final_policies = self.config.policies.clone().unwrap_or_else(|| {
240 policy::confirm_run_command(None)
242 });
243
244 let has_allow_all = final_policies.iter().any(|p| {
253 p.tool == "*"
254 && p.decision == crate::policy::Decision::Approve
255 && p.name == "allow_all"
256 });
257
258 if !has_allow_all {
259 let workspaces = self.config.workspaces.clone().unwrap_or_else(|| {
260 std::env::current_dir().map_or_else(
261 |_| Vec::new(),
262 |cwd| vec![cwd.to_string_lossy().into_owned()],
263 )
264 });
265
266 if !workspaces.is_empty() {
267 let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
268 let app_data_dir = self
269 .config
270 .app_data_dir
271 .clone()
272 .unwrap_or_else(|| format!("{home}/.gemini/antigravity"));
273 let mut allowed_paths = workspaces;
274 allowed_paths.push(app_data_dir);
275 let mut ws_policies = policy::workspace_only(allowed_paths);
276 ws_policies.append(&mut final_policies);
277 final_policies = ws_policies;
278 }
279 }
280
281 if has_write_tools && final_policies.is_empty() {
283 return Err(anyhow!(
284 "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."
285 ));
286 }
287
288 if !final_policies.is_empty() {
289 let enforcer = Arc::new(PolicyEnforcer::new(final_policies));
290 self.hook_runner.register(enforcer).await;
291 }
292
293 for tool in &self.config.tools {
295 self.tool_runner.register(tool.clone()).await;
296 }
297
298 #[cfg(target_arch = "wasm32")]
300 {
301 let mut cap = self.config.capabilities.clone();
302 if let Some(ref schema) = self.config.response_schema {
303 cap.finish_tool_schema_json = Some(schema.clone());
304 }
305
306 let strategy = crate::wasm::WasmConnectionStrategy {
307 gemini_config: self.config.gemini_config.clone(),
308 capabilities_config: cap,
309 system_instructions: self.config.system_instructions.clone(),
310 save_dir: self.config.save_dir.clone(),
311 workspaces: self.config.workspaces.clone().unwrap_or_default(),
312 skills_paths: self.config.skills_paths.clone(),
313 tool_runner: Some(self.tool_runner.clone()),
314 hook_runner: Some(self.hook_runner.clone()),
315 conversation_id: self.config.conversation_id.clone().unwrap_or_default(),
316 };
317
318 let conn = strategy.connect().await?;
319 let conversation = Arc::new(Conversation::new(
320 crate::connection::AnyConnection::Wasm(Arc::new(conn)),
321 None,
322 ));
323
324 let mut trigger_runner = None;
326 if !self.config.triggers.is_empty() {
327 let runner = TriggerRunner::new(self.config.triggers.clone());
328 runner.start(&conversation.connection());
329 trigger_runner = Some(runner);
330 }
331
332 Ok(Agent {
333 config: self.config,
334 tool_runner: self.tool_runner,
335 hook_runner: self.hook_runner,
336 state: Started {
337 conversation,
338 trigger_runner,
339 },
340 })
341 }
342
343 #[cfg(not(target_arch = "wasm32"))]
344 {
345 let mut cap = self.config.capabilities.clone();
346 if let Some(ref schema) = self.config.response_schema {
347 cap.finish_tool_schema_json = Some(schema.clone());
348 }
349
350 let strategy = LocalConnectionStrategy::new(
351 binary_path,
352 self.config.gemini_config.clone(),
353 cap,
354 self.config.system_instructions.clone(),
355 self.config.save_dir.clone(),
356 self.config.workspaces.clone().unwrap_or_default(),
357 self.config.skills_paths.clone(),
358 Some(self.tool_runner.clone()),
359 Some(self.hook_runner.clone()),
360 self.config.conversation_id.clone().unwrap_or_default(),
361 );
362
363 let conn = strategy.connect().await?;
364 let conversation = Arc::new(Conversation::new(
365 crate::connection::AnyConnection::Local(Arc::new(conn)),
366 None,
367 ));
368
369 let trigger_runner = if self.config.triggers.is_empty() {
371 None
372 } else {
373 let runner = TriggerRunner::new(self.config.triggers.clone());
374 runner.start(&conversation.connection());
375 Some(runner)
376 };
377
378 Ok(Agent {
379 config: self.config,
380 tool_runner: self.tool_runner,
381 hook_runner: self.hook_runner,
382 state: Started {
383 conversation,
384 trigger_runner,
385 },
386 })
387 }
388 }) }
390}
391
392impl Agent<Started> {
393 pub async fn chat(&self, prompt: &str) -> Result<ChatResponse, anyhow::Error> {
399 self.state.conversation.chat_to_completion(prompt).await
400 }
401
402 pub fn conversation(&self) -> Arc<Conversation> {
404 self.state.conversation.clone()
405 }
406
407 pub fn conversation_id(&self) -> String {
409 self.state.conversation.conversation_id().to_string()
410 }
411
412 pub async fn stop(&self) -> Result<(), anyhow::Error> {
418 self.state.conversation.disconnect().await?;
419 Ok(())
420 }
421}
422
423#[derive(Debug)]
424pub struct NoPolicies;
425#[derive(Debug)]
426pub struct HasPolicies;
427
428pub struct AgentBuilder<P = NoPolicies> {
429 config: AgentConfig,
430 _policy_marker: std::marker::PhantomData<P>,
431}
432
433impl<P> std::fmt::Debug for AgentBuilder<P> {
434 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
435 f.debug_struct("AgentBuilder")
436 .field("config", &self.config)
437 .finish()
438 }
439}
440
441impl AgentBuilder<NoPolicies> {
442 pub fn new() -> Self {
443 Self {
444 config: AgentConfig::default(),
445 _policy_marker: std::marker::PhantomData,
446 }
447 }
448}
449
450impl Default for AgentBuilder<NoPolicies> {
451 fn default() -> Self {
452 Self::new()
453 }
454}
455
456impl<P> AgentBuilder<P> {
457 pub fn binary_path(mut self, path: impl Into<String>) -> Self {
458 self.config.binary_path = Some(path.into());
459 self
460 }
461
462 pub fn gemini_config(mut self, gemini_config: GeminiConfig) -> Self {
463 self.config.gemini_config = gemini_config;
464 self
465 }
466
467 pub fn api_key(mut self, key: impl Into<String>) -> Self {
468 self.config.gemini_config.api_key = Some(key.into());
469 self
470 }
471
472 pub fn default_model(mut self, model: impl Into<String>) -> Self {
473 self.config.gemini_config.models.default.name = model.into();
474 self
475 }
476
477 pub fn capabilities(mut self, capabilities: CapabilitiesConfig) -> Self {
478 self.config.capabilities = capabilities;
479 self
480 }
481
482 pub fn system_instructions(mut self, system_instructions: SystemInstructions) -> Self {
483 self.config.system_instructions = Some(system_instructions);
484 self
485 }
486
487 pub fn save_dir(mut self, save_dir: impl Into<String>) -> Self {
488 self.config.save_dir = Some(save_dir.into());
489 self
490 }
491
492 pub fn workspaces(mut self, workspaces: Vec<String>) -> Self {
493 self.config.workspaces = Some(workspaces);
494 self
495 }
496
497 pub fn skills_paths(mut self, skills_paths: Vec<String>) -> Self {
498 self.config.skills_paths = skills_paths;
499 self
500 }
501
502 pub fn hooks(mut self, hooks: Vec<Arc<dyn DynHook>>) -> Self {
503 self.config.hooks = hooks;
504 self
505 }
506
507 pub fn triggers(mut self, triggers: Vec<Arc<dyn DynTrigger>>) -> Self {
508 self.config.triggers = triggers;
509 self
510 }
511
512 pub fn tools(mut self, tools: Vec<Arc<dyn DynTool>>) -> Self {
513 self.config.tools = tools;
514 self
515 }
516
517 pub fn tool(mut self, tool: Arc<dyn DynTool>) -> Self {
518 self.config.tools.push(tool);
519 self
520 }
521
522 pub fn hook(mut self, hook: Arc<dyn DynHook>) -> Self {
523 self.config.hooks.push(hook);
524 self
525 }
526
527 pub fn trigger(mut self, trigger: Arc<dyn DynTrigger>) -> Self {
528 self.config.triggers.push(trigger);
529 self
530 }
531
532 pub fn policy(mut self, policy: Policy) -> AgentBuilder<HasPolicies> {
533 let mut policies = self.config.policies.take().unwrap_or_default();
534 policies.push(policy);
535 self.config.policies = Some(policies);
536 AgentBuilder {
537 config: self.config,
538 _policy_marker: std::marker::PhantomData,
539 }
540 }
541
542 pub fn conversation_id(mut self, conversation_id: impl Into<String>) -> Self {
543 self.config.conversation_id = Some(conversation_id.into());
544 self
545 }
546
547 pub fn app_data_dir(mut self, app_data_dir: impl Into<String>) -> Self {
548 self.config.app_data_dir = Some(app_data_dir.into());
549 self
550 }
551
552 pub fn response_schema(mut self, response_schema: impl Into<String>) -> Self {
553 self.config.response_schema = Some(response_schema.into());
554 self
555 }
556
557 pub fn policies(self, policies: Vec<Policy>) -> AgentBuilder<HasPolicies> {
558 let mut config = self.config;
559 config.policies = Some(policies);
560 AgentBuilder {
561 config,
562 _policy_marker: std::marker::PhantomData,
563 }
564 }
565
566 pub fn allow_all(self) -> AgentBuilder<HasPolicies> {
567 let mut config = self.config;
568 config.policies = Some(vec![policy::allow_all()]);
569 AgentBuilder {
570 config,
571 _policy_marker: std::marker::PhantomData,
572 }
573 }
574
575 pub fn read_only(self) -> AgentBuilder<HasPolicies> {
576 let mut config = self.config;
577 let read_only_tools = BuiltinTools::read_only();
578 let mut policies = vec![policy::deny_all()];
579 for tool in read_only_tools {
580 policies.push(policy::allow(tool.as_str()));
581 }
582 config.policies = Some(policies);
583 AgentBuilder {
584 config,
585 _policy_marker: std::marker::PhantomData,
586 }
587 }
588
589 pub fn build_unchecked(self) -> Agent<Unstarted> {
591 Agent::new(self.config)
592 }
593}
594
595impl AgentBuilder<HasPolicies> {
596 pub fn build(self) -> Agent<Unstarted> {
597 Agent::new(self.config)
598 }
599}
600
601#[cfg(not(target_arch = "wasm32"))]
602fn get_default_binary_path() -> Option<String> {
603 if let Ok(path) = std::env::var("ANTIGRAVITY_HARNESS_PATH") {
604 return Some(path);
605 }
606 if let Ok(paths) = std::env::var("PATH") {
608 for path in std::env::split_paths(&paths) {
609 let p = path.join("localharness");
610 if p.exists() {
611 return Some(p.to_string_lossy().into_owned());
612 }
613 }
614 }
615 if let Some(output) = std::process::Command::new("python3")
617 .args([
618 "-c",
619 "import site; print('\\n'.join(site.getsitepackages()))",
620 ])
621 .output()
622 .ok()
623 .filter(|o| o.status.success())
624 {
625 let stdout = String::from_utf8_lossy(&output.stdout);
626 for line in stdout.lines() {
627 let p = std::path::Path::new(line.trim())
628 .join("google")
629 .join("antigravity")
630 .join("bin")
631 .join("localharness");
632 if p.exists() {
633 return Some(p.to_string_lossy().into_owned());
634 }
635 }
636 }
637 None
638}