DeepSeek Harness Dependency-Driven Loading, Auto-Reload, and Nested Contexts

Previously we already learned about the Fiber state machine. This article looks at how the state machine is driven by service dependencies.

Why does a plugin automatically unload when a dependent service disappears? And why can it automatically reload after the service recovers?


Dependency-driven loading

A plugin that declares inject will wait for all required services to be ready before entering LOADING and executing apply.

This mechanism expresses the loading order through service dependencies, eliminating the need to manually orchestrate the startup order.

Example

// File path: scratch-plugin/src/my-plugin.ts
// Declare that this plugin depends on two services: tools and llm
export const inject = ['tools', 'llm']

export function apply(ctx: Context) {
  // At this point ctx.tools and ctx.llm are ready and safe to use.
}

If the dependent service disappears (for example, when the provider is replaced), the plugin will be automatically unloaded, going directly from ACTIVE to DISPOSED.

When the service recovers, the plugin will be reloaded, going through PENDING → LOADING → ACTIVE again.

卸载与重载时序图

The exact wording from the official documentation: if the dependent service disappears (for example, when the provider is replaced), the plugin will be automatically unloaded (ACTIVE → DISPOSED), and reloaded after the service recovers.


Guarantee of automatic reload

Automatic reload is safe because all registrations made through ctx are revoked upon unload.

The old instance's listeners, tools, and adapters will not linger; the new instance re-registers them.

This prevents plugins from calling services that no longer exist, and also prevents duplicate listeners or duplicate registrations.


Nested context: ctx.plugin()

ctx.plugin() is used to register a sub-plugin inside the current plugin.

A child plugin creates a child Fiber, which inherits the parent context but has an independent lifecycle.

The child Fiber will be unloaded together with the parent plugin.

Example

// File path: scratch-plugin/src/parent-plugin.ts
export function apply(ctx: Context) {
  // Register a child plugin; childPlugin can be a function, object, or Service subclass
  ctx.plugin(childPlugin)

  // Sub-plugins have their own Fiber, and are unloaded together with the parent plugin.
}

Nested contexts are very common in plugin composition: an "aggregator" plugin organizes multiple small plugins together and manages their lifecycles uniformly.


Manual early termination: fiber.dispose()

In most cases, plugins are unloaded when the context is destroyed, but sometimes you need to terminate a plugin instance early during runtime.

The Fiber object returned by ctx.plugin() providesdispose()method.

Example

// File path: scratch-plugin/src/manual-dispose.ts
import type { Context } from '@deepseek-ai/cordis'

// Type declarations: context and plugin functions (illustrative)
declare const ctx: Context
declare function myPlugin(ctx: Context): void

// Create a plugin instance and get its Fiber
const fiber = ctx.plugin(myPlugin)

// Call dispose later when manual termination is needed
// Note: dispose returns a Promise, so await the asynchronous cleanup to complete
await fiber.dispose()

dispose guarantees three things:

WarrantyDescription
All registrations removedAll registrations owned by the plugin—listeners, tools, adapters, etc.—are revoked.
Recursive unload of sub-pluginsChild Fibers created via ctx.plugin() will also be cleaned up together
Wait for asynchronous cleanup to complete.The returned Promise is resolved only after all async disposers have finished

In other words, when await fiber.dispose() returns, this plugin along with its entire subtree has fully come to a stop.


Summary self-test

Dependency-driven loading makes plugins start only when services are ready, automatically unload when services disappear, and automatically reload when services recover; ctx.plugin() creates child Fibers that can be recursively unloaded.

Test yourself:

  1. When a service declared by inject is not ready, which state does the plugin stay in?
  2. What is the relationship between the child Fiber created by ctx.plugin() and the parent Fiber?
  3. When await fiber.dispose() returns, what things are guaranteed to have been completed?
other extensions