00 · Before you start
Before you start
The official tutorial assumes you can already run DeepSeek Harness from a source checkout. Work from the repository root so the commands and patch path resolve as shown.
- A local checkout of deepseek-ai/deepseek-harness
- Project dependencies installed with the repository package manager
- A terminal at the repository root
- Node.js and pnpm versions compatible with the checked-out revision
DeepSeek Harness and its plugin contracts are in developer preview. Pin the revision used for development.
Step 1
Create a local plugin project
Create a temporary project inside the Harness repository. Keeping this first experiment local makes the loading path explicit and easy to remove.
mkdir -p scratch-plugin/srcStep 2
Write the plugin module
A Harness plugin is a TypeScript module that exports an apply function. The framework calls apply with a Cordis Context; use that context to register capabilities.
Create scratch-plugin/src/my-plugin.ts with the following minimal implementation. The console message gives us an observable verification signal.
import type { Context } from '@deepseek-ai/cordis'
export const name = 'hello-plugin'
export function apply(ctx: Context) {
// Required dependencies are ready before apply runs.
console.log('[hello-plugin] plugin loaded!')
}Step 3
Register it in cordis.yml
Run pwd at the repository root, then create scratch-plugin/cordis.yml as a Web configuration overlay. Replace the example repository path with the absolute path printed by pwd.
- insert:
- id: hello
name: '/absolute/path/to/deepseek-harness/scratch-plugin/src/my-plugin.ts'Step 4
Start the Web UI with the overlay
Start the Web UI from the repository root with your patch file. Then open http://127.0.0.1:3080.
pnpm dsh web --patch ./scratch-plugin/cordis.yml05 · Cordis
Lifecycle and service dependencies
Cordis scopes registrations to the plugin context. Understand these two mechanisms before your plugin owns timers, connections, tools, or other shared capabilities.
Clean up side effects with ctx.effect()
Context-managed registrations are removed with the plugin. For resources that need explicit disposal—such as timers or network connections—return a cleanup function from ctx.effect().
import type { Context } from '@deepseek-ai/cordis'
export function apply(ctx: Context) {
ctx.effect(() => {
const timer = setInterval(() => {
console.log('heartbeat')
}, 5000)
// Runs automatically when the plugin unloads.
return () => clearInterval(timer)
})
}Declare required services with inject
If a plugin needs tools, llm, or another service, declare it in inject. The framework waits for those services before calling apply.
import type { Context } from '@deepseek-ai/cordis'
export const name = 'my-tool-plugin'
export const inject = ['tools']
export function apply(ctx: Context) {
// ctx.tools is ready here.
ctx.tools.register(/* ... */)
}06 · API
Choose the smallest plugin form
Cordis accepts function, object, and class forms. Start with a function; move to a service class only when the plugin must provide a service to other plugins.
Function
The default for focused capabilities. It is easy to read, test, and unload.
Object
Useful when name, inject, apply, and related metadata belong together.
Service class
Use when the plugin exposes a named Cordis service and owns service lifecycle.
Object
export default {
name: 'my-plugin',
inject: ['tools'],
apply(ctx: Context) {
// Register capabilities here.
},
}Service class
import { Service, type Context } from '@deepseek-ai/cordis'
export default class MyService extends Service {
static inject = ['tools']
constructor(ctx: Context) {
super(ctx, 'myService')
// Perform synchronous initialization here.
}
}07 · Verify
Verification checklist
Do not stop when the process starts. Confirm the load path, lifecycle, and removal behavior.
- The Web UI opens at 127.0.0.1:3080.
- The terminal shows the hello-plugin load message once.
- Changing the absolute path to an invalid file produces an understandable load error.
- Stopping or unloading the plugin cleans up timers and other effects.
- Removing the patch restores the original Web profile behavior.
Common problems
Module cannot be resolved+
Confirm the path in cordis.yml is absolute, points to the .ts file, and matches the current checkout.
Plugin does not log+
Run the command from the repository root and confirm --patch points to ./scratch-plugin/cordis.yml.
Service is undefined+
Add the service name to inject and access it only after apply has been called.
Port 3080 is unavailable+
Stop the process already using the port or follow the current upstream Web UI options.
Continue developing
Continue developing
The minimal plugin proves loading and lifecycle. Next, build a real tool, add validated configuration, package the plugin, and test it against a pinned Harness revision.