From 932b4f0a7fcb19733c74e7fb9a5efbda8a487639 Mon Sep 17 00:00:00 2001 From: worldmozara Date: Tue, 6 Oct 2026 23:23:12 +0800 Subject: [PATCH] core/agent: keep a rolled-back turn's reasoning with its calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A failed turn keeps the calls that already ran as a bare pairing carrier, and clipping the chain-of-thought off that carrier sent the call back to the API with no reasoning at all — thinking mode refuses a call whose reasoning never came back. The carrier keeps the reasoning and drops only the text. --- src/plyngent/agent/chat.py | 9 ++++++--- tests/test_agent/test_rollback.py | 11 ++++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/plyngent/agent/chat.py b/src/plyngent/agent/chat.py index 32eb5f1..2ade16b 100644 --- a/src/plyngent/agent/chat.py +++ b/src/plyngent/agent/chat.py @@ -256,8 +256,11 @@ def _executed_tool_pair( """Assistant call record plus the results of the calls that produced one. Pairs by call id (first result per id), so the history keeps the API's - one-result-per-call shape even when a model repeats an id. Text and - reasoning go with the rest of the unfinished turn. + one-result-per-call shape even when a model repeats an id. Text goes with + the rest of the unfinished turn; the reasoning stays, because thinking mode + refuses a tool call whose reasoning never came back (see + ``responses_bridge`` / the DeepSeek chat client, which can only send an + empty part once the real chain-of-thought is gone). """ by_id: dict[str, ToolChatMessage] = {} for result in results: @@ -273,7 +276,7 @@ def _executed_tool_pair( paired.append(by_id[call.id]) if not executed: return [] - record = msgspec.structs.replace(assistant, content=UNSET, reasoning_content=UNSET, tool_calls=executed) + record = msgspec.structs.replace(assistant, content=UNSET, tool_calls=executed) return [record, *paired] diff --git a/tests/test_agent/test_rollback.py b/tests/test_agent/test_rollback.py index c4f68b8..8453a2c 100644 --- a/tests/test_agent/test_rollback.py +++ b/tests/test_agent/test_rollback.py @@ -28,7 +28,11 @@ def _call(call_id: str, name: str = "some_tool") -> AssistantFunctionToolCall: def test_rollback_tail_keeps_the_executed_call_of_a_cut_short_batch() -> None: messages: list[AnyChatMessage] = [ UserChatMessage(content="do both"), - AssistantChatMessage(content="first, then the other", tool_calls=[_call("call_a"), _call("call_b")]), + AssistantChatMessage( + content="first, then the other", + reasoning_content="step by step", + tool_calls=[_call("call_a"), _call("call_b")], + ), ToolChatMessage(tool_call_id="call_a", content="a ran"), ] @@ -37,9 +41,10 @@ def test_rollback_tail_keeps_the_executed_call_of_a_cut_short_batch() -> None: assert len(tail) == 2 record, result = tail assert isinstance(record, AssistantChatMessage) - # The assistant row survives only as the carrier of the pairing. + # The assistant row survives as the carrier of the pairing, with the + # reasoning thinking mode demands on a call. assert record.content is UNSET - assert record.reasoning_content is UNSET + assert record.reasoning_content == "step by step" calls = record.tool_calls assert calls is not UNSET assert [call.id for call in calls] == ["call_a"]