Chapter 9: Getting the Work to Actually Loop — Agent Loop

Xiao A wired tools into his agent, only to find it would still "do one step": asked to fix a failing test, it read the failure output and stopped, quietly waiting for the next instruction.
"Doesn't it have tools now? Why can't it keep going on its own?" Xiao A asked.
"Tools give the model a bounded entry point for action, but a single call can only make a single request," Lao Z said. "What real task finishes in one step? Read, judge, execute, verify again — round after round. Stringing those rounds together is what makes an agent."
This chapter covers the Agent Loop, the message stream, ReAct, stop conditions, and concurrency boundaries. It does not equate looping with autonomy or with reliability.
Align the names first
| Term | English / abbreviation | Working definition in this book | What it is not |
|---|---|---|---|
| agent loop | Agent Loop | the control flow that repeatedly calls the model, handles tool results, and updates state | the model automatically holding the right to execute |
| turn | turn | one model response and its follow-up tool results as a unit of state | one user input necessarily issuing exactly one model request |
| ReAct | Reasoning and Acting | a research method that alternates reasoning traces with actions | a mandatory implementation spec for every agent |
| stop condition | stop condition | a terminating transition such as budget, completion, failure, or cancellation | the model outputting the word "done" |
| event / state machine | event / state machine | a control model of observable facts and permitted transitions | logging used only for the UI |
| cancellation | cancellation | requesting a stop of current work via a signal / controller | automatic rollback of side effects that already happened |
9.1 The minimum job of the loop
"So what's the least this 'stringing together' has to do?" Xiao A asked.
"The Agent Loop maintains message state, calls the model, executes approved tools, and feeds the results back as input for the next round. The model proposes the candidate next step; the program decides whether to execute it and when to stop," Lao Z said. "Think of it as the circulating nurse in an operating room."
Metaphor: the circulating nurse
The model proposes; the program turns the proposal into a controlled action — much like the circulating nurse in an operating room. The surgeon (the model) proposes the next maneuver; the anesthesia and nursing team (the program) confirm, execute, and record it, then hand back to the surgeon for the next decision. Every round leaves a record, and the next decision is based on the state that actually happened, not on the model saying "done."
initialize messages
↓
request model ── no tool request ──> finish or ask for clarification
↓ tool request
validate, approve, run tools
↓
write results into message state ──> request model next round"That's it?" Xiao A asked.
"That's a simplified sketch. A production implementation also has to handle streaming, cancellation, persistence, budgets, and event subscriptions."
"Before the model speaks for the first time, what has to be ready?"
"At least three things: the system rules, the tool list (with signatures and descriptions), and the message history so far," Lao Z said. "These must be assembled before the first request. If the tool list and the real executor don't line up — a wrong signature, an out-of-date description — the model either won't call the tool or the call fails validation, and the first round stalls. Assembling context is a layer of code in its own right, whose job is 'what does the model see this round'; the loop itself has the job 'where do we go after this round.' People often write both in the same function, and when something breaks you can't tell whose fault it is."
"How does the loop know which way to go this round?"
"Look at whether this round's output contains a tool call. If it does, go to validate, approve, execute, write back. If it doesn't, check the stop conditions," Lao Z said. "The two branches cost very differently: the model-only branch spends generation fees; the tool branch piles on execution overhead and side effects on top. Using the presence of a tool request as the round boundary is far more reliable than having the model say in natural language 'I want to keep going.'"
"Then why not think the whole task through in one shot instead of spinning round after round?"
"Because in one call the model can only see and do one step," Lao Z said. "In a multi-step task, the later steps depend on the real results of the earlier tools: what the file looks like after the edit, whether the test passes after the run — you only know after execution. Trying to compute all of that in advance is asking the model to invent what comes next with no external evidence at all, which is exactly where hallucination is most likely to appear. Going round by round is what lets each decision step on the real result of the step before."
"With that many rounds, won't it take longer?"
"It does take longer. Every round is at least one model round trip, plus tool execution time; a five-step task is much slower than a single question and answer," Lao Z said. "What you get in exchange is auditability, interruptibility, and recoverability: the task can stop on any round without losing progress. Do this trade-off in advance. Loops suit multi-step tasks that need external verification; something a single step can answer doesn't need a loop, and forcing one takes the long way around."
9.2 Messages, context, and audit records
"So where do the results of all those rounds get recorded?" Xiao A asked.
"Common messages include system rules, user input, model responses, and tool results," Lao Z said. "The exact role names and fields vary by protocol, but you must preserve the associations: who proposed which call, which executor returned which result, and whether the result succeeded or was rejected. Don't keep only natural-language summaries in the history — otherwise you can't reliably recover or audit the side effects."
"Didn't the model already see the tool results? Why save them?"
"Because the next round, recovery after a restart, and post-incident auditing all rely on the same record of fact," Lao Z said. "The model's context is a temporary workbench; the application's state is what the control loop runs on."
"Save the raw results or the summaries?"
"Both, but store them separately. The raw record is for auditing and debugging — it tells you what actually happened at the time. What enters the model's context is a restricted summary or a truncated version, used to control the budget," Lao Z said. "A summary can drop exactly the things you need when debugging — the reason for failure, the truncation marker — so a summary can't replace the raw record. And stuffing every raw record into context blows the budget."
"Then if context gets truncated, can the history still line up?"
"That is precisely why message state and context are separate," Lao Z said. "If the application retains the original messages separately, trimming the model context need not delete history. A message array alone does not guarantee completeness or persistence; in-memory state may be lost when the process exits. Recovery and auditing require explicitly saved session events and tool execution records, including configuration and compaction changes. Messages are one part of that record; context is the restricted view sent to the model for this request."
"What fields does every message at least need?"
"Enough to reconstruct the events," Lao Z said. "Role or source, content, the tool call ID, the matching result, the result status (success, failure, rejection), the timestamp, and the model version used. The ID association is the crux: without the call ID you can't tell which result belongs to which request; without the status field you can't audit in retrospect whether it actually executed."
| Field | What it does | What breaks without it |
|---|---|---|
| role or source | distinguishes system, user, model, tool | the conversation thread can't be reconstructed |
| tool call ID | links a request to its matching result | you can't tell which result belongs to which request |
| result status | records success, failure, rejection | an audit can't reconstruct what happened |
| timestamp and model version | locates the incident, fixes the replay baseline | you can't say which model was in use at the time |
"Save everything and won't the context explode?" Xiao A was a little worried.
"The space the records take and the context budget are not the same thing," Lao Z said. "Persisted messages usually go to files or a database, at a cost far below context tokens; what's truly expensive is feeding the full text to the model. So store the two separately: keep message state as auditing requires, cut the context view as the budget requires. The full log of a fifty-round conversation might be several thousand lines; what enters the model is a summary plus the last few rounds verbatim."
"Doesn't storing raw results create a leak risk?"
"It does. Tool results may contain secrets, internal paths, or sensitive data," Lao Z said. "So message state and logs need tiered authorization, redaction, or encryption: the summaries the model sees get sensitive fields handled at assembly time; the audit store keeps the raw records but restricts access. Writing sensitive information straight into context hands the permissions over to the model's output, and by the time you want them back it's too late."
"Can this message state be used to 'replay'?"
"It can — replay is the most common audit move," Lao Z said. "Export the message state in time order and check it line by line: 'what did the model request this round, what did the tool return, did the host approve, did the result get written back.' Whichever line is missing, the replay breaks at that link, and where it breaks is usually where the problem is."
"So what granularity should message state record at?" Xiao A asked.
"Action-level is enough," Lao Z said. "Each tool call, each approval decision, each model request counts as one action. Going finer, to the character level, is meaningless; going coarser — just recording 'this round finished' — leaves you not knowing at recovery time what actually finished. Let the audit and recovery requirements set the granularity: record what's needed."
9.3 ReAct is a framework for understanding, not a mandated flow
"I've heard of something called ReAct. Do we have to follow it?" Xiao A asked.
"No. The original ReAct paper (Yao et al., 2022) studies alternating reasoning traces with task actions, where the actions pull in extra information from external sources," Lao Z said. "The paper doesn't require every round to emit visible 'thinking', and you can't derive from it that all agents follow the same order. Plan-driven execution, workflow routing, and human-in-the-loop can all embed this kind of loop or replace it."
"Then what exactly does it give us?" Xiao A pressed.
"A framework for understanding the shape of a loop: how observations (tool results) affect the next action (the model's decision)," Lao Z said. "It makes you aware that no round stands alone — the tool results of the previous round are inputs to this round's decision. But 'understanding the shape' and 'having to output in ReAct format' are two different things. Many loops never emit visible 'thinking text' at all: the reasoning stays inside the model, and externally you only see actions."
"Doesn't not emitting thinking make it harder to control?"
"Visibility is a separate dimension," Lao Z said. "Some models support emitting the reasoning explicitly — chain of thought — and some don't. Visible reasoning helps review, but the host still holds the stopping judgment. Whether the model thought, and whether it thought correctly, the host controls when the work counts as done."
"Apart from ReAct, what other loop shapes are there?" Xiao A asked.
"At least four common ones — don't treat them as the same thing." Lao Z compressed the differences into one table:
| Loop shape | Key trait | Typical cost |
|---|---|---|
| ReAct-style | decide after observing tool results, alternating | one model request per round; tokens grow linearly with rounds |
| plan-driven | produce a plan first, then execute step by step with checks | the plan can drift from reality; re-planning mid-task |
| workflow routing | route the task through fixed stages | handoffs between stages lose details |
| human-in-the-loop | stop at key nodes for human confirmation | high latency, lowest side-effect risk |
"Can they be mixed?"
"They can, and mixing is the norm," Lao Z said. "Many systems use workflow routing as the skeleton, embed a ReAct-style loop inside one stage, and hand the high-risk actions to human confirmation for closure. Which shape you pick depends on how stable the task is: fixed steps suit routing; exploratory work suits alternating observation and action; irreversible actions go to a human. 'Must output in ReAct form' is not a design principle — it's one option among several."
"Where does a ReAct-style loop most easily spin out of control?" Xiao A asked.
"In the gap between 'acting' and 'observing'," Lao Z said. "The model proposes an action, the tool executes it, but the result gets ignored or never written back. Next round the model acts as if it never happened and requests the same action again. The ReAct loop doesn't repair that break on its own. Writing the tool result completely back into message state matters more than hoping the model 'remembers.'"
"Does emitting visible thinking cost anything?"
"It does. Thinking text costs tokens too, and the longer it is, the more expensive. And inside a loop, the visible reasoning the model wrote last round enters this round's context, taking up space and possibly pulling the next steps in the same direction," Lao Z said. "Visibility is an engineering choice you should price. Leaving it on by default may not pay off."
9.4 One happy path and one failure path
"What does a real task actually look like walking through the loop?" Xiao A asked.
"Here's the simplified event sequence for a file-fix task." Lao Z typed out:
User: fix the failing test
Model: requests running the tests
Tool: returns the failure summary
Model: requests reading the relevant file
Tool: returns restricted file content
Model: requests a modification and a re-test
Tool: returns the edit result and the passing test
Model: reports completion"And if reading the file fails?" Xiao A asked.
"When a read fails, the right move isn't to invent some content to fill the gap. It's to return 'path does not exist' or 'permission denied' honestly, so the model can change the path, ask for help, or stop," Lao Z said. "If the same failure keeps coming back, the loop should recognize the repeated state and terminate or escalate to a human, instead of going on burning resources."
"How do you tell 'repeated'?" Xiao A pressed.
"Look at whether the state is treading water," Lao Z said. "If for several rounds the model requests the same tool with the same parameters and gets the same failure, that's a repeated state. Detection can be as simple as a counter or a state fingerprint, comparing this round's request plus result against last round's for practical identity. The point of detection is cutting losses: a loop stuck in repetition just burns budget and produces nothing."
"And 'escalate to a human' concretely means what?"
"Handing control back to a person," Lao Z said. "The loop pauses, records 'attempted N rounds, K identical failures in a row', and waits for a human to decide: change the task, change the tools, or give up. Escalation isn't failure — it's a controlled stop, preferable to both an infinite loop and a silent error."
"Besides 'path does not exist', what other typical failures are there?" Xiao A pressed.
"At least four, each looking and behaving differently." Lao Z laid out a counterexample table:
| Failure type | What it looks like | What to do |
|---|---|---|
| target does not exist | keeps returning "path does not exist" | count and stop or escalate; don't retry forever |
| permission denied | a read or write is refused | change path, change identity, or request approval; avoid looping retries |
| unexpected content | the file was read, but the content isn't what was expected | validate the content before using it; "read it" is not "read it correctly" |
| timeout or interruption | the tool doesn't return within the time limit | mark it unfinished, then decide to retry, degrade, or stop |
"So does 'the file was read' count as success?"
"It counts as tool success, not task success," Lao Z said. "Tool success only means the executor completed the action — say, returned some content. Task success further requires the model to verify the content is what was expected. These two layers often get merged into one state: the tool returned cleanly, so the task is called done. Then during debugging the logs are full of 'success' and nobody can say at which step it actually failed."
"On the happy path, is there anything to watch for?"
"There is — guard against two things," Lao Z said. "One: the model reports 'done' and nobody verifies it. It says the test passed; the loop never checks the test result, and the task gets marked completed. Two: the tool returns partial data — say, only the first 100 lines of a file — and the model treats 'read it' as 'read all of it,' so the conclusion rests on an incomplete input. The failure path handles exceptions; the happy path also has to check that this 'normal' is really normal."
9.5 Stopping and concurrency are safety conditions
"How do I keep it from looping forever?" Xiao A asked.
"A loop should limit at least the total number of rounds, total tokens or cost, per-round timeout, consecutive failure count, and user cancellation," Lao Z said. "When any limit is hit, record the explicit stop reason and the actions already completed. The model's 'done' text cannot override these external constraints."
"Then if I fire several tools concurrently, it'll be faster, right?" Xiao A jumped in.
"Concurrent tool calls are not a default optimization," Lao Z said. "Independent read-only operations can run concurrently. Shared file writes, database changes, deployments, and external communication need serialization, locks, an idempotency strategy, or approval. Before going parallel, draw out the resources and side effects — don't just count how many calls the model proposed at once."
"Where does each stop condition end up, state-wise?" Xiao A asked.
"Every stop reason should map to a clear exit state plus one recordable explanation." Lao Z sorted five stop reasons into a table:
| Stop reason | Suggested exit state | What the loop leaves in the record |
|---|---|---|
| task completed | completed | list of actions completed + final message summary |
| budget exhausted | budget_exhausted | how many tokens / how much cost spent, how far it got |
| timeout | timeout | the last action before the timeout and its timestamp |
| consecutive failures | failed | failure count, last round's request and result |
| user cancelled | cancelled | who cancelled, the action running at cancel time |
"And how does a user cancellation actually take effect?"
"Cooperatively, not by killing," Lao Z said. "The host sends a signal to the running call or subprocess asking it to stop. Whether the tool responds, and how fast, depends on the tool's implementation. Cancellation is a 'request to stop', not a 'guarantee to stop': if the tool doesn't respond, the host can only wait, set a grace period, or finally force-terminate — and must record what actually happened."
"What should the record say?"
"Two things: 'cancellation requested' and 'actual stop time'," Lao Z said. "If the timeline doesn't match, the audit misjudges. Whether the tool responded to the cancellation or simply ran to completion on its own — only the record can tell them apart. Like this:"
Host issues cancellation ──> tool receives signal
├─ responds in time: abort, record cancelled
└─ no response: wait out the grace ──> force terminate ──> record force_terminated"How exactly do I judge whether resources are independent?" Xiao A asked.
"Look at what's read and what's written." Lao Z drew a matrix:
| Combination of operations | Concurrent? | What it needs for safety |
|---|---|---|
| read file + read file | yes | nothing (read-only) |
| write file + write the same file | no | serialize or take a file lock |
| write DB + write the same table | no | a transaction or a lock |
| external communication + external communication | depends on the target | idempotency or approval |
| read + write the same resource | no | ordered execution or a version check |
"Is this matrix set in stone?"
"It's a starting point for judgment, not a conclusion," Lao Z said. "The same file, the same database record — that's the real conflict point. Different files and different records usually don't interfere with each other. When drawing the resource dependency graph, bring the granularity down to the concrete object; don't stop at tool names."
"When a stop condition and the budget conflict, whose call wins?"
"Whichever fires first," Lao Z said. "Say the task finishes and the tokens happen to be nearly exhausted too: go with 'completed' as completed, and record the remaining budget along with it. Conversely, if the budget runs out first and the task isn't done, go with budget_exhausted. When both conditions trigger, the state machine transitions only once — don't let two sets of logic write the state at the same time."
Key point
A loop must have stop conditions and concurrency boundaries. The first prevents runaway; the second prevents conflicts. Neither comes from the model "behaving itself."
9.6 Model the loop as a state machine
"How do I make sure the loop doesn't spin out of order?" Xiao A asked.
"Write the loop as a state machine, and avoid the implicit control flow of 'text arrived, so keep going'," Lao Z said. "A minimal set of states can include idle, requesting_model, awaiting_approval, running_tools, completed, failed, and cancelled; allow only the transitions you defined in advance."
idle → requesting_model → running_tools → requesting_model
│ │
├→ completed ├→ awaiting_approval → running_tools
└→ failed └→ failed / cancelled"What does a state machine buy me?" Xiao A asked.
"The cost is more complex implementation and event recording. The gains are that you can define recovery rules, test illegal transitions, and say precisely what a task had already done when it was interrupted," Lao Z said. "Concurrent tools get decided by the resource dependency graph, not by whether the model happened to request several calls at once."
"And what goes wrong with the state machine itself?"
"Three common mistakes," Lao Z said. "First, an illegal transition: jumping to completed before the tools finish — usually because 'the model said it's done' was taken as the transition condition. Second, a dropped event: some tool return triggers no transition at all, and the loop sits stuck. Third, stale state: a concurrent tool's result arrives late and you're still working from an old snapshot. The first two get closed off by defining the transition table; the third by tagging every event with its source and a sequence marker."
"Are fewer states always better?"
"There's no rule of thumb — it depends on the recovery requirement," Lao Z said. "Fewer states means a simpler implementation, but when interrupted you can't say how far it got. More states means every transition has to be tested. A pragmatic starting point is the minimal set in this chapter; add what your task is missing."
"Does every transition need a budget check?"
"Yes. Put the checkpoints on the transitions, not in the text," Lao Z said. "For example, check the remaining budget before requesting_model → running_tools, and check the round count before running_tools → requesting_model. With the budget check attached to the transition, you can't end up with 'this round blew the budget but the loop is still spinning.'"
"Can this state machine be drawn in more detail, with the guard conditions?" Xiao A asked.
"Sure." Lao Z drew a version with checkpoints:
idle ──> requesting_model (validate the tool list, budget ≥ 0)
requesting_model ── tool request ──> validating_tools ──> running_tools
│ no tool request
├─> check stop conditions ──> completed / failed
running_tools ── results written back ──> requesting_model (round +1)
└─ user cancels ──> cancelled"What does this version add?"
"Transition conditions, and an observable output for each state," Lao Z said. "validating_tools handles validation alone: is the signature right, are the permissions enough, is approval needed? The validation result is itself an event that goes into the audit. A state machine isn't better for being more complex — but a missing checkpoint you're supposed to have is more dangerous than a complex state machine."
Illustration: when a user cancellation lands while tools are running, the system first requests an abort, then records "cancellation request issued." If the subprocess can't stop immediately, you must not claim the task was cancelled. Likewise, after the model returns its final text, the loop still has to confirm there are no pending tool calls before entering
completed.
9.7 A budget-constrained state machine
"Can you give me a version with the budget in it?" Xiao A asked.
"Sure." Lao Z drew one:
idle → requesting_model → awaiting_tool_decision → running_tools
↑ │ │ │
└─────────────┴────────────────────┴───────────────────┘
requesting_model → completed | failed | cancelled | budget_exhausted"So what about the happy path for fixing the test, and the counterexamples?" Xiao A asked.
"The happy path is the same set: read the failure summary, pick the file, modify, rerun and verify — and every tool result goes into the next turn," Lao Z said. "Put the two counterexamples into the state machine and they get an answer too. Counterexample one: the same 'path does not exist' comes back repeatedly — the state machine should count and then stop or escalate, not loop forever. Counterexample two: the cancellation lands while a subprocess is executing — record that cancellation was requested; if the process can't stop immediately, you must not pretend anything was rolled back."
"Is token the only kind of budget?" Xiao A asked.
"No. Tokens / cost, round count, wall-clock time, and consecutive failure count are different budgets, and exhausting any one of them should form an explicit stop condition," Lao Z said. "The event stream lets the UI, persistence, and auditing see the same transitions; the cost is more complex implementation of concurrency, cancellation, and recovery."
"How does each of those budgets get spent?"
"Their trigger conditions differ, and so does the pace at which they drain." Lao Z laid out the four budgets:
| Budget type | What drains it | Typical consequence when it runs out |
|---|---|---|
| tokens / cost | long inputs, repeated retries across rounds, long outputs | requests fail or cost runs away |
| round count | a stuck loop, probing back and forth | the task never converges |
| wall-clock time | a round waiting, slow tool execution | the user never gets a result |
| consecutive failures | the same error recurring | resources burned over and over |
"At what granularity should the budget be checked?"
"At least two," Lao Z said. "Check the total budget before each round begins, and check the allowance before and after each tool call. budget_exhausted should be the same observable fact for the UI, the audit, and the recovery logic — nobody should be guessing from their own logs. Treat the budget as a first-class state, not an if-check on the side, and the boundaries of the loop become sayable."
Lao Z drew a full timeline:
t0 idle
t1 requesting_model (check the total budget, passed)
t2 running_tools (tool A executes, returns a result)
t3 requesting_model (round +1, still within budget)
t4 running_tools (tool B times out, moves into failed)
t5 record stop reason=timeout; write the action list into message state"If every step leaves an event, how much can recovery reconstruct?"
"Enough to reconstruct 'how far it got, why it stopped, and what's still undone'," Lao Z said. "To replay the intermediate state exactly as it was, you'd also need consistency snapshots from before and after each tool call. Without snapshots, recovery can only continue from the last persisted message. Auditing means knowing what happened; recovery means being able to pick it back up. Don't conflate the two levels."
"Does recovery have a priority order?"
"It does — order it by side effects," Lao Z said. "Writes that already executed and external calls already sent outrank plans that were never executed. When recovering, first record 'which side effects already happened', then decide whether to rerun from the breakpoint or roll the whole thing back. What recovery has to answer is 'what already happened,' not 'what the model intended to do.'"
"And where in the loop does persistence itself sit?"
"After every completed state transition, not just at the end of each round," Lao Z said. "When a tool finishes and its result is written back into message state, persist right away. While the model request is still in flight, there's nothing to write. The persistence frequency decides how far back a power cut can recover: the more frequent, the less you lose, but the more IO you pay. Trade the two off against the value of the task."
Summary
The Agent Loop chains "model output, tool execution, message update" into a self-sustaining feedback circuit. A single question and answer doesn't make an agent; the loop is what sets it apart from an ordinary chatbot. One turn is one model response plus all the tool results it triggers; the loop advances from turn to turn until some stop condition is met — budget exhausted, task complete, failure occurred, external cancellation. Any one of them brings the loop to a halt. A loop without explicit stop conditions most realistically ends by burning through the bill in an infinite loop. Events and a state machine make the loop observable and debuggable; cancellation requests a stop cooperatively via a signal — note the word "requests": if the tool doesn't respond to the signal, the cancellation doesn't actually take effect.
The misconception this chapter corrects is believing the model's self-declared "done." ReAct's alternation of observation and action helps you understand the shape of a loop, but the loop's reliability comes from external state, failure-path design, budget control, and concurrency management — not from the model saying "I'm finished." When work counts as done is controlled by the host's stop judgment. Handing that judgment to the model means handing over the loop's brakes as well, and that is the most dangerous concession in agent design.
Persisted session events and tool execution records together provide the audit basis; message state is only part of it. Context is the restricted message view sent to the model. Trimming that view need not delete separately retained history, but persistence must be implemented explicitly; an in-memory loop does not gain recovery merely by retaining a message array. What auditing needs is the associations — who proposed which call, which executor returned what, did the result succeed — along with timestamps and model versions. Keep the raw records separate from the summaries that enter context, and put sensitive fields under their own authorization or redaction. ReAct is only one loop shape; plan-driven execution, workflow routing, and human-in-the-loop each have their place. Most systems combine them, and the choice depends on the stability of the task, not on what format the model happened to output.
Stop conditions must map to explicit exit states, and budgets must be kept in separate accounts: tokens / cost, round count, wall-clock time, and consecutive failure count each drain at their own pace. Put the checkpoints on the state machine's transitions, and let the budget events themselves flow into the event stream. Concurrency only holds when the resources are independent; shared writes require serialization, locks, or an idempotency strategy. Cancellation is a cooperative request: the signal goes out, and the tool may not respond right away, so the record must state two things — "cancellation requested" and "actual stop time" — and never merge them into a single "cancelled." The model saying "done" and the tool returning "OK" both fall short of the task actually being complete: tool success only means the executor completed the action; task success still requires the host to verify the result.
Hands-on Verification
For a toy agent with two tools, record every round's events: input, model request, validation result, tool result, and stop reason. Make a tool deliberately return "path does not exist," and confirm it gets written back into state and that the loop exits once the repeated-failure threshold is reached. Then try two calls that write the same file, and confirm the executor does not write them concurrently.
