# `PhoenixKitAgent.Limits`
[🔗](https://github.com/BeamLabEU/phoenix_kit_agent/blob/v0.1.0/lib/phoenix_kit_agent/limits.ex#L1)

The spending and turn limits of a user (spec §7.3) and who is told when one
is near or reached (spec §7.4).

## What is counted

  * Money — `PhoenixKitAgent.Spend`: the user's rows of the AI request
    journal whose `metadata.source` is `"agent"`, so a call that describes
    an attachment counts as well as a model call of a turn; every row with
    a cost, a failed call's included. Micro-dollars, `$1 = 1_000_000`.
  * Turns — the user's `user` messages with a `turn_ref` (one per turn), in
    any of their chats.

The windows are the calendar day and month of the site time zone. The
limits in force are `PhoenixKitAgent.UserConfig.effective/1`'s: the
user's own, else the module's. A limit of `0` allows nothing.

The money limits apply only on an endpoint whose calls get what they cost
in the journal (`PhoenixKitAgent.Models.reports_cost?/2`: a model call of a
turn, or a picture's description on the vision endpoint, where the provider
reports a cost or `phoenix_kit_ai` has a price for the model): on another,
nothing would be counted, so only the turn limit applies there — the admin
pages say so for the default model. An endpoint that cannot be found keeps
the money limits.

## When

`check/3` runs before a turn, while the turn holds its locks
(`PhoenixKitAgent.Turns`), so the previous turn of the user — and every
journal row it wrote — is over by then. A turn that started under its
limits runs to its end, even past them.

## Notices

Each is sent once per limit and period: a row of
`phoenix_kit_agent_limit_events` (`user, kind, period`, unique) is inserted
first with `ON CONFLICT DO NOTHING`, and only an insert that went through
sends anything.

  * 80 % of a limit (`check/3`) — an in-app notice to the user
    (`PhoenixKit.Notifications.upsert_inapp/3`, key
    `agent:limit:<kind>:<period>`). Event kind `warn_80`, period key
    `cost:<period>` or `turns:<period>`, so each of the three limits warns
    on its own.
  * A limit reached (`announce_reached/2`) — the in-app notice to the user
    (the same key, so an unread warning becomes the new text), and, only
    when the user's `notify_on_limit` is on, a notification of type
    `"agent_limit"` to the watchers (`create_many/2`, which respects a
    watcher who switched that type off). The two have their own events —
    the watchers' period key is `w:<period>`, written only when they are
    told — so `notify_on_limit` switched on later in the period still
    tells the watchers of that period, once.

A call refused by the `phoenix_kit_ai` budget is signalled by
`phoenix_kit_ai` itself (its `limit_reached` event, to its watchers and
to those of the task, `guard_watchers/1`); the turn's notice in the chat
says the budget is used up.

The watchers are `agent_limit_watchers` (empty: every Owner) and the user's
own `notify_user_uuids`, without the user, who is told as the user.

Notices name the user by e-mail and the limit with its numbers; nothing of a
chat. A notice that cannot be sent is logged as the class of the failure.

# `kind`

```elixir
@type kind() :: :daily_cost | :monthly_cost | :daily_turns
```

A limit of a user.

# `status`

```elixir
@type status() :: %{
  kind: kind(),
  used: non_neg_integer(),
  limit: non_neg_integer(),
  period_key: String.t()
}
```

A limit as it stands: what was used of it in its period.

# `usage`

```elixir
@type usage() :: %{
  day_micro_usd: non_neg_integer(),
  month_micro_usd: non_neg_integer(),
  day_turns: non_neg_integer(),
  day_key: String.t(),
  month_key: String.t()
}
```

What a user used today and this month, and the keys of those periods.

# `announce_reached`

```elixir
@spec announce_reached(PhoenixKitAgent.UserConfig.effective(), status()) :: :ok
```

Tells that the user of `settings` reached a limit (`status` from
`check/3`): once per limit and period — the user in the app, and the
watchers when the user's `notify_on_limit` is on.

# `check`

```elixir
@spec check(PhoenixKitAgent.UserConfig.effective(), String.t() | nil, keyword()) ::
  :ok | {:error, {:limit_reached, status()}}
```

Whether the user of `settings` (`UserConfig.effective/1`) may start a turn
on `endpoint_uuid` now: `:ok`, or `{:error, {:limit_reached, status}}` for
the first limit used up — the month's money, the day's money, the day's
turns. On `:ok`, a limit at 80 % or more is announced to the user (once per
period). Sends nothing about a limit reached: that is
`announce_reached/2`.

Options: `:use` — what the call on the endpoint is (`:chat`, the default,
or `:vision`, see `money_counted?/2`); `:now`.

# `guard_watchers`

```elixir
@spec guard_watchers(String.t() | nil) :: [String.t()]
```

The watchers the module adds to the AI guard's own for its tasks
(`watchers` of `PhoenixKitAgent.ai_tasks/0`, called with the user's uuid
or nil): `agent_limit_watchers`, plus the user's `notify_user_uuids` when
their `notify_on_limit` is on, without the user. An empty
`agent_limit_watchers` adds nobody: the guard tells its own watchers
(every Owner by default) anyway.

# `money_counted?`

```elixir
@spec money_counted?(String.t() | nil, PhoenixKitAgent.Models.call_kind()) ::
  boolean()
```

Whether money counts for a `call` (`:chat` or `:vision`) on
`endpoint_uuid` (see the moduledoc): an endpoint whose such calls get a
cost (`PhoenixKitAgent.Models.reports_cost?/2`), or one that cannot be
found.

# `usage`

```elixir
@spec usage(String.t(), DateTime.t()) :: usage()
```

What `user_uuid` used in the day and month of `now` (see the moduledoc).

# `watchers`

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

Who is told about the user's limits: `agent_limit_watchers` (empty: every
Owner) and the user's own `notify_user_uuids`, without the user.

---

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