fix(agent): focus automatic compression on recent user turns

This commit is contained in:
konsisumer 2026-06-03 13:18:52 +02:00 committed by Teknium
parent db7714d5f1
commit 434c684bfa
2 changed files with 71 additions and 4 deletions

View file

@ -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):

View file

@ -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