Drives a %Spectre.Turn{} decision to a delivered outcome.
Every host that consumes Spectre.turn/3 ends up writing the same loop:
branch on the decision tuple, auto-resolve a policy the host can already
satisfy, execute the pending effect, and pick a fallback reply when the
model produced nothing deliverable. This module owns that protocol so the
host only implements delivery.
The only required callback is deliver_reply/3. Everything else has a
protocol-level default:
{:reply, result}—deliver_reply/3with the reply text, orfallback_reply/2→no_response/2when the text is empty{:no_response, result}—no_response/2{:awaiting, awaitable, result}—satisfied_resolution/2first; a resolution re-entersSpectre.Turn.resolve_policy/3and the loop continues with the new decision.:not_satisfieddelivers the policy request viapolicy_request/3.{:needs, effect, result}—execute?/3(defaulttrue) thenSpectre.execute/3; the executed result re-enters the loop. Afalseveto delivers throughsuppressed/3.{:completed, completion, result}—action_result/3
Example
defmodule MyApp.ChatDelivery do
@behaviour Spectre.Turn.Dispatcher
@impl true
def deliver_reply(text, _result, opts) do
MyApp.Chat.send(Keyword.fetch!(opts, :conversation_id), text)
end
end
{:ok, turn} = Spectre.turn(instance, message.text, opts)
{:ok, _delivered} = Spectre.Turn.Dispatcher.dispatch(turn, MyApp.ChatDelivery)The loop is bounded: a turn crossing more than 4 policy-resolution or
execution steps returns {:error, {:dispatch_loop_exceeded, decision}}
instead of spinning.
Summary
Callbacks
Delivers an executed action result. Default: deliver_reply/3 with the result text.
Delivers reply text to the user. The only required callback.
Final host veto before executing a pending effect. Default: true.
Reply used when the result text is empty. Default: nil (fall through to no_response/2).
Handles a turn that intentionally produces no reply. Default: {:ok, :no_response}.
Delivers an open policy request. Default: deliver_reply/3 with the result text.
Resolves an open policy from host state without asking the user.
Delivers the outcome of a vetoed effect. Default: deliver_reply/3 with the result text.
Functions
Dispatches the turn's decision through the handler module.
Callbacks
@callback action_result(term(), Spectre.Result.t(), keyword()) :: {:ok, term()} | {:error, term()}
Delivers an executed action result. Default: deliver_reply/3 with the result text.
The completion is the terminal %Spectre.Effect{}: status: :completed after a
normal execution, status: :cancelled when a before_action guard suppressed
the capability call (the result text then carries the guard's reply).
@callback deliver_reply(String.t(), Spectre.Result.t(), keyword()) :: {:ok, term()} | {:error, term()}
Delivers reply text to the user. The only required callback.
@callback execute?(Spectre.Effect.t(), Spectre.Result.t(), keyword()) :: boolean()
Final host veto before executing a pending effect. Default: true.
@callback fallback_reply( Spectre.Result.t(), keyword() ) :: String.t() | nil
Reply used when the result text is empty. Default: nil (fall through to no_response/2).
@callback no_response( Spectre.Result.t(), keyword() ) :: {:ok, term()} | {:error, term()}
Handles a turn that intentionally produces no reply. Default: {:ok, :no_response}.
@callback policy_request(Spectre.Awaitable.t(), Spectre.Result.t(), keyword()) :: {:ok, term()} | {:error, term()}
Delivers an open policy request. Default: deliver_reply/3 with the result text.
@callback satisfied_resolution( Spectre.Awaitable.t(), keyword() ) :: {:ok, Spectre.Policy.resolution()} | :not_satisfied
Resolves an open policy from host state without asking the user.
Return {:ok, resolution} when the host already knows the answer (for
example, terms accepted in a previous session); the dispatcher feeds it to
Spectre.Turn.resolve_policy/3 and continues with the resulting decision.
Default: :not_satisfied.
@callback suppressed(Spectre.Effect.t(), Spectre.Result.t(), keyword()) :: {:ok, term()} | {:error, term()}
Delivers the outcome of a vetoed effect. Default: deliver_reply/3 with the result text.
Functions
@spec dispatch(Spectre.Turn.t(), module(), keyword()) :: {:ok, term()} | {:error, term()}
Dispatches the turn's decision through the handler module.
{:ok, delivered} = Spectre.Turn.Dispatcher.dispatch(turn, MyApp.ChatDelivery)opts are merged over turn.opts and passed to every callback.