Clarify parallel tool-call batching
Some checks are pending
Build And Publish Docker Images / plan (push) Waiting to run
Build And Publish Docker Images / build (push) Blocked by required conditions

- 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
This commit is contained in:
Alessandro 2026-06-12 18:32:34 +02:00
parent b704a0e3f5
commit 6eeddf2f04
4 changed files with 37 additions and 4 deletions

View file

@ -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.

View file

@ -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
}

View file

@ -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

View file

@ -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.