用五个概念理解 Cordis
Cordis 是 dsh 底层的内嵌(vendored)插件框架。五个概念支撑起一切:
- 插件是实现 Service 的对象——一个带可选
inject与apply(ctx)字段的函数,或一个 Cordis 挂载到当前上下文中的Service子类。 - 上下文是服务的仓库——服务认领一个稳定的
ctx.<key>,如ctx.tools或ctx.llm。 - 通过
inject声明依赖——插件列出它需要的服务并等待其就绪。 - 用类型化事件通信——通过 TypeScript 声明合并声明,以
emit、waterfall、parallel或serial分发。 - 注册项是可逆副作用——通过
ctx.effect()或ctx.on()安装,使重载与拆除时可预期地展开。
产品的每个部分都是插件,包括模型适配器、工具注册表、session 日志乃至 agent 循环本身——因此每个部分都可以从配置替换。
包
框架位于 vendor/ 下,重命名为 @deepseek-ai/ 作用域(fork 自 cordis / @cordisjs/*)。每个都是独立的 workspace 包:
| 内嵌包 | 版本 | 角色 |
|---|---|---|
@deepseek-ai/cordis | 核心:Context、Service、fiber、ctx.effect、类型化事件 | |
@deepseek-ai/cordis-plugin-loader | 运行时插件加载器:持有 EntryTree、导入模块、应用配置、保持图同步 | |
@deepseek-ai/cordis-plugin-include | 文件驱动的加载树:把 YAML/JSON 文件读成条目、应用补丁、写回 | |
@deepseek-ai/cordis-plugin-group | 把 provider 与它的消费者放进同一个 isolate 领域的行 | |
@deepseek-ai/cordis-plugin-hmr | 由 loader 管理的插件的热模块替换 | |
@deepseek-ai/cordis-plugin-logger-console | 控制台日志 | |
@deepseek-ai/cordis-plugin-timer | 可感知销毁的定时器(ctx.timeout、ctx.interval) | |
@deepseek-ai/schemastery | 类型驱动的 schema 校验器(配置校验) | |
@deepseek-ai/cosmokit | 共享工具集 |
服务与 ctx
服务是安装在 ctx.<key> 上的值。消费者 inject 它而不是 import 具体类。例如 packages/core/agent-loop/src/index.ts 声明:
export class AgentLoop extends Service implements AgentFactory {
static inject = ['agents', 'sessions', 'llm', 'tools', 'systemPrompt']
}由于 dsh 通过声明合并把 Service 本体安装进 @deepseek-ai/cordis,整个产品共享同一个上下文。packages/core/* 对照表(见 架构一览)列出主干 key——ctx.sessions、ctx.systemPrompt、ctx.tools、ctx.agents、ctx.agentLoop——而各组中包所属的接缝还覆盖 ctx.llm、ctx.fs、ctx.shell、ctx.terminals、ctx.subprocess、ctx.sandbox、ctx.commands、ctx.jobs 以及几十个更多。
类型化事件与分发模式
每个事件只有一种分发模式,由对应方法分发:
| 模式 | 是否等待 | 分发顺序 | 返回值 |
|---|---|---|---|
emit | 否 | 监听者按注册顺序观察 | 无 |
waterfall | 否 | 监听者按注册顺序观察 | 有 |
parallel | 是 | 所有监听者并行观察 | 无 |
serial | 是 | 监听者按注册顺序观察 | 有 |
事件通过 TypeScript 模块增强声明,带 @mode 标记,让生成的目录可以把声明与分发点对照检查。例子来自 packages/core/agent/src/runtime-types.ts:
declare module '@deepseek-ai/cordis' {
interface Events {
/** … @mode waterfall */
'agent/pre-step'(this: Scoped<Agent>, payload: {
agent: Agent; messages: UserMessage[]; turn: number; step: number; signal: AbortSignal;
}, next: () => Promise<PreStepDecision>): Promise<PreStepDecision>
}
}Waterfall 是环绕式中间件。监听者收到 (...args, next);调用 next() 委托被包裹的结果;不调用 next() 直接返回则短路。新 harness 事件用 @mode 记录模式。
可逆副作用与插件生命周期
每个注册项都应有一个 disposer——要么由 ctx.effect() 返回,要么由 Cordis 辅助函数处理。Cordis 把每个插件跟踪在一条 fiber 上;插件挂载时执行其 apply(ctx),卸载或重载时,通过该上下文安装的每个副作用按相反顺序展开。这就是为什么 profile 重载(HMR)与完整拆除行为可预期:prompt 段、工具 schema、监听者与 provider 都随其所属插件一起消失。
loader 把配置应用到一行,并通过 ctx.loader.await() 让插件图 settle;dsh 的 assertEntriesActivated(packages/boot/app-boot/src/index.ts)在已 settle 的树中,若某个启用条目没有 fiber 或仍处于 pending,则拒绝该树。
配置行与 schemastery 校验
loader 配置是一个行列表;每行命名一个插件并携带 config。include 插件解析 YAML/JSON 方言,!!js 标量变成 Loader 针对条目注入就绪上下文插值的表达式节点。补丁按 id 瞄准一行并替换其整个 config,或插入新行。
每个可配置插件通过 schemastery 校验其行。例如 AgentLoop.Config:
static Config = z.object({
maxParallelToolCalls: z.number().step(1).min(1).default(DEFAULT_MAX_PARALLEL_TOOL_CALLS),
agents: z.array(z.object({
id: z.string().required(), sessionId: z.string().min(1), provider: z.string(),
model: z.string(), maxTokens: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER),
cwd: z.string(), resumeSessionId: z.string(),
})).default([]),
}) as z<Config>z 就是 schemastery——既可作校验器也可作构造器,可通过 Schema.extend() 扩展,且可跨环境序列化/水合。
dsh 如何包装 Cordis:host runner 与 client runner
扩展系统有"面向模型"的一半和浏览器可见的一半:
@deepseek-ai/dsh-cordis-host-runner(提供ctx.dynamicCordisRunner)——动态包的 host 半:定义注册表、为cordis-dynamic组 fiber 下的 host 半提供node:vm沙箱、invoke-handler 表,以及 request-run 往返。纯 host 包在本进程内运行;带浏览器半的包发出cordis/request-run、挂起,并等待人来允许或拒绝它。@deepseek-ai/dsh-cordis-client-runner——加载进页面的浏览器半(通过包的dshClient声明);其 host 侧apply()为空,因此该行出现在 host 配置中而逻辑由浏览器半承载。@deepseek-ai/dsh-tool-cordis——在ctx.tools上注册面向模型的工具:cordis_inspect_list、cordis_inspect_query、cordis_inspect_self、cordis_define、cordis_run、cordis_stop、cordis_undefine,外加一个可读改 harness 检出自身的自引用工具集。@deepseek-ai/dsh-ui-cordis——浏览器界面:操作每个定义的框架级面板与只读的 define 卡片。
仓库中的一个最小插件形态
普通(非动态)插件的形态就是 Cordis 的形态。web-app 命令行 provider packages/bundle/web-app/src/startup.ts 具有代表性:
export const name = 'web-startup'
export const inject = ['cmdlineArgs']
export const WEB_STARTUP_SERVICE = 'webStartup'
export function apply(ctx: Context): void {
const program = new Command() // 为 --host/--port/--trusted-host 准备的 commander 程序
ctx.provide(WEB_STARTUP_SERVICE, { … })
}而 tool-cordis 通过产品 API 注册工具(packages/extensions/tool-cordis/src/index.ts):
export const name = 'tool-cordis'
export const inject = ['tools', 'systemPrompt', 'dynamicCordisRunner', 'cordisInspect']
export function apply(ctx: Context): void {
ctx.systemPrompt.section({ name: 'tool:cordis', order: 115, text: CORDIS_SYSTEM_PROMPT })
ctx.tools.register(defineTool({
name: 'cordis_inspect_list',
description: 'List every Cordis Inspect Provider …',
parameters: {},
execute(_args, _exec) {
return Promise.resolve({ providers: ctx.cordisInspect.list() })
},
}))
}通过 apply(ctx) 注册的一切都是作用域/副作用绑定的:行卸载时,prompt 段与工具注册项随之展开。
dsh-plugin 生态
"树外插件"指通过 dsh plugin --profile <name> add <pkg>(apps/cli/src/plugin.ts 中的轻量 pnpm 转发器)安装进 profile 的包。只有当其 package.json 声明 dsh.bundle.patch 时它才成为 profile 层。想成为 Cordis 插件行的插件,是任何 Cordis 能挂载其默认导出(apply/Service)的包;想成为可复用 bundle,它还需附带 cordis.patch.yml 与 dsh.bundle 清单字段。盒内插件通过修复后的扁平 profiles/node_modules 回退访问,因此树外同级解析到安装的单一 Cordis 实例。
延伸阅读
- Cordis 入门——同一组五个概念的更深入导览(仓库中的
docs/cordis-primer.md)。 - 运行时与 Agent 生命周期——事件与副作用如何驱动真实 agent。
- 启动流程与 CLI——行与补丁如何变成运行中的树。
- 仓库文档:
docs/cordis-primer.md、docs/cordis-tutorial/index.md、docs/subsystems/extensions.md。 - 内嵌源码与 README:
vendor/README.md、vendor/cordis/、vendor/loader/、vendor/include/、vendor/schemastery/。 - 扩展包:
packages/extensions/README.md以及packages/extensions/下每个包的README.md。