アーキテクチャ概要
Hermes Agent は Nous Research がオープンソース(MIT)で開発するデスクトップ AI エージェントで、「共に成長する永続的エージェント」を位置付けている。メッセージが Telegram/Discord/Slack などから届き、コアループがモデル+ツール (tool) を呼び出してタスクを完遂し、その経験を記憶とスキルに蓄積して次回再利用する。
レイヤー構成
各層の一言解説
- UI 層: TUI / CLI / Web / ACP(エディタプロトコル) — 単なる入口、業務ロジックは持たない。
- ゲートウェイ (gateway):
gateway/run.pyが統一アクセス、各プラットフォームはアダプタ (adapter)、セッション (session) と配信は分離、ストリーム (stream) と hooks は差し替え可能。 - コアループ:
run_conversationが「モデル呼び出し → ツール呼び出し (tool dispatch) → 結果反映 → 予算 (budget) 減少」を編成し、失敗時はフォールバック (fallback)。 - 機能層: Tool はアトミック関数(
tools/registry.py)、Skill は高階で進化可能なフロー(学習ループが書き換え)、Provider/MCP/Plugins がモデルと外部機能を供給。 - スケジュール: Cron が定時駆動、Subagent は隔離委譲(コンテキスト (context) コストゼロ)、6 つのサンドボックス (sandbox) バックエンドが実行を隔離。
- 学習ループ: 対話の経験をスキルとユーザーモデルに蓄積し、「使うほど良くなる」フライホイールを形成。
トップダウン:各層の代表的なシグネチャ
Hermes のレイヤー構成を理解する一番の近道は、各層がコード上でどんな「シグネチャ」を露出しているかを見ることだ。
UI 層 → コアループへの転送器。すべてのシェルは AIAgent.run_conversation を呼ぶが、これは単なる転送だ(run_agent.py:6350)。
def run_conversation(self, user_message, system_message=None,
conversation_history=None, ..., moa_config=None) -> Dict[str, Any]:
"""Forwarder — see ``agent.conversation_loop.run_conversation``."""
from agent.conversation_loop import run_conversation
token = set_conversation_context(self._conversation_root_id())
with scoped_runtime_main({}):
return run_conversation(self, user_message, ...)会話 ID は ContextVar 経由で伝達される。これにより、メインループ内のすべての LLM 呼び出し(圧縮 (compression)、ビジョン、MoA スロット、バックグラウンドレビューの fork)が自動的にタグ付けされる。
ゲートウェイ層 → プラットフォームアダプタの総司。GatewayRunner は mixin で認証/カンバン/スラッシュコマンド機能を組み立てる(gateway/run.py:22392)。エントリの start_gateway(replace=True) は古いインスタンスを先に殺す——systemd 再起動時に前のプロセスが残ったままになるケースへの抜け道だ。
class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, GatewaySlashCommandsMixin):
"""Main gateway controller. Manages the lifecycle of all platform adapters
and routes messages to/from the agent."""コアループ → 本当に仕事をする場所。run_conversation は agent/conversation_loop.py にあり、「モデル呼び出し → ツール呼び出し → 結果反映 → 予算減少」を編成し、失敗時はフォールバックする。シグネチャは前述の通り。
機能層 → ツール登録。各 tools/*.py はモジュール import 時に registry.register を呼ぶ(tools/registry.py:365)。override=True のときだけプラグインが組み込みツールを上書きできる。
def register(self, name, toolset, schema, handler, check_fn=None,
requires_env=None, is_async=False, description="", emoji="",
max_result_size_chars=None, dynamic_schema_overrides=None, override=False):
"""Register a tool. Called at module-import time by each tool file.
``override=True`` is an explicit opt-in for plugins ... Without it,
registrations that would shadow an existing tool are rejected."""プラグイン (plugin) が組み込みツールを差し替えるには、明示的に override=True を指定し、allow_tool_override 設定を通す必要がある——プラグインがこっそり browser ツールをすり替えるのを防ぐためだ。MCP ツールの同名上書きは別ルートで処理される。
スケジュール → cron の tick。tick(cron/scheduler.py:3888)はファイルロックで保護された 1 回のスキャンで、gateway は 60 秒ごとにバックグラウンドスレッドから呼ぶ。adapters パラメータにより、cron タスクは gateway が既に張ったプラットフォーム接続を再利用し、自分で再接続しなくて済む。
学習ループ → 学習グラフの集約。build_learning_graph(agent/learning_graph.py:248-328)は、学習された(組み込みでない)スキルと記憶をノード/エッジに組み立てる。まず build_skill_nodes(_skill_roots()) で base と profile の 2 つのルートを走査し、source != "base" かつ created_by == "agent" または use_count > 0 のスキルを「学習された」として抽出する——組み込みスキルは学習グラフに入れないことで、進化シグナルがノイズに埋もれないようにしている。
レイヤー構成の動機
なぜこの分割か?一言で言えば:変えられる層で変えられない層を汚さないため。
- UI 層はシェルの張り替えが頻繁(TUI/Web/IDE プロトコルそれぞれの反復リズムがある)。シェルの差し替えで業務を変えさせてはいけない——だからシェルはコールバック(
step_callback/event_callback)を渡すだけでrun_conversationの中には入らない。 - ゲートウェイ層では新プラットフォームの追加が日常(QQbot、Enterprise WeChat、新しい Discord スタイル)。しかしコアループはプラットフォームを意識すべきでない——だからプラットフォームロジックはすべてアダプタで、ゲートウェイはメッセージと agent 呼び出しを疎結合に保つだけだ。
- 機能層は「アトム」、学習ループは「進化」——アトムは進化パスに汚染されてはいけない(さもないとバックグラウンドレビューのバグがツールを壊し、メインループまで連鎖する)。だから
learning_mutationsが書き換えるのはtools/ではなくskills/だ。 - スケジュール拡張は横断的:Cron と Subagent はどちらも「コアループの外からコアループを起動する」もので、
run_conversationを共有するが agent の状態を直接は持たない。
よくある誤読
- 「Hermes は agent フレームワークだ」——違う。これは具体的なデスクトップ agent の実装で、コアループ、ツールセット、スキルディレクトリはどれも Hermes 自前のもの。フレームワークとしての能力(MCP、Plugin、Provider の抽象)は、拡張できるように開いた境界であって、本体ではない。
- 「学習ループがプロンプト (prompt) を自動最適化する」——しない。学習ループが書き換えるのは
skills/の SKILL.md と~/.hermes/skills/の記憶だけで、システムプロンプト (system prompt) のテンプレートは変えない。プロンプトテンプレートはリポジトリ内の固定ファイルで、変えるにはリリースが必要だ。 - 「Subagent は分散」——違う。Subagent は同じプロセス内で agent を fork(runtime を共有)する。サンドボックスは実行の隔離(コードが Docker/SSH/Modal/Daytona で走る)であって、agent 状態の分散ではない。
- 「ゲートウェイ層 = メッセージプラットフォーム接入」——半分だけ正しい。ゲートウェイはセッション状態(
session/)、配律重複排除(delivery_ledger)、ストリーム配信(stream_*)、スラッシュコマンド、kanban watchers まで担う——これらはすべて「メッセージプラットフォーム ↔ agent」間の職責で、単なる接入にとどまらない。