From a0d406dcd8a78cc466dcc99976f2f1b9cb48e983 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Sat, 8 Aug 2026 14:01:56 -0500 Subject: [PATCH] fix(personality): stop writing personality into agent.system_prompt Persist display.personality only; apply rendered text as an in-session overlay across CLI, TUI config.set, and gateway /personality. Co-authored-by: kyssta-exe Co-authored-by: EMT5320 <1908937833@qq.com> --- cli.py | 7 +++++-- gateway/run.py | 10 +++++---- gateway/slash_commands.py | 36 ++++++++++++++++---------------- hermes_cli/cli_commands_mixin.py | 23 +++++++++++++++----- tui_gateway/server.py | 9 +++++--- 5 files changed, 53 insertions(+), 32 deletions(-) diff --git a/cli.py b/cli.py index 2decc0e204..ce7ca7e30f 100644 --- a/cli.py +++ b/cli.py @@ -4502,10 +4502,13 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin): # AGENTS.md/SOUL.md/.cursorrules and persistent memory are not loaded. self.ignore_rules = ignore_rules or os.environ.get("HERMES_IGNORE_RULES") == "1" - # Ephemeral system prompt: env var takes precedence, then config + # Ephemeral system prompt: env var takes precedence, then + # display.personality / agent.system_prompt from config. + from hermes_cli.config import resolve_ephemeral_system_prompt_from_config + self.system_prompt = ( os.getenv("HERMES_EPHEMERAL_SYSTEM_PROMPT", "") - or CLI_CONFIG["agent"].get("system_prompt", "") + or resolve_ephemeral_system_prompt_from_config(CLI_CONFIG) ) self.personalities = CLI_CONFIG["agent"].get("personalities", {}) diff --git a/gateway/run.py b/gateway/run.py index 8160038b44..55f6b1305b 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -8170,15 +8170,17 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew @staticmethod def _load_ephemeral_system_prompt() -> str: """Load ephemeral system prompt from config or env var. - - Checks HERMES_EPHEMERAL_SYSTEM_PROMPT env var first, then falls back to - agent.system_prompt in ~/.hermes/config.yaml. + + Checks HERMES_EPHEMERAL_SYSTEM_PROMPT env var first, then + ``display.personality`` / ``agent.system_prompt`` in config.yaml. """ + from hermes_cli.config import resolve_ephemeral_system_prompt_from_config + prompt = os.getenv("HERMES_EPHEMERAL_SYSTEM_PROMPT", "") if prompt: return prompt cfg = _load_gateway_runtime_config() - return str(cfg_get(cfg, "agent", "system_prompt", default="") or "").strip() + return resolve_ephemeral_system_prompt_from_config(cfg) def _resolve_model_for_channel( self, diff --git a/gateway/slash_commands.py b/gateway/slash_commands.py index 8675f317f8..7205516a04 100644 --- a/gateway/slash_commands.py +++ b/gateway/slash_commands.py @@ -2519,34 +2519,34 @@ class GatewaySlashCommandsMixin: lines.append(t("gateway.personality.usage")) return "\n".join(lines) - def _resolve_prompt(value): - if isinstance(value, dict): - parts = [value.get("system_prompt", "")] - if value.get("tone"): - parts.append(f'Tone: {value["tone"]}') - if value.get("style"): - parts.append(f'Style: {value["style"]}') - return "\n".join(p for p in parts if p) - return str(value) + from hermes_cli.config import ( + _prompt_text, + render_personality_prompt, + ) if args in {"none", "default", "neutral"}: + # Persist the selection only. Never clear agent.system_prompt — + # that field is the user-owned manual overlay. try: - if "agent" not in config or not isinstance(config.get("agent"), dict): - config["agent"] = {} - config["agent"]["system_prompt"] = "" + if "display" not in config or not isinstance(config.get("display"), dict): + config["display"] = {} + config["display"]["personality"] = "" atomic_config_write(config_path, config) except Exception as e: return t("gateway.personality.save_failed", error=str(e)) - self._ephemeral_system_prompt = "" + self._ephemeral_system_prompt = _prompt_text( + cfg_get(config, "agent", "system_prompt", default="") + ) return t("gateway.personality.cleared") elif args in personalities: - new_prompt = _resolve_prompt(personalities[args]) + new_prompt = render_personality_prompt(personalities[args]) - # Write to config.yaml, same pattern as CLI save_config_value. + # Persist the personality name only — never write personality text + # into agent.system_prompt (user-owned manual overlay). try: - if "agent" not in config or not isinstance(config.get("agent"), dict): - config["agent"] = {} - config["agent"]["system_prompt"] = new_prompt + if "display" not in config or not isinstance(config.get("display"), dict): + config["display"] = {} + config["display"]["personality"] = args atomic_config_write(config_path, config) except Exception as e: return t("gateway.personality.save_failed", error=str(e)) diff --git a/hermes_cli/cli_commands_mixin.py b/hermes_cli/cli_commands_mixin.py index bb5e67874f..889fb02a4c 100644 --- a/hermes_cli/cli_commands_mixin.py +++ b/hermes_cli/cli_commands_mixin.py @@ -1340,21 +1340,34 @@ class CLICommandsMixin: personality_name = parts[1].strip().lower() if personality_name in {"none", "default", "neutral"}: - self.system_prompt = "" + # Persist the selection only. Never clear agent.system_prompt — + # that field is the user-owned manual overlay. + saved = save_config_value("display.personality", "") + try: + from hermes_cli.config import cfg_get, read_raw_config, _prompt_text + + self.system_prompt = _prompt_text( + cfg_get(read_raw_config(), "agent", "system_prompt", default="") + ) + except Exception: + self.system_prompt = "" self.agent = None # Force re-init - if save_config_value("agent.system_prompt", ""): + if saved: print("(^_^)b Personality cleared (saved to config)") else: print("(^_^) Personality cleared (session only)") print(" No personality overlay — using base agent behavior.") elif personality_name in self.personalities: - self.system_prompt = self._resolve_personality_prompt(self.personalities[personality_name]) + personality_prompt = self._resolve_personality_prompt( + self.personalities[personality_name] + ) + self.system_prompt = personality_prompt self.agent = None # Force re-init - if save_config_value("agent.system_prompt", self.system_prompt): + if save_config_value("display.personality", personality_name): print(f"(^_^)b Personality set to '{personality_name}' (saved to config)") else: print(f"(^_^) Personality set to '{personality_name}' (session only)") - print(f" \"{self.system_prompt[:60]}{'...' if len(self.system_prompt) > 60 else ''}\"") + print(f" \"{personality_prompt[:60]}{'...' if len(personality_prompt) > 60 else ''}\"") else: print(f"(._.) Unknown personality: {personality_name}") print(f" Available: none, {', '.join(self.personalities.keys())}") diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 67083a6926..4b58252b34 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -6471,8 +6471,9 @@ def _make_agent( pass cfg = _load_cfg() - agent_cfg = cfg.get("agent") or {} - system_prompt = _prompt_text(agent_cfg.get("system_prompt", "")) + from hermes_cli.config import resolve_ephemeral_system_prompt_from_config + + system_prompt = resolve_ephemeral_system_prompt_from_config(cfg) startup_skills = _parse_tui_skills_env() if startup_skills: from agent.skill_commands import build_preloaded_skills_prompt @@ -11336,8 +11337,10 @@ def _(rid, params: dict) -> dict: elif key == "personality": sid_key = params.get("session_id", "") pname, new_prompt = _validate_personality(str(value or ""), cfg) + # Personality text is an in-session overlay. Keep the + # user-owned global system prompt intact so changing a + # personality cannot destroy manual configuration. _write_config_key("display.personality", pname) - _write_config_key("agent.system_prompt", new_prompt) nv = str(value or "none") history_reset, info = _apply_personality_to_session( sid_key, session, new_prompt, pname