diff --git a/server.py b/server.py index 3bf17c2..35dbd30 100644 --- a/server.py +++ b/server.py @@ -221,6 +221,80 @@ async def texts(limit: int = 20): return out[:limit] +@app.get("/api/history") +async def history(hours: float = 12.0): + """Return a chronological replay stream from the events table. + + Query params: + hours: how far back to load (default 12). + + Each frame contains: + ts, tick, kind, agents: {id: {x,y,energy,knowledge,influence, + credits,mood,tool,model,tau,pace,rationale}}, + clocks, drift. + Frames are capped at 2000 to keep the payload small. + """ + import sqlite3 + + since = time.time() - hours * 3600 + rows = _query( + "SELECT ts, kind, actor, payload FROM events WHERE ts > ? ORDER BY id ASC", + (since,), + ) + frames = [] + last_agents: dict[str, dict] = {} + last_tick: dict = {} + last_clocks: dict = {} + last_drift: dict = {} + for r in rows: + try: + p = json.loads(r["payload"]) + except Exception: + continue + if r["kind"] == "tick": + last_tick = {"tick": p.get("tick"), "ts": r["ts"]} + last_clocks = p.get("clocks", {}) + last_drift = p.get("drift", {}) + elif r["kind"] == "action": + a = p.get("agent") + if a: + last_agents[a] = { + "x": p.get("x"), + "y": p.get("y"), + "energy": p.get("energy"), + "knowledge": p.get("knowledge"), + "influence": p.get("influence"), + "credits": p.get("credits"), + "mood": p.get("mood"), + "tool": p.get("tool"), + "model": p.get("model"), + "tau": p.get("tau"), + "pace": p.get("pace"), + "rationale": p.get("rationale"), + "name": p.get("name", a), + } + frames.append({ + "ts": r["ts"], + "kind": r["kind"], + "actor": r["actor"], + "tick": last_tick.get("tick"), + "agents": dict(last_agents), + "clocks": dict(last_clocks), + "drift": dict(last_drift) if last_drift else None, + "event": p if r["kind"] == "action" else None, + }) + # Cap size + if len(frames) > 2000: + frames = frames[-2000:] + return { + "hours": hours, + "frames": frames, + "total": len(frames), + "since": since, + "grid": {"w": world.GRID_W, "h": world.GRID_H}, + } + + @app.post("/api/turn/{agent_id}") async def force_turn(agent_id: str, body: dict): tool_name = body.get("tool") diff --git a/web/app.js b/web/app.js index cb41cca..81a1c6b 100644 --- a/web/app.js +++ b/web/app.js @@ -4,6 +4,8 @@ const ctx = canvas.getContext('2d'); const GRID = 240; let snapshot = null; let actions = []; +let replay = { frames: [], idx: 0, playing: false, raf: null, lastFrame: 0 }; +let mode = 'live'; // 'live' or 'replay' const AGENT_COLORS = { anchor: '#ffd166', flora: '#6cf0c2', @@ -27,8 +29,39 @@ const LANDMARK_COLORS = { home_spark: '#3a3a3a', }; +function effectiveState() { + if (mode === 'replay' && replay.frames.length) { + const f = replay.frames[Math.min(replay.idx, replay.frames.length - 1)]; + const agents = []; + for (const id of Object.keys(f.agents)) { + const a = f.agents[id]; + agents.push({ + id, name: a.name || id, + x: a.x, y: a.y, + energy: a.energy ?? 0, + knowledge: a.knowledge ?? 0, + influence: a.influence ?? 0, + credits: a.credits ?? 0, + mood: a.mood || 'neutral', + model: a.model || '', + tool: a.tool || '', + }); + } + return { + agents, + landmarks: snapshot ? snapshot.landmarks : [], + clocks: f.clocks || {}, + drift: f.drift, + tick: f.tick, + _replayFrame: f, + }; + } + return snapshot; +} + function draw() { - if (!snapshot) return; + const state = effectiveState(); + if (!state) return; const W = canvas.width, H = canvas.height; ctx.fillStyle = '#060a0e'; ctx.fillRect(0, 0, W, H); @@ -43,7 +76,7 @@ function draw() { } // landmarks - for (const lm of snapshot.landmarks) { + for (const lm of state.landmarks) { const x = (lm.x / GRID) * W; const y = (lm.y / GRID) * H; const isHome = lm.id.startsWith('home_'); @@ -60,7 +93,7 @@ function draw() { } // agents - for (const a of snapshot.agents) { + for (const a of state.agents) { const x = (a.x / GRID) * W; const y = (a.y / GRID) * H; ctx.fillStyle = AGENT_COLORS[a.id] || '#fff'; @@ -71,13 +104,42 @@ function draw() { ctx.font = 'bold 10px ui-monospace'; ctx.textAlign = 'center'; ctx.fillText(a.name[0], x, y + 3); - // energy halo ctx.strokeStyle = '#ffd166'; ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(x, y, 8 + (100 - a.energy) * 0.06, 0, Math.PI * 2); ctx.stroke(); } + + if (mode === 'replay' && state._replayFrame) { + drawReplayOverlay(state._replayFrame); + } +} + +function drawReplayOverlay(f) { + const W = canvas.width, H = canvas.height; + ctx.fillStyle = 'rgba(6,10,14,0.75)'; + ctx.fillRect(8, H - 66, 220, 58); + ctx.strokeStyle = '#1c2733'; + ctx.lineWidth = 1; + ctx.strokeRect(8, H - 66, 220, 58); + ctx.fillStyle = '#6cf0c2'; + ctx.font = '11px ui-monospace'; + ctx.textAlign = 'left'; + const t = new Date(f.ts * 1000).toLocaleTimeString(); + ctx.fillText(`tick ${f.tick ?? '---'} | ${t}`, 14, H - 50); + if (f.kind === 'action' && f.event) { + const a = f.event.agent || f.actor; + const tool = f.event.tool || ''; + ctx.fillStyle = AGENT_COLORS[a] || '#d6e2ee'; + ctx.fillText(`${a} -> ${tool}`, 14, H - 34); + const model = shortModel(f.event.model || ''); + ctx.fillStyle = '#8aa1b6'; + ctx.fillText(model || 'fallback', 14, H - 18); + } else { + ctx.fillStyle = '#8aa1b6'; + ctx.fillText('tick update', 14, H - 34); + } } function shortModel(name) { @@ -205,8 +267,9 @@ function addFeed(entry) { } function refreshHeader() { - document.getElementById('tick').textContent = snapshot?.tick || 0; - document.getElementById('agentCount').textContent = snapshot?.agents.length || 0; + const state = effectiveState(); + document.getElementById('tick').textContent = state?.tick || 0; + document.getElementById('agentCount').textContent = state?.agents.length || 0; } async function refreshAll() { @@ -303,9 +366,152 @@ document.getElementById('manual').addEventListener('submit', async (e) => { console.log('manual turn result', out); }); +function refreshTexts() { + fetch('/api/texts').then(r => r.json()).then(d => { + document.getElementById('textsCount').textContent = `(${d.length})`; + const wrap = document.getElementById('texts'); + if (!d.length) { + wrap.innerHTML = 'Noch keine Texte. Agenten generieren welche ueber write_blog, add_to_billboard, speak_to_all, say_to_agent, add_to_longterm_memory.'; + return; + } + wrap.innerHTML = ''; + for (const t of d) { + const el = document.createElement('div'); + el.className = 'text-card'; + const isLocal = !((t.model || '').includes('/')); + const src = t.source === 'fallback' ? 'fallback' : 'llm'; + el.innerHTML = ` +