{"repo":"NousResearch/hermes-agent","public_id":"9999","display_label":"Sanitizer misclassifies tool results due to whitespace in tool_call_id","summary":"These issues and fixes address a bug where the run_agent sanitizer incorrectly classifies tool results when the tool_call_id contains surrounding whitespace. The proposed fixes involve normalizing or stripping whitespace from tool_call_id before performing comparisons.","subsystem":"agent","counts":{"members":4,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":55519,"merged_at":"2026-06-30T08:43:40+00:00"},"resolution":{"state":"resolved","by":"pr-55519"},"freshness":{"state":"verified","last_verified_at":"2026-06-30T08:43:40+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.5},"fix_edges":[{"issue_number":9999,"pr_number":55519,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-06-30T08:43:40+00:00"}],"members":[{"kind":"issue","number":9999,"title":"run_agent sanitizer misclassifies tool results when tool_call_id has surrounding whitespace","url":null,"state":"closed"},{"kind":"pr","number":10000,"title":"fix(run-agent): normalize tool_call_id before matching results","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"fa3ab2ffd0f18069b8ef10117d4e1e4e4c5a9bd8","number":0},{"kind":"pr","value":"#10039","number":10039},{"kind":"pr","value":"#9999","number":9999}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":10432,"title":"fix(agent): strip whitespace from tool_call_id in sanitizer","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#10119","number":10119}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":55519,"title":"fix: normalize tool_call_id whitespace in sanitizer","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/agent_runtime_helpers.py","run_agent.py","tests/run_agent/test_agent_guardrails.py"],"fix_hunks":[{"file":"agent/agent_runtime_helpers.py","hunks":["@@ -2163,7 +2163,7 @@ def sanitize_api_messages(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]\n     result_call_ids: set = set()\n     for msg in messages:\n         if msg.get(\"role\") == \"tool\":\n-            cid = msg.get(\"tool_call_id\")\n+            cid = (msg.get(\"tool_call_id\") or \"\").strip()\n             if cid:\n                 result_call_ids.add(cid)\n ","@@ -2172,7 +2172,7 @@ def sanitize_api_messages(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]\n     if orphaned_results:\n         messages = [\n             m for m in messages\n-            if not (m.get(\"role\") == \"tool\" and m.get(\"tool_call_id\") in orphaned_results)\n+            if not (m.get(\"role\") == \"tool\" and (m.get(\"tool_call_id\") or \"\").strip() in orphaned_results)\n         ]\n         _ra().logger.debug(\n             \"Pre-call sanitizer: removed %d orphaned tool result(s)\","]},{"file":"run_agent.py","hunks":["@@ -3429,8 +3429,8 @@ def _build_system_prompt(self, system_message: str = None) -> str:\n     def _get_tool_call_id_static(tc) -> str:\n         \"\"\"Extract call ID from a tool_call entry (dict or object).\"\"\"\n         if isinstance(tc, dict):\n-            return tc.get(\"call_id\", \"\") or tc.get(\"id\", \"\") or \"\"\n-        return getattr(tc, \"call_id\", \"\") or getattr(tc, \"id\", \"\") or \"\"\n+            return (tc.get(\"call_id\", \"\") or tc.get(\"id\", \"\") or \"\").strip()\n+        return (getattr(tc, \"call_id\", \"\") or getattr(tc, \"id\", \"\") or \"\").strip()\n \n     @staticmethod\n     def _get_tool_call_name_static(tc) -> str:"]},{"file":"tests/run_agent/test_agent_guardrails.py","hunks":["@@ -108,6 +108,30 @@ def test_sdk_object_tool_calls(self):\n         assert len(out) == 2\n         assert out[1][\"tool_call_id\"] == \"c6\"\n \n+    def test_tool_result_with_leading_whitespace_preserved(self):\n+        \"\"\"Tool result IDs with leading/trailing whitespace should match assistant call IDs.\"\"\"\n+        msgs = [\n+            {\"role\": \"assistant\", \"tool_calls\": [assistant_dict_call(\"functions.cronjob:24\")]},\n+            tool_result(\" functions.cronjob:24\"),  # leading whitespace\n+        ]\n+        out = AIAgent._sanitize_api_messages(msgs)\n+        # Should NOT inject a stub — the tool result is valid after stripping\n+        assert len(out) == 2\n+        assert out[1][\"role\"] == \"tool\"\n+        assert out[1][\"content\"] == \"ok\"\n+\n+    def test_truly_orphaned_with_whitespace_still_removed(self):\n+        \"\"\"Truly orphaned tool results with whitespace should still be removed.\"\"\"\n+        msgs = [\n+            {\"role\": \"assistant\", \"tool_calls\": [assistant_dict_call(\"c_valid\")]},\n+            tool_result(\" c_ORPHAN \"),  # whitespace + no matching call\n+        ]\n+        out = AIAgent._sanitize_api_messages(msgs)\n+        assert len(out) == 2  # assistant + stub for c_valid, orphan removed\n+        tool_msgs = [m for m in out if m[\"role\"] == \"tool\"]\n+        assert len(tool_msgs) == 1\n+        assert tool_msgs[0][\"tool_call_id\"] == \"c_valid\"\n+\n \n # ---------------------------------------------------------------------------\n # Phase 2a — _cap_delegate_task_calls\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}