Modules
AI agent module for PhoenixKit.
Files attached to a chat (spec §5.3, §9).
The technical bounds of attachment processing (spec §9): how much of a file
the model is given and how long the work may take. They are not settings of
the module; a host or a test may override them with config :phoenix_kit_agent, attachment_limits: [text_chars: …].
Runs the external programs of attachment processing (pdfinfo, pdftotext,
pdftoppm — poppler) with a time limit, and says plainly when one is not
installed. What a program prints is never returned or logged on failure: it
can quote the file.
What an uploaded file is, told by its content (spec §9: png/jpg/webp/heic, pdf, xlsx/csv, txt/md).
Turns the bytes of an attachment into what the model reads (spec §9): the text of a text file, a CSV file or a PDF; CSV text of a workbook; JPEG or PNG pictures of an image or of the first pages of a scan.
Takes the metadata out of a JPEG, without decoding or re-encoding it.
The work done on an attachment before the turn that sends it (spec §9), in a
task under PhoenixKitAgent.Attachments.TaskSupervisor: the row goes from
pending to ready with what the model will read, or to failed with a
code in error; the chat's topic then hears {:agent_attachment_processed, conversation_uuid, attachment_uuid}.
The places for work on files on the node (spec §9): at most
Bounds concurrent_jobs files are processed at the same time, of whom at
most jobs_per_user are one user's. A file that finds no place waits for
one, in turn, up to queue_wait_ms; with queued_jobs waiting already, or
when the time is up, it is refused ({:error, :unavailable}) and the file
failed as unavailable.
Reads the parts of a workbook out of an uploaded zip without trusting it.
The scratch space of attachment processing: a private directory tree in the
system's temporary directory (pk_agent_att/), where a user's file is copied
to be read and the pages of a scan are rendered.
The module settings (phoenix_kit_settings, keys agent_*) and their
defaults. A per-user value (PhoenixKitAgent.Schemas.UserSettings) wins over
these; these win over the built-in defaults below.
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.
The chats and messages of the assistant (spec §5.1-5.2, §8, §10).
Gettext backend of the agent module's admin pages and permission labels.
The module's own Storage library, where attachment bytes are kept.
The spending and turn limits of a user (spec §7.3) and who is told when one is near or reached (spec §7.4).
Module-owned versioned migrations for phoenix_kit_agent — the protocol core's
mix phoenix_kit.update discovers through PhoenixKitAgent.migration_module/0
(current_version/0, migrated_version_runtime/1, up/1, down/1). Same
shape as PhoenixKitDashboards.Migrations: two version readers, up/1
re-reading the version before it changes anything, the SQL kept as data
(up_statements/2, down_statements/2).
The choice of a model for a user (spec §7.2). The module has no models of its
own: the choice is among the endpoints of phoenix_kit_ai (an endpoint is a
connection plus a model), and only endpoints that can chat.
The system prompt of a turn (spec §7.1), put together in the order of priority, each part separated by a blank line
A file attached to a message (phoenix_kit_agent_attachments). The bytes live
in Storage, in the module's hidden library (file_uuid); the row keeps what
the model reads: the extracted text of a PDF or a sheet, truncated to a limit.
A user's chat with the assistant (phoenix_kit_agent_conversations).
A limit notice that was sent (phoenix_kit_agent_limit_events): at most one per
user, kind and period (period_key is "2026-10-19" for a day, "2026-10" for
a month), which is how a limit is announced once. A warn_80 row names its
limit in the key — "cost:2026-10-19", "cost:2026-10", "turns:2026-10-19" —
so each of the three limits warns once (PhoenixKitAgent.Limits); the
watchers' event of a limit reached is keyed "w:<period>". used and
limit are in the limit's own unit (micro-dollars or turns; none for
ai_budget). user_uuid is set by the caller.
One message of a chat (phoenix_kit_agent_messages): what the user wrote, a
model answer (with the tool calls it asked for), a tool result, or a notice
(a limit, an error, a refusal). The table is also the assistant's journal of
tool calls: who, what, when, and the outcome.
The assistant settings of one user (phoenix_kit_agent_user_settings, keyed by
user_uuid). A nil field means "the module default" (PhoenixKitAgent.Config);
enabled: false switches the assistant off for this user even with the agent
permission.
What users spent on the assistant (spec §7.3), counted from the AI request
journal (phoenix_kit_ai_requests) rather than from the module's own
messages: a row of the user (user_uuid) whose metadata.source is
"agent" — every model call of the module passes both, so a call that
describes an attachment is counted too. The journal is not pruned by age, so
the month never loses rows. The journal keeps a cost in dollars (cost, a
numeric that can be finer than a micro-dollar); the sums are in
micro-dollars ($1 = 1_000_000): each window is added up in dollars and only
then rounded, half up, to a whole micro-dollar
(PhoenixKitAgent.Config.cost_to_micro/1) — two rows of $0.0000004 are
1, not 0. Only endpoints that report a cost add to it.
A turn of the assistant (spec §6.1): the user's message, then rounds of
model calls and tool calls until the model answers, or until a write waits
for the user's confirmation (PhoenixKitAgent.Confirmations).
What a turn shows the model of the chat so far (spec §6.6): the user
messages and the final assistant answers of earlier turns, newest kept
first, up to agent_history_chars. Tool calls and tool results of earlier
turns are left out — after a loss of rights the model must not see old
data — and so are notices, but for one kind: the outcome of a confirmation
card (PhoenixKitAgent.Confirmations) goes in as a user message with the
module's own fixed phrase in the user's language — "[confirmation] done
The two locks of a turn (spec §6.1 step 3), both rows in the database
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 rounds of one turn (spec §6.1, "раунд r = 1..max_tool_rounds"), run in the turn's task. The task first stores the user's message (spec §6.1 step
The processes of the turn loop, started by the host through
PhoenixKitAgent.children/0
How the tools of a round are shown to the model, and how what it sends back is taken (spec §6.1 b, f; §6.2, §6.4).
The assistant settings of one user (spec §5.4, §7) and how they combine with the module's.
What the module does before core deletes a user (before_user_delete/1,
spec §5.7). Core calls it outside the delete's transaction, whether or not the
module is enabled, and a failure here never stops the delete.
The permission checks of the module's admin pages, made again on every event.
The assistant in the admin header (spec §10): a button before the
notifications bell, the list of the user's chats under it, and the chat
panel on the right. A sticky LiveView, rendered by core's admin layout as
the module's header widget (PhoenixKitAgent.header_widgets/0), so it keeps
its state across admin navigation.
The pieces of the chat panel (PhoenixKitAgent.Web.Dock): one message of
the feed, a confirmation card and a file waiting for a message.
The markup of the chat panel (PhoenixKitAgent.Web.Dock) and its browser
hooks: the button with the list of chats, and the panel with the feed, the
indicator and the composer. The LiveView holds the state and the events;
this module only draws them.
What the chat panel (PhoenixKitAgent.Web.Dock) tells a person instead of a
code: the refusals of PhoenixKitAgent.Conversations,
PhoenixKitAgent.Turns and PhoenixKitAgent.Attachments, the codes of a
failed attachment (PhoenixKitAgent.Attachments.Processor), the notices of
a turn (stored in English, shown by their code), the states and refusals of
a confirmation card (PhoenixKitAgent.Confirmations) and the upload errors
of LiveView. A code this module does not know gets a general text, never
the code itself.
The markdown of a model answer as the chat panel shows it (spec §8.2): the text a model wrote may carry what a tool result or a file put in it, so the page must load nothing because of it.
Settings → AI Agent: the module defaults (PhoenixKitAgent.Config, spec
§5.6, §7) — the model the assistant uses and the one that describes
pictures, the per-user limits a user without settings of their own gets,
the limits of one turn, the module's system prompt and who is told when
someone reaches a limit.
Admin → Agent → Spending: what every user spent on the assistant today and
this month, and how many turns they started today — everyone who used it
this month, also a user who no longer holds agent. Money comes from the AI
request journal (PhoenixKitAgent.Spend, micro-dollars shown as dollars;
only endpoints that report a cost add to it), in the day and month of the
site time zone.
The card "What the assistant can use now" of the admin pages: the admin
tools of every module, the ones that read and the ones that write, as core
shows them for a scope (PhoenixKit.API.Executor.tool_availability/1) —
available, or closed and why. The assistant sees exactly the available ones
(PhoenixKitAgent.Turns.Tools: core's visible_for/3 with allow: :all,
no filter of the module's own), and a write still waits for the user's
confirmation.
Admin → Agent → Users: the people who hold the agent permission, each with
the model they use, their limits, what they spent today and this month and
whether a limit being hit is reported — and a form to set one user's
settings (the switch, the model, the admin's prompt, the limits, the notice
flag; PhoenixKitAgent.UserConfig.admin_update/4).