In the previous lesson, you learned how to run multiple independent conversations concurrently using Promise.all(). That approach optimized execution at the application level by starting multiple agent.run() calls at once. Now, we'll take this optimization one level deeper by parallelizing tool calls within a single conversation turn. When Claude responds with multiple tool_use blocks in a single turn, these tool calls are independent of each other, and there's no reason to wait for one to finish before starting the next. By executing these independent tool calls concurrently, we can significantly reduce the time it takes for the agent to complete complex multi-step tasks.
Let's examine how the current agent processes tool calls. When Claude returns a response with stop_reason === "tool_use", the agent loops through all the tool_use blocks in the response content:
Notice the await this.callTool(contentItem) inside the loop. This means the agent processes each tool call one at a time, waiting for each to complete before starting the next. If Claude requests three tool calls and each takes 1 second, the total time is 3 seconds. Since these tool calls are independent, we could execute them all at once and reduce the total time to approximately 1 second, which is exactly what we'll implement next.
To parallelize tool execution, we'll use the same Promise.all() pattern you learned in the previous lesson, but this time we'll apply it inside the Agent.run() method. The strategy consists of three key steps:
-
Collect tool calls as Promise tasks:
Loop through thetool_useblocks in the response and, instead of awaiting each tool call immediately, collect each as a Promise task. -
Execute all tool tasks concurrently:
UsePromise.all()to run all the collected tool tasks at the same time. -
Gather and add results to the message history:
Once all tool calls complete, gather their results and add them to the message history together.
This approach requires one important change to our callTool method, which we'll tackle first.
Currently, some tools might be synchronous functions, while others might be asynchronous. To use Promise.all(), we need all our tasks to be Promises, so we'll update callTool to always return a Promise regardless of whether the underlying tool is sync or async:
The key change is wrapping the tool call with Promise.resolve(). This built-in JavaScript function takes any value and returns a Promise. If the value is already a Promise (from an async function), it returns that Promise unchanged. If the value is a regular synchronous result, it wraps it in a resolved Promise. This means whether this.tools[toolName]() returns a number like 5 or a Promise that resolves to 5, we always get back a Promise that we can await. With this uniform handling in place, we're ready to collect tool tasks for concurrent execution.
To help us better understand the timing of tool executions, we'll also add timestamp logging to our callTool method. This will let us see exactly when each tool starts executing, making it easy to observe whether tools are running sequentially or concurrently.
Now, let's modify the tool execution loop to collect Promise tasks instead of awaiting each tool call immediately:
We've introduced a new array called tasks that will hold our Promise objects. When we encounter a regular tool call (not a handoff), instead of writing await this.callTool(contentItem), we simply push the Promise returned by this.callTool(contentItem) into the tasks array. Notice there's no await keyword here, which means the tool call starts immediately but doesn't block the loop. The loop continues, starting all the tool calls one after another without waiting for any of them to finish, and now we're ready to execute them all concurrently.
After collecting all the tool tasks, we use Promise.all() to execute them concurrently and gather their results:
The if (tasks.length > 0) check ensures we only call Promise.all() when there are actually tool tasks to execute. This handles the case where Claude's response contains only handoff calls—the tasks array would be empty, and toolResults would only contain handoff results. While Promise.all([]) would work (returning an empty array), checking first makes the intent clearer.
The Promise.all(tasks) call waits for all tool calls to complete and returns results in the same order as the tasks. We use the spread operator ...parallelResults to add these results to toolResults, which might already contain handoff results from earlier in the loop. Finally, we add all the tool results to the message history as a single user message, now with the benefit of concurrent execution.
Handoff calls are treated differently from regular tool calls, and for good reason:
-
Immediate Control Transfer:
A handoff call transfers control to a different agent and returns the result of that agent's entire conversation. This is fundamentally different from a regular tool call, which simply returns a result. -
Synchronous Processing:
When a handoff succeeds, we immediately return from the current agent'srun()method with the result from the target agent. This means we do not continue processing other tool calls in the same turn. -
Error Handling:
If a handoff fails, we add the error message totoolResultsso the agent can see what went wrong and potentially try a different approach. -
No Parallelization:
Because handoffs involve special control flow and can end the current agent's execution, they must be processed immediately and cannot be parallelized with other tool calls.
This special handling ensures that the agent system maintains correct control flow and error reporting when working with handoffs.
When you run the code after parallelizing tool calls, you'll notice that the output looks very similar to what you'd see with sequential tool execution, even though we've implemented concurrent execution:
This lack of visible difference is because our current tools are synchronous and CPU-bound (simple math functions). In JavaScript, when you call a synchronous function—even if you wrap it in a Promise or start it as part of a Promise.all()—the function starts and finishes immediately, blocking the event loop until it completes. This means that, even though the code is written to start all tool calls "at the same time," each tool call actually runs to completion before the next one can begin, so the log output appears in sequential order.
The real power of this pattern becomes visible when we use asynchronous tools, which we'll explore next.
The real payoff of our concurrent tool execution pattern appears when tools return Promises that involve I/O operations or other asynchronous work. A perfect example is using agents as tools—when a tool internally calls agent.run(), it returns a Promise that represents an entire conversation with potentially multiple turns and API calls.
Let's revisit the agents-as-tools pattern from a previous lesson, but now with concurrent execution. We'll create a system where an orchestrator agent delegates multiple independent calculations to specialist calculator agents:
To understand the performance improvement, let's compare what happens without concurrent execution (sequential) versus with concurrent execution (parallel).
Here's what the output looks like when tool calls execute one after another:
Notice the timestamps: the first agent starts at 12:51:31, finishes around 12:51:37 (6 seconds). Only after it completes does the second agent start at 12:51:40, taking another 6 seconds. Then the third agent runs from 12:51:48 to 12:51:54 (6 seconds). The three agent calls take approximately 23 seconds total because they wait for each other.
Now observe what happens when we execute tool calls concurrently using Promise.all():
Notice the dramatic difference: all three calculator agent calls launch simultaneously at 12:39:03.790-791Z (within 1 millisecond of each other). The three agent conversations run in parallel, each making their own math tool calls independently, and all three complete by 12:39:10. The total time for all three agents is approximately 6 seconds instead of 23 seconds.
The difference between sequential and concurrent execution becomes crystal clear when we visualize the timeline. In sequential execution, each agent must wait for the previous one to finish, stacking their execution times end-to-end. With concurrent execution using Promise.all(), all three agents start immediately and run simultaneously, so the total time equals the duration of the longest-running agent rather than the sum of all agents:
When tools involve I/O operations like API calls, database queries, or entire agent conversations, concurrent execution means the total time is determined by the slowest operation, not the sum of all operations. In this example, we've reduced execution time by approximately 74% (from 23 seconds to 6 seconds).
When working with async tools, especially agents as tools, keep these practices in mind:
-
Explicit prompting for parallel execution:
Make your orchestrator's system prompt explicit about issuing multiple tool calls "in the same turn" when appropriate. This helps Claude understand that it should produce multipletool_useblocks together rather than sequencing them across turns. -
Idempotent operations:
Design your tools so that calling them multiple times with the same input produces the same result. This makes retries safe if something goes wrong. -
Error handling:
Each tool call in thePromise.all()array can fail independently. Our current implementation catches errors withincallTooland returns them as tool results, allowing the agent to see what went wrong and potentially adjust its approach. -
Concurrency limits:
If you expect many parallel tool calls, consider adding a concurrency limiter to respect API rate limits or system resources. For most applications, the natural limit of how many tools Claude requests in a single turn is sufficient.
You've now learned how to transform sequential tool execution into concurrent execution using Promise.all(), and you've seen the dramatic performance benefits when working with async tools like agents. The pattern is simple but powerful:
- Collect tool calls as Promise tasks instead of awaiting them immediately
- Use
Promise.all()to execute all tasks concurrently - Gather results and continue the conversation
With synchronous tools, this pattern prepares your code for future optimization. With async tools, it delivers immediate, substantial performance improvements. Now you're ready to practice implementing this pattern yourself and see firsthand how parallelization transforms the efficiency of your agentic systems!
