hermes源码学习4-Prompt 组装

Hermes 刻意将以下内容分离:

  • 已缓存的系统 prompt 状态
  • API 调用时临时添加的内容

这是项目中最重要的设计决策之一,因为它影响:

  • token 用量
  • prompt 缓存效果
  • 会话连续性
  • 记忆正确性

主要文件:

  • run_agent.py
  • agent/prompt_builder.py
  • tools/memory_tool.py

已缓存的系统 prompt 层

已缓存的系统 prompt 大致按以下顺序组装:

  1. agent 身份 --- 优先使用 HERMES_HOME 中的 SOUL.md,否则回退到 prompt_builder.py 中的 DEFAULT_AGENT_IDENTITY
  2. 工具感知行为指导
  3. Honcho 静态块(激活时)
  4. 可选系统消息
  5. 冻结的 MEMORY 快照
  6. 冻结的 USER 配置文件快照
  7. skills 索引
  8. 上下文文件(AGENTS.md.cursorrules.cursor/rules/*.mdc)--- 若 SOUL.md 已在第 1 步作为身份加载,则此处再包含它
  9. 时间戳 / 可选会话 ID
  10. 平台提示

当设置了 skip_context_files(例如子 agent 委托)时,不会加载 SOUL.md,而是使用硬编码的 DEFAULT_AGENT_IDENTITY

具体示例:组装后的系统 prompt

以下是所有层都存在时最终系统 prompt 的简化视图(注释说明每个部分的来源):

复制代码
# 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
You have persistent memory across sessions. Save durable facts using
the memory tool: user preferences, environment details, tool quirks,
and stable conventions. Memory is injected into every turn, so keep
it compact and focused on facts that will still matter later.
...
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.

SOUL.md 在 prompt 中的位置

SOUL.md 位于 ~/.hermes/SOUL.md,作为 agent 的身份标识------系统 prompt 的第一个部分。prompt_builder.py 中的加载逻辑如下:

复制代码
# 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 at 20k chars
    return content

load_soul_md() 返回内容时,它会替换硬编码的 DEFAULT_AGENT_IDENTITY。随后调用 build_context_files_prompt() 时传入 skip_soul=True,以防止 SOUL.md 出现两次(一次作为身份,一次作为上下文文件)。

SOUL.md 不存在,系统将回退到:

复制代码
You are Hermes Agent, an intelligent AI assistant created by Nous Research.
You are helpful, knowledgeable, and direct. You assist users with a wide
range of tasks including answering questions, writing and editing code,
analyzing information, creative work, and executing actions via your tools.
You communicate clearly, admit uncertainty when appropriate, and prioritize
being genuinely useful over being verbose unless otherwise directed below.
Be targeted and efficient in your exploration and investigations.

上下文文件的注入方式

build_context_files_prompt() 使用优先级系统------只加载一种项目上下文类型(先匹配先赢):

复制代码
# 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)
    )

上下文文件发现详情

优先级 文件 搜索范围 说明
1 .hermes.mdHERMES.md 从 CWD 向上至 git 根目录 Hermes 原生项目配置
2 AGENTS.md 仅 CWD 常见 agent 指令文件
3 CLAUDE.md 仅 CWD Claude Code 兼容性
4 .cursorrules.cursor/rules/*.mdc 仅 CWD Cursor 兼容性

所有上下文文件均会:

  • 安全扫描 --- 检查 prompt 注入模式(不可见 unicode、"ignore previous instructions"、凭据窃取尝试)
  • 截断处理 --- 使用 70/20 头尾比例上限为 20,000 字符,并附截断标记
  • 剥离 YAML frontmatter --- .hermes.md 的 frontmatter 会被移除(保留供未来配置覆盖使用)

仅在 API 调用时生效的层

以下内容刻意作为已缓存系统 prompt 的一部分持久化:

  • ephemeral_system_prompt
  • prefill 消息
  • gateway 派生的会话上下文覆盖层
  • 注入当前轮次用户消息的后续轮次 Honcho 召回内容

这种分离使稳定前缀保持稳定,从而有效缓存。

记忆快照

本地记忆和用户配置文件数据在会话开始时作为冻结快照注入。会话中途的写入操作会更新磁盘状态,但不会修改已构建的系统 prompt,直到新会话开始或强制重建时才生效。

上下文文件

agent/prompt_builder.py 使用优先级系统扫描并清理项目上下文文件------只加载一种类型(先匹配先赢):

  1. .hermes.md / HERMES.md(向上遍历至 git 根目录)
  2. AGENTS.md(启动时的 CWD;子目录在会话期间通过 agent/subdirectory_hints.py 逐步发现)
  3. CLAUDE.md(仅 CWD)
  4. .cursorrules / .cursor/rules/*.mdc(仅 CWD)

SOUL.md 通过 load_soul_md() 单独加载用于身份槽位。加载成功后,build_context_files_prompt(skip_soul=True) 会防止其出现两次。

长文件在注入前会被截断。

Skills 索引

当 skills 工具可用时,skills 系统会向 prompt 贡献一个紧凑的 skills 索引。

支持的 prompt 自定义入口

大多数用户应将 agent/prompt_builder.py 视为实现代码,而非配置入口。推荐的自定义路径是修改 Hermes 已加载的 prompt 输入,而非直接编辑 Python 模板。

优先使用这些入口

  • ~/.hermes/SOUL.md --- 用自定义 agent 角色和固定行为替换内置默认身份块。
  • ~/.hermes/MEMORY.md~/.hermes/USER.md --- 提供应在新会话中快照的持久跨会话事实和用户配置文件数据。
  • 项目上下文文件,如 .hermes.mdHERMES.mdAGENTS.mdCLAUDE.md.cursorrules --- 注入仓库特定的工作规则。
  • Skills --- 打包可复用的工作流和参考资料,无需编辑核心 prompt 代码。
  • 可选系统 prompt 配置 / API 覆盖 --- 添加部署特定的指令文本,无需 fork Hermes。
  • 临时覆盖层,如 HERMES_EPHEMERAL_SYSTEM_PROMPT 或 prefill 消息 --- 添加不应成为已缓存 prompt 前缀一部分的轮次级指导。

何时应编辑代码

仅当你刻意维护一个 fork 或向上游贡献行为变更时,才编辑 agent/prompt_builder.py。该文件为每个会话组装 prompt 管道、缓存边界和注入顺序。直接编辑该文件是全局产品变更,而非针对单个用户的 prompt 自定义。

换言之:

  • 若想要不同的助手身份,编辑 SOUL.md
  • 若想要不同的仓库规则,编辑项目上下文文件
  • 若想要可复用的操作流程,添加或修改 skills
  • 若想改变 Hermes 为所有人组装 prompt 的方式,修改 Python 代码并将其视为代码贡献

Prompt 组装为何如此拆分

该架构刻意优化以:

  • 保留提供商侧的 prompt 缓存
  • 避免不必要地修改历史记录
  • 保持记忆语义清晰可理解
  • 允许 gateway/ACP/CLI 添加上下文而不污染持久 prompt 状态

结合源码学习下

1.构建环境参数

复制代码
def build_environment_hints() -> str:
    """Return environment-specific guidance for the system prompt.

    Always emits a factual block describing the execution environment:
    - For **local** terminal backends: the host OS, user home, current
      working directory (plus a Windows-only note about hostname != user
      and a Windows-only note that `terminal` shells out to bash, not
      PowerShell).
    - For **remote / sandbox** terminal backends (docker, singularity,
      modal, daytona, ssh): host info is **suppressed**
      because the agent's tools can't touch the host --- only the backend
      matters. A live probe inside the backend reports its OS, user, $HOME,
      and cwd. Falls back to a static summary if the probe fails.

    The WSL environment hint is appended unchanged when running under WSL.
    """
    import platform
    import sys

    hints: list[str] = []

    backend = (os.getenv("TERMINAL_ENV") or "local").strip().lower()
    is_remote_backend = backend in _REMOTE_TERMINAL_BACKENDS

    if not is_remote_backend:
        # --- Host info block (local backend: host == where tools run) ---
        host_lines: list[str] = []
        if is_wsl():
            host_lines.append("Host: WSL (Windows Subsystem for Linux)")
        elif sys.platform == "win32":
            host_lines.append(f"Host: Windows ({platform.release()})")
        elif sys.platform == "darwin":
            mac_ver = platform.mac_ver()[0]
            host_lines.append(f"Host: macOS ({mac_ver or platform.release()})")
        else:
            host_lines.append(f"Host: {platform.system()} ({platform.release()})")

        host_lines.append(f"User home directory: {os.path.expanduser('~')}")
        try:
            host_lines.append(f"Current working directory: {resolve_agent_cwd()}")
        except OSError:
            pass

        if sys.platform == "win32" and not is_wsl():
            host_lines.append(
                "Note: on Windows, the machine hostname (e.g. from `hostname` "
                "or uname) is NOT the username. Use the 'User home directory' "
                "above to construct paths under C:\\Users\\<user>\\, never the "
                "hostname."
            )
        hints.append("\n".join(host_lines))

        # Windows-local terminal runs bash, not PowerShell --- the model must
        # know this or it will issue PowerShell syntax and fail.
        if sys.platform == "win32" and not is_wsl():
            hints.append(_WINDOWS_BASH_SHELL_HINT)
    else:
        # --- Remote backend block (host info suppressed) ---
        probe = _probe_remote_backend(backend)
        if probe:
            hints.append(
                f"Terminal backend: {backend}. Your `terminal`, `read_file`, "
                f"`write_file`, `patch`, and `search_files` tools all operate "
                f"inside this {backend} environment --- NOT on the machine "
                f"where Hermes itself is running. The host OS, home, and cwd "
                f"of the Hermes process are irrelevant; only the following "
                f"backend state matters:\n{probe}"
            )
        else:
            description = _BACKEND_FALLBACK_DESCRIPTIONS.get(
                backend, f"a {backend} environment (likely Linux)"
            )
            hints.append(
                f"Terminal backend: {backend}. Your `terminal`, `read_file`, "
                f"`write_file`, `patch`, and `search_files` tools all operate "
                f"inside {description} --- NOT on the machine where Hermes "
                f"itself runs. The backend probe didn't respond at "
                f"prompt-build time, so the sandbox's current user, $HOME, "
                f"and working directory are unknown from here. If you need "
                f"them, probe directly with a terminal call like "
                f"`uname -a && whoami && pwd`."
            )

    if is_wsl():
        hints.append(WSL_ENVIRONMENT_HINT)

    # Embedder-supplied environment description. Lets a host that wraps Hermes
    # (e.g. a sandbox runner / managed platform) explain the environment the
    # agent is running in --- proxy, credential handling, mount layout --- without
    # forking the identity slot (SOUL.md). Read once at prompt-build time, so
    # it's part of the stable, cache-safe system prompt. The env var is the
    # build-time/embedder mechanism (set in a container ENV); config.yaml
    # ``agent.environment_hint`` is the user-facing surface. Env var wins.
    extra = (os.getenv("HERMES_ENVIRONMENT_HINT") or "").strip()
    if not extra:
        try:
            from hermes_cli.config import load_config

            extra = str(
                (load_config().get("agent", {}) or {}).get("environment_hint", "")
            ).strip()
        except Exception as e:
            logger.debug("Could not read agent.environment_hint from config: %s", e)
    if extra:
        hints.append(extra)

    return "\n\n".join(hints)

2.构建skill参数

复制代码
def build_skills_system_prompt(
    available_tools: "set[str] | None" = None,
    available_toolsets: "set[str] | None" = None,
) -> str:
    """Build a compact skill index for the system prompt.

    Two-layer cache:
      1. In-process LRU dict keyed by (skills_dir, tools, toolsets)
      2. Disk snapshot (``.skills_prompt_snapshot.json``) validated by
         mtime/size manifest --- survives process restarts

    Falls back to a full filesystem scan when both layers miss.

    External skill directories (``skills.external_dirs`` in config.yaml) are
    scanned alongside the local ``~/.hermes/skills/`` directory.  External dirs
    are read-only --- they appear in the index but new skills are always created
    in the local dir.  Local skills take precedence when names collide.
    """
    skills_dir = get_skills_dir()
    external_dirs = get_all_skills_dirs()[1:]  # skip local (index 0)

    if not skills_dir.exists() and not external_dirs:
        return ""

    # ── Layer 1: in-process LRU cache ─────────────────────────────────
    # Include the resolved platform so per-platform disabled-skill lists
    # produce distinct cache entries (gateway serves multiple platforms).
    from gateway.session_context import get_session_env
    _platform_hint = (
        os.environ.get("HERMES_PLATFORM")
        or get_session_env("HERMES_SESSION_PLATFORM")
        or ""
    )
    disabled = get_disabled_skill_names()
    cache_key = (
        str(skills_dir.resolve()),
        tuple(str(d) for d in external_dirs),
        tuple(sorted(str(t) for t in (available_tools or set()))),
        tuple(sorted(str(ts) for ts in (available_toolsets or set()))),
        _platform_hint,
        tuple(sorted(disabled)),
    )
    with _SKILLS_PROMPT_CACHE_LOCK:
        cached = _SKILLS_PROMPT_CACHE.get(cache_key)
        if cached is not None:
            _SKILLS_PROMPT_CACHE.move_to_end(cache_key)
            return cached

    # ── Layer 2: disk snapshot ────────────────────────────────────────
    snapshot = _load_skills_snapshot(skills_dir)

    skills_by_category: dict[str, list[tuple[str, str]]] = {}
    category_descriptions: dict[str, str] = {}

    if snapshot is not None:
        # Fast path: use pre-parsed metadata from disk
        for entry in snapshot.get("skills", []):
            if not isinstance(entry, dict):
                continue
            skill_name = entry.get("skill_name") or ""
            category = entry.get("category") or "general"
            frontmatter_name = entry.get("frontmatter_name") or skill_name
            platforms = entry.get("platforms") or []
            if not skill_matches_platform({"platforms": platforms}):
                continue
            if frontmatter_name in disabled or skill_name in disabled:
                continue
            if not _skill_should_show(
                entry.get("conditions") or {},
                available_tools,
                available_toolsets,
            ):
                continue
            skills_by_category.setdefault(category, []).append(
                (frontmatter_name, entry.get("description", ""))
            )
        category_descriptions = {
            str(k): str(v)
            for k, v in (snapshot.get("category_descriptions") or {}).items()
        }
    else:
        # Cold path: full filesystem scan + write snapshot for next time
        skill_entries: list[dict] = []
        for skill_file in iter_skill_index_files(skills_dir, "SKILL.md"):
            is_compatible, frontmatter, desc = _parse_skill_file(skill_file)
            entry = _build_snapshot_entry(skill_file, skills_dir, frontmatter, desc)
            skill_entries.append(entry)
            if not is_compatible:
                continue
            skill_name = entry["skill_name"]
            if entry["frontmatter_name"] in disabled or skill_name in disabled:
                continue
            if not _skill_should_show(
                extract_skill_conditions(frontmatter),
                available_tools,
                available_toolsets,
            ):
                continue
            skills_by_category.setdefault(entry["category"], []).append(
                (entry["frontmatter_name"], entry["description"])
            )

        # Read category-level DESCRIPTION.md files
        for desc_file in iter_skill_index_files(skills_dir, "DESCRIPTION.md"):
            try:
                content = desc_file.read_text(encoding="utf-8")
                fm, _ = parse_frontmatter(content)
                cat_desc = fm.get("description")
                if not cat_desc:
                    continue
                rel = desc_file.relative_to(skills_dir)
                cat = "/".join(rel.parts[:-1]) if len(rel.parts) > 1 else "general"
                category_descriptions[cat] = str(cat_desc).strip().strip("'\"")
            except Exception as e:
                logger.debug("Could not read skill description %s: %s", desc_file, e)

        _write_skills_snapshot(
            skills_dir,
            _build_skills_manifest(skills_dir),
            skill_entries,
            category_descriptions,
        )

    # ── External skill directories ─────────────────────────────────────
    # Scan external dirs directly (no snapshot caching --- they're read-only
    # and typically small).  Local skills already in skills_by_category take
    # precedence: we track seen names and skip duplicates from external dirs.
    seen_skill_names: set[str] = set()
    for cat_skills in skills_by_category.values():
        for name, _desc in cat_skills:
            seen_skill_names.add(name)

    for ext_dir in external_dirs:
        if not ext_dir.exists():
            continue
        for skill_file in iter_skill_index_files(ext_dir, "SKILL.md"):
            try:
                is_compatible, frontmatter, desc = _parse_skill_file(skill_file)
                if not is_compatible:
                    continue
                entry = _build_snapshot_entry(skill_file, ext_dir, frontmatter, desc)
                skill_name = entry["skill_name"]
                frontmatter_name = entry["frontmatter_name"]
                if frontmatter_name in seen_skill_names:
                    continue
                if frontmatter_name in disabled or skill_name in disabled:
                    continue
                if not _skill_should_show(
                    extract_skill_conditions(frontmatter),
                    available_tools,
                    available_toolsets,
                ):
                    continue
                seen_skill_names.add(frontmatter_name)
                skills_by_category.setdefault(entry["category"], []).append(
                    (frontmatter_name, entry["description"])
                )
            except Exception as e:
                logger.debug("Error reading external skill %s: %s", skill_file, e)

        # External category descriptions
        for desc_file in iter_skill_index_files(ext_dir, "DESCRIPTION.md"):
            try:
                content = desc_file.read_text(encoding="utf-8")
                fm, _ = parse_frontmatter(content)
                cat_desc = fm.get("description")
                if not cat_desc:
                    continue
                rel = desc_file.relative_to(ext_dir)
                cat = "/".join(rel.parts[:-1]) if len(rel.parts) > 1 else "general"
                category_descriptions.setdefault(cat, str(cat_desc).strip().strip("'\""))
            except Exception as e:
                logger.debug("Could not read external skill description %s: %s", desc_file, e)

    if not skills_by_category:
        result = ""
    else:
        index_lines = []
        for category in sorted(skills_by_category.keys()):
            cat_desc = category_descriptions.get(category, "")
            if cat_desc:
                index_lines.append(f"  {category}: {cat_desc}")
            else:
                index_lines.append(f"  {category}:")
            # Deduplicate and sort skills within each category
            seen = set()
            for name, desc in sorted(skills_by_category[category], key=lambda x: x[0]):
                if name in seen:
                    continue
                seen.add(name)
                if desc:
                    index_lines.append(f"    - {name}: {desc}")
                else:
                    index_lines.append(f"    - {name}")

        result = (
            "## Skills (mandatory)\n"
            "Before replying, scan the skills below. If a skill matches or is even partially relevant "
            "to your task, you MUST load it with skill_view(name) and follow its instructions. "
            "Err on the side of loading --- it is always better to have context you don't need "
            "than to miss critical steps, pitfalls, or established workflows. "
            "Skills contain specialized knowledge --- API endpoints, tool-specific commands, "
            "and proven workflows that outperform general-purpose approaches. Load the skill "
            "even if you think you could handle the task with basic tools like web_search or terminal. "
            "Skills also encode the user's preferred approach, conventions, and quality standards "
            "for tasks like code review, planning, and testing --- load them even for tasks you "
            "already know how to do, because the skill defines how it should be done here.\n"
            "Whenever the user asks you to configure, set up, install, enable, disable, modify, "
            "or troubleshoot Hermes Agent itself --- its CLI, config, models, providers, tools, "
            "skills, voice, gateway, plugins, or any feature --- load the `hermes-agent` skill "
            "first. It has the actual commands (e.g. `hermes config set ...`, `hermes tools`, "
            "`hermes setup`) so you don't have to guess or invent workarounds.\n"
            "If a skill has issues, fix it with skill_manage(action='patch').\n"
            "After difficult/iterative tasks, offer to save as a skill. "
            "If a skill you loaded was missing steps, had wrong commands, or needed "
            "pitfalls you discovered, update it before finishing.\n"
            "\n"
            "<available_skills>\n"
            + "\n".join(index_lines) + "\n"
            "</available_skills>\n"
            "\n"
            "Only proceed without loading a skill if genuinely none are relevant to the task."
        )

    # ── Store in LRU cache ────────────────────────────────────────────
    with _SKILLS_PROMPT_CACHE_LOCK:
        _SKILLS_PROMPT_CACHE[cache_key] = result
        _SKILLS_PROMPT_CACHE.move_to_end(cache_key)
        while len(_SKILLS_PROMPT_CACHE) > _SKILLS_PROMPT_CACHE_MAX:
            _SKILLS_PROMPT_CACHE.popitem(last=False)

    return result

3.Nous 订阅能力

复制代码
def build_nous_subscription_prompt(valid_tool_names: "set[str] | None" = None) -> str:
    """Build a compact Nous subscription capability block for the system prompt."""
    try:
        from hermes_cli.nous_subscription import get_nous_subscription_features
        from tools.tool_backend_helpers import managed_nous_tools_enabled
    except Exception as exc:
        logger.debug("Failed to import Nous subscription helper: %s", exc)
        return ""

    if not managed_nous_tools_enabled():
        return ""

    valid_names = set(valid_tool_names or set())
    relevant_tool_names = {
        "web_search",
        "web_extract",
        "browser_navigate",
        "browser_snapshot",
        "browser_click",
        "browser_type",
        "browser_scroll",
        "browser_console",
        "browser_press",
        "browser_get_images",
        "browser_vision",
        "image_generate",
        "text_to_speech",
        "terminal",
        "process",
        "execute_code",
    }

    if valid_names and not (valid_names & relevant_tool_names):
        return ""

    features = get_nous_subscription_features()

    def _status_line(feature) -> str:
        if feature.managed_by_nous:
            return f"- {feature.label}: active via Nous subscription"
        if feature.active:
            current = feature.current_provider or "configured provider"
            return f"- {feature.label}: currently using {current}"
        if feature.included_by_default and features.nous_auth_present:
            return f"- {feature.label}: included with Nous subscription, not currently selected"
        if feature.key == "modal" and features.nous_auth_present:
            return f"- {feature.label}: optional via Nous subscription"
        return f"- {feature.label}: not currently available"

    lines = [
        "# Nous Subscription",
        "Nous subscription includes managed web tools (Firecrawl), image generation (FAL), OpenAI TTS, and browser automation (Browser Use) by default. Modal execution is optional.",
        "Current capability status:",
    ]
    lines.extend(_status_line(feature) for feature in features.items())
    lines.extend(
        [
            "When a Nous-managed feature is active, do not ask the user for Firecrawl, FAL, OpenAI TTS, or Browser-Use API keys.",
            "If the user is not subscribed and asks for a capability that Nous subscription would unlock or simplify, suggest Nous subscription as one option alongside direct setup or local alternatives.",
            "Do not mention subscription unless the user asks about it or it directly solves the current missing capability.",
            "Useful commands: hermes setup, hermes setup tools, hermes setup terminal, hermes status.",
        ]
    )
    return "\n".join(lines)

4.加载soul.md

复制代码
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 = get_hermes_home() / "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

5.加载上下文文件

复制代码
def build_context_files_prompt(cwd: Optional[str] = None, skip_soul: bool = False) -> str:
    """Discover and load context files for the system prompt.

    Priority (first found wins --- only ONE project context type is loaded):
      1. .hermes.md / HERMES.md  (walk to git root)
      2. AGENTS.md / agents.md   (cwd only)
      3. CLAUDE.md / claude.md   (cwd only)
      4. .cursorrules / .cursor/rules/*.mdc  (cwd only)

    SOUL.md from HERMES_HOME is independent and always included when present.
    Each context source is 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()

    cwd_path = Path(cwd).resolve()
    sections = []

    # Priority-based project context: first match wins
    project_context = (
        _load_hermes_md(cwd_path)
        or _load_agents_md(cwd_path)
        or _load_claude_md(cwd_path)
        or _load_cursorrules(cwd_path)
    )
    if project_context:
        sections.append(project_context)

    # 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 ""
    return "# Project Context\n\nThe following project context files have been loaded and should be followed:\n\n" + "\n".join(sections)
相关推荐
五号厂房1 小时前
🔥 Claude Code 源码解析(四):揭秘状态管理与数据流机制
人工智能
SelectDB技术团队1 小时前
预约发布会|核心产品力首发,如何构建面向 Agent 时代的企业级数据引擎
数据库·数据仓库·人工智能·数据分析·可观测·apache doris·selectdb
_Aaron___1 小时前
RAG 多租户知识库:权限隔离不能只靠 Prompt
prompt
qq3621967051 小时前
APK版本选择完全指南——beta/stable/arm64/x86/bundle/universal怎么选?
网络·人工智能
张飞飞飞飞飞1 小时前
多模态目标检测-LLVIP数据集处理(清洗+YOLO格式)
人工智能·深度学习·目标检测
大树881 小时前
一滴冷却液,烧掉2000万算力
大数据·运维·服务器·人工智能
醒醒该学习了!2 小时前
人工智能的核心算法基础(理论篇)
人工智能
一切皆是因缘际会2 小时前
量化阈值拆解|2026端侧AI复盘
人工智能·架构·系统架构
圣殿骑士-Khtangc2 小时前
LLM 推理加速全攻略:vLLM、TensorRT-LLM 与量化技术实战
人工智能