DeepSeek Harnessdsh是由 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视为一个依赖注入容器或者说可以将整个Cordis视为一个建立在依赖注入容器上的插件系统。我们通过相应的服务注册API在容器上注册相应的服务服务的消费者则从这个容器中提取依赖服务对象。为了解决多个同名服务并存的问题针对同一个名称注册了不同的服务实例供不同的插件使用Context采用树形层次化设计顶层是根Context包含日志、注册表、事件总线和服务接口;根Context向下派生出多个子Context每个子Context可以挂载不同的插件模型适配器、工具插件、事件钩子等;Context的继承与隔离特性保证服务可继承、可覆盖同时保持隔离性。这种设计的优势在于层次化父子Context形成树状结构支持继承与隔离;灵活性不同子Context可以挂载不同插件互不干扰;统一性共享Context空间保证服务调用一致避免碎片化。针对Context的介绍会贯穿整个系列这里我们简单看看定义了核心成员的Context接口的定义interfaceContext{[symbols.isolate]:Dictsymbol[symbols.intercept]:Dict root:thisbaseUrl?:stringevents:EventsService logger:LoggerService reflect:ReflectService registry:RegistryService}定义在Context接口中的核心成员包括[symbols.isolate]维护服务隔离映射表。当某个服务需要在不同作用域下拥有独立实例要求同名服务可以在不同Context下可独立存在互不干扰[symbols.intercept]存储服务的拦截配置。通过intercept方法插件可以在加载时注入额外配置或修改服务行为。利用这个字典实现插件级的配置合并与行为定制而不影响父级Contextroot 指向应用的根Context。无论当前Context层级多深都能通过ctx.root访问全局服务与配置它保证所有子Context都能追溯到统一的根环境baseUrl 定义插件或模块的基础路径。用于解析相对路径的模块加载或资源定位它让插件能在不同运行环境下正确定位依赖events事件总线服务。提供ctx.on、ctx.emit等方法实现插件间通信促成了Cordis松耦合的事件驱动架构logger日志服务。通过ctx.logger(name)获取命名日志器支持分级与上下文追踪提供统一日志输出方便调试与监控reflect反射层服务。支撑上面提到过的基于代理机制的动态属性解析它让Context能像对象一样直接访问服务registry插件注册表。负责插件的加载、卸载与依赖管理。2. 插件的表现形式Context相当于Cordis这个插件内核的执行上下文它提供了注册、执行和释放插件所需的运行时信息和服务对象。插件本质上是注册到某个Context中并在此上下文中执行的操作插件一般会有对应的配置和元数据。表示插件的Plugin是由如下三个泛型接口组成的联合类型参数T表示的是插件对应配置的类型。exporttypePluginTany|Plugin.FunctionT|Plugin.ConstructorT|Plugin.ObjectT表示三种插件形态的接口都是针对如下这个Base接口的扩展该接口定义了插件的基本结构它的所有成员都是可缺省的。exportnamespacePlugin{exportinterfaceBaseTany{name?:stringConfig?:StandardSchemaV1any,Tinject?:Inject provide?:string|string[]intercept?:Dictboolean}}exporttypeInjectMDict(keyofM)[]|{[KinkeyofM]?:M[K]}Base接口定义的成员说明如下name插件的名称标识;Config插件的配置Schema;inject注入的依赖服务声明。可以是注入的服务名称列表有可以是同时携带配置的字典provide插件对外提供的服务名称列表intercept可拦截服务声明指明可以消费哪些服务的拦截配置。2.1 函数形式函数形式作为简单明了只需要提供一个将Context和配置对象作为输入的函数即可对应与如下这个Plugin.FunctionT接口。exportnamespacePlugin{exportinterfaceFunctionTanyextendsBaseT{(ctx:Context,config:T):any}}在如下的演示程序中我们注册的插件体现为定义的greet函数配置类型利用timeOfDay字段提供表示输出问候语的时间部分morning、afternoon和evening。整个插件的目的就是在控制台输出一条根据指定时间生成的的问候语。import{Context}fromdeepseek-ai/cordisimport*asreadlinefromreadlinefunctiongreet(context:Context,config:{timeOfDay:string}){console.log(Good${config.timeOfDay})}asyncfunctionmain(){constctxnewContext();constrlreadline.createInterface({input:process.stdin,output:process.stdout});constloopfunction(){rl.question(Enter time of day or exit: ,(answer){if(answerexit){rl.close()}else{ctx.plugin(greet,{timeOfDay:answer});setTimeout(()loop(),1000);}});}loop();}main();我们在main函数中我们创建了一个Context对象并构建了一个循环利用输入来指定greet插件的配置。我们在注册插件的时候利用Context的registry属性得到表示注册服务的RegistryService对象并调用其plugin方法完成插件的注册。执行这个程序以插件配置的形式输入不同的时间注册的插件会输出相应的问候语。Entertimeof day or exit: morning Good morning Entertimeof day or exit: afternoon Good afternoon Entertimeof day or exit: evening Good evening由于ctx.plugin方法会以混入Mixin的形式映射到ctx的plugin方法所以我们一般会直接调用ctx.plugin方法来注册插件。2.2 类形式插件的第二种形式体现为如下这个Plugin.ConstructorT接口它包含一个具有与上述函数相同签名的构造函数。exportnamespacePlugin{exportinterfaceConstructorTanyextendsBaseT{new(ctx:Context,config:T):any}}这意味着我们可以将插件定义成一个具有指定构造函数的类。所谓上面演示的通过函数greet定义的插件也可以通过如下这个greeter类来定义。import{Context}fromdeepseek-ai/cordisimport*asreadlinefromreadlineclassgeeter{constructor(context:Context,config:{timeOfDay:string}){console.log(Good${config.timeOfDay});}}asyncfunctionmain(){constctxnewContext();constrlreadline.createInterface({input:process.stdin,output:process.stdout});constloopfunction(){rl.question(Enter time of day or exit: ,(answer){if(answerexit){rl.close()}else{ctx.plugin(geeter,{timeOfDay:answer});setTimeout(()loop(),1000);}});}loop();}main();2.3 对象形式插件的第三种形式体现为如下所示的Plugin.ObjectT接口该接口必须包含一个名为apply的方法该函数同样具有与上面一致的签名。exportnamespacePlugin{exportinterfaceObjectTanyextendsBaseT{apply(ctx:Context,config:T):any}}如果采用对象形式上面演示程序针对插件的注册可以改成如下的形式。import{Context}fromdeepseek-ai/cordisimport*asreadlinefromreadlinefunctiongreet(context:Context,config:{timeOfDay:string}){console.log(Good${config.timeOfDay})}asyncfunctionmain(){constctxnewContext();constrlreadline.createInterface({input:process.stdin,output:process.stdout});constloopfunction(){rl.question(Enter time of day or exit: ,(answer){if(answerexit){rl.close()}else{ctx.plugin({apply:greet},{timeOfDay:answer});setTimeout(()loop(),1000);}});}loop();}main();2.4 插件的最终表现形式虽然我们可以采用上述三种方式来定义和注册插件但是对于Cordis来说插件就是一个单纯的函数。两者之间的转换体现在如下这个resolve函数中具体的转换逻辑如下如果原始形态就是函数直接使用该函数对于其它两种形式取其apply成员构造函数的apply成员或者插件对象的apply方法。resolve(plugin:Plugin):Function|undefined{try{if(typeofpluginfunction)returnpluginif(isApplicable(plugin))returnplugin.apply}catch{}}functionisApplicable(object:Plugin){returnobjecttypeofobjectobjecttypeofobject.applyfunction}3. 插件函数的返回值我们再来看看函数和对象形式的插件接口Plugin.FunctionT和Plugin.ObjectT的定义可以看出两者的apply方法对返回类型都没有限制any。由于插件是可拔插的组件我们希望当插件被卸载的时候它引用的资源能够全部释放掉。在这种情况下我们可以让函数返回一个执行清理工作的函数。exportnamespacePlugin{exportinterfaceFunctionTanyextendsBaseT{(ctx:Context,config:T):any}exportinterfaceObjectTanyextendsBaseT{apply(ctx:Context,config:T):any}}插件的生命周期管理由其关联的Fiber对象来负责RegistryService的plugin方法就返回这么一个对象具体可以参考我的文章DeepSeek Harness插件内核-03:利用Fiber管理Context和插件的生命周期。在如下演示程序中我们分别以函数和对象形式注册了两个插件并得到对应的Fiber对象。插件函数都返回了一个函数输出一段相应的指示性文本。在完成插件注册后我们等待一秒让插件正常执行再先后调用两个Fiber对象的dispose方法。从输出的结果可以看出插件函数返回的函数会因Fiber对象的释放被调用。import{Context}fromdeepseek-ai/cordisconstcontextnewContext();constfiber1context.registry.plugin(ctx{console.log(Plugin1 executes...);return()console.log(Plugin1 is disposed...);})constfiber2context.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.ConstructorT接口它仅仅要求提供一个指定签名的构造函数。构造函数最终的使命是创建对象有没有想过生成的这个对象对插件本身有何作用exportnamespacePlugin{exportinterfaceConstructorTanyextendsBaseT{new(ctx:Context,config:T):any}}其实我们可以使用两个预定义的Symbol为插件对象定义两个特殊成员symbols.initHooks初始化自动执行的钩子函数symbols.init用于提供插件卸载时执行清理工作的回调函数。具体实现体现在如下的演示程序中classPlugin{[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...);}}constcontextnewContext();constfibercontext.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...
阅读完成 · 觉得有帮助?