docs / agent-memory / concepts

Concepts

The mental model behind agent-memory, in the order you'll meet it.

The record format

A MemoryRecord is a typed, timestamped, provenance-carrying unit. Four types: fact · episode · procedure · rule.

Identity is content-addressed:

id = "mem:" + sha256( canonical_json(birth_content) )[:32 hex]

where birth content is {type, body, slots, provenance, created_at} — and only that. Lifecycle fields (state, links, confidence, flags, tags, signature) are versioned around the stable id:

Other format properties:

Ownership & signing

An agent's memory is a file it owns. Two signature layers:

Both use a domain-separated signing input (fg-agent-memory/v1 with per-context tags), so a memory signature can never be replayed as an identity artifact.

Two deliberate rules:

The lifecycle state machine

Enforced only in lifecycle.py as pure functions — never by convention, never by an LLM. States: active · transitional · superseded · archived.

The complete legality table (LEGAL_TRANSITIONS):

From To
active superseded, transitional, archived
transitional active, superseded, archived
superseded archived
archived — (terminal)

Rules enforced in code:

Recall — the read stage

Retrieval is state-aware and budgeted:

Operators & proposals

Every mutation an operator — LLM or heuristic — wants must arrive as a typed Proposal of one of four kinds: create · supersede · transition · archive. apply_proposal validates it against the lifecycle machine before any write; an invalid proposal raises without writing — rejected, never repaired — and rejections are logged in the consolidation report.

The default operators are deliberately dumb, model-free heuristics (trigram Jaccard near-dup, slot/negation-polarity contradiction, a cautious newer-wins resolver), so the whole engine runs with zero model dependency. Swap in an LLM operator and the validation gate is identical.

The consolidation pass order

consolidate() runs a fixed sequence:

  1. Exact dedup — byte-identical content addresses merge.
  2. Near-dup merge — trigram similarity above threshold (default 0.6).
  3. Contradiction detection — opens transition pairs.
  4. Resolution — only if a resolver operator is configured; the porcelain default has no auto-resolver, so contradictions stay visible until Memory.resolve.
  5. Episode → rule promotion — a rule is proposed once 3 episodes share a signature (threshold configurable), carrying those episodes as evidence.
  6. Rule-flag sweep — flags rules whose evidence has all been invalidated.

Blocked candidate generation (records sharing a stemmed content word or slot key) keeps consolidation near-linear.

Footgun: pipeline-level default_operators() includes an auto-resolver (HeuristicResolutionOperator) — bare consolidate(store) picks winners automatically. The Memory porcelain deliberately runs without an auto-resolver, so contradictions stay visible until you call Memory.resolve. Also note Memory.load into an existing store merges: records whose ids already exist are skipped, never overwritten.

Decay & salience

Decay is mechanical, not judgment: salience combines recency (half-life 30 days), touch reinforcement (bonus 0.25 per touch), and confidence. Records below the archive threshold (default 0.05) are archived — a lifecycle state, not deletion — and protected records (e.g. disputed ones) are reported rather than touched. Touch counts live in an engine-local salience.json sidecar; losing it degrades gracefully.

Redaction vs archival

Two different exits with different guarantees:

Redaction is available on all three stores, the porcelain, and the MCP server — but it is deliberately not on the RecordStore port and is unreachable by operators. No proposal kind can destroy content.

Storage model — append-only, three stores

RecordStore.put never overwrites: each put of an existing id appends a new version; get returns the latest; history returns all versions oldest-first. Deletion is not in the contract. Three shipped stores pass one shared port-contract suite:

Search indexes: SQLiteSearchIndex (FTS5 BM25 over body + tags; query text is reduced to alphanumeric OR-joined tokens so raw text can't inject FTS syntax) and InMemorySearchIndex (cosine over an Embedder, default the deterministic HashEmbedder). Sidecars (salience.json, index.sqlite) are engine-local, non-standard state — losing them degrades gracefully.

Portability & golden vectors

RecordStore / SearchIndex / Embedder / Operator are ports; every invariant is stateable over the record format alone. Export produces one canonical-JSON MemoryFile (signed if an identity is set); import via Memory.load. Byte-level constructions are pinned by golden vectors in spec/vectors.jsonwhere prose and code disagree, the vectors win.