mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-17 14:42:06 +00:00
fix(agent): preserve recent turns during compression
This commit is contained in:
parent
bbf020e709
commit
aec38855b5
2 changed files with 58 additions and 7 deletions
|
|
@ -156,6 +156,11 @@ _FALLBACK_TURN_MAX_CHARS = 700
|
|||
_AUTO_FOCUS_MAX_TURNS = 3
|
||||
_AUTO_FOCUS_TURN_MAX_CHARS = 260
|
||||
_AUTO_FOCUS_MAX_CHARS = 700
|
||||
# Keep a short run of recent messages verbatim even when the token budget is
|
||||
# already exhausted. The public ``protect_last_n`` default is intentionally
|
||||
# high for small/light tails, but using all 20 as a hard floor here would bring
|
||||
# back the old large-tool-output case where nothing can be compacted.
|
||||
_MAX_TAIL_MESSAGE_FLOOR = 8
|
||||
|
||||
|
||||
_PATH_MENTION_RE = re.compile(r"(?:/|~/?|[A-Za-z]:\\)[^\s`'\")\]}<>]+")
|
||||
|
|
@ -1990,11 +1995,12 @@ This compaction should PRIORITISE preserving all information related to the focu
|
|||
derived from ``summary_target_ratio * context_length``, so it
|
||||
scales automatically with the model's context window.
|
||||
|
||||
Token budget is the primary criterion. A hard minimum of 3 messages
|
||||
is always protected, but the budget is allowed to exceed by up to
|
||||
1.5x to avoid cutting inside an oversized message (tool output, file
|
||||
read, etc.). If even the minimum 3 messages exceed 1.5x the budget
|
||||
the cut is placed right after the head so compression still runs.
|
||||
Token budget is the primary criterion. A bounded message-count floor
|
||||
keeps a short run of recent turns verbatim even when the budget is
|
||||
exhausted, but the budget is allowed to exceed by up to 1.5x to avoid
|
||||
cutting inside an oversized message (tool output, file read, etc.). If
|
||||
even that floor exceeds 1.5x the budget, the cut is placed right after
|
||||
the head so compression still runs.
|
||||
|
||||
Never cuts inside a tool_call/result group. Always ensures the most
|
||||
recent user message is in the tail (see ``_ensure_last_user_message_in_tail``).
|
||||
|
|
@ -2002,8 +2008,19 @@ This compaction should PRIORITISE preserving all information related to the focu
|
|||
if token_budget is None:
|
||||
token_budget = self.tail_token_budget
|
||||
n = len(messages)
|
||||
# Hard minimum: always keep at least 3 messages in the tail
|
||||
min_tail = min(3, n - head_end - 1) if n - head_end > 1 else 0
|
||||
# Hard minimum: always keep a bounded recent-message floor in the tail.
|
||||
# ``protect_last_n`` remains a minimum up to the cap; the cap avoids
|
||||
# preserving a whole run of bulky tool outputs on every compaction.
|
||||
available_tail = max(0, n - head_end - 1)
|
||||
min_tail_floor = max(3, min(self.protect_last_n, _MAX_TAIL_MESSAGE_FLOOR))
|
||||
# Leave at least two non-head messages available to summarize on short
|
||||
# transcripts; otherwise compression can replace a tiny middle with a
|
||||
# summary and save no messages at all.
|
||||
compressible_tail_cap = max(3, available_tail - 2)
|
||||
min_tail = (
|
||||
min(min_tail_floor, compressible_tail_cap, available_tail)
|
||||
if available_tail > 1 else 0
|
||||
)
|
||||
soft_ceiling = int(token_budget * 1.5)
|
||||
accumulated = 0
|
||||
cut_idx = n # start from beyond the end
|
||||
|
|
|
|||
|
|
@ -1804,6 +1804,40 @@ class TestTokenBudgetTailProtection:
|
|||
tail_size = len(messages) - cut
|
||||
assert tail_size >= 3, f"Tail is only {tail_size} messages, min should be 3"
|
||||
|
||||
def test_tiny_budget_preserves_bounded_recent_turns(self, budget_compressor):
|
||||
"""A token-exhausted tail must preserve more than just the latest ask.
|
||||
|
||||
Regression for #9413: the previous hard-coded 3-message floor could
|
||||
leave the latest user message live while summarizing the assistant/tool
|
||||
context immediately before it, which made the post-compression turn feel
|
||||
like a fresh conversation.
|
||||
"""
|
||||
c = budget_compressor
|
||||
c.tail_token_budget = 10
|
||||
c.protect_last_n = 20
|
||||
messages = [
|
||||
{"role": "system", "content": "sys"},
|
||||
{"role": "user", "content": "old start"},
|
||||
{"role": "assistant", "content": "old ack"},
|
||||
{"role": "user", "content": "middle work"},
|
||||
{"role": "assistant", "content": "middle ack"},
|
||||
{"role": "user", "content": "middle ask 2"},
|
||||
{"role": "assistant", "content": "middle answer 2"},
|
||||
{"role": "user", "content": "middle ask 3"},
|
||||
{"role": "assistant", "content": "middle answer 3"},
|
||||
{"role": "user", "content": "recent ask 1"},
|
||||
{"role": "assistant", "content": "recent answer 1"},
|
||||
{"role": "user", "content": "recent ask 2"},
|
||||
{"role": "assistant", "content": "recent answer 2"},
|
||||
{"role": "user", "content": "latest ask"},
|
||||
]
|
||||
|
||||
cut = c._find_tail_cut_by_tokens(messages, head_end=1)
|
||||
|
||||
assert len(messages) - cut >= 8
|
||||
assert messages[cut]["content"] == "middle answer 2"
|
||||
assert messages[-1]["content"] == "latest ask"
|
||||
|
||||
def test_soft_ceiling_allows_oversized_message(self, budget_compressor):
|
||||
"""The 1.5x soft ceiling allows an oversized message to be included
|
||||
rather than splitting it."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue