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.

💡 Dica: A pergunta-chave para escolher um padrão de evento é "você precisa de intercepção?" — notificação pura usa emit, interrompível usa bail, processamento sequencial usa serial, passagem em cadeia usa waterfall.

📋 Pré-requisitos: Ter completado 14-inject.md, entender injeção de dependência

1. O Que Você Vai Aprender

Modos de Despacho de Eventos


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:

TYPESCRIPT
// 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

(4) Múltiplos Listeners

TYPESCRIPT
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:

TYPESCRIPT
// 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

(4) Exemplo de Política de Aprovação

TYPESCRIPT
// 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

TYPESCRIPT
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:

TYPESCRIPT
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

(4) Diferença do emit

TYPESCRIPT
// 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:

TYPESCRIPT
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:

TYPESCRIPT
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

(5) Interrompendo a Cadeia

TYPESCRIPT
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 /:

TEXT 📖 Somente leitura
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:

(4) Inscrevendo-se em um Domínio Específico

TYPESCRIPT
// 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

TYPESCRIPT
// 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

TYPESCRIPT
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

TYPESCRIPT
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

TYPESCRIPT
// 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

TEXT 📖 Somente leitura
{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

P emit e bail podem compartilhar o mesmo nome de evento?
R Não recomendado. Embora tecnicamente possível, misturar listeners emit e bail sob o mesmo nome causa confusão. Use nomes de evento diferentes.
P O que acontece se eu esquecer de chamar next() em um waterfall?
R A cadeia é interrompida; listeners subsequentes não executam. A chamada waterfall retorna os dados atuais. Isso pode ser intencional (interrupção condicional) ou um bug (esqueceu de chamar).
P Listeners de evento podem ser registrados múltiplas vezes?
R Sim. A mesma função de listener registrada múltiplas vezes será chamada múltiplas vezes. Use ctx.off() para remover — requer a mesma referência de função.
P A ordem de execução dos listeners pode ser controlada?
R Padrão é ordem de registro. Alguns frameworks suportam parâmetros de prioridade, mas Cordis atualmente usa ordem FIFO.
P Listeners async são aguardados em emit?
R Não. emit não aguarda listeners async completarem. Use serial se precisar aguardar.
P Como ver todos os listeners de evento registrados?
R typescript ctx.logger.info('listeners:', ctx.listenerCount('tool/beforeExecute'))

📖 Resumo


📝 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.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%