What it is
Memory is how an agent keeps useful context across turns. In Phero, memory is a pluggable component: the agent can retrieve prior messages before each turn and save the new messages after each turn.
Different implementations provide different behavior:
- Short-term: keep the last N messages (fast, simple)
- File-backed: persist messages locally as JSON across runs
- PostgreSQL-backed: persist session history in a shared database
- NATS JetStream KV-backed: persist session history in NATS; survives restarts, supports named sessions
- Summarized: compress older history into a structured summary
- Semantic: retrieve by similarity search (RAG-backed)
Simple FIFO memory (in-process)
memory/simple stores recent llm.Message values in a fixed-size FIFO ring buffer.
When the capacity is reached, older messages roll off.
import memory "github.com/henomis/phero/v2/memory/simple"
conversationMemory := memory.New(20)
a.SetMemory(conversationMemory)
This is the default choice for REPL-style conversational agents.
Snapshots: a summary that stands for the oldest messages
A memory that implements memory.SnapshotStore keeps a log of messages plus a stored
memory.Snapshot: a summary (and a few pinned messages, such as the task) that stands for the oldest
messages. Retrieve then returns the summary followed by the messages after it, so a long
conversation stays short without losing its log. memory/simple, memory/jsonfile,
memory/psql and memory/nats implement it; the agent's compaction uses it.
snap, tail, err := store.LoadSnapshot(ctx) // latest snapshot (or nil) and the messages after it, with cursors
// ... summarize, then cover everything up to a message:
err = store.SaveSnapshot(ctx, memory.Snapshot{Summary: text}, tail[len(tail)-5].Cursor, snap)
- A
memory.Cursoris an absolute position in the log: it stays valid while older messages are evicted and newer ones appended, even by another writer. SaveSnapshotcompares and swaps: it fails withmemory.ErrSnapshotConflictwhen the latest snapshot is no longerprev, and withmemory.ErrInvalidCursorfor a cursor that does not move past it.- The summary is sent as a user message (
memory.SummaryMessages), not a system message, so it keeps its place in the conversation. The agent'sSummaryCompactorupdates the previous summary with the new messages (see the agent docs). - The messages a snapshot covers are kept by default;
WithPruneCompacted(true)(simple, jsonfile, psql) drops them;memory/natsdrops them by default, as a session must fit in one NATS message. Snapshots can't be combined with a backend's ownWithSummarization, which rewrites the log (memory.ErrSnapshotsUnsupported).
JSON file memory (persistent local state)
memory/jsonfile stores the full conversation in a JSON file on disk.
The file path acts as the session identifier, so restarting the process and pointing at the same file resumes the same history.
import memory "github.com/henomis/phero/v2/memory/jsonfile"
conversationMemory, err := memory.New("memory.json")
if err != nil {
panic(err)
}
a.SetMemory(conversationMemory)
This is a good fit for local assistants or skill workflows that need persistence without introducing a database.
It keeps snapshots too: the file stays a plain JSON array until the first snapshot,
then holds the messages and the snapshot (older versions of the file are still read), and
jsonfile.WithPruneCompacted(true) rewrites it without the messages a snapshot covers.
For an app that keeps one file per session, SetMeta(key, value) stores metadata in the file, such
as the session's title (an empty value removes the key; Meta() reads it back, and
Clear removes it). jsonfile.Inspect(path) reads a file without opening it as a memory,
to list sessions: the number of messages ever saved, when the file was last written, whether it holds a
snapshot, and its metadata. Keep what a list shows, such as a title from the first prompt, in the metadata: it
outlives pruning.
_ = mem.SetMeta("title", "fix the failing test")
files, _ := filepath.Glob(filepath.Join(sessionsDir, "*.json"))
for _, f := range files {
info, err := memory.Inspect(f)
if err != nil {
continue // not a memory file
}
fmt.Printf("%s %d messages %s %s\n", filepath.Base(f), info.Messages,
info.UpdatedAt.Format(time.DateTime), info.Meta["title"])
}
Summarization: use the agent's compaction
To keep a long conversation short, let the agent summarize it: agent.SetCompaction compacts by
tokens, during the run, and stores the summary as a snapshot in any of these memories,
so the log is never rewritten and the next run starts from the summary.
conversationMemory := memory.New(200)
a.SetMemory(conversationMemory)
a.SetCompaction(agent.NewSummaryCompactor(llmClient, agent.WithSummaryKeepRecent(20)), 150_000)
Deprecated: the backends' own WithSummarization(llm, threshold, size) still works
as before, but it counts messages rather than tokens, calls the LLM inside Save, and replaces the
oldest size messages with a summary system message, rewriting the stored history. It can't be
combined with snapshots (memory.ErrSnapshotsUnsupported). The size must be below the threshold:
a larger one is lowered to threshold - 1.
Semantic memory (RAG-backed)
For long-term recall, you can back memory with a RAG store: messages are embedded and stored in a vector store, then retrieved by similarity search at query time.
The adapter lives at memory/rag and wraps a rag.RAG instance.
import (
ragmemory "github.com/henomis/phero/v2/memory/rag"
"github.com/henomis/phero/v2/rag"
)
ragEngine, _ := rag.New(store, embedder)
conversationMemory := ragmemory.New(ragEngine)
a.SetMemory(conversationMemory)
This is used in the Long-Term Memory example (RAG + Qdrant).
NATS JetStream KV memory (persistent, network-accessible)
memory/nats stores conversation history in a NATS JetStream Key-Value bucket.
Each session is a single KV key; the value is a JSON-encoded []llm.Message.
Because JetStream persists bucket data to disk, memory survives process restarts automatically.
Multiple sessions can coexist in the same bucket by using different session IDs — useful for running independent conversations side-by-side without separate databases.
Several processes can also share one session, for example replicas of the same agent.
Save writes only if the key has not changed since it was read; if another process wrote
first, it reloads the history and tries again, so no messages are overwritten. If the session keeps
changing on every attempt, Save gives up with ErrConcurrentUpdate and stores
nothing.
Size limit. A session is stored as one value, so it must fit in one NATS message: the
server's max_payload (1MB by default), or the bucket's MaxValueSize if that is
smaller. Images are stored base64-encoded, so a few screenshots can be enough. Past the limit,
Save returns ErrSessionTooLarge (a *SessionTooLargeError with the
session and its size). Through an agent, this shows up as agent.ErrSessionSaveFailed: the
answer is still returned, but the turn is not stored, and later turns will not be either. To stay under
it, use the agent's compaction, whose snapshots drop the messages they cover (and their images);
start a new session or Clear this one; raise max_payload on the NATS server;
or, for long sessions with many images, use memory/psql, which stores each message as its
own row.
import (
"os"
"github.com/nats-io/nats.go"
natsmemory "github.com/henomis/phero/v2/memory/nats"
)
nc, _ := nats.Connect(os.Getenv("NATS_URL"))
js, _ := nc.JetStream()
kv, _ := js.CreateKeyValue(&nats.KeyValueConfig{Bucket: "phero_memory"})
conversationMemory, err := natsmemory.New(kv, "session-123")
if err != nil {
// handle error
}
a.SetMemory(conversationMemory)
Start a local NATS server with JetStream enabled:
docker run --rm -p 4222:4222 nats -js
The full walkthrough is in the NATS Memory example, which demonstrates session resumption across two process runs.
A session is one KV value, so it must fit in one NATS message (1 MB by default). It keeps
snapshots, written with the same compare-and-swap as Save, and saving one
drops the messages it covers by default, so a long session stays under the limit;
natsmemory.WithPruneCompacted(false) keeps them.
PostgreSQL conversation memory (persistent)
If you want conversation history to survive process restarts (or be shared across replicas), use memory/psql.
It's a session-scoped store backed by PostgreSQL (JSONB rows) that keeps snapshots, as memory/simple does.
import (
"database/sql"
"os"
_ "github.com/jackc/pgx/v5/stdlib"
"github.com/henomis/phero/v2/memory/psql"
)
db, err := sql.Open("pgx", os.Getenv("DATABASE_URL"))
if err != nil {
// handle error
}
conversationMemory, err := psql.New(db, "session-123")
if err != nil {
// handle error
}
a.SetMemory(conversationMemory)
By default the store auto-creates its table/index (you can disable that with psql.WithEnsureSchema(false)).
It keeps snapshots in a second table named after the first, with
_snapshots appended: one row per session, whose cursor is the seq of the last message it
covers. SaveSnapshot compares and swaps in SQL, so two processes on the same session cannot
overwrite each other's snapshot, and psql.WithPruneCompacted(true) deletes the covered rows in the
same transaction. With WithEnsureSchema(false), create it yourself:
CREATE TABLE IF NOT EXISTS conversation_memory_snapshots (
session_id text PRIMARY KEY,
upto_seq bigint NOT NULL,
snapshot jsonb NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
Run an example
The conversational agent example uses simple FIFO memory, with an option to enable the agent's compaction.
# from repo root
go run ./examples/conversational-agent
# with summarization enabled
go run ./examples/conversational-agent -summarize -compact-at 2000 -keep-recent 6 -max-messages 20
The skills example uses local JSON-backed memory:
cd ./examples/skills
go run .
For NATS-backed persistent memory (start NATS first):
docker run --rm -p 4222:4222 nats -js
go run ./examples/nats-memory -session my-session
For semantic recall, try the long-term memory example (requires Qdrant):
go run ./examples/long-term-memory -qdrant-host localhost -qdrant-collection long_term_memory
Picking the right memory
- Fast prototyping: use
memory/simplewith a reasonable max messages value - Local persistence: use
memory/jsonfilewhen you want history to survive restarts on one machine - Shared persistence: use
memory/psqlwhen sessions should live in PostgreSQL - Lightweight network persistence: use
memory/natswhen you want NATS JetStream KV as the store (easy Docker setup, named sessions) - Long conversations: use the agent's compaction (
SetCompaction) to keep context compact - Long-term recall: use semantic memory via
memory/rag
Related packages
- agent: uses memory to seed/snapshot sessions
- rag: semantic retrieval engine used by RAG memory
- embedding: generates vectors for semantic memory
- vectorstore: stores vectors for semantic memory