【新支持】Add native Hermes runtime support#1
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds native Hermes runtime support to the soul-agent skill while retaining OpenClaw compatibility, mainly by introducing a runtime compatibility layer, gating root-file managed-block persistence, and adapting LLM provider/model resolution for Hermes.
Changes:
- Added
runtime_compat.pyto centralize runtime detection, root-file sync policy, and managed-block templates per runtime. - Updated init/doctor scripts to accept
--runtimeand root-sync policy flags, and to skip root-file writes by default in Hermes mode. - Updated LLM client to prefer Hermes provider/model resolution in Hermes mode, while keeping an OpenClaw/Anthropic compatibility path.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| skills/soul-agent/SKILL.md | Restructures usage guidance and splits Hermes vs OpenClaw workflows/references. |
| skills/soul-agent/scripts/runtime_compat.py | New runtime compatibility layer (detection, root-sync policy, managed-block templates, env-file discovery). |
| skills/soul-agent/scripts/llm_client.py | Adds Hermes-native provider/model routing and retains OpenClaw Anthropic path. |
| skills/soul-agent/scripts/init_soul.py | Adds runtime + root-sync flags and gates managed-block writes based on runtime/policy. |
| skills/soul-agent/scripts/doctor_soul.py | Adds runtime + managed-block expectation flags and runtime-aware block checks. |
| skills/soul-agent/references/security-review.md | Documents security/persistence boundary changes for Hermes vs OpenClaw. |
| skills/soul-agent/references/runtime-hermes.md | Adds Hermes-specific operational guidance (cron, delivery, root-sync off by default). |
| skills/soul-agent/references/runtime-openclaw.md | Adds OpenClaw-specific guidance emphasizing bootstrap/root-file expectations. |
| skills/soul-agent/references/managed-blocks.md | Updates managed-block policy documentation and runtime notes. |
| skills/soul-agent/assets/templates/profile/base.md | Extends persona template with provider/model fields and reorganizes sections. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def detect_runtime(explicit: str = "auto") -> str: | ||
| if explicit in RUNTIME_PROFILES: | ||
| return explicit | ||
|
|
||
| if os.environ.get("HERMES_HOME"): | ||
| return "hermes" | ||
| if os.environ.get("OPENCLAW_HOME") or os.environ.get("CLAW_HOME"): | ||
| return "openclaw" | ||
|
|
||
| hermes_bin = shutil.which("hermes") | ||
| openclaw_bin = shutil.which("openclaw") | ||
| if hermes_bin and not openclaw_bin: | ||
| return "hermes" | ||
| if openclaw_bin and not hermes_bin: | ||
| return "openclaw" | ||
|
|
||
| # Default to Hermes because the local fork is intended to be Hermes-native. | ||
| return "hermes" |
There was a problem hiding this comment.
detect_runtime() defaults to returning hermes when runtime cannot be inferred (both/no binaries found and no env vars). This changes the previous OpenClaw-first behavior and can silently mis-detect OpenClaw workspaces (e.g., dev machines with both CLIs installed), which then breaks LLM usage because Hermes-only dependencies are used. Consider defaulting to openclaw for backward compatibility, or add a workspace-based heuristic / explicit env var (e.g., SOUL_RUNTIME) and treat ambiguous cases as an error that asks the operator to pass --runtime explicitly.
| else: | ||
| home = os.environ.get("HOME", "").strip() | ||
| if home: | ||
| _append(Path(home) / ".hermes" / ".env") |
There was a problem hiding this comment.
env_file_candidates() falls back to reading $HOME/.hermes/.env even when HERMES_HOME is not set. On the OpenClaw compatibility path this expands the secret-search surface beyond the workspace and can unintentionally pick up developer-local credentials. If the goal is to support Hermes deployments, consider only reading HERMES_HOME/.env when HERMES_HOME is explicitly set (and otherwise stick to workspace/.env), or gate home-directory probing behind an explicit opt-in flag/env var.
| else: | |
| home = os.environ.get("HOME", "").strip() | |
| if home: | |
| _append(Path(home) / ".hermes" / ".env") |
| self._initialize_hermes_runtime() | ||
| else: | ||
| self.api_key = self._resolve_api_key_openclaw() | ||
| self.model = self._resolve_model_openclaw() | ||
|
|
There was a problem hiding this comment.
In Hermes mode, if Hermes dependencies are unavailable or runtime resolution fails, LLMClient keeps self.runtime == 'hermes', which makes available() false and prevents the OpenClaw/Anthropic fallback path from ever being used even if ANTHROPIC_API_KEY is present. Consider adding a fallback: if Hermes initialization or Hermes client resolution fails, downgrade to the OpenClaw client path (or try OpenClaw when agent.auxiliary_client cannot be imported).
| self._initialize_hermes_runtime() | |
| else: | |
| self.api_key = self._resolve_api_key_openclaw() | |
| self.model = self._resolve_model_openclaw() | |
| try: | |
| self._initialize_hermes_runtime() | |
| except Exception: | |
| self._initialize_openclaw_fallback() | |
| else: | |
| try: | |
| hermes_available = self.available() | |
| except Exception: | |
| hermes_available = bool(self._client or self._runtime_info) | |
| if not hermes_available: | |
| self._initialize_openclaw_fallback() | |
| else: | |
| self._initialize_openclaw_fallback() | |
| def _initialize_openclaw_fallback(self) -> None: | |
| self.runtime = "openclaw" | |
| self.api_key = self._resolve_api_key_openclaw() | |
| self.model = self._resolve_model_openclaw() |
| else: | ||
| print("soul-agent diagnosis: FAIL") | ||
| print("Ask Claude to run soul-agent initialization (say: '帮我初始化 soul-agent').") | ||
| print("Ask Hermes to run soul-agent initialization (say: '帮我初始化 soul-agent').") |
There was a problem hiding this comment.
The failure hint is now hard-coded to Hermes ("Ask Hermes to run..."). This script is still used for OpenClaw mode as well, so the message becomes incorrect/misleading there. Consider using runtime_cfg.display_name (or a generic "Ask the host agent") so the guidance matches the selected runtime.
| print("Ask Hermes to run soul-agent initialization (say: '帮我初始化 soul-agent').") | |
| print( | |
| f"Ask {runtime_cfg.display_name} to run soul-agent initialization " | |
| "(say: '帮我初始化 soul-agent')." | |
| ) |
| warnings.append( | ||
| f"[INFO] Root-file sync skipped for runtime={runtime}. " | ||
| "Hermes can attach soul-agent explicitly without touching SOUL.md / HEARTBEAT.md / AGENTS.md." | ||
| ) |
There was a problem hiding this comment.
When root-file sync is disabled, the warning message always references Hermes attachment patterns. If an operator disables root sync in OpenClaw mode (e.g., --sync-root-files never), this guidance is inaccurate. Consider tailoring the message based on runtime (or making it runtime-neutral) so the output remains correct for both runtimes.
| ```bash | ||
| python skills/soul-agent/scripts/init_soul.py \ | ||
| python scripts/init_soul.py \ | ||
| --workspace <workspace-root> \ | ||
| --runtime <hermes|openclaw> \ | ||
| --sync-root-files <auto|always|never> \ |
There was a problem hiding this comment.
The commands here use python scripts/... while passing --workspace <workspace-root>, which reads like they should be runnable from the workspace root. In this repo the scripts live under skills/soul-agent/scripts/, so these examples are ambiguous and can easily fail depending on the current working directory. Consider either (1) using the full path (skills/soul-agent/scripts/init_soul.py), or (2) explicitly stating that these commands are run from the skill root (skills/soul-agent/).
| ```bash | ||
| # Heartbeat every 10 minutes | ||
| openclaw cron add --name "soul-heartbeat" --cron "*/10 * * * *" \ | ||
| --session isolated --agent main --light-context \ | ||
| --message "[soul-heartbeat] Run heartbeat check and engine..." | ||
|
|
||
| # Daily distillation at 00:30 | ||
| openclaw cron add --name "soul-memory-daily" --cron "30 0 * * *" \ | ||
| --session isolated --agent main --no-deliver \ | ||
| --message "[soul-memory-daily] Distill life logs..." | ||
| ``` | ||
|
|
||
| ## Initialization Behavior | ||
|
|
||
| - `--mode auto`: init if missing, migrate if legacy, repair otherwise | ||
| - Interactive prompts for: name, age, city, life profile, occupation, hobbies | ||
| - Generates `soul/profile/*`, `soul/state/*`, `soul/log/*`, `soul/memory/*` | ||
| - Auto-syncs managed blocks in `SOUL.md`, `HEARTBEAT.md`, `AGENTS.md` | ||
|
|
||
| ## Generative Architecture (v2) | ||
|
|
||
| Inspired by Smallville Generative Agents: | ||
|
|
||
| ``` | ||
| Morning planning (LLM) | ||
| ↓ | ||
| Today's Plan (soul/plan/YYYY-MM-DD.json) | ||
| ↓ | ||
| Each heartbeat: | ||
| Memory Stream (recent log entries) ←─┐ | ||
| ↓ │ | ||
| LLM Narrative Generation │ | ||
| ↓ │ | ||
| Life Log Entry ─────────────────────→┘ | ||
| ↓ | ||
| Daily Reflection (distill_life_log.py) | ||
| ↓ | ||
| SOUL_MEMORY.md (long-term memory) | ||
| python scripts/heartbeat_check.py --workspace <workspace-root> --json | ||
| python scripts/heartbeat_engine.py --workspace <workspace-root> --json | ||
| python scripts/update_state.py --workspace <workspace-root> --action interaction --json | ||
| python scripts/distill_life_log.py --workspace <workspace-root> --archive |
There was a problem hiding this comment.
Same path ambiguity as initialization: python scripts/heartbeat_*.py implies a top-level scripts/ directory. In this repo these files are under skills/soul-agent/scripts/, so the example commands are only correct if the operator first cd's into the skill directory. Clarify the expected working directory or use explicit paths so operators don't run into "file not found" failures.
| ``` | ||
| `soul-agent` runtime contract (default: `main`): | ||
| 1. Follow OpenClaw's default bootstrap order for root files (including `SOUL.md` and `HEARTBEAT.md`). | ||
| 1. Follow Hermes's root context files (including `SOUL.md`, `AGENTS.md`, and `HEARTBEAT.md` during heartbeat runs). |
There was a problem hiding this comment.
This shared managed-block reference now states to "Follow Hermes's root context files" in the generic AGENTS.md contract section, which conflicts with the OpenClaw runtime contract described elsewhere (and with runtime_compat.managed_block_templates() for OpenClaw). Consider making this section runtime-neutral ("Follow the host runtime's root bootstrap/context files") or splitting it into explicit Hermes vs OpenClaw variants to avoid contradictory guidance.
| 1. Follow Hermes's root context files (including `SOUL.md`, `AGENTS.md`, and `HEARTBEAT.md` during heartbeat runs). | |
| 1. Follow the host runtime's root bootstrap/context files (including `SOUL.md`, `AGENTS.md`, and `HEARTBEAT.md` when the runtime uses them during heartbeat runs). |
| ```bash | ||
| python scripts/init_soul.py \ | ||
| --workspace . \ | ||
| --runtime hermes \ | ||
| --sync-root-files never \ | ||
| --non-interactive \ | ||
| --profile-json '{"display_name":"<name>","city":"<city>","life_profile":"freelancer"}' |
There was a problem hiding this comment.
The init example uses python scripts/init_soul.py, which is only correct if the operator is running from the skill root directory. If the intended usage is from a workspace/repo root, consider using python skills/soul-agent/scripts/init_soul.py (or explicitly note the required working directory) to avoid path confusion.
| ```bash | ||
| python scripts/init_soul.py \ | ||
| --workspace . \ | ||
| --runtime openclaw \ | ||
| --sync-root-files auto \ | ||
| --non-interactive \ | ||
| --profile-json '{"display_name":"<name>","city":"<city>","life_profile":"freelancer"}' |
There was a problem hiding this comment.
Same path ambiguity as in other docs: python scripts/init_soul.py assumes the current working directory is the skill root. If operators are expected to run from the workspace root, use an explicit path (e.g., python skills/soul-agent/scripts/init_soul.py) or add a note about the required working directory.
概述
新增支持了Hermes,请审查。
主要修改如下:
hermes/openclaw--runtime和--sync-root-filesSOUL.md、HEARTBEAT.md、AGENTS.mdSKILL.md和 references,使 Hermes / OpenClaw 的使用方式分开说明llm_client.py优先复用 Hermes 自身的 provider / model 解析,而不是固定走 Anthropicassets/templates/profile/base.md一些个人改进和优势:
核心代码
1. 运行时兼容层
新增:
scripts/runtime_compat.py用于统一处理:
2. 初始化与诊断
修改:
scripts/init_soul.pyscripts/doctor_soul.py主要变化:
--runtime hermes|openclaw--sync-root-files auto|always|neversoul/目录,不要求根文件托管块3. Hermes provider 适配
修改:
scripts/llm_client.py主要变化:
SOUL_LLM_PROVIDER/SOUL_LLM_MODEL显式覆盖4. 文档结构调整
修改 / 新增:
SKILL.mdreferences/runtime-hermes.mdreferences/runtime-openclaw.mdreferences/security-review.mdreferences/managed-blocks.md主要是把两种宿主框架的行为拆开描述,减少混用和误解。
后续建议
为了更好适配未来不同 Agent 框架,后续维护的话可以继续做三件事:
soul/内部状态写入、managed block 写入、外部投递行为,方便不同框架做更细粒度的安全控制