localharness
A Rust-native agent SDK for Gemini. Build production agents with
streaming text, custom tools, safety policies, and background triggers
— all from a single cargo add.
use ;
async
Status: alpha · pre-1.0 · the 0.2.x line is mid-pivot — see
DESIGN.mdfor the roadmap.
Roadmap (0.2.x)
localharness started life (0.1.x) as a Rust client for Google's
google-antigravity Python SDK, talking to a bundled Go
runtime binary. The 0.2.x line replaces that runtime with a Rust
agent loop that hits the Gemini API directly — no Go binary, no
Python install, no external process. The public API (Agent,
Conversation, Tool, Policy, Hook, Trigger) is preserved.
| Phase | Version | What lands |
|---|---|---|
| 1 | 0.2.0-alpha.1 |
Gemini backend, text-only chat, streaming |
| 2 | 0.2.0-alpha.2 |
Tool calling + read-only built-ins |
| 3 | 0.2.0-alpha.3 |
Write tools + workspace sandbox |
| 4 | 0.2.0-beta.1 |
Thoughts, structured output, image gen, ask-question |
| 5 | 0.2.0 GA |
LocalConnectionStrategy deprecated; Gemini default |
See DESIGN.md for the full plan with module-by-module specs.
Contents
- Install
- Concepts —
Agent,Conversation,Connection - Examples — streaming, tools, hooks, policies, triggers, multimodal
- Architecture
- Design notes
- FAQ
- License
Install
[]
= "0.1"
= { = "1", = ["macros", "rt-multi-thread"] }
0.1.x (current) — Go-backed
Today's release proxies to a Go runtime binary called localharness.
The Python SDK ships it; install once to grab the binary:
If localharness is already on your PATH, the env var is optional.
0.2.x (in progress) — no external runtime
Agent::start_gemini(config) will talk to the Gemini API directly.
A single cargo add is all you'll need. Track progress in
DESIGN.md and CHANGELOG.md.
Concepts
The SDK is layered so you can pick the surface that fits the task:
| Layer | Type | Use when |
|---|---|---|
| 1 | Agent |
One-shot or short-running scripts. Batteries included. |
| 2 | Conversation / ChatResponse |
Long-lived sessions, history introspection, custom turn shapes. |
| 3 | Connection |
Embed the SDK in your own runtime, swap the transport. |
Examples
use StreamExt;
let response = agent.chat.await?;
let mut tokens = response.text_stream;
while let Some = tokens.next.await
Every cursor (text_stream, thoughts, tool_calls) replays from chunk
zero and advances independently — safe to consume concurrently from
multiple tasks.
use StreamExt;
let response = agent.chat.await?;
let thoughts = async ;
let calls = async ;
let = join!;
a?; b?;
use ;
use json;
let weather = new;
let agent = start_local.await?;
use ;
use Arc;
let policies = vec!;
Precedence matches the Python SDK: specific deny ≻ specific ask ≻ specific allow ≻ wildcard deny ≻ wildcard ask ≻ wildcard allow.
use workspace_only;
let policies = workspace_only;
// view_file / create_file / edit_file outside the workspace are denied.
// Component-wise comparison; "/home/me/project-evil" is NOT a match.
use Duration;
use every;
let watchdog = every;
let agent = start_local.await?;
use ;
let chart = from_path?
.with_description;
let spec = from_path?;
let prompt: Content = vec!.into;
let response = agent.chat.await?;
Media is stored as Bytes — cloning into multiple stream frames is
refcounted, so a 30 MB PDF is never copied.
let agent = start_local.await?;
Architecture
┌──────────────────────────────────────────────────────┐
│ L1 Agent start · chat · shutdown │
├──────────────────────────────────────────────────────┤
│ L2 Conversation history · usage · streams │
│ ChatResponse text · thoughts · tool_calls │
├──────────────────────────────────────────────────────┤
│ L3 Connection transport abstraction │
│ LocalConnection ws + stdio handshake │
└──────────────────────────────────────────────────────┘
│
│ localharness (Go binary)
▼
Gemini
Inside LocalConnection:
┌──────────┐ inbox: mpsc(InputEvent, cap 16) ┌────────────────┐
│ callers │ ─────────────────────────────────►│ ws_writer │
└──────────┘ └────────┬───────┘
│
┌──────▼──────┐
│ websocket │
└──────▲──────┘
│
┌────────────┐ broadcast(Step, cap 256) ┌────────────┴──────┐
│ subscribers│ ◄─────────────────────────│ ws_reader │
└────────────┘ └───────────────────┘
A single tokio::select! supervisor owns the WebSocket and arbitrates
inbox writes against incoming frames. A separate task supervises the
child process (kill_on_drop on the handle, plus an explicit
shutdown flag).
Design notes (performance & safety)
A short tour of the load-bearing choices:
- Lock-free idle polling.
Connection::is_idle()reads anAtomicBool— no mutex, no syscalls, nanoseconds. Trigger handlers can call it inside hot loops. - Broadcast fan-out for steps. Any number of cursors can subscribe without blocking the producer. Replays are bounded (256 in flight); a slow consumer fails fast with a "lagged" error rather than ballooning memory.
- Bounded backpressure everywhere. The writer inbox is 16, the step
broadcast is 256. There's no unbounded
Vec<Message>waiting for the socket to drain. - Lock-free tool-context swap.
arc_swap::ArcSwapOptionreplaces the runtime context atomically. Concurrent tool calls never serialize on a mutex just to fetch the context. - No mutex poisoning footguns.
parking_lot::{Mutex,RwLock}meanlock()doesn't returnResult; one panicking thread doesn't taint every other reader. - Typed errors, no
unwrapon hot paths.Erroris a flatthiserrorenum.io::Error,serde_json::Error, andprosterrors fold into it via#[from];?works everywhere. - Zero-copy media.
Media::dataisbytes::Bytes. Cloning a part into multiple frames is a refcount bump; a 30 MB PDF is never copied. - Bounded resource lifetimes.
kill_on_dropon the child, a 10 s handshake timeout, idempotentshutdown(), and aDropimpl that flips the shutdown flag so leaked agents don't leak processes. - Strict policy precedence. Component-wise path containment defeats
prefix tricks like
/foo/bar-evilvs/foo/bar. Wildcard rules always lose to specific rules.
FAQ
What's the Go binary I keep hearing about? Today's 0.1.x release
talks to a runtime binary that happens to be written in Go (Google
ships it inside the google-antigravity Python wheel). You never write
Go; you just point an env var at the binary once. The 0.2.x line
removes the binary entirely — see Roadmap.
Does this need GEMINI_API_KEY? Yes — either set the env var or
pass it via LocalAgentConfig::with_api_key().
Why does write-tool access require a policy? Enabling tools that
write to disk or run commands without a policy is almost always a bug.
Add policies: vec![allow_all()] to opt in, or use workspace_only(…)
to scope.
MSRV? Rust 1.85 (edition 2024).
Async runtime? Tokio.
How do I get tracing logs?
fmt.with_env_filter.init;
Origin of the project. 0.1.x began life as a port of Google's
google-antigravity Python SDK. See
UPSTREAM.md for the historical record and
DESIGN.md for the Rust-native pivot plan.
License
Apache-2.0. The LICENSE file is inherited from upstream
for attribution.