DeepSeek Harness Runs on an Engine Where the Agent Loop Itself Is a Replaceable Plugin
Why Does DeepSeek Harness Dare to Say "Everything is a Plugin"? Dissecting the Five Core Mechanisms of the Cordis Engine
Author: 一拳不是超人 Tags: Frontend, Artificial Intelligence, Agent
Most Agent frameworks lock their core loop into the code; changing the scheduling logic requires a fork. DeepSeek Harness (DSH) chose a more radical path: even the agent loop itself is a plugin. The underlying engine powering this design is called Cordis—this article dissects its five core mechanisms: plugins, lifecycle and side effects, services, events, and configurable plugins.
1. The Problem: The "Black Box Dilemma" of Agent Frameworks
Most Agent frameworks—LangChain, AutoGen, CrewAI—follow the same pattern: a fixed core engine with some extensible tools and model adapters attached. You can add things at the edges, but the core loop, context management, and scheduling strategy are locked inside the framework.
Want to change the scheduling logic of an agent loop? Either fork the entire framework, or submit a PR and wait for review. Want to replace the session storage method? Sorry, it's hardcoded in the core.
DeepSeek Harness (DSH) chose a different path: everything is a plugin. The model adapter is a plugin, the tool registry is a plugin, the session log is a plugin, and the agent loop itself is a plugin. There is no privileged core, no irreplaceable component.
The underlying engine powering this design is called Cordis—a "meta-framework" developed by the author of the Koishi framework and validated by over 4000 community plugins. It does only three things: manages plugin loading/unloading, manages service dependencies, and manages event distribution. All Agent business logic exists on top of it in the form of plugins.
Cordis Layered Architecture—The bottom layer is the meta-framework, the middle layer is DSH core plugins, and the top layer is user extensions. There are no hardcoded dependencies between the three layers; all collaboration happens through service keys and events.
Note a key feature in the architecture diagram: there are no arrows pointing to a "core" between the three layers—because no privileged core exists. Components like agent-loop, session, tools, which look like "framework skeletons", are the same kind of thing as the top-layer "custom tools": ordinary plugins. You can write a plugin at the top layer to replace any component in the middle layer, no fork required, no PR needed.
The next five chapters dissect the five core mechanisms of this architecture one by one.
2. Plugins: A Building Block with Its Own Manual
In Cordis, what is a plugin? It's not an injected script, not an implementation class of an interface—it is simply a function that receives a ctx (context) and does its work inside it. That's it.
Three Ways to Write, the Same Thing
Cordis supports three equivalent ways to define a plugin:
// Method 1: Pure function (most common)
export function apply(ctx) {
ctx.on('tool/call', (event) => {
console.log('Tool called', event.name)
})
}
// Method 2: Object with dependency declaration and name
export const name = 'my-plugin'
export const inject = ['tools', 'session']
export function apply(ctx) {
// At this point ctx.tools and ctx.session are guaranteed ready
}
// Method 3: Class
class MyPlugin {
static inject = ['tools']
constructor(ctx) {
// Equivalent to apply(ctx)
}
}
The three methods are completely equivalent at runtime. The only core convention: a plugin needs an apply(ctx) entry point (the function itself is apply, the class's constructor is equivalent, objects need an apply method).
What "Everything is a Plugin" Looks Like in Harness
DSH deploys 159 plugins by default. This is not an exaggeration—even the agent loop (the core scheduler responsible for driving each round of conversation) is an ordinary plugin, mounted on the ctx.agentLoop service key. You can directly disable it and replace it with your own scheduling logic.
Let's look at a real example. Suppose you want to add an "audit log before every tool call" feature to the agent:
export const name = 'audit-log'
export const inject = ['tools'] // Depends on the tools service
export function apply(ctx) {
// Listen for the tools/pre-execute event (waterfall type)
ctx.on('tools/pre-execute', (event, next) => {
console.log(
`[Audit] Tool=${event.name} Args=${JSON.stringify(event.args)}`
)
next() // Continue the execution chain, do not block
})
}
That's it. No need to inherit a base class, implement an interface, or register with a global registry. Inside apply(ctx), you get the context, and you can listen to events, register tools, and provide services. Cordis is responsible for mounting your plugin onto the plugin tree and calling apply at the right time.
Key Point: The essence of a plugin is not "implementing an interface" but "registering side effects after getting ctx". ctx is the mediator for everything—you don't need to import any concrete implementation; all collaboration is done through services and events on ctx.
3. Lifecycle and Side Effects: Come Clean, Leave Clean
The number one challenge of a plugin system is not "how to load" but "how to unload".
A plugin might register 5 event listeners, start 2 timers, connect to a database, and register 3 tools when it starts. When you unload it, how do you ensure all of these are correctly cleaned up? The traditional approach is to have plugin authors write a cleanup() method themselves—but people always forget, and once forgotten, it's a memory leak.
Cordis's answer is: funnel all side effects into a single primitive ctx.effect(), letting the framework automatically track and roll them back.
Fiber State Machine: The Life of a Plugin
Each plugin in Cordis is wrapped into a Fiber instance with its own state machine:
Fiber State Machine—The complete lifecycle from waiting for dependencies to finishing unloading. After DISPOSED, if dependencies reappear, it automatically returns to PENDING and reloads.
First, let's clarify a point that's easy to confuse: Fiber is the lifecycle container for the plugin, not the lifecycle container for side effects. Side effects are "children" attached to the Fiber, managed and reclaimed uniformly by the Fiber, but the two have different statuses:
Fiber (Plugin's lifecycle container)
├── State Machine: PENDING → LOADING → ACTIVE → DISPOSING → DISPOSED
├── Dependency Declaration: inject = ['tools', 'session'] ← Fiber handles resolution
│
├── Side Effect 1: ctx.on('agent/pre-step', handler)
├── Side Effect 2: ctx.provide('notify', {...})
├── Side Effect 3: ctx.effect(() => clearInterval(timer))
└── Child Fiber (if ctx.plugin(child) is called)
└── Has its own state machine and side effects...
Simply put: Fiber is the shell, side effects are the things inside the shell. You dispose the Fiber (the shell), and the Fiber is responsible for cleaning up the side effects inside one by one. Side effects themselves have no state machine—only two states: "registered" and "cleaned up".
A few key details:
- PENDING → LOADING: The plugin declares
inject: ['tools', 'session'], Cordis waits for both services to be ready before executingapply(). No need to write polling logic manually. - ACTIVE → DISPOSING: When the plugin is unloaded, or a service it depends on is unloaded, the Fiber enters the DISPOSING state and starts executing all disposers in reverse order.
- DISPOSED → PENDING: If a depended-on service reappears (e.g., hot reload), the plugin automatically reloads. This is the basis for hot-plugging.
ctx.effect(): "Reversible Registration" of Side Effects
The core mechanism is ctx.effect(). It receives a function that performs side effect operations and returns a teardown function:
export function apply(ctx) {
ctx.effect(() => {
// === Perform Side Effects ===
const timer = setInterval(() => {
console.log('Heartbeat')
}, 5000)
const off = ctx.on('tool/call', handler)
// === Return Teardown Function ===
return () => {
clearInterval(timer)
off()
}
})
}
When this plugin is unloaded, Cordis automatically calls the returned teardown function. The timer is cleared, the event listener is removed—fully automated.
If a plugin registers multiple effects, they are torn down in LIFO (Last-In, First-Out) order, similar to stack unwinding:
Reversible side effects are LIFO rollbacks—the last registered is the first to be torn down, ensuring dependency relationships are not broken.
Real Role in Harness
DSH's Hot Module Replacement (HMR) plugin is a beneficiary of this mechanism. When you modify the code of a tool plugin, Cordis will:
- Unload the old plugin → Automatically roll back all its registered tools, listeners, timers
- Load the new plugin → Re-execute
apply(ctx), register new side effects - The entire process is transparent to other plugins—they only see "the tool list changed", without needing to know who is hot-reloading
This is Cordis's 'Temporal Composability': the side effects of a component can be completely reversed upon removal. It's not about people writing cleanup code, but about the framework automatically tracking it.
Note:
ctx.effect()can only roll back modifications made through the Context—registering events, providing services, mounting child plugins. For irreversible operations on the external world (sent HTTP requests, data written to databases, sent emails), the framework cannot automatically roll back. Plugin authors need to handle these boundaries themselves.
When Do You Need to Manually Call ctx.effect()?
One rule to remember: Things registered by Cordis's built-in APIs are automatically reclaimed; manual cleanup of external resources requires ctx.effect().
export function apply(ctx) {
// ✅ These don't need to be wrapped in effect; Fiber automatically tears them down on unload
ctx.on('agent/pre-step', handler) // listener automatically removed
ctx.provide('notify', { ... }) // service automatically unregistered
ctx.middleware((next, send) => { ... }) // middleware automatically removed
ctx.plugin(childPlugin) // child Fiber automatically disposed
// ❌ These are external resources; Cordis can't manage them, must manually register effect
const timer = setInterval(() => heartbeat(), 5000)
ctx.effect(() => clearInterval(timer)) // otherwise timer leaks
const db = await connectDatabase(url)
ctx.effect(() => db.close()) // otherwise connection leaks
const watcher = fs.watch('./config.json', reload)
ctx.effect(() => watcher.close()) // otherwise watcher leaks
const server = app.listen(3000)
ctx.effect(() => server.close()) // otherwise port leaks
}
Judgment mnemonic: Was this resource created by a Cordis API? Yes → no effect needed; No → use effect. Common things needing effect: setInterval, setTimeout, EventEmitter.on, fs.watch, database connections, HTTP servers, WebSockets, child_process, third-party library subscriptions.
4. Services: The "Secret Code" Between Plugins
How do plugins collaborate? If Plugin A needs to call Plugin B's functionality, does it directly import B's code? No—that creates hard coupling; if B is replaced, A breaks.
Seam is Not an Optional Design Pattern, It's the Fundamental Collaboration Method of the Plugin System
First, answer a key question: What exactly is Seam? Is it a design pattern used when customizing services, or is it something inherent to the plugin system itself?
The answer is the latter. Seam is the only cross-plugin collaboration model in the Cordis plugin system. There is no choice between "using Seam" and "not using Seam"—whenever you access another plugin's capability via ctx.xxx, you are consuming a Seam; whenever you register a capability via ctx.provide('xxx', ...), you are providing a Seam.
All core services in DSH—ctx.tools, ctx.llm, ctx.sessions, ctx.agentLoop—are all Seams. They didn't "happen to use this pattern"; it's the built-in service registry mechanism of the Cordis framework itself. The framework only recognizes service keys, not concrete implementations. This means:
- Core services are also Seams: The
core/toolsplugin registers the tools service viactx.provide('tools', ...), following the exact same path as your custom plugin registeringctx.provide('myService', ...). No privileges. - Replacing a core service = providing a new provider: Want to swap out the default tool execution pipeline? Write a plugin,
ctx.provide('tools', yourImpl), and original consumers automatically switch to the new implementation. - No "bypass": You cannot bypass Seam and directly import another plugin's code. Cordis's module isolation mechanism ensures plugins can only communicate through service keys on ctx.
The Complete Lifecycle of a Seam: Define → Provide → Consume
Let's look at a complete example. Suppose DSH doesn't have a "notification service", and you want to build one yourself—allowing other plugins to send desktop notifications.
Step 1: Define the Service Interface (Contract)
// Notification service interface contract—agrees on what methods consumers can call
interface NotificationService {
notify(title: string, body: string): void
setEnabled(enabled: boolean): void
}
In DSH, service interfaces usually exist as TypeScript type declarations, serving as the "contract" between plugins. Consumers know what methods they can call just by looking at the interface, without needing to see the provider's implementation code.
Step 2: Provider—Register the Service Implementation
// desktop-notify.js — Provider of the notification service
export const name = 'desktop-notify'
export function apply(ctx) {
let enabled = true
// Register the service: mount the implementation onto the 'notify' key
ctx.provide('notify', {
notify(title, body) {
if (!enabled) return
// Call system notification API
process.stdout.write(`\x1b]9;${title}^${body}\x07`)
},
setEnabled(val) {
enabled = val
}
})
// When the provider is unloaded, the framework automatically unregisters the 'notify' service key
// Consumers will perceive the service disappearance and automatically enter PENDING to wait
}
Step 3: Consumer—Use the Service via ctx
// task-reminder.js — Consumer of the notification service
export const name = 'task-reminder'
export const inject = ['notify'] // Declare dependency
export function apply(ctx) {
// Here, ctx.notify is guaranteed ready
// Because inject declares the dependency, the framework ensures the loading order
ctx.on('task/completed', (event) => {
ctx.notify.notify('Task Completed', `"${event.taskName}" has been completed`)
})
}
Three roles, each with its own responsibility: the definer manages "what can be called", the provider manages "how to implement", and the consumer manages "when to call". The provider can be replaced at any time, and the consumer code doesn't need a single line changed.
Seam Model—This is not a pattern that some custom service "happened to use", but the fundamental collaboration method of the Cordis plugin system. All ctx.xxx accesses are Seams.
All core services in DSH follow this model:
| Service Key | Provider | Capability |
|---|---|---|
ctx.tools |
core/tools plugin | Tool registry and protected execution pipeline |
ctx.llm |
llm/llm plugin | Message vocabulary and model adapter seam |
ctx.sessions |
core/session plugin | Append-only event log and in-memory storage |
ctx.agentLoop |
core/agent-loop plugin | Default Turn/Step driver implementation |
ctx.systemPrompt |
core/system-prompt plugin | Prompt paragraph and tool schema assembly |
Note: These "core" services follow the exact same registration path as ctx.notify in the example above. Inside the core/tools plugin, it's also written as ctx.provide('tools', {...}), with no privileged API.
Replacing a Provider = Changing Half the Product
The most powerful aspect of Seam is: swap a provider, and the consumer code doesn't need a single line changed.
Let's look at a real scenario in DSH. By default, the filesystem provider points to the local disk—Bash tools execute locally, the file editor modifies local files. Now you want to move all execution to a remote sandbox:
// remote-sandbox.js — Replace the fs service provider
export const name = 'remote-sandbox'
export function apply(ctx) {
// Provide a new fs service implementation, overriding the default local filesystem
ctx.provide('fs', {
readFile: (path) => rpc.call('remote_read', path),
writeFile: (path, data) => rpc.call('remote_write', path, data),
exec: (cmd) => rpc.call('remote_exec', cmd),
})
}
After mounting this plugin, the Bash, PTY, and LSP tools automatically migrate to the remote sandbox—because they consume the ctx.fs service key, not an import of a specific local filesystem module. The provider changed, the consumers are unaware.
Effect of Replacing a Provider—From local filesystem to remote sandbox, zero code changes. This is the direct benefit of "core services are also Seams": even infrastructure like the filesystem can be replaced by an ordinary plugin.
inject: Declared Dependencies, Automatic Loading Order
Plugins declare which services they need via inject. Cordis automatically derives the loading order based on this declaration:
// This plugin needs the tools and session services
export const inject = ['tools', 'session']
export function apply(ctx) {
// Here, ctx.tools and ctx.session are guaranteed ready
// No need for if (ctx.tools) checks
ctx.tools.register({
name: 'search',
execute: (args) => { ... }
})
}
If the tools service is not yet ready (the provider hasn't loaded), this plugin's Fiber will stay in the PENDING state until tools is available before entering LOADING. Conversely, if the provider of the tools service is unloaded, this plugin will be automatically unloaded first (because the dependency is gone), and automatically reloaded when tools reappears.
This is Cordis's 'Spatial Composability': components declare dependencies through services, and the framework automatically manages the causal relationships of loading and unloading. You don't need to write a single line of "wait for the other party to be ready" code.
5. Events: The Nervous System of Plugins
Services solve "how plugins call each other's capabilities", but there's another class of problems services can't solve: how do plugins intervene at key points?
For example: before each model request, check if the message contains sensitive information. After each tool execution, record the elapsed time. Before each turn ends, decide whether to append a step. These are not "calling a service"—they are "intercepting or observing at a certain timing".
Cordis solves this problem with typed events, having four dispatch modes:
Four Event Modes
| Mode | Behavior | Analogy |
|---|---|---|
emit |
Fire-and-forget, all listeners execute synchronously, return values ignored | Broadcast notification—"Something happened to me, handle it yourself if you hear it" |
waterfall |
Chain passing, each listener receives the previous result, must call next() to continue |
Middleware pipeline—"Data passes through my hands, I can change it, or directly block it" |
serial |
Serial execution, no next(), cannot delegate |
Ask one by one—"Everyone says a word, no right to refute" |
parallel |
Parallel fan-out, all listeners execute simultaneously | Group task assignment—"Everyone work together, wait for the slowest one" |
How to choose in DSH?
- Need to intercept/rewrite data → waterfall (e.g., agent/pre-step can reject or rewrite messages)
- Need to observe/record → emit (e.g., session/created does not affect the flow)
- Need to decide one by one → serial (e.g., agent/turn-stopping each listener votes)
In Practice: The Event Flow Inside Agent Loop
DSH's agent loop is the best teaching case for the event system. A round of conversation (Turn) is sliced into multiple steps (Step), and each key node has a corresponding event:
The diagram above is the complete event flow of the Agent Loop—those marked as "extension points" are interceptable events (waterfall/serial), the rest are durable persistence events.
Note the difference between the two types of nodes in the diagram:
- durable nodes: Persistence events, written to the session log. Used to record "what happened"—fork, resume, replay all derive from this event stream.
- extension point nodes: waterfall or serial events, which are "seams" that plugins can intercept.
Let's take a practical interception example. Suppose you want to make a "sensitive word filter" plugin—check messages before each model request, intercept if sensitive words are found:
export const name = 'sensitive-filter'
export const inject = ['agent']
export function apply(ctx) {
// agent/pre-step is a waterfall event
ctx.on('agent/pre-step', (event, next) => {
const messages = event.messages
const hasSensitive = messages.some(m =>
m.content.includes('password') || m.content.includes('token')
)
if (hasSensitive) {
// Don't call next(), directly reject—short-circuit the entire chain
return { kind: 'reject', reason: 'Sensitive information detected' }
}
// No problem, let it through
next()
})
}
The key lies in next(). In waterfall events, each listener receives two parameters (event, next). Calling next() hands control to the next listener; not calling it directly short-circuits—subsequent listeners and default behavior will not execute. This is the same idea as Koa middleware and Express middleware.
Events vs Services: When to use which? There's a simple judgment principle: Use events for interception and strategy, use service methods for directly calling stable capabilities. For example, "check permissions before every tool call" is a strategy, use the
tools/pre-executeevent; "register a new tool" is a direct capability, use thectx.tools.register()service method.
6. Configurable Plugins: Building LEGO with Config Files
So far, we've talked about "writing plugins with code". But DSH has an even higher-level capability: composing plugins with configuration files, allowing you to customize your Agent without writing a single line of code.
This system consists of three concepts: Bundle, Profile, Patch.
Four Layers of Configuration, from Coarse to Fine
Four-Layer Configuration Cascading Model—From Bundle to CLI overlay, layer by layer override.
The role of each layer:
- Bundle: A distribution format of a set of Cordis configuration lines + corresponding code.
dsh-baseis the first layer for all Profiles, providing core capabilities. Stackdsh-web-app(adds browser UI) ordsh-headless(adds headless runner) on top. - Profile Patch: An override file targeting a specific Profile. For example, if your web Profile wants to swap a different model adapter, patch it here.
- Home Patch: Global override, effective for all Profiles. For example, if you want to globally disable a certain tool.
- CLI Overlay: Command-line
--patchparameter, temporary highest-priority override. Suitable for debugging and one-off experiments.
What Does a Patch Look Like
A Patch file is just a YAML, locating the configuration to replace or add via line IDs:
# cordis.patch.yml
# Replace the default LLM adapter with a custom provider
- id: llm-deepseek
replace:
plugin: my-custom-llm
config:
apiKey: ${env.MY_API_KEY}
model: deepseek-v4-pro
# Add a new audit log plugin
- id: audit-log
insert:
plugin: @my-org/dsh-audit
config:
logPath: /var/log/dsh-audit.jsonl
Want to see what your machine actually started? One command:
dsh --profile web --dump-config
This will print the merged complete plugin tree—every line can be overridden by your own patch.
Role in Harness: Four Modes
DSH has four built-in Profile modes, each loading a different set of plugins:
| Mode | Loaded Plugins | Applicable Scenario |
|---|---|---|
| Standard Mode | Full tool suite + Web UI | Daily development use |
| PTC Mode | Programmatic tool calling—model generates code to compose multi-round tools | Complex workflow automation |
| Minimal Mode | Only Shell + File editing tools | Model benchmarking in minimal environments |
| Creative Mode | Inspectable runtime, experiment with Cordis plugins in memory | Composing and creating new modes |
The difference between these four modes is merely the different set of plugins loaded—no if-else branches are written in the code. Switching modes is switching Profiles, which is switching a plugin tree. This is what "everything is a plugin" means in practice: even the "product form" itself is configuration.
7. The Advantages of This Architecture
Five chapters dissected, back to the original question: Why does DSH use Cordis? What exactly is good about this architecture?
Advantage 1: Zero-Fork Extension
Traditional frameworks, to change core behavior, the path is fork → modify source code → maintain diffs. Cordis's path is write a plugin → ctx.provide('xxx', newImpl) → done.
The remote sandbox replacement seen earlier is a typical case: swapping the local filesystem for remote RPC, Bash/PTY/LSP three tools automatically migrated with zero code changes. In traditional frameworks, this is a major project—you need to change all fs.readFile call sites in the framework source code. In Cordis, you just provide a new Seam provider.
Advantage 2: Safe Hot-Plugging
ctx.effect() + LIFO rollback ensures plugins "come clean, leave clean". This means you can:
- Hot Reload: After modifying plugin code, automatically unload the old, load the new, other plugins are unaware
- Dynamic Start/Stop: Load/unload plugins on demand at runtime, no need to restart the process
- A/B Experiments: Load two plugins implementing different strategies simultaneously, switch which one is active via configuration
The foundation of these capabilities is the framework automatically tracking side effects—not relying on plugin authors to consciously write cleanup, but relying on the reversible registration mechanism of ctx.effect().
Advantage 3: Self-Organizing Dependencies
inject declaration + Fiber state machine = automatic dependency relationship derivation. You don't need to:
- Manually arrange plugin loading order
- Write
if (ctx.tools)to check if a service is ready - Worry about circular dependencies—the framework can detect them at the loading stage
Plugins declare dependencies through service keys, and the framework handles topological sorting and lifecycle linkage. When a dependency disappears, consumers are automatically unloaded; when the dependency recovers, they are automatically reloaded. All of this is declarative.
Advantage 4: Configuration as Product Form
The difference between the four Profile modes (Standard/PTC/Minimal/Creative) is merely that different plugin sets are loaded. No if (mode === 'ptc') is written in the code. Switching product form = switching configuration file = switching a plugin tree.
This means you can use the same codebase, through different Bundle + Patch combinations, to derive completely different product forms—development tools, CI robots, benchmarking platforms—without maintaining multiple forks.
One-Sentence Summary
When the Agent field is still rapidly evolving—new model capabilities, new tool types, new scheduling strategies emerging endlessly—what you need is not a fixed framework, but a base that allows all parts to be freely replaced, freely combined, and freely hot-plugged. Cordis is this base: no privileged core, everything is a plugin, registration is reversible, dependencies self-organize.
References:
- deepseek-ai/deepseek-harness — GitHub Repository
- cordiverse/cordis — Cordis Meta-Framework
- A Programming Paradigm for Spatiotemporal Composability — DeepSeek AI & Peking University, 2026-08-13
- cordis.moe — Cordis Official Documentation
- Koishi — Four years of development, 4000+ community plugins, Cordis's first large-scale validation case