Agents
Agents are reusable AI configurations — a class that defines provider, model, instructions, tools, and behavior.
Defining an Agent
Extend Atlasphp\Atlas\Agent and override the methods you need. All methods have sensible defaults — an agent with no overrides is valid.
use Atlasphp\Atlas\Agent;
use Atlasphp\Atlas\Enums\Provider;
use Atlasphp\Atlas\Providers\Tools\ProviderTool;
class SupportAgent extends Agent
{
public function key(): string
{
return 'support'; // Auto-generated by default
}
public function name(): string
{
return 'Support'; // Auto-generated by default
}
public function description(): ?string
{
return 'Handles customer support inquiries.';
}
public function provider(): Provider|string|null
{
return 'anthropic';
}
public function model(): ?string
{
return 'claude-sonnet-4-20250514';
}
public function instructions(): ?string
{
return <<<'PROMPT'
You are a customer support specialist for {APP_NAME}.
## Customer Context
- **Name:** {customer_name}
- **Account Tier:** {account_tier}
## Guidelines
- Always greet the customer by name
- For order inquiries, use `lookup_order` before providing details
PROMPT;
}
public function tools(): array
{
return [
LookupOrderTool::class,
ProcessRefundTool::class,
];
}
public function providerTools(): array
{
return [];
}
public function temperature(): ?float
{
return 0.7;
}
public function maxTokens(): ?int
{
return null;
}
public function maxSteps(): ?int
{
return 8;
}
public function concurrent(): bool
{
return false;
}
public function providerOptions(): array
{
return [];
}
}Auto-Key Convention
The key() method defaults to the class name in kebab-case, minus the "Agent" suffix:
SupportAgent→'support'BillingAssistantAgent→'billing-assistant'CustomerServiceAgent→'customer-service'
Using Agents
use Atlasphp\Atlas\Atlas;
$response = Atlas::agent('support')
->withVariables([
'customer_name' => 'Sarah',
'account_tier' => 'Premium',
])
->message('I need help with my order.')
->asText();
echo $response->text;Streaming
$stream = Atlas::agent('support')
->withVariables(['customer_name' => 'Sarah'])
->message('Where is my order #12345?')
->asStream();
foreach ($stream as $chunk) {
echo $chunk->text;
}With Media
Send images, documents, audio, or video alongside your message. The agent's provider processes the media inline (e.g. vision for images, transcription for audio):
use Atlasphp\Atlas\Input\Image;
use Atlasphp\Atlas\Input\Document;
// Send an image for the agent to analyze
$response = Atlas::agent('support')
->message('What does this receipt show?', Image::fromUpload($request->file('receipt')))
->asText();
// Send a document for context
$response = Atlas::agent('analyst')
->message('Summarize this report', Document::fromStorage('reports/q4.pdf'))
->asText();
// Multiple attachments
$response = Atlas::agent('reviewer')
->message('Compare these two images', [
Image::fromUrl('https://example.com/before.jpg'),
Image::fromUrl('https://example.com/after.jpg'),
])
->asText();Media can be loaded from uploads, URLs, local paths, Laravel storage, or base64. See Message Attachments for persistence and how agent-generated files are tracked.
Structured Output
use Atlasphp\Atlas\Schema\Schema;
$schema = Schema::object('sentiment', 'Sentiment analysis result')
->enum('sentiment', 'Detected sentiment', ['positive', 'negative', 'neutral'])
->number('confidence', 'Confidence score 0-1')
->string('reasoning', 'Explanation')
->build();
$response = Atlas::agent('analyzer')
->withSchema($schema)
->message('I love this product!')
->asStructured();
$response->structured['sentiment']; // "positive"
$response->structured['confidence']; // 0.95Voice
Start a real-time voice session using the agent's tools, instructions, and voice config:
$session = Atlas::agent('sarah-voice')
->for($user)
->forConversation($conversationId)
->asVoice();
return response()->json($session->toClientPayload());The browser connects directly to the provider for audio. Tools are executed server-side via the package-provided endpoint.
Voice agents can define a voice() method to set their voice identity:
class SarahVoiceAgent extends Agent
{
public function voice(): ?string
{
return 'eve'; // xAI voice ID
}
}When forConversation($id) is set, the voice agent receives the conversation's text history as context — so it knows what was discussed in prior text messages. Voice transcripts are stored in a dedicated VoiceCall record, not as individual messages. Listen for VoiceCallCompleted to post-process the transcript (summarize, create messages, embed into memory).
See Voice Modality for transport details, Voice Integration for the full setup guide, and Post-Processing Patterns for handling transcripts.
Runtime Overrides
Every agent configuration can be overridden at call time via fluent methods on AgentRequest:
$response = Atlas::agent('support')
->withProvider('openai', 'gpt-4o')
->withMaxTokens(500)
->withTemperature(0.3)
->withMaxSteps(5)
->withConcurrent()
->withTools([ExtraTool::class])
->withProviderTools([new WebSearch])
->withProviderOptions(['seed' => 12345])
->withVariables(['customer_name' => 'John'])
->withMeta(['user_id' => 123])
->withMessages($conversationHistory)
->withSchema($schema)
->message('Hello')
->asText();Agent with Tools
Define tools the agent can call. Atlas handles the tool loop automatically — calling the model, executing tools, and feeding results back until the model produces a final response.
class ResearchAgent extends Agent
{
public function provider(): Provider|string|null
{
return 'openai';
}
public function model(): ?string
{
return 'gpt-4o';
}
public function instructions(): ?string
{
return 'You are a research assistant. Use your tools to find and analyze information.';
}
public function tools(): array
{
return [
SearchWebTool::class,
ReadUrlTool::class,
SummarizeTool::class,
];
}
public function maxSteps(): ?int
{
return 10;
}
}Delegating to another agent
You can also list an agent in tools() to hand a task off to it. See Sub-agents.
Use maxSteps() to limit tool loop iterations and prevent runaway execution.
Concurrent Tool Execution
By default, tool calls execute sequentially — one at a time. This is the safest option and works with all configurations including persistence tracking.
Enabling Concurrency
Override concurrent() in your agent to run tool calls in parallel:
class ResearchAgent extends Agent
{
public function concurrent(): bool
{
return true;
}
// ...
}Or enable at call time:
$response = Atlas::agent('research')
->withConcurrent()
->message('Research these three topics')
->asText();How It Works
When the model requests multiple tool calls in a single step, Atlas runs them simultaneously using Laravel's Concurrency facade:
- With
spatie/fork+pcntl— true parallel execution via OS-level process forking - Without — falls back to sequential execution through the sync driver
A step that contains only one tool call always runs inline — there is nothing to parallelize.
Concurrent Sub-agents
Sub-agent delegations parallelize too. When the model delegates to several sub-agents in a single step, they run at the same time — each in its own forked process — and every sub-agent's response is returned to the parent. The parent's tool loop resumes only once all of them complete, so it always continues with the full set of results:
class CoordinatorAgent extends Agent
{
public function concurrent(): bool
{
return true;
}
public function tools(): array
{
// Three independent sub-agents the model can fan out to in one step.
return [ResearchAgent::class, PricingAgent::class, LegalAgent::class];
}
}The complete execution lineage is preserved across the fork boundary: the parent → child tree, each sub-agent's own token usage, the rolled-up subtree usage, and the depth/cycle guards all behave exactly as they do sequentially. Only wall-clock time changes.
Real-time events from concurrent sub-agents
The parent still emits its own AgentToolCallStarted / AgentToolCallCompleted events for each delegation (so the call and its result broadcast normally). But a sub-agent's internal orchestration events — its own AgentStarted / AgentCompleted, step events, and nested tool-call events — fire inside the forked child process and are not delivered to in-process listeners in the parent. There is no inter-process channel back.
This affects only live, in-process observability while a sub-agent is still running (broadcasting each step as it happens, listener-driven side effects). It does not limit what you can show after it completes. The child writes its full execution / step / tool-call tree to the database from inside the fork, so once a concurrent sub-agent finishes you can load and display everything it did — its final response, each step's text, and every tool it ran (with arguments, results, and timing) — by drilling into the delegation tree:
use Atlasphp\Atlas\Persistence\Models\Execution;
// Open a completed delegation tool call → the sub-agent that ran it.
$child = Execution::where('parent_tool_call_id', $toolCallId)->first();
$child->steps; // each step, with ->content (response text) and ->reasoning
$child->toolCalls; // every tool it ran, with ->arguments, ->result, ->duration_ms
$child->usage; // its own token usageSo a UI that opens a finished sub-agent call shows its complete response, steps, and tools. Only the live, as-it-happens stream of a sub-agent's internals is unavailable under concurrency — for that, use sequential delegation (the default).
Persistence & Fork Safety
Persistence tracking writes to the database from inside the forked child processes. Database connections (PostgreSQL, MySQL) are not fork-safe — a child that inherits and uses the parent's connection socket would corrupt the wire protocol.
Atlas handles this for you. Before forking, it closes the parent's resolved database connections so each child opens its own fresh connection; the parent transparently reconnects on its next query. This reset runs only when the fork driver is active and persistence is enabled, so the in-process sync driver and non-persistent runs are never affected. Concurrent execution with persistence tracking is safe out of the box — no configuration required.
Requirements & Environment
- Atlas ships with
spatie/forkas a dependency — no extra installation needed. True parallelism requires thepcntlPHP extension (standard on Linux/macOS, not available on Windows). Withoutpcntl, concurrent mode falls back to sequential execution. - Fork-based parallelism runs in CLI, queue-worker, and Artisan contexts. It does not run inside PHP-FPM web requests (forking a live web worker is unsafe), where Atlas falls back to sequential. To parallelize long-running agent work from a web request without blocking it, dispatch the execution to the queue and let workers run concurrently.
When to Use
Enable concurrency when:
- Tools or sub-agents are independent and don't depend on each other's results
- They make external API calls (including sub-agent model calls) where parallelism reduces wall-clock time
Keep sequential (default) when:
- Tools have side effects that depend on execution order
- You're running on Windows or an environment without
pcntl(it falls back to sequential anyway)
Conversations (Optional)
For persistent conversations, use ->for() and ->forConversation(). Requires persistence to be enabled.
// Start a new conversation for a user
$response = Atlas::agent('support')
->for($user)
->message('I need help with my account.')
->asText();
// Continue an existing conversation
$response = Atlas::agent('support')
->for($user)
->forConversation($conversationId)
->message('What about my billing?')
->asText();
// Multi-user conversation — specify who is sending the message
$response = Atlas::agent('support')
->for($team, as: $currentUser)
->forConversation($conversationId)
->message('Adding a note to this conversation.')
->asText();Respond Without User Message
Proactively have the agent respond in a conversation thread without a new user message. Useful for scheduled follow-ups, background task results, or proactive notifications.
// Agent responds to the thread without a new user message
$response = Atlas::agent('support')
->forConversation($conversationId)
->respond()
->asText();Requires Existing Conversation
respond() requires forConversation($id). The agent must join an existing conversation — there's no user message to create one.
Retry Last Response
Regenerate the last assistant response to get a different answer — like hitting "regenerate" in a chat UI.
$response = Atlas::agent('support')
->forConversation($conversationId)
->retry()
->asText();When you retry:
- The current active response is deactivated (
is_active = false) - A new response is generated with the same conversation context
- The new response shares the same
parent_idas the original — creating a sibling - Only the latest response is active and included in future conversation history
You can retry multiple times. Each retry creates another sibling. Only one is active at a time.
Can Retry?
A response can only be retried if no user message was sent after it. Once the conversation continues, earlier responses are locked.
Sibling Messages
Retries create sibling messages — multiple responses to the same user message. Only one sibling group is active at a time.
For chat UIs that show "1 of 3" navigation between response alternatives:
use Atlasphp\Atlas\Persistence\Services\ConversationService;
$service = app(ConversationService::class);
// Get sibling info for a message
$info = $service->siblingInfo($message);
// ['current' => 2, 'total' => 3, 'groups' => [...]]
// Switch to a different sibling
$service->cycleSibling($conversation, $message->parent_id, $targetIndex);See the Conversations Guide for the full persistence system.
Auto-Discovery
Agents are automatically discovered from your configured directory. Set the path and namespace in config/atlas.php:
'agents' => [
'path' => app_path('Agents'),
'namespace' => 'App\\Agents',
],Any class extending Agent in that directory is registered and available via Atlas::agent('key').
API Reference
Agent Methods
All methods have sensible defaults. Override only what you need.
| Method | Return | Default | Description |
|---|---|---|---|
key() | string | Class name in kebab-case | Unique key for Atlas::agent('key') |
name() | string | Class name with spaces | Display name |
description() | ?string | null | Description for agent-to-agent delegation |
provider() | Provider|string|null | null | Provider (falls back to config default) |
model() | ?string | null | Model identifier (falls back to config default) |
instructions() | ?string | null | System instructions with {variable} interpolation |
tools() | array | [] | Atlas tool classes the agent can invoke |
providerTools() | array | [] | Provider-native tools (web search, code execution) |
temperature() | ?float | null | Sampling temperature |
maxTokens() | ?int | null | Maximum response tokens |
maxSteps() | ?int | null | Maximum tool loop iterations |
concurrent() | bool | false | Execute tool calls concurrently |
providerOptions() | array | [] | Provider-specific options passed through directly |
AgentRequest Fluent Methods
Returned by Atlas::agent('key'). Chain these before a terminal method.
| Method | Description |
|---|---|
->message(string $text, array|Input $media = []) | Set the user message (with optional media) |
->instructions(string $directive) | Override the agent's system instructions |
->withVariables(array $variables) | Set variables for instruction interpolation |
->withMeta(array $meta) | Attach metadata accessible in tools via ToolContext |
->withProvider(Provider|string $provider, string $model) | Override provider and model |
->withMaxTokens(int $tokens) | Override max response tokens |
->withTemperature(float $temp) | Override sampling temperature |
->withMaxSteps(?int $maxSteps) | Override max tool loop iterations |
->withConcurrent(bool $concurrent = true) | Override concurrent tool execution |
->withTools(array $tools) | Add tools in addition to the agent's tools |
->withProviderTools(array $providerTools) | Add provider tools at runtime |
->withProviderOptions(array $options) | Override provider-specific options |
->withSchema(Schema $schema) | Set structured output schema |
->withMessages(array $messages) | Provide conversation history |
->for(Model $owner, ?Model $as = null) | Set conversation owner. Optionally pass as: to set a different message sender (persistence) |
->forConversation(int $id) | Join an existing conversation (persistence) |
->asUser(Model $owner) | (Deprecated) Use for($owner, as: $user) instead |
->withMessageLimit(int $limit) | Override message history limit (persistence) |
->respond() | Respond without a new user message (persistence) |
->retry() | Retry the last assistant response (persistence) |
->asText() | Execute and return TextResponse |
->asStream() | Execute and return StreamResponse |
->asStructured() | Execute and return StructuredResponse |
->asVoice() | Start a voice session and return VoiceSession |
Artisan Command
php artisan make:agent SupportAgentNext Steps
- Instructions — Variable interpolation in instructions
- Tools — Build tools agents can call
- Text — Text generation, streaming, and structured output
- Schema — Schema fields for structured output
- Middleware — Add middleware to agent execution