Skip to content

Instantly share code, notes, and snippets.

@khandar-william
Created August 14, 2026 07:34
Show Gist options
  • Select an option

  • Save khandar-william/56070997a87093dd7ee6f30dd53619d6 to your computer and use it in GitHub Desktop.

Select an option

Save khandar-william/56070997a87093dd7ee6f30dd53619d6 to your computer and use it in GitHub Desktop.
DeepSeek Cordis

Cordis is not a plugin host with a privileged core. It is a small runtime whose only unit of work is a plugin. This repo vendors that core as @deepseek-ai/cordis from packages/core on that tree. YAML loading is a separate plugin (@cordisjs/plugin-loader). The kernel would still work if you never used a config file.

The implementation is five mechanisms that lock together: a callback, a fiber, a proxied context, a reactive provide/inject graph, and effects.

1. A plugin is a callback, not a class hierarchy

The registry accepts three syntactic shapes and collapses them to one function:

export type Plugin<T = any> =
  | Plugin.Function<T>
  | Plugin.Constructor<T>
  | Plugin.Object<T>
// ...
  export interface Function<T = any> extends Base<T> {
    (ctx: Context, config: T): any
  }
  export interface Constructor<T = any> extends Base<T> {
    new (ctx: Context, config: T): any
  }
  export interface Object<T = any> extends Base<T> {
    apply(ctx: Context, config: T): any
  }

plugin() does not “install a package.” It resolves that callback, records a runtime keyed by the function identity, and constructs a fiber — one live instance with its own config and cleanup list:

plugin(plugin: Plugin, config?: any, getOuterStack = buildOuterStack()) {
  const callback = this.resolve(plugin)
  if (!callback) throw new Error('invalid plugin, expect function or object with an "apply" method, received ' + typeof plugin)
  this.ctx.fiber.assertActive()
  // ...
  const fiber = new Fiber(this.ctx, config, Inject.resolve(plugin.inject), runtime, getOuterStack)

At activation time the fiber either news a class or calls the function. That is the entire difference between a Service subclass and export function apply:

execute: function () {
  if (isConstructor(runtime.callback)) {
    const instance = new runtime.callback(this.ctx, this.config)
    // ...
    return instance?.[symbols.init]?.()
  } else {
    return runtime.callback(this.ctx, this.config)
  }
},

Upstream’s own README shows the same two shapes side by side: a Counter service class, and a greeter function with inject: ['counter'], both started with root.plugin(...).

2. The fiber is the plugin’s lifetime

A fiber is not a thread. It is a state machine plus a stack of disposers:

PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED
                 ↘ FAILED
  • PENDING — declared, but a required service is missing.
  • LOADING / ACTIVE — the callback is running / has finished.
  • UNLOADING — collected disposers run in reverse.

ctx.plugin(child) from inside a plugin creates a child fiber. Disposing the parent disposes the children. That is how composition nests without a second tree type: a plugin that mounts other plugins is still just fibers.

3. ctx is a Proxy, not a bag of fields

new Context() installs four built-in services (reflect, registry, events, logger) and returns a Proxy. Ordinary reads such as ctx.counter do not look up an own property. They go through ReflectService.handler.get:

static handler: ProxyHandler<Context> = {
  get: (target, prop, ctx: Context) => {
    // own properties and symbols first...
    return ctx.events.waterfall('internal/get', ctx, prop, error, () => {
      const key = target[symbols.isolate][prop]
      let fiber = (ctx[symbols.shadow] as Context ?? ctx).fiber
      while (true) {
        const impl = fiber.store?.[prop]
        if (impl) return getTraceable(ctx, impl.value)
        if (prop in fiber.inject) {
          throw error  // required service, fiber not active
        }
        // walk parent fibers while isolation label matches
        fiber = fiber.parent.fiber
      }
    })
  },

That is why plugins never import a concrete Counter. They declare inject: ['counter'] and then read ctx.counter. The name is the coupling. TypeScript declaration merging (interface Context { counter: Counter }) is only the type side of the same name.

ctx.plugin, ctx.on, ctx.effect, ctx.provide look like methods on Context. They are mixins: accessors that forward to registry / events / fiber / reflect, so those APIs dispose with the core services.

this.mixin('reflect', ['get', 'set', 'provide', 'accessor', 'mixin'])
this.mixin('fiber', ['runtime', 'effect'])
this.mixin('registry', ['inject', 'plugin'])
this.mixin('events', ['on', 'once', 'parallel', 'emit', 'serial', 'bail', 'waterfall'])

4. Provide/inject is a reactive graph, not boot order

When a Service subclass runs super(ctx, 'counter'), it immediately provides itself. Provide is an effect: the implementation is stored under an isolation symbol, and dependents are notified.

provide(name: string, value?: any, check?: () => boolean) {
  return this.ctx.fiber.effect(() => {
    this.ctx.root[symbols.isolate][name] ??= Symbol(name)
    const key = this.ctx[symbols.isolate][name]
    const impl: Impl = { name, value, fiber: this.ctx.fiber, check }
    if (this.store[key]) {
      throw new Error(`service "${name}" has been registered at <${this.store[key].fiber.name}>`)
    }
    this.store[key] = impl
    // ...
    return async () => {
      delete this.store[key]
      const fibers = this.notify([name])
      await Promise.allSettled(fibers.map(fiber => fiber.await()))
    }
  }, `ctx.provide(${JSON.stringify(name)})`)
}

notify walks every fiber that listed that name in inject, updates its local impl snapshot, and calls _refresh:

notify(names: string[], ...) {
  for (const runtime of this.ctx.registry.values()) {
    for (const fiber of runtime.fibers) {
      // ...
      fiber._checkImpl(name)
      fiber._refresh()
    }
  }
}

_refresh concatenates the providing fibers’ uids into an epoch. Missing any injectee sets the epoch inactive (unload). When every name is present, the fiber loads:

_refresh() {
  let epoch: string | boolean = false
  epoch = ''
  for (const name of Object.keys(this.inject)) {
    const impl = this._store[name]
    if (!impl) {
      epoch = INACTIVE
      break
    }
    epoch += ':' + impl.fiber.uid
  }
  this._setEpoch(epoch)
}

That is the whole “plugin system” scheduling story:

  • You can plugin() dependents before providers. They sit in PENDING.
  • When the provider becomes ACTIVE, dependents activate themselves.
  • If the provider unloads (HMR, config disable, dispose), dependents unload, then reload if a replacement appears.
  • Two plugins cannot provide the same name in the same isolation realm; the store key is a Symbol, and a duplicate throws.

Load order is a graph of service availability, not a sorted list of files.

flowchart LR
  A["ctx.plugin(Greeter)<br/>inject: counter"] --> P["fiber PENDING"]
  B["ctx.plugin(Counter)<br/>provide('counter')"] --> N["notify('counter')"]
  N --> R["_refresh epoch"]
  R --> L["Greeter LOADING → apply()"]
  L --> E["ctx.on('app/ready') is an effect"]
Loading

5. Everything a plugin “does” is an effect

ctx.effect(fn) runs fn immediately and collects every disposer it returns, yields, or awaits. Those disposers run in reverse on unload. ctx.on is the same primitive: it pushes a listener, and the disposer splices it out.

register(label: string, hooks: Hook[], callback: any, options: EventOptions): () => void {
  const method = options.prepend ? 'unshift' : 'push'
  return this.ctx.fiber.effect(() => {
    hooks[method]({ ctx: this.ctx, callback, ...options })
    return () => this.unregister(hooks, callback)
  }, label)
}

So a plugin does not mutate a global singleton and hope teardown remembers. Its footprint is the fiber’s disposer list: provided services, listeners, child plugins, timers you wrapped yourself. Dispose the fiber and the plugin is gone — which is also how HMR works: dispose the old fiber, start a new one with the new module.

That is the “temporal” half of Cordis’s spatiotemporal composability claim: plugins appear and disappear over time, and the runtime’s job is to keep the graph consistent.

6. Plugins talk through events, including around-middleware

Declaration merging types the bus (interface Events { 'app/ready'(message: string): void }). Dispatch modes are different folding strategies over the same hook list:

Mode Behavior
emit fire-and-forget, registration order
parallel await all
serial / bail stop on first truthy result
waterfall each listener receives next; skip next() to veto

Waterfall is Koa-style wrapping, implemented by treating the last argument as the inner callback and shifting listeners:

waterfall(...args: any[]) {
  const cbs = this.dispatch('waterfall', args)
  const inner = args.pop()
  const next = () => {
    const cb = cbs.shift() ?? inner
    return cb(...args)
  }
  args.push(next)
  return next()
}

Listeners are also filtered by context. A service can set Context.filter so an event dispatched as that service only reaches listeners in the same isolation scope. That is how one ctx.tools registry can have per-agent listeners without a second event bus.

7. Space: isolate and intercept

ctx.isolate('counter') returns a child context whose isolation map has a new Symbol for counter. Provides and lookups under that child use a different store slot, so a second Counter can exist without colliding with the parent’s. Sharing a label joins two subtrees into one realm.

ctx.intercept('loader', { await: true }) stacks config on a prototype chain; Service[resolveConfig] merges ancestor intercepts. That is per-plugin config overlay, not a second DI container.

Together with child fibers, this is the “spatial” half: the same plugin code can run in many places at once, each with its own services and cleanup.

8. The Loader is optional — YAML is a plugin too

Core Cordis has no cordis.yml. @cordisjs/plugin-loader is itself a Service that owns an entry tree. For each row it import()s name, unwraps default vs named exports, and calls registry.plugin(plugin, config) — the same API you would call from TypeScript.

That is why DeepSeek Harness can say the agent loop is a plugin: boot() is new Context(); ctx.plugin(Loader); mount include. After that, every product row is one more fiber.


The trick, compressed: Cordis implements plugins by making lifetime first-class (fiber + effects), making names the only coupling (provide / inject / Proxy), and making activation a reactive function of those names (notify → _refresh). There is no plugin manager sitting above that graph. The graph is the application.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment