diff --git a/lyra/self_state.py b/lyra/self_state.py index 273c4ad..458350d 100644 --- a/lyra/self_state.py +++ b/lyra/self_state.py @@ -150,6 +150,20 @@ def _safe_json(s: str) -> dict | None: return None +def _fmt_reflection(label: str, d: dict | None) -> str: + """Readable block of a reflection's key fields, for the live-log inspector.""" + if not d: + return f"{label}:\n (none)" + keys = ("mood", "valence", "energy", "confidence", "curiosity", + "self_narrative", "relationship", "new_reflections") + lines = [f"{label}:"] + for k in keys: + if k in d and d[k] not in (None, "", []): + v = " | ".join(d[k]) if isinstance(d[k], list) else d[k] + lines.append(f" {k}: {v}") + return "\n".join(lines) + + def reflect(backend: Backend | None = None, session_id: str | None = None) -> dict: """Reflect on recent activity and update the self-state. Returns new state. @@ -189,7 +203,7 @@ def reflect(backend: Backend | None = None, session_id: str | None = None) -> di )) # Step 2 โ€” examine her own draft and revise it into a more honest version. - update, critique = draft, None + update, critique, revised = draft, None, None if draft: examine_body = body + "\n\nYOUR DRAFT REFLECTION:\n" + json.dumps(draft, indent=2) revised = _safe_json(llm.complete( @@ -217,9 +231,18 @@ def reflect(backend: Backend | None = None, session_id: str | None = None) -> di 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), - critiqued=bool(critique)) + + # Surface the actual self-correction (draft -> revised -> critique) to the live + # log as an expandable block, so the two-step reflection is observable. + detail = ( + _fmt_reflection("DRAFT (first pass)", draft) + "\n\n" + + _fmt_reflection("REVISED (committed)", + revised if revised else None) + + ("" if revised else "\n (examine step didn't parse โ€” kept the draft)") + + "\n\nSELF-CRITIQUE:\n " + (critique or "(none recorded this pass)") + ) + logbus.log("info", "reflection", mood=state.get("mood"), + critiqued=bool(critique), detail=detail) return state diff --git a/lyra/web/server.py b/lyra/web/server.py index bf79b3e..e39a837 100644 --- a/lyra/web/server.py +++ b/lyra/web/server.py @@ -125,6 +125,13 @@ def create_app() -> FastAPI: """Lyra's current interiority + when it last changed.""" return {"state": self_state.load(), "updated_at": memory.self_state_updated_at()} + @app.post("/self/reflect") + async def self_reflect() -> dict: + """Run one two-step reflection now, in this process, so the draft -> + revised -> critique lands in the live log (/logs).""" + state = await asyncio.to_thread(self_state.reflect) + return {"ok": True, "mood": state.get("mood")} + @app.get("/stream/logs") async def stream_logs(request: Request) -> StreamingResponse: """Live activity feed: replay the recent buffer, then stream new events.""" diff --git a/lyra/web/static/index.html b/lyra/web/static/index.html index b8cc3f9..a418060 100644 --- a/lyra/web/static/index.html +++ b/lyra/web/static/index.html @@ -756,7 +756,7 @@ ${escapeHtml(level)} ${escapeHtml(event.msg || '')} ${fieldStr ? `${escapeHtml(fieldStr)}` : ''} - ${detail ? `
view full prompt
${escapeHtml(detail)}
` : ''} + ${detail ? `
view details
${escapeHtml(detail)}
` : ''} `; thinkingContent.appendChild(eventDiv); diff --git a/lyra/web/static/logs.html b/lyra/web/static/logs.html index 2ac2623..3bf5347 100644 --- a/lyra/web/static/logs.html +++ b/lyra/web/static/logs.html @@ -210,7 +210,7 @@ `${esc(ev.msg || '')}` + `` + (fieldStr ? `
${esc(fieldStr)}
` : '') + - (detail ? `
view full prompt
${esc(detail)}
` : ''); + (detail ? `
view details
${esc(detail)}
` : ''); if (!matches(line)) line.classList.add('hidden'); logEl.appendChild(line); diff --git a/lyra/web/static/self.html b/lyra/web/static/self.html index d0dc34f..85befdb 100644 --- a/lyra/web/static/self.html +++ b/lyra/web/static/self.html @@ -25,6 +25,12 @@ .topbar h1 { font-size: 1.05rem; margin: 0; font-weight: 600; } .topbar a.back { color: var(--accent); text-decoration: none; font-size: .95rem; } .updated { margin-left: auto; color: var(--fade); font-size: .78rem; } + #reflectBtn { + background: #1b2333; border: 1px solid var(--border); color: var(--accent); + border-radius: 8px; padding: 6px 11px; font-size: .82rem; cursor: pointer; + -webkit-tap-highlight-color: transparent; + } + #reflectBtn:disabled { opacity: .5; cursor: default; } .dot { width: 9px; height: 9px; border-radius: 50%; background: var(--good); box-shadow: 0 0 8px var(--good); flex: none; opacity: .35; transition: opacity .2s; } .dot.pulse { opacity: 1; } @@ -64,6 +70,8 @@

๐Ÿง  Lyra ยท Mind

โ† Chat + logs โ†— + โ€” @@ -172,6 +180,16 @@ } } + const reflectBtn = document.getElementById('reflectBtn'); + reflectBtn.addEventListener('click', async () => { + reflectBtn.disabled = true; + const old = reflectBtn.textContent; + reflectBtn.textContent = 'โ€ฆ thinking'; + try { await fetch('/self/reflect', { method: 'POST' }); await refresh(); } + catch (e) { /* ignore */ } + finally { reflectBtn.disabled = false; reflectBtn.textContent = old; } + }); + refresh(); setInterval(refresh, 12000); document.addEventListener('visibilitychange', () => { if (!document.hidden) refresh(); });