Chapter 6: The Agent Loop: Orchestrating Multi-Step Tool Execution

Learning Objectives

Part 1 — Pre-Reading Check: The Core Loop & Controlling It

1. In the ReAct pattern, which actor actually executes a tool and touches the outside world?

2. What single mechanical step turns a one-shot tool call into a multi-step loop?

3. In a modern function-calling agent, how does the loop know the model is done and should return a final answer?

4. According to the Infinite Agentic Loop (IAL) research, what is the dominant consequence of unbounded agent loops?

5. Why is a deterministic code-level max-iterations cap considered a "guarantee" while a prompt-level stop instruction is only a "hope"?

6. Iteration caps bound how many steps run. What additional control is needed to stop a loop where a single tool call hangs on a slow external API?

Part 1: The Core Loop & Controlling It

Key Points

The ReAct cycle: thought, action, observation

The agent loop is best understood through the ReAct pattern — Reasoning and Acting. It connects the LLM's "thinking" to external actions through a structured cycle of thought, action, and observation. Think of a chef working through an unfamiliar recipe without reading ahead: read the current step (thought), chop the onions (action), check whether they are fine enough (observation), and only then decide what to do next. The chef interleaves reasoning and acting, one step at a time.

Each iteration proceeds in three stages, mapping directly onto three actors in your code:

StageWho performs itInputOutput
ThoughtThe LLMConversation + tool listReasoning about next step
ActionThe LLMReasoningA tool call + arguments (structured JSON)
ObservationYour orchestration codeTool call + argumentsThe tool's return value (result or error)

Notice that the LLM never touches the outside world directly. It only emits a request; your loop actually executes it and hands back the result. This separation is the foundation of loop safety.

flowchart LR T["Thought
(LLM reasons)"] --> A["Action
(LLM emits tool call)"] A --> O["Observation
(your code runs tool)"] O -->|"append result, recall model"| T O -.->|"no tool call = done"| F["Final Answer"]
[Animation slot: the ReAct spiral — show thought/action/observation returning to the same three stages, but with the conversation transcript growing longer each loop.]

Appending results and terminating

The single most important mechanical detail: after a tool executes, its result is appended back to the conversation, and the model is called again. The conversation grows like an accumulating transcript — each [tool] message is an observation the model sees on its next call. This is why the loop is drawn as a spiral rather than a circle: it returns to the same three stages, but with more context each time.

A ReAct loop must stop, and there are two fundamentally different ways it can. First, the model signals completion: in modern function-calling agents, it responds with no tool calls. Second, a hard safety cap is hit: a maximum number of loops forces termination even if the model never signals completion. This is explicitly a safety mechanism, not a convenience — it prevents infinite loops when the model enters a repetitive cycle.

Figure 6.2: A multi-step tool run for rescheduling an appointment

sequenceDiagram participant U as User participant O as Orchestration loop participant M as Model (LLM) participant T as Tools (calendar) U->>O: "Move dentist appt, tell my wife" O->>M: Conversation + tool list M-->>O: tool_call calendar.find O->>T: calendar.find(query="dentist") T-->>O: {id: evt_88, start: 15:00} O->>M: Append observation, recall M-->>O: tool_call calendar.free_slots O->>T: calendar.free_slots(next week) T-->>O: [slot1, slot2] O->>M: Append observation, recall M-->>O: tool_call calendar.move O->>T: calendar.move(id=evt_88) T-->>O: {ok: true} O->>M: Append observation, recall M-->>O: Final answer (no tool calls) O-->>U: "Done, and I messaged your wife."

Figure 6.1: The read-act-observe loop with its termination and max-iteration branches

flowchart TD A["Start: user message + tools"] --> B{"Iterations < max?"} B -->|"no"| F["Force final answer (partial)"] B -->|"yes"| C["READ: call model with full conversation"] C --> D["Append model response to conversation"] D --> E{"Tool calls present?"} E -->|"no (final answer)"| G["Return response text"] E -->|"yes"| H["ACT + OBSERVE: execute each tool"] H --> I["Append tool results as observations"] I --> B F --> Z["End"] G --> Z

Controlling the loop: budgets, guardrails, timeouts

Left unbounded, an agent loop is a liability. An IAL occurs when "an agentic feedback path repeatedly triggers execution without an effective stopping bound." The dominant consequence is API cost exhaustion in 95.6% of cases. The first and most important control is the max iterations cap, recommended at 10–15 for most task-completion agents. Beyond iteration count, production agents also track token budgets and cost budgets, because a loop with few iterations but enormous per-step context can still be ruinously expensive.

A robust agent layers its defenses rather than trusting any single mechanism:

The cardinal principle: deterministic stopping bounds are safer than model-dependent termination. Never let the model be the only thing that decides when to stop, and place each bound inside the actual feedback path that repeats. Iteration caps bound how many steps run, but not how long they take — so production loops add wall-clock timeouts (per-step and overall run) and support cancellation for interactive assistants where a user may abandon a request mid-loop.

[Animation slot: layered defenses — show a runaway loop passing through prompt instruction, then max_iterations, then debounce/timeout gates, illustrating defense in depth.]
Part 1 — Post-Reading Check: The Core Loop & Controlling It

1. In the ReAct pattern, which actor actually executes a tool and touches the outside world?

2. What single mechanical step turns a one-shot tool call into a multi-step loop?

3. In a modern function-calling agent, how does the loop know the model is done and should return a final answer?

4. According to the Infinite Agentic Loop (IAL) research, what is the dominant consequence of unbounded agent loops?

5. Why is a deterministic code-level max-iterations cap considered a "guarantee" while a prompt-level stop instruction is only a "hope"?

6. Iteration caps bound how many steps run. What additional control is needed to stop a loop where a single tool call hangs on a slow external API?

Part 2 — Pre-Reading Check: State & Reliability

7. What is the "scratchpad" in an agent loop?

8. Why does token cost grow roughly quadratically over the course of a long loop?

9. How do ReAct and Plan-and-Execute differ in when they reason?

10. When a tool call fails (times out or returns a 500), what is the recommended handling in the core loop?

11. For agent observability, what is the central recommended artifact?

12. Feeding tool errors back as observations lets the model recover — but why must that recovery itself be bounded?

Part 2: State Across Steps & Reliability and Observability

Key Points

Accumulating state: the scratchpad

The agent loop is only useful because it remembers. Each tool result is appended to the conversation as an observation, and the mechanism that formalizes this accumulation is the scratchpad: an area storing the agent's sequence of thoughts, actions, and observations from previous steps. LangChain's create_react_agent manages this by appending each Thought, Action, and Observation to a dynamically growing string inside the prompt, giving the model full context of prior steps so it does not repeat work.

The analogy is a detective's notepad. Each interview (tool call) yields a fact jotted down. Before the next interview, the detective glances back over every note, so they never ask the same question twice. Remove the notepad and the detective would interrogate the same witness in circles — exactly the duplicate-call failure the DebounceHook guards against.

The scratchpad holds two kinds of content: intermediate reasoning (thoughts) — the "R" in ReAct — and tool observations, the raw results returned by your tools. This dual nature reflects a broader design contrast:

DimensionReAct (interleaved)Plan-and-Execute
When it reasonsAt every step, using latest observationOnce up front, then executes
AdaptabilityHigh — course-corrects each stepLower — plan may go stale
Scratchpad growthGrows every iterationPlan fixed; execution logs grow
Best forUncertain, exploratory tasksWell-defined, predictable workflows
Model callsOne per step (more expensive)Fewer reasoning calls

Context growth during long loops

Because the scratchpad is a dynamically growing string, every iteration makes the prompt larger. A ten-step loop re-sends everything from steps 1 through 9 on step 10. This has three compounding costs: token cost grows roughly quadratically, latency increases as context lengthens, and context-window exhaustion becomes a hard failure mode. The mitigations are the context-management techniques from Chapter 3, now applied inside the loop: summarize or compact old observations, truncate verbose tool outputs, and drop intermediate reasoning that has served its purpose. A tool returning a 50 KB JSON blob should be distilled to the few fields the model actually needs before it enters the scratchpad.

[Animation slot: the growing transcript — visualize the scratchpad re-sending steps 1..N-1 on each call, with the token count rising quadratically, then show truncation/compaction flattening the curve.]

Reliability: partial failures and observability

An agent loop that works in a demo can fail in a hundred quiet ways in production. Failures are rarely a single API error; they are sequences of decisions, tool calls, and routing events, each of which must be individually traceable. Agent observability rests on the classic triad — traces, metrics, logs — and the central artifact is an end-to-end trace with a span for each LLM call, each tool call, and each state transition. A span is a timed, named record of one unit of work with its inputs, outputs, and metadata; a trace is the tree of spans for one complete run.

Two production practices are worth committing to memory: instrument intermediate tool responses as separate spans (silent malformations that agents "hallucinate around" won't surface in final-response logging alone), and call flush_traces() in background workers so queue consumers and cron jobs don't lose telemetry.

In a multi-step loop, some steps will fail. The wrong answer is to crash the whole loop on the first tool error. The right answer is to catch the error and feed it back to the model as an observation (e.g., {"error": "calendar service unavailable"}). The model can then reason about it: retry, try a different tool, or tell the user it couldn't complete that step. But recovery must itself be bounded, or you recreate the top IAL failure pattern: retry feedback without bounds accounts for 25% of infinite-loop cases. Cap retries explicitly and let the duplicate-call debouncer catch a model that keeps re-issuing the same failing call.

Figure 6.3: Handling a partial tool failure as a bounded, recoverable observation

flowchart TD A["Execute tool call"] --> B{"Tool succeeded?"} B -->|"yes"| C["result = return value"] B -->|"no"| D["Catch ToolError"] D --> E["result = {error: message}"] C --> F["Append result as observation"] E --> F F --> G{"Retries within max_retry?"} G -->|"no"| H["Stop: report partial to user"] G -->|"yes"| I{"Duplicate call blocked?"} I -->|"yes (BLOCKED)"| H I -->|"no"| J["Model reasons on next iteration"]

Preventing infinite loops: defense in depth

Everything assembles into a defense-in-depth checklist against the single most dangerous failure of an agent:

DefenseWhat it stopsLayer
Prompt-level stop instruction~60–70% of loops in simple agentsPrompt
max_iterations (10–15)Tool-call iteration without boundsCode (deterministic)
max_retry capRetry feedback without boundsCode (deterministic)
DebounceHook / duplicate detectionRepeated identical tool callsCode (deterministic)
State TTL / reentry counterMessage reentry without boundsCode (deterministic)
Per-step & overall timeoutsHung tools and stalled runsCode (deterministic)
Per-tool-call spans + retry metricsDetecting loops that slip throughObservability

Two root-cause reminders anchor the checklist: exit conditions must not rely solely on LLM outputs, and the bound must sit inside the actual feedback path — a limit placed outside the loop that repeats will never fire.

Part 2 — Post-Reading Check: State & Reliability

7. What is the "scratchpad" in an agent loop?

8. Why does token cost grow roughly quadratically over the course of a long loop?

9. How do ReAct and Plan-and-Execute differ in when they reason?

10. When a tool call fails (times out or returns a 500), what is the recommended handling in the core loop?

11. For agent observability, what is the central recommended artifact?

12. Feeding tool errors back as observations lets the model recover — but why must that recovery itself be bounded?

Your Progress

Answer Explanations