·10 min read·AlgoMindset Team

Google ADK Tutorial: From Zero to a Deployed Agent

What ADK actually is

Google's Agent Development Kit is a Python framework for building agents, plus a runtime contract that Vertex AI Agent Engine knows how to host. Those are two separate things and it helps to keep them separate in your head: you can use ADK locally with no Google Cloud involvement at all, and the deployment story is a second decision layered on top.

The framework's opinion is that an agent is a model, a set of tools, an instruction, and a session. That maps almost exactly onto the loop from the first post in this series — which is the useful lens. ADK is not doing anything exotic; it is packaging the loop with session handling and a deployment target attached.

The smallest thing that works

An Agent needs a model, a name, an instruction and a list of tools. Tools are plain Python functions — ADK builds the JSON schema from the signature and the docstring, which is why both matter far more than they look like they do.

That docstring is not documentation. It is the tool description sent to the model on every turn, and the argument descriptions come from it too. A function with a one-line docstring gives the model almost nothing to select on.

python
from google.adk.agents import Agent

def get_order_status(order_id: str) -> dict:
    """Look up the current status of a single order.

    Use when the customer asks where an order is or whether it shipped.
    Do not use to list a customer's orders - call list_orders for that.

    Args:
        order_id: The order reference, e.g. 'ORD-10432'. Not an email.
    """
    row = db.orders.get(order_id)
    if row is None:
        return {"ok": False,
                "error": f"No order {order_id}.",
                "hint": "Check the reference, or call list_orders."}
    return {"ok": True, "status": row.status, "carrier": row.carrier}


root_agent = Agent(
    name="order_support",
    model="gemini-2.5-flash",
    instruction=SYSTEM_PROMPT,
    tools=[get_order_status],
)

Type hints are load-bearing

ADK derives the tool schema from your annotations, so an unannotated parameter becomes an untyped field the model has to guess at. Annotate everything, and prefer concrete types over permissive ones — a `str` the model must format correctly is worse than an enum it cannot get wrong.

Two things to avoid. Complex nested Pydantic models as parameters: they serialise into schemas large enough to crowd the context window, and models fill them inconsistently. And optional parameters with defaults that are actually required in practice — if the function breaks without it, mark it required and let the model see that.

Sessions are where the state lives

ADK separates the agent (stateless, reusable) from the session (the conversation and its state). Locally you get an in-memory session service, which is exactly what you want for development and exactly wrong for production, since a restart drops every conversation.

The important design point is that session state is not just message history. It is a place to stash resolved facts — the customer ID you looked up three turns ago, the order under discussion — so the agent stops re-deriving them. Every re-derivation is a tool call, a turn, and a chance to resolve to something different.

This is the same idea as the context budget: what occupies the window on each turn is a decision, not an accident.

python
from google.adk.sessions import InMemorySessionService
from google.adk.runners import Runner

session_service = InMemorySessionService()
runner = Runner(agent=root_agent, app_name="support",
                session_service=session_service)

session = await session_service.create_session(
    app_name="support", user_id="u_123",
    state={"customer_id": None, "locale": "en-IE"},
)

async for event in runner.run_async(user_id="u_123",
                                    session_id=session.id,
                                    new_message=message):
    if event.content:
        for part in event.content.parts:
            if part.text:
                print(part.text, end="")

Run it locally before you think about deployment

ADK ships a dev UI that is genuinely the fastest way to iterate: it shows each turn, every tool call with its arguments, and what the tool returned. Most early bugs are visible there in seconds and invisible from the final answer alone.

The specific thing to watch for is tool selection. Run twenty realistic inputs and check which tool gets called first each time. If the model is reaching for the wrong one, that is a docstring problem, and it is far cheaper to fix now than after you have built three more tools on top of it.

bash
# from the directory containing your agent package
adk web

# or headless, for scripted checks
adk run order_support

The packaging boundary that breaks deploys

Here is the part the quickstart does not prepare you for, and it is the single most common way an ADK deploy fails.

When you deploy to Agent Engine, your agent object is serialised locally and reconstructed inside a container built from a requirements list you supply. Two environments, two installs. If the library versions differ across that boundary, the object that comes out the other side is subtly not the object that went in — and the symptom is not a clear import error, it is an agent that returns nothing, or an unrelated-looking type error deep inside the framework.

The defence is to pin every framework package to the version actually installed in the environment doing the deploying, rather than a range. Ranges are fine for application code; across a serialisation boundary they are a trap. The next two posts cover the deployment path and that specific failure in detail.

python
import importlib.metadata as md

def pin(pkg: str, extras: str = "") -> str:
    """Pin to the version installed HERE - the environment doing the
    serialising - so the runtime container reconstructs the same object."""
    return f"{pkg}{extras}=={md.version(pkg)}"

REQUIREMENTS = [
    pin("google-adk"),
    pin("google-cloud-aiplatform", "[adk,agent-engines]"),
    "httpx>=0.27.0",          # ranges are fine for your own dependencies
]

What to take from this

ADK's real contribution is not the Agent class — you could write that yourself, and the first post in this series does. It is sessions with a pluggable backend, a dev UI that makes the loop visible, and a runtime that Agent Engine hosts without you writing a server.

The parts that will cost you time are the parts that look trivial: docstrings, because they are the tool descriptions; type hints, because they are the schema; and version pinning, because of the serialisation boundary. None of those appear in a twenty-line quickstart, and all three will find you in week two.