Cordis in five ideas
Cordis is the vendored plugin framework underneath dsh. Its five ideas power everything:
- A plugin is an object that implements Service — a function with optional
injectandapply(ctx)fields, or aServicesubclass Cordis mounts into the current context. - A context is a repository of services — a service claims a stable
ctx.<key>such asctx.toolsorctx.llm. - Declare dependencies via
inject— a plugin names the services it needs and waits until they exist. - Typed Events for communication — declared through TypeScript declaration merging, dispatched as
emit,waterfall,parallel, orserial. - Registrations are reversible effects — installed through
ctx.effect()orctx.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 package | Role |
|---|---|
@deepseek-ai/cordis | Core: Context, Service, fibers, ctx.effect, typed events |
@deepseek-ai/cordis-plugin-loader | Runtime plugin loader: owns an EntryTree, imports modules, applies config, keeps the graph in sync |
@deepseek-ai/cordis-plugin-include | File-backed loader tree: reads a YAML/JSON file into entries, applies patches, writes back |
@deepseek-ai/cordis-plugin-group | Group rows that give one isolate realm to a provider and its consumers |
@deepseek-ai/cordis-plugin-hmr | Hot module replacement for loader-managed plugins |
@deepseek-ai/cordis-plugin-logger-console | Console logger |
@deepseek-ai/cordis-plugin-timer | Disposal-aware timers (ctx.timeout, ctx.interval) |
@deepseek-ai/schemastery | Type-driven schema validator (config validation) |
@deepseek-ai/cosmokit | Shared 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:
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:
| Mode | Awaited? | Dispatch order | Return value |
|---|---|---|---|
emit | No | listeners observe in registration order | No |
waterfall | No | listeners observe in registration order | Yes |
parallel | Yes | all listeners observe in parallel | No |
serial | Yes | listeners observe in registration order | Yes |
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:
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:
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(providesctx.dynamicCordisRunner) — the host half of dynamic packages: a definition registry, anode:vmsandbox for host halves under thecordis-dynamicgroup 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 emitscordis/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'sdshClientdeclaration); its host-sideapply()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 onctx.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:
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):
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.mdin 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.mdand each package'sREADME.mdunderpackages/extensions/.