fix: normalize tool request fallback fields

Alessandro committed Apr 27, 2026 at 19:24 UTC b782d40c3a1b8e64ad9264b98023058c62951a1a
3 files changed +62 -16
agent.py
+9 -14
@@ -872,18 +872,20 @@ class Agent:
872 # search for tool usage requests in agent message
873 tool_request = extract_tools.json_parse_dirty(msg)
874
875 + raw_tool_name = ""
876 + tool_args = {}
877 +
878 # Only validate when extraction produced an object; None means no JSON tool
876 - # block was found — the misformat warning path below handles that.
879 + # block was found - the misformat warning path below handles that.
880 if tool_request is not None:
881 try:
879 - await self.validate_tool_request(tool_request)
882 + raw_tool_name, tool_args = extract_tools.normalize_tool_request(
883 + tool_request
884 + )
885 except ValueError:
886 tool_request = None # treat structural validation errors as misformat
887
888 if tool_request is not None:
884 - raw_tool_name = tool_request.get("tool_name", tool_request.get("tool","")) # Get the raw tool name
885 - tool_args = tool_request.get("tool_args", tool_request.get("args", {}))
886 -
889 tool_name = raw_tool_name # Initialize tool_name with raw_tool_name
890 tool_method = None # Initialize tool_method
891
@@ -977,14 +979,7 @@ class Agent:
979
980 @extension.extensible
981 async def validate_tool_request(self, tool_request: Any):
980 - if not isinstance(tool_request, dict):
981 - raise ValueError("Tool request must be a dictionary")
982 - tool_name = tool_request.get("tool_name") or tool_request.get("tool")
983 - if not tool_name or not isinstance(tool_name, str):
984 - raise ValueError("Tool request must have a tool_name (type string) field")
985 - tool_args = tool_request.get("tool_args", tool_request.get("args"))
986 - if tool_args is None or not isinstance(tool_args, dict):
987 - raise ValueError("Tool request must have a tool_args (type dictionary) field")
982 + extract_tools.normalize_tool_request(tool_request)
983
984
985
@@ -1049,4 +1044,4 @@ class Agent:
1044 message=message,
1045 loop_data=loop_data,
1046 **kwargs,
1052 - )
\ No newline at end of file
1047 + )
helpers/extract_tools.py
+17 -2
@@ -19,6 +19,23 @@ def json_parse_dirty(json: str) -> dict[str, Any] | None:
19 return None
20 return None
21
22 +
23 +def normalize_tool_request(tool_request: Any) -> tuple[str, dict]:
24 + if not isinstance(tool_request, dict):
25 + raise ValueError("Tool request must be a dictionary")
26 + tool_name = tool_request.get("tool_name")
27 + if not tool_name or not isinstance(tool_name, str):
28 + tool_name = tool_request.get("tool")
29 + if not tool_name or not isinstance(tool_name, str):
30 + raise ValueError("Tool request must have a tool_name (type string) field")
31 + tool_args = tool_request.get("tool_args")
32 + if not isinstance(tool_args, dict):
33 + tool_args = tool_request.get("args")
34 + if not isinstance(tool_args, dict):
35 + raise ValueError("Tool request must have a tool_args (type dictionary) field")
36 + return tool_name, tool_args
37 +
38 +
39 def extract_json_root_string(content: str) -> str | None:
40 if not content or not isinstance(content, str):
41 return None
@@ -81,5 +98,3 @@ def fix_json_string(json_string):
98 r'(?<=: ")(.*?)(?=")', replace_unescaped_newlines, json_string, flags=re.DOTALL
99 )
100 return fixed_string
84 -
85 -
tests/test_tool_request_normalization.py new
+36
@@ -0,0 +1,36 @@
1 +from __future__ import annotations
2 +
3 +import sys
4 +from pathlib import Path
5 +
6 +import pytest
7 +
8 +PROJECT_ROOT = Path(__file__).resolve().parents[1]
9 +if str(PROJECT_ROOT) not in sys.path:
10 + sys.path.insert(0, str(PROJECT_ROOT))
11 +
12 +from helpers.extract_tools import normalize_tool_request
13 +
14 +
15 +def test_normalize_tool_request_accepts_fallback_keys() -> None:
16 + assert normalize_tool_request({"tool": "response", "args": {"text": "ok"}}) == (
17 + "response",
18 + {"text": "ok"},
19 + )
20 +
21 +
22 +def test_normalize_tool_request_uses_fallback_when_canonical_name_is_empty() -> None:
23 + assert normalize_tool_request(
24 + {"tool_name": "", "tool": "response", "args": {"text": "ok"}}
25 + ) == ("response", {"text": "ok"})
26 +
27 +
28 +def test_normalize_tool_request_uses_fallback_when_canonical_args_are_invalid() -> None:
29 + assert normalize_tool_request(
30 + {"tool_name": "response", "tool_args": None, "args": {"text": "ok"}}
31 + ) == ("response", {"text": "ok"})
32 +
33 +
34 +def test_normalize_tool_request_rejects_missing_args() -> None:
35 + with pytest.raises(ValueError, match="tool_args"):
36 + normalize_tool_request({"tool_name": "response"})