Change-detectors, tautologies, source-reading tests, redundant duplicates, mock-echo tests and dead/unrunnable tests. Per-test rationale in the lane ledger (category + reason for every removal).
170 lines
7.0 KiB
Python
170 lines
7.0 KiB
Python
"""Compression hygiene: small-context threshold floor, reasoning-trace
|
|
exclusion, and bounded summary size.
|
|
|
|
Covers the July 2026 compression tuning pass:
|
|
|
|
1. Reasoning traces (native ``reasoning`` field AND inline ``<think>``-style
|
|
blocks) must never reach the summarizer prompt, and traces emitted BY the
|
|
summarizer model must never be stored in the summary.
|
|
2. Head/tail protection budgets stay proportionate (tail = 20% of threshold).
|
|
3. Summary token budget is bounded to the 1K-10K envelope.
|
|
4. Models with context windows below 512K get their compression threshold
|
|
floored at 75% (raise-only — a higher configured value always wins).
|
|
"""
|
|
|
|
from unittest.mock import patch
|
|
|
|
import agent.context_compressor as cc
|
|
from agent.context_compressor import ContextCompressor
|
|
|
|
|
|
def _make(ctx: int, pct: float = 0.50) -> ContextCompressor:
|
|
with patch.object(cc, "get_model_context_length", return_value=ctx):
|
|
comp = ContextCompressor(
|
|
model="test/model", threshold_percent=pct, quiet_mode=True,
|
|
)
|
|
# Resolve while the mock is active — lazy init (#32221) defers the
|
|
# window probe (and the floor application) past __init__.
|
|
_ = comp.context_length
|
|
return comp
|
|
|
|
|
|
class TestSmallContextThresholdFloor:
|
|
|
|
|
|
|
|
|
|
def test_update_model_rederives_floor_both_directions(self):
|
|
comp = _make(128_000, pct=0.50)
|
|
assert comp.threshold_percent == 0.75
|
|
# small -> large: back to the configured 50%
|
|
comp.update_model("big", 1_000_000)
|
|
assert comp.threshold_percent == 0.50
|
|
assert comp.threshold_tokens == 500_000
|
|
# large -> small: floor re-applies
|
|
comp.update_model("small", 200_000)
|
|
assert comp.threshold_percent == 0.75
|
|
assert comp.threshold_tokens == 150_000
|
|
|
|
|
|
class TestReasoningExcludedFromSummarizer:
|
|
def test_serializer_drops_inline_think_blocks(self):
|
|
comp = _make(128_000)
|
|
turns = [
|
|
{"role": "user", "content": "do the thing"},
|
|
{"role": "assistant", "content": "<think>INLINE_TRACE</think>visible answer"},
|
|
{"role": "assistant", "content": "<reasoning>VARIANT_TRACE</reasoning>other answer"},
|
|
]
|
|
ser = comp._serialize_for_summary(turns)
|
|
assert "INLINE_TRACE" not in ser
|
|
assert "VARIANT_TRACE" not in ser
|
|
assert "visible answer" in ser
|
|
assert "other answer" in ser
|
|
|
|
|
|
def test_summarizer_output_think_block_stripped_before_store(self):
|
|
comp = _make(128_000)
|
|
|
|
class FakeMsg:
|
|
content = "<think>OUTPUT_TRACE</think>\n## Active Task\nUser asked X"
|
|
|
|
class FakeChoice:
|
|
message = FakeMsg()
|
|
|
|
class FakeResp:
|
|
choices = [FakeChoice()]
|
|
|
|
with patch.object(cc, "call_llm", return_value=FakeResp()):
|
|
out = comp._generate_summary([{"role": "user", "content": "hi"}])
|
|
assert out is not None
|
|
assert "OUTPUT_TRACE" not in out
|
|
# The iterative-update seed must be clean too, or the trace compounds
|
|
# across every subsequent compaction.
|
|
assert "OUTPUT_TRACE" not in (comp._previous_summary or "")
|
|
|
|
|
|
|
|
class TestSummaryBudgetEnvelope:
|
|
def test_no_max_tokens_wire_cap_on_summary_call(self):
|
|
"""The summary budget is PROMPT GUIDANCE only ("Target ~N tokens").
|
|
|
|
A wire-level max_tokens cap truncates summaries mid-section on the
|
|
Anthropic Messages / NVIDIA NIM paths (which forward the param), and
|
|
thinking models burn the cap on reasoning before emitting the summary
|
|
body — producing truncated or thinking-only summaries and compaction
|
|
loops. The call must NOT carry max_tokens.
|
|
"""
|
|
comp = _make(128_000)
|
|
captured = {}
|
|
|
|
class FakeMsg:
|
|
content = "## Active Task\nUser asked X"
|
|
|
|
class FakeChoice:
|
|
message = FakeMsg()
|
|
|
|
class FakeResp:
|
|
choices = [FakeChoice()]
|
|
|
|
def fake_call_llm(**kw):
|
|
captured.update(kw)
|
|
return FakeResp()
|
|
|
|
with patch.object(cc, "call_llm", side_effect=fake_call_llm):
|
|
out = comp._generate_summary([{"role": "user", "content": "hi"}])
|
|
assert out is not None
|
|
assert "max_tokens" not in captured
|
|
|
|
|
|
|
|
|
|
|
|
class TestTailBudgetProportionality:
|
|
def test_tail_budget_is_target_ratio_of_threshold(self):
|
|
# Legacy-mode contract: the threshold-proportional formula. The
|
|
# default is lean (clamped 10K-25K) since the tail-default flip, so
|
|
# this pins the LEGACY path explicitly.
|
|
comp = _make(128_000)
|
|
comp.tail_mode = "legacy"
|
|
comp._tail_token_budget = None # force mode-aware recompute
|
|
assert comp.tail_token_budget == int(comp.threshold_tokens * comp.summary_target_ratio)
|
|
# Sanity: tail protection stays a modest slice of the window (<= 20%).
|
|
assert comp.tail_token_budget <= comp.context_length * 0.20
|
|
|
|
|
|
def test_tail_budget_never_exceeds_window_share(self):
|
|
"""The lean 10K floor is 61% of a 16K window and 122% of an 8K one: on a local 27B the
|
|
"protected" tail was the whole request and compaction reclaimed nothing. Whatever the
|
|
formula, the verbatim tail stays within ``TAIL_MAX_CONTEXT_FRACTION`` of the window."""
|
|
from agent.context_compressor import LEAN_TAIL_FLOOR_TOKENS, TAIL_MAX_CONTEXT_FRACTION
|
|
|
|
for ctx in (8_192, 16_384, 32_768):
|
|
comp = _make(ctx)
|
|
assert comp.tail_token_budget <= ctx * TAIL_MAX_CONTEXT_FRACTION, ctx
|
|
assert comp.tail_token_budget > 0, ctx
|
|
# Big windows are untouched: the lean clamp still binds.
|
|
assert _make(131_072).tail_token_budget == LEAN_TAIL_FLOOR_TOKENS
|
|
|
|
def test_small_window_compress_leaves_a_real_middle(self):
|
|
"""End to end through the boundary walk: on an 8K window a tool-heavy transcript must yield a
|
|
compressible middle that is most of the transcript, and the retained tail must stay near the
|
|
window share (one atomic tool group of overrun is allowed for the required anchors)."""
|
|
from agent.context_compressor import TAIL_MAX_CONTEXT_FRACTION, _estimate_msg_budget_tokens
|
|
|
|
ctx = 8_192
|
|
comp = _make(ctx)
|
|
msgs: list = [{"role": "system", "content": "sys"}]
|
|
for i in range(12):
|
|
msgs.append({"role": "user", "content": f"step {i}"})
|
|
msgs.append({
|
|
"role": "assistant", "content": "",
|
|
"tool_calls": [{"id": f"c{i}", "type": "function", "function": {"name": "terminal", "arguments": "{}"}}],
|
|
})
|
|
msgs.append({"role": "tool", "tool_call_id": f"c{i}", "content": "x" * 4_000})
|
|
msgs.append({"role": "assistant", "content": f"done {i}"})
|
|
start, end = comp._compress_window(msgs)
|
|
tail_tokens = sum(_estimate_msg_budget_tokens(m) for m in msgs[end:])
|
|
one_turn = sum(_estimate_msg_budget_tokens(m) for m in msgs[-4:])
|
|
assert tail_tokens <= ctx * TAIL_MAX_CONTEXT_FRACTION + one_turn
|
|
assert end - start >= (len(msgs) - start) // 2, (start, end, len(msgs))
|