docs(i18n): align Agent Note terminology
This commit is contained in:
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-06-11-content-block-vocabulary.md: 9aad01cee6083b1f380be66869af3137a07d9f1f
|
||||
2026-06-11-content-block-vocabulary.zh.md: 1427ef1a47ee3a54c1c93885befdf580b84f6399
|
||||
2026-06-11-content-block-vocabulary.zh.md: 5720f0742a0729a3f98e4b05ab37acf97ae78db5
|
||||
|
||||
@@ -25,4 +25,4 @@ harness 需要一套统一的内部消息语言,供 agent loop(智能体循
|
||||
- 多模态块只有在适配器、UI 和上下文压缩(context compaction)三方协同支持后才会回归;见 [drop-image Agent Note](../simplification/2026-07-04-drop-image-content-block.md)。
|
||||
- 缓存提示与 assistant prefill 在有实际适配器能兑现之前保持缺席;见[无生产者的词汇变体](../simplification/2026-07-04-prune-producerless-vocabulary-variants.md)与[无端到端可用路径的请求旋钮](../simplification/2026-07-04-drop-inert-request-knobs.md) Agent Note。
|
||||
- 每个适配器都需承担翻译成本;首批真实适配器已验证了流式输出协议,新适配器应继续在适配器本地测试中验证其提供方特有的映射。
|
||||
- 跨包(package)边界的 ID 使用品牌类型(`CallId`、agent 与 session 共享的 `SessionId`)——零运行时开销的名义类型。
|
||||
- 跨包(package)边界的 ID 使用品牌类型(`CallId`、agent 与会话共享的 `SessionId`)——零运行时开销的名义类型。
|
||||
|
||||
@@ -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-custom-schema-dsl.md: 947d53555df078bfa9f3dac48eab4b8c0074007c
|
||||
2026-06-11-custom-schema-dsl.zh.md: 7317e950a4d9822edfca190ccfc22b65809f078a
|
||||
2026-06-11-custom-schema-dsl.zh.md: 26ebfe2fb15a6c034e809b3f51187342fa500193
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Agent Note: 使用自定义类型化 tool-schema DSL 替代 schemastery
|
||||
# Agent Note: 使用自定义类型化工具 schema DSL 替代 schemastery
|
||||
|
||||
Status: implemented
|
||||
|
||||
|
||||
@@ -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-dev-invariants-over-deep-readonly.md: 8f0e79f15af82ce3125b1f6f767d4ea727aa6d29
|
||||
2026-06-11-dev-invariants-over-deep-readonly.zh.md: 9998ddd784f956e3497a0bb9d8f5096047f14a70
|
||||
2026-06-11-dev-invariants-over-deep-readonly.zh.md: 2f787bbd55b5a9a91bc5342351756e45cb0515d3
|
||||
|
||||
@@ -34,7 +34,7 @@ TypeScript readonly 类型不是充分的运行时边界。它们在程序运行
|
||||
|
||||
`dsh-invariants` 注册可配置的 `ctx.invariants` 服务,本身不包含产品检查。每个包发布一个 `./invariant` 所有权配套插件;`dsh-session`、`dsh-agent`、`dsh-scope` 和 `dsh-agent-loop` 目前添加需要 trace 状态或观察另一个 seam 的规则:单调递增的序列号、轮次与步骤嵌套、工具调用/结果配对、合法的 agent(智能体)状态转换、主体正确的作用域分发,以及循环构建的请求与从其会话日志前缀重建的请求之间的等价性。全局启用和包名 regex 过滤器归该服务所有(见[包拥有的不变式服务](2026-07-19-package-owned-invariant-service.md))。
|
||||
|
||||
当 session 配套插件附加到已有或已播种的会话时,它回放不可变日志以重建跟踪状态。服务为每项贡献提供一个可 dispose(资源释放)的子 fiber,因此轮次中途热重载是安全的,同时不赋予诊断逻辑对会话存储的所有权。
|
||||
当会话配套插件附加到已有或已播种的会话时,它回放不可变日志以重建跟踪状态。服务为每项贡献提供一个可 dispose(资源释放)的子 fiber,因此轮次中途热重载是安全的,同时不赋予诊断逻辑对会话存储的所有权。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
@@ -56,5 +56,5 @@ TypeScript readonly 类型不是充分的运行时边界。它们在程序运行
|
||||
- `session.events` 暴露稳定的不可变快照,而非私有的增长数组。
|
||||
- 请求侧的修改无法通过派生消息触及已存储的历史。
|
||||
- 开发构建可以启用关系断言而不改变存储行为;dispose 或过滤一个配套插件不会削弱日志不可变性。
|
||||
- `dsh-invariants` 配置全局启用状态以及包 allow/block regex 列表;每项检查仍由其产品包拥有并测试。
|
||||
- `dsh-invariants` 配置全局启用状态以及包允许/阻止 regex 列表;每项检查仍由其产品包拥有并测试。
|
||||
- 运行时边界对每个被接受的事件产生一次递归快照与冻结的开销;后续读取者和缓存投影复用已拥有的不可变记录。
|
||||
|
||||
@@ -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-event-sourced-sessions.md: 15ba7b23d5eae48e7dee2328b5924493d54aeeb0
|
||||
2026-06-11-event-sourced-sessions.zh.md: fc900b0dc43b18ba2016b4dd57584cf15707b016
|
||||
2026-06-11-event-sourced-sessions.zh.md: da3be5965a6900076f253cad065b847c6f5ce17e
|
||||
|
||||
@@ -6,7 +6,7 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
MVP 要求严格的基于事件的追踪,以及完全可回放的会话(严格的基于事件的 trace、logging 系统,session 完全可回放)。
|
||||
MVP 要求严格的基于事件的追踪,以及完全可回放的会话(严格的基于事件的 trace、logging 系统,会话完全可回放)。
|
||||
|
||||
## 决策
|
||||
|
||||
|
||||
@@ -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-structured-error-taxonomy.md: 9122193b3d01cf5a4c315e6f7a7218153fd4a60a
|
||||
2026-06-11-structured-error-taxonomy.zh.md: cb29503314edf248c90f2eecd54e01f5b203b99c
|
||||
2026-06-11-structured-error-taxonomy.zh.md: 56a196ccd10a81b51953887f18e522412cd9463b
|
||||
|
||||
@@ -18,7 +18,7 @@ Status: implemented
|
||||
|
||||
## 后果
|
||||
|
||||
- 错误端到端可机器路由:插件可以基于 `error.code` 分支,而无需对 message 做子串匹配。
|
||||
- 错误端到端可机器路由:插件可以基于 `error.code` 分支,而无需对消息做子串匹配。
|
||||
- 一个基类被广泛导入,但它位于所有包已经依赖的包中,代价仅是一条 import 语句,而非新的依赖边。
|
||||
- `deriveMessages` 不会将 `error` 暴露到模型历史中——模型仍然看到文本块;结构化字段服务于代码和回放。
|
||||
- 参数校验保留其既有的 code 和行为;包自有的诊断不变式独立携带稳定 code,使不变式注册表无需导入产品包。共享基类增加了跨 seam 的路由元数据,不改变面向模型的文本。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-06-11-tool-schemas-in-prompt-assembly.md: 3643ac3d61be08f629ef0cd0424fef5cb9696c3a
|
||||
2026-06-11-tool-schemas-in-prompt-assembly.zh.md: 645b829627674e5ccd1507002309b7e8364ac59f
|
||||
2026-06-11-tool-schemas-in-prompt-assembly.zh.md: 10389fd7c63755e5b00b3c508fd303a541287f2c
|
||||
|
||||
@@ -20,4 +20,4 @@ Status: implemented
|
||||
|
||||
- 一条 waterfall 统管模型的常驻上下文;plan 模式等插件可以在一个监听器中同时替换提示词文本和可见工具。
|
||||
- assembly 接口通过声明合并实现可扩展(没有无类型的 `extras` 包——扩展即声明合并),为未来的槽位预留空间。
|
||||
- 将 schema 放在「提示词」服务中略有概念上的意外感,已在本文及 package README 中加以说明。
|
||||
- 将 schema 放在「提示词」服务中略有概念上的意外感,已在本文及包 README 中加以说明。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-06-13-capability-seams.md: 7c755dced7825d2831acc0901f6412b8e5afe95a
|
||||
2026-06-13-capability-seams.zh.md: f3aa1ebdee3a16bf095f317b86d01e00b6bbf0c7
|
||||
2026-06-13-capability-seams.zh.md: 4148c79cb5e1930dca77eaf3afd2024f508275b5
|
||||
|
||||
@@ -8,7 +8,7 @@ Status: implemented
|
||||
|
||||
harness 具有可替换的能力:当前是 bash 执行,未来会有沙箱化/远程执行器和替代模型提供方。一项能力涉及三个关注点,它们以不同速率、因不同原因变化:*契约*(这项能力是什么)、*实现*(它如何运行)、*消费方接口*(模型和其他插件面向什么编程)。将三者捆绑在一个包(package)中会耦合这些变化速率——把本地执行器换成沙箱化执行器时,模型看到的工具 schema 也会被搅动,尽管面向模型的契约从未改变。
|
||||
|
||||
这与「谁在运行时提供、谁需要一项能力」是不同的问题,后者 Cordis 已通过 service + `inject` 解决(提供方注册 `ctx.bash`;消费方声明 `inject: ['bash']`,其 fiber 挂起直到服务存在)。该机制是必要的,但不决定包的边界;本 Agent Note 决定的是包的边界。
|
||||
这与「谁在运行时提供、谁需要一项能力」是不同的问题,后者 Cordis 已通过服务 + `inject` 解决(提供方注册 `ctx.bash`;消费方声明 `inject: ['bash']`,其 fiber 挂起直到服务存在)。该机制是必要的,但不决定包的边界;本 Agent Note 决定的是包的边界。
|
||||
|
||||
## 决策
|
||||
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-06-14-session-persistence.md: 1122a52471c6279eff7454cfd31692f05a7bba76
|
||||
2026-06-14-session-persistence.zh.md: 0f2bcd4948a3c95cd0497cfa63ef4454630635c7
|
||||
2026-06-14-session-persistence.zh.md: ecaefb0eb4fc0993ad0792cef9060008b1b3e4fd
|
||||
|
||||
@@ -19,15 +19,15 @@ Status: implemented
|
||||
|
||||
以下关键选择记录于此,因为它们是持久性的、有争议的、且出人意料的:
|
||||
|
||||
- **规范的持久日志逐字节保留每个 `SessionEvent`,包括 `assistant/chunk`。** `deriveMessages()` 跳过 chunk,而过滤 chunk 的方案(Codex 的 `policy.rs`)很有吸引力,但 `seq = log.length` 以及加载验证 `events[i].seq === i` 要求日志是*连续*的;过滤掉 chunk 会留下空洞,同时破坏契约和恢复功能。基于 chunk 过滤的投影可以作为派生视图在后续实现(带有自己的重新编号),但它不是规范日志。
|
||||
- **仅追加;崩溃的轮次被关闭,而非截断。** 已刷写的事件永不被重写。[语义检查点策略](../bug-fix/2026-07-21-semantic-session-checkpoints.md)会在模型分发前排空请求、在工具分发前排空已记录的顶层调用,并在步骤结束后排空完整的响应/结果批次;循环则排空最终轮次边界。由于一个被中断的轮次可能包含大量有效工作,`load` 保留其连续、可解析的事件,并为未应答的 assistant 调用追加按风险分类的错误结果、补一个缺失的 `step/end`,以及带 `{ kind: 'interrupted' }` 的 `turn/end`。合成的结果保证恢复后的 provider transcript(文本记录)仍然有效。只有不完整的最后一条记录会被丢弃;在最后一个真实 `turn/end` 处或之前出现解析错误或序号间隙,属于数据损坏,会使该会话不可加载。
|
||||
- **规范的持久日志逐字节保留每个 `SessionEvent`,包括 `assistant/chunk`。** `deriveMessages()` 跳过分片,而过滤分片的方案(Codex 的 `policy.rs`)很有吸引力,但 `seq = log.length` 以及加载验证 `events[i].seq === i` 要求日志是*连续*的;过滤掉分片会留下空洞,同时破坏契约和恢复功能。基于分片过滤的投影可以作为派生视图在后续实现(带有自己的重新编号),但它不是规范日志。
|
||||
- **仅追加;崩溃的轮次被关闭,而非截断。** 已刷写的事件永不被重写。[语义检查点策略](../bug-fix/2026-07-21-semantic-session-checkpoints.md)会在模型分发前排空请求、在工具分发前排空已记录的顶层调用,并在步骤结束后排空完整的响应/结果批次;循环则排空最终轮次边界。由于一个被中断的轮次可能包含大量有效工作,`load` 保留其连续、可解析的事件,并为未应答的 assistant 调用追加按风险分类的错误结果、补一个缺失的 `step/end`,以及带 `{ kind: 'interrupted' }` 的 `turn/end`。合成的结果保证恢复后的提供方 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。agent loop(智能体循环)不会硬注入 `sessionPersistence`(那样会让非持久化的演示永远挂起);当它不存在时,`resume` 以明确的错误拒绝。
|
||||
- **`ctx.agents.create()` 和 `ctx.agents.resume()` 是异步工厂;恢复还跨越持久化边界。** `ctx.agents.resume({ resumeSessionId })` 等待 `ctx.sessionPersistence.load`,用加载的事件重建活跃会话(使 `lastTurnNumber`/`deriveMessages` 得以延续),并以原样恢复的 id 注册新 agent。agent loop(智能体循环)不会硬注入 `sessionPersistence`(那样会让非持久化的演示永远挂起);当它不存在时,`resume` 会以明确的错误拒绝。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
上述每个关键选择都在陈述处记录了被否决的替代方案:**过滤 chunk 的规范日志**(Codex 的 `policy.rs` 形式)破坏连续 seq 契约;**截断崩溃的轮次**会静默销毁长时间自主运行中的真实工作;**日志内 `session/meta` 事件作为第 0 行**——元数据不是可回放状态;**将 `sessionPersistence` 硬注入循环**会让非持久化的演示永远挂起。
|
||||
上述每个关键选择都在陈述处记录了被否决的替代方案:**过滤分片的规范日志**(Codex 的 `policy.rs` 形式)破坏连续 seq 契约;**截断崩溃的轮次**会静默销毁长时间自主运行中的真实工作;**日志内 `session/meta` 事件作为第 0 行**——元数据不是可回放状态;**将 `sessionPersistence` 硬注入循环**会让非持久化的演示永远挂起。
|
||||
|
||||
格式版本控制:header 携带一个 `version`;`load` 拒绝任何非当前版本(不做迁移——预发布阶段的会话格式固定为 `SESSION_FORMAT_VERSION = 0` 并吸收形状变动,遵循 AGENTS.md 的预发布立场)。坦率地说:仅追加 + 刷写对部分尾部写入是健壮的(加载时容忍),但对行写入中途的无 fsync 断电不健壮;数据库/WAL 后端是后续更强的选项。
|
||||
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# 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: 70ebf1c6de97cb14e27377ec1f9bac18fc0766a6
|
||||
2026-06-18-agent-lifecycle-and-ownership-seams.zh.md: 2091182b663dfbd1ac306550de60c1f00d12656f
|
||||
2026-06-18-agent-lifecycle-and-ownership-seams.zh.md: 285c42fd8a8086b5a1ee0b9f79112b048099077a
|
||||
|
||||
@@ -10,17 +10,17 @@ ACP(Agent Client Protocol)与 tool-bash 的若干限制是同一个缺失 se
|
||||
|
||||
## 决策
|
||||
|
||||
三个 seam:队列感知的 cancel、`AgentHandle` 释放器,以及 bash 所有者令牌。
|
||||
三个 seam:队列感知的取消、`AgentHandle` 释放器,以及 bash 所有者令牌。
|
||||
|
||||
### 1. 队列感知的 `Agent.cancel(cause?)`
|
||||
|
||||
`Agent` 接口新增 `cancel()` 动词——唯一的公开停止原语。(它最初与范围更窄、仅作用于步骤的 `abort()` 一同交付;后者后来因无人使用而移除,使 `cancel()` 成为唯一公开的停止工作方式。)它清空 inbox 的 queued + steering FIFO,在存在活跃轮次时中止它,并保留一个不带 cause 的 pre-run 标记,使在取得所有权前被取消的 prompt 永不运行,而后来的 prompt 仍保持独立。有效调用会在清空或中止前发出 `agent/cancel-requested`,携带类型化的 `user | parent` cause;空闲取消不发出任何事件,也不会使下一条 prompt 搁浅。`whenIdle()` 会到达取消后的静默状态,ACP 的 `session/cancel` 映射到 `user`。[显式轮次取消决策](2026-07-16-explicit-turn-cancellation.md)拥有当前的 cause、signal 生命周期与协作式结算契约。
|
||||
`Agent` 接口新增 `cancel()` 动词——唯一的公开停止原语。(它最初与范围更窄、仅作用于步骤的 `abort()` 一同交付;后者后来因无人使用而移除,使 `cancel()` 成为唯一公开的停止工作方式。)它清空 inbox 的 queued + steering FIFO,在存在活跃轮次时中止它,并保留一个不带 cause 的 pre-run 标记,使在取得所有权前被取消的提示词永不运行,而后来的提示词仍保持独立。有效调用会在清空或中止前发出 `agent/cancel-requested`,携带类型化的 `user | parent` cause;空闲取消不发出任何事件,也不会使下一条提示词搁浅。`whenIdle()` 会到达取消后的静默状态,ACP 的 `session/cancel` 映射到 `user`。[显式轮次取消决策](2026-07-16-explicit-turn-cancellation.md)拥有当前的 cause、signal 生命周期与协作式结算契约。
|
||||
|
||||
### 2. `AgentHandle` 异步释放器
|
||||
|
||||
`ctx.agents.create`/`resume`(以及 `AgentFactory` 接口)返回 `AgentHandle = { agent: Agent; dispose(): Promise<void> }`。释放器是一种**消费方能力**——仅持有裸 `Agent` 的注册表观察者无法将其拆除。调用方 fiber 和已注册的 factory 提供方是结构上的共同所有者:调用方卸载强制结构化所有权,而提供方卸载必须停止旧实例,因为其实例作用域的依赖 surface 通过该提供方解析。三条路径都会进入同一个 memoize 的拆除过程:停止循环、等待其退出与空闲刷写完成(真正的静默,而非仅把状态翻转为 `disposed`)、分离 agent、分离其会话,然后解除其 scope。每个公开 ID 在其精确注册表条目分离时变得可复用;不存在独立的保留释放阶段。由配置创建的 agent 已归 `AgentLoop` fiber 所有(handle 被丢弃)。ACP 在其 `SessionRecord` 中保存每个会话的释放器,并在断连/拆除时运行它,因此单纯的客户端断连不会留下已注册 agent 或 session-store 条目——即使 `session/load` 与拆除竞争(刚恢复的 handle 会在 closed-guard 抛出前释放)。
|
||||
`ctx.agents.create`/`resume`(以及 `AgentFactory` 接口)返回 `AgentHandle = { agent: Agent; dispose(): Promise<void> }`。释放器是一种**消费方能力**——仅持有裸 `Agent` 的注册表观察者无法将其拆除。调用方 fiber 和已注册的 factory 提供方是结构上的共同所有者:调用方卸载强制结构化所有权,而提供方卸载必须停止旧实例,因为其实例作用域的依赖 surface 通过该提供方解析。三条路径都会进入同一个 memoize 的拆除过程:停止循环、等待其退出与空闲刷写完成(真正的静默,而非仅把状态翻转为 `disposed`)、分离 agent、分离其会话,然后解除其 scope。每个公开 ID 在其精确注册表条目分离时变得可复用;不存在独立的保留释放阶段。由配置创建的 agent 已归 `AgentLoop` fiber 所有(handle 被丢弃)。ACP 在其 `SessionRecord` 中保存每个会话的释放器,并在断连/拆除时运行它,因此单纯的客户端断连不会留下已注册 agent 或会话存储条目——即使 `session/load` 与拆除竞争(刚恢复的 handle 会在 closed-guard 抛出前释放)。
|
||||
|
||||
**拆除顺序对持久性至关重要**,实现将会话生命周期折叠进 agent 的单个复合 Cordis effect(`SessionStore.prepare`/`enter`/`announce`,取代兄弟 effect 拆分)。fiber 卸载会并发释放兄弟 effect(`Promise.all`),这会让 session store 的 append 发布钩子移除与循环关闭时的 `session/flush` 竞争,从而丢失关闭的 `turn/end`;在一个 effect 内,释放器作为有序的 LIFO 链运行(停止循环 + `await agent.done` 在会话分离之前),因此无论 handle 的 `dispose()` 还是 fiber 卸载,都会捕获循环的最终刷写。被隔离的 `agent/disposed` 和 `session/disposed` 通知无法拒绝该链或跳过后续拆除。
|
||||
**拆除顺序对持久性至关重要**,实现将会话生命周期折叠进 agent 的单个复合 Cordis effect(`SessionStore.prepare`/`enter`/`announce`,取代兄弟 effect 拆分)。fiber 卸载会并发释放兄弟 effect(`Promise.all`),这会让会话存储的 append 发布钩子移除与循环关闭时的 `session/flush` 竞争,从而丢失关闭的 `turn/end`;在一个 effect 内,释放器作为有序的 LIFO 链运行(停止循环 + `await agent.done` 在会话分离之前),因此无论 handle 的 `dispose()` 还是 fiber 卸载,都会捕获循环的最终刷写。被隔离的 `agent/disposed` 和 `session/disposed` 通知无法拒绝该链或跳过后续拆除。
|
||||
|
||||
### 3. Bash seam 中的所有者令牌
|
||||
|
||||
@@ -30,14 +30,14 @@ ACP(Agent Client Protocol)与 tool-bash 的若干限制是同一个缺失 se
|
||||
|
||||
以下不变式已经成立,并由测试固定:
|
||||
|
||||
- ACP 断连/会话关闭后,不留下该会话的任何已注册 agent 或 session-store 条目,即使 `session/load` 与拆除竞争。
|
||||
- 已入队的 prompt 启动前执行 `session/cancel`,能阻止该 prompt 运行;后来接受的 prompt 仍是独立的已入队轮次。
|
||||
- ACP 断连/会话关闭后,不留下该会话的任何已注册 agent 或会话存储条目,即使 `session/load` 与拆除竞争。
|
||||
- 已入队的提示词启动前执行 `session/cancel`,能阻止该提示词运行;后来接受的提示词仍是独立的已入队轮次。
|
||||
- `tool-bash` HMR 重载不会使另一个会话能够读取或终止已有的后台任务(所有权保留在执行器上)。
|
||||
- 既有的非 ACP 演示无需显式管理 handle 仍能工作;由配置创建的 agent 仍归 `AgentLoop` 插件 fiber 所有。
|
||||
|
||||
## 会话所有者令牌在存活 agent 中唯一
|
||||
|
||||
bash 所有者 token 比较依赖共享的 `Agent.id`/`SessionId` 在存活 agent 中唯一。并发的同 ID 操作可以都私下准备,但发布会依次进入 session 和 agent;`SessionStore.enter()` 拒绝重复的存活 session id,每个失败事务都回滚自己的私有状态。因此程序化调用方无法发布两个共享同一会话 token 的存活 agent。访问*策略*(token 比较)留在消费方 `tool-bash`;bash seam 只存储不透明的 `owner` 字符串且从不解释它——这是正确的接口/实现/消费方拆分。
|
||||
bash 所有者 token 比较依赖共享的 `Agent.id`/`SessionId` 在存活 agent 中唯一。并发的同 ID 操作可以都私下准备,但发布会依次进入会话和 agent;`SessionStore.enter()` 拒绝重复的存活会话 id,每个失败事务都回滚自己的私有状态。因此程序化调用方无法发布两个共享同一会话 token 的存活 agent。访问*策略*(token 比较)留在消费方 `tool-bash`;bash seam 只存储不透明的 `owner` 字符串且从不解释它——这是正确的接口/实现/消费方拆分。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-06-18-session-surface.md: 80034881d0112076759a68737b5931c8ff659d15
|
||||
2026-06-18-session-surface.zh.md: 49303c87aa87569bcc61f215642c9b7f0c408dbf
|
||||
2026-06-18-session-surface.zh.md: 26a3119faf0b6988049a7599ea9551a8ae65d63d
|
||||
|
||||
@@ -63,7 +63,7 @@ export type SurfaceOp =
|
||||
## 后果
|
||||
|
||||
- **`packages/core/session`**:`surface.ts`(`SurfaceManager`)维护一个用于候选接纳和实时投影的有序 seq 数组;`SessionSurface` 是其只读公共视图。`SurfaceOp`/`SurfaceIntent` 与顶层会话事件字段记录条目如何加入它。`append()` 要求 surface 事件携带 `SurfaceIntent`,`deriveMessages()` 以遍历 surface 作为唯一派生路径,`repair.ts` 则发出 surface 感知的闭合事件。种子构造函数拒绝缺少 `surfaceOp` 标记的 surface 可达种子事件(见「不变式」一节)。
|
||||
- **`packages/core/agent-loop`**:所有 surface 可达的追加操作传入 surface 选项。收集 chunk seq 用于 `assistant/message` 溯源;捕获 `tool/call` seq 用于 `tool/result` 溯源。
|
||||
- **`packages/core/agent-loop`**:所有 surface 可达的追加操作传入 surface 选项。收集分片 seq 用于 `assistant/message` 溯源;捕获 `tool/call` seq 用于 `tool/result` 溯源。
|
||||
- **`packages/session-persistence/session-persistence-sqlite`**:`events` 表新增两个可空 TEXT 列(`source_event_seqs`、`surface_op`);`SCHEMA_VERSION` 递增(bump-and-reject,无迁移)。
|
||||
- **`packages/session-persistence/session-persistence-jsonl`**:无需改动。
|
||||
- **`packages/session-persistence/session-persistence`**:抽象接口不变。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# 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: ea9c4fb74f7c1bd68fb62efedd3e1657da96ea65
|
||||
2026-06-18-shared-persistence-write-coordinator.zh.md: 31a66e98800510f0ca5a8e3693d98af4264ea4b5
|
||||
2026-06-18-shared-persistence-write-coordinator.zh.md: 55b6aca31cb75ff58f032ad49569e25b13baee53
|
||||
|
||||
@@ -6,7 +6,7 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
`dsh-session-persistence-jsonl` 与 `dsh-session-persistence-sqlite` 有意在不同存储介质上证明同一份 `SessionPersistence` 契约,但它们重复实现了写入路径编排:per-session 状态、`session/created` 接管、后端特定的前缀读取、write-behind 控制、按 id 串行执行操作、HMR(热模块替换)种子注入与 dispose(资源释放)排空。纯粹的种子前缀碰撞检查与可序列化守卫已迁入 seam 包;剩余的编排仍然对正确性要求很高,且同样的修复被应用了两次。唯一的差异在于存储原语(写字节 vs. INSERT 行)。
|
||||
`dsh-session-persistence-jsonl` 与 `dsh-session-persistence-sqlite` 有意在不同存储介质上证明同一份 `SessionPersistence` 契约,但它们重复实现了写入路径编排:每会话状态、`session/created` 接管、后端特定的前缀读取、write-behind 控制、按 id 串行执行操作、HMR(热模块替换)种子注入与 dispose(资源释放)排空。纯粹的种子前缀碰撞检查与可序列化守卫已迁入 seam 包;剩余的编排仍然对正确性要求很高,且同样的修复被应用了两次。唯一的差异在于存储原语(写字节 vs. INSERT 行)。
|
||||
|
||||
## 决策
|
||||
|
||||
|
||||
@@ -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-branded-ids.md: 93bab1d47c793cc4dd1f1d19fa3af22d1721be29
|
||||
2026-06-20-branded-ids.zh.md: edf5cade38b579346f34a7ed2ee0a876af7a4c8b
|
||||
2026-06-20-branded-ids.zh.md: 6cc8a45717b547614a873d3f13c54c3950484a22
|
||||
|
||||
@@ -6,13 +6,13 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
harness 使用 `Branded<B> = string & { readonly [BRAND]: B }` 机制,为 `CallId`(`packages/llm/llm/src/brand.ts`)和共享的 agent/session `SessionId`(`packages/core/session/src/types.ts`)做 brand 处理;该机制由纯类型包(package) `@deepseek-ai/dsh-brand` 拥有,位于 `packages/util/brand/`,见其 [README](../../../../packages/util/brand/README.md),并为每个类型提供零开销的 cast 工厂。`dsh-brand` 还声明了治理策略:*「Branding 用于跨包边界且可能被混淆的 id;不是每个 string 都需要 brand。」* 这条策略是正确的;问题在于它只落实了一半。两处缺口使得结构相同但语义错误的 string 今天仍能通过类型检查器。
|
||||
harness 使用 `Branded<B> = string & { readonly [BRAND]: B }` 机制,为 `CallId`(`packages/llm/llm/src/brand.ts`)和 agent/会话共享的 `SessionId`(`packages/core/session/src/types.ts`)做 brand 处理;该机制由纯类型包(package) `@deepseek-ai/dsh-brand` 拥有,位于 `packages/util/brand/`,见其 [README](../../../../packages/util/brand/README.md),并为每个类型提供零开销的 cast 工厂。`dsh-brand` 还声明了治理策略:*「Branding 用于跨包边界且可能被混淆的 id;不是每个 string 都需要 brand。」* 这条策略是正确的;问题在于它只落实了一半。两处缺口使得结构相同但语义错误的 string 今天仍能通过类型检查器。
|
||||
|
||||
**缺口 1:bash seam 中未 brand 的跨边界 ID。** 后台 task id 是普通 `string`:`BashTask.id: string`(`packages/bash/bash/src/types.ts`),作为 `string` 贯穿整个执行器 seam(`packages/bash/bash/src/index.ts` 中的 `BashExecutor.get`/`ownerOf`/`readOutput`/`kill(id: string)`),再由面向模型的工具以 `string` 校验并传递(`validateTaskId`、`assertTaskAccess`、`packages/bash/tool-bash/src/index.ts` 中 `task_id` 的 schema 参数)。它由每执行器计数器生成——`packages/bash/bash-local/src/index.ts` 中的 `` `bash-${this.nextTaskId++}` ``——其形状与 `SessionId` 的默认值**完全相同,都是 `name-N`**(`packages/core/session/src/index.ts` 中的 `` `session-${++counter}` ``)。bash task id 和 session id 在调用点轻易就能互换,而编译器毫无反应。这是用户询问的核心案例,并且它是面向模型的 id(模型会把 `task_id` 传回 `bash_output`/`bash_kill`),所以该混淆可由不受信任的输入触达。
|
||||
**缺口 1:bash seam 中未 brand 的跨边界 ID。** 后台 task id 是普通 `string`:`BashTask.id: string`(`packages/bash/bash/src/types.ts`),作为 `string` 贯穿整个执行器 seam(`packages/bash/bash/src/index.ts` 中的 `BashExecutor.get`/`ownerOf`/`readOutput`/`kill(id: string)`),再由面向模型的工具以 `string` 校验并传递(`validateTaskId`、`assertTaskAccess`、`packages/bash/tool-bash/src/index.ts` 中 `task_id` 的 schema 参数)。它由每执行器计数器生成——`packages/bash/bash-local/src/index.ts` 中的 `` `bash-${this.nextTaskId++}` ``——其形状与 `SessionId` 的默认值**完全相同,都是 `name-N`**(`packages/core/session/src/index.ts` 中的 `` `session-${++counter}` ``)。bash task id 和会话 id 在调用点轻易就能互换,而编译器毫无反应。这是用户询问的核心案例,并且它是面向模型的 id(模型会把 `task_id` 传回 `bash_output`/`bash_kill`),所以该混淆可由不受信任的输入触达。
|
||||
|
||||
bash **owner token** 是相关的子情形:`BashExecRequest.owner?: string` 和 `BashExecSpec.owner: string | undefined`(`packages/bash/bash/src/types.ts`)被文档描述为刻意*不透明*的隔离键,但在所有实际调用方中,该值就是所属 agent(智能体)共享的 `Agent.id`/`SessionId`(`callerToken = (exec) => exec.agent?.id`,位于 `packages/bash/tool-bash/src/index.ts`),只是披着另一个 seam 本地名称。它被用于访问控制比较(`owner !== callerToken(exec)`),因此一个不匹配但类型正确的 string 在此处就是跨会话隔离 bug,而当前类型系统无法捕获。这正是[统一 agent/session 标识决策](../simplification/2026-06-20-unify-agent-and-session-id.md)覆盖的共享 id 别名。
|
||||
|
||||
**缺口 2:*已经 brand* 的 ID 在 seam 处被侵蚀。** 就连 `CallId` 和 `SessionId` 也恰好在最容易混淆的地方退化为裸 `string`:注册表/store 键类型和公开方法参数。代表性位置包括 session store、agent 注册表(二者都以共享的 `SessionId` 为键)、`ToolPresenter` 的 call-id map、ACP 的 session-id 记录和 loading set,以及持久化协调器。在集合键处丢弃 brand,会让既有 brand 在查找时毫无价值;它们的价值只实现了一部分。
|
||||
**缺口 2:*已经 brand* 的 ID 在 seam 处被侵蚀。** 就连 `CallId` 和 `SessionId` 也恰好在最容易混淆的地方退化为裸 `string`:注册表/store 键类型和公开方法参数。代表性位置包括会话存储、agent 注册表(二者都以共享的 `SessionId` 为键)、`ToolPresenter` 的 call-id map、ACP 的会话 id 记录和 loading set,以及持久化协调器。在集合键处丢弃 brand,会让既有 brand 在查找时毫无价值;它们的价值只实现了一部分。
|
||||
|
||||
## 决策
|
||||
|
||||
@@ -46,13 +46,13 @@ export function OwnerToken(id: string): OwnerToken {
|
||||
|
||||
### 为什么不把 `owner` 类型标注为 `SessionId`?
|
||||
|
||||
显而易见的捷径是直接把 `owner` 类型标注为 `SessionId`——它确实*总是*一个 session id。我们否决这个方案。bash 执行器 seam 是能力 seam(接口 `dsh-bash`、实现 `dsh-bash-local`、消费方 `dsh-tool-bash`),其 owner token 被*明确记录为刻意不透明*:执行器「从不解释它(seam 中没有访问策略——那是消费方的职责)」(`packages/bash/bash/src/types.ts`)。把 seam 字段类型标注为 `SessionId`,会把 `dsh-session` 的词汇引入一个不应知道 owner token *含义*的包——这会让通用执行后端耦合 session 模型,并违背不透明 token 的设计。取代 `dsh-bash-local` 的沙箱或远程执行器不应继承 session 依赖。独立的 `OwnerToken` brand 使 seam 保持解耦:`dsh-bash` 只知道「owner 是某种带 brand 的不透明 token」,而已经决定访问策略的 `dsh-tool-bash` 消费方,是把其 `SessionId` cast 为 `OwnerToken` 的唯一边界。该 brand 仍带来安全收益(不能把 `BashTaskId` 或裸 string 传到 owner 位置),且不引入耦合。
|
||||
显而易见的捷径是直接把 `owner` 类型标注为 `SessionId`——它确实*总是*一个会话 id。我们否决这个方案。bash 执行器 seam 是能力 seam(接口 `dsh-bash`、实现 `dsh-bash-local`、消费方 `dsh-tool-bash`),其 owner token 被*明确记录为刻意不透明*:执行器「从不解释它(seam 中没有访问策略——那是消费方的职责)」(`packages/bash/bash/src/types.ts`)。把 seam 字段类型标注为 `SessionId`,会把 `dsh-session` 的词汇引入一个不应知道 owner token *含义*的包——这会让通用执行后端耦合会话模型,并违背不透明 token 的设计。取代 `dsh-bash-local` 的沙箱或远程执行器不应继承会话依赖。独立的 `OwnerToken` brand 使 seam 保持解耦:`dsh-bash` 只知道「owner 是某种带 brand 的不透明 token」,而已经决定访问策略的 `dsh-tool-bash` 消费方,是把其 `SessionId` cast 为 `OwnerToken` 的唯一边界。该 brand 仍带来安全收益(不能把 `BashTaskId` 或裸 string 传到 owner 位置),且不引入耦合。
|
||||
|
||||
## 不在范围内 / 可能的扩展
|
||||
|
||||
遵循「不是每个 string 都需要 brand」的策略,刻意保持窄范围。以下每项都是合理的未来 brand 候选,附带推迟理由而非承诺:
|
||||
|
||||
- **`ModelId`**(`GenerateOptions.model`,`LlmService` 适配器注册表的键):一个真正的跨包查找键(config → agent → llm → adapter);合理的下一个 brand,仅为控制本 Agent Note 的影响范围而暂不纳入。
|
||||
- **`ModelId`**(`GenerateOptions.model`,`LlmService` 适配器注册表的键):一个真正的跨包查找键(config → agent → llm → 适配器);合理的下一个 brand,仅为控制本 Agent Note 的影响范围而暂不纳入。
|
||||
- **`ToolName`**(`ToolRegistry` 的键):由作者定义、人类可读,且很少与其他 id 混淆;最弱的候选,可能不值得加 brand。
|
||||
- **`ErrorCode`**(`HarnessError.code`):一个封闭词汇(`ABORTED`、`NO_ADAPTER`……),不是逐实例的 id;如果要做,string 字面量联合类型比 brand 更合适。
|
||||
- **数值序号**:轮次号、步骤号和事件 `seq` 是 `number` 而非 `string`,`Branded<string>` 不适用;可以用并行的 `number & { readonly [BRAND]: B }` 变体来 brand 它们,但它们是位置序号、很少跨边界传递,收益较低。
|
||||
@@ -60,10 +60,10 @@ export function OwnerToken(id: string): OwnerToken {
|
||||
|
||||
## 验证
|
||||
|
||||
已落地的不变式:`BashTaskId` 和 `OwnerToken` 定义在 `dsh-bash` 中,并端到端贯穿执行器 seam、`dsh-bash-local` 生成点与 `dsh-tool-bash` 面向模型的 surface,且 `dsh-bash` 未添加对 `dsh-session` 的依赖;没有任何以范围内 brand id(`CallId`/`SessionId`/`BashTaskId`)为键的集合使用裸 `string`;公开方法参数和导出签名保留 brand;每个原始 string 进入的边界(提供方 call id、ACP session id、模型提供的 `task_id`)都通过 cast 工厂构造 brand,而不是散落的 `as` cast。
|
||||
已落地的不变式:`BashTaskId` 和 `OwnerToken` 定义在 `dsh-bash` 中,并端到端贯穿执行器 seam、`dsh-bash-local` 生成点与 `dsh-tool-bash` 面向模型的 surface,且 `dsh-bash` 未添加对 `dsh-session` 的依赖;没有任何以范围内 brand id(`CallId`/`SessionId`/`BashTaskId`)为键的集合使用裸 `string`;公开方法参数和导出签名保留 brand;每个原始 string 进入的边界(提供方 call id、ACP 会话 id、模型提供的 `task_id`)都通过 cast 工厂构造 brand,而不是散落的 `as` cast。
|
||||
|
||||
## 后果
|
||||
|
||||
- **两个接口面的机械性改动。** 传播 brand 涉及 bash seam(接口 + 实现 + 消费方)以及 ACP session-id 接口和持久化协调器。改动面广但严重度低:遗漏的位置是编译错误而非静默 bug。变更可观察地为纯类型变更——无快照或 e2e 行为差异。它与[统一 agent/session 标识决策](../simplification/2026-06-20-unify-agent-and-session-id.md)相邻,因为二者都触及 session-id / owner-token 边界;`OwnerToken` 出于上述解耦理由仍与统一后的 id 保持独立。
|
||||
- **Brand 不做校验。** Brand 是混淆防护,不是正确性证明:一个*错误的* session id 只要仍是合法的 string,就和以前一样能通过类型检查器。本 Agent Note 不关闭这个缺口(见「不在范围内」)——它只阻止这类*类别*错误:传入错误*种类*的 id。
|
||||
- **两个接口面的机械性改动。** 传播 brand 涉及 bash seam(接口 + 实现 + 消费方)以及 ACP 会话 id 接口和持久化协调器。改动面广但严重度低:遗漏的位置是编译错误而非静默 bug。变更可观察地为纯类型变更——无快照或 e2e 行为差异。它与[统一 agent/会话标识决策](../simplification/2026-06-20-unify-agent-and-session-id.md)相邻,因为二者都触及会话 id / owner-token 边界;`OwnerToken` 出于上述解耦理由仍与统一后的 id 保持独立。
|
||||
- **Brand 不做校验。** Brand 是混淆防护,不是正确性证明:一个*错误的* 会话 id 只要仍是合法的 string,就和以前一样能通过类型检查器。本 Agent Note 不关闭这个缺口(见「不在范围内」)——它只阻止这类*类别*错误:传入错误*种类*的 id。
|
||||
- **「在哪里停下」仍是判断题。** 为 `BashTaskId` 加 brand 但不为 `ToolName` 加,为 `OwnerToken` 加但不为 `ModelId` 加,是对哪些 string「可能被混淆」的品味判断。合理的评审者可能想要更多或更少;`brand.ts` 中的策略是裁决依据,本 Agent Note 倾向于面向模型或用于访问控制的 id。
|
||||
|
||||
@@ -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-extract-example-app-packages.md: 8e87a7164d9cc4789705b3d8fe83dc4978b4def0
|
||||
2026-06-20-extract-example-app-packages.zh.md: b2877f25b8e7e049a2def7acddc96e67fa107ff9
|
||||
2026-06-20-extract-example-app-packages.zh.md: bb5bf6a78d749bf2fc0637a48bff4496399a787d
|
||||
|
||||
@@ -6,7 +6,7 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
示例目录本应是*精简的*——只包含演示的可变接线,而非演示的基础设施。在此次变更之前,它是臃肿的。每个示例都携带一份手写的 `start.ts` 启动引导、一段基础设施前导(`timer`,以及 stdio 演示所需的 `logger` + `hmr`(热模块替换))、三个共享 YAML 片段的嵌套引用(`base.yml` / `base-core.yml` / `acp-agent/acp-tail.yml`),还有各示例自身的 `agent-loop`/persistence/system-prompt 配置。真正的应用——每个 agent(智能体)都需要的服务主干——散落在叶子配置和那些 include 中。
|
||||
示例目录本应是*精简的*——只包含演示的可变接线,而非演示的基础设施。在此次变更之前,它是臃肿的。每个示例都携带一份手写的 `start.ts` 启动引导、一段基础设施前导(`timer`,以及 stdio 演示所需的 `logger` + `hmr`(热模块替换))、三个共享 YAML 片段的嵌套引用(`base.yml` / `base-core.yml` / `acp-agent/acp-tail.yml`),还有各示例自身的 `agent-loop`/持久化/系统提示词配置。真正的应用——每个 agent(智能体)都需要的服务主干——散落在叶子配置和那些 include 中。
|
||||
|
||||
叶子配置还拥有耦合的前门。ACP(Agent Client Protocol)要求 stdout 纯净,并通过 `session/new` 创建 agent;终端应用和 Headless 应用则预创建 `main`,但进程 I/O 契约不同。防止错误组合的唯一屏障是文档中的文字警告,而三个 `start.ts` 文件重复着 Loader 引导和生命周期代码。
|
||||
|
||||
@@ -14,7 +14,7 @@ Status: implemented
|
||||
|
||||
每个示例现在**主要是对一个应用包(package)的调用**,沿着既有的[接口 / 实现 / 消费方 seam](2026-06-13-capability-seams.md) 拆分接线:**应用包拥有组合**,叶子 `cordis.yml` 只拥有**可替换的选择**(哪个 LLM(大语言模型)适配器、哪个 bash 执行器、模型、提示词、持久化根目录)。
|
||||
|
||||
- **`@deepseek-ai/dsh-agent-spine-demo`**([packages/examples/agent-spine-demo](../../../../packages/examples/agent-spine-demo))组合了不含 provider、不含执行器、不含 UI 的主干,并转发 agent loop(智能体循环)的 agent 列表配置。它对具体 loop 的依赖是有意为之,因为该包组合的是主干而非扩展主干;替换 loop 意味着提供另一个 bundle。
|
||||
- **`@deepseek-ai/dsh-agent-spine-demo`**([packages/examples/agent-spine-demo](../../../../packages/examples/agent-spine-demo))组合了不含提供方、不含执行器、不含 UI 的主干,并转发 agent loop(智能体循环)的 agent 列表配置。它对具体 loop 的依赖是有意为之,因为该包组合的是主干而非扩展主干;替换 loop 意味着提供另一个 bundle。
|
||||
- **`@deepseek-ai/dsh-tui-demo`**、**`@deepseek-ai/dsh-cli-demo`** 和 **`@deepseek-ai/dsh-acp-demo`** 各自内置其进程角色。TUI 包含全屏 UI 和预创建的 `main`;Headless 包含 one-shot driver 和预创建的 `main`;ACP 包含 bridge 且不预创建 agent。三者都包含 JSONL 持久化,并省略 stdout logger。
|
||||
- **`start.ts` 已移除。** 每个应用包都暴露一个 bin;`demo:*` 脚本调用它。Loader 引导、`.env` 加载和快速失败守卫位于共享的 [`@deepseek-ai/dsh-app-boot`](../../../../packages/ui/app-boot) 包(在逐文件覆盖率门禁下有单元测试——见[共享应用 bin 的启动胶水](../simplification/2026-07-04-share-app-bin-boot-glue.md));精简的自执行入口由 keyless 的 Loader 路径测试驱动。
|
||||
- **每个叶子 `cordis.yml` 精简为**后端、可选产品工具,以及一个承载应用配置的 app 条目。TUI 和 Headless 把模型/会话选择路由到预创建的 agent;ACP 把初始提供方/模型路由到 bridge。
|
||||
@@ -51,7 +51,7 @@ Status: implemented
|
||||
|
||||
## 相关
|
||||
|
||||
- 取代[使共享示例基础配置与提供方无关](../../rejected/architecture/2026-06-20-providerless-example-base.md):一旦主干移入 `dsh-agent-spine-demo` 且 `base*.yml` 文件被删除,将 `base.yml` 重命名为无 provider 核心便不再有意义。
|
||||
- 取代[使共享示例基础配置与提供方无关](../../rejected/architecture/2026-06-20-providerless-example-base.md):一旦主干移入 `dsh-agent-spine-demo` 且 `base*.yml` 文件被删除,将 `base.yml` 重命名为无提供方核心便不再有意义。
|
||||
- 基于[能力 seam](2026-06-13-capability-seams.md)的接口/实现/消费方拆分——后端和展示层保持为叶子选择;主干是共享 bundle。
|
||||
- 与[将包重组为模块化层级结构](2026-06-20-package-hierarchy.md)互补:新的 app/core 包按该层级结构归入既有分组(`core` 放可复用的主干 bundle,`ui` 放应用特有的前门)。
|
||||
- 后续的[冗余 agent 移除](../simplification/2026-07-20-remove-stdio-and-echo-agents.md)拥有最终的 TUI/Headless 拆分,并移除行式与仅 mock 的叶子。
|
||||
|
||||
@@ -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-package-hierarchy.md: ab0732172934414f2361b9e4976b54e5daee298c
|
||||
2026-06-20-package-hierarchy.zh.md: a347b055d92d4cc6431833765bed083c92bc04c7
|
||||
2026-06-20-package-hierarchy.zh.md: 57058860de5b4cba3e8a45ecad9d611d068fb82c
|
||||
|
||||
@@ -55,7 +55,7 @@ packages/
|
||||
|
||||
包列表此前在五个地方重复枚举。统一的深度 2 布局使大部分可以被推导:
|
||||
|
||||
- `tsconfig.base.json` 通过一条 `@deepseek-ai/dsh-*` `paths` 通配符(每个分组列一个候选)映射所有包,取代了逐包条目。聚合配置(`tsconfig.host.json`、`tsconfig.client.json`)复用该源映射,并携带显式 project references 以保持 package/vendor 类型检查边界完整。(这里引入了一个细节:路径候选中包含 `/*/`,朴素的正则注释剥离器会将其误认为块注释——`scripts/doc-typecheck.ts` 正是因此通过 TypeScript 解析器读取 JSONC 配置,而非手动剥离注释。)
|
||||
- `tsconfig.base.json` 通过一条 `@deepseek-ai/dsh-*` `paths` 通配符(每个分组列一个候选)映射所有包,取代了逐包条目。聚合配置(`tsconfig.host.json`、`tsconfig.client.json`)复用该源映射,并携带显式 project references 以保持包/vendor 类型检查边界完整。(这里引入了一个细节:路径候选中包含 `/*/`,朴素的正则注释剥离器会将其误认为块注释——`scripts/doc-typecheck.ts` 正是因此通过 TypeScript 解析器读取 JSONC 配置,而非手动剥离注释。)
|
||||
- `scripts/publint-all.ts` 通过读取层级结构(`packages/<group>/<pkg>`)推导列表,解决了 `TODO(package-inventory)`。
|
||||
- 聚合配置的 project `references` 仍为显式列表——TypeScript project references 没有通配符形式。从 manifest(元数据清单)生成这些引用留作后续工作(见[通过发现机制获取包清单](../../proposed/process/2026-06-20-discover-package-inventory.md))。
|
||||
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# 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: 4f4e821fec9494fe9ea96894267707d9dd202e4d
|
||||
2026-06-24-web-capability-seam.zh.md: 1d81d06d7ff0202174f4348146117e22ea1de038
|
||||
2026-06-24-web-capability-seam.zh.md: d7c07a8ae0365c321120102a0af401d85d7e2eae
|
||||
|
||||
@@ -10,7 +10,7 @@ harness 需要面向模型的 web 工具,但不能将模型契约绑定到某
|
||||
|
||||
面向模型的接口必须保持稳定,而后端可以更换。更换搜索提供方不应改变模型发起查询的方式;更换 fetch 实现不应改变模型请求 URL 的方式。反过来,提供方包也不应仅仅因为自己有额外的提供方特有旋钮就暴露自己的面向模型工具 schema。
|
||||
|
||||
如果把搜索和 fetch 直接放进 `dsh-tool-web`,面向模型的工具就要同时承担提供方选择、后端请求映射、传输策略、结果归一化、prompt 引导、展示和 schema 注册。让每个提供方注册自己的工具则有相反的问题:工具的可用性、名称、描述和参数将取决于恰好加载了哪些提供方包,提供方特有字段会泄漏到模型契约中。
|
||||
如果把搜索和 fetch 直接放进 `dsh-tool-web`,面向模型的工具就要同时承担提供方选择、后端请求映射、传输策略、结果归一化、提示词引导、展示和 schema 注册。让每个提供方注册自己的工具则有相反的问题:工具的可用性、名称、描述和参数将取决于恰好加载了哪些提供方包,提供方特有字段会泄漏到模型契约中。
|
||||
|
||||
还有一个提供方选择的问题。现有的 `tool-bash` 和 `tool-fs` 可以依赖 Cordis 的 `inject`,因为只有一个后端服务键。Web 有两项独立能力(`search` 和 `fetch`),每项能力可能有多个提供方。`inject: ['web']` 能证明 seam 存在,但不能证明存在可用的搜索或 fetch 提供方,也无法定义多个提供方注册时谁胜出。
|
||||
|
||||
@@ -20,9 +20,9 @@ Web 访问是一个一等能力 seam,遵循[能力 seam Agent Note](2026-06-13
|
||||
|
||||
1. `@deepseek-ai/dsh-web`(`packages/web/web`)拥有 `ctx.web`、提供方注册、提供方选择、共享的请求/结果词汇,以及 web 特有的错误。
|
||||
2. 提供方包实现具体后端并向 `ctx.web` 注册能力,例如 `@deepseek-ai/dsh-web-search-exa`、`@deepseek-ai/dsh-web-search-perplexity`、`@deepseek-ai/dsh-web-search-deepseek` 和 `@deepseek-ai/dsh-web-fetch-local`。
|
||||
3. `@deepseek-ai/dsh-tool-web`(`packages/web/tool-web`)拥有面向模型的 `web_search` 和 `web_fetch` 工具 schema、prompt 段落、参数校验、结果格式化,以及通过 `ctx.web` 实现的工具展示。
|
||||
3. `@deepseek-ai/dsh-tool-web`(`packages/web/tool-web`)拥有面向模型的 `web_search` 和 `web_fetch` 工具 schema、提示词段落、参数校验、结果格式化,以及通过 `ctx.web` 实现的工具展示。
|
||||
|
||||
提供方不注册工具。提供方注册能力。`dsh-tool-web` 是面向模型的名称、描述、prompt 引导、JSON Schema、展示的唯一所有者。
|
||||
提供方不注册工具。提供方注册能力。`dsh-tool-web` 是面向模型的名称、描述、提示词引导、JSON Schema、展示的唯一所有者。
|
||||
|
||||
搜索和 fetch 是两个独立工具,但属于同一个 web 访问 seam。`ctx.web` 为两个并行注册表统一拥有提供方选择、abort/错误词汇和部署配置。它们的请求 schema 和提供方逻辑保持独立;共享的服务是触达 web 的产品边界。
|
||||
|
||||
@@ -66,7 +66,7 @@ flowchart LR
|
||||
toolWeb -->|ctx.tools.register| webFetch["tool: web_fetch"]
|
||||
```
|
||||
|
||||
`@deepseek-ai/dsh-web` 仅依赖 Cordis 和底层 harness 支持。它声明 `ctx.web`、提供方接口、请求/结果类型、提供方可用性契约和错误码。它不导入 tool、agent、session、LLM 或提供方包。
|
||||
`@deepseek-ai/dsh-web` 仅依赖 Cordis 和底层 harness 支持。它声明 `ctx.web`、提供方接口、请求/结果类型、提供方可用性契约和错误码。它不导入工具、agent、会话、LLM 或提供方包。
|
||||
|
||||
提供方包仅依赖 `dsh-web` 和 Cordis。它们拥有凭证、端点、协议格式映射、解析和 `WebError` 转换,使用平台 `fetch`。每个提供方注入共享服务并注册后端;只有 `dsh-web` 拥有 `ctx.web` 键。提供方私有的协议形状不会产生对 `ctx.llm` 或 Cordis HTTP 服务的依赖。
|
||||
|
||||
@@ -161,7 +161,7 @@ interface WebService {
|
||||
|
||||
`max_results` 不暴露给模型。它是 `dsh-tool-web` 层的决策:工具设定结果上限——`searchMaxResults` 插件配置,默认 `8`(与 OpenCode 的 Exa 默认值对齐),类似 `dsh-tool-fs` 的 `readLimit`——并作为 `WebSearchRequest` 上的 `maxResults` 传给 seam。将其排除在模型 schema 之外意味着模型只需提问,产品控制返回多少上下文;该字段日后可以提升为面向模型的参数而不破坏 seam。
|
||||
|
||||
`maxResults` 沿 tool → seam → provider 流动,上限在返回路径上强制执行:
|
||||
`maxResults` 沿工具 → seam → 提供方流动,上限在返回路径上强制执行:
|
||||
|
||||
- `dsh-tool-web` 拥有该值并将其放在 `WebSearchRequest.maxResults` 上。
|
||||
- `ctx.web` 将请求原样传递给选定的提供方。
|
||||
@@ -244,7 +244,7 @@ SSRF/私有网络防护(阻断私有、回环、链路本地、多播及其他
|
||||
|
||||
## 工具消费方行为
|
||||
|
||||
`dsh-tool-web` 拥有两个 `ToolDefinition`:`web_search` 和 `web_fetch`。它拥有面向模型的 JSON Schema、snake_case 参数名、prompt 段落、结果渲染为 `ContentBlock[]`、`presentCall` 和 `presentResult`。
|
||||
`dsh-tool-web` 拥有两个 `ToolDefinition`:`web_search` 和 `web_fetch`。它拥有面向模型的 JSON Schema、snake_case 参数名、提示词段落、结果渲染为 `ContentBlock[]`、`presentCall` 和 `presentResult`。
|
||||
|
||||
`dsh-tool-web` 禁止枚举提供方或直接调用提供方的 `available()`。它进入 seam 的唯一路径是 `ctx.web.search()`/`ctx.web.fetch()`。这将提供方选择保持在单一层;否则工具包可能判定某个提供方可用,而执行时解析出不同的状态。
|
||||
|
||||
@@ -252,7 +252,7 @@ SSRF/私有网络防护(阻断私有、回环、链路本地、多播及其他
|
||||
|
||||
提供方可用性变化影响执行结果和诊断信息,而非面向模型的 schema 是否存在。如果产品完全不需要 web 工具,在配置中禁用 `dsh-tool-web` 或单个 web 工具即可;如果需要 web 工具但后端配置有误,模型在执行时看到结构化的工具错误。
|
||||
|
||||
prompt 引导解释了语义分工——`web_search` 用于发现和获取当前信息,`web_fetch` 用于模型需要特定 URL 内容的场景——prompt 和工具结果告诉模型用 Markdown 链接引用相关 URL。
|
||||
提示词引导解释了语义分工——`web_search` 用于发现和获取当前信息,`web_fetch` 用于模型需要特定 URL 内容的场景——提示词和工具结果告诉模型用 Markdown 链接引用相关 URL。
|
||||
|
||||
面向模型的输出以文本为先,因为工具结果是 `ContentBlock[]`,但 seam 的产出保持结构化,以便 UI 展示和未来的适配器无需解析渲染后的文本。
|
||||
|
||||
@@ -286,7 +286,7 @@ prompt 引导解释了语义分工——`web_search` 用于发现和获取当前
|
||||
|
||||
### 让每个提供方注册自己的面向模型工具
|
||||
|
||||
这与最灵活的提供方插件系统一致:每个提供方可以暴露其完整的原生 schema。在 harness 中被否决,因为它将面向模型的名称、描述、prompt 引导和结果格式化的所有权交给了提供方包。多个搜索提供方会产生重复的工具名或提供方特有的工具名,模型将学到后端细节而非稳定的产品能力。
|
||||
这与最灵活的提供方插件系统一致:每个提供方可以暴露其完整的原生 schema。在 harness 中被否决,因为它将面向模型的名称、描述、提示词引导和结果格式化的所有权交给了提供方包。多个搜索提供方会产生重复的工具名或提供方特有的工具名,模型将学到后端细节而非稳定的产品能力。
|
||||
|
||||
### 将提供方调度直接放在 `dsh-tool-web` 中
|
||||
|
||||
@@ -324,7 +324,7 @@ prompt 引导解释了语义分工——`web_search` 用于发现和获取当前
|
||||
|
||||
## 推迟工作
|
||||
|
||||
- `web_fetch` 的 SSRF/私有网络防护:阻断私有、回环、链路本地、多播及其他非公开目的地,使 `web_fetch` 不再是 SSRF 原语。正确实现不仅仅是 URL 字符串检查——需要先 DNS 解析再连接到已验证的 IP(防御 DNS rebinding/TOCTOU)、跨重定向的每跳重新验证,以及 IPv6 边缘处理(私有范围、IPv4 映射地址)。所调研的参考实现均未做 IP 级阻断(OpenCode 做前缀检查后直接 fetch;Claude Code 依赖集中式主机名黑名单加「私有 URL 会失败」的 prompt),因此没有可复制的实现,且这是 harness 唯一的 SSRF 防线——值得一次专门的设计/spike。在其落地之前,`web_fetch` 只能在无法触达敏感内部目标的部署中启用。
|
||||
- `web_fetch` 的 SSRF/私有网络防护:阻断私有、回环、链路本地、多播及其他非公开目的地,使 `web_fetch` 不再是 SSRF 原语。正确实现不仅仅是 URL 字符串检查——需要先 DNS 解析再连接到已验证的 IP(防御 DNS rebinding/TOCTOU)、跨重定向的每跳重新验证,以及 IPv6 边缘处理(私有范围、IPv4 映射地址)。所调研的参考实现均未做 IP 级阻断(OpenCode 做前缀检查后直接 fetch;Claude Code 依赖集中式主机名黑名单加「私有 URL 会失败」的提示词),因此没有可复制的实现,且这是 harness 唯一的 SSRF 防线——值得一次专门的设计/spike。在其落地之前,`web_fetch` 只能在无法触达敏感内部目标的部署中启用。
|
||||
- `pdf` `WebFetchBody` 类别:`local-http` 提供方将可文本提取的 PDF 解码(尽力而为、有上限、`truncated`)为 `{ kind: 'pdf'; content; pageCount? }` 分支,`tool-web` 渲染它。这是 fetch 而非 `web_extract`——PDF 获取是具体的 HTTP 200 加确定性的本地解码,不是提供方侧对非 HTTP 资源的提取。添加它是跨 `dsh-web`(声明分支)、提供方(解码 + 将「二进制拒绝」收窄为「拒绝二进制,但可文本提取的 PDF 除外」;需要 OCR 的扫描/图片 PDF 不在范围内)和 `tool-web`(渲染)的协调变更。封闭的 `WebFetchBody` 联合类型使消费方在新分支被处理之前编译失败。
|
||||
- 提供方支撑的提取作为独立的 `web_extract` 能力,而非静默扩展 `web_fetch`。
|
||||
- 推迟的权限系统落地后的权限策略集成。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-06-26-file-context-as-event-gate.md: 4700222aa2e0f91d9f355495c228e2eb92825f55
|
||||
2026-06-26-file-context-as-event-gate.zh.md: cf490c9f59f1914f1d5d6bdd9fdf4c08711db91e
|
||||
2026-06-26-file-context-as-event-gate.zh.md: 5edfda5a2da3a7a06093dae65c89fc5ea49b43a8
|
||||
|
||||
@@ -110,7 +110,7 @@ interface Events {
|
||||
|
||||
## 工具契约(`dsh-tool-fs`)
|
||||
|
||||
工具保留其面向模型的 schema(`read`/`write`/`edit`,逐字节不变)和 prompt 段落。prompt 引导仍以策略优先,因为加载 fs 工具的部署预期也会加载 `dsh-fs-policy`:模型仍被告知在覆写或编辑前先读取,任何声称「后端」要求如此的措辞应修正为 fs-policy 插件要求如此。裸提供方回退不改变 prompt 立场。
|
||||
工具保留其面向模型的 schema(`read`/`write`/`edit`,逐字节不变)和提示词段落。提示词引导仍以策略优先,因为加载 fs 工具的部署预期也会加载 `dsh-fs-policy`:模型仍被告知在覆写或编辑前先读取,任何声称「后端」要求如此的措辞应修正为 fs-policy 插件要求如此。裸提供方回退不改变提示词立场。
|
||||
|
||||
`dsh-tool-fs` 获得从旧 `fileContext` 方法服务迁移来的执行器职责,包括**读取渲染**(`read-render.ts`:`buildWindow` + `formatReadOutput`、`READ_MAX_BYTES`、`READ_MAX_LINE_LENGTH`、`FileReadOutcome`/`FileTextLine`,以及 `read.ts` 中的 `STREAM_MIN_SIZE`),这些现在是工具的渲染细节,因为读取已由工具拥有。这些读取渲染类型和辅助函数移入 `dsh-tool-fs`;策略插件不得继续作为工具的类型依赖。
|
||||
|
||||
@@ -118,7 +118,7 @@ interface Events {
|
||||
|
||||
通过让 waterfall 惰性产出期望值来最小化 `stat` 预算——裸默认返回 `undefined`(无守卫),从不 stat:
|
||||
|
||||
- **read**——一次 `stat`(类型 + 大小路由 + 版本),然后 `readText`/`streamText`,然后 `buildWindow`,然后 `emit('fs/observed', target, info.version, exec)`。旧 `fileContext.read` 中读后确认的 `stat` 被移除;在路由 stat 和读取之间竞争的写入者最多只能使*后续*有守卫的编辑误报 `FS_STALE_VERSION`(快速失败:模型重新读取,从不基于错误版本写入,因为 `editText` 在其锁内重新检查)。
|
||||
- **read**——一次 `stat`(类型 + 大小路由 + 版本),然后 `readText`/`streamText`,然后 `buildWindow`,然后 `emit('fs/observed', target, info.version, exec)`。旧 `fileContext.read` 中读后确认的 `stat` 被移除;在路由 stat 和读取之间竞争的写入者最多只能使*后续*有守卫的编辑误报 `FS_STALE_VERSION`(为安全起见拒绝写入:模型会重新读取;由于 `editText` 会在其锁内复查,模型绝不会基于错误版本写入)。
|
||||
- **write**——`expectation = await ctx.waterfall('fs/write-intent', target, exec, () => undefined)`,然后 `ctx.fs.writeText(target, content, expectation)`,然后 `emit('fs/observed', target, outcome.version, exec)`。无论是否有 `dsh-fs-policy`,**工具内零 stat**。
|
||||
- **edit**——`expectation = await ctx.waterfall('fs/edit-intent', target, exec, () => undefined)`,然后 `ctx.fs.editText(target, edit, expectation)`,然后 `emit('fs/observed', target, outcome.version, exec)`。两种情况下**工具内零 stat**:裸默认为 `undefined`(无条件编辑),因此工具从不 stat 来制造基准。如果目标不存在,提供方即使在无守卫路径上也报告 `FS_STALE_VERSION`。
|
||||
|
||||
@@ -165,7 +165,7 @@ interface Events {
|
||||
## 后果
|
||||
|
||||
- **事件间接层取代方法调用。** 一次 waterfall + emit 不如 `await ctx.fileContext.edit(...)` 直接。收益是移除了工具到策略的方法依赖,同时保留默认策略插件;代价是多一套事件词汇需要学习。通过保持三个事件的窄小范围并在每个事件上记录 default-thunk 语义来缓解。
|
||||
- **策略事件位于存储 seam 中。** `dsh-fs` 增加了两个版本决策事件和一个记录事件,尽管它「只是存储」。这是解耦的代价(发射方不能依赖策略插件)。这些事件只携带 `dsh-fs` 词汇加一个不透明的 `object` actor,不携带面向模型的概念,因此 seam 不沾染行窗口/观测策略类型,也不沾染 agent/session owner 结构。
|
||||
- **策略事件位于存储 seam 中。** `dsh-fs` 增加了两个版本决策事件和一个记录事件,尽管它「只是存储」。这是解耦的代价(发射方不能依赖策略插件)。这些事件只携带 `dsh-fs` 词汇加一个不透明的 `object` actor,不携带面向模型的概念,因此 seam 不沾染行窗口/观测策略类型,也不沾染 agent/会话所有者结构。
|
||||
- **单一策略占位者,按约定先到先得。** `fs/write-intent`/`fs/edit-intent` 槽位恰好容纳一个决策者;先注册(或 `prepend`)的监听器获胜,其余被短路。`dsh-fs-policy` 占据该槽位是部署约定,而非事件系统强制的不变式——一个先注册的第二决策者会绕过它。这是可接受的,因为第二个 fs 版本策略决策者是配置错误,而非功能。如果未来出现*分层* fs 版本策略的需求,那是一个新 Agent Note(可组合的值传递 seam),而非在这些事件上静默添加第二个监听器。分层的权限/审计/沙箱拦截已有其归属:`tools/execute`。
|
||||
- **移除读后确认 stat** 使后续*有守卫*的编辑在 read/write 竞争下偶尔快速失败(`FS_STALE_VERSION` → 重新读取)。这是丢失的 UX 便利,绝非正确性漏洞;提供方锁仍阻止基于错误版本的写入。
|
||||
- **移除读后确认 stat** 使后续*有守卫*的编辑在 read/write 竞争下偶尔为安全起见拒绝写入(`FS_STALE_VERSION` → 重新读取)。这是丢失的 UX 便利,绝非正确性漏洞;提供方锁仍阻止基于错误版本的写入。
|
||||
- **裸提供方不做先读后写/编辑,也不做版本检查。** 没有 `dsh-fs-policy` 的部署允许模型无条件覆写或编辑任何已有文件。这正是保持工具独立于策略服务的有意含义:安全纪律存在于 `dsh-fs-policy` 插件中。省略它的部署是有意选择无约束的文件系统;对于发布 fs 工具的配置而言,这不是预期的姿态。
|
||||
|
||||
@@ -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-event-domain-semantics.md: 7310840088d4ff77ddf83c5c16753b4d46256692
|
||||
2026-06-30-event-domain-semantics.zh.md: 882779fafa5953eab7048d234429a8a030dc695e
|
||||
2026-06-30-event-domain-semantics.zh.md: 61d99878ab5e04b74d55d02789c434e0919bd785
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Agent Note: 事件域语义——session 是事实日志,agent 是运行时表面
|
||||
# Agent Note: 事件域语义——会话是事实日志,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`)存在。消费方对同一事实有两个真源,每次生命周期变更都必须同时更新两处。第二,即将到来的钩子子系统需要一个连贯且有文档的订阅表面——插件作者(以及基于其上构建的 Claude Code / Codex 钩子桥接)必须在不阅读循环代码的情况下知道应该监听会话事件还是 agent 事件,以及原因。
|
||||
|
||||
这套词汇是拦截决策、持久的 `hook/*` 日志,以及 Claude Code 和 Codex 桥接的基础。
|
||||
|
||||
@@ -21,17 +21,17 @@ 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/event` 读取;token 流(`assistant/chunk`)和中途 steering(中途引导)(`steering/message`)同理。
|
||||
- **`tools/*`——工具注册表与执行 seam。**
|
||||
|
||||
**边界规则:** 持久的、可回放的事实是 `SessionEvent`;实时拦截或瞬态/活对象信号是 `agent`/`tools` Cordis 事件。轮次或步骤边界是持久事实,因此存在于 session 日志中并从 `session/event` 源读取——不会被镜像为 `agent/*` emit。
|
||||
**边界规则:** 持久的、可回放的事实是 `SessionEvent`;实时拦截或瞬态/活对象信号是 `agent`/`tools` Cordis 事件。轮次或步骤边界是持久事实,因此存在于会话日志中并从 `session/event` 源读取——不会被镜像为 `agent/*` emit。
|
||||
|
||||
**将规则应用于边界镜像:** 全部四个边界镜像——`agent/turn-start`、`agent/turn-end`、`agent/step-start`、`agent/step-end`——被**移除**。没有生产消费方需要在边界处获取活的 `Agent`:ACP 桥接从 `session/event` 的 `turn/end` 加 `agent/status` 结算;唯一的轮次镜像消费方(`dsh-ui-stdio`,一个一次性测试 REPL)从 `session/event` 渲染边界,同时保留其实时目标对象用于固定的 `main` 标签。步骤镜像先被移除(它们完全没有消费方);轮次镜像在 ui-stdio 迁移后随之移除,见[移除边界镜像事件 Agent Note](../simplification/2026-06-20-remove-agent-boundary-mirror-events.md),该决策由它负责。移除 emit 也简化了循环的 `closeStep`/`closeTurn`(各只需一次 append,无需配对 emit)。
|
||||
|
||||
## 后果
|
||||
|
||||
- 循环不再 emit 任何边界镜像;`closeStep` 仅追加 `step/end`,`closeTurn` 仅追加 `turn/end`。`Session.append` 负责 post-commit observer 隔离,因此抛出异常的边界 observer 无法改变轮次结果或饿死后续消费方;接受或内部校验失败仍会在边界进入日志之前逃逸。
|
||||
- 之前通过已移除 emit 观察边界的测试,现在观察持久的 `turn/start`/`turn/end`/`step/start`/`step/end` session 事件——它们固定的行为(边界顺序、步骤计数)不变;只是读取的源移到了规范源。那些测试*抛出异常的轮次边界 emit 监听器*的用例被删除,因为该代码路径不再存在(没有 emit 可供抛出)。按照 [AGENTS.md「测试记录行为,而非黄金真相」](../../../../AGENTS.md),行为与其测试一同迁移(或一同消亡)。
|
||||
- 之前通过已移除 emit 观察边界的测试,现在观察持久的 `turn/start`/`turn/end`/`step/start`/`step/end` 会话事件——它们固定的行为(边界顺序、步骤计数)不变;只是读取的源移到了规范源。那些测试*抛出异常的轮次边界 emit 监听器*的用例被删除,因为该代码路径不再存在(没有 emit 可供抛出)。按照 [AGENTS.md「测试记录行为,而非黄金真相」](../../../../AGENTS.md),行为与其测试一同迁移(或一同消亡)。
|
||||
- 循环仅在 `append('step/start')` 返回后才标记步骤已打开(`stepOpen = true`)。内部分发校验在日志推入之前运行,可能在不打开步骤的情况下拒绝;post-commit `session/event` observer 的失败被隔离在 `Session.append` 内部。因此该标记精确表示已提交的、欠一个后续 `step/end` 的边界。
|
||||
- 完整实现见[简化 Agent Note「停止将持久边界镜像为 agent 事件」](../simplification/2026-06-20-remove-agent-boundary-mirror-events.md):全部四个边界镜像被移除,所有消费方从 `session/event` 读取边界。`agent/steering`(不是边界镜像)不在该 Agent Note 范围内,由其后续 Agent Note [移除 `agent/steering` 镜像 emit](../simplification/2026-07-04-remove-agent-steering-mirror.md) 单独移除——它镜像的是持久的 `steering/message`。
|
||||
- Cordis 事件目录(`docs/cordis-catalog/events.md`)重新生成以移除镜像事件。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-02-fs-per-session-cwd.md: aeb17e0a235720bee277f6533468dbdb6cae82dc
|
||||
2026-07-02-fs-per-session-cwd.zh.md: 34b868761112b3dab31699cd7c945a64e22b7d59
|
||||
2026-07-02-fs-per-session-cwd.zh.md: b9ad4b868c472c8feb9e6f65e42b1658ffdcd2e0
|
||||
|
||||
@@ -6,7 +6,7 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
ACP(Agent Client Protocol)桥接层为每个会话提供独立的工作区:`session/new` 将编辑器的项目目录记录为 `SessionHeader.cwd`,`dsh-tool-bash` 将每次 bash 调用的 `workdir` 默认设为调用方 agent(智能体)的 `session.header.cwd`(见 [`packages/ui/acp`](../../../../packages/ui/acp) 中的 per-session cwd Agent Note 工作与 `dsh-tool-bash` 中的 `resolveWorkdir`)。因此会话 A 中的 bash 命令在 A 的项目目录执行,会话 B 中的在 B 的项目目录执行——一个服务器进程,N 个工作区。
|
||||
ACP(Agent Client Protocol)桥接层为每个会话提供独立的工作区:`session/new` 将编辑器的项目目录记录为 `SessionHeader.cwd`,`dsh-tool-bash` 将每次 bash 调用的 `workdir` 默认设为调用方 agent(智能体)的 `session.header.cwd`(见 [`packages/ui/acp`](../../../../packages/ui/acp) 中的每会话 cwd Agent Note 工作与 `dsh-tool-bash` 中的 `resolveWorkdir`)。因此会话 A 中的 bash 命令在 A 的项目目录执行,会话 B 中的在 B 的项目目录执行——一个服务器进程,N 个工作区。
|
||||
|
||||
文件系统解析使用的是插件加载时的 cwd,而 bash 使用的是会话的项目目录。因此,当编辑器项目目录与服务器启动目录不同时,相对路径的解析结果就会不一致;快照测试因为让这两个路径相同而掩盖了这个 bug。
|
||||
|
||||
@@ -34,6 +34,6 @@ ACP(Agent Client Protocol)桥接层为每个会话提供独立的工作区
|
||||
|
||||
- 在 ACP 演示中,fs 工具与 bash 现在对每个会话的工作区达成一致;编辑器可以打开任意项目目录,两类工具都在该目录下操作。
|
||||
- 对于包含 `symlink/..` 的会话 cwd,或普通符号链接 cwd 搭配含父目录遍历的相对路径,bash、文件系统工具和沙箱授权都会从同一个物理工作区解析;词法父目录不会获得授权。
|
||||
- `FsTarget` 的标识不变:`targetKey` 仍为解析后绝对路径的 realpath,因此 observed-state 键控与符号链接标识不受影响——正确的 per-session cwd 产生与 bash 目标相同的 key。
|
||||
- `FsTarget` 的标识不变:`targetKey` 仍为解析后绝对路径的 realpath,因此 observed-state 键控与符号链接标识不受影响——正确的每会话 cwd 产生与 bash 目标相同的 key。
|
||||
- 向后兼容:所有现有的 `resolve(path)` 调用(均在测试中)继续正常工作;新参数是可选的。
|
||||
- 单会话 stdio 演示不受影响:它不提供会话 cwd(其 agent 的会话没有 `cwd`),因此解析回退到 `config.cwd = process.cwd()`,即工作区本身。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-02-tool-render-intent-union.md: c7adf8f405000ec7940f82e1e1e2406d86253461
|
||||
2026-07-02-tool-render-intent-union.zh.md: 9e9248b95acbbf9d197322acd2f38f594aef3bf1
|
||||
2026-07-02-tool-render-intent-union.zh.md: 01dff0daaacc45f5c85eb2c4380dc78800ce9a81
|
||||
|
||||
@@ -12,7 +12,7 @@ Status: implemented
|
||||
- 哪些组合是*合法的*没有文档说明:一个设置了 `content` 的 `terminal` 调用意味着「卡片上方的描述」;一个设置了 `terminal` 的 generic 调用毫无意义但类型上可表达。类型允许无意义的状态存在。
|
||||
- 无法表达编辑器最需要的文件工具能力:**diff 卡片**(`{path, oldText, newText}`,Zed 将其渲染为内联 diff / 新文件预览)。`ToolCallPresentation.content` 使用的是 *LLM(大语言模型)* 的 `ContentBlock[]` 词汇(text/image),工具根本无法请求 diff 展示。
|
||||
|
||||
`packages/core/tools/src/index.ts` 中已有的 `FIXME(tool-presentation)` 指出了修复方向:「重新设计类型,让工具一次性声明其渲染意图(例如按卡片种类的带标签联合类型),而非一堆由 bridge 拼接的可选字段。」被否决的 Agent Note [折叠工具拥有的 UI 呈现](../../rejected/simplification/2026-06-20-generic-tool-rendering.md)明确推迟了此事:富渲染「应当在至少有两个真实工具和两个真实消费方验证词汇之后,以带标签 render-intent 联合类型的形式回归。」该条件现已满足:两个生产者族(`dsh-tool-bash`、`dsh-tool-fs`)和两个消费方(ACP bridge 实时路径 + snapshot 回放路径)。
|
||||
`packages/core/tools/src/index.ts` 中已有的 `FIXME(tool-presentation)` 指出了修复方向:「重新设计类型,让工具一次性声明其渲染意图(例如按卡片种类的带标签联合类型),而非一堆由 bridge 拼接的可选字段。」被否决的 Agent Note [折叠工具拥有的 UI 呈现](../../rejected/simplification/2026-06-20-generic-tool-rendering.md)明确推迟了此事:富渲染「应当在至少有两个真实工具和两个真实消费方验证词汇之后,以带标签 render-intent 联合类型的形式回归。」该条件现已满足:两个生产者族(`dsh-tool-bash`、`dsh-tool-fs`)和两个消费方(ACP bridge 实时路径 + 快照回放路径)。
|
||||
|
||||
## 决策
|
||||
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-03-filesystem-directory-listing-seam.md: c7db576ff3c7a56622f90a4400bd9297c9591bef
|
||||
2026-07-03-filesystem-directory-listing-seam.zh.md: 44996d4c680bb2a0c3e880a120e0f9bd420493cd
|
||||
2026-07-03-filesystem-directory-listing-seam.zh.md: 75ee6851127ca6d8c3fc60a66115d521d4627cdc
|
||||
|
||||
@@ -10,7 +10,7 @@ Status: implemented
|
||||
|
||||
直接的压力来自 skill(技能)加载:读取单个 `SKILL.md` 已经可以走 `ctx.get('fs')`,但发现哪些 skill 根目录包含 `<name>/SKILL.md` 或 `<name>.md` 仍需要目录枚举。如果仅在 `dsh-skill` 中添加目录列举,要么保留对 Node 的直接依赖,要么在文件系统提供方栈之外发明一个一次性的本地辅助函数。
|
||||
|
||||
本决策只添加提供方能力,不涉及面向模型的 `ls`/`list` 工具或 skill 发现机制的变更。那些消费方需要独立的 UX、prompt 与策略决策。
|
||||
本决策只添加提供方能力,不涉及面向模型的 `ls`/`list` 工具或 skill 发现机制的变更。那些消费方需要独立的 UX、提示词与策略决策。
|
||||
|
||||
## 决策
|
||||
|
||||
@@ -26,7 +26,7 @@ Status: implemented
|
||||
|
||||
它从不读取文件内容。递归遍历、glob 匹配、分页、搜索、文件监听和面向模型的渲染均有意不在范围内。
|
||||
|
||||
本地后端通过 `readdir({ withFileTypes: true })`、`resolveLocalTarget` 以及元数据 `stat`/`realpath` 探测来实现。结果顺序是确定性的(`name.localeCompare`),以保持未来消费方的 prompt/列表输出稳定,并提高前缀缓存复用率。
|
||||
本地后端通过 `readdir({ withFileTypes: true })`、`resolveLocalTarget` 以及元数据 `stat`/`realpath` 探测来实现。结果顺序是确定性的(`name.localeCompare`),以保持未来消费方的提示词/列表输出稳定,并提高前缀缓存复用率。
|
||||
|
||||
损坏或已消失的子项可以表示为 `type: 'other'`(不带 `version`/`size`);它们不会中止整个列举。在列举目录或解析/探测子项元数据时遇到权限或后端 I/O 故障,则以结构化的 `FsError` 错误码使整个列举失败:
|
||||
|
||||
@@ -38,7 +38,7 @@ Status: implemented
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
**在添加 seam 的同时添加面向模型的 list 工具。** 否决。其 prompt、schema 和渲染契约与提供方原语相互独立。
|
||||
**在添加 seam 的同时添加面向模型的 list 工具。** 否决。其提示词、schema 和渲染契约与提供方原语相互独立。
|
||||
|
||||
**让每个消费方自行枚举目录。** 否决。这会将 `dsh-skill` 等产品包绑定到 Node/本地文件系统行为上,绕过策略/远程/沙箱后端。
|
||||
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# 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: 94f5fa409e7b539b48750d12576c7a342a30c9ba
|
||||
2026-07-05-prompt-variables-and-tool-guidance-ownership.zh.md: 572a95c34381f6d73d1f6e053e565f30704026d3
|
||||
2026-07-05-prompt-variables-and-tool-guidance-ownership.zh.md: f1379e143a94a3ae3a07b3120c6f0b9fc8561fe9
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Agent Note: Prompt 变量与工具指导归属
|
||||
# Agent Note: 提示词变量与工具指导归属
|
||||
|
||||
Status: implemented
|
||||
|
||||
@@ -8,7 +8,7 @@ Status: implemented
|
||||
|
||||
组装后的系统提示词存在四个缺陷,同属一类:harness 已知的事实在别处被手工重述,然后漂移。
|
||||
|
||||
**模型无法知道自己的名字。** `AgentOptions.model` 驱动每个请求,但没有任何 prompt 文本携带它——也不可能携带:`dsh-system-prompt` 中的 section 是上下文全局的,而模型名称是 per-agent 的,`assemble()` 根本不接受任何 per-agent 输入。
|
||||
**模型无法知道自己的名字。** `AgentOptions.model` 驱动每个请求,但没有任何提示词文本携带它——也不可能携带:`dsh-system-prompt` 中的 section 是上下文全局的,而模型名称是 per-agent 的,`assemble()` 根本不接受任何 per-agent 输入。
|
||||
|
||||
**工具指导是 leaf YAML 中的手写行文。** bash/subagent/todo_write 的使用指导存放在 coding-agent 和 ACP persona 字符串里——两份漂移的副本(ACP 那份已经被删减)——而 `dsh-tool-fs` 和 `dsh-tool-web` 则通过 `ctx.systemPrompt.section()` 贡献各自的指导。加载或卸载一个工具插件意味着手动编辑每个部署的 persona;两份 YAML 都带着一条 `FIXME(config-comments)` 为这种分裂的症状道歉,旧终端欢迎横幅也手动枚举了工具集。
|
||||
|
||||
@@ -18,55 +18,55 @@ Status: implemented
|
||||
|
||||
## 决策
|
||||
|
||||
**一条原则:prompt 中的每个事实恰好有一个归属方。** 模型名称和工作区是配置/会话事实 → harness 将它们暴露为变量,persona 引用它们。每个工具的语义和何时使用 → 工具的 `description`。description 无法承载的跨调用习惯 → 工具包(package)的 prompt section。harness 来源标识 → 静态的 `harness:identity` section。部署角色与行为 → 部署的 persona。
|
||||
**一条原则:提示词中的每个事实恰好有一个归属方。** 模型名称和工作区是配置/会话事实 → harness 将它们暴露为变量,persona 引用它们。每个工具的语义和何时使用 → 工具的 `description`。description 无法承载的跨调用习惯 → 工具包(package)的提示词 section。harness 来源标识 → 静态的 `harness:identity` section。部署角色与行为 → 部署的 persona。
|
||||
|
||||
### 组装上下文
|
||||
|
||||
`SystemPrompt.assemble(context)` 接受一个可合并扩展的 `AssembleContext`。`dsh-system-prompt` 声明可选的 `scope` 选择器用于 scoped 路由,而 `dsh-agent` 通过声明合并将可选的类型化 `agent` 字段附加到其上(类型层面的 `agent → system-prompt` 边,无运行时依赖循环)。循环在每个步骤调用 `assembleContextFor(agent)`,使两个字段标识同一个 agent;section 文本提供方可以读取该上下文,`system-prompt/assemble` waterfall(瀑布式事件)也接收它,监听器可据此按 agent 过滤或扩展。
|
||||
|
||||
### Prompt 变量
|
||||
### 提示词变量
|
||||
|
||||
插件通过 `ctx.systemPrompt.variable(name, provider)` 注册 `{{name}}` 值。组装过程将它们解析到 waterfall 可见的变量映射中。渲染阶段拒绝以下情况:引用了未知的 own-property、已注册的 provider 返回 `undefined`、格式错误的完整引用、以及仍包含闭合 `}}` 的不平衡引用;孤立的未匹配 `{{` 保留为行文,替换后的值不会被重新扫描。注册阶段拒绝无效或重复的变量名,section 名称也必须唯一。
|
||||
插件通过 `ctx.systemPrompt.variable(name, provider)` 注册 `{{name}}` 值。组装过程将它们解析到 waterfall 可见的变量映射中。渲染阶段拒绝以下情况:引用了未知的 own-property、已注册的提供方返回 `undefined`、格式错误的完整引用、以及仍包含闭合 `}}` 的不平衡引用;孤立的未匹配 `{{` 保留为行文,替换后的值不会被重新扫描。注册阶段拒绝无效或重复的变量名,section 名称也必须唯一。
|
||||
|
||||
`dsh-agent-loop` 注册两个内置变量,均为上下文 agent 的纯投影:`model`(= `options.model`)和 `cwd`(= `session.header.cwd`)。示例 persona 写 `powered by the {{model}} model`——模型名称只在 `model:` 配置键中声明一次。`{{cwd}}` 仅在 ACP 示例中演示:每个 ACP 会话携带客户端的 cwd,而配置预创建的 stdio agent 没有 cwd(在那里声称 `{{cwd}}` 的 persona 会导致该轮次失败——这是有意为之)。变量留在 loop 插件上(不同于下面的 section):它们是本循环驱动的 agent 的运行时事实,替换循环自行提供自己的变量。
|
||||
|
||||
### Persona 作为 order-0 section
|
||||
|
||||
`dsh-system-prompt` 拥有 order 为 `-100` 的 `harness:identity` 和 order 为 0 的配置 `deployment:persona`,因此两者在循环被替换时仍然存活。prompt 渲染只有一条路径 `renderPrompt(assembly)`,已路由请求 header 因此会记录准确的 prompt,稍后由 `ctx.tokenMeter` 为压缩压力回放。agent 作用域的 `deployment:persona` 遮蔽全局默认值,允许 subagent provider 在发布前安装 persona。约定的 order 区间为:identity `-100`、persona `0`、工具指导 `100–199`。
|
||||
`dsh-system-prompt` 拥有 order 为 `-100` 的 `harness:identity` 和 order 为 0 的配置 `deployment:persona`,因此两者在循环被替换时仍然存活。提示词渲染只有一条路径 `renderPrompt(assembly)`,已路由请求 header 因此会记录准确的提示词,稍后由 `ctx.tokenMeter` 为压缩压力回放。agent 作用域的 `deployment:persona` 遮蔽全局默认值,允许 subagent 提供方在发布前安装 persona。约定的 order 区间为:identity `-100`、persona `0`、工具指导 `100–199`。
|
||||
|
||||
### 工具指导归属
|
||||
|
||||
每个工具的语义和选择指导放在工具 description 中。prompt section 只承载跨调用习惯,例如检查 bash 退出标记或优先使用文件系统工具而非 shell 命令。`todo_write` 和 subagent 工具不需要 section,因为它们的 description 包含完整契约。部署 persona 只包含角色和行为。
|
||||
每个工具的语义和选择指导放在工具 description 中。提示词 section 只承载跨调用习惯,例如检查 bash 退出标记或优先使用文件系统工具而非 shell 命令。`todo_write` 和 subagent 工具不需要 section,因为它们的 description 包含完整契约。部署 persona 只包含角色和行为。
|
||||
|
||||
### Subagent 对话历史描述符
|
||||
|
||||
`SubagentProvider.inheritsParentContext` 描述的是对话种子,而非作用域、服务、工具或权限。spawn 和 ACP 将其设为 `false`;fork 设为 `true`。`dsh-tool-subagent` 根据该标志派生工具和 prompt 参数的描述,包括 fork 继承已完成轮次但不继承进行中轮次这一点。provider 生命周期事件使该措辞与响应式 provider 注册保持同步;其设计动机见[提供方生命周期事件 Agent Note](2026-07-05-subagent-provider-lifecycle-events.md)。
|
||||
`SubagentProvider.inheritsParentContext` 描述的是对话种子,而非作用域、服务、工具或权限。spawn 和 ACP 将其设为 `false`;fork 设为 `true`。`dsh-tool-subagent` 根据该标志派生工具和提示词参数的描述,包括 fork 继承已完成轮次但不继承进行中轮次这一点。提供方生命周期事件使该措辞与响应式提供方注册保持同步;其设计动机见[提供方生命周期事件 Agent Note](2026-07-05-subagent-provider-lifecycle-events.md)。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
- **循环自行组合一行 identity 文本**:在必须保持精简的那个包(「用插件,不改循环」)中硬编码面向模型的行文,且在 section 流水线之外构成第二条组合路径。(identity 确实以代码字面量交付——但作为 `dsh-system-prompt` 注册的普通 section,其 `system-prompt/assemble` waterfall 仍是部署需要移除它时的逃生阀。)
|
||||
- **通过 `agent/request` waterfall 注入模型名称**:prompt 文本会在两处组合,更早渲染的 persona 也可能与最终已路由 header 不一致。拥有延迟路由的请求插件还必须拥有该模型在 prompt 中更早出现的声明。
|
||||
- **通过 `agent/request` waterfall 注入模型名称**:提示词文本会在两处组合,更早渲染的 persona 也可能与最终已路由 header 不一致。拥有延迟路由的请求插件还必须拥有该模型在提示词中更早出现的声明。
|
||||
- **在每个 persona 中手写模型名称**:与上方一行的 `model:` 键重复,配置修改后静默失实;正是本 Agent Note 要治愈的病症。
|
||||
- **宽松插值(未知引用保留原样或替换为空)**:一个拼写错误 `{{modle}}`(或一个空洞)会被发送给模型,直到 transcript(文本记录)审查时才会被发现。
|
||||
- **在配置中为每个 subagent 实例编写措辞**:面向模型的行文回到每个部署 × 实例中,重蹈 P2 病症。**根据 provider 名称选择措辞**:`providerName` 本身是配置,重命名 provider 后会静默获得错误的措辞。
|
||||
- **在 `apply` 时解析 provider(加载顺序要求)** 与 **仅用 section 承载 subagent 措辞(在 assemble 时惰性解析)**:provider 生命周期事件的替代方案;两者均在[提供方生命周期事件 Agent Note](2026-07-05-subagent-provider-lifecycle-events.md)中被否决。
|
||||
- **在配置中为每个 subagent 实例编写措辞**:面向模型的行文回到每个部署 × 实例中,重蹈 P2 病症。**根据提供方名称选择措辞**:`providerName` 本身是配置,重命名提供方后会静默获得错误的措辞。
|
||||
- **在 `apply` 时解析提供方(加载顺序要求)** 与 **仅用 section 承载 subagent 措辞(在 assemble 时惰性解析)**:提供方生命周期事件的替代方案;两者均在[提供方生命周期事件 Agent Note](2026-07-05-subagent-provider-lifecycle-events.md)中被否决。
|
||||
|
||||
## 不在范围内
|
||||
|
||||
- 更多变量(`date`、platform、git 状态):注册表使每个变量成为拥有该事实的插件的一行贡献;本 Agent Note 不认领任何一个。
|
||||
- 为预创建的 stdio agent 提供配置 `cwd`(可让 stdio persona 使用 `{{cwd}}` 并按真实路径分区持久化):推迟到 session-cwd 方案重新讨论时。
|
||||
- 为预创建的 stdio agent 提供配置 `cwd`(可让 stdio persona 使用 `{{cwd}}` 并按真实路径分区持久化):推迟到会话 cwd 方案重新讨论时。
|
||||
|
||||
## 交付的不变式
|
||||
|
||||
- tui-agent 的 prompt 通过一条组装路径依次渲染 identity、带插值模型名的 persona,然后是 fs/bash/web 指导。
|
||||
- fork 和 fresh subagent 的描述反映 provider 是否继承已完成的对话轮次;工具随 provider 生命周期变化而出现、消失和重新措辞。
|
||||
- tui-agent 的提示词通过一条组装路径依次渲染 identity、带插值模型名的 persona,然后是 fs/bash/web 指导。
|
||||
- fork 和 fresh subagent 的描述反映提供方是否继承已完成的对话轮次;工具随提供方生命周期变化而出现、消失和重新措辞。
|
||||
- 未知、无值、格式错误或不平衡的变量引用会指明 section 名称并抛出异常;重复的 section、变量和工具注册同样抛出异常。
|
||||
- 快照回放与 prompt 无关:它按轮次和步骤索引已记录的 chunk 流,不比较发出的请求。
|
||||
- 快照回放与提示词无关:它按轮次和步骤索引已记录的分片流,不比较发出的请求。
|
||||
|
||||
## 后果
|
||||
|
||||
- 组装后的 prompt 中每个事实现在恰好有一个归属方,leaf YAML 中手工维护的工具行文已消除:加载或卸载一个工具插件不再需要编辑任何部署的 persona。
|
||||
- `{{model}}` 在组装时反映 `AgentOptions.model`。如果一个插件在 `agent/request` waterfall 中切换模型,prompt 对该步骤的声明就会过时;如果一个插件在那里提供模型(options.model 未设置——循环文档中记载的回退路径),变量在渲染时无值,包含 `{{model}}` 的 persona 会在 waterfall 运行前失败。两者的补救方式相同,就是归属规则本身:拥有延迟绑定模型事实的插件在 `system-prompt/assemble` waterfall 上提前声明它(`assembly.variables['model'] = …`)——一个归属方,两处声明;一个循环测试端到端固定了 supply 路径。已接受。
|
||||
- 当一个已绑定的 provider 不存在时(尚未激活、已卸载、HMR(热模块替换)重载中),subagent 工具不存在,该窗口内的模型请求中不会包含它。这是诚实的状态——替代方案是注册一个 description 或执行都不可信的工具。
|
||||
- 组装后的提示词中每个事实现在恰好有一个归属方,leaf YAML 中手工维护的工具行文已消除:加载或卸载一个工具插件不再需要编辑任何部署的 persona。
|
||||
- `{{model}}` 在组装时反映 `AgentOptions.model`。如果一个插件在 `agent/request` waterfall 中切换模型,提示词对该步骤的声明就会过时;如果一个插件在那里提供模型(options.model 未设置——循环文档中记载的回退路径),变量在渲染时无值,包含 `{{model}}` 的 persona 会在 waterfall 运行前失败。两者的补救方式相同,就是归属规则本身:拥有延迟绑定模型事实的插件在 `system-prompt/assemble` waterfall 上提前声明它(`assembly.variables['model'] = …`)——一个归属方,两处声明;一个循环测试端到端固定了 supply 路径。已接受。
|
||||
- 当一个已绑定的提供方不存在时(尚未激活、已卸载、HMR(热模块替换)重载中),subagent 工具不存在,该窗口内的模型请求中不会包含它。这是诚实的状态——替代方案是注册一个 description 或执行都不可信的工具。
|
||||
- 严格性意味着 persona 可能在渲染时导致轮次失败(例如在无 cwd 的会话上使用 `{{cwd}}`)。失败是受控的——该轮次以 `error` 结束,循环存活——且这是一个我们希望大声暴露的撰写错误。
|
||||
- 目前没有在 prompt 行文中转义字面 `{{name}}` 的语法;如果真实 prompt 确实需要,再行添加。
|
||||
- 目前没有在提示词行文中转义字面 `{{name}}` 的语法;如果真实提示词确实需要,再行添加。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-05-reconstructable-requests.md: 153d37a2faf2265134d5ff9e88f0bbfa275328e0
|
||||
2026-07-05-reconstructable-requests.zh.md: c14738f8a2bc8fd25b66a3b3f82b650dd52231c1
|
||||
2026-07-05-reconstructable-requests.zh.md: caf51c3065e416fd11aebc1c1d4dc2ee248e2e5c
|
||||
|
||||
@@ -24,11 +24,11 @@ Status: implemented
|
||||
|
||||
`EpochHeader` 记录请求的非历史状态:调用配置、渲染后的系统提示词、工具 schema 和会话前缀,空值规范化为缺失。`request/header` 始终写入完整快照:首个循环实例使用 reason `initial`,后续实例使用 `resume`,实例内变更使用 `change`。`foldRequestHeader` 选择最新快照。旧的 `request/header-delta` 事件和已移除的 `fallback` reason 在追加或加载时都会被拒绝。
|
||||
|
||||
每个步骤重建 prompt 组装。在实例的首个步骤中,`agent/session-prefix` 以一个冻结的空种子为基础,用仅限请求的开场消息进行扩展;结果在通用 `agent/pre-step` 检查点与边界快照之前被冻结并缓存于该循环实例。首次调用配置从显式的 `AgentOptions` 出发,保留 fork 覆盖和恢复重配置;后续调用从折叠后的 header 出发。`agent/request` 只能替换那个冻结的配置种子,模型可见内容通过已记录的通道进入。循环记录欠下的 header 事件(前缀唯一的持久归宿),从前缀、快照和 header 构建 `GenerateOptions`,对其深度冻结但保持 `AbortSignal` 活跃。每实例状态仅有缓存的前缀和锚定快照是否已写入。
|
||||
每个步骤重建提示词组装。在实例的首个步骤中,`agent/session-prefix` 以一个冻结的空种子为基础,用仅限请求的开场消息进行扩展;结果在通用 `agent/pre-step` 检查点与边界快照之前被冻结并缓存于该循环实例。首次调用配置从显式的 `AgentOptions` 出发,保留 fork 覆盖和恢复重配置;后续调用从折叠后的 header 出发。`agent/request` 只能替换那个冻结的配置种子,模型可见内容通过已记录的通道进入。循环记录欠下的 header 事件(前缀唯一的持久归宿),从前缀、快照和 header 构建 `GenerateOptions`,对其深度冻结但保持 `AbortSignal` 活跃。每实例状态仅有缓存的前缀和锚定快照是否已写入。
|
||||
|
||||
**`step/start` 是重建边界。** 一个步骤从该序列之前的事件推导消息。快照之后的注入加入下一次请求,事件发布期间的重入追加被拒绝。`agent/pre-step(agent, turn, step, signal)` 仍是当前请求所需内容的通用 seam。header 重建选择该步骤的 `request/header`,或在无新 header 写入时沿用前一个快照。
|
||||
|
||||
**强制执行。** `dsh-agent-loop/invariant` 配套插件向 `ctx.invariants` 注册,并在被选用时通过一个全新的 `Session` 独立重建每个循环请求,使活跃缓存无法为自身背书,然后在 `llm/stream` 处比较消息和折叠后的 header 字段。循环通过 `dsh-llm` 的 `markAgentLoopRequest()` 记录精确的冻结请求;这一进程内标识让配套插件和其他请求观察者识别对话工作,而直接的一次性调用无论其冻结形状或 session id 如何都保持排除。正确性依赖于序列有界的重建,而非监听器顺序。带密钥的 e2e 要求首次请求之后有正值的 cache-read token;逐步骤用量是生产信号,header 变更或压缩表现为下一步骤的 cache-read 下降。
|
||||
**强制执行。** `dsh-agent-loop/invariant` 配套插件向 `ctx.invariants` 注册,并在被选用时通过一个全新的 `Session` 独立重建每个循环请求,使活跃缓存无法为自身背书,然后在 `llm/stream` 处比较消息和折叠后的 header 字段。循环通过 `dsh-llm` 的 `markAgentLoopRequest()` 记录精确的冻结请求;这一进程内标识让配套插件和其他请求观察者识别对话工作,而直接的一次性调用无论其冻结形状或会话 id 如何都保持排除。正确性依赖于序列有界的重建,而非监听器顺序。带密钥的 e2e 要求首次请求之后有正值的 cache-read token;逐步骤用量是生产信号,header 变更或压缩表现为下一步骤的 cache-read 下降。
|
||||
|
||||
### MiniCode 形态:采纳,但溯源箭头反转
|
||||
|
||||
@@ -48,9 +48,9 @@ Status: implemented
|
||||
|
||||
- 一个日志无法解释的请求不可能被意外构造——无论是循环还是监听器;变异已构建的请求会抛异常;每个 header 变更都是持久的、可 diff 的日志事件。
|
||||
- 在建议性通道之间做选择是变更频率的决策,而本设计使稳定的那个在结构上成为默认:`agent/session-prefix` 的贡献在每个循环实例中只组合一次并逐字复用,因此以零边际成本扩展可缓存前缀,且不可能在会话中途击穿提供方缓存;会话中途变化的内容通过仅追加的历史通道流入——`agent.inject()` 以及工具/prompt-submit 的 `additionalContexts`——每条都是持久的 `context/message`,付出一次代价后即被前缀缓存,代价是在历史和日志中累积。将会话冻结的开场内容路由到前缀,将变更通知路由到历史通道;逐步骤的仅限请求尾部槽位被有意放弃(无消费方,且持久追加覆盖了当前所有更新模式)。
|
||||
- 在提供方处仍需全价计算的内容是固有的且已记录的:压缩(其 `compact/*` 事件和替换条目)、真正的 prompt、工具或配置变更(reason 为 `change` 的 `request/header`),或带漂移的进程边界(不同的 `resume` 快照)。提供方自身的 reasoning-content 排除由服务端管理。
|
||||
- 在提供方处仍需全价计算的内容是固有的且已记录的:压缩(其 `compact/*` 事件和替换条目)、真正的提示词、工具或配置变更(reason 为 `change` 的 `request/header`),或带漂移的进程边界(不同的 `resume` 快照)。提供方自身的 reasoning-content 排除由服务端管理。
|
||||
- `step/start` 监听器行为变更(见上文)是对插件唯一可观察的语义变更;`agent/pre-step` 是当前请求的 seam。
|
||||
- 工具结果裁剪(计划中)无需新机制:一个已记录的单条目 surface replace(`start === end`),携带同一 `callId` 下裁剪后的 `tool/result`——属压缩家族,回放正确,缓存击穿由相同的压力逻辑批量处理。
|
||||
- 会话日志每个循环实例增长一个 `request/header` 快照,并在真正变更时增加快照。它比 delta 编解码器更大,但相对 chunk 密集型日志仍然很小,并只保留一种回放表示。`SESSION_FORMAT_VERSION` 保持 `0`;旧的 delta 事件被拒绝而非迁移。
|
||||
- 快照 expected output 变更一次(每个 transcript(文本记录)增加其 header 事件);写入文件系统的 fixture(测试前置数据)以规范化的撰写形式存储,工具参数使用 cwd 相对路径,因为回放只对 cwd 无关的参数路径做往返。
|
||||
- 会话日志每个循环实例增长一个 `request/header` 快照,并在真正变更时增加快照。它比 delta 编解码器更大,但相对分片密集型日志仍然很小,并只保留一种回放表示。`SESSION_FORMAT_VERSION` 保持 `0`;旧的 delta 事件被拒绝而非迁移。
|
||||
- 快照预期输出变更一次(每个 transcript(文本记录)增加其 header 事件);写入文件系统的 fixture(测试前置数据)以规范化的撰写形式存储,工具参数使用 cwd 相对路径,因为回放只对 cwd 无关的参数路径做往返。
|
||||
- FIXME(call-config-shape):重新审视 `LlmCallConfig` 的确切字段集——哪些字段对缓存而言真正属于 epoch 级别(`model` 毫无疑问;采样标量出于谨慎放在那里),以及当适配器需要时,提供方特定的额外项(reasoning 选项、额外 body 参数)应归属何处。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# 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: afd45027e8b56cbf1d17e6dec749d8602c81124d
|
||||
2026-07-05-subagent-provider-lifecycle-events.zh.md: 98bcaed31d88c0b6c10ffd7942e77f8048733302
|
||||
2026-07-05-subagent-provider-lifecycle-events.zh.md: 58d439936a3f2cc51d8190cbebe8e68cdb14c855
|
||||
|
||||
@@ -6,7 +6,7 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
[prompt-variables Agent Note](2026-07-05-prompt-variables-and-tool-guidance-ownership.md) 让 `dsh-tool-subagent` 从其提供方派生面向模型的措辞:`SubagentProvider.inheritsParentContext`(spawn/ACP 为 `false`,fork 为 `true`)同时驱动工具描述和 `prompt` 参数描述,使 fork 工具不再在上下文继承问题上对模型撒谎。这一修复引入了跨 fiber 的数据依赖:工具描述在工具注册时固定(这是有意为之——描述是 tool-choice 引导所在之处),但提供方在自己的插件 fiber 上到达,时机不确定。
|
||||
[提示词变量 Agent Note](2026-07-05-prompt-variables-and-tool-guidance-ownership.md) 让 `dsh-tool-subagent` 从其提供方派生面向模型的措辞:`SubagentProvider.inheritsParentContext`(spawn/ACP 为 `false`,fork 为 `true`)同时驱动工具描述和 `prompt` 参数描述,使 fork 工具不再在上下文继承问题上对模型撒谎。这一修复引入了跨 fiber 的数据依赖:工具描述在工具注册时固定(这是有意为之——描述是 tool-choice 引导所在之处),但提供方在自己的插件 fiber 上到达,时机不确定。
|
||||
|
||||
如果在工具插件的 `apply` 时刻解析提供方,就会产生一个隐式的加载顺序要求(「在 cordis.yml 中把后端列在工具前面」)。这个要求不成立,因为 Cordis Loader 并发启动同级条目,且 `Entry.init()` 不会等待激活完成:延迟到达的后端即使列在前面,也可能让工具 fiber 失败。Loader 不提供同级顺序保证——「异步状态不是同步状态」(见[防御性模式](../../../../docs/defensive-patterns.md))。
|
||||
|
||||
@@ -25,12 +25,12 @@ Status: implemented
|
||||
|
||||
- **在 `apply` 时解析提供方,不存在则抛异常**:否决。「先列后端」这一要求声称了 Loader 并不存在的顺序保证。
|
||||
- **重试查找(轮询直到提供方出现)**:最终能收敛,但在框架已有的机制(effect 注册 + disposal)之外发明了一套私有就绪协议;它也无法感知提供方离开,因此 HMR 会遗留一个措辞描述已 dispose 后端的工具。
|
||||
- **仅在 section 中放置 subagent 措辞,在组装时惰性解析**:同样能容忍任意加载顺序,但将 tool-choice 引导移出了描述,与 prompt-variables Agent Note 建立的所有权规则相矛盾(每个工具的语义和何时使用属于描述)。响应式注册既保持描述的权威性,又不依赖顺序。
|
||||
- **仅在 section 中放置 subagent 措辞,在组装时惰性解析**:同样能容忍任意加载顺序,但将 tool-choice 引导移出了描述,与提示词变量 Agent Note 建立的所有权规则相矛盾(每个工具的语义和何时使用属于描述)。响应式注册既保持描述的权威性,又不依赖顺序。
|
||||
- **根据提供方名称而非提供方对象确定措辞**:`providerName` 本身是配置,重命名后的提供方会静默获得错误的措辞;从已解析提供方自身的 `inheritsParentContext` 派生则不会漂移。
|
||||
|
||||
## 后果
|
||||
|
||||
- 从命名提供方派生状态的消费方响应 `subagent/provider-added`/`-removed` 事件,而非在 `apply` 时读取注册表;`dsh-tool-subagent` 是参考实现。
|
||||
- **添加时大声失败;移除时按监听器隔离。** 添加监听器可以回滚注册。移除在 disposal 期间运行,因此单个监听器抛异常只会被记录日志,不会饿死后续镜像或干扰拆解流程。`start()` 仍在每次运行时按名称解析提供方,防止陈旧工具调用已移除的后端。见[事件目录](../../../../docs/cordis-catalog/events.md)与[生产者/消费方映射](../../../../docs/event-producer-consumer.md)。
|
||||
- **工具不存在的窗口期。** 在后端 disposal 与重新注册之间(HMR 重载期间),模型看不到 subagent 工具。这是诚实的状态——替代方案是一个向空处分发的工具——工具注册表的 `tools/change` 事件发出会保持 prompt 组装的时效性。
|
||||
- **工具不存在的窗口期。** 在后端 disposal 与重新注册之间(HMR 重载期间),模型看不到 subagent 工具。这是诚实的状态——替代方案是一个向空处分发的工具——工具注册表的 `tools/change` 事件发出会保持提示词组装的时效性。
|
||||
- **两个等待中的 fiber 共享同一 `toolName` 是无效配置,被延迟捕获。** 如果两个 `dsh-tool-subagent` 加载实例命名了不同的提供方但相同的 `toolName`,两者都会等待,先到达的提供方先注册;第二次注册仅在其提供方到达时才抛异常。插件中的 `TODO(subagent-dup-toolname)` 记录了这一影响范围;工具注册表的重名拒绝机制仍是最终防线。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# 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: 11d4b8cd48dd345d2324b63e01bd726f12d846b4
|
||||
2026-07-06-timeout-deadline-library.zh.md: c6e5706e9a62877dc71d56951e82bc892b02a75e
|
||||
2026-07-06-timeout-deadline-library.zh.md: 6ac9d684582e04d5c7c21a74692fb6452a2846dd
|
||||
|
||||
@@ -92,7 +92,7 @@ export function timeoutOf(x: AbortSignal | { reason?: unknown }, code?: string):
|
||||
|
||||
- **web_fetch**:工具层保持校验并转发;提供方手写的 controller + `setTimeout` + 手动监听器 + `finally` + `signal.reason` 恢复被替换为提供方自有的 `deadline`/`timeoutOf`。已预先中止的上游信号仍然立即抛出 `WEB_ABORTED`;否则 `fetch` 使用融合后的 `d.signal` 运行,`translateAbortOrNetwork` 根据信号分类抛出的错误(`timeoutOf` → `WEB_FETCH_TIMEOUT`,否则已中止 → `WEB_ABORTED`,否则网络错误 → `WEB_PROVIDER_ERROR`)。公开的错误码契约不变,`TimeoutReason` 永远不会作为公开错误跨越 web seam。
|
||||
- **bash**:`resolve()` 将请求钳位为显式规格。前台 `run()` 创建 deadline 并将其信号传给进程执行,后者既有的 abort 监听器执行进程组 kill。执行器将首个 abort 分类为超时或取消。后台启动保持无超时,仅转发上游取消。
|
||||
- **LLM 适配器**:`dsh-llm-deepseek` 和 `dsh-llm-pi-ai` 用 `idleWatchdog` 包装实际的传输迭代。配置的五分钟间隔只覆盖尚未结算的提供方 demand,不包括下游消费方在 chunk 之间花费的时间。稳定信号在整个调用期间传给 `fetch` 或 SDK,因此超时会关闭底层请求并映射为 `TIMEOUT`,而更早的调用方中止映射为 `ABORTED`。
|
||||
- **LLM 适配器**:`dsh-llm-deepseek` 和 `dsh-llm-pi-ai` 用 `idleWatchdog` 包装实际的传输迭代。配置的五分钟间隔只覆盖尚未结算的提供方 demand,不包括下游消费方在分片之间花费的时间。稳定信号在整个调用期间传给 `fetch` 或 SDK,因此超时会关闭底层请求并映射为 `TIMEOUT`,而更早的调用方中止映射为 `ABORTED`。
|
||||
|
||||
## 后果
|
||||
|
||||
@@ -102,11 +102,11 @@ export function timeoutOf(x: AbortSignal | { reason?: unknown }, code?: string):
|
||||
- `AbortSignal.any` 和 `using`/`Symbol.dispose` 在此首次进入本仓库(Node ≥ 24 基线,已满足)。
|
||||
- 模型流现在共享一个可重启的定时器契约,不会把滑动的空闲间隔变成总调用截止时间,也不会计入消费方思考时间。该原语仍然只做通知;适配器测试证明其传输观察到稳定信号并终止。
|
||||
|
||||
以下内容不在本次范围内,列出以标明边界:`web_search` 可以在其 tool-schema/snapshot 覆盖率规划就绪后获得可选的面向模型的 `timeout_ms`;未来基于 ripgrep 的文件系统发现工具可以在存在后消费同样的提供方自有 deadline 形状;`tools/execute` waterfall(瀑布式事件)中间件可以通过驱动 `exec.signal` 为每次工具调用设置默认 deadline——那将是一个*消费*本库的插件,仍然只做通知,硬终止仍是各能力自己的事。
|
||||
以下内容不在本次范围内,列出以标明边界:`web_search` 可以在其工具 schema/快照覆盖率规划就绪后获得可选的面向模型的 `timeout_ms`;未来基于 ripgrep 的文件系统发现工具可以在存在后消费同样的提供方自有 deadline 形状;`tools/execute` waterfall(瀑布式事件)中间件可以通过驱动 `exec.signal` 为每次工具调用设置默认 deadline——那将是一个*消费*本库的插件,仍然只做通知,硬终止仍是各能力自己的事。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
**统一的超时*插件* / `ctx.timeout` 服务。** 基于微内核原则否决。一个能停止任何工具工作的服务必须理解每个能力的终止机制(进程组 SIGKILL、socket 拆除、系统调用边界检查),这正是架构所禁止的「内核知道太多」。Codex 的 `ExecExpiration` 被限定于 exec 族,正是因为它驱动的 kill(`killpg`)是进程族特有的;MCP 和 model-stream 各自保有自己的。不存在一个连贯的中间层能为所有东西拥有终止权,因此共享部分只能是纯计时/分类那一半——一个库,而非服务。
|
||||
**统一的超时*插件* / `ctx.timeout` 服务。** 基于微内核原则否决。一个能停止任何工具工作的服务必须理解每个能力的终止机制(进程组 SIGKILL、socket 拆除、系统调用边界检查),这正是架构所禁止的「内核知道太多」。Codex 的 `ExecExpiration` 被限定于 exec 族,正是因为它驱动的 kill(`killpg`)是进程族特有的;MCP 和模型流各自保有自己的。不存在一个连贯的中间层能为所有东西拥有终止权,因此共享部分只能是纯计时/分类那一半——一个库,而非服务。
|
||||
|
||||
**每个工具各自实现超时,不共享代码(先前的现状,也是 Claude Code 的选择)。** 否决,因为它已经在产生分化和重复的正确性负担:web_fetch 手写了与未来网络/进程类工具各自需要重新推导的完全相同的 controller/reason 逻辑,而融合 + `signal.reason` 恢复正是容易出错的部分。Claude Code 容忍完全重复;本仓库有一个统一的共享 abort 通道(每次 `execute` 上的 `exec.signal`),使得一个小型共享原语严格更优,因此成本/收益不同。
|
||||
|
||||
|
||||
@@ -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-agent-scope-contexts.md: e4c076189a8e8a438b561232d3779ad1f6ab0d08
|
||||
2026-07-08-agent-scope-contexts.zh.md: ff8243ebdd541e4a7821d9c4ff99217a8060c2b4
|
||||
2026-07-08-agent-scope-contexts.zh.md: 81decfe54c1a13ac9f8361fb98c94804bb6b80cc
|
||||
|
||||
@@ -14,9 +14,9 @@ Status: implemented
|
||||
|
||||
## 决策
|
||||
|
||||
每个存活的 agent 拥有一个扁平的注册层,通过 `agent.ctx` 暴露。代码通过拥有某项贡献的 context 进行注册;具备作用域感知的服务将部署全局注册与恰好一个匹配的 agent 层合并;操作从其真实 agent 选择该层;该层在 agent 的完整发布生命周期内存在。
|
||||
每个存活的 agent 拥有一个扁平的注册层,通过 `agent.ctx` 暴露。代码通过拥有某项贡献的上下文进行注册;具备作用域感知的服务将部署全局注册与恰好一个匹配的 agent 层合并;操作从其真实 agent 选择该层;该层在 agent 的完整发布生命周期内存在。
|
||||
|
||||
Cordis 是 SDK 底层的插件框架。Cordis **context** 是插件用来访问服务和注册效果的对象,效果的清理跟随该 context。[Cordis 入门](../../../../docs/cordis-primer.md)对该框架有更详细的说明。
|
||||
Cordis 是 SDK 底层的插件框架。Cordis **上下文**是插件用来访问服务和注册效果的对象,效果的清理跟随该上下文。[Cordis 入门](../../../../docs/cordis-primer.md)对该框架有更详细的说明。
|
||||
|
||||
对大多数贡献者而言,完整契约是四条规则:
|
||||
|
||||
@@ -49,11 +49,11 @@ flowchart LR
|
||||
|
||||
### 注册来源决定可见性与清理
|
||||
|
||||
通过普通插件 context 进行的注册是部署全局的,随该插件一起 dispose(资源释放)。同一方法通过 `agent.ctx` 调用则贡献给一个 agent,随该 agent 的作用域一起 dispose。
|
||||
通过普通插件上下文进行的注册是部署全局的,随该插件一起 dispose(资源释放)。同一方法通过 `agent.ctx` 调用则贡献给一个 agent,随该 agent 的作用域一起 dispose。
|
||||
|
||||
| 注册来源 | 默认可见性 | 随谁 dispose |
|
||||
|---|---|---|
|
||||
| 普通插件 context | 每个符合条件的 agent 视图 | 注册插件 |
|
||||
| 普通插件上下文 | 每个符合条件的 agent 视图 | 注册插件 |
|
||||
| `agent.ctx` | 仅该 agent 的视图 | agent 作用域 |
|
||||
|
||||
工具、提示词段落与变量、工具限制、守卫以及作用域事件监听器都遵循此契约。命名的本地值通常对该 agent 遮蔽同名全局值;各所属服务文档会说明例外与合并行为。
|
||||
@@ -88,7 +88,7 @@ await handle.dispose()
|
||||
ctx.tools.get('review_summary', handle.agent) // undefined: scope is gone
|
||||
```
|
||||
|
||||
setup 接收一个完整的受信 Cordis context,因此可以组合普通插件和服务。其契约仅限组合:不支持通过 cast 或内部注册表调用来驱动或发布正在构建中的 agent。
|
||||
setup 接收一个完整的受信 Cordis 上下文,因此可以组合普通插件和服务。其契约仅限组合:不支持通过 cast 或内部注册表调用来驱动或发布正在构建中的 agent。
|
||||
|
||||
### 操作选择视图
|
||||
|
||||
@@ -104,19 +104,19 @@ setup 接收一个完整的受信 Cordis context,因此可以组合普通插
|
||||
|
||||
在 Cordis 层面,`Scoped<T>` 是一个不透明的路由接收器。它携带用于选择监听器的过滤器,但本身不是领域对象。因此事件签名将真实的 `Agent`、工具执行、审批请求或其他主体作为显式参数保留,供监听器检查。
|
||||
|
||||
以 `{ global: true }` 注册的监听器有意绕过上下文受众过滤,但其清理仍跟随注册 context。注册表成员变更通知保持不过滤,因为它们描述的是共享注册表状态而非某个 agent 的操作。生成的[事件目录](../../../../docs/cordis-catalog/events.md)是详尽的事件参考。
|
||||
以 `{ global: true }` 注册的监听器有意绕过上下文受众过滤,但其清理仍跟随注册上下文。注册表成员变更通知保持不过滤,因为它们描述的是共享注册表状态而非某个 agent 的操作。生成的[事件目录](../../../../docs/cordis-catalog/events.md)是详尽的事件参考。
|
||||
|
||||
### 创建最后发布,dispose 最后撤销
|
||||
|
||||
`ctx.agents.create()` 和 `resume()` 构建未发布的会话、作用域、agent 和驱动器。它们等待 `setup`,准入最终的会话和 agent 条目,按序公告,启动循环,然后才返回 handle。
|
||||
|
||||
可选的创建信号仅在 create 或 resume 挂起期间取消工作。promise resolve 后,返回的 `AgentHandle` 拥有显式 dispose 权。
|
||||
可选的创建信号仅在创建或恢复挂起期间取消工作。promise resolve 后,返回的 `AgentHandle` 拥有显式 dispose 权。
|
||||
|
||||
如果加载、setup、准入或发布失败,私有事务回滚其准备的一切。使用同一个调用方提供的存活 ID 的并发操作可能都到达 setup,但最终注册表条目只准入一个;每个失败者拒绝并清理其私有资源。在等待 dispose 完成后的顺序复用仍然有效。
|
||||
|
||||
`AgentHandle.dispose()` 反转边界。它停用创建或驱动,等待同步发布解除,停止并排空驱动器和最终会话刷写,分离 agent 和会话,最后 dispose 作用域。重复或竞争的 dispose 请求合并为一个完成 promise。
|
||||
|
||||
调用方的 Cordis context 和具体的 AgentLoop 工厂是结构性共同所有者。卸载任一方都会 dispose 事务或存活 agent。
|
||||
调用方的 Cordis 上下文和具体的 AgentLoop 工厂是结构性共同所有者。卸载任一方都会 dispose 事务或存活 agent。
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
@@ -140,7 +140,7 @@ flowchart TB
|
||||
|
||||
agent 作用域组合的是受信的同进程注册。它不沙箱化插件、不定义父到子的权限格、不在创建时冻结授权、也不保证子级不能做超出父级的事。
|
||||
|
||||
父级可以拥有一个可见工具比自身更广的子级,因为生命周期所有权不赠予也不限制注册。持有 Cordis context 的插件同样运行在同一进程中,可以直接调用可用服务。
|
||||
父级可以拥有一个可见工具比自身更广的子级,因为生命周期所有权不赠予也不限制注册。持有 Cordis 上下文的插件同样运行在同一进程中,可以直接调用可用服务。
|
||||
|
||||
需要非升权保证的部署需要独立的权限表示、传播规则和执行检查。父集合授权、创建时授权快照、显式未来授权 API,以及通用的能力/输出/终止标签均不在本决策范围内。
|
||||
|
||||
@@ -166,6 +166,6 @@ agent 作用域组合的是受信的同进程注册。它不沙箱化插件、
|
||||
|
||||
## 后果
|
||||
|
||||
贡献者使用一种熟悉的模式:通过插件 context 注册共享行为,通过 `agent.ctx` 注册本地行为,在操作中选择真实 agent,dispose 返回的 handle。从观察者角度看 setup 是原子的,拆除则保留本地行为直到工作停止。
|
||||
贡献者使用一种熟悉的模式:通过插件上下文注册共享行为,通过 `agent.ctx` 注册本地行为,在操作中选择真实 agent,dispose 返回的 handle。从观察者角度看 setup 是原子的,拆除则保留本地行为直到工作停止。
|
||||
|
||||
代价是显式的主体选择、异步的编程式创建,以及服务需要逐个采纳作用域。扁平注册作用域有意不等同于权限,subagent 组合控制作为独立功能存在,而非隐藏的作用域语义。
|
||||
|
||||
@@ -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-agent-scope-runtime-design.md: cf42bbacbfb9d2fcbd72aafba5ea02ddf83e1ce5
|
||||
2026-07-12-agent-scope-runtime-design.zh.md: 56a3d3b47d651115c6f5f215849712607ae41bca
|
||||
2026-07-12-agent-scope-runtime-design.zh.md: 3d19aa42a7475f01070fbe375b386e782b5f81b7
|
||||
|
||||
@@ -14,7 +14,7 @@ Status: implemented
|
||||
|
||||
## 决策
|
||||
|
||||
运行时对每个独立事实使用一种机制。作用域路由有一个不透明载体与共享 layer store;每个活跃的注册表对象有一条注册表条目;每个创建或恢复操作有一个事务;类型化的同进程调用借用 readonly 值;真实数据边界只物化一次;协作式 prompt 组装的结果即为权威;worker/进程代码仅在不同所有者确实可能竞争时才保留独立的终止态和完全停稳态。
|
||||
运行时对每个独立事实使用一种机制。作用域路由有一个不透明载体与共享 layer store;每个活跃的注册表对象有一条注册表条目;每个创建或恢复操作有一个事务;类型化的同进程调用借用 readonly 值;真实数据边界只物化一次;协作式提示词组装的结果即为权威;worker/进程代码仅在不同所有者确实可能竞争时才保留独立的终止态和完全停稳态。
|
||||
|
||||
该设计可概括为七项选择:
|
||||
|
||||
@@ -25,26 +25,26 @@ Status: implemented
|
||||
| 协调创建/恢复 | 单个 `AgentCreationTransaction` |
|
||||
| 保护持久化、队列、模型或协议格式数据 | 在该边界处一次性物化 |
|
||||
| 在同一进程内传递类型化值 | Readonly 借用契约 |
|
||||
| 组合模型可见的 prompt 与工具表面 | 单个共享工具视图加权威的 assembly-waterfall 结果 |
|
||||
| 组合模型可见的提示词与工具表面 | 单个共享工具视图加权威的 assembly-waterfall 结果 |
|
||||
| 协调 subagent、worker 和进程关闭 | 单个取消信号加该边界独立的终止态/完全停稳态事实 |
|
||||
|
||||
本 Agent Note 余下部分按依赖顺序展开这些选择:Cordis 机制、作用域路由、创建与会话提交、工具与 prompt、subagent 与工作流,最后是可执行检查。
|
||||
本 Agent Note 余下部分按依赖顺序展开这些选择:Cordis 机制、作用域路由、创建与会话提交、工具与提示词、subagent 与工作流,最后是可执行检查。
|
||||
|
||||
[7 月 8 日 Agent Note](2026-07-08-agent-scope-contexts.md)仍然是贡献者契约。独立的 [subagent 组合控制 Agent Note](../feature/2026-07-12-subagent-persona-tool-filter-and-depth.md)拥有 `persona`、`toolFilter` 和 `maxDepth`;本文仅讨论它们的 setup 如何融入生命周期。
|
||||
|
||||
## Cordis 模型:context、fiber、effect、receiver 与 waterfall
|
||||
## Cordis 模型:上下文、fiber、effect、receiver 与 waterfall
|
||||
|
||||
理解实现需要五个 Cordis 概念。Context 选择服务和注册所有权;fiber 是一个活跃的插件或子生命周期;effect 将清理逻辑附加到 fiber;event receiver 选择监听器;waterfall(瀑布式事件)让监听器按顺序变换或否决一个操作。
|
||||
理解实现需要五个 Cordis 概念。上下文选择服务和注册所有权;fiber 是一个活跃的插件或子生命周期;effect 将清理逻辑附加到 fiber;事件接收器选择监听器;waterfall(瀑布式事件)让监听器按顺序变换或否决一个操作。
|
||||
|
||||
### Context 是贯穿单个服务图的所有权路径
|
||||
### 上下文是贯穿单个服务图的所有权路径
|
||||
|
||||
所有 agent 共享一个 Cordis 服务图。派生的 context 不会克隆 `ToolRegistry`、`SystemPrompt`、持久化或模型适配器;它改变的是:通过该 context 进行的注册如何被标记,以及哪些 effect 拥有其清理逻辑。
|
||||
所有 agent 共享一个 Cordis 服务图。派生的上下文不会克隆 `ToolRegistry`、`SystemPrompt`、持久化或模型适配器;它改变的是:通过该上下文进行的注册如何被标记,以及哪些 effect 拥有其清理逻辑。
|
||||
|
||||
`agent.ctx` 就是这样一个派生 context。服务调用仍然到达共享实例,而注册操作可以检查其调用 context 并将贡献存储在最近的作用域键下。普通的插件 context 不携带作用域键,因此注册到全局。
|
||||
`agent.ctx` 就是这样一个派生上下文。服务调用仍然到达共享实例,而注册操作可以检查其调用上下文并将贡献存储在最近的作用域键下。普通的插件上下文不携带作用域键,因此注册到全局。
|
||||
|
||||
### Fiber 与 effect 使清理成为结构性的
|
||||
|
||||
Cordis fiber 是插件或子 context 被激活时创建的活跃实例。其状态记录该生命周期是 active、unloading、failed 还是 disposed。`ctx.effect()` 和 `ctx.on()` 返回 disposer,同时将这些 disposer 附加到注册所在的 fiber,因此卸载一个插件或 agent 作用域会移除通过该 context 注册的一切,无需单独的清单。
|
||||
Cordis fiber 是插件或子上下文被激活时创建的活跃实例。其状态记录该生命周期是 active、unloading、failed 还是 disposed。`ctx.effect()` 和 `ctx.on()` 返回 disposer,同时将这些 disposer 附加到注册所在的 fiber,因此卸载一个插件或 agent 作用域会移除通过该上下文注册的一切,无需单独的清单。
|
||||
|
||||
vendor 中的 Cordis fiber 实现在任意 setup 或 `internal/plugin` 观察者运行之前就建立了所有权。可重入的卸载可以看到已启动的子 fiber 或 effect,拒绝卸载开始后添加的 effect,并通过一个公开的一次性 disposer 加入已启动的清理。拆除观察者被逐个隔离,因此一个回调无法阻止结构性清理。
|
||||
|
||||
@@ -56,7 +56,7 @@ Cordis 使用 dispatch receiver(`this`)过滤监听器,而 harness 的监
|
||||
|
||||
因此,产品辅助函数构造载体并单独传递领域主体。这防止监听器路由变成另一套对象模型,并使事件签名在不了解载体内部的情况下也可理解。
|
||||
|
||||
Cordis waterfall 是中间件风格的 dispatch。每个监听器接收 `next()`:调用它则委托给剩余监听器和基础操作,不调用则否决或替换下游结果。Waterfall 驱动 prompt 组装和工具策略;普通 emit 事件同步通知,parallel 事件等待所有监听器但没有否决结果。
|
||||
Cordis waterfall 是中间件风格的 dispatch。每个监听器接收 `next()`:调用它则委托给剩余监听器和基础操作,不调用则否决或替换下游结果。Waterfall 驱动提示词组装和工具策略;普通 emit 事件同步通知,parallel 事件等待所有监听器但没有否决结果。
|
||||
|
||||
## 作用域路由:一个不透明键选择一层
|
||||
|
||||
@@ -66,19 +66,19 @@ scope 包实现了 Cordis 路由所需的最小对象。其载体仅持有一个
|
||||
|
||||
`ScopeKey` 是一个按标识比较的不透明对象。Harness 使用活跃的 `Agent` 作为自身的键,但该原语与领域无关,支持其他作用域所有者。
|
||||
|
||||
`createScope(parent, key)` 返回一个作用域,其 `ctx` 共享父级的服务,其 effect 被标记为该键。`scopeOf(ctx)` 读取最近的注册键。`scopeTarget(base, key)` 创建 event receiver,其过滤器保留 base receiver 的 Cordis 服务过滤器,然后接纳无作用域的监听器和具有该确切键的监听器。
|
||||
`createScope(parent, key)` 返回一个作用域,其 `ctx` 共享父级的服务,其 effect 被标记为该键。`scopeOf(ctx)` 读取最近的注册键。`scopeTarget(base, key)` 创建事件接收器,其过滤器保留 base receiver 的 Cordis 服务过滤器,然后接纳无作用域的监听器和具有该确切键的监听器。
|
||||
|
||||
Receiver 是一个小型载体而非领域对象的透明代理。需要 agent 的代码接收显式的事件参数;需要注册所有权的代码接收 `agent.ctx`。
|
||||
|
||||
### 注册表读取叠加一个精确 layer
|
||||
|
||||
作用域感知的注册表使用 `ScopedLayers`,拥有一个即时创建的全局 aggregate 和按标识键惰性创建的 aggregate。读取解析全局 layer 和至多一个精确局部 layer;它不创建状态,也从不遍历父级链。注册可见性与 Cordis effect 所有权都从同一个 context 派生,而回收会等待具体 layer 的完整 aggregate 变空(见[决策](2026-07-12-scoped-layers-store.md))。
|
||||
作用域感知的注册表使用 `ScopedLayers`,拥有一个即时创建的全局 aggregate 和按标识键惰性创建的 aggregate。读取解析全局 layer 和至多一个精确局部 layer;它不创建状态,也从不遍历父级链。注册可见性与 Cordis effect 所有权都从同一个上下文派生,而回收会等待具体 layer 的完整 aggregate 变空(见[决策](2026-07-12-scoped-layers-store.md))。
|
||||
|
||||
每个服务保留其领域规则。命名 command 和 prompt 视图使用共享的、保持插入顺序的 shadow merge;工具保留更丰富的 resolver,因为限制会在加入局部工具前过滤全局工具,保留的 Code Mode transport 则单独插入。Prompt 变量和工具 guard 保持实时迭代,而工具提供方成员关系按每次 assembly 物化。Scope 提供存储生命周期和命名遮蔽,而非通用的注册表视图。
|
||||
每个服务保留其领域规则。命名 command 和提示词视图使用共享的、保持插入顺序的 shadow 合并;工具保留更丰富的 resolver,因为限制会在加入局部工具前过滤全局工具,保留的 Code Mode transport 则单独插入。提示词变量和工具 guard 保持实时迭代,而工具提供方成员关系按每次 assembly 物化。Scope 提供存储生命周期和命名遮蔽,而非通用的注册表视图。
|
||||
|
||||
### 融合 dispatch 辅助函数防止主体漂移
|
||||
|
||||
`agentEvents(context, agent)` 构造 agent 的载体并注入同一个 agent 作为事件主体。Session、tool、approval、prompt 和 subagent 服务同样从它们已拥有的对象派生路由,而非接受一个无关的键。
|
||||
`agentEvents(context, agent)` 构造 agent 的载体并注入同一个 agent 作为事件主体。会话、工具、approval、提示词和 subagent 服务同样从它们已拥有的对象派生路由,而非接受一个无关的键。
|
||||
|
||||
类型标记拒绝普通的裸 receiver 误用,开发环境不变式覆盖直接 JavaScript 或强制转换的 dispatch。主体保持显式,因为路由正确性和有用的事件数据是不同的关注点。
|
||||
|
||||
@@ -96,7 +96,7 @@ detach 闭包捕获其确切注册表条目。它仅在映射仍指向该注册
|
||||
|
||||
### 事务在等待之前就拥有准备工作
|
||||
|
||||
事务在持久化加载或 setup 可能挂起之前,就被安装到调用方的 Cordis context 和具体的 AgentLoop 工厂下。它还在公开操作结算之前观察可选的创建/恢复信号。
|
||||
事务在持久化加载或 setup 可能挂起之前,就被安装到调用方的 Cordis 上下文和具体的 AgentLoop 工厂下。它还在公开操作结算之前观察可选的创建/恢复信号。
|
||||
|
||||
创建准备一个新 Session。恢复加载并验证持久化的 Session,然后准备相同的活跃会话标识。两条路径随后构建作用域、agent 和 driver,并调用相同的 setup/发布算法。
|
||||
|
||||
@@ -104,7 +104,7 @@ detach 闭包捕获其确切注册表条目。它仅在映射仍指向该注册
|
||||
|
||||
### Setup 是私有世界内的可信组合
|
||||
|
||||
Setup 接收完整的子 context,可以等待插件激活。它可以注册工具、prompt 段、限制、监听器和其他 effect,但公开契约不支持通过强制转换或内部注册表调用来驱动或发布正在创建中的 agent。
|
||||
Setup 接收完整的子上下文,可以等待插件激活。它可以注册工具、提示词段、限制、监听器和其他 effect,但公开契约不支持通过强制转换或内部注册表调用来驱动或发布正在创建中的 agent。
|
||||
|
||||
事务将异步加载和 setup 与停用进行竞争,而非无限等待外部代码拥有的 promise。如果取消或所有者卸载获胜,即使外部 promise 永不结算,公开创建也会在事务拥有的清理之后拒绝。
|
||||
|
||||
@@ -112,7 +112,7 @@ Setup 接收完整的子 context,可以等待插件激活。它可以注册工
|
||||
|
||||
发布按观察者所需的顺序接纳和宣告资源:
|
||||
|
||||
1. 将 session 写入注册表。
|
||||
1. 将会话写入注册表。
|
||||
2. 将 agent 写入注册表。
|
||||
3. 宣告 `session/created`。
|
||||
4. 宣告 `agent/created`。
|
||||
@@ -152,11 +152,11 @@ sequenceDiagram
|
||||
1. 停用创建或驱动,让同步发布完成。
|
||||
2. 停止并排空 driver,包括空闲注入刷新。
|
||||
3. 分离 agent。
|
||||
4. 分离 session。
|
||||
4. 分离会话。
|
||||
5. Dispose agent 作用域。
|
||||
6. 退役事务所有权追踪。
|
||||
|
||||
此顺序让最终的 agent 和 session 事件能使用匹配的作用域监听器,并使持久化观察者在最终刷新完成前保持附加。作用域 dispose 放在最后,因为注册撤销是外部可见的生命期边界。
|
||||
此顺序让最终的 agent 和会话事件能使用匹配的作用域监听器,并使持久化观察者在最终刷新完成前保持附加。作用域 dispose 放在最后,因为注册撤销是外部可见的生命期边界。
|
||||
|
||||
## 会话追加:物化、验证、提交、通知
|
||||
|
||||
@@ -200,9 +200,9 @@ Session 头部、种子和追加的事件是无损 JSON 数据。Session 构造
|
||||
|
||||
回调隔离与数据所有权是分开的。监听器是任意扩展代码,即使其参数是可信的也可能抛出异常;发布和提交后路径仍按其事件契约隔离失败。
|
||||
|
||||
## 工具与 prompt:单一视图、权威组装、已提交的结果
|
||||
## 工具与提示词:单一视图、权威组装、已提交的结果
|
||||
|
||||
工具展示和执行共享一个私有解析器。Prompt 组装仍然是可信的协作式组合:注册表提供有序输入,assembly waterfall 的返回值就是 agent loop(智能体循环)记录和发送的内容。执行仅在策略或结果结算必须单调时才使用独立的单向边界。
|
||||
工具展示和执行共享一个私有解析器。提示词组装仍然是可信的协作式组合:注册表提供有序输入,assembly waterfall 的返回值就是 agent loop(智能体循环)记录和发送的内容。执行仅在策略或结果结算必须单调时才使用独立的单向边界。
|
||||
|
||||
### 一个解析器定义工具视图
|
||||
|
||||
@@ -224,7 +224,7 @@ Session 头部、种子和追加的事件是无损 JSON 数据。Session 构造
|
||||
|
||||
### Assembly waterfall 拥有最终的模型可见组合
|
||||
|
||||
SystemPrompt 首先将全局加 agent 的段、变量和工具提供方解析为确定性的注册表贡献。作用域过滤的 `system-prompt/assemble` waterfall 随后可以重排、替换、添加或移除任何段、变量或 schema。其返回的组装结果即为权威;没有后续的恢复步骤,普通 prompt 段、工具定义或提供方结果上也没有终态元数据。
|
||||
SystemPrompt 首先将全局加 agent 的段、变量和工具提供方解析为确定性的注册表贡献。作用域过滤的 `system-prompt/assemble` waterfall 随后可以重排、替换、添加或移除任何段、变量或 schema。其返回的组装结果即为权威;没有后续的恢复步骤,普通提示词段、工具定义或提供方结果上也没有终态元数据。
|
||||
|
||||
这是一个可信的同进程扩展 seam,而非权限边界。修改 Code Mode 的 `run_code` schema 或 `tools:sdk` 指令,或结构化子级的捕获 schema 或指令的监听器,有责任在其返回的组装中保持协议的一致性。ToolRegistry 仍然保留 `run_code` 不受普通工具注册和限制影响,因为那些是注册表不变式,但 assembly 中间件仍然可以自由变换最终的模型可见表面。
|
||||
|
||||
@@ -244,7 +244,7 @@ Scope 直接解决了真正的隔离问题。结构化输出贡献注册在子
|
||||
|
||||
### 三个执行边界有意设为单向
|
||||
|
||||
Prompt 组装有意是协作式的,但三个执行事实在其可扩展阶段之后需要单向结算:
|
||||
提示词组装有意是协作式的,但三个执行事实在其可扩展阶段之后需要单向结算:
|
||||
|
||||
| 边界 | 最终权力 | 为何普通监听器顺序不够 |
|
||||
|---|---|---|
|
||||
@@ -286,7 +286,7 @@ Spawn 使用空会话种子。Fork 使用经验证的已完成轮次前缀。对
|
||||
|
||||
ACP 提供方跨越真实的进程和协议格式边界,因此它保留验证、环境清洗、消息序列化、abort/进程竞争,以及从 kill 到进程退出并完全停稳的过程。
|
||||
|
||||
Start 仅在 `initialize` 和 `newSession` 成功后才 resolve。Abort、spawn 失败、RPC 失败或无效启动响应在拒绝前回收进程。就绪后,result 映射 ACP prompt 结果和流式输出;dispose 请求取消、关闭连接并通过一条记忆化路径等待进程退出。
|
||||
Start 仅在 `initialize` 和 `newSession` 成功后才 resolve。Abort、spawn 失败、RPC 失败或无效启动响应在拒绝前回收进程。就绪后,result 映射 ACP 提示词结果和流式输出;dispose 请求取消、关闭连接并通过一条记忆化路径等待进程退出。
|
||||
|
||||
## 工作流与 ACP UI:仅保留独立的异步事实
|
||||
|
||||
@@ -306,11 +306,11 @@ Worker 边界仍然序列化请求和结果。宿主保留首个终端结果仲
|
||||
|
||||
公开 dispose 在调用回调之前取得其记忆化 promise 的所有权。Worker 死亡在处理任何排队的迟到子级请求之前关闭准入,合成缺失的生命周期结束,并启动子级/进程清理而不重写已声明的结果。
|
||||
|
||||
### ACP prompt 结算不依赖渲染成功
|
||||
### ACP 提示词结算不依赖渲染成功
|
||||
|
||||
ACP UI 直接将 prompt 与其观察到的轮次关联。它不从 `logWatermark` 扫描,也不使用会话状态作为第二个调和预言机。
|
||||
ACP UI 直接将提示词与其观察到的轮次关联。它不从 `logWatermark` 扫描,也不使用会话状态作为第二个调和预言机。
|
||||
|
||||
Prompt 处理在 transcript(文本记录)渲染的 `finally` 中结算关联。渲染失败可以导致展示失败,但不能跳过 prompt 结算或让会话永久处于进行中状态。对同一持久化的调用方提供的会话 ID 的并发加载仍被排除,因为那是真实的持久化标识竞争,而非 UUID 碰撞问题。
|
||||
提示词处理在 transcript(文本记录)渲染的 `finally` 中结算关联。渲染失败可以导致展示失败,但不能跳过提示词结算或让会话永久处于进行中状态。对同一持久化的调用方提供的会话 ID 的并发加载仍被排除,因为那是真实的持久化标识竞争,而非 UUID 碰撞问题。
|
||||
|
||||
## 正确性强制
|
||||
|
||||
@@ -318,7 +318,7 @@ Prompt 处理在 transcript(文本记录)渲染的 `finally` 中结算关联
|
||||
|
||||
### 类型使常规路径难以误用
|
||||
|
||||
Readonly 契约描述借用的同进程值。`Scoped<T>` 标记 event receiver,`agentEvents()` 融合载体和主体,工具输入省略注册表拥有的 token,subagent 异步返回类型直接暴露就绪性。
|
||||
Readonly 契约描述借用的同进程值。`Scoped<T>` 标记事件接收器,`agentEvents()` 融合载体和主体,工具输入省略注册表拥有的 token,subagent 异步返回类型直接暴露就绪性。
|
||||
|
||||
TypeScript 无法管控 JavaScript 强制转换、直接 Cordis dispatch、进程消息或持久化文件,因此运行时强制保留在这些逃逸点。
|
||||
|
||||
@@ -326,13 +326,13 @@ TypeScript 无法管控 JavaScript 强制转换、直接 Cordis dispatch、进
|
||||
|
||||
`dsh-scope/invariant` 配套插件在被选用时验证每个声明的作用域事件使用带标记的载体,以及暴露主体的事件族使用匹配的键。独立的 `dsh-session/invariant` 贡献在追加提交前暂存 trace 验证,并在同一事件提交后推进;二者都通过 `ctx.invariants` 注册。
|
||||
|
||||
该插件不通过扫描注册表来管控可信 setup,也不拒绝通过强制转换构造的 prompt assembly 对象。这些检查会将组合契约变成推测性的运行时机制,却不保护真实的外部边界。
|
||||
该插件不通过扫描注册表来管控可信 setup,也不拒绝通过强制转换构造的提示词 assembly 对象。这些检查会将组合契约变成推测性的运行时机制,却不保护真实的外部边界。
|
||||
|
||||
### 生成的产物使公开契约保持对齐
|
||||
|
||||
事件目录、服务目录、生产者/消费方矩阵、配置目录、模块图、工具目录、type-equiv 块和作用域事件解析器映射都是从源码生成或受新鲜度门禁约束的。[TypeScript 语义门禁 Agent Note](../process/2026-07-14-typescript-program-backed-semantic-gates.md)拥有 Program 构造、语义事件发现和解析器生成规则。
|
||||
|
||||
行为测试固定了作用域路由和 dispose、最终写入注册表时的碰撞清理、发布回滚、有序完全停稳、持久化前/后提交行为、跨展示和执行的活跃工具过滤、协作式 prompt 组装、原生和 Code Mode 中的结构化输出提交、异步 subagent 启动和信号取消、worker 终端仲裁、ACP 结算和进程拆除。
|
||||
行为测试固定了作用域路由和 dispose、最终写入注册表时的碰撞清理、发布回滚、有序完全停稳、持久化前/后提交行为、跨展示和执行的活跃工具过滤、协作式提示词组装、原生和 Code Mode 中的结构化输出提交、异步 subagent 启动和信号取消、worker 终端仲裁、ACP 结算和进程拆除。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
@@ -342,7 +342,7 @@ TypeScript 无法管控 JavaScript 强制转换、直接 Cordis dispatch、进
|
||||
|
||||
模拟主体的代理必须保持属性、可调用、可构造、私有字段、描述符和代理不变式行为,而监听器路由从不需要这些。一个小型不透明载体保持过滤器和键,而显式事件参数携带主体。
|
||||
|
||||
### 在 setup 前预留 agent 和 session ID
|
||||
### 在 setup 前预留 agent 和会话 ID
|
||||
|
||||
预留防止重复的私有 setup 工作,但需要跨服务能力、释放排序、废弃预留清理和已准备对象绑定。ID 由调用方提供,并发复用是调用方错误;最终写入注册表时可以选择赢家,而失败的事务干净地回滚。
|
||||
|
||||
@@ -358,7 +358,7 @@ TypeScript 无法管控 JavaScript 强制转换、直接 Cordis dispatch、进
|
||||
|
||||
这将提供方接受与就绪分离,迫使每个消费方注册部分 run、附加结果观察、等待就绪并清理就绪失败。异步 start promise 使提供方到调用方的所有权转移本身成为就绪边界。
|
||||
|
||||
### 在 assembly 之后恢复选定的 prompt 或工具贡献
|
||||
### 在 assembly 之后恢复选定的提示词或工具贡献
|
||||
|
||||
Waterfall 之后的恢复步骤会在文档化的协作式 seam 之后创建第二套组合规则。正确分配规范的存在或缺失还需要为任意工具 schema 提供方制定所有权和碰撞规则,而这些提供方的普通输出可能包含重复名称。作用域注册已经提供了所需的按 agent 隔离,可信的 assembly 监听器拥有其返回内容的协议一致性,因此命名恢复增加了机制却不建立独立边界。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user