Reorganize packages into a modular hierarchy
将包重组为模块化层级结构
`packages/` was flat: 18 packages all sat at `packages/<name>/`, so a package's location said nothing about whether it was core product API, a swappable capability seam, a provider adapter, a product integration, or example/test support. The package README carried a `FIXME(package-hierarchy)` and `scripts/publint-all.ts` a `TODO(package-inventory)` flagging exactly this. Core packages, provider integrations, capabili
English
Problem
packages/ was flat: 18 packages all sat at packages/<name>/, so a package's location said nothing about whether it was core product API, a swappable capability seam, a provider adapter, a product integration, or example/test support. The package README carried a FIXME(package-hierarchy) and scripts/publint-all.ts a TODO(package-inventory) flagging exactly this. Core packages, provider integrations, capability seams, example UI support, and snapshot-only replay support all looked equally foundational.
This was not just cosmetic. Because every top-level package looked like part of the same public surface, future removal was harder, and publish/lint/doc scripts had to encode intent through comments or hand-maintained static lists rather than reading it off the layout.
Decision
Packages are grouped by modular role at a uniform packages/<group>/<pkg>/ depth. Group directories are pure containers (no package.json); every package keeps its @deepseek-ai/dsh-<pkg> name — this is repo structure and maintenance policy, not package renaming.
packages/
core/ (product API spine)
session/
system-prompt/
tools/
agent/
agent-loop/
llm/ (product — capability family)
llm/
llm-deepseek/
llm-pi-ai/
bash/ (product — capability family)
bash/
bash-local/
tool-bash/
session-persistence/ (product — capability family)
session-persistence/
session-persistence-jsonl/
session-persistence-sqlite/
acp/ (product automation integration)
acp/
ui/ (human interaction and presentation)
support/ (dev/test/example infrastructure)
invariants/
ui-stdio/
llm-replay/
Placement decisions
- Same-name nesting for capability families. A family's interface package sits at
packages/<group>/<group>/(llm/llm,bash/bash,session-persistence/session-persistence), with implementations and consumers as flat siblings. There is no extraadapters//impls/sub-tier — every package is exactly depth 2, which keeps the workspace glob a cleanpackages/*/*and lets one@deepseek-ai/dsh-*tsconfig wildcard resolve every package (unique dir names make first-on-disk-wins unambiguous). sessionstays incore/; persistence is its own family. The session log is core product API. Its storage backends form a parallel capability family (session-persistence/) mirroringllm/andbash/, rather than nesting undercore/session/.agent-loopis incore/. It is the one concrete implementation of theagentseam, but it ships as the harness's default product loop, so it lives with the core spine. Plugins still depend on theagentvocabulary, never onagent-loop, so the loop stays swappable.- Product automation and human UI are separate groups.
acpis a product transport underacp/, while commands, approvals, interaction, and presentation adapters live underui/. Dev-only invariants and replay infrastructure remain undersupport/.
Deduplicating the package lists
The package list had been enumerated in five places. The uniform depth-2 layout lets most of them be derived instead:
tsconfig.base.jsonmaps every package through a single@deepseek-ai/dsh-*pathswildcard listing one candidate per group, in place of per-package entries. The aggregate configs (tsconfig.host.json,tsconfig.client.json) reuse that source map and carry the explicit project references that keep package/vendor typecheck boundaries intact. (One subtlety this introduced: a path candidate contains/*/, which a naive regex comment-stripper mistakes for a block comment —scripts/doc-typecheck.tsreads the JSONC config through TypeScript's parser rather than stripping comments by hand for exactly this reason.)scripts/publint-all.tsderives its list by reading the hierarchy (packages/<group>/<pkg>), resolving theTODO(package-inventory).- The aggregates' project
referencesstay explicit lists — TypeScript project references have no wildcard form. Generating these from a manifest is left to a follow-up (see discover package inventories).
Guardrails added
Two doc-sync/hygiene gates keep the structure and its references honest, so the manual checks this restructure required do not have to be repeated by hand:
scripts/verify-package-paths.tsflags apackages/<path>reference (in Markdown or a.tscomment/string) that does not resolve and names a real package in a segment — i.e. a stale path to a moved package. A path naming a package that exists nowhere (a forward-looking proposal) is left alone, so the gate applies uniformly across proposed/implemented/rejected.scripts/check-workspace-constraints.tsasserts thepackages/<group>/<pkg>shape: group dirs carry nopackage.json, and no package sits flat at the root or nests deeper. Group names stay open — a new group may be added without editing the gate; only the depth-2 shape is fixed.
Alternatives considered
- A third tier (
adapters//impls/under each family) — rejected: uniform depth 2 keeps the workspace glob a cleanpackages/*/*and lets one@deepseek-ai/dsh-*tsconfig wildcard resolve every package. - Nesting persistence under
core/session/— rejected: the storage backends form a parallel capability family mirroringllm/andbash/, while the session log itself stays core product API. ui-stdiounderui/— rejected: it was example-coupled dev support, not a product surface.
Consequences
The restructure churned imports, workspace globs, doc links, build references, and package paths in one coordinated move. That churn is acceptable pre-release (per the AGENTS.md foundation-over-blast-radius stance) because it stops the flat layout from fossilizing support packages as product contracts, and it is a one-time cost: the wildcard paths, the glob-derived publint list, and the shape gate mean a new package needs no further structural edits.
中文
问题
packages/ 原先是扁平的:18 个包(package)全部位于 packages/<name>/,从路径上完全看不出一个包属于核心产品 API、可替换的能力 seam、提供方适配器、产品集成,还是示例/测试支撑。包的 README 带着 FIXME(package-hierarchy),scripts/publint-all.ts 带着 TODO(package-inventory),标记的正是这个问题。核心包、提供方集成、能力 seam、示例 UI 支撑和仅用于快照的回放支撑看起来同样基础。
这不仅仅是外观问题。由于每个顶层包看起来都属于同一个公开接口,未来移除更加困难,而 publish/lint/doc 脚本不得不通过注释或手工维护的静态列表来编码意图,而不是从布局中直接读取。
决策
按模块角色将包分组,统一放在 packages/<group>/<pkg>/ 深度。分组目录是纯容器(没有 package.json);每个包保留其 @deepseek-ai/dsh-<pkg> 名称——这是仓库结构与维护策略的调整,不是包的重命名。
packages/
core/ (product API spine)
session/
system-prompt/
tools/
agent/
agent-loop/
llm/ (product — capability family)
llm/
llm-deepseek/
llm-pi-ai/
bash/ (product — capability family)
bash/
bash-local/
tool-bash/
session-persistence/ (product — capability family)
session-persistence/
session-persistence-jsonl/
session-persistence-sqlite/
acp/ (product automation integration)
acp/
ui/ (human interaction and presentation)
support/ (dev/test/example infrastructure)
invariants/
ui-stdio/
llm-replay/
放置决策
- 能力族使用同名嵌套。 一个族的接口包位于
packages/<group>/<group>/(llm/llm、bash/bash、session-persistence/session-persistence),实现和消费方作为扁平兄弟并列。不设额外的adapters//impls/子层——每个包恰好在深度 2,这使 workspace glob 保持简洁的packages/*/*,并让一条@deepseek-ai/dsh-*tsconfig 通配符即可解析所有包(唯一的目录名使 first-on-disk-wins 无歧义)。 session留在core/;持久化独立成族。 会话日志是核心产品 API。其存储后端构成一个平行的能力族(session-persistence/),与llm/和bash/对称,而非嵌套在core/session/下。agent-loop在core/中。 它是agentseam 唯一的具体实现,但作为 harness 的默认产品循环交付,因此与核心主干同处。插件仍然依赖agent的词汇,从不依赖agent-loop,所以循环仍可替换。- 产品自动化与面向人类的 UI 是两个独立分组。
acp是位于acp/下的产品传输层,而命令、审批、交互和展示适配器位于ui/下。仅开发用的 invariants 与回放基础设施仍留在support/中。
去重包列表
包列表此前在五个地方重复枚举。统一的深度 2 布局使大部分可以被推导:
tsconfig.base.json通过一条@deepseek-ai/dsh-*paths通配符(每个分组列一个候选)映射所有包,取代了逐包条目。聚合配置(tsconfig.host.json、tsconfig.client.json)复用该源映射,并携带显式 project references 以保持包/vendor 类型检查边界完整。(这里引入了一个细节:路径候选中包含/*/,朴素的正则注释剥离器会将其误认为块注释——scripts/doc-typecheck.ts正是因此通过 TypeScript 解析器读取 JSONC 配置,而非手动剥离注释。)scripts/publint-all.ts通过读取层级结构(packages/<group>/<pkg>)推导列表,解决了TODO(package-inventory)。- 聚合配置的 project
references仍为显式列表——TypeScript project references 没有通配符形式。从 manifest(元数据清单)生成这些引用留作后续工作(见通过发现机制获取包清单)。
新增的护栏
两道 doc-sync/hygiene 门禁确保结构及其引用保持正确,使本次重组所需的手动检查无需日后重复:
scripts/verify-package-paths.ts标记 Markdown 或.ts注释/字符串中的packages/<path>引用,如果该引用无法解析且某个路径段命名了一个真实存在的包,即指向已移动包的陈旧路径。如果路径命名的包在任何地方都不存在(前瞻性提案),则不予标记,因此该门禁在 proposed/implemented/rejected 中统一适用。scripts/check-workspace-constraints.ts断言packages/<group>/<pkg>形状:分组目录不带package.json,且没有包扁平地位于根层或嵌套更深。分组名称保持开放——添加新分组无需修改门禁;只有深度 2 的形状是固定的。
曾考虑的替代方案
- 第三层(每个族下设
adapters//impls/):否决。统一深度 2 使 workspace glob 保持简洁的packages/*/*,并让一条@deepseek-ai/dsh-*tsconfig 通配符即可解析所有包。 - 将持久化嵌套在
core/session/下:否决。存储后端构成一个平行的能力族,与llm/和bash/对称,而会话日志本身属于核心产品 API。 ui-stdio放在ui/下:否决。它曾是与示例耦合的开发支撑,不是产品接口。
后果
本次重组在一次协调的变更中搅动了 import、workspace glob、文档链接、构建引用和包路径。这种变动在发布前是可接受的(依据 AGENTS.md 中「基础优先于爆炸半径」的立场),因为它阻止了扁平布局将支撑包固化为产品契约,且这是一次性成本:通配符 paths、glob 推导的 publint 列表和形状门禁意味着新增一个包无需额外的结构性编辑。