Merge remote-tracking branch 'origin/master' into worktree/acp-automation-protocol

# Conflicts:
#	.agents/notes/implemented/architecture/2026-06-14-session-persistence.md
#	.agents/notes/implemented/architecture/2026-06-20-package-hierarchy.md
#	.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.md
#	.agents/notes/implemented/feature/2026-06-14-acp-agent-client-protocol.md
#	.agents/notes/implemented/feature/2026-06-14-acp-multi-session.md
#	.agents/notes/implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md
#	.agents/notes/implemented/feature/2026-07-19-model-facing-goal-tools.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-19-model-facing-goal-tools.md
#	.agents/notes/implemented/feature/2026-07-19-model-facing-goal-tools.zh.md
#	.agents/notes/implemented/simplification/2026-07-04-trim-acp-bridge-unreachable-surface.md
#	docs/architecture.i18n.yaml
#	docs/cookbook/extension-cookbook.i18n.yaml
#	docs/cookbook/extension-cookbook.md
#	docs/cookbook/extension-cookbook.zh.md
#	docs/core-data-structures/approval.md
#	docs/core-data-structures/user-interaction.md
#	docs/event-producer-consumer.md
#	docs/persistence-catalog.md
#	docs/testing.md
#	docs/tool-catalog.md
#	examples/acp-agent/tests/fixtures/live-mode-switching-2026-07-07.session.jsonl
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/stdout.expected.jsonl
#	examples/acp-agent/tests/snapshots/permission-switching/session.jsonl
#	packages/goal/tool-goal/README.md
#	packages/ui/acp/README.md
#	packages/ui/acp/acp-feature-support.md
#	packages/ui/acp/src/index.ts
#	packages/ui/acp/tests/bridge.spec.ts
#	packages/ui/acp/tests/dispose.spec.ts
#	packages/ui/acp/tests/edges.spec.ts
#	packages/ui/acp/tests/turns.spec.ts
This commit is contained in:
Tianyi Cui
2026-07-24 23:43:10 +08:00
829 changed files with 23607 additions and 1815 deletions

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# 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: 80fe043e365352b17d2a5b3efa1ab8d396d311c4
2026-06-19-drop-mutable-session-summary.zh.md: e875d96d003367dbe8a671e6057a6b4a6a44ca74

View File

@@ -2,6 +2,8 @@
Status: implemented
English | [中文](2026-06-19-drop-mutable-session-summary.zh.md)
## Problem
The [session-persistence seam](../architecture/2026-06-14-session-persistence.md) split a session's out-of-log metadata into two types owned by `dsh-session`: an immutable `SessionHeader` (`version`, `id`, `createdAt`, `cwd?`, `parentSession?`) written once at creation, and a mutable `SessionSummary` (`updatedAt`, `title?`, `firstPrompt?`) "updateable without touching the append-only log". Their union was `SessionMeta = SessionHeader & SessionSummary`, and the abstract `SessionPersistence` service carried a seventh method — `update(id, summary)` — for rewriting the summary. Each backend implemented the mutable store its own way: JSONL wrote a separate atomic `.summary.json` **sidecar** beside the log (temp-write + rename, best-effort), SQLite kept `updated_at`/`title`/`first_prompt` **columns** bumped inside the append transaction.

View File

@@ -0,0 +1,35 @@
# Agent Note: 移除可变的会话摘要
Status: implemented
[English](2026-06-19-drop-mutable-session-summary.md) | 中文
## 问题
[会话持久化 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` 的全部表面积都是**死状态**
- `SessionPersistence.update()` **零个生产调用方**(所有 `.update(` 匹配都是 `createHash().update()` 或测试代码)。
- `firstPrompt` 在生产代码中**从未被读取**。
- `title` *确实*在 ACPAgent Client Protocol桥接层被读取过但读的是工具调用的 **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`
摘要原本要提供的一切,在消费方真正需要时都**可从仅追加日志中派生**`firstPrompt` = 第一条 `user/message`;近期度 = 最后一个事件的 `time` 或文件 mtime或者已经存在于不可变 header 中(`createdAt``cwd`)。唯一*不可*派生的是用户*手动编辑*的标题,但它从未实现,纯属 YAGNI如果未来真有功能需要它可以作为独立的日志事件或 header 字段回归。
这被记录为一项决策,因为它具有**持久性**(它同时收窄两个后端的公共服务契约和磁盘格式)、**争议性**summary 是有意为未来设计的结果,而非意外),也具有**意外性**(未来读者在原 Agent Noteagent 决策记录)描述 `SessionMeta` 的位置发现 `SessionHeader`,否则会追问 summary 为何消失)。它还为[共享持久化写入协调器](../architecture/2026-06-18-shared-persistence-write-coordinator.md)扫清障碍:不再有可变 summary 后,协调器的钩子接口不需要 `updateSummary` 钩子JSONL sidecar 与 SQLite 列之间的持久性分歧也随之消失,使两个后端的写入路径趋于一致。
## 无需迁移
这是未发布的软件(见[根 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) 中,本次变更即为其实例。
<!-- agent-note-format: alternatives-not-recorded (pre-format Agent Note) -->

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# 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: fce5c48ef6fcb1abc6e2fbb95dc7e83d22956660
2026-06-20-collapse-trace-only-session-events.zh.md: 5be2d77644c7b4084b892013a2c1e5b97a333d74

View File

@@ -2,6 +2,8 @@
Status: implemented
English | [中文](2026-06-20-collapse-trace-only-session-events.zh.md)
## Problem
The session event vocabulary includes first-class events that are not part of replayable conversation history and have little or no production consumption. `usage` is already present as a model stream chunk before the loop also appends a separate `usage` event. `error` duplicates the `turn/end { kind: 'error', message, code }` reason for loop failures; ACP settlement reads the turn-end reason, while message and UI projections skip the standalone `error` event.

View File

@@ -0,0 +1,44 @@
# Agent Note: 将仅用于追踪的会话事实折叠进承载性事件
Status: implemented
[English](2026-06-20-collapse-trace-only-session-events.md) | 中文
## 问题
会话事件词汇中包含一些一等事件,它们不属于可回放的对话历史,在生产环境中几乎没有消费方。`usage` 已经作为模型流分片存在,之后循环又追加了一个独立的 `usage` 事件。`error``turn/end { kind: 'error', message, code }` 中的循环失败原因重复ACPAgent Client Protocol结算读取轮次结束原因ACP 渲染忽略 `error` 事件,`deriveMessages()` 也跳过它。
这些事件让规范的 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` 之后不再有第二条会话日志错误记录。
用户对话日志包含渲染、恢复、审计和计费所需的全部信息,消费方无需协调重复的追踪行。
## 曾考虑的替代方案
**保留独立行作为遥测**——这些事件让规范 transcript 看起来比实际更像遥测数据,代价是增加了事件变体、不变式、测试、快照和持久化用例,却没有任何消费方使用。如果分析需求真正出现,正确的形态是投影辅助工具或带有独立保留策略的专用遥测存储,而非对话日志中的重复追踪行。
## 验证
`SessionEventMap` 不再包含独立的 `usage``error`agent loop智能体循环不再追加独立的 usage 事件,持久性失败通过 `turn/end { kind: 'error', step, message, code? }` 记录ACP 快照和持久化测试断言不存在仅追踪行;已录制的 fixture测试前置数据使用新事件形状会话格式版本固定为 `0`(后端按预发布格式策略拒绝任何非 `0` 的存储日志);文档说明了 token 用量和操作错误的观测位置。
## 后果
消费方不能再从规范日志中筛选独立的 `usage` 或步骤级 `error` 行,必须从承载它们的 assistant/failure 事件中读取这些事实。只有在实现 PRPull Request证明相同事实仍然存在的前提下这才是合理的简化否则独立事件应予保留。
## 实现说明
按提案落地,但有一处范围细化(遵循 AGENTS.md 所述“Agent Noteagent 决策记录)是提案,而非绝对真理”):
- **空内容 `assistant/message` 承载 usage无数据丢失。** 提案要求的证明(不会有已持久化的 usage 分片无处安放)落在 max-tokens 路径上:一个被截断的步骤有 usage 但内容为空(例如只有一个被丢弃的工具调用),以前会发出独立的 `usage`。现在它记录一个空内容的 `assistant/message { content: [], usage }`。为防止这向提供方 transcript 注入一个无内容的虚假 assistant 轮次,`deriveMessages()` 跳过空内容的 `assistant/message` 事件。回归测试断言 usage 仍被表示,且派生历史未被破坏。
**格式版本。** 此变更影响已持久化的事件,但预发布会话格式仍固定为 `0`,拒绝任何其他版本且不做迁移。`dsh-session` 拥有写入方和加载校验使用的常量。单调递增的格式版本从首次正式发布开始。
Usage 现在通过 `assistant/message.usage` 观测;操作错误的步骤编号通过 `turn/end.reason`(当 `kind: 'error'` 时)观测。`agent/error` 与日志用于实时诊断,保持不变。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# 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: a3c7c089d7dfa1a4cd6a891c416bf270dc7eff3d
2026-06-20-drop-unconsumed-llm-adapter-change-event.zh.md: d9129386dc95bae0716253fdf50236b25ebfdf75

View File

@@ -2,6 +2,8 @@
Status: implemented
English | [中文](2026-06-20-drop-unconsumed-llm-adapter-change-event.zh.md)
## Problem
`LlmService.registerAdapter()` emits `llm/adapter-change` on registration and disposal ([packages/llm/llm/src/index.ts](../../../../packages/llm/llm/src/index.ts)). Grepping `llm/adapter-change` across `packages/*/src` and `examples/*/src` finds only the declaration, emit sites, docs, and tests; no production listener subscribes to it.

View File

@@ -0,0 +1,36 @@
# Agent Note: 移除未被消费的 `llm/adapter-change` 事件
Status: implemented
[English](2026-06-20-drop-unconsumed-llm-adapter-change-event.md) | 中文
## 问题
`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`。保留一个没有监听器的适配器变更事件,只是在更小范围内重复[删除无用 summary](2026-06-19-drop-mutable-session-summary.md) 的模式。
这个事件并非零成本。`registerAdapter()` 在发出 `llm/adapter-change` 之前先 yield 回滚 disposer这样抛出异常的监听器会回退变更而非泄漏适配器条目包内还有针对该监听器抛出路径的测试。这种防御性排序保护的是一个只有测试才能触发的失败模式。
## 决策
只移除 `llm/adapter-change`:包括 `dsh-llm``interface Events` 中的声明、`ctx.emit('llm/adapter-change')` 调用,以及 `LlmService.registerAdapter` JSDoc 中“在注册和释放时发出 `llm/adapter-change`”的句子。`registerAdapter()` 的效应生成器为 HMR热模块替换/释放保留变更与回滚 disposer但移除仅因该事件而存在的监听器抛错回滚顺序。适配器 disposer 测试断言返回的 disposer 会移除适配器,不再订阅事件;监听器抛错回滚测试则随其测试对象一起消失。[docs/architecture.md](../../../../docs/architecture.md) 和 [packages/llm/llm/README.md](../../../../packages/llm/llm/README.md) 中的事件分类也在同一变更中更新。
## 曾考虑的替代方案
### 为什么不移除所有注册表变更事件?
由注册表通告变更的微内核是一种一致的约定。当 UI 能够实时刷新可用工具或提示词章节时,`tools/change``system-prompt/change` 可能会有用。本 Agent Noteagent 决策记录)在存在合理用户侧消费方的位置保留该约定,只删除当前及可能的未来消费方都不明确的适配器变更事件。
如果将来需要 LLM 适配器浏览器或动态模型选择器用到此信号届时再连同消费方一起重新引入并提供比「something changed」更清晰的 payload。
## 验证
`llm/adapter-change` 及其 emit 已消失,重新生成的 Cordis 目录保持新鲜HMR 安全性仍成立(释放贡献该适配器的 fiber 会移除它);`tools/change``system-prompt/change` 仍有文档与测试ACPAgent Client Protocol快照和无密钥 Headless Loader 冒烟则固定了未变的生产路径。
## 后果
- **移除一个已文档化的 emit 事件属于公开接口变更。** 它出现在分类体系表中,读起来像有意设计的 API。但「已声明且已发出」不等于「已被消费」——这与移除可变 summary 时的判断依据相同。分类体系表在同一个变更中更新,因此文档不会漂移。
- **注册表变更约定变得不均匀。** 这是可接受的,因为 LLM 适配器注册与工具或提示词段落不是同一层面的面向用户概念。不均匀但诚实,胜过统一但无用。
这是一个小裁剪,但它退役了一条守护着并不存在的消费方的正确性不变式。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# 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: b6b596e822b4bd6fd1bd891c336c622ad675ad45
2026-06-20-drop-unconsumed-llm-assembled-surfaces.zh.md: bafc5d3bc630d89c776bbcf53719b29223e1c90d

View File

@@ -2,6 +2,8 @@
Status: implemented
English | [中文](2026-06-20-drop-unconsumed-llm-assembled-surfaces.zh.md)
## Problem
`LlmService` ([packages/llm/llm/src/index.ts](../../../../packages/llm/llm/src/index.ts)) exposes three call surfaces over a model:

View File

@@ -0,0 +1,39 @@
# Agent Note: 移除未被消费的 LLM 组装便捷接口
Status: implemented
[English](2026-06-20-drop-unconsumed-llm-assembled-surfaces.md) | 中文
## 问题
`LlmService`[packages/llm/llm/src/index.ts](../../../../packages/llm/llm/src/index.ts))在模型之上暴露了三个调用接口:
- `stream()`:原始 `StreamChunk`,通过 `llm/stream` waterfall瀑布式事件分发。
- `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()`:将原始分片送入自己的 `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。
这属于[删除可变会话 summary](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` 游标字段,仅为支持按序增量产出而存在。`generate()` 拖带了 `GenerateResult``BlockAssembler.result()` 以及 `llm/generate` waterfall——在同一底层流之上的第二个拦截面。agent loop 对 assembler 的使用仅限于 `push()` / `message()` / `usage` / `finish`,不涉及流式 flush 或一次性服务组装。
## 决策
`stream()` 是唯一的公开 LLM 调用接口。移除 `streamBlocks``generate`、其事件/结果类型,以及仅被该路径使用的 assembler 辅助方法。适配器测试通过本地辅助函数对公开流进行组装;`BlockAssembler` 仅保留有生产消费方的操作。
## 曾考虑的替代方案
**保留 `generate()` 作为仅供测试的便捷方法**:否决。适配器测试通过共享 assembler 手动消费 `stream()`,走的是与生产完全相同的流式路径;一个唯一调用方只有测试的公开方法,正是 [drop-mutable-summary 先例](2026-06-19-drop-mutable-session-summary.md)所淘汰的死接口形态。未来如果有消费方需要不带增量的组装块,届时再为该消费方引入一个聚焦的辅助方法。
## 验证
`streamBlocks``generate``llm/generate` 及仅供它们使用的 assembler 辅助函数均已移除,且未产生新的无用导出;两个真实适配器都通过 `stream()` 和共享 assembler 接受测试循环行为保持一致ACPAgent Client Protocol快照预期输出未变README、架构文档和模块文档也不再提及已删除表面。
## 后果
- **从一个核心词汇包中移除了公开方法。** 未来如果有插件需要不带增量的组装块,它需要直接调用 `stream()` 并使用 `BlockAssembler`,或在有真实消费方时重新引入一个聚焦的辅助方法。鉴于预发布阶段「基础优先于预设未来」的立场([AGENTS.md](../../../../AGENTS.md)),现在正是裁剪仅供测试的公开接口的合适时机。
- **适配器测试变得更显式。** 它们失去了便捷的 `generate()` 包装层,但这是有益的压力:测试走的是与生产相同的流式路径。
- **waterfall 使用者失去 `llm/generate`。** 不存在生产监听者。未来的缓存/重试/日志插件应包装 `llm/stream`,它仍然是唯一的提供方调用路径。
改动规模不大,但它从 LLM 包中干净地移除了预设的接口面积,为生产和测试留下唯一一份模型调用契约。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# 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: 70596d908bb1d7559e876d93bce0e25874ce1ff0
2026-06-20-prune-dead-seam-methods.zh.md: 1fbc7dbf2f7abb5b17b1df10ef06e490b11335a4

View File

@@ -2,6 +2,8 @@
Status: implemented
English | [中文](2026-06-20-prune-dead-seam-methods.zh.md)
> **Implementation note:** Only `SessionPersistence.has()` and `.delete()` were removed. `BashExecutor.get()` and `.list()` remain because removing their one-line lookup surface required substantially more completion-tracking machinery in consumers. Their id branding is covered by the [branded-ids Agent Note](../architecture/2026-06-20-branded-ids.md).
## Problem

View File

@@ -0,0 +1,43 @@
# Agent Note: 从持久化 seam 中移除无用方法
Status: implemented
[English](2026-06-20-prune-dead-seam-methods.md) | 中文
> **实现说明:** 仅移除了 `SessionPersistence.has()` 和 `.delete()`。`BashExecutor.get()` 和 `.list()` 仍然保留,因为删除它们的单行查找表面会要求消费方增加显著更多的完成跟踪机制。其 id 品牌化由[品牌化 id Agent Noteagent 决策记录)](../architecture/2026-06-20-branded-ids.md)负责。
## 问题
能力 seam[接口 / 实现 / 消费方](../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)ACPAgent 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`,而非持久化。`has`/`delete` 的唯一调用者是契约测试套件和各后端的 spec。
`has()` 不仅未被使用:在 `loadStored(id)` 已负责持久化存在性检查的情况下,它仍增加了协调器的已跟踪/未跟踪探测和一个契约分支。`delete()` 则拖入每个后端都必须实现的 `deleteStored` 后端钩子。这属于[删除可变会话 summary](2026-06-19-drop-mutable-session-summary.md) 的同类模式:契约测试覆盖了两者,但已发布代码从不会询问“这个会话是否已持久化?”或删除某个会话。
## 决策
没有消费方使用的方法被移除——从抽象 seam、实现以及仅为覆盖它们而存在的契约/spec 测试套件中移除:
- `SessionPersistence.has()` / `.delete()` 已移除:抽象声明、协调器的 `has`/`delete`/`deleteCore`,以及 `PersistenceBackend.deleteStored` 钩子均消失jsonl 和 sqlite 都只是为了满足该钩子才实现 `deleteStored`,这些实现也一并移除)。后端属于[双后端](../architecture/2026-06-14-session-persistence.md)设计,其他方面不在范围内;删除它们为没有消费方的钩子所做的实现,是删除钩子的一部分,而非重新设计后端。
- 所有文档和源码注释引用都已更新为保留下来的四方法、仅含 `list()` 的契约——不仅包括字面上的 `has(`/`delete(`/`deleteStored` 拼写,还包括 `{@link has}`/`{@link delete}` JSDoc 链接和“六个公共方法”的计数——涉及 seam 和后端 README、[docs/architecture.md](../../../../docs/architecture.md)、[会话持久化](../architecture/2026-06-14-session-persistence.md)与[写入协调器](../architecture/2026-06-18-shared-persistence-write-coordinator.md) Agent Note以及协调器/后端 JSDoc。
## 曾考虑的替代方案
### 为什么不以「seam 应当完整」为由保留?
「持久化 seam 理应提供 delete」这种直觉是真实的——但它恰恰是预发布阶段所警惕的投机性完整[AGENTS.md](../../../../AGENTS.md):为正确的基础优化,而非为你并不拥有的假想调用者优化)。`delete()` 是一个方法,等消费方真正需要时再加回来即可:一个删除旧会话的会话管理 UI 会需要它——到那时再加,基于该 UI 的真实需求来设计(软删除?级联?确认?),而非现在猜测。
在有活跃消费方的情况下重新添加一个 seam 方法,成本低且设计更优,因为消费方锚定了契约。在无人使用的情况下保留它,意味着每个实现(以及未来的每个后端)都必须实现和测试一个无实际作用的方法。
## 验证
`has`/`delete`/`deleteStored` 已从持久化 seam、实现和契约测试套件中移除没有新增无用导出剩余操作`create`/`append`/`load`/`list`未受影响ACP `session/list` 和崩溃恢复行为完全一致seam README 和 `docs/architecture.md` 仅列出存留的方法。
## 后果
- **`delete()` 是产品最终会需要的操作。** 确实如此,但「最终」正是关键。现在删除、将来基于真实消费方重新添加,严格优于发布一份猜测的契约。两个后端各自减少了一个 `deleteStored` 实现,这是在本次范围之外的包中的有限改动。
- **低耦合。** 移除局限于持久化 seam + 实现 + 测试;没有跨包消费方引用被移除的方法,因此除文档外没有涟漪效应。
规模不大,但它将 seam 从「实现必须为无人提供什么」恢复为「恰好是消费方使用的东西」。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# 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: 81a21de30bfbc25688069efbffb21647889b1bdb
2026-06-20-public-agent-stop-surface.zh.md: 1f5a9ea3c1c3dd6f598c258574a81e52e291f3b7

View File

@@ -2,6 +2,8 @@
Status: implemented
English | [中文](2026-06-20-public-agent-stop-surface.zh.md)
> **Implementation note:** Only `abort()` was removed. `whenIdle()` remains because it is the public quiescence signal and safely handles waiter settlement and replacement-turn races; consumers should not reconstruct that behavior from status transitions.
## Problem

View File

@@ -0,0 +1,39 @@
# Agent Note: 保留单一公开停止原语
Status: implemented
[English](2026-06-20-public-agent-stop-surface.md) | 中文
> **实现说明:** 仅移除了 `abort()`。`whenIdle()` 予以保留,因为它是公开的完全停稳信号,能安全处理等待者结算与替换轮次竞态;消费方不应从状态转换中自行重建该行为。
## 问题
公共 `Agent` handle 暴露了两种相互重叠的在途工作停止方式:仅针对步骤的 `abort()` 和感知队列的 `cancel()`。前者保留已排队输入,后者则清除已排队和 steering中途引导工作并中止活动轮次。在生产中ACPAgent Client Protocol`session/cancel` 使用 `cancel()`,生命周期拥有者则通过 `AgentHandle.dispose()` 拆除 agent智能体。没有生产调用方需要一个裸的、仅针对步骤的 abort。
行为差异确实存在但已发布代码不需要较窄的操作。AgentLoop 改为为整个轮次拥有一个私有取消 holder。`cancel(cause?)` 携带类型化的 `user``parent` 原因,默认为 `user`,并丢弃待处理输入;释放仍是单独的生命周期中断。完整的归属与传播契约位于[显式轮次取消 Agent Noteagent 决策记录)](../architecture/2026-07-16-explicit-turn-cancellation.md)。
多余的公开接口使得循环不得不承载一个本质上属于内部拆卸的公开动词:`abort()` 必须被文档描述为有别于队列感知的取消,尽管 UI 取消几乎总是需要更广泛的操作。
## 决策
`cancel()``Agent` 上唯一的公共*停止*原语。生命周期拥有者使用 `AgentHandle.dispose()` 停止并注销 agent非拥有者使用 `cancel()` 放弃当前和已排队工作。实现保留一个私有轮次取消 holder但它不属于面向插件的 `Agent` 契约。
`whenIdle()` **保留**为公开的完全停稳观测原语agent 从 `running` 状态稳定后 resolve已处于 idle 时立即 resolvedispose 后等待循环退出)。它不是停止动词;它是非所有者在不 dispose agent 的前提下观测停止*完成*的方式。它的活跃消费方是 ACP 和通过此公开 seam 等待结算的 agent 测试(`packages/ui/acp/tests``packages/core/agent-loop/tests`);生产环境的 ACP 桥接层拥有其 agent 并通过 `AgentHandle.dispose()` 销毁它们,因此 `packages/ui/acp/src` 本身没有 `whenIdle()` 调用。
公共 `abort()` 已不存在disposer 仍为异步并等待循环停止。测试通过公共类型化原因和显式 signal seam 验证取消,而不会伸入 holder 内部。
## 曾考虑的替代方案
**同时移除 `whenIdle()`**:最初提案的形态,在对照代码验证前提后被推翻(上方的实现说明记录了完整过程):它是承重的完全停稳原语,迫使消费方手动观测 `running``idle` 转换正是防御性模式所警告的脆弱路径。
## 验证
`Agent` 不再暴露公开的 `abort()`,而 `cancel()``whenIdle()``steer()` 保留ACP 取消调用 `cancel()`;拆卸通过 handle disposal 等待完全停稳,`whenIdle()` 在完全停稳时为非所有者观测者 resolve测试套件覆盖取消和 disposal 作为两条受支持的停止路径。
## 后果
未来的插件无法通过公开接口仅中止当前模型/工具步骤而保留队列中的提示词。如果该用例变为现实需求,它应当带着一个具名消费方和更窄的契约回归。目前它是将私有循环机制保持公开的潜在泛化。
## 相关
本 Agent Note 只移除冗余的停止动词。轮次中途 steering 仍是一条有意保留的消息路径;完全停稳观察仍通过 `whenIdle()` 完成。最终公共表面包括 `send()``steer()``inject()``cancel()``whenIdle()`、status、options、会话和 identity。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# 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: 8c5bb74f2347fe0269cbb9c6504137de761ab919
2026-06-20-remove-agent-boundary-mirror-events.zh.md: 1bb166b7f4453b4f0605e2b1b9120bc5a05266b0

View File

@@ -2,6 +2,8 @@
Status: implemented
English | [中文](2026-06-20-remove-agent-boundary-mirror-events.zh.md)
<!-- Shipped in AMENDED, narrowed form: the four turn/step BOUNDARY mirrors are
removed; `agent/steering` and `agent/stream-chunk` were RETAINED here (they
are not durable-boundary mirrors — see "Scope: what is and isn't removed").

View File

@@ -0,0 +1,47 @@
# Agent Note: 停止将持久化边界镜像为 agent 事件
Status: implemented
[English](2026-06-20-remove-agent-boundary-mirror-events.md) | 中文
<!-- 以修订、收窄后的形式落地:
移除了四个轮次/步骤边界镜像;此处保留了 `agent/steering`
`agent/stream-chunk`(它们不是持久边界镜像——参见
“范围:移除什么、不移除什么”)。原始提案将 `agent/steering` 与其他项一并
移除;把它排除在外,使本 Agent Note 的范围保持在边界上。后来每个保留事件
都由各自的决策移除——参见
[停止将 token 流镜像为 agent 事件](2026-07-02-remove-stream-chunk-mirror.md)
和[移除 `agent/steering` 镜像 emit](2026-07-04-remove-agent-steering-mirror.md)。 -->
## 问题
循环在 `SessionEvent` 中记录规范 transcript文本记录同时还发出一组并行的实时 `agent/*` 边界镜像事件:`agent/turn-start``agent/turn-end``agent/step-start``agent/step-end`。这些镜像迫使消费方在同一持久事实的两个真源之间做选择。ACPAgent Client Protocol已经为面向编辑器的 transcript 选择会话日志因为它是唯一持久、可重放的记录消费实时镜像需要把它的时序与日志中已经存储的边界进行调和。stdio UI 是唯一仍从镜像事件渲染轮次边界的生产消费方;它已经从 `session/event` 渲染工具调用和结果。
这种重复并非零成本。每次生命周期变更都需要同时更新会话事件、镜像事件、文档、不变式、测试和快照预期。重复的边界事件还使失败排序变得微妙:一个轮次可能在实时 `agent/turn-end` 监听器运行之前就已被持久化关闭,因此边界之后的监听器失败在日志中已没有合法位置可以插入,只能带外上报。
## 决策
`session/event` 作为唯一的实时边界/transcript文本记录流。需要渲染轮次、工具调用、工具结果、助手消息和持久化边界的消费方统一订阅 `session/event`,从持久化层使用的同一套事件词汇中派生 UI。
四个持久边界镜像——`agent/turn-start``agent/turn-end``agent/step-start``agent/step-end`——已从 agent智能体事件分类中移除。希望在边界处取得 agent handle 的 UI 会保留来自 `agent/created`/`agent/disposed` 的实时目标对象,并直接比较其会话;`dsh-ui-stdio` 据此为应用拥有的 agent 标记 `[main turn N]` 头部,其他会话则渲染其持久 id。规范记录仍是事件溯源会话日志。
步骤镜像(完全没有消费方)最先在[事件域语义 Agent Noteagent 决策记录)](../architecture/2026-06-30-event-domain-semantics.md) 中移除;该 Agent Note 当时以 stdio UI 需要在轮次边界取得 `Agent` handle 为由,保留了轮次镜像。本 Agent Note 完成余下工作:`dsh-ui-stdio` 是可随时丢弃的测试 REPL其渲染可以自由变化因此“ui-stdio 需要它”并不是保留镜像的理由——它读取 `session/event`,只保留自己的实时目标对象。
## 范围:移除什么、不移除什么
已移除(持久边界镜像——每项都以会话日志为权威):`agent/turn-start``agent/turn-end``agent/step-start``agent/step-end`
保留——不是持久边界镜像,因此不在本决策范围内:
- `agent/steering`——不是边界,因此不在本决策范围内(原始提案将其一并移除;在此会造成范围蔓延)。它镜像持久的 `steering/message` 控制记录,而非边界,后来由自己的后续决策移除:[移除 `agent/steering` 镜像 emit](2026-07-04-remove-agent-steering-mirror.md)。
- `agent/stream-chunk`——实时 token 流。不在本决策范围内(它镜像持久的 `assistant/chunk`,而非边界),后来由自己的后续决策移除:[停止将 token 流镜像为 agent 事件](2026-07-02-remove-stream-chunk-mirror.md)。
- `agent/created``agent/disposed``agent/status``agent/error``agent/queued`——不属于 transcript 数据的生命周期/控制事件。尤其是 `agent/queued`,它是在任何持久事件存在之前触发的 inbox 确认(取消的排队工作可能永远不会进入日志),所以有意只保留为实时事件。
## 曾考虑的替代方案
- **将 `agent/steering` 一并移除**——原始提案的形状;作为范围蔓延被排除:它镜像持久的 `steering/message` 控制记录,而非边界,后来由[自己的决策](2026-07-04-remove-agent-steering-mirror.md)移除(`agent/stream-chunk` 也由[流分片镜像 Agent Note](2026-07-02-remove-stream-chunk-mirror.md)移除)。
- **为 stdio UI 保留轮次镜像**——[事件域语义 Agent Note](../architecture/2026-06-30-event-domain-semantics.md) 的原始立场;在此否决,因为 `dsh-ui-stdio` 是可随时丢弃的测试 REPL而非承载关键约束的消费方并且它改为根据 `session/event` 加自己的实时目标对象渲染边界。
## 后果
插件不能再从便捷的 `Agent` 优先事件观察轮次/步骤边界。它需要订阅 `session/event`;如果需要实时对象,则通过 `ctx.agents` 解析共享 id或保留自己已经拥有的对象。这是可以接受的取舍边界消费方不应依赖可能与持久日志发生漂移的第二条事件 feed。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-06-20-unify-agent-and-session-id.md: c55152f4f13fe0acb530503e84f465799007cff7
2026-06-20-unify-agent-and-session-id.zh.md: 1fa2fe1fd64478bfe17c590e45abd0cf8281cbe4

View File

@@ -2,6 +2,8 @@
Status: implemented
English | [中文](2026-06-20-unify-agent-and-session-id.zh.md)
## Problem
A live agent/session pair needs one identity for registry routing, event sourcing, and persistence. Giving the factory independent `agentId` and `sessionId` inputs would permit pairings no production path can use, while forcing every consumer to choose or translate between two names for the same lifecycle.

View File

@@ -0,0 +1,40 @@
# Agent Note: 统一 agent id 与会话 id
Status: implemented
[English](2026-06-20-unify-agent-and-session-id.md) | 中文
## 问题
一个实时 agent智能体/会话对需要使用同一 identity 完成注册表路由、事件溯源和持久化。让 factory 接受相互独立的 `agentId``sessionId` 输入,会允许任何生产路径都无法使用的配对,同时迫使每个消费方为同一生命周期在两个名称之间选择或转换。
ACPAgent Client Protocol对两种 identity 使用相同值。Stdio 和钩子也在会话事件流上工作,并且直接需要对应的实时 agent没有生产路径会把一个实时 agent 对象重新附着到多个会话,或通过多个 agent id 驱动一个会话。
[agent 范围运行时](../architecture/2026-07-12-agent-scope-runtime-design.md)使用同一个 `AgentCreationTransaction` 执行创建和恢复agent/会话条目共享相同的最终条目冲突规则。第二个 identity 并不代表单独的存活性、回滚或完全停稳;它只会围绕同一事务增加 API 与转换状态。
会话 identity 同样只有一个归属,即 `Session.header.id``Session.id` 是派生访问器,而非需要重复验证的独立状态。
## 决策
agent 的注册表 id 等于其会话 id。`CreateAgentOptions` 接受一个 `sessionId`,同时用于两个最终注册表条目;恢复时以 `resumeSessionId` 注册 agent进程内 subagent 创建使用子会话 id`Session.id` 则派生自 `header.id`。远程 ACP 运行没有本地 agent/会话对:它保留一个由父项铸造的生命周期 id而子服务器线协议内的会话 id 仅用于 ACP 调用。现有创建事务、最终条目冲突检查和精确条目分离语义保持不变;唯一职责是在本地 id 之间转换的 map 与字段已经消失。
配置驱动路径保留 `agents[].id` 作为稳定配置标签,而非实时路由 identity。普通的全新启动会铸造组合 id `${label}-session-${randomUUID()}`,使持久重启不会冲突。耦合应用可以预先铸造并传入精确的 `sessionId`首次使用时创建它而当持久化服务已经存在时AgentLoop 重新挂载会在同一 identity 下恢复已物化历史。`resumeSessionId` 则要求已有的持久化 identity。两个精确 id 输入互斥。Stdio 使用“恢复或创建”形式,使配置创建的 agent 和 UI 在循环重载之间共享一个不透明 identity而不是根据前缀猜测。日志可以使用稳定标签而所有实时与持久查找都使用同一个 `SessionId`
`agent/created``agent/disposed` 保留。它们是成对的发布生命周期事件,而非 identity 别名;以后若发现没有消费方并要移除,必须先重新搜索,再提出独立提案。
## 曾考虑的替代方案
**保持路由与日志 identity 分离。** 稳定的配置标签加全新的持久对话确实有用,但不需要两个实时 identity标签可以继续作为配置/显示元数据,而每次运行的组合 `SessionId` 负责路由和持久化。保留两个 id 会让转换 map 持续存在,允许不可能的配对,却不会增加生命周期功能。
## 验证
- Agent 创建/恢复和 subagent 创建只携带一个 identity`Session` 也只在一个位置存储它。
- 创建事务继续覆盖最终条目冲突、精确条目分离、回滚和完全停稳,无需 identity 特有的生命周期状态。
- ACP、stdio、钩子、bash 归属、持久化和 lineage 直接使用共享 `SessionId`。ACP subagent 后端在父命名空间中铸造其生命周期 id因为子服务器返回的会话 id 仅在服务器本地有效ACP bridge 根据正向会话 map 验证精确的 `Agent` 归属JSON-RPC 只转发生命周期事件中由服务快照保存的 `local` 标记为 true 的事件,从带范围的事件 carrier 取得委托父项,并且不保留子 identity 或 lineage cache。
- 配置驱动的恢复还是创建策略是显式的,并在持久化重启场景下得到覆盖。
- 生产监听器搜索确认保留 `agent/created`/`agent/disposed` 及其发布语义。
- 类型检查、覆盖率、快照、doc-sync、module-graph 校验、构建与 hygiene 全部通过。
## 后果
这排除了潜在的多会话 actor 和会话交接设计,并使由客户端选择、已持久化的会话 identity 成为注册表 identity。如果独立路由 identity 成为真实需求,就需要显式的生命周期设计,而不是由调用方提供一对不受约束的值。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# 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: d496f273e2635624e0ab8e70e06c8729563c5466
2026-06-26-fsspec-style-fs-seam.zh.md: 18e4be5177f593253dc100a864b6c741904a90b2

View File

@@ -2,6 +2,8 @@
Status: implemented
English | [中文](2026-06-26-fsspec-style-fs-seam.zh.md)
## Problem
The filesystem capability from [filesystem-capability-seam](../architecture/2026-06-17-filesystem-capability-seam.md) currently makes one abstract `FileSystem` service own two different jobs:

View File

@@ -0,0 +1,130 @@
# Agent Note: 拆分文件系统 seam——提供方文本变更操作与 `dsh-fs-policy` 插件
Status: implemented
[English](2026-06-26-fsspec-style-fs-seam.md) | 中文
## 问题
[文件系统能力 seam](../architecture/2026-06-17-filesystem-capability-seam.md)中的文件系统能力目前让一个抽象 `FileSystem` 服务同时负责两项不同工作:
1. **提供方操作**——解析目标、stat/版本元数据、文本读取/流式读取、原子写入,以及受保护的字面编辑。
2. **面向 agent智能体的策略**——行窗口、字面编辑语义,以及读后写/编辑的观测状态。
这导致每个未来的后端都要重新实现面向模型的读取语义和观测策略。`readPage` 返回带行号的行和视图元数据;基础服务按 owner 存储文件状态,并区分 `full``partial` 读取。这些是有用的策略,但它们不是文件系统提供方的原语。字面文本变更则不同:版本守卫、字面匹配、歧义检测与原子重写必须留在提供方的变更边界内,但当前的 `applyEdit` 命名及其周围的 seam 将这一提供方操作绑定到了旧的读后编辑策略形状上。
这还造成了一个真实的用户体验死胡同:窗口化读取记录 `view: partial`,而 partial 视图无法授权 `edit`。一个模型读取了大文件的第 100-150 行,如果想编辑第 120 行,就必须先获取一次 `full` 读取,而对于超过读取上限的文件这可能做不到。字面编辑实际上只需要新鲜度:被匹配的字节仍然来自模型所读取的那个版本即可。
旧 Agent Noteagent 决策记录)已经推迟了独立的 `@deepseek-ai/dsh-fs-policy` 包。本 Agent Note 构建该层,使 `ctx.fs` 保持接近 fsspec 风格的存储原语(`info`/`cat`/`open`),但不把它变成完整的 fsspec。
## 决策
将栈拆为四层:
```text
tool dsh-tool-fs model-facing schemas + read windowing + text rendering; the EXECUTOR (reads/writes/edits via ctx.fs, dispatches the fs/* events)
policy dsh-fs-policy observed-state + read-before-edit + write/edit freshness, contributed through the fs/* event gate (no service)
provider seam dsh-fs ctx.fs: text IO + atomic mutation primitives (optional version guard)
provider dsh-fs-local local implementation of ctx.fs
```
`dsh-tool-fs` 保持相同的面向模型的 `read`/`write`/`edit` schema。它是执行器注入 `fs`(不是策略服务)并直接访问 `ctx.fs`,拥有读取窗口化逻辑,并分发 `fs/*` 事件以便 `dsh-fs-policy` 进行门控和记录。
本 Agent Note 决定了四层拆分、提供方契约和新鲜度策略。随后,[事件门禁 Agent Note](../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 加受保护的文本变更:
```ts ignore-check
abstract resolve(path: string, opts?: { cwd?: string; signal?: AbortSignal }): Promise<FsTarget>
abstract stat(target: FsTarget, signal?: AbortSignal): Promise<FsInfo | undefined>
abstract readText(target: FsTarget, signal?: AbortSignal): Promise<string>
abstract streamText(target: FsTarget, signal?: AbortSignal): Promise<AsyncIterable<string>>
abstract writeText(target: FsTarget, content: string, expected: FsWriteIntent, signal?: AbortSignal): Promise<FsWriteOutcome>
abstract editText(target: FsTarget, edit: FsEditRequest, expected: { version: FsVersion }, signal?: AbortSignal): Promise<FsEditOutcome>
interface FsInfo {
version: FsVersion
type: 'file' | 'directory' | 'other'
size?: number
}
type FsWriteIntent =
| { kind: 'createIfAbsent' }
| { kind: 'replaceIfVersion'; version: FsVersion }
```
`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`。
`editText` 是提供方级别的受保护文本变更。启用守卫时,它首先验证目标仍以 `expected.version` 存在,然后读取当前文本、应用字面替换并原子写入。陈旧检查必须在字面匹配之前发生,这样基于旧读取的编辑会报告 `FS_STALE_VERSION`,而不是对更新内容进行匹配后报告 `FS_EDIT_NOT_FOUND` 或 `FS_AMBIGUOUS_EDIT`。将此原语保留在提供方 seam 上,保持了后端本地锁定的能力,也让未来的远程后端能够实现原生的 compare-and-edit而无需策略层拉取整个文件。
这是一个*文本存储* seam刻意比字节级 fsspec`cat`/`open` 返回原始字节高半个层次。UTF-8 解码、二进制/NUL 拒绝、受保护的全文件写入和受保护的字面文本编辑都在提供方内完成,因此策略层从不接触原始字节、不重新实现跨分片解码、也不将陈旧检查与变更临界区分离。面向模型的概念仍然不下沉到提供方:行窗口、带行号的行、渲染的页脚、观测状态存储都不会泄漏下去。
从 `dsh-fs` 删除:`readPage`、`FsExpectation`、`FsView`、`FsStateSource`、`FsReadRequest`、`FsTextLine`、行/窗口常量、`formatReadBody` 和 observed-state `WeakMap`。`applyEdit` 由更窄的提供方原语 `editText` 取代,其契约是带版本守卫的字面文本变更,而非策略层读取授权。`FS_PARTIAL_OBSERVATION` code 也从 `FsErrorCode` 分类中移除:新鲜度授权没有部分/完整之分,因此没有任何路径会抛出它。`FsTargetKey` 和 `FsVersion` 按现有[品牌化 id Agent Note](../architecture/2026-06-20-branded-ids.md) 成为品牌化不透明 id。
## 策略契约
`@deepseek-ai/dsh-fs-policy` 是插件,而非服务:它不注册任何 `ctx.*` 键,也不注入任何内容。它拥有不应位于 `FileSystem` 提供方基类上的写入/编辑新鲜度策略和 observed state否则沙箱/远程后端会继承不该由其承载的面向模型观察策略)。它通过执行器分派的 `fs/*` 事件门禁贡献该策略。(本 Agent Note 最初提议带有 `read`/`write`/`edit` 方法的具体 `ctx.fileContext` 服务;[事件门禁 Agent Note](../architecture/2026-06-26-file-context-as-event-gate.md) 将其细化为本文所述插件,使工具永远不会在方法层与策略耦合。)
观测状态以 `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`。
该插件不做任何文件系统 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/*` 事件。
每个变更操作先分发其 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` 没有监听器。加载插件后即可叠加读后写/编辑策略。
## 并发边界
进程内更新是安全的:本地后端保持既有的按目标变更锁,因此版本检查-然后-rename 是串行化的,失败的更新会看到 `FS_STALE_VERSION`。
进程内创建由同一个按目标变更锁保护:两个调用者以 `createIfAbsent` 竞争时串行化,一个创建成功,另一个看到目标已存在并收到 `FS_NOT_OBSERVED`。跨进程创建仅为尽力而为;本地的 stat-then-rename 守卫无法在所有未来后端上提供可移植的排他创建保证。
跨进程写入是尽力而为的新鲜度加原子替换:`mtime:size` 通常能捕获编辑器保存,但同一 tick 相同大小的写入可能遗漏;原子的 temp+rename 防止文件撕裂但不能防止所有丢失更新。
## 取代
本 Agent Note 推翻[文件系统能力 seam](../architecture/2026-06-17-filesystem-capability-seam.md)中的两项决策,并收窄第三项:
- 读后写/编辑策略从 `ctx.fs` 移出,进入 `dsh-fs-policy` 插件(通过 `fs/*` 事件门控)。
- 文本读取不再返回后端编号的行记录或 `full`/`partial` 视图;授权基于版本新鲜度,因此窗口化读取在文件未变时即可授权编辑。
- 字面编辑不再位于旧的 `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` 工具的读取记录观测状态;直接 `ctx.fs` 读取不记录);`dsh-fs-policy` 具有 HMR热模块替换/dispose资源释放覆盖率。
## 后续扩展
后来,[为文件系统 seam 添加直接目录列表](../architecture/2026-07-03-filesystem-directory-listing-seam.md)进一步扩展了该 seam。该后续工作单独跟踪使本 Agent Note 的验收标准继续描述最初落地的 fsspec 风格改造。
## 曾考虑的替代方案
- **字节级 fsspec`cat`/`open` 返回原始字节)**:否决。该 seam 刻意定位为文本存储,比字节级高半个层次,这样 UTF-8 解码、二进制/NUL 拒绝和受保护的文本变更只在提供方实现一次,策略层从不接触原始字节,也不将陈旧检查与变更临界区分离。
- **具体的 `ctx.fileContext` 方法服务**——本 Agent Note 最初的策略形状;[事件门禁 Agent Note](../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`。这比旧的视图检查更弱,但避免了大文件无法编辑的问题;提示词引导仍然不鼓励盲目的全文件替换。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# 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: 5dd816940a4b2c2b63980e01f1bd4e0aac53a3e2
2026-07-02-remove-stream-chunk-mirror.zh.md: ca65a3c2ad46d14835416c14d0087ef542de03bb

View File

@@ -2,6 +2,8 @@
Status: implemented
English | [中文](2026-07-02-remove-stream-chunk-mirror.zh.md)
## Problem
The loop records every model token delta as a durable `assistant/chunk` session event AND emitted a parallel live `agent/stream-chunk` Cordis event carrying the identical data. In `packages/core/agent-loop/src/loop.ts` the two sat one line apart:

View File

@@ -0,0 +1,47 @@
# Agent Note: 停止将 token 流镜像为 agent 事件
Status: implemented
[English](2026-07-02-remove-stream-chunk-mirror.md) | 中文
## 问题
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 })
chunkSeqs.push(chunkEvent.seq)
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)`)。
这与[移除边界镜像](2026-06-20-remove-agent-boundary-mirror-events.md)为轮次/步骤边界消除的重复相同:消费方面对同一持久事实的两个真源,每次变更都必须同时触及两者。该 Agent Noteagent 决策记录)没有把分片流一并纳入,而是推迟处理(“`assistant/chunk` 持久化仍承载关键约束,所以以后可以将分片流作为镜像评估,但那是一项独立决策”)。本 Agent Note 就是那项独立决策。
推迟所依赖的前提已经明确:分片持久化是权威的,且将保留。停止持久化分片、仅保留瞬态实时流事件的提案已被[否决](../../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
**消费方。** 唯一重要的生产消费方——ACPAgent Client Protocol桥接`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`)之间共享,分片与边界在该标志上竞争时没有确定的顺序;单一监听器按追加顺序处理,使交错变为确定性的。
## 范围
移除:`agent/stream-chunk`。
未触及:
- `assistant/chunk`(持久会话事件)——权威 token 流,原样保留。本 Agent Note 移除的是实时镜像,而非持久化(移除持久化的提案已单独遭到拒绝——见上文)。
- `agent/steering`——本决策未触及(它是控制信号,不是 token 流)。其持久孪生事件是 `steering/message`,镜像发射由其自身的后续 Agent Note 移除:[移除 `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` 序列。在此前提确定后,实时发射才是配对中冗余的那一半。
## 后果
插件不能再从 `Agent` 优先事件观察 token 增量。它需要订阅 `session/event`、过滤 `assistant/chunk`,并在需要时通过 `ctx.agents.get(session.id)` 直接查找对应的实时 handle。没有生产消费方需要在分片时刻取得实时 `Agent`;这与移除边界镜像所作的取舍相同,均可接受。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# 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: 566803ab5b9f213b7dc87fcf779ed7a963d988fd
2026-07-04-drop-image-content-block.zh.md: 667ddbc1b7bce2a276fe7528f46d3e43916bad83

View File

@@ -2,6 +2,8 @@
Status: implemented
English | [中文](2026-07-04-drop-image-content-block.zh.md)
## Problem
`ImageBlock` (`packages/llm/llm/src/types.ts`) had no production producer, and every consumer on every path DROPPED it: the DeepSeek adapter's serializer skipped image blocks (a documented MVP limitation), the pi-ai converter skipped them as unrepresentable, and the compaction estimator charged a flat token constant and rendered `[image]`. ACP independently rejected image prompt content. An `ImageBlock` constructed then would silently vanish from the provider wire — the vocabulary advertised a capability no path honored, which is the silent-data-loss shape AGENTS.md's defensive patterns warn against. The only constructors anywhere were tests pinning the skip/drop/estimate branches.

View File

@@ -0,0 +1,29 @@
# Agent Note: 移除 `image` 内容块,直到有路径能真正处理它
Status: implemented
[English](2026-07-04-drop-image-content-block.md) | 中文
## 问题
`ImageBlock``packages/llm/llm/src/types.ts`没有任何生产环境的生产者而每条路径上的每个消费方都将其丢弃deepseek 适配器的序列化器跳过 image 块(这是文档中注明的 MVP 限制pi-ai 转换器因无法表示而跳过ACPAgent Client Protocol编解码器既不宣告图像提示词能力、也不向外转发 image 块并且会拒绝入站的图像提示词内容压缩compaction估算器对其收取一个固定 token 常量并渲染为 `[image]`。此时构造的 `ImageBlock` 会在协议格式wire format上静默消失——词汇宣告了一种没有任何路径兑现的能力这正是 AGENTS.md 防御性模式所警告的静默数据丢失形态。唯一的构造调用出现在测试中,用于覆盖 skip/drop/estimate 分支。
## 决策
移除 `ImageBlock`、其 map 条目以及适配器、ACP 渲染和压缩中的 image 专用分支。在同一个变更中更新所属的词汇文档与生成的引用。未知扩展块仍然覆盖默认分支ACP 继续独立于 harness 词汇拒绝入站的图像提示词内容。
## 曾考虑的替代方案
### 为什么不保留?
当适配器、ACP 和压缩全部支持 image 时,`ContentBlockMap` 可以重新引入。保留一个唯一实现就是拒绝的核心类型,等于宣告一个不可用的对外服务接口;移除后,生产者会立即得到编译期错误。
评审中记录的回退方案(假如评审决定保留该槽位):保留 `ImageBlock`,但将所有静默跳过替换为显式拒绝,并在词汇文档中记录该策略——静默丢弃是唯一没有辩护者的状态。评审最终决定移除;此回退方案作为文档化的替代方案保留,以备该槽位在完整功能就绪之前回归。
## 验证
除 Agent Noteagent 决策记录)之外,没有任何地方构造 harness `ImageBlock`。ACP 独立的入站图像拒绝路径仍有测试适配器、codec 和压缩的默认分支则使用插件定义的块类型覆盖。
## 后果
日后重新添加核心词汇类型需要同时改动多个包package——但这种协调变更本就是真正的多模态功能所需的形态适配器映射、ACP 能力宣告、压缩定价),而当前并不存在需要保留的实现。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# 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: 06fa6c1c539f9ff0cfabf76bc41c53800bd46c8c
2026-07-04-drop-inert-request-knobs.zh.md: 42aadde2b279a453fac9444060b8ac34bf9e3c8b

View File

@@ -2,6 +2,8 @@
Status: implemented
English | [中文](2026-07-04-drop-inert-request-knobs.zh.md)
## Problem
Two request-contract knobs rode the whole request pipeline, yet neither could do anything:

View File

@@ -0,0 +1,35 @@
# Agent Note: 移除 `GenerateOptions.prefill` 与 `ToolSchema.strict`——无端到端可用路径的请求旋钮
Status: implemented
[English](2026-07-04-drop-inert-request-knobs.md) | 中文
## 问题
两个请求契约旋钮贯穿了整条请求流水线,却都无法产生任何效果:
- **`prefill`**`packages/llm/llm/src/types.ts`)没有生产级的 setteragent 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)不受影响。
## 决策
-`GenerateOptions` 中移除 `prefill`,同时移除两个适配器的 UNSUPPORTED 守卫、固定抛错行为的测试、[core.md](../../../../docs/core-data-structures/core.md) 中的粘贴行,以及记录该拒绝行为的适配器 README 表格行。实操手册中的 UNSUPPORTED 指导([adding-an-llm-adapter.md](../../../../docs/cookbook/adding-an-llm-adapter.md))改为通用表述规则——提供方无法遵守的 `GenerateOptions` 字段应抛出 `LlmError(..., 'UNSUPPORTED')`——而不再以 prefill 为例。[内容块词汇 Agent Noteagent 决策记录)](../architecture/2026-06-11-content-block-vocabulary.md)的后果按照 [implemented/AGENTS.md](../AGENTS.md),将 prefill 记录为由生产者门控,而不是已有归属。
-`ToolSchema``DefineToolOptions``defineTool``schemas()` 允许列表、deepseek 序列化分支及其 wire-type 字段,以及工具目录渲染器的 `Strict:` 行中移除 `strict`。pi-ai 的 payload 修补逻辑简化为对 pi-ai 自身逐工具 strict 默认值的无条件清除pi-ai 在每个序列化的工具上打 `strict: false`手写的孪生适配器不发送此字段因此清除逻辑为保持协议格式对等而保留由其序列化器测试固定。setter 测试和 core.md 粘贴行已移除;`GenerateOptions``ToolSchema``scripts/type-equiv.manifest.json` 中保留各自的行,因为两个类型只是少了一个字段,本身仍然存在。
本 Agent Note 刻意不触及 `temperature``stop``maxTokens`:两个适配器都会端到端遵守它们,而且它们自然是 `agent/request` 上修改请求的钩子插件首批目标。
## 曾考虑的替代方案
### 为什么不保留?
「显式的 UNSUPPORTED throw 是诚实的契约行为」——但一个在两个孪生适配器中唯一的实现就是拒绝的旋钮,什么也没承诺;删除它反而升级了失败模式:意外的 setter 变成编译错误而非运行时 throw。「Strict schema 遵循是官方文档记载的提供方功能,且管道完整」——但一个旋钮在有已发布的工具设置它并且有端点兑现它之前,不构成产品表面;今天两者都不成立。它们各自随首个真实 producer 回归:`prefill` 随实现了 chat-prefix completion 的适配器(以及对不支持该功能的适配器的明确策略)一起回来;`strict` 随需要它的工具和 beta 端点方案一起回来。
## 验证
`rg prefill` 只返回 Agent Note 记录(本文及[内容块词汇 Agent Note](../architecture/2026-06-11-content-block-vocabulary.md)中由生产者门控的后果);限定在工具 schema 范围内的 `rg strict` 只返回本 Agent Note、保留下来的 pi-ai 清理逻辑,以及 `strictEqual` 等无关正文。两个适配器的契约测试都能在没有守卫的情况下通过pi-ai 修正仍会清理库的 strict 默认值——其 serializer 测试固定了线协议一致性。
## 后果
已发布的钩子桥接不设置任何请求字段,而请求变更插件(`agent/request` waterfall瀑布式事件监听器使用的是 `temperature`/`stop`(保留且可用),而非适配器拒绝的字段。如果 chat-prefix completion 或 strict 模式成为产品功能,重新添加将随适配器/端点工作一起落地,届时契约能说明实际发生了什么,而不是「所有人都 throw」。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# 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: 5b1cb1307c63ef7c298200ee1655119026b9ebf5
2026-07-04-drop-unconsumed-web-observation-surface.zh.md: c4351c7e66d00f47e1bf9ed117c5f7b043045808

View File

@@ -2,6 +2,8 @@
Status: implemented
English | [中文](2026-07-04-drop-unconsumed-web-observation-surface.zh.md)
## Problem
`WebService` exposes an observation surface no production code observes:

View File

@@ -0,0 +1,34 @@
# Agent Note: 移除未被消费的 web 观测接口——`providers-change` 事件与 status 方法
Status: implemented
[English](2026-07-04-drop-unconsumed-web-observation-surface.md) | 中文
## 问题
`WebService` 暴露了一组没有任何生产代码观测的观测接口:
- **`web/providers-change`**`packages/web/web/src/index.ts`)在每次提供方注册和 dispose资源释放时声明并发出且每个注册 effect 的回滚 yield 被刻意排在 emit 之前,唯一目的是让抛出异常的 change listener 能回退注册。在该包自身的两个单元测试之外没有任何 listener其中一个测试的存在仅仅是为了固定那个回滚顺序
- **`searchStatus()` / `fetchStatus()``WebCapabilityStatus` 联合类型**(同一包)没有生产调用方:`dsh-tool-web` 直接通过 `ctx.web.search()`/`fetch()` 执行,并把不可用性呈现为 seam 在执行时抛出的结构化 `WebError` code`packages/web/tool-web/src/search.ts``packages/web/tool-web/src/fetch.ts`);唯一的 status 调用方是 web 包自己的测试。`packages/web/tool-web/README.md` 和 [architecture.md](../../../../docs/architecture.md) 中的正文声称工具“只读取聚合的 `searchStatus()`/`fetchStatus()`”——这种漂移之所以存续,只是因为没有机制对照调用位置检查正文。
seam 自身的设计使这两个接口天然没有消费方:工具注册跟随产品 ENABLEMENT 而非提供方可用性(`packages/web/tool-web/src/index.ts`提供方选择在执行时解析且从不缓存——因此没有需要失效的缓存、没有需要重算的注册集合、也没有调用方需要一个有别于「执行并路由结构化错误」的可用性探测。HMR热模块替换清理由 effect disposer 自身承载。
这与[删除无人消费的 `llm/adapter-change` 事件](2026-06-20-drop-unconsumed-llm-adapter-change-event.md)相呼应;后者从 `LlmService` 移除了相同的通知形状、相同的 emit 前回滚机制和相同的监听器抛错测试。该 Agent Noteagent 决策记录)的保留/删除标准是:为可能面向用户的工具列表消费方保留 `tools/change`删除启动时后端注册表信号。按这一标准web 提供方注册表明确属于删除一侧status 方法则是把同一判断应用于拉取表面,而非推送表面。
## 决策
移除注册表变更事件、聚合 status 方法与类型,以及它们的专属测试。提供方私有的 status 保留用于执行时选择。面向调用方的覆盖率现在断言成功执行或结构化的选择错误web 文档描述该按需调用契约。
## 曾考虑的替代方案
### 为什么不保留?
web seam Agent Note 刻意规定了两者——事件作为最小 HMR 可见性信号status 方法作为工具的聚合诊断——未来也可以设想提供方状态面板。但同一 Agent Note 的其他选择让它们失去了生存条件:调用时派生选择和基于启用状态的注册,使任何消费方都不可能需要其中任一项;已发布工具展示了真实模式(执行并路由结构化错误);发生漂移的 README 句子则表明承诺中的消费方从未出现。按照 AGENTS.md 所述“Agent Note 是提案,而非绝对真理”,代码后来证明提案中的这些部分超出了需要;未来的观察者应根据真实消费方的形状,重新引入它实际消费的最小信号或查询。
## 验证
除 Agent Note 历史外,不再存在 `providers-change``searchStatus``fetchStatus``WebCapabilityStatus` 拼写;目录保持新鲜(`verify-cordis-catalog` 为绿色);注册/释放 HMR 安全性测试通过执行行为证明清理tool-web README 和架构段落也描述了工具实际拥有的执行时错误路由契约。
## 后果
未来若有提供方选择器 UI 或诊断面板需要变更通知或 status 查询,它将重新添加自身所消费的最小接口;相同的判断及其反转条件已记录在 LLM大语言模型先例中。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# 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: 01165df19942e8e0adf84ede3a371f53ad12e464
2026-07-04-fold-stdio-ui-helper.zh.md: 37b316283e96d7640e1e7af5fdbb05ab487aa6fa

View File

@@ -2,6 +2,8 @@
Status: implemented
English | [中文](2026-07-04-fold-stdio-ui-helper.zh.md)
The later [redundant-agent removal](2026-07-20-remove-stdio-and-echo-agents.md) supersedes this package-placement decision and removes the folded package, app, and line-oriented surface entirely.
## Problem

View File

@@ -0,0 +1,30 @@
# Agent Note: 将 stdio UI 辅助模块折入 stdio 应用
Status: implemented
[English](2026-07-04-fold-stdio-ui-helper.md) | 中文
后来的[冗余 agent智能体移除](2026-07-20-remove-stdio-and-echo-agents.md)取代了这项包放置决策,并完整移除合并后的包、应用和面向行的表面。
## 问题
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且没有其他消费方能有意义地使用它。
## 决策
当时,该辅助函数移入 `@deepseek-ai/dsh-stdio`,成为终端通道插件。`createStdioChat`、其 `StdioRuntime` 测试 seam 和单元测试随之一同迁移,使 EOF 处理、渲染、释放以及管道/TTY 行为继续受逐文件覆盖率门禁约束,而不会劫持进程全局量。该模块保留应用挂载所消费的具名 `name`/`inject`/`Config`/`apply` 导出形状;当时的 Echo 和 REPL Loader 冒烟证明组合树,插件形状套件则固定显式 `unwrapExports` 行为。上方取代本文的移除记录负责当前包和示例状态。
早期的支持辅助包已移除其清单、tsconfig 引用、模块图行和 README 行均已消失,其余文档改为描述包内模块。
## 曾考虑的替代方案
### 为什么不将其提升到 `ui/` 而是折入?
提升可以解决 support 与 product 之间的错位,同时保留边界——只有在 readline UI 是一个可独立替换的集成或有第二个组合方时才是正确选择,而消费方普查表明两者皆非。结构化的 ACPAgent Client Protocol桥接保留为独立包因为它是具有自身契约和快照层级的产品协议表面readline 辅助模块只是一个应用前门的脚手架。在发布前重新拆分成本很低:如果将来有第二个产品应用需要 readline UI届时再拆出来由那个消费方来塑造包契约。
## 后果
- stdio 应用完整拥有自己的前门;叶子 `cordis.yml` 仍然只加载一个应用包,演示的形态没有变化。
- 未来如果有独立的终端 UI 需要将该辅助模块作为包使用,届时由那个第二消费方驱动重新引入,而非仓库为假设性的复用保留一条边界。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# 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: 34492e6906cd2d795f880310b1bcd120e3953fcf
2026-07-04-prune-producerless-vocabulary-variants.zh.md: a68a8b04fedefed8a2ab92f08baf5e8b3ea90222

View File

@@ -2,6 +2,8 @@
Status: implemented
English | [中文](2026-07-04-prune-producerless-vocabulary-variants.zh.md)
## Problem
The merge-extensible vocabulary maps are designed to grow by declaration merging, and the codebase already states the admission policy on `TurnEndReasonMap` (`packages/core/session/src/types.ts`): a variant like `refusal` is "deliberately omitted until" an adapter or loop first emits it. Three declared vocabulary items violated that policy — each had no producer and no consumer, and two had not even a test:

View File

@@ -0,0 +1,33 @@
# Agent Note: 裁剪无生产者的词汇变体(块缓存提示、`agent` 消息来源、`continuation` 轮次触发器)
Status: implemented
[English](2026-07-04-prune-producerless-vocabulary-variants.md) | 中文
## 问题
可合并扩展的词汇映射表设计上通过声明合并来增长,代码库已在 `TurnEndReasonMap``packages/core/session/src/types.ts`)上明确了准入策略:像 `refusal` 这样的变体「在适配器或循环首次发出它之前,有意不纳入」。三个已声明的词汇项违反了该策略——每个都既无生产者也无消费方,其中两个甚至没有测试:
- **`TextBlock`/`ToolResultBlock` 上的 `CacheHint` 及其 `cache?: CacheHint` 块字段**`packages/llm/llm/src/types.ts`;图像块曾有第三个此类字段,已随图像块一同移除——参见[删除图像 Agent Noteagent 决策记录)](2026-07-04-drop-image-content-block.md))。任何地方都没有构造带 `cache:` 的块——src、测试和文档粘贴均为空——两个适配器也都不读取 `.cache`DeepSeek 的提示词缓存是自动的,因此适配器会从响应中映射出 `prompt_cache_hit_tokens`,却从不向请求中发送 hint。这是没有任何提供方能够遵守的 Anthropic 风格 `cache_control` 表面。
- **`MessageSourceMap.agent`**`{ kind: 'agent'; agentId: string }`同一文件。零个构造点包括测试在内。它预期的生产者在实现时并未使用它subagent 后端将父级的提示词发送给子级时不带 `source`,因此记录为 `{ kind: 'user' }`,通用信封渲染器在插值 `source.kind` 时也从未对其做路由。
- **`TurnTriggerMap.continuation`**`packages/core/session/src/types.ts`。agent loop智能体循环在结构上不可能发出它——continuation 发生在一个轮次*内部*作为后续步骤,而非作为新轮次——循环只构造 `message``injection` 触发器。唯一的写入者是一个手工构建的测试 fixture测试前置数据它只需要一个任意的非消息触发器`packages/support/llm-replay/tests/llm-replay.spec.ts``injection` 触发器同样满足需求;唯一的生产环境触发器读取方 ACPAgent Client Protocol桥接层只过滤 `kind === 'message'`
## 决策
`CacheHint`、其 `cache?` 块字段、`agent` 消息来源变体和 `continuation` 轮次触发器变体均已删除已发布词汇不再携带它们。llm-replay fixture 使用 `injection` 触发器(任何非 `message` 触发器都能满足其用途)。[core.md](../../../../docs/core-data-structures/core.md) 和 [session.md](../../../../docs/core-data-structures/session.md) 中的 type-equiv 粘贴与裁剪后的 map 匹配——两个符号仍保留在 `scripts/type-equiv.manifest.json` 中的行,因为每个 map 都只是少了一个成员而继续存在——并且[内容块词汇 Agent Note](../architecture/2026-06-11-content-block-vocabulary.md)的后果按照 [implemented/AGENTS.md](../AGENTS.md),将 cache hint 记录为由生产者门控,而不是已有归属。
每个变体在获得真正的生产者之日回归,这正是映射表设计的增长方式:缓存功能连同传输它的适配器一起重新添加 `cache`subagent 归属连同打标的后端和路由它的消费方一起重新添加 `agent`;真正启动新轮次的自动续行功能连同发出它的插件一起重新添加 `continuation`
## 曾考虑的替代方案
### 为什么不保留它们?
[内容块词汇 Agent Note](../architecture/2026-06-11-content-block-vocabulary.md)曾把“cache hint……有了归属”列为设计后果预留槽位也确实能表明意图。但空槽位是每个实现和消费方都必须考虑的契约表面我的适配器是否必须遵守 `cache`?我的 renderer 是否必须路由 `agent` 来源?),而相邻 map 自身的 JSDoc 已经拒绝“无 emitter 先预留”——`refusal``max_turn_requests` 被点名为*首次有内容发出它们时*再添加的变体,而不是提前声明。让已经声明但无用的变体遵守同一标准,才能使词汇真正有意义:只要它位于 map 中,就必须有内容生产它。
## 验证
`CacheHint``agent` 消息来源拼写和 `continuation` 触发器拼写运行 `rg`,只会返回 Agent Note 记录(本文,以及[删除图像 Agent Note](2026-07-04-drop-image-content-block.md)对图像块自身 `cache` 字段的说明llm-replay fixture 使用 `injection` 触发器断言相同的重放行为;核心数据结构粘贴和 type-equiv 清单保持同步。
## 后果
操作行为没有变化——原本就没有内容能够构造这些值。镜像事件移除([边界镜像 Agent Note](2026-06-20-remove-agent-boundary-mirror-events.md)、[流分片 Agent Note](2026-07-02-remove-stream-chunk-mirror.md))只触及瞬态 `agent/*` 事件,从不触及持久词汇,因此不存在冲突。其他位置已经遵守准入策略:`rejected``prompt/blocked``hook/invoked`/`hook/result` 都有实时生产者——本 Agent Note 将同一门槛扩展到缺少生产者的三个变体。图像块自身的 `cache?` 字段归属[删除图像 Agent Note](2026-07-04-drop-image-content-block.md),后者将其与该块一同移除;本 Agent Note 覆盖剩余块类型上的两个字段。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# 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: 6cfd5d9ab8a2fc6322814d384fba735c06681976
2026-07-04-prune-write-only-fs-surface.zh.md: cb7494b5e958c8ed86ffa3ba8ffbe82748d1db03

View File

@@ -2,6 +2,8 @@
Status: implemented
English | [中文](2026-07-04-prune-write-only-fs-surface.zh.md)
## Problem
The [fs seam split](2026-06-26-fsspec-style-fs-seam.md) moved read routing and policy out of the backend into `dsh-tool-fs` and `dsh-fs-policy`. Four pieces of surface kept the pre-split shape — populated on every call, read by nobody:

View File

@@ -0,0 +1,32 @@
# Agent Note: 从 fs seam 中移除只写字段与一个无效的路由旋钮
Status: implemented
[English](2026-07-04-prune-write-only-fs-surface.md) | 中文
## 问题
[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 声称提供一个实际不存在的「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](../../../../docs/core-data-structures/filesystem.md) 中的粘贴、`packages/fs/fs/README.md`,以及不得不虚构已删除字段的测试 fake 都随类型一同收窄。
## 曾考虑的替代方案
### 为什么不保留?
未来的权限/隔离层可能需要解析前的路径来生成错误文本——但它需要的是*请求*,每个调用点仍然持有请求。「替换了 N 处」可能成为面向模型的文本——这是一个需要时再设计的行为变更,且后端内部的计数为其错误消息而保留。读取页脚可能展示 `limit`——但页脚展示的一切已经可以从 `lines`/`totalLines` 推导。与此同时,每个现有和未来的后端(远程、原生)都必须编造无人消费的协议字段,每个测试 mock 都必须满足它们。
## 验证
已删除表面不复存在——`dsh-fs-local` 中的 `STREAM_MIN_SIZE`/`streamMinSize``FsTarget.inputPath``FsEditOutcome.replacements`/`.replaceAll`,以及 `FileReadOutcome.limit`/`.version`——而请求侧 `replaceAll``FsEditRequest`)和其他 outcome 类型上的版本字段保持不变;测试 fake 随类型一同收窄。`formatEditOutput` 在两个 `replace_all` 分支中生成的文本都没有变化,因此没有快照预期输出发生改动。
## 后果
后端不增加新义务反而卸下了四个无人消费的字段。fs 发现功能glob/grep 工具)涉及相同的 `dsh-fs` 类型文件——这是文本层面而非设计层面的重叠,可以机械地合并解决。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# 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: 9f7cd5abe968ff216cbd7012163ea1c04dc00599
2026-07-04-remove-agent-steering-mirror.zh.md: 63f575347d989f288b3129e0a53e5690b85bb4e8

View File

@@ -2,6 +2,8 @@
Status: implemented
English | [中文](2026-07-04-remove-agent-steering-mirror.zh.md)
## Problem
`agent/steering` was the last remaining transient mirror of a durable session event. The loop's steering drain appends the durable `steering/message { turn, content, source }` and, on the very next line, emitted `agent/steering(agent, turn, content, source)` — the identical fact as a fire-and-forget event (`packages/core/agent-loop/src/loop.ts`, `drainSteering`). It had zero production listeners: the only subscriber anywhere was a loop regression test asserting the emit carried `source` — the same fact the durable event already records one line above.

View File

@@ -0,0 +1,33 @@
# Agent Note: 移除 `agent/steering` 镜像 emit
Status: implemented
[English](2026-07-04-remove-agent-steering-mirror.md) | 中文
## 问题
`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` 仍保留为纯瞬态信号,因为它在持久化之前触发,覆盖了可能在进入日志前被取消的工作。
Steering 承载真实生产流量——钩子 bridge 的轮次延续决策通过 `inbox.steer()` 注入其理由,最终成为由钩子矩阵预期输出固定的持久 `steering/message` 事件——而这些消费方无一例外都观察持久事件。没有任何内容观察镜像。
## 决策
`agent/steering` 已从 agent 事件分类中移除:包括 `packages/core/agent/src/types.ts` 中的声明(以及其中实时事件 JSDoc 列表对它的提及)、`drainSteering` 中的 emit当时已无用的 `ctx` 参数也随之移除)、`packages/core/agent/README.md` 中的表格行,以及循环伪代码块(`packages/core/agent-loop/src/loop.ts` 模块文档和 [architecture.md](../../../../docs/architecture.md))中的 emit 行Cordis 目录重新生成后不再包含它。唯一的回归测试改为在持久 `steering/message` 事件上固定来源保留行为——所固定的事实存在于日志上。
三份已实现 Agent Noteagent 决策记录)曾说明保留该事件;按照 [implemented/AGENTS.md](../AGENTS.md),每份记录都已修改并指向本文作为移除记录:包括[边界 Agent Note](2026-06-20-remove-agent-boundary-mirror-events.md) 的保留列表条目、[流分片 Agent Note](2026-07-02-remove-stream-chunk-mirror.md) 的范围条款,以及[事件域语义 Agent Note](../architecture/2026-06-30-event-domain-semantics.md) 的瞬态 emit 枚举。
## 曾考虑的替代方案
### 为什么不保留?
“它是控制信号,不是边界”——但该分类的实际区分是镜像/仅实时,而非控制/边界,并且该事件确实是镜像。希望在入队时收到通知的消费方可以使用 `agent/queued`(及其 steering 标记);希望在排空时收到通知的消费方,本质上是在要求获知 `steering/message` 被追加的时刻,而 `session/event` 会交付相同 payload 并附带持久性。遭拒绝的[退役轮次中途 steering Agent Note](../../rejected/simplification/2026-06-20-retire-mid-turn-steering.md)所捍卫的是 steering *功能*——`steer()`、持久事件、强制延续——本次移除不会触及其中任何一项。
## 验证
`agent/steering` 拼写只存在于 Agent Note 正文中(本 Agent Note、上方三份已修改 Agent Note以及已冻结的[遭拒绝 steering 功能 Agent Note](../../rejected/simplification/2026-06-20-retire-mid-turn-steering.md),其正文记录了它所否决的提案);目录已重新生成;重新定向的测试在 `steering/message` 上固定来源保留行为。
## 后果
生产环境中没有需要迁移的监听者,两种瞬态通知需求各有归宿:入队时由 `agent/queued`(带 `steering` flag承载drain 时由 `session/event` 在持久事件 `steering/message` 落地时承载。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# 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: 7a763eba8a229ec5017387edb54657a5c367105b
2026-07-04-share-app-bin-boot-glue.zh.md: d65a6613f7b05cdea0f99529808c992aff4256e9

View File

@@ -2,6 +2,8 @@
Status: implemented
English | [中文](2026-07-04-share-app-bin-boot-glue.zh.md)
## Problem
The stdio and ACP bins duplicated environment loading, fail-loud handling, entry validation, and boot logic, including subtle Loader failure behavior. Their copies had already drifted and lived in self-executing files excluded from unit coverage, making their helper exports unusable.

View File

@@ -0,0 +1,27 @@
# Agent Note: 共享应用 bin 的启动胶水代码,而非维护两份副本
Status: implemented
[English](2026-07-04-share-app-bin-boot-glue.md) | 中文
## 问题
stdio 和 ACPAgent Client Protocol两个 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 的诊断前缀参数化,并在其副作用 seamwarn sink、process slice处支持注入使单元测试套件能覆盖每个分支——包括 `boot()` 在进程内驱动真实 Loader、使用相对路径 specifier 配置的场景,既覆盖已稳定树的正常路径,也覆盖无 fiber 入口的拒绝路径。该包启用逐文件 100% 覆盖率门禁Loader 失败的相关知识只有一个归属地。
每个 `bin.ts` 都是在共享辅助函数之上加应用特有生命周期的精简自执行组合ACP bin重放模式环境变量跳过和 stdin EOF 释放stdio bin没有额外逻辑。这些 bin 仍排除在覆盖率之外且不导出任何内容;已发布产物守卫保持不变——按照“真实入口路径即已发布产物”的防御模式,已构建 bin 冒烟仍在具有 node_modules 形状的临时目录中用纯 node 运行每个 bin现在也会符号链接 `ui/app-boot`),并继续断言缺失配置时以非零状态退出。[提取示例应用包 Agent Noteagent 决策记录)](../architecture/2026-06-20-extract-example-app-packages.md)中的 bin 归属事实已据此修改。
## 曾考虑的替代方案
### 为何不保留重复?
这些 bin 当时被定位为归属相互独立的已发布产物而新包会带来固定开销清单、README、tsconfig 引用、publint 表面),与去重的行数相当。但创建 bin 的 Agent Note 从未权衡应用间共享——它把三份示例 `start.ts` 副本合并进 bin 后便止步于此;漂移是已经观察到的事实;覆盖率缺口的理由也独立于去重理由:这是仓库中唯一免受逐文件 100% 门禁约束的非平凡运行时逻辑。记录的后备方案(只将纯逻辑提取到各应用模块)会结束豁免,但会继续让相关知识拥有两个归属。
## 后果
- 启动胶水代码的变更(新增守卫、修复路径解析)只需落地一次,两个已发布 bin 自动继承bin 之间不会再次漂移。
- `dsh-app-boot` 保持轻量依赖cordis + loader/include 对)——它是启动机制,不是应用表面积。
- bin 自身的文件几乎是平凡的组合;所有含分支的逻辑都在覆盖率门禁之下。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# 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: a1972ee8ef486982268ba8886b2413f3557061b4
2026-07-04-tighten-hook-protocol-contract.zh.md: 4917a8f551672f51b0ebc27b1267c7e8e8eb2178

View File

@@ -2,6 +2,8 @@
Status: implemented
English | [中文](2026-07-04-tighten-hook-protocol-contract.zh.md)
## Problem
Four pieces of the `dsh-hook-protocol`/bridge contract missed the discipline the [subagent-observe-enrich Agent Note](../feature/2026-06-30-subagent-observe-enrich.md) records — it dropped an `agentType` lifecycle field for lacking a consumer, and these failed the same test:

View File

@@ -0,0 +1,32 @@
# Agent Note: 收紧 hook-protocol 契约——dialect、废弃字段、双重默认值与 lib 拥有的 `hook/result` 语义
Status: implemented
[English](2026-07-04-tighten-hook-protocol-contract.md) | 中文
## 问题
`dsh-hook-protocol`/bridge 契约中有四部分没有遵守 [subagent observe/enrich Agent Noteagent 决策记录)](../feature/2026-06-30-subagent-observe-enrich.md)记下的准则——后者因缺少消费方而删除 `agentType` 生命周期字段,以下各项没有通过同一检验:
1. **`HookDialect``'native'` 变体**`packages/hooks/hook-protocol/src/types.ts`没有生产者——bridge 会标记 `'claude'``'codex'`;所有位置中唯一构造 `'native'` 的是该库自己的单元测试。字段自身的 JSDoc 将 `dialect` 定义为“运行它的 bridge”而 native 不是 bridge[拦截 seam Agent Note](../feature/2026-06-30-interception-seams.md) 记载 native 钩子不是一个包并且“native 插件无需持久钩子日志即可使用类型化 Decision”旗舰 native 插件实践示例恰好断言了这一点(完全没有 `hook/*` 事件)。
2. **`HookOutput.suppressOutput`**(同一文件)被 codec 解析后在所有路径上均被丢弃:没有 bridge 分支处理它、没有合并 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 记录)。从结构上看根本无物可抑制:钩子 stdout 从不进入任何 transcript文本记录上下文仅通过 `additionalContext` 流入;日志只记录 `decision`/`stderrSummary`),因此钩子作者设置 `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 内部常量。
## 曾考虑的替代方案
### 为什么不保留它们?
不受支持的词汇可以在真正有消费方时回归。`durationMs` 保留因为持久化的审计计时独立于当前是否有读取方而有价值。Bridge 特有的 payload 构造留在各自 bridge 中,而共享持久化事件的归一化属于协议库。
## 验证
`HookDialect` 仅包含 Claude 和 Codex`suppressOutput` 在源码、已解析字段文档和归一化逻辑中均不存在。`durationMs` 保留在事件和 fixture测试前置数据回放时做清洗。`600_000``500` 两个默认值各只在协议库中出现一次;每个钩子的超时覆盖仍然生效;两个 bridge 的测试套件均验证了由库拥有的 stderr 截断和 decision 规则。
## 后果
`dialect``suppressOutput`、可调参数和语义变更在线协议和预期输出中均不可见。代价是 `dsh-hook-protocol` 和两个 bridge 中的改动——在预发布立场下成本很低,也比让一项持久事件语义的两个副本各自老化更便宜。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# 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: ce0c623930192d02c8347c3956c497ee13048904
2026-07-04-trim-acp-bridge-unreachable-surface.zh.md: e89b0e7c9c7c8438fb71adacf1b525a0b94bf04b

View File

@@ -2,6 +2,8 @@
Status: implemented
English | [中文](2026-07-04-trim-acp-bridge-unreachable-surface.zh.md)
> The handshake-identity simplification remains current. The generic-card fallback was removed when [ACP became automation-only](2026-07-23-acp-automation-only-protocol.md); UI transports retain the provider-neutral presentation contract.
## Problem

View File

@@ -0,0 +1,26 @@
# Agent Note: 裁剪不可达的 ACP 桥接层表面——品牌配置项与 kind 嗅探回退
Status: implemented
[English](2026-07-04-trim-acp-bridge-unreachable-surface.md) | 中文
## 问题
`dsh-acp` 有两处对外表面在任何已交付的配置中都不可达:
1. **`AcpConfig.agentName` / `agentVersion`**`packages/ui/acp/src/index.ts`)。已发布应用包只向 bridge 传递 `{ model }``packages/examples/acp-demo/src/index.ts`),因此没有任何叶子 `cordis.yml`——唯一的生产配置表面——能够设置这些配置项;只有直接挂载 bridge 才能设置它们,而这种做法只存在于一个单元测试中。每份快照预期输出——包括钩子矩阵场景——都固定 schema 默认值(`deepseek-harness-acp` / `0.0.1`)。这对配置项还带有一个尚未解决的 `TODO(double-default)`字面量存在两次schema `.default(...)``??` 后备值TODO 要求为它们选择一个归属。
2. **`toolKindFor` 名称启发式**(同一文件)在通用回退路径中对 `bash*`/`read*`/`write`/`edit*` 工具名做了特殊处理。自[render-intent 联合类型](../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。
## 曾考虑的替代方案
### 为什么不保留?
品牌配置可以在 app 包将其暴露给部署环境时再回来。从未知工具名推断呈现方式违反了 render-intent 契约;中性回退卡片还能为格式错误的调用和损坏的 presenter 保留原始输入。
## 后果
除上述回退渲染的取舍外没有其他影响——退化路径下,中性卡片比推断出的第一方卡片更易于诊断。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# 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: b0ed7585882328b6abdcf57974200053d9c26048
2026-07-12-drop-unconsumed-skill-provider-events.zh.md: fd380b2c0421abfc3032b88b550b4a3e8b88bf38

View File

@@ -2,6 +2,8 @@
Status: implemented
English | [中文](2026-07-12-drop-unconsumed-skill-provider-events.zh.md)
## Problem
Two skill-registry notifications are produced but have no production listener. The generated producer/consumer matrix and exact event-name searches find only declarations, emit sites, tests, generated catalogs, and prose for `skill/provider-added` and `skill/provider-removed`.

View File

@@ -0,0 +1,29 @@
# Agent Note: 移除无消费方的 skill 提供方事件
Status: implemented
[English](2026-07-12-drop-unconsumed-skill-provider-events.md) | 中文
## 问题
skill技能注册表产出两个通知事件但没有生产环境的监听方。生成的生产者/消费方矩阵以及对事件名的精确搜索表明,`skill/provider-added``skill/provider-removed` 仅出现在声明、emit 站点、测试、生成的 catalog 和行文中。
skill 发现按需读取当前的提供方映射表,提供方注册时同步清除已完成的 catalog而 await 后的版本检查阻止了陈旧的发现结果进入缓存。没有兄弟插件通过这些事件等待 skill 提供方——与之形成对比的是活跃的 `subagent/provider-added` 消费方,它容忍兄弟并发加载。
`tools/change``system-prompt/change` 明确不在本提案范围内。既有的简化决策将它们保留为面向实时工具和提示词 UI 的有意观测点,且自引用的已挂载插件已在使用 `tools/change`。本提案同样不改动 `subagent/provider-added`/`removed`,因为 `tool-subagent` 有生产环境的生命周期消费方。
## 决策
skill 注册表不再声明和 emit 提供方成员变更事件。提供方的注册与 dispose资源释放仍为 effect 所有的直接状态变更,同步使已完成的 catalog 失效;查找与发现按需读取当前提供方映射表。测试通过提供方查找和收集到的输出来观察清理行为,而非依赖生命周期通知。
生成式事件目录、API 目录和生产者/消费方矩阵均不再包含已删除通知。skill system Agent Noteagent 决策记录)和包文档改为通过其由 effect 直接拥有的状态与 cache 失效契约描述注册。
## 曾考虑的替代方案
**为未来插件保留 skill 提供方通知。** 第三方插件可能想观察提供方的可用性,但直接提供方注册与按需查找才是扩展契约;当前没有消费方需要推送信号。如果将来出现兄弟加载竞态,可以像 subagent 注册表那样,引入一个带有该消费方实际所需的身份与就绪语义的通知。
## 后果
生成的事件矩阵中不再有 `skill/provider-added``skill/provider-removed` 的行。skill 发现、直接运行时注册、提供方 effect 回滚/dispose、缓存失效与注册表查找清理保持不变监听方触发的回滚随事件一起消失。`tools/change``system-prompt/change` 以及已被消费的 subagent 提供方生命周期事件不受影响。
预发布消费方失去 skill 提供方观测点,但仍保留两种贡献 skill 的方式:直接运行时注册与提供方注册。未来若有消费方需要实时的提供方可用性信息,必须新增一个带有其实际所需的身份与就绪语义的专用通知。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# 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: c50bf44161579a44b09113fc501f3d67fb5d6855
2026-07-12-prune-unused-web-seam-fields.zh.md: 401bdd0c812175cffc572e722141d2829a3fc2d5

View File

@@ -2,6 +2,8 @@
Status: implemented
English | [中文](2026-07-12-prune-unused-web-seam-fields.zh.md)
## Problem
The web capability carries request/result/status values that every shipped implementation populates but no production consumer reads. `WebSearchResult.providerId` and `query` and `WebFetchResult.providerId` are result echoes; `tool-web` formats only content/sources/truncation or final URL/status/body/truncation, and no other runtime reads them. Search providers return `WebProviderStatus.reason`, but resolution checks only `available` and intentionally emits a generic unavailable diagnostic.

View File

@@ -0,0 +1,27 @@
# Agent Note: 裁剪 web seam 中未使用的字段
Status: implemented
[English](2026-07-12-prune-unused-web-seam-fields.md) | 中文
## 问题
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使用工具定义的 timeout 加 `exec.signal` 作为调用方截止时间,并依赖本地提供方的配置默认值作为兜底。这个未使用的逐请求覆盖迫使 `web-fetch-local` 暴露 `maxTimeoutMs`、对两个 timeout 来源做 clamp并为没有任何产品路径能选中的优先级规则编写文档和测试。`WebExecContext` 则是另一个单字段包装层:每个调用方分配 `{ signal }`,每个提供方立即解包 `exec?.signal`;不存在第二个执行控制字段。
## 决策
web seam 移除搜索/抓取结果中的 `providerId` 回显和搜索的 `query` 回显;调用方本身已持有请求和提供方选择信息。提供方以返回布尔值的方法暴露可用性。抓取请求不再有逐请求 timeout 或 `maxTimeoutMs` clamp本地提供方保留其可配置的默认 timeout工具保留自身的截止时间。提供方方法直接接收一个可选的 `AbortSignal`,而非单字段的 `WebExecContext` 包装层。
所有 web 实现与面向模型的工具使用更精简的契约。接口/实现/消费方的包package拆分、提供方选择、来源引用、最终 URL/状态数据、截断报告与安全限制保持不变。
## 曾考虑的替代方案
**保留自描述结果、逐请求截止时间与可扩展的执行上下文对象。** 结果回显可以帮助通用遥测,请求级 timeout 可以帮助受信的程序化调用方,包装层则为未来的控制字段留出空间。但目前不存在这样的消费方或第二个字段;在每个提供方中携带重复的身份标识、第二套截止时间策略以及包装/解包管道,使当前契约更难实现和解释。如果遥测或逐调用预算控制到来,届时应当定义哪个截止时间优先、在哪里观测提供方身份,以及多个控制字段是否足以证明需要一个上下文对象。
## 后果
保留下来的每个 web request/result 字段,要么被生产代码消费,要么是执行提供方请求所必需的。工具可见的搜索/抓取输出、提供方回退、中止行为、可配置的 timeout 兜底、截断与引用仍然被覆盖,无需请求级 timeout 优先级分支或执行上下文包装层。
预发布阶段的程序化调用方失去了结果来源回显和逐请求的抓取截止时间。提供方仍具备部署级可配置 timeout 并尊重取消信号,因此这次精简移除的是可配置性,而非安全边界。

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-07-12-simplify-session-log-representation.md: a40f4013a97a9c940012dbb37d59beb2faf8fb22
2026-07-12-simplify-session-log-representation.zh.md: a4ecd8c7340affda71d95505ab44910539e36bb9

View File

@@ -2,6 +2,8 @@
Status: implemented
English | [中文](2026-07-12-simplify-session-log-representation.zh.md)
## Problem
The session log maintains two representations that cost more machinery than their consumers require: a pseudo-linked surface and custom request-header deltas.

View File

@@ -0,0 +1,35 @@
# Agent Note: 简化会话日志表示
Status: implemented
[English](2026-07-12-simplify-session-log-representation.md) | 中文
## 问题
会话日志维护着两种表示,其机制复杂度超出了消费方的实际需求:一个伪链表 surface 和自定义的请求头增量。
`SurfaceManager` 同时在数组、seq map 和可变 `prev`/`next` 链接中存储相同顺序。生产代码从不读取任一链接compact 的工具配对 balance 根据按 surface 顺序缓存的每个切点 balance 作答。替换已经使用 `indexOf`,因此链接并未使其主导操作成为常数时间。使用线性替换查找的 seq 数组具有相同的渐近替换成本,却只有一种表示需要验证。
请求头子系统实现了一套自定义的系统/工具增量编解码器和传输决策层,尽管其契约声明增量只是编码优化,而非可重建性要求。在每个 agent loop智能体循环实例边界保留初始/恢复的完整快照,然后在该实例的组装头发生变化时写入一条规范的完整 `request/header`,即可保留回放能力,同时删除 `SystemDelta``ToolsDelta`、往返回退逻辑以及持久化的 `request/header-delta` 变体。编解码器专属的词汇随编解码器一起消失,并非因为其各分支本身无效。
实现保留追加与替换 `sourceEventSeqs`、崩溃修复 provenance以及所有 `SessionStartSource` 变体,因为这些字段承担审计/拦截职责,当前没有读取方并不能推翻这一点。
## 决策
`SurfaceManager.nodes` 是由事件序号组成的 `readonly number[]`;公共 `SurfaceNode` 形状、node 链接和 seq-to-node map 均已移除。内部替换 generation 信号保留。session-query 使用的完整 `foldSurface()` 读取会返回相同的数字数组表示和替换元数据,而无需让增量 manager 保留历史。工具配对 balance 和压缩使用事件序号与 surface 位置;由 compact 拥有的每切点 balance cache 不依赖 node 链接。
请求头只使用规范的完整快照。初始与恢复锚点即使没有变化也仍是完整快照;实例内变化会追加另一个完整 `request/header`reason 为 `change`。delta 事件、codec 类型、diff/apply 辅助函数,以及仅供 codec 使用的 `fallback` reason 均已移除。请求重建选择最新快照。
`SESSION_FORMAT_VERSION` 仍固定为 `0`,因此 seed、追加和持久化加载验证会显式拒绝旧 v0 `request/header-delta` 事件,以及携带已删除 `fallback` reason 的完整快照。不存在兼容性 fold 或迁移。JSONL 与 SQLite 测试固定了这一响亮失败边界ACPAgent Client Protocol快照 harness 则把合法的会话中途变更表示为完整固定请求头和完整可读提示词。
## 曾考虑的替代方案
**保留链表节点和紧凑增量以备未来扩展。** 链接可能有助于未来的游标 API增量在大型工具 schema 仅有少量变化时可以缩减日志。但没有已发布的游标使用这些链接,而完整快照以磁盘空间换取了显著更简单的正确性。如果头部体积确实成为问题,可以基于真实 trace 设计压缩方案或经过度量的规范增量方案。
## 验证
单元覆盖率固定有序 surface 的追加/替换行为、工具配对、压缩、完整请求头 fold/记录、请求重建和开发不变量。Seed 验证以及 JSONL、SQLite 加载测试会在重放前拒绝旧事件。无密钥 ACP 套件以新形状覆盖记录、刷新、重放、变化请求头固定,以及沙箱模式切换 fixture测试前置数据
## 后果
完整请求头会增加日志体积,线性替换查找在极大 surface 上也可能较慢。由于先前实现调用 `indexOf`替换原本就是线性的benchmark 推迟到真实 trace 表明更简单的数组成为瓶颈时再进行。格式版本仍为 `0`因此显式拒绝旧事件是预发布格式边界的永久组成部分。作为交换surface 顺序和请求头状态现在各自只有一种表示删除了链接维护、map、codec 分支、往返 fallback 和感知 delta 的快照规范化。