mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-23 16:36:23 +00:00
The salvaged estimator ran a per-character Python loop on every
estimate_tokens_rough() call — a ~28,000,000x slowdown vs (len+3)//4 on a
1MB ASCII tool output (measured ~3.0s per call). Gate it:
- str.isascii() O(1) fast path keeps pure-ASCII text bit-identical to the
classic (len+3)//4 rule at ~1.3x baseline cost (0.23us vs 0.17us per
1MB call).
- Non-ASCII text counts dense CJK chars via a compiled character-class
regex in C (len(text) - len(re.sub(''))): ~352ms/1MB hangul vs ~2.1s
for the per-char loop.
- Non-ASCII-but-non-CJK text (accents, Cyrillic, emoji) keeps the classic
rule.
Also: parity tests against the per-char reference implementation, and
updated two stale expectations that encoded the old behavior (CJK now
counted ~1 token/char; short string content now ceil-divided instead of
floored to 0). The continuity test now detects merged-into-tail summaries
via _is_context_summary_content.
114 lines
4.7 KiB
Python
114 lines
4.7 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,
|
|
memory_context="",
|
|
):
|
|
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)
|
|
|
|
|
|
def test_handoff_in_protected_head_is_replaced_not_duplicated():
|
|
"""Re-compaction must replace a protected old handoff with the updated one."""
|
|
compressor = _compressor()
|
|
old_summary = "OLD-PROTECTED-HANDOFF unique old summary body"
|
|
|
|
with patch("agent.context_compressor.call_llm", return_value=_response("UPDATED summary body")):
|
|
compressed = compressor.compress(_messages_with_handoff(old_summary))
|
|
|
|
# The summary may be emitted standalone or merged into the first tail
|
|
# message (alternation corner case), so detect it the same way the
|
|
# compressor does rather than via a startswith(SUMMARY_PREFIX) check.
|
|
summary_messages = [
|
|
msg
|
|
for msg in compressed
|
|
if isinstance(msg, dict)
|
|
and ContextCompressor._is_context_summary_content(msg.get("content"))
|
|
]
|
|
assert len(summary_messages) == 1
|
|
assert "UPDATED summary body" in str(summary_messages[0]["content"])
|
|
assert old_summary not in str(summary_messages[0]["content"])
|
|
assert old_summary not in "\n".join(str(msg.get("content") or "") for msg in compressed)
|