Introduction & Context

Welcome back! In the previous lesson, you converted the agent's Gemini calls to work inside an async workflow, so multiple conversations can make progress while waiting for model responses. That was an important first step, but there is still another bottleneck: tool execution.

When Gemini asks for tools, the agent receives one or more function calls in the model response. If the agent executes those function calls one at a time, a single slow tool can delay every other tool result in that turn. In this lesson, you'll make tool execution non-blocking and then run multiple requested tools concurrently with asyncio.create_task() and asyncio.gather().

Understanding the Tool Execution Bottleneck

The agent already wraps Gemini API calls with asyncio.to_thread(), because the Google Gen AI Python SDK call is synchronous. We need the same idea for regular Python tools. A math function like sum_numbers() returns quickly, but in real systems a tool might call another service, read a file, query a database, or do other blocking work. If we call that function directly from an async method, it blocks the event loop.

The first improvement is to make _call_tool() itself async and run synchronous tools in a worker thread:

This change prevents a blocking tool from freezing the event loop, so other conversations can continue while the tool runs.

Scheduling Multiple Tool Calls

Making _call_tool() async is useful, but if the run() method still awaits each tool immediately, tools in the same turn still execute one after another:

To run independent tools in parallel, collect them as tasks first:

asyncio.create_task() schedules each tool call without waiting for it immediately. That gives all tool calls in the same model turn a chance to run concurrently.

Gathering Tool Results

Once all tool tasks are scheduled, asyncio.gather() waits for them and returns their results in the same order as the task list:

Because _call_tool() catches its own exceptions and returns formatted function_response parts, one failed tool can be sent back to Gemini as an error result without crashing the whole agent loop.

Adding a Synchronous Wrapper

After converting run() to async, synchronous scripts can no longer call agent.run(...) directly. They receive a coroutine instead of a final result. For notebooks, tests, or simple scripts that are not already inside an event loop, a small wrapper can make the async agent easier to use:

This wrapper does not replace the async API. It simply provides a convenient bridge for synchronous callers. If your application is already async, you should continue to use await agent.run(...) directly.

Summary & Practice Exercises

In this lesson, you refined your agent's parallel capabilities by removing the tool execution bottleneck. You'll first make _call_tool() async with asyncio.to_thread(), then practice launching multiple tool calls with asyncio.create_task() and collecting them with asyncio.gather(). Finally, you'll add a run_sync() helper so synchronous code can still use the async agent safely.

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal