跪拜 Guibai
← Back to the summary

DeepSeek Harness: A Plugin Runtime Where the Agent Loop Itself Is Replaceable

DeepSeek Harness: Composable Plugin Runtime & Everything is a Plugin

This article is written based on the deepseek-harness repository source code and the official docs/cordis-tutorial/ tutorial, and in the last chapter, it is deeply compared with Claude Code, Codex, and AgentScope (Alibaba). After reading, you should be able to: understand the design philosophy of Harness, run through the complete chain from "Hello Plugin" to "Real Coding Agent", understand cordis.yml composition and HMR, and clearly know its differences from other mainstream frameworks.

GitHub: https://deepseek-harness.github.io/deepseek-harness

1. What is DeepSeek Harness

1.1 One-Sentence Definition

DeepSeek Harness is an Agent runtime framework officially produced by DeepSeek. It is not a "hardcoded assistant", but a composable plugin runtime: you use a YAML configuration file (cordis.yml) to piece together all capabilities like building blocks—"session management, system prompts, tools, LLM adapters, file access, subprocesses, sandboxes, and even the agent main loop itself".

Its underlying layer is a micro plugin framework called Cordis (source code vendored in vendor/cordis/). Cordis provides a shared Context, and each capability is a plugin mounted onto ctx.

One-sentence positioning:

Claude Code is a "product", DeepSeek Harness is a "framework", and AgentScope is a "development library". The former gives you an out-of-the-box coding assistant; Harness gives you an Agent engine that can be freely assembled, self-hosted, and embedded into your own products; and AgentScope gives you a Python-based "library for building agent applications", focusing on multi-agent and engineering toolchains.

1.2 Multi-Dimensional Breakdown: What Exactly Is It

To truly understand Harness, you need to look at it from five dimensions simultaneously:

Dimension What It Is Counterexample (What It Is Not)
Delivery Form A framework + a set of official plugins + a CLI/ACP entry point A closed, out-of-the-box product
Kernel Paradigm A Cordis-based plugin runtime, everything is a plugin A monolithic program with hardcoded logic
Composition Method Declarative cordis.yml configuration-driven, assembled by id Imperative new instances in code
Capability Boundary Abstract Service (Service Definition) + replaceable Provider + Consumer Hardcoding implementation details at the caller
Scope of Application Self-hostable, embeddable in own products, supports long-term sessions Terminal-only interactive assistant

1.3 Design Philosophy: Why Design It This Way?

DeepSeek Harness adopts the "everything is a plugin" design philosophy. We use a plugin-based open architecture to build the Agent Harness: all Agent capabilities such as models, tools, skills, sessions, sandboxes, storage, loops, scheduling, and UI are composed of plugins, freely replaceable and flexibly reorganized.

Harness's design is not "pluggable for the sake of being pluggable", but a systematic trade-off made around several clear engineering goals and constraints. Let's analyze them one by one.

Idea 1: Use "Everything is a Plugin" to Eliminate Hardcoded Loop Logic

The agent main loop (call model → run tools → repeat) of most coding assistants is hardcoded in the core. Harness's counter-intuitive decision is: the entire repository only has the dsh-agent-loop package containing specific loop logic (explicitly stated in packages/core/agent-loop/README.md), and the rest are all abstract services or extension point plugins.

Design Motivation:

Idea 2: The "Three-Role" Model for Capability Layering (capability-seam)

Each capability is deliberately split into three independently evolving roles (see docs/glossary.md#capability-seam):

Design Motivation:

Idea 3: Registration is an Effect, Handing Lifecycle to the Framework

An iron rule from AGENTS.md: "Registrations are effects: every contribution goes through ctx.effect() / ctx.on(); a registry's register() returns the disposer."

Design Motivation:

Idea 4: Dependency Injection is "Continuous Tracking", Not a One-Time Check

Consumers write inject: ['tools'], and Cordis will keep the plugin PENDING until ctx.tools exists. At runtime, if the service disappears (provider unloaded/hot-swapped), the dependent plugin is unloaded accordingly, and reloaded when the service recovers.

Design Motivation:

Idea 5: The "Model-Visible ⟺ Logged" Audit Constraint

A hard constraint from AGENTS.md: "Model-visible ⟺ logged: anything that reaches a model request must be reconstructable from the session log; a new model-visible input requires a session event."

Design Motivation:

Idea 6: Explicit Over Implicit, Fail Loudly on Error, Never Silently

Multiple rules from AGENTS.md:

Design Motivation:

Idea 7: Security is Layered, Not Single-Point

docs/defensive-patterns.md provides specific rules:

Design Motivation: Treat "executing untrusted output" as a top threat, defending simultaneously from the environment, file, and process layers, rather than relying on the convention of "users shouldn't run strange commands".

1.3.1 Everything is a Plugin Design

image.png

1.3.2 Multiple Operation Modes

For different usage scenarios, DeepSeek Harness provides four modes, each loading a different set of plugins by default:

image.png

1.4 How These Seven Ideas Converge into a System

Stringing the above seven points together, the main design thread of Harness is:

Use a plugin runtime (Cordis) to host all capabilities, use "abstract service + replaceable provider" to isolate changes, use effects to hand the lifecycle to the framework, use dynamic satisfaction of injection to achieve configuration-driven composition, use events to push behavior to extension points, use session logs as reconstructable truth, and use explicit validation and layered security to guard deployment and execution boundaries.

It thus presents an orientation distinctly different from Claude Code / Codex (products) and AgentScope (Python development library): the former cares about "users getting started out-of-the-box", the latter cares about "researchers quickly building multi-agent systems", while Harness cares about "how platform/product engineers can reliably self-host and evolve an Agent foundation over the long term". This is also why its engineering discipline is extremely strict (100% coverage gate, type-equiv doc sync, branded types, declarative surface), because the reliability of the foundation is the prerequisite for everything above it.

2. Core Mental Model: Everything is a Plugin

Chapter 1 answered "why design it this way" (seven ideas + design motivations); this chapter answers "how it actually runs", and uses an overall architecture diagram to map these seven ideas onto the physical structure. If you skipped Chapter 1, just remember one sentence: There is no "hardcoded assistant" in Harness; all capabilities are plugins mounted on the shared ctx.

DeepSeek Harness is built on the Cordis plugin system, which features spatiotemporal composability. The Cordis meta-framework is only responsible for loading, unloading, and dependency management of plugins. All specific components of Agent Harness are different Cordis plugins. Plugins collaborate with each other through Cordis services and events, and can be freely combined at the configuration layer.

Developers do not need to modify the source code of DeepSeek Harness itself to independently select, replace, or extend any capability in the form of plugins. This is the most important design principle of DeepSeek Harness: Everything is a Plugin.

The entire repository follows an iron rule (see AGENTS.md):

Everything is a plugin.

This means:

All plugins share the same ctx and collaborate through three mechanisms:

Mechanism Keyword Role
Dependency Injection inject: ['tools'] Plugin declares it depends on a service; Cordis starts it after the service is ready
Registration / effect ctx.effect() / ctx.on() Plugin contributes capabilities (registers tools, listens to events); automatically revoked on unload
Events ctx.on(event, cb) / ctx.waterfall() Decoupled inter-plugin communication

Key Design: Registration is an "effect". Every contribution is made through ctx.effect(), and contributions are automatically revoked when the plugin is unloaded. This is the core of Cordis lifecycle management and the fundamental point that distinguishes it from frameworks that "manually manage global singletons" (like many Python agent libraries).

image.png

2.1 Overall Architecture Diagram

The following diagram is drawn based on packages/bundle/base/cordis.patch.yml (the 45+ id lines of the base bundle) and packages/core/agent-loop/README.md (the 5 services injected by agent-loop), reflecting the real composition relationship, not a schematic.

graph TB
    subgraph RUNTIME["Cordis Runtime (vendor/cordis)"]
        ROOT["Root Context
(shared ctx)"]
        LOADER["Loader Plugin
reads cordis.yml / --profile patch layers"]
        HMR["@cordis-plugin-hmr
File change hot reload"]
        TIMER["@cordis-plugin-timer"]
    end

    subgraph CORE["Core Backbone (packages/core + base bundle)"]
        AGENTLOOP["dsh-agent-loop
(ctx.agentLoop)
The only concrete loop driver"]
        AGENT["dsh-agent
(ctx.agents factory)"]
        TOOLS["dsh-tools
(ctx.tools registry)"]
        LLM["dsh-llm
(ctx.llm abstract + DeepSeek provider)"]
        SYSPROMPT["dsh-system-prompt
(ctx.systemPrompt)"]
        SESSION["dsh-session
(ctx.session persistence/projection)"]
    end

    subgraph DRIVE["5 Interface Services Injected by agent-loop (from README)"]
        AGENTLOOP -.injects.-> AGENT
        AGENTLOOP -.injects.-> SESSION
        AGENTLOOP -.injects.-> LLM
        AGENTLOOP -.injects.-> TOOLS
        AGENTLOOP -.injects.-> SYSPROMPT
    end

    subgraph EXEC["Execution / Sandbox Capabilities (Provider Plugins)"]
        SHELL["dsh-shell-local / dsh-shell-pwsh"]
        SUBPROC["dsh-subprocess-local"]
        SANDBOX["dsh-sandbox-local
+ sandbox-policy"]
        FS["dsh-tool-fs / fs-search
(dsh-fs-sandbox)"]
        TERMINAL["terminal / code-runtime"]
    end

    subgraph MODEL["Model / Retrieval Capabilities"]
        WEB["dsh-web (web_search)"]
        DEEPSEEK["dsh-llm-deepseek
(native adapter)"]
        PIAI["dsh-llm-pi-ai
(multi-provider twin)"]
        RETRY["dsh-llm-retry"]
    end

    subgraph ORCH["Orchestration / Subtask Capabilities"]
        SUBAGENT["dsh-subagent
(spawn / fork provider)"]
        WORKFLOW["dsh-workflow
(worker-thread)"]
        JOBS["dsh-jobs-local"]
        TODO["dsh-tool-todo"]
        GOAL["dsh-goal / command-goal"]
    end

    subgraph SESS["Session / Human-Machine Collaboration"]
        PERSIST["session-persistence-jsonl"]
        QUERY["session-query-sqlite
(full-text search, optional)"]
        PROJ["session-projection"]
        COMPACT["compaction-basic
+ tool-result-pruner"]
        APPROVAL["user-approval + permission-presets"]
        INTERACT["interaction / commands / plan-mode"]
    end

    subgraph EXT["Extensions / Interoperability"]
        SKILL["dsh-skill + tool-skill"]
        HOOKS["hooks-claude-code / hooks-codex
(Hook interoperability bridges)"]
        ACP["dsh-acp
(automation protocol server)"]
        EXTENSIONS["extensions
(agent self-modification plugins)"]
        BUNDLE["dsh-bundle
(--profile patch layers)"]
    end

    %% Configuration-driven loading
    LOADER -->|"Assemble by id
Activate when service ready"| CORE
    LOADER --> EXEC
    LOADER --> MODEL
    LOADER --> ORCH
    LOADER --> SESS
    LOADER --> EXT
    HMR --> LOADER

    %% Tool registration: capability plugins register tools into ctx.tools
    SHELL --> TOOLS
    FS --> TOOLS
    WEB --> TOOLS
    SUBAGENT --> TOOLS
    WORKFLOW --> TOOLS
    TODO --> TOOLS
    SKILL --> TOOLS

    %% Model chain
    DEEPSEEK --> LLM
    PIAI --> LLM
    RETRY --> LLM

    %% Session chain
    PERSIST --> SESSION
    QUERY --> SESSION
    PROJ --> SESSION
    COMPACT --> SESSION

    %% Event bus (decoupled plugin communication)
    EVENTS{{"Event Bus
agent/* · tools/result · agent/request
approval/request · session/event"}}
    AGENTLOOP --> EVENTS
    TOOLS --> EVENTS
    SHELL --> EVENTS
    APPROVAL --> EVENTS
    SANDBOX --> EVENTS
    INTERACT --> EVENTS

    %% Entry
    ENTRY["CLI (pnpm dsh)
/ ACP / JSON-RPC Entry"]
    ENTRY --> ROOT
    BUNDLE --> LOADER

2.2 Diagram Relationships Explained with Source Code

1. The Physical Form of "Everything is a Plugin"

2. agent-loop is the Only "Concrete Loop"

3. Tools are "Registered", Not "Hardcoded"

4. Replaceable Provider Three-Role Model

5. Self-Modification and Interoperability

The 5 dashed injection lines of dsh-agent-loop, the 7+ tool registrations of dsh-tools, and the --profile patch layer assembly in the diagram all come directly from the source code facts in packages/bundle/base/cordis.patch.yml and packages/core/agent-loop/README.md.

3. Environment Setup (5 Minutes)

3.1 Background You Need to Know First (Must-Read for Beginners)

The examples in this article use TypeScript to write plugins, but you don't need to be proficient in TS. Just understand the following four points:

3.2 Environment Prerequisites

Prerequisites (see docs/development.md for details):

git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install
pnpm run typecheck   # Verify environment is ready

Create a temporary tutorial directory (tmp/ is already git-ignored, will not be committed):

mkdir -p tmp/cordis-tutorial
cd tmp/cordis-tutorial

All subsequent examples are run from this same directory:

node --import tsx ../../vendor/cordis/bin.js

This single-file launcher will: ① create the root Context; ② mount the Loader plugin; ③ read ./cordis.yml from the current directory and load each plugin listed inside. No build steps are required (--import tsx allows Node to run TS directly).

4. Hands-On: Your First Plugin

4.1 Write a Plugin

Create hello.ts under tmp/cordis-tutorial:

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

export const name = 'hello'

export function apply(ctx: Context) {
  console.log('hello from my first plugin')
}

4.2 Compose the Application

Create cordis.yml:

- name: './hello.ts'

This is a list of configuration items. name is a module specifier (relative path or npm package name).

4.3 Run

node --import tsx ../../vendor/cordis/bin.js

Output:

hello from my first plugin

4.4 Three Plugin Forms

import { Service, type Context } from '@deepseek-ai/cordis'

// 1. Function plugin (most common)
export function apply(ctx: Context) {}

// 2. Object plugin: object with apply method
export const objectPlugin = { name: 'obj', apply(ctx: Context) {} }

// 3. Class plugin: Service subclass (used when exposing services, see Chapter 6)
export class MyService extends Service {
  constructor(ctx: Context) { super(ctx, 'myService') }
}

Advice for beginners: Use the function form until you need to expose a service.

4.5 Fault Tolerance Behavior (Must-Know for Beginners)

5. Lifecycle and Effects (Automatic Resource Reclamation)

Cordis plugins may be unloaded due to configuration changes, hot reload, explicit resource release, or the disappearance of required services. Registrations established through the Cordis API are effects and will be revoked when the owning plugin is unloaded; resources managed outside these APIs must be wrapped in ctx.effect().

Create lifecycle.ts:

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

export const name = 'lifecycle-demo'

function heartbeat(ctx: Context) {
  console.log('heartbeat plugin loading')
  ctx.effect(() => {
    const timer = setInterval(() => console.log('tick'), 200)
    return () => {
      clearInterval(timer)
      console.log('heartbeat cleaned up')
    }
  })
}

export function apply(ctx: Context) {
  const fiber = ctx.plugin(heartbeat)
  ctx.effect(() => {
    const timer = setTimeout(async () => {
      await fiber.dispose()
      console.log('disposed')
      process.exit(0)
    }, 700)
    return () => clearTimeout(timer)
  })
}

Output after running:

heartbeat plugin loading
tick / tick / tick
heartbeat cleaned up
disposed

Three key points:

Fiber State Machine

Each loaded plugin instance has a fiber, transitioning between the following states:

PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED
                 ↘ FAILED

Operations That Are Already Effects (You Rarely Need to Write ctx.effect() Manually)

Order Note: Disposers are started in reverse order of registration, but multiple async disposers run concurrently; if teardown must be sequential, put the steps inside the same disposer and await them in order.

6. Services: Registration and Consumption of Capabilities

Services are named capabilities provided by plugins and consumed by other plugins through ctx. In harness, ctx.tools, ctx.llm, ctx.agents are all services. Consumers only specify a capability name like 'tools', without importing the provider—thus configuration can choose the provider without changing consumer code.

6.1 Providing a Service

greeter.ts:

import { Service, type Context } from '@deepseek-ai/cordis'

declare module '@deepseek-ai/cordis' {
  interface Context {
    greeter: GreeterService
  }
}

export class GreeterService extends Service {
  constructor(ctx: Context) {
    super(ctx, 'greeter')
  }
  greet(who: string) {
    return `Hello, ${who}!`
  }
}

export const name = 'greeter'
export function apply(ctx: Context) {
  ctx.plugin(GreeterService)
}

Two parts work together:

6.2 Consuming a Service (inject)

consumer.ts:

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

export const name = 'consumer'
export const inject = ['greeter']

export function apply(ctx: Context) {
  console.log(ctx.greeter.greet('world'))
}

inject lists the services this plugin needs. Cordis will keep the plugin PENDING until every service exists, so within apply, ctx.greeter is guaranteed to be ready—loading order is irrelevant.

- name: './greeter.ts'
- name: './consumer.ts'

Output Hello, world!. Swapping the order of the two lines does not change the output; if greeter.ts is completely removed (or the package name is misspelled), the consumer will remain PENDING / fail to start—it neither crashes nor runs halfway when dependencies are missing, but explicitly stops in a waiting state (see Chapter 9 for the diagnoser).

Terminology Distinction: In this example, export const name = 'consumer' is the plugin display name; greeter in inject: ['greeter'] is the service name (registered by super(ctx, 'greeter')). The two namespaces are different—plugins can be arbitrarily named, but injection must exactly match the service name, otherwise it will be PENDING forever.

6.3 inject is Continuous Tracking, Not a One-Time Check

If a required service disappears at runtime (e.g., the provider is unloaded, hot-swapped), each dependent plugin is unloaded accordingly, and reloaded when the service recovers. Combined with effects, this prevents consumers from holding references to unavailable services. This is precisely why configuration can replace services: unload dsh-shell-local, mount another shell provider, and all plugins with inject: ['shell'] will restart using the new implementation.

6.4 Optional Dependencies

inject is a hard dependency. If it's acceptable to work when missing, skip inject and probe:

export function apply(ctx: Context) {
  const greeter = ctx.get('greeter')
  console.log(greeter?.greet('maybe') ?? 'no greeter available')
}

Principle: Extension plugins depend on Service Definition (abstract service) rather than a specific provider. This way, LLM adapters, executors, etc., can be hot-swapped without affecting each other. Service names share a flat namespace; prefix your own services (harness has already occupied tools, llm, etc.).

7. Event System: Decoupled Communication and Interception

Services support direct invocation; events allow plugins to broadcast without knowing who is listening. harness uses events to handle interactions like tool results, model requests, and approval decisions.

7.1 Declaring, Emitting, Listening

stats.ts (Counting Service):

import { Service, type Context } from '@deepseek-ai/cordis'

declare module '@deepseek-ai/cordis' {
  interface Context { stats: StatsService }
  interface Events {
    'stats/report'(name: string, count: number): void
  }
}

export class StatsService extends Service {
  private counts = new Map<string, number>()
  constructor(ctx: Context) { super(ctx, 'stats') }
  bump(name: string) {
    const next = (this.counts.get(name) ?? 0) + 1
    this.counts.set(name, next)
    this.ctx.emit('stats/report', name, next)
  }
}

reporter.ts:

import type { Context } from '@deepseek-ai/cordis'
import type {} from './stats.ts'

export const name = 'reporter'
export const inject = ['stats']

export function apply(ctx: Context) {
  ctx.on('stats/report', (name, count) => {
    console.log(`[stats] ${name} -> ${count}`)
  })
  ctx.stats.bump('tool_call'); ctx.stats.bump('tool_call'); ctx.stats.bump('prompt')
}

import type {} from './stats.ts' lets TS see the declaration merging (no runtime side effects). Output:

[stats] tool_call -> 1
[stats] tool_call -> 2
[stats] prompt -> 1

ctx.on() is an effect, the listener disappears with the plugin, never needing manual removeListener.

Note declare module '@deepseek-ai/cordis' { interface Events { ... } } this declaration merging: it writes 'stats/report' and its signature into Cordis's global event table, so ctx.emit / ctx.on check event names and parameter types at compile time. Forgetting the declaration merging, the event is still a valid string event, but loses type protection—this is the most common pitfall when developing based on Cordis.

7.2 Five Dispatch Modes

Mode Invocation Semantics
emit ctx.emit(name, ...) Synchronous broadcast; does not wait/collect return values
parallel await ctx.parallel(...) All concurrent and awaited together
serial await ctx.serial(...) Sequential wait; first non-null/false/undefined wins and stops
bail ctx.bail(...) Synchronous version of serial
waterfall ctx.waterfall(name, ...args, next) Wrapping middleware, can transform or short-circuit

7.3 waterfall: Transform or Short-Circuit

declare module '@deepseek-ai/cordis' {
  interface Events {
    'demo/transform'(input: string, next: () => Promise<string>): Promise<string>
  }
}
// Listener 1: wrap downstream result
ctx.on('demo/transform', async (input, next) => {
  const downstream = await next()
  return downstream.toUpperCase()
})
// Listener 2: short-circuit when it has the decision
ctx.on('demo/transform', async (input, next) => {
  if (input.includes('blocked')) return '** blocked **'
  return next()
})

await ctx.waterfall('demo/transform', 'hello', async () => 'hello')        // HELLO
await ctx.waterfall('demo/transform', 'blocked words', async () => '...') // ** BLOCKED **

Discipline: Waterfall listeners that only observe/annotate must call next(); not calling it represents an intentional short-circuit. A logging listener that forgets next() will silently swallow all downstream default behavior—this is a standing rule in this repository. Harness uses waterfall for collaborative decisions: agent/request allows plugins to replace model call configuration, approval/request allows policies to answer on behalf of the user.

8. Configuration: Declarative and Fail Loudly

Each configuration item in cordis.yml can carry a config block, and the plugin exports a schema that is validated before apply. Misconfiguration causes a load failure with an accurate error: a plugin will never start with incomplete configuration.

import type { Context } from '@deepseek-ai/cordis'
import Schema from '@deepseek-ai/schemastery'

export const name = 'config-demo'
export interface Config { greeting: string; targets: string[] }
export const Config: Schema<Config> = Schema.object({
  greeting: Schema.string().default('Hello'),
  targets: Schema.array(String).default(['world']),
})
export function apply(ctx: Context, config: Config) {
  for (const target of config.targets) console.log(`${config.greeting}, ${target}!`)
}
- name: './config-demo.ts'
  config:
    targets: ['alpha', 'beta']

Output Hello, alpha! / Hello, beta! (when greeting is not provided, it is filled with the schema default, apply always receives a complete and validated configuration).

Passing an invalid value:

config: { targets: 'not-an-array' }
ValidationError: invalid config:
  - $.targets expected array but got not-an-array (at targets)

The fiber enters FAILED, and the launcher prints the error and exits with code 1. Failing loudly is better than silently skipping is a consistent convention of this repository.

The loader also supports the !!js tag for computed values at load time:

config:
  greeting: !!js process.env.DEMO_GREETING ?? 'Hello'

!!js is only valid within config and item disabled; disabled: !!js ... can gate a line by platform/environment (a repository extension).

9. Composition, HMR, and Diagnostics

cordis.yml selects the plugin tree to apply. Configuration items can also carry metadata like id, disabled, nested group, isolate:

- id: greeter
  name: './greeter.ts'
- id: consumer
  name: './consumer.ts'
  disabled: true     # Keep the entry but skip mounting

id provides a stable identifier, allowing the loader to distinguish "modify an existing item" from "delete then add". disabled: true unloads the plugin without deleting the entry; changing it back reloads it along with its PENDING dependencies. group can load/unload a sub-list as a unit; isolate provides a group with an independent instance of a service name (two groups each see differently configured shell providers, without affecting each other).

9.1 Hot Module Replacement (HMR)

Unloading releases effects, loading follows dependencies, so HMR can unload then load to replace running plugins. @deepseek-ai/cordis-plugin-hmr watches files and executes this process on save:

- id: logger
  name: '@deepseek-ai/cordis-plugin-logger-console'
- id: timer
  name: '@deepseek-ai/cordis-plugin-timer'
- id: hmr
  name: '@deepseek-ai/cordis-plugin-hmr'
  config: { root: ['.'] }
- id: hello
  name: './hello.ts'

After editing and saving hello.ts:

hello from my first plugin
2026-07-22 15:44:36 [I] hmr watching [ '.' ]
2026-07-22 15:44:39 [I] hmr reload plugin at hello.ts
hello from my EDITED plugin

The old instance is unloaded first (effects rolled back), then the new code is loaded. Editing cordis.yml itself also triggers an update: the loader compares by id and only changes the differing parts. Entries without id get a new id on every read, and will be treated as delete-then-add, remounting—this is the significance of explicit id.

9.2 Diagnosing a Plugin That Never Loads

The other side of dependency-driven loading: if inject specifies a service no one provides, it will remain PENDING forever, producing no output. This is not an error (PENDING is a legal state). You can directly enumerate the state:

import { FiberState, type Context } from '@deepseek-ai/cordis'
export const name = 'diagnose'
export function apply(ctx: Context) {
  setTimeout(() => {
    for (const runtime of ctx.registry.values())
      for (const fiber of runtime.fibers)
        if (fiber.state === FiberState.PENDING)
          console.log(`${fiber.name} is PENDING — a required service is missing`)
  }, 500)
}

When inject: ['timer'] has no provider, the diagnoser will print needs-timer is PENDING — a required service is missing. When a "plugin doesn't react", check the fiber state first.

10. Connecting Tools to a Real Agent

Continue using the tmp/cordis-tutorial directory created in Chapter 3, place all files here, and the run command is still node --import tsx ../../vendor/cordis/bin.js.

This is the "aha moment" of understanding Harness: write a tool that can be called by the model, passing through the real execution pipeline. No key needed, no model called.

10.1 Tool Plugin greet-tool.ts

import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
import { CallId } from '@deepseek-ai/dsh-llm'

export const name = 'greet-tool'
export const inject = ['tools']

export function apply(ctx: Context) {
  ctx.tools.register(defineTool({
    name: 'greet',
    description: 'Greet the named person.',
    parameters: {
      name: { type: 'string', required: true, description: 'Who to greet' },
    },
    output: {
      schema: { type: 'string' },
      render: (_args, value) => [{ type: 'text', text: value }],
    },
    async execute(args) {
      return `Hello, ${args.name}!`
    },
  }))

  void (async () => {
    const result = await ctx.tools.execute({
      callId: CallId('demo-1'),
      name: 'greet',
      arguments: { name: 'Cordis' },
      signal: new AbortController().signal,
    })
    console.log('tool replied:', JSON.stringify(result.content))
  })()
}

10.2 Observer Plugin tool-logger.ts

import type { Context } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/dsh-tools'

export const name = 'tool-logger'
export const inject = ['tools']

export function apply(ctx: Context) {
  ctx.on('tools/result', (exec, result) => {
    const text = result.content.map(b => (b.type === 'text' ? b.text : '')).join('')
    console.log(`[tool-logger] ${exec.name} -> ${text}`)
  })
}

10.3 Compose and Run

- name: '@deepseek-ai/dsh-system-prompt'
- name: '@deepseek-ai/dsh-tools'
- name: './tool-logger.ts'
- name: './greet-tool.ts'
node --import tsx ../../vendor/cordis/bin.js
[tool-logger] greet -> Hello, Cordis!
tool replied: [{"type":"text","text":"Hello, Cordis!"}]

Key Points:

  1. defineTool converts parameters into JSON Schema for the model, and validates parameters before execute.
  2. The logging plugin triggers first: tools/result is emitted during the materialization of the result, earlier than the fulfillment of execute's promise. The two plugins are unaware of each other—they are connected by the registry service and events.
  3. If the tool plugin lacks a systemPrompt provider in the composition, it will remain PENDING (missing dependency), which is exactly the manifestation of the inject mechanism.

At this point, you can already read every item in examples/headless-agent/cordis.yml. A real agent = this composition + LLM adapter + agent-loop + persistence + entry point.

10.4 Concretization: The Sequence of an Agent Loop (Idea 1 / Idea 5)

The following sequence diagram comes directly from the authoritative lifecycle diagram generated by the repository (docs/agent-lifecycle.md, produced by scripts/gen-doc-graphs.ts). Now that you understand events like tools/result, agent/*, session/event, it's the perfect time to use it to map Idea 1 from Chapter 1 (only agent-loop is the concrete loop, other behaviors rely on event subscriptions) and Idea 5 (anything delivered to the model is written into the session log, log is the truth) onto each step of a real turn/step.

sequenceDiagram
    participant User
    participant Agent
    participant Driver as dsh-agent-loop
    participant Hooks as hook listeners
    participant Prompt as ctx.systemPrompt
    participant LLM as ctx.llm
    participant Tools as ctx.tools
    participant Session
    participant SDK as UI/SDK listener

    User->>Agent: followup(content)
    Agent-->>SDK: agent/inbox/spliced / agent/inbox/inserted
    Agent->>Driver: queued work wakes driver
    Driver-->>SDK: agent/status running
    Driver->>Session: turn/start
    Note over Agent,Driver: claim pending next-step input + one queued prompt

    Driver->>Hooks: agent/pre-step waterfall
    Hooks-->>Driver: authoritative reject or enter(messages)
    alt pre-step rejected / failed
        Driver-->>Driver: claimed batch stays removed, turn spends no step
    else enter proposed step
        Driver->>Session: step/start
        Driver->>Session: user/message per entered message
        Driver->>Prompt: system-prompt/assemble waterfall
        Driver->>LLM: agent/request waterfall → llm/stream waterfall
        LLM-->>Driver: StreamChunk*
        Driver->>Session: assistant/chunk*
        Session-->>SDK: session/event assistant/chunk*
        alt adapter/terminal request failure
            Driver->>Session: step/end
            Driver->>Hooks: agent/request-error waterfall
            Hooks-->>Driver: retry action or keep original error
        else model request succeeded
            Driver->>Session: assistant/message
            Driver->>Tools: classify pending call by executionMode
            opt call starts
                Driver->>Session: tool/call
                Driver->>Tools: ordered pre, concurrent execute
                Tools-->>Session: tool-owned events when applicable
            end
            opt next model-order result ready
                Driver->>Tools: ordered post
                Driver->>Session: tool/result
            end
        end
        Driver->>Session: step/end
        opt natural stop + inbox empty
            Driver->>Hooks: agent/turn-stopping serial checkpoint
        end
    end
    Driver->>Session: turn/end
    Driver-->>SDK: agent/status idle

How to Read This Diagram (Corresponding to Two Ideas)

In one sentence: The loop is responsible for "walking the process", plugins are responsible for "adding behavior", and the session log is responsible for "keeping the truth". The three are connected by the event bus, without hardcoding each other.

11. Capability Layering and Practical Operation

11.1 Capability Layering (Capability Seams)

Harness splits each capability into three roles, each evolving independently:

Role Responsibility
Service Definition Abstract interface (what the capability "is")
Service Provider Concrete implementation (e.g., local / cloud / E2B)
Consumer Model-facing tool or caller

Taking shell as an example: dsh-shell defines the capability, dsh-shell-local / dsh-shell-pwsh are providers, and dsh-shell's model tool is the Consumer.

Excerpt from the complete capability list (packages/README.md):

Design Philosophy: Maintainable dependencies are preferred over handwritten ones; use Branded<B> branded types for cross-boundary ids (not bare string); only perform validation at runtime at validation boundaries (config, model/tool JSON, files, workers, processes, threads), trust TypeScript for in-process type boundaries.

11.2 Practical: Run a Real Coding Agent

Requires building first (see docs/development.md for details) and setting the Key:

pnpm run build
# Repository root .env or environment variable
DEEPSEEK_API_KEY=sk-...
DEEPSEEK_BASE_URL=https://...   # Optional
pnpm dsh --profile headless "summarize this workspace"

Other demos:

pnpm run demo:cordis   # Agent inspects and modifies its own live plugin runtime
pnpm run demo:acp      # Expose automated Agent sessions via JSON-RPC stdio (ACP protocol)

Behind dsh --profile is the dsh-bundle patch layer: the base composition is patched by deployment overlays.

11.3 Common Command Quick Reference

pnpm install              # Install + lefthook hooks
pnpm run typecheck        # Type check (pre-push hook)
pnpm run test             # vitest unit tests
pnpm run test:coverage    # CI coverage gate (per-file 100%)
pnpm run lint
pnpm run build            # tsc emit lib/types + tsdown bundle
pnpm run hygiene          # knip + publint + constraints + NodeNext check
pnpm run doc-sync         # Documentation gate (including type-equiv validation)

pnpm dsh --profile headless "task"   # Run task from source (requires Key)
pnpm run demo:cordis                 # Self-referencing Cordis demo (requires Key)
pnpm run demo:acp                    # ACP automation server (requires Key)

Before committing/pushing, follow the "relevant checks" principle in AGENTS.md, only run the checks covering the surfaces you changed, no need to blindly run the full suite—CI is responsible for exhaustive coverage.

12. Advantage Analysis & Comparison with Claude Code / Codex / AgentScope

12.1 Horizontal Comparison of Three + One Framework

Dimension DeepSeek Harness Claude Code Codex (CLI) AgentScope (Alibaba)
Essence Agent Framework/Runtime Closed-source product (coding assistant) Closed-source product (coding Agent) Open-source development library (mainly Python)
Kernel Cordis plugin runtime, everything is a plugin Monolithic application Monolithic application Class + Pipeline DSL, ReAct paradigm
Language TypeScript (Node) Not disclosed Not disclosed Mainly Python
Composability Extremely high: assembled via cordis.yml, including replaceable agent-loop Low (settings/hooks) Low (settings/hooks) Medium (components replaceable, but assembled via code, not declarative config)
Model Binding LLM layer replaceable, default DeepSeek Locked to Claude Locked to OpenAI Multi-model (including Tongyi/OpenAI/local), model-agnostic
Self-hosting/Embedding ✅ Fully self-hostable, embeddable in products ❌ SaaS ❌ SaaS ✅ Open-source, self-deployable
Hook Interop Built-in Claude Code/Codex bridges None (independent ecosystem)
Session Persistence First-class citizen (JSONL/SQLite/lineage/full-text search) Yes (conversation history) Yes (weaker) Yes (Memory/long-term memory modules)
Multi-Agent First-class: subagent, workflow, jobs Limited Limited Strength: Built-in Debate, Concurrent, Handoffs workflows
Visualization/Engineering acp + documented subsystems Terminal UI Terminal UI Studio + Tracing + OpenJudge evaluation + RAG + TTS
Sandbox First-class: sandbox (bwrap/Landlock/Seatbelt) Relies on shell restrictions Relies on sandbox environment Runtime sandbox
Automation Protocol ACP server built-in None (via CLI/hooks) None A2A (Agent-to-Agent)
Source Open ✅ Full repository readable, modifiable, contributable ✅ (Apache-2.0-like open source)
Target Audience Platform/product engineers, self-developed Agent teams Terminal developers Terminal developers Algorithm/application developers, multi-agent researchers

12.2 Core Advantages of DeepSeek Harness

  1. Truly "Composable" Rather Than "Configurable"
    Claude Code / Codex lets you configure existing behaviors; Harness lets you rewrite behaviors—even the agent main loop, file access policies, and permission models can be swapped with your own plugins. This is the essential difference between "framework vs product".
  2. Model-Agnostic Capability Layer
    dsh-llm abstracts the LLM into a Service, with DeepSeek being just one provider. Theoretically, swapping providers does not change upper-layer tools and loops. This aligns with AgentScope's "multi-model agnostic" philosophy, but Harness makes this replacement declarative, configuration-driven, and hot-swappable through Cordis's inject/effect, which is more thorough than AgentScope swapping class instances in code.
  3. Hook Interoperability
    Through the hooks-claude-code / hooks-codex bridge packages, your existing Claude Code / Codex hooks.json can run directly on Harness. Migration cost is extremely low, and native extension points are "typed interception points", stronger than shell hooks. This is cross-ecosystem compatibility that AgentScope completely lacks.
  4. Sessions are First-Class Citizens
    session + session-query provide persistence, projection, lineage, semantic filtering, SQLite full-text search—suitable for long-term memory and knowledge-base applications. AgentScope also has Memory/long-term memory modules, but Harness elevates "session log is the truth" (model-visible ⟺ logged) to an architectural constraint, ensuring any input delivered to the model can be reconstructed from the session log—extremely valuable for auditing, replay, and compliance.
  5. Extremely Strict Engineering Discipline
    100% coverage gate, declarative cordis-surface, type-equiv doc sync, branded types, explicit boundary validation—making it suitable as a foundation for production-grade products, not a toy. AgentScope's engineering (Studio/evaluation/Tracing) leans more towards "application development experience", while Harness's engineering leans more towards "reliability and maintainability of the framework itself".
  6. Self-Modification Capability
    The extensions package allows the agent to inspect/mount/unmount its own plugins at runtime (i.e., demo:cordis). This is a framework-level plugin self-loading capability—distinct from business-layer dynamic switching (like AgentScope's runtime agent swapping), Harness can add/remove real Cordis plugins and roll back their effects without restarting the process. Claude Code / Codex do not offer such a mechanism.

12.3 Key Differences from AgentScope (Focus)

AgentScope is an open-source agent application development library by Alibaba (mainly Python, 1.0 paper see arXiv:2508.16279), positioned as "developer-centric for building agentic applications". The two are often compared, but have different orientations:

Orientation DeepSeek Harness AgentScope
Paradigm Plugin runtime (Cordis), everything is a plugin, configuration-driven Component library + Pipeline DSL, ReAct paradigm, code-driven
Language Ecosystem TypeScript / Node, naturally suited for frontend/tool/IDE integration Python, naturally suited for algorithm/data/ML researchers
Composition Method Declarative cordis.yml, dependency graph auto-sorted, HMR, service isolation Imperative code assembly of Agent/Pipeline/Workflow
Multi-Agent First-class citizen subagent/workflow/jobs, plugin-based collaboration Strength: Built-in Debate, Concurrent, Routing, Handoffs out-of-the-box workflows
Observability/Evaluation Session logs + ACP protocol + subsystem documentation Strength: Studio visualization, Tracing, OpenJudge evaluator, RAG, TTS, Tuner
Production Landing Framework-level reliability (100% coverage, type boundaries, sandbox) Application-level engineering (sandbox, evaluation, visualization) complete
Model Default DeepSeek, LLM layer replaceable Multi-model (Tongyi/OpenAI/local) out-of-the-box support
runtime sandbox bwrap/Landlock/Seatbelt first-class support runtime sandbox support

One-Sentence Summary of Differences:

12.4 When to Choose Which (Decision Tree)

  1. Terminal developer, wants an out-of-the-box coding assistant → Claude Code / Codex.
  2. Platform/product engineer, self-hosting, embedding in own products, replaceable models/loops/tools, long-term sessions and memory, reuse Claude Code/Codex hooks → DeepSeek Harness.
  3. Algorithm/application developer, Python ecosystem, multi-agent orchestration, visualization and evaluation system → AgentScope.

13. Learning Roadmap and Next Steps

30-Minute Path for Beginners:

  1. Run through Chapter 4 "First Plugin" (5 minutes, no key)
  2. Run through Chapter 10 "Connecting Tools to a Real Agent" (10 minutes, no key)
  3. Read examples/headless-agent/cordis.yml, cross-reference with this article line by line (10 minutes)
  4. Set DEEPSEEK_API_KEY, run pnpm dsh --profile headless "..." (5 minutes)

In-Depth Reading (by docs/):

Advanced Directions: