Skip to content

DRAFT: feat: add ParallelToolExecutor and ToolExecutorSemaphore for parallel tool execution#2390

Draft
VascoSch92 wants to merge 1 commit intomainfrom
parallel-tool-execution
Draft

DRAFT: feat: add ParallelToolExecutor and ToolExecutorSemaphore for parallel tool execution#2390
VascoSch92 wants to merge 1 commit intomainfrom
parallel-tool-execution

Conversation

@VascoSch92
Copy link
Contributor

@VascoSch92 VascoSch92 commented Mar 11, 2026

Summary

This PR adds infrastructure for executing multiple tool calls concurrently with a configurable global concurrency limit, as proposed in #2350.

Classes Added

ToolExecutorSemaphore

Process-global singleton that limits concurrent tool executions across all agents and sub-agents.

  • Singleton via __new__: ToolExecutorSemaphore() always returns the same instance
  • Configured via environment variable: OPENHANDS_TOOL_CONCURRENCY_LIMIT (default: 8)
  • Context manager support: with semaphore: ...

ParallelToolExecutor

Executes batches of tool calls concurrently using ThreadPoolExecutor.

  • Takes a sequence of ActionEvent objects and a tool_runner callable
  • Returns results in original order regardless of completion order
  • Uses the singleton semaphore to control actual concurrency

Design Decisions

Single layer of concurrency control

We use only OPENHANDS_TOOL_CONCURRENCY_LIMIT to control concurrency. The ThreadPoolExecutor is sized to the batch, but the semaphore gates actual execution. This keeps configuration simple.

Singleton pattern with __new__

The semaphore uses __new__ for the singleton pattern, so anyone instantiating ToolExecutorSemaphore() gets the same instance. No get_instance() method needed.

Why ThreadPoolExecutor?

Tool execution is I/O-bound (terminal commands, file operations, MCP network calls). Python's GIL is released during I/O, so threads work well. Also follows existing patterns in base.py:322-339.

No reset() method exposed

For testing, we directly reset class attributes via pytest fixtures rather than exposing a public reset method.

Integration Points (not in this PR)

This PR provides the infrastructure. Integration requires changes to:

  1. Agent._execute_actions() - Use ParallelToolExecutor instead of sequential loop
  2. DelegateExecutor._delegate_tasks() - Wrap sub-agent execution with semaphore to prevent concurrency explosion when delegating to multiple sub-agents

Concurrency explosion scenario (from issue #2350):

Parent Agent
  └── delegate(tasks={A, B, C})           → 3 sub-agent threads
      ├── Sub-agent A: step() → 4 tools   → 4 threads
      ├── Sub-agent B: step() → 4 tools   → 4 threads  
      └── Sub-agent C: step() → 4 tools   → 4 threads
                                           → 15 concurrent threads

With the semaphore (e.g., OPENHANDS_TOOL_CONCURRENCY_LIMIT=8), max 8 tool executions run at once across all agents.

Usage

export OPENHANDS_TOOL_CONCURRENCY_LIMIT=4
executor = ParallelToolExecutor()
results = executor.execute_batch(action_events, tool_runner)
# results[i] corresponds to action_events[i]

Tests

13 tests covering:

  • Singleton pattern
  • Environment variable configuration (valid, invalid, negative values)
  • Context manager
  • Concurrency limiting
  • Result ordering
  • Exception propagation

Related to #2350


Agent Server images for this PR

GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server

Variants & Base Images

Variant Architectures Base Image Docs / Tags
java amd64, arm64 eclipse-temurin:17-jdk Link
python amd64, arm64 nikolaik/python-nodejs:python3.13-nodejs22 Link
golang amd64, arm64 golang:1.21-bookworm Link

Pull (multi-arch manifest)

# Each variant is a multi-arch manifest supporting both amd64 and arm64
docker pull ghcr.io/openhands/agent-server:0250777-python

Run

docker run -it --rm \
  -p 8000:8000 \
  --name agent-server-0250777-python \
  ghcr.io/openhands/agent-server:0250777-python

All tags pushed for this build

ghcr.io/openhands/agent-server:0250777-golang-amd64
ghcr.io/openhands/agent-server:0250777-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:0250777-golang-arm64
ghcr.io/openhands/agent-server:0250777-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:0250777-java-amd64
ghcr.io/openhands/agent-server:0250777-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:0250777-java-arm64
ghcr.io/openhands/agent-server:0250777-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:0250777-python-amd64
ghcr.io/openhands/agent-server:0250777-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-amd64
ghcr.io/openhands/agent-server:0250777-python-arm64
ghcr.io/openhands/agent-server:0250777-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-arm64
ghcr.io/openhands/agent-server:0250777-golang
ghcr.io/openhands/agent-server:0250777-java
ghcr.io/openhands/agent-server:0250777-python

About Multi-Architecture Support

  • Each variant tag (e.g., 0250777-python) is a multi-arch manifest supporting both amd64 and arm64
  • Docker automatically pulls the correct architecture for your platform
  • Individual architecture tags (e.g., 0250777-python-amd64) are also available if needed

… tool execution

Add infrastructure for executing multiple tool calls concurrently with a
configurable global concurrency limit.

Classes:
- ToolExecutorSemaphore: Process-global singleton that limits concurrent
  tool executions across all agents and sub-agents. Configured via
  OPENHANDS_TOOL_CONCURRENCY_LIMIT environment variable (default: 8).
- ParallelToolExecutor: Executes batches of tool calls concurrently using
  ThreadPoolExecutor, with concurrency controlled by the semaphore.

Key design decisions:
- Single layer of concurrency control via environment variable
- Singleton pattern using __new__ for ToolExecutorSemaphore
- ThreadPoolExecutor for I/O-bound tool execution
- Results returned in original order regardless of completion order

Related to #2350

Co-authored-by: openhands <openhands@all-hands.dev>
@github-actions
Copy link
Contributor

API breakage checks (Griffe)

Result: Passed

Action log

@github-actions
Copy link
Contributor

Agent server REST API breakage checks (OpenAPI)

Result: Failed

Log excerpt (first 1000 characters)
{"asctime": "2026-03-11 19:41:56,572", "levelname": "WARNING", "name": "openhands.agent_server.config", "filename": "config.py", "lineno": 173, "message": "\u26a0\ufe0f OH_SECRET_KEY was not defined. Secrets will not be persisted between restarts."}
::error title=openhands-agent-server REST API::Breaking REST API change detected without MINOR version bump (1.13.0 -> 1.13.0).

Breaking REST API changes detected compared to baseline release:
- the 'file' request property type/format changed from 'string'/'' to 'string'/'binary'
/home/runner/work/software-agent-sdk/software-agent-sdk/.venv/lib/python3.13/site-packages/litellm/llms/custom_httpx/async_client_cleanup.py:66: DeprecationWarning: There is no current event loop
  loop = asyncio.get_event_loop()

Action log

@github-actions
Copy link
Contributor

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-sdk/openhands/sdk/agent
   parallel_executor.py74593%108, 112, 192, 195, 198
TOTAL20004580470% 

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants