T1 · Intermediate · 8 minStable · Last reviewed: 2026-08-19Early draft

The Tool-Call Loop

In one line · The real work is a loop: the model requests a tool, you run it, you feed the result back, and it continues — until it reaches a final answer.

Glossary:AgentContext WindowInferenceTool Calling
Why it matters

The model itself never runs any tool; you own this loop. Miss it and your app stalls after the first tool request.

Like a back-and-forth call: the model says 'get me X', you fetch and return it, and it may then ask for Y. You keep looping until it says 'done, here's the answer'.

An analogy

Like a cook requesting ingredients one after another from the pantry, deciding the next need as each arrives, until the dish is done.

A user asks 'was customer 7's invoice paid?' — the model calls get_invoice(7), you return 'paid', and it replies to the user in plain words 'yes, the invoice was paid'.

How it works

Each loop turn is a fresh, full request: (1) you send the conversation plus tool definitions. (2) The model replies with either final text or a tool request (name + inputs + id). (3) Your code validates the inputs, executes, and appends the result as a message tied to that same id. (4) You resend the whole conversation and loop. The conversation grows inside the context window every turn, and each turn is a paid inference call — which is why a max-iterations cap must stop the loop from spinning forever.

The loop skeleton in simplified TypeScript:

let messages = [{ role: "user", content: userQuestion }];
for (let turn = 0; turn < MAX_TURNS; turn++) {
  const reply = await llm.chat({ messages, tools });
  if (reply.type === "text") return reply.text; // final answer

  // The model requested a tool — you validate and execute
  const input = validate(reply.toolName, reply.input);
  const result = await runTool(reply.toolName, input);

  messages.push(assistantToolRequest(reply));
  messages.push(toolResult(reply.id, result)); // tied to the same id
}
throw new Error("hit max turns");
Practice

🧪 Try it yourself: a user asks 'send a reminder to the customer whose invoice is overdue'. Trace the loop on paper step by step: turn 1, what does the model request? (find_overdue_invoice) — what do you return? Turn 2, what does it request? (send_reminder with the customer's email) — and turn 3, what does the final reply say? Write out the messages array at each turn.

Common mistakes

Top mistakes: (1) treating the first reply containing a tool call as the final answer and showing it to the user. (2) Forgetting to return the tool result, leaving the model waiting forever. (3) No max iterations, so a lost model loops endlessly and burns money. (4) Returning the result without the request's id, so the API rejects it or the model gets confused. (5) Executing without validation — the model's request is not a command.

When to use it

Any app with tools needs this loop — one question may need a chain of tools: find the customer, fetch their invoice, email them.

When NOT to use it

If the task is always a single, known-in-advance call (structured JSON extraction, say), you don't need a full loop — handle the one request and skip the complexity.

LET'S BUILD ITDone reading — now build it yourself.Trace a two-tool loop step by step