Configuration
php artisan packstub-agents:install publishes config/packstub-agents.php. What the app registers in code goes through the Agents facade (below); config is what every runtime (queue, console, MCP requests) reads, so anything a panel plugin sets fluently is mirrored into it.
config/packstub-agents.php
| Key | Default | Env | What it does |
|---|---|---|---|
name |
Assistant |
AGENT_NAME |
how the assistant introduces itself |
panel |
null |
the panel the assistant lives in; set by the Filament plugin when it registers, null otherwise |
|
provider |
anthropic |
AGENT_PROVIDER |
anthropic, openai, gemini or xai have picker entries; any other laravel/ai text provider (ollama, openrouter, mistral, groq, deepseek…) runs on its smartest and cheapest models. The platform default; a workspace may bring its own |
failover |
[] |
AGENT_FAILOVER |
providers to fall back to, in order (gemini,openai), when the platform provider refuses a turn before it started answering; see Failover |
enabled |
null |
AGENT_ENABLED |
null = enabled when a key exists for the provider in use or for the provider of any catalog entry; false switches the agent off (the MCP endpoint stays) |
models |
see below | AGENT_MODEL, AGENT_MODEL_FAST, AGENT_MODEL_DEEP |
the model catalog per provider: model, effort, an optional label (the model's name otherwise) and optionally the provider the entry runs on |
max_steps |
12 |
tool round-trips one turn may take before the agent has to answer | |
max_tokens |
4096 |
answer length | |
max_conversation_messages |
40 |
how many earlier messages a long chat replays | |
middleware |
[] |
your own agent middleware, run on every turn after the package's guard rails; see Middleware | |
history.max_tokens |
24000 |
AGENT_HISTORY_MAX_TOKENS |
the history window, in estimated tokens; what no longer fits is folded into a rolling summary the model reads first |
history.keep_tool_results_turns |
3 |
tool results older than this many turns are replaced by a one-line placeholder when replayed | |
history.notice_share |
0.7 |
from this share of the window a chat surface suggests continuing in a new chat | |
history.meter_share |
0.25 |
from this share of the window a chat surface shows a context meter | |
history.compress_keep_turns |
2 |
how many of the latest exchanges AgentConversationStore::compactNow() keeps verbatim while it folds the rest into the rolling summary |
|
chat.driver |
queue |
AGENT_TURN_DRIVER |
how a turn runs: queue hands the job to a worker, sync runs it inside the request (no worker; an answer ends with the request that asked) |
chat.queue_connection |
null |
AGENT_QUEUE_CONNECTION |
the queue connection the turn job runs on with the queue driver; null = the app's default |
chat.queue |
null |
AGENT_QUEUE |
the queue name; null = the connection's default |
chat.job_timeout |
600 |
AGENT_JOB_TIMEOUT |
how long one turn may run on the worker, in seconds; a turn whose job went quiet for longer is marked failed |
chat.poll_interval |
600 |
AGENT_POLL_INTERVAL |
how often a chat surface asks for the answer so far while a turn runs, in milliseconds |
chat.path |
agents |
where the poll endpoint lives: GET {path}/chat/{conversation}/turn, see Routes |
|
chat.middleware |
['web', 'auth'] |
the middleware of that endpoint; the Filament plugin registers its own on the panel's routes instead | |
chat.keep_turns_days |
90 |
AGENT_KEEP_TURNS_DAYS |
how long ended turns (the per-turn record) are kept; null keeps them; pruned by model:prune --model=Packstub\Agents\Models\AgentTurn |
log.channel |
null |
AGENT_LOG_CHANNEL |
the log channel that gets one line per ended turn (provider, model, tokens, tools, duration, how it ended); null logs nothing. See What each turn cost |
limits.* |
see Budgets and limits | AGENT_TURNS_PER_MINUTE … |
the platform ceiling |
limits_connection |
null |
AGENT_LIMITS_CONNECTION |
the connection of the agent_limits table (the central one in a database-per-tenant app) |
mcp.enabled |
true |
AGENT_MCP_ENABLED |
the MCP endpoint |
mcp.path |
mcp |
the endpoint path; mcp/{tenant} with workspaces |
|
mcp.server |
null |
an AgentServer subclass; null = the package's server with the tools given to Agents::useTools() |
|
mcp.middleware |
['throttle:60,1', 'auth:sanctum', AuthenticateAgent::class] |
the endpoint's middleware | |
run_migrations |
true |
run the package migrations from the vendor directory; false to publish and split them |
The provider keys themselves live in laravel/ai's config/ai.php (ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, XAI_API_KEY, and so on for the other providers).
models
'models' => [
'anthropic' => [
'auto' => ['label' => null, 'model' => env('AGENT_MODEL', 'claude-opus-5'), 'effort' => 'medium'],
'fast' => ['label' => null, 'model' => env('AGENT_MODEL_FAST', 'claude-haiku-4-5'), 'effort' => null],
'deep' => ['label' => null, 'model' => env('AGENT_MODEL_DEEP', 'claude-opus-5'), 'effort' => 'xhigh'],
// 'flash' => ['label' => 'Gemini Flash', 'provider' => 'gemini', 'model' => 'gemini-3.5-flash-lite', 'effort' => 'low'],
// 'local' => ['label' => 'Local', 'provider' => 'ollama', 'model' => 'llama3.3', 'effort' => null, 'failover' => []],
],
'openai' => [
'auto' => ['label' => null, 'model' => env('AGENT_MODEL'), 'effort' => 'medium'],
'fast' => ['label' => null, 'model' => env('AGENT_MODEL_FAST'), 'effort' => 'low'],
'deep' => ['label' => null, 'model' => env('AGENT_MODEL_DEEP'), 'effort' => 'high'],
],
'gemini' => [
'auto' => ['label' => null, 'model' => env('AGENT_MODEL', 'gemini-3.8-flash'), 'effort' => 'medium'],
'fast' => ['label' => null, 'model' => env('AGENT_MODEL_FAST', 'gemini-3.5-flash-lite'), 'effort' => 'low'],
'deep' => ['label' => null, 'model' => env('AGENT_MODEL_DEEP', 'gemini-3.8-flash'), 'effort' => 'high'],
],
'xai' => [
'auto' => ['label' => null, 'model' => env('AGENT_MODEL', 'grok-4.6'), 'effort' => 'medium'],
'fast' => ['label' => null, 'model' => env('AGENT_MODEL_FAST', 'grok-4.6'), 'effort' => 'low'],
'deep' => ['label' => null, 'model' => env('AGENT_MODEL_DEEP', 'grok-4.6'), 'effort' => 'xhigh'],
],
],
Rename, remove or add entries; a picker shows whatever is there. A null label names the entry after the model it runs — Claude Opus 5, Claude Haiku 4.5 — and a second unlabelled entry on the same model adds its key to tell them apart (Claude Opus 5 · Deep); set a label to show something else ('label' => 'Fast'). A null model resolves to the provider's smartest model (or cheapest for the fast key), and the entry is named after the model that resolves. A provider with no entries at all (Ollama, OpenRouter, Mistral, Groq, DeepSeek…) gets its smartest (auto) and cheapest (fast) models with no effort; add an entry to pin models or to offer a deep one. Effort is passed as Anthropic's output_config.effort, OpenAI's and xAI's reasoning.effort (reasoning models only) or Gemini's thinking level (low, medium, high; xhigh is sent as high). When you pin a model that rejects the parameter, set its effort to null.
The catalog is the list of the provider in use (provider, or the workspace's own). An entry in that list may name another provider to run on — 'provider' => 'gemini' on the commented flash entry above puts Gemini Flash next to Claude on an Anthropic install. Such an entry is listed only when its provider has a key in config/ai.php; when the catalog holds entries of more than one provider, AgentModels::groups() groups them under provider headings, the catalog's own provider first. Its model and effort are in that provider's terms (a Gemini thinking level on a Gemini entry), and it fails over down the failover list like any entry, with its own provider left out — the platform provider included when it is listed. Give an entry its own failover list to override the global one: [] keeps a local Ollama model local, for data that must not leave the server. A workspace on its own key sees only the entries of its provider; see Tenancy. The agent is on when the provider in use has a key, or when any listed entry's provider has one.
The Agents facade
Packstub\Agents\Facades\Agents is how the app registers itself, from a service provider's boot():
use Packstub\Agents\Facades\Agents;
Agents::useAgent(Assistant::class);
Agents::useServer(AcmeServer::class);
Agents::useTools([SearchOrders::class, ConfirmOrder::class]);
Agents::useResources([Orders::class, Customers::class]);
Agents::useMiddleware([AuditTurns::class]);
Agents::authorizeUsing(fn (string $ability): bool => auth()->user()->can($ability));
Agents::roleLabelUsing(fn (): ?string => auth()->user()->role?->getLabel());
Agents::credentialsUsing(fn (): ?WorkspaceCredentials => ...);
Agents::limitsAuthorizeUsing(fn (): bool => auth()->user()->is_admin);
Agents::tenantModel(Team::class, slugAttribute: 'slug');
Agents::tenantUsing(fn (): ?Team => auth()->user()?->currentTeam);
Agents::enteringTenant(fn (Team $team): ?Closure => ...);
| Method | |
|---|---|
useAgent(class) |
your Agent subclass (default: the package's DefaultAgent) |
useServer(class) |
the AgentServer subclass with the tool list, name and instructions |
useTools(array) |
the tool list when there is no server class |
addTools(array|Closure) |
tools appended to the server's own list (what the Filament plugin uses for show-table) |
useResources(array) |
the AgentResource classes for filters, summaries and page context |
useMiddleware(array) |
your own agent middleware — classes with handle(AgentPrompt $prompt, Closure $next), instances or closures — run on every turn after the package's guard rails, after the ones in config; see Middleware |
authorizeUsing(fn (string $ability): bool) |
how a tool's ability is checked for the current person (default: the Gate when it has that ability, otherwise allowed) |
roleLabelUsing(fn (): ?string) |
the person's role label for the prompt and refusals |
credentialsUsing(fn (): ?WorkspaceCredentials) |
where a workspace's own provider, key and model come from |
limitsAuthorizeUsing(fn (): bool) |
who may edit the agent_limits rows; read back with canManageLimits() |
tenantModel(class, ?slugAttribute) |
the workspace model, and the attribute the MCP path names it by |
tenantUsing(fn (): ?Model) |
how the current workspace is found; unregistered, the app is one workspace |
enteringTenant(fn (Model $tenant): ?Closure) |
what a worker or an MCP request does on entering a workspace; the returned closure runs on leaving |
It also reads back what the app told the package: name(), tenant(), inPanel(), toolClasses(), agentClass(), serverClass(), resourceClasses(), middleware(), allows($ability), roleLabel(), credentials(), canManageLimits(), and context() — the AgentContext that knows who is acting and where (Support\Context\LaravelContext, or the Filament plugin's FilamentContext in a panel). Tools use it; your own code may too.
In a Filament panel, AgentsPlugin::make() has a fluent method for each of these and adds the pages; see Filament Agents.
Translations
Strings are __() calls keyed by the English text, with JSON files for German, Spanish, Romanian and Russian in resources/lang. Add your own language by publishing a JSON file with the same keys into your app's lang/ directory.