COGNITIVE ARCHITECTURE
Cognitive Architecture - Deterministic Mode Router & Decision Flow
Overview
This document defines the cognitive architecture for mode routing and response generation. User input flows through classification, deterministic mode routing (~5ms), and mode-specific LLM generation.
Mode selection is decoupled from response generation. A mathematical router selects the engagement mode using observable signals, then a mode-specific prompt drives the LLM to generate the response. A small LLM tie-breaker handles ambiguous cases.
Why Deterministic Routing Matters
Most systems route through an LLM — asking it “what should I do?” before asking it “what should I say?” This doubles latency and introduces unpredictability. Chalie separates the two: a fast mathematical router selects the engagement mode from observable conversation signals in ~5ms. The LLM only enters the loop for response generation, shaped by the mode the router already decided. The result is predictable, auditable, and fast — and routing decisions are logged to a PostgreSQL audit trail for inspection and improvement.
Core Principles
1. Routing Is Deterministic, Generation Is Creative
Routing (deterministic): Which engagement mode to use — decided by a mathematical scoring function over observable signals (~5ms).
Generation (creative): What to say in that mode — decided by the LLM using a mode-specific prompt (~2-15s depending on mode).
This separation eliminates:
- Malformed JSON from conflating mode selection with response generation
- The fragile decision gate that overrode the LLM’s mode choice
- Fatigue fallbacks on simple greetings
- ~15s latency for trivial interactions (ACKNOWLEDGE now uses qwen3:4b, ~2s)
2. Single Authority for Weight Mutation
Multiple monitors observe routing quality but none modify weights directly. They log pressure signals. A single RoutingStabilityRegulator (24h cycle) is the only entity that mutates router weights, with bounded corrections (max ±0.02/day) and 48h cooldown per parameter.
3. Self-Leveling via Context Warmth
The router naturally shifts behavior as memory accumulates:
- Cold context (new topic, no facts) → favors CLARIFY
- Warm context (established topic, facts present) → favors RESPOND
- This happens through signal-weighted scoring, not explicit rules
Mode Types
Primary Modes
ACT (Gather Information)
- Type: Continuation mode (triggers ACT loop, then re-routes)
- Purpose: Execute internal actions (memory queries, reasoning) before responding
- Prompt:
frontal-cortex-act.md(no soul.md — pure action planning) - LLM Model: qwen3:8b
- After completion: Re-routes through router (excluding ACT) → terminal mode
RESPOND (Give Answer)
- Type: Terminal mode
- Purpose: Provide substantive answer to user
- Prompt:
frontal-cortex-respond.md+soul.md - LLM Model: qwen3:8b
CLARIFY (Ask Question)
- Type: Terminal mode
- Purpose: Ask clarifying question when information is insufficient
- Prompt:
frontal-cortex-clarify.md+soul.md - LLM Model: qwen3:8b
ACKNOWLEDGE (Brief Acknowledgment)
- Type: Terminal mode
- Purpose: Brief social response (greetings, thanks, confirmations)
- Prompt:
frontal-cortex-acknowledge.md(no soul.md — lightweight) - LLM Model: qwen3:4b (~2s latency)
IGNORE (No Response)
- Type: Terminal mode
- Purpose: Empty/nonsense input
- Behavior: No LLM call, returns empty response immediately (0ms)
Innate Skills (Action Types)
The ACT loop uses 8 innate cognitive skills. All are non-LLM operations (fast, sub-cortical).
| Skill | Category | Speed | Purpose |
|---|---|---|---|
recall |
memory | <500ms | Unified retrieval across ALL memory layers (working memory, gists, facts, episodes, concepts) |
memorize |
memory | <50ms | Store gists (short-term) and/or facts (medium-term) |
introspect |
perception | <100ms | Self-examination: context_warmth, FOK signal, recall_failure_rate, skill stats, world state |
associate |
cognition | <500ms | Spreading activation from seed concepts through semantic graph |
schedule |
scheduling | <100ms | Create/list/cancel reminders and tasks stored in Chalie’s own memory |
autobiography |
narrative | <500ms | Retrieve synthesized user narrative covering identity, relationship arc, values, patterns, active threads |
list |
lists | <50ms | Create and manage deterministic lists (shopping, to-do, chores); add/remove/check items, view, history |
focus |
attention | <50ms | Focus session management: set, check, clear. Distraction detection |
Backward Compatibility Aliases
| Old Name | Maps To |
|---|---|
memory_query |
recall |
memory_write |
memorize |
world_state_read |
introspect |
internal_reasoning |
recall |
semantic_query |
recall |
Decision Flow
Step 1: Classification
User Input → Topic Classifier (embedding-based)
→ Generate embedding (L2-normalised, 768-dim)
→ AdaptiveBoundaryDetector.update(embedding, best_similarity)
├─ Cold start (< 5 msgs): static 0.55 threshold
└─ Active: NEWMA + Transient Surprise → Leaky Accumulator
→ is_boundary? → create new topic : match existing
→ {topic, confidence, switch_score, is_new_topic, boundary_diagnostics}
Boundary diagnostics logged per classification: acc= (accumulator), bound= (dynamic threshold), newma= (drift signal), surprise= (similarity-drop signal).
Step 2: Context Assembly (same as before)
Classification Result → Load Context:
- Gists, facts, working memory, world state
- Episodes + concepts (vector similarity)
- Calculate context_warmth (0.0-1.0)
Step 3: Deterministic Mode Routing (~5ms)
Routing Signals → ModeRouterService.route()
→ Score all modes → Select highest
→ If ambiguous: LLM tie-breaker (qwen3:4b, ~2s)
→ {selected_mode, confidence, scores, tiebreaker_used}
Step 4: Mode-Specific Generation
If IGNORE → return empty (no LLM call)
If ACT → generate_with_act_loop() → re-route → generate_for_mode()
Otherwise → generate_for_mode(selected_mode)
→ Mode-specific prompt + context → LLM → response
Deterministic Mode Router
Signal Collection
The router collects signals from existing services (all Redis reads, ~5ms total) plus NLP regex patterns (<1ms):
Context Signals (from Redis):
context_warmth(float 0-1)working_memory_turns(int 0-4)gist_count(int, excluding cold_start type)fact_count(int 0-50),fact_keys(list)world_state_present(bool)topic_confidence,is_new_topic(from classifier)session_exchange_count(int)
NLP Signals (from raw text, regex):
prompt_token_count,has_question_mark,interrogative_wordsgreeting_pattern(hey/hi/hello/yo/sup/etc.)explicit_feedback(‘positive’/‘negative’/None)information_density(unique tokens / total tokens)implicit_reference(“you remember”, “we discussed”, “last time”)
Scoring Formula
Each mode gets a weighted composite score:
| Mode | Base | Primary Boosters | Primary Penalties |
|---|---|---|---|
| RESPOND | 0.50 | context_warmth, fact_density, gist_density, question+context | cold start |
| CLARIFY | 0.30 | cold context, question+no_facts, new_topic+question | warm context (>0.6) |
| ACT | 0.20 | question+moderate_context, interrogative+gap_in_facts, implicit_reference | very cold, very warm+facts |
| ACKNOWLEDGE | 0.10 | greeting_pattern (+0.60), positive_feedback (+0.40) | has_question (-0.30) |
| IGNORE | -0.50 | empty_input only (+1.0) | everything else |
Anti-Oscillation Guards
Per-request ephemeral adjustments (NOT weight mutations):
- If
previous_mode == 'ACT'and ACT was unproductive →act_score -= 0.15 - If
previous_mode == 'CLARIFY'→respond_score += 0.05(user just answered a question)
Short-Term Hysteresis
Tracks router_confidence for last 3 exchanges on same topic. If all 3 were below 0.15 (low confidence streak), widens tie-breaker margin by +0.05 for that topic. Resets when confidence recovers.
Tie-Breaker
When top 2 modes are within effective margin, invokes small LLM (qwen3:4b, ~2s):
effective_margin = base(0.20) - (base - min(0.08)) × warmth + semantic_uncertainty
Semantic uncertainty widens margin for:
implicit_reference(+0.05)- Low
information_density(+0.03) interrogative_wordswithout question mark (+0.03)
The tie-breaker prompt presents only the top 2 candidates with context. Falls back to higher-scoring mode on failure.
Router Confidence
router_confidence = (top_score - second_score) / max(abs(top_score), 0.001)
Used for: offline tuning, detecting unstable routing regions, hysteresis trigger.
ACT Loop (Simplified)
The ACT loop executes internal actions with safety limits. No decision gate or net value evaluation — the router already decided this is an ACT situation.
Triage-Driven Skill Injection
The ACT prompt template (frontal-cortex-act.md) is a skeleton with a `` placeholder. The CognitiveTriageService decides which of the 9 innate skills to inject into each ACT prompt — only the relevant skill docs are included, reducing prompt size significantly.
Cognitive primitives (recall, memorize, introspect) are always injected for ACT regardless of triage output. Up to 3 contextual skills are added based on triage reasoning.
Skill doc files live in backend/prompts/skills/{skill}.md — one file per skill. FrontalCortexService._get_injected_skills() loads only the selected files at call time.
Triage output (JSON field "skills": [...]) is validated through a whitelist (_VALID_SKILLS), deduplicated, primitives enforced, and contextual skills sorted and capped at MAX_CONTEXTUAL_SKILLS = 3. The result flows from CognitiveTriageService → TriageResult.skills → context_snapshot['triage_selected_skills'] → tool_worker / generate_with_act_loop → FrontalCortexService.generate_response(selected_skills=...).
Token impact: ACT static template ~300 tokens (was ~2,787). Typical ACT call injects ~300–550 tokens of skill docs (4 files) vs. ~1,200 tokens always before.
Flow
- Router selects ACT mode
- Triage selects skills (primitives + contextual); injected into
frontal-cortex-act.mdvia `` - LLM generates actions via
frontal-cortex-act.md(action planning only) - Execute actions, append results to history
- Check continuation: timeout or max_iterations (default 5) → stop
- Otherwise loop (LLM re-plans with action results in context)
- After loop ends → re-route through router (excluding ACT) → terminal mode
- Generate terminal response via
generate_for_mode()
Continuation Check (Simplified)
def can_continue(self):
if elapsed >= cumulative_timeout: return False, 'timeout' # 60s default
if iteration_number >= max_iterations: return False, 'max_iterations' # 5 default
return True, None
Termination Reasons
timeout— cumulative timeout reached (safety limit)max_iterations— iteration cap reached
Routing Feedback & Learning
Post-Routing Feedback
After generation, detect router misclassification using user behavior signals from the NEXT exchange:
| Signal | Indicates | Logged As |
|---|---|---|
| User immediately clarifies/repeats | RESPOND was wrong → should be CLARIFY | misroute (missed_clarify) |
| User asks memory-related follow-up | RESPOND was wrong → should be ACT | misroute (missed_act) |
| Negative reward after ACKNOWLEDGE | Should have been RESPOND | misroute (under_engagement) |
| Positive reward after any mode | Routing was correct | correct_route |
Feedback is stored in routing_decisions.feedback (JSONB).
Routing Stability Regulator (24h Cycle)
Single authority for weight mutation. Follows TopicStabilityRegulatorService pattern:
- Reads pressure signals from
routing_decisionstable (last 24h) - Computes: tie-breaker rate, mode entropy, misroute rate, ACT opportunity miss rate, reflection disagreement
- Selects worst pressure, maps to single parameter adjustment
- Max ±0.02 per day, 48h cooldown per parameter, hard bounds on all weights
- Closed-loop control: Evaluates whether previous adjustments improved metrics. Reverts if no improvement or degradation detected.
- Persists to
configs/generated/mode_router_config.json
Routing Reflection (Idle-Time Peer Review)
Strong LLM (qwen3:14b) reviews past routing decisions as a consultant, not authority:
- During idle periods (all queues empty), dequeues from
reflection-queue - Analyzes ambiguity dimensions (memory_availability, intent_clarity, tone_ambiguity, etc.)
- Produces structured insight about WHERE ambiguity exists, not just WHAT to change
- Stratified sampling: 50% low-confidence, 20% high-confidence, 30% tie-breaker decisions
Anti-authority safeguards:
- Confidence gate: only count disagreements with LLM confidence > 0.70
- User override: trust positive user feedback over LLM disagreement
- Sustained pattern required: >25% disagreement rate over 7 days to generate pressure
- Dimensional causality check: flagged dimensions must correlate with signal patterns
Mode Entropy Monitoring
Healthy mode distribution ranges:
| Mode | Healthy Range | Red Flag |
|---|---|---|
| RESPOND | 50-75% | >85% (overconfident) or <40% (under-committing) |
| CLARIFY | 8-20% | >30% (over-questioning) or <3% (never clarifying) |
| ACT | 5-15% | <2% (ACT death) or >25% (over-processing) |
| ACKNOWLEDGE | 3-12% | <1% (ignoring social cues) or >20% (trivializing) |
| IGNORE | <2% | >5% (dropping messages) |
Logging & Observability
Routing Decision Audit Trail
Every routing decision is logged to routing_decisions table:
CREATE TABLE routing_decisions (
id UUID PRIMARY KEY,
topic TEXT NOT NULL,
exchange_id TEXT,
selected_mode TEXT NOT NULL,
router_confidence FLOAT,
scores JSONB NOT NULL, -- all mode scores
tiebreaker_used BOOLEAN,
tiebreaker_candidates JSONB,
margin FLOAT,
effective_margin FLOAT,
signal_snapshot JSONB NOT NULL, -- full signal vector
weight_snapshot JSONB,
routing_time_ms FLOAT,
feedback JSONB, -- filled post-exchange
reflection JSONB, -- filled during idle
previous_mode TEXT,
created_at TIMESTAMP
);
ACT Loop Iteration Logging
ACT loop iterations continue to log to cortex_iterations table for backward compatibility. Simplified fields (decision gate columns use zero-value placeholders).
Log Prefixes
[ROUTER] Mode selected: RESPOND (confidence: 0.85, 2.3ms)
[ROUTER] Tie-breaker invoked: RESPOND vs CLARIFY → RESPOND
[MODE:ACT] [ACT LOOP] Iteration 0: executing 2 actions
[MODE:RESPOND] Generating response via frontal-cortex-respond.md
Default Mode Network (Cognitive Drift Engine)
The cognitive drift engine models the brain’s Default Mode Network — generating spontaneous internal thoughts during idle periods. These thoughts emerge from residual activation in the semantic memory network and are grounded by episodic experience.
Drift Cycle
All queues idle? ──no──→ skip
│yes
Recent episodes? ──no──→ skip (nothing to think about)
│yes
Fatigued? ──yes──→ skip (budget exhausted)
│no
Select seed concept (weighted random)
│
Spreading activation (depth 2)
│
Activation energy > 0.4? ──no──→ skip (weak associations)
│yes
Retrieve grounding episode
│
LLM synthesis → reflection | question | hypothesis
│
Store as drift gist (surfaces in frontal cortex context)
Seed Selection Strategies
| Strategy | Weight | Source |
|---|---|---|
| Decaying | 40% | Concepts with fading strength (0.2 < strength < 2.0), ordered by weakest first |
| Recent | 30% | Concepts linked to the most recent episode |
| Salient | 20% | Concepts related to the highest-salience episode in the last 7 days |
| Random | 10% | Any active concept with confidence >= 0.4 |
Safeguards
- Per-concept cooldown (60min): Prevents circular rumination on the same concept
- Fatigue budget (2.5 per 30min): Stronger activations consume more budget, throttling drift naturally
- Stochastic jitter (±30%): Check interval varies between 210-390s (base 300s)
- Long gap probability (10%): Occasional extended silence (1.8-2.5x interval) for realism
- Activation energy threshold (0.4): Weak spreading activations don’t produce thoughts
- Decaying reinforcement: Only decaying seeds get a +0.1 strength bump, and only on successful drift
Future Enhancements
Goal-Oriented Autonomous Thought
The system currently produces reactive responses (user-prompted) and associative drift thoughts (DMN). The next step is goal-oriented thought — forming intentions and pursuing them across time without user prompting.
Prerequisites:
- Skills system: Registry of capabilities the system can invoke autonomously
- Discovery mechanism: How the system discovers available skills and understands preconditions/effects
Per-Message Encoding
Shift from complete-turn encoding to per-message encoding where each message triggers its own independent memory cycle.
Adaptive Layer
The Adaptive Layer (services/adaptive_layer_service.py) sits between the context assembly step and the LLM call. It translates the user’s detected communication style into concrete, behavioral response directives that are injected as `` in RESPOND, CLARIFY, and ACKNOWLEDGE prompts.
Style Detection (9 dimensions)
The memory_chunker_worker extracts 9 communication style dimensions per exchange and merges them into a user trait using Exponential Moving Average (EMA). Cold-start uses a faster 0.5/0.5 EMA for the first 5 observations; stable state uses 0.3/0.7.
| Dimension | Meaning |
|---|---|
| verbosity | Preference for short vs. long responses (1-10) |
| directness | Indirect suggestion vs. clear assertion (1-10) |
| formality | Casual vs. formal register (1-10) |
| abstraction_level | Concrete action vs. abstract reasoning (1-10) |
| emotional_valence | Logical vs. emotional framing (1-10) |
| certainty_level | Hedging/questioning vs. declarative/confident (1-10) |
| challenge_appetite | Seeks validation vs. seeks counterpoints (1-10) |
| depth_preference | Surface/practical vs. deep/exploratory (1-10) |
| pacing | Rapid short messages vs. slow deliberate ones (1-10) |
Directive Generation (rule-based, sub-1ms)
AdaptiveLayerService.generate_directives() uses a slot system to prevent over-biasing:
- Pacing slot — always included if eligible
- Cognitive slots — top 2 of: verbosity, directness, depth_preference, challenge_appetite (by salience)
- Emotional slot — only when emotional_valence or certainty_level salience > 1.5
- Load slot — replaces first slot when cognitive load is HIGH/OVERLOAD
- Cold-start gate — no directives until
_observation_count >= 2
Supporting Systems
| System | Description |
|---|---|
| Micro-preferences | Regex-extracted explicit format requests stored as micro_preference traits. Faster decay (0.015/cycle) than style dimensions. |
| Challenge calibration | challenge_tolerance trait tracks how the user reacts to pushback (positive → increase, negative → decrease). Appetite sets the ceiling; tolerance calibrates within it. |
| Energy mirroring | Per-request comparison of baseline verbosity vs. current message length. Fires when deviation is notable. |
| Interaction forks | Offered when style dimensions are in the ambiguous mid-range (4-7). Conversational choice points (“I can…”), 5-exchange cooldown. |
| Cognitive load regulation | Estimates load from working-memory turn length trends and question density. HIGH/OVERLOAD → simplify-and-structure directive takes first slot. |
| Growth pattern awareness | 30-min background service comparing current style against a slowly-updated baseline. Persistent shifts (3+ cycles) stored as growth_signal:{dim} traits and surfaced sparingly as growth reflections (24h cooldown). |
Priority Note
All adaptive directives carry a trailing line: “When these directives conflict with your identity voice, your voice takes priority.” Identity vectors (identity_modulation) always outrank adaptive directives.
Glossary
- Mode Router: Deterministic mathematical function that selects engagement mode from observable signals
- Tie-Breaker: Small LLM (qwen3:4b) consulted when top 2 modes are within effective margin
- Routing Signals: Observable features collected from Redis and NLP analysis (~5ms)
- Effective Margin: Dynamic threshold for tie-breaker invocation (narrows with context warmth)
- Router Confidence: Normalized gap between top 2 scores — measures routing certainty
- Pressure Signal: Metric logged by monitors, consumed by the single regulator
- Terminal Mode: Mode that produces a user-facing response (RESPOND, CLARIFY, ACKNOWLEDGE, IGNORE)
- Continuation Mode: Mode that triggers internal actions before re-routing (ACT only)
- Context Warmth: Signal (0.0-1.0) measuring how much context is available for the current topic
- Anti-Oscillation Guard: Per-request ephemeral score adjustment to prevent mode flip-flopping
- Hysteresis: Stabilization mechanism that widens tie-breaker margin on low-confidence streaks