DeepSeek Harness 插件開發技能
讓任何 Agent 都能正確、高效、符合規範地開發 DeepSeek Harness(DSH)插件。 本技能是 DSH 插件開發的操作手冊:先給出心智模型與鐵律,再給出可直接照抄的程式碼模板與分步流程,最後給出驗證清單。深度背景請見
References/目錄下的精簡提煉文件。
0. 何時使用本技能
當任務涉及以下任一項時,必須載入本技能:
- 為 DeepSeek Harness 編寫、修改或除錯插件(tool、LLM adapter、服務提供方、鉤子、UI、協定橋等)
- 編寫或修改
cordis.yml/cordis.patch.yml/dsh.profile/dsh.bundle配置 - 用
dsh plugin ...安裝、移除、打包插件 - 理解 DSH 中
ctx.tools、ctx.llm、ctx.agents、ctx.sessions等服務或tools/*、agent/*、session/event等事件 - 研究 DSH 的插件模型(Cordis 框架)如何運作
先讀本 SKILL.md 全篇,再動手。動手前先讀專案裡已有的 cordis.yml / profile / 相關原始碼,確認現有約定。
1. 心智模型:DSH 是構建在 Cordis 之上的插件化系統
一句話概括:DeepSeek Harness 是一個 Agent Harness SDK,其中每一項能力——工具、LLM 適配器、檔案存取、agent loop 本身——都是一個插件(component),掛載到一個共享的上下文(context)上。
三個核心概念(來自論文《A Programming Paradigm for Spatiotemporal Composability》,見 References/10-spatiotemporal.md):
| 概念 | 含義 | 在程式碼中的體現 |
|---|---|---|
| 可逆效應(revertible effect) | 元件對共享環境的每次修改都攜帶一個逆操作,執行時期追蹤並在元件卸載時按 LIFO 順序還原 | ctx.effect(() => () => cleanup);ctx.on()、ctx.tools.register() 等都是 effect,卸載自動撤銷 |
| 反應式共效應(reactive coeffect) | 元件宣告它依賴哪些服務;服務出現/消失/換實現時,元件自動激活/停用/重載 | export const inject = ['tools'];依賴消失時插件自動 dispose,恢復時自動重載 |
| Fiber(元件實例) | 每個已載入的插件實例是一個 fiber,有生命週期狀態機 | PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED(apply 拋錯則 FAILED) |
由此得到的鐵律:
- 所有註冊都必須通過
ctx完成(ctx.on/ctx.tools.register/ctx.effect/ctx.plugin/ctx.provide)。凡是經過ctx的註冊,卸載時自動清理,絕不手動 removeListener / clearInterval。 - 凡是不經過
ctx管理的資源(網路連線、自建定時器、檔案句柄、watcher),必須包在ctx.effect(() => { ...; return () => cleanup })裡。 - 依賴必須宣告(
inject),不要用ctx.get()探測必需依賴,不要臆想載入順序——順序由依賴決定,不由cordis.yml行序決定。 - 配置必須可配置:任何不同部署取值可能不同的參數都必須成為
Config欄位,絕無硬編碼(檢驗標準:能否在cordis.yml中改這個值而不改程式碼?)。 - 配置錯誤要響亮:用 Schemastery schema 在載入時校驗,無效配置直接載入失敗並給出明確錯誤,絕不帶病執行。
- 回呼可被回放/熱重載:展示類回呼(
output.render、presentCall、presentResult)必須是純函數;插件隨時可能被 HMR 卸載重載,不要依賴跨重載的全域狀態。
2. 環境與工具鏈
- 插件是 TypeScript 模組(也接受 JS),執行於 Node.js ESM 環境。
- 核心依賴套件(都來自
@deepseek-ai作用域):@deepseek-ai/cordis— 插件框架(context、fiber、事件、註冊表)@deepseek-ai/schemastery— 配置 schema 校驗(Standard Schema 實現)@deepseek-ai/dsh-tools—defineTool與工具註冊表服務@deepseek-ai/dsh-llm—LlmAdapter、StreamChunk、GenerateOptions、CallId- 各能力 seam 套件:
dsh-shell、dsh-session、dsh-agent、dsh-system-prompt、dsh-fs、dsh-jobs、dsh-credentials等(完整清單見References/08-capability-layering.md)
- 兩個開發入口(任選):
- 原始碼檢出(推薦,教學場景):克隆
deepseek-ai/deepseek-harness,pnpm install,用pnpm dsh web --patch <你的cordis.yml>啟動 Web UI,用node --import tsx ../../vendor/cordis/bin.js跑純 Cordis 教學範例。 - 已安裝 CLI:
dsh --profile <name> ...啟動 profile,dsh plugin --profile <name> <pnpm args>管理插件。
- 原始碼檢出(推薦,教學場景):克隆
3. 插件的最小形態(先學會這個)
一個插件就是一個匯出 apply 函數的模組:
import type { Context } from '@deepseek-ai/cordis'
export const name = 'my-plugin' // 可選,僅用於診斷顯示
export function apply(ctx: Context) {
// 在這裡註冊能力
}
透過 cordis.yml 或 patch 掛載:
- name: './src/my-plugin.ts' # 相對路徑(原始碼/開發)或 npm 套件名(安裝後)
id: my-plugin # 穩定標識,loader 按 id 做配置對帳
三種插件形態
- 函數形態(預設推薦):
export const name = 'greet-tool' export const inject = ['tools'] export function apply(ctx: Context) { /* ... */ } - 物件形態:
export default { name, inject, apply(ctx) {} } - 類別形態(需要對外提供服務時):
export default class MyService extends Service { constructor(ctx) { super(ctx, 'myService') } }
在需要提供服務之前一律用函數形態。
4. 開發一個 Tool(最高頻任務)
工具註冊到 ctx.tools,用 defineTool 定義。模板(可直接照抄):
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'my-tool'
export const inject = ['tools']
export function apply(ctx: Context) {
ctx.tools.register(defineTool({
name: 'my_tool', // 模型看到的工具名(snake_case 慣例)
description: 'One sentence about what it does.', // 模型看到的功能描述,要具體、說清何時用
parameters: {
path: { type: 'string', required: true, description: 'What the param means' },
limit: { type: 'number' }, // 未標 required 即為可選
},
output: {
schema: { type: 'string' }, // 宣告 execute 返回的規範 JSON 值
render: (_args, value) => [{ type: 'text', text: value }], // 轉為模型可見內容
},
async execute(args, exec) {
// args 已被 schema 校驗並推斷型別
// exec.signal 是取消訊號,長任務必須回應它
return `result for ${args.path}`
},
}))
}
defineTool 的完整契約(務必遵守)
parameters:參數 schema 映射,根是隱式開放物件;每屬性可required: true。支援string/number/integer/boolean/null/array/object/oneOf;顯式 object 節點必須宣告additionalProperties: true | false。output.schema:宣告execute返回的規範 JSON 值(物件/陣列/純量/null 皆可)。這是給程式化呼叫方(Code Mode 的await tools.xxx())用的 API,要設計成直接返回控制代碼與欄位;面向人的解釋放output.render。execute(args, exec):- args 已經過執行時期校驗,但 schema DSL 表達不了的約束(非空字串、正數、跨欄位)要自己
throw new Error(...)。 - 返回
output.schema宣告的規範值。不要返回內容塊,不要逼呼叫方從自然語言裡解析 id。 - 遵守
exec.signal:訊號觸發時取消進行中的工作。 - 基礎設施故障(拋異常/無效返回值)→ 結果標記為
isError。成功但結果不理想(如非零退出碼)→ 仍返回規範值,在 render 裡解釋。
- args 已經過執行時期校驗,但 schema DSL 表達不了的約束(非空字串、正數、跨欄位)要自己
output.render(args, value):純函數,把規範值轉成ContentBlock[](模型看到的文字)。絕不在此做 I/O、讀會話狀態。- 可選
output.presentationMeta(args, value):從規範值派生可回放的 UI 元資料(純函數)。 - 可選
presentCall(args)/presentResult(args, result):宣告工具在 UI 中的卡片渲染意圖(card: 'generic' | 'terminal' | 'diff' | 'search' | 'web' | 'read')。必須純函數(會在即時串流和會話回放中執行),不做 I/O。 - 不要把
timeoutMs、isConcurrencySafe、finalizeContent等執行時期元資料當作模型可見 schema——它們不會洩漏給模型。 - 參數不可變:把
args當唯讀;註冊後不要改 schema 或替換回呼。
長時間執行的任務(後台任務)
若要支援 run_in_background:
const jobs = ctx.get('jobs')
if (!jobs) throw new Error('background jobs unavailable: load @deepseek-ai/dsh-jobs')
return {
kind: 'background',
jobId: jobs.start({
kind: 'my-work',
label: args.command,
...exec.agent ? { owner: exec.agent } : {},
run: () => { /* 返回 { cancel, done, readOutput? } */ },
}),
}
要點:發布 jobId 之後用任務自己的取消訊號(歸 job_kill / owner dispose 管),不再用 exec.signal;成功輸出 schema 要能承載 { kind: 'background', jobId } 這種規範控制代碼。參考 References/03-tools.md 與 @deepseek-ai/dsh-tool-bash 實現。
工具的執行策略(擴充點,按需用)
| 擴充點 | 模式 | 用途 |
|---|---|---|
tools/pre-execute | waterfall | 允許/拒絕/詢問策略(權限門禁);返回 {kind:'allow'} / {kind:'deny',reason} / {kind:'ask'} |
ctx.tools.guard() | 單調守衛 | 最終拒絕,後續監聽器無法撤銷(返回 reason 即拒絕) |
tools/execute | waterfall | 包裹分發:超時/重試/指標;可替換 exec.signal 但不可移除 |
tools/post-execute | waterfall | 替換內容/值、阻止結果、附加模型上下文 |
tools/result | emit | 唯讀觀測不可變最終結果(日誌、稽核) |
策略不要內建進工具;把部署策略放進鉤子插件(普通 Cordis 插件監聽這些事件即可,如權限門禁範例見 References/11-cookbook.md)。
5. 插件配置(Schemastery)
插件匯出 Config(同名的 TS 介面 + Schemastery schema),Cordis 在 apply 前校驗並填充預設值:
import type { Context } from '@deepseek-ai/cordis'
import Schema from '@deepseek-ai/schemastery'
export interface Config {
greeting: string
maxRetries: number
verbose?: boolean
}
export const Config: Schema<Config> = Schema.object({
greeting: Schema.string().default('Hello'),
maxRetries: Schema.number().default(3),
verbose: Schema.boolean().default(false),
})
export function apply(ctx: Context, config: Config) {
// config 一定是完整且經過校驗的
}
在 cordis.yml 中傳配置:
- name: './src/my-plugin.ts'
config:
greeting: 'Hi there'
maxRetries: 5
要點:
Config必須是一個 Standard Schema 物件(用 Schemastery 構建),不能匯出普通物件。- 常用構造:
Schema.string().required()/.default(x)、Schema.number()、Schema.boolean()、Schema.array(String)、Schema.union(['a','b'])、Schema.object({...})巢狀。 - 支援
!!js運算式在載入時求值(如greeting: !!js process.env.GREETING ?? 'Hello');!!js僅對config與disabled欄位有效。 - 配置變更會觸發 HMR:舊實例卸載(註冊自動清理)→ 新實例載入。不要在插件外部快取配置。
6. 服務與依賴(服務端開發)
使用服務(消費方)
export const inject = ['tools'] // 必需依賴:不滿足就不載入
export function apply(ctx: Context) {
ctx.tools.register(/* ... */) // apply 時保證已就緒
}
可選依賴:不寫 inject,使用時 ctx.get('service') 探測(返回 undefined 表示沒有提供方)。
提供服務(提供方)
用 Service 子類別(類別形態插件):
import { Service, type Context } from '@deepseek-ai/cordis'
declare module '@deepseek-ai/cordis' {
interface Context {
metrics: MetricsService // 宣告合併:讓 ctx.metrics 有型別
}
}
export default class MetricsService extends Service {
static inject = ['llm'] // 服務也可以依賴其他服務
constructor(ctx: Context) {
super(ctx, 'metrics') // 'metrics' 即服務名
}
record(event: string, value: number) { /* ... */ }
}
export const name = 'metrics'
export function apply(ctx: Context) {
ctx.plugin(MetricsService)
}
低層 API(不常用但要知道):ctx.provide(name, value) 註冊服務實現(effect,卸載自動登出)、ctx.set/get 讀寫儲存、ctx.accessor 定義計算屬性、ctx.mixin 把服務成員掛到 ctx 上。
服務行為語義(理解而非背誦)
inject不是一次性啟動檢查:執行期間必需服務消失 → 依賴插件自動 dispose;服務恢復 → 自動重載。這保證消費方永遠不會持有對已卸載服務的引用。- 服務名是扁平全域命名空間:
tools、llm等已被佔用;自有服務名要有辨識度前綴(如myCap、metrics)。 - 服務隔離:
cordis.yml中用 group +isolate讓不同插件組看到同一服務名的不同實例(如兩組各自配置不同 timeout 的shell提供方)。
7. 事件系統(插件間通訊)
分發模式(事件名的一部分,必須選對)
| 模式 | 呼叫 | 語義 |
|---|---|---|
emit | ctx.emit(name, ...args) | 同步廣播,不等待、不收集返回值 |
parallel | await ctx.parallel(...) | 所有監聽器並發執行並等待 |
serial | await ctx.serial(...) | 按序執行等待,第一個非 null/false/undefined 返回值勝出並停止 |
bail | ctx.bail(...) | serial 的同步版 |
waterfall | await ctx.waterfall(name, ...args, next) | 環繞中介軟體;監聽器收到 next,包裝下游返回值 |
waterfall 鐵律:只觀察/標註的監聽器必須呼叫 next();不呼叫 next() 直接返回 = 有意短路(否決/攔截)。 忘記呼叫 next() 會靜默吞掉下游預設行為。
型別安全的事件
declare module '@deepseek-ai/cordis' {
interface Events {
'my-plugin/ready': (payload: { id: string }) => void
'my-plugin/check': (input: string) => boolean | undefined
}
}
事件命名約定 namespace/action(如 agent/step、tools/result、session/event)。監聽器用 ctx.on() 註冊即自動隨插件卸載清理。
常用 Harness 事件(開發時經常用到)
tools/pre-execute/tools/execute/tools/post-execute/tools/result— 工具管線(見上表)agent/*(agent/request、agent/pre-step、agent/session-start、agent/turn-stopping等)— agent 生命週期與請求建構session/event— 持久化的會話事件流(turn/*、step/*、tool/call、tool/result、assistant/chunk都是這裡的event.type,不是同名 Cordis 事件)approval/request— 審批 waterfallsystem-prompt/assemble— 系統提示詞整體裝配(權威返回,監聽者有責任保留既有貢獻)
完整簽名/觸發模式見各子系統頁面生成的 cordis-surface 區塊(References/08-capability-layering.md 有指引)。
8. LLM 適配器(接入新模型提供方)
適配器 = 繼承 LlmAdapter、實現 stream() 的類別,把 Harness 提供方無關請求轉成具體 API 呼叫並轉回 StreamChunk 分片。
import type { Context } from '@deepseek-ai/cordis'
import Schema from '@deepseek-ai/schemastery'
import { LlmAdapter, type GenerateOptions, type StreamChunk } from '@deepseek-ai/dsh-llm'
class MyAdapter extends LlmAdapter {
async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
// 1. options.messages → 提供方格式
// 2. 調流式 API(必須傳 options.signal)
// 3. 響應 → StreamChunk 序列
}
}
export const name = 'my-llm-adapter'
export const inject = ['llm']
export const Config = Schema.object({
apiKey: Schema.string().required(),
providers: Schema.array(String).required(),
})
export function apply(ctx: Context, config: Config) {
ctx.llm.registerAdapter(config.providers, new MyAdapter(config.apiKey))
}
StreamChunk 協議(嚴格遵守)
async function* chunks(): AsyncIterable<StreamChunk> {
yield { type: 'block-start', index: 0, blockType: 'text' }
yield { type: 'text-delta', index: 0, text: 'Hello' }
yield { type: 'block-end', index: 0, block: { type: 'text', text: 'Hello world' } }
// 工具呼叫塊:
yield { type: 'block-start', index: 1, blockType: 'tool-call' }
yield { type: 'tool-call-delta', index: 1, id: CallId('c1'), name: 'bash', argumentsDelta: '{"command":"ls"}' }
yield { type: 'block-end', index: 1, block: { type: 'tool-call', id: CallId('c1'), name: 'bash', arguments: '{"command":"ls"}' } }
yield { type: 'usage', usage: { inputTokens: 100, outputTokens: 50 } } // 必須在 finish 前
yield { type: 'finish', reason: { kind: 'stop' } } // 必須最後
}
規則:每個 block-start 必有對應 block-end;index 從 0 遞增;tool-call 的 arguments 全程是原始 JSON 字串;finish 是最後一塊;usage 在 finish 前。
錯誤與元資料
- 傳輸/協定故障:
throw new LlmError(msg, 'STABLE_CODE')(帶穩定 code),不要依賴普通Error自動轉換。 - 提供方不支援
GenerateOptions中的某個欄位:throw new LlmError(..., 'UNSUPPORTED'),絕不靜默丟棄。 - 每個 HTTP 請求合併
attributionHeaders(),並傳遞options.signal。 - 可選:覆寫
resolveModel()(返回提供方/模型身份 + 可選 context/reasoning 元資料)與listModels()(公佈模型選項)。
完整教學見 References/05-llm-adapter.md;參考實現:packages/llm/llm-deepseek(OpenAI 相容 SSE)與 packages/llm/llm-pi-ai。
9. 能力分層(三個角色,可替換能力的設計模式)
當一個能力需要可替換的提供方(如 Bash 執行),拆成三個角色、放入不同套件:
- Service Definition(如
dsh-shell):定義 Cordis 服務介面 + Request/Result 型別。abstract class MyCapService extends Service { super(ctx,'myCap'); abstract execute(req): Promise<res> } - Service Provider(如
dsh-bash-local):實現介面。class MyCapLocal extends MyCapService { ... };apply(ctx){ ctx.plugin(MyCapLocal) } - Consumer(如
dsh-tool-bash):把能力暴露為模型工具。inject: ['tools','myCap']
依賴方向:Provider 依賴 Definition;Consumer 依賴 Definition;Provider 與 Consumer 互不依賴。更換提供方只改 cordis.yml 一行,Definition 與 Consumer 不動。
不要預防性拆分:只有角色需要獨立演進才拆包。簡單工具插件一個包就夠。
10. 打包與安裝(把插件交付給使用者)
兩個概念
- 組合包(bundle):附帶一個配置層的 npm 套件,manifest 宣告
dsh.bundle。是你編寫並分發的東西。 - profile:位於
$DSH_HOME/profiles/<name>的可啟動組合,manifest 宣告dsh.profile(含有序bundles列表)。是使用者啟動的東西。
打包一個組合包
hello-plugin/
├── package.json # 宣告 dsh.bundle
├── cordis.patch.yml # 層內容
└── index.js # 插件模組
// package.json
{
"name": "dsh-hello-plugin",
"version": "0.1.0",
"type": "module",
"main": "index.js",
"files": ["index.js", "cordis.patch.yml"],
"dsh": { "bundle": { "patch": "cordis.patch.yml" } }
}
# cordis.patch.yml
plugins:
- name: dsh-hello-plugin
安裝組合包:dsh plugin install --profile <name> dsh-hello-plugin(或 pnpm add + 手動改 cordis.yml)。
建立一個 profile
如果整套配置要作為可重用的啟動設定,就寫一個 profile:
# $DSH_HOME/profiles/my-work/dsh.profile
version: "1"
summary: "My Work Harness"
needs:
bundles:
- dsh-hello-plugin@^0.1
- @deepseek-ai/dsh-core
plugins:
- name: dsh-hello-plugin # 套件內插件
disabled: false
config:
greeting: Bonjour
然後 dsh --profile my-work 啟動。
原生支援打包(已整合的 CLI)
如果使用 DSH 的整合 CLI(dsh 命令):
# 開發中快速測試
$ dsh plugin install --profile dev ./path/to/my-plugin
# 打包為 tarball 並安裝
$ cd my-plugin && npm pack
$ dsh plugin install --profile dev ./dsh-my-plugin-0.1.0.tgz
# 發佈 npm 後
$ dsh plugin install --profile dev dsh-my-plugin
推薦:開發階段用
dsh plugin install --profile dev ./src/my-plugin.ts直接載入原始碼,結合 HMR 快速迭代。發佈前再打包。
11. 驗證清單(動手前 / PR 前檢查)
| # | 檢查項 | 通過條件 |
|---|---|---|
| 1 | 所有註冊透過 ctx | 無 addListener、setInterval、fs.watch 等裸調用 |
| 2 | 非 ctx 資源包在 ctx.effect | 每個手動資源都有 ctx.effect(() => { create; return () => destroy }) |
| 3 | inject 宣告正確 | 必需依賴在 inject 中列出;可選依賴用 ctx.get() 探測 |
| 4 | Config 有 Schemastery schema | 所有可配置參數有 schema 欄位 + 預設值 |
| 5 | 工具使用 defineTool | 工具定義符合 name/description/parameters/output/execute 契約 |
| 6 | execute 返回規範值 | 非 isError 時返回 output.schema 宣告的型別,不返回內容塊 |
| 7 | render 是純函數 | 不做 I/O、不讀 ctx、不產生副作用 |
| 8 | 服務提供方用 extends Service | 註冊透過 ctx.plugin(ServiceSubclass) |
| 9 | 長任務回應 exec.signal | 耗時操作檢查 signal,收到後取消 |
| 10 | schema 約束不足處有顯式檢查 | 非空、正數等附加約束在 execute 頂部 throw |
| 11 | 無硬編碼可配置參數 | 所有部署差異參數都在 Config 中 |
| 12 | 使用 defineTool 註冊 | 無遺漏的 ctx.tools.register |
12. 參考文件索引
References/ 目錄下的簡潔提煉(均為單頁 Macchiato Markdown):
| 檔案 | 內容 |
|---|---|
01-cordis.md | Cordis 核心概念:context、fiber、effect、coeffect、服務、事件模式 |
02-schemastery.md | Schemastery schema 構造速查 |
03-tools.md | 工具定義、執行管道、擴充點詳解 |
04-ui-card.md | 工具卡片渲染(presentCall/presentResult)規範 |
05-llm-adapter.md | LLM 適配器完整教程 |
06-events.md | Harness 事件總表 |
07-packaging.md | 打包、bundle、profile 詳解 |
08-capability-layering.md | 能力分層架構與 seam 設計模式 |
09-sandboxing.md | 工具沙箱與安全模型 |
10-spatiotemporal.md | 時空可組合性論文摘要 |
11-cookbook.md | 常見模式與食譜:權限門禁、配置熱重載、自訂事件 |
本技能參考了 @deepseek-ai/cordis 3.x 與 @deepseek-ai/dsh-tools 0.x 的 API。