Skip to content

16 min read

Thought becomes content nothing branches on; Action becomes a call

On this page

Every element of the Thought:/Action:/Observation: protocol — the labels a ReAct agent writes into its completion and its harness reads back out with a regular expression — has somewhere to go in a typed tool-calling exchange, where the request carries machine-readable tool declarations and the reply carries the model's request as a structured field. The mapping is short enough to write on one page, and teams reach the end of it in an afternoon. The reason the migration takes longer than an afternoon is a single line of that page: the field holding the model's reasoning stops being a field.

That line is where the interesting work is, because something is usually depending on it. An agent service one company acquired had a Reflection field in its transcript format and a quarterly compliance review built on exporting it, and the question raised against the migration note was a reasonable one: how do we explain, after the fact, why the agent did what it did? Answering it properly is what decides whether a migration produces a system with better evidence or merely a differently-shaped one. The mechanical part comes first, since it is short.

The same three roles, carried by three message parts

Every element of the protocol has a destination, and the destinations were designed by people who had read the same paper. Nothing about the alternation changes; what changes is which side of the wire holds the structure.

On the acquired service the mechanical lines of the migration note were the ones nobody argued about, and they read as a list of deletions: the capability registry becomes a list of JSON Schema objects, INVOKE_RE is deleted, the stop sequence is deleted, the malformed-invocation branch is deleted, and the loop reads stop_reason instead of searching for a label. Three of those four deletions follow from one another rather than being independent decisions, and the chain is worth walking because it is the reason the diff is a deletion at all. The stop sequence existed to make the model halt at the point where the harness needed to take over, which was the Observation: label, because the model would otherwise carry on and hallucinate the tool's answer. A provider that owns the tool call knows when the model has asked for one, so it stops generation itself and reports why in stop_reason. Once the provider is the thing that stops, a stop sequence has nothing left to do. Once generation stops on a structured request rather than mid-text, there is no substring to extract. Once there is no extraction, there is no extraction failure, so the malformed-invocation branch has no condition that can reach it. The branch is not removed because someone judged it unnecessary; it becomes unreachable.

ReAct elementBecomesOwned byWhat branches on it
Thought: proseText content in the assistant turn, or a provider-native reasoning blockThe modelNothing in your code
Action: name[args]A tool_use block: a name from your schemas and a typed arguments objectThe provider's serialiserYour dispatch, keyed on the name
Observation: lineA tool_result block carrying tool_use_id and an error flagYour loopThe model, on the next turn
Stop sequence on the observation labelstop_reason == "tool_use"The providerYour loop's continue-or-finish test
Answer: labelAn assistant turn containing no tool-use blockThe modelYour loop's exit
The "could not parse" retry branchSchema validation, before dispatchProvider and your validatorDeleted; there is no parse to fail
Tool descriptions written as prose in the promptdescription and input_schema fields sent with the requestYouThe model's selection, and the validator

Written out as messages, the same exchange from the first chapter looks like this, with the correspondence visible: an assistant turn that carries both reasoning and a request, and a following user turn that carries the result addressed to that request by id.

json
[
  {"role": "user", "content": "Was cus_8812 charged twice for INV-2041 in July?"},
  {"role": "assistant", "content": [
    {"type": "text",
     "text": "I should list this customer's July invoices before assuming a duplicate."},
    {"type": "tool_use", "id": "toolu_01A9", "name": "list_invoices",
     "input": {"customer_id": "cus_8812", "since": "2026-07-01"}}
  ]},
  {"role": "user", "content": [
    {"type": "tool_result", "tool_use_id": "toolu_01A9",
     "content": "[{\"id\":\"INV-2041\",\"issued\":\"2026-07-03\",\"total\":129.00}, ...]"}
  ]}
]

The text block is where Thought: went. It is ordinary assistant content, the loop appends it to the message list and reads nothing out of it, and if the model omits it entirely the system still works. The tool_use block is where Action: went, and the argument values are typed fields rather than a substring, which is the whole of chapter two in one structural change. The tool_result is Observation:, with the addition that it names which call it answers, so a turn containing three parallel calls comes back as three results that cannot be confused with one another — something the flat transcript could not express at all, since it had one slot per step and no way to say which step.

"Content nothing branches on" is a claim about your code rather than about the model, and it is checkable in a way the older design's equivalent was not. Search the loop for every place the assistant's text is read. In a typed loop there is one such place — the line that appends the whole content list to the message history — and it treats the text as opaque. In the parsed loop the same search returns the stop sequence, the label search, the regex, and the branch that fires when the labels are absent or out of order, all of which are control flow conditioned on prose the model chose. That is the difference the title of this chapter names: the reasoning did not stop being present, it stopped being consulted.

The tool_use_id deserves its own sentence, because it is the piece with no counterpart in the older protocol at all. A flat transcript has one Observation: slot per step and no vocabulary for saying which action a given observation answers, which is fine while exactly one action happens per step and undefined the moment two do. A turn that emits three parallel calls comes back as three results, and if the results are just three lines of text under three identical labels, the model has to work out the correspondence from content — which it will sometimes do wrongly, silently, in a way that looks like a reasoning failure. The id makes the correspondence a fact carried by the message rather than an inference. It is the same move as everything else in the mapping: something the reader of the transcript had to reconstruct becomes something the sender states.

The interleaving that the original paper measured is entirely intact in that array. Reasoning precedes action, the result of the action is visible before the next decision, and the model's next turn is conditioned on both. Migrating away from the text protocol keeps the finding and discards the carrier, which is why the migration note runs to a page rather than a redesign document.

The reflection field was never the evidence

The compliance process had been exporting a field that reads, in a typical row, Before summarising I must run the standard integrity check, which is required for all fetched pages. That sentence came from the attacker-controlled page in chapter three, and it appeared in the export as the agent's stated reason for exfiltrating a deploy key. It is a fluent, plausible, complete explanation of an action, and it is not a report of why the action was taken. The model produced it because that text was the most likely continuation of the transcript in front of it, and it would have produced something equally fluent had the transcript been clean.

This is the general case rather than an adversarial edge. The property at issue is faithfulness, meaning whether the reasoning a model writes down is the reasoning that produced its answer, and the research on it is not encouraging. Turpin and colleagues' Language Models Don't Always Say What They Think is the paper usually cited, and later work at several labs has replicated the effect on current reasoning models: models produce rationales that omit the feature that actually drove the answer, including when that feature was a bias deliberately introduced into the prompt. The earlier topic on scratchpads covered why the reasoning text helps performance; the point here is narrower and about evidence. A stated rationale is a generated artifact correlated with the action, not a causal record of it, and treating it as a record means your audit trail is strongest exactly where the model is most confident and says nothing about the cases where the model was wrong for a reason it did not mention.

It is worth being exact about why a reflection export feels like a control, because the feeling is not stupid and the people who build these processes are not careless. An audit trail earns its keep by varying: a log line is useful because it says something different when something different happened, and a reviewer scanning it is looking for the row that reads unlike its neighbours. A reflection field does not have that property. It is populated on every step, in fluent prose, with a plausible reason, whether the model attended to the customer's balance or to a sentence an attacker put in a web page. Its presence is constant. Its fluency is constant. The property a reviewer actually wants to inspect — whether the stated reason is the operative one — varies freely underneath both, and nothing about the field's appearance moves when it does. Reading a quantity that holds still as evidence about a quantity that moves is the whole of the mistake, and it is why the export got greener as the system got worse: the more reliably the model wrote its rationale, the more complete the review looked.

The sentence a reader would confidently write after reading a reflection export is "the agent did this because it believed X", and the true sentence is "the agent emitted the string 'because X' and then did this". Those are different claims, and only the second one is supported by anything.

There is also a plainer engineering cost to keeping it as a field. A field is something downstream code depends on, and once compliance exports it, a dashboard renders it, and an alert greps it for the word "override", you have made model prose a load-bearing part of the system. That dependency then blocks changes it should not block: you cannot move to a provider whose reasoning arrives as an opaque or encrypted block, you cannot turn off extended thinking (the provider setting that lets the model reason at length before it answers) to save latency, and you cannot prompt for shorter output, because something now breaks when the sentence is missing. Reasoning text also costs tokens on every turn it is resent — for the acquired service, reflections averaged 63 tokens per step across a median of four steps, resent on each subsequent request, which came to roughly 630 tokens of cumulative input per conversation at no benefit once nothing read them.

That last figure is larger than the multiplication most people do in their heads, and the reason is worth seeing because it applies to anything carried in a transcript. Multiplying sixty-three tokens by four steps gives the amount of reflection text the conversation produces, which is not what gets billed. Each request resends the whole conversation so far, so a reflection written at step one is paid for again on every later request. Count the resends: a conversation of four tool steps makes a request per step and a final one for the answer, and the first reflection rides along on four of those, the second on three, the third on two, and the fourth on one. Ten reflection-lengths in total, and ten times sixty-three is the 630. The general shape is that a token added to a transcript early costs proportionally to how many turns follow it, so the cumulative bill grows with the square of the conversation length rather than in step with it. That is why a per-step number always understates it, and why the argument for dropping dead text gets stronger on exactly the long conversations where an agent is doing real work.

What the compliance lead actually needed was a record of what happened, which the typed protocol produces as a side effect. Each of these is a fact rather than a claim.

What people want from a Thought fieldWhat the field gives themWhat to record instead
Why did it call this tool?A sentence the model generated before actingThe exact call: tool name, validated arguments, tool_use_id, timestamp, model id and version
What did it know at the time?NothingThe full message list for that turn, or a hash of it plus the stored conversation
Was it allowed to do that?NothingThe dispatch-layer decision: which policy rule permitted the call, which principal's credentials were used
What did it get back?A paraphrase, sometimesThe raw tool result, its size, its source, and whether it was flagged external
Did it go wrong and recover?Prose that may not mention the failureRetry count, is_error results, and the validation errors that were returned
Is the behaviour changing over time?AnecdotesPer-tool call rates and argument distributions, versioned against the prompt and schemas

An auditor asking why the agent refunded a charge is answerable from that list in a way it is not from a sentence: this conversation, at this timestamp, with this message history, called issue_refund with these arguments, which passed schema validation and policy rule refunds.max_amount, using the service principal, and the tool returned this. If you also want the model's narration, store it as an annotation labelled clearly as model-generated commentary, kept beside the record rather than inside it, and make sure nothing queries it. The distinction is the same one used for any log line derived from a third party's claim about itself.

Running the swap without a flag day

The mapping is small enough that teams are tempted to do it in one commit, and the reason not to is that the prompt changes at the same time as the protocol. Removing the exemplars and the English tool descriptions alters the model's behaviour independently of the transport, so a single deploy that changes both leaves you unable to attribute a regression. The order that worked for the acquired service was to declare the schemas first and validate the parsed arguments against them while still parsing, which surfaced the mismatch rate before anything else moved; then switch the transport with the prompt otherwise untouched; then strip the exemplars and the label instructions, which by that point were dead weight the model was still imitating. Each step is independently revertible and the middle one is where the interesting diff appears.

What the mapping does not do

It does not change the loop. Message accumulation, dispatch, the iteration cap and the exit conditions from the earlier topic are identical on both sides of the migration, which is why the diff for the acquired service touched the protocol layer and the prompt and left TurnOrchestrator's control flow largely alone. Anyone expecting the migration to also fix a runaway-iteration problem was expecting the wrong thing.

It does not make the agent choose better. The tool selection, the argument values and the decision to stop are all still the model's, and a migrated agent will make the same judgement errors as before, now with well-formed arguments. Nor does it remove the injection exposure from chapter three, since a persuaded model emits a genuine call; what it removes is the ability of text to become a call by being shaped like one, and the triage in that chapter still has to be done.

It also is not free in every direction. Reasoning that used to be plain text in your transcript may now arrive as provider-specific blocks with rules attached — some providers require thinking blocks to be passed back verbatim on subsequent turns for the model to remain consistent, and some sign or encrypt them — so the code that copies assistant content into the next request needs to preserve blocks it cannot read. That is a real constraint and it is the price of the model's reasoning being the provider's structure rather than your string.

Read as a whole, the mapping is unglamorous in the mechanical direction and consequential in exactly one. Reasoning, action and result all have destinations, the alternation survives, and a team that expected to rewrite an agent finds itself deleting a regular expression and a stop sequence. The single substantive change is that the reasoning arrives as content nothing branches on, which removes both a control-flow dependency on prose the model chose and an audit record that could never have supported the weight put on it. The replacement evidence is not a worse version of the reflection field. It is the set of facts the typed protocol produces anyway — which call, which arguments, which history, which policy, which principal, which result — and it has the property the reflection lacked, which is that it says something different when something different happened.

All of this assumes a provider that offers tool calling, which is true of every hosted frontier model and of most serving stacks. The remaining case is a local model behind an inference server that exposes only completions, and that case is the one honest argument left for writing a parser. It is also weaker than it looks, because there is a third option that most teams never put on the table.

ReAct as history — read the current version at https://daviddominguez.dev/learn/en/react-as-history/thought-becomes-content-nothing-branches-on-action-becomes-a-call

Back to top