From 4714b5dcb9fc9e27d5ce2d62c67d6a7da55ccf94 Mon Sep 17 00:00:00 2001 From: Alessandro <155005371+3clyp50@users.noreply.github.com> Date: Tue, 16 Jun 2026 13:27:59 +0200 Subject: [PATCH] Block document query in parallel calls Reject document_query during parallel tool-call normalization before worker jobs are started. Add prompt guidance so the model avoids batching document_query, and update DOX plus regressions for the new sequential-only contract. --- helpers/parallel_tools.py | 5 +++++ helpers/parallel_tools.py.dox.md | 1 + prompts/agent.system.tool.parallel.md | 1 + tests/test_parallel_tool.py | 12 ++++++++++++ tests/test_tool_request_normalization.py | 1 + tools/parallel.py.dox.md | 1 + 6 files changed, 21 insertions(+) diff --git a/helpers/parallel_tools.py b/helpers/parallel_tools.py index 48bd08f97..7412e48cd 100644 --- a/helpers/parallel_tools.py +++ b/helpers/parallel_tools.py @@ -31,6 +31,7 @@ CHILD_PARALLEL_TOOL_NAME_KEY = "parallel_tool_name" DEFAULT_MAX_CALLS = 8 DEFAULT_TIMEOUT_SECONDS = 300 POLL_INTERVAL_SECONDS = 0.5 +DISALLOWED_PARALLEL_TOOLS = {"document_query"} TERMINAL_STATES = {"success", "error", "cancelled", "timeout"} JobState = Literal["pending", "running", "success", "error", "cancelled", "timeout"] @@ -93,6 +94,10 @@ def normalize_parallel_tool_calls(raw_calls: Any) -> list[NormalizedToolCall]: if tool_name == "parallel": raise ValueError("`parallel` cannot be nested inside another `parallel` call.") + if tool_name in DISALLOWED_PARALLEL_TOOLS: + raise ValueError( + f"`{tool_name}` cannot be used inside `parallel`; call it sequentially." + ) calls.append( NormalizedToolCall( diff --git a/helpers/parallel_tools.py.dox.md b/helpers/parallel_tools.py.dox.md index 9ce49d53b..cd73ee32f 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. +- 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`. - 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 44cbf97aa..b2bdd9909 100644 --- a/prompts/agent.system.tool.parallel.md +++ b/prompts/agent.system.tool.parallel.md @@ -8,6 +8,7 @@ Batch all independent calls that are ready now into one `tool_calls` list, even Rules: - 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` +- Never include `document_query` in `tool_calls`; it is too heavy for parallel workers, so call it sequentially. - `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` - if extras list running or ready parallel jobs, collect them before final synthesis diff --git a/tests/test_parallel_tool.py b/tests/test_parallel_tool.py index d0e9d30cc..b23974d77 100644 --- a/tests/test_parallel_tool.py +++ b/tests/test_parallel_tool.py @@ -110,6 +110,18 @@ def test_normalize_parallel_tool_calls_rejects_nested_parallel() -> None: ) +def test_normalize_parallel_tool_calls_rejects_document_query() -> None: + with pytest.raises(ValueError, match="document_query.*parallel"): + parallel_tools.normalize_parallel_tool_calls( + [ + { + "tool_name": "document_query", + "tool_args": {"document": "/tmp/report.pdf"}, + } + ] + ) + + @pytest.mark.asyncio async def test_parallel_jobs_extras_lists_running_and_ready_jobs() -> None: agent = _FakeAgent() diff --git a/tests/test_tool_request_normalization.py b/tests/test_tool_request_normalization.py index 403baab4e..6b4ce1c4e 100644 --- a/tests/test_tool_request_normalization.py +++ b/tests/test_tool_request_normalization.py @@ -93,3 +93,4 @@ def test_parallel_prompt_encourages_mixed_independent_batches() -> None: 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 + assert "Never include `document_query`" in prompt diff --git a/tools/parallel.py.dox.md b/tools/parallel.py.dox.md index 988a75e05..b7cc19fc9 100644 --- a/tools/parallel.py.dox.md +++ b/tools/parallel.py.dox.md @@ -22,6 +22,7 @@ - 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. +- `document_query` is intentionally excluded from wrapped calls because it is too heavy for parallel workers and must be called sequentially. - `action="start"` starts calls and optionally waits according to `wait`. - `action="await"` waits for requested job IDs until completion or `timeout`; timeout returns running job handles without canceling them. - `action="collect"` returns completed job results without waiting.