第四层 · 动手扩展实践

Cordis 教程 7 章精要

官方 docs/cordis-tutorial/ 是 7 章可运行教程,每章在 tmp/cordis-tutorial/ 跑一个真实例子,最后把插件接到真实 harness 服务。本章把 7 章浓缩成可照着做的精要。无需 API key

环境

git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install
mkdir -p tmp/cordis-tutorial && cd tmp/cordis-tutorial
# 每章都跑这一条命令:
node --import tsx ../../vendor/cordis/bin.js

这个单文件 launcher(vendor/cordis/bin.js)建一个根 Context、挂载 Loader 插件、让它加载当前目录的 ./cordis.yml--import tsx 让 Node 直接跑 TypeScript,无需构建。

① 你的第一个插件

一个 Cordis 插件命名导出一个 apply 函数。加载时 Cordis 调 apply(ctx)ctx 就是你注册一切的对象。

// hello.ts
import type { Context } from '@deepseek-ai/cordis'
export const name = 'hello'
export function apply(ctx: Context) {
  console.log('hello from my first plugin')
}
# cordis.yml —— 一个插件条目列表
- name: './hello.ts'

跑出来打印 hello from my first plugin 后进程自己退出。列表位置不保证加载顺序——顺序来自服务依赖(inject,第 3 章)。三种插件形态:函数 / 带 apply 的对象 / Service 子类(第 3 章)。解析不到的模块名走 logger 报告,可能丢在 console exporter 就绪前——加的条目没反应先查拼写。

② 生命周期与效果

Cordis 管的注册在插件卸载时按逆序回滚。返回 disposer 的注册,把 disposer 附在插件上:

export function apply(ctx: Context) {
  const timer = setInterval(() => console.log('tick'), 1000)
  ctx.effect(() => () => clearInterval(timer))  // disposer = "逆"
}

插件一卸载,定时器被清。这是论文 Theorem 7 的运行时兑现。

③ 服务

ctx 上暴露一个能力,并用 inject 依赖它。声明 ctx 键靠声明合并

declare module '@deepseek-ai/cordis' {
  interface Context { greeter: Greeter }
}
// 提供实现
ctx.provide('greeter', new Greeter())
// 别的插件 inject ['greeter'],等它就绪

需要暴露服务时,Service 子类才值得——函数形态够用就一直用函数。

④ 事件

类型化事件、广播分派、waterfall 短路。声明事件名(声明合并进 Events),ctx.on 返回 disposer:

declare module '@deepseek-ai/cordis' {
  interface Events { 'my/event'(n: number): void }
}
ctx.on('my/event', (n) => console.log(n))   // 返回 disposer
ctx.emit('my/event', 1)

⑤ 配置

cordis.yml 校验过的 config:schemasteryConfig schema,坏输入大声失败。错的配置在加载时就报错,不留隐患。

⑥ 组合与 HMR

配置文件即插件树。改 cordis.yml 触发热重载——卸载旧插件、加载新插件,注册按逆序回滚。本章还教"诊断一个永远不加载的插件":通常 inject 的依赖没就绪,插件停在 PENDING。

⑦ 接进 harness——注册一个模型可调用工具

把前面所有模式用上,对着真实 ctx.tools 注册工具并跑过真实执行管道:

// greet-tool.ts
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
import { CallId } from '@deepseek-ai/dsh-llm'

export const name = 'greet-tool'
export const inject = ['tools']                      // 等工具注册表就绪
export function apply(ctx: Context) {
  ctx.tools.register(defineTool({
    name: 'greet', description: 'Greet the named person.',
    parameters: { name: { type: 'string', required: true, description: 'Who to greet' } },
    output: { schema: { type: 'string' }, render: (_a, v) => [{ type: 'text', text: v }] },
    async execute(args) { return `Hello, ${args.name}!` },
  }))
  void (async () => {                                   // 站在模型的位置调一次
    const result = await ctx.tools.execute({
      callId: CallId('demo-1'), name: 'greet',
      arguments: { name: 'Cordis' }, signal: new AbortController().signal,
    })
    console.log('tool replied:', JSON.stringify(result.content))
  })()
}
# cordis.yml
- name: '@deepseek-ai/dsh-system-prompt'   # tools 注入 systemPrompt,所以要列它的 provider
- name: '@deepseek-ai/dsh-tools'
- name: './tool-logger.ts'                  # 另一个插件,监听 tools/result
- name: './greet-tool.ts'
[tool-logger] greet -> Hello, Cordis!      # tools/result 在 execute 的 promise resolve 前就发了
tool replied: [{"type":"text","text":"Hello, Cordis!"}]
这一章的意义
两个插件互不知道——注册表服务与事件把它们连起来。一个真实 agent 就是这套组合再加:LLM 适配器、agent loop、持久化、入口点。对照 examples/headless-agent/cordis.yml——里面每个条目你现在都读得懂了。