Mental Model: How emotional-memory Works¶
This page is for developers who want to understand the library in 5 minutes before reading the API docs or the research papers.
The core idea¶
Standard LLM memory stores text and retrieves it by semantic similarity. emotional-memory stores text plus its emotional context and retrieves by semantic similarity and emotional congruence.
This means: if you were anxious when you encoded a memory, the library will surface that memory more readily when you are anxious again — even if the query is semantically distant. This is mood-congruent recall (Bower 1981), a well-replicated psychological phenomenon.
What happens on encode¶
text + current emotional state
│
▼
┌─────────────┐
│ AppraisalVector │ ← "Is this novel? Relevant to my goals? Threatening?"
│ (Scherer CPM) │ 5 dimensions → mapped to CoreAffect delta
└──────┬──────┘
│
▼
┌─────────────┐
│ CoreAffect │ ← (valence, arousal, dominance) — 3D PAD emotional substrate
└──────┬──────┘
│
▼
┌────────────────┐
│ AffectiveState │ ← updates momentum (velocity of affect change)
│ .update() │ updates MoodField (slow-moving background mood)
└──────┬─────────┘
│
▼
┌──────────────┐
│ EmotionalTag │ ← snapshot: core_affect, momentum, mood_snapshot,
│ │ consolidation_strength, appraisal_vector, ...
└──────┬────────┘
│
▼
embed(text) → vector
│
▼
store.save(Memory(content, embedding, tag))
│
▼
build ResonanceLinks ← bidirectional graph: semantic / emotional /
temporal / causal / contrastive links
Hebbian strengthening on co-activation
The result is a Memory object: text + embedding vector + a rich EmotionalTag capturing the full affective context at encoding time.
Dual-path encoding (optional, dual_path_encoding=True): mirrors LeDoux's two-route model. The fast path skips appraisal and encodes immediately with raw affect (pending_appraisal=True). The slow path runs full appraisal later via elaborate(memory_id) and blends the result back in (70% appraised / 30% raw).
What happens on retrieve¶
query text
│
▼
embed(query) → query_vector
│
▼
Pass 1 — score all candidates (6 signals, no spreading):
s1 semantic similarity (cosine of embeddings)
s2 mood congruence (how close is memory's mood to current mood?)
s3 core affect proximity (distance in PAD space, 3D, sqrt(6) normalizer)
s4 momentum alignment (do affect trajectories point the same way?)
s5 recency / decay (ACT-R power-law: strength decays with time)
s6 resonance boost (0 in Pass 1; populated in Pass 2 via spreading activation)
Weights are adaptive: `adaptive_weights()` modulates w1-w6 with the current
MoodField *before* scoring. Calm/neutral mood → semantic (s1) dominates; negative
mood → mood congruence (s2) and affect proximity (s3) rise at semantic's expense;
high arousal → momentum (s4) rises. This applies to whatever base vector is in
effect — **including per-query routed weights** (`QueryClassifierConfig.routed_weights`):
a routed vector is a starting point, not the final weights. A routed
`[1, 0, 0, 0, 0, 0]` is pure-semantic only under a neutral mood.
│
▼
Top-k seeds
│
▼
spreading_activation(seeds) ← multi-hop BFS on ResonanceGraph
│ returns activation_map: memory_id → score
▼
Pass 2 — re-score seeds with resonance boost (s6 = activation_map[id])
│
▼
Ranking plan available via retrieve_with_explanations()
│
│ breakdown.raw_signals → ranking-time signal values
│ breakdown.weighted_signals → score contributions after adaptive weights
│ pass1_rank / pass2_rank → how resonance changed ordering
│
▼
Per memory: compute_ape() — Affective Prediction Error
│ "How surprising is this memory given the current context?"
▼
APE-gated reconsolidation:
high APE → open lability window → memory becomes reconsolidatable
retrieval within open window → update EmotionalTag with current state
│
▼
hebbian_strengthen() on co-retrieved ResonanceLinks
│
▼
return top-k Memory objects
The query is never appraised. Of the six signals, only s1 (semantic similarity) reads the query text — the embedding is all the query contributes. s2-s4 compare each memory's stored
EmotionalTagagainst the current runtime affective state (the mood, core affect and momentum the engine is in whenretrieve()is called); s5 is time-based; s6 is graph-based. An emotionally phrased query ("when did I feel relieved?") does not, by itself, activate the affect channel: its emotion words only shift the embedding. Affective retrieval is state-dependent (Bower 1981) — it favours memories congruent with how the agent feels now, not with what the query asks about. To bias retrieval toward a feeling, change the state first (observe()/set_affect()) or route the query (QueryClassifierConfig).
There are now two retrieval entrypoints:
retrieve()returns the post-retrievalMemoryobjects and is the normal runtime path.retrieve_with_explanations()runs the same ranking pipeline but also returns the ranking-time decomposition for each result.
This distinction matters: the explanation captures the score before retrieval-side
updates such as reconsolidation and Hebbian strengthening, while the returned memory
reflects the post-retrieval stored state.
The 5 layers at a glance¶
| Layer | What it is | Why it matters for retrieval |
|---|---|---|
| CoreAffect | (valence, arousal, dominance) point in PAD space | Mood-congruent matching: memories from similar emotional states rank higher |
| AffectiveMomentum | Velocity + acceleration of affect change | Captures the direction of emotion, not just its position |
| MoodField | Slow-moving background mood (EMA) | Provides the "ambient" emotional filter that shifts retrieval weights |
| AppraisalVector | 5 Scherer evaluation checks → CoreAffect delta | Explains why something felt the way it did |
| ResonanceLinks | Bidirectional associative graph | Surfaces memories linked by association, not just by text similarity |
What makes this different from plain vector memory¶
| Plain vector memory | emotional-memory | |
|---|---|---|
| Stored per memory | text + embedding | text + embedding + full emotional tag |
| Retrieval signal | cosine similarity | 6-signal weighted score (mood, affect, momentum, decay, resonance) |
| Memory strength | flat (or TTL) | ACT-R power-law decay modulated by arousal at encoding |
| Memory update | none / overwrite | APE-gated reconsolidation: high-surprise retrieval rewrites the tag |
| Association graph | none | Bidirectional typed graph with Hebbian strengthening |
| Global state | none | MoodField (persists across encodes/retrieves) |
Key objects¶
from emotional_memory import (
EmotionalMemory, # main entry point
EmotionalMemoryConfig,
InMemoryStore, # reference store (swap for SQLiteStore or custom)
CoreAffect, # (valence: float, arousal: float) — both frozen Pydantic
EmotionalTag, # snapshot attached to each Memory at encoding time
Memory, # content + embedding + tag
AffectiveState, # mutable engine state (mood, momentum, history)
)
EmotionalMemory is the façade. It orchestrates all 5 layers. The normal interaction
path is set_affect, encode, retrieve; use retrieve_with_explanations() when you
need the ranking plan and signal breakdown for debugging, evaluation, or UI inspection.
Minimal working example¶
from emotional_memory import EmotionalMemory, InMemoryStore, CoreAffect
from emotional_memory.embedders import SentenceTransformerEmbedder
em = EmotionalMemory(
store=InMemoryStore(),
embedder=SentenceTransformerEmbedder(),
)
# Encode with a positive, high-arousal state (e.g. after shipping a feature)
em.set_affect(CoreAffect(valence=0.8, arousal=0.7))
em.encode("Finally shipped the new recommendation engine after three hard weeks.")
em.encode("Team celebrated with pizza — everyone relieved and proud.")
# Encode with a negative state (e.g. an incident)
em.set_affect(CoreAffect(valence=-0.7, arousal=0.9))
em.encode("Database outage at 3am — on-call for two hours.")
# Retrieve from a positive state → mood-congruent memories rank first
em.set_affect(CoreAffect(valence=0.6, arousal=0.5))
results = em.retrieve("project work", top_k=2)
# results[0] will be one of the positive memories, not the outage
for m in results:
print(m.content, "| valence:", m.tag.core_affect.valence)
Further reading¶
- Full theoretical foundations:
docs/research/ - Known limitations:
docs/research/08_limitations.md - API reference: generated from docstrings (run
make docs-serve) - Source of truth for retrieval scoring:
src/emotional_memory/retrieval.py