DeepSeek Harness: ケイパビリティ3役
最終更新:2026-08-31
Cordis のケイパビリティシステムは「すべてはプラグイン」の基盤です。機能を3つの役割に分割します:インターフェースを定義する者、実装する者、使用する者。この分離により、実装の置き換えは電池の交換のように簡単——古いものを引き抜き、新しいものを差し込めば、システムは正常に動作します。
📋 前提知識:19-service.md の完了、Service 基底クラスを理解していること
1. 学習内容
- Definition:インターフェースの宣言
- Provider:インターフェースの実装
- Consumer:インターフェースの使用
- seam 概念
- ケイパビリティ登録グラフ
- Provider の置き換え = 製品全体の動作変更
2. ケイパビリティシステム概要
(1) なぜ3つの役割か
ケイパビリティシステムなしでは、機能は直接ハードコードされます:
// ❌ ハードコードされた実装
class FileAnalyzer {
analyze(path: string) {
const stat = fs.statSync(path) // ローカルファイルシステムしか使えない
return { size: stat.size, type: 'local' }
}
}
ケイパビリティシステムがあれば、機能は3層に分割されます:
// ✅ 3役分離
// Definition:インターフェース宣言
interface FileStats {
getSize(path: string): Promise<number>
getType(path: string): Promise<string>
}
// Provider:実装(ローカル)
class LocalFileStats implements FileStats { ... }
// Provider:実装(リモートサンドボックス)
class SandboxFileStats implements FileStats { ... }
// Consumer:使用(具体的な実装を気にしない)
class FileAnalyzer {
constructor(private stats: FileStats) {}
analyze(path: string) {
const size = await this.stats.getSize(path)
return { size }
}
}
▶ サンプル 2:
graph LR
DEF[Definition<br/>インターフェース宣言] --> PROV[Provider<br/>インターフェース実装]
PROV --> CON[Consumer<br/>インターフェース使用]
CON --> DEF
(3) 例え
| 役割 | 例え | 実世界の対応 |
|---|---|---|
| Definition | コンセント規格 | 国のコンセント仕様 |
| Provider | コンセントの実装 | 壁のコンセント |
| Consumer | 電気機器 | テレビ、冷蔵庫 |
テレビは電力がどの発電所から来るか気にしません——コンセントが規格を満たしているかだけを気にします。同様に、Consumer は Provider が誰かを気にしません——Definition が定義するインターフェースだけを気にします。
3. Definition:インターフェースの宣言
(1) ケイパビリティの定義
Definition はケイパビリティのインターフェースを宣言——実装を含まず、「このケイパビリティで何ができるか」を記述するだけ:
import { defineCapability } from '@deepseek-ai/cordis'
export const FileStats = defineCapability({
name: 'file-stats',
description: 'File statistics and metadata access',
interface: {
getSize(path: string): Promise<number>
getType(path: string): Promise<string>
exists(path: string): Promise<boolean>
list(dir: string): Promise<string[]>
}
})
(2) Definition の要素
| 要素 | 説明 |
|---|---|
name |
ケイパビリティ識別子、グローバルに一意 |
description |
ケイパビリティの説明 |
interface |
TypeScript インターフェース定義 |
(3) なぜ Definition を分離するか
Definition と Provider を分離する利点:
- 型制約:Provider はすべてのインターフェースメソッドを実装しなければならない
- ドキュメント価値:Definition はケイパビリティの使用ドキュメント
- 置換可能性:インターフェースを満たす新しい Provider が古いものを置き換え可能
- コンパイル時チェック:TypeScript がインターフェース整合性を保証
(4) 規約
Definition は通常 Provider とは別ファイルに配置:
capabilities/
├── file-stats/
│ ├── definition.ts ← Definition
│ ├── local.ts ← Provider(ローカル実装)
│ └── sandbox.ts ← Provider(サンドボックス実装)
4. Provider:インターフェースの実装
(1) ケイパビリティの実装
Provider は Definition が宣言したインターフェースを実装:
import { FileStats } from './definition'
export default class LocalFileStatsProvider extends Service {
static inject = ['fs']
constructor(ctx: Context) {
super(ctx, 'file-stats')
ctx.implement(FileStats, {
async getSize(path: string) {
const stat = await ctx.fs.stat(path)
return stat.size
},
async getType(path: string) {
const stat = await ctx.fs.stat(path)
return stat.isDirectory ? 'directory' : 'file'
},
async exists(path: string) {
try {
await ctx.fs.stat(path)
return true
} catch {
return false
}
},
async list(dir: string) {
const entries = await ctx.fs.readdir(dir)
return entries.map(e => e.name)
}
})
}
}
(2) ctx.implement()
ctx.implement(capability, implementation) は実装をケイパビリティに登録:
ctx.implement(FileStats, {
getSize: async (path) => { ... },
getType: async (path) => { ... },
// すべてのインターフェースメソッドを実装しなければならない
})
メソッドが欠けていると、TypeScript がコンパイル時エラーを報告します。
▶ サンプル 3:
ローカル Provider:
export default class LocalFileStatsProvider extends Service {
constructor(ctx: Context) {
super(ctx, 'file-stats')
ctx.implement(FileStats, {
async getSize(path) {
const stat = await ctx.fs.stat(path)
return stat.size
},
async getType(path) {
return (await ctx.fs.stat(path)).isDirectory ? 'directory' : 'file'
},
async exists(path) {
try { await ctx.fs.stat(path); return true } catch { return false }
},
async list(dir) {
return (await ctx.fs.readdir(dir)).map(e => e.name)
}
})
}
}
リモートサンドボックス Provider:
export default class SandboxFileStatsProvider extends Service {
constructor(ctx: Context) {
super(ctx, 'file-stats')
ctx.implement(FileStats, {
async getSize(path) {
const resp = await fetch(`http://sandbox:8080/stat?path=${path}`)
return (await resp.json()).size
},
async getType(path) {
const resp = await fetch(`http://sandbox:8080/stat?path=${path}`)
return (await resp.json()).type
},
async exists(path) {
const resp = await fetch(`http://sandbox:8080/exists?path=${path}`)
return (await resp.json()).exists
},
async list(dir) {
const resp = await fetch(`http://sandbox:8080/ls?dir=${dir}`)
return (await resp.json()).entries
}
})
}
}
5. Consumer:インターフェースの使用
(1) ケイパビリティの消費
Consumer は inject で依存を宣言し、ctx を通じてケイパビリティを使用:
export const inject = ['file-stats']
export function apply(ctx: Context) {
ctx.tools.register(defineTool({
name: 'file_info',
description: 'Get file information',
parameters: {
type: 'object',
properties: {
path: { type: 'string', description: 'File path' }
},
required: ['path']
},
async execute({ path }, ctx) {
const size = await ctx['file-stats'].getSize(path)
const type = await ctx['file-stats'].getType(path)
return { path, size, type }
}
}))
}
(2) Consumer は Provider を知らない
Consumer は Definition が定義するインターフェースにのみ依存し、具体的な実装には依存しない:
// 下層が Local でも Sandbox でも、Consumer コードは同一
const size = await ctx['file-stats'].getSize(path)
▶ サンプル 3:
// 型拡張による
declare module '@deepseek-ai/cordis' {
interface Context {
'file-stats': {
getSize(path: string): Promise<number>
getType(path: string): Promise<string>
exists(path: string): Promise<boolean>
list(dir: string): Promise<string[]>
}
}
}
6. seam 概念
(1) seam とは
seam は Definition と Provider の接合点——システムが「引き抜いて置き換え」できる箇所です:
graph LR
CON[Consumer] -->|依存| DEF[Definition<br/>seam]
DEF -->|実装| PROV_A[Provider A<br/>ローカル実装]
DEF -.->|置き換え| PROV_B[Provider B<br/>サンドボックス実装]
(2) seam の価値
seam なし:
Consumer → Provider A (ハードコード、置き換え不可)
seam あり:
Consumer → Definition(seam)→ Provider A
(seam)→ Provider B (置き換え!)
seam はあらゆるケイパビリティポイントでシステムを置換可能にします。
(3) 良い seam の識別
| 基準 | 良い seam | 悪い seam |
|---|---|---|
| 抽象レベル | 適切 | 細かすぎまたは粗すぎ |
| 実装数 | 複数あり得る | 1つしかあり得ない |
| 変更頻度 | 実装が変更される可能性 | 実装が永不変 |
| 依存方向 | Consumer がインターフェースに依存 | Consumer が実装に依存 |
(4) seam の粒度
粗い seam:FileSystem (ファイルシステム全体が置き換え可能)
中位の seam:FileStats (ファイル統計が置き換え可能)
細かい seam:FileSize (ファイルサイズ照会が置き換え可能)
粗すぎ → 置換コスト高;細かすぎ → インターフェース断片化。中位の粒度を選択。
7. ケイパビリティ登録グラフ
(1) 登録フロー
graph TB
DEF[defineCapability<br/>インターフェース宣言] --> REG[Definition を登録]
REG --> PROV1[Provider A 実装]
REG --> PROV2[Provider B 実装]
PROV1 --> ACTIVE_A[現在アクティブ:A]
PROV2 --> WAIT_B[待機中:B]
ACTIVE_A --> CONSUMER[Consumer が使用]
(2) 置換フロー
graph LR
OLD[Provider A<br/>現在アクティブ] -->|アンロード| INACTIVE_A[非アクティブ化]
NEW[Provider B<br/>新規登録] -->|実装| ACTIVE_B[現在アクティブ]
ACTIVE_B --> CONSUMER[Consumer<br/>自動切替]
(3) マルチケイパビリティ連携
graph TB
FS_DEF[FileStats Definition] --> FS_PROV[FileStats Provider]
DB_DEF[Database Definition] --> DB_PROV[Database Provider]
FS_PROV --> TOOL[file_info Tool]
DB_PROV --> TOOL
TOOL --> AGENT[Agent]
8. Provider の置き換え = 製品全体の動作変更
(1) コアバリュー
これがケイパビリティシステムの最も強力な機能:
シナリオ:ローカル開発からサンドボックス実行に切り替え
1. LocalFileStatsProvider をアンロード
2. SandboxFileStatsProvider をロード
3. すべての Consumer が自動的にサンドボックス実装を使用
4. Consumer コード:変更ゼロ
(2) 設定による切り替え
# ローカル開発
plugins:
file-stats:
$insert:./providers/local-file-stats
# サンドボックス環境(この1行だけ変更)
plugins:
file-stats:
$replace:./providers/sandbox-file-stats
(3) ランタイム切り替え
// $replace で動的に切り替え
ctx.on('config/updated', (config) => {
if (config.environment === 'sandbox') {
// フレームワークが自動リロード、Sandbox Provider に切替
}
})
(4) A/B テスト
# グループ A:ローカル実装
realms:
group-a:
plugins:
file-stats:
$insert:./providers/local-file-stats
group-b:
plugins:
file-stats:
$insert:./providers/sandbox-file-stats
❓ よくある質問
undefined のケースを処理すべきです。📖 まとめ
- ケイパビリティ3役:Definition(インターフェース宣言)、Provider(インターフェース実装)、Consumer(インターフェース使用)
- seam は Definition と Provider の接合点で、システムの置換可能性の鍵
- Provider 置き換え後、Consumer は自動的に新実装を使用、コード変更ゼロ
- 良い seam は中位の粒度と適切な抽象を選択
- ケイパビリティシステムはマルチデプロイ環境、A/B テスト、拡張可能アーキテクチャに適する
📝 練習問題
1. ⭐ 基礎:TimeService ケイパビリティ(Definition)を定義し、now():number メソッドを持たせてください。LocalTimeProvider を実装し登録し、Consumer プラグインから呼び出すこと。
2. ⭐⭐ 応用:MockTimeProvider(固定タイムスタンプを返す)を実装し、$replace で Provider を切り替えてください。Consumer の呼び出し結果がリアル時刻から固定時刻に変わることを、Consumer コードの変更なしに確認。
3. ⭐⭐⭐ チャレンジ:SearchEngine ケイパビリティを設計し、search(query:string):Promise<string[]> インターフェースを定義してください。2つの Provider を実装:LocalGrepProvider(grep で検索)と RemoteAPIProvider(検索 API を呼び出し)。Realm 設定で2つの Agent が異なる検索実装を使用し、分離を確認すること。