mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-21 16:18:55 +00:00
fix(context): persist fallback compaction breaker
This commit is contained in:
parent
5ce827cac9
commit
af7dceaf77
10 changed files with 394 additions and 91 deletions
|
|
@ -210,6 +210,70 @@ class TestFutilityGuard:
|
|||
assert cc.awaiting_real_usage_after_compression is False
|
||||
assert cc._ineffective_compression_count == 0
|
||||
|
||||
def test_fallback_streak_survives_ordinary_fitting_responses(self):
|
||||
cc = _compressor(threshold_tokens=24_576)
|
||||
|
||||
cc.record_completed_compaction(used_fallback=True)
|
||||
cc.update_from_response({"prompt_tokens": 20_000})
|
||||
assert cc._fallback_compression_streak == 1
|
||||
|
||||
# Context regrows through ordinary successful turns before the next
|
||||
# fallback boundary. Those turns reset real-usage effectiveness, not
|
||||
# the independent summary-quality breaker.
|
||||
cc.update_from_response({"prompt_tokens": 20_000})
|
||||
cc.record_completed_compaction(used_fallback=True)
|
||||
cc.update_from_response({"prompt_tokens": 20_000})
|
||||
|
||||
assert cc._fallback_compression_streak == 2
|
||||
assert not cc.should_compress(33_564)
|
||||
|
||||
def test_usage_less_fallback_boundary_still_counts(self):
|
||||
cc = _compressor(threshold_tokens=24_576)
|
||||
|
||||
cc.record_completed_compaction(used_fallback=True)
|
||||
cc.awaiting_real_usage_after_compression = True
|
||||
cc.update_from_response({})
|
||||
|
||||
assert cc._fallback_compression_streak == 1
|
||||
assert cc._verify_compaction_cleared_threshold is False
|
||||
assert cc.awaiting_real_usage_after_compression is False
|
||||
|
||||
def test_healthy_boundary_resets_only_fallback_streak(self):
|
||||
cc = _compressor(threshold_tokens=24_576)
|
||||
cc.record_completed_compaction(used_fallback=True)
|
||||
cc.record_completed_compaction(used_fallback=False)
|
||||
|
||||
assert cc._fallback_compression_streak == 0
|
||||
assert cc._verify_compaction_cleared_threshold is True
|
||||
|
||||
def test_model_switch_resets_and_persists_fallback_streak(self, tmp_path):
|
||||
from hermes_state import SessionDB
|
||||
|
||||
db = SessionDB(db_path=tmp_path / "state.db")
|
||||
db.create_session("s1", source="cli")
|
||||
cc = _compressor(threshold_tokens=24_576)
|
||||
cc.bind_session_state(db, "s1")
|
||||
cc.record_completed_compaction(used_fallback=True)
|
||||
|
||||
cc.update_model("next-model", 100_000)
|
||||
|
||||
assert cc._fallback_compression_streak == 0
|
||||
assert db.get_compression_fallback_streak("s1") == 0
|
||||
|
||||
def test_same_runtime_context_recalibration_preserves_fallback_streak(self, tmp_path):
|
||||
from hermes_state import SessionDB
|
||||
|
||||
db = SessionDB(db_path=tmp_path / "state.db")
|
||||
db.create_session("s1", source="cli")
|
||||
cc = _compressor(threshold_tokens=24_576)
|
||||
cc.bind_session_state(db, "s1")
|
||||
cc.record_completed_compaction(used_fallback=True)
|
||||
|
||||
cc.update_model(cc.model, 64_000, provider=cc.provider)
|
||||
|
||||
assert cc._fallback_compression_streak == 1
|
||||
assert db.get_compression_fallback_streak("s1") == 1
|
||||
|
||||
def test_a_failed_pass_records_exactly_one_strike(self):
|
||||
"""A compaction that leaves the real prompt over the threshold: one strike.
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import os
|
|||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from agent.context_compressor import ContextCompressor
|
||||
from hermes_state import SessionDB
|
||||
|
||||
|
||||
|
|
@ -130,3 +131,96 @@ class TestPlatformForwardedAtBoundary:
|
|||
kwargs = calls[-1].kwargs
|
||||
assert kwargs.get("platform") == "telegram"
|
||||
assert kwargs.get("boundary_reason") == "compression"
|
||||
|
||||
|
||||
class TestFallbackStreakFollowsRotation:
|
||||
def test_fallback_boundary_persists_on_child_session(self, tmp_path: Path):
|
||||
db = SessionDB(db_path=tmp_path / "state.db")
|
||||
parent = "PARENT_FALLBACK_ROT"
|
||||
db.create_session(parent, source="telegram")
|
||||
with patch(
|
||||
"agent.context_compressor.get_model_context_length",
|
||||
return_value=100_000,
|
||||
):
|
||||
compressor = ContextCompressor(
|
||||
model="test/model",
|
||||
threshold_percent=0.85,
|
||||
protect_first_n=2,
|
||||
protect_last_n=2,
|
||||
quiet_mode=True,
|
||||
)
|
||||
compressor.bind_session_state(db, parent)
|
||||
|
||||
# A fallback streak must survive the session-id rotation itself. The
|
||||
# boundary then records the just-completed fallback on the child row.
|
||||
compressor.record_completed_compaction(used_fallback=True)
|
||||
assert db.get_compression_fallback_streak(parent) == 1
|
||||
db.create_session(
|
||||
"CHILD_FALLBACK_ROT",
|
||||
source="telegram",
|
||||
parent_session_id=parent,
|
||||
)
|
||||
compressor.on_session_start(
|
||||
"CHILD_FALLBACK_ROT",
|
||||
session_db=db,
|
||||
boundary_reason="compression",
|
||||
old_session_id=parent,
|
||||
)
|
||||
assert compressor._fallback_compression_streak == 1
|
||||
|
||||
compressor.record_completed_compaction(used_fallback=True)
|
||||
assert compressor._fallback_compression_streak == 2
|
||||
assert db.get_compression_fallback_streak("CHILD_FALLBACK_ROT") == 2
|
||||
|
||||
resumed = ContextCompressor(
|
||||
model="test/model",
|
||||
threshold_percent=0.85,
|
||||
protect_first_n=2,
|
||||
protect_last_n=2,
|
||||
quiet_mode=True,
|
||||
)
|
||||
resumed.bind_session_state(db, "CHILD_FALLBACK_ROT")
|
||||
assert resumed._fallback_compression_streak == 2
|
||||
|
||||
def test_real_rotation_records_fallback_after_lifecycle_rebind(self, tmp_path: Path):
|
||||
db = SessionDB(db_path=tmp_path / "state.db")
|
||||
parent = "PARENT_REAL_FALLBACK_ROT"
|
||||
db.create_session(parent, source="telegram")
|
||||
agent = _build_agent_with_db(db, parent, platform="telegram")
|
||||
|
||||
with patch(
|
||||
"agent.context_compressor.get_model_context_length",
|
||||
return_value=100_000,
|
||||
):
|
||||
compressor = ContextCompressor(
|
||||
model="test/model",
|
||||
threshold_percent=0.85,
|
||||
protect_first_n=2,
|
||||
protect_last_n=2,
|
||||
quiet_mode=True,
|
||||
)
|
||||
compressor.bind_session_state(db, parent)
|
||||
compressed = [
|
||||
{"role": "user", "content": "[CONTEXT COMPACTION] fallback"},
|
||||
{"role": "assistant", "content": "tail"},
|
||||
]
|
||||
|
||||
def _fallback_compress(*_args, **_kwargs):
|
||||
compressor._last_summary_error = "empty summary"
|
||||
compressor._last_summary_fallback_used = True
|
||||
compressor._last_compression_made_progress = True
|
||||
return compressed
|
||||
|
||||
with patch.object(
|
||||
compressor,
|
||||
"compress",
|
||||
side_effect=_fallback_compress,
|
||||
):
|
||||
compressor.compression_count = 1
|
||||
setattr(agent, "context_compressor", compressor)
|
||||
agent._compress_context(_msgs(), "sys", approx_tokens=120_000)
|
||||
child = getattr(agent, "session_id")
|
||||
|
||||
assert child != parent
|
||||
assert compressor._fallback_compression_streak == 1
|
||||
assert db.get_compression_fallback_streak(child) == 1
|
||||
|
|
|
|||
|
|
@ -65,62 +65,6 @@ class TestUpdateFromResponse:
|
|||
compressor.update_from_response({})
|
||||
assert compressor.last_prompt_tokens == 0
|
||||
|
||||
def test_fallback_compaction_counts_as_ineffective_even_when_prompt_fits(self, compressor):
|
||||
compressor.threshold_tokens = 85_000
|
||||
compressor.awaiting_real_usage_after_compression = True
|
||||
compressor.last_compression_rough_tokens = 90_000
|
||||
compressor._verify_compaction_cleared_threshold = True
|
||||
compressor._last_summary_fallback_used = True
|
||||
|
||||
compressor.update_from_response({
|
||||
"prompt_tokens": 5_000,
|
||||
"completion_tokens": 100,
|
||||
"total_tokens": 5_100,
|
||||
})
|
||||
|
||||
assert compressor.last_real_prompt_tokens == 5_000
|
||||
assert compressor.last_rough_tokens_when_real_prompt_fit == 90_000
|
||||
assert compressor._ineffective_compression_count == 1
|
||||
assert compressor._verify_compaction_cleared_threshold is False
|
||||
assert compressor.awaiting_real_usage_after_compression is False
|
||||
|
||||
def test_successful_compaction_fit_still_clears_prior_ineffective_count(self, compressor):
|
||||
compressor.threshold_tokens = 85_000
|
||||
compressor.awaiting_real_usage_after_compression = True
|
||||
compressor.last_compression_rough_tokens = 90_000
|
||||
compressor._verify_compaction_cleared_threshold = True
|
||||
compressor._last_summary_fallback_used = False
|
||||
compressor._ineffective_compression_count = 1
|
||||
|
||||
compressor.update_from_response({
|
||||
"prompt_tokens": 5_000,
|
||||
"completion_tokens": 100,
|
||||
"total_tokens": 5_100,
|
||||
})
|
||||
|
||||
assert compressor._ineffective_compression_count == 0
|
||||
|
||||
def test_second_fallback_strike_blocks_future_compression(self, compressor):
|
||||
compressor.threshold_tokens = 85_000
|
||||
compressor.last_prompt_tokens = 90_000
|
||||
assert compressor.should_compress() is True
|
||||
|
||||
for expected in (1, 2):
|
||||
compressor.awaiting_real_usage_after_compression = True
|
||||
compressor.last_compression_rough_tokens = 90_000
|
||||
compressor._verify_compaction_cleared_threshold = True
|
||||
compressor._last_summary_fallback_used = True
|
||||
compressor.update_from_response({
|
||||
"prompt_tokens": 5_000,
|
||||
"completion_tokens": 100,
|
||||
"total_tokens": 5_100,
|
||||
})
|
||||
assert compressor._ineffective_compression_count == expected
|
||||
compressor.last_prompt_tokens = 90_000
|
||||
|
||||
assert compressor.should_compress() is False
|
||||
|
||||
|
||||
class TestPreflightDeferral:
|
||||
def test_defers_when_recent_real_usage_fit_and_rough_growth_is_small(self, compressor):
|
||||
compressor.threshold_tokens = 85_000
|
||||
|
|
|
|||
|
|
@ -165,6 +165,7 @@ def test_reset_session_state_rebinds_builtin_compressor_after_session_switch(tmp
|
|||
db.create_session("old-sid", source="cli")
|
||||
db.create_session("new-sid", source="cli")
|
||||
db.record_compression_failure_cooldown("old-sid", 4_000_000_000.0, "old-timeout")
|
||||
db.set_compression_fallback_streak("old-sid", 2)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"agent.context_compressor.get_model_context_length",
|
||||
|
|
@ -188,7 +189,9 @@ def test_reset_session_state_rebinds_builtin_compressor_after_session_switch(tmp
|
|||
|
||||
assert compressor._session_id == "new-sid"
|
||||
assert compressor.get_active_compression_failure_cooldown() is None
|
||||
assert compressor._fallback_compression_streak == 0
|
||||
assert db.get_compression_failure_cooldown("old-sid") is not None
|
||||
assert db.get_compression_fallback_streak("old-sid") == 2
|
||||
|
||||
compressor._record_compression_failure_cooldown(30.0, "new-timeout")
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue