ツールを呼び出せるAgent Core自体は複雑なものではない。最小実装なら、メッセージを維持してモデルへリクエストし、Tool Callを実行し、Tool Resultを次のリクエストに含めるだけだ。
Agent Frameworkを急速に膨張させる要素は、たいていLoopの外側に現れる。
Coding Agentはgk 파일システムツール、Shell、MCP、Skill、権限制御、モデル切り替え、Session、Compaction、コマンド、UI、テレメトリ、SubAgentなどを素早く追加していく。機能を1つ追加するたびに、実装の選択肢は2つある。Coreを修正し続けるか、安定した拡張の入口を用意して外部モジュールを接続するかだ。
Piは後者を選んだ。比較的集中したAgent Runtimeを維持しながら、多くの製品機能をExtensionによってCoreの外側に置いている。
本稿の主題はPiがどのようなプラグインAPIを用意しているかではない。Coreに入れるべき機能と、Harnessで拡張すべき機能を分ける、より根本的なフレームワーク設計の問題だ。
Tool Loopから膨らみ続けるCoreへ
第1ユニットでは、比較的安定したAgent Loopの境界が得られている。構造は次のとおりだ。
Context
↓
Model
↓
Tool
↓
Input Delivery
↓
Stop Condition
この構造ならAgentの1回の作業がどう進むかは説明できるが、完全な製品が持つすべての機能を収めるには足りない。
権限制御を例に取る。最直接には、各Toolの実行前に判定を追加すればよい。
次に監査も必要になる。
さらにHookを追加する。
さらにリモート実行を加える。
すべての機能をこの方式でメインの実行関数に入れていくと、Agent Loopは次第にシステム全体の統合点になる。どの機能からでも変更でき、どの変更も他の機能に影響する可能性がある。
ここでHarnessの最初の役割が現れる。
Coreが安定した実行セマンティクスを定義し、Harnessは外部機能をそのセマンティクスの境界に沿って組織する。
拡張システムの価値は『第三者によるプラグイン作成を許可する』ことではなく、Coreの膨らみを制御することにある。
PiのExtensionは安定したCore + 拡張の入口を選んだ
Piは自身を「最小限のターミナル向けコーディングHarness」と位置づけている。既定のCoding Agentは少数の基礎ツールしか提供せず、多くの挙動はExtensions、Skills、Prompt Templates、Packagesで追加される。[1][2]
ExtensionはToolを登録できる。
ライフサイクルも購読できる。
Command、Shortcut、Providerの登録や、実行中のSessionへのメッセージ注入にも使える。[1][3]
一見するとこれらのAPIはまったく異なるが、実際には2つの方向に整理できる。
1種類目はContributionだ。
Extension
↓
向 Runtime 注册一项能力
Tool
Command
Provider
Renderer
...
2種類目はHook / Interceptionだ。
Runtime lifecycle
↓
Extension observes / modifies / blocks
前者は『システムに何があるか』を、後者は『システムがある境界に達したときに何が起きるか』を拡張する。
したがって、Pi Extensionの中心構造は次のように抽象化できる。
ToolPlugin、CommandPlugin、HookPluginを別々に設計するより、このモデルのほうが統一的な開発モデルを生みやすい。Extension自身はクロージャーを保持し、同じExtension内のTool、Hook、Commandは状態を直接共有できる。
PiはかつてHookとカスタムToolを分けていたが、後にExtensionへ統一した。この変遷も同じ方向を示している。拡張単位は、登録の種類ではなく『1つの機能モジュール』を軸に構成すべきだ。[4]
ExtensionAPIは境界であって、コンテナではない
プラグインアーキテクチャでよく見られる落とし穴は、プラグインに『何でもできる』ことを許すため、Application全体や内部オブジェクトをそのまま公開してしまうことだ。
短期的には非常に柔軟だが、長期的には事実上の内部APIになる。プラグインはあらゆる実装詳細にアクセスできるようになり、Coreにはリファクタリングの余地がなくなる。
Piはプラグインの入口としてExtensionAPIを採用している。Extensionはこれを介して、機能の登録、メッセージ送信、管理下にある状態の読み取りや変更を行う。[1][3]
その関係は次のように理解できる。
Extension
│
▼
ExtensionAPI
│
▼
Runtime / Session / UI / Registry
APIの意味は、プラグインがどのシステム境界に影響できるかを規定することにある。
たとえば、次のようなAPIがある。
registerTool()
これによりExtensionは、モデルが呼び出せる機能を追加できる。
on('tool_call')
これによりExtensionは、ツール呼び出しの境界で制御に関与できる。
sendMessage()
これによりExtensionは、新しい入力をAgent Runtimeへ送れる。
これらの機能は強力だが、呼び出しはHarnessの定める入口を必ず通る。Core内部の状態構造はそのまま進化し続けていられる。
したがって、拡張APIでより重要なのは機能の境界が安定しているかであり、メソッド数の多さは表層的な問題にすぎない。
HookをObserverとしてだけ理解してはいけない理由
pi.on(...)はEvent Busのように誤解されやすいが、PiのHookによっては実行時の挙動を変更したり、実行そのものを止めたりできる。
Tool Call Hookは呼び出しを検査・変更・拒否できる。Context Hookは、モデルが次に目にするメッセージを調整できる。[1]
この種のHookはMiddleware / Interceptorに近い。
Runtime operation
↓
Extension A
↓
Extension B
↓
Core behavior
Observerの基本セマンティクスは、すでに起きた事実を観測することだ。
event happened
↓
listener notified
一方、Interceptorは実行チェーンの内部に立つ。
operation requested
↓
interceptor
↓
modify / block / continue
この違いはプラグインシステムの複雑さに直接影響する。
純粋なObserverで考えるべきなのは、通知と例外の隔離だけだ。Interceptorではさらに、順序、短絡、エラー伝播、非同期挙動、複数の拡張が同時に結果を変更したときの組み合わせセマンティクスを定義する必要がある。
Piの現在のtool_callには、前段のHandlerによる入力変更が後段のHandlerにも見えること、Handlerが戻り値によって呼び出しを止められることが明記されている。イベントは単純な放送ではなく、実行プロトコルの一部になっている。[1]
これもHarnessの設計で制御すべき境界だ。すべてのイベントに変更権を与えるべきではない。
拡張ポイントが観測だけ需要一个ならread-onlyのままにする。ポリシーの介入が本当に必要な実行境界にだけInterceptionを提供する。
動的登録が解決するのは実行時の拡張で、ライフサイクルは別の問題
Pi Extensionは実行中にToolを登録できる。登録後、新しいToolは現在のSessionのツール集合に追加される。
利用側から見れば、動的プラグインシステムに重要な機能がすでに一つ備わっている。
Runtime
│
├── Tool A
├── Tool B
│
└── Extension loads
↓
Tool C
ただし、動的に機能を『追加』できるのは、問題の半分にすぎない。
もっと複雑な問題は、機能がシステムから取り除かれるときに生じる。
あるExtensionが次を行うと仮定しよう。
register Tool A
register Command B
subscribe tool_call
start timer
provide service
プラグインをアンロードするとき、車はこれらの変更をすべて元に戻す必要がある。
依存関係をさらに増やすと、問題はさらに拡大する。
Plugin A
└── provides Service X
Plugin B
├── depends on Service X
└── provides Service Y
Plugin C
└── depends on Service Y
Aをアンロードしたあと、Bは動き続けるのか。Bが提供するYは有効なままなのか。Cはどうするのか。Aが再びロードされたら、BとCを再起動すべきなのか。
これらの問題はすでにExtension Registryそのものを超え、動的な組み合わせとライフサイクル管理の問題になっている。
Piは、比較的安定したRuntimeを軸にExtensionを組み立てる設計だ。次のように表現できる。
stable runtime
+
dynamic contributions
+
lifecycle hooks
DeepSeek Harnessはより攻めた構成を採用している。Agent Loop、Session、Tool Registry、LLM Adapterといった能力自体をプラグインが提供するのである。この場合、プラグインシステムはRuntimeの構造そのものの動的変化を引き受ける必要がある。[5]
CordisがDeepSeek Harnessで担う役割は、まさにこれだ。
Coreの境界を判断する一つの方法
ある機能をAgent Coreに入れるべきかどうかは、3つの問いで判断できる。
まず、その機能がすべてのAgent Stepの基本セマンティクスに関わるかどうか。
たとえばContextの構築、Model Request、Tool Resultを戻す処理、停止条件は、Loopの基本的な実行構造を構成する。これらを完全に取り除くと、Agent Loop自体が成立しなくなる。
次に、その機能に置き換え可能な実装が多くあるかどうか。
永続化、Sandbox、Model Adapter、Telemetryには、いずれも多数の実装がある。特定の実装をCoreに固定すれば、交換コストが急速に増える。
最後に、その機能が独立したライフサイクルを必要とするかどうか。
動的なロード、アンロード、分離、リロード、他の機能への依存を必要とする機能は、すでにコンポーネントとしての性質を持っているため、Harnessに任せるのが適している。
実用的な判断基準は次のようにまとめられる。
Agent Core
负责不可再分的执行语义
Harness
负责能力的组合、替换与生命周期
この境界はCoreを極端に小さくすることを要求しない。過剰に分割すれば、それだけ理解コストが増える。重要なのは、すべての製品機能が『同じLoopを書き換える』という方法でしか接続できないことを避けることだ。
Piは比較的保守的で、理解しやすい案を示した。安定したRuntimeを保ち、ExtensionからContributionsとHooksを拡張する。
DeepSeek Harnessが目指す範囲がさらに広がると、別の種類の問題が生じる。コンポーネントを実行中に出現させ、消失させ、互いに依存させる場合、組み合わせの関係をどう正しく保つべきか。
次回はCordisの背景にある論文『A Programming Paradigm for Spatiotemporal Composability』を扱う。この論文は、この問題に対してより一般的なモデルを組み立てようとしている。
参考文献
[1] Pi Extensions: https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md
[2] Pi Coding Agent: https://github.com/earendil-works/pi/blob/main/packages/coding-agent/README.md
[3] Pi Extension types: https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/extensions/types.ts
[4] Pi Extension unified design discussion: https://github.com/earendil-works/pi/issues/454
[5] DeepSeek Harness Architecture: https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/architecture.md