Design Principles: Affective Field Theory¶
This document translates the philosophical and neuroscientific foundations into concrete architectural principles for the library. Each principle is traced to its theoretical sources.
The Central Model: Affective Field Theory (AFT)¶
The model we propose is an original synthesis that transcends the limitations of all existing frameworks. We call it Affective Field Theory because emotion is not a point in a space, but a field — distributed, dynamic, multi-layered, permeating every cognitive operation.
No existing model (PAD, OCC, Barrett, Plutchik, Panksepp) captures the complexity of emotion in memory on its own. AFT integrates them as distinct layers of a unified representation.
The Five Layers of the Affective Field¶
Layer 1 — Core Affect¶
Inspiration: Mehrabian & Russell (1974), Russell (1980), Barrett (2017)
Structure: Continuous coordinate (valence: float, arousal: float, dominance: float) in PAD space
The fundamental affective substrate — always active, like the background hum of the system. Corresponds to Barrett's interoception: a continuous stream of valence, arousal, and dominance before any categorization.
CoreAffect:
valence: float # [-1.0, +1.0] from very negative to very positive
arousal: float # [0.0, 1.0] from completely calm to maximum excitement
dominance: float # [0.0, 1.0] from helpless to full control (PAD third axis)
Principle: Discrete emotion labels (joy, fear, anger) are not primitives — they are regions in Core Affect space. They are computed contextually from the full PAD vector + contextual information.
Layer 2 — Affective Momentum¶
Inspiration: Spinoza (Ethica, III: affects as transitions), classical physics (kinematics) Structure: Temporal derivatives of Core Affect
Spinoza's fundamental insight: "Laetitia est transitio hominis a minore ad maiorem perfectionem" — joy is the passage, not the destination. A system in valence=+0.8 and rising is qualitatively different from one in valence=+0.8 and falling.
AffectiveMomentum:
d_valence: float # dv/dt — first derivative of valence
d_arousal: float # da/dt — first derivative of arousal
dd_valence: float # d²v/dt² — acceleration of valence
dd_arousal: float # d²a/dt² — acceleration of arousal
Momentum informs: - Prediction: where affect will be at t+1 - Retrieval: memories on a similar trajectory are more congruent - Reconsolidation: an abrupt change in momentum (emotional surprise) triggers the opening of the reconsolidation window
Layer 3 — MoodField¶
Inspiration: Heidegger (Being and Time, §29–30) — Stimmung as a mode of being-in-the-world; Bower (1981) mood-congruent memory Structure: Slowly varying PAD vector — the "background tone" of the system
Heideggerian Stimmung is not an emotion but a mode of being-in-the-world — it does not
change with individual events but through accumulation. It is the gravitational field that orients
all cognitive operations. In the library this concept is embodied by MoodField.
MoodField:
valence: float # [-1.0, +1.0] weighted moving average of affective history
arousal: float # [0.0, 1.0]
dominance: float # [0.0, 1.0] — PAD extension
inertia: float # [0.0, 1.0] resistance to change (0=volatile, 1=stable)
timestamp: datetime # when it was last updated
Update: Exponentially weighted moving average of Core Affect over time:
whereα is small (0.05–0.15) — Mood changes slowly.
Effect on retrieval: Mood weights all retrieval signals — in a negative Mood state, the bias toward negative memories increases (mood-congruent memory).
Layer 4 — Appraisal Vector¶
Inspiration: Scherer CPM (2009), Lazarus (1991), Stoics (evaluation as generator of emotion) Structure: Multi-component evaluation vector of an event
Emotion is not given — it is generated by the evaluation of the event against the system's goals and norms. Layer 4 is the engine that produces Core Affect (Layer 1) from semantic input.
AppraisalVector:
novelty: float # [-1.0, +1.0] from completely expected to entirely novel
goal_relevance: float # [-1.0, +1.0] from obstructing goals to furthering them
coping_potential: float # [0.0, 1.0] from no coping ability to full control
norm_congruence: float # [-1.0, +1.0] from norm-violating to norm-conforming
self_relevance: float # [0.0, 1.0] how much it concerns the system's "self"
Appraisal → Core Affect mapping:
# coping_potential [0,1] is converted to signed [-1,+1]:
# coping=1 → +1 (confidence), coping=0 → -1 (helplessness)
coping_signed = 2.0 * coping_potential - 1.0
valence = 0.4*goal_relevance + 0.3*norm_congruence + 0.2*coping_signed + 0.1*novelty
arousal = 0.5*abs(novelty) + 0.3*(1-coping_potential) + 0.2*self_relevance
# coping_potential maps directly to the dominance (PAD) axis:
dominance = coping_potential # 0 = helpless, 1 = full control
The mapping is configurable — this is the most empirically open component of the system.
Layer 5 — Associative Resonance¶
Inspiration: Aristotle (associationism by similarity/contiguity/contrast), Hume (sympathy), Bower (affective associative network) Structure: Graph of connections between memories based on emotional resonance
When a memory is encoded or retrieved, it resonates with others through: 1. Semantic similarity: similar content 2. Emotional congruence: similar Core Affect at encoding time 3. Temporal contiguity: temporally proximate event 4. Perceived causality: causally connected event 5. Emotional contrast: affective opposition (anxiety ↔ relief)
ResonanceLink:
source_id: str
target_id: str
strength: float # [0.0, 1.0]
link_type: Literal['semantic', 'emotional', 'temporal', 'causal', 'contrastive']
Resonance links form emotional clusters that mutually reinforce each other during retrieval (spreading activation).
The EmotionalTag: Complete Structure¶
Every memory carries an EmotionalTag that captures all 5 layers at encoding time:
class EmotionalTag(BaseModel): # Pydantic, frozen=True
# Layer 1: Core Affect at encoding time
core_affect: CoreAffect # (valence, arousal, dominance)
# Layer 2: Momentum at encoding time
momentum: AffectiveMomentum # (d_valence, d_arousal, dd_valence, dd_arousal)
# Layer 3: Mood snapshot at encoding time
mood_snapshot: MoodField # (valence, arousal, dominance, inertia, timestamp)
# Layer 4: Appraisal that generated this emotion
appraisal: AppraisalVector | None # None if exogenous emotion
# Layer 5: Resonance links with other memories
resonance_links: list[ResonanceLink]
# Metadata
timestamp: datetime
consolidation_strength: float # [0.0, 1.0] — modulated by arousal
last_retrieved: datetime | None
retrieval_count: int
reconsolidation_count: int # number of reconsolidations that have occurred
# Dual-path encoding (LeDoux, 1996)
pending_appraisal: bool # True when fast-path; elaborate() will complete the tag
# APE-gated reconsolidation window (Pearce & Hall, 1980)
window_opened_at: datetime | None # window open timestamp; None = closed
# Pearce-Hall predictive learning
expected_affect: CoreAffect | None # EMA prediction of future core_affect
prediction_learning_rate: float # adaptive rate in [0.05, 0.80]
# Plutchik discrete emotion label (auto_categorize)
emotion_label: EmotionLabel | None # None if auto_categorize=False
Encoding Principles¶
Principle 1 — Emotional Tagging Is Pervasive¶
Source: Colombetti (2014), Richter-Levin (2004)
Every memory — without exception — carries an EmotionalTag. There is no "emotionally neutral"
memory. If there is no explicit appraisal, the tag uses the current Core Affect and Mood as
a baseline.
Principle 2 — Consolidation Strength Is a Function of Arousal¶
Source: McGaugh (2004), Brown & Kulik (1977), Yerkes-Dodson
def consolidation_strength(arousal: float, mood_arousal: float) -> float:
# Inverted-U relationship — non-linear
# Peak around arousal=0.7, degradation at extreme values
effective_arousal = 0.7 * arousal + 0.3 * mood_arousal
return -4 * (effective_arousal - 0.7)**2 + 1.0
# or: parametric Yerkes-Dodson curve
Memories with high arousal (but not maximum) have the highest consolidation_strength → slower decay → higher retrieval priority.
Principle 3 — Dual-Speed Encoding¶
Source: LeDoux (1996), dual pathway theory
- Fast path: Immediate encoding with coarse affective tag (valence + arousal from current Core Affect, no appraisal). Occurs within the next cycle.
- Slow path: Tag update with full appraisal (Layer 4) after contextual elaboration. Opens the reconsolidation window.
Retrieval Principles¶
Principle 4 — Multi-Signal Retrieval¶
Source: original synthesis, Emotional RAG (2024), Bower (1981)
def retrieval_score(
query_embedding: np.ndarray,
query_affect: Tuple[float, float], # current Core Affect
current_mood: MoodField,
current_momentum: AffectiveMomentum,
memory: Memory,
activation_map: dict[str, float] # pre-computed by spreading_activation()
) -> float:
w = adaptive_weights(current_mood) # weights modulated by Mood
s1 = cosine_similarity(query_embedding, memory.embedding)
s2 = emotional_congruence(current_mood, memory.tag.mood_snapshot)
s3 = core_affect_proximity(query_affect, memory.tag.core_affect)
s4 = momentum_alignment(current_momentum, memory.tag.momentum)
s5 = recency_decay(memory.tag.timestamp, memory.tag.consolidation_strength)
s6 = activation_map.get(memory.id, 0.0) # spreading activation boost
return w[0]*s1 + w[1]*s2 + w[2]*s3 + w[3]*s4 + w[4]*s5 + w[5]*s6
Principle 5 — Adaptive Weights Modulated by Mood¶
Source: Heidegger (disclosure through mood), Bower (1981)
The weights w are not fixed — they depend on the current Mood:
def adaptive_weights(mood: MoodField, base: list[float], config: AdaptiveWeightsConfig) -> np.ndarray:
# Weights are modulated with smooth sigmoid (tanh) — no hard threshold
w = np.array(base, dtype=float)
# Negative Mood → emotional signals dominate (s2, s3 ↑, s1 ↓)
neg = smooth_gate(mood.valence, center=-0.3, sharpness=5.0) # activates below center
w[1] += config.negative_mood_strength * neg * (2/3) # mood congruence
w[2] += config.negative_mood_strength * neg * (1/3) # affect proximity
w[0] -= config.negative_mood_strength * neg # semantic
# High arousal → momentum alignment dominates (s4 ↑, s1 ↓)
high_a = smooth_gate(mood.arousal, center=0.6, sharpness=5.0) # activates above center
w[3] += config.high_arousal_strength * high_a
w[0] -= config.high_arousal_strength * high_a
# Calm/neutral Mood → semantic retrieval prevails (s1 ↑, s2, s3 ↓)
calm = smooth_gate(mood.arousal, center=0.3, sharpness=-5.0) * \
(1.0 - abs(mood.valence)) # low arousal + neutral valence
w[0] += config.calm_semantic_strength * calm
w[1] -= config.calm_semantic_strength * calm * (2/3)
w[2] -= config.calm_semantic_strength * calm * (1/3)
w = w.clip(0.0) # no negative weights
total = w.sum()
return w / total if total > 0 else np.full(len(w), 1.0 / len(w)) # uniform fallback
Decay Principles¶
Principle 6 — Non-Linear Decay Modulated by Arousal¶
Source: ACT-R (Anderson, 1983), McGaugh (2004), Merleau-Ponty (habits)
def effective_decay_rate(
base_decay: float,
arousal_at_encoding: float,
retrieval_count: int
) -> float:
# Power-law base (ACT-R style)
# Modulation by arousal: high arousal = slower decay
arousal_factor = 1.0 - 0.5 * arousal_at_encoding
# Retrieval reinforces memory (spacing effect)
retrieval_factor = 1.0 / (1.0 + 0.1 * retrieval_count)
return base_decay * arousal_factor * retrieval_factor
def consolidation_at_time_t(
initial_strength: float,
decay_rate: float,
time_delta_seconds: float,
floor: float = 0.0
) -> float:
# Power-law decay with minimum floor for important memories
decayed = initial_strength * (time_delta_seconds ** (-decay_rate))
return max(decayed, floor)
Minimum floor: Memories with arousal above threshold maintain a non-zero minimum
consolidation_strength — they are never completely forgotten.
Principle 7 — Reconsolidation at Retrieval¶
Source: Nader, Schiller (2000, 2010), prediction error theory
Retrieval does not always open the reconsolidation window — it is the Affective Prediction Error
(APE) that determines it. Only when the APE exceeds a configurable threshold does the window
open (window_opened_at is set). Any subsequent retrieval during the open window performs
reconsolidation. Tags are immutable (Pydantic frozen=True): model_copy(update=...) is used
to produce new instances.
def on_retrieval(memory: Memory, current_state: AffectiveState) -> Memory:
# Update metadata (returns new immutable instance)
new_tag = memory.tag.model_copy(update={
"last_retrieved": datetime.now(tz=UTC),
"retrieval_count": memory.tag.retrieval_count + 1,
})
# Compute APE and update prediction (Pearce & Hall, 1980)
ape = compute_ape(new_tag, current_state.core_affect)
new_tag = update_prediction(new_tag, current_state.core_affect)
# Open window only if APE exceeds threshold
if ape >= APE_THRESHOLD and new_tag.window_opened_at is None:
new_tag = new_tag.model_copy(update={"window_opened_at": datetime.now(tz=UTC)})
# Reconsolidate if window is open (any retrieval within the window)
if new_tag.window_opened_at is not None:
alpha = min(ape * RECONSOLIDATION_LEARNING_RATE, 0.5)
blended = CoreAffect(
valence=(1-alpha)*new_tag.core_affect.valence + alpha*current_state.core_affect.valence,
arousal=(1-alpha)*new_tag.core_affect.arousal + alpha*current_state.core_affect.arousal,
)
new_tag = new_tag.model_copy(update={
"core_affect": blended,
"reconsolidation_count": new_tag.reconsolidation_count + 1,
"window_opened_at": None, # window closes after reconsolidation
})
return memory.model_copy(update={"tag": new_tag})
Principle 8 — Appraisal as Generator of Emotion¶
Source: Lazarus (1991), Scherer (2009), Stoics
Emotion is not an input — it is an output of the appraisal of the event against the system's goals. The system must be able to define a goal set (conatus) that orients the appraisal.
class AppraisalEngine(Protocol):
"""Duck-typed protocol — no inheritance required.
Available implementations:
- LLMAppraisalEngine : uses an LLM (OpenAI-compatible) with Scherer CPM prompt
- KeywordAppraisalEngine: keyword-based rules, no external dependency
- StaticAppraisalEngine : returns a fixed vector (for testing)
"""
def appraise(
self, event_text: str, context: dict | None = None
) -> AppraisalVector: ...
Principle 9 — Associative Resonance¶
Source: Aristotle (De Memoria), Hume (association), Bower (spreading activation)
At encoding time, automatically build resonance links:
def build_resonance_links(
new_memory: Memory,
existing_memories: list[Memory],
top_k: int = 5
) -> list[ResonanceLink]:
links = []
for mem in existing_memories:
# Semantic similarity
sem_sim = cosine_similarity(new_memory.embedding, mem.embedding)
# Emotional congruence
emo_sim = emotional_congruence(
new_memory.tag.core_affect,
mem.tag.core_affect
)
# Temporal contiguity
temporal_prox = temporal_proximity(new_memory.tag.timestamp, mem.tag.timestamp)
# Composite score
resonance = 0.5*sem_sim + 0.3*emo_sim + 0.2*temporal_prox
if resonance > RESONANCE_THRESHOLD:
link_type = classify_link_type(sem_sim, emo_sim, temporal_prox)
links.append(ResonanceLink(
source_id=new_memory.id,
target_id=mem.id,
strength=resonance,
link_type=link_type
))
return sorted(links, key=lambda l: l.strength, reverse=True)[:top_k]
Principle 9b — Bidirectional Links and Spreading Activation¶
Source: Collins & Loftus (1975), "A spreading-activation theory of semantic processing"
Resonance links are bidirectional: when memory A connects to memory B, the inverse link on B is also created. This ensures that activation propagates in both directions in the associative network.
During retrieval, spreading activation BFS multi-hop propagates activation from seed memories toward adjacent ones: each hop multiplies the activation by the strength of the traversed link. If two paths converge on the same memory, the maximum is taken (not the sum) to avoid artificial inflation from the number of converging paths.
def spreading_activation(
seeds: dict[str, float], # memory_id -> initial activation
store: MemoryStore,
max_hops: int = 2,
) -> dict[str, float]: # memory_id -> max activation reached
...
Principle 9c — Hebbian Co-Retrieval Strengthening¶
Source: Hebb (1949), "The Organization of Behavior"
"Neurons that fire together, wire together."
Every time a group of memories is retrieved together in the same query, the existing links
between them are reinforced by a fixed increment (hebbian_increment, default 0.05). This models
the fact that association consolidates with use: frequently co-activated memories become
progressively more connected, emerging spontaneously as thematic or emotional clusters without
explicit supervision.
System Architecture¶
Memory Lifecycle¶
INPUT: event/conversation
|
v
[1. Appraisal Engine]
Compute AppraisalVector
|
v
[2. Core Affect Update]
Update the system's affective state
|
v
[3. Momentum Update]
Compute temporal derivatives
|
v
[4. Mood Update]
Weighted moving average (small α)
|
v
[5. Encoding]
Create EmotionalTag with snapshot of all 5 layers
Compute consolidation_strength
|
v
[6. Storage]
Save in vector store with embedding + tag
|
v
[7. Resonance Building]
Build links with existing memories
|
v
OUTPUT: memory with full EmotionalTag
Retrieval Cycle¶
QUERY: text + current affective state
|
v
[1. Query Processing]
Embedding + current Core Affect + Mood
|
v
[2. Adaptive Weights]
Compute weights as a function of Mood
|
v
[3. Multi-Signal Scoring]
Compute score for each candidate
|
v
[4. Spreading Activation]
Multi-hop BFS from seed set (Collins & Loftus 1975)
Build activation_map for Pass 2
|
v
[5. Pass 2 Scoring + Top-K Selection]
Re-score with resonance boost from activation_map
Select the most relevant memories
|
v
[6. Reconsolidation Check]
For each retrieved memory: open window,
compute APE, update tag if necessary
|
v
[7. Hebbian Strengthening]
Reinforce links between co-retrieved memories (Hebb 1949)
|
v
OUTPUT: retrieved memories + updated tags
Comparison with Neuroscience and Philosophy Principles¶
| Principle | Theoretical source | Implementation |
|---|---|---|
| Affect as transition | Spinoza, Ethica III | Layer 2: Affective Momentum |
| Stimmung as global field | Heidegger, BT §29 | Layer 3: MoodField |
| Emotion as appraisal | Stoics, Scherer, Lazarus | Layer 4: Appraisal Vector |
| Associative resonance | Aristotle, Hume, Bower | Layer 5: Resonance Links |
| Consolidation ∝ arousal | McGaugh, LeDoux | consolidation_strength formula |
| Reconsolidation | Nader, Schiller | on_retrieval() with APE |
| Mood-congruent retrieval | Bower (1981) | adaptive_weights() |
| Non-linear decay | ACT-R, Yerkes-Dodson | effective_decay_rate() |
| Dual-speed encoding | LeDoux dual pathway | fast path + slow path |
| Pervasive tagging | Colombetti, Memory Bear AI | EmotionalTag on every memory |