maestroerror/laragent
LarAgent is an AI agent development framework for Laravel. Build and maintain agents with an Eloquent-style API, first-class tools and MCP server support, pluggable memory and context, multi-agent workflows, and structured output for reliable results.
LarAgent is an agent framework, not just an LLM wrapper. The mental model to adopt on day one is: an agent is a class like an Eloquent model, and a chat is a keyed session like a cache entry. Once that clicks, the rest of the package is discoverable.
Minimum viable setup
composer require maestroerror/laragent
php artisan vendor:publish --tag="laragent-config"
Requirements are stricter than most Laravel packages: PHP 8.3+ and Laravel 10+. Check your CI matrix before committing — this is the most common first blocker on legacy projects.
Set OPENAI_API_KEY (or your provider key) in .env, then scaffold your first agent:
php artisan make:agent SupportAgent
That generates a class extending LarAgent\Agent with $model, $history, $provider, $tools, plus instructions() and prompt(). The two methods matter:
instructions() — the system prompt. Treat it as your primary lever for behavior.prompt($message) — transforms the user message before it hits the provider. Useful for injecting context, formatting, or guardrails.First use case: build a single-turn assistant with no tools and no persistence to prove the wiring, then add one trait at a time. A good starter is a "docs Q&A" or "internal ops helper" agent — low stakes, easy to eyeball output quality.
SupportAgent::forUser(auth()->user())->respond('How do I reset my password?');
Where to look first, in order:
config/laragent.php after publishing — this is the source of truth for providers and defaults.docs.laragent.ai — the README is intentionally thin and points outward.v0.7 → v0.8 and v1.0 changelogs before you copy code from a blog post. There are real breaking changes in the tool-execution event signatures.Providers supply defaults; the agent class overrides them. Keep provider configs generic and push behavior into the agent. This is the pattern the package is designed around and it keeps agents portable across providers.
class AnalystAgent extends Agent
{
protected $provider = 'default';
protected $model = 'gpt-4';
protected $temperature = 0.2; // low temp for analysis
protected $parallelToolCalls = false; // deterministic ordering
protected $history = \LarAgent\History\CacheChatHistory::class;
public function instructions() { /* ... */ }
public function prompt($message) { return $message; }
}
for() and forUser()This is the pattern you will use every single day. The history key determines which conversation the agent continues.
// Per-user, per-agent default thread
SupportAgent::forUser($user)->respond($message);
// Explicit named thread — great for multi-tab or multi-topic chats
SupportAgent::for("support:{$user->id}:billing")->respond($message);
Choose your key strategy early and standardize it. A common convention: "{agent}:{user_id}:{channel}". Getting this wrong late means migrating conversation storage or silently orphaning history.
| Backend | Use when |
|---|---|
InMemoryChatHistory |
Tests, single-request jobs, stateless one-shots |
CacheChatHistory |
Default for real chat UIs; fast, TTL-managed |
| JSON / custom | Audit trails, compliance, cross-service replay |
The default is InMemoryChatHistory, which means history vanishes between HTTP requests. This surprises people constantly. If you're building a chat, you almost certainly want CacheChatHistory or a custom ChatHistory implementation wired to your DB.
LarAgent gives you attribute-based tools, class-based tools, and a Tool facade. Pick one convention per codebase.
#[Tool('Get the current weather in a given location')]
public function exampleWeatherTool($location, $unit = 'celsius')
{
return "The weather in {$location} is 20 degrees {$unit}";
}
The attribute style is the lowest-friction and what most developers will reach for. Parameter names and types are inferred — document them in the description string because that text is what the model uses to decide when and how to call. Vague descriptions are the number-one cause of bad tool selection.
Disable parallelToolCalls when your tools have side effects (writes, emails, payments) and ordering matters. Leave it on for read-only fan-out.
Provider arrays are a genuinely useful production feature — not just a convenience:
// Ordered fallback
protected $provider = ['default', 'gemini', 'claude'];
// With per-provider overrides
protected $provider = [
'default',
'gemini' => ['model' => 'gemini-2.0-flash'],
'claude',
];
Use this for uptime, but remember: capability is not identical across providers. A tool-calling workflow that works on OpenAI may degrade on a fallback. Test the fallback path explicitly, or restrict fallback chains to agents with simple, provider-agnostic prompts.
For reasoning_effort-style models, use the OpenAiResponsesDriver and pass parameters through $extras:
protected $provider = 'openai_responses';
protected $model = 'gpt-5.4-mini';
protected $extras = ['reasoning_effort' => 'high'];
For third-party Responses-compatible endpoints, OpenAiResponsesCompatible plus an api_url handles it.
Nearly everything is hookable — this is the framework's strongest operational feature. Use it for logging, cost tracking, and redaction rather than polluting agent classes.
Event::listen(BeforeToolExecution::class, function ($event) {
Log::info('Tool call', [
'id' => $event->toolCall->getId(),
'tool' => $event->toolCall->getToolName(),
'args' => json_decode($event->toolCall->getArguments(), true),
]);
});
Verify signatures against the changelog. In v0.8, beforeToolExecution() went from 2 to 3 parameters and afterToolExecution() from 3 to 4. Any callback written against v0.7 will silently receive the wrong arguments.
Agent turns are network-bound and can take seconds to minutes. Wrap them in jobs:
ProcessAgentMessage::dispatch($conversationId, $message);
The package provides multi-agent workflows with queues and chainable tasks — use those primitives rather than hand-rolling dispatch()->chain() with agent calls inside, so the framework's history and event plumbing stays consistent.
The built-in OpenAI-compatible schema means you can front an agent with a chat-completions-shaped endpoint and point existing OpenAI SDK clients at it. Great for internal tooling and for drop-in replacing a provider during migration.
History is ephemeral until you change it. InMemoryChatHistory is the shipped default. If your agent "forgets" between requests, this is why — not a bug.
default_max_completion_tokens: 100 in the sample config is very low. The published config example shows 100 tokens. If your responses are getting cut off mid-sentence, this is the culprit. Raise it per provider and override per agent where needed.
Truncation is silent. default_truncation_threshold (50000 in the default provider) drops older context once exceeded. Combined with persistent history this can produce confident-but-wrong answers about things "discussed earlier." Log truncation events, and for long conversations prefer summarization or retrieval over a bigger threshold.
Model names drift. The README uses gpt-4 and gpt-5.4-mini in examples. Model identifiers change and get retired. Keep model strings in config or env, not hardcoded in agent classes, so a provider deprecation is a config change rather than a deploy.
Version the tool event contract. As above: v0.8 changed event and hook arities. If you have listeners in a long-lived app, pin the version or convert listeners to the new signatures in the same PR as the upgrade. There is no deprecation shim mentioned.
Anthropic strict tool use is on by default in v1.0. This improves structured output reliability but tightens the schema your tool definitions must satisfy. Tools with loose or nullable parameters may start failing validation.
Provider array order is the failover order, and the first is primary. The associative form ('gemini' => [...]) mixes keys and values — read it carefully; it's easy to accidentally make a provider primary when you meant it as a fallback.
Tool descriptions are prompt engineering, not documentation. Write them for the model: when to use, what each argument means, what it returns. Include negative guidance ("do not use for X") where tools overlap — overlapping tools with thin descriptions cause erratic selection.
Test without burning tokens. The package's own test suite uses testsManual/*.gitignore files holding API keys. Mirror that pattern: fake drivers or recorded fixtures for CI, real-key tests only locally or on a nightly job. Nothing drains a budget faster than a green CI run that hits a live API on every commit.
Extension points worth knowing before you fork anything: custom drivers (LarAgent\Drivers\...), custom chat history classes implementing the history interface, custom Tool classes for tools with dependencies, and the event system for anything cross-cutting. In this order, you can almost always avoid touching vendor code.
Standalone usage exists. The framework supports non-Laravel usage, which is useful for a CLI worker or a separate service sharing the same agent definitions — but you lose the artisan scaffolding and config helpers, so budget for that.
Pick a conversation-key convention on day one. Retrofitting for()/forUser() naming after users have live threads is a data migration, not a refactor.
MCP is a separate package. Tool and resource management from MCP servers lives in RedberryProducts/mcp-client-laravel, not in core. If you're planning MCP integration, add it to your dependency review up front.
Community and support posture. 645 stars, MIT, actively released, Diamond-tier Laravel partner backing, Discord plus a real docs site — that's a healthy signal for a young package. But dependents are effectively zero, which means you are an early adopter: budget for reading source when docs are thin, and pin your version.
How can I help you explore Laravel packages today?