The Complete Guide to Google's Agent Development Kit (ADK)
Build production-grade multi-agent systems with Google's open-source ADK v1.25. From YAML-based authoring to visual builders to streaming deployment, no vendor lock-in.
Part of the AI Agents topic hub.
Table of Contents
The Agent Development Kit (ADK) is Google’s open-source framework for building intelligent agent systems. When I first published this guide in April 2025, ADK was promising but raw. A handful of agent types, basic tooling, and a lot of “coming soon” energy. Ten months and twenty-five releases later, it’s a full-featured framework powering real agentic use-cases.
Do we really need another agent framework? Probably not. But ADK has carved out a real niche: it’s model-agnostic despite being Google-built, it handles multi-agent orchestration better than most alternatives, and the tooling ecosystem around it has gotten impressive. With Gemini 3 now powering the default experience, the framework has matured alongside the models.
In this mammoth guide, I’ll walk you through everything ADK has to offer, from basic agent creation to multi-agent orchestration, from YAML-based config authoring to deploying with a single CLI command. By the end, you’ll be able to build, test, and deploy a production multi-agent system. Let’s dive in.
What’s new since the original guide? ADK has gone from v1.7 to v1.25. The big themes: you can now define agents in YAML or build them visually with a drag-and-drop UI, deploy to Cloud Run with a single command, and communicate across frameworks via the A2A protocol. There are also multi-language SDKs (TypeScript, Go, Java), built-in OpenTelemetry tracing, and a bunch of new tools and integrations. This guide has been fully rewritten to cover all of it.
PS - I also recommend reading my guide on How to Train Your Dragon How To Design AI Agents, where I talk through different architectures and components of effective AI agents.
Key Features and Capabilities
Here’s what makes ADK worth your time over the dozen other frameworks out there:
-
Multi-Agent Architecture: create modular, scalable applications where different agents handle specific tasks, working in concert to achieve complex goals
-
Model Flexibility: use Gemini 3 models natively, access 200+ models via Vertex AI Model Garden, or use LiteLLM to plug in Claude, GPT, Mistral, and others. No vendor lock-in.
-
Rich Tool Ecosystem: pre-built tools (Google Search, Code Execution, Computer Use), GCP integrations (Spanner, BigQuery, Bigtable, Pub/Sub), MCP tools and resources, third-party libraries (LangChain, LlamaIndex), SkillToolsets, or even other agents as tools. The ecosystem here is deep.
-
Built-in Streaming: progressive SSE streaming enabled by default since v1.22, plus native bidirectional audio and video streaming for real-time interactions.
-
Flexible Orchestration: structured workflows using specialized workflow agents (Sequential, Parallel, Loop) for predictable execution patterns, and dynamic LLM-driven routing for more adaptive behavior.
-
YAML Config-Based Authoring: define agents declaratively in YAML files as an alternative to Python code. Great for simple configurations and non-developer collaboration.
-
Integrated Developer Experience: a powerful CLI, visual Web UI with the new drag-and-drop Agent Builder, hot reloading, and integrated trace views for debugging.
-
Built-in Evaluation: systematically assess agent performance with predefined test cases and LLM-powered user simulation that generates realistic multi-turn conversations.
-
Deployment Options: one-command Cloud Run deployment, managed hosting on Vertex AI Agent Engine, Agent Starter Pack templates, or containerize and deploy anywhere.
-
Multi-Language SDKs: build agents in Python, TypeScript, Go, or Java, all with first-party support from Google.
-
OpenTelemetry Observability: built-in tracing and instrumentation that integrates with Google Cloud Observability, Arize, Langfuse, and other monitoring platforms.
-
A2A Protocol: the Agent-to-Agent protocol enables cross-framework agent communication via gRPC, so your ADK agents can collaborate with agents built in any framework.
The Architecture of ADK
To understand ADK, it helps to understand the building blocks. The architecture is surprisingly clean once you see how the pieces fit:
Core Components:
-
Agents: The central entities that make decisions and take actions. ADK supports LLM-powered agents, workflow agents (Sequential, Parallel, Loop), and custom agents via
BaseAgent. -
Tools: Functions or capabilities that agents can use to perform specific actions: searching the web, executing code, querying databases, controlling a browser.
-
Runners: Components that manage the execution flow of agents, handling the orchestration of messages, events, and state management. All runner operations are async-first since v1.0.
-
Sessions: Maintain the context and state of conversations. Sessions now support auto-creation, rewind to previous states, and context compaction for managing large conversation histories.
-
Events: The communication mechanism between components in the system, representing steps in agent execution.
-
Artifact Service: Handles file and blob storage for agents: documents, images, or any binary data that agents produce or consume.
-
Memory Service: Provides long-term memory that persists beyond individual sessions, allowing agents to build knowledge over time.
Architectural Patterns:
ADK is built around a flexible, async-first, event-driven architecture that enables:
-
Modular Design: Components can be combined and reconfigured to create different agent behaviors
-
Extensibility: The system can be extended with new tools, models, agent types, and service backends
-
Separation of Concerns: Clear boundaries between reasoning (agents), capabilities (tools), execution (runners), state management (sessions), and observability (OpenTelemetry)
-
Observable by Default: Built-in OpenTelemetry instrumentation means every agent operation is automatically traced, no custom logging code needed
The beauty here is separation of concerns. You focus on what your agents should do, and ADK handles the gnarly parts: orchestration, state management, communication, and observability.
Getting Started with ADK
Alright, let’s get cooking. ADK requires Python 3.10 or later, and I’d recommend using a virtual environment.
Basic Installation
# Create a virtual environment (recommended)
python -m venv .venv
# Activate the virtual environment
# On macOS/Linux:
source .venv/bin/activate
# On Windows (CMD):
.venv\Scripts\activate.bat
# On Windows (PowerShell):
.venv\Scripts\Activate.ps1
# Install ADK
pip install google-adk
# Optional: install extras for A2A, evaluation, or cloud deployment
# pip install google-adk[a2a,eval,cloud]
This installs the core ADK package with everything you need to build and run agents locally. Pop your GOOGLE_API_KEY in a .env file and you’re good to go. If you’re using Vertex AI instead, set GOOGLE_GENAI_USE_VERTEXAI=TRUE in your environment.
Creating Your First Basic Agent
Let’s create a simple agent that can tell you the weather and time for a specific city. This example will demonstrate the basic structure of an ADK project.
This is the directory structure for our agent:
parent_folder/
weather_time_agent/
__init__.py
agent.py
.env
Create the necessary files in your terminal:
mkdir -p weather_time_agent
echo "from . import agent" > weather_time_agent/__init__.py
touch weather_time_agent/agent.py
touch weather_time_agent/.env
Now edit agent.py to create your agent:
import datetime
from zoneinfo import ZoneInfo
from google.adk.agents import Agent
def get_weather(city: str) -> dict:
"""Retrieves the current weather report for a specified city.
Args:
city (str): The name of the city for which to retrieve the weather report.
Returns:
dict: status and result or error msg.
"""
if city.lower() == "new york":
return {
"status": "success",
"report": (
"The weather in New York is sunny with a temperature of 25 degrees"
" Celsius (41 degrees Fahrenheit)."
),
}
else:
return {
"status": "error",
"error_message": f"Weather information for '{city}' is not available.",
}
def get_current_time(city: str) -> dict:
"""Returns the current time in a specified city.
Args:
city (str): The name of the city for which to retrieve the current time.
Returns:
dict: status and result or error msg.
"""
if city.lower() == "new york":
tz_identifier = "America/New_York"
else:
return {
"status": "error",
"error_message": (
f"Sorry, I don't have timezone information for {city}."
),
}
tz = ZoneInfo(tz_identifier)
now = datetime.datetime.now(tz)
report = (
f'The current time in {city} is {now.strftime("%Y-%m-%d %H:%M:%S %Z%z")}'
)
return {"status": "success", "report": report}
weather_time_agent = Agent(
name="weather_time_agent",
model="gemini-3-flash-preview",
description=(
"Agent to answer questions about the time and weather in a city."
),
instruction=(
"I can answer your questions about the time and weather in a city."
),
tools=[get_weather, get_current_time],
)
Add your API keys to the .env file, then run the agent with adk run weather_time_agent.
The Same Agent in YAML
Since v1.12, ADK supports YAML-based agent authoring as an alternative to Python code. Here’s the same weather agent defined declaratively:
# weather_time_agent/agent.yaml
name: weather_time_agent
model: gemini-3-flash-preview
description: Agent to answer questions about the time and weather in a city.
instruction: I can answer your questions about the time and weather in a city.
tools:
- get_weather
- get_current_time
The YAML config references the same Python tool functions. You still define those in agent.py. But the agent definition itself becomes a config file that’s easier to read, version, and share with non-developers. You can also load YAML agents programmatically:
from google.adk.agents import Agent
agent = Agent.load("weather_time_agent/agent.yaml")
My recommendation: use YAML for simple agent configs and Python when you need custom logic, callbacks, or dynamic behavior. The two are fully interchangeable, so start with whatever feels right.
Visual Agent Builder
If you’re more of a visual thinker (or you’re collaborating with non-developers), ADK’s drag-and-drop Agent Builder lets you design multi-agent systems right in the browser:
adk ui
This launches a web interface where you can visually compose agent hierarchies, configure tools, and test interactions, all without writing code. The builder generates YAML configs that you can export and version control. It’s excellent for prototyping and for collaborating with non-developers on agent design.
Of course, this weather agent is really basic and doesn’t need a framework. But now that you’ve got the scaffolding down, let’s get to the good stuff.
Building Agents: The Foundation
ADK gives you several agent types to work with, each suited to different patterns. If you’ve read my guide on how to design AI agents, you’ll recognize these patterns immediately:
LLM Agent
The LlmAgent (often simply referred to as Agent) is the most commonly used agent type. It leverages a Large Language Model to understand user requests, make decisions, and generate responses. This is the “thinking” component of your application.
from google.adk.agents import Agent # This is actually an LlmAgent
my_agent = Agent(
name="my_first_agent",
model="gemini-3-flash-preview",
description="A helpful assistant that answers general questions.",
instruction="You are a friendly AI assistant. Be concise and helpful.",
tools=[] # Optional tools
)
The LlmAgent is non-deterministic. Its behaviour depends on the LLM’s interpretation of instructions and context. It can use tools, transfer to other agents, or just respond directly. This is the agent type you’ll use 90% of the time.
Workflow Agents
Workflow agents provide deterministic orchestration for sub-agents. Unlike LLM agents, they follow predefined execution patterns:
SequentialAgent: Executes sub-agents one after another, in order:
from google.adk.agents import SequentialAgent
step1 = Agent(name="data_collector", model="gemini-3-flash-preview")
step2 = Agent(name="data_analyzer", model="gemini-3-flash-preview")
pipeline = SequentialAgent(
name="analysis_pipeline",
sub_agents=[step1, step2] # Will execute in this order
)
ParallelAgent: Executes sub-agents concurrently:
from google.adk.agents import ParallelAgent
fetch_weather = Agent(name="weather_fetcher", model="gemini-3-flash-preview")
fetch_news = Agent(name="news_fetcher", model="gemini-3-flash-preview")
parallel_agent = ParallelAgent(
name="information_gatherer",
sub_agents=[fetch_weather, fetch_news] # Will execute in parallel
)
LoopAgent: Repeatedly executes sub-agents until a condition is met:
from google.adk.agents import LoopAgent
process_step = Agent(name="process_item", model="gemini-3-flash-preview")
check_condition = Agent(name="check_complete", model="gemini-3-flash-preview")
loop_agent = LoopAgent(
name="processing_loop",
sub_agents=[process_step, check_condition],
max_iterations=5 # Optional maximum iterations
)
Custom Agents
For specialized needs, you can create custom agents by extending the BaseAgent class:
from google.adk.agents import BaseAgent
from google.adk.agents.invocation_context import InvocationContext
from google.adk.events import Event
from typing import AsyncGenerator
class MyCustomAgent(BaseAgent):
name: str = "custom_agent"
description: str = "A specialized agent with custom behavior"
async def _run_async_impl(self, context: InvocationContext) -> AsyncGenerator[Event, None]:
# Custom implementation logic here
# You must yield at least one Event
yield Event(author=self.name, content=...)
Custom agents are useful when you need deterministic behavior that doesn’t fit into the existing workflow agent patterns, or when you want to integrate with external systems in custom ways.
YAML-Defined Agents
Beyond the basic YAML agent config shown earlier, you can define workflow agents declaratively too:
# sequential_pipeline.yaml
name: analysis_pipeline
type: sequential
sub_agents:
- name: data_collector
model: gemini-3-flash-preview
instruction: Collect and validate the input data.
output_key: collected_data
- name: data_analyzer
model: gemini-3-flash-preview
instruction: Analyze the data in state["collected_data"] and provide insights.
# parallel_gatherer.yaml
name: information_gatherer
type: parallel
sub_agents:
- name: weather_fetcher
model: gemini-3-flash-preview
instruction: Fetch current weather data.
output_key: weather_data
- name: news_fetcher
model: gemini-3-flash-preview
instruction: Fetch the latest relevant news.
output_key: news_data
You can mix and match YAML-defined and Python-defined agents in the same system. I find YAML great for prototyping and for agents that are mostly configuration, and Python essential for anything with custom logic.
Visual Agent Builder
The Visual Agent Builder (launched in v1.18) provides a drag-and-drop browser interface for designing multi-agent systems. Launch it with adk ui and you’ll get:
- A canvas for visually composing agent hierarchies
- Configuration panels for each agent’s model, instructions, and tools
- An AI-powered development assistant that can suggest agent architectures
- Real-time testing and debugging within the builder
- YAML export for version control and deployment
The builder generates the same YAML config files you’d write by hand. Pretty sweet for getting stakeholders involved in agent design without making them learn Python.
Configuring an Agent: Models, Instructions, Descriptions
Now, let’s talk about the knobs you can turn. The behaviour of an agent comes down to a few key parameters:
Model Selection
The model parameter specifies which LLM powers your agent’s reasoning (for LlmAgent). Since v1.22, the model parameter is optional. If omitted, ADK falls back to a default model.
# Using the latest Gemini 3 models
agent = Agent(
name="gemini_agent",
model="gemini-3-flash-preview", # Fast, capable, agentic vision
# Or use: "gemini-3-pro-preview" for complex reasoning
# Or use: "gemini-2.5-flash" for stable production workloads
)
Setting Instructions
The instruction parameter provides guidance to the agent on how it should behave. This is one of the most important parameters for shaping agent behaviour:
agent = Agent(
name="customer_support",
model="gemini-3-flash-preview",
instruction="""
You are a customer support agent for TechGadgets Inc.
When helping customers:
1. Greet them politely and introduce yourself
2. Ask clarifying questions if the issue isn't clear
3. Provide step-by-step troubleshooting when appropriate
4. For billing issues, use the check_account_status tool
5. For technical problems, use the diagnostic_tool
6. Always end by asking if there's anything else you can help with
Never share internal company information or promise specific refund amounts.
"""
)
Best practices for effective instructions:
-
Be specific about the agent’s role and persona
-
Include clear guidelines for when and how to use available tools
-
Use formatting (headers, numbered lists) for readability
-
Provide examples of good and bad responses
-
Specify any constraints or boundaries
Defining Descriptions
The description parameter provides a concise summary of the agent’s purpose:
agent = Agent(
name="billing_specialist",
description="Handles customer billing inquiries and invoice issues.",
# Other parameters...
)
While the description is optional for standalone agents, it becomes critical in multi-agent systems. Other agents use this description to determine when to delegate tasks to this agent. A good description should:
-
Clearly state the agent’s specific domain of expertise
-
Be concise (usually 1-2 sentences)
-
Differentiate the agent from others in the system
Setting Output Key
The optional output_key parameter allows an agent to automatically save its response to the session state:
recommendation_agent = Agent(
name="product_recommender",
# Other parameters...
output_key="product_recommendation"
)
This is a huge deal in multi-agent workflows. Subsequent agents can just grab the output from state without any extra plumbing.
Working with Multiple LLM Providers
Here’s where it gets interesting. ADK isn’t locked to Gemini. Through LiteLLM integration, you can run any model from any provider. Want your router agent on Gemini 3 and your code writer on Claude Opus? Go for it.
First, install the LiteLLM package: pip install litellm
Then, configure your API keys for the models you want to use:
export OPENAI_API_KEY="your-openai-key"
export ANTHROPIC_API_KEY="your-anthropic-key"
Add others as needed
Use the LiteLlm wrapper when defining your agent:
from google.adk.agents import Agent
from google.adk.models.lite_llm import LiteLlm
# Using OpenAI's GPT-5.2
gpt_agent = Agent(
name="gpt_agent",
model=LiteLlm(model="openai/gpt-5.2"),
description="A GPT-powered agent",
# Other parameters...
)
# Using Anthropic's Claude Opus 4.6
claude_agent = Agent(
name="claude_agent",
model=LiteLlm(model="anthropic/claude-opus-4-6"),
description="A Claude-powered agent",
# Other parameters...
)
# Using Mistral AI's model
mistral_agent = Agent(
name="mistral_agent",
model=LiteLlm(model="mistral/mistral-large-latest"),
description="A Mistral-powered agent",
# Other parameters...
)
This approach allows you to:
-
Match models to specific tasks based on their strengths
-
Build resilience by having alternatives if one provider has issues
-
Optimize for cost by using less expensive models for simpler tasks
Now let’s talk about giving your agents superpowers with tools.
Tools: Extending Agent Capabilities
An LLM on its own can think and talk, but it can’t do anything. Tools are how you give your agents hands. They can fetch real-time data, run calculations, call APIs, execute code, control a browser. Basically anything you can write a Python function for.
The agent decides when to use a tool and what parameters to pass. The tool does the actual work and returns results. Simple pattern, powerful results.
Creating Custom Function Tools
The most common way to create tools in ADK is by defining Python functions. These functions can then be passed to an agent, which will call them when appropriate based on its reasoning.
Basic Tool Definition
def calculate_mortgage_payment(principal: float, annual_interest_rate: float, years: int) -> dict:
"""Calculates the monthly payment for a mortgage loan.
Use this tool to determine monthly payments for a home loan based on
principal amount, interest rate, and loan term.
Args:
principal: The initial loan amount in dollars.
annual_interest_rate: The annual interest rate as a percentage (e.g., 5.5 for 5.5%).
years: The loan term in years.
Returns:
dict: A dictionary containing the status and payment details.
"""
try:
monthly_rate = (annual_interest_rate / 100) / 12
num_payments = years * 12
if monthly_rate <= 0 or principal <= 0 or num_payments <= 0:
return {"status": "error", "error_message": "All inputs must be positive."}
monthly_payment = principal * (monthly_rate * (1 + monthly_rate) ** num_payments) / ((1 + monthly_rate) ** num_payments - 1)
return {
"status": "success",
"monthly_payment": round(monthly_payment, 2),
"total_payments": round(monthly_payment * num_payments, 2),
"total_interest": round((monthly_payment * num_payments) - principal, 2)
}
except Exception as e:
return {"status": "error", "error_message": f"Failed to calculate: {str(e)}"}
from google.adk.agents import Agent
mortgage_advisor = Agent(
name="mortgage_advisor",
model="gemini-3-flash-preview",
description="Helps calculate and explain mortgage payments.",
instruction="You are a mortgage advisor that helps users understand their potential mortgage payments. When asked about payments, use the calculate_mortgage_payment tool.",
tools=[calculate_mortgage_payment]
)
Tool Context and State Management
For more advanced tools that need to access or modify the conversation state, ADK provides the ToolContext object. By adding this parameter to your function, you gain access to the session state and can influence the agent’s subsequent actions.
Accessing and Modifying State
from google.adk.tools.tool_context import ToolContext
def update_user_preference(category: str, preference: str, tool_context: ToolContext) -> dict:
"""Updates a user's preference for a specific category.
Args:
category: The category for which to set a preference (e.g., "theme", "notifications").
preference: The preference value to set.
tool_context: Automatically provided by ADK, do not specify when calling.
Returns:
dict: Status of the preference update operation.
"""
user_prefs_key = "user:preferences"
preferences = tool_context.state.get(user_prefs_key, {})
preferences[category] = preference
tool_context.state[user_prefs_key] = preferences
return {
"status": "success",
"message": f"Your {category} preference has been set to {preference}"
}
Controlling Agent Flow
The ToolContext also allows tools to influence the agent’s execution flow through the actions attribute:
def escalate_to_support(issue_type: str, severity: int, tool_context: ToolContext) -> dict:
"""Escalates an issue to a human support agent.
Args:
issue_type: The type of issue being escalated.
severity: The severity level (1-5, where 5 is most severe).
tool_context: Automatically provided by ADK.
Returns:
dict: Status of the escalation.
"""
tool_context.state["escalation_details"] = {
"issue_type": issue_type,
"severity": severity
}
if severity >= 4:
tool_context.actions.transfer_to_agent = "human_support_agent"
return {
"status": "success",
"message": "High-severity issue. Transferring you to a human specialist."
}
return {
"status": "success",
"message": f"Your {issue_type} issue has been logged with severity {severity}."
}
Built-in Tools and Integrations
Here’s where ADK really shines compared to other frameworks. The built-in tool ecosystem is massive and growing fast:
Google Search
from google.adk.tools import google_search
search_agent = Agent(
name="research_assistant",
model="gemini-3-flash-preview",
instruction="You help users research topics. Use google_search for up-to-date information.",
tools=[google_search]
)
Code Execution
from google.adk.tools import code_interpreter
coding_assistant = Agent(
name="coding_assistant",
model="gemini-3-flash-preview",
instruction="You help with coding tasks. Use the code_interpreter to execute Python code.",
tools=[code_interpreter]
)
Computer Use Toolset
New in v1.8, the Computer Use Toolset enables agents to interact with desktop environments: clicking, typing, scrolling, and taking screenshots:
from google.adk.tools.computer_use import ComputerUseToolset
browser_agent = Agent(
name="browser_agent",
model="gemini-3-flash-preview",
description="An agent that can browse the web and interact with websites.",
instruction="You can use the computer to browse websites and complete tasks.",
tools=[ComputerUseToolset()],
)
This is powered by Gemini 3’s agentic vision capabilities. Your agent can literally browse websites, fill out forms, and click buttons. That’s wild.
GCP Tool Integrations
ADK includes first-party integrations with Google Cloud services:
from google.adk.tools.spanner import SpannerToolset
from google.adk.tools.bigquery import BigQueryToolset
from google.adk.tools.bigtable import BigtableToolset
from google.adk.tools.pubsub import PubSubTool
# Query your Spanner database with natural language
spanner_tools = SpannerToolset(
project="my-project",
instance="my-instance",
database="my-database"
)
# Natural language BigQuery queries, forecasting, and anomaly detection
bq_tools = BigQueryToolset(project="my-project", dataset="my-dataset")
# Read and write to Bigtable
bigtable_tools = BigtableToolset(
project="my-project",
instance="my-instance",
table="my-table"
)
These tools let your agents query databases, publish messages, and interact with cloud infrastructure using natural language.
Simplified MCP Tools
If you’ve read my MCP series, you know how powerful MCP tools are. The good news: ADK’s MCP integration was massively simplified in v1.0. No more exit stack management or async agent creation. Just point at an MCP server and go:
from google.adk.tools.mcp import MCPToolset, StdioConnectionParams, StreamableHTTPConnectionParams
# Connect to an MCP server via stdio
tools = MCPToolset(
connection_params=StdioConnectionParams(
command="npx",
args=["-y", "@modelcontextprotocol/server-filesystem", "/path/to/dir"]
)
)
# Or connect via Streamable HTTP (new transport, ideal for scalable deployments)
tools = MCPToolset(
connection_params=StreamableHTTPConnectionParams(
url="http://localhost:8080/mcp"
)
)
# Use MCP prompts as agent instructions (v1.18+)
from google.adk.tools.mcp import McpInstructionProvider
agent = Agent(
name="mcp_agent",
model="gemini-3-flash-preview",
tools=[tools],
instruction=McpInstructionProvider(tools), # Auto-load prompts from MCP server
)
Three transport types are now supported: StdioConnectionParams, SseConnectionParams, and StreamableHTTPConnectionParams. Since v1.25, you can also load MCP resources as tools using the MCP Resource Tool.
SkillToolset
New in v1.25, SkillToolset lets you wrap existing agents or capabilities as reusable skills:
from google.adk.tools.skill import SkillToolset
# Wrap an existing agent as a reusable skill
email_skill = SkillToolset(
agent=email_agent,
description="Send and manage emails"
)
# Use skills in other agents
assistant = Agent(
name="executive_assistant",
model="gemini-3-flash-preview",
tools=[email_skill, calendar_skill, travel_skill],
)
Best Practices for Tool Design
Creating effective tools is crucial for agent performance:
-
Verb-Noun Names: Use descriptive names like
fetch_stock_pricerather thanget_stockorstocks -
Rich Docstrings: The LLM reads your docstring to understand when and how to use the tool. Be thorough.
-
Consistent Result Structure: Always return
{"status": "success/error", ...}so the agent knows how to handle results -
No Default Values: Let the LLM decide all parameter values from context
-
Single Responsibility: Each tool should do one thing well
-
Input Validation: Validate inputs early to prevent cascading errors
-
ToolContext for State: Use
ToolContextwhen tools need to read or write session state, or control agent flow
State and Memory: Creating Context-Aware Agents
Here’s the thing about agents: they’re useless if they can’t remember anything. State in ADK is how you give your agents memory. Unlike conversation history (which is just a transcript of messages), state is a structured key-value store that agents can read from and write to. Think of it as context engineering at the framework level.
The Role of Session State
Session state serves several critical functions:
-
Contextual Memory: Allows agents to remember information from earlier in the conversation
-
Preference Storage: Maintains user preferences across interactions
-
Workflow Tracking: Keeps track of where users are in multi-step processes
-
Data Persistence: Stores data that needs to be accessible between different agents
State Structure and Scope
ADK’s state management system has different scopes to address various persistence needs:
session.state = {
# Session-specific state (default scope)
"last_query": "What's the weather in London?",
"current_step": 3,
# User-specific state (persists across sessions)
"user:preferred_temperature_unit": "Celsius",
"user:name": "Alex",
# Application-wide state (shared across all users)
"app:version": "1.2.3",
"app:maintenance_mode": False,
# Temporary state (not persisted beyond current execution)
"temp:calculation_result": 42
}
The prefixes determine the scope:
-
No prefix: Session-specific, persists only for the current session
-
user:: User-specific, persists across all sessions for a particular user -
app:: Application-wide, shared across all users and sessions -
temp:: Temporary, exists only during the current execution cycle
Basic State Access
Note that all session service calls are now async (since v1.0):
from google.adk.sessions import InMemorySessionService
session_service = InMemorySessionService()
APP_NAME = "my_application"
USER_ID = "user_123"
SESSION_ID = "session_456"
# Create or retrieve a session (async!)
session = await session_service.create_session(
app_name=APP_NAME,
user_id=USER_ID,
session_id=SESSION_ID
)
# Reading from state
last_city = session.state.get("last_city", "New York")
# Writing to state
session.state["last_city"] = "London"
Since v1.23, sessions can be auto-created. If you don’t specify a session ID, ADK creates one automatically.
Accessing State in Tools
Tools can access and modify state through the ToolContext parameter:
from google.adk.tools.tool_context import ToolContext
def remember_favorite_city(city: str, tool_context: ToolContext) -> dict:
"""Remembers the user's favorite city.
Args:
city: The city to remember as favorite.
tool_context: Automatically provided by ADK.
"""
tool_context.state["user:favorite_city"] = city
tool_context.state["user:favorite_city_set_at"] = datetime.datetime.now().isoformat()
return {"status": "success", "message": f"I've remembered that your favorite city is {city}."}
Using output_key for Automatic State Updates
The output_key parameter automatically saves an agent’s response to state:
weather_reporter = Agent(
name="weather_reporter",
model="gemini-3-flash-preview",
instruction="You provide weather reports for cities. Be concise but informative.",
tools=[get_weather],
output_key="last_weather_report"
)
When the agent responds, its output gets automatically saved to session.state["last_weather_report"]. No extra code needed. This is chef’s kiss in multi-agent workflows where one agent’s output feeds directly into another.
State-Aware Agent Instructions
Include instructions on how to use state:
personalized_agent = Agent(
name="personalized_assistant",
model="gemini-3-flash-preview",
instruction="""
You are a personalized assistant.
CHECK THESE STATE VALUES AT THE START OF EACH INTERACTION:
- If state["user:name"] exists, greet the user by name.
- If state["user:favorite_city"] exists, personalize weather recommendations.
- If state["current_workflow"] exists, continue that workflow where you left off.
MAINTAIN THESE STATE VALUES:
- When the user mentions their name, use the remember_name tool to store it.
- When discussing a city positively, use the remember_favorite_city tool.
"""
)
State Patterns
Preference Tracking
def set_preference(category: str, value: str, tool_context: ToolContext) -> dict:
"""Stores a user preference."""
preferences = tool_context.state.get("user:preferences", {})
preferences[category] = value
tool_context.state["user:preferences"] = preferences
return {"status": "success", "message": f"Preference set: {category} = {value}"}
def get_preferences(tool_context: ToolContext) -> dict:
"""Retrieves all user preferences."""
preferences = tool_context.state.get("user:preferences", {})
return {"status": "success", "preferences": preferences}
Workflow State Tracking
def start_workflow(workflow_name: str, tool_context: ToolContext) -> dict:
"""Starts a new workflow and tracks it in state."""
workflow = {
"name": workflow_name,
"current_step": 1,
"started_at": datetime.datetime.now().isoformat(),
"data": {}
}
tool_context.state["current_workflow"] = workflow
return {"status": "success", "workflow": workflow}
def update_workflow_step(step: int, data: dict, tool_context: ToolContext) -> dict:
"""Updates the current workflow step and associated data."""
workflow = tool_context.state.get("current_workflow", {})
if not workflow:
return {"status": "error", "message": "No active workflow found."}
workflow["current_step"] = step
workflow["data"].update(data)
tool_context.state["current_workflow"] = workflow
return {"status": "success", "workflow": workflow}
Session Rewind and Context Compaction
Two powerful features added in later releases:
Session Rewind (v1.17): Roll back a session to any previous invocation point. This is useful for debugging, “undo” functionality, or replaying conversations with different parameters:
# Rewind a session to a previous state
await session_service.rewind_session(
app_name=APP_NAME,
user_id=USER_ID,
session_id=SESSION_ID,
target_event_id="event_xyz" # Rewind to this point
)
Context Compaction (v1.16): When conversations grow long and approach context window limits, ADK can automatically summarize earlier parts of the conversation while preserving the most important information:
agent = Agent(
name="long_conversation_agent",
model="gemini-3-flash-preview",
instruction="You handle lengthy customer conversations.",
# Context compaction happens automatically when the context grows too large.
# Since v1.25, you can configure token-threshold-based compaction
# that triggers after each invocation.
)
Long-Term Memory Service
Beyond session state (which lives within a session), ADK provides a Memory Service for persisting knowledge across sessions:
from google.adk.memory import InMemoryMemoryService
memory_service = InMemoryMemoryService()
# Explicitly save session memories for long-term storage
# Available in ToolContext and CallbackContext since v1.21
await tool_context.add_session_to_memory()
# Since v1.25, you can also save specific events
await tool_context.add_events_to_memory(events=[event1, event2])
Think of sessions as short-term memory (what happened in this conversation) and the Memory Service as long-term memory (what the agent knows about the world). The distinction matters when you’re building agents that need to learn over time.
Built-in Session Services
ADK provides several session service backends:
InMemorySessionService: For development and testing (data lost on restart)DatabaseSessionService: Backed by SQLite or any SQL database (v1.19+, with a new JSON-based schema since v1.22)VertexAiSessionService: Managed session storage on Google Cloud- Or roll your own by extending the
BaseSessionServiceabstract class
Building Multi-Agent Systems
Now we’re getting into the real meat of ADK. Single agents are fine, but the magic happens when you compose multiple specialized agents into a system. ADK organizes these as hierarchies: parent-child relationships where agents can delegate, specialize, and coordinate.
Creating an Agent Hierarchy
The foundation is the sub_agents parameter:
from google.adk.agents import Agent
weather_specialist = Agent(
name="weather_specialist",
model="gemini-3-flash-preview",
description="Provides detailed weather information for any location.",
instruction="You are a weather specialist. Provide accurate, detailed weather information.",
tools=[get_weather]
)
restaurant_specialist = Agent(
name="restaurant_specialist",
model="gemini-3-flash-preview",
description="Recommends restaurants based on location, cuisine, and preferences.",
instruction="You are a restaurant specialist. Recommend restaurants based on user preferences.",
tools=[find_restaurants]
)
coordinator = Agent(
name="travel_assistant",
model="gemini-3-flash-preview",
description="Helps plan trips and activities.",
instruction="""
You are a travel assistant that helps users plan trips.
- For weather questions, delegate to weather_specialist
- For restaurant questions, delegate to restaurant_specialist
- For general travel questions, handle yourself
""",
sub_agents=[weather_specialist, restaurant_specialist]
)
Hierarchy Rules
-
Single Parent Rule: An agent can have only one parent
-
Name Uniqueness: Each agent in the hierarchy must have a unique name
-
Hierarchical Navigation: Use
agent.parent_agent,agent.sub_agents, orroot_agent.find_agent(name)to traverse the tree -
Scope of Control: By default, an agent can transfer to its parent, siblings, or children
Agent-to-Agent Delegation
ADK provides several mechanisms for agents to collaborate:
LLM-Driven Delegation (Auto-Flow)
The most flexible approach. The agent’s LLM decides when to delegate based on query understanding and agent descriptions:
customer_service = Agent(
name="customer_service",
model="gemini-3-flash-preview",
description="Handles general customer inquiries and routes to specialists.",
instruction="""
Analyze each query and delegate appropriately:
- Billing questions → billing_specialist
- Technical issues → tech_support
- Product questions → handle yourself
""",
sub_agents=[
Agent(
name="billing_specialist",
model="gemini-3-flash-preview",
description="Handles all billing, payment, and invoice inquiries."
),
Agent(
name="tech_support",
model="gemini-3-flash-preview",
description="Resolves technical issues and troubleshooting problems."
)
]
)
The key to making this work: clear, distinctive descriptions for each agent and explicit routing instructions for the parent. If your agent descriptions overlap, the LLM won’t know where to send things and you’ll get weird delegation behavior. Ask me how I know.
Explicit Invocation with AgentTool
For more controlled delegation, wrap an agent as a tool:
from google.adk.tools import AgentTool
calculator_agent = Agent(
name="calculator",
model="gemini-3-flash-preview",
description="Performs complex mathematical calculations.",
instruction="You perform mathematical calculations with precision."
)
calculator_tool = AgentTool(
agent=calculator_agent,
description="Use this tool to perform complex calculations."
)
math_tutor = Agent(
name="math_tutor",
model="gemini-3-flash-preview",
description="Helps students learn mathematics.",
instruction="""
When a student asks a question requiring calculations:
1. Explain the concept
2. Use the calculator tool to compute the result
3. Explain the significance of the result
""",
tools=[calculator_tool]
)
With AgentTool, the parent agent decides when to invoke the sub-agent, and the sub-agent’s result is returned to the parent for further processing.
Shared Session State for Communication
Agents can communicate through shared state:
from google.adk.agents import SequentialAgent
information_gatherer = Agent(
name="information_gatherer",
model="gemini-3-flash-preview",
instruction="Gather travel information from the user.",
tools=[save_travel_details],
output_key="gathering_complete"
)
recommendation_generator = Agent(
name="recommendation_generator",
model="gemini-3-flash-preview",
instruction="""
Generate recommendations based on:
- state["travel_destination"]
- state["travel_dates"]
- state["travel_preferences"]
""",
tools=[get_recommendations]
)
travel_planner = SequentialAgent(
name="travel_planner",
sub_agents=[information_gatherer, recommendation_generator]
)
Workflow Patterns
Sequential Workflow
data_processor = SequentialAgent(
name="data_processor",
sub_agents=[
Agent(name="data_validator", model="gemini-3-flash-preview", output_key="validation_result"),
Agent(name="data_transformer", model="gemini-3-flash-preview", output_key="transformed_data"),
Agent(name="data_analyzer", model="gemini-3-flash-preview", output_key="analysis_result"),
Agent(name="report_generator", model="gemini-3-flash-preview")
]
)
Each agent’s output is saved to state via output_key for the next agent to use.
Parallel Workflow
from google.adk.agents import ParallelAgent
data_gatherer = ParallelAgent(
name="data_gatherer",
sub_agents=[
Agent(name="weather_fetcher", model="gemini-3-flash-preview", output_key="weather_data"),
Agent(name="traffic_fetcher", model="gemini-3-flash-preview", output_key="traffic_data"),
Agent(name="news_fetcher", model="gemini-3-flash-preview", output_key="news_data")
]
)
All fetchers run concurrently. Parallel execution is ideal for gathering independent information simultaneously.
Loop Workflow
from google.adk.agents import LoopAgent, BaseAgent
from google.adk.agents.invocation_context import InvocationContext
from google.adk.events import Event, EventActions
from typing import AsyncGenerator
class ConditionChecker(BaseAgent):
name: str = "condition_checker"
async def _run_async_impl(self, context: InvocationContext) -> AsyncGenerator[Event, None]:
completed = context.session.state.get("task_completed", False)
current = context.session.state.get("current_iteration", 0)
context.session.state["current_iteration"] = current + 1
if completed or current >= 5:
yield Event(author=self.name, actions=EventActions(escalate=True))
else:
yield Event(author=self.name, content=None)
iterative_processor = LoopAgent(
name="iterative_processor",
sub_agents=[
Agent(name="task_processor", model="gemini-3-flash-preview"),
ConditionChecker()
],
max_iterations=10
)
Designing Effective Agent Teams
Clear Agent Specialization
Each agent should have a non-overlapping domain of expertise with clear descriptions that differentiate it from others in the system.
Effective Coordination
Choose the right coordination pattern for your use case:
Hub and Spoke: A coordinator delegates to specialists based on query type.
Pipeline: Sequential processing where each stage transforms the data.
Hierarchical: Multi-level delegation for complex organizational structures.
# Multi-level hierarchy
project_manager = Agent(
name="project_manager",
model="gemini-3-flash-preview",
sub_agents=[
Agent(
name="design_lead",
model="gemini-3-flash-preview",
sub_agents=[
Agent(name="ui_designer", model="gemini-3-flash-preview"),
Agent(name="ux_researcher", model="gemini-3-flash-preview")
]
),
Agent(
name="development_lead",
model="gemini-3-flash-preview",
sub_agents=[
Agent(name="frontend_developer", model="gemini-3-flash-preview"),
Agent(name="backend_developer", model="gemini-3-flash-preview")
]
),
Agent(name="qa_lead", model="gemini-3-flash-preview")
]
)
Error Handling and Fallbacks
Design your system to handle failures gracefully by using sequential workflows where a fallback agent checks for errors from the primary agent:
primary_handler = Agent(
name="primary_handler",
model="gemini-3-flash-preview",
instruction="Handle the task. If you encounter an error, set state['error_detected'] = True.",
tools=[process_task]
)
fallback_handler = Agent(
name="fallback_handler",
model="gemini-3-flash-preview",
instruction="""
Check state["error_detected"]. If True, provide a simplified but functional response.
If False, just pass through. The primary handler succeeded.
""",
)
robust_handler = SequentialAgent(
name="robust_handler",
sub_agents=[primary_handler, fallback_handler]
)
Observability with OpenTelemetry
Since v1.23, ADK uses OpenTelemetry for all tracing. No custom logging code needed:
# Set the OTLP endpoint and all agent operations are automatically traced
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
Every agent invocation, tool call, and LLM request generates spans that you can view in Google Cloud Observability, Arize, Langfuse, SigNoz, Dynatrace, or any OpenTelemetry-compatible backend. No more rolling your own logging. This is how it should have worked from the start.
A2A Protocol: Cross-Framework Agent Communication
The Agent-to-Agent (A2A) protocol enables ADK agents to communicate with agents built in any framework. Since v1.8, ADK includes utilities for exposing your agents as A2A-compatible servers:
from google.adk.a2a import to_a2a
# Convert any ADK agent to an A2A ASGI application
a2a_app = to_a2a(
agent=my_agent,
session_service=session_service,
)
# Run as a standalone A2A server
import uvicorn
uvicorn.run(a2a_app, host="0.0.0.0", port=8000)
A2A v0.3 supports gRPC transport, standardized authentication, and agent card discovery. This is a big deal. Your ADK agents can now collaborate with agents from LangChain, CrewAI, AutoGen, or any framework that implements A2A. It’s like a universal language for agent communication.
You can also create custom agent cards for discovery:
from google.adk.a2a import AgentCardBuilder
card = AgentCardBuilder(
name="weather_specialist",
description="Provides weather information for any location worldwide.",
capabilities=["weather_lookup", "forecast"],
supported_protocols=["a2a/v0.3"]
).build()
Advanced Features and Patterns
Alright, you’ve got the fundamentals. Now let’s look at the more sophisticated features that separate toy projects from production systems.
Implementing Safety Guardrails with Callbacks
Callbacks are hooks that intercept agent behavior at key execution points. You’ll want these for safety guardrails, logging, and custom business logic. If you’ve ever shipped an agent to production and watched it do something unexpected, you know why these matter.
Input Validation with before_model_callback
from google.adk.agents.callback_context import CallbackContext
from google.adk.models.llm_request import LlmRequest
from google.adk.models.llm_response import LlmResponse
from google.genai import types
from typing import Optional
def content_safety_filter(
callback_context: CallbackContext, llm_request: LlmRequest
) -> Optional[LlmResponse]:
"""Checks user input for prohibited content before it reaches the LLM."""
prohibited_terms = ["badword1", "badword2"]
last_user_message = ""
if llm_request.contents:
for content in reversed(llm_request.contents):
if content.role == 'user' and content.parts:
if content.parts[0].text:
last_user_message = content.parts[0].text
break
if any(term in last_user_message.lower() for term in prohibited_terms):
callback_context.state["content_filter_triggered"] = True
return LlmResponse(
content=types.Content(
role="model",
parts=[types.Part(text="I cannot respond to messages containing inappropriate language.")]
)
)
return None # Allow the request to proceed
safe_agent = Agent(
name="safe_agent",
model="gemini-3-flash-preview",
instruction="You are a helpful assistant.",
before_model_callback=content_safety_filter
)
Tool Usage Control with before_tool_callback
from google.adk.tools.base_tool import BaseTool
from typing import Dict, Any
def tool_usage_validator(
tool: BaseTool, args: Dict[str, Any], tool_context: ToolContext
) -> Optional[Dict]:
"""Prevents tool calls for restricted inputs."""
if tool.name == "get_weather" and "city" in args:
restricted_cities = ["restricted_city_1", "restricted_city_2"]
if args["city"].lower() in restricted_cities:
return {
"status": "error",
"error_message": f"Weather for {args['city']} is not available due to policy restrictions."
}
return None # Allow the call to proceed
restricted_agent = Agent(
name="restricted_agent",
model="gemini-3-flash-preview",
tools=[get_weather],
before_tool_callback=tool_usage_validator
)
Combining Callbacks
For comprehensive safety, use all four callback points:
comprehensive_agent = Agent(
name="comprehensive_agent",
model="gemini-3-flash-preview",
tools=[get_weather, search_web, send_email],
before_model_callback=content_safety_filter, # Filter unsafe input
after_model_callback=output_sanitizer, # Clean up model responses
before_tool_callback=tool_usage_validator, # Validate tool usage
after_tool_callback=tool_result_logger # Log tool results
)
Building Evaluation Frameworks
Test Cases
Define test cases that cover the range of interactions your agent should handle:
test_cases = [
{
"name": "Basic weather query",
"input": "What's the weather in New York?",
"expected_tool_calls": ["get_weather"],
"expected_tool_args": {"city": "New York"},
"expected_response_contains": ["weather", "New York"]
},
{
"name": "City not supported",
"input": "What's the weather in Atlantis?",
"expected_tool_calls": ["get_weather"],
"expected_response_contains": ["don't have information", "Atlantis"]
}
]
Using the AgentEvaluator
from google.adk.evaluation import AgentEvaluator
evaluator = AgentEvaluator(agent=weather_agent)
evaluation_results = await evaluator.evaluate(test_cases=test_cases)
for result in evaluation_results:
print(f"Test: {result.test_case['name']}")
print(f" Status: {'PASS' if result.success else 'FAIL'}")
if not result.success:
print(f" Feedback: {result.feedback}")
success_rate = sum(1 for r in evaluation_results if r.success) / len(evaluation_results)
print(f"Overall success rate: {success_rate:.2%}")
LLM-Powered User Simulation
New in v1.18, the user simulator generates realistic multi-turn conversations for testing. Instead of scripting rigid test cases, you provide a conversation scenario and the simulator dynamically generates the user side:
from google.adk.evaluation import UserSimulator, ConversationScenario
simulator = UserSimulator(model="gemini-3-flash-preview")
scenario = ConversationScenario(
starting_prompt="I need to return a defective laptop I bought last week.",
conversation_plan=[
"Express frustration about the product quality",
"Ask about the return policy and timeline",
"Provide order number when asked",
"Ask about getting a replacement instead of a refund",
],
max_turns=8,
)
results = await simulator.run(
agent=customer_support_agent,
scenario=scenario,
session_service=session_service,
)
print(f"Conversation completed in {results.num_turns} turns")
print(f"Agent handled all user goals: {results.goals_met}")
print(f"Conversation transcript:\n{results.transcript}")
This completely changes how you evaluate agents. Instead of testing “does the agent respond correctly to this one message,” you’re testing “can the agent handle a frustrated customer who keeps changing their mind over 8 turns.” Much closer to reality.
Streaming and Real-Time Interactions
Nobody likes staring at a loading spinner. ADK has built-in streaming support, and since v1.22, progressive SSE streaming is enabled by default. No extra configuration needed.
Implementing Streaming Responses
import asyncio
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.genai import types
session_service = InMemorySessionService()
APP_NAME = "streaming_app"
USER_ID = "user_123"
SESSION_ID = "session_456"
session = await session_service.create_session(
app_name=APP_NAME, user_id=USER_ID, session_id=SESSION_ID
)
runner = Runner(
agent=my_agent,
app_name=APP_NAME,
session_service=session_service
)
async def stream_response(query: str):
"""Streams the agent's response token by token."""
content = types.Content(role='user', parts=[types.Part(text=query)])
print(f"User: {query}")
print("Agent: ", end="", flush=True)
async for event in runner.run_async(
user_id=USER_ID,
session_id=SESSION_ID,
new_message=content
):
if event.content and event.partial:
print(event.content.parts[0].text, end="", flush=True)
if event.is_final_response():
print()
await stream_response("What's the weather in New York?")
Bidirectional Audio Streaming
ADK also supports voice-based interactions with bidirectional audio streaming:
async def audio_conversation():
"""Conducts a voice conversation with the agent."""
import sounddevice as sd
import numpy as np
import wave, io
# Record audio
sample_rate = 16000
audio_data = sd.rec(int(5 * sample_rate), samplerate=sample_rate, channels=1, dtype='int16')
sd.wait()
# Convert to WAV
audio_bytes = io.BytesIO()
with wave.open(audio_bytes, 'wb') as wf:
wf.setnchannels(1)
wf.setsampwidth(2)
wf.setframerate(sample_rate)
wf.writeframes(audio_data.tobytes())
# Send to agent
audio_part = types.Part.from_bytes(audio_bytes.getvalue(), mime_type="audio/wav")
content = types.Content(role='user', parts=[audio_part])
async for event in runner.run_async(
user_id=USER_ID, session_id=SESSION_ID, new_message=content
):
if event.content and event.partial and event.content.parts[0].text:
print(event.content.parts[0].text, end="", flush=True)
if event.is_final_response() and event.content:
for part in event.content.parts:
if part.mime_type and part.mime_type.startswith('audio/'):
audio = np.frombuffer(part.bytes_value, dtype=np.int16)
sd.play(audio, sample_rate)
sd.wait()
Common Multi-Agent Patterns
Critic-Generator Pattern
One agent generates content, another critiques it, and a third refines based on the critique:
from google.adk.agents import SequentialAgent
critique_workflow = SequentialAgent(
name="critique_workflow",
sub_agents=[
Agent(
name="content_generator",
model="gemini-3-flash-preview",
instruction="Create content based on the user's request. Be creative and comprehensive.",
output_key="generated_content"
),
Agent(
name="content_critic",
model="gemini-3-flash-preview",
instruction="""
Review state["generated_content"] for accuracy, clarity, and comprehensiveness.
Provide specific suggestions for improvement.
""",
output_key="critique"
),
Agent(
name="content_refiner",
model="gemini-3-flash-preview",
instruction="""
Refine state["generated_content"] based on state["critique"].
Maintain the original style while addressing the issues highlighted.
"""
)
]
)
Research and Synthesis Pattern
Parallel information gathering followed by synthesis:
from google.adk.agents import ParallelAgent, SequentialAgent
research_framework = SequentialAgent(
name="research_framework",
sub_agents=[
ParallelAgent(
name="parallel_researchers",
sub_agents=[
Agent(name="economic_researcher", model="gemini-3-flash-preview",
instruction="Research economic aspects. Store findings in state.",
output_key="research_economic"),
Agent(name="environmental_researcher", model="gemini-3-flash-preview",
instruction="Research environmental aspects. Store findings in state.",
output_key="research_environmental"),
Agent(name="social_researcher", model="gemini-3-flash-preview",
instruction="Research social aspects. Store findings in state.",
output_key="research_social"),
]
),
Agent(
name="synthesizer",
model="gemini-3-flash-preview",
instruction="""
Synthesize findings from state["research_economic"],
state["research_environmental"], and state["research_social"].
Identify connections, conflicts, and gaps. Present a balanced view.
"""
)
]
)
A2UI: Agent-Driven Interfaces
A2UI (Agent-to-User Interface) is an open standard that lets agents “speak UI.” Instead of returning plain text, agents send declarative JSON describing UI intent, which client apps render using native widgets.
Integrated into ADK since v1.23, A2UI enables agents to return rich interactive elements like forms, tables, charts, and cards:
from google.adk.agents import Agent
dashboard_agent = Agent(
name="dashboard_agent",
model="gemini-3-flash-preview",
instruction="""
When presenting data, use A2UI components to render rich interfaces:
- Use tables for structured data
- Use charts for trends and comparisons
- Use forms when collecting user input
- Use cards for summarizing entities
Return A2UI JSON alongside your text responses so client apps can render
interactive widgets.
""",
)
Client applications (React, Flutter, SwiftUI) interpret the A2UI JSON and render appropriate native components. The ADK Web UI supports A2UI rendering out of the box since v1.24. Your agent can hand back a chart instead of a wall of numbers. That’s A2UI.
Gemini Interactions API
The Interactions API (v1.21) provides server-side conversation management, taking the burden of history tracking off your application:
from google.adk.agents import Agent
from google.adk.models.gemini import GeminiLlmConnection
# Enable the Interactions API for server-side history management
agent = Agent(
name="managed_agent",
model=GeminiLlmConnection(
model="gemini-3-flash-preview",
use_interactions_api=True, # Server manages conversation history
),
instruction="You are a helpful assistant with server-managed conversation state.",
)
Key benefits:
- Server-side history: No need to manage conversation context client-side
- Background execution: Fire-and-forget long-running agent tasks
- Native thoughts: Model reasoning is tracked as first-class objects
- Managed agents: Access specialized agents like
deep-research-pro-previewthrough a unified interface
Deployment and Production
Building agents is fun. Shipping them is where most people get stuck. ADK has some of the best deployment story of any agent framework I’ve used. Let me walk through the options.
Local Development
During development, you’ve got two main tools:
# Run your agent in the terminal
adk run my_agent
# Launch the web UI with Visual Agent Builder, trace views, and A2UI support
adk ui
# Run with verbose logging for debugging
adk run my_agent -v
The CLI supports hot reloading (--reload_agents) so your agents update automatically as you edit code.
Cloud Run Deployment
The simplest path to production is the Cloud Run deployment CLI (v1.12+):
adk deploy cloud-run \
--project my-gcp-project \
--region us-central1 \
--agent-dir ./my_agent \
--service-name my-agent-service
One command. That’s it. It builds a container, pushes it to Artifact Registry, and deploys to Cloud Run with sensible defaults. Your agent gets a public HTTPS endpoint with auto-scaling, logging, and monitoring. Going from “works on my laptop” to “deployed in production” has never been this easy.
Vertex AI Agent Engine
For fully managed hosting on Google Cloud:
from google.cloud import aiplatform
# Deploy to Vertex AI Agent Engine
aiplatform.init(project="my-project", location="us-central1")
remote_agent = aiplatform.Agent.create(
agent=my_agent,
requirements=["google-adk>=1.25"],
)
# The agent is now hosted and auto-scaled on Vertex AI
response = await remote_agent.query(
user_id="user_123",
message="What's the weather in New York?"
)
Agent Engine provides managed compute, auto-scaling, monitoring, and integration with other Vertex AI services.
Agent Starter Pack
For quick bootstrapping, the Agent Starter Pack provides pre-built templates:
# Initialize a new agent project from a template
adk init --template customer-support
adk init --template research-assistant
adk init --template data-analyst
Each template includes a working agent, tools, tests, a Dockerfile, and Cloud Run deployment configuration.
Containerized Deployment
For custom infrastructure, ADK agents are just Python applications that can be containerized:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
EXPOSE 8080
CMD ["adk", "api_server", "--port", "8080", "--agent-dir", "./my_agent"]
The adk api_server command starts a FastAPI server with REST and SSE endpoints. Deploy this container anywhere: Kubernetes, ECS, Azure Container Apps, a plain VM, whatever your infrastructure team insists on.
Production Considerations
OpenTelemetry: Configure the OTLP endpoint for production monitoring:
export OTEL_EXPORTER_OTLP_ENDPOINT=https://your-collector:4317
export OTEL_SERVICE_NAME=my-agent-service
All agent operations (LLM calls, tool invocations, agent transfers) are automatically instrumented.
Session Persistence: Use DatabaseSessionService or VertexAiSessionService instead of InMemorySessionService:
from google.adk.sessions import DatabaseSessionService
session_service = DatabaseSessionService(
connection_string="sqlite:///sessions.db"
# Or: "postgresql://user:pass@host/db"
)
Health Checks: Since v1.25, the API server exposes /health and /version endpoints for load balancer integration.
Breaking Changes to Watch: If upgrading from older ADK versions:
- All core service methods are async (v1.0+). Add
awaitto session/memory/artifact calls - Python 3.10 is the minimum (v1.19+)
- OpenTelemetry replaces custom tracing (v1.23+)
- Credential manager accepts
tool_contextinstead ofcallback_context(v1.24+) - Database session service uses a new JSON-based schema (v1.22+). Run
adk migrate sessionto upgrade
Putting It All Together
That’s enough theory. Let’s build some real agents. No more weather bots, I promise.
Customer Support Agent
This system handles inquiries about products, orders, billing, and technical support with persistent storage, CRM integration, personalization, and escalation paths.
Architecture
Customer Service System (ADK)
├── Root Coordinator Agent
│ ├── Greeting & Routing Agent
│ ├── Product Information Agent
│ │ └── Tools: product_catalog_lookup, get_specifications
│ ├── Order Status Agent
│ │ └── Tools: order_lookup, track_shipment
│ ├── Billing Agent
│ │ └── Tools: get_invoice, update_payment_method
│ ├── Technical Support Agent
│ │ └── Tools: troubleshoot_issue, create_ticket
│ └── Human Escalation Agent
│ └── Tools: create_escalation_ticket, notify_supervisor
└── Services
├── DatabaseSessionService (persistent storage)
├── Customer Data Service (CRM integration)
└── OpenTelemetry (observability)
Session Management with Persistent Storage
from google.adk.sessions import DatabaseSessionService
# Use the built-in DatabaseSessionService for production
session_service = DatabaseSessionService(
connection_string="postgresql://user:pass@localhost/customer_service"
)
# Sessions are automatically persisted across restarts
session = await session_service.create_session(
app_name="customer_service",
user_id="user_123",
session_id="session_456"
)
CRM Integration Tool
def get_customer_info(customer_id: str, tool_context: ToolContext) -> dict:
"""Retrieves customer information from the CRM system.
Args:
customer_id: The unique identifier for the customer.
tool_context: Provides access to session state.
"""
# In production, this would call your CRM API
customer = crm_client.get_customer(customer_id)
if customer:
tool_context.state["customer_info"] = customer
return {"status": "success", "customer": customer}
else:
return {"status": "error", "error_message": f"Customer {customer_id} not found"}
Issue Escalation
def escalate_to_human(
issue_summary: str, priority: str, customer_id: str, tool_context: ToolContext
) -> dict:
"""Escalates an issue to a human representative.
Args:
issue_summary: Brief description of the issue.
priority: Urgency level ("low", "medium", "high", "urgent").
customer_id: The customer's ID.
tool_context: Provides access to session state.
"""
customer_info = tool_context.state.get("customer_info", {})
customer_tier = customer_info.get("tier", "standard")
sla_hours = {
"low": {"standard": 48, "premium": 24},
"medium": {"standard": 24, "premium": 12},
"high": {"standard": 8, "premium": 4},
"urgent": {"standard": 4, "premium": 1}
}
response_time = sla_hours[priority.lower()][customer_tier]
import hashlib, time
ticket_id = hashlib.md5(f"{customer_id}:{time.time()}".encode()).hexdigest()[:8].upper()
tool_context.actions.transfer_to_agent = "human_support_agent"
return {
"status": "success",
"ticket_id": ticket_id,
"message": f"Escalated. Ticket {ticket_id}. Response within {response_time} hours."
}
Tech Support Agent
tech_support_agent = Agent(
name="technical_support_agent",
model="gemini-3-flash-preview",
description="Handles technical support inquiries and troubleshooting.",
instruction="""
You are a technical support specialist.
Check state["customer_info"]["support_history"] for prior interactions.
For technical issues:
1. Use troubleshoot_issue to analyze the problem
2. Guide through basic troubleshooting steps
3. If unresolved, use create_ticket to log the issue
For complex issues, use escalate_to_human.
Maintain a professional but empathetic tone.
""",
tools=[troubleshoot_issue, create_ticket, escalate_to_human]
)
Personalization Callback
def personalization_callback(
callback_context: CallbackContext, llm_request: LlmRequest
) -> Optional[LlmResponse]:
"""Adds personalization context to LLM requests."""
customer_info = callback_context.state.get("customer_info")
if customer_info:
note = (
f"\nCustomer: {customer_info.get('name', 'valued customer')}"
f"\nTier: {customer_info.get('tier', 'standard')}"
f"\nRecent purchases: {', '.join(customer_info.get('recent_purchases', []))}"
)
if llm_request.contents:
llm_request.contents.insert(0, types.Content(
role="system",
parts=[types.Part(text=note)]
))
return None
YAML Hierarchy Definition
The same agent hierarchy can also be defined in YAML:
# customer_service/agent.yaml
name: customer_service_coordinator
model: gemini-3-flash-preview
description: Routes customer inquiries to the appropriate specialist.
instruction: |
Analyze each query and delegate:
- Product questions → product_specialist
- Order status → order_specialist
- Billing → billing_specialist
- Technical issues → technical_support_agent
- Complex issues → human_support_agent
before_model_callback: personalization_callback
sub_agents:
- name: product_specialist
model: gemini-3-flash-preview
description: Handles product information inquiries.
tools: [product_catalog_lookup, get_specifications]
- name: order_specialist
model: gemini-3-flash-preview
description: Handles order status and shipping inquiries.
tools: [order_lookup, track_shipment]
- name: billing_specialist
model: gemini-3-flash-preview
description: Handles billing, payment, and invoice inquiries.
tools: [get_invoice, update_payment_method]
- name: technical_support_agent
model: gemini-3-flash-preview
description: Handles technical support and troubleshooting.
tools: [troubleshoot_issue, create_ticket, escalate_to_human]
Code Generation and Debugging Agent
A sequential agent that analyzes requirements, creates tests, writes code, and reviews it:
from google.adk.agents import SequentialAgent
from google.adk.tools import code_interpreter
code_generator = SequentialAgent(
name="tdd_code_generator",
sub_agents=[
Agent(
name="requirement_analyzer",
model="gemini-3-flash-preview",
instruction="""
Analyze the coding requirements and break them down into:
1. Functional requirements
2. Edge cases to consider
3. Needed data structures and algorithms
Be specific and comprehensive.
""",
output_key="requirements_analysis"
),
Agent(
name="test_writer",
model="gemini-3-flash-preview",
instruction="""
Based on state["requirements_analysis"], write comprehensive test cases covering:
1. Main functionality
2. Edge cases
3. Error handling
Use pytest for Python or Jest for JavaScript.
""",
tools=[code_interpreter],
output_key="test_code"
),
Agent(
name="code_implementer",
model="gemini-3-flash-preview",
instruction="""
Implement code that passes all tests in state["test_code"].
Use the code_interpreter to verify your implementation runs correctly.
Follow best practices and handle all identified edge cases.
""",
tools=[code_interpreter],
output_key="implementation"
),
Agent(
name="code_reviewer",
model="gemini-3-flash-preview",
instruction="""
Review state["implementation"] for:
1. Correctness against requirements
2. Efficiency and optimization
3. Readability and structure
4. Error handling
5. Security issues
Provide specific improvement suggestions.
""",
tools=[code_interpreter],
output_key="code_review"
)
]
)
The built-in code_interpreter tool handles code execution in a sandboxed environment, so you don’t need to build your own execution infrastructure. If you want to go deeper on building coding agents, check out my tutorial on building a coding agent from scratch.
Multi-Language Agent with A2A
One of ADK’s most powerful capabilities is cross-language agent communication via the A2A protocol. Here’s a Python ADK agent collaborating with a TypeScript ADK agent:
Python Agent (research specialist):
from google.adk.agents import Agent
from google.adk.a2a import to_a2a
research_agent = Agent(
name="research_specialist",
model="gemini-3-flash-preview",
description="Researches topics and provides comprehensive analysis.",
instruction="You research topics thoroughly and provide detailed analysis.",
tools=[google_search]
)
# Expose as an A2A server
a2a_app = to_a2a(agent=research_agent, session_service=session_service)
# Run on port 8001
import uvicorn
uvicorn.run(a2a_app, host="0.0.0.0", port=8001)
TypeScript Agent (content writer) connecting to the Python agent:
import { Agent, A2AClient } from '@anthropic/adk';
// Connect to the Python research agent via A2A
const researchClient = new A2AClient({
url: 'http://localhost:8001',
});
const writerAgent = new Agent({
name: 'content_writer',
model: 'gemini-3-flash-preview',
description: 'Writes polished content based on research.',
instruction: `You are a content writer. Use the research_specialist tool
to gather information, then write polished articles based on the findings.`,
tools: [researchClient.asTool()],
});
// The TypeScript writer agent can now invoke the Python research agent
const response = await writerAgent.run({
message: 'Write an article about the latest developments in quantum computing',
});
This pattern lets you:
- Build agents in the language best suited for each task
- Distribute agents across different services and teams
- Integrate ADK agents with agents from other frameworks (LangChain, CrewAI, AutoGen)
- Scale individual agents independently
Next Steps
That was a lot to take in. You should probably bookmark this page. It’s the kind of reference you’ll come back to. Here’s the order I’d recommend working through things:
- Build the weather agent, both the Python and YAML versions, to get familiar with ADK basics
- Try the Visual Agent Builder (
adk ui) to prototype a multi-agent system visually - Add tools and state management by building something with custom tools and session state
- Build a multi-agent system using the customer support or code gen patterns from this guide
- Deploy with Cloud Run to get your agent running in production with one command
- Explore A2A and connect agents across languages and frameworks
- Set up OpenTelemetry for production-grade observability
Resources
- Official ADK Documentation
- ADK Python GitHub | TypeScript | Go | Java
- ADK Release Notes
- A2A Protocol Specification
For a deeper dive into agent design patterns, check out my guides on How To Design AI Agents and Context Engineering for Agents. If you’re curious about other frameworks, I’ve also covered the OpenAI Agents SDK and building a coding agent from scratch.
Now go build something. And if you build something cool with ADK, let me know. I’d love to see it.
Related Posts
Claude Cowork: A Guide To Getting AI To Work For You
Cowork turns Claude from a chatbot into an agent that actually does work. I built my own version called Jarvis and here's what I learned.
Ralph Wiggum: The Dumbest Smart Way to Run Coding Agents
Forget agent swarms and complex orchestrators. The secret to overnight AI coding is a bash for-loop. I break down the Ralph Wiggum technique and show you how to actually ship working code with long-running agents.
A Guide to Context Engineering: Setting Agents Up for Success
Context beats prompts. Learn the systems that separate high-performing AI agents from the ones that hallucinate and fail—with real architecture patterns.