Gates and non-goals
How ctx.waitFor(gate) opens a TurnGate from the runner's perspective, and the behaviors run() deliberately refuses to perform.
Turn Runner covers the construction contract. Gates covers the full gate primitive — read it before you write another tool handler.
Gates and ctx.waitFor
You need gates. Yes, you.
If you are reading this and thinking my agent does not need a gating mechanism — stop. Open the Gates page and read it before you ship anything. The thought "I will add safety later" is the exact reasoning behind every agent that has deleted a production database, leaked a credential, charged a card without authorization, or filed a ticket as the wrong user. Later arrives as an incident report.
The ADK cannot make your tools safe. It cannot enforce your permissions, validate authority, or verify identity for you. Those decisions live in your application — and the only place they can be enforced is inside the code path that actually performs the side effect. If your tool calls touch anything you would not let an anonymous internet user trigger, you need gates at the handler, not after the model has already proposed the action, not in middleware downstream of where the damage happens.
There is no scenario where a non-trivial agent does not need this. "My tools are read-only" — until someone adds a write one, and the gate was never wired. "My users are authenticated" — authenticated users are not authorized users, and the model is not a user at all. "I will add it before launch" — you will not, because launch pressure always wins, and the gating story you skipped today is the postmortem you write next quarter. The library shipped this primitive because every agent we have ever seen needed it, and the ones that did not have it had it written into them, badly, after something went wrong.
ctx.waitFor(gate) opens a TurnGate — the cooperative suspension primitive the runner owns. Gates are the seam through which every safety, authorization, and human-oversight feature attaches to the ADK. The ADK's contribution is bounded; your contribution is the rest.
The runner's contract is small and bounded:
- One settlement per gate, ever. Subsequent
TurnGate.resolve/TurnGate.reject/TurnGate.abortcalls no-op. - The turn's
AbortControlleris wired in — turn abort rejects every open gate withE_TURN_GATE_ABORTED. - If the gate carries a schema,
resolve(value)validates first; failed validation throwsE_INVALID_TURN_GATE_RESOLUTIONsynchronously in the resolver's context and leaves the gate open. turnGateOpenfires synchronously at construction;turnGateClosedfires on settlement with one of'resolved' | 'rejected' | 'aborted' | 'timeout'.
One honest caveat on the open emission
"Synchronously at construction" means: if construction succeeds. new TurnGate(...) validates its own raw input against rawTurnGateSchema, and a malformed raw gate throws E_INVALID_INITIAL_TURN_GATE_VALUE before the turnGateOpen emission line is reached. In that case there is no gate to emit — ctx.waitFor(...) throws synchronously in the middleware that called it, which then surfaces as the relevant pipeline error (E_INPUT_PIPELINE_ERROR, E_OUTPUT_PIPELINE_ERROR, or E_DISPATCH_PIPELINE_ERROR) on the error bus. Build your gate's raw input correctly and this never fires; do it wrong and you see a pipeline error, not a missing-emission mystery.
Where the gate is opened decides what blocks
The middleware pipelines are sequential — each middleware does work, calls await next(), then resumes. Awaiting a gate before next() holds every downstream middleware in the same pipeline until the gate settles. Awaiting it after next() holds only the post-step. A gate inside a tool handler holds that dispatch iteration. The runner itself does not pause; other turns are unaffected; the turn-level abort still fires. There is no "the awaiter pauses, the turn keeps going" — choose the gate's location based on what should actually stop.
The runner owns the lifecycle. It owns nothing about who can resolve, how an operator sees the request, how the resolver finds the gate, or whether the gate survives a process restart. That is your contract — by design, because the only safe defaults at that boundary are the ones you choose.
Read Gates before you write another tool handler. That page covers the position the ADK takes on safety, the patterns gates are built for, how to handle durability across process restarts, and worked examples of RBAC-gated handlers and webhook-resolved handoffs. Skipping it is a choice; pretending it does not apply to your agent is also a choice. Both are wrong.
What run() does not do
DANGER
run() does not retry. It does not bound iterations. It does not impose policy. It does not interpret Message.role semantically. It does not decide when to call tools, or in what order. It does not chunk, summarize, or trim context.
Those are all behaviors, and behaviors are yours. The executor calls the model; middleware shapes the context, dispatches tools, and enforces the bounds you want. The runner threads the context through them, emits, and resolves. The ADK is the ADK; the agent is yours.
The seams where behavior actually lives are in Extending. The closest companions to this page are LLM Dispatch (the iteration loop and the executor seam), Pipelines (the four pipelines and their ctx.stash contract), and Events (full payload shapes for both buses).