« 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

  1. What is Google ADK, precisely?
  2. The trust boundary in ADK: the model proposes, the Runner executes
  3. The LlmAgent: the reasoning unit
  4. FunctionTool: turning a Python function into a callable tool
  5. The Runner and the Event-driven execution model
  6. output_key: how state threads through an agent
  7. The key distinction: deterministic orchestration vs LLM-driven delegation
  8. SequentialAgent: pipelines that pass state
  9. ParallelAgent: independent fan-out and merge
  10. LoopAgent: repeat until escalation or max_iterations
  11. Multi-agent hierarchies: sub_agents and transfer
  12. Sessions and scoped state: the prefix system
  13. Memory: short-term state vs the long-term MemoryService
  14. Callbacks: the interception and guardrail layer
  15. Deployment: Vertex AI Agent Engine
  16. ADK vs LangGraph vs the OpenAI Agents SDK
  17. Common misconceptions
  18. Lab walkthrough
  19. Success criteria
  20. Interview Q&A
  21. 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:

  1. 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.
  2. Model-agnostic, Gemini-optimized. ADK can drive any model (Gemini, and via the LiteLlm wrapper, 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.
  3. A small, opinionated set of primitives. An LlmAgent (the reasoning unit), FunctionTool and friends (capabilities), a Runner that executes an agent and emits Events, a SessionService that 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 a BaseLlm, or a LiteLlm(...) wrapper for non-Google models). In the labs this is an injected pure Policy for 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 good description is 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 to session.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 — strstring, intinteger, floatnumber, boolboolean, listarray, dictobject, 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 a state_delta (the change this event made to session state) and control signals like escalate and transfer_to_agent.

The loop the Runner drives (this is the ADK execution model, and Lab 01 builds it):

  1. Call the model with the conversation so far.
  2. Emit a model-response Event.
  3. 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.
  4. If the model produced final text: apply the output_key state 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's sub_agents. At runtime, the coordinator's model reads each child's description and decides which one to hand control to (it emits a transfer_to_agent action). 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, or LoopAgent. These are BaseAgents whose run schedules their sub_agents in 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 with actions.escalate=True, ADK's "we're done, stop the loop" signal, typically set by an exit_loop FunctionTool or a critic sub-agent that judged the work good enough; or
  • max_iterations is 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.

PrefixScopeShared acrossUse for
(none)sessionjust this conversationthis chat's working memory
user:userall of one user's sessionsuser preferences, profile
app:appevery session of the appglobal config, feature flags
temp:temporarythis 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 the SessionService.
  • MemoryService is 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:

CallbackFiresA non-None return does
before_agent_callbackbefore the agent body runsskips the whole agent, using the returned content
after_agent_callbackafter the agent finishesreplaces the final output
before_model_callbackbefore each model callskips the model call, using the returned response
after_model_callbackafter each model responsereplaces the response
before_tool_callbackbefore each tool runsskips the tool, using the returned result
after_tool_callbackafter each tool returnsreplaces 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_callback that returns a cached response on a hash hit skips the paid model call entirely.
  • Guardrails (Phase 10): a before_tool_callback that 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_callback can redact PII from a result; an after_model_callback can 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 ADKLangGraph (Phase 18)OpenAI Agents SDK
Core abstractionLlmAgent + workflow agents + Runnera StateGraph of nodes and edgesAgent + Runner
Orchestrationworkflow agents (Sequential/Parallel/Loop) or LLM sub_agent transferyou draw the graph (nodes, conditional edges, cycles)handoffs between agents
State modelscoped session state (user:/app:/temp:) via SessionServicetyped channels with reducers, checkpointedconversation + context object
Determinism seamworkflow agents are code-definedthe graph topology is code-definedthe loop is built-in
Sweet spotGoogle Cloud / Gemini; managed Vertex deploymentmaximum control over arbitrary control flowtight OpenAI integration, minimal API
Interceptionsix before/after callbacksnode wrappers / graph structureguardrails (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/LoopAgent fix the order in code; only sub_agents + transfer let the model route. Confusing these is the fastest way to fail the ADK question.
  • "ParallelAgent branches 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. Writing user:tier is not the same as writing tier.
  • "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_key is cosmetic." It is the state-passing seam the entire workflow-agent composition depends on; without it a SequentialAgent's steps can't see each other's results.
  • "More sub_agents autonomy 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.

  1. Lab 01 — LlmAgent, FunctionTool & the Runner. Implement FunctionTool._derive_schema (annotations → JSON types, defaults → required), the ModelResponse validation, the LlmAgent tool-normalization, and the Runner.run loop that emits model_response/tool_call/tool_result/final Events and writes output_key to state. This is §3§6 made concrete.

  2. Lab 02 — Workflow Agents. Implement LlmAgent.run (leaf: policy → text → output_key write), CheckerAgent.run (escalate), and the three workflow agents. The subtle bits: ParallelAgent must snapshot the input so branches don't see each other; LoopAgent must stop immediately on escalation and honor max_iterations. This is §7§10.

  3. 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 the Runner.run callback 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 + a Runner that emits Events.
  • You can explain how a FunctionTool derives 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, and LoopAgent, 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 lab and solution (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 a Runner that 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 Runner drives. 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/