Agent Mentor Learn
Agent Harness Fundamentals: Loops and Control · Lesson 2 of 6

Lesson 2: The Core Loop: From One Round-Trip to Continuous Operation

Learning goals:

  • Recite the four steps of the multi-turn loop that stop_reason drives, and connect one tool-call round-trip into a while loop that keeps running
  • Use the value of stop_reason (tool_use / end_turn) to decide whether the loop continues or stops, and explain why that field is the loop's while condition
  • Point out which boundaries this skeleton loop is missing, and explain why the history grows on every turn and why you can't rely on the model alone saying "I'm done"

Prerequisites: You've read Lesson 1 and know that a harness is the layer of control code around the model; you can read the tool_use / tool_result of a single tool-call round-trip | Prev: Lesson 1 << | Next: Lesson 3 >>

One Round-Trip Stops Being Enough

Lesson 1 already took a full tool-call round-trip apart: the model returns stop_reason: "tool_use" along with a tool_use block, your host code reads out name and input, actually runs the thing, packs the output into a tool_result and sends it back, and only then does the model give its final answer. Three chunks of JSON, one trip, done.

Real tasks are rarely that polite. Change the scenario: you're writing an on-call bot, and a user says, "Restart the api service for me, then check whether the logs still have errors, and paste them if they do." That one sentence packs in two jobs, and the second depends on the first — checking the logs means nothing until the restart has finished. The model can't do both in the first turn. All it can do is this:

  1. Turn one returns tool_use, calling restart_service. You run it and send "restart succeeded" back.
  2. Turn two returns tool_use again, this time calling read_logs. You run it and send the log contents back.
  3. Turn three finally returns stop_reason: "end_turn", with a line like "Restart complete; the logs have two timeout errors, pasted below."

One user request, three trips back and forth. What the model can see at each step, and what it does next, depends on what came back in the previous tool_result — which is exactly Anthropic's definition of an agent: an LLM using tools based on environmental feedback in a loop.1 The single round-trip from Lesson 1 is just the special case where that loop happened to turn only once. What this lesson does is connect "one round-trip" into "round-trips that keep going," and get a clear look at what the loop in the middle is, what drives it, and where it needs a brake.

The Four Steps of the Loop

Turning a single round-trip into a loop doesn't require inventing anything new. You just repeat the moves you already know. The Claude API docs write this multi-turn process out as a fixed sequence:2

  1. You send a request carrying messages and the tools manifest (tools has to go along on every single turn).
  2. The model returns a response. If it still needs a tool, stop_reason is "tool_use" and content carries one or more tool_use blocks.
  3. You execute every tool_use block and turn each output into a tool_result block. The key word in that step is every: however many tool_use blocks came back in one response, the next user message needs that many matching tool_result blocks, each claimed by its tool_use_id, all packed into that one immediately following user message. The docs put the rule this way: "Whichever strategy you use, return one tool_result for each tool_use block, all together in the next user message. Match each result to its call with tool_use_id, and put every tool_result block before any text content in that message."3
  4. You append both the model's full response for that turn (assistant role) and the tool_result batch you assembled (user role) to messages, then send another request.

Then comes the sentence that matters most: repeat from step 2 while stop_reason is "tool_use".2 That "repeat while" is the axle that stretches a single round-trip into a loop. Lesson 1's example stopped at the first end_turn because one trip was all that task needed; the on-call bot runs steps 2 through 4 three times over, until turn three comes back end_turn.

Worth remembering: the fields on the tool_use and tool_result blocks haven't changed at all. A tool_use block carries id / name / input; a tool_result block carries tool_use_id / content, plus an optional is_error when the call failed.3 The loop doesn't rewrite what any of those fields mean. It only makes the same set of fields get filled in and sent back, over and over.

stop_reason Is the Loop's while Condition

That line above — "repeat while stop_reason is still tool_use" — translates into code as the test on a while loop. And the one sentence you should carry out of this lesson is this: deciding whether the loop continues or stops comes down to that single field, stop_reason. It has many possible values, but for loop control, telling two of them apart is enough to start:

  • "tool_use": the model still wants a tool. It hands the request to you and waits for you to execute and send the result back before continuing. The loop turns another round.
  • "end_turn": the model doesn't want a tool anymore; it thinks it's said what it has to say. The loop ends on its own and you hand the final text to the user.

An open-source roadmap on harness engineering puts this control layer bluntly: what drives a harness is the while loop running model→tools→model.4 And what sits in that while loop's condition expression is stop_reason. Same model, same tool set — how many rounds it turns and when it stops is decided entirely by how the host reads that field and how it writes that condition. Which is why Lesson 1 said "Same model, different harness, completely different result."4

One thing to settle up front, so you don't read the signal backwards: stop_reason: "tool_use" means the model wants to use a tool, not that a tool has been used. The model never executes anything on its own. It emits a structured request; the thing that actually runs the tool is your host code (or Anthropic's servers), and the result only flows back into the conversation afterward.2 So at the instant tool_use shows up in the loop, nothing has happened yet. The action happens in the few lines of your code that read out name and input and go do the work. Treating "I received a tool_use" as "the tool finished running" is the easiest trip-up when moving from a single round-trip to a loop — it makes you misjudge which step the loop is actually on right now.

Written Out, It's Only a Few Lines

Put those four steps and the stop_reason while condition into JavaScript and the skeleton is surprisingly short:

Walk the four steps once more against the code: the while line is "repeat while it's still tool_use"; inside the body, both the assistant response and the user message of tool_result blocks get pushed into messages, and then response gets reassigned. That last assignment is what makes stopping possible at all — drop it, and response.stop_reason keeps its old value forever, so the while loop never exits (that flavor of infinite loop is the star of Lesson 4).

This code runs, but it's a skeleton — simple enough to make the loop itself visible, and nowhere near safe to hand to production. It assumes the model will always come back with end_turn on some turn, assumes every tool executes fine, and assumes it doesn't matter how long the history gets. Those three assumptions are exactly what the next few lessons take apart one at a time.

Every Extra Turn Makes the History Longer

Look again at those messages.push lines: every turn of the loop stuffs two more messages into messages — the model's assistant response and the tool_result batch you sent back. And the next callModel has to ship the whole of messages again, unchanged. So the longer this loop runs, the more history each request carries, and it only ever grows.

That isn't an oversight in the implementation. It's an inherent property of the loop as a structure: an agent running in a loop generates more and more data that could be relevant for the next turn of inference.5 Three turns of the on-call bot only piles up a restart result plus a chunk of logs. But a task that needs dozens of turns rolls the history into something enormous.

Hidden in here is a problem that only gets opened up in the "Agent Memory and State" course, but that has to be planted now: models have an "attention budget," and every new token introduced depletes that budget by some amount.5 Longer history means more tokens, and as the number of tokens in the context window increases, the model's ability to accurately recall information from that context decreases.5 Note that this is a performance gradient that slopes down gently with length, not a hard cliff you fall off past some threshold5 — don't read it as "over the limit means useless." But the direction is unambiguous: context has to be treated as a finite resource with diminishing marginal returns.5 That thoughtless messages.push in the skeleton loop does nothing about any of this. It assumes history can grow forever — and "Agent Memory and State" is the course that comes back to settle that bill.

A Loop Alone Isn't Enough; Boundaries Have to Be Added

You now have a loop that turns. But "can turn" and "turns safely" are two different things. The skeleton loop hands the stop-or-continue decision entirely to the model: whichever turn it returns end_turn is the turn the loop stops. Yet agents are systems where the model dynamically directs its own process and tool usage.1 That autonomy is precisely where the usefulness comes from, and precisely where the risk lives: autonomy means higher costs and the potential for compounding errors accumulating around turn after turn of the loop.1 The model will potentially operate for many turns, and you have to have some level of trust in its decision-making before you let it run.1

The catch is that trust isn't the same as no supervision. If the model gets stuck on some step, or gets pulled off course by what a tool returned, and simply never comes back with end_turn, a loop that watches nothing but stop_reason will keep spinning alongside it indefinitely. So on top of the model's own finishing signal, you usually add explicit stopping conditions as well — a cap on the maximum number of iterations, for instance, to keep control on your side.1 The bare while (response.stop_reason === "tool_use") in the skeleton has no such fuse: it trusts the model without leaving itself a way out.

That plants both of this lesson's setups: this loop needs boundaries (you can't depend on the model saying end_turn; you need explicit stopping conditions — Lesson 3), and the history this loop produces needs managing (tokens are a finite resource, so you can't just shovel things in — left to the Agent Memory and State course). What a loop actually looks like when it goes off the rails, and how to catch it, is Lesson 4's subject. For this lesson it's enough to get the axle set: how the loop turns, and that stop_reason is what drives it.

Recap

  • Connecting one tool-call round-trip into a loop needs no new mechanism, just four repeated steps: send the request → read stop_reason and the tool_use blocks → run the tools and package the tool_result blocks → append to the history and send again; repeat while stop_reason is still tool_use2
  • stop_reason is this loop's while condition: tool_use means the model still wants a tool and the loop continues, end_turn means the model is wrapping up and the loop ends on its own — the model→tools→model axle is driven by that field4
  • tool_use is the signal that the model wants a tool, not a receipt saying one has run; the model never executes anything itself, and the action happens where the host reads out name and input and goes to work2
  • Every turn of the loop makes the history longer and never shorter, since an agent in a loop keeps generating more data that could be relevant5; and with a finite attention budget, recall degrades as context grows, so tokens have to be treated as a finite resource with diminishing marginal returns5
  • A loop alone isn't enough: autonomy brings higher costs and compounding errors, and the model may operate for many turns1, so on top of the model's own end_turn you usually add explicit stopping conditions (a maximum number of iterations, say) to keep control on your side1 — how to set those is Lesson 3's subject

>> Lesson 3: Stop Conditions: When an Agent Should Quit

Footnotes

  1. Building Effective AI Agents — Anthropic Engineering — https://www.anthropic.com/engineering/building-effective-agents 2 3 4 5 6 7

  2. How tool use works — Claude API — https://platform.claude.com/docs/en/agents-and-tools/tool-use/how-tool-use-works 2 3 4 5

  3. Handle tool calls — Claude API — https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls 2

  4. The 2026 Agent Engineering Roadmap — GitHub (codejunkie99/agent-roadmap-2026) — https://github.com/codejunkie99/agent-roadmap-2026 2 3

  5. Effective context engineering for AI agents — Anthropic Engineering — https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents 2 3 4 5 6 7

Exercises

01

A scheduling-assistant agent is set up with two tools, search_calendar (look at the calendar) and create_event (book something). The user says, "See if I'm free Thursday afternoon, and if I am, book a 30-minute review." The host drives it with this lesson's skeleton loop, and the stop_reason sequence that actually happens is:

Level 1: Count How Many Times This Loop Turns
  • 1st callModel returns → stop_reason: "tool_use" (one tool_use block, calling search_calendar)
  • 2nd callModel returns → stop_reason: "tool_use" (one tool_use block, calling create_event)
  • 3rd callModel returns → stop_reason: "end_turn" (text: "Booked the review for Thursday at 14:00")

Answer these: (1) How many times was callModel called in total? (2) How many times was executeTool called in total? (3) How many times did the while loop body execute? (4) On reading which stop_reason did the loop exit?

Done criteria · checked locally
02

A colleague tried to turn a single round-trip into a multi-turn loop and wrote the code below. It "seems to work" on tasks that only need one tool call, but the moment a task needs the model to call tools twice in a row, the process hangs, resources get eaten, and the logs show the same tool being called over and over. Find the root cause and fix it.

Level 2: Why This Loop Never Stops
Done criteria · checked locally