A Rust-native agent SDK for Google's Gemini API.
Streaming text, custom tools, safety policies, hooks, background triggers, MCP bridge, context compaction -- one crate, zero external binaries. Compiles to native (tokio) and wasm32 (browser).
use localharness::{Agent, GeminiAgentConfig};
#[tokio::main]
async fn main() -> localharness::Result<()> {
let agent = Agent::start_gemini(
GeminiAgentConfig::new(std::env::var("GEMINI_API_KEY").unwrap())
.with_system_instructions("You are a concise code reviewer."),
).await?;
let response = agent.chat("Review: fn add(a: i32, b: i32) -> i32 { a - b }").await?;
println!("{}", response.text().await?);
agent.shutdown().await?;
Ok(())
}[dependencies]
localharness = "0.10"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }Get an API key from Google AI Studio.
No Python, no Go binary, no harness process -- cargo build and you have an agent.
- Streaming. Independent cursors for text, thoughts, and tool calls -- safe to consume concurrently.
- Tools. 15 built-in tools (filesystem, shell, image gen, sub-agents, inter-agent RPC, in-browser Rust compiler) plus
ClosureToolfor custom tools. MCP stdio bridge for external tool servers. - Hooks and policies. Six hook points. Deny-by-default policy engine with
allow,deny,ask.workspace_only()sandboxes file tools. - Triggers. Background tasks that inject prompts on a schedule or condition.
- Wasm. Same
Agentloop compiles towasm32-unknown-unknown. File tools use OPFS. Onlyrun_commandand MCP are native-only. - Multimodal. Images, PDFs, audio, video via
Media/Partwith zero-copybytes::Bytesstorage.
Layer Type Purpose
1 Agent High-level facade: connect, chat, shutdown.
2 Conversation / ChatResponse Stateful session, multi-cursor streams.
3 Connection Transport abstraction (swap backends).
aux Filesystem Pluggable FS for file tools (Native / OPFS / custom).
| Feature | Default | Description |
|---|---|---|
native |
yes | Tokio runtime, run_command, MCP stdio bridge, NativeFilesystem. |
wallet |
no | secp256k1 keypair, BIP-39, on-chain registry client. Works on every target. |
browser-app |
no | In-browser IDE as a wasm cdylib (wasm-pack). Enables wallet transitively. |
Default config exposes the read-only subset; CapabilitiesConfig::unrestricted() enables all 15.
| Tool | Mode | Description |
|---|---|---|
list_directory |
R | Sorted children with name, kind, size. |
view_file |
R | UTF-8 read with optional line range; 256 KiB cap. |
find_file |
R | Glob-matched recursive name search; 1000-match cap. |
search_directory |
R | Regex content search with optional file glob; 500-match cap. |
create_file |
W | Atomic write via tempfile + rename; refuses to overwrite. |
edit_file |
W | Exact substring replace (or replace_all); atomic write. |
delete_file |
W | Remove file or directory (recursive). |
rename_file |
W | Rename/move; atomic on native. |
run_command |
W | Shell exec, 30s default / 600s max timeout. Native only. |
generate_image |
W | Image model call; returns base64 + MIME. |
ask_question |
I/O | No-op default; register a custom impl for interactive UI. |
start_subagent |
spawn | One-shot subagent with isolated context. |
call_agent |
RPC | Inter-agent message by subdomain name. |
compile_rustlite |
exec | Compile Rust-subset source to wasm and run it in-browser. |
finish |
term | Terminate turn + capture structured output. |
Custom tools with the same name as a built-in override it.
Custom tool
use localharness::{allow_all, Agent, ClosureTool, GeminiAgentConfig};
use serde_json::json;
let weather = ClosureTool::new(
"get_weather",
"Return the weather for a city.",
json!({ "type": "object", "properties": { "city": { "type": "string" } } }),
|args, _ctx| async move {
let city = args["city"].as_str().unwrap_or("?");
Ok(json!({ "weather": format!("sunny in {city}") }))
},
);
let agent = Agent::start_gemini(
GeminiAgentConfig::new(api_key)
.with_tool(weather)
.with_policies(vec![allow_all()]),
).await?;Stream tokens
use futures_util::StreamExt;
let response = agent.chat("Write a haiku about Rust.").await?;
let mut tokens = response.text_stream();
while let Some(chunk) = tokens.next().await {
print!("{}", chunk?);
}Policies + workspace sandbox
use localharness::{deny_all, Policy, CapabilitiesConfig, GeminiAgentConfig};
let policies = vec![
deny_all(),
Policy::allow("view_file"),
Policy::ask("run_command", std::sync::Arc::new(|call| {
eprintln!("approve `{}`? {:?}", call.name, call.args);
true
})),
];
// Or sandbox everything to a directory:
let agent = Agent::start_gemini(
GeminiAgentConfig::new(api_key)
.with_capabilities(CapabilitiesConfig::unrestricted())
.with_workspace("/home/me/project"),
).await?;Background trigger
use localharness::every;
let watchdog = every(std::time::Duration::from_secs(60), "deploy_watch", |ctx| async move {
ctx.send_when_idle("Check the deployment status.").await
});MCP bridge (native only)
use localharness::types::McpServerConfig;
let agent = Agent::start_gemini(
GeminiAgentConfig::new(api_key)
.with_mcp_server(McpServerConfig::Stdio {
command: "uvx".into(),
args: vec!["mcp-server-fetch".into()],
}),
).await?;The same agent loop runs in a browser tab.
Live demo: localharness.xyz
git clone https://github.com/compusophy/localharness && cd localharness
./scripts/build-web.sh # wasm-pack build -> web/pkg/
python -m http.server 8765 -d web