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 with Conversation Persistence

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

 Build reliable tool-calling AI agents in Laravel using the AI SDK, with conversation history persisted to the database and structured output contracts enforced at the PHP layer.

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

ShareCopy linkCopied

 ![Laravel AI SDK: Tool-Calling Agents with Conversation Persistence](https://cdn.msaied.com/465/a3f3acc02afd0ab7084573cab1424c97.png) 

  On this page +1. [Building Tool-Calling AI Agents in Laravel](#building-tool-calling-ai-agents-in-laravel)
2. [Persisting Conversation History](#persisting-conversation-history)
3. [Registering Tools](#registering-tools)
4. [The Agent Loop](#the-agent-loop)
5. [Enforcing Structured Output](#enforcing-structured-output)
6. [Key Takeaways](#key-takeaways)

 Building Tool-Calling AI Agents in Laravel
------------------------------------------

The Laravel AI SDK (`prism-php/prism`, or the first-party `laravel/ai` package landing in Laravel 13) gives you a clean abstraction over LLM providers. The interesting engineering challenge is not the API call itself — it is making agents *reliable*: persisting conversation turns, enforcing structured output, and keeping tool execution auditable.

This article focuses on those three concerns with concrete, production-ready patterns.

---

### Persisting Conversation History

Every multi-turn agent needs a conversation store. A simple `agent_conversations` table works well:

```php
Schema::create('agent_conversations', function (Blueprint $table) {
    $table->ulid('id')->primary();
    $table->foreignId('user_id')->constrained()->cascadeOnDelete();
    $table->string('agent');
    $table->json('messages')->default('[]');
    $table->timestamps();
});

```

The `messages` column stores the raw message array that the LLM expects, so replaying or resuming a conversation is trivial.

```php
final class ConversationRepository
{
    public function append(AgentConversation $conv, array $newMessages): void
    {
        $conv->messages = array_merge($conv->messages, $newMessages);
        $conv->save();
    }

    public function forUser(int $userId, string $agent): AgentConversation
    {
        return AgentConversation::firstOrCreate(
            ['user_id' => $userId, 'agent' => $agent],
            ['messages' => []]
        );
    }
}

```

---

### Registering Tools

Tools are plain PHP callables with a schema. Keep each tool in its own class so it can be unit-tested independently.

```php
final class GetOrderStatusTool
{
    public string $name = 'get_order_status';
    public string $description = 'Returns the current status of an order by ID.';

    public function parameters(): array
    {
        return [
            'order_id' => ['type' => 'string', 'description' => 'The order UUID'],
        ];
    }

    public function __invoke(string $order_id): string
    {
        $order = Order::findOrFail($order_id);
        return json_encode([
            'status'      => $order->status->value,
            'updated_at'  => $order->updated_at->toIso8601String(),
        ]);
    }
}

```

Bind all tools through the service container so the agent class stays slim:

```php
$this->app->tag([
    GetOrderStatusTool::class,
    CancelOrderTool::class,
], 'agent.tools.order');

```

---

### The Agent Loop

A tool-calling agent runs in a loop until the model stops requesting tools or a step limit is reached.

```php
final class OrderSupportAgent
{
    public function __construct(
        private readonly ConversationRepository $repo,
        private readonly PrismClient $prism,
        private readonly iterable $tools,
    ) {}

    public function handle(int $userId, string $userMessage): string
    {
        $conv = $this->repo->forUser($userId, 'order-support');

        $this->repo->append($conv, [['role' => 'user', 'content' => $userMessage]]);

        $steps = 0;
        do {
            $response = $this->prism->chat(
                model: 'gpt-4o-mini',
                messages: $conv->messages,
                tools: $this->tools,
            );

            $this->repo->append($conv, $response->newMessages());

            if ($response->finishReason() === 'tool_calls') {
                $toolResults = $this->executeTools($response->toolCalls());
                $this->repo->append($conv, $toolResults);
            }

            $steps++;
        } while ($response->finishReason() === 'tool_calls' && $steps < 5);

        return $response->text();
    }

    private function executeTools(array $calls): array { /* ... */ }
}

```

The `$steps < 5` guard prevents runaway loops — a must in production.

---

### Enforcing Structured Output

When you need machine-readable responses (not just prose), use a typed DTO and validate the model output against it:

```php
final readonly class RefundDecision
{
    public function __construct(
        public bool   $approved,
        public string $reason,
        public ?float $amount,
    ) {}

    public static function fromArray(array $data): self
    {
        return new self(
            approved: (bool) ($data['approved'] ?? false),
            reason:   (string) ($data['reason'] ?? ''),
            amount:   isset($data['amount']) ? (float) $data['amount'] : null,
        );
    }
}

```

Request JSON mode from the provider and decode into the DTO immediately. If decoding fails, throw a domain exception — never let a malformed LLM response silently corrupt downstream state.

---

### Key Takeaways

- **Persist messages as a JSON column** keyed by user + agent; replay is free.
- **One tool = one class**: testable, taggable, and swappable via the container.
- **Always cap the agent loop** with a step limit to prevent infinite tool-call cycles.
- **Decode LLM output into typed DTOs** immediately; validate at the boundary.
- **Log every tool invocation** with its input/output for debugging and auditing.

- [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 
---------------------------

  Why store messages as JSON rather than individual rows?LLM APIs expect the full message array on every request. A single JSON column lets you load and append the history in one query without reassembling rows, which keeps the hot path simple and fast.

   How do you prevent a tool-calling agent from looping indefinitely?Track the number of tool-call iterations and break the loop once a configurable maximum (e.g. 5) is reached. Return a graceful fallback message to the user rather than throwing an exception, so the conversation remains usable.

   Can I unit-test individual tools without hitting the LLM?Yes. Because each tool is a plain invokable class, you can instantiate it directly in a Pest test, pass mock arguments, and assert the returned JSON string — no HTTP call required.

   ![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 articleAI Coding Agents Pass Tests — But Can They Write Idiomatic Laravel?](https://www.msaied.com/public/articles/ai-coding-agents-pass-tests-but-can-they-write-idiomatic-laravel) [Next articleProduction AI Agents in Laravel: Streaming, Token Budgets, and Structured Output Contracts](https://www.msaied.com/public/articles/production-ai-agents-in-laravel-streaming-token-budgets-and-structured-output-contracts-3)  

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

1. [Building Tool-Calling AI Agents in Laravel](#building-tool-calling-ai-agents-in-laravel)
2. [Persisting Conversation History](#persisting-conversation-history)
3. [Registering Tools](#registering-tools)
4. [The Agent Loop](#the-agent-loop)
5. [Enforcing Structured Output](#enforcing-structured-output)
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)
