# 架构调研结论 > 调研对象:[dsh-std](https://github.com/Yan-Zero/dsh-std)(main `c99b7e7`,2025-08 快照) > 与 `@deepseek-ai/dsh@0.1.6-alpha.2`(本机实测版本)。 > 所有结论均附可核对证据;推测与未验证项单独标注。 ## 0. 结论速览 | 问题 | 结论 | |---|---| | 插件挂在哪一层? | 挂在 `@dsh-std/core` **元协议**之上,作为一份**独立领域协议**的 provider/consumer | | 需要定义新协议吗? | **需要**。`meeting.dsh/v1alpha1` / `BriefingBoard` | | 能复用 Agent 协议吗? | **不能**。`@dsh-std/agent` 没有发布 npm 包(E404) | | 能复用 Session 协议吗? | **不能**。操作集穷举封闭,没有 append / turn / prompt | | 最小运行时依赖包面 | `core` + `manifest` + `composition@0.1.1-rc.1` + `lifecycle` + `sdk`(5 个) | | 要不要依赖 `adapter-dsh`? | **不要**。它是宿主适配层,业务插件不应 import 它 | | 多 Agent 能力从哪来? | 自建适配层,隔离 DSH 的 `ctx.agentTeams` / `ctx.subagents` | --- ## 1. dsh-std 的分层,以及我们的落点 ### 1.1 三层结构(来自官方 `docs/architecture.zh.md`) ``` @dsh-std/core 元协议:只定义 protocol 如何声明与协商,零领域概念 │ 域协议 connection / command / tool / session / presentation / agent ... │ 各自独立版本化,可独立实现与替换 │ 产品实现 host / tui / web / gui / plugin ``` `@dsh-std/core` 的定位(`README.md:18-24`): > `@dsh-std/core` **knows zero domain business fields**(它甚至不知道什么是 command 或 model)。 > 它纯粹是"protocols about protocols":如何标识协议(`apiVersion` + `kind`)、 > 参与者如何声明需求(`requires`)与能力(`supports`)、如何跑纯函数协商产出结构化兼容性报告。 **关键推论**:core 不预设 resource / capability / provider / execution plane / endpoint / UI (`docs/architecture.zh.md:23`)。所以"例会协调器"这个新领域**不需要改 core**, 只需要定义一份新协议 + 一个 `ProtocolDefinition`。 ### 1.2 我们的落点 ``` @dsh-std/core ← 挂在这一层 协议声明与协商(真实实现) │ ┌─────────────────┼─────────────────┐ │ │ │ meeting.dsh/v1alpha1 session.dsh/... messages.dsh/... BriefingBoard(新) (不可用) (可选,观察会话消息) │ coordinator facet (support) + agent facet × N (require) │ @dsh-std/lifecycle 的 LifecycleCoordinator 负责激活与回收 ``` --- ## 2. 为什么必须定义新协议(三个硬证据) ### 证据 1:`@dsh-std/agent` 没有 npm 包 ```console $ npm view @dsh-std/agent version npm error code E404 npm error 404 Not Found - GET https://registry.npmjs.org/@dsh-std%2fagent - Not found ``` 同批只有提案文档、**没有对应发布包**的还有: `@dsh-std/content`(E404)、`@dsh-std/events`(E404)、`@dsh-std/permission`(E404)。 `docs/proposals/README.zh.md` 收录了 `agent.zh.md`(状态:草案), 但 `packages/` 目录下没有 `agent` 包。 **所以"复用 Agent 协议"在依赖层面不可能。** 即使语义上合适也用不了。 ### 证据 2:`@dsh-std/session` 的操作集穷举且封闭 `@dsh-std/session@0.1.1-rc.2` 的三个 kind(`docs/proposals/session.zh.md:22-28`): | kind | 操作集 | |---|---| | `SessionCatalog` | `list` \| `get` \| `create` \| `rename` \| `delete` \| `watch` | | `SessionHistory` | `read` \| `follow` \| `fork` | | `SessionEvent` | resource 声明(vocabulary),不是操作 | 而提案明确写了 **`No arbitrary append`**(`session.zh.md:378-382`): > `v1alpha1` 不向普通 History client 提供任意 append。能够读取 Session 或声明 > SessionEvent resource,不表示能够伪造 Agent、tool、permission 或其他组件拥有的事件。 **我们需要的核心写操作是"Agent 提交一份简报",而 SessionHistory 恰好刻意不提供 append。** `create` 的输入只有 `title` + `requestId`(`session.zh.md:182-186`),装不下简报内容。 ### 证据 3:dsh-std 自己鼓励走私有协议 仓库 `AGENTS.md:16`: > Private protocols use their own namespaced `apiVersion` and participate through > the same core declaration and negotiation mechanism as public protocols. 我们照做:`meeting.dsh/v1alpha1` 走 core 的标准 `ProtocolDefinition` / `validateRequirement` / `validateSupport` / `negotiate` / `validateAgreement`, 并显式遵守 core 的确定性要求(结果与注册顺序无关、多 provider 歧义必须由 policy 仲裁)。 ### 结论 **定义一份私有领域协议,而不是扩展现有协议。** 这既符合标准意图,也是唯一可行路径。 --- ## 3. 最小依赖包面 ### 3.1 被选中的 5 个包(运行时依赖) | 包 | 版本 | 用途 | 关键导出(实测) | |---|---|---|---| | `@dsh-std/core` | 0.1.1-rc.2 | 元协议本体 | `ProtocolCatalog`、`ProtocolDefinition`、`defineProtocolDeclaration`、`protocolKey`、`satisfiesVersionRange` | | `@dsh-std/manifest` | 0.1.1-rc.2 | 静态清单 | `parseManifest`、`projectManifest`、`ManifestDefinitionCatalog.validate`、`validateManifest`、`facetKey` | | `@dsh-std/composition` | **0.1.1-rc.1** | 组合预检 | `compose`、`CompositionRuleCatalog`、`ProtocolCompositionRule` | | `@dsh-std/lifecycle` | 0.1.1-rc.2 | 激活与回收 | `LifecycleCoordinator`、`ActivationDriverRegistry`、`PublicationRegistry`、`facetModuleActivationDefinition` | | `@dsh-std/sdk` | 0.1.1-rc.2 | facet 写法 | `defineFacet`、`defineProtocolKey`、`protocol`、`optionalProtocol` | 这些都是**真实实现**,不是只有类型:`ProtocolCatalog.negotiate` 有完整的 分组/校验/definition 分派逻辑;`compose()` 含去重、拓扑排序与 FNV-1a digest; `LifecycleCoordinator` 含状态机、事务回滚与 scope 清理。 ### 3.2 ⚠️ 版本陷阱:`composition@0.1.0-rc1` 会装出两份 core 实测 `npm view @dsh-std/composition` 的依赖: ``` 0.1.0-rc1 → { '@dsh-std/core': '0.1.0-rc1', '@dsh-std/manifest': '0.1.0-rc1' } ← 精确钉死旧版 0.1.1-rc.1 → { '@dsh-std/core': '^0.1.1-rc.1', '@dsh-std/manifest': '^0.1.1-rc.1' } ``` 用 `0.1.0-rc1` 时 pnpm 会同时安装 `core@0.1.0-rc1` 与 `core@0.1.1-rc.2`, 于是 `compose(input.protocols)` 报类型错误: ``` Type 'ProtocolCatalog' is not assignable to type 'ProtocolCatalog'. Types have separate declarations of a private property 'definitions'. ``` 这是 dsh-std 早期草案的真实版本管理问题。**装机前先跑一次依赖解析**是值得的。 本项目已把 `composition` 锁到 `0.1.1-rc.1` 并在 README 中记录了原因。 ### 3.3 刻意**不**依赖的包 #### `@dsh-std/adapter-dsh` —— 业务插件不该 import 它 它的 README 明说(`packages/adapter-dsh/README.md:9`): > Standard plugins neither declare `dsh.bundle` nor import this adapter. 它在架构上是**宿主侧**的 shock absorber:把 DSH 的 session / llm / tools / skills 映射成标准协议,供宿主使用。对业务插件它是错误方向的依赖。 另外实测:它当前的 peer 下界(`0.1.1-rc.2` 上是 `>=0.1.2-alpha.2 <0.1.3`) 与本机 DSH `0.1.6-alpha.2` **不匹配**(`0.1.1-rc.3` 的 rc 通道已扩成双区间)。 **我们抄它的写法,不依赖它** —— 见第 5 节。 #### `@dsh-std/storage` —— 语义不符 `@dsh-std/storage@0.1.0-rc1` 的自我定位是"Component **私有** JSON 键值存储"。 简报板的本质是**跨 Agent 共享**通道,不是组件私有状态。语义不符,因此自建追加式 JSONL。 #### 未发布的那 4 个 `agent` / `content` / `events` / `permission` 都是 E404,无法依赖。 --- ## 4. Facet 模型:我们怎么用,以及 v0.15 的真实边界 ### 4.1 官方四层(`docs/architecture.zh.md:90-99`) ``` Component 安装、版本和来源单位 └── Facet 静态选择与激活单位 └── Activation instance 生命周期、权限和本地归属单位 └── Participant 某次 core 协商中的运行实体 ``` ### 4.2 真实 v0.15 清单只允许**一个** facet `@dsh-std/manifest` 发布的 `schema/dsh-plugin-0.15.schema.json` 硬性要求: ```jsonc "facets": { "type": "object", "additionalProperties": false, "required": ["host"], "properties": { "host": { ... } } } ``` 即 Community v0.15 清单里**只能有一个 `host` facet**。 更丰富的多 facet 结构只存在于宿主内部的 `ComponentManifest` 投影 (`projectManifest()` 的产物,带 `spec.facets[]` 数组)。 ### 4.3 我们的做法:清单一个 facet,组合期多个 facet - **发布的清单**:单个 `host` facet(合规) - **宿主组合期**:由 `activateMeetingHost()` 构造一个 provider facet(`coordinator`) \+ 每个领域方向一个 consumer facet(`agent-`) - 因此"一个领域方向 = 一个 facet",**按需激活、卸载即回收**是真的: ``` unload → scope.close() → 逆序执行 disposer → runtime.close(handle) → 协调器 forget(slot) ``` ### 4.4 ⚠️ 关键坑:`activationOrder` 只由 composition rule 的 bindings 决定 这是本项目花费时间最多的一个上游细节,必须记下来。 **现象**:只注册 `ProtocolDefinition` 时,`compose()` 产出的激活顺序是 participantId 的**字母序**: ``` io.github.dsh-meeting.coordinator.live-trading@0.1.0#agent-live-trading ← consumer 先 io.github.dsh-meeting.coordinator.neural-net@0.1.0#agent-neural-net io.github.dsh-meeting.coordinator@0.1.0#coordinator ← provider 后 ``` 于是 `LifecycleCoordinator` 先激活成员 facet;成员 facet 在激活前要做 pre-activation 协商(`activateOne` 里的 `this.protocols.negotiate([plannedDeclaration, ...publications.declarations()])`), 此时协调器的 support 还没发布 → 协商失败: ``` facet ...#agent-live-trading requirements are unavailable: 没有任何 support 提供 BriefingBoard。 ``` **根因**:`facetActivationOrder()` 的顺序边完全来自 `bindings` (`node_modules/@dsh-std/composition/lib/index.js`);而 `bindings` 只由 **相应协议的 `ProtocolCompositionRule.preflight`** 产出。**只声明 requires/supports 不够。** **解法**:注册 `briefingBoardCompositionRule`(`src/protocol/meeting-composition-rule.ts`), 在 `preflight` 里为每个 requirement 指出候选 support,从而建立 provider→consumer 边。 修好后顺序正确: ``` io.github.dsh-meeting.coordinator@0.1.0#coordinator ← provider 先 io.github.dsh-meeting.coordinator.live-trading@0.1.0#agent-live-trading io.github.dsh-meeting.coordinator.neural-net@0.1.0#agent-neural-net ``` **对生态的意义**:任何想用 dsh-std facet 模型做"provider facet + consumer facet"的插件, 都必须同时提供 composition rule。建议 dsh-std 在文档中明确这一点 (当前 `composition.zh.md` 只说"拓扑排序 provider 先于 consumer", 没说顺序边依赖 rule)。 --- ## 5. 适配层设计:如何隔离上游破坏性变更 ### 5.1 上游 DSH 的真实插件契约(实测 0.1.6-alpha.2) | 事实 | 证据 | |---|---| | 内核**不读** `dsh-plugin.json` | 在 checkout 内 grep `dsh-plugin.json` 零命中 | | 内核读 `package.json` 的 `dsh.bundle.patch` | 社区插件 `dsh-pdf-edit@0.4.5`、`dsh-noletme@0.3.3` 都用它挂载 | | 插件是 Cordis 插件 | `export function apply(ctx, config)` + `export const inject` | | 有多 Agent 服务 | `ctx.agentTeams`(`dsh-experimental-agent-team`)与 `ctx.subagents` | `ctx.agentTeams`(`TeamService`)的自我描述就是: > Agent Teams service façade over roster, mailbox, task, and runtime lifecycle owners. > **Implicit-root Agent Teams roster, durable peer mailbox, and shared task DAG** 我们实际用到的方法(签名已逐条核对 `.d.ts`): ```ts spawnTeammate(caller: Agent, req: { name: string; description: string; prompt: ContentBlock[]; context: 'fresh' | 'fork'; provider: string; signal: AbortSignal }): Promise<{ member: TeamMemberView }> sendMessage(caller: Agent, req: { target: string; content: ContentBlock[]; signal: AbortSignal }): Promise<{ messageId: TeamMessageId; status: 'accepted' | 'queued' }> interrupt(caller: Agent, targetName: string): { previousStatus: 'running' | 'idle' | 'inactive' } createTask(caller: Agent, req: { subject: string; description: string; blockedBy?: TeamTaskId[]; writeScopes?: string[] }): Promise waitForChange(caller: Agent, timeoutMs: number, signal: AbortSignal): Promise ``` ### 5.2 我们的适配层三条原则 `src/adapters/dsh-team-runtime.ts` 是**唯一**接触上游形状的文件: **原则 1:零 `@deepseek-ai/*` import。** 全仓库不存在对上游包的类型依赖。上游改导出、改包名、改类型都不会让本插件编译失败。 (这抄自 `@dsh-std/adapter-dsh` 自己:它把会话适配器的上游形状声明为本地结构接口, 并用新旧字段双读兼容 `seedLength` vs `inheritedEventCount`。) **原则 2:所有上游成员声明为 optional。** 上游删掉 `interrupt`,这里只是能力探测返回 `canInterrupt: false`, 而不是编译爆炸或运行期 `undefined is not a function`。 **原则 3:能力探测,不假设。** `capabilities()` 在任意时刻可无副作用调用,返回 `canSpawn` / `canDeliver` / `canWake` / `canInterrupt` / `canWaitForActivity` / `hasNativeTaskBoard` \+ 人类可读的 `notes`。拿不到就明确报降级。 ### 5.3 两条上游路径的取舍(诚实记录) | | `ctx.agentTeams` | `ctx.subagents` | |---|---|---| | 持久 peer 信箱 | ✅ 原生 | ❌ | | 共享任务板 / 写域隔离 | ✅ `createTask(writeScopes)` | ❌ | | 指定 `model` / `reasoningEffort` | ❌ 无 `agentOptions` | ✅ `agentOptions: { model }` | | 关闭成员 | ❌ 只有 `interrupt` | ✅ `stop` | 例会需要"持久信箱 + 名册 + 任务 DAG",所以**默认走 `agentTeams`**; 领域隔离靠**不同的 systemPrompt** 达成,不靠不同模型。 需要按槽位选模型时显式 `preferred: 'subagents'`,代价是失去原生信箱。 这个取舍被写进 `capabilities().notes`,运行时可见。 ### 5.4 对"隔离上游变更"的诚实评估 dsh-std 的 `adapter-dsh` 用 peerDependencies 区间(手工维护的 OR 列表, 会跳过整段版本号)来声明兼容。**光靠版本区间隔离不了上游变更**—— 上游改一个运行时字段形状,peer 通过但代码照样炸。 真正起作用的是**结构化鸭子类型 + 可选成员 + 能力探测**: 上游只有在**运行时形状**变化时才会影响我们,而那时能力探测会先报降级, 而不是让插件在用户面前崩掉。 **但要说清楚**:本插件**不能**声称对 DSH 内部实现零依赖。 它必须调用 `ctx.agentTeams` 才能创建 Agent——这是 DSH 今天唯一可用的编排入口。 我们的隔离目标是"把耦合收敛到 1 个文件、且失败时优雅降级", 不是"不依赖"。 --- ## 6. 演进路径 ``` 现在(MVP 第一步 + 第二步) 同进程 / 内存或 agentTeams / 追加式 JSONL 简报板 │ ├─▶ 步骤 2.5:接活 DSH 会话 │ 把 dist/host.js 挂进 profile,用真实 ctx.agentTeams 跑一次例会 │ ├─▶ 第三步:动态引入新 Agent │ 开会时 call 新 Agent 进来(带新提示词),会后撤出 │ 协议侧已留好位置:BriefingBoardOperation 'convene' + facet 按需激活 │ 缺的是按槽位指定模型(需切 subagents 后端)与"冷启动简报"生成 │ └─▶ 跨进程例会 用 @dsh-std/connection 的 connection-scoped attachment 传限长摘要 协议 BriefingBoard 已经能承载;缺的是连接层接线 (对方进程提交 requirement/support,Host 做 federation) ``` ## 7. 未决问题(留给后续) 1. **`ctx.schedule` 不可用** —— 包存在于 `profiles/node_modules/@deepseek-ai/dsh-schedule`, 但在所有 `cordis.patch.yml` 中零命中,未 mount。定时驱动目前用 `ctx.effect` + `setInterval`。 2. **`agentTeams` 无 close/kill** —— 成员只能 `interrupt`,无法真正销毁。 这削弱了"卸载即回收"的上游保证(我们只能回收本地句柄)。 3. **权限无人执行** —— `@dsh-std/permission` 未发布;`compose()` 的 `CompositionPolicy.authorizePermission` 在整个 dsh-std 仓库无调用者。 所以清单里的 permission 目前只是声明,插件内自行兜底。 4. **`agentTeams` 的 `fork` 模式对领域隔离有害** —— `fork` 继承父上下文, 正是我们要解决的污染。本插件默认 `context: 'fresh'` 且把 `fork` 标为不推荐。