mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
Merge 55a4c5385f into 13038dc747
This commit is contained in:
commit
625a5b0c1b
2 changed files with 94 additions and 0 deletions
|
|
@ -288,6 +288,10 @@ class ContextCompressor(ContextEngine):
|
|||
def name(self) -> str:
|
||||
return "compressor"
|
||||
|
||||
def _recovery_growth_tokens(self) -> int:
|
||||
"""How much new prompt growth warrants another compression attempt."""
|
||||
return max(int(self.threshold_tokens * 0.15), 8_000)
|
||||
|
||||
def on_session_reset(self) -> None:
|
||||
"""Reset all per-session state for /new or /reset."""
|
||||
super().on_session_reset()
|
||||
|
|
@ -297,6 +301,8 @@ class ContextCompressor(ContextEngine):
|
|||
self._last_summary_error = None
|
||||
self._last_compression_savings_pct = 100.0
|
||||
self._ineffective_compression_count = 0
|
||||
self._last_compression_time = 0.0
|
||||
self._last_compression_prompt_tokens = 0
|
||||
|
||||
def update_model(
|
||||
self,
|
||||
|
|
@ -389,6 +395,8 @@ class ContextCompressor(ContextEngine):
|
|||
# Anti-thrashing: track whether last compression was effective
|
||||
self._last_compression_savings_pct: float = 100.0
|
||||
self._ineffective_compression_count: int = 0
|
||||
self._last_compression_time: float = 0.0
|
||||
self._last_compression_prompt_tokens: int = 0
|
||||
self._summary_failure_cooldown_until: float = 0.0
|
||||
self._last_summary_error: Optional[str] = None
|
||||
|
||||
|
|
@ -407,6 +415,28 @@ class ContextCompressor(ContextEngine):
|
|||
tokens = prompt_tokens if prompt_tokens is not None else self.last_prompt_tokens
|
||||
if tokens < self.threshold_tokens:
|
||||
return False
|
||||
# Anti-thrashing recovery: allow another attempt after either enough
|
||||
# wall-clock time has passed or enough new prompt growth has
|
||||
# accumulated to make compression worthwhile again.
|
||||
if self._ineffective_compression_count >= 2:
|
||||
recovered = False
|
||||
if (self._last_compression_time > 0
|
||||
and time.monotonic() - self._last_compression_time >= 300):
|
||||
recovered = True
|
||||
if not self.quiet_mode:
|
||||
logger.info(
|
||||
"Anti-thrashing cooldown expired — re-enabling auto-compression"
|
||||
)
|
||||
elif (self._last_compression_prompt_tokens > 0
|
||||
and tokens - self._last_compression_prompt_tokens >= self._recovery_growth_tokens()):
|
||||
recovered = True
|
||||
if not self.quiet_mode:
|
||||
logger.info(
|
||||
"Anti-thrashing reset after prompt growth (%d new tokens)",
|
||||
tokens - self._last_compression_prompt_tokens,
|
||||
)
|
||||
if recovered:
|
||||
self._ineffective_compression_count = 0
|
||||
# Anti-thrashing: back off if recent compressions were ineffective
|
||||
if self._ineffective_compression_count >= 2:
|
||||
if not self.quiet_mode:
|
||||
|
|
@ -1279,6 +1309,8 @@ The user has requested that this compaction PRIORITISE preserving all informatio
|
|||
saved_estimate = display_tokens - new_estimate
|
||||
|
||||
# Anti-thrashing: track compression effectiveness
|
||||
self._last_compression_time = time.monotonic()
|
||||
self._last_compression_prompt_tokens = display_tokens
|
||||
savings_pct = (saved_estimate / display_tokens * 100) if display_tokens > 0 else 0
|
||||
self._last_compression_savings_pct = savings_pct
|
||||
if savings_pct < 10:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
"""Tests for agent/context_compressor.py — compression logic, thresholds, truncation fallback."""
|
||||
|
||||
import time
|
||||
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
|
|
@ -38,6 +40,66 @@ class TestShouldCompress:
|
|||
assert compressor.should_compress(prompt_tokens=50000) is False
|
||||
|
||||
|
||||
class TestAntiThrashingRecovery:
|
||||
"""Anti-thrashing protection should recover after a cooldown period (#14694)."""
|
||||
|
||||
def test_anti_thrash_blocks_after_two_ineffective(self, compressor):
|
||||
"""Two ineffective compressions should block further auto-compression."""
|
||||
compressor._ineffective_compression_count = 2
|
||||
compressor.last_prompt_tokens = 90000
|
||||
assert compressor.should_compress() is False
|
||||
|
||||
def test_anti_thrash_recovers_after_cooldown(self, compressor):
|
||||
"""After 300s cooldown, the anti-thrashing counter resets and
|
||||
compression is re-enabled. This test fails without the fix."""
|
||||
compressor._ineffective_compression_count = 2
|
||||
compressor._last_compression_time = time.monotonic() - 301
|
||||
compressor.last_prompt_tokens = 90000
|
||||
assert compressor.should_compress() is True
|
||||
assert compressor._ineffective_compression_count == 0
|
||||
|
||||
def test_anti_thrash_still_blocks_within_cooldown(self, compressor):
|
||||
"""Within 300s the block should remain active."""
|
||||
compressor._ineffective_compression_count = 2
|
||||
compressor._last_compression_time = time.monotonic() - 60
|
||||
compressor.last_prompt_tokens = 90000
|
||||
assert compressor.should_compress() is False
|
||||
|
||||
def test_anti_thrash_recovers_after_meaningful_prompt_growth(self, compressor):
|
||||
"""Large prompt growth should re-enable compression even before cooldown."""
|
||||
compressor._ineffective_compression_count = 2
|
||||
compressor._last_compression_time = time.monotonic() - 60
|
||||
compressor._last_compression_prompt_tokens = 90_000
|
||||
compressor.last_prompt_tokens = 105_000
|
||||
assert compressor.should_compress() is True
|
||||
assert compressor._ineffective_compression_count == 0
|
||||
|
||||
def test_anti_thrash_small_growth_does_not_reset(self, compressor):
|
||||
"""Minor growth should not immediately re-enable compression."""
|
||||
compressor._ineffective_compression_count = 2
|
||||
compressor._last_compression_time = time.monotonic() - 60
|
||||
compressor._last_compression_prompt_tokens = 90_000
|
||||
compressor.last_prompt_tokens = 94_000
|
||||
assert compressor.should_compress() is False
|
||||
|
||||
def test_anti_thrash_no_recovery_without_timestamp(self, compressor):
|
||||
"""Without time or growth metadata, recovery should not trigger."""
|
||||
compressor._ineffective_compression_count = 2
|
||||
compressor._last_compression_time = 0.0
|
||||
compressor._last_compression_prompt_tokens = 0
|
||||
compressor.last_prompt_tokens = 90000
|
||||
assert compressor.should_compress() is False
|
||||
|
||||
def test_session_reset_clears_compression_time(self, compressor):
|
||||
"""on_session_reset() should reset anti-thrash recovery metadata."""
|
||||
compressor._last_compression_time = 12345.0
|
||||
compressor._last_compression_prompt_tokens = 90000
|
||||
compressor._ineffective_compression_count = 2
|
||||
compressor.on_session_reset()
|
||||
assert compressor._last_compression_time == 0.0
|
||||
assert compressor._last_compression_prompt_tokens == 0
|
||||
assert compressor._ineffective_compression_count == 0
|
||||
|
||||
|
||||
class TestUpdateFromResponse:
|
||||
def test_updates_fields(self, compressor):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue