diff --git a/src/clean_agent_preview.py b/src/clean_agent_preview.py index 1d5b466a5..4589f7329 100644 --- a/src/clean_agent_preview.py +++ b/src/clean_agent_preview.py @@ -3899,6 +3899,7 @@ def provider_wire_messages(messages): item.get('role') == 'assistant' and not item.get('content') and not item.get('tool_calls') + and not item.get('reasoning_content') ): continue cleaned.append(item) @@ -4483,7 +4484,7 @@ async def stream_preview(*, endpoint_url, model, messages, headers, turn_contrac 'function': {'name': private_browser_name}, } force_private_browser_next_round = False - pending, content = {}, '' + pending, content, round_reasoning = {}, '', '' streamed_round_text = False request = search_tool_choice_request(request) request = provider_compatible_tool_choice_request(request, model) @@ -4528,6 +4529,14 @@ async def stream_preview(*, endpoint_url, model, messages, headers, turn_contrac if not choices: continue delta = choices[0].get('delta') or {} + reasoning = ( + delta.get('reasoning_content') + or delta.get('reasoning') + or delta.get('thinking') + or '' + ) + if reasoning: + round_reasoning += str(reasoning) text = delta.get('content') or '' if text: first_token = first_token or time.monotonic() @@ -4610,6 +4619,12 @@ async def stream_preview(*, endpoint_url, model, messages, headers, turn_contrac yield event({'type': 'model_tool_proposal', 'round': round_number, 'function': proposal.get('function', {})}) message = {'role': 'assistant', 'content': content or None} + if round_reasoning and 'deepseek' in str(model or '').casefold(): + # DeepSeek requires each tool-round reasoning payload to + # be echoed verbatim on subsequent requests. Unlike local + # Qwen/Nemotron templates, its API owns this structured + # field and does not reinterpret it as visible output. + message['reasoning_content'] = round_reasoning if proposed: message['tool_calls'] = protocol_safe_tool_calls(proposed) history.append(message) diff --git a/tests/test_clean_agent_preview.py b/tests/test_clean_agent_preview.py index 5dd85663b..05c8376c3 100644 --- a/tests/test_clean_agent_preview.py +++ b/tests/test_clean_agent_preview.py @@ -531,18 +531,30 @@ def test_repeated_off_contract_artifact_calls_trigger_single_file_body_handoff() ) +def test_provider_wire_keeps_deepseek_reasoning_only_turn_for_continuity(): + import src.clean_agent_preview as module + + messages = [{ + 'role': 'assistant', + 'content': None, + 'reasoning_content': 'private provider reasoning token stream', + }] + + assert module.provider_wire_messages(messages) == messages + + @pytest.mark.asyncio async def test_repeated_off_contract_calls_recover_via_required_artifact_body(monkeypatch): import src.clean_agent_preview as module responses = iter([ - {'choices': [{'delta': {'tool_calls': [{ + {'choices': [{'delta': {'reasoning_content': 'reasoning-one', 'tool_calls': [{ 'index': 0, 'id': 'bad-bash', 'function': {'name': 'bash', 'arguments': json.dumps({ 'command': 'echo nope', })}, }]}}]}, - {'choices': [{'delta': {'tool_calls': [{ + {'choices': [{'delta': {'reasoning_content': 'reasoning-two', 'tool_calls': [{ 'index': 0, 'id': 'bad-python', 'function': {'name': 'python', 'arguments': json.dumps({ 'code': 'print("nope")', @@ -566,11 +578,14 @@ async def test_repeated_off_contract_calls_recover_via_required_artifact_body(mo yield 'data: ' + json.dumps(self.payload) yield 'data: [DONE]' + requests = [] + class Client: def __init__(self, **kwargs): pass async def __aenter__(self): return self async def __aexit__(self, *args): pass def stream(self, *args, **kwargs): + requests.append(kwargs['json']) return Response(next(responses)) executed = [] @@ -602,6 +617,11 @@ async def test_repeated_off_contract_calls_recover_via_required_artifact_body(mo )] assert [block.tool_type for block in executed] == ['write_file'] + prior_tool_turn = next( + message for message in requests[1]['messages'] + if message.get('role') == 'assistant' and message.get('tool_calls') + ) + assert prior_tool_turn['reasoning_content'] == 'reasoning-one' assert executed[0].content == ( '/workspace/output.html\nRecovered' )