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:
@@ -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
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user