Async in Simple Words
Async means wait for many tools at once. Simulate concurrent jobs with a queue and an in-flight limit. Skip event-loop internals.
Sometimes an agent needs three tools. If you wait for each one to finish before starting the next, you waste time. Search can run while a page fetch runs. That overlap is the point of async.
Async does not mean extra CPU cores. It means: wait for many things without blocking the others. You will not run real asyncio well in every browser. We simulate with a queue of tasks in a loop. We will not teach event loop internals. You still need the idea: many jobs in flight.
Heavy math does not get faster from async alone. Network waits do. Disk waits sometimes do. time.sleep in a normal function blocks everything. That is why the stdlib lesson warned you.
Why wait together
Three jobs take 3, 2, and 4 ticks.
| Schedule | Total ticks |
|---|---|
| One after another | 3 + 2 + 4 = 9 |
| All started together | 4 (the longest) |
If each tick is a network wait, this is a snappy agent vs a bored user. If the jobs must run in order (write then read the same file), overlap is a bug. Concurrent is for independent waits.
Three waits of 3, 2, and 4. In a line they cost 9. Together they cost 4 — the longest.
Sequential vs overlapasync and await in simple words
In real laptop code you may see:
async def search(q):
result = await client.get(q)
return resultasync def marks a function that can pause. await is the pause: “wait for this, let other work run.” You only need this when you wait (network, disk). Do not run that snippet here. Joeven’s sandbox is not a full asyncio playground. The words are vocabulary for when you leave the browser.
If you call search(q) without awaiting, you have not run the body yet in real asyncio. That surprise is why we simulate with a plain loop first. A queue you can print is easier than an event loop you cannot see.
Concurrent means many jobs in flight
Concurrent means many jobs are in flight: started, not finished yet. A queue is a waiting line of jobs that have not started. A loop starts jobs until a limit (say 2 at a time), then ticks the clock, then finishes whoever is done.
Reads that do not touch each other are safe to overlap. Two tools that write the same file are not. Start simple: overlap reads, run writes one at a time. Two searches with different queries are independent. Search plus finish is not a pair to overlap if finish needs the search result. Data dependence is an order. Independence is overlap.
A limit of 2 in flight is a courtesy to the API and to your timeout slots. Unlimited in-flight is another way to 429.
A queue of tasks
Each job has a wait (how many ticks it needs). Each tick, every in-flight job loses one tick. When a job hits zero, it is done. The queue fills in-flight up to a max. No threads. No event loop talk.
collections.deque is a list that is cheap to pop from the left. popleft is “next job please.” A normal list pop(0) also works for tiny queues and is slower for huge ones. For three jobs, either is fine.
Timeouts still matter. A hung tool that never finishes holds a slot. Cap waits. The retry lesson already taught you to stop. In this simulation, every job’s wait is finite. Real tools need a timeout so left cannot stay positive forever.
Wall time is max(waits) only if every job starts at once. With max_in_flight=1, wall time is the sum. With a limit of 2, wall time sits between those two numbers. Print start and done times. That print is how you debug “why is this turn slow?” without an event loop lecture.
Do not overlap a tool that needs the previous tool’s output. search then read the first hit is sequential on purpose. Overlap two searches. The agent loop can still be a simple for-step; only the executor’s wait is concurrent. That split keeps max_steps honest.
Common mistakes
- Overlapping writes to one file.
- Unlimited in-flight.
- Thinking async speeds CPU loops.
- Using async for a single tool call with no siblings.
- Skipping timeouts so a hung job blocks a slot.
Run to execute this in your browser. Nothing is sent to a server.
Change max_in_flight to 1. Wall ticks should grow. Change it to 3. All three can start at t=0. In flight is the idea. That is enough.
How agents use this
A turn can fire several independent tools. Sequential HTTP wastes the user’s time. Concurrent means many jobs in flight, not “smarter math.” Keep the agent loop easy to read. Use a queue (or asyncio.gather later, on a laptop) only for waits that do not collide. You do not need event loop internals to ship that rule.
The outer agent loop can stay synchronous: one think, then maybe gather two reads, then observe, then think. You do not have to make run_agent itself async on day one. Push concurrency to the executor: call_many(names). Test that function with fake waits, like this lesson.
Never overlap a tool that spends money with no cap. Two concurrent model calls are two bills. Two concurrent searches may be cheaper than sequential if the user is waiting. Measure. The queue’s max_in_flight is a budget too.
If one of three jobs fails, decide: cancel the others, or keep them. Cancel is kinder to the wallet. This simulation has no cancel; jobs run until left is 0. On a laptop, a timeout is your cancel. Record which names finished. The observation should list successes and failures separately so the model does not assume all three returned.
Check your understanding