Steps 7 and 8 of the request flow, written as small functions over the
executor's request state so PhoenixKit.API.Executor.execute/4 stays a plain chain:
... Flow.merge_header -> validate -> Flow.split_control ->
Flow.idempotency -> Flow.gate -> runEach step returns {:ok, state} to go on, {:error, code, detail, context, state}
to refuse (the executor answers a problem document), or {:done, state} when the
answer is already in state.body (a preview, a replay): the executor then responds
without running the handler. finish/1 and release/1 are called when the
response is sent: they store the answer under the idempotency claim, or give the
claim up.
The request state is the executor's map. Besides its own keys the steps read and set
control, dry_run?, input_hash, claim, replayed?, held_again?, store?,
outcome, confirmation, preview and result (the answer as a decoded map, for a
transport that wants the map rather than the text). The executor resolves
request_context once, before the gate, for every context the steps build.
Summary
Functions
Called when the answer is sent. Stores a 200 body under the claim when it may be
stored (Idempotency.storable?/1), otherwise gives the claim up. Never raises: the
handler has already run, so a store that fails (a database hiccup) is logged and the
answer is sent anyway; the claim then runs out with its lease.
Step 8. A read tool passes. dry_run: true runs the handler with ctx.dry_run? == true
and answers the preview (no code, no approval). A dangerous call (dangerous?/1) goes
through the confirmation or approval flow; any other call passes.
Claims the request's Idempotency-Key, if it has one. A dry run never claims.
A public call without a cart token has no scope to claim in and ignores the key.
The Idempotency-Key header of a write call becomes the idempotency_key field, so
the schema validates it like the field. Both given and different, or the header sent
twice, is a validation error. A read tool ignores the header, and so does a request
that does not read it (idempotency_header?: false, the MCP transport: its key is the
argument).
Called when the request is refused: gives the idempotency claim up. Never raises.
Takes the control fields out of the validated input. The handler gets what is left;
input_hash is computed from it, so a confirming call hashes like its preview.
Functions
Called when the answer is sent. Stores a 200 body under the claim when it may be
stored (Idempotency.storable?/1), otherwise gives the claim up. Never raises: the
handler has already run, so a store that fails (a database hiccup) is logged and the
answer is sent anyway; the claim then runs out with its lease.
Step 8. A read tool passes. dry_run: true runs the handler with ctx.dry_run? == true
and answers the preview (no code, no approval). A dangerous call (dangerous?/1) goes
through the confirmation or approval flow; any other call passes.
Claims the request's Idempotency-Key, if it has one. A dry run never claims.
A public call without a cart token has no scope to claim in and ignores the key.
The Idempotency-Key header of a write call becomes the idempotency_key field, so
the schema validates it like the field. Both given and different, or the header sent
twice, is a validation error. A read tool ignores the header, and so does a request
that does not read it (idempotency_header?: false, the MCP transport: its key is the
argument).
Called when the request is refused: gives the idempotency claim up. Never raises.
Takes the control fields out of the validated input. The handler gets what is left;
input_hash is computed from it, so a confirming call hashes like its preview.
The input of a write tool must not hold a NUL character (JSON \u0000): a code or an
approval keeps the input and its preview in jsonb, which cannot store one, so the call
is refused here, before anything is claimed or written.