Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions agent/agent_runtime_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -611,7 +611,8 @@ def strip_think_blocks(agent, content: str) -> str:
``<think>`` in prose aren't over-stripped.
3. Stray orphan open/close tags that slip through.
4. Tag variants: ``<think>``, ``<thinking>``, ``<reasoning>``,
``<REASONING_SCRATCHPAD>``, ``<thought>`` (Gemma 4), all
``<REASONING_SCRATCHPAD>``, ``<thought>`` (Gemma 4),
``<mm:think>`` (MiniMax M3), all
case-insensitive.

Additionally strips standalone tool-call XML blocks that some open
Expand All @@ -638,6 +639,7 @@ def strip_think_blocks(agent, content: str) -> str:
content = re.sub(r'<reasoning>.*?</reasoning>', '', content, flags=re.DOTALL | re.IGNORECASE)
content = re.sub(r'<REASONING_SCRATCHPAD>.*?</REASONING_SCRATCHPAD>', '', content, flags=re.DOTALL | re.IGNORECASE)
content = re.sub(r'<thought>.*?</thought>', '', content, flags=re.DOTALL | re.IGNORECASE)
content = re.sub(r'<mm:think>.*?</mm:think>', '', 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 <tool_call> in prose is already vanishingly rare.
Expand Down Expand Up @@ -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'</?(?:think|thinking|reasoning|thought|REASONING_SCRATCHPAD)>\s*',
r'</?(?:think|thinking|reasoning|thought|REASONING_SCRATCHPAD|mm:think)>\s*',
'',
content,
flags=re.IGNORECASE,
Expand Down
9 changes: 5 additions & 4 deletions cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ def realign_markdown_tables(*args, **kwargs):
"thinking",
"reasoning",
"thought",
"mm:think",
)


Expand All @@ -202,8 +203,8 @@ def _strip_reasoning_tags(text: str) -> str:
partial-content dumps.

Covers the variants emitted by reasoning models today: ``<think>``,
``<thinking>``, ``<reasoning>``, ``<REASONING_SCRATCHPAD>``, and
``<thought>`` (Gemma 4). Must stay in sync with
``<thinking>``, ``<reasoning>``, ``<REASONING_SCRATCHPAD>``,
``<thought>`` (Gemma 4), and ``<mm:think>`` (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.

Expand Down Expand Up @@ -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 = ("<REASONING_SCRATCHPAD>", "<think>", "<reasoning>", "<THINKING>", "<thinking>", "<thought>")
_CLOSE_TAGS = ("</REASONING_SCRATCHPAD>", "</think>", "</reasoning>", "</THINKING>", "</thinking>", "</thought>")
_OPEN_TAGS = ("<REASONING_SCRATCHPAD>", "<think>", "<reasoning>", "<THINKING>", "<thinking>", "<thought>", "<mm:think>")
_CLOSE_TAGS = ("</REASONING_SCRATCHPAD>", "</think>", "</reasoning>", "</THINKING>", "</thinking>", "</thought>", "</mm:think>")

# Append to a pre-filter buffer first
self._stream_prefilt = getattr(self, "_stream_prefilt", "") + text
Expand Down
4 changes: 2 additions & 2 deletions gateway/stream_consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,11 @@ class GatewayStreamConsumer:
# run_agent.py _strip_think_blocks() tag variants.
_OPEN_THINK_TAGS = (
"<REASONING_SCRATCHPAD>", "<think>", "<reasoning>",
"<THINKING>", "<thinking>", "<thought>",
"<THINKING>", "<thinking>", "<thought>", "<mm:think>",
)
_CLOSE_THINK_TAGS = (
"</REASONING_SCRATCHPAD>", "</think>", "</reasoning>",
"</THINKING>", "</thinking>", "</thought>",
"</THINKING>", "</thinking>", "</thought>", "</mm:think>",
)

# Class-wide monotonic counter for native-streaming draft ids. Telegram
Expand Down
9 changes: 9 additions & 0 deletions tests/cli/test_stream_delta_think_tag.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <mm:think> channel marker should not reach the TUI."""
cli = _make_cli_stub()
cli._stream_delta("<mm:think>provider reasoning</mm:think>Visible 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."""
Expand Down
7 changes: 7 additions & 0 deletions tests/gateway/test_stream_consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -1452,6 +1452,13 @@ def test_thought_tag_variant(self):
c._filter_and_accumulate("<thought>Gemma style</thought>Output")
assert c._accumulated == "Output"

def test_minimax_mm_think_tag_variant(self):
"""MiniMax M3 emits <mm:think> blocks that must stay hidden."""
c = _make_consumer()
c._filter_and_accumulate("<mm:think>provider reasoning</mm:think>Answer")
assert c._accumulated == "Answer"
assert "provider reasoning" not in c._accumulated

def test_reasoning_scratchpad_variant(self):
c = _make_consumer()
c._filter_and_accumulate(
Expand Down
9 changes: 9 additions & 0 deletions tests/run_agent/test_run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,15 @@ def test_thought_block_removed(self, agent):
assert "<thought>" not in result
assert "answer" in result

def test_minimax_mm_think_block_removed(self, agent):
"""MiniMax M3 uses <mm:think> tags for provider-side reasoning."""
result = agent._strip_think_blocks(
"<mm:think>internal reasoning</mm:think> answer"
)
assert "internal reasoning" not in result
assert "<mm:think>" not in result
assert "answer" in result

def test_orphaned_thought_tag(self, agent):
result = agent._strip_think_blocks("<thought>orphaned reasoning without close")
assert "<thought>" not in result
Expand Down
Loading