mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
docs(i18n): re-translate RFC batch with the prompt-v4 pipeline
146 篇 RFC 译文按 v4 基线(#348)重出:v4 模板+术语表、金标 few-shot、三段协议、切换行后处理;全量机械核对零异常(一处 task id 术语违规已修)。三篇超长 RFC(code-mode 已入,web-seam/ agent-scope/sandbox/cds-core 仍在长文档通道产出)随后补。
This commit is contained in:
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-06-19-drop-mutable-session-summary.md: 0d790191906a9128ad12d40526fde3b9f8fa939f
|
||||
2026-06-19-drop-mutable-session-summary.zh.md: 97293c296cd74b5a33ce7e1ebe473c970ee95d57
|
||||
2026-06-19-drop-mutable-session-summary.zh.md: 6b234eb0dbdf764223e078519c810216c28603be
|
||||
|
||||
@@ -6,30 +6,30 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
[会话持久化 seam](../architecture/2026-06-14-session-persistence.md) 将会话的日志外元数据拆分为 `dsh-session` 拥有的两种类型:一个不可变的 `SessionHeader`(`version`、`id`、`createdAt`、`cwd?`、`parentSession?`),在创建时一次性写入;一个可变的 `SessionSummary`(`updatedAt`、`title?`、`firstPrompt?`),「无需触碰仅追加日志即可更新」。二者的联合类型为 `SessionMeta = SessionHeader & SessionSummary`,抽象的 `SessionPersistence` 服务为此多出第七个方法 `update(id, summary)`,用于重写摘要。各后端各自实现可变存储:JSONL 在日志旁写一个独立的原子 `.summary.json` **伴随文件**(临时写入 + rename,尽力而为);SQLite 在追加事务内更新 `updated_at`/`title`/`first_prompt` **列**。
|
||||
[session-persistence seam](../architecture/2026-06-14-session-persistence.md) 将会话的日志外元数据拆分为 `dsh-session` 拥有的两种类型:一个不可变的 `SessionHeader`(`version`、`id`、`createdAt`、`cwd?`、`parentSession?`),在创建时一次性写入;一个可变的 `SessionSummary`(`updatedAt`、`title?`、`firstPrompt?`),「可在不触碰仅追加日志的情况下更新」。二者的联合类型为 `SessionMeta = SessionHeader & SessionSummary`,抽象的 `SessionPersistence` 服务为此多出第七个方法 `update(id, summary)`,用于重写摘要。各后端各自实现可变存储:JSONL 在日志旁写一个独立的原子 `.summary.json` **伴随文件**(临时写入 + rename,尽力保证);SQLite 在追加事务内更新 `updated_at`/`title`/`first_prompt` **列**。
|
||||
|
||||
摘要的设计初衷是服务于未来的会话选择器(通过 `updatedAt` 排序、用 `title`/`firstPrompt` 预览)。该选择器从未实现。对整个仓库的审计表明,`SessionSummary` 的全部表面积都是**死状态**:
|
||||
摘要是为未来的会话选择器设计的(通过 `updatedAt` 排序近期会话,用 `title`/`firstPrompt` 做预览)。该选择器从未实现。对整个仓库的审计表明,`SessionSummary` 的全部表面积都是**死状态**:
|
||||
|
||||
- `SessionPersistence.update()` 的**生产调用方为零**(所有 `.update(` 命中都是 `createHash().update()` 或测试代码)。
|
||||
- `SessionPersistence.update()` **零个生产调用方**(所有 `.update(` 匹配都是 `createHash().update()` 或测试代码)。
|
||||
- `firstPrompt` 在生产代码中**从未被读取**。
|
||||
- `title` 确实在 ACP bridge 中被读取,但来源是工具调用的 **presenter**(`present.title`),而非存储的会话元数据。
|
||||
- `updatedAt` **没有消费方**:`list()` 唯一的生产调用方读取的是 `meta.cwd`(`SessionHeader` 字段),用于在 `session/load` 时校验工作区;resume 读取的是 `createdAt`/`cwd`/`parentSession`,全部是 header 字段。
|
||||
- 决定性的事实:活跃的 `Session.header` 早已被类型化为 `SessionHeader` 而非 `SessionMeta`——摘要从未存在于活跃会话对象上;它只存在于持久化层,除了自身的契约测试之外无人写入、无人读取。
|
||||
- `title` 确实在 ACP 桥接层被读取过,但读的是工具调用的 **presenter**(`present.title`),从未读取存储的会话元数据。
|
||||
- `updatedAt` **没有消费方**:`list()` 唯一的生产调用方读取的是 `meta.cwd`(`SessionHeader` 字段),用于在 `session/load` 时校验工作区;恢复会话读取的是 `createdAt`/`cwd`/`parentSession`——全是 header 字段。
|
||||
- 决定性的一点:活跃的 `Session.header` 类型本来就是 `SessionHeader` 而非 `SessionMeta`——摘要从未存在于活跃会话对象上;它只存在于持久化层,除了自身的契约测试外无人写入、无人读取。
|
||||
|
||||
## 决策
|
||||
|
||||
彻底删除可变的会话摘要。`SessionSummary` 与 `SessionMeta` 这个名称一并移除;后端存储和返回的元数据仅为 `SessionHeader`。`SessionPersistence.update()` 从抽象服务和所有后端中移除。JSONL 去掉整套伴随文件机制(`writeSidecar`/`readSidecar`/`touchSummary`/`removeSidecars`/`sidecarPath` 以及 load/list 的覆盖逻辑);SQLite 删除 `updated_at`/`title`/`first_prompt` 列及每次追加时的 `updated_at` 更新,其 `SCHEMA_VERSION` 从 `1 → 2`。
|
||||
彻底删除可变的会话摘要。`SessionSummary` 与 `SessionMeta` 这个名称一并移除;后端存储和返回的元数据仅为 `SessionHeader`。`SessionPersistence.update()` 从抽象服务和所有后端中移除。JSONL 去掉整套伴随文件机制(`writeSidecar`/`readSidecar`/`touchSummary`/`removeSidecars`/`sidecarPath` 以及 load/list 的覆盖逻辑);SQLite 去掉 `updated_at`/`title`/`first_prompt` 列以及每次追加时的 `updated_at` 更新,其 `SCHEMA_VERSION` 从 `1 → 2`。
|
||||
|
||||
摘要原本要提供的一切,在消费方真正需要时都**可从仅追加日志中派生**(`firstPrompt` = 第一条 `user/message`;最近活跃时间 = 最后一个事件的 `time` 或文件 mtime),或者已经存在于不可变的 header 中(`createdAt`、`cwd`)。唯一*不可*派生的——用户*手动编辑*的标题——没有任何实现,纯属 YAGNI;如果未来真有功能需要,它可以作为独立的日志事件或 header 字段回归。
|
||||
摘要原本要提供的一切,在消费方真正需要时都**可从仅追加日志中派生**(`firstPrompt` = 第一条 `user/message`;近期度 = 最后一个事件的 `time` 或文件 mtime),或者已经存在于不可变 header 中(`createdAt`、`cwd`)。唯一不可派生的是用户*手动编辑*的标题,但它从未实现,纯属 YAGNI;如果未来真有功能需要,它可以作为独立的日志事件或 header 字段回归。
|
||||
|
||||
将此记录为决策,是因为它**持久**(收窄了一个公开服务契约和两个后端的磁盘格式)、**有争议**(摘要是有意的前瞻性设计,不是意外产物)、**出人意料**(未来读者看到 `SessionHeader` 而原始 RFC 描述的是 `SessionMeta`,否则会疑惑摘要为何消失)。它还为[共享持久化写入协调器](../architecture/2026-06-18-shared-persistence-write-coordinator.md)扫清了障碍:没有可变摘要,协调器的钩子接口就不需要 `updateSummary` 钩子,JSONL 伴随文件与 SQLite 列之间的持久性差异也随之消失,两个后端的写入路径得以收敛。
|
||||
将此记录为决策,原因有三:**持久性**(它收窄了一个公开服务契约和跨两个后端的磁盘格式)、**争议性**(摘要是有意的前瞻性设计,而非意外产物)、**意外性**(未来读者看到 `SessionHeader` 而原始 RFC 描述的是 `SessionMeta`,否则会疑惑摘要为何消失)。它还为 [shared persistence write coordinator](../architecture/2026-06-18-shared-persistence-write-coordinator.md) 扫清了障碍:没有可变摘要后,协调器的钩子接口无需 `updateSummary` 钩子,JSONL 伴随文件与 SQLite 列之间的持久性分歧也随之消失,两个后端的写入路径得以统一。
|
||||
|
||||
## 无需迁移
|
||||
|
||||
这是未发布的软件(见[根 AGENTS.md](../../../../AGENTS.md)「预发布立场:地基优先于爆炸半径」一节),因此不存在需要保留的磁盘数据库或日志。SQLite 不迁移 v1 数据库:`openDatabase` 守卫现在拒绝任何非当前版本的磁盘 `user_version`(`onDisk !== 0 && onDisk !== SCHEMA_VERSION`),无论更旧还是更新,因此陈旧的 v1 数据库会被干净地拒绝,而非在新列集上半读半错。新建数据库写入当前版本号;这是唯一需要工作的路径。
|
||||
这是未发布的软件(见[根 AGENTS.md](../../../../AGENTS.md)「Pre-release stance: foundation over blast radius」一节),因此没有需要保留的磁盘数据库或日志。SQLite 不迁移 v1 数据库:`openDatabase` 守卫现在拒绝任何非当前版本的磁盘 `user_version`(`onDisk !== 0 && onDisk !== SCHEMA_VERSION`),无论更旧还是更新,因此陈旧的 v1 数据库会被干净地拒绝,而非在新列集下被半读取。新建数据库写入当前版本号;这是唯一需要正常工作的路径。
|
||||
|
||||
## 后果
|
||||
|
||||
未来的会话选择器现在必须从日志派生预览和排序信息(或重新引入一个类型化字段),而不能直接读取现成的摘要行。这是正确的代价:为一个不存在的功能维护缓存,是每个后端都要承担的死重,也是每个契约测试都要断言的负担。这一原则——**通过的测试固定的是当前行为,不一定是正确行为;行为可能是过去妥协的产物**——现已作为独立约定记录在[根 AGENTS.md](../../../../AGENTS.md) 中,本次变更即为其实例。
|
||||
未来的会话选择器现在必须从日志派生预览/排序信息(或重新引入一个类型化字段),而不能直接读取现成的摘要行。这是正确的代价:为一个尚不存在的功能维护缓存,是每个后端都要付出维护成本、每个契约测试都要付出断言成本的死重。这一原则——**通过的测试固定的是当前行为,不一定是正确行为;行为可能是过去妥协的产物**——现已作为独立约定记录在[根 AGENTS.md](../../../../AGENTS.md) 中,本次变更即为其实例。
|
||||
|
||||
<!-- rfc-format: alternatives-not-recorded (pre-format RFC) -->
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-06-20-collapse-trace-only-session-events.md: 9156c2ab356b1c46758d9d2047d491cd1952cbc4
|
||||
2026-06-20-collapse-trace-only-session-events.zh.md: f2ecfbf46d8d70cf478d57eae2ed3a18ea8746fa
|
||||
2026-06-20-collapse-trace-only-session-events.zh.md: c4555f3a772096fc36de968d5d3085f5a2e879f3
|
||||
|
||||
@@ -6,39 +6,39 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
会话事件词汇中包含一些一等事件,它们既不属于可回放的对话历史,在生产环境中也几乎没有消费方。`usage` 在模型流式分片中已经存在,但循环又额外追加了一个独立的 `usage` 事件。`error` 与 `turn/end { kind: 'error', message, code }` 中的循环失败原因重复;ACP(Agent Client Protocol)结算读取的是 turn-end 原因,ACP 渲染忽略 `error` 事件,`deriveMessages()` 也跳过它。
|
||||
会话事件词汇中包含一些一等事件,它们不属于可回放的对话历史,在生产环境中几乎没有消费方。`usage` 已经作为模型流分片存在,之后循环又追加了一个独立的 `usage` 事件。`error` 与 `turn/end { kind: 'error', message, code }` 中的循环失败原因重复;ACP(Agent Client Protocol)结算读取 turn-end 原因,ACP 渲染忽略 `error` 事件,`deriveMessages()` 也跳过它。
|
||||
|
||||
这些事件让规范的 transcript(文本记录)看起来比实际更像遥测数据。它们增加了事件变体、不变式、测试、快照和持久化用例,但作为独立记录并不承载实际负荷。它们携带的事实仍然有用:token 用量应当保留以供核算,错误的步骤编号也不应悄然消失。简化的方式是将这些事实折叠进消费方本就必须理解的邻近事件,而非减少记录的信息量。
|
||||
这些事件让规范的 transcript(文本记录)看起来比实际更像遥测数据。它们增加了事件变体、不变式、测试、快照和持久化用例,但作为独立记录并不承载实际功能。它们携带的事实仍然有用:token 用量应当保留以供计费,错误的步骤编号也不应悄然消失。简化的方式是将这些事实折叠进消费方本已必须理解的邻近事件,而非减少记录的信息量。
|
||||
|
||||
## 决策
|
||||
|
||||
仅在信息已被保留、无需并行记录的位置移除独立的追踪事件:
|
||||
仅在信息已被保留、无需并行记录的情况下,移除独立的追踪事件:
|
||||
|
||||
- 成功步骤的 usage 折叠进对应的 `assistant/message`(`assistant/message { turn, step, content, usage? }`),使组装好的模型输出与其核算信息一同传递。
|
||||
- 失败或中止的步骤如果有 usage 但没有 assistant 内容,则将 usage 挂在一个空内容的 `assistant/message` 上(下方实现说明给出了无信息丢失的证明)——不会有任何已持久化的 usage 分片失去表示。
|
||||
- 独立 `error` 事件中的步骤编号折叠进 `turn/end.reason`(当 `kind: 'error'` 时:`{ kind: 'error', step, message, code? }`)——`turn/end` 是 ACP 和恢复机制已在消费的持久化轮次结果。
|
||||
- `agent/error` 和日志保留用于实时诊断;`turn/end` 之后不再有第二条会话日志错误记录。
|
||||
- 成功步骤的 usage 折叠进匹配的 `assistant/message`(`assistant/message { turn, step, content, usage? }`),使组装好的模型输出与其计费信息一同传递。
|
||||
- 失败或中止的步骤如果有 usage 但没有 assistant 内容,则将 usage 放在一个空内容的 `assistant/message` 上(下方实现说明给出了无信息丢失的证明)——不会有已持久化的 usage 分片无处安放。
|
||||
- 独立 `error` 事件中的步骤编号折叠进 `turn/end.reason`(当 `kind: 'error'` 时:`{ kind: 'error', step, message, code? }`)——`turn/end` 是 ACP 和恢复机制已经消费的持久轮次结果。
|
||||
- `agent/error` 与日志保留用于实时诊断;`turn/end` 之后不再有第二条会话日志错误记录。
|
||||
|
||||
用户对话日志包含渲染、恢复、审计和核算交互所需的全部信息,消费方无需对账重复的追踪行。
|
||||
用户对话日志包含渲染、恢复、审计和计费所需的全部信息,消费方无需协调重复的追踪行。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
**保留独立行作为遥测**:这些事件让规范的 transcript 看起来比实际更像遥测数据,代价是增加了事件变体、不变式、测试、快照和持久化用例,却没有消费方使用。如果分析需求真正出现,正确的形态是投影辅助工具或带有独立保留策略的专用遥测存储,而非在对话日志中放置重复的追踪行。
|
||||
**保留独立行作为遥测**——这些事件让规范 transcript 看起来比实际更像遥测数据,代价是增加了事件变体、不变式、测试、快照和持久化用例,却没有任何消费方使用。如果分析需求真正出现,正确的形态是投影辅助工具或带有独立保留策略的专用遥测存储,而非对话日志中的重复追踪行。
|
||||
|
||||
## 验证
|
||||
|
||||
`SessionEventMap` 不再包含独立的 `usage` 或 `error`;循环不再追加独立的 usage 事件,持久化的失败通过 `turn/end { kind: 'error', step, message, code? }` 记录;ACP 快照和持久化测试断言不存在仅追踪行;录制的 fixture(测试前置数据)已采用新事件形状,会话格式版本固定为 `0`(按预发布格式策略,后端拒绝任何非 `0` 的存储日志);文档说明了 token 用量和操作错误的观测位置。
|
||||
`SessionEventMap` 不再包含独立的 `usage` 或 `error`;agent loop(智能体循环)不再追加独立的 usage 事件,持久性失败通过 `turn/end { kind: 'error', step, message, code? }` 记录;ACP 快照和持久化测试断言不存在仅追踪行;已录制的 fixture(测试前置数据)使用新事件形状,会话格式版本固定为 `0`(后端按预发布格式策略拒绝任何非 `0` 的存储日志);文档说明了 token 用量和操作错误的观测位置。
|
||||
|
||||
## 后果
|
||||
|
||||
消费方不能再从规范日志中筛选独立的 `usage` 或步骤级 `error` 行,必须从承载它们的 assistant/failure 事件中读取这些事实。只有当实现 PR 证明相同的事实仍然存在时,这才是合理的简化;否则独立事件应当保留。
|
||||
消费方不能再从规范日志中筛选独立的 `usage` 或步骤级 `error` 行,必须从承载它们的 assistant/failure 事件中读取这些事实。只有在实现 PR(Pull Request)证明相同事实仍然存在的前提下,这才是合理的简化;否则独立事件应予保留。
|
||||
|
||||
## 实现说明
|
||||
|
||||
按提案交付,有一处范围细化(遵循 AGENTS.md「RFC 是提案,不是金科玉律」):
|
||||
|
||||
- **空内容的 `assistant/message` 承载 usage,无数据丢失。** 提案要求的证明(不会有已持久化的 usage 分片失去表示)落在 max-tokens 路径上:一个被截断的步骤有 usage 但内容为空(例如只有一个被丢弃的工具调用),此前会发出独立的 `usage`。现在它记录一条空内容的 `assistant/message { content: [], usage }`。为避免这向提供方 transcript 注入一个无内容的虚假 assistant 轮次,`deriveMessages()` 跳过空内容的 `assistant/message` 事件。一个回归测试断言 usage 仍有表示,且派生历史未被破坏。
|
||||
- **空内容 `assistant/message` 承载 usage,无数据丢失。** 提案要求的证明(不会有已持久化的 usage 分片无处安放)落在 max-tokens 路径上:一个被截断的步骤有 usage 但内容为空(例如只有一个被丢弃的工具调用),以前会发出独立的 `usage`。现在它记录一个空内容的 `assistant/message { content: [], usage }`。为防止这向 provider transcript 注入一个无内容的虚假 assistant 轮次,`deriveMessages()` 跳过空内容的 `assistant/message` 事件。回归测试断言 usage 仍被表示,且派生历史未被破坏。
|
||||
|
||||
**格式版本。** 此变更改动了持久化事件,但预发布会话格式仍固定为 `0`,拒绝任何其他版本且不做迁移。`dsh-session` 拥有写入方和加载校验使用的常量。单调递增的格式版本从首次正式发布开始。
|
||||
**格式版本。** 此变更影响已持久化的事件,但预发布会话格式仍固定为 `0`,拒绝任何其他版本且不做迁移。`dsh-session` 拥有写入方和加载校验使用的常量。单调递增的格式版本从首次正式发布开始。
|
||||
|
||||
Usage 现在通过 `assistant/message.usage` 观测;操作错误的步骤编号通过 `turn/end.reason`(当 `kind: 'error'` 时)观测。`agent/error` 加日志用于实时诊断,保持不变。
|
||||
Usage 现在通过 `assistant/message.usage` 观测;操作错误的步骤编号通过 `turn/end.reason`(当 `kind: 'error'` 时)观测。`agent/error` 与日志用于实时诊断,保持不变。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-06-20-drop-unconsumed-llm-adapter-change-event.md: efe90c0197671ef4385ce517540b4b238962c3b5
|
||||
2026-06-20-drop-unconsumed-llm-adapter-change-event.zh.md: 13f01566472a030cc9d4f97f6e4438fe4c3ecbf8
|
||||
2026-06-20-drop-unconsumed-llm-adapter-change-event.zh.md: b26610cfe273820112113c73b9313557cd78262c
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# RFC:移除无消费方的 `llm/adapter-change` 事件
|
||||
# RFC:移除未被消费的 `llm/adapter-change` 事件
|
||||
|
||||
Status: implemented
|
||||
|
||||
@@ -6,31 +6,31 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
`LlmService.registerAdapter()` 在注册和 dispose(资源释放)时发射 `llm/adapter-change`([packages/llm/llm/src/index.ts](../../../../packages/llm/llm/src/index.ts))。在 `packages/*/src` 和 `examples/*/src` 中 grep `llm/adapter-change`,只能找到声明、发射点、文档和测试;没有任何生产代码监听它。
|
||||
`LlmService.registerAdapter()` 在注册和 dispose(资源释放)时发出 `llm/adapter-change` 事件([packages/llm/llm/src/index.ts](../../../../packages/llm/llm/src/index.ts))。在 `packages/*/src` 和 `examples/*/src` 中搜索 `llm/adapter-change`,只能找到声明、emit 站点、文档和测试;没有任何生产环境的监听器订阅它。
|
||||
|
||||
这与 `tools/change` 和 `system-prompt/change` 不同。后两个事件目前同样无消费方,但它们是合理的注册表变更信号,未来的实时工具/提示词 UI 可能用到。LLM 适配器注册更像是启动时的实现细节:适配器不是用户可见的面板,真正的模型调用拦截 seam 是 `llm/stream`。保留一个没有监听者的 adapter-change 事件,是 [drop-the-dead-summary](../../implemented/simplification/2026-06-19-drop-mutable-session-summary.md) 模式在更小尺度上的重复。
|
||||
这与 `tools/change` 和 `system-prompt/change` 不同。后两个事件目前同样未被消费,但它们是合理的注册表变更信号,未来可能服务于实时工具/提示词 UI。LLM(大语言模型)适配器注册更接近启动时的实现细节:适配器不是用户可见的面板,真正的模型调用拦截 seam 是 `llm/stream`。保留一个没有监听器的 adapter-change 事件,是在更小规模上重复 [drop-the-dead-summary](../../implemented/simplification/2026-06-19-drop-mutable-session-summary.md) 的模式。
|
||||
|
||||
这个事件并非零成本。`registerAdapter()` 在发射 `llm/adapter-change` 之前先 yield 回滚 disposer,这样抛异常的监听者会回退变更而不是泄漏一条适配器条目;包里还有测试覆盖这条监听者抛异常的路径。这种防御性排序所保护的失败模式,只有测试才能触发。
|
||||
这个事件并非零成本。`registerAdapter()` 在发出 `llm/adapter-change` 之前先 yield 回滚 disposer,这样抛出异常的监听器会回退变更而非泄漏适配器条目;包内还有针对该监听器抛出路径的测试。这种防御性排序保护的是一个只有测试才能触发的失败模式。
|
||||
|
||||
## 决策
|
||||
|
||||
只移除 `llm/adapter-change`:`dsh-llm` 的 `interface Events` 中的声明、`ctx.emit('llm/adapter-change')` 调用,以及 `LlmService.registerAdapter` JSDoc 中「在注册和 dispose 时发射 `llm/adapter-change`」的描述。`registerAdapter()` 的 effect generator 保留变更与回滚 disposer(用于 HMR(热模块替换)/dispose),但去掉仅为已移除事件而存在的监听者抛异常回滚排序。适配器 disposer 测试断言返回的 disposer 能移除适配器,不再订阅该事件;监听者抛异常的回滚测试随其主题一同移除。[docs/architecture.md](../../../architecture.md) 和 [packages/llm/llm/README.md](../../../../packages/llm/llm/README.md) 中的事件分类体系在同一个变更中更新。
|
||||
仅移除 `llm/adapter-change`:`dsh-llm` 的 `interface Events` 中的声明、`ctx.emit('llm/adapter-change')` 调用,以及 `LlmService.registerAdapter` JSDoc 中的 "Emits `llm/adapter-change` on registration and disposal" 语句。`registerAdapter()` 的 effect generator 保留变更与回滚 disposer 以支持 HMR(热模块替换)/dispose,但去掉了仅为已移除事件而存在的监听器抛出回滚排序。适配器 disposer 测试断言返回的 disposer 能移除适配器,而不再订阅该事件;监听器抛出回滚测试随其主题一同移除。[docs/architecture.md](../../../architecture.md) 和 [packages/llm/llm/README.md](../../../../packages/llm/llm/README.md) 中的事件分类体系在同一个变更中更新。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
### 为什么不移除所有注册表变更事件?
|
||||
|
||||
一个注册表主动广播变更的微内核是一种自洽的约定。`tools/change` 和 `system-prompt/change` 在 UI 能实时刷新可用工具或提示词段落时可能变得有用。本 RFC 在有合理的面向用户消费方的地方保留该约定,仅裁掉当前和可预见未来都没有明确消费方的 adapter-change 事件。
|
||||
一个注册表主动广播变更的微内核是一种自洽的约定。`tools/change` 和 `system-prompt/change` 在 UI 能实时刷新可用工具或提示词段落时可能变得有用。本 RFC 保留该约定中有合理的面向用户消费方的部分,仅裁掉当前和可预见未来消费方都不明确的 adapter-change 事件。
|
||||
|
||||
如果将来需要 LLM 适配器浏览器或动态模型选择器,届时再连同消费方一起重新引入该事件,并给出比「something changed」更清晰的 payload。
|
||||
如果将来需要 LLM 适配器浏览器或动态模型选择器用到此信号,届时再连同消费方一起重新引入,并提供比「something changed」更清晰的 payload。
|
||||
|
||||
## 验证
|
||||
|
||||
`llm/adapter-change` 及其发射点已移除,重新生成的 cordis catalog 是最新的;HMR 安全性保持(dispose 一个贡献 fiber 会移除对应适配器);`tools/change` 和 `system-prompt/change` 仍有文档和测试;没有任何生产路径的可观测行为发生变化——ACP 快照 golden 和 echo-agent 冒烟测试逐字节不变。
|
||||
`llm/adapter-change` 及其 emit 已移除,重新生成的 cordis catalog 是最新的;HMR 安全性保持(dispose 一个贡献 fiber 会移除对应适配器);`tools/change` 和 `system-prompt/change` 仍有文档和测试;没有任何生产路径的可观察行为发生变化——ACP(Agent Client Protocol)快照 golden 和 echo-agent 冒烟测试逐字节未变。
|
||||
|
||||
## 后果
|
||||
|
||||
- **移除一个已文档化的发射事件属于公开接口变更。** 它出现在分类体系表中,读起来像是有意为之的 API。但「已声明并发射」不等于「有消费方」——这正是当初移除可变 summary 时所依据的同一区分。分类体系表在同一个变更中更新,因此文档不会漂移。
|
||||
- **注册表变更约定变得不均匀。** 这是可以接受的,因为 LLM 适配器注册与工具或提示词段落不是同一层面的用户可见概念。不均匀但诚实,胜过统一但空转。
|
||||
- **移除一个已文档化的 emit 事件属于公开接口变更。** 它出现在分类体系表中,读起来像有意设计的 API。但「已声明且已发出」不等于「已被消费」——这与移除可变 summary 时的判断依据相同。分类体系表在同一个变更中更新,因此文档不会漂移。
|
||||
- **注册表变更约定变得不均匀。** 这是可接受的,因为 LLM 适配器注册与工具或提示词段落不是同一层面的面向用户概念。不均匀但诚实,胜过统一但无用。
|
||||
|
||||
这是一个小裁剪,但它退役了一条守护着不存在的消费方的常设正确性不变式。
|
||||
这是一个小裁剪,但它退役了一条守护着并不存在的消费方的正确性不变式。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-06-20-drop-unconsumed-llm-assembled-surfaces.md: c8999dd0e19b2c8eaff854c8ff544bae2fc068b6
|
||||
2026-06-20-drop-unconsumed-llm-assembled-surfaces.zh.md: 090d3e779e3e7a9f1f0a65af7740ad685f723cf6
|
||||
2026-06-20-drop-unconsumed-llm-assembled-surfaces.zh.md: de297a8cb64d9002fce2c857fd3caa5f2b25f43a
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# RFC:移除未被消费的 LLM 组装便利接口
|
||||
# RFC:移除未被消费的 LLM 组装便捷接口
|
||||
|
||||
Status: implemented
|
||||
|
||||
@@ -9,31 +9,31 @@ Status: implemented
|
||||
`LlmService`([packages/llm/llm/src/index.ts](../../../../packages/llm/llm/src/index.ts))在模型之上暴露了三个调用接口:
|
||||
|
||||
- `stream()`:原始 `StreamChunk`,通过 `llm/stream` waterfall(瀑布式事件)分发。
|
||||
- `streamBlocks()`:一个"便利视图",将 chunk 送入 `BlockAssembler` 并按流顺序 yield 已组装完成的 `ContentBlock`([index.ts:137-144](../../../../packages/llm/llm/src/index.ts))。
|
||||
- `generate()`:一个完整组装的 `GenerateResult`,通过第二个 `llm/generate` waterfall 分发([index.ts:151-157](../../../../packages/llm/llm/src/index.ts))。
|
||||
- `streamBlocks()`:一个「便捷视图」,将分片送入 `BlockAssembler` 并按流顺序产出已组装的 `ContentBlock`([index.ts:137-144](../../../../packages/llm/llm/src/index.ts))。
|
||||
- `generate()`:一个完整组装的 `GenerateResult`,通过第二条 `llm/generate` waterfall 分发([index.ts:151-157](../../../../packages/llm/llm/src/index.ts))。
|
||||
|
||||
LLM(大语言模型)服务唯一的生产消费方是 agent loop(智能体循环),它只使用 `stream()`:将原始 chunk 送入自己的 `BlockAssembler`,以便在并行组装的同时记录 chunk 用于回放保真([packages/core/agent-loop/src/loop.ts](../../../../packages/core/agent-loop/src/loop.ts) 中的 `ctx.llm.stream(req)` 步骤)。在 `packages/*/src` 和 `examples/*/src` 中搜索 `streamBlocks` 与 `ctx.llm.generate`,找不到任何生产调用方。引用它们的只有服务方法定义、文档和测试;适配器测试用 `generate()` 作为便利驱动,但它们完全可以通过同一个 assembler 辅助函数手动消费 `stream()`,无需保留一个公开的生产 API。
|
||||
LLM(大语言模型)服务唯一的生产消费方是 agent loop(智能体循环),它只使用 `stream()`:将原始分片送入自己的 `BlockAssembler`,以便在并行组装的同时记录分片,保证回放保真度([packages/core/agent-loop/src/loop.ts](../../../../packages/core/agent-loop/src/loop.ts),`ctx.llm.stream(req)` 步骤)。在 `packages/*/src` 和 `examples/*/src` 中 grep `streamBlocks` 与 `ctx.llm.generate`,找不到任何生产调用方。仅有的引用来自服务方法定义、文档和测试;适配器测试用 `generate()` 作为便捷驱动,但它们完全可以通过同一个 assembler 辅助函数手动消费 `stream()`,无需为此保留一个公开的生产 API。
|
||||
|
||||
这与 [drop-mutable-session-summary](../../implemented/simplification/2026-06-19-drop-mutable-session-summary.md) 是同一模式:拥有测试契约的组装视图 API,消费方只有测试而非生产代码。它们是为"不关心 token 级增量"的消费方预先构建的,但唯一的真实消费方恰恰需要增量,以便持久化高保真的回放数据。
|
||||
这与 [drop-mutable-session-summary](../../implemented/simplification/2026-06-19-drop-mutable-session-summary.md) 是同一模式:拥有经过测试的契约的组装视图 API,消费方却只有测试而非生产代码。它们是为「不关心 token 级增量」的消费方预设的,但唯一的真实消费方恰恰需要增量,以便持久化高保真回放数据。
|
||||
|
||||
`streamBlocks()` 拖带了 `BlockAssembler` 中一块专用逻辑:`flushReady()` 和 `flushRemaining()`([packages/llm/llm/src/assembler.ts:138-168](../../../../packages/llm/llm/src/assembler.ts))以及 `flushed` 游标字段,仅为支持按序增量 yield 而存在。`generate()` 拖带了 `GenerateResult`、`BlockAssembler.result()` 以及 `llm/generate` waterfall——在同一底层流之上多出的第二个拦截面。agent loop 对 assembler 的使用仅限 `push()` / `message()` / `usage` / `finish`,不涉及流式 flush 或一次性服务组装。
|
||||
`streamBlocks()` 拖带了 `BlockAssembler` 的一块专用逻辑:`flushReady()` 与 `flushRemaining()`([packages/llm/llm/src/assembler.ts:138-168](../../../../packages/llm/llm/src/assembler.ts))以及 `flushed` 游标字段,仅为支持按序增量产出而存在。`generate()` 拖带了 `GenerateResult`、`BlockAssembler.result()` 以及 `llm/generate` waterfall——在同一底层流之上的第二个拦截面。agent loop 对 assembler 的使用仅限于 `push()` / `message()` / `usage` / `finish`,不涉及流式 flush 或一次性服务组装。
|
||||
|
||||
## 决策
|
||||
|
||||
`stream()` 是唯一的公开 LLM 调用接口。移除 `streamBlocks`、`generate`、其事件/结果类型,以及仅被该路径使用的 assembler 辅助方法。适配器测试通过本地辅助函数对公开的 stream 进行组装,`BlockAssembler` 只保留有生产消费方的操作。
|
||||
`stream()` 是唯一的公开 LLM 调用接口。移除 `streamBlocks`、`generate`、其事件/结果类型,以及仅被该路径使用的 assembler 辅助方法。适配器测试通过本地辅助函数对公开的 stream 进行组装;`BlockAssembler` 仅保留有生产消费方的操作。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
**保留 `generate()` 作为仅供测试的便利方法**:否决。适配器测试通过共享 assembler 手动消费 `stream()`,走的是与生产相同的流式路径;一个唯一调用方是测试的公开方法,正是[移除可变摘要先例](2026-06-19-drop-mutable-session-summary.md)所清退的死接口形态。未来如果有消费方需要不带增量的组装块,届时再引入一个有真实消费方的专用辅助方法。
|
||||
**保留 `generate()` 作为仅供测试的便捷方法**:否决。适配器测试通过共享 assembler 手动消费 `stream()`,走的是与生产完全相同的流式路径;一个唯一调用方只有测试的公开方法,正是 [drop-mutable-summary 先例](2026-06-19-drop-mutable-session-summary.md)所淘汰的死接口形态。未来如果有消费方需要不带增量的组装块,届时再为该消费方引入一个聚焦的辅助方法。
|
||||
|
||||
## 验证
|
||||
|
||||
`streamBlocks`、`generate`、`llm/generate` 以及仅被它们使用的 assembler 辅助方法已全部移除,无新增死导出;两个真实适配器通过 `stream()` 加共享 assembler 得到充分测试;agent loop 行为不变(ACP 快照 golden 文件无变化);README、架构文档与模块文档中不再提及被移除的接口。
|
||||
`streamBlocks`、`generate`、`llm/generate` 及其独占的 assembler 辅助方法已移除,无新增死导出;两个真实适配器通过 `stream()` 和共享 assembler 得到验证;agent loop 行为不变(ACP 快照 golden 文件无变化);README、架构文档与模块文档中不再提及已移除的接口。
|
||||
|
||||
## 后果
|
||||
|
||||
- **从一个核心词汇包中移除了公开方法。** 未来如果有插件需要不带增量的组装块,它需要直接调用 `stream()` 并使用 `BlockAssembler`,或在有真实消费方时重新引入一个专用辅助方法。鉴于预发布阶段「基础优先于投机性未来」的立场([AGENTS.md](../../../../AGENTS.md)),现在正是清除仅供测试的公开形状的正确时机。
|
||||
- **适配器测试变得更显式。** 它们失去了便利的 `generate()` 包装层,但这是有益的压力:测试走的是与生产相同的流式路径。
|
||||
- **waterfall 使用方失去 `llm/generate`。** 不存在生产监听者。未来的缓存/重试/日志插件应包装 `llm/stream`,它仍是唯一的提供方调用路径。
|
||||
- **从一个核心词汇包中移除了公开方法。** 未来如果有插件需要不带增量的组装块,它需要直接调用 `stream()` 并使用 `BlockAssembler`,或在有真实消费方时重新引入一个聚焦的辅助方法。鉴于预发布阶段「基础优先于预设未来」的立场([AGENTS.md](../../../../AGENTS.md)),现在正是裁剪仅供测试的公开接口的合适时机。
|
||||
- **适配器测试变得更显式。** 它们失去了便捷的 `generate()` 包装层,但这是有益的压力:测试走的是与生产相同的流式路径。
|
||||
- **waterfall 使用者失去 `llm/generate`。** 不存在生产监听者。未来的缓存/重试/日志插件应包装 `llm/stream`,它仍然是唯一的提供方调用路径。
|
||||
|
||||
变更规模不大,但它干净地从 LLM 包中移除了投机性的接口面积,为生产和测试留下唯一一份模型调用契约。
|
||||
改动规模不大,但它从 LLM 包中干净地移除了预设的接口面积,为生产和测试留下唯一一份模型调用契约。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-06-20-prune-dead-seam-methods.md: cb3eb576dbae209ddccbea7f42a80ef09f842887
|
||||
2026-06-20-prune-dead-seam-methods.zh.md: d3c658ea2ce994e640ec4fd2cf5f74d82f695e69
|
||||
2026-06-20-prune-dead-seam-methods.zh.md: 988da3c44d8860d89f090f0ea9e2af49ae9007dd
|
||||
|
||||
@@ -1,43 +1,43 @@
|
||||
# RFC:清理持久化 seam 中的无用方法
|
||||
# RFC:从 persistence seam 中移除无用方法
|
||||
|
||||
[English](2026-06-20-prune-dead-seam-methods.md) | 中文
|
||||
|
||||
Status: implemented
|
||||
|
||||
> **实现说明:** 最终只移除了 `SessionPersistence.has()` 和 `.delete()`。`BashExecutor.get()` 和 `.list()` 保留,因为移除它们的单行查找接口需要在消费方引入大量额外的完成状态跟踪机制。它们的 id 品牌化由 [branded-ids RFC](../architecture/2026-06-20-branded-ids.md) 覆盖。
|
||||
> **实现说明:** 最终只移除了 `SessionPersistence.has()` 和 `.delete()`。`BashExecutor.get()` 和 `.list()` 保留,因为移除它们的单行查询接口需要在消费方引入大量额外的完成状态追踪机制。它们的 id 品牌化由 [branded-ids RFC](../architecture/2026-06-20-branded-ids.md) 覆盖。
|
||||
|
||||
## 问题
|
||||
|
||||
一个能力 seam([接口/实现/消费方](../../implemented/architecture/2026-06-13-capability-seams.md))携带了没有任何消费方调用的抽象方法。seam 存在的意义是让实现与消费方独立演进,但一个没有消费方编程依赖的方法不是 seam,而是投机性的接口面——每个实现仍然必须实现并测试它。
|
||||
一个能力 seam([接口/实现/消费方](../../implemented/architecture/2026-06-13-capability-seams.md))承载着没有任何消费方调用的抽象方法。seam 的存在是为了让实现与消费方独立演进,但一个没有消费方编程依赖的方法不是 seam,而是每个实现仍须实现和测试的投机性接口面。
|
||||
|
||||
### `SessionPersistence.has()` 与 `.delete()`
|
||||
|
||||
抽象服务在 create/append 之外声明了更多操作:`load`、`list`、`has`、`delete`。`ctx.sessionPersistence` 的生产消费方只用到两个:agent loop(智能体循环)的恢复路径调用 `load()`([packages/core/agent-loop/src/index.ts:176](../../../../packages/core/agent-loop/src/index.ts)),ACP 桥接层为 `session/list` 调用 `list()`([packages/ui/acp/src/index.ts:494](../../../../packages/ui/acp/src/index.ts))。在 `packages/*/src` 和 `examples/` 中 grep 所有 `sessionPersistence.*` / `persistence.*` 用法,找不到对该服务的 `has(` 或 `delete(` 调用。`packages/ui/acp/src/index.ts` 中的 `.has(`/`.delete(` 调用作用于内存中的 `SessionStore` 和一个本地的 loading id `Set`,而非持久化服务。`has`/`delete` 的唯一调用方是契约测试套件和各后端的 spec。
|
||||
该抽象服务在 create/append 之外声明了更多操作:`load`、`list`、`has`、`delete`。`ctx.sessionPersistence` 的生产消费方只用了两个:agent loop(智能体循环)的恢复路径调用 `load()`([packages/core/agent-loop/src/index.ts:176](../../../../packages/core/agent-loop/src/index.ts)),ACP(Agent Client Protocol)桥接层为 `session/list` 调用 `list()`([packages/ui/acp/src/index.ts:494](../../../../packages/ui/acp/src/index.ts))。在 `packages/*/src` 和 `examples/` 中 grep 所有 `sessionPersistence.*` / `persistence.*` 的使用,找不到对该服务的 `has(` 或 `delete(` 调用。`packages/ui/acp/src/index.ts` 中的 `.has(`/`.delete(` 调用作用于内存中的 `SessionStore` 和一个本地的 loading id `Set`,而非 persistence。`has`/`delete` 的唯一调用者是契约测试套件和各后端的 spec。
|
||||
|
||||
`has()` 不仅仅是未使用——它还是共享协调器中最复杂的分支:一个 tracked-vs-untracked 双探测(`loadLive(id, cwd)` 用于活跃跟踪的会话,`loadStored(id)` 用于未跟踪的会话),附带多行注释说明理由。`delete()` 则拖带了 `deleteStored` 后端钩子,每个后端都必须实现它。这与 [drop-mutable-session-summary](../../implemented/simplification/2026-06-19-drop-mutable-session-summary.md) 是同一模式:契约测试覆盖了两者,但没有任何发布代码会问「这个会话是否已持久化?」或删除一个会话。
|
||||
`has()` 不仅是未使用——它还是共享协调器中最复杂的分支:一个 tracked-vs-untracked 双探测(`loadLive(id, cwd)` 用于活跃追踪的会话,`loadStored(id)` 用于未追踪的会话),附带多行注释说明理由。`delete()` 则拖带了 `deleteStored` 后端钩子,每个后端都必须实现它。这与 [drop-mutable-session-summary](../../implemented/simplification/2026-06-19-drop-mutable-session-summary.md) 是同一模式:契约测试覆盖了两者,但没有任何发布代码会问「这个会话是否已持久化?」或删除一个会话。
|
||||
|
||||
## 决策
|
||||
|
||||
没有消费方使用的方法被移除——从抽象 seam、实现,以及仅为覆盖它们而存在的契约/spec 测试套件中移除:
|
||||
|
||||
- `SessionPersistence.has()` / `.delete()` 已移除:抽象声明、协调器的 `has`/`delete`/`deleteCore`,以及 `PersistenceBackend.deleteStored` 钩子(jsonl 和 sqlite 各自实现 `deleteStored` 仅仅是为了满足该钩子——那些实现也一并移除)。后端属于[双后端](../../implemented/architecture/2026-06-14-session-persistence.md)设计,本身不在本 RFC 范围内;移除它们为无消费方实现的钩子是移除钩子的一部分,而非后端重设计。
|
||||
- 所有文档和源码注释中的引用都已更新为存活的四方法、仅含 `list()` 的契约——不仅是字面的 `has(`/`delete(`/`deleteStored` 拼写,还包括 `{@link has}`/`{@link delete}` JSDoc 链接和「六个公开方法」之类的计数——涉及 seam 和后端 README、[docs/architecture.md](../../../architecture.md)、[session-persistence](../../implemented/architecture/2026-06-14-session-persistence.md) 与 [write-coordinator](../../implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md) RFC,以及协调器/后端的 JSDoc。
|
||||
- `SessionPersistence.has()` / `.delete()` 已移除:抽象声明、协调器的 `has`/`delete`/`deleteCore`,以及 `PersistenceBackend.deleteStored` 钩子(jsonl 和 sqlite 各自实现 `deleteStored` 仅为满足该钩子——这些实现也一并移除)。后端属于[双后端](../../implemented/architecture/2026-06-14-session-persistence.md)设计,本身不在本次范围内;移除它们为无消费方实现的钩子是移除钩子的一部分,而非后端重新设计。
|
||||
- 所有文档和源码注释中的引用都已更新为存留的四方法、仅含 `list()` 的契约——不仅是字面的 `has(`/`delete(`/`deleteStored` 拼写,还包括 `{@link has}`/`{@link delete}` JSDoc 链接和「六个公开方法」之类的计数——涉及 seam 和后端 README、[docs/architecture.md](../../../architecture.md)、[session-persistence](../../implemented/architecture/2026-06-14-session-persistence.md) 和 [write-coordinator](../../implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md) RFC,以及协调器/后端的 JSDoc。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
### 为什么不以「seam 应当完整」为由保留?
|
||||
|
||||
「持久化 seam 理应提供 delete」这种直觉是真实的——而它恰恰是预发布阶段所警惕的投机完整性([AGENTS.md](../../../../AGENTS.md):为正确的基础优化,而非为你并不拥有的假想调用方优化)。`delete()` 只是一个方法,等到消费方真正需要时再加回来即可:一个删除旧会话的会话管理 UI 会需要它——到那时再加,针对该 UI 的真实需求设计(软删除?级联?确认?),而非现在猜测。
|
||||
「persistence seam 理应提供 delete」这种直觉是真实的——但它恰恰是预发布阶段所警惕的投机性完整([AGENTS.md](../../../../AGENTS.md):为正确的基础优化,而非为你并不拥有的假想调用者优化)。`delete()` 是一个方法,等消费方真正需要时再加回来即可:一个删除旧会话的会话管理 UI 会需要它——到那时再加,基于该 UI 的真实需求来设计(软删除?级联?确认?),而非现在猜测。
|
||||
|
||||
在有活跃消费方时重新加入一个 seam 方法,成本低且设计更优,因为消费方锁定了契约。无人使用地携带它,意味着每个实现(以及未来的每个后端)都必须实现并测试一个什么也不做的方法。
|
||||
在有活跃消费方的情况下重新添加一个 seam 方法,成本低且设计更优,因为消费方锚定了契约。在无人使用的情况下保留它,意味着每个实现(以及未来的每个后端)都必须实现和测试一个无实际作用的方法。
|
||||
|
||||
## 验证
|
||||
|
||||
`has`/`delete`/`deleteStored` 已从持久化 seam、实现和契约测试套件中移除,没有新增无用导出;剩余操作(`create`/`append`/`load`/`list`)未受影响,ACP `session/list` 和崩溃恢复行为完全一致;seam README 和 `docs/architecture.md` 只列出存活的方法。
|
||||
`has`/`delete`/`deleteStored` 已从 persistence seam、实现和契约测试套件中移除,没有新增无用导出;剩余操作(`create`/`append`/`load`/`list`)未受影响,ACP `session/list` 和崩溃恢复行为完全一致;seam README 和 `docs/architecture.md` 仅列出存留的方法。
|
||||
|
||||
## 后果
|
||||
|
||||
- **`delete()` 是产品最终会需要的那类操作。** 确实如此——但「最终」正是关键。现在删除、等有真实消费方时再加回来,严格优于发布一份猜测的契约。双后端各自去掉了一个 `deleteStored` 实现,这是在本来不在范围内的包中的有限改动。
|
||||
- **低耦合。** 移除局限于持久化 seam + 实现 + 测试;没有跨包消费方引用被移除的方法,因此文档之外没有涟漪效应。
|
||||
- **`delete()` 是产品最终会需要的操作。** 确实如此,但「最终」正是关键。现在删除、将来基于真实消费方重新添加,严格优于发布一份猜测的契约。两个后端各自减少了一个 `deleteStored` 实现,这是在本次范围之外的包中的有限改动。
|
||||
- **低耦合。** 移除局限于 persistence seam + 实现 + 测试;没有跨包消费方引用被移除的方法,因此除文档外没有涟漪效应。
|
||||
|
||||
规模不大,但它将 seam 从「实现必须为无人提供什么」恢复为「恰好是消费方使用的东西」。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-06-20-public-agent-stop-surface.md: 8c371911616b0a156156355b2ca15d795cfd21e5
|
||||
2026-06-20-public-agent-stop-surface.zh.md: 31deaa3649026a7579702e8e47edfdf05d2543ae
|
||||
2026-06-20-public-agent-stop-surface.zh.md: bbd61fa1738fda64ec5e068dae84062163937c1a
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
# RFC:保留单一公开停止原语
|
||||
|
||||
[English](2026-06-20-public-agent-stop-surface.md) | 中文
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-06-20-public-agent-stop-surface.md) | 中文
|
||||
|
||||
> **实现说明:** 仅移除了 `abort()`。`whenIdle()` 予以保留,因为它是公开的静默信号,能安全处理等待者结算与替换轮次竞态;消费方不应从状态转换中自行重建该行为。
|
||||
|
||||
## 问题
|
||||
|
||||
公开的 `Agent` 句柄暴露了两种重叠的方式来停止进行中的工作:`abort(reason?)` 与 `cancel(reason?)`。`abort()` 仅终止当前正在执行的步骤,不影响队列中的工作;`cancel()` 清除队列中的工作与 steering(中途引导),终止正在运行的步骤,并处理步骤前竞态。在生产环境中,ACP(Agent Client Protocol)使用 `cancel()` 实现 `session/cancel`,而生命周期所有者通过 `AgentHandle.dispose()` 拆除 agent。没有生产调用方需要裸 `abort()`。
|
||||
公开的 `Agent` 句柄暴露了两种重叠的方式来停止进行中的工作:`abort(reason?)` 和 `cancel(reason?)`。`abort()` 仅终止当前步骤,不影响队列中的工作;`cancel()` 清除队列中的工作和 steering(中途引导)工作、中止正在运行的步骤,并处理步骤前竞态。在生产环境中,ACP(Agent Client Protocol)使用 `cancel()` 实现 `session/cancel`,而生命周期所有者通过 `AgentHandle.dispose()` 销毁 agent(智能体)。没有生产调用方需要裸 `abort()`。
|
||||
|
||||
`abort()` 与 `cancel()` 的区别是真实存在的:`abort()` 保留队列中的提示词和 steering,而 `cancel()` 丢弃它们。但没有已发布的代码调用过公开的 `abort()` 动词。agent loop(智能体循环)自身的停止路径(`cancel()` 与 dispose)直接终止当前 `AbortController`,而非经由 `Agent.abort()` 路由。大多数调用 `abort()` 的测试实际上中断的是空队列,可以改用 `cancel(reason)`;那个刻意依赖队列保留的 steering 重投递测试则直接驱动进行中的 `AbortController`,因为 `cancel()` 会丢弃它试图证明在步骤终止后仍存活的队列 steering。无参 `abort()` 的默认原因(`'aborted'`)随动词一起删除,而非意外保留;`cancel()` 保留自己的默认值 `'cancelled'`。
|
||||
`abort()`/`cancel()` 的区别是真实存在的:`abort()` 保留队列中的提示词和 steering,而 `cancel()` 丢弃它们。但没有任何已上线的代码调用过公开的 `abort()` 动词。循环自身的停止路径(`cancel()` 和 disposal)直接中止当前 `AbortController`,而不经由 `Agent.abort()` 路由。大多数调用 `abort()` 的测试中断的是空队列,可以改用 `cancel(reason)`;那个刻意依赖队列保留的 steering 重投递测试则直接驱动进行中的 `AbortController`,因为 `cancel()` 会丢弃它试图证明在步骤中止后仍存活的已排队 steering。无参 `abort()` 的默认原因(`'aborted'`)随该动词一起删除,而非被意外保留;`cancel()` 保留自己的 `'cancelled'` 默认值。
|
||||
|
||||
多余的公开接口面使 agent loop 不得不承载一个本质上是拆除内部机制的公开动词:`abort()` 必须被文档描述为与队列感知的取消不同,尽管 UI 取消几乎总是需要更广义的操作。
|
||||
多余的公开接口使得循环不得不承载一个本质上属于内部拆卸的公开动词:`abort()` 必须被文档描述为有别于队列感知的取消,尽管 UI 取消几乎总是需要更广泛的操作。
|
||||
|
||||
## 决策
|
||||
|
||||
`cancel()` 是 `Agent` 上唯一的公开*停止*原语。生命周期所有者使用 `AgentHandle.dispose()` 停止并注销 agent;非所有者使用 `cancel()` 放弃当前与队列中的工作。实现内部保留一个私有 abort controller,但它不属于面向插件的 `Agent` 契约。
|
||||
`cancel()` 是 `Agent` 上唯一的公开*停止*原语。生命周期所有者使用 `AgentHandle.dispose()` 停止并注销 agent;非所有者使用 `cancel()` 放弃当前和队列中的工作。实现内部保留一个私有的 abort controller,但它不属于面向插件的 `Agent` 契约。
|
||||
|
||||
`whenIdle()` 作为公开的静默观测原语**予以保留**(agent 脱离 `running` 状态后 resolve;已处于 idle 时立即 resolve;dispose 后等待循环退出)。它不是停止动词;它是非所有者观测停止*完成*而无需 dispose agent 的方式。它的活跃消费方是 ACP 和通过此公开 seam 等待结算的 agent 测试(`packages/ui/acp/tests`、`packages/core/agent-loop/tests`);生产环境的 ACP 桥接层拥有其 agent 并通过 `AgentHandle.dispose()` 拆除它们,因此 `packages/ui/acp/src` 本身没有 `whenIdle()` 调用。
|
||||
`whenIdle()` **保留**为公开的静默观测原语(agent 从 `running` 状态稳定后 resolve,已处于 idle 时立即 resolve,dispose 后等待循环退出)。它不是停止动词;它是非所有者在不 dispose agent 的前提下观测停止*完成*的方式。它的活跃消费方是 ACP 和通过此公开 seam 等待结算的 agent 测试(`packages/ui/acp/tests`、`packages/core/agent-loop/tests`);生产环境的 ACP 桥接层拥有其 agent 并通过 `AgentHandle.dispose()` 销毁它们,因此 `packages/ui/acp/src` 本身没有 `whenIdle()` 调用。
|
||||
|
||||
公开的 `abort()` 被删除,连同将其作为独立 API 测试的用例以及将步骤级终止描述为嵌入特性的文档。空队列终止测试迁移到 `cancel(reason)`,仍然验证取消行为;测试对象为 agent loop 内部 `AbortController` 的测试通过包内类型转换直接驱动该 controller 的私有字段;仅固定已移除的无参 `abort()` 默认值的测试随方法一起删除。disposer 仍为异步,仍等待循环停止。
|
||||
公开的 `abort()` 被删除,连同将其作为独立 API 测试的用例以及将步骤级中止描述为嵌入特性的文档。空队列中止测试迁移到 `cancel(reason)`,仍然验证取消行为;以循环内部 `AbortController` 为测试对象的用例通过包内类型转换直接驱动该 controller 的私有字段;仅固定已移除的无参 `abort()` 默认值的测试随方法一起删除。disposer 仍为异步,仍等待循环停止。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
**同时移除 `whenIdle()`**:最初提案的形态,在对照代码验证前提后被推翻(上方的实现说明记录了完整过程):它是承重的静默原语,强迫消费方手动观测 `running`→`idle` 转换正是防御性模式所警告的脆弱路径。
|
||||
**同时移除 `whenIdle()`**:最初提案的形态,在对照代码验证前提后被推翻(上方的实现说明记录了完整过程):它是承重的静默原语,迫使消费方手动观测 `running`→`idle` 转换正是防御性模式所警告的脆弱路径。
|
||||
|
||||
## 验证
|
||||
|
||||
`Agent` 不再暴露公开的 `abort()`,而 `cancel()`、`whenIdle()` 与 `steer()` 保留;ACP 取消调用 `cancel()`;拆除通过 handle disposal 等待静默,`whenIdle()` 为非所有者观测者在静默时 resolve;测试套件覆盖取消与 disposal 作为两条受支持的停止路径。
|
||||
`Agent` 不再暴露公开的 `abort()`,而 `cancel()`、`whenIdle()` 和 `steer()` 保留;ACP 取消调用 `cancel()`;拆卸通过 handle disposal 等待静默,`whenIdle()` 在静默时为非所有者观测者 resolve;测试套件覆盖取消和 disposal 作为两条受支持的停止路径。
|
||||
|
||||
## 后果
|
||||
|
||||
未来的插件无法通过公开接口仅终止当前模型/工具步骤而保留队列中的提示词。如果该用例变为现实需求,它应当带着一个具名消费方和更窄的契约重新引入。目前它只是把一个私有循环机制暴露为公开接口的潜在泛化。
|
||||
未来的插件无法通过公开接口仅中止当前模型/工具步骤而保留队列中的提示词。如果该用例变为现实需求,它应当带着一个具名消费方和更窄的契约回归。目前它是将私有循环机制保持公开的潜在泛化。
|
||||
|
||||
## 相关
|
||||
|
||||
本 RFC 仅移除冗余的停止动词。中途 steering 仍是有意保留的消息路径;静默观测仍通过 `whenIdle()` 提供。最终的公开接口面为 `send()`、`steer()`、`inject()`、`cancel()`、`whenIdle()`、status、options、session 与 identity。
|
||||
本 RFC 仅移除冗余的停止动词。中途 steering 仍是有意保留的消息路径;静默观测仍通过 `whenIdle()` 提供。最终的公开接口为 `send()`、`steer()`、`inject()`、`cancel()`、`whenIdle()`、status、options、session 和 identity。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-06-20-remove-agent-boundary-mirror-events.md: 46b5e43951885915d4c3dd3f867ced6c31d32035
|
||||
2026-06-20-remove-agent-boundary-mirror-events.zh.md: dd6952ec8923c17d703fc6850197bef09b1c0ee7
|
||||
2026-06-20-remove-agent-boundary-mirror-events.zh.md: 15be07f43997b1d899f0297d311c3ad83f088ee0
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
# RFC:停止将持久化边界镜像为 agent 事件
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-06-20-remove-agent-boundary-mirror-events.md) | 中文
|
||||
|
||||
Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
agent loop(智能体循环)曾通过可回放的 `SessionEvent` 日志和实时 `agent/*` 镜像两条路径暴露持久化的轮次与步骤边界。消费方不得不在两个表达同一事实的来源之间做选择,并协调二者的时序。ACP(Agent Client Protocol)和持久化层已经使用事件日志;stdio UI 是唯一仍在消费镜像事件的组件,而它也已经从 `session/event` 渲染工具调用和工具结果。
|
||||
agent loop(智能体循环)通过可回放的 `SessionEvent` 日志和实时 `agent/*` 镜像两条路径暴露持久化的轮次与步骤边界。消费方不得不在同一事实的两个来源之间做选择,并协调二者的时序。ACP(Agent Client Protocol)和持久化层已经使用日志;stdio UI 是唯一仍在消费镜像的组件,而它已经从 `session/event` 渲染工具调用和工具结果。
|
||||
|
||||
这种重复并非零成本。每次生命周期变更都要同时更新会话事件、镜像事件、文档、不变式、测试和快照预期。重复的边界事件还使失败排序变得微妙:一个轮次可能在实时 `agent/turn-end` 监听器运行之前就已被持久化关闭,因此边界之后的监听器失败在日志中已没有合法的位置可插入,只能带外报告。
|
||||
这种重复并非零成本。每次生命周期变更都需要同时更新会话事件、镜像事件、文档、不变式、测试和快照预期。重复的边界事件还使失败排序变得微妙:一个轮次可能在实时 `agent/turn-end` 监听器运行之前就已被持久化关闭,因此边界之后的监听器失败在日志中已没有合法位置可以插入,只能带外上报。
|
||||
|
||||
## 决策
|
||||
|
||||
让 `session/event` 成为唯一的实时边界/transcript(文本记录)流。需要渲染轮次、工具调用、工具结果、助手消息和持久化边界的消费方统一订阅 `session/event`,从持久化层使用的同一套事件词汇派生 UI。
|
||||
将 `session/event` 作为唯一的实时边界/transcript(文本记录)流。需要渲染轮次、工具调用、工具结果、助手消息和持久化边界的消费方统一订阅 `session/event`,从持久化层使用的同一套事件词汇中派生 UI。
|
||||
|
||||
移除 `agent/turn-start`、`agent/turn-end`、`agent/step-start` 和 `agent/step-end`。边界消费方改为订阅 `session/event`。需要 agent 标签的 UI 通过 `agent/created` 和 `agent/disposed` 维护一份 session 到 agent 的映射,因为持久化的 `turn/start` 携带轮次编号但不携带 agent id。
|
||||
移除 `agent/turn-start`、`agent/turn-end`、`agent/step-start` 和 `agent/step-end`。边界消费方改为订阅 `session/event`。如果 UI 需要 agent 标签,则通过 `agent/created` 和 `agent/disposed` 维护一份 session 到 agent 的映射,因为持久化的 `turn/start` 携带轮次编号但不携带 agent id。
|
||||
|
||||
步骤镜像没有消费方,已由 [event-domain-semantics RFC](../architecture/2026-06-30-event-domain-semantics.md) 率先移除。该决策保留了轮次镜像供 stdio UI 使用;本 RFC 在将测试 REPL 迁移到 `session/event` 加 id 映射之后,将轮次镜像也一并移除。
|
||||
步骤镜像已无消费方,由 [event-domain-semantics RFC](../architecture/2026-06-30-event-domain-semantics.md) 先行移除。该决策保留了轮次镜像供 stdio UI 使用;本 RFC 在将测试 REPL 迁移到 `session/event` 加 id 映射之后,将轮次镜像也一并移除。
|
||||
|
||||
## 范围:移除什么、不移除什么
|
||||
|
||||
本决策仅涉及持久化的轮次与步骤边界。`agent/steering` 镜像的是一条控制记录,`agent/stream-chunk` 镜像的是 token 流,因此各自单独处理:见 [steering](2026-07-04-remove-agent-steering-mirror.md) 和 [stream chunks](2026-07-02-remove-stream-chunk-mirror.md)。`agent/created`、`agent/disposed`、`agent/status`、`agent/error` 和 `agent/queued` 仍作为实时生命周期或控制事件保留,而非 transcript 镜像;排队的输入可能在任何持久化事件产生之前就被取消。
|
||||
本决策仅涉及持久化的轮次与步骤边界。`agent/steering` 镜像的是一条控制记录,`agent/stream-chunk` 镜像的是 token 流,因此各自单独处理:[steering](2026-07-04-remove-agent-steering-mirror.md) 与 [stream chunks](2026-07-02-remove-stream-chunk-mirror.md)。`agent/created`、`agent/disposed`、`agent/status`、`agent/error` 和 `agent/queued` 仍作为实时生命周期或控制事件保留,而非 transcript 镜像;排队中的输入可能在任何持久化事件产生之前就被取消。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
- **在同一个变更中移除 `agent/steering`**:否决,因为它镜像的是控制记录而非边界。
|
||||
- **为 stdio UI 保留轮次镜像**:否决,因为 UI 可以渲染 `session/event` 并从 id 映射中恢复 agent 标签。
|
||||
- **在同一个变更中一并移除 `agent/steering`**:否决,因为它是控制记录的镜像而非边界镜像。
|
||||
- **为 stdio UI 保留轮次镜像**:否决,因为 UI 可以渲染 `session/event` 并通过 id 映射恢复 agent 标签。
|
||||
|
||||
## 后果
|
||||
|
||||
插件不再能从便捷的 `Agent` 优先事件中观察轮次/步骤边界。它必须订阅 `session/event` 或自行维护 session 到 agent 的关联。这是可接受的取舍:边界消费方不应依赖一条可能与持久化日志产生漂移的第二事件源。
|
||||
插件不再能从便捷的 `Agent` 优先事件中观察轮次/步骤边界,必须订阅 `session/event` 或自行维护 session 到 agent 的关联。这是可接受的取舍:边界消费方不应依赖一条可能与持久化日志产生漂移的第二事件源。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-06-26-fsspec-style-fs-seam.md: 493af9341177aaaed4cdca03a3c20f326c5c4dac
|
||||
2026-06-26-fsspec-style-fs-seam.zh.md: ba6a366990f749c5fb84e30142965dc5a2d1d0b7
|
||||
2026-06-26-fsspec-style-fs-seam.zh.md: 4aa9b260396c22433ea9a4c9af0b4101fa6895e7
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# RFC:拆分文件系统 seam——提供方文本变更与 `dsh-fs-policy` 插件
|
||||
|
||||
Status: implemented
|
||||
# RFC:拆分文件系统 seam——提供方文本变更操作与 `dsh-fs-policy` 插件
|
||||
|
||||
[English](2026-06-26-fsspec-style-fs-seam.md) | 中文
|
||||
|
||||
Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
[filesystem-capability-seam](../../implemented/architecture/2026-06-17-filesystem-capability-seam.md) 引入的文件系统能力目前让一个抽象 `FileSystem` 服务同时承担两类职责:
|
||||
[filesystem-capability-seam](../../implemented/architecture/2026-06-17-filesystem-capability-seam.md) 中引入的文件系统能力目前让一个抽象的 `FileSystem` 服务承担两类不同的职责:
|
||||
|
||||
1. **提供方操作**——解析目标、stat/版本元数据、文本读取/流式读取、原子写入,以及带守卫的字面编辑。
|
||||
2. **面向 agent 的策略**——行窗口、字面编辑语义,以及读后写/编辑的 observed-state。
|
||||
1. **提供方操作**——解析目标、stat/版本元数据、文本读取/流式读取、原子写入,以及受保护的字面编辑。
|
||||
2. **面向 agent(智能体)的策略**——行窗口、字面编辑语义,以及读后写/编辑的观测状态。
|
||||
|
||||
这导致每个未来的后端都要重新实现面向模型的读取语义和观测策略。`readPage` 返回带行号的行和视图元数据;基类服务按 owner 存储文件状态,并区分 `full` 与 `partial` 读取。这些是有用的策略,但它们不是文件系统提供方的原语。字面文本变更则不同:版本守卫、字面匹配、歧义检测与原子重写必须在提供方变更边界内保持一体,但当前的 `applyEdit` 命名及其周围的 seam 把这个提供方操作绑定到了旧的读后编辑策略形状上。
|
||||
这导致每个未来的后端都要重新实现面向模型的读取语义和观测策略。`readPage` 返回带行号的行和视图元数据;基础服务按 owner 存储文件状态,并区分 `full` 与 `partial` 读取。这些是有用的策略,但它们不是文件系统提供方的原语。字面文本变更则不同:版本守卫、字面匹配、歧义检测与原子重写必须留在提供方的变更边界内,但当前的 `applyEdit` 命名及其周围的 seam 将这一提供方操作绑定到了旧的读后编辑策略形状上。
|
||||
|
||||
这还造成了一个真实的 UX 死胡同:窗口化读取记录 `view: partial`,而 partial 视图无法授权 `edit`。一个模型读取了大文件的第 100-150 行,除非先获得一次 `full` 读取,否则无法编辑第 120 行——而对于超过读取上限的文件,full 读取可能不可行。字面编辑真正需要的只是新鲜度:被匹配的字节必须仍来自模型所读的那个版本。
|
||||
这还造成了一个真实的用户体验死胡同:窗口化读取记录 `view: partial`,而 partial 视图无法授权 `edit`。一个模型读取了大文件的第 100-150 行,如果想编辑第 120 行,就必须先获取一次 `full` 读取,而对于超过读取上限的文件这可能做不到。字面编辑实际上只需要新鲜度:被匹配的字节仍然来自模型所读取的那个版本即可。
|
||||
|
||||
旧 RFC 已经推迟了独立的 `@deepseek-ai/dsh-fs-policy` 包。本 RFC 构建该层,并让 `ctx.fs` 贴近 fsspec 风格的存储原语(`info`/`cat`/`open`),但不将其变成完整的 fsspec。
|
||||
旧 RFC 已经推迟了独立的 `@deepseek-ai/dsh-fs-policy` 包(package)。本 RFC 构建该层,并让 `ctx.fs` 贴近 fsspec 风格的存储原语(`info`/`cat`/`open`),但不将其变成完整的 fsspec。
|
||||
|
||||
## 决策
|
||||
|
||||
@@ -28,13 +28,13 @@ provider seam dsh-fs ctx.fs: text IO + atomic mutation primitives (op
|
||||
provider dsh-fs-local local implementation of ctx.fs
|
||||
```
|
||||
|
||||
`dsh-tool-fs` 保持相同的面向模型的 `read`/`write`/`edit` schema。它是执行器:注入 `fs`(不是策略服务)并直接访问 `ctx.fs`,拥有读取窗口化逻辑,并派发 `fs/*` 事件以便 `dsh-fs-policy` 进行门控和记录。
|
||||
`dsh-tool-fs` 保持相同的面向模型的 `read`/`write`/`edit` schema。它是执行器:注入 `fs`(不是策略服务)并直接访问 `ctx.fs`,拥有读取窗口化逻辑,并分发 `fs/*` 事件以便 `dsh-fs-policy` 进行门控和记录。
|
||||
|
||||
本 RFC 决定了四层拆分、提供方契约和新鲜度策略。工具↔策略的**耦合方式**随后由[事件门控 RFC](../architecture/2026-06-26-file-context-as-event-gate.md) 细化:`dsh-fs-policy` 是一个门控**插件**,通过 `fs/*` 事件参与而非提供 `ctx.fileContext` 方法服务,因此工具不与它产生方法耦合,读取窗口化与 fs I/O 留在 `dsh-tool-fs` 中。本文描述的是最终落地的事件门控形态;提供方的版本守卫是可选的(省略 = 无条件裸提供方)。
|
||||
|
||||
## 提供方契约
|
||||
|
||||
`@deepseek-ai/dsh-fs` 收缩为提供方文本 IO 加带守卫的文本变更:
|
||||
`@deepseek-ai/dsh-fs` 收缩为提供方文本 IO 加受保护的文本变更:
|
||||
|
||||
```ts ignore-check
|
||||
abstract resolve(path: string): Promise<FsTarget>
|
||||
@@ -55,76 +55,76 @@ type FsWriteIntent =
|
||||
| { kind: 'replaceIfVersion'; version: FsVersion }
|
||||
```
|
||||
|
||||
`stat` 返回元数据而非内容。`version` 是新鲜度令牌;`type` 让执行器在读取前拒绝目录/特殊文件;`size` 让 `read` 工具无需通过失败来探测即可选择 `readText` 还是 `streamText`。返回 `undefined` 表示目标不存在。
|
||||
`stat` 返回元数据而非内容。`version` 是新鲜度令牌;`type` 让执行器在读取前拒绝目录/特殊文件;`size` 让 `read` 工具无需通过失败探测即可选择 `readText` 还是 `streamText`。`undefined` 表示目标不存在。
|
||||
|
||||
`readText` 读取整个常规文本文件。`streamText` 以相同的文本语义流式读取大文件。两个提供方原语负责常规文件检查、UTF-8 解码、二进制/NUL 拒绝以及 `FS_NOT_TEXT`;策略层从不处理原始字节,也不重新实现跨分片解码。`readText` 是小文件/直接全文件原语,而面向模型的大文件读取使用 `streamText`。
|
||||
|
||||
`writeText` 是原子性的临时文件 + rename,带有显式的写入意图。`createIfAbsent` 创建不存在的目标,对已存在的目标以 `FS_NOT_OBSERVED` 拒绝;这是 owner 没有先前读取时使用的路径。`replaceIfVersion` 仅在目标以观测到的版本存在时替换;目标不存在或版本不匹配时抛出 `FS_STALE_VERSION`。
|
||||
`writeText` 是原子的临时文件 + rename,带有显式的写入期望。`createIfAbsent` 创建不存在的目标,对已存在的目标以 `FS_NOT_OBSERVED` 拒绝;这是 owner 没有先前读取时使用的路径。`replaceIfVersion` 仅在目标以观测到的版本存在时替换;目标不存在或版本不匹配时抛出 `FS_STALE_VERSION`。
|
||||
|
||||
`editText` 是提供方级别的带守卫文本变更。启用守卫时,它先验证目标仍以 `expected.version` 存在,然后读取当前文本、应用字面替换并原子写入。陈旧检查必须在字面匹配之前发生,这样基于旧读取的编辑会报告 `FS_STALE_VERSION`,而不是对更新内容做匹配后报告 `FS_EDIT_NOT_FOUND` 或 `FS_AMBIGUOUS_EDIT`。将此原语保留在提供方 seam 上,保持了后端本地锁定能力,也让未来的远程后端可以实现原生的 compare-and-edit 而无需策略层拉取整个文件。
|
||||
`editText` 是提供方级别的受保护文本变更。启用守卫时,它首先验证目标仍以 `expected.version` 存在,然后读取当前文本、应用字面替换并原子写入。过期检查必须在字面匹配之前发生,这样基于旧读取的编辑会报告 `FS_STALE_VERSION`,而不是对更新内容进行匹配后报告 `FS_EDIT_NOT_FOUND` 或 `FS_AMBIGUOUS_EDIT`。将此原语保留在提供方 seam 上,保持了后端本地锁定的能力,也让未来的远程后端能够实现原生的 compare-and-edit,而无需策略层拉取整个文件。
|
||||
|
||||
这是一个*文本存储* seam,刻意比字节级 fsspec(`cat`/`open` 返回原始字节)高半层。UTF-8 解码、二进制/NUL 拒绝、带守卫的全文件写入和带守卫的字面文本编辑都在提供方内完成,使策略层从不接触原始字节、不重新实现跨分片解码、也不将陈旧检查与变更临界区分离。面向模型的概念仍然不下沉到提供方:行窗口、带行号的行、渲染的页脚、observed-state 存储都不会泄漏下去。
|
||||
这是一个*文本存储* seam,刻意比字节级 fsspec(`cat`/`open` 返回原始字节)高半个层次。UTF-8 解码、二进制/NUL 拒绝、受保护的全文件写入和受保护的字面文本编辑都在提供方内完成,因此策略层从不接触原始字节、不重新实现跨分片解码、也不将过期检查与变更临界区分离。面向模型的概念仍然不下沉到提供方:行窗口、带行号的行、渲染的页脚、观测状态存储都不会泄漏下去。
|
||||
|
||||
从 `dsh-fs` 中删除:`readPage`、`FsExpectation`、`FsView`、`FsStateSource`、`FsReadRequest`、`FsTextLine`、行/窗口常量、`formatReadBody`,以及 observed-state `WeakMap`。`applyEdit` 被更窄的提供方原语 `editText` 取代,后者的契约是版本守卫的字面文本变更,而非策略层的读取授权。`FS_PARTIAL_OBSERVATION` 错误码也从 `FsErrorCode` 分类体系中移除:新鲜度授权没有 partial/full 之分,因此没有任何场景会抛出它。`FsTargetKey` 和 `FsVersion` 按照既有的 [branded-ids RFC](../../implemented/architecture/2026-06-20-branded-ids.md) 成为品牌化的不透明 id。
|
||||
从 `dsh-fs` 中删除的内容:`readPage`、`FsExpectation`、`FsView`、`FsStateSource`、`FsReadRequest`、`FsTextLine`、行/窗口常量、`formatReadBody`,以及观测状态 `WeakMap`。`applyEdit` 被更窄的提供方原语 `editText` 取代,后者的契约是版本守卫的字面文本变更,而非策略层的读取授权。`FS_PARTIAL_OBSERVATION` 错误码也从 `FsErrorCode` 分类体系中移除:新鲜度授权没有 partial/full 之分,因此没有什么能触发它。`FsTargetKey` 和 `FsVersion` 按照既有的 [branded-ids RFC](../../implemented/architecture/2026-06-20-branded-ids.md) 成为品牌化的不透明 id。
|
||||
|
||||
## 策略契约
|
||||
|
||||
`@deepseek-ai/dsh-fs-policy` 是一个插件而非服务:它不注册任何 `ctx.*` 键,也不注入任何东西。它拥有写入/编辑新鲜度策略和 observed-state——这些不属于 `FileSystem` 提供方基类(否则沙箱化/远程后端会继承它无需承担的面向模型的观测策略)。它通过执行器派发的 `fs/*` 事件门控来贡献这些策略。(本 RFC 最初提出了一个具体的 `ctx.fileContext` 方法服务,带 `read`/`write`/`edit` 方法;[事件门控 RFC](../architecture/2026-06-26-file-context-as-event-gate.md) 将其细化为此处描述的门控插件,使工具从不与策略产生方法耦合。)
|
||||
`@deepseek-ai/dsh-fs-policy` 是一个插件,不是服务:它不注册任何 `ctx.*` 键,也不注入任何东西。它拥有写入/编辑新鲜度策略和观测状态,这些不属于 `FileSystem` 提供方基类(否则沙箱/远程后端会继承它无需承载的面向模型的观测策略)。它通过执行器分发的 `fs/*` 事件门控贡献该策略。(本 RFC 最初提出了一个具体的 `ctx.fileContext` 方法服务,带有 `read`/`write`/`edit` 方法;[事件门控 RFC](../architecture/2026-06-26-file-context-as-event-gate.md) 将其改造为此处描述的门控插件,使工具从不与策略产生方法耦合。)
|
||||
|
||||
Observed state 以 `WeakMap<owner, Map<targetKey, FsVersion>>` 形式存在于此。当且仅当 owner 读取、写入或编辑过该目标时条目才存在(每次成功都会发出 `fs/observed`),因此条目的存在*本身就是*先前观测记录——没有单独的 `hasRead` 标志。owner 从不透明的事件 actor(`{ agent?: { session? } }`)结构化派生,该形状定义在 `dsh-fs-policy` 中而非 `dsh-fs` 中。
|
||||
观测状态以 `WeakMap<owner, Map<targetKey, FsVersion>>` 的形式存放于此。当且仅当 owner 读取、写入或编辑过该目标时,条目才存在(每次成功都会发出 `fs/observed`),因此条目的存在*本身就是*先前观测的记录——没有单独的 `hasRead` 标志。owner 从不透明的事件 actor(`{ agent?: { session? } }`)结构化派生,该形状定义在 `dsh-fs-policy` 中而非 `dsh-fs` 中。
|
||||
|
||||
该插件决定三个 `fs/*` 事件:
|
||||
|
||||
- `fs/write-intent`——无先前观测 ⇒ `{ kind: 'createIfAbsent' }`(只有新文件可以盲创建);有先前观测 ⇒ `{ kind: 'replaceIfVersion', version: vObserved }`(已有文件仅在自观测以来未变时才替换)。单槽决策;不调用 `next()`。
|
||||
- `fs/edit-intent`——要求 owner 有先前观测(否则 `FS_NOT_OBSERVED`);返回 `{ version: vObserved }` 作为 CAS 基础。它不实现字面替换——它授权并提供版本,提供方的变更临界区负责应用守卫,因此基于同一观测版本的并发编辑仍然是一个赢/一个陈旧。
|
||||
- `fs/observed`——在成功的读取/写入/编辑后为该 owner+target 记录 `{ version }`。同步、仅副作用的 `WeakMap.set`。
|
||||
- `fs/edit-intent`——要求 owner 有先前观测(否则 `FS_NOT_OBSERVED`);返回 `{ version: vObserved }` 作为 CAS 基础。它不实现字面替换——它授权并提供版本,提供方的变更临界区负责应用守卫,因此基于同一观测版本的并发编辑仍然是一赢一过期。
|
||||
- `fs/observed`——在成功的读取/写入/编辑后,为该 owner+target 记录 `{ version }`。同步、仅副作用的 `WeakMap.set`。
|
||||
|
||||
该插件不做任何文件系统 I/O:「你是否观测过这个文件?」是一次 `WeakMap` 查找,而「你读到的版本是否仍然是当前版本?」在 `ctx.fs.editText`/`writeText` 内部的同一原子锁中决定(该锁同时执行变更)——插件只提供 `vObserved` 作为基础。
|
||||
该插件不做任何文件系统 I/O:「你是否观测过此文件?」是一次 `WeakMap` 查找,而「你读取的版本是否仍然是当前版本?」在 `ctx.fs.editText`/`writeText` 内部、与执行变更相同的原子锁中决定——插件只提供 `vObserved` 作为基础。
|
||||
|
||||
## 工具契约
|
||||
|
||||
`dsh-tool-fs` 保持相同的 schema 和提示词表面。`read` 仍暴露 `file_path`、`offset` 和 `limit`;`write` 和 `edit` 不变。它是执行器:验证模型参数,通过 `ctx.fs` 直接读取/写入/编辑,拥有行窗口化和结果渲染(`N: text`、页脚、`<path>/<content>` 信封),并派发 `fs/*` 事件。
|
||||
`dsh-tool-fs` 保持相同的 schema 和提示词表面。`read` 仍然暴露 `file_path`、`offset` 和 `limit`;`write` 和 `edit` 不变。它是执行器:验证模型参数,通过 `ctx.fs` 直接读取/写入/编辑,拥有行窗口化和结果渲染(`N: text`、页脚、`<path>/<content>` 信封),并分发 `fs/*` 事件。
|
||||
|
||||
每次变更先派发其 intent waterfall(瀑布式事件)并以 `undefined` 作为裸提供方默认值,然后调用 `ctx.fs`,再发出 `fs/observed`:例如 `write` 执行 `ctx.waterfall('fs/write-intent', target, exec, () => undefined)` → `ctx.fs.writeText(target, content, intent)` → `ctx.emit('fs/observed', …)`。`read` 做一次 stat、读取/流式读取、构建窗口,然后发出 `fs/observed`。将 `exec` 作为 actor 传入,让 `dsh-fs-policy` 无需工具深入策略即可派生 owner。
|
||||
每个变更操作先分发其 intent waterfall(瀑布式事件),带有 `undefined` 裸提供方默认值,然后调用 `ctx.fs`,再发出 `fs/observed`。例如 `write` 执行 `ctx.waterfall('fs/write-intent', target, exec, () => undefined)` → `ctx.fs.writeText(target, content, intent)` → `ctx.emit('fs/observed', …)`。`read` 先 stat 一次,然后读取/流式读取,构建窗口,最后发出 `fs/observed`。将 `exec` 作为 actor 传递,让 `dsh-fs-policy` 无需工具深入策略即可派生 owner。
|
||||
|
||||
由于策略通过带 `undefined` 默认值的事件贡献,`dsh-tool-fs` 不与 `dsh-fs-policy` 产生方法耦合:插件不存在时,每个 intent waterfall 落入 `undefined`(无条件裸提供方写入/编辑),`fs/observed` 无监听者。加载插件后即叠加读后写/编辑策略。
|
||||
由于策略通过带有 `undefined` 默认值的事件贡献,`dsh-tool-fs` 不与 `dsh-fs-policy` 产生方法耦合:在插件缺席时,每个 intent waterfall 都落到 `undefined`(无条件裸提供方写入/编辑),`fs/observed` 没有监听器。加载插件后即可叠加读后写/编辑策略。
|
||||
|
||||
## 并发边界
|
||||
|
||||
进程内更新是安全的:本地后端保持既有的按目标变更锁,因此版本检查-然后-rename 是串行化的,失败的更新看到 `FS_STALE_VERSION`。
|
||||
进程内更新是安全的:本地后端保持既有的按目标变更锁,因此版本检查-然后-rename 是串行化的,失败的更新会看到 `FS_STALE_VERSION`。
|
||||
|
||||
进程内创建由同一按目标变更锁守卫:两个调用者以 `createIfAbsent` 竞争时串行化,一个创建成功,下一个看到目标已存在并收到 `FS_NOT_OBSERVED`。跨进程创建仅尽力而为;本地的 stat-then-rename 守卫无法在所有未来后端上提供可移植的排他创建保证。
|
||||
进程内创建由同一个按目标变更锁保护:两个调用者以 `createIfAbsent` 竞争时串行化,一个创建成功,另一个看到目标已存在并收到 `FS_NOT_OBSERVED`。跨进程创建仅为尽力而为;本地的 stat-then-rename 守卫无法在所有未来后端上提供可移植的排他创建保证。
|
||||
|
||||
跨进程写入是尽力新鲜度加原子替换:`mtime:size` 通常能捕获编辑器保存,但同一时刻相同大小的写入可能遗漏;原子性的 temp+rename 防止文件撕裂但不能防止所有丢失更新。
|
||||
跨进程写入是尽力而为的新鲜度加原子替换:`mtime:size` 通常能捕获编辑器保存,但同一 tick 相同大小的写入可能遗漏;原子的 temp+rename 防止文件撕裂但不能防止所有丢失更新。
|
||||
|
||||
## 取代
|
||||
|
||||
本 RFC 逆转了 [filesystem-capability-seam](../../implemented/architecture/2026-06-17-filesystem-capability-seam.md) 的两项决策,并收窄了第三项:
|
||||
本 RFC 逆转了 [filesystem-capability-seam](../../implemented/architecture/2026-06-17-filesystem-capability-seam.md) 中的两项决策,并收窄了第三项:
|
||||
|
||||
- 读后写/编辑策略从 `ctx.fs` 移出,进入 `dsh-fs-policy` 插件(在 `fs/*` 事件门控上)。
|
||||
- 读后写/编辑策略从 `ctx.fs` 移出,进入 `dsh-fs-policy` 插件(通过 `fs/*` 事件门控)。
|
||||
- 文本读取不再返回后端编号的行记录或 `full`/`partial` 视图;授权基于版本新鲜度,因此窗口化读取在文件未变时即可授权编辑。
|
||||
- 字面编辑不再位于旧的 `applyEdit` API 之后(该 API 混合了后端变更与 seam 拥有的观测策略)。它作为 `editText` 保留为提供方原语,因为版本守卫 + 字面匹配 + 原子重写必须在提供方的变更临界区内保持一体,以确保正确的错误归因和并发行为。
|
||||
- 字面编辑不再位于旧的 `applyEdit` API 之后(该 API 混合了后端变更与 seam 拥有的观测策略)。它作为 `editText` 保留为提供方原语,因为版本守卫 + 字面匹配 + 原子重写必须留在提供方的变更临界区内。
|
||||
|
||||
保留的内容:接口/实现/消费方纪律、消费方不导入后端规则、后端定义的 target/version/display 元数据、原子本地写入,以及共享的 `FsError` 分类体系。
|
||||
|
||||
## 验证
|
||||
|
||||
`dsh-fs` 精确暴露 `resolve`/`stat`/`readText`/`streamText`/`writeText`/`editText`(`stat` 返回 `FsInfo | undefined`,`writeText` 接受 `FsWriteIntent`),已删除的类型/原语不再存在;`dsh-fs-local` 不携带行、视图或 `formatReadBody` 逻辑;面向模型的 schema 逐字节未变。测试固定了以下行为:窗口化读取可以授权对未变文件的后续编辑;基于陈旧读取的编辑在尝试字面匹配之前报告 `FS_STALE_VERSION`;版本 CAS 行为得到保持;观测契约成立(通过 `read` 工具的读取记录 observed-state;直接的 `ctx.fs` 读取不记录);`dsh-fs-policy` 具有 HMR/dispose 覆盖率。
|
||||
`dsh-fs` 精确暴露 `resolve`/`stat`/`readText`/`streamText`/`writeText`/`editText`(`stat` 返回 `FsInfo | undefined`,`writeText` 接受 `FsWriteIntent`),已删除的类型/原语不再存在;`dsh-fs-local` 不包含行、视图或 `formatReadBody` 逻辑;面向模型的 schema 保持逐字节不变。测试固定了以下行为:窗口化读取授权对未变文件的后续编辑;基于过期读取的编辑在尝试字面匹配之前报告 `FS_STALE_VERSION`;版本 CAS 行为得以保留;观测契约成立(`read` 工具的读取记录观测状态;直接 `ctx.fs` 读取不记录);`dsh-fs-policy` 具有 HMR(热模块替换)/dispose(资源释放)覆盖率。
|
||||
|
||||
## 后续扩展
|
||||
|
||||
该 seam 后来由 [Add direct directory listing to the filesystem seam](../architecture/2026-07-03-filesystem-directory-listing-seam.md) 扩展了直接目录列表功能。该后续工作单独跟踪,以使本 RFC 的验收标准继续描述最初交付的 fsspec 风格改造。
|
||||
该 seam 后来由 [Add direct directory listing to the filesystem seam](../architecture/2026-07-03-filesystem-directory-listing-seam.md) 扩展了直接目录列表功能。该后续工作单独跟踪,以使本 RFC 的验收标准继续描述最初交付的 fsspec 风格重构。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
- **字节级 fsspec(`cat`/`open` 返回原始字节)**——否决:该 seam 刻意定位为文本存储,比字节级高半层,使 UTF-8 解码、二进制/NUL 拒绝和带守卫的文本变更在提供方内只实现一次,策略层从不接触原始字节,也不将陈旧检查与变更临界区分离。
|
||||
- **具体的 `ctx.fileContext` 方法服务**——本 RFC 最初的策略形态;由[事件门控 RFC](../architecture/2026-06-26-file-context-as-event-gate.md) 改造为门控插件,使工具从不与策略产生方法耦合。
|
||||
- **将 `readPage` 和 `full`/`partial` 视图授权保留在提供方上**——改造前的形态,即「取代」一节所逆转的内容:视图完整性不是编辑安全所需的信号,版本新鲜度才是;视图规则使超过读取上限的大文件无法编辑。
|
||||
- **字节级 fsspec(`cat`/`open` 返回原始字节)**:否决。该 seam 刻意定位为文本存储,比字节级高半个层次,这样 UTF-8 解码、二进制/NUL 拒绝和受保护的文本变更只在提供方实现一次,策略层从不接触原始字节,也不将过期检查与变更临界区分离。
|
||||
- **具体的 `ctx.fileContext` 方法服务**:本 RFC 最初的策略形态;被[事件门控 RFC](../architecture/2026-06-26-file-context-as-event-gate.md) 改造为门控插件,使工具从不与策略产生方法耦合。
|
||||
- **在提供方保留 `readPage` 和 `full`/`partial` 视图授权**:「取代」一节所逆转的重构前形态。视图完整性不是编辑安全所需的,版本新鲜度才是;而视图规则使超过读取上限的大文件无法编辑。
|
||||
|
||||
## 后果
|
||||
|
||||
- 新增第四个 fs 包和一个新的插件层。这是有意为之:它是此前推迟的策略层,而非第二个抽象后端 seam。
|
||||
- 直接使用 `ctx.fs` 会绕过策略:直接的 `ctx.fs.readText` 不发出 `fs/observed`,因此在默认策略下,后续的 `edit` 会以 `FS_NOT_OBSERVED` 拒绝,直到通过 `read` 工具读取该文件。该失败是显式且有文档记录的。
|
||||
- 大文件行窗口化从后端移至 `dsh-tool-fs` 中的 `read` 工具;文本解码和二进制拒绝留在 `ctx.fs.streamText` 中,因此这只是窗口化逻辑的迁移,不是第二套文本 IO 实现。
|
||||
- 将 `editText` 保留在提供方 seam 上意味着每个后端都必须实现字面替换契约。这是有意为之:该操作不是纯存储,但陈旧守卫 + 字面匹配 + 原子重写是必须保持一体的单元,以确保正确的错误归因和并发行为。该契约应保持窄且仅限文本,以便未来后端可以原生实现或通过全文件重写实现。
|
||||
- 新鲜度允许在窗口化读取后执行全文件 `write`。这比旧的视图检查更弱,但避免了大文件无法编辑的问题;提示词引导仍然不鼓励盲目的全文件替换。
|
||||
- 直接使用 `ctx.fs` 会绕过策略:直接 `ctx.fs.readText` 不发出 `fs/observed`,因此在默认策略下,后续 `edit` 会以 `FS_NOT_OBSERVED` 拒绝,直到通过 `read` 工具读取该文件。这一失败是显式且有文档记录的。
|
||||
- 大文件行窗口化从后端移至 `dsh-tool-fs` 中的 `read` 工具;文本解码和二进制拒绝留在 `ctx.fs.streamText` 中,因此这只是窗口化逻辑的迁移,而非第二套文本 IO 实现。
|
||||
- 将 `editText` 保留在提供方 seam 上意味着每个后端都必须实现字面替换契约。这是有意为之:该操作不是纯存储,但过期守卫 + 字面匹配 + 原子重写是必须保持在一起的单元,以确保正确的错误归因和并发行为。该契约应保持窄且仅限文本,以便未来后端可以原生实现或通过全文件重写实现。
|
||||
- 新鲜度允许在窗口化读取后进行全文件 `write`。这比旧的视图检查更弱,但避免了大文件无法编辑的问题;提示词引导仍然不鼓励盲目的全文件替换。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-02-remove-stream-chunk-mirror.md: 1ec633b09c8e53ae7145a49061aced191a9aa765
|
||||
2026-07-02-remove-stream-chunk-mirror.zh.md: 83674658d24621b12a866262bb58dde166bedf2d
|
||||
2026-07-02-remove-stream-chunk-mirror.zh.md: 7cf8a5d056c1bb4193263c58a8e4258173fe8b4e
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# RFC:停止将 token 流镜像为 agent 事件
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-02-remove-stream-chunk-mirror.md) | 中文
|
||||
|
||||
Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
agent loop(智能体循环)将模型的每个 token 增量同时记录为持久的 `assistant/chunk` 会话事件,并发射一个携带相同数据的并行实时 `agent/stream-chunk` Cordis 事件。在 `packages/core/agent-loop/src/loop.ts` 中,两者仅相隔一行:
|
||||
agent loop(智能体循环)将模型的每个 token delta 同时记录为持久的 `assistant/chunk` 会话事件,并发射一个携带相同数据的并行实时 `agent/stream-chunk` Cordis 事件。在 `packages/core/agent-loop/src/loop.ts` 中,二者仅相隔一行:
|
||||
|
||||
```ts ignore-check
|
||||
const chunkEvent = session.append('assistant/chunk', { turn, step, chunk })
|
||||
@@ -17,17 +17,17 @@ ctx.emit('agent/stream-chunk', agent, turn, step, chunk) // ← the mirror
|
||||
- 持久事件:`assistant/chunk: { turn, step, chunk }`。
|
||||
- 实时发射:`agent/stream-chunk(agent, turn, step, chunk)`——相同的 `StreamChunk`,相同的 `turn`/`step`。
|
||||
|
||||
实时发射相比会话事件唯一多出的东西是实时的 `Agent` 句柄,而唯一的消费方丢弃了它(其处理函数签名为 `(_agent, _turn, _step, chunk)`)。
|
||||
实时发射相比会话事件唯一多出的东西是实时的 `Agent` 句柄,而唯一的消费方直接丢弃了它(其处理函数签名为 `(_agent, _turn, _step, chunk)`)。
|
||||
|
||||
这与[边界镜像移除](2026-06-20-remove-agent-boundary-mirror-events.md)为轮次/步骤边界消除的重复如出一辙:消费方对同一个持久事实有两个真源,每次修改都必须同时触及两处。那份 RFC 将分片流推迟处理(「`assistant/chunk` 的持久化仍然是承重的,因此分片流后续可以作为镜像来评估,但那是一个独立的决策」),而非一并打包。本 RFC 就是那个独立的决策。
|
||||
这与[边界镜像移除](2026-06-20-remove-agent-boundary-mirror-events.md)为 turn/step 边界消除的重复如出一辙:消费方对同一个持久事实有两个真源,每次变更都要同时修改两处。那份 RFC 将 chunk 流推迟处理(「`assistant/chunk` 的持久化仍然是承重的,因此 chunk 流后续可以作为镜像来评估,但那是一个独立决策」),而非一并纳入。本 RFC 即是那个独立决策。
|
||||
|
||||
推迟所依赖的前提已经尘埃落定:分片持久化是权威的,且将保留。停止持久化分片、仅保留瞬态实时流事件的提案已被[否决](../../rejected/simplification/2026-06-20-assembled-assistant-messages-only.md)——高保真回放、部分失败的流以及快照回放都依赖持久化的 `assistant/chunk` 流。因此 `session/event` 上的 `assistant/chunk` 是持久的、承重的 token 流,而 `agent/stream-chunk` 是它的纯冗余镜像。
|
||||
推迟所依赖的前提已经明确:chunk 持久化是权威的,且将保留。停止持久化 chunk、仅保留瞬态实时流事件的提案已被[否决](../../rejected/simplification/2026-06-20-assembled-assistant-messages-only.md)——高保真回放、部分失败的流以及快照回放都依赖持久化的 `assistant/chunk` 序列。因此 `session/event` 上的 `assistant/chunk` 是持久的、承重的 token 流,而 `agent/stream-chunk` 是它的纯冗余镜像。
|
||||
|
||||
## 决策
|
||||
|
||||
从 agent 事件分类体系中移除 `agent/stream-chunk`。token 流通过 `session/event` 以 `assistant/chunk` 的形式读取——持久化和回放已经使用的正是同一条流。`session/event` 是唯一的实时 transcript(文本记录)流(assistant 分片、轮次/步骤边界、工具活动、todo)。
|
||||
从 agent 事件分类体系中移除 `agent/stream-chunk`。token 流通过 `session/event` 以 `assistant/chunk` 的形式读取——持久化与回放已经使用的正是同一个序列。`session/event` 是唯一的实时 transcript(文本记录)流(assistant chunk、turn/step 边界、工具活动、todo)。
|
||||
|
||||
**消费方。** 唯一重要的生产消费方——ACP 桥接层(`dsh-acp`,真正面向编辑器的流式输出接口)——已经从 `session/event` 渲染 `assistant/chunk`,从未使用 `agent/stream-chunk`,因此不受影响。stdio UI(`dsh-ui-stdio`,一个一次性的测试 REPL)是唯一的实时消费方;它在边界迁移时已经有了 `session/event` 监听器,因此其分片渲染被折叠进该监听器的 `assistant/chunk` 分支。合并为一个监听器还消除了一个潜在隐患:`inReasoning` dim-SGR 标志此前在两个独立的监听器(`agent/stream-chunk` 和 `session/event`)之间共享,分片与边界在该标志上竞争时没有确定的顺序;单一监听器按追加顺序处理,使交错变为确定性的。
|
||||
**消费方。** 唯一重要的生产消费方——ACP 桥接(`dsh-acp`,面向编辑器的真实流式输出接口)——已经从 `session/event` 渲染 `assistant/chunk`,从未使用 `agent/stream-chunk`,因此不受影响。stdio UI(`dsh-ui-stdio`,一个一次性的测试 REPL)是唯一的实时消费方;它在边界迁移时已经有了 `session/event` 监听器,因此其 chunk 渲染被折叠进该监听器作为 `assistant/chunk` 分支。合并为一个监听器还消除了一个潜在隐患:`inReasoning` dim-SGR 标志此前在两个独立监听器(`agent/stream-chunk` 和 `session/event`)之间共享,chunk 与边界在该标志上竞争时没有确定的顺序;单一监听器按追加顺序处理,使交错变为确定性的。
|
||||
|
||||
## 范围
|
||||
|
||||
@@ -35,13 +35,13 @@ ctx.emit('agent/stream-chunk', agent, turn, step, chunk) // ← the mirror
|
||||
|
||||
未触及:
|
||||
- `assistant/chunk`(持久会话事件)——权威的 token 流,原样保留。本 RFC 移除的是实时镜像,而非持久化(持久化移除提案已被单独否决,见上文)。
|
||||
- `agent/steering`——本决策未触及(它是控制信号,不是 token 流)。其持久孪生事件是 `steering/message`,镜像发射由其自己的后续 RFC 移除:[移除 `agent/steering` 镜像发射](2026-07-04-remove-agent-steering-mirror.md)。
|
||||
- `agent/steering`——本决策未触及(它是控制信号,不是 token 流)。其持久孪生事件是 `steering/message`,镜像发射由其自身的后续 RFC 移除:[移除 `agent/steering` 镜像发射](2026-07-04-remove-agent-steering-mirror.md)。
|
||||
- `agent/status`、`agent/error`、`agent/created`/`agent/disposed`、`agent/queued`、`agent/session-start`——生命周期/控制事件,不是 transcript 数据,也没有持久副本。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
**移除持久化、仅保留瞬态实时流**——反向裁剪,已被[单独否决](../../rejected/simplification/2026-06-20-assembled-assistant-messages-only.md):高保真回放、部分失败的流以及快照回放都依赖持久化的 `assistant/chunk` 流。这一点既已确定,实时发射就是配对中冗余的那一半。
|
||||
**移除持久化、仅保留瞬态实时流**——反向裁剪,已被[单独否决](../../rejected/simplification/2026-06-20-assembled-assistant-messages-only.md):高保真回放、部分失败的流以及快照回放都依赖持久化的 `assistant/chunk` 序列。在此前提确定后,实时发射才是配对中冗余的那一半。
|
||||
|
||||
## 后果
|
||||
|
||||
插件不再能从以 `Agent` 为首参的事件观察 token 增量。它应订阅 `session/event` 并过滤 `assistant/chunk`(如需 `Agent` 句柄,可从 `agent/created`/`agent/disposed` 构建的 session-id→agent 映射中恢复,与边界消费方的做法完全一致)。没有任何生产消费方在分片时需要实时的 `Agent`;这与边界镜像移除所做的权衡完全相同,是可接受的。
|
||||
插件不再能通过以 `Agent` 为首参的事件观察 token delta。它需要订阅 `session/event` 并过滤 `assistant/chunk`(如需 `Agent` 句柄,可通过 `agent/created`/`agent/disposed` 构建的 session-id→agent 映射恢复,与边界消费方已有的做法完全一致)。没有任何生产消费方在 chunk 时需要实时的 `Agent`;这与边界镜像移除所做的权衡相同,是可接受的。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-04-drop-image-content-block.md: 8222880b61c225c39a1e132353c5f343fb90cb4b
|
||||
2026-07-04-drop-image-content-block.zh.md: d1379d69a0c5056cdfcc744182cd9b6c52f222d5
|
||||
2026-07-04-drop-image-content-block.zh.md: cb9372e50863193cd579c0bf8de991810db95206
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
# RFC:移除 `image` 内容块,直到有路径能真正处理它
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-04-drop-image-content-block.md) | 中文
|
||||
|
||||
Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
`ImageBlock`(`packages/llm/llm/src/types.ts`)没有任何生产环境的生产者,而每条路径上的每个消费方都将其**丢弃**:DeepSeek 适配器的序列化器跳过 image 块(这是文档中注明的 MVP 限制);pi-ai 转换器因无法表示而跳过;ACP 编解码器既不声明 image prompt 能力、也不向外转发 image 块,并且对入站的 image prompt 内容直接**拒绝**;压缩(compaction)估算器对其收取一个固定 token 常量并渲染为 `[image]`。此时构造的 `ImageBlock` 会在协议格式(wire format)上静默消失——词汇表声明了一种没有任何路径兑现的能力,这正是 `AGENTS.md` 防御性模式所警告的静默数据丢失形态。唯一的构造点是用于固定 skip/drop/estimate 分支的测试。
|
||||
`ImageBlock`(`packages/llm/llm/src/types.ts`)没有任何生产环境的生产者,而每条路径上的每个消费方都将其**丢弃**:deepseek 适配器的序列化器跳过 image 块(这是文档中注明的 MVP 限制);pi-ai 转换器因无法表示而跳过;ACP 编解码器既不宣告 image prompt 能力、也不向外转发 image 块,并且会拒绝入站的 image prompt 内容;压缩(compaction)估算器对其收取一个固定 token 常量并渲染为 `[image]`。此时构造的 `ImageBlock` 会在协议格式(wire format)上静默消失——词汇宣告了一种没有任何路径兑现的能力,这正是 AGENTS.md 防御性模式所警告的静默数据丢失形态。唯一的构造调用出现在测试中,用于覆盖 skip/drop/estimate 分支。
|
||||
|
||||
## 决策
|
||||
|
||||
移除 `ImageBlock`、其 map 条目,以及适配器、ACP 渲染和压缩中的 image 专用分支。在同一个变更中更新所属词汇文档与生成的引用。未知的扩展块仍然覆盖 default 分支,ACP 继续独立于 harness 词汇拒绝入站 image prompt 内容。
|
||||
移除 `ImageBlock`、其 map 条目,以及适配器、ACP 渲染和压缩中的 image 专用分支。在同一个变更中更新所属的词汇文档与生成的引用。未知扩展块仍然覆盖默认分支,ACP 继续独立于 harness 词汇拒绝入站的 image prompt 内容。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
### 为什么不保留?
|
||||
|
||||
当适配器、ACP 与压缩全部支持 image 时,`ContentBlockMap` 可以重新引入它。保留一个唯一实现是拒绝的核心类型,等于向外声明一个不可用的接口;移除则让生产者在编译期立即失败。
|
||||
当适配器、ACP 和压缩全部支持 image 时,`ContentBlockMap` 可以重新引入。保留一个唯一实现就是拒绝的核心类型,等于宣告一个不可用的对外服务接口;移除后,生产者会立即得到编译期错误。
|
||||
|
||||
记录在案的回退方案(假设评审决定保留该槽位):保留 `ImageBlock`,但将每处静默跳过替换为显式拒绝,并在词汇文档中记录该策略——静默丢弃是唯一没有辩护者的状态。评审最终决定移除;此回退方案作为文档化的替代方案保留,以备该槽位在完整功能之前回归。
|
||||
评审中记录的回退方案(假如评审决定保留该槽位):保留 `ImageBlock`,但将所有静默跳过替换为显式拒绝,并在词汇文档中记录该策略——静默丢弃是唯一没有辩护者的状态。评审最终决定移除;此回退方案作为文档化的替代方案保留,以备该槽位在完整功能就绪之前回归。
|
||||
|
||||
## 验证
|
||||
|
||||
RFC 记录之外没有任何地方构造 harness `ImageBlock`。ACP 独立的入站 image 拒绝仍有测试覆盖,而适配器、编解码器与压缩的 default 分支则通过插件定义的块类型覆盖。
|
||||
RFC 记录之外没有任何地方构造 harness 的 `ImageBlock`。ACP 独立的入站 image 拒绝仍有测试覆盖,适配器、编解码器和压缩的默认分支则通过插件定义的块类型来覆盖。
|
||||
|
||||
## 后果
|
||||
|
||||
日后重新添加核心词汇类型会同时涉及多个包——但这种协调变更正是真正的多模态功能所需的形态(适配器映射、ACP 能力声明、压缩定价),而当前并没有什么需要保留的实现。
|
||||
日后重新添加核心词汇类型需要同时改动多个包(package)——但这种协调变更本就是真正的多模态功能所需的形态(适配器映射、ACP 能力宣告、压缩定价),而当前并不存在需要保留的实现。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-04-drop-inert-request-knobs.md: d5484aa5ec64f89ce705ddd0a434c2c4dfbad460
|
||||
2026-07-04-drop-inert-request-knobs.zh.md: 877b073c4693f0c87b5f25003a1d043743b658fa
|
||||
2026-07-04-drop-inert-request-knobs.zh.md: 9bd13cd024bc0e2ba7795a190d77063c3457fe98
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# RFC:移除 `GenerateOptions.prefill` 与 `ToolSchema.strict`——无可用端到端路径的请求旋钮
|
||||
# RFC:移除 `GenerateOptions.prefill` 与 `ToolSchema.strict`——无端到端可用路径的请求旋钮
|
||||
|
||||
[English](2026-07-04-drop-inert-request-knobs.md) | 中文
|
||||
|
||||
@@ -6,30 +6,30 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
两个请求契约旋钮贯穿了整条请求流水线,但都无法产生任何效果:
|
||||
两个请求契约旋钮贯穿了整条请求流水线,却都无法产生任何效果:
|
||||
|
||||
- **`prefill`**(`packages/llm/llm/src/types.ts`)没有生产环境的赋值方:agent loop(智能体循环)组装的只有 `model`/`system`/`tools`/`messages` 加 `sessionId`/`signal`,上下文压缩(context compaction)后端只追加 `maxTokens`;而且**两个**适配器都拒绝它:`packages/llm/llm-deepseek/src/serialize.ts` 和 `packages/llm/llm-pi-ai/src/adapter.ts` 各自在 `prefill` 非 undefined 时抛出 `LlmError('UNSUPPORTED')`。该字段全部可观测行为就是两个 throw,各由一个适配器测试固定。DeepSeek 的 chat-prefix completion 是一个 Beta 功能,使用的 base URL 两个适配器都未指向。
|
||||
- **`strict`**(`ToolSchema`,同一文件)贯穿了 `DefineToolOptions`/`defineTool`(`packages/core/tools/src/schema.ts`)、注册表的 `schemas()` 白名单(`packages/core/tools/src/index.ts`)、deepseek 协议格式(wire format)映射(`packages/llm/llm-deepseek/src/serialize.ts`,其 wire-type 注释记录了 strict 模式需要适配器未使用的 `/beta` base URL)、`packages/llm/llm-pi-ai/src/adapter.ts` 中的逐工具 payload 修补,以及 tool-catalog 渲染器(`scripts/gen-tool-catalog.ts`)中的条件 `Strict:` 行。没有任何已发布的工具设置过它:在所有 `tool-*` 包 src 和 `examples/` 中 `rg` 搜索,`strict:` 的生产方为零;唯一的赋值方是 dsh-tools 单元测试。
|
||||
- **`prefill`**(`packages/llm/llm/src/types.ts`)没有生产级的 setter:agent loop(智能体循环)组装的是 `model`/`system`/`tools`/`messages` 加 `sessionId`/`signal`,上下文压缩(context compaction)后端只追加 `maxTokens`;而且**两个**适配器都拒绝它:`packages/llm/llm-deepseek/src/serialize.ts` 和 `packages/llm/llm-pi-ai/src/adapter.ts` 各自在 `prefill` 非 undefined 时抛出 `LlmError('UNSUPPORTED')`。该字段的全部可观测行为就是两个 throw,各由一条适配器测试固定。DeepSeek 的 chat-prefix completion 是一个 Beta 功能,运行在两个适配器都未指向的 base URL 上。
|
||||
- **`strict`**(`ToolSchema`,同一文件)穿过了 `DefineToolOptions`/`defineTool`(`packages/core/tools/src/schema.ts`)、注册表的 `schemas()` 允许列表(`packages/core/tools/src/index.ts`)、deepseek 协议格式(wire format)映射(`packages/llm/llm-deepseek/src/serialize.ts`,其 wire-type 注释记录了 strict 模式需要适配器未使用的 `/beta` base URL)、`packages/llm/llm-pi-ai/src/adapter.ts` 中的逐工具 payload 修补逻辑,以及 tool-catalog 渲染器(`scripts/gen-tool-catalog.ts`)中的条件 `Strict:` 行。没有任何已发布的工具设置过它——在所有 `tool-*` 包的 src 和 `examples/` 中执行 `rg` 搜索,`strict:` 的生产者为零;唯一的 setter 出现在 dsh-tools 单元测试中。
|
||||
|
||||
两个旋钮在适配器间是对称的,因此移除时两个孪生适配器一并清理——[孪生适配器设计](../architecture/2026-06-13-twin-llm-adapters.md)不受影响。
|
||||
两个旋钮在适配器间是对称的,因此移除操作将它们从两个孪生适配器中一并剥离——[孪生适配器设计](../architecture/2026-06-13-twin-llm-adapters.md)不受影响。
|
||||
|
||||
## 决策
|
||||
|
||||
- 从 `GenerateOptions` 中移除 `prefill`,同时移除两个适配器的 UNSUPPORTED 守卫、固定这些 throw 的测试、[core.md](../../../core-data-structures/core.md) 中的粘贴行,以及适配器 README 中记录拒绝行为的行。实操手册(Cookbook)中的 UNSUPPORTED 指导([adding-an-llm-adapter.md](../../../cookbook/adding-an-llm-adapter.md))改为泛化表述——你的提供方无法兑现的 `GenerateOptions` 字段应抛出 `LlmError(..., 'UNSUPPORTED')`——而不再以 prefill 为例。[内容块词汇 RFC](../architecture/2026-06-11-content-block-vocabulary.md) 的后果部分将 prefill 记录为「受生产方门控」而非「已有归属」,遵照 [implemented/AGENTS.md](../AGENTS.md)。
|
||||
- 从 `ToolSchema`、`DefineToolOptions`、`defineTool`、`schemas()` 白名单、deepseek 序列化器分支及其 wire-type 字段、以及 tool-catalog 渲染器的 `Strict:` 行中移除 `strict`。pi-ai 的 payload 修补简化为无条件擦除 pi-ai 自身的逐工具 strict 默认值(pi-ai 在每个序列化工具上打 `strict: false`;手写的孪生适配器不发送此字段,因此擦除逻辑为保持协议格式对等而保留,由其序列化器测试固定)。赋值测试和 core.md 粘贴行已移除;`GenerateOptions` 与 `ToolSchema` 在 `scripts/type-equiv.manifest.json` 中保留各自的行,因为两个类型本身仍然存在,只是少了一个字段。
|
||||
- 从 `GenerateOptions` 中移除 `prefill`,同时移除两个适配器的 UNSUPPORTED 守卫、固定这些 throw 的测试、[core.md](../../../core-data-structures/core.md) 中的粘贴行,以及适配器 README 中记录拒绝行为的行。实操手册(cookbook)中的 UNSUPPORTED 指引([adding-an-llm-adapter.md](../../../cookbook/adding-an-llm-adapter.md))改为泛化表述——你的 provider 无法兑现的 `GenerateOptions` 字段应抛出 `LlmError(..., 'UNSUPPORTED')`——而不再以 prefill 为例。[内容块词汇 RFC](../architecture/2026-06-11-content-block-vocabulary.md) 的后果部分将 prefill 记录为「受 producer 门控」而非「已有归属」,依据 [implemented/AGENTS.md](../AGENTS.md)。
|
||||
- 从 `ToolSchema`、`DefineToolOptions`、`defineTool`、`schemas()` 允许列表、deepseek 序列化分支及其 wire-type 字段,以及 tool-catalog 渲染器的 `Strict:` 行中移除 `strict`。pi-ai 的 payload 修补逻辑简化为对 pi-ai 自身逐工具 strict 默认值的无条件清除(pi-ai 在每个序列化的工具上打 `strict: false`;手写的孪生适配器不发送此字段,因此清除逻辑为保持协议格式对等而保留,由其序列化器测试固定)。setter 测试和 core.md 粘贴行已移除;`GenerateOptions` 与 `ToolSchema` 在 `scripts/type-equiv.manifest.json` 中保留各自的行,因为两个类型只是少了一个字段,本身仍然存在。
|
||||
|
||||
本 RFC 有意**不**触及 `temperature`、`stop` 或 `maxTokens`:这些字段被两个适配器端到端地兑现,是 `agent/request` 上请求变更钩子插件的自然首选目标。
|
||||
本 RFC 有意**不**触碰 `temperature`、`stop` 或 `maxTokens`:它们在两个适配器中都被端到端地兑现,是 `agent/request` 上请求变更钩子插件的自然首选目标。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
### 为什么不保留?
|
||||
|
||||
「显式的 UNSUPPORTED throw 是诚实的契约行为」——但一个旋钮在两个孪生适配器中的唯一实现都是拒绝,它什么也不承诺;删除它反而升级了失败模式:意外的赋值从运行时 throw 变为编译错误。「strict schema 遵循是官方文档记录的提供方功能,且管道完整」——但一个旋钮在有已发布工具设置它**且**有端点兑现它之前,都不是产品表面;今天两者都不成立。二者各自随其第一个真实生产方回归:`prefill` 随实现了 chat-prefix completion 的适配器(以及对不支持它的适配器的明确策略)一起回来,`strict` 随需要它的工具和 beta 端点方案一起回来。
|
||||
「显式的 UNSUPPORTED throw 是诚实的契约行为」——但一个在两个孪生适配器中唯一的实现就是拒绝的旋钮,什么也没承诺;删除它反而升级了失败模式:意外的 setter 变成编译错误而非运行时 throw。「Strict schema 遵循是官方文档记载的 provider 功能,且管道完整」——但一个旋钮在有已发布的工具设置它**并且**有端点兑现它之前,不构成产品表面;今天两者都不成立。它们各自随首个真实 producer 回归:`prefill` 随实现了 chat-prefix completion 的适配器(以及对不支持该功能的适配器的明确策略)一起回来;`strict` 随需要它的工具和 beta 端点方案一起回来。
|
||||
|
||||
## 验证
|
||||
|
||||
`rg prefill` 仅返回 RFC 记录(本文与[内容块词汇 RFC](../architecture/2026-06-11-content-block-vocabulary.md) 的 producer-gated 后果);在 tool-schema 范围内 `rg strict` 仅返回本 RFC、保留的 pi-ai 擦除逻辑,以及无关行文(如 `strictEqual`)。两个适配器的契约测试在移除守卫后通过,pi-ai 修补仍然擦除库的 strict 默认值——协议格式对等由其序列化器测试固定。
|
||||
`rg prefill` 仅返回 RFC 记录(本 RFC 与[内容块词汇 RFC](../architecture/2026-06-11-content-block-vocabulary.md) 中 producer-gated 的后果);在 tool-schema 范围内执行 `rg strict` 仅返回本 RFC、保留的 pi-ai 清除逻辑,以及 `strictEqual` 等无关文本。两个适配器的契约测试在移除守卫后通过,pi-ai 修补逻辑仍然清除库的 strict 默认值——协议格式对等由其序列化器测试固定。
|
||||
|
||||
## 后果
|
||||
|
||||
已发布的钩子桥接不设置任何请求字段,而请求变更插件(`agent/request` waterfall(瀑布式事件)监听器)使用的是 `temperature`/`stop`(保留且可用),而非适配器拒绝的字段。如果 chat-prefix completion 或 strict 模式成为产品功能,重新添加将随适配器/端点工作一起落地,届时契约能说明实际发生了什么,而非「所有人都 throw」。
|
||||
已发布的钩子桥接不设置任何请求字段,而请求变更插件(`agent/request` waterfall(瀑布式事件)监听器)使用的是 `temperature`/`stop`(保留且可用),而非适配器拒绝的字段。如果 chat-prefix completion 或 strict 模式成为产品功能,重新添加将随适配器/端点工作一起落地,届时契约能说明实际发生了什么,而不是「所有人都 throw」。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-04-drop-unconsumed-web-observation-surface.md: c48ff5b80c916cd6cc04d6a8339a8555d05b0d40
|
||||
2026-07-04-drop-unconsumed-web-observation-surface.zh.md: ce8a108450ed9e9308066ab45b8f000dc20fde39
|
||||
2026-07-04-drop-unconsumed-web-observation-surface.zh.md: 4988a5aa77604f528cc23409a3c2290c890a7f5a
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# RFC:移除未被消费的 web 观测面——`providers-change` 事件与 status 方法
|
||||
# RFC:移除未被消费的 web 观测接口——`providers-change` 事件与 status 方法
|
||||
|
||||
Status: implemented
|
||||
|
||||
@@ -6,29 +6,29 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
`WebService` 暴露了一组没有任何生产代码观测的观测面:
|
||||
`WebService` 暴露了一组没有任何生产代码观测的观测接口:
|
||||
|
||||
- **`web/providers-change`**(`packages/web/web/src/index.ts`)在每次 provider 注册和 dispose(资源释放)时声明并发射。每个注册 effect 的回滚 yield 被刻意排在 emit 之前,唯一目的是让一个抛异常的 change listener 能回退注册。该事件在包自身的两个单元测试之外没有任何 listener(其中一个测试的存在就是为了固定那个回滚顺序)。
|
||||
- **`searchStatus()` / `fetchStatus()` 与 `WebCapabilityStatus` 联合类型**(同一个包)没有任何生产调用方:`dsh-tool-web` 直接通过 `ctx.web.search()`/`fetch()` 执行,并将不可用状态以 seam 在执行时抛出的结构化 `WebError` 错误码呈现(`packages/web/tool-web/src/search.ts`、`packages/web/tool-web/src/fetch.ts`);唯一的 status 调用方是 web 包自身的测试。`packages/web/tool-web/README.md` 与 [architecture.md](../../../architecture.md) 中的行文声称工具「只读取聚合的 `searchStatus()`/`fetchStatus()`」——这种漂移之所以存活,仅仅因为没有什么机制会拿行文与调用点做比对。
|
||||
- **`web/providers-change`**(`packages/web/web/src/index.ts`)在每次 provider 注册和 dispose(资源释放)时声明并发出,且每个注册 effect 的回滚 yield 被刻意排在 emit 之前,唯一目的是让抛出异常的 change listener 能回退注册。在该包自身的两个单元测试之外没有任何 listener(其中一个测试的存在仅仅是为了固定那个回滚顺序)。
|
||||
- **`searchStatus()` / `fetchStatus()` 与 `WebCapabilityStatus` 联合类型**(同一个包)没有任何生产调用方:`dsh-tool-web` 通过 `ctx.web.search()`/`fetch()` 直接执行,并将不可用性表现为 seam 在执行时抛出的结构化 `WebError` 错误码(`packages/web/tool-web/src/search.ts`、`packages/web/tool-web/src/fetch.ts`);唯一的 status 调用方是 web 包自身的测试。`packages/web/tool-web/README.md` 和 [architecture.md](../../../architecture.md) 中的行文声称该工具「只读取聚合的 `searchStatus()`/`fetchStatus()`」——这是一处漂移,仅因没有机制检查行文与调用点的一致性而幸存。
|
||||
|
||||
seam 自身的设计使两个观测面都失去了消费方:工具注册跟随产品 ENABLEMENT 而非 provider 可用性(`packages/web/tool-web/src/index.ts`),provider 选择在执行时解析、从不缓存——因此没有需要失效的缓存、没有需要重算的注册集合,也没有调用方需要一个独立于「执行并路由结构化错误」的可用性探针。HMR(热模块替换)清理由 effect disposer 自身承载。
|
||||
seam 自身的设计使这两个接口天然没有消费方:工具注册跟随产品 ENABLEMENT 而非 provider 可用性(`packages/web/tool-web/src/index.ts`),provider 选择在执行时解析且从不缓存——因此没有需要失效的缓存、没有需要重算的注册集合、也没有调用方需要一个有别于「执行并路由结构化错误」的可用性探测。HMR(热模块替换)清理由 effect disposer 自身承载。
|
||||
|
||||
这与[移除未被消费的 `llm/adapter-change` 事件](../../implemented/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.md)如出一辙:那次从 `LlmService` 移除了相同的通知形态、相同的回滚先于 emit 机制,以及相同的 listener-throw 测试。该 RFC 的保留/裁剪判据——保留 `tools/change`(因为它有合理的面向用户的工具列表消费方),裁剪启动期后端注册表信号——把 web provider 注册表信号明确归入裁剪一侧;status 方法则是同一判断应用于拉取面而非推送面。
|
||||
这与 [移除未被消费的 `llm/adapter-change` 事件](../../implemented/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.md) 如出一辙:那个 RFC 从 `LlmService` 中移除了相同的通知形态、相同的 rollback-before-emit 机制和相同的 listener-throw 测试。该 RFC 的保留/裁剪判据——为 `tools/change` 保留其合理的面向用户的工具列表消费方,裁剪启动时的后端注册表信号——将 web provider 注册表明确归入裁剪一侧;status 方法是同一判断应用于 pull 接口而非 push 接口。
|
||||
|
||||
## 决策
|
||||
|
||||
移除注册表变更事件、聚合 status 方法与类型,以及它们的专属测试。provider 私有的 status 保留用于执行时选择。面向调用方的覆盖率现在断言成功执行或结构化的选择错误,web 相关文档描述该按需调用契约。
|
||||
移除注册表变更事件、聚合 status 方法与类型,以及它们的专属测试。provider 私有的 status 保留用于执行时选择。面向调用方的覆盖率现在断言成功执行或结构化的选择错误,web 文档描述该按需调用契约。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
### 为什么不保留?
|
||||
|
||||
web seam RFC 当初有意指定了两者——事件作为最小的 HMR 可见性信号,status 方法作为工具的聚合诊断——且未来的 provider 状态面板是可以想象的。但同一 RFC 的其他选择使它们失去了消费方:按需派生的选择与基于 enablement 的注册使得没有消费方**能**需要它们;已交付的工具展示了真实模式(执行并路由结构化错误);漂移的 README 语句表明承诺的消费方从未实现。按照 AGENTS.md 的原则「RFC 是提案,不是金科玉律」,这些正是该提案中被代码证明过度延伸的部分;未来的观测者重新引入它实际消费的最小信号或查询,由该消费方塑造其形态。
|
||||
web seam RFC 有意指定了两者——事件作为最小的 HMR 可见性信号,status 方法作为工具的聚合诊断——且未来的 provider 状态面板是可以想象的。但同一 RFC 的其他设计选择使它们失去了消费方:按需派生的选择与基于 enablement 的注册使得没有消费方**能**需要这两者;已交付的工具展示了真实模式(执行并路由结构化错误);漂移的 README 语句表明承诺的消费方从未实现。按 AGENTS.md「RFC 是提案,不是金科玉律」的原则,这些是该提案中代码已证明过度设计的部分;未来的观测者按其实际消费的需求重新引入最小的信号或查询,由该消费方塑造其形态。
|
||||
|
||||
## 验证
|
||||
|
||||
`providers-change`、`searchStatus`、`fetchStatus` 和 `WebCapabilityStatus` 在 RFC 历史之外不再有任何拼写残留;catalog 是最新的(`verify-cordis-catalog` 绿色);注册/释放的 HMR 安全测试通过执行行为证明清理正确;tool-web README 与架构段落描述了工具实际拥有的执行时错误路由契约。
|
||||
在 RFC 历史之外不再有 `providers-change`、`searchStatus`、`fetchStatus` 或 `WebCapabilityStatus` 的拼写残留;catalog 是最新的(`verify-cordis-catalog` 绿色);注册/释放的 HMR 安全测试通过执行行为证明清理正确;tool-web README 与 architecture 段落描述了工具实际拥有的执行时错误路由契约。
|
||||
|
||||
## 后果
|
||||
|
||||
未来如果有 provider 选择器 UI 或诊断面板需要变更通知或 status 查询,它会重新添加自己实际消费的最小观测面;相同的判断及其反转条件已记录在 LLM 先例中。
|
||||
未来若有 provider 选择器 UI 或诊断面板需要变更通知或 status 查询,它将重新添加自身所消费的最小接口;相同的判断及其反转条件已记录在 LLM(大语言模型)先例中。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-04-fold-stdio-ui-helper.md: 8d26af190a957b424960519f77cbe132291c74de
|
||||
2026-07-04-fold-stdio-ui-helper.zh.md: 879cce0d40e396b56f1a961b5ff190bab4fd70dd
|
||||
2026-07-04-fold-stdio-ui-helper.zh.md: 795edf082258a56d3c11afbbb8de8cfa0e74e74e
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
# RFC:将 stdio UI 辅助模块折入 stdio 应用
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-04-fold-stdio-ui-helper.md) | 中文
|
||||
|
||||
Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
readline UI 曾是一个完整的包(`@deepseek-ai/dsh-ui-stdio`,位于 `packages/support/`),其唯一的运行时导入方是应用包 `@deepseek-ai/dsh-stdio-demo`。示例通过加载该应用来使用 readline UI,从不自行组合这个辅助模块;仓库中所有其他引用都是因为包边界存在而存在的机械性或描述性表面:manifest 与 tsconfig 条目、生成的 module-graph 行、依赖图与 README 行,以及命名该包的文档注释。ui 分组 README 记录了 support 放置的理由("主要为示例和覆盖率门禁而存在——`ui/` 保留给作为产品交付的界面"),这留下了一个持续的张力:一个已交付的产品应用依赖一个被文档标注为非产品表面的 support 包。
|
||||
readline UI 曾是一个完整的包(`packages/support/` 下的 `@deepseek-ai/dsh-ui-stdio`),其唯一的运行时导入方是应用包 `@deepseek-ai/dsh-stdio-demo`。示例通过加载应用来使用 readline UI,从不自行组合该辅助模块;仓库中所有其他引用都是因为包边界存在而存在的机械性或描述性表面:manifest(元数据清单)与 tsconfig 条目、生成的 module-graph 行、依赖图与 README 行,以及命名该包的文档注释。ui 组 README 记录了 support 放置的理由("主要为示例和覆盖率门禁而存在,`ui/` 保留给作为产品交付的界面"),这留下了一个持续的张力:一个已交付的产品应用依赖一个被明确标注为非产品表面的 support 包。
|
||||
|
||||
这条边界带来的是包元数据、workspace 与 tsconfig 引用、module-graph 行、README 条目,以及 publint 表面——服务于一个并不可独立替换的辅助模块:stdio 应用的前门集群总是包含 readline UI,且没有其他东西能有意义地消费它。
|
||||
这条边界换来的是:包元数据、workspace 与 tsconfig 引用、module-graph 行、README 条目,以及 publint 表面——服务于一个并不可独立替换的辅助模块:stdio 应用的前门集群始终包含 readline UI,且没有其他消费方能有意义地使用它。
|
||||
|
||||
## 决策
|
||||
|
||||
该辅助模块以终端通道插件的形式存在于 `@deepseek-ai/dsh-stdio` 中(`packages/ui/stdio/src/index.ts`):`createStdioChat`、其 `StdioRuntime` 测试 seam 及单元测试(`packages/ui/stdio/tests/stdio.spec.ts`、`readline.spec.ts`)一并迁入,因此 EOF 处理、渲染、dispose(资源释放)以及 piped-vs-TTY 行为在按文件覆盖率门禁下仍有单元测试覆盖,且无需劫持进程全局对象。该模块保持具名的 `name`/`inject`/`Config`/`apply` 导出形状——即应用通过 `ctx.plugin(uiStdio, …)` 挂载时消费的契约——而 `examples/echo-agent` 与 `examples/coding-agent` 中的 keyless Loader 路径冒烟测试继续证明组合树能通过真实 Loader 启动(stdio 包的插件形状单元测试套件固定了显式的 `unwrapExports` 断言,因为缺少 `inject` 的 bundle 会跳过一个意外的 default 导出而非崩溃)。
|
||||
该辅助模块作为终端通道插件存放在 `@deepseek-ai/dsh-stdio` 中(`packages/ui/stdio/src/index.ts`):`createStdioChat`、其 `StdioRuntime` 测试 seam 及单元测试(`packages/ui/stdio/tests/stdio.spec.ts`、`readline.spec.ts`)一并迁入,因此 EOF 处理、渲染、dispose(资源释放)以及管道/TTY 行为在按文件覆盖率门禁下仍有单元测试覆盖,且无需劫持进程全局对象。该模块保留具名的 `name`/`inject`/`Config`/`apply` 导出形状——即应用的 `ctx.plugin(uiStdio, …)` 挂载所消费的契约——而 `examples/echo-agent` 与 `examples/coding-agent` 中的 keyless Loader 路径冒烟测试继续证明组合树能通过真实 Loader 启动(stdio 包的插件形状单元测试套件固定了显式的 `unwrapExports` 断言,因为缺少 `inject` 的 bundle 会跳过一个意外的 default 导出而不是崩溃)。
|
||||
|
||||
`packages/support/ui-stdio` 包已删除:manifest、tsconfig 引用、module-graph 行与 README 行均已清理;原先命名该包的文档注释(示例 e2e 模块文档、`packages/README.md`、support 与 todo README、[ui 分组 README](../../../../packages/ui/README.md))现在描述的是包内模块。
|
||||
`packages/support/ui-stdio` 包已移除:manifest、tsconfig 引用、module-graph 行与 README 行均已删除;曾命名该包的文档注释(示例 e2e 模块文档、`packages/README.md`、support 与 todo README、[ui 组 README](../../../../packages/ui/README.md))现在描述的是包内模块。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
### 为什么不将其提升到 `ui/`?
|
||||
### 为什么不将其提升到 `ui/` 而是折入?
|
||||
|
||||
提升可以解决 support 与产品之间的错位,同时保留包边界——但只有在 readline UI 是一个可独立替换的集成或拥有第二个组合方时才是正确选择,而消费方普查表明两者都不成立。结构化的 ACP 桥接保持独立包,因为它是产品协议表面,拥有自己的契约和快照层级;readline 辅助模块只是一个应用前门的脚手架。在正式发布前重新拆出的成本很低:如果未来有第二个产品应用需要 readline UI,届时再拆出,由那个消费方来塑造包契约。
|
||||
提升可以解决 support 与 product 之间的错位,同时保留边界——只有在 readline UI 是一个可独立替换的集成或有第二个组合方时才是正确选择,而消费方普查表明两者皆非。结构化的 ACP 桥接保留为独立包,因为它是具有自身契约和快照层级的产品协议表面;readline 辅助模块只是一个应用前门的脚手架。在发布前重新拆分成本很低:如果将来有第二个产品应用需要 readline UI,届时再拆出来,由那个消费方来塑造包契约。
|
||||
|
||||
## 后果
|
||||
|
||||
- stdio 应用完整拥有自己的前门;一个叶子 `cordis.yml` 仍然只加载一个应用包,演示的形状没有变化。
|
||||
- stdio 应用完整拥有自己的前门;叶子 `cordis.yml` 仍然只加载一个应用包,演示的形态没有变化。
|
||||
- 未来如果有独立的终端 UI 需要将该辅助模块作为包使用,届时由那个第二消费方驱动重新引入,而非仓库为假设性的复用保留一条边界。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-04-prune-producerless-vocabulary-variants.md: 271b97f6217a2694f36b7fe7339eab6176dba9e5
|
||||
2026-07-04-prune-producerless-vocabulary-variants.zh.md: 5d75c0327e2faf5e6e37f8db959509161beda702
|
||||
2026-07-04-prune-producerless-vocabulary-variants.zh.md: 2fe8d41a011c37919bd01022d5be6d309b865bf7
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
# RFC:清理无生产者的词汇变体(块缓存提示、`agent` 消息来源、`continuation` 轮次触发器)
|
||||
|
||||
Status: implemented
|
||||
# RFC:裁剪无生产者的词汇变体(块缓存提示、`agent` 消息来源、`continuation` 轮次触发器)
|
||||
|
||||
[English](2026-07-04-prune-producerless-vocabulary-variants.md) | 中文
|
||||
|
||||
Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
合并可扩展的词汇映射表设计上通过声明合并来增长,代码库已在 `TurnEndReasonMap`(`packages/core/session/src/types.ts`)上声明了准入策略:像 `refusal` 这样的变体「在适配器或循环首次发出它之前,有意不加入」。三个已声明的词汇项违反了这一策略——每个都既无生产者也无消费方,其中两个甚至没有测试:
|
||||
可合并扩展的词汇映射表设计上通过声明合并来增长,代码库已在 `TurnEndReasonMap`(`packages/core/session/src/types.ts`)上明确了准入策略:像 `refusal` 这样的变体「在适配器或循环首次发出它之前,有意不纳入」。三个已声明的词汇项违反了该策略——每个都既无生产者也无消费方,其中两个甚至没有测试:
|
||||
|
||||
- **`CacheHint` 及其 `cache?: CacheHint` 块字段**,位于 `TextBlock`/`ToolResultBlock`(`packages/llm/llm/src/types.ts`;image block 上还有第三个同类字段,随 image block 一起移除——见[移除 image 的 RFC](2026-07-04-drop-image-content-block.md))。没有任何地方构造过带 `cache:` 的块——src、测试和文档粘贴全部搜索为空——两个适配器也都不读 `.cache`:DeepSeek 的 prompt 缓存是自动的,适配器只从响应中映射出 `prompt_cache_hit_tokens`,从不向请求中发送提示。这是 Anthropic 风格的 `cache_control` 接口面,却没有任何提供方能兑现它。
|
||||
- **`MessageSourceMap.agent`**(`{ kind: 'agent'; agentId: string }`,同一文件)。零个构造点,测试中也没有。它预期的生产者在上线时并未使用它:subagent 后端将父级的 prompt 发送给子级时不带 `source`,因此日志中记录为 `{ kind: 'user' }`,通用信封渲染器在插值 `source.kind` 时也从不按它路由。
|
||||
- **`TurnTriggerMap.continuation`**(`packages/core/session/src/types.ts`)。agent loop(智能体循环)在结构上不可能发出它——续写发生在一个轮次*内部*作为后续步骤,从不作为新轮次——循环只构造 `message` 和 `injection` 触发器。唯一的写入者是一个手工构建的测试 fixture(测试前置数据)(`packages/support/llm-replay/tests/llm-replay.spec.ts`),它只需要一个任意的非 message 触发器,`injection` 触发器同样满足需求;唯一的生产环境触发器读取者 ACP 桥接层只过滤 `kind === 'message'`。
|
||||
- **`CacheHint` 及其 `cache?: CacheHint` 块字段**,位于 `TextBlock`/`ToolResultBlock`(`packages/llm/llm/src/types.ts`;image block 上还有第三个同类字段,已随 image block 一起移除——见[移除 image block 的 RFC](2026-07-04-drop-image-content-block.md))。没有任何地方构造过带 `cache:` 的块——src、测试和文档粘贴全部搜索为空——两个适配器也都不读 `.cache`:DeepSeek 的 prompt 缓存是自动的,适配器只从响应中映射出 `prompt_cache_hit_tokens`,从不向请求中发送提示。这是 Anthropic 风格的 `cache_control` 接口面,却没有能兑现它的提供方。
|
||||
- **`MessageSourceMap.agent`**(`{ kind: 'agent'; agentId: string }`,同一文件)。零个构造点,包括测试在内。它预期的生产者在实现时并未使用它:subagent 后端将父级的 prompt 发送给子级时不带 `source`,因此记录为 `{ kind: 'user' }`,通用信封渲染器在插值 `source.kind` 时也从未对其做路由。
|
||||
- **`TurnTriggerMap.continuation`**(`packages/core/session/src/types.ts`)。agent loop(智能体循环)在结构上不可能发出它——continuation 发生在一个轮次*内部*作为后续步骤,而非作为新轮次——循环只构造 `message` 和 `injection` 触发器。唯一的写入者是一个手工构建的测试 fixture(测试前置数据),它只需要一个任意的非 message 触发器(`packages/support/llm-replay/tests/llm-replay.spec.ts`),`injection` 触发器同样满足需求;唯一的生产环境触发器读取方 ACP 桥接层只过滤 `kind === 'message'`。
|
||||
|
||||
## 决策
|
||||
|
||||
删除 `CacheHint`、其 `cache?` 块字段、`agent` 消息来源变体和 `continuation` 轮次触发器变体:已发布的词汇不再包含它们。llm-replay fixture 改用 `injection` 触发器(任何非 `message` 触发器都能满足其用途)。[core.md](../../../core-data-structures/core.md) 和 [session.md](../../../core-data-structures/session.md) 中的 type-equiv 粘贴与裁剪后的映射表一致——两个符号保留在 `scripts/type-equiv.manifest.json` 中,因为每个映射表只是少了一个成员——[内容块词汇 RFC](../architecture/2026-06-11-content-block-vocabulary.md) 的「后果」部分将缓存提示记录为「受生产者门控」而非「已有归属」,遵照 [implemented/AGENTS.md](../AGENTS.md)。
|
||||
删除 `CacheHint`、其 `cache?` 块字段、`agent` 消息来源变体与 `continuation` 轮次触发器变体:发布的词汇表不再包含它们。llm-replay fixture 改用 `injection` 触发器(任何非 `message` 触发器均满足其用途)。[core.md](../../../core-data-structures/core.md) 和 [session.md](../../../core-data-structures/session.md) 中的 type-equiv 粘贴与裁剪后的映射表一致——两个符号保留在 `scripts/type-equiv.manifest.json` 中,因为每个映射表本身仍然存在,只是少了一个成员——[内容块词汇 RFC](../architecture/2026-06-11-content-block-vocabulary.md) 的后果部分将缓存提示记录为「受生产者门控」而非「已有归属」,依照 [implemented/AGENTS.md](../AGENTS.md)。
|
||||
|
||||
每个变体在获得真正的生产者之日回归,这正是映射表设计上的增长方式:缓存功能连同传输它的适配器一起重新添加 `cache`;subagent 归属连同打标的后端和路由它的消费方一起重新添加 `agent`;真正启动新轮次的自动续写功能连同发出它的插件一起重新添加 `continuation`。
|
||||
每个变体在获得真正的生产者之日回归,这正是映射表设计的增长方式:缓存功能连同传输它的适配器一起重新添加 `cache`;subagent 归属连同打标的后端和路由它的消费方一起重新添加 `agent`;真正启动新轮次的自动续行功能连同发出它的插件一起重新添加 `continuation`。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
### 为什么不保留它们?
|
||||
|
||||
[内容块词汇 RFC](../architecture/2026-06-11-content-block-vocabulary.md) 将「缓存提示……已有归属」列为设计后果,预留的槽位确实能传达意图。但一个空槽位是契约面,每个实现和消费方都必须考虑它(我的适配器是否必须兑现 `cache`?我的渲染器是否必须路由 `agent` 来源?),而兄弟映射表自身的 JSDoc 已经拒绝了「无发出者的预留」——`refusal` 和 `max_turn_requests` 被标注为*当有东西首次发出它们时*再添加的变体,而非提前声明。对已声明但无生产者的变体执行同一标准,才能让词汇表有意义:如果它在映射表里,就一定有东西在生产它。
|
||||
[内容块词汇 RFC](../architecture/2026-06-11-content-block-vocabulary.md) 将「缓存提示……已有归属」列为设计后果,预留槽位确实能表达意图。但一个空槽位是每个实现和消费方都必须考虑的契约面(我的适配器需要兑现 `cache` 吗?我的渲染器需要路由 `agent` 来源吗?),而同族映射表自身的 JSDoc 已经拒绝了「无发出者的预留」——`refusal` 和 `max_turn_requests` 被明确标注为*当某物首次发出它们时*再添加的变体,而非提前声明。对已声明但无生命的变体施加同样的标准,使词汇表具有实际意义:如果它在映射表中,就一定有东西在生产它。
|
||||
|
||||
## 验证
|
||||
|
||||
`rg` 搜索 `CacheHint`、`agent` 消息来源的拼写和 `continuation` 触发器的拼写,只返回 RFC 记录(本文,以及[移除 image 的 RFC](2026-07-04-drop-image-content-block.md) 中关于 image block 自身 `cache` 字段的描述);llm-replay fixture 使用 `injection` 触发器断言相同的回放行为;核心数据结构粘贴与 type-equiv manifest(元数据清单)保持同步。
|
||||
对 `CacheHint`、`agent` 消息来源拼写和 `continuation` 触发器拼写执行 `rg` 搜索,结果仅返回 RFC 记录(本文,以及[移除 image block 的 RFC](2026-07-04-drop-image-content-block.md) 中关于 image block 自身 `cache` 字段的说明);llm-replay fixture 使用 `injection` 触发器断言了相同的回放行为;core-data-structures 粘贴与 type-equiv manifest 保持同步。
|
||||
|
||||
## 后果
|
||||
|
||||
没有运行时行为改变——本来就没有任何东西能构造这些值。镜像事件的移除([边界镜像 RFC](2026-06-20-remove-agent-boundary-mirror-events.md)、[流式分片镜像 RFC](2026-07-02-remove-stream-chunk-mirror.md))只涉及瞬态的 `agent/*` 事件,从不触及持久化词汇,因此不存在冲突。其他地方准入策略已经生效:`rejected`、`prompt/blocked` 和 `hook/invoked`/`hook/result` 各自都有活跃的生产者——本 RFC 将同一标准延伸到缺少生产者的三个变体。image block 自身的 `cache?` 字段属于[移除 image 的 RFC](2026-07-04-drop-image-content-block.md),随该块一起移除;本 RFC 覆盖的是保留下来的块类型上的两个字段。
|
||||
没有任何运行时行为改变——本来就没有东西能构造这些值。镜像事件的移除([boundary-mirror RFC](2026-06-20-remove-agent-boundary-mirror-events.md)、[stream-chunk RFC](2026-07-02-remove-stream-chunk-mirror.md))只涉及瞬态的 `agent/*` 事件,从不涉及持久词汇,因此不存在冲突。其他地方准入策略已经成立:`rejected`、`prompt/blocked` 和 `hook/invoked`/`hook/result` 各自都有活跃的生产者——本 RFC 将同一标准延伸到缺少生产者的三个变体。image block 自身的 `cache?` 字段属于[移除 image block 的 RFC](2026-07-04-drop-image-content-block.md),已随该块一起移除;本 RFC 覆盖的是留存块类型上的两个字段。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-04-prune-write-only-fs-surface.md: ac2cbcc282b848b26a3d5d327e0ab54612b5ac91
|
||||
2026-07-04-prune-write-only-fs-surface.zh.md: 799847aa77599a292c1aa48e150aae99fa130ae3
|
||||
2026-07-04-prune-write-only-fs-surface.zh.md: e854df76cae74033404aa9cc1986fdd118f19b10
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
# RFC:从 fs seam 中移除只写字段与一个无效路由旋钮
|
||||
|
||||
Status: implemented
|
||||
# RFC:从 fs seam 中移除只写字段与一个无效的路由旋钮
|
||||
|
||||
[English](2026-07-04-prune-write-only-fs-surface.md) | 中文
|
||||
|
||||
Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
[fs seam 拆分](2026-06-26-fsspec-style-fs-seam.md)将读取路由与策略从后端移入 `dsh-tool-fs` 和 `dsh-fs-policy`。四处接口保留了拆分前的形态——每次调用都填充,却无人读取:
|
||||
[fs seam 拆分](2026-06-26-fsspec-style-fs-seam.md)将读取路由与策略从后端移至 `dsh-tool-fs` 和 `dsh-fs-policy`。有四处接口保留了拆分前的形态——每次调用都填充,却无人读取:
|
||||
|
||||
1. **`dsh-fs-local` 中的 `STREAM_MIN_SIZE` + `FsIoInternals.streamMinSize`**——*在本次变更之前已被"无硬编码可调参数"审计移除,该审计将路由阈值改为 `dsh-tool-fs` 的 `readStreamMinSize` 配置;此处记录是为了完整呈现整个裁剪。*原始位置(`packages/fs/fs-local/src/fsio.ts`,从 `packages/fs/fs-local/src/index.ts` 再导出):包括 fs-local 自身源码和测试在内,全仓库零读取者。后端不做读取路由——`readWholeText`/`streamWholeText` 是调用方自行选择的独立原语——真正的路由常量在消费方(`packages/fs/tool-fs/src/read.ts`,与 `info.size` 比较)。10 MiB 这个事实有两份镜像;后端那份是死代码,而该旋钮的 JSDoc 声称提供一个并不存在的"读取路由"覆盖。
|
||||
2. **`FsTarget.inputPath`**(`packages/fs/fs/src/types.ts`):每个后端和每个测试 fake 都必须编造一个"仅用于诊断"的值,而生产环境零读取者——策略插件和所有错误消息使用的是 `targetKey`/`displayPath`。`listDir` 的生产者暴露了语义摇摆:目录子项拿到的是裸条目名,这不是任何人的"输入路径"。
|
||||
3. **`FsEditOutcome.replacements` + `.replaceAll`**(`packages/fs/fs/src/types.ts`):`replacements` 生产环境零读取者(单匹配策略本身保留——它由后端内部的 `FS_AMBIGUOUS_EDIT`/`FS_EDIT_NOT_FOUND` 抛出强制执行,错误消息保留了内部计数);`replaceAll` 仅被 `packages/fs/tool-fs/src/edit.ts` 中的 `formatEditOutput` 读取——作为工具已持有的 `replace_all` 参数的回声。精简后,`FsEditOutcome` 变为 `{ version, before, after }`,与 `FsWriteOutcome` 中真正由后端发现的字段对齐。
|
||||
4. **`FileReadOutcome.limit` + `.version`**(`packages/fs/tool-fs/src/read-render.ts`):由读取工具填充,但 `formatReadOutput` 只渲染 `offset`/`lines`/`totalLines`/`truncatedByBytes`,而 `fs/observed` 事件直接使用 `info.version`,不使用 outcome 的副本。
|
||||
1. **`dsh-fs-local` 中的 `STREAM_MIN_SIZE` + `FsIoInternals.streamMinSize`**——*在本次变更之前已被「禁止硬编码可调参数」审计移除,该审计将路由阈值改为 `dsh-tool-fs` 的 `readStreamMinSize` 配置;此处记录是为了完整呈现整次清理。* 原始位置(`packages/fs/fs-local/src/fsio.ts`,从 `packages/fs/fs-local/src/index.ts` 重导出):包括 fs-local 自身源码和测试在内,全仓库零读取者。后端没有读取路由——`readWholeText`/`streamWholeText` 是调用方自行选择的两个独立原语——真正的路由常量位于消费方(`packages/fs/tool-fs/src/read.ts`,与 `info.size` 比较)。同一个 10 MiB 事实的两份镜像;后端那份是死代码,且该旋钮的 JSDoc 声称提供一个实际不存在的「read routing」覆盖。
|
||||
2. **`FsTarget.inputPath`**(`packages/fs/fs/src/types.ts`):每个后端和每个测试 mock 都必须为这个「仅供诊断」的字段编造一个值,而生产环境零读取者——策略插件和所有错误消息使用的是 `targetKey`/`displayPath`。`listDir` 的生产者暴露了语义上的摇摆:目录子项得到的是裸条目名,这不是任何人的「input」。
|
||||
3. **`FsEditOutcome.replacements` + `.replaceAll`**(`packages/fs/fs/src/types.ts`):`replacements` 生产环境零读取者(单匹配策略本身保留——它由后端内部 `FS_AMBIGUOUS_EDIT`/`FS_EDIT_NOT_FOUND` 抛出来强制执行,错误消息保留了内部计数);`replaceAll` 仅被 `packages/fs/tool-fs/src/edit.ts` 中的 `formatEditOutput` 读取——作为工具本身已持有的 `replace_all` 参数的回声。精简后,`FsEditOutcome` 变为 `{ version, before, after }`,与 `FsWriteOutcome` 中真正由后端发现的字段对齐。
|
||||
4. **`FileReadOutcome.limit` + `.version`**(`packages/fs/tool-fs/src/read-render.ts`):由读取工具填充,但 `formatReadOutput` 只渲染 `offset`/`lines`/`totalLines`/`truncatedByBytes`,且 `fs/observed` 事件发射直接使用 `info.version` 而非 outcome 的副本。
|
||||
|
||||
## 决策
|
||||
|
||||
删除 fs-local 常量及其再导出和 `streamMinSize` 旋钮(`FsIoInternals` 中剩余的旋钮确实被原子写入测试使用);从 `FsTarget` 中移除 `inputPath`;将 `FsEditOutcome` 精简为 `{ version, before, after }`,并将 `replaceAll` 从解析后的参数传给 `formatEditOutput`;从 `FileReadOutcome` 中移除 `limit`/`version`。[filesystem.md](../../../core-data-structures/filesystem.md) 中的粘贴内容、`packages/fs/fs/README.md`,以及那些不得不编造被移除字段的测试 fake 随类型一起精简。
|
||||
删除 fs-local 的常量及其重导出,以及 `streamMinSize` 旋钮(`FsIoInternals` 中剩余的旋钮确实被原子写入测试使用);从 `FsTarget` 中移除 `inputPath`;将 `FsEditOutcome` 精简为 `{ version, before, after }`,并将 `replaceAll` 从解析后的参数传入 `formatEditOutput`;从 `FileReadOutcome` 中移除 `limit`/`version`。[filesystem.md](../../../core-data-structures/filesystem.md) 中的粘贴内容、`packages/fs/fs/README.md`,以及那些不得不为已移除字段编造值的测试 mock,都随类型一起缩减。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
### 为什么不保留?
|
||||
|
||||
未来的权限/隔离层可能需要解析前的路径来生成错误文本——但它需要的是*请求*,每个调用点仍然持有请求。"替换了 N 处"可能成为面向模型的文本——那是需要时再设计的行为变更,且后端内部的计数为其错误消息保留着。读取页脚可能展示 `limit`——页脚展示的一切已经可以从 `lines`/`totalLines` 推导。与此同时,当前和未来的每个后端(远程、原生)都必须编造无人消费的协议格式(wire format)字段,每个测试 fake 都必须满足它们。
|
||||
未来的权限/隔离层可能需要解析前的路径来生成错误文本——但它需要的是*请求*,每个调用点仍然持有请求。「替换了 N 处」可能成为面向模型的文本——这是一个需要时再设计的行为变更,且后端内部的计数为其错误消息而保留。读取页脚可能展示 `limit`——但页脚展示的一切已经可以从 `lines`/`totalLines` 推导。与此同时,每个现有和未来的后端(远程、原生)都必须编造无人消费的协议字段,每个测试 mock 都必须满足它们。
|
||||
|
||||
## 验证
|
||||
|
||||
被移除的接口已不存在——`dsh-fs-local` 中的 `STREAM_MIN_SIZE`/`streamMinSize`、`FsTarget.inputPath`、`FsEditOutcome.replacements`/`.replaceAll`、`FileReadOutcome.limit`/`.version`——而请求侧的 `replaceAll`(`FsEditRequest`)和其他 outcome 类型上的 version 字段未受影响;测试 fake 随类型一起精简。`formatEditOutput` 在 `replace_all` 两个分支下的输出文本不变,因此没有快照 golden 被搅动。
|
||||
被移除的接口已消失——`dsh-fs-local` 中的 `STREAM_MIN_SIZE`/`streamMinSize`、`FsTarget.inputPath`、`FsEditOutcome.replacements`/`.replaceAll`,以及 `FileReadOutcome.limit`/`.version`——而请求侧的 `replaceAll`(`FsEditRequest`)和其他 outcome 类型上的 version 字段未受影响;测试 mock 随类型一起缩减。`formatEditOutput` 在 `replace_all` 两个分支下输出的文本不变,因此没有快照黄金文件被搅动。
|
||||
|
||||
## 后果
|
||||
|
||||
后端不增加新义务;它们卸下了四个无人消费的字段。fs 发现工作(glob/grep 工具)触及相同的 `dsh-fs` 类型文件——这是文本层面而非设计层面的重叠,可以机械地解决。
|
||||
后端不增加新义务,反而卸下了四个无人消费的字段。fs 发现功能(glob/grep 工具)涉及相同的 `dsh-fs` 类型文件——这是文本层面而非设计层面的重叠,可以机械地合并解决。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-04-remove-agent-steering-mirror.md: 311f0f8ffd278adf71a35617b900d4d16037055a
|
||||
2026-07-04-remove-agent-steering-mirror.zh.md: 4ceb31a0263a7c386ceed071d3f0db315c7a9f23
|
||||
2026-07-04-remove-agent-steering-mirror.zh.md: 24198afb0714863867f4d7e17ae19ea8af6a88bd
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# RFC:移除 `agent/steering` 镜像事件发射
|
||||
# RFC:移除 `agent/steering` 镜像 emit
|
||||
|
||||
[English](2026-07-04-remove-agent-steering-mirror.md) | 中文
|
||||
|
||||
@@ -6,28 +6,28 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
`agent/steering` 是最后一个仍存在的、对持久会话事件的瞬态镜像。循环的 steering 排空逻辑先追加持久事件 `steering/message { turn, content, source }`,紧接着下一行就发射 `agent/steering(agent, turn, content, source)`——同一个事实以 fire-and-forget 事件的形式重复一遍(`packages/core/agent-loop/src/loop.ts`,`drainSteering`)。它在生产环境中没有任何监听者:唯一的订阅方是一个循环回归测试,断言该发射携带了 `source`——而这个事实在上一行的持久事件中已经记录。
|
||||
`agent/steering` 是最后一个仍存在的、对持久会话事件的瞬态镜像。agent loop(智能体循环)的 steering(中途引导)drain 逻辑先追加持久事件 `steering/message { turn, content, source }`,紧接着下一行就 emit `agent/steering(agent, turn, content, source)`——同一个事实以 fire-and-forget 事件的形式重复发出(`packages/core/agent-loop/src/loop.ts`,`drainSteering`)。它在生产环境中没有任何监听者:唯一的订阅方是一个 agent loop 回归测试,断言 emit 携带了 `source`——而这同一个事实已经由上一行的持久事件记录。
|
||||
|
||||
`agent/steering` 以相同的 payload 复制了紧邻其前的持久事件 `steering/message`。`agent/queued` 则保留为纯 live 信号,因为它在持久化之前触发,覆盖了可能在进入日志前被取消的工作。
|
||||
`agent/steering` 以相同的 payload 重复了紧接其前的持久事件 `steering/message`。`agent/queued` 仍保留为纯瞬态信号,因为它在持久化之前触发,覆盖了可能在进入日志前被取消的工作。
|
||||
|
||||
steering(中途引导)承载着真实的生产流量:钩子桥的轮次续行决策通过 `inbox.steer()` 注入原因,落地为持久的 `steering/message` 事件,钩子矩阵的 golden 文件固定了这些事件。所有这些消费方观察的都是持久事件,没有任何消费方观察镜像。
|
||||
steering 承载着真实的生产流量:hook bridge 的轮次续行决策通过 `inbox.steer()` 注入理由,落地为持久的 `steering/message` 事件,hook-matrix 的 golden 文件对此进行固定——所有这些消费方观察的都是持久事件。没有任何消费方观察镜像事件。
|
||||
|
||||
## 决策
|
||||
|
||||
`agent/steering` 从 agent 事件分类体系中移除:`packages/core/agent/src/types.ts` 中的声明(及其在 live-events JSDoc 列表中的提及)、`drainSteering` 中的发射(随之移除的还有当时已无用的 `ctx` 参数)、`packages/core/agent/README.md` 中的对应行,以及循环伪代码块中的发射行(`packages/core/agent-loop/src/loop.ts` 模块文档与 [architecture.md](../../../architecture.md));Cordis catalog 重新生成后不再包含它。唯一的回归测试改为在持久事件 `steering/message` 上固定 source 保持——它所固定的事实存在于日志中。
|
||||
`agent/steering` 从 agent 事件分类体系中移除:`packages/core/agent/src/types.ts` 中的声明(及其在 live-events JSDoc 列表中的提及)、`drainSteering` 中的 emit(随之移除的还有当时已无用的 `ctx` 参数)、`packages/core/agent/README.md` 中的对应行,以及 loop 伪代码块中的 emit 行(`packages/core/agent-loop/src/loop.ts` 模块文档与 [architecture.md](../../../architecture.md));Cordis catalog 重新生成后不再包含它。唯一的回归测试改为在持久事件 `steering/message` 上固定 source 保持性——它所固定的事实存在于日志中。
|
||||
|
||||
三份已实施的 RFC 曾声明保留该事件,每份均按 [implemented/AGENTS.md](../AGENTS.md) 修订,指向本 RFC 作为移除记录:[boundary RFC](2026-06-20-remove-agent-boundary-mirror-events.md) 的保留列表条目、[stream-chunk RFC](2026-07-02-remove-stream-chunk-mirror.md) 的范围条款,以及 [event-domain-semantics RFC](../architecture/2026-06-30-event-domain-semantics.md) 的瞬态发射枚举。
|
||||
三份已实施的 RFC 曾声明保留该事件,每份均按 [implemented/AGENTS.md](../AGENTS.md) 的要求修订,指向本 RFC 作为移除记录:[boundary RFC](2026-06-20-remove-agent-boundary-mirror-events.md) 的保留列表条目、[stream-chunk RFC](2026-07-02-remove-stream-chunk-mirror.md) 的范围条款,以及 [event-domain-semantics RFC](../architecture/2026-06-30-event-domain-semantics.md) 的瞬态 emit 枚举。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
### 为什么不保留?
|
||||
|
||||
"它是控制信号,不是边界事件"——但分类体系的实际区分维度是「镜像 vs 纯 live」,而非「控制 vs 边界」,而这个事件属于镜像。需要入队时通知的消费方有 `agent/queued`(带 steering flag);需要排空时通知的消费方本质上是在请求 `steering/message` 被追加的那一刻,而 `session/event` 以相同 payload 加上持久性提供了这一点。被否决的 [retire-mid-turn-steering RFC](../../rejected/simplification/2026-06-20-retire-mid-turn-steering.md) 捍卫的是 steering **能力**——`steer()`、持久事件、续行强制——本次移除对这些全部不动。
|
||||
"它是控制信号,不是边界事件"——但分类体系的操作性区分是「镜像 vs. 纯瞬态」,而非「控制 vs. 边界」,而这个事件属于镜像。需要入队时通知的消费方有 `agent/queued`(带 steering flag);需要 drain 时通知的消费方,本质上是在请求 `steering/message` 被追加的那一刻,而 `session/event` 以相同 payload 加上持久性提供了这一通知。被否决的 [retire-mid-turn-steering RFC](../../rejected/simplification/2026-06-20-retire-mid-turn-steering.md) 捍卫的是 steering **能力**——`steer()`、持久事件、续行强制——本次移除对这些全部保持不变。
|
||||
|
||||
## 验证
|
||||
|
||||
`agent/steering` 这一拼写仅存在于 RFC 行文中(本 RFC、上述三份修订后的 RFC,以及冻结的[被否决 steering 能力 RFC](../../rejected/simplification/2026-06-20-retire-mid-turn-steering.md),其文本记录了它所拒绝的提案);catalog 已重新生成;重定向后的测试在 `steering/message` 上固定 source 保持。
|
||||
`agent/steering` 这一拼写仅存于 RFC 行文中(本 RFC、上述三份修订的 RFC,以及冻结的[被否决的 steering 能力 RFC](../../rejected/simplification/2026-06-20-retire-mid-turn-steering.md),其文本记录了它所拒绝的提案);catalog 已重新生成;重定向后的测试在 `steering/message` 上固定 source 保持性。
|
||||
|
||||
## 后果
|
||||
|
||||
没有需要迁移的生产监听者。两种 live 通知需求都保留了归属:入队时通知归 `agent/queued`(带 `steering` flag),排空时通知归 `session/event`(持久的 `steering/message` 落地时触发)。
|
||||
生产环境中没有需要迁移的监听者,两种瞬态通知需求各有归宿:入队时由 `agent/queued`(带 `steering` flag)承载,drain 时由 `session/event` 在持久事件 `steering/message` 落地时承载。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-04-share-app-bin-boot-glue.md: afaa61fe909f3dbf900337518969410780020a88
|
||||
2026-07-04-share-app-bin-boot-glue.zh.md: 2a022fbb8dcdf6977ede81780a87c570715ce441
|
||||
2026-07-04-share-app-bin-boot-glue.zh.md: 33fe2f27df9c8b172e4296ece0720813f9775f86
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
# RFC:共享应用 bin 的启动胶水代码,不再维护两份副本
|
||||
|
||||
Status: implemented
|
||||
# RFC:共享应用 bin 的启动胶水代码,而非维护两份副本
|
||||
|
||||
[English](2026-07-04-share-app-bin-boot-glue.md) | 中文
|
||||
|
||||
Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
stdio 和 ACP bin 各自重复了环境加载、fail-loud 处理、入口校验与启动逻辑,包括微妙的 Loader 失败行为。两份副本已经发生漂移,且位于自执行文件中、被排除在单元测试覆盖率之外,导致其中的辅助导出无法被复用。
|
||||
stdio 和 ACP 两个 bin 各自重复了环境加载、fail-loud 处理、入口校验与启动逻辑,包括微妙的 Loader 失败行为。两份副本已经发生漂移,且位于自执行文件中、被排除在单元测试覆盖率之外,导致其导出的辅助函数无法被复用。
|
||||
|
||||
## 决策
|
||||
|
||||
辅助逻辑只存在一处:[`@deepseek-ai/dsh-app-boot`](../../../../packages/ui/app-boot)(`packages/ui/app-boot`,归入 `ui` 组,因为 bin 是已发布产物,其运行时依赖本身也必须是已发布的,而非 `support/`)。包含:`resolveConfigPath`(快照感知,两个 bin 共用的唯一路径解析器)、`loadEnv`、`installFailLoud`、`assertEntriesLoaded` 和 `boot`,每个函数都按 bin 的诊断前缀参数化,并在其副作用 seam(warn sink、process 切片)处可注入,使单元测试套件能覆盖每个分支——包括 `boot()` 在进程内驱动真实 Loader、使用相对路径 specifier 的配置,涵盖已就绪树的正常路径和无 fiber 入口的拒绝路径。该包(package)启用了逐文件 100% 覆盖率门禁;Loader 失败的经验知识只有一个归属地。
|
||||
辅助函数只存在一处:[`@deepseek-ai/dsh-app-boot`](../../../../packages/ui/app-boot)(`packages/ui/app-boot`,归入 `ui` 分组,因为 bin 是已发布产物,其运行时依赖本身也必须是已发布的包,而非 `support/`)。包含:`resolveConfigPath`(快照感知,两个 bin 共用的唯一路径解析器)、`loadEnv`、`installFailLoud`、`assertEntriesLoaded` 与 `boot`,每个函数都通过 bin 的诊断前缀参数化,并在其副作用 seam(warn sink、process slice)处支持注入,使单元测试套件能覆盖每个分支——包括 `boot()` 在进程内驱动真实 Loader、使用相对路径 specifier 配置的场景,既覆盖已稳定树的正常路径,也覆盖无 fiber 入口的拒绝路径。该包启用逐文件 100% 覆盖率门禁;Loader 失败的相关知识只有一个归属地。
|
||||
|
||||
每个 `bin.ts` 是一个精简的自执行组合:在共享辅助逻辑之上叠加各自应用特有的生命周期(ACP bin:replay 模式下跳过环境加载与 stdin-EOF dispose;stdio bin:无额外逻辑)。bin 文件仍然被排除在覆盖率之外且不导出任何内容;已发布产物的防护措施不变——built-bin 冒烟测试仍然在一个 node_modules 形状的临时目录下用原生 node 运行每个 bin(现在也 symlink 了 `ui/app-boot`),并仍然断言缺少配置时的非零退出码,遵循「真实入口路径意味着已发布产物」的防御模式。[extract-example-app-packages RFC](../architecture/2026-06-20-extract-example-app-packages.md) 中关于 bin 归属的事实已相应修订。
|
||||
每个 `bin.ts` 是一个精简的自执行组合,基于共享辅助函数加上各自特有的应用生命周期(ACP bin:replay 模式下跳过 env 加载与 stdin-EOF dispose;stdio bin:无额外逻辑)。bin 文件仍被排除在覆盖率之外且不导出任何内容;已发布产物的守卫不变——built-bin 冒烟测试仍在 node_modules 形状的临时目录中以原生 node 运行每个 bin(现在也符号链接了 `ui/app-boot`),并仍断言缺少配置时的非零退出码,遵循「真实入口路径即已发布产物」的防御模式。[extract-example-app-packages RFC](../architecture/2026-06-20-extract-example-app-packages.md) 中关于 bin 归属的事实已相应修订。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
### 为什么不保留重复?
|
||||
### 为何不保留重复?
|
||||
|
||||
bin 被定位为独立拥有的已发布产物,而新增一个包有固定开销(manifest、README、tsconfig reference、publint 表面积),与去重的代码行数相当。但创建 bin 的那份 RFC 从未权衡过应用间共享——它把三个示例 `start.ts` 副本合并**进**了 bin 就止步了;漂移是已观察到的事实;而覆盖率缺口的论点独立于去重论点:这是仓库中唯一被豁免于逐文件 100% 门禁的非平凡运行时逻辑。记录在案的回退方案(仅将纯逻辑提取为各应用模块)可以结束豁免,但会保留两个经验知识归属地。
|
||||
bin 被定位为独立拥有的已发布产物,而新增一个包(package)带来的固定开销(manifest(元数据清单)、README、tsconfig reference、publint 表面积)与去重的代码行数相当。但创建 bin 的那份 RFC 从未权衡过应用间共享的可能——它将三份示例 `start.ts` 副本合并进 bin 后便止步了;漂移是已观察到的事实;而覆盖率缺口的论据独立于去重论据:这是仓库中唯一免于逐文件 100% 门禁的非平凡运行时逻辑。记录在案的备选方案(仅将纯逻辑提取为各应用自己的模块)虽能终结豁免,但会保留两个知识归属地。
|
||||
|
||||
## 后果
|
||||
|
||||
- 启动胶水代码的变更(新增守卫、修复解析)只需落地一次,两个已发布 bin 自动继承;bin 之间不会再次漂移。
|
||||
- `dsh-app-boot` 保持依赖精简(cordis + loader/include 对)——它是启动机制,不是应用接口。
|
||||
- bin 自身的文件是近乎平凡的组合;所有带分支的逻辑都在覆盖率门禁之下。
|
||||
- 启动胶水代码的变更(新增守卫、修复路径解析)只需落地一次,两个已发布 bin 自动继承;bin 之间不会再次漂移。
|
||||
- `dsh-app-boot` 保持轻量依赖(cordis + loader/include 对)——它是启动机制,不是应用表面积。
|
||||
- bin 自身的文件几乎是平凡的组合;所有含分支的逻辑都在覆盖率门禁之下。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-04-tighten-hook-protocol-contract.md: df438516b836315902378afe7f4fd09e512c0966
|
||||
2026-07-04-tighten-hook-protocol-contract.zh.md: decc6ba86131f6d1930eb51a1267a9668d91dcb2
|
||||
2026-07-04-tighten-hook-protocol-contract.zh.md: 256da42993c8581e8bce861ecbfac22dbf5f0545
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# RFC:收紧 hook 协议契约——dialect、废弃字段、双重默认值与 lib 拥有的 `hook/result` 语义
|
||||
# RFC:收紧 hook-protocol 契约——dialect、废弃字段、双重默认值与 lib 拥有的 `hook/result` 语义
|
||||
|
||||
[English](2026-07-04-tighten-hook-protocol-contract.md) | 中文
|
||||
|
||||
@@ -8,25 +8,25 @@ Status: implemented
|
||||
|
||||
`dsh-hook-protocol`/bridge 契约中有四处遗漏了 [subagent-observe-enrich RFC](../feature/2026-06-30-subagent-observe-enrich.md) 所记录的纪律——该 RFC 因缺乏消费方而移除了 `agentType` 生命周期字段,以下四处未通过同样的检验:
|
||||
|
||||
1. **`HookDialect` 的 `'native'` 变体**(`packages/hooks/hook-protocol/src/types.ts`)没有任何生产者——bridge 只打 `'claude'` 和 `'codex'` 标记;唯一的 `'native'` 构造出现在 lib 自身的单元测试中。该字段自己的 JSDoc 将 `dialect` 定义为「执行它的 bridge」,而 native 不是 bridge:[interception-seams RFC](../feature/2026-06-30-interception-seams.md) 记录了 native 钩子不是一个 package,且「native 插件已经可以直接使用类型化的 Decisions」而无需持久化的 hook 日志;旗舰 native 插件的工作示例也正是如此断言的(完全没有 `hook/*` 事件)。
|
||||
2. **`HookOutput.suppressOutput`**(同一文件)被 codec 解析后在所有路径上都被丢弃:没有 bridge 分支、没有 merge fold、没有 warn、没有 deferred-list 行——在所有「被解析但未兑现」的同类字段中,它是唯一没有明确延期声明的(`updatedInput` → 一条 warn 日志加 [pre-tool-input-rewrite 提案](../../proposed/feature/2026-06-30-pre-tool-input-rewrite.md);`systemMessage` → 一条 warn 日志加 README deferred 行;`continue`/`stopReason` → 一个 `TODO(hook-continue-false)` 锚点加 `'stop'` decision 记录)。从结构上看根本没有什么可 suppress 的:hook 的 stdout 从不进入任何 transcript(文本记录)(上下文仅通过 `additionalContext` 流入;日志只记录 `decision`/`stderrSummary`),因此 hook 作者设置 `suppressOutput: true` 得到的是无声的空操作,连 warn 都没有。
|
||||
3. **`defaultTimeoutMs` 在两个 bridge 配置中被双重默认,使用浮动字面量**——一个 schema `.default(600_000)` 加一个 `?? 600_000` 回退(`packages/hooks/hooks-claude/src/index.ts`、`packages/hooks/hooks-codex/src/index.ts`),每个 bridge 为同一个协议级常量提供两个归属,两个 bridge 可能在共享默认值上悄然分歧。*本提案最初的补救——彻底删除该配置项——被 no-hardcoded-tunables 审计取代,后者保留了该配置项作为 bridge 拥有的显式配置(并在旁边新增了 `stderrSummaryMaxChars`);剩下需要修复的是字面量的归属。*
|
||||
4. **`hook/result` 的语义存在于两个 bridge 中(各一份),而非拥有该事件的 lib。** `summarize()`——stderr 截断规则——在 `packages/hooks/hooks-claude/src/index.ts` 和 `packages/hooks/hooks-codex/src/index.ts` 中逐字节相同,decision 字符串规则 `output.decision ?? (output.continue === false ? 'stop' : 'pass')` 也是如此;然而 `dsh-hook-protocol` 声明了 `hook/result`、将 `stderrSummary` 文档化为「已截断」却不拥有截断逻辑,将 decision 值文档化却不拥有映射逻辑。如果某个 bridge 漂移(不同的上限、不同的回退),共享的持久化事件的语义就会悄然分叉。
|
||||
1. **`HookDialect` 的 `'native'` 变体**(`packages/hooks/hook-protocol/src/types.ts`)没有任何生产者——bridge 只会标记 `'claude'` 和 `'codex'`;唯一构造 `'native'` 的地方是 lib 自身的单元测试。该字段的 JSDoc 将 `dialect` 定义为「运行它的 bridge」,而 native 并非 bridge:[interception-seams RFC](../feature/2026-06-30-interception-seams.md) 记录了 native hook 不是一个 package,且「native 插件已经可以直接使用类型化的 Decisions」而无需持久化 hook 日志;旗舰 native-plugin 示例也正是如此断言的(完全没有 `hook/*` 事件)。
|
||||
2. **`HookOutput.suppressOutput`**(同一文件)被 codec 解析后在所有路径上均被丢弃:没有 bridge 分支处理它、没有 merge fold、没有 warn、没有 deferred-list 行——在所有「被解析但未兑现」的同类字段中它是唯一没有明确延期声明的(`updatedInput` → 一条 warn 日志加 [pre-tool-input-rewrite 提案](../../proposed/feature/2026-06-30-pre-tool-input-rewrite.md);`systemMessage` → 一条 warn 日志加 README deferred 行;`continue`/`stopReason` → 一个 `TODO(hook-continue-false)` 锚点加 `'stop'` decision 记录)。从结构上看根本无物可抑制:hook stdout 从不进入任何 transcript(文本记录)(上下文仅通过 `additionalContext` 流入;日志只记录 `decision`/`stderrSummary`),因此 hook 作者设置 `suppressOutput: true` 得到的是无声的空操作,且无任何警告。
|
||||
3. **`defaultTimeoutMs` 在两个 bridge 配置中以浮动字面量双重默认**——schema 的 `.default(600_000)` 加上一个 `?? 600_000` 回退(`packages/hooks/hooks-claude/src/index.ts`、`packages/hooks/hooks-codex/src/index.ts`),一个协议级常量在每个 bridge 中有两个归属地,两个 bridge 可能在共享默认值上悄然分歧。*提案最初的补救措施是彻底删除该旋钮,但被 no-hardcoded-tunables 审计所取代:审计保留了该旋钮作为 bridge 拥有的显式配置(并在旁边新增了 `stderrSummaryMaxChars`);剩下要修的是字面量的归属地。*
|
||||
4. **`hook/result` 的语义存在于两个 bridge 中(各一份),而非拥有该事件的 lib。** `summarize()`——stderr 截断规则——在 `packages/hooks/hooks-claude/src/index.ts` 与 `packages/hooks/hooks-codex/src/index.ts` 中逐字节相同;decision 字符串规则 `output.decision ?? (output.continue === false ? 'stop' : 'pass')` 同样如此。然而 `dsh-hook-protocol` 声明了 `hook/result`、在文档中将 `stderrSummary` 描述为「已截断」却不拥有截断逻辑,记录了 decision 值却不拥有映射逻辑。如果某个 bridge 漂移(不同的上限、不同的回退),共享持久化事件的语义就会悄然分叉。
|
||||
|
||||
## 决策
|
||||
|
||||
`HookDialect` 是封闭的 bridge 集合,`'claude' | 'codex'`;`HookOutput` 移除不受支持的 `suppressOutput`。`hook/result.durationMs` 保留为持久化的审计计时,仅在快照中做归一化。参考默认值各只存在一处:`DEFAULT_HOOK_TIMEOUT_MS` 和 `DEFAULT_STDERR_SUMMARY_MAX_CHARS`。`HookResultRecord` 与 `appendHookResult` 为两个 bridge 统一拥有 stderr 摘要化和 decision 推导逻辑。`BLOCKING_EXIT_CODE` 为 codec 内部常量。
|
||||
`HookDialect` 是封闭的 bridge 集合:`'claude' | 'codex'`;`HookOutput` 移除了不受支持的 `suppressOutput`。`hook/result.durationMs` 保留为持久化的审计计时,仅在快照中做归一化。参考默认值各只存在一处:`DEFAULT_HOOK_TIMEOUT_MS` 与 `DEFAULT_STDERR_SUMMARY_MAX_CHARS`。`HookResultRecord` 与 `appendHookResult` 为两个 bridge 统一拥有 stderr 摘要化和 decision 推导逻辑。`BLOCKING_EXIT_CODE` 为 codec 内部常量。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
### 为什么不保留?
|
||||
### 为什么不保留它们?
|
||||
|
||||
不受支持的词汇(vocabulary)可以在真正有消费方时回归。`durationMs` 保留,因为持久化的审计计时独立于当前是否有读取者而有价值。Bridge 特有的 payload 构造留在各自 bridge 中,而共享的持久化事件归一化属于协议库。
|
||||
不受支持的词汇可以在真正有消费方时回归。`durationMs` 保留,因为持久化的审计计时独立于当前是否有读取方而有价值。Bridge 特有的 payload 构造留在各自 bridge 中,而共享持久化事件的归一化属于协议库。
|
||||
|
||||
## 验证
|
||||
|
||||
`HookDialect` 只包含 Claude 和 Codex,`suppressOutput` 在源码、解析字段文档和归一化逻辑中均不存在。`durationMs` 保留在事件和 fixture(测试前置数据)中,回放时做擦除。`600_000` 和 `500` 默认值各只在协议库中出现一次,per-hook 超时覆盖仍然生效,两个 bridge 的测试套件都验证了库拥有的 stderr 截断和 decision 规则。
|
||||
`HookDialect` 仅包含 Claude 和 Codex,`suppressOutput` 在源码、已解析字段文档和归一化逻辑中均不存在。`durationMs` 保留在事件和 fixture(测试前置数据)中,回放时做清洗。`600_000` 和 `500` 两个默认值各只在协议库中出现一次;per-hook 超时覆盖仍然生效;两个 bridge 的测试套件均验证了由库拥有的 stderr 截断和 decision 规则。
|
||||
|
||||
## 后果
|
||||
|
||||
`dialect`、`suppressOutput`、可调参数与语义变更在协议格式(wire format)和 golden 文件上不可见。代价是 `dsh-hook-protocol` 和两个 bridge 的代码变动——在预发布阶段这很廉价,且比让持久化事件语义的两份副本各自老化要廉价得多。
|
||||
`dialect`、`suppressOutput`、可调参数与语义的变更在协议格式(wire format)和 golden 文件中均不可见。代价是 `dsh-hook-protocol` 与两个 bridge 的代码变动——在预发布阶段这很廉价,且比让持久化事件语义的两份副本各自老化要廉价得多。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-04-trim-acp-bridge-unreachable-surface.md: 6decb494dcbfd348777577002187007597a8c374
|
||||
2026-07-04-trim-acp-bridge-unreachable-surface.zh.md: 77e22f75c96704ee0379c45d2ed23167df047324
|
||||
2026-07-04-trim-acp-bridge-unreachable-surface.zh.md: 9d588e6f1132869ead60586b4df6844400307863
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
# RFC:裁剪不可达的 ACP bridge 接口——品牌旋钮与 kind 嗅探回退
|
||||
|
||||
Status: implemented
|
||||
# RFC:裁剪不可达的 ACP 桥接层表面——品牌配置项与 kind 嗅探回退
|
||||
|
||||
[English](2026-07-04-trim-acp-bridge-unreachable-surface.md) | 中文
|
||||
|
||||
Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
`dsh-acp` 有两处接口在任何已交付的配置下都不可达:
|
||||
`dsh-acp` 有两处对外表面在任何已交付的配置中都不可达:
|
||||
|
||||
1. **`AcpConfig.agentName` / `agentVersion`**(`packages/ui/acp/src/index.ts`)。已交付的 app 包(package)只向 bridge 传入 `{ model }`(`packages/examples/acp-demo/src/index.ts`),因此唯一的生产配置面——叶子 `cordis.yml`——根本无法设置这两个旋钮;它们只能通过直接挂载 bridge 来设置,而只有单元测试这样做。所有快照 golden(包括 hook-matrix 场景)都固定了 schema 默认值(`deepseek-harness-acp` / `0.0.1`)。这对字段还带着一条活跃的 `TODO(double-default)`:字面量存在两份(schema 的 `.default(...)` 加 `??` 回退),TODO 要求选定一个归属。
|
||||
2. **`toolKindFor` 名称启发式**(同一文件)在通用回退路径中对 `bash*`/`read*`/`write`/`edit*` 工具名做了特殊处理。自 [render-intent union](../architecture/2026-07-02-tool-render-intent-union.md) 以来,这些分支匹配到的每个第一方工具都自带 `presentCall` 并携带其 kind,而没有 presenter 的生产工具(`subagent`、`subagent_fork`)本来就落入 `other`。这些分支在生产中可达的唯一情况是:某个工具拒绝自行呈现其调用——`presentCall` 抛出异常(containment 回退),或模型参数未通过工具 schema 导致 `defineTool` 的 `presentCall` 包装层返回 `undefined`(例如 `bash` 调用缺少必需的 `description`)——而 bridge 自身的模块文档明确声明了该启发式所违反的设计规则:「bridge 从不对工具名做特殊处理」。
|
||||
1. **`AcpConfig.agentName` / `agentVersion`**(`packages/ui/acp/src/index.ts`)。已交付的 app 包(`packages/examples/acp-demo/src/index.ts`)只向桥接层传递 `{ model }`,因此没有任何叶子 `cordis.yml`(唯一的生产配置表面)能设置这两个配置项;它们只有通过直接挂载桥接层才能设置,而只有单元测试这样做。所有快照 golden(包括 hook-matrix 场景)都固定了 schema 默认值(`deepseek-harness-acp` / `0.0.1`)。这对字段还带着一个活跃的 `TODO(double-default)`:字面量存在两份(schema 的 `.default(...)` 加 `??` 回退),TODO 要求选定一个归属。
|
||||
2. **`toolKindFor` 名称启发式**(同一文件)在通用回退路径中对 `bash*`/`read*`/`write`/`edit*` 工具名做了特殊处理。自 [render-intent union](../architecture/2026-07-02-tool-render-intent-union.md) 以来,这些分支匹配到的每个第一方工具都自带 `presentCall` 并携带其 kind,而没有 presenter 的生产工具(`subagent`、`subagent_fork`)本来就落入 `other`。这些分支只有在工具拒绝自行呈现调用时才在生产中可达:`presentCall` 抛出异常(容错回退),或模型参数未通过工具 schema 导致 `defineTool` 的 `presentCall` 包装层返回 `undefined`(例如 `bash` 调用缺少必需的 `description`)。而桥接层自身的模块文档明确声明了该启发式所违反的设计规则:"桥接层绝不对工具名做特殊处理"。
|
||||
|
||||
## 决策
|
||||
|
||||
在初始化时硬编码现有的握手标识 `{ name: 'deepseek-harness-acp', version: '0.0.1' }`,移除不可达的配置字段与重复默认值。在两处 presenter 回退中,将 `toolKindFor` 替换为中性的 `'other'`。正常的第一方呈现不受影响;格式错误或失败的呈现现在渲染一张诚实的通用卡片,而非从工具名推断 kind。初始化测试和快照固定握手标识;只有 `hook-codex-posttool-block` 中格式错误的调用改变了回退卡片的 kind。
|
||||
在初始化时硬编码现有的握手标识 `{ name: 'deepseek-harness-acp', version: '0.0.1' }`,移除不可达的配置字段与重复默认值。在两个 presenter 回退处,将 `toolKindFor` 替换为中性的 `'other'`。正常的第一方呈现不受影响;格式错误或失败的呈现现在会渲染一个诚实的通用卡片,而非从工具名推断 kind。初始化测试和快照固定握手标识;只有 `hook-codex-posttool-block` 中格式错误的调用改变了回退卡片的 kind。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
### 为什么不保留?
|
||||
|
||||
品牌旋钮可以在 app 包将其暴露给部署时回归。从未知工具名推断呈现方式违反了 render-intent 契约;中性回退卡片还能为格式错误的调用和损坏的 presenter 保留原始输入。
|
||||
品牌配置可以在 app 包将其暴露给部署环境时再回来。从未知工具名推断呈现方式违反了 render-intent 契约;中性回退卡片还能为格式错误的调用和损坏的 presenter 保留原始输入。
|
||||
|
||||
## 后果
|
||||
|
||||
除上述回退渲染的取舍外无其他影响——退化路径下,中性卡片比推断出的第一方卡片更易于诊断。
|
||||
除上述回退渲染的取舍外没有其他影响——退化路径下,中性卡片比推断出的第一方卡片更易于诊断。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-12-drop-unconsumed-skill-provider-events.md: 5ec9d201939b8f58334647353f599361bd2e58a0
|
||||
2026-07-12-drop-unconsumed-skill-provider-events.zh.md: 8ab2a5f55b2a1eed676b28a1ca7804d6237f63fd
|
||||
2026-07-12-drop-unconsumed-skill-provider-events.zh.md: 15dbfedb07afa36b677074c403812d0f164c8bd5
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
# RFC:移除无消费方的 skill 提供方事件
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-12-drop-unconsumed-skill-provider-events.md) | 中文
|
||||
|
||||
Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
skill(技能)注册表产出了两个通知事件,但在生产代码中没有任何监听方。生成的生产者/消费方矩阵以及精确的事件名搜索表明,`skill/provider-added` 和 `skill/provider-removed` 只出现在声明、发射点、测试、生成目录和行文中。
|
||||
skill(技能)注册表产出两个通知事件,但没有生产环境的监听方。生成的生产者/消费方矩阵以及对事件名的精确搜索表明,`skill/provider-added` 与 `skill/provider-removed` 仅出现在声明、emit 站点、测试、生成的 catalog 和行文中。
|
||||
|
||||
skill 发现按需读取当前提供方映射表,提供方注册同步清除已完成的目录缓存,await 后的修订检查防止陈旧的发现结果进入缓存。没有兄弟插件通过这些事件等待 skill 提供方,不同于 `subagent/provider-added` 的实际消费方(它容忍兄弟并发加载)。
|
||||
skill 发现按需读取当前的提供方映射表,提供方注册时同步清除已完成的 catalog,而 await 后的版本检查阻止了陈旧的发现结果进入缓存。没有兄弟插件通过这些事件等待 skill 提供方——与之形成对比的是活跃的 `subagent/provider-added` 消费方,它容忍兄弟并发加载。
|
||||
|
||||
`tools/change` 和 `system-prompt/change` 明确不在本提案范围内。既有的简化决策将它们保留为面向实时工具和提示词 UI 的有意观测点,且自引用的已挂载插件已在使用 `tools/change`。本提案同样不改动 `subagent/provider-added`/`removed`,因为 `tool-subagent` 有生产级的生命周期消费方。
|
||||
`tools/change` 与 `system-prompt/change` 明确不在本提案范围内。既有的简化决策将它们保留为面向实时工具和提示词 UI 的有意观测点,且自引用的已挂载插件已在使用 `tools/change`。本提案同样不改动 `subagent/provider-added`/`removed`,因为 `tool-subagent` 有生产环境的生命周期消费方。
|
||||
|
||||
## 决策
|
||||
|
||||
skill 注册表不再声明和发射提供方成员变更事件。提供方的注册与 dispose(资源释放)仍为 effect 拥有的直接状态变更,同步使已完成的目录缓存失效;查找与发现按需读取当前提供方映射表。测试通过提供方查找和收集的输出来观察清理行为,而非生命周期通知。
|
||||
skill 注册表不再声明和 emit 提供方成员变更事件。提供方的注册与 dispose(资源释放)仍为 effect 所有的直接状态变更,同步使已完成的 catalog 失效;查找与发现按需读取当前提供方映射表。测试通过提供方查找和收集到的输出来观察清理行为,而非依赖生命周期通知。
|
||||
|
||||
生成的事件目录、API 目录与生产者/消费方矩阵不再包含已删除的通知。skill 系统 RFC 和包文档通过 effect 拥有的直接状态及缓存失效契约来描述注册行为。
|
||||
生成的事件 catalog、API catalog 与生产者/消费方矩阵不再包含已删除的通知。skill 系统 RFC 与包文档通过 effect 所有的直接状态及缓存失效契约来描述注册行为。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
**为未来插件保留 skill 提供方通知。** 第三方插件可能想观察提供方的可用性,但直接提供方注册与按需查找才是扩展契约;当前没有消费方需要推送信号。如果未来出现兄弟加载竞态,可以像 subagent 注册表那样引入一个带有该消费方实际所需的身份与就绪语义的通知。
|
||||
**为未来插件保留 skill 提供方通知。** 第三方插件可能想观察提供方的可用性,但直接提供方注册与按需查找才是扩展契约;当前没有消费方需要推送信号。如果将来出现兄弟加载竞态,可以像 subagent 注册表那样,引入一个带有该消费方实际所需的身份与就绪语义的通知。
|
||||
|
||||
## 后果
|
||||
|
||||
生成的事件矩阵中不再有 `skill/provider-added` 或 `skill/provider-removed` 的行。skill 发现、直接运行时注册、提供方 effect 回滚/dispose、缓存失效与注册表查找清理均保留;随事件一起消失的是监听器触发的回滚。`tools/change`、`system-prompt/change` 以及已被消费的 subagent 提供方生命周期事件不受影响。
|
||||
生成的事件矩阵中不再有 `skill/provider-added` 或 `skill/provider-removed` 的行。skill 发现、直接运行时注册、提供方 effect 回滚/dispose、缓存失效与注册表查找清理保持不变;监听方触发的回滚随事件一起消失。`tools/change`、`system-prompt/change` 以及已被消费的 subagent 提供方生命周期事件不受影响。
|
||||
|
||||
预发布消费方失去 skill 提供方观测点,但仍保留贡献 skill 的两种方式:直接运行时注册与提供方注册。未来若有消费方需要实时的提供方可用性信息,须新增一个带有其实际所需的身份与就绪语义的专用通知。
|
||||
预发布消费方失去 skill 提供方观测点,但仍保留两种贡献 skill 的方式:直接运行时注册与提供方注册。未来若有消费方需要实时的提供方可用性信息,必须新增一个带有其实际所需的身份与就绪语义的专用通知。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-12-prune-unused-web-seam-fields.md: b4773c2706cf6d18ea4bb96720cd6c932cdf8942
|
||||
2026-07-12-prune-unused-web-seam-fields.zh.md: 1beb9e597c4b990f2c4aaaf3e34e71027c3f3fb7
|
||||
2026-07-12-prune-unused-web-seam-fields.zh.md: 2c18fbcb440ce85798c8f36cdc5dc649149d8ba9
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
# RFC:裁剪 web seam 中未使用的字段
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-12-prune-unused-web-seam-fields.md) | 中文
|
||||
|
||||
Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
web 能力携带了一组 request/result/status 值,每个已交付的实现都填充了它们,但没有生产消费方读取。`WebSearchResult.providerId`、`query` 和 `WebFetchResult.providerId` 是结果回显;`tool-web` 只格式化 content/sources/truncation 或 final URL/status/body/truncation,其他运行时也不读取这些字段。搜索提供方返回 `WebProviderStatus.reason`,但可用性检查只看 `available`,并有意输出一条通用的不可用诊断。
|
||||
web 能力携带的 request/result/status 值,虽然每个已交付的实现都会填充,但没有任何生产环境的消费方读取它们。`WebSearchResult.providerId`、`query`与 `WebFetchResult.providerId` 是结果回显;`tool-web` 只格式化 content/sources/truncation 或最终 URL/status/body/truncation,没有其他运行时读取这些字段。搜索提供方返回 `WebProviderStatus.reason`,但可用性检查只看 `available`,并有意输出一条通用的不可用诊断信息。
|
||||
|
||||
`WebFetchRequest.timeoutMs` 同样没有生产调用方设置。`tool-web` 只提供 URL,用工具定义的超时加 `exec.signal` 作为调用方截止时间,并依赖本地提供方的配置默认值作为兜底。这个未使用的按请求超时覆盖迫使 `web-fetch-local` 暴露 `maxTimeoutMs`、钳位两个超时源,并为没有产品路径能选中的优先级规则编写文档和测试。`WebExecContext` 则是另一个单字段包装层:每个调用方分配 `{ signal }`,每个提供方立即解包 `exec?.signal`;不存在第二个执行控制字段。
|
||||
`WebFetchRequest.timeoutMs` 同样从未被生产调用方设置。`tool-web` 只提供 URL,使用工具定义的 timeout 加 `exec.signal` 作为调用方截止时间,并依赖本地提供方的配置默认值作为兜底。这个未使用的逐请求覆盖迫使 `web-fetch-local` 暴露 `maxTimeoutMs`、对两个 timeout 来源做 clamp,并为没有任何产品路径能选中的优先级规则编写文档和测试。`WebExecContext` 则是另一个单字段包装层:每个调用方分配 `{ signal }`,每个提供方立即解包 `exec?.signal`;不存在第二个执行控制字段。
|
||||
|
||||
## 决策
|
||||
|
||||
web seam 省略搜索/抓取的 `providerId` 结果回显和搜索 `query` 回显;调用方本身已持有请求和提供方选择信息。提供方以返回布尔值的方法暴露可用性。抓取请求不再有按请求超时或 `maxTimeoutMs` 钳位;本地提供方保留其可配置的默认超时,工具保留自身的截止时间。提供方方法接收一个直接的可选 `AbortSignal`,而非单字段的 `WebExecContext` 包装层。
|
||||
web seam 移除搜索/抓取结果中的 `providerId` 回显和搜索的 `query` 回显;调用方本身已持有请求和提供方选择信息。提供方以返回布尔值的方法暴露可用性。抓取请求不再有逐请求 timeout 或 `maxTimeoutMs` clamp;本地提供方保留其可配置的默认 timeout,工具保留自身的截止时间。提供方方法直接接收一个可选的 `AbortSignal`,而非单字段的 `WebExecContext` 包装层。
|
||||
|
||||
所有 web 实现和面向模型的工具使用更小的契约。接口/实现/消费方的包拆分、提供方选择、来源引用、最终 URL/状态数据、截断报告与安全限制保持不变。
|
||||
所有 web 实现与面向模型的工具使用更精简的契约。接口/实现/消费方的包(package)拆分、提供方选择、来源引用、最终 URL/状态数据、截断报告与安全限制保持不变。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
**保留自描述结果、按请求截止时间和可扩展的执行上下文对象。** 结果回显可以帮助通用遥测,请求超时可以帮助受信的编程调用方,包装层对象为未来的控制留出空间。但这样的消费方或第二字段并不存在;在每个提供方中携带重复的身份信息、第二套截止时间策略以及包装/解包管道,使当前契约更难实现和解释。如果遥测或按调用的预算控制到来,它应当定义哪个截止时间获胜、在哪里观测提供方身份,以及多个控制是否足以证明上下文对象的存在。
|
||||
**保留自描述结果、逐请求截止时间与可扩展的执行上下文对象。** 结果回显可以帮助通用遥测,请求级 timeout 可以帮助受信的程序化调用方,包装层则为未来的控制字段留出空间。但目前不存在这样的消费方或第二个字段;在每个提供方中携带重复的身份标识、第二套截止时间策略以及包装/解包管道,使当前契约更难实现和解释。如果遥测或逐调用预算控制到来,届时应当定义哪个截止时间优先、在哪里观测提供方身份,以及多个控制字段是否足以证明需要一个上下文对象。
|
||||
|
||||
## 后果
|
||||
|
||||
保留下来的每个 web request/result 字段都被生产代码消费或为执行提供方请求所必需。工具可见的搜索/抓取输出、提供方回退、中止行为、配置的超时兜底、截断与引用仍被覆盖,无需请求超时优先级分支或执行上下文包装层。
|
||||
保留下来的每个 web request/result 字段,要么被生产代码消费,要么是执行提供方请求所必需的。工具可见的搜索/抓取输出、提供方回退、中止行为、可配置的 timeout 兜底、截断与引用仍然被覆盖,无需请求级 timeout 优先级分支或执行上下文包装层。
|
||||
|
||||
预发布的编程调用方失去结果来源回显和按请求的抓取截止时间。提供方仍有部署可配置的超时并尊重取消信号,因此这次精简移除的是可配置性而非安全边界。
|
||||
预发布阶段的程序化调用方失去了结果来源回显和逐请求的抓取截止时间。提供方仍具备部署级可配置 timeout 并尊重取消信号,因此这次精简移除的是可配置性,而非安全边界。
|
||||
|
||||
Reference in New Issue
Block a user