DeepSeek Harness: Event System

Last updated: 2026-08-31

Events are Cordis's core mechanism for loosely-coupled inter-plugin communication — plugins don't call each other directly, but collaborate through event broadcasting and subscription. Four event patterns cover all scenarios from simple notifications to complex pipelines.

💡 Tip: The key question for choosing an event pattern is "do you need interception?" — pure notification uses emit, interruptible uses bail, sequential processing uses serial, chain-passing uses waterfall.

📋 Prerequisites: Completed 14-inject.md, understand dependency injection

1. What You'll Learn


Event Dispatch Modes

2. emit: General Event Broadcasting

(1) Basic Usage

emit is the simplest event pattern — broadcast a notification; all listeners receive it, return values are ignored:

TYPESCRIPT
// Emit event
ctx.emit('session/created', { id: 'abc-123', user: 'Alice' })

// Listen to event
ctx.on('session/created', (data) => {
  ctx.logger.info(`new session: ${data.id}`)
})

(2) Characteristics

Characteristic Description
Broadcast All listeners are called
No return value Listener return values are ignored
No interruption Listeners cannot prevent subsequent listeners from executing
Sequential Listeners execute in registration order

(3) Typical Scenarios

(4) ▶ Example 4

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' })
// All three listeners execute

3. bail: Interruptible Events

(1) Basic Usage

bail is an interruptible event — when a listener returns a non-undefined value, subsequent listeners don't execute:

TYPESCRIPT
// Listener can "intercept" the event
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 /' } })
// → Returns { denied: true, reason: 'dangerous command' }
// Subsequent listeners don't execute

(2) Characteristics

Characteristic Description
Interruptible Returning non-undefined interrupts propagation
Short-circuit First listener to return a value terminates propagation
Has return value bail call returns the intercepted value
Order-sensitive Earlier-registered listeners intercept first

(3) Typical Scenarios

(4) ▶ Example 4

TYPESCRIPT
// Approval policy plugin
ctx.on('tool/beforeExecute', (data) => {
  const policy = getApprovalPolicy(data.tool)
  if (policy === 'deny') {
    return { denied: true, reason: `${data.tool} is denied by policy` }
  }
  if (policy === 'ask') {
    return { pending: true, requiresApproval: true }
  }
  // Return undefined → don't intercept, continue propagation
})

// Sandbox plugin (after approval)
ctx.on('tool/beforeExecute', (data) => {
  if (!isInSandbox(data.params.cwd)) {
    return { denied: true, reason: 'execution outside sandbox' }
  }
})

(5) ▶ Example 5

TYPESCRIPT
const result = ctx.bail('tool/beforeExecute', data)
if (result) {
  // Intercepted
  ctx.logger.warn('tool execution denied:', result.reason)
} else {
  // Not intercepted, can execute
  await executeTool(data)
}

4. serial: Sequential Events

(1) Basic Usage

serial executes async listeners in order, each waiting for the previous one to complete:

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)
})

// Three listeners execute sequentially
await ctx.serial('session/initialized', { userId: 'alice', workspaceId: 'ws-1' })

(2) Characteristics

Characteristic Description
Sequential Listeners execute one by one in order
Async Supports async listeners
Waits for completion serial call waits for all listeners to finish
No interruption Listeners cannot prevent subsequent execution

(3) Typical Scenarios

(4) Difference from emit

TYPESCRIPT
// emit: parallel (doesn't wait)
ctx.emit('session/created', data)   // Doesn't wait for listeners to complete

// serial: sequential (waits)
await ctx.serial('session/created', data)  // Waits for all listeners to complete

5. waterfall: Chain-Passing Events

(1) Basic Usage

waterfall passes the previous listener's return value to the next, forming a chain:

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 → collapse → truncate)

(2) Characteristics

Characteristic Description
Chain-passing Previous output is next input
next() call Listeners must call next() to pass to the next
Modifiable Each listener can modify data
Interruptible Not calling next() interrupts the chain

(3) next() Function

Each waterfall listener receives a next parameter:

TYPESCRIPT
ctx.on('event/name', async (data, next) => {
  // Modify data
  data.field = newValue
  
  // Call next to pass to next listener
  return next(data)
  
  // Not calling next → chain interrupted, data not passed further
})

(4) Typical Scenarios

(5) Interrupting the Chain

TYPESCRIPT
ctx.on('request/process', async (data, next) => {
  if (!data.authenticated) {
    return { error: 'unauthenticated' }  // Don't call next, chain interrupted
  }
  return next(data)
})

6. Event Domains

(1) Domain Partitioning

Cordis events are partitioned by domain, separated with /:

TEXT 📖 Display only
session/created        → session domain
session/destroyed      → session domain
tool/beforeExecute     → tool domain (capability subdomain)
tool/afterExecute      → tool domain
agent/initialized      → agent domain
llm/request            → llm domain

(2) Core Event Domains

Domain Prefix Typical Events
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) Domain Purpose

Event domains aren't syntactic sugar — the framework optimizes based on domains:

(4) Subscribing to a Specific Domain

TYPESCRIPT
// Subscribe to all events in the tool domain
ctx.on('tool/*', (eventName, data) => {
  ctx.logger.info(`tool event: ${eventName}`)
})

7. Custom Events and Type Safety

(1) Declaring Custom Events

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) Type-Safe Event Emission

TYPESCRIPT
ctx.emit('my-plugin/data-loaded', { source: 'api', count: 42 })  // ✅ Type correct
ctx.emit('my-plugin/data-loaded', { wrong: true })                // ❌ Type error

(3) Type-Safe Listening

TYPESCRIPT
ctx.on('my-plugin/data-loaded', (data) => {
  // data automatically inferred as { source: string; count: number }
  ctx.logger.info(`loaded ${data.count} items from ${data.source}`)
})

(4) Event Type Definition Pattern

TYPESCRIPT
// Plugin internal events
interface InternalEvents {
  'cache/hit': { key: string; age: number }
  'cache/miss': { key: string }
  'cache/evicted': { key: string; reason: string }
}

// Extend global Events interface
declare module '@deepseek-ai/cordis' {
  interface Events extends InternalEvents {}
}

// Export for other plugins to use
export type CacheEvents = InternalEvents

(5) Event Naming Conventions

TEXT 📖 Display only
{domain}/{verb-past-tense}    ✅ session/created
{domain}/{verb-present}       ✅ tool/execute (in progress)
{domain}/before{Action}       ✅ tool/beforeExecute (pre-hook)
{domain}/after{Action}        ✅ tool/afterExecute (post-hook)
{domain}/{noun}-{state}       ✅ fiber/errored (state)

❓ FAQ

Q Can emit and bail share the same event name?
A Not recommended. While technically possible, mixing emit and bail listeners under the same name causes confusion. Use different event names.
Q What happens if I forget to call next() in a waterfall?
A The chain is interrupted; subsequent listeners don't execute. The waterfall call returns current data. This could be intentional (conditional interruption) or a bug (forgot to call).
Q Can event listeners be registered multiple times?
A Yes. The same listener function registered multiple times will be called multiple times. Use ctx.off() to remove — it requires the same function reference.
Q Can the execution order of event listeners be controlled?
A Default is registration order. Some frameworks support priority parameters, but Cordis currently uses FIFO order.
Q Are async listeners awaited in emit?
A No. emit doesn't wait for async listeners to complete. Use serial if you need to wait.
Q How to view all registered event listeners?
A typescript ctx.logger.info('listeners:', ctx.listenerCount('tool/beforeExecute'))

📖 Summary


📝 Exercises

1. ⭐ Basic: Write a plugin that broadcasts a my-plugin/loaded event via emit, and listen to it in another plugin with log output.

2. ⭐⭐ Intermediate: Implement an approval interceptor using bail on tool/beforeExecute to intercept shell commands containing rm. Test: ls executes normally, rm -rf / is intercepted.

3. ⭐⭐⭐ Challenge: Use waterfall to implement a message processing pipeline: trim → remove sensitive words → truncate oversized text. Each step is an independent listener; intermediate steps can modify data, and the last step returns the final result. Write tests to verify pipeline behavior.

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏