# `PhoenixKitAgent.Confirmations`
[🔗](https://github.com/BeamLabEU/phoenix_kit_agent/blob/v0.1.0/lib/phoenix_kit_agent/confirmations.ex#L1)

The confirmation cards of the chat (spec §12 Ф2, Б2.2): a write the model
asked for waits for the user, who runs it, turns it down or asks for it
again.

A turn never writes (`PhoenixKitAgent.Turns.Runner`): core answers a write of
a live session with a preview and holds the call in a confirmation row bound
to the user (`PhoenixKit.API.Executor.execute_as/5`, `allow: :all`), and the
turn stores a `tool` message `needs_confirmation` with the row's uuid
(`confirmation_uuid`) and the answer that asked for it
(`assistant_message_uuid`), and ends there. A card is that message; it
waits while the chat has no `notice` with the same `confirmation_uuid`.

Like `PhoenixKitAgent.Conversations`, every function acts for a **live
browser session** and takes the answer of `MultiSession.impersonated?/1` for
it: an impersonated session, a session that cannot act, a user who may not
use the assistant, someone else's chat, or a message that is not a card of
it, are refused before anything else is read (the refusals of
`Conversations.owned_conversation/3`, then `:not_waiting`).

## A click

`confirm/5`, `reject/5` and `repeat/5` take the user's turn lock for the
click (`PhoenixKitAgent.Turns.Locks.acquire_user/4`): while a turn of the
user runs, a click is `{:error, :turn_running}`, while another click runs
`{:error, :click_running}`, and no turn starts while a click runs (the turn
is refused `:click_in_flight`). The lock blocks a turn for 30 s from the
moment it was last kept: a process next to the click, under
`PhoenixKitAgent.Confirmations.TaskSupervisor`, keeps it fresh every 10 s
for as long as the click runs (`Locks.keep_user/4`; `config
:phoenix_kit_agent, click_keep_ms:`, held between 100 ms and 15 s, half the
hold), and stops with it, so a lock never released (its node went down)
frees the user's turns 30 s after its last keep. Should the database refuse
the keeps for longer than the hold, a turn may start while the click still
runs. A click calls no model and stores no user message, so it spends
neither money nor a turn of the user's limits. The card is read again under
the lock.

  * `confirm/5` redeems the row (`Executor.redeem_as/6`) with the same tool
    and the arguments the model sent — the registry name of the card and
    `PhoenixKitAgent.Turns.Tools.arguments/1` of the call in its assistant
    message — under the rights of the session that clicks, now. Core checks
    everything again (the session, the tool and its module, the "Agent may
    act" box, the scope and the permission, the rate limit, the input, a
    fresh dry run against the preview's fingerprint) and spends the row
    atomically before the handler runs.
  * `reject/5` turns the row down (`PhoenixKit.API.Confirm.reject_for_user/2`).
  * `repeat/5`, for a card whose row ran out (or that core has deleted),
    asks for the same call again (`execute_as/5`): a new card.

What became of the card is a new `notice` with its `confirmation_uuid`, the
tool's name and `tool_outcome`:

  * `confirmed` — the handler ran; and also for a row core had spent
    already with no notice of it here (core's `used` to `redeem_as/6` or
    `reject_for_user/2`, read back as `used` with `get_for_user/2`, or,
    when that read is withheld because the user may no longer use the
    tool, with `lookup_for_user/2`): the click that spent it was lost after
    core spent it, and core spends the row right before the handler runs.
    Should the row not read so, the card keeps waiting with the reason;
  * `rejected`;
  * `expired` — the row ran out, or is gone: core's `invalid` (it deletes
    its rows) reads as `expired` too;
  * `changed` — the values changed under the preview: core holds the call
    in a new row, which is a new card with the same assistant message,
    stored right after the notice in the same transaction (as is the new
    card of "Ask again" with the `expired` notice of a card not yet
    closed): both are there, or neither;
  * `failed` — only when the handler failed after core spent the row (an
    error of `redeem_as/6` with the row `used`).

A notice the chat refuses (it was deleted meanwhile) makes the click
`{:error, :not_stored}`, whatever core did.

A card can also stop waiting with no click to say so: its row ran out, core
deleted it, or the click that settled it was lost with its node.
`settle_lapsed/3` (the chat panel, when it opens a chat) and
`settle_lapsed_in_turn/2` (a turn, before it reads the history) give such a
card its notice by what core's row says — `used` is `confirmed`, `rejected`
is `rejected`, a row that ran out or is gone is `expired` — so neither the
panel nor the model takes it for waiting. A card the user may no longer run
(the right to its tool is gone) still waits, for "Cancel the change".

A refusal before the row is spent — the session, the rights, the module's
box, the handler's own dry run, the rate limit — writes nothing: the card
keeps waiting and the click answers `{:error, {:refused, code}}` with
core's code. Such a notice is the only thing of a click the model learns,
in the history of the next turn (`PhoenixKitAgent.Turns.History`), where a
card with no notice yet reads as waiting.

Neither the arguments, nor the preview, nor what the handler returned are
stored or logged here.

# `card`

```elixir
@type card() :: %{
  state: String.t(),
  tool: String.t(),
  preview: map() | nil,
  arguments: String.t() | nil,
  expires_at: DateTime.t() | nil,
  repeatable?: boolean(),
  rejectable?: boolean()
}
```

What a card shows (see `cards/4`).

# `refusal`

```elixir
@type refusal() ::
  PhoenixKitAgent.Conversations.refusal()
  | :not_waiting
  | :not_expired
  | :already_waiting
  | :turn_running
  | :click_running
  | :no_arguments
  | :not_stored
  | {:refused, atom()}
```

Why a click did nothing.

# `cards`

```elixir
@spec cards(PhoenixKitAgent.Conversations.session_ref(), boolean(), term(), [
  %{:uuid =&gt; term(), optional(atom()) =&gt; term()}
]) ::
  {:ok, %{required(String.t()) =&gt; card()}}
  | {:error, PhoenixKitAgent.Conversations.refusal()}
```

What the chat panel shows of the cards among `messages` (messages of the
user's chat `conversation_uuid`, as `PhoenixKitAgent.Conversations` hands
them out): a map from the uuid of each card to `t:card/0`. Only the uuids
of `messages` are used; the rows are read again in this chat, and a message
that is not a card of it is left out.

  * `state` — `"waiting"`, an outcome of its notice, `"expired"` for a row
    that ran out or is gone, or `"unavailable"` (the session cannot read it,
    or the row was settled without a notice here);
  * `preview`, `expires_at`, `arguments` — for a waiting card only: core's
    preview (`PhoenixKit.API.Confirm.get_for_user/2`, read with this
    session), when it runs out, and the arguments the model sent, as plain
    JSON text (to be shown as text, never as markup), cut to
    2000 characters;
  * `repeatable?` — the card ran out, its call is still available, it was
    not asked for again and no other card waits for the same write;
  * `rejectable?` — it can be turned down: it waits, or it is
    `"unavailable"` because the user may no longer use its tool (core lets
    such a row be turned down, not run).

# `confirm`

```elixir
@spec confirm(
  PhoenixKitAgent.Conversations.session_ref(),
  boolean(),
  term(),
  term(),
  keyword()
) ::
  {:ok, :confirmed | :changed | :expired | :rejected | :failed}
  | {:error, refusal()}
```

The user runs the write the card `message_uuid` waits for (see the
moduledoc). `{:ok, outcome}` — `:confirmed`, `:changed`, `:expired`,
`:rejected` or `:failed`, written as the card's notice — or `{:error,
reason}`, after which the card still waits.

Options: `:request_context` — as for `PhoenixKitAgent.Turns.start/6`;
`:request_id` is the click's own.

# `outcomes`

```elixir
@spec outcomes() :: [String.t()]
```

The outcomes a card's notice takes.

# `reject`

```elixir
@spec reject(
  PhoenixKitAgent.Conversations.session_ref(),
  boolean(),
  term(),
  term(),
  keyword()
) ::
  {:ok, :rejected | :expired | :confirmed} | {:error, refusal()}
```

The user turns down the write the card `message_uuid` waits for:
`{:ok, :rejected}` (or `:expired` when the row ran out or is gone, and
`:confirmed` when core had spent it already: see the moduledoc), written as
the card's notice.

# `repeat`

```elixir
@spec repeat(
  PhoenixKitAgent.Conversations.session_ref(),
  boolean(),
  term(),
  term(),
  keyword()
) ::
  {:ok, :repeated} | {:error, refusal()}
```

The user asks again for the write of the card `message_uuid`, whose row ran
out or is gone: core holds the same call in a new row, a new card with the
same assistant message (`{:ok, :repeated}`). The old card is closed as
`expired` if it was not yet. `{:error, :not_expired}` for any other card,
and for one asked for again already; `{:error, :already_waiting}` when
another card of this chat still waits for the same tool and arguments.

# `settle_lapsed`

```elixir
@spec settle_lapsed(PhoenixKitAgent.Conversations.session_ref(), boolean(), term()) ::
  :ok
  | {:error,
     PhoenixKitAgent.Conversations.refusal() | :turn_running | :click_running}
```

Gives each card of the user's chat `conversation_uuid` that core no longer
holds, though no notice says so, the notice of what became of it (see the
moduledoc). Nothing is written, and no lock is asked for, when there is no
such card; else under the user's lock, as a click: `{:error,
:turn_running}` or `{:error, :click_running}`, with nothing written, while a
turn or a click holds it.

# `waiting_cards`

```elixir
@spec waiting_cards(PhoenixKitAgent.Conversations.session_ref(), boolean(), term()) ::
  {:ok, [PhoenixKitAgent.Schemas.Message.t()]}
  | {:error, PhoenixKitAgent.Conversations.refusal()}
```

The cards of the user's chat `conversation_uuid` that wait — no notice of
their `confirmation_uuid` in the chat — wherever they are in it, oldest
first, as `PhoenixKitAgent.Conversations` hands messages out: what the chat
panel shows of the cards that are not on the page of the chat it loaded.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
