Why architecture matters here

It is easy to demo an agent with a while-loop around a chat completion. It is very hard to run one in production, because the while-loop hides five architectural decisions that come due later. State: if conversation history and working state live in process memory, you cannot scale horizontally, resume after a crash, or audit a complaint. Composition: when one agent becomes five (triage, research, coding, review, summarizer), ad-hoc delegation becomes spaghetti; you need explicit parent-child semantics and transfer rules. Interception: legal wants PII redaction, security wants tool-argument validation, finance wants token budgets — if there is no principled hook layer these get hacked into prompts and tool bodies inconsistently. Observability: 'the agent did something weird' is only debuggable if every model request, tool call, and state change is a first-class recorded event. Portability: the runtime that works in a notebook must be the same runtime behind an HTTP endpoint, or your evals test the wrong thing.

ADK's architecture is a direct answer to those five. The Runner owns the turn lifecycle, so process topology is a deployment detail. The agent tree gives composition explicit structure — LLM agents delegate via transfer, workflow agents (sequential, parallel, loop) encode deterministic orchestration without burning tokens on orchestration-by-prompt. Callbacks form the interception lattice at every boundary: before/after agent, model, and tool. The event stream is the observability substrate. And because services (session, artifact, memory) are interfaces, swapping in-memory for database-backed implementations changes configuration, not code.

Understanding this architecture matters even if you use another framework: the Runner/event/session/callback decomposition is rapidly becoming the standard shape of production agent runtimes, the way MVC became the standard shape of web frameworks.

Advertisement

The architecture: every piece explained

Top row: the control plane of a turn. The Runner is the entry point — runner.run_async(user_id, session_id, new_message) — and owns the loop: append the user message to the session, invoke the root agent, stream resulting events to the caller, persist them as they are yielded. The agent tree is the program: LlmAgent nodes think with a model and can transfer control to named sub-agents; workflow agents — SequentialAgent, ParallelAgent, LoopAgent — run children in order, concurrently, or until an exit condition, with no model call spent on orchestration. The LLM flow is the request loop inside an LlmAgent: assemble instructions + history + tool declarations, call the model (Gemini natively; Anthropic, OpenAI, and others via LiteLLM), and interpret the response — plain text ends the step; function calls route to tools and loop back with results.

Middle row: the data plane. The event stream is the spine — every meaningful occurrence (user message, model text, function call, function response, state delta, control signal like transfer or escalate) is an immutable Event with an author, invocation id, and optional actions. The session service persists events and materializes session.state — a key-value store with scope prefixes (user:, app:, temp:) folded from state deltas; implementations range from in-memory (dev) to database and Vertex-managed (prod). Tool execution wraps plain Python/Java functions as function tools, plus built-ins (search, code execution) and MCP servers mounted as toolsets — the docstring/schema becomes the function declaration the model sees. Callbacks wrap every boundary: before_model can rewrite the request or short-circuit with a cached/blocked response; after_tool can validate or redact results; before_agent can enforce authorization.

Bottom rows: artifact service stores named binary blobs (files, images) referenced from events; memory service offers cross-session recall — from naive keyword search in dev to vector-backed managed memory in production. Deployment is the same runtime hosted three ways: local CLI/web for development, Cloud Run for self-managed serving, Agent Engine for fully managed sessions and scaling. The ops strip — evals on recorded sessions, OpenTelemetry tracing, guardrail callbacks, cost budgets — attaches to the same event stream everything else uses.

ADK runtime — Runner + event loop + sessions + tools + callbacksthe execution spine of every agentRunnerorchestrates turnsAgent treeLLM + workflow agentsLLM flowmodel request loopModel (Gemini/LiteLLM)generate + function callsEvent streamappend-only turn logSession servicestate + historyTool executionfunction tools + MCPCallbacksbefore/after hooksArtifact + memory servicesfiles + long-term recallDeploymentAgent Engine / Cloud Run / localOps — evals + tracing + guardrails + cost budgetsemit eventspersistcall toolsinterceptstorerecallshipoperateoperate
ADK runtime: the Runner drives the agent tree through an event loop; sessions, tools, callbacks, and services hang off it.
Advertisement

End-to-end flow

Trace one production turn. A support user types: 'refund order 88213, it arrived broken.' The API layer resolves user and session, then calls the Runner. The Runner appends the user-message event and invokes the root LlmAgent — a triage agent whose instruction names two sub-agents: refunds and shipping.

before_agent callbacks fire first: an authorization check confirms this user may discuss this order (loaded into temp: state by the API layer). The triage agent's LLM flow assembles the request — instructions, conversation history from the session, and the transfer tool declarations — and calls Gemini. The model replies with a transfer to refunds. That is an event with a control action; the Runner re-roots the invocation at the refunds agent.

The refunds agent declares two function tools: lookup_order(order_id) and issue_refund(order_id, amount, reason). Its model call returns a function call for lookup_order("88213"). before_tool validates the argument shape; the tool hits the order service; after_tool strips internal SKU cost fields before the result re-enters the context. The model now sees the order (₹4,200, delivered yesterday) and emits issue_refund(...). Policy lives in a before_tool callback here: refunds above a threshold set state['needs_approval'] via a state-delta event and short-circuit the tool with a 'queued for human approval' response instead of executing. The model summarizes honestly: 'I've queued a refund for approval; you'll hear back within an hour.'

Every step — transfer, both function calls, the short-circuit, state deltas — was yielded to the caller as it happened (streamed to the UI as SSE) and appended to the session service. The approval workflow later resumes the same session: a human approves, an operator message triggers the loop again, needs_approval is cleared, and issue_refund executes for real. Nothing about this flow changes between the developer's laptop (in-memory session service, adk web) and production (Agent Engine sessions, Cloud Trace spans per event) — which is precisely why recorded sessions double as eval fixtures: rerun the event log against a new prompt version and diff the trajectories before shipping.