DeepSeek Harness: Capability Three Roles

Last updated: 2026-08-31

Cordis's capability system is the foundation of "everything is a plugin." It splits a feature into three roles: the one who defines the interface, the one who implements it, and the one who uses it. This separation makes replacing an implementation as easy as swapping batteries — pull out the old one, plug in the new one, and the system runs normally.

💡 Tip: The core value of the three-role capability model is "replaceability" — replace one Provider and you replace the entire product behavior, while Definition and Consumer remain unchanged. That's the power of the seam.

📋 Prerequisites: Completed 19-service.md, understand the Service base class

1. What You'll Learn


Capability Three Roles

2. Capability System Overview

(1) Why Three Roles

Without a capability system, features are directly hardcoded:

TYPESCRIPT
// ❌ Hardcoded implementation
class FileAnalyzer {
  analyze(path: string) {
    const stat = fs.statSync(path)  // Can only use local filesystem
    return { size: stat.size, type: 'local' }
  }
}

With a capability system, features are split into three layers:

TYPESCRIPT
// ✅ Three-role separation
// Definition: declare interface
interface FileStats {
  getSize(path: string): Promise<number>
  getType(path: string): Promise<string>
}

// Provider: implementation (local)
class LocalFileStats implements FileStats { ... }

// Provider: implementation (remote sandbox)
class SandboxFileStats implements FileStats { ... }

// Consumer: usage (doesn't care about specific implementation)
class FileAnalyzer {
  constructor(private stats: FileStats) {}
  analyze(path: string) {
    const size = await this.stats.getSize(path)
    return { size }
  }
}

(2) ▶ Example 2

100%
graph LR
    DEF[Definition<br/>Declare interface] --> PROV[Provider<br/>Implement interface]
    PROV --> CON[Consumer<br/>Use interface]
    CON --> DEF

(3) Analogy

Role Analogy Real-world counterpart
Definition Power outlet standard National outlet specification
Provider Outlet implementation Wall outlet
Consumer Electrical device TV, refrigerator

A TV doesn't care which power plant the electricity comes from — it only cares that the outlet meets the standard. Similarly, a Consumer doesn't care who the Provider is — it only cares about the interface defined by the Definition.


3. Definition: Declaring Interfaces

(1) Defining a Capability

Definition declares the capability's interface — it contains no implementation, only describes "what this capability can do":

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 Elements

Element Description
name Capability identifier, globally unique
description Capability description
interface TypeScript interface definition

(3) Why Separate Definitions

Benefits of separating Definition from Provider:

(4) Convention

Definitions are typically placed in a separate file from Providers:

TEXT 📖 Display only
capabilities/
├── file-stats/
│   ├── definition.ts    ← Definition
│   ├── local.ts         ← Provider (local implementation)
│   └── sandbox.ts       ← Provider (sandbox implementation)

4. Provider: Implementing Interfaces

(1) Implementing a Capability

Provider implements the interface declared by 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) registers the implementation to the capability:

TYPESCRIPT
ctx.implement(FileStats, {
  getSize: async (path) => { ... },
  getType: async (path) => { ... },
  // Must implement all interface methods
})

If methods are missing, TypeScript reports a compile-time error.

(3) ▶ Example 3

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

Remote Sandbox 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: Using Interfaces

(1) Consuming a Capability

Consumer declares dependencies via inject and uses the capability through 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 Doesn't Know the Provider

Consumer only depends on the interface defined by Definition, not the specific implementation:

TYPESCRIPT
// Consumer code is identical regardless of Local or Sandbox underneath
const size = await ctx['file-stats'].getSize(path)

(3) ▶ Example 3

TYPESCRIPT
// Via type extension
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 Concept

(1) What is a seam

A seam is the joint point between Definition and Provider — it's where the system can be "unplugged and replaced":

100%
graph LR
    CON[Consumer] -->|depends on| DEF[Definition<br/>seam]
    DEF -->|implements| PROV_A[Provider A<br/>Local impl]
    DEF -.->|replace with| PROV_B[Provider B<br/>Sandbox impl]

(2) Value of seam

TEXT 📖 Display only
Without seam:
  Consumer → Provider A (hardcoded, cannot replace)

With seam:
  Consumer → Definition (seam) → Provider A
                        (seam) → Provider B (replaced!)

seam makes the system replaceable at every capability point.

(3) Identifying Good seams

Criterion Good seam Bad seam
Abstraction level Just right Too fine or too coarse
Implementation count Could have multiple Only one possible
Change frequency Implementation may change Implementation never changes
Dependency direction Consumer depends on interface Consumer depends on implementation

(4) seam Granularity

TEXT 📖 Display only
Coarse seam: FileSystem (entire filesystem replaceable)
Medium seam: FileStats (file statistics replaceable)
Fine seam: FileSize (file size query replaceable)

Too coarse → high replacement cost; too fine → interface fragmentation. Choose medium granularity.


7. Capability Registration Graph

(1) Registration Flow

100%
graph TB
    DEF[defineCapability<br/>Declare interface] --> REG[Register Definition]
    REG --> PROV1[Provider A implement]
    REG --> PROV2[Provider B implement]
    PROV1 --> ACTIVE_A[Currently active: A]
    PROV2 --> WAIT_B[Waiting: B]
    ACTIVE_A --> CONSUMER[Consumer uses]

(2) Replacement Flow

100%
graph LR
    OLD[Provider A<br/>Currently active] -->|unload| INACTIVE_A[Deactivated]
    NEW[Provider B<br/>New registration] -->|implement| ACTIVE_B[Currently active]
    ACTIVE_B --> CONSUMER[Consumer<br/>Auto-switches]

(3) Multi-Capability Collaboration

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. Replacing a Provider = Replacing Entire Product Behavior

(1) Core Value

This is the capability system's most powerful feature:

TEXT 📖 Display only
Scenario: Switch from local development to sandbox execution

1. Unload LocalFileStatsProvider
2. Load SandboxFileStatsProvider
3. All Consumers automatically use the sandbox implementation
4. Consumer code: zero modifications

(2) Configuration Switching

YAML
# Local development
plugins:
  file-stats:
    $insert: ./providers/local-file-stats

# Sandbox environment (only change this one line)
plugins:
  file-stats:
    $replace: ./providers/sandbox-file-stats

(3) Runtime Switching

TYPESCRIPT
// Switch dynamically via $replace
ctx.on('config/updated', (config) => {
  if (config.environment === 'sandbox') {
    // Framework auto-reloads, switches to Sandbox Provider
  }
})

(4) A/B Testing

YAML
# Group A: local implementation
realms:
  group-a:
    plugins:
      file-stats:
        $insert: ./providers/local-file-stats
  
  group-b:
    plugins:
      file-stats:
        $insert: ./providers/sandbox-file-stats

❓ FAQ

Q Must Definition be an interface?
A Yes. Definition describes a pure interface (only method signatures, no implementation). If you need shared code, put it in a separate utility module.
Q Can one Definition have multiple active Providers?
A Not by default — later registrations override earlier ones. If coexistence is needed, use realm isolation.
Q Is there interruption when a Provider is replaced?
A Briefly. Between the old Provider unloading and new Provider loading, the capability is temporarily unavailable. Consumers should handle undefined cases.
Q How to decide if a feature should be abstracted as a capability?
A Ask yourself: might this feature have different implementations in the future? If yes, abstract it; if definitely only one implementation, use Service directly.
Q What's the difference between seam and inject?
A inject is the dependency declaration mechanism ("I need X"); seam is replaceability design ("X can be replaced"). inject is a prerequisite for seam — Consumer declares dependency via inject, seam guarantees the dependency is replaceable.
Q Is the capability system worth the added complexity?
A For simple projects, maybe not. But when a project has multiple deployment environments (local/sandbox/remote) or needs A/B testing, the capability system's benefits far outweigh its costs.

📖 Summary


📝 Exercises

1. ⭐ Basic: Define a TimeService capability (Definition) with a now(): number method. Implement a LocalTimeProvider, register it, and call it from a Consumer plugin.

2. ⭐⭐ Intermediate: Implement a MockTimeProvider (returns a fixed timestamp), use $replace to switch Providers. Verify the Consumer's call results change from real time to fixed time without any Consumer code modifications.

3. ⭐⭐⭐ Challenge: Design a SearchEngine capability, defining a search(query: string): Promise<string[]> interface. Implement two Providers: LocalGrepProvider (searches with grep) and RemoteAPIProvider (calls a search API). Use realm configuration to let two Agents use different search implementations, verifying isolation.

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%

🙏 帮我们做得更好

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

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