AI agents are stateless by default. Every session starts from zero — no memory of corrections, no recall of preferences, no knowledge of what tools exist. Users repeat themselves. Agents make the same mistakes. The problem is not intelligence but amnesia.
An engram is the smallest unit of persistent memory. The term comes from neuroscience — Richard Semon's 1904 hypothesis that experiences leave physical traces (engrams) in the brain. We borrow the concept: each engram is a single learned fact, correction, preference, or behavioral pattern that an agent can recall and apply.
The engram model draws on three lines of cognitive science research:
This is an open standard. The specification defines the data format and behavioral model — not a specific implementation. Any agent framework can implement engram-compatible memory. PLUR is one implementation; there should be others.
The benchmark results demonstrate that agents with engram memory outperform stateless agents in 31 of 35 decided contests (89%), with the strongest gains in convention adherence (12–0) and tool discoverability (10–2). The engram specification explains the data structures and algorithms behind those results.
An engram is a YAML object with a fixed set of fields. Every field has a defined type, default value, and purpose. The schema is designed to be human-readable (YAML, not binary), individually addressable (unique ID per engram), and self-describing (each engram carries its own metadata).
| Field | Type | Description |
|---|---|---|
| id | string | Unique identifier. Format: ENG-YYYY-MMDD-NNN. Prefixes: ENG (standard), ABS (abstract), META (meta-engram) |
| version | integer | Schema version (currently 2.1) |
| status | enum | active | dormant | retired | candidate |
| type | enum | behavioral | terminological | procedural | architectural |
| scope | string | Hierarchical namespace: agent:X | command:X | global | space:X |
| statement | string | The learned knowledge, written as actionable guidance (25–60 words) |
| rationale | string | Why this engram exists — the incident or pattern that prompted it |
| tags | string[] | Freeform tags for keyword matching during retrieval |
| visibility | enum | private | public | template. Default: private |
| domain | string | SKOS-style hierarchical domain (e.g., gtd/org-mode, dev/typescript) |
| contraindications | string[] | When this engram should not be applied |
| polarity | enum | null | do (directive) | dont (prohibition) | null (unclassified) |
| consolidated | boolean | Whether this engram survived reconsolidation. Default: false |
The type field classifies the kind of knowledge an engram encodes. This affects how agents apply it and how it surfaces during retrieval.
| Type | Description | Example |
|---|---|---|
| behavioral | How to act — preferences, corrections, habits | "Validate org-mode syntax before writing to .org files" |
| terminological | Definitions, naming, factual knowledge | "The API returns snake_case, not camelCase" |
| procedural | Step-by-step processes and workflows | "Deploy via git pull then systemctl restart" |
| architectural | System design, conventions, structural patterns | "Tags use :colon: format in org-mode, #hash in PKM" |
The activation block tracks how an engram decays and strengthens over time. These fields drive the ACT-R-inspired retrieval model described in the next section.
| Field | Type | Description |
|---|---|---|
| retrieval_strength | 0.0–1.0 | How easily the engram can be recalled. Decays over time, increases on access. |
| storage_strength | 0.0–1.0 | How deeply encoded the engram is. Only increases (reinforcement, consolidation). |
| frequency | integer | Total number of times this engram has been accessed (injected, referenced). |
| last_accessed | date | When the engram was last injected into an agent session. |
Engrams are connected to each other via weighted, typed links. These links form the graph structure that enables spreading activation during retrieval — finding related engrams that weren't directly matched by keyword search.
Strength is capped at 0.95. The co_accessed type is learned automatically: when two engrams are recalled together, their association strengthens by +0.05 (created at 0.3 for new pairs, max 5 co-access edges per engram). Co-access edges are bidirectional and only form between the top half of recall results. All co_accessed associations decay via exponential decay (lambda=0.01, floor=0.02).
Typed entity references extracted from the engram statement. Enables graph queries across the knowledge base — find all engrams mentioning a person, technology, or project.
| Field | Type | Description |
|---|---|---|
| entities[].name | string | Entity name |
| entities[].type | enum | person | organization | technology | concept | project | tool | place | event | standard | other |
| entities[].uri | URL | Optional canonical URI for the entity |
Bi-temporal validity model inspired by Zep's temporal knowledge graph. Tracks when knowledge was learned and when it is valid — enabling time-scoped queries and automatic expiration.
| Field | Type | Description |
|---|---|---|
| temporal.learned_at | ISO date | When the knowledge was acquired |
| temporal.valid_from | ISO date | When this knowledge becomes valid (optional) |
| temporal.valid_until | ISO date | When this knowledge expires (optional) |
Episodic memory fields capture the emotional significance and confidence level of a learning. Higher emotional weight slows decay — painful lessons persist longer.
| Field | Type | Description |
|---|---|---|
| episodic.emotional_weight | 1–10 | Significance of this learning. Higher = slower decay. Default: 5 |
| episodic.confidence | 1–10 | How certain this pattern holds. Affects injection priority. Default: 5 |
| episodic.trigger_context | string | What situation prompted this learning |
| episodic.journal_ref | string | Reference to the journal entry where this was learned |
Automatic hit/miss tracking for injection quality. Every time an engram is injected, the system records whether it was actually useful (hit) or irrelevant (miss). This data drives injection scoring over time.
| Field | Type | Description |
|---|---|---|
| usage.injections | integer | Total times injected into agent sessions |
| usage.hits | integer | Times the injection was useful (positive feedback) |
| usage.misses | integer | Times the injection was irrelevant (negative feedback) |
| usage.last_hit_at | ISO date | When the last positive hit occurred |
Marketplace fitness metrics for engrams shared via the exchange protocol. Tracks how well an engram performs across different environments and users.
| Field | Type | Description |
|---|---|---|
| exchange.fitness_score | 0.0–1.0 | Overall fitness for sharing (higher = more universally useful) |
| exchange.environmental_diversity | integer | Number of distinct environments where this engram is active |
| exchange.adoption_count | integer | How many users have installed this engram |
| exchange.contradiction_rate | 0.0–1.0 | How often this engram conflicts with local knowledge |
| Field | Type | Description |
|---|---|---|
| source | string | Origin identifier (e.g., user/personal) |
| derivation_count | integer | How many times this pattern was independently captured. Default: 1 |
| pack | string | null | Pack this engram belongs to (if installed from a pack) |
| abstract | string | null | ID of the abstract engram this was derived from |
| derived_from | string | null | ID of the parent engram this was derived from |
| feedback_signals | object | Counters: {positive, negative, neutral} |
The structured_data field is an open key-value map for domain-specific extensions. Meta-engrams use structured_data.meta to store structural templates, evidence chains, and falsification criteria. Other domains can add their own namespaced fields without modifying the core schema.
Engrams with high derivation_count are the most valuable — patterns independently re-derived across sessions represent convergent evolution. If three separate sessions captured the same insight, it is almost certainly important.
While engrams store learned knowledge (assertions that persist and decay), episodes store events — timestamped records of what happened. Episodes answer "how did we fix this last time?" and "what happened on Tuesday?" They complement engrams the way a journal complements a reference library.
An episode is a YAML object with these fields:
| Field | Type | Description |
|---|---|---|
| id | string | Unique identifier. Format: EP-YYYY-MMDD-NNN |
| timestamp | ISO datetime | When the event occurred |
| summary | string | What happened — a resolution, incident, decision, or milestone |
| agent | string | Which agent recorded this (e.g., claude-code, openclaw, hermes) |
| channel | string | Where it happened (e.g., terminal, telegram, slack) |
| session_id | string | Links episodes within the same work session |
Episodes are stored in episodes.yaml alongside engrams.yaml. They are append-only — no decay, no activation model. Query by time range, agent, channel, or free text.
Engrams and episodes serve different purposes: engrams are knowledge (assertions the agent should remember), episodes are history (events the agent can reference). A typical session creates both: engrams from corrections, episodes from resolutions.
The activation model determines which engrams are injected into an agent session and which are left dormant. It is inspired by ACT-R's base-level learning equation: memory items that are accessed frequently and recently have higher activation.
An engram's retrieval_strength decays over time based on days since last access. This implements Ebbinghaus's forgetting curve — without reinforcement, memories fade.
The emotional_weight field modifies the decay rate. Painful or significant lessons persist longer:
An engram with emotional_weight = 10 decays at half the normal rate. An engram with the default weight of 5 decays at 75% of the base rate.
Emotional weight also affects injection scoring as a multiplier: 1 + (emotional_weight - 5) × 0.04, mapping the [1,10] range to [0.84, 1.20]. This means emotionally significant memories are both slower to decay and more likely to be injected when relevant — a ±20% swing at extremes, neutral at the default of 5.
Retrieval strength determines an engram's lifecycle status and whether it gets injected into agent context:
| Retrieval strength | Status | Effect |
|---|---|---|
| > 0.5 | Active | Injected into agent sessions when relevant |
| 0.3 – 0.5 | Fading | Injected only if context budget allows |
| 0.1 – 0.3 | Dormant | Retained in storage, not injected |
| < 0.1 | Retirement candidate | Flagged for review — retire or reinforce |
When an engram is accessed — injected into an agent session, referenced during review, or explicitly reinforced by the user — its retrieval_strength increases and frequency increments. The storage_strength only increases, never decreases, representing the depth of encoding.
When a new learning contradicts an existing engram, both surface as a pair during review. The user resolves the contradiction — keep one, merge them, or mark both as valid in different contexts. The survivor gains consolidated: true and a 2x storage_strength boost, making it more resistant to future decay.
Forgetting is not failure — it is curation. An unbounded memory that retains everything becomes a search problem worse than having no memory at all. The activation model ensures that frequently useful knowledge stays accessible while irrelevant knowledge gracefully fades.
Engrams improve through use. Every session generates feedback signals that train the system on what to inject next time. This is the core learning loop that makes memory better over time, not just bigger.
Feedback signals directly affect future injection priority. An engram with consistently positive feedback scores higher during retrieval. An engram with negative feedback is deprioritized — even if its retrieval strength is high.
| Signal | Effect on retrieval | When to use |
|---|---|---|
| Positive | Boost | Engram directly helped with the task |
| Neutral | No change | Engram was present but neither helped nor hurt |
| Negative | Penalize | Engram was irrelevant or misleading for this context |
Not every observation becomes an engram. Raw patterns go through a quality gate pipeline before promotion:
Expected pass rate is 30–50% of raw patterns. If consistently above 50%, the gates are too loose. Below 30%, too strict.
Every engram lifecycle change is logged to monthly JSONL files (history/YYYY-MM.jsonl). This creates an auditable trail for reporting, team dashboards, and the structural upgrade recommendations pipeline.
| Event | When fired |
|---|---|
engram_created |
New engram added via learn() |
engram_updated |
Engram content or activation modified (includes decay transitions) |
engram_merged |
Two engrams consolidated into one during review |
engram_retired |
Engram removed via forget() |
engram_promoted |
Candidate engram approved and activated |
feedback_received |
Positive, negative, or neutral signal recorded |
recurrence_detected |
A new learning matches an existing engram (duplicate prevented, strength bumped) |
contradiction_detected |
A new learning conflicts with an existing engram (flagged for reconsolidation) |
scope_promoted |
Project-scoped engram promoted to global (appeared in multiple projects) |
buffer_pruned |
Raw learning file entries removed after engram promotion |
weekly_review |
Batch decay run, escalation analysis, and health metrics computed |
similaritySearch() returns cosine similarity scores alongside engrams, enabling automated dedup classification. Scores are clamped to [0, 1]. Thresholds: > 0.9 = duplicate, 0.7 – 0.9 = related, < 0.7 = new knowledge.
batchDecay() applies ACT-R exponential decay to all primary store engrams. Emotional weight slows decay for painful lessons. Scope-matched engrams are immune (returning to a project after months gives full memory). Status transitions are logged to the event history.
When an agent starts a session, the system must find the most relevant engrams from potentially thousands of candidates. The search pipeline combines keyword matching, semantic similarity, and graph traversal to surface the right knowledge.
Before indexing, each engram is expanded into a searchable text representation that includes fields beyond the statement:
This enrichment means a search for "trading" will find engrams whose statement doesn't mention trading but whose domain is trading/risk or whose anchor snippet references a trading journal entry.
The search pipeline uses two retrieval strategies and fuses their results:
Full BM25 with IDF weighting, term frequency saturation (k1=1.2), and document length normalization (b=0.75). Runs in-process over enriched engram text. Fast, exact, and good at matching specific identifiers like server names or tool names. Zero external dependencies.
Dense vector search using BGE embeddings. Finds conceptually related engrams even when keywords don't overlap. "Deploy to production" matches "push to live server."
Results from both strategies are fused using Reciprocal Rank Fusion (RRF) — a parameter-free method that combines ranked lists without needing score normalization. Each result's final score is:
Where k is a constant (typically 60) and ranki is the document's position in each retrieval strategy's results. Documents that rank highly in both keyword and semantic search surface to the top.
This is a zero-cost hybrid search. BM25 runs on SQLite (already present for storage). Embeddings are generated locally using a small model. No external API calls, no cloud dependencies, no per-query costs. The entire search pipeline runs on the user's machine.
After retrieval, engrams are scored for injection priority using a composite formula:
The top-scoring engrams are selected as directives (up to 10, within a token budget). Next-best become consider items (up to 5). Finally, a spreading activation pass traverses the association graph to discover related engrams not matched by search:
This adds up to 3 more engrams from the association graph. Combined ceiling: 18 engrams per injection (10 directives + 5 consider + 3 spreading).
The engram specification is deliberately storage-agnostic, but we document the reference implementation choices for implementors.
Engrams are stored as YAML arrays in flat files. One file per scope or space. YAML was chosen over SQLite or JSON for three reasons: human-readability (engrams should be editable by hand), git-friendliness (meaningful diffs), and simplicity (no database driver required).
The YAML files are the source of truth. BM25 search runs in-process over engram text — no database required for the default search path. An optional SQLite database can serve as a read index for faster filtered queries at scale (enable with index: true in config). The SQLite index is rebuilt from YAML on demand. If the .db file is lost or corrupted, it is regenerated from the YAML files with zero data loss.
Implementations expose engram operations through whatever tool protocol their agent framework supports. The reference implementation uses MCP (Model Context Protocol). The core operations:
| Operation | Description |
|---|---|
| plur.learn | Create a candidate engram from a statement, type, and scope |
| plur.recall | Search engrams by keyword or semantic similarity |
| plur.inject | Context-aware injection at session start (returns directives + consider) |
| plur.feedback | Rate an injected engram: positive, negative, or neutral |
| plur.forget | Retire an engram (sets status to retired) |
| plur.capture | Record a timestamped episode (incident, resolution, session event) |
| plur.timeline | Query episode history by time range, agent, or channel |
| plur.compact | Remove retired engrams from storage |
| plur.session.start | Begin a session — describes task, triggers injection |
| plur.session.end | End a session — updates co-access associations, runs decay |
| plur.sync | Sync engrams across machines via git |
| plur.status | Memory health check — counts, scores, decay status |
Implementations should scan engram statements for secrets before persisting. The reference implementation detects AWS keys, API keys (sk-/pk- prefixes), passwords in assignments, connection strings, JWTs, private key blocks, and bearer tokens. On detection, learn() throws and refuses to save. ingest() silently skips secret-containing candidates. Configurable via allow_secrets: true for environments where this is acceptable.
An implementation is engram-compatible if it supports:
Everything else — associations, spreading activation, co-access learning, schema emergence — is optional. Start minimal. Add complexity when the simple version stops being sufficient.