阅读对象:e:\code\deepseek-harness\cordis(Cordis v4,cordis@4.0.0-rc.8
方法:三观递进之第一观「整体观」——先看见,再看懂,最后看透。本文负责「看见」:为读者建立一张完整的项目认知地图。
系列:01 整体观(本篇)/ 02 具体观 / 03 深刻观,三篇层层递进。


在这里插入图片描述

总述:不是又一个 DI 容器,而是一门组合的"元"范式

Cordis v4 是一个可组合性元框架(Meta-Framework of Spatiotemporal Composability)。它不是一个业务框架,也不绑定任何具体领域;它回答的是一个更底层的问题:当软件由大量组件在运行时动态地装载、替换、卸载时,如何保证体系不会崩坏?

传统上,插件系统对这个问题只有"尽力而为"的答案:注册时给回调,卸载时凭自觉清理,依赖靠文档约定加载顺序。Cordis 把这两件事从工程纪律升级为运行时语义承诺:任何组件装载所产生的副作用,都可以被完整回滚(时间可组合性);任何组件对别的组件的依赖,都可以被声明并自动响应(空间可组合性)。正如其配套论文《A Programming Paradigm for Spatiotemporal Composability》所阐述的,这两个维度分别由"可逆效果"(revertible effects)与"响应式余效果"(reactive coeffects)两种机制承载,并统一进一个核心概念——Context(上下文)。

这套设计并非纸上谈兵:Cordis v4 是 Koishi 生态(@cordisjs 系列包)的下一代底座,同时被 @deepseek-ai/deepseek-harness(自进化 agent 的编排框架)以 vendor 形式整体引入作为底层插件框架。本仓库配套论文的动机场景,正是"self-evolving agent harnesses"——运行时不断修改自身的软件。

这份文档将按"项目定位 → 代码结构 → 设计理念 → 实现原理 → 整体流程 → 使用指南"的路径展开,并用五张图建立视觉化的认知坐标。读完本文,你应该能回答三个问题:它是什么?它由什么组成?它如何运转?


一分 · 项目定位与生态方位

1.1 它是什么:Koishi 生态底座的一次范式重写

Cordis 的名字在 Koishi 生态中历史悠久——它是 Koishi 长期使用的插件容器。v4 版本对这套机制做了彻底重写,从"Koishi 内部的插件库"升级为独立的元框架packages/core/README.md 首行即定义 “A Meta-Framework of Spatiotemporal Composability”,并附上论文链接与文档链接;packages/core/package.json 将其描述为 “Meta-Framework for Modern Applications”。

配套论文《A Programming Paradigm for Spatiotemporal Composability》收录于同一组织的仓库 cordiverse/paper(preprint,Draft of August 13, 2026,官方注明 under active revision,引用时需标注版本)。论文摘要明确指出两块历史空白:现代软件(从插件系统到自进化 agent harness)越来越依赖动态组合,但其形式化基础却长期缺失。Cordis 的贡献在于把两个正交维度形式化并实现:

维度 问题 论文机制 Cordis 实现
时间可组合性 Temporal Composability 组件被移除时,其副作用必须被完全回滚 可逆效果(revertible effects):每个上下文变换携带逆变换,由运行时追踪 Fiber 效果系统、ctx.effect()DisposableList 逆序清理
空间可组合性 Spatial Composability 组件间依赖需要被声明响应式管理 响应式余效果(reactive coeffects):上下文变化按规格通知组件 ReflectService 服务注册表、inject 声明、provide/notify 广播
统一 读写两个世界合二为一 单一上下文类型(context type) Context 代理对象

图 1-1 · C4 Context 生态定位图:本图把一个典型 Cordis 应用放在三层坐标中——最上层是需要动态组合的应用(Koishi 生态、DeepSeek Harness),中间是 Cordis 元框架本身(core 内核 + loader/include/hmr 装配层),底层是 Node.js ESM 运行时。看懂这张图,就知道 Cordis"夹在应用与运行时之间"的元框架位置。

运行时层

Cordis 元框架层

应用层

引入

vendor 引入

Koishi 生态
聊天机器人应用

DeepSeek Harness
自进化 Agent 编排

core 内核
Context × Fiber × Reflect

装配扩展
loader / include / hmr

Node.js ESM 运行时
(22/23/24)

1.2 它在解决什么问题:组合的两难

任何鼓吹"插件化"的软件都会遇到两堵墙:

第一堵墙是时间之墙(回滚难)。装一个插件很容易——注册事件、创建服务、开定时器;但卸掉一个插件很难——要先撤销它做的每一件事,而开发者常常忘记、或者撤销顺序出错。Cordis 的答案是:把"卸载即回滚"变成运行时的结构性义务,而不是开发者的自觉。所有注册都通过 ctx.effect() / ctx.on() 等入口进行,它们自动登记进当前 Fiber 的逆序清理链。移除组件 = 按逆序执行一串 disposer,与 Git 的 revert 有异曲同工之妙。

第二堵墙是空间之墙(依赖难)。插件 A 需要插件 B 的服务,传统做法是硬 import、或者在启动清单里排好顺序。一旦体系变大,顺序编排会变成一场灾难。Cordis 的答案是:依赖用 inject 字段声明,服务用 provide 注册到统一仓库,谁先就绪、谁后启动,由运行时用通知机制动态调和——加载顺序从"脚本"变成"求解"

这两堵墙的拆除,形成了 Cordis 全部代码的形式因。接下来我们从代码里验证这件事。


二分 · 代码结构与模块职责

2.1 Monorepo 拓扑:九个包,一个焦点

仓库采用 yarn 4 workspaces + yakumo 构建链(esbuild 打包 + tsc 类型 + vitest 测试 + eslint 校验),packages/* 下共 9 个包,全部 ESM:

类型 一句话职责
@cordisjs/corecordis 运行时内核 唯一的"真理之源":Context / Fiber / Reflect / Events / Logger 五个核心对象
@cordisjs/plugin-loader 装配层 声明式加载:Entry 树建模、配置调和、Node ModuleLoader 对接、isolate 隔离域
@cordisjs/plugin-include 装配层 cordis.yml 配置文件的双向读写(!!js 表达式、patch 补丁、原子写)
@cordisjs/plugin-hmr 装配层 热模块替换:依赖图分析、缓存清理、失败回滚
@cordisjs/plugin-timer 工具插件 effect 化的 timeout / interval / throttle / debounce
@cordisjs/plugin-group 工具插件 声明式 EntryGroup:一组配置条目作为一个可整体装卸的单元
@cordisjs/plugin-logger-console 工具插件 将 LoggerService 的日志消息导出到控制台的 Exporter
@cordisjs/utils 工具库 与框架紧耦合但非运行时必需的小工具
create-cordis 脚手架 create-cordis CLI:从 npm 拉模板生成一个 Cordis 应用

注意依赖的单向性core 不依赖任何 @cordisjs/plugin-*(它们只是 optional peerDependencies,用于类型层面的注入声明);loader 依赖 coreincludehmr 又依赖 loader。也就是说,无论外围装多少层,内核永远只有 core 一个包,业务逻辑永远长在外围插件里——这本身就是"元框架"的第一条纪律。

图 1-2 · 包依赖与职责分层图:Cordis 把"组合"拆成一条装配流水线——core 提供原语,loader 把配置声明解释成 Entry 树,include 让配置文件成为一等公民(可读可写),hmr 让源码热替换成为开发体验。三者都只是"内核之上的插件",印证了元框架的自我克制。

工具层

装配层(声明式)

脚手架

内核层(唯一运行时)

Context 代理

Fiber 效果系统

Reflect 服务仓库

Events 事件总线

Logger 日志服务

loader
Entry 树 + 配置调和

include
cordis.yml 双向读写

hmr
热模块替换

timer

group

logger-console

create-cordis

2.2 内核的九个源文件:一台精密机器的零件

packages/core/src/ 下只有 9 个 TypeScript 文件(8 个实现 + 1 个统一导出),这是 Cordis 的全部"物理存在":

文件 核心对象 职责一句话
context.ts Context 唯一入口对象:由 ReflectService.handler 代理包装,提供 root / extend / isolate / intercept
fiber.ts Fiber 组件的生命周期单元:六态状态机、epoch 依赖指纹、惯性锁、ctx.effect() 可逆效果登记与逆序回滚
reflect.ts ReflectService 服务仓库:provide / set / get / notify / accessor / mixin,是 ctx 代理 handler 与 createTraceable 的来源
registry.ts RegistryService 插件注册表:ctx.plugin() 门口、Runtime 缓存、@Inject 装饰器与依赖解析
events.ts EventsService 事件总线:emit / parallel / serial / bail / waterfall 五种分发模式 + internal/* 内部事件族
logger.ts LoggerService 可调用服务范式(callable service)的样板:哈希配色、格式串、Exporter 管道
service.ts Service 所有服务的基类:provide 自动注册、@Service.init / check / config 静态协议、代理安全的 instanceof
utils.ts 若干工具 DisposableListcreateTraceablecomposeError 长栈、createCallable、各类 symbol 定义
index.ts 导出 统一 re-export,拼装成 cordis 包的公共 API

要点:内核几乎不含业务代码——它只定义"对象之间的关系",这正是"元框架"的含义。下一节将揭示这些对象之间的组合模型。

2.3 外围三包:声明式装配的完整闭环

  • loader 定义"声明 → 运行"的映射:EntryTree(配置树)→ Entry(单个条目)→ EntryGroup(条目组)→ ctx.plugin() 实例化出 Fiber。它还负责把 Node 内部 ModuleLoader(分 Node 22/23 的 v1 与 Node 24 的 v2 两个版本)接入,使得"import 一个插件模块"可以被追踪与清理。
  • include 让配置文件具备两种身份:既是输入(启动时读入装配),也是输出(插件修改配置时回写文件,原子写保证安全)。
  • hmr 站在二者之上:监听文件变化 → 分析 ESM 依赖图 → 分类 accepted/declined → 清理双缓存(ESM loadCache + CJS require.cache)→ 重载相关插件,失败则整体回滚。


三分 · 设计理念与架构模式

3.1 三大核心理念

理念一:一切皆插件。 没有"框架内置功能"与"第三方插件"的硬界线:loaderincludehmrtimergroup 全部以插件身份装入(loader 自己就是 Loader extends EntryTree 的服务插件;连 ctx.plugin(isolate) 都被 loader 用于装配隔离域)。框架与扩展只有"内核原语"与"普通插件"的分层,没有特权。

理念二:一切皆服务。 服务是组件的对外接口:一个服务以稳定的键(如 ctx.timerctx.loader)挂在 Context 上,其他插件按而不是按具体实现来找它。依赖倒置在这里被贯彻到极致——插件之间从不互相 import,只通过 ctx.<key>inject 声明相遇。

理念三:注册皆可逆。 任何跨时间的资源占用都必须"成对出现":ctx.effect(() => { /* 安装 */ return () => { /* 卸载 */ } })。事件监听、服务提供、定时器、文件 watcher……全部收敛到这一条纪律上。这是 Cordis 全部可组合性的一号定理。

图 1-3 · Context → Fiber → Plugin 组合运行模型:图中看到 Cordis 的"三体"关系——Context 是舞台(服务仓库 + 事件总线),Fiber 是演员(插件的生命周期化身),Plugin 是剧本(函数签名 / Service 类 / apply 对象)。一个插件被多次装载,就有多个 Fiber;而 Fiber 的生死完全由 epoch 依赖图驱动。

ctx.plugin

实例化

再装载

注册服务

inject 等待

监听事件

监听事件

notify 通知变化

notify 通知变化

Plugin 三种形态
Function / Service 子类 / Object.apply

Context 代理对象
(ReflectService.handler)

Fiber A
epoch=1, ACTIVE

Fiber B
epoch=1, ACTIVE

依赖声明 inject

服务仓库 reflect.store
key → symbol → Impl

事件总线 EventsService
internal/* 与业务事件

3.2 架构模式识别

Cordis 不是发明新花样,而是把四种经典模式"可逆化"地焊在一起:

  1. Service Locator(服务定位器)+ IoC:经典 DI 容器的问题在于——服务一旦注册便成为全局事实,卸载无从谈起。Cordis 保留了"按名查找"的形态,但将注册动作本身放进 Fiber 的效果链,使服务的安装与卸载都是可逆事务(详见具体观第 2 节对 provide 的解剖)。
  2. Proxy 拦截器Context 的属性访问全部经由 ReflectService.handler 这个 Proxy handler 解释——ctx.xxx 的每一次读操作都是一次"运行时路由"。这把「依赖查找失败、isolate 越界」等错误从"悄悄 undefined"变成"掷地有声的异常"。
  3. 事件总线(内部事件族)events.ts 内置 internal/plugininternal/statusinternal/serviceinternal/updateinternal/getinternal/setinternal/listenerinternal/dispatch 八个内部事件。框架自身就是事件驱动的:loader 拦截 internal/update 回写配置,reflect 通过 internal/service 通知服务变化——内部总线与业务事件共用一套分发机制,真正做到"知行合一"。
  4. 状态机驱动:Fiber 从诞生到销毁严格走在 PENDING → LOADING → ACTIVE → UNLOADING 的轨道上(另有 FAILED / DISPOSED 两个终态),并由"时刻变化的 epoch 指纹"裁决何时迁移(详见 4.2)。

3.3 一句话总结本节的架构观

Cordis 的架构可以概括为:一个可代理的 Context(所有读取皆路由)、一组可回滚的 Fiber(所有写入皆事务)、一张可广播的依赖图(所有变化皆通知)。三者叠加,构成了动态组合的最小完备集。


四分 · 实现原理概述

本节只做鸟瞰,算法级别的细节留给《具体观》。这里的目标是让你带着"它们大概怎么工作"的直觉继续前进。

4.1 Context 是"宇宙":唯一可变量

Context 是整个系统的唯一"坐标系原点"。new Context() 会创建 ReflectServiceRegistryServiceEventsServiceLoggerService 四件套,并把返回的 Proxy(而非原始对象)作为对外身份。之后所有扩展——ctx.extend()ctx.isolate()ctx.intercept()——都只是在这个坐标系上切出子空间。读取 ctx.xxx 时,Proxy handler 按"特殊属性 → 服务访问 → 内部 get 事件 → fiber.store 沿父链冒泡"的顺序解析;任何未声明的读取都会抛错,而不是静默返回 undefined"找不到"是一种一等错误,这让配置错误在启动期就显形。

4.2 Fiber 是"生命周期":一个插件实例的时空胶囊

每次 ctx.plugin(plugin, config) 都会诞生一个 Fiber。Fiber 的核心职责有三:

  • 承载效果:插件回调体内所有 ctx.effect() 注册的 disposer,都被收进该 Fiber 的 _disposablesDisposableList),卸载时逆序执行(先装后拆,天然满足依赖的栈式语义)。
  • 计算依赖:Fiber 读取其 inject 声明中的每个服务,把服务提供者的 uid 拼进一个 epoch 字符串——epoch 就是 Fiber 的"依赖指纹"。任何被依赖的服务卸载/重建,epoch 都会变化,触发该 Fiber 的 _refresh()
  • 接受惯性:当依赖频繁抖动(如 HMR 期间连续重载),this.inertia 锁保证"卸载—重载"的循环不会并发踩踏,且无论中间抖多少次,最终收敛到一致状态。

图 1-4 · Fiber 六态状态机:这张图是 Cordis 的"心跳图"。_setEpoch 是唯一的换挡器:epoch 激活则 _reload(LOADING → ACTIVE/FAILED),epoch 失效则 _unload(UNLOADING → 重载或归位 PENDING),而 dispose()uid 置空进入终态 DISPOSED。惯性与 epoch 的配合保证:状态无论怎么抖,最终只会停留在 ACTIVE 或 PENDING。

_setEpoch 激活

_reload 成功

_reload 抛错(_error)

依赖 epoch 失效

加载中 epoch 变化

依赖恢复,急切重载

卸载完成不再重启

清理失败状态

dispose()

dispose()

dispose()

PENDING

LOADING

ACTIVE

FAILED

UNLOADING

DISPOSED

4.3 服务依赖 = 响应式广播

ctx.reflect.provide('timer', timer) 做两件事:把实现按"isolate 键"存入全局 store,并在当前 Fiber 的效果链上登记"卸载时删除"。任何 inject: ['timer'] 的 Fiber 都会在 notify 时被 _checkImpl 检查:服务是否已就绪?若就绪,_refresh() 重算 epoch 并重载依赖方;若未就绪,依赖方保持 PENDING 等待。这就是"空间可组合性"的运行时形态inject 是空间上的声量,notify 是空间上的回响。

4.4 配置驱动装配:声明 → 运行

cordis.yml 里写下的每个条目(- id: xxx, name: '@scope/pkg'),由 include 解析为 EntryOptions,loader 将其组织成 Entry 树,最后每个 Entry 调 registry.plugin() 生成对应 Fiber。配置树与运行树一一对应:改配置 = 改声明;运行树会自动 diff 并执行最小化的装载/卸载。这一步把"程序的启动脚本"变成了"可被增量计算的声明"。


五分 · 整体流程

5.1 端到端启动链路

packages/core/bin.jscordis 包自带的 CLI 入口)为例,完整链路只有三步,却走完了整个框架:

const ctx = new Context()
ctx.baseUrl = pathToFileURL(process.cwd()).href + '/'
await ctx.plugin(Loader)              // ① 装入 loader 插件
await ctx.loader.create({             // ② 声明 include 插件读 cordis.yml
  name: '@cordisjs/plugin-include',
  config: { path: './cordis.yml' },
})

执行时发生的事情:new Context() 创建内核四件套 → ctx.plugin(Loader) 让 Loader 开始托管声明式加载 → loader.create(...) 把 include 作为 tree 根节点挂上 → include 读取 cordis.yml(js-yaml + !!js 自定义类型)→ root.update(data) 触发 EntryGroup.update 的 oldMap/newMap 差量调和 → 每个新条目 Entry._init()import(name) 真加载插件模块 → ctx.registry.plugin(plugin, config) → 生成 Fiber_reload() 执行插件回调 → ACTIVE。

图 1-5 · 启动时序与配置变更闭环:上半段展示"文件 → Entry → Fiber → ACTIVE"的装配时序;下半段的 loop 块展示运行期闭环——配置文件变化后,include 重新读取并驱动 diff 更新,插件 Fibers 随即被增量调整。启动是一条线,运行是一个环。

FileSystem Plugin Fiber EntryTree/Group include 插件 loader 插件 bin.js 入口 FileSystem Plugin Fiber EntryTree/Group include 插件 loader 插件 bin.js 入口 loop [运行期配置变更闭环] ctx.plugin(Loader) loader.create(include) mount EntryTree 读取 cordis.yml(!!js 求值) root.update(data) oldMap/newMap diff entry._init() → import(name) registry.plugin() → _reload() ACTIVE 就绪 文件 change 事件 refresh() 重读 root.update(新数据) entry.update → fiber.restart() 增量重载完成

5.2 运行期的两个闭环

  • 配置闭环(空间的响应)internal/update 是配置变更的统一入口。include 的文件变化、entry.update() 的程序化修改、插件内 ctx.fiber.update(config) 的行为,都会经瀑布 internal/update 汇聚到对应 Entry,最终落到 fiber.restart()——配置即事件,事件即配置
  • 生命周期闭环(时间的响应):服务 provide/卸载、HMR 重载都会触发 notify → 依赖方 _refresh() → epoch 变化 → 受影响 Fiber 走 UNLOADING → LOADING。任何一个字节的变化,都会在依赖图上荡开一圈涟漪,而这些涟漪全部是可逆的。

5.3 卸载回滚链

最高层的抽象是一条逆时间线:dispose() 置空 uid → emit internal/plugin → registry 移除 → _setEpoch(INACTIVE)_unload() → 逆序执行全部 disposer(服务卸载、事件解绑、timer 清空,全部成对)→ 通知依赖方它们的世界已变。无怪乎论文把这一整套称为"revertible effects"——每一段副作用都带着自己的逆变换,时间在这里可以被倒放



六分 · 使用指南与典型案例

实战向章节。DeepSeek Harness 项目(parent directory 中的 @deepseek-ai/deepseek-harness)已将 Cordis vendor 化,其 docs/cordis-primer.mdexamples/*/cordis.yml 是本框架真实生产用法的一手素材,下列指南与案例均以它们为蓝本。

6.1 五个核心概念速查

用 Cordis 写插件,你只需要理解五件事(对应 cordis-primer.md 的 “Cordis In Five Ideas”):

# 概念 一句话 对应 API
1 插件(Plugin) 一个有 apply(ctx, config) 的函数、Service 子类、或 { apply } 对象的普通模块 ctx.plugin(fn, config)@Inject 装饰器
2 上下文(Context) 服务的仓库;服务以稳定键 ctx.<key> 挂载 new Context()ctx.extend()
3 声明依赖(inject) 插件声明所需服务;运行时等待其就绪,代替手工排序 inject: ['tools', 'llm']
4 类型化事件 服务用 TS 声明合并定义事件名与负载,按语义选分发模式 ctx.emit / waterfall / parallel / serial
5 可逆注册 一切注册都走 ctx.effect(),卸载时逆序回滚 ctx.effect(() => () => {})

6.2 事件分发模式指南

每种事件在声明时就要定下一种分发模式(dispatch mode),并且只能由对应方法派发。primer 给出四种,加上 Cordis 内置的短路模式 bail,共五种:

模式 方法 是否 await 顺序 有返回值? 典型用途
观察 emit 注册序 生命周期通知、日志
环绕中间件 waterfall 注册序 有(next() 传递) 请求渲染、策略裁决
扇出 parallel 并行 可并发的通知
顺序裁决 serial 注册序 顺序管道,可短路
短路 bail 注册序 有(首个非假值即停) 单决策事件

waterfall 语义精讲(primer 原文核心):ctx.waterfall 是"包裹式中间件"。监听器收到 (...args, next):调用 next() 把(可能被包裹的)结果交给下一个服务;不调用 next() 直接 return,就是短路。值经由 next() 的返回值向下游传播。协作型监听器通常改写共享的请求/决策对象后放行;prepend: true 仅当必须跑到普通注册之前时使用。规则:新事件的 @mode 声明要与派发调用点交叉校验,声明与调用不一致即视为公共契约破损。

6.3 配置编写指南

@cordisjs/plugin-include!!js 解析为表达式节点,cordis.yml 因此是"活"的:

  • 条目结构id(唯一标识,: 分隔分层)、name(包名/模块名)、config(插件配置)、inject(覆盖依赖声明)、disabled(是否停用)、group(嵌套子组)。
  • !!js 求值时机(primer 原文规则):条目的 config已声明的注入激活之后、针对该插件的上下文(ctx.serviceName)求值;disabled 则在每次装载决策时、针对 loader 上下文求值;include 会保留嵌套行表达式直到目标激活。其余元数据保持字面量。
  • overlay 补丁(insert:当环境决定"在既有装配之上追加"时使用 - insert: 语法,按 id 定位目标 group 后追加条目(见 6.5 案例)。

6.4 真实案例一:jsonrpc-agent 的扁平装配

deepseek-harness/examples/jsonrpc-agent/cordis.yml 是一个无人值守的 coding-agent 部署(stdout 预留给 JSON-RPC),16 个条目平铺在顶层列表:

- id: sdk-jsonrpc-server
  name: '@deepseek-ai/dsh-sdk-jsonrpc-server'
  config:
    maxTokensAsSuccess: !!js "process.env.DSH_MAX_TOKENS_AS_SUCCESS === undefined ? true : JSON.parse(process.env.DSH_MAX_TOKENS_AS_SUCCESS)"
- id: llm-deepseek
  name: '@deepseek-ai/dsh-llm-deepseek'
  config:
    thinking: enabled
    reasoningEffort: max
- id: subprocess
  name: '@deepseek-ai/dsh-subprocess-local'
- id: bash
  name: '@deepseek-ai/dsh-bash-local'
  config:
    cwd: !!js process.env.DSH_CWD ?? process.cwd()
    timeoutMs: 60000
- id: agent-spine
  name: '@deepseek-ai/dsh-agent-spine-demo'
  ...
- id: sessions
  name: '@deepseek-ai/dsh-session-persistence-jsonl'
  config:
    root: !!js process.env.DSH_SESSION_ROOT ?? './.sessions'
    compression: !!js "process.env.DSH_SNAPSHOT === undefined ? 'zstd' : 'none'"
- id: token-meter
  name: '@deepseek-ai/dsh-token-meter'
- id: compaction-basic
  name: '@deepseek-ai/dsh-compaction-basic'
  config:
    thresholdRatio: 0.8
    retainRatio: 0.16
    maxTokens: 8192

三个值得玩味的细节:

  1. 装配顺序不决定加载顺序lsdk-jsonrpc-server 在前、tool-fs 在后,但谁先 ACTIVE 由 inject 依赖图决定,而不是文件顺序——这正是"声明式替代命令式排序"的现场演示。
  2. !!js 环境插值cwd: !!js process.env.DSH_CWD ?? process.cwd() 在插件上下文求值,使得同一份配置可以部署到不同机器而无需改动 YAML;compression 行则用 DSH_SNAPSHOT 是否设置来切换 zstd/明文帧——配置文件的语义是启动时刻的,不是编辑时刻的
  3. 无配置条目依然有意义subprocesstoken-meter 只写了 id/name,表示"用默认配置装载",是配置冗余度控制的范例。

6.5 真实案例二:web-schedule 的 overlay 补丁

web-schedule/cordis.yml 只有 8 行,却演示了 Cordis 最优雅的用法——在既有装配上叠一层

# Opt-in Schedule patch over the shipped Web composition. The owner observes
# only roots published after this overlay loads.
- insert:
    - id: time-context
      name: '@deepseek-ai/dsh-time-context'
    - id: schedule
      name: '@deepseek-ai/dsh-schedule'

- insert: 告诉 include:把下面的条目插入到现有配置树的根 group(top-level list)之后,而不是替换它。于是"web 组合 + 时间上下文 + 调度"成为一棵树,而补丁文件本身可以独立开关、独立版本化——组合是增量叠加出来的。insert 的实现要点:id 定位目标 group,同名冲突需 name 校验,config 等覆盖字段按 overrides 规则合并(详见具体观第 6 节)。

6.6 实践规则三条(primer “Practical Rules” 提炼)

  1. 按领域封装插件:工具管道事件归 ctx.tools,模型流式归 ctx.llm,agent 协同归 ctx.agents——服务键就是领域边界。
  2. 事件 vs 服务方法选型:拦截与策略用事件(可插拔、可短路);直接能力调用用服务方法(可靠、可 await)。不要用事件做"必然会执行一次"的调用。
  3. disposer 纪律:每个注册都要有 disposer——要么在 ctx.effect() 里返回它,要么用 Cordis 自带 helper(ctx.on()ctx.timeout() 等已内置)。如果拆卸顺序重要,把相关操作放进同一个 effect,让回滚按意图顺序展开。

总述 · 收束:一张认知地图

把全文合起来,Cordis 的整体画像是一张四象限的地图:Context 是坐标系(一个可代理、可扩展、可隔离的宇宙,所有读取皆经路由);Fiber 是时间(每个组件的一生是一条可逆的时间线,epoch 是快进/回放的档位);Reflect 是空间(服务仓库上空的依赖图,notify 是每一次空间地震的余波);Loader/Include 是地图本身cordis.yml 的声明树与运行树一一对映,改图即改世界)。四个部件都不大——内核 9 个文件——但它们凑在一起,就让"动态组合"从工程技巧升格成了一个有论文、有演算、有实现的范式。

现在你已经"看见"了 Cordis。下一篇《具体观》将潜入水下,逐一切开这四件套的算法与实现——每刀都配上论文与代码。

下一篇预告:可逆效果系统(_execute 的四种归一化)、响应式余效应(provide/notify 双通道)、Proxy 语义(createTraceable)、五种事件分发、长栈工程(composeError)、配置调和(group diff)、热替换(accepted/declined 不动点)。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐