mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
fix(tui): tolerate null top-level sections in config.yaml
YAML parses bare keys like `agent:` or `display:` as None. `dict.get(key, {})`
returns that None instead of the default (defaults only fire on missing keys),
so every `cfg.get("agent", {}).get(...)` chain in tui_gateway/server.py
crashed agent init with `'NoneType' object has no attribute 'get'`.
Guard all 21 sites with `(cfg.get(X) or {})`. Regression test covers the
null-section init path reported on Twitter against the new TUI.
This commit is contained in:
parent
c61547c067
commit
fd9b692d33
2 changed files with 49 additions and 21 deletions
|
|
@ -46,3 +46,31 @@ def test_make_agent_passes_resolved_provider():
|
||||||
assert call_kwargs.kwargs["base_url"] == "https://api.anthropic.com"
|
assert call_kwargs.kwargs["base_url"] == "https://api.anthropic.com"
|
||||||
assert call_kwargs.kwargs["api_key"] == "sk-test-key"
|
assert call_kwargs.kwargs["api_key"] == "sk-test-key"
|
||||||
assert call_kwargs.kwargs["api_mode"] == "anthropic_messages"
|
assert call_kwargs.kwargs["api_mode"] == "anthropic_messages"
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_agent_tolerates_null_config_sections():
|
||||||
|
"""Bare `agent:` / `display:` keys in ~/.hermes/config.yaml parse as
|
||||||
|
None. cfg.get("agent", {}) returns None (default only fires on missing
|
||||||
|
key), so downstream .get() chains must be guarded. Reported via Twitter
|
||||||
|
against the new TUI; CLI path is unaffected."""
|
||||||
|
|
||||||
|
fake_runtime = {
|
||||||
|
"provider": "openrouter",
|
||||||
|
"base_url": "https://api.synthetic.new/v1",
|
||||||
|
"api_key": "sk-test",
|
||||||
|
"api_mode": "chat_completions",
|
||||||
|
"command": None,
|
||||||
|
"args": None,
|
||||||
|
"credential_pool": None,
|
||||||
|
}
|
||||||
|
null_cfg = {"agent": None, "display": None, "model": {"default": "glm-5"}}
|
||||||
|
|
||||||
|
with patch("tui_gateway.server._load_cfg", return_value=null_cfg), \
|
||||||
|
patch("tui_gateway.server._get_db", return_value=MagicMock()), \
|
||||||
|
patch("hermes_cli.runtime_provider.resolve_runtime_provider", return_value=fake_runtime), \
|
||||||
|
patch("run_agent.AIAgent") as mock_agent:
|
||||||
|
|
||||||
|
from tui_gateway.server import _make_agent
|
||||||
|
_make_agent("sid-null", "key-null")
|
||||||
|
|
||||||
|
assert mock_agent.called
|
||||||
|
|
|
||||||
|
|
@ -593,13 +593,13 @@ def _coerce_statusbar(raw) -> str:
|
||||||
def _load_reasoning_config() -> dict | None:
|
def _load_reasoning_config() -> dict | None:
|
||||||
from hermes_constants import parse_reasoning_effort
|
from hermes_constants import parse_reasoning_effort
|
||||||
|
|
||||||
effort = str(_load_cfg().get("agent", {}).get("reasoning_effort", "") or "").strip()
|
effort = str((_load_cfg().get("agent") or {}).get("reasoning_effort", "") or "").strip()
|
||||||
return parse_reasoning_effort(effort)
|
return parse_reasoning_effort(effort)
|
||||||
|
|
||||||
|
|
||||||
def _load_service_tier() -> str | None:
|
def _load_service_tier() -> str | None:
|
||||||
raw = (
|
raw = (
|
||||||
str(_load_cfg().get("agent", {}).get("service_tier", "") or "").strip().lower()
|
str((_load_cfg().get("agent") or {}).get("service_tier", "") or "").strip().lower()
|
||||||
)
|
)
|
||||||
if not raw or raw in {"normal", "default", "standard", "off", "none"}:
|
if not raw or raw in {"normal", "default", "standard", "off", "none"}:
|
||||||
return None
|
return None
|
||||||
|
|
@ -609,11 +609,11 @@ def _load_service_tier() -> str | None:
|
||||||
|
|
||||||
|
|
||||||
def _load_show_reasoning() -> bool:
|
def _load_show_reasoning() -> bool:
|
||||||
return bool(_load_cfg().get("display", {}).get("show_reasoning", False))
|
return bool((_load_cfg().get("display") or {}).get("show_reasoning", False))
|
||||||
|
|
||||||
|
|
||||||
def _load_tool_progress_mode() -> str:
|
def _load_tool_progress_mode() -> str:
|
||||||
raw = _load_cfg().get("display", {}).get("tool_progress", "all")
|
raw = (_load_cfg().get("display") or {}).get("tool_progress", "all")
|
||||||
if raw is False:
|
if raw is False:
|
||||||
return "off"
|
return "off"
|
||||||
if raw is True:
|
if raw is True:
|
||||||
|
|
@ -1104,20 +1104,20 @@ def _wire_callbacks(sid: str):
|
||||||
|
|
||||||
def _resolve_personality_prompt(cfg: dict) -> str:
|
def _resolve_personality_prompt(cfg: dict) -> str:
|
||||||
"""Resolve the active personality into a system prompt string."""
|
"""Resolve the active personality into a system prompt string."""
|
||||||
name = (cfg.get("display", {}).get("personality", "") or "").strip().lower()
|
name = ((cfg.get("display") or {}).get("personality", "") or "").strip().lower()
|
||||||
if not name or name in ("default", "none", "neutral"):
|
if not name or name in ("default", "none", "neutral"):
|
||||||
return ""
|
return ""
|
||||||
try:
|
try:
|
||||||
from cli import load_cli_config
|
from cli import load_cli_config
|
||||||
|
|
||||||
personalities = load_cli_config().get("agent", {}).get("personalities", {})
|
personalities = (load_cli_config().get("agent") or {}).get("personalities", {})
|
||||||
except Exception:
|
except Exception:
|
||||||
try:
|
try:
|
||||||
from hermes_cli.config import load_config as _load_full_cfg
|
from hermes_cli.config import load_config as _load_full_cfg
|
||||||
|
|
||||||
personalities = _load_full_cfg().get("agent", {}).get("personalities", {})
|
personalities = (_load_full_cfg().get("agent") or {}).get("personalities", {})
|
||||||
except Exception:
|
except Exception:
|
||||||
personalities = cfg.get("agent", {}).get("personalities", {})
|
personalities = (cfg.get("agent") or {}).get("personalities", {})
|
||||||
pval = personalities.get(name)
|
pval = personalities.get(name)
|
||||||
if pval is None:
|
if pval is None:
|
||||||
return ""
|
return ""
|
||||||
|
|
@ -1139,15 +1139,15 @@ def _available_personalities(cfg: dict | None = None) -> dict:
|
||||||
try:
|
try:
|
||||||
from cli import load_cli_config
|
from cli import load_cli_config
|
||||||
|
|
||||||
return load_cli_config().get("agent", {}).get("personalities", {}) or {}
|
return (load_cli_config().get("agent") or {}).get("personalities", {}) or {}
|
||||||
except Exception:
|
except Exception:
|
||||||
try:
|
try:
|
||||||
from hermes_cli.config import load_config as _load_full_cfg
|
from hermes_cli.config import load_config as _load_full_cfg
|
||||||
|
|
||||||
return _load_full_cfg().get("agent", {}).get("personalities", {}) or {}
|
return (_load_full_cfg().get("agent") or {}).get("personalities", {}) or {}
|
||||||
except Exception:
|
except Exception:
|
||||||
cfg = cfg or _load_cfg()
|
cfg = cfg or _load_cfg()
|
||||||
return cfg.get("agent", {}).get("personalities", {}) or {}
|
return (cfg.get("agent") or {}).get("personalities", {}) or {}
|
||||||
|
|
||||||
|
|
||||||
def _validate_personality(value: str, cfg: dict | None = None) -> tuple[str, str]:
|
def _validate_personality(value: str, cfg: dict | None = None) -> tuple[str, str]:
|
||||||
|
|
@ -1257,7 +1257,7 @@ def _make_agent(sid: str, key: str, session_id: str | None = None):
|
||||||
from hermes_cli.runtime_provider import resolve_runtime_provider
|
from hermes_cli.runtime_provider import resolve_runtime_provider
|
||||||
|
|
||||||
cfg = _load_cfg()
|
cfg = _load_cfg()
|
||||||
system_prompt = cfg.get("agent", {}).get("system_prompt", "") or ""
|
system_prompt = (cfg.get("agent") or {}).get("system_prompt", "") or ""
|
||||||
if not system_prompt:
|
if not system_prompt:
|
||||||
system_prompt = _resolve_personality_prompt(cfg)
|
system_prompt = _resolve_personality_prompt(cfg)
|
||||||
runtime = resolve_runtime_provider(requested=None)
|
runtime = resolve_runtime_provider(requested=None)
|
||||||
|
|
@ -2838,18 +2838,18 @@ def _(rid, params: dict) -> dict:
|
||||||
return _ok(rid, {"prompt": _load_cfg().get("custom_prompt", "")})
|
return _ok(rid, {"prompt": _load_cfg().get("custom_prompt", "")})
|
||||||
if key == "skin":
|
if key == "skin":
|
||||||
return _ok(
|
return _ok(
|
||||||
rid, {"value": _load_cfg().get("display", {}).get("skin", "default")}
|
rid, {"value": (_load_cfg().get("display") or {}).get("skin", "default")}
|
||||||
)
|
)
|
||||||
if key == "personality":
|
if key == "personality":
|
||||||
return _ok(
|
return _ok(
|
||||||
rid, {"value": _load_cfg().get("display", {}).get("personality", "default")}
|
rid, {"value": (_load_cfg().get("display") or {}).get("personality", "default")}
|
||||||
)
|
)
|
||||||
if key == "reasoning":
|
if key == "reasoning":
|
||||||
cfg = _load_cfg()
|
cfg = _load_cfg()
|
||||||
effort = str(cfg.get("agent", {}).get("reasoning_effort", "medium") or "medium")
|
effort = str((cfg.get("agent") or {}).get("reasoning_effort", "medium") or "medium")
|
||||||
display = (
|
display = (
|
||||||
"show"
|
"show"
|
||||||
if bool(cfg.get("display", {}).get("show_reasoning", False))
|
if bool((cfg.get("display") or {}).get("show_reasoning", False))
|
||||||
else "hide"
|
else "hide"
|
||||||
)
|
)
|
||||||
return _ok(rid, {"value": effort, "display": display})
|
return _ok(rid, {"value": effort, "display": display})
|
||||||
|
|
@ -2857,7 +2857,7 @@ def _(rid, params: dict) -> dict:
|
||||||
allowed_dm = frozenset({"hidden", "collapsed", "expanded"})
|
allowed_dm = frozenset({"hidden", "collapsed", "expanded"})
|
||||||
raw = (
|
raw = (
|
||||||
str(
|
str(
|
||||||
_load_cfg().get("display", {}).get("details_mode", "collapsed")
|
(_load_cfg().get("display") or {}).get("details_mode", "collapsed")
|
||||||
or "collapsed"
|
or "collapsed"
|
||||||
)
|
)
|
||||||
.strip()
|
.strip()
|
||||||
|
|
@ -2868,13 +2868,13 @@ def _(rid, params: dict) -> dict:
|
||||||
if key == "thinking_mode":
|
if key == "thinking_mode":
|
||||||
allowed_tm = frozenset({"collapsed", "truncated", "full"})
|
allowed_tm = frozenset({"collapsed", "truncated", "full"})
|
||||||
cfg = _load_cfg()
|
cfg = _load_cfg()
|
||||||
raw = str(cfg.get("display", {}).get("thinking_mode", "") or "").strip().lower()
|
raw = str((cfg.get("display") or {}).get("thinking_mode", "") or "").strip().lower()
|
||||||
if raw in allowed_tm:
|
if raw in allowed_tm:
|
||||||
nv = raw
|
nv = raw
|
||||||
else:
|
else:
|
||||||
dm = (
|
dm = (
|
||||||
str(
|
str(
|
||||||
cfg.get("display", {}).get("details_mode", "collapsed")
|
(cfg.get("display") or {}).get("details_mode", "collapsed")
|
||||||
or "collapsed"
|
or "collapsed"
|
||||||
)
|
)
|
||||||
.strip()
|
.strip()
|
||||||
|
|
@ -2883,7 +2883,7 @@ def _(rid, params: dict) -> dict:
|
||||||
nv = "full" if dm == "expanded" else "collapsed"
|
nv = "full" if dm == "expanded" else "collapsed"
|
||||||
return _ok(rid, {"value": nv})
|
return _ok(rid, {"value": nv})
|
||||||
if key == "compact":
|
if key == "compact":
|
||||||
on = bool(_load_cfg().get("display", {}).get("tui_compact", False))
|
on = bool((_load_cfg().get("display") or {}).get("tui_compact", False))
|
||||||
return _ok(rid, {"value": "on" if on else "off"})
|
return _ok(rid, {"value": "on" if on else "off"})
|
||||||
if key == "statusbar":
|
if key == "statusbar":
|
||||||
display = _load_cfg().get("display")
|
display = _load_cfg().get("display")
|
||||||
|
|
@ -3721,7 +3721,7 @@ def _mirror_slash_side_effects(sid: str, session: dict, command: str) -> str:
|
||||||
_apply_personality_to_session(sid, session, new_prompt)
|
_apply_personality_to_session(sid, session, new_prompt)
|
||||||
elif name == "prompt" and agent:
|
elif name == "prompt" and agent:
|
||||||
cfg = _load_cfg()
|
cfg = _load_cfg()
|
||||||
new_prompt = cfg.get("agent", {}).get("system_prompt", "") or ""
|
new_prompt = (cfg.get("agent") or {}).get("system_prompt", "") or ""
|
||||||
agent.ephemeral_system_prompt = new_prompt or None
|
agent.ephemeral_system_prompt = new_prompt or None
|
||||||
agent._cached_system_prompt = None
|
agent._cached_system_prompt = None
|
||||||
elif name == "compress" and agent:
|
elif name == "compress" and agent:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue