Framework

Plugins & Lifecycle

Deep dive into plugin instantiation, dependency resolution, ready hooks, and leak-free teardown.

In long-running autonomous AI agent systems, plugins are not merely loaded once and kept perpetually static. As tool implementations update, model providers switch dynamically, and multi-agent subtasks initialize and terminate, the plugin architecture must provide sub-millisecond dynamic scheduling and leak-free resource reclamation.

Underneath DeepSeek Harness, every plugin instance is governed by a lightweight lifecycle controller called a Fiber.

This tutorial uses a “Distributed WebSocket Gateway & Resource Monitor” as a concrete case study to explore Fiber lifecycle states, ctx.effect teardown mechanics, and hot-reload auto-healing.


Core Architecture: Fiber State Machine and Transitions

When a plugin registers into the Cordis microkernel, it transitions across a deterministic lifecycle state machine:

       [ PENDING ] ──(all deps satisfied)──> [ LOADING ] ──(apply ok)──> [ ACTIVE ]
            │                                      │                         │
      (missing dependencies)                  (apply error)              (unload/HMR)
            │                                      ▼                         ▼
            └────────────────────────────────> [ FAILED ]               [ UNLOADING ]
                                                                             │
                                                                      (disposers done)
                                                                             ▼
                                                                        [ DISPOSED ]

State Machine Specification Table:

Lifecycle StateDefinition & Microkernel Behavior
PENDINGDeclared, but upstream dependencies listed in inject are not yet active; startup is paused
LOADINGAll dependencies satisfied; microkernel is executing apply(ctx, config)
ACTIVEPlugin is operating normally with all event listeners, tools, and timers active
FAILEDAn unhandled exception occurred during apply; isolated in sandbox to protect the host
UNLOADINGPlugin is unmounting; microkernel triggers all disposers registered via ctx.effect concurrently
DISPOSEDPlugin and all child sub-fibers fully unmounted and memory handles released

Reactive Auto-Healing: Dependency-Driven Scheduling

Exporting the inject array declares mandatory service dependencies:

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

export const name = 'distributed-socket-worker';

// Declare strong dependencies on tools and llm services
export const inject = ['tools', 'llm'];

export function apply(ctx: Context) {
    // Guarantee: ctx.tools and ctx.llm are in ACTIVE state when apply runs
    console.log('[SocketWorker] Dependencies satisfied. Initializing distributed stream channel...');
}

Self-Healing Mechanics: If an upstream dependency (like the llm adapter) is hot-swapped or unmounted at runtime, dependent plugins automatically and cleanly unmount (ACTIVE → DISPOSED); once the new adapter mounts, the microkernel automatically re-activates the dependent plugin.


Step 1: Managed Resource Disposal via ctx.effect

For plugins managing non-standard resources (WebSockets, database pools, or file descriptors), register disposal logic using ctx.effect:

typescript
import type { Context } from '@deepseek-ai/cordis';
import { createWebSocketClient } from './socket-client';

export const name = 'realtime-stream-gateway';

export function apply(ctx: Context) {
    // 1. Context-tracked event: unbound automatically when the plugin unmounts
    ctx.on('trajectory/step', (step) => {
        console.log(`[Stream] Trajectory step captured: #${step.index}`);
    });

    // 2. Physical network resource: managed via ctx.effect
    ctx.effect(() => {
        console.log('[Stream] Establishing remote WebSocket channel...');
        const client = createWebSocketClient('wss://agent-hub.internal/events');

        // The returned Disposer function is invoked automatically when UNLOADING
        return async () => {
            console.log('[Stream] Gracefully terminating WebSocket connection and flushing buffers...');
            await client.close();
        };
    });
}

Four Registrations Tracked Automatically by Cordis:

  • ctx.on(event, handler) — Event listeners auto-unbound;
  • ctx.tools.register(tool) — Model tools auto-unregistered;
  • ctx.llm.registerAdapter(name, adapter) — LLM adapters auto-unregistered;
  • ctx.effect(() => disposer) — Custom resource teardown callbacks executed.

Step 2: Nested Sub-Fibers and Explicit dispose Teardown

Complex plugins can dynamically attach sub-plugins by invoking ctx.plugin():

typescript
import type { Context } from '@deepseek-ai/cordis';
import SubWorkerPlugin from './sub-worker';

export function apply(ctx: Context) {
    // Mount child plugin and acquire Fiber handle
    const workerFiber = ctx.plugin(SubWorkerPlugin, { workerId: 'worker-node-1' });

    // Explicitly tear down child fiber upon event
    ctx.on('admin/terminate-worker', async () => {
        console.log('[Parent] Termination command received. Disposing child fiber branch...');
        await workerFiber.dispose();
    });
}

fiber.dispose() initiates recursive teardown throughout the sub-tree, resolving only after all asynchronous disposers have completed.


Frequently Asked Questions (FAQ)

Q1: Does an exception inside apply crash the parent Harness process?

Answer: No. The Cordis microkernel executes apply within an isolated sandbox boundary. A failing plugin transitions to FAILED and logs errors, while the rest of the agent runtime remains healthy.

Q2: Why shouldn't sequential teardown tasks be split across separate ctx.effect calls?

Answer: Cordis invokes all registered disposers concurrently to maximize performance. If cleanup task A must complete before task B starts, place them inside a single ctx.effect disposer and sequence them using await.

Q3: Are dynamically mounted child plugins pruned when the parent unloads?

Answer: Yes. Child plugins belong to the parent's Fiber hierarchy. When the parent unloads, Cordis recursively invokes dispose() on all child fibers, clearing the entire tree.

Q4: How is memory cleaned up during HMR code reloads?

Answer: Upon detecting file edits, HMR disposes of the old Fiber, waits for all listeners and ctx.effect callbacks to finish, re-imports the updated module, and mounts it into a fresh Context instance.