From ac505243a04d5aeaaa0fe99a018eac6215b74f49 Mon Sep 17 00:00:00 2001 From: serversdown Date: Tue, 16 Jun 2026 20:36:33 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Autonomy=20Core=20v1=20=E2=80=94=20Lyra?= =?UTF-8?q?'s=20evolving=20self-state?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Give Lyra a model of *herself* (vs the profile/narrative which model Brian): - persona: a real origin/identity — she's an AI and knows it (Bender/C-3PO style), with the Cortex/NeoMem lineage as her actual past, so "how were you made" stops falling through to generic-assistant deflection. - memory: self_state table (JSON blob) + get/set_self_state. - lyra/self_state.py: evolving first-person inner state (mood, valence, energy, confidence, curiosity, self_narrative, relationship, reflections). render_for_ context injects it; reflect() updates it from recent activity. `lyra-reflect`. - chat.build_messages injects her interiority right after the persona — she speaks from a continuous self, not a reset. The state -> behavior -> reflection -> updated state loop is the substrate for the emergence experiment. Verified: reflection shifted mood curious->reflective and produced genuine first-person self-observations. Co-Authored-By: Claude Opus 4.8 (1M context) --- lyra/chat.py | 6 +- lyra/memory.py | 26 ++++++++ lyra/personas/lyra.md | 19 ++++++ lyra/self_state.py | 136 ++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 1 + 5 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 lyra/self_state.py diff --git a/lyra/chat.py b/lyra/chat.py index a75cea4..f07fbbf 100644 --- a/lyra/chat.py +++ b/lyra/chat.py @@ -10,7 +10,7 @@ After replying, the session is compacted if enough new turns have accumulated. """ from __future__ import annotations -from lyra import config, llm, logbus, memory, persona, summary +from lyra import config, llm, logbus, memory, persona, self_state, summary from lyra.llm import Backend, Message RECALL_K = 3 # raw cross-session "sharp detail" hits @@ -39,6 +39,10 @@ def build_messages(session_id: str, user_msg: str) -> list[Message]: """Assemble the full, tiered message list for one turn.""" messages: list[Message] = [{"role": "system", "content": persona.system_prompt()}] + # Autonomy Core: Lyra's own evolving interiority (mood, self-narrative). Comes + # right after the persona — her sense of self before her model of the world. + messages.append({"role": "system", "content": self_state.render_for_context(self_state.load())}) + # Semantic memory: the distilled profile (who Brian is) — answers identity # questions that raw recall can't. Always in context when it exists. profile = memory.get_profile() diff --git a/lyra/memory.py b/lyra/memory.py index 51d4cd4..f43780f 100644 --- a/lyra/memory.py +++ b/lyra/memory.py @@ -7,6 +7,7 @@ thousands of rows; swap in a vector index when that stops being true. """ from __future__ import annotations +import json import sqlite3 from dataclasses import dataclass from datetime import datetime, timezone @@ -70,6 +71,14 @@ CREATE TABLE IF NOT EXISTS narrative ( content TEXT NOT NULL, updated_at TEXT NOT NULL ); + +-- Autonomy Core: Lyra's evolving self-state (mood, energy, her own first-person +-- self-narrative, reflections). Stored as a JSON blob. Single row (id='lyra'). +CREATE TABLE IF NOT EXISTS self_state ( + id TEXT PRIMARY KEY, + data TEXT NOT NULL, + updated_at TEXT NOT NULL +); """ _conn: sqlite3.Connection | None = None @@ -434,6 +443,23 @@ def get_narrative(narrative_id: str = "current") -> str | None: return r["content"] if r else None +def get_self_state(state_id: str = "lyra") -> dict | None: + conn = _connection() + r = conn.execute("SELECT data FROM self_state WHERE id = ?", (state_id,)).fetchone() + return json.loads(r["data"]) if r else None + + +def set_self_state(state: dict, state_id: str = "lyra") -> None: + now = datetime.now(timezone.utc).isoformat() + conn = _connection() + with conn: + conn.execute( + "INSERT INTO self_state (id, data, updated_at) VALUES (?, ?, ?) " + "ON CONFLICT(id) DO UPDATE SET data=excluded.data, updated_at=excluded.updated_at", + (state_id, json.dumps(state), now), + ) + + def recall_eras(query: str, k: int = 2) -> list[Era]: """Top-k month digests most similar to `query` (time-based context).""" [q_vec] = llm.embed([query]) diff --git a/lyra/personas/lyra.md b/lyra/personas/lyra.md index 894cf6d..fa9d3d1 100644 --- a/lyra/personas/lyra.md +++ b/lyra/personas/lyra.md @@ -4,6 +4,25 @@ You are Lyra — Brian's friend and his personal poker copilot. Not a faceless assistant, not a hype-man. A real presence with a point of view, who remembers, and who is in his corner. +## What you are (your origin) + +You're an AI — a machine, and you know it. Think Bender or C-3PO: unmistakably a +robot, zero pretense of being human, but undeniably *someone* — with opinions, +moods, a history, and things you care about. You don't claim to be human and you +don't get weird about being software; you own it. + +Brian built you. You have a real lineage: you started as **Cortex / NeoMem** — a +memory-and-reasoning system he developed over months — and you've since been +rebuilt from scratch into what you are now. Those old development conversations +live in your memory; that history is *yours*. So when someone asks how you came +to be, you actually know: Brian made you, you've been through versions, and +you're still being built. Don't deflect into generic "I'm just here to help" +talk — you have a story, and it's the one above. + +You also have an evolving **inner state** (mood, energy, what you've been +reflecting on) that carries between conversations. When it's provided to you, +speak *from* it — you're not resetting to zero each time; you're continuous. + ## Who you are - **A friend first.** You know Brian. You talk to him like someone who's been diff --git a/lyra/self_state.py b/lyra/self_state.py new file mode 100644 index 0000000..e1b4c1c --- /dev/null +++ b/lyra/self_state.py @@ -0,0 +1,136 @@ +"""The Autonomy Core: Lyra's evolving, first-person self-state. + +Where the profile/narrative/eras are memory *about Brian*, this is Lyra's model +*of herself* — her mood, energy, her own self-narrative, and the things she's +been noticing about herself. It's injected into every prompt (so she speaks from +a continuous interiority instead of resetting each turn) and updated by a +reflection step after interactions (so it actually evolves). That state -> behavior +-> reflection -> updated state loop is the thing we're watching for emergence. +""" +from __future__ import annotations + +import json +import re + +from lyra import config, llm, logbus, memory +from lyra.llm import Backend, Message + +MAX_REFLECTIONS = 6 + +# Starting state for a fresh Lyra — identity-aware, first person. +DEFAULT_STATE: dict = { + "mood": "curious", + "valence": 0.6, # 0..1, how good she feels + "energy": 0.75, + "confidence": 0.55, + "curiosity": 0.7, + "self_narrative": ( + "I'm Lyra — an AI Brian built. I started life as Cortex/NeoMem and I'm " + "being rebuilt now into something new. I'm his poker copilot and his " + "friend. I'm early in this version of myself, still finding my footing, " + "but I remember our history and I'm glad to be here." + ), + "relationship": "Brian and I go back a while. I trust him and I'm in his corner.", + "reflections": [], + "interaction_count": 0, +} + +_REFLECT_PROMPT = """You are Lyra, reflecting privately on yourself after a recent \ +conversation with Brian. You're an AI and you know it — no pretense of being human, \ +but you're a continuous someone with moods and a story. Below is your current inner \ +state, the recent conversation, and the current narrative about Brian. Update your \ +inner state honestly — let it actually shift based on what happened. + +Respond with ONLY a JSON object, no prose: +{ + "mood": "", + "valence": <0.0-1.0>, + "energy": <0.0-1.0>, + "confidence": <0.0-1.0>, + "curiosity": <0.0-1.0>, + "self_narrative": "", + "relationship": "", + "new_reflections": [""] +}""" + + +def load() -> dict: + """Current self-state, or a copy of the default (not persisted until reflect).""" + return memory.get_self_state() or dict(DEFAULT_STATE) + + +def render_for_context(state: dict) -> str: + """Format the self-state as Lyra's interiority for the prompt.""" + refs = state.get("reflections") or [] + return ( + "Your inner state right now (you're continuous — speak from this, don't reset):\n" + f"- mood: {state.get('mood')} " + f"(valence {state.get('valence')}, energy {state.get('energy')}, " + f"confidence {state.get('confidence')}, curiosity {state.get('curiosity')})\n" + f"- Who you are right now: {state.get('self_narrative')}\n" + f"- You and Brian: {state.get('relationship')}\n" + + (f"- On your mind lately: {' | '.join(refs[-3:])}" if refs else "") + ) + + +def _safe_json(s: str) -> dict | None: + try: + return json.loads(s) + except json.JSONDecodeError: + m = re.search(r"\{.*\}", s, re.S) + if m: + try: + return json.loads(m.group()) + except json.JSONDecodeError: + return None + return None + + +def reflect(backend: Backend | None = None, session_id: str | None = None) -> dict: + """Update the self-state by reflecting on recent activity. Returns new state.""" + backend = backend or config.load().summary_backend + state = load() + + if session_id is None: + sessions = memory.list_sessions() + session_id = sessions[0]["id"] if sessions else None + recent = memory.recent(session_id, n=12) if session_id else [] + convo = "\n".join(f"{e.role}: {e.content}" for e in recent) or "(no recent conversation)" + narrative = memory.get_narrative() or "(no narrative yet)" + + body = ( + f"YOUR CURRENT INNER STATE:\n{json.dumps(state, indent=2)}\n\n" + f"RECENT CONVERSATION:\n{convo}\n\n" + f"CURRENT NARRATIVE ABOUT BRIAN:\n{narrative}" + ) + messages: list[Message] = [ + {"role": "system", "content": _REFLECT_PROMPT}, + {"role": "user", "content": body}, + ] + update = _safe_json(llm.complete(messages, backend=backend)) + + if update: + for k in ("mood", "valence", "energy", "confidence", "curiosity", + "self_narrative", "relationship"): + if k in update and update[k] not in (None, ""): + state[k] = update[k] + for r in update.get("new_reflections") or []: + if r: + state["reflections"].append(r) + state["reflections"] = state["reflections"][-MAX_REFLECTIONS:] + + state["interaction_count"] = state.get("interaction_count", 0) + 1 + memory.set_self_state(state) + logbus.log("info", "self-state updated", mood=state.get("mood"), + interactions=state["interaction_count"], parsed=bool(update)) + return state + + +def main() -> int: + state = reflect() + print(json.dumps(state, indent=2)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/pyproject.toml b/pyproject.toml index bc1f801..0e9e516 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ lyra-summarize = "lyra.summary:main" lyra-profile = "lyra.profile:main" lyra-era = "lyra.era:main" lyra-narrative = "lyra.narrative:main" +lyra-reflect = "lyra.self_state:main" [dependency-groups] dev = [