mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-23 16:36:23 +00:00
fix(compress): allow manual /compress when auto-compaction is disabled [overflow error directs users there]
compression.enabled: false is documented (agent/conversation_loop.py overflow path) as disabling *automatic* compaction only — the terminal context-overflow error explicitly tells users to run /compress manually, and the gateway handler has never gated on the flag. But the classic CLI (_manual_compress) and the ACP adapter (/compact) refused with 'Compression is disabled', leaving users at a full context with no manual escape hatch on those surfaces. Remove the stale gates (they predate the overflow-path design; the CLI gate came from the original /compress commit's boilerplate) and unify force=True across all manual-compaction call sites: ACP /compact and the TUI's _compress_session_history (manual-only helper) now bypass the summary-failure cooldown exactly like the CLI and gateway already did. Also reword the ACP /context status line so a disabled-compression agent no longer implies /compact is unavailable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
e907ecccef
commit
a007ac55c6
6 changed files with 117 additions and 7 deletions
|
|
@ -2021,7 +2021,10 @@ class HermesACPAgent(acp.Agent):
|
|||
lines.append(f"Compression threshold: ~{threshold_tokens:,} tokens")
|
||||
|
||||
if getattr(agent, "compression_enabled", True) is False:
|
||||
lines.append("Compression is disabled for this agent.")
|
||||
lines.append(
|
||||
"Auto-compaction is disabled (compression.enabled: false); "
|
||||
"/compact still compresses manually."
|
||||
)
|
||||
else:
|
||||
lines.append("Tip: run /compress to compress manually before the threshold.")
|
||||
|
||||
|
|
@ -2048,8 +2051,9 @@ class HermesACPAgent(acp.Agent):
|
|||
return "Nothing to compress — conversation is empty."
|
||||
try:
|
||||
agent = state.agent
|
||||
if not getattr(agent, "compression_enabled", True):
|
||||
return "Context compression is disabled for this agent."
|
||||
# No compression_enabled gate: the flag disables *automatic*
|
||||
# compaction only; manual /compact must keep working (matches
|
||||
# the CLI /compress and gateway handlers).
|
||||
if not hasattr(agent, "_compress_context"):
|
||||
return "Context compression not available for this agent."
|
||||
|
||||
|
|
@ -2074,6 +2078,7 @@ class HermesACPAgent(acp.Agent):
|
|||
getattr(agent, "_cached_system_prompt", "") or "",
|
||||
approx_tokens=approx_tokens,
|
||||
task_id=state.session_id,
|
||||
force=True,
|
||||
)
|
||||
finally:
|
||||
agent._session_db = original_session_db
|
||||
|
|
|
|||
8
cli.py
8
cli.py
|
|
@ -9929,9 +9929,11 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
|
|||
print("(._.) No active agent -- send a message first.")
|
||||
return
|
||||
|
||||
if not self.agent.compression_enabled:
|
||||
print("(._.) Compression is disabled in config.")
|
||||
return
|
||||
# No compression_enabled gate here: the config flag disables
|
||||
# *automatic* compaction only. Manual /compress is an explicit user
|
||||
# action — the context-overflow error path (conversation_loop.py)
|
||||
# directs users here when auto-compaction is off, and the gateway's
|
||||
# /compress handler has never gated on the flag.
|
||||
|
||||
from hermes_cli.partial_compress import (
|
||||
extract_compress_flags,
|
||||
|
|
|
|||
|
|
@ -1723,12 +1723,13 @@ class TestSlashCommands:
|
|||
original_session_db = object()
|
||||
state.agent._session_db = original_session_db
|
||||
|
||||
def _compress_context(messages, system_prompt, *, approx_tokens, task_id):
|
||||
def _compress_context(messages, system_prompt, *, approx_tokens, task_id, force):
|
||||
assert state.agent._session_db is None
|
||||
assert messages == state.history
|
||||
assert system_prompt == "system"
|
||||
assert approx_tokens == 40
|
||||
assert task_id == state.session_id
|
||||
assert force is True
|
||||
return [{"role": "user", "content": "summary"}], "new-system"
|
||||
|
||||
state.agent._compress_context = MagicMock(side_effect=_compress_context)
|
||||
|
|
@ -1756,9 +1757,43 @@ class TestSlashCommands:
|
|||
"system",
|
||||
approx_tokens=40,
|
||||
task_id=state.session_id,
|
||||
force=True,
|
||||
)
|
||||
mock_save.assert_called_once_with(state.session_id)
|
||||
|
||||
def test_compact_works_when_auto_compaction_disabled(self, agent, mock_manager):
|
||||
"""compression.enabled: false disables *automatic* compaction only —
|
||||
manual /compact must still compress (matches CLI /compress and the
|
||||
gateway handler)."""
|
||||
state = self._make_state(mock_manager)
|
||||
state.history = [
|
||||
{"role": "user", "content": "one"},
|
||||
{"role": "assistant", "content": "two"},
|
||||
{"role": "user", "content": "three"},
|
||||
{"role": "assistant", "content": "four"},
|
||||
]
|
||||
state.agent.compression_enabled = False
|
||||
state.agent._cached_system_prompt = "system"
|
||||
state.agent.tools = None
|
||||
state.agent._session_db = None
|
||||
state.agent._compress_context = MagicMock(
|
||||
return_value=([{"role": "user", "content": "summary"}], "new-system")
|
||||
)
|
||||
|
||||
with (
|
||||
patch.object(agent.session_manager, "save_session"),
|
||||
patch(
|
||||
"agent.model_metadata.estimate_request_tokens_rough",
|
||||
side_effect=[40, 12],
|
||||
),
|
||||
):
|
||||
result = agent._handle_slash_command("/compact", state)
|
||||
|
||||
assert "disabled" not in result.lower()
|
||||
assert "Context compressed: 4 -> 1 messages" in result
|
||||
state.agent._compress_context.assert_called_once()
|
||||
assert state.agent._compress_context.call_args.kwargs.get("force") is True
|
||||
|
||||
def test_unknown_command_returns_none(self, agent, mock_manager):
|
||||
state = self._make_state(mock_manager)
|
||||
result = agent._handle_slash_command("/nonexistent", state)
|
||||
|
|
|
|||
|
|
@ -188,6 +188,40 @@ def test_manual_compress_does_not_flush_full_history_when_session_id_unchanged()
|
|||
shell.agent._flush_messages_to_session_db.assert_not_called()
|
||||
|
||||
|
||||
def test_manual_compress_runs_when_auto_compaction_disabled(capsys):
|
||||
"""compression.enabled: false disables *automatic* compaction only.
|
||||
|
||||
Manual /compress must still work: the context-overflow error path
|
||||
(agent/conversation_loop.py) explicitly directs users to /compress when
|
||||
auto-compaction is off, and the gateway's /compress handler has never
|
||||
gated on the flag. Regression for the CLI refusing with "Compression is
|
||||
disabled in config."
|
||||
"""
|
||||
shell = _make_cli()
|
||||
history = _make_history()
|
||||
compressed = [
|
||||
{"role": "user", "content": "[summary]"},
|
||||
history[-1],
|
||||
]
|
||||
shell.conversation_history = history
|
||||
shell.agent = MagicMock()
|
||||
shell.agent.compression_enabled = False
|
||||
shell.agent._cached_system_prompt = ""
|
||||
shell.agent.tools = None
|
||||
shell.agent.session_id = shell.session_id
|
||||
shell.agent._compress_context.return_value = (compressed, "")
|
||||
|
||||
with patch("agent.model_metadata.estimate_request_tokens_rough", return_value=100):
|
||||
shell._manual_compress()
|
||||
|
||||
output = capsys.readouterr().out
|
||||
assert "Compression is disabled" not in output
|
||||
shell.agent._compress_context.assert_called_once()
|
||||
# Manual compression bypasses the summary-failure cooldown.
|
||||
assert shell.agent._compress_context.call_args.kwargs.get("force") is True
|
||||
assert shell.conversation_history == compressed
|
||||
|
||||
|
||||
def test_manual_compress_no_sync_when_session_id_unchanged():
|
||||
"""If compression is a no-op (agent.session_id didn't change), the CLI
|
||||
must NOT clear _pending_title or otherwise disturb session state.
|
||||
|
|
|
|||
|
|
@ -5864,6 +5864,34 @@ def test_config_set_personality_preserves_history_and_returns_info(monkeypatch):
|
|||
assert ("session.info", "sid", {"model": "?"}) in emits
|
||||
|
||||
|
||||
def test_compress_session_history_passes_force():
|
||||
"""_compress_session_history is manual-only (session.compress RPC, slash
|
||||
compress/compact, slash-worker mirror) — it must bypass the
|
||||
summary-failure cooldown via force=True, matching the CLI and gateway
|
||||
manual-compress handlers."""
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
agent = MagicMock()
|
||||
agent.context_compressor = None # keep _get_usage on the simple path
|
||||
compressed = [{"role": "user", "content": "summary"}]
|
||||
agent._compress_context.return_value = (compressed, "")
|
||||
session = _session(
|
||||
agent=agent,
|
||||
history=[
|
||||
{"role": "user", "content": "one"},
|
||||
{"role": "assistant", "content": "two"},
|
||||
{"role": "user", "content": "three"},
|
||||
{"role": "assistant", "content": "four"},
|
||||
],
|
||||
)
|
||||
|
||||
removed, _usage = server._compress_session_history(session)
|
||||
|
||||
assert removed == 3
|
||||
assert session["history"] == compressed
|
||||
assert agent._compress_context.call_args.kwargs.get("force") is True
|
||||
|
||||
|
||||
def test_session_compress_uses_compress_helper(monkeypatch):
|
||||
agent = types.SimpleNamespace()
|
||||
server._sessions["sid"] = _session(agent=agent)
|
||||
|
|
|
|||
|
|
@ -3644,12 +3644,18 @@ def _compress_session_history(
|
|||
# cached prompt (which already contains the agent identity block)
|
||||
# makes the rebuild append the identity a second time. Mirrors the
|
||||
# CLI's _manual_compress fix for issue #15281.
|
||||
# force=True: every caller of this helper is a manual /compress path
|
||||
# (session.compress RPC, slash compress/compact, slash-worker mirror) —
|
||||
# auto-compaction runs inside the agent loop, not here. Manual
|
||||
# compaction bypasses the summary-failure cooldown, matching the CLI
|
||||
# and gateway handlers.
|
||||
try:
|
||||
compressed, _ = agent._compress_context(
|
||||
history,
|
||||
None,
|
||||
approx_tokens=approx_tokens,
|
||||
focus_topic=focus_topic or None,
|
||||
force=True,
|
||||
defer_context_engine_notification=True,
|
||||
)
|
||||
except Exception:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue