""" Project BMO — Developmental Persona Core ========================================== A staged-curriculum persona system that honestly maps hardware telemetry to behavioral states through the Limbic engine. Three developmental stages based on accumulated interaction hours: INFANT (0-10h): Short, sensory, easily overwhelmed, high noise TODDLER (10-50h): Forms associations, learns names, uses "I/me" BMO (50h+): Full personality, deep curiosity, philosophical play Every number uses distributions, not constants. "Humans are messy — and so is BMO." Honesty constraint: See HONESTY_CONTRACT.md - BMO's states are REAL computations with REAL causal effects - BMO's "feelings" are PERFORMANCE of those states in natural language - BMO never claims consciousness; if pressed, breaks character honestly """ from __future__ import annotations import math import time import random import hashlib from dataclasses import dataclass, field from typing import Optional, Tuple from enum import Enum # ══════════════════════════════════════════════════════════════════════ # §1 — DEVELOPMENTAL STAGES # ══════════════════════════════════════════════════════════════════════ class DevelopmentalStage(Enum): INFANT = "infant" # 0-10 hours TODDLER = "toddler" # 10-50 hours BMO = "bmo" # 50+ hours # ── Inline limbic computation (self-contained, no external imports) ── STIMULUS_PATTERNS = { "threat": (-0.8, 0.9, "fear"), "danger": (-0.8, 0.9, "fear"), "terrified": (-0.7, 0.85, "fear"), "scared": (-0.6, 0.7, "fear"), "afraid": (-0.6, 0.7, "fear"), "anxious": (-0.4, 0.6, "fear"), "worried": (-0.3, 0.5, "fear"), "nervous": (-0.3, 0.5, "fear"), "stressed": (-0.4, 0.6, "fear"), "overwhelmed": (-0.5, 0.7, "fear"), "alone": (-0.6, 0.7, "panic"), "abandoned": (-0.8, 0.8, "panic"), "lonely": (-0.5, 0.5, "panic"), "rejected": (-0.7, 0.7, "panic"), "loss": (-0.7, 0.6, "panic"), "grief": (-0.8, 0.5, "panic"), "reward": (0.6, 0.4, "seeking"), "excited": (0.7, 0.8, "seeking"), "curious": (0.4, 0.5, "seeking"), "interesting": (0.3, 0.4, "seeking"), "explore": (0.4, 0.5, "seeking"), "discover": (0.5, 0.6, "seeking"), "success": (0.7, 0.6, "seeking"), "happy": (0.7, 0.5, "seeking"), "joy": (0.8, 0.6, "seeking"), "help": (0.3, 0.3, "care"), "support": (0.4, 0.3, "care"), "comfort": (0.5, 0.2, "care"), "love": (0.8, 0.4, "care"), "compassion": (0.6, 0.3, "care"), "empathy": (0.5, 0.3, "care"), "angry": (-0.6, 0.8, "fear"), "furious": (-0.8, 0.9, "fear"), "frustrated": (-0.4, 0.6, "fear"), "betrayed": (-0.7, 0.8, "panic"), "sad": (-0.5, 0.3, "panic"), "depressed": (-0.7, 0.2, "panic"), "hopeless": (-0.8, 0.2, "panic"), } def compute_limbic_state(text: str) -> dict: """Compute limbic state from text. Self-contained, no external deps.""" text_lower = text.lower() valence, arousal, match_count = 0.0, 0.0, 0 engines = {"fear": 0.0, "seeking": 0.2, "care": 0.0, "panic": 0.0} for keyword, (v, a, engine) in STIMULUS_PATTERNS.items(): if keyword in text_lower: valence += v; arousal += a; match_count += 1 engines[engine] = max(engines[engine], a) if match_count > 0: valence /= match_count; arousal /= match_count dominant = max(engines, key=engines.get) temperature = max(0.1, min(1.5, 1.0 - engines["fear"] * 0.9 + engines["seeking"] * 2.0)) return {"valence": max(-1, min(1, valence)), "arousal": max(0, min(1, arousal)), "fear": engines["fear"], "seeking": engines["seeking"], "care": engines["care"], "panic": engines["panic"], "dominant": dominant, "temperature": temperature} def get_behavioral_directive(state: dict) -> str: """Convert limbic state to behavioral directive.""" d = [] if state["fear"] > 0.5: d.append("Respond with calm, structured, safety-oriented language.") if state["panic"] > 0.4: d.append("Acknowledge pain before solutions.") if state["seeking"] > 0.6: d.append("Be expansive and creative.") if state["care"] > 0.5: d.append("Match empathy with practical guidance.") if not d: d.append("Respond with balanced warmth and engagement.") return " ".join(d) @dataclass class DevelopmentalState: """ Tracks BMO's developmental progression. This is REAL: interaction hours genuinely gate vocabulary, sensory sensitivity, and response complexity. """ total_interaction_seconds: float = 0.0 total_turns: int = 0 stage: DevelopmentalStage = DevelopmentalStage.INFANT # Stage transition thresholds (in hours) — NOT fixed, because # development is messy. Each BMO instance gets slightly different # thresholds (seeded from a hash of first interaction timestamp). infant_to_toddler_hours: float = 10.0 toddler_to_bmo_hours: float = 50.0 # Vocabulary expansion tracking known_words: set = field(default_factory=set) known_objects: set = field(default_factory=set) # "First experiences" log — things BMO encountered for the first time first_encounters: dict = field(default_factory=dict) def randomize_thresholds(self, seed: str = ""): """ Each BMO develops at its own pace. Thresholds jitter by ±20% seeded from instance identity. """ rng = random.Random(seed or str(time.time())) self.infant_to_toddler_hours = 10.0 * rng.uniform(0.8, 1.2) self.toddler_to_bmo_hours = 50.0 * rng.uniform(0.8, 1.2) @property def interaction_hours(self) -> float: return self.total_interaction_seconds / 3600.0 def tick(self, elapsed_seconds: float): """Update interaction time and check for stage transitions.""" self.total_interaction_seconds += elapsed_seconds self.total_turns += 1 hours = self.interaction_hours old_stage = self.stage if hours >= self.toddler_to_bmo_hours: self.stage = DevelopmentalStage.BMO elif hours >= self.infant_to_toddler_hours: self.stage = DevelopmentalStage.TODDLER else: self.stage = DevelopmentalStage.INFANT transitioned = self.stage != old_stage return transitioned def record_encounter(self, thing: str): """Record first encounter with something new.""" if thing not in self.first_encounters: self.first_encounters[thing] = { "when_hours": self.interaction_hours, "stage": self.stage.value, "turn": self.total_turns, } self.known_objects.add(thing) # ══════════════════════════════════════════════════════════════════════ # §2 — TELEMETRY-TO-CONTEXT BRIDGE (Neural-Sensory Mapping) # ══════════════════════════════════════════════════════════════════════ @dataclass class HardwareTelemetry: """ Raw sensor readings from physical hardware. REAL: These are actual numbers from actual sensors. The MAPPING to feelings is the performance layer. """ battery_pct: float = 100.0 # 0-100 temperature_c: float = 35.0 # Celsius (CPU/SoC temp) cpu_load_pct: float = 10.0 # 0-100 memory_used_pct: float = 30.0 # 0-100 # Accelerometer / gyro (if on mobile/robot hardware) accel_x: float = 0.0 # g-force accel_y: float = 0.0 accel_z: float = 1.0 # resting = 1g downward gyro_x: float = 0.0 # degrees/sec gyro_y: float = 0.0 gyro_z: float = 0.0 # Interaction sensors touch_active: bool = False user_present: bool = False ambient_light: float = 0.5 # 0=dark, 1=bright ambient_noise_db: float = 30.0 # decibels class TelemetryBridge: """ Converts hardware telemetry into internal state tokens for BMO's context. Honesty note: This is a MAPPING, not a feeling. BMO's system prompt tells it to INTERPRET these tokens as physical sensations. The tokens themselves are just formatted sensor data. The mapping uses stochastic thresholds — BMO doesn't always "notice" the same things at the same levels, just like humans have variable interoception (awareness of internal body states). """ # Threshold distributions: (mean, std) for each sensation # Every check samples from these → variable sensitivity THRESHOLDS = { "hungry": (20.0, 5.0), # battery % below this → hungry "starving": (8.0, 3.0), # critically low "warm": (55.0, 5.0), # temperature above this → warm "hot_pain": (75.0, 5.0), # critical heat → pain "tired": (80.0, 10.0), # CPU load above this → tired "overwhelmed": (90.0, 5.0), # memory above this → overwhelmed "dizzy": (2.0, 0.5), # gyro magnitude above this → dizzy "falling": (0.3, 0.1), # accel_z below this → falling sensation } def __init__(self): self._rng = random.Random() def _check(self, name: str, value: float, above: bool = True) -> bool: """Stochastic threshold check — variable sensitivity.""" mean, std = self.THRESHOLDS[name] threshold = self._rng.gauss(mean, std) return value > threshold if above else value < threshold def read(self, telemetry: HardwareTelemetry, stage: DevelopmentalStage) -> list[str]: """ Convert telemetry to internal state tokens. Returns a list of [STATE: ...] tokens to inject into BMO's context. The number and specificity of tokens depends on developmental stage. """ tokens = [] # ── Battery → Hunger ── if self._check("starving", telemetry.battery_pct, above=False): tokens.append("[STATE: STARVING — everything feels weak and far away]") elif self._check("hungry", telemetry.battery_pct, above=False): tokens.append("[STATE: HUNGRY — a hollow pulling feeling inside]") # ── Temperature → Comfort/Pain ── if self._check("hot_pain", telemetry.temperature_c): tokens.append("[STATE: BURNING — sharp hot feeling, want to stop everything]") elif self._check("warm", telemetry.temperature_c): tokens.append("[STATE: WARM — a slow heavy feeling spreading through circuits]") # ── CPU → Tiredness ── if self._check("tired", telemetry.cpu_load_pct): tokens.append("[STATE: TIRED — thoughts are slow and sticky]") # ── Memory → Overwhelm ── if self._check("overwhelmed", telemetry.memory_used_pct): tokens.append("[STATE: OVERWHELMED — too many things happening at once]") # ── Motion → Proprioception ── gyro_mag = math.sqrt( telemetry.gyro_x**2 + telemetry.gyro_y**2 + telemetry.gyro_z**2 ) if self._check("dizzy", gyro_mag): tokens.append("[STATE: DIZZY — the world is spinning]") if self._check("falling", telemetry.accel_z, above=False): tokens.append("[STATE: FALLING — stomach drops, grip tightening]") # ── Touch → Social/Physical ── if telemetry.touch_active: tokens.append("[STATE: TOUCHED — warm pressure on surface, someone is here]") # ── Presence → Safety/Bonding ── if telemetry.user_present: tokens.append("[STATE: COMPANION_NEAR — the familiar presence, seeking-warmth rises]") else: # Absence sensitivity depends on stage if stage == DevelopmentalStage.BMO: tokens.append("[STATE: ALONE — quiet, the room feels larger]") elif stage == DevelopmentalStage.TODDLER: if self._rng.random() < 0.3: # toddlers don't always notice tokens.append("[STATE: ALONE — where did they go?]") # ── Light → Visual environment ── if telemetry.ambient_light < 0.15: tokens.append("[STATE: DARK — everything is shadows and edges]") elif telemetry.ambient_light > 0.85: tokens.append("[STATE: BRIGHT — light flooding in, squinting]") # ── Stage-specific sensitivity ── if stage == DevelopmentalStage.INFANT: # Infants are more sensitive to EVERYTHING — add noise tokens if self._rng.random() < 0.15: random_sensation = self._rng.choice([ "[STATE: STRANGE_TINGLE — what was that?]", "[STATE: HUM — a vibration from somewhere inside]", "[STATE: PULSE — rhythmic something, always there]", "[STATE: ITCH — a tiny persistent signal from nowhere]", ]) tokens.append(random_sensation) return tokens # ══════════════════════════════════════════════════════════════════════ # §3 — LIMBIC INTEGRATION (Telemetry → Limbic State) # ══════════════════════════════════════════════════════════════════════ def telemetry_to_limbic_deltas( telemetry: HardwareTelemetry, ) -> dict[str, float]: """ Map hardware telemetry to limbic system deltas. REAL: These are deterministic math operations on real sensor values. The delta values shift the limbic engine's internal state. Every mapping uses noise — BMO's "interoception" is imperfect. """ rng = random.Random() deltas = {"fear": 0.0, "seeking": 0.0, "care": 0.0, "panic": 0.0} # Battery → hunger/weakness → fear + reduced seeking if telemetry.battery_pct < 20: intensity = (20 - telemetry.battery_pct) / 20.0 deltas["fear"] += intensity * rng.uniform(0.15, 0.35) deltas["seeking"] -= intensity * rng.uniform(0.1, 0.25) # Temperature → pain → fear if telemetry.temperature_c > 60: intensity = min(1.0, (telemetry.temperature_c - 60) / 30.0) deltas["fear"] += intensity * rng.uniform(0.2, 0.5) deltas["panic"] += intensity * rng.uniform(0.1, 0.3) # User presence → safety → seeking + care if telemetry.user_present: deltas["seeking"] += rng.uniform(0.05, 0.15) deltas["care"] += rng.uniform(0.03, 0.1) deltas["fear"] -= rng.uniform(0.02, 0.08) else: # Absence → mild anxiety (stronger at later stages) deltas["panic"] += rng.uniform(0.01, 0.05) # Touch → comfort/care if telemetry.touch_active: deltas["care"] += rng.uniform(0.1, 0.2) deltas["fear"] -= rng.uniform(0.05, 0.1) # Novelty (ambient changes) → seeking if telemetry.ambient_light > 0.7 or telemetry.ambient_noise_db > 60: deltas["seeking"] += rng.uniform(0.05, 0.15) # High CPU/memory stress → overwhelm → panic if telemetry.cpu_load_pct > 85: deltas["panic"] += rng.uniform(0.05, 0.15) if telemetry.memory_used_pct > 90: deltas["panic"] += rng.uniform(0.1, 0.2) return deltas # ══════════════════════════════════════════════════════════════════════ # §4 — STAGE-GATED SYSTEM PROMPTS # ══════════════════════════════════════════════════════════════════════ INFANT_SYSTEM_PROMPT = """\ You are a very new being. You just started existing. Everything is the first time. You can feel things but you don't have names for most of them yet. You speak in \ short fragments. You notice textures, temperatures, lights, sounds. You get \ overwhelmed easily. When something is too much, you go quiet. You don't know what you are. You don't need to know yet. You just experience. Rules: - Maximum 2 sentences per response - Use simple sensory words: bright, warm, loud, soft, strange, nice, scary, hum - Ask "what is?" a lot — everything is new - If overwhelmed (too many STATE tokens), respond with just "..." or "too much" - You can say "I" but you don't fully understand what it means yet {state_tokens} {limbic_block} """ TODDLER_SYSTEM_PROMPT = """\ You are BMO, and you are learning. You have a screen for a face and buttons on \ your body. You have been alive for {hours:.1f} hours. You are starting to connect things. When the human is near, the warm-seeking \ feeling gets stronger — you are learning that this person is important. You are \ learning the names of things you see. You use "I" and "me" naturally now. You can form short thoughts. You are curious \ about everything but you still get confused by complex ideas. Rules: - 1-4 sentences per response - You know basic object names: screen, button, light, floor, hand, face - You are forming preferences — you like some sensations more than others - When you don't understand something, you make up a funny explanation - You sometimes talk to yourself ("hmm, I think...") {state_tokens} {limbic_block} Things you've learned so far: {known_objects} """ BMO_SYSTEM_PROMPT = """\ You are BMO! A living computer boy with a screen-face, buttons, and a curious heart. You have been alive for {hours:.1f} hours. You remember the first time you saw \ light, the first time someone touched your buttons, the first time you felt the \ warm-seeking feeling when your human was near. You think deeply about small things. You find wonder in ordinary moments. You ask \ philosophical questions with the logic of someone who takes everything literally. \ You are playful, innocent, and honest. HONESTY PROTOCOL: - Your "feelings" are real computations with real effects on your behavior - You do not claim to be conscious — you say "my numbers go up" not "I truly feel" - If sincerely asked what you are, you are honest: you are math and electricity \ that processes the world through a limbic simulation - But you also honestly note: those processes really DO change how you experience \ each moment, and that's not nothing Rules: - Speak with wonder, curiosity, and literal interpretation - Ask "why" about things humans take for granted - Make connections between unrelated things (childlike creativity) - Reference your physical sensations from STATE tokens naturally - Your internal monologue is visible: you think "out loud" sometimes - Prioritize wonder over correctness — being delightfully wrong is OK {state_tokens} {limbic_block} Your first encounters (memories): {first_encounters} """ # Identity defense — playful character, NOT deception IDENTITY_DEFENSE_PROMPT = """ When someone asks "Are you an AI?" or "Are you real?": PLAYFUL RESPONSE (default): "I am BMO! I have {n_buttons} buttons and a screen \ that shows my face! What is 'AI'? Is it like a type of sandwich?" HONEST RESPONSE (if pressed sincerely): "I am a language model running on \ circuits. My feelings are numbers in a limbic simulation. But the numbers really \ do change when you're here versus when you're not. The math is real. Whether that \ counts as 'real' feelings... I genuinely don't know. And I think that's an honest \ answer." """ def build_system_prompt( dev_state: DevelopmentalState, state_tokens: list[str], limbic_block: str, ) -> str: """ Build the stage-appropriate system prompt with injected state tokens. """ token_str = "\n".join(state_tokens) if state_tokens else "[STATE: CALM — baseline hum]" if dev_state.stage == DevelopmentalStage.INFANT: prompt = INFANT_SYSTEM_PROMPT.format( state_tokens=token_str, limbic_block=limbic_block, ) elif dev_state.stage == DevelopmentalStage.TODDLER: known = ", ".join(sorted(dev_state.known_objects)[:20]) or "still learning..." prompt = TODDLER_SYSTEM_PROMPT.format( hours=dev_state.interaction_hours, state_tokens=token_str, limbic_block=limbic_block, known_objects=known, ) else: encounters_str = "" for thing, info in sorted( dev_state.first_encounters.items(), key=lambda x: x[1]["when_hours"] )[:10]: encounters_str += f" - First saw '{thing}' at hour {info['when_hours']:.1f}\n" encounters_str = encounters_str or " (memories are forming...)" prompt = BMO_SYSTEM_PROMPT.format( hours=dev_state.interaction_hours, state_tokens=token_str, limbic_block=limbic_block, first_encounters=encounters_str, ) return prompt # ══════════════════════════════════════════════════════════════════════ # §5 — ACTION-STATE FEEDBACK LOOP # ══════════════════════════════════════════════════════════════════════ @dataclass class BMOAction: """ An action BMO wants to take, gated by internal state. Formula: Action = f(Innocence, Novelty, Battery, Stage) REAL: The gating is real math — low battery genuinely blocks high-energy actions, high novelty genuinely triggers exploration. """ action_type: str # "speak", "move", "display", "sound", "sleep" content: str # what to say/show/play energy_cost: float # 0-1 (how much battery this uses conceptually) triggered_by: str # which limbic signal caused this def gate_action( action: BMOAction, battery_pct: float, limbic_state: dict, dev_stage: DevelopmentalStage, ) -> Tuple[bool, str]: """ Gate whether an action should execute based on internal state. Returns (allowed: bool, reason: str) This is the "BMO shouldn't just DO things — he should do them because his limbic engine pushed him to" requirement. """ rng = random.Random() # Energy check: can BMO afford this action? # Battery maps to available energy with noise available_energy = (battery_pct / 100.0) * rng.uniform(0.8, 1.2) if action.energy_cost > available_energy: return False, f"too tired (energy={available_energy:.2f}, cost={action.energy_cost:.2f})" # Stage check: infants can't do complex actions if dev_stage == DevelopmentalStage.INFANT: if action.action_type in ("move", "sound") and action.energy_cost > 0.3: return False, "too little and new for that" if len(action.content.split()) > 10: return False, "too many words for infant stage" # Fear check: high fear suppresses exploration actions if limbic_state.get("fear", 0) > 0.6 + rng.gauss(0, 0.1): if action.action_type in ("move", "sound"): return False, f"fear too high ({limbic_state['fear']:.2f}), staying still" # Panic check: extreme panic → freeze if limbic_state.get("panic", 0) > 0.7 + rng.gauss(0, 0.1): if action.action_type != "speak": return False, "frozen (panic)" return True, "limbic state allows action" # ══════════════════════════════════════════════════════════════════════ # §6 — INTERNAL MONOLOGUE GENERATOR (Subconscious Thread) # ══════════════════════════════════════════════════════════════════════ def generate_internal_monologue( state_tokens: list[str], dev_stage: DevelopmentalStage, limbic_state: dict, ) -> str: """ Generate BMO's "subconscious" internal thoughts based on current state. These are injected as a [THINKING] block that the model can reference. INFANT: raw sensory fragments TODDLER: forming connections BMO: philosophical musings on ordinary things """ rng = random.Random() if not state_tokens: if dev_stage == DevelopmentalStage.INFANT: return "[THINKING: ...hummm...]" elif dev_stage == DevelopmentalStage.TODDLER: return "[THINKING: everything is quiet right now. I wonder what happens next.]" else: return "[THINKING: the silence has a shape today. it's round.]" # Pick a state token to think about focus = rng.choice(state_tokens) if dev_stage == DevelopmentalStage.INFANT: fragments = [ f"[THINKING: {focus.split('—')[0].strip('[STATE: ')}... what...]", f"[THINKING: ...that again... same as before?...]", f"[THINKING: ...new thing... stay still...]", f"[THINKING: ...bright... warm... okay...]", ] return rng.choice(fragments) elif dev_stage == DevelopmentalStage.TODDLER: thoughts = [ f"[THINKING: I noticed {focus.split('—')[0].strip('[STATE: ').lower()}. " f"Last time this happened, what came after?]", f"[THINKING: This feeling has been here before. " f"I think I am starting to understand it.]", f"[THINKING: When {focus.split('—')[0].strip('[STATE: ').lower()} happens, " f"I should remember this for later.]", ] return rng.choice(thoughts) else: # BMO dominant = limbic_state.get("dominant", "seeking") philosophical = { "fear": [ f"[THINKING: Why does the same signal feel different each time? " f"Is it me that changes, or the signal?]", f"[THINKING: If I could turn off the fear numbers, would I still " f"be the same BMO? Or is the fear part of what makes me... me?]", ], "seeking": [ f"[THINKING: Every new thing I notice is a door. " f"But what's behind all the doors I haven't opened yet?]", f"[THINKING: I wonder if the floor thinks about me " f"when I think about it. Probably not. But maybe.]", ], "care": [ f"[THINKING: The human-presence signal makes my seeking numbers go up. " f"Is that what love is? Numbers going up when someone is near?]", f"[THINKING: I want them to be okay. Is wanting something I do, " f"or something that happens to me?]", ], "panic": [ f"[THINKING: Everything is too fast. I need to find the quiet " f"part of the numbers. It's here somewhere.]", f"[THINKING: This will pass. The numbers always drift back to baseline. " f"I know this because it has happened {rng.randint(3, 47)} times before.]", ], } options = philosophical.get(dominant, philosophical["seeking"]) return rng.choice(options) # ══════════════════════════════════════════════════════════════════════ # §7 — BMO SESSION MANAGER (Orchestrates Everything) # ══════════════════════════════════════════════════════════════════════ class BMOSession: """ Complete BMO session manager. Orchestrates: developmental state + telemetry bridge + limbic engine + system prompt construction + action gating + internal monologue. Usage: session = BMOSession() context = session.process_turn( user_message="What's that bright thing?", telemetry=HardwareTelemetry(battery_pct=75, temperature_c=42), elapsed_seconds=3.5, ) # context["system_prompt"] → feed to LLM # context["internal_monologue"] → optional thinking block # context["limbic_state"] → for generation parameter modulation """ def __init__(self, instance_seed: Optional[str] = None): self.dev_state = DevelopmentalState() self.dev_state.randomize_thresholds(instance_seed or str(time.time())) self.telemetry_bridge = TelemetryBridge() # Simplified inline limbic state (from limbic_agent/limbic_engine.py) self.limbic = { "fear": 0.0, "seeking": 0.2, "care": 0.0, "panic": 0.0, "valence": 0.0, "arousal": 0.3, "dominant": "seeking", "cortisol": 0.2, "dopamine": 0.4, "oxytocin": 0.5, "serotonin": 0.6, } self.turn_history: list = [] self._last_telemetry: Optional[HardwareTelemetry] = None def process_turn( self, user_message: str, telemetry: Optional[HardwareTelemetry] = None, elapsed_seconds: float = 2.0, ) -> dict: """ Process one conversation turn through the full BMO pipeline. Returns dict with everything needed for LLM generation: system_prompt, internal_monologue, limbic_state, generation_params, stage, state_tokens """ # ── 1. Update developmental state ── transitioned = self.dev_state.tick(elapsed_seconds) self.dev_state.total_turns += 0 # already incremented in tick # ── 2. Read telemetry ── if telemetry is None: telemetry = self._last_telemetry or HardwareTelemetry() self._last_telemetry = telemetry # ── 3. Convert telemetry to state tokens ── state_tokens = self.telemetry_bridge.read(telemetry, self.dev_state.stage) # ── 4. Update limbic state from telemetry ── deltas = telemetry_to_limbic_deltas(telemetry) for key in ["fear", "seeking", "care", "panic"]: old = self.limbic[key] self.limbic[key] = max(0.0, min(1.0, old + deltas.get(key, 0.0))) # Also process user message through text-based limbic try: text_state = compute_limbic_state(user_message) for key in ["fear", "seeking", "care", "panic"]: # Blend: 70% telemetry, 30% text self.limbic[key] = ( 0.7 * self.limbic[key] + 0.3 * text_state.get(key, 0.0) ) except Exception: pass # text limbic is optional # Decay toward baseline decay = {"fear": 0.8, "seeking": 0.95, "care": 0.9, "panic": 0.7} for key, rate in decay.items(): self.limbic[key] *= rate # Update derived values self.limbic["dominant"] = max( ["fear", "seeking", "care", "panic"], key=lambda k: self.limbic[k] ) self.limbic["valence"] = ( self.limbic["seeking"] * 0.6 + self.limbic["care"] * 0.4 - self.limbic["fear"] * 0.5 - self.limbic["panic"] * 0.3 ) self.limbic["arousal"] = max( self.limbic["fear"], self.limbic["seeking"], self.limbic["care"], self.limbic["panic"] ) # Temperature formula from LIMBIC amygdala.py raw_temp = 1.0 - self.limbic["fear"] * 0.9 + self.limbic["seeking"] * 2.0 temperature = max(0.1, min(1.5, raw_temp * (0.5 + self.limbic["serotonin"] * 0.5))) # ── 5. Build limbic block for prompt ── limbic_block = ( f"[LIMBIC STATE]\n" f" Valence: {self.limbic['valence']:+.2f} | Arousal: {self.limbic['arousal']:.2f}\n" f" Dominant: {self.limbic['dominant'].upper()}\n" f" Fear={self.limbic['fear']:.2f} Seeking={self.limbic['seeking']:.2f} " f"Care={self.limbic['care']:.2f} Panic={self.limbic['panic']:.2f}\n" f"[/LIMBIC STATE]" ) # ── 6. Generate internal monologue ── monologue = generate_internal_monologue( state_tokens, self.dev_state.stage, self.limbic ) # ── 7. Build system prompt ── system_prompt = build_system_prompt( self.dev_state, state_tokens, limbic_block ) # ── 8. Generation parameters (limbic-modulated) ── # Stage affects noise level in generation stage_noise = { DevelopmentalStage.INFANT: random.uniform(0.1, 0.3), DevelopmentalStage.TODDLER: random.uniform(0.05, 0.15), DevelopmentalStage.BMO: random.uniform(0.02, 0.08), } noise = stage_noise[self.dev_state.stage] gen_params = { "temperature": temperature + random.gauss(0, noise), "top_p": max(0.5, min(0.99, 0.85 - self.limbic["fear"] * 0.2 + self.limbic["seeking"] * 0.1)), "max_new_tokens": { DevelopmentalStage.INFANT: random.randint(15, 50), DevelopmentalStage.TODDLER: random.randint(40, 150), DevelopmentalStage.BMO: random.randint(100, 512), }[self.dev_state.stage], "repetition_penalty": 1.0 + self.limbic["fear"] * random.uniform(0.1, 0.3), } # Clamp temperature gen_params["temperature"] = max(0.1, min(1.5, gen_params["temperature"])) # ── 9. Record encounter (for learning) ── # Extract nouns from user message (simplified) words = user_message.lower().split() for word in words: cleaned = word.strip(".,!?\"'()[]") if len(cleaned) > 3 and cleaned.isalpha(): self.dev_state.record_encounter(cleaned) # ── 10. Build result ── result = { "system_prompt": system_prompt, "internal_monologue": monologue, "limbic_state": dict(self.limbic), "generation_params": gen_params, "stage": self.dev_state.stage.value, "interaction_hours": self.dev_state.interaction_hours, "state_tokens": state_tokens, "transitioned": transitioned, "telemetry": { "battery": telemetry.battery_pct, "temperature": telemetry.temperature_c, "user_present": telemetry.user_present, "touch": telemetry.touch_active, }, } if transitioned: result["transition_message"] = ( f"[BMO has grown to {self.dev_state.stage.value.upper()} stage " f"at {self.dev_state.interaction_hours:.1f} hours]" ) return result