API reference
Import path: fg_agent_memory. All record and result dataclasses are frozen.
Most of the surface below is exported from the top-level package. Exceptions: the
consolidation operators (TrigramNearDupOperator, HeuristicContradictionOperator,
HeuristicResolutionOperator, the operator ABCs, trigram_similarity) and the report
primitives (Action, Rejection) import from fg_agent_memory.pipeline; and
REDACTED_BODY is a class attribute (MemoryRecord.REDACTED_BODY), not a module constant.
RememberResult/RecallResult are keyword-only dataclasses. Everything else.
Memory — the porcelain
The one class most consumers touch.
Memory(
path: str | Path | None = "./memory", *,
store: RecordStore | None = None,
indexes: tuple[SearchIndex, ...] | list[SearchIndex] | None = None,
extractor: Operator | None = None,
operators: ConsolidationOperators | None = None,
consolidation_config: ConsolidationConfig | None = None,
decay_config: DecayConfig | None = None,
identity: KeyPair | None = None,
source_kind: str = "conversation",
auto_consolidate: bool = True,
auto_consolidate_every: int = 4,
)
Methods
remember(text: str, *, tags=None, source: str | None = None) -> RememberResult
# automatic content-address dedup: a repeat reinforces + merges tags
# rather than duplicating. Raises ValueError on empty text.
recall(query: str, *, budget_tokens: int = 2000,
include_history: bool = False,
include_archived: bool = False) -> RecallResult
consolidate() -> ConsolidationReport
decay() -> DecayReport
resolve(record_id: str, winner_id: str, reason: str) -> None
# settle a contradiction; loser superseded-but-addressable.
# Requires a reason and two distinct ids that are the two sides
# of the same transition.
redact(record_id: str, reason: str) -> MemoryRecord
# §11 governed content destruction — tombstones every version.
export(path) -> MemoryFile # portable file; signed if identity set
Memory.load(file_path, *, path=None, verify=True, **kwargs) -> Memory
# classmethod. With verify=True (default) the file signature is REQUIRED
# and every record naming a signer is verified.
status() -> dict[str, object] # counts by state/type, open contradictions,
# path, agent
store # property -> underlying RecordStore
Result types (frozen)
RememberResult(record_id, deduped, ingest, consolidation)
RecallResult(block) # .records, .ids, __len__, __iter__
RecallResult.as_prompt_block() -> str
# renders "- body" lines with markers:
# [disputed: reason] · [superseded — kept for history]
# [archived] · [flagged: reason]
Records — the portable format
class RecordType(StrEnum): FACT · EPISODE · PROCEDURE · RULE
class RecordState(StrEnum): ACTIVE · SUPERSEDED · TRANSITIONAL · ARCHIVED
Provenance(kind: str, ref: str | None = None, agent: str | None = None)
# frozen; model_dump / model_validate
Transition(sides: tuple[str, str], reason: str)
# both contradicting records carry the same block
MemoryRecord — frozen dataclass, kw_only. Fields:
| Field | Notes |
|---|---|
id |
content address; computed when empty |
type, body, slots |
birth content |
state |
lifecycle state |
confidence |
float in [0, 1]; 4-decimal string on the wire |
created_at, updated_at |
write time |
occurred_at |
optional event time; immutable, signed, omitted when unset |
provenance |
source of the record |
evidence |
record ids; required non-empty for rules |
supersedes, superseded_by, transition, state_reason |
link graph |
tags, flagged, flag_reason |
annotations |
signed_by, signature |
record signature |
redacted, redaction_reason, birth_digest |
redaction tombstone fields |
extra |
unknown wire fields — preserved and signed |
Methods & classmethods:
MemoryRecord.create(type, body, provenance, *, slots, confidence=1.0,
evidence, supersedes, tags, created_at, occurred_at, extra)
MemoryRecord.tombstone(original, *, reason, now)
record.evolve(update) # new lifecycle version; CLEARS the signature
record.sign(keys, address)
record.verify()
record.model_copy() / model_dump() / MemoryRecord.model_validate(...)
REDACTED_BODY = "[redacted]"
MemoryFile (frozen): fields format=FORMAT_VERSION, agent, created_at,
records, signature, extra. Methods: create(records, *, agent, created_at),
canonical_bytes(), sign(keys), verify(), model_dump/validate/copy.
Confidence wire helpers:
confidence_to_wire(float) -> str # 4-decimal string (canonical JSON forbids floats)
confidence_from_wire(Any) -> float
Lifecycle — pure state machine
LEGAL_TRANSITIONS: frozenset[tuple[RecordState, RecordState]]
# active→superseded, active→transitional, transitional→active,
# transitional→superseded, {active,superseded,transitional}→archived
is_legal(current, target) -> bool
assert_transition(current, target) -> None # raises LifecycleError
supersede(old, successor, *, reason, now=None) -> MemoryRecord
# successor must already reference old via `supersedes`
begin_transition(a, b, *, reason, now=None) -> tuple[MemoryRecord, MemoryRecord]
resolve_transition(winner, loser, *, reason, now=None) -> tuple[MemoryRecord, MemoryRecord]
archive(record, *, reason, now=None) -> MemoryRecord
refresh_rule_flag(rule, evidence_records: dict[str, MemoryRecord],
*, now=None) -> MemoryRecord
# flags a rule when ALL evidence is archived/superseded;
# any live evidence clears the flag
Ports & proposals
class RecordStore(ABC):
put · get · history · list(*, state=None, type=None, tag=None) · __iter__
# `redact` exists on concrete stores but is deliberately NOT on the port
class Embedder(ABC):
dimensions # property
embed(text) -> tuple[float, ...]
class SearchIndex(ABC):
index(record)
candidates(query, k) -> tuple[tuple[str, float], ...]
class Operator(ABC):
propose(text, provenance) -> tuple[Proposal, ...]
Reference implementations: InMemoryRecordStore, InMemorySearchIndex,
HashEmbedder(dimensions=256) (deterministic, L2-normalized bag-of-words),
HeuristicExtractor(*, confidence=0.5, tags=()) (regex sentence shapes; never
creates rules).
class ProposalKind(StrEnum): CREATE · SUPERSEDE · TRANSITION · ARCHIVE
# the only mutations an operator may request
Proposal(kind, record=None, target_id=None, other_id=None, reason="") # frozen
apply_proposal(store, proposal) -> tuple[MemoryRecord, ...]
apply_proposals(store, iterable)
# validate against the lifecycle machine, then apply;
# raise WITHOUT writing on invalid
Retrieval
retrieve(query, *, store, indexes, budget_tokens, now=None,
include_archived=False, include_history=False,
touches=None, candidates_per_index=32) -> RetrievedBlock
TouchCounts # sidecar: touch(id), count(id)
RetrievedRecord(record, score, tokens, reasons)
RetrievedBlock(query, records, budget_tokens, total_tokens) # .ids
Scoring: score = relevance × state_weight × recency × salience × confidence.
Constants: half-life 30 days · recency floor 0.1 · state weights active 1.0 /
transitional 0.8 / superseded 0.3 / archived 0.1 · ~4 chars/token.
Pipeline
Write
ingest(source, *, extractor, store, provenance, now=None,
scores=None) -> IngestReport(accepted, deduped, rejected)
Consolidation
consolidate(store, *, operators=None, now=None, config=None)
-> ConsolidationReport(actions, rejections, touched) # .by_pass(name)
resolve(store, *, winner_id, loser_id, reason, now=None)
default_operators(config=None)
ConsolidationConfig(near_dup_threshold=0.6, promotion_threshold=3)
ConsolidationOperators(near_dup, contradiction, resolver=None)
# operator ABCs: NearDupOperator · ContradictionOperator · ResolutionOperator
# shipped heuristics: TrigramNearDupOperator(*, threshold),
# HeuristicContradictionOperator, HeuristicResolutionOperator
trigram_similarity(a, b) -> float
Decay
decay(store, *, scores=None, now=None, config=None)
-> DecayReport(archived, protected, saliences)
salience(record, *, touches, last_touched, now, config) -> float
SalienceScores # sidecar: touch, touches, last_touched, to_dict, from_dict
DecayConfig(half_life_days=30.0, reinforcement_bonus=0.25, archive_threshold=0.05)
Report primitives
Action(pass_name, action, record_ids, before, after, reason)
Rejection(pass_name, detail, error)
Signing
DOMAIN = "fg-agent-memory/v1"
CONTEXT_RECORD = "record"
CONTEXT_MEMORY_FILE = "memory-file"
domain_tag(context)
signing_input(context, payload) -> bytes
# uint16be(len(tag)) || tag || canonical_json(payload)
sign_payload(keys, context, payload) -> str
verify_payload(public, context, payload, signature)
verify_by_address(address, context, payload, signature)
Domain separation prevents cross-replay with identity artifacts.
Stores
FileRecordStore # git-friendly JSON directory; atomic, concurrent-writer-safe
SQLiteRecordStore # single file, WAL, BEGIN IMMEDIATE, retry on contention
SQLiteSearchIndex # FTS5 keyword/BM25 (scores as -bm25; injection-safe tokenizing)
All record stores pass one shared port-contract suite.
Errors
AgentMemoryError
├── RecordError
├── LifecycleError
├── StoreError
└── ProposalError
Constants
FORMAT_VERSION = "fg-agent-memory/0.1" ·
console script fg-agent-memory-mcp (MCP stdio server, [mcp] extra).