From 6eeddf2f046153f520526da46da4a3c584c74649 Mon Sep 17 00:00:00 2001 From: Alessandro <155005371+3clyp50@users.noreply.github.com> Date: Fri, 12 Jun 2026 18:32:34 +0200 Subject: [PATCH] Clarify parallel tool-call batching - document that parallel accepts full reply-shaped tool call objects and ignores planning-only fields - steer prompt guidance toward one mixed batch for ready independent calls instead of splitting by tool type - add normalization and prompt regression coverage plus matching DOX notes --- helpers/parallel_tools.py.dox.md | 1 + prompts/agent.system.tool.parallel.md | 10 +++++---- tests/test_tool_request_normalization.py | 28 ++++++++++++++++++++++++ tools/parallel.py.dox.md | 2 ++ 4 files changed, 37 insertions(+), 4 deletions(-) diff --git a/helpers/parallel_tools.py.dox.md b/helpers/parallel_tools.py.dox.md index f3949a678..4f65fed81 100644 --- a/helpers/parallel_tools.py.dox.md +++ b/helpers/parallel_tools.py.dox.md @@ -23,6 +23,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. - `call_subordinate` jobs run in isolated child chat contexts tagged with parent-chat metadata; they must not be added to the scheduler task list. - Direct tool jobs run in isolated background contexts and are blocked from recursively invoking `parallel`. - Parent-visible child log items are created for each wrapped call so the WebUI can inspect concurrent children separately while the wrapper result remains model-history-only. diff --git a/prompts/agent.system.tool.parallel.md b/prompts/agent.system.tool.parallel.md index 53d2d1e64..fd374f29c 100644 --- a/prompts/agent.system.tool.parallel.md +++ b/prompts/agent.system.tool.parallel.md @@ -1,10 +1,12 @@ ### parallel run independent tool calls concurrently, or await/cancel background parallel jobs. -Use only for independent work. Each `tool_calls` item is a normal tool request object: `{ "tool_name": "...", "tool_args": { ... } }`. +Use only for independent work. Each `tool_calls` item is a normal tool request object using the same `tool_name` and `tool_args` shape as a top-level reply: `{ "tool_name": "...", "tool_args": { ... } }`. +Only `tool_name` and `tool_args` are used; if an item is copied from a full reply object, planning fields like `thoughts` or `headline` are ignored. +Batch all independent calls that are ready now into one `tool_calls` list, even when they use different tools. Do not split by tool type. Rules: -- do not use for one simple call, dependent steps, ordered steps, or shared mutable state +- do not use for one simple call, dependent steps, ordered steps, shared mutable state, or state/tool-availability changes that must happen in the parent context - never nest `parallel` - `call_subordinate` inside `parallel` starts an isolated child chat under the parent chat, not a scheduler task - use `wait: false` only when you will collect results later with `job_ids` @@ -18,8 +20,8 @@ Start and wait: "tool_name": "parallel", "tool_args": { "tool_calls": [ - {"tool_name": "call_subordinate", "tool_args": {"message": "Research option A.", "reset": true}}, - {"tool_name": "call_subordinate", "tool_args": {"message": "Research option B.", "reset": true}} + {"tool_name": "call_subordinate", "tool_args": {"message": "Review option A and return key risks.", "reset": true}}, + {"tool_name": "search_engine", "tool_args": {"query": "official API changelog release notes"}} ], "wait": true } diff --git a/tests/test_tool_request_normalization.py b/tests/test_tool_request_normalization.py index 81472c561..403baab4e 100644 --- a/tests/test_tool_request_normalization.py +++ b/tests/test_tool_request_normalization.py @@ -10,6 +10,7 @@ if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) from helpers.extract_tools import normalize_tool_request +from helpers import parallel_tools def test_normalize_tool_request_accepts_canonical_keys() -> None: @@ -65,3 +66,30 @@ def test_normalize_tool_request_preserves_explicit_action_over_method() -> None: def test_normalize_tool_request_rejects_missing_args() -> None: with pytest.raises(ValueError, match="tool_args"): normalize_tool_request({"tool_name": "response"}) + + +def test_normalize_parallel_tool_calls_accepts_full_agent_reply_shape() -> None: + calls = parallel_tools.normalize_parallel_tool_calls( + [ + { + "thoughts": ["This is independent and ready to run."], + "headline": "Search Python release notes", + "tool_name": "search_engine", + "tool_args": {"query": "latest Python version changelog"}, + } + ] + ) + + assert calls[0].tool_name == "search_engine" + assert calls[0].tool_args == {"query": "latest Python version changelog"} + + +def test_parallel_prompt_encourages_mixed_independent_batches() -> None: + prompt = (PROJECT_ROOT / "prompts" / "agent.system.tool.parallel.md").read_text( + encoding="utf-8" + ) + + assert "same `tool_name` and `tool_args` shape as a top-level reply" in prompt + assert "planning fields like `thoughts` or `headline` are ignored" in prompt + assert "even when they use different tools" in prompt + assert "Do not split by tool type" in prompt diff --git a/tools/parallel.py.dox.md b/tools/parallel.py.dox.md index aec0a4ffc..7a25edd5e 100644 --- a/tools/parallel.py.dox.md +++ b/tools/parallel.py.dox.md @@ -21,6 +21,7 @@ - Tool modules must define `helpers.tool.Tool` subclasses and return `helpers.tool.Response` from `execute(...)`. - Wrapped items use the same schema as normal tool calls: a tool name plus arguments. - The tool is intended for independent calls only; dependent operations remain sequential. +- Independent calls should share one batch even when they use different tools; split only for dependencies, ordering, shared mutable state, or parent-context state/tool-availability changes. - `action="start"` starts calls and optionally waits according to `wait`. - `action="await"` waits for requested job IDs. - `action="collect"` returns completed job results without waiting. @@ -31,6 +32,7 @@ ## Key Concepts - `tool_calls`, `calls`, and `items` are accepted aliases for the wrapped call list. +- 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. - Visible child rows are emitted by `helpers/parallel_tools.py` before each background job starts.