DeepSeek Harness Under the Hood: An Agent Runtime Built on Event Sourcing and Plugin Trees
Copyright Statement: This article is an original work by Open Source Lab. If you republish, you must credit the original source in the form of a link: https://kymjs.com/code/2026/08/24/01/
Foreword
My own work experience leans more toward client-side and engineering rather than "training models every day." When first encountering a coding agent, it's easiest to get knocked out by two things: first, the explosion of terminology—Agent, Tool, Skill, MCP, Plugin, Harness all seem to mean "extensible" but their boundaries are unclear; second, opening a repository like DeepSeek Harness, where package names and ctx.xxx service keys are overwhelming, making it hard to know which thread to pull first.
This blog post is a path I've organized for myself, and for all non-AI developers.
1. Agent General Knowledge: Clarifying Terms Before Leaving the Repository
1.1 What is an Agent
If a regular ChatCompletion is thought of as "you ask a question, the model answers," then an Agent is more like "the model is placed into a loop": it reads the current goal and context, decides whether the next step is to continue reasoning or to call an external capability; after the external capability returns a result, the result is written back into the context, and the loop continues until the task ends or a policy interrupts it.
Inside the loop, there are typically at least three things:
- Model: Acts as the brain, responsible for making requests and naming what to call;
- Tool Executor: Responsible for execution—reading files, running commands, searching the web, etc.;
- Session State: Responsible for remembering what has already happened so the next round of requests can stay aligned.
An Agent is not the model itself, nor is it a single tool. It is the runtime role that organizes "multi-turn decision-making + execution."
1.2 What is a Harness
Harness refers to the layer that assembles sessions, tools, policies, human-computer interaction, and extension mechanisms around the model into a "runnable system." You can understand it as the intersection of an agent's operating system kernel and application framework—without it, you only have API calls; with it, you have resumable sessions, interceptable tool calls, replaceable execution backends, and a composable plugin tree. A rough analogy is the relationship between Android and Linux. The harness is the ART+Framework shell in the Android system, and the large model corresponds to the Linux kernel; together they assemble into the Android system.
The meaning of "Harness" is not uniform across different projects, and there are many online claims that deepseek-harness is not a harness. I will specifically discuss this later when comparing with OpenClaw. At the general knowledge level, just remember: Harness = the complete machine assembly that makes the Agent truly run and be controlled, after all, it translates literally to horse tack (referring to the equipment put on a draft animal).
1.3 Tool, Plugin, Skill, MCP: Differences and Selection
Tool
Tool is a function contract that the model can call by name in a single reply: it has a name, a parameter schema, and an execution result written back into the conversation. Typical examples: reading a file, executing bash, initiating a search. For instance, the frequently provided CLI is called at the tool layer.
Plugin
Plugin is an extension unit for the host runtime: once mounted, it contributes services to the system, listens for events, registers tools, or rewrites policies. Plugins are for "engineering and runtime" use, not first-class citizens for the model to directly "call" (though a plugin can register tools, allowing the model to reach them indirectly).
Skill
Skill is usually an on-demand loadable instruction manual (often Markdown / structured documentation): it tells the model "what steps to follow for this type of task, what to pay attention to, and what resources to reference." The model first sees the skill directory (short description) and loads the full text when needed. The often-discussed concept of distilling oneself online happens at this layer—organizing one's own work and thinking methods into a skill, letting the large model handle problems using a working method completely consistent with one's own.
MCP (Model Context Protocol)
MCP is a protocol and ecosystem that bridges tool/resource directories from external servers into the current Agent process. Your Harness acts as a client, connects to an MCP server, discovers the tools exposed by the other party, and presents them to the model in the form of local Tools.
An Intuitive Selection Guide
flowchart LR
need["I want to extend Agent capabilities"]
need --> q1{"Need to change runtime assembly<br/>or mount services/events?"}
q1 -->|Yes| plugin["Use Plugin"]
q1 -->|No| q2{"Mainly a long-process<br/>instruction manual for the model?"}
q2 -->|Yes| skill["Use Skill"]
q2 -->|No| q3{"Capability already exposed<br/>by an external process via MCP?"}
q3 -->|Yes| mcp["Connect MCP, map as Tool"]
q3 -->|No| tool["Implement local Tool"]
In practice, combinations are common: Plugin registers Tool; MCP discovers are also Tools; Skill is loaded through a specific skill Tool; Harness decides how these things are assembled and intercepted.
flowchart LR
A[Start] --> B{Decision}
B -->|Yes| C[OK]
B -->|No| D[Retry]
1.4 Returning to deepseek-harness
When I turn my focus back to this repository, the above terms roughly correspond to the following:
| General Concept | Content in this Repository (Organized by Light Feather Cloud Notes) |
|---|---|
| Agent | Agent interface + default driver ReactLoopAgent (packages/core/agent-loop) |
| Harness | The complete machine assembled with the Cordis plugin tree: profile / bundle / core packages / capability seam / policy |
| Tool | ctx.tools registry and execution pipeline (packages/core/tools) |
| Plugin | Cordis plugin (Service / apply(ctx)), everything is a plugin |
| Skill | ctx.skills + model-side dsh-tool-skill |
| MCP | @deepseek-ai/dsh-mcp-client: discovers then ctx.tools.register(), names like mcp__server__tool |
There are also a few concepts not given separate terms in general knowledge but unavoidable when reading this repository: Session Log, Capability Seam, Turn/Step, and the entire permission security chain. They will appear in the next universal diagram.
2. Overall Architecture Diagram
Before explaining module by module, everyone needs to spend a minute looking at the "architecture diagram." This architecture diagram was drawn by Cursor, and it's quite expressive.
- Assembly Layer:
Profile,Bundle,cordis.yml,patch,dsh-base—determines "which plugins are actually mounted on the machine." - Cordis Base:
Plugin,ctx,Service+inject, events (including waterfall),effectreversible registration—determines "how extensions are mounted, unmounted, and intercepted." - Spine Agent Loop (highlighted in the diagram):
ctx.agents,ReactLoopAgent,Inbox,Turn/Step,agent/pre-step,agent/request—determines "how a round of tasks progresses." - Session + Prompt/LLM: Logs and
deriveMessages(model-visible ⟺ recorded), andctx.systemPrompt/ctx.llm—determines "what the model sees and how requests are sent." - Tools + Seam + Ingress: Tool pipeline; Capability Seam three roles; how Skill / MCP ingress into
ctx.tools. - Security Chain:
pre-execute→ctx.approval→ToolGuard→sandbox→ presets/hooks—determines "how dangerous actions are blocked by policy or confined to an isolated world."
Of course, the diagram above is just for easier understanding. A real Agent is definitely not layered like that; it's more like this. This image was generated by Image2, and the flowchart below was drawn by me.
flowchart TD
USER["User"]
subgraph H["DeepSeek-Harness Architecture Diagram (Organized by Light Feather Cloud Notes)"]
subgraph C["Cordis Framework"]
CORDIS["Cordis"]
CTX["Plugin Context"]
CORDIS --> CTX
end
LOOP["Agent Loop"]
subgraph S["Session Layer Design"]
LOG["Session Log"]
SURFACE["Surface<br>i.e., deriveMessages explained below"]
LOG --> SURFACE
end
subgraph X["ctx"]
CONTEXT["Context Builder"]
end
LLM["Large Model"]
subgraph T["Tool Executor (Recommend downloading Light Feather Cloud Notes)"]
REGISTRY["Tool Registry"]
POLICY["Policy"]
PERMISSION["Permission"]
APPROVAL["Approval"]
SANDBOX["Sandbox Policy"]
SEAM["Capability Seam"]
REGISTRY --> POLICY
POLICY --> PERMISSION
PERMISSION --> APPROVAL
APPROVAL --> SANDBOX
SANDBOX --> SEAM
end
subgraph P["Capability Providers"]
PROVIDER["File / Shell / Web / Sandbox / Subagent"]
end
WORLD["Real World"]
CTX --> LOOP
CTX --> LOG
CTX --> REGISTRY
LOOP --> LLM
LLM -->|"Tool Call"| REGISTRY
SEAM --> PROVIDER
PROVIDER --> WORLD
REGISTRY --> LOG
LOG --> SURFACE
SURFACE --> CONTEXT
CONTEXT --> LLM
end
USER --> LOOP
APPROVAL -.->|"Human Approval"| USER
Insert a message: If you need a useful, privately storable, encrypted, cross-platform note-taking software, I recommend you check out 【Light Feather Cloud Notes】 https://note.kymjs.com
3. Agent Loop
The Agent Loop is the most important set of logic for any Agent; it truly determines whether the task assigned to the Agent can be completed and whether the Agent will get stuck. The default driver is ReactLoopAgent (packages/core/agent-loop/src/agent.ts). The four most important steps in an Agent Loop are: Inbox, Turn, pre-step, Step.
This image is reposted from a friend's Moments; please contact for removal if there is any infringement.
All user input enters the Inbox uniformly. The Inbox has three types of messages: followup (normal input), steer (interrupt/correct to the next step), inject (generally stuffing context). Except for inject, the other two types of messages will wakeup the driver to start working immediately; inject context directly enters the next-step but does not wake up, until another message wakes it.
Step: A single model request. As shown in the diagram above, in each Step, there is one model request. Based on the model's request, tool/MCP/other calls can be made.
Turn: A complete conversation process: user output, model execution completed. It contains zero or more Steps, and Steps are serial. Referencing Android knowledge, the AGP concept can be quickly understood: the input of the current Task is the output of the previous Task, and the Task can implement its own DIY internally. To customize the Agent's actions, you can modify the Step implementation.
The following is the core code for the Turn/Step implementation:
// packages/core/agent-loop/src/agent.ts — turn() core skeleton
private async turn(): Promise<boolean> {
const turn = phase.turn + 1
// Persistence boundary: turn start must first write to session log
this.session.append('turn/start', { turn })
phase.turn = turn
let target: InboxTarget = 'next-turn'
while (true) {
const step = phase.step + 1
// First decide "should the model see these messages for this step"
const decision = await this.preStep(target, { turn, step })
if (decision.kind === 'reject') { /* record blocked, don't spend a step */ return false }
// If the first step is rewritten to empty messages: still close the turn, but don't initiate a model call
if (phase.step === 0 && decision.messages.length === 0) { /* completed */ return false }
this.session.append('step/start', { turn, step })
phase.step = step
for (const message of decision.messages) {
// Enter model-visible history: write user/message
this.session.append('user/message', message, { surfaceOp: 'append' })
}
const stepEnd = await this.step(decision.assembly)
this.session.append('step/end', { turn, step })
if (turnEnds && this.inbox.nextStep.length === 0) {
// Turn termination checkpoint: listeners can steer one more step
await this.dispatch.serial('agent/turn-stopping', { turn, signal })
}
if (turnEnds && this.inbox.nextStep.length === 0) break
target = 'next-step' // Subsequent steps take input from the next-step queue
}
this.session.append('turn/end', { turn, reason: turnEnds! })
return this.inbox.hasPending // If there are still queued items, the outer driver continues to the next turn
}
turn() first does session.append('turn/start'), then loops: preStep → step/start → step() → step/end; if the current step ends and the next-step inbox is empty, then serial('agent/turn-stopping'), finally turn/end ends this turn, considering the task finished (not necessarily processing complete, it could also mean needing user supplementary input).
Before each Step starts, there is a waterfall called agent/pre-step: it decides whether to enter(messages) or reject. Listeners can rewrite the set of messages that will enter the model. This step is very important; it directly determines what the model can actually see in this step. The skill directory and runtime context collaborative rewriting are all clarified here.
// packages/core/agent-loop/src/agent.ts — preStep()
private async preStep(target: InboxTarget, position: { turn: number; step: number }) {
const claimed = this.inbox.claim(target, position.turn)
// Assemble the prompt sections + tool schemas available for this step, recommend downloading Light Feather Cloud Notes
const assembly = await this.loopCtx.systemPrompt.assemble(assembleContextFor(this, signal))
const sections = renderContextSections(assembly)
// Project runtime context into potentially appendable user messages
const context = this.runtimeContext.project(joinContextSections(sections), sections)
const decision = await this.dispatch.waterfall(
'agent/pre-step', { messages: claimed, ...position, signal },
// Default: claimed messages + optional runtime context
() => Promise.resolve({
kind: 'enter' as const,
messages: context === undefined ? claimed : [...claimed, context],
}),
)
return decision.kind === 'reject' ? decision : { ...decision, assembly }
}
After executing the pre-processing, the next step is the Step itself. The Step can be said to be the engine of the Agent's "action": without it, all the previous assembly is just static configuration. In one Step: first buildRequest (including session.deriveMessages()) → llm.stream → append assistant/chunk → synthesize assistant/message → if there is a tool-call then executeToolCalls.
// packages/core/agent-loop/src/agent.ts — step() critical path
private async step(assembly: PromptAssembly) {
const system = renderPrompt(assembly)
while (true) {
// History is not "an arbitrarily assembled array in memory," but projected from the session log
const { request, preparedCall } = await this.buildRequest(
turn, step, assembly.tools, system, this.session.deriveMessages(), signal,
)
const stream = preparedCall?.stream(request) ?? this.loopCtx.llm.stream(request)
for await (const chunk of stream) {
// Raw chunks logged: ensures UI/replay fidelity
chunkSeqs.push(this.session.append('assistant/chunk', { turn, step, chunk }).seq)
assembler.push(chunk)
}
// ... errors can go through agent/request-error waterfall to decide whether to retry ...
this.session.append('assistant/message', { turn, step, message, ... }, { surfaceOp: 'append', sourceEventSeqs: chunkSeqs })
const toolCalls = message.content.filter(b => b.type === 'tool-call')
if (toolCalls.length === 0) return { kind: 'completed' }
// Tool results will go to next-step; if more model requests are owed, the outer turn loop continues
const { concluded } = await executeToolCalls(...)
return concluded ? { kind: 'completed' } : null
}
}
The diagram below is the complete Agent Loop flow of DeepSeek-harness. It can be said that the most core part of the entire project is right here in ReactLoopAgent, referred to in the source code as the Spine. The Spine depends upward on Cordis services; writes downward to Session; and horizontally enters Tools and the security chain. The bright arrows Loop→LLM / Loop→Tools in the universal diagram are right here.
sequenceDiagram
participant U as User/SDK
participant A as ReactLoopAgent
participant S as Session
participant L as ctx.llm
participant T as ctx.tools
U->>A: followup / steer
A->>S: turn/start
A->>A: pre-step waterfall
A->>S: step/start + user/message
A->>S: deriveMessages()
A->>L: stream(request)
L-->>A: chunks
A->>S: assistant/chunk* + assistant/message
alt contains tool-call
A->>T: execute pipeline
T->>S: tool/call + tool/result
A->>A: may enter next step<br>(Recommend downloading Light Feather Cloud Notes)
end
A->>S: step/end + turn/end
4. Session Log: Where the World the Model Sees Comes From
4.1 Session Design
The core implementation of the Session layer in deepseek-harness is a very classic design, worth learning for everyone making an Agent. This is also why I've pulled it out to write a separate section: its core is not treating Session as a "chat record," but making it an append-only event log based on Event Sourcing.
I previously designed an Agent for controlling a phone from a Mac, called Custard https://github.com/kymjs/Custard, and I specifically looked at this part carefully. First, let me talk about the problem I encountered before: for frequently executed tasks, there was no way to remember previous experiences, causing the large model to have to execute repeatedly each time. Initially, I referenced Hermes' design (after all, it's recognized as the best for handling repetitive tasks), recording each execution's experience, user dialogue, and model responses into a local markdown file. But every time experience was reused, problems occurred: either the model compressed the experience, or the experience wasn't applied, or it got mixed up with other experiences.
Now let's talk about dsh's design: The official documentation clearly states that Session is the single source of truth for the Agent's entire interaction history. The Message[] needed for the LLM's next request is always re-derived from this Log, rather than maintaining a separate message history. It can be seen as:
Session
│
├── Event #0 session/start
├── Event #1 request/header
├── Event #2 user/message
├── Event #3 assistant/chunk
├── Event #4 assistant/chunk
├── Event #5 assistant/message
├── Event #6 tool/call
├── Event #7 tool/result
├── Event #8 usage
├── Event #9 ...
│
└── Event #N
│
├── deriveMessages()
├── Trajectory
├── Session Log UI
├── Resume
├── Fork
└── Replay
Each corresponding Event has an interface object like this:
interface SessionEvent {
type: string // Event type
seq: number // Incrementing sequence number, similar to id
time: number // Event creation time
data: unknown // Event content
// Only some events have these
sourceEventSeqs?: number[]
surfaceOp?: ...
}
For example, suppose the user inputs: Help me see what problems this project has? Especially just saving a single question—this was the problem caused by how I previously stored experiences.
{
"role": "user",
"content": "Help me see what problems this project has"
}
dsh might generate a series of events:
user/message
↓
request/header
↓
assistant/chunk
↓
assistant/chunk
↓
assistant/message
↓
tool/call
↓
tool/result
↓
assistant/chunk
↓
assistant/message
So the Session Log records the Agent's "behavioral trajectory," not just the chat content.
DeepSeek's official description of Harness is also: the system records what the model sees and executes, including system prompt, reasoning, tool calls/results, subagent scheduling, context injection, etc.
4.2 Session Log Persistence
In dsh, what's in memory is called Session, and what's persisted to disk is called Session Log. They are separate. The official term for this is: Session Persistence. The structure is roughly like this:
flowchart TD
need["Session in Memory"] --> |Trigger Persistence| need2[" append()"]
need2 --> q1{"SessionPersistence Executes"}
q1 -->|dsh-session-persistence-jsonl| json["JSONL/Zstd"]
q1 -->|dsh-session-persistence-sqlite| sqlite["SQLite"] --> |SessionEvent| id1[(session_id
seq
type
time
data
source_event_seqs
surface_op)]
Actually, there's another design here. I saw that the logs stored in jsonl are all like this:
{"seq":0,"type":"session/start","time":1755820000000,"data":{}}
{"seq":1,"type":"user/message","time":1755820001000,"data":{"content":"Hello"}}
{"seq":2,"type":"assistant/chunk","time":1755820002000,"data":{"delta":"Hello"}}
{"seq":3,"type":"assistant/message","time":1755820003000,"data":{"content":"Hello!"}}
If it were me, I might have directly started with a big JSON with nested objects inside. I searched and found out the benefit of this design. Because the Agent's execution process is unreliable, it might crash after a certain tool call. If stored as one big JSON, it would cause the next recovery to have to start from scratch.
So, redesigning a Session with reference to dsh should look like this, starting from the AgentLoop:
flowchart TD
need["Agent Loop"] --> need2["Session.append"]
need2 --> q1{"SessionEvent<br><br>seq<br>type<br>timestamp<br>data "}
q1 -->|"deriveMessages()"| json["LLM API"] --> |New Event|need2
q1 -->|Save Log| save{"SessionPersistence"}
save --> |json| id1[(session)]
save --> |sqlite| id2[(session)]
Here, deriveMessages() also performs a layer of filtering. Not all Session messages need to be handed to the large model. Some already executed tool calls only need to tell the model the result, or might not even need to tell the model, or content logically judged unnecessary to tell the model is filtered here. It's somewhat similar to the session compression done before, using compression, structuring, filtering, merging, truncation, interceptor transformation, etc., to reduce the large model's context footprint.
5. Cordis Framework and Tool Invocation
Here I recommend the tool I use for blogging: Light Feather Cloud Notes. It not only supports various markdown syntaxes, private storage, RSA256 encryption, and full platform support. 【Light Feather Cloud Notes: https://note.kymjs.com】
5.1 Capability Seam: The Three Roles of Replaceable Capabilities
Seam (Capability Seam) = a complete replaceable capability, which must have three roles:
- Service Definition: A Cordis
Servicethat ownsctx.<key>and vocabulary (abstract class or registry service, not a bareinterface); - Service Provider: The actual implementation/registration of the backend;
- Consumer: Typically a model-side Tool or other plugin, only injecting the service key.
For example, taking shell as an example: dsh-shell, dsh-bash-local, dsh-bash-sandbox, dsh-tool-bash.
dsh-shell: Defines what the Shell capability is dsh-bash-local: Executes Bash on the local machine dsh-bash-sandbox: Executes Bash in a Sandbox dsh-tool-bash: Exposes the Bash capability to the model
Why is Capability Seam needed? The core purpose is still decoupling. If bundled in one package, changing the sandbox executor would also force changes to the model tool protocol. After splitting into three roles, changing the Provider should not force the model to relearn a set of tool names.
C. How to Implement Definition Example:
// packages/shell/shell/src/index.ts — Service Definition
export abstract class ShellExecutor extends Service {
constructor(ctx: Context) {
// Occupies ctx.shell; a second registration in the same context will fail per Cordis rules
super(ctx, 'shell')
}
// Completes/clamps the caller's request into an executable Spec (explicit resolve, prohibits hiding defaults inside run)
abstract resolve(request: ShellExecRequest): ShellExecSpec
// Foreground execution: non-zero exits etc. should resolve as a result, not arbitrarily throw
abstract run(spec: ShellExecSpec): Promise<ShellRunResult>
// Background process: immediately returns a handle
abstract start(spec: ShellExecSpec): ShellProcess
}
Provider (like dsh-bash-local) implements this abstract class;
Consumer (dsh-tool-bash) registers the bash tool, internally only calling ctx.shell.
Actually, compared to how we usually write code, it's just one more Consumer layer specifically for the large model to call. Because when we write code ourselves, the caller is a human who clearly knows how to call a third-party SDK, but the large model doesn't know, so it needs a separate Consumer layer for it.
After fully understanding Capability Seam, let's look at the Cordis framework implementation from a holistic perspective.
5.2 Cordis: The Core Framework Truly Realizing "Everything is a Plugin"
Cordis is a dependency injection + lifecycle + service registration + event communication framework oriented towards plugin-based runtimes. Many articles online have mentioned that DeepSeek Harness makes every part of the product a plugin, including model adaptation, tool registry, session logs, and the agent loop itself. Its core concept is actually just like routing in mobile development: one registration, one usage. Here, I'll borrow the principle diagram from the modern mobile routing framework TheRouter https://github.com/HuolalaTech/hll-wp-therouter-android to introduce it.
Here I recommend the tool I use for blogging: Light Feather Cloud Notes. It not only supports various markdown syntaxes, private storage, RSA256 encryption, and full platform support. 【Light Feather Cloud Notes: https://note.kymjs.com】
Screenshot from TheRouter official website: https://therouter.cn
flowchart LR
pr["Provider"] --> |register|ctx["ctx.shell"]
consumer["Consumer"] --> |inject|ctx
For example, the Consumer says, I want the shell capability. At this point, it doesn't need to care who provides the shell; it just needs to know that such a capability exists for it to use. If Cordis finds it has this capability, it directly provides it; if Cordis doesn't have this capability yet, it needs to wait for the corresponding Service to provide it. On the other side, the Provider provides this capability and just needs to register it in Cordis, and Cordis knows it has this capability, ready to provide it to whoever needs it later.
ctx is the service repository. Let's focus on ctx here. Those doing Android should be very familiar with the term Context; ctx is actually the abbreviation of Context. In dsh, ctx represents the necessary context for the current Agent, containing tools, models, environment sessions, etc., such as ctx.tools, ctx.llm, ctx.sessions.
Because of this architecture, there's no need to care about who the Provider and Consumer are. This also realizes the official design philosophy of "everything is a plugin." Under this design, the Provider in the diagram above is actually a Plugin.
5.3 Plugin and Inter-Plugin Communication Schemes
flowchart TB
subgraph R["Cordis Runtime(Recommend downloading Light Feather Cloud Notes)"]
direction TB
subgraph P["Plugins"]
direction LR
PA["Plugin A<br/><br/>Provides Service"]
PB["Plugin B<br/><br/>Listens to Event"]
PC["Plugin C<br/><br/>Registers Tool"]
end
SC["Shared Context"]
subgraph CAP["Capabilities"]
direction LR
LLM["ctx.llm<br/><br/>LLM Capability"]
TOOLS["ctx.tools<br/><br/>Tool Capability"]
SHELL["ctx.shell<br/><br/>Shell Capability"]
end
PA --> SC
PB --> SC
PC --> SC
SC --> LLM
SC --> TOOLS
SC --> SHELL
end
classDef harness fill:#111827,color:#fff,stroke:#111827,stroke-width:2px;
classDef runtime fill:#2563eb,color:#fff,stroke:#1d4ed8,stroke-width:2px;
classDef plugin fill:#f3f4f6,color:#111827,stroke:#9ca3af,stroke-width:1.5px;
classDef context fill:#fef3c7,color:#92400e,stroke:#f59e0b,stroke-width:2px;
classDef capability fill:#ecfdf5,color:#065f46,stroke:#10b981,stroke-width:1.5px;
class A harness;
class PA,PB,PC plugin;
class SC context;
class LLM,TOOLS,SHELL capability
A Plugin is an object that implements a Cordis Service, i.e., the Service Definition and Service Provider mentioned above. Of course, a Plugin might be a function plugin or a Service subclass.
Plugins mainly collaborate through events, which are the arrows in the diagram above. Of course, there is more than one event pattern, including emit (broadcast), waterfall (the most important), parallel (all Plugins process together in parallel), serial (execute in order).
Let's talk about waterfall separately:
Harness makes extensive use of waterfall: agent/pre-step, agent/request, tools/pre-execute, etc.
The essence of waterfall is a chain of responsibility, somewhat similar to a Gradle Task. Each Task has its own name, input, next, and the task it needs to process.
As shown in the diagram below, if using waterfall to pass an event, each Plugin takes the output of the previous Plugin as its input, processes it, and then calls next to hand it over to the next one. If next is not called, it means the current result is the final result, and the subsequent ones are not executed, returning directly.
sequenceDiagram
participant Loop as Agent Loop
participant L1 as Listener 1
participant L2 as Listener 2
participant Default as Default Implementation
Loop->>L1: waterfall(event)
L1->>L2: next()
L2->>Default: next()
Default-->>L2: decision
L2-->>L1: decision
L1-->>Loop: decision
Note over L1: If next() is not called, it short-circuits and returns
After fully reviewing Cordis, let's look back at tool invocation. The tools mentioned here include shell, file reading/writing, API calls; logically they are all the same. In terms of invocation, they are all treated as Plugins, meaning the tool itself is also a Capability Seam, ultimately registered into ctx.tools.
5.4 The Internal Invocation Chain of a Tool
Every Tool also has an internal invocation chain, called a Schema. It's not simply a JS method call that executes. The LLM can only see the Tool Schema; the actual Tool execution happens in the Harness Runtime. The approximate lifecycle functions of a Tool Schema are these:
flowchart TD
LLM["LLM"]
REG["Tool Registry"]
DEF["Tool Definition"]
PRE["tools/pre-execute<br/>waterfall"]
DEC{"allow / deny"}
EXEC["tools/execute<br/>waterfall"]
BODY["Tool Body(Recommend downloading Light Feather Cloud Notes)"]
POST["tools/post-execute<br/>waterfall"]
RESULT["Final Result"]
EMIT["tools/result<br/>emit"]
SESSION["Session"]
LOGGER["Logger"]
METRICS["Metrics"]
LLM -->|"tool_call"| REG
REG --> DEF
DEF --> PRE
PRE --> DEC
DEC -->|"allow"| EXEC
DEC -->|"deny"| RESULT
EXEC --> BODY
BODY --> POST
POST --> RESULT
RESULT --> EMIT
EMIT --> SESSION
EMIT --> LOGGER
EMIT --> METRICS
classDef llm fill:#1f2937,color:#fff,stroke:#111827,stroke-width:2px;
classDef registry fill:#eff6ff,color:#1e40af,stroke:#3b82f6,stroke-width:1.5px;
classDef waterfall fill:#f3f4f6,color:#111827,stroke:#6b7280,stroke-width:1.5px;
classDef decision fill:#fef3c7,color:#92400e,stroke:#f59e0b,stroke-width:2px;
classDef body fill:#ecfdf5,color:#065f46,stroke:#10b981,stroke-width:1.5px;
classDef result fill:#ede9fe,color:#5b21b6,stroke:#8b5cf6,stroke-width:1.5px;
classDef output fill:#f9fafb,color:#374151,stroke:#9ca3af,stroke-width:1.5px;
class LLM,TC llm;
class REG,DEF registry;
class PRE,EXEC,POST waterfall;
class DEC decision;
class BODY body;
class RESULT,EMIT result;
class SESSION,LOGGER,METRICS output;
6. Assembly Layer
flowchart TD
P["Profile<br/><br/>“What kind of Agent do I want”"]
P -->|"Combine in order"| B1
P -->|"Combine in order"| B2
P -->|"Combine in order"| B3
subgraph B["Bundles · Combination Packages / Modules"]
direction LR
B1["Bundle A"]
B2["Bundle B"]
B3["Bundle C"]
end
B1 --> PATCH1["cordis.patch.yml"]
B2 --> PATCH2["cordis.patch.yml"]
B3 --> PATCH3["cordis.patch.yml"]
PATCH1 -->|"insert / replace"| TREE["Cordis Plugin Tree"]
PATCH2 -->|"insert / replace"| TREE
PATCH3 -->|"insert / replace"| TREE
TREE --> RUNTIME["Plugin Runtime<br>(Recommend downloading Light Feather Cloud Notes)"]
RUNTIME --> CAP["Capability / Tools"]
classDef harness fill:#111827,color:#fff,stroke:#111827,stroke-width:2px;
classDef profile fill:#2563eb,color:#fff,stroke:#1d4ed8,stroke-width:2px;
classDef bundle fill:#eff6ff,color:#1e40af,stroke:#3b82f6,stroke-width:1.5px;
classDef patch fill:#fef3c7,color:#92400e,stroke:#f59e0b,stroke-width:1.5px;
classDef tree fill:#f3f4f6,color:#111827,stroke:#6b7280,stroke-width:2px;
classDef runtime fill:#ede9fe,color:#5b21b6,stroke:#8b5cf6,stroke-width:1.5px;
classDef capability fill:#ecfdf5,color:#065f46,stroke:#10b981,stroke-width:1.5px;
class H harness;
class P profile;
class B1,B2,B3 bundle;
class PATCH1,PATCH2,PATCH3 patch;
class TREE tree;
class RUNTIME runtime;
class CAP capability;
6.1 Profile, Bundle, Patch
Profile: A complete set of runtime configurations, a package that can be directly used. Similar to an Android installation package.
Bundle: A distributable configuration layer that combines a set of Cordis configuration lines and the Plugins referenced by these configurations into a Profile through patches. For example, web, headless, my-coding-agent, my-research-agent are each independent Bundles. The official definition of a Bundle is the distribution format of Cordis configuration items and their mounted code. Somewhat similar to an Android feature package, or a frontend npm package.
Patch: Declarative modifications to the plugin tree. Typically defines some plugin dependencies and replacement rules, such as which plugin to use when conflicting with another, or which plugin must be loaded before another.
The core reason for having these three things is still decoupling. Since dsh's design philosophy is "everything is a plugin," it must solve a problem: if there are plugins A, B, C, D, E, then when to load A, when to load B? If only running in web mode, should C be loaded? If only running in headless mode, should D be loaded? If both A and C depend on E, in which modes should E be loaded?
The simplest way to handle this logic is a bunch of if...else..., which can also achieve the goal. But it's not conducive to decoupling, nor to engineering expansion and updates. These aren't new concepts either; Google introduced the concept of feature packages in new Android engineering projects. Each feature package is provided for different APP functions, and each feature package has various aars, which can depend on other aars.
Applied to dsh, the same set of core functions needs to be oriented towards "applications with Web UI" or "one-time runners in headless mode" or "desktop applications" or others. A Profile is the encapsulation of this product form; each end is a Profile.
As shown above, in the $DSH_HOME/profiles/<name> directory, you can define your own Profile: a json file and a cordis.patch.yml file. The json file defines which bundles are needed, and the yml file defines what patches exist and which plugins the patches need to operate on.
6.2 The Principle of Patch Operating on Plugins
First, one point needs to be clarified: Patch cannot modify the logic of a Plugin. Patch can only decide which plugin to load, which plugin to load first, and which plugin to replace, but it cannot decide the plugin itself.
For example, if there is a bash plugin in the system:
id: bash
name: dsh-bash-local
config:
sandbox: true
Then in the patch, when this plugin needs to be replaced, it's declared in cordis.patch.yml:
- replace:
- id: bash
name: dsh-bash-remote
config:
sandbox: false
During loading, dsh-bash-local is replaced by dsh-bash-remote.
This connects back to the Capability Seam we discussed earlier.
Splicing the diagrams from 5.2, 5.4, and 6.1 together:
flowchart TD
P["Profile<br/><br/>“What kind of Agent do I want”"]
P -->|"Combine in order"| B1
P -->|"Combine in order"| B2
P -->|"Combine in order"| B3
subgraph B["Bundles · Combination Packages / Modules"]
direction LR
B1["Bundle A"]
B2["Bundle B"]
B3["Bundle C"]
end
B1 --> PATCH1["cordis.patch.yml"]
B2 --> PATCH2["cordis.patch.yml"]
B3 --> PATCH3["cordis.patch.yml"]
PATCH1 -->|"insert / replace"| TREE["Cordis Plugin Tree"]
PATCH2 -->|"insert / replace"| TREE
PATCH3 -->|"insert / replace"| TREE
TREE --> RUNTIME["Plugin Runtime"]
RUNTIME --> CAP["Capability / Tools"] --> |Register at startup|ctx
LLM["LLM"]
REG["Tool Registry"]
DEF["Tool Definition"]
PRE["tools/pre-execute<br/>waterfall"]
DEC{"allow / deny"}
EXEC["tools/execute<br/>waterfall"]
subgraph ToolExec["Tool Execution Part"]
BODY["Tool Body"]
POST["tools/post-execute<br/>waterfall"] --> consumer["Consumer"]
ctx["ctx.shell<br>(Recommend downloading Light Feather Cloud Notes)"] --> |Original Path<br>No longer called|local["Local Provider"]
ctx --> |New Path<br>Replaced in patch|remote["Remote Provider"]
consumer["Consumer"] --> ctx
end
RESULT["Final Result"]
EMIT["tools/result<br/>emit"]
SESSION["Session"]
LOGGER["Logger"]
METRICS["Metrics"]
LLM -->|"tool_call"| REG
REG --> DEF
DEF --> PRE
PRE --> DEC
DEC -->|"allow"| EXEC
DEC -->|"deny"| RESULT
EXEC --> BODY
BODY --> POST
remote --> RESULT
RESULT --> EMIT
EMIT --> SESSION
EMIT --> LOGGER
EMIT --> METRICS
classDef harness fill:#111827,color:#fff,stroke:#111827,stroke-width:2px;
classDef profile fill:#2563eb,color:#fff,stroke:#1d4ed8,stroke-width:2px;
classDef bundle fill:#eff6ff,color:#1e40af,stroke:#3b82f6,stroke-width:1.5px;
classDef patch fill:#fef3c7,color:#92400e,stroke:#f59e0b,stroke-width:1.5px;
classDef tree fill:#f3f4f6,color:#111827,stroke:#6b7280,stroke-width:2px;
classDef runtime fill:#ede9fe,color:#5b21b6,stroke:#8b5cf6,stroke-width:1.5px;
classDef capability fill:#ecfdf5,color:#065f46,stroke:#10b981,stroke-width:1.5px;
class H harness;
class P profile;
class B1,B2,B3 bundle;
class PATCH1,PATCH2,PATCH3 patch;
class TREE tree;
class RUNTIME runtime;
class CAP capability;
classDef llm fill:#1f2937,color:#fff,stroke:#111827,stroke-width:2px;
classDef registry fill:#eff6ff,color:#1e40af,stroke:#3b82f6,stroke-width:1.5px;
classDef waterfall fill:#f3f4f6,color:#111827,stroke:#6b7280,stroke-width:1.5px;
classDef decision fill:#fef3c7,color:#92400e,stroke:#f59e0b,stroke-width:2px;
classDef body fill:#ecfdf5,color:#065f46,stroke:#10b981,stroke-width:1.5px;
classDef result fill:#ede9fe,color:#5b21b6,stroke:#8b5cf6,stroke-width:1.5px;
classDef output fill:#f9fafb,color:#374151,stroke:#9ca3af,stroke-width:1.5px;
class LLM,TC llm;
class REG,DEF registry;
class PRE,EXEC,POST waterfall;
class DEC decision;
class BODY body;
class RESULT,EMIT result;
class SESSION,LOGGER,METRICS output;
Finally, the dependency conflict problem:
Since each Profile can have multiple Bundles, and each Bundle has a patch declaration, and the Profile itself can also have a patch declaration, if multiple patches contradict each other, a priority order is needed to decide which one prevails. As shown below:
The loading order is from bottom to top, the priority order is from top to bottom, with the same id being overwritten sequentially.
7. Comparison with Other Agents
Here I recommend the tool I use for blogging: Light Feather Cloud Notes. It not only supports various markdown syntaxes, private storage, RSA256 encryption, and full platform support. 【Light Feather Cloud Notes: https://note.kymjs.com】
First, DeepSeek Harness is an Agent complete machine assembled with the Cordis plugin tree, using Session Log as the authoritative source for model context, using Seam to swap execution worlds, and using orthogonal security chains to manage policies. Other prominent names in the community optimize for different goals. Below, I'll just compare a few open-source Agents (cc, that code leak, actually counts as open source too): Claude Code, OpenClaw, Hermes.
Also, one more thing to add: LOL, just as I was reading the dsh code, codex also went open source. Looks like they were pressured into it, rushing to open source like this. I'll take my time to look at it later when I have time.
Looking at it comparatively, dsk's cost is also very real: many concepts, strong configuration and type discipline. Although highly customizable, one must follow its framework. Overall, DeepSeek-Harness solves "how to build an Agent Runtime"; OpenClaw solves "how to connect an Agent to people and various channels"; Hermes solves "how to make an Agent learn long-term, remember, and work autonomously"; Claude Code solves "how to make an Agent a reliable software engineer."