Dashboard
Built-in web UI at /ai-tasks — runs list with stats, filters, and per-run detail (request, response, tokens, cost).

'dashboard' => [ 'enabled' => env('AI_DASHBOARD_ENABLED', true), 'path' => env('AI_DASHBOARD_PATH', 'ai-tasks'), 'middleware' => ['web'], 'poll_interval' => env('AI_DASHBOARD_POLL', 3), // seconds; 0 = off 'theme' => env('AI_DASHBOARD_THEME', 'system'), // light|dark|system 'per_page' => env('AI_DASHBOARD_PER_PAGE', 50), 'stuck_after_minutes' => env('AI_DASHBOARD_STUCK_AFTER', 15),],The theme toggle in the UI is saved in the browser and overrides theme. Request and prompt contents are shown only when store_request is enabled.
To customize the views:
php artisan vendor:publish --tag=ai-viewsDriver state
Section titled “Driver state”Above the filters, one row per driver (those with an API key, plus any that ran in the last 24 hours): its current state, last successful answer, last error, and runs / errors / average duration over 24 hours.
The state comes from consecutive transient failures — connection errors, timeouts, 429, 5xx — counted in the cache: degraded after one, down after three, back to ok on the next answer, unknown before any data.
A queued run that switched to a fallback driver records only the driver that answered in ai_runs, so this is where the failure of the first one shows up. Rejected requests (4xx) and runs with a tenant’s own key (providerOverride) don’t count: they say nothing about the shared driver.
Stuck runs
Section titled “Stuck runs”A run is stuck once it has been queued or running for longer than stuck_after_minutes without progress. The usual cause is a queue payload that never reached a worker (a Redis restart between AI::queue() writing the row and the worker picking it up), or a queue no worker consumes: nothing is left to fail the run, so it stays queued forever and no retry reaches it.
A delayed run is measured from its due time, not from when it was queued.
Stuck runs get their own stat card, a stuck status filter and a badge on the row. Raise the threshold above your slowest task’s runtime, or long legitimate runs are flagged too.
From code: AiRun::stuck() scope and $run->isStuck().
Actions
Section titled “Actions”Available on each row and on the run page:
- Retry — rebuilds the task from
ai_runs.requestand re-dispatches it, reusing the same row. Available forerror/deadruns (except a failed sync attempt that a fallback driver covered) and for stuckqueued/runningones; arunningrun that isn’t stuck is left alone — a worker is still on it. Requiresstore_requestto have been enabled when the run was recorded, otherwise there are no constructor arguments to revive. - Dead — closes a run you’ve given up on:
status = dead, with the reason inerror. Fires noAiRunFailedevent — the actual failure happened earlier and silently, and listeners shouldn’t be notified about an admin’s click.
The same from the CLI, including stuck runs:
php artisan ai:retry --since=24h --stuck --dry-runSee ai:retry.