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
|
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.
|
"""Discover and load context files for the system prompt.
|
||||||
|
|
||||||
Discovery: AGENTS.md (recursive), .cursorrules / .cursor/rules/*.mdc,
|
Discovery: AGENTS.md (recursive), .cursorrules / .cursor/rules/*.mdc,
|
||||||
and SOUL.md from HERMES_HOME only. Each capped at 20,000 chars.
|
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:
|
if cwd is None:
|
||||||
cwd = os.getcwd()
|
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")
|
hermes_md_content = _truncate_content(hermes_md_content, ".hermes.md")
|
||||||
sections.append(hermes_md_content)
|
sections.append(hermes_md_content)
|
||||||
|
|
||||||
# SOUL.md from HERMES_HOME only
|
# SOUL.md from HERMES_HOME only — skip when already loaded as identity
|
||||||
try:
|
if not skip_soul:
|
||||||
from hermes_cli.config import ensure_hermes_home
|
soul_content = load_soul_md()
|
||||||
ensure_hermes_home()
|
if soul_content:
|
||||||
except Exception as e:
|
sections.append(soul_content)
|
||||||
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)
|
|
||||||
|
|
||||||
if not sections:
|
if not sections:
|
||||||
return ""
|
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.context_compressor import ContextCompressor
|
||||||
from agent.prompt_caching import apply_anthropic_cache_control
|
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.usage_pricing import estimate_usage_cost, normalize_usage
|
||||||
from agent.display import (
|
from agent.display import (
|
||||||
KawaiiSpinner, build_tool_preview as _build_tool_preview,
|
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.
|
is stable across all turns in a session, maximizing prefix cache hits.
|
||||||
"""
|
"""
|
||||||
# Layers (in order):
|
# 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)
|
# 2. User / gateway system prompt (if provided)
|
||||||
# 3. Persistent memory (frozen snapshot)
|
# 3. Persistent memory (frozen snapshot)
|
||||||
# 4. Skills guidance (if skills tools are loaded)
|
# 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)
|
# 6. Current date & time (frozen at build time)
|
||||||
# 7. Platform-specific formatting hint
|
# 7. Platform-specific formatting hint
|
||||||
# If an AI peer name is configured in Honcho, personalise the identity line.
|
|
||||||
_ai_peer_name = (
|
# Try SOUL.md as primary identity (unless context files are skipped)
|
||||||
self._honcho_config.ai_peer
|
_soul_loaded = False
|
||||||
if self._honcho_config and self._honcho_config.ai_peer != "hermes"
|
if not self.skip_context_files:
|
||||||
else None
|
_soul_content = load_soul_md()
|
||||||
)
|
if _soul_content:
|
||||||
if _ai_peer_name:
|
prompt_parts = [_soul_content]
|
||||||
_identity = DEFAULT_AGENT_IDENTITY.replace(
|
_soul_loaded = True
|
||||||
"You are Hermes Agent",
|
|
||||||
f"You are {_ai_peer_name}",
|
if not _soul_loaded:
|
||||||
1,
|
# 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:
|
if _ai_peer_name:
|
||||||
_identity = DEFAULT_AGENT_IDENTITY
|
_identity = DEFAULT_AGENT_IDENTITY.replace(
|
||||||
prompt_parts = [_identity]
|
"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-aware behavioral guidance: only inject when the tools are loaded
|
||||||
tool_guidance = []
|
tool_guidance = []
|
||||||
|
|
@ -2065,7 +2075,7 @@ class AIAgent:
|
||||||
prompt_parts.append(skills_prompt)
|
prompt_parts.append(skills_prompt)
|
||||||
|
|
||||||
if not self.skip_context_files:
|
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:
|
if context_files_prompt:
|
||||||
prompt_parts.append(context_files_prompt)
|
prompt_parts.append(context_files_prompt)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue