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.
📋 Pré-requisitos: Ter completado 22-capability.md, entender capacidade três papéis
1. O Que Você Vai Aprender
- Exemplo completo: Definition → Provider → Consumer
- Definindo uma capacidade de estatísticas de arquivo
- Implementando um Provider local
- Implementando um Provider sandbox remoto
- Consumindo a capacidade em uma ferramenta
- Efeitos da alternância de Providers
2. Estrutura do Projeto
(1) Diretórios
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
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:
- Contar arquivos
- Calcular tamanho do diretório
- Classificar por extensão
- Verificar se um caminho existe
(2) Código da Definition
▶ Exemplo 1: Definition da Capacidade FileStats
// 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
// 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
// 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
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:
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
// 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
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
// 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
# cordis.yml — Desenvolvimento local
plugins:
file-stats:
$insert: /home/alice/dev/file-stats-capability/providers/local
Agent chama ferramenta file_info:
👤 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
# 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:
👤 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
yaml file-stats: $insert: ./providers/sandbox config: endpoint: http://sandbox:8080 # config específica do Provider sandbox providerInfo() à Definition; cada Provider retorna suas próprias informações.📖 Resumo
- Fluxo completo: Definition → Provider → Consumer → verificação de alternância
- Definition declara a interface
FileStatsCapabilitycom countFiles/calcSize/analyze/exists - Provider Local usa API fs diretamente; Provider sandbox delega via HTTP para serviço remoto
- Consumer depende apenas da interface — não sabe nem se importa com o Provider específico
- Alternar Providers requer apenas mudar configuração cordis.yml; código Consumer tem zero modificações
- Todos os Providers devem retornar resultados no mesmo formato — esta é a restrição central da Definition
📝 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.