PhoenixKit.API.Flow (phoenix_kit v2.63.0)

Copy Markdown View Source

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 -> run

Each 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

finish(map)

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.

gate(state)

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.

idempotency(state)

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.

merge_header(state)

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).

release(map)

Called when the request is refused: gives the idempotency claim up. Never raises.

split_control(state)

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.