DeepSeek Harness: Practice: Building a Replaceable Capability
Last updated: 2026-08-31
You understand the three roles in theory, but theory alone isn't enough. This lesson walks through a complete practical project — implementing a replaceable file statistics capability from scratch: defining the interface, writing a local implementation, writing a sandbox implementation, consuming the capability, and switching Providers. Full workflow, end to end.
📋 Prerequisites: Completed 22-capability.md, understand capability three roles
1. What You'll Learn
- Complete example: Definition → Provider → Consumer
- Defining a file statistics capability
- Implementing a local Provider
- Implementing a remote sandbox Provider
- Consuming the capability in a tool
- Effects of switching Providers
2. Project Structure
(1) ▶ Example 1
file-stats-capability/
├── definition.ts ← Definition
├── providers/
│ ├── local.ts ← Local Provider
│ └── sandbox.ts ← Remote Sandbox Provider
├── consumer/
│ └── file-info-tool.ts ← Consumer plugin
├── types.ts ← Type declarations
└── index.ts ← Exports
(2) ▶ Example 2
graph TB
DEF[definition.ts] --> LOCAL[providers/local.ts]
DEF --> SANDBOX[providers/sandbox.ts]
DEF --> TOOL[consumer/file-info-tool.ts]
3. Defining the File Statistics Capability
(1) Requirements Analysis
The file statistics capability needs to provide:
- Count files
- Calculate directory size
- Classify by extension
- Check if a path exists
(2) ▶ Example 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) Type Declarations
// types.ts
import { FileStatsCapability } from './definition'
declare module '@deepseek-ai/cordis' {
interface Context {
'file-stats': FileStatsCapability
}
}
4. Implementing the Local Provider
(1) Code
// 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) Register in cordis.yml
plugins:
file-stats:
$insert: /home/alice/dev/file-stats-capability/providers/local
5. Implementing the Remote Sandbox Provider
(1) Design
The sandbox Provider delegates file operations to a remote service:
graph LR
TOOL[Consumer] -->|calls| CAP[file-stats capability]
CAP -->|HTTP| SANDBOX[Sandbox service<br/>sandbox:8080]
SANDBOX -->|operates on| FS[Isolated filesystem]
(2) Code
// 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) Register in cordis.yml
plugins:
file-stats:
$replace: /home/alice/dev/file-stats-capability/providers/sandbox
config:
endpoint: http://sandbox:8080
timeout: 15000
6. Consuming the Capability in a Tool
(1) Consumer Plugin Code
// 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) Key Observation
The Consumer code contains no mention of local or sandbox — it only depends on the file-stats capability interface. That's the value of the capability system.
7. Effects of Switching Providers
(1) Using the Local Provider
# cordis.yml — Local development
plugins:
file-stats:
$insert: /home/alice/dev/file-stats-capability/providers/local
Agent calls file_info tool:
👤 Alice: Analyze the /home/alice/project directory
🤖 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) Switching to the Sandbox Provider
# cordis.yml — Sandbox environment
plugins:
file-stats:
$replace: /home/alice/dev/file-stats-capability/providers/sandbox
config:
endpoint: http://sandbox:8080
After restart, Agent calls the same tool:
👤 Alice: Analyze the /workspace/project directory
🤖 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 code is completely unchanged, result format is identical — only the underlying implementation changed from local filesystem to remote sandbox API.
(3) Comparison
| Dimension | Local Provider | Sandbox Provider |
|---|---|---|
| Implementation | Direct fs calls | HTTP API calls |
| Filesystem | Local disk | Sandboxed environment |
| Latency | < 1ms | 50-200ms |
| Security | Direct access | Sandbox isolation |
| Consumer code | Same | Same |
| Return format | Same | Same |
❓ FAQ
yaml file-stats: $insert: ./providers/sandbox config: endpoint: http://sandbox:8080 # sandbox Provider-specific config providerInfo() method to the Definition; each Provider returns its own info.📖 Summary
- Complete flow: Definition → Provider → Consumer → switch verification
- Definition declares the
FileStatsCapabilityinterface with countFiles/calcSize/analyze/exists - Local Provider uses fs API directly; sandbox Provider delegates via HTTP to remote service
- Consumer only depends on the interface — it neither knows nor cares about the specific Provider
- Switching Providers only requires changing cordis.yml configuration; Consumer code has zero modifications
- All Providers must return results in the same format — this is Definition's core constraint
📝 Exercises
1. ⭐ Basic: Follow this lesson's steps to create the FileStats capability's Definition and local Provider, register it, and verify via Consumer tool calls.
2. ⭐⭐ Intermediate: Implement an InMemoryProvider — file data pre-stored in a memory Map (no real filesystem access), suitable for unit testing. Verify the Consumer tool works normally with InMemoryProvider.
3. ⭐⭐⭐ Challenge: Implement a CachedProvider — decorator pattern, adding a cache layer in front of another Provider. Cache the last N analysis results; same paths return cached results directly. Note: CachedProvider itself is also a Provider; it internally delegates to another Provider.