PhoenixKitAgent.Turns.Monitor (PhoenixKitAgent v0.1.0)

Copy Markdown View Source

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 a deadline notice 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 interrupted notice.

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

exit_class()

@type exit_class() :: :timeout | :noproc | :shutdown | :killed | :unavailable | :crash

What kind of exit made a start fail — never the exit term itself, which can hold the whole turn.

Functions

broadcast_stop(turn_ref)

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

check_stop()

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

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

request_stop(pid, reason)

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

start_turn(turn, deadline_ms, callers)

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

stop_grace_ms()

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

topic(turn_ref)

@spec topic(String.t()) :: String.t()

The PubSub topic of the turn turn_ref: agent:turn:<turn_ref>.