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 <kyssta-exe@users.noreply.github.com>
Co-authored-by: EMT5320 <1908937833@qq.com>
This commit is contained in:
Brooklyn Nicholson
2026-08-08 14:01:56 -05:00
parent da6f0030ab
commit a0d406dcd8
5 changed files with 53 additions and 32 deletions
+5 -2
View File
@@ -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", {})
+6 -4
View File
@@ -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,
+18 -18
View File
@@ -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))
+18 -5
View File
@@ -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())}")
+6 -3
View File
@@ -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