refactor(memory): unify structured consolidation flow

This commit is contained in:
chengyongru
2026-08-19 18:40:20 +08:00
committed by chengyongru
parent d4de0e4e3d
commit 16be6b0f36
14 changed files with 257 additions and 340 deletions
+31 -32
View File
@@ -7,7 +7,6 @@ import pytest
from nanobot.agent.autocompact import AutoCompact
from nanobot.session.manager import Session, SessionManager
from nanobot.session.summary import SessionSummary
def _runtime(_session: Session | None = None):
@@ -176,24 +175,6 @@ class TestIsExpired:
assert ac._is_expired(expired, now=now) is True
# ---------------------------------------------------------------------------
# SessionSummary
# ---------------------------------------------------------------------------
class TestSessionSummary:
"""Test prompt rendering for the structured summary value."""
def test_formats_prompt(self):
last_active = datetime(2026, 5, 13, 14, 30, 0)
summary = SessionSummary("User discussed Python.", last_active)
assert summary.for_prompt() == (
"Previous conversation summary (last active 2026-05-13T14:30:00):\n"
"User discussed Python."
)
# ---------------------------------------------------------------------------
# check_expired
# ---------------------------------------------------------------------------
@@ -490,7 +471,7 @@ class TestArchiveDelegates:
entry = ac._summaries.get("cli:test")
assert entry is not None
assert entry.text == "Hello."
assert entry["text"] == "Hello."
@pytest.mark.asyncio
async def test_no_summary_when_compact_returns_empty(self):
@@ -569,21 +550,29 @@ class TestPrepareSession:
ac = _make_autocompact()
session = _make_session()
last_active = datetime(2026, 5, 13, 14, 0, 0)
ac._summaries["cli:test"] = SessionSummary("Hot summary.", last_active)
ac._summaries["cli:test"] = {
"text": "Hot summary.",
"last_active": last_active.isoformat(),
}
result_session, summary = ac.prepare_session(session, "cli:test")
assert result_session is session
assert summary is not None
assert summary.text == "Hot summary."
assert "Previous conversation summary" in summary.for_prompt()
assert summary == {
"text": "Hot summary.",
"last_active": last_active.isoformat(),
}
def test_hot_path_pops_summary_one_shot(self):
"""Hot path should pop the summary (one-shot; second call returns None)."""
ac = _make_autocompact()
session = _make_session()
last_active = datetime(2026, 1, 1)
ac._summaries["cli:test"] = SessionSummary("One-shot.", last_active)
ac._summaries["cli:test"] = {
"text": "One-shot.",
"last_active": last_active.isoformat(),
}
_, summary1 = ac.prepare_session(session, "cli:test")
assert summary1 is not None
@@ -606,7 +595,7 @@ class TestPrepareSession:
assert result_session is session
assert summary is not None
assert summary.text == "Cold summary."
assert summary["text"] == "Cold summary."
def test_cold_path_tolerates_malformed_last_active(self):
"""A malformed persisted last_active must not raise on the turn path.
@@ -629,8 +618,10 @@ class TestPrepareSession:
assert result_session is session
assert summary is not None
assert summary.text == "Cold summary."
assert summary.last_active == fallback
assert summary == {
"text": "Cold summary.",
"last_active": fallback.isoformat(),
}
def test_cold_path_tolerates_missing_last_active(self):
"""A _last_summary dict without last_active must not raise."""
@@ -645,8 +636,10 @@ class TestPrepareSession:
assert result_session is session
assert summary is not None
assert summary.text == "Cold summary."
assert summary.last_active == fallback
assert summary == {
"text": "Cold summary.",
"last_active": fallback.isoformat(),
}
def test_cold_path_missing_text_returns_none(self):
"""A _last_summary without a non-empty string text yields no summary."""
@@ -677,7 +670,10 @@ class TestPrepareSession:
ac.sessions = mock_sm
key = "dream:20260602-155256"
ac._archiving.add(key)
ac._summaries[key] = SessionSummary("Hot summary.", datetime(2026, 6, 2, 15, 52, 56))
ac._summaries[key] = {
"text": "Hot summary.",
"last_active": "2026-06-02T15:52:56",
}
session = _make_session(
key=key,
updated_at=datetime.now() - timedelta(minutes=20),
@@ -717,9 +713,12 @@ class TestPrepareSession:
},
})
last_active = datetime(2026, 5, 13, 14, 0, 0)
ac._summaries["cli:test"] = SessionSummary("Hot summary.", last_active)
ac._summaries["cli:test"] = {
"text": "Hot summary.",
"last_active": last_active.isoformat(),
}
_, summary = ac.prepare_session(session, "cli:test")
assert summary is not None
assert summary.text == "Hot summary."
assert summary["text"] == "Hot summary."
# After hot path pops, cold path would kick in on next call