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/5redeems the row (Executor.redeem_as/6) with the same tool and the arguments the model sent — the registry name of the card andPhoenixKitAgent.Turns.Tools.arguments/1of 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/5turns 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'susedtoredeem_as/6orreject_for_user/2, read back asusedwithget_for_user/2, or, when that read is withheld because the user may no longer use the tool, withlookup_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'sinvalid(it deletes its rows) reads asexpiredtoo;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 theexpirednotice of a card not yet closed): both are there, or neither;failed— only when the handler failed after core spent the row (an error ofredeem_as/6with the rowused).
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.
Summary
Functions
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 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.
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.
The outcomes a card's notice takes.
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.
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.
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.
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.
Types
@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).
@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.
Functions
@spec cards(PhoenixKitAgent.Conversations.session_ref(), boolean(), term(), [ %{:uuid => term(), optional(atom()) => term()} ]) :: {:ok, %{required(String.t()) => 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 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).
@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.
@spec outcomes() :: [String.t()]
The outcomes a card's notice takes.
@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.
@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.
@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.
@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.