Examples
Runnable recipes. These snippets mirror the repo's README (including the ghost-memory
demo) and its examples/ directory — quickstart.py is the hello world below,
llm_operator.py the LLM-operator recipe; the wiring example composes documented APIs.
Quickstart — two calls
from fg_agent_memory import Memory
memory = Memory("./memory") # readable directory you can commit to git
memory.remember("John's favorite editor is Zed.")
print(memory.recall("what editor does John use?").as_prompt_block())
Ghost memory — contradiction, resolve, history
from fg_agent_memory import Memory
memory = Memory("./memory")
a = memory.remember("The staging database is Postgres.")
b = memory.remember("The staging database is not Postgres.")
memory.consolidate() # detects contradiction; neither side dropped
print(memory.recall("staging database").as_prompt_block())
# Relevant memories (query: ...):
# - The staging database is Postgres. [disputed: one side negates the other]
# - The staging database is not Postgres. [disputed: one side negates the other]
memory.resolve(a.record_id, b.record_id,
"checked infra: staging moved off Postgres in March")
print(memory.recall("staging database").as_prompt_block())
# Relevant memories (query: ...):
# - The staging database is not Postgres.
print(memory.recall("staging database", include_history=True).as_prompt_block())
# Relevant memories (query: ...):
# - The staging database is not Postgres.
# - The staging database is Postgres. [superseded — kept for history]
The loser keeps its full version trail
(active → transitional → superseded across history), with
superseded_by pointing at the winner and the resolve reason recorded as
state_reason.
Value-swap detection — no negation word needed
Contradictions aren't only "X" vs "not X". Inferred subject→value slots catch "X is A" vs "X is B":
from fg_agent_memory import Memory, RecordState
memory = Memory(tmp_path / "memory", auto_consolidate=False)
first = memory.remember("The staging database is Postgres.")
second = memory.remember("The staging database is MySQL.")
memory.consolidate()
assert memory.store.get(first.record_id).state is RecordState.TRANSITIONAL
Also covered by the heuristics: morphological negation (requires vs
does not require) and polarity frames (no restart needed vs requires a restart).
Non-disputes stay untouched — likes coffee vs likes tea is not a contradiction.
Auto-consolidation cadence
By default the porcelain consolidates every 4 remembers. Tighten the cadence and
disputes surface without ever calling consolidate():
memory = Memory(tmp_path / "memory", auto_consolidate_every=2)
a = memory.remember("The deploy pipeline is green.")
b = memory.remember("The deploy pipeline is not green.")
assert b.consolidation is not None # cadence hit; dispute already surfaced
MCP server
fg-agent-memory-mcp --path ~/agent-memory
{ "mcpServers": { "memory": { "command": "fg-agent-memory-mcp",
"args": ["--path", "/home/me/agent-memory"] } } }
Tools exposed: remember, recall, consolidate, redact, status.
Bring your own store, index, and operators
Every seam is a constructor argument — the porcelain composes ports. The shipped
operators import straight from the top-level package, and ConsolidationOperators
defaults near_dup to the trigram operator at the standard threshold, so swapping
in one custom operator is a one-field construction:
from fg_agent_id import KeyPair
from fg_agent_memory import (
Memory, SQLiteRecordStore, SQLiteSearchIndex,
ConsolidationOperators, HeuristicContradictionOperator,
)
memory = Memory(
store=SQLiteRecordStore("memory.db"),
indexes=(SQLiteSearchIndex("index.sqlite"),),
operators=ConsolidationOperators(
contradiction=HeuristicContradictionOperator(),
resolver=None, # keep contradictions human-resolved
),
identity=KeyPair.generate(),
)
A custom write stage is one more kwarg — extractor= takes any Operator
(propose(text, provenance)). An LLM extractor or consolidation operator still
only emits typed Proposals — the lifecycle validation gate is identical, and
invalid proposals are rejected, never repaired.
An LLM as a validated operator
The repo's examples/llm_operator.py
implements the ContradictionOperator port with a model doing the detection.
Everything around the model call is real — prompt construction, strict response
parsing, fail-closed error handling (a flaky model must never manufacture a
dispute), and the transition Proposal the pipeline validates. Only call_llm
is a stub: wire it to any provider and the rest works unchanged.
from llm_operator import LLMContradictionOperator # examples/llm_operator.py
from fg_agent_memory import Memory, ConsolidationOperators
memory = Memory("./memory", operators=ConsolidationOperators(
contradiction=LLMContradictionOperator(), # llm= defaults to call_llm
))
Export and load a signed memory file
from fg_agent_id import KeyPair
from fg_agent_memory import Memory
keys = KeyPair.generate()
memory = Memory("./memory", identity=keys)
memory.remember("Deploys go out on Tuesdays.")
# or skip key management entirely: a keyfile path is created on first
# run and reloaded ever after
memory = Memory("./memory", identity="agent.key")
memory.export("agent.memory.json") # one canonical-JSON MemoryFile, file-signed
# elsewhere — verification is on by default and REQUIRED:
restored = Memory.load("agent.memory.json", path="./restored-memory")
# verify=True demands a valid file signature and verifies every
# record that names a signer; it never silently downgrades.
Redaction — the governed escape hatch
record = memory.redact(record_id, "contained an API token")
assert record.redacted
assert record.body == "[redacted]"
Every stored version is replaced by one tombstone: id, type, timestamps, and the link
graph survive; body, slots, tags, and provenance are destroyed; the full
birth_digest is recorded. Distinct from decay — archival keeps content. Redaction is
unreachable by operators and deliberately absent from the RecordStore port.