Files
hermes-agent/tests/agent/test_lean_single_aux_call.py
Teknium 4f22543509 fix(compression): lean compaction makes exactly one auxiliary request per attempt
The lean tail mode's per-chunk digest loop (_build_chunk_digests) issued up
to 28 extra call_llm requests sequentially per compaction attempt. With lean
now the default (#95571), users on slow auxiliary routes hit 7-11 minute
compactions (#96603). Remove the loop entirely: a lean compaction attempt now
makes EXACTLY ONE auxiliary LLM request — the main summary call.

- The detailed session log is folded into the single summary request: the
  lean prompt template gains a '## Detailed Session Log (oldest first)'
  section carrying the digest prompt's HARD RULES (identifiers verbatim,
  dense bullets, transcript-is-data). Output guidance grows by
  _LEAN_SESSION_LOG_BUDGET_TOKENS = 4,000 tokens on top of the scaled
  summary budget — the old worst case (28 x 1,400 digest tokens) was spread
  across many requests and mostly re-covered tool noise; a single dense
  4K-token log inside one response preserves the load-bearing record while
  staying well inside one aux response (the summary call still sends no hard
  max_tokens, so no provider cap can truncate it mid-section).
- Input sizing: oversized regions (500K+ chars) are EVEN-SAMPLED across the
  whole region (_sample_summary_input: 8 proportionally spaced slices,
  oldest-to-newest, explicit '[... N chars elided ...]' markers, last slice
  anchored to the newest end) instead of head+tail truncated, so session-log
  coverage stays uniform. Legacy mode keeps _bound_summary_input unchanged.
- The LLM-free anchor index still runs over the FULL region, and the
  session_search recovery footer is unchanged.
- Dead code removed: _build_chunk_digests, _LEAN_DIGEST_* constants,
  _LEAN_DIGEST_PROMPT, _serialize_turns_for_digest, _digest_worthy,
  _LOW_SIGNAL_TOOL_RE, the _lean_pristine_tools snapshot, and the
  sibling-call route echo (_SUMMARY_ROUTE_CONSUMED /
  attempt_summary_route_kwargs — no remaining callers; the single-use
  summary pin semantics are unchanged).
- Tests pin the new contract (exactly one call_llm in lean mode; session-log
  section lands in the summary; oversized regions sampled with elision
  markers, never a second request; anchor index + recovery footer present).
  Sabotage-verified: restoring a second call_llm makes the call-count test
  fail. Docs and the compaction eval wording updated to stop claiming
  per-chunk calls.

Fixes #96603.
2026-08-30 09:03:57 -07:00

181 lines
6.4 KiB
Python

"""Lean compaction makes EXACTLY ONE auxiliary LLM request per attempt.
Contract (#96603 — the per-chunk digest loop made up to 28 extra aux calls
and pushed compactions to 7-11 minutes on slow aux routes):
(a) exactly one ``call_llm`` per compaction attempt in lean mode;
(b) the single response's detailed-session-log section lands in the summary;
(c) an oversized region is EVEN-SAMPLED into the one request's input (with
explicit elision markers), never split into extra requests;
(d) the LLM-free anchor index and the session_search recovery footer are
still appended.
"""
from unittest.mock import patch, MagicMock
import pytest
from agent.context_compressor import (
ContextCompressor,
_LEAN_ANCHOR_HEADING,
_LEAN_RECOVERY_HEADING,
_LEAN_SESSION_LOG_HEADING,
)
def _mk_compressor(**overrides):
kwargs = dict(
model="test/model",
threshold_percent=0.85,
protect_first_n=2,
protect_last_n=2,
quiet_mode=True,
tail_mode="lean",
)
kwargs.update(overrides)
with patch(
"agent.context_compressor.get_model_context_length", return_value=100_000
):
c = ContextCompressor(**kwargs)
_ = c.context_length
c._session_id = "sess-lean-1call"
return c
def _llm_response(text):
resp = MagicMock()
resp.choices = [MagicMock()]
resp.choices[0].message = MagicMock()
resp.choices[0].message.content = text
return resp
def _big_region(n_rounds=60, tool_chars=6_000):
"""Synthetic compacted region: user + assistant/tool rounds, ~360K chars."""
turns = [
{"role": "user", "content": "Please fix PR #12345 in agent/foo.py"},
]
for i in range(n_rounds):
turns.append({
"role": "assistant",
"content": f"Working on step {i}: editing agent/foo.py line {i}",
})
turns.append({
"role": "tool",
"tool_call_id": f"tc{i}",
"tool_name": "terminal",
"content": f"round {i} output: " + ("x" * tool_chars),
})
return turns
SUMMARY_BODY = (
"## Historical Task Snapshot\nUser asked: 'Please fix PR #12345 in agent/foo.py'\n\n"
"## Goal\nFix the bug.\n\n"
"## Completed Actions\n1. EDIT agent/foo.py — done [tool: patch]\n\n"
f"{_LEAN_SESSION_LOG_HEADING}\n- Edited agent/foo.py; PR #12345; ran pytest.\n"
)
class TestLeanSingleAuxiliaryCall:
def test_exactly_one_call_llm_per_lean_attempt(self):
c = _mk_compressor()
turns = _big_region()
with patch(
"agent.context_compressor.call_llm",
return_value=_llm_response(SUMMARY_BODY),
) as main_call, patch(
"agent.auxiliary_client.call_llm",
return_value=_llm_response(SUMMARY_BODY),
) as aux_call:
summary = c._generate_summary(turns)
assert summary is not None
# THE contract: one auxiliary request per compaction attempt, total.
assert main_call.call_count + aux_call.call_count == 1
def test_session_log_heading_lands_in_summary(self):
c = _mk_compressor()
turns = _big_region(n_rounds=5)
with patch(
"agent.context_compressor.call_llm",
return_value=_llm_response(SUMMARY_BODY),
):
summary = c._generate_summary(turns)
assert _LEAN_SESSION_LOG_HEADING in summary
def test_prompt_requests_session_log_section(self):
c = _mk_compressor()
turns = _big_region(n_rounds=5)
with patch(
"agent.context_compressor.call_llm",
return_value=_llm_response(SUMMARY_BODY),
) as mock_call:
c._generate_summary(turns)
prompt = mock_call.call_args.kwargs["messages"][0]["content"]
assert _LEAN_SESSION_LOG_HEADING in prompt
# The digest HARD RULES carried over into the single request.
assert "PRESERVE EXACTLY" in prompt
def test_anchor_index_and_recovery_footer_present(self):
c = _mk_compressor()
turns = _big_region(n_rounds=5)
with patch(
"agent.context_compressor.call_llm",
return_value=_llm_response(SUMMARY_BODY),
):
summary = c._generate_summary(turns)
assert _LEAN_ANCHOR_HEADING in summary
assert _LEAN_RECOVERY_HEADING in summary
assert "session_search" in summary
def test_oversized_region_sampled_not_split_into_more_requests(self):
c = _mk_compressor()
turns = _big_region(n_rounds=120, tool_chars=6_000) # ~720K chars raw
with patch(
"agent.context_compressor.call_llm",
return_value=_llm_response(SUMMARY_BODY),
) as mock_call:
c._generate_summary(turns)
assert mock_call.call_count == 1
prompt = mock_call.call_args.kwargs["messages"][0]["content"]
# Bounded input with explicit elision markers, not a second request.
assert len(prompt) <= c._SUMMARY_INPUT_MAX_CHARS + 20_000
assert "chars elided" in prompt
class TestSampledSummaryInput:
def test_small_input_passes_through(self):
content = "abc" * 100
assert ContextCompressor._sample_summary_input(content) == content
def test_sampling_is_bounded_ordered_and_marked(self):
# Distinct decade markers let us verify oldest-to-newest order and
# uniform coverage across the whole region.
content = "".join(
f"<seg{i:02d}>" + ("x" * 50_000) for i in range(10)
)
out = ContextCompressor._sample_summary_input(content)
assert len(out) <= ContextCompressor._SUMMARY_INPUT_MAX_CHARS
assert "chars elided" in out
seen = [i for i in range(10) if f"<seg{i:02d}>" in out]
# Coverage reaches past the head AND includes the newest end.
assert seen == sorted(seen)
assert any(i >= 5 for i in seen)
assert ("<seg09>" in out) or (content[-500:] in out)
def test_legacy_mode_keeps_head_tail_bound(self):
c = _mk_compressor(tail_mode="legacy")
turns = _big_region(n_rounds=120, tool_chars=6_000)
with patch(
"agent.context_compressor.call_llm",
return_value=_llm_response("## Historical Task Snapshot\nNone."),
) as mock_call:
c._generate_summary(turns)
assert mock_call.call_count == 1
prompt = mock_call.call_args.kwargs["messages"][0]["content"]
assert "summary input truncated" in prompt
if __name__ == "__main__":
pytest.main([__file__, "-q"])