mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-29 06:31:32 +00:00
Snapshot review_agent._session_messages before teardown so close() can
clean per-session state without dropping the user-visible
self-improvement summary. Adds two regressions:
- bg-review summarizer receives captured review-agent tool messages
after review_agent.close() runs
- context-compressor protected-head handoff rehydration populates
_previous_summary and keeps the old handoff out of newly summarized
turns
Salvaged from PR #26039 onto current main after agent/background_review.py
extraction. Original commit 63eaf6055; bg-review test updated to patch
the module-level summarize_background_review_actions in
agent.background_review instead of the now-forwarder
AIAgent._summarize_background_review_actions.
87 lines
3.5 KiB
Python
87 lines
3.5 KiB
Python
"""Regression tests for iterative context-summary continuity."""
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from agent.context_compressor import ContextCompressor, SUMMARY_PREFIX
|
|
|
|
|
|
def _compressor() -> ContextCompressor:
|
|
with patch("agent.context_compressor.get_model_context_length", return_value=100000):
|
|
return ContextCompressor(
|
|
model="test/model",
|
|
threshold_percent=0.85,
|
|
protect_first_n=1,
|
|
protect_last_n=1,
|
|
quiet_mode=True,
|
|
)
|
|
|
|
|
|
def _response(content: str):
|
|
mock_response = MagicMock()
|
|
mock_response.choices = [MagicMock()]
|
|
mock_response.choices[0].message.content = content
|
|
return mock_response
|
|
|
|
|
|
def _messages_with_handoff(summary_body: str):
|
|
return [
|
|
{"role": "system", "content": "system prompt"},
|
|
{"role": "user", "content": f"{SUMMARY_PREFIX}\n{summary_body}"},
|
|
{"role": "assistant", "content": "handoff acknowledged after resume"},
|
|
{"role": "user", "content": "new user turn after resume"},
|
|
{"role": "assistant", "content": "new assistant work after resume"},
|
|
{"role": "user", "content": "more new work after resume"},
|
|
{"role": "assistant", "content": "latest tail response"},
|
|
{"role": "user", "content": "final active request stays in protected tail"},
|
|
]
|
|
|
|
|
|
def test_existing_previous_summary_is_not_serialized_again_as_new_turn():
|
|
"""Same-process iterative compression should not feed the old handoff twice."""
|
|
compressor = _compressor()
|
|
old_summary = "OLD-SUMMARY-BODY unique continuity facts"
|
|
compressor._previous_summary = old_summary
|
|
|
|
with patch("agent.context_compressor.call_llm", return_value=_response("updated summary")) as mock_call:
|
|
compressor.compress(_messages_with_handoff(old_summary))
|
|
|
|
prompt = mock_call.call_args.kwargs["messages"][0]["content"]
|
|
assert "PREVIOUS SUMMARY:" in prompt
|
|
assert "NEW TURNS TO INCORPORATE:" in prompt
|
|
assert prompt.count(old_summary) == 1
|
|
assert f"[USER]: {SUMMARY_PREFIX}" not in prompt
|
|
|
|
|
|
def test_resume_rehydrates_previous_summary_from_handoff_message():
|
|
"""After restart/resume, the persisted handoff should regain summary identity."""
|
|
compressor = _compressor()
|
|
old_summary = "RESUMED-SUMMARY-BODY durable continuity facts"
|
|
assert compressor._previous_summary is None
|
|
|
|
with patch("agent.context_compressor.call_llm", return_value=_response("updated summary")) as mock_call:
|
|
compressor.compress(_messages_with_handoff(old_summary))
|
|
|
|
prompt = mock_call.call_args.kwargs["messages"][0]["content"]
|
|
assert "PREVIOUS SUMMARY:" in prompt
|
|
assert "NEW TURNS TO INCORPORATE:" in prompt
|
|
assert "TURNS TO SUMMARIZE:" not in prompt
|
|
assert prompt.count(old_summary) == 1
|
|
assert f"[USER]: {SUMMARY_PREFIX}" not in prompt
|
|
|
|
|
|
def test_handoff_in_protected_head_populates_previous_summary_before_update():
|
|
"""A resumed protected-head handoff should restore iterative-summary state."""
|
|
compressor = _compressor()
|
|
old_summary = "PROTECTED-HEAD-SUMMARY durable facts from before restart"
|
|
seen_turns = []
|
|
|
|
def fake_generate_summary(turns_to_summarize, focus_topic=None):
|
|
seen_turns.extend(turns_to_summarize)
|
|
return "new summary from resumed turns"
|
|
|
|
with patch.object(compressor, "_generate_summary", side_effect=fake_generate_summary):
|
|
compressor.compress(_messages_with_handoff(old_summary))
|
|
|
|
assert compressor._previous_summary == old_summary
|
|
assert seen_turns
|
|
assert all(old_summary not in str(msg.get("content", "")) for msg in seen_turns)
|