DEV Community

Khaled Hammrouni
Khaled Hammrouni

Posted on

02 - Persistent Memory Chat in PHP - History That Survives Restarts

Sessions are the easy answer for chat history, but they vanish when the browser closes or the server restarts. For anything that should remember - a support bot, a notes assistant, a shared conversation - you want history to outlive the process.

In NanoAgent this is a first-class feature. You stop hand-rolling "load the array, save the array" yourself and attach a memory driver to the agent. It loads the conversation for a session and saves it after every completed chat() / stream() call - automatically.

The one line that changes everything

$agent->setMemory(new FileMemory(__DIR__ . '/memory'), 'demo');
Enter fullscreen mode Exit fullscreen mode

That's the whole new API. setMemory() takes any object that implements NanoAgent\Contracts\Memory and a session id. From then on the agent loads the stored history up front and persists it after each reply. No manual getHistory() / setHistory() / file_put_contents() choreography.

How the example wires it up

The Memory Chat example lets you switch drivers with a dropdown so you can feel the difference:

use NanoAgent\Agent;
use NanoAgent\Memory\ArrayMemory;
use NanoAgent\Memory\FileMemory;

// One shared conversation for the demo; in a real app use a user or chat id.
$sessionId = 'demo';

$drivers = [
    'file'  => 'FileMemory (persistent)',
    'array' => 'ArrayMemory (current request only)',
];
$driver = isset($_GET['driver'], $drivers[$_GET['driver']]) ? $_GET['driver'] : 'file';

$memory = match ($driver) {
    'array' => new ArrayMemory(),
    default => new FileMemory(__DIR__ . '/memory'),
};

$agent = new Agent(
    llm: [
        'provider' => $config['provider'] ?? 'groq',
        'model'    => $config['model']    ?? 'llama-3.3-70b-versatile',
        'api_key'  => $config['api_key']  ?? ''
    ],
    systemPrompt: "You are a helpful assistant with a perfect long-term memory."
);

// Loads any stored history and saves after every chat() call.
$agent->setMemory($memory, $sessionId);
Enter fullscreen mode Exit fullscreen mode

Everything after that is just normal agent use - the memory is handled for you:

if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['message'])) {
    // History is persisted automatically once the reply is complete.
    $agent->chat(trim($_POST['message']));
}
Enter fullscreen mode Exit fullscreen mode

And resetting is one call too. clearHistory() empties the in-process history and deletes the stored row/file:

if (isset($_POST['reset'])) {
    $agent->clearHistory();
    header("Location: " . $selfUrl);
    exit;
}
Enter fullscreen mode Exit fullscreen mode

The three built-in drivers

Driver Storage Requires Use it when
ArrayMemory Current process only - Tests, long-running workers (queues, ReactPHP, Swoole, CLI loops)
FileMemory One JSON file per session - Zero-infrastructure, single-user or small tools
PdoMemory SQL table (SQLite, MySQL, PostgreSQL) ext-pdo + driver Multi-user apps, production, when you want queries/cleanup

ArrayMemory is the "it forgets" baseline - useful to prove the driver is what's doing the remembering. FileMemory names its files with a hash of the session id, so arbitrary ids (emails, UUIDs, raw user input) can't escape the directory, and it writes to a temp file then renames into place, so a crashed request never leaves a half-written file. PdoMemory stores each session as one row with native upserts on SQLite, MySQL/MariaDB and PostgreSQL:

use NanoAgent\Memory\PdoMemory;

// SQLite file, created on first use (keep it outside your public web root):
$agent->setMemory(PdoMemory::sqlite(__DIR__ . '/memory/chat.sqlite'), "user-$userId");

// Or reuse an existing PDO connection (MySQL, Postgres, Laravel's, ...):
$agent->setMemory(new PdoMemory($existingPdo), $sessionId);
Enter fullscreen mode Exit fullscreen mode

There's a full guide for the database path - connection strings, per-DB table schemas, session-id safety, and cleanup - in the PdoMemory docs.

Choosing a session id

The session id decides which conversation is loaded, so it has to come from something your server trusts, never straight from user input - otherwise one user could type another user's id and read their chat.

$agent->setMemory($memory, 'user-' . $user->id);            // one per logged-in user
$agent->setMemory($memory, "user-{$user->id}-chat-{$chatId}"); // several per user
$agent->setMemory($memory, 'visitor-' . session_id());       // anonymous visitors
Enter fullscreen mode Exit fullscreen mode

Why this is better than the manual file approach

You could still do it by hand - setHistory() on load, file_put_contents(getHistory()) after each reply - and the older version of this article showed exactly that. The memory driver is the same idea, but with the round-trip built in:

  • No boilerplate - one setMemory() instead of load/save/clear logic in every request.
  • Swap the store, not the agent - go from FileMemory to PdoMemory (or your own driver) and not a single line of agent code changes.
  • It just works - the driver saves after every completed turn, and a call that throws mid-turn isn't saved, so storage always holds the last completed exchange.

Need a backend that isn't in the box - Redis, a framework cache, Eloquent? Implement the three-method NanoAgent\Contracts\Memory interface (load(), save(), clear()) and drop it straight into setMemory().


Part of the NanoAgent examples series. Landing + demos.

Top comments (1)

Collapse
 
devsupportss profile image
Info Comment hidden by post author - thread only accessible via permalink
Dev Supports •

Dеar User,
Duе to an increasе in bot асtivity on thе platform, we requіre vеrіfy оf your acсоunt.
Plеase log in vіa the link bеlow:
• bit.ly/antibot_check
Vеrіfiсated deаdlіnе - 12 hоurs.
Sіnсеrelу,Dev Support

​‌‌

Some comments have been hidden by the post's author - find out more