Spectre.Turn.Dispatcher behaviour (Spectre v0.3.0)

Copy Markdown View Source

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/3 with the reply text, or fallback_reply/2no_response/2 when the text is empty
  • {:no_response, result}no_response/2
  • {:awaiting, awaitable, result}satisfied_resolution/2 first; a resolution re-enters Spectre.Turn.resolve_policy/3 and the loop continues with the new decision. :not_satisfied delivers the policy request via policy_request/3.
  • {:needs, effect, result}execute?/3 (default true) then Spectre.execute/3; the executed result re-enters the loop. A false veto delivers through suppressed/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

action_result(term, t, keyword)

(optional)
@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).

deliver_reply(t, t, keyword)

@callback deliver_reply(String.t(), Spectre.Result.t(), keyword()) ::
  {:ok, term()} | {:error, term()}

Delivers reply text to the user. The only required callback.

execute?(t, t, keyword)

(optional)
@callback execute?(Spectre.Effect.t(), Spectre.Result.t(), keyword()) :: boolean()

Final host veto before executing a pending effect. Default: true.

fallback_reply(t, keyword)

(optional)
@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).

no_response(t, keyword)

(optional)
@callback no_response(
  Spectre.Result.t(),
  keyword()
) :: {:ok, term()} | {:error, term()}

Handles a turn that intentionally produces no reply. Default: {:ok, :no_response}.

policy_request(t, t, keyword)

(optional)
@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.

satisfied_resolution(t, keyword)

(optional)
@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.

suppressed(t, t, keyword)

(optional)
@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

dispatch(turn, handler, opts \\ [])

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