DeepSeek Harness: Sistema de Eventos
Última atualização: 2026-08-31
Eventos são o mecanismo central do Cordis para comunicação fracamente acoplada entre plugins — plugins não chamam uns aos outros diretamente, mas colaboram através de broadcast e inscrição de eventos. Quatro padrões de evento cobrem todos os cenários, de simples notificações a pipelines complexos.
📋 Pré-requisitos: Ter completado 14-inject.md, entender injeção de dependência
1. O Que Você Vai Aprender
- emit: broadcast de evento geral
- bail: eventos interrompíveis
- serial: eventos sequenciais
- waterfall: eventos de passagem em cadeia
- Domínios de evento: session/agent/capability
- Eventos customizados e segurança de tipo
2. emit: Broadcast de Evento Geral
(1) Uso Básico
▶ Exemplo 1: Broadcast e Escuta de Evento emit
emit é o padrão de evento mais simples — faz broadcast de uma notificação; todos os listeners a recebem, valores de retorno são ignorados:
// Emitir evento
ctx.emit('session/created', { id: 'abc-123', user: 'Alice' })
// Escutar evento
ctx.on('session/created', (data) => {
ctx.logger.info(`nova sessão: ${data.id}`)
})
(2) Características
| Característica | Descrição |
|---|---|
| Broadcast | Todos os listeners são chamados |
| Sem valor de retorno | Valores de retorno dos listeners são ignorados |
| Sem interrupção | Listeners não podem impedir listeners subsequentes de executar |
| Sequencial | Listeners executam em ordem de registro |
(3) Cenários Típicos
- Notificações:
session/created,plugin/loaded - Logging:
tool/executed,llm/request - Estatísticas:
request/completed,error/occurred
(4) Múltiplos Listeners
ctx.on('session/created', (data) => {
ctx.logger.info(`logger: ${data.id}`)
})
ctx.on('session/created', (data) => {
ctx.metrics.inc('session_count')
})
ctx.on('session/created', (data) => {
ctx.cache.set(`session:${data.id}`, data)
})
ctx.emit('session/created', { id: 'abc', user: 'Alice' })
// Todos os três listeners executam
3. bail: Eventos Interrompíveis
(1) Uso Básico
▶ Exemplo 2: Intercepção com bail
bail é um evento interrompível — quando um listener retorna um valor não-undefined, listeners subsequentes não executam:
// Listener pode "interceptar" o evento
ctx.on('tool/beforeExecute', (data) => {
if (data.tool === 'shell' && data.params.command.includes('rm')) {
return { denied: true, reason: 'dangerous command' }
}
})
ctx.bail('tool/beforeExecute', { tool: 'shell', params: { command: 'rm -rf /' } })
// → Retorna { denied: true, reason: 'dangerous command' }
// Listeners subsequentes não executam
(2) Características
| Característica | Descrição |
|---|---|
| Interrompível | Retornar não-undefined interrompe a propagação |
| Short-circuit | Primeiro listener a retornar um valor termina a propagação |
| Tem valor de retorno | Chamada bail retorna o valor interceptado |
| Sensível à ordem | Listeners registrados antes interceptam primeiro |
(3) Cenários Típicos
- Verificações de permissão:
tool/beforeExecute(negar operações perigosas) - Filtragem de conteúdo:
message/beforeSend(filtrar conteúdo sensível) - Pulagem condicional:
task/beforeRun(pular tarefas inaplicáveis)
(4) Exemplo de Política de Aprovação
// Plugin de política de aprovação
ctx.on('tool/beforeExecute', (data) => {
const policy = getApprovalPolicy(data.tool)
if (policy === 'deny') {
return { denied: true, reason: `${data.tool} negado pela política` }
}
if (policy === 'ask') {
return { pending: true, requiresApproval: true }
}
// Retornar undefined → não interceptar, continuar propagação
})
// Plugin de sandbox (após aprovação)
ctx.on('tool/beforeExecute', (data) => {
if (!isInSandbox(data.params.cwd)) {
return { denied: true, reason: 'execução fora do sandbox' }
}
})
(5) Verificando Resultado do bail
const result = ctx.bail('tool/beforeExecute', data)
if (result) {
// Interceptado
ctx.logger.warn('execução da ferramenta negada:', result.reason)
} else {
// Não interceptado, pode executar
await executeTool(data)
}
4. serial: Eventos Sequenciais
(1) Uso Básico
serial executa listeners async em ordem, cada um aguardando o anterior completar:
ctx.on('session/initialized', async (data) => {
await loadUserPreferences(data.userId)
})
ctx.on('session/initialized', async (data) => {
await setupWorkspace(data.workspaceId)
})
ctx.on('session/initialized', async (data) => {
await warmCache(data.projectPath)
})
// Três listeners executam sequencialmente
await ctx.serial('session/initialized', { userId: 'alice', workspaceId: 'ws-1' })
(2) Características
| Característica | Descrição |
|---|---|
| Sequencial | Listeners executam um a um em ordem |
| Assíncrono | Suporta listeners async |
| Aguarda conclusão | Chamada serial aguarda todos os listeners terminarem |
| Sem interrupção | Listeners não podem impedir execução subsequente |
(3) Cenários Típicos
- Fluxos de inicialização:
session/initialized(carregar config → estabelecer conexões → aquecer cache em ordem) - Fluxos de limpeza:
session/closing(salvar dados → desconectar → limpar arquivos temporários em ordem) - Pipelines de dados:
data/transform(transformar dados passo a passo)
(4) Diferença do emit
// emit: paralelo (não aguarda)
ctx.emit('session/created', data) // Não aguarda listeners completarem
// serial: sequencial (aguarda)
await ctx.serial('session/created', data) // Aguarda todos os listeners completarem
5. waterfall: Eventos de Passagem em Cadeia
(1) Uso Básico
▶ Exemplo 3: Pipeline waterfall de Processamento de Mensagem
waterfall passa o valor de retorno do listener anterior para o próximo, formando uma cadeia:
ctx.on('message/format', async (data, next) => {
data.text = data.text.trim()
return next(data)
})
ctx.on('message/format', async (data, next) => {
data.text = data.text.replace(/\s+/g, ' ')
return next(data)
})
ctx.on('message/format', async (data, next) => {
data.text = data.text.substring(0, 4096)
return next(data)
})
const result = await ctx.waterfall('message/format', { text: ' hello world ' })
// result.text === 'hello world' (trim → colapsar → truncar)
(2) Características
| Característica | Descrição |
|---|---|
| Passagem em cadeia | Saída anterior é entrada do próximo |
| Chamada next() | Listeners devem chamar next() para passar ao próximo |
| Modificável | Cada listener pode modificar dados |
| Interrompível | Não chamar next() interrompe a cadeia |
(3) Função next()
Cada listener waterfall recebe um parâmetro next:
ctx.on('event/name', async (data, next) => {
// Modificar dados
data.field = newValue
// Chamar next para passar ao próximo listener
return next(data)
// Não chamar next → cadeia interrompida, dados não passam adiante
})
(4) Cenários Típicos
- Processamento de mensagem:
message/format(formatar → filtrar → truncar) - Pipeline de requisição:
request/process(autenticar → autorizar → processar → logar) - Transformação de dados:
data/transform(parsear → validar → normalizar → sair)
(5) Interrompendo a Cadeia
ctx.on('request/process', async (data, next) => {
if (!data.authenticated) {
return { error: 'unauthenticated' } // Não chamar next, cadeia interrompida
}
return next(data)
})
6. Domínios de Evento
(1) Particionamento de Domínio
Eventos do Cordis são particionados por domínio, separados com /:
session/created → domínio session
session/destroyed → domínio session
tool/beforeExecute → domínio tool (subdomínio capability)
tool/afterExecute → domínio tool
agent/initialized → domínio agent
llm/request → domínio llm
(2) Domínios de Evento Centrais
| Domínio | Prefixo | Eventos Típicos |
|---|---|---|
| session | session/ |
created, destroyed, forked |
| agent | agent/ |
initialized, stopped, error |
| tool | tool/ |
beforeExecute, afterExecute, error |
| llm | llm/ |
request, response, stream, error |
| fiber | fiber/ |
created, active, disposing, disposed, errored |
| config | config/ |
updated, validated |
(3) Propósito do Domínio
Domínios de evento não são açúcar sintático — o framework otimiza com base em domínios:
- Filtragem de evento: Inscrever-se apenas em eventos de um domínio específico
- Isolamento de escopo: Eventos do domínio session propagam dentro de contextos de sessão
- Agrupamento de auditoria: Coletar logs de evento por domínio
(4) Inscrevendo-se em um Domínio Específico
// Inscrever-se em todos os eventos do domínio tool
ctx.on('tool/*', (eventName, data) => {
ctx.logger.info(`evento tool: ${eventName}`)
})
7. Eventos Customizados e Segurança de Tipo
(1) Declarando Eventos Customizados
// events.ts
interface MyPluginEvents {
'my-plugin/data-loaded': { source: string; count: number }
'my-plugin/data-error': { source: string; error: Error }
}
declare module '@deepseek-ai/cordis' {
interface Events extends MyPluginEvents {}
}
(2) Emissão de Evento com Tipagem Segura
ctx.emit('my-plugin/data-loaded', { source: 'api', count: 42 }) // ✅ Tipo correto
ctx.emit('my-plugin/data-loaded', { wrong: true }) // ❌ Erro de tipo
(3) Escuta com Tipagem Segura
ctx.on('my-plugin/data-loaded', (data) => {
// data automaticamente inferido como { source: string; count: number }
ctx.logger.info(`carregados ${data.count} itens de ${data.source}`)
})
(4) Padrão de Definição de Tipo de Evento
// Eventos internos do plugin
interface InternalEvents {
'cache/hit': { key: string; age: number }
'cache/miss': { key: string }
'cache/evicted': { key: string; reason: string }
}
// Estender interface Events global
declare module '@deepseek-ai/cordis' {
interface Events extends InternalEvents {}
}
// Exportar para outros plugins usarem
export type CacheEvents = InternalEvents
(5) Convenções de Nomenclatura de Eventos
{domínio}/{verbo-passado} ✅ session/created
{domínio}/{verbo-presente} ✅ tool/execute (em andamento)
{domínio}/before{Ação} ✅ tool/beforeExecute (pre-hook)
{domínio}/after{Ação} ✅ tool/afterExecute (post-hook)
{domínio}/{substantivo}-{estado} ✅ fiber/errored (estado)
❓ Perguntas Frequentes
ctx.off() para remover — requer a mesma referência de função.typescript ctx.logger.info('listeners:', ctx.listenerCount('tool/beforeExecute')) 📖 Resumo
- Quatro padrões de evento: emit (broadcast), bail (interrompível), serial (sequencial async), waterfall (passagem em cadeia)
- emit para notificações, bail para intercepção, serial para inicialização ordenada, waterfall para pipelines de dados
- Domínios de evento separados por
/: session/agent/tool/llm/fiber/config - Eventos customizados alcançam segurança de tipo através de
declare moduleestendendo a interface Events - A chamada next() do waterfall é crítica para propagação da cadeia; esquecer de chamar interrompe a cadeia
📝 Exercícios
1. ⭐ Básico: Escreva um plugin que faz broadcast de um evento my-plugin/loaded via emit, e escute-o em outro plugin com saída de log.
2. ⭐⭐ Intermediário: Implemente um interceptor de aprovação usando bail em tool/beforeExecute para interceptar comandos shell contendo rm. Teste: ls executa normalmente, rm -rf / é interceptado.
3. ⭐⭐⭐ Desafio: Use waterfall para implementar um pipeline de processamento de mensagem: trim → remover palavras sensíveis → truncar texto excessivo. Cada passo é um listener independente; passos intermediários podem modificar dados, e o último passo retorna o resultado final. Escreva testes para verificar o comportamento do pipeline.