Skip to content

CH 19 · プラグイン三形態:関数 / オブジェクト / クラス

全文字数2871 字所要時間約 20 分前提CH 18(最初のプラグイン)難易度手を動かせる

本章目標

CH 18 で書いたのはプラグインの最も一般的な書き方でした。本章ではプラグインの三つの形態(関数、オブジェクト、クラス)を一気にまとめます。それぞれがどういう姿をしているか、どれをいつ使うべきか、そしてなぜクラス形態が「公共の窓口を開く」ことになるのか——これはプラグイン同士が協調し合うための鍵です。本章を読み終えれば、一つのプラグインをどの形態で書くべきかが分かるようになり、ただ一つのテンプレートしか使えない状態から抜け出せます。

三形態を一覧で把握

まずは一覧表で全体像を掴み、詳細はひとつずつ下で分解します:

形態姿一言で言うと使う場面
関数export function apply(ctx) {}自分で一件の用事を済ませるデフォルトの選択肢、九割はこれで足りる
オブジェクトexport default { name, inject, apply(ctx) }名前とフローをまとめて持ち歩くプラグインに静的な宣言を添えたいとき
クラスexport default class extends Service {}公共の窓口を開く、誰もが来て用事を済ませられる他のプラグインから自分の機能を呼び出してほしいとき

判断は一つだけ覚えれば十分です:Agent に機能を足したいだけなら関数、他のプラグインに依存される側になりたいならクラス。オブジェクト形態は中間に位置し、機能的にはほぼ関数と同じで、name などの静的なメタデータが付くだけの違いです。

関数形態:もう使える

CH 18 で書いた hello-plugin がまさに関数形態です:

js
export function apply(ctx) {
  // Register your capabilities here
}

プラグイン読み込み時にフレームワークが apply(ctx) を呼び出し、コンテキストを渡してくれます。ほとんどのプラグイン——ツール登録、イベント監視、タイマー設置——はこの形で十分です。他のプラグインから依存されることが確定するまでは、常にまず関数形態で書き始めてください。

オブジェクト形態:名前とフローをまとめる

オブジェクト形態は nameinjectapply を一つのオブジェクトにまとめて export する書き方です:

js
export default {
  name: 'my-plugin',
  inject: ['tools'],
  apply(ctx) {
    // Here ctx.tools is guaranteed to be available
  },
}

これは関数形態とほぼ等価で、違いは静的な宣言(名前、依存関係)と apply を「パッケージ化された仕様書」としてまとめている点だけです。使いどころは? プラグインの「自分は誰か、どのサービスを使うか、何をするか」を一目で伝えたいとき、オブジェクト形態の方がすっきり書けます。機能面で関数形態にできないことはありません。

クラス形態:プラグインが「サービスを提供する」とき

本章のメインどころです。まず概念をひとつ確立しましょう:

Service(サービス)とは ctx にぶら下がった名前付き機能である。 みな日々サービスを使っています——ctx.tools(ツール)、ctx.llm(モデル)、ctx.agents(子 Agent)はすべてサービスです。どのプラグインでも自分のサービスを定義して、他のプラグインから呼び出してもらうことができます。

関数/オブジェクト形態は「自分で用事を済ませる」スタイル。クラス形態は公共の窓口を開くことで、誰もがそこに用事を頼みに来るスタイルです。例えるなら、関数形態はあなた自身が役所まで足を運んで自分の用事を済ますこと。クラス形態はあなた自身が役所の中に窓口を開き、他の人(他のプラグイン)がその窓口に申請書を出しにきて、結果を受け取るイメージです。

提供側:Service のサブクラスを書く

js
import { Service } from '@deepseek-ai/cordis'

export default class MetricsService extends Service {
  constructor(ctx) {
    super(ctx, 'metrics')  // Register a service called metrics
  }

  record(event, value) {
    console.log('[metrics]', event, value)
  }
}

押さえるべきポイントは二つです:

  • extends Service + super(ctx, 'metrics'):これで metrics という名前を ctx にぶら下げ、他のプラグインから ctx.metrics で参照できるようになります。
  • record(event, value) はこのサービスが外に公開するメソッドです——誰かが ctx.metrics.record(...) を呼ぶと、ここに到達します。

消費側:inject で依存を宣言する

js
export const name = 'consumer'
export const inject = ['metrics']

export function apply(ctx) {
  ctx.metrics.record('plugin_loaded', 1)
}

inject: ['metrics'] は「metrics というサービスを使いたい」という宣言です。フレームワークは次のことを保証します:apply が実行される時点で、inject に宣言されたサービスは必ず準備済みになっている。もし対象サービスがまだ用意されていなければ、あなたのプラグインは待機し、早すぎるタイミングで動き出すことはありません。

図一枚で協調関係を理解する

提供側が ctx にサービスをぶら下げ、消費側は依存を宣言したうえで直接呼び出します。toolsllmagents といった組み込み機能も本質は同じ仕組み——みなさんこれらを使うとき、まさに「消費側」になっているわけです。

実機演習:二つのプラグインに実際に対話をさせる

ここまでは概念の話でした。ここからは実際に提供側と消費側を書いて、本当に通話を成立させましょう。プラグインを置いておきたい作業ディレクトリで(まず cd してから)ディレクトリを作ります:

powershell
New-Item -ItemType Directory -Path "service-demo\src" -Force

第 1 ステップ:プラグインディレクトリで依存関係をインストール

クラス形態ではフレームワークの @deepseek-ai/cordis パッケージを import する必要があります。みなさんの作業ディレクトリにはまだ入っていないので、直接ロードすると Cannot find package '@deepseek-ai/cordis' と怒られます。先にプラグインディレクトリに入ってインストールしてしまいましょう:

powershell
cd "E:\software-workspace\DeepSeek harness demo\service-demo"   # replace with your directory
npm init -y
npm install @deepseek-ai/cordis

インストール後、package.json を開いて一番外側のオブジェクトに "type": "module" を一行追加してください——これを入れないと、Node がプラグインをロードするたびにモジュール形式を推測しようとして warning を大量に出します:

json
{
  "name": "service-demo",
  "version": "1.0.0",
  "type": "module"
}

第 2 ステップ:提供側を書く

service-demo\src\provider.js を新規作成します:

js
import { Service } from '@deepseek-ai/cordis'

export default class MetricsService extends Service {
  constructor(ctx) {
    super(ctx, 'metrics')
  }

  record(event, value) {
    console.log('[metrics]', event, value)
  }
}

第 3 ステップ:消費側を書く

service-demo\src\consumer.js を新規作成します:

js
export const name = 'consumer'
export const inject = ['metrics']

export function apply(ctx) {
  ctx.metrics.record('plugin_loaded', 1)
}

第 4 ステップ:宣言して検証する

service-demo\cordis.yml を新規作成します(パスは自分のものに置き換え、空白は %20 と書くことを忘れずに):

yaml
- insert:
    - id: provider
      name: 'file:///E:/your-workspace/service-demo/src/provider.js'
    - id: consumer
      name: 'file:///E:/your-workspace/service-demo/src/consumer.js'

headless でサクッと検証します:

powershell
cd your-workspace-directory
dsh --profile headless --patch "./service-demo/cordis.yml" "Just reply: hi"

出力にこう表示されます:

text
[metrics] plugin_loaded 1
hi

[metrics] plugin_loaded 1 は消費側が提供側のメソッドを呼んだ証拠です——二つのプラグインが本当に対話できたということです。これで関数・オブジェクト・クラスの三形態をすべて自分の手で検証できました。

ターミナル出力で黄色くハイライトされた [metrics] plugin_loaded 1 の行が、消費側が ctx.metrics.record(...) 経由で提供側のメソッドを呼んだ瞬間です——二つのプラグインが本当に対話を成立させました。

dsh に任せる:プロンプト一発で完結

ここまでのディレクトリ、依存、二つのファイルはすべて手作業で用意しました。例によって、プラグインを書く作業こそ dsh 自身が得意とする領域であり、「サービスをどう定義すべきか」「依存をどう注入すべきか」を dsh はみなさんより熟知しています。

Web UI の入力欄に次のプロンプトをそのまま貼り付けてください:

text
In my current workspace, help me write a "service form" plugin example: a service-providing plugin (Service subclass) and a plugin that uses it (inject depending on it), make them actually talk. First read the official dsh plugin dev docs to understand how to define services, how to inject dependencies, implement per the official spec, verify it can load and call normally, and finally tell me the result and where the files are.

dsh は自分で公式ドキュメントを読み込み、自分で依存をインストールし、自分で二つのプラグインを書き、自分で本当に通話を成立できるか検証してくれます。みなさんは dsh が書いたものを確認するだけで OK です。

よくある落とし穴

症状何が起きているか対処方法
Cannot find package '@deepseek-ai/cordis' と怒られるプラグインディレクトリに依存が入っていないプラグインディレクトリで npm install @deepseek-ai/cordis
MODULE_TYPELESS_PACKAGE_JSON の警告が大量に出るpackage.json でモジュール種別を宣言していない"type": "module" を追加
消費側がサービスを取得できないサービス名が一致していないsuper(ctx, 'xxx') の名前と inject: ['xxx'] が完全一致しているか確認
クラスプラグインが読み込み失敗extends Service がないか super を呼んでいないクラス形態では必ず Service を継承し、super(ctx, 'サービス名') を呼ぶ
ctx.metrics が undefined消費側が inject を宣言していない消費側で export const inject = ['metrics'] を書く

本章で学んだこと

下記をクリアできていれば合格です:

  • [ ] 三形態それぞれの姿と、使い分けのタイミングを言える
  • [ ] サービス = ctx にぶら下がった名前付き機能であることを理解し、tools/llm/agents もサービスだと分かる
  • [ ] Service のサブクラスを提供側として書き、super(ctx, '名前') でサービスを登録できる
  • [ ] inject を使う消費側を書き、apply 内で ctx.サービス名.メソッド() を直接呼び出せる
  • [ ] クラス形態のプラグインは事前にプラグインディレクトリへ @deepseek-ai/cordis をインストールしておく必要があると知っている

Open Source · MIT · Community Driven