From c7dc81c0c3e11a81e3784893284a3b52431b49eb Mon Sep 17 00:00:00 2001 From: jenkins Date: Sat, 15 Aug 2026 13:35:31 -0300 Subject: [PATCH] hermes: sanitize classifier tool history --- services/hermes/scripts/classifier_broker.py | 28 +++++++++++--------- testing/tests/test_hermes_chat_quality.py | 25 ++++++++++++++++- 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/services/hermes/scripts/classifier_broker.py b/services/hermes/scripts/classifier_broker.py index bf1b0152..ba0d64c1 100644 --- a/services/hermes/scripts/classifier_broker.py +++ b/services/hermes/scripts/classifier_broker.py @@ -79,18 +79,20 @@ def _compact_message(message: dict[str, Any], limit: int) -> dict[str, Any]: result = copy.deepcopy(message) if "content" in result: result["content"] = _compact_content(result["content"], limit) - tool_calls = result.get("tool_calls") - if isinstance(tool_calls, list): - kept: list[dict[str, Any]] = [] - for call in tool_calls[-4:]: - if not isinstance(call, dict): - continue - item = copy.deepcopy(call) - function = item.get("function") - if isinstance(function, dict) and isinstance(function.get("arguments"), str): - function["arguments"] = _bounded_text(function["arguments"], 600) - kept.append(item) - result["tool_calls"] = kept + # The local judge only classifies the current boundary. OpenAI-compatible + # Ollama validates assistant tool-call JSON and tool/result pairing before + # inference, so a bounded or selectively retained transcript can become an + # invalid conversation even though its text is sufficient for routing. + # Preserve tool evidence as plain user text and remove protocol metadata. + had_tool_calls = bool(result.pop("tool_calls", None)) + result.pop("tool_call_id", None) + result.pop("name", None) + if result.get("role") == "tool": + result["role"] = "user" + content = result.get("content") + result["content"] = f"[tool evidence]\n{content or ''}" + elif had_tool_calls and not result.get("content"): + result["content"] = "[assistant requested an external tool]" return result @@ -153,6 +155,8 @@ def compact_payload(payload: dict[str, Any]) -> dict[str, Any]: # The judge never needs tools or binary inputs. Switchyard supplies a # response schema separately, and that contract must remain untouched. result.pop("tools", None) + result.pop("tool_choice", None) + result.pop("parallel_tool_calls", None) return result diff --git a/testing/tests/test_hermes_chat_quality.py b/testing/tests/test_hermes_chat_quality.py index c4ab80f3..78c918fb 100644 --- a/testing/tests/test_hermes_chat_quality.py +++ b/testing/tests/test_hermes_chat_quality.py @@ -1229,10 +1229,24 @@ def test_classifier_broker_bounds_history_without_losing_routing_intent(monkeypa "role": "user", "content": "Build and verify the Cassandra release safely.", }, - {"role": "assistant", "content": "working"}, + { + "role": "assistant", + "content": None, + "tool_calls": [ + { + "id": "call_large", + "type": "function", + "function": { + "name": "large_tool", + "arguments": '{"command":"' + ("x" * 5000) + '"}', + }, + } + ], + }, { "role": "tool", "content": "unbounded test output " * 10000, + "tool_call_id": "call_large", }, { "role": "user", @@ -1243,6 +1257,8 @@ def test_classifier_broker_bounds_history_without_losing_routing_intent(monkeypa }, ], "tools": [{"type": "function", "function": {"name": "large_tool"}}], + "tool_choice": "auto", + "parallel_tool_calls": True, "response_format": {"type": "json_object"}, } @@ -1252,6 +1268,13 @@ def test_classifier_broker_bounds_history_without_losing_routing_intent(monkeypa assert compacted["model"] == payload["model"] assert compacted["response_format"] == payload["response_format"] assert "tools" not in compacted + assert "tool_choice" not in compacted + assert "parallel_tool_calls" not in compacted + assert all(message.get("role") != "tool" for message in compacted["messages"]) + assert all("tool_call_id" not in message for message in compacted["messages"]) + assert all("tool_calls" not in message for message in compacted["messages"]) + assert "[tool evidence]" in encoded + assert "[assistant requested an external tool]" in encoded assert "Build and verify the Cassandra release safely." in encoded assert "Turn this cat into a cute clown." in encoded assert "image attachment available to the selected worker" in encoded