实操手册
动手做:添加一个 workspace 包、添加一个模型工具、接入一个 LLM 适配器
三个动手配方:添加一个 workspace 包、添加一个模型工具、接入一个 LLM(大语言模型)适配器。它们都要求一个源码 checkout(见安装与启动的「从源码运行」)。
配方一:添加一个 workspace 包
1. 创建包
packages/<group>/<pkg>/
package.json # 从 packages/core/tools 复制,调整 name/description/deps
tsconfig.json # extends ../../../tsconfig.base.json,rootDir src,outDir lib/types
src/index.ts # 服务默认导出,或插件(name/inject/apply/Config)
README.md # 服务 API、事件、扩展点与设计说明package.json 的关键不变式:private: true、version 与根 package.json 一致、type: module、@deepseek-ai/cordis 同时出现在 peerDependencies 与 devDependencies(相同范围)、@deepseek-ai/schemastery 放在 dependencies(它是运行时校验器)。
2. 在根配置中注册
- Host 包在
tsconfig.host.json、Client 包在tsconfig.client.json的references里加{ "path": "./packages/<group>/<pkg>" }。普通包恰好属于一个 aggregate,绝不两个都加。 - Client 插件包额外声明
dsh.client,并 extendstsconfig.base.client.json。
3. 确定包拓扑
可替换能力按 Service Definition / Service Provider / Consumer 三种角色拆分到不同包(需要独立演进时);单一用途的插件保持一个包。角色名要描述当前稳定职责,不用首个实现或未来扩展命名。
4. 验证
pnpm install
pnpm run constraints && pnpm run typecheck && pnpm run lint
pnpm run build && pnpm run hygiene配方二:添加一个模型工具
创建一个 greet 工具。替换插件文件为:
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'greet-tool'
export const inject = ['tools']
export function apply(ctx: Context) {
ctx.tools.register(defineTool({
name: 'greet',
description: 'Greet someone by name.',
parameters: {
name: { type: 'string', required: true, description: 'The name to greet' },
},
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
async execute(args) {
return `Hello, ${args.name}!`
},
}))
}inject 让 Cordis 等待工具注册表就绪;defineTool 根据 parameters 推导并校验 args。
pnpm dsh web --patch ./scratch-plugin/cordis.yml打开 http://127.0.0.1:3080,输入 Use the greet tool to greet Ada.,模型即可调用 greet。工具约定的完整细节见注册模型工具。
配方三:接入一个 LLM 适配器
适配器把 Harness 提供方无关的请求翻译成提供方 API 调用,再把响应翻译回 Harness 的分片:
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 {
private apiKey: string
constructor(apiKey: string) {
super()
this.apiKey = apiKey
}
async *stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
// 1. 把 options.messages 转成提供方格式。
// 2. 调用流式 API。
// 3. 把响应转成 StreamChunk 序列。
}
}
export interface Config {
apiKey: string
providers: string[]
}
export const Config: Schema<Config> = Schema.object({
apiKey: Schema.string().required(),
providers: Schema.array(Schema.string()).required(),
})
export const name = 'my-llm-adapter'
export const inject = ['llm']
export function apply(ctx: Context, config: Config) {
ctx.llm.registerAdapter(config.providers, new MyAdapter(config.apiKey))
}流式分片的硬规则:每个 block-start 都要有配对的 block-end;index 从 0 递增标识内容块顺序;工具调用的 arguments 全程是原始 JSON 字符串;usage 必须在 finish 之前发出;finish 是最后一块。无法满足的字段抛 LlmError 而不是静默丢弃。
在 cordis.yml 中使用:
- id: my-llm
name: './src/my-llm-adapter.ts'
config:
apiKey: !!js process.env.MY_API_KEY
providers:
- my-provider
- id: agent-loop
name: '@deepseek-ai/dsh-agent-loop'
config:
agents:
- id: main
provider: my-provider
model: my-model-v1仓库里的参考实现:packages/llm/llm-deepseek(直接 HTTP)与 packages/llm/llm-pi-ai(封装 LLM 库)。