fix(agent): focus automatic compression on recent user turns
This commit is contained in:
parent
db7714d5f1
commit
434c684bfa
2 changed files with 71 additions and 4 deletions
|
|
@ -143,6 +143,9 @@ _SUMMARY_FAILURE_COOLDOWN_SECONDS = 600
|
||||||
# become another unbounded transcript copy after the LLM summarizer failed.
|
# become another unbounded transcript copy after the LLM summarizer failed.
|
||||||
_FALLBACK_SUMMARY_MAX_CHARS = 8_000
|
_FALLBACK_SUMMARY_MAX_CHARS = 8_000
|
||||||
_FALLBACK_TURN_MAX_CHARS = 700
|
_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`'\")\]}<>]+")
|
_PATH_MENTION_RE = re.compile(r"(?:/|~/?|[A-Za-z]:\\)[^\s`'\")\]}<>]+")
|
||||||
|
|
@ -1454,7 +1457,7 @@ Use this exact structure:
|
||||||
prompt += f"""
|
prompt += f"""
|
||||||
|
|
||||||
FOCUS TOPIC: "{focus_topic}"
|
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:
|
try:
|
||||||
call_kwargs = {
|
call_kwargs = {
|
||||||
|
|
@ -1623,6 +1626,41 @@ The user has requested that this compaction PRIORITISE preserving all informatio
|
||||||
return True
|
return True
|
||||||
return any(text.startswith(p) for p in _HISTORICAL_SUMMARY_PREFIXES)
|
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
|
@classmethod
|
||||||
def _find_latest_context_summary(
|
def _find_latest_context_summary(
|
||||||
cls,
|
cls,
|
||||||
|
|
@ -2070,7 +2108,8 @@ The user has requested that this compaction PRIORITISE preserving all informatio
|
||||||
)
|
)
|
||||||
|
|
||||||
# Phase 3: Generate structured summary
|
# 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
|
# If summary generation failed, behavior splits on
|
||||||
# ``abort_on_summary_failure`` (config: compression.abort_on_summary_failure):
|
# ``abort_on_summary_failure`` (config: compression.abort_on_summary_failure):
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,7 @@ def test_compress_passes_focus_to_generate_summary():
|
||||||
|
|
||||||
|
|
||||||
def test_compress_none_focus_by_default():
|
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()
|
compressor = _make_compressor()
|
||||||
|
|
||||||
received_kwargs = {}
|
received_kwargs = {}
|
||||||
|
|
@ -141,4 +141,32 @@ def test_compress_none_focus_by_default():
|
||||||
|
|
||||||
compressor.compress(messages, current_tokens=100000)
|
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
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue