« Phase 22 README · Hitchhiker's · Deep Dive · Principal Deep Dive · Core Contributor · Staff Notes
Phase 22 Warmup — Google Agent Development Kit (ADK) From First Principles
Who this is for: someone who has built the agent primitives by hand (the ReAct loop, tool validation, scoped memory, guardrails) in earlier phases and now needs to reason about a production framework — Google's ADK — at the level an interviewer probes. By the end you will hold ADK's execution model, its signature distinction (deterministic workflow agents vs LLM-driven transfer), its scoped-state and callback systems, and where it fits against LangGraph and the OpenAI Agents SDK. Nothing here needs a GPU, an API key, or a network call — the labs inject the model as a pure policy so the mechanism is visible and deterministic.
Table of Contents
- What is Google ADK, precisely?
- The trust boundary in ADK: the model proposes, the Runner executes
- The LlmAgent: the reasoning unit
- FunctionTool: turning a Python function into a callable tool
- The Runner and the Event-driven execution model
- output_key: how state threads through an agent
- The key distinction: deterministic orchestration vs LLM-driven delegation
- SequentialAgent: pipelines that pass state
- ParallelAgent: independent fan-out and merge
- LoopAgent: repeat until escalation or max_iterations
- Multi-agent hierarchies: sub_agents and transfer
- Sessions and scoped state: the prefix system
- Memory: short-term state vs the long-term MemoryService
- Callbacks: the interception and guardrail layer
- Deployment: Vertex AI Agent Engine
- ADK vs LangGraph vs the OpenAI Agents SDK
- Common misconceptions
- Lab walkthrough
- Success criteria
- Interview Q&A
- References
1. What is Google ADK, precisely?
The Agent Development Kit (ADK) is Google's open-source, code-first framework for building, evaluating, and deploying AI agents. Strip the marketing and it is three commitments:
- Code-first. You define agents, tools, and orchestration in ordinary Python (or Java) — not in a low-code GUI or a YAML DAG. An agent is an object; a tool is a function; a pipeline is a composition of objects. This makes agents versionable, testable, and reviewable like any other code, which is exactly why it shows up in enterprise-platform JDs.
- Model-agnostic, Gemini-optimized. ADK can drive any model (Gemini, and via the
LiteLlmwrapper, OpenAI/Anthropic/others), but it is tuned for Gemini and Google Cloud: first-class function calling, built-in tools (Google Search, code execution), and one-command deployment to Vertex AI Agent Engine. If your shop is on Google Cloud, ADK is the path of least friction. - A small, opinionated set of primitives. An
LlmAgent(the reasoning unit),FunctionTooland friends (capabilities), aRunnerthat executes an agent and emits Events, aSessionServicethat holds scoped state, workflow agents for deterministic orchestration, and callbacks for interception. That is essentially the whole surface.
The mental model to carry: an ADK agent is an LLM in a loop with tools and scoped memory, driven
by a Runner that emits an Event stream — the same loop you built from scratch in Phase 01,
with production ergonomics (state services, workflow composition, callbacks, deployment) bolted
on. Everything below is an elaboration of that sentence.
Why "code-first" is a design stance, not a slogan. Because agents are unreliable (Phase 00's
0.95^n), the parts you can make deterministic and testable are the parts you should. Code-first orchestration means the structure around the fuzzy LLM reasoning is ordinary, reviewable, unit-testable code — which is the entire pitch of the workflow agents in §7.
2. The trust boundary in ADK: the model proposes, the Runner executes
The most important sentence from Phase 00 applies unchanged here: the model proposes; the application executes. In ADK the boundary is concrete and named:
- The
LlmAgent's model emits text. When it "calls a tool", it physically emits a structured function call — a name plus arguments — which is untrusted text produced by a stochastic process. - The
Runner(and the tool's own validation) is the trusted executor. It parses the proposed call, checks it against the tool's schema, runs it, and feeds the result back. Nothing the model says does anything until your code, on the trusted side, honors it.
This is why ADK's callbacks (§14) are a
security feature, not just an observability one: a before_tool_callback sits exactly on that
boundary and can refuse a proposed call before it executes. And it is why the labs inject the
model as a pure Policy: the loop — propose, validate, execute, observe, finalize — is correct
regardless of whether the model is right, which is the whole discipline of production agent
engineering.
UNTRUSTED (the LlmAgent's model) TRUSTED (your ADK code)
┌────────────────────────────────┐ ┌─────────────────────────────────────┐
│ emits a function call: │ │ Runner: parse → FunctionTool.run │
│ get_weather(city="Paris") │ ──► │ validate args vs derived schema │ ──► real
│ ...or final text │ │ before_tool_callback? (guardrail) │ action
└────────────────────────────────┘ │ execute → Event(tool_result) │
a PROPOSAL └─────────────────────────────────────┘
3. The LlmAgent: the reasoning unit
LlmAgent (exported under the alias Agent) is the core building block. Its fields are the whole
story of how one agent behaves:
name— a unique identifier. It matters beyond logging: in a multi-agent hierarchy, a parent transfers control to a child by name (§11).model— which LLM drives it ("gemini-2.0-flash", or aBaseLlm, or aLiteLlm(...)wrapper for non-Google models). In the labs this is an injected purePolicyfor determinism.instruction— the system prompt / role: what this agent is for and how it should behave. It can reference state with templating so per-user or per-app values flow into the prompt.description— a one-line summary of the agent's capability. This is not cosmetic: it is the text other agents read to decide whether to delegate to this one. A gooddescriptionis the difference between correct and broken LLM-driven routing.tools— the capabilities the agent may use (see §4).output_key— if set, the agent's final text is written tosession.state[output_key](see §6).sub_agents— child agents forming a hierarchy the model can delegate to.
The behavior is the ReAct loop: given the instruction plus the conversation and any tool results
so far, the model either requests tool calls or produces a final response. The Runner
turns that into an Event stream. In Lab 01 you build exactly this: an LlmAgent, an injected
model policy that returns a ModelResponse (tool calls or text), and the loop that executes it.
4. FunctionTool: turning a Python function into a callable tool
A FunctionTool wraps a plain Python function so the model can call it. The mechanism worth
internalizing is automatic schema generation: ADK reads the function's signature and
docstring to build the function declaration the model sees. Concretely, for
def get_weather(city: str, unit: str = "celsius") -> str:
"""Return the current weather for a city."""
...
ADK derives a declaration roughly like:
{"name": "get_weather",
"description": "Return the current weather for a city.",
"parameters": {"type": "object",
"properties": {"city": {"type": "string"}, "unit": {"type": "string"}},
"required": ["city"]}}
Three rules do the work, and Lab 01 implements all three: each parameter becomes a typed property
(mapping Python annotations — str→string, int→integer, float→number, bool→boolean,
list→array, dict→object, unknown→string); a parameter with no default is
required; the docstring's first line becomes the description. This is why idiomatic ADK code
uses typed, docstringed tool functions — the quality of the auto-derived schema is the
quality of the model's tool use.
The other half of a tool is execution with validation. When the model proposes
get_weather(city="Paris"), the tool validates the arguments against the schema (missing-required
or unknown-argument becomes a structured error, not a raw TypeError crash) and only then calls
the function. That validation sits on the trust boundary from §2.
Beyond FunctionTool, ADK offers agent-as-a-tool (call a whole sub-agent like a function),
built-in tools (Google Search, code execution), MCP tools (Phase 03 — connect to any MCP
server's tools), and OpenAPI tools (generate tools from an API spec). All present the same
"declaration + execute" contract to the model.
5. The Runner and the Event-driven execution model
The Runner is the engine. You hand it an agent and a SessionService, then call
runner.run(user_id, session_id, new_message), and it yields a stream of Event objects. An
Event is ADK's unit of "what happened", and it is the single most important observability
primitive in the framework. Each Event carries:
- an
author(which agent or tool produced it), content(which may hold model text, a function call, or a function response),actions, including astate_delta(the change this event made to session state) and control signals likeescalateandtransfer_to_agent.
The loop the Runner drives (this is the ADK execution model, and Lab 01 builds it):
- Call the model with the conversation so far.
- Emit a model-response Event.
- If the model requested tools: emit a tool-call Event, run each tool, emit a tool-result Event, append results to the conversation, and loop back to the model.
- If the model produced final text: apply the
output_keystate delta, emit a final Event (event.is_final_response()is true), and stop.
A max_steps-style guard bounds the loop so a model that never finalizes cannot spin forever —
the reliable step budget from Phase 00, enforced by the framework. Consumers typically iterate the
stream and pull the answer out with event.is_final_response(). Because the Runner streams,
you can show intermediate reasoning, tool activity, and partial output in a UI — and you can log
every Event for tracing (Phase 14).
6. output_key: how state threads through an agent
output_key is small but load-bearing. If an LlmAgent sets output_key="summary", then when it
finishes, the Runner writes its final response into session.state["summary"]. That is the seam by
which one agent's output becomes another agent's input.
Why it matters: it is the mechanism that makes the workflow agents (§8–§10)
compose. A SequentialAgent of [writer(output_key="draft"), reviewer] works because the writer's
final text lands in state["draft"], and the reviewer's instruction can read {draft} from state.
No glue code, no manual passing — the state dict is the shared bus, and output_key is how you
publish to it. In Lab 01 you implement the final-event state delta; in Lab 02 you rely on it for
pipeline data flow; in Lab 03 you see it write into scoped state.
7. The key distinction: deterministic orchestration vs LLM-driven delegation
This is the section an interviewer is really testing when they ask about ADK. There are two fundamentally different ways to coordinate multiple agents, and knowing when to use each is the Staff-level judgment:
-
LLM-driven delegation — put children in an
LlmAgent'ssub_agents. At runtime, the coordinator's model reads each child'sdescriptionand decides which one to hand control to (it emits atransfer_to_agentaction). The routing is flexible (it adapts to the input) but non-deterministic (the model chooses, and it can choose wrong). -
Deterministic orchestration — use a workflow agent:
SequentialAgent,ParallelAgent, orLoopAgent. These areBaseAgents whoserunschedules theirsub_agentsin a guaranteed structure fixed in code. No LLM decides the order. The flow is predictable, testable, and replayable.
The ADK philosophy in one line: deterministic structure around LLM reasoning. You put
predictable control flow (the workflow agents) around unpredictable-but-powerful leaves (the
LlmAgents), and you get reliability and capability. This connects straight back to Phase 00:
reliability compounds, so anything you can make deterministic, you should — an LLM deciding "run A
then B then C" is a coin flip you didn't need to take when the order was known in advance.
The rule of thumb: if the flow is knowable, encode it as a workflow agent; reserve LLM-driven
transfer for genuinely runtime routing (e.g. a support triage agent that must read a message to
decide whether it's billing, technical, or sales). Lab 02 builds the deterministic side; the
sub_agents transfer side is §11.
8. SequentialAgent: pipelines that pass state
A SequentialAgent runs its sub_agents in listed order, threading the same session
state through each. Because state is shared, step 2 sees whatever step 1 wrote via output_key.
This is the ADK pipeline pattern.
The canonical example is a coding pipeline: SequentialAgent([write_code(output_key="code"), review_code(output_key="review"), refactor_code]). The reviewer's instruction reads {code} from
state; the refactorer reads {code} and {review}. Each leaf is an LlmAgent doing fuzzy
reasoning; the order and data flow are deterministic code. Lab 02's SequentialAgent is a few
lines — for sub in self.sub_agents: yield from sub.run(ctx) — and the whole lesson is that the
shared ctx.state is what makes output_key propagate. A test asserts agent-2 literally reads
agent-1's output.
9. ParallelAgent: independent fan-out and merge
A ParallelAgent runs its sub_agents concurrently against the same input, then merges
their outputs. The classic use is fan-out research: three sub-agents gather from three sources at
once, each writing a distinct output_key, and a later step reads all three.
The critical property — and a favorite interview trap — is that parallel branches must be independent: because they run concurrently with no ordering guarantee, one branch must not depend on another branch's output. In real ADK the branches share the session but run in isolated branches of the invocation; the safe pattern is "each writes its own key, a downstream step combines them." Lab 02 models the concurrency deterministically (run each branch against the same input snapshot, then merge the resulting state deltas) so the semantics are identical and the test is reproducible — and one test proves that neither branch sees the other's write in its input. That is not a simplification of the contract; it is the contract made testable.
Why fan out at all? Latency (Phase 00): independent tool calls done in parallel turn a sum of
step latencies into a max. ParallelAgent is the framework's answer to the ReWOO/plan-execute
observation that independent evidence-gathering should not be serialized.
10. LoopAgent: repeat until escalation or max_iterations
A LoopAgent runs its sub_agents repeatedly until one of two things happens:
- a sub-agent signals
escalate— it yields an Event withactions.escalate=True, ADK's "we're done, stop the loop" signal, typically set by anexit_loopFunctionToolor a critic sub-agent that judged the work good enough; or max_iterationsis reached — the safety bound so a loop that never escalates still terminates.
This is the iterative-refinement / critic loop: LoopAgent([refiner, quality_checker]) runs
refine → check, refine → check, … until the checker escalates or you hit the cap. It is the
framework expression of the "verify instead of trusting" reliability lever from Phase 00 — a
generate/critique/refine loop that self-terminates when a bar is met. In Lab 02 you implement the
termination logic precisely: escalation stops the loop immediately (a later sub-agent in that
same iteration does not run), and max_iterations bounds a non-escalating loop. Both are tested,
including the off-by-one an interviewer probes ("does the escalating agent's own event still get
yielded?" — yes; "does the next sub-agent run?" — no).
11. Multi-agent hierarchies: sub_agents and transfer
Set sub_agents=[...] on an LlmAgent and you have a hierarchy with LLM-driven
delegation. The parent (a "coordinator" or "dispatcher") is given the children's descriptions,
and at runtime its model decides whether to answer directly or transfer control to a child by
emitting a transfer_to_agent action naming the child. Control can transfer down (parent → child)
and, in some designs, back up. This is how you build a support agent that routes to a billing
specialist, or a "manager" that dispatches to domain experts.
Contrast with §7:
here the model is the router, so the routing is adaptive but non-deterministic and only as good
as the children's descriptions and the model's judgment. Real systems combine both patterns: an
LLM coordinator at the top where runtime routing is genuinely needed, with deterministic workflow
agents underneath wherever the flow is known. The interview-ready summary: sub_agents +
transfer = the LLM picks the path; workflow agents = the code fixes the path.
12. Sessions and scoped state: the prefix system
A Session is one conversation; a SessionService (InMemory / Database / VertexAI) owns
sessions and their state. State is a dict — the agent's working memory across turns — but with
a twist that is pure Phase 04 context-engineering: scope prefixes decide how widely a key is
shared.
| Prefix | Scope | Shared across | Use for |
|---|---|---|---|
| (none) | session | just this conversation | this chat's working memory |
user: | user | all of one user's sessions | user preferences, profile |
app: | app | every session of the app | global config, feature flags |
temp: | temporary | this turn only (not persisted) | scratch values within one invocation |
The elegance: session.state is a single dict-like view that routes each read and write to the
right backing store by prefix. Write state["user:tier"] = "gold" in one conversation and it is
visible in every other session that same user opens — because both sessions' State views point
at the same shared user store. Write an unprefixed key and it stays private to the session. In
Lab 03 you build exactly this: a State MutableMapping that dispatches on prefix, and an
InMemorySessionService that holds the three tiers and hands each session a correctly-wired view.
Tests prove user: sharing across a user's sessions, app: sharing across all users, and that
neither leaks across the boundary (different user, different app).
The production payoff: you swap InMemorySessionService for DatabaseSessionService or
VertexAiSessionService and the same scoping semantics now persist to a real store — you did not
write any of the tiering logic in your agent, the framework did.
13. Memory: short-term state vs the long-term MemoryService
ADK separates two kinds of "memory", and conflating them is a common mistake:
- Session state (§12) is short-term:
the working memory of the current (and, via
user:/app:, related) conversations. It lives in theSessionService. MemoryServiceis long-term: a searchable store of knowledge across many past sessions — "what did this user tell me last week?" You ingest completed sessions into it and retrieve relevant snippets into a new conversation's context (typically via a memory tool). This is the retrieval story from Phases 05–06 applied to agent history.
The distinction maps onto human memory: state is what you're holding in your head right now;
MemoryService is what you can recall by searching your notes. In-memory implementations exist for
both for local dev; production uses Vertex AI's managed services. Lab 03 focuses on the short-term
scoped state (the mechanism you must be able to build); MemoryService is an extension.
14. Callbacks: the interception and guardrail layer
Callbacks are how you inject your own logic into an agent's lifecycle without editing the
framework. ADK fires six, in paired before/after form, around three step types:
| Callback | Fires | A non-None return does |
|---|---|---|
before_agent_callback | before the agent body runs | skips the whole agent, using the returned content |
after_agent_callback | after the agent finishes | replaces the final output |
before_model_callback | before each model call | skips the model call, using the returned response |
after_model_callback | after each model response | replaces the response |
before_tool_callback | before each tool runs | skips the tool, using the returned result |
after_tool_callback | after each tool returns | replaces the tool result |
The load-bearing rule — memorize this — is the short-circuit contract: a before_*
callback that returns a value skips the wrapped step and uses that value as its result;
returning None means "proceed normally". This one rule turns callbacks into the framework's
interception and control layer:
- Caching (Phase 14): a
before_model_callbackthat returns a cached response on a hash hit skips the paid model call entirely. - Guardrails (Phase 10): a
before_tool_callbackthat inspects the proposed arguments and returns a rejection blocks a dangerous tool before it executes — the tool function never runs. This is the callback sitting exactly on the trust boundary from §2. - Transformation: an
after_tool_callbackcan redact PII from a result; anafter_model_callbackcan reshape or filter model output. - Observability: any callback can log/emit metrics as a side effect and return
None.
Lab 03 builds all six into a CallbackAgent + Runner and tests the semantics precisely: the
callbacks fire in order, a before_model cache hit means the model is called zero times, a
before_tool block means the dangerous function ran zero times, and after_* transforms
change the output. Being able to say "a blocked tool provably never executed" is the difference
between a guardrail and a hope.
15. Deployment: Vertex AI Agent Engine
ADK's deployment story is what makes it attractive to Google-Cloud shops. You develop locally with
a Runner and an in-memory SessionService; to go to production you deploy to Vertex AI Agent
Engine, a managed runtime that provides: managed, persistent sessions (swap
InMemorySessionService for the Vertex one, same code), autoscaling, integrated tracing and
monitoring (the Event stream becomes observable spans — Phase 14), and identity/security via
Google Cloud IAM. You can also containerize an ADK agent and run it on Cloud Run or GKE if you
want more control. ADK ships a local dev UI and a CLI (adk web, adk run) for iterating on the
Event stream before you ship.
The interview point: the same agent code runs locally and in production; deployment swaps the
services (session, memory) for managed ones. That portability — dev/prod parity by swapping a
SessionService implementation — is a direct consequence of the code-first, service-oriented
design from §1.
16. ADK vs LangGraph vs the OpenAI Agents SDK
All three frameworks implement the same underlying loop (while not done: proposal = model(context); execute); they differ in how you express orchestration and state.
| Google ADK | LangGraph (Phase 18) | OpenAI Agents SDK | |
|---|---|---|---|
| Core abstraction | LlmAgent + workflow agents + Runner | a StateGraph of nodes and edges | Agent + Runner |
| Orchestration | workflow agents (Sequential/Parallel/Loop) or LLM sub_agent transfer | you draw the graph (nodes, conditional edges, cycles) | handoffs between agents |
| State model | scoped session state (user:/app:/temp:) via SessionService | typed channels with reducers, checkpointed | conversation + context object |
| Determinism seam | workflow agents are code-defined | the graph topology is code-defined | the loop is built-in |
| Sweet spot | Google Cloud / Gemini; managed Vertex deployment | maximum control over arbitrary control flow | tight OpenAI integration, minimal API |
| Interception | six before/after callbacks | node wrappers / graph structure | guardrails (input/output) |
When to choose ADK: you are on Google Cloud / Gemini, you want first-class Vertex AI
Agent Engine deployment (managed sessions, scaling, tracing), and you like the opinionated
split between deterministic workflow agents and LLM leaves. LangGraph wins when you need to
express a complex, arbitrary control-flow graph (fan-out/fan-in with custom reducers, intricate
cycles, fine-grained checkpointing/resume) — it hands you the graph directly. OpenAI Agents SDK
wins when you're OpenAI-centric and want the smallest possible API (Agent + Runner + handoffs).
The deeper point, and the reason these labs build miniatures instead of teaching the API: the ideas — an agent as (model + instruction + tools), a runner that emits events, deterministic orchestration around fuzzy leaves, scoped state, interception callbacks — are portable across all three. Master the mechanism and you can pick up any of them in a day; memorize one API and you're stuck when the JD names a different one.
17. Common misconceptions
- "ADK is Gemini-only." No — it is model-agnostic (Gemini natively, others via
LiteLlm). It is optimized for Gemini/Vertex, which is different from locked to it. - "Workflow agents use an LLM to decide the order." The opposite — that is their whole point.
SequentialAgent/ParallelAgent/LoopAgentfix the order in code; onlysub_agents+ transfer let the model route. Confusing these is the fastest way to fail the ADK question. - "
ParallelAgentbranches can read each other's output." No — they run concurrently with no ordering guarantee; branches must be independent, and you combine their outputs in a downstream step. Depending on a sibling's write is a race. - "State is just one dictionary." It is a scoped view —
user:/app:/temp:route to different sharing tiers. Writinguser:tieris not the same as writingtier. - "Callbacks are only for logging." The
before_*short-circuit makes them the control layer: caches (skip the model) and guardrails (block the tool) live here. Returning a value is an action, not a note. - "
output_keyis cosmetic." It is the state-passing seam the entire workflow-agent composition depends on; without it aSequentialAgent's steps can't see each other's results. - "More
sub_agentsautonomy is more advanced." Seniority is preferring the deterministic workflow agent whenever the flow is known (Phase 00) — reliability compounds; don't spend it on a routing decision you could have hard-coded.
18. Lab walkthrough
Do the three labs in order — they mirror this warmup and build on each other.
-
Lab 01 — LlmAgent, FunctionTool & the Runner. Implement
FunctionTool._derive_schema(annotations → JSON types, defaults →required), theModelResponsevalidation, theLlmAgenttool-normalization, and theRunner.runloop that emitsmodel_response/tool_call/tool_result/finalEvents and writesoutput_keyto state. This is §3–§6 made concrete. -
Lab 02 — Workflow Agents. Implement
LlmAgent.run(leaf: policy → text →output_keywrite),CheckerAgent.run(escalate), and the three workflow agents. The subtle bits:ParallelAgentmust snapshot the input so branches don't see each other;LoopAgentmust stop immediately on escalation and honormax_iterations. This is §7–§10. -
Lab 03 — Sessions, Scoped State & Callbacks. Implement
State._store(prefix routing),InMemorySessionService.create_session(allocate id, seed state through the router, wire the shared stores), and theRunner.runcallback chain with its short-circuits. This is §12 and §14.
For each: run LAB_MODULE=solution pytest test_lab.py -v first to see green, then make your
lab.py match, then read solution.py's main() output — it is this warmup in running code.
19. Success criteria
-
You can describe ADK in three sentences: code-first, model-agnostic/Gemini-optimized,
LlmAgent+ tools + aRunnerthat emits Events. -
You can explain how a
FunctionToolderives its schema from a signature and why typed, docstringed functions matter. - You can state the deterministic-orchestration vs LLM-driven-delegation distinction and give a rule for choosing.
-
You can give the exact semantics of
SequentialAgent,ParallelAgent, andLoopAgent, including how a loop terminates. -
You can explain the
user:/app:/temp:state scopes and what shares with what. -
You can explain the callback
before_*short-circuit and give a cache and a guardrail example. - You can compare ADK to LangGraph and the OpenAI Agents SDK and say when you'd pick ADK.
-
All three labs pass under both
labandsolution(24 + 19 + 25 = 68 tests).
20. Interview Q&A
Q: What is Google ADK and what makes it different from just calling Gemini? A: ADK is
Google's open-source, code-first agent framework: model-agnostic but Gemini/Vertex-optimized. A
raw Gemini call is prompt-in/text-out, once. ADK gives you the loop — an LlmAgent (instruction
- tools +
output_key) driven by aRunnerthat emits an Event stream, with scoped session state, deterministic workflow orchestration, callbacks for guardrails, and one-command deployment to Vertex AI Agent Engine. It packages the primitives you'd otherwise build by hand.
Q: What is the single most important distinction in ADK's design? A: Deterministic
orchestration vs LLM-driven delegation. The workflow agents (SequentialAgent, ParallelAgent,
LoopAgent) fix the control flow in code — the order is guaranteed, not chosen by a model.
sub_agents + transfer_to_agent let the coordinator's model route at runtime. The philosophy is
"deterministic structure around LLM reasoning": reliability compounds, so you make the structure
deterministic and reserve LLM routing for genuinely runtime decisions.
Q: Walk me through what runner.run(agent, ...) actually does. A: It drives the agent loop
and yields Events. It calls the model with the conversation; if the model requests tools, it
emits tool-call Events, validates and executes each tool (the trust boundary), emits tool-result
Events, and loops back with the observations; if the model returns final text, it applies the
output_key state delta and emits a final Event (is_final_response() true). A step guard bounds
the loop. Consumers iterate the stream and pull the answer from the final Event.
Q: How does SequentialAgent pass data between steps? A: Through shared session state via
output_key. All sub-agents run over the same session, so when step 1 sets output_key="draft",
its final text lands in state["draft"], and step 2's instruction reads {draft}. There's no
manual passing — the state dict is the bus. That's also why the labs make output_key the
through-line: it's the composition seam.
Q: Why must ParallelAgent branches be independent? A: Because they run concurrently with no
ordering guarantee — a branch can't reliably read another branch's write, that's a race. The safe
pattern is each branch writes its own output_key and a downstream step combines them. You fan out
for latency (independent tool calls become a max, not a sum) and fan in deterministically.
Q: How does a LoopAgent know when to stop? A: Two ways: a sub-agent yields an Event with
actions.escalate=True (set by an exit_loop tool or a critic sub-agent that judged the work good
enough), or max_iterations is reached. Escalation stops it immediately — a later sub-agent in
that iteration doesn't run — and max_iterations is the safety bound so a never-escalating loop
still terminates. It's the generate/critique/refine pattern with a self-termination condition.
Q: Explain ADK's state scopes. A: A session's state is a scoped view. Unprefixed keys are
private to the session; user:-prefixed keys are shared across all of that user's sessions (for
preferences); app:-prefixed keys are shared across every session of the app (global config);
temp: is scratch for the current turn only, never persisted. The State object routes each
read/write to the right backing store by prefix, so session.state["user:tier"] reads the shared
user store while session.state["draft"] reads the private session store.
Q: How would you implement a guardrail that blocks a dangerous tool in ADK? A: A
before_tool_callback. It receives the tool name and proposed arguments; if it returns a value,
ADK skips the tool and uses that value as the result — so returning a rejection means the
dangerous function provably never runs. That callback sits exactly on the trust boundary. Same
mechanism gives you response caching via before_model_callback (return a cached response, skip
the paid model) and PII redaction via after_tool_callback (transform the result). The key
property is testable: assert the tool executed zero times.
Q: When would you choose ADK over LangGraph or the OpenAI Agents SDK? A: ADK when you're on
Google Cloud / Gemini and want managed Vertex AI Agent Engine deployment and the opinionated
workflow-agent model. LangGraph when you need an arbitrary, complex control-flow graph with custom
reducers and fine-grained checkpoint/resume — it hands you the graph directly. OpenAI Agents SDK
when you're OpenAI-centric and want the minimal Agent + Runner + handoffs API. The abstractions
are portable; I'd pick by ecosystem and how much orchestration control the task needs.
Q: Your ADK agent's Gemini bill is surprising. Where do you look? A: Same Phase 00 shape.
Check the Event stream for step count (is a LoopAgent not escalating, or a coordinator looping?),
observation sizes going into context, and whether a before_model_callback cache is in place for
repeated requests. If the flow is actually known, replace an LLM coordinator with a workflow agent
to remove per-run re-planning. Tracing via Agent Engine gives per-step token attribution.
21. References
- Google, ADK documentation — the primary source for agents, tools, runners, sessions, and callbacks. https://google.github.io/adk-docs/ and https://adk.dev/
- Google, ADK — Agents (LlmAgent, workflow agents, multi-agent). https://google.github.io/adk-docs/agents/
- Google, ADK — Tools (FunctionTool, built-in, MCP, OpenAPI, agent-as-a-tool). https://google.github.io/adk-docs/tools/
- Google, ADK — Sessions, State & Memory (SessionService, scope prefixes, MemoryService). https://google.github.io/adk-docs/sessions/
- Google, ADK — Callbacks (the six hooks and the short-circuit contract). https://google.github.io/adk-docs/callbacks/
- Google Cloud, Vertex AI Agent Engine — managed deployment/runtime for agents. https://cloud.google.com/vertex-ai/generative-ai/docs/agent-engine/overview
- Anthropic, Building Effective Agents (2024) — workflows vs agents; the "deterministic structure" argument that ADK's workflow agents embody. https://www.anthropic.com/research/building-effective-agents
- Yao et al., ReAct (2022) — the interleaved reason+act loop the
Runnerdrives. https://arxiv.org/abs/2210.03629 - LangGraph docs (Phase 18 — the graph-structured alternative). https://langchain-ai.github.io/langgraph/
- OpenAI Agents SDK docs (the
Runner/handoffs alternative). https://openai.github.io/openai-agents-python/