From b11818bfffa6519bc16140d3e6db079a515e87d0 Mon Sep 17 00:00:00 2001 From: Alessandro <155005371+3clyp50@users.noreply.github.com> Date: Fri, 26 Jun 2026 13:47:35 +0200 Subject: [PATCH] Accept JSON-string parallel tool calls Restore parallel normalization tolerance for provider/model outputs that stringify the tool_calls array inside tool_args. The wrapper still expects normal tool-call objects after decoding, so nested parallel and document_query guards continue to run through the same validation path. --- helpers/parallel_tools.py | 7 +++++++ helpers/parallel_tools.py.dox.md | 1 + tests/test_parallel_tool.py | 35 ++++++++++++++++++++++++++++++++ tools/parallel.py.dox.md | 2 +- 4 files changed, 44 insertions(+), 1 deletion(-) diff --git a/helpers/parallel_tools.py b/helpers/parallel_tools.py index 207806018..e9d830bef 100644 --- a/helpers/parallel_tools.py +++ b/helpers/parallel_tools.py @@ -78,6 +78,13 @@ def extract_tool_calls(args: dict[str, Any]) -> Any: def normalize_parallel_tool_calls(raw_calls: Any) -> list[NormalizedToolCall]: + if isinstance(raw_calls, str): + try: + raw_calls = json.loads(raw_calls) + except json.JSONDecodeError as exc: + raise ValueError( + "`tool_calls` must be an array of normal tool-call objects." + ) from exc if not isinstance(raw_calls, list): raise ValueError("`tool_calls` must be an array of normal tool-call objects.") if not raw_calls: diff --git a/helpers/parallel_tools.py.dox.md b/helpers/parallel_tools.py.dox.md index 1194cd081..39e65c4cc 100644 --- a/helpers/parallel_tools.py.dox.md +++ b/helpers/parallel_tools.py.dox.md @@ -24,6 +24,7 @@ - Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together. - Wrapped tool-call items must use the same shape as normal tool calls: a tool name plus arguments. - Normalization accepts full agent-reply-shaped objects when `tool_name` and `tool_args` are present; non-contract planning fields such as `thoughts` or `headline` are ignored. +- `tool_calls` should be an array, but normalization also accepts a valid JSON string encoding of that array to recover provider/model stringification. - Normalization rejects `document_query` inside `parallel` because document parsing and Q&A fan out into heavier worker/model paths that must run sequentially. - `call_subordinate` jobs run in isolated child chat contexts tagged with parent-chat metadata; they must not be added to the scheduler task list and may use normal child-chat tools, including `parallel`. - Direct tool jobs run in isolated background contexts and are blocked from recursively invoking `parallel`. diff --git a/tests/test_parallel_tool.py b/tests/test_parallel_tool.py index 3bd2a35dd..587604aa8 100644 --- a/tests/test_parallel_tool.py +++ b/tests/test_parallel_tool.py @@ -103,6 +103,41 @@ def test_normalize_parallel_tool_calls_accepts_normal_tool_request_shapes() -> N assert calls[1].tool_args == {"method": "list_tasks", "action": "list_tasks"} +def test_normalize_parallel_tool_calls_accepts_json_string_array() -> None: + calls = parallel_tools.normalize_parallel_tool_calls( + json.dumps( + [ + { + "tool_name": "call_subordinate", + "tool_args": { + "profile": "researcher", + "reset": True, + "message": "Research nuclear fusion news in French.", + }, + "headline": "Researching nuclear fusion news in French", + }, + { + "tool_name": "call_subordinate", + "tool_args": { + "profile": "researcher", + "reset": True, + "message": "Research nuclear fusion news in Italian.", + }, + "headline": "Researching nuclear fusion news in Italian", + }, + ] + ) + ) + + assert [call.tool_name for call in calls] == [ + "call_subordinate", + "call_subordinate", + ] + assert calls[0].tool_args["profile"] == "researcher" + assert calls[0].tool_args["reset"] is True + assert calls[1].tool_args["message"] == "Research nuclear fusion news in Italian." + + def test_normalize_parallel_tool_calls_rejects_nested_parallel() -> None: with pytest.raises(ValueError, match="cannot be nested"): parallel_tools.normalize_parallel_tool_calls( diff --git a/tools/parallel.py.dox.md b/tools/parallel.py.dox.md index b7cc19fc9..e2fcdc511 100644 --- a/tools/parallel.py.dox.md +++ b/tools/parallel.py.dox.md @@ -32,7 +32,7 @@ ## Key Concepts -- `tool_calls`, `calls`, and `items` are accepted aliases for the wrapped call list. +- `tool_calls`, `calls`, and `items` are accepted aliases for the wrapped call list; a valid JSON string encoding of the list is tolerated for provider/model recovery. - Wrapped call items can use the same `tool_name`/`tool_args` shape as top-level agent replies; extra planning fields are ignored by normalization. - `job_ids` can be supplied as a string or list when awaiting, collecting, or canceling existing jobs. - The response is compact JSON intended for the model to read and continue with.