Skip to content

Support parallel tool execution in ToolInvoker.invoke_all #112

@Aureliolo

Description

@Aureliolo

Problem

ToolInvoker.invoke_all currently executes tool calls sequentially:

async def invoke_all(self, tool_calls):
    return tuple([await self.invoke(call) for call in tool_calls])

When the LLM returns multiple tool calls (e.g. search + read_file), they wait for each other unnecessarily. This becomes a real bottleneck with I/O-bound tools (HTTP requests, file reads, MCP calls).

Proposed Solution

Use asyncio.TaskGroup (Python 3.11+) for structured concurrency:

async def invoke_all(self, tool_calls, *, max_concurrency: int | None = None):
    calls = list(tool_calls)
    results: list[ToolResult] = [None] * len(calls)  # type: ignore[list-item]

    sem = asyncio.Semaphore(max_concurrency) if max_concurrency else None

    async def _run(idx: int, call: ToolCall) -> None:
        if sem:
            async with sem:
                results[idx] = await self.invoke(call)
        else:
            results[idx] = await self.invoke(call)

    async with asyncio.TaskGroup() as tg:
        for i, call in enumerate(calls):
            tg.create_task(_run(i, call))

    return tuple(results)

Key design points:

  • Results returned in input order regardless of completion order
  • max_concurrency parameter for bounded parallelism
  • max_concurrency=1 preserves current sequential behavior
  • Non-recoverable errors (MemoryError, RecursionError) propagate via ExceptionGroup
  • Recoverable errors captured as ToolResult(is_error=True) without cancelling siblings

Acceptance Criteria

  • invoke_all executes tools concurrently by default
  • max_concurrency parameter for bounded parallelism
  • max_concurrency=1 preserves current sequential behavior
  • Results are returned in input order regardless of completion order
  • Non-recoverable errors propagate correctly
  • Recoverable errors don't cancel other in-flight tool calls
  • Tests for concurrent, bounded, and sequential modes
  • Verify DESIGN_SPEC.md §11.1.1 and CLAUDE.md async concurrency convention remain consistent after implementation

Metadata

Metadata

Assignees

No one assigned

    Labels

    prio:highImportant, should be prioritizedscope:smallLess than 1 day of workspec:toolsDESIGN_SPEC Section 11 - Tool & Capability Systemtype:featureNew feature implementation

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions