diff --git a/agent/agent_runtime_helpers.py b/agent/agent_runtime_helpers.py index 691d796273f7..ea0a02917eba 100644 --- a/agent/agent_runtime_helpers.py +++ b/agent/agent_runtime_helpers.py @@ -611,7 +611,8 @@ def strip_think_blocks(agent, content: str) -> str: ```` in prose aren't over-stripped. 3. Stray orphan open/close tags that slip through. 4. Tag variants: ````, ````, ````, - ````, ```` (Gemma 4), all + ````, ```` (Gemma 4), + ```` (MiniMax M3), all case-insensitive. Additionally strips standalone tool-call XML blocks that some open @@ -638,6 +639,7 @@ def strip_think_blocks(agent, content: str) -> str: content = re.sub(r'.*?', '', content, flags=re.DOTALL | re.IGNORECASE) content = re.sub(r'.*?', '', content, flags=re.DOTALL | re.IGNORECASE) content = re.sub(r'.*?', '', content, flags=re.DOTALL | re.IGNORECASE) + content = re.sub(r'.*?', '', content, flags=re.DOTALL | re.IGNORECASE) # 1b. Tool-call XML blocks (openclaw/openclaw#67318). Handle the # generic tag names first — they have no attribute gating since # a literal in prose is already vanishingly rare. @@ -667,14 +669,14 @@ def strip_think_blocks(agent, content: str) -> str: # Strip from the tag to end of string. Fixes #8878 / #9568 # (MiniMax M2.7 leaking raw reasoning into assistant content). content = re.sub( - r'(?:^|\n)[ \t]*<(?:think|thinking|reasoning|thought|REASONING_SCRATCHPAD)\b[^>]*>.*$', + r'(?:^|\n)[ \t]*<(?:think|thinking|reasoning|thought|REASONING_SCRATCHPAD|mm:think)\b[^>]*>.*$', '', content, flags=re.DOTALL | re.IGNORECASE, ) # 3. Stray orphan open/close tags that slipped through. content = re.sub( - r'\s*', + r'\s*', '', content, flags=re.IGNORECASE, diff --git a/cli.py b/cli.py index 4dc546bac4e8..ae8e79584879 100644 --- a/cli.py +++ b/cli.py @@ -188,6 +188,7 @@ def realign_markdown_tables(*args, **kwargs): "thinking", "reasoning", "thought", + "mm:think", ) @@ -202,8 +203,8 @@ def _strip_reasoning_tags(text: str) -> str: partial-content dumps. Covers the variants emitted by reasoning models today: ````, - ````, ````, ````, and - ```` (Gemma 4). Must stay in sync with + ````, ````, ````, + ```` (Gemma 4), and ```` (MiniMax M3). Must stay in sync with ``run_agent.py::_strip_think_blocks`` and the stream consumer's ``_OPEN_THINK_TAGS`` / ``_CLOSE_THINK_TAGS`` tuples. @@ -5628,8 +5629,8 @@ def _stream_delta(self, text) -> None: # suppress them during streaming too — unless show_reasoning is # enabled, in which case we route the inner content to the # reasoning display box instead of discarding it. - _OPEN_TAGS = ("", "", "", "", "", "") - _CLOSE_TAGS = ("", "", "", "", "", "") + _OPEN_TAGS = ("", "", "", "", "", "", "") + _CLOSE_TAGS = ("", "", "", "", "", "", "") # Append to a pre-filter buffer first self._stream_prefilt = getattr(self, "_stream_prefilt", "") + text diff --git a/gateway/stream_consumer.py b/gateway/stream_consumer.py index a08e169f2f99..fdef157f538c 100644 --- a/gateway/stream_consumer.py +++ b/gateway/stream_consumer.py @@ -104,11 +104,11 @@ class GatewayStreamConsumer: # run_agent.py _strip_think_blocks() tag variants. _OPEN_THINK_TAGS = ( "", "", "", - "", "", "", + "", "", "", "", ) _CLOSE_THINK_TAGS = ( "", "", "", - "", "", "", + "", "", "", "", ) # Class-wide monotonic counter for native-streaming draft ids. Telegram diff --git a/tests/cli/test_stream_delta_think_tag.py b/tests/cli/test_stream_delta_think_tag.py index 331988bfab1d..ac8ccb703496 100644 --- a/tests/cli/test_stream_delta_think_tag.py +++ b/tests/cli/test_stream_delta_think_tag.py @@ -125,6 +125,15 @@ def test_reasoning_tags_are_case_insensitive(self, tag): assert full == "Visible answer" assert "hidden reasoning" not in full + def test_minimax_mm_think_tag_suppressed(self): + """MiniMax M3's channel marker should not reach the TUI.""" + cli = _make_cli_stub() + cli._stream_delta("provider reasoningVisible answer") + assert not cli._in_reasoning_block + full = "".join(cli._emitted) + assert full == "Visible answer" + assert "provider reasoning" not in full + class TestFlushRecovery: """_flush_stream should recover content from false-positive reasoning blocks.""" diff --git a/tests/gateway/test_stream_consumer.py b/tests/gateway/test_stream_consumer.py index cd49d3d74782..89382e2d5272 100644 --- a/tests/gateway/test_stream_consumer.py +++ b/tests/gateway/test_stream_consumer.py @@ -1452,6 +1452,13 @@ def test_thought_tag_variant(self): c._filter_and_accumulate("Gemma styleOutput") assert c._accumulated == "Output" + def test_minimax_mm_think_tag_variant(self): + """MiniMax M3 emits blocks that must stay hidden.""" + c = _make_consumer() + c._filter_and_accumulate("provider reasoningAnswer") + assert c._accumulated == "Answer" + assert "provider reasoning" not in c._accumulated + def test_reasoning_scratchpad_variant(self): c = _make_consumer() c._filter_and_accumulate( diff --git a/tests/run_agent/test_run_agent.py b/tests/run_agent/test_run_agent.py index da8a446bf8d8..25e28b16335d 100644 --- a/tests/run_agent/test_run_agent.py +++ b/tests/run_agent/test_run_agent.py @@ -392,6 +392,15 @@ def test_thought_block_removed(self, agent): assert "" not in result assert "answer" in result + def test_minimax_mm_think_block_removed(self, agent): + """MiniMax M3 uses tags for provider-side reasoning.""" + result = agent._strip_think_blocks( + "internal reasoning answer" + ) + assert "internal reasoning" not in result + assert "" not in result + assert "answer" in result + def test_orphaned_thought_tag(self, agent): result = agent._strip_think_blocks("orphaned reasoning without close") assert "" not in result