Quickstart
Build a Gemini-backed agent from cargo new to a working function tool, and learn what flows through the event stream.
In ten minutes you will create a new binary crate, wire an LlmAgent to Gemini, stream its events to stdout, and then teach it a weather tool with the #[tool] macro — touching every core concept in adk-rs along the way.
Create the project
cargo new hello-agent
cd hello-agentAdd adk-rs with the gemini and macros features (defaults are empty — see Installation), plus tokio for the runtime, futures for stream combinators, and serde/schemars for the tool we add later.
[package]
name = "hello-agent"
version = "0.1.0"
edition = "2024"
[dependencies]
adk-rs = { version = "0.6", features = ["gemini", "macros"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
futures = "0.3"
serde = { version = "1", features = ["derive"] }
schemars = "0.8"Write the agent
use adk_rs::agents::LlmAgent;
use adk_rs::core::{Model, SessionService};
use adk_rs::providers::gemini::Gemini;
use adk_rs::runner::Runner;
use adk_rs::services::mem::InMemorySessionService;
use futures::StreamExt;
use std::sync::Arc;
#[tokio::main]
async fn main() -> adk_rs::Result<()> {
let model: Arc<dyn Model> = Arc::new(Gemini::from_env("gemini-2.5-flash")?);
let agent = Arc::new(
LlmAgent::builder("greeter")
.description("A friendly greeter")
.model(model)
.instruction("You greet the user warmly and concisely.")
.build()?,
);
let svc: Arc<dyn SessionService> = Arc::new(InMemorySessionService::new());
let runner = Runner::builder()
.app_name("hello")
.agent(agent)
.session_service(svc)
.build()?;
let mut events = runner.run("user", None, "Hello!").await?;
while let Some(ev) = events.next().await {
let ev = ev?;
if let Some(c) = ev.response.content {
println!("[{}] {}", ev.author, c.text_concat());
}
}
Ok(())
}Three pieces do all the work, and they recur in every adk-rs program:
- The agent.
LlmAgentis built with a fluent builder: a name, aModelimplementation behindArc<dyn Model>(hereGemini::from_env, which readsGOOGLE_API_KEY), and a systeminstruction.build()validates the configuration and returns a plain value. - The runner. The
Runnerowns the services and orchestrates a turn. It needs anapp_name, the root agent, and aSessionService— here the always-availableInMemorySessionService, which keeps sessions as append-only event logs in memory. - The event stream.
runner.run(user_id, session_id, user_text)starts one turn. PassingNoneassession_idauto-creates a session. It returns anEventStream— an ordinaryfutures::StreamofResult<Event>you consume withStreamExt::next. EachEventnames itsauthorand may carry modelContent;text_concat()joins the text parts.
Run it
export GOOGLE_API_KEY="your-key"
cargo run[greeter] Hello there! It's wonderful to meet you.A single-text turn produces one model event. The interesting structure appears once tools enter the picture.
Add a function tool
Annotate any async function with #[adk_rs::tool]. The macro derives the JSON schema from the argument struct (via schemars::JsonSchema), generates the FunctionDeclaration the model sees, and emits a constructor returning an Arc<dyn Tool>. The doc comments become the tool and parameter descriptions.
use adk_rs::tool;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Deserialize, JsonSchema)]
struct GetWeatherArgs {
/// City name in English (e.g. "Paris").
city: String,
}
#[derive(Serialize)]
struct WeatherReport {
city: String,
temp_c: f32,
description: String,
}
/// Look up the current weather in `args.city`.
#[tool]
async fn get_weather(
args: GetWeatherArgs,
_ctx: &mut adk_rs::core::ToolContext,
) -> adk_rs::Result<WeatherReport> {
Ok(WeatherReport {
city: args.city,
temp_c: 22.0,
description: "sunny".into(),
})
}Attach the tool on the builder and adjust the instruction; everything else stays the same:
let agent = Arc::new(
LlmAgent::builder("weather")
.model(model)
.instruction("Use the get_weather tool to answer questions about cities' weather.")
.tool(get_weather())
.build()?,
);
// ...
let mut events = runner.run("user", None, "What's the weather in Paris?").await?;Reading the event stream
A tool-using turn is no longer one event. The model first emits a function call; the framework dispatches your Rust function and appends a function response; the model then reads the result and produces the final text. Event exposes helpers for the tool legs:
while let Some(ev) = events.next().await {
let ev = ev?;
if let Some(c) = ev.response.content.as_ref() {
let text = c.text_concat();
if !text.is_empty() {
println!("[{}] {}", ev.author, text);
}
}
for fc in ev.function_calls() {
println!(" -> tool call: {} args={}", fc.name, fc.args);
}
for fr in ev.function_responses() {
println!(" <- tool response: {} = {}", fr.name, fr.response);
}
} -> tool call: get_weather args={"city":"Paris"}
<- tool response: get_weather = {"city":"Paris","temp_c":22.0,"description":"sunny"}
[weather] It's currently sunny and 22°C in Paris.Where next
- Agents overview — the
BaseAgenttrait and workflow agents. - Function tools — the
#[tool]macro and the manualTooltrait. - Events — the full anatomy of an
Event. - Sessions and state — persisting conversations beyond memory.
- Example: Gemini chat — the minimal version of this page, annotated.