Building Multi-Agent Systems with ADK-Rust
Learn when and how to use different multi-agent patterns—from simple coordination to complex graph-based orchestration—with Rust's type safety and performance.
1. Introduction
The Problem: AI That Hits a Wall
You've built your first AI agent. It's impressive—it can answer questions, summarize documents, maybe even write code. But then reality hits:
"Can you check my last invoice and also help me set up the API?"
Your agent struggles. It wasn't trained on billing systems AND developer docs AND troubleshooting workflows. Its instruction prompt is already 2000 tokens trying to cover everything. Response quality degrades.
This is the single-agent ceiling. As your application grows, you face painful tradeoffs:
- Bloated prompts: Every new capability means longer instructions, higher latency, and more confusion for the model
- Jack of all trades: One agent handling billing, support, AND sales becomes mediocre at all three
- Impossible maintenance: Changing billing logic shouldn't risk breaking your support flows
- No specialization: Your math agent can't have a calculator tool while your research agent has web search—they share everything
The Solution: Specialized Agents Working Together
Multi-agent systems solve this by decomposing complex tasks into specialized roles. Instead of one overwhelmed generalist, you create focused specialists:
- Customer service: A coordinator routes users to billing, technical support, or sales specialists—each with focused training and tools
- Content creation: A research agent gathers facts, a writer crafts the narrative, an editor polishes—each agent masters one skill
- Code generation: A planner designs the architecture, a coder implements, a reviewer catches bugs—different perspectives improve quality
The result? Each agent stays focused, prompts stay manageable, and you can update billing logic without touching support. It's microservices for AI.
What You'll Learn
ADK-Rust provides three progressively powerful patterns for multi-agent orchestration. This tutorial will teach you:
- When to use each pattern based on your requirements
- How to implement them with production-ready Rust code
- Why the architectural tradeoffs matter for your specific use case
2. Choosing the Right Pattern
Before diving into code, let's understand what each pattern offers:
| Pattern | Best For | Control Level | Complexity |
|---|---|---|---|
| Coordinator | Conversation handoffs | LLM decides | Low |
| AgentTool | Response processing | Coordinator processes | Medium |
| Supervisor Graph | Complex workflows | Full state management | High |
3. Pattern 1: The Coordinator (Sub-Agents)
🎯 Use Case: Customer Service Routing
You're building a customer service bot. Users might ask about billing, request technical help, or inquire about new features. Each domain requires specialized knowledge, but users shouldn't need to know which department to contact.
The Coordinator pattern uses automatic agent transfer. When you add sub-agents via .sub_agent(), ADK-Rust injects a transfer_to_agent tool. The LLM decides when to hand off based on the conversation.
Key Characteristics
- Seamless handoff: User continues naturally with the specialist
- LLM-driven routing: The coordinator decides based on conversation context
- Conversation continuity: Session history is maintained across transfers
- No response processing: Specialist talks directly to user after transfer
Implementation
use adk_rust::prelude::*;
use std::sync::Arc;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let api_key = std::env::var("GOOGLE_API_KEY")?;
let model = Arc::new(GeminiModel::new(&api_key, "gemini-2.0-flash")?);
// Specialist: Billing Agent
// Clear description helps the coordinator know when to transfer
let billing_agent = LlmAgentBuilder::new("billing_agent")
.description(
"Handles all billing questions: invoices, payments, refunds, subscription plans, and account charges. Transfer here for any money-related questions."
)
.instruction(
"You are a billing specialist. Answer questions about invoices, payments, and subscription plans. Be concise and accurate. If asked about technical issues, suggest transferring to support."
)
.model(model.clone())
.build()?;
// Specialist: Technical Support Agent
let support_agent = LlmAgentBuilder::new("support_agent")
.description(
"Provides technical support: troubleshooting, bug reports, feature questions, and integration help. Transfer here for any technical problems."
)
.instruction(
"You are a technical support specialist. Help users troubleshoot issues step by step. Ask clarifying questions when needed. Be patient and thorough."
)
.model(model.clone())
.build()?;
// Coordinator: Routes to specialists
let coordinator = LlmAgentBuilder::new("coordinator")
.description("Customer service coordinator")
.instruction(
"You are a friendly customer service coordinator. Your job is to:\n 1. Greet users warmly\n 2. Understand their needs\n 3. Route to the right specialist:\n - Billing questions → transfer to billing_agent\n - Technical issues → transfer to support_agent\n 4. Handle general questions yourself\n\n Always explain who you're connecting them with."
)
.model(model.clone())
.sub_agent(Arc::new(billing_agent))
.sub_agent(Arc::new(support_agent))
.build()?;
// Run with the built-in launcher
Launcher::new(Arc::new(coordinator))
.run()
.await?;
Ok(())
}Example Conversation
User: Hi, I have a question about my bill
[coordinator]: Hello! I'd be happy to help with your billing question. Let me connect you with our billing specialist who can assist you.
System: 🔄 Transfer to: billing_agent
[billing_agent]: Hi there! I'm the billing specialist. I can help with invoices, payments, and subscription questions. What would you like to know about your bill?
User: Why was I charged twice this month?
[billing_agent]: I'll look into that duplicate charge for you...
✅ When to Use Coordinator
- User should interact directly with specialists
- Routing decisions are straightforward
- You don't need to process specialist responses
- Conversation flow is linear (one specialist at a time)
4. Pattern 2: Agents as Tools (AgentTool)
🎯 Use Case: Knowledge Aggregation
You're building a smart assistant that answers questions spanning multiple domains. A user asks "What is 15% of 250, and why is that number significant in history?" You need to call a math expert, then a trivia expert, and combine their answers.
The AgentTool pattern wraps agents as callable tools. Unlike sub-agents, the coordinator invokes specialists programmatically and receives their responses to process or combine before replying to the user.
Key Differences from Coordinator
Coordinator (Sub-Agents)
- • Specialist talks to user directly
- • One specialist at a time
- • No response processing
AgentTool
- • Coordinator receives responses
- • Can call multiple specialists
- • Aggregates and summarizes
Implementation
use adk_agent::LlmAgentBuilder;
use adk_tool::{AgentTool, FunctionTool};
use adk_core::ToolContext;
use serde_json::{json, Value};
use std::sync::Arc;
// Calculator tool for the math agent
async fn calculator(
_ctx: Arc<dyn ToolContext>,
args: Value
) -> Result<Value, adk_core::AdkError> {
let operation = args["operation"].as_str().unwrap_or("add");
let a = args["a"].as_f64().unwrap_or(0.0);
let b = args["b"].as_f64().unwrap_or(0.0);
let result = match operation {
"add" => a + b,
"multiply" => a * b,
"percent" => a * (b / 100.0),
_ => return Err(adk_core::AdkError::Tool(
format!("Unknown operation: {}", operation)
)),
};
Ok(json!({ "result": result }))
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let api_key = std::env::var("GOOGLE_API_KEY")?;
let model = Arc::new(GeminiModel::new(&api_key, "gemini-2.5-flash")?);
// Create calculator tool
let calc_tool = FunctionTool::new(
"calculator",
"Performs arithmetic: add, multiply, percent. Args: operation (string), a (number), b (number)",
calculator,
);
// Math Expert agent - has its own tools
let math_agent = LlmAgentBuilder::new("math_expert")
.description(
"A math expert that performs calculations. Use for any math-related questions, percentages, or numerical analysis."
)
.instruction(
"You are a math expert. Use the calculator tool for calculations. Show your work step by step. Be precise."
)
.model(model.clone())
.tool(Arc::new(calc_tool))
.build()?;
// Trivia Expert agent - uses LLM knowledge
let trivia_agent = LlmAgentBuilder::new("trivia_expert")
.description(
"A trivia and history expert. Use for questions about historical facts, pop culture, science facts, and trivia."
)
.instruction(
"You are a trivia expert with vast knowledge across domains. Answer questions accurately and include interesting related facts."
)
.model(model.clone())
.build()?;
// Wrap agents as tools with configuration
let math_tool = AgentTool::new(Arc::new(math_agent))
.skip_summarization(false) // Summarize lengthy responses
.forward_artifacts(true); // Pass through any generated files
let trivia_tool = AgentTool::new(Arc::new(trivia_agent))
.skip_summarization(false);
// Coordinator uses agents as tools
let coordinator = LlmAgentBuilder::new("coordinator")
.description("Smart assistant that combines expert knowledge")
.instruction(
"You are a helpful assistant with access to expert agents:\n - math_expert: For calculations and math problems\n - trivia_expert: For facts, history, and trivia\n\n When questions span multiple domains, call multiple experts and synthesize their responses into a cohesive answer."
)
.model(model)
.tool(Arc::new(math_tool))
.tool(Arc::new(trivia_tool))
.build()?;
Launcher::new(Arc::new(coordinator)).run().await?;
Ok(())
}Example: Multi-Domain Question
User: What is 15% of 250, and is that number significant in history?
System: // Coordinator calls math_expert tool
[math_expert responds]: 15% of 250 is 37.5
System: // Coordinator calls trivia_expert tool
[trivia_expert responds]: 37 and 38 are less historically notable, but 37.5°C is human body temperature...
System: // Coordinator synthesizes
[coordinator]: 15% of 250 equals 37.5. Interestingly, 37.5°C (99.5°F) is close to the average human body temperature of 37°C, making it a medically significant number!
✅ When to Use AgentTool
- You need to combine responses from multiple experts
- Coordinator should summarize or filter specialist output
- Specialists have their own tools (nested capabilities)
- You want programmatic control over agent invocation
5. Pattern 3: The Supervisor Graph
🎯 Use Case: Content Creation Pipeline
You're building a content creation system. Given a topic, you need to: (1) research it, (2) write an article, (3) add code examples. The supervisor dynamically decides the order based on the task, and workers can cycle back for revisions.
The Supervisor Graph pattern uses ADK-Rust's graph-based workflow system. A supervisor agent dynamically routes to workers, with full state management and cyclical execution support.
Why Use a Graph?
- Dynamic routing: Supervisor decides next worker based on current state
- Cyclical execution: Workers can loop back for iterations
- Shared state: All nodes read/write to a common state object
- Conditional edges: Different paths based on LLM decisions
- Recursion limits: Prevent infinite loops
Implementation
use adk_agent::LlmAgentBuilder;
use adk_graph::{
StateGraph,
edge::{START, END},
node::{AgentNode, ExecutionConfig, NodeOutput},
state::State,
};
use adk_model::GeminiModel;
use serde_json::json;
use std::sync::Arc;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let api_key = std::env::var("GOOGLE_API_KEY")?;
let model = Arc::new(GeminiModel::new(&api_key, "gemini-2.0-flash")?);
// Supervisor: Decides which worker should act next
let supervisor = LlmAgentBuilder::new("supervisor")
.description("Routes tasks to specialized workers")
.instruction(
"You are a task supervisor. Based on the task and work done so far, decide who should work next.\n\n Workers available:\n - researcher: Gathers information and facts\n - writer: Writes content based on research\n - coder: Creates code examples\n\n Respond with ONLY one word: 'researcher', 'writer', 'coder', or 'done'."
)
.model(model.clone())
.build()?;
// Workers with specialized roles
let researcher = LlmAgentBuilder::new("researcher")
.instruction("Research the topic. Provide key facts as bullet points.")
.model(model.clone())
.build()?;
let writer = LlmAgentBuilder::new("writer")
.instruction("Write engaging content based on the research provided.")
.model(model.clone())
.build()?;
let coder = LlmAgentBuilder::new("coder")
.instruction("Write clean, documented code examples for the topic.")
.model(model.clone())
.build()?;
// Create AgentNodes with input/output mappers
let supervisor_node = AgentNode::new(Arc::new(supervisor))
.with_input_mapper(|state| {
let task = state.get("task").and_then(|v| v.as_str()).unwrap_or("");
let history = state.get("history")
.and_then(|v| v.as_array())
.map(|arr| arr.iter()
.filter_map(|h| h.get("agent").and_then(|a| a.as_str()))
.map(|s| format!("- {} completed", s))
.collect::<Vec<_>>()
.join("\n"))
.unwrap_or_default();
adk_core::Content::new("user").with_text(format!(
"Task: {}\n\nWork completed:\n{}\n\nWho next?",
task,
if history.is_empty() { "None yet" } else { &history }
))
})
.with_output_mapper(|events| {
let mut updates = std::collections::HashMap::new();
for event in events {
if let Some(content) = event.content() {
let text: String = content.parts.iter()
.filter_map(|p| p.text())
.collect();
let next = if text.to_lowercase().contains("researcher") {
"researcher"
} else if text.to_lowercase().contains("writer") {
"writer"
} else if text.to_lowercase().contains("coder") {
"coder"
} else {
"done"
};
updates.insert("next_agent".to_string(), json!(next));
}
}
updates
});
// Build the graph
let graph = StateGraph::with_channels(&[
"task", "next_agent", "history",
"research_output", "written_content", "code_output"
])
.add_node(supervisor_node)
.add_node(AgentNode::new(Arc::new(researcher)))
.add_node(AgentNode::new(Arc::new(writer)))
.add_node(AgentNode::new(Arc::new(coder)))
// Finalize node compiles all outputs
.add_node_fn("finalize", |ctx| async move {
let research = ctx.get("research_output").and_then(|v| v.as_str());
let content = ctx.get("written_content").and_then(|v| v.as_str());
let code = ctx.get("code_output").and_then(|v| v.as_str());
let result = format!(
"=== FINAL OUTPUT ===\n\n{}\n\n{}\n\n{}",
research.unwrap_or("No research"),
content.unwrap_or("No content"),
code.unwrap_or("No code")
);
Ok(NodeOutput::new().with_update("final_result", json!(result)))
})
// Graph structure
.add_edge(START, "supervisor")
.add_conditional_edges(
"supervisor",
|state| state.get("next_agent")
.and_then(|v| v.as_str())
.unwrap_or("done")
.to_string(),
[
("researcher", "researcher"),
("writer", "writer"),
("coder", "coder"),
("done", "finalize"),
],
)
// Workers cycle back to supervisor
.add_edge("researcher", "supervisor")
.add_edge("writer", "supervisor")
.add_edge("coder", "supervisor")
.add_edge("finalize", END)
.compile()?
.with_recursion_limit(15); // Prevent infinite loops
// Execute
let mut input = State::new();
input.insert("task".to_string(), json!("Create a guide about Rust error handling"));
input.insert("history".to_string(), json!([]));
let result = graph.invoke(input, ExecutionConfig::new("content-thread")).await?;
println!("{}", result.get("final_result").and_then(|v| v.as_str()).unwrap_or(""));
Ok(())
}✅ When to Use Supervisor Graph
- Workflow order is dynamic and LLM-determined
- Workers may need to iterate or cycle back
- Complex state needs to be shared between agents
- You need checkpointing or resumable workflows
- Task decomposition requires multiple sequential steps
6. Pattern Comparison
| Feature | Coordinator | AgentTool | Supervisor Graph |
|---|---|---|---|
| User talks to | Specialist directly | Coordinator only | Final output |
| Multi-agent calls | ❌ One at a time | ✅ Parallel possible | ✅ Orchestrated |
| Response processing | ❌ | ✅ | ✅ |
| Cyclic workflows | ❌ | ❌ | ✅ |
| Shared state | Session only | Session only | Full graph state |
| Setup complexity | 🟢 Low | 🟡 Medium | 🔴 High |
7. Conclusion
Multi-agent systems let you build sophisticated AI applications by combining specialized agents. Choose your pattern based on your needs:
- Coordinator: Quick to set up, great for customer service routing
- AgentTool: When you need to process or combine responses
- Supervisor Graph: Complex, dynamic, multi-step workflows