fix: normalize tool_call_id whitespace in sanitizer

_sanitize_api_messages() compared raw tool_call_id strings without
stripping whitespace. When assistant-side IDs and tool-result IDs
diverged due to surrounding whitespace, valid tool results were treated
as orphaned and replaced with [Result unavailable] stub placeholders.

Strip whitespace in _get_tool_call_id_static() (both call_id/id paths,
dict and object) and at the two result_call_id comparison sites in
sanitize_api_messages(). Adds regression tests for preserved-whitespace
results and orphaned-whitespace removal.

Closes #9999
This commit is contained in:
nightq 2026-06-30 01:22:02 -07:00 committed by Teknium
parent f9b619dfae
commit fa3ab2ffd0
3 changed files with 28 additions and 4 deletions

View file

@ -2163,7 +2163,7 @@ def sanitize_api_messages(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]
result_call_ids: set = set()
for msg in messages:
if msg.get("role") == "tool":
cid = msg.get("tool_call_id")
cid = (msg.get("tool_call_id") or "").strip()
if cid:
result_call_ids.add(cid)
@ -2172,7 +2172,7 @@ def sanitize_api_messages(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]
if orphaned_results:
messages = [
m for m in messages
if not (m.get("role") == "tool" and m.get("tool_call_id") in orphaned_results)
if not (m.get("role") == "tool" and (m.get("tool_call_id") or "").strip() in orphaned_results)
]
_ra().logger.debug(
"Pre-call sanitizer: removed %d orphaned tool result(s)",

View file

@ -3429,8 +3429,8 @@ class AIAgent:
def _get_tool_call_id_static(tc) -> str:
"""Extract call ID from a tool_call entry (dict or object)."""
if isinstance(tc, dict):
return tc.get("call_id", "") or tc.get("id", "") or ""
return getattr(tc, "call_id", "") or getattr(tc, "id", "") or ""
return (tc.get("call_id", "") or tc.get("id", "") or "").strip()
return (getattr(tc, "call_id", "") or getattr(tc, "id", "") or "").strip()
@staticmethod
def _get_tool_call_name_static(tc) -> str:

View file

@ -108,6 +108,30 @@ class TestSanitizeApiMessages:
assert len(out) == 2
assert out[1]["tool_call_id"] == "c6"
def test_tool_result_with_leading_whitespace_preserved(self):
"""Tool result IDs with leading/trailing whitespace should match assistant call IDs."""
msgs = [
{"role": "assistant", "tool_calls": [assistant_dict_call("functions.cronjob:24")]},
tool_result(" functions.cronjob:24"), # leading whitespace
]
out = AIAgent._sanitize_api_messages(msgs)
# Should NOT inject a stub — the tool result is valid after stripping
assert len(out) == 2
assert out[1]["role"] == "tool"
assert out[1]["content"] == "ok"
def test_truly_orphaned_with_whitespace_still_removed(self):
"""Truly orphaned tool results with whitespace should still be removed."""
msgs = [
{"role": "assistant", "tool_calls": [assistant_dict_call("c_valid")]},
tool_result(" c_ORPHAN "), # whitespace + no matching call
]
out = AIAgent._sanitize_api_messages(msgs)
assert len(out) == 2 # assistant + stub for c_valid, orphan removed
tool_msgs = [m for m in out if m["role"] == "tool"]
assert len(tool_msgs) == 1
assert tool_msgs[0]["tool_call_id"] == "c_valid"
# ---------------------------------------------------------------------------
# Phase 2a — _cap_delegate_task_calls