Google’s Agent Development Kit (ADK) is an open-source framework for building, evaluating, and deploying LLM agents as ordinary code rather than as a pile of prompt strings. It gives you a small set of composable primitives — agents that think, tools that act, sessions that remember, and a runtime that drives the loop — and a clear place for each concern in an agent’s life. The pitch is that the same objects you write in a notebook run unchanged behind a production endpoint, so the thing you tested is the thing you ship. This article is the hub: it introduces every core building block briefly and coherently, sketches how a single request flows through the whole system, and places ADK against LangGraph, CrewAI, and LlamaIndex. It stays broad on purpose — the runtime and tool deep-dives live in their own articles — so you leave with an accurate mental model of the pieces and how they fit, and know where to go next for depth.
What ADK actually is
ADK is a code-first agent framework: you describe an agent as a Python (or Java) object — a model, an instruction, a list of tools, and optionally some child agents — and ADK supplies the machinery that turns that description into a running conversation. It is not a low-code canvas and not a single monolithic ‘agent class’ with a hundred flags; it is a set of small primitives that compose. The headline objects are the Agent (usually an LlmAgent), Tool, Session and its State, the Runner, the Event stream, and a handful of pluggable services for sessions, memory, and artifacts.
What distinguishes ADK from a weekend while-loop around a chat completion is that every one of those concerns has an explicit home. History is not a Python list you hope survives a restart — it is a persisted session. Interception is not a special case buried in a tool body — it is a callback. Composition is not string-concatenated sub-prompts — it is a tree of agents with transfer semantics. That structure is what lets an agent grow from a demo into something you can scale, audit, and evaluate without a rewrite.
Why it exists: the gap between a demo and production
Anyone can wire an LLM to a couple of functions and get a convincing demo in an afternoon. The demo hides the hard part. Five problems come due the moment you try to run that agent for real, and ADK is a direct answer to each. State: if history lives in process memory you cannot scale horizontally, resume after a crash, or reconstruct what happened when a user complains. Composition: one agent becomes five — triage, research, coding, review, summarizer — and ad-hoc delegation turns to spaghetti. Interception: legal wants PII redaction, security wants argument validation, finance wants token budgets. Observability: ‘the agent did something weird’ is only debuggable if every model call, tool call, and state change is a recorded event. Portability: the runtime in your notebook must be the same runtime behind the HTTP endpoint, or your evaluations test the wrong thing.
ADK exists because these five recur in every serious agent, and solving them ad-hoc each time is wasted, error-prone work. The framework bakes in a principled answer so you spend your effort on the agent’s actual behavior, not on re-inventing its plumbing.
Code-first, in Python and Java
ADK’s central design choice is that an agent is code, and its capabilities are ordinary typed functions. A tool is just a Python function with a docstring; ADK reads the signature and docstring to build the schema the model sees. There is no separate DSL, no YAML graph to keep in sync with your logic, no visual editor that emits code you cannot read. The consequence is that everything you already know about writing, testing, and versioning software applies: unit tests, type checkers, code review, dependency management, and your IDE all work.
from google.adk.agents import LlmAgent
def get_weather(city: str) -> dict:
"""Return the current weather for a city.
Args:
city: Name of the city, e.g. 'Bengaluru'.
"""
return {"city": city, "temp_c": 29, "sky": "clear"}
root_agent = LlmAgent(
name="weather_agent",
model="gemini-2.0-flash",
instruction="Answer weather questions. Use get_weather for live data.",
tools=[get_weather],
)ADK ships first-class in both Python and Java, so a JVM shop is not forced onto Python to adopt it. The concepts — agents, tools, sessions, events, the runner — are the same across both; only the surface syntax differs, which matters for teams standardizing agent infrastructure across mixed stacks.
The core primitive: Agent and LlmAgent
The atom of ADK is the agent. The one you reach for most is the LlmAgent (often aliased Agent): a reasoning unit that wraps a model and decides, each turn, whether to answer in text, call a tool, or hand off to another agent. You configure it with a name (agents address each other by name), a model, an instruction that defines its role and policy, a list of tools, and optionally a list of sub_agents.
An LlmAgent is non-deterministic by nature: the model chooses the path. That flexibility is the point, but it is also why ADK pairs it with deterministic workflow agents for the parts of a pipeline that should not be left to a coin flip. The instruction is where most of an agent’s behavior actually lives — it names the tools’ purpose, states when to transfer to a sub-agent, and encodes the guardrails you want expressed in natural language. A well-scoped agent has a narrow instruction and a short tool list; when either grows unwieldy, that is the signal to split the work across several collaborating agents rather than overload one.
Composition: multi-agent systems and workflow agents
Real systems are rarely one agent. ADK models a team as a tree: a root agent with sub-agents, each of which may have its own. There are two ways control moves through that tree. An LlmAgent can transfer — the model decides, from the sub-agents’ names and descriptions, to hand the conversation to a specialist. That is dynamic routing driven by reasoning.
For the parts of a process that must be deterministic, ADK provides workflow agents that orchestrate children by structure, not by model call: SequentialAgent runs them in order (pipeline), ParallelAgent runs independent children concurrently (fan-out), and LoopAgent repeats children until an exit condition is met (iterate-until-good). Because these spend no tokens deciding what to do next, they are cheaper, faster, and reproducible — ideal for ‘research, then draft, then critique, then revise’ shaped work. The art of ADK design is mixing the two: use LLM agents where judgment is required and workflow agents where order is known, so the model’s freedom is spent only where it earns its keep. One agent can even be wrapped as a tool for another (AgentTool), giving you a third composition style on top of transfer and workflow.
Tools: how an agent acts on the world
An agent that can only talk is a chatbot; a tool is what lets it act. In ADK a tool is most often a plain typed function — ADK extracts the name, the description (from the docstring), and a JSON schema of the parameters (from the signature) to produce the declaration the model reads. The model selects tools by their descriptions and emits function calls; the runtime executes them, possibly several in parallel, and feeds the results back into the conversation. Errors are results too — a well-formed error the model can act on (‘service timed out, safe to retry’) is worth far more than a raw stack trace.
Beyond your own functions, ADK offers built-in tools (grounded search, code execution), MCP toolsets that mount external Model Context Protocol servers as native tools, and OpenAPI toolsets that compile a REST spec into one tool per operation. A special ToolContext gives tool code a side channel to read and write session state, load and save artifacts, and access auth — all invisible to the model’s schema. Tool design is deep enough to warrant its own article; here the point is that tools are the agent’s hands, and its real capability envelope is exactly the union of what they permit.
Sessions and State: the agent's memory of the conversation
A Session is one conversation thread: its ordered history of events plus a State dictionary of working values. It is scoped to a user and an app, and it is managed by a pluggable SessionService — in-memory for development, database-backed or Vertex-managed for production — so persistence is a configuration choice, not a code change. This is what lets an agent survive a process restart, scale across many workers, and be resumed hours later by a human-approval step.
State is a key-value store the agent reads and writes as it works, and its keys carry scope prefixes that control lifetime: a plain key belongs to this session, user: keys persist across all of one user’s sessions, app: keys are shared app-wide, and temp: keys live only for the current turn. State does not change by silent mutation; changes are recorded as state deltas carried on events, which is what keeps the history a faithful, replayable log. Distinguishing durable conversation state from throwaway scratch values via these prefixes is one of the small design decisions that keeps a growing agent maintainable.
The Runner and the Runtime
The Runner is the engine. You do not call an agent directly; you hand a user message to the Runner — conceptually runner.run_async(user_id, session_id, new_message) — and it owns the turn lifecycle: append the message to the session, invoke the root agent, drive the model-and-tool loop, and stream every resulting event back to you as it happens while persisting it to the session service. Because the Runner encapsulates the loop, where it runs — a notebook, a web server, a managed cloud service — becomes a deployment detail rather than a rewrite.
The Runtime is the broader execution environment the Runner operates in: the event loop, the wiring to the session, memory, and artifact services, and the callback hooks that fire at each boundary. The mental model to hold is a cooperative loop: the agent yields events (a piece of text, a function call, a state change), the runtime processes and persists each, and control passes back and forth until the turn completes. That streaming, event-yielding shape is what makes ADK agents observable and interruptible instead of opaque black boxes — the runtime deep-dive covers exactly how that loop is structured.
Events: the append-only spine of every turn
Everything meaningful that happens in ADK is an Event. A user message, a chunk of model text, a function call, a function response, a state delta, a control signal like a transfer or an escalation — each is an immutable Event with an author, an invocation id, and optional actions. Events are what the Runner streams to your application and what the session service stores, which means the event log is both the live UI feed and the permanent record.
This event-sourced design pays off in three ways that are easy to underrate up front. It makes agents observable — you can trace exactly which tool was called with which arguments and what came back. It makes them resumable — the state is a fold over the events, so a conversation can be reconstructed or continued from its log. And it makes them testable — a recorded session doubles as an evaluation fixture: re-run the same events against a new prompt or model version and diff the trajectories before you ship. When people say ADK is ‘production-shaped,’ the event stream is a large part of what they mean.
Callbacks: the interception layer for guardrails
Callbacks are ADK’s answer to every ‘before you do that, let me check something’ requirement. They are functions you register to fire at defined boundaries: before_agent and after_agent, before_model and after_model, before_tool and after_tool. Each can inspect what is about to happen, modify it, or short-circuit it entirely by returning a value in place of the real call.
This is where cross-cutting policy lives, kept out of your prompts and tool bodies. A before_model callback can strip PII from a request, enforce a token budget, or return a cached response without calling the model at all. A before_tool callback can validate arguments, check that this user is authorized for this action, or queue a sensitive operation for human approval instead of executing it. An after_tool callback can redact fields from a result before they re-enter the model’s context. Because the hooks are uniform and principled rather than special-cased, security, legal, and finance requirements become a consistent layer you can reason about — the difference between a guardrail and a hack scattered through the codebase.
Memory and Artifacts: recall beyond the current thread
A Session remembers one conversation; Memory is how an agent recalls things across many. The MemoryService is a pluggable store for long-term knowledge — past conversations, user facts, a knowledge base — that an agent can search when the current thread does not contain the answer. Implementations range from naive keyword search in development to vector-backed managed memory (such as Vertex AI’s) in production, and an agent typically reaches memory through a tool that queries it. The clean split is worth internalizing: session is short-term working memory for the active thread; memory service is the long-term store that outlives it.
Alongside memory, the ArtifactService handles binary and large payloads — files, images, generated documents — that have no business sitting inline in the conversation. A tool can save a 30 KB report as a named artifact and return a short reference, keeping the model’s context lean while the full payload stays retrievable. Together, memory and artifacts round out ADK’s storey of state: fast working state in the session, durable recall in memory, and heavy blobs in artifacts, each with its own service interface you can swap.
A mental model: how one request flows through the system
Tie the pieces together with a single turn. A user sends a message; your application resolves the user and session and hands the message to the Runner. The Runner appends it to the session as an event and invokes the root agent. Before the agent runs, before_agent callbacks fire — say, an authorization check. The agent’s LlmAgent assembles its instruction, the session history, and its tool declarations, and calls the model.
The model replies. If it is plain text, that becomes an event, streamed to the caller and persisted; the turn may end. If it is a function call, before_tool validates it, the runtime executes the tool, after_tool can sanitize the result, and the result re-enters the context as another event — the model then decides its next move. If it transfers, control re-roots at a sub-agent. Any state the tools wrote rides back as state deltas on those events; anything worth remembering long-term can be written to memory. Every hop is an event, streamed live and stored for replay. That single loop — Runner drives agent, agent calls model, model calls tools, everything emitted as events — is the whole system in miniature, and it runs identically on your laptop and in production.
Model-agnostic by design: Gemini and beyond
ADK is a Google framework and Gemini is its native, best-integrated model, but it is deliberately not Gemini-only. The agent’s model is a swappable field, and through a LiteLLM integration ADK can drive Anthropic’s Claude, OpenAI’s GPT models, and many others behind the same LlmAgent interface. You can also point at models served on Vertex AI’s model garden or self-hosted open-weight models via compatible endpoints. The agent’s logic — its tools, instruction, and place in the tree — does not change when you change the model.
This matters for two practical reasons. First, it avoids lock-in: you can start on Gemini for its price-performance and grounding, then route a specific sub-agent to a different model that happens to be better at, say, code or long-context reasoning. Second, it makes evaluation honest — because swapping the model is a one-line change, you can A/B two providers on the same recorded sessions and compare trajectories directly. The design keeps the orchestration (ADK’s job) cleanly separated from the reasoning engine (the model’s job), which is exactly the seam you want to be able to move.
How ADK compares to LangGraph, CrewAI, and LlamaIndex
ADK sits in a crowded field, and the contrasts sharpen what it is. LangGraph models an agent as an explicit state graph of nodes and edges — maximally flexible and precise about control flow, but you author the graph. ADK leans on the agent tree plus workflow agents for structure and lets the LLM handle dynamic routing via transfer, which is less graph-wiring for common shapes. CrewAI centers on a role-playing metaphor — agents with roles and goals collaborating — which is fast and intuitive for multi-agent brainstorming but less opinionated about the production concerns (persisted sessions, event sourcing, callbacks) that ADK treats as first-class. LlamaIndex grew from retrieval and remains the strongest at RAG and data-connector breadth; its agent layer is capable but secondary to its data-framework heritage, whereas ADK is agent-first and reaches data through tools and memory.
| Framework | Core metaphor | Sweet spot |
|---|---|---|
| ADK | Agent tree + workflow agents + event-sourced runtime | Production agents on Google Cloud, code-first teams |
| LangGraph | Explicit state graph | Precise, custom control flow you author by hand |
| CrewAI | Role-playing collaborators | Fast multi-agent prototyping |
| LlamaIndex | Data framework with agents on top | RAG-heavy, data-connector-rich apps |
None of these is strictly better; the honest chooser asks whether you want to author control flow explicitly (LangGraph), move fast on collaboration (CrewAI), build on retrieval (LlamaIndex), or want an opinionated, production-shaped runtime tightly integrated with Google Cloud (ADK).
Deployment: from your laptop to Agent Engine
ADK’s portability promise is concrete: the same agent object runs in three escalating environments without code changes. Locally, adk web gives you a browser dev UI and adk run a CLI, both backed by an in-memory session service — perfect for iterating and eyeballing the event stream. Cloud Run is the self-managed step: containerize the agent and serve it as an autoscaling HTTP endpoint, wiring in a database-backed session service so state survives restarts and scales across instances. Vertex AI Agent Engine is the fully managed target: it hosts the runtime, manages sessions and scaling for you, and integrates with managed memory and tracing — you deploy the agent and Google operates the infrastructure.
| Target | You manage | Use it for |
|---|---|---|
| Local (adk web / run) | Everything, in-memory | Development and evals |
| Cloud Run | Container + services | Self-managed serving, full control |
| Vertex AI Agent Engine | Just the agent | Managed sessions, memory, scaling |
The through-line is that deployment is a hosting decision layered on an unchanged runtime. Because the Runner and services are interfaces, moving from laptop to Cloud Run to Agent Engine swaps implementations behind the same code — which is exactly why the agent you evaluated is the agent you ship.