Memory
Memory gives an Agent knowledge that outlives one conversation: preferences, decisions, conventions, and facts. Several conversations can share one memory. A memory comes in one of two kinds:
| File memory | Record memory | |
|---|---|---|
| Unit | A text file with a path | A short record with an ID |
| How the model finds it | An index at the start of a run, then view and grep | Recall at the start of a run, then similarity search |
| Writes | Each write checks its condition against the current file | The last write wins |
| Stores | DirectoryFileStore, or your own FileStore |
mem0 through MEM0_PLATFORM or MEM0_OSS, or your own RecordStore |
Use working state for tasks and notes that belong to one conversation.
File memory
File memory keeps a small tree of text files. Each write checks its condition against the current file when it runs, so a change based on stale content fails instead of overwriting another conversation's work.
Mount a local memory
DirectoryFileStore keeps a memory as plain files under one directory. FileMemoryCapability mounts it under a name:
import asyncio
from a13n_harness import AgentSpec, HarnessBuilder
from a13n_harness.capabilities import FileMemoryCapability, FileMount, MemoryCursors
from a13n_harness.providers.memory import DirectoryFileStore
async def main() -> None:
store = DirectoryFileStore(".memory/user")
cursors = MemoryCursors()
memory = FileMemoryCapability(
[FileMount("user", store, "write", always_load=("README.md",))],
cursors=cursors,
)
executable = HarnessBuilder().build(
AgentSpec(model="openai-responses:gpt-5"),
output_type=str,
capabilities=(memory,),
)
first = await executable.run("I prefer answers in Chinese. Remember that.")
print(first.output_or_raise())
second = await executable.run("What do you know about me?", previous_state=first.state)
print(second.output_or_raise())
asyncio.run(main())
The model addresses the memory by its mount name, user. It never sees the directory path.
What the model gets
- Instructions. Each mount is listed with its name, access, and guide.
guide=NoneusesDEFAULT_FILE_GUIDE, a short rule for what to keep and how to organize it; pass your own text, or""for no guide. - Tools. The
memory_file_*tools are the only way to read and change a memory. Shell and Environment file tools cannot reach it. - Context. At the start of each run, one
<memory-context>block per memory shows itsalways_loadfiles and an index with onepath: descriptionline per file, or only the files that changed since the conversation last saw it.
| Tool | Does | Fails when |
|---|---|---|
memory_file_view |
Lists a directory or reads a file with its version | The path does not exist |
memory_file_grep |
Finds lines containing a text, literal and ignoring case | The regular expression is invalid (with regex=True) |
memory_file_create |
Creates a new file | The path exists |
memory_file_edit |
Replaces old_string with new_string |
old_string does not occur exactly once |
memory_file_append |
Adds text at the end of a file | The file does not exist |
memory_file_move |
Renames a file | The source is missing or the destination exists |
memory_file_delete |
Deletes a file at the version the model viewed | The file changed or no longer exists |
A failed call changes nothing and returns the current file, so the model can read it and decide again. For example, when two conversations viewed a file containing "likes tea" and "reply in English", one can edit "reply in English" to "reply in Chinese" and the other can then edit "likes tea" to "likes coffee": both changes remain. A later edit of "reply in English" fails and returns the file as it is now.
A read mount offers only memory_file_view and memory_file_grep. tools=("view", "grep") narrows the tools for every mount. Tool permission rules can target the tool IDs memory.file.view through memory.file.delete.
Write good memory files
A file is UTF-8 text, at most 64 KiB. Its index line uses the frontmatter description when present, otherwise its first non-empty line:
Paths are relative, such as prefs/language.md; directories exist implicitly. always_load names files whose full content every run starts with. Only the code that builds the mount chooses them, so a conversation cannot pin its own writes into every later conversation.
Keep context small across runs
MemoryCursors records where each memory's context in the conversation stands. With the same cursors, a later run gets only the files changed since the last delivered context, or nothing when nothing changed. Persist cursors.snapshot() next to the conversation's HarnessState and pass MemoryCursors(saved) when you build the next run's Capability. Without cursors, every run gets full context. After automatic compaction or a handoff replaces the history, the Capability clears the cursors, so the next run gets full context again.
DirectoryFileStore has no change journal. Any change to the memory gives the next run full context rather than a change list.
All memory context of one run shares a budget:
from a13n_harness.capabilities import FileMemoryLimits
limits = FileMemoryLimits(context_bytes=16_384, always_load_bytes=4_096, write_retries=3)
memory = FileMemoryCapability([FileMount("user", store, "write")], limits=limits, cursors=cursors)
always_load files come first, then the indexes share the rest. A large index collapses directories into lines such as archive/ (37 files) and is cut with a pointer to memory_file_view when it still does not fit.
Share a memory between processes
Several processes can mount the same directory. DirectoryFileStore serializes each change with a lock file under .a13n-memory/, which it never lists. Cursors belong to a conversation, so keep one MemoryCursors per conversation. The directory store keeps no history; back the directory up yourself, or use a store with history, such as the Service's.
Bring your own file store
A store implements the FileStore protocol from a13n_harness.providers.memory: list, read, compare-and-swap write, move, and delete, changes for the context cursor, and purge. Add search to implement SearchableFileStore; otherwise memory_file_grep reads the files. validate_path() and describe() apply the shared file rules. Origin tells a store with history which run, principal, and tool call made each change.
The File Memory specification defines the complete store contract, context budget, and failure codes.
Record memory
Record memory keeps short records, such as "prefers green tea", and recalls the ones closest in meaning to each run's input. Records have no versions: the last write wins.
Mount mem0
MEM0_OSS opens a namespace of a self-hosted mem0 REST server as a RecordStore, and RecordMemoryCapability mounts it under a name:
import asyncio
import httpx2
from a13n_harness import AgentSpec, HarnessBuilder
from a13n_harness.capabilities import RecordMemoryCapability, RecordMount
from a13n_harness.providers.memory import MEM0_OSS
async def main() -> None:
async with (
httpx2.AsyncClient(timeout=30) as http,
MEM0_OSS.open({"base_url": "http://localhost:8888"}, namespace="alice", http=http) as store,
):
memory = RecordMemoryCapability([RecordMount("facts", store, "write")])
executable = HarnessBuilder().build(
AgentSpec(model="openai-responses:gpt-5"),
output_type=str,
capabilities=(memory,),
)
first = await executable.run("I drink green tea. Remember that.")
print(first.output_or_raise())
# A new conversation recalls the record.
second = await executable.run("What should I order at the cafe?")
print(second.output_or_raise())
asyncio.run(main())
The namespace is the mem0 user_id that holds this memory's records; give each memory its own. Without http, the store opens its own client, which reaches only public HTTPS endpoints, so a local server needs a client of your own. The hosted Platform takes an API key and defaults to https://api.mem0.ai:
import os
from a13n_harness.providers.memory import MEM0_PLATFORM
async with MEM0_PLATFORM.open({}, {"api_key": os.environ["MEM0_API_KEY"]}, namespace="alice") as store:
...
Both add records verbatim, confirm every write by reading it back, and never show or change a record of another namespace.
What the model gets from records
- Instructions. Each mount is listed with its name, access, and guide.
guide=NoneusesDEFAULT_RECORD_GUIDE; pass your own text, or""for no guide. - Recall. At the start of each run, one
<memory-recall>block per memory shows the records closest to the input's text.recall=Falseon a mount turns it off. A recall that fails or takes longer thanrecall_secondsis skipped, and the run goes on. - Tools. The
memory_record_*tools search, list, and change records.
| Tool | Does | Fails when |
|---|---|---|
memory_record_search |
Finds the records closest in meaning to a query | The store is unavailable |
memory_record_list |
Lists records one page at a time | The cursor is not a next_cursor the store returned |
memory_record_add |
Adds a record | The text is blank or longer than record_chars |
memory_record_update |
Replaces a record's whole text | The record does not exist in this memory |
memory_record_delete |
Deletes a record | The record does not exist in this memory |
A write that the store cannot confirm fails with write_unconfirmed: it may or may not have happened, so the model searches before it writes again. A read mount offers only search and list, and tools=("search", "add") narrows the tools for every mount. Tool permission rules can target the tool IDs memory.record.search through memory.record.delete.
RecordMemoryLimits sets the record size and each run's recall:
from a13n_harness.capabilities import RecordMemoryLimits
limits = RecordMemoryLimits(record_chars=8000, recall_limit=5, recall_bytes=8192, recall_seconds=2.0)
memory = RecordMemoryCapability([RecordMount("facts", store, "write")], limits=limits)
A recall block holds up to recall_limit records per memory and keeps the closest ones that fit recall_bytes.
Bring your own record store
A store implements the RecordStore protocol from a13n_harness.providers.memory: search, list with a cursor, add, update, delete, and purge, all bound to one namespace. validate_record_text() applies the shared text rule, and MemoryStoreError carries the failure codes the tools report.
The Record Memory specification defines the complete store contract, the mem0 rules, and recall.