diff --git a/cli.py b/cli.py index 7f2b2394a05..b744447a1bb 100755 --- a/cli.py +++ b/cli.py @@ -3120,8 +3120,8 @@ class HermesCLI: level = "none (disabled)" else: level = rc.get("effort", "medium") - display_state = "on" if self.show_reasoning else "off" - _cprint(f" {_GOLD}Reasoning effort: {level}{_RST}") + display_state = "on ✓" if self.show_reasoning else "off" + _cprint(f" {_GOLD}Reasoning effort: {level}{_RST}") _cprint(f" {_GOLD}Reasoning display: {display_state}{_RST}") _cprint(f" {_DIM}Usage: /reasoning {_RST}") return @@ -3133,14 +3133,16 @@ class HermesCLI: self.show_reasoning = True if self.agent: self.agent.reasoning_callback = self._on_reasoning - _cprint(f" {_GOLD}Reasoning display: ON{_RST}") - _cprint(f" {_DIM}Model thinking will be shown during and after each response.{_RST}") + save_config_value("display.show_reasoning", True) + _cprint(f" {_GOLD}✓ Reasoning display: ON (saved){_RST}") + _cprint(f" {_DIM} Model thinking will be shown during and after each response.{_RST}") return if arg in ("hide", "off"): self.show_reasoning = False if self.agent: self.agent.reasoning_callback = None - _cprint(f" {_GOLD}Reasoning display: OFF{_RST}") + save_config_value("display.show_reasoning", False) + _cprint(f" {_GOLD}✓ Reasoning display: OFF (saved){_RST}") return # Effort level change @@ -3155,9 +3157,9 @@ class HermesCLI: self.agent = None # Force agent re-init with new reasoning config if save_config_value("agent.reasoning_effort", arg): - _cprint(f" {_GOLD}Reasoning effort set to '{arg}' (saved to config){_RST}") + _cprint(f" {_GOLD}✓ Reasoning effort set to '{arg}' (saved to config){_RST}") else: - _cprint(f" {_GOLD}Reasoning effort set to '{arg}' (session only){_RST}") + _cprint(f" {_GOLD}✓ Reasoning effort set to '{arg}' (session only){_RST}") def _on_reasoning(self, reasoning_text: str): """Callback for intermediate reasoning display during tool-call loops.""" @@ -4544,7 +4546,7 @@ class HermesCLI: # Check for commands if isinstance(user_input, str) and user_input.startswith("/"): - print(f"\n⚙️ {user_input}") + _cprint(f"\n⚙️ {user_input}") if not self.process_command(user_input): self._should_exit = True # Schedule app exit diff --git a/run_agent.py b/run_agent.py index cce83f6b6bb..608dde94cda 100644 --- a/run_agent.py +++ b/run_agent.py @@ -2442,6 +2442,16 @@ class AIAgent: """ reasoning_text = self._extract_reasoning(assistant_message) + # Fallback: extract inline blocks from content when no structured + # reasoning fields are present (some models/providers embed thinking + # directly in the content rather than returning separate API fields). + if not reasoning_text: + content = assistant_message.content or "" + think_blocks = re.findall(r'(.*?)', content, flags=re.DOTALL) + if think_blocks: + combined = "\n\n".join(b.strip() for b in think_blocks if b.strip()) + reasoning_text = combined or None + if reasoning_text and self.verbose_logging: preview = reasoning_text[:100] + "..." if len(reasoning_text) > 100 else reasoning_text logging.debug(f"Captured reasoning ({len(reasoning_text)} chars): {preview}") diff --git a/tests/test_reasoning_command.py b/tests/test_reasoning_command.py index 2cca80f3033..425e28a58c7 100644 --- a/tests/test_reasoning_command.py +++ b/tests/test_reasoning_command.py @@ -342,6 +342,90 @@ class TestExtractReasoningFormats(unittest.TestCase): self.assertIsNone(result) +# --------------------------------------------------------------------------- +# Inline block extraction fallback +# --------------------------------------------------------------------------- + +class TestInlineThinkBlockExtraction(unittest.TestCase): + """Test _build_assistant_message extracts inline blocks as reasoning + when no structured API-level reasoning fields are present.""" + + def _build_msg(self, content, reasoning=None, reasoning_content=None, reasoning_details=None, tool_calls=None): + """Create a mock API response message.""" + msg = SimpleNamespace(content=content, tool_calls=tool_calls) + if reasoning is not None: + msg.reasoning = reasoning + if reasoning_content is not None: + msg.reasoning_content = reasoning_content + if reasoning_details is not None: + msg.reasoning_details = reasoning_details + return msg + + def _make_agent(self): + """Create a minimal agent with _build_assistant_message.""" + from run_agent import AIAgent + agent = MagicMock(spec=AIAgent) + agent._build_assistant_message = AIAgent._build_assistant_message.__get__(agent) + agent._extract_reasoning = AIAgent._extract_reasoning.__get__(agent) + agent.verbose_logging = False + agent.reasoning_callback = None + return agent + + def test_single_think_block_extracted(self): + agent = self._make_agent() + api_msg = self._build_msg("Let me calculate 2+2=4.The answer is 4.") + result = agent._build_assistant_message(api_msg, "stop") + self.assertEqual(result["reasoning"], "Let me calculate 2+2=4.") + + def test_multiple_think_blocks_extracted(self): + agent = self._make_agent() + api_msg = self._build_msg("First thought.Some textSecond thought.More text") + result = agent._build_assistant_message(api_msg, "stop") + self.assertIn("First thought.", result["reasoning"]) + self.assertIn("Second thought.", result["reasoning"]) + + def test_no_think_blocks_no_reasoning(self): + agent = self._make_agent() + api_msg = self._build_msg("Just a plain response.") + result = agent._build_assistant_message(api_msg, "stop") + # No structured reasoning AND no inline think blocks → None + self.assertIsNone(result["reasoning"]) + + def test_structured_reasoning_takes_priority(self): + """When structured API reasoning exists, inline think blocks should NOT override.""" + agent = self._make_agent() + api_msg = self._build_msg( + "Inline thought.Response text.", + reasoning="Structured reasoning from API.", + ) + result = agent._build_assistant_message(api_msg, "stop") + self.assertEqual(result["reasoning"], "Structured reasoning from API.") + + def test_empty_think_block_ignored(self): + agent = self._make_agent() + api_msg = self._build_msg("Hello!") + result = agent._build_assistant_message(api_msg, "stop") + # Empty think block should not produce reasoning + self.assertIsNone(result["reasoning"]) + + def test_multiline_think_block(self): + agent = self._make_agent() + api_msg = self._build_msg("\nStep 1: Analyze.\nStep 2: Solve.\nDone.") + result = agent._build_assistant_message(api_msg, "stop") + self.assertIn("Step 1: Analyze.", result["reasoning"]) + self.assertIn("Step 2: Solve.", result["reasoning"]) + + def test_callback_fires_for_inline_think(self): + """Reasoning callback should fire when reasoning is extracted from inline think blocks.""" + agent = self._make_agent() + captured = [] + agent.reasoning_callback = lambda t: captured.append(t) + api_msg = self._build_msg("Deep analysis here.Answer.") + agent._build_assistant_message(api_msg, "stop") + self.assertEqual(len(captured), 1) + self.assertIn("Deep analysis", captured[0]) + + # --------------------------------------------------------------------------- # Config defaults # ---------------------------------------------------------------------------