Agents overview
The BaseAgent trait that every agent implements, the four built-in agent kinds, and how agents observe the invocation context.
Every agent in adk-rs — LLM-powered or hand-written — implements one trait: BaseAgent. An agent has a name, a description the model reads, optional children, and a single run method that turns an invocation into a stream of events.
Because the contract is one async trait, composition is just nesting values: an LlmAgent holds Arc<dyn BaseAgent> children, a SequentialAgent holds a Vec of them, and your own type can hold whatever it likes. The runner only ever sees the root.
The BaseAgent trait
#[async_trait]
pub trait BaseAgent: Send + Sync + std::fmt::Debug + 'static {
fn name(&self) -> &str;
fn description(&self) -> &str {
""
}
fn sub_agents(&self) -> &[Arc<dyn BaseAgent>] {
&[]
}
fn find_agent(&self, name: &str) -> Option<Arc<dyn BaseAgent>> {
// default impl: depth-first search over sub_agents()
}
async fn run(self: Arc<Self>, ctx: Arc<InvocationContext>) -> Result<EventStream<'static>>;
}fn name(&self) -> &str- The agent’s name. Must be a valid identifier and unique within an agent tree; the framework addresses agents by name for transfer.
fn description(&self) -> &str- Human-readable description, shown to the model. Keep it short and informative — an LLM parent uses it to decide whether to delegate. Defaults to the empty string.
fn sub_agents(&self) -> &[Arc<dyn BaseAgent>]- Direct children in the agent tree. Defaults to an empty slice.
fn find_agent(&self, name: &str) -> Option<Arc<dyn BaseAgent>>- Resolves a descendant by name, depth-first. The default implementation recurses through
sub_agents();LlmAgentuses it to routetransfer_to_agentcalls — searching its own subtree first, then the whole tree fromInvocationContext.root_agent, so siblings and ancestors are reachable too. async fn run(self: Arc<Self>, ctx: Arc<InvocationContext>) -> Result<EventStream<'static>>- Runs the agent within the given invocation context and returns a stream of events. The stream terminates when the agent has produced its final response (or errors out). Note the
Arc<Self>receiver: agents are always run as shared values.
EventStream is a type alias defined in src/core/stream.rs: Pin<Box<dyn Stream<Item = Result<Event>> + Send + 'a>> — an owned, dynamically-typed futures::Stream of Event values. You consume it with ordinary combinators (next, collect, try_for_each).
The four built-in agents
| Agent | Construction | Behaviour |
|---|---|---|
LlmAgent | LlmAgent::builder("name")...build()? | Talks to a model in an LLM↔tool loop; can hold tools, sub-agents, structured-output schemas, and a code executor. |
SequentialAgent | SequentialAgent::new(name, desc, subs)? | Runs sub-agents one after another; each child sees the cumulative event history. |
ParallelAgent | ParallelAgent::new(name, desc, subs)? | Spawns every child concurrently and merges their event streams, tagging each event with a branch label. |
LoopAgent | LoopAgent::new(name, desc, subs, max_iterations)? | Repeats its sub-agents until one escalates (e.g. via the exit_loop tool) or the iteration cap is hit. |
The deterministic workflow agents never call a model themselves — orchestration is plain Rust control flow. Use them when ordering matters; use LlmAgent sub-agents with transfer when the model should pick the route.
What agents observe: InvocationContext
The Arc<InvocationContext> passed to run is the per-invocation world view, constructed by the runner. Every agent in the tree shares the same context (parallel branches included), so state, the LLM-call budget and cancellation propagate everywhere.
pub session: Arc<Mutex<Session>>- The live session being mutated. Agents push their events here; the runner mirrors them into the
SessionService. pub user_content: Option<Content>- The user content for this invocation, if any (resumed invocations may carry only a
FunctionResponse). pub cancellation: CancellationToken- Cooperative cancellation flag, flipped by
Runner::cancelor the A2Atasks/cancelhandler. See Cancellation & resume. pub root_agent: Option<Arc<dyn BaseAgent>>- Root of the agent tree, set by the runner. Agent transfer resolves targets from here, so siblings and ancestors are reachable. A breaking addition for code that builds the struct literally.
pub fn is_cancelled(&self) -> bool- True once the token has flipped. Agents check this at safe points — between LLM↔tool iterations, between sub-agents — and halt cleanly.
pub fn check_and_inc_llm_call(&self) -> Result<()>- Increments the shared LLM-call counter; errors when
RunConfig::max_llm_callsis reached.LlmAgentcalls this before every model request. pub fn new_id() -> String- Generates a fresh
inv-{uuid}invocation id.
The context also carries app_name, user_id, invocation_id, the optional artifact/memory/credential services, the per-invocation RunConfig, an InvocationOrigin (Api, Cli, or Web), and a free-form attributes bag that the framework uses for pause/resume bookkeeping.
Writing a custom agent
Implementing BaseAgent by hand is the escape hatch for logic that is neither an LLM call nor a canned workflow. Build one or more Event values and return them as a pinned, boxed stream:
use std::sync::Arc;
use adk_rs::Result;
use adk_rs::agents::BaseAgent;
use adk_rs::core::{Event, EventStream, InvocationContext};
use async_trait::async_trait;
#[derive(Debug)]
struct EchoAgent {
name: String,
}
#[async_trait]
impl BaseAgent for EchoAgent {
fn name(&self) -> &str {
&self.name
}
fn description(&self) -> &str {
"Echoes the user's message back."
}
async fn run(self: Arc<Self>, ctx: Arc<InvocationContext>) -> Result<EventStream<'static>> {
let text = ctx
.user_content
.as_ref()
.map(|c| c.text_concat())
.unwrap_or_default();
let mut event = Event::model_text(self.name.clone(), format!("echo: {text}"));
event.invocation_id = ctx.invocation_id.clone();
Ok(Box::pin(futures::stream::once(async move { Ok(event) })))
}
}Related pages
- LlmAgent — the builder, instruction templating, and the LLM↔tool loop.
- Workflow agents — Sequential, Parallel and Loop in depth.
- Multi-agent systems — transfer,
AgentTool, and coordinator patterns. - Events — the
Eventpayload your stream yields. - Runner — how invocations are created and persisted.