mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
fix(reasoning): default /reasoning <level> to session scope in CLI and TUI
Parity with the gateway /reasoning handler and the new /model default: a bare /reasoning <level> now applies to the current session only; --global persists agent.reasoning_effort to config.yaml. --session is still accepted as an explicit alias for the default. Display toggles (show/hide/full/clamp) remain persistent as before — they are user preferences, not conversation state. Builds on YAMAGUCHI Seiji's #51158 (session-scope plumbing + /new reset, cherry-picked as the previous commit) with the default flipped to match the session-first policy. Fixes the CLI half of #54084.
This commit is contained in:
parent
8590c2d0d9
commit
dc0dbc9387
6 changed files with 59 additions and 25 deletions
|
|
@ -2515,8 +2515,8 @@ class CLICommandsMixin:
|
|||
|
||||
Usage:
|
||||
/reasoning Show current effort level and display state
|
||||
/reasoning <level> Set effort (none, minimal, low, medium, high, xhigh, max, ultra)
|
||||
/reasoning <level> --session Set reasoning effort for this session only
|
||||
/reasoning <level> Set effort for this session only (none, minimal, low, medium, high, xhigh, max, ultra)
|
||||
/reasoning <level> --global Persist reasoning effort to config.yaml
|
||||
/reasoning show|on Show model thinking/reasoning in output
|
||||
/reasoning hide|off Hide model thinking/reasoning from output
|
||||
/reasoning full Show complete thinking (no 10-line clamp)
|
||||
|
|
@ -2538,14 +2538,20 @@ class CLICommandsMixin:
|
|||
full_state = "full" if getattr(self, "reasoning_full", False) else "clamped to 10 lines"
|
||||
_cprint(f" {_ACCENT}Reasoning effort: {level}{_RST}")
|
||||
_cprint(f" {_ACCENT}Reasoning display: {display_state} ({full_state}){_RST}")
|
||||
_cprint(f" {_DIM}Usage: /reasoning <none|minimal|low|medium|high|xhigh|max|ultra|show|hide|full|clamp> [--session]{_RST}")
|
||||
_cprint(f" {_DIM}Usage: /reasoning <none|minimal|low|medium|high|xhigh|max|ultra|show|hide|full|clamp> [--global]{_RST}")
|
||||
return
|
||||
|
||||
arg = parts[1].strip().lower()
|
||||
arg_tokens = arg.split()
|
||||
explicit_session = "--session" in arg_tokens
|
||||
if explicit_session:
|
||||
arg = " ".join(token for token in arg_tokens if token != "--session")
|
||||
# Session scope is the default; --global opts into persisting to
|
||||
# config.yaml. --session is accepted as an explicit no-op for parity
|
||||
# with /model and the gateway /reasoning handler.
|
||||
explicit_global = "--global" in arg_tokens
|
||||
if explicit_global or "--session" in arg_tokens:
|
||||
arg = " ".join(
|
||||
token for token in arg_tokens
|
||||
if token not in ("--global", "--session")
|
||||
)
|
||||
|
||||
# Display toggle
|
||||
if arg in {"show", "on"}:
|
||||
|
|
@ -2585,23 +2591,23 @@ class CLICommandsMixin:
|
|||
_cprint(f" {_DIM}(._.) Unknown argument: {arg}{_RST}")
|
||||
_cprint(f" {_DIM}Valid levels: none, minimal, low, medium, high, xhigh, max, ultra{_RST}")
|
||||
_cprint(f" {_DIM}Display: show, hide{_RST}")
|
||||
_cprint(f" {_DIM}Scope: saved by default, --session for a temporary override{_RST}")
|
||||
_cprint(f" {_DIM}Scope: session-scoped by default, --global to persist{_RST}")
|
||||
return
|
||||
|
||||
self.reasoning_config = parsed
|
||||
self.agent = None # Force agent re-init with new reasoning config
|
||||
|
||||
if not explicit_session and save_config_value("agent.reasoning_effort", arg):
|
||||
if explicit_global and save_config_value("agent.reasoning_effort", arg):
|
||||
agent_cfg = CLI_CONFIG.get("agent")
|
||||
if not isinstance(agent_cfg, dict):
|
||||
agent_cfg = {}
|
||||
CLI_CONFIG["agent"] = agent_cfg
|
||||
agent_cfg["reasoning_effort"] = arg
|
||||
_cprint(f" {_ACCENT}✓ Reasoning effort set to '{arg}' (saved to config){_RST}")
|
||||
elif not explicit_session:
|
||||
elif explicit_global:
|
||||
_cprint(f" {_ACCENT}✓ Reasoning effort set to '{arg}' (session only; config save failed){_RST}")
|
||||
else:
|
||||
_cprint(f" {_ACCENT}✓ Reasoning effort set to '{arg}' (session only){_RST}")
|
||||
_cprint(f" {_ACCENT}✓ Reasoning effort set to '{arg}' (this session — use --global to persist){_RST}")
|
||||
|
||||
def _handle_busy_command(self, cmd: str):
|
||||
"""Handle /busy — control what Enter does while Hermes is working.
|
||||
|
|
|
|||
|
|
@ -153,8 +153,8 @@ COMMAND_REGISTRY: list[CommandDef] = [
|
|||
CommandDef("yolo", "Toggle YOLO mode (skip all dangerous command approvals)",
|
||||
"Configuration"),
|
||||
CommandDef("reasoning", "Manage reasoning effort and display", "Configuration",
|
||||
args_hint="[level|show|hide|full|clamp] [--session]",
|
||||
subcommands=("none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra", "show", "hide", "on", "off", "full", "clamp", "--session")),
|
||||
args_hint="[level|show|hide|full|clamp] [--global]",
|
||||
subcommands=("none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra", "show", "hide", "on", "off", "full", "clamp", "--global")),
|
||||
CommandDef("fast", "Toggle fast mode — OpenAI Priority Processing / Anthropic Fast Mode (Normal/Fast)", "Configuration",
|
||||
args_hint="[normal|fast|status]",
|
||||
subcommands=("normal", "fast", "status", "on", "off")),
|
||||
|
|
|
|||
|
|
@ -154,8 +154,20 @@ class TestHandleReasoningCommand(unittest.TestCase):
|
|||
level = rc.get("effort", "medium")
|
||||
self.assertEqual(level, "xhigh")
|
||||
|
||||
def test_effort_defaults_to_global_save(self):
|
||||
"""Plain /reasoning <level> keeps the existing config-writing behavior."""
|
||||
def test_effort_defaults_to_session_only(self):
|
||||
"""Plain /reasoning <level> is session-scoped — no config write."""
|
||||
from hermes_cli.cli_commands_mixin import CLICommandsMixin
|
||||
|
||||
stub = self._make_cli(reasoning_config={"enabled": True, "effort": "medium"})
|
||||
with patch("cli.save_config_value") as save_config, patch("cli._cprint"):
|
||||
CLICommandsMixin._handle_reasoning_command(stub, "/reasoning high")
|
||||
|
||||
save_config.assert_not_called()
|
||||
self.assertEqual(stub.reasoning_config, {"enabled": True, "effort": "high"})
|
||||
self.assertIsNone(stub.agent)
|
||||
|
||||
def test_effort_global_flag_persists_config(self):
|
||||
"""--global opts into persisting the effort to config.yaml."""
|
||||
from cli import CLI_CONFIG
|
||||
from hermes_cli.cli_commands_mixin import CLICommandsMixin
|
||||
|
||||
|
|
@ -163,7 +175,7 @@ class TestHandleReasoningCommand(unittest.TestCase):
|
|||
with patch.dict(CLI_CONFIG.setdefault("agent", {}), {"reasoning_effort": "medium"}), \
|
||||
patch("cli.save_config_value", return_value=True) as save_config, \
|
||||
patch("cli._cprint"):
|
||||
CLICommandsMixin._handle_reasoning_command(stub, "/reasoning high")
|
||||
CLICommandsMixin._handle_reasoning_command(stub, "/reasoning high --global")
|
||||
self.assertEqual(CLI_CONFIG["agent"]["reasoning_effort"], "high")
|
||||
|
||||
save_config.assert_called_once_with("agent.reasoning_effort", "high")
|
||||
|
|
@ -171,7 +183,7 @@ class TestHandleReasoningCommand(unittest.TestCase):
|
|||
self.assertIsNone(stub.agent)
|
||||
|
||||
def test_effort_session_flag_does_not_persist_config(self):
|
||||
"""--session opts into a temporary session-only effort override."""
|
||||
"""--session (explicit no-op alias for the default) stays session-only."""
|
||||
from hermes_cli.cli_commands_mixin import CLICommandsMixin
|
||||
|
||||
stub = self._make_cli(reasoning_config={"enabled": True, "effort": "medium"})
|
||||
|
|
|
|||
|
|
@ -4649,13 +4649,13 @@ def test_complete_slash_details_args():
|
|||
assert any(item["text"] == "expanded" for item in resp_mode["result"]["items"])
|
||||
|
||||
|
||||
def test_complete_slash_reasoning_includes_current_efforts_and_session_scope():
|
||||
def test_complete_slash_reasoning_includes_current_efforts_and_global_scope():
|
||||
resp = server.handle_request(
|
||||
{"id": "1", "method": "complete.slash", "params": {"text": "/reasoning "}}
|
||||
)
|
||||
|
||||
values = {item["text"] for item in resp["result"]["items"]}
|
||||
assert {"max", "ultra", "--session"} <= values
|
||||
assert {"max", "ultra", "--global"} <= values
|
||||
|
||||
|
||||
def test_config_set_reasoning_updates_live_session_and_agent(tmp_path, monkeypatch):
|
||||
|
|
|
|||
|
|
@ -278,19 +278,31 @@ describe('createSlashHandler', () => {
|
|||
})
|
||||
})
|
||||
|
||||
it.each(['low', 'max', 'ultra'])('sends plain /reasoning %s as global config.set', effort => {
|
||||
it.each(['low', 'max', 'ultra'])('sends plain /reasoning %s without a scope (session default)', effort => {
|
||||
patchUiState({ sid: 'sid-abc' })
|
||||
const ctx = buildCtx()
|
||||
|
||||
expect(createSlashHandler(ctx)(`/reasoning ${effort}`)).toBe(true)
|
||||
expect(ctx.gateway.rpc).toHaveBeenCalledWith('config.set', {
|
||||
key: 'reasoning',
|
||||
scope: 'global',
|
||||
session_id: 'sid-abc',
|
||||
value: effort
|
||||
})
|
||||
})
|
||||
|
||||
it('sends /reasoning <level> --global as global config.set', () => {
|
||||
patchUiState({ sid: 'sid-abc' })
|
||||
const ctx = buildCtx()
|
||||
|
||||
expect(createSlashHandler(ctx)('/reasoning high --global')).toBe(true)
|
||||
expect(ctx.gateway.rpc).toHaveBeenCalledWith('config.set', {
|
||||
key: 'reasoning',
|
||||
scope: 'global',
|
||||
session_id: 'sid-abc',
|
||||
value: 'high'
|
||||
})
|
||||
})
|
||||
|
||||
it('strips /reasoning session flags before config.set', () => {
|
||||
patchUiState({ sid: 'sid-abc' })
|
||||
const ctx = buildCtx()
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ const USAGE_CTA = 'Run /subscription to change plan · /topup to add to your bal
|
|||
|
||||
const TUI_SESSION_MODEL_RE = new RegExp(`(?:^|\\s)${TUI_SESSION_MODEL_FLAG}(?:\\s|$)`)
|
||||
const REASONING_SESSION_FLAGS = new Set(['--session'])
|
||||
const REASONING_EFFORT_VALUES = new Set(['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'])
|
||||
const REASONING_GLOBAL_FLAGS = new Set(['--global'])
|
||||
|
||||
const modelValueForConfigSet = (arg: string) => {
|
||||
const trimmed = arg.trim()
|
||||
|
|
@ -47,17 +47,21 @@ const reasoningConfigPayload = (arg: string, sid: string) => {
|
|||
|
||||
for (const part of parts) {
|
||||
const flag = part.toLowerCase()
|
||||
if (REASONING_GLOBAL_FLAGS.has(flag)) {
|
||||
scope = 'global'
|
||||
continue
|
||||
}
|
||||
if (REASONING_SESSION_FLAGS.has(flag)) {
|
||||
scope = 'session'
|
||||
// Session scope is the default; accept the flag for parity with /model.
|
||||
if (!scope) {
|
||||
scope = 'session'
|
||||
}
|
||||
continue
|
||||
}
|
||||
valueParts.push(part)
|
||||
}
|
||||
|
||||
const value = valueParts.join(' ')
|
||||
if (!scope && REASONING_EFFORT_VALUES.has(value.toLowerCase())) {
|
||||
scope = 'global'
|
||||
}
|
||||
|
||||
return {
|
||||
key: 'reasoning',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue