docs(i18n): address RFC translation review

This commit is contained in:
Tianyi Cui
2026-07-22 22:29:39 +08:00
parent 0b1857904a
commit 1a39953f69
447 changed files with 1106 additions and 1106 deletions
@@ -2,5 +2,5 @@
# 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-11-content-block-vocabulary.md: 9414bda624fa6e5fc7e9b11b7a738d32b269af6b
2026-06-11-content-block-vocabulary.zh.md: 764791cbef65c2031b8337af4f4cb6835e9d312a
2026-06-11-content-block-vocabulary.md: 6a90813ec90c6522a88a09d3536ce0cef8250238
2026-06-11-content-block-vocabulary.zh.md: c9c65ae7e556cdf4eced0d45051862ff87fcd0fe
@@ -1,9 +1,9 @@
# RFC: Provider-neutral content-block vocabulary owned by dsh-llm
English | [中文](2026-06-11-content-block-vocabulary.zh.md)
Status: implemented
English | [中文](2026-06-11-content-block-vocabulary.zh.md)
## Problem
The harness needs one internal language for messages that the loop, session log, and all plugins speak.
@@ -1,4 +1,4 @@
# RFC由 dsh-llm 拥有的提供方无关内容块词汇
# RFC: 由 dsh-llm 拥有的提供方无关内容块词汇
Status: implemented
@@ -2,5 +2,5 @@
# 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-11-custom-schema-dsl.md: 4c4d572b15d5e474e00e99fc5e7dd89240251c63
2026-06-11-custom-schema-dsl.zh.md: eca0428d46ba3b3a4beac0cf9e8f02a9fa198388
2026-06-11-custom-schema-dsl.md: 34c018b779d45c5eadb7337cb060c43b6f8d09b1
2026-06-11-custom-schema-dsl.zh.md: 674a6b1a0b67617ffb4ab919899aa108c646a489
@@ -1,9 +1,9 @@
# RFC: Custom typed tool-schema DSL instead of schemastery
English | [中文](2026-06-11-custom-schema-dsl.zh.md)
Status: implemented
English | [中文](2026-06-11-custom-schema-dsl.zh.md)
## Problem
Tool parameters must reach the model as standard JSON Schema while giving tool authors typed `execute(args)` without casts. Schemastery already serves plugin config, but the tool-author API needs per-property `required: true` booleans rather than JSON Schema's separate `required` array.
@@ -1,4 +1,4 @@
# RFC使用自定义类型化 tool-schema DSL 替代 schemastery
# RFC: 使用自定义类型化 tool-schema DSL 替代 schemastery
Status: implemented
@@ -14,7 +14,7 @@ Status: implemented
## 曾考虑的替代方案
**Schemastery**(已作为 vendor 引入,用于插件 Config)经评估后被否决:它面向的是基于 StandardSchema 的校验/转换,而非 JSON Schema **生成**,因此会增加间接层却无法干净地产出协议格式(wire format)。
**Schemastery**(已作为 vendor 引入,用于插件 Config)经评估后被否决:它面向的是基于 StandardSchema 的校验/转换,而非 JSON Schema *生成*,因此会增加间接层却无法干净地产出协议格式(wire format)。
## 后果
@@ -2,5 +2,5 @@
# 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-11-dev-invariants-over-deep-readonly.md: dc89b5b66d02bde2f4fe2794e04b76ecdeac62ae
2026-06-11-dev-invariants-over-deep-readonly.zh.md: 2c3c9e221b42f4b45216d09e825a41b1be3bcee9
2026-06-11-dev-invariants-over-deep-readonly.md: 01a9e45fac77d2513924e78566a44a6059dd9d28
2026-06-11-dev-invariants-over-deep-readonly.zh.md: 8f8db9f5bb095b8fc9c600b741c16d630a6c8167
@@ -1,9 +1,9 @@
# RFC: Source-owned session immutability and dev-mode invariants
English | [中文](2026-06-11-dev-invariants-over-deep-readonly.zh.md)
Status: implemented
English | [中文](2026-06-11-dev-invariants-over-deep-readonly.zh.md)
## Problem
The session log needs two different protections: immutable ownership of each stored fact, and checks for relationships among facts across time and service seams. Conflating them in an optional development plugin would leave production history vulnerable; trying to express both through TypeScript readonly types would not create a runtime boundary or describe relational rules.
@@ -1,4 +1,4 @@
# RFC源端拥有的会话不可变性与开发模式不变式
# RFC: 源端拥有的会话不可变性与开发模式不变式
Status: implemented
@@ -2,5 +2,5 @@
# 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-11-event-sourced-sessions.md: 04ff974826ffbc9052c7eb9f5794bc16557241f9
2026-06-11-event-sourced-sessions.zh.md: 12d6aedcc32b4d93f0dbe010854bcd3a0721703f
2026-06-11-event-sourced-sessions.md: c1460b84c5928a53537a5f6a51f41c4d5b101a12
2026-06-11-event-sourced-sessions.zh.md: 938404524cd23a5a5b3d6d0b7fd5020df3d35a51
@@ -1,9 +1,9 @@
# RFC: Event-sourced sessions with derived message history
English | [中文](2026-06-11-event-sourced-sessions.zh.md)
Status: implemented
English | [中文](2026-06-11-event-sourced-sessions.zh.md)
## Problem
The MVP requires strict event-based tracing with fully replayable sessions (严格的基于事件的trace、logging系统,session完全可回放).
@@ -1,9 +1,9 @@
# RFC事件溯源的会话与派生消息历史
[English](2026-06-11-event-sourced-sessions.md) | 中文
# RFC: 事件溯源的会话与派生消息历史
Status: implemented
[English](2026-06-11-event-sourced-sessions.md) | 中文
## 问题
MVP 要求严格的基于事件的追踪,以及完全可回放的会话(严格的基于事件的 trace、logging 系统,session 完全可回放)。
@@ -14,7 +14,7 @@ MVP 要求严格的基于事件的追踪,以及完全可回放的会话(严
追加操作是同步的(热路径从不阻塞于 I/O);`session/event` 是同步通知;持久化插件在后台缓冲写入,并在每个轮次结束时触发的 `session/flush` 检查点处等待排空。
顺序契约:agent loop(智能体循环)追加到会话,再发出对应的 Cordis 事件;`agent/step-result` waterfall(瀑布式事件)在 `assistant/message` 追加之前运行,因此日志记录的是工具调度实际使用的消息。回归测试固定了这一顺序。
顺序契约:agent loop(智能体循环)*先*追加到会话,再发出对应的 Cordis 事件;`agent/step-result` waterfall(瀑布式事件)在 `assistant/message` 追加之前运行,因此日志记录的是工具调度实际使用的消息。回归测试固定了这一顺序。
## 曾考虑的替代方案
@@ -2,5 +2,5 @@
# 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-11-microkernel-event-taxonomy.md: c66968257a5a6304f187ccb5b9a162aa143e608d
2026-06-11-microkernel-event-taxonomy.zh.md: 7becf5872aee16fe21b4acbbc61920ed91c40f44
2026-06-11-microkernel-event-taxonomy.md: 47e363f949b506756d9b20ee7dc53ca81e3f5e4d
2026-06-11-microkernel-event-taxonomy.zh.md: 3b0aa54108bf8737e6fe341c7b0797a554d328ec
@@ -1,9 +1,9 @@
# RFC: Microkernel — extension via Cordis event taxonomy, one concrete loop
English | [中文](2026-06-11-microkernel-event-taxonomy.zh.md)
Status: implemented
English | [中文](2026-06-11-microkernel-event-taxonomy.zh.md)
## Problem
The product principle is "everything is a plugin": hooks, /goal, /loop, dynamic workflows, compaction, sandboxing, permissions, UI, persistence, MCP, skills must all be writable as plugins without modifying the core.
@@ -1,4 +1,4 @@
# RFC微内核——通过 Cordis 事件分类体系实现扩展,唯一具体循环
# RFC: 微内核——通过 Cordis 事件分类体系实现扩展,唯一具体循环
Status: implemented
@@ -2,5 +2,5 @@
# 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-11-runtime-arg-validation.md: 6da117643166d304bee1d368a314cc1602cac828
2026-06-11-runtime-arg-validation.zh.md: 5f41ff99a2d56f39ff8d61191d92dc782f163c82
2026-06-11-runtime-arg-validation.md: 82d01263225ce719e80631d1890f0a6cc328119d
2026-06-11-runtime-arg-validation.zh.md: 4bd6720f7ec87fcef028ba94f445a5210f1b0d7a
@@ -1,9 +1,9 @@
# RFC: Runtime arg validation at the model boundary
English | [中文](2026-06-11-runtime-arg-validation.zh.md)
Status: implemented
English | [中文](2026-06-11-runtime-arg-validation.zh.md)
## Problem
`defineTool` ([the custom schema DSL](2026-06-11-custom-schema-dsl.md)) gives tool authors a typed `execute(args)` via the `InferArgs<S>` mapping. But that type is a compile-time claim about a value that arrives at runtime as model-generated JSON: nothing forced the model to honor the schema, so a malformed call — missing a required key, a string where a number was declared, an enum value outside the set — reached `execute` typed-in-name-only. The tool body then either crashed on the bad shape (a generic stack trace the model can't act on) or, worse, silently misbehaved. Meanwhile the converter already encodes the exact structure a validator would need to walk.
@@ -1,4 +1,4 @@
# RFC模型边界处的运行时参数校验
# RFC: 模型边界处的运行时参数校验
Status: implemented
@@ -2,5 +2,5 @@
# 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-11-structured-error-taxonomy.md: 2baf88a1f942215e79561e565f455276c80178c4
2026-06-11-structured-error-taxonomy.zh.md: 90b0fdd7f6c8b4f0565c6538c2ddd4cb31680c38
2026-06-11-structured-error-taxonomy.md: 5a5f75038b6124457d2d4d08cbe0bec80415a387
2026-06-11-structured-error-taxonomy.zh.md: d5a45a447ca00d48cb8366911a10f34c584d915c
@@ -1,9 +1,9 @@
# RFC: Structured error taxonomy
English | [中文](2026-06-11-structured-error-taxonomy.zh.md)
Status: implemented
English | [中文](2026-06-11-structured-error-taxonomy.zh.md)
## Problem
Failures crossed seams as bare strings. A tool error flattened to a text block — name, code, and stack lost — so a future sandbox/retry plugin couldn't tell ENOENT from EACCES, and the model got less actionable feedback than it could. A non-Error throw degraded further: the loop wrapped it in `new Error(String(x))`, dropping any code. And `LlmError` was the only typed error in the system, with no shared base, so there was nothing for a consumer to `instanceof` against generically.
@@ -1,4 +1,4 @@
# RFC结构化错误分类体系
# RFC: 结构化错误分类体系
Status: implemented
@@ -2,5 +2,5 @@
# 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-11-tool-schemas-in-prompt-assembly.md: 443e6f20115e5a76001b4466c2d756675adbd886
2026-06-11-tool-schemas-in-prompt-assembly.zh.md: 03624b98fabdedf691f3fb448cc5f37ba5a649ef
2026-06-11-tool-schemas-in-prompt-assembly.md: 260d56ffab054d874e13eb34eac029bf59b381fc
2026-06-11-tool-schemas-in-prompt-assembly.zh.md: 5ae98dfac676574d00f34580ec079bb6a8f2331f
@@ -1,9 +1,9 @@
# RFC: Tool schemas are part of the system-prompt assembly
English | [中文](2026-06-11-tool-schemas-in-prompt-assembly.zh.md)
Status: implemented
English | [中文](2026-06-11-tool-schemas-in-prompt-assembly.zh.md)
## Problem
On the wire, tool schemas travel in a dedicated `tools` field of the model request, not in prompt text. Architecturally, though, "what the model is told it can do" is one coherent concern: prompt sections and the tool list are assembled from the same plugin contributions and consumed at the same moment.
@@ -1,4 +1,4 @@
# RFC工具 schema 是系统提示词组装的一部分
# RFC: 工具 schema 是系统提示词组装的一部分
Status: implemented
@@ -2,5 +2,5 @@
# 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-13-capability-seams.md: e9d417dbd2bafcaece39601b12dbb310feb1e19b
2026-06-13-capability-seams.zh.md: c569f3df083ec48bd05e6be2d3a1e5875fde362f
2026-06-13-capability-seams.md: f10182737347fc54ff3fd06edb299e569397773d
2026-06-13-capability-seams.zh.md: e65d2f31eb80ce80e92b3e4bbb4d5b8948185c44
@@ -1,9 +1,9 @@
# RFC: Capability seams — interface / implementation / consumer split
English | [中文](2026-06-13-capability-seams.zh.md)
Status: implemented
English | [中文](2026-06-13-capability-seams.zh.md)
## Problem
The harness has swappable capabilities — bash execution today, sandboxed/remote executors and alternative model providers tomorrow. A capability has three concerns that change at different rates and for different reasons: the *contract* (what the capability is), the *implementation* (how it runs), and the *consumer surface* (what the model and other plugins program against). Bundling them in one package couples those rates of change — swapping a local executor for a sandboxed one would churn the tool schemas the model sees, even though the model-facing contract never changed.
@@ -1,9 +1,9 @@
# RFC能力 seam——接口/实现/消费方三分
[English](2026-06-13-capability-seams.md) | 中文
# RFC: 能力 seam——接口/实现/消费方三分
Status: implemented
[English](2026-06-13-capability-seams.md) | 中文
## 问题
harness 具有可替换的能力:当前是 bash 执行,未来会有沙箱化/远程执行器和替代模型提供方。一项能力涉及三个关注点,它们以不同速率、因不同原因变化:*契约*(这项能力是什么)、*实现*(它如何运行)、*消费方接口*(模型和其他插件面向什么编程)。将三者捆绑在一个包(package)中会耦合这些变化速率——把本地执行器换成沙箱化执行器时,模型看到的工具 schema 也会被搅动,尽管面向模型的契约从未改变。
@@ -25,7 +25,7 @@ harness 具有可替换的能力:当前是 bash 执行,未来会有沙箱化
## 曾考虑的替代方案
- **单一合并包**:否决。因为它重新耦合了三分设计本要分离的三种变化速率(这正是拆分的意义所在)。
- **`@cordisjs/plugin-capability`**:这是完全不同的维度。它是一个权限/能力*安全*服务(具名权限加继承,通过 `ctx.capability.test` 对会话进行检测),是延后的权限/沙箱工作(`tools/pre-execute` deny/ask seam)的候选方案,**不是**替换实现的机制。混淆这两个「能力」概念正是本 RFC 所指出的陷阱。
- **`@cordisjs/plugin-capability`**:这是完全不同的维度。它是一个权限/能力*安全*服务(具名权限加继承,通过 `ctx.capability.test` 对会话进行检测),是延后的权限/沙箱工作(`tools/pre-execute` deny/ask seam)的候选方案,不是替换实现的机制。混淆这两个「能力」概念正是本 RFC 所指出的陷阱。
## 后果
@@ -2,5 +2,5 @@
# 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-13-twin-llm-adapters.md: 4efefcf4e2f6b1d60567ba3bfed7ae1ea53a7a4e
2026-06-13-twin-llm-adapters.zh.md: 6cabd95c5361afdea5b33ffdee34a5acb6026f7f
2026-06-13-twin-llm-adapters.md: edd7080e2a16e9f1af2ebd56a4265040dc970c17
2026-06-13-twin-llm-adapters.zh.md: 2087d498f65326fa0a2cb74be409a926a5e343f5
@@ -1,9 +1,9 @@
# RFC: Two LLM adapters as a design-verification twin
English | [中文](2026-06-13-twin-llm-adapters.zh.md)
Status: implemented
English | [中文](2026-06-13-twin-llm-adapters.zh.md)
## Problem
`dsh-llm` owns a provider-neutral streaming vocabulary — the `StreamChunk` protocol (`block-start`, `text-delta`, `reasoning-delta`, `tool-call-delta`, `block-end`, `usage`, `finish`) and the content-block types ([the content-block vocabulary](2026-06-11-content-block-vocabulary.md)). A vocabulary defined against a single adapter risks baking that adapter's quirks into the "neutral" contract: anything the one implementation happens to do becomes the de-facto spec, and the abstraction is unverified until a second provider arrives — by which point the leak is expensive to fix.
@@ -1,4 +1,4 @@
# RFC以两个 LLM 适配器作为设计验证孪生体
# RFC: 以两个 LLM 适配器作为设计验证孪生体
Status: implemented
@@ -2,5 +2,5 @@
# 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-14-session-persistence.md: 4078487b71862791dd25cf2afcfc03255cfeb0be
2026-06-14-session-persistence.zh.md: a44ef9647ca4003bc81023a54b7e9b5945003b02
2026-06-14-session-persistence.md: a50c076746981892239304266194d1e8e309574f
2026-06-14-session-persistence.zh.md: d609e663bd9e36f74bf34404c021a30f54d3bd76
@@ -1,9 +1,9 @@
# RFC: Session persistence as an abstract service over the existing `SessionEvent`
English | [中文](2026-06-14-session-persistence.zh.md)
Status: implemented
English | [中文](2026-06-14-session-persistence.zh.md)
## Problem
Sessions lived only in memory. The example `session-jsonl.ts` plugin (duplicated byte-for-byte in both examples) was write-only telemetry: it buffered `session/event` and appended JSON lines, with no read/replay path, no crash-safety (no fsync, no atomic write, a fire-and-forget dispose drain), no listing, and no format versioning. Nothing could rehydrate a past session from disk into a live agent, so durable resume ("continue yesterday's task"), durable forking, and the ACP `session/load` method ([ACP support](../../implemented/feature/2026-06-14-acp-agent-client-protocol.md)) were all impossible.
@@ -1,4 +1,4 @@
# RFC会话持久化作为基于现有 `SessionEvent` 的抽象服务
# RFC: 会话持久化作为基于现有 `SessionEvent` 的抽象服务
Status: implemented
@@ -23,7 +23,7 @@ Status: implemented
- **仅追加;崩溃的轮次被关闭,而非截断。** 已刷写到 `turn/end` 的事件永不被重写,且循环仅在轮次结束时刷写。由于一个被中断的轮次可能包含大量有效工作,`load` 保留其连续、可解析的事件,并为未应答的工具调用追加错误结果、补一个缺失的 `step/end`,以及带 `{ kind: 'interrupted' }``turn/end`。合成的结果保证恢复后的 provider transcript(文本记录)仍然有效。只有不完整的最后一条记录会被丢弃;在最后一个真实 `turn/end` 处或之前出现解析错误或序号间隙,属于数据损坏,会使该会话不可加载。
- **文件后端为规范实现,数据库后端为经过验证的直接替换。** `SessionEvent` 1:1 映射到一行 `(session_id, seq, type, time, data)``append` 是 INSERT(在一个断言连续 seq 契约的事务中),`load` 是 SELECT … ORDER BY seq。`dsh-session-persistence-sqlite` 正是如此:一个 `SessionPersistence` 子类,接口无变化(opencode 在 SQLite/WAL 上运行的正是这个形状),且通过与 JSONL 后端相同的 `runPersistenceContract` 测试套件。该契约以相同的语义约束两个后端(惰性物化、加载时关闭中断轮次、连续 seq),一次表达在文件字节上,一次表达在数据库行上。
- **元数据在日志之外。** 格式版本、cwd 和谱系是存储关注点,不是可回放的对话状态,因此它们存放在 `dsh-session` 拥有的 `SessionHeader` 中,并通过新的只读属性 `session.header` 附加到 `Session` 上——永远不进入 `SessionEventMap`,永远不到达 `deriveMessages()`。替代方案(一个可合并扩展的 `session/meta` 事件作为日志第 0 行)被否决:日志内事件会随 seed/fork 的会话免费携带,但元数据不是可回放状态,因此显式的日志外 header seam 是更干净的代价。(header 最初被拆分为不可变的 `SessionHeader` 加可变的 `SessionSummary`,二者的联合类型为 `SessionMeta`;可变 summary 后来因属于死状态而被移除——见 [移除可变会话摘要](../simplification/2026-06-19-drop-mutable-session-summary.md)。)
- **`ctx.agents.create()``ctx.agents.resume()` 是异步工厂;resume 还跨越持久化边界。** `ctx.agents.resume({ resumeSessionId })` 等待 `ctx.sessionPersistence.load`,用加载的事件重建活跃会话(使 `lastTurnNumber`/`deriveMessages` 得以延续),并在恢复的 id 上启动一个新 agent(不是 `${agentId}-session`)。agent loop(智能体循环)不会硬注入 `sessionPersistence`(那样会让非持久化的演示永远挂起);当 `sessionPersistence` 不存在时,`resume` 以明确的错误拒绝。
- **`ctx.agents.create()``ctx.agents.resume()` 是异步工厂;resume 还跨越持久化边界。** `ctx.agents.resume({ resumeSessionId })` 等待 `ctx.sessionPersistence.load`,用加载的事件重建活跃会话(使 `lastTurnNumber`/`deriveMessages` 得以延续),并在恢复的 id 上启动一个新 agent(不是 `${agentId}-session`)。agent loop(智能体循环)不会硬注入 `sessionPersistence`(那样会让非持久化的演示永远挂起);当不存在时,`resume` 以明确的错误拒绝。
## 曾考虑的替代方案
@@ -2,5 +2,5 @@
# 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-15-turn-enclosure-invariant.md: bf0789f21ba7bd928e023bb5fd844d9ec78c4bc1
2026-06-15-turn-enclosure-invariant.zh.md: 644e7b975e024286d5307f586f509c2b4a9f21ea
2026-06-15-turn-enclosure-invariant.md: 26ae1890e481288b3be17f97db14c332f69be4b8
2026-06-15-turn-enclosure-invariant.zh.md: 1946f829100e39afed826b8cf3ba82d22058c27d
@@ -1,9 +1,9 @@
# RFC: Every session event is enclosed in a turn
English | [中文](2026-06-15-turn-enclosure-invariant.zh.md)
Status: implemented
English | [中文](2026-06-15-turn-enclosure-invariant.zh.md)
## Problem
A durable session-persistence backend (added in a companion change) uses the **turn** as its crash-recovery boundary: a crash can leave an unclosed final turn, which `load` closes with a synthetic `turn/end {kind:'interrupted'}` while preserving the turn's real events (see [session persistence](2026-06-14-session-persistence.md)). This recovery is only well-defined if nothing *legitimately* durable sits OUTSIDE a turn — between the last `turn/end` and the next `turn/start` — since such an event would be swept into the next turn's interrupted close.
@@ -1,4 +1,4 @@
# RFC每个会话事件都封闭在一个轮次内
# RFC: 每个会话事件都封闭在一个轮次内
Status: implemented
@@ -10,7 +10,7 @@ Status: implemented
这一假设并不成立。有两条路径在任何轮次之外记录了事件:
1. **排队的用户消息。** agent loop(智能体循环)排空排队消息并在 `turn/start` **之前**追加 `user/message`——于是一个轮次自身的提示词落在了前一个 `turn/end` 与下一个 `turn/start` 之间的间隙中。
1. **排队的用户消息。** agent loop(智能体循环)排空排队消息并在 `turn/start` *之前*追加 `user/message`——于是一个轮次自身的提示词落在了前一个 `turn/end` 与下一个 `turn/start` 之间的间隙中。
2. **空闲时的上下文注入。** `agent.inject()` 直接追加一条 `context/message`。它在生产环境中的真实调用方是 `dsh-tool-bash`,后者从 `ctx.bash.onTaskDone` 注入后台任务完成通知——该回调在后台 bash 任务完成时触发,而这经常发生在 agent **空闲**(轮次之间)时。
在情况 2 中,如果注入的 `context/message` 是 flush/dispose 之前的最后一个事件(之后没有轮次追加 `turn/end`),`scanLog` 会将其视为崩溃残留并在**恢复时丢弃**——注入的上下文已持久写入磁盘,但重新加载后被静默丢失。情况 1 本身无害(`user/message` 之后总会跟着它触发的轮次),但使「什么可以出现在轮次之外」这条规则变得模糊。
@@ -2,5 +2,5 @@
# 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-17-filesystem-capability-seam.md: c502ae712de22e97661192057d4410c7c55ea044
2026-06-17-filesystem-capability-seam.zh.md: 4e544e24c548a52bde254886a65ff2fdb559a986
2026-06-17-filesystem-capability-seam.md: c87c8a44d2e956a9613039378d26f72e8ee97ee7
2026-06-17-filesystem-capability-seam.zh.md: 4864e77ced5c078fc8c1e970a2ff88c78a37af2a
@@ -1,9 +1,9 @@
# RFC: Filesystem capability seam — ctx.fs, local backend, and model-facing filesystem tools
English | [中文](2026-06-17-filesystem-capability-seam.zh.md)
Status: implemented
English | [中文](2026-06-17-filesystem-capability-seam.zh.md)
## Problem
The harness has a concrete `bash` capability seam (`dsh-bash` / `dsh-bash-local` / `dsh-tool-bash`), but filesystem operations are about to be added as model-facing tools without an equivalent seam. If `read`, `write`, and `edit` directly use `node:fs`, the model-facing tool package will own filesystem execution policy, local path resolution, atomic write behavior, text decoding, symlink behavior, and edit semantics all at once.
@@ -1,9 +1,9 @@
# RFC文件系统能力 seam——ctx.fs、本地后端与面向模型的文件系统工具
[English](2026-06-17-filesystem-capability-seam.md) | 中文
# RFC: 文件系统能力 seam——ctx.fs、本地后端与面向模型的文件系统工具
Status: implemented
[English](2026-06-17-filesystem-capability-seam.md) | 中文
## 问题
harness 已有一个具体的 `bash` 能力 seam`dsh-bash` / `dsh-bash-local` / `dsh-tool-bash`),但文件系统操作即将作为面向模型的工具加入,却没有等价的 seam。如果 `read``write``edit` 直接使用 `node:fs`,面向模型的工具包将同时承担文件系统执行策略、本地路径解析、原子写入行为、文本解码、符号链接行为和编辑语义。
@@ -80,10 +80,10 @@ harness 已有一个具体的 `bash` 能力 seam`dsh-bash` / `dsh-bash-local`
解析后的目标必须至少暴露三个概念:
- 原始输入路径,用于诊断。
- 不透明的 `targetKey`,用于过期守护和文件状态查找。本地后端可能使用类似 realpath 的键;远程后端可能使用工作区 URI 或文件 id。消费方禁止解析或假设它是本地绝对路径。
- 不透明的 `targetKey`,用于陈旧守护和文件状态查找。本地后端可能使用类似 realpath 的键;远程后端可能使用工作区 URI 或文件 id。消费方禁止解析或假设它是本地绝对路径。
- `displayPath`,用于面向模型/UI 的输出。根据后端不同,它可能是本地绝对路径、工作区相对路径或远程 URI。
读取和变更结果必须包含不透明的文件 `version`。本地后端可以使用 mtime/size 或类似 hash 的令牌;远程后端可以使用 revision id。`dsh-fs-policy` 插件记录版本用于过期检查;消费方可以展示相关元数据但禁止解释版本令牌。
读取和变更结果必须包含不透明的文件 `version`。本地后端可以使用 mtime/size 或类似 hash 的令牌;远程后端可以使用 revision id。`dsh-fs-policy` 插件记录版本用于陈旧检查;消费方可以展示相关元数据但禁止解释版本令牌。
提供方返回已解码的文本:`readText` 返回整个常规文本文件,`streamText` 为大文件流式传输相同的文本语义。两者负责常规文件检查;有界的行/输出处理不是它们的职责——行窗口化、带行号渲染和总行数统计位于执行器(`dsh-tool-fs`)中,执行器通过 `ctx.fs` 读取并渲染面向模型的窗口。提供方负责 UTF-8 解码和二进制/NUL 拒绝;它不知道行窗口或视图。
@@ -91,7 +91,7 @@ harness 已有一个具体的 `bash` 能力 seam`dsh-bash` / `dsh-bash-local`
全文件写入创建或替换 UTF-8 文本文件。后端在支持且有文档说明时可以创建父目录。已有的非常规目标被拒绝。`writeText` 接受一个可选期望:`createIfAbsent` 创建缺失的目标并拒绝已存在的(报 `FS_NOT_OBSERVED`,这是策略为未观测 owner 使用的路径);`replaceIfVersion` 仅在目标处于观测版本时替换,否则报 `FS_STALE_VERSION`;省略期望则为无条件的裸提供方创建或覆盖。策略插件根据 owner 的观测状态选择提供哪个期望。
字面编辑是提供方原语(`editText`),而非在 `tool-fs` 中由读取加写入组合而成。字面匹配、重复匹配拒绝、CRLF 保留、二进制拒绝、可选的过期版本检查和原子读-改-写必须一起留在后端的变更临界区内。`editText` 接受相同的可选版本期望;过期检查在字面匹配之前运行,因此基于旧读取的编辑会报 `FS_STALE_VERSION`。远程后端可以将编辑实现为原生的 compare-and-edit 操作;消费方不强制本地风格的组合。
字面编辑是提供方原语(`editText`),而非在 `tool-fs` 中由读取加写入组合而成。字面匹配、重复匹配拒绝、CRLF 保留、二进制拒绝、可选的陈旧版本检查和原子读-改-写必须一起留在后端的变更临界区内。`editText` 接受相同的可选版本期望;陈旧检查在字面匹配之前运行,因此基于旧读取的编辑会报 `FS_STALE_VERSION`。远程后端可以将编辑实现为原生的 compare-and-edit 操作;消费方不强制本地风格的组合。
策略插件(而非 `ctx.fs`)对先前观测进行门控:`edit` 要求 owner 有先前观测(否则报 `FS_NOT_OBSERVED`),记录的版本作为 CAS 基础传给 `editText`。在策略插件缺席时,`ctx.fs` 本身是一个完整的无约束 seam(无条件写入/编辑);工具从不与策略方法耦合。
@@ -129,8 +129,8 @@ harness 已有一个具体的 `bash` 能力 seam`dsh-bash` / `dsh-bash-local`
本仓库曾踩过的防御性模式类别被直接固定:
- **原子写入临时文件安全。** 写入/编辑通过目标旁边一个私有随机 `0700` 目录中的独占 owner-only`'wx'``0o600`)临时文件暂存,失败时清理,最后原子 rename——与 bash 溢出文件规则一致,因为可预测的 world-readable 临时路径招致符号链接竞争和信息泄露。测试断言权限,并断言已存在的临时路径不会被覆盖;此原语是 seam 的常设要求。
- **通过符号链接的 `targetKey` 同一性。** 两个输入路径解析到同一 realpath 时共享一个观测状态条目:通过路径 A 的 `read` 满足通过符号链接路径 B 的 `edit` 的读后编辑守护,通过一个路径的过期写入可通过另一个路径检测到。
- **并发/过期竞争。** 对同一目标的两个并发写入/编辑操作确定性地收敛——一个成功,另一个被 `FS_STALE_VERSION` 拒绝——成功的编辑刷新记录状态,使同一 owner 的下一次编辑可以继续。
- **通过符号链接的 `targetKey` 同一性。** 两个输入路径解析到同一 realpath 时共享一个观测状态条目:通过路径 A 的 `read` 满足通过符号链接路径 B 的 `edit` 的读后编辑守护,通过一个路径的陈旧写入可通过另一个路径检测到。
- **并发/陈旧竞争。** 对同一目标的两个并发写入/编辑操作确定性地收敛——一个成功,另一个被 `FS_STALE_VERSION` 拒绝——成功的编辑刷新记录状态,使同一 owner 的下一次编辑可以继续。
- **HMR(热模块替换)安全与 dispose(资源释放)。** dispose 后端的 fiber 会撤回 `ctx.fs` 提供方;后续的提供方以无继承状态启动。
## 曾考虑的替代方案
@@ -155,6 +155,6 @@ harness 已有一个具体的 `bash` 能力 seam`dsh-bash` / `dsh-bash-local`
**观测状态持久化被推迟。** 观测状态存在于内存中(`dsh-fs-policy` 内部的 `WeakMap`),因此恢复的会话保守地要求文件在写入/编辑前重新读取,直到未来的会话事件或持久化机制使观测可回放。
**错误码成为 seam 的一部分。** `FsError` 错误码使过期版本和观测失败可通过既有的结构化错误分类体系进行机器路由。代价是 `dsh-fs``dsh-llm` 导入共享的 `HarnessError` 基类;该依赖是有意为之且限于错误词汇。
**错误码成为 seam 的一部分。** `FsError` 错误码使陈旧版本和观测失败可通过既有的结构化错误分类体系进行机器路由。代价是 `dsh-fs``dsh-llm` 导入共享的 `HarnessError` 基类;该依赖是有意为之且限于错误词汇。
**包拆分的成本前置。** 三包拆分在只有一个后端时就增加了样板代码。这是有意为之:文件系统访问是可能的沙箱/远程边界,在面向模型的工具发布后再改包接口代价更高。
@@ -2,5 +2,5 @@
# 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-18-agent-lifecycle-and-ownership-seams.md: a70e7db8d809efd68ae770995795fc7b3d1b83d2
2026-06-18-agent-lifecycle-and-ownership-seams.zh.md: 3fb68336c56b5296f18b3587ea42399a05362733
2026-06-18-agent-lifecycle-and-ownership-seams.md: c37578020366dd40250c061e34a86d2de6907e19
2026-06-18-agent-lifecycle-and-ownership-seams.zh.md: 1f3cb609da5508d22046f445e08a75dc5a7901cd
@@ -1,9 +1,9 @@
# RFC: Agent lifecycle and ownership seams
English | [中文](2026-06-18-agent-lifecycle-and-ownership-seams.zh.md)
Status: implemented
English | [中文](2026-06-18-agent-lifecycle-and-ownership-seams.zh.md)
## Problem
Several ACP and tool-bash limitations were symptoms of the same missing seam: plugins could create or resume agents through `ctx.agents`, but they could not own and dispose one agent independently, and long-running bash tasks carried no stable owner in the executor itself. ACP aborted and awaited agents on disconnect but could not unregister just that session's agent; `session/cancel` could not cancel queued-but-not-yet-started work; and `tool-bash` kept task ownership in a plugin-local `Map`, so an HMR reload could make an old task look unowned.
@@ -1,9 +1,9 @@
# RFCAgent 生命周期与所有权 seam
[English](2026-06-18-agent-lifecycle-and-ownership-seams.md) | 中文
# RFC: Agent 生命周期与所有权 seam
Status: implemented
[English](2026-06-18-agent-lifecycle-and-ownership-seams.md) | 中文
## 问题
ACPAgent Client Protocol)与 tool-bash 的若干限制是同一个缺失 seam 的症状:插件可以通过 `ctx.agents` 创建或恢复 agent(智能体),但无法独立拥有和 dispose(资源释放)单个 agent,而长时间运行的 bash 任务在执行器中也没有稳定的所有者。ACP 在断连时中止并等待 agent,却无法仅注销该会话的 agent;`session/cancel` 无法取消已入队但尚未开始的工作;`tool-bash` 将任务所有权保存在插件本地的 `Map` 中,因此一次 HMR(热模块替换)重载就可能让旧任务看起来无主。
@@ -2,5 +2,5 @@
# 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-18-session-surface.md: 31297166b735468147850a81d7fd43a8fa30a1e8
2026-06-18-session-surface.zh.md: 159aefc10261ac5380701f46c3d4a367674940b1
2026-06-18-session-surface.md: bcad236eaa2bead3c140e7a12772729710d43f83
2026-06-18-session-surface.zh.md: 2ec60501c183d1bdab22ceb67b7f6aa0040ae82b
@@ -1,9 +1,9 @@
# RFC: Session surface — a linked list over the event log for LLM message derivation
English | [中文](2026-06-18-session-surface.zh.md)
Status: implemented
English | [中文](2026-06-18-session-surface.zh.md)
## Problem
The event log is authoritative, but history manipulation had no durable shared mechanism. Plugins such as compaction would otherwise rewrite derived requests through order-sensitive listeners, leave no provenance, and require repeated changes to `deriveMessages()`.
@@ -1,9 +1,9 @@
# RFC会话 surface——基于事件日志的链表,用于 LLM 消息派生
[English](2026-06-18-session-surface.md) | 中文
# RFC: 会话 surface——基于事件日志的链表,用于 LLM 消息派生
Status: implemented
[English](2026-06-18-session-surface.md) | 中文
## 问题
事件日志是权威数据源,但历史操纵此前没有持久化的共享机制。如果没有这样的机制,上下文压缩(context compaction)等插件只能通过顺序敏感的监听器改写派生请求,不留溯源信息,且每次新增操纵都要反复修改 `deriveMessages()`
@@ -2,5 +2,5 @@
# 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-18-shared-persistence-write-coordinator.md: 3fc30dc2e1382fd983d050433123f46a2cd0ed19
2026-06-18-shared-persistence-write-coordinator.zh.md: 38e900d38cc48317836717ddeda5323cf97df993
2026-06-18-shared-persistence-write-coordinator.md: 7648c6f8e43fb8c78f881838aa4d476aba209dca
2026-06-18-shared-persistence-write-coordinator.zh.md: 19f583c2f3d78fcdac1378cfe00e49c0be943eb1
@@ -1,9 +1,9 @@
# RFC: Shared persistence write coordinator
English | [中文](2026-06-18-shared-persistence-write-coordinator.zh.md)
Status: implemented
English | [中文](2026-06-18-shared-persistence-write-coordinator.zh.md)
## Problem
`dsh-session-persistence-jsonl` and `dsh-session-persistence-sqlite` intentionally prove the same `SessionPersistence` contract over different storage media, but their write-path orchestration was duplicated: per-session state, `session/created` adoption, backend-specific prefix reads, write-behind buffers, serialized flush chains, HMR seeding, and dispose drains. The pure seed-prefix collision and serializability guards had already moved into the seam package; the remaining orchestration was still correctness-heavy and received the same fixes twice. A code-level diff showed the two backends were byte-identical — or same-algorithm — for ALL of it: the four maps (`states`/`buffers`/`chains`/`inits`), `installWritePath`, `initFor`, `onCreated`'s four cases, `flush`, `drain`, `serialize`, `adopt`, `adoptLivePrefix`, `assertVersion`, and the `create`/`append`/`load` skeletons. Only the storage primitives (write bytes vs. INSERT rows) differed.
@@ -1,12 +1,12 @@
# RFC共享持久化写入协调器
[English](2026-06-18-shared-persistence-write-coordinator.md) | 中文
# RFC: 共享持久化写入协调器
Status: implemented
[English](2026-06-18-shared-persistence-write-coordinator.md) | 中文
## 问题
`dsh-session-persistence-jsonl``dsh-session-persistence-sqlite` 有意在不同存储介质上证明同一份 `SessionPersistence` 契约,但它们的写入路径编排是重复的:per-session 状态、`session/created` 接管、后端特定的前缀读取、write-behind 缓冲区、序列化的 flush 链、HMR(热模块替换)种子注入与 dispose(资源释放)排空。纯粹的种子前缀碰撞检查与可序列化守卫已迁入 seam 包;剩余的编排仍然对正确性要求很高,且同样的修复被应用了两次。代码级 diff 表明两个后端在**全部**这些逻辑上要么字节相同、要么算法相同:四个 map(`states`/`buffers`/`chains`/`inits`)、`installWritePath``initFor``onCreated` 的四种分支、`flush``drain``serialize``adopt``adoptLivePrefix``assertVersion`,以及 `create`/`append`/`load` 的骨架。唯一的差异在于存储原语(写字节 vs. INSERT 行)。
`dsh-session-persistence-jsonl``dsh-session-persistence-sqlite` 有意在不同存储介质上证明同一份 `SessionPersistence` 契约,但它们的写入路径编排是重复的:per-session 状态、`session/created` 接管、后端特定的前缀读取、write-behind 缓冲区、序列化的 flush 链、HMR(热模块替换)种子注入与 dispose(资源释放)排空。纯粹的种子前缀碰撞检查与可序列化守卫已迁入 seam 包;剩余的编排仍然对正确性要求很高,且同样的修复被应用了两次。代码级 diff 表明两个后端在全部这些逻辑上要么字节相同、要么算法相同:四个 map(`states`/`buffers`/`chains`/`inits`)、`installWritePath``initFor``onCreated` 的四种分支、`flush``drain``serialize``adopt``adoptLivePrefix``assertVersion`,以及 `create`/`append`/`load` 的骨架。唯一的差异在于存储原语(写字节 vs. INSERT 行)。
## 决策
@@ -19,16 +19,16 @@ Status: implemented
六个方法(五个必需 + 一个可选的生命周期钩子)——协调器与存储之间唯一的 seam:
- `name`——后端标签,用于 dispose 失败时的 `AggregateError`
- `loadStored(id)`——按 id 读取已存储的前缀,扫描**任何**存储范围(JSONL 的每个 cwd bucketSQLite 的 id 全局唯一)。用于恢复/加载,以及通过 `!== undefined` 进行创建碰撞探测。
- `loadLive(id, cwd)`——读取**限定于 `cwd`** 的已存储前缀。**与 `loadStored` 有意区分**HMR live-adoption 只能接管与存活会话处于**同一 cwd** 的持久化日志;同 id 但不同 cwd 的日志是碰撞而非恢复。合并二者会重新引入跨 cwd 接管 bug。SQLite 忽略 `cwd`
- `appendBatch(meta, events, isMaterialized)`——持久追加一个连续批次,在尚未物化时**原子地**惰性物化会话(物化写入与首批事件必须一起提交——崩溃不得留下一个已物化但为空的会话;这就是为什么没有单独的 `materialize` 钩子)。
- `loadStored(id)`——按 id 读取已存储的前缀,扫描任何存储范围(JSONL 的每个 cwd bucketSQLite 的 id 全局唯一)。用于恢复/加载,以及通过 `!== undefined` 进行创建碰撞探测。
- `loadLive(id, cwd)`——读取限定于 `cwd` 的已存储前缀。**与 `loadStored` 有意区分**HMR live-adoption 只能接管与存活会话处于同一 cwd 的持久化日志;同 id 但不同 cwd 的日志是碰撞而非恢复。合并二者会重新引入跨 cwd 接管 bug。SQLite 忽略 `cwd`
- `appendBatch(meta, events, isMaterialized)`——持久追加一个连续批次,在尚未物化时原子地惰性物化会话(物化写入与首批事件必须一起提交——崩溃不得留下一个已物化但为空的会话;这就是为什么没有单独的 `materialize` 钩子)。
- `commitRepair(meta, tornMarker, closers)`——使崩溃修复持久化:截断损坏的尾部(当且仅当 `tornMarker !== undefined`)并追加 `closers`。**不要求原子性**——JSONL 合理地分两步 fsync(先截断再追加),SQLite 在一个事务中完成 DELETE+INSERT。用于 `load`(截断 + 合成 closers)和 live-adoption(仅截断,`closers = []`)。
- `list()`——列出所有已存储的元数据。
- `close?()`——可选的生命周期清理(SQLite 关闭 db 句柄;JSONL 省略),在 dispose effect 中于静默排空**之后**被 await,因此 close 失败不会掩盖排空错误。
- `close?()`——可选的生命周期清理(SQLite 关闭 db 句柄;JSONL 省略),在 dispose effect 中于静默排空之后被 await,因此 close 失败不会掩盖排空错误。
### 不透明的 torn marker
保持 seam 整洁的唯一设计选择:崩溃修复中「损坏尾部在哪里」的 token 对协调器是**不透明的**。协调器计算合成 closers(它拥有来自 `dsh-session``interruptedTurnClosers`),但它只测试 `tornMarker !== undefined` 并将值原样传回 `commitRepair`——从不检视其内容。每个后端选择自己的 marker 类型:JSONL 使用要截断到的字节偏移,SQLite 使用要从其开始删除的 seq(两者恰好都是 `number`)。JSONL 后端将其 `committedBytes < buffer.byteLength` 比较折叠**在钩子内部**,因此返回的 marker 已经是 `number | undefined`;如果不做这层折叠,协调器就必须了解字节长度。
保持 seam 整洁的唯一设计选择:崩溃修复中「损坏尾部在哪里」的 token 对协调器是不透明的。协调器计算合成 closers(它拥有来自 `dsh-session``interruptedTurnClosers`),但它只测试 `tornMarker !== undefined` 并将值原样传回 `commitRepair`——从不检视其内容。每个后端选择自己的 marker 类型:JSONL 使用要截断到的字节偏移,SQLite 使用要从其开始删除的 seq(两者恰好都是 `number`)。JSONL 后端将其 `committedBytes < buffer.byteLength` 比较折叠在钩子内部,因此返回的 marker 已经是 `number | undefined`;如果不做这层折叠,协调器就必须了解字节长度。
## 测试
@@ -2,5 +2,5 @@
# 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-branded-ids.md: f6d066857d8904ae5343f12310266663806a0ae2
2026-06-20-branded-ids.zh.md: 80c158e598f3007416d31a89a6704a759798e44e
2026-06-20-branded-ids.md: aab47a1413451edb707ae797e5b99cda6e34efad
2026-06-20-branded-ids.zh.md: 1ccfda3282069510bcdbfbddf4546305fd0923a5
@@ -1,9 +1,9 @@
# RFC: Branded IDs everywhere they belong
English | [中文](2026-06-20-branded-ids.zh.md)
Status: implemented
English | [中文](2026-06-20-branded-ids.zh.md)
## Problem
The harness already brands three identifiers — `CallId` (`packages/llm/llm/src/brand.ts`), `SessionId` (`packages/core/session/src/types.ts`), and `AgentId` (`packages/core/agent/src/types.ts`) — using the `Branded<B> = string & { readonly [BRAND]: B }` machinery (owned by the type-only `@deepseek-ai/dsh-brand` package at `packages/util/brand/` — see its [README](../../../../packages/util/brand/README.md)) and a zero-cost cast factory per type. `dsh-brand` also states the governing policy: *"Branding is for ids that cross package boundaries and could plausibly be confused; not every string needs a brand."* That policy is right; the problem is that it is only half-applied. Two gaps let a structurally-identical-but-semantically-wrong string slip through the type checker today.
@@ -1,16 +1,16 @@
# RFC在所有应有之处使用 branded ID
[English](2026-06-20-branded-ids.md) | 中文
# RFC: 在所有应有之处使用 branded ID
Status: implemented
[English](2026-06-20-branded-ids.md) | 中文
## 问题
harness 已经为三个标识符做了 brand 处理:`CallId``packages/llm/llm/src/brand.ts`)、`SessionId``packages/core/session/src/types.ts`)和 `AgentId``packages/core/agent/src/types.ts`),使用 `Branded<B> = string & { readonly [BRAND]: B }` 机制(由纯类型包(package `@deepseek-ai/dsh-brand` 拥有,位于 `packages/util/brand/`,见其 [README](../../../../packages/util/brand/README.md)),并为每个类型提供零开销的 cast 工厂。`dsh-brand` 还声明了治理策略:*"Branding 用于跨包边界且可能被混淆的 id;不是每个 string 都需要 brand。"* 这条策略是正确的;问题在于它只落实了一半。两处缺口使得结构相同但语义错误的 string 今天仍能通过类型检查器。
**缺口 1bash seam 中未 brand 的 ID。** `BashTask.id` 以及所有执行器/工具边界使用裸 `string`,尽管生成的值与默认 session id 具有相同的 `name-N` 形状。模型还通过 `task_id` 返回该值,因此混淆 task id 和 session id 既类型正确又可达。
bash **owner token** 是相关的子情形:`BashExecRequest.owner?: string``BashExecSpec.owner: string | undefined``packages/bash/bash/src/types.ts`)被文档描述为刻意*不透明*的隔离键,但在所有实际调用方中,该值就是所属 agent(智能体)的 `session.header.id``callerToken = (exec) => exec.agent?.session.header.id`,位于 `packages/bash/tool-bash/src/index.ts`),即一个穿着 `string` 外衣的 `SessionId`。它被用于访问控制比较(`owner !== callerToken(exec)`),因此一个不匹配但类型正确的 string 在此处就是一个跨会话隔离 bug,而当前类型系统无法捕获。这正是 [unify-the-agent-id-and-the-session-id](../../proposed/simplification/2026-06-20-unify-agent-and-session-id.md) 提案所称的"bash owner-token alias hole"。
bash **owner token** 是相关的子情形:`BashExecRequest.owner?: string``BashExecSpec.owner: string | undefined``packages/bash/bash/src/types.ts`)被文档描述为刻意*不透明*的隔离键,但在所有实际调用方中,该值就是所属 agent(智能体)的 `session.header.id``callerToken = (exec) => exec.agent?.session.header.id`,位于 `packages/bash/tool-bash/src/index.ts`),即一个穿着 `string` 外衣的 `SessionId`。它被用于访问控制比较(`owner !== callerToken(exec)`),因此一个不匹配但类型正确的 string 在此处就是一个跨会话隔离 bug,而当前类型系统无法捕获。这正是 [unify-the-agent-id-and-the-session-id](../../proposed/simplification/2026-06-20-unify-agent-and-session-id.md) 提案所称的`session.header.id` 作为 owner 的别名缺口("bash owner-token alias hole"
**缺口 2:既有 brand 的侵蚀。** `CallId``SessionId``AgentId` 在注册表 map、公开查找参数、ACP 会话跟踪和持久化协调器中退化为裸 string。在查找边界丢弃 brand 会使其主要保护失效。
@@ -18,7 +18,7 @@ bash **owner token** 是相关的子情形:`BashExecRequest.owner?: string`
纯类型变更。Brand 是零开销 cast;运行时行为、序列化、比较和协议格式(wire format)均不变。工作分三部分,全部遵循既有的"不是每个 string 都需要"策略。
- **为 bash task id 加 brand。** 在 `packages/bash/bash/src/types.ts`(拥有该 id 的包)中添加 `BashTaskId = Branded<'BashTaskId'>` 及其同名工厂,从 `@deepseek-ai/dsh-brand` 导入 `Branded`,方式与 `SessionId`/`AgentId` 完全一致。brand 原语位于无依赖的 `dsh-brand` 工具包中,正是为了让 `dsh-bash` 仅依赖它就能为自己的 id 加 brand,而无需引入 `dsh-llm`(或 `dsh-session`)来获取 `Branded`。将其贯穿 `BashTask.id``BashExecutor` seam 方法(`get`/`ownerOf`/`readOutput`/`kill`)、`dsh-bash-local` 中的生成点(在创建时对计数器输出做一次 brand),以及 `dsh-tool-bash` 的校验/访问面(`validateTaskId` 返回 `BashTaskId``task_id` 在模型 string 到达的工具边界处被 brand)。
- **为 bash task id 加 brand。** 在 `packages/bash/bash/src/types.ts`*拥有*该 id 的包)中添加 `BashTaskId = Branded<'BashTaskId'>` 及其同名工厂,从 `@deepseek-ai/dsh-brand` 导入 `Branded`,方式与 `SessionId`/`AgentId` 完全一致。brand 原语位于无依赖的 `dsh-brand` 工具包中,正是为了让 `dsh-bash` 仅依赖它就能为自己的 id 加 brand,而无需引入 `dsh-llm`(或 `dsh-session`)来获取 `Branded`。将其贯穿 `BashTask.id``BashExecutor` seam 方法(`get`/`ownerOf`/`readOutput`/`kill`)、`dsh-bash-local` 中的生成点(在创建时对计数器输出做一次 brand),以及 `dsh-tool-bash` 的校验/访问面(`validateTaskId` 返回 `BashTaskId``task_id` 在模型 string 到达的工具边界处被 brand)。
- **铸造独立的 `OwnerToken` brand。** 在 `packages/bash/bash/src/types.ts` 中添加 `OwnerToken = Branded<'OwnerToken'>`;将 `BashExecRequest.owner` / `BashExecSpec.owner` / `BashExecutor.ownerOf` 的类型标注为 `OwnerToken | undefined``dsh-tool-bash` 消费方在边界处将 agent 的 `session.header.id`(一个 `SessionId`cast 为 `OwnerToken`——这是两套词汇唯一交汇的地方。bash seam 从不导入 `dsh-session`。(理由见下一节。)
@@ -46,7 +46,7 @@ export function OwnerToken(id: string): OwnerToken {
### 为什么不把 `owner` 类型标注为 `SessionId`
执行器将 ownership 视为不透明的,不应依赖 session 模型。独立的 `OwnerToken` 保留了这一边界,同时防止裸 string 或 task id 被当作 owner 传入。`dsh-tool-bash` 拥有访问策略,由它执行 `SessionId` 到 `OwnerToken` 的唯一转换。
执行器将 ownership 视为不透明的,不应依赖 session 模型。独立的 `OwnerToken` 保留了这一边界,同时防止裸 string 或 task id 被当作 owner 传入。`dsh-tool-bash` 拥有访问策略,由它执行来自 `SessionId` 的唯一转换。
## 不在范围内 / 可能的扩展
@@ -65,5 +65,5 @@ export function OwnerToken(id: string): OwnerToken {
## 后果
- **两个接口面的机械性改动。** 传播 brand 涉及 bash seam(接口 + 实现 + 消费方)以及 ACP session-id 接口和持久化协调器。改动面广但严重度低:遗漏的位置是编译错误而非静默 bug。变更可观察地为纯类型变更——无快照或 e2e 行为差异。它与 [unify-the-agent-id-and-the-session-id](../../proposed/simplification/2026-06-20-unify-agent-and-session-id.md) 提案相邻(两者都触及 session-id / owner-token 边界);如果该提案落地,`OwnerToken` 出于上述解耦理由仍与统一后的 id 保持独立。
- **Brand 不做校验。** Brand 是混淆防护,不是正确性证明:一个*错误的* session id 只要仍是合法的 string,就和以前一样能通过类型检查器。本 RFC 不关闭这个缺口(见"不在范围内")——它只阻止传入错误*类*的 id 这种错误
- **Brand 不做校验。** Brand 是混淆防护,不是正确性证明:一个*错误的* session id 只要仍是合法的 string,就和以前一样能通过类型检查器。本 RFC 不关闭这个缺口(见"不在范围内")——它只阻止这类*类别*错误:传入错误*类*的 id。
- **"在哪里停下"仍是判断题。** 为 `BashTaskId` 加 brand 但不为 `ToolName` 加,为 `OwnerToken` 加但不为 `ModelId` 加,是对哪些 string"可能被混淆"的品味判断。合理的评审者可能想要更多或更少;`brand.ts` 中的策略是裁决依据,本 RFC 倾向于面向模型或用于访问控制的 id。
@@ -2,5 +2,5 @@
# 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-extract-example-app-packages.md: 3a0a0f5d4b329afed72bd3c00bf880989e24fe54
2026-06-20-extract-example-app-packages.zh.md: 8945f0c97727479c9e847711a96fc5d18118e09e
2026-06-20-extract-example-app-packages.md: b0dd1fe32b2389578d3f3bd3672435923eb1a5d1
2026-06-20-extract-example-app-packages.zh.md: 9f47c98c9c54357a304490e3d4075fad6cea855a
@@ -1,9 +1,9 @@
# RFC: Extract example apps into packages
English | [中文](2026-06-20-extract-example-app-packages.zh.md)
Status: implemented
English | [中文](2026-06-20-extract-example-app-packages.zh.md)
## Problem
An example folder is supposed to be *thin* — the variable wiring of a demo, not the demo's machinery. Before this change it was thick. Each example carried a hand-rolled `start.ts` boot bootstrap, an infra preamble (`timer`, and — for the stdio demos — `logger` + `hmr`), nested includes of three shared YAML fragments (`base.yml` / `base-core.yml` / `acp-agent/acp-tail.yml`), and per-example `agent-loop`/persistence/system-prompt config. The actual app — the spine of services every agent needs — was spread across the leaf and those includes.
@@ -1,9 +1,9 @@
# RFC将示例应用提取为独立包
[English](2026-06-20-extract-example-app-packages.md) | 中文
# RFC: 将示例应用提取为独立包
Status: implemented
[English](2026-06-20-extract-example-app-packages.md) | 中文
## 问题
示例目录本应是*精简的*——只包含演示的可变接线,而非演示的基础设施。在此次变更之前,它是臃肿的。每个示例都携带一份手写的 `start.ts` 启动引导、一段基础设施前导(`timer`,以及 stdio 演示所需的 `logger` + `hmr`(热模块替换))、三个共享 YAML 片段的嵌套引用(`base.yml` / `base-core.yml` / `acp-agent/acp-tail.yml`),还有各示例自身的 `agent-loop`/persistence/system-prompt 配置。真正的应用——每个 agent(智能体)都需要的服务主干——散落在叶子配置和那些 include 中。
@@ -2,5 +2,5 @@
# 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-package-hierarchy.md: faf5815222b20699a32f1af489e625ba3e891230
2026-06-20-package-hierarchy.zh.md: 118367f2655bffbd270f259df4ade37a70dcb2d7
2026-06-20-package-hierarchy.md: a06e963d118d895cc55a0f5e378bbf12678f3491
2026-06-20-package-hierarchy.zh.md: a136f15a4197645b06fa0a68564a9e0464af1cba
@@ -1,9 +1,9 @@
# RFC: Reorganize packages into a modular hierarchy
English | [中文](2026-06-20-package-hierarchy.zh.md)
Status: implemented
English | [中文](2026-06-20-package-hierarchy.zh.md)
## Problem
`packages/` was flat: 18 packages all sat at `packages/<name>/`, so a package's location said nothing about whether it was core product API, a swappable capability seam, a provider adapter, a product integration, or example/test support. The package README carried a `FIXME(package-hierarchy)` and `scripts/publint-all.ts` a `TODO(package-inventory)` flagging exactly this. Core packages, provider integrations, capability seams, example UI support, and snapshot-only replay support all looked equally foundational.
@@ -1,9 +1,9 @@
# RFC将包重组为模块化层级结构
[English](2026-06-20-package-hierarchy.md) | 中文
# RFC: 将包重组为模块化层级结构
Status: implemented
[English](2026-06-20-package-hierarchy.md) | 中文
## 问题
`packages/` 原先是扁平的:18 个包(package)全部位于 `packages/<name>/`,从路径上完全看不出一个包属于核心产品 API、可替换的能力 seam、提供方适配器、产品集成,还是示例/测试支撑。包的 README 带着 `FIXME(package-hierarchy)``scripts/publint-all.ts` 带着 `TODO(package-inventory)`,标记的正是这个问题。核心包、提供方集成、能力 seam、示例 UI 支撑和仅用于快照的回放支撑看起来同样基础。
@@ -2,5 +2,5 @@
# 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-21-mandatory-app-attribution-headers.md: 4fa773e089b3a4b682e42269a66d85aeaf5c18f6
2026-06-21-mandatory-app-attribution-headers.zh.md: 42cc396b5719adb2a2d71e0e9cf0d3554c5533a5
2026-06-21-mandatory-app-attribution-headers.md: 132c827b48693af85fde67cf9d2d71cc84e853c6
2026-06-21-mandatory-app-attribution-headers.zh.md: 9bba6d912b0b9bb33174c1ebb51a84dfdb47af0f
@@ -1,9 +1,9 @@
# RFC: Mandatory `User-Agent` attribution for provider requests
English | [中文](2026-06-21-mandatory-app-attribution-headers.zh.md)
Status: implemented
English | [中文](2026-06-21-mandatory-app-attribution-headers.zh.md)
## Problem
LLM provider requests should identify the product making them. That is useful for provider-side support, abuse investigation, compatibility debugging, and traffic analytics. Before this RFC the harness only partially did this: the hand-rolled DeepSeek adapter sent a hand-copied `User-Agent` constant (`packages/llm/llm-deepseek/src/adapter.ts`), while the pi-ai-backed twin sent no harness-owned headers at all (`packages/llm/llm-pi-ai/src/adapter.ts`). New adapters could therefore omit attribution silently, and a library-backed adapter could drift from the hand-rolled adapter even though [the twin-adapter RFC](2026-06-13-twin-llm-adapters.md) exists to keep the provider seam honest across both implementations.
@@ -1,9 +1,9 @@
# RFC对提供方请求强制携带 `User-Agent` 归属标识
[English](2026-06-21-mandatory-app-attribution-headers.md) | 中文
# RFC: 对提供方请求强制携带 `User-Agent` 归属标识
Status: implemented
[English](2026-06-21-mandatory-app-attribution-headers.md) | 中文
## 问题
LLM(大语言模型)提供方请求应当标识发出请求的产品。这对提供方侧的技术支持、滥用调查、兼容性调试和流量分析都有价值。在本 RFC 之前,harness 只做了部分工作:手写的 DeepSeek 适配器发送了一个手动复制的 `User-Agent` 常量(`packages/llm/llm-deepseek/src/adapter.ts`),而基于 pi-ai 的孪生适配器则完全不发送 harness 自有的头部(`packages/llm/llm-pi-ai/src/adapter.ts`)。因此新适配器可以悄无声息地省略归属标识,而基于库的适配器也可能与手写适配器产生偏差——尽管[孪生适配器 RFC](2026-06-13-twin-llm-adapters.md) 的存在正是为了让两种实现在提供方 seam 上保持诚实。
@@ -15,9 +15,9 @@ LLM(大语言模型)提供方请求应当标识发出请求的产品。这
- **OpenRouter 的机制是提供方特有的。** 其当前文档说明应用归属通过 `HTTP-Referer`(必需)、`X-OpenRouter-Title``X-OpenRouter-Categories` 来追踪;`X-Title` 仅为向后兼容而接受。其 API 参考称这些头部为可选,并说它们使应用在 OpenRouter 上可被发现。这是一份具体的 OpenRouter 契约,而非 IETF 或 OpenAI 兼容 API 标准。
- **在 agent 工具生态中,`HTTP-Referer` 是一种 OpenRouter 感知的约定,而非通用 agent 约定。** 它足够常见,以至于 OpenRouter SDK 和示例直接暴露它,面向 OpenRouter 的框架通常需要一种方式来透传它。但 ACPAgent Client Protocol)等 agent 协议在自己的 initialize 消息中协商名称、版本和能力,而模型提供方请求仍需 HTTP 层面的身份标识。因此「在 agent 世界中被接受」意味着「被 OpenRouter 集成所识别」,而非「可跨 agent 运行时或提供方移植」。
- **编程 agent 在 `User-Agent` 中标识产品和版本。** 公开实现在环境细节和提供方特有的附加头部上各有不同,但产品身份是共同契约;不存在通用的精确格式。
- **标准化的通用客户端身份头部是 `User-Agent`。** RFC 9110 第 10.1.5 节将 `User-Agent` 定义为用户代理软件身份,说明它用于互操作性报告和分析,并说用户代理*应当*在每个请求中发送它(除非被配置为不发送)。这是唯一直接对应「哪个产品在发出此 HTTP 请求」的标准头部。
- **标准化的通用客户端身份头部是 `User-Agent`。** RFC 9110 第 10.1.5 节将 `User-Agent` 定义为用户代理软件身份,说明它用于互操作性报告和分析,并说用户代理应当在每个请求中发送它(除非被配置为不发送)。这是唯一直接对应「哪个产品在发出此 HTTP 请求」的标准头部。
- **`Referer` 是标准的,但 OpenRouter 的 `HTTP-Referer` 不是标准字段。** RFC 9110 第 10.1.3 节将 `Referer` 定义为获取目标 URI 的来源 URI,并用大量篇幅讨论隐私限制。OpenRouter 则要求 `HTTP-Referer`,将其用作应用 URL 标识符。该名称和含义是 OpenRouter 特有的,尽管它形似标准 `Referer` 头部的 CGI 环境变量形式。
- **`From` 是标准的,但不适合作为强制默认值。** RFC 9110 第 10.1.2 节将 `From` 定义为负责用户代理的人的电子邮件地址。机器人代理*应当*发送它以便服务器联系运营者,但非机器人代理出于隐私和安全策略考虑不应在未经用户显式配置的情况下发送。harness 可以后续支持运营者联系方式,但不得凭空捏造或全局强制要求。
- **`From` 是标准的,但不适合作为强制默认值。** RFC 9110 第 10.1.2 节将 `From` 定义为负责用户代理的人的电子邮件地址。机器人代理应当发送它以便服务器联系运营者,但非机器人代理出于隐私和安全策略考虑不应在未经用户显式配置的情况下发送。harness 可以后续支持运营者联系方式,但不得凭空捏造或全局强制要求。
- **请求体中的 `user``metadata` 字段不是应用归属。** 部分模型 API 暴露稳定的终端用户标识符、请求元数据、标签或项目/账户头部。这些对滥用监控、内部计费、仪表盘或链路追踪有用,但它们要么标识的是终端用户而非产品,要么是提供方特有的 body schema,要么不保证能通过 OpenAI 兼容网关透传。它们不能替代静态的应用身份头部。
- **SDK 遥测头部标识的是 SDK,而非应用。** 官方和第三方 SDK 常发送库/版本头部。这些帮助 SDK 维护者调试其客户端,但除非应用显式提供产品归属层,否则它们不能标识 harness 作为应用。
- **pi-ai 有一流的头部钩子。** `@earendil-works/pi-ai``StreamOptions.headers` 将调用方头部最后合并(覆盖提供方默认值),因此基于库的适配器无需包装或上游改动即可满足与手写适配器相同的协议格式契约。mock 服务器测试套件对两个适配器都断言头部到达了线路。
@@ -2,5 +2,5 @@
# 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-24-web-capability-seam.md: 3d0cdd89bb8366749ee0b3959244db1c57c0ccaa
2026-06-24-web-capability-seam.zh.md: 1cb1169f99b6eed22bcca650e0b3fe184f331307
2026-06-24-web-capability-seam.md: fdb611be9efbf29717e41ffa2db86c55258ebe2e
2026-06-24-web-capability-seam.zh.md: f5db538f0565c4fc5667dcbec0791579246d238d
@@ -1,9 +1,9 @@
# RFC: Web capability seam - stable tools over multiple providers
English | [中文](2026-06-24-web-capability-seam.zh.md)
Status: implemented
English | [中文](2026-06-24-web-capability-seam.zh.md)
## Problem
The harness needs model-facing web tools without binding the model contract to one vendor's API shape. Search is the immediate pressure point: supporting both Exa search and Perplexity search from the start — two deliberately different provider shapes (Exa returns a flat `results[]` of `{title, url, highlights, publishedDate}`; Perplexity returns a generated answer plus citations) — is what proves the normalized seam does not just mirror one vendor. Fetch is a separate capability: an anonymous public HTTP(S) fetch backend has transport, security, redirect, decoding, and size-limit concerns that are not the same as provider-backed search.
@@ -1,4 +1,4 @@
# RFCWeb 能力 seam——稳定的工具覆盖多个提供方
# RFC: Web 能力 seam——稳定的工具覆盖多个提供方
Status: implemented
@@ -2,5 +2,5 @@
# 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-file-context-as-event-gate.md: 6e78e2df5f7969b5ed9b74c0b597e2fcacbe8e82
2026-06-26-file-context-as-event-gate.zh.md: d69ccdbcea4b14dbd0291cf69af0bf7d5f3fadfc
2026-06-26-file-context-as-event-gate.md: ac9a8bcbc5bfda2b35cae4993497273608abf35b
2026-06-26-file-context-as-event-gate.zh.md: eda637f9adf8346779c70d59575863bf7e1162c9
@@ -1,9 +1,9 @@
# RFC: Make `dsh-fs-policy` an event-gate plugin, not a method interface
English | [中文](2026-06-26-file-context-as-event-gate.zh.md)
Status: implemented
English | [中文](2026-06-26-file-context-as-event-gate.zh.md)
## Problem
[The split-fs-seam RFC](../simplification/2026-06-26-fsspec-style-fs-seam.md) put `ctx.fileContext` between the model-facing tools and the `ctx.fs` provider: `dsh-tool-fs` injects `fileContext` and routes every `read`/`write`/`edit` through its methods. That makes `fileContext` **in-path and mandatory**. The tool cannot reach `ctx.fs` without it, the policy layer owns the fs I/O and the read windowing, and a deployment that does not want observed-state policy cannot simply drop the package — `dsh-tool-fs` would fail to resolve `ctx.fileContext`.
@@ -1,4 +1,4 @@
# RFC将 `dsh-fs-policy` 改为事件门控插件,而非方法接口
# RFC: 将 `dsh-fs-policy` 改为事件门控插件,而非方法接口
Status: implemented
@@ -72,7 +72,7 @@ editText(target: FsTarget, edit: FsEditRequest, expected?: { version: FsVersion
**两个 `fs/*` 决策事件是单槽、先到先得的 waterfall。** `dsh-fs-policy` 不调用 `next()` 直接返回,因此在默认部署中它占据该槽位;更早注册或使用 `prepend` 的监听器会替代该策略。权限、审计和沙箱关注点仍留在可组合的 `tools/execute` waterfall 上。
actor 在 `dsh-fs` 中类型为 `object`——一个纯粹的不透明载体,提供方 seam 从不读取或收窄它。owner 的推导(`actor.agent?.session`)和 `{ agent?: { session? } }` 结构形状完全留在 `dsh-fs-policy` 内部,由其在监听器中将 `object` actor 收窄为该形状。`dsh-fs` 拥有事件名和 fs 词汇;它**不**拥有策略层的运行时 owner 结构。
actor 在 `dsh-fs` 中类型为 `object`——一个纯粹的不透明载体,提供方 seam 从不读取或收窄它。owner 的推导(`actor.agent?.session`)和 `{ agent?: { session? } }` 结构形状完全留在 `dsh-fs-policy` 内部,由其在监听器中将 `object` actor 收窄为该形状。`dsh-fs` 拥有事件名和 fs 词汇;它拥有策略层的运行时 owner 结构。
```ts
import type { FsTarget, FsVersion, FsWriteIntent } from '@deepseek-ai/dsh-fs'
@@ -154,7 +154,7 @@ interface Events {
## 验证
测试固定了两条路径:无 `dsh-fs-policy` 时,根工具插件对 `dsh-fs-local` 启动,read、create、overwrite 和未读 edit 均成功;有策略时,未读 edit 返回 `FS_NOT_OBSERVED`,未读 overwrite 被 `createIfAbsent` 门控。策略决定后,后注册的 intent 监听器不会被触达。陈旧编辑通过提供方 CAS 失败,而策略不执行 `stat`;工具预算在两条路径上保持 read 一次 `stat`write 或 edit 零次 `stat`。面向模型的 schema 逐字节不变,因此快照不变。
测试固定了两条路径:无 `dsh-fs-policy` 时,根工具插件对 `dsh-fs-local` 启动,read、create、overwrite 和未读 edit 均成功;有策略时,未读 edit 返回 `FS_NOT_OBSERVED`,未读 overwrite 被 `createIfAbsent` 门控。策略决定后,后注册的 intent 监听器不会被触达。陈旧编辑通过提供方 CAS 失败,而策略不执行 `stat`;工具预算在两条路径上保持 read 一次 `stat`write 或 edit 均为零次。面向模型的 schema 逐字节不变,因此快照不变。
## 曾考虑的替代方案
@@ -2,5 +2,5 @@
# 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-30-bash-stdin-env-trusted-plugin-surface.md: 72aae03361cbc088cf64f3548a43ac6253eb21eb
2026-06-30-bash-stdin-env-trusted-plugin-surface.zh.md: f661199048b7eaa359f792e96ac52baf8cd61fdf
2026-06-30-bash-stdin-env-trusted-plugin-surface.md: 712c17532bc1e6b95479aec5b544bc7e296952ec
2026-06-30-bash-stdin-env-trusted-plugin-surface.zh.md: db50f08dae30aa0c43e741a4f27f948f712954e0
@@ -1,9 +1,9 @@
# RFC: stdin + extra env on the bash seam
English | [中文](2026-06-30-bash-stdin-env-trusted-plugin-surface.zh.md)
Status: implemented
English | [中文](2026-06-30-bash-stdin-env-trusted-plugin-surface.zh.md)
## Problem
The hooks subsystem runs external hook commands the way Claude Code and Codex do: a hook is a shell command that receives its event payload as **JSON on stdin** and reads context from a handful of **environment variables** (`CLAUDE_PROJECT_DIR`, `CLAUDE_PLUGIN_ROOT`, `PLUGIN_ROOT`, …). The harness already has a perfectly good command runner behind the `ctx.bash` capability seam ([dsh-bash](../../../../packages/bash/bash) → [dsh-bash-local](../../../../packages/bash/bash-local)), with process-group kills, output truncation/spill, and a credential scrub. Reusing it for hook execution means a hook bridge does not re-implement subprocess plumbing — but the seam had no way to write stdin or set extra env. This RFC adds those two inputs.
@@ -1,4 +1,4 @@
# RFC在 bash seam 上支持 stdin 与额外 env
# RFC: 在 bash seam 上支持 stdin 与额外 env
Status: implemented
@@ -2,5 +2,5 @@
# 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-30-event-domain-semantics.md: e05c238c52052454d3e01e82767cddd9af316a9d
2026-06-30-event-domain-semantics.zh.md: 9048679ec7a7992852cce76bf43269f5499da792
2026-06-30-event-domain-semantics.md: a9d33a7bfb549e1ec2102c23af78f91d9ae4b904
2026-06-30-event-domain-semantics.zh.md: dc7390e3d2e1cda3469fa77389d0d6ad39b7a713
@@ -1,9 +1,9 @@
# RFC: Event-domain semantics — session is the fact log, agent is the live surface
English | [中文](2026-06-30-event-domain-semantics.zh.md)
Status: implemented
English | [中文](2026-06-30-event-domain-semantics.zh.md)
## Problem
The harness extends the agent loop through a Cordis event taxonomy (see [the microkernel event-taxonomy RFC](2026-06-11-microkernel-event-taxonomy.md)). As that taxonomy grew, the line between the three event domains blurred:
@@ -1,4 +1,4 @@
# RFC事件域语义——session 是事实日志,agent 是运行时表面
# RFC: 事件域语义——session 是事实日志,agent 是运行时表面
Status: implemented
@@ -12,7 +12,7 @@ harness 通过 Cordis 事件分类体系扩展 agent loop(智能体循环)
- `agent/*` 承载运行时实时信号,向插件传递 `Agent` 句柄。
- `tools/*` 承载工具注册表与执行 seam。
两个问题促使我们固定语义。第一,若干轮次/步骤边界同时作为持久的 `SessionEvent``turn/start``turn/end``step/start``step/end`**和**镜像的 `agent/*` emit`agent/turn-start``agent/turn-end``agent/step-start``agent/step-end`)存在。消费方对同一事实有两个真源,每次生命周期变更都必须同时更新两处。第二,即将到来的 Hooks 子系统需要**一个**连贯且有文档的订阅表面——插件作者(以及基于其上构建的 Claude Code / Codex 钩子桥接)必须在不阅读循环代码的情况下知道应该监听 session 事件还是 agent 事件,以及原因。
两个问题促使我们固定语义。第一,若干轮次/步骤边界同时作为持久的 `SessionEvent``turn/start``turn/end``step/start``step/end`镜像的 `agent/*` emit`agent/turn-start``agent/turn-end``agent/step-start``agent/step-end`)存在。消费方对同一事实有两个真源,每次生命周期变更都必须同时更新两处。第二,即将到来的 Hooks 子系统需要一个连贯且有文档的订阅表面——插件作者(以及基于其上构建的 Claude Code / Codex 钩子桥接)必须在不阅读循环代码的情况下知道应该监听 session 事件还是 agent 事件,以及原因。
这套词汇是拦截决策、持久的 `hook/*` 日志,以及 Claude Code 和 Codex 桥接的基础。
@@ -21,7 +21,7 @@ harness 通过 Cordis 事件分类体系扩展 agent loop(智能体循环)
**三个域,各司其职,以一条边界规则统一。**
- **`session/*`——持久的、可回放的事实日志。** 拥有 `SessionEventMap`;每条记录仅含 JSON(无活对象)。每次追加触发一次 `session/event` emit,加上 `session/flush` 并行持久性检查点。它同时也是实时 transcript(文本记录)源:想渲染或响应已发生事件的消费方在此订阅,因此实时渲染与 `session/load` 回放共享同一路径。
- **`agent/*`——运行时实时表面。** 始终携带活的 `Agent`。两种形态:拦截 waterfall(瀑布式事件)(`agent/request``agent/step-result``agent/turn-continuation`)可变更或否决;瞬态 emit`agent/status``agent/error``agent/created`/`agent/disposed``agent/queued`)在持有 `Agent` 的情况下通知。轮次和步骤**边界**不在此处——它们是持久的 session 事件,从 `session/event` 读取;token 流(`assistant/chunk`)和中途 steering(中途引导)(`steering/message`)同理。
- **`agent/*`——运行时实时表面。** 始终携带活的 `Agent`。两种形态:拦截 waterfall(瀑布式事件)(`agent/request``agent/step-result``agent/turn-continuation`)可变更或否决;瞬态 emit`agent/status``agent/error``agent/created`/`agent/disposed``agent/queued`)在持有 `Agent` 的情况下通知。轮次和步骤边界不在此处——它们是持久的 session 事件,从 `session/event` 读取;token 流(`assistant/chunk`)和中途 steering(中途引导)(`steering/message`)同理。
- **`tools/*`——工具注册表与执行 seam。**
**边界规则:** 持久的、可回放的事实是 `SessionEvent`;实时拦截或瞬态/活对象信号是 `agent`/`tools` Cordis 事件。轮次或步骤边界是持久事实,因此存在于 session 日志中并从 `session/event` 源读取——不会被镜像为 `agent/*` emit。
@@ -31,7 +31,7 @@ harness 通过 Cordis 事件分类体系扩展 agent loop(智能体循环)
## 后果
- 循环不再 emit 任何边界镜像;`closeStep` 仅追加 `step/end``closeTurn` 仅追加 `turn/end``Session.append` 负责 post-commit observer 隔离,因此抛出异常的边界 observer 无法改变轮次结果或饿死后续消费方;接受或内部校验失败仍会在边界进入日志之前逃逸。
- 之前通过已移除 emit 观察边界的测试,现在观察持久的 `turn/start`/`turn/end`/`step/start`/`step/end` session 事件——它们固定的行为(边界顺序、步骤计数)不变;只是读取的源移到了规范源。那些测试抛出异常的 turn 边界 emit 监听器的用例被删除,因为该代码路径不再存在(没有 emit 可供抛出)。按照 [AGENTS.md「测试记录行为,而非黄金真相」](../../../../AGENTS.md),行为与其测试一同迁移(或一同消亡)。
- 之前通过已移除 emit 观察边界的测试,现在观察持久的 `turn/start`/`turn/end`/`step/start`/`step/end` session 事件——它们固定的行为(边界顺序、步骤计数)不变;只是读取的源移到了规范源。那些测试*抛出异常的 turn 边界 emit 监听器*的用例被删除,因为该代码路径不再存在(没有 emit 可供抛出)。按照 [AGENTS.md「测试记录行为,而非黄金真相」](../../../../AGENTS.md),行为与其测试一同迁移(或一同消亡)。
- 循环仅在 `append('step/start')` 返回后才标记步骤已打开(`stepOpen = true`)。内部分发校验在日志推入之前运行,可能在不打开步骤的情况下拒绝;post-commit `session/event` observer 的失败被隔离在 `Session.append` 内部。因此该标记精确表示已提交的、欠一个后续 `step/end` 的边界。
- 完整实现见[简化 RFC「停止将持久边界镜像为 agent 事件」](../simplification/2026-06-20-remove-agent-boundary-mirror-events.md):全部四个边界镜像被移除,所有消费方从 `session/event` 读取边界。`agent/steering`(不是边界镜像)不在该 RFC 范围内,由其后续 RFC [移除 `agent/steering` 镜像 emit](../simplification/2026-07-04-remove-agent-steering-mirror.md) 单独移除——它镜像的是持久的 `steering/message`
- Cordis 事件目录(`docs/cordis-catalog/events.md`)重新生成以移除镜像事件。
@@ -2,5 +2,5 @@
# 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-fs-per-session-cwd.md: 00643955d918dff87241f4b240bb7e6774d21a0b
2026-07-02-fs-per-session-cwd.zh.md: 73176cde3747a2eb8c03aadbf3f419bf27173d70
2026-07-02-fs-per-session-cwd.md: 2513294d6bc991ee39155bbf20869829cca1a188
2026-07-02-fs-per-session-cwd.zh.md: 2c3957113642fe2bf7b3a4f2b6186246b32aff3f
@@ -1,9 +1,9 @@
# RFC: Resolve filesystem paths against the caller's session cwd
English | [中文](2026-07-02-fs-per-session-cwd.zh.md)
Status: implemented
English | [中文](2026-07-02-fs-per-session-cwd.zh.md)
## Problem
The ACP bridge gives every session its own workspace: `session/new` records the editor's project directory as `SessionHeader.cwd`, and `dsh-tool-bash` defaults each bash call's `workdir` to the calling agent's `session.header.cwd` (see [the per-session cwd RFC work in `packages/ui/acp`](../../../../packages/ui/acp) and `resolveWorkdir` in `dsh-tool-bash`). So a bash command in session A runs in A's project, and in session B runs in B's — one server process, N workspaces.
@@ -1,9 +1,9 @@
# RFC相对文件系统路径按调用方的会话 cwd 解析
[English](2026-07-02-fs-per-session-cwd.md) | 中文
# RFC: 相对文件系统路径按调用方的会话 cwd 解析
Status: implemented
[English](2026-07-02-fs-per-session-cwd.md) | 中文
## 问题
ACPAgent Client Protocol)桥接层为每个会话提供独立的工作区:`session/new` 将编辑器的项目目录记录为 `SessionHeader.cwd``dsh-tool-bash` 将每次 bash 调用的 `workdir` 默认设为调用方 agent(智能体)的 `session.header.cwd`(见 [`packages/ui/acp`](../../../../packages/ui/acp) 中的 per-session cwd RFC 工作与 `dsh-tool-bash` 中的 `resolveWorkdir`)。因此会话 A 中的 bash 命令在 A 的项目目录执行,会话 B 中的在 B 的项目目录执行——一个服务器进程,N 个工作区。
@@ -24,7 +24,7 @@ ACPAgent Client Protocol)桥接层为每个会话提供独立的工作区
提供方 seam 不得依赖 `dsh-agent``dsh-session`——它是一个文本存储后端,沙箱或远程实现同样满足该接口,而这些实现没有「agent 会话」的概念。工具已经接收了 `ToolExecution``exec`),其中携带 agent,因此工具是将 `exec → cwd` 投影并向提供方传递一个纯字符串的正确位置。这遵循「包(package)边界处显式优于隐式」的约定:基准目录作为显式参数传入,提供方据此行动,而非让提供方越界去读取它不应知晓的会话。这也与 `dsh-tool-bash` 一一对应,使两个面向模型的文件操作接口以相同方式解析路径。
默认值只存在于**一个**地方——提供方的 `config.cwd``sessionCwd` 在没有会话时返回 `undefined` 而非 `process.cwd()`,因此工具永远不会自行制造一个提供方本应自行选择的基准目录。
默认值只存在于一个地方——提供方的 `config.cwd``sessionCwd` 在没有会话时返回 `undefined` 而非 `process.cwd()`,因此工具永远不会自行制造一个提供方本应自行选择的基准目录。
## 后果
@@ -2,5 +2,5 @@
# 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-result-time-applied-hunk-diffs.md: 81ab8b9827ddaec39d63e2a3f8fbb864085a9ac9
2026-07-02-result-time-applied-hunk-diffs.zh.md: 2914c4242c4246ede8588967ed36b3f6c725c607
2026-07-02-result-time-applied-hunk-diffs.md: fd02951f21319be2933bf3ffaddeb96bc2a6819c
2026-07-02-result-time-applied-hunk-diffs.zh.md: 4ed9a29bb023343cab28b8114453d17408bc429a
@@ -1,9 +1,9 @@
# RFC: Result-time applied-hunk diffs for file mutations
English | [中文](2026-07-02-result-time-applied-hunk-diffs.zh.md)
Status: implemented
English | [中文](2026-07-02-result-time-applied-hunk-diffs.zh.md)
## Problem
The [tagged render-intent union](2026-07-02-tool-render-intent-union.md) gave `dsh-tool-fs` write/edit a `card:'diff'` at CALL time, derived purely from the tool's args: write ⇒ `{oldText:null, newText:content}` (the whole new file), edit ⇒ `{oldText:old_string, newText:new_string}` (the bare replaced snippet). An editor renders that as an inline diff, but it is a **context-free** diff — the bare `old_string``new_string` with no surrounding lines, and a `replace_all` that touched five scattered sites still renders as one snippet pair.
@@ -1,14 +1,14 @@
# RFC结果时刻的 applied-hunk diff 用于文件变更
[English](2026-07-02-result-time-applied-hunk-diffs.md) | 中文
# RFC: 结果时刻的 applied-hunk diff 用于文件变更
Status: implemented
[English](2026-07-02-result-time-applied-hunk-diffs.md) | 中文
## 问题
[tagged render-intent union](2026-07-02-tool-render-intent-union.md) 为 `dsh-tool-fs` 的 write/edit 在调用时刻提供了 `card:'diff'`,纯粹从工具参数推导:write ⇒ `{oldText:null, newText:content}`(整个新文件),edit ⇒ `{oldText:old_string, newText:new_string}`(裸替换片段)。编辑器将其渲染为行内 diff,但这是一个**无上下文**的 diff:裸的 `old_string``new_string` 没有周围行,而一次触及五个分散位置的 `replace_all` 仍然渲染为一对片段。
在对接 `claude-agent-acp` 自身的 ACPAgent Client Protocol bridge 时可以看到完整编辑器 diff 的样子:变更应用后,它发出第二个 `tool_call_update`,其 diff 是**带 ±3 行上下文的 applied hunk**`replace_all` 的每个变更位置各一个 hunk),由工具的 `structuredPatch` 重建。这个结果时刻的 hunk 正是让 Zed 在文件中**原位**显示变更(而非浮动片段)的关键。我们的工具止步于调用时刻的片段;完成后的结果只携带纯文本 "updated successfully",没有 diff。
在对接 `claude-agent-acp` 自身的 ACPAgent Client Protocol bridge 时可以看到完整编辑器 diff 的样子:变更应用后,它发出第二个 `tool_call_update`,其 diff 是**带 ±3 行上下文的 applied hunk**`replace_all` 的每个变更位置各一个 hunk),由工具的 `structuredPatch` 重建。这个结果时刻的 hunk 正是让 Zed 在文件中*原位*显示变更(而非浮动片段)的关键。我们的工具止步于调用时刻的片段;完成后的结果只携带纯文本 "updated successfully",没有 diff。
障碍在于一个 seam 边界:`presentResult(args, result)`**`args` + 面向模型的 `result``{content, isError}`)的纯函数**——它在实时流式输出和会话日志回放中都会运行,因此必须具备回放确定性且不能做 I/O。它看不到文件的前后内容,而 `FsEditOutcome`/`FsWriteOutcome` 只携带替换计数和版本号,没有文本。因此无法计算——甚至无法携带——applied hunk 给 presenter。
@@ -37,7 +37,7 @@ type ToolExecuteReturn = ContentBlock[] | { content: ContentBlock[]; meta?: unkn
### 3. Bridge 渲染 `diff` 结果卡片
`ToolResultView` 新增 `DiffResultView { card:'diff'; title?; diffs: FileDiff[] }`bridge 结果侧的 `switch (view.card)` 增加 `diff` 分支,发出 `{type:'diff'}``ToolCallContent` 块(与调用侧分支对称)。ACP 的 `tool_call_update.content` 在编辑器中**替换**调用时的内容,因此结果 diff **取代**调用时刻的片段(并防止面向模型的结果文本覆盖它)——两次更新序列(先调用片段,再结果 diff)与 `claude-agent-acp` 完全一致。
`ToolResultView` 新增 `DiffResultView { card:'diff'; title?; diffs: FileDiff[] }`bridge 结果侧的 `switch (view.card)` 增加 `diff` 分支,发出 `{type:'diff'}``ToolCallContent` 块(与调用侧分支对称)。ACP 的 `tool_call_update.content` 在编辑器中替换调用时的内容,因此结果 diff **取代**调用时刻的片段(并防止面向模型的结果文本覆盖它)——两次更新序列(先调用片段,再结果 diff)与 `claude-agent-acp` 完全一致。
## 曾考虑的替代方案
@@ -2,5 +2,5 @@
# 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-tool-render-intent-union.md: 8256f09f9c297658627d0c3d9e99ee1c5424b254
2026-07-02-tool-render-intent-union.zh.md: 35bd775545c9131a20da9e7f7424506e4554eb4b
2026-07-02-tool-render-intent-union.md: 6b828c32fb79f43c0f974f2c9f032e11430de5b4
2026-07-02-tool-render-intent-union.zh.md: 8c44178ca3e8986579e7ed696aec2f900705e9ad
@@ -1,9 +1,9 @@
# RFC: Tagged render-intent union for tool-call presentation
English | [中文](2026-07-02-tool-render-intent-union.zh.md)
Status: implemented
English | [中文](2026-07-02-tool-render-intent-union.zh.md)
## Problem
A tool declares how its calls render in a UI (an editor's tool-call card) through two callbacks, `presentCall`/`presentResult` on `ToolDefinition`, returning `ToolCallPresentation` / `ToolResultPresentation` with an optional `ToolTerminal` sub-shape. These grew incrementally into a **bag of optional fields**: `title`, `kind`, `rawInput`, `content`, `locations`, `terminal` on the call; `title`, `content`, `terminal` on the result; `cwd`/`output`/`exitCode`/`signal` on `ToolTerminal`. The split of responsibility is muddy:
@@ -1,4 +1,4 @@
# RFC用于工具调用展示的带标签 render-intent 联合类型
# RFC: 用于工具调用展示的带标签 render-intent 联合类型
Status: implemented
@@ -2,5 +2,5 @@
# 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-03-filesystem-directory-listing-seam.md: bb8d9c4deda18b320b85d548bbd5bcb32f1c1d72
2026-07-03-filesystem-directory-listing-seam.zh.md: ccc5ca67f58537134da5c5484b3d527ba84fe8d3
2026-07-03-filesystem-directory-listing-seam.md: 8920e54993eb1402bd310d6ce78fb9866de628c9
2026-07-03-filesystem-directory-listing-seam.zh.md: 5ca9b195d4bf5c1450c60451dd469a19dd54166f
@@ -1,9 +1,9 @@
# RFC: Add direct directory listing to the filesystem seam
English | [中文](2026-07-03-filesystem-directory-listing-seam.zh.md)
Status: implemented
English | [中文](2026-07-03-filesystem-directory-listing-seam.zh.md)
## Problem
`@deepseek-ai/dsh-fs` is the provider seam for filesystem access, with local and future non-local backends behind the same `ctx.fs` contract. Before this change it could resolve paths, stat targets, read text, stream text, write text, and edit text. That was enough for model-facing file tools, but not for non-model-facing consumers that need to enumerate directories without importing `node:fs`.
@@ -1,9 +1,9 @@
# RFC为文件系统 seam 添加直接目录列举能力
[English](2026-07-03-filesystem-directory-listing-seam.md) | 中文
# RFC: 为文件系统 seam 添加直接目录列举能力
Status: implemented
[English](2026-07-03-filesystem-directory-listing-seam.md) | 中文
## 问题
`@deepseek-ai/dsh-fs` 是文件系统访问的提供方 seam,本地后端与未来的非本地后端共享同一个 `ctx.fs` 契约。在本次变更之前,它能解析路径、stat 目标、读取文本、流式读取文本、写入文本和编辑文本。这对面向模型的文件工具已经足够,但对于需要枚举目录而又不想直接导入 `node:fs` 的非模型侧消费方来说还不够。
@@ -2,5 +2,5 @@
# 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-05-prompt-variables-and-tool-guidance-ownership.md: fce9d555c8843b99fdbfa7b652b46d0b88053935
2026-07-05-prompt-variables-and-tool-guidance-ownership.zh.md: 93a639a2ddac33cb1ceac57101cb6185fe6034ad
2026-07-05-prompt-variables-and-tool-guidance-ownership.md: 1819a730b214270304a8feb103a7f26e9c8d6e6e
2026-07-05-prompt-variables-and-tool-guidance-ownership.zh.md: 4211241b1b24fd461f5341bf8d777007a7a36482
@@ -1,9 +1,9 @@
# RFC: Prompt variables and tool-guidance ownership
English | [中文](2026-07-05-prompt-variables-and-tool-guidance-ownership.zh.md)
Status: implemented
English | [中文](2026-07-05-prompt-variables-and-tool-guidance-ownership.zh.md)
## Problem
The assembled system prompt had four defects, all of one family: facts the harness already knows were restated by hand somewhere else, and drifted.
@@ -1,9 +1,9 @@
# RFCPrompt 变量与工具指导归属
[English](2026-07-05-prompt-variables-and-tool-guidance-ownership.md) | 中文
# RFC: Prompt 变量与工具指导归属
Status: implemented
[English](2026-07-05-prompt-variables-and-tool-guidance-ownership.md) | 中文
## 问题
组装后的系统提示词存在四个缺陷,同属一类:harness 已知的事实在别处被手工重述,然后漂移。
@@ -32,7 +32,7 @@ Status: implemented
### Persona 作为 order-0 section
`dsh-system-prompt` 拥有 order 为 `-100``harness:identity` 和 order 为 `0` 的配置 `deployment:persona`,因此两者在循环被替换时仍然存活。prompt 渲染只有一条路径 `renderPrompt(assembly)``agent/pre-step` 因此测量的正是用于压缩(compaction)的确切 prompt。agent 作用域的 `deployment:persona` 遮蔽全局默认值,允许 subagent provider 在发布前安装 persona。约定的 order 区间为:identity `-100`、persona `0`、工具指导 `100199`
`dsh-system-prompt` 拥有 order 为 `-100``harness:identity` 和 order 为 0 的配置 `deployment:persona`,因此两者在循环被替换时仍然存活。prompt 渲染只有一条路径 `renderPrompt(assembly)``agent/pre-step` 因此测量的正是用于压缩(compaction)的确切 prompt。agent 作用域的 `deployment:persona` 遮蔽全局默认值,允许 subagent provider 在发布前安装 persona。约定的 order 区间为:identity `-100`、persona `0`、工具指导 `100199`
### 工具指导归属
@@ -66,7 +66,7 @@ Status: implemented
## 后果
- 组装后的 prompt 中每个事实现在恰好有一个归属方,leaf YAML 中手工维护的工具行文已消除:加载或卸载一个工具插件不再需要编辑任何部署的 persona。
- `{{model}}` 在组装时反映 `AgentOptions.model`。如果一个插件在 `agent/request` waterfall 中切换模型,prompt 对该步骤的声明就会过时;如果一个插件在那里**提供**模型(options.model 未设置——循环文档中记载的回退路径),变量在渲染时无值,包含 `{{model}}` 的 persona 会在 waterfall 运行前失败。两者的补救方式相同,就是归属规则本身:拥有延迟绑定模型事实的插件在 `system-prompt/assemble` waterfall 上提前声明它(`assembly.variables['model'] = …`)——一个归属方,两处声明;一个循环测试端到端固定了 supply 路径。已接受。
- `{{model}}` 在组装时反映 `AgentOptions.model`。如果一个插件在 `agent/request` waterfall 中切换模型,prompt 对该步骤的声明就会过时;如果一个插件在那里提供模型(options.model 未设置——循环文档中记载的回退路径),变量在渲染时无值,包含 `{{model}}` 的 persona 会在 waterfall 运行前失败。两者的补救方式相同,就是归属规则本身:拥有延迟绑定模型事实的插件在 `system-prompt/assemble` waterfall 上提前声明它(`assembly.variables['model'] = …`)——一个归属方,两处声明;一个循环测试端到端固定了 supply 路径。已接受。
- 当一个已绑定的 provider 不存在时(尚未激活、已卸载、HMR(热模块替换)重载中),subagent 工具不存在,该窗口内的模型请求中不会包含它。这是诚实的状态——替代方案是注册一个 description 或执行都不可信的工具。
- 严格性意味着 persona 可能在渲染时导致轮次失败(例如在无 cwd 的会话上使用 `{{cwd}}`)。失败是受控的——该轮次以 `error` 结束,循环存活——且这是一个我们**希望**大声暴露的撰写错误。
- 严格性意味着 persona 可能在渲染时导致轮次失败(例如在无 cwd 的会话上使用 `{{cwd}}`)。失败是受控的——该轮次以 `error` 结束,循环存活——且这是一个我们希望大声暴露的撰写错误。
- 目前没有在 prompt 行文中转义字面 `{{name}}` 的语法;如果真实 prompt 确实需要,再行添加。
@@ -2,5 +2,5 @@
# 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-05-reconstructable-requests.md: 0978cd8760c6a0420be1bf0a3baf6b50c1a04a13
2026-07-05-reconstructable-requests.zh.md: a4864c9e795ccc0da2cbbf0ca4d17a90c029858c
2026-07-05-reconstructable-requests.md: b07dbceaf6f3ecad64a464df6b97ee2a32284768
2026-07-05-reconstructable-requests.zh.md: cb57d0915547b8a38b6afde7e36bec36c86e19c3
@@ -1,9 +1,9 @@
# RFC: Every LLM request is reconstructable from the session log
English | [中文](2026-07-05-reconstructable-requests.zh.md)
Status: implemented
English | [中文](2026-07-05-reconstructable-requests.zh.md)
## Problem
The request pipeline did not guarantee prefix stability for provider caching, and the session log could not reconstruct what the model saw. It omitted model, system prompt, and tool schemas while allowing per-call request rewrites. Cache behavior and replay equivalence therefore depended on whichever plugins happened to be loaded.
@@ -1,4 +1,4 @@
# RFC每个 LLM(大语言模型)请求都可从会话日志重建
# RFC: 每个 LLM(大语言模型)请求都可从会话日志重建
Status: implemented
@@ -46,7 +46,7 @@ Status: implemented
## 后果
- 一个日志无法解释的请求不可能被意外构造——无论是循环还是监听器;变异已构建的请求会抛异常;每个 header 变更都是持久的、可 diff 的日志事件。
- 在建议性通道之间做选择是变更频率的决策,而本设计使稳定的那个在结构上成为默认:`agent/session-prefix` 的贡献在每个循环实例中只组合一次并逐字复用,因此以零边际成本扩展可缓存前缀,且**不可能**在会话中途击穿提供方缓存;会话中途变化的内容通过仅追加的历史通道流入——`agent.inject()``tools/post-execute` 决策的 `additionalContext`、prompt-submit 的 `additionalContext`——每条都是持久的 `context/message`,付出一次代价后即被前缀缓存,代价是在历史和日志中累积。将会话冻结的开场内容路由到前缀,将变更通知路由到历史通道;逐步骤的仅限请求尾部槽位被有意放弃(无消费方,且持久追加覆盖了当前所有更新模式)。
- 在建议性通道之间做选择是变更频率的决策,而本设计使稳定的那个在结构上成为默认:`agent/session-prefix` 的贡献在每个循环实例中只组合一次并逐字复用,因此以零边际成本扩展可缓存前缀,且不可能在会话中途击穿提供方缓存;会话中途变化的内容通过仅追加的历史通道流入——`agent.inject()``tools/post-execute` 决策的 `additionalContext`、prompt-submit 的 `additionalContext`——每条都是持久的 `context/message`,付出一次代价后即被前缀缓存,代价是在历史和日志中累积。将会话冻结的开场内容路由到前缀,将变更通知路由到历史通道;逐步骤的仅限请求尾部槽位被有意放弃(无消费方,且持久追加覆盖了当前所有更新模式)。
- 在提供方处仍需全价计算的内容是固有的且已记录的:压缩(其 `compact/*` 事件和 replace 节点)、真正的 prompt/工具变更(`request/header-delta`)、配置切换(同上)、带漂移的进程边界(`'resume'` 快照与前一快照不同)。提供方自身的 reasoning-content 排除由服务端管理。
- `step/start` 监听器行为变更(见上文)是对插件唯一可观察的语义变更;`agent/pre-step` 是当前请求的 seam。
- 工具结果裁剪(计划中)无需新机制:一个已记录的单节点 surface replace`start === end`),携带同一 `callId` 下裁剪后的 `tool/result`——属压缩家族,回放正确,缓存击穿由相同的压力逻辑批量处理。
@@ -2,5 +2,5 @@
# 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-05-subagent-provider-lifecycle-events.md: 6d711f2a6d8496a8a229ec63d86dd89816efb6f8
2026-07-05-subagent-provider-lifecycle-events.zh.md: f412b031644c14ca70caae3efc72eefa9ce2c2ac
2026-07-05-subagent-provider-lifecycle-events.md: b5300706ce4c53a6d348a86803d221a4dcca9632
2026-07-05-subagent-provider-lifecycle-events.zh.md: 8694612e5e3d4d65117896e337c170f47b31abb0
@@ -1,9 +1,9 @@
# RFC: Subagent provider-lifecycle events — `subagent/provider-added` / `subagent/provider-removed`
English | [中文](2026-07-05-subagent-provider-lifecycle-events.zh.md)
Status: implemented
English | [中文](2026-07-05-subagent-provider-lifecycle-events.zh.md)
## Problem
[The prompt-variables RFC](2026-07-05-prompt-variables-and-tool-guidance-ownership.md) makes `dsh-tool-subagent` DERIVE its model-facing wording from its provider: `SubagentProvider.inheritsParentContext` (spawn/ACP `false`, fork `true`) drives both the tool description and the `prompt` parameter description (`providerWording`), so the fork tool stops lying about context inheritance. That fix created a cross-fiber data dependency: a tool's description is fixed at TOOL REGISTRATION (deliberately — the description is where tool-choice guidance lives), but the provider arrives on its own plugin fiber, on no particular schedule.
@@ -1,12 +1,12 @@
# RFCSubagent 提供方生命周期事件——`subagent/provider-added` / `subagent/provider-removed`
[English](2026-07-05-subagent-provider-lifecycle-events.md) | 中文
# RFC: Subagent 提供方生命周期事件——`subagent/provider-added` / `subagent/provider-removed`
Status: implemented
[English](2026-07-05-subagent-provider-lifecycle-events.md) | 中文
## 问题
[prompt-variables RFC](2026-07-05-prompt-variables-and-tool-guidance-ownership.md) 让 `dsh-tool-subagent` 从其提供方**派生**面向模型的措辞:`SubagentProvider.inheritsParentContext`spawn/ACP 为 `false`fork 为 `true`)同时驱动工具描述和 `prompt` 参数描述(`providerWording`),使 fork 工具不再在上下文继承问题上对模型撒谎。这一修复引入了跨 fiber 的数据依赖:工具描述在**工具注册时**固定(这是有意为之——描述是 tool-choice 引导所在之处),但提供方在自己的插件 fiber 上到达,时机不确定。
[prompt-variables RFC](2026-07-05-prompt-variables-and-tool-guidance-ownership.md) 让 `dsh-tool-subagent` 从其提供方派生面向模型的措辞:`SubagentProvider.inheritsParentContext`spawn/ACP 为 `false`fork 为 `true`)同时驱动工具描述和 `prompt` 参数描述(`providerWording`),使 fork 工具不再在上下文继承问题上对模型撒谎。这一修复引入了跨 fiber 的数据依赖:工具描述在工具注册时固定(这是有意为之——描述是 tool-choice 引导所在之处),但提供方在自己的插件 fiber 上到达,时机不确定。
如果在工具插件的 `apply` 时刻解析提供方,就会产生一个隐式的加载顺序要求("在 cordis.yml 中把后端列在工具前面")。这个要求不成立,因为 Cordis Loader 并发启动同级条目,且 `Entry.init()` 不会等待激活完成:延迟到达的后端即使列在前面,也可能让工具 fiber 失败。Loader 不提供同级顺序保证——"异步状态不是同步状态"(见[防御性模式](../../../defensive-patterns.md))。
@@ -17,15 +17,15 @@ Status: implemented
- **`subagent/provider-added(provider)`**:一个提供方在 `ctx.subagents` 注册表中变为可解析。在注册时发出。
- **`subagent/provider-removed(name)`**:一个提供方离开注册表(其插件 fiber 被 dispose(资源释放)——卸载或 HMR(热模块替换)重载)。从注册的 disposer 中发出。
`dsh-tool-subagent` 镜像其命名提供方的生命周期:当提供方可用(或变为可用)时注册工具——在那一刻从该提供方派生措辞——当提供方离开时注销工具,并在重新注册时(HMR 重载)重新派生。提供方不在时工具不存在,因此不会对模型撒谎。这里有意**不留下**任何需要文档化的加载顺序要求:事件让顺序问题消失,而非将其钉死。
`dsh-tool-subagent` 镜像其命名提供方的生命周期:当提供方可用(或变为可用)时注册工具——在那一刻从该提供方派生措辞——当提供方离开时注销工具,并在重新注册时(HMR 重载)重新派生。提供方不在时工具不存在,因此不会对模型撒谎。这里有意不留下任何需要文档化的加载顺序要求:事件让顺序问题消失,而非将其钉死。
这些事件还完善了 seam 的词汇:`ctx.subagents` 是一个命名注册表,多个委派后端(`spawn``fork``acp`)在其上共存;一个其他插件从中派生状态的注册表,应当以类型化事件广播成员变化,而非要求轮询或依赖加载顺序。
## 曾考虑的替代方案
- **在 `apply` 时解析提供方,不存在则抛异常**:否决。"先列后端"这一要求声称了 Loader 并不存在的顺序保证。
- **重试查找(轮询直到提供方出现)**:最终能收敛,但在框架已有的机制(effect 注册 + disposal)之外发明了一套私有就绪协议;它也无法感知提供方**离开**,因此 HMR 会遗留一个措辞描述已 dispose 后端的工具。
- **仅在 section 中放置 subagent 措辞,在组装时惰性解析**:同样能容忍任意加载顺序,但将 tool-choice 引导移出了**描述**,与 prompt-variables RFC 建立的所有权规则相矛盾(每个工具的语义和何时使用属于描述)。响应式注册既保持描述的权威性,又不依赖顺序。
- **重试查找(轮询直到提供方出现)**:最终能收敛,但在框架已有的机制(effect 注册 + disposal)之外发明了一套私有就绪协议;它也无法感知提供方离开,因此 HMR 会遗留一个措辞描述已 dispose 后端的工具。
- **仅在 section 中放置 subagent 措辞,在组装时惰性解析**:同样能容忍任意加载顺序,但将 tool-choice 引导移出了描述,与 prompt-variables RFC 建立的所有权规则相矛盾(每个工具的语义和何时使用属于描述)。响应式注册既保持描述的权威性,又不依赖顺序。
- **根据提供方名称而非提供方对象确定措辞**`providerName` 本身是配置,重命名后的提供方会静默获得错误的措辞;从已解析提供方自身的 `inheritsParentContext` 派生则不会漂移。
## 后果
@@ -33,4 +33,4 @@ Status: implemented
- 从命名提供方派生状态的消费方响应 `subagent/provider-added`/`-removed` 事件,而非在 `apply` 时读取注册表;`dsh-tool-subagent` 是参考实现。
- **添加时大声失败;移除时按监听器隔离。** 添加监听器可以回滚注册。移除在 disposal 期间运行,因此单个监听器抛异常只会被记录日志,不会饿死后续镜像或干扰拆解流程。`start()` 仍在每次运行时按名称解析提供方,防止陈旧工具调用已移除的后端。见[事件目录](../../../cordis-catalog/events.md)与[生产者/消费者映射](../../../event-producer-consumer.md)。
- **工具不存在的窗口期。** 在后端 disposal 与重新注册之间(HMR 重载期间),模型看不到 subagent 工具。这是诚实的状态——替代方案是一个向空处分发的工具——工具注册表的 `tools/change` 事件发出会保持 prompt 组装的时效性。
- **两个等待中的 fiber 共享同一 `toolName` 是无效配置,被延迟捕获。** 如果两个 `dsh-tool-subagent` 加载实例命名了不同的提供方但相同的 `toolName`,两者都会等待,先到达的提供方先注册;第二次注册仅在**其**提供方到达时才抛异常。插件中的 `TODO(subagent-dup-toolname)` 记录了这一影响范围;工具注册表的重名拒绝机制仍是最终防线。
- **两个等待中的 fiber 共享同一 `toolName` 是无效配置,被延迟捕获。** 如果两个 `dsh-tool-subagent` 加载实例命名了不同的提供方但相同的 `toolName`,两者都会等待,先到达的提供方先注册;第二次注册仅在提供方到达时才抛异常。插件中的 `TODO(subagent-dup-toolname)` 记录了这一影响范围;工具注册表的重名拒绝机制仍是最终防线。
@@ -2,5 +2,5 @@
# 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-06-timeout-deadline-library.md: 9906aa7cce40ffd7b5f7082199d05edb8d0a54b2
2026-07-06-timeout-deadline-library.zh.md: dd1a60f9d7b91575b32e1cc85b3800cf823b6a6d
2026-07-06-timeout-deadline-library.md: e8a688fbd5285c5e177eca5339bb204e2e4b01bc
2026-07-06-timeout-deadline-library.zh.md: 427aa4e10fe90a69b8773ed739d92e37754a4998
@@ -1,9 +1,9 @@
# RFC: A shared timeout/deadline primitive, with hard-kill left to each capability
English | [中文](2026-07-06-timeout-deadline-library.zh.md)
Status: implemented
English | [中文](2026-07-06-timeout-deadline-library.zh.md)
## Problem
Timeout handling was drifting apart across the tool-bearing capabilities, and the divergence was not superficial — it was the same logic re-implemented three ways, each with its own subtle correctness burden.
@@ -1,4 +1,4 @@
# RFC共享的超时/截止时间原语,硬终止留给各能力自行实现
# RFC: 共享的超时/截止时间原语,硬终止留给各能力自行实现
Status: implemented
@@ -2,5 +2,5 @@
# 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-07-tool-call-timeout-policy.md: 0e69c8504dd34dfc4427bf1f3865a80be93eff9d
2026-07-07-tool-call-timeout-policy.zh.md: 2f4eca6eff9d135aad3fe538b887402a42ac6f84
2026-07-07-tool-call-timeout-policy.md: 20e0b5d70166404e034c0d0ece75cec61cab0df9
2026-07-07-tool-call-timeout-policy.zh.md: 00b85ce9e2e7ae4ba31a9770beefce9c2eb8c319
@@ -1,9 +1,9 @@
# RFC: Tool-call timeout policy as a plugin
English | [中文](2026-07-07-tool-call-timeout-policy.zh.md)
Status: implemented
English | [中文](2026-07-07-tool-call-timeout-policy.zh.md)
## Problem
The [timeout/deadline RFC](2026-07-06-timeout-deadline-library.md) extracted the timing-and-classification primitive into `@deepseek-ai/dsh-timeout`, but timeout policy was still attached to individual capabilities and model-facing schemas. `bash` exposed `timeoutMs`; `web_fetch` exposed `timeout_ms`; `web_search` had no model-facing timeout even though providers already honor `exec.signal`; a future grep/glob tool would either import the timeout library directly or invent its own timeout policy. That is the wrong authoring shape for a plugin SDK: a tool author should normally forward `exec.signal` to the implementation it calls, and deployment policy should decide the budget.
@@ -1,9 +1,9 @@
# RFC工具调用超时策略作为插件
[English](2026-07-07-tool-call-timeout-policy.md) | 中文
# RFC: 工具调用超时策略作为插件
Status: implemented
[English](2026-07-07-tool-call-timeout-policy.md) | 中文
## 问题
[超时/截止时间 RFC](2026-07-06-timeout-deadline-library.md) 将计时与分类原语提取到了 `@deepseek-ai/dsh-timeout`,但超时策略仍然附着在各个能力和面向模型的 schema 上。`bash` 暴露了 `timeoutMs``web_fetch` 暴露了 `timeout_ms``web_search` 没有面向模型的超时参数,尽管提供方已经遵循 `exec.signal`;未来的 grep/glob 工具要么直接导入超时库,要么自行发明超时策略。对于一个插件 SDK 来说,这是错误的编写范式:工具作者通常只需将 `exec.signal` 转发给其调用的实现,而部署策略来决定预算。
@@ -36,7 +36,7 @@ ctx.tools.execute(exec)
`@deepseek-ai/dsh-tools` 声明了一个 `tools/execute` waterfall,其基础 `next()` 是带规范化的分发 thunk——即同一个内部 `try`/`catch`,将抛出的工具错误(或未知工具错误)转换为 `isError``ToolExecutionResult`。监听器接收 `(exec, next)`:调用 `next()` 委托给分发(返回其结果,可选地包装),或返回替代结果以短路分发。整个流水线仍位于 `execute` 的外层 try/catch 内,因此抛出异常的监听器会变成 `isError` 结果,而非轮次失败。
catch 是基础 `next()`(而非 waterfall 之外的东西)这一点至关重要:当提供方看到超时信号并抛出自己的上游中止错误时,注册表分发首先将其转换为普通错误结果,然后 `timeout-policy` 才能将最终结果替换为 `TOOL_TIMEOUT`
catch 是基础 `next`(而非 waterfall 之外的东西)这一点至关重要:当提供方看到超时信号并抛出自己的上游中止错误时,注册表分发首先将其转换为普通错误结果,然后 `timeout-policy` 才能将最终结果替换为 `TOOL_TIMEOUT`
### `timeout-policy` 插件
@@ -108,4 +108,4 @@ function toolTimeoutResult(timeoutMs: number): ToolExecutionResult {
- 多个 `tools/execute` 监听器按普通 Cordis waterfall 顺序组合:调用 `next()` 的监听器包装下游监听器加分发;不调用 `next()` 直接返回的监听器短路它们。一个同时组合超时与未来重试/沙箱/指标包装器的部署通过注册顺序选择语义(「超时覆盖整个重试」vs「超时覆盖每次尝试」)。
- 按声明加入是一个有意的误配置风险:工具可以声明 `timeoutMs` 但不遵循 `exec.signal`,这样的工具在超时时不会停止。插件契约声明:声明预算意味着协作;web 工具在已转发信号的工具上验证了这一模式。
- 过渡期间 `bash` 和已迁移的 web 工具有意使用不同的超时路径:`TOOL_TIMEOUT` 是面向模型的工具调用预算,而 `BASH_TIMEOUT` 仍是 bash 和钩子使用的 bash 后端超时。
- 与字面提案的偏差,按 implemented-RFC 规则记录:插件包为 `@deepseek-ai/dsh-timeout-policy`(而非 `tool-timeout`);信号替换是在 `next()` 之前就地修改 `exec.signal`(而非 `next({ ...exec, signal })`,Cordis 会忽略后者);逐工具预算声明在 `ToolDefinition` 上(`timeoutMs`,由拥有该工具的插件从其配置中设置),而非在本插件配置中按工具名映射——因此执行器是零配置的,拼错工具名不可能发生。以上三点均在上文「决策」一节中描述。
- 与字面提案的偏差,按 implemented-RFC 规则记录:插件包为 `@deepseek-ai/dsh-timeout-policy`(而非 `tool-timeout`);信号替换是在 `next()` 之前就地修改 `exec.signal`(而非 `next({ ...exec, signal })`,Cordis 会忽略后者);逐工具预算声明在 `ToolDefinition` 上(`timeoutMs`,由拥有该工具的插件从其配置中设置),而非在本插件配置中按工具名映射——因此执行器是零配置的,拼错工具名不可能发生。以上三点均在上文 `## Decision` 中描述。
@@ -2,5 +2,5 @@
# 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-08-agent-scope-contexts.md: f238d58d90413d36e81b34c1a2c94e1291e889de
2026-07-08-agent-scope-contexts.zh.md: 0f4de12e782fc72d3761b6d46cd953ab4650654d
2026-07-08-agent-scope-contexts.md: b67d51f06f74f0460f76e9fc4b45699d00b7db46
2026-07-08-agent-scope-contexts.zh.md: e37ea60418095cb34d354150aed79a937b1b3552

Some files were not shown because too many files have changed in this diff Show More