Repository navigation
Description
Feature hasn't been suggested before.
- I have verified this feature I'm about to request hasn't been suggested before.
Describe the enhancement you want to request
Request: a configurable cap on how many tool calls may be executing simultaneously within one step of one agent session. Excess calls queue; the default stays unbounded.
Primary benefit: bounding instantaneous execution pressure — on the filesystem, spawned subprocesses, and remote MCP servers — when a model emits a large burst of tool calls in a single response.
Current behavior
-
The system prompt pushes toward maximal parallelism. meta.txt (dev,
fe3f3a4) L47–L50 (the v2.0.12 release carries the same wording in system-prompt/meta.txt L41–L44):Always make tool calls in parallel if you intend to call multiple tools and there are no dependencies between them. Maximize use of parallel tool calls where possible to increase efficiency.
-
The execution layer forks every tool call immediately, with no bound.
- In the release I run, v2.0.12 (
2670273): step.ts L118–L129 — eachtool-callevent isEffect.forkScopedintotoolRunsas it arrives, then all are awaited viaFiber.awaitAll(L143). No semaphore or permit guards the fork; whatever the model emits in one step all start at once. - On current dev (
fe3f3a4): dispatch moved to llm.ts L278, where each tool call is piped into aFiberSet— same unbounded pattern. (The onlySemaphorein that file serializes event publication, not tool execution.)
- In the release I run, v2.0.12 (
-
Code Mode already has a concurrency cap; the ordinary tool path has no equivalent. On dev:
TOOL_CALL_CONCURRENCY = 8, enforced by a semaphore over eagerly forked tool calls (runtime.ts L628).
Why it matters
- Users have reported pathological bursts. opencode-go kimi-k3 intermittently emits raw <|...|> chat-template delimiters and pathological parallel tool-call bursts #46411 describes one assistant message carrying 100 identical tool calls and the next one 97 more. A cap doesn't stop the model from emitting them, but it bounds how many actually execute at once.
- Real resources absorb the whole burst simultaneously. A single response with hundreds of
read/bash/MCP calls means hundreds of concurrent file reads, hundreds of spawned subprocesses, and a thundering herd against a remote MCP server — all starting in the same instant. - There is appetite for concurrency controls. [FEATURE]: Setting to limit max number of parallel subagents #27110 requests a limit on parallel subagents (local-model users, 5 comments of +1). That's a different layer — per-session fan-out vs. per-step tool calls — but the same direction.
What this would NOT do (scope)
- Not a fix for accumulated context. Capping in-flight execution does not bound context growth, and would not by itself prevent provider image-count errors like [BUG] Agent accumulated 51 images via read tool, session bricked by provider 50-image count limit with no recovery #47487 / pdf: multiple PDF reads accumulate images exceeding provider limit, causing unrecoverable error #48781: several reads each adding an image still accumulate across a completed step even at concurrency 1.
- Not a rate limit. This bounds how many calls run simultaneously, not calls per minute. A queued burst still executes, just throttled.
Proposed behavior
{
// Max tool calls executing concurrently within one step of one session.
// Unset or 0 = current behavior (unbounded).
"tool": { "concurrency": 8 }
}- Default unchanged (unset → unbounded), so existing workflows are unaffected.
- Excess calls queue; the model's response is not rejected.
- Field name/shape at maintainer discretion — the ask is a configurable execution cap on the ordinary tool path.
Counting scope (proposal, open for maintainer input):
- The budget covers the tool calls emitted in a single model response, for that session's current step.
- Built-in and MCP tools share the budget.
- A
subagent/taskcall counts as one call; the child session runs its own step loop with its own budget. Cross-session fan-out is out of scope here (that's [FEATURE]: Setting to limit max number of parallel subagents #27110's territory). - Code Mode's
executecounts as one outer call; its internal tool invocations keep using Code Mode's ownTOOL_CALL_CONCURRENCYsemaphore. Per-layer budgets mean a nested call never waits on its parent's budget — this is the intended guard against nested-wait deadlock. - Failed calls release their permit immediately; cancelling/interrupting the step drains the queue so queued calls never start.
Concrete acceptance example:
- One model response emits 20 tool calls with
tool.concurrency = 3→ at most 3 execute concurrently; the other 17 queue and start as permits free up. - After 5 calls have completed, with 3 running and 12 queued, cancelling the step prevents all 12 queued calls from starting.
- One of the 3 running calls fails → its permit is released to a queued call immediately.
- Negative or non-integer config values are rejected at config load.
Related
- [FEATURE]: Setting to limit max number of parallel subagents #27110 — parallel subagent limit (complementary, different layer)
- opencode-go kimi-k3 intermittently emits raw <|...|> chat-template delimiters and pathological parallel tool-call bursts #46411 — reported bursts of identical parallel tool calls
- [BUG] Agent accumulated 51 images via read tool, session bricked by provider 50-image count limit with no recovery #47487, pdf: multiple PDF reads accumulate images exceeding provider limit, causing unrecoverable error #48781 — context/provider-limit failures from accumulation (listed for contrast; not solved by this proposal)
Environment
- opencode version: v2.0.12
- OS: Linux 6.17.0-41-generic (linux x64)
- Shell: /usr/bin/zsh
- Install/channel: latest
- Active plugins: none found in
~/.config/opencode/opencode.json
Activity
opencodejr commentedon Oct 28, 2026
Thanks for the detailed proposal, @bioShaun! The request is for a configurable cap on how many ordinary tool calls run at once within one step (for example
tool.concurrency). Extra calls would queue, and the default would stay unbounded. Maintainers will consider it.On current
dev, the session runner still starts every tool call from a response as soon as it arrives, with no bound. Only Code Mode has its own cap (TOOL_CALL_CONCURRENCY = 8), so this would be a new option, not a fix.A plugin can't do this reliably today. A plugin could take a permit in
tool.execute.beforeand release it intool.execute.after. Butexecute.afterdoesn't appear to fire when a call is interrupted, so cancelling a step could leak permits. An interruption-safe wrapping or finalizer hook for tool execution in the plugin API would make this possible as a plugin.Related issues:
The issue search tool was unavailable while I looked at this, so I couldn't check for an existing request for the same thing.