Skip to content

Cordis in five ideas

Cordis is the vendored plugin framework underneath dsh. Its five ideas power everything:

  1. A plugin is an object that implements Service — a function with optional inject and apply(ctx) fields, or a Service subclass Cordis mounts into the current context.
  2. A context is a repository of services — a service claims a stable ctx.<key> such as ctx.tools or ctx.llm.
  3. Declare dependencies via inject — a plugin names the services it needs and waits until they exist.
  4. Typed Events for communication — declared through TypeScript declaration merging, dispatched as emit, waterfall, parallel, or serial.
  5. Registrations are reversible effects — installed through ctx.effect() or ctx.on() so reload and teardown unwind them.

Every part of the product is a plugin, including the model adapter, the tool registry, the session log, and the agent loop itself — so every part is replaceable from configuration.

Vendored packages

Vendored packageRole
@deepseek-ai/cordisCore: Context, Service, fibers, ctx.effect, typed events
@deepseek-ai/cordis-plugin-loaderRuntime plugin loader: owns an EntryTree, imports modules, applies config, keeps the graph in sync
@deepseek-ai/cordis-plugin-includeFile-backed loader tree: reads a YAML/JSON file into entries, applies patches, writes back
@deepseek-ai/cordis-plugin-groupGroup rows that give one isolate realm to a provider and its consumers
@deepseek-ai/cordis-plugin-hmrHot module replacement for loader-managed plugins
@deepseek-ai/cordis-plugin-logger-consoleConsole logger
@deepseek-ai/cordis-plugin-timerDisposal-aware timers (ctx.timeout, ctx.interval)
@deepseek-ai/schemasteryType-driven schema validator (config validation)
@deepseek-ai/cosmokitShared utilities

Services and ctx

A service is a value installed at a ctx.<key>. Consumers inject it rather than importing the concrete class. For example packages/core/agent-loop/src/index.ts declares:

ts
export class AgentLoop extends Service implements AgentFactory {
  static inject = ['agents', 'sessions', 'llm', 'tools', 'systemPrompt']
}

Because dsh installs the Service ontology into @deepseek-ai/cordis via declaration merging, the whole product shares one context. The packages/core/* table (see overview) lists the spine keys — ctx.sessions, ctx.systemPrompt, ctx.tools, ctx.agents, ctx.agentLoop — and the package-owning seams across the groups cover ctx.llm, ctx.fs, ctx.shell, ctx.terminals, ctx.subprocess, ctx.sandbox, ctx.commands, ctx.jobs, and dozens more.

Typed events and dispatch modes

Every event has one dispatch mode, dispatched by the matching method:

ModeAwaited?Dispatch orderReturn value
emitNolisteners observe in registration orderNo
waterfallNolisteners observe in registration orderYes
parallelYesall listeners observe in parallelNo
serialYeslisteners observe in registration orderYes

Events are declared through TypeScript module augmentation, with an @mode tag so the generated catalog can check declarations against dispatch sites. Example from packages/core/agent/src/runtime-types.ts:

ts
declare module '@deepseek-ai/cordis' {
  interface Events {
    /** … @mode waterfall */
    'agent/pre-step'(this: Scoped<Agent>, payload: {
      agent: Agent; messages: UserMessage[]; turn: number; step: number; signal: AbortSignal;
    }, next: () => Promise<PreStepDecision>): Promise<PreStepDecision>
  }
}

Waterfall is around-middleware. A listener receives (...args, next); calling next() delegates the wrapped result; returning without next() short-circuits. New harness events document the mode with @mode.

Reversible effects and plugin lifecycle

Every registration should have a disposer — either returned from ctx.effect() or handled by a Cordis helper. Cordis tracks each plugin on a fiber; when the plugin mounts, its apply(ctx) runs, and when it unloads or reloads, every effect installed through that context unwinds in reverse order. This is why a profile reload (HMR) and a full teardown behave predictably: prompt sections, tool schemas, listeners, and providers all go away with their owning plugin.

The loader applies config to a row and settles the plugin graph via ctx.loader.await(); dsh's assertEntriesActivated in packages/boot/app-boot/src/index.ts rejects a settled tree where an enabled entry has no fiber or is still pending.

Config rows and schemastery validation

A loader config is a list of rows; each row names a plugin and carries config. The include plugin parses the YAML/JSON dialect and !!js scalars become expression nodes the Loader interpolates against the entry's injection-ready context. A patch targets a row by id and replaces its whole config, or inserts new rows.

Each configurable plugin validates its row through schemastery. For example AgentLoop.Config:

ts
static Config = z.object({
  maxParallelToolCalls: z.number().step(1).min(1).default(DEFAULT_MAX_PARALLEL_TOOL_CALLS),
  agents: z.array(z.object({
    id: z.string().required(), sessionId: z.string().min(1), provider: z.string(),
    model: z.string(), maxTokens: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER),
    cwd: z.string(), resumeSessionId: z.string(),
  })).default([]),
}) as z<Config>

z is schemastery — usable as a validator or constructor, extensible via Schema.extend(), and serializable/hydratable across environments.

How dsh wraps Cordis: host runner vs client runner

The extension system has a "model-faced" half and a browser-visible half:

  • @deepseek-ai/dsh-cordis-host-runner (provides ctx.dynamicCordisRunner) — the host half of dynamic packages: a definition registry, a node:vm sandbox for host halves under the cordis-dynamic group fiber, an invoke-handler table, and the request-run round trip. A host-only package runs in this process; a package with a browser half emits cordis/request-run, suspends, and waits for a person to allow or decline it.
  • @deepseek-ai/dsh-cordis-client-runner — the browser half loaded into the page (through the package's dshClient declaration); its host-side apply() is empty, so the row appears in the host config while the browser half carries the logic.
  • @deepseek-ai/dsh-tool-cordis — registers the model-facing tools on ctx.tools: cordis_inspect_list, cordis_inspect_query, cordis_inspect_self, cordis_define, cordis_run, cordis_stop, cordis_undefine, plus a self-referential toolset that reads and edits the harness checkout.
  • @deepseek-ai/dsh-ui-cordis — browser surfaces: the frame-wide panel operating every definition and the read-only define card.

A minimal plugin shape from the repo

The ordinary (non-dynamic) plugin shape is exactly Cordis's. The web-app command-line provider packages/bundle/web-app/src/startup.ts is representative:

ts
export const name = 'web-startup'
export const inject = ['cmdlineArgs']
export const WEB_STARTUP_SERVICE = 'webStartup'

export function apply(ctx: Context): void {
  const program = new Command() // commander program for --host/--port/--trusted-host
  ctx.provide(WEB_STARTUP_SERVICE, { … })
}

And tool-cordis registers tools through the product API (packages/extensions/tool-cordis/src/index.ts):

ts
export const name = 'tool-cordis'
export const inject = ['tools', 'systemPrompt', 'dynamicCordisRunner', 'cordisInspect']

export function apply(ctx: Context): void {
  ctx.systemPrompt.section({ name: 'tool:cordis', order: 115, text: CORDIS_SYSTEM_PROMPT })
  ctx.tools.register(defineTool({
    name: 'cordis_inspect_list',
    description: 'List every Cordis Inspect Provider …',
    parameters: {},
    execute(_args, _exec) {
      return Promise.resolve({ providers: ctx.cordisInspect.list() })
    },
  }))
}

Everything registered through apply(ctx) is scope/effect-bound: when the row unloads, the prompt section and tool registrations unwind.

The dsh-plugin ecosystem

"Out-of-tree plugin" means a package installed into a profile via dsh plugin --profile <name> add <pkg> (a thin pnpm forwarder in apps/cli/src/plugin.ts). It becomes a profile layer only if its package.json declares dsh.bundle.patch. A plugin that wants to be a Cordis plugin row is any package whose default export (apply/Service) Cordis can mount; to be a reusable bundle, it also ships a cordis.patch.yml and the dsh.bundle manifest field. In-box plugins are reached through the healed flat profiles/node_modules fallback, so out-of-tree peers resolve against the installation's single Cordis instance.

Further reading

  • Cordis Primer — a deeper tour of the same five ideas (see docs/cordis-primer.md in the repo).
  • Runtime & agent lifecycle — how the events and effects drive a real agent.
  • Boot process & CLI — how rows and patches become a running tree.
  • Repo docs: docs/cordis-primer.md, docs/cordis-tutorial/index.md, docs/subsystems/extensions.md.
  • Vendored source & READMEs: vendor/README.md, vendor/cordis/, vendor/loader/, vendor/include/, vendor/schemastery/.
  • Extension packages: packages/extensions/README.md and each package's README.md under packages/extensions/.