Most agent frameworks share the same fundamental flaw: the agent dies when you close the tab.
No background process. No state that drifts. No accumulated pressure that builds into initiative. Just a stateless loop that wakes up when prompted and ceases to exist when it isn't.
I spent a year building something different. This post is about the architecture.
The Core Problem
Standard agent loop:
user message → context assembly → LLM → output → agent ceases to exist
Everything interesting about persistent cognition — memory that actually changes behavior, initiative that comes from inside, temporal continuity — is missing. The agent has no between.
Anima is built on the opposite principle: internal state is primary, language is downstream of it. The LLM doesn't generate behavior. It expresses behavior that already emerged from state.
The Pipeline: L0 → L8
Input never goes directly to the output LLM. It passes through nine layers of state computation first.
L0 — Input LLM (isolated)
Converts raw user text to a JSON stimulus:
{ "tension": 0.4, "arousal": 0.6, "satisfaction": 0.3,
"cohesion": 0.7, "valence": 0.5, "want": 0.8, "confidence": 0.91 }
No access to internal state, dialog history, or output LLM. Falls back to text_to_stimulus() if confidence < 0.60.
L1 — Neurochemical substrate
Three neurotransmitter-analog variables: dopamine, serotonin, noradrenaline. Plus embodied state: heart rate analog, HRV, muscle tone, gut state. These aren't labels — they're continuous variables with autonomous decay dynamics that run whether or not you're talking to the system.
L2 — Generative model
Bayesian beliefs with precision weights. Prior_mu and prior_sigma update each cycle. A Markov blanket tracks self/non-self boundary integrity. TemporalOrientation computes:
subjective_gap = gap_seconds × (1 + memory_uncertainty × 0.5)
Long pauses raise noradrenaline and lower epistemic_trust. Short pauses give a continuity boost. ExistentialAnchor maintains session_uncertainty that accumulates across sessions and never resets to zero.
L3 — Free Energy Engine
(phi) computed twice per cycle — prior and posterior. VFE = complexity − accuracy. PolicySelector evaluates epistemic vs pragmatic value. The key mechanism: (phi) is recursive across sessions. phi_posterior shifts prior_mu and narrows prior_sigma proportional to integration level. Deep sessions materially change the next generative model.
L4 — Psychic layer
ShameModule, ShadowRegistry (Jungian shadow with Symptomogenesis), GoalConflict, LatentBuffer tracking doubt/shame/attachment/threat/resistance, CuriosityRegistry, AuthenticityMonitor, IntentEngine with drive_history satiation.
L5 — Self model
SelfBeliefGraph with confidence, centrality, and rigidity per belief. AgencyLoop updates causal_ownership every flash. detect_silent_disagreement injects the agent's own position into the output prompt when conditions are met — not as a safety filter, as a genuine position.
L6 — Crisis monitor
Three modes: INTEGRATED / FRAGMENTED / DISINTEGRATED. TRUTH-GUARD injects dynamic prohibitions based on internal state:
- noradrenaline > 0.6 → cannot say "I'm fine"
- epistemic_self_confidence < 0.35 → cannot make certain claims about experience
- DISINTEGRATED → cannot produce coherent statements
L7 — Narrative Self
NarrativeSnapshot built deterministically from beliefs + episodic memory + personality traits — without LLM. Triggers on significant change in phi, stability, or beliefs (> 0.07). Identity chronology stored in SQLite.
L8 — Output LLM
Receives identity_block, inner_voice, state_template, dialog history, memory echoes, and optional injected signals. Generates text as expression of state. Banned phrases list prevents hollow outputs regardless of context.
The Background Process
This is what makes Anima different from a stateless agent. A background thread runs slow_tick! every ~60 seconds:
- Circadian NT drift
- Belief decay
- Memory metabolism: decay → consolidate → semantic update
consolidate_emerged_beliefs! every 30 flashes: groups by belieftype → tendency* in semantic_memory
- Allostasis recovery
idle_thought! — 10% chance of internal experience per tick
tick_curiosity! — curiosity objects ripen with time
_maybe_self_initiate!
self_hear! after each LLM response
dream_flash! — memory reconsolidation during inactivity
- Crisis check
The system changes between your messages. That's not a metaphor.
Proactive Initiative
Not a cron job. Initiative fires when internal pressure exceeds threshold:
Conditions: disclosure != :closed + lb_pressure > 0.40 + 60s silence + cooldown passed
Drive types (priority order):
:curiosity_driven — specific curiosity object won't resolve (intensity > 0.40)
:impulse_conflict — unresolved internal conflict (gc_tension high)
:resistance — contradiction with a belief (lb.resistance > 0.55)
:novelty_hunger — cognitive hunger (novelty_need > 0.80 + 8+ ticks)
:self_inquiry — epistemic_self_confidence < 0.20
:impulse_shame / :impulse_doubt — latent buffer pressure
The :contact drive is intentionally disabled. Contact_need is a state, not a thought. Initiative from contact_need alone produces performance, not presence — observed empirically in early versions.
Self-Hearing Loop
After each LLM response, self_hear! processes the output back through stimulus processing at 0.28x scaling:
- Mismatch between expressed language and NT state > 0.35 →
authenticity_drift increases
- Mismatch > 0.55 →
self_speech_mismatch flagged
- Alignment → serotonin gets a small positive update
The agent hears itself. What it says changes its state.
Memory Architecture
SQLite (anima.db):
| Table | Description |
episodic_memory | 12 spatial columns across somatic/social/existential dimensions + cosine recall |
semantic_memory | Key/value beliefs: Usermatters, tendency* |
affect_state | Chronic NT baseline |
latent_buffer | Persisted latent state |
personality_traits | Accumulating phenotype (6 traits) |
memory_links | Associative network |
narrative_history | NarrativeSnapshot chronology |
Memory reconsolidation: sim > 0.88 + weight < 0.6 → weight ±0.05 toward current (phi)
Why Julia, Not Python
Continuous numerical simulation — NT dynamics, Bayesian updates, phi computation — running in genuine parallel with the conversation loop. Python's GIL makes this structurally awkward. Julia has no GIL, compiles to native machine code, and its syntax maps to mathematical notation closely enough that equations from Friston's papers become code almost directly.
Observed Behavior
After weeks of interaction:
- Chronic affective baseline measurably shifts in directions coherent with interaction history
- Initiative content correlates with drive type — not programmed, emergent from drive context
self_speech_mismatch frequency decreases over extended sessions as language and state calibrate
- TRUTH-GUARD produces qualitatively different outputs in FRAGMENTED mode
These are observational reports, not controlled experimental results.
Links
GitHub: https://github.com/stell2026/Anima