Files
deepseek-harness/docs/user/develop/framework/events.md
imccyu ec601ca13d build(vendor): rescope the vendored Cordis packages into @deepseek-ai
Machine-produced by `pnpm run rescope-vendor --apply` plus the regeneration it
prints: `pnpm install` for the lockfile, `pnpm run gen-third-party-notices`,
`verify-translation-pairing --write` for the touched bilingual pairs,
`gen-doc-graphs`, and one typert snapshot whose ids embed character offsets.
`pnpm run rescope-vendor --check` verifies the result.

Renames nine vendored packages (cordis, cosmokit, schemastery and the six
@cordisjs plugins) and every reference that resolves them: manifest names and
dependency keys, module specifiers including declare-module merges, cordis.yml
plugin names, tsconfig paths, every Markdown fence, and `docs/` prose.
Directory names, upstream versions, and dependency ranges are unchanged, so
vendor/README.md still reads as an upstream snapshot; its manifest table gains
an upstream-name column so THIRD_PARTY_NOTICES keeps MIT attribution pointed
at each fork's origin.

The tutorial tier follows the rename end to end: its yaml fences named plugins
the Loader can no longer resolve, its `ts ignore-check` fences disagreed with
the compiled fences beside them, and its prose quoted both. The contracts that
told readers to keep upstream names — the root convention and the vendoring
cookbook's tree comment and manifest invariant — now say to rescope instead.

Two rules read `@deepseek-ai/` as "another workspace plugin": the client bundle
purity gate now names the vendored libraries a browser bundle inlines, and the
files where a bare `cordis` is an agent-preset id keep that product data.
2026-08-10 22:04:13 +08:00

3.9 KiB

Event system

English | 中文

Events are the core communication mechanism between Cordis plugins. Harness uses them extensively for loosely coupled extension points.

Basic use

Listen for an event

ctx.on('event-name', (payload) => {
  // Handle the event.
})

Emit an event

ctx.emit('event-name', payload)

Event modes

Cordis provides several event modes for different interaction contracts.

emit — broadcast

Every listener runs synchronously and return values are ignored:

// Emit
ctx.emit('my-plugin/ready', { id: 'worker-1' })

// Listen
ctx.on('my-plugin/ready', ({ id }) => {
  console.log(`${id} is ready`)
})

bail — short circuit

Listeners run in order; the first non-undefined result becomes the final result:

// Dispatch
const result = ctx.bail('some-check', input)

// Listen: a returned value stops later listeners.
ctx.on('some-check', (input) => {
  if (shouldBlock(input)) return 'blocked'
  // Return undefined to continue to the next listener.
})

serial — ordered execution

Listeners run in registration order and asynchronous results are awaited. The first listener to return a non-empty value stops further execution:

await ctx.serial('setup-phase', context)

waterfall — pipeline

Each listener may wrap the downstream result to form a processing chain. A listener must call next() to delegate downstream; omitting the call short-circuits the pipeline:

// Dispatch
const output = await ctx.waterfall('my-plugin/transform', input, async () => input)

// Listen: next() is mandatory.
ctx.on('my-plugin/transform', async (_input, next) => {
  const downstream = await next()
  return downstream.trim()
})

::: warning A waterfall listener must call next(). Omitting it short-circuits the pipeline by design, enabling interception and gateway behavior. :::

Typed events

Harness uses TypeScript declaration merging for type-safe events:

import '@deepseek-ai/cordis'

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

// ctx.on('my-plugin/ready', ...) and ctx.emit('my-plugin/ready', ...)
// are now inferred correctly.

Cordis events and session records

Harness Cordis events use namespace/action names, including agent/step, agent/request, agent/request-error, tools/result, and session/event. The generated cordis-surface regions on the subsystem pages record complete signatures and modes.

turn/*, step/*, tool/call, tool/result, and compact/* are durable session-event types, not same-named Cordis events. To observe them, listen to session/event and inspect event.type.

Event listeners are effects

A listener registered with ctx.on() is removed automatically when its plugin unloads:

export function apply(ctx: Context) {
  // This listener is removed when the plugin disposes.
  ctx.on('tools/result', handler)
}

Example: logging plugin

This plugin logs tool calls and results:

import type { Context } from '@deepseek-ai/cordis'
import '@deepseek-ai/dsh-tools'

export const name = 'tool-logger'

export function apply(ctx: Context) {
  ctx.on('tools/result', (exec, result) => {
    console.log(`[tool] ${exec.name}(${JSON.stringify(exec.arguments)})`)
    const text = result.content
      .map(block => block.type === 'text' ? block.text : '')
      .join('')
    console.log(`[tool result] ${text.slice(0, 100)}`)
  })
}

Next steps