Metadata-Version: 2.5
Name: memfmt
Version: 0.4.0
Summary: An agent's memory as Markdown files you own — read, write and check the format.
Project-URL: Homepage, https://github.com/alibaizhanov/memfmt
Project-URL: Source, https://github.com/alibaizhanov/memfmt
Author: Ali Baizhanov
License-Expression: MIT
License-File: LICENSE
Keywords: agent,format,llm,markdown,memory,obsidian
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Libraries
Classifier: Topic :: Text Processing :: Markup :: Markdown
Requires-Python: >=3.9
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == 'dev'
Description-Content-Type: text/markdown

# 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

```bash
pip install memfmt
```

## Use it

```bash
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
```

```python
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:

```bash
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`

````markdown
---
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`

```markdown
---
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`

```markdown
---
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 [commit → deploy] (12✓/0✗)
2. watch the boot log [deploy → boot log]
3. verify /health — expect 200 within 60s [deploy →] (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.

### What a step consumes and produces

The brackets are optional, and they are names rather than types. The point is
not to check shapes; it is to make a failure addressable. Told only that step 3
failed, you cannot tell whether step 3 is broken or whether step 1 handed it
something broken — and that is the difference between fixing the right thing
and rewriting a step that was never at fault.

`memfmt validate` checks that the chain joins up, and reports any step asking
for something no earlier step produces:

```
1 step input(s) with no source:
  send digest — step 3 needs 'summary', nothing before it produces that
```

A step with no brackets declares nothing, which is not the same as declaring
nothing is needed.

### 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.

1. **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.
2. **`memfmt_type` marks a file as ours.** Files without it are ignored, so a
   memory folder can live inside a vault full of somebody's own notes.
3. **Unknown fields are left alone.** Nothing is silently dropped for being
   unrecognised.
4. **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.
5. **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.
6. **A missing record means untracked, not failed.** Absence of brackets on a
   step, or of counts on an `Evolution` line, says nobody measured — never that
   something went wrong.
7. **Trust is smoothed, records are not.** `success_count` and `fail_count`
   are 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.
8. **Round-trip or it is not the format.** `parse(serialise(m)) == m`, and
   serialising what you parsed is byte-identical. `memfmt validate` checks
   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](https://mengram.io?utm_source=memfmt&utm_medium=readme)
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.

```bash
pip install -e ".[dev]"
pytest
```

MIT licensed.
