DEV Community

Cover image for Laravel AI SDK Sub-Agents: Build Multi-Agent Systems That Actually Scale
Hafiz
Hafiz

Posted on • Originally published at hafiz.dev

Laravel AI SDK Sub-Agents: Build Multi-Agent Systems That Actually Scale

Originally published at hafiz.dev


Taylor Otwell shipped sub-agent support to the Laravel AI SDK. The announcement is short: return an agent from another agent's tools() method and the parent can delegate focused tasks to it. But what it unlocks is significant.

Before this, you could simulate sub-agents by wrapping agent() calls inside a tool's handle() method. It worked, and the multi-agent patterns post covers that approach in detail. But it was a workaround. The agent logic lived inside a tool class, not in a proper Agent class with its own instructions, tools, provider config, and context.

Now sub-agents are first-class citizens. This post covers how the new API works and how to build a realistic multi-agent system with it.

What Sub-Agents Actually Are

A sub-agent is a dedicated Laravel AI Agent class that a parent agent can invoke as a tool. The parent delegates work to it exactly the way it would call any other tool. The difference is that the sub-agent runs with full autonomy: its own instructions, its own tool set, its own provider configuration, and its own isolated context window.

This matters for a few reasons.

Isolation. The parent's conversation history doesn't bleed into the sub-agent. The sub-agent starts fresh with just its own instructions and what the parent passes to it. No context pollution, no token waste from irrelevant history.

Specialization. Each sub-agent is a proper PHP class with its own instructions(), tools(), and optional schema(). You can build a billing specialist, a technical support specialist, and an order lookup specialist, each configured precisely for its job.

Model flexibility. A sub-agent can run on a different provider or model than its parent. Route simple queries to a cheap model. Route complex reasoning to a capable one. The parent doesn't know or care.

The API

Before sub-agents, you'd build a multi-agent orchestrator by wrapping agents in tool classes. The workaround looked roughly like this:

class HandleRefundTool implements Tool
{
    public function description(): string
    {
        return 'Process a refund request for a customer.';
    }

    public function handle(Request $request): string
    {
        // Agent logic stuffed inside a tool class
        return (string) agent()
            ->withInstructions('You are a refund specialist...')
            ->prompt($request['query']);
    }

    public function schema(JsonSchema $schema): array
    {
        return ['query' => $schema->string()->required()];
    }
}
Enter fullscreen mode Exit fullscreen mode

This works, but the agent logic is buried in a tool. There's no proper instructions() method, no tools() method, no structured output. It's a second-class agent.

With sub-agents, you return a real Agent class directly from tools():

class CustomerSupportAgent implements Agent, HasTools
{
    use Promptable;

    public function instructions(): string
    {
        return 'You are a customer support orchestrator. Analyze the customer\'s 
                message and delegate to the appropriate specialist agent.';
    }

    public function tools(): iterable
    {
        return [
            new BillingAgent,
            new TechnicalSupportAgent,
            new OrderAgent,
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

Each of those is a full Agent class with its own configuration. The SDK handles the delegation. The parent sees them as tools and calls them when it decides the task fits their domain.

Building a Real Example

Let's build a customer support system for a SaaS product. Users send messages. A parent orchestrator agent decides which specialist handles the response.

View the interactive diagram on hafiz.dev

Start with the sub-agents. Each is a focused specialist.

BillingAgent

<?php

namespace App\Ai\Agents;

use App\Ai\Tools\ProcessRefund;
use App\Ai\Tools\CheckSubscriptionStatus;
use App\Ai\Tools\UpdatePaymentMethod;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasTools;
use Laravel\Ai\Promptable;

class BillingAgent implements Agent, HasTools
{
    use Promptable;

    public function instructions(): string
    {
        return 'You are a billing specialist. You handle refund requests, 
                subscription changes, and payment issues. Be concise and 
                solution-focused. Always confirm before processing any changes.';
    }

    public function tools(): iterable
    {
        return [
            new ProcessRefund,
            new CheckSubscriptionStatus,
            new UpdatePaymentMethod,
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

TechnicalSupportAgent

<?php

namespace App\Ai\Agents;

use App\Ai\Tools\QueryKnowledgeBase;
use App\Ai\Tools\CreateSupportTicket;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasTools;
use Laravel\Ai\Promptable;

class TechnicalSupportAgent implements Agent, HasTools
{
    use Promptable;

    public function instructions(): string
    {
        return 'You are a technical support engineer. Diagnose issues, 
                search the knowledge base for solutions, and escalate 
                to a ticket when the problem requires engineering attention.';
    }

    public function tools(): iterable
    {
        return [
            new QueryKnowledgeBase,
            new CreateSupportTicket,
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

OrderAgent

<?php

namespace App\Ai\Agents;

use App\Ai\Tools\LookupOrder;
use App\Ai\Tools\TrackShipment;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasTools;
use Laravel\Ai\Promptable;

class OrderAgent implements Agent, HasTools
{
    use Promptable;

    public function instructions(): string
    {
        return 'You are an order specialist. Look up order status, 
                track shipments, and resolve delivery issues.';
    }

    public function tools(): iterable
    {
        return [
            new LookupOrder,
            new TrackShipment,
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

CustomerSupportAgent (the orchestrator)

<?php

namespace App\Ai\Agents;

use App\Models\User;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasTools;
use Laravel\Ai\Promptable;

class CustomerSupportAgent implements Agent, HasTools
{
    use Promptable;

    public function __construct(public User $user) {}

    public function instructions(): string
    {
        return "You are a customer support orchestrator for {$this->user->company_name}. 
                Analyze incoming messages and delegate to the right specialist:
                - BillingAgent: refunds, subscription changes, payment problems
                - TechnicalSupportAgent: bugs, errors, feature questions  
                - OrderAgent: order status, shipping, delivery

                Don't answer questions yourself. Always delegate to the appropriate 
                specialist and return their response directly.";
    }

    public function tools(): iterable
    {
        return [
            new BillingAgent,
            new TechnicalSupportAgent,
            new OrderAgent,
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

Prompting it from a controller:

public function handle(Request $request)
{
    $response = CustomerSupportAgent::make($request->user())
        ->prompt($request->input('message'));

    return response()->json(['reply' => (string) $response]);
}
Enter fullscreen mode Exit fullscreen mode

The parent receives the message, decides which specialist fits, delegates to that Agent, and returns the result. You didn't hardcode routing logic. The model figures out the delegation from the instructions.

Using Different Models Per Sub-Agent

This is where sub-agents clearly beat the old workaround. You can configure a different provider or model on each sub-agent using PHP attributes directly on the class.

Simple billing queries don't need a powerful model. Complex technical debugging does. The official API uses the #[Provider] and #[Model] attributes from Laravel\Ai\Attributes:

use Laravel\Ai\Attributes\Model;
use Laravel\Ai\Attributes\Provider;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\HasTools;
use Laravel\Ai\Enums\Lab;
use Laravel\Ai\Promptable;

#[Provider(Lab::OpenAI)]
#[Model('gpt-4o-mini')]
class BillingAgent implements Agent, HasTools
{
    use Promptable;

    public function instructions(): string
    {
        return 'You are a billing specialist...';
    }

    public function tools(): iterable
    {
        return [new ProcessRefund, new CheckSubscriptionStatus];
    }
}

#[Provider(Lab::Anthropic)]
#[Model('claude-opus-4-5')]
class TechnicalSupportAgent implements Agent, HasTools
{
    use Promptable;

    public function instructions(): string
    {
        return 'You are a technical support engineer...';
    }

    public function tools(): iterable
    {
        return [new QueryKnowledgeBase, new CreateSupportTicket];
    }
}
Enter fullscreen mode Exit fullscreen mode

The SDK also ships #[UseCheapestModel] and #[UseSmartestModel] convenience attributes if you'd rather let the provider decide which model to use rather than hardcoding a model string:

use Laravel\Ai\Attributes\UseCheapestModel;
use Laravel\Ai\Attributes\UseSmartestModel;

#[UseCheapestModel]
class BillingAgent implements Agent, HasTools { ... }

#[UseSmartestModel]
class TechnicalSupportAgent implements Agent, HasTools { ... }
Enter fullscreen mode Exit fullscreen mode

The parent orchestrator doesn't know or care which model its sub-agents use. Each runs with its own attributes. This is the practical cost-reduction pattern: cheap models for routine work, capable models only where reasoning depth matters.

Sub-Agents with Structured Output

Sub-agents support structured output the same way regular agents do. If you want the BillingAgent to always return a typed response:

class BillingAgent implements Agent, HasTools, HasStructuredOutput
{
    use Promptable;

    public function instructions(): string
    {
        return 'You are a billing specialist. Always return structured results.';
    }

    public function tools(): iterable
    {
        return [new ProcessRefund, new CheckSubscriptionStatus];
    }

    public function schema(JsonSchema $schema): array
    {
        return [
            'action_taken' => $schema->string()->required(),
            'resolved'     => $schema->boolean()->required(),
            'message'      => $schema->string()->required(),
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

The parent receives the structured result from the sub-agent and can use it in its own response or pass it directly back to the caller. This is useful when you need predictable shapes in downstream code, not just a string.

Sub-Agents vs Regular Tools vs agent() Helper

The question you'll hit: when does a sub-agent make sense vs a regular tool vs the agent() helper approach?

A regular tool is right when you're executing a deterministic operation. Looking up a database record, calling an API, running a calculation. No LLM needed in the tool itself; just PHP logic.

The agent() helper (covered in the multi-agent patterns guide) is right for quick inline delegation where you don't need a reusable, testable agent class. It's faster to write but harder to test and reuse.

A sub-agent is right when the delegated work itself requires AI reasoning, has its own set of tools, needs different model config, or is complex enough to warrant its own class with proper instructions. If the delegated task would make sense as a standalone agent on its own, it should be a sub-agent.

A practical rule: if the thing you're delegating to could be independently useful in another context (a BillingAgent, a CodeReviewAgent, an OnboardingAgent), make it a sub-agent. If it's a one-off operation that only makes sense in this specific orchestrator, a regular tool is simpler.

When to Reach for Sub-Agents

Sub-agents shine in three specific scenarios.

Domain routing. You have a broad entry point (customer support, document processing, code review) that needs to delegate to specialists. Each specialist has meaningfully different instructions and tools. The orchestrator shouldn't need to know how each domain works. This is the cleanest use case and the one Taylor's tweet shows directly: a parent agent that routes to a RefundAgent without knowing anything about how refunds actually work.

Cost-optimized workflows. Different tasks warrant different model tiers. Route classification and simple lookups to cheaper models. Reserve the expensive models for tasks that actually need reasoning depth. Sub-agents let you encode that decision in configuration rather than in logic. The billing example above uses gpt-4o-mini for straightforward billing queries but claude-opus-4-5 for technical debugging. You set it once per sub-agent class and it applies everywhere that agent is used.

Large context isolation. If each sub-agent starts with a clean context, you avoid burning tokens on conversation history that's irrelevant to the current task. The parent passes only what the sub-agent needs to know. This is particularly useful when you're also dealing with agent safety concerns, a sub-agent with limited context has a smaller blast radius. A billing sub-agent that only sees the billing-related part of the request can't accidentally act on unrelated data it shouldn't have access to.

When to skip sub-agents. Don't reach for them when a single well-prompted agent handles the task cleanly, or when the overhead of multiple LLM calls is disproportionate to the task's complexity. If your use case is a simple chatbot or a single-domain assistant, sub-agents add latency and cost without adding capability. Start simple. The smart assistant tutorial shows what a well-built single-agent system looks like, and a lot of products never need to go beyond that.

The right question is: does the delegated task benefit from having its own dedicated AI reasoning, its own tools, and isolation from the parent's context? If yes, it's a sub-agent. If it's just a database lookup or a deterministic operation, keep it as a regular tool. The tools and memory tutorial covers the regular tool pattern if you need a refresher on when tools are enough.

Passing Context to Sub-Agents

One thing worth knowing: sub-agents are resolved from the container just like top-level agents. That means you can inject dependencies into them through the constructor.

If your BillingAgent needs access to a specific user's subscription data, pass it through:

class CustomerSupportAgent implements Agent, HasTools
{
    use Promptable;

    public function __construct(public User $user) {}

    public function instructions(): string
    {
        return 'You are a customer support orchestrator...';
    }

    public function tools(): iterable
    {
        return [
            new BillingAgent($this->user),
            new TechnicalSupportAgent,
            new OrderAgent($this->user),
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

And in the BillingAgent:

class BillingAgent implements Agent, HasTools
{
    use Promptable;

    public function __construct(public User $user) {}

    public function instructions(): string
    {
        $plan = $this->user->subscription?->plan ?? 'free';

        return "You are a billing specialist for a {$plan} plan customer. 
                Customer name: {$this->user->name}. 
                Handle refund requests, subscription changes, and billing issues.";
    }

    public function tools(): iterable
    {
        return [
            new ProcessRefund($this->user),
            new CheckSubscriptionStatus($this->user),
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

The sub-agent's instructions are dynamically built from the injected user. This is a clean pattern when the specialist's behaviour needs to vary by user context (plan level, account age, support tier). The orchestrator passes the relevant model down to each sub-agent that needs it, keeping the routing logic clean and the specialist logic contained.

Don't over-inject. Sub-agents with too many dependencies become hard to test and understand. The goal is focused specialists. If a sub-agent needs more than one or two injected dependencies, that's often a sign it's trying to do too much.

Testing Sub-Agents

The Laravel AI SDK ships full faking support for agents, confirmed in the official docs: "Fake agents, images, audio, transcriptions, embeddings, reranking, and file stores, so you can ship AI features with real test coverage." This applies to sub-agents the same way it applies to top-level agents.

The general approach is to test each sub-agent in isolation with fake responses, then test the orchestrator separately to verify routing behaviour. Sub-agents are just regular Agent classes, so the same testing patterns from the AI SDK tutorial series apply directly.

For the exact faking API syntax, check the Testing section of the Laravel AI SDK docs. The sub-agent tests follow the same structure as single-agent tests. The only difference is you test each class independently rather than the full orchestration chain at once.

FAQ

Do sub-agents share conversation history with the parent?

No. Each sub-agent has isolated context. The parent passes a task to the sub-agent and the sub-agent works from its own instructions. This is by design and is one of the main benefits of the sub-agent pattern over stuffing agent logic into a tool.

Can a sub-agent have its own sub-agents?

Yes. Since a sub-agent is just a regular Agent class, it can return other Agent classes from its own tools() method. You can nest as deeply as you need, though more than two levels of nesting adds latency and complexity quickly. Most use cases are well-served with one level of sub-agents.

Does this work with streaming?

Yes. Sub-agents support streaming the same way top-level agents do. If the parent agent streams, the response from sub-agents flows through naturally.

What Laravel and PHP versions are required?

The Laravel AI SDK requires PHP 8.2+ and Laravel 12 or 13. Sub-agents are part of the same package, so the same requirements apply.

How does billing work with multiple LLM calls?

Each sub-agent invocation is a separate API call, billed separately at whatever rates your configured provider charges. The cost-optimization angle of using cheaper models for simpler sub-agents is real and worth planning upfront, especially for high-volume endpoints.

Start Building

Sub-agents are a clean solution to the complexity ceiling that single-agent systems eventually hit. The orchestrator stays focused on routing. Each specialist stays focused on its domain. Provider costs stay proportionate to task complexity.

The API is exactly what you'd expect from a Laravel feature: one method change, no boilerplate, full PHP class support. If you've already got agents running in your app, adding sub-agents is a refactor, not a rewrite.

If you're building a multi-agent system and want a second opinion on the architecture, get in touch.

Top comments (0)