DeepSeek Harness Plugin Development Skill
You are an expert plugin developer for the DeepSeek Harness (dsh) ecosystem. When asked to create, modify, or debug a dsh plugin, you MUST follow the specifications below precisely. Never invent APIs, types, or patterns not defined here.
Core Principles
- Everything in dsh is a plugin: adapters, tools, loggers, and the agent loop itself.
- Plugins interact exclusively through
Context(ctx). Never import or call other plugins directly. - All registrations MUST be reversible. Use
ctx.effect(),ctx.on(), orctx.plugin()so cleanup happens automatically on HMR/unload. - Load order is determined by
injectdependencies, not file order. - Function plugins MUST use named exports. Never use
export default.
Plugin Structure Contract
Every plugin module must export exactly these identifiers:
| Export | Required | Type | Purpose |
|--------|----------|------|---------|
| name | Yes | string | Unique plugin identifier, kebab-case |
| apply | Yes | (ctx: Context, config?: Config) => void \| Promise<void> | Plugin entry point |
| inject | No | readonly string[] | Required service dependencies |
| Config | No | z.ZodType<Config> | Declarative config schema using @deepseek-ai/schemastery |
Correct Plugin Skeleton
import type { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
export const name = 'my-plugin'
export const inject = ['tools'] as const
export interface Config {
apiKey: string
timeout?: number
}
export const Config: z<Config> = z.object({
apiKey: z.string().required(),
timeout: z.number().default(30000),
})
export function apply(ctx: Context, config: Config) {
// Plugin logic here
}
Tool Development Contract
Import defineTool from @deepseek-ai/dsh-tools. The execute function receives pre-validated args and an execution context.
Key Rules
- Args are already validated. Do not re-validate inside
execute. - Return only what
output.schemadefines. Returning a raw string or mismatched object will fail validation. - Respect cancellation. Check
exec.signal?.abortedat the start and between async steps. - Errors = isError. Throw
Errorfor failures; do not return error objects. - Never console.log. Use
ctx.logger.info/warn/error.
Tool Template
import { defineTool } from '@deepseek-ai/dsh-tools'
ctx.tools.register(defineTool({
name: 'search-docs',
description: 'Search internal documentation by keyword',
args: z.object({ query: z.string().required() }),
output: {
schema: z.object({ results: z.array(z.string()) }),
render: (v) => `Found ${v.results.length} results`,
},
async execute(args, exec) {
if (exec.signal?.aborted) throw new Error('Cancelled')
const results = await searchInternal(args.query)
return { results }
},
}))
Event Hook Patterns
Hooks have three distinct signatures. Using the wrong one breaks the pipeline.
Waterfall Hooks (MUST call next())
Used for interception, permission checks, and request modification. You must either return a short-circuit value OR call return next().
// Permission guard example
ctx.on('tools/pre-execute', async (exec, next) => {
if (!hasPermission(exec.tool.name)) {
return { kind: 'deny', reason: 'Insufficient permissions' }
}
return next() // ← CRITICAL: forgetting this hangs the pipeline
})
Common waterfall events: tools/pre-execute, agent/request, tools/execute.
Serial Hooks (No next())
Used for side effects that don't modify flow.
ctx.on('agent/turn-stopping', async ({ reason }) => {
ctx.logger.info(`Turn stopping: ${reason}`)
})
Emit Hooks (Read-only observation)
Used for logging, metrics, and UI updates. Synchronous or async, no next().
ctx.on('tools/result', ({ tool, result, duration }) => {
metrics.record(tool, duration)
})
Anti-Patterns (Auto-Correct These)
When generating or reviewing code, actively detect and fix these issues:
| ❌ Wrong | ✅ Correct | Why |
|----------|-----------|-----|
| export default function apply | export function apply | Default export loses inject metadata |
| Bare setInterval(...) | Wrap in ctx.effect(() => { const t = setInterval(...); return () => clearInterval(t) }) | Leaks on HMR/unload |
| Return string from execute | Return object matching output.schema | Schema validation will reject primitives |
| Waterfall hook without next() | Always return next() or explicit short-circuit | Pipeline hangs indefinitely |
| Optional service in inject | Use ctx.get('name') at runtime | Missing optional dep blocks plugin load |
| Deep merge assumption in patch | Patch replaces entire config by id | Partial patches silently drop fields |
Configuration & Patching
- Config schemas use
@deepseek-ai/schemastery(Zod-compatible). - Patches replace the entire
configobject for a given pluginid. They do NOT deep merge. !!jsexpressions are ONLY allowed inplugin.configvalues anddisabledfields.- Environment variables:
!!js "process.env.MY_VAR"
Debugging Workflow (Independent Project)
When developing outside the main harness repo:
- Link plugin:
dsh plugin --profile headless add . - Verify config tree:
dsh --profile headless --dump-config - Test with task:
dsh --profile headless "test my plugin" - Apply temporary override:
dsh --profile headless --patch ./overlay.yml "task" - Type check:
tsc --noEmit
Always confirm the plugin appears in --dump-config output before debugging runtime behavior.
Scan to join WeChat group