test(agent): lock the reasoning-only stop row shape on the stall-guard and prefill paths

#113188 (merge 578bff8) keeps a reasoning-only clean stop out of the assistant
row's ``content`` (promoted text lives in the ``api_content`` sidecar) and the
merge commit stamped the same sidecar on the stall-guard / codex-ack interim row
and made the gateway history rebuild replay sidecar-only rows. The interim-row
half landed without a test, and the thinking-prefill stub named in #111761 had
no guard that its ``content`` stays empty.

- stall-guard interim row: promoted reasoning that trips
  ``trailing_continue_intent`` must persist ``content=""`` + ``api_content`` and
  the continuation request must carry the text as the assistant turn (red on
  578bff8^, where the interim row had no sidecar).
- thinking-prefill stub: the row appended by ``recover_empty_response`` keeps
  the reasoning in its reasoning fields only, and no transcript row ever stores
  the chain-of-thought as ordinary content (red when the stub copies reasoning
  into ``content``).

Part of #111761
This commit is contained in:
teknium1
2026-09-16 22:00:13 -07:00
committed by Teknium
parent d41aec0beb
commit 194695b58a
2 changed files with 63 additions and 0 deletions

View File

@@ -78,6 +78,35 @@ def test_promoted_reasoning_is_returned_but_persisted_row_keeps_content_empty(lo
assert "api_content" not in assistant_rows[0]
def test_stall_guard_interim_row_carries_promoted_text_as_sidecar(loop_agent):
"""Promoted reasoning that tails on an announced next action trips the stall guard; the interim
row it appends must follow the same shape as the final row — ``content`` empty, the promoted
text in ``api_content`` — so the continuation request replays a real assistant turn, not an
empty one (#111761)."""
from tests.agent.test_run_agent import _mock_response
stalled = "The user wants the file contents. Let me now read the file."
loop_agent.valid_tool_names = {"read_file"}
loop_agent._stall_guards = True
result = _run(loop_agent, [
_mock_response(content="", finish_reason="stop", reasoning_content=stalled),
_mock_response(content="Here is the file.", finish_reason="stop"),
])
assert result["final_response"] == "Here is the file."
interim = [m for m in result["messages"] if m.get("role") == "assistant"][0]
assert not interim.get("content")
assert interim["reasoning"] == stalled
assert interim["api_content"] == stalled
# The continuation request carried the promoted text as the interim assistant turn.
second_call = loop_agent.client.chat.completions.create.call_args_list[1].kwargs["messages"]
interim_wire = [m for m in second_call if m.get("role") == "assistant"][0]
assert interim_wire["content"] == stalled
assert "api_content" not in interim_wire
def test_reasoning_only_clean_stop_logs_warning_with_route(loop_agent, caplog):
from tests.agent.test_run_agent import _mock_response

View File

@@ -127,6 +127,40 @@ class TestThinkingPrefillTrailingTurn:
f"Empty assistant stub(s) reached the wire: {empty_assistants}"
)
def test_prefill_row_keeps_reasoning_out_of_content(self, loop_agent):
"""The prefill stub carries the model's reasoning in its reasoning fields only: its
``content`` stays empty when appended, and no transcript row ever stores the
chain-of-thought as an ordinary reply (#111761)."""
import agent.turn_empty_response as ter
reasoning = "Let me work through the request step by step."
loop_agent.client.chat.completions.create.side_effect = [
_thinking_only_response(),
_final_response(),
]
appended = []
real_append = ter.append_message
def spy(messages, msg, *args, **kwargs):
appended.append(dict(msg))
return real_append(messages, msg, *args, **kwargs)
with (
patch.object(ter, "append_message", spy),
patch.object(loop_agent, "_persist_session"),
patch.object(loop_agent, "_save_trajectory"),
patch.object(loop_agent, "_cleanup_task_resources"),
):
result = loop_agent.run_conversation("do the thing")
stubs = [m for m in appended if m.get("_thinking_prefill")]
assert len(stubs) == 1
assert not (stubs[0].get("content") or "").strip()
assert stubs[0]["reasoning"] == reasoning
assert not any(
m.get("role") == "assistant" and m.get("content") == reasoning for m in result["messages"]
)
def test_internal_marker_never_reaches_the_wire(self, loop_agent):
"""``_thinking_prefill`` survives the API-copy build on purpose, but the
transport must still keep it off the wire."""