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 <test@test.com>
This commit is contained in:
parent
1fa3737134
commit
e4a3ffa9c1
2 changed files with 65 additions and 36 deletions
|
|
@ -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 ""
|
||||
|
|
|
|||
46
run_agent.py
46
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)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue