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
Alessandro committed
Jun 12, 2026 at 18:32 UTC
6eeddf2f046153f520526da46da4a3c584c74649
4 files changed
+37
-4
helpers/parallel_tools.py.dox.md
+1
@@ -23,6 +23,7 @@
23
24
- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
25
- Wrapped tool-call items must use the same shape as normal tool calls: a tool name plus arguments.
26
+- 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.
27
- `call_subordinate` jobs run in isolated child chat contexts tagged with parent-chat metadata; they must not be added to the scheduler task list.
28
- Direct tool jobs run in isolated background contexts and are blocked from recursively invoking `parallel`.
29
- 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.
prompts/agent.system.tool.parallel.md
+6
-4
@@ -1,10 +1,12 @@
1
### parallel
2
run independent tool calls concurrently, or await/cancel background parallel jobs.
3
4
-Use only for independent work. Each `tool_calls` item is a normal tool request object: `{ "tool_name": "...", "tool_args": { ... } }`.
4
+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": { ... } }`.
5
+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.
6
+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.
7
8
Rules:
7
-- do not use for one simple call, dependent steps, ordered steps, or shared mutable state
9
+- 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
10
- never nest `parallel`
11
- `call_subordinate` inside `parallel` starts an isolated child chat under the parent chat, not a scheduler task
12
- use `wait: false` only when you will collect results later with `job_ids`
@@ -18,8 +20,8 @@ Start and wait:
20
"tool_name": "parallel",
21
"tool_args": {
22
"tool_calls": [
21
- {"tool_name": "call_subordinate", "tool_args": {"message": "Research option A.", "reset": true}},
22
- {"tool_name": "call_subordinate", "tool_args": {"message": "Research option B.", "reset": true}}
23
+ {"tool_name": "call_subordinate", "tool_args": {"message": "Review option A and return key risks.", "reset": true}},
24
+ {"tool_name": "search_engine", "tool_args": {"query": "official API changelog release notes"}}
25
],
26
"wait": true
27
}
tests/test_tool_request_normalization.py
+28
@@ -10,6 +10,7 @@ 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
+from helpers import parallel_tools
14
15
16
def test_normalize_tool_request_accepts_canonical_keys() -> None:
@@ -65,3 +66,30 @@ def test_normalize_tool_request_preserves_explicit_action_over_method() -> None:
66
def test_normalize_tool_request_rejects_missing_args() -> None:
67
with pytest.raises(ValueError, match="tool_args"):
68
normalize_tool_request({"tool_name": "response"})
69
+
70
+
71
+def test_normalize_parallel_tool_calls_accepts_full_agent_reply_shape() -> None:
72
+ calls = parallel_tools.normalize_parallel_tool_calls(
73
+ [
74
+ {
75
+ "thoughts": ["This is independent and ready to run."],
76
+ "headline": "Search Python release notes",
77
+ "tool_name": "search_engine",
78
+ "tool_args": {"query": "latest Python version changelog"},
79
+ }
80
+ ]
81
+ )
82
+
83
+ assert calls[0].tool_name == "search_engine"
84
+ assert calls[0].tool_args == {"query": "latest Python version changelog"}
85
+
86
+
87
+def test_parallel_prompt_encourages_mixed_independent_batches() -> None:
88
+ prompt = (PROJECT_ROOT / "prompts" / "agent.system.tool.parallel.md").read_text(
89
+ encoding="utf-8"
90
+ )
91
+
92
+ assert "same `tool_name` and `tool_args` shape as a top-level reply" in prompt
93
+ assert "planning fields like `thoughts` or `headline` are ignored" in prompt
94
+ assert "even when they use different tools" in prompt
95
+ assert "Do not split by tool type" in prompt
tools/parallel.py.dox.md
+2
@@ -21,6 +21,7 @@
21
- Tool modules must define `helpers.tool.Tool` subclasses and return `helpers.tool.Response` from `execute(...)`.
22
- Wrapped items use the same schema as normal tool calls: a tool name plus arguments.
23
- The tool is intended for independent calls only; dependent operations remain sequential.
24
+- 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.
25
- `action="start"` starts calls and optionally waits according to `wait`.
26
- `action="await"` waits for requested job IDs.
27
- `action="collect"` returns completed job results without waiting.
@@ -31,6 +32,7 @@
32
## Key Concepts
33
34
- `tool_calls`, `calls`, and `items` are accepted aliases for the wrapped call list.
35
+- Wrapped call items can use the same `tool_name`/`tool_args` shape as top-level agent replies; extra planning fields are ignored by normalization.
36
- `job_ids` can be supplied as a string or list when awaiting, collecting, or canceling existing jobs.
37
- The response is compact JSON intended for the model to read and continue with.
38
- Visible child rows are emitted by `helpers/parallel_tools.py` before each background job starts.