Starts the task of each turn under PhoenixKitAgent.Turns.TaskSupervisor,
watches it, and ends the turn whatever way the task ends (spec §6.1 step 5):
- the task returns — the turn wrote its own answer or notice;
- the deadline passes (
agent_turn_deadline_s, counted from the go-ahead) — the task is asked to stop (below) and adeadlinenotice is written; - the user stops it (
PhoenixKitAgent.Turns.stop/4,request_stop/2) — nothing is written, a partial answer is not kept; - its chat was deleted (
{:shutdown, :chat_gone}) — nothing is written; - it ends in any way before its go-ahead (see below) — nothing is written: the turn wrote nothing yet;
- anything else (a crash, a message the database refused, a kill) — an
interruptednotice.
Then both locks of the turn are released and the chat's topic hears
{:agent_turn_finished, conversation_uuid, turn_ref} (also for a turn that
never started — a listener matches the turn_ref it got from
PhoenixKitAgent.Turns.start/6). Those writes run in a short-lived process
of their own, so a slow database delays no other start and no deadline; if
that process cannot be started (the task supervisor is restarting), they
run here.
Stopping: a request first, a kill after the grace period
Neither "Stop" nor the deadline kills the task outright: a model call under
way would die before phoenix_kit_ai writes its journal row, and what it
cost would be lost to the limits. The monitor sends the task a stop request,
which the task honours between its steps (check_stop/0, see
PhoenixKitAgent.Turns.Runner), and kills it only if it is still there
after the grace period (:turn_stop_grace_ms, default 30 s — within the 60
s a lock outlives its deadline). However the task then ends, the turn ends
as stopped or past its deadline. A second request, or a deadline after a
stop, changes nothing.
Stopping a turn of another node
A "Stop" that reaches a node where the turn does not run (its task is not in
this node's PhoenixKitAgent.Turns.Registry) is passed on over PubSub
(broadcast_stop/1): the topic agent:turn:<turn_ref> (topic/1) hears
{:agent_stop_turn, turn_ref} — the turn ref and nothing else. The monitor
listens to the topic of every turn it runs, from its start to its end, and
takes such a request as it takes a local "Stop" (request_stop/2); a request
for a turn ref it does not run is ignored. Who may stop the turn is checked
where the request is made (PhoenixKitAgent.Turns.stop/4).
Nothing of a turn in the log
A turn carries the user's text, the history, the system prompt and the
session reference. None of it may reach the log, so: the task runs the turn
inside a guard and ends with {:shutdown, _} whatever happens (no crash
report; a crash is logged as the exception's module and where it happened,
without arguments); a refusal returns only the class of an exit
(exit_class/0), never the exit term; and format_status/1 keeps the
turn out of this process's own crash report.
The start handshake
The task is monitored before it does anything. It registers in
PhoenixKitAgent.Turns.Registry under its turn_ref, tells the process
that starts the turn so, and waits for that process's go-ahead — not this
one's. So a turn runs only once start_turn/3 has returned {:ok, pid} to
its caller, and is then already reachable for "Stop". A caller that gave up
(this process was too busy to take the start in time, or the task did not
report ready) sends no go-ahead, and the task ends on its own without
touching the chat or the locks its caller has already released.
The deadline is armed only when the task reports that it got the
go-ahead, so a task whose caller gave up can never be killed as :deadline
and write a notice under a turn ref nobody saw.
If this process goes down, the supervisor (:rest_for_one) takes the
running turns down with it; their locks then expire by their TTL.
Summary
Types
What kind of exit made a start fail — never the exit term itself, which can hold the whole turn.
Functions
Asks the monitor of whichever node runs turn_ref to stop it (see "Stopping
a turn of another node"); the message is the turn ref only. Asynchronous: a
turn that no node runs is not told. {:error, :not_running} when PubSub
refuses the message or is not there (it restarts).
In a turn's task: ends it as {:shutdown, reason} if it was asked to stop,
else :ok. The turn calls it between its steps.
Returns a specification to start this module under a supervisor.
Asks the turn whose task is pid to stop (reason :stopped); it is
killed if it has not stopped within the grace period. Asynchronous.
{:error, :not_running} while the monitor is not up (it restarts, and its
tasks go with it).
Starts turn (see PhoenixKitAgent.Turns.Runner) in a task with
deadline_ms to live, and lets it run once it is registered (see "The start
handshake"). callers become the task's $callers, so the process that
started the turn is its caller, as for a Task it starts.
The grace period, in ms, a task asked to stop has before it is killed
(:turn_stop_grace_ms, default 30 s).
The PubSub topic of the turn turn_ref: agent:turn:<turn_ref>.
Types
Functions
@spec broadcast_stop(String.t()) :: :ok | {:error, :not_running}
Asks the monitor of whichever node runs turn_ref to stop it (see "Stopping
a turn of another node"); the message is the turn ref only. Asynchronous: a
turn that no node runs is not told. {:error, :not_running} when PubSub
refuses the message or is not there (it restarts).
@spec check_stop() :: :ok
In a turn's task: ends it as {:shutdown, reason} if it was asked to stop,
else :ok. The turn calls it between its steps.
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec request_stop(pid(), :stopped) :: :ok | {:error, :not_running}
Asks the turn whose task is pid to stop (reason :stopped); it is
killed if it has not stopped within the grace period. Asynchronous.
{:error, :not_running} while the monitor is not up (it restarts, and its
tasks go with it).
@spec start_turn(PhoenixKitAgent.Turns.Runner.turn(), pos_integer(), [pid()]) :: {:ok, pid()} | {:error, {:monitor_unavailable, exit_class()} | {:not_started, exit_class()}}
Starts turn (see PhoenixKitAgent.Turns.Runner) in a task with
deadline_ms to live, and lets it run once it is registered (see "The start
handshake"). callers become the task's $callers, so the process that
started the turn is its caller, as for a Task it starts.
@spec stop_grace_ms() :: non_neg_integer()
The grace period, in ms, a task asked to stop has before it is killed
(:turn_stop_grace_ms, default 30 s).
The PubSub topic of the turn turn_ref: agent:turn:<turn_ref>.