DeepSeek Harness 解析含源码解读
个人观点不代表官方
1. 引言:DeepSeek Harness 是什么?
DeepSeek Harness(简称 dsh,读作 D-S-H)是一个由深度求索公司开源的 AI 智能体(Agent)开发框架。你可以把它想象成一个“AI 全能社团”的管理系统——它能把大语言模型、外部工具(如搜索引擎、代码解释器)、记忆模块等零件像乐高积木一样拼装起来,快速构建出一个能自主完成复杂任务的 AI 助手。
1.1 核心特点
- 完全插件化:每一个功能(调用模型、搜索网页、执行代码)都是一个独立的插件,可以自由增删和替换。
- 动态可热更新:无需重启,就能在运行时安装或卸载插件,非常适合实验和快速迭代。
- 基于 Cordis 框架:底层依赖插件管理内核 Cordis,负责插件生命周期、依赖注入和副作用清理。
- 开箱即用的配置:通过配置文件(Profile)可以组合出不同形态的应用,比如网页版(Web App)或命令行版(Headless)。
1.2 谁适合使用?
- 对 AI Agent 开发感兴趣的研究者或开发者
- 希望快速搭建自定义 AI 助手的爱好者
- 想学习现代插件架构和依赖注入思想的学生
1.3快速安装dsh
安装环境
1.首先点我跳转后下载setup.exe,然后一路确定到最后
2.打开power shell,然后
nvm install 24.19.0
nvm use 24.19.0
npm install -g pnpm@latest
源码安装
git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install
pnpm run build
pnpm dsh web
进入浏览器dsh web: http://127.0.0.1:3080,如果你想用deepseek那么直接写key然后点右边按钮,否则点左边按钮

然后进入设置可以看到语言选项

也可以自定义模型不一定非得deepseek

然后你可以自己选择插件

让你的手机也能访问dsh
1.进入ngrok界面点我跳转

选择第一个,然后copy key让codex、claudcode等给你配置,比如:
我的ngrok的认证是这个****,把本机的3080让我可以访问
1.4 插件市场可视化

DeepSeek Harness 可以通过插件市场管理和安装插件,例如安装 dshmarket:
dsh plugin --profile web add dshmarket
如果出现:
'dsh' 不是内部或外部命令,也不是可运行的程序或批处理文件。
说明当前使用的是 DeepSeek Harness 源码运行方式,dsh 并没有被安装为 Windows 全局命令。
这时进入 DeepSeek Harness 源码根目录,例如:
cd /d E:\path\to\deepseek-harness
然后改用:
pnpm dsh plugin --profile web add dshmarket
源码模式下可以简单记住:
启动 DeepSeek Harness
→ pnpm dsh web
安装插件
→ pnpm dsh plugin --profile web add <插件名>
例如:
pnpm dsh plugin --profile web add dshmarket
安装成功时通常可以看到类似:
Already up to date
Progress: resolved ..., added ..., done
Done in ...ms using pnpm v...
如果安装过程中出现:
[ERR_PNPM_IGNORED_BUILDS]
Ignored build scripts: ...
说明 Web Profile 中有依赖的构建脚本尚未经过 pnpm 授权。
先进入 Web Profile:
cd /d %USERPROFILE%\.dsh\profiles\web
执行:
pnpm approve-builds
根据实际需要选择允许执行构建脚本的依赖。
如果之前还出现:
[ERR_PNPM_VIRTUAL_STORE_DIR_MAX_LENGTH_DIFF]
可以先在同一目录执行:
pnpm install
让 pnpm 重新创建 Web Profile 的 node_modules。
处理完成后,再回到 DeepSeek Harness 源码目录:
cd /d E:\path\to\deepseek-harness
重新执行:
pnpm dsh plugin --profile web add dshmarket
注意:Windows CMD 跨盘符切换目录时建议使用
cd /d,否则虽然输入了其他盘符的路径,当前命令行可能仍停留在原来的盘符,导致pnpm dsh找不到 DeepSeek Harness 项目中的 CLI。
安装完成后重新启动:
pnpm dsh web
然后进入 Web 页面即可使用插件市场。
1.5 绝大多数模型可调用的api(还有免费api路由自动帮你找!)
注册API
点我跳转
然后点击get apikey并完成一系列注册
复制key并进入dsh操作

免费模型路由
选如下的配置,可以看到还是挺快的,36min输入了3.3M tok,输出84.1K

2. Cordis
Cordis 是 DeepSeek Harness 底层使用的 插件框架(Plugin Framework)。
官方文档中的原话是:
Cordis is the vendored plugin framework underneath DeepSeek Harness.
这里的 vendored(原意供应商但这里不是这个意思) 可以理解为:Cordis 的代码被直接纳入 DeepSeek Harness 项目中维护,而不是在运行时简单依赖一个外部包。
如果继续使用“大学社团”的比喻:
- DeepSeek Harness = 整个大学社团;
- LLM、Tools、Agent、Storage 等插件 = 社团中的不同部门;
- Cordis = 社团的组织制度 + 行政管理系统。
Cordis 本身并不负责“思考问题”或者“调用搜索引擎”。它主要解决的是:
这么多插件怎样被加载、怎样获得其他能力、怎样相互通信,以及插件卸载以后怎样把自己留下的运行时影响清理干净。
所以,要真正读懂 DeepSeek Harness 的插件源码,首先需要理解 Cordis。
官方 Primer 将 Cordis 概括为 五个核心思想:
1. Plugin / Service
↓
2. Context
↓
3. Dependency Injection
↓
4. Typed Events
↓
5. Reversible Effects
下面分别说明。
2.1 插件与 Service:社团里的各个部门
Plugin 发音:【普】拉金
Service 发音:【色】维斯
Cordis 中首先需要理解的是 Plugin(插件)。
可以把插件想象成大学社团中的一个部门:
- 搜索插件 = 外联部;
- 代码执行插件 = 技术部;
- LLM 插件 = 模型部门;
- Agent 插件 = 任务协调部门;
- Storage 插件 = 档案室。
每个插件负责相对独立的一类功能,并由 Cordis 管理它什么时候加载、什么时候卸载,以及它依赖哪些能力。
官方 Primer 对插件的描述是:
A plugin is a object that implements Service.
这里不要简单理解成:
所有 Plugin
都必须
extends Service
实际上 Cordis 支持多种 Plugin 形式,包括函数、带有 apply() 的对象,以及 Service 子类。
因此可以先把 Plugin 理解成:
被 Cordis 管理生命周期的功能单元。
而 Service 则是:
Plugin 向 Context 暴露的、具有固定名字并可以被其他 Plugin 或 Service 使用的一种能力。
例如:
某个 Plugin
↓
提供 tools Service
↓
Context
↓
ctx.tools
↓
其他 Plugin 使用
所以:
Plugin ≠ Service
更准确地说是:
Plugin
可以提供
Service
而其他 Plugin 可以依赖并使用这些 Service。
仍然用大学社团来类比,可以把:
Plugin
理解成:
一个部门
而:
Service
则更像这个部门对外开放的一个“服务窗口”。
例如:
技术部 Plugin
↓
提供
↓
代码执行 Service
其他部门真正关心的通常不是:
这个代码执行能力
到底由技术部里的谁实现
而是:
我现在有没有一个
可以使用的代码执行 Service
对应到 Cordis 中就是:
Plugin A
↓
提供 Service
↓
Context
↓
ctx.service
↑
│
Plugin B 使用
这样做的好处是,Plugin B 不需要直接绑定 Plugin A。
它真正依赖的是:
某一种能力
而不是:
某一个具体实现
所以后面我们会看到类似:
inject = ['tools']
它表达的并不是:
我依赖某个叫 tools 的 Plugin
而是:
我启动之前
必须存在一个叫 tools 的 Service
这里还要继续区分一个很容易忽略的问题:
Service 本身并不一定需要依赖其他 Service。
有些 Service 可以直接建立在 Cordis 的 Context 和生命周期机制之上,然后再向其他 Plugin 提供能力。
例如:
Cordis
↓
Context
↓
Service A
↓
Plugin B
这里的 Service A 没有依赖其他业务 Service。
这种 Service 可以把它理解为整个依赖关系中比较靠底层的:
基础 Service
注意,“基础 Service”这里只是为了方便理解,并不是 Cordis 专门定义的一种新的 Service 类型。
它仍然是普通 Service,只是:
没有声明其他业务 Service 依赖
例如一个非常简单的 Service 可以是:
class GreeterService extends Service {
constructor(ctx: Context) {
super(ctx, 'greeter')
}
greet(name: string) {
return `Hello, ${name}!`
}
}
这里并没有:
static inject = [...]
因此它不需要等待其他业务 Service。
创建以后:
super(ctx, 'greeter')
会把它注册为:
greeter Service
于是:
GreeterService
↓
注册 greeter
↓
Context
↓
ctx.greeter
其他 Plugin 则可以进一步依赖它:
GreeterService
↓
greeter Service
↓
ctx.greeter
↑
│
Consumer Plugin
不过这里说的“不依赖其他 Service”,指的是:
不依赖其他业务 Service
它仍然运行在:
Cordis Runtime
+
Context
+
生命周期机制
之上。
也就是说,我们可以区分两层依赖:
Cordis 框架依赖
│
├── Context
├── Fiber
└── 生命周期机制
业务 Service 依赖
│
├── tools
├── llm
├── storage
├── sessions
└── ...
一个 Service 可以没有第二层依赖,但仍然属于 Cordis 的运行体系。
更进一步,Service 自己也可以继续依赖其他 Service。
所以 Service 并不只是:
被 Plugin 使用
它自己也可能是另一个 Service 的消费者。
例如:
Service A
↓
Service B
↓
Plugin C
这里的 Service B:
向下
依赖 Service A
同时
向上
提供 Service B
因此它同时具有两个身份:
对 Service A 来说
→ Consumer
对 Plugin C 来说
→ Provider
DSH 中就存在这种真实结构。
例如 ToolRuntime 本身是一个 Service:
export class ToolRuntime extends Service {
static inject = ['systemPrompt']
constructor(ctx: Context, config = {}) {
super(ctx, 'tools')
}
}
这里两行代码的方向完全不同。
首先:
static inject = ['systemPrompt']
表示:
ToolRuntime
依赖
systemPrompt Service
而:
super(ctx, 'tools')
则表示:
ToolRuntime
向 Context 提供
tools Service
于是完整关系变成:
systemPrompt Service
↓
ToolRuntime
↓
tools Service
↓
ctx.tools
↓
其他 Plugin / Service
所以 DSH 中的 Service 并不是一层平铺的。
更接近这样的结构:
Cordis
↓
Context
↓
基础 Service
↓
组合 Service
↓
更上层 Service
↓
业务 Plugin
当然,实际系统不会只有一条直线,而更像一张依赖网络:
Cordis
↓
Context
↓
基础 Service A
↙ ↘
Service B Service C
↓ ↓
Plugin D Service E
↓
Plugin F
于是整个系统可以不断进行:
使用已有 Service
↓
组合新的能力
↓
再提供新的 Service
↓
继续被其他 Plugin / Service 使用
因此,Plugin、Service 和 Context 三者之间更完整的关系可以理解为:
Plugin A
↓
提供 Service A
↓
Context
↓
Plugin / Service B 使用 Service A
↓
又可以提供 Service B
↓
Context
↓
继续被其他 Plugin 使用
所以最终可以先记住:
Plugin
=
功能实现和生命周期单元
Service
=
通过 Context 暴露的具名能力
Context
=
这些 Service 被注册、查找和连接的运行环境
以及一个非常重要的关系:
Plugin 可以不提供 Service
Service 可以不依赖其他业务 Service
Service 也可以依赖另一个 Service
一个 Plugin / Service
既可以是 Consumer
也可以是 Provider
因此 Cordis 真正组织的并不是:
Plugin A
直接调用
Plugin B
再直接调用
Plugin C
而更接近:
Plugin / Service
↓
提供能力
↓
Service
↓
Context
↓
其他 Plugin / Service
按能力进行依赖
这也是后面理解 inject 的关键:
Cordis 更关注“你需要什么 Service”,而不是“你必须依赖哪个具体 Plugin”。
2.1.1 函数式插件
最简单的函数式插件甚至可以只有:
import type { Context } from '@deepseek-ai/cordis'
export function apply(ctx: Context) {
console.log('plugin loaded')
}
这里没有:
inject
也完全合法。
这说明:
并不是所有 Plugin 都必须依赖其他 Service。
如果一个插件不需要其他业务能力,Cordis 可以直接调用:
apply(ctx)
让它开始工作。
如果这个插件必须使用其他 Service,才需要声明 inject。
例如:
import type { Context } from '@deepseek-ai/cordis'
export const name = 'my-plugin'
export const inject = ['tools', 'llm']
export f
unction apply(ctx: Context) {
// 此时 ctx.tools 和 ctx.llm 已经可用
}
这里可以先注意两个东西:
inject
apply(ctx)
其中:
inject:声明这个插件启动所必需的 Service;apply(ctx):依赖满足以后,插件真正获得 Context 并开始注册功能的位置。
需要注意,应该写:
inject = ['tools', 'llm']
而不是:
inject = ['ctx.tools', 'ctx.llm']
因为:
tools
llm
是 Service 的名字;
而:
ctx.tools
ctx.llm
是获得 Context 以后访问这些 Service 的方式。
例如:
inject = ['tools']
实际表达的是:
“我的插件工作之前,必须先存在一个名为
tools的 Service。”
如果 tools 还不存在:
Plugin
↓
PENDING
↓
等待 tools Service
等到其他模块把 tools Service 注册进 Context:
tools Service
↓
进入 Context
↓
ctx.tools 可用
↓
依赖满足
↓
执行 apply(ctx)
因此,inject 依赖的严格来说并不是“另一个具体 Plugin”。
例如:
Plugin A
↓
提供
↓
tools Service
↓
ctx.tools
↑
│
inject: ['tools']
│
Plugin B
Plugin B 真正依赖的是:
tools Service
而不是:
Plugin A
这意味着 Plugin B 不需要知道:
“到底是哪一个插件实现了 tools?”
它只需要知道:
“我启动时需要 tools 这个能力。”
如果以后换成另外一个 Plugin 来提供同名 tools Service,只要这个 Service 的接口保持一致,消费方通常不需要修改。
所以可以把 inject 记成:
插件告诉 Cordis:“我不关心是谁提供这些能力,但在我开始工作之前,请确保这些 Service 已经存在。”
如果某个 Service 只是可选能力,也可以不把它写进 inject,而是在使用时通过:
const service = ctx.get('serviceName')
判断它是否存在。
因此:
必须存在的 Service
→ inject
可有可无的 Service
→ ctx.get()
完全不需要其他 Service
→ 不写 inject
另外,Cordis 也支持把 name、inject 和 apply() 放在同一个对象中:
const myPlugin = {
name: 'my-plugin',
inject: ['tools'],
apply(ctx) {
// 插件初始化逻辑
}
}
这种严格来说属于 Object Plugin,但和函数式插件表达的核心机制一样:
声明依赖
↓
等待 Service
↓
获得 Context
↓
执行 apply(ctx)
2.1.2 Service 子类
另一种方式是让 Plugin 本身成为一个 Service:
import { Service, type Context } from '@deepseek-ai/cordis'
class MyService extends Service {
constructor(ctx: Context) {
super(ctx, 'myService')
}
hello() {
return 'hello'
}
}
这里最关键的一行是:
super(ctx, 'myService')
其中:
myService
就是这个 Service 注册到 Context 中的名字。
因此可以理解成:
MyService
↓
super(ctx, 'myService')
↓
注册 Service
↓
Context
↓
ctx.myService
其他 Plugin 就可以声明:
export const inject = ['myService']
然后使用:
export function apply(ctx: Context) {
ctx.myService.hello()
}
于是形成:
Service Plugin
↓
提供 myService
↓
Context
↓
ctx.myService
↑
│
inject: ['myService']
│
Consumer Plugin
这里还有一点很重要。
当前 Cordis 的 Service 在执行:
super(ctx, 'myService')
时,就会把当前实例注册进 Context,并纳入 Cordis 的生命周期管理。
因此它的实际关系更接近:
Service 创建
↓
注册到 Context
↓
运行
↓
所属 Plugin / Fiber 被卸载
↓
Service 自动从 Context 中移除
而不是必须自己手工写:
mount()
unmount()
再自行删除 ctx.myService。
因此 Service 的价值不仅是“写成一个 class”,更重要的是:
它允许一个 Plugin 向整个 Context 提供稳定、具名、可被其他 Plugin 注入的能力。
可以用大学社团继续理解:
Plugin
=
一个部门
Service
=
这个部门开放给其他部门的服务窗口
例如:
技术部
=
Plugin
代码执行窗口
=
Service
其他部门不需要知道技术部内部具体是谁负责实现,只需要知道:
我要使用代码执行服务
这就是:
依赖能力
而不是
依赖具体实现
2.1.3 带你读源码:Todo 插件中的核心业务逻辑
理解了 Plugin、Service、inject 和 Context 以后,我们可以真正进入一个 DSH 插件源码,看看这些概念是怎样组合起来的。
这里选择 Todo 插件作为第一个例子。
Todo 可以理解成 DSH 给 Agent 提供的一张“任务清单”。
当 Agent 面对一个比较复杂的任务时,往往不是一步就能完成,而是需要先拆成几个步骤,例如:
分析项目结构
↓
找到需要修改的代码
↓
修改代码
↓
运行测试
这时,Agent 可以用 Todo 记录这些任务目前处于什么状态。
DSH 中 Todo 主要有三种状态:
pending
→ 还没有开始
in_progress
→ 正在进行
completed
→ 已经完成
例如一个任务执行到一半时,Todo 可能类似:
分析项目结构 completed
找到相关源码 completed
修改代码 in_progress
运行测试 pending
所以 Todo 并不负责“执行任务”。
它更像是:
帮助 Agent 记录和更新当前任务计划以及执行进度的工具。
在 DSH 中,模型真正使用的是一个名为:
todo_write
的 Tool。
模型每次调用 todo_write 时,会提交当前的完整 Todo 列表。新的列表会替换之前的列表,而不是只修改其中某一项。
因此可以先把整个过程简单理解成:
Agent
↓
把复杂任务拆成多个 Todo
↓
调用 todo_write
↓
提交完整 Todo 列表
↓
Todo Plugin 处理这份列表
↓
写入当前 Session
↓
得到当前 Todo 状态
↓
界面显示任务进度
这样一来,Todo 插件就很适合拿来理解前面讲过的 Plugin、Service、Context 和 inject。
因为它自己是一个 Plugin,但工作时又需要使用其他 Service,然后再通过这些 Service 把 todo_write 等能力接入 DSH。
它的源码位于:
packages/todo/tool-todo/src/index.ts
Todo 本身并不是一个 Service 子类,而是一个函数式 Plugin。
它首先声明自己的名字和依赖:
export const name = 'tool-todo'
export const inject = [
'tools',
'sessionProjections'
]
这里:
tool-todo
是这个 Plugin 的名字。
而:
tools
sessionProjections
则是它运行时必须存在的两个 Service。
这表示 Todo Plugin 自己并不提供:
ctx.todo
这样的 Service。
相反,它需要别人已经把下面两个 Service 注册到 Context:
tools
sessionProjections
于是形成:
Context
│
├── ctx.tools
│
└── ctx.sessionProjections
↑
│
Todo Plugin
或者从依赖方向看:
Todo Plugin
│
├── 依赖 tools
│
└── 依赖 sessionProjections
这里正好可以对应前面讲过的:
inject = [
'tools',
'sessionProjections'
]
它的意思不是:
Todo Plugin
依赖两个具体 Plugin
而是:
Todo Plugin 启动之前,需要 Context 中已经存在
tools和sessionProjections这两个 Service。
如果其中一个还不存在,Todo Plugin 就不能正常开始工作。
等这两个 Service 都准备好以后,Cordis 才会执行:
apply(ctx, config)
这时候 Todo Plugin 就可以通过 Context 使用它们:
ctx.tools
ctx.sessionProjections
然后分别注册自己的能力。
首先:
ctx.tools
↓
注册 todo_write Tool
这样 Agent 才能看到并调用:
todo_write
其次:
ctx.sessionProjections
↓
注册 todos 状态投影
这个 Projection 用来根据 Session 中发生的事件,计算:
“当前这一刻的 Todo 列表应该是什么?”
所以整个关系实际上是:
Todo Plugin
│
inject 所需 Service
│
┌───────────┴───────────┐
↓ ↓
tools sessionProjections
│ │
↓ ↓
ctx.tools ctx.sessionProjections
│ │
↓ ↓
注册 todo_write Tool 注册 todos Projection
│ │
└───────────┬───────────┘
↓
Todo 功能
如果再把 Agent 和 Session 加进来,整个过程会更直观:
Agent
↓
调用 todo_write
↓
Todo Plugin
↓
验证 Todo 数据
↓
写入 todo/write 事件
↓
Session
↓
todos Projection
↓
计算当前 Todo 状态
↓
界面显示
这也正好说明:
Plugin
Service
Tool
Projection
虽然经常一起出现,但它们并不是同一个概念。
在 Todo 中:
tool-todo
→ Plugin
→ 负责把整个 Todo 功能组织起来
tools / sessionProjections
→ Service
→ Todo Plugin 运行时需要使用的已有能力
todo_write
→ Tool
→ Agent 真正能够调用的工具
todos
→ Projection
→ 根据 Session 事件计算当前 Todo 状态
所以可以简单记成:
Todo Plugin
↓
使用 Service
↓
注册 Tool 和 Projection
↓
共同实现 Todo 功能
理解了这一层以后,再看 Todo 内部真正处理数据和状态的业务逻辑就会容易很多。
这个 Todo 插件中还包含 Schema 定义、配置、工具注册和 Cordis 接入等框架性代码。如果暂时忽略这些 boilerplate,只看真正处理 Todo 数据和状态的业务逻辑,可以重点关注三个部分:
Todo 插件
│
├── toTodoList()
│ └── 验证并转换 Todo
│
├── count()
│ └── 统计不同状态的任务数量
│
└── apply()
└── 注册 Tool,并根据事件维护 Todo 状态投影
这三个部分可以先简单理解成:
Todo 数据是否合法 → 当前任务完成得怎么样 → Todo 状态怎样进入 DSH 的 Session 体系
接下来再逐个看源码。
1. 数据验证与转换:toTodoList()
首先看最核心的业务函数:
function toTodoList(
raw: { content: string; status: string }[],
allowParallel: boolean,
): TodoItem[] {
const todos: TodoItem[] = []
const seen = new Set<string>()
let active = 0
for (const item of raw) {
const content = item.content.trim()
if (content.length === 0) {
throw new Error(
'invalid todo: `content` must be a non-empty string',
)
}
if (seen.has(content)) {
throw new Error(
`invalid todos: duplicate content ${JSON.stringify(content)}`,
)
}
seen.add(content)
if (item.status === 'in_progress') active++
todos.push({
content,
status: item.status as TodoItem['status'],
})
}
if (!allowParallel && active > 1) {
throw new Error(
`invalid todos: at most one task may be in_progress (got ${active})`,
)
}
return todos
}
等效py代码(逻辑上)
import json
from typing import Literal, TypedDict, cast
TodoStatus = Literal[
"pending",
"in_progress",
"completed",
]
class RawTodoItem(TypedDict):
content: str
status: str
class TodoItem(TypedDict):
content: str
status: TodoStatus
def to_todo_list(
raw: list[RawTodoItem],
allow_parallel: bool,
) -> list[TodoItem]:
"""
验证并转换 Todo 列表。
前提:
raw 已经通过 JSON Schema 基础校验,
因此 content/status 的基本类型以及 status 枚举值已合法。
参数:
raw:
AI 返回的原始 Todo 列表。
allow_parallel:
是否允许多个任务同时处于 in_progress 状态。
返回:
合法且规范化后的 TodoItem 列表。
异常:
如果 Todo 违反业务规则,则抛出 ValueError。
"""
todos: list[TodoItem] = []
seen: set[str] = set()
active = 0
for item in raw:
# 1. 去掉任务内容首尾空白
content = item["content"].strip()
# 2. 禁止空任务
if not content:
raise ValueError(
"invalid todo: `content` must be a non-empty string"
)
# 3. 禁止重复任务
if content in seen:
duplicate = json.dumps(
content,
ensure_ascii=False,
)
raise ValueError(
f"invalid todos: duplicate content {duplicate}"
)
seen.add(content)
# 4. 统计进行中的任务
if item["status"] == "in_progress":
active += 1
# JSON Schema 已经验证 status,
# cast() 对应 TypeScript 中的 `as TodoItem['status']`
status = cast(TodoStatus, item["status"])
# 5. 构造合法 TodoItem
todos.append(
{
"content": content,
"status": status,
}
)
# 6. 非并行模式下,最多只能有一个进行中的任务
if not allow_parallel and active > 1:
raise ValueError(
"invalid todos: at most one task may be "
f"in_progress (got {active})"
)
return todos
这个函数可以概括为:
把 AI 返回的原始 Todo 数据进一步进行业务规则校验,并转换成系统内部使用的
TodoItem[]。
它接收两个输入:
raw
→ AI 返回的 Todo 列表
allowParallel
→ 当前是否允许多个任务同时处于 in_progress
最终输出:
TodoItem[]
整体处理流程为:
AI 返回 Todo
↓
遍历每个任务
↓
trim() 去除首尾空格
↓
检查内容是否为空
↓
检查任务是否重复
↓
统计 in_progress 数量
↓
构造 TodoItem
↓
检查 allowParallel
↓
返回 TodoItem[]
首先:
const content = item.content.trim()
负责清理任务内容前后的空白。
例如:
" 阅读 Cordis 文档 "
会转换成:
"阅读 Cordis 文档"
随后:
if (content.length === 0)
用于防止 AI 返回只有空白字符、实际上没有任何内容的 Todo。
接下来:
const seen = new Set<string>()
用于记录已经出现过的任务内容。
每处理一个任务,就通过:
seen.has(content)
判断相同任务是否已经存在。
例如:
阅读文档
编写代码
阅读文档
第二个“阅读文档”会被判定为重复内容,并直接抛出异常。
这里使用 Set 的原因也很直接:
它非常适合进行成员存在性检查,从而实现 Todo 内容去重。
随后:
if (item.status === 'in_progress') active++
统计当前有多少任务处于:
in_progress
遍历完成后,再根据:
allowParallel
决定这种状态组合是否合法。
如果:
allowParallel = false
那么:
任务 A in_progress
任务 B pending
是合法的。
但:
任务 A in_progress
任务 B in_progress
则违反业务规则。
对应代码就是:
if (!allowParallel && active > 1)
因此 toTodoList() 实际主要负责四件事:
① 内容规范化
② 空内容检查
③ 重复内容检查
④ in_progress 并行策略检查
这里还有一个值得注意的地方:
status: item.status as TodoItem['status']
其中:
as TodoItem['status']
是 TypeScript 的类型断言。
它并不会在程序运行时真正检查:
status 到底是否合法
而只是告诉 TypeScript:
当前这个值可以按照
TodoItem['status']类型处理。
为什么这里可以这样写?
因为 toTodoList() 的输入有一个前提:
AI 返回的数据已经经过 JSON Schema 基础校验。
因此这里实际上形成了两层不同职责的验证:
AI Output
↓
JSON Schema Validation
│
├── 数据结构
├── 字段类型
└── status 枚举
↓
toTodoList()
│
├── 内容规范化
├── 空内容检查
├── 重复检查
└── 并行策略检查
↓
TodoItem[]
所以:
JSON Schema 负责结构是否合法,
toTodoList()负责业务语义是否合法。
这也是这里最值得注意的设计点之一。
AI 输出并不是直接被系统接受,而是继续经过确定性程序规则检查。
从复杂度来看,toTodoList() 只对任务列表进行一次主要遍历,因此时间复杂度为:
O(n)
其中 n 为 Todo 数量。
seen 最多存储 n 个任务内容,因此额外空间复杂度为:
O(n)
2. 状态统计:count()
Todo 验证完成以后,还需要统计不同状态的任务数量。
源码中使用了一个简单的辅助函数:
const count = (
status: TodoItem['status'],
): number =>
todos.filter(t => t.status === status).length
它的作用就是:
统计 Todo 列表中指定状态出现了多少次。
例如现在有:
任务 A pending
任务 B in_progress
任务 C pending
任务 D completed
执行:
count('pending')
得到:
2
实际使用时可以生成:
counts: {
pending: count('pending'),
inProgress: count('in_progress'),
completed: count('completed'),
}
最终得到类似:
{
"pending": 2,
"inProgress": 1,
"completed": 1
}
算法本身就是:
Todo 列表
↓
filter(status)
↓
length
↓
数量
一次 count() 调用需要扫描整个 Todo 列表,因此时间复杂度为:
O(n)
代码中虽然分别统计三个状态,相当于进行了三次扫描,但:
3n
在大 O 表示法中仍然属于:
O(n)
补充:allowParallelInProgress 是 Todo 插件中控制任务是否可以并行进行的配置参数,源码定义在 packages/todo/tool-todo/src/index.ts,但实际使用时应在项目的 cordis.yml 中找到 Todo 插件的 config 并修改它:设为 true 时允许多个任务同时处于 in_progress,设为 false 时最多只允许一个;该配置既会影响 AI 看到的 Todo 提示词,也会在 toTodoList() 中进行运行时校验。
3. 根据事件更新 Todo 状态:apply()
Todo 插件中还有一段重要逻辑,用来根据系统中发生的事件更新当前 Todo 状态。
这段代码位于:
packages/todo/tool-todo/src/index.ts
并注册在:
ctx.inject(...)
↓
sessionProjections.register(...)
↓
apply(state, event)
对应源码为:
ctx.inject(['sessionProjections'], (projectionCtx) => {
projectionCtx.sessionProjections.register<'todos', TodoItem[] | null>({
key: 'todos',
schema: todosProjectionSchema,
init: () => null,
apply: (state, event) => {
if (event.type === 'todo/write') {
return event.data.todos
}
if (event.type === 'turn/start') {
return null
}
return state
},
view: state => state,
stateVersion: 2,
})
})
等效py代码
def register_todo_projection(projection_ctx):
def apply(state, event):
if event["type"] == "todo/write":
return event["data"]["todos"]
if event["type"] == "turn/start":
return None
return state
projection_ctx.session_projections.register(
key="todos",
schema=todos_projection_schema,
init=lambda: None,
apply=apply,
view=lambda state: state,
state_version=2,
)
ctx.inject(
["sessionProjections"],
register_todo_projection,
)
这里的核心其实就是内部的:
apply: (state, event) => {
...
}
它接收两个参数:
state → 当前 Todo 状态
event → 刚刚发生的事件
然后根据事件类型决定新的 Todo 状态。
如果收到:
todo/write
则:
return event.data.todos
也就是直接使用事件携带的新 Todo 列表。
如果收到:
turn/start
则:
return null
表示新一轮任务开始时清空上一轮 Todo。
如果是其他事件:
return state
则保持当前 Todo 不变。
因此整个逻辑可以简化为:
todo/write → 更新 Todo
turn/start → 清空 Todo
其他事件 → 保持不变
对于主要熟悉 Python 的读者,可以把它近似等效地理解成:
def update_todo_state(state, event):
if event["type"] == "todo/write":
return event["data"]["todos"]
if event["type"] == "turn/start":
return None
return state
例如当前状态是:
state = [
{"content": "查资料", "status": "in_progress"}
]
随后收到:
event = {
"type": "todo/write",
"data": {
"todos": [
{"content": "查资料", "status": "completed"},
{"content": "写报告", "status": "pending"}
]
}
}
执行:
state = update_todo_state(state, event)
新的状态就变成:
[
{"content": "查资料", "status": "completed"},
{"content": "写报告", "status": "pending"}
]
而这个 todo/write 事件来自工具执行过程中的:
exec.agent.session.append('todo/write', { todos })
因此可以把整个过程理解为:
AI 生成 Todo
↓
toTodoList() 校验
↓
合法 Todo
↓
session.append('todo/write', ...)
↓
产生 todo/write 事件
↓
apply(state, event)
↓
更新当前 Todo 状态
需要注意,源码中实际上有两个 apply:
apply(ctx, config)
→ Todo 插件的加载入口
apply(state, event)
→ 根据事件计算 Todo 状态的投影回调
这里讨论的是第二个。
所以这段代码最简单的理解就是:
execute()产生 Todo 事件,而投影中的apply()根据这些事件计算当前 Todo 应该是什么状态。
这种根据事件计算当前状态的方式,在代码中属于 状态投影(State Projection)。
4. 三个核心逻辑是怎样配合的?
把前面的三个部分连接起来,可以得到:
AI 生成 Todo
│
↓
JSON Schema Validation
│
↓
toTodoList()
│
↓
TodoItem[]
│
↓
todo/write Event
│
↓
apply()
│
↓
Todo State Projection
│
↓
count()
│
┌────────────┼────────────┐
↓ ↓ ↓
pending in_progress completed
因此它们分别承担不同职责:
| 核心逻辑 | 输入 | 输出 | 主要职责 | 时间复杂度 |
|---|---|---|---|---|
toTodoList() | raw[]、allowParallel | TodoItem[] | 规范化和业务规则验证 | O(n) |
count() | status | number | 统计指定状态 Todo 数量 | O(n) |
apply() | state、event | 新投影状态 | 根据 Event 更新 Todo Projection | O(1) |
补充说明:append() 负责告诉系统“发生了什么”,apply() 负责根据这些事件计算“现在的 Todo 状态是什么”。
这里可以看到 Todo 数据完整的处理主线:
Schema
↓
Business Validation
↓
Typed Data
↓
Event
↓
Projection
也就是:
结构校验
↓
业务校验
↓
标准化数据
↓
事件
↓
状态投影
5. 为什么不直接使用 AI 输出?
如果采用最简单的写法,可能直接:
const todos = aiOutput
但 AI 输出可能存在:
空任务
重复任务
非法 status
不符合并行策略的状态组合
因此 Todo 插件采用的思路更接近:
AI Output
↓
JSON Schema Validation
↓
toTodoList()
↓
TodoItem[]
↓
Event
↓
Projection
这里最值得注意的是:
LLM 负责产生候选结果,确定性的程序规则负责决定这些结果是否能够被系统接受。
所以不要把整个过程理解成:
LLM
↓
直接修改状态
而应该理解成:
LLM
↓
候选输出
↓
确定性验证
↓
标准化数据
↓
Event
↓
State Projection
这也是阅读 Agent 框架源码时非常重要的一个视角:
不要只关注模型生成了什么,还要继续寻找模型输出经过了哪些确定性约束,以及这些数据最终怎样进入系统状态。
6. 从这个插件应该学到什么?
Todo 插件本身的算法并不复杂。
真正值得学习的是它体现出来的数据处理边界:
AI Output
≠
可信的内部状态
而是:
AI Output
↓
Schema Validation
↓
Business Validation
↓
Typed Data
↓
Event
↓
Projection
因此,在后面阅读更复杂的 DeepSeek Harness 插件时,也可以沿用同样的方法:
先忽略部分框架样板代码
↓
找到真正处理数据的函数
↓
确定输入和输出
↓
寻找确定性校验规则
↓
寻找 Event
↓
寻找 Projection
↓
最后再回到 Cordis
理解这些逻辑是怎样被框架连接起来的
这样就不会一开始陷入大量插件注册和类型定义,而可以先抓住:
这个插件真正改变了什么数据,以及它怎样改变这些数据。
2.2 Context:插件的工作环境和服务仓库
Context 发音:英式偏 【康】泰克斯特,美式偏 【看】泰克斯特
如果 Plugin 是一个部门,那么 Context(ctx) 就可以理解成这个部门进入系统后获得的:
工作环境 + 服务目录。
Cordis Primer 对 Context 有一句很重要的定义:
A context is a repository of services.
也就是说:
Context 是 Service 的仓库。
在代码中,Context 通常写作:
ctx
插件可以通过:
ctx.<key>
访问 Context 中提供的 Service,例如:
ctx.tools
ctx.llm
ctx.sessions
可以简单理解为:
ctx.tools → 工具相关 Service
ctx.llm → LLM 相关 Service
ctx.sessions → Session 相关 Service
整体关系可以表示为:
Context(ctx)
│
┌───────────┼───────────┐
↓ ↓ ↓
ctx.tools ctx.llm ctx.sessions
│ │ │
↓ ↓ ↓
工具能力 模型能力 会话能力
Context 在代码中怎么理解?
从实现思路上,可以把 Context 近似理解成一个保存 Service 的“字典”:
services.set('tools', toolsService)
services.set('sessions', sessionService)
对于熟悉 Python 的读者,可以近似理解为:
services = {
"tools": tools_service,
"sessions": session_service,
}
因此:
ctx.tools
可以近似理解成 Python 中:
services["tools"]
也就是:
根据 Service 的名字,找到对应的服务实例。
例如 Todo 插件中:
ctx.tools.register(...)
可以拆成两步:
ctx.tools
→ 获取 tools Service
.register(...)
→ 调用这个 Service 的 register() 方法
对应的 Python 思路可以写成:
tools = services["tools"]
tools.register(...)
所以 ctx.tools.register(...) 本质上就是:
从 Context 获取工具服务,再通过它注册新的工具。
不过还有一个问题:
如果代码需要的 Service 还没有准备好怎么办?
Cordis 使用 inject 来声明这种依赖。
例如:
ctx.inject(['sessionProjections'], (projectionCtx) => {
projectionCtx.sessionProjections.register(...)
})
可以先简单理解成:
这段代码需要
sessionProjectionsService,在它可用后再执行。
对于 Python 读者,可以近似理解为:
def register_projection(projection_ctx):
projection_ctx.session_projections.register(...)
ctx.inject(
["sessionProjections"],
register_projection,
)
如果只看它表达的逻辑,则更像:
if service_is_ready("sessionProjections"):
register_projection(ctx)
else:
run_when_service_ready(
"sessionProjections",
register_projection,
)
这并不是 Cordis 的真实 Python 实现,而只是帮助理解:
inject的重点是声明“这段代码依赖哪个 Service”。
因此可以把 Context 和 inject 放在一起理解:
Service
↓
进入 Context
↓
ctx.<key>
↓
插件获取并使用 Service
如果代码依赖某个 Service
↓
inject
↓
声明依赖
↓
Service 可用后执行相关逻辑
对于熟悉 Python 的读者,整体还可以近似理解成:
# Context:保存各种服务
services = {
"tools": tools_service,
"sessions": session_service,
}
# ctx.tools.register(...)
tools = services["tools"]
tools.register(...)
# ctx.inject(...)
run_when_service_ready(
"sessionProjections",
register_projection,
)
所以这一节最核心的一句话是:
Context(
ctx)是插件获取系统 Service 的统一入口,而inject用来声明代码需要哪些 Service。
2.3 Dependency Injection:自动安排插件依赖
Dependency 发音:迪【喷】登西
Injection 发音:因【杰】克申
有了 Context 和 Service 以后,又会出现一个问题:
如果一个 Plugin 启动的时候需要
ctx.tools,但是toolsService 还没有注册进 Context,怎么办?
前面 Todo Plugin 中其实已经出现过这个问题。
再看一次它的源码:
export const name = 'tool-todo'
export const inject = [
'tools',
'sessionProjections'
]
这几行看起来很简单,但其实已经告诉 Cordis:
Todo Plugin 要开始工作之前
必须先有:
tools Service
+
sessionProjections Service
也就是说:
Todo Plugin
│
├── inject: tools
│
└── inject: sessionProjections
这里需要再次注意,inject 中写的是:
'tools'
'sessionProjections'
而不是:
'ctx.tools'
'ctx.sessionProjections'
因为前者是:
Service 的名字
后者:
ctx.tools
ctx.sessionProjections
则是 Service 注册进 Context 以后,Plugin 实际访问它们的方式。
那么问题来了:
Cordis 看到
inject = ['tools', 'sessionProjections']后,到底做了什么?
可以直接进入 Cordis 自己的源码。
在:
vendor/cordis/src/registry.ts
中,可以看到 Inject.resolve():
export namespace Inject {
export function resolve(
inject: Inject | null | undefined,
result: Dict = Object.create(null),
) {
if (!inject) return result
if (Array.isArray(inject)) {
for (const name of inject) {
result[name] = null
}
}
return result
}
}
先不用看其他分支,只看 Todo 使用的数组形式:
[
'tools',
'sessionProjections'
]
Cordis 会逐个读取:
for (const name of inject) {
result[name] = null
}
于是概念上相当于把:
inject = [
'tools',
'sessionProjections'
]
整理成:
需要的 Service:
tools
sessionProjections
也就是说,Cordis 从这里开始关心的已经不是:
哪个 Plugin 在前面
哪个 Plugin 在后面
而是:
当前这个 Plugin
需要哪些 Service?
接着继续往下看 RegistryService.plugin()。
Cordis 加载一个 Plugin 时,会执行:
const fiber = new Fiber(
this.ctx,
config,
Inject.resolve(plugin.inject),
runtime,
getOuterStack,
)
这里非常关键。
可以把它拆开看:
plugin.inject
↓
Inject.resolve()
↓
整理成 Service 依赖
↓
new Fiber(...)
也就是说:
Plugin 声明的 Service 依赖,最终会交给它自己的 Fiber 管理。
这里又出现了一个前面提到过的概念:
Fiber
暂时可以把 Fiber 理解成:
Cordis 用来管理一次 Plugin 运行实例的生命周期对象。
也就是说:
Plugin
↓
被 Cordis 加载
↓
创建 Fiber
↓
Fiber 管理:
依赖
状态
加载
卸载
清理
所以 Dependency Injection 并不是 Loader 简单看一眼:
“tools 有了吗?”
然后就结束了。
这个依赖关系会被交给 Fiber 持续管理。
继续看:
vendor/cordis/src/fiber.ts
Fiber 的状态定义中有:
export const enum FiberState {
PENDING,
LOADING,
ACTIVE,
FAILED,
DISPOSED,
UNLOADING,
}
这里第一个状态就是:
PENDING
源码注释对它的定义就是:
waiting for required services
也就是:
正在等待所需 Service。
所以一个 Plugin 被 Cordis 发现以后,并不意味着:
立刻执行 apply()
它首先可能处于:
PENDING
状态。
例如 Todo:
Todo Plugin
│
├── tools ?
│
└── sessionProjections ?
│
↓
PENDING
Fiber 创建以后,源码会继续执行:
for (const name of Object.keys(this.inject)) {
this._checkImpl(name)
}
this._refresh()
这里就已经非常直观了。
前面 Todo 的:
inject = [
'tools',
'sessionProjections'
]
经过 Inject.resolve() 以后进入 Fiber。
然后:
Object.keys(this.inject)
得到:
tools
sessionProjections
Cordis 接下来逐个执行:
this._checkImpl(name)
也就是概念上的:
检查 tools
↓
Context 中有没有?
检查 sessionProjections
↓
Context 中有没有?
因此可以把这段源码翻译成人话:
for 每一个声明的依赖 {
去 Context 里看看
这个 Service 现在有没有
}
再看 _checkImpl():
_checkImpl(name: string) {
const impl = this.ctx.reflect._getImpl(name, true)
if (!impl) {
return delete this._store[name]
}
this._store[name] = impl
}
虽然实际源码还有一些可用性检查,但核心逻辑非常清楚。
首先:
this.ctx.reflect._getImpl(name, true)
可以理解成:
去当前 Context 中寻找这个名字对应的 Service 实现。
例如:
name = "tools"
那么 Cordis 就是在确认:
当前 Context
↓
有没有 tools Service?
如果不存在:
if (!impl) {
return delete this._store[name]
}
也就是:
tools 没找到
↓
这个依赖目前不满足
如果找到:
this._store[name] = impl
就把当前找到的 Service 实现记录下来。
所以 Todo 的检查过程大概就是:
Todo Fiber
│
├── 查找 tools
│ ↓
│ 找到了?
│
└── 查找 sessionProjections
↓
找到了?
检查完以后调用:
this._refresh()
继续看 _refresh() 中最重要的部分:
for (const name of Object.keys(this.inject)) {
const impl = this._store[name]
if (!impl) {
epoch = INACTIVE
break
}
epoch += ':' + impl.fiber.uid
}
this._setEpoch(epoch)
这一段就是 Dependency Injection 真正开始决定:
这个 Plugin 到底能不能运行
的地方。
它会再次遍历所有必需 Service:
for (const name of Object.keys(this.inject))
如果其中任何一个没有找到:
if (!impl) {
epoch = INACTIVE
break
}
那么整个 Plugin 就不能进入正常工作状态。
比如:
tools ✓
sessionProjections ✗
虽然 tools 已经有了,但:
sessionProjections
还不存在。
那么结果仍然是:
Todo Plugin
↓
PENDING
而不是:
“先运行一半再说”
所以 inject 表示的是:
这些 Service 都是 Plugin 正常运行的必要条件。
只有:
tools ✓
sessionProjections ✓
全部满足以后,才能继续:
Todo Plugin
↓
LOADING
↓
执行 apply(ctx, config)
↓
ACTIVE
这里就可以回到 Todo 自己的代码。
当依赖全部满足以后:
export function apply(ctx: Context, config: Config): void {
...
}
才真正开始执行。
而这时它已经可以直接使用:
ctx.tools
ctx.sessionProjections
例如:
ctx.sessionProjections.register(...)
以及:
ctx.tools.register(...)
所以:
export const inject = [
'tools',
'sessionProjections'
]
实际上给后面的:
ctx.tools
ctx.sessionProjections
建立了一个运行时保证:
如果 apply() 已经开始执行
那么 inject 声明的这些 Service
应该已经满足可用条件
因此 Todo 的完整过程其实是:
加载 tool-todo
↓
读取 inject
↓
tools
sessionProjections
↓
Inject.resolve()
↓
交给 Fiber
↓
逐个去 Context 查找 Service
↓
├── 有缺失
│ ↓
│ PENDING
│
└── 全部存在
↓
LOADING
↓
apply(ctx, config)
↓
ACTIVE
那么 tools 又是从哪里来的?
继续看 DSH:
packages/core/tools/src/index.ts
其中有一个:
export class ToolRuntime extends Service {
static inject = ['systemPrompt']
constructor(ctx: Context, config: Config = {}) {
super(ctx, 'tools')
}
}
这里:
super(ctx, 'tools')
非常关键。
它表示这个 ToolRuntime 会向 Context 提供一个名字叫:
tools
的 Service。
而 Cordis Service 基类内部最终会执行类似:
ctx.reflect.provide(name, self, ...)
因此:
super(ctx, 'tools')
最终形成:
ToolRuntime
↓
提供 tools Service
↓
Context
↓
ctx.tools
于是现在 Todo 的整个依赖链终于能连起来了:
ToolRuntime
↓
提供
↓
tools Service
↓
Context
↓
ctx.tools
↑
│
Todo inject: ['tools']
│
Todo Plugin
所以 Todo 根本不需要知道:
ToolRuntime 这个 class
到底在哪个文件
是怎么实现的
Todo 只声明:
inject = ['tools']
意思就是:
我不关心谁实现 tools,我只要求我运行时 Context 中有 tools 这个 Service。
这正是 Dependency Injection 中“依赖能力而不是依赖具体实现”的意思。
更有意思的是,刚才的 ToolRuntime 自己又写了:
static inject = ['systemPrompt']
也就是说:
ToolRuntime
自己也是一个 Consumer。
它需要:
systemPrompt Service
同时它又提供:
tools Service
所以真实依赖链可以继续向下画:
systemPrompt Service
↓
ToolRuntime
↓
tools Service
↓
Todo Plugin
↓
todo_write Tool
这就说明 DSH 并不是简单地:
Plugin A
↓
Plugin B
↓
Plugin C
硬编码调用。
而是在逐层组合 Service:
底层 Service
↓
某个 Plugin / Service 使用
↓
组合出新的 Service
↓
再被上层 Plugin 使用
这也是为什么加载顺序不需要完全依靠:
先启动 A
再启动 B
再启动 C
最后启动 D
人工写死。
例如即使 Todo Plugin 先被 Loader 创建:
Todo Plugin
↓
发现 tools 不存在
↓
PENDING
之后 ToolRuntime 才出现:
ToolRuntime
↓
提供 tools
Cordis 仍然可以重新检查依赖。
而 _refresh() 后面的 _setEpoch() 正是在管理这种变化:
if (epoch !== INACTIVE && oldEpoch === INACTIVE) {
this.inertia = this._reload()
return FiberState.LOADING
} else {
this.inertia = this._unload()
return FiberState.UNLOADING
}
先不用纠结 epoch 的具体实现。
这里只需要看两个方向。
当原来:
依赖不满足
后来变成:
依赖满足
Cordis 会:
_reload()
↓
LOADING
↓
执行 Plugin
↓
ACTIVE
反过来,如果 Plugin 正在运行时,一个必需 Service 消失:
ACTIVE
↓
某个 inject Service 消失
↓
依赖不再满足
↓
_unload()
↓
UNLOADING
因此 inject 并不是:
“启动的时候检查一次依赖。”
而更准确地说是:
Cordis 会让 Plugin 的生命周期跟随这些 Service 的可用状态变化。
例如:
tools 不存在
↓
Todo PENDING
tools 出现
↓
Todo LOADING
↓
Todo ACTIVE
tools 消失
↓
Todo UNLOADING
tools 再出现
↓
Todo 再次 LOADING
↓
Todo ACTIVE
所以 Dependency Injection 在 Cordis 中不仅解决:
谁先启动?
它实际上还在维护:
谁依赖哪些 Service?
这些 Service 现在存在吗?
依赖满足时谁应该启动?
依赖消失时谁应该卸载?
依赖恢复以后谁应该重新运行?
整个关系可以总结成:
Provider
↓
提供 Service
↓
Context
↑
Fiber 查找 Service
↑
inject 声明需要
↑
Consumer Plugin
2.3.1 Dependency Injection 真正解决什么?
看完源码以后,再回头理解 Dependency Injection 就比较容易了。
它首先解决的是:
Plugin 到底依赖“某个具体 Plugin”,还是依赖“某种能力”?
例如 Todo 并没有写:
我依赖 ToolRuntime
它写的是:
inject = ['tools']
所以真实关系不是:
Todo Plugin
↓
ToolRuntime
而是:
ToolRuntime
↓
提供
↓
tools Service
↓
Context
↑
inject: ['tools']
↑
Todo Plugin
也就是说:
Todo 依赖的是
tools这种能力,而不是ToolRuntime这个具体实现。
第二,它解决了:
什么时候可以启动?
源码中的逻辑是:
读取 inject
↓
逐个检查 Service
↓
缺一个
→ PENDING
全部存在
→ LOADING
→ apply()
→ ACTIVE
所以加载顺序不需要简单写死成:
先 A
再 B
再 C
真正决定 Plugin 能否工作的,是:
它依赖的 Service
是否已经可用
第三,它解决了:
依赖运行过程中消失怎么办?
Cordis 的 Fiber 会继续跟踪依赖。
因此:
Service 消失
↓
Consumer 的依赖失效
↓
卸载 Consumer
如果以后 Service 恢复:
Service 恢复
↓
依赖重新满足
↓
Consumer 重新加载
所以 inject 建立的不是一次性的启动检查,而是:
Service 可用状态与 Plugin 生命周期之间的关系。
第四,它让依赖关系直接写在源码中。
例如我们第一次看到 Todo:
export const inject = [
'tools',
'sessionProjections'
]
甚至还没有继续读 apply(),就已经可以判断:
Todo 是 Consumer
它需要:
tools
sessionProjections
所以以后阅读 DSH 源码时,看到一个 Plugin,可以先看:
name
↓
它是谁?
inject
↓
它依赖谁?
apply(ctx)
↓
它拿这些 Service 做什么?
super(ctx, 'xxx')
↓
它是否又向 Context 提供新的 Service?
例如:
class ToolRuntime extends Service {
static inject = ['systemPrompt']
constructor(ctx) {
super(ctx, 'tools')
}
}
我们甚至不需要先读完整个 class,就能看出:
systemPrompt
↓
ToolRuntime
↓
tools
也就是说:
它消费 systemPrompt
同时提供 tools
这就是阅读 Cordis/DSH 源码时非常实用的一种方法。
因此,Dependency Injection 可以最终记成:
Plugin 或 Service 只声明“我需要哪些 Service”,Cordis 通过 Context 找到这些 Service,并由 Fiber 根据它们的可用状态决定什么时候加载、什么时候卸载以及什么时候重新运行。
换成一张图就是:
Provider Plugin / Service
↓
provide
↓
Service
↓
Context
↑
查找 Service
↑
Fiber
↑
inject 声明依赖
↑
Consumer Plugin
所以 Cordis 中真正被组织起来的,不是一条简单的:
Plugin A
→ Plugin B
→ Plugin C
而是一张:
Service Dependency Graph
也就是:
Service 依赖图
Plugin 和 Service 在这张图中不断:
消费已有 Service
↓
实现自己的功能
↓
有些还会提供新的 Service
↓
继续被更上层使用
这才是 Cordis 中 Dependency Injection 真正做的事情。
2.4 Typed Events:社团的大群通知系统
Typed 发音:太普特
Events 发音:伊**【文】**茨
Service 解决的是:
“我现在需要某项能力。”
但插件之间还存在另一类通信:
“某件事情已经发生了。”
这种情况下就需要 Event(事件)。
继续使用社团比喻。
假设技术部已经执行完代码。
它可以发送一个通知:
“代码执行完成!”
然后:
Code Finished
│
↓
Event System
┌──────┼──────┐
↓ ↓ ↓
Agent UI Logger
不同模块根据需要监听这个事件。
例如:
- Agent 继续下一步;
- UI 更新界面;
- Logger 写入日志;
- Telemetry 收集运行信息。
2.5 Service 和 Event 的区别
这两个概念很容易混。
最简单的区别是:
Service
=
“帮我做件事”
而:
Event
=
“有件事发生了”
例如:
| 场景 | 更适合 |
|---|---|
| 调用 LLM | Service |
| 执行工具 | Service |
| 保存数据 | Service |
| 工具执行完成 | Event |
| 用户提交消息 | Event |
| 模型流式输出发生变化 | Event |
| 策略检查 | Event / Service,取决于具体设计 |
官方 Practical Rules 中给出了一个非常实用的原则:
直接能力调用优先使用 Service Method;拦截和 Policy 更适合使用 Event。
所以:
Direct capability call
→ Service
Interception / Policy
→ Event
这个区别后面读 Harness 源码时非常重要。
2.6 Typed Events:五种事件分发模式
前面已经知道,Plugin 之间除了可以通过 Service 共享能力,还经常需要通过 Event互相通知。
例如:
Session 结束了
工具执行完了
模型请求开始了
审批结果出来了
这些事情通常不适合写成:
Plugin A
↓
直接调用
↓
Plugin B
因为发送事件的一方往往根本不知道:
到底有几个 Plugin
正在关心这个事件?
所以 Cordis 提供了 Event 系统。
不过在真正看代码之前,我们先把 Cordis 中经常出现的五个英文单词认识一下:
emit
parallel
serial
bail
waterfall
这五个单词其实已经很形象地描述了五种 Event 的工作方式。
emit 的英文原意是:
发出
发射
散发
例如:
emit light
→ 发出光
emit a signal
→ 发出信号
所以在 Cordis 中:
emit
可以理解成:
“发出一个通知。”
最简单的记法就是:
emit
=
我说一声
大家听一下
例如:
ctx.emit('session/end')
可以理解成:
“Session 结束了,
关心这件事的人都知道一下。”
parallel 的英文原意是:
平行的
并行的
例如两条平行线:
────────────→
────────────→
它们同时向前。
所以在 Cordis 中:
parallel
表示:
“多个 Listener 同时开始工作。”
最简单记成:
parallel
=
大家一起做
例如:
parallel
│
┌───────┼───────┐
↓ ↓ ↓
Listener A Listener B Listener C
而调用方会等待:
A 完成
+
B 完成
+
C 完成
以后再继续。
所以完整一点就是:
大家同时开工,我等所有人都做完。
serial 的英文原意是:
连续的
按顺序的
串行的
在计算机里经常看到:
Serial
→ 串行
也就是:
一个接一个
所以 Cordis 中的 serial 可以先理解成:
Listener A
↓
Listener B
↓
Listener C
但是它还有一个重要特点:
只要某个 Listener 给出了有效结果,就可以停止继续询问后面的 Listener。
因此最好记成:
serial
=
一个一个问
谁先给出有效答案
就用谁的答案
例如:
Strategy A
↓
没答案
↓
Strategy B
↓
有答案
↓
return
X
Strategy C 不再执行
bail 这个词第一次看到可能比较陌生。
它在英语里经常有:
退出
离开
脱身
中止
这样的意思。
例如口语中的:
bail out
就有:
退出
撤离
的感觉。
放在 Cordis 中就非常形象了:
Listener A
↓
没有结果
↓
Listener B
↓
有结果
↓
bail
↓
退出
所以可以记成:
“一旦有人给出了有效结果,就退出后面的 Listener。”
它和 serial 非常像。
区别主要在于:
serial
→ 可以 await
→ 异步串行
bail
→ 不 await
→ 同步串行
因此最简单可以记成:
bail = serial 的同步版本。
或者更口语一点:
bail
=
一个个问
有答案就撤
最后一个:
waterfall
英文原意就是:
瀑布
也就是水一层一层往下流:
上面
↓
↓
↓
中间
↓
↓
↓
下面
Cordis 中的 waterfall 也有这种:
一层一层往下传
的感觉。
但是它不是普通的:
A
↓
B
↓
C
而是每一层都有一个:
next()
例如:
Listener A
│
└── next()
↓
Listener B
│
└── next()
↓
Listener C
│
└── next()
↓
默认行为
每一个 Listener 都可以决定:
继续往下
→ next()
或者:
到我这里结束
→ 不调用 next()
所以可以把 waterfall 简单记成:
一层一层往下传,每一层都可以选择继续,也可以拦住。
因此,这五个单词可以先用一句非常简单的话记住:
emit
= 通知大家
parallel
= 大家同时做
serial
= 一个一个问,谁先有答案就停
bail
= serial 的同步版,有答案就撤
waterfall
= 通过 next() 一层一层往下传
或者用一句口诀:
emit 是“说一声”,parallel 是“一起做”,serial 是“排队问”,bail 是“有答案就撤”,waterfall 是“层层往下传”。
有了这个直觉以后,再来看 Cordis 中的 Typed Events 就容易很多了。
例如官方教程中的一个事件:
declare module '@deepseek-ai/cordis' {
interface Events {
'stats/report'(
name: string,
count: number
): void
}
}
这里实际上是在告诉 TypeScript:
事件名称
=
stats/report
参数
=
name: string
count: number
返回值
=
void
于是发送时:
ctx.emit(
'stats/report',
name,
count
)
监听时:
ctx.on(
'stats/report',
(name, count) => {
console.log(name, count)
}
)
TypeScript 都知道参数应该是什么类型。
所以这里所谓:
Typed Events
可以先简单理解成:
Event 的名称、参数和返回值都通过 TypeScript 类型明确声明。
但是 Cordis 的 Event 还有另外一个非常重要的东西:
Dispatch Mode
也就是:
这个事件到底应该怎样把消息分发给 Listener?
当前 Cordis 一共有五种主要分发模式:
emit
parallel
serial
bail
waterfall
源码中直接定义为:
export type DispatchMode =
| 'emit'
| 'parallel'
| 'serial'
| 'bail'
| 'waterfall'
因此,一个 Event 的契约不只是:
事件叫什么
+
传什么参数
还包括:
它应该怎样被 dispatch
也就是说:
Event Contract
│
├── Event Name
├── Listener Signature
└── Dispatch Mode
不同 Dispatch Mode 的行为并不一样:
| 模式 | 英文直觉 | 调用形式 | 核心语义 |
|---|---|---|---|
emit | 发出 | ctx.emit(...) | 同步广播,不等待 Listener 返回的 Promise |
parallel | 并行 | await ctx.parallel(...) | Listener 并发执行,等待全部结束 |
serial | 串行 | await ctx.serial(...) | 按顺序等待,出现有效结果立即停止 |
bail | 有结果就退出 | ctx.bail(...) | serial 的同步版本 |
waterfall | 瀑布式向下传 | ctx.waterfall(...) | Listener 通过 next() 层层包裹,可以继续或短路 |
下面再直接对着 Cordis 源码看这五种模式到底是怎么实现的。
2.7 Cordis 的整体运行逻辑:把前面的概念全部串起来
讲到这里,Plugin、Service、Context、Dependency Injection、Typed Events 和 Reversible Effects 已经分别介绍过了。
现在最重要的不是继续记更多 API,而是把它们真正连起来。
先从一个 Plugin 开始。
Plugin
它是 Cordis 管理的功能单元。
例如:
Todo Plugin
Search Plugin
LLM Plugin
Agent Plugin
一个 Plugin 被 Cordis 加载以后,会运行在自己的Context中。而 Context 又是 Plugin 获取各种能力的重要入口。
例如:
ctx.tools
ctx.llm
ctx.sessions
ctx.sessionProjections
这些:
ctx.xxx
背后对应的是一个个:
Service
所以最基本的关系是:
Plugin
↓
运行在
↓
Context
↓
访问
↓
Service
如果一个 Plugin 工作之前必须依赖某些 Service,它会通过:
inject
提前声明。
例如前面 Todo Plugin:
export const inject = [
'tools',
'sessionProjections'
]
意思是:
Todo Plugin
开始工作之前
必须存在:
tools Service
+
sessionProjections Service
然后 Cordis 会把这些依赖交给 Fiber 管理。
如果依赖还没有准备好:
Plugin
↓
PENDING
等依赖全部出现以后:
Service 准备完成
↓
Plugin LOADING
↓
执行 apply()
↓
ACTIVE
所以:
inject
解决的是:
Plugin 需要哪些能力,以及什么时候才具备运行条件。
而 Plugin 之间除了直接使用 Service,还经常需要:
Event
进行松耦合通信。
例如:
Session 结束
模型请求开始
Tool 即将执行
配置即将更新
发送者不需要知道:
到底有哪些 Plugin
正在监听?
它只负责发出 Event。
Cordis 再按照这个 Event 的 Dispatch Mode 处理 Listener。
目前我们已经看到五种:
emit
parallel
serial
bail
waterfall
可以简单记成:
emit
→ 通知大家
parallel
→ 大家同时做
serial
→ 一个一个问,有结果就停
bail
→ serial 的同步版本
waterfall
→ 通过 next() 层层包裹,可以继续,也可以短路
而 Plugin 在运行过程中还会不断做各种:
Registration
例如:
注册 Listener
注册 Tool
注册 Provider
注册 Prompt Section
注册 Watcher
这些 Registration 又应该尽量能够:
撤销
因此 Cordis 使用:
Effect
+
disposer
把:
创建
和:
清理
绑定起来。
最终就形成:
Cordis
│
↓
Plugin
│
创建 / 管理 Fiber
│
↓
Context
┌───────────┼───────────┐
│ │ │
↓ ↓ ↓
Service Event Effect
↑ │ │
│ │ ↓
inject Listener disposer
│ │
└──────────┐ │
↓ ↓
Plugin Lifecycle
│
┌───────────┴───────────┐
↓ ↓
Load Unload
↓ ↓
创建能力和注册 清理 Registration
所以前面这些概念其实分别回答了不同的问题:
Plugin
→ 谁在实现功能?
Context
→ Plugin 在什么运行环境中工作?
Service
→ 能力通过什么方式提供?
inject
→ Plugin 依赖什么能力?
Typed Events
→ Plugin 之间怎样松耦合通信?
Effect / disposer
→ Plugin 离开以后怎样把自己留下的东西清理掉?
Fiber
→ 谁把依赖、加载、卸载和 Effect 生命周期串起来?
这就是 Cordis 的主体结构。
2.7.1 Loader:这些 Plugin 又是从哪里来的?
到这里还有一个问题没有完全解决。
前面一直在讨论:
Plugin 已经进入 Cordis 以后
会发生什么?
但是实际启动 DeepSeek Harness 时,首先还需要决定:
这次到底加载哪些 Plugin?
这就是 Loader 要解决的问题。
例如一个 Profile 中可能有:
Profile
│
├── Agent Plugin
├── LLM Plugin
├── Tools Plugin
├── Session Plugin
├── Storage Plugin
└── Web Plugin
Loader 会读取这些配置,然后建立对应的 Plugin。
所以可以先把 Loader 理解成:
Cordis Plugin 的装配入口。
简单来说:
配置文件
↓
Loader
↓
决定加载哪些 Plugin
↓
ctx.plugin(...)
↓
Cordis
↓
Fiber
↓
依赖检查
↓
真正激活 Plugin
Loader 还需要处理诸如:
config
disabled
include
overlay
之类的配置机制。
例如:
disabled
可以决定:
当前这个 Plugin 到底要不要加载。
而:
config
则是 Plugin 真正运行时所使用的配置。
可以简单区分成:
disabled
→ 要不要这个 Plugin?
config
→ 如果要,它用什么配置?
另外,不同运行环境可能需要不同 Plugin 组合。
例如:
Development
Testing
Production
开发环境可能需要:
Debug Plugin
Mock Provider
而生产环境不需要。
这时比起在每个 Plugin 中写:
if development ...
if production ...
更适合让:
Base Configuration
│
├── Development Overlay
│
└── Production Overlay
去描述不同环境下的装配差异。
所以 Loader 关注的是:
哪些 Plugin
+
用什么 Config
+
当前是否启用
+
不同环境如何组合
而 Cordis Runtime 关注的是:
Plugin 加载以后
依赖是否满足
什么时候运行
什么时候卸载
怎样清理
两者不要混在一起。
2.7.2 一个 DeepSeek Harness Plugin 到底是怎样运行起来的?
现在可以把前面的知识全部放进一次完整启动流程。
假设我们启动 DeepSeek Harness。
首先读取 Profile:
Profile
│
├── Agent Plugin
├── LLM Plugin
├── Tools Plugin
├── Session Plugin
└── Web Plugin
然后 Loader 读取配置:
Profile
↓
Loader
↓
解析 Plugin 配置
↓
建立 Plugin
Plugin 被交给 Cordis 以后:
Plugin
↓
ctx.plugin(...)
↓
Registry
↓
创建 Fiber
如果 Plugin 声明了:
inject = [
'tools',
'llm'
]
Cordis 会读取这些依赖。
前面已经看到,这一步会经过:
plugin.inject
↓
Inject.resolve()
↓
统一 Service 依赖表
↓
Fiber
Fiber 接下来检查:
tools Service
llm Service
是否已经存在。
如果:
tools ✓
llm ✗
则:
Plugin
↓
PENDING
不会直接执行 apply()。
之后提供 llm 的 Service 出现:
tools ✓
llm ✓
于是:
Fiber
↓
LOADING
↓
apply(ctx, config)
↓
ACTIVE
Plugin 这时才真正开始工作。
然后 Plugin 可以通过 Context 使用已经声明的 Service:
ctx.tools
ctx.llm
也可以注册:
Event Listener
Tool
Provider
Prompt Section
其他 Effect
于是运行阶段变成:
Plugin ACTIVE
│
├── 使用 Service
│
├── 发出 Event
│
├── 监听 Event
│
└── 创建 Effect
如果用户输入:
“帮我搜索资料并总结。”
一个非常粗略的 Harness 工作链可能类似:
用户
↓
Web / Session
↓
Agent
↓
LLM Service
↓
模型生成下一步动作
↓
Tools Service
↓
Tool Pipeline
↓
相关 Event / Waterfall
↓
Tool Provider
↓
Tool Result
↓
Agent
↓
LLM
↓
最终回答
这里需要特别注意:
Cordis 并不负责决定 Agent 应该搜索什么,也不负责模型本身的推理。
它更像是下面这一层:
LLM
Tools
Agent
Sessions
Providers
Plugins
之间的:
连接系统
+
依赖系统
+
生命周期系统
所以可以理解成:
DeepSeek Harness
负责构建 AI Agent 应用
Cordis
负责让组成这个应用的模块
能够可靠地装载、连接、通信和卸载
当某个 Plugin 被更新或卸载时,流程又会反过来。
例如:
Tool Plugin v1
要替换为:
Tool Plugin v2
理想过程是:
Tool Plugin v1
↓
Fiber unload
↓
执行 disposer
↓
清理:
Listener
Tool
Provider
Effect
...
↓
环境恢复干净
↓
Tool Plugin v2
↓
创建新的 Fiber
↓
检查 inject
↓
ACTIVE
因此 Reversible Effects 在这里就真正发挥作用了。
如果没有清理:
Tool Plugin v1 Listener
+
Tool Plugin v2 Listener
那么旧版本留下的状态就可能继续影响新版本。
所以:
Dependency Injection
+
Fiber Lifecycle
+
Reversible Effects
实际上是连在一起工作的。
2.7.3 Cordis 的几个实际设计规则
理解运行流程以后,再看 Cordis 的一些设计规则会容易很多。
首先:
直接能力调用更适合放在 Service 中。
例如:
我要调用工具
→ ctx.tools
我要调用模型能力
→ 相应 LLM Service
我要访问 Session 能力
→ 相应 Session Service
也就是说:
我要某个能力
→ Service Method
而:
我要观察
我要拦截
我要修改
我要参与决策
则更适合:
Event
所以可以简单记成:
直接要能力
→ Service
多个 Plugin 要参与某个过程
→ Event
其中:
Waterfall
特别适合:
Interception
Policy
Transformation
Decision
例如:
Tool 执行之前
↓
Policy Listener
↓
允许?
/ \
是 否
↓ ↓
next() short-circuit
第二条非常重要的规则是:
Registration 应该尽量拥有对应的 disposer。
也就是说写:
怎么注册?
的时候,同时应该想:
以后怎么撤销?
例如:
+ Listener
应该对应:
- Listener
+ Watcher
应该对应:
- Watcher
+ Provider
也应该拥有相应的生命周期清理。
因此:
Registration
↓
disposer
↓
Fiber Lifecycle
是 Cordis 很重要的一条工程思路。
另外,如果多个资源之间存在明确的创建和清理顺序:
创建 A
↓
创建 B
↓
创建 C
那么卸载时通常应该:
销毁 C
↓
销毁 B
↓
销毁 A
也就是:
LIFO
后创建的资源先清理。
Cordis 的 Effect disposer 本身就按照相反注册顺序处理。
所以相关资源最好组织在清晰的 Effect 生命周期中,而不是:
这里创建一点
那里创建一点
然后希望以后记得全部删掉
2.7.4 DeepSeek Harness 和 Cordis 到底是什么关系?
现在可以把两者放进同一张图:
DeepSeek Harness
│
├── Agent
├── LLM
├── Tools
├── Sessions
├── Storage
├── Web / Headless
├── Providers
├── Adapters
│
└── Cordis
│
├── Plugin / Service
├── Context
├── Fiber
├── Dependency Injection
│ └── inject
│
├── Typed Events
│ ├── emit
│ ├── parallel
│ ├── serial
│ ├── bail
│ └── waterfall
│
├── Reversible Effects
│ └── disposer
│
└── Loader
├── config
├── disabled
├── include
└── overlay
因此可以概括成:
DeepSeek Harness 是完整的 AI Agent 应用框架,而 Cordis 是支撑 Harness 模块化插件体系运行的底层插件框架。
换句话说:
Harness
=
“这个 AI Agent 系统要做什么”
Cordis
=
“组成这个系统的模块怎样组织起来”
比如:
Agent 怎么调用模型?
Tool 怎么接入?
Session 怎么提供?
Plugin 什么时候启动?
依赖消失怎么办?
Event 怎样分发?
Plugin 卸载以后怎么清理?
其中后一类:
模块组织
依赖管理
生命周期
事件通信
资源清理
正是 Cordis 重点解决的问题。
2.7.5 再用大学社团把整个 Cordis 串一次
如果前面的代码还是觉得抽象,可以继续使用大学社团这个比喻。
| Cordis 概念 | 大学社团比喻 |
|---|---|
| Plugin | 社团里的一个部门 |
| Service | 部门对外提供的公共能力 |
| Context | 当前部门的工作环境和服务入口 |
ctx.<key> | 找到某个公共服务窗口 |
inject | 开工前声明必须具备哪些服务 |
| Dependency Injection | 自动检查开工条件和安排依赖 |
| Fiber | 记录这个部门本次任期和工作状态的管理档案 |
| Event | 社团内部广播 / 协作通知 |
emit | 群里说一声 |
parallel | 多个部门同时行动 |
serial | 一个部门一个部门问,有答案就结束 |
bail | serial 的同步版,有答案马上结束 |
waterfall | 一层层审批,通过 next() 继续 |
next() | 交给下一层 |
| Short-circuit | 当前这一层直接做最终决定 |
prepend | 把某个处理人安排到队伍最前面 |
ctx.effect() | 建立一项带清理方式的工作 |
| disposer | 任期结束时撤销这项工作 |
| Overlay | 不同场景采用不同部门组合 |
| Loader | 根据配置安排哪些部门进入系统 |
| Cordis | 整套组织制度和行政管理系统 |
把完整过程放到学校中,就是:
学校决定成立哪些部门
↓
Loader
部门进入学校
↓
Plugin
给这个部门建立工作环境
↓
Context
查看部门开工需要哪些公共能力
↓
inject
条件不足
↓
PENDING
条件满足
↓
ACTIVE
部门通过公共服务窗口工作
↓
Service
不同部门互相通知和协调
↓
Event
部门任期内产生各种工作和权限
↓
Effect
部门离开
↓
disposer
↓
清理所有遗留
这就是整个 Cordis。
2.7.6 最后只记住六句话
如果前面的源码和概念很多,一时记不住,也没有关系。
先记住下面六句话就够了。
第一:
Plugin 是 Cordis 管理的功能单元。
第二:
Context 是 Plugin 的运行环境,也是访问 Service 的入口。
第三:
Service 是通过稳定名称公开的能力,例如
ctx.tools。
第四:
Plugin 使用
inject声明自己必须依赖哪些 Service,Fiber 根据这些依赖决定什么时候加载和卸载。
第五:
Typed Events 用于插件之间的松耦合通信,目前主要有
emit、parallel、serial、bail和waterfall五种分发方式。
第六:
Plugin 创建的 Registration 应该尽量可逆,让对应 disposer 在 Fiber 卸载时把资源清理干净。
把这六句话再压缩成一张图:
Plugin
↓
Context
↓
Service
↑
inject
│
Fiber 管生命周期
│
├── Event
│
└── Effect
↓
disposer
如果这张图已经能够看懂,那么 Cordis 的主体思想基本就已经串起来了。
2.8 推荐的学习顺序与本章总结
到这里,Cordis 最核心的概念已经基本讲完了。
不过需要注意,官方的 Cordis Primer 本身并不是完整的 API 手册。
它更重要的作用是:
在真正阅读 Cordis API、DSH Plugin 和底层源码之前,先建立正确的心智模型。
因此学习 Cordis 时,不建议一开始就钻进:
vendor/cordis/src/
然后从几千行框架源码开始硬啃。
更适合的顺序是:
Cordis Primer
↓
Cordis Tutorial
↓
Subsystem / Service / Event Reference
↓
简单 Plugin 源码
↓
自己写 Plugin
↓
最后再深入 Cordis 内核源码
首先可以看:
docs/cordis-primer.md
Primer 主要帮助你建立这些基本概念:
Plugin
Service
Context
inject
Event
Effect
Lifecycle
也就是先回答:
Cordis 整体到底是怎样组织 Plugin 的?
然后再看:
docs/cordis-tutorial/index.md
Tutorial 会把前面的抽象概念真正变成代码。
例如:
定义 Plugin
↓
提供 Service
↓
通过 Context 使用 Service
↓
声明 inject
↓
注册 Event
↓
创建 Effect
等 Tutorial 跑通以后,再去看:
Subsystem Reference
Service Reference
Event Reference
这时候你看到:
ctx.tools
ctx.sessions
ctx.llm
或者:
emit
parallel
serial
bail
waterfall
就不会只把它们当成孤立的 API,而是知道它们在整个 Cordis 架构中处于什么位置。
接下来就可以挑一个比较简单的 DSH Plugin 阅读。
例如我们前面已经看过的 Todo Plugin:
packages/todo/tool-todo/src/
阅读时可以继续沿用前面总结出来的方法:
先找 name
↓
这个 Plugin 是谁?
再找 inject
↓
它依赖哪些 Service?
再看 apply(ctx)
↓
它使用这些 Service 做什么?
再找 ctx.xxx
↓
它访问了哪些能力?
再找 register / on / effect
↓
它向系统注册了什么?
最后看 disposer / Fiber
↓
这些东西卸载时怎样清理?
这样读源码会比从第一行开始逐字翻译有效得多。
最后,再尝试自己写一个最小 Plugin。
例如:
import type { Context } from '@deepseek-ai/cordis'
export const name = 'hello-plugin'
export function apply(ctx: Context) {
console.log('hello cordis')
}
然后慢慢加入:
inject
↓
Service
↓
Event
↓
Effect
这样 Cordis 的各个概念就会真正连起来。
所以推荐的学习路线可以压缩成:
先建立概念
↓
Primer
再跑真实代码
↓
Tutorial
再查具体接口
↓
Reference
再看真实实现
↓
DSH Plugin
再自己动手
↓
自己写 Plugin
最后深入框架
↓
Cordis Source
也就是说:
学习 Cordis 最好的方式不是死记 API,而是先理解架构,再通过真实 Plugin 把这些概念串起来。
到这里,也可以对这一章做最后一次总结。
Cordis 最核心的问题并不是:
“AI 怎么推理?”
也不是:
“模型应该调用哪个 Tool?”
这些属于 Agent、LLM 和具体业务逻辑需要解决的问题。
Cordis 真正关心的是:
“组成 AI Agent 的各种功能模块,怎样可靠地生活在同一个系统里?”
例如:
Plugin 怎么进入系统?
Plugin 怎样获得其他能力?
Plugin 之间怎样通信?
依赖还没准备好怎么办?
依赖运行过程中消失怎么办?
Plugin 注册的 Listener 和 Tool 谁来清理?
Plugin 更新以后怎样避免旧资源残留?
Cordis 通过前面讲过的几个核心机制来回答这些问题:
Plugin / Service
+
Context
+
Dependency Injection
+
Typed Events
+
Reversible Effects
+
Fiber Lifecycle
把它们全部连起来,可以画成:
Cordis
│
↓
Plugin
│
创建 Fiber
│
↓
Context
│
┌───────────────┼───────────────┐
↓ ↓ ↓
Service Events Effects
│ │ │
│ ┌───────┼───────┐ ↓
│ ↓ ↓ ↓ disposer
│ emit parallel serial
│ │
│ bail
│ │
│ waterfall
│ │
│ next()
│
↑
inject
│
↓
Dependency Injection
│
↓
Fiber 管理依赖状态
│
┌──────┴──────┐
↓ ↓
Load Unload
↓ ↓
ACTIVE 清理 Effects
如果再从 Plugin 的角度看一次:
Plugin 被 Loader 创建
↓
Cordis 创建 Fiber
↓
读取 inject
↓
检查所需 Service
↓
是否满足?
/ \
否 是
↓ ↓
PENDING LOADING
↓
apply(ctx)
↓
ACTIVE
↓
┌─────────┼─────────┐
↓ ↓ ↓
使用 Service Event Effect
↓
disposer
↓
Plugin Unload
↓
自动清理
所以这些概念并不是互相独立的。
例如:
Context
不是单纯一个“装变量的对象”。
它连接着:
Service
Event
Effect
Fiber
而:
inject
也不是单纯一个配置数组。
它最终会影响:
Fiber
↓
PENDING
↓
LOADING
↓
ACTIVE
↓
UNLOADING
整个 Plugin Lifecycle。
Event 同样不是简单的“消息通知”。
根据不同 Dispatch Mode,它可以承担:
emit
→ 广播通知
parallel
→ 并发协作
serial
→ 异步顺序决策
bail
→ 同步顺序决策
waterfall
→ 环绕、修改、拦截和短路
而 ctx.on() 创建的 Listener 又不是永久留在系统里的。
它最终还会进入:
Fiber
↓
Effect
↓
disposer
从而在 Plugin 离开时自动清理。
所以 Cordis 最值得理解的,并不是某一个 API,而是这一整套:
装载
↓
依赖
↓
运行
↓
通信
↓
注册
↓
清理
的生命周期模型。
最终可以用一句话概括:
Cordis 是 DeepSeek Harness 底层负责 Plugin、Service、Context、Dependency Injection、Typed Events、Effect 和 Plugin Lifecycle 管理的插件框架。
再口语一点:
DeepSeek Harness 负责构建“AI Agent 能做什么”,Cordis 负责管理“这些功能模块怎样装进来、怎样找到彼此、怎样协作,以及不用以后怎样安全拆掉”。
如果只想记住整章最核心的一张图,可以记这一张:
Cordis
│
Plugin
│
Fiber
│
Context
┌────────┼────────┐
↓ ↓ ↓
Service Event Effect
↑ ↓
inject disposer
│ │
└────────┬─────────┘
↓
Plugin Lifecycle
如果这张图能够看懂,那么 Cordis 的主体思想基本就已经理解了。
附:本章常见英文词发音
| 单词 | 中文近似 | 含义 |
|---|---|---|
| Cordis | 【考】迪斯 | 插件框架名称 |
| Plugin | 【普】拉金 | 插件 |
| Context | 【康】泰克斯特 | 上下文 / 运行环境 |
| Service | 【色】维斯 | 服务 / 能力 |
| Dependency | 迪【喷】登西 | 依赖 |
| Injection | 因【杰】克申 | 注入 |
| Inject | 因【杰】克特 | 声明 / 注入依赖 |
| Resolve | 瑞【造】夫 | 解析 / 确定 |
| Event | 伊【文】特 | 事件 |
| Emit | 伊【米】特 | 发出 |
| Parallel | 【派】若莱尔 | 并行 |
| Serial | 【西】瑞欧 | 串行 |
| Bail | 【贝】尔 | 退出 / 中止 |
| Waterfall | 【沃】特佛 | 瀑布 |
| Effect | 伊【费】克特 | Effect / 副作用 |
| Reversible | 瑞【沃】瑟伯 | 可逆的 |
| Dispose | 迪【斯波兹】 | 清理 / 释放 |
| Disposer | 迪【斯波】泽 | 清理函数 |
| Loader | 【漏】德 | 加载器 |
| Overlay | 【欧】弗雷 | 覆盖层 |
| Lifecycle | 【莱】夫赛口 | 生命周期 |
| Fiber | 【发】伊伯 | Fiber / 插件运行实例 |
| Harness | 【哈】尼斯 | Harness |
| Agent | 【埃】真特 | 智能体 |
| Profile | 【普】肉发奥 | 配置组合 |

2224

被折叠的 条评论
为什么被折叠?



