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:
ZiyaZhang
2026-07-22 03:07:36 -07:00
parent 839b88a53a
commit 8ea5cdd894
292 changed files with 2819 additions and 2820 deletions

View File

@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-06-16-typed-event-schemas.md: 8d14c3b2d90d8dcf295e122e95267c2c0d7b2a17
2026-06-16-typed-event-schemas.zh.md: 87bd6b89a2332b7a3a609a9c36d1e9835e42a0b1
2026-06-16-typed-event-schemas.zh.md: 34f46a87058b409ecdab38d851654db49d87e201

View File

@@ -1,4 +1,4 @@
# RFC事件词汇的运行时 schemaZod 与 merge-extensible-map 模式之
# RFC事件词汇的运行时 schemaZod 与 merge-extensible-map 模式之
[English](2026-06-16-typed-event-schemas.md) | 中文
@@ -6,72 +6,72 @@ Status: proposed
## 问题
harness 将其核心词汇——内容块、消息来源、结束原因、轮次触发器、轮次结束原因与会话事件——建模为 **merge-extensible map**:一个 TypeScript `interface`(如 `SessionEventMap``ContentBlockMap`),插件通过声明合并对其扩展,公开联合类型以 `Map[keyof Map]` 派生。这是本仓库的通用扩展模式,记录在 [docs/architecture.md](../../../architecture.md) 中("The same merge-extensible-map pattern is used for `MessageSource`, `FinishReason`, `TurnTrigger`, and `TurnEndReason`"并被 `defineTool``InferArgs` DSL `assertNever`尽性约定依赖。
harness 将其核心词汇——内容块、消息来源、结束原因、轮次触发器、轮次结束原因与会话事件——建模为 **merge-extensible map**:一个 TypeScript `interface`(如 `SessionEventMap``ContentBlockMap`),插件通过声明合并对其扩展,公开联合类型`Map[keyof Map]` 派生。这是本仓库的通用扩展模式,记录在 [docs/architecture.md](../../../architecture.md) 中("The same merge-extensible-map pattern is used for `MessageSource`, `FinishReason`, `TurnTrigger`, and `TurnEndReason`"`defineTool``InferArgs` DSL `assertNever`约定依赖于它
该模式**仅存在于编译期**。类型在运行时消失:没有 schema 对象可供校验传入值、解析不可信输入或在运行时枚举。[会话持久化契约](../../implemented/architecture/2026-06-14-session-persistence.md)暴露了两个后果:
该模式**仅存在于编译期**。类型在运行时消失:没有 schema 对象可供校验传入值、解析不可信输入或在运行时枚举变体。[会话持久化契约](../../implemented/architecture/2026-06-14-session-persistence.md)暴露了两个后果:
1. **持久化将 `event.data` 视为不透明 JSON。** JSONL/SQLite 后端对每个事件逐字 `JSON.stringify`/`JSON.parse`;唯一的运行时守卫是 `isJsonValue`(往返可序列化性——拒绝 BigInt、函数、循环引用、非有限数等而**不是**结构校验。一个损坏但仍为合法 JSON 的事件数据(字段类型错误、字段缺失)会静默往返,只有在之后被消费方的 `switch` 处理时才可能被发现
2. **插件新增变体没有运行时契约。** 一个通过声明合并添加新 `SessionEventMap` 键的插件,在自身代码中获得了编译期类型,但没有任何机制校验它产出的值是否匹配它声明的形状——无论在生产、持久化边界还是重新加载时。
1. **持久化将 `event.data` 视为不透明 JSON。** JSONL/SQLite 后端对每个事件逐字 `JSON.stringify`/`JSON.parse`;唯一的运行时守卫是 `isJsonValue`(往返可序列化性检查:拒绝 BigInt、函数、循环引用、非有限数等而****结构校验。一个损坏但仍为合法 JSON 的事件数据(字段类型错误、字段缺失)会静默往返,只有在后续消费方的 `switch` 才可能被捕获
2. **插件新增变体没有运行时契约。** 一个通过声明合并添加新 `SessionEventMap` 键的插件,在自身代码中获得了编译期类型,但没有任何机制校验它产出的值是否符合它所声明的形状——无论在生产者处、持久化边界还是重新加载时。
由此引出问题:事件词汇是否应迁移到 **Zod** 或其他运行时 schema 库,使持久化边界与插件边界拥有运行时 schema 而非被擦除的类型。
由此引出问题:事件词汇是否应迁移到 **Zod** 或其他运行时 schema 库,使持久化插件边界拥有运行时 schema 而非被擦除的类型。
本 RFC 界定这一问题的范围,不提出具体实现。
本 RFC 界定问题的范围,不提出具体实现。
## 为什么这不是一个持久化变更
## 为什么这不是一个持久化层的改动
很容易把「用 Zod 做序列化」理解为对 `dsh-session-persistence-jsonl/src/format.ts` 的局部改。但它不是,原因在于一个结构性事实:**插件无法通过声明合并扩展一个 Zod schema。** 声明合并是 TypeScript 编译期机制Zod schema 是运行时值。要用 Zod 校验事件,需要一个**运行时注册表**,每个产出事件的包向其贡献自己的 schema`ctx.sessionEvents.register('compaction/marker', z.object({…}))`),每个消费方从中读取。这个注册表——而非持久化后端——将成为词汇的真源,取代 merge-extensible interface。
很容易把「用 Zod 做序列化」理解为对 `dsh-session-persistence-jsonl/src/format.ts` 的局部改。但它不是,原因在于一个结构性事实:**插件无法 Zod schema 进行声明合并。** 声明合并是 TypeScript 编译期机制Zod schema 是运行时值。要用 Zod 校验事件,需要一个**运行时注册表**,每个产出事件的包package向其贡献自己的 schema`ctx.sessionEvents.register('compaction/marker', z.object({…}))`),每个消费方从中读取。这个注册表——而非持久化后端——将成为词汇的真源,取代 merge-extensible interface。
因此真正的提案是:**用运行时 schema 注册表替换编译期的 merge-extensible-map 模式,覆盖全仓库。** 这是一次核心词汇的重新设计。
因此真正的提案是:**用运行时 schema 注册表替换编译期的 merge-extensible-map 模式,范围覆盖整个仓库。** 这是一次核心词汇的重新设计。
## 影响范围(实测
## 影响范围(已度量
将事件/词汇表面迁移到运行时 schema至少涉及
- **六个 merge-extensible map**(约 370 行核心类型):`ContentBlockMap``MessageSourceMap``FinishReasonMap` `dsh-llm``TurnTriggerMap``TurnEndReasonMap``SessionEventMap` `dsh-session`)。
- **约 10 `declare module` 扩展点**,分布在 `dsh-agent``dsh-agent-loop``dsh-bash``dsh-llm``dsh-session``dsh-session-persistence``dsh-system-prompt``dsh-tools` 中——每都将从声明合并改为运行时 `register()` 调用。
- **事件生产**——agent loop 中 16 处 `session.append(...)` 调用——形状不变,但现在在边界处被校验。
- **约 7 个 switch 消费方**这些联合类型分支:`deriveMessages``dsh-session`)、`BlockAssembler``dsh-llm`)、`dsh-invariants` 插件、两个 LLM 适配器(`dsh-llm-deepseek``dsh-llm-pi-ai`)以及工具 schema 层(`dsh-tools`)。`assertNever` 对封闭联合的穷尽性 vs 对可扩展联合的 fall-through 约定(一条已文档化的 lint 规则)需要重新考量——运行时变体不具备静态穷尽性
- **`defineTool``InferArgs` DSL**`dsh-tools`),它从编译期 schema 规派生零强制转换的 `execute` 参数类型——这是当前方案的标杆用例。
- **文档**architecture.md该模式被描述为基础性的、[开发模式不变式](../../implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md),以及任何引用该模式的 RFC。
- **六个 merge-extensible map**(约 370 行核心类型):`ContentBlockMap``MessageSourceMap``FinishReasonMap`位于 `dsh-llm``TurnTriggerMap``TurnEndReasonMap``SessionEventMap`位于 `dsh-session`)。
- **约 10 `declare module` 扩展点**,分布在 `dsh-agent``dsh-agent-loop``dsh-bash``dsh-llm``dsh-session``dsh-session-persistence``dsh-system-prompt``dsh-tools` 各包中——每都将从声明合并改为运行时 `register()` 调用。
- **事件生产**——agent loop(智能体循环)中 16 处 `session.append(...)` 调用——形状不变,但现在在边界处被校验。
- **约 7 个 switch 消费方**这些联合类型进行分支:`deriveMessages``dsh-session`)、`BlockAssembler``dsh-llm`)、`dsh-invariants` 插件、两个 LLM(大语言模型)适配器(`dsh-llm-deepseek``dsh-llm-pi-ai`)以及工具 schema 层(`dsh-tools`)。`assertNever` 对封闭联合类型的穷举 vs 对可扩展联合类型的 fall-through 约定(一条已记录的 lint 规则)需要重新考量——运行时变体在静态层面不可穷举
- **`defineTool``InferArgs` DSL**`dsh-tools`),它从编译期 schema 规派生出零类型转换的 `execute` 参数类型——这是当前方案的标杆用例。
- **文档**architecture.md该模式被描述为基础性的、[dev-mode invariants](../../implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md),以及所有引用该模式的 RFC。
这是一次仓库级别的词汇重新设计,不是持久化的实现细节。
这是一次仓库级别的词汇重新设计,而非持久化的实现细节。
## 曾考虑的替代方案
### A. 维持现状——merge-extensible 类型 + 持久化边界 `isJsonValue`
保留编译期模式。持久化继续使用不透明 JSON + 可序列化性守卫。插件通过声明合并扩展;事件*形状*的正确性由生产负责,编译期由 TypeScript 强制,在开发模式下由 `dsh-invariants` 插件的结构检查强制
### A. 维持现状——merge-extensible 类型 + 持久化边界 `isJsonValue`
保留编译期模式。持久化继续使用不透明 JSON + 可序列化性守卫。插件通过声明合并扩展;事件*形状*的正确性由生产负责,编译期由 TypeScript 保证,开发模式下由 `dsh-invariants` 插件的结构检查保证
- **优点**:零变;插件扩展只需一行 `interface` 声明合并,具备完整类型推断且无运行时注册仪式;无新运行时依赖;`defineTool` DSL 与 `assertNever`尽性保持正常工作。
- **优点**:零变;插件扩展只需一行 `interface` 增补,享有完整类型推断,无需运行时注册仪式;无新运行时依赖;`defineTool` DSL 与 `assertNever`举继续工作。
- **缺点**:持久化边界和插件 seam 处无运行时结构校验;格式错误但仍为合法 JSON 的数据被延迟捕获。
### B. 仅对 header/封闭形状做校验schemastery事件保持不透明
仅对那些已有手写类型守卫的真正封闭形状加强校验——例如 JSONL 的 `HeaderLine` 守卫(`isHeaderLine`)——使用 **schemastery**仓库现有的 schema 库,已用于每个插件的 `static Config`。merge-extensible 事件联合保持不变。
### B. 仅对头部/封闭形状做校验schemastery事件仍为不透明
仅对那些已有手写类型守卫的真正封闭形状加以收紧——例如 JSONL 的 `HeaderLine` 守卫(`isHeaderLine`)——使用 **schemastery**(仓库现有的 schema 库,已用于每个插件的 `static Config`。merge-extensible 事件联合类型保持不变。
- **优点**:改动小,契合有约定schemastery非新库用声明式 schema 替换封闭形状上的手写守卫;无核心重设计。
- **缺点**:不解决事件数据校验问题;仅固定的元数据记录得到改善。
- **优点**:改动小,契合有约定schemastery非新库);用声明式 schema 替换封闭形状上的手写守卫;无核心重设计。
- **缺点**:不解决事件数据校验问题;仅固定的元数据记录得到改善。
### C. 为整个词汇建立运行时 schema 注册表Zod 或 schemastery
用运行时注册表替换 merge-extensible map生产向其贡献 schema持久化/消费方据其校验。
用运行时注册表替换 merge-extensible map生产向其贡献 schema持久化/消费路径据此校验。
- **优点**:持久化边界插件 seam 处真正的运行时校验;单一真源;支持通用工具(自动生成文档、模糊测试、协议格式检查)。
- **缺点**:上述完整影响范围;**Zod 目前不是直接依赖**(仅作为 `@earendil-works/pi-ai` 的传递依赖),仓库选定的 schema 库是 **schemastery**——广泛引入 Zod 本身就是一个依赖决策;声明合并的人体工学(一行插件扩展、完整推断)被运行时注册 + 手动类型接线取代;`assertNever`尽性保证弱化(运行时变体不具备静态穷尽性)。
- **优点**:持久化边界插件 seam 处获得真正的运行时校验;单一真源;可支撑通用工具(自动生成文档、模糊测试、协议格式检查)。
- **缺点**:上述全部影响范围;**Zod 目前不是直接依赖**(仅作为 `@earendil-works/pi-ai` 的传递依赖),仓库选定的 schema 库是 **schemastery**——广泛引入 Zod 本身就是一个依赖决策;声明合并的人体工学(一行插件扩展、完整推断)被运行时注册 + 手动类型接线取代;`assertNever`保证弱化(运行时变体在静态层面不可穷举)。
## 提案
暂缓。如果需要在持久化边界做运行时校验,**方案 B**(用 schemastery 校验封闭的 header 与元数据形状)是有约定的适度步骤。**方案 C** 是一架构决策,需要自己的实现 RFC包括 Zod 与 schemastery 之间做出选择。
推迟。如果需要在持久化边界做运行时校验,**方案 B**对封闭的头部和元数据形状使用 schemastery有约定的适度步骤。**方案 C** 是一架构决策,需要自己的实现 RFC其中包括 Zod 与 schemastery 之间选择。
## 验收标准
- 方案 C 只能通过自己的实现 RFC 推进,绝不作为持久化的附带效果
- 如果采纳方案 B封闭的 header/元数据形状JSONL 的 `isHeaderLine` 守卫及同类)改用 schemastery 校验替代手写守卫merge-extensible map 保持不
- 方案 C 只能通过自己的实现 RFC 推进,绝不作为持久化的附带改动
- 如果采纳方案 B封闭的头部/元数据形状JSONL 的 `isHeaderLine` 守卫及同类)改用 schemastery 校验替代手写守卫merge-extensible map 保持不
## 风险
- 暂缓意味着事件 `data` 在持久化边界仍无结构校验:格式错误但仍为合法 JSON 的数据被延迟捕获,由消费方的 `switch` 处理——这是现状的代价,有意接受。
- 如果方案 C 最终被采纳,人体工学损失是实的:一行声明合并变为运行时注册加手动类型接线,`assertNever` 的静态穷尽性保证弱化。
- 推迟意味着事件 `data` 在持久化边界仍无结构校验:格式错误但仍为合法 JSON 的数据被延迟捕获,由消费方的 `switch` 兜底——这是现状的代价,有意接受。
- 如果方案 C 最终被采纳,人体工学损失是实的:一行声明合并变为运行时注册加手动类型接线,`assertNever` 的静态穷保证弱化。
## 待解问题
- 如果采用注册表,schema 库选 **schemastery**(已在依赖树中,已配置 schema 库)还是 **Zod**(生态更丰富,目前仅为传递依赖)?同时维护两个 schema 库本身就是成本。
- 能否采用混合方案:保留编译期推断(使 `defineTool` 和插件 DX 不受影响),同时为每个变体添加*可选*的运行时 schema仅在持久化/协议边界校验而非每次进程内 append 校验?
- `dsh-invariants` 插件在开发模式下是否已覆盖了足够多的运行时形状缺口,使得边界校验仅在面对真正不可信输入(重新加载外部修改的日志)时才有必要?
- 如果采用注册表,库选 **schemastery**(已在仓库中,已作为配置 schema 库)还是 **Zod**(生态更丰富,目前仅为传递依赖)?同时维护两个 schema 库本身就是一种成本。
- 能否采用混合方案:保留编译期推断(使 `defineTool` 和插件开发体验不受影响),同时为每个变体添加*可选*的运行时 schema仅在持久化/协议边界校验而非每次进程内 append 校验?
- `dsh-invariants` 插件在开发模式下是否已覆盖了足够多的运行时形状缺口,使得边界校验仅在面对真正不可信输入(重新加载外部修改的日志)时才有必要?

View File

@@ -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-generic-long-running-tool-runtime.md: ea773a651b5aeec87179aac2ed419f176486977f
2026-06-20-generic-long-running-tool-runtime.zh.md: d50eb6858c11b9c98817b24bfe40f4e2c780f4b9
2026-06-20-generic-long-running-tool-runtime.zh.md: 25c1b282b19bb7da08b552485e348c23b11dcecf

View File

@@ -1,43 +1,43 @@
# RFC提取通用的长时运行工具运行时
[English](2026-06-20-generic-long-running-tool-runtime.md) | 中文
# RFC提取通用的长时运行工具运行时
Status: proposed
[English](2026-06-20-generic-long-running-tool-runtime.md) | 中文
## 问题
bash 能力 seam 同时支持前台命令和长时运行的后台任务。后台支持体量不小:抽象执行器暴露 `start``get``ownerOf``list``readOutput``kill``onTaskDone`本地执行器跟踪任务、增量读取、owner token、进程清理和完成监听;模型侧看到三个工具(`bash``bash_output``bash_kill`);工具插件将完成通知注入回所属 agent 的会话。本地执行器用 owner token 隔离任务访问,因为可预测的全局 task id 会带来跨会话的读取/终止风险。
bash 能力 seam 同时支持前台命令和长时运行的后台任务。后台支持体量不小:抽象执行器暴露 `start``get``ownerOf``list``readOutput``kill``onTaskDone`;本地执行器负责跟踪任务、增量读取、owner token、进程清理和完成监听模型侧看到三个工具`bash``bash_output``bash_kill`);工具插件将完成通知注入回所属 agent(智能体)的会话。本地执行器用 owner token 隔离任务访问,因为可预测的全局 task id 会带来跨会话的读取/终止风险。
[工具实操手册](../../../cookbook/adding-a-tool.md)已经指出了真正的设计异味:后台 bash 质上是寄居在个工具内部的通用长时运行工具基础设施。如果未来的工具也需要后台执行、轮询、终止、所有权和完成通知,这些语义不应藏在 `dsh-bash` 里。
[工具实操手册](../../../cookbook/adding-a-tool.md)已经指出了真正的设计异味:后台 bash 质上是寄居在个工具内部的通用长时运行工具基础设施。如果未来的工具也需要后台执行、轮询、终止、所有权和完成通知,这些语义不应藏在 `dsh-bash` 里。
## 提案
将长时运行任务的语义从 bash 上方抽出,放入一个与工具无关的运行时。bash 仍然能运行后台命令,但不再拥有 task id、ownership token、轮询、取消、完成通知以及模型侧「读取/终止此任务」命令等通用概念。
将长时运行任务的语义从 bash 上移到一个与工具无关的运行时。bash 仍然能运行后台命令,但不再拥有 task id、ownership token、轮询、取消、完成通知以及模型侧「读取/终止此任务」命令等通用概念。
该运行时应拥有:
- 稳定的 task id owner token按调用方的会话/agent 键
- 注册一个长时运行任务,附带增量输出的生产者和一个完成 promise。
- 稳定的 task id owner token按调用方的会话/agent 键。
- 注册一个长时运行任务,附带增量输出的生产者和一个完成 promise。
- 通用的 read/cancel/list 操作,对所有工具使用相同的跨会话授权规则。
- 向所属会话注入完成通知。
- 待处理/运行中/已完成任务状态的展示钩子bash 只提供命令特有的标签和输出格式化。
- 针对 pending/running/completed 任务状态的展示钩子bash 只提供命令特有的标签和输出格式化。
`dsh-bash` 随后只保留 bash 特有的执行契约:将请求解析为命令规格、运行前台命令,或启动进程并将其流/进程句柄交给通用运行时。`dsh-tool-bash` 保留模型侧的命令工具,但后续操作变为通用的长时运行工具操作(或 bash 向其注册的共享工具层),而非定制`bash_output`/`bash_kill` 管道。
`dsh-bash` 保留 bash 特有的执行契约:将请求解析为命令规格、运行前台命令,或启动进程并将其流/进程句柄交给通用运行时。`dsh-tool-bash` 保留模型侧的命令工具,但后续操作变为通用的长时运行工具操作,或者 bash 向其注册的共享工具,而不是专属`bash_output`/`bash_kill` 管道。
## 当前 seam 消费情况
当前消费方划分清晰:`dsh-tool-bash` 使用完整的前台/后台 seam而钩子桥接只使用前台的 `resolve``run`(带受信的 `stdin` `env`)。`get``list` 仅在测试中使用;`BashTask.done` 仅在实现内部用于 dispose资源释放生产环境的完成通知 `onTaskDone`。提取出的运行时应暴露单一的公开完成机制,保留钩子所需的简单前台路径,并决定后台的 `timeoutMs` 是否属于 `start`。如果运行时拥有进程 spawn还应集中处理目前重复的凭证清洗逻辑。
当前消费方划分清晰:`dsh-tool-bash` 使用完整的前台/后台 seam而钩子桥接只使用前台的 `resolve``run`(带受信的 `stdin` `env`)。`get``list` 仅在测试中使用;`BashTask.done` 仅在实现内部用于 dispose资源释放生产环境的完成通知使用 `onTaskDone`。提取出的运行时应暴露一个公开完成机制,保留钩子所需的简单前台路径,并决定后台的 `timeoutMs` 是否属于 `start`。如果拥有进程 spawn 的职责,还应集中处理目前重复的凭证清洗逻辑。
## 验收标准
- bash 特有的包不再定义通用的任务注册表、owner-token 授权、轮询、取消或完成通知机制。
- 一个共享的长时运行任务服务或工具层拥有这些语义,并为未来任何具备后台能力的工具的文档化路径。
- bash 后台行为仍可通过共享层使用,测试证明跨会话隔离依然成立。
- ACP 和快照 fixture测试前置数据通过共享任务词汇渲染后台 bash而非通过 bash 独有的生命周期语义。
- [工具实操手册](../../../cookbook/adding-a-tool.md)将长时运行工具指向共享运行时,而非告诉每个工具自行发明任务协议。
- bash 特有的包package不再定义通用的任务注册表、owner-token 授权、轮询、取消或完成通知机制。
- 一个共享的长时运行任务服务或工具层拥有这些语义,并被文档化为未来任何具备后台能力的工具的接入路径。
- bash 后台行为仍可通过共享层使用,测试证明跨会话隔离依然成立。
- ACP 和快照 fixture测试前置数据通过共享任务词汇渲染后台 bash而非通过 bash 专属的生命周期语义。
- [工具实操手册](../../../cookbook/adding-a-tool.md)将长时运行工具指向共享运行时,而不是让每个工具自行发明任务协议。
## 风险
bash 包失去了对一个已经可用的后台任务实现的本地所有权,实 PR 可能暂时搅动模型侧的工具名称或 transcript文本记录展示。如果最终结果是留下一份后台任务契约、而非让每个未来的长时运行工具克隆 bash 的私有协议,这种搅动是值得的。
bash 包失去了对一个已经可用的后台任务实现的本地所有权,实 PRPull Request可能暂时搅动模型侧的工具名称或 transcript文本记录展示。如果最终结果是留下一份后台任务契约,而不是让每个未来的长时运行工具克隆 bash 的私有协议,这种搅动是值得的。
<!-- rfc-format: alternatives-not-recorded (pre-format RFC) -->

View File

@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-06-30-pre-tool-input-rewrite.md: add84bfc76434eb25870f09e71860279663d291e
2026-06-30-pre-tool-input-rewrite.zh.md: c636cf42d5c55f0ee1192a46283f8cdbe8c4cadc
2026-06-30-pre-tool-input-rewrite.zh.md: 13d66208be07992fd414f2984c493557ad1e87a3

View File

@@ -1,53 +1,53 @@
# RFC工具执行前输入写——一致性设计
[English](2026-06-30-pre-tool-input-rewrite.md) | 中文
# RFC工具执行前输入写——一致性设计
Status: proposed
[English](2026-06-30-pre-tool-input-rewrite.md) | 中文
## 问题
[拦截 seam RFC](../../implemented/feature/2026-06-30-interception-seams.md) 将 `tools/pre-execute` 定义为一道 allow/deny/ask 门禁,作用于身份已受保护、参数已被深度冻结的执行对象。Claude Code 的 `PreToolUse` 钩子还提供了 `updatedInput`,因此忠实的桥接需要一个显式的写机制。写不能是对现有执行对象的可变逃逸口:它必须保持持久化历史、审计记录、展示层与实际执行值之间的一致性。
[拦截 seam RFC](../../implemented/feature/2026-06-30-interception-seams.md) 将 `tools/pre-execute` 定义为一道针对执行的允许/拒绝/询问门禁,此时执行的身份标识已受保护、参数已被深度冻结。Claude Code 的 `PreToolUse` 钩子还提供了 `updatedInput`,因此忠实的桥接需要一个显式的写机制。写不能是对现有执行对象的可变逃逸口:它必须保持持久化历史、审计记录、展示层与实际执行值之间的一致性。
## 问题本质:执行前参数的三个读取方
在 agent loop智能体循环工具调用的参数在工具执行之前就已提交到日志并被活跃消费方读取:
在 agent loop智能体循环工具调用的参数在工具执行**之前**就已提交到日志并被实时消费方读取:
1. **`assistant/message`** 在工具分发之前追加——它是 `deriveMessages()` 回放时的模型历史来源,因此携带的是模型自身生成的工具调用参数。
1. **`assistant/message`** 在工具分发之前追加——它是 `deriveMessages()` 回放时的模型历史来源,因此携带模型自身输出的工具调用参数。
2. **`tool/call`** 是持久化的审计记录,在 `ctx.tools.execute()` 之前追加。
3. **展示层实时读取 `tool/call.arguments`**ACP 桥接记住这些参数并传给 `presentResult``dsh-tool-bash` 从中派生卡片标题、rawInput、cwd 以及终端/后台处理方式。
3. **展示层实时读取 `tool/call.arguments`**ACPAgent Client Protocol桥接记住这些参数并传给 `presentResult``dsh-tool-bash` 从中派生卡片标题、rawInput、cwd 以及终端/后台处理方式。
如果只做执行层面的UI 会示一条命令而实际运行的是另一条,并且结果会对着错误的参数渲染。注册表目前阻止了这种失败模式:`arguments` 做 structured-clone 并深度冻结,将执行身份属性设为不可写,且不暴露任何可替换它们的测试 shim 或监听路径。写设计必须保持这一受保护的身份边界,而非削弱它。
如果只做执行层面的UI 会示一条命令而实际运行的是另一条,并且结果会对着错误的参数渲染。注册表目前通过以下方式防止这种失败模式:对 `arguments` 做 structured-clone 并深度冻结,将执行身份属性设为不可写,且不暴露任何可替换它们的测试 shim 或监听路径。写设计必须维护这一受保护的身份边界,而非削弱它。
## 提案
写是一「身份构造前的一致性事务」。当钩子提供 `updatedInput` 时,有效值必须在注册表构造不可变的 `ToolExecution` 之前确定,并原子地反映到全部三个读取方:
写是一「身份标识创建前的一致性事务」。当钩子提供 `updatedInput` 时,有效值必须在注册表构造不可变的 `ToolExecution` 之前确定,并且必须原子地反映到全部三个读取方:
- `tool/call` 审计事件记录**写后**的参数(原始参数保留在一个 sidecar 字段中用于审计追踪——钩子改了调用,原始参数生效参数都是值得保留的事实)。
- 派生历史中的 `assistant/message` 必须与实际执行一致——待评估的选项:就地写 assistant 消息中的工具调用块(改变模型「看到自己说过的话」),或记录一条单独的修正下一次请求携带。CC 的模型是让模型看到写已生效。
- 展示层(`presentCall`/`presentResult`)读取写后的参数UI 展示的是实际运行的内容。
- `tool/call` 审计事件记录**写后**的参数(原始参数保留在一个伴随字段中,作为审计线索——钩子改了调用,原始参数生效参数都是值得保留的事实)。
- 派生历史中的 `assistant/message` 必须与实际执行一致待评估的选项:就地写 assistant 消息中的工具调用块(改变模型「看到自己说了什么」),或记录一条单独的修正下一次请求携带。Claude Code 的模型是让模型看到写已生效。
- 展示层(`presentCall`/`presentResult`)读取写后的参数,使 UI 显示实际运行的内容。
`PreToolDecision` 当前的触发点上做扩展不够:此时两条持久化记录已存在,执行身份已受保护。实现必须要么将相关决策移到日志提交之前,要么在待处理的模型调用上增加一个专门的更早期改写决策。当循环将生效参数提交到历史和审计之后,再按常规构造不可变执行对象,并照常运行现有的 allow/deny/ask 与工具流水线。
`PreToolDecision` 当前的触发点上做扩展不够:此时两条持久化记录已存在,执行身份已受保护。实现必须将相关决策移到日志提交之前,或者增加一个专门的更早的重写决策点来处理待定的模型调用。agent loop 将生效参数提交到历史和审计之后,再构造普通的不可变执行对象,并照常运行现有的允许/拒绝/询问和工具流水线。
## 曾考虑的替代方案
### 为什么不直接修改执行对象?
允许 pre-execute 监听器赋值 `exec.arguments` 只能提供执行层面的写,模型历史、审计和展示层不会跟着变。保持身份受保护使得这种局部行为无法被表达。在一致性事务实现之前CC/Codex 桥接对 `updatedInput` 只做日志记录并发出警告,而非声称已兑现;循环分发`TODO(pre-tool-input-rewrite)` 锚定了这个缺失的更早阶段。
允许 pre-execute 监听器赋值 `exec.arguments` 只能提供执行层面的写,模型历史、审计和展示层不会随之改变。保持身份标识受保护使得这种局部行为不可表达。在一致性事务实现之前CC/Codex 桥接对 `updatedInput` 记录日志并发出警告,而非声称已兑现;循环分发`TODO(pre-tool-input-rewrite)` 标记了缺失的更早阶段。
## 验收标准
- 请求的写在 `ToolExecution` 身份创建之前完成解析,并原子地反映到全部三个读取方:`tool/call` 审计记录写后的参数(原始参数保留在 sidecar 字段)、派生历史与实际执行一致、展示层渲染写后的参数。
- 生效的 `ToolExecution.arguments` 在 pre-policy、guards、dispatch、post-policy 和最终观测的全过程中保持深度冻结且不可写;不引入任何可变 shim。
- CC/Codex 桥接兑现 `updatedInput`,不再输出忠实但降级的警告。
- 请求的写在 `ToolExecution` 身份标识创建之前解决,并原子地反映到全部三个读取方:`tool/call` 审计记录写后的参数(原始参数保留在伴随字段)、派生历史与实际执行一致、展示层渲染写后的参数。
- 生效的 `ToolExecution.arguments` 在 pre-policy、守卫、分发、post-policy 和最终观测全程保持深度冻结且不可写;不引入任何可变 shim。
- CC/Codex 桥接兑现 `updatedInput`,不再记录忠实但降级的警告。
## 风险
- `assistant/message` 中的工具调用块会改变模型「看到自己说过的话」;是否有提供方在回放时拒绝这种改,是一个必须在决策形态冻结前通过实验验证的开放问题。
- 更早期的改写阶段改变了 `assistant/message``tool/call`、钩子审计事件与执行之间的顺序关系;设计必须固定这一顺序,同时不削弱轮次封闭性或 call/result 邻接性。
- `assistant/message` 中的工具调用块会改变模型「看到自己说了什么」;是否有提供方在回放时拒绝这种改,是一个需要通过实验确定的开放问题,必须在决策形状冻结之前解决
- 更早的重写阶段改变了 `assistant/message``tool/call`、钩子审计事件与执行之间的顺序关系;设计必须固定这一顺序,同时不削弱轮次封闭性或调用/结果邻接性。
## 开放问题
- `assistant/message` 中的工具调用块是否会破坏某些提供方在回放时的预期?还是记录一条单独的修正更安全?
- 原始参数是否应保留在 `tool/call` 事件(审计)上?如果是,放在哪个字段?
- 写决策是移到日志提交之前,还是成为一个专门的更早 seam现有的 pre-tool allow/deny 钩子如何避免运行两次?
- 这与未来的权限 `ask` 流程(用户批准一个被写的调用)如何交互?
- `assistant/message` 中的工具调用块是否会破坏某些提供方在回放时的预期?还是单独的修正更安全?
- 原始参数是否应保留在 `tool/call` 事件(审计)上?如果是,放在什么字段?
- 写决策是移到日志提交之前,还是成为一个专门的更早 seam现有的 pre-tool 允许/拒绝钩子如何避免运行两次?
- 这与未来的权限 `ask` 流程(用户批准一个被写的调用)如何交互?

View File

@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-07-07-claude-code-and-codex-subagent-backends.md: 1ebf01dd8df0980f6c464be8b27033bdfab942f3
2026-07-07-claude-code-and-codex-subagent-backends.zh.md: 0ed5b42bc9d60b54ac610a8f34f8261f3aeefaed
2026-07-07-claude-code-and-codex-subagent-backends.zh.md: dd26a49962ee46a8ce0965557ff3dbd5805fdcfe

View File

@@ -1,4 +1,4 @@
# RFCClaude Code 与 Codex subagent 后端(进程外委派至外部编码 agent
# RFCClaude Code 与 Codex subagent 后端(外部编码 agent 的进程外委派
[English](2026-07-07-claude-code-and-codex-subagent-backends.md) | 中文
@@ -6,84 +6,84 @@ Status: proposed
## 问题
为 Claude Code 和 Codex 添加隔离的 subagent 提供方。既有的[命名提供方 seam](../../implemented/feature/2026-06-21-subagent-capability-seam.md) 和 [ACP 后端](../../implemented/feature/2026-06-22-acp-subagent-backend.md)已确立了进程边界的形状。harness 的一个轮次应当能够将一个自包含任务委派给上述任一产品,并接收其最终答,同时不暴露父进程的密钥,也不继承来自 `~/.claude``~/.codex` 的宿主配置。
为 Claude Code 和 Codex 添加隔离的 subagent 提供方。既有的[命名提供方 seam](../../implemented/feature/2026-06-21-subagent-capability-seam.md) 和 [ACP 后端](../../implemented/feature/2026-06-22-acp-subagent-backend.md)已确立了进程边界的形状。harness 的一个轮次应将一个自包含任务委派给上述任一产品,并接收其最终答,同时不暴露父进程的密钥,也不继承来自 `~/.claude``~/.codex` 的宿主配置。
##
##
两个兄弟提供方包ACP 后端的结构变体),加一次提取:
- `@deepseek-ai/dsh-subagent-claude-code`:通过 `@anthropic-ai/claude-agent-sdk``query()` 驱动一个 Claude Code 子进程SDK 在父进程中运行,并将其捆绑`claude` CLI 作为子进程 spawn。提供方名称 `claude-code`:子进程是 Claude Code 这个**产品**,而非 Anthropic 模型适配器——"claude" 保留给未来的 `dsh-llm` 适配器。
- `@deepseek-ai/dsh-subagent-codex`spawn `codex app-server`,通过其 JSON-RPC-over-stdio 协议,使用包内一个手写的换行 JSON 客户端(约 200300 行)驱动一个 thread/turn
- `@deepseek-ai/dsh-subagent-process`:纯库(`subagent-inprocess` 先例),提取 `dsh-subagent-acp` 已有且两个新后端都需要的内容:凭证环境清洗(`SENSITIVE_ENV_PATTERN`/`buildChildEnv`、EOF → SIGTERM → SIGKILL 的 dispose 阶梯,以及新的隔离配置目录辅助函数(`mkdtemp` 创建、尽力删除。ACP 后端迁移到该库上;`bash-local` 的兄弟副本保持不动以限制变更范围。
- `@deepseek-ai/dsh-subagent-claude-code`:通过 `@anthropic-ai/claude-agent-sdk``query()` 驱动一个 Claude Code 子进程SDK 在父进程中运行,并将其内置`claude` CLI 作为子进程 spawn。提供方名称 `claude-code`:子进程是 Claude Code 这个**产品**,而非 Anthropic 模型适配器——"claude" 保留给未来的 `dsh-llm` 适配器。
- `@deepseek-ai/dsh-subagent-codex`spawn `codex app-server`,通过其 JSON-RPC-over-stdio 协议驱动一个 thread/turn,使用包内一个手写的换行 JSON 客户端(约 200300 行)。
- `@deepseek-ai/dsh-subagent-process`:纯库(沿用 `subagent-inprocess` 先例),提取 `dsh-subagent-acp` 已有且两个新后端都需要的内容:凭证环境清洗(`SENSITIVE_ENV_PATTERN`/`buildChildEnv`、EOF → SIGTERM → SIGKILL 的 dispose 阶梯,以及新的隔离配置目录辅助函数(`mkdtemp` 创建、尽力删除。ACP 后端迁移到该库上;`bash-local` 的兄弟副本保持不动以限制变更范围。
两个提供方遵循 ACP 后端契约:每次 `start` 创建一个全新子进程、一次 prompt 往返、不继承父上下文也不声明可选能力、忽略 `request.parent``request.agentOptions`、使用随机品牌 agent id。`result` 不 reject子进程失败映射为 stop reason原始错误送入 logger。每个提供方不同的工具名挂载 `dsh-tool-subagent`。工具结果是唯一新增的模型可见产物,因此不需要新的会话事件;工作区变更仍是 transcript 回放之外的环境副作用。
两个提供方遵循 ACP 后端契约:每次 `start` 创建一个全新子进程、一次 prompt 往返、不继承父上下文也不声明可选能力、忽略 `request.parent``request.agentOptions`、使用随机品牌 agent id。`result` 不 reject子进程失败映射为 stop reason原始错误送入 logger。每个提供方不同的工具名挂载 `dsh-tool-subagent`。工具结果是唯一新增的模型可见产物,因此无需新的会话事件;工作区变更仍是 transcript(文本记录)回放之外的环境副作用。
## 已验证的接口事实(固定版本)
两个集成面在本提案之前均已针对固定实现进行了验证——读类型与捆绑源码、运行 keyless spike——而非仅依赖厂商文档。固定版本是验证基线不是运行时契约后端不执行运行时版本探测`codex --version`、无 SDK 版本嗅探)。兼容性在开发时强制执行——每次依赖升级都重跑 keyless 套件以验证真实加载路径——运行时则通过大声失败来保障:协议层的意外通过 `onError` 结算为 `error`,绝不静默异常。
两个集成面在本提案之前均已针对固定版本进行了验证——读类型与打包源码、运行无需密钥的 spike——而非仅依赖厂商文档。固定版本是验证基线不是运行时契约后端不执行运行时版本探测`codex --version`、无 SDK 版本嗅探)。兼容性在开发时强制执行——每次依赖升级都会针对真实加载路径重跑无密钥套件——运行时则通过大声失败来保障:协议层的意外通过 `onError` 结算为 `error`,绝不静默异常。
**`@anthropic-ai/claude-agent-sdk` 0.3.202。** `options.env` 会**替换**子进程环境(不与 `process.env` 合并),这正是清洗所需的行为`settingSources` 默认加载所有文件系统设置——隔离要求显式传入 `[]`。结果子类型为 `success` | `error_during_execution` | `error_max_turns` | `error_max_budget_usd` | `error_max_structured_output_retries`。中止时 SDK 自行升级 CLI 子进程:立即关闭 stdin若子进程忽略则约 2 秒后发送 SIGTERM已观察到无残留进程——无需自定义 kill 回退。`outputFormat: {type: 'json_schema'}``agents` 选项已存在,为 seam 的 `outputSchema` 能力和命名 subagent 类型提供了未来着陆点;者均不在本 RFC 范围内。
**`@anthropic-ai/claude-agent-sdk` 0.3.202。** `options.env` 会**替换**子进程环境(不与 `process.env` 合并),恰好满足清洗需求`settingSources` 默认加载所有文件系统设置——隔离要求显式传入 `[]`。结果子类型为 `success` | `error_during_execution` | `error_max_turns` | `error_max_budget_usd` | `error_max_structured_output_retries`。中止时 SDK 自行升级 CLI 子进程:立即关闭 stdin约 2 秒后若子进程未退出则发送 SIGTERM已观察到无残留进程——无需自定义 kill 回退。`outputFormat: {type: 'json_schema'}``agents` 选项已存在,为 seam 的 `outputSchema` 能力和命名 subagent 类型提供了未来着陆点;者均不在本 RFC 范围内。
**codex CLI 0.142.5`codex app-server`v2 词汇)。** LF 分隔的 JSONJSON-RPC 2.0 形状但省略 `"jsonrpc"` 头。
- 生命周期:`initialize{clientInfo}` + `initialized``thread/start`(接受 `cwd``model``sandbox``approvalPolicy``ephemeral`;未认证即可成功)→ `turn/start{threadId, input:[{type:'text',text}]}` 立即返回一个 `inProgress` 的 turn终止信号是携带 `Turn{status: completed|interrupted|failed|inProgress, error}``turn/completed` 通知。
- 审批服务端发起的请求——`item/commandExecution/requestApproval``item/fileChange/requestApproval``item/permissions/requestApproval``item/tool/requestUserInput``mcpServer/elicitation/request`——以 `accept`/`decline` 系列决策应答。
- 认证:`account/login/start{type:'apiKey', apiKey}` 是一等 RPC`account/read` 报告 `requiresOpenaiAuth`——且未认证的 `turn/start` 不会快速失败(它会挂在重试中),因此后端必须预检认证并大声结算 `error`,而非等待 turn。
- 隔离:`CODEX_HOME` 重定向被尊重(`initialize` 响应会回显它,测试可据此断言隔离),`ephemeral: true` 的 thread 完全不留会话文件。
- 审批服务端发起的请求——`item/commandExecution/requestApproval``item/fileChange/requestApproval``item/permissions/requestApproval``item/tool/requestUserInput``mcpServer/elicitation/request`——以 `accept`/`decline` 系列决策应答。
- 认证:`account/login/start{type:'apiKey', apiKey}` 是一等 RPC`account/read` 报告 `requiresOpenaiAuth`——且未认证的 `turn/start` 不会快速失败(它会挂在重试中),因此后端**必须**预检认证状态,并在失败时大声结算 `error`,而非等待 turn。
- 隔离:`CODEX_HOME` 重定向被尊重(`initialize` 响应会回显它,测试可据此断言隔离),`ephemeral: true` 的 thread 不留任何会话文件。
## 隔离与凭证
认证仅使用 API key。每次运行使用一个全新的配置目录Claude Code 用 `CLAUDE_CONFIG_DIR` 配合 `settingSources: []`Codex 用 `CODEX_HOME`dispose 时尽力删除;配置也可选择一个持久目录。共享的子进程环境辅助函数转发 `PATH``HOME``TMPDIR`、locale代理设置等普通值,移除凭证形的名称,并叠加显式的 `config.env`。Claude Code 通过该叠加接收 API keyCodex 通过 `account/login/start` 接收,而非手写认证文件。
认证方式仅限 API key。每次运行使用一个全新的配置目录Claude Code 用 `CLAUDE_CONFIG_DIR` 配合 `settingSources: []`Codex 用 `CODEX_HOME`dispose 时尽力删除;配置也可选择一个持久目录。共享的子进程环境辅助函数转发 `PATH``HOME``TMPDIR`、locale代理设置等普通值,移除凭证形的名称,并叠加显式的 `config.env`。Claude Code 通过该叠加接收 API keyCodex 通过 `account/login/start` 接收,而非手写认证文件。
## 权限与审批策略
每个后端暴露其引擎原生策略词汇。Claude Code 默认 `permissionMode: default` 配合 `permission: reject`Codex 默认 `sandboxMode: read-only``approvalPolicy: never`,以及相同的拒绝回退。示例可选择启用 `acceptEdits``workspace-write`。已知的审批、用户输入和 elicitation 请求接收配置的应答;未知方法接收 method-not-found未知通知被消费。没有 prompt 到达人类,子进程也不会因等待不可用的输入而无限挂起。
每个后端暴露其引擎原生策略词汇。Claude Code 默认 `permissionMode: default` 配合 `permission: reject`Codex 默认 `sandboxMode: read-only``approvalPolicy: never`,以及相同的拒绝回退。示例可选择启用 `acceptEdits``workspace-write`。已知的审批、用户输入和 elicitation 请求接收配置的应答;未知方法接收 method-not-found未知通知被消费。没有 prompt 到达人类,子进程也不会因等待不可用的输入而无限挂起。
## StopReason 映射
Claude Code`success``completed``error_max_turns``error_during_execution``error_max_budget_usd``error_max_structured_output_retries``error`(与 ACP 对 `max_turn_requests` 的处理对齐:未完成的任务不成功);生成器中止 → `aborted`;未知值 → `error`。Codex`Turn.status` `completed``completed``interrupted``aborted``failed``codexErrorInfo: 'contextWindowExceeded'``max-tokens`,其他 `failed``error`;传输/spawn/认证预检失败 → `error`(若已请求取消则为 `aborted`)。两者中,`cancel()` 采用 ACP 形状:标志位 + abort/interrupt + 一个 cancel-settled 竞争分支,使不合作的子进程无法阻塞结果。
Claude Code`success``completed``error_max_turns``error_during_execution``error_max_budget_usd``error_max_structured_output_retries``error`(与 ACP 对 `max_turn_requests` 的处理对齐:未完成的任务不成功);生成器中止 → `aborted`;未知值 → `error`。Codex`Turn.status` `completed``completed``interrupted``aborted``failed``codexErrorInfo: 'contextWindowExceeded'``max-tokens`,其他 `failed``error`;传输/spawn/认证预检失败 → `error`(若已请求取消则为 `aborted`)。两者中,`cancel()` 采用 ACP 形状:标志位 + abort/interrupt + 一个 cancel-settled 竞争分支,使不合作的子进程无法阻塞结果。
活性姿态明确声明teardown 时序是配置项turn 时长不是。两个后端将 dispose 阶梯的宽限期作为带默认值的已验证配置字段ACP 后端的 `disposeEofGraceMs`/`disposeGraceMs` 形状,由提取库承载),但刻意**不设** turn 时长或启动超时——与 ACP 一致turn 期间的活性由调用方通过 `cancel()`/abort signal 掌控subagent turn 合理地可达数分钟, Codex 认证预检消除了唯一验证的必然挂起场景;需要墙钟上限的部署从父进程取消即可。
活性姿态明确声明teardown 时序是配置项turn 时长不是。两个后端将 dispose 阶梯的宽限期作为带默认值的已验证配置字段ACP 后端的 `disposeEofGraceMs`/`disposeGraceMs` 形状,由提取库承载),但**刻意不设** turn 时长或启动超时——与 ACP 一致turn 期间的活性由调用方通过 `cancel()`/abort signal 掌控subagent turn 合理地可达数分钟, Codex 认证预检消除了唯一验证的必然挂起场景;需要墙钟上限的部署从父取消即可。
## 测试
每个适用层级都要求覆盖:
- **Keyless 单元/集成:** 通过真实 SDK 驱动一个假 Claude CLI通过真实 wire 客户端驱动一个脚本化的 Codex app-server。在逐文件 100% 覆盖率下,覆盖往返、每 stop 映射、两条取消路径及预中止、权限策略、未知消息、spawn 失败、reload 清理、导出形状、清洗后的环境、临时目录删除,以及 Codex 认证预检失败。
- **带 key 的 e2e** 每个真实引擎在 `acceptEdits``workspace-write` 下执行文件操作;跳过时命名缺失的二进制文件或 key,并断言无残留子进程。
- **快照:** `TODO(claude-code-subagent-replay)``TODO(codex-subagent-replay)` 延后,等待 [subagent 回放 RFC](../../implemented/testing/2026-06-22-subagent-snapshot-replay.md) 描述的进程特定回放形状。
- **无密钥单元/集成测试** 通过真实 SDK 驱动一个假 Claude CLI通过真实协议客户端驱动一个脚本化的 Codex app-server。在逐文件 100% 覆盖率下,验证往返、每 stop 映射、两条取消路径及预中止、权限策略、未知消息、spawn 失败、reload 清理、导出形状、清洗后的环境、临时目录删除,以及 Codex 认证预检失败。
- **有密钥 e2e 测试** 每个真实引擎在 `acceptEdits``workspace-write` 下执行文件操作;跳过时命名缺失的二进制或密钥,并断言无残留子进程。
- **快照测试** 标记为 `TODO(claude-code-subagent-replay)``TODO(codex-subagent-replay)` 推迟,等待 [subagent 回放 RFC](../../implemented/testing/2026-06-22-subagent-snapshot-replay.md) 描述的进程特定回放形状。
## 曾考虑的替代方案
### 为什么不用官方 `@openai/codex-sdk` 而手写客户端?
### 为什么不用官方 `@openai/codex-sdk` 而手写客户端?
dispose 阶梯和环境清洗要求拥有子进程spawn 参数、env、信号、exit 等待SDK 隐藏了进程。协议格式极其简单LF JSON形状可按固定版本生成`codex app-server generate-json-schema`仓库先例(`hook-protocol`)是自有精简协议核心而非包装他人运行时。SDK 能节省协议演进的维护成本,但代价是失去本后端存在的意义所在的精确控制。
dispose 阶梯和环境清洗要求拥有子进程spawn 参数、env、信号、exit 等待SDK 隐藏了进程。协议格式极其简单LF JSON形状可按固定版本生成`codex app-server generate-json-schema`),仓库先例(`hook-protocol`)是拥有薄协议核心而非包装他人运行时。SDK 能节省协议演进的维护成本,但代价是失去本后端存在的意义所在的精确控制。
### 为什么不用模型可见的 `subagent_type` 参数(单一 Task 风格工具)?
Claude Code 自的 Task 工具将 subagent 类型放在模型可见的 schema 中,选择一个 prompt + 工具集人格。这里的选择是在**执行引擎**之间做出的,而只有部署者知道哪些引擎配置了凭证——因此选择留在部署配置,保持 `dsh-tool-subagent` 文档化的一提供方一工具契约。人格的类型选择器应是针对工具的独立 RFC而非后端。
Claude Code 自的 Task 工具将 subagent 类型放在模型可见的 schema 中,选择一个 prompt + 工具集人格。这里的选择是在**执行引擎**之间做出的,而只有部署者知道哪些引擎配置了凭证——因此选择留在部署配置,保持 `dsh-tool-subagent` 文档中的「一个提供方对应一个工具契约。人格风格的类型选择器应是针对工具的另一个 RFC而非针对后端。
### 为什么不用登录态凭证和用户自的配置?
### 为什么不用登录态凭证和用户自的配置?
继承 `~/.claude` / `~/.codex`订阅登录、用户设置、skill、MCP 服务器)会子进程行为依赖宿主机状态,并在 ACP 后端和 bash 执行器确立的「凭证通过 `config.env` 显式进入,绝不隐式继承」规则上打一个隐式例外。仅 API key 加强制配置目录隔离保持了运行可复现;需要共享状态的部署可以意将配置目录字段指向一个持久目录。
继承 `~/.claude` / `~/.codex`订阅登录、用户设置、skill、MCP 服务器)会使子进程行为依赖宿主机状态,并在 ACP 后端和 bash 执行器确立的「凭证通过 `config.env` 显式进入,绝不隐式继承」规则上打一个隐式例外。仅 API key 加强制配置目录隔离使运行可复现;需要共享状态的部署可以意将配置目录字段指向一个持久目录。
### 为什么不为 Claude Code keyless 测试注入一个驱动 seam
### 为什么不为 Claude Code 无密钥测试注入驱动 seam
注入一个`query()` 会 mock 我们自己的边界,使真实 SDK 加载路径未被测试docs/testing.md 中的 real-over-mock 策略。曾考虑此方案的风险——SDK↔CLI 的 stream-json 控制协议是内部——已被 spike 消除:假 CLI harness 今天能对真实固定版本的 SDK 工作。如果 SDK 升级破坏了 mockkeyless 套件会让升级 PR 失败,这正是门禁在发挥作用。
注入假 `query()` 会 mock 我们自己的边界,使真实 SDK 加载路径未被测试docs/testing.md 中的 real-over-mock 策略。曾考虑此方案的风险——SDK↔CLI 的 stream-json 控制协议是内部实现——已被 spike 消除:假 CLI harness 今天能对真实固定版本的 SDK 正常工作。如果 SDK 升级破坏了 mock无密钥套件会让升级 PR 失败,这正是门禁在发挥作用。
### 为什么不用 ACP 适配器(如 `claude-code-acp`)复用既有后端?
社区 shim 将两个引擎包装为 ACP这会它们在 `dsh-subagent-acp` 上变成「仅配置」。但这在 harness 与引擎之间插入了一个非官方第三方层,抹了本 RFC 暴露的原生控制面permissionMode、sandboxMode/approvalPolicy、配置目录隔离、apiKey RPC并以 shim 的发布节奏换取第一方协议的稳定性。第一方接口——Agent SDK 和 app-server——才是受支持的集成点。
社区 shim 将两个引擎包装为 ACP这会使它们在 `dsh-subagent-acp` 上变成「仅配置」。但这在 harness 与引擎之间插入了一个非官方第三方层,抹了本 RFC 暴露的原生控制面permissionMode、sandboxMode/approvalPolicy、配置目录隔离、apiKey RPC并以 shim 的发布节奏替换了第一方协议的稳定性。第一方接口——Agent SDK 和 app-server——才是受支持的集成点。
## 验收标准
同时配置了两个引擎和 key 的机器上:一个 REPL 驱动的模型通过 `subagent_claude_code` 完成一个真实文件任务,通过 `subagent_codex` 完成另一个,工具结果为子进程的最终答,父会话日志中仅有 `tool/call` + `tool/result`Keyless 套件在无凭证环境以逐文件 100% 覆盖率通过,断言隔离(清洗后的子进程 env、dispose 后无残留临时配置目录)以及 `~/.claude` / `~/.codex` 的存在与否不影响子进程行为。取消父轮次后两个后端在有界时间内静默无残留子进程。e2e 套件干净地自跳过,命名缺失的前置条件。
两个引擎和密钥均已配置的机器上:一个 REPL 驱动的模型通过 `subagent_claude_code` 完成一个真实文件任务,通过 `subagent_codex` 完成另一个,工具结果为子进程的最终答,父会话日志中仅有 `tool/call` + `tool/result`无密钥套件在无凭证环境以逐文件 100% 覆盖率通过,断言隔离(清洗后的子进程环境、dispose 后无残留临时配置目录),并断言 `~/.claude` / `~/.codex` 的存在与否不影响子进程行为。取消父轮次后两个后端在有界时间内静默无残留子进程。e2e 套件干净地自跳过,命名缺失的前置条件。
## 风险
- `codex app-server` CLI flag 标记为实验性,其 v1/v2 词汇共存;客户端固定 0.142.5、仅实现 v2、消费未知方法/通知而不崩溃,但未来 codex 升级仍可能迫使返工(每次升级重新生成 schema 并重跑 keyless 套件——这是上述「不做运行时版本探测」立场背后的开发时强制执行)。
- Claude Code 假 CLI mock 依赖一个内部协议:任何 SDK 升级都必须通过 keyless 套件,控制协议的破坏性变更意味着返工 mock回退方案上面否决的驱动注入 seam 成为逃生口)。
- SDK 的 optionalDependencies 每平台约 280MB——已接受限制在单个后端包内。
- SDK 的 SIGKILL 分支EOF→SIGTERM 之后)未被观察到,信任其存在e2e 保留无残留进程断言。
- Codex 是部署前置条件(无 npm 捆绑的二进制文件);缺失或不兼容的二进制文件表现为大声的 spawn/协议 `error`,而非版本探测。
- 每次运行付出一个全新子进程的代价,且仅最终答浮出——思考、工具卡片和用量被消费后丢弃;池、中间进度浮出、`sendMessage`/`resume`、通过 SDK 的 `outputFormat` 实现 `outputSchema`、以及通过 SDK 的 `agents` 选项实现命名 subagent 类型,均为刻意延后
- `codex app-server` CLI 标记为实验性,其 v1/v2 词汇共存;客户端固定 0.142.5、仅实现 v2、未知方法/通知消费而不崩溃,但未来 codex 升级仍可能迫使返工(每次升级重新生成 schema 并重跑无密钥套件——这是上述「不做运行时版本探测」立场背后的开发时强制执行)。
- Claude Code 假 CLI mock 依赖一个内部协议:任何 SDK 升级都必须通过无密钥套件,控制协议的破坏性变更意味着返工 mock回退方案上面否决的驱动注入 seam 成为逃生口)。
- SDK 的 optionalDependencies 每平台约 280MB——已接受限制在单个后端包内。
- SDK 的 SIGKILL 分支EOF→SIGTERM 之后)未被观察到,信任其实现e2e 保留无残留进程断言。
- Codex 是部署前置条件(无 npm 内置二进制);缺失或不兼容的二进制大声的 spawn/协议 `error` 呈现,而非版本探测。
- 每次运行付出一个全新子进程的代价,且仅最终答浮出——思考、工具卡片和用量被消费后丢弃;连接池、中间进度浮出、`sendMessage`/`resume`、通过 SDK 的 `outputFormat` 实现 `outputSchema`、以及通过 SDK 的 `agents` 选项实现命名 subagent 类型,均为刻意推迟

View File

@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-07-08-interactive-side-sessions.md: 250a906d9ec339399a0e0e29e70b2b8dc189fa72
2026-07-08-interactive-side-sessions.zh.md: 17d416e2297320e8dfa238569230ecdec91dfa32
2026-07-08-interactive-side-sessions.zh.md: d86a2b69232bc8ccad78555911b44cf727780e0c

View File

@@ -1,41 +1,41 @@
# RFC交互式侧会话与合并回写
Status: proposed
[English](2026-07-08-interactive-side-sessions.md) | 中文
Status: proposed
## 问题
用户可能希望在不改变当前会话主上下文的前提下探索一个问题。现有原语无法提供这种产品形态:[session-store fork](../../implemented/feature/2026-06-30-session-store-fork-api.md) 创建的是一个无关联的会话,而 [fork subagent](../../implemented/feature/2026-06-21-subagent-capability-seam.md) 是模型驱动的任务,其 transcript文本记录会折叠为一条工具结果。两者都不能给用户一个独立的对话也都不能将结论带着来源信息写回父会话。
用户可能希望在不改变当前会话主上下文的前提下探索一个来自活跃会话的问题。现有原语无法提供这种产品形态:[session-store fork](../../implemented/feature/2026-06-30-session-store-fork-api.md) 创建的是一个无关联的会话,而 [fork subagent](../../implemented/feature/2026-06-21-subagent-capability-seam.md) 是模型驱动的任务,其 transcript文本记录会折叠为一条工具结果。两者都不能给用户一个独立的对话也都不能将结论带着出处信息记录回父会话。
## 提案
**侧会话side session** 是一个普通的活跃会话,从源会话最后一个已完成轮次 fork 而来,附属于自己的 agent只读顾问的角色运行,并能**合并回写**一条精笔记。
**侧会话side session** 是一个普通的活跃会话,从源会话最后一个已完成轮次 fork 而来,绑定到自己的 agent定位为只读顾问,并能**合并回写**一条精笔记。
- **Fork 并附属** 以父会话的衡已完成轮次前缀创建子会话,并在其元数据中标记 `parentSession``seedLength`。这组合了 `ctx.agents.create({ seed, meta })`;不新增核心服务或 session-store 方法。
- **顾问框架** 创建后注入一条插件来源的 `context/message`,告知子会话只做解释,不执行变更或继续任务。保持系统提示词逐字节一致,以保留提供方对继承历史的前缀缓存。
- **合并回写:** 向子会话请求一条有长度上限的交还内容,然后向父会话注入一条插件来源的 `context/message`。父会话的下一次请求在其日志位置看到,保持回放与[请求可重建性](../../implemented/architecture/2026-07-05-reconstructable-requests.md),无需新增会话事件。
- **呈现:** 调用方式、会话切换与交还内容的渲染属于首个客户端拥有的界面。本 RFC 仅规定与界面无关的机制。
- **Fork 并绑定** 以父会话的衡已完成轮次前缀创建子会话,并在其元数据中标记 `parentSession``seedLength`。这组合了 `ctx.agents.create({ seed, meta })`;不新增核心服务或 session-store 方法。
- **顾问定位** 创建后注入一条插件来源的 `context/message`,告知子会话只做解释,不执行变更或继续任务。保持系统提示词逐字节一致,可在继承的历史上保留提供方的前缀缓存。
- **合并回写:** 向子会话请求一条有长度上限的 handback,然后向父会话注入一条插件来源的 `context/message`。父会话的下一次请求在其日志位置看到该消息,保持回放与[请求可重建性](../../implemented/architecture/2026-07-05-reconstructable-requests.md),无需新增会话事件。
- **呈现:** 调用方式、会话切换与 handback 渲染属于首个客户端拥有的界面。本 RFC 仅规定与界面无关的机制。
回退产品化、会话树视图、面向模型的侧会话工具,以及 `forkName`/`mergedInto` 元数据不在本 RFC 范围内。一次 live-adapter 原型验证了源日志隔离、继承上下文、多轮子会话交互,以及合并回写在父会话下一轮次中的可见性。
回退产品化、会话树视图、面向模型的侧会话工具,以及 `forkName`/`mergedInto` 元数据不在本 RFC 范围内。一次 live-adapter spike 已验证了源日志隔离、继承上下文、多轮子会话交互,以及合并回写在父会话下一轮次中的可见性。
## 曾考虑的替代方案
- **使用 subagent seam** 否决。侧会话是用户驱动的、客户端可见的,且可能父会话的一个轮次存活更久subagent 是模型驱动的运行,返回一条工具结果。
- **修改子会话的系统提示词:** 默认否决,因为任何字节变化都会从第零个 token 起使前缀缓存失效。部署方仍可选择更强的隔离。
- **新增 `sidechat/*` 事件:** 推迟。插件来源的 `context/message`提供持久性、来源信息与回放能力;只有当某个界面需要区分渲染时,专用事件才有正当理由。
- **现在就绑定协议界面:** 否决。当前 UI 由客户端拥有。实时呈现最终必须从持久消息派生,以确保回放渲染出相同的记录。
- **使用 subagent seam** 否决。侧会话是用户驱动的、客户端可见的,且可能存活超过父会话的一个轮次subagent 是模型驱动的运行,返回一条工具结果。
- **修改子会话的系统提示词:** 默认否决,因为任何字节变化都会从第零个 token 起使前缀缓存失效。部署方仍可选择这种更强的隔离方式
- **新增 `sidechat/*` 事件:** 延后。插件来源的 `context/message` 已提供持久性、出处与回放能力;只有当某个界面需要差异化渲染时,专用事件才有正当理由。
- **现在就绑定一个协议界面:** 否决。当前 UI 由客户端拥有。实时呈现最终必须从持久消息派生,以使回放渲染出相同的记录。
## 验收标准
- Fork 不改源会话,创建一个子会话,子会话具有衡的已完成轮次前缀、`parentSession``seedLength`,以及逐字节一致的系统提示词。
- 顾问框架在子会话追加历史的头部恰好添加一条插件来源的 `context/message`,而非修改其系统提示词。
- Fork 不改源会话,创建子会话具有衡的已完成轮次前缀、`parentSession``seedLength`,以及逐字节一致的系统提示词。
- 顾问定位在子会话追加历史的头部恰好添加一条插件来源的 `context/message`,而非修改其系统提示词。
- 合并回写恰好添加一条有长度上限的 `context/message`,来源为 `plugin: sidechat`;父会话的下一次请求与回放在相同位置看到它。
- 父会话与子会话并发运行,日志流之间无串扰。
- 父会话与子会话并发运行,日志流之间无串扰。
- 单元测试覆盖 fork/attach 与合并回写;快照覆盖率随首个绑定界面一起落地。
## 风险
- 只读行为在 `tools/pre-execute` 拒绝门禁强制执行之前仅为建议性[拦截 seam](../../implemented/feature/2026-06-30-interception-seams.md) 可在不改变本机制的前提下添加该门禁。
- 经过压缩compaction的源会话 fork 出的是其压缩视图,因此绑定界面应当告知用户子会话继承的是摘要而非被替换的轮次。
- 反复的交还内容会消耗父会话上下文。每次合并的长度上限约束了单条笔记的大小;后续整合属于压缩的职责。
- 只读行为在 `tools/pre-execute` 拒绝门禁强制执行之前仅为建议性[拦截 seam](../../implemented/feature/2026-06-30-interception-seams.md) 可在不改变本机制的前提下添加该门禁。
- 经过压缩compaction的源会话 fork 出的是其压缩视图,因此绑定界面应当告知用户子会话继承的是摘要而非被替换的轮次。
- 反复的 handback 会消耗父会话上下文。每次合并的长度上限约束了单条笔记的大小;后续的合并整理属于上下文压缩的职责。

View File

@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-07-10-sqlite-session-query-provider.md: 8b67baf420433feca9d5cd09d58852bb9b1545a9
2026-07-10-sqlite-session-query-provider.zh.md: de97e59ac6f2f90a738ff5c8b9c2872d54ba3954
2026-07-10-sqlite-session-query-provider.zh.md: ad6b44363ab54b66b597941adb13938f27971bd5

View File

@@ -6,48 +6,48 @@ Status: proposed
## 问题
精确读取服务 `ctx.sessionQuery` 有意不维护派生索引。大规模持久化的历史记录需要全文搜索,而不能在每次查询扫描所有事件;同时,当前活跃会话需要一个比上次持久性检查点更新的覆盖层。搜索还需要具体的排序、摘要片段、过滤、分页、取消以及重建行为。
精确读取 `ctx.sessionQuery` 服务有意不维护派生索引。大规模持久化的历史记录需要全文搜索,而不每次查询扫描全部事件;当前活跃会话需要一个比上次持久性检查点更新的覆盖层。搜索还需要具体的排序、摘要片段、过滤、分页、取消以及重建行为。
如果把这些关注点拆分到一个推测性的 provider 协调器和一个数据库实现会产生两个耦合的协调状态机。第一个真实实现应当将源观察、提取、SQLite 事务、generation 管理和查询作为一个完整生命周期来拥有。
如果把这些关注点拆分到一个推测性的 provider 协调器和一个数据库实现之间会产生两个耦合的协调状态机。第一个真实实现应当将源观察、提取、SQLite 事务、generation 管理和查询作为一个完整生命周期来拥有。
## 提案
在精确读取包旁新增 `@deepseek-ai/dsh-session-query-sqlite`。该包将暴露一个搜索服务或以其实际消费方所需的最小 API 扩展现有服务族;第一阶段不预先承诺 provider 注册协议。它将依赖 `ctx.sessions` 和可选的 `ctx.sessionPersistence`,拥有一个独立的派生 SQLite 数据库,并复用规范的 `foldSurface()` 分类。
在精确读取包exact-read package旁新增 `@deepseek-ai/dsh-session-query-sqlite`。该包将暴露一个搜索服务或以其实际消费方所需的最小 API 扩展服务族;第一阶段不预先承诺 provider 注册协议。它将依赖 `ctx.sessions` 和可选的 `ctx.sessionPersistence`,拥有一个独立的派生 SQLite 数据库,并复用规范的 `foldSurface()` 分类。
实现拥有一个串行化的协调/数据库事务状态机。一次事务观察权威的持久化元数据和活跃快照,提取语义文档,更新派生表,推进相关的游标 generation并执行或启用应的查询。没有第二个服务维护并行的指纹、脏标记、活跃 ID 集合或失效 generation。
实现拥有一个串行化的协调/数据库事务状态机。一次事务观察权威的持久化元数据和活跃快照,提取语义文档,更新派生表,推进相关的游标 generation并执行或启用应的查询。没有第二个服务维护并行的指纹、脏标记、活跃 ID 集合或失效 generation。
持久化文档在重启后保留。活跃覆盖层是连接局部的,同一会话遮蔽持久化行,在活跃所有者或数据库关闭时消失。派生数据库与规范持久化分离,因此索引重置、损坏、分词器变更和 schema 变动不会危及持久的对话日志。
持久化文档在重启后存活。活跃覆盖层是连接本地的,同一会话持久化行进行遮蔽,在活跃所有者或数据库关闭时消失。派生数据库与规范持久化分离,确保索引重置、损坏、分词器变更和 schema 变动不会危及持久的对话日志。
## 随实现确定的搜索语义
实现必须从可执行的用例出发定义跨会话和会话内两种搜索范围。每个可搜索事件是一个文档包含会话元数据、事件元数据、surface 分类、归一化语义文本和有界的纯文本摘要片段。会话级结果按其最强匹配事件分组;数值化的后端分数保持私有。
实现必须从可执行的用例出发定义跨会话和会话内两种搜索范围。每个可搜索事件是一个文档包含会话元数据、事件元数据、surface 分类、归一化语义文本和有界的纯文本摘要片段。会话级结果按其最强匹配事件分组;数值化的后端分数保持私有。
过滤器在排序之前编译为参数化 SQL。查询语法为数据处理。排序包含稳定的平局字段。不透明游标绑定到归一化的请求形状和最小相关 generation不相关的会话变更不应使会话内游标失效。取消操作必须停止调用方等待并在运行时允许的范围内中断 SQLite 工作。
过滤器在排序之前编译为参数化 SQL。查询语法被视为数据。排序包含稳定的平局字段。不透明游标绑定到归一化的请求形状和最小相关 generation不相关的会话变更不应使会话内游标失效。取消操作必须停止调用方等待并在运行时允许的范围内中断 SQLite 工作。
分词器选择仍是一个实现实验。FTS5 trigram 支持子串召回,但会拒绝短于三字符的有用词项并增大索引体积;提案在将其入契约之前,必须对默认 Unicode 分词器基准测试。
分词器选择仍是实现层面的实验。FTS5 trigram 支持子串召回,但会拒绝短于三字符的有用词项并增大索引体积;提案在将其入契约之前,必须对该权衡与默认 Unicode 分词器进行基准测试。
## 提取与协调
该包首先为消息、推理(reasoning、工具调用/结果、被拦截的提示词、上下文、steering中途引导、待办事项和错误/状态详情提供第一方语义提取。结构性事件和流式分片不贡献文档。未知的声明合并事件/内容类型保持不可搜索,除非有真实的扩展消费方证明需要公开的提取器注册表。
该包首先为以下内容提供第一方语义提取:消息、reasoning、工具调用/结果、被阻止的提示词、上下文、steering中途引导、待办事项和错误/状态详情。结构性事件和流式分片不贡献文档。未知的声明合并事件/内容类型保持不可搜索,除非有真实的扩展消费方证明需要公开的提取器注册表。
协调可以使用稳定指纹来避免重写未变更的持久化会话,但指纹的计算和存储由数据库包拥有。当源观察或提取失败时它绝不能报告某行为最新。provider-schema 不匹配只重置派生数据库;普通的源变更使用事务性 upsert/delete。已挂载但不可读的持久化使受影响的搜索失败但不影响规范写入或已知的活跃精确读取。
协调可以使用稳定指纹来避免重写未变更的持久化会话,但数据库包拥有指纹的计算和存储。当源观察或提取失败时它绝不能报告某行为最新。provider-schema 不匹配只重置派生数据库;普通的源变更使用事务性 upsert/delete。已挂载但不可读的持久化使受影响的搜索失败,但不影响规范写入或已知的活跃精确读取。
## 曾考虑的替代方案
- **规范持久化数据库中添加 FTS 表**:否决可重建的索引不应与权威日志共享 schema/重置/故障边界。
- **在第一阶段重新引入 provider 协调**:否决只有一个计划中的实现,没有证据表明存在稳定的多 provider seam。
- **立即持久化活跃覆盖层**:否决活跃事件在现有检查点提交之前不是规范的。
- **返回 BM25 分数**:否决。提供方特有的数值尺度在语料变化时不稳定。
- **将 FTS 表添加到规范持久化数据库中**:否决,因为可重建的索引不应与权威日志共享 schema/重置/故障边界。
- **重新引入第一阶段的 provider 协调**:否决,因为只有一个计划中的实现,没有证据表明存在稳定的多 provider seam。
- **立即持久化活跃覆盖层**:否决,因为活跃事件在现有检查点提交之前不是规范的。
- **返回 BM25 分数**:否决,因为 provider 特定的数值尺度在语料变化时不稳定。
## 验收标准
- 重启测试覆盖未变更、新增、变更和删除的持久化会话,且不重建整个索引。
- 重新打开时保留持久化行并移除活跃行;活跃行先遮蔽、后显露其持久化基
- 重启测试覆盖未变更、新增、变更和删除的持久化会话,且不重建整个索引。
- 重新打开时保留持久化行并移除活跃行;活跃行先遮蔽、后显露其持久化基
- 测试覆盖两种搜索范围、元数据过滤、surface 默认值、摘要片段、转义、确定性平局、分页、范围内的陈旧游标、取消、动态持久化挂载/卸载,以及事务失败后的恢复。
- schema 不匹配只重置派生数据库。
- 一个 key 的端到端测试将真实的持久化后端与真实的 SQLite 搜索包组合使用。
- 在移 `implemented/` 之前,本 RFC 须修订为实际实现的分词器和公开 API。
- 一个 keyless 的端到端测试将真实的持久化后端与真实的 SQLite 搜索包组合使用。
- 在移 `implemented/` 之前,本 RFC 须修订为实际实现的分词器和公开 API。
## 风险
单一所有者比提供方无关的 seam 更简单,但初期可复用性较低。这是有意为之:第二个真实后端揭示应当抽取什么。SQLite 运行时差异可能影响 FTS 排序和摘要片段,因此测试只能固定契约控制的排序和呈现。独立数据库增加了配置和生命周期工作,但保全了规范存储的安全边界。
单一所有者比提供方无关的 seam 更简单,但初期可复用性较低。这是有意为之:第二个真实后端可以揭示应当抽取什么。SQLite 运行时差异可能影响 FTS 排序和摘要片段,因此测试只能固定契约控制的排序和呈现。独立数据库增加了配置和生命周期工作,但保全了规范存储的安全边界。

View File

@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-07-13-stream-workflow-progress-through-tool-calls.md: 525f2793052a80d82de29d2d370cfd747d002af6
2026-07-13-stream-workflow-progress-through-tool-calls.zh.md: b5cc86df3ca42e513bda7e56b485a8287f275613
2026-07-13-stream-workflow-progress-through-tool-calls.zh.md: 8dcb4aea2de50cdd278c702c9f6c85ab66e6be34

View File

@@ -6,38 +6,38 @@ Status: proposed
## 问题
工作流引擎有意为 run、phase、narration 和子 agent 进度发出成对平衡`workflow/*` 观察事件,但目前没有生产消费方呈现它们。因此编辑器在最终结果到来之前只显示一张 pending 状态的工作流工具卡片,尽管引擎已经报告了当前活跃的 phase、脚本日志内容以及哪些子 agent 已启动或已完成。[dynamic-workflows 决策](../../implemented/feature/2026-07-05-dynamic-workflows.md)明确将 ACP 进度 UI 保留给这事件流。
工作流引擎有意为 run、phase、narration 和子 agent(智能体)进度发出成对的 `workflow/*` observation 事件,但目前没有生产消费方呈现这些事件。因此编辑器在最终结果返回之前只显示一张 pending 状态的工作流工具卡片,尽管引擎已经报告了当前活跃的 phase、脚本日志内容以及哪些子 agent 已启动或已结束。[dynamic-workflows 决策](../../implemented/feature/2026-07-05-dynamic-workflows.md)明确将 ACPAgent Client Protocol进度 UI 保留给这事件流。
如果让 `dsh-acp` 直接监听工作流事件,会反转能力边界:通用的 UI 桥接层将依赖一个可选的工作流包package并对一个工具名做特殊处理。工具流水线已经拥有实时更新所需的路由信息agent 和 call id但只暴露了纯粹的 pending/final 展示器,因此长时间运行的工具没有提供方无关的方式在二者之间报告瞬态 UI 状态。
如果让 `dsh-acp` 直接监听工作流事件,会反转能力边界:通用的 UI 桥接层将依赖一个可选的工作流包package并对一个工具名做特殊处理。工具流水线已经拥有实时更新所需的路由信息agent 和 call id但只暴露了纯粹的 pending/final 展示器,因此长时间运行的工具没有提供方无关的方式在二者之间报告瞬态 UI 状态。
## 提案
`dsh-tools` 添加一条实时进度通道。注册表有的 `ToolExecution` 新增 `reportProgress(view): boolean`,其中 `view` 是一个独立的、提供方无关的通用进度快照,包含可选的替换标题和面向 UI 的内容块。进度不能改调用的 args 派生卡片标签、kind、原始输入、locations、terminal intent 或 diff intent它只更新初选定的展示形式中的实时标题/内容。执行活跃期间,该方法校验并快照 view然后发一个受限的、agent 作用域的 `tools/progress` 观察事件,携带权威的执行标识与快照。一旦 final-result 处理开始,方法返回 `false` 且不再发,确保迟到的异步报告者无法覆盖终态卡片。观察者异常被记录不会导致工具失败。
`dsh-tools` 添加一条实时进度通道。注册表有的 `ToolExecution` 新增 `reportProgress(view): boolean`,其中 `view` 是一个独立的、提供方无关的通用进度快照,包含可选的替换标题和面向 UI 的内容块。进度不能改调用的 args 派生卡片标签、kind、原始输入、locations、terminal intent 或 diff intent它只更新在最初选定的展示方式内的实时标题/内容。执行处于活跃状态时,该方法校验并快照 view然后发一个受限的、agent 作用域的 `tools/progress` observation,携带权威的执行标识与快照。一旦 final-result 处理开始,方法返回 `false` 且不再发,因此迟到的异步报告者无法覆盖终态卡片。观察者异常被记录日志,不会导致工具失败。
`dsh-acp` 以通用方式消费 `tools/progress`。它通过有的 agent-to-session 映射解析执行所属的 agent并为同一 call id 发出一条 in-progress 的 `tool_call_update`。由于报告仅在工具执行流水线内可用,持久化的 `tool/call` 及其 ACP `tool_call` 始终先于第一条 update`tools/result` 之前关闭报告者确保没有进度更新出现在 completed/failed 卡片之后。进度是实时 UI 状态而非模型输入或持久历史:会话回放继续从 `tool/call``tool/result` 重建 pending 与 final 卡片,无需重放瞬态更新。
`dsh-acp` 以通用方式消费 `tools/progress`。它通过有的 agent-to-session 映射解析执行所属的 agent并为同一 call id 发出 in-progress 的 `tool_call_update`。由于报告仅在工具执行流水线内可用,持久化的 `tool/call` 及其 ACP `tool_call` 始终先于第一条 update`tools/result` 之前关闭报告者确保进度更新不会出现在 completed/failed 卡片之后。进度是实时 UI 状态而非模型输入或持久历史:会话回放继续从 `tool/call``tool/result` 重建 pending 与 final 卡片,无需重放瞬态更新。
`dsh-tool-workflow` 成为第一个生产者。每次工具执行在调用 `ctx.workflows.start()` 之前安装一个紧凑的事件捕获器,因为合法的引擎可能在 `start()` 内部同步发出进度。在调用返回之前,捕获器将观察到的事件按 `WorkflowRunInfo.id` 归约为候选状态;随后选取返回的 `WorkflowRun.id`丢弃其他候选报告累积的快照,并将后续匹配事件直接路由。如果 `start()` 抛出异常,捕获器被 dispose其候选被丢弃。这在不向 `WorkflowStartRequest` 添加观察者关联、也不要求进度等到 `start()` 返回的前提下,保持了引擎的可替换性。
`dsh-tool-workflow` 成为第一个生产者。每次工具执行在调用 `ctx.workflows.start()` 之前安装一个紧凑的事件捕获器,因为合法的引擎可能在 `start()` 内部同步发出进度。在调用返回之前,捕获器将观察到的事件按 `WorkflowRunInfo.id` 归约为候选状态;随后选取返回的 `WorkflowRun.id`丢弃其他候选报告累积的快照,并将后续匹配事件直接路由。如果 `start()` 抛出异常,捕获器被 dispose(资源释放),其候选状态被丢弃。这在不向 `WorkflowStartRequest` 添加观察者关联、也不要求进度等到 `start()` 返回的前提下,保持了引擎的可替换性。
归约器消费有的 start、phase、log、agent-start、agent-end 和 end 事件,报告一个替换快照,包含当前 phase、最新日志行、活跃子 agent 标签以及 completed/failed/cancelled 计数。它不累积 narration transcript;已完成的子 agent 离开活跃集合、转为计数。`workflow/end`、工具结算或插件 dispose 移除归约器条目和事件捕获器。六种工作流事件、它们的元数据、成对的子 agent 生命周期、run handle、取消通道和观察者隔离保持不变第三方观察者可继续直接消费它们
归约器消费有的 start、phase、log、agent-start、agent-end 和 end 事件,报告一个替换快照,包含当前 phase、最新日志行、活跃子 agent 标签以及 completed/failed/cancelled 计数。它不累积 narration transcript(文本记录);已结束的子 agent 离开活跃集合,变为计数`workflow/end`、工具结算或插件 dispose 移除归约器条目和事件捕获器。六种工作流事件及其元数据、成对的子 agent 生命周期、run handle、取消通道和观察者隔离保持不变第三方观察者可继续直接消费这些事件
更新工具执行/展示文档、生成的事件与 API 目录、工作流包文档以及工作流数据结构目录。ACP 集成覆盖率必须使用脚本化的模型边界真实的工作流工具和 worker seam 进行测试;主 ACP 快照套件新增一个 workflow-progress 场景,因为此变更改变了面向编辑器的 transcript。
更新工具执行/展示文档、生成的事件与 API 目录、工作流包文档以及工作流数据结构目录。ACP 集成覆盖率必须使用脚本化的模型边界测试真实的工作流工具和 worker seam主 ACP 快照套件新增一个 workflow-progress 场景,因为改变了面向编辑器的 transcript。
## 曾考虑的替代方案
**删除工作流观察面。** 在 [collapse-workflow 简化提案](../../rejected/simplification/2026-07-12-collapse-workflow-to-foreground-core.md)中被否决:这些事件及其平衡的生命周期是有意设计的,缺失的部分是消费方。
**删除工作流 observation 表面。** 在 [collapse-workflow 简化提案](../../rejected/simplification/2026-07-12-collapse-workflow-to-foreground-core.md)中被否决:这些事件及其成对生命周期是有意设计的,缺少的是消费方。
**让 ACP 直接了解工作流。** 这可以将 `WorkflowRunInfo` 映射到会话和卡片,但会使通用桥接层依赖一个可选能力,并绕过「工具拥有展示意图」的规则。工具进度通道为所有长时间运行的工具解决了同的路由问题。
**让 ACP 直接了解工作流。** 这可以将 `WorkflowRunInfo` 映射到会话和卡片,但会使通用桥接层依赖一个可选能力,并绕过「工具拥有展示意图」的规则。工具进度通道为每个长时间运行的工具解决了同的路由问题。
**将每进度更新持久化为会话事件。** 这会使实时 narration 可回放,但会用一种权威持久结果已由 tool call/result 对表达的状态永久膨胀日志。如果可恢复的工作流进度成为产品需求,需要一个工作流日志化设计,而非伪装成持久事实的 UI 快照。
**将每进度更新持久化为会话事件。** 这会使实时 narration 可回放,但会用一种状态永久膨胀日志,而该状态的权威持久结果已经是工具调用/结果对。如果可恢复的工作流进度成为产品需求,需要一个工作流日志化设计,而非伪装成持久事实的 UI 快照。
## 验收标准
- `ToolExecution.reportProgress()` 由注册表有、agent 作用域、快照化、观察者隔离,且在终态处理开始后返回 `false` 而不发。
- ACP 将进度路由到正确实时会话中的正确调用;不同会话中的并发工作流不能串扰,且 `tool_call_update` 不会出现在其 `tool_call` 之前或终态更新之后。
- 工作流进度显示当前 phase、最新日志行、活跃子 agent 和结果计数,同时保所有`workflow/*` 事件和 run 语义;一个在 `start()` 内部同步发出 start、phase、log、child 和 end 事件的 seam 测试引擎不会丢失任何归约器状态。
- `ToolExecution.reportProgress()` 由注册表有、agent 作用域、快照化、观察者隔离,且在终态处理开始后返回 `false` 而不发。
- ACP 将进度路由到正确实时会话中的正确调用;不同会话中的并发工作流不能串扰,且 `tool_call_update` 不会出现在其 `tool_call` 之前或终态更新之后。
- 工作流进度显示当前 phase、最新日志行、活跃子 agent 和结果计数,同时保所有`workflow/*` 事件和 run 语义;一个在 `start()` 内部同步发出 start、phase、log、child 和 end 事件的 seam 测试引擎不会丢失任何归约器状态。
- 取消、worker 死亡、工具失败、会话关闭和插件 dispose 释放归约器状态;回放仅发出持久的 pending/final 卡片对。
- 单元测试、工作流集成测试、ACP 集成测试、快照、类型检查、覆盖率、doc-sync、module-graph、构建和 hygiene 门禁全部通过。
## 风险
此变更向工具 seam 添加了一个公开的实时进度方法和事件,因此实现方必须精确维护 active/terminal 边界并在观察者看到快照之前将其分离。pre-start 捕获器可能短暂观察到无关的工作流 run因此它仅按 run id 持有紧凑的候选状态,并在 `start()` 返回后立即丢弃所有不匹配的候选。一个工作流可能发出大量进度变更;有界归约器避免了 transcript 增长,但在关联后仍会为每个有意义的事件发送一 UI 更新。如果实测客户端需要合并更新,必须通过带默认值的、经过校验的桥接配置实现,而非硬编码的节流。瞬态进度在回放时有意消失,因此最终工具结果仍是唯一持久的工作流卡片内容。
本提案向工具 seam 添加了一个公开的实时进度方法和事件,因此实现方必须精确维护 active/terminal 边界并在观察者看到快照之前将其分离。pre-start 捕获器可能短暂观察到无关的工作流 run因此它仅按 run id 持有紧凑的候选状态,并在 `start()` 返回后立即丢弃所有不匹配的候选。一个工作流可能发出大量进度变更;有界归约器避免了 transcript 增长,但在关联完成后仍会为每个有意义的事件发送一 UI 更新。如果经测量的客户端需要合并更新,必须是一个带默认值的、经过校验的桥接配置,而非硬编码的节流。瞬态进度在回放时有意消失,因此最终工具结果仍是唯一持久的工作流卡片内容。

View File

@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-06-11-api-extractor-reports.md: 0f3f736ba662fd6366eb8d7f26887fb319b2b563
2026-06-11-api-extractor-reports.zh.md: 3c472d64bc96c7fffa784c091f8a7a70bb0beb56
2026-06-11-api-extractor-reports.zh.md: cf0eb3f9edbcfb2ae862f075af0628e716693a86

View File

@@ -4,29 +4,29 @@
Status: proposed
> 从最初的「Doc-sync 与 API 报告」RFC2026-06-11中拆出。第 12 部分(文档块类型检查、事件分类体系校验)已交付——见 [doc-sync 强制](../../implemented/process/2026-06-11-doc-sync-enforcement.md)。本文是被推迟的第 3 部分,作为独立提案保留。
> 从最初的「Doc-sync 与 API 报告」RFC2026-06-11中拆出。第 12 部分(文档块类型检查、事件分类体系校验)已交付见 [doc-sync 强制](../../implemented/process/2026-06-11-doc-sync-enforcement.md)。本文是被推迟的第 3 部分,作为独立提案保留。
## 问题
公开 API 的变更是不可见的:没有任何机制让「这个 commit 改变了公开接口」为一个显式、可评审的事实。评审者阅读 diff 时可能遗漏个导出类型新增了字段方法签名发生了变化。
公开 API 的变更是不可见的:没有任何机制将「此次提交改变了公开接口」为一个显式、可评审的事实。评审者阅读 diff 时可能遗漏个导出类型新增了字段,或某个方法签名发生了变化。
## 提案
使用 api-extractor`tsc --emitDeclarationOnly` 加一份归一化的公开接口导出为每个包package生成一份签入仓库的 `etc/<pkg>.api.md`如果重新生成结果与签入版本不同CI 失败。这样每一次公开 API 变更都会成评审者(或评审 agent必须看到的一行 diff。
使用 api-extractor`tsc --emitDeclarationOnly` 加一份规范化的公开接口导出为每个包package生成一份签入仓库的 `etc/<pkg>.api.md`CI 在重新生成结果与签入报告不一致时失败。这样每一次公开 API 变更都会成评审者(或评审 agent(智能体))必须看到的一行 diff。
## 曾考虑的替代方案
**`tsc --emitDeclarationOnly`一份归一化的公开接口导出**:如果 api-extractor 被证明过重,这是更轻量的机制;两者都满足提案所需的「签入仓库、可 diff」的报告形态。
**`tsc --emitDeclarationOnly`规范化的公开接口导出**:如果 api-extractor 过于笨重,这是更轻量的机制;两者都满足提案所需的「签入仓库、可 diff」的报告形态。
## 验收标准
- 每个包有一份签入仓库的 `etc/<pkg>.api.md`;重新生成结果与已提交报告不同时 CI 失败。
- 每个包有一份签入仓库的 `etc/<pkg>.api.md`CI 在重新生成结果与已提交报告不一致时失败。
- 公开 API 变更(新增导出、字段放宽、签名变化)在评审中以报告 diff 行的形式可见。
## 风险
该依赖重且难伺候——这正是它被推迟的原因——且报告格式会随编译器升级而变动,在各包尚未发布的阶段增加了一个收益甚微的维护面
该依赖重且难以调教(这正是它被推迟的原因且报告格式会随编译器升级而变动,增加一个维护面;在各包尚未发布的阶段,收益有限
## 推迟原因
在 doc-sync 落地时被推迟:对于评审者已经能看到源码 diff 的内部 monorepo 而言价值有限,且依赖重、难伺候。如果这些包将来对外发布,届时一份稳定、可 diff 的公开接口报告才值得其维护成本。
在 doc-sync 落地时被推迟:对于一个内部 monorepo评审者已经能看到源码 diff价值不高;且依赖重、难以调教。如果包将来对外发布,再重新评估——届时一份稳定、可 diff 的公开接口报告才值得其维护成本。

View File

@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-06-11-architectural-conformance.md: 40858d049af2df1928e27280238d0b198a5202f7
2026-06-11-architectural-conformance.zh.md: d61751210ef78b26e05a05efae4d5abccbfd2e5c
2026-06-11-architectural-conformance.zh.md: b68355dc1c04a4f807efdb95f813159cb7f9f178

View File

@@ -6,31 +6,31 @@ Status: proposed
## 问题
两项架构保证目前仅存在于行文中1任何包不得依赖具体的 loop 包([微内核承诺](../../implemented/architecture/2026-06-11-microkernel-event-taxonomy.md)2每个 LlmAdapter 都正确地遵循 chunk 协议。者都应当机械化([质量门禁原则](../../implemented/process/2026-06-11-quality-gates.md))。
目前有两项架构保证仅存在于行文中1没有任何东西依赖具体的 loop 包([微内核承诺](../../implemented/architecture/2026-06-11-microkernel-event-taxonomy.md)2每个 LlmAdapter 都正确地遵循 chunk 协议。者都应当机械化[质量门禁原则](../../implemented/process/2026-06-11-quality-gates.md))。
## 提案
**dependency-cruiser** 配合以下规则:
- `packages/*`agent-loop 自身的测试和 examples/ 外)禁止导入 `@deepseek-ai/dsh-agent-loop`
- `packages/*`agent-loop 自身的 tests 和 examples/ 外)禁止导入 `@deepseek-ai/dsh-agent-loop`
- 禁止跨包深层导入(`@deepseek-ai/dsh-*/src/...` 路径)——只允许使用公开入口点。
- packages/ 内禁止任何导入循环。
- packages/ 内禁止导入循环。
- `vendor/*` 禁止从 `packages/*` 导入。
- 分层dsh-llm 不导入其他 dsh 包dsh-session 导入 dsh-llm以此类推packages/README.md 中的依赖表,强制执行)。
- 分层dsh-llm 不导入其他 dsh 包dsh-session 导入 dsh-llm以此类推packages/README.md 中的依赖表,强制执行)。
**适配器一致性套件**位于 dsh-llm`@deepseek-ai/dsh-llm/conformance`):一个可复用的 vitest 套件,以适配器工厂为参数,断言 chunk 协议契约——每个 block index 单调递增、`block-end` 之后该 index 不再有 delta、恰好一个 `finish`、usage 至多出现一次、每个 `tool-call-delta` 携带 call id、abort 被及时响应。当前对 mock 运行DeepSeek V4 适配器从第一天起继承该套件。可选地提供一个 dev 模式的 `strictAdapter()` 包装层,在 debug flag 下于运行时强制执行相同约束(与 [dev 模式不变式](../../implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md)配对)。
**适配器一致性套件**位于 dsh-llm`@deepseek-ai/dsh-llm/conformance`):一个可复用的 vitest 套件,以适配器工厂为参数,断言 chunk 协议契约——每个 block index 单调递增、`block-end` 之后该 index 不再有 delta、恰好一个 `finish`、usage 至多出现一次、每个 `tool-call-delta` 携带 call id、abort 被及时响应。当前对 mock 运行DeepSeek V4 适配器从第一天起继承该套件。可选地提供一个 dev 模式的 `strictAdapter()` 包装层,在 debug flag 下于运行时强制执行相同规则(与 [dev 模式不变式](../../implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md) 配对)。
## 计划
先落地 dependency-cruiser 配置与 CI 步骤(约一小时工作量,永久保证);一致性套件随其首个消费方测试(针对 MockAdapter一起落地并作为 V4 适配器阶段的前置条件。
先落地 dependency-cruiser 配置与 CI 步骤(约一小时工作量,换来永久保证);一致性套件随其首个消费方测试(针对 MockAdapter一起落地并作为 V4 适配器阶段的前置条件。
## 验收标准
- dependency-cruiser 在 CI 中运行上述规则族;违规导入导致构建失败。
- 一致性套件对 mock 适配器和两个正式适配器运行通过;新适配器包通过调用该套件并传入自己的工厂即可继承测试。
- 一致性套件对 mock 适配器和两个正式适配器运行新适配器包通过调用该套件并传入自己的工厂即可继承测试。
## 风险
随着包的增加需要维护 dep-cruiser 规则——应保持规则基于模式(`dsh-*`)而非逐一枚举。
随着包的增加dep-cruiser 规则需要维护——规则基于模式(`dsh-*`)而非逐一枚举。
<!-- rfc-format: alternatives-not-recorded (pre-format RFC) -->

View File

@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-06-11-supply-chain-and-vendor-drift.md: 306e185e9175e3e7af24455cf95167f54b3d1c17
2026-06-11-supply-chain-and-vendor-drift.zh.md: 1aeb0a8eff3f335bc87c05742acc4502a19bf3c5
2026-06-11-supply-chain-and-vendor-drift.zh.md: a840e766c49182d7a9ca648acbbab1a2762688f5

View File

@@ -1,4 +1,4 @@
# RFC供应链检查与 vendor 漂移
# RFC供应链检查与 vendor 漂移验
[English](2026-06-11-supply-chain-and-vendor-drift.md) | 中文
@@ -6,30 +6,30 @@ Status: proposed
## 问题
vendor manifest[vendor 决策](../../implemented/process/2026-06-11-vendor-cordis-as-source.md))在提交时只做*正向*强制vendor 代码变更 ⇒ manifest 更新),但没有任何机制验 manifest 的*声明*:即 vendor/ 确实等于上游指定 SHA 的代码 + 日志中记录的修改。此外,少量真正的 npm 依赖也没有安全公告监控或更新节奏。
vendor manifest元数据清单)(见[引入 vendor 决策](../../implemented/process/2026-06-11-vendor-cordis-as-source.md))在提交时仅在**正向**强制执行vendor 变更 ⇒ manifest 更新),但没有任何机制验 manifest 的**声明**:即 vendor/ 确实等于上游指定 SHA 的内容加上所记录的修改。此外,少量真正的 npm 依赖也没有安全公告监控或更新节奏。
## 提案
1. **Vendor 漂移检查**(夜间 CI以 manifest 中的 SHA 浅克隆上游仓库,复制对应 package 源码,与 `vendor/*/src` 做 diff。除非 diff 与日志中的本地修改一致(每项修改保存为一个入库的 patch 文件,使日志条目成为可校验的产物而非纯文字),否则 job 失败。
2. **依赖安全公告**:对 lockfile 运行 osv-scanner`pnpm audit`),按计划调度 + 在涉及 lockfile 的 PR 上触发。
3. **许可证清单**:一个脚本断言每个 vendor 化的 package 都携带 LICENSE 文件,且 package.json 的 `license` 字段与 vendor/README.md 中的清单一致(我们混合了 vendor 的 MIT 与自有的 BSD-3作为 CI 步骤运行。
4. **Renovate**(或一个定时 agent 任务)以小 PR 提议 npm 依赖更新,这些 PR 走完整门禁套件vendor 化的 package 排除在外(它们的更新遵循 manifest 同步流程,理想情况下作为半自动化的 agent 工作流:拉取上游、重新应用 patch、运行门禁、打开 PR 并更新 manifest 表格)。
1. **Vendor 漂移检查**(夜间 CI以 manifest 中记录的 SHA 浅克隆上游仓库,复制对应 package 源码,与 `vendor/*/src` 做 diff。除非 diff 与已记录的本地修改一致(每项修改以签入的 patch 文件保存——日志条目从行文描述变为可验证的产物),否则任务失败。
2. **依赖安全公告**:对 lockfile 运行 osv-scanner`pnpm audit`),按计划定期执行,并在涉及 lockfile 变更的 PR 上触发。
3. **许可证清单**:一个脚本断言每个 vendor 都携带 LICENSE 文件,且 package.json 的 `license` 字段与 vendor/README.md 中的清单一致(我们混合了 vendor 的 MIT 与自有的 BSD-3——作为 CI 步骤运行。
4. **Renovate**(或定时 agent 任务)以小 PR 的形式提议 npm 依赖更新,这些 PR 走完整门禁套件vendor 包不在其列(它们的更新遵循 manifest 同步流程,理想情况下半自动化的 agent 工作流:拉取上游、重新应用 patch、运行门禁、以更新后的 manifest 表格开 PR)。
## 计划
3 最简单先做。1 需要 CI 能通过网络访问上游仓库(私有镜像,需要 token并将现有两项已记录的修改转为 patch 文件。2 和 4 属于配置工作。
3 最简单,先做。1 需要 CI 能通过网络访问上游仓库(私有仓库,需要 token并将现有两项已记录的修改转为 patch 文件。第 2 项和第 4 项是配置工作。
## 曾考虑的替代方案
- **用 `pnpm audit` 替 osv-scanner**:两者都满足安全公告扫描的需求;具体选择推迟到实现阶段决定。
- **用定时 agent 任务替 Renovate**:在「以小 PR 提议更新并走完整门禁」这件事上效果等价vendor 化的 package 无论哪种方案都排除在外(它们的更新遵循 manifest 同步流程)。
- **用 `pnpm audit` osv-scanner**:两者都满足安全公告扫描的需求;具体选择推迟到实现阶段决定。
- **用定时 agent 任务替 Renovate**:在提议小型更新 PR 并走完整门禁套件方面效果等价vendor 无论哪种方案都不在其列(它们的更新遵循 manifest 同步流程)。
## 验收标准
- 许可证清单脚本在 CI 中运行,缺少 LICENSE 或 `license` 字段与 `vendor/README.md` 清单矛盾时失败。
- 夜间漂移 job 从 manifest SHA 加入库 patch 文件重建 `vendor/`,出现任何无法解释的 diff 时失败。
- 安全公告扫描按计划对 lockfile 运行,并在涉及 lockfile 的 PR 上运行。
- 许可证清单脚本在 CI 中运行,缺少 LICENSE 或 `license` 字段与 `vendor/README.md` 中的清单矛盾时失败。
- 夜间漂移任务从 manifest SHA 加签入的 patch 文件重建 `vendor/`,出现任何无法解释的 diff 时失败。
- 安全公告扫描按计划定期运行,并在涉及 lockfile 变更的 PR 上运行。
## 风险
上游仓库是私有镜像CI 凭证与可用性是漂移检查的主要阻力。如果受阻,改为本地定时 agent 任务而非 CI 运行
上游仓库是私有镜像CI 凭证与可用性是漂移检查的主要阻力。如果受阻,改为本地定时 agent 任务而非 CI。

View File

@@ -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-discover-package-inventory.md: 22b3e9acbe4dad8ef829d0dd30415c516031d66b
2026-06-20-discover-package-inventory.zh.md: 8b62d42d2d514f60016690ba55d5ce77a50b3aff
2026-06-20-discover-package-inventory.zh.md: 4eeaed9ed6b390608095281775883f8e7a52e954

View File

@@ -1,36 +1,36 @@
# RFC通过发现机制获取包清单取代静态列表维护
[English](2026-06-20-discover-package-inventory.md) | 中文
# RFC通过发现机制获取包清单而非维护静态列表
Status: proposed
[English](2026-06-20-discover-package-inventory.md) | 中文
## 问题
package与门禁清单在 TypeScript project references、package 文档、CI 行文、Knip 覆盖项以及快照场景元数据中反复出现。其中大部分只是重述包布局、manifest 数据、聚合命令内容或 fixture测试前置数据文件。每新增一个包或场景都会产生本可避免的同步点。
package与门禁清单在 TypeScript project references、文档、CI 描述、Knip 覆盖项以及快照场景元数据中反复出现。大多数只是重述包布局、manifest 数据、聚合命令内容或 fixture测试前置数据文件。因此每新增一个包或场景都会产生本可避免的同步点。
[包层级结构](../../implemented/architecture/2026-06-20-package-hierarchy.md)已经手动消除了其中若干:`scripts/publint-all.ts` 现在从 `packages/<group>/<pkg>` 布局推导清单,两份 `tsconfig``paths` 映射也合并为一个 `@deepseek-ai/dsh-*` 通配符。剩下的是无法用 glob 消除的清单——主要是 `tsconfig.build.json` 的 project `references`TypeScript 要求它是一个显式数组(没有通配符形式)。
[包层级结构](../../implemented/architecture/2026-06-20-package-hierarchy.md)已经手动消除了其中若干:`scripts/publint-all.ts` 现在从 `packages/<group>/<pkg>` 布局推导列表,两份 `tsconfig``paths` 映射也合并为一个 `@deepseek-ai/dsh-*` 通配符。剩下的是无法用 glob 消除的清单主要是 `tsconfig.build.json` 的 project `references`——TypeScript 要求它是显式数组(没有通配符形式)。
静态列表编码策略时是合理的;当它们只是重复 `package.json`、workspace glob 或包层级结构中已有的 manifest 数据或布局事实时,就是无谓的摩擦。
静态列表编码的是策略时,它们是合理的;当它们只是重复 `package.json`、workspace glob 或包层级结构中已有的 manifest 数据或布局事实时,就是不必要的摩擦。
## 提案
让剩余的包/门禁清单可被发现。一个唯一的权威来源——`packages/<group>/<pkg>` 层级结构加上 package manifest——应当驱动 `tsconfig.build.json``references`、模块图以及任何全量包列表,并配合一个生成加校验步骤(沿用现有的 `gen-module-graph` / `gen-cordis-catalog` 模式:生成器写产物,`--check` 模式在 `hygiene`/`doc-sync` 中检测已提交副本是否陈旧)。模块图生成已经在读取 package manifest。`doc-sync` 应当成为定义并打印其子门禁的唯一命令,文档链接到该命令而非重述第二份清单
让剩余的包/门禁清单可被发现。一个唯一的权威来源——`packages/<group>/<pkg>` 层级结构加上 manifest(元数据清单)——应当驱动 `tsconfig.build.json``references`、模块图以及任何全量包列表,并配合一个生成加校验步骤(沿用现有的 `gen-module-graph` / `gen-cordis-catalog` 模式:生成器写产物,`hygiene`/doc-sync(文档同步门禁)中的 `--check` 模式在提交副本陈旧时报错)。模块图生成已经在读取 manifest。`doc-sync` 应当成为定义并打印其子门禁的唯一命令,文档链接到该命令而非重述第二份列表
层级结构不需要编码一个包的所有信息但应当编码宽泛的维护策略core/product 包、集成包、能力 seam 包与 support/test/example 包不应在脚本能区分它们之前先要求一份手工维护的例外清单
层级结构不需要编码关于包的所有事实但应当编码宽泛的维护策略core/product 包、集成包、能力 seam 包与 support/test/example 包不应在脚本能区分它们之前先要求一份手工维护的例外列表
有两项被编目的内容根本不需要生成器: e2e 入口 glob 折入 knip 的默认 stanza 即可直接删除包的重`childSessions`从每个场景的 fixture 目录发现,场景表只声明策略(`recorded``hasModelTurn``comparesLog`)。而即便这些策略字段,今天也在追踪可从 fixture 推导的事实(`comparesLog` ⟺ 已提交的日志在头行之后有内容`recorded``hasModelTurn` 且没有 `replay.override.json` 兄弟文件),因此每个新场景类都在不断添加 fixture 目录已经能回答的开关。
有两类编目项根本不需要生成器: e2e 入口 glob 折入 knip 的默认配置段即可直接删除包的重复声明`childSessions` 可从每个场景的 fixture 目录发现,使场景表只声明策略(`recorded``hasModelTurn``comparesLog`)。而即便这些策略字段,今天也在追踪可从 fixture 推导的事实(`comparesLog` ⟺ 已提交的日志在头行之后还有条目`recorded``hasModelTurn` 且没有 `replay.override.json` 兄弟文件),因此每个新场景类都在不断添加 fixture 目录本身已经能回答的开关。
## 验收标准
- `tsconfig.build.json` 的 project `references` 由层级结构生成(生成器输出它们;`--check` 门禁在提交副本陈旧时失败),而非手工维护。
- 新增一个包不需要为任何门禁编辑静态包列表。
- `tsconfig.build.json` 的 project `references` 由层级结构生成(生成器输出它们;`--check` 门禁在提交副本陈旧时报错),而非手工维护。
- 新增一个包时,不需要为任何门禁编辑静态包列表。
- 文档描述真源,而非重复生成的清单。
- CI 调用聚合命令,由这些命令自行管理其子门禁列表。
- `knip.json` 仅在编码真实信息(额外入口文件、被忽略的依赖)时才携带 per-package 覆盖项,绝不重述默认 stanza
- `knip.json` 仅在编码真实信息(额外入口文件、被忽略的依赖)时才携带逐包覆盖项,绝不重述默认配置段
- 快照场景只声明策略,不声明可从其 fixture 目录发现的事实。
## 风险
发现脚本可能变得过于精巧。实现应保持朴素:读取 manifest、按显式字段过滤、打印解析后的列表、出错时大声报错。收益在于消除手工清单漂移而非发明一套构建系统。
发现脚本可能变得过于精巧。实现应保持朴素:读取 manifest、按显式字段过滤、打印解析后的列表、出错时大声报错。收益在于消除手工清单漂移,而非发明一套构建系统。
<!-- rfc-format: alternatives-not-recorded (pre-format RFC) -->

View File

@@ -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-unify-agent-and-session-id.md: 3a6daa411673003eb1c3017e7a717ae4bf98b735
2026-06-20-unify-agent-and-session-id.zh.md: c931831c028e3147bfb84ed4fdeefe83037e92f4
2026-06-20-unify-agent-and-session-id.zh.md: 6b1b996879125c6ab85aed7ba419aff15d077b42

View File

@@ -6,37 +6,37 @@ Status: proposed
## 问题
agent 工厂为每个活跃的 agent/会话对维护两个 id`agentId``AgentRegistry` 的路由句柄)和 `sessionId`(事件溯源与持久化日志的身份标识)。`CreateAgentOptions` 接收两者;`ResumeAgentOptions` 接收 `agentId``resumeSessionId`;进程内 subagent 铸造两个独立的 UUID尽管血缘关系另行记录。
agent 工厂为每个活跃的 agent/session 对维护两个 id`agentId``AgentRegistry` 的路由句柄)和 `sessionId`(事件溯源与持久化日志的标识)。`CreateAgentOptions` 接收两者;`ResumeAgentOptions` 接收 `agentId``resumeSessionId`;进程内 subagent 各自铸造两个独立的 UUID尽管血缘关系另行记录。
ACPAgent Client Protocol已经对两个身份使用同一个值。二者在配置创建的 agent、恢复的会话和进程内子 agent 中才出现分歧,但没有任何生产路径会一个活跃 agent 重新关联到多个会话,或让一个会话经过多个 agent id。Stdio 保留 `labelBySession` 仅仅是为了从会话事件中恢复 agent 标签,而钩子同时暴露两个值让使用者自行对齐
ACPAgent Client Protocol已经对两个标识使用同一个值。它们在配置创建的 agent(智能体)、恢复的会话和进程内子 agent 中才出现分歧,但没有任何生产路径会一个活跃 agent 重新关联到多个会话,或让一个会话经过多个 agent id。Stdio 保留 `labelBySession` 仅仅是为了从会话事件中恢复 agent 标签,而钩子同时暴露两个值让使用者自行调和
[agent 作用域运行时](../../implemented/architecture/2026-07-12-agent-scope-runtime-design.md)没有与身份相关的留状态:创建和恢复使用同一个 `AgentCreationTransaction`,两个注册表条目使用相同的 final-entry 碰撞规则。分离的 id 并未复制活跃性、回滚或静默机制。统一后删除一个调用方提供的 id、每个进程内子 agent 的一个 UUID 以及剩余的翻译路径,而不改变事务生命周期;同时使活跃 agent 注册表强制执行后台任务所有权所使用的会话身份
[agent-scope 运行时](../../implemented/architecture/2026-07-12-agent-scope-runtime-design.md)没有与标识相关的留状态:创建和恢复使用同一个 `AgentCreationTransaction`,两个注册表条目使用相同的 final-entry 碰撞规则。分离的 id 并不会使活跃性、回滚或静默机制产生重复。统一后删除一个调用方提供的 id、每个进程内子 agent 的一个 UUID 以及剩余的转换路径,而不改变事务生命周期;同时使活跃 agent 注册表强制执行后台任务所有权所使用的会话标识
`Session` 另外同时暴露 `Session.id``Session.header.id`,尽管构造时要求二者一致。持久化边界必须校验这重复值,消费方必须在同一事实的两个归属位置之间做选择。
`Session` 另外同时暴露 `Session.id``Session.header.id`,尽管构造时要求二者必须一致。持久化边界必须校验这重复值,消费方必须在同一事实的两个归属位置之间做选择。
## 提案
对 agent 注册表条目和 `session.header.id` 使用同一个 id。`CreateAgentOptions` 为两个最终条目接一个身份标识;恢复操作以被恢复的 session id 注册 agentsubagent 创建铸造一个合并后的 id`Session` 只保留一个身份归属位置。保留当前的事务、final-entry 碰撞检查、exact-entry 摘除、回滚与静默机制;仅移除唯一职责是在两个 id 之间做翻译的 map 和字段。
对 agent 注册表条目和 `session.header.id` 使用同一个 id。`CreateAgentOptions` 为两个最终条目接一个标识;恢复操作以被恢复的 session id 注册 agentsubagent 创建铸造一个合并后的 id`Session` 只保留一个标识归属位置。保留当前的事务、final-entry 碰撞检查、exact-entry 摘除、回滚与静默机制;仅移除唯一职责是在两个 id 之间做转换的 map 和字段。
配置驱动的路径必须先确定其恢复还是创建的策略。前它使用一个稳定的 agent 标签加一个带 UUID 后缀的新 session id以避免在下次运行时与已有的持久化日志碰撞。统一后它必须明确选择恢复一个固定 id、铸造一个新的合并 id将该策略暴露出来;实现不得默默做出选择。
配置驱动的路径必须先确定其恢复还是创建的策略。前它使用一个稳定的 agent 标签加一个带 UUID 后缀的新 session id以避免在下次运行时与已有的持久化日志碰撞。统一后它必须明确选择:恢复一个固定 id、铸造一个新的合并 id还是将该策略暴露出来;实现不得默默做出选择。
`agent/created``agent/disposed` 不在本提案范围内。它们是发布生命周期事件而非身份别名;移除它们需要单独的生产方-消费方审计与决策。
`agent/created``agent/disposed` 不在本提案范围内。它们是发布生命周期事件而非标识别名;移除它们需要单独的生产方-消费方审计与决策。
## 曾考虑的替代方案
**保留分离的路由身份与日志身份** 一个稳定的配置 agent 标签配一个新的对话,是这种区分的真实用途。如果确实需要该显示或路由身份,则应否决本提案,转而显式强制 session id 唯一性,而不是将翻译隐藏在另一个 map 中。
**保留分离的路由标识与日志标识** 一个稳定的配置 agent 标签配一个新的对话,是这种区分的真实用途。如果确实需要该显示或路由标识,请否决本提案,转而显式强制 session id 唯一性,而不是把转换隐藏在另一个 map 中。
## 验收标准
- agent 创建/恢复与 subagent 创建只携带一个身份标识;`Session` 将其存储在一个位置。
- 创建事务保留 final-entry 碰撞、exact-entry 摘除、回滚与静默保证,且不依赖与身份相关的生命周期状态
- ACP、stdio、钩子、bash 所有权、持久化与血缘关系无需 agent/session id 翻译
- 配置驱动的恢复还是创建策略是显式的,并在持久化重启场景得到覆盖。
- agent 创建/恢复与 subagent 创建只携带一个标识;`Session` 将其存储在一个位置。
- 创建事务在不依赖标识相关生命周期状态的前提下,保留 final-entry 碰撞、exact-entry 摘除、回滚与静默保证。
- ACP、stdio、钩子、bash 所有权、持久化与血缘关系无需进行 agent/session id 转换
- 配置驱动的恢复还是创建策略是显式的,并在持久化重启场景得到覆盖。
- `agent/created``agent/disposed` 仅在单独的生产方-消费方审计之后才变更。
- 类型检查、覆盖率、快照、doc-sync、module-graph 校验、构建与 hygiene 全部通过。
## 风险
统一后将无法再拥有一个跨多个会话日志的稳定 actor 身份,包括未来可能的交接或 fork保留 actor 但更换会话)。重新引入该设计需要一个新的显式 actor 身份。统一还使一个持久化的、可能由客户端选的 session id 成为注册表句柄,并改变每个创建/恢复调用点 fixture测试前置数据
统一后将无法再拥有一个跨多个会话日志的稳定 actor 标识,包括未来可能出现的、在保留 actor 的同时切换会话的 handoff 或 fork 场景。重新引入该设计需要一个新的显式 actor 标识。统一还使一个持久化的、可能由客户端选的 session id 成为注册表句柄,并改变每个创建/恢复调用点 fixture测试前置数据
配置重启策略是阻塞性的设计决策:固定的合并 id 可能与已有日志碰撞,而每次运行生成新 id 则放弃了稳定的配置标签。如果确实需要独立的 actor 身份或稳定标签/新会话的配对,则应否决本提案,保留分离的 id 并加上显式的唯一性守卫。
配置重启策略是阻塞性的设计决策:固定的合并 id 可能与已有日志碰撞,而每次运行生成新 id 则放弃了稳定的配置标签。如果确实需要独立的 actor 标识或稳定标签/新会话的配对,否决本提案,保留分离的 id 并加上显式的唯一性守卫。

View File

@@ -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-dead-core-spine-surface.md: c46fe464e8627dcfc39a1d3fbb38a9cbd84269cf
2026-07-04-prune-dead-core-spine-surface.zh.md: 86830904ef0d1262d4cc132fb8d4b6a50e033e1d
2026-07-04-prune-dead-core-spine-surface.zh.md: 67e89a580b086a08aed702d9e0b87bdb6e32c944

View File

@@ -1,4 +1,4 @@
# RFC裁剪无用的公开接口与结果
# RFC裁剪无用的公开与结果接口
Status: proposed
@@ -6,57 +6,57 @@ Status: proposed
## 问题
若干包根导出、结果字段和便利方法没有生产消费方。它们之所以存活,要么是因为测试通过公开入口导入内部实现,要么是因为某个类型预了一个从未出现的调用者。每一项单独看都很小,但合在一起,它们扩大了 SDK 契约、生成的目录、文档和回归矩阵,却没有支撑任何已交付的路径。
若干包根导出、结果字段和便利方法没有生产消费方。它们之所以存活,要么是因为测试通过公开入口导入内部实现,要么是因为某个类型预了一个从未出现的调用者。每一项单独看都很小,但合在一起,它们扩大了 SDK 契约、生成的 catalog、文档和回归矩阵,却没有支撑任何已交付的路径。
生产语料库是 `packages/*/*/src`、示例源码/配置和运行时脚本。测试、package README 和 RFC 行文是发布的证据,但不是固定调用者。`cordis_inspect` 使 `packages/cordis/tool-cordis/src/api-catalog.ts` 对模型可见,`cordis_mount` 可以通过受保护的真实服务代理调用注入的服务,因此被编目的服务方法和返回形状是真正的动态产品。下表因此区分「没有固定的仓库调用者」与「不可达」:涉及编目词汇的行有意收缩模型编写的 mount 能发现和调用的内容,而包根实现辅助函数并不通过该服务门面可达。精确符号搜索得出以下清单:
生产语料库是 `packages/*/*/src`、示例源码/配置和运行时脚本。测试、包(package README 和 RFC 行文是发布的证据,但不是固定调用者。`cordis_inspect` 使 `packages/cordis/tool-cordis/src/api-catalog.ts` 对模型可见,`cordis_mount` 可以通过受保护的真实服务代理调用注入的服务,因此 catalog 中的服务方法和返回形状是真正的动态产品接口。下表因此区分「没有固定的仓库调用者」与「不可达」:涉及 catalog 词汇的行有意收缩模型编写的 mount 能发现和调用的内容,而包根实现辅助函数并不通过该服务门面可达。精确符号搜索得出以下清单:
| 接口 | 生产证据 | 简化方式 |
| 接口 | 生产证据 | 简化方式 |
| --- | --- | --- |
| `SurfaceManager.invalidate()` | 其单元测试调用seeding 在惰性创建的 manager 存在之前就已完成,且会话从不替换其日志引用。 | 删除该方法及其不可能触发的整体替换契约。 |
| `ToolExecutionResult.callId` | 每个钩子已经接收不可变的 `ToolExecution`;循环和 ACPAgent Client Protocol通过 call/session 事件关联。没有消费方读取这个重复的结果字段。 | 移除该字段、复制/不匹配守卫,以及证明该重复不不一致的测试。 |
| `ReactLoopAgent` 根导出 | 包外的名导入都是测试;生产代码面向 `Agent` 编程,通过 `ctx.agents` 创建/恢复。 | 返回/接口类型为 `Agent`,将具体循环类为包内部;保留有意为之的同步配置 `AgentLoop.create()` 路径。 |
| `workflow-workerthread` 的 protocol/runtime/session 再导出与具名 `WorkerWorkflowEngine` | 所有包名消费方使用默认引擎workflow RFC 已将 worker 协议格式定义为私有。 | 保留默认插件类/配置契约;移除重复的名类导出,将协议模块为源码私有。 |
| `code-runtime-worker` 的 protocol/bootstrap 再导出 | 包外的生产/e2e 消费方使用 `WorkerCodeRuntime` 和配置,而非 `BootstrapPort``PatchableStream` 或 worker 消息/启动类型。 | 保留运行时类/配置契约,将其协议格式/bootstrap 词汇为源码私有。 |
| ACP 的 translation/presenter 根导出 | `agentOptions``streamSessionEventUpdate``todosToPlan``ToolPresenter``nullToolPresenter``TerminalRendering` 有同文件或 ACP 测试消费方;唯一的包外生产消费方挂载的是插件命名空间。 | 保留 `name``inject``Config``AcpConfig``apply`;将 translation/presentation 辅助函数为源码私有,在包内测试。 |
| `providerWording` `completedTurnPrefix` 根导出 | 各有一个同包生产调用者; balanced-prefix 辅助函数有一个同包白盒测试。 | 为源码私有,通过 provider 行为测试。 |
| `depthOf``SubagentDepthError``SENSITIVE_ENV_PATTERN``waitForExit` `exitsWithin` 根导出 | 生产 subagent 后端消费的是进程内 runner 和子进程构造/释放辅助函数,而非这些强制/测试内部实现。 | 保留深度/环境/退出行为,但将辅助函数和 error/regex 为源码私有;通过 spawn 和释放来测试。 |
| `PersistenceCoordinator.inits`、后端 `inits` 访问器、`seedCoversPrefix` `assertSerializable` | 访问器为白盒测试而存在;`seedCoversPrefix` 没有包外生产导入者;`assertSerializable` 没有生产调用者,且与 coordinator append 边界的无损快照重复。 | 通过 `session/flush` 观察初始化,将 `seedCoversPrefix` 为源码私有,删除 `assertSerializable`。保留两个后端、`SessionHeader` 和 SQLite 的版本契约。 |
| `LlmError.status` 与 replay status | 适配器/replay 填充它,但生产分支基于稳定的 error code/message从不读取原始 status。 | 移除未读字段和 replay 管道,同时保留错误分类。 |
| `SurfaceManager.invalidate()` | 只有其单元测试调用seeding 在惰性创建的 manager 存在之前就已完成,且会话从不替换其日志引用。 | 删除及其不可能触发的整体替换契约。 |
| `ToolExecutionResult.callId` | 每个钩子已经接收不可变的 `ToolExecution`;循环和 ACPAgent Client Protocol通过 call/session 事件关联。没有消费方读取这个重复的结果字段。 | 移除该字段、复制/不匹配守卫,以及证明该重复不可能不一致的测试。 |
| `ReactLoopAgent` 根导出 | 包外的名导入都是测试;生产代码面向 `Agent` 编程,通过 `ctx.agents` 创建/恢复。 | 返回/接口类型为 `Agent`,将具体循环类为包内部;保留有意设计的同步、仅配置 `AgentLoop.create()` 路径。 |
| `workflow-workerthread` 的 protocol/runtime/session 再导出与命名的 `WorkerWorkflowEngine` | 每个包名消费方使用默认引擎workflow RFC 已将 worker 协议格式wire format定义为私有。 | 保留默认插件类/配置契约;移除重复的名类导出,将协议模块保持为源码私有。 |
| `code-runtime-worker` 的 protocol/bootstrap 再导出 | 包外的生产/e2e 消费方使用 `WorkerCodeRuntime` 和配置,而非 `BootstrapPort``PatchableStream` 或 worker 消息/启动类型。 | 保留运行时类/配置契约,将其协议格式/bootstrap 词汇为源码私有。 |
| ACP 的 translation/presenter 根导出 | `agentOptions``streamSessionEventUpdate``todosToPlan``ToolPresenter``nullToolPresenter``TerminalRendering` 有同文件或 ACP 测试消费方;唯一的包外生产消费方挂载的是插件命名空间。 | 保留 `name``inject``Config``AcpConfig``apply`;将 translation/presentation 辅助函数为源码私有,在包内测试。 |
| `providerWording` `completedTurnPrefix` 根导出 | 各有一个同包生产调用者;只有 balanced-prefix 辅助函数有一个同包白盒测试。 | 为源码私有,测试 provider 行为。 |
| `depthOf``SubagentDepthError``SENSITIVE_ENV_PATTERN``waitForExit` `exitsWithin` 根导出 | 生产 subagent 后端消费的是进程内 runner 和子进程构造/dispose资源释放辅助函数,而非这些强制/测试内部实现。 | 保留深度/环境/退出行为,但将辅助函数和 error/regex 为源码私有;通过 spawn 和 dispose 测试。 |
| `PersistenceCoordinator.inits`、后端 `inits` 访问器、`seedCoversPrefix` `assertSerializable` | 访问器为白盒测试而存在;`seedCoversPrefix` 没有包外生产导入者;`assertSerializable` 没有生产调用者,且与 coordinator append 边界的无损快照重复。 | 通过 `session/flush` 观察初始化,将 `seedCoversPrefix` 为源码私有,删除 `assertSerializable`。保留两个后端、`SessionHeader` 和 SQLite 的版本契约。 |
| `LlmError.status` 与 replay status | 适配器/replay 填充它,但生产分支基于稳定的 error code/message 判断,从不读取原始 status。 | 移除未读字段和 replay 管道,保留错误分类。 |
| `BlockAssembler.push()` 返回值 | 两个生产调用者都忽略返回的已完成块。 | 返回 `void`;保留有意公开的 `blocks()`/`message()` 契约。 |
| `compactRegion` 的独立 `session` 参数 | 固定调用者传入的对象与 `agent.session` 已经是同一个;模型可见的 mount API 也能调用该方法,但接受两个身份允许挂载的插件提供不一致的配对。 | 保留手动区域 seam同时有意将其收窄为以 `agent.session` 为唯一真源。 |
| `CompactionResult.startSeq``summarySeq``endSeq` `summary` | 生产消费方只读取 shadowed range/seq/token 统计;持久日志拥有摘要和事件标识。 | 移除四个结果回显,同时保留两个共享的 transcript文本记录渲染器。 |
| `BasicCompactService` 的 estimation/summarization 可见性 | 没有包外生产调用者调用这五个方法;已实现的 RFC `estimateContentTokens()``summarize()` 为子类钩子。 | 将这两个方法`protected`将三个仅用于编排的估算器为 private。 |
| `CodeLogEntry.source`/`level` `RunCodeMeta.dispatches` | 所有生产消费方将日志映射为文本;没有 presenter/模型路径读取其他字段或持久化的 dispatch 计数。 | 将 code-runtime 日志改为字符串(或纯文本条目),移除 result-meta dispatch 管道;保留用于生成确定性 dispatch id 的本地计数器。 |
| `ToolNotFoundError.toolName``SystemPrompt.config` `BashTask.command` | 每个存储的公开值都没有生产读取者。 | 移除未读字段,同时保留错误消息、已解析的配置行为和任务生命周期。 |
| 后端包根实现辅助函数 | 下方精确清单仅通过相对同包导入调用。生产命名空间导入挂载的是保留的插件契约,不读取这些属性;名根消费方是测试。 | 保留每个适配器/提供方/服务及其配置/错误契约;停止在包根导出所列辅助函数/常量。 |
| 消费方包根实现辅助函数 | 下方精确清单有同包生产调用者。生产命名空间导入挂载插件契约,不读取辅助属性;名根消费方是测试。 | 保留插件契约和稳定错误码;将测试移至包内模块或公开行为,停止在包根导出所列辅助函数。 |
| `compactRegion` 的独立 `session` 参数 | 固定调用者传入的对象与 `agent.session` 上已有的是同一个;模型可见的 mount API 也能调用该方法,但接受两个身份允许挂载的插件提供不一致的配对。 | 保留手动 region seam同时有意将其收窄为以 `agent.session` 为唯一真源。 |
| `CompactionResult.startSeq``summarySeq``endSeq` `summary` | 生产消费方只读取 shadowed range/seq/token 统计;持久日志拥有 summary 和事件标识。 | 移除四个结果回显,保留两个共享的 transcript文本记录渲染器。 |
| `BasicCompactService` 的 estimation/summarization 可见性 | 没有包外生产调用者调用这五个方法;已实现的 RFC `estimateContentTokens()``summarize()` 命名为子类钩子。 | 将这两个方法`protected`其余三个编排专用的估算器为 private。 |
| `CodeLogEntry.source`/`level` `RunCodeMeta.dispatches` | 每个生产消费方将日志映射为文本;没有 presenter/模型路径读取其他字段或持久化的 dispatch 计数。 | 将 code-runtime 日志改为字符串(或纯文本条目),移除 result-meta dispatch 管道;保留用于生成确定性 dispatch id 的本地计数器。 |
| `ToolNotFoundError.toolName``SystemPrompt.config` `BashTask.command` | 每个存储的公开值都没有生产读取者。 | 移除未读字段,保留错误消息、已解析的配置行为和任务生命周期。 |
| 后端包根实现辅助函数 | 下方精确清单仅通过相对路径的同包导入调用。生产命名空间导入挂载的是保留的插件契约,不读取这些属性;名根消费方是测试。 | 保留每个适配器/provider/服务及其配置/错误契约;停止在包根导出所列辅助函数/常量。 |
| 消费方包根实现辅助函数 | 下方精确清单有同包生产调用者。生产命名空间导入挂载的是插件契约,不读取辅助属性;名根消费方是测试。 | 保留插件契约和稳定错误码;将测试迁移到包内模块或公开行为,停止在包根导出所列辅助函数。 |
### 分组辅助导出清单
- `dsh-llm-deepseek``httpErrorCode``serializeMessages``serializeRequest``DONE``parseSse``mapFinishReason``mapUsage` `translate``dsh-llm-pi-ai``buildModel``mapStopReason``mapUsage``toPiContext` `toStreamChunks`
- `dsh-bash-local``DEFAULT_GRACE_MS``ENV_OVERRIDES``killGroup``OutputCollector` `runBash``dsh-bash-sandbox``shellQuote``classifyDenial` `classifyRunnerFailure``dsh-sandbox-local``bwrapProfileArgs``landlockProfileArgs` `seatbeltProfileArgs`。公开的可变测试注入字段及其类型不在本提案范围内。
- `dsh-fs-local``applyLiteralEdit``listDirectory``probe``readForEdit``readTextForDiff``readWholeText``resolveLocalTarget``restoreLineEndings``streamWholeText` `writeFileAtomic`
- `dsh-web-fetch-local``classifyContentType``decoderForCharset``isSameOrigin``parseCharset` `validateFetchUrl``dsh-web-search-exa``mapExaResponse` `mapExaResult``dsh-web-search-deepseek``citationSnippets` `mapAnthropicResponse``dsh-web-search-perplexity``mapPerplexityResponse` `mapPerplexityResult`
- `dsh-tool-fs``READ_LIMIT``STREAM_MIN_SIZE``READ_MAX_BYTES``READ_MAX_LINE_LENGTH``DIFF_CONTEXT``applyReadTool``parseReadArgs``applyWriteTool``formatWriteOutput``parseWriteArgs``applyEditTool``formatEditOutput``parseEditArgs``buildWindow``formatReadOutput``computeHunkDiffs` `diffsFromMeta`
- `dsh-tool-web``WEB_SEARCH_MAX_RESULTS``applyWebSearchTool``formatSearchOutput``parseSearchArgs``presentSearchCall``applyWebFetchTool``formatFetchOutput``parseFetchArgs``presentFetchCall``renderBody` `htmlToMarkdown``dsh-timeout-policy``toolTimeoutResult``dsh-compact-basic``resolveConfig``dsh-tool-bash``renderResult`
- `dsh-llm-deepseek``httpErrorCode``serializeMessages``serializeRequest``DONE``parseSse``mapFinishReason``mapUsage` `translate``dsh-llm-pi-ai``buildModel``mapStopReason``mapUsage``toPiContext` `toStreamChunks`
- `dsh-bash-local``DEFAULT_GRACE_MS``ENV_OVERRIDES``killGroup``OutputCollector` `runBash``dsh-bash-sandbox``shellQuote``classifyDenial` `classifyRunnerFailure``dsh-sandbox-local``bwrapProfileArgs``landlockProfileArgs` `seatbeltProfileArgs`。公开的可变测试注入字段及其类型不在本提案范围内。
- `dsh-fs-local``applyLiteralEdit``listDirectory``probe``readForEdit``readTextForDiff``readWholeText``resolveLocalTarget``restoreLineEndings``streamWholeText` `writeFileAtomic`
- `dsh-web-fetch-local``classifyContentType``decoderForCharset``isSameOrigin``parseCharset` `validateFetchUrl``dsh-web-search-exa``mapExaResponse` `mapExaResult``dsh-web-search-deepseek``citationSnippets` `mapAnthropicResponse``dsh-web-search-perplexity``mapPerplexityResponse` `mapPerplexityResult`
- `dsh-tool-fs``READ_LIMIT``STREAM_MIN_SIZE``READ_MAX_BYTES``READ_MAX_LINE_LENGTH``DIFF_CONTEXT``applyReadTool``parseReadArgs``applyWriteTool``formatWriteOutput``parseWriteArgs``applyEditTool``formatEditOutput``parseEditArgs``buildWindow``formatReadOutput``computeHunkDiffs` `diffsFromMeta`
- `dsh-tool-web``WEB_SEARCH_MAX_RESULTS``applyWebSearchTool``formatSearchOutput``parseSearchArgs``presentSearchCall``applyWebFetchTool``formatFetchOutput``parseFetchArgs``presentFetchCall``renderBody` `htmlToMarkdown``dsh-timeout-policy``toolTimeoutResult``dsh-compact-basic``resolveConfig``dsh-tool-bash``renderResult`
## 提案
以一次有界的、协调的公开接口清理,移除或降级上述每一行。更新 package README、JSDoc、生成的 API/事件目录、type-equiv 记录、必要的 exports map 以及测试,使测试通过所属的公开 seam 验证行为,而非保留仅为测试而存在的入口。不折叠任何能力 seam、LLM大语言模型适配器、持久化后端或生命周期静默契约。
以一次有界的、协调的公开接口清理,移除或降级上述每一行。同步更新包 README、JSDoc、生成的 API/事件 catalog、type-equiv 记录、必要的 exports map 以及测试,使测试通过所属的公开 seam 验证行为,而非保留仅为测试而存在的入口。不折叠任何能力 seam、LLM大语言模型适配器、持久化后端或生命周期静默契约。
## 曾考虑的替代方案
**保留测试便利函数和自包含结果字段为公开。** 公开辅助函数可以让白盒测试更方便,自包含的结果字段看起来更符合人体工学,未来的嵌入者可能需要具体循环类或枚举方法。这些好处是假设性的;今天它们让每处实现和文档都要解释没有已交付调用者能观察到的状态。真正的消费方可以引入它所需的最小契约,其所有权和失败语义已知
**保留测试便利函数和自包含结果字段为公开。** 公开辅助函数可以让白盒测试更方便,自包含的结果字段看起来更符合人体工学,未来的嵌入者可能需要具体循环类或枚举方法。这些好处是假设性的;当前它们让每处实现和文档都要解释没有已交付调用者能观察到的状态。真正的消费方可以引入它所需的最小契约,其所有权和失败语义明确
**模型编写的 mount 保留所有编目成员** 自引用工具集是一条真实的通用消费路径,而非生成文档的噪音。然而,它的价值来自准确、可组合的服务,而非无限期保留重复字段或不一致的参数对;上述每一项编目收缩都移除了在同一次执行、agent智能体或结果上其他位置已可获得的事实,并在同一变更中更新 API 参考。
**保留所有 catalog 成员以供模型编写的 mount 使用** 自引用工具集是一条真实的通用消费路径,而非生成文档的噪音。然而,它的价值来自准确、可组合的服务接口,而非无限期保留重复字段或不一致的参数对;上述每一项 catalog 收缩都移除了在同一 execution、agent 或 result 上其他位置已可获得的事实,并在同一变更中更新 API 参考。
## 验收标准
- 精确符号搜索显示被移除的接口面不出现在本 RFC 任何已实现 RFC 修正之外。
- 本 RFC 列出的每一项接口面均已按指定方式移除或降级;清单之外有意保留的扩展/测试契约不受影响
- 工具执行、压缩(compaction、两个 LLM 适配器、两个持久化后端、工作流隔离以及 agent 创建/恢复保持其已交付行为。
- 类型检查、覆盖率、快照、doc-sync(文档同步门禁)、module-graph 校验、构建和 hygiene 全部通过。
- 精确符号搜索显示在本 RFC 任何已实现 RFC 修正之外,没有被移除的接口
- 本 RFC 列出的每个接口均按指定方式缺失或降级;清单之外有意保留的扩展/测试契约不
- 工具执行、上下文压缩context compaction、两个 LLM 适配器、两个持久化后端、workflow 隔离以及 agent 创建/恢复保持其已交付行为。
- 类型检查、覆盖率、快照、doc-sync、module-graph 校验、构建和 hygiene 通过。
## 风险
大多数移除在编译时可见但运行时无影响。压缩参数清理有意禁止 session/context 不匹配,同时保留手动区域 seam。外部预发布嵌入者和现有模型编写的 mount 可能导入更少的辅助函数、传更少的参数或接收更窄的结果形状;这是有意的产品接口收缩,而非仅仅是生成目录的清理。仓库尚未发布,因此承载不受支持的接口才是更大的基础成本。
大多数移除在编译时可见但运行时无影响。上下文压缩参数清理有意禁止 session/context 不匹配,同时保留手动 region seam。外部预发布嵌入者和现有模型编写的 mount 可能导入更少的辅助函数、传更少的参数或接收更窄的结果形状;这是有意的产品接口收缩,而非仅仅是生成 catalog 的清理。仓库尚未发布,因此承载不受支持的接口才是更大的基础成本。

View File

@@ -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-simplify-session-log-representation.md: 52720231d6e4cbe0cbb412332cd016ba63f83569
2026-07-12-simplify-session-log-representation.zh.md: 468f9a565177089c8d49c06e8d490ab56980054a
2026-07-12-simplify-session-log-representation.zh.md: 1286a7d3c571fac66310a613c548920c3f25812d

View File

@@ -6,33 +6,33 @@ Status: proposed
## 问题
会话日志维护着两种表示,其机制开销超出了消费方的实际需求:伪链表 surface 自定义请求头增量编码
会话日志维护着两种表示,其机制复杂度超出了消费方的实际需求:一个伪链表 surface 自定义请求头增量。
`SurfaceManager` 将同一顺序存储在数组、seq 映射和可变的 `prev`/`next` 链接三处。生产代码从不读取 `prev`压缩compaction唯一一次读取 `next` 是取数组位置的后继。替换操作已经使用 `indexOf`,因此链接并未使其主要操作达到常数时间。一个 seq 数组加线性替换查找具有相同的渐近替换开销,且只有一种表示需要验。
`SurfaceManager` 用一个数组、一个 seq 映射和可变的 `prev`/`next` 链接存储相同的顺序。生产代码从不读取 `prev`压缩compaction唯一 `next` 读取是取数组位置的后继。替换操作已经使用 `indexOf`,因此链接并未其主要操作达到常数时间。一个 seq 数组加线性替换查找具有相同的渐近替换开销,且只有一种表示需要验
请求头子系统实现了自定义的 system/tool 增量编解码器传输决策层,尽管其契约声明增量只是编码优化而非可重建性要求。在每个 agent loop 实例边界保留 initial/resume 完整快照,然后在该实例的组装头发生变化时写入一条规范的完整 `request/header`,即可保留回放能力,同时删除 `SystemDelta``ToolsDelta`、往返 fallback 以及持久化的 `request/header-delta` 变体。编解码器专词汇随编解码器一起消失,并非因为其各分支本身无效。
请求头子系统实现了一套自定义的 system/tool 增量编解码器传输决策层,尽管其契约声明增量只是编码优化而非可重建性要求。在每个 agent loop(智能体循环)实例边界保留初始/恢复的完整快照,然后在该实例的组装头发生变化时写入一条规范的完整 `request/header`,即可保留回放能力,同时删除 `SystemDelta``ToolsDelta`、往返回退逻辑以及持久化的 `request/header-delta` 变体。编解码器专属的词汇随编解码器一起消失,并非因为其各分支本身无效。
本提案有意保留 append 与 replacement `sourceEventSeqs`、崩溃恢复溯源,以及所有 `SessionStartSource` 变体:已实施的 RFC 赋予这些字段审计/拦截角色,零当前读者不足以推翻这一点
本提案有意保留追加和替换`sourceEventSeqs`、崩溃恢复来源信息以及所有 `SessionStartSource` 变体:已实施的 RFC 赋予这些字段审计/拦截角色,零当前读者这一事实不足以推翻它们
## 提案
`SurfaceManager.nodes` 改为事件序列号的 `readonly number[]`,移除公开的 `SurfaceNode` 形状。保留内部的 replace-generation 信号;更新工具配对平衡压缩调用方,使其通过数组值/索引获取前驱、后继替换范围,移除节点链接 seq-to-node 映射。将锚点后的请求头增量替换为规范的完整变更头快照,移除增量编解码器/事件/测试;initial 与 resume 锚点即使折叠后的头未变也仍为完整快照。
`SurfaceManager.nodes` 改为事件序列号的 `readonly number[]`,移除公开的 `SurfaceNode` 形状。保留内部的替换代信号;更新 tool 配对平衡压缩调用方,使其通过数组值/索引获取前驱、后继替换范围,移除节点链接 seq-to-node 映射。规范的完整变更头快照替代锚点后的头增量,移除增量编解码器/事件/测试;初始和恢复锚点即使折叠后的头未变也仍为完整快照。
修订会话 surface 与可重建请求的 RFC 中描述已移除编码的部分。更新事件类型/不变式、请求日志/回放、持久化 fixture测试前置数据、生成的 catalog、包文档快照。将编解码器专`fallback` 原因替换为显式 `change` 原因(用于锚点后的完整快照),以区别于保留的 `initial` `resume` 锚点。
修订 session-surface 和 reconstructable-request RFC 中描述已移除编码的部分。更新事件类型/不变式、请求日志/回放、持久化 fixture测试前置数据、生成的 catalog、包文档快照。将编解码器专`fallback` 原因替换为锚点后完整快照的显式 `change` 原因,使其与保留的 `initial` `resume` 锚点区分开来
`SESSION_FORMAT_VERSION` 有意保持 `0`,因此包含 `request/header-delta` 的旧 v0 日志在增量折叠被删除后,若不做处理将通过版本检查并静默丢失头变更。seed/load 校验必须在格式边界处拒绝该遗留事件并快速失败;不添加兼容折叠或迁移。
`SESSION_FORMAT_VERSION` 有意保持 `0`,因此一份包含 `request/header-delta` 的旧 v0 日志在增量折叠被删除后,本会通过版本检查并静默丢失头变更。seed/load 校验必须在格式边界处拒绝该遗留事件并显式报错;不添加兼容折叠或迁移。
## 曾考虑的替代方案
**保留链表节点紧凑增量以备未来规模** 链接可能有助于未来的游标 API增量在大型工具 schema 仅有少量变化时能减小日志体积。但没有已发布的游标使用这些链接,而完整快照以磁盘空间换取显著更简单的正确性。如果头部体积确实成为问题,可以基于真实 trace 设计压缩方案或经过度量的规范增量方案。
**保留链表节点紧凑增量以备未来扩展** 链接可能有助于未来的游标 API增量在大型工具 schema 仅有少量变化时可以缩减日志。但没有已发布的游标使用这些链接,而完整快照以磁盘空间换取显著更简单的正确性。如果头部体积确实成为问题,可以基于真实 trace 设计压缩方案或经过度量的规范增量方案。
## 验收标准
- `SurfaceManager.nodes` 是一个有序 seq 数组,没有 `SurfaceNode`、链接字段或 seq-to-node 映射;增量追加处理内部 replace-generation 信号保留。
- `SurfaceManager.nodes` 是一个有序 seq 数组,没有 `SurfaceNode`、链接字段或 seq-to-node 映射;增量追加处理内部替换代信号保留。
- 回放完整变更头快照能重建出完全相同的请求;不再存在任何 header-delta 事件/类型/编解码器。
- 包含遗留 `request/header-delta` 的 v0 seed 或持久化日志在回放前被拒绝JSONL SQLite 加载路径均有覆盖。
- 新形状的 v0 JSONL/SQLite 回放、溯源、崩溃恢复、压缩、快照、不变式、类型检查、覆盖率、doc-sync、构建与 hygiene 全部通过。
- 包含遗留 `request/header-delta` 的 v0 seed 或持久化日志在回放前被拒绝JSONL SQLite 加载路径均有覆盖
- 新形状的 v0 JSONL/SQLite 回放、来源信息、崩溃恢复、压缩、快照、不变式、类型检查、覆盖率、doc-sync hygiene 全部通过。
## 风险
完整头会增加日志体积,线性替换查找在非常大的 surface 上可能更慢。替换操作目前已经是线性的,因为实现调用了 `indexOf`;只有真实 trace 表明更简单的数组成为瓶颈时才应添加基准测试。由于格式版本保持为 `0`,如果遗漏了对遗留事件的显式拒绝,后果将是静默数据损坏而非类型错误;因此快速失败的加载测试是本提案的组成部分,而非可选的清理工作。
完整头会增加日志体积,线性替换查找在非常大的 surface 上可能更慢。替换操作已经是线性的,因为实现调用了 `indexOf`;只有真实 trace 表明更简单的数组成为瓶颈时才应添加基准测试。由于格式版本保持为 `0`,如果遗漏了对遗留事件的显式拒绝,后果将是静默数据损坏而非类型错误;因此显式报错的加载测试是本提案的组成部分,而非可选的清理工作。

View File

@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-06-11-deterministic-and-stress-testing.md: e4ed7043d1880b55dd7d77b3e09a81dd58739a70
2026-06-11-deterministic-and-stress-testing.zh.md: d933c1dda329b95573b7a5a3fbe3a61ef94390cb
2026-06-11-deterministic-and-stress-testing.zh.md: 4e4ee9d28025a43349b702e399096535539a91d2

View File

@@ -6,28 +6,28 @@ Status: proposed
## 问题
若干 agent loop智能体循环测试通过 `setTimeout(30)` 睡眠来同步——这是一笔不稳定性债务,浪费 agent 重试周期,还可能掩盖序 bug。另一方面,我们的核心架构承诺(任何会话日志回放后都能得到完全相同的派生历史)目前只在两个测试中断言,但在*所有地方*断言的成本低。此外inbox 唤醒竞态只被手动验证过一次,没有任何东西持续地重新验证它
若干 agent loop智能体循环测试通过 `setTimeout(30)` 睡眠来同步——这是一笔不稳定性债务,浪费 agent 重试周期,还可能掩盖序 bug。另,我们的核心架构承诺(任何会话日志回放后都能得到相同的派生历史)目前只在两个测试中断言,但在**所有**测试中断言的成本低。此外inbox 唤醒竞态只被手动验证过一次,没有任何机制持续复验
## 提案
三项措施:
1. **测试中禁止挂钟睡眠。**`setTimeout(N)` 等待替换为事件驱动等待(有的 `waitForIdle` 模式,扩展为 `waitForStatus``waitForEvent(n)`),或在需要测试时间本身时使用 vitest fake timers。通过 lint 规则强制:禁止在 `packages/*/tests` 中使用 `setTimeout`,白名单辅助模块除外。
2. **通用回放 fixture测试前置数据** 一个共享测试辅助函数包装 agent loop harness使每个测试结束后agent 的会话日志被回放到一个全新的 Session 中,并自动断言 `deriveMessages()` 相等。这样该不变式在每次 CI 运行中会被检查数百次(覆盖套件产生的所有场景,而非仅两次。
3. **夜间竞态压力测试。** 一个 CI job 以 `vitest --repeat=200`(加 `--shuffle`)运行 agent-loop 和 inbox 套件,以暴露调度依赖的失败;发现的任何不稳定测试都为 bug 修复,绝不靠重试掩盖。
1. **测试中禁止挂钟睡眠。**`setTimeout(N)` 等待替换为事件驱动等待(有的 `waitForIdle` 模式,扩展为 `waitForStatus``waitForEvent(n)`),或在需要测试时间本身时使用 vitest fake timer。通过 lint 规则强制执行:禁止在 `packages/*/tests` 中使用 `setTimeout`,白名单辅助模块除外。
2. **通用回放 fixture测试前置数据** 一个共享测试辅助函数包装 agent loop harness使每个测试结束后agent 的会话日志被回放到一个全新的 Session 中,并自动断言 `deriveMessages()` 相等。这样该不变式在每次 CI 运行中会被套件产生的所有场景检查数百次,而非仅两次。
3. **夜间竞态压力测试。** 一个 CI job 以 `vitest --repeat=200`(加 `--shuffle`)运行 agent-loop 和 inbox 套件,以暴露调度依赖的失败;发现的任何不稳定测试都为 bug 修复,绝不靠重试掩盖。
## 计划
措施 1 和 2 一起落地(它们改动相同的辅助模块);在套件消除所有睡眠后再添加夜间 job使重复运行足够快。
措施 1 和 2 一起落地(它们改动相同的辅助模块);在套件消除所有睡眠后再添加夜间 job以确保重复运行速度快。
## 验收标准
- `packages/*/tests` 中不再有 `setTimeout`(白名单辅助模块除外),由 lint 规则强制。
- 共享 harness 每个测试的会话日志进行回放,将其注入全新的 `Session` 并自动断言 `deriveMessages()` 相等,覆盖整个套件。
- 夜间 job 以 `--repeat``--shuffle` 运行 agent-loop 和 inbox 套件;发现的不稳定测试作为 bug 分诊处理,绝不靠重试掩盖。
- `packages/*/tests` 中不再有 `setTimeout`(白名单辅助模块除外),由 lint 规则强制执行
- 共享 harness 每个测试的会话日志回放到全新的 `Session` 中,并自动断言 `deriveMessages()` 相等,覆盖整个套件。
- 夜间 job 以 `--repeat``--shuffle` 运行 agent-loop 和 inbox 套件;发现的不稳定测试作为 bug 分诊,绝不靠重试掩盖。
## 风险
Fake timers 与 agent loop 中的 Promise 调度存在微妙交互——优先使用事件驱动等待;仅在测试 timer 服务行为本身时才使用 fake timers
Fake timer 与 agent loop 中的 Promise 调度存在微妙交互——优先使用事件驱动等待;仅在测试 timer 服务行为本身时才使用 fake timer。
<!-- rfc-format: alternatives-not-recorded (pre-format RFC) -->

View File

@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-06-11-mutation-testing.md: 344263d1c91a5e6c83320f367bf76ed6f7ef5a49
2026-06-11-mutation-testing.zh.md: aa89b3335a143b6f34858bdc2f3344a6a7d758d9
2026-06-11-mutation-testing.zh.md: 28bb7253c12827dbcddd141481f26f60b3a72b7a

View File

@@ -1,4 +1,4 @@
# RFC变异测试作为覆盖率的制衡
# RFC变异测试作为覆盖率的制衡手段
[English](2026-06-11-mutation-testing.md) | 中文
@@ -6,31 +6,31 @@ Status: proposed
## 问题
逐文件 100% 覆盖率门禁([质量门禁决策](../../implemented/process/2026-06-11-quality-gates.md))证明的是每一行在测试中*执行*了,而非任何断言会在该行出错时有所察觉。在 agent 编写测试的场景下,覆盖率压力可能催生「执行但断言」的测试。变异测试衡量的正是覆盖率无法衡量的:测试套件是否能*杀死*被刻意注入的缺陷。
逐文件 100% 覆盖率门禁([质量门禁决策](../../implemented/process/2026-06-11-quality-gates.md))证明每一行代码在测试中都被*执行*了,但不能证明如果该行出错,任何断言会注意到。在 agent(智能体)编写测试的场景下,覆盖率压力可能产出「执行但断言」的测试。变异测试衡量的正是覆盖率无法衡量的:测试套件是否能*杀死*被刻意注入的缺陷。
## 提案
`packages/*/src` 上运行 Stryker`@stryker-mutator/vitest-runner`
- **PR 粒度的增量运行**(仅变更文件),作为 CI job调优后足够快,可以作为合并门禁。
- **每夜全量运行**,跟踪变异分数;先记录基线,再将阈值设为观测到的基线并只升不降(与覆盖率策略一致:阈值只收紧)。
- 存活的变异体是待办工作agent 选取一个存活体、编写杀死它的测试、循环往复——一个形态良好的自主循环。
- 等价变异体(可证明不改变行为的)加带理由的排除注解,与 `/* v8 ignore */` 策略对称
- **PR 范围的增量运行**(仅变更文件),作为一个 CI job调优后速度足以作为合并门禁。
- **每夜全量运行**,跟踪变异分数;先记录基线,再将阈值设为观测到的基线并只升不降(与覆盖率策略一致:阈值只收紧)。
- 存活的变异体是待办项agent 选取一个存活体、编写杀死它的测试、循环往复——一个形态良好的自主循环。
- 等价变异体(可证明不改变行为的)加注释排除并附理由,与 `/* v8 ignore */` 策略一致
## 计划
1. 添加 Stryker 配置范围限定在一个包llm最小、最具算法性测量运行时间。
2. 扩展到所有包;在配置中记录基线分数。
3. 接入每夜 job运行时间可接受后添加 PR 粒度的增量 job。
3. 接入每夜 job运行时间可接受后添加 PR 范围的增量 job。
## 验收标准
- Stryker 配置在 `packages/*/src` 上以 vitest runner 运行;每夜 job 记录变异分数,当分数低于记录的基线时,运行失败(阈值只升不降)
- PR 粒度的增量运行在运行时间可接受后作为合并门禁;或者明确保持仅每夜运行,并将该结论记录于此。
- 等价变异体带有附理由的排除注解,与 `/* v8 ignore */` 策略对称
- Stryker 配置在 `packages/*/src` 上以 vitest runner 运行;每夜 job 记录变异分数,当分数低于记录的基线时,通过只升不降的阈值使运行失败。
- PR 范围的增量运行在运行时间可接受后作为合并门禁;或者明确保持仅每夜运行,并将该结论记录于此。
- 等价变异体带有注释排除及理由,与 `/* v8 ignore */` 策略一致
## 风险
运行时间:变异测试开销大;逐文件 100% 覆盖率有所帮助(每个变异体至少会被执行到)。如果 PR 粒度的运行始终慢,则保持仅每夜运行,依赖分数只升不降的机制。
运行时间:变异测试开销大;逐文件 100% 覆盖率有所帮助(每个变异体至少会被执行到)。如果 PR 范围的运行始终慢,则保持仅每夜运行,依赖分数只升不降的机制。
<!-- rfc-format: alternatives-not-recorded (pre-format RFC) -->