fix: run tool calls concurrently in the Python client - #137
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The Python client awaited each tool inside its WebSocket read loop, so it never read the next request until the current tool finished. Code fanning out with
Promise.alltook the sum of its calls instead of the slowest one.Everything above the client was already concurrent — the Deno op is
#[op2(async)], the registry holds no lock across its await, and the server gives each call its own request id and response channel. The requests left the server together and queued at the client.Fix
pctx-py/src/pctx_client/_websocket_client.py:ExecuteToolRequestruns in its own task, so the read loop stays free. Tasks are held in a set (asyncio keeps only weak references) and cancelled as a group on disconnect.asyncio.to_thread— calling one inline blocked the event loop for its whole duration, stalling every other in-flight call behind it.4 × 2s tool under
Promise.all: 8.12s → 2.11s, all four starting at t=0.14.Tests
test_concurrent_tool_calls_run_in_parallelandtest_concurrent_sync_tool_calls_run_in_parallelassert on observed overlap (peak in-flight) rather than wall time, so they fail on serialization rather than on a slow machine. Both verified to fail on the unfixed client (peaked at 1). Full suite: 150 passed.Notes
plans/tool-call-concurrency-controls.md. Deliberately out of scope here.🤖 Generated with Claude Code