DSPy's changelog does the new module a disservice. dspy.ReActV2, it says, "is a new version of ReAct built around native tool calling." True, and it makes it sound like a plumbing upgrade — the framework learned to emit OpenAI-style tool calls instead of parsing them out of freeform text. Useful, unglamorous, skippable.

It is not skippable. The change underneath the tool-calling headline fixes a cost bug that has been quietly taxing ReAct agents since the pattern was invented, and the fix is worth understanding because it is not really about DSPy. It is about the single most expensive decision in any agent loop: how you store the memory of what already happened.

The trajectory was the problem#

Classic ReAct keeps a trajectory — the running log of thought, action, observation, thought, action, observation. In DSPy's original implementation, that trajectory was a string, and on every step of the loop it was serialized and dropped into a single user message, then the whole thing was sent to the model to produce the next action.

Read that again with a bill in mind. Every step re-sends the entire history as fresh prompt text. Step one sends a little. Step ten sends everything from steps one through nine, plus the system prompt and the tool definitions, all as one freshly-rendered blob. The context grows linearly with the number of steps, and you pay for the whole thing at every step, so the total input you're billed for over a run grows with the square of the trajectory. This is the same quadratic that makes long agent runs get expensive faster than people expect — ReAct just walks straight into it.

You would think prompt caching saves you here. That is exactly the feature for it: providers like Anthropic and OpenAI let you pay a small fraction of the input price for a prefix you have already sent, precisely so that repeated, mostly-stable prompts get cheap. Agent loops send "similar prompts repeatedly," as DSPy's own caching guide notes — the textbook case.

Why the cache never fired#

Here is the trap. Provider prompt caching only reuses a prefix that is byte-for-byte identical to a previous request. And the classic ReAct trajectory changed its own prefix as it grew.

A cache keyed on an exact prefix is useless if you rewrite the prefix every turn. ReAct's growing scratchpad did exactly that.

When you flatten thought/action/observation into one string and re-render it, small things shift — spacing, ordering, the way the latest observation is spliced in, the framing text that wraps the whole trajectory. Even when the content of earlier steps is unchanged, the serialized prefix the cache sees is a new string. Miss. Next step, new string. Miss. The one feature designed to make agent loops affordable sat there never firing, because the data structure feeding it was a rewrite-in-place buffer wearing the costume of an append-only log.

What ReActV2 actually changes#

ReActV2 stops flattening. Each turn is stored in dspy.History as structured messages — a user turn, an assistant turn with its tool calls, tool turns with their results — instead of one ever-growing user message. Tools become dspy.Tool objects; the model's calls and their outputs are carried as dspy.ToolCalls and ToolCallResults, each call paired with its result by ID, so parallel tool calls survive intact rather than being smeared back into prose.

The performance consequence falls out of the shape. Prior turns are now appended as stable messages. Turn six does not rewrite turns one through five; it adds to them. The prefix — system prompt, tool definitions, the settled earlier turns — stays identical from one step to the next, which is the one thing the provider cache demands. So it reuses it. DSPy reports "up to 50% decreases in cost for some tasks" from this alone. Not a new model, not fewer steps — the same loop, represented so the cache can do its job.

This is the same principle behind caching tool results instead of re-sending them: the win comes from keeping the expensive, repeated part stable and addressable rather than regenerating it.

The lesson is bigger than one module#

There is a reflex in this field to treat the agent loop — ReAct versus Plan-and-Execute versus Reflexion — as a reasoning-strategy choice, a question of how the agent thinks. ReActV2 is a reminder that the loop is also a data-structure choice, and that choice sets your economics before the model reasons about anything.

The rule it encodes is simple enough to carry to any framework, including one you write yourself: an agent's history should be append-only and byte-stable, because that is the shape a prompt cache can reward. Rewrite your context in place — reformat it, re-summarize it, re-splice it every turn — and you are not just spending tokens, you are actively disabling the discount built to refund them. The trajectory string felt like the natural way to hold an agent's memory. It was also, quietly, the most expensive one.