跪拜 Guibai
← Back to the summary

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.

image.png

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:

image.png

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:

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:

image.png

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:

  1. Unload the old plugin → Automatically roll back all its registered tools, listeners, timers
  2. Load the new plugin → Re-execute apply(ctx), register new side effects
  3. 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:

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.

image.png

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.

image.png

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"

image.png

How to choose in DSH?

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:

image.png

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:

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-execute event; "register a new tool" is a direct capability, use the ctx.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

image.png

Four-Layer Configuration Cascading Model—From Bundle to CLI overlay, layer by layer override.

The role of each layer:

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:

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:

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: