Files
deepseek-harness/docs/user/develop/framework/index.md
T
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.6 KiB

Plugins and lifecycle

English | 中文

This page describes the Cordis plugin model and lifecycle state machine.

Fiber state machine

Every loaded plugin owns a Fiber scope with the following states:

PENDING → LOADING → ACTIVE
                 ↘ FAILED
ACTIVE → UNLOADING → DISPOSED
State Meaning
PENDING Declared, but required dependencies are not ready
LOADING Dependencies are ready and apply is running
ACTIVE The plugin is running
FAILED apply threw an error
UNLOADING The plugin is unloading and disposing resources
DISPOSED The plugin is fully unloaded

Dependency-driven loading

A plugin with inject waits for every required service before loading:

export const inject = ['tools', 'llm']

export function apply(ctx: Context) {
  // ctx.tools and ctx.llm are ready here.
}

If a required service disappears, for example during provider replacement, the plugin unloads automatically (ACTIVE → DISPOSED) and loads again when the service returns.

Automatic cleanup

Every registration made through ctx is undone when the plugin unloads:

export function apply(ctx: Context) {
  // Event listener: removed automatically on unload.
  ctx.on('some-event', handler)

  // Custom resource: the returned disposer runs on unload.
  ctx.effect(() => {
    const connection = createConnection()
    return () => connection.close()
  })
}

The framework tracks and disposes all of these operations:

  • ctx.on(event, handler) — event listener
  • ctx.tools.register(tool) — tool registration
  • ctx.llm.registerAdapter(names, adapter) — LLM adapter registration
  • ctx.effect(() => cleanup) — custom resource

During unload, disposer invocation starts in reverse registration order, but multiple async disposers run concurrently and have no serial completion guarantee. Put order-dependent cleanup in one disposer returned from a single ctx.effect() and await its steps serially there.

Nested contexts

ctx.plugin() creates a child Fiber that inherits the parent context but has an independent lifecycle:

export function apply(ctx: Context) {
  // Register a child plugin.
  ctx.plugin(childPlugin)

  // The child has its own Fiber and unloads with its parent.
}

Dispose semantics

To stop a plugin instance early:

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

declare const ctx: Context
declare function myPlugin(ctx: Context): void

const fiber = ctx.plugin(myPlugin)

// Dispose it manually later.
await fiber.dispose()

dispose guarantees:

  1. All registrations owned by the plugin are removed.
  2. Child plugins are recursively unloaded.
  3. The returned promise resolves after all asynchronous cleanup finishes.

Hot replacement (HMR)

With @deepseek-ai/cordis-plugin-hmr loaded from cordis.yml, editing a plugin source file triggers:

  1. Unload the old plugin and clean up its registrations.
  2. Load the new code.
  3. Run the new apply.

Because plugin registrations clean themselves up, hot replacement does not retain registrations from the old instance.

Example lifecycle

export function apply(ctx: Context) {
  console.log('plugin loading')

  ctx.effect(() => {
    console.log('effect registered')
    return () => console.log('effect cleaned up')
  })
}

Loading prints:

plugin loading
effect registered

Unloading prints:

effect cleaned up

Next steps