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-harnessrepository source code and the officialdocs/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", understandcordis.ymlcomposition and HMR, and clearly know its differences from other mainstream frameworks.
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:
- The loop only describes the "driving protocol" and does not mix in specific capabilities. Behaviors like hooks, sandbox, plan mode, retry, subagent, compaction, etc., are all implemented by listening to
agent/*,tools/*,session/*events, rather than modifying the loop code. - This brings the hard constraint that "behavior is on extension points, not in the loop"—as stated in
AGENTS.md: "Plugins, not loop changes: new behavior goes on documented extension points; changingagent-looprequires updating docs/architecture.md." - Underlying Principle: When the loop becomes the sole and stable driver, all variable behaviors are pushed to the event subscription side, so the addition/removal of capabilities = mounting/unmounting of plugins, completely decoupled from the main loop.
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):
- Service Definition: A Cordis
Servicethat ownsctx.<key>and a vocabulary type, which is an abstract class or a concrete registry (likeShellExecutor,WebRuntime), definitely not a TypeScriptinterface—because it must be mounted as a real service. - Service Provider: One or more implementations, such as
dsh-shell-local/dsh-shell-pwsh. - Consumer: A plugin that injects the service and exposes tools to the model, such as
dsh-tool-bash.
Design Motivation:
- Roles evolve independently: When only the provider needs to change (local → sandbox → E2B), the definition and consumer code do not need to change at all. This confines "change" to the narrowest boundary.
- Take shell as an example:
dsh-shell(definition) →dsh-shell-local/dsh-shell-pwsh(provider,disabledby platform) +dsh-bash-sandbox(sandbox policy). The same applies to LLM:dsh-llm(definition) →dsh-llm-deepseek(native) /dsh-llm-pi-ai(multi-provider twin). - Swappable capability: A seam is a "complete capability", not a single role—the documentation specifically emphasizes "reserve the term for that meaning", because mistaking a role for a capability would cause consumers to directly depend on the implementation, destroying replaceability.
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:
- Plugins do not hold the "responsibility for tearing down" their own resources. Any registration (
ctx.tools.register,ctx.on, child plugins, service instances) is attached to the plugin that calls it and is automatically revoked when the plugin is unloaded. ctx.plugin(child)allows one plugin to mount another as a "child", and parent and child are disposed together, recursively unloaded.- Underlying Principle: Resource ownership = plugin lifecycle, not manual
ifbranches. This mechanically eliminates resource leaks like "forgetting to remove listeners/clear timers"—docs/defensive-patterns.mdlists "Dispose must reach quiescence" as the top defect class rule: teardown must asynchronously await until truly quiescent, not just send a kill.
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:
- Config-replaceable services: Unload
dsh-shell-local, mount anothershellprovider, and all plugins withinject: ['shell']automatically restart using the new implementation—this is the physical basis of "framework-level hot-swapping". - Order-independent: The order of plugin lines in
cordis.ymldoes not affect correctness, only the order of readiness. After completely removing a service, the dependent party remains PENDING, neither crashing nor running halfway. - Underlying Principle: The dependency graph is satisfied dynamically at runtime, not statically bound at build time, so composition itself is data (YAML), not code.
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:
- Any input delivered to the model (tool results, system prompt slices, variables) must correspond to a session event, making the session log the source of truth—replayable, auditable, forkable, resumable.
- Sessions are first-class citizens:
session(JSONL/SQLite persistence, projection, lineage),session-query(SQLite full-text search),compaction(compression + tool result pruning) together support long-term memory and compliance. - Underlying Principle: Elevating "reconstructability" to an architectural invariant, rather than relying on developer consciousness. This makes debugging an incorrect answer = replaying that session event stream, rather than guessing what the model "saw" at the time.
Idea 6: Explicit Over Implicit, Fail Loudly on Error, Never Silently
Multiple rules from AGENTS.md:
- "Misconfiguration fails loud at load when self-contained, otherwise at the earliest resolvable point; never silently skip a missing referent."
- "No hardcoded tunables in plugins: deployment-varying choices are validated
Configfields changeable from cordis.yml." - Use
Branded<B>branded types for opaque ids across boundaries, not barestring; only perform runtime validation at validation boundaries (config, model/tool JSON, files, workers, processes, threads), trust TypeScript for in-process type boundaries.
Design Motivation:
- Deployability comes from "variables are validatable Config", not hidden defaults like
?? defaultin code (those are fixed for protocol constants/security invariants). - Diagnosability comes from "fail loudly on missing references", avoiding "plugin doesn't react and you don't know why" (see Chapter 9 on the diagnoser).
- Underlying Principle: Separate the two types of changes—"deployment differences" and "security invariants"—the former goes into Config and is schema-validated, the latter is hardcoded and cannot be bypassed by configuration.
Idea 7: Security is Layered, Not Single-Point
docs/defensive-patterns.md provides specific rules:
- Generated commands receive a sanitized environment (discarding
*KEY*/*SECRET*/*TOKEN*/*PASSWORD*), preventing harness credentials from leaking into output or spill files. - Temp/spill files use private (0700) directories, random names, exclusive owner-only open (
'wx',0o600), avoiding symlink races and leaks caused by predictable paths. - Sandbox is a first-class capability:
sandbox(bwrap/Landlock/Seatbelt), execution and file system access can be wrapped with sandbox policies.
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
1.3.2 Multiple Operation Modes
For different usage scenarios, DeepSeek Harness provides four modes, each loading a different set of plugins by default:
- Standard Mode: Provides a complete set of tools;
- PTC Mode: Programmatic Tool Calling, where a piece of code generated by the model combines multiple rounds of tool calls;
- Minimal Mode: Only retains one shell tool and one file editing tool, used for model benchmarking in minimal environments;
- Creative Mode: Can inspect the current runtime, experiment with Cordis plugins in memory, and compose and create new modes based on this.
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:
- Tools are plugins (
dsh-tools) - Large language model adapters are plugins (
dsh-llm+ DeepSeek provider) - File system access is a plugin (
dsh-fs) - shell / subprocess / terminal are plugins (
dsh-shell/dsh-subprocess/dsh-terminal) - Even the agent main loop (agent-loop) itself is a replaceable plugin (
dsh-agent-loop)
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).
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"
- The launcher =
node --import tsx ../../vendor/cordis/bin.js, which only creates the rootContextand mounts the Loader. - The Loader reads
cordis.ymlor--profilepatch layers (dsh-bundle). The base bundle declares all default plugins inpackages/bundle/base/cordis.patch.ymlwith 45+idlines, and line order is irrelevant (activation is driven by "service availability"). - Each line is a plugin;
id(e.g.,agent-loop,tools,llm-deepseek) is a stable identifier, and subsequent patch layers override it byid.
2. agent-loop is the Only "Concrete Loop"
- According to
dsh-agent-loop/README.md: Only this one package in the entire harness contains specific loop logic, all others are abstract services or extension point plugins. - It injects and depends on 5 interface services:
agents,sessions,llm,tools,systemPrompt(dashed lines in the diagram). These 5 are all services onctx, and specific providers can be hot-swapped. - All behaviors beyond "call model → run tools → repeat" (hooks, sandbox, plan, retry, subagent, compaction) are implemented by listening to
agent/*,tools/*,session/*events—this is the event bus in the diagram.
3. Tools are "Registered", Not "Hardcoded"
- Execution/orchestration plugins like
bash,fs,web,subagent,workflow,todo,skillregister tools into thedsh-toolsregistry viactx.tools.register(...)(effect), and are then consumed by agent-loop in events liketools/result. The two plugins are unaware of each other's existence.
4. Replaceable Provider Three-Role Model
- Taking shell as an example:
dsh-shell(definition) →dsh-shell-local/dsh-shell-pwsh(provider,disabledby platform) +dsh-bash-sandbox(sandbox policy). The same applies to LLM:dsh-llm(definition) →dsh-llm-deepseek(native) /dsh-llm-pi-ai(multi-provider twin).
5. Self-Modification and Interoperability
extensionsallows the agent to load/unload plugins at runtime;hooks-claude-code/hooks-codexbridge external Hooks;acpexposes an automation protocol server. These are outside the base bundle and are overlaid as needed.
The 5 dashed injection lines of
dsh-agent-loop, the 7+ tool registrations ofdsh-tools, and the--profilepatch layer assembly in the diagram all come directly from the source code facts inpackages/bundle/base/cordis.patch.ymlandpackages/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:
- ESM and
import: Code usesimport { x } from 'pkg'to introduce dependencies; all relative imports in this article carry the.tssuffix (e.g.,'./hello.ts'), which is a convention of the Cordis loader. - Workspace Package Names:
@deepseek-ai/cordis,@deepseek-ai/dsh-tools, etc., are internal npm package names within the repository (resolved by pnpm workspace), not downloaded from the network.import type { Context } from '@deepseek-ai/cordis'is just fetching types from Cordis. cordis.ymlis a YAML List: Each- name: ...is a plugin item; indentation uses two spaces, do not mix with Tabs.- What is
ctx: Thectxthat runs through the entire text is Cordis's shared context, through which all plugins register capabilities and listen to events. You can think of it as "the main patch panel for the entire runtime".
3.2 Environment Prerequisites
Prerequisites (see docs/development.md for details):
- Node.js 22.19+ or 24+ (CI covers 22.19 / 24 / 26)
- pnpm (enable Corepack:
corepack enable), repository locked to[email protected] - Git 2.26+
- Optional: DeepSeek API Key (
DEEPSEEK_API_KEY), only needed when actually running models; Chapters 3 to 10 of this tutorial (including HMR and tool pipelines) are fully runnable without a key
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')
}
- Plugins are mounted by the loader through the named export
applyfunction. ctxis the Cordis context, through which plugins register all contributions.nameis an optional display name used for diagnostic information.
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)
- If a plugin's
applythrows an error → the process crashes directly and reports the error (will not silently skip). - If the module path/package name in
cordis.ymlis misspelled (resolution fails) → Cordis only reports via logger, will not crash. When a new plugin "doesn't react", check the spelling first.
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:
ctx.plugin(heartbeat)mounts a function from code as a plugin, exactly the same as what the YAML loader does for each configuration item. The call returns a fiber—the runtime handle of the loaded plugin instance.- The effect body runs during loading, and the returned disposer runs during unloading. For resources whose lifecycle matches the plugin's, you never need to manually call the disposer.
fiber.dispose()waits for all cleanup of that plugin (including async disposers) to complete before finishing, and recursively unloads the child plugins it mounted.
Fiber State Machine
Each loaded plugin instance has a fiber, transitioning between the following states:
PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED
↘ FAILED
- PENDING: Declared, but required services are not yet available (see Chapter 6).
- LOADING / ACTIVE:
applyis running / has completed. - FAILED:
applyor config validation threw an exception. - UNLOADING / DISPOSED: Disposer is running / has been torn down.
Operations That Are Already Effects (You Rarely Need to Write ctx.effect() Manually)
ctx.on(event, listener): Listener is automatically removed when the plugin is unloaded.ctx.plugin(child): Child plugin is disposed together with the parent plugin.- The return disposers of service registrations, harness registries (like
ctx.tools.register(...)) are all attached to the calling plugin and automatically revoked.
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:
- Runtime:
super(ctx, 'greeter')registers the instance with the namegreeter,ctx.greeteris accessible everywhere; registration is an effect, removed on unload. - Compile-time:
declare moduleuses TS declaration merging to addgreeterto theContextinterface, giving consumers type safety (runtime still works without this declaration, but loses types).
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;greeterininject: ['greeter']is the service name (registered bysuper(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, soctx.emit/ctx.oncheck event names and parameter types at compile time. Forgetting the declaration merging, the event is still a validstringevent, 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-tutorialdirectory created in Chapter 3, place all files here, and the run command is stillnode --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:
defineToolconvertsparametersinto JSON Schema for the model, and validates parameters beforeexecute.- The logging plugin triggers first:
tools/resultis emitted during the materialization of the result, earlier than the fulfillment ofexecute's promise. The two plugins are unaware of each other—they are connected by the registry service and events. - If the tool plugin lacks a
systemPromptprovider in the composition, it will remain PENDING (missing dependency), which is exactly the manifestation of theinjectmechanism.
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)
Idea 1 (Everything is a plugin, the loop only drives, does not implement)
Driver(dsh-agent-loop) only does the skeleton of "take input → emit events → wait for results". Hooks, sandbox, plan, retry, subagent, compaction do not appear in the loop body, but are subscribed by external plugins as waterfall / serial events likeagent/pre-step,agent/request,agent/request-error,agent/turn-stopping.- For example,
dsh-compaction-basicusesagent/pre-stepfor pressure checks before request construction, andagent/request-erroronly triggers pruning on context overflow—these are behaviors "hung" on loop events, not branches inside the loop. This is exactly "Plugins, not loop changes".
Idea 5 (Model-visible ⟺ logged, log is the truth)
- Every single thing delivered to the model is recorded as a session event:
system-prompt/assemble(prompt slices),agent/request(model request),llm/stream→assistant/chunk*(streaming output),assistant/message(a successful call),tool/call/tool/result(tool calls and results). - The durable copies of these events are all on
session/event(Session-->>SDK: session/event ...), whileagent/*is just the "live coordination API" (queue/state/intercept/redirect/continue/error). - Therefore, an incorrect answer can be fully replayed: reconstruct from the
session/eventstream what prompt the model saw at the time, which tools were called, what results were obtained—no guessing needed. This is the physical landing point of "log is the truth".
- Every single thing delivered to the model is recorded as a session event:
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):
- Core:
core(session, prompt, tools, agent, agent-loop),api,typert,sdk - LLM:
llm(abstract + DeepSeek provider) - Execution:
shell,subprocess,terminal,code-runtime,sandbox(bwrap/Landlock/Seatbelt) - Tools:
fs,lsp,web,skill,subagent,workflow,todo,plan - Session/Persistence:
session,session-query,compaction,storage,attachment - Human-Machine Collaboration:
interaction(approval/permissions/ask-user),hooks,acp - Self-Modification:
extensions(agent can inspect/mount its own plugins at runtime) - Composition Distribution:
bundle(dsh --profilepatch layers),preset
Design Philosophy: Maintainable dependencies are preferred over handwritten ones; use
Branded<B>branded types for cross-boundary ids (not barestring); 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
- 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". - Model-Agnostic Capability Layer
dsh-llmabstracts 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'sinject/effect, which is more thorough than AgentScope swapping class instances in code. - Hook Interoperability
Through thehooks-claude-code/hooks-codexbridge packages, your existing Claude Code / Codexhooks.jsoncan 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. - Sessions are First-Class Citizens
session+session-queryprovide 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. - Extremely Strict Engineering Discipline
100% coverage gate, declarativecordis-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". - Self-Modification Capability
Theextensionspackage 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:
- Want to build your own Agent platform/product foundation, need replaceable models and loops, long-term sessions and auditing, smooth migration of Claude Code/Codex hooks → Choose DeepSeek Harness (TypeScript ecosystem, plugin-based, configuration-driven).
- Want to quickly build multi-agent applications in Python, need ready-made debate/concurrency/routing workflows, Studio visualization and evaluation system → Choose AgentScope (application development experience, ML ecosystem).
- Want to give end-users an out-of-the-box coding assistant → Choose Claude Code / Codex (product form).
12.4 When to Choose Which (Decision Tree)
- Terminal developer, wants an out-of-the-box coding assistant → Claude Code / Codex.
- 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.
- Algorithm/application developer, Python ecosystem, multi-agent orchestration, visualization and evaluation system → AgentScope.
13. Learning Roadmap and Next Steps
30-Minute Path for Beginners:
- Run through Chapter 4 "First Plugin" (5 minutes, no key)
- Run through Chapter 10 "Connecting Tools to a Real Agent" (10 minutes, no key)
- Read
examples/headless-agent/cordis.yml, cross-reference with this article line by line (10 minutes) - Set
DEEPSEEK_API_KEY, runpnpm dsh --profile headless "..."(5 minutes)
In-Depth Reading (by docs/):
docs/cordis-tutorial/: 7-chapter complete Cordis hands-on tutorial (this article is its condensed and expanded version)docs/cordis-primer.md: Concept quick referencedocs/architecture.md: System map (must-read before modifyingpackages/)docs/capability-seams.md: Three-layer capability designdocs/user/: For Harness plugin development (develop/basic/tool.md, etc.)docs/cookbook/adding-a-tool.md: Tool UI presentation designpackages/*/README.md: Purpose, API, extension points of each package
Advanced Directions:
- Write a custom
Service Definition + Provider(refer todsh-shell) - Use
dsh-bundleto create your own--profilepatch layer - Use
hooks-claude-codeto bridge existing hooks - Use
extensionsto implement agent self-modification - Run
pnpm run doc-syncto regenerate all architecture/lifecycle diagrams (includingdocs/agent-lifecycle.mdreferenced in this article, produced byscripts/gen-doc-graphs.ts) - Compare with the AgentScope paper to appreciate the architectural trade-offs between "configuration-driven plugin runtime" vs "imperative component library"