DeepSeek Harness: 実践:置換可能なケイパビリティの構築

最終更新:2026-08-31

理論として3役を理解していても、それだけでは不十分です。このレッスンでは完全な実践プロジェクトを一歩ずつ進めます——置換可能なファイル統計ケイパビリティをゼロから実装:インターフェース定義、ローカル実装の作成、サンドボックス実装の作成、ケイパビリティの消費、Provider の切り替え。全ワークフロー、エンドツーエンド。

💡 ヒント:このプロジェクトの要点は「Provider を切り替えても Consumer コードが変わらない」——Consumer にコード変更が必要なら、Definition の抽象化が不十分です。

📋 前提知識22-capability.md の完了、ケイパビリティ3役を理解していること

1. 学習内容

MyCap 構造


2. プロジェクト構造

▶ サンプル 1:

TEXT 📖 参照専用
file-stats-capability/
├── definition.ts            ← Definition
├── providers/
│   ├── local.ts             ← ローカル Provider
│   └── sandbox.ts           ← リモートサンドボックス Provider
├── consumer/
│   └── file-info-tool.ts    ← Consumer プラグイン
├── types.ts                 ← 型宣言
└── index.ts                 ← エクスポート

▶ サンプル 2:

100%
graph TB
    DEF[definition.ts] --> LOCAL[providers/local.ts]
    DEF --> SANDBOX[providers/sandbox.ts]
    DEF --> TOOL[consumer/file-info-tool.ts]

3. ファイル統計ケイパビリティの定義

(1) 要件分析

ファイル統計ケイパビリティが提供すべき機能:

▶ サンプル 2:

TYPESCRIPT
// definition.ts
import { defineCapability } from '@deepseek-ai/cordis'

export interface FileStatsResult {
  totalFiles: number
  totalDirs: number
  totalSize: number
  byExtension: Record<string, number>
}

export interface FileStatsCapability {
  countFiles(dir: string, recursive?: boolean): Promise<number>
  calcSize(dir: string): Promise<number>
  analyze(dir: string): Promise<FileStatsResult>
  exists(path: string): Promise<boolean>
}

export const FileStats = defineCapability({
  name: 'file-stats',
  description: 'File statistics and analysis capability',
  interface: {} as FileStatsCapability
})

(3) 型宣言

TYPESCRIPT
// types.ts
import { FileStatsCapability } from './definition'

declare module '@deepseek-ai/cordis' {
  interface Context {
    'file-stats': FileStatsCapability
  }
}

4. ローカル Provider の実装

(1) コード

TYPESCRIPT
// providers/local.ts
import { Service, Context } from '@deepseek-ai/cordis'
import { FileStats, FileStatsResult } from '../definition'
import { readdir, stat } from 'fs/promises'
import { join } from 'path'

export default class LocalFileStatsProvider extends Service {
  static inject = ['fs']

  constructor(ctx: Context) {
    super(ctx, 'file-stats')
    
    ctx.implement(FileStats, {
      async countFiles(dir: string, recursive = false): Promise<number> {
        const result = await this._walk(dir, recursive)
        return result.totalFiles
      },

      async calcSize(dir: string): Promise<number> {
        const result = await this._walk(dir, true)
        return result.totalSize
      },

      async analyze(dir: string): Promise<FileStatsResult> {
        return await this._walk(dir, true)
      },

      async exists(path: string): Promise<boolean> {
        try {
          await stat(path)
          return true
        } catch {
          return false
        }
      }
    })
  }

  private async _walk(dir: string, recursive: boolean): Promise<FileStatsResult> {
    let totalFiles = 0
    let totalDirs = 0
    let totalSize = 0
    const byExtension: Record<string, number> = {}

    await this._walkInner(dir, recursive, (fileStat) => {
      totalFiles++
      totalSize += fileStat.size
      const ext = fileStat.name.includes('.')
        ? '.' + fileStat.name.split('.').pop()!.toLowerCase()
        : '(no extension)'
      byExtension[ext] = (byExtension[ext] || 0) + 1
    }, () => {
      totalDirs++
    })

    return { totalFiles, totalDirs, totalSize, byExtension }
  }

  private async _walkInner(
    dir: string,
    recursive: boolean,
    onFile: (f: { name: string; size: number }) => void,
    onDir: () => void
  ): Promise<void> {
    const entries = await readdir(dir, { withFileTypes: true })
    for (const entry of entries) {
      if (entry.isFile()) {
        const s = await stat(join(dir, entry.name))
        onFile({ name: entry.name, size: s.size })
      } else if (entry.isDirectory()) {
        onDir()
        if (recursive) {
          await this._walkInner(join(dir, entry.name), recursive, onFile, onDir)
        }
      }
    }
  }
}

(2) cordis.yml に登録

YAML
plugins:
  file-stats:
    $insert:/home/alice/dev/file-stats-capability/providers/local

5. リモートサンドボックス Provider の実装

(1) 設計

サンドボックス Provider はファイル操作をリモートサービスに委譲:

100%
graph LR
    TOOL[Consumer] -->|呼び出し| CAP[file-stats ケイパビリティ]
    CAP -->|HTTP| SANDBOX[Sandbox サービス<br/>sandbox:8080]
    SANDBOX -->|操作| FS[分離ファイルシステム]

(2) コード

TYPESCRIPT
// providers/sandbox.ts
import { Service, Context } from '@deepseek-ai/cordis'
import { FileStats, FileStatsResult } from '../definition'

export const Config = Schema.object({
  endpoint: Schema.string().default('http://sandbox:8080').description('Sandbox API endpoint'),
  timeout: Schema.number().default(30000).description('Request timeout in ms')
})

export default class SandboxFileStatsProvider extends Service {
  static inject = []

  private endpoint: string
  private timeout: number

  constructor(ctx: Context) {
    super(ctx, 'file-stats')
    this.endpoint = ctx.config.endpoint
    this.timeout = ctx.config.timeout
    
    ctx.implement(FileStats, {
      countFiles: (dir, recursive) => 
        this._call('count-files', { dir, recursive }).then(r => r.count),
      
      calcSize: (dir) => 
        this._call('calc-size', { dir }).then(r => r.size),
      
      analyze: (dir) => 
        this._call<FileStatsResult>('analyze', { dir }),
      
      exists: (path) => 
        this._call('exists', { path }).then(r => r.exists)
    })
  }

  private async _call<T = any>(action: string, params: Record<string, any>): Promise<T> {
    const controller = new AbortController()
    const timer = setTimeout(() => controller.abort(), this.timeout)

    try {
      const response = await fetch(`${this.endpoint}/file-stats/${action}`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(params),
        signal: controller.signal
      })

      if (!response.ok) {
        throw new Error(`sandbox error: ${response.status} ${response.statusText}`)
      }

      return await response.json() as T
    } finally {
      clearTimeout(timer)
    }
  }
}

(3) cordis.yml に登録

YAML
plugins:
  file-stats:
    $replace:/home/alice/dev/file-stats-capability/providers/sandbox
    config:
      endpoint:http://sandbox:8080
      timeout:15000

6. ツールでのケイパビリティの消費

(1) Consumer プラグインコード

TYPESCRIPT
// consumer/file-info-tool.ts
import { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh'

export const name = 'tool-file-info'
export const inject = ['tools', 'file-stats']

export function apply(ctx: Context) {
  ctx.tools.register(defineTool({
    name: 'file_info',
    description: 'Get detailed file statistics for a directory. Returns file count, total size, and breakdown by extension.',
    parameters: {
      type: 'object',
      properties: {
        path: {
          type: 'string',
          description: 'Directory path to analyze'
        },
        recursive: {
          type: 'boolean',
          description: 'Include subdirectories in analysis',
          default: true
        }
      },
      required: ['path']
    },
    async execute({ path, recursive }, ctx) {
      try {
        const exists = await ctx['file-stats'].exists(path)
        if (!exists) {
          return { error: true, message: `Path does not exist: ${path}` }
        }

        const stats = await ctx['file-stats'].analyze(path)
        
        return {
          path,
          recursive,
          totalFiles: stats.totalFiles,
          totalDirs: stats.totalDirs,
          totalSize: stats.totalSize,
          totalSizeHuman: formatSize(stats.totalSize),
          byExtension: stats.byExtension
        }
      } catch (error: any) {
        return {
          error: true,
          message: `Failed to analyze directory: ${error.message}`
        }
      }
    }
  }))
}

function formatSize(bytes: number): string {
  if (bytes < 1024) return `${bytes} B`
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
  if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`
  return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`
}

(2) 重要な観察

Consumer コードには localsandbox への言及が一切ない——file-stats ケイパビリティのインターフェースにのみ依存。それがケイパビリティシステムの価値です。


7. Provider 切り替えの効果

(1) ローカル Provider の使用

YAML
# cordis.yml — ローカル開発
plugins:
  file-stats:
    $insert:/home/alice/dev/file-stats-capability/providers/local

Agent が file_info ツールを呼び出し:

TEXT 📖 参照専用
👤 Alice:/home/alice/project ディレクトリを分析して

🤖 Agent:
🔧 Using tool:file_info
  → path:/home/alice/project
  
  Result:{
    path:"//home/alice/project",
    totalFiles:42,
    totalDirs:5,
    totalSize:245760,
    totalSizeHuman:"240.0 KB",
    byExtension:{ ".ts":28, ".js":10, ".json":4 }
  }

(2) サンドボックス Provider への切り替え

YAML
# cordis.yml — サンドボックス環境
plugins:
  file-stats:
    $replace:/home/alice/dev/file-stats-capability/providers/sandbox
    config:
      endpoint:http://sandbox:8080

再起動後、Agent が同じツールを呼び出し:

TEXT 📖 参照専用
👤 Alice:/workspace/project ディレクトリを分析して

🤖 Agent:
🔧 Using tool:file_info
  → path:/workspace/project
  
  Result:{
    path:"//workspace/project",
    totalFiles:42,
    totalDirs:5,
    totalSize:245760,
    totalSizeHuman:"240.0 KB",
    byExtension:{ ".ts":28, ".js":10, ".json":4 }
  }

Consumer コードは完全に変更なし、結果フォーマットも同一——変わったのは下層の実装がローカルファイルシステムからリモートサンドボックス API に変わっただけ。

(3) 比較

次元 ローカル Provider サンドボックス Provider
実装 直接 fs 呼び出し HTTP API 呼び出し
ファイルシステム ローカルディスク サンドボックス環境
レイテンシ < 1ms 50-200ms
セキュリティ 直接アクセス サンドボックス分離
Consumer コード 同じ 同じ
戻りフォーマット 同じ 同じ

❓ よくある質問

Q 両 Provider は全く同じフォーマットを返さなければなりませんか?
A はい。これが Definition のコア制約——すべての Provider は同じインターフェースに従う必要があります。異なる戻りフォーマットは Consumer の解析エラーを引き起こします。
Q サンドボックス Provider のレイテンシが高い——Consumer は対応する必要がありますか?
A いいえ。レイテンシは Consumer にとって透過的です。最適化が必要な場合、Consumer 層にキャッシュを追加するか非同期インジケータを使用してください。
Q Provider がすべてのメソッドを実装していることを保証するには?
A TypeScript のコンパイル時チェック。Provider が Definition のメソッドを欠いている場合、コンパイルが失敗します。
Q ケイパビリティシステムはプラグイン設定で使用できますか?
A はい。異なる Provider は異なる Config を持てます:yaml file-stats:$insert:./providers/sandbox config:endpoint:http://sandbox:8080 # sandbox Provider 固有の設定
Q Consumer は現在アクティブな Provider がどれかを知る方法は?
A 通常は知る必要がありません。本当に必要な場合、Definition に providerInfo() メソッドを追加し、各 Provider が自身の情報を返すようにします。

📖 まとめ


📝 練習問題

1. ⭐ 基礎:このレッスンの手順に従って FileStats ケイパビリティの Definition とローカル Provider を作成し、登録し、Consumer ツール呼び出しで確認してください。

2. ⭐⭐ 応用InMemoryProvider を実装してください——ファイルデータはメモリ Map に事前格納(実際のファイルシステムアクセスなし)、ユニットテストに適する。Consumer ツールが InMemoryProvider でも正常に動作することを確認。

3. ⭐⭐⭐ チャレンジCachedProvider を実装してください——デコレータパターンで、別の Provider の前にキャッシュ層を追加。最後の N 件の分析結果をキャッシュ;同じパスはキャッシュ結果を直接返す。注:CachedProvider 自身も Provider;内部で別の Provider に委譲する。

Web-Tutorial.com

Web-Tutorial 技術チーム

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

100%