运行时不变量
一句话版:
@deepseek-ai/dsh-invariants提供ctx.invariants注册表服务;每个工作区包用一个./invariant配套入口注册对自身 npm 包名契约的运行时检查,违规抛带稳定code:'INVARIANT'和packageName的InvariantError。这是 DSH 对 session/agent/hook/compaction/goal/jobs 等运行时契约做持续自检的机制。
这是 运行时自省与临时插件 的姊妹篇:前者让模型手动检查/临时扩展运行时,这里让包自己声明"我的运行时契约不能坏"。读完你能给自己的插件加运行时健康断言,并理解 DSH 的持续自检机制。
一、整体模型
- 服务层:
InvariantRegistry(ctx.invariants)是一个可配置注册表,不含任何产品检查、也不导入产品包。 - 配套入口层:每个工作区包发布一个
./invariant伴随入口,注册它精确的 npm 包名。 - 检查策略:
README明说 —— 发布和注册是全覆盖的,但运行时断言刻意不人为捏造。只在包拥有可观察的事件关系或相关可变数据关系时装检查。
二、服务:ctx.invariants
// packages/runtime-diagnostics/invariants/src/index.ts
interface Config {
/** Global switch; defaults to `true`. */
readonly enabled?: boolean
/** Case-sensitive JavaScript regex sources that admit package names; empty admits all. */
readonly package_allowlist?: string[]
/** Case-sensitive JavaScript regex sources that exclude package names after allowlist matching. */
readonly package_blocklist?: string[]
}
| 配置项 | 默认 | 说明 |
|---|---|---|
enabled | true | 服务级总开关;false 时所有包都不被选中 |
package_allowlist | [] | 逐一用 new RegExp(pattern) 编译的正则源;空则全部放行 |
package_blocklist | [] | 同样正则源;blocklist 匹配优先于 allowlist |
// packages/runtime-diagnostics/invariants/src/index.ts
private selected(packageName: string): boolean {
if (!this.enabled) return false
if (this.packageAllowlist.length > 0
&& !this.packageAllowlist.some(pattern => pattern.test(packageName))) return false
return !this.packageBlocklist.some(pattern => pattern.test(packageName))
}
选中的三个条件缺一不可:服务启用 ∧(allowlist 空 ∨ 至少一个 pattern 匹配完整 npm 名)∧ 无 blocklist pattern 匹配。
正则过滤的约定
- 匹配是区分大小写的 JS 正则源,用
new RegExp(pattern)编译;不解析/pattern/flags语法。 - 除非源自供
^/$,否则不锚定(子串匹配)。 - 同一列表里空白、带前后空格、无效、或重复条目 → 服务启动失败(
index.ts的compilePatterns)。 - 有效 pattern 可以不匹配当前任何已加载包,这样后续加载和 HMR 保持确定性。
register(packageName, installer)
// packages/runtime-diagnostics/invariants/src/index.ts(截选)
register(packageName: string, installer: InvariantInstaller): () => void {
// 校验包名非空、无空格、不重复注册
// const ctx = this.ownerCtx
return ctx.effect(async () => {
if (!this.selected(packageName)) {
return () => { registrations.delete(packageName) }
}
const child = ctx.plugin(installer.inject === undefined
? installInvariant
: Object.assign(installInvariant, { inject: installer.inject }))
try { await child } catch (error) { await child.dispose(); throw error }
return async () => { try { await child.dispose() } finally { registrations.delete(packageName) } }
}, `invariants.register(${JSON.stringify(packageName)})`)
}
要点:
- 保留一个活动注册:即使过滤器让 installer 保持非活动,包名也被保留(用于占位)。
- 专用子 fiber:enabled 贡献在专用子 Cordis fiber 里运行,通过
installer.inject可声明所需服务接口。 fail(message)注入:installer 收到fail,调用它抛出一个绑定到注册包的InvariantError(不会返回)。- 启动 join:同步或异步 installer 完成都会被 join,注册成功前不返回;失败会原子地 dispose 子 fiber 并释放归属,绝不留"半注册"。
- disposer 归属:服务拥有每个注册 fiber;返回的 disposer 还属于配套 fiber。卸载任一侧都会移除监听器、跟踪状态与保留,所以配套入口可以重载并重新注册同一包名而不留旧状态。
- applies to reload:session 支撑的配套入口从持久事件重建 baseline;仅 live 的配套入口观察 reload 之后开始的操作。
三、InvariantError 与其契约
// packages/runtime-diagnostics/invariants/src/index.ts
export class InvariantError extends Error {
readonly code = 'INVARIANT' as const
readonly packageName: string
constructor(packageName: string, message: string) {
super(`invariant violated by "${packageName}": ${message}`)
this.name = 'InvariantError'
this.packageName = packageName
}
}
InvariantError 扩展 Error,携带稳定的 code: 'INVARIANT'(机器可读),并暴露归属的 packageName,且不给服务本身加任何产品依赖。
四、配套入口:./invariant 伴随文件
每个包的配套入口是一个标准 cordis 插件,但在 install 阶段注册。以 dsh-invariants 自己的配套入口为例:
// packages/runtime-diagnostics/invariants/src/invariant.ts
const PACKAGE_NAME = '@deepseek-ai/dsh-invariants'
export const name = 'invariants-invariant'
export const inject = ['invariants']
/* 空 installer:注册归属与子生命周期本身就是服务的变更边界,
* 在同一注册表里观察它只会重复实现。 */
const install: InvariantInstaller = () => {}
export const apply = (ctx: Context): Promise<() => void> =>
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
配套入口的约定:
export const name=<package>-invariant;export const inject = ['invariants'](缺服务就挂起等待)。apply调用ctx.invariants.register(PACKAGE_NAME, install)并返回 disposer。- 空 installer:没有合理运行时关系时,用空 installer + 包专属的前缀注释
No runtime invariant: ...说明为何(纯工具、行为已被接口包观察的薄实现、仅组合包、二进制、持久化适配器、测试支持包都常见)。当 owner 获得可变状态或事件协议时,必须重新审视该说明。
加一个真实检查的配套入口
要给自己的插件加运行时健康断言,在包目录加一个 src/invariant.ts,让 apply 用 installer.inject 声明所需服务,并在 installer 里用 fail() 报违规:
// my-plugin/src/invariant.ts
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@scope/my-plugin' // 必须是精确 npm 包名
export const name = 'my-plugin-invariant'
export const inject = ['invariants']
const install: InvariantInstaller = {
inject: ['tools'], // installer 需要声明的服务
(ctx, fail) => {
ctx.on('tool/call', () => { /* 观察事件,若违规 */ fail('trace invariant broken') })
},
}
export const apply = (ctx: Context): Promise<() => void> =>
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
关键判断:确认必需方法、插件名、注入、effect 或固定纯函数结果属于类型 / 加载 / 单元测试关注点,而非运行时不变量。 只有存在可观察的事件关系或相关可变数据关系时才装运行时检查。
五、DSH 对自己做持续自检的机制
DSH 把运行时契约的自检挂在这些包的配套入口上(README.md 的 companions 表):
| 配套入口 | 检查 |
|---|---|
dsh-session、dsh-agent、dsh-scope、dsh-agent-loop | 会话包含关系、调用/结果跟踪、agent 状态转换、inbox FIFO 守恒、作用域 subject、模型请求重建 |
dsh-llm、dsh-llm-retry、dsh-tools、dsh-system-prompt | 流语法、持久重试位置与边界、工具流水线阶段与冻结结果、权威提示词组装数据 |
dsh-compaction、dsh-hook-protocol、dsh-sandbox-policy | 持久 compaction 与钩子配对、compaction 元数据、沙箱 mode 词汇 |
dsh-fs、dsh-subagent、dsh-workflow | 文件系统事件身份、provider/子级配对、workflow 与 agent 生命周期身份 |
dsh-goal、dsh-goal-round-driver | 持久 goal 来源/内容一致性、修订与生命周期转换、时间戳、依次获准的 round、重建的继续提示词 |
dsh-permission-presets、dsh-user-approval | 活动 preset 引用、审批 asked/decided 审计配对 |
dsh-jobs、dsh-tool-todo | 任务快照生命周期/归属字段、持久整表 todo 结构 |
dsh-time-context | 持久时钟读数与会话"正在进行轮次/下一步骤位置/已用时间基线"一致;渲染时间可解析、不晚于其事件 |
关键区分:Session 自己负责不可变、表面有效的日志存储(每候选项一次无损 JSON 快照、校验引用源事件覆盖与位置替换、tool/result 替换限为一个当前结果的 content、深冻结、经不可变数组快照暴露);dsh-session 配套入口检查的是 Session 不负责的其余跨记录规则。这符合"每包对自己的 npm 契约负责、服务只是注册表"的定位。
六、挂载与组合
标准 agent 组合挂载服务和四个核心有状态配套入口;自定义组合为想检查的其他已加载包显式加配套入口。过滤器可以不改包入口就禁用/选择注册。
// packages/runtime-diagnostics/invariants/README.md(组合示例)
import type { Context } from '@deepseek-ai/cordis'
import InvariantRegistry from '@deepseek-ai/dsh-invariants'
import * as SessionInvariant from '@deepseek-ai/dsh-session/invariant'
declare const ctx: Context
ctx.plugin(InvariantRegistry, {
enabled: true,
package_allowlist: ['^@deepseek-ai/dsh-'],
package_blocklist: ['^@deepseek-ai/dsh-agent-loop$'],
})
ctx.plugin(SessionInvariant)
每个 owner 的根入口独立于诊断:单独加载服务不装任何产品检查;没有服务时加载配套入口会等它的 invariants 注入。根入口普通 / Vitest 拓扑挂载显式启用的服务 + 当前测试包的配套入口;聚焦套件覆盖可执行配套入口的合法与违规观测,一个穷尽拓扑挂载全部配套入口以证明注册与 dispose 接线。
七、验证与试一试
# 全仓最低归属校验:发现所有工作区包,拒绝生成标记、未说明的空 installer、
# 省略/忽略 reporter 的非空 installer、错误注册名,以及不完整的导出/发布/依赖/
# TS 引用/bundle 接线
cd ~/.dsh/source/current && pnpm run verify-package-invariants
# 单独看某个包根入口是否独立于诊断(应能加载而不装产品检查)
grep -rn "invariants" packages/runtime-diagnostics/invariants/package.json
下一步
- 运行时自省与动态插件:Inspect 后定义不可变 Package,并通过
run/update/stop/undefine管理生命周期 - 写一个服务:能力,而非只是工具
- 监听事件:在事件上挂钩