NANOAGENT

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

php
$agent->setMemory(new FileMemory(__DIR__ . '/memory'), 'demo');

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() / fileputcontents() choreography.

How the example wires it up

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

php
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);

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

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

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

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

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:

php
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);

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.

php
$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

Why this is better than the manual file approach

You could still do it by hand — setHistory() on load, fileputcontents(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().

    Ready to ship an agent?

    $ composer require hammrouni/nanoagent

    MIT, zero dependencies. Explore the full series - each article teaches one pattern.

    Get each new article by email - one per publish day + the 12-part cheatsheet when it drops.