El harness de agentes de DeepSeek AI que convierte cada capacidad — modelo, herramienta, memoria, agente — en un plugin intercambiable.
DeepSeek Harness (comando dsh) es el framework de agentes open-source de DeepSeek AI que explotó en GitHub con 144.000+ estrellas y la arquitectura "todo es un plugin". No hay núcleo privilegiado: el adaptador de modelos, el registro de herramientas, el log de sesión y el propio bucle del agente son plugins montados sobre Cordis, un framework de composición con contexto, servicios y eventos tipados. Eso significa que puedes extenderlo desde configuración: añadir un proveedor de IA, crear una herramienta nueva con unas pocas líneas de TypeScript, o publicar tu propio bundle. En este curso aprenderás a instalarlo, a manejar la Web UI y la CLI, a usar el Python SDK, a entender Cordis y a escribir y publicar tu primer plugin con herramientas propias.
DeepSeek Harness (comando dsh) es un harness de agentes open-source desarrollado por DeepSeek AI, distribuido con licencia MIT. Un harness es la infraestructura que envuelve a un modelo de lenguaje y le da piernas: acceso a archivos, shell, herramientas, memoria de sesión, delegación a subagentes y un bucle de razonamiento. Es el mismo concepto que está detrás de otros agentes de terminal, pero con una diferencia radical en el diseño.
Su arquitectura se resume en una frase: everything is a plugin (todo es un plugin). El adaptador de modelos, el registro de herramientas, el log de sesión, la política de permisos e incluso el propio bucle del agente son plugins que se montan sobre Cordis, un framework de composición cuyas ideas se describen en el paper A Programming Paradigm for Spatiotemporal Composability. No existe un núcleo que haya que parchear: para extender dsh, montas un plugin junto a los demás.
| Dato | Valor |
|---|---|
| Repo | deepseek-ai/deepseek-harness |
| Lenguaje | TypeScript (runtime npm) + Python SDK |
| Estrellas | 144.000+ |
| Licencia | MIT |
| Estado | Developer preview — hay cambios que rompen compatibilidad |
| Framework subyacente | Cordis (vendorizado dentro del repo) |
| Web UI | http://127.0.0.1:3080 por defecto |
El proyecto está en developer preview e itera rápido. La documentación oficial avisa: THERE WILL BE COMPATIBILITY-BREAKING CHANGES. Úsalo para experimentar y construir, pero no lo trates todavía como una plataforma estable en producción.
Si ya conoces agentes como OpenClaw o DeerFlow, piensa en dsh como el que lleva la filosofía de plugins hasta el extremo: cada parte del producto es reemplazable desde configuración, sin tocar código del framework.
La forma más rápida de probar dsh es desde npm con npx, sin clonar el repositorio. Solo necesitas Node.js instalado.
npx @deepseek-ai/dsh web
El comando arranca la Web UI y la sirve en http://127.0.0.1:3080 por defecto. El proceso usa el directorio desde el que lo lanzas como ubicación por defecto del filesystem.
Si quieres la versión de desarrollo (por ejemplo para escribir plugins en el propio repo), clona y compila:
git clone https://github.com/deepseek-ai/deepseek-harness.git && cd deepseek-harness
pnpm install — el repo es un monorepo pnpm; necesitas pnpm instalado.
pnpm run build — las ejecuciones en producción requieren artefactos compilados.
pnpm dsh web — ejecuta la entrada TypeScript y reenvía los argumentos.
$ pnpm dsh web
[dsh] Web UI listening on http://127.0.0.1:3080
Los perfiles web y headless se auto-inicializan en el primer uso a partir de plantillas incluidas. No necesitas crear nada a mano para empezar.
Abre http://127.0.0.1:3080. Una Web UI recién arrancada no tiene ningún workspace seleccionado, así que el primer paso es elegir uno.
dsh y selecciónalo. El compositor de sesiones no se habilita hasta que hay un workspace.Inicia una sesión y escribe algo como:
Resume este repositorio e identifica sus paquetes principales.
El agente puede leer y editar archivos del workspace, ejecutar comandos, delegar trabajo y mantener un plan. La Web UI pide aprobación antes de las operaciones que lo requieran según la política de permisos activa (por defecto, el perfil web pide confirmación para acciones sensibles).
| Acción del agente | ¿Pide aprobación? |
|---|---|
| Leer archivos del workspace | No, en general |
| Editar archivos | Según política de permisos |
| Ejecutar comandos de shell | Sí, según la política activa |
| Delegar a subagentes | Depende de la composición |
El agente puede modificar cualquier cosa dentro del workspace que selecciones. Para pruebas, usa un directorio aislado y descartable, no tu carpeta personal.
dsh no te ata a DeepSeek. Además de la tarjeta oficial, puedes añadir proveedores del catálogo (Anthropic, OpenAI…) y proveedores personalizados compatibles con OpenAI para gateways corporativos o servidores self-hosted.
Elige Add provider, selecciona un proveedor como Anthropic u OpenAI, introduce su API key y guarda. El catálogo instalado aporta endpoint, protocolo y lista de modelos. Los proveedores con autenticación nativa (Bedrock, Vertex, Azure, Codex) necesitan sus credenciales nativas: credenciales AWS, proyecto ADC, api-version o OAuth respectivamente.
Elige Add a custom provider para un endpoint propio. Necesitas un Provider ID en minúsculas, base URL, protocolo API, credencial y al menos un modelo.
# $DSH_HOME/settings.yaml (ejemplo de proveedor personalizado con visión)
llm-pi-ai:
providers:
my-gateway:
apiKeyEnv: GATEWAY_API_KEY
api: openai-completions
baseURL: https://gateway.example/v1
models:
- id: legacy-chat
- id: vision-preview
input: [text, image]
| Campo | Qué hace |
|---|---|
apiKeyEnv | Variable de entorno con la credencial |
api | Protocolo: openai-completions, etc. |
baseURL | Endpoint del gateway |
models | Lista de modelos; input: [text, image] habilita imagen para ese modelo |
defaultInput | Fallback de modalidades para modelos no descritos |
Las claves se guardan en $DSH_HOME/.credentials.yaml y son write-only: la UI recibe solo un descriptor redactado, nunca el secreto literal. Los ajustes guardan únicamente la referencia a la credencial.
MISSING_CREDENTIAL: guarda la clave por la página Models o define la variable de entorno referenciada. UNKNOWN_MODEL: selecciona un modelo configurado o añade el que falte al proveedor personalizado. 401 al listar modelos: la detección llama a GET /models; si tu endpoint no lo expone, introduce los modelos a mano.
El comando dsh es el lanzador de perfiles: composiciones ordenadas de capas de bundles de plugins bajo tus propios overlays. Los modos principales:
| Comando | Propósito |
|---|---|
dsh --profile <nombre> | Arranca el perfil nombrado bajo $DSH_HOME/profiles/<nombre> |
dsh web | Alias de --profile web: Web UI con servidor |
dsh --profile headless "job" | Ejecuta una sesión nueva, imprime la respuesta final y sale (sin servidor) |
dsh plugin --profile <n> <pnpm args> | Gestiona los plugins del perfil reenviando a pnpm |
dsh --dump-config | Muestra el árbol compuesto sin arrancar nada |
Las capas se aplican sobre una lista vacía en este orden, y las capas posteriores ganan por fila:
dsh.profile.bundles (empezando por @deepseek-ai/dsh-base).cordis.patch.yml del propio perfil.cordis.patch.yml de nivel home ($DSH_HOME) — preferencias locales compartidas por todos los perfiles.--patch <path> en orden de argv.Los bundles base son la primera capa de todo perfil: dsh-base (adaptadores de modelo, herramientas, persistencia, sandbox, permisos, ajustes, credenciales, telemetría), dsh-web-app (aplicación de navegador) y dsh-headless (runner de una sola pasada sin servidor).
dsh --profile web --dump-config imprime el árbol que tu máquina arranca de verdad. Cualquier fila que imprima puede ser reemplazada por un patch propio — así se extiende dsh sin tocar el framework.
Un patch que sobrescribe una fila por id sustituye todo su config, no hace merge de claves. Si sobrescribes una fila de dsh-base, debes repetir todas las claves que esa fila necesite, no solo la que cambias.
Si prefieres Python a la Web UI, el SDK publicado deepseek-harness-sdk permite lanzar agentes desde tu propio programa. El runtime empaquetado del SDK no necesita Node.js del sistema.
git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
python -m venv .venv
. .venv/bin/activate
python -m pip install deepseek-harness-sdk
export DEEPSEEK_API_KEY=sk-tu-clave
python examples/jsonrpc-agent/minimal.py \
--workspace /ruta/absoluta/workspace \
--session-root /ruta/absoluta/sessions \
--session-id example-001 \
"Inspecciona el repositorio y arregla los tests que fallan."
from pathlib import Path
from deepseek_harness import DeepSeekHarness
config = Path("examples/jsonrpc-agent/minimal.cordis.yml").resolve()
workspace = Path("/ruta/absoluta/workspace").resolve()
sessions = Path("/ruta/absoluta/sessions").resolve()
with DeepSeekHarness(
provider="deepseek-official",
model="deepseek-v4-flash",
max_tokens=49_152,
cwd=str(workspace),
session_root=str(sessions),
cordis=str(config),
) as harness:
result = harness.run(
"Inspecciona el repositorio y arregla los tests que fallan.",
session_id="example-001",
)
print(result.final_response)
DeepSeekHarness arranca el runtime perezosamente y lo reutiliza hasta que el context manager sale. Reutilizar el mismo harness y el mismo session_id conserva el proceso Bash de la sesión: directorio de trabajo, variables exportadas y funciones de shell. Para una tarea independiente usa un session id nuevo.
| Propiedad | Valor del ejemplo minimal |
|---|---|
| System prompt | DSH_SYSTEM_PROMPT o "You are a helpful software engineer assistant." |
| Modelo | --model → DSH_MODEL → deepseek-v4-flash |
| Herramientas visibles | bash persistente y str_replace_editor |
| Timeout de bash | 300 segundos |
| Persistencia | JSONL sin comprimir en DSH_SESSION_ROOT |
| Sandbox | danger-full-access — solo en checkout desechable o contenedor |
La composición minimal usa danger-full-access: Bash y el editor pueden modificar cualquier ruta visible por el proceso. Ejecútala solo dentro de un checkout desechable o un contenedor, nunca contra tu sistema personal.
Para escribir plugins necesitas las cinco ideas de Cordis, el framework vendorizado debajo de dsh:
inject y apply(ctx) opcionales, o una subclase de Service cuyo ciclo de vida Cordis monta en el contexto actual.ctx.<key> (como ctx.tools, ctx.llm, ctx.sessions); otros plugins lo encuentran por clave, sin importar una implementación concreta.inject. Un plugin que nombra servicios requeridos espera a que existan; el orden de carga se expresa por requisitos, no por secuencia manual.emit, waterfall, parallel o serial según si los listeners observan, envuelven, fan-out o corren en orden.ctx.effect() o ctx.on() para que reload y teardown los deshagan de forma predecible.| Modo | ¿Espera? | Orden de dispatch | ¿Devuelve valor? |
|---|---|---|---|
emit | No | Orden de registro | No |
waterfall | No | Orden de registro | Sí |
parallel | Sí | Todos en paralelo | No |
serial | Sí | Orden de registro | Sí |
El bucle se organiza en steps (una petición de modelo + las herramientas que llama) y turns (cero o más steps, desde que se reclama la primera entrada hasta que no queda nada pendiente). Los eventos del log de sesión (turn/start, step/start, assistant/*, tool/*) son hechos durables; los eventos agent/* son puntos de extensión en vivo (inbox, status, request, steering).
| Paquete | Es dueño de | Clave ctx |
|---|---|---|
| core/session | Log append-only SessionEvent | ctx.sessions |
| core/system-prompt | Ensamblado de secciones de prompt y esquemas | ctx.systemPrompt |
| core/tools | Registro de herramientas con scope y pipeline de ejecución | ctx.tools |
| core/agent + agent-loop | Interfaz Agent, registro y driver por defecto | ctx.agents, ctx.agentLoop |
| llm/llm | Vocabulario de mensajes/stream y el seam de adaptadores | ctx.llm |
¿Dónde va cada cosa nueva? Añadir un proveedor de modelo → registrar su adaptador en ctx.llm. Añadir una capacidad visible por el modelo → registrarla en ctx.tools. Interceptar una petición o un turno → escuchar sus eventos agent/* o tools/*. Los eventos son el punto de extensión; los métodos de servicio, la llamada directa.
Un plugin de dsh es un módulo TypeScript que exporta una función apply. El framework la llama al cargar el plugin y le pasa un contexto ctx con el que registras capacidades. Eso es toda la configuración.
Desde la raíz del checkout del repositorio:
mkdir -p scratch-plugin/src
Crea scratch-plugin/src/my-plugin.ts:
Un overlay Web que inserta el plugin local (la ruta debe ser absoluta).
pnpm dsh web --patch ./scratch-plugin/cordis.yml y abrir http://127.0.0.1:3080: el terminal imprime [hello-plugin] plugin loaded!.
// scratch-plugin/src/my-plugin.ts
import type { Context } from '@deepseek-ai/cordis'
export const name = 'hello-plugin'
export function apply(ctx: Context) {
// Las dependencias requeridas están listas antes de que apply corra.
console.log('[hello-plugin] plugin loaded!')
}
# scratch-plugin/cordis.yml — overlay Web que inserta el plugin local
- insert:
- id: hello
name: '/ruta/absoluta/al/deepseek-harness/scratch-plugin/src/my-plugin.ts'
Cualquier cosa registrada con ctx — listeners, herramientas, timers — se limpia al descargar el plugin; no necesitas removeListener manual. Para un recurso con limpieza explícita (una conexión de red), usa ctx.effect() y devuelve su disposer. Si tu plugin consume otro servicio como tools o llm, decláralo en inject y Cordis esperará a que exista:
import type { Context } from '@deepseek-ai/cordis'
export const name = 'my-tool-plugin'
export const inject = ['tools']
export function apply(ctx: Context) {
// ctx.tools está listo aquí.
ctx.tools.register(/* ... */)
}
Además de la forma función, un plugin puede usar forma objeto ({ name, inject, apply }) o forma clase (subclase de Service con static inject y constructor que llama a super(ctx, 'miServicio')). La forma función basta en la mayoría de los casos; usa la clase cuando tu plugin preste un servicio a otros plugins.
Las herramientas son la forma en que el modelo toca el mundo real. Vamos a añadir una herramienta greet a la Web UI, reutilizando el directorio scratch-plugin de la lección anterior.
// scratch-plugin/src/my-plugin.ts
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'greet-tool'
export const inject = ['tools']
export function apply(ctx: Context) {
ctx.tools.register(defineTool({
name: 'greet',
description: 'Saluda a alguien por su nombre.',
parameters: {
name: { type: 'string', required: true, description: 'El nombre a saludar' },
},
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
async execute(args) {
return `Hello, ${args.name}!`
},
}))
}
inject hace que Cordis espere al registro de herramientas. defineTool infiere y valida args a partir de parameters; execute devuelve el valor canónico declarado por output.schema, y output.render convierte ese valor en contenido visible por el modelo.
pnpm dsh web --patch ./scratch-plugin/cordis.yml
http://127.0.0.1:3080
Escribe: Usa la herramienta greet para saludar a Ada. El modelo puede llamar a greet y recibe Hello, Ada! como resultado de la herramienta.
Para aceptar configuración vía cordis.yml, exporta un tipo Config y un esquema del mismo nombre (Schemastery) con los valores por defecto directamente en los campos:
import type { Context } from '@deepseek-ai/cordis'
import Schema from '@deepseek-ai/schemastery'
export const name = 'my-plugin'
export interface Config {
greeting: string
maxRetries: number
verbose?: boolean
}
export const Config: Schema<Config> = Schema.object({
greeting: Schema.string().default('Hello'),
maxRetries: Schema.number().default(3),
verbose: Schema.boolean().default(false),
})
export function apply(ctx: Context, config: Config) {
console.log(config.greeting) // Valor del usuario o default del esquema.
}
Al cargar el plugin, Cordis usa el esquema exportado para validar la configuración y rellenar los defaults. No exportes un objeto plano como Config: no implementa la interfaz Standard Schema que Cordis necesita.
El esquema es la fuente de verdad para la UI de configuración del plugin: los defaults del esquema son los que ve el usuario, y los valores del cordis.yml los sobrescriben.
Hasta ahora cargamos plugins locales con overlays --patch. Para distribuirlos, dsh usa dos conceptos definidos en package.json bajo la clave dsh:
dsh.bundle (un patch file que inserta o sobrescribe filas de plugins).$DSH_HOME/profiles/<nombre> que describe una composición ejecutable. Su manifiesto declara dsh.profile (qué bundles componen el perfil, en qué orden).Un bundle es lo que escribes y distribuyes; un profile es lo que el usuario arranca con dsh --profile. Nada es ambas cosas.
hello-plugin/
├── package.json # declara dsh.bundle
├── cordis.patch.yml # capa que se aplica al listar el bundle en un perfil
└── index.js # módulos de plugin referenciados por las filas del patch
// hello-plugin/package.json
{
"name": "dsh-hello-plugin",
"version": "0.1.0",
"type": "module",
"main": "index.js",
"files": ["index.js", "cordis.patch.yml"],
"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }
}
# hello-plugin/cordis.patch.yml — la fila referencia el paquete por nombre
- insert:
- id: hello
name: dsh-hello-plugin
dsh plugin reenvía a pnpm dentro del directorio del perfil, así que cualquier verbo pnpm funciona. El primer uso inicializa el perfil con @deepseek-ai/dsh-base como primer bundle y añade el tuyo:
dsh plugin --profile demo add ./hello-plugin
dsh --profile demo --dump-config # muestra la capa "# == dsh-hello-plugin"
dsh --profile demo
dsh plugin --profile demo remove dsh-hello-plugin elimina tanto la dependencia como la capa.
También puedes instalar directo de un host git: dsh plugin --profile demo add github:you/hello-plugin. Pero una instalación git trae fuentes, no artefactos compilados: si tu paquete es TypeScript, llegará sin su salida lib/ y fallará al cargar. Necesitas dos cosas:
prepare que compile los entry points publicados de forma autónoma.pnpm-workspace.yaml del perfil: allowBuilds: { dsh-hello-plugin: true }, porque pnpm ≥ 10 se niega a ejecutar scripts prepare de dependencias git sin permiso explícito.Esa autorización es permiso para ejecutar el código del paquete en tu máquina durante la instalación, fuera del sandbox del agente. Solo permite paquetes cuya fuente confíes, y fija un commit (github:you/hello-plugin#<sha>) para que un push posterior no cambie silenciosamente lo que se ejecuta.
¿No quieres pedir permiso de build a tus usuarios? Distribuye artefactos ya compilados: publica a npm con lib/ construido en pnpm publish, o genera un tarball con pnpm pack e instala con dsh plugin add ./hello-plugin-0.1.0.tgz. Así no se necesita ninguna autorización de build.
Para la Web UI y la CLI sí: el comando npx @deepseek-ai/dsh web requiere Node.js. Pero el Python SDK (deepseek-harness-sdk) incluye un runtime empaquetado que no necesita Node.js del sistema — solo Python 3.10+.
Sí. El catálogo incluye proveedores como Anthropic u OpenAI (con sus credenciales nativas en el caso de Bedrock, Vertex, Azure o Codex), y puedes añadir cualquier proveedor personalizado compatible con OpenAI apuntando a tu propio gateway o servidor self-hosted.
En $DSH_HOME/.credentials.yaml, con formato write-only: la UI guarda una referencia redactada y nunca muestra el secreto literal. Los ajustes de settings.yaml pueden referenciar variables de entorno con apiKeyEnv.
El proyecto está en developer preview y avisa explícitamente de cambios que rompen compatibilidad. Es ideal para experimentar y construir agentes propios, pero no lo uses todavía como plataforma crítica sin plan de contingencia ante breaking changes.