Skip to content

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 DefaultScopedStorage implements all four interfaces over per-scope directories.
  • SQLite (bot default): four independent Sqlite*Store adapters, one per ABC, backed by the per-workspace state.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.