What the AI task guard noticed (PhoenixKitAI.GuardEvent) and who is told.
Once per window
An event that is signalled once per window carries a dedupe_key naming
the window; the key is unique, the row is inserted with ON CONFLICT DO NOTHING, and only the insert that went through signals:
| event | key |
|---|---|
| a limit at its warn percent | limit_warning:<task>:<user or ->:<unit>:<period key> |
| a limit reached | limit_reached:<task>:<user or ->:<unit>:<period key> |
a PhoenixKitAI.Budget refusal | limit_reached:budget:<scope>:<user, endpoint or ->:<local date> |
| a model that no longer fits a task | model_unfit:<task>:<endpoint>:<local date> |
| an off-purpose flag | off_purpose:<task>:<user or ->:<turn_ref or request_uuid> |
| the watchers told of a person's flags | off_purpose_signal:<task>:<user or ->:<local date> |
| an anomaly rule broken | anomaly:<task>:<user or ->:<rule>:<local date> |
The period key is the local date (2026-10-22) or month (2026-10) of the
site time zone. A key that was inserted (here or, seen as a conflict, on
another node) is also kept on this node until its window ends, so a limit
that stays past its threshold costs no further write. A forgiving
(limit_forgiven), a block (blocked) and a lift (unblocked) have no
key: every one is kept. After forgiving, the new count's start is part of
the key, so its warning and reached signals can fire again in the same
window.
Who is told
The watchers (watchers/2): the setting ai_guard_watchers (a JSON list of
user uuids, changed with set_watchers/2; empty — every Owner) and the
people the task's watchers callback names for the user, without the user
themself. They get a notification of type "ai_guard"
(PhoenixKit.Notifications.create_many/2, which leaves out a watcher who
switched the type off), in the site's content language. A user's own limit
is told to the user in the app (PhoenixKit.Notifications.upsert_inapp/3):
at the warn percent the user only, when reached the user and the watchers.
Each signalled event is written to the Activity log as guard.<kind> with
the event as its resource (resource_uuid; never target_uuid, which
would notify the user a second time). A text names the task, the user by
e-mail and the limit with its numbers — never a reason or anything of a
call's content. The reason of an off-purpose flag stays in its event row
(flags/2 shows it to those allowed); a block's reason and a lift's note
stay in the block row.
A signal never fails the call that caused it: a failure is logged as its class only.
Summary
Functions
An anomaly rule of spec broken by the call of request (rule:
"source", "calls_per_hour" or "tokens_per_call", with the value
seen and the task's limit): an event and the watchers told, once a day per
task, person and rule.
A block set or lifted (kind: "blocked" or "unblocked") by
actor_uuid: the event, the Activity entry, the watchers (but the actor)
told and the person told in the app — none of them with the reason or the
note.
A PhoenixKitAI.Budget cap refused a call: a limit_reached event (scope
budget:<scope>) and the watchers told, once per local day.
The configured watchers (ai_guard_watchers); empty means every Owner.
The off-purpose flags of a task in the last days (default 30), for the
Tasks page as viewer (a scope) may see them: the count per person (most
first) and the latest limit flags (default 20). A flag's reason and its
journal row are left out (hidden: true) of a private task's flags unless
the viewer may manage the guard (PhoenixKitAI.Guard.reasons_visible?/2).
An amount of a unit for display: dollars for money, a count otherwise.
Dollars with at least two decimals: Decimal.new("1.5") is "1.50".
A limit forgiven: the count of scope and period starts again at now.
Written to the Activity log with who forgave it; nobody is notified.
A limit in words, e.g. "daily request limit".
A limit of spec reached (see the moduledoc).
A limit of spec at its warn percent (see the moduledoc).
The model of endpoint no longer has what spec needs (missing): an
event and the watchers told, once a day per task and endpoint.
The notification type of the guard's signals.
An off-purpose flag of spec for user_uuid (nil for none): one event
per turn or call (ref: "turn_ref" or "request_uuid"; the request
uuid is linked when its row exists), and on the person's first flag in the
task that day, an off_purpose_signal and the watchers told — without the
reason. {:ok, event}, or :duplicate when the turn or call was flagged.
The limit events of the current windows of a task (for the Tasks page).
Inserts an event. {:ok, event} when it was written, :duplicate when its
dedupe_key was already taken — known on this node (kept until :until,
default the end of the local day) or found taken in the database.
An anomaly rule in words.
Sets the watchers (ai_guard_watchers); an empty list means every Owner.
A uuid of nobody is left out, with a warning, and the rest are saved; the
answer lists who was saved. Needs ai.manage; written to the Activity log
with who changed it.
Who is told about spec (nil for no task) and user_uuid: the configured
watchers (or every Owner) and the task's own, without the user.
The setting holding the watchers.
Functions
@spec anomaly( PhoenixKitAI.TaskSpec.t(), PhoenixKitAI.Request.t(), String.t(), term(), term(), DateTime.t() ) :: :ok
An anomaly rule of spec broken by the call of request (rule:
"source", "calls_per_hour" or "tokens_per_call", with the value
seen and the task's limit): an event and the watchers told, once a day per
task, person and rule.
@spec block_event(String.t(), PhoenixKitAI.GuardBlock.t(), String.t() | nil) :: :ok
A block set or lifted (kind: "blocked" or "unblocked") by
actor_uuid: the event, the Activity entry, the watchers (but the actor)
told and the person told in the app — none of them with the reason or the
note.
@spec budget_refused( PhoenixKitAI.Budget.scope(), PhoenixKitAI.Endpoint.t(), keyword(), DateTime.t() ) :: :ok
A PhoenixKitAI.Budget cap refused a call: a limit_reached event (scope
budget:<scope>) and the watchers told, once per local day.
@spec configured_watchers() :: [String.t()]
The configured watchers (ai_guard_watchers); empty means every Owner.
@spec flags(PhoenixKitAI.TaskSpec.t(), term(), keyword()) :: %{ counts: [{String.t() | nil, pos_integer()}], flags: [map()] }
The off-purpose flags of a task in the last days (default 30), for the
Tasks page as viewer (a scope) may see them: the count per person (most
first) and the latest limit flags (default 20). A flag's reason and its
journal row are left out (hidden: true) of a private task's flags unless
the viewer may manage the guard (PhoenixKitAI.Guard.reasons_visible?/2).
@spec format_amount( PhoenixKitAI.TaskLimits.unit(), PhoenixKitAI.TaskLimits.amount() | :unmigrated ) :: String.t()
An amount of a unit for display: dollars for money, a count otherwise.
Dollars with at least two decimals: Decimal.new("1.5") is "1.50".
@spec limit_forgiven( String.t(), String.t() | nil, PhoenixKitAI.TaskLimits.scope(), PhoenixKitAI.TaskLimits.period(), String.t() | nil, DateTime.t() ) :: {:ok, PhoenixKitAI.GuardEvent.t()} | :duplicate
A limit forgiven: the count of scope and period starts again at now.
Written to the Activity log with who forgave it; nobody is notified.
@spec limit_name(PhoenixKitAI.TaskLimits.unit(), PhoenixKitAI.TaskLimits.period()) :: String.t()
A limit in words, e.g. "daily request limit".
@spec limit_reached( PhoenixKitAI.TaskSpec.t(), String.t() | nil, PhoenixKitAI.TaskLimits.status() ) :: :ok
A limit of spec reached (see the moduledoc).
@spec limit_warning( PhoenixKitAI.TaskSpec.t(), String.t() | nil, PhoenixKitAI.TaskLimits.status() ) :: :ok
A limit of spec at its warn percent (see the moduledoc).
@spec model_unfit( PhoenixKitAI.TaskSpec.t(), PhoenixKitAI.Endpoint.t(), [PhoenixKitAI.TaskSpec.need()], DateTime.t() ) :: :ok
The model of endpoint no longer has what spec needs (missing): an
event and the watchers told, once a day per task and endpoint.
@spec notice_type() :: String.t()
The notification type of the guard's signals.
@spec off_purpose( PhoenixKitAI.TaskSpec.t(), String.t() | nil, String.t() | nil, map(), DateTime.t() ) :: {:ok, PhoenixKitAI.GuardEvent.t()} | :duplicate
An off-purpose flag of spec for user_uuid (nil for none): one event
per turn or call (ref: "turn_ref" or "request_uuid"; the request
uuid is linked when its row exists), and on the person's first flag in the
task that day, an off_purpose_signal and the watchers told — without the
reason. {:ok, event}, or :duplicate when the turn or call was flagged.
@spec reached_users(String.t(), DateTime.t()) :: [PhoenixKitAI.GuardEvent.t()]
The limit events of the current windows of a task (for the Tasks page).
@spec record(map()) :: {:ok, PhoenixKitAI.GuardEvent.t()} | :duplicate
Inserts an event. {:ok, event} when it was written, :duplicate when its
dedupe_key was already taken — known on this node (kept until :until,
default the end of the local day) or found taken in the database.
An anomaly rule in words.
@spec set_watchers(term(), [String.t()]) :: {:ok, [String.t()]} | {:error, :forbidden | :invalid_uuid | :too_long | term()}
Sets the watchers (ai_guard_watchers); an empty list means every Owner.
A uuid of nobody is left out, with a warning, and the rest are saved; the
answer lists who was saved. Needs ai.manage; written to the Activity log
with who changed it.
@spec watchers(PhoenixKitAI.TaskSpec.t() | nil, String.t() | nil) :: [String.t()]
Who is told about spec (nil for no task) and user_uuid: the configured
watchers (or every Owner) and the task's own, without the user.
@spec watchers_setting() :: String.t()
The setting holding the watchers.