Cómo crear un plugin propio para dsh de DeepSeek Harness
Crea un plugin dsh desde una carpeta vacía: configura package.json, añade el archivo de parche, monta una herramienta real e implementa los dos hooks necesarios.
Qué es realmente un plugin de dsh
Un plugin de dsh es un paquete npm que exporta una función apply e incluye un archivo YAML pequeño que indica a DeepSeek Harness que debe cargarlo. No es necesario aprender primero un SDK de plugins independiente. dsh es una aplicación de Cordis, y «todo es un plugin» debe entenderse literalmente: el registro de herramientas, el bucle del agente, el almacén de sesiones y el servidor web son filas del mismo árbol de plugins al que se incorpora su paquete.
Cordis es un framework general de composición. Se desarrolló de forma independiente y se ha utilizado durante años como base del framework de chatbot Koishi. Gestiona la carga y descarga, y resuelve las dependencias entre plugins. No sabe nada sobre agentes. Todo lo relacionado con los agentes procede de los paquetes del harness que se ejecutan sobre él. Por eso la estructura del plugin que se muestra a continuación es tan pequeña. La mayor parte de lo que obtiene se hereda.
Un plugin tiene dos partes. La parte del host se ejecuta en Node, registra herramientas y listeners de eventos, y puede proporcionar sus propios servicios. La parte del navegador se ejecuta dentro de la interfaz web y registra espacios de interfaz. El primer plugin casi siempre sólo se ejecuta en el host, así que considere opcional la parte del navegador hasta que la necesite.
Esta guía se redactó para @deepseek-ai/dsh versión 0.1.0-rc.7, la etiqueta npm latest del 19 August 2026. dsh es una versión preliminar para desarrolladores y su propio README indica que habrá cambios incompatibles. Cada nombre de clave que aparece a continuación se obtuvo de la documentación upstream y del repositorio en esa fecha. Vuelva a comprobarlos antes de depender de ellos, porque una API preliminar puede cambiar los nombres de los campos entre candidatos de lanzamiento. Si el harness todavía no se está ejecutando, configúrelo primero mediante DeepSeek Harness en un VPS y la clave de API y la configuración del modelo de dsh, y después vuelva aquí.
Cargue un solo archivo de prueba antes de empaquetar nada
Empezar por el empaquetado es la forma más lenta de aprender esto. Cargue un solo archivo, confirme que el runtime llama a su código y, después, empaquételo.
Cree una carpeta fuera del checkout del harness y coloque un archivo en ella.
import type { Context } from '@deepseek-ai/cordis'
export const name = 'hello-plugin'
export function apply(ctx: Context) {
console.log('[hello-plugin] plugin loaded')
}export const name son metadatos que se usan para identificar el plugin en los diagnósticos. apply es el contrato completo: Cordis lo llama una vez y le pasa un contexto limitado a su plugin. Todo lo que registre en ese contexto se deshace automáticamente cuando se elimina el plugin.
Junto a él, escriba cordis.yml.
- insert:
- id: hello
name: '/absolute/path/to/scratch-plugin/hello.ts'Ahora inicie un perfil con ese archivo superpuesto.
dsh web --patch ./scratch-plugin/cordis.ymlSi dsh no está en su PATH, npx @deepseek-ai/dsh web --patch ./scratch-plugin/cordis.yml realiza la misma función. Esa vía con npx puede proporcionarle un release candidate antiguo en caché en lugar de la versión descrita en esta guía. Por tanto, si el harness rechaza directamente un flag documentado, consulte las soluciones para los errores de instalación y versión de dsh antes de empezar a dudar de su propio archivo. Debería ver [hello-plugin] plugin loaded en la terminal desde la que inició dsh. Si no aparece nada, la fila no se resolvió.
El campo name acepta un nombre de paquete npm o una ruta del sistema de archivos, y la documentación upstream indica que la ruta debe ser absoluta. Un ./hello.ts relativo es lo primero que debe comprobar cuando un plugin de prueba no produce ninguna salida. Lo segundo es la extensión del archivo. El flujo documentado se ejecuta como pnpm dsh web --patch ... desde un clon del repositorio del harness, donde las entradas de TypeScript se cargan mediante tsx. Si obtuvo dsh desde npm, indique en la fila un archivo JavaScript simple o compile primero el archivo.
--patch es un flag del launcher y su overlay se aplica al final, después de todos los bundles y de su propio parche de perfil. Por tanto, un overlay de prueba siempre tiene prioridad, que es exactamente lo que necesita mientras itera.
Escriba la herramienta más pequeña que haga algo útil
Una línea de registro demuestra que el complemento se carga. Una herramienta demuestra que el complemento forma parte del agente.
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: 'Greet someone by name.',
parameters: {
name: { type: 'string', required: true, description: 'The name to greet' },
},
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
async execute(args) {
return `Hello, ${args.name}!`
},
}))
}export const inject = ['tools'] es la línea que suele omitirse. Las entradas de una configuración de Cordis se inician de forma simultánea, por lo que la posición de una fila en el archivo no garantiza ningún orden de carga. El orden procede de las dependencias declaradas. inject indica a Cordis que espere hasta que exista ctx.tools antes de llamar a su apply. Sin esto, el código puede ejecutarse cuando el registro aún no esté disponible para registrar la herramienta.
El resto del objeto define el contrato que ve el modelo. parameters es el esquema de argumentos y execute recibe los argumentos que ya se han validado y analizado según ese esquema. output.schema describe el valor que devuelve execute, mientras que render convierte ese valor en los bloques de contenido que lee el modelo. Mantener ambos aspectos separados permite que la interfaz muestre una cosa mientras el modelo lee otra.
Inicie el perfil y pida al asistente que salude a alguien por su nombre. La respuesta vuelve a través de su execute. El registro mediante ctx se puede revertir, por lo que al liberar el complemento la herramienta se anula automáticamente. Para cualquier recurso que Cordis no pueda conocer, como un socket o un descriptor de archivo, llame a ctx.effect() y proporciónele una función de liberación.
Los dos puntos de extensión que realmente usa el primer plugin
La lista completa de puntos de integración es larga. Estos dos cubren casi todos los primeros plugins.
Los eventos de conversación son el flujo persistente registrado. Sus nombres son session/event, turn/start, turn/end, step/start, step/end, user/message, assistant/message, assistant/chunk, tool/call y tool/result. Se les conecta un listener normal.
ctx.on('tool/call', (payload) => {
console.log('[my-plugin] tool/call', JSON.stringify(payload))
})Imprima el payload una vez y léalo. No copie los nombres de los campos del payload de ninguna guía, incluida esta, porque la estructura del payload es la parte de una API preliminar que cambia con mayor frecuencia.
El segundo punto de extensión es el waterfall. agent/pre-step, agent/request, agent/request-error, llm/stream y los eventos tools/* son waterfalls, y un listener de waterfall tiene una firma distinta. Recibe un callback next, y la cadena sólo continúa si lo llama.
ctx.on('agent/request', async (payload, next) => {
const startedAt = Date.now()
const downstream = await next()
console.log('[my-plugin] model request took', Date.now() - startedAt, 'ms')
return downstream
})Si olvida await next(), no ha añadido un hook. Ha sustituido la llamada al modelo por nada, y el agente se detiene ahí, porque el cortocircuito es el comportamiento previsto para un plugin de gateway que deniega una petición deliberadamente. Esta diferencia causa la mayoría de las confusiones al crear el primer plugin. Escriba la llamada a next() antes de escribir cualquier otra cosa a su alrededor.
agent/request envuelve la propia llamada al modelo. Su payload contiene el agente que realiza la llamada, el número del turno abierto, el paso al que pertenece la petición y la señal de cancelación de ese turno. Por eso es el punto adecuado para un registrador de peticiones o un limitador de tasa. Los waterfalls tools/* tienen la misma estructura un nivel más abajo. tools/pre-execute permite, deniega o solicita aprobación antes del envío. tools/execute envuelve el envío. tools/post-execute puede sustituir o bloquear el resultado normalizado. tools/result sólo observa el resultado congelado.
Empaquetarlo como un paquete instalable
Un bundle es un paquete de npm cuyo package.json declara un campo dsh.bundle que apunta a su archivo de parche. Esa declaración es la única diferencia entre un archivo provisional y algo que se puede instalar.
{
"name": "dsh-plugin-hello",
"version": "0.1.0",
"type": "module",
"main": "lib/index.js",
"files": ["lib", "cordis.patch.yml", "README.md", "LICENSE"],
"engines": { "node": "^22.19 || >=24", "dsh": ">=0.1.0-rc.6" },
"dsh": { "bundle": { "patch": "./cordis.patch.yml" } },
"keywords": ["dsh-plugin", "deepseek-harness"],
"scripts": { "build": "tsdown", "prepare": "pnpm run build" },
"exports": {
".": { "types": "./lib/index.d.ts", "default": "./lib/index.js" },
"./cordis.patch.yml": "./cordis.patch.yml",
"./package.json": "./package.json"
}
}El cordis.patch.yml que aparece junto a él es corto.
- insert:
- id: dsh-plugin-hello
name: dsh-plugin-helloLa fila name es el nombre del paquete, por lo que esas dos cadenas deben coincidir. La fila id es el identificador al que una capa posterior apunta cuando un usuario anula la configuración, así que elija uno estable y no lo reutilice para otro plugin.
files debe incluir cordis.patch.yml. Si se omite, el tarball publicado contiene un dsh.bundle.patch que apunta a un archivo que nunca se incluyó en el paquete, por lo que el paquete se instala pero no aporta nada al árbol.
Instálelo en un perfil desde el directorio que contiene la carpeta del plugin.
dsh plugin --profile demo add ./dsh-plugin-hello
dsh --profile demo --dump-config
dsh --profile demodsh plugin --profile <name> reenvía el resto de sus argumentos a pnpm dentro del directorio de ese perfil, por lo que add y remove se comportan como en pnpm. Desinstálelo con dsh plugin --profile demo remove dsh-plugin-hello. Los perfiles web y headless se crean a partir de plantillas incluidas la primera vez que se usan, y cualquier otro nombre de perfil debe crearse mediante dsh plugin.
Por qué falta su fila en el árbol compuesto
La composición comienza con una lista de entradas vacía y apila las capas en un orden fijo. Primero, cada bundle incluido en dsh.profile.bundles del perfil, en el orden en que aparece. Después, el cordis.patch.yml propio del perfil. Luego, $DSH_HOME/cordis.patch.yml. Por último, cualquier overlay --patch indicado en la línea de comandos. Las capas posteriores sustituyen las filas anteriores por id.
Los perfiles se encuentran en $DSH_HOME/profiles/<name>. Un directorio de perfil contiene un package.json con el manifiesto dsh.profile y su lista ordenada de bundles, además del archivo de parches propio del usuario. Los nombres de los bundles se resuelven primero desde la instalación de dsh y después desde node_modules del perfil. Ahí es donde pnpm coloca un plugin externo al árbol.
dsh --profile demo --dump-config muestra el árbol completamente compuesto sin iniciar nada. Esa salida separa las dos vías de diagnóstico. Si falta el id de su fila, el problema está en la composición: un nombre no se resuelve o nunca se incluyó un archivo de parches. Si la fila está presente y no ocurre nada, el problema está en su código. Responda primero a esa pregunta y evitará la mayor parte de las conjeturas.
Dónde aparecen realmente los errores de carga
Un error lanzado dentro de apply es evidente. El proceso termina con esa excepción y se muestra un seguimiento de pila que señala la línea del código propio.
Los errores de resolución no son visibles de forma directa. El cargador informa mediante el registrador de Cordis sobre los módulos que no puede resolver, en lugar de provocar un fallo del proceso. El tutorial original advierte que estos mensajes pueden perderse durante el arranque porque se emiten antes de asociar los exportadores de consola. Por eso, un error tipográfico en una ruta puede parecer exactamente un plugin que se cargó pero no hizo nada. Esta es la razón por la que conviene ejecutar la comprobación --dump-config anterior antes de revisar el código.
Mantenga un console.log como primera instrucción de apply durante el desarrollo. Si no aparece, sabrá qué parte del problema tiene, y podrá eliminarlo después sin consecuencias. En un servidor, ejecute el arnés en primer plano mientras realiza cambios, en lugar de ejecutarlo mediante un gestor de servicios. Así, la salida del cargador llegará al terminal, en vez de quedar en un journal que después tendría que consultar.
Iterar sin reiniciar todo
La respuesta honesta para la parte del host hoy es reiniciar. El paquete de la aplicación web se publica con su mecanismo compartido de recarga en caliente de módulos deshabilitado, y el archivo incluye una nota que indica que se volverá a habilitar cuando se haya probado el ciclo de vida de la recarga. La cadena de recarga del lado del cliente siempre está montada, pero permanece inactiva hasta que un observador de compilación vuelve a escribir los paquetes del cliente, por lo que tampoco hace nada para la parte de Node.
Haga que el reinicio sea rápido en lugar de intentar conseguir una recarga que todavía no existe. Mantenga el plugin en un solo archivo. Cárguelo con --patch en lugar de instalarlo en un perfil, para que no haya ningún paso de compilación ni de pnpm entre una edición y una ejecución. Registre todo mediante ctx para que un reinicio no deje una herramienta duplicada ni un listener obsoleto. Encapsule todo lo que reserve por su cuenta en ctx.effect() con un disposer real, porque el síntoma habitual de la ausencia de un disposer es que la segunda ejecución falla en un puerto que la primera todavía mantiene ocupado.
Si desarrolla contra un harness que se ejecuta en un servidor en lugar de en su portátil, nada de lo anterior cambia, pero el enlace de Web UI sí importa. El enlace de loopback en el puerto 3080 explica por qué la página no se abre por sí sola y qué hacer al respecto.
La parte del navegador y hasta qué punto confiar en ella
Añádela sólo cuando tu plugin necesite su propia interfaz. Se declara en el mismo campo dsh que el bundle.
{
"dsh": {
"client": {
"platform": "web",
"inject": [],
"external": [],
"immediately": false
}
},
"exports": {
".": "./src/index.ts",
"./client": "./src/client/apply.ts",
"./package.json": "./package.json"
}
}"platform": "web" es obligatorio, y el escáner genera un error si el paquete no tiene una exportación ./client. Por tanto, el mapa de exportaciones forma parte del manifiesto y no es un elemento opcional. La entrada del cliente recibe el Context de Cordis ampliado con el tipo de ejecución del cliente, y todos los registros se realizan dentro de apply mediante ctx.slots.register. Allí no se permiten efectos secundarios a nivel de módulo.
import type { Context } from 'cordis'
import type { DshClientContext } from '@deepseek-ai/dsh-client-runtime'
export async function apply(ctx: Context & DshClientContext) {
ctx.slots.register({ name: 'domain.entry.slot' }, MyComponent)
}Conviene conocer dos detalles antes de empezar. inject en el manifiesto del cliente sirve como documentación, no para programar la activación: registra las dependencias del paquete y no controla el orden de activación. external es donde se declaran las solicitudes de módulos que están fuera de la base, para que se materialicen antes de que el plugin las solicite. Esta es la parte que cambia con mayor rapidez en la versión preliminar, así que consulta packages/client/AGENTS.md en el repositorio del arnés el día que escribas el código, no el día que leas una guía sobre el tema.
Publica el plugin y explica qué toca
Añadir el tema dsh-plugin a un repositorio de GitHub lo incluye en la lista que consultan las personas cuando buscan plugins. Eso implica confiar en un proyecto desconocido y conlleva obligaciones. Esas obligaciones reflejan lo que nuestra guía para evaluar un plugin de dsh antes de instalarlo indica que se debe comprobar, así que redactar el plugin conforme a esa lista es la forma más sencilla de cumplirla.
- Fija las dependencias. Un rango con acento circunflejo en una dependencia transitiva puede hacer que un paquete que era seguro la semana pasada ejecute código diferente esta semana. Ese es el mecanismo exacto de los ataques a la cadena de suministro de npm contra un servidor.
- Haz que el manifiesto indique qué tocas. La lista
injectes un resumen honesto y legible por máquina de los servicios del harness que utilizas. Un revisor la lee en segundos y se forma una opinión a partir de ella. - No hagas llamadas de red silenciosas. Si una herramienta llama a una API, indica el host en el README y permite configurar el endpoint. Las personas que auditan estos proyectos retirarán de la lista un plugin que se conecte a un servidor que no mencionó.
- Mantén
filesajustado. Publicar una carpeta de trabajo completa es la forma de que un archivo de credenciales olvidado llegue al registro. - Proporciona a los instaladores de git un script
prepareque compile sin asumir dependencias exclusivas del desarrollo, e indica en el README que deben incluir explícitamente esa compilación enpnpm-workspace.yamldel perfil. - Fecha el README con respecto al candidato de lanzamiento contra el que compilaste y realizaste las pruebas. Los lectores de una API preliminar necesitan saber qué versión utilizaste.
Para ver desde fuera cómo es un plugin terminado, lee los plugins de dsh que merece la pena instalar y observa qué explica cada README antes de instalarlo. Si has escrito extensiones para otro agente, cómo se estructuran los plugins de Claude Code ofrece una comparación útil. El harness te proporciona un grafo de objetos activo y un registro reversible. Eso ofrece más capacidad que un manifiesto de archivos y también implica más responsabilidad.
FAQ
¿Necesito publicar en npm para escribir un plugin de dsh?
No. Una ruta del sistema de archivos en una superposición cordis.yml, cargada con dsh web --patch ./scratch-plugin/cordis.yml, basta para ejecutar su propio código dentro del harness. La ruta debe ser absoluta. El empaquetado sólo importa cuando otra persona instala el plugin. Incluso entonces, puede instalar una carpeta local con dsh plugin --profile demo add ./my-plugin para probar la forma empaquetada sin acceder a un registro.
¿Por qué se carga mi plugin, pero la herramienta nunca aparece?
Ejecute primero dsh --profile demo --dump-config. Si el id de su fila no aparece en esa salida, el plugin nunca se montó y la causa está en la composición, no en el código. Si la fila aparece, compruebe export const inject = ['tools']. Las entradas de una configuración de Cordis se inician de forma simultánea, por lo que el orden de los archivos no determina el orden de carga. Sin esa declaración, Cordis no espera al registro de herramientas y su apply puede ejecutarse cuando ctx.tools todavía no está disponible para registrarse.
¿Cuál es la diferencia entre cordis.yml y cordis.patch.yml?
cordis.yml es una lista completa de entradas. cordis.patch.yml es una capa que se aplica sobre una lista y que selecciona filas por id para insertar otras nuevas o reemplazar una configuración existente. Un bundle apunta a su propio archivo de parches mediante dsh.bundle.patch en package.json. Las capas se aplican en un orden fijo: primero cada bundle en el orden indicado por el perfil, después el archivo de parches del perfil, luego $DSH_HOME/cordis.patch.yml y, por último, cualquier superposición --patch. Las capas posteriores tienen prioridad.
¿Puedo recargar en caliente un plugin de dsh mientras el agente está en ejecución?
No para la parte del host en el perfil web, al menos en 0.1.0-rc.7. Ese bundle incluye deshabilitada la fila compartida de recarga de módulos en caliente, con una nota en el archivo que indica que volverá cuando se haya probado su ciclo de recarga. En su lugar, diseñe el plugin para reiniciarse rápidamente: use un solo archivo, cárguelo mediante --patch sin un paso de compilación y realice todos los registros mediante ctx para evitar fugas entre ejecuciones. Use ctx.effect() con un disposer para los recursos que Cordis no pueda limpiar por sí solo.