DeepSeek Harness(dsh)是由 DeepSeek AI 开发的开源 agent harness。它采用一切皆插件的架构,并由 Cordis 驱动,其设计参见论文 A Programming Paradigm for Spatiotemporal Composability。Cordis 本质上是插件的的执行内核,Harness 则是基于 Cordis 的完整代理运行框架。理解 Cordis 就是理解 Harness 的底层机制:插件化、上下文共享、配置驱动、事件通信。这让开发者能灵活组合模型与工具,快速构建可扩展的 AI Agent 系统。在这个系列中,我将从源码为出发点,从各个角度Cordis的设计和实现。
1. 从Context说起
Cordis下的插件究竟是一个怎样的对象呢?要回答这个问题,就必须先了解Cordis整个架构体系一个最核心的对象,那就是作为上下文的Context对象。在 Cordis 框架下,Context 是它的核心抽象之一,可以理解为 插件运行的统一环境与依赖容器。它的作用和地位主要体现在以下几个方面:
- 统一环境:所有插件都挂载在同一个 Context 上,形成共享的运行空间;
- 服务解析:插件通过 Context 提供的服务解析器来访问其他插件暴露的功能(如日志、事件、注册表);
- 事件总线:Context 内置事件系统,支持
ctx.on、ctx.emit等方法,插件间通信通过它完成; - 依赖注入:插件可以通过
ctx.inject获取其他服务,避免硬编码依赖; - 生命周期管理:Context 负责插件的加载、卸载、资源清理,保证运行时稳定。
Context在Cordis中被定义一个类,由于采用了基于代理的机制动态解析成员调用,让这个类型拥有了极强的扩展性,我们几乎可以将任意对象挂载到Context对象上。我们可以将Context视为一个依赖注入容器,或者说我们可以将整个Cordis视为一个建立在依赖注入容器上的插件系统。我们通过相应的服务注册API在容器上注册相应的服务,服务的消费者则从这个容器中提取依赖服务对象。为了解决多个同名服务并存的问题(针对同一个名称注册了不同的服务实例供不同的插件使用),Context采用树形层次化设计。
- 顶层是根Context,包含日志、注册表、事件总线和服务接口;
- 根Context 向下派生出多个 子 Context,每个子 Context 可以挂载不同的插件(模型适配器、工具插件、事件钩子等);
- 所有子 Context 最终共享同一个 上下文空间,保证服务可继承、可覆盖,同时保持隔离性;
这种设计的优势在于:
- 层次化:父子 Context 形成树状结构,支持继承与隔离;
- 灵活性:不同子 Context 可以挂载不同插件,互不干扰;
- 统一性:共享 Context 空间保证服务调用一致,避免碎片化;
针对Context的介绍会贯穿整个系列,这里我们简单看看定义了核心成员的Context接口的定义:
interface Context {
[symbols.isolate]: Dict<symbol>
[symbols.intercept]: Dict
root: this
baseUrl?: string
events: EventsService
logger: LoggerService
reflect: ReflectService
registry: RegistryService
}
定义在Context接口中的核心成员包括:
- [symbols.isolate]:维护服务隔离映射表。当某个服务需要在不同作用域下拥有独立实例(例如不同模型或插件),上面介绍的同名服务在不同 Context 下可独立存在,互不干扰就通过它来实现;
- [symbols.intercept]:存储服务的拦截配置。通过
intercept方法,插件可以在加载时注入额外配置或修改服务行为。利用这个字典实现插件级的配置合并与行为定制,而不影响父级 Context; - root: 指向应用的根 Context。无论当前 Context 层级多深,都能通过
ctx.root访问全局服务与配置,它保证所有子 Context 都能追溯到统一的根环境; - baseUrl: 定义插件或模块的基础路径。用于解析相对路径的模块加载或资源定位,它让插件能在不同运行环境下正确定位依赖;
- events:事件总线服务。提供
ctx.on、ctx.emit等方法,实现插件间通信,促成了Cordis松耦合的事件驱动架构; - logger:日志服务。通过
ctx.logger(name)获取命名日志器,支持分级与上下文追踪,提供统一日志输出,方便调试与监控; - reflect:反射层服务。支撑上面提到过的基于代理机制的动态属性解析,它让 Context 能像对象一样直接访问服务;
- registry:插件注册表。负责插件的加载、卸载与依赖管理。它形成 Cordis 的插件树结构,是整个系统的“调度中心”。
2. 插件的表现形式
Context相当于Cordis这个插件内核的执行上下文,它提供了注册、执行和释放插件所需的运行时信息和服务对象,所以插件本质上可以视为针对Context上下文的一项操作。由于我们在定义插件的时候,一般会提供相应的配置来控制其执行行为,所以作为插件操作的输入还应该包括配置。除此之外,插件还需要一些额外的元数据信息。从这个角度去理解Cordis的插件类型就很容易了。如下面的代码所示,表示插件的Plugin是由三个泛型接口组成的联合,类型参数T表示的是插件对应配置的类型。
export type Plugin<T = any> =
| Plugin.Function<T>
| Plugin.Constructor<T>
| Plugin.Object<T>
表示三种插件形态的接口都是针对如下这个Base接口的扩展,该接口定义了一个插件的基本结构。由于Base接口定义的所有成员都是可缺省的,所以具体的插件类型可以针对性地提供:
export namespace Plugin {
export interface Base<T = any> {
name?: string
Config?: StandardSchemaV1<any, T>
inject?: Inject
provide?: string | string[]
intercept?: Dict<boolean>
}
}
Base接口定义的成员说明如下:
- name:插件的名称标识。用于在注册表中区分不同插件,便于调试和依赖管理。保证插件在 Context 中有唯一可识别的身份;
- Config:插件的配置模式(Schema)。定义插件可接受的配置结构和类型,确保配置合法性。提供类型安全和配置验证,避免运行时错误。很明显这里命名错误,应该是config;
- inject:依赖注入声明。指定插件运行所需的外部服务或其他插件。通过 Context 自动解析依赖,避免硬编码耦合。
- provide:插件对外提供的服务名称。声明插件能暴露哪些功能接口,供其他插件使用。形成服务契约,实现插件间的协作。
- intercept:拦截器配置。声明插件是否拦截某些服务调用或事件。允许插件在运行时修改或增强其他服务的行为。
2.1 函数形式
函数形式作为简单明了,只需要提供一个将Context和配置对象作为输入的函数即可,对应与如下这个Plugin.Function<T>接口。
export namespace Plugin {
export interface Function<T = any> extends Base<T> {
(ctx: Context, config: T): any
}
}
在如下的演示程序中,我们注册的插件体现为greet函数。该函数具有上述的签名,作为配置的类型需要利用timeOfDay提供表示输出问候语的时间部分(morning、afternoon和evening)。整个插件的目的就是在控制台输出一条由提供配置格式化的问候语。
import { Context } from '@deepseek-ai/cordis'
import * as readline from "readline"
function greet (context: Context, config: {timeOfDay:string}){
console.log(`Good ${config.timeOfDay}`)
}
async function main() {
const ctx = new Context();
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout});
const loop = function(){
rl.question("Enter time of day or exit: ", (answer) => {
if(answer == "exit"){
rl.close()
} else{
ctx.registry.plugin(greet, {timeOfDay: answer});
setTimeout(()=>loop(),1000);
}
});
}
loop();
}
main();
我们在main函数中我们创建了一个Context对象,并构建了一个循环,利用输入来指定greet插件的配置。我们在注册插件的时候,利用Context的registry属性得到表示注册服务的RegistryService对象,并调用其plugin方法完成插件的注册。执行这个程序,以插件配置的形式输入不同的时间,注册的插件会输出相应的问候语。
Enter time of day or exit: morning
Good morning
Enter time of day or exit: afternoon
Good afternoon
Enter time of day or exit: evening
Good evening
2.2 类形式
插件的第二种形式体现为如下这个Plugin.Constructor<T>接口,它包含一个具有与上述函数相同签名的构造函数。
export namespace Plugin {
export interface Constructor<T = any> extends Base<T> {
new (ctx: Context, config: T): any
}
}
这意味着我们可以将插件定义成一个具有指定构造函数的类。所谓上面演示的通过函数greet定义的插件,也可以通过如下这个greeter类来定义。
import { Context } from '@deepseek-ai/cordis'
import * as readline from "readline"
class geeter{
constructor(context: Context, config: {timeOfDay:string}){
console.log(`Good ${config.timeOfDay}`) ;
}
}
async function main() {
const ctx = new Context();
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout});
const loop = function(){
rl.question("Enter time of day or exit: ", (answer) => {
if(answer == "exit"){
rl.close()
} else{
ctx.registry.plugin(geeter, {timeOfDay: answer});
setTimeout(()=>loop(),1000);
}
});
}
loop();
}
main();
2.3 对象形式
插件的第三种形式体现为如下所示的Plugin.Object<T>接口,该接口必须包含一个名为apply的方法,该函数同样具有与上面一致的签名。
export namespace Plugin {
export interface Object<T = any> extends Base<T> {
apply(ctx: Context, config: T): any
}
}
如果采用对象形式,上面演示程序针对插件的注册可以改成如下的形式。
import { Context } from '@deepseek-ai/cordis'
import * as readline from "readline"
function greet (context: Context, config: {timeOfDay:string}){
console.log(`Good ${config.timeOfDay}`)
}
async function main() {
const ctx = new Context();
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout});
const loop = function(){
rl.question("Enter time of day or exit: ", (answer) => {
if(answer == "exit"){
rl.close()
} else{
ctx.registry.plugin({apply : greet}, {timeOfDay: answer});
setTimeout(()=>loop(),1000);
}
});
}
loop();
}
main();
2.4 插件的最终表现形式
虽然我们可以采用上述三种方式来定义和注册插件,但是对于Cordis来说,它所谓的插件就是一个单纯的函数。两者之间的转换体现在如下这个resolve函数中:
- 如果原始形态就是函数,直接使用该函数;
- 对于其它两种形式,取其
apply成员(构造函数的apply成员或者插件对象的apply方法)。
resolve(plugin: Plugin): Function | undefined {
try {
if (typeof plugin === 'function') return plugin
if (isApplicable(plugin)) return plugin.apply
} catch {}
}
function isApplicable(object: Plugin) {
return object && typeof object === 'object' && typeof object.apply === 'function'
}
3. 插件函数的返回值
我们再来看看函数和对象形式的插件接口Plugin.Function<T>和Plugin.Object<T>的定义,可以前者和后者的apply方法对返回类型都没有限制。由于插件是可拔插的组件,我们希望当插件被卸载的时候,它引用的资源能够全部释放掉。在这种情况下,我们可以让函数返回一个执行清理工作的函数。
export namespace Plugin {
export interface Function<T = any> extends Base<T> {
(ctx: Context, config: T): any
}
export interface Object<T = any> extends Base<T> {
apply(ctx: Context, config: T): any
}
}
由于插件的生命周期管理由其关联的Fiber对象来负责,实际上RegistryService的plugin方法就返回这么一个对象。具体可以参考我的文章DeepSeek Harness插件内核-03:利用Fiber管理Context和插件的生命周期。在如下演示程序中,我们分别以函数和对象形式注册了两个插件,并得到对应的Fiber对象。插件函数都返回了一个函数输出一段相应的执行性文本。在插件注册,我们等待一秒让插件正常执行,在先后调用两个Fiber对象的dispose方法。从输出的结果可以看出,插件函数返回的函数会因Fiber对象的释放被调用。
import { Context } from '@deepseek-ai/cordis'
const context = new Context();
const fiber1 = context.registry.plugin(ctx =>{
console.log("Plugin1 executes...");
return ()=>console.log("Plugin1 is disposed...");
})
const fiber2 = context.registry.plugin({
apply(ctx:Context){
console.log("Plugin2 executes...");
return ()=>console.log("Plugin2 is disposed...");
}
})
setTimeout(() => {
console.log("Fiber1 is disposed...")
fiber1.dispose();
console.log("Fiber2 is disposed...")
fiber2.dispose();
}, 1000);
输出:
Plugin1 executes...
Plugin2 executes...
Fiber1 is disposed...
Fiber2 is disposed...
Plugin1 is disposed...
Plugin2 is disposed...
4. 针对类形式插件的特殊处理
再来看看表示类形式插件的Plugin.Constructor<T>接口,它仅仅要求提供一个指定签名的构造函数。构造函数最终的使命是创建对象,有没有想过生成的这个对象对插件本身有何作用?
export namespace Plugin {
export interface Constructor<T = any> extends Base<T> {
new (ctx: Context, config: T): any
}
}
其实我们可以使用两个预定义的Symbol为通过生成的对象设置两个特殊的成员,一个利用symbols.initHooks定义一个属性用来提供一组会在初始化自动执行的构造函数,另一个则是利用symbols.init定义的属性来提供插件的返回值,作为插件卸载时执行清理工作的处理函数。具体实现体现在如下的演示程序中:
class Plugin{
[symbols.initHooks] = [
()=>console.log("Initial hook 1 execute..."),
()=>console.log("Initial hook 2 execute...")];
[symbols.init] = ()=>console.log("Plugin is disposed...");
constructor(ctx: Context){
console.log("Plugin initialize...");
}
}
const context = new Context();
const fiber = context.registry.plugin(Plugin);
setTimeout(() => {
console.log("Fiber is disposed...")
fiber.dispose();
}, 1000);
process.stdin.resume();
输出:
Plugin initialize...
Initial hook 1 execute...
Initial hook 2 execute...
Plugin is disposed...
Fiber is disposed...

386

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



