Laravel AI SDK: Tool-Calling Agents Guide | Mohamed Said       [Skip to content](#main)  [ ![](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png) Mohamed SaidLaravel Backend Engineer ](https://www.msaied.com/public) - [Home](https://www.msaied.com/public)
- [Projects](https://www.msaied.com/public/projects)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [About](https://www.msaied.com/public#about)

           [  Contact](https://www.msaied.com/public#contact) Menu 

Menu
----

Close 

 - [HomeStart here](https://www.msaied.com/public)
- [ProjectsCase studies](https://www.msaied.com/public/projects)
- [ArticlesEngineering notes](https://www.msaied.com/public/articles)
- [CertificatesCredentials](https://www.msaied.com/public/certificates)
- [AboutHow I work](https://www.msaied.com/public#about)
- [ContactGet in touch](https://www.msaied.com/public#contact)

  [Start a conversation](https://www.msaied.com/public#contact) [WhatsApp](https://wa.me/201094619204) [Email](mailto:hello@msaied.com) 

 1. [Home](https://www.msaied.com/public)
2. /
3. [Articles](https://www.msaied.com/public/articles)
4. /
5. Laravel AI SDK: Tool-Calling Agents and Conversation Persistence

 Laravel AI SDK: Tool-Calling Agents and Conversation Persistence
=================================================================

 Build reliable tool-calling AI agents in Laravel using the AI SDK, with typed tool definitions, structured responses, and conversation history persisted to your database without leaking state between requests.

 ![](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp) [Mohamed Said](https://www.msaied.com/public#person) Published 29 Jun 2026 · Updated 29 Jun 2026 · 3 min read

ShareCopy linkCopied

 ![Laravel AI SDK: Tool-Calling Agents and Conversation Persistence](https://cdn.msaied.com/321/6c6dac115e95b0b01f6e7c04c430614c.png) 

  On this page +1. [Building Tool-Calling Agents in Laravel with Conversation Persistence](#building-tool-calling-agents-in-laravel-with-conversation-persistence)
2. [Defining Typed Tools](#defining-typed-tools)
3. [Persisting Conversation History](#persisting-conversation-history)
4. [Running the Agent Loop](#running-the-agent-loop)
5. [Structured Output Contracts](#structured-output-contracts)
6. [Key Takeaways](#key-takeaways)

 Building Tool-Calling Agents in Laravel with Conversation Persistence
---------------------------------------------------------------------

The Laravel AI SDK (the `prism` package from EchoLabs, or the first-party `laravel/ai` direction emerging in the ecosystem) gives you a clean PHP-first interface for building agents that can invoke tools, maintain conversation history, and return structured output. This article focuses on the practical wiring: typed tool definitions, safe state persistence, and avoiding the common trap of leaking conversation context across unrelated sessions.

### Defining Typed Tools

A tool is a callable the LLM can request. Keep each tool as a dedicated class implementing a `Tool` contract so it stays testable in isolation.

```php
use EchoLabs\Prism\Tool;
use EchoLabs\Prism\Schema\StringSchema;
use EchoLabs\Prism\Schema\ObjectSchema;

class LookupOrderTool extends Tool
{
    public function __construct(private OrderRepository $orders) {}

    public function name(): string { return 'lookup_order'; }

    public function description(): string
    {
        return 'Fetch the current status of a customer order by order ID.';
    }

    public function parameters(): ObjectSchema
    {
        return new ObjectSchema(
            name: 'lookup_order_params',
            description: 'Parameters for order lookup',
            properties: [new StringSchema('order_id', 'The UUID of the order')],
            requiredFields: ['order_id'],
        );
    }

    public function handle(string $order_id): string
    {
        $order = $this->orders->findOrFail($order_id);
        return json_encode(['status' => $order->status, 'eta' => $order->eta]);
    }
}

```

Bind it in a service provider so the container resolves its dependencies automatically:

```php
$this->app->bind(LookupOrderTool::class, fn ($app) =>
    new LookupOrderTool($app->make(OrderRepository::class))
);

```

### Persisting Conversation History

The most common mistake is storing conversation turns in the session or, worse, in a static property that survives across Octane workers. Use a proper `conversations` table instead.

```php
// Migration
Schema::create('ai_conversation_turns', function (Blueprint $table) {
    $table->id();
    $table->ulid('conversation_id')->index();
    $table->string('role'); // user | assistant | tool
    $table->text('content');
    $table->json('tool_calls')->nullable();
    $table->timestamps();
});

```

A thin repository hydrates the message array Prism expects:

```php
class ConversationRepository
{
    public function history(string $conversationId): array
    {
        return AiConversationTurn::where('conversation_id', $conversationId)
            ->orderBy('id')
            ->get()
            ->map(fn ($turn) => [
                'role' => $turn->role,
                'content' => $turn->content,
            ])
            ->all();
    }

    public function append(string $conversationId, string $role, string $content): void
    {
        AiConversationTurn::create(compact('conversationId', 'role', 'content'));
    }
}

```

### Running the Agent Loop

```php
use EchoLabs\Prism\Prism;
use EchoLabs\Prism\Enums\Provider;

class AgentService
{
    public function __construct(
        private ConversationRepository $conversations,
        private LookupOrderTool $lookupOrder,
    ) {}

    public function respond(string $conversationId, string $userMessage): string
    {
        $this->conversations->append($conversationId, 'user', $userMessage);

        $response = Prism::text()
            ->using(Provider::OpenAI, 'gpt-4o-mini')
            ->withMessages($this->conversations->history($conversationId))
            ->withTools([$this->lookupOrder])
            ->withMaxSteps(5)
            ->generate();

        $reply = $response->text;
        $this->conversations->append($conversationId, 'assistant', $reply);

        return $reply;
    }
}

```

`withMaxSteps` caps the tool-call loop so a misbehaving model cannot spin indefinitely and exhaust your token budget.

### Structured Output Contracts

When you need machine-readable responses, use a schema-bound response instead of parsing free text:

```php
use EchoLabs\Prism\Schema\ObjectSchema;
use EchoLabs\Prism\Schema\StringSchema;
use EchoLabs\Prism\Schema\EnumSchema;

$schema = new ObjectSchema(
    name: 'support_decision',
    description: 'Triage decision for a support ticket',
    properties: [
        new EnumSchema('priority', 'Ticket priority', ['low', 'medium', 'high']),
        new StringSchema('summary', 'One-sentence summary'),
    ],
    requiredFields: ['priority', 'summary'],
);

$response = Prism::structured()
    ->using(Provider::OpenAI, 'gpt-4o-mini')
    ->withPrompt($ticketBody)
    ->withSchema($schema)
    ->generate();

$decision = $response->structured; // typed array matching your schema

```

### Key Takeaways

- **Persist turns to the database**, never to session or static state — especially critical under Octane.
- **Cap `withMaxSteps`** to prevent runaway tool-call loops from burning tokens.
- **One class per tool** keeps each tool independently testable and container-resolvable.
- **Structured output schemas** eliminate brittle regex parsing of LLM responses.
- **Bind tools via the service container** so they receive their own dependencies cleanly.

- [laravel](https://www.msaied.com/public/articles?search=laravel)
- [ai](https://www.msaied.com/public/articles?search=ai)
- [agents](https://www.msaied.com/public/articles?search=agents)
- [llm](https://www.msaied.com/public/articles?search=llm)

 Frequently asked questions 
---------------------------

  How do I prevent conversation history from growing too large and exceeding the context window?Implement a sliding-window strategy in your repository: return only the last N turns, or summarise older turns into a single system message. You can also store a `summary` column on the conversation and regenerate it periodically with a background job.

   Can tool-calling agents be tested without hitting the real OpenAI API?Yes. Prism ships with a fake driver you can swap in tests via `Prism::fake()`. You provide canned responses and assert which tools were called, keeping your test suite fast and deterministic.

   ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)About the author
----------------

[Mohamed Said](https://www.msaied.com/public#person)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

[About](https://www.msaied.com/public#about) [GitHub ↗](https://github.com/EG-Mohamed) [LinkedIn ↗](https://www.linkedin.com/in/msaiedm/) [WhatsApp ↗](https://wa.me/201094619204) [Email Address ↗](mailto:hello@msaied.com) [My CV ↗](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)  

   [Previous articleLivewire v3 Internals: Morph Markers, JS Hooks, and Alpine Integration](https://www.msaied.com/public/articles/livewire-v3-internals-morph-markers-js-hooks-and-alpine-integration-1) [Next articleYour Symfony App Now Runs on Laravel Cloud](https://www.msaied.com/public/articles/your-symfony-app-now-runs-on-laravel-cloud)  

   On this page
-------------

1. [Building Tool-Calling Agents in Laravel with Conversation Persistence](#building-tool-calling-agents-in-laravel-with-conversation-persistence)
2. [Defining Typed Tools](#defining-typed-tools)
3. [Persisting Conversation History](#persisting-conversation-history)
4. [Running the Agent Loop](#running-the-agent-loop)
5. [Structured Output Contracts](#structured-output-contracts)
6. [Key Takeaways](#key-takeaways)

 ###  Have a technical challenge?

 Tell me what you’re building. I reply within two working days.

[Start a conversation](https://www.msaied.com/public#contact) 

   Related articles
-----------------

 [ ![](https://cdn.msaied.com/740/cce86edc21eddcbdd2f2454fadaf9c70.png)  · 3 min read### The Pipeline Pattern in Laravel: Custom Pipelines Beyond Middleware

5 Oct 2026 ](https://www.msaied.com/public/articles/the-pipeline-pattern-in-laravel-custom-pipelines-beyond-middleware-1) [ ![](https://cdn.msaied.com/739/2d6897fdcdcf090613f96f72a64b8a78.png)  · 4 min read### MySQL Full-Text Search in Laravel: Indexes, Relevance Scoring, and Boolean Mode

4 Oct 2026 ](https://www.msaied.com/public/articles/mysql-full-text-search-in-laravel-indexes-relevance-scoring-and-boolean-mode) [ ![](https://cdn.msaied.com/738/073696a3fefe18bec825beec5ac658f5.png)  · 4 min read### Laravel Queue Rate-Limited Middleware: Throttling Jobs Without Losing Work

4 Oct 2026 ](https://www.msaied.com/public/articles/laravel-queue-rate-limited-middleware-throttling-jobs-without-losing-work) 

  Have a technical challenge?
----------------------------

Tell me what you’re building. I reply within two working days.

 [Discuss your project ↗](https://www.msaied.com/public#contact) 

  © 2026 Mohamed Said · Built with Laravel, meant to last.Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

 - [Home](https://www.msaied.com/public)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [GitHub](https://github.com/EG-Mohamed)
- [LinkedIn](https://www.linkedin.com/in/msaiedm/)
- [WhatsApp](https://wa.me/201094619204)
- [Email Address](mailto:hello@msaied.com)
- [My CV](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
- [Sitemap](https://www.msaied.com/public/sitemap.xml)
