Google ADK for the Battle-Tested Agent Developer

A Comprehensive Evaluation of Google's Agent Development Kit for Experienced Developers

Rick Hightower

Originally published on Medium.

A Comprehensive Evaluation of Google's Agent Development Kit for Experienced Developers

Cover image: A battle-tested developer evaluating Google ADK alongside LangGraph and Claude Code agent frameworks Cover image: A battle-tested developer evaluating Google ADK alongside LangGraph and Claude Code agent frameworks

  • Part 1 (this article): Hands-on evaluation of ADK’s core abstractions, DX, and model flexibility
  • Part 2: Multi-agent patterns with SequentialAgent, ParallelAgent, and LoopAgent
  • Part 3: Production deployment, scaling, and real-world architecture

ADK’s four-layer architecture: Agent Layer (LlmAgent, SequentialAgent, ParallelAgent, LoopAgent), Tool Layer (FunctionTool, AgentTool, custom tools), Execution Layer (Runner, SessionService, ArtifactService), and Model Layer (Gemini native, LiteLLM bridge to Claude/GPT/Ollama). ADK’s four-layer architecture: Agent Layer (LlmAgent, SequentialAgent, ParallelAgent, LoopAgent), Tool Layer (FunctionTool, AgentTool, custom tools), Execution Layer (Runner, SessionService, ArtifactService), and Model Layer (Gemini native, LiteLLM bridge to Claude/GPT/Ollama).

pip install google-adk
my_agent/
    __init__.py      
# Marks this as a Python package
    agent.py         
# Your agent definition (ADK looks for root_agent here)
    .
env
             
# API keys (GOOGLE_API_KEY, etc.)
from
 google.adk.agents 
import
 LlmAgent
# This is the entry point ADK looks for when you run `adk web`
root_agent = LlmAgent(
    name=
"my_first_agent"
,
    model=
"gemini-3.1-flash"
,          
# Default model - fast and capable
    instruction=
"You are a helpful assistant that answers questions clearly."
,
    tools=[]                             
# We'll add tools shortly
)
<
dependency
>
    
<
groupId
>
com.google.adk
</
groupId
>
    
<
artifactId
>
google-adk
</
artifactId
>
    
<
version
>
0.9.0
</
version
>
</
dependency
>
<!-- Optional: adds the browser-based dev UI -->
<
dependency
>
    
<
groupId
>
com.google.adk
</
groupId
>
    
<
artifactId
>
google-adk-dev
</
artifactId
>
    
<
version
>
0.9.0
</
version
>
</
dependency
>
import
 com.google.adk.agents.LlmAgent;
// Builder pattern - standard Java idiom, nothing surprising here
LlmAgent
 
agent
 
=
 LlmAgent.builder()
    .name(
"my_first_agent"
)
    .description(
"A helpful assistant"
)
    .model(
"gemini-3.1-flash"
)
    .instruction(
"You are a helpful assistant that answers questions clearly."
)
    .build();
import
 com.
google
.
adk
.
tools
.
Annotations
.
Schema
;
public
 
class
 
WeatherTools
 {
    
// @Schema tells ADK how to describe this tool to the LLM
    
@Schema
(description = 
"Get weather information for a city"
)
    
public
 
static
 
Map
<
String
, 
String
> 
getWeather
(
        
@Schema
(description = 
"City name"
) 
String
 city
    
) {
        
return
 
Map
.
of
(
"city"
, city, 
"temperature"
, 
"72F"
, 
"condition"
, 
"Sunny"
);
    }
}
// Wire the annotated method into the agent as a callable tool
LlmAgent
 agent = 
LlmAgent
.
builder
()
    .
name
(
"weather_bot"
)
    .
tools
(
List
.
of
(
FunctionTool
.
create
(
WeatherTools
.
class
, 
"getWeather"
)))
    .
build
();
from
 google.adk.agents 
import
 LlmAgent
weather_agent = LlmAgent(
    name=
"weather_bot"
,                 
# Unique name for this agent
    model=
"gemini-2.5-flash"
,           
# Which LLM to use
    instruction=
"Help users check the weather. Use the get_weather tool "
                
"when asked about weather."
,
    tools=[get_weather],                
# List of callable tools
    output_key=
"last_weather_response"
  
# Store output in session state
)
def 
get_weather
(
city
: str) -> dict:
    
""
"Get weather information for a city.
Args:
        city: The name of the city to get weather for.
    Returns:
        Weather information dictionary.
    "
""
    
# In production, this would call a real weather API
    
return
 {
"city"
: city, 
"temperature"
: 
"72F"
, 
"condition"
: 
"Sunny"
}
def 
calculate_price
(
    
base_price
: 
float
,
    
quantity
: 
int
,
    
discount_percent
: 
float
 = 
0
) -> dict:
    
""
"Calculate total price with optional discount.
    Args:
        base_price: The base price per unit.
        quantity: Number of units to purchase.
        discount_percent: Optional discount percentage (0-100).
    Returns:
        Dictionary with calculation breakdown and total.
    "
""
    subtotal = base_price * quantity
    discount = subtotal * (discount_percent / 
100
)
    total = subtotal - discount
    
return
 {
"subtotal"
: subtotal, 
"discount"
: discount, 
"total"
: total}
  1. Type hints are required. Skip them and the LLM won’t know what types to pass. ADK won’t crash; it just generates a weaker tool schema.
  2. Docstrings drive the tool description. Write a bad docstring, get a bad tool. The Args: section is what the LLM reads to understand each parameter.
  3. Default values become optional parameters. discount_percent: float = 0 tells the LLM this parameter is optional.
# LangGraph approach - requires the 
@tool
 decorator
from langchain_core.tools import tool
@tool
def
 
get_weather
(
city:
 str
) -> 
dict:
    
""
"Get weather information for a city."
""
    
return
 {
"city"
: city, 
"temperature"
: 
"72F"
, 
"condition"
: 
"Sunny"
}
from
 google.adk.tools import ToolContext
def 
save_preference
(
preference
: str, 
tool_context
: ToolContext) -> dict:
    
""
"Save a user preference to session state.
    Args:
        preference: The preference to save.
        tool_context: Injected automatically by ADK - do not pass manually.
    Returns:
        Confirmation of saved preference.
    "
""
    
# ToolContext gives you access to session state, artifacts, and more
    tool_context.state[
"user_preference"
] = preference
    
return
 {
"status"
: 
"saved"
, 
"preference"
: preference}
from
 google.adk.agents 
import
 LlmAgent
from
 google.adk.sessions 
import
 InMemorySessionService
from
 google.adk.runners 
import
 Runner
from
 google.genai 
import
 types
# Step 1: Define the agent (you already know this part)
agent = LlmAgent(
    name=
"weather_bot"
,
    model=
"gemini-2.5-flash"
,
    instruction=
"Help users check the weather."
,
    tools=[get_weather]
)
# Step 2: Create the execution infrastructure
session_service = InMemorySessionService()   
# Stores session state in memory
runner = Runner(
    agent=agent,                             
# Which agent to run
    app_name=
"weather_app"
,                  
# Namespace for sessions
    session_service=session_service           
# Where to store state
)
# Step 3: Create a session (represents one conversation)
session = 
await
 session_service.create_session(
    app_name=
"weather_app"
,
    user_id=
"user_123"
)
# Step 4: Send a message and process the response stream
content = types.Content(
    role=
"user"
,
    parts=[types.Part.from_text(
"What's the weather in Austin?"
)]
)
async
 
for
 event 
in
 runner.run_async(
    user_id=
"user_123"
,
    session_id=session.
id
,
    new_message=content
):
    
if
 event.content 
and
 event.content.parts:
        
print
(event.content.parts[
0
].text)
# The development shortcut - one line instead of twenty
await
 agent.run_debug(message=
"What's the weather in Austin?"
)
from
 google.adk.agents 
import
 LlmAgent
from
 google.adk.sessions 
import
 InMemorySessionService
from
 google.adk.runners 
import
 Runner
from
 google.genai 
import
 types
import
 asyncio
# --- Tool Definitions ---
# Each tool is a plain Python function. ADK extracts the signature
# and docstring to generate the tool schema for the LLM.
def
 
get_weather
(
city: 
str
) -> 
dict
:
    
"""Get current weather for a city.
    Args:
        city: City name to check weather for.
    Returns:
        Weather data including temperature and conditions.
    """
    weather_data = {
        
"Austin"
: {
"temp"
: 
"85F"
, 
"condition"
: 
"Sunny"
, 
"humidity"
: 
"45%"
},
        
"Seattle"
: {
"temp"
: 
"58F"
, 
"condition"
: 
"Cloudy"
, 
"humidity"
: 
"72%"
},
        
"New York"
: {
"temp"
: 
"68F"
, 
"condition"
: 
"Partly Cloudy"
, 
"humidity"
: 
"55%"
},
    }
    
return
 weather_data.get(city, {
"temp"
: 
"N/A"
, 
"condition"
: 
"Unknown city"
})
def
 
calculate_price
(
    base_price: 
float
,
    quantity: 
int
,
    discount_percent: 
float
 = 
0
) -> 
dict
:
    
"""Calculate total price with optional discount.
    Args:
        base_price: Price per unit in dollars.
        quantity: Number of units.
        discount_percent: Discount percentage (0-100). Defaults to 0.
    Returns:
        Price breakdown with subtotal, discount amount, and total.
    """
    subtotal = base_price * quantity
    discount = subtotal * (discount_percent / 
100
)
    total = subtotal - discount
    
return
 {
        
"subtotal"
: 
f"$
{subtotal:
.2
f}
"
,
        
"discount"
: 
f"$
{discount:
.2
f}
"
,
        
"total"
: 
f"$
{total:
.2
f}
"
    }
def
 
check_inventory
(
product: 
str
) -> 
dict
:
    
"""Check inventory status for a product.
    Args:
        product: Product name to check.
    Returns:
        Inventory status including quantity available and restock date.
    """
    inventory = {
        
"widget_a"
: {
"available"
: 
150
, 
"restock"
: 
"N/A"
},
        
"widget_b"
: {
"available"
: 
0
, 
"restock"
: 
"2026-04-15"
},
        
"gadget_x"
: {
"available"
: 
42
, 
"restock"
: 
"N/A"
},
    }
    ret
  1. Three tools, one agent. The LLM decides which tool to call based on the user’s query. You don’t write routing logic.
  2. Session persistence. The session carries forward across all three queries, so the agent remembers context. In a raw LLM integration, you’d manage message history yourself.
  3. Plain Python functions. No decorators, no base classes, no registration calls. Write a function, add it to the tools list, done.
  4. Type hints matter. ADK uses them to generate the tool schema. Skip type hints and the LLM won’t know what types to pass.

DX Comparison showing the workflow differences between ADK, Claude Code, and LangGraph DX Comparison showing the workflow differences between ADK, Claude Code, and LangGraph

Google ADK for the Battle-Tested Agent Developer

Model-Agnostic Flow showing native Gemini path and LiteLLM bridge to other providers Model-Agnostic Flow showing native Gemini path and LiteLLM bridge to other providers

from
 google.adk.agents import Agent
from
 google.adk.models.lite_llm import LiteLlm
import os
# -------------------------------------------------------------------
# Environment variables
# -------------------------------------------------------------------
os.environ.
setdefault
(
"OPENAI_API_KEY"
, 
"sk-..."
)
os.environ.
setdefault
(
"ANTHROPIC_API_KEY"
, 
"sk-ant-..."
)
os.environ.
setdefault
(
"GOOGLE_API_KEY"
, 
"..."
)
os.environ.
setdefault
(
"GROQ_API_KEY"
, 
"..."
)
os.environ.
setdefault
(
"OLLAMA_BASE_URL"
, 
"http://localhost:11434"
)
# -------------------------------------------------------------------
# Example tools
# -------------------------------------------------------------------
def 
get_weather
(
location
: str) -> str:
    
return
 f
"Weather in {location} is sunny."
def 
calculate_price
(
x
: 
int
, 
y
: 
int
) -> 
int
:
    
return
 x * y
# -------------------------------------------------------------------
# Factory
# -------------------------------------------------------------------
def 
create_agent
(model_config, 
agent_name
: str) -> Agent:
    
return
 
Agent
(
        model=model_config,
        name=agent_name,
        instruction=
"You are a helpful assistant. Use tools when appropriate."
,
        tools=[get_weather, calculate_price],
    )
# -------------------------------------------------------------------
# Recommended 2026 model setup
# -------------------------------------------------------------------
# Gemini via native ADK model string
# Conservative / stable choice for ADK-native usage
gemini_agent = 
create_agent
(
    
"gemini-2.5-flash"
,
    
"gemini_agent"
,
)
# If you explicitly want Gemini 3.x preview behavior instead:
# gemini_agent = create_agent("gemini-3.1-flash-lite-preview", "gemini_agent")
# OpenAI via LiteLLM
# Likely current "small fast" choice; verify exact mapping in your LiteLLM version if needed
gpt_agent = 
create_agent
(
    
LiteLlm
(model=
"openai/gpt-5.4-mini"
),
    
"gpt_agent"
,
)
# Higher-cost / stronger option:
# gpt_agent = create_agent(LiteLlm(model="openai/gpt-5.4"), "gpt_agent")
# Anthropic via LiteLLM
claude_agent = 
create_agent
(
    
LiteLlm
(model=
"anthropic/claude-sonnet-4.6"
),
    
"claude_agent"
,
)
# Stronger option:
# claude_agent = create_agent(LiteLlm(model="anthropic/claude-opus-4.6"), "claude_agent")
# Groq via LiteLLM
groq_agent = 
create_agent
(
    
LiteLlm
(model=
"groq/llama-3.3-70b"
),
    
"groq_agent"
,
)
# Ollama via LiteLLM
ollama_agent = 
create_agent
(
    
LiteLlm
(model=
"ollama_chat/llama3.3"
),
    
"ollama_agent"
,
)
# Other local options:
# LiteLlm(model="ollama_chat/qwen3")
# LiteLlm(model="ollama_chat/mistral")
adk web

Google ADK for the Battle-Tested Agent Developer

  • You’re a Java shop and need first-class agent infrastructure

  • You want built-in multi-agent primitives without building your own orchestration

  • You’re already in the Google ecosystem (Vertex AI, GCP, Gemini)

  • You value explicit, code-first agent definitions over graph-based or conversational approaches

  • You need to mix models across agents in a multi-agent system

  • You need deep model flexibility as a core requirement (LangGraph wins here)

  • You prefer implicit orchestration with minimal boilerplate (Claude Agent SDK wins here or LangChain Deep Agent if you want model agnostic with observability and maturity)

  • You need production observability out of the box (LangSmith + LangGraph wins here)

  • You’re building a single-model application with Claude (just use Claude Agent SDK directly)

  • What is Google ADK? A code-first agent framework from Google supporting Python and Java, with built-in multi-agent orchestration patterns.

  • What are the core abstractions? LlmAgent (the agent), FunctionTool (plain functions as tools), Runner/Session (execution and state management).

  • How does model flexibility work? Native Gemini support plus LiteLLM bridge for Claude, GPT, Groq, and Ollama models.

  • Is the Java SDK production-ready? At version 0.9.0 with builder patterns, RxJava3, and @Schema annotations, it's the most serious Java agent SDK available.

  • How does ADK compare to LangGraph? Less graph magic, more declarative agents. Simpler state management, less flexible routing.

  • How does ADK compare to Claude Code? More boilerplate, more control. Explicit tools vs. implicit orchestration.

  • What’s the biggest gotcha? The Runner/Session boilerplate for simple cases, and the ollama_chat/ prefix for Ollama models.

#Google ADK #Agent Development Kit #agent development #Google AI #multi-agent #framework evaluation