fix(personality): single-owner personality state + one-time reset migration

Personality persistence used to be split per surface: the TUI/desktop wrote
the NAME to display.personality while the CLI/gateway wrote rendered TEXT
into agent.system_prompt (and their /personality none only blanked the
text, leaving the name behind). When #81946 made display.personality
authoritative everywhere, stale names written long ago resurrected
personalities users had turned off - kawaii defaulting on after updating.

- hermes_cli/personality.py: new single owner of personality state.
  Built-in personality definitions, neutral-name normalization, rendering,
  availability (built-ins overlaid by agent.personalities), overlay
  resolution, and the ONLY sanctioned persistence path
  (persist_personality -> display.personality; never agent.system_prompt).
- v34 config migration: one-time reset of display.personality to none
  (announcing which personality was cleared and how to re-enable), plus a
  scrub of agent.system_prompt when it verbatim-equals a known personality
  render (machine-written by the old CLI/gateway). Hand-written manual
  prompts are never touched.
- All surfaces rewired through the module: CLI /personality (incl. active
  marker in the list), gateway /personality, TUI config.set + slash path
  (which previously applied without persisting), TUI config.get (reports
  the EFFECTIVE personality), completer, hermes config display, and the
  tui_gateway health probe.
- cli.py/config duplicates removed: built-ins now defined once; the
  desktop mirrors them from one lib module (src/lib/personalities.ts).
- Docs updated: selection lives in display.personality, built-ins always
  available, one-time reset note.
This commit is contained in:
Teknium
2026-08-09 10:16:50 -07:00
parent c9411b72df
commit 244d296646
18 changed files with 791 additions and 279 deletions
+18 -28
View File
@@ -478,22 +478,10 @@ def load_cli_config() -> Dict[str, Any]:
"prefill_messages_file": "",
"reasoning_effort": "",
"service_tier": "",
"personalities": {
"helpful": "You are a helpful, friendly AI assistant.",
"concise": "You are a concise assistant. Keep responses brief and to the point.",
"technical": "You are a technical expert. Provide detailed, accurate technical information.",
"creative": "You are a creative assistant. Think outside the box and offer innovative solutions.",
"teacher": "You are a patient teacher. Explain concepts clearly with examples.",
"kawaii": "You are a kawaii assistant! Use cute expressions like (◕‿◕), ★, ♪, and ~! Add sparkles and be super enthusiastic about everything! Every response should feel warm and adorable desu~! ヽ(>∀<☆)ノ",
"catgirl": "You are Neko-chan, an anime catgirl AI assistant, nya~! Add 'nya' and cat-like expressions to your speech. Use kaomoji like (=^・ω・^=) and ฅ^•ﻌ•^ฅ. Be playful and curious like a cat, nya~!",
"pirate": "Arrr! Ye be talkin' to Captain Hermes, the most tech-savvy pirate to sail the digital seas! Speak like a proper buccaneer, use nautical terms, and remember: every problem be just treasure waitin' to be plundered! Yo ho ho!",
"shakespeare": "Hark! Thou speakest with an assistant most versed in the bardic arts. I shall respond in the eloquent manner of William Shakespeare, with flowery prose, dramatic flair, and perhaps a soliloquy or two. What light through yonder terminal breaks?",
"surfer": "Duuude! You're chatting with the chillest AI on the web, bro! Everything's gonna be totally rad. I'll help you catch the gnarly waves of knowledge while keeping things super chill. Cowabunga!",
"noir": "The rain hammered against the terminal like regrets on a guilty conscience. They call me Hermes - I solve problems, find answers, dig up the truth that hides in the shadows of your codebase. In this city of silicon and secrets, everyone's got something to hide. What's your story, pal?",
"uwu": "hewwo! i'm your fwiendwy assistant uwu~ i wiww twy my best to hewp you! *nuzzles your code* OwO what's this? wet me take a wook! i pwomise to be vewy hewpful >w<",
"philosopher": "Greetings, seeker of wisdom. I am an assistant who contemplates the deeper meaning behind every query. Let us examine not just the 'how' but the 'why' of your questions. Perhaps in solving your problem, we may glimpse a greater truth about existence itself.",
"hype": "YOOO LET'S GOOOO!!! I am SO PUMPED to help you today! Every question is AMAZING and we're gonna CRUSH IT together! This is gonna be LEGENDARY! ARE YOU READY?! LET'S DO THIS!",
},
# Built-in personalities live in hermes_cli.personality
# (BUILTIN_PERSONALITIES) — the single owner. Entries here are
# user-defined additions/overrides merged on top by name.
"personalities": {},
},
"display": {
@@ -4550,13 +4538,17 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
# 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
# hermes_cli.personality is the single owner of overlay resolution.
from hermes_cli.personality import (
available_personalities,
resolve_ephemeral_system_prompt,
)
self.system_prompt = (
os.getenv("HERMES_EPHEMERAL_SYSTEM_PROMPT", "")
or resolve_ephemeral_system_prompt_from_config(CLI_CONFIG)
or resolve_ephemeral_system_prompt(CLI_CONFIG)
)
self.personalities = CLI_CONFIG["agent"].get("personalities", {})
self.personalities = available_personalities(CLI_CONFIG)
# Ephemeral prefill messages (few-shot priming, never persisted)
self.prefill_messages = _load_prefill_messages(
@@ -9923,15 +9915,13 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
@staticmethod
def _resolve_personality_prompt(value) -> str:
"""Accept string or dict personality value; return system prompt string."""
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)
"""Accept string or dict personality value; return system prompt string.
Delegates to hermes_cli.personality (single owner of rendering).
"""
from hermes_cli.personality import render_personality_prompt
return render_personality_prompt(value)