Laravel AI Tool-Calling Agents with Prism | Mohamed Said        [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://www.msaied.com) [ Home ](https://www.msaied.com) [ Projects ](https://www.msaied.com/projects) [ Articles  ](https://www.msaied.com/articles) [ Certificates ](https://www.msaied.com/certificates) [ Contact ](https://www.msaied.com#contact-section) 

       [  ](https://github.com/EG-Mohamed)       

 [ Home ](https://www.msaied.com) [ Projects ](https://www.msaied.com/projects) [ Articles ](https://www.msaied.com/articles) [ Certificates ](https://www.msaied.com/certificates) [ Contact ](https://www.msaied.com#contact-section) 

  [ home ](https://www.msaied.com)    [ articles ](https://www.msaied.com/articles)    Laravel AI SDK: Tool-Calling Agents and Conversation Persistence        On this page       1. [  Building Tool-Calling AI Agents in Laravel with Prism ](#building-tool-calling-ai-agents-in-laravel-with-prism)
2. [  Why Tool Calling Changes Everything ](#why-tool-calling-changes-everything)
3. [  Defining Typed Tools ](#defining-typed-tools)
4. [  Persisting Conversation History ](#persisting-conversation-history)
5. [  Running the Agent Loop with an Abort Guard ](#running-the-agent-loop-with-an-abort-guard)
6. [  Idempotency for Destructive Tools ](#idempotency-for-destructive-tools)
7. [  Takeaways ](#takeaways)

  ![Laravel AI SDK: Tool-Calling Agents and Conversation Persistence](https://cdn.msaied.com/609/675722df30f32b9b1d547c9b86dce00b.png)

  #laravel   #ai   #agents   #prism   #llm  

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

     30 Aug 2026      4 min read    ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said  

       Table of contents

1. [  01   Building Tool-Calling AI Agents in Laravel with Prism  ](#building-tool-calling-ai-agents-in-laravel-with-prism)
2. [  02   Why Tool Calling Changes Everything  ](#why-tool-calling-changes-everything)
3. [  03   Defining Typed Tools  ](#defining-typed-tools)
4. [  04   Persisting Conversation History  ](#persisting-conversation-history)
5. [  05   Running the Agent Loop with an Abort Guard  ](#running-the-agent-loop-with-an-abort-guard)
6. [  06   Idempotency for Destructive Tools  ](#idempotency-for-destructive-tools)
7. [  07   Takeaways  ](#takeaways)

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

The "chat with your data" demo is easy. A production agent that calls real tools, recovers from errors, and persists multi-turn context across requests is not. This article focuses on that harder problem using [Prism](https://prism.echolabs.dev), the first-class Laravel AI SDK, with OpenAI-compatible providers.

### Why Tool Calling Changes Everything

A plain completion call is stateless and safe. A tool-calling loop is a **state machine**: the model emits a tool call, your code executes it, the result feeds back, and the loop continues until the model emits a final text response or you abort. Each iteration can mutate real data. Getting this wrong means runaway loops, duplicate side effects, and unbounded token spend.

### Defining Typed Tools

Prism tools are plain PHP objects. Keep them thin — they should validate input and delegate to an existing service, never contain business logic themselves.

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

$lookupOrder = Tool::as('lookup_order')
    ->for('Retrieve an order by its numeric ID')
    ->withParameter(new NumberSchema('order_id', 'The order ID to look up'))
    ->using(function (int $order_id): string {
        $order = Order::with('lines')->findOrFail($order_id);
        return json_encode([
            'id'     => $order->id,
            'status' => $order->status->value,
            'total'  => $order->total_cents / 100,
        ]);
    });

```

The closure **must** return a string — that string becomes the tool result message the model sees next.

### Persisting Conversation History

Multi-turn agents need history. Store it as a JSON column on a `conversations` table and hydrate Prism `Message` objects on each request.

```php
// Migration
$table->json('messages')->default('[]');

// Hydration
use EchoLabs\Prism\ValueObjects\Messages\UserMessage;
use EchoLabs\Prism\ValueObjects\Messages\AssistantMessage;

$history = collect($conversation->messages)->map(fn (array $m) =>
    $m['role'] === 'user'
        ? new UserMessage($m['content'])
        : new AssistantMessage($m['content'])
)->all();

```

After each completed agent turn, serialize the updated message list back:

```php
$conversation->update([
    'messages' => collect($response->messages)
        ->map(fn ($m) => ['role' => $m->role->value, 'content' => $m->content])
        ->all(),
]);

```

### Running the Agent Loop with an Abort Guard

Never let the model loop unbounded. Enforce a hard iteration cap and surface a clean error when it trips.

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

$MAX_STEPS = 6;

$response = Prism::text()
    ->using(Provider::OpenAI, 'gpt-4o')
    ->withSystemPrompt('You are a helpful order support agent.')
    ->withMessages($history)
    ->withPrompt($userMessage)
    ->withTools([$lookupOrder, $cancelOrder])
    ->withMaxSteps($MAX_STEPS)
    ->asText();

if ($response->finishReason === FinishReason::ToolCalls) {
    // Model still wanted to call tools after MAX_STEPS — abort gracefully
    throw new AgentLoopException("Agent exceeded {$MAX_STEPS} steps.");
}

```

`withMaxSteps` tells Prism how many tool-call/result round trips to allow before it stops and returns whatever the model last produced.

### Idempotency for Destructive Tools

Tools like `cancel_order` must be idempotent. The model may call the same tool twice if the first result was ambiguous. Guard at the service layer:

```php
->using(function (int $order_id): string {
    $order = Order::findOrFail($order_id);
    if ($order->status === OrderStatus::Cancelled) {
        return "Order {$order_id} was already cancelled.";
    }
    $order->cancel(); // fires domain event, sends email, etc.
    return "Order {$order_id} cancelled successfully.";
})

```

### Takeaways

- **Cap iterations** with `withMaxSteps` and handle the `ToolCalls` finish reason explicitly.
- **Persist messages as JSON** and hydrate typed `Message` objects — never pass raw strings back into the model.
- **Keep tool closures thin**: validate, delegate, return a string. Business logic belongs in services.
- **Make destructive tools idempotent** — the model will sometimes call them twice.
- **Serialize after every turn**, not just at conversation end, so a crash mid-session doesn't lose context.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fwww.msaied.com%2Farticles%2Flaravel-ai-sdk-tool-calling-agents-and-conversation-persistence-3&text=Laravel+AI+SDK%3A+Tool-Calling+Agents+and+Conversation+Persistence) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fwww.msaied.com%2Farticles%2Flaravel-ai-sdk-tool-calling-agents-and-conversation-persistence-3) 

 Frequently Asked Questions 
----------------------------

  3 questions  

     Q01  Does Prism support providers other than OpenAI for tool calling?        Yes. Prism abstracts the provider layer, so Anthropic Claude and other OpenAI-compatible endpoints that support function/tool calling work with the same API. Check the Prism docs for the current provider matrix, as tool-calling support varies by model. 

      Q02  How should I handle tool execution errors so the agent can recover?        Catch exceptions inside the tool closure and return a descriptive error string rather than letting the exception bubble. The model receives the error text as the tool result and can decide to retry with different parameters or inform the user — giving you a recoverable loop instead of a 500. 

      Q03  Is it safe to run the agent loop synchronously in a web request?        Only for short, low-step interactions. For anything that might take more than a couple of seconds, dispatch a queued job, stream progress via Reverb or SSE, and poll from the frontend. Synchronous loops block a PHP-FPM worker for their entire duration. 

  Continue reading

 More Articles 
---------------

 [ View all    ](https://www.msaied.com/articles) 

 [ ![Blackfire & Xdebug Profiling in Laravel: Finding Real Bottlenecks](https://cdn.msaied.com/608/26a2b1fe183034ea35445954544f68f1.png) laravel performance profiling 

### Blackfire &amp; Xdebug Profiling in Laravel: Finding Real Bottlenecks

Stop guessing where your Laravel app is slow. Learn how to use Blackfire and Xdebug profiling together to pinp...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 30 Aug 2026     3 min read  

  Read    

 ](https://www.msaied.com/articles/blackfire-xdebug-profiling-in-laravel-finding-real-bottlenecks-2) [ ![Recursive CTEs and Hierarchical Data in Laravel with PostgreSQL](https://cdn.msaied.com/607/ac508dd27011f0f2c57b0bee7707b740.png) laravel postgresql eloquent 

### Recursive CTEs and Hierarchical Data in Laravel with PostgreSQL

Learn how to query trees and hierarchies—categories, org charts, threaded comments—using recursive CTEs in Pos...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 29 Aug 2026     3 min read  

  Read    

 ](https://www.msaied.com/articles/recursive-ctes-and-hierarchical-data-in-laravel-with-postgresql) [ ![Laravel API Resources: Sparse Fieldsets, Cursor Pagination, and Per-Route Rate Limiting](https://cdn.msaied.com/606/93349b03f4527b9100157c6774bb4ce2.png) laravel api eloquent 

### Laravel API Resources: Sparse Fieldsets, Cursor Pagination, and Per-Route Rate Limiting

Go beyond basic JsonResource usage. This guide covers sparse fieldsets, cursor-based pagination for large data...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 29 Aug 2026     4 min read  

  Read    

 ](https://www.msaied.com/articles/laravel-api-resources-sparse-fieldsets-cursor-pagination-and-per-route-rate-limiting) 

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

Explore

- [Home](https://www.msaied.com)
- [Projects](https://www.msaied.com/projects)
- [Articles](https://www.msaied.com/articles)
- [Certificates](https://www.msaied.com/certificates)
- [Contact](https://www.msaied.com#contact-section)

Connect

- [   hello@msaied.com ](mailto:hello@msaied.com)
- [   +20 109 461 9204 ](tel:+201094619204)

© 2026 Mohamed Said. All rights reserved.

 [  ](https://github.com/EG-Mohamed) [  ](https://www.linkedin.com/in/msaiedm/) [  ](https://wa.me/201094619204) [  ](mailto:hello@msaied.com) [  ](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
