mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-08 13:12:08 +00:00
fix(compression): preserve user turn after compaction
This commit is contained in:
parent
2d3eac5fbd
commit
6e176e4c21
2 changed files with 56 additions and 0 deletions
|
|
@ -391,6 +391,27 @@ def conversation_history_after_compression(agent: Any, messages: list) -> Option
|
|||
return None
|
||||
|
||||
|
||||
def _ensure_compressed_has_user_turn(original_messages: list, compressed: list) -> None:
|
||||
"""Preserve a real user turn when a compressor returns assistant/tool-only context."""
|
||||
if any(isinstance(msg, dict) and msg.get("role") == "user" for msg in compressed):
|
||||
return
|
||||
for msg in reversed(original_messages):
|
||||
if not isinstance(msg, dict) or msg.get("role") != "user":
|
||||
continue
|
||||
restored = dict(msg)
|
||||
restored.pop("_db_persisted", None)
|
||||
compressed.append(restored)
|
||||
return
|
||||
compressed.append({
|
||||
"role": "user",
|
||||
"content": (
|
||||
"Continue from the compressed conversation context above. "
|
||||
"This marker exists because the compacted transcript contained "
|
||||
"no preserved user turn."
|
||||
),
|
||||
})
|
||||
|
||||
|
||||
def compress_context(
|
||||
agent: Any,
|
||||
messages: list,
|
||||
|
|
@ -647,6 +668,7 @@ def compress_context(
|
|||
todo_snapshot = agent._todo_store.format_for_injection()
|
||||
if todo_snapshot:
|
||||
compressed.append({"role": "user", "content": todo_snapshot})
|
||||
_ensure_compressed_has_user_turn(messages, compressed)
|
||||
|
||||
agent._invalidate_system_prompt()
|
||||
new_system_prompt = agent._build_system_prompt(system_message)
|
||||
|
|
|
|||
|
|
@ -202,6 +202,40 @@ def test_skipped_compression_returns_messages_unchanged(tmp_path: Path) -> None:
|
|||
agent.context_compressor.compress.assert_not_called()
|
||||
|
||||
|
||||
def test_compression_restores_user_turn_when_compressor_drops_all_users(tmp_path: Path) -> None:
|
||||
"""Provider chat templates need at least one user message after compaction.
|
||||
|
||||
A plugin or future compressor can legally return a compacted context made
|
||||
only of assistant/tool summary rows. Before the guard in
|
||||
``compress_context``, that transcript went straight into the next API call;
|
||||
LM Studio / llama.cpp Jinja templates then failed with "No user query found
|
||||
in messages." Preserve the last real user turn from the pre-compression
|
||||
transcript instead of inventing a new active request.
|
||||
"""
|
||||
db = SessionDB(db_path=tmp_path / "state.db")
|
||||
parent_sid = "NO_USER_AFTER_COMPRESS"
|
||||
db.create_session(parent_sid, source="cli")
|
||||
|
||||
agent = _build_agent_with_db(db, parent_sid)
|
||||
agent.context_compressor.compress.side_effect = lambda *_a, **_kw: [
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "[CONTEXT COMPACTION] earlier work was summarized",
|
||||
}
|
||||
]
|
||||
messages = [
|
||||
{"role": "user", "content": "first request"},
|
||||
{"role": "assistant", "content": "first answer"},
|
||||
{"role": "user", "content": "please continue from here"},
|
||||
{"role": "assistant", "content": "working"},
|
||||
]
|
||||
|
||||
compressed, _sp = agent._compress_context(messages, "sys", approx_tokens=120_000)
|
||||
|
||||
user_messages = [msg for msg in compressed if msg.get("role") == "user"]
|
||||
assert user_messages == [{"role": "user", "content": "please continue from here"}]
|
||||
|
||||
|
||||
def test_lock_refresh_keeps_owner_live_past_initial_ttl(tmp_path: Path, monkeypatch) -> None:
|
||||
"""The owning compression call must keep its lease alive while it runs."""
|
||||
real_try_acquire = SessionDB.try_acquire_compression_lock
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue