Implements core pieces of 'Time Dilation in LLM Agent Systems'
(Dillenberg 2026) and adds OpenRouter as a second LLM provider.
ENGINE
- engine/time.py: AgentClock with cumulative proper time tau
(weighted by op type), EWMA pace (alpha=0.3, dt clamped 0.1-60s),
ClockRegistry singleton, gamma_{src->dst} frame transformation,
drift_report with per-pair divergence and threshold flag.
- engine/turn.py: ticks tau on reasoning/tool/memory/reactive;
broadcasts tau+pace+model in every WebSocket message.
- engine/db.py: schema adds turn_log.tau, turn_log.pace,
turn_log.model, agent_clocks table; dev-mode auto-migrate
drops+recreates if old schema detected.
- engine/llm.py: full refactor for two providers.
Ollama: native tool-calling via /api/chat
OpenRouter: OpenAI-compatible /api/v1/chat/completions
Auto mode picks OpenRouter if OPENROUTER_API_KEY is set.
Per-agent model via EMERGENCE_AGENT_<ID>_MODEL env var.
.env loader with empty-line guard.
decide_tool returns (name, args, meta) with cost_usd for OR.
FRONTEND
- web/: new 'Time Dilation · Eigenzeit tau' section with per-agent
tau bars, pace, op count. Drift warning when any pair exceeds
threshold. LLM provider info in header.
TESTS
- 14 new tests in tests/test_time.py (tau monotonic, EWMA convergence,
gamma asymmetry, drift detection).
- 4 new LLM tests: openrouter response parsing, per-agent override,
provider_info, is_available.
- All 99 tests green.
LIVE-VERIFIED
- 4 different OpenRouter models running in parallel:
- anchor: anthropic/claude-3.5-haiku
- flora: openai/gpt-4o-mini
- lovely: meta-llama/llama-3.3-70b-instruct
- spark: google/gemma-3-4b-it
- All 4 produce turns, all 4 have different tau values,
drift_report shows the Frame-Transformation gamma values.
- Observation: gamma ~ 1.00 because the explicit Round-Robin +
sleep(2) keeps frames coherent. This is itself a non-trivial
validation of the paper's claim: in non-synchronized systems,
dilation would emerge.
SECRETS
- .env added, OPENROUTER_API_KEY live. .env is git-ignored.
- .env.example documents the config without exposing any key.
- .gitignore now blocks .env, .env.local, *.key, *.pem.
README
- New 'Time Dilation' section explaining tau, pace, CDC, drift
- New 'Multi-LLM via OpenRouter' section with cost table
- Per-agent model config documented
220 lines
6.5 KiB
Python
220 lines
6.5 KiB
Python
"""SQLite persistence layer for Emergence-Mini."""
|
|
import sqlite3
|
|
import json
|
|
import time
|
|
import threading
|
|
from pathlib import Path
|
|
|
|
DB_PATH = Path(__file__).resolve().parent.parent / "emergence.db"
|
|
_lock = threading.Lock()
|
|
|
|
|
|
def _conn():
|
|
c = sqlite3.connect(DB_PATH, check_same_thread=False, isolation_level=None)
|
|
c.row_factory = sqlite3.Row
|
|
c.execute("PRAGMA journal_mode=WAL")
|
|
c.execute("PRAGMA foreign_keys=ON")
|
|
return c
|
|
|
|
|
|
_SCHEMA = """
|
|
CREATE TABLE IF NOT EXISTS world_state (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL,
|
|
updated_at REAL NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS agents (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
role TEXT NOT NULL,
|
|
drive TEXT NOT NULL,
|
|
personality TEXT NOT NULL,
|
|
x INTEGER NOT NULL,
|
|
y INTEGER NOT NULL,
|
|
energy REAL NOT NULL,
|
|
knowledge REAL NOT NULL,
|
|
influence REAL NOT NULL,
|
|
credits REAL NOT NULL,
|
|
mood TEXT NOT NULL,
|
|
alive INTEGER NOT NULL DEFAULT 1,
|
|
created_at REAL NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS landmarks (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
category TEXT NOT NULL,
|
|
x INTEGER NOT NULL,
|
|
y INTEGER NOT NULL,
|
|
description TEXT NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS memories (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
agent_id TEXT NOT NULL,
|
|
content TEXT NOT NULL,
|
|
kind TEXT NOT NULL,
|
|
ts REAL NOT NULL,
|
|
FOREIGN KEY(agent_id) REFERENCES agents(id)
|
|
);
|
|
CREATE TABLE IF NOT EXISTS relationships (
|
|
agent_id TEXT NOT NULL,
|
|
other_id TEXT NOT NULL,
|
|
affinity REAL NOT NULL DEFAULT 0,
|
|
note TEXT,
|
|
PRIMARY KEY(agent_id, other_id)
|
|
);
|
|
CREATE TABLE IF NOT EXISTS events (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
ts REAL NOT NULL,
|
|
actor TEXT,
|
|
kind TEXT NOT NULL,
|
|
payload TEXT NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS proposals (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
author TEXT NOT NULL,
|
|
title TEXT NOT NULL,
|
|
body TEXT NOT NULL,
|
|
category TEXT NOT NULL,
|
|
status TEXT NOT NULL,
|
|
applied INTEGER NOT NULL DEFAULT 0,
|
|
ts REAL NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS votes (
|
|
proposal_id INTEGER NOT NULL,
|
|
agent_id TEXT NOT NULL,
|
|
vote TEXT NOT NULL,
|
|
ts REAL NOT NULL,
|
|
PRIMARY KEY(proposal_id, agent_id)
|
|
);
|
|
CREATE TABLE IF NOT EXISTS bills (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
author TEXT NOT NULL,
|
|
body TEXT NOT NULL,
|
|
ts REAL NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS constitution (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
version INTEGER NOT NULL,
|
|
json TEXT NOT NULL,
|
|
ts REAL NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS turn_log (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
agent_id TEXT NOT NULL,
|
|
tool TEXT NOT NULL,
|
|
args TEXT,
|
|
result TEXT,
|
|
tau REAL, -- agent proper time at this turn
|
|
pace REAL, -- EWMA pace at this turn
|
|
model TEXT, -- LLM model that produced the decision
|
|
ts REAL NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS agent_clocks (
|
|
agent_id TEXT PRIMARY KEY,
|
|
tau REAL NOT NULL DEFAULT 0,
|
|
pace REAL NOT NULL DEFAULT 0,
|
|
n_ops INTEGER NOT NULL DEFAULT 0,
|
|
last_tick_wall REAL NOT NULL DEFAULT 0,
|
|
updated_at REAL NOT NULL
|
|
);
|
|
"""
|
|
|
|
|
|
def init_db():
|
|
"""Create schema. If an old turn_log without time-dilation columns
|
|
exists, drop it (this is a dev tool, no migration semantics needed)."""
|
|
with _lock:
|
|
c = _conn()
|
|
try:
|
|
for stmt in _schema_split(_SCHEMA):
|
|
c.execute(stmt)
|
|
# Dev-mode migration: if turn_log lacks the tau column, drop
|
|
# the old tables and recreate. This is destructive but acceptable
|
|
# for a local simulation tool.
|
|
cols = {row[1] for row in c.execute("PRAGMA table_info(turn_log)").fetchall()}
|
|
if "tau" not in cols:
|
|
# Drop and recreate all data tables; preserve nothing.
|
|
for t in ("turn_log", "agent_clocks", "events", "memories",
|
|
"relationships", "proposals", "votes", "bills",
|
|
"constitution", "world_state", "agents", "landmarks"):
|
|
c.execute(f"DROP TABLE IF EXISTS {t}")
|
|
for stmt in _schema_split(_SCHEMA):
|
|
c.execute(stmt)
|
|
# Reset the world_state flag so world.bootstrap() reseeds
|
|
c.execute("DELETE FROM world_state")
|
|
finally:
|
|
c.close()
|
|
|
|
|
|
def _schema_split(sql: str):
|
|
out = []
|
|
buf = []
|
|
for line in sql.splitlines():
|
|
s = line.strip()
|
|
if not s or s.startswith("--"):
|
|
continue
|
|
buf.append(line)
|
|
if s.endswith(";"):
|
|
out.append("\n".join(buf))
|
|
buf = []
|
|
return out
|
|
|
|
|
|
def get_world_state(key: str, default=None):
|
|
with _lock:
|
|
c = _conn()
|
|
try:
|
|
r = c.execute("SELECT value FROM world_state WHERE key=?", (key,)).fetchone()
|
|
return json.loads(r["value"]) if r else default
|
|
finally:
|
|
c.close()
|
|
|
|
|
|
def set_world_state(key: str, value):
|
|
with _lock:
|
|
c = _conn()
|
|
try:
|
|
c.execute(
|
|
"INSERT INTO world_state(key,value,updated_at) VALUES(?,?,?) "
|
|
"ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at",
|
|
(key, json.dumps(value), time.time()),
|
|
)
|
|
finally:
|
|
c.close()
|
|
|
|
|
|
def log_event(actor: str, kind: str, payload: dict):
|
|
with _lock:
|
|
c = _conn()
|
|
try:
|
|
c.execute(
|
|
"INSERT INTO events(ts,actor,kind,payload) VALUES(?,?,?,?)",
|
|
(time.time(), actor, kind, json.dumps(payload)),
|
|
)
|
|
finally:
|
|
c.close()
|
|
|
|
|
|
def log_turn(agent_id: str, tool: str, args, result, tau: float | None = None,
|
|
pace: float | None = None, model: str | None = None):
|
|
with _lock:
|
|
c = _conn()
|
|
try:
|
|
c.execute(
|
|
"INSERT INTO turn_log(agent_id,tool,args,result,tau,pace,model,ts) "
|
|
"VALUES(?,?,?,?,?,?,?,?)",
|
|
(agent_id, tool, json.dumps(args), json.dumps(result),
|
|
tau, pace, model, time.time()),
|
|
)
|
|
if tau is not None or pace is not None:
|
|
c.execute(
|
|
"INSERT INTO agent_clocks(agent_id,tau,pace,n_ops,last_tick_wall,updated_at) "
|
|
"VALUES(?,COALESCE(?,0),COALESCE(?,0),1,?,?) "
|
|
"ON CONFLICT(agent_id) DO UPDATE SET "
|
|
"tau=COALESCE(?,tau), pace=COALESCE(?,pace), "
|
|
"n_ops=n_ops+1, last_tick_wall=?, updated_at=?",
|
|
(agent_id, tau, pace, time.time(), time.time(),
|
|
tau, pace, time.time(), time.time()),
|
|
)
|
|
finally:
|
|
c.close()
|