Cordis Primer
The core concepts of the Cordis plugin framework underneath DeepSeek Harness: plugins, ctx, services, events and reversible effects
Cordis is the vendored plugin framework underneath DeepSeek Harness. Before writing a harness plugin, learn these core concepts; the full service/event reference lives in each capability subsystem's generated documentation.
Five core ideas
- A plugin is an object that implements Service. It can be a function with optional
injectandapply(ctx)fields, or aServicesubclass whose lifecycle Cordis mounts into the current context. - A context is a repository of services. A service claims a stable
ctx.<key>such asctx.tools,ctx.llm, orctx.sessions; other plugins find services by key instead of importing a concrete implementation. - Declare service dependencies with
inject. A plugin that names required services waits until those services exist, so load order is expressed through service requirements rather than manual boot sequencing. - Typed events for communication. Services declare event names through TypeScript declaration merging, then dispatch them as
emit,waterfall,parallel, orserialdepending on whether listeners observe, wrap, fan out, or run in order. - Registrations are reversible effects. Prompt sections, tool schemas, adapters, providers, and listeners are installed through
ctx.effect()orctx.on()so reload and teardown unwind them predictably.
Dispatch modes
Every event has one of the following dispatch modes and can only be dispatched by the corresponding method.
| Mode | Awaited? | Dispatch order | Has return value? |
|---|---|---|---|
emit | No | listeners observe in registration order | No |
waterfall | No | listeners observe in registration order | Yes |
parallel | Yes | all listeners observe the event in parallel | No |
serial | Yes | listeners observe in registration order | Yes |
The dispatch mode is part of an event's public contract.
Waterfall semantics
ctx.waterfall is around-middleware. A listener receives (...args, next); calling next() brings the downstream result back to the current layer, which may wrap it before returning. Returning without next() short-circuits the chain.
Cooperative listeners usually mutate a shared request or decision object, then delegate. A listener that only annotates or observes must delegate; a policy listener that owns the decision may return directly.
A minimal plugin
import type { Context } from '@deepseek-ai/cordis'
export const name = 'my-plugin'
export const inject = ['tools']
export function apply(ctx: Context) {
ctx.effect(() => {
const timer = setInterval(heartbeat, 5000)
return () => clearInterval(timer) // disposer
})
}inject makes the plugin wait for tools to be ready; the function returned from ctx.effect() runs when the plugin unloads.
Practical rules
- Encapsulate behavior into plugins: the tool pipeline belongs to
ctx.tools, model streaming belongs toctx.llm, and live agent loop coordination belongs toctx.agents. - Prefer events for interception and policy; prefer service methods for direct capability calls.
- Every registration should have a disposer, either returned from
ctx.effect()or provided by a Cordis helper. If teardown order matters, keep the related work in one effect.
Next steps
- Use this model to read the Architecture overview.
- When you want hands-on examples, see the Cookbook.