Typed Rust interface for the Google Antigravity
agent runtime — the localharness binary that ships inside the
google-antigravity wheels.
Tested against google-antigravity 0.1.10.
Maturity warning: this crate is new and should be considered highly untested. Upstream is alpha (
0.1.x) and reserves protobuf extension ranges on its hottest messages, so expect churn. Wire captures that break the types are very welcome in issues.
google-antigravity on PyPI is a Python client for a compiled Go binary
called localharness, which is where the agent loop, the built-in tools, and
the model calls actually live. This crate is a client for that same binary — a
sibling of the Python SDK, not a binding to it. No Python at runtime.
The harness is distributed only inside the platform wheels on PyPI. There
is no standalone release, so nothing will put it on your PATH for you:
pip download google-antigravity --no-deps -d /tmp/ag
unzip -o -j /tmp/ag/*.whl 'google/antigravity/bin/localharness' -d ~/.local/bin
export ANTIGRAVITY_HARNESS_PATH=~/.local/bin/localharnessDiscovery order is $ANTIGRAVITY_HARNESS_PATH, then localharness on PATH,
then whatever you pass to HarnessOptions::binary.
stdio is used only to bootstrap, then everything moves to a loopback WebSocket:
| Step | Transport | Payload |
|---|---|---|
| 1. Handshake | stdio, u32le-length-prefixed |
binary protobuf InputConfig → OutputConfig (port + API key) |
| 2. Connect | ws://127.0.0.1:{port}/ |
x-goog-api-key header |
| 3. Initialize | WebSocket | InitializeConversationEvent → InitializeConversationResponse |
| 4. Converse | WebSocket | InputEvent ↔ OutputEvent |
Everything after the handshake is protobuf's canonical JSON mapping:
camelCase members, 64-bit integers as strings, bytes as base64, enums as
value names.
A conversation must be configured with at least one model. A harness initialised with none exits immediately and drops the socket without an error frame — the crate surfaces the process's stderr in that case, because that is the only diagnosis available.
Two things that bite on a first run:
- Built-in tools are off unless enabled. A harness with none will answer
"I do not have file reading or command execution tools enabled" rather than
read your workspace.
HarnessOptionsdefaults toHarnessSideTools::read_only()— list, search, find, view, fetch — matching the reference Python SDK. Widen withHarnessSideTools::all()(shell and file writes) or narrow with::none(). - Free-tier quota is per model, and the
promodels have none. A request against one returns429 … limit: 0rather than an answer.gemini-flash-latestworks on a free key.
[dependencies]
antigravity-codes = "0.1"use antigravity_codes::{Client, HarnessOptions, ModelBuilder};
#[tokio::main]
async fn main() -> antigravity_codes::Result<()> {
let mut client = Client::launch(
HarnessOptions::new()
.workspace("/tmp/project")
.model(ModelBuilder::gemini(
"gemini-flash-latest",
std::env::var("GEMINI_API_KEY").unwrap(),
)),
)
.await?;
let mut turn = client.send("What files are here?").await?;
while let Some(step) = turn.next_step().await? {
if let Some(text) = step.user_facing_text() {
println!("{text}");
}
}
client.shutdown().await
}A turn is not one-way. Depending on configuration the harness stops and waits for the client, and stays blocked until answered:
| Request | Raised when | Answered with |
|---|---|---|
ToolCall |
the model calls a tool declared via HarnessOptions::tool |
ToolResponse |
CallHookRequest |
a lifecycle hook registered via HarnessOptions::hook fires |
CallHookResponse |
PolicyDecisionRequest |
a dynamic policy rule needs adjudicating | PolicyDecisionResponse |
UserQuestionsRequest |
the agent asks the user something | UserQuestionsResponse |
| tool confirmation | a tool needs approval before it runs | ToolConfirmation |
Client answers all five from the Handlers you register. None of them arrive
unless the corresponding feature was configured, so an empty Handlers is fine
for plain chat. When one does arrive unhandled, the defaults keep the turn
moving: an unimplemented tool fails that one call, hooks return "no opinion",
policy returns NO_MATCH, questions are cancelled, and tool confirmations are
refused — silently approving would undo the control you asked for.
| Type | What it gives you |
|---|---|
RawClient |
The frames, unchanged. You drive the loop. |
Client |
Turn-oriented: streams assembled Steps and answers the harness for you. |
| Feature | Description | WASM-compatible |
|---|---|---|
types |
Wire types and the handshake codec only (serde) | Yes |
async-client |
Async WebSocket client using tokio | No |
integration-tests |
Enables tests that need a real harness binary | No |
antigravity-codes = { version = "0.1", default-features = false, features = ["types"] }export GEMINI_API_KEY=... # https://aistudio.google.com/apikey
export ANTIGRAVITY_HARNESS_PATH=~/.local/bin/localharness
cargo run -p antigravity-codes --example stream_chat -- "what files are here?"
cargo run -p antigravity-codes --example custom_tool
cargo run -p antigravity-codes --example capture_frames -- ./captures "hello"All three take ANTIGRAVITY_MODEL to override the model, defaulting to
gemini-flash-latest.
The wire types are generated from the FileDescriptorProto embedded in the
wheel's localharness_pb2.py — not from the .proto files in the upstream
repo, which run ahead of what ships and are written in protobuf edition 2024
(unparseable by protoc < v31, and unsupported by prost).
pip download google-antigravity --no-deps -d /tmp/ag
python3 ../scripts/codegen_antigravity.py --wheel /tmp/ag/*.whlDrift against the latest published wheel is checked nightly by
scripts/check_antigravity_schema_drift.py, which reads ~30 KB of the 37 MB
wheel using HTTP range requests.
cargo test -p antigravity-codes --all-features
# Against a real harness. Most of these need no API key: the harness runs the
# whole turn lifecycle locally and only fails when it calls the model.
ANTIGRAVITY_HARNESS_PATH=~/.local/bin/localharness \
cargo test -p antigravity-codes --features integration-testsApache-2.0