メモリープロバイダープラグインを作る
目次
- 設置の形
- ディレクトリ型のプロバイダー
- パッケージ型のプロバイダー
- MemoryProvider 抽象基底クラス
- 初期化時に渡されるコンテキスト
- 実装が必要なメソッド
- 基本のライフサイクル
- 設定
- 任意のフック
- 大きすぎる prefetch の結果
- 圧縮前のチェックポイント(失敗したら止める)
- セットアップまわりの使い勝手 — 単独のプロバイダーが保てるもの
- 設定スキーマ
- 設定の保存
- プラグインのエントリーポイント
- plugin.yaml
- スレッドの取り決め
- プロファイルの分離
- 複数のプロファイルを扱うゲートウェイ
- テスト
- CLI コマンドを足す
- 仕組み
- 例
- 手本になる実装
- CLI を含むディレクトリ構成
- プロバイダーは 1 つだけ
- HERMES_HOME が残ることの約束(ラッパーが頼れること)
メモリープロバイダープラグインを使うと、Hermes Agent は組み込みの MEMORY.md や USER.md の枠を超えて、セッションをまたいで残る知識を持てるようになります。このガイドでは、その作り方を説明します。
設置の形
Hermes は次の 4 か所からメモリープロバイダーを見つけます。優先順位はこの並びのとおりです。
| 取得元 | 場所 | 補足 |
|---|---|---|
| 同梱 | plugins/memory/<name>/ |
Hermes に最初から入っています。新しいプロバイダーの追加は受け付けていません — CONTRIBUTING を参照してください。 |
| ユーザー | $HERMES_HOME/plugins/<name>/ |
利用者が自分で置きます。プロファイルごとに分かれます。 |
| プロジェクト | ./.hermes/plugins/<name>/ |
HERMES_ENABLE_PROJECT_PLUGINS=1 を設定したときだけ有効になります。 |
| パッケージ | hermes_agent.memory_providers のエントリーポイント |
インストールの管理者が用意した配布物を使います。ファイルをコピーする必要はありません。 |
名前がぶつかったときは先に挙げた取得元が勝ちます。そのため、作業ツリーに 置かれたディレクトリが同梱のプロバイダーを覆い隠すことはありません。
見つける処理は *列挙するだけ* で、プロバイダーを読み込むことはありません。 memory.provider に名前が書かれるまで、何も動きません。
エントリーポイントからの検出は、パッケージをインストールしません。Hermes が選んでいる環境へ pip でプロバイダーを押し込まないでください。PM で管理しているインストールでは、Python の依存関係を 宣言したディレクトリ型のプロバイダーとして配布します。プラグインの受け入れ処理と hermes memory setup が、使う前に PM を通してそれらを準備します。管理者が自分で管理するビルド (Nix など)では、エントリーポイント型の配布物を宣言的に含めることができます。
CLI とダッシュボードの設定は、候補の準備処理を共有しています。PM は、プロバイダーの pyproject.toml、または旧来の pip_dependencies / python_dependencies を、有効な プラグイン全体の依存関係の集合に加えます。モジュールを import できても、宣言されたバージョンの 制約を飛ばすことはできません。ダッシュボードの準備状況の確認は、同じ入力を何もインストールせずに 調べます。準備が成功しても、選ばれた依存関係の世代を動作中のプロセスが使えるようになるには、 Hermes の再起動が必要なことがあります。外部のサイドカーの確認や設定コマンドは、Python の 依存関係の集合とは別に扱われます。
ディレクトリ型のプロバイダー
ディレクトリ型のプロバイダーは、Hermes に同梱される場合は plugins/memory/<name>/、利用者が入れる場合は $HERMES_HOME/plugins/<name>/、 プロジェクト専用の場合は ./.hermes/plugins/<name>/ に置きます。
plugins/memory/my-provider/
├── __init__.py # MemoryProvider implementation + register() entry point
├── plugin.yaml # Metadata (name, description, hooks)
└── README.md # Setup instructions, config reference, toolsパッケージ型のプロバイダー
pip で入れるプロバイダーは、hermes_agent.memory_providers グループに エントリーポイントを公開します。エントリーポイントの名前が、利用者が memory.provider で選ぶプロバイダー名になり、その値はプロバイダーの register(ctx) 関数を指します。
[project.entry-points."hermes_agent.memory_providers"]
my-provider = "my_provider:register"エントリーポイントは パッケージ、またはその中の register(ctx) を指すように して、実装・スキル・その他のリソースは通常の Python パッケージの構成のまま置いて ください。$HERMES_HOME/plugins/ の下にコピーする必要はありません。
パッケージのエントリーポイントでも、ディレクトリに置いた場合とまったく同じことが できます。読み込みではなくディスクから直接読まれる 2 つのファイル、つまり config_schema.py(ダッシュボードの設定パネル)と cli.py(hermes <provider> のサブコマンド)も含みます。どちらもパッケージの __init__.py の隣で探されるので、 このどちらかを配布するなら、エントリーポイントは単一モジュールではなくパッケージを 指すようにしてください。
MemoryProvider 抽象基底クラス
プラグインでは、agent/memory_provider.py にある MemoryProvider 抽象基底クラスを実装します。
from agent.memory_provider import MemoryProvider
from agent.secret_scope import get_secret
class MyMemoryProvider(MemoryProvider):
@property
def name(self) -> str:
return "my-provider"
def is_available(self) -> bool:
"""Check if this provider can activate. NO network calls."""
return bool(get_secret("MY_API_KEY"))
def initialize(self, session_id: str, **kwargs) -> None:
"""Called once at agent startup.
kwargs always includes:
hermes_home (str): Active HERMES_HOME path. Use for storage.
"""
self._api_key = get_secret("MY_API_KEY") or ""
self._session_id = session_id
# ... implement remaining methods初期化時に渡されるコンテキスト
AIAgent は MemoryManager.initialize_all() を通じて、セッションのコンテキストを initialize(session_id, **kwargs) に渡します。**kwargs を受け取れるようにし、省略可能な 項目が欠けていても動くようにしてください。エージェントやセッションのデータベースなしでプロバイダーが初期化されることもあります。
| キーワード | 意味 |
|---|---|
hermes_home |
有効なプロファイルの保存先ディレクトリ。 |
platform |
セッションの利用面。cli、gui、acp、telegram など。 |
session_title |
保存されているセッションのタイトル(ある場合)。表示用のラベルが、利用者の選んだ識別名とは限りません。 |
session_title_source |
保存されているタイトルの出どころ(ある場合)。derived、llm、user のいずれか。自動で付いたタイトルを、明示的な識別名の指定と取り違えないでください。出どころがなければ、プロバイダーは従来の挙動のままです。共通の定数は hermes_state_common.py にあります。 |
cwd |
AIAgent(cwd=...) で渡された空でない論理ワークスペース。プロバイダーの初期化より前から使えます。None や空文字列のときは含まれません。 |
gateway_session_key |
チャットごとにセッションを分けるための、メッセージングのチャットの安定した識別子。 |
user_id, user_id_alt, user_name, chat_id |
ゲートウェイの識別情報。ある場合に含まれます。 |
agent_identity |
有効なプロファイル名(ある場合)。 |
agent_workspace, agent_context |
実行時のエージェントの範囲。agent_workspace は hermes です。agent_context はスケジューラーからの実行なら cron、delegate_task で生まれた子なら subagent、それ以外は primary になります。primary 以外の値のときは自動での書き込みを飛ばしてください。 |
os.getcwd() が会話のワークスペースを表すとは考えないでください。1 つの Desktop やゲートウェイのバックエンドが、複数のセッションを受け持つことがあるためです。cwd がなく、ディレクトリで 振り分ける必要があるときは、agent.runtime_cwd.resolve_agent_cwd() がセッションの cwd コンテキスト、次にスコープ付きの terminal.cwd(内部では TERMINAL_CWD として受け渡されます)、最後に 起動ディレクトリの順で参照します。構築時のワークスペース情報を使うために、 プロセスの cwd を変えたり、既存の会話のシステムプロンプトを作り直したりする必要はありません。
Desktop や TUI でワークスペースを変えると、tui_gateway/session_workdir.py::_register_session_cwd を通じて 実行中のエージェントの session_cwd が同期されます。ワークスペースを移したあとに遅延して エージェントが接続された場合も同様です。これにより、初回または再起動した Codex app-server のセッションは、構築時の cwd ではなく現在のワークスペースを使えます。 ただし、すでに動いている Codex のスレッドを移したり、メモリー プロバイダーを初期化し直したり、既存の Honcho セッションの識別子を変えたり、キャッシュ済みの システムプロンプトを無効にしたりはしません。プロバイダーの初期化には引き続き構築時の ワークスペースが渡されます。cwd がないか空の場合は、固定されないままです。
実装が必要なメソッド
基本のライフサイクル
| メソッド | 呼ばれるタイミング | 実装は必須か |
|---|---|---|
name(プロパティ) |
常時 | 必須 |
is_available() |
エージェント初期化時、有効化の前 | 必須 — ネットワーク通信はしないこと |
initialize(session_id, **kwargs) |
エージェント起動時 | 必須 |
get_tool_schemas() |
初期化後、ツールを差し込むとき | 必須 |
handle_tool_call(tool_name, args, **kwargs) |
エージェントがこのツールを使うとき | 必須(ツールを持つ場合) |
設定
| メソッド | 目的 | 実装は必須か |
|---|---|---|
get_config_schema() |
hermes memory setup 用に設定項目を宣言する |
必須 |
save_config(values, hermes_home) |
秘密でない設定を、自前の保存先に書き出す | 必須(環境変数だけで済む場合を除く) |
任意のフック
| メソッド | 呼ばれるタイミング | 使いどころ |
|---|---|---|
system_prompt_block() |
システムプロンプトの組み立て時 | プロバイダーの固定情報を渡す |
prefetch(query, *, session_id="") |
API 呼び出しのたび、その前 | 思い出した内容を返す |
queue_prefetch(query, *, session_id="") |
各ターンの後 | 次のターンに備えて先に温めておく |
sync_turn(user, assistant, *, session_id="", messages=None) |
1 ターンが終わるたび | 会話を保存する |
on_session_end(messages) |
会話が終わったとき | 最後の抽出・書き出し |
on_pre_compress(messages) |
コンテキスト圧縮の前 | 捨てられる前に気づきを保存する |
on_memory_write(action, target, content, metadata=None) |
組み込みメモリーへの書き込み時 | 自前のバックエンドにも同じ内容を残す |
shutdown() |
プロセス終了時 | 接続の後始末 |
組み込みの replace と remove では、metadata["previous_content"] に、組み込みの保存先の ロックをかけた状態で選ばれたエントリーの全文が入ります。通知は、書き込みや一括処理がすべて 成功してからしか出ません。一括処理の通知は操作の順番を保ち、各操作の変更前の内容には、その 一括処理の中で先に行われた操作が反映されています。 old_text は呼び出し側の検索文字列であって、変更されたエントリーを特定するものではありません。 古い Hermes のバージョンでは previous_content が省かれることがあります。エントリーを正確に 特定する必要があるプロバイダーは、これが無いときは、消去を伴う書き写しを見送ってください。
大きすぎる prefetch の結果
外部プロバイダーの prefetch() の結果は、既定では全文がそのまま返され、 プロバイダーが関連度の順に並べた想起の結果が保たれます。ただし安全のための上限があり、 hooks.output_spill.max_chars の 10 倍(最低でも 100,000 文字)を超えた分は、 それでも退避されます。プロバイダー自身の想起の量は、この上限よりかなり小さく抑えてください。 大きすぎる結果を退避させたいときは、使用中のプロファイルで次のように設定します:
memory:
prefetch_spill_enabled: true # default: false有効にすると、共通の hooks.output_spill.max_chars のしきい値(既定では 10,000 文字)を 超えた結果は専用の退避ファイルに書き出され、設定に沿った先頭と末尾のプレビューと、 そのファイルのパスに置き換えられます。既定のプレビューに残るのは最初と最後の 500 文字だけで、 間の部分を取り戻すにはモデルがファイルを読む必要があります。しきい値以下の結果はそのままです。
共通の hooks.output_spill のプレビューの長さと保存先の設定は引き続き効き、 hooks.output_spill.enabled: false にすると、メモリ側で有効にしていても退避は行われません。 どちらの設定も外部プロバイダーが登録された時点の値で固定されるので、既存のセッションに 反映するには Hermes を再起動してください。組み込みのメモリと、通常のプラグインのフックでの 退避には影響しません。 プラグイン — 大きすぎるコンテキストの退避 を参照してください。
圧縮前のチェックポイント(失敗したら止める)
on_pre_compress() は、既定では「できる範囲でやる」扱いです。プロバイダーが例外を 投げても、ホストは失敗を記録したうえで圧縮を続けます。気づきの抽出が目的なら それでよいのですが、情報が失われる書き換えの *前に* 会話の記録を確実な保存先へ 残すことが仕事のプロバイダーには向きません。そうした場合のために、ホストは 希望者だけが使えるチェックポイントの取り決め(API v2)を用意しています。
from agent.memory_provider import MemoryProvider
class MyArchivingProvider(MemoryProvider):
# Opt in: every successful on_pre_compress() return means the durable
# checkpoint is committed. Raise on any failure — do not return partial
# success. Version 1 (the inherited default) is the implicit historical
# contract: best-effort semantics, raw message list.
pre_compress_checkpoint_api_version = 2
def on_pre_compress(self, messages, *, require_checkpoint=False):
# require_checkpoint mirrors the operator's checkpoint_required
# setting: True means a raise here blocks the lossy rewrite.
ids = self._archive(messages) # must be durable before returning
return f"checkpoint: {ids}" # forwarded into the summary prompt運用する側は、環境ごとにこの強制を有効にできます。
compression:
checkpoint_required: true # default: falseこの仕組みを有効にすると、API を宣言している有効なプロバイダーがチェックポイントを 完了しないかぎり、情報が失われる書き換えに入る前に圧縮が 止まります。圧縮前の 会話記録はそのまま保たれ、圧縮の試みは BLOCKED_MISSING_PREREQUISITE で失敗し、 保存先が復旧すればやり直せます。無効のまま(既定)なら、これまでのプロバイダーの 動きは何も変わりません。
Hermes に同梱されているプロバイダーは、どれもチェックポイント API v2 を宣言して いません。この取り決めは希望者だけが使うもので、第三者の記録保管プロバイダーの ために用意されています。そうしたプロバイダーが無いまま checkpoint_required を 有効にすると、手動・自動を問わず圧縮の試みがすべて止まります。エージェントの 初期化時には、有効なプロバイダー名を挙げた警告が記録され、拒否のたびに解除の ための項目として compression.checkpoint_required が示されます。
この仕組みは、Hermes の要約機能だけでなく、圧縮を行うすべての主体に効きます。 サーバー側のネイティブ圧縮(compression.codex_responses_native)は、仕組みが 有効な間は抑止されます。ターン後の小刻みな圧縮(compression.micro_compact)は エージェント初期化時に強制的に無効化されます(古いやり取りを回転する要約に 吸収してしまい、その経路にはチェックポイントのフックがないためです)。 codex_app_server の API モードもエージェント初期化時に拒否されます。codex エージェントは自分のスレッドを自分で圧縮し、圧縮前と言える正しい区切りが 存在しないため、必要なチェックポイントを保証できないからです。チェックポイントを 理解する Hermes の圧縮機能だけが、情報を失う書き換えを行える唯一の主体になります。
プロバイダーが受け取る内容は、宣言した API のバージョンによって変わります。 バージョン 1 のプロバイダー(暗黙の既定であり、これまでのすべてのプロバイダー)は これまでどおりの取り決めのままで、生のメッセージ一覧をそのまま受け取ります。 バージョン 2 のチェックポイント対応プロバイダーは、代わりに整えられた直接の証拠を 受け取ります。つまり、ユーザーとアシスタントのテキスト行だけです。ツールの実行結果、 システムメッセージ、アシスタントメッセージの tool_calls の中身(本文の言葉は 残ります)、それ以前の圧縮要約は、ホスト側で取り除かれます。以前の要約は、 プロセスを再起動しても残る _compressed_summary という印で見分けられるので、 再開したセッションが要約の要約を保存先に送り込むことはありません。
チェックポイントは、何度実行しても同じ結果になるように作ってください。 圧縮が 止められたあと、次の試みでは同じ会話記録で on_pre_compress() がもう一度呼ばれます。 少し伸びただけの会話記録からは、ほとんど重なった証拠ができます。保存の書き込みは 内容(たとえば会話記録のダイジェスト)をキーにして上書き保存にしておき、やり直しや 重なりが積み上がらず、まとまるようにしてください。
取り決めのテスト: tests/agent/test_pre_compress_checkpoint_contract.py。
セットアップまわりの使い勝手 — 単独のプロバイダーが保てるもの
Hermes が同梱のプロバイダーに用意しているセットアップの画面や手順は、いずれもプロバイダー 自身のディレクトリにあるファイルで動いています。プラグインのカタログから入れたプロバイダーでも、 そのすべてをそのまま使えます。
| 画面・手順 | プロバイダー側が用意するもの |
|---|---|
| Desktop → Capabilities → Tools → Memory(設定パネル) | config_schema.py(後述) |
hermes memory setup のウィザード |
get_config_schema() がウィザードで尋ねる項目を宣言し、save_config(config, hermes_home) がそれを保存し、post_setup(hermes_home, config) がそのあとの対話的な処理(OAuth、初回の同期など)を行います。get_status_config() は hermes memory status の表示に使われます |
hermes <provider> … のサブコマンド |
register_cli(subparser) を持つ cli.py(CLI コマンドを足す) |
| Python の依存関係 | pyproject.toml の [project] dependencies(または plugin.yaml の python_dependencies)。インストール時に Hermes 自身の固定バージョンのもとで入り、hermes update のたびに当て直されます |
プロバイダーの名前、memory.<name> の設定セクション、データディレクトリ、ツール名は、 すでに使っている人との約束事です。本体から外に出たプロバイダーも、この 4 つをそのまま保ちます。 そのうえで Hermes は、memory.provider がいまもそのプロバイダーを指している人のために、 カタログのプラグインを自動で入れます(hermes update のとき、および security.allow_lazy_installs が有効なら起動時に 1 回)。
設定スキーマ
get_config_schema() は、hermes memory setup が使う設定項目の一覧を返します。
def get_config_schema(self):
return [
{
"key": "api_key",
"description": "My Provider API key",
"secret": True, # → written to .env
"required": True,
"env_var": "MY_API_KEY", # explicit env var name
"url": "https://my-provider.com/keys", # where to get it
},
{
"key": "region",
"description": "Server region",
"default": "us-east",
"choices": ["us-east", "eu-west", "ap-south"],
},
{
"key": "project",
"description": "Project identifier",
"default": "hermes",
},
]secret: True と env_var が付いた項目は .env に書かれます。秘密でない項目は save_config() に渡されます。
設定の保存
def save_config(self, values: dict, hermes_home: str) -> None:
"""Write non-secret config to your native location."""
import json
from pathlib import Path
config_path = Path(hermes_home) / "my-provider.json"
config_path.write_text(json.dumps(values, indent=2))環境変数だけで動くプロバイダーなら、既定の何もしない実装のままで構いません。
プラグインのエントリーポイント
def register(ctx) -> None:
"""Called by the memory plugin discovery system."""
ctx.register_memory_provider(MyMemoryProvider())プロバイダーは、同じコールバックから読み取り専用のスキルを公開することもできます。 スキルはエントリーポイントの名前で修飾され、そのメモリープロバイダーが有効なとき だけ読み込まれます。
from pathlib import Path
SKILLS_DIR = Path(__file__).parent / "skills"
def register(ctx) -> None:
ctx.register_memory_provider(MyMemoryProvider())
ctx.register_skill(
"maintenance",
SKILLS_DIR / "maintenance" / "SKILL.md",
"Maintain the provider's memory store",
)my-provider のエントリーポイントが有効なとき、このスキルは skill_view() から my-provider:maintenance として使えます。
plugin.yaml
name: my-provider
version: 1.0.0
description: "Short description of what this provider does."
hooks:
- on_session_end # list hooks you implementスレッドの取り決め
sync_turn() は処理をせき止めてはいけません。 バックエンドに待ち時間がある場合(API 呼び出しや LLM の処理など)は、デーモンスレッドで実行してください。スレッドは agent.memory_provider.spawn_context_thread で起動し、素の threading.Thread は決して使いません。プロファイルの分離(有効な HERMES_HOME や、ターンごとのシークレットの範囲)は contextvars に入っていますが、素のスレッドは空のコンテキストで始まります。そのため、複数のプロファイルを同時に扱う構成では、*既定の*プロファイルの保存先へ気づかないうちに書き込んでしまい、そこでは get_secret() が安全側に倒れて失敗します。
from agent.memory_provider import spawn_context_thread
def sync_turn(self, user_content, assistant_content, *, session_id="", messages=None):
def _sync():
try:
self._api.ingest(user_content, assistant_content, session_id=session_id, messages=messages)
except Exception as e:
logger.warning("Sync failed: %s", e)
if self._sync_thread and self._sync_thread.is_alive():
self._sync_thread.join(timeout=5.0)
self._sync_thread = spawn_context_thread(_sync, name="myprovider-sync")
self._sync_thread.start()同じことは、先読み(prefetch)用や書き込み用のスレッドにも当てはまります。小さな JSON の設定ファイル($HERMES_HOME/<provider>.json)は、utils.read_json_or_empty で読み、utils.atomic_json_write で書きます。config.yaml の中身はすべて hermes_cli.config.save_config(..., merge_existing=True) を通して書き込みます。
messages は任意で、ターンが終わった時点での OpenAI 形式の会話内容です。渡される 場合は、ユーザーとアシスタントのメッセージ、アシスタントのツール呼び出し、ツールの 実行結果メッセージが含まれます。生のターン内容を必要としないプロバイダーは、 messages の引数を省略できます。その場合、Hermes は従来の形のまま呼び出しを 続けます。
クラウド側のプロバイダーは、messages のどの部分が端末の外へ送られるのかを 書き残してください。ツール呼び出しやその実行結果には、ファイルのパス、コマンドの 出力、その他の作業内容が入っていることがあります。
プロファイルの分離
保存先のパスは 必ず initialize() に渡される hermes_home の値を使ってください。~/.hermes を直接書き込んではいけません。
# CORRECT — profile-scoped, bound on the instance
def initialize(self, session_id, **kwargs):
self._data_dir = Path(kwargs["hermes_home"]) / "my-provider"
# WRONG — shared across all profiles
data_dir = Path("~/.hermes/my-provider").expanduser()複数のプロファイルを扱うゲートウェイ
1 つのゲートウェイのプロセスが複数のプロファイルを同時に受け持つことがあり、それぞれのターンは別々のスレッドで並行して動きます。エージェントごとに専用の MemoryManager と専用のプロバイダーのインスタンスがあり、それぞれ 1 つのプロファイル向けに初期化されます。プロバイダーは次を守る必要があります。
- 読み込むたびに新しいインスタンスを返す。
register(ctx)ではプロバイダーを生成してください(ctx.register_memory_provider(MyProvider()))。モジュール単位のシングルトンを渡してはいけません。別のプロファイルのマネージャーがすでに結びつけたインスタンスは、マネージャーが初期化を拒み、memory_provider_instance_sharedをログに残します。 initialize()で結びつける。 保存先、パス、クライアント、バンク、テナントはすべてhermes_homeキーワード(と、initializeの中でget_secret()を通して読んだ秘密情報)から導き、その結びつきをselfに保持してください。- プロファイル固有のものをモジュールやクラスの単位に置かない。 全体で共有する「現在の保存先」を持たず、呼び出しのたびに共有の結びつきを付け替えることもしないでください。2 つのプロファイルの呼び出しは交互に入り込むので、呼び出しごとの付け替えは競合を起こします。
残りの半分はマネージャーが強制します。ツール呼び出しとすべてのライフサイクルフック(prefetch、queue_prefetch、sync_turn、on_memory_write、on_session_end など)は、インスタンスを初期化したプロファイルからしか実行されません。複数のプロファイルを扱うゲートウェイでは、プロファイルを名乗らない呼び出し元は拒否され、その拒否はプロファイルの errors.log(hermes logs errors)と gateway.log に memory_caller_profile_unresolved として記録されます。バックグラウンドのフックは、持ち主のプロファイルの HERMES_HOME で動きます。handle_tool_call(..., **kwargs) には hermes_home、agent_identity、caller_gsk も渡されます。これらはインスタンスがすでに結びついているプロファイルを示すので、確認のために使うのはかまいませんが、ここから結びつけ直さないでください。
Hermes はプロバイダーの内部までは検証できません。プロセス全体で共有する結びつきを持つプロバイダーは、こうした検査があっても複数のプロファイルのメモリーを混ぜてしまいます。
テスト
通しの書き方は、tests/agent/test_memory_provider.py と、その周辺のメモリー関連テスト(tests/agent/test_memory_session_switch.py、tests/agent/test_memory_user_id.py、tests/agent/test_memory_provider_init.py)を参照してください。
from agent.memory_manager import MemoryManager
mgr = MemoryManager()
mgr.add_provider(my_provider)
mgr.initialize_all(session_id="test-1", platform="cli")
# Test tool routing
result = mgr.handle_tool_call("my_tool", {"action": "add", "content": "test"})
# Test lifecycle
mgr.sync_all("user msg", "assistant msg")
mgr.on_session_end([])
mgr.shutdown_all()CLI コマンドを足す
メモリープロバイダープラグインは、自前の CLI サブコマンド群を登録できます(たとえば hermes my-provider status や hermes my-provider config)。決まった置き方をたどって見つける仕組みなので、本体のファイルに手を入れる必要はありません。
仕組み
- プラグインのディレクトリに
cli.pyを置きます - argparse の構成を組み立てる
register_cli(subparser)関数を定義します - メモリープラグインの仕組みが、起動時に
discover_plugin_cli_commands()でそれを見つけます - コマンドが
hermes <provider-name> <subcommand>として現れます
有効なプロバイダーだけに現れます: CLI コマンドが現れるのは、設定の memory.provider として自分のプロバイダーが有効になっているときだけです。利用者がそのプロバイダーを設定していなければ、コマンドは hermes --help に出てきません。
例
# plugins/memory/my-provider/cli.py
def my_command(args):
"""Handler dispatched by argparse."""
sub = getattr(args, "my_command", None)
if sub == "status":
print("Provider is active and connected.")
elif sub == "config":
print("Showing config...")
else:
print("Usage: hermes my-provider <status|config>")
def register_cli(subparser) -> None:
"""Build the hermes my-provider argparse tree.
Called by discover_plugin_cli_commands() at argparse setup time.
"""
subs = subparser.add_subparsers(dest="my_command")
subs.add_parser("status", help="Show provider status")
subs.add_parser("config", help="Show provider config")
subparser.set_defaults(func=my_command)手本になる実装
13 個のサブコマンド、プロファイルをまたいだ管理(--target-profile)、設定の読み書きまでそろった完全な例として、Honcho プラグインの cli.py を参照してください。
CLI を含むディレクトリ構成
plugins/memory/my-provider/
├── __init__.py # MemoryProvider implementation + register()
├── plugin.yaml # Metadata
├── cli.py # register_cli(subparser) — CLI commands
└── README.md # Setup instructionsプロバイダーは 1 つだけ
同時に有効にできる外部のメモリープロバイダーは 1 つ だけです。2 つめを登録しようとすると、MemoryManager が警告を出して拒否します。これは、ツールの定義が膨れ上がることや、保存先どうしがぶつかることを防ぐためです。
HERMES_HOME が残ることの約束(ラッパーが頼れること)
ここでの対象は、実行環境を Hermes が管理する Python の外にあるサイドカーの venv に置くラッパー型のプロバイダーです(走査されるプラグインのルートに依存関係の定義 — pyproject.toml、pip_dependencies、python_dependencies のどれも — が無いもの。外部または入れ子のサイドカーだけに属する pyproject.toml は走査されません)。
- 場所。
$HERMES_HOME/plugins/<name>/がプロファイルごとのプラグインの置き場所です。HERMES_HOMEは、有効なコンテキストの上書き、次に$HERMES_HOME、次にプラットフォームの既定値の順で決まります。プロファイルの分離を保つため、ラッパーやサイドカーを起動するときはHERMES_HOMEを引き継いでください。MemoryManager.initialize_allは、有効なhermes_homeをすべてのプロバイダーに渡します。 - 残ること。 通常の Hermes の更新(pm による管理 venv の再構築や置き換えを含む)は、
$HERMES_HOME/plugins/**を削除も書き換えもしません。インストール済みのラッパーのディレクトリと、その目印のファイル(例:mnemosyne-wrapper.json)は残ります。プラグインを明示的に更新・削除する流れ(hermes uninstall、hermes plugins remove、プロファイルの削除、利用者による削除)は、この保証の対象外です。 - サイドカーの分離。 依存関係の定義が無いプラグインのルートは、pm のワークスペースの依存関係の集合に加わりません。再同期や venv の再構築をしても、その依存関係を用意することも、そのツリーに触れることもありません。
- 衝突。 共有 venv を使うネイティブのプラグインでは、依存関係の集合が解決できないと、はっきり失敗します。候補のプラグインは有効化されず、import もされません(受け入れを判断する側が、設定を反映する前に拒否し、プラグインの識別名と依存解決の理由を報告します。再有効化や再試行の手順と、機械で読める pm の受領記録も付きます)。依存関係の解決が、ほかのプラグインを自動で無効にしたり、原因の切り分け(bisect)を走らせたりすることはありません。プラグインの明示的な更新や削除、独立したセキュリティの確認は、別の操作です。