How to Build a Background AI Agent in Laravel
Prerequisites: Laravel 11+, PHP 8.2+, familiarity with Artisan commands and database migrations. Time to complete: ~50 minutes. Companion repository: github.com/buildwithabdallah/laravel-background-agent
What This Tutorial Covers
This is a production-style demonstration of building a background AI agent in Laravel. The agent runs as a supervised daemon process, uses an LLM for reasoning, stores observations in the database, and sends Telegram notifications.
By the end, you will have:
- An Artisan command that runs a background agent loop
- A tool registry with allowlisted, controlled actions
- Persistent memory stored in SQLite/MySQL
- Action logging and rate limiting
- Telegram alerts for agent activity
- Supervisor/systemd deployment configuration
- A realistic example: monitoring a website or RSS feed, summarizing changes, and alerting on Telegram
How This Differs From Existing Resources
Laravel already has excellent AI tooling:
- Laravel AI SDK — first-party SDK for building AI-powered features with tools and structured output.
- Building AI Agents with Laravel (Official Blog) — introduction to agent concepts using the Laravel AI SDK.
- Prism — elegant LLM abstraction for OpenAI, Anthropic, and Ollama.
- LarAgent — agent orchestration with local LLMs via Ollama.
This tutorial goes further by focusing on background execution, safety, and observability:
| Feature | Laravel AI SDK / LarAgent Tutorials | This Tutorial |
|---|---|---|
| Background loop | One-shot execution | Continuous daemon with Supervisor |
| Persistent memory | In-memory or session | Database-backed, survives restarts |
| Controlled tools | Open tool calling | Allowlisted + validated + logged |
| Action logs | Basic output | Full audit trail per step |
| Telegram alerts | Not covered | Native notification channel |
| Safety guardrails | Not emphasized | Rate limits, approval gates, no shell |
| Deployment | Manual execution | Supervisor + systemd ready |
Architecture Overview
┌─────────────────────────────────────────────────────────────┐
│ Supervisor/Systemd │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Laravel Artisan Command │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────┐ │ │
│ │ │ Agent │───▶│ LLM │◀───│ Memory │ │ │
│ │ │ Loop │ │ (OpenAI/ │ │ (DB) │ │ │
│ │ │ │◀───│ Ollama) │───▶│ │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Tool │───▶│ Action │ │ │
│ │ │ Registry │ │ Logger │ │ │
│ │ │(allowlisted)│ │ (DB) │ │ │
│ │ └─────────────┘ └─────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────┐ │ │
│ │ │ Telegram │ │ │
│ │ │ Notification│ │ │
│ │ └─────────────┘ │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Execution flow:
- Supervisor keeps the Artisan command running
- Agent receives a goal (e.g., "Monitor example.com hourly")
- Agent queries LLM for reasoning + tool selection
- Only allowlisted tools execute; all calls are logged
- Observations stored in database (memory)
- Telegram notification sent on completion or anomaly
- Loop sleeps, then repeats
Project Setup
Step 1: Create Laravel Project
composer create-project laravel/laravel background-agent-demo
cd background-agent-demo
Step 2: Install Laravel AI SDK
The Laravel AI SDK provides a clean, first-party interface for LLM interactions:
composer require laravel/ai
php artisan vendor:publish --provider="Laravel\Ai\AiServiceProvider"
Add credentials to .env:
# Option A: Cloud LLM
OPENAI_API_KEY=sk-...
# Option B: Local LLM via Ollama (free, runs on your machine)
OLLAMA_BASE_URL=http://localhost:11434
OLLAMA_MODEL=llama3.2:3b
See Laravel AI SDK docs for full configuration options.
Step 3: Install Telegram Notifications
composer require laravel-notification-channels/telegram
Add to .env:
TELEGRAM_BOT_TOKEN=your_bot_token
TELEGRAM_CHAT_ID=your_chat_id
Step 4: Create Database Tables
php artisan make:migration create_agent_runs_table
php artisan make:migration create_agent_logs_table
php artisan make:migration create_agent_memory_table
agent_runs:
Schema::create('agent_runs', function (Blueprint $table) {
$table->id();
$table->string('goal');
$table->text('plan')->nullable();
$table->integer('steps_taken')->default(0);
$table->string('status')->default('running'); // running, completed, failed, paused
$table->json('context')->nullable();
$table->text('summary')->nullable();
$table->timestamps();
});
agent_logs:
Schema::create('agent_logs', function (Blueprint $table) {
$table->id();
$table->foreignId('agent_run_id')->constrained()->onDelete('cascade');
$table->string('step');
$table->string('tool')->nullable();
$table->text('input')->nullable();
$table->text('output')->nullable();
$table->string('level')->default('info'); // info, warning, error
$table->timestamps();
});
agent_memory:
Schema::create('agent_memory', function (Blueprint $table) {
$table->id();
$table->string('category')->default('general');
$table->string('key')->index();
$table->text('value');
$table->timestamps();
});
php artisan migrate
The Agent Core: Safe Reasoning and Tool Execution
Why Safety First
The agent receives instructions from an LLM. Without guardrails, a malformed response could execute unintended actions. This implementation uses:
- Allowlist-only tools — The agent cannot execute arbitrary code
- Structured JSON parsing — Prevents prompt injection from executing commands
- Rate limiting — Prevents runaway LLM calls
- Action logging — Every step is auditable
- No shell execution — Tools are PHP classes, not system commands
Agent Class
php artisan make:class Services/Agent/BackgroundAgent
<?php
namespace App\Services\Agent;
use Laravel\Ai\Ai;
use App\Models\AgentRun;
use App\Models\AgentLog;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\RateLimiter;
class BackgroundAgent
{
private ToolRegistry $tools;
private AgentRun $run;
private array $context = [];
private int $maxSteps = 8;
private int $currentStep = 0;
public function __construct(ToolRegistry $tools)
{
$this->tools = $tools;
}
public function execute(string $goal): AgentRun
{
$this->run = AgentRun::create([
'goal' => $goal,
'status' => 'running',
]);
$this->log('info', 'Agent started', ['goal' => $goal]);
$this->context = [
['role' => 'system', 'content' => $this->systemPrompt()],
['role' => 'user', 'content' => "Task: {$goal}"],
];
while ($this->currentStep < $this->maxSteps) {
if (!$this->checkRateLimit()) {
$this->log('warning', 'Rate limit exceeded, pausing');
break;
}
$decision = $this->think();
if (isset($decision['complete'])) {
$this->complete($decision['complete']);
return $this->run;
}
if (isset($decision['tool'])) {
$result = $this->act($decision['tool'], $decision['input'] ?? '');
$this->observe($result);
$this->currentStep++;
$this->run->increment('steps_taken');
}
if (isset($decision['error'])) {
$this->log('error', 'LLM returned invalid decision', ['error' => $decision['error']]);
break;
}
}
$this->fail('Maximum steps reached or rate limit exceeded');
return $this->run;
}
private function systemPrompt(): string
{
$toolList = $this->tools->descriptions();
return <<<PROMPT
You are a supervised background agent running in a Laravel application.
You help users by monitoring sources, summarizing information, and sending notifications.
You have access to these tools:
{$toolList}
Rules:
1. You may ONLY use the tools listed above.
2. You CANNOT execute shell commands, modify files directly, or make financial decisions.
3. You CAN monitor websites, read RSS feeds, summarize text, store observations, and send notifications.
4. You CAN draft summaries and recommendations for human review.
5. You CANNOT take destructive actions without explicit human approval.
Response format (JSON only):
- To use a tool: {"tool": "tool_name", "input": "tool input"}
- When finished: {"complete": "summary of what was accomplished"}
- If stuck: {"complete": "Unable to complete: [reason]"}
Be concise. Prefer facts over speculation.
PROMPT;
}
private function think(): array
{
try {
$response = Ai::chat()
->withModel(config('ai.model', 'gpt-4o-mini'))
->withMessages($this->context)
->send();
$content = $response->text();
$this->log('info', 'LLM response', ['content' => $content]);
return $this->parseDecision($content);
} catch (\Exception $e) {
$this->log('error', 'LLM call failed', ['error' => $e->getMessage()]);
return ['error' => $e->getMessage()];
}
}
private function parseDecision(string $content): array
{
// Extract JSON block if wrapped in markdown
if (preg_match('/```json\s*(.*?)\s*```/s', $content, $matches)) {
$content = $matches[1];
}
$decoded = json_decode($content, true);
if (!is_array($decoded)) {
// Fallback: treat as complete if no JSON found
return ['complete' => $content];
}
if (isset($decoded['tool']) && isset($decoded['input'])) {
// Validate tool exists
if (!$this->tools->exists($decoded['tool'])) {
return ['complete' => "Tool '{$decoded['tool']}' not in allowlist. Stopping."];
}
return $decoded;
}
if (isset($decoded['complete'])) {
return $decoded;
}
return ['error' => 'Unrecognized response format'];
}
private function act(string $toolName, string $input): string
{
$this->log('info', 'Executing tool', ['tool' => $toolName, 'input' => $input]);
$tool = $this->tools->get($toolName);
if (!$tool) {
return "Error: Tool '{$toolName}' not found in registry.";
}
try {
$result = $tool->execute($input);
$this->log('info', 'Tool completed', ['tool' => $toolName, 'output' => substr($result, 0, 500)]);
return $result;
} catch (\Exception $e) {
$this->log('error', 'Tool failed', ['tool' => $toolName, 'error' => $e->getMessage()]);
return "Error: {$e->getMessage()}";
}
}
private function observe(string $result): void
{
$this->context[] = [
'role' => 'assistant',
'content' => json_encode(['observation' => $result]),
];
}
private function checkRateLimit(): bool
{
return RateLimiter::attempt(
'agent-llm-calls',
10, // attempts
fn() => true,
60 // per minute
);
}
private function complete(string $summary): void
{
$this->run->update([
'status' => 'completed',
'summary' => $summary,
'context' => $this->context,
]);
$this->log('info', 'Agent completed', ['summary' => $summary]);
}
private function fail(string $reason): void
{
$this->run->update([
'status' => 'failed',
'summary' => $reason,
]);
$this->log('error', 'Agent failed', ['reason' => $reason]);
}
private function log(string $level, string $message, array $data = []): void
{
AgentLog::create([
'agent_run_id' => $this->run->id,
'step' => $this->currentStep,
'level' => $level,
'output' => $message . ' ' . json_encode($data),
]);
Log::channel('agent')->{$level}($message, $data);
}
}
Tool Registry: Controlled Actions Only
Tool Interface
php artisan make:interface Services/Agent/Tools/AgentTool
<?php
namespace App\Services\Agent\Tools;
interface AgentTool
{
public function name(): string;
public function description(): string;
public function execute(string $input): string;
}
Tool Registry
php artisan make:class Services/Agent/ToolRegistry
<?php
namespace App\Services\Agent;
use App\Services\Agent\Tools\AgentTool;
class ToolRegistry
{
private array $tools = [];
public function register(AgentTool $tool): void
{
$this->tools[$tool->name()] = $tool;
}
public function get(string $name): ?AgentTool
{
return $this->tools[$name] ?? null;
}
public function exists(string $name): bool
{
return isset($this->tools[$name]);
}
public function descriptions(): string
{
$lines = [];
foreach ($this->tools as $tool) {
$lines[] = "- {$tool->name()}: {$tool->description()}";
}
return implode("\n", $lines);
}
}
Safe Tools
Web Fetch Tool (Read-Only):
php artisan make:class Services/Agent/Tools/WebFetchTool
<?php
namespace App\Services\Agent\Tools;
use Illuminate\Support\Facades\Http;
class WebFetchTool implements AgentTool
{
public function name(): string
{
return 'fetch_url';
}
public function description(): string
{
return 'Fetch and extract text from a URL. Input: URL string. Output: Plain text content (max 3000 chars).';
}
public function execute(string $input): string
{
// Validate URL
if (!filter_var($input, FILTER_VALIDATE_URL)) {
return 'Error: Invalid URL format.';
}
// Block private networks
$host = parse_url($input, PHP_URL_HOST);
$ip = gethostbyname($host);
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
return 'Error: Private/reserved URLs not allowed.';
}
try {
$response = Http::timeout(15)->withOptions([
'verify' => true, // Require valid SSL
])->get($input);
if (!$response->successful()) {
return "Error: HTTP {$response->status()}";
}
$text = strip_tags($response->body());
$text = preg_replace('/\s+/', ' ', $text);
$text = trim($text);
return substr($text, 0, 3000) . (strlen($text) > 3000 ? '... [truncated]' : '');
} catch (\Exception $e) {
return "Error: {$e->getMessage()}";
}
}
}
RSS Feed Reader:
php artisan make:class Services/Agent/Tools/RssReaderTool
<?php
namespace App\Services\Agent\Tools;
use Illuminate\Support\Facades\Http;
class RssReaderTool implements AgentTool
{
public function name(): string
{
return 'read_rss';
}
public function description(): string
{
return 'Read an RSS/Atom feed and return recent items. Input: RSS feed URL. Output: JSON array of items.';
}
public function execute(string $input): string
{
if (!filter_var($input, FILTER_VALIDATE_URL)) {
return 'Error: Invalid URL.';
}
try {
$response = Http::timeout(15)->get($input);
$xml = simplexml_load_string($response->body());
$items = [];
$count = 0;
foreach ($xml->channel->item ?? [] as $item) {
if ($count++ >= 10) break;
$items[] = [
'title' => (string) $item->title,
'link' => (string) $item->link,
'pubDate' => (string) $item->pubDate,
];
}
return json_encode($items, JSON_PRETTY_PRINT);
} catch (\Exception $e) {
return "Error: {$e->getMessage()}";
}
}
}
Text Summarizer (Local):
php artisan make:class Services/Agent/Tools/SummarizeTool
<?php
namespace App\Services\Agent\Tools;
class SummarizeTool implements AgentTool
{
public function name(): string
{
return 'summarize';
}
public function description(): string
{
return 'Create a brief summary of provided text. Input: Text to summarize. Output: 2-3 sentence summary.';
}
public function execute(string $input): string
{
$sentences = preg_split('/(?<=[.!?])\s+/', $input, -1, PREG_SPLIT_NO_EMPTY);
if (count($sentences) <= 3) {
return $input;
}
// Simple extraction: first sentence + most significant sentence
$first = $sentences[0];
$longest = array_reduce($sentences, fn($a, $b) => strlen($a) > strlen($b) ? $a : $b, '');
return "Summary: {$first} Key point: {$longest}";
}
}
Store Observation:
php artisan make:class Services/Agent/Tools/StoreObservationTool
<?php
namespace App\Services\Agent\Tools;
use App\Models\AgentMemory;
class StoreObservationTool implements AgentTool
{
public function name(): string
{
return 'store_observation';
}
public function description(): string
{
return 'Store an observation in persistent memory. Input: JSON with {"key": "unique_key", "value": "text", "category": "optional"}.';
}
public function execute(string $input): string
{
$data = json_decode($input, true);
if (!isset($data['key']) || !isset($data['value'])) {
return 'Error: Input must include "key" and "value".';
}
AgentMemory::updateOrCreate(
['key' => $data['key']],
[
'value' => $data['value'],
'category' => $data['category'] ?? 'general',
]
);
return "Stored observation with key: {$data['key']}";
}
}
Memory: Database-Backed Persistence
The agent stores observations in the database, allowing it to:
- Compare current vs. previous states
- Resume after restarts
- Build knowledge over time
php artisan make:class Services/Agent/Memory
<?php
namespace App\Services\Agent;
use App\Models\AgentMemory;
class Memory
{
public function store(string $key, string $value, string $category = 'general'): void
{
AgentMemory::updateOrCreate(
['key' => $key],
['value' => $value, 'category' => $category]
);
}
public function retrieve(string $key): ?string
{
return AgentMemory::where('key', $key)->first()?->value;
}
public function retrieveByCategory(string $category): array
{
return AgentMemory::where('category', $category)
->pluck('value', 'key')
->toArray();
}
public function hasChanged(string $key, string $currentValue): bool
{
$previous = $this->retrieve($key);
return $previous !== $currentValue;
}
}
The Loop: Background Execution
php artisan make:command AgentRun --command="agent:run"
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Services\Agent\BackgroundAgent;
use App\Services\Agent\ToolRegistry;
use App\Services\Agent\Memory;
use App\Services\Agent\Tools\WebFetchTool;
use App\Services\Agent\Tools\RssReaderTool;
use App\Services\Agent\Tools\SummarizeTool;
use App\Services\Agent\Tools\StoreObservationTool;
use App\Notifications\AgentActivity;
use Illuminate\Support\Facades\Notification;
class AgentRun extends Command
{
protected $signature = 'agent:run
{goal : The task for the agent}
{--continuous : Run in a loop with sleep interval}
{--interval=3600 : Seconds between runs in continuous mode}';
protected $description = 'Run the background AI agent';
public function handle()
{
$goal = $this->argument('goal');
$continuous = $this->option('continuous');
$interval = (int) $this->option('interval');
// Build tool registry
$registry = new ToolRegistry();
$registry->register(new WebFetchTool());
$registry->register(new RssReaderTool());
$registry->register(new SummarizeTool());
$registry->register(new StoreObservationTool());
$agent = new BackgroundAgent($registry);
$memory = new Memory();
$this->info("Agent starting: {$goal}");
do {
$run = $agent->execute($goal);
$this->info("Run {$run->id}: {$run->status} in {$run->steps_taken} steps");
// Send Telegram notification
Notification::route('telegram', config('services.telegram.chat_id'))
->notify(new AgentActivity($run));
if ($continuous) {
$this->info("Sleeping for {$interval} seconds...");
sleep($interval);
}
} while ($continuous);
return 0;
}
}
Telegram Notifications
php artisan make:notification AgentActivity
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use NotificationChannels\Telegram\TelegramMessage;
use App\Models\AgentRun;
class AgentActivity extends Notification
{
use Queueable;
private AgentRun $run;
public function __construct(AgentRun $run)
{
$this->run = $run;
}
public function via($notifiable): array
{
return ['telegram'];
}
public function toTelegram($notifiable): TelegramMessage
{
$statusEmoji = match($this->run->status) {
'completed' => '✅',
'failed' => '❌',
default => '🔄',
};
$message = <<>MSG
{$statusEmoji} **Agent Activity Report**
**Task:** {$this->run->goal}
**Status:** {$this->run->status}
**Steps:** {$this->run->steps_taken}
**Time:** {$this->run->created_at->format('Y-m-d H:i:s')}
**Summary:**
{$this->run->summary}
MSG;
return TelegramMessage::create()
->to(config('services.telegram.chat_id'))
->content($message);
}
}
Running 24/7 with Supervisor
Install Supervisor
# Ubuntu/Debian
sudo apt-get install supervisor
# macOS
brew install supervisor
Configuration File
Create /etc/supervisor/conf.d/laravel-agent.conf:
[program:laravel-agent]
process_name=%(program_name)s_%(process_num)02d
command=php /path/to/artisan agent:run "Monitor https://example.com/feed and notify on changes" --continuous --interval=3600
directory=/path/to/project
autostart=true
autorestart=true
user=www-data
numprocs=1
redirect_stderr=true
stdout_logfile=/var/log/laravel-agent.log
stopwaitsecs=3600
Start and Monitor
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start laravel-agent
# View logs
tail -f /var/log/laravel-agent.log
# Check status
sudo supervisorctl status
Realistic Example: Website Monitor
Use Case
Monitor a website or RSS feed hourly. Store the content hash. If content changes, summarize the change and send a Telegram notification.
Implementation
php artisan make:command AgentMonitorWebsite --command="agent:monitor"
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Services\Agent\BackgroundAgent;
use App\Services\Agent\ToolRegistry;
use App\Services\Agent\Memory;
use App\Services\Agent\Tools\WebFetchTool;
use App\Services\Agent\Tools\RssReaderTool;
use App\Services\Agent\Tools\SummarizeTool;
use App\Services\Agent\Tools\StoreObservationTool;
use App\Notifications\AgentActivity;
use Illuminate\Support\Facades\Notification;
class AgentMonitorWebsite extends Command
{
protected $signature = 'agent:monitor
{url : Website or RSS URL to monitor}
{--interval=3600 : Seconds between checks}
{--rss : Treat URL as RSS feed}';
protected $description = 'Monitor a website or RSS feed for changes';
public function handle()
{
$url = $this->argument('url');
$interval = (int) $this->option('interval');
$isRss = $this->option('rss');
$registry = new ToolRegistry();
$registry->register(new WebFetchTool());
$registry->register(new RssReaderTool());
$registry->register(new SummarizeTool());
$registry->register(new StoreObservationTool());
$memory = new Memory();
$agent = new BackgroundAgent($registry);
$this->info("Starting monitor for: {$url}");
$this->info("Interval: {$interval} seconds");
while (true) {
$goal = $isRss
? "Read RSS feed at {$url}, compare with previous observations, summarize any new items"
: "Fetch content from {$url}, compare with previous hash, summarize changes if different";
$run = $agent->execute($goal);
// Only notify if something changed or if there was an error
if ($run->status === 'completed' && str_contains($run->summary, 'changed')) {
Notification::route('telegram', config('services.telegram.chat_id'))
->notify(new AgentActivity($run));
$this->info('Change detected — notification sent.');
} elseif ($run->status === 'failed') {
Notification::route('telegram', config('services.telegram.chat_id'))
->notify(new AgentActivity($run));
$this->warn('Monitor failed — notification sent.');
} else {
$this->info('No changes detected.');
}
$this->info("Sleeping for {$interval} seconds...");
sleep($interval);
}
}
}
Run It
# One-time check
php artisan agent:monitor https://example.com/feed --rss
# Continuous background monitor
php artisan agent:monitor https://example.com/feed --rss --interval=1800
Safety & Production Considerations
Allowlisted Tools Only
The agent can only execute tools explicitly registered in ToolRegistry. There is no generic "execute code" or "run shell command" tool.
| Allowed | Not Allowed |
|---|---|
| Fetch URL (read-only) | Shell execution |
| Read RSS feed | File modification |
| Store observation | Database writes (except agent_memory) |
| Summarize text | API calls to external services |
| Send Telegram notification | Financial transactions |
Rate Limiting
LLM calls are rate-limited using Laravel's built-in RateLimiter:
RateLimiter::attempt('agent-llm-calls', 10, fn() => true, 60);
This prevents runaway agents from consuming API credits.
Action Logging
Every tool execution is logged to the agent_logs table:
# View recent activity
php artisan tinker
>>> App\Models\AgentLog::latest()->take(10)->get();
Logs Channel
Add to config/logging.php:
'agent' => [
'driver' => 'daily',
'path' => storage_path('logs/agent.log'),
'level' => 'debug',
'days' => 14,
],
Human Approval for Destructive Actions
The agent is designed as a monitor and reporter, not an actor. It:
- ✅ Reads and summarizes
- ✅ Stores observations
- ✅ Sends notifications
- ❌ Does not modify data
- ❌ Does not execute commands
- ❌ Does not make financial decisions
For actions that modify state, implement an approval queue:
// In a tool that requires approval
class DataModificationTool implements AgentTool
{
public function execute(string $input): string
{
// Queue for human approval instead of executing
ApprovalQueue::create([
'tool' => $this->name(),
'input' => $input,
'status' => 'pending',
]);
return 'Action queued for human approval.';
}
}
Environment Security
- Store API keys in
.env, never commit them - Use Laravel's encrypted environment files for production
- Restrict Telegram bot to known chat IDs
- Validate all URLs before fetching (no private networks)
Testing and Debugging
Run Single Task
php artisan agent:run "Fetch and summarize https://example.com"
View Agent State
php artisan tinker
>>> App\Models\AgentRun::latest()->first();
>>> App\Models\AgentLog::where('agent_run_id', 1)->get();
>>> App\Models\AgentMemory::all();
Test Individual Tools
php artisan tinker
>>> (new App\Services\Agent\Tools\WebFetchTool)->execute('https://example.com');
Check Logs
tail -f storage/logs/agent.log
Extending the Agent
Adding New Tools
- Implement
AgentToolinterface - Register in the command's
ToolRegistry - The LLM automatically discovers it via the system prompt
Example: Database Query Tool (Read-Only)
class DatabaseQueryTool implements AgentTool
{
public function name(): string { return 'db_query'; }
public function description(): string {
return 'Execute a read-only SELECT query. Input: SQL query string.';
}
public function execute(string $input): string
{
if (!preg_match('/^\s*SELECT/i', $input)) {
return 'Error: Only SELECT queries permitted.';
}
return json_encode(\DB::select($input));
}
}
Multi-Agent Setup
Create specialized agents for different domains:
class ResearchAgent extends BackgroundAgent {
protected function systemPrompt(): string {
return 'You are a research agent. Gather information and summarize findings.';
}
}
class MonitorAgent extends BackgroundAgent {
protected function systemPrompt(): string {
return 'You are a monitoring agent. Watch for changes and alert on anomalies.';
}
}
Queue-Based Goals
Instead of hardcoding goals, read from a queue:
// In the command loop
$goal = Redis::lpop('agent:goals');
if ($goal) {
$agent->execute($goal);
}
Comparison With Existing Packages
| Package | Best For | This Tutorial Adds |
|---|---|---|
| Laravel AI SDK | First-party AI features | Background execution, safety layer |
| Prism | LLM abstraction | Tool control, action logging |
| LarAgent | Local LLM agents | Persistent memory, deployment |
| NeuronAI | Agent monitoring | Notification channels |
This tutorial is not a replacement for these packages — it is a production blueprint that demonstrates how to build a supervised, observable, safe background agent using Laravel primitives.
Conclusion
This tutorial demonstrated how to build a background AI agent in Laravel that:
- Runs continuously as a supervised daemon
- Uses an LLM for reasoning with structured JSON responses
- Executes only allowlisted, safe tools
- Stores observations in persistent database memory
- Sends Telegram notifications on activity
- Logs every action for auditability
- Includes rate limiting and safety guardrails
The example use case — monitoring a website or RSS feed, storing observations, summarizing changes, and alerting via Telegram — is a realistic starting point that can be extended to many domains.
Resources and References
- Laravel AI SDK — Official first-party SDK for AI features
- Building AI Agents with Laravel (Official Blog) — Laravel team's introduction to agents
- Prism PHP — Elegant LLM abstraction for multiple providers
- LarAgent — Agent orchestration with local LLMs
- ReAct Paper — Reasoning and Acting in Language Models
- Supervisor Documentation — Process control for background daemons
Questions or improvements? Drop a comment or reach out on Twitter/X or LinkedIn.
Built with Laravel, local LLMs, and careful engineering. 🛡️