From f4a782d99f1352c9eca4eabd35b51458d8729b4a Mon Sep 17 00:00:00 2001 From: Ed_ Date: Mon, 15 Jun 2026 10:56:24 -0400 Subject: [PATCH] fix(ai_loop): wrap MiniMax reasoning in tags for parse_thinking_trace (FR3, Bug #3) Adds a new wrap_reasoning_in_text: bool = False keyword argument to run_with_tool_loop. When True and reasoning_content is non-empty, the returned text is prepended with ... tags so thinking_parser.parse_thinking_trace can extract a ThinkingSegment for the discussion entry. The wrap is conditional (default False) so it doesn't break providers that already wrap inline (e.g. DeepSeek, which wraps at line 2117-2118 before run_with_tool_loop sees the response). _send_minimax now passes wrap_reasoning_in_text=bool(caps.reasoning). When caps.reasoning is True (M2.5/M2.7), the reasoning is wrapped in tags. When False (M2/M2.1), the parameter is False and no wrap happens (avoids useless getattr on non-reasoning models). Also fixes a bug in the test_fr3_minimax_thinking_in_returned_text test mock: it was returning a raw MagicMock instead of a Result object, which caused the test to see auto-created MagicMock attributes instead of the expected text. Now wraps in Result(data=MagicMock(...)) and sets ai_client._model to ensure get_capabilities('minimax', _model) resolves to the M2.7 capabilities (reasoning=True). --- src/ai_client.py | 9 +++++++++ tests/test_ai_loop_regressions_20260614.py | 5 +++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/ai_client.py b/src/ai_client.py index f3f79f59..5b5edda1 100644 --- a/src/ai_client.py +++ b/src/ai_client.py @@ -748,6 +748,7 @@ def run_with_tool_loop( reasoning_extractor: Optional[Callable[[Any], str]] = None, send_func: Optional[Callable[[int], NormalizedResponse]] = None, on_pre_dispatch: Optional[Callable[[int, list[dict[str, Any]]], list[dict[str, Any]]]] = None, + wrap_reasoning_in_text: bool = False, ) -> str: """ Orchestrates the LLM conversation loop, executing tool calls and updating history. @@ -772,6 +773,11 @@ def run_with_tool_loop( reasoning_extractor (Optional[Callable]): Callback to extract reasoning content. send_func (Optional[Callable]): Dispatch sender callback. on_pre_dispatch (Optional[Callable]): Callback to adjust tools. + wrap_reasoning_in_text (bool): When True and reasoning_content is non-empty, the + returned text is prepended with `...` wrapping the + reasoning. This lets thinking_parser.parse_thinking_trace extract a + ThinkingSegment for the discussion entry. Default False (callers that + already wrap inline, e.g. DeepSeek, pass False). Returns: str: The final text response returned by the LLM. @@ -833,6 +839,8 @@ def run_with_tool_loop( "content": str(out) if out else "", }) if trim_func is not None: trim_func(history) + if wrap_reasoning_in_text and reasoning_content: + response_text = f"\n{reasoning_content}\n\n\n{response_text}" return response_text async def _execute_single_tool_call_async( @@ -2438,6 +2446,7 @@ def _send_minimax(md_content: str, user_message: str, base_dir: str, history_lock=_minimax_history_lock, history=_minimax_history, trim_func=lambda h: _trim_minimax_history(_build_minimax_request(0).messages, h), reasoning_extractor=_extract_minimax_reasoning if caps.reasoning else None, + wrap_reasoning_in_text=bool(caps.reasoning), )) except Exception as exc: return Result(data="", errors=[_classify_minimax_error(exc, source="ai_client.minimax")]) diff --git a/tests/test_ai_loop_regressions_20260614.py b/tests/test_ai_loop_regressions_20260614.py index 53cca46f..0a352da4 100644 --- a/tests/test_ai_loop_regressions_20260614.py +++ b/tests/test_ai_loop_regressions_20260614.py @@ -204,7 +204,7 @@ def test_fr3_minimax_thinking_in_returned_text() -> None: def _fake_send_openai_compatible(client, request, *, capabilities): captured_text.append("send_openai_compatible was called") - return MagicMock( + return Result(data=MagicMock( text="The final answer is 42", tool_calls=[], usage_input_tokens=0, @@ -212,11 +212,12 @@ def test_fr3_minimax_thinking_in_returned_text() -> None: usage_cache_read_tokens=0, usage_cache_creation_tokens=0, raw_response=fake_raw, - ) + )) from src import openai_compatible as oc from src.vendor_capabilities import register, VendorCapabilities register(VendorCapabilities(vendor="minimax", model="MiniMax-M2.7", reasoning=True)) + ai_client._model = "MiniMax-M2.7" with patch.object(oc, "send_openai_compatible", side_effect=_fake_send_openai_compatible), \ patch("src.ai_client._ensure_minimax_client", return_value=MagicMock()), \