hermes: sanitize classifier tool history

This commit is contained in:
jenkins 2026-08-15 13:35:31 -03:00
parent 7dbde8e85d
commit c7dc81c0c3
2 changed files with 40 additions and 13 deletions

View File

@ -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

View File

@ -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