启动、Profile、Bundle 与配置叠层
运行中的 dsh 如何由有序插件树组合出来
结论:启动器不是“创建应用”,而是求值一份配置程序
一次 dsh 启动的核心产物,不是某个预先写死的 Application 对象,而是一棵由空根、有序 patch 层、服务依赖和 Cordis 生命周期共同求值出来的插件树。
这一区分非常关键。Profile 决定“采用哪些 bundle”,bundle 给出大块产品能力,用户和命令行 overlay 修改最终配置;Loader 再根据服务是否可用驱动插件激活。配置在这里不是启动参数的集合,而是产品架构本身。
1. CLI 的职责边界很窄
apps/cli/src/bin.ts 先把 argv 解析成三种模式,再动态导入对应实现:普通 profile 启动、插件包管理、配置导出。profile 分支只传入冻结的环境快照、profile 名、按 argv 顺序保留的 --patch 文件和剩余应用参数。CLI-PROFILE-DISPATCH
因此,“CLI 参数”被刻意拆成两类:
| 输入 | 所有者 | 进入配置树的方式 | 在线重组时是否重算 |
|---|---|---|---|
--profile、--patch | 外层 launcher | 选择 profile 并形成 patch 层 | patch 文件内容重读;文件列表不变 |
| 剩余参数 | 树内应用插件 | 由 cmdlineArgs service 提供,不写进 patch | 不重算,随本次 invocation 存活 |
| 环境变量 | launcher 冻结 provenance | 挂树之前提供环境快照;!!js 也可读取进程环境 | launch-time 快照不变 |
命令行驱动能力没有侵入通用 boot。Web、headless 或未来的新 surface 可以把自己的 argv 解释器作为普通 provider 装入树;launcher 只拥有“选哪份配置程序”以及“如何安全结束进程”。
2. Profile 是可写的组合工作区
Profile 名必须是单一路径段,且不能是 .、.. 或 node_modules。首次使用内置模板时,web 初始化为 base + web-app,headless 初始化为 base + headless;初始化只补齐不存在的 package.json、cordis.patch.yml 和 pnpm-workspace.yaml,不会覆盖用户已有文件。PROFILE-TEMPLATES
| Profile 文件 | 职责 | 可写性与风险 |
|---|---|---|
package.json | dsh.profile.bundles 声明 bundle 的有序列表,同时容纳 profile 私有插件依赖 | 改变产品能力集合与解析闭包 |
cordis.patch.yml | 该 profile 的用户覆盖层 | 在线监听;可改配置、disable 或 insert 插件 |
pnpm-workspace.yaml | 为 out-of-tree 插件固定 hoisted linker 与 peer 策略 | 支持 profile 自己安装插件,同时共享 Cordis/service definition 实例 |
cordis.yml | Loader 所需的真实 include 锚点 | 每次启动重写为空数组;不应由用户维护 |
launcher 每次启动都会重写 cordis.yml 为空 entry list。这样即使 Loader 曾将运行树写回配置,也不会在下次启动把已经组合的 row 与 bundle insert 重复叠加;该文件只负责提供 profile 目录的解析锚点。PROFILE-ROOT
3. 精确的六层优先级
最终 entry list 从空数组开始,按下列顺序应用。越靠后优先级越高:
Bundle patches
严格按 dsh.profile.bundles 的 manifest 顺序串接;通常先 base,再 surface bundle。
Profile user patch
$DSH_HOME/profiles/<name>/cordis.patch.yml,只影响当前 profile。
Home user patch
$DSH_HOME/cordis.patch.yml,是跨 profile 的机器级偏好,因此覆盖 profile 层。
CLI overlays
所有 --patch 文件,按 argv 中的出现顺序应用。
Launcher assembly overlay
若存在 agent-presets row,launcher 注入随安装发布的只读 preset root。
Telemetry hard-disable
DSH_TELEMETRY_DISABLED 为任意非空值时,以最终 overlay 强制禁用对应 row;隐私开关宁可误关,不可误开。
allPatches() 将 bundle、profile、home 和 launcher overlays 展平;composeProfile() 先用前四层建立 row 索引,再追加 preset 与 telemetry 两个由 launcher 独占语义的覆盖层。PROFILE-COMPOSE
配置导出、flag 推导和真实启动都调用同一套 applyEntryPatches([], ...) 语义;列出的 bundle 如果没有在 manifest 中声明 dsh.bundle.patch 会直接失败,而不是被当作空层忽略。PROFILE-LOADPROFILE-ORDER-TEST
4. Patch 语义:按 row 定位,但 config 整块替换
dsh-base 在空根上执行一次大 insert,后续 patch 按 id 命中 row,最后写入者获胜。命中 row 时,config 是整体替换而非递归深合并;entry 在文件中的先后也不代表加载时序,激活由 service availability 驱动。BASE-BUNDLE-SEMANTICS
| 操作 | 效果 | 常见误区 |
|---|---|---|
{ id, config } | 替换目标 row 的完整 config | 只写一个字段不会保留 base 中的其他字段 |
{ id, disabled } | 改变该 row 的挂载资格 | disable 并非从配置中删除;后续层仍能明确覆盖 |
{ insert: [...] } | 加入新的插件 rows | 插入位置主要帮助阅读,不是依赖排序机制 |
命中不存在的 id | Loader 给出 skipped-patch warning | 允许共享 overlay 跨 surface 使用,但拼错 id 也可能只表现为警告 |
“整块替换”让单个 row 的归属和最终形状可预测,避免深合并数组、删除字段与表达式时出现隐式规则;代价是上层作者必须复述完整配置。它适合少量部署覆盖,不适合把巨大、频繁演进的嵌套对象直接暴露给普通用户。
5. 模块解析:安装层优先,Profile 层扩展
bundle 解析依次尝试 dsh 安装锚点与 profile 自身的 package.json。启动前还会对应用的 dependencies + peerDependencies 闭包做 BFS,把可解析包扁平链接到 $DSH_HOME/profiles/node_modules;Node 从 profile 私有 node_modules 向父目录回退时即可找到安装自带包。PROFILE-MODULE-FALLBACK
这形成两个同时存在的解析世界:
扁平 fallback 不只是方便找到包;它还尽量保证 out-of-tree 插件与官方包解析到同一个 Cordis 和同一组 service-definition 实例。若两边各带一份框架实例,类型看似相同,运行时 context key 和 registry identity 却可能彻底断裂。
6. Boot 是提交式事务,不是 best-effort 挂载
boot() 先创建根 Context、提供 dsh home、安装 Loader,再执行 host prepare;之后挂载根 Include、等待 Loader settle,并检查每个 entry 已激活。任一步失败都会先 dispose 部分根,再把阶段、包装链和最深层插件错误栈一并抛出。BOOT-TRANSACTION
| 阶段 | 可见状态 | 失败所有权 |
|---|---|---|
| Context + Loader | 尚无配置树插件 | 标记为 host preparation failure |
| Host prepare | 环境、argv 等冻结事实已提供 | 失败时处置整个根 |
| Include mount / settle | 插件按依赖逐步激活 | 事务性 Loader 包装失败;根统一回滚 |
| Activation audit | 只有已结算且存活的树可返回 | 缺 fiber / 永不激活都拒绝启动 |
| 运行期 | signal 或 surface 可处置根 | 根 fiber 的单次 disposal 统一撤销 effects |
这种启动协议把“插件声明存在”与“运行时真的可用”分开:只有 Loader 完成和 activation audit 通过才是提交点。它牺牲部分容错启动能力,换取不让半棵插件树悄悄服务请求。
7. 在线配置刷新保留最后一棵有效树
profile patch 或 home patch 变化时,launcher 会在每一代同时重新读取两个用户文件,并重新叠加固定 bundle 和更高优先级 overlays;每代使用新的深克隆,防止 Include 对 insert row 的原位修改污染下一次组合。刷新最终通过根 Include 的 entry.update() 事务化应用。PROFILE-LIVEBOOT-HMR
集成级测试依次写入有效配置、插件启动失败配置、语法错误配置、恢复配置并删除文件。两种失败都广播错误且继续保留上一棵有效树;恢复后应用新树,删除后退回应用自有层。PROFILE-HMR-TEST
Web bundle 暂时关闭共享模块 HMR,但 launcher 不允许配置热更新合同随之消失:若树中没有 HMR,它会临时挂载 timer 和一个无模块根的 watch-only HMR,只负责两个用户 patch 文件。
8. Base、Headless 与 Web:同一骨架,不同产品面
base bundle 直接装配 LLM registry、session、Typert API、Agent、jobs、retry、settings、credentials、多 provider adapter 与 JSONL persistence 等共享骨架,后面还继续加入工具、压缩、多 Agent 和 loop。BASE-BUNDLE-SPINE
| Surface | 在 base 之上增加什么 | Agent 能力归属 | 生命周期 |
|---|---|---|---|
| Headless | Code runtime、启动参数 provider、一次性 runner;不挂 Host/HTTP/Web/browser | base 中的进程级 Agent plane | 任务结束后请求有界退出 |
| Web | Host、storage、projection cache、HTTP/API、浏览器 module roster 与 UI | 禁用 base 的 per-Agent rows,改由每个 session preset realm 挂载 | 长驻,多会话并存 |
headless bundle 明确声明不挂载 Web/HTTP,并让普通 provider 解释 task positional,runner 再通过 core registry 创建 Agent。HEADLESS-BUNDLE
Web 逐项禁用 base 中属于单个 Agent 的工具、prompt、压缩与 delegation rows,让 preset 在会话域内重建这些能力;而 jobs、goal、token meter、subagent 等跨会话 registry 或被 host RPC 读取的服务保留在 host plane。WEB-AGENT-PLANE
“某能力是否应该进入 preset”不能凭功能名称判断,而要追踪谁读取它、registry 是否跨会话、provider 名是否全局唯一,以及 host RPC 从哪个 context resolve service。Web patch 中的大段注释本质上是一份 struct-lifetime audit:把进程级状态误放进 session scope,会导致浏览器 API 找不到服务、跨 session 查询断裂或重复注册碰撞。
9. 设计收益、代价与真实风险
| 设计选择 | 得到什么 | 付出什么 |
|---|---|---|
| 空根 + patch composition | 最终树可导出、覆盖、热更新;产品无需硬编码 | 配置即代码,错误可触及任意插件生命周期 |
| 按 id 整块替换 | 覆盖结果确定、删除语义简单 | 上层配置容易因未复述字段而随版本漂移 |
| service-driven activation | 声明顺序与依赖顺序解耦 | 只读 YAML 无法直观看出真实启动拓扑 |
| 两锚点模块解析 | 官方安装与用户插件可共同组成 profile | peer identity、符号链接与包管理器行为成为运行时正确性的一部分 |
| 事务 boot/HMR | 不暴露半有效树,失败保留最后好状态 | Loader 与 effect disposal 必须非常可靠,诊断链更复杂 |
| Web per-session presets | 每会话能力和信任配置可隔离 | host/session plane 的所有权判断复杂,新增插件必须重新审计 |
DeepSeek Harness 选择把“部署、产品 surface、Agent 能力和用户扩展”统一成一种 patchable plugin-tree 语言。这种统一性很强:配置导出、启动、热更新和 per-session preset 可以共享底层语义。但它并没有消除复杂度,而是把复杂度集中到 entry id 稳定性、scope 所有权、service dependency、模块 identity 和事务化生命周期。只有同时拥有 invariant、组合测试与可观测配置树,这一设计才真正可维护。
本章复核清单
- 已追踪 CLI profile dispatch 到
runProfile()。 - 已逐层确认 bundle、profile、home、CLI 与 launcher overlays 的实际顺序。
- 已确认 config 整块替换、row order 不承载激活语义。
- 已确认安装锚点、profile 锚点和 BFS module fallback。
- 已追踪 boot commit point、失败清理、signal/退出竞态。
- 已用失败/恢复测试验证 HMR 的 last-known-good 语义。
- 已分别审计 base、headless、web 及 host/agent plane 所有权。
我的学习体会
内容仅自动保存到当前浏览器,不上传、不进入仓库。你可以导出 Markdown 自行归档。