from types import SimpleNamespace import pytest from litellm import ChatCompletionMessageToolCall, Message from agent.core import agent_loop from agent.core.agent_loop import ( LLMResult, _call_llm_streaming, _assistant_message_from_result, _extract_thinking_state, ) def test_extract_thinking_state_from_litellm_message(): message = Message( role="assistant", content="working", thinking_blocks=[{"type": "thinking", "thinking": "reasoned"}], reasoning_content="reasoned", ) thinking_blocks, reasoning_content = _extract_thinking_state(message) assert thinking_blocks == [{"type": "thinking", "thinking": "reasoned"}] assert reasoning_content == "reasoned" def test_extract_thinking_state_from_provider_fields(): message = SimpleNamespace( provider_specific_fields={ "thinking_blocks": [{"type": "thinking", "thinking": "reasoned"}], "reasoning_content": "reasoned", }, ) thinking_blocks, reasoning_content = _extract_thinking_state(message) assert thinking_blocks == [{"type": "thinking", "thinking": "reasoned"}] assert reasoning_content == "reasoned" def test_assistant_message_from_result_preserves_thinking_with_tool_calls(): tool_call = ChatCompletionMessageToolCall( id="call_1", type="function", function={"name": "bash", "arguments": '{"command": "date"}'}, ) result = LLMResult( content=None, tool_calls_acc={}, token_count=12, finish_reason="tool_calls", thinking_blocks=[{"type": "thinking", "thinking": "reasoned"}], reasoning_content="reasoned", ) message = _assistant_message_from_result( result, model_name="anthropic/claude-opus-4-6", tool_calls=[tool_call], ) assert message.tool_calls == [tool_call] assert message.thinking_blocks == [{"type": "thinking", "thinking": "reasoned"}] assert message.reasoning_content == "reasoned" def test_assistant_message_from_result_strips_non_anthropic_reasoning_content(): result = LLMResult( content=None, tool_calls_acc={}, token_count=12, finish_reason="tool_calls", thinking_blocks=[{"type": "thinking", "thinking": "reasoned"}], reasoning_content="reasoned", ) message = _assistant_message_from_result( result, model_name="openai/Qwen/Qwen3-Next-80B-A3B-Instruct", ) assert getattr(message, "thinking_blocks", None) is None assert getattr(message, "reasoning_content", None) is None def test_assistant_message_from_result_omits_absent_thinking_fields(): result = LLMResult( content="done", tool_calls_acc={}, token_count=12, finish_reason="stop", ) message = _assistant_message_from_result( result, model_name="anthropic/claude-opus-4-6", ) assert message.content == "done" assert getattr(message, "thinking_blocks", None) is None assert getattr(message, "reasoning_content", None) is None @pytest.mark.asyncio async def test_streaming_call_rebuilds_anthropic_thinking_state(monkeypatch): async def fake_stream(): yield SimpleNamespace( choices=[ SimpleNamespace( delta=SimpleNamespace(content="done", tool_calls=None), finish_reason="stop", ) ], ) yield SimpleNamespace(choices=[], usage=SimpleNamespace(total_tokens=3)) async def fake_acompletion(**_kwargs): return fake_stream() def fake_chunk_builder(chunks, **_kwargs): assert len(chunks) == 2 return SimpleNamespace( choices=[ SimpleNamespace( message=Message( role="assistant", content="done", thinking_blocks=[{"type": "thinking", "thinking": "reasoned"}], reasoning_content="reasoned", ) ) ] ) events = [] async def send_event(event): events.append(event) session = SimpleNamespace( config=SimpleNamespace(model_name="anthropic/claude-opus-4-6"), is_cancelled=False, send_event=send_event, ) monkeypatch.setattr(agent_loop, "acompletion", fake_acompletion) monkeypatch.setattr(agent_loop, "stream_chunk_builder", fake_chunk_builder) result = await _call_llm_streaming( session, messages=[Message(role="user", content="hi")], tools=[], llm_params={"model": "anthropic/claude-opus-4-6"}, ) assert result.content == "done" assert result.thinking_blocks == [{"type": "thinking", "thinking": "reasoned"}] assert result.reasoning_content == "reasoned" @pytest.mark.asyncio async def test_streaming_call_rebuilds_anthropic_delta_thinking_state(monkeypatch): async def fake_stream(): yield SimpleNamespace( choices=[ SimpleNamespace( delta=SimpleNamespace( content=None, tool_calls=None, thinking_blocks=[ { "type": "thinking", "thinking": "reasoned", "signature": "", } ], ), finish_reason=None, ) ], ) yield SimpleNamespace( choices=[ SimpleNamespace( delta=SimpleNamespace( content=None, tool_calls=None, thinking_blocks=[ { "type": "thinking", "thinking": "", "signature": "signed", } ], ), finish_reason=None, ) ], ) yield SimpleNamespace( choices=[ SimpleNamespace( delta=SimpleNamespace(content="done", tool_calls=None), finish_reason="stop", ) ], ) yield SimpleNamespace(choices=[], usage=SimpleNamespace(total_tokens=3)) async def fake_acompletion(**_kwargs): return fake_stream() def fake_chunk_builder(chunks, **_kwargs): assert len(chunks) == 4 return SimpleNamespace( choices=[ SimpleNamespace( message=Message( role="assistant", content="done", thinking_blocks=[ { "type": "thinking", "thinking": "reasoned", "signature": "signed", } ], reasoning_content="reasoned", ) ) ] ) events = [] async def send_event(event): events.append(event) session = SimpleNamespace( config=SimpleNamespace(model_name="anthropic/claude-opus-4-7"), is_cancelled=False, send_event=send_event, ) monkeypatch.setattr(agent_loop, "acompletion", fake_acompletion) monkeypatch.setattr(agent_loop, "stream_chunk_builder", fake_chunk_builder) result = await _call_llm_streaming( session, messages=[Message(role="user", content="hi")], tools=[], llm_params={"model": "anthropic/claude-opus-4-7"}, ) assert result.content == "done" assert result.thinking_blocks == [ {"type": "thinking", "thinking": "reasoned", "signature": "signed"} ] assert result.reasoning_content == "reasoned" @pytest.mark.asyncio async def test_streaming_call_skips_chunk_rebuild_for_non_anthropic(monkeypatch): async def fake_stream(): yield SimpleNamespace( choices=[ SimpleNamespace( delta=SimpleNamespace(content="done", tool_calls=None), finish_reason="stop", ) ], ) async def fake_acompletion(**_kwargs): return fake_stream() def fail_chunk_builder(*_args, **_kwargs): raise AssertionError("stream_chunk_builder should not run") events = [] async def send_event(event): events.append(event) session = SimpleNamespace( config=SimpleNamespace(model_name="openai/Qwen/Qwen3"), is_cancelled=False, send_event=send_event, ) monkeypatch.setattr(agent_loop, "acompletion", fake_acompletion) monkeypatch.setattr(agent_loop, "stream_chunk_builder", fail_chunk_builder) result = await _call_llm_streaming( session, messages=[Message(role="user", content="hi")], tools=[], llm_params={"model": "openai/Qwen/Qwen3"}, ) assert result.content == "done" assert result.thinking_blocks is None assert result.reasoning_content is None