Some checks failed
CI / validate (pull_request) Failing after 5s
Adds the perception adapter, experience store, trajectory logger, and
consciousness loop that give Timmy a body in the Nexus.
Architecture:
BIRTH.md — Thin system prompt. SOUL.md conscience + embodied
awareness. No meta-knowledge about implementation.
perception_adapter — Translates WS events to natural-language sensory
descriptions. Parses model output into WS actions.
experience_store — SQLite-backed lived-experience memory. The model
remembers only what it perceived through its channel.
trajectory_logger — Logs every perceive→think→act cycle as ShareGPT JSONL,
compatible with the AutoLoRA training pipeline.
nexus_think — The consciousness loop. Connects to WS gateway,
receives perceptions, thinks via Ollama, sends actions.
The 8B model wakes up knowing nothing but its values and what it
experiences. Training loops close on lived experience — emergence
through the channel, not through fine-tuning toward behaviors.
Run: python -m nexus.nexus_think --model timmy:v0.1-q4 --ws ws://localhost:8765
160 lines
5.4 KiB
Python
160 lines
5.4 KiB
Python
"""
|
|
Nexus Experience Store — Embodied Memory
|
|
|
|
SQLite-backed store for lived experiences only. The model remembers
|
|
what it perceived, what it thought, and what it did — nothing else.
|
|
|
|
Each row is one cycle of the perceive→think→act loop.
|
|
"""
|
|
|
|
import sqlite3
|
|
import json
|
|
import time
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
DEFAULT_DB = Path.home() / ".nexus" / "experience.db"
|
|
MAX_CONTEXT_EXPERIENCES = 20 # Recent experiences fed to the model
|
|
|
|
|
|
class ExperienceStore:
|
|
def __init__(self, db_path: Optional[Path] = None):
|
|
self.db_path = db_path or DEFAULT_DB
|
|
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
self.conn = sqlite3.connect(str(self.db_path))
|
|
self.conn.execute("PRAGMA journal_mode=WAL")
|
|
self.conn.execute("PRAGMA synchronous=NORMAL")
|
|
self._init_tables()
|
|
|
|
def _init_tables(self):
|
|
self.conn.executescript("""
|
|
CREATE TABLE IF NOT EXISTS experiences (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
timestamp REAL NOT NULL,
|
|
perception TEXT NOT NULL,
|
|
thought TEXT,
|
|
action TEXT,
|
|
action_result TEXT,
|
|
cycle_ms INTEGER DEFAULT 0,
|
|
session_id TEXT
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS summaries (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
timestamp REAL NOT NULL,
|
|
summary TEXT NOT NULL,
|
|
exp_start INTEGER NOT NULL,
|
|
exp_end INTEGER NOT NULL
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_exp_ts
|
|
ON experiences(timestamp DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_exp_session
|
|
ON experiences(session_id);
|
|
""")
|
|
self.conn.commit()
|
|
|
|
def record(
|
|
self,
|
|
perception: str,
|
|
thought: Optional[str] = None,
|
|
action: Optional[str] = None,
|
|
action_result: Optional[str] = None,
|
|
cycle_ms: int = 0,
|
|
session_id: Optional[str] = None,
|
|
) -> int:
|
|
"""Record one perceive→think→act cycle."""
|
|
cur = self.conn.execute(
|
|
"""INSERT INTO experiences
|
|
(timestamp, perception, thought, action, action_result,
|
|
cycle_ms, session_id)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)""",
|
|
(time.time(), perception, thought, action,
|
|
action_result, cycle_ms, session_id),
|
|
)
|
|
self.conn.commit()
|
|
return cur.lastrowid
|
|
|
|
def recent(self, limit: int = MAX_CONTEXT_EXPERIENCES) -> list[dict]:
|
|
"""Fetch the most recent experiences for context."""
|
|
rows = self.conn.execute(
|
|
"""SELECT id, timestamp, perception, thought, action,
|
|
action_result, cycle_ms
|
|
FROM experiences
|
|
ORDER BY timestamp DESC
|
|
LIMIT ?""",
|
|
(limit,),
|
|
).fetchall()
|
|
|
|
return [
|
|
{
|
|
"id": r[0],
|
|
"timestamp": r[1],
|
|
"perception": r[2],
|
|
"thought": r[3],
|
|
"action": r[4],
|
|
"action_result": r[5],
|
|
"cycle_ms": r[6],
|
|
}
|
|
for r in reversed(rows) # Chronological order
|
|
]
|
|
|
|
def format_for_context(self, limit: int = MAX_CONTEXT_EXPERIENCES) -> str:
|
|
"""Format recent experiences as natural language for the model."""
|
|
experiences = self.recent(limit)
|
|
if not experiences:
|
|
return "You have no memories yet. This is your first moment."
|
|
|
|
lines = []
|
|
for exp in experiences:
|
|
ago = time.time() - exp["timestamp"]
|
|
if ago < 60:
|
|
when = f"{int(ago)}s ago"
|
|
elif ago < 3600:
|
|
when = f"{int(ago / 60)}m ago"
|
|
else:
|
|
when = f"{int(ago / 3600)}h ago"
|
|
|
|
line = f"[{when}] You perceived: {exp['perception']}"
|
|
if exp["thought"]:
|
|
line += f"\n You thought: {exp['thought']}"
|
|
if exp["action"]:
|
|
line += f"\n You did: {exp['action']}"
|
|
if exp["action_result"]:
|
|
line += f"\n Result: {exp['action_result']}"
|
|
lines.append(line)
|
|
|
|
return "Your recent experiences:\n\n" + "\n\n".join(lines)
|
|
|
|
def count(self) -> int:
|
|
"""Total experiences recorded."""
|
|
return self.conn.execute(
|
|
"SELECT COUNT(*) FROM experiences"
|
|
).fetchone()[0]
|
|
|
|
def save_summary(self, summary: str, exp_start: int, exp_end: int):
|
|
"""Store a compressed summary of a range of experiences.
|
|
Used when context window fills — distill old memories."""
|
|
self.conn.execute(
|
|
"""INSERT INTO summaries (timestamp, summary, exp_start, exp_end)
|
|
VALUES (?, ?, ?, ?)""",
|
|
(time.time(), summary, exp_start, exp_end),
|
|
)
|
|
self.conn.commit()
|
|
|
|
def get_summaries(self, limit: int = 5) -> list[dict]:
|
|
"""Fetch recent experience summaries."""
|
|
rows = self.conn.execute(
|
|
"""SELECT id, timestamp, summary, exp_start, exp_end
|
|
FROM summaries ORDER BY timestamp DESC LIMIT ?""",
|
|
(limit,),
|
|
).fetchall()
|
|
return [
|
|
{"id": r[0], "timestamp": r[1], "summary": r[2],
|
|
"exp_start": r[3], "exp_end": r[4]}
|
|
for r in reversed(rows)
|
|
]
|
|
|
|
def close(self):
|
|
self.conn.close()
|