mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
feat(cli): add /reasoning slash command to manage reasoning effort
Cherry-picked from PR #789 by Aum08Desai, rebased onto current main with conflict resolution and improvements: - Added /reasoning command: view current level or set to none|low|medium|high|xhigh - Persists to config via save_config_value, forces agent re-init - Resolved conflict with COMMANDS_BY_CATEGORY refactor (added to Configuration category) - Restricted valid levels to none, low, medium, high, xhigh (removed 'minimal') - Updated _parse_reasoning_config in cli.py and _load_reasoning_config in gateway/run.py - Improved display messages (show all valid options, clearer defaults/disabled state) - Added EXPECTED_COMMANDS entry for regression guard - Expanded test suite: 16 tests covering all levels, rejection, display, case insensitivity, config save failure Co-authored-by: Aum08Desai <145567217+Aum08Desai@users.noreply.github.com>
This commit is contained in:
parent
c837ef949d
commit
e6b325cc24
5 changed files with 176 additions and 5 deletions
37
cli.py
37
cli.py
|
|
@ -115,7 +115,7 @@ def _load_prefill_messages(file_path: str) -> List[Dict[str, Any]]:
|
|||
def _parse_reasoning_config(effort: str) -> dict | None:
|
||||
"""Parse a reasoning effort level into an OpenRouter reasoning config dict.
|
||||
|
||||
Valid levels: "xhigh", "high", "medium", "low", "minimal", "none".
|
||||
Valid levels: "none", "low", "medium", "high", "xhigh".
|
||||
Returns None to use the default (medium), or a config dict to override.
|
||||
"""
|
||||
if not effort or not effort.strip():
|
||||
|
|
@ -123,7 +123,7 @@ def _parse_reasoning_config(effort: str) -> dict | None:
|
|||
effort = effort.strip().lower()
|
||||
if effort == "none":
|
||||
return {"enabled": False}
|
||||
valid = ("xhigh", "high", "medium", "low", "minimal")
|
||||
valid = ("low", "medium", "high", "xhigh")
|
||||
if effort in valid:
|
||||
return {"enabled": True, "effort": effort}
|
||||
logger.warning("Unknown reasoning_effort '%s', using default (medium)", effort)
|
||||
|
|
@ -2366,6 +2366,37 @@ class HermesCLI:
|
|||
print(" Usage: /personality <name>")
|
||||
print()
|
||||
|
||||
def _handle_reasoning_command(self, cmd: str):
|
||||
"""Handle the /reasoning command to view or set reasoning effort."""
|
||||
parts = cmd.split(maxsplit=1)
|
||||
valid_efforts = ("none", "low", "medium", "high", "xhigh")
|
||||
|
||||
if len(parts) == 1:
|
||||
if self.reasoning_config is None:
|
||||
current = "medium (default)"
|
||||
elif self.reasoning_config.get("enabled") is False:
|
||||
current = "none (reasoning disabled)"
|
||||
else:
|
||||
current = self.reasoning_config.get("effort", "medium")
|
||||
|
||||
print(f" Reasoning effort: {current}")
|
||||
print(" Usage: /reasoning <none|low|medium|high|xhigh>")
|
||||
return
|
||||
|
||||
effort = parts[1].strip().lower()
|
||||
if effort not in valid_efforts:
|
||||
print(f" (._.) Invalid reasoning level: '{effort}'")
|
||||
print(" Valid levels: none, low, medium, high, xhigh")
|
||||
return
|
||||
|
||||
self.reasoning_config = _parse_reasoning_config(effort)
|
||||
self.agent = None # Force re-init with new reasoning config
|
||||
|
||||
if save_config_value("agent.reasoning_effort", effort):
|
||||
print(f" (^_^)b Reasoning effort set to: {effort} (saved to config)")
|
||||
else:
|
||||
print(f" (^_^) Reasoning effort set to: {effort} (session only)")
|
||||
|
||||
def _handle_cron_command(self, cmd: str):
|
||||
"""Handle the /cron command to manage scheduled tasks."""
|
||||
parts = cmd.split(maxsplit=2)
|
||||
|
|
@ -2830,6 +2861,8 @@ class HermesCLI:
|
|||
elif cmd_lower.startswith("/personality"):
|
||||
# Use original case (handler lowercases the personality name itself)
|
||||
self._handle_personality_command(cmd_original)
|
||||
elif cmd_lower.startswith("/reasoning"):
|
||||
self._handle_reasoning_command(cmd_original)
|
||||
elif cmd_lower == "/retry":
|
||||
retry_msg = self.retry_last()
|
||||
if retry_msg and hasattr(self, '_pending_input'):
|
||||
|
|
|
|||
|
|
@ -366,7 +366,7 @@ class GatewayRunner:
|
|||
"""Load reasoning effort from config or env var.
|
||||
|
||||
Checks HERMES_REASONING_EFFORT env var first, then agent.reasoning_effort
|
||||
in config.yaml. Valid: "xhigh", "high", "medium", "low", "minimal", "none".
|
||||
in config.yaml. Valid: "none", "low", "medium", "high", "xhigh".
|
||||
Returns None to use default (medium).
|
||||
"""
|
||||
effort = os.getenv("HERMES_REASONING_EFFORT", "")
|
||||
|
|
@ -385,7 +385,7 @@ class GatewayRunner:
|
|||
effort = effort.lower().strip()
|
||||
if effort == "none":
|
||||
return {"enabled": False}
|
||||
valid = ("xhigh", "high", "medium", "low", "minimal")
|
||||
valid = ("low", "medium", "high", "xhigh")
|
||||
if effort in valid:
|
||||
return {"enabled": True, "effort": effort}
|
||||
logger.warning("Unknown reasoning_effort '%s', using default (medium)", effort)
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ COMMANDS_BY_CATEGORY = {
|
|||
"/provider": "Show available providers and current provider",
|
||||
"/prompt": "View/set custom system prompt",
|
||||
"/personality": "Set a predefined personality",
|
||||
"/reasoning": "Show or change reasoning effort (none|low|medium|high|xhigh)",
|
||||
"/verbose": "Cycle tool progress display: off → new → all → verbose",
|
||||
"/skin": "Show or change the display skin/theme",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ EXPECTED_COMMANDS = {
|
|||
"/personality", "/clear", "/history", "/new", "/reset", "/retry",
|
||||
"/undo", "/save", "/config", "/cron", "/skills", "/platforms",
|
||||
"/verbose", "/compress", "/title", "/usage", "/insights", "/paste",
|
||||
"/reload-mcp", "/rollback", "/background", "/skin", "/quit",
|
||||
"/reload-mcp", "/rollback", "/background", "/skin", "/reasoning", "/quit",
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
137
tests/test_cli_reasoning_command.py
Normal file
137
tests/test_cli_reasoning_command.py
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
"""Tests for /reasoning slash command in HermesCLI."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def _make_cli(**kwargs):
|
||||
"""Create a HermesCLI instance with minimal mocking."""
|
||||
import cli as _cli_mod
|
||||
from cli import HermesCLI
|
||||
|
||||
clean_config = {
|
||||
"model": {
|
||||
"default": "anthropic/claude-opus-4.6",
|
||||
"base_url": "https://openrouter.ai/api/v1",
|
||||
"provider": "auto",
|
||||
},
|
||||
"display": {"compact": False, "tool_progress": "all"},
|
||||
"agent": {"reasoning_effort": "medium"},
|
||||
"terminal": {"env_type": "local"},
|
||||
}
|
||||
|
||||
clean_env = {"LLM_MODEL": "", "HERMES_MAX_ITERATIONS": ""}
|
||||
|
||||
with (
|
||||
patch("cli.get_tool_definitions", return_value=[]),
|
||||
patch.dict("os.environ", clean_env, clear=False),
|
||||
patch.dict(_cli_mod.__dict__, {"CLI_CONFIG": clean_config}),
|
||||
):
|
||||
return HermesCLI(**kwargs)
|
||||
|
||||
|
||||
# -- setting valid effort levels -------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize("level", ["low", "medium", "high", "xhigh"])
|
||||
def test_reasoning_command_sets_effort_and_persists(level):
|
||||
cli_obj = _make_cli()
|
||||
cli_obj.agent = object() # ensure command forces re-init
|
||||
|
||||
with patch("cli.save_config_value", return_value=True) as mock_save:
|
||||
keep_running = cli_obj.process_command(f"/reasoning {level}")
|
||||
|
||||
assert keep_running is True
|
||||
assert cli_obj.reasoning_config == {"enabled": True, "effort": level}
|
||||
assert cli_obj.agent is None
|
||||
mock_save.assert_called_once_with("agent.reasoning_effort", level)
|
||||
|
||||
|
||||
def test_reasoning_command_sets_none_disables_reasoning():
|
||||
cli_obj = _make_cli()
|
||||
cli_obj.agent = object()
|
||||
|
||||
with patch("cli.save_config_value", return_value=True) as mock_save:
|
||||
keep_running = cli_obj.process_command("/reasoning none")
|
||||
|
||||
assert keep_running is True
|
||||
assert cli_obj.reasoning_config == {"enabled": False}
|
||||
assert cli_obj.agent is None
|
||||
mock_save.assert_called_once_with("agent.reasoning_effort", "none")
|
||||
|
||||
|
||||
# -- rejecting invalid levels ---------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad_level", ["ultra", "minimal", "max", "off", "0", "150"])
|
||||
def test_reasoning_command_rejects_invalid_level(capsys, bad_level):
|
||||
cli_obj = _make_cli()
|
||||
before = cli_obj.reasoning_config
|
||||
|
||||
with patch("cli.save_config_value", return_value=True) as mock_save:
|
||||
keep_running = cli_obj.process_command(f"/reasoning {bad_level}")
|
||||
|
||||
out = capsys.readouterr().out
|
||||
assert keep_running is True
|
||||
assert "Invalid reasoning level" in out
|
||||
assert cli_obj.reasoning_config == before
|
||||
mock_save.assert_not_called()
|
||||
|
||||
|
||||
# -- display current level -------------------------------------------------
|
||||
|
||||
|
||||
def test_reasoning_shows_current_effort(capsys):
|
||||
cli_obj = _make_cli()
|
||||
cli_obj.reasoning_config = {"enabled": True, "effort": "high"}
|
||||
|
||||
cli_obj.process_command("/reasoning")
|
||||
|
||||
out = capsys.readouterr().out
|
||||
assert "Reasoning effort: high" in out
|
||||
|
||||
|
||||
def test_reasoning_shows_default_when_none_set(capsys):
|
||||
cli_obj = _make_cli()
|
||||
cli_obj.reasoning_config = None
|
||||
|
||||
cli_obj.process_command("/reasoning")
|
||||
|
||||
out = capsys.readouterr().out
|
||||
assert "medium (default)" in out
|
||||
|
||||
|
||||
def test_reasoning_shows_disabled_when_none_effort(capsys):
|
||||
cli_obj = _make_cli()
|
||||
cli_obj.reasoning_config = {"enabled": False}
|
||||
|
||||
cli_obj.process_command("/reasoning")
|
||||
|
||||
out = capsys.readouterr().out
|
||||
assert "none (reasoning disabled)" in out
|
||||
|
||||
|
||||
# -- case insensitivity ----------------------------------------------------
|
||||
|
||||
|
||||
def test_reasoning_command_is_case_insensitive(capsys):
|
||||
cli_obj = _make_cli()
|
||||
|
||||
with patch("cli.save_config_value", return_value=True):
|
||||
cli_obj.process_command("/reasoning HIGH")
|
||||
|
||||
assert cli_obj.reasoning_config == {"enabled": True, "effort": "high"}
|
||||
|
||||
|
||||
# -- config save failure ---------------------------------------------------
|
||||
|
||||
|
||||
def test_reasoning_shows_session_only_on_save_failure(capsys):
|
||||
cli_obj = _make_cli()
|
||||
|
||||
with patch("cli.save_config_value", return_value=False):
|
||||
cli_obj.process_command("/reasoning high")
|
||||
|
||||
out = capsys.readouterr().out
|
||||
assert "session only" in out
|
||||
Loading…
Add table
Add a link
Reference in a new issue