mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-08 13:12:08 +00:00
fix(gateway): pass full transcript to compressor instead of filtered messages (#58551)
Both gateway compression entry points (session-hygiene auto-compress in run.py; manual /compress in slash_commands.py) filtered the transcript to user/assistant-only, content-bearing messages before calling _compress_context. That starved the compressor: - tool results are usually the bulk of the context, and _prune_old_tool_results never saw them - short filtered histories tripped the protect-first/last early-return, so compression became a no-op even on huge sessions - assistant tool_calls stubs (content=None) were dropped, so even the summary lost the tool activity Pass user/assistant/tool messages through intact, matching what the agent loop itself feeds _compress_context. Port of PR #3854 onto current main (the manual-compress handler moved from run.py to slash_commands.py since the PR branched); regression test asserts tool messages reach the compressor. Authored-by: David Zhang <david.d.zhang@gmail.com> (@Git-on-my-level) Co-authored-by: David Zhang <david.d.zhang@gmail.com>
This commit is contained in:
parent
d810ff2f2b
commit
485ae54c9f
3 changed files with 66 additions and 7 deletions
|
|
@ -10794,11 +10794,18 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
user_config=_hyg_data if isinstance(_hyg_data, dict) else None,
|
||||
)
|
||||
if _hyg_runtime.get("api_key"):
|
||||
# Pass the FULL transcript (tool results included).
|
||||
# Filtering to user/assistant-only starved the
|
||||
# compressor: tool results are usually the bulk of
|
||||
# the context, _prune_old_tool_results never saw
|
||||
# them, and short filtered histories tripped the
|
||||
# protect-first/last early-return so nothing was
|
||||
# compressed at all (#3854). The agent loop passes
|
||||
# its full message list to _compress_context — the
|
||||
# gateway now matches.
|
||||
_hyg_msgs = [
|
||||
{"role": m.get("role"), "content": m.get("content")}
|
||||
for m in history
|
||||
if m.get("role") in {"user", "assistant"}
|
||||
and m.get("content")
|
||||
m for m in history
|
||||
if m.get("role") in {"user", "assistant", "tool"}
|
||||
]
|
||||
|
||||
if len(_hyg_msgs) >= 4:
|
||||
|
|
|
|||
|
|
@ -3140,10 +3140,14 @@ class GatewaySlashCommandsMixin:
|
|||
if not runtime_kwargs.get("api_key"):
|
||||
return t("gateway.compress.no_provider")
|
||||
|
||||
# Pass the FULL transcript (tool results included) — same
|
||||
# rationale as the session-hygiene auto-compress in
|
||||
# gateway/run.py (#3854): filtering to user/assistant-only
|
||||
# starves the compressor's tool-result pruning and can trip the
|
||||
# protect-first/last early-return on short filtered histories.
|
||||
msgs = [
|
||||
{"role": m.get("role"), "content": m.get("content")}
|
||||
for m in history
|
||||
if m.get("role") in {"user", "assistant"} and m.get("content")
|
||||
m for m in history
|
||||
if m.get("role") in {"user", "assistant", "tool"}
|
||||
]
|
||||
|
||||
# Boundary-aware split: only the head is summarized; the most
|
||||
|
|
|
|||
|
|
@ -483,3 +483,51 @@ async def test_compress_command_overrides_stale_resolver_identity():
|
|||
# Source-derived identity overrides the stale resolver values, passed once.
|
||||
assert kwargs["platform"] == "telegram"
|
||||
assert kwargs["gateway_session_key"] == runner._session_key_for_source(_make_source())
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_compress_command_passes_tool_messages_to_compressor():
|
||||
"""Tool results must reach _compress_context (#3854).
|
||||
|
||||
Filtering the transcript to user/assistant-only starved the
|
||||
compressor's tool-result pruning — tool messages are usually the bulk
|
||||
of the context.
|
||||
"""
|
||||
history = [
|
||||
{"role": "user", "content": "run it"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [{"id": "t1", "type": "function",
|
||||
"function": {"name": "x", "arguments": "{}"}}],
|
||||
},
|
||||
{"role": "tool", "content": "BIG RESULT " * 50, "tool_call_id": "t1"},
|
||||
{"role": "assistant", "content": "done"},
|
||||
{"role": "user", "content": "thanks"},
|
||||
{"role": "assistant", "content": "np"},
|
||||
]
|
||||
runner = _make_runner(history)
|
||||
agent_instance = MagicMock()
|
||||
agent_instance.shutdown_memory_provider = MagicMock()
|
||||
agent_instance.close = MagicMock()
|
||||
agent_instance._cached_system_prompt = ""
|
||||
agent_instance.tools = None
|
||||
agent_instance.context_compressor.has_content_to_compress.return_value = True
|
||||
agent_instance.session_id = "sess-1"
|
||||
agent_instance._compress_context.return_value = (list(history), "")
|
||||
|
||||
with (
|
||||
patch("gateway.run._resolve_runtime_agent_kwargs", return_value={"api_key": "test-key"}),
|
||||
patch("gateway.run._resolve_gateway_model", return_value="test-model"),
|
||||
patch("run_agent.AIAgent", return_value=agent_instance),
|
||||
patch("agent.model_metadata.estimate_request_tokens_rough", return_value=100),
|
||||
):
|
||||
await runner._handle_compress_command(_make_event())
|
||||
|
||||
args, _kwargs = agent_instance._compress_context.call_args
|
||||
passed = args[0]
|
||||
roles = [m.get("role") for m in passed]
|
||||
assert "tool" in roles, f"tool messages filtered out: {roles}"
|
||||
# Assistant tool_calls stubs (content=None) must survive too, or the
|
||||
# tool message would dangle without its call.
|
||||
assert any(m.get("tool_calls") for m in passed), "assistant tool_calls stub dropped"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue