A stateless agent forgets the previous message the instant it replies. Build a chat feature on top of one and every question lands cold — no idea what the user just asked. Wiring up Laravel AI SDK conversation history takes three moving parts: publishable migrations, a RemembersConversations trait, and a messages() hook. I'll add memory to an agent, resume a conversation by ID, and keep long threads from wrecking the token bill.
Publish the Laravel AI SDK conversation migrations#
The SDK persists turns in two tables it ships as migrations, so the first move is to get those tables into your database. Install the package, publish its config and migrations, then run migrate. If the SDK isn't set up in your app yet, my complete Laravel AI SDK guide walks through provider configuration and API keys first.
composer require laravel/ai
# Publishes config/ai.php and the conversation migrations
php artisan vendor:publish --provider="Laravel\Ai\AiServiceProvider"
php artisan migrate
That migration creates two tables: agent_conversations, which holds one row per conversation, and agent_conversation_messages, which holds one row per turn. Treat them as SDK-owned — you interact with a conversation through the SDK and its returned id, not by reaching into the columns yourself. That keeps your code working if the internal schema shifts between releases.
Add the RemembersConversations trait to your agent#
Memory is opt-in per agent. Generate an agent class, have it implement the Conversational contract, and pull in the RemembersConversations trait alongside Promptable. That combination tells the SDK to load prior turns before every prompt and write the new user and assistant messages after the model responds — persistent memory with zero storage code.
php artisan make:agent SupportAssistant
<?php
namespace App\Ai\Agents;
use Laravel\Ai\Concerns\RemembersConversations;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\Conversational;
use Laravel\Ai\Promptable;
class SupportAssistant implements Agent, Conversational
{
use Promptable, RemembersConversations;
public function instructions(): string
{
return 'You are a support assistant. Use the prior conversation for context and stay consistent with earlier answers.';
}
}
One trap worth calling out: the trait satisfies the Conversational interface for you by supplying its own messages(). If you also hand-write a messages() method on the same agent, your method wins under PHP's trait resolution rules and the automatic history loading silently switches off. Pick one approach per agent. An agent can still carry typed tools the model calls — memory works the same whether or not it does.
Resume a Laravel AI SDK conversation by ID#
A conversation is identified by an id the SDK returns on the first response. Start a fresh thread with forUser(), read conversationId off the response, and stash it somewhere durable — the session, a cache entry, or a column on your own chat model. On the next request you replay that id to pick the thread back up.
use App\Ai\Agents\SupportAssistant;
// First turn — start a new conversation for the signed-in user.
$response = (new SupportAssistant)
->forUser($request->user())
->prompt('My order #4821 never arrived.');
$conversationId = $response->conversationId;
// Persist the id so a later request can continue this thread.
$request->session()->put('support_conversation', $conversationId);
return (string) $response;
To resume, pass the stored id to continue() with the user as the named as: argument, then prompt as normal. The SDK reloads every turn tied to that id, appends the new user message, and stores the assistant reply.
// A later request — continue the same conversation.
$conversationId = $request->session()->get('support_conversation');
$response = (new SupportAssistant)
->continue($conversationId, as: $request->user())
->prompt('It still has not shown up — what happens now?');
return $response->text;
Read the assistant's answer either by casting the response to a string, as in the first turn, or via the $response->text property. Both return the same generated text.
Replay prior turns with a custom messages() method#
The trait is the easy path, but sometimes you want to own the store — your own table, your own ordering, your own cap. Drop RemembersConversations, implement Conversational directly, and return the history from messages(). Whatever this method returns is the context the model sees, so order matters: return it oldest-first, and the SDK appends the new user turn after it.
<?php
namespace App\Ai\Agents;
use App\Models\ChatMessage;
use Illuminate\Contracts\Auth\Authenticatable;
use Laravel\Ai\Contracts\Agent;
use Laravel\Ai\Contracts\Conversational;
use Laravel\Ai\Messages\Message;
use Laravel\Ai\Promptable;
class SupportAssistant implements Agent, Conversational
{
use Promptable;
public function __construct(public Authenticatable $user) {}
public function instructions(): string
{
return 'You are a support assistant.';
}
/**
* The exact context the model sees — oldest turn first.
*
* @return array<int, Message>
*/
public function messages(): iterable
{
return ChatMessage::where('user_id', $this->user->getAuthIdentifier())
->latest()
->limit(50)
->get()
->reverse()
->map(fn (ChatMessage $message) => new Message($message->role, $message->content))
->all();
}
}
The trade-off is ownership: with the trait gone, nothing writes turns for you. You persist each user and assistant message to chat_messages yourself around the prompt call. In exchange you get full control over exactly which rows become context — which is what makes the next step possible.
Trim and summarise long histories to control tokens#
Every replayed turn is input tokens you pay for on every request. Leave a chat running for an afternoon and each new prompt quietly drags a thousand-message transcript along with it. Two levers keep that in check: a hard window that caps how many recent turns go in verbatim, and a rolling summary that stands in for everything older.
public function messages(): iterable
{
$recent = ChatMessage::where('user_id', $this->user->getAuthIdentifier())
->latest()
->limit(20) // hard window: only the last 20 turns go in verbatim
->get()
->reverse()
->map(fn (ChatMessage $message) => new Message($message->role, $message->content))
->all();
// Everything older is collapsed into a single system message.
if ($summary = $this->user->conversation_summary) {
array_unshift($recent, new Message('system', "Summary of the earlier conversation: {$summary}"));
}
return $recent;
}
Regenerate conversation_summary periodically — say every 20 turns — by asking a cheap model to compress the turns you're about to drop. Meter the payoff by tracking token usage and cost so you know when a thread actually needs trimming. And if you need genuine long-term recall rather than a lossy summary, don't replay everything — retrieve only the relevant past turns with a pgvector RAG pipeline. One thing not to reach for here: the #[MaxTokens] attribute caps what the model generates, not the history you send, so it won't trim context for you.
Wrapping Up#
Persistent memory is the line between a demo and a real assistant. Start with the RemembersConversations trait — it's the least code and covers most apps — and only reach for a custom messages() when you need to bound tokens or own the schema. From here, cover the behaviour by faking agents in your Pest tests so your memory logic doesn't regress silently.
FAQ#
How does the Laravel AI SDK store conversation history?
It ships two database migrations. When you publish and run them, the SDK creates the agent_conversations and agent_conversation_messages tables and manages them for you. Each conversation gets a row, and every user and assistant turn is written to the messages table. You read a thread back by its id through the SDK rather than querying the tables directly.
What is the RemembersConversations trait?
It's a trait, Laravel\Ai\Concerns\RemembersConversations, that you add to an agent implementing the Conversational contract. Once it's on the agent, the SDK automatically loads prior turns before each prompt and stores the new user and assistant messages after the model replies — persistent memory without any storage code. Don't also define a messages() method on the same agent, because your method would override the trait and turn the automatic loading off.
How do I resume a previous conversation with a Laravel AI agent?
Capture the id from the first response with $response->conversationId and store it somewhere durable, such as the session, a cache entry, or a column on your chat model. On the next request, call continue($conversationId, as: $user) before prompt(). The SDK reloads every turn tied to that id as context, so the model answers with full awareness of what came before.
How do I stop conversation history from blowing the token limit?
Bound what you replay. Switch from the trait to a custom messages() implementation and cap it with a query limit so only the last N turns go in verbatim. For longer memory, collapse older turns into a single summary system message and prepend that instead of every message. Remember that #[MaxTokens] only caps what the model generates — it does not trim the history you send, so that part is on you.
Can I store AI conversation history in my own table?
Yes. Skip the RemembersConversations trait, implement the Conversational interface, and return your own records from messages() as Message objects ordered oldest-first. Whatever that method returns is exactly the context the model sees. The trade-off is that you become responsible for writing each new turn to your table, since the trait's automatic persistence is no longer in play.