プラグインシステムで最も簡単なのは、プラグインのロードだ。
たとえば、次のモジュールがある。
このモジュールを呼び出すと、新しい機能がシステムに加わる。
コンポーネントをプロセス起動時に一度だけロードし、プロセス終了時にまとめて破棄するモデルなら、通常は十分だ。動的な Harness の制約はもっと厳しい。プラグインは、無効化、交換、ホットアップデートされることがある。依存関係が存在するときだけ実行すべきプラグインもある。依存関係が失われればプラグインは停止し、依存関係が戻れば再有効化されなければならない。
Cordis はこの種の問題を Spatiotemporal Composability(時空の組み合せ可能性)とまとめている。2026年8月13日に公開されたプレプリント『A Programming Paradigm for Spatiotemporal Composability』は、この問題を互いに独立した2つの次元へ分けている。[1]
- Temporal Composability:コンポーネントがシステムから外れるとき、そのコンポーネントが生んだ副作用を完全に巻き戻せる。
- Spatial Composability:コンポーネントは環境への要件を宣言し、環境の変化に応じて有効かどうかを動的に調整できる。
論文はさらに、Effect、Coeffect、統一された Context を用いてこの2つの次元を説明している。
これらの形式定義を論文の章立てに沿って並べるのではなく、まずエンジニアリング上の問題を整理し、その後にこれらの抽象化がなぜ成立するかを説明する。
動的プラグインが最初に直面するのは「巻き戻し」
プラグインは実行後、通常は複数の外部構造を変更する。
ロード後、実行環境には3つの変化が生じる。
Tools + tool
Listeners + listener
Timers + timer
アンロードがプラグインオブジェクトを削除するだけなら、これらの変化はそのまま残る。
したがって、動的プラグインのライフサイクルは、単に読み込むだけでなく、プラグインが環境に対して行った変更も記録する必要がある。
plugin = loaded
最も直接的な方法は、各操作に対応する巻き戻し関数を返させることだ。
これは、論文の revertible effect にかなり近い。
1回の登録を状態変換として書くと、次のようになる。
Context C
│
│ apply effect E
▼
Context C'
コンポーネントを安全に取り除くには、対応する逆操作が必要になる。
Context C'
│
│ revert E
▼
Context C
重要なのは、メモリ上のすべてのバイトを厳密に復元することではない。共有実行環境に対してコンポーネントが生じた観測可能な影響を、ライフサイクルシステムで巻き戻せることにある。
Temporal Composability の工学的意味はこれである。
Effect は Runtime が所有する
すべてのプラグイン作者が cleanup callback を手作業で一式維持すれば、理論的には動く。しかし、プラグイン内部に非同期初期化や入れ子のリソース、複数登録、エラー時のロールバックが増えていくと、ライフサイクルはすぐに整合性を失いやすい。
より安定したモデルは次の形になる。
プラグインの実行中に発生した Effect は、現在のランタイムユニットが所有する。
アンロード時は次のようになる。
dispose runtime unit
↓
revert owned effects
プラグインのライフサイクルは、「関数を呼び出す」から「リソースを所有するランタイムユニットを作る」に変わる。
Cordis はこのランタイムユニットを Fiber と呼ぶ。[2][3]
ただし、JavaScript の coroutine や Effect-TS の Fiber とは別の抽象概念である。Cordis の Fiber は、より Plugin Runtime Instance に近い。プラグインの構成情報、依存関係のスナップショット、ライフサイクル状態、現在所有している Effect を保持する。
DeepSeek Harness に同梱された Cordis の現行ソースでは、Fiber の状態は次を含む。
PENDING
LOADING
ACTIVE
FAILED
UNLOADING
DISPOSED
このことは、プラグインが loaded=true/false という真偽値ではなく、依存待ち、ロード、アンロード、失敗などを経験する状態機械であることを示す。[3]
可逆な副作用だけでは、依存関係を扱えない
2つのプラグインを考える。
Plugin A
└── provides FileSystem
Plugin B
└── requires FileSystem
B が A より先に起動したら、どうなるか。
従来の Plugin Manager はロード順序に依存するかもしれない。
A
↓
B
規模が大きくなるにつれ、手動による順序付けは次第に依存グラフ管理の問題になる。
さらに重要なのは、動的システムでは依存関係が実行中に変化することだ。
A は、B が動作し始めた後でもアンロードされることがある。
time ───────────────────────────>
A: ACTIVE ─────────── DISPOSED
B: ?
B が動き続けると、B が前提とする依存関係は失効している。Runtime が起動時にしか依存関係を確認しない場合も、問題は解決しない。
したがって、コンポーネントは「自分が何を変更するか」だけでなく、「環境から何を提供してもらう必要があるか」も宣言しなければならない。
論文では、後者の制約を Coeffect で表す。[1]
Effect は、コンポーネントが環境に与える作用を表す。
component → environment
Coeffect は、環境がコンポーネントへ提供しなければならない条件を表す。
environment → component
たとえば、次のような要件が考えられる。
requires FileSystem
requires ToolRegistry
requires SessionStore
これらの要件がすべて満たされるかどうかで、コンポーネントが現在実行可能かが決まる。
Reactive Coeffect が扱うのは依存関係の変化
ふつうの依存注入は、オブジェクトの生成時に行われる。
construct B(FileSystem)
オブジェクトを生成した後は、依存関係はほぼ固定される。
動的な Harness の要件は、次の流れに近い。
論文では、Context の変化に応じてコンポーネントの有効性を判断し直す仕組みを reactive coeffect と呼ぶ。[1]
ここでの「reactive」は重要だ。
依存関係の検査が一度きりのコンストラクタ検証ではなく、Runtime が継続的に維持する関係であることを意味するからだ。
Context changed
↓
which coeffects are affected?
↓
re-evaluate components
実装側がシステム全体を常にポーリングする必要はない。より合理的なのは、Service の登録・削除をきっかけに依存関係を更新することだ。
Cordis の現行実装はまさにイベント駆動だ。Service が変化すると、その Service に依存する Fiber に通知され、Fiber は自分の dependency epoch を再計算する。そのうえで、ACTIVE のまま維持するか、UNLOADING に入るか、再び LOADING に入るかを決める。[3]
したがって、「再計算」は、動き続ける while (true) ではない。
再計算は、環境に構造的な変化が起きたときに発生する。
なぜ Spatial Composability と呼ばれるのか
「空間」からは物理的な位置が想像されやすいが、ここでの意味は、同じ時点におけるコンポーネントどうしの組み合わせ関係に近い。
たとえば、次のような構成を考える。
A provides X
B requires X, provides Y
C requires Y
ある時点で有効なシステムは、インストール済みプラグインすべての集合とは限らない。現在の Context の中で満たせる依存関係によって決まる。
Installed:
A B C D E
Current Context:
A ACTIVE
B ACTIVE
C ACTIVE
D PENDING
E PENDING
X が消えたときは次のようになる。
A removed
↓
X disappears
↓
B loses requirement
↓
B unloads
↓
Y disappears
↓
C unloads
Spatial Composability が注目するのは、その時点でどのコンポーネントを共存させられるかだ。
Temporal Composability とつなげると、動的な変化の全体像になる。
依赖消失
↓
组件失效
↓
撤销组件 Effects
↓
组件提供的 Service 消失
↓
影响下一层依赖
ここには連鎖反応が生じるが、Runtime にシステム全体の再帰的な推論を要求するわけではない。Service を1つ巻き戻すと次の局所的な依存変化が生じ、システムが新しい安定状態に達するまで、その過程が続く。
Context が Effect と Coeffect を統一する必要がある理由
Effect と依存関係を別々の世界で管理すると、コンポーネントが変更しているのはどの環境で、依存関係が観察しているのはどの環境なのか、という难题が生じる。
論文は effect context と coeffect context を1つの Context 型に統合することを提案している。[1]
エンジニアリングの観点からは、次のように理解できる。
Context は同時に次の役割を担う。
- 利用可能な Service を検索する
- 新しい Service を登録する
- リスナーやその他の Effect を登録する
- これらの Effect がどのランタイムユニットに属するかを記録する
- 子 Scope / Isolation を形成する
- 構造変化後に関係する依存関係へ通知する
こうすると、プラグインがグローバルの Application、ServiceRegistry、LifecycleManager、EventBus などのオブジェクトを別々に取得する必要はなくなる。同じ Context を使って実行環境とやり取りし、Runtime はそのやり取りから生じるライフサイクル上の関係をまとめて追跡できる。
1つのコンポーネントからコンポーネントのシステムへ
論文が最終的に重視するのは、1つのプラグインを安全にアンロードできるかだけかではなく、複数のコンポーネントが錯綜して変化しても、これらの性質が成り立つかだ。
たとえば、次のような構成がある。
A provides X
B requires X -> provides Y
C requires X + Y
A をアンロードすると、次のようになる。
X removed
↓
B invalid
C invalid
↓
B effects reverted
C effects reverted
↓
Y removed
その後 A が戻ると、次のようになる。
X available
↓
B activatable
↓
B provides Y
↓
C activatable
システム全体が固定した起動順序に依存しているわけではない。その順序は、現在の Context における依存関係から自然に生じている。
「Composable」の真価はここにある。各コンポーネントは自分が提供するもの、必要なものを宣言するだけでよい。組み合わせた後のライフサイクルは Runtime が推論する。
このモデルが Agent Harness に適している理由
Agent Harness の機能は本質的に非常に動的だ。
LLM Provider は差し替えられる。Tool Provider は Session 単位で分離される。Sandbox は実行環境に応じて変化する。MCP Server は切断されることもある。SubAgent がある実行区間にしか存在しないこともある。UI Contribution もプラグインの起動・停止に応じて現れたり消えたりする。
これらの関係をすべて手動の起動順序と手動の cleanup で維持すれば、Harness 内にはすぐに大量のモジュール横断ライフサイクルコードが積み上がる。
時空の組み合せモデルは、別の構成方法を提供している。
この3つのオブジェクトが、Cordis の中核をほぼ構成している。
次回は実際のソースコードに入り、いくつかのより具体的な問いに答える。
- 依存している Service がアンロードされたとき、その Service に依存する Fiber はどう変化を検知するのか
- 下層の Service でなぜ連鎖的な無効化が起こるのか
- アンロード中に依存関係が戻った場合、Runtime はライフサイクルの競合をどう避けるのか
- DeepSeek Harness はこの仕組みに基づいて Everything is a Plugin をどう実装しているのか
参考資料
[1] A Programming Paradigm for Spatiotemporal Composability: https://github.com/cordiverse/paper
[2] Cordis リポジトリ: https://github.com/cordiverse/cordis
[3] DeepSeek Harness に同梱された Cordis: https://github.com/deepseek-ai/deepseek-harness/tree/master/vendor/cordis
[4] DeepSeek Harness Cordis Primer: https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/cordis-primer.md