CH 08 · プラグインツリーのメンタルモデル:すべてはプラグイン
この章の目標
CH 02 で「すべてはプラグイン」と述べました。この章ではそれをメンタルモデルに変えます:動いている dsh とは何か、なぜすべての部分を差し替えられるのか、そして「Provider を差し替えれば製品も差し替わる」という一文がなぜ実際に成り立つのか。
ステージを整える 1 文:動いている dsh = プラグインツリー
公式アーキテクチャドキュメントの表現はこうです:動いている dsh はプラグインツリーで、起動時に順序付けられたレイヤ群から合成される。 覚えるために 3 層に分ければ、アーキテクチャ全体が見えます:
図を上から下に読むと、ロジックは:① まず Profile を選ぶ → ② プラグインレイヤを順に重ねる → ③ 全レイヤが同じ Cordis カーネル上で動く。対応する 3 点:
- 最下層が Cordis カーネル:プラグインがサービスを注入し、イベントをブロードキャストし、エフェクトをマウントできる共有コンテキスト。
- 中間がプラグインツリーそのもの:さまざまなプラグインがツリー状に重なり、それぞれが 1 つの決まった仕事を担当。
- 最上層が Profile:新しい能力を加えるわけではなく「このツリーをどう見せるか」だけを決める —— web、headless、あるいは自分で組み合わせたカスタム(次節で扱う)。
Cordis:すべてを動かすフレームワーク
dsh は Cordis というプラグインフレームワーク上で動いています(公式 README が明示的に「powered by Cordis」と書いている)。Cordis の中核は約束が 3 つだけ:
- プラグインは共有コンテキストにサービスを注入する。能力を足したければ、プラグインを書いて登録する。
- プラグインは型付きイベントをブロードキャストする。他者がそれを購読して協調する、互いに直接 import する必要はない。
- プラグインの登録は可逆エフェクト —— プラグインがアンロードされると、登録したものは自動的に巻き戻り、残骸は残らない。
最も重要なのは最後です —— これが「すべてはプラグイン」が成立する理由です:dsh に特権コアは存在しない、改造対象がない。 モデルアダプタもプラグイン、ツールレジストリもプラグイン、セッションログもプラグイン、Agent のメインループ自体もプラグイン。dsh を拡張する手段は「脇にプラグインをぶら下げる」ことで、「どこかの中核ファイルを編集する」ことではありません。だから変更コストがきわめて低い:変えられないなら差し替える、差し替えられないなら足す。
これと他のツールはどう違うか?一番使われている CLI Agent 2 つを並べると、違いが際立ちます:
| 比較対象 | 拡張アプローチ | 「骨格」に触れることができるか |
|---|---|---|
| Claude Code | ツールは拡張可能(MCP、skill)、ただしモデルとメインループは差し替え不可 | 半開き:ツールは足せるが、モデル適応、セッション、Agent ループは溶接済み、モデルは自エコシステムにロック |
| Codex | ツールは拡張可能、ただしモデルとメインループは差し替え不可 | 半開き:ツールは足せるが、モデル適応、セッション、Agent ループは溶接済み、モデルは自エコシステムにロック |
| dsh | 脇にプラグインをぶら下げ、設定を変えて差し替える | 完全に開く:セッションログ、Agent ループ、モデルアダプタすらプラグイン、特権コアなし |
1 行でまとめると:Claude Code と Codex の「骨格」は溶接されている;dsh の「骨格」もまたプラグインである。 だからこそどんなモデルでも接続できる(CH 06 の Codex との比較がその実例)、セッションも自分の実装に差し替えられる —— 骨格自体も交換可能、唯一の制限は「変えたいと思うか」だけです。
--dump-config から実物のツリーを見る
理論を 10 回言うより、実物を 1 回見るほうがよい。CH 05 で dsh --profile headless --dump-config を一度走らせました。出力されるのは @deepseek-ai/dsh-* プラグインが積み重なった長いツリーで、llm(モデル)、session(セッション)、credentials(キー)、session-persistence-jsonl(セッション永続化)…… これらのプラグインがそれぞれ自分の仕事を担当し、積み重なって実行可能な dsh になります。
公式チームが中核プラグインを明確に列挙しています;それぞれがツリーの「大枝」に対応:
| 中核プラグイン | 担当 |
|---|---|
core/session | 追記専用セッションログ(モデルが見たものはすべて記録される) |
core/system-prompt | システムプロンプトの組み立て、ツールスキーマのパッケージング方法 |
core/tools | ツールレジストリと制御された実行パイプライン |
core/agent | Agent インタフェースとライブレジストリ |
core/agent-loop | デフォルトの「リクエスト → ツール呼び出し」ループドライバ |
llm/llm | メッセージとストリーミング語彙 + モデルアダプタシーム |
最後の行の「シーム」に注意 —— これが次のキー概念です。
Profile と Bundle:プラグインはどう「モード」に組み合わされるか
プラグイン = 料理(チャーハン、家スープ、サイド);bundle = 定食セット(ご飯、メイン、箸がパック済み);profile = メニューカード(どの料理をどんな順で並べるか);奥の厨房にある鍋が次節の Cordis カーネル。
ツリーが何でできているかは Profile が決めます —— それは手に取ったメニューカードと考えるとよい:
- Profile は名前付きの組み合わせで、Harness ディレクトリに保存されます。どの Bundle を重ねるか、追加でどのプラグインを入れるかを列挙し、自分の
cordis.patch.ymlも持ちます。カードは料理を作らず「この食事に何が入っているか」だけを伝えます:公式工場はwebとheadlessのメニューカードをすぐ使える形で出荷;CH 02 の「4 つの実行モード」はその下にある 4 つのプラグイン組み合わせプリセットに相当し、カードの追加分です。 - Bundle は「一群のプラグイン + マウントされる設定」の配布形式で、パック済みの定食セットに相当:
dsh-baseは全 profile の第 1 層(基盤:モデル、ツール、永続化、サンドボックス、承認、設定、キー、テレメトリ);web-appは基本セットに「店内飲食環境」(ブラウザ界面)を追加;headlessは「テイクアウト」(ワンショットコマンドライン、サーバなし)を加えます。
混同しやすい点を先に:Profile はプラグインではありません。プラグインは「仕事をするもの」(料理)、Profile は単なる「カード」(注文するだけで調理はしない)。「Minimal モード」を選んでも新しいプラグインが入るわけではなく、プラグインツリーがよりミニマルな組み合わせに差し替わるだけです —— 違うカード、同じ厨房。
プラグインの重ね方には厳格な順序があり(公式)、厨房がカードに従って準備し、後のノートは先のものを上書きできます:
各 bundle(profile に列挙された順) ← まずは定食セット
→ profile 自身の cordis.patch.yml ← カードに書かれたメモ
→ Harness ディレクトリレベルのもの ← 上司の共通要件
→ コマンドライン --patch のオーバーレイ ← 臨時メモ、後勝ち各レイヤは上のものを上書きできます —— パッチは id で 1 行を定位し、その設定全体を置換したり、新しい行を挿入したりします。だから CH 05 の --patch は「余計な穴」ではなく、このツリーを堂々と改造する手段です:--dump-config が出力するどの 1 行も、自分のパッチで置き換えられます。
自前の Profile を作るのは、ゼロからコードを書くことではありません —— 本質は「カードを 1 枚コピーして直す」ことです:
- web より少なくしたい? → web の設定をコピーして不要なものを削る(「店内飲食、でもスープ抜き」);
- headless より多くしたい(例:自分が書いたツールプラグインをデフォルトでマウント)? → headless をコピーして bundle リストに 1 行足す(「テイクアウト、プラス持ち込みサイド」);
- 一番軽いのは「カードを変えず、メモだけ足す」:既存 profile にプラグインを入れるには
dsh plugin --profile <name> add <パッケージ>(「既存カードにサイドを 1 品追加」)、一時的に上書きするには--patch(「この食事メモ:スープを Sprite に変更」)—— どちらも CH 05 で登場済みです。
ロード過程を一本のタイムラインにまとめると、以上がすべて着地します:
- 起動時、dsh はまず選んだ Profile を読む —— それはロードマニフェストで「どのレイヤをどんな順で重ねるか」を伝えます;
- マニフェスト順にレイヤ単位でロード:まず第 1 層 ベース(dsh-base)、次に第 2 層 追加(web-app / headless / 自分のプラグイン)、最後に第 3 層 上書き(cordis.patch.yml +
--patch)—— 後ろにロードしたものほど、先のものを上書きできます; - 全レイヤは同じ Cordis カーネル上にインストールされ、互いにサービスを注入し合います。
着地点の 1 行:Profile = 起動時の注文順;プラグインの木 = その順で積まれた料理;Cordis = 料理を積む鍋。
イベントと能力シーム:プラグインはどう話すか
プラグインは互いに import し合わず、2 つのものを介して協調します:
イベントは拡張点で、3 つの領域があります:
- Session イベント:永続的事実、ログに追記され、ブロードキャストされ、再起動を跨いで生き残る(例:「このメッセージは送信済み」)。
- Agent イベント(
agent/*):ライブ Agent を運ぶ、実行中の作業を観察または介入するために使う(例:「このステップ開始前に介入」)。 - 能力イベント(
fs/*、tools/*、telemetry/*):メインループを変えずにシームにポリシーとアダプタを取り付ける。
能力シームは差し替え可能な能力で、3 つの固定役割があります:
- Service Definition:インタフェースの形状を宣言する;
- Service Provider:実際に実装する;
- Consumer:それを使う(通常はモデルが呼び出すツール)。
なぜ「Provider を差し替えれば製品も差し替わる」のか?ファイルシステムとサブプロセスの Provider が同じ「実行ワールド」を共有しているため —— それらをリモートサンドボックスに向ければ、Bash、PTY、LSP もみんなついてきて、能力ごとに別系統を書く必要はありません。モデルアダプタも同じ:ctx.llm に登録されているアダプタが、製品全体で使われるモデルになります。他は何も変える必要はありません。
メンタルモデルを 1 行で
3 文しか持ち帰れないなら、次の 3 文を覚えてください(冒頭の小さなレストランの比喩と一緒に):
- dsh にはコアがない —— すべての部分がプラグイン、それ自身も含む;脇に料理を足せばいい、抜けば自動で巻き戻る。
- Profile がツリーの形を決める —— web / headless / カスタムはすべて同じツリーの違う「メニューカード」;調理はせず、注文するだけ。
- イベントとシームがプラグイン協調のインタフェース —— 取り付ければ動く、外せば巻き戻る、だから自由に差し替えるも足すも;Provider を差し替えれば製品も差し替わる。
この章で学んだこと
- [ ] 「動いている dsh = プラグインツリー」であり、それが順序付けられたレイヤ群から合成されることを言える
- [ ] Cordis の 3 つの中核約束(サービス注入 / イベント / 可逆エフェクト)を言え、特権コアがない理由を言える
- [ ]
--dump-configの出力から中核プラグイン(llm / session / credentials / tools など)を識別できる - [ ] Profile と Bundle の関係と、プラグイン重ねのレイヤ順を説明できる
- [ ] 「注文」の比喩でプラグイン / bundle / profile の関係を説明でき、自前 profile 構築は「カードをコピーして直す」こと、
plugin add/--patchが最も軽い改造手段であることを知っている - [ ] 能力シームの 3 役割(Definition / Provider / Consumer)を言え、「Provider を差し替えれば製品も差し替わる」理由を説明できる
