スポンサーLobeHubLobeHub詳しく見る
dshfind

第 11 課:プラグインコードの解剖:DSH パッケージはどんな姿をしているか

一言でいうと:DSH では、「エージェントに新しい能力を追加する」ことはソースコードを改変することではなく、パッケージを書くことです——src/index.tsname(私は誰か)、inject(何が必要か)、apply(何を提供するか)をエクスポートし、それを cordis.yml に登録すれば、フレームワークが ctx.use でそれをライフサイクルを持つ fiber としてインスタンス化します:ロードすれば即座に有効、アンロードすれば即座に元通り


1. ユーザーストーリー:DSH で「新しい能力を追加する」

DSH がインストールされ、Web UI が動いているマシンがあるとします。ここでエージェントにもう一つ芸を持たせたい:たとえば挨拶をする greet ツールや、他のプラグインのために記帳する metrics サービスなどです。従来のフレームワークなら、ソースコードを fork してメインループを改造する必要があったかもしれませんが、DSH では次の 3 つのことをするだけです:

  1. コードを書く:新しい TypeScript パッケージを作り、プラグインファイルを置く;
  2. 登録するcordis.yml というアセンブリファイルにそれを記載する;
  3. 起動する:フレームワークがプラグインをロードし、能力が即座に有効になります。

まず、公式チュートリアルによる「プラグインとは何か」の定義を見てみましょう(出典:docs/user/develop/basic/index.zh.md):

Harness では、プラグインとは apply 関数をエクスポートする TypeScript モジュールです。フレームワークはロード時に apply を呼び出し、ctx(コンテキストオブジェクト)を渡します。あなたは ctx を通じて能力を登録します。

最小のプラグインはこんな姿です——これがすべての構造です:

import type { Context } from 'cordis'

export const name = 'my-plugin'

export function apply(ctx: Context) {
  // Register capabilities here.
}

(出典:docs/user/develop/basic/index.zh.md

3 つの要素を一つずつ分解します:

要素何か一言でいうと
nameプラグインの名前。ローダーが診断に使う「私は誰か」
apply(ctx)フレームワークがロード時に呼び出すエフェクト関数「私は何をするか」——ctx に能力を登録する
ctxコンテキストオブジェクトプラグインとシステムが共有する「共有ホワイトボード」

他のプラグインが提供する能力(たとえばツールレジストリ tools)を使いたい場合は、inject を 1 行追加します:

import type { Context } from 'cordis'

export const name = 'my-tool-plugin'
export const inject = ['tools']

export function apply(ctx: Context) {
  // ctx.tools is ready here.
  ctx.tools.register(/* ... */)
}

(出典:docs/user/develop/basic/index.zh.md

inject の意味は「私はこれらに依存している」——フレームワークはこれらの依存が準備できてからあなたの apply を実行することを保証します。まだ準備ができていなければ、プラグインは待ちます。早まって実行されることはありません。

💡 この最小スケルトンを覚えておいてください:name + inject + apply。これ以降の各セクションは、このスケルトンに何かを追加していくだけです。


2. パッケージの物理構造:ディレクトリ、ファイル、アセンブリ登録

「パッケージを書く」とは、具体的にファイルをどこに置くことなのでしょうか?リポジトリの実践マニュアルがファイルごとのチェックリストを示しています(出典:docs/cookbook/adding-a-package.md):

packages/<group>/<pkg>/
  package.json     # パッケージ名、依存関係、ビルド成果物のエントリ
  tsconfig.json    # TypeScript コンパイル設定
  src/index.ts     # service のデフォルトエクスポートまたはプラグイン(name/inject/apply/Config)
  README.md        # サービス API、イベント、拡張ポイント、設計ノート

ここで「グループ」は純粋なコンテナにすぎません——リポジトリは能力ファミリーごとにパッケージを corellmshellcompactionsubagenttodoutil などのグループに分類し、各パッケージはちょうどグループの 1 階層下に置かれます。3 つのコアファイルにはそれぞれ役割があります:

ファイル何をするかたとえ
src/index.tsプラグインのすべてのロジック:name / inject / apply をエクスポートするか、サービスクラスをデフォルトエクスポートするエンジン
package.jsonパッケージ名(@deepseek-ai/dsh-xxx のような形)、バージョン、依存関係、ビルドエントリ銘板と原材料表
README.md人間向けの説明書:API、イベント、設計ノートユーザーマニュアル

実際のリポジトリはこんな姿:fs 能力ファミリー

DSH リポジトリの packages/fs/ ディレクトリを開くと、一つの能力が複数のパッケージに分割されているのが見えます——これこそ第 2 課で学んだ「シーム(seam)」思想の実践です(出典:packages/fs/README.zh.md):

パッケージ役割ctx キー
fs/Service Definition:パスの正規化、テキスト I/O、アトミックな変更プリミティブ。fs/* ポリシーイベントを所有ctx.fs
fs-local/ローカルファイルシステムの実装ctx.fs を登録)
fs-sandbox/強制サンドボックスの実装:モードとワークスペースルートのポリシーで書き込み/編集を制約ctx.fs を登録)
fs-observation-policy/ポリシーゲートプラグイン:fs/* イベントゲートを通じて編集前読み取りなどを提供(サービスなし、リスナーのみ)
tool-fs/モデル向けの read/write/edit ツールとエグゼキュータctx.tools に登録)
tool-fs-search/モデル向けの glob/grep 探索ツールctx.tools に登録)

役割分担に注目してください:定義fs/)は「ファイルシステム能力がどんな姿か」を規定するだけで、プロバイダーfs-local/fs-sandbox/)がそれぞれ実装し、コンシューマーtool-fs/)はモデル向けにツールを登録するだけです。サンドボックス実装を入れ替えたい?プロバイダーパッケージを差し替えるだけで、定義、ポリシー、ツールスキーマは一行も変更不要です。

cordis.yml に登録する:プラグインをシステムに「装着」する

パッケージは書けました。では、どうすれば実行中の DSH に登場させられるでしょうか?答えはアセンブリファイル cordis.yml です。ローカル開発では、insert で現在のアセンブリに差し込みます(出典:docs/user/develop/basic/index.zh.md):

- insert:
    - id: hello
      name: './src/my-plugin.ts'

そしてこのオーバーレイを付けて起動します:

pnpm run dsh web --patch ./scratch-plugin/cordis.yml

リポジトリ付属の examples/web-cordis/cordis.yml も同じパターンです——insert@deepseek-ai/dsh-tool-cordis を挿入しています(出典:examples/web-cordis/cordis.yml)。本番環境のアセンブリは、このようなプラグインを順番に並べたリストで、フレームワークはその地図どおりにロードし、依存を解決し、インスタンス化します。

📦 このパッケージを他人がインストールできる正式パッケージとして公開したい?それは「実践マニュアル」の仕事です:docs/cookbook/adding-a-package.md に完全なファイルごとのチェックリストがあります(package.json 不変条件、ルート設定への登録、検証コマンド pnpm run constraints && pnpm run typecheck && pnpm run build)。この課ではまず「パッケージがどんな姿か」を理解し、次の章で実際に手を動かして書きます。


3. コンポーネント定義の核心:inject、apply、そして ctx.use の fiber

ここで「プラグイン」という言葉をその学名——コンポーネント(component)——に置き換えましょう。第 2 章の論文精読で学んだように、コンポーネント定義は 2 つの半分から組み立てられます:

半分学名日常表現コードでの姿
inject依存宣言 d私に必要なものexport const inject = ['tools', 'fs']
applyエフェクト関数 e私が提供するものexport function apply(ctx, config) { ... }

これが DSH の実際の本番パッケージの姿です(出典:packages/fs/tool-fs/src/index.ts):

/** Cordis plugin name used by loader diagnostics. */
export const name = 'tool-fs'

/** Services required by the filesystem tool suite. */
export const inject = ['tools', 'fs', 'systemPrompt']

/** Register the full `read`/`write`/`edit` filesystem tool suite. */
export function apply(ctx: Context, config: Config): void {
  // schemastery (Config) has already filled every defaulted field.
  const resolved = config as ResolvedConfig
  assertPositiveInteger('readLimit', resolved.readLimit)
  applyReadTool(ctx, { /* ... */ })
  const sandbox = new FsSandboxSurface(ctx)
  applyWriteTool(ctx, sandbox)
  applyEditTool(ctx, sandbox)
}

(出典:packages/fs/tool-fs/src/index.ts、一部の実装詳細は省略)

この実際のコードを読み解きましょう:「私は tools(ツールレジストリ)、fs(ファイルシステム能力)、systemPrompt(システムプロンプトの組み立て)が必要」と宣言し、apply の中で一気に 3 つのツールを提供しています(読み取り、書き込み、編集)。apply は第 2 引数 config も受け取れることに注目してください——プラグインはこれでユーザー設定を受け付けられます(tool-fs は schemastery の z.object で設定項目とデフォルト値を宣言しています)。

ctx.use:コンポーネント定義を fiber に「インスタンス化」する

name + inject + apply設計図にすぎません。設計図を動く機械に変えるのが ctx.use の仕事です——これはコンポーネントを fiber(ファイバー)、つまり完全なライフサイクルを持つランタイムオブジェクトにインスタンス化します:

组件定义inject(我需要)依赖声明 dapply(我贡献)效应函数 ectx.use实例化fiber(运行时)parent / ctx(子上下文)epoch(目标状态版本)dispose(累积逆函数)inertia(迁移句柄)

组件 = 声明我需要什么 + 贡献什么;ctx.use 把它变成带生命周期的 fiber

fiber が携える 5 つのものは、ちょうど第 2 章の論文の概念のコード形態です:

fiber 上のフィールド何を保持するか論文での名前
parent親コンテキスト、誰がこれをインスタンス化したかコンテキストタワーの階層
ctx親から派生した子コンテキストコンポーネント専用のホワイトボード
epoch目標状態の「バージョン番号」。依存が変われば変わる𝜀𝑑(𝜎)
dispose累積された「取り消しリスト」(逆関数)recover
inertia進行中のマイグレーションハンドル慣性状態マシン
  • 依存の変化epoch が変わる → フレームワークがこの fiber をリロードするかアンロードするかを決定;
  • アンロードdispose に蓄積された逆関数を実行 → コンポーネントが登録したすべてが取り消される;
  • マイグレーションは始まったら最後まで走り切る——これが「慣性」です。

🔗 これはまさに第 2 章「論文精読」の第 10、11 課で徹底的に学んだメカニズムです:コンポーネント = 必要なものの宣言 + 提供するもの、ctx.use がそれをライフサイクル付きの fiber に変える。DSH のすべてのプラグインパッケージは、本質的に一つ以上のコンポーネント定義です——いま見ているのは、この理論が本番リポジトリで実際に使われている姿です。


4. ツール、サービス、ライフサイクル:システムに登録し、アンロードで元通り

4.1 ツール:ctx.tools に登録する(schema + 実行関数)

最も一般的なプラグインはツールプラグインです:ctx.tools に「仕様書 + エグゼキュータ」を登録します。これは公式チュートリアルの greet ツールです(出典:docs/user/develop/basic/tool.zh.md):

import type { Context } from 'cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'

export const name = 'greet-tool'
export const inject = ['tools']

export function apply(ctx: Context) {
  ctx.tools.register(defineTool({
    name: 'greet',
    description: 'Greet someone by name.',
    parameters: {
      name: { type: 'string', required: true, description: 'The name to greet' },
    },
    output: {
      schema: { type: 'string' },
      render: (_args, value) => [{ type: 'text', text: value }],
    },
    async execute(args) {
      return `Hello, ${args.name}!`
    },
  }))
}

(出典:docs/user/develop/basic/tool.zh.md

defineTool はツールを 3 つのブロックに定義します:parameters(パラメータスキーマ。モデルが見る「仕様書」で、args の型の推論と検証も担当)、execute(実際に仕事をする実行関数)、output(戻り値の正規スキーマの宣言 + 値をモデルが見える内容にレンダリングする render)。登録後、スキーマは自動的にシステムプロンプトの組み立てに流れ込み——モデルは次のリクエストでこのツールを見て呼び出せるようになります。

4.2 サービス:能力定義、プロバイダー、コンシューマーの 3 つの役割

他のプラグインにあなたの能力を使ってほしいなら、サービスを提供します(出典:docs/user/develop/framework/service.zh.md):

サービスとは、プラグインが他のプラグインに公開する能力です。inject はプラグインがどのサービスを必要とするかを宣言します。Harness では、toolsllmagents はすべてサービスです——サービスとは ctx にマウントされた名前付きの能力です。

サービスの提供には Service 基底クラスを使います——tool-fs にあった ctx.fs を覚えていますか?あれは誰かが Service で提供したものです:

import { Service, type Context } from 'cordis'

export default class MetricsService extends Service {
  static inject = ['llm']  // A service may depend on other services.

  constructor(ctx: Context) {
    super(ctx, 'metrics')  // 'metrics' is the service name.
  }

  // Public service method.
  record(event: string, value: number) {
    // ...
  }
}

(出典:docs/user/develop/framework/service.zh.md

このプラグインがロードされると、消費側は ctx.metrics を通じてアクセスできます:

export const inject = ['metrics']

export function apply(ctx: Context) {
  ctx.metrics.record('tool_call', 1)
}

(出典:docs/user/develop/framework/service.zh.md

すべての能力には 3 つの役割があります(第 2 課で説明済み):

Service Definition(能力定義:この能力はどんな姿か)
        ↕
Service Provider(プロバイダー:誰が仕事をするか)
        ↕
Consumer(コンシューマー:誰が使うか)

第 2 節の fs 能力ファミリーの表に戻りましょう:fs/ が Definition、fs-local/fs-sandbox/ が Provider、tool-fs/ が Consumer——3 端が分離され、どの端も単独で交換できます。リポジトリの実践マニュアルの原文(出典:docs/cookbook/adding-a-package.md):

交換可能な能力について、Service Definition/Service Provider/Consumer の各役割を独立に進化させる必要がある場合は、それらを別々のパッケージに分割します——bash の 3 コンポーネントがテンプレートです。

4.3 ライフサイクルの自動管理:ロードで即有効、アンロードで元通り

コンポーネントがシステムに登録された後、ライフサイクルはあなたが管理する必要がありません。公式チュートリアルの原文(出典:docs/user/develop/basic/index.zh.md):

ctx を通じて登録されたものはすべて——イベントリスナー、ツール、タイマー——プラグインのアンロード時に自動的にクリーンアップされます。手動で removeListener や clearInterval を呼ぶ必要はありません。

これは fiber の dispose が働いているのです:ツールの登録自体が副作用であり、プラグインのアンロード = この fiber の dispose = ツールの自動登録解除です。ツールリファレンスマニュアルの一節(出典:docs/cookbook/adding-a-tool.md):

登録は副作用ベースです:プラグイン fiber を dispose(リソース解放)すれば、そのツールは登録解除されます。

依存関係のライフサイクルも同様に自動です(出典:docs/user/develop/framework/service.zh.md):アプリの実行中に必須サービスが消えた場合(たとえばそのプロバイダーがアンロードされた)、それに依存するプラグインは自動的に dispose されます。サービスが再び現れると、プラグインは自動的に再ロードされます

手動管理が必要な少数のリソース(たとえばネットワーク接続)だけが、ctx.effect() でフレームワークにクリーンアップ方法を伝えます——返される関数こそが「取り消し手順書」であることに注目してください:

export function apply(ctx: Context) {
  ctx.effect(() => {
    const timer = setInterval(() => {
      console.log('heartbeat')
    }, 5000)

    // The returned function runs when the plugin unloads.
    return () => clearInterval(timer)
  })
}

(出典:docs/user/develop/basic/index.zh.md

🔁 第 2 章との呼応:Cordis の「時空間合成可能性」の約束は、装着でき、取り外せ、取り外したら跡を残さない——ロードで即有効、アンロードで元通り。プラグインがどれほど複雑でも、システムへのすべての変更は記帳され、アンロード時に一括で精算されます。これこそ、DSH がエージェントに「自己言及的な改変」を許せる根拠です。


重要ポイントの振り返り

  • プラグイン = apply 関数をエクスポートする TypeScript モジュールname(私は誰か)、inject(何が必要か。フレームワークが準備完了を保証してから実行)、apply(何を提供するか。ctx に能力を登録)。
  • パッケージの物理構造packages/グループ/パッケージ名/ の下に置かれ、コアは src/index.tspackage.jsonREADME.md。アセンブリは cordis.ymlinsert で登録(ローカルでは dsh web --patch で有効化)。
  • コンポーネント定義 = inject(依存宣言 d)+ apply(エフェクト関数 e)ctx.use が定義を fiber にインスタンス化し、parent / ctx(子コンテキスト)/ epoch / dispose / inertia の 5 つのライフサイクルフィールドを携えます。
  • ツールとサービス:ツール = ctx.tools.register(defineTool({ parameters, execute, output }));サービス = Service 基底クラスを ctx にマウント。能力は Definition / Provider / Consumer の 3 役割に分割されます。
  • ライフサイクルの自動管理:ロードで即有効、アンロードで元通り——ツール登録、イベントリスナー、タイマーはすべて fiber の dispose で自動クリーンアップ。依存が消えれば自動アンロード、復活すれば自動リロード。ctx.effect() が少数の手動リソースを処理します。
  • 公開はラストワンマイルdocs/cookbook/adding-a-package.md のファイルごとのチェックリストに従って manifest と検証を整えれば、他人がインストールできる @deepseek-ai/dsh-xxx パッケージになります。

🚀 この課では「パッケージ」を外から内まで一通り見ました:ディレクトリ、ファイル、コンポーネント定義、fiber、ツールとサービス。次の章「第 4 章 · プラグイン開発実践」ではコードを見るのではなく、自分の手で書きます:ゼロから最初のプラグインを作り、Web UI で動かし、設定、ホットリロード、公開へと一歩ずつ進みます。

セルフチェック · プラグインコードの解剖

回答を終えたら「解答を送信」をクリックすると、正誤と解説を確認できます。

1. プラグインの inject と apply について、正しい説明はどれですか?
2. ctx.use はコンポーネント定義を何に変えますか?
3. 交換可能な能力(seam)はどの 3 つの役割で構成されますか?
4. DSH パッケージの物理構造とアセンブリについて、正しい説明はどれですか?