Skip to content

[FEATURE]: Configurable concurrency limit for parallel tool calls within a step #50563

Open

Description

@bioShaun

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

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

  2. The execution layer forks every tool call immediately, with no bound.

    • In the release I run, v2.0.12 (2670273): step.ts L118–L129 — each tool-call event is Effect.forkScoped into toolRuns as it arrives, then all are awaited via Fiber.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 a FiberSet — same unbounded pattern. (The only Semaphore in that file serializes event publication, not tool execution.)
  3. 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

What this would NOT do (scope)

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/task call 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 execute counts as one outer call; its internal tool invocations keep using Code Mode's own TOOL_CALL_CONCURRENCY semaphore. 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

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

  1. opencodejr commented on Oct 28, 2026

    @opencodejr
    Contributor

    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.before and release it in tool.execute.after. But execute.after doesn'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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions