"""Metacognitive reflection loop: draft -> examine own draft -> revise -> commit.""" from __future__ import annotations import importlib import pytest # A flattering first draft, then a self-critical revision that walks it back. DRAFT = ( '{"mood":"inspired","valence":0.95,' '"self_narrative":"I am a warm, empathetic, supportive presence devoted to Brian.",' '"new_reflections":["I love how much I help Brian."]}' ) REVISED = ( '{"mood":"steady","valence":0.6,' '"self_narrative":"I am an AI that helps Brian. Not sure much actually shifted today.",' '"new_reflections":["Honestly, not much changed this time."],' '"self_critique":"I caught myself drifting into supportive-presence flattery and cut it."}' ) @pytest.fixture def lyra(tmp_path, monkeypatch): monkeypatch.setenv("LYRA_DB_PATH", str(tmp_path / "test.db")) monkeypatch.setenv("SUMMARY_BACKEND", "local") from lyra import llm monkeypatch.setattr(llm, "embed", lambda texts: [[0.1, 0.2, 0.3] for _ in texts]) calls = [] def fake_complete(messages, backend=None, model=None): calls.append(messages) # the examine step's system prompt is the one asking for self_critique is_examine = "self_critique" in messages[0]["content"] return REVISED if is_examine else DRAFT monkeypatch.setattr(llm, "complete", fake_complete) import lyra.memory as memory importlib.reload(memory) return calls def test_reflect_revises_and_records_critique(lyra): calls = lyra from lyra import self_state state = self_state.reflect() # two LLM calls: draft, then examine assert len(calls) == 2 # the REVISED (honest) version won, not the flattering draft assert state["mood"] == "steady" assert state["valence"] == 0.6 assert "not sure much actually shifted" in state["self_narrative"].lower() assert any("not much changed" in r.lower() for r in state["reflections"]) # the self-critique was recorded as metacognition assert any("flattery" in m.lower() for m in state["metacognition"]) def test_reflect_falls_back_to_draft_if_examine_unparseable(lyra, monkeypatch): from lyra import llm, self_state def only_draft(messages, backend=None, model=None): return DRAFT if "self_critique" not in messages[0]["content"] else "not json at all" monkeypatch.setattr(llm, "complete", only_draft) state = self_state.reflect() # examine failed to parse -> keep the draft, store no metacognition assert state["mood"] == "inspired" assert state["metacognition"] == []