mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-13 09:01:54 +00:00
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.
|
||||
_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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue