Files
deepseek-harness/.agents/notes/implemented/architecture/2026-07-29-package-regrouping.zh.md
T

14 KiB
Raw Blame History

Agent Note: 按实测聚类重新划分 packages/ 分组

Status: implemented

English | 中文

Problem

两级 packages/<group>/<pkg> 层级结构(原始决策)自 6 月以来已经漂移:167 个包(package)彼时坐落在 42 个组里,若干组边界已经对不上这些包的实际聚类。

  • ui/ 混杂了四个互不相关的平面:人类终端通道(tui)、SDK 的 JSON-RPC 服务端一半(jsonrpc,它对 dsh-sdk-protocol 的对等依赖(peer dependency)把它绑在 SDK 通信栈上)、人机交互 seam(user-interaction、user-approval、permission、tool-ask-user、commands),以及与通道无关的 boot 胶水(app-boot)。它自己的 README 只能逐一叙述这堆混杂,说不出一个统一职责。
  • 会话家族被割裂在五个组里——session-persistence/、session-projection/、session-query/、session-title/ 与 telemetry/——而实测依赖边明明把它们连成一体(query → persistence、title → projection、projection → persistence;见 docs/module-graph.md)。
  • 两个组名与不相干的包撞名:telemetry/(会话上报)撞上 dsh-telemetry(启动器侧 SDK telemetry),timeout/(一个工具调用守卫)撞上 util/timeout(通用 promise 工具)。
  • cordis/ 拿所有包共同依托的框架给自己的组命名,这个名字因此毫无区分度;组里唯一的包 tool-cordis 是运行时自我修改工具集。
  • 旧 sdk/ 的目录命名不一致:sdk/sdk-client 和 sdk/sdk-protocol 重复了组名,而 sdk/telemetry、sdk/helper、sdk/scripts 没有。

这次重新分组的指导准则:**聚类紧密的包同处一组。**聚类以实测为准(对等依赖边与 co-change),而非按主题归类。孤立的 seam 家族可以自成一个小组;要避免的失败形态,是名字概括不出单一职责的大杂烩组。

Decision

重组六个组;其余每个组都保持先前的边界与内容不变(依赖分析确认各能力家族——bash/、pty/、code-runtime/、sandbox/、subprocess/、fs/、lsp/、web/、skill/ 及其余——本来就划得正确)。npm 包名一个未改;整个变更全部由目录树承载。

组 成员(目录名) 来源
session/ session-persistence、session-persistence-jsonl、session-persistence-sqlite、session-checkpoint-policy、session-projection、session-projection-cache、session-title、session-title-llm、session-title-first-message-llm、session-title-all-messages-llm、session-telemetry、session-telemetry-otel session-persistence/ + session-projection/ + session-title/ + telemetry/
interaction/ user-interaction、user-approval、permission、tool-ask-user、commands、tui ui/
boot/ app-boot ui/
scaffold/ helper、scripts、create-sdk、protocol、client、server、telemetry sdk/ + ui/jsonrpc
guard/ repeat-tool-guard、timeout-policy guard/ + timeout/
self-modification/ tool-cordis cordis/
  • session/ 是持久会话数据平面:持久化 seam 连同其各后端与检查点策略、从该日志折叠(fold)出全量值对外供值的投影、日志兜底的标题,以及 OTel 上报。标题折叠本身就是读取侧的承重构件(session-query 对 dsh-session-title 声明对等依赖),所以标题属于数据平面,而非某个「派生服务」附属区。用这个朴素的名字是有意为之(名字要像人起的);旁边的 core/session 包仍是常驻内存的实时服务,本组则是围绕它的持久家族。session-query/ 保持独立成组:这个读取/工具面自带模型工具和 SQLite FTS 后端,其消费不依赖持久化内部实现。吸收 telemetry/ 之后,与 dsh-telemetry 的组名冲突就此终结。
  • interaction/ 是人机协作平面加上应答它的终端通道:提问/批准 seam、权限预设、面向模型的 ask_user_question 工具、人类命令注册表(plan-mode 与 command-goal 已经把 commands 和各交互 seam 放在一起消费),以及 tui——这个交互通道是该平面最重的提供方与消费方(对 commands 与 user-interaction 均有对等依赖边),而一个单包 tui/ 组会把一个顶层名字花在一个插件上。
  • boot/ 是角色完备的单包组:不归属任何通道也不归属任何组装的共享 bin boot 胶水(被 apps/cli、scaffold/ 的启动器和 examples/ 各演示 bin 消费)。
  • scaffold/ 是开发者工具家族:项目 helper、启动器、初始化器、连同两端的通信协议(server 即原先的 ui/jsonrpc),以及启动器侧 telemetry。从 sdk/ 改名:整个 packages/ 树本身就是 SDK,树里再放一个叫 sdk/ 的组等于什么都没说;scaffold/ 说出了「创建/启动/驱动项目」这一实际角色。目录去掉遗留的 sdk- 前缀(protocol、client、server),与 client//host/ 的角色命名风格一致;在推迟的改名落地之前,受影响的三个 npm 名在 tsconfig.base.json 里于组通配符旁显式映射。
  • guard/ 保留其文档记载的角色(循环卫生守卫),并新纳入强制执行工具调用超时的包;那个与 util/timeout 撞名的单包组 timeout/ 随之解散。
  • self-modification/ 把 cordis/ 遮蔽掉的角色说了出来:它是 agent(智能体)检查并挂载自身实时运行时中插件所用的工具集,也是未来自我修改类包的落点。

42 个组变为 39 个;收益在聚类正确与名实相符,不在数量增减。

Deferred renames (FIXME markers)

五个 npm 名最终应当改掉,但在这次重组内部改名,会把一个纯移动的 PR(Pull Request)变成大量翻改 import 的 PR。因此每个受影响包的模块 JSDoc 里带有一条 FIXME,写明意图中的新名字。FIXME 会阻塞打 tag 的发布(标记语义),这正是想要的倒逼机制:只有趁还没有外部消费方使用这些包时,这些改名才是零成本的。

当前 npm 名 目标名 原因
@deepseek-ai/dsh-jsonrpc @deepseek-ai/dsh-sdk-server 名字说的是协议编码而非角色;它是 SDK 协议的服务端一半
@deepseek-ai/dsh-telemetry @deepseek-ai/dsh-sdk-telemetry 与 dsh-session-telemetry 家族撞名;它是启动器侧 SDK telemetry
@deepseek-ai/dsh-helper @deepseek-ai/dsh-sdk-helper 作为公开发布名空泛得站不住脚
@deepseek-ai/dsh-scripts @deepseek-ai/dsh-sdk-scripts 同上
@deepseek-ai/dsh-timeout-policy @deepseek-ai/dsh-timeout-guard 仅为建议、尚未定案:使名字与其 guard/ 归属对齐;到解决时再定

前四个是已定的意图;兑现之后,SDK 通信栈的 npm 名随之收敛为 dsh-sdk-*(npm 前缀指产品栈,scaffold/ 目录名指角色)。@deepseek-ai/create-sdk 保留其文档记载的 npm 初始化器特例。

What the move touched

移动以纯 git mv 形式落地,历史由重命名检测承载。组移动触及了:被移动包的 tsconfig.json 相对 references 及每个依赖方的对应条目(含 apps/cli 的 project references);tsconfig 聚合与路径映射;各组 README(五组新的双语三文件配对、被解散组的 README 删除、packages/README.md 的层级结构表、根 AGENTS.md 的布局图);重新生成的产物(docs/module-graph.md、内嵌路径的目录、锁文件的 importer 键);以及散文与门禁脚本中以仓库根为基准的 packages/... 引用。其余每一处组路径引用(workspace 配置、测试 glob、lint 键)都由验收门禁的响亮失败机械地找了出来——这正是本仓库自己的「配置错误必须响亮失败」规则。

组移动未触及:npm 包名、import、cordis.yml 配置、快照 fixture(测试前置数据)、pnpm-workspace.yaml 与 tsdown 的 glob(都是 packages/*/*),以及 Python 运行时 manifest(元数据清单)——它们全部按 npm 包名引用包。

client/ 与 host/ 不在本次范围内,保持不变。

Alternatives considered

粗粒度领域桶(exec/ = subprocess+sandbox+bash+pty+code-runtime,workspace/ = fs+lsp+workspace,orchestration/ = subagent+workflow+tasks,knowledge/ = web+skill,collab/ = plan+todo+goal;约 16 个组)。不予采纳:实测依赖图与这些合并相矛盾。sandbox 和 subprocess 是被各家族跨界消费的共享基础设施(与 bash ×5、fs ×5、pty、lsp、mcp、subagent、scaffold 均有依赖边),web ↔ skill 之间零依赖边,而大桶只会在更大尺度上复现 ui/ 式大杂烩。

抽象分层名(capability/、policy/、extension/、provider/)。不予采纳:这些名字对每个插件都同样地不达意,而且一个 capability/ 桶会装下约 50 个包。

一轮全量 npm 重命名(每个包都改为 dsh-<group>-<pkg>)。不予采纳:npm 包名是扁平的,加组前缀只会在 import、配置和 fixture 之间制造改动,却换不来任何消歧收益;用 FIXME 跟踪的定点改名足以覆盖真正的撞名。

在重组内部一并完成那五个改名。 不予采纳:改名会成倍放大开放 PR 的冲突,并破坏纯移动的评审属性。FIXME 标记让这些改名保持为可见的发布阻塞项,留待以小型后续 PR 逐一解决。

会话两分法(session-core/ + session-utils/)。不予采纳:query 放哪一侧都不干净,而且 session-core 容易与 core/session 混淆(后者是 dsh-session,常驻内存的实时服务,留在 core/ 不动)。

会话三分法(session-store/ + session-query/ + session-utils/)。不予采纳:session-utils/ 是靠否定条件圈出来的附属区(「派生的、没有承重方依赖」)——正是指导准则禁止的大杂烩形态,而且事实层面也站不住(session-query 对 dsh-session-title 声明对等依赖)。杜撰的复合名也读起来不像人起的;一个朴素的 session/ 组说的就是人会说的话。query 无论如何都保持独立:它是被独立消费的读取面,自带自己的工具包与后端。

把 ui/ 重组为单一 channels/ 组(tui + jsonrpc + acp + 交互 seam + boot)。不予采纳:不过是换个名字的同一个大杂烩——这些包服务于四个平面,jsonrpc 的实测聚类归属是 SDK 通信栈,而 acp/ 是自动化传输通道,不是人类通道。

独立的单包 tui/ 组。 不予采纳:tui 是交互平面最重的提供方/消费方(对 commands、user-interaction 有对等依赖边),把一个顶层名字花在一个插件上只添组不添信息;它折入 interaction/。

保留组名 sdk/。 不予采纳:整个 packages/ 树本身就是 SDK,树里的 sdk/ 组毫无区分度——与 cordis/ 同病。scaffold/ 说出了实际角色(从外部创建、启动、驱动项目)。

把 app-boot 挪到 apps/。 不予采纳:apps/ 是包层之上的组装层,而 dsh-app-boot 是被包层代码 import 的库(scaffold/scripts 的启动器对它声明对等依赖)——放进 apps/ 会颠倒层级,并把一个 workspace 库放到 packages/*/* 构建 glob 之外。它仍是一个包;boot/ 是它角色完备的家。

把 tool-cordis 挪进 core/。 不予采纳:自我修改是独立的产品 seam,预期还会生长;主干保持精简。该组最初命名为 self-evolve/;名字最终定为更朴素的 self-modification/。

把 context/ 改名为 request-context/。 不予采纳:在这棵树里,该组就地看并无歧义;这份改动开销并不值得。

Consequences

  • 目录树与映射表一致:重组的六个组恰好持有所列成员;ui/、sdk/、telemetry/、timeout/、cordis/、session-persistence/、session-projection/、session-title/ 这些组不复存在;其余每个组的内容不变。workspace 的包名集合在前后完全相同(npm 改名为零),五条 FIXME 标记钉住推迟的改名。日后若某条 FIXME 被证明不对,必须连同理由显式移除,绝不允许无声消失。
  • 结果由以下检查钉住:pnpm run typecheck、每个被移动组的单元测试套件、verify-package-paths、verify-md-links 与全语料翻译配对在移动后的树上全部通过;vitest.snapshot.config.ts 中按组划定的测试 glob 随移动一并改写,套件收集到与移动前相同的测试文件(glob 匹配为空会无声地丢失覆盖)。
  • 每个触碰被移动文件的开放 PR 都跨过这次移动做一次变基;重命名检测可机械化解决大多数改动块。
  • 单包组依然存在(boot/、self-modification/,以及 acp/ 等既有单包组)。这是有意接受的:每个都是角色完备的整体而非某个家族的碎片,一个名实相符的小组胜过一次徒有其名的合并。
  • 在推迟的改名落地之前,scaffold/ 的目录名与其 npm 名并不一致——这是唯一的过渡性不对称,由 tsconfig.base.json 里三条显式 paths 映射承载,并由 FIXME 改名最终消除。
  • 这次变更放弃了什么: 功能上一无所失——变更只关乎导航。肌肉记忆和指向旧 GitHub 路径的外部链接会失效;在 pre-release、尚无外部消费者的前提下,这可以接受。