CH 19 · プラグイン三形態:関数 / オブジェクト / クラス
本章目標
CH 18 で書いたのはプラグインの最も一般的な書き方でした。本章ではプラグインの三つの形態(関数、オブジェクト、クラス)を一気にまとめます。それぞれがどういう姿をしているか、どれをいつ使うべきか、そしてなぜクラス形態が「公共の窓口を開く」ことになるのか——これはプラグイン同士が協調し合うための鍵です。本章を読み終えれば、一つのプラグインをどの形態で書くべきかが分かるようになり、ただ一つのテンプレートしか使えない状態から抜け出せます。
三形態を一覧で把握
まずは一覧表で全体像を掴み、詳細はひとつずつ下で分解します:
| 形態 | 姿 | 一言で言うと | 使う場面 |
|---|---|---|---|
| 関数 | export function apply(ctx) {} | 自分で一件の用事を済ませる | デフォルトの選択肢、九割はこれで足りる |
| オブジェクト | export default { name, inject, apply(ctx) } | 名前とフローをまとめて持ち歩く | プラグインに静的な宣言を添えたいとき |
| クラス | export default class extends Service {} | 公共の窓口を開く、誰もが来て用事を済ませられる | 他のプラグインから自分の機能を呼び出してほしいとき |
判断は一つだけ覚えれば十分です:Agent に機能を足したいだけなら関数、他のプラグインに依存される側になりたいならクラス。オブジェクト形態は中間に位置し、機能的にはほぼ関数と同じで、name などの静的なメタデータが付くだけの違いです。
関数形態:もう使える
CH 18 で書いた hello-plugin がまさに関数形態です:
export function apply(ctx) {
// Register your capabilities here
}プラグイン読み込み時にフレームワークが apply(ctx) を呼び出し、コンテキストを渡してくれます。ほとんどのプラグイン——ツール登録、イベント監視、タイマー設置——はこの形で十分です。他のプラグインから依存されることが確定するまでは、常にまず関数形態で書き始めてください。
オブジェクト形態:名前とフローをまとめる
オブジェクト形態は name、inject、apply を一つのオブジェクトにまとめて export する書き方です:
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 のサブクラスを書く
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 で依存を宣言する
export const name = 'consumer'
export const inject = ['metrics']
export function apply(ctx) {
ctx.metrics.record('plugin_loaded', 1)
}inject: ['metrics'] は「metrics というサービスを使いたい」という宣言です。フレームワークは次のことを保証します:apply が実行される時点で、inject に宣言されたサービスは必ず準備済みになっている。もし対象サービスがまだ用意されていなければ、あなたのプラグインは待機し、早すぎるタイミングで動き出すことはありません。
図一枚で協調関係を理解する
提供側が ctx にサービスをぶら下げ、消費側は依存を宣言したうえで直接呼び出します。tools、llm、agents といった組み込み機能も本質は同じ仕組み——みなさんこれらを使うとき、まさに「消費側」になっているわけです。
実機演習:二つのプラグインに実際に対話をさせる
ここまでは概念の話でした。ここからは実際に提供側と消費側を書いて、本当に通話を成立させましょう。プラグインを置いておきたい作業ディレクトリで(まず cd してから)ディレクトリを作ります:
New-Item -ItemType Directory -Path "service-demo\src" -Force第 1 ステップ:プラグインディレクトリで依存関係をインストール
クラス形態ではフレームワークの @deepseek-ai/cordis パッケージを import する必要があります。みなさんの作業ディレクトリにはまだ入っていないので、直接ロードすると Cannot find package '@deepseek-ai/cordis' と怒られます。先にプラグインディレクトリに入ってインストールしてしまいましょう:
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 を大量に出します:
{
"name": "service-demo",
"version": "1.0.0",
"type": "module"
}第 2 ステップ:提供側を書く
service-demo\src\provider.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 を新規作成します:
export const name = 'consumer'
export const inject = ['metrics']
export function apply(ctx) {
ctx.metrics.record('plugin_loaded', 1)
}第 4 ステップ:宣言して検証する
service-demo\cordis.yml を新規作成します(パスは自分のものに置き換え、空白は %20 と書くことを忘れずに):
- 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 でサクッと検証します:
cd your-workspace-directory
dsh --profile headless --patch "./service-demo/cordis.yml" "Just reply: hi"出力にこう表示されます:
[metrics] plugin_loaded 1
hi[metrics] plugin_loaded 1 は消費側が提供側のメソッドを呼んだ証拠です——二つのプラグインが本当に対話できたということです。これで関数・オブジェクト・クラスの三形態をすべて自分の手で検証できました。

ターミナル出力で黄色くハイライトされた [metrics] plugin_loaded 1 の行が、消費側が ctx.metrics.record(...) 経由で提供側のメソッドを呼んだ瞬間です——二つのプラグインが本当に対話を成立させました。
dsh に任せる:プロンプト一発で完結
ここまでのディレクトリ、依存、二つのファイルはすべて手作業で用意しました。例によって、プラグインを書く作業こそ dsh 自身が得意とする領域であり、「サービスをどう定義すべきか」「依存をどう注入すべきか」を dsh はみなさんより熟知しています。
Web UI の入力欄に次のプロンプトをそのまま貼り付けてください:
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をインストールしておく必要があると知っている
