Packstub.

Budgets and limits

A chat that calls a frontier model on every question needs a ceiling. The package counts what laravel/ai already stores with every assistant message (the provider's token usage) and refuses a turn before it reaches the provider when a limit is hit. The provider's own spend limit stays the real backstop.

The limits

Limit Scope Config key / env
Questions per minute per user turns_per_minute / AGENT_TURNS_PER_MINUTE (6)
Answers per day per workspace turns_per_day / AGENT_TURNS_PER_DAY (150)
Tokens per month per workspace, all token kinds tokens_per_month / AGENT_TOKENS_PER_MONTH (3,000,000)
Tokens per day per user, inside the workspace user_tokens_per_day / AGENT_USER_TOKENS_PER_DAY (100,000)
Tokens per month per user, inside the workspace user_tokens_per_month / AGENT_USER_TOKENS_PER_MONTH (1,500,000)
Max question length characters prompt_max_chars / AGENT_PROMPT_MAX_CHARS (2,000)

null (or 0 in the environment) disables a limit. The values in config/packstub-agents.php are the platform's ceiling; the operator page below overrides them.

When a turn is refused the person sees the reason in the chat ("This workspace reached today's limit of 150 answers. It resets at midnight.") and nothing is sent to the provider.

The AI limits resource

An operator panel (the central panel of a SaaS, or the admin panel of a single app) registers the limits resource:

AgentsPlugin::make()
    ->chat(false)
    ->agentAccess(false)
    ->limits(authorize: fn () => (bool) auth()->user()?->is_admin)

The AI limits resource on an operator panel: platform defaults, two workspace rows and one user switched off

AI limits then lists rows with three scopes:

Scope Applies to Fields
Everyone (global) every workspace and user all of them, plus an on/off switch
One workspace one tenant (only offered in a panel with tenancy) all of them
One user one account, in every workspace the per-user fields: on/off, questions per minute, tokens per day and per month, max question length

Empty fields inherit: user → workspace → everyone → the config defaults. The Assistant switch on a workspace row turns the chat off for that workspace entirely (the pages and buttons hide themselves); on a user row it does the same for one person.

Rows live in the agent_limits table on packstub-agents.limits_connection (AGENT_LIMITS_CONNECTION), the central connection in a database-per-tenant app, since limits are the operator's, not the workspace's. Resolved limits are cached for the request; the resource flushes the cache after every edit.

In code

use Packstub\Agents\Support\AgentBudget;
use Packstub\Agents\Support\AgentLimits;

AgentLimits::effective();          // the merged limits for the current tenant and user
AgentLimits::effective($tenant, $user);
AgentBudget::refusal($prompt);     // the reason a turn may not start, or null
AgentBudget::summary();            // turns today, tokens this month, per-user counters and their limits

AgentBudget::summary() is what a workspace settings page shows next to "your AI usage this month". Every counter comes from agent_conversation_messages, so no extra bookkeeping is needed.