Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 37 additions & 18 deletions agent/prompt_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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 ""
Expand Down
46 changes: 28 additions & 18 deletions run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
)
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,
Expand Down Expand Up @@ -1935,28 +1935,38 @@ def _build_system_prompt(self, system_message: str = None) -> str:
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 = []
Expand Down Expand Up @@ -2052,7 +2062,7 @@ def _build_system_prompt(self, system_message: str = None) -> str:
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)

Expand Down
Loading