daniel8919's picture
Add Project BMO: bmo_core.py
e2b15b1 verified
"""
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