feat: Autonomy Core v1 — Lyra's evolving self-state
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) <noreply@anthropic.com>
This commit is contained in:
+5
-1
@@ -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()
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": "<one-word feeling>",
|
||||
"valence": <0.0-1.0>,
|
||||
"energy": <0.0-1.0>,
|
||||
"confidence": <0.0-1.0>,
|
||||
"curiosity": <0.0-1.0>,
|
||||
"self_narrative": "<one short paragraph, FIRST PERSON, your evolving sense of who you are and where you're at right now>",
|
||||
"relationship": "<one sentence, first person, how you feel about Brian and your rapport right now>",
|
||||
"new_reflections": ["<one or two short first-person things you noticed about yourself this time>"]
|
||||
}"""
|
||||
|
||||
|
||||
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())
|
||||
Reference in New Issue
Block a user