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 State | Definition & Microkernel Behavior |
|---|---|
PENDING | Declared, but upstream dependencies listed in inject are not yet active; startup is paused |
LOADING | All dependencies satisfied; microkernel is executing apply(ctx, config) |
ACTIVE | Plugin is operating normally with all event listeners, tools, and timers active |
FAILED | An unhandled exception occurred during apply; isolated in sandbox to protect the host |
UNLOADING | Plugin is unmounting; microkernel triggers all disposers registered via ctx.effect concurrently |
DISPOSED | Plugin 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:
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
llmadapter) 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:
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():
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.