A tool-calling loop is built around a simple contract: the model emits a `tool_use` block, your code runs the tool, and a `tool_result` comes back before the model's next turn. That contract works perfectly when the tool answers in milliseconds — a lookup, a calculation, a search. It breaks down the moment the tool represents real work: compiling a large codebase, rendering a video, running a batch data job, fine-tuning a model. Those take minutes or hours, not milliseconds, and the loop has no native concept of 'come back later.'
If you force a long job to run synchronously inside a single tool call, you pay for it twice. First in money: the conversation sits open, tokens accrue, and infrastructure (yours or the API's) holds a connection idle for the full duration of the job. Second in reliability: most transports enforce a request timeout well under an hour, often well under a minute, so a job that takes longer than that ceiling never gets to report its result at all — the call simply dies mid-flight, and the model never learns whether the work even finished.
This lesson is about closing that gap deliberately: designing tools so a long job can be started, tracked, and resolved without ever asking the model — or the API connection carrying the conversation — to sit blocked for the duration. The pattern is old (it predates LLM tool calling by decades, under names like 'async job', 'ticket', or 'operation handle'), but applying it correctly to a model-driven loop has specific constraints that this lesson works through one at a time, using `claude-opus-5` in every example.