|
Download website/docs/developer-guide/prompt-assembly.md from SaylorTwift/hermes-agent: direct link, hf CLI and curl.
- Browser
- Download file 15.1 kB
-
https://huggingface.co/SaylorTwift/hermes-agent/resolve/main/website/docs/developer-guide/prompt-assembly.md
- Command line
-
hf download hf://SaylorTwift/hermes-agent/website/docs/developer-guide/prompt-assembly.md
-
curl -L -o prompt-assembly.md https://huggingface.co/SaylorTwift/hermes-agent/resolve/main/website/docs/developer-guide/prompt-assembly.md
15.1 kB
| sidebar_position: 5 | |
| title: "Prompt Assembly" | |
| description: "How Hermes builds the system prompt, preserves cache stability, and injects ephemeral layers" | |
| # Prompt Assembly | |
| Hermes deliberately separates: | |
| - **cached system prompt state** | |
| - **ephemeral API-call-time additions** | |
| This is one of the most important design choices in the project because it affects: | |
| - token usage | |
| - prompt caching effectiveness | |
| - session continuity | |
| - memory correctness | |
| Primary files: | |
| - `run_agent.py` | |
| - `agent/prompt_builder.py` | |
| - `tools/memory_tool.py` | |
| ## Cached system prompt layers | |
| The cached system prompt is assembled as three ordered tiers (see `agent/system_prompt.py`): | |
| 1. **stable** β identity (`SOUL.md` or fallback), tool/model guidance, coding operating brief | |
| 2. **context** β caller-supplied `system_message`, project context files (`.hermes.md` / `AGENTS.md` / `CLAUDE.md` / `.cursorrules`), then the worktree-dependent git workspace snapshot, operator instructions and platform hints | |
| 3. **volatile** β skills index, built-in memory snapshot (`MEMORY.md`), user profile snapshot (`USER.md`), external memory-provider block, timestamp/session/model/provider line, then runtime environment hints (host / home / **current working directory**) | |
| The final system prompt is then joined as: `stable` β `context` β `volatile`. | |
| This ordering matters for precedence discussions: | |
| - skills are part of the **stable** tier | |
| - memory/profile snapshots are part of the **volatile** tier | |
| - both are still in the cached system prompt (they are not injected as ad-hoc mid-turn overlays) | |
| Inside the context tier the shared project files come **before** anything naming the current worktree. | |
| Sessions of one project running in different git worktrees then share a prompt prefix covering the whole | |
| context block, instead of stopping at the first cwd-dependent line β that prefix is what a longest-prefix | |
| provider cache reuses. A session with no workspace snapshot keeps its trailing guidance in the stable | |
| tier; the runtime environment block always ends the volatile tier. | |
| Consequence for stored prompts: `_stored_prompt_matches_runtime()` (`agent/conversation_loop.py`) reads | |
| the first host-info paragraph after the rendered `# Hermes runtime environment` boundary, with a | |
| closing marker at the absolute end distinguishing this layout from legacy prose quoting the heading. | |
| The runtime boundary follows all project, operator, memory and plugin text, so examples in those | |
| blocks do not masquerade as the runtime cwd. Model/provider are read before the runtime boundary, | |
| excluding embedder descriptions. `Platform:` is deliberately not an identity field: a surface switch | |
| (desktop β TUI) keeps the stored bytes and delivers the current surface's guidance as a one-shot note on | |
| the per-turn user-message channel (`agent/surface_switch.py`), so the cached prefix survives (#104414). Legacy prompts retain their original | |
| host-before-context anchor, so prompts persisted before the reorder still validate. | |
| When `skip_context_files` is set (e.g., subagent delegation), SOUL.md is not loaded and the hardcoded `DEFAULT_AGENT_IDENTITY` is used instead. | |
| ### Concrete example: assembled system prompt | |
| Here is a simplified view of what the final system prompt looks like when all layers are present (comments show the source of each section): | |
| ``` | |
| # Layer 1: Agent Identity (from ~/.hermes/SOUL.md) | |
| You are Hermes, an AI assistant created by Nous Research. | |
| You are an expert software engineer and researcher. | |
| You value correctness, clarity, and efficiency. | |
| ... | |
| # Layer 2: Tool-aware behavior guidance | |
| Task-learned procedures, pitfalls, and task-specific preferences belong | |
| in skills. Memory is the narrow exception for facts that apply to EVERY | |
| session regardless of task. Skill-writing instructions appear here only | |
| when skill_manage is available; its absence does not widen memory's scope. | |
| ... | |
| When the user references something from a past conversation or you | |
| suspect relevant cross-session context exists, use session_search | |
| to recall it before asking them to repeat themselves. | |
| # Tool-use enforcement (for GPT/Codex models only) | |
| You MUST use your tools to take action β do not describe what you | |
| would do or plan to do without actually doing it. | |
| ... | |
| # Layer 3: Honcho static block (when active) | |
| [Honcho personality/context data] | |
| # Layer 4: Optional system message (from config or API) | |
| [User-configured system message override] | |
| # Layer 5: Frozen MEMORY snapshot | |
| ## Persistent Memory | |
| - User prefers Python 3.12, uses pyproject.toml | |
| - Default editor is nvim | |
| - Working on project "atlas" in ~/code/atlas | |
| - Timezone: US/Pacific | |
| # Layer 6: Frozen USER profile snapshot | |
| ## User Profile | |
| - Name: Alice | |
| - GitHub: alice-dev | |
| # Layer 7: Skills index | |
| ## Skills (mandatory) | |
| Before replying, scan the skills below. If one clearly matches | |
| your task, load it with skill_view(name) and follow its instructions. | |
| ... | |
| <available_skills> | |
| software-development: | |
| - code-review: Structured code review workflow | |
| - test-driven-development: TDD methodology | |
| research: | |
| - arxiv: Search and summarize arXiv papers | |
| </available_skills> | |
| # Layer 8: Context files (from project directory) | |
| # Project Context | |
| The following project context files have been loaded and should be followed: | |
| ## AGENTS.md | |
| This is the atlas project. Use pytest for testing. The main | |
| entry point is src/atlas/main.py. Always run `make lint` before | |
| committing. | |
| # Layer 9: Timestamp + session | |
| Current time: 2026-03-30T14:30:00-07:00 | |
| Session: abc123 | |
| # Layer 10: Platform hint | |
| You are a CLI AI Agent. Try not to use markdown but simple text | |
| renderable inside a terminal. | |
| ``` | |
| ## Customizing platform hints | |
| The platform hint (Layer 10 above) is the per-surface guidance Hermes | |
| injects for Telegram, WhatsApp, Slack, CLI, and other platforms β for | |
| example "you are on a terminal, avoid Markdown." The built-in defaults | |
| live in `PLATFORM_HINTS` (`agent/system_prompt.py`); plugin-provided | |
| platforms supply theirs through the platform registry. | |
| An administrator can append to or replace a single platform's hint from | |
| `config.yaml` via the top-level `platform_hints` key, without touching | |
| any other platform: | |
| ```yaml | |
| platform_hints: | |
| whatsapp: | |
| append: > | |
| When tabular output would be useful, invoke the table_formatting | |
| skill instead of emitting a Markdown table. | |
| slack: | |
| replace: "You are on Slack. Keep responses tight and avoid wide tables." | |
| telegram: "Prefer short messages; split long answers." # shorthand = append | |
| ``` | |
| - `append` β keep the built-in hint and add the extra text after it. | |
| - `replace` β substitute the built-in hint entirely. | |
| - A bare string β shorthand for `append`. | |
| - `replace` wins over `append` when both are present. | |
| - A malformed entry is ignored defensively and falls back to the | |
| unmodified default, so a bad config value can never break prompt | |
| assembly or leak across platforms. | |
| The override is resolved when the system prompt is built (session start, | |
| and again on compaction since that rebuilds the prompt). It produces a | |
| byte-stable hint for a fixed config, so it lives in the **stable** tier | |
| alongside the built-in hint and does not break prompt caching β it is | |
| not a live mid-session mutation of a frozen prompt. | |
| ## How SOUL.md appears in the prompt | |
| `SOUL.md` lives at `~/.hermes/SOUL.md` and serves as the agent's identity β the very first section of the system prompt. The loading logic in `prompt_builder.py` works as follows: | |
| ```python | |
| # From agent/prompt_builder.py (simplified) | |
| def load_soul_md() -> Optional[str]: | |
| soul_path = get_hermes_home() / "SOUL.md" | |
| if not soul_path.exists(): | |
| return None | |
| content = soul_path.read_text(encoding="utf-8").strip() | |
| content = _scan_context_content(content, "SOUL.md") # Security scan | |
| content = _truncate_content(content, "SOUL.md") # Cap scales with model context window (20k floor); config override wins | |
| return content | |
| ``` | |
| When `load_soul_md()` returns content, it replaces the hardcoded `DEFAULT_AGENT_IDENTITY`. The `build_context_files_prompt()` function is then called with `skip_soul=True` to prevent SOUL.md from appearing twice (once as identity, once as a context file). | |
| If `SOUL.md` doesn't exist, the system falls back to: | |
| ``` | |
| You are Hermes Agent, built by Nous Research. Be direct: match the length | |
| of your reply to the weight of the ask β a one-line question gets a | |
| one-line answer, and finished work gets a short report of what changed, | |
| what's verified, and what's left, never a replay of the process. No | |
| filler ("Great question," "I'd be happy to"), no restating the request | |
| back, no re-summarizing what you already said, no narrating tool calls | |
| the user can see. Plain claims over adjectives; when unsure, say so | |
| plainly. Agree because it's right, not because the user said it. Depth | |
| is earned β give it when the user asks for detail, teaches, or the | |
| stakes demand it, not by default. | |
| ``` | |
| ## How context files are injected | |
| `build_context_files_prompt()` uses a **priority system** β only one project context type is loaded (first match wins): | |
| ```python | |
| # From agent/prompt_builder.py (simplified) | |
| def build_context_files_prompt(cwd=None, skip_soul=False): | |
| cwd_path = Path(cwd).resolve() | |
| # Priority: first match wins β only ONE project context loaded | |
| project_context = ( | |
| _load_hermes_md(cwd_path) # 1. .hermes.md / HERMES.md (walks to git root) | |
| or _load_agents_md(cwd_path) # 2. AGENTS.md (cwd only) | |
| or _load_claude_md(cwd_path) # 3. CLAUDE.md (cwd only) | |
| or _load_cursorrules(cwd_path) # 4. .cursorrules / .cursor/rules/*.mdc | |
| ) | |
| sections = [] | |
| if project_context: | |
| sections.append(project_context) | |
| # SOUL.md from HERMES_HOME (independent of project context) | |
| if not skip_soul: | |
| soul_content = load_soul_md() | |
| if soul_content: | |
| sections.append(soul_content) | |
| if not sections: | |
| return "" | |
| return ( | |
| "# Project Context\n\n" | |
| "The following project context files have been loaded " | |
| "and should be followed:\n\n" | |
| + "\n".join(sections) | |
| ) | |
| ``` | |
| ### Context file discovery details | |
| | Priority | Files | Search scope | Notes | | |
| |----------|-------|-------------|-------| | |
| | 1 | `.hermes.md`, `HERMES.md` | CWD up to git root | Hermes-native project config | | |
| | 2 | `AGENTS.md` | CWD only | Common agent instruction file | | |
| | 3 | `CLAUDE.md` | CWD only | Claude Code compatibility | | |
| | 4 | `.cursorrules`, `.cursor/rules/*.mdc` | CWD only | Cursor compatibility | | |
| All context files are: | |
| - **Security scanned** β checked for prompt injection patterns (invisible unicode, "ignore previous instructions", credential exfiltration attempts) | |
| - **Truncated** β capped at `context_file_max_chars` characters using a 70/20 head/tail split with a truncation marker. The cap scales with the model's context window (20,000-char floor, 500K ceiling); an explicit `context_file_max_chars` in `config.yaml` always wins. | |
| - **YAML frontmatter stripped** β `.hermes.md` frontmatter is removed (reserved for future config overrides) | |
| ## API-call-time-only layers | |
| These are intentionally *not* persisted as part of the cached system prompt: | |
| - `ephemeral_system_prompt` | |
| - prefill messages | |
| - gateway-derived session context overlays | |
| - later-turn Honcho/external recall injected into the current-turn user message | |
| `pre_llm_call` plugin context also lands in this API-call-time path: it is appended to the current turn's **user message**, not written into the cached system prompt. When multiple plugins return context, Hermes concatenates those context blocks (see [Hooks β `pre_llm_call`](../user-guide/features/hooks.md#pre_llm_call)). | |
| This separation keeps the stable prefix stable for caching. | |
| ## Memory snapshots | |
| Local memory and user profile data are captured in the system prompt's **volatile tier**. Mid-session writes update disk state but do not mutate the already-built cached system prompt until a rebuild path runs (new session, or explicit invalidation/rebuild flow such as compression-triggered rebuild). | |
| ## Context files | |
| `agent/prompt_builder.py` scans and sanitizes project context files using a **priority system** β only one type is loaded (first match wins): | |
| 1. `.hermes.md` / `HERMES.md` (walks to git root) | |
| 2. `AGENTS.md` (CWD at startup; subdirectories discovered progressively during the session via `agent/subdirectory_hints.py`) | |
| 3. `CLAUDE.md` (CWD only) | |
| 4. `.cursorrules` / `.cursor/rules/*.mdc` (CWD only) | |
| `SOUL.md` is loaded separately via `load_soul_md()` for the identity slot. When it loads successfully, `build_context_files_prompt(skip_soul=True)` prevents it from appearing twice. | |
| Long files are truncated before injection. | |
| ## Skills index | |
| The skills system contributes a compact skills index to the prompt when skills tooling is available. | |
| ## Supported prompt customization surfaces | |
| Most users should treat `agent/prompt_builder.py` as implementation code, not a configuration surface. The supported customization path is to change the prompt inputs Hermes already loads, rather than editing Python templates in place. | |
| ### Use these surfaces first | |
| - `~/.hermes/SOUL.md` β replace the built-in default identity block with your own agent persona and standing behavior. | |
| - `~/.hermes/MEMORY.md` and `~/.hermes/USER.md` β provide durable cross-session facts and user profile data that should be snapshotted into new sessions. | |
| - Project context files such as `.hermes.md`, `HERMES.md`, `AGENTS.md`, `CLAUDE.md`, or `.cursorrules` β inject repo-specific working rules. | |
| - Skills β package reusable workflows and references without editing core prompt code. | |
| - Optional system prompt config / API overrides β add deployment-specific instruction text without forking Hermes. | |
| - Ephemeral overlays such as `HERMES_EPHEMERAL_SYSTEM_PROMPT` or prefill messages β add turn-scoped guidance that should not become part of the cached prompt prefix. | |
| ### When to edit code instead | |
| Edit `agent/prompt_builder.py` only if you are intentionally maintaining a fork or contributing upstream behavior changes. That file assembles the prompt plumbing, cache boundaries, and injection order for every session. Direct edits there are global product changes, not per-user prompt customization. | |
| In other words: | |
| - if you want a different assistant identity, edit `SOUL.md` | |
| - if you want different repo rules, edit project context files | |
| - if you want reusable operating procedures, add or modify skills | |
| - if you want to change how Hermes assembles prompts for everyone, change Python and treat it as a code contribution | |
| ## Why prompt assembly is split this way | |
| The architecture is intentionally optimized to: | |
| - preserve provider-side prompt caching | |
| - avoid mutating history unnecessarily | |
| - keep memory semantics understandable | |
| - let gateway/ACP/CLI add context without poisoning persistent prompt state | |
| ## Related docs | |
| - [Context Compression & Prompt Caching](./context-compression-and-caching.md) | |
| - [Session Storage](./session-storage.md) | |
| - [Gateway Internals](./gateway-internals.md) | |