From e4a3ffa9c1d9698be9cf2b2a0094c1e7596779e8 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 18 Mar 2026 04:11:20 -0700 Subject: [PATCH] feat: use SOUL.md as primary agent identity instead of hardcoded default (#1922) SOUL.md now loads in slot #1 of the system prompt, replacing the hardcoded DEFAULT_AGENT_IDENTITY. This lets users fully customize the agent's identity and personality by editing ~/.hermes/SOUL.md without it conflicting with the built-in identity text. When SOUL.md is loaded as identity, it's excluded from the context files section to avoid appearing twice. When SOUL.md is missing, empty, unreadable, or skip_context_files is set, the hardcoded DEFAULT_AGENT_IDENTITY is used as a fallback. The default SOUL.md (seeded on first run) already contains the full Hermes personality, so existing installs are unaffected. Co-authored-by: Test --- agent/prompt_builder.py | 55 +++++++++++++++++++++++++++-------------- run_agent.py | 46 ++++++++++++++++++++-------------- 2 files changed, 65 insertions(+), 36 deletions(-) diff --git a/agent/prompt_builder.py b/agent/prompt_builder.py index 8dc3124ba..b9a415c1d 100644 --- a/agent/prompt_builder.py +++ b/agent/prompt_builder.py @@ -429,11 +429,42 @@ def _truncate_content(content: str, filename: str, max_chars: int = CONTEXT_FILE return head + marker + tail -def build_context_files_prompt(cwd: Optional[str] = None) -> str: +def load_soul_md() -> Optional[str]: + """Load SOUL.md from HERMES_HOME and return its content, or None. + + Used as the agent identity (slot #1 in the system prompt). When this + returns content, ``build_context_files_prompt`` should be called with + ``skip_soul=True`` so SOUL.md isn't injected twice. + """ + try: + from hermes_cli.config import ensure_hermes_home + ensure_hermes_home() + except Exception as e: + logger.debug("Could not ensure HERMES_HOME before loading SOUL.md: %s", e) + + soul_path = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) / "SOUL.md" + if not soul_path.exists(): + return None + try: + content = soul_path.read_text(encoding="utf-8").strip() + if not content: + return None + content = _scan_context_content(content, "SOUL.md") + content = _truncate_content(content, "SOUL.md") + return content + except Exception as e: + logger.debug("Could not read SOUL.md from %s: %s", soul_path, e) + return None + + +def build_context_files_prompt(cwd: Optional[str] = None, skip_soul: bool = False) -> str: """Discover and load context files for the system prompt. Discovery: AGENTS.md (recursive), .cursorrules / .cursor/rules/*.mdc, and SOUL.md from HERMES_HOME only. Each capped at 20,000 chars. + + When *skip_soul* is True, SOUL.md is not included here (it was already + loaded via ``load_soul_md()`` for the identity slot). """ if cwd is None: cwd = os.getcwd() @@ -523,23 +554,11 @@ def build_context_files_prompt(cwd: Optional[str] = None) -> str: hermes_md_content = _truncate_content(hermes_md_content, ".hermes.md") sections.append(hermes_md_content) - # SOUL.md from HERMES_HOME only - try: - from hermes_cli.config import ensure_hermes_home - ensure_hermes_home() - except Exception as e: - logger.debug("Could not ensure HERMES_HOME before loading SOUL.md: %s", e) - - soul_path = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) / "SOUL.md" - if soul_path.exists(): - try: - content = soul_path.read_text(encoding="utf-8").strip() - if content: - content = _scan_context_content(content, "SOUL.md") - content = _truncate_content(content, "SOUL.md") - sections.append(content) - except Exception as e: - logger.debug("Could not read SOUL.md from %s: %s", soul_path, e) + # SOUL.md from HERMES_HOME only — skip when already loaded as identity + if not skip_soul: + soul_content = load_soul_md() + if soul_content: + sections.append(soul_content) if not sections: return "" diff --git a/run_agent.py b/run_agent.py index 430e316e6..f1adf086a 100644 --- a/run_agent.py +++ b/run_agent.py @@ -85,7 +85,7 @@ from agent.model_metadata import ( ) from agent.context_compressor import ContextCompressor from agent.prompt_caching import apply_anthropic_cache_control -from agent.prompt_builder import build_skills_system_prompt, build_context_files_prompt +from agent.prompt_builder import build_skills_system_prompt, build_context_files_prompt, load_soul_md from agent.usage_pricing import estimate_usage_cost, normalize_usage from agent.display import ( KawaiiSpinner, build_tool_preview as _build_tool_preview, @@ -1948,28 +1948,38 @@ class AIAgent: is stable across all turns in a session, maximizing prefix cache hits. """ # Layers (in order): - # 1. Default agent identity (always present) + # 1. Agent identity — SOUL.md when available, else DEFAULT_AGENT_IDENTITY # 2. User / gateway system prompt (if provided) # 3. Persistent memory (frozen snapshot) # 4. Skills guidance (if skills tools are loaded) - # 5. Context files (SOUL.md, AGENTS.md, .cursorrules) + # 5. Context files (AGENTS.md, .cursorrules — SOUL.md excluded here when used as identity) # 6. Current date & time (frozen at build time) # 7. Platform-specific formatting hint - # If an AI peer name is configured in Honcho, personalise the identity line. - _ai_peer_name = ( - self._honcho_config.ai_peer - if self._honcho_config and self._honcho_config.ai_peer != "hermes" - else None - ) - if _ai_peer_name: - _identity = DEFAULT_AGENT_IDENTITY.replace( - "You are Hermes Agent", - f"You are {_ai_peer_name}", - 1, + + # Try SOUL.md as primary identity (unless context files are skipped) + _soul_loaded = False + if not self.skip_context_files: + _soul_content = load_soul_md() + if _soul_content: + prompt_parts = [_soul_content] + _soul_loaded = True + + if not _soul_loaded: + # Fallback to hardcoded identity + _ai_peer_name = ( + self._honcho_config.ai_peer + if self._honcho_config and self._honcho_config.ai_peer != "hermes" + else None ) - else: - _identity = DEFAULT_AGENT_IDENTITY - prompt_parts = [_identity] + if _ai_peer_name: + _identity = DEFAULT_AGENT_IDENTITY.replace( + "You are Hermes Agent", + f"You are {_ai_peer_name}", + 1, + ) + else: + _identity = DEFAULT_AGENT_IDENTITY + prompt_parts = [_identity] # Tool-aware behavioral guidance: only inject when the tools are loaded tool_guidance = [] @@ -2065,7 +2075,7 @@ class AIAgent: prompt_parts.append(skills_prompt) if not self.skip_context_files: - context_files_prompt = build_context_files_prompt() + context_files_prompt = build_context_files_prompt(skip_soul=_soul_loaded) if context_files_prompt: prompt_parts.append(context_files_prompt)