Editor’s note
Agent harnesses have acquired a remarkable amount of jargon for something often built around a simple ReAct loop: think, call a tool, inspect the result, repeat.
Over the next three issues, we’re taking that loop apart and watching the harness grow around it as the problems get harder. Part 1 strips the ReAct loop back to its basics. Part 2 adds planning and nudges to help keep it on course. Part 3 turns the same loop into a coding agent, then tackles what happens when its context and capabilities no longer fit.
We’re publishing all three parts on Fridays to see whether a technical deep dive fares better once the midweek inbox stampede has passed. After Part 3, we’ll return to our usual schedule.
Crack open any agent thread these days, and the buzzwords fly: Multi-Agent, Deep Research, Skill, Sub-Agent, Orchestrator… each one more abstract and more intimidating than the last. But peel the fancy wrapping off, layer by layer, and what’s actually running underneath is the same loop: the model thinks, calls a tool, looks at the result, thinks again. A bare-bones ReAct loop.
The staircase looks roughly like this:
Each step adds almost nothing to the last. Multi-turn just lets the loop spin more.
Plan gives the loop a persistent TODO list. Coding Agent swaps web_search for read_file and bash. Offloading and Skill are two different compromises for when stuff stops fitting in the context window. The point of internalising a harness isn’t memorising each level’s name but rather seeing clearly what problem each level solves that the level below it didn’t.
That’s the path this series takes. We start with the weather forecast.
Single-turn ReAct loop: Weather forecast
Picture the smallest possible scenario: the user asks, “is it cold in Beijing today?” We give the model a single tool: get_weather(city).
The model takes one look at the question and makes one decision: “I can’t answer this on my own, I need to look it up.” So it emits a tool_use: get_weather(city=”Beijing”). The harness picks up that tool_use, actually runs the function, takes the result (say {”temp”: 3, “condition”: “sunny”}), and shoves it back into the message queue as a tool_result. The model takes another look and decides whether to call another tool or answer the user. If it answers, what it emits is plain text (no tool_use) and the loop ends.
That’s one full ReAct round-trip: Reason (decide what to call) → Act (emit tool_call) → Observe (look at tool_result), then either go again or hand back the final answer.
What the harness actually is
Strip it down, and the harness is almost embarrassingly simple:
messages = [{”role”: “user”, “content”: user_input}]
while True:
response = llm.call(messages, tools=TOOLS)
messages.append(response)
if not response.tool_calls:
# Model didn’t call any tools — loop terminates
break
# Run all tool_calls in parallel, pack results back in
results = run_tools_parallel(response.tool_calls)
messages.extend(results)
return response.content
The harness doesn’t ask “have we searched enough?” or “should we try a different keyword?” The only thing it checks is: does this assistant turn contain a tool_call? If yes, execute. If no, exit. All the thinking is the model’s; the harness is just a courier.
Emitting multiple tool_calls in one turn
The interesting bit comes with the next question: “which is colder, Beijing or Shanghai?”
A competent model isn’t going to look up Beijing first and then Shanghai. It emits two tool_use calls in the same assistant turn:
{
“role”: “assistant”,
“content”: [
{”type”: “tool_use”, “id”: “t1”, “name”: “get_weather”, “input”: {”city”: “Beijing”}},
{”type”: “tool_use”, “id”: “t2”, “name”: “get_weather”, “input”: {”city”: “Shanghai”}}
]
}
On the harness side, as long as you wrote run_tools to execute in parallel (the run_tools_parallel in the pseudocode above), both queries fire at once. Two tool_results come back together. Next turn, the model sees them side by side and answers the comparison directly.
This is a property of ReAct that people consistently underrate: a single assistant turn can fan out to any number of tool_calls. When you write your harness, don’t default to “one at a time” — the moment you hit “compare three cities” or “read ten files in parallel,” it’ll be painfully slow.
Is single-turn enough?
For a straightforward weather check, yes. But ask for Beijing’s chance of rain this Friday compared with the same week over the past five years, and one lookup no longer does the job.
The model has to produce the forecast, retrieve the historical data, compare the two, and perhaps go back to fill in anything it missed. It needs a multi-turn loop, with each step shaped by what the previous one returned.
Multi-turn loop: Deep research
Swap get_weather for two more general tools (web_search and web_fetch) and you’ve got the skeleton of Deep Research.
Two tools, but the behaviour space they open up jumps by orders of magnitude. The model can search, pull back a pile of candidate links, pick the most relevant one or two and fetch the full text, realise it’s missing something and search again with a different keyword, fetch the next link. The whole trajectory is meandering, stop-and-go. How many steps, when to pivot — entirely the model’s call.
In a multi-turn run, every tool call depends on what the previous one returned. Search and retrieval interleave, and the trajectory is not known in advance: the agent might converge in three turns or still be missing pieces at fifteen.
The shape of a multi-turn loop
Drawn as a sequence diagram, the shape is clean:
The harness code is unchanged. The loop simply spins more times.
Who decides when the loop stops?
This is the most counterintuitive part of multi-turn ReAct: the LLM decides when to halt, not the harness.
Whether the loop continues or not is the model’s call. If it thinks it doesn’t have enough, it emits another web_search. If it thinks it does, it skips tool_use and just produces text. The harness doesn’t get a vote.
What’s still missing
The multi-turn loop solves “one step isn’t enough.” But it has an obvious flaw: the model is purely reactive — wherever its eyes land, that’s where it goes next. No global view. Easy to wander off. A complex question that really should be researched along five dimensions might trap it on the first dimension for seven or eight searches while the others get forgotten. Or it picks a bad keyword early and turns spinning in the wrong direction, eating tokens and producing nothing.
Pure ReAct can keep moving, but it cannot guarantee that it is going anywhere. It has tactics, but no strategy. And an agent without a strategy is just a rather expensive way to walk in circles.
In Part 2 of this series, we give it a plan.
Agentic Engineering grew out of a desire to create something useful for people trying to make sense of agentic AI and beyond. If you’d like to support that direction, please tell your friends about us.
That’s it for this one. We’ll pick up the conversation next week.
Until then, keep building.
Tanya D’cruz
Editor-in-Chief







Thanks for the writeup. Every buzzword as the same loop plus one layer is the right teaching order, most explainers start from the layers and never show the loop.
I turned these concepts into a runnable tutorial using Python, now with 700+ GitHub stars: https://github.com/hardness1020/awesome-agent-architecture/tree/main/sections/01-agent-loop