feat: make the two-step reflection observable (draft -> revised -> critique)
You couldn't see her actually correct herself — /self showed only the result. Now: - reflect() logs the draft, the revised/committed version, and the self-critique to the live log as an expandable "view details" block - POST /self/reflect runs a reflection in the web process so it lands in /logs live (reflections normally run in the dream process, whose logs only go to journald); "↻ Reflect now" button on /self triggers it, with a logs ↗ link - log viewers relabel the expander "view full prompt" -> "view details" (it now carries prompts and reflection diffs) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+27
-4
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -756,7 +756,7 @@
|
||||
<span class="log-level log-level-${level}">${escapeHtml(level)}</span>
|
||||
<span class="log-msg">${escapeHtml(event.msg || '')}</span>
|
||||
${fieldStr ? `<span class="log-fields">${escapeHtml(fieldStr)}</span>` : ''}
|
||||
${detail ? `<details class="log-detail"><summary>view full prompt</summary><pre>${escapeHtml(detail)}</pre></details>` : ''}
|
||||
${detail ? `<details class="log-detail"><summary>view details</summary><pre>${escapeHtml(detail)}</pre></details>` : ''}
|
||||
`;
|
||||
|
||||
thinkingContent.appendChild(eventDiv);
|
||||
|
||||
@@ -210,7 +210,7 @@
|
||||
`<span class="msg">${esc(ev.msg || '')}</span>` +
|
||||
`</div>` +
|
||||
(fieldStr ? `<div class="fields">${esc(fieldStr)}</div>` : '') +
|
||||
(detail ? `<details class="detail"><summary>view full prompt</summary><pre>${esc(detail)}</pre></details>` : '');
|
||||
(detail ? `<details class="detail"><summary>view details</summary><pre>${esc(detail)}</pre></details>` : '');
|
||||
|
||||
if (!matches(line)) line.classList.add('hidden');
|
||||
logEl.appendChild(line);
|
||||
|
||||
@@ -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 @@
|
||||
<span class="dot" id="dot"></span>
|
||||
<h1>🧠 Lyra · Mind</h1>
|
||||
<a class="back" href="/">← Chat</a>
|
||||
<a class="back" href="/logs" target="_blank" rel="noopener" title="Watch the live log">logs ↗</a>
|
||||
<button id="reflectBtn" title="Make her reflect now (draft → self-critique → revise). Watch it in /logs.">↻ Reflect now</button>
|
||||
<span class="updated" id="updated">—</span>
|
||||
</div>
|
||||
</header>
|
||||
@@ -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(); });
|
||||
|
||||
Reference in New Issue
Block a user