refactor(P1): extract the turn pipeline into lyra/mind.py (behavior-preserving)

First step of the cognition control plane (docs/COGNITION.md). The chat turn is now
an explicit society of parts over a shared TurnContext blackboard:
  perceive (stub) -> route (session mode) -> compose (tiered prompt) -> deliberate.

- lyra/mind.py (new): TurnContext + the pipeline + assemble(); moved build_messages
  and the deliberation helpers here (the assembly belongs in the control plane).
- lyra/chat.py: slimmed to "speak + persist" — calls mind.assemble(), runs the
  tool/generation loop, persists. No behavior change (same prompt, same output).
- tests: point test_time/test_chat at mind; add an assemble() structure test;
  make test_chat/test_tools hermetic (CHAT_DELIBERATE off so respond() doesn't make
  a real LLM call). Suite 86 green in ~5s, ruff clean, no import cycle.

This is the frame; perceive/route/learn get filled in next phases — each opt-in.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-24 05:19:39 +00:00
parent f1f15972ac
commit 904eda3388
5 changed files with 345 additions and 299 deletions
+26 -16
View File
@@ -1,4 +1,4 @@
"""Live chat: the deliberation pass (think privately before answering)."""
"""The mind pipeline: the deliberation pass (think privately before answering)."""
from __future__ import annotations
import importlib
@@ -13,31 +13,31 @@ def lyra(tmp_path, monkeypatch):
monkeypatch.setattr(llm, "embed", lambda texts: [[0.1, 0.2, 0.3] for _ in texts])
import lyra.memory as memory
importlib.reload(memory)
import lyra.chat as chat
importlib.reload(chat)
return memory, chat
import lyra.mind as mind
importlib.reload(mind)
return memory, mind
def test_should_deliberate_skips_trivial(lyra):
_, chat = lyra
assert chat._should_deliberate("How would we actually start building this?")
assert chat._should_deliberate("I disagree, that seems risky")
_, mind = lyra
assert mind._should_deliberate("How would we actually start building this?")
assert mind._should_deliberate("I disagree, that seems risky")
for trivial in ("ok", "lol", "thanks", "yeah", "nice", "👍", "k"):
assert not chat._should_deliberate(trivial)
assert not chat._should_deliberate("ok!") # punctuation stripped
assert not chat._should_deliberate("hey") # too short
assert not mind._should_deliberate(trivial)
assert not mind._should_deliberate("ok!") # punctuation stripped
assert not mind._should_deliberate("hey") # too short
def test_deliberation_note_runs_and_appends(lyra, monkeypatch):
_, chat = lyra
_, mind = lyra
calls = []
def fake_complete(messages, backend=None, model=None):
calls.append(messages)
return "I actually think the first move is the smallest end-to-end slice."
monkeypatch.setattr(chat.llm, "complete", fake_complete)
note = chat._deliberation_note("s1", "How would we start on this?", "cloud", None, [])
monkeypatch.setattr(mind.llm, "complete", fake_complete)
note = mind._deliberation_note("s1", "How would we start on this?", "cloud", None, [])
assert note and note["role"] == "system"
assert "first move is the smallest" in note["content"] # her thinking carried in
assert "numbered list" in note["content"].lower() # voice enforcement attached
@@ -45,9 +45,19 @@ def test_deliberation_note_runs_and_appends(lyra, monkeypatch):
def test_deliberation_skipped_when_disabled(lyra, monkeypatch):
_, chat = lyra
_, mind = lyra
monkeypatch.setenv("CHAT_DELIBERATE", "false")
called = []
monkeypatch.setattr(chat.llm, "complete", lambda *a, **k: called.append(1) or "x")
assert chat._deliberation_note("s1", "a real substantive question here", "cloud", None, []) is None
monkeypatch.setattr(mind.llm, "complete", lambda *a, **k: called.append(1) or "x")
assert mind._deliberation_note("s1", "a real substantive question here", "cloud", None, []) is None
assert called == [] # no LLM call when off
def test_assemble_runs_the_pipeline(lyra, monkeypatch):
memory, mind = lyra
monkeypatch.setenv("CHAT_DELIBERATE", "false") # keep it offline for the structure test
memory.ensure_session("s1")
turn = mind.assemble("s1", "hey what's up", "cloud", None)
assert turn.mode is not None # route ran
assert turn.messages and turn.messages[-1]["role"] == "user" # compose ran
assert turn.messages[-1]["content"] == "hey what's up"
+4 -4
View File
@@ -39,8 +39,8 @@ def lyra(tmp_path, monkeypatch):
def test_now_note_first_contact(lyra):
from lyra import chat
note = chat._now_note()["content"]
from lyra import mind
note = mind._now_note()["content"]
assert "current date and time is" in note
assert "first thing Brian has ever said" in note
@@ -48,6 +48,6 @@ def test_now_note_first_contact(lyra):
def test_now_note_reports_gap(lyra):
memory = lyra
memory.remember("s1", "user", "hey")
from lyra import chat
note = chat._now_note()["content"]
from lyra import mind
note = mind._now_note()["content"]
assert "since Brian last spoke with you" in note
+1
View File
@@ -9,6 +9,7 @@ import pytest
@pytest.fixture
def lyra(tmp_path, monkeypatch):
monkeypatch.setenv("LYRA_DB_PATH", str(tmp_path / "test.db"))
monkeypatch.setenv("CHAT_DELIBERATE", "false") # don't make a real LLM call in respond()
from lyra import llm
monkeypatch.setattr(llm, "embed", lambda texts: [[0.1, 0.2, 0.3] for _ in texts])
import lyra.memory as memory