agy-bridge
Rust bridge for the
Google Antigravity SDK
via PyO3.
This is not a Google product. Use at your own risk.
Installation
Add agy-bridge to your Cargo.toml:
[dependencies]
agy-bridge = "0.1.0"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
Install the Python SDK:
pip install google-antigravity watchfiles
Optional Python Dependencies
Some features require additional Python packages:
| Package |
Required for |
Install |
watchfiles |
TriggerConfig::on_file_change() triggers |
pip install watchfiles |
The every() timer trigger works out of the box. File-change triggers
lazily import watchfiles at runtime and raise a clear ImportError if
it is missing.
Quick Start
use agy_bridge::AgyBridge;
#[tokio::main]
async fn main() -> Result<(), agy_bridge::error::Error> {
# agy_bridge::load_dotenv();
let bridge = AgyBridge::builder().build()?;
# let agent = bridge.agent(
# agy_bridge::config::AgentConfig::builder()
# .system_instructions("Reply with plain text only. Never use tools.")
# .build(),
# ).await?;
let text = agent.chat("Hello!").await?.text().await?;
println!("{text}");
agent.shutdown().await?;
Ok(())
}
Streaming Responses
use agy_bridge::{AgyBridge, config::AgentConfig};
#[tokio::main]
async fn main() -> Result<(), agy_bridge::error::Error> {
# agy_bridge::load_dotenv();
let bridge = AgyBridge::builder().build()?;
let agent = bridge
.agent(
AgentConfig::builder()
.system_instructions("You are a poet.")
.build(),
)
.await?;
let mut response = agent.chat("Write a short poem about space.").await?;
if let Some(mut stream) = response.take_text_stream() {
while let Some(chunk) = stream.recv().await {
print!("{chunk}");
}
}
println!();
agent.shutdown().await?;
Ok(())
}
Features
Multimodal Content
Pass text, images, audio, video, and documents in a single chat turn:
use agy_bridge::{
AgyBridge,
content::{Content, ContentPrimitive, Image},
};
#[tokio::main]
async fn main() -> Result<(), agy_bridge::error::Error> {
# agy_bridge::load_dotenv();
let bridge = AgyBridge::builder().build()?;
let agent = bridge.default_agent().await?;
let image = Image::from_file("blank.png")?;
let content = Content::Multi {
parts: vec![
ContentPrimitive::Text { text: "Describe this image.".into() },
ContentPrimitive::Image(image),
],
};
let text = agent.chat(content).await?.text().await?;
println!("{text}");
agent.shutdown().await?;
Ok(())
}
Custom Tools
Define tools with the #[llm_tool] proc macro — doc comments become descriptions:
use agy_bridge::{AgyBridge, config::AgentConfig, prelude::*, tools::ToolRegistry};
#[llm_tool]
fn get_weather(
city: &str,
) -> Result<String, String> {
Ok(format!("It's sunny in {city}."))
}
#[tokio::main]
async fn main() -> Result<(), agy_bridge::error::Error> {
# agy_bridge::load_dotenv();
let bridge = AgyBridge::builder().build()?;
let mut registry = ToolRegistry::new();
registry.register(GetWeather);
let agent = bridge
.agent(AgentConfig::builder().build())
.tools(registry)
.await?;
let text = agent.chat("What's the weather in Tokyo?").await?.text().await?;
println!("{text}");
agent.shutdown().await?;
Ok(())
}
For full control, implement the RustTool trait directly:
use agy_bridge::tools::{JsonSchema, RustTool, ToolContext, ToolError, ToolOutput, ToolRegistry};
use serde::Deserialize;
#[derive(Deserialize, JsonSchema)]
struct SearchParams {
query: String,
}
struct SearchTool;
impl RustTool for SearchTool {
type Params = SearchParams;
const NAME: &'static str = "search";
const DESCRIPTION: &'static str = "Search a knowledge base";
async fn call(&self, params: Self::Params, _ctx: &ToolContext) -> Result<ToolOutput, ToolError> {
Ok(format!("Results for: {}", params.query).into())
}
}
let registry = ToolRegistry::new().with_tool(SearchTool);
assert_eq!(registry.definitions().len(), 1);
MCP Integration
Connect external MCP servers:
use agy_bridge::config::{AgentConfig, McpServer};
let server = McpServer::stdio("npx")
.args([
"-y",
"@modelcontextprotocol/server-postgres",
"postgresql://postgres:postgres@localhost:5432/postgres",
])
.build();
let config = AgentConfig::builder().mcp_servers([server]).build();
assert_eq!(config.mcp_servers.len(), 1);
Hooks and Policies
Control agent behavior with hooks and a declarative policy system:
use agy_bridge::{
AgyBridge,
config::AgentConfig,
hooks::{HookResult, Hooks},
};
#[tokio::main]
async fn main() -> Result<(), agy_bridge::error::Error> {
# agy_bridge::load_dotenv();
let bridge = AgyBridge::builder().build()?;
let mut hooks = Hooks::new();
hooks.on_pre_turn("turn_logger", |ctx| {
println!("[turn {}] {}", ctx.turn_number, ctx.prompt);
});
hooks.on_pre_tool_call_decide("safety_gate", |ctx| {
if ctx.tool_name == "dangerous_tool" {
HookResult::deny("blocked by policy")
} else {
HookResult::allow()
}
});
let config = AgentConfig::builder().build();
let agent = bridge
.agent(config)
.hooks(hooks)
.await?;
let text = agent.chat("What is the capital of Japan?").await?.text().await?;
println!("{text}");
agent.shutdown().await?;
Ok(())
}
use agy_bridge::policies::{PolicyRule, PolicySet};
let mut policies = PolicySet::new();
policies.push(PolicyRule::allow("view_file")).unwrap();
policies.push(PolicyRule::deny("run_command")).unwrap();
policies.push(PolicyRule::DenyAll).unwrap();
assert!(policies.evaluate("view_file").is_allowed());
assert!(policies.evaluate("run_command").is_denied());
assert!(policies.evaluate("unknown_tool").is_denied());
Triggers
Run background tasks that react to timers or file changes:
use agy_bridge::{
AgyBridge,
config::AgentConfig,
triggers::{TriggerConfig, TriggerEntry},
};
#[tokio::main]
async fn main() -> Result<(), agy_bridge::error::Error> {
# agy_bridge::load_dotenv();
# std::fs::create_dir_all("/tmp/workspace")?;
let bridge = AgyBridge::builder().build()?;
let periodic = TriggerEntry {
name: "poll_status".into(),
config: TriggerConfig::every_secs(30),
message_template: "Check deployment status".into(),
};
let file_watch = TriggerEntry {
name: "watch_workspace".into(),
config: TriggerConfig::on_file_change("/tmp/workspace"),
message_template: "Files changed: {changes}".into(),
};
let config = AgentConfig::builder()
.triggers(vec![periodic, file_watch])
.build();
let agent = bridge.agent(config).await?;
let text = agent.chat("Hello!").await?.text().await?;
println!("{text}");
agent.shutdown().await?;
# let _ = std::fs::remove_dir_all("/tmp/workspace");
Ok(())
}
Note: on_file_change triggers require the watchfiles Python package
(pip install watchfiles). Timer triggers (every) work without extra
dependencies.
Subagents
Spawn child agents that share the parent's runtime:
use agy_bridge::{AgyBridge, config::AgentConfig};
#[tokio::main]
async fn main() -> Result<(), agy_bridge::error::Error> {
# agy_bridge::load_dotenv();
let bridge = AgyBridge::builder().build()?;
let parent = bridge.agent(
AgentConfig::builder()
.system_instructions("You are a coordinator.")
.build(),
).await?;
let child = parent.spawn_subagent(
AgentConfig::builder()
.system_instructions("You are a math specialist.")
.model("gemini-3.5-flash")
.build(),
None,
).await?;
let text = child.chat("What is 17 * 23?").await?.text().await?;
println!("{text}");
child.shutdown().await?;
parent.shutdown().await?;
Ok(())
}
Examples
The examples/ directory contains runnable programs for every feature:
Getting Started
Deep Dives
cargo run --example getting_started_hello_world
License
Licensed under either of:
at your option.