Block final responses from parallel calls
Keep response tool calls at the top level so they can end the parent message loop instead of forcing an unnecessary follow-up turn.\n\nDocument the parallel constraint and cover both runtime rejection and prompt guidance.
Alessandro committed
Jul 26, 2026 at 00:57 UTC
a76b0b5b7d5f0fd7ced8bdd0f942d87da4a424af
6 files changed
+10
-6
helpers/parallel_tools.py
+1
-1
@@ -31,7 +31,7 @@ CHILD_PARALLEL_TOOL_NAME_KEY = "parallel_tool_name"
31
DEFAULT_MAX_CALLS = 8
32
DEFAULT_TIMEOUT_SECONDS = 300
33
POLL_INTERVAL_SECONDS = 0.5
34
-DISALLOWED_PARALLEL_TOOLS = {"document_query"}
34
+DISALLOWED_PARALLEL_TOOLS = {"document_query", "response"}
35
36
TERMINAL_STATES = {"success", "error", "cancelled", "timeout"}
37
JobState = Literal["pending", "running", "success", "error", "cancelled", "timeout"]
helpers/parallel_tools.py.dox.md
+1
-1
@@ -25,7 +25,7 @@
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
- `tool_calls` should be an array, but normalization also accepts a valid JSON string encoding of that array to recover provider/model stringification.
28
-- Normalization rejects `document_query` inside `parallel` because document parsing and Q&A fan out into heavier worker/model paths that must run sequentially.
28
+- Normalization rejects `document_query` and `response` inside `parallel`: document parsing and Q&A must run sequentially, while `response` must remain top-level so it can end the message loop.
29
- `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`.
30
- Direct tool jobs run in isolated background contexts and are blocked from recursively invoking `parallel`.
31
- Direct tool background context cleanup removes both the in-memory context and any transient chat folder left on disk.
prompts/agent.system.tool.parallel.md
+1
@@ -9,6 +9,7 @@ Rules:
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
- Never include `document_query` in `tool_calls`; it is too heavy for parallel workers, so call it sequentially.
12
+- Call `response` only as a top-level tool so it ends the message loop; never wrap it inside `parallel.tool_calls`.
13
- `call_subordinate` inside `parallel` starts an isolated child chat under the parent chat, not a scheduler task
14
- use `wait: false` only when you will collect results later with `job_ids`
15
- if extras list running or ready parallel jobs, collect them before final synthesis
tests/test_parallel_tool.py
+5
-4
@@ -145,13 +145,14 @@ def test_normalize_parallel_tool_calls_rejects_nested_parallel() -> None:
145
)
146
147
148
-def test_normalize_parallel_tool_calls_rejects_document_query() -> None:
149
- with pytest.raises(ValueError, match="document_query.*parallel"):
148
+@pytest.mark.parametrize("tool_name", ["document_query", "response"])
149
+def test_normalize_parallel_tool_calls_rejects_disallowed_tools(tool_name: str) -> None:
150
+ with pytest.raises(ValueError, match=rf"{tool_name}.*parallel"):
151
parallel_tools.normalize_parallel_tool_calls(
152
[
153
{
153
- "tool_name": "document_query",
154
- "tool_args": {"document": "/tmp/report.pdf"},
154
+ "tool_name": tool_name,
155
+ "tool_args": {},
156
}
157
]
158
)
tests/test_tool_request_normalization.py
+1
@@ -174,3 +174,4 @@ def test_parallel_prompt_encourages_mixed_independent_batches() -> None:
174
assert "even when they use different tools" in prompt
175
assert "Do not split by tool type" in prompt
176
assert "Never include `document_query`" in prompt
177
+ assert "Call `response` only as a top-level tool" in prompt
tools/parallel.py.dox.md
+1
@@ -23,6 +23,7 @@
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
- `document_query` is intentionally excluded from wrapped calls because it is too heavy for parallel workers and must be called sequentially.
26
+- `response` is excluded because a wrapped response cannot end the parent message loop; final responses must be top-level calls.
27
- `action="start"` starts calls and optionally waits according to `wait`.
28
- `action="await"` waits for requested job IDs until completion or `timeout`; timeout returns running job handles without canceling them.
29
- `action="collect"` returns completed job results without waiting.