---
url: 'https://adk.nht.io/the-loop/turn-runner/gates-and-non-goals.md'
description: >-
  ctx.waitFor and TurnGate from the runner's side, and what run() explicitly
  does not do.
---

# Gates and non-goals

How `ctx.waitFor(gate)` opens a [`TurnGate`](../../api/) from the runner's perspective, and the behaviors `run()` deliberately refuses to perform.

[Turn Runner](../turn-runner) covers the construction contract. [Gates](../gates) covers the full gate primitive — read it before you write another tool handler.

## Gates and `ctx.waitFor`

::: danger You need gates. Yes, you.
If you are reading this and thinking *my agent does not need a gating mechanism* — stop. Open the [Gates](../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`](../../api/) — 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`](https://adk.nht.io/api/@nhtio/adk/common/interfaces/TurnGate#resolve) / [`TurnGate.reject`](https://adk.nht.io/api/@nhtio/adk/common/interfaces/TurnGate#reject) / [`TurnGate.abort`](https://adk.nht.io/api/@nhtio/adk/common/interfaces/TurnGate#abort) calls no-op.
* The turn's `AbortController` is wired in — turn abort rejects every open gate with [`E_TURN_GATE_ABORTED`](https://adk.nht.io/api/@nhtio/adk/exceptions/variables/E_TURN_GATE_ABORTED).
* If the gate carries a schema, `resolve(value)` validates first; failed validation throws [`E_INVALID_TURN_GATE_RESOLUTION`](https://adk.nht.io/api/@nhtio/adk/exceptions/variables/E_INVALID_TURN_GATE_RESOLUTION) synchronously in the resolver's context and leaves the gate open.
* `turnGateOpen` fires synchronously at construction; `turnGateClosed` fires on settlement with one of `'resolved' | 'rejected' | 'aborted' | 'timeout'`.

::: warning 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`](https://adk.nht.io/api/@nhtio/adk/exceptions/variables/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.
:::

::: warning 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](../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](../../assembly/). The closest companions to this page are [LLM Dispatch](../llm-dispatch) (the iteration loop and the executor seam), [Pipelines](../pipelines) (the four pipelines and their `ctx.stash` contract), and [Events](../events) (full payload shapes for both buses).
