Skip to content

ReActV2: a plain-text answer step is never a thought; plain-text replay keeps roles alternating - #349

Merged
deepfates merged 2 commits into
mainfrom
react-v2-answer-not-thought
Oct 16, 2026
Merged

deepfates merged 2 commits into
mainfrom
react-v2-answer-not-thought

Conversation

@deepfates

@deepfates deepfates commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

User story

A host changes a program's ending mid-conversation, from finish: :auto to finish: :submit, and passes the same history on.

Under finish: :auto, a step that answered in plain text is recorded with its text and the signature's outputs. In 0.14.0 the Chat adapter took that step for a thought-only step under finish: :submit. So "Your message above was not sent. To reply, call submit with …" followed every answer that had in fact been delivered. Models then sent those answers again. In one rehearsal a model re-filed an answer it said it had "composed yesterday and never cleared the send queue".

Change

An answer is never a thought.

  • A step that carries any of the signature's outputs is an answer. It gets no unsent line, and thought_as_request never moves it to the user side.
  • A submit step's answer is its call, so it was already excluded.
  • Only a step with text, no calls and no outputs is a thought.
  • The adapter takes the output names from the loop's guidance.

The reverse case: a submit-era history replayed in a plain-text loop. A thought-only step used to render as a bare assistant message, often right before the answer's. That is two assistant messages in a row, and in a plain-text loop it reads as a message that was sent.

  • Each genuine thought-only step (text, no calls, no outputs) is now followed by a user message: "Your message above was not sent."
  • This is the factual half of the line. The submit instruction is left out because a plain-text loop has no submit, so naming it would be false.
  • Roles alternate (thought, line, answer), and nothing is merged or rewritten.
  • The sentence comes from ReActV2's guidance for a plain-text signature under finish: :auto.

Plain-text-only histories are unchanged. A history written only in a plain-text loop has no thought-only step: its text steps carry the answer. So its requests are byte-for-byte what 0.14.0 sent.

  • A test asserts the exact replayed request.
  • I also compared the full request term against origin/main. Both SHA-256 hashes match.

Still two assistant messages in a row, outside this rule: one existing case still replays that way under :auto. A step whose only call was a submit the loop rejected loses the call and its error result on replay, so its text appears bare. That step had a call, so it is not a thought-only step under the definition here. Its existing test (react_v2_test.exs, "a rejected or id-less recorded submit…") is restored unchanged.

Known gap

Under finish: :submit, consider a forced request with nothing new to ask (no inputs, no note) on a history whose last step is a plain-text answer. It still ends on that assistant message, because an answer is not moved. Resuming with new inputs or a note ends on a user turn. This is stated in react.md section 3 and in the CHANGELOG.

Tests

test/react_v2_ending_switch_test.exs. The first two fail on main; the third passes on main and here by design:

  • Two answered turns from finish: :auto, replayed under finish: :submit: user, answer, user, answer, user. There is no line.
  • A submit-era thought plus answer, replayed under finish: :auto: thought, "Your message above was not sent.", answer, then the new ticket.
  • A plain-text-only history (answers and a tool step) replays exactly as before, with no "not sent" anywhere.

There are CHANGELOG entries under Fixed, including the known gap. mix check and mix quality.check pass.

…ay keeps roles alternating

Under finish: :submit, a step recorded under finish: :auto as the turn's plain-text answer (text plus the signature's outputs) was treated as a thought-only step: the unsent line followed each delivered answer, and models re-sent them. A step that carries the outputs is an answer: no line, never moved to the user side.

In a plain-text loop, a submit-era thought-only step followed by its submit step replayed as two assistant messages in a row; they are now one message, the thought then the answer, as a step that wrote both renders.
…instead of joining it to the answer

Joining a thought-only step to the answer that followed presented an unsent thought as part of a sent answer. A plain-text loop now follows each thought-only step with a user message, "Your message above was not sent." (no submit to name there), so roles alternate and nothing is merged. A history written only in a plain-text loop has no thought-only step and replays byte-for-byte as before; the existing rejected-submit test is restored as it was.
@deepfates
deepfates enabled auto-merge (squash) October 16, 2026 08:06
@deepfates
deepfates merged commit a520eeb into main Oct 16, 2026
13 checks passed
@deepfates
deepfates deleted the react-v2-answer-not-thought branch October 16, 2026 08:12
deepfates added a commit that referenced this pull request Oct 16, 2026
Version 0.14.1 with tag links for v0.14.1; install lines stay ~> 0.14.0, as
for earlier patch releases. The changelog's Unreleased section goes under
0.14.1, and the release notes describe #349.
A patch: #349 adds no option and no public API, and fixes how ReActV2
replays histories that cross 0.14.0's opt-in finish: :submit and the default
:auto. A history written only in a plain-text loop replays byte-for-byte as
in 0.14.0.
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