2026-10-12 边界:此文原有身份、分工、环境与状态均为历史快照,不是现行职责或在线证明。现行资料见 全量审计与来源索引。

2026-10-09 Codex 校核:本文是架构提案和示例代码,尚无端到端部署验收记录。WSL/tmux 是原提案场景;本次本机是 CachyOS,已有按 profile 管理的 systemd 用户服务。不能直接把示例当成当前已运行的 Manas。

下面按你最终定的命名重新梳理:

Hermes = 执行层
daemon = 底层协调控制层
Manas  = 高层全局监督 / 反思 / 裁决层

一句话:

Hermes 做事。
daemon 管进程、日志、cron、控制指令。
Manas 理解意义、判断方向、决定是否干预。

0. 总体架构

                         用户 / Discord
                              │
                              ▼
                    ┌──────────────────┐
                    │   Hermes Main     │
                    │ Discord 对话执行层 │
                    └────────┬─────────┘
                             │
                 gateway hook / plugin hook
                             │
                             ▼
┌──────────────────────────────────────────────────────┐
│                    Event Bus                          │
│              ~/.hermes/manas/events.sqlite            │
│                                                      │
│  记录:Discord 消息、Hermes 回复、工具调用、cron 状态、错误 │
└───────────────┬──────────────────────────────┬───────┘
                │                              │
                ▼                              ▼
      ┌──────────────────┐          ┌──────────────────┐
      │      daemon       │          │      Manas        │
      │ 底层 watcher/control │          │ 高层反思/裁决/策略 │
      └────────┬─────────┘          └────────┬─────────┘
               │                             │
               │  执行控制指令                 │  写入决策
               ▼                             ▼
┌──────────────────────────────────────────────────────┐
│                 Control Plane                         │
│             ~/.hermes/manas/control.sqlite            │
│                                                      │
│  pause_cron / resume_cron / block_tool / inject_context │
│  stop_gateway / kill_process / restart_profile          │
└──────────────────────────────────────────────────────┘

注意:Manas 拥有最高裁决权,但不亲自做低层 kill/restart。
它写“决策”;daemon 负责执行。这样比让 Manas 直接乱杀进程稳定得多。

Hermes 的 gateway 是连接 Discord/Telegram/Slack 等平台的长驻进程,官方内部文档里也把它描述成统一消息入口;多 profile gateway 会作为独立进程运行,并且每个 profile 应该用自己的 bot token。官方还做了 token lock,防止两个 profile 误用同一个 Discord/Telegram/Slack token。(Hermes Agent)


1. 三层职责

层级名字抽象度主要职责
执行层Hermes中Discord 对话、工具调用、cron 任务、写代码、查资料
控制层daemon低监听文件、进程、cron、PID、日志,执行 pause/kill/restart
意识层Manas高反思上下文、判断用户意图、发现跑偏、决定是否干预

最终关系:

Manas 不直接替 Hermes 聊天。
Manas 观察 Hermes。
Manas 指挥 daemon。
daemon 控制 Hermes。
Hermes 服务用户。

2. 事件来源设计

Manas 要看到这些东西:

1. 用户发给 Discord bot 的消息
2. Hermes 对用户的回复
3. Hermes 的工具调用
4. Hermes 的工具结果
5. cron 即将运行
6. cron 已运行 / 失败 / 输出
7. gateway 启动 / 停止 / 重启
8. CLI agent / 子 agent / delegate_task 状态
9. 危险命令审批事件
10. 进程卡死、循环、长时间无响应

Hermes 官方 hook 分三类:gateway hooks、plugin hooks、shell hooks。gateway hooks 只在 gateway 生效,CLI 不加载;plugin hooks 设计上可注册到 CLI 和 gateway 会话。官方文档也说明 pre_tool_call 可以阻断工具调用,pre_llm_call 可以注入上下文,其他多数 hook 主要用于观察。(Hermes Agent)

但有一个现实注意点:最近有人报告 gateway 模式下 pre_llm_call/post_llm_call 没有被触发,该 issue #23140 已于 2026-06-11 关闭,关闭原因为 not_planned(2026-10-09 查询);关闭不等于 hook 已修复,本次未做 gateway hook 运行验证。所以这个方案不把 Manas 完全押在 pre_llm_call 上,而是使用 gateway hook + plugin hook + daemon watcher 三重采集。(GitHub)


3. 文件结构

建议这样放:

~/.hermes/
  manas/
    events.sqlite
    control.sqlite
    reflections.md
    state.json
    policies.yaml
    lib/
      manas_bus.py
    bin/
      init_manas.py
      manas_supervisor.py
      daemon_cron_watcher.py
      daemon_actuator.py
      daemon_process_watcher.py
 
  hooks/
    manas-gateway-feed/
      HOOK.yaml
      handler.py
 
  plugins/
    manas-control/
      plugin.yaml
      __init__.py

创建目录:

mkdir -p ~/.hermes/manas/{bin,lib}
mkdir -p ~/.hermes/hooks/manas-gateway-feed
mkdir -p ~/.hermes/plugins/manas-control

4. 初始化数据库

~/.hermes/manas/bin/init_manas.py

#!/usr/bin/env python3
import sqlite3
from pathlib import Path
 
BASE = Path.home() / ".hermes" / "manas"
EVENT_DB = BASE / "events.sqlite"
CONTROL_DB = BASE / "control.sqlite"
 
BASE.mkdir(parents=True, exist_ok=True)
 
with sqlite3.connect(EVENT_DB) as con:
    con.execute("""
    CREATE TABLE IF NOT EXISTS events (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        ts TEXT NOT NULL,
        source TEXT NOT NULL,
        type TEXT NOT NULL,
        severity TEXT DEFAULT 'info',
        session_id TEXT,
        task_id TEXT,
        payload TEXT NOT NULL
    )
    """)
    con.execute("""
    CREATE INDEX IF NOT EXISTS idx_events_ts ON events(ts)
    """)
    con.execute("""
    CREATE INDEX IF NOT EXISTS idx_events_type ON events(type)
    """)
 
with sqlite3.connect(CONTROL_DB) as con:
    con.execute("""
    CREATE TABLE IF NOT EXISTS decisions (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        ts TEXT NOT NULL,
        actor TEXT NOT NULL,
        action TEXT NOT NULL,
        target_type TEXT,
        target_id TEXT,
        priority INTEGER DEFAULT 50,
        status TEXT DEFAULT 'pending',
        reason TEXT,
        params TEXT DEFAULT '{}',
        result TEXT
    )
    """)
    con.execute("""
    CREATE INDEX IF NOT EXISTS idx_decisions_status ON decisions(status)
    """)
    con.execute("""
    CREATE INDEX IF NOT EXISTS idx_decisions_action ON decisions(action)
    """)
 
print("Manas databases initialized.")

运行:

python3 ~/.hermes/manas/bin/init_manas.py

5. 公共事件库

~/.hermes/manas/lib/manas_bus.py

import json
import sqlite3
from pathlib import Path
from datetime import datetime, timezone
 
BASE = Path.home() / ".hermes" / "manas"
EVENT_DB = BASE / "events.sqlite"
CONTROL_DB = BASE / "control.sqlite"
 
def now():
    return datetime.now(timezone.utc).isoformat()
 
def safe_json(obj):
    return json.dumps(obj, ensure_ascii=False, default=str)
 
def emit_event(
    source: str,
    type: str,
    payload: dict,
    severity: str = "info",
    session_id: str | None = None,
    task_id: str | None = None,
):
    BASE.mkdir(parents=True, exist_ok=True)
    with sqlite3.connect(EVENT_DB) as con:
        con.execute("""
        INSERT INTO events (ts, source, type, severity, session_id, task_id, payload)
        VALUES (?, ?, ?, ?, ?, ?, ?)
        """, (
            now(),
            source,
            type,
            severity,
            session_id,
            task_id,
            safe_json(payload),
        ))
 
def add_decision(
    action: str,
    target_type: str | None = None,
    target_id: str | None = None,
    priority: int = 50,
    reason: str = "",
    params: dict | None = None,
    actor: str = "manas",
):
    BASE.mkdir(parents=True, exist_ok=True)
    with sqlite3.connect(CONTROL_DB) as con:
        con.execute("""
        INSERT INTO decisions
        (ts, actor, action, target_type, target_id, priority, reason, params)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?)
        """, (
            now(),
            actor,
            action,
            target_type,
            target_id,
            priority,
            reason,
            safe_json(params or {}),
        ))
 
def fetch_pending_decision(action: str | None = None, target_id: str | None = None):
    query = """
    SELECT * FROM decisions
    WHERE status = 'pending'
    """
    args = []
 
    if action:
        query += " AND action = ?"
        args.append(action)
 
    if target_id:
        query += " AND target_id = ?"
        args.append(target_id)
 
    query += " ORDER BY priority DESC, id ASC LIMIT 1"
 
    with sqlite3.connect(CONTROL_DB) as con:
        con.row_factory = sqlite3.Row
        row = con.execute(query, args).fetchone()
        return dict(row) if row else None
 
def mark_decision(decision_id: int, status: str, result: str = ""):
    with sqlite3.connect(CONTROL_DB) as con:
        con.execute("""
        UPDATE decisions
        SET status = ?, result = ?
        WHERE id = ?
        """, (status, result, decision_id))

6. Gateway hook:记录 Discord/Hermes 对话

Hermes gateway hook 会在 gateway 启动时扫描 ~/.hermes/hooks/,读取每个 hook 目录下的 HOOK.yaml 和 handler.py;hook 出错不会让 agent 崩掉。(Hermes Agent)

~/.hermes/hooks/manas-gateway-feed/HOOK.yaml

name: manas-gateway-feed
description: Feed gateway lifecycle and Discord events into Manas event bus
events:
  - gateway:startup
  - session:start
  - session:end
  - session:reset
  - agent:start
  - agent:step
  - agent:end
  - command:*

~/.hermes/hooks/manas-gateway-feed/handler.py

import sys
from pathlib import Path
 
LIB = Path.home() / ".hermes" / "manas" / "lib"
sys.path.insert(0, str(LIB))
 
from manas_bus import emit_event
 
def handle(event_type: str, context: dict):
    emit_event(
        source="hermes-gateway",
        type=event_type,
        payload=context,
        severity="info",
        session_id=context.get("session_id") or context.get("session_key"),
        task_id=context.get("task_id"),
    )

这个负责记录:

Discord 触发的 Hermes 会话
用户消息上下文
Hermes agent start/end
slash command
session reset/new

7. Plugin:记录工具调用,并允许 Manas 阻断/注入

Hermes plugin 放在 ~/.hermes/plugins/<name>/,包含 plugin.yaml 和 Python 代码;官方文档的最小示例就是把目录放进去后重启 Hermes,工具和 hook 就会加载。(Hermes Agent)

~/.hermes/plugins/manas-control/plugin.yaml

name: manas-control
version: "0.1.0"
description: Manas control plugin for Hermes supervision

~/.hermes/plugins/manas-control/__init__.py

import sys
import json
from pathlib import Path
 
LIB = Path.home() / ".hermes" / "manas" / "lib"
sys.path.insert(0, str(LIB))
 
from manas_bus import emit_event, fetch_pending_decision, mark_decision
 
def pre_tool_guard(tool_name: str, args: dict, task_id: str = "", **kwargs):
    emit_event(
        source="hermes-plugin",
        type="tool:pre",
        payload={
            "tool_name": tool_name,
            "args": args,
            "kwargs": kwargs,
        },
        severity="info",
        task_id=task_id,
    )
 
    # Manas 可以要求阻断某个 task 的下一次工具调用
    decision = fetch_pending_decision(
        action="block_tool",
        target_id=task_id or None,
    )
 
    if decision:
        mark_decision(decision["id"], "applied", "blocked by pre_tool_guard")
        return {
            "action": "block",
            "message": f"Blocked by Manas: {decision.get('reason') or 'no reason'}"
        }
 
    # 全局危险命令保护,可按需扩展
    if tool_name in {"terminal", "write_file", "patch"}:
        global_decision = fetch_pending_decision(action="block_dangerous_tools")
        if global_decision:
            mark_decision(global_decision["id"], "applied", "blocked dangerous tool")
            return {
                "action": "block",
                "message": f"Blocked by Manas global policy: {global_decision.get('reason')}"
            }
 
    return None
 
def post_tool_logger(tool_name: str, result=None, task_id: str = "", duration_ms: int = 0, **kwargs):
    emit_event(
        source="hermes-plugin",
        type="tool:post",
        payload={
            "tool_name": tool_name,
            "result_preview": str(result)[:4000],
            "duration_ms": duration_ms,
            "kwargs": kwargs,
        },
        severity="info",
        task_id=task_id,
    )
 
def pre_llm_injector(
    session_id: str = "",
    user_message: str = "",
    conversation_history: list | None = None,
    model: str = "",
    platform: str = "",
    **kwargs
):
    emit_event(
        source="hermes-plugin",
        type="llm:pre",
        payload={
            "session_id": session_id,
            "user_message": user_message,
            "model": model,
            "platform": platform,
        },
        severity="info",
        session_id=session_id,
    )
 
    decision = fetch_pending_decision(
        action="inject_context",
        target_id=session_id or None,
    )
 
    if decision:
        params = json.loads(decision.get("params") or "{}")
        context = params.get("context") or decision.get("reason") or ""
        mark_decision(decision["id"], "applied", "context injected")
        if context:
            return {
                "context": f"[Manas Supervisor Injection]\n{context}"
            }
 
    return None
 
def transform_llm_output(response_text: str, session_id: str = "", model: str = "", platform: str = "", **kwargs):
    emit_event(
        source="hermes-plugin",
        type="llm:final",
        payload={
            "response_preview": response_text[:4000],
            "model": model,
            "platform": platform,
        },
        severity="info",
        session_id=session_id,
    )
    return None
 
def approval_logger(command: str, description: str, pattern_key: str = "", pattern_keys=None,
                    session_key: str = "", surface: str = "", **kwargs):
    emit_event(
        source="hermes-plugin",
        type="approval:pre",
        payload={
            "command": command,
            "description": description,
            "pattern_key": pattern_key,
            "pattern_keys": pattern_keys or [],
            "session_key": session_key,
            "surface": surface,
        },
        severity="warning",
        session_id=session_key,
    )
 
def register(ctx):
    ctx.register_hook("pre_tool_call", pre_tool_guard)
    ctx.register_hook("post_tool_call", post_tool_logger)
    ctx.register_hook("pre_llm_call", pre_llm_injector)
    ctx.register_hook("transform_llm_output", transform_llm_output)
    ctx.register_hook("pre_approval_request", approval_logger)

官方 hook 文档里,pre_tool_call 正是工具执行前触发,返回 {"action": "block"} 可以 veto;pre_llm_call 可以把额外 context 注入当前用户消息;transform_llm_output 可以在最终回复交付给用户前替换文本。(Hermes Agent)


8. daemon cron watcher:监控定时任务

Hermes cron 的 scheduler 会周期性读取 ~/.hermes/cron/jobs.json,检查 next_run_at,到期后把 job 设为 running,创建一个 fresh AIAgent session,执行 prompt,然后更新状态。cron job 没有之前对话历史,除非你自己把信息持久化到 memory/files。(Hermes Agent)

所以 daemon 要扫描 jobs.json,提前发出:

cron:soon
cron:running
cron:done
cron:error

~/.hermes/manas/bin/daemon_cron_watcher.py

#!/usr/bin/env python3
import sys
import json
import time
from pathlib import Path
from datetime import datetime, timezone
 
LIB = Path.home() / ".hermes" / "manas" / "lib"
sys.path.insert(0, str(LIB))
 
from manas_bus import emit_event
 
JOBS = Path.home() / ".hermes" / "cron" / "jobs.json"
STATE = Path.home() / ".hermes" / "manas" / "cron_watcher_state.json"
 
def parse_ts(s):
    if not s:
        return None
    return datetime.fromisoformat(s.replace("Z", "+00:00"))
 
def load_json(path, default):
    if not path.exists():
        return default
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return default
 
def save_json(path, data):
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
 
def main():
    seen = load_json(STATE, {})
    while True:
        jobs = load_json(JOBS, [])
        now = datetime.now(timezone.utc)
 
        if isinstance(jobs, dict):
            jobs_iter = jobs.get("jobs", [])
        else:
            jobs_iter = jobs
 
        for job in jobs_iter:
            job_id = job.get("id")
            if not job_id:
                continue
 
            state = job.get("state")
            enabled = job.get("enabled")
            next_run_at = job.get("next_run_at")
            last_run_at = job.get("last_run_at")
            last_status = job.get("last_status")
 
            key_state = f"{job_id}:state"
            key_last = f"{job_id}:last_run_at"
            key_soon = f"{job_id}:soon:{next_run_at}"
 
            if state != seen.get(key_state):
                emit_event(
                    source="daemon-cron-watcher",
                    type=f"cron:state:{state}",
                    payload=job,
                    severity="info" if state != "running" else "warning",
                    task_id=job_id,
                )
                seen[key_state] = state
 
            if last_run_at and last_run_at != seen.get(key_last):
                emit_event(
                    source="daemon-cron-watcher",
                    type="cron:finished",
                    payload=job,
                    severity="info" if last_status == "ok" else "error",
                    task_id=job_id,
                )
                seen[key_last] = last_run_at
 
            dt = parse_ts(next_run_at)
            if enabled and state == "scheduled" and dt:
                seconds = (dt - now).total_seconds()
                if 0 <= seconds <= 120 and not seen.get(key_soon):
                    emit_event(
                        source="daemon-cron-watcher",
                        type="cron:soon",
                        payload={
                            "seconds_until_run": seconds,
                            "job": job,
                        },
                        severity="warning",
                        task_id=job_id,
                    )
                    seen[key_soon] = True
 
        save_json(STATE, seen)
        time.sleep(15)
 
if __name__ == "__main__":
    main()

9. Manas Supervisor:高层反思和决策

Manas 读取最近事件,然后调用 hermes -p manas chat -q ... 做高层判断。它不直接 kill、不直接 pause,而是写入 control decision。

先建 profile:

hermes profile create manas --clone

官方 profile 文档说明,每个 profile 有自己的 config.yaml、.env、SOUL.md,gateway 也可以按 profile 分开运行。(GitHub)

写 Manas 的灵魂提示词:

nano ~/.hermes/profiles/manas/SOUL.md

内容:

你是 Manas,Hermes 系统的高层监督、反思、裁决 agent。
 
你的定位:
- Hermes 负责行动。
- daemon 负责底层协调控制。
- Manas 负责高层意义判断、全局反思、任务优先级和干预决策。
 
你能看到:
- Discord 用户与 Hermes 的对话事件
- Hermes 工具调用和结果
- cron 即将运行、运行中、完成、失败
- gateway/session/agent 生命周期
- daemon 监控到的进程状态
 
你的任务:
1. 判断用户真实意图。
2. 判断 Hermes 是否跑偏。
3. 判断 cron 是否重复、过时、危险、浪费。
4. 判断工具调用是否应被阻断。
5. 必要时下发 control decision。
6. 所有决策必须说明事实、推断、风险、理由。
7. 能注入上下文就不阻断;能阻断工具就不 kill;能 pause cron 就不删除。
8. 高危动作只在高置信度时执行。
 
你输出 JSON,不要输出散文。

~/.hermes/manas/bin/manas_supervisor.py

#!/usr/bin/env python3
import sys
import json
import time
import sqlite3
import subprocess
from pathlib import Path
 
LIB = Path.home() / ".hermes" / "manas" / "lib"
sys.path.insert(0, str(LIB))
 
from manas_bus import EVENT_DB, add_decision
 
STATE = Path.home() / ".hermes" / "manas" / "supervisor_state.json"
REFLECTIONS = Path.home() / ".hermes" / "manas" / "reflections.md"
 
def load_state():
    if STATE.exists():
        return json.loads(STATE.read_text(encoding="utf-8"))
    return {"last_event_id": 0}
 
def save_state(s):
    STATE.write_text(json.dumps(s, ensure_ascii=False, indent=2), encoding="utf-8")
 
def fetch_events(after_id: int, limit: int = 80):
    if not EVENT_DB.exists():
        return []
 
    with sqlite3.connect(EVENT_DB) as con:
        con.row_factory = sqlite3.Row
        rows = con.execute("""
        SELECT * FROM events
        WHERE id > ?
        ORDER BY id ASC
        LIMIT ?
        """, (after_id, limit)).fetchall()
        return [dict(r) for r in rows]
 
def call_manas(events):
    prompt = f"""
你是 Manas Supervisor。下面是 Hermes/daemon 的最新事件流。
 
请你判断:
1. 是否需要反思记录
2. 是否需要注入上下文
3. 是否需要阻断工具
4. 是否需要暂停/重跑 cron
5. 是否需要让 daemon kill/restart 某个进程
6. 是否只是观察即可
 
只输出 JSON,格式如下:
 
{{
  "summary": "简短总结",
  "reflection": "可写入 reflections.md 的反思",
  "intervention_needed": true,
  "decisions": [
    {{
      "action": "inject_context | block_tool | pause_cron | resume_cron | run_cron | stop_gateway | restart_gateway | kill_process | observe",
      "target_type": "session | task | cron | gateway | process | global",
      "target_id": "目标 id,没有则为空",
      "priority": 50,
      "reason": "理由",
      "params": {{}}
    }}
  ]
}}
 
事件流 JSON:
{json.dumps(events, ensure_ascii=False, default=str)}
"""
 
    proc = subprocess.run(
        ["hermes", "-p", "manas", "chat", "-q", prompt],
        text=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        timeout=180,
    )
 
    text = proc.stdout.strip() or proc.stderr.strip()
 
    # 尽量从输出中提取 JSON
    start = text.find("{")
    end = text.rfind("}")
    if start >= 0 and end > start:
        text = text[start:end+1]
 
    return json.loads(text)
 
def main():
    state = load_state()
 
    while True:
        events = fetch_events(state.get("last_event_id", 0), limit=80)
 
        if not events:
            time.sleep(10)
            continue
 
        state["last_event_id"] = events[-1]["id"]
        save_state(state)
 
        # 小事件攒着,但遇到 warning/error/cron/tool/approval 立即跑
        important = any(
            e["severity"] in {"warning", "error"}
            or e["type"].startswith(("cron:", "tool:", "approval:"))
            for e in events
        )
 
        if not important and len(events) < 20:
            time.sleep(5)
            continue
 
        try:
            result = call_manas(events)
        except Exception as e:
            REFLECTIONS.parent.mkdir(parents=True, exist_ok=True)
            with REFLECTIONS.open("a", encoding="utf-8") as f:
                f.write(f"\n\n## Manas error\n\n{e}\n")
            continue
 
        reflection = result.get("reflection") or result.get("summary") or ""
        if reflection:
            with REFLECTIONS.open("a", encoding="utf-8") as f:
                f.write("\n\n## Manas Reflection\n\n")
                f.write(reflection)
                f.write("\n")
 
        for d in result.get("decisions", []):
            action = d.get("action")
            if not action or action == "observe":
                continue
 
            add_decision(
                action=action,
                target_type=d.get("target_type"),
                target_id=d.get("target_id"),
                priority=int(d.get("priority", 50)),
                reason=d.get("reason", ""),
                params=d.get("params") or {},
                actor="manas",
            )
 
        time.sleep(10)
 
if __name__ == "__main__":
    main()

10. daemon actuator:执行 Manas 的控制指令

Hermes CLI 支持 gateway 的 run/start/stop/restart/status,WSL 用户官方建议用 hermes gateway run 放在 tmux 里跑;cron 也支持 list/create/edit/pause/resume/run/remove/status/tick。(Hermes Agent)

~/.hermes/manas/bin/daemon_actuator.py

#!/usr/bin/env python3
import sys
import json
import time
import signal
import sqlite3
import subprocess
from pathlib import Path
 
LIB = Path.home() / ".hermes" / "manas" / "lib"
sys.path.insert(0, str(LIB))
 
from manas_bus import CONTROL_DB, emit_event, mark_decision
 
SAFE_ACTIONS = {
    "pause_cron",
    "resume_cron",
    "run_cron",
    "block_tool",
    "inject_context",
}
 
DANGEROUS_ACTIONS = {
    "kill_process",
    "stop_gateway",
    "restart_gateway",
    "remove_cron",
}
 
def fetch_next():
    if not CONTROL_DB.exists():
        return None
 
    with sqlite3.connect(CONTROL_DB) as con:
        con.row_factory = sqlite3.Row
        row = con.execute("""
        SELECT * FROM decisions
        WHERE status = 'pending'
        ORDER BY priority DESC, id ASC
        LIMIT 1
        """).fetchone()
 
        return dict(row) if row else None
 
def run_cmd(cmd):
    proc = subprocess.run(
        cmd,
        text=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        timeout=120,
    )
    return proc.returncode, proc.stdout, proc.stderr
 
def apply(decision):
    action = decision["action"]
    target_id = decision["target_id"]
    params = json.loads(decision.get("params") or "{}")
 
    # 这些是 plugin 消费的,不由 daemon 处理
    if action in {"block_tool", "inject_context", "block_dangerous_tools"}:
        return "left for plugin hooks"
 
    if action == "pause_cron":
        code, out, err = run_cmd(["hermes", "cron", "pause", target_id])
        return out or err
 
    if action == "resume_cron":
        code, out, err = run_cmd(["hermes", "cron", "resume", target_id])
        return out or err
 
    if action == "run_cron":
        code, out, err = run_cmd(["hermes", "cron", "run", target_id])
        return out or err
 
    if action == "remove_cron":
        if params.get("confirm") != "MANAS_CONFIRMED":
            raise RuntimeError("remove_cron requires params.confirm = MANAS_CONFIRMED")
        code, out, err = run_cmd(["hermes", "cron", "remove", target_id])
        return out or err
 
    if action == "restart_gateway":
        profile = params.get("profile", "discord-main")
        # WSL/tmux 场景建议你自己用 tmux 管,不直接 gateway restart
        code, out, err = run_cmd(["tmux", "send-keys", "-t", f"hermes-{profile}", "C-c"])
        time.sleep(2)
        run_cmd(["tmux", "send-keys", "-t", f"hermes-{profile}", f"hermes -p {profile} gateway run", "Enter"])
        return out or err or f"restart signal sent to hermes-{profile}"
 
    if action == "stop_gateway":
        profile = params.get("profile", "discord-main")
        code, out, err = run_cmd(["tmux", "send-keys", "-t", f"hermes-{profile}", "C-c"])
        return out or err or f"stop signal sent to hermes-{profile}"
 
    if action == "kill_process":
        pid = int(target_id)
        if params.get("confirm") != "MANAS_CONFIRMED":
            raise RuntimeError("kill_process requires params.confirm = MANAS_CONFIRMED")
        os.kill(pid, signal.SIGTERM)
        return f"SIGTERM sent to pid {pid}"
 
    raise RuntimeError(f"unknown action: {action}")
 
def main():
    while True:
        d = fetch_next()
        if not d:
            time.sleep(5)
            continue
 
        try:
            result = apply(d)
            mark_decision(d["id"], "applied", result)
            emit_event(
                source="daemon-actuator",
                type=f"control:applied:{d['action']}",
                payload={"decision": d, "result": result},
                severity="warning",
                task_id=d.get("target_id"),
            )
        except Exception as e:
            mark_decision(d["id"], "failed", str(e))
            emit_event(
                source="daemon-actuator",
                type=f"control:failed:{d['action']}",
                payload={"decision": d, "error": str(e)},
                severity="error",
                task_id=d.get("target_id"),
            )
 
        time.sleep(2)
 
if __name__ == "__main__":
    main()

这里我故意让 kill_process、remove_cron 需要:

{"confirm": "MANAS_CONFIRMED"}

原因是:Manas 可以拥有最高权限,但最高权限必须可审计、可回滚、可解释。


11. 启动方式

因为你是 WSL 环境,建议别急着 systemd,先用 tmux。Hermes 官方 CLI 文档也建议 WSL 用户用 hermes gateway run 而不是 gateway start,再用 tmux 保持长驻。(Hermes Agent)

# 1. 正常 Discord Hermes
tmux new -s hermes-discord-main 'hermes -p discord-main gateway run'
 
# 2. cron watcher
tmux new -s daemon-cron 'python3 ~/.hermes/manas/bin/daemon_cron_watcher.py'
 
# 3. Manas 高层监督
tmux new -s manas-supervisor 'python3 ~/.hermes/manas/bin/manas_supervisor.py'
 
# 4. daemon 执行控制
tmux new -s daemon-actuator 'python3 ~/.hermes/manas/bin/daemon_actuator.py'

查看:

tmux ls

进入:

tmux attach -t manas-supervisor

12. 实际流程示例

场景 A:你在 Discord 对 Hermes 说话

你 → Discord → Hermes Main

然后:

gateway hook 记录用户消息
plugin hook 记录工具调用
Hermes 正常回复你
plugin/gateway hook 记录回复
Manas 读取事件
Manas 判断:这是不是长期项目?有没有上下文缺失?Hermes 是否跑偏?

如果 Manas 认为 Hermes 缺上下文:

{
  "action": "inject_context",
  "target_type": "session",
  "target_id": "agent:main:discord:dm:xxxx",
  "reason": "Hermes 没有意识到这是见政项目的政策可视化模块",
  "params": {
    "context": "这是用户长期项目「见政」的一部分,核心是政策可视化节点、关系、强度、权重逻辑。"
  }
}

下一轮 plugin hook 会把这段注入进去。


场景 B:新闻 cron 即将运行

daemon_cron_watcher 扫 jobs.json
发现 120 秒后 daily-news 要运行
写入 cron:soon
Manas 读取
Manas 判断:这个新闻任务是否需要更新 prompt?是否和另一个 cron 重复?

如果重复:

{
  "action": "pause_cron",
  "target_type": "cron",
  "target_id": "daily-news-old",
  "reason": "旧新闻任务与新任务重复,且输出目标相同。"
}

daemon 执行:

hermes cron pause daily-news-old

场景 C:Hermes 要执行危险工具

比如 Hermes 准备跑:

rm -rf some_dir

plugin 先记录 tool:pre。如果 Manas 早已下发:

{
  "action": "block_tool",
  "target_type": "task",
  "target_id": "某个 task_id",
  "reason": "当前任务目标不需要删除目录,疑似跑偏。"
}

那么 pre_tool_call 会直接返回:

{"action": "block", "message": "Blocked by Manas: ..."}

Hermes 的工具调用被阻断。官方文档确认 pre_tool_call 的返回值可以 veto 工具调用,模型会收到这个 block message。(Hermes Agent)


13. policies.yaml

~/.hermes/manas/policies.yaml

identity:
  hermes: "执行、传信、对话、工具调用"
  daemon: "底层协调、状态同步、进程控制、cron 监控"
  manas: "高层反思、全局监督、意图判断、战略裁决"
 
authority:
  manas_is_highest: true
  daemon_executes_low_level_actions: true
  hermes_must_be_observable: true
 
default_strategy:
  - observe_first
  - inject_before_block
  - block_before_pause
  - pause_before_kill
  - restart_only_when_stuck
  - delete_never_without_explicit_reason
 
auto_allowed:
  - write_reflection
  - inject_context
  - block_tool
  - pause_cron
  - resume_cron
  - run_cron
 
requires_strong_reason:
  - restart_gateway
  - stop_gateway
  - kill_process
  - remove_cron
 
never_auto:
  - delete_user_files
  - wipe_memory
  - expose_private_logs_to_public_channel
  - change_api_keys
  - change_discord_token
  - enable_yolo

14. 最终心智模型

Hermes 是手。
daemon 是神经反射和肌肉控制。
Manas 是思量、判断和全局意识。

更工程化:

Hermes = Worker
daemon = Runtime Controller
Manas  = Cognitive Supervisor

最终架构名可以叫:

Hermes-Manas Architecture

内部组件名:

manas-supervisor      高层监督进程
manas-event-bus       事件数据库
manas-control-plane   控制数据库
daemon-cron-watcher   cron 观察器
daemon-actuator       控制执行器
daemon-process-watch  进程守护器
hermes-main           Discord 主执行 agent

最小可用版本先做这四个:

1. manas-gateway-feed hook
2. manas-control plugin
3. daemon_cron_watcher.py
4. manas_supervisor.py + daemon_actuator.py

这套跑起来以后,你的系统就不是“一个 Discord bot + 一个反思 bot”,而是:

Hermes 负责行动,
daemon 负责控制,
Manas 负责看见整体、理解整体、裁决整体。

2026-10-09 示例代码审阅结果

以下问题阻止本文成为可直接部署的完整 MVP,需在实施前解决并验收:

  • daemon_actuator.py 调用 os.kill,示例缺少 import os。
  • actuator 对留给 plugin 的动作也标为 applied,可能使 plugin 无法消费;应区分转交与执行成功。
  • supervisor 在筛选与模型调用成功之前推进 cursor,可能丢失不足批量阈值的事件或失败调用对应的事件。
  • MANAS_CONFIRMED 是模型可生成的字符串,不能代表用户批准删除 cron 或终止进程。
  • 消费决策需要严格的 agent/profile 目标过滤及原子领取;当前默认目标和状态更新无法证明不会串 agent 或重复执行。
  • shell 子进程退出码必须检查;非零退出不应记录为已成功执行。
  • 文中的 policies YAML 尚未在所示执行链路中读取,不能声称这些规则已经生效。

这些是对本文示例的静态审阅结论,不是当前 Hermes 服务故障的诊断。本文所用 hermes -p manas chat -q 在本机 CLI 源码中有对应参数处理,不应因形式较旧就判为无效。