Skip to content

ReActV2 finish: :submit: a thought-only step continues before submit is forced - #346

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

deepfates merged 2 commits into
mainfrom
react-v2-thought-continues

Conversation

@deepfates

@deepfates deepfates commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

User story

A host runs a program with finish: :submit, so the person sees only what goes through submit. Today, when the model writes a thought and calls no tool, Imp immediately sends the forced submit with every other tool removed. This goes wrong in three ways:

  • It cuts off what the model said it would do. "Let me read the thread first." must now be answered without reading the thread.
  • It adds a request that can lose the reply. An ordinary provider error on the forced request goes to last_request_failed with no fallback, and so does a spend stop. Neither does the forced submit check whether the deadline has passed, as the last text request does.
  • The extractor's output gets published. When the forced request falls through to the extractor, its instructions ask for an honest report of limitations. A host then publishes that report as the answer.

Change

A thought-only step continues the loop (finish: :submit only).

  • The next request is an ordinary step: every tool and tool_choice: "auto".
  • A second thought-only step in a row is the :empty_tool_calls interruption, and the forced submit follows as before. So is a step that says nothing, or only whitespace: there is no thought to show, and the same request would be sent again.
  • A step that calls a tool resets the count.
  • max_iters still ends the turn with the forced submit.
  • Signatures that get submit under finish: :auto keep DSPy's ending, which forces at once. This scope is deliberate: the measurement below covers single-text final steps only.
  • The bound is one continued step, and it is not an option. One ordinary step lets the model take the action it announced, or submit what it wrote. In the measurement, a model that writes text a second time is about one sample in eight, and continuing again would mostly spend more requests before the same forced submit. If a host needs a different bound, it can become an option then.

No request after the deadline. If the process's Imp.Deadline has passed, none of these is sent:

  • the continued step;
  • the forced submit;
  • the "required" retry;
  • the extraction request.

The turn ends :incomplete with termination_cause: :deadline_exceeded, as the last text request already does.

Extracted outputs are recorded in the history. On an :extracted turn, the history used to end on the forced step's thought. It now ends with the extracted outputs, recorded as a step that called no tool, together with any inputs no step spent. The host's record then holds what the turn produced, and the episode is delimited by its outputs as a submit turn's is.

  • This step renders no message on replay. thought_as_request therefore looks past it.
  • Without that, resuming after an :extracted turn would end on the forced step's thought as an assistant message. Mistral and Gemini refuse such a request.

What changes under finish: :auto. The default is not entirely unchanged. For every signature with submit:

  • the deadline checks above apply;
  • an :extracted turn's history now ends with the extracted outputs.

The continuing step applies only under finish: :submit.

Evidence

Probe 5 (2026-10-16): 42 real final steps from 11 models, sent through OpenRouter in the request shape above. On the ordinary step after a thought-only step:

  • Most samples called submit with the thought's reply, copied or paraphrased. Meta's models did so 6 of 6 with nothing forced, which matters because their provider cannot force a tool. Kimi, GPT and Gemini models did the same.
  • A thought that was a closing note was submitted as an empty answer.
  • A DeepSeek model went on to fetch what it had said it would read.
  • 5 of 42 wrote text again.

The measurement is recorded in decisions.md, with react.md sections 2 and 3 and the termination tables updated.

Open

On replay, the extracted outputs are in the record but not shown to the model. On a continued conversation, the model reads the forced step's thought, shown as a user message, but not the answer the extractor produced. A submit turn shows its answer through the submit call.

Rendering the extracted outputs would show the model an answer it did not write itself. It would also change the prompt for every replayed history that holds one. I chose to look past the step instead, and the trade-off is left here for review.

Tests

test/react_v2_thought_continues_test.exs, plus one updated test in test/react_v2_finish_submit_test.exs. Each fails on main:

  • thought, then the next ordinary step submits (:submit; the second request is auto with every tool and ends on the thought);
  • thought, thought, then forced submit (:forced_submit, :empty_tool_calls);
  • thought, tool call, thought, submit: every request is auto, and the tool call resets the count;
  • the step limit after a thought still forces submit (:max_iters);
  • a deadline that has passed sends no forced submit (:incomplete, :deadline_exceeded, one request);
  • a deadline that passes before the continued step, the "required" retry or the extractor sends no further request;
  • a whitespace-only thought is not continued;
  • extracted outputs end the history;
  • resuming after an :extracted turn does not end on the model's turn.

mix check and mix quality.check pass.

…e submit is forced

Under finish: :submit a step that writes a thought and calls no tool is followed by an ordinary step (every tool, tool_choice auto, the thought as the request's last user message). A second thought-only step in a row, or one that says nothing, is the :empty_tool_calls interruption and forces submit; max_iters and the deadline still do. Measured on 42 real final steps from 11 models: most submitted the thought's reply unforced, Meta's models included; some went on with the announced work; 5 of 42 wrote text again.

The forced submit is not sent once the deadline has passed, as the last text request already was not. An :extracted turn's history now ends with the extracted outputs, recorded as a step that called no tool.
…n ends on the user side

thought_as_request looks past a last step that called nothing and wrote nothing (the extracted outputs), so resuming after an :extracted turn no longer ends on the forced step's thought as an assistant message. No continued step, required retry or extraction request is sent once the deadline has passed. A whitespace-only thought is not continued. Docs: the deadline ends the turn incomplete rather than forcing submit; with written tool calls the request ends on the tool listing; the react.md example makes the lookup it announces.
@deepfates
deepfates enabled auto-merge (squash) October 16, 2026 05:09
@deepfates
deepfates merged commit cc51859 into main Oct 16, 2026
13 checks passed
@deepfates
deepfates deleted the react-v2-thought-continues branch October 16, 2026 05:15
deepfates added a commit that referenced this pull request Oct 16, 2026
…say it was not sent (#347)

* ReActV2 finish: :submit: keep the thought as the model's message and say it was not sent

On the step that follows a thought-only step, the thought now stays the model's own (assistant) message and one user message follows it: "Your message above was not sent. To reply, call `submit` with <the outputs>; to keep working, call a tool." It follows every thought-only step, in later requests and on replay, so each request extends the previous one. The forced submit keeps its shape: its last thought is shown as the request's last user message, with no note after it.

Measured on 21 real final steps x 2 samples across 11 model families: the thought shown back as a user message had 5 samples write text again, and some models took it for an echo and submitted nothing; with the note, none wrote text again and replies were submitted intact.

Also widens the margins of the deadline tests from #346, which could fail on a loaded machine.

* ReActV2: no unsent note after an answered thought; drop it on the forced request only when the thought moves

A thought the turn's extracted outputs directly follow was answered, so on replay it gets no "Your message above was not sent" line; the extracted outputs are still not rendered. The forced submit drops the line after its last thought only when thought_as_request moves that thought to the user side, so a forced request with new inputs keeps it. Docs that said Imp adds nothing to the last request now name the unsent line as the exception.
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