[DeepSeek Harness插件内核-12]这算Cordis一个大BUG吗?[续]

前文中,我们介绍了通过调用isolate方法创建的子Context因与父Context共享同一个Fiber导致隔离能力丧失的问题。在文中为了演示这个问题,我刻意调用inject方法而不是plugin方法来为子Context注册插件,这是因为后者注册的插件根本无法执行。

1. 调用plugin方法注册的插件无法执行

以如下的演示程序为例,我们创建了一个作为根的Context,并采用指定的名称foobar注册了一个基于函数的服务,函数输出字符串 foo; 然后我们调用Contextisolate方法针对服务名称foobar创建了一个与根Context隔离的子Context,并在子Context上面使用相同的名称注册了另一个函数,后者输出bar。我们在子Context注册了一个插件,该插件会从当前Context中提取并执行注册的foobar服务函数。为了确认插件对应Fiber的状态,我们分别在得到该Fiber的时候以及一秒后两次输出它的状态。

import { Context} from '@deepseek-ai/cordis'

declare module '@deepseek-ai/cordis'{
interface Context {
    foobar: ()=>void;
    }
}

const context = new Context();
context.provide("foobar", ()=>console.log("foo"));
const subContext = context.isolate("foobar");
subContext.provide("foobar", ()=>console.log("bar"));

const FiberStateNames = ['PENDING', 'LOADING', 'ACTIVE', 'FAILED', 'DISPOSED', 'UNLOADING'];
const fiber = subContext.plugin(ctx=>ctx.foobar()); 
console.log(FiberStateNames[fiber.state]);
setTimeout(()=>console.log(FiberStateNames[fiber.state]),1000);

输出:

LOADING
FAILED

从输出结果可以看出Cordis试图加载注册的插件,但是失败了。

2. 捕捉插件执行异常

为了捕捉插件在执行过程中究竟发生了怎样的异常,我们将插件函数针对注册服务的调用封装在如下所示的try/catch块中。

import { Context} from '@deepseek-ai/cordis'

declare module '@deepseek-ai/cordis'{
interface Context {
    foobar: ()=>void;
    }
}

const context = new Context();
context.provide("foobar", ()=>console.log("foo"));
const subContext = context.isolate("foobar");
subContext.provide("foobar", ()=>console.log("bar"));

const FiberStateNames = ['PENDING', 'LOADING', 'ACTIVE', 'FAILED', 'DISPOSED', 'UNLOADING'];
const fiber = subContext.plugin(ctx=>{
    try{
        ctx.foobar();
    }catch(e){
        console.error(e);
        throw e;
   }
}); 
console.log(FiberStateNames[fiber.state]);
setTimeout(()=>console.log(FiberStateNames[fiber.state]),1000);
process.stdin.resume();

输出:

LOADING
Error: cannot get property "foobar" without inject
    at Object.callback (file:///E:/projects/typescript/ts-learning/App2/app.js:9:13)
    at Fiber.execute (file:///E:/projects/typescript/ts-learning/node_modules/@deepseek-ai/cordis/lib/index.js:1070:28)
    at file:///E:/projects/typescript/ts-learning/node_modules/@deepseek-ai/cordis/lib/index.js:1141:34
    at composeError (file:///E:/projects/typescript/ts-learning/node_modules/@deepseek-ai/cordis/lib/index.js:199:18)
    at Fiber._execute (file:///E:/projects/typescript/ts-learning/node_modules/@deepseek-ai/cordis/lib/index.js:1136:10)
    at Fiber._reload (file:///E:/projects/typescript/ts-learning/node_modules/@deepseek-ai/cordis/lib/index.js:1355:16)
    at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
FAILED

虽然没有从输出中得到抛出异常的确切未知,但是得到一个错误的消息:cannot get property "foobar" without inject。可以确定错误发生在从Context中提取foobar属性的时候,原因是不能提取未显式声明注入的服务,其实这是合理的。

3. 先声明,再消费

在 Cordis 框架中,插件只能消费显式声明注入的服务是其依赖注入的核心安全机制。简单来说,如果一个插件想要使用某个服务,它必须在注册时通过 inject 属性显式声明,除非这个服务注册在当前Fiber绑定的Context上。否则,即使该服务在全局真实存在,该插件在运行时也无法访问它,就会出现上述的错误。从抛出的这个异常也可以进一步验证前文的论断:由于作为根的context和通过调用isolate方法创建的subContext,它们共享同一个Fiber,所以它最终只会保留第二次存储的服务实例。由于插件具有自己专属的Fiber,我们必须按照如下的方式调用inject方法以提供服务注入的方式来注册插件。

import { Context} from '@deepseek-ai/cordis'

declare module '@deepseek-ai/cordis'{
interface Context {
    foobar: ()=>void;
    }
}

const context = new Context();
context.provide("foobar", ()=>console.log("foo"));
const subContext = context.isolate("foobar");
subContext.provide("foobar", ()=>console.log("bar"));

const FiberStateNames = ['PENDING', 'LOADING', 'ACTIVE', 'FAILED', 'DISPOSED', 'UNLOADING'];
const fiber = subContext.inject(["foobar"], ctx=>ctx.foobar()); 
console.log(FiberStateNames[fiber.state]);
setTimeout(()=>console.log(FiberStateNames[fiber.state]),1000);

输出:

LOADING
bar
ACTIVE

这种先声明后消费的模式是为了解决传统 Agent 框架中插件顺序依赖、组件硬编码以及热插拔时程序容易崩溃的痛点,可以实现如下的特性和解决如下的问题:

3.1 实现真正的“热插拔”与故障隔离

在 DeepSeek Harness 中,所有的基础能力(如工具中心、模型服务、文件系统、会话管理等)全部都是动态加载、甚至随时可能被卸载的插件。如果不声明,你的插件直接通过 import 或硬编码去调用另一个服务,一旦那个服务因为沙箱断开、底层实现卸载或配置更新而突然消失,你的插件就会因为找不到服务而直接引发未捕获的运行时报错(Crash)。

通过 inject 声明后,Cordis 能够精确追踪谁依赖了谁。底层实现被卸载,依赖它的工具也会一起停下。 新的实现准备好后,工具再重新加载。” 调用方不会拿着一个已经失效的过时句柄去盲目执行,从而保证了系统整体的稳定性。

3.2 靠依赖关系决定启动顺序,而非书写顺序在传统的单体架构中

开发者必须小心翼翼地安排代码的加载顺序(例如:必须先初始化大模型服务,再初始化工具,最后启动 Agent 循环)。而在 Cordis 中,“加载顺序通过服务依赖表达,而非手动编排启动序列。” 当你声明了某个依赖,插件声明所需的服务后,会等待这些服务就绪才启动 。这让 Cordis 微内核可以在启动时,自动在内存中拓扑排序并安全地组装整套插件组合。

3.3 解耦具体实现(面向接口编程)

在 DeepSeek Harness 内部,其他插件通过key 查找服务,而非导入具体实现。 当你通过 inject 声明依赖一个如 ctx.llmctx.tools 的服务 Key 时,你不需要关心这个 LLM 到底是 DeepSeek-V3 还是 GPT-4,也不用管它是本地沙箱还是远程沙箱。通过先声明,系统可以在运行时动态替换底层供给者,而调用方的代码完全不需要修改。

4. 自己提供的服务可以不同声明

如果不提供服务注入声明,唯有一途,那就是按照如下的方式将服务注册在当前Context上:

import { Context} from '@deepseek-ai/cordis'

declare module '@deepseek-ai/cordis'{
interface Context {
    foobar: ()=>void;
    }
}

const context = new Context();
context.provide("foobar", ()=>console.log("foo"));
const subContext = context.isolate("foobar");

const FiberStateNames = ['PENDING', 'LOADING', 'ACTIVE', 'FAILED', 'DISPOSED', 'UNLOADING'];
const fiber = subContext.plugin(ctx=>{
   const disposable = ctx.provide("foobar", ()=>console.log("bar")); 
   ctx.foobar();
   return disposable;
}); 
console.log(FiberStateNames[fiber.state]);
setTimeout(()=>console.log(FiberStateNames[fiber.state]),1000);

输出:

LOADING
bar
ACTIVE

5. 异常具体在哪里抛出来的?

由于插件仅仅是调用ctx.foobar()方法提取并执行服务函数,而且我们知道这个插件的ctx只是一个代理而已,针对foobar方法的读取会被代理处理器拦截,既然是在读取ctxfoobar属性时抛出了异常,我们就通过代理处理器的定义来看看它究竟注入了怎样的拦截操作。通过DeepSeek Harness插件内核-07:Context全面解析的介绍,我们知道这个代理处理器定义在ReflectService的静态属性handler上。

export class ReflectService {
  static handler: ProxyHandler<Context> = {
    get: (target, prop, ctx: Context) => {      
      ...
      const error = new Error(`cannot get property "${prop}" without inject`)
      try {
        ...
        return ctx.events.waterfall('internal/get', ctx, prop, error, () => {
          const key = target[symbols.isolate][prop]
          let fiber = (ctx[symbols.shadow] as Context ?? ctx).fiber
          while (true) {
            const impl = fiber.store?.[prop]
            if (impl) return getTraceable(ctx, impl.value)
            if (prop in fiber.inject) {
              error.message = `cannot get required service "${prop}" in inactive context`
              throw error
            }
            if (!fiber.runtime) throw error
            if (fiber.parent[symbols.isolate][prop] !== key) throw error
            fiber = fiber.parent.fiber
          }
        })
      } catch (e: any) {
        throw e === error ? enhanceError(e) : e
      }
    },
  }
}

很幸运我们一眼就发现了抛出错误的那行代码,我们来做一下简单的分析:当我们利用subContext代理去读取foobar属性时,它会根据提供的服务名称foobartarget(就是subContext对象)的symbols.isolate字典中提取作为服务实例唯一标识的Symbol,此时得到的这个Symbol是指向我们希望的那个服务(第二次注册的输出字符bar的那个函数),由于当前Fiberstore并不能提供目标服务,所以第一轮while循环会走完,当进入第二轮循环后,程序会走到倒数第二行(if (fiber.parent[symbols.isolate][prop] !== key) throw error),此时它将这个Symbol与父Contextsymbols.isolate字典存储的对应Symbol进行比较(对应第一次注册的服务)。两个Symbol自然不相等,于是异常被抛出。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值