---
url: 'https://adk.nht.io/the-loop/llm-dispatch/adk-facts.md'
description: >-
  What the runner does around the executor — store queueing, sealed-stream
  rules, abort wiring — and why helpers and persistence are decoupled.
---

# ADK-side facts and helpers vs persistence

The runner-side constraints on the executor surface, and the deliberate decoupling between streaming helpers and the persistence layer.

[The executor seam](./executor-seam) covers the callback contract and what the runner puts on `ctx`.

## ADK-side facts that constrain the surface

A handful of ADK-side facts constrain the executor's surface. They are not about how the executor should be written; they are about what the runner does around it.

::: warning Stores queue as deltas
`ctx.store*` / `ctx.mutate*` / `ctx.delete*` calls during an iteration queue as deltas. They flush to the parent [`TurnContext`](https://adk.nht.io/api/@nhtio/adk/types/interfaces/TurnContext) at the end of the iteration on the derived path, or drain locally on the standalone path. Abort or [`DispatchContext.nack`](https://adk.nht.io/api/@nhtio/adk/types/interfaces/DispatchContext#nack) mid-iteration discards the queue — partial writes never reach the parent. Successful iterations always flush before `iterationEnd`.
:::

::: warning Reporting after a stream is sealed throws
Once `helpers.reportMessage(id, …, { isComplete: true })` — or the equivalent for thoughts or tool calls — has been called for an `id`, any further `report*` for that `id` throws a bare `Error`. Uncaught, it surfaces as `E_LLM_EXECUTION_EXECUTOR_ERROR` and nacks the dispatch. Pick stable `id`s and complete each one exactly once.
:::

::: warning `ctx.abortSignal` is the abort surface
Wire [`DispatchContext.abortSignal`](https://adk.nht.io/api/@nhtio/adk/types/interfaces/DispatchContext#property-abortsignal) (or a signal linked to it) into whatever long-running operation the executor performs. The turn-level abort fires this signal; if the executor doesn't observe it, the request continues until it completes on its own.
:::

::: tip Nack vs throw is the executor's call
The runner treats them differently. A throw is wrapped as `E_LLM_EXECUTION_EXECUTOR_ERROR` and reported on the `error` bus. `ctx.nack(error)` reports the original error unwrapped. Use `nack` for expected failure modes you have a stable error code for; let throws surface bugs.
:::

The executor must not assume what middleware did. Read [`DispatchContext.turnMessages`](https://adk.nht.io/api/@nhtio/adk/types/interfaces/DispatchContext#property-turnmessages), [`DispatchContext.turnRetrievables`](https://adk.nht.io/api/@nhtio/adk/types/interfaces/DispatchContext#property-turnretrievables), and friends if you need to know what is in scope; do not capture middleware-local state in closures.

## Helpers vs persistence

Helpers emit on the event bus; persistence writes to your store. They are **deliberately decoupled**.

* Helpers stream the wire shape — enough for a UI to render a partial response and enough for downstream consumers to consume chunks.
* Persistence stores the canonical record — including fields (`identity`, [`SpooledArtifact`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact) results, [`Thought.replayCompatibility`](https://adk.nht.io/api/@nhtio/adk/common/classes/Thought#property-replaycompatibility)) that the wire shape does not carry.

::: warning Two function calls, not one — emit is not persistence
Calling `helpers.reportMessage` without later calling `ctx.storeMessage` creates a ghost message: the UI saw it, storage did not, and the next turn has no record it happened. The wire payload and the persisted record are not the same data, and an emit-only message is invisible to every later turn. Emit-only messages are allowed only when you deliberately want a ghost — intentionally ephemeral progress chatter, "thinking..." placeholders, throwaway debug surfacing. The ADK cannot tell the difference and will not flag it. Audit every `report*` without a matching `store*`.
:::
