Guide: A multi-agent pipeline
Build a research-and-write pipeline from parallel specialist agents, a state-driven writer, and an optional review loop.
Workflow agents let you compose LLM agents the way you compose functions: fan two researchers out with ParallelAgent, hand their findings to a writer through session state, polish the result in a LoopAgent, and wrap the stages in SequentialAgent. This guide builds that pipeline end to end with real, compiling code.
1. Set up the project
[dependencies]
adk-rs = { version = "0.6", features = ["gemini"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
futures = "0.3"2. Fan out two researchers with ParallelAgent
Each researcher is an ordinary LlmAgent with its own angle and its own output_key — when an agent produces its final response, the text is stamped into the event’s state_delta under that key. ParallelAgent::new(name, description, sub_agents) takes children as Vec<Arc<dyn BaseAgent>>, fails fast on an empty list, and merges their event streams as results arrive, tagging each child’s events with a branch (researchers.0, researchers.1, …).
use adk_rs::agents::{LlmAgent, LoopAgent, ParallelAgent, SequentialAgent};
use adk_rs::core::Model;
use adk_rs::providers::gemini::Gemini;
use std::sync::Arc;
fn build_pipeline(model: Arc<dyn Model>) -> adk_rs::Result<Arc<SequentialAgent>> {
let tech = Arc::new(
LlmAgent::builder("tech_researcher")
.model(model.clone())
.instruction("Give 3-5 dense bullet points on the technical state of the art for the user's topic. Facts only.")
.output_key("tech_findings")
.build()?,
);
let market = Arc::new(
LlmAgent::builder("market_researcher")
.model(model.clone())
.instruction("Give 3-5 dense bullet points on adoption, players, and trends for the user's topic.")
.output_key("market_findings")
.build()?,
);
let researchers = Arc::new(ParallelAgent::new(
"researchers",
"Runs the research specialists concurrently",
vec![tech, market],
)?);3. A writer that reads the researchers’ state
The writer never sees the researchers’ events directly. It reads {tech_findings} and {market_findings} from session state — the templater resolves each {key} and errors if a required key is missing (append ? to make one optional).
let writer = Arc::new(
LlmAgent::builder("writer")
.model(model.clone())
.instruction(
"Write a concise brief on the user's topic using ONLY this research.\n\nTechnical findings:\n{tech_findings}\n\nMarket findings:\n{market_findings}",
)
.output_key("draft")
.build()?,
);4. Add a review loop with exit_loop
A LoopAgent re-runs its children up to max_iterations times (the cap must be > 0). To stop early, give the reviewer the built-in exit_loop tool: calling it sets escalate, which breaks the loop. Because the reviewer shares output_key("draft") with the writer, each rewrite replaces the draft in state.
let reviewer = Arc::new(
LlmAgent::builder("reviewer")
.model(model.clone())
.instruction(
"Review this draft against the research in the conversation. If accurate and complete, call the exit_loop tool. Otherwise reply with a corrected rewrite.\n\nDraft:\n{draft}",
)
.output_key("draft")
.tool(adk_rs::tools::exit_loop())
.build()?,
);
let refine = Arc::new(LoopAgent::new(
"refine",
"Reviews and refines the draft until it passes",
vec![reviewer],
3, // max_iterations: hard cap even if exit_loop is never called
)?);5. Wrap the stages in SequentialAgent
SequentialAgent::new(
"research_pipeline",
"Researches a topic in parallel, drafts a brief, then refines it",
vec![researchers, writer, refine],
)
.map(Arc::new)
}6. Run it and attribute events
A composed agent runs exactly like a single one: hand the root to a Runner and consume the stream. Use event.author (the emitting agent’s name) and event.branch (set by ParallelAgent) to attribute output — parallel events interleave in completion order, so never rely on stream position. Set GOOGLE_API_KEY and run:
use adk_rs::runner::Runner;
use adk_rs::services::mem::InMemorySessionService;
use futures::StreamExt;
#[tokio::main]
async fn main() -> adk_rs::Result<()> {
let model: Arc<dyn Model> = Arc::new(Gemini::from_env("gemini-2.5-flash")?);
let runner = Runner::builder()
.app_name("research")
.agent(build_pipeline(model)?)
.session_service(Arc::new(InMemorySessionService::new()))
.build()?;
let mut events = runner.run("user", None, "Rust async runtimes in 2026").await?;
while let Some(ev) = events.next().await {
let ev = ev?;
let who = match ev.branch.as_deref() {
Some(branch) => format!("{} ({branch})", ev.author),
None => ev.author.clone(),
};
if let Some(c) = ev.response.content.as_ref() {
let text = c.text_concat();
if !text.is_empty() {
println!("[{who}]\n{text}\n");
}
}
}
Ok(())
}You will see [tech_researcher (researchers.0)] and [market_researcher (researchers.1)] interleave, then [writer], then [reviewer] — ending with either a sign-off exit_loop call or a final rewrite. The finished brief also survives the run in session state under draft.
Where next
- Workflow agents — full
Sequential/Parallel/Loopsemantics. - Multi-agent patterns — LLM-driven transfer with
sub_agentsas an alternative to fixed pipelines. - Guide: Persistent sessions — keep
draftand friends across restarts. - Cancellation and resume — pausing and resuming pipelines with checkpoints.