memfmt
An agent's memory as Markdown files you own.
Every agent that remembers anything invents its own way to store it: one
MEMORY.md that grows until it stops fitting in context, a bespoke JSON blob,
a folder of notes with no rules. Nobody can read anybody else's, nothing
diffs cleanly, and moving between tools means writing a converter.
memfmt is a small spec and a dependency-free Python library for the boring
version of that: memory as plain Markdown, one thing per file, relations as
[[wikilinks]]. Git gives you diffs, review and rollback. Obsidian draws the
graph with no configuration, because the links are the graph.
memory/
MEMORY.md index — what is in here
entities/Ali.md what is true
episodes/2026-07-30-deploy-failed.md what happened
procedures/deploy to Railway.md how to do it, and whether it works
profile.md
No account, no server, no network. This library reads and writes files.
Install
pip install memfmt
Use it
memfmt stat ./memory # what is in here
memfmt validate ./memory # would any file lose data if a tool rewrote it?
memfmt context ./memory "why did the deploy fail" # the relevant bits, to pipe into a model
from memfmt import load, serialise, write_dir, canonical
memory = load("./memory")
for p in memory.procedures:
print(p.name, p.version, p.reliability) # deploy to Railway 3 86% reliable
write_dir(serialise(memory), "./memory")
memfmt context is the one to try first. It picks the files relevant to a
question and prints them, so you can pipe your agent's own memory into a
prompt without a database:
memfmt context ./memory "deploy railway pool" | pbcopy
The format
Three kinds of memory, because agents forget in three different ways.
Entities — what is true
memory/entities/<name>.md
---
memfmt_type: entity
entity_type: person
id: e1
---
# Ali
## Facts
- prefers Rust for memory safety
- based in Tokyo
## Relations
- works at → [[Mengram]] — since 2024
- mentored by ← [[Kenji]]
## Knowledge
**[snippet] deploy command** — how the service ships
```
railway up --detach
```
→ is outgoing, ← incoming. Text after — is a note on the relation.
When a name cannot be a filename, the link carries an alias and the real name
survives: [[cloud-api.py|cloud/api.py]].
Episodes — what happened
memory/episodes/<date>-<summary>.md
---
memfmt_type: episode
id: ep1
happened: 2026-07-30
outcome: rolled back, raised pool_max
valence: negative
importance: 4
participants:
- Ali
- Railway
---
# deploy failed on a cold pool
Two workers booted at once and the session pooler refused the fourth client.
**Outcome** — rolled back, raised pool_max
An event with no outcome teaches nothing, so outcome is the field that earns
an episode its place.
Procedures — how to do something, and whether it works
memory/procedures/<name>.md
---
memfmt_type: procedure
id: p1
version: 3
success_count: 11
fail_count: 1
---
# deploy to Railway (v3 · 86% reliable)
**When** — a change lands on main
**Preconditions**
- tests pass
- pool_max is set
## Steps
1. push to main — the webhook does the rest (12✓/0✗)
2. watch the boot log
3. verify /health — expect 200 within 60s (9✓/3✗)
## Evolution
- v1 → v2 (2026-06-02, 4✓/2✗): added the health check
- v2 → v3 (9✓/1✗): wait for the pool before probing
This is the file the format exists for. A workflow on its own is a guess
somebody wrote down. With 11 ✓ / 1 ✗ and the revisions that produced it, it
is evidence — and an agent can tell the difference between a step that has
worked eleven times and one nobody has ever run.
The counts in an Evolution line are what the retiring version had achieved,
not the new one's. They exist so a revision can inherit a prior.
Why steps carry their own record
Because whole runs are frequently too unlike each other to compare. "Deploy a service" is a different job every time — different service, different failure — so a success rate over runs of the workflow is close to meaningless.
The steps inside them are not. verify /health either held or it did not, on
each of those otherwise incomparable runs. A record on the step survives the
variation that destroys a record on the procedure, and it is also what stops
one flaky dependency from indicting every step beside it: above, step 3 is the
one to look at, and steps 1 and 2 are not implicated.
A step with no brackets is untracked, not failing. Nothing written before this existed changes meaning.
Why the heading is not success / total
Because a bare ratio punishes the revision. A new v3 opens at 0 ✓ / 0 ✗ and reads worse than the v2 it was written to fix, so an agent comparing the two keeps choosing the version that already failed. Progressive delivery and CI met this years ago — a canary confidence record and a flake quarantine ledger are the same idea — and both answered it by smoothing against a prior instead of comparing raw counts.
So the percentage is a smoothed estimate, and the word after it tells you which kind of claim it is:
| state | means |
|---|---|
untested |
no runs, and no lineage to draw on |
81% expected |
no runs of its own; this is what its lineage suggests |
86% reliable |
its own record, smoothed so one lucky run is not 100% |
The prior is the previous version's record at retirement, discounted by half: a predecessor's history is evidence about a successor without being a claim about it. Carrying the counts forward whole would be that claim, and false. The raw counts stay in frontmatter unsmoothed — the estimate is derived, the record is data.
Rules
A short list, because a format nobody can hold in their head gets implemented wrong.
- Frontmatter is the source of truth. The
(v3 · 86% reliable)in a heading is rendered from it for the reader. Edit the heading and the numbers do not change — the parser reads the frontmatter. memfmt_typemarks a file as ours. Files without it are ignored, so a memory folder can live inside a vault full of somebody's own notes.- Unknown fields are left alone. Nothing is silently dropped for being unrecognised.
- Bullets are one line. Facts, steps and relation notes are collapsed to a single line when written, so what a file says and what a parser reads back are the same thing.
- A folder is a set, not a list. Reading a directory cannot recover the
order of the list that wrote it. Use
canonical()to compare two memories, and to keep git diffs to the lines that actually changed. - A missing record means untracked, not failed. Absence of brackets on a
step, or of counts on an
Evolutionline, says nobody measured — never that something went wrong. - Trust is smoothed, records are not.
success_countandfail_countare exactly what happened. The percentage in the heading is an estimate derived from them and from the lineage, and is never a source of truth. - Round-trip or it is not the format.
parse(serialise(m)) == m, and serialising what you parsed is byte-identical.memfmt validatechecks exactly this against a real folder.
Why files
Because the alternative is that your agent's memory lives somewhere you cannot read, cannot grep, cannot correct, and cannot take with you.
Files give you the things a database makes hard: git diff on what your agent
learned this week, a pull request when it learns something wrong, git revert
when it learns something harmful, and a graph view for free. And when the tool
that wrote them goes away, the memory does not.
Where files stop being enough
Honestly: at a few hundred of them.
Word overlap is the best memfmt context can do without embeddings, and it
starts missing things that are phrased differently. Syncing a folder between
machines or a team is a real problem, not a git pull away. Deduplicating
facts that contradict each other needs a model.
That is a server's job, and memfmt does not pretend otherwise. If you get
there, Mengram
writes this format today — mengram export markdown ./memory hands you a tree
this library reads — and adds the search, sync and deduplication that files
alone cannot do. Syncing a folder back into it is not built yet.
Either way the files stay yours, and if you never need a server, this library does not expire.
Contributing
The test suite is the specification in executable form. If you are proposing a
change to the format, the change to tests/test_roundtrip.py is the proposal.
pip install -e ".[dev]"
pytest
MIT licensed.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file memfmt-0.3.0.tar.gz.
File metadata
- Download URL: memfmt-0.3.0.tar.gz
- Upload date:
- Size: 21.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
24c1d1dcdf4144f225619a09a88442bc21e1660c73c23ac8854a3c333218ec67
|
|
| MD5 |
e7d8ba68e942dd6d5eea205f08a70cf1
|
|
| BLAKE2b-256 |
44509a2c3d6c9a04311a05174008d68889d431d38dac6d489eedd2a008ac28d5
|
File details
Details for the file memfmt-0.3.0-py3-none-any.whl.
File metadata
- Download URL: memfmt-0.3.0-py3-none-any.whl
- Upload date:
- Size: 21.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2935323dbd6d9b5dd03cdd52decccad5c0d42b213fcac3fc96d67bd5c599b97f
|
|
| MD5 |
e5654f3917ad0670f73eb9d4f3d09aac
|
|
| BLAKE2b-256 |
2e7eb784b64bdc38ccaf51ac3299e718000f126d5b846d0bda992e68865b3342
|