Cordis,底层的内核

Cordis 是来自 Koishi 社区的 TypeScript plugin 框架。DSH 没有实现自己的 plugin 系统。它直接使用此框架,这就是为什么 DSH plugin 同时也是 Cordis plugin。

Context 是 Service 的存储库

每个 plugin 都会接收一个 Context,通常命名为 ctx。Service 驻留在其上,使用如 ctx.tools、ctx.llm 或 ctx.sessions 等稳定键值。需要工具表的 plugin 访问该键,而非具体实现,因此该键背后的实现可以在 plugin 无感知的情况下进行更改。

context
import type { Context } from 'cordis'

// Services live on the context under stable keys.
// A plugin reaches for the key, not for a concrete class.
export function apply(ctx: Context) {
  ctx.tools     // the tool table
  ctx.sessions  // the append-only session log
  ctx.llm       // whichever model adapter is loaded
}

插件即实现 Service 的一切对象

实际上,这意味着一个带有 apply(ctx) 主体的函数,或者一个继承自 Service 的类,Cordis 会将其生命周期挂载到当前 Context 中。除了这一点,没有注册清单,也没有需要继承的插件基类。

加载顺序是声明式的,而非序列化的

插件在 inject 中列出其所需内容。Cordis 会将其置于等待状态,直到所有列出的服务都存在,然后才会调用 apply。无需手动维护引导顺序,插件如果在其依赖项之前加载,只需等待即可,而不会崩溃。

inject
export const inject = ['tools', 'sessions']

export function apply(ctx: Context) {
  // Cordis holds this plugin in a pending state until both
  // services exist, so neither lookup below can be undefined.
  const session = ctx.sessions.current()
  ctx.tools.list().forEach((tool) => session.note(tool.name))
}

在 apply 内部,inject 中命名的所有内容都保证已存在。

副作用是可逆的

通过 ctx.on 和 ctx.effect 进行的注册会被记录。当插件卸载或重新加载时,Cordis 会遍历该记录并撤销每一项。监听器被移除,服务被释放,定时器被清除。这使得插件的热插拔是安全的,而非导致缓慢的内存泄漏。

reversible effect
export function apply(ctx: Context) {
  const dispose = ctx.tools.register('read_file', async ({ path }) => {
    return readFile(path, 'utf8')
  })

  // Returning the disposer is what makes the plugin removable:
  // unloading it takes the tool back out of the table.
  return dispose
}

调度事件的四种方式

插件既可以通过事件也可以通过服务进行通信,调度模式决定了监听器可以执行的操作。

emit

触发并遗忘。每个监听器都会运行,不返回任何内容。

parallel

每个监听器并发运行,调用者会等待所有监听器执行完毕。

serial

监听器按顺序运行,直到某个监听器返回一个值,该值即为结果。

waterfall

每个监听器都会接收 next,并可以转换参数、委托后续处理或短路逻辑。

这在 DSH 中是如何体现的

每一个 DSH 能力都是 Context 上的一个服务。阅读该列表是理解 harness 实际构成的最快方式。

  • 模型适配器占用一个服务键。更换提供商意味着针对同一个键加载不同的插件。
  • 每个工具都将自身注册到工具表中作为可逆副作用,因此移除工具就是卸载其插件。
  • 会话服务拥有追加日志。存储后端是其背后的独立插件。
  • 代理循环本身就是一个插件,这就是为什么像 Minimal 和 PTC 这样的预设在共享内核的同时差异如此之大。

Cordis 背后的组合性模型撰写在关于时空组合性(spatiotemporal composability)的论文中,该论文链接在 DSH 仓库中。