"""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 ````-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_sub_512k_floors_to_75_percent(self): for ctx in (128_000, 200_000, 262_144, 511_999): comp = _make(ctx, pct=0.50) assert comp.threshold_percent == 0.75, ctx assert comp.threshold_tokens == int(ctx * 0.75), ctx 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": "INLINE_TRACEvisible answer"}, {"role": "assistant", "content": "VARIANT_TRACEother 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 = "OUTPUT_TRACE\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 assert "## Active Task" 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 # The budget still lands as prompt guidance, within the envelope. prompt = captured["messages"][0]["content"] import re m = re.search(r"Target ~(\d+) tokens", prompt) assert m, "prompt-level token target guidance missing" assert 1_000 <= int(m.group(1)) <= 10_000 def test_budget_capped_at_10k_even_on_1m_window(self): comp = _make(1_000_000) huge = [{"role": "assistant", "content": "x" * 8000} for _ in range(200)] assert comp._compute_summary_budget(huge) <= 10_000 assert comp.max_summary_tokens <= 10_000 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_default_lean_tail_is_clamped(self): # Default-mode contract after the flip: lean clamp, never the # threshold-proportional hoard. from agent.context_compressor import LEAN_TAIL_CAP_TOKENS, LEAN_TAIL_FLOOR_TOKENS comp = _make(128_000) assert comp.tail_mode == "lean" assert LEAN_TAIL_FLOOR_TOKENS <= comp.tail_token_budget <= LEAN_TAIL_CAP_TOKENS