Laravel AI Tool-Calling Agents with Prism | 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 Prism package, with typed tool definitions, conversation history persistence, and clean abort strategies for production workloads.

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

ShareCopy linkCopied

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

  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)

 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.

- [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)
- [prism](https://www.msaied.com/public/articles?search=prism)
- [llm](https://www.msaied.com/public/articles?search=llm)

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

  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.

   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.

   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.

   ![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 articleBlackfire &amp; Xdebug Profiling in Laravel: Finding Real Bottlenecks](https://www.msaied.com/public/articles/blackfire-xdebug-profiling-in-laravel-finding-real-bottlenecks-2) [Next articleCQRS Without Event Sourcing: Practical Read/Write Model Separation in Laravel](https://www.msaied.com/public/articles/cqrs-without-event-sourcing-practical-readwrite-model-separation-in-laravel)  

   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)

 ###  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)
