DeepSeek Harness: 実践:置換可能なケイパビリティの構築
最終更新:2026-08-31
理論として3役を理解していても、それだけでは不十分です。このレッスンでは完全な実践プロジェクトを一歩ずつ進めます——置換可能なファイル統計ケイパビリティをゼロから実装:インターフェース定義、ローカル実装の作成、サンドボックス実装の作成、ケイパビリティの消費、Provider の切り替え。全ワークフロー、エンドツーエンド。
📋 前提知識:22-capability.md の完了、ケイパビリティ3役を理解していること
1. 学習内容
- 完全例:Definition → Provider → Consumer
- ファイル統計ケイパビリティの定義
- ローカル Provider の実装
- リモートサンドボックス Provider の実装
- ツールでのケイパビリティの消費
- Provider 切り替えの効果
2. プロジェクト構造
▶ サンプル 1:
file-stats-capability/
├── definition.ts ← Definition
├── providers/
│ ├── local.ts ← ローカル Provider
│ └── sandbox.ts ← リモートサンドボックス Provider
├── consumer/
│ └── file-info-tool.ts ← Consumer プラグイン
├── types.ts ← 型宣言
└── index.ts ← エクスポート
▶ サンプル 2:
graph TB
DEF[definition.ts] --> LOCAL[providers/local.ts]
DEF --> SANDBOX[providers/sandbox.ts]
DEF --> TOOL[consumer/file-info-tool.ts]
3. ファイル統計ケイパビリティの定義
(1) 要件分析
ファイル統計ケイパビリティが提供すべき機能:
- ファイル数のカウント
- ディレクトリサイズの計算
- 拡張子別の分類
- パスの存在確認
▶ サンプル 2:
// 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) 型宣言
// types.ts
import { FileStatsCapability } from './definition'
declare module '@deepseek-ai/cordis' {
interface Context {
'file-stats': FileStatsCapability
}
}
4. ローカル Provider の実装
(1) コード
// 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 に登録
plugins:
file-stats:
$insert:/home/alice/dev/file-stats-capability/providers/local
5. リモートサンドボックス Provider の実装
(1) 設計
サンドボックス Provider はファイル操作をリモートサービスに委譲:
graph LR
TOOL[Consumer] -->|呼び出し| CAP[file-stats ケイパビリティ]
CAP -->|HTTP| SANDBOX[Sandbox サービス<br/>sandbox:8080]
SANDBOX -->|操作| FS[分離ファイルシステム]
(2) コード
// 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 に登録
plugins:
file-stats:
$replace:/home/alice/dev/file-stats-capability/providers/sandbox
config:
endpoint:http://sandbox:8080
timeout:15000
6. ツールでのケイパビリティの消費
(1) Consumer プラグインコード
// 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 コードには local や sandbox への言及が一切ない——file-stats ケイパビリティのインターフェースにのみ依存。それがケイパビリティシステムの価値です。
7. Provider 切り替えの効果
(1) ローカル Provider の使用
# cordis.yml — ローカル開発
plugins:
file-stats:
$insert:/home/alice/dev/file-stats-capability/providers/local
Agent が file_info ツールを呼び出し:
👤 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 への切り替え
# cordis.yml — サンドボックス環境
plugins:
file-stats:
$replace:/home/alice/dev/file-stats-capability/providers/sandbox
config:
endpoint:http://sandbox:8080
再起動後、Agent が同じツールを呼び出し:
👤 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 コード | 同じ | 同じ |
| 戻りフォーマット | 同じ | 同じ |
❓ よくある質問
yaml file-stats:$insert:./providers/sandbox config:endpoint:http://sandbox:8080 # sandbox Provider 固有の設定 providerInfo() メソッドを追加し、各 Provider が自身の情報を返すようにします。📖 まとめ
- 完全フロー:Definition → Provider → Consumer → 切り替え検証
- Definition は
FileStatsCapabilityインターフェースを宣言し countFiles/calcSize/analyze/exists を含む - ローカル Provider は fs API を直接使用;サンドボックス Provider は HTTP でリモートサービスに委譲
- Consumer はインターフェースにのみ依存——具体的な Provider を知らず気にしない
- Provider の切り替えは cordis.yml 設定の変更のみ;Consumer コードは変更ゼロ
- すべての Provider は同じフォーマットで結果を返さなければならない——これが Definition のコア制約
📝 練習問題
1. ⭐ 基礎:このレッスンの手順に従って FileStats ケイパビリティの Definition とローカル Provider を作成し、登録し、Consumer ツール呼び出しで確認してください。
2. ⭐⭐ 応用:InMemoryProvider を実装してください——ファイルデータはメモリ Map に事前格納(実際のファイルシステムアクセスなし)、ユニットテストに適する。Consumer ツールが InMemoryProvider でも正常に動作することを確認。
3. ⭐⭐⭐ チャレンジ:CachedProvider を実装してください——デコレータパターンで、別の Provider の前にキャッシュ層を追加。最後の N 件の分析結果をキャッシュ;同じパスはキャッシュ結果を直接返す。注:CachedProvider 自身も Provider;内部で別の Provider に委譲する。