fix(context_management): move a client compaction summary into the system prompt when clearing runs before compact

This commit is contained in:
mateo-berri 2026-10-28 12:10:17 -07:00
parent 2209041e4a
commit 7fd95b5f69
2 changed files with 24 additions and 16 deletions

View file

@ -163,17 +163,17 @@ async def prepare_context_managed_request(
)
# Skip the client-history pre-processing when a ``compact_20260112``
# polyfill spec will run: that editor already slices around any client-sent
# polyfill edit runs first: that editor already slices around any client-sent
# compaction block in its Phase A (and uses the full post-compaction tail
# for its token-threshold check). Pre-collapsing to just the latest user
# question here would starve the polyfill of conversation context and
# silently drop intermediate turns.
polyfill_will_run: Final = _polyfill_will_run(
compact_runs_first: Final = _compact_runs_first(
context_management_spec=context_management_spec,
additional_drop_params=additional_drop_params,
)
if polyfill_will_run:
if compact_runs_first:
history_result: PolyfillResult | None = None
working_messages: _AnthropicMessages = messages
working_system: _AnthropicSystem = system
@ -211,7 +211,7 @@ async def prepare_context_managed_request(
# ``_run_polyfill_if_enabled``), apply the slice-only fallback now so
# Anthropic-specific ``compaction`` content blocks don't leak through
# to non-Anthropic backends that would reject them.
if polyfill_will_run and history_result is None:
if compact_runs_first and history_result is None:
history_result = apply_client_compaction_block_history(
messages=messages,
system=system,
@ -219,18 +219,18 @@ async def prepare_context_managed_request(
return history_result
def _polyfill_will_run(
def _compact_runs_first(
*,
context_management_spec: _ContextManagementSpec,
additional_drop_params: list[str] | None,
) -> bool:
"""Return True when ``compact_20260112`` will run via the polyfill dispatcher.
"""Return True when ``compact_20260112`` is the first edit the polyfill dispatcher runs.
Mirrors the gating in ``_run_polyfill_if_enabled``: an empty spec or an
explicit ``context_management`` entry in ``additional_drop_params``
short-circuits the polyfill. The pre-processing skip only applies when the
dispatcher will actually invoke ``apply_compact_20260112`` (which has its
own compaction-block slicing).
short-circuits the polyfill. The pre-processing skip only applies when
``apply_compact_20260112`` (which has its own compaction-block slicing)
sees the messages before any other editor does.
"""
edits: Final = _normalize_spec_edits(
context_management_spec=context_management_spec,
@ -243,7 +243,7 @@ def _polyfill_will_run(
COMPACT_EDIT_TYPE,
)
return any(edit.get("type") == COMPACT_EDIT_TYPE for edit in edits)
return bool(edits) and edits[0].get("type") == COMPACT_EDIT_TYPE
def _spec_has_non_compact_edits(

View file

@ -724,8 +724,9 @@ def test_input_tokens_trigger_counts_the_system_prompt(system: str | list[dict[s
assert with_system["cleared_input_tokens"] == met_without_system["cleared_input_tokens"] > 0
async def test_input_tokens_trigger_counts_a_client_compaction_summary() -> None:
"""A client compaction summary moves into the system prompt, so the trigger counts it too."""
@pytest.mark.parametrize("compact_position", [None, "after", "before"], ids=["alone", "before_compact", "after_compact"])
async def test_input_tokens_trigger_counts_a_client_compaction_summary(compact_position: str | None) -> None:
"""A client compaction summary moves into the system prompt, so the trigger counts it too, in any edit order."""
tail: Final = _make_history(n_pairs=3)
edit_spec: Final = {
"type": "clear_tool_uses_20250919",
@ -733,6 +734,10 @@ async def test_input_tokens_trigger_counts_a_client_compaction_summary() -> None
"keep": {"type": "tool_uses", "value": 1},
}
compacted_turn: Final = {"role": "assistant", "content": [{"type": "compaction", "content": "Earlier work. " * 400}]}
compact_spec: Final = {"type": "compact_20260112", "trigger": {"type": "input_tokens", "value": 50_000}}
edits: Final = {None: [edit_spec], "after": [edit_spec, compact_spec], "before": [compact_spec, edit_spec]}[
compact_position
]
async def prepare(messages: list[dict[str, object]]) -> PolyfillResult | None:
return await prepare_context_managed_request(
@ -740,18 +745,21 @@ async def test_input_tokens_trigger_counts_a_client_compaction_summary() -> None
messages=messages,
tools=None,
system=None,
context_management_spec={"edits": [edit_spec]},
context_management_spec={"edits": edits},
litellm_metadata=None,
additional_drop_params=None,
llm_router=None,
)
def cleared(result: PolyfillResult | None) -> list[object]:
applied: Final = result.applied_edits if result is not None else []
return [edit.get("cleared_tool_uses") for edit in applied if edit.get("type") == "clear_tool_uses_20250919"]
without_summary: Final = await prepare(deepcopy(tail))
with_summary: Final = await prepare([compacted_turn, *deepcopy(tail)])
assert without_summary is None or not without_summary.applied_edits
assert with_summary is not None
assert [edit.get("cleared_tool_uses") for edit in with_summary.applied_edits] == [2]
assert cleared(without_summary) == []
assert cleared(with_summary) == [2]
@pytest.mark.parametrize(("clear_tool_inputs", "triggers"), [(False, False), (True, True)])