DeepSeek Harness: ケイパビリティ3役

最終更新:2026-08-31

Cordis のケイパビリティシステムは「すべてはプラグイン」の基盤です。機能を3つの役割に分割します:インターフェースを定義する者、実装する者、使用する者。この分離により、実装の置き換えは電池の交換のように簡単——古いものを引き抜き、新しいものを差し込めば、システムは正常に動作します。

💡 ヒント:3役ケイパビリティモデルの核心的価値は「置換可能性」——1つの Provider を置き換えるだけで製品全体の動作が変わり、Definition と Consumer は不変。それが seam の力です。

📋 前提知識19-service.md の完了、Service 基底クラスを理解していること

1. 学習内容

能力三角色モデル


2. ケイパビリティシステム概要

(1) なぜ3つの役割か

ケイパビリティシステムなしでは、機能は直接ハードコードされます:

TYPESCRIPT
// ❌ ハードコードされた実装
class FileAnalyzer {
  analyze(path: string) {
    const stat = fs.statSync(path)  // ローカルファイルシステムしか使えない
    return { size: stat.size, type: 'local' }
  }
}

ケイパビリティシステムがあれば、機能は3層に分割されます:

TYPESCRIPT
// ✅ 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:

100%
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 はケイパビリティのインターフェースを宣言——実装を含まず、「このケイパビリティで何ができるか」を記述するだけ:

TYPESCRIPT
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 を分離する利点:

(4) 規約

Definition は通常 Provider とは別ファイルに配置:

TEXT 📖 参照専用
capabilities/
├── file-stats/
│   ├── definition.ts    ← Definition
│   ├── local.ts         ← Provider(ローカル実装)
│   └── sandbox.ts       ← Provider(サンドボックス実装)

4. Provider:インターフェースの実装

(1) ケイパビリティの実装

Provider は Definition が宣言したインターフェースを実装:

TYPESCRIPT
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) は実装をケイパビリティに登録:

TYPESCRIPT
ctx.implement(FileStats, {
  getSize: async (path) => { ... },
  getType: async (path) => { ... },
  // すべてのインターフェースメソッドを実装しなければならない
})

メソッドが欠けていると、TypeScript がコンパイル時エラーを報告します。

▶ サンプル 3:

ローカル Provider:

TYPESCRIPT
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:

TYPESCRIPT
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 を通じてケイパビリティを使用:

TYPESCRIPT
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 が定義するインターフェースにのみ依存し、具体的な実装には依存しない:

TYPESCRIPT
// 下層が Local でも Sandbox でも、Consumer コードは同一
const size = await ctx['file-stats'].getSize(path)

▶ サンプル 3:

TYPESCRIPT
// 型拡張による
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 の接合点——システムが「引き抜いて置き換え」できる箇所です:

100%
graph LR
    CON[Consumer] -->|依存| DEF[Definition<br/>seam]
    DEF -->|実装| PROV_A[Provider A<br/>ローカル実装]
    DEF -.->|置き換え| PROV_B[Provider B<br/>サンドボックス実装]

(2) seam の価値

TEXT 📖 参照専用
seam なし:
  Consumer → Provider A (ハードコード、置き換え不可)

seam あり:
  Consumer → Definition(seam)→ Provider A
                      (seam)→ Provider B (置き換え!)

seam はあらゆるケイパビリティポイントでシステムを置換可能にします。

(3) 良い seam の識別

基準 良い seam 悪い seam
抽象レベル 適切 細かすぎまたは粗すぎ
実装数 複数あり得る 1つしかあり得ない
変更頻度 実装が変更される可能性 実装が永不変
依存方向 Consumer がインターフェースに依存 Consumer が実装に依存

(4) seam の粒度

TEXT 📖 参照専用
粗い seam:FileSystem (ファイルシステム全体が置き換え可能)
中位の seam:FileStats (ファイル統計が置き換え可能)
細かい seam:FileSize (ファイルサイズ照会が置き換え可能)

粗すぎ → 置換コスト高;細かすぎ → インターフェース断片化。中位の粒度を選択。


7. ケイパビリティ登録グラフ

(1) 登録フロー

100%
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) 置換フロー

100%
graph LR
    OLD[Provider A<br/>現在アクティブ] -->|アンロード| INACTIVE_A[非アクティブ化]
    NEW[Provider B<br/>新規登録] -->|実装| ACTIVE_B[現在アクティブ]
    ACTIVE_B --> CONSUMER[Consumer<br/>自動切替]

(3) マルチケイパビリティ連携

100%
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) コアバリュー

これがケイパビリティシステムの最も強力な機能:

TEXT 📖 参照専用
シナリオ:ローカル開発からサンドボックス実行に切り替え

1. LocalFileStatsProvider をアンロード
2. SandboxFileStatsProvider をロード
3. すべての Consumer が自動的にサンドボックス実装を使用
4. Consumer コード:変更ゼロ

(2) 設定による切り替え

YAML
# ローカル開発
plugins:
  file-stats:
    $insert:./providers/local-file-stats

# サンドボックス環境(この1行だけ変更)
plugins:
  file-stats:
    $replace:./providers/sandbox-file-stats

(3) ランタイム切り替え

TYPESCRIPT
// $replace で動的に切り替え
ctx.on('config/updated', (config) => {
  if (config.environment === 'sandbox') {
    // フレームワークが自動リロード、Sandbox Provider に切替
  }
})

(4) A/B テスト

YAML
# グループ A:ローカル実装
realms:
  group-a:
    plugins:
      file-stats:
        $insert:./providers/local-file-stats
  
  group-b:
    plugins:
      file-stats:
        $insert:./providers/sandbox-file-stats

❓ よくある質問

Q Definition は必ずインターフェースでなければなりませんか?
A はい。Definition は純粋なインターフェース(メソッドシグネチャのみ、実装なし)を記述します。共有コードが必要な場合は、別のユーティリティモジュールに配置してください。
Q 1つの Definition に複数のアクティブ Provider が存在できますか?
A デフォルトでは不可——後から登録されたものが前を上書きします。共存が必要な場合は Realm 分離を使用してください。
Q Provider の置き換え時に中断はありますか?
A 一時的に。旧 Provider のアンロードと新 Provider のロードの間、ケイパビリティは一時的に利用不可になります。Consumer は undefined のケースを処理すべきです。
Q 機能をケイパビリティとして抽象化するかどうかの判断基準は?
A 自問してください:将来この機能に異なる実装が存在する可能性はあるか? はい → 抽象化;確実に1つの実装しかない → Service を直接使用。
Q seam と inject の違いは?
A inject は依存宣言機構(「X が必要」);seam は置換可能性設計(「X は置き換え可能」)。inject は seam の前提——Consumer は inject で依存を宣言し、seam はその依存が置換可能であることを保証します。
Q ケイパビリティシステムは追加する複雑さに見合いますか?
A シンプルなプロジェクトでは多分見合わない。しかし、複数のデプロイ環境(ローカル/サンドボックス/リモート)や 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 が異なる検索実装を使用し、分離を確認すること。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%