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:
parent
f9b619dfae
commit
fa3ab2ffd0
|
|
@ -2163,7 +2163,7 @@ def sanitize_api_messages(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]
|
||||||
result_call_ids: set = set()
|
result_call_ids: set = set()
|
||||||
for msg in messages:
|
for msg in messages:
|
||||||
if msg.get("role") == "tool":
|
if msg.get("role") == "tool":
|
||||||
cid = msg.get("tool_call_id")
|
cid = (msg.get("tool_call_id") or "").strip()
|
||||||
if cid:
|
if cid:
|
||||||
result_call_ids.add(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:
|
if orphaned_results:
|
||||||
messages = [
|
messages = [
|
||||||
m for m in 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(
|
_ra().logger.debug(
|
||||||
"Pre-call sanitizer: removed %d orphaned tool result(s)",
|
"Pre-call sanitizer: removed %d orphaned tool result(s)",
|
||||||
|
|
|
||||||
|
|
@ -3429,8 +3429,8 @@ class AIAgent:
|
||||||
def _get_tool_call_id_static(tc) -> str:
|
def _get_tool_call_id_static(tc) -> str:
|
||||||
"""Extract call ID from a tool_call entry (dict or object)."""
|
"""Extract call ID from a tool_call entry (dict or object)."""
|
||||||
if isinstance(tc, dict):
|
if isinstance(tc, dict):
|
||||||
return tc.get("call_id", "") or tc.get("id", "") or ""
|
return (tc.get("call_id", "") or tc.get("id", "") or "").strip()
|
||||||
return getattr(tc, "call_id", "") or getattr(tc, "id", "") or ""
|
return (getattr(tc, "call_id", "") or getattr(tc, "id", "") or "").strip()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_tool_call_name_static(tc) -> str:
|
def _get_tool_call_name_static(tc) -> str:
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,30 @@ class TestSanitizeApiMessages:
|
||||||
assert len(out) == 2
|
assert len(out) == 2
|
||||||
assert out[1]["tool_call_id"] == "c6"
|
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
|
# Phase 2a — _cap_delegate_task_calls
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue