# `PhoenixKitAI.GuardEvents`
[🔗](https://github.com/BeamLabEU/phoenix_kit_ai/blob/v0.26.1/lib/phoenix_kit_ai/guard_events.ex#L1)

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.

# `anomaly`

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

# `block_event`

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

# `budget_refused`

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

# `configured_watchers`

```elixir
@spec configured_watchers() :: [String.t()]
```

The configured watchers (`ai_guard_watchers`); empty means every Owner.

# `flags`

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

# `format_amount`

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

# `format_usd`

```elixir
@spec format_usd(Decimal.t()) :: String.t()
```

Dollars with at least two decimals: `Decimal.new("1.5")` is `"1.50"`.

# `limit_forgiven`

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

# `limit_name`

```elixir
@spec limit_name(PhoenixKitAI.TaskLimits.unit(), PhoenixKitAI.TaskLimits.period()) ::
  String.t()
```

A limit in words, e.g. "daily request limit".

# `limit_reached`

```elixir
@spec limit_reached(
  PhoenixKitAI.TaskSpec.t(),
  String.t() | nil,
  PhoenixKitAI.TaskLimits.status()
) ::
  :ok
```

A limit of `spec` reached (see the moduledoc).

# `limit_warning`

```elixir
@spec limit_warning(
  PhoenixKitAI.TaskSpec.t(),
  String.t() | nil,
  PhoenixKitAI.TaskLimits.status()
) ::
  :ok
```

A limit of `spec` at its warn percent (see the moduledoc).

# `model_unfit`

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

# `notice_type`

```elixir
@spec notice_type() :: String.t()
```

The notification type of the guard's signals.

# `off_purpose`

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

# `reached_users`

```elixir
@spec reached_users(String.t(), DateTime.t()) :: [PhoenixKitAI.GuardEvent.t()]
```

The limit events of the current windows of a task (for the Tasks page).

# `record`

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

# `rule_text`

```elixir
@spec rule_text(String.t(), term()) :: String.t()
```

An anomaly rule in words.

# `set_watchers`

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

# `watchers`

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

# `watchers_setting`

```elixir
@spec watchers_setting() :: String.t()
```

The setting holding the watchers.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
