Skip to content
5 min read · 916 words

The Loop

This section is the wiring diagram for one ADK turn. It shows what the runner owns, what it refuses to own, and where your code becomes the agent. If you are looking for a hidden orchestrator, stop looking. The seams are the product. Each page picks one seam — turn entry, the model loop, primitives, tools, events, middleware, trust, failure, budgets — and explains what it owns, what it doesn't, and how your code attaches to it.

First time looking at an ADK?

Start with How agents work for a plain-English orientation to the vocabulary — turn, dispatch, iteration, tool, context, middleware — before diving into the contract surface here. The rest of this section assumes you know what those words mean.

A turn is one end-to-end agent request: input arrives, the ADK threads it through your code, the model is called (possibly many times, with tool calls in between), state gets persisted, the turn resolves. The ADK does the bookkeeping; you bring the behaviour.

The runner is the bookkeeper, not the agent

There is no hidden agent loop. There is no orchestrator that retries on your behalf. There is no policy quietly intercepting your messages. If something happens during a turn, it happens because your code, your middleware, or your executor made it happen.

That position is the whole point. An ADK is not a black box that "just works" until it sets something on fire. Its job is to give you a tight set of primitives, force you to declare the behavior you want, make every safety property you declared traceable to code you wrote, and run against the model you picked. Everything in this section is a closer read of one of those primitives.

What one turn actually does

TIP

Every node above is a link. Click any stage to jump to the page that owns its contract.

A turn is initiated by exactly one call to TurnRunner.run(). The runner threads a TurnContext through five stages:

  1. Validate. Raw input is checked against a schema. A missing TurnContext.systemPrompt, a broken abort controller, a malformed standing instruction — rejected before any callback fires. There is no "we'll fix it up for you."
  2. Run input middleware. Each turnInputPipeline runs in order. Retrieval happens here. Memories load. History gets packed. Policy gets enforced. Anything that should happen before the model sees the turn.
  3. Dispatch to the LLM. DispatchRunner takes over. One DispatchContext per iteration. Your TurnRunnerConfig.executorCallback is the only place ADK code calls a model — the ADK has no opinion about which provider. The loop continues until the executor signals ctx.ack() (DispatchContext.ack, done) or ctx.nack(error) (DispatchContext.nack, failed).
  4. Run output middleware. Each turnOutputPipeline runs in order. Tool calls get dispatched. Results get persisted. Refusals get filtered. Telemetry gets recorded. Anything that should happen after the model produced output but before the turn returns.
  5. Resolve. run() resolves Promise<void>. There is no return value. Everything you need to see was emitted through events.

No iteration limit. No retry policy. No termination heuristic.

The ADK owns the bookkeeping; the agent's behavior is yours. If you need bounds — and you do — you build them out of ctx.iteration, ctx.toolCallCount, and middleware. They are easy. They just are not done for you.

What plugs in, and where

The ADK has four classes of seam, sorted by when they run. Put code in the wrong seam and the bug is not subtle: retrieval runs ten times, telemetry becomes load-bearing, or state leaks across iterations. Turn-scope middleware runs once around the dispatch loop. Iteration-scope middleware runs every time the model is called. Storage callbacks run on demand whenever the loop reads or writes a primitive. Event listeners run whenever the loop emits. The table below is not decoration. It is the blast-radius map.

Turn-scope

Once per turn

Runs once around the dispatch loop. This is where retrieval, persistence, and policy live.

Iteration-scope

Once per LLM iteration

Runs inside the dispatch loop, once each time the model is called.

On-demand

Storage callbacks

Called whenever the loop needs to read or write a primitive. Your store, your shape, your transactions.

Events

When the loop emits

Two buses with the same surface, separated by intent. Functional handlers are product behaviour; observability handlers are instrumentation.

The shape of each seam is fixed. The implementation behind it is yours. The bundled batteries (LLM adapters, storage, tool catalogs) are reference implementations of those seams — they are not load-bearing. The ADK runs identically whether every callback is a database hit, an in-memory map, or a no-op.

What this section covers

  • Turn Runner — the entry point, eager config validation, the TurnContext, the two event buses.
  • LLM DispatchDispatchRunner, the iteration loop, the executor seam, the ack / nack lifecycle.
  • PrimitivesMessage, Memory, Thought, ToolCall, Retrievable, Tokenizable, Identity.
  • ToolsTool, ToolRegistry, schema-owned argument validation, collision policy.
  • ArtifactsSpooledArtifact, the handle pattern, the ephemeral SpooledArtifact.forgeTools lifecycle.
  • Events — functional vs observability events and the rule that separates them.
  • Pipelines — input and output pipelines, ctx.stash for cross-middleware state.
  • GatesTurnContext.waitFor and the position the ADK takes on safety, RBAC, and human-in-the-loop.
  • Trust Tiers — envelopes, multi-identity rendering, RAG tiering, reasoning fences.
  • Failure — exception codes, validation errors, gate failures, what DispatchContext.ack / DispatchContext.nack actually mean.
  • Budgets — context-window estimation, runtime-resource bounds, spool-backed artifact access.

Three other places to look

  • Extending — the recipe-and-pattern half of the docs.
  • API reference — generated from source; the field-by-field contract surface that never drifts.
  • Trust Tiers — the deepest rationale, with per-tier research sub-pages carrying the threat models and external citations.