From 434c684bfa3276c82d1b023f46ff2a7ab13f1379 Mon Sep 17 00:00:00 2001 From: konsisumer Date: Wed, 3 Jun 2026 13:18:52 +0200 Subject: [PATCH] fix(agent): focus automatic compression on recent user turns --- agent/context_compressor.py | 43 ++++++++++++++++++++++++++++-- tests/agent/test_compress_focus.py | 32 ++++++++++++++++++++-- 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/agent/context_compressor.py b/agent/context_compressor.py index 2995bf92451..a1dd7142166 100644 --- a/agent/context_compressor.py +++ b/agent/context_compressor.py @@ -143,6 +143,9 @@ _SUMMARY_FAILURE_COOLDOWN_SECONDS = 600 # become another unbounded transcript copy after the LLM summarizer failed. _FALLBACK_SUMMARY_MAX_CHARS = 8_000 _FALLBACK_TURN_MAX_CHARS = 700 +_AUTO_FOCUS_MAX_TURNS = 3 +_AUTO_FOCUS_TURN_MAX_CHARS = 260 +_AUTO_FOCUS_MAX_CHARS = 700 _PATH_MENTION_RE = re.compile(r"(?:/|~/?|[A-Za-z]:\\)[^\s`'\")\]}<>]+") @@ -1454,7 +1457,7 @@ Use this exact structure: prompt += f""" FOCUS TOPIC: "{focus_topic}" -The user has requested that this compaction PRIORITISE preserving all information related to the focus topic above. For content related to "{focus_topic}", include full detail — exact values, file paths, command outputs, error messages, and decisions. For content NOT related to the focus topic, summarise more aggressively (brief one-liners or omit if truly irrelevant). The focus topic sections should receive roughly 60-70% of the summary token budget. Even for the focus topic, NEVER preserve API keys, tokens, passwords, or credentials — use [REDACTED].""" +This compaction should PRIORITISE preserving all information related to the focus topic above. For content related to "{focus_topic}", include full detail — exact values, file paths, command outputs, error messages, and decisions. For content NOT related to the focus topic, summarise more aggressively (brief one-liners or omit if truly irrelevant). The focus topic sections should receive roughly 60-70% of the summary token budget. Even for the focus topic, NEVER preserve API keys, tokens, passwords, or credentials — use [REDACTED].""" try: call_kwargs = { @@ -1623,6 +1626,41 @@ The user has requested that this compaction PRIORITISE preserving all informatio return True return any(text.startswith(p) for p in _HISTORICAL_SUMMARY_PREFIXES) + @classmethod + def _derive_auto_focus_topic( + cls, + messages: List[Dict[str, Any]], + tail_start: int, + ) -> Optional[str]: + """Infer a compact focus hint from the most recent real user turns.""" + candidates: list[str] = [] + del tail_start # Reserved for callers that already know the protected-tail boundary. + for idx in range(len(messages) - 1, -1, -1): + msg = messages[idx] + if msg.get("role") != "user": + continue + content = msg.get("content") + if cls._is_context_summary_content(content): + continue + text = redact_sensitive_text(_content_text_for_contains(content).strip()) + if not text: + continue + text = " ".join(text.split()) + if len(text) > _AUTO_FOCUS_TURN_MAX_CHARS: + text = text[: _AUTO_FOCUS_TURN_MAX_CHARS - 1].rstrip() + "…" + candidates.append(text) + if len(candidates) >= _AUTO_FOCUS_MAX_TURNS: + break + + if not candidates: + return None + + candidates.reverse() + focus = "Recent user focus:\n" + "\n".join(f"- {item}" for item in candidates) + if len(focus) > _AUTO_FOCUS_MAX_CHARS: + focus = focus[: _AUTO_FOCUS_MAX_CHARS - 1].rstrip() + "…" + return focus + @classmethod def _find_latest_context_summary( cls, @@ -2070,7 +2108,8 @@ The user has requested that this compaction PRIORITISE preserving all informatio ) # Phase 3: Generate structured summary - summary = self._generate_summary(turns_to_summarize, focus_topic=focus_topic) + summary_focus_topic = focus_topic or self._derive_auto_focus_topic(messages, compress_end) + summary = self._generate_summary(turns_to_summarize, focus_topic=summary_focus_topic) # If summary generation failed, behavior splits on # ``abort_on_summary_failure`` (config: compression.abort_on_summary_failure): diff --git a/tests/agent/test_compress_focus.py b/tests/agent/test_compress_focus.py index 8b5b1d35da3..8ffc5e2e712 100644 --- a/tests/agent/test_compress_focus.py +++ b/tests/agent/test_compress_focus.py @@ -116,7 +116,7 @@ def test_compress_passes_focus_to_generate_summary(): def test_compress_none_focus_by_default(): - """compress() passes None focus_topic by default.""" + """Auto compression derives focus_topic from recent user turns by default.""" compressor = _make_compressor() received_kwargs = {} @@ -141,4 +141,32 @@ def test_compress_none_focus_by_default(): compressor.compress(messages, current_tokens=100000) - assert received_kwargs.get("focus_topic") is None + focus_topic = received_kwargs.get("focus_topic") + assert focus_topic.startswith("Recent user focus:") + assert "- second" in focus_topic + assert "- third" in focus_topic + assert "- fourth" in focus_topic + + +def test_auto_focus_skips_context_summary_handoff(): + """Persisted handoff messages should not become the inferred focus.""" + compressor = _make_compressor() + messages = [ + {"role": "system", "content": "System prompt"}, + { + "role": "user", + "content": "[CONTEXT COMPACTION — REFERENCE ONLY] stale Bybit topic", + }, + {"role": "assistant", "content": "handoff acknowledged"}, + {"role": "user", "content": "Can OpenViking support sqlite backends?"}, + {"role": "assistant", "content": "Let's inspect that."}, + {"role": "user", "content": "Compare OpenViking postgres and sqlite options."}, + {"role": "assistant", "content": "Working on it."}, + {"role": "user", "content": "Now focus on OpenViking database support."}, + {"role": "assistant", "content": "Latest tail response"}, + ] + + focus_topic = compressor._derive_auto_focus_topic(messages, tail_start=1) + + assert "OpenViking" in focus_topic + assert "Bybit" not in focus_topic