Skip to main content

Crate agy_bridge

Crate agy_bridge 

Source
Expand description

§agy-bridge

Build LLM agents in Rust — with tools, hooks, streaming, and multimodal input.

Rust bridge for the Google Antigravity SDK via PyO3.

Rust’s compile-time checks make it a natural fit for vibe coding agents — schema mismatches, missing parameters, and typos in tool definitions (via #[llm_tool]) are caught at build time, not at runtime.

§Installation

Add agy-bridge to your Cargo.toml:

[dependencies]
agy-bridge = "0.1.4"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

Install the Python SDK:

pip install google-antigravity watchfiles

watchfiles is only needed for file-change triggers; timer triggers work without it.

§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.default_agent().await?;

    // Send a message and get the full reply text.
    let text = agent
        .chat("Reply with 'Hello!' and nothing else.")
        .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()?;

    // bridge.agent() returns an AgentBuilder — chain .tools() / .hooks()
    // before .await, or .await directly for a bare agent.
    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?;

    // Load an image from a file path (auto-detects MIME type):
    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};

/// Gets the current weather for a city.
#[llm_tool]
fn get_weather(
    /// The city to look up.
    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 {
    /// The search query string.
    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()?;

    // 1. Register lifecycle hooks
    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()
        }
    });

    // 2. Create the agent configuration
    let config = AgentConfig::builder().build();

    // 3. Pass hooks to the agent builder
    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();
    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(std::env::current_dir()?),
        message_template: "Files changed: {changes}".into(),
    };

    let config = AgentConfig::builder()
        .triggers(vec![periodic, file_watch])
        .build();

    let agent = bridge.agent(config).await?;

    // The agent will now automatically run triggers in the background.
    let text = agent.chat("Hello!").await?.text().await?;
    println!("{text}");

    agent.shutdown().await?;
    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

ExampleDescription
hello_worldMinimal agent — create, prompt, print
streamingStream text tokens as they arrive
custom_tools#[llm_tool] proc macro
multimodalText + image input
mcp_toolsMCP server integration
structured_outputJSON schema–constrained responses
hooksLifecycle hooks
policiesDeclarative tool access control
triggersPeriodic and file-change triggers
subagentsParent/child agent spawning
human_in_the_loopHuman-in-the-loop chat patterns
persistenceConversation persistence
observabilityTracing and usage metadata
error_handlerStructured error handling patterns
autonomous_shellAutonomous shell command execution
persona_configCustom persona and model configuration
agent_skillsAgent skill registration
app_data_dir_overrideCustom app data directory

§Deep Dives

ExampleDescription
async_chatAdvanced async chat patterns
round_based_chatMulti-round structured conversations
agent_middlewareHook-based middleware pipeline
host_tool_hooksHost-side tool interception hooks
interactive_cliMulti-turn CLI chat application
multimodal_pipelineMulti-stage multimodal processing
doc_maintenance_agentDocumentation maintenance agent
docstring_maintenance_agentCode docstring maintenance agent
cargo run --example getting_started_hello_world

§License

Licensed under either of:

at your option.


This is not a Google product. Use at your own risk. agy-bridge: Standalone reusable PyO3 bridge for the Google Antigravity SDK.

Re-exports§

pub use config::AgentConfig;
pub use config::BuiltinTools;
pub use config::CapabilitiesConfig;
pub use config::GeminiConfig;
pub use config::LocalAgentConfig;
pub use config::McpServer;
pub use config::McpSseServer;
pub use config::McpStdioServer;
pub use config::McpStreamableHttpServer;
pub use config::SystemInstructions;
pub use content::Audio;
pub use content::Content;
pub use content::ContentPrimitive;
pub use content::Document;
pub use content::Image;
pub use content::Video;
pub use error::Error;
pub use hooks::HookCallback;
pub use hooks::HookEntry;
pub use hooks::HookPoint;
pub use hooks::HookResult;
pub use hooks::HookSet;
pub use hooks::Hooks;
pub use policies::AskUserHandler;
pub use policies::PolicyDecision;
pub use policies::PolicyRule;
pub use policies::PolicySet;
pub use runtime::RuntimeConfig;
pub use streaming::ChatResponseHandle;
pub use streaming::ChatResult;
pub use streaming::ResponseEvent;
pub use streaming::StreamChunk;
pub use triggers::TriggerConfig;
pub use triggers::TriggerEntry;
pub use types::ConversationMessage;
pub use types::MessageRole;
pub use types::Step;
pub use types::UsageMetadata;

Modules§

agent
Agent lifecycle management: creation, chat, shutdown. Agent lifecycle management for the Antigravity SDK bridge.
config
Configuration types for agents, models, capabilities, and MCP servers. Agent configuration bridge types.
content
Multimodal content types for chat input (text, image, document, audio, video). Multimodal content types for chat input (text, image, document, audio, video).
error
Error types for the bridge. Bridge error types and helpers for mapping Python exceptions to Rust errors.
hooks
Pre/post-turn and tool-call lifecycle hooks. Hook bridge for the Antigravity SDK.
policies
Policy rules for tool-call filtering and workspace scoping. Policy bridge for the Antigravity SDK.
prelude
Convenience prelude — pull in everything you need with a single glob import.
quota
Quota tracking and backoff state. Quota tracking and exponential backoff with jitter.
runtime
Python runtime bridge: command dispatch over a dedicated thread. Python runtime manager: owns a dedicated Python thread with an asyncio event loop.
streaming
Streaming response channels for text, thought, and tool-call events. Streaming response bridge for the Antigravity SDK.
tools
Custom Rust tool dispatch and definition types. Custom tool registration for the Antigravity SDK bridge.
triggers
Event-driven trigger definitions. Trigger configuration types for the Python Antigravity SDK.
types
Shared domain types (messages, steps, usage metadata). Core types for the agent SDK bridge.

Structs§

AgentBuilder
Builder for creating an Agent on an AgyBridge.
AgyBridge
Primary entry point for the Antigravity bridge.
AgyBridgeBuilder
Builder for constructing an AgyBridge instance.
ToolContext
Context passed to Rust tools during dispatch, mirroring the Python SDK’s ToolContext.
ToolDefinition
Describes a custom tool that can be registered with an agent.
ToolError
An error returned from a tool execution. The error message is sent back to the model as the tool’s error response. Structured metadata can be attached for hooks and logging — it is not sent to the model.
ToolOutput
The return value of a Rust tool execution.
ToolRegistry

Traits§

RustTool
A custom tool implemented entirely in Rust with strongly-typed parameters.

Functions§

load_dotenv
Load environment variables from a .env file into the process environment.

Type Aliases§

Agent
Convenience alias for an agent backed by the bridge’s runtime.

Attribute Macros§

llm_tool
Re-export the #[llm_tool] proc-macro so users only need agy_bridge in their dependency list. Transforms a function into a RustTool implementation.