Memory¶
An agent that forgets everything between turns is a chatbot with tools. ModexAgent gives agents layered memory, so short turns add up to long-term competence.
The tiers¶
| Tier | Purpose |
|---|---|
| Session | The active conversation's working memory. |
| Archive | Long-term storage for history that has left the session window. |
| Core Memory | Durable, distilled knowledge the agent manages in-context: SOUL.md (agent identity), USER.md (user profile), MEMORY.md (distilled facts). |
| Pruned | A catalog of messages removed by cleanup, so the model still knows what was taken out. |
| Experience | Reusable lessons written as EXPERIENCE.md reference files. |
Core Memory, not Knowledge
This tier was renamed from "Knowledge" to Core Memory to keep it
distinct from retrieval-style knowledge bases. The XML tag values
(<your_identity>, <user_profile>, <known_facts>) and the file names
(SOUL.md / USER.md / MEMORY.md) are not renamed: they are
agent-facing prompt artifacts and user workspace data.
Core Memory vs. KnowledgeBase¶
These two concepts are easy to conflate. They are distinct:
| Core Memory | KnowledgeBase | |
|---|---|---|
| What | Agent identity, user profile, distilled facts | Retrievable domain knowledge (RAG corpus, FAQ, reference data) |
| Where | In-context: always injected into the system prompt | Out-of-context: retrieved on demand via search / tool / injection |
| Managed by | The agent itself, through scoped file tools | Typically shared across agents or scoped to a pool / workspace |
| Aligned with | Letta's "Core Memory" (persona + human blocks) |
LangChain / LlamaIndex / mem0's RAG-vs-Memory distinction |
The framework deliberately does not ship a KnowledgeBase abstraction: retrieval over domain knowledge is an application-layer concern, not a memory tier. Core Memory is the in-context half of the distinction, and it is the half ModexAgent owns.
One graph-mode feature can masquerade as a KnowledgeBase at first glance: the
shared knowledge base that graph nodes read and write through the
knowledge_base tool (GraphKnowledgeBaseTool). It is a per-workflow
scratchpad for nodes cooperating inside a single graph run. It is not a
memory tier, not part of any agent's long-term memory, and it disappears with
its graph instance. See
The shared knowledge base on the
Graph Engine page.
Pruned: no silent holes¶
Pruned works independently of the archive. When cleanup prunes messages,
they are written to a pruned/{session_id}/ catalog as a grep-able markdown
transcript: a three-line header, then one numbered block per message, every
block closed by a --- separator, so grep "^## \[" prints a line-numbered
table of contents of exactly what was taken out. A PrunedProvider feeds
that catalog back to the model as XML. Every agent, main or subagent, gets
this injection, so pruning never creates silent holes in context.
Memory sections carry no numeric injection priorities. Providers are ordered by their position in the pipeline list, and the order is core memory first, then archive summaries, then the pruned catalog.
Experience: written, not just stored¶
To the agent, Experience behaves like the other tiers: EXPERIENCE.md
reference files it can consult. Structurally, though, it is wired as a tool
supplement, not as memory-stack plumbing. An agent declares
tool_supplements: [experience]; the scope compiler name-merges the
experience tool into the compiled roster, and the same declaration
auto-injects the experience_review hook (an AfterGraph hook that fires when
the agent's run finishes). The supplement is main-agent-only, matching the
main_agent_memory preset family; the subagent_memory preset carries no
experience. In the shipped bot project it is declared on the default and
reviewer main agents.
The experience_review hook spawns an ExperienceReviewAgent, itself a
ReAct agent, which reviews conversations and creates or updates
EXPERIENCE.md files, turning one-off interactions into reference knowledge
the agent can consult on later tasks. The experience layer is injected as
<available_experiences> XML.
Long-term memory is opt-in¶
Session memory, compaction, and the pruned catalog are always on. The two long-term layers, Archive and Core Memory, ship off by default, because they are the layers that accumulate without bound.
Their config home is the scope declaration
(config/scopes/bot.yml): a memory: override block on an agent node.
agents:
default:
memory:
archive_enabled: true
core_enabled: true
core_enabled requires archive_enabled. Core Memory is fed by archive
consolidation, so enabling it without the archive fails declaration
validation at boot. The toggles apply within the position-derived preset
family: main agents start from main_agent_memory, which is eligible for
archive, core, and experience but ships with every long-term layer off, and
a memory: block switches layers on. See
Scope & Assembly for the declaration tree and the
position-derived defaults.
The same toggles are editable in the WebUI Scope editor, which writes the
memory: block back to bot.yml; changes take effect after a restart. The
Dream Engine (below) runs only when both layers are on.
Scopes: who a memory belongs to¶
Every memory carries a scope that decides who shares it. Available scopes
include Session, User, Tenant, Agent, Channel, Chat, Composite, and Global,
with SessionScope, UserScope, and GlobalScope as the common cases. A fact
stored at User scope follows the user across sessions; a Session-scoped fact
dies with the conversation.
Storage contract: split store ABCs¶
Memory storage is backend-pluggable through split store ABCs (per ADR-0023). Four focused interfaces replace the old monolithic storage god-interface:
| ABC | Responsibility |
|---|---|
MessageStore |
Session messages: append, read, prune, soft-delete |
KVStore |
Key-value pairs (core memory blocks, cursors) |
CursorStore |
Read cursors for archive consumers |
ArchiveStore |
Long-term archive documents |
A MemoryStoreBundle composes all four into one frozen value returned by the
storage registry. Two backends ship:
- File (framework default): one
DefaultScopedStorageimplements all four interfaces over per-scope directories. - SQLite (bot default): four independent
Sqlite*Storeadapters, one per ABC, backed by the per-workspacestate.db.
Switching backends is a config flag (persistence.backend); no data migrates
automatically, so users opt in.
Compression vs. governance¶
Two very different processes shape what the model sees, and mixing them up corrupts memory.
Governance output is never written back
Compression mutates persisted session and archive memory through lifecycle hooks. Governance mutates only the copy sent to the LLM before a model call; its output is never written back to the session. Governance is a lens on memory, not an editor of it.
Compression runs as a hook-driven cleanup pipeline. When a session crosses its
token threshold, cleanup picks a tool-chain-safe prune boundary, generates a
structured compact summary, commits [compact_summary] plus the kept tail
back to the session, writes the pruned catalog, and, only when the archive
layer is enabled, generates archive documents. Compact summaries include
reasoning_content: the compactor serializes each assistant message's
reasoning chain alongside its content, so the summary captures what the agent
thought, not just what it said.
One structural invariant applies everywhere: tool-call chains must stay legal.
An assistant message with tool_calls must never be separated from its
matching tool results, or the model API will reject the context. Compression
honors this: if a boundary would split a tool chain, the whole chain is
evicted (archived), never partially kept.
Memory also has its own hook family, separate from the runtime HookPoint
set: five MemoryHookPoints dispatched by the memory system.
| Hook point | Fires when |
|---|---|
CLEANUP_TRIGGERED |
Session cleanup starts. |
CLEANUP_FINISHED |
Cleanup has committed the session, pruned catalog, and archive. |
CONTEXT_ASSEMBLED |
A memory context assembly finishes, with per-section token accounting. |
CORE_MEMORY_UPDATED |
A core memory file changes. |
CONSOLIDATION_FINISHED |
A core or dream consolidation finishes. |
The built-in TodoReorientationHook fires at CLEANUP_FINISHED, for example,
to re-orient the agent's todo list after a prune.
The Dream Engine: closing the self-learning loop¶
Over time, archives pile up. The Dream Engine consolidates archived history
into Core Memory, so what the agent has done becomes what the agent knows. It
is the only path from archive to Core Memory: nothing else writes
consolidation results. Together with the ExperienceReviewAgent, it closes
the self-learning loop:
flowchart TD
S["Sessions"] -->|compression| A["Archive"]
A -->|Dream Engine<br/>consolidation| C["Core Memory<br/>SOUL · USER · MEMORY"]
S -->|ExperienceReviewAgent| E["Experience<br/>EXPERIENCE.md"]
C -->|injected every turn| LLM["LLM context"]
E -->|injected as XML| LLM
Sessions feed archives, archives feed Core Memory, and conversations feed Experience; all three flow back into the LLM context on every turn.
Subagent memory¶
Subagents run leaner. Position in the declaration tree fixes their memory
family: every non-root node gets the subagent_memory preset, which carries
session compression, the pruned catalog, and governance, nothing else. A
subagent's session memory is temporary and cleared when the subagent
finishes. A memory: block on a non-root node cannot opt into long-term
layers; its only override face is the session compression threshold.
Long-term remembering, and experience review, are the main agent's job.
Where to next¶
- Memory feeds the LLM node of the Graph Engine on every pass.
- Where the
memory:block lives, and the position-derived defaults it overrides, is Scope & Assembly. - Subagent session-only memory is part of the Multi-Agent isolation story.
- See memory in action by running the bot in Get Started.