Skip to content

feat: rewrite react around raw history#9

Draft
isaacbmiller wants to merge 1 commit intocmpnd/react-native-toolsfrom
cmpnd/react-runtime-history
Draft

feat: rewrite react around raw history#9
isaacbmiller wants to merge 1 commit intocmpnd/react-native-toolsfrom
cmpnd/react-runtime-history

Conversation

@isaacbmiller
Copy link
Copy Markdown

@isaacbmiller isaacbmiller commented Mar 13, 2026

Why this PR exists

ReAct is still a declarative DSPy module. What changes here is how the signature gets enacted.

Before this PR, ReAct executed through a flattened trajectory dict and a synthetic finish tool. That works, but it leaves a few real limitations:

  • execution state is not a real transcript
  • the final assistant answer is not part of the interaction history
  • follow-up turns like "convert that number to C" do not have a faithful antecedent in history
  • completion is split between finish and a separate extract pass instead of being a typed commit of the declared outputs

This PR moves ReAct to a transcript-native runtime built on History.raw() while keeping the declarative signature model intact.

What changes

  • History.raw() becomes the canonical execution state for ReAct
  • finish is replaced with typed submit(**signature_outputs)
  • successful completion materializes the final assistant answer back into history
  • extract(history=...) remains as the fallback path, but it is normalized into the same terminal transcript shape
  • trajectory remains only as a derived compatibility output

What this unlocks

1. Follow-up turns that refer to prior results

This PR makes the final user-facing answer part of the transcript, not just an extracted side effect.

That means patterns like this become well-founded:

react = dspy.ReAct("question -> answer", tools=[weather, convert_units])

first = react(question="What is the weather in Paris in F?")
second = react(question="Convert that number to C.", history=first.history)

The second turn now has a real antecedent in history:

  • the prior user request
  • the assistant tool call
  • the tool observation
  • the final assistant answer

That is a much better execution model than asking the model to infer meaning from a flattened trajectory blob.

2. A transcript that matches the actual runtime

The runtime now records real assistant/tool turns instead of encoding everything into thought_i, tool_name_i, observation_i, etc.

That has two benefits:

  • inspect_history() becomes a faithful debugging tool
  • future resumability can be built on serialized History instead of hidden in-memory state

The compaction summaries that may appear in that history come from the underlying DSPy summary program introduced earlier in the stack, so even prompt compression stays within the DSPy execution model.

3. Declarative completion instead of ad hoc finish semantics

submit(**signature_outputs) commits the declared outputs directly.

For a simple signature:

react = dspy.ReAct("question -> answer", tools=[search])
result = react(question="...")
print(result.answer)

For a multi-output signature, completion is still typed and still canonical. The runtime materializes the final assistant message from the declared outputs rather than inventing a second free-form response channel.

4. A cleaner substrate for the next layer of ReAct features

This PR is the runtime rewrite, not the entire roadmap. But it creates the right foundation for:

  • resumable runs from serialized history
  • stronger history-backed optimization traces
  • more modern tool-execution behavior over a real transcript

DX after this change

The common call site does not get more complicated:

react = dspy.ReAct("question -> answer", tools=[search, lookup])
result = react(question="What is the weather in Paris in F?")
print(result.answer)

The new pattern this enables is follow-up interaction over prior execution state:

result = react(question="Convert that number to C.", history=result.history)

The important part is that history is now the canonical state of the run. trajectory still exists for compatibility, but it is no longer the thing the runtime is built around.

Compatibility and review guidance

  • ReAct remains a declarative DSPy module
  • saved-program loading is preserved
  • extract is still present, but only as the fallback completion path
  • trajectory remains available as a derived compatibility output

Review this PR as the ReAct runtime rewrite itself. The adapter/native-tool substrate is intentionally below it in the stack.

Diff

  • 3 files changed
  • 791 insertions, 510 deletions

Validation

  • uv run pytest --deno -q tests/predict/test_react.py tests/predict/test_code_act.py tests/primitives/test_module.py -k "load_dspy_program_cross_version or test_react or test_code_act"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant