Cordis:5.4k stars、今日 +959 的 "时空可组合性" 元框架,正在悄悄支撑 DeepSeek Harness 的 Agent 运行时

有一类开源项目,作者的代码量很克制,文档却厚得像论文;他们不发 Twitter,不在 Discord 里” 营销”,GitHub Trending 上也从来不是那种” 一次最多待两小时就掉榜” 的流星。这种项目往往是某个被悄悄复用的” 基础设施级” 东西 —— 你日常用的一个大玩具底下,可能就垫着它。

上周我就撞到了一起。

我本来在研究 DeepSeek 前一阵开源的 DeepSeek Harness(DSH)—— 一个跟 LangGraph / CrewAI 同一梯队的 AI Agent 编排框架,按理说它的 plugin 体系应该自己写一套嘛,对吧?结果翻到 DSH 的 vendor 目录,第一条注释就把我逗笑了:

“Cordis is vendored in DeepSeek Harness as the underlying plugin framework.”

DSH 不是” 用了” Cordis—— 它是把 @deepseek-ai/cordis 4.0.1 整包 vendor 进仓库里。换句话说,DeepSeek 在做自己那一整套 Agent 编排哲学的时候,选择不重新发明插件系统,而是把一个名字你可能没听过、社区讨论不多、只在 paper 一栏挂了一篇 PDF 的 Node.js 框架,请到工程的心脏位置。

这个框架就是 cordiverse/cordis—— 一款自称 “A Meta-Framework of Spatiotemporal Composability”(时空可组合性的元框架)的 TypeScript 开源项目。它今天已经爬到 GitHub Trending 的 #9,5,378 stars / 290 forks但更炸的数字是今日 +959 stars—— 对一个 4 年历史的 repo 来说,这种日增量是反常的。

我花了周末把它的 paper(2026 年 8 月 13 日的最新草稿)、cordis-primer、核心包代码、最近一周的 PR 全过了一遍。下面是我最关心的那几个问题,以及我给出的答案。


一、为什么又一款” 插件框架” 能上 Trending—— 而且日增近千

先把” 为什么这件事重要” 说清楚。

我自己写过的、或者读过的 Node.js 插件框架,少说也有十几款了:NestJS 的 Module、Vue 的 Plugin、Rollup 的 Plugin、Fastify 的 Plugin、vscode 的 extension host、Electron 的 IPC、Koishi 的 plugin,甚至 Cordova 的 hook…… 每款都说自己是” 插件系统的最优解”。

但如果你把” 插件系统” 这个词拆开看,每家其实只回答了其中一部分问题:

  • NestJS / Vue 答了” 组件怎么挂到根上下文”—— 但完全没有” 插件卸载的时候副作用能不能干净地回滚”。
  • Rollup / Webpack 答了” 如何按声明的依赖驱动加载顺序”—— 但你问它” 插件之间能不能互相 override 配置、有冲突时回退到上一次的 committed 值”,它就沉默了。
  • VSCode extension host 答了” 插件之间靠 extension API 通信,进程隔离”—— 但它是用进程隔离换隔离,代价是 IPC,而且 Undo 仍然 owner 在插件自己手里。
  • LangGraph / CrewAI / AutoGen 这种 2024 年以后火起来的 AI Agent 编排层,看上去” 很动态”—— 但它们在底层仍然要落一个插件系统,最后要么自己写一个迷你版(LangGraph 的 Pregel),要么干脆把 LangChain 那套 Retryable / Runnable 的迁移语义直接当配置层用。

真正难的问题是这样的

当你装一个 AI Agent 框架的时候,你装进去的不只是一个对象 —— 你装进去的是一个可能 30 秒后会重写自己上下文的、有副作用的、可能还会在执行到一半时被要求撤回的活物。

想清楚这件事,你才能体会 Cordis 想要解决的” 两个正交维度”:

  • 时间可组合性(Temporal Composability):组件卸载时,它的副作用能不能完整地、可预测地回滚?
  • 空间可组合性(Spatial Composability):组件之间互相声明依赖,加载顺序由依赖关系自动推导,而不是手工编排启动序列?

Cordis 把这两个维度形式化成 effect(效应)coeffect(余效应) 两套 runtime 机制。这一点,跟经典的 algebraic effects /coeffects 一脉相承,但项目方把它” 焊接” 到了一个具体的、能跑、能 hot reload、能 inspect 的 TypeScript 框架里。

paper 摘要里有句话特别点睛:

“Modern software—from plugin systems to self-evolving agent harnesses—increasingly requires dynamic composition, yet its formal foundations remain underdeveloped.”

self-evolving agent harnesses. 我很久没在 paper 里读到一个这么有野心的词组了。


二、五个核心概念:Cordis 的全部世界观

cordis-primer(也就是 DSH 给 harness 插件作者写的入门手册)一开始就把整套系统压成了五个核心概念。我把它原样翻译,再补一些我的解读。

1. 插件 = 实现 Service 的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { Context, Service } from '@cordis/core'

class Hello extends Service {
static inject = ['http']

constructor(ctx: Context, options: { greeting: string }) {
super(ctx, 'hello', true)
this.greeting = options.greeting
}

say(name: string) {
return `${this.greeting}, ${name}!`
}
}

一个插件要么是个injectapply(ctx) 的函数,要么是上面这种 Service 子类。它的整个生命周期由 Cordis 挂到当前 context 上:实例化靠 inject 等待依赖、活跃期间可以通过 ctx.foo 拿其他服务、被卸载时所有 ctx.effect 注册的内容自动反转。

这跟 NestJS 的 Provider 看上去很像,但关键差异在第 5 点

2. Context = 服务的容器,每个服务占据一个稳定的 ctx.<key>

1
2
3
4
const ctx = new Context()
ctx.plugin(Hello, { greeting: 'Hi' })

console.log(ctx.hello.say('小戴')) // Hi, 小戴!

注意是 ctx.hello 而不是 Hello.say(...)。整个框架里你几乎不会”import 实现类”—— 所有跨插件通信都通过 ctx.<key> 拿服务。这就把” 插件之间是平等的、互相不知道实现细节” 这件事做死了,是后面 effect 反转能干净的基础。

3. inject = 声明式依赖,加载顺序由依赖推导

1
static inject = ['http', 'database']

写完上面这一行,Cordis 会自动等 ctx.httpctx.database 都 ready,再启动你的插件。需要多轮依赖也没问题,依赖图是 DAG 的话就一定能解出来。

这对于写 Agent harness 的人来说极友好:你写个 LLM 调用插件,只关心 ctx.llmctx.toolsctx.sessions 在不在;至于谁先谁后,由 harness 编排层决定。

4. 类型化事件分发(emit /waterfall/parallel /serial)

这是 Cordis 我觉得最值得吹的设计。

它把事件分发模式显式做成四种,每种只能用自己的方法触发:

模式 是否 await 顺序 返回值
emit 注册顺序
waterfall 注册顺序 (每个监听器可包 / 可短路)
parallel 并行
serial 注册顺序

注意一个非常硬核的细节:事件分发模式是事件公开约定的一部分。也就是说你定义事件时就要声明它是 emit 还是 waterfall,然后这个声明会被 harness 生成的目录跟实际分发调用点做交叉校验 —— 一个 typecheck 帮你找出” 我以为是 waterfall 但被当 emit 用了” 的 bug。

waterfall 的语义最像 Koa / Express 的中间件:

1
2
3
4
ctx.on('strategy/select', async (req, next) => {
if (req.kind === 'simple') return 'short-circuit'
return next() // 下游监听器处理
})

策略类插件如果不调用 next(),就拿到了” 最终决策权”,相当于短路返回;如果是观察类插件,不调用 next() 是错的 —— 你必须委托。

5. 注册 = 可逆的副作用

这一条才是 Cordis 跟” 另一个 NestJS” 的关键差异。

所有” 会留下痕迹” 的操作 —— 挂个 listener、注册个 tool schema、装一个 prompt 片段、初始化一条 WS 连接 —— 都通过 ctx.effect() 或者 ctx.on() 注册并返回一个 disposer

1
2
3
4
ctx.effect(() => {
const id = setInterval(() => tick(), 1000)
return () => clearInterval(id) // dispose 时跑
})

卸载插件 / 热重载 /harness roll back 时,Cordis 按注册相反顺序遍历 disposer,依次执行回滚。

这才是 AI Agent 框架真正想要的特性

你想想看:一个 agent 在执行第 7 步的时候,用户说了” 算了,撤销”。那么从第 7 步到第 1 步之间注册的 timer、订阅的消息队列、申请到的临时文件、产生的中间状态 —— 能 100% 还原吗?传统插件框架里,这件事 100% 是写插件的人自己保证的(”please remember to clearInterval on dispose”)。Cordis 把” 可逆” 从道德劝说升级成了 runtime contract

paper 把这个特性叫做 revertible effects:每个 context 变换都自带一个 inverse,runtime 帮你追踪。


三、技术架构:effect × coeffect 的” 形式化骨架”

光看五点概念还不够炸。我把 paper 和核心包交叉看了一遍之后,画一个简化版的架构图给感兴趣的读者。

3.1 两个 Context:Effect context + Coeffect context

传统插件系统只有一个 context(” 我有什么”),Cordis 把它劈成两个:

  • Effect context:记录” 我这个插件对外造成了什么”—— 已注册的 listeners、已分配的 timer、已 effect 化的资源。Cordis 通过这些记录反推” 卸载时该怎么撤销”。
  • Coeffect context:记录” 我这个插件依赖别人提供了什么”—— 通过 inject 声明的服务集,加上 ctx.on(...) 的监听器对应的” 我希望上游给我什么”。

每个 context 变化,runtime 都会通知所有相关组件(component)。

这听起来抽象,换个 AI Agent 场景就具体了:

你写一个 prompt-rewrite 插件,声明 inject = ['llm', 'prompt-policy'],并 ctx.on('prompt/build', ...) 注册了一个监听器。

  • coeffect 端:Cordis 看到 inject,发现你依赖 ctx.llmctx.prompt-policy,等它们 ready。
  • effect 端:Cordis 把你的 prompt/build 监听器登记进分发器,生成对应的 disposer—— 一旦插件卸载,prompt/build 上别人再 emit 的时候,你这个监听器不在了。

prompt-policy 后来改了一版,harness 触发 hot reload,Cordis 会沿着依赖图反推” 谁依赖了 prompt-policy → 谁要重 build → 谁注册的 prompt 要 recompose”,整个过程是用 effect/coeffect 机制推导出来的,不是手工编排。

3.2 一个统一的 Context type

paper 第二段提到,Cordis 把 effect 和 coeffect 统一到一个 context type 里。这件事读起来像废话,做起来却意味着:

  • 一段代码不需要在” 我现在是 producer” 和” 我现在是 consumer” 之间反复横跳;
  • type 系统天然就能告诉你:现在调用 ctx.effect(fn),fn 的返回值就是 disposer;现在写 static inject = [...],声明的就是上游该给你什么。

这给了 Cordis 一个 Node.js 生态少见的东西:“插件系统” 作为 type-level 的契约,而不仅仅是一组函数调用约定。

3.3 Component 与动态组合演算(Calculus of dynamic composition)

“After that, we combine these mechanisms into the notion of a component and give a calculus of dynamic composition, whose metatheory carries spatiotemporal composability from a single component to a whole system of interleaved components.”

这句是 paper 里最不容易啃的一段,但核心意思其实可以一句话转述:

单组件的时空可组合性,可以被元理论 (metatheory) 推到整个系统的可组合性

也就是说,paper 给出了一个数学化的保证:每个 plugin 自身可逆 + 每个 plugin 显式声明依赖 → 整个组合系统也是可逆、可推导的。

这种” 用形式化保证动态组件系统正确性” 的论文路线,函数式编程那一支比较熟(见 Eff 语言 / Koka 的 coeffect 论文),Cordis 是少有的把它工程化到一个 producer-grade runtime 的尝试。

3.4 实际代码骨架

不写代码就跑题了。Cordis 的最小完整例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { Context, Service } from '@cordis/core'

const ctx = new Context()

class Database extends Service {
constructor(ctx: Context) {
super(ctx, 'database', true)
}
query() { return 'SELECT 1' }
}

class Reporter extends Service {
static inject = ['database']

constructor(ctx: Context) {
super(ctx, 'reporter', true)
ctx.on('report/build', (req, next) => {
req.batches.push(this.ctx.database.query())
return next()
})
}
}

ctx.plugin(Database)
ctx.plugin(Reporter)

console.log(await ctx.parallel('report/build', { batches: [] }))

如果你接下来 ctx.registry.delete(Reporter.service),所有 Reporter 注册的东西 ——report/build 监听器、database 依赖图中的边 —— 都会被 inverse 回滚。


四、谁在用 Cordis:DeepSeek Harness 是冰山一角

我以为 DSH 是孤例,于是顺手扫了一圈 issue 区。

Issue #76(2026-08-16 提交,作者 mook-wenyu)的标题非常直观:

“Subagent’s ask_user_question never surfaces to the parent agent — child keeps running until max-tokens (report from DSH, a cordis-based framework)”

内容翻译过来是:DSH 是基于 cordis fork 的应用,它的 Web GUI 里子 agent 调用 ask_user_question 这个工具,结果问题不会透到主 agent 或 UI,子 agent 会一直被 token 限制顶死。Issue 里把 DSH 内部 5 个包的具体代码位置 + 报错路径 + 修复期望全列了。

这种” 上游 issue 区里写应用层 bug report” 的现象很有意思 —— 因为 DSH 仓库自己关了 issues,所以团队把反馈 sheet 写到了 cordiverse/cordis 这边。这间接证明 Cordis 的 vendoring 不是一次性的” 塞个库进去”,而是 DSH 的整个 agent 模型都深深地靠着 cordis 的 dispose /inject 语义在工作

另外几个间接的” 重度用户” 线索:

  • 仓库的 contributors 最近一周里有 tdwhere123(来自 cordis loader 的复杂 PR)、randomcat4(另一位持续贡献者),以及更早的 OpenAI Codex 自动 PR(PR #75 docs: add contribution guide 由 GPT-5 代写,author disclosure 写了 Autonomous by OpenAI Codex)。
  • Claude Code 本身作为 PR generator 在 §6.7 substrate 的 Wasm 路线实验中现身(issue #78,作者 inso1337 用 Claude Code 写了一整套 WAT / AssemblyScript / Python 组件化实验)。
  • 上游 cordiverse 跟 deepseek-ai 的关系,从 @deepseek-ai/cordis 这个 fork package name 一眼可见 —— 两个组织之间的依赖路径是”cordiverse 发布 core,deepseek-ai vendor 一份做修改,挂自己的 issue 追踪”。

这给我们一个非常关键的旁证

一个严肃的 AI Agent 框架团队,没有自己造 plugin runtime,而是选择 fork 一款学术味道很重、正在快速迭代的 Node.js 元框架。这是” 开源界对 Cordis 投的现实票”。


五、快速上手:一个能在 5 分钟内跑完的例子

如果你想自己验证一下,不是花 5 分钟就能跑通的 ——Cordis 仍然处在 4.0.x 阶段,作者在 README 里写得很直白:

“Cordis is under active development. The API is not yet stable and may change without notice.”

这句话非常诚实。如果你打算把它接到生产 Agent harness 上,至少要做好两件事:锁版本 + 跟 upstream 同步 PR。

5.1 安装(Yarn workspace 模式)

Cordis 自己用 Yarn 4 + Corepack:

1
2
3
4
corepack enable
yarn --no-immutable
yarn install
yarn test

JS / TS 端的依赖入口:

1
2
3
4
5
6
7
8
{
"dependencies": {
"@cordis/core": "workspace:*",
"@cordisjs/plugin-loader": "workspace:*",
"@cordisjs/plugin-group": "workspace:*",
"@cordisjs/plugin-include": "workspace:*"
}
}

5.2 用 Cordis 写一个最简单的 plugin 与一个 hot reload 实验

1
2
3
4
5
6
7
8
9
10
11
12
// greeter.ts
import { Service, Context } from '@cordis/core'

export default class Greeter extends Service {
constructor(ctx: Context, public greeting: string) {
super(ctx, 'greeter', true)
}

hello(name: string) {
return `${this.greeting}, ${name}!`
}
}
1
2
3
4
5
6
7
8
9
10
11
12
// main.ts
import { Context } from '@cordis/core'
import Greeter from './greeter'

const ctx = new Context()
const sub = ctx.plugin(Greeter, { greeting: 'Hi' })

console.log(sub.greeter.hello('小戴')) // Hi, 小戴!

// 改 greeting → hot reload
sub.update({ greeting: 'Hello' })
console.log(sub.greeter.hello('小戴')) // Hello, 小戴!

sub.update() 触发的就是 时空可组合性 ——effect context 把 greeting 的旧值 dispose、coeffect 解析重新跑一遍依赖、运行时切换到新 config。整个过程不需要重启进程、不需要重 import 模块

这就是 Cordis 想给 AI Agent harness 的那种” 边跑边换零件” 的能力。

5.3 一些不那么友好的真相

实话说,如果你想” 立刻把 Cordis 接到生产”,目前会有几个痛点:

  1. API 不稳定:作者在 README 里白纸黑字写了”may change without notice”,我读代码也确认 Effect 类型和 Fiber 状态机在最近 5 个 PR 里都还在大改。生产集成建议锁 patch 版本,等 minor 稳定了再升级。
  2. 学习曲线靠 paper 而不是文档cordis-primer 写得简洁且数学,但 internal/plugininternal/updateinternal/filter 三层之间的差别,没有一个完整 tour 走完,你很难做出” 我要不要 fork 这个 loader” 的判断。
  3. Niche:生态不大,遇到坑大概率要直接看 packages/core/src/*.ts 源码。Cordis 当前的” 出货” 是给你一个元框架 + 一组 helper,不是给你一个” 开箱即用” 的 Agent SDK。

但反过来 —— 这恰恰是 DeepSeek Harness 在做的事:自己 fork 一份 + 自己补上层 + 跟 upstream 一起演进。Cordis 的真实身份更像一个研究型基础设施,而不是一个” 普通开发者今天就拿来用的工具”。


六、总结:谁应该现在关注 Cordis

写到这里我已经想清楚了今天这篇博客的结论,应该分两段说。

给” 造 AI Agent 框架” 的人

Cordis 给你的是一套可形式化推理的 plugin 系统。如果你正在设计一个 agent harness,并且开始被以下问题困扰:

  • 加载顺序复杂到需要写一堆插件优先级,
  • HMR 重新装插件之后有副作用泄漏,
  • 多 agent 上下文切换时工具注册 / 卸载不可控,
  • 撤销 /rollback 流程需要每个插件自己维护 dispose,

那 Cordis 的 effect /coeffect 模型值得你花一两个周末读一遍 paper + 跑一遍 primer。哪怕最终你决定自己造 —— 你至少能清楚地知道自己要放弃什么。

给” 用 AI Agent 框架” 的人

如果你和我一样,平时更多是 LangGraph / CrewAI / DSH / OpenAI Agents SDK 的使用者,那 Cordis 今天的 959 日增 stars 至少说明一件事:

严肃团队正在认真地把” 动态可组合性” 这件事当成一等公民来设计。这件事最终会渗透到你日常用的 Agent 框架里,让 hot reload、rollback、tool registration 变得越来越自然。

你不用立刻学 Cordis 的 API。但你应该知道这个流派的存在,因为它会在接下来一两年决定 AI Agent 框架的能力上限。

最后照例贴上链接:

今天就到这里。如果周末有空,我打算把 Cordis 的 effect 形式化部分再翻译一版 —— 重点对照 paper 的 §3~§4,把”revertible effects 的 monotone trace” 那段拆开讲讲。