Most agent frameworks want you to understand their entire abstraction layer before you can run anything. Google's Agent Development Kit takes a different approach: define the agent, give it tools and run it. That's the core loop, and it stays that way whether the agent is a simple time-teller or a multi-agent orchestration system.
ADK was launched at Google Cloud NEXT 2025 as an open-source framework for designing, building, testing, evaluating, and deploying AI agents. It's the same framework powering agents inside Google products like Agentspace and the Google Customer Engagement Suite. ADK is available in Python, TypeScript, Go, Java, and Kotlin.
This article walks through building a simple agent - one that tells the current time for a given city - from environment setup to running it in a browser UI, in both Python and Java.
Core Concepts Before Writing Any Code
Every ADK agent is built from three things:
- Agent — the central object that defines the model being used, what the agent is supposed to do (via an instruction), and which tools it has access to.
- Tools — functions the agent can call when reasoning through a task. Without tools, an agent can only use its built-in language model knowledge. With tools, it can call APIs, query databases, run code, or anything else you write.
- Session — the context of a single conversation, including message history and working state. ADK manages this automatically; at the start, you don't need to think about it much.
Let's start building….
Step 1: Install ADK and Set Up a Virtual Environment
Python 3.10 or later is required.
python3 -m venv .venv
source .venv/bin/activate # macOS / Linux
# .venv\Scripts\activate.bat # Windows Command Prompt
pip install google-adk
Using a virtual environment keeps the ADK installation isolated from system Python packages. This matters more than it sounds once the project count grows.
Step 2: Create the Agent Project
ADK ships with a create command that scaffolds a project with the minimum required files.
adk create my_agent
This generates:
my_agent/
agent.py # the main agent definition
.env # for API keys
__init__.py
The only file ADK strictly requires is agent.py with a root_agent variable defined inside it. Everything else is structure.
Step 3: Get a Gemini API Key
ADK uses Gemini models by default. Get a free API key from Google AI Studio, then write it into the .env file:
echo 'GOOGLE_API_KEY="YOUR_API_KEY"' > my_agent/.env
ADK reads this file automatically at runtime. No manual os.environ setup is needed.
Step 4: Define the Agent with a Tool
Open my_agent/agent.py and replace the generated content with the following:
from google.adk.agents.llm_agent import Agent
def get_current_time(city: str) -> dict:
"""Returns the current time in a specified city."""
return {"status": "success", "city": city, "time": "10:30 AM"}
root_agent = Agent(
model="gemini-flash-latest",
name="root_agent",
description="Tells the current time in a specified city.",
instruction=(
"You are a helpful assistant that tells the current time in cities. "
"Use the 'get_current_time' tool for this purpose."
),
tools=[get_current_time],
)
A few things worth noting here:
- The
get_current_timefunction is a mock - it returns a hardcoded time. In a real system, this would call a time zone API. The point is that the agent decides when to call the tool and what arguments to pass to it, based on the user's message and the function's docstring. - The docstring on the function is not optional decoration. ADK uses it to inform the model what this tool does and when to use it. A vague or missing docstring produces unreliable tool invocation.
- The
instructionfield is the system prompt. It shapes the agent's behavior across the entire conversation.
Step 5: Run the Agent
Via CLI:
From the directory containing the my_agent folder, run:
adk run my_agent
This opens an interactive terminal session where you can type messages and see the agent respond. Type a city name and watch the agent decide to invoke get_current_time, pass the city as the argument, and format the response.
Via the browser UI:
adk web --port 8000
This starts a local web server at http://localhost:8000 with a full chat interface. Select the agent from the top-left dropdown and start chatting. The ADK web UI is a development tool - useful for debugging and observing tool calls step by step, but not intended for production deployment.
Java Walkthrough
The Java version of ADK follows the same conceptual model - agent, tools and session - just expressed through a builder pattern instead of Python's keyword arguments.
Step 1: Prerequisites
Java 17 or later and Maven 3.9 or later are required.
Step 2: Set Up the Project Structure
Create the directory structure manually:
mkdir -p my_agent/src/main/java/com/example/agent
touch my_agent/src/main/java/com/example/agent/HelloTimeAgent.java
touch my_agent/src/main/java/com/example/agent/AgentCliRunner.java
touch my_agent/pom.xml
touch my_agent/.env
The project structure:
my_agent/
src/main/java/com/example/agent/
HelloTimeAgent.java # agent definition
AgentCliRunner.java # CLI runner
pom.xml # Maven config and dependencies
.env # API key
Step 3: Define the Agent and Tool
Add the following to HelloTimeAgent.java:
package com.example.agent;
import com.google.adk.agents.BaseAgent;
import com.google.adk.agents.LlmAgent;
import com.google.adk.tools.Annotations.Schema;
import com.google.adk.tools.FunctionTool;
import java.util.Map;
public class HelloTimeAgent {
public static BaseAgent ROOT_AGENT = initAgent();
private static BaseAgent initAgent() {
return LlmAgent.builder()
.name("hello-time-agent")
.description("Tells the current time in a specified city")
.instruction("""
You are a helpful assistant that tells the current time in a city.
Use the 'getCurrentTime' tool for this purpose.
""")
.model("gemini-flash-latest")
.tools(FunctionTool.create(HelloTimeAgent.class, "getCurrentTime"))
.build();
}
@Schema(description = "Get the current time for a given city")
public static Map<String, String> getCurrentTime(
@Schema(name = "city", description = "Name of the city to get the time for") String city) {
return Map.of(
"city", city,
"time", "10:30 AM"
);
}
}
The @Schema annotations serve the same purpose as Python's docstrings - they describe the tool and its parameters to the model. FunctionTool.create(HelloTimeAgent.class, "getCurrentTime") registers the static method as a callable tool. ADK uses reflection to wire this up automatically.
Step 4: Configure Maven Dependencies
Add the following to pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example.agent</groupId>
<artifactId>adk-agents</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.google.adk</groupId>
<artifactId>google-adk</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>com.google.adk</groupId>
<artifactId>google-adk-dev</artifactId>
<version>1.4.0</version>
</dependency>
</dependencies>
</project>
Two dependencies: google-adk is the core runtime; google-adk-dev provides the web UI for local development.
Step 5: Set the API Key
echo 'export GOOGLE_API_KEY="YOUR_API_KEY"' > my_agent/.env
source my_agent/.env
Step 6: Create the CLI Runner
Add the following to AgentCliRunner.java. This class bootstraps a session and runs a simple read-print loop so messages can be sent to the agent from the terminal.
package com.example.agent;
import com.google.adk.agents.RunConfig;
import com.google.adk.events.Event;
import com.google.adk.runner.InMemoryRunner;
import com.google.adk.sessions.Session;
import com.google.genai.types.Content;
import com.google.genai.types.Part;
import io.reactivex.rxjava3.core.Flowable;
import java.util.Scanner;
import static java.nio.charset.StandardCharsets.UTF_8;
public class AgentCliRunner {
public static void main(String[] args) {
RunConfig runConfig = RunConfig.builder().build();
InMemoryRunner runner = new InMemoryRunner(HelloTimeAgent.ROOT_AGENT);
Session session = runner
.sessionService()
.createSession(runner.appName(), "user1234")
.blockingGet();
try (Scanner scanner = new Scanner(System.in, UTF_8)) {
while (true) {
System.out.print("\nYou > ");
String userInput = scanner.nextLine();
if ("quit".equalsIgnoreCase(userInput)) {
break;
}
Content userMsg = Content.fromParts(Part.fromText(userInput));
Flowable<Event> events = runner.runAsync(
session.userId(), session.id(), userMsg, runConfig
);
System.out.print("\nAgent > ");
events.blockingForEach(event -> {
if (event.finalResponse()) {
System.out.println(event.stringifyContent());
}
});
}
}
}
}
InMemoryRunner keeps session state in memory - fine for local development, not for production deployments that need state to survive across restarts. RunConfig is where streaming mode, safety settings, and other execution parameters get configured. The defaults work for a basic setup.
Step 7: Run the Agent
Via CLI:
cd my_agent
source .env
mvn compile exec:java -Dexec.mainClass="com.example.agent.AgentCliRunner"
Via the browser UI:
mvn compile exec:java \
-Dexec.mainClass="com.google.adk.web.AdkWebServer" \
-Dexec.args="--adk.agents.source-dir=target --server.port=8000"
Open http://localhost:8000, select the agent, and send a message.
What Happens at Runtime
When the agent receives "What time is it in Tokyo?", the following sequence runs:
- The model reads the instruction and the user message.
- It reasons that this requires calling
get_current_time(orgetCurrentTimein Java) withcity = "Tokyo". - ADK invokes the function and returns the result to the model.
- The model formats a natural-language response using the returned data.
- The final response is streamed back to the user.
None of this orchestration is written manually. The agent definition — model, instruction, and tools - is everything ADK needs to run that full loop.
Adding a Real Tool
Swapping the mock time function for a real one requires changing only the tool implementation, not the agent definition:
Python:
from datetime import datetime
import pytz
def get_current_time(city: str) -> dict:
"""Returns the current time in a specified city."""
city_timezones = {
"tokyo": "Asia/Tokyo",
"london": "Europe/London",
"new york": "America/New_York",
"mumbai": "Asia/Kolkata",
}
tz_name = city_timezones.get(city.lower())
if not tz_name:
return {"status": "error", "message": f"Timezone for {city} not found."}
tz = pytz.timezone(tz_name)
current_time = datetime.now(tz).strftime("%I:%M %p")
return {"status": "success", "city": city, "time": current_time}
Java:
@Schema(description = "Get the current time for a given city")
public static Map<String, String> getCurrentTime(
@Schema(name = "city", description = "Name of the city to get the time for") String city) {
Map<String, String> cityTimezones = Map.of(
"tokyo", "Asia/Tokyo",
"london", "Europe/London",
"new york", "America/New_York",
"mumbai", "Asia/Kolkata"
);
String tzName = cityTimezones.get(city.toLowerCase());
if (tzName == null) {
return Map.of("status", "error", "message", "Timezone for " + city + " not found.");
}
ZonedDateTime now = ZonedDateTime.now(ZoneId.of(tzName));
String time = now.format(DateTimeFormatter.ofPattern("hh:mm a"));
return Map.of("status", "success", "city", city, "time", time);
}
The agent definition stays identical. Tool implementation changes don't require touching the model, instruction, or session logic - which is the point of separating them.
ADK is built to let you start with a simple agent using prompts and tool calls, then grow into multi-agent orchestration, graph-based workflows, and deployment to enterprise-scale infrastructure without rewriting what you've already built. The agent built here - a single LLM with a single tool - is the same structural foundation used in multi-agent systems where an orchestrator delegates to specialized sub-agents across a workflow. The surface area expands; the core pattern does not.
Top comments (0)