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.

💡 Tip: The key point of this project is "Consumer code doesn't change when switching Providers" — if the Consumer needs code changes, your Definition abstraction isn't good enough.

📋 Prerequisites: Completed 22-capability.md, understand capability three roles

1. What You'll Learn


MyCap Structure

2. Project Structure

(1) ▶ Example 1

TEXT 📖 Display only
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

100%
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:

(2) ▶ Example 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) Type Declarations

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

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

4. Implementing the Local Provider

(1) Code

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) Register in cordis.yml

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

100%
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

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) Register in cordis.yml

YAML
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

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) 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

YAML
# cordis.yml — Local development
plugins:
  file-stats:
    $insert: /home/alice/dev/file-stats-capability/providers/local

Agent calls file_info tool:

TEXT 📖 Display only
👤 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

YAML
# 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:

TEXT 📖 Display only
👤 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

Q Must both Providers return the exact same format?
A Yes. This is Definition's core constraint — all Providers must follow the same interface. Different return formats cause Consumer parsing errors.
Q The sandbox Provider has higher latency — does the Consumer need to handle this?
A No. Latency is transparent to the Consumer. If optimization is needed, add caching at the Consumer layer or use async indicators.
Q How to ensure a Provider implements all methods?
A TypeScript compile-time checking. If a Provider is missing methods from the Definition, compilation fails.
Q Can the capability system be used in plugin configuration?
A Yes. Different Providers can have different Config: yaml file-stats: $insert: ./providers/sandbox config: endpoint: http://sandbox:8080 # sandbox Provider-specific config
Q How does a Consumer know which Provider is currently active?
A Usually it doesn't need to. If truly needed, add a providerInfo() method to the Definition; each Provider returns its own info.

📖 Summary


📝 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.

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏