DeepSeek Harness: Prática

Última atualização: 2026-08-31

Você entende os três papéis na teoria, mas teoria sozinha não basta. Esta lição percorre um projeto prático completo — implementando uma capacidade de estatísticas de arquivo substituível do zero: definindo a interface, escrevendo uma implementação local, escrevendo uma implementação sandbox, consumindo a capacidade e alternando Providers. Fluxo de trabalho completo, ponta a ponta.

💡 Dica: O ponto-chave deste projeto é "código Consumer não muda ao alternar Providers" — se o Consumer precisa de mudanças de código, sua abstração Definition não é boa o suficiente.

📋 Pré-requisitos: Ter completado 22-capability.md, entender capacidade três papéis

1. O Que Você Vai Aprender

Estrutura do MyCap


2. Estrutura do Projeto

(1) Diretórios

TEXT 📖 Somente leitura
file-stats-capability/
├── definition.ts            ← Definition
├── providers/
│   ├── local.ts             ← Provider Local
│   └── sandbox.ts           ← Provider Sandbox Remoto
├── consumer/
│   └── file-info-tool.ts    ← Plugin Consumer
├── types.ts                 ← Declarações de tipo
└── index.ts                 ← Exports

(2) Diagrama

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

3. Definindo a Capacidade de Estatísticas de Arquivo

(1) Análise de Requisitos

A capacidade de estatísticas de arquivo precisa fornecer:

(2) Código da Definition

▶ Exemplo 1: Definition da Capacidade FileStats

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) Declarações de Tipo

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

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

4. Implementando o Provider Local

(1) Código

▶ Exemplo 2: Provider Local Completo

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) Registrar no cordis.yml

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

5. Implementando o Provider Sandbox Remoto

(1) Design

O Provider sandbox delega operações de arquivo a um serviço remoto:

100%
graph LR
    TOOL[Consumer] -->|chama| CAP[capacidade file-stats]
    CAP -->|HTTP| SANDBOX[Serviço Sandbox<br/>sandbox:8080]
    SANDBOX -->|opera em| FS[Sistema de arquivos isolado]

(2) Código

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) Registrar no cordis.yml

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

6. Consumindo a Capacidade em uma Ferramenta

(1) Código do Plugin Consumer

▶ Exemplo 3: Ferramenta Consumer Usando a Capacidade

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) Observação-Chave

O código Consumer não contém menção de local ou sandbox — ele depende apenas da interface de capacidade file-stats. Esse é o valor do sistema de capacidade.


7. Efeitos da Alternância de Providers

(1) Usando o Provider Local

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

Agent chama ferramenta file_info:

TEXT 📖 Somente leitura
👤 Alice: Analise o diretório /home/alice/project

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

(2) Alternando para o Provider Sandbox

YAML
# cordis.yml — Ambiente sandbox
plugins:
  file-stats:
    $replace: /home/alice/dev/file-stats-capability/providers/sandbox
    config:
      endpoint: http://sandbox:8080

Após reiniciar, Agent chama a mesma ferramenta:

TEXT 📖 Somente leitura
👤 Alice: Analise o diretório /workspace/project

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

Código Consumer está completamente inalterado, formato de resultado é idêntico — apenas a implementação subjacente mudou de sistema de arquivos local para API sandbox remota.

(3) Comparação

Dimensão Provider Local Provider Sandbox
Implementação Chamadas fs diretas Chamadas API HTTP
Sistema de arquivos Disco local Ambiente sandbox
Latência < 1ms 50-200ms
Segurança Acesso direto Isolamento sandbox
Código Consumer Mesmo Mesmo
Formato de retorno Mesmo Mesmo

❓ Perguntas Frequentes

P Ambos os Providers devem retornar exatamente o mesmo formato?
R Sim. Esta é a restrição central da Definition — todos os Providers devem seguir a mesma interface. Formatos de retorno diferentes causam erros de parsing no Consumer.
P O Provider sandbox tem latência maior — o Consumer precisa tratar isso?
R Não. Latência é transparente para o Consumer. Se otimização é necessária, adicione cache na camada Consumer ou use indicadores async.
P Como garantir que um Provider implementa todos os métodos?
R Verificação em tempo de compilação TypeScript. Se um Provider está sem métodos da Definition, a compilação falha.
P O sistema de capacidade pode ser usado na configuração de plugin?
R Sim. Diferentes Providers podem ter Config diferente: yaml file-stats: $insert: ./providers/sandbox config: endpoint: http://sandbox:8080 # config específica do Provider sandbox
P Como um Consumer sabe qual Provider está ativo?
R Geralmente não precisa. Se realmente necessário, adicione um método providerInfo() à Definition; cada Provider retorna suas próprias informações.

📖 Resumo


📝 Exercícios

1. ⭐ Básico: Siga os passos desta lição para criar a Definition e o Provider local da capacidade FileStats, registre-o, e verifique via chamadas da ferramenta Consumer.

2. ⭐⭐ Intermediário: Implemente um InMemoryProvider — dados de arquivo pré-armazenados em um Map de memória (sem acesso real ao sistema de arquivos), adequado para testes unitários. Verifique que a ferramenta Consumer funciona normalmente com InMemoryProvider.

3. ⭐⭐⭐ Desafio: Implemente um CachedProvider — padrão decorator, adicionando uma camada de cache na frente de outro Provider. Cache os últimos N resultados de análise; mesmos caminhos retornam resultados em cache diretamente. Nota: CachedProvider também é um Provider; ele internamente delega a outro Provider.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%