DeepSeek Harness Sistema de eventos: emit / bail / serial / waterfall
¿Cómo se comunican los plugins con bajo acoplamiento? Los eventos son el mecanismo central de comunicación de Cordis.
Harness utiliza ampliamente los eventos para implementar puntos de extensión escalables; en este artículo se explican en detalle los cinco modos de distribución.
Uso básico
Los eventos tienen dos extremos: escucha y disparo.
Ejemplo
ctx.on('event-name', (payload) => {
// Manejar el evento
})
// Disparar el evento: transmitir a todos los listeners
ctx.emit('event-name', payload)
Los listeners registrados mediante ctx.on se eliminan automáticamente al desinstalar el plugin.
Cinco modos de distribución
Cordis proporciona varios modos de distribución, adecuados para diferentes contratos de interacción.
| Modo | Método de distribución | ¿Usa await? | Orden | ¿Tiene valor de retorno? | Escenario típico |
|---|---|---|---|---|---|
| emitDifusión | ctx.emit | no | Según el orden de registro | no | Tipo notificación: todos los listeners observan |
| bailCortocircuito | ctx.bail | no | Según el orden de registro | Sí | Tipo decisión: gana el primer valor de retorno válido |
| serialSecuencial | ctx.serial | Sí | Según el orden de registro | Sí | Inicialización por fases: ejecutar en orden y esperar |
| waterfallCanalización (pipe) | ctx.waterfall | no | Según el orden de registro | Sí | Cadena de procesamiento: envolver el valor de retorno downstream capa por capa |
| parallelParalelo | ctx.parallel | Sí | Todo en paralelo | no | Fan-out: múltiples listeners procesan en paralelo |
Cada evento tiene exactamente un modo de distribución, y solo se puede distribuir mediante el método correspondiente.
La documentación oficial de introducción marca waterfall como "sin await"; pero como los listeners suelen ser funciones asíncronas, en los ejemplos del tutorial generalmente se escribe await ctx.waterfall(...) para esperar el resultado final de toda la cadena de procesamiento. Solo hay que recordar esta diferencia; ninguna de las dos formas es incorrecta.
emit: difusión
Todos los listeners se ejecutan de forma síncrona y los valores de retorno se ignoran.
Ejemplo
ctx.emit('my-plugin/ready', { id: 'ejemplo-worker-1' })
// Listener: al recibir el mensaje de listo, imprime un registro
ctx.on('my-plugin/ready', ({ id }) => {
console.log(`${id} is ready`)
})
bail: cortocircuito
Los listeners se ejecutan en orden; el primer valor de retorno que no sea null, false ni undefined se convierte en el resultado final.
Ejemplo
const result = ctx.bail('some-check', input)
// Listener: si cumple la condición de intercepción, devuelve 'blocked'; de lo contrario, continúa
ctx.on('some-check', (input) => {
if (shouldBlock(input)) return 'blocked'
// Devolver null / false / undefined significa "no tengo opinión", permitiendo que los listeners siguientes continúen
})
serial: ejecución secuencial
Los listeners se ejecutan en orden de registro y esperan resultados asíncronos.
El primer valor de retorno que no sea null, false ni undefined detiene la ejecución posterior.
Ejemplo
await ctx.serial('setup-phase', context)
waterfall: canalización (pipe)
Cada listener puede envolver el valor de retorno downstream, formando una cadena de procesamiento.
El listener recibe (...args, next); llamar a next() ejecuta el listener downstream, y el valor de retorno downstream se devuelve a la capa de envoltura actual mediante next().
Ejemplo
const output = await ctx.waterfall('my-plugin/transform', input, async () => input)
// Listener: debe llamar a next(), procesar el resultado downstream y luego devolverlo
ctx.on('my-plugin/transform', async (_input, next) => {
const downstream = await next()
return downstream.trim()
})
El listener de waterfall debe llamar a next(). No llamar a next cortocircuita toda la canalización; esto es un diseño intencional para implementar lógica de intercepción o puerta de enlace (gateway).
Un listener de estrategia, cuando tiene poder de decisión, puede devolver directamente sin llamar a next(), bloqueando así toda la cadena.
Un listener que solo anota u observa debe delegar en el downstream.
Eventos con seguridad de tipos
Utiliza la declaración de fusión (declaration merging) de TypeScript para proporcionar seguridad de tipos a los eventos.
Ejemplo
import '@deepseek-ai/cordis'
// Declaración de fusión: registrar el nombre del evento y su firma
declare module '@deepseek-ai/cordis' {
interface Events {
'my-plugin/ready': (payload: { id: string }) => void
'my-plugin/check': (input: string) => boolean | undefined
'my-plugin/transform': (input: string, next: () => Promise<string>) => Promise<string>
}
}
// En adelante, los argumentos de ctx.on('my-plugin/ready', ...) y ctx.emit('my-plugin/ready', ...)
// se infieren automáticamente; escribir mal el nombre del evento o el tipo de los parámetros generará errores en tiempo de compilación
Eventos Cordis y registro de sesión
Los eventos Cordis de Harness siguen la nomenclatura namespace/action, por ejemplo agent/step, agent/request, agent/request-error, tools/result y session/event.
Ten en cuenta la distinción entre dos tipos de eventos:
| Evento | Qué es | Cómo observarlo |
|---|---|---|
| agent/step, tools/result, etc. | Eventos Cordis, distribución en tiempo real | Directamente con ctx.on('tools/result', ...) |
| turn/*、step/*、tool/call、tool/result、compaction/* | Tipos de eventos de sesión persistidos | Escucha session/event y verifica event.type |
turn/*, step/*, tool/call, tool/result y compaction/* son tipos de eventos de sesión persistidos, no eventos Cordis del mismo nombre.
Cuando necesites observarlos, escucha session/event y verifica event.type.
Ejemplo práctico: plugin de registro
La documentación oficial usa el siguiente plugin para registrar las llamadas a herramientas y sus resultados, escuchando el evento Cordis tools/result.
Ejemplo
import type { Context } from '@deepseek-ai/cordis'
import '@deepseek-ai/dsh-tools'
export const name = 'tool-logger'
export function apply(ctx: Context) {
// Escuchar el evento tools/result: se dispara cada vez que se completa la ejecución de una herramienta
ctx.on('tools/result', (exec, result) => {
// Imprimir el nombre de la herramienta y sus parámetros
console.log(`[tool] ${exec.name}(${JSON.stringify(exec.arguments)})`)
// Concatenar los bloques de texto del contenido del resultado e imprimir solo los primeros 100 caracteres
const text = result.content
.map(block => block.type === 'text' ? block.text : '')
.join('')
console.log(`[tool result] ${text.slice(0, 100)}`)
})
}
Este plugin imprime en la terminal el nombre de la herramienta, los parámetros y el resumen del resultado cada vez que el Agent llama a una herramienta.
Los listeners se registran mediante ctx.on y se eliminan automáticamente al desinstalar el plugin.
Resumen y autoevaluación
Los cinco modos de distribución cubren cinco tipos de contratos: "difusión, decisión, secuencial, canalización y paralelo"; waterfall debe llamar a next(); la fusión con declare module garantiza tipos para los nombres de eventos y sus parámetros.
Ponte a prueba:
- ¿Qué método se utiliza para disparar cada uno de los cinco modos de distribución? ¿Cuáles tienen valor de retorno?
- ¿Qué ocurre si un listener de waterfall no llama a next()? ¿Es un bug o un diseño intencional?
- Para eventos de sesión persistidos como turn/start, ¿qué evento se debe escuchar y qué campo se debe verificar?