This document describes the high-level architecture, design patterns, and components of the Antigravity Rust SDK.
The Antigravity SDK orchestrates interactions between an LLM-based agent (running inside a local or remote harness) and the local client system. It manages process execution, IPC handshake, WebSocket event streaming, tool calls, policy middleware, and user hooks.
graph TD
A[Agent] --> B[Conversation]
A --> C[LocalConnection / WasmConnection]
A --> D[ToolRunner]
A --> E[HookRunner]
A --> I[TriggerRunner]
C -->|Subprocess IPC / WebSocket| F[localharness]
C -->|Network WebSocket| F
D -->|Executes| G[Local Tools]
E -->|Intercepts| H[User Hooks / Policies]
I -->|Spawns| J[Background Triggers]
The SDK leverages several object-oriented and functional design patterns:
- Connection Trait: Defines an abstraction for communication. This allows swapping the local subprocess harness with other backends (e.g., remote, mock, or WASM-based network harnesses) in the future.
- LocalConnectionStrategy: Configures and initializes a
LocalConnectionby spawning a local helper subprocess (for native / non-WASM environments). - WasmConnectionStrategy: Configures and initializes a
WasmConnectionthat connects to a remote or host-sidelocalharnessWebSocket server over TCP (enabled fortarget_arch = "wasm32"environments where subprocess spawning is not supported).
- Hook Trait: Defines lifecycle hooks that users can register to observe and modify agent actions:
on_session_start()pre_turn()pre_tool_call()post_tool_call()on_tool_error()on_interaction()
- HookRunner: Coordinates a thread-safe list of observers (
Arc<dyn DynHook>) and dispatches events asynchronously.
- Policy: Acts as a middleware layer to authorize, deny, or intercept tool calls before they are executed.
- Included Policies:
workspace_only(paths): Blocks tool calls targeting directories outside the specified workspaces.confirm_run_command(): Prompts user authorization or automatically enforces constraints before running shell commands.
- Tool Trait: Encapsulates specific capabilities (e.g., file edits, command execution, directory searching) into unified command units.
- ToolRunner: Coordinates registration and execution of these command objects, mapping harness tool calls to their respective handlers.
- Trigger Trait: Defines asynchronous background tasks (such as status polling, listener intervals, etc.) that can interact with the connection session concurrently.
- TriggerRunner: Coordinates and spawns registered triggers in separate tasks when the agent session starts.
- Enforces at compile-time that session-level actions (e.g., calling
chat(), callingstop(), or accessing the activeconversation()) can only be performed after the agent has been successfully started. - The
Agent<S>struct is generic over a marker lifecycle typeS: AgentLifecyclewhich can beUnstartedorStarted. - Calling
agent.start().awaitconsumes theAgent<Unstarted>instance and returns a Result holding anAgent<Started>instance upon a successful handshake.
- The
AgentBuilderprovides a fluent configuration API. - Leverages compile-time phantom data state (
NoPoliciesvsHasPolicies) to guarantee that safety policies must be explicitly configured or bypassed before an agent can be constructed viabuild(). - An escape hatch
build_unchecked()is provided for advanced scenarios (e.g., during programmatic test setup).
The connection to localharness via subprocess follows a strict handshake and upgrade protocol:
sequenceDiagram
participant SDK as Rust SDK
participant Sub as Subprocess (localharness)
participant WS as WebSocket Server
SDK->>Sub: Spawn subprocess (stdout/stderr piped)
SDK->>Sub: Send InputConfig (length-prefixed proto)
Sub->>SDK: Reply OutputConfig (length-prefixed proto with Port & API Key)
Note over SDK,Sub: Stdin/Stdout handshake complete
SDK->>WS: Establish WebSocket Connection (with API Key header)
WS->>SDK: Handshake Completed
Note over SDK,WS: Step execution loop active
- Subprocess Spawn: The SDK spawns the
localharnessbinary as a child process. - Handshake: The SDK sends an
InputConfig(serialized protocol buffer, prefixed by its length in bytes) over stdin. The harness replies with anOutputConfigcontaining the dynamically selected port and a secure API key. - Upgrade: The SDK initiates a WebSocket client connection to the harness server using the retrieved port and API key, upgrading communication to a structured bi-directional stream.
- Disconnection: When dropped, the subprocess is killed cleanly.
For WebAssembly targets (wasm32-wasip1), the SDK cannot spawn a subprocess since WASM runtimes lack process control. Instead, it connects to a running host-side localharness process over a network WebSocket connection via WasmConnectionStrategy and WasmConnection.
sequenceDiagram
participant SDK as WASM Rust SDK
participant Host as Host Machine (localharness)
SDK->>Host: Open TCP Connection (Host:Port)
SDK->>Host: WebSocket Client Handshake (Sec-WebSocket-Version: 13, x-goog-api-key)
Host->>SDK: Handshake Upgrade Response (101 Switching Protocols)
SDK->>Host: Send InitializeConversationEvent (HarnessConfig via WebSocket JSON)
par Async Event Loop Reader Task
Host-->>SDK: Stream StepUpdate & TrajectoryStateUpdate events
Note over SDK: Map to Step, dispatch hooks, execute local tools
and Async Event Loop Sender Task
SDK-->>Host: Send InputEvent (UserInput, ToolResponse, etc.)
end
Note over SDK,Host: TrajectoryStateUpdate (STATE_IDLE) sent by Host
Note over SDK: Reader detects IDLE, pushes IDLE_SENTINEL, closes stream
The lifecycle details:
- TCP Connection: The SDK establishes a standard TCP stream to the host running the harness (configured via environment variables
ANTIGRAVITY_HARNESS_HOSTandANTIGRAVITY_HARNESS_PORT). - WebSocket Upgrade & Authentication: It performs a client upgrade handshake with the harness using the
x-goog-api-keyheader to authenticate. - Stream Non-blocking Upgrade: The TCP stream is transitioned to non-blocking mode to support cooperative asynchronous scheduling.
- Harness Initialization: An
InitializeConversationEventis sent over the WebSocket containing the fullHarnessConfigprotobuf serialized as JSON. This registers active capabilities, workspaces, custom tools, and system instructions. - Event Loops & Sentinel Termination: The connection spawns a Reader task and a Sender task. The event loops stream step updates. Once a
TrajectoryStateUpdatewithSTATE_IDLEis received, the connection knows the execution trajectory is complete. It pushes anIDLE_SENTINELstep to the receiver channel, which signals the consumer stream to yieldNoneand terminate cleanly.
While standard tool executions are managed internally by the remote/local harness and streamed as native harness steps, client-side tools (custom tool registrations) run within the Rust process context. To ensure they are fully visible to client interfaces and timeline loggers:
- Step Interception: Upon receiving a
ToolCallevent, the SDK connection intercepts it prior to calling the registry. - ACTIVE Step Emission: The connection immediately constructs a synthetic
Stepwithstatus: StepStatus::Activeand streams it down to thestep_txchannel. This notifies client interfaces that the tool is active, allowing them to render executing cards/spinners. - Execution & Hook Processing: The connection runs pre-tool-call hooks/policies. If any policy denies execution, the connection generates a synthetic
ERRORstate step, sends a denial response back to the harness, and terminates the tool task. - Completion Step Emission: If allowed and executed, the outcome of the client tool is captured. The connection emits a synthetic
DONEstate step containing the execution outputs (or anERRORstate step containing the execution panic/error message) down tostep_tx. - Synthetic Indexing: To prevent collisions with actual step numbers tracked by the
localharness(which are typically sequential starting from 0/1), all synthetic client-side tool steps are indexed sequentially starting at50,000.
- Lock Scoping: Mutexes (
tokio::sync::Mutex) are carefully scoped to minimize contention. Mutex guards are explicitly dropped before any.awaitpoints to avoid deadlocks. - Hook Dispatch: Hook guards are cloned and dropped prior to executing hooks asynchronously, ensuring the agent's internal state remains responsive.
Standard native runtimes run on multi-threaded thread pools. In contrast, WASM runtimes (like wasm32-wasip1) operate on a single-threaded execution model. To ensure robustness and prevent deadlocks/starvation:
- Non-Blocking IO: The underlying socket in
WasmConnectionis set to non-blocking. - Cooperative Yielding: The WS Sender task utilizes
try_lock()on the shared socket mutex. If the socket is locked by the reader or another operation, the sender cooperatively sleeps with an exponential backoff (5msdoubling up to50ms), yielding the thread back to the runtime executor to prevent starving other tasks on the single thread. - Task Spawning: The SDK uses a unified
spawn_taskabstraction that adapts to target runtimes, ensuring background futures run concurrently.
The SDK has been fully refactored to leverage native async traits (stable since Rust 1.75 / Rust 2024), completely removing the dependency on the #[async_trait] macro.
- Native Async Traits: Traits like
Connection,Hook,Tool, andTriggerare implemented as native async traits using standardasync fnsyntax or returningimpl Future + Sendto ensure compiler-enforced thread safety boundaries. - Companion Trait Pattern for Dynamic Dispatch: Native async traits are not directly object-safe (
dyn Traitcompatible) because they return anonymous concrete futures. To support dynamic dispatch, the SDK defines companion traitsDynHook,DynTool, andDynTriggerwhich are object-safe and return boxed futures (BoxFuture). - Zero-overhead Blanket Implementations: The companion traits are automatically implemented via blanket implementations for any type implementing the base trait:
This provides the best of both worlds: clean, idiomatic implementation of async traits for developers using standard Rust 2024 features, while preserving the internal ability to handle collections of dynamic trait objects (e.g.
pub trait DynHook: Send + Sync { fn on_session_start(&self) -> BoxFuture<'_, Result<(), anyhow::Error>>; // ... } impl<T: Hook + ?Sized> DynHook for T { fn on_session_start(&self) -> BoxFuture<'_, Result<(), anyhow::Error>> { Box::pin(async move { self.on_session_start().await }) } // ... }
Arc<dyn DynHook>inHookRunner,Arc<dyn DynTool>inToolRunner,AnyConnectionenum dispatch, etc.).