« Phase 22 README · Warmup · Hitchhiker's · Deep Dive · Core Contributor · Staff Notes
Phase 22 — Principal Deep Dive: Google Agent Development Kit (ADK)
The mechanism doc treats ADK as a fold over an Event stream. This doc treats it as a component in a production system on Google Cloud, and asks the questions a principal engineer owns: where does this run at scale, what breaks under load, what is the blast radius when a tenant's data leaks, and what does each design choice cost in dollars and milliseconds. ADK's one-sentence thesis — deterministic structure around LLM reasoning — is not a slogan; it is a bet about where to spend reliability, and the architecture is the shape of that bet.
Dev/prod parity as a first-class property
The single most important architectural decision in ADK is that the agent is decoupled from its
services through interfaces. An agent talks to a SessionService, a MemoryService, and a model
provider — never to a concrete store. This is the seam that makes dev/prod parity real rather than
aspirational:
- Local dev:
InMemorySessionService, an injected model policy,adk web/adk runto watch the Event stream. - Production:
VertexAiSessionService(managed, persistent, autoscaled), Gemini on Vertex, deployed to Vertex AI Agent Engine.
The agent code does not change. You swap the service implementation and the same orchestration, the same scoped-state semantics, the same callbacks run against managed infrastructure. The principal-level consequence: your test suite exercises the real control flow because the control flow lives in the agent, not the platform. Contrast the anti-pattern where orchestration lives in the deployment substrate — then your local tests prove nothing about production behavior. ADK puts the determinism where you can test it.
Scaling: turning a sum into a max, and killing per-run planning cost
Two performance levers dominate, and both come from choosing structure over LLM improvisation.
ParallelAgent turns a sum of latencies into a max. A research assistant that gathers from three
sources serially costs t₁ + t₂ + t₃. Run them as a ParallelAgent and the wall-clock cost is
max(t₁, t₂, t₃) plus a merge. With three ~2s retrievals, that is ~6s → ~2s — a 3× latency cut for
free, because the branches are independent (an antichain in the dependency DAG). This is the ReWOO
plan-then-parallel-execute insight, made a first-class agent. The moment a principal sees three
independent evidence-gathering steps chained sequentially, the review comment writes itself.
Deterministic workflow agents eliminate per-run re-planning. An LLM coordinator with
sub_agents pays for a model call every run just to decide "do A, then B, then C" — a decision
that was knowable at design time. That is tokens spent, latency added, and a non-deterministic
failure mode introduced, all to re-derive a fixed plan. A SequentialAgent encodes the plan once,
in code, and pays zero model calls to route. Across millions of runs, replacing a routing LLM call
with a workflow agent is both a cost line item and a reliability win. The capacity math: if
coordination is ~1 model call at ~500 tokens per run and you serve 10M runs/day, that is 5B tokens/
day of pure routing overhead you delete by using a workflow agent where the flow is known.
Scoped state as a multi-tenancy boundary
The user:/app:/temp:/unprefixed scope prefixes are not ergonomics — they are tenancy
boundaries, and a principal reads them as such:
- unprefixed = per-conversation. Blast radius of a bad write: one session.
user:= per-user, shared across all of that user's sessions. Blast radius: one user's entire history. This is where preferences and profile live.app:= global, shared across every session of every user. Blast radius: the whole tenant population. A wrongapp:write is a config change that hits everyone at once.temp:= this turn only, never persisted. Blast radius: nil — which is exactly why secrets and scratch values belong here.
The design forces you to name the sharing scope at the write site. state["user:tier"] = "gold"
is a different, deliberate act from state["tier"] = "gold". The failure mode a principal guards
against: an unprefixed key that should have been user:, so a preference silently fails to
persist across sessions; or its inverse, a per-request value written to app:, leaking one
request's context into every user's next turn. The VertexAiSessionService persists these tiers to
managed storage with the same prefix routing — so a scoping bug written against the in-memory
store is a cross-tenant data leak in production. The prefixes are the multi-tenant contract, and
they are enforced by the State view's dispatch-on-prefix, not by convention.
Cross-cutting concerns ride the callback and Event rails
Because state changes and control flow all pass through Events and callbacks, the platform-level concerns compose cleanly onto them:
- Security lives in
before_tool_callback. It sits on the trust boundary: the model proposes a tool call, the callback inspects the proposed arguments, and returning a rejection means the tool function provably never executes. A hard block on any production-write tool is one callback, testable by asserting the function ran zero times. Authorization does not belong in the agent's prompt; it belongs on this rail. - Cost lives in
before_model_callback. Hash the request; on a cache hit, return the stored response and the paid model call is skipped entirely. For a support agent seeing repeated questions, this is a direct, measurable cut in Gemini spend — the short-circuit contract doing exactly what it is for. - Observability lives in the Event stream. Because every step is an Event with an
author,content, andstate_delta, the stream maps directly onto traces and spans in Vertex AI Agent Engine. You get per-step token attribution, latency per agent, and a replayable transcript without instrumenting anything — the observability was a byproduct of the execution model.
The principal insight: these are not three subsystems bolted on. They are three interpretations of the same interception points. Add a new cross-cutting concern (rate limiting, redaction, audit) and it is another callback or another Event consumer — the architecture already has a place for it.
Failure modes and blast radius
- A LoopAgent that never escalates spins to
max_iterations— bounded cost, but if the bound is high and the loop calls a model each iteration, it is a quiet cost amplifier. Blast radius: one run's budget, multiplied by however long it takes someone to notice the bill. - An LLM coordinator that mis-routes sends a request down the wrong sub-agent path. Blast radius: one request's correctness, and — because it is non-deterministic — an intermittent failure that is hard to reproduce. This is the argument for workflow agents wherever the flow is known.
- A ParallelAgent branch reading a sibling's write is a race that passes in dev and fails in prod. Blast radius: silent wrong answers, correlated with load. The structural fix (branches are an antichain) is cheaper than any amount of retry logic.
- A scoping bug (
app:where you meant session) is the highest-blast-radius failure: one request's data written to global scope contaminates every tenant. This is why the scope prefix is a review-gate item, not a stylistic preference.
Why the "looks wrong but is intentional" parts are right
Two ADK choices look like limitations and are load-bearing decisions. First, the model is
injectable and the loop is model-agnostic — the framework refuses to hardwire Gemini, so the same
orchestration runs against any provider via LiteLlm. That is what lets a Google-Cloud shop adopt
ADK without betting the whole stack on one model's availability. Second, workflow agents are
"just" BaseAgents with no LLM — they look underpowered next to an autonomous coordinator, but
that is the point: the powerful, non-deterministic part is confined to the leaves, and the
structure around them is ordinary code you can unit-test, replay, and defend in a design review.
ADK's entire value proposition is knowing which parts deserve determinism and building the API so
the deterministic choice is the easy one. A principal's job is to keep the LLM in the leaves and the
control flow in code — and to recognize, at review time, every place someone let it drift the other
way.