neuron-explorer
Steering-first neuron explorer for Qwen3.5-4B: push every MLP neuron, keep every rollout, browse the results
Build text agents and Live voice sessions with Rust builders, tool policies, and testable conversation flows.
Developer toolsRust · JavaScriptGitHub @vamsiramakrishnanOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
Full Rust SDK for the Gemini Multimodal Live API — wire protocol, agent runtime, and fluent DX in three layered crates.
Taken from the repository README
Loading the full README.
Read the full README ↗Rust SDK and runtime for text agents and live voice conversations.
Rust SDKs and runtime components for Gemini text agents, Gemini Live sessions, governed conversation flows, typed tools, telephony, and offline flow simulation.
Book: https://vamsiramakrishnan.github.io/gemini-rs/
API reference: https://docs.rs/gemini-adk-fluent-rs
| Build | Start here | What to verify |
|---|---|---|
| A text agent | Text agent | One authenticated request and its returned text |
| A microphone conversation | Voice session | Audio dependencies, connection, and playback |
| A conversation with permitted actions | Governed flows | State transitions and tool admission |
| A transport integration without the fluent layer | Crate responsibilities | The smallest crate and feature set you need |
Start with one path. Text agents do not require the system audio stack. Flow simulation is useful before a live call, but a passing simulation does not establish microphone, telephony, or provider behavior.
The workspace is split by responsibility:
| Crate | Responsibility |
|---|---|
gemini-genai-rs | Gemini API transport and model-facing types |
gemini-adk-rs | state, flows, tools, extraction, phases, watchers, and runtime semantics |
gemini-adk-fluent-rs | fluent authoring API over the runtime |
The runtime can enforce flow state while a Live session is active. A tool call can be rejected when the current flow has not admitted that tool yet.
The same flow can be serialized, validated, simulated offline, edited in Flow Studio, and generated back into Rust.
Text agents need Rust 1.99+.
[dependencies]
gemini-adk-fluent-rs = "4.0.0"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
# For typed answers and tool arguments:
serde = { version = "1", features = ["derive"] }
schemars = "0.8"
Voice I/O is optional:
gemini-adk-fluent-rs = { version = "4.0.0", features = ["voice-io"] }
On Linux, voice builds need pkg-config, libssl-dev, and libasound2-dev. The text path does not pull in the system audio stack.
Google AI:
export GEMINI_API_KEY=...
Vertex AI:
export GOOGLE_GENAI_USE_VERTEXAI=true
export GOOGLE_CLOUD_PROJECT=my-project
export GOOGLE_CLOUD_LOCATION=us-central1
gcloud auth application-default login
GeminiLlm::from_env() reads these and fails before any request, naming the
variable to set, when one is missing. Live sessions use the same variables.
Ask a question:
use gemini_adk_fluent_rs::prelude::*;
#[tokio::main]
async fn main() -> Result<(), AgentError> {
let agent = AgentBuilder::new("assistant")
.instruction("Answer in one sentence.")
.build(GeminiLlm::from_env()?)?;
println!("{}", agent.ask("What is the Gemini Live API?").await?);
Ok(())
}
Each program below adds one idea. Hold a conversation and stream the reply:
use gemini_adk_fluent_rs::prelude::*;
use std::io::Write;
#[tokio::main]
async fn main() -> Result<(), AgentError> {
let agent = AgentBuilder::new("assistant")
.instruction("Be brief.")
.build(GeminiLlm::from_env()?)?;
let mut chat = agent.chat();
for message in ["Hi, I'm Ada.", "What's my name?"] {
println!("> {message}");
let mut reply = chat.send_stream(message);
while let Some(event) = reply.next().await {
if let RunEvent::TextDelta(text) = event? {
print!("{text}");
std::io::stdout().flush().ok();
}
}
println!();
}
Ok(())
}
Get a typed answer — the type's JSON Schema is sent as the response schema, and a reply that does not parse is sent back once for correction:
use gemini_adk_fluent_rs::prelude::*;
/// The model's answer, as a Rust type: its JSON Schema is the response schema.
#[derive(serde::Deserialize, schemars::JsonSchema)]
struct City {
name: String,
country: String,
/// Population, in millions.
population_millions: f64,
}
#[tokio::main]
async fn main() -> Result<(), AgentError> {
let agent = AgentBuilder::new("geographer").build(GeminiLlm::from_env()?)?;
let city: City = agent.ask_as("Describe the largest city in Japan.").await?;
println!(
"{}, {}: {:.1} million people",
city.name, city.country, city.population_millions
);
Ok(())
}
Give the agent a tool — a documented async fn, whose doc comment is what the
model reads:
use gemini_adk_fluent_rs::prelude::*;
/// Get the current weather for a city.
///
/// # Arguments
///
/// * `city` - The city name, e.g. "Paris".
#[tool]
async fn get_weather(city: String) -> serde_json::Value {
// A real tool would call a weather service here.
serde_json::json!({ "city": city, "condition": "rain", "celsius": 14 })
}
#[tokio::main]
async fn main() -> Result<(), AgentError> {
let agent = AgentBuilder::new("weather")
.instruction("Answer weather questions using your tool.")
.tool(get_weather())
.build(GeminiLlm::from_env()?)?;
println!("{}", agent.ask("Do I need an umbrella in Paris?").await?);
Ok(())
}
Test it without a model. MockLlm replies from a script and records every
request, so a test asserts on what the agent sent as well as what it answered:
use gemini_adk_fluent_rs::prelude::*;
use gemini_adk_fluent_rs::testing::{LlmResponse, MockLlm};
/// Look up the status of an order.
///
/// # Arguments
///
/// * `id` - The order id, e.g. "A-17".
#[tool]
async fn order_status(id: String) -> String {
format!("order {id} has shipped")
}
#[tokio::test]
async fn the_agent_checks_the_order_before_answering() -> Result<(), Box<dyn std::error::Error>> {
let llm = MockLlm::script([
LlmResponse::tool_call("order_status", serde_json::json!({ "id": "A-17" })),
LlmResponse::from_text("Your order has shipped."),
]);
let agent = AgentBuilder::new("support")
.tool(order_status())
.build(llm.clone())?;
assert_eq!(
agent.ask("Where is order A-17?").await?,
"Your order has shipped."
);
// The tool ran, and its result went back to the model.
let followup = serde_json::to_string(&llm.last_request().unwrap().contents)?;
assert!(followup.contains("order A-17 has shipped"));
Ok(())
}
From a clone:
cargo run -p example-quickstart --bin hello-text # also: chat, typed, tool
cargo test -p example-quickstart
use gemini_adk_fluent_rs::prelude::*;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
Live::builder()
.instruction("You are a concise concierge.")
.greeting("Ask how you can help.")
.connect_from_env()
.await?
.talk()
.await?;
Ok(())
}
From the workspace:
cargo run -p example-quickstart --features voice --bin hello-voice
talk() uses the local audio devices. The lower-level voice::pump() API accepts and emits audio frames without depending on a specific device transport.
A Flow describes step ordering, tool admission, completion guards, and terminal conditions.
let flow = Flow::new()
.step("gather")
.allow(["capture_party"])
.done(Guard::captured(["party_size", "requested_time"]))
.step("check")
.after("gather")
.allow(["check_availability"])
.done(Guard::is_true("availability_checked"))
.step("book")
.after("check")
.allow(["book_table"])
.done(Guard::called_ok("book_table"))
.never("book_table")
.until(Guard::is_true("availability_checked"))
.build()?;
When the flow is attached to a Live session, the runtime evaluates these guards before admitting gated tools.
The guard vocabulary is serializable. A SessionSpec can therefore carry the flow, tool declarations, extraction schemas, phases, watchers, runtime settings, and embedded tests as data rather than only as Rust source.
Run the editor locally:
cargo run -p gemini-adk-web-rs
Open:
http://localhost:25125/flows
Flow Studio reads and writes the same session document consumed by the runtime. The preview path runs the offline simulator against embedded tests. The code view uses the session document's Rust generator.
The studio does not define separate runtime semantics; it is a client of the same document and validation code.
A tool is a documented async fn marked #[tool]: the doc comment is its
description, a # Arguments section describes its parameters, the parameter
types are its schema, and it may return any Serialize value or a Result
with any error. The same tool works on a text agent (AgentBuilder::tool) and
a Live session (Live::builder().tool), and a governed flow can gate it.
See the Tool System chapter for closures with typed arguments (T::typed), confirmation, MCP, HTTP tools, and result handling.
Live audio and control work have different latency requirements. The runtime separates callbacks into three paths:
Callbacks that may block should use the concurrent variants rather than run on the audio path.
The audio core is shared across local devices and phone transports.
The repository includes examples for:
sip feature;These transports feed audio and DTMF into the same session state and flow runtime.
SIP registration and SRTP are not implemented in the current example path. See the telephony chapter for the current support matrix.
| Task | Start here |
|---|---|
| Text agent: ask, chat, typed answers, tools, tests | examples/quickstart |
| Voice session | examples/quickstart with voice feature |
| Governed flow | example-cookbook binary 37-governed-flow |
| Flow Studio | gemini-adk-web-rs |
| End-to-end voice spec | examples/voice-spec-demo |
| Telephony | examples/telephony and examples/audiohook |
| Cookbook index | examples/INDEX.md |
Scaffold an application with:
cargo install gemini-adk-cli-rs
adk create my-agent
cd my-agent && cargo run
The generated src/main.rs is compiled in this repository's CI, so a new
project builds on the release it names.
git clone https://github.com/vamsiramakrishnan/gemini-rs.git
cd gemini-rs
cargo test --workspace
The README quickstart programs are compiled in CI from the checked examples so documentation changes that break those examples fail the build.
MIT. See LICENSE.