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"})