docs(i18n): re-translate cds/postmortem batch with the prompt-v4 pipeline
22 篇(core-data-structures 18、postmortem 3、rfc/README)译文按 v4 基线重出;机械核对零异常;rfc/README.zh 页内锚点按门禁规则改回 英文侧锚名。
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
|
||||
approval.md: 772582955145092f3d483c297f7704b2b8506375
|
||||
approval.zh.md: dc45e1c6969099a2b285b4da071153510356acce
|
||||
approval.zh.md: 706613b84784d81cf112622059c755e815c23f16
|
||||
@@ -2,19 +2,19 @@
|
||||
|
||||
[English](approval.md) | 中文
|
||||
|
||||
[dsh-user-approval](../../packages/ui/user-approval) 的用户审批 seam 回答一个问题:这个具体操作是否可以继续?它拥有共享的请求/结果词汇、`ctx.approval` 分发服务、`approval/request` 应答者 waterfall(瀑布式事件)、仅记录日志的审计事件对,以及按会话的 `ask`/`never` 策略。UI 通道(如 [dsh-acp](../../packages/ui/acp))提供应答者;调用方(如 [dsh-tools](../../packages/core/tools) 和 [dsh-tool-bash](../../packages/bash/tool-bash))消费封闭的结果,并在结果不是 `allowed-once` 时默认拒绝。
|
||||
[dsh-user-approval](../../packages/ui/user-approval) 的用户审批 seam 回答一个问题:这个具体操作是否可以继续?它拥有共享的请求/结果词汇、`ctx.approval` 分发服务、`approval/request` 应答者 waterfall(瀑布式事件)、仅记录日志的审计事件对,以及按会话的 `ask`/`never` 策略。UI 通道如 [dsh-acp](../../packages/ui/acp) 提供应答者;调用方如 [dsh-tools](../../packages/core/tools) 和 [dsh-tool-bash](../../packages/bash/tool-bash) 消费闭合的结果,除非结果为 `allowed-once`,否则一律拒绝。
|
||||
|
||||
源码:[`packages/ui/user-approval/src/index.ts`](../../packages/ui/user-approval/src/index.ts)
|
||||
|
||||
## 标识与结果
|
||||
|
||||
每个请求获得一个新的 `ApprovalRequestId`。该品牌类型将 `approval/asked` 与 `approval/decided` 审计事件配对,同时防止审批 id 与工具调用、会话或 agent id 混用。
|
||||
每个请求获得一个全新的 `ApprovalRequestId`。该品牌类型将 `approval/asked` 和 `approval/decided` 审计事件配对,同时确保审批 id 不会与 tool-call、session 或 agent id 混用。
|
||||
|
||||
```ts type-equiv
|
||||
type ApprovalRequestId = Branded<'ApprovalRequestId'>
|
||||
```
|
||||
|
||||
`ApprovalOutcome` 是封闭的,且默认拒绝。`allowed-once` 仅授权被询问的那个操作;调用方在遇到 `rejected`、`cancelled` 和 `unavailable` 时一律拒绝。缺失的、不拥有该请求的、抛出异常的或不符合规范的应答者会产生 `unavailable`,而不是放行。
|
||||
`ApprovalOutcome` 是闭合的,且默认拒绝。`allowed-once` 仅授权所询问的那一个操作;调用方对 `rejected`、`cancelled` 和 `unavailable` 均执行拒绝。缺失、无所有权、抛异常或不合规的应答者会产生 `unavailable`,而非放行。
|
||||
|
||||
```ts type-equiv
|
||||
type ApprovalOutcome = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable'
|
||||
@@ -22,17 +22,17 @@ type ApprovalOutcome = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable'
|
||||
|
||||
## 按会话策略
|
||||
|
||||
`ApprovalPolicy` 决定在交互式应答者运行之前发生什么。`ask` 委托给组合的应答者链,其无应答默认值为 `unavailable`;`never` 确定性地返回 `rejected`,不分发任何应答者。生效值取会话日志中最后一条 `approval/policy` 事件,回退到服务配置。`setApprovalPolicy(session, policy)` 是唯一的写入路径,因此回放能重建覆盖值。
|
||||
`ApprovalPolicy` 决定在交互式应答者运行之前发生什么。`ask` 委托给组合的应答者链,链的无应答默认值为 `unavailable`;`never` 确定性地返回 `rejected`,不分发任何应答者。生效值为会话日志中最后一条 `approval/policy` 事件,回退到服务配置。`setApprovalPolicy(session, policy)` 是唯一的写入路径,因此回放能重建覆盖值。
|
||||
|
||||
```ts type-equiv
|
||||
type ApprovalPolicy = 'ask' | 'never'
|
||||
```
|
||||
|
||||
提示词段落会声明 `never` 的确定性行为,并用服务自有的标记记录当前策略。重启后,pre-step 叙述器从已记录的请求头中读取该标记;它不从部署 persona 行文中推断状态。ACP 中空闲时的策略切换会被桥接层持有到下一次 `turn/start`,因为审批审计事件和策略事件必须保持在轮次内,以确保持久回放的正确性。
|
||||
提示词段落会声明 `never` 的确定性行为,并以服务自有的标记记录当前策略。重启后,步骤前叙述器从已记录的请求头中读取该标记,而非从部署 persona 行文中推断状态。ACP 空闲切换会在 bridge 中保持,直到下一个 `turn/start`,因为审批审计和策略事件必须保持在轮次内,以确保持久回放的正确性。
|
||||
|
||||
## 审批请求
|
||||
|
||||
`ApprovalRequest` 足够精确地标识 agent 和工具操作,以便路由和审计该问题。它有意省略工具参数:应答者通过 `callId` 将提示附加到已流式输出的工具调用上,而不是渲染可能漂移的第二份副本。
|
||||
`ApprovalRequest` 以足够精确的方式标识 agent 和工具操作,以便路由和审计该问题。它有意省略工具参数:应答者通过 `callId` 将提示附加到已流式输出的工具调用上,而非渲染一份可能漂移的副本。
|
||||
|
||||
```ts type-equiv
|
||||
interface ApprovalRequest {
|
||||
@@ -61,6 +61,6 @@ interface ApprovalRequest {
|
||||
|
||||
## 分发与审计
|
||||
|
||||
`ctx.approval.request(req)` 要求发起请求的会话处于一个打开的轮次内。它追加 `approval/asked`,获取一个结果,追加匹配的 `approval/decided`,然后以该结果 resolve。`never` 策略在服务内部、waterfall 分发之前就已强制执行,因此即使后来用 `prepend` 注册的应答者也无法绕过它。应答者在拥有该请求时返回结果,否则调用 `next()` 委托;第一个应答占据唯一的决策槽位。
|
||||
`ctx.approval.request(req)` 要求发起请求的会话处于一个打开的轮次内。它追加 `approval/asked`,获取一个结果,追加对应的 `approval/decided`,然后以该结果 resolve。`never` 策略在服务内部、waterfall 分发之前强制执行,因此即使后来以 `prepend` 注册的应答者也无法绕过它。应答者在拥有该请求时返回结果,否则调用 `next()` 委托;第一个应答占据唯一的决策槽位。
|
||||
|
||||
审计事件仅记录日志,不进入模型 transcript(文本记录)。模型可见的行为是调用方派生的工具结果,而请求头记录的是模型实际看到的提示词策略。服务 dispose(资源释放)时会同时移除其提示词段落和 pre-step 叙述器;应答者监听器独立地通过 effect 绑定到其所属插件。
|
||||
审计事件仅写入日志,不进入模型 transcript(文本记录)。模型可见的行为是调用方派生的工具结果,而请求头记录的是模型实际看到的提示词策略。服务 dispose(资源释放)时会一并移除其提示词段落和步骤前叙述器;应答者监听器独立地通过 effect 绑定到其所属插件。
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
bash.md: 7b5c779b832ef5be6591e626980f7f22db54f239
|
||||
bash.zh.md: 519a7bf973fdf9fd9d7c4be2cc6ebf77e576135d
|
||||
bash.zh.md: 8a209b4853e44d1846460c41f4d5b9a43082a65b
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
[English](bash.md) | 中文
|
||||
|
||||
Bash 执行 seam:典型的[能力 seam](../rfc/implemented/architecture/2026-06-13-capability-seams.md) 示例,拆分为三个包(package):接口([dsh-bash](../../packages/bash/bash),`ctx.bash`)、实现([dsh-bash-local](../../packages/bash/bash-local),本地子进程)、消费方([dsh-tool-bash](../../packages/bash/tool-bash),`bash`/`bash_output`/`bash_kill` 工具 schema)。Bash 是**一项可选能力**,不属于 agent loop(智能体循环)主干,因此其词汇定义在此,而非 [core.md](core.md)。沙箱化、容器化或远程后端只需作为兄弟包实现同一接口。
|
||||
Bash 执行 seam:典型的[能力 seam](../rfc/implemented/architecture/2026-06-13-capability-seams.md) 示例,拆分为三个包(package):接口([dsh-bash](../../packages/bash/bash),`ctx.bash`)、实现([dsh-bash-local](../../packages/bash/bash-local),本地子进程)和消费方([dsh-tool-bash](../../packages/bash/tool-bash),`bash`/`bash_output`/`bash_kill` 工具 schema)。Bash 是**一项可选能力**,不属于 agent loop(智能体循环)主干,因此其词汇定义在此处而非 [core.md](core.md)。沙箱化、容器化或远程后端是实现同一接口的兄弟包。
|
||||
|
||||
源码:[`packages/bash/bash/src/types.ts`](../../packages/bash/bash/src/types.ts)
|
||||
|
||||
## 请求与规格:`resolve()` 拆分
|
||||
|
||||
该 seam 将**面向模型/插件的请求**(`workdir`/`timeoutMs` 可选,由配置填充)与**执行器实际执行的完全解析规格**(这些字段为必填)分开。工具层在二者之间调用 `ctx.bash.resolve(request)`。这是本仓库「包边界处显式优于隐式」规则的具体体现:读到一个 `BashExecSpec` 的人永远不必猜测工作目录从何而来。
|
||||
该 seam 将**面向模型/插件的请求**(`workdir`/`timeoutMs` 可选,由配置填充)与**执行器实际执行的完全解析规格**(这些字段为必填)分离。工具层在二者之间调用 `ctx.bash.resolve(request)`。这是本仓库「在包边界处显式优于隐式」规则的具体体现:阅读 `BashExecSpec` 的人永远不会疑惑工作目录从何而来。
|
||||
|
||||
```ts type-equiv
|
||||
interface BashExecRequest {
|
||||
@@ -108,15 +108,15 @@ interface BashExecSpec {
|
||||
}
|
||||
```
|
||||
|
||||
`owner` token 是隔离键:执行器存储它但从不解释它(访问策略是消费方的职责),因此一个 agent 启动的后台任务不会被跨会话读取。必填但可空的字段设计使得遗忘 owner 会表现为一个可见的 `undefined`,而非一个静默无主的任务。
|
||||
`owner` token 是隔离键:执行器存储它但从不解释它(访问策略是消费方的职责),因此一个 agent 启动的后台任务不会被跨会话读取。必填但可空的字段使遗忘的 owner 成为一个可见的 `undefined`,而非一个静默无主的任务。
|
||||
|
||||
受信的进程内插件使用 `stdin` 和 `env` 传递钩子载荷和钩子专用变量。面向模型的 bash 工具从其命名 schema 字段构造请求,不暴露这两个输入,因为 shell 语法已提供等价能力;测试会防止未来出现 `...args` 展开。这是请求形状纪律,而非安全边界:`dsh-bash-local` 无论这些字段如何都会清洗环境凭证,然后叠加调用方已持有的显式值。详见 [bash stdin/env RFC](../rfc/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md)。
|
||||
受信的进程内插件使用 `stdin` 和 `env` 传递钩子载荷与钩子专用变量。面向模型的 bash 工具从其命名的 schema 字段构造请求,不暴露这两个输入,因为 shell 语法本身已提供等价能力;测试防止未来出现 `...args` 展开。这是请求形状的纪律约束,而非安全边界:`dsh-bash-local` 无论这些字段如何都会清洗环境凭证,然后叠加调用方已持有的显式值。见 [bash stdin/env RFC](../rfc/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md)。
|
||||
|
||||
该 seam 处理的两个 id 都是[品牌化](core.md)的(零成本 `string` 品牌,与 `SessionId`/`AgentId` 同一套机制):`BashTaskId`(被追踪的后台任务,由本地执行器生成 `bash-N`)和 `OwnerToken`(不透明的隔离键)。`OwnerToken` 刻意是与 `SessionId` **不同**的品牌,而非别名:bash seam 是一个能力 seam,它不得知道 owner token *意味着什么*,因此从不导入 `dsh-session` 的词汇。将所属 agent 的 `SessionId` 转换为 `OwnerToken` 的唯一边界是 `dsh-tool-bash` 消费方。对两者都做品牌化,可以防止裸 `string`(或在需要 `OwnerToken` 的位置传入 `BashTaskId`,反之亦然)在面向模型的 `task_id` 路径上通过类型检查。
|
||||
该 seam 处理的两个 id 都是[品牌化的](core.md)(零成本 `string` 品牌,与 `SessionId`/`AgentId` 相同的机制):`BashTaskId`(被跟踪的后台任务,由本地执行器生成 `bash-N`)和 `OwnerToken`(不透明的隔离键)。`OwnerToken` 刻意是一个与 `SessionId` **不同**的品牌,而非别名:bash seam 是一个能力 seam,不得知道 owner token *意味着*什么,因此它从不导入 `dsh-session` 的词汇。`dsh-tool-bash` 消费方是唯一将拥有者 agent 的 `SessionId` 转换为 `OwnerToken` 的边界。对两者施加品牌化,可以防止裸 `string`(或在需要 `OwnerToken` 的地方传入 `BashTaskId`,反之亦然)在面向模型的 `task_id` 路径上通过类型检查。
|
||||
|
||||
## 前台运行:`BashRunResult`
|
||||
|
||||
一次已完成(或被终止)的前台运行的结果。正交的结果**独立报告**:一个进程可以既超时又以 exit 0 退出(因为它捕获了信号),因此 `timedOut`、`aborted`、`signal` 和 `exitCode` 各自独立为一个字段;调用方永远不会把一次被截断的运行误读为干净的成功。
|
||||
一次已完成(或被终止)的前台运行的结果。正交的结果**独立报告**:一个进程可以同时超时并以退出码 0 退出(因为它捕获了信号),因此 `timedOut`、`aborted`、`signal` 和 `exitCode` 各自独立为一个字段;调用方永远不会把一次被截断的运行误读为干净的成功。
|
||||
|
||||
```ts type-equiv
|
||||
interface BashRunResult {
|
||||
@@ -156,9 +156,9 @@ interface CollectedOutput {
|
||||
|
||||
## 文件沙箱:`BashSandboxInfo`
|
||||
|
||||
消费沙箱的执行器(`dsh-bash-sandbox`)通过 `BashExecutor.sandboxMode` 暴露其配置的回退模式。工具层折叠每个 agent 会话的持久 `bash/sandbox-mode` 覆盖,将生效模式盖章到请求上,并可能为一次用户批准的严格更宽调用替换它。工具层刻意不声明当前模式,也不叙述切换过程;拒绝结果会指明该命令实际运行时所处的模式。模式/强制词汇由 [`@deepseek-ai/dsh-sandbox` seam](sandbox.md) 拥有并编目,其提供方包装执行器的 argv;模式仅管辖文件效果,不管网络或进程可见性。
|
||||
消费沙箱的执行器(`dsh-bash-sandbox`)通过 `BashExecutor.sandboxMode` 暴露其配置的回退模式。工具层折叠每个 agent 会话的持久 `bash/sandbox-mode` 覆盖,将生效模式印到请求上,并可为一次用户批准的严格更宽调用替换它。它刻意既不声明当前模式也不叙述切换过程;拒绝结果会指明该命令实际运行时所处的模式。模式/执行词汇由 [`@deepseek-ai/dsh-sandbox` seam](sandbox.md) 拥有并编目,其提供方包装执行器的 argv;模式仅管控文件效果,不涉及网络或进程可见性。
|
||||
|
||||
沙箱化运行始终在 `BashRunResult.sandbox` 上报告其执行时的事实:`denied` 是执行器对「失败由沙箱引起」的保守分类(一次失败退出且 stderr 带有文件系统权限签名——从不是干净退出或信号终止),从收集的 stderr 尾部读取;`enforcement` 报告所选后端对该模式文件效果的治理完整度(`SandboxEnforcement = 'full' | 'partial'`——当较旧的 Landlock ABI 仅治理所请求访问的子集时为 `partial`;`danger-full-access` 下不存在,因为什么都没被限制);`runnerFailed` 标记与拒绝相反的情况——沙箱 runner 本身失败,命令从未运行(仅在已结算的后台任务上盖章;前台运行通过抛出 `SANDBOX_UNAVAILABLE` 错误暴露同一状况):
|
||||
沙箱化运行始终在 `BashRunResult.sandbox` 上报告其执行时的事实:`denied` 是执行器对失败的保守分类——判定为沙箱导致(退出失败且 stderr 携带文件系统权限特征——从不是干净退出或信号终止),从收集到的 stderr 尾部读取;`enforcement` 报告所选后端对该模式文件效果的管控完整程度(`SandboxEnforcement = 'full' | 'partial'`:`partial` 表示较旧的 Landlock ABI 仅管控所请求访问的子集;`danger-full-access` 下不存在此字段,因为没有任何限制);`runnerFailed` 标记与拒绝相反的情况——沙箱运行器本身失败、命令从未执行(仅在已结算的后台任务上标记;前台运行通过抛出 `SANDBOX_UNAVAILABLE` 错误暴露同一状况):
|
||||
|
||||
```ts type-equiv
|
||||
interface BashSandboxInfo {
|
||||
@@ -195,11 +195,11 @@ interface BashSandboxInfo {
|
||||
}
|
||||
```
|
||||
|
||||
还有一个词汇完成整幅图景:`SANDBOX_UNAVAILABLE` 错误码(由 [sandbox seam](sandbox.md) 拥有)是 `ctx.sandbox` 提供方在受限模式没有可用后端时抛出的——执行器将其传播。所选 runner 拒绝其 profile 也会到达同一个快速失败的前台错误;已结算的后台任务则记录 `runnerFailed`。模型在结果中收到拒绝/runner 事实,仅在拒绝标记指明模式时才得知生效模式,并可通过 `sandbox_permissions` 加 `justification` 请求一次严格更宽的重试;`ctx.approval` 必须在任何执行之前批准该确切调用。完整的策略与切换设计见 [sandbox RFC](../rfc/implemented/feature/2026-07-06-sandbox.md)。
|
||||
还有一个词汇完成整幅图景:`SANDBOX_UNAVAILABLE` 错误码(由 [sandbox seam](sandbox.md) 拥有)是 `ctx.sandbox` 提供方在受限模式没有可用后端时抛出的错误,执行器将其传播。所选运行器拒绝其 profile 时也触发同一快速失败的前台错误;已结算的后台任务则记录 `runnerFailed`。模型在结果中接收拒绝/运行器事实,仅在拒绝标记指明模式时才获知生效模式,并可通过 `sandbox_permissions` 加 `justification` 请求一次严格更宽的重试;`ctx.approval` 必须在任何执行之前批准该确切调用。完整的策略与切换设计见 [sandbox RFC](../rfc/implemented/feature/2026-07-06-sandbox.md)。
|
||||
|
||||
## 后台任务:`BashTask`
|
||||
|
||||
通过 `start()` 启动的长时间运行命令被追踪为 `BashTask`。`BashTaskStatus` 为 `'running' | 'completed' | 'killed'`;`done` 在底层进程关闭时 resolve,从不 reject。沙箱化执行器在任务结算后盖章 `sandbox`(分类针对已结算任务收集的 stderr 运行),因此该字段在运行中以及非沙箱化执行器下不存在。
|
||||
通过 `start()` 启动的长时间运行命令被跟踪为 `BashTask`。`BashTaskStatus` 为 `'running' | 'completed' | 'killed'`;`done` 在底层进程关闭时 resolve,从不 reject。沙箱化执行器在任务结算后标记 `sandbox`(分类针对已结算任务收集到的 stderr 运行),因此该字段在运行中以及非沙箱化执行器下不存在。
|
||||
|
||||
```ts type-equiv
|
||||
interface BashTask {
|
||||
@@ -224,7 +224,7 @@ interface BashTask {
|
||||
}
|
||||
```
|
||||
|
||||
`readOutput()` 返回增量的 `BashTaskRead`:自上次读取以来产生的输出,附带一个 `lossy` 标志表示截断丢弃了未读字节:
|
||||
`readOutput()` 返回增量的 `BashTaskRead`:自上次读取以来产生的输出,附带一个 `lossy` 标志指示截断是否丢弃了未读字节:
|
||||
|
||||
```ts type-equiv
|
||||
interface BashTaskRead {
|
||||
@@ -242,4 +242,4 @@ interface BashTaskRead {
|
||||
|
||||
## 服务
|
||||
|
||||
`BashExecutor`(`ctx.bash`,抽象——定义于 [`packages/bash/bash/src/index.ts`](../../packages/bash/bash/src/index.ts))镜像 `LlmService`/`LlmAdapter` 的拆分:`resolve`(请求→规格)、`run`(前台)、`start`(后台)、`get`/`ownerOf`/`list`/`readOutput`/`kill`,以及 `onTaskDone`(`BashTaskListener` 完成回调)。spawn 的命令获得一个**清洗后的 env**(丢弃 `*KEY*`/`*SECRET*`/`*TOKEN*`),溢出文件使用一个权限为 0700 的私有目录(随机文件名、仅所有者可打开)——模型输出永远拿不到宿主环境或可预测路径。提供这一切的实现是 `dsh-bash-local`;调用它的面向模型的 `bash`/`bash_output`/`bash_kill` schema 位于 `dsh-tool-bash`(并通过[工具呈现词汇](tools.md#tool-presentation-ui-vocabulary)以终端形式展示)。
|
||||
`BashExecutor`(`ctx.bash`,抽象——定义于 [`packages/bash/bash/src/index.ts`](../../packages/bash/bash/src/index.ts))遵循 `LlmService`/`LlmAdapter` 的拆分模式:`resolve`(请求→规格)、`run`(前台)、`start`(后台)、`get`/`ownerOf`/`list`/`readOutput`/`kill`,以及 `onTaskDone`(`BashTaskListener` 完成回调)。spawn 的命令获得一个**清洗后的 env**(丢弃 `*KEY*`/`*SECRET*`/`*TOKEN*`),溢出文件使用一个权限为 0700 的私有目录(随机文件名、仅所有者可打开)。模型输出永远不会获得环境变量或可预测路径。提供这一切的实现是 `dsh-bash-local`;调用它的面向模型的 `bash`/`bash_output`/`bash_kill` schema 位于 `dsh-tool-bash`(并通过[工具展示词汇](tools.md#tool-presentation-ui-vocabulary)作为终端呈现)。
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
code-runtime.md: 28152947d0853fb10228c472ca3e121e77b7b598
|
||||
code-runtime.zh.md: f12816fd5392f1efff3a1faeee232fb004142f37
|
||||
code-runtime.zh.md: 8270d12e7cce8c3b2443da9de268d95cf9d692d8
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
[English](code-runtime.md) | 中文
|
||||
|
||||
代码执行 seam:一个[能力 seam](../rfc/implemented/architecture/2026-06-13-capability-seams.md),其接口([dsh-code-runtime](../../packages/code-runtime/code-runtime),`ctx.codeRuntime`)负责运行一段模型编写的程序,对接宿主提供的异步绑定,并报告程序打印和返回的内容。代码执行是**一项可选能力**,不属于 agent loop(智能体循环)主干,因此其词汇定义在此处而非 [core.md](core.md)。后端因执行基底和源语言而异,二者均为服务上的只读描述符;worker-thread 后端与工具注册表消费方(Code Mode)在 [Code Mode RFC](../rfc/implemented/feature/2026-06-15-code-mode.md) 中规定。
|
||||
代码执行 seam:一个[能力 seam](../rfc/implemented/architecture/2026-06-13-capability-seams.md),其接口([dsh-code-runtime](../../packages/code-runtime/code-runtime),`ctx.codeRuntime`)运行一段模型编写的程序,对接宿主提供的异步绑定,并报告程序的打印输出与返回值。代码执行是**一项可选能力**,不属于 agent loop(智能体循环)主干,因此其词汇定义在此,而非 [core.md](core.md)。后端因执行基底和源语言而异,二者都是服务上的只读描述符;worker-thread 后端与工具注册表消费方(Code Mode)在 [Code Mode RFC](../rfc/implemented/feature/2026-06-15-code-mode.md) 中定义。
|
||||
|
||||
源码:[`packages/code-runtime/code-runtime/src/types.ts`](../../packages/code-runtime/code-runtime/src/types.ts)
|
||||
|
||||
## 运行:请求进,结果出
|
||||
|
||||
`CodeRunRequest` 携带**运行时所需的全部信息**。按照「包(package)seam 处显式优于隐式」的规则,默认值(时间预算、输出上限)由实现的已校验配置提供,绝不是 `run()` 内部隐藏的 `??`:
|
||||
`CodeRunRequest` 携带**运行时所需的一切**。按照「包(package)边界处显式优于隐式」的规则,默认值(时间预算、输出上限)来自实现的已校验配置,绝不是 `run()` 内部隐藏的 `??`:
|
||||
|
||||
```ts type-equiv
|
||||
interface CodeRunRequest {
|
||||
@@ -30,7 +30,7 @@ interface CodeRunRequest {
|
||||
}
|
||||
```
|
||||
|
||||
结果将错误报告为一个**字段**,而非 `run()` 的 rejection:报告程序失败是调用方的职责,不是异常路径(与 `BashExecutor.run` 的 resolve-on-failure 契约一致):
|
||||
结果将错误报告为一个**字段**,而非 `run()` 的 rejection。报告失败的程序是调用方的职责,不走异常路径(与 `BashExecutor.run` 的 resolve-on-failure 契约一致):
|
||||
|
||||
```ts type-equiv
|
||||
interface CodeRunResult {
|
||||
@@ -50,7 +50,7 @@ interface CodeRunResult {
|
||||
|
||||
## 绑定:宿主函数作为程序全局变量
|
||||
|
||||
每个 `CodeBindingNamespace` 在程序内部成为一个由异步可调用成员组成的全局对象(Code Mode 消费方传入一个:`tools`)。参数与解析值必须可 structured-clone:运行时可能跨序列化边界桥接调用。运行时将绑定名视为不可信输入(`__proto__` 是普通的 own property,绝不会产生原型碰撞):
|
||||
每个 `CodeBindingNamespace` 在程序内成为一个由异步可调用函数组成的全局对象(Code Mode 消费方传入一个:`tools`)。参数与返回值必须可 structured-clone(运行时可能跨序列化边界桥接调用),且运行时将绑定名视为不可信输入(`__proto__` 是普通自有属性,绝不会发生原型碰撞):
|
||||
|
||||
```ts type-equiv
|
||||
interface CodeBindingNamespace {
|
||||
@@ -67,9 +67,9 @@ type CodeBindingFunction = (args: unknown) => Promise<unknown>
|
||||
|
||||
## 捕获的输出与失败分类体系
|
||||
|
||||
日志是按发出顺序排列的纯字符串。运行时捕获程序的 console 和流输出,但通道与 console 方法的元数据不属于 seam 的一部分,因为消费方只渲染文本。实现对聚合输出设上限,并在输出内标记截断。
|
||||
日志是按发出顺序排列的纯字符串。运行时捕获程序的 console 与流输出,但通道和 console 方法的元数据不属于 seam 的一部分,因为消费方只渲染文本。实现对聚合输出设上限,并在输出内标记截断。
|
||||
|
||||
失败类型是**正交的结果,独立报告**(见 [defensive-patterns](../defensive-patterns.md)):预算耗尽不是异常,中止不是超时,基底崩溃(如 OOM)也不是二者之一:
|
||||
失败类型是**正交的结果,独立报告**(见 [defensive-patterns](../defensive-patterns.md)):预算耗尽不是异常,中止不是超时,基底崩溃(如 OOM)也不是二者中的任何一个:
|
||||
|
||||
```ts type-equiv
|
||||
interface CodeRunFailure {
|
||||
@@ -82,4 +82,4 @@ interface CodeRunFailure {
|
||||
|
||||
## 服务
|
||||
|
||||
`CodeRuntime`(`ctx.codeRuntime`,抽象服务,定义于 [`packages/code-runtime/code-runtime/src/index.ts`](../../packages/code-runtime/code-runtime/src/index.ts))是 `run(request)` 加两个只读描述符:`language`(程序必须使用的语言:`'typescript'` 是已知值;生成语言相关展示的消费方据此分支,遇到无法展示的语言时应显式报错)和 `isolation`(执行基底:`'worker-thread'`、`'process'`、`'container'`;是诊断标签,**不是安全承诺**)。实现必须保证各次运行彼此隔离(无跨运行状态),并在 dispose(资源释放)时达到静止状态:进行中的运行在 teardown 完成前被终止并 await。
|
||||
`CodeRuntime`(`ctx.codeRuntime`,抽象服务,定义于 [`packages/code-runtime/code-runtime/src/index.ts`](../../packages/code-runtime/code-runtime/src/index.ts))由 `run(request)` 加两个只读描述符组成:`language`(程序必须使用的语言,`'typescript'` 是已知值;生成语言相关展示的消费方据此切换,遇到无法展示的语言时应显式报错)和 `isolation`(执行基底,`'worker-thread'`、`'process'`、`'container'`;仅为诊断标签,**不构成安全承诺**)。实现必须保证各次运行彼此隔离(无跨运行状态),且 dispose(资源释放)至静默:进行中的运行在 teardown 完成前被终止并等待结束。
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
compaction.md: e82cc103932ad05e68bc5311dec23c3f2c1a7ce4
|
||||
compaction.zh.md: 889cdba416767e90592359361fc0f65bcb2472c3
|
||||
compaction.zh.md: ad8b524a1984357b5bb911b59300a194407bdbb9
|
||||
@@ -1,28 +1,28 @@
|
||||
# 压缩
|
||||
# 上下文压缩
|
||||
|
||||
[English](compaction.md) | 中文
|
||||
|
||||
压缩(compaction)seam 是一个[能力 seam](../rfc/implemented/architecture/2026-06-13-capability-seams.md),按 bash 模式拆分:接口([dsh-compact](../../packages/compact/compact),`ctx.compact`)、实现(后端,如 [dsh-compact-basic](../../packages/compact/compact-basic))、消费方(一个 `/compact` 工具,暂缓)。压缩是**一项可选能力**,不属于 agent loop(智能体循环)主干,因此其词汇定义在此,而非 [core.md](core.md)。基于 tokenizer 或模板的后端是实现同一接口的兄弟包。与 bash 不同的是,该接口必然依赖 `dsh-session` 和 `dsh-llm`:它的动词定义在 `Session` 之上,输出是 `ContentBlock` 词汇(见[压缩能力 seam RFC](../rfc/implemented/feature/2026-06-18-compaction-capability-seam.md))。
|
||||
上下文压缩(context compaction)的 seam 是一个[能力 seam](../rfc/implemented/architecture/2026-06-13-capability-seams.md),按 bash 式拆分:接口([dsh-compact](../../packages/compact/compact),`ctx.compact`)、实现(后端,如 [dsh-compact-basic](../../packages/compact/compact-basic))、消费方(一个 `/compact` 工具,暂缓实现)。上下文压缩是**一项可选能力**,不属于 agent loop(智能体循环)主干,因此其词汇定义在此处,而非 [core.md](core.md)。基于 tokenizer 或模板的后端是实现同一接口的兄弟包。与 bash 不同的是,该接口必然依赖 `dsh-session` 和 `dsh-llm`:它的动词定义在 `Session` 之上,输出使用 `ContentBlock` 词汇(见[上下文压缩能力 seam RFC](../rfc/implemented/feature/2026-06-18-compaction-capability-seam.md))。
|
||||
|
||||
源码:[`packages/compact/compact/src/types.ts`](../../packages/compact/compact/src/types.ts)
|
||||
|
||||
## `compact/*` 会话事件
|
||||
|
||||
压缩通过声明合并为 [`SessionEventMap`](session.md) 扩展了三种事件类型。三者均为**仅日志**事件:它们记录压缩锁及其来源信息,永远不进入 surface。`SurfaceEventType` 被刻意**不**扩展(只有产生消息的事件才到达模型),因此摘要本身搭载在一条单独的 `user/message` 上,带有 `surfaceOp: { op: 'replace', start, end }`——唯一的 surface 变更。关于为何复用 `user/message` 是诚实的做法而非变通手段,见 RFC。
|
||||
上下文压缩通过声明合并为 [`SessionEventMap`](session.md) 扩展了三种事件类型。三者均为**仅日志**事件:它们记录压缩锁及其来源信息,永远不进入 surface。`SurfaceEventType` 被刻意**不**扩展(只有产生消息的事件才到达模型),因此摘要本身搭载在一条独立的 `user/message` 上,带有 `surfaceOp: { op: 'replace', start, end }`——唯一的 surface 变更。关于为何复用 `user/message` 是诚实的做法而非权宜之计,见 RFC。
|
||||
|
||||
| 事件 | 载荷 | 作用 |
|
||||
|---|---|---|
|
||||
| `compact/start` | `{ turn }` | 获取日志记录的锁 |
|
||||
| `compact/summary` | `{ summary, shadowedRange, shadowedSeqs, shadowedTokenCount, model, maxTokens? }` | 来源信息:摘要块、被遮蔽的 surface 边界对(`start`/`end` seq——位置跨度,非数值区间)、按 surface 顺序排列的被遮蔽 seq、估算 token 数量,以及摘要调用的信封(`model`,加上生效时的生成上限)——记录下来以便从日志 + 代码重建一次性请求(可重建性 RFC) |
|
||||
| `compact/end` | `{ turn, error? }` | 释放锁(摘要生成抛出异常时设置 `error`) |
|
||||
| `compact/summary` | `{ summary, shadowedRange, shadowedSeqs, shadowedTokenCount, model, maxTokens? }` | 来源信息:摘要块、被遮蔽的 surface 边界对(`start`/`end` seq,是位置跨度而非数值区间)、按 surface 顺序排列的被遮蔽 seq、估算的 token 数量,以及摘要调用的信封(`model`,加上生效时的生成上限)。记录这些信息使得单次请求可从日志加代码重建(reconstructability RFC) |
|
||||
| `compact/end` | `{ turn, error? }` | 释放锁(摘要调用抛出异常时设置 `error`) |
|
||||
|
||||
锁括住**整个**操作:先追加 `compact/start`,然后执行摘要生成、落入 `compact/summary` 来源记录和 `user/message` 替换,最后才追加 `compact/end`。最后释放锁意味着操作中途崩溃会变成一个可检测的遗留锁(有 `compact/start` 而无匹配的 `compact/end`),而不是一个虚假声称压缩已完成的 `compact/end`。
|
||||
锁括住**整个**操作:先追加 `compact/start`,然后执行摘要生成、写入 `compact/summary` 来源记录与 `user/message` 替换,最后才追加 `compact/end`。最后释放锁意味着操作中途崩溃会表现为可检测的遗留锁(有 `compact/start` 而无匹配的 `compact/end`),而非一个虚假声称压缩已完成的 `compact/end`。
|
||||
|
||||
这些变体在 `declare module '@deepseek-ai/dsh-session'` 块内合并,因此——与其他子页面上的顶层类型不同——它们不以漂移检查的 ` ```ts type-equiv ` 块粘贴(`verify-type-equiv` 提取器只按名称匹配顶层声明)。上方的载荷表即为目录条目;权威形状请循源码链接查看。
|
||||
|
||||
## `CompactionResult`
|
||||
|
||||
一次成功的压缩返回给调用方的内容:三个追加的 `compact/*` 事件的 seq、摘要块,以及被遮蔽的范围/seq 加上估算 token 数量。
|
||||
一次成功的压缩返回给调用方的内容:三个追加的 `compact/*` 事件的 seq、摘要块,以及被遮蔽的范围/seq 和估算的 token 数量。
|
||||
|
||||
```ts type-equiv
|
||||
interface CompactionResult {
|
||||
@@ -52,6 +52,6 @@ interface CompactionResult {
|
||||
|
||||
## 服务
|
||||
|
||||
`CompactService` 暴露 `compactIfNeeded(...)` 用于压力触发的压缩(不需要压缩时返回 `null`),以及 `compactRegion(...)` 用于对显式的 surface 闭区间执行压缩。pre-step 调用方提供 agent、完整提示词、会话前缀和 abort signal;实现必须将该 signal 转发给摘要生成。估算、保留策略、事件排序和摘要生成均为后端策略。
|
||||
`CompactService` 暴露 `compactIfNeeded(...)` 用于压力触发的压缩(不需要压缩时返回 `null`),以及 `compactRegion(...)` 用于对显式的闭区间 surface 范围进行压缩。pre-step 调用方提供 agent、完整 prompt、会话前缀和 abort signal;实现必须将该 signal 转发给摘要生成。估算、保留策略、事件排序与摘要生成均为后端策略。
|
||||
|
||||
自动压缩在串行的 `agent/pre-step` 时运行,位于步骤和请求推导之前,因此它可以替换 surface 节点,同时将 trace 事件保持在步骤之外。区域边界保留工具调用/结果的配对,但不保留完整轮次,允许一个超大轮次中较早关闭的步骤被压缩。保留策略与失败处理的细节由 `dsh-compact-basic` 负责。
|
||||
自动压缩在串行的 `agent/pre-step` 时运行,位于步骤和请求推导之前,因此可以在替换 surface 节点的同时将 trace 事件保持在步骤之外。区域边界保持工具调用/结果配对,但不保持完整轮次,允许一个超大轮次中已关闭的早期步骤被压缩。保留策略与失败处理的细节由 `dsh-compact-basic` 负责。
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
filesystem.md: 8bdc2323a0bf63588e01520926f093538fee4912
|
||||
filesystem.zh.md: 93ca9b26e054cadb40382391404573c95a1c8d05
|
||||
filesystem.zh.md: 1b636dd35e3c614d87973c617ea052058995e0e0
|
||||
@@ -2,15 +2,15 @@
|
||||
|
||||
[English](filesystem.md) | 中文
|
||||
|
||||
可选的文件系统能力由四部分组成:[dsh-fs](../../packages/fs/fs) 拥有 `ctx.fs` 以及带可选版本守卫的原子文本操作,[dsh-fs-local](../../packages/fs/fs-local) 实现本地磁盘后端,[dsh-fs-policy](../../packages/fs/fs-policy) 通过事件(而非服务)添加观测状态与新鲜度规则,[dsh-tool-fs](../../packages/fs/tool-fs) 直接执行面向模型的 read/write/edit 调用并渲染窗口。它位于 agent loop 主干之外;替换后端不会改变策略或工具 schema。
|
||||
可选的文件系统能力由四个部分组成:[dsh-fs](../../packages/fs/fs) 拥有 `ctx.fs` 以及带可选版本守卫的原子文本操作;[dsh-fs-local](../../packages/fs/fs-local) 实现本地磁盘后端;[dsh-fs-policy](../../packages/fs/fs-policy) 通过事件(而非服务)添加观测状态与新鲜度规则;[dsh-tool-fs](../../packages/fs/tool-fs) 直接执行面向模型的 read/write/edit 调用并渲染窗口。它位于 agent loop(智能体循环)主干之外;替换后端不会改变策略或工具 schema。
|
||||
|
||||
该模型是**加法式而非减法式**的:`ctx.fs` 本身就是一个完整、无约束的文本存储 seam(`write` 无条件创建或覆盖,`edit` 无条件替换字面文本)。`dsh-fs-policy` 是一个在此之上*添加*策略的插件,通过裁决 `fs/*` waterfall(瀑布式事件)实现;移除它只会留下裸提供方,而不会破坏工具,因为工具与策略之间没有方法级耦合。加载了 `dsh-tool-fs` 的部署预期同时加载 `dsh-fs-policy`,使默认行为为先读后写/编辑。
|
||||
该模型是**加法式而非减法式**的:`ctx.fs` 本身就是一个完整、无约束的文本存储 seam(`write` 无条件创建或覆盖,`edit` 无条件替换字面文本)。`dsh-fs-policy` 是一个插件,通过裁决 `fs/*` waterfall(瀑布式事件)在上层*叠加*策略;移除它只会暴露裸提供方,而不会破坏工具,因为工具与策略之间没有方法级耦合。加载了 `dsh-tool-fs` 的部署通常也应加载 `dsh-fs-policy`,使默认行为为「先读后写/编辑」。
|
||||
|
||||
提供方源码:[`packages/fs/fs/src/types.ts`](../../packages/fs/fs/src/types.ts) 与 [`packages/fs/fs/src/index.ts`](../../packages/fs/fs/src/index.ts)。策略源码:[`packages/fs/fs-policy/src/types.ts`](../../packages/fs/fs-policy/src/types.ts)。读取渲染源码:[`packages/fs/tool-fs/src/read-render.ts`](../../packages/fs/tool-fs/src/read-render.ts)。
|
||||
|
||||
## 目标标识与元数据(提供方 seam)
|
||||
|
||||
每个操作首先将用户提供的路径解析为一个不透明的后端目标。消费方可以展示 `displayPath`,但不得解析 `targetKey`(一个品牌化的不透明 id),也不得假设它是本地绝对路径。
|
||||
每个操作首先将用户提供的路径解析为不透明的后端目标。消费方可以显示 `displayPath`,但禁止解析 `targetKey`(一个品牌化的不透明 id),也不得假设它是本地绝对路径。
|
||||
|
||||
```ts type-equiv
|
||||
interface FsTarget {
|
||||
@@ -19,7 +19,7 @@ interface FsTarget {
|
||||
}
|
||||
```
|
||||
|
||||
后端拥有文件版本 token:即 write/edit 所守卫的新鲜度 token。策略插件存储它们用于陈旧检查;消费方不解释其含义。两个 id 都是品牌化的不透明字符串。
|
||||
后端拥有文件版本 token,即 write/edit 所守卫的新鲜度 token。策略插件存储它们以进行陈旧检查;消费方不解释其内容。两个 id 都是品牌化的不透明字符串。
|
||||
|
||||
```ts type-equiv
|
||||
type FsTargetKey = Branded<'FsTargetKey'>
|
||||
@@ -29,7 +29,7 @@ type FsTargetKey = Branded<'FsTargetKey'>
|
||||
type FsVersion = Branded<'FsVersion'>
|
||||
```
|
||||
|
||||
`stat` 返回元数据(从不返回内容),目标不存在时返回 `undefined`。`type` 让工具在读取前拒绝目录/特殊文件,`size` 让工具无需通过失败探测即可选择 `readText` 还是 `streamText`。
|
||||
`stat` 返回元数据(从不返回内容),目标不存在时返回 `undefined`。`type` 让工具在读取前拒绝目录或特殊文件;`size` 让工具无需通过失败探测即可选择 `readText` 还是 `streamText`。
|
||||
|
||||
```ts type-equiv
|
||||
interface FsInfo {
|
||||
@@ -39,7 +39,7 @@ interface FsInfo {
|
||||
}
|
||||
```
|
||||
|
||||
`listDir` 以稳定的名称顺序返回直接子条目。每个条目携带子项的 basename、类型、已解析的目标,以及后端能廉价报告时的元数据。它不得读取文件内容,因此 `size` 仅适用于普通文件,`version` 来源于元数据。损坏或消失的子项可以作为 `other` 返回且不带元数据;列举或解析子项元数据时的权限或后端 I/O 失败会以 `FS_PERMISSION_DENIED` 或 `FS_IO_ERROR` 使整个列举失败。
|
||||
`listDir` 按稳定的名称顺序返回直接子条目。每个条目携带子项的 basename、类型、已解析目标,以及后端能报告时的廉价元数据。它禁止读取文件内容,因此 `size` 仅用于普通文件,`version` 来自元数据。已损坏或已消失的子项可以作为 `other` 返回且不带元数据;列出或解析子项元数据时的权限或后端 I/O 失败会以 `FS_PERMISSION_DENIED` 或 `FS_IO_ERROR` 使整个列表操作失败。
|
||||
|
||||
```ts type-equiv
|
||||
interface FsDirEntry {
|
||||
@@ -53,7 +53,7 @@ interface FsDirEntry {
|
||||
|
||||
## 写入与编辑守卫(提供方 seam)
|
||||
|
||||
`writeText` 和 `editText` 都以可选方式接受版本守卫:省略即为无条件(裸提供方)变更,提供即为守卫。`writeText` 的守卫是 `FsWriteIntent`:`createIfAbsent` 创建缺失的目标,若目标已存在则以 `FS_NOT_OBSERVED` 拒绝;`replaceIfVersion` 仅在目标存在且版本匹配时替换,否则报 `FS_STALE_VERSION`。省略 `expected` 则无条件创建或覆盖。联合类型本身只携带两种守卫意图;「无守卫」通过省略表达,因此 write 和 edit 共享同一个对称的 `expected?` 形状。
|
||||
`writeText` 和 `editText` 的版本守卫都是可选的:省略它执行无条件(裸提供方)变更,提供它则启用守卫。`writeText` 的守卫是 `FsWriteIntent`:`createIfAbsent` 在目标缺失时创建,目标已存在时以 `FS_NOT_OBSERVED` 拒绝;`replaceIfVersion` 仅在目标存在且版本匹配时替换,否则报 `FS_STALE_VERSION`。省略 `expected` 则无条件创建或覆盖。联合类型本身只包含两种有守卫的意图;「无守卫」通过省略表达,因此 write 和 edit 共享同一个对称的 `expected?` 形状。
|
||||
|
||||
```ts type-equiv
|
||||
type FsWriteIntent =
|
||||
@@ -70,7 +70,7 @@ interface FsWriteOutcome {
|
||||
}
|
||||
```
|
||||
|
||||
`editText` 是提供方级别的变更,而非在别处组合的 `read` 加 `write`。守卫模式下,它在字面匹配之前先验证预期版本(因此对陈旧内容的编辑报 `FS_STALE_VERSION`,而非对更新内容的匹配失败);无守卫模式下,它编辑当前内容。无论哪种路径,它都应用替换并原子写入——将匹配、行尾处理、陈旧检查与原子替换保持在同一个变更临界区内——且目标缺失时两种路径都报 `FS_STALE_VERSION`。
|
||||
`editText` 是提供方级别的变更操作,而非在别处组合的 `read` 加 `write`。带守卫时,它在字面匹配之前先验证预期版本(因此对陈旧内容的编辑报 `FS_STALE_VERSION`,而非对更新内容的匹配失败);不带守卫时,它编辑当前内容。无论哪种路径,它都应用替换并原子写入——将匹配、行尾处理、陈旧检查和原子替换保持在一个变更临界区内——目标缺失时两条路径都报 `FS_STALE_VERSION`。
|
||||
|
||||
```ts type-equiv
|
||||
interface FsEditRequest {
|
||||
@@ -90,13 +90,13 @@ interface FsEditOutcome {
|
||||
|
||||
## fs 策略事件(提供方 seam 词汇)
|
||||
|
||||
`dsh-fs` 拥有三个事件,由工具派发、策略插件监听,使发射方(`dsh-tool-fs`)和监听方(`dsh-fs-policy`)共享词汇而无需发射方依赖策略插件。它们只携带 `dsh-fs` 词汇加一个不透明的 `object` actor,不含面向模型的概念,也不含 agent/会话所有者结构。
|
||||
`dsh-fs` 拥有三个事件,由工具分发、策略插件监听,使发射方(`dsh-tool-fs`)与监听方(`dsh-fs-policy`)共享词汇,而发射方无需依赖策略插件。它们只携带 `dsh-fs` 词汇加一个不透明的 `object` actor,不含面向模型的概念,也不含 agent/session 所有者结构。
|
||||
|
||||
`fs/write-intent` 和 `fs/edit-intent` 是**单槽决策 waterfall**:工具派发时附带一个默认 thunk(返回 `undefined`,即裸提供方),监听方完全裁决而不调用 `next()`。该槽按注册顺序先到先得——策略插件占据该槽是部署约定,而非强制不变式。`fs/observed` 是一个即发即忘的记录事件,通过普通 `ctx.emit` 派发;其监听方必须是同步且仅有副作用的,因为工具不守卫该 emit——抛出异常的监听方会作为工具对一个已成功变更的 `isError` 结果暴露出来。生成的目录在 [events.md](../cordis-catalog/events.md) 展示确切签名。
|
||||
`fs/write-intent` 与 `fs/edit-intent` 是**单槽决策 waterfall**:工具分发时附带一个默认 thunk(返回 `undefined`,即裸提供方),监听方完全决策而不调用 `next()`。该槽按注册顺序先到先得——由策略插件占据是部署约定,而非强制不变式。`fs/observed` 是一个即发即弃的记录事件,通过普通 `ctx.emit` 分发;其监听方必须是同步的、仅产生副作用,因为工具不守卫该 emit——抛异常的监听方会在一次已成功的变更上表现为工具的 `isError` 结果。生成的目录在 [events.md](../cordis-catalog/events.md) 中展示确切签名。
|
||||
|
||||
## 执行上下文(策略插件)
|
||||
|
||||
策略插件只需要足够的执行上下文来从 `fs/*` 事件携带的不透明 `object` actor 中窄化出观测状态的所有者。`ToolExecution` 满足此形状,因此 `dsh-tool-fs` 将其执行对象作为 actor 透传,而无需让 `dsh-fs-policy` 导入工具、agent 或会话包。
|
||||
策略插件只需要足够的执行上下文,通过收窄 `fs/*` 事件携带的不透明 `object` actor 来推导观测状态的所有者。`ToolExecution` 满足此形状,因此 `dsh-tool-fs` 将其执行对象作为 actor 直接传递,而无需让 `dsh-fs-policy` 导入 tool、agent 或 session 包。
|
||||
|
||||
```ts type-equiv
|
||||
interface FsPolicyExec {
|
||||
@@ -108,7 +108,7 @@ interface FsPolicyExec {
|
||||
|
||||
## 读取结果(消费方 / 读取渲染)
|
||||
|
||||
文本读取受行窗口、字节上限和后端限制约束。面向模型的 `read` 工具渲染的结果纯粹是展示性的;不存在 `full`/`partial` 视图区分——授权基于新鲜度(工具直接以 stat 的版本 emit `fs/observed`),因此任何窗口化读取在文件未变时都能授权后续的 write/edit。读取窗口化与此结果形状位于 `dsh-tool-fs`(拥有读取的执行器),而非策略插件。
|
||||
文本读取受行窗口、字节上限和后端限制约束。面向模型的 `read` 工具渲染的结果纯粹是展示性的;不存在 `full`/`partial` 视图区分——授权基于新鲜度(工具直接用 stat 的版本 emit `fs/observed`),因此任何窗口化读取在文件未变时都能授权后续的 write/edit。读取窗口化与此结果形状位于 `dsh-tool-fs`(拥有读取操作的执行器)中,而非策略插件中。
|
||||
|
||||
```ts type-equiv
|
||||
interface FileReadOutcome {
|
||||
@@ -121,11 +121,11 @@ interface FileReadOutcome {
|
||||
|
||||
## 已观测文件状态(策略插件)
|
||||
|
||||
已观测状态是 `dsh-fs-policy` 插件内部持有的 `WeakMap<owner, Map<targetKey, { version }>>`。条目存在**当且仅当**所有者已读取、写入或编辑过该目标(每次成功都 emit `fs/observed`),因此条目的存在本身就是先前观测的记录——没有单独的 `hasRead` 标志,也没有视图区分。所有者从事件 actor 派生(通常是 `exec.agent.session`),被视为不透明且从不读取。成功的 read/write/edit 会刷新该所有者对应的已记录版本;dispose 时丢弃全部数据(HMR 安全)。
|
||||
已观测状态是 `dsh-fs-policy` 插件内部持有的 `WeakMap<owner, Map<targetKey, { version }>>`。当且仅当所有者已读取、写入或编辑过该目标时(每次成功都 emit `fs/observed`),条目才存在,因此其存在本身就是先前观测的记录——没有单独的 `hasRead` 标志,也没有视图区分。所有者从事件 actor 推导(通常是 `exec.agent.session`),被视为不透明且从不读取。成功的 read/write/edit 会刷新该所有者对应的已记录版本;dispose(资源释放)时丢弃全部数据(HMR(热模块替换)安全)。
|
||||
|
||||
## 错误分类体系(提供方 seam)
|
||||
|
||||
文件系统失败使用稳定的 `FsErrorCode` 字符串,由 `FsError`(`HarnessError`)携带。工具注册表在错误结果上保留 `{ name, code }`,使重试、权限和 UI 层无需解析文本即可分支。
|
||||
文件系统故障使用稳定的 `FsErrorCode` 字符串,由 `FsError`(`HarnessError`)携带。工具注册表在错误结果上保留 `{ name, code }`,使重试、权限和 UI 层可以按 code 分支而无需解析文本。
|
||||
|
||||
```ts type-equiv
|
||||
type FsErrorCode =
|
||||
@@ -142,8 +142,8 @@ type FsErrorCode =
|
||||
| 'FS_ABORTED'
|
||||
```
|
||||
|
||||
`FS_NOT_DIRECTORY`、`FS_PERMISSION_DENIED` 和 `FS_IO_ERROR` 用于目录列举,分别区分目标存在但不是目录、列举被拒绝、以及意外的后端 I/O 失败。`FS_NOT_OBSERVED` 表示策略插件没有该所有者的先前观测记录(或 `createIfAbsent` 遇到了已存在的文件)。`FS_STALE_VERSION` 表示后端版本不再匹配已观测版本(或编辑遇到了缺失的目标)。新鲜度授权没有 partial/full 区分,因此不存在 `FS_PARTIAL_OBSERVATION`。
|
||||
`FS_NOT_DIRECTORY`、`FS_PERMISSION_DENIED` 和 `FS_IO_ERROR` 用于目录列表操作,分别区分目标存在但不是目录、列表被拒绝、以及意外的后端 I/O 故障。`FS_NOT_OBSERVED` 表示策略插件对该所有者没有先前观测记录(或 `createIfAbsent` 遇到了已存在的文件)。`FS_STALE_VERSION` 表示后端版本不再匹配已观测版本(或 edit 遇到了缺失的目标)。新鲜度授权没有 partial/full 区分,因此不存在 `FS_PARTIAL_OBSERVATION`。
|
||||
|
||||
## 服务与插件
|
||||
|
||||
`FileSystem`(`ctx.fs`,抽象)拥有提供方原语:`resolve`、`stat`、`readText`、`streamText`、`listDir`、`writeText` 和 `editText`。`dsh-fs-policy` **不注册服务**——它是一个通过 `fs/*` 事件门添加策略的插件:它裁决 write/edit intent waterfall(提供 `createIfAbsent`/`replaceIfVersion`/`{ version }` 或抛出 `FS_NOT_OBSERVED`),并在 `fs/observed` 上记录。执行器是 `dsh-tool-fs`:它通过 `ctx.fs` 读/写/编辑,派发 waterfall,并 emit 记录事件。生成的接线目录在 [services.md](../cordis-catalog/services.md#ctxfs--filesystem-abstract-seam) 展示确切的 `ctx.fs` 签名。
|
||||
`FileSystem`(`ctx.fs`,抽象)拥有提供方原语:`resolve`、`stat`、`readText`、`streamText`、`listDir`、`writeText` 和 `editText`。`dsh-fs-policy` 不注册任何服务——它是一个通过 `fs/*` 事件门控叠加策略的插件:它裁决 write/edit intent waterfall(提供 `createIfAbsent`/`replaceIfVersion`/`{ version }` 或抛出 `FS_NOT_OBSERVED`),并在 `fs/observed` 上记录。执行器是 `dsh-tool-fs`:它通过 `ctx.fs` 读取/写入/编辑,分发 waterfall,并 emit 记录事件。生成的接线目录在 [services.md](../cordis-catalog/services.md#ctxfs--filesystem-abstract-seam) 中展示确切的 `ctx.fs` 签名。
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
llm-streaming.md: ffd276b4647be8d10afcab0fb3c3f6daad790d20
|
||||
llm-streaming.zh.md: 051d6f5d28059bf81ba38c348ea306c46e73d4e2
|
||||
llm-streaming.zh.md: 4cb87c9fe65e264f9ce2f39d6ea3b5c6183d5cc1
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
## `StreamChunk`:原始协议
|
||||
|
||||
一次流式响应会交错多种类型的块(文本、推理、多个工具调用)。`index` 将每个 delta 关联到对应的块;`block-end` 携带完整组装好的 `ContentBlock`,消费方无需自行重新组装 delta。这是一个**封闭的**可辨识联合类型:对 `type` 的 `switch` 以 `assertNever` 结尾,因此新增变体会在每个必须处理它的消费方处触发编译错误。
|
||||
一个流式响应交错包含多种类型的块(文本、推理(reasoning)、多个工具调用)。`index` 将每个 delta 关联到其所属块;`block-end` 携带完整组装好的 `ContentBlock`,消费方无需自行重新组装 delta。这是一个**封闭的**可辨识联合类型:对 `type` 的 `switch` 以 `assertNever` 结尾,因此新增变体会在每个必须处理它的消费方处触发编译错误。
|
||||
|
||||
```ts type-equiv
|
||||
type StreamChunk =
|
||||
@@ -23,18 +23,18 @@ type StreamChunk =
|
||||
|
||||
## 适配器契约
|
||||
|
||||
每个适配器必须遵守以下规则,每个消费方可以依赖它们:
|
||||
每个适配器**必须**遵守以下规则,每个消费方可以依赖它们:
|
||||
|
||||
- **`usage` 在 `finish` 之前,`finish` 之后不再有任何分片。** 将两者都推迟到提供方的流结束标记,这样尾部的 usage-only 分片就不会违反顺序。
|
||||
- **工具调用的 `arguments` 全程保持原始 JSON 字符串。** 部分片段通过 `argumentsDelta` 流式传输;如果提供方返回的是已解析的对象,适配器在 `block-end` 时重新序列化。
|
||||
- **两条认可的错误路径。** 失败可以从 `stream()` 抛出异常(传输/协议错误),**或者**以 `finish {kind:'error'|'aborted'}` 结束流(提供方带内错误,适用于无法在流中途抛出异常的适配器)。消费方必须同时处理*两种*情况。agent loop(智能体循环)将 finish-error/aborted 转化为轮次错误,绝不会为失败的步骤记录一条正常完成的 assistant 消息。
|
||||
- **每个提供方 HTTP 请求都携带应用归属头。** 适配器发送 `attributionHeaders()`(见下文)作为 `User-Agent` 基线,并通过协议级测试证明这一点(mock 服务器断言收到的 header,或库支持的适配器使用库的 header 钩子)。
|
||||
- **工具调用的 `arguments` 全程保持原始 JSON 字符串。** 部分片段通过 `argumentsDelta` 流式传输;如果提供方返回的是已解析的对象,适配器在 `block-end` 时重新序列化为字符串。
|
||||
- **两条许可的错误路径。** 失败可以从 `stream()` 中 THROW(传输/协议错误),**或者**以 `finish {kind:'error'|'aborted'}` 结束流(提供方带内错误,适用于无法在流中途抛出异常的适配器)。消费方必须同时处理*两种*情况。agent loop(智能体循环)将 finish-error/aborted 转化为轮次错误,绝不会为失败的步骤记录一条正常完成的 assistant 消息。
|
||||
- **每个提供方 HTTP 请求都携带应用归属头。** 适配器发送 `attributionHeaders()`(见下文)作为 `User-Agent` 基线,并通过协议级测试加以证明(mock 服务器断言收到的 header,或对基于库的适配器使用库的 header 钩子)。
|
||||
|
||||
这份契约正是两个适配器作为有意配对存在的原因:`dsh-llm-deepseek`(手写的 fetch/SSE(Server-Sent Events))与 `dsh-llm-pi-ai`(通过 `@earendil-works/pi-ai` 访问同一端点)。两套独立的内部实现共享一份契约,正是它将协议钉死的方式:库支持的适配器无法在流中途抛异常,因此它行使了手写适配器可能不会走到的 finish-chunk 错误路径。
|
||||
这份契约正是两个适配器作为刻意配对存在的原因:`dsh-llm-deepseek`(手写 fetch/SSE(Server-Sent Events))与 `dsh-llm-pi-ai`(通过 `@earendil-works/pi-ai` 访问同一端点)。两套独立内部实现共享一份契约,正是将协议固定下来的方式:基于库的适配器无法在流中途抛出异常,因此它走通了手写适配器可能不会走到的 finish-chunk 错误路径。
|
||||
|
||||
## `AppIdentity`:应用归属
|
||||
|
||||
每个适配器向提供方发送的静态公开应用身份([`packages/llm/llm/src/attribution.ts`](../../packages/llm/llm/src/attribution.ts))。`attributionHeaders(identity?)` 仅将其映射为标准 `User-Agent` header;本契约有意不支持 OpenRouter 特有的应用归属 header。默认的 `APP_IDENTITY` 从包(package)的 manifest(元数据清单)获取版本号;每个字段都是公开的产品事实,不含密钥、路径、会话 id 或用户标识符,且没有任何逐请求的值可以影响这些字段。设计依据见 [强制 `User-Agent` 归属](../rfc/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md)。
|
||||
每个适配器向提供方发送的静态公开应用标识([`packages/llm/llm/src/attribution.ts`](../../packages/llm/llm/src/attribution.ts))。`attributionHeaders(identity?)` 仅将其映射为标准 `User-Agent` header;本契约有意不支持 OpenRouter 特有的应用归属 header。默认的 `APP_IDENTITY` 从 package manifest(元数据清单)获取版本号;每个字段都是公开的产品事实,不含密钥、路径、会话 id 或用户级标识符,且任何请求级信息都不得影响这些值。设计依据见 [Mandatory `User-Agent` attribution](../rfc/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md)。
|
||||
|
||||
```ts type-equiv
|
||||
interface AppIdentity {
|
||||
@@ -60,13 +60,13 @@ interface TokenUsage {
|
||||
|
||||
## `BlockAssembler`
|
||||
|
||||
`BlockAssembler`([`packages/llm/llm/src/assembler.ts`](../../packages/llm/llm/src/assembler.ts))是唯一的共享实现,负责将 `StreamChunk` 流折叠回 `ContentBlock` 序列与最终的 `Message`。agent loop 记录原始分片(保证回放保真度),同时将相同的分片送入 assembler;这样权威日志保留了 token 级别的细节,而派生的消息可以确定性地重建。需要组装结果但不想重新实现折叠逻辑的消费方使用它。
|
||||
`BlockAssembler`([`packages/llm/llm/src/assembler.ts`](../../packages/llm/llm/src/assembler.ts))是唯一的共享实现,将 `StreamChunk` 流折叠回 `ContentBlock` 列表与最终的 `Message`。agent loop 记录原始分片(保证回放保真度),同时将相同的分片送入 assembler,因此权威日志保留了 token 级细节,而派生消息可确定性地重建。需要组装结果而不想重新实现折叠逻辑的消费方使用它。
|
||||
|
||||
## seam
|
||||
|
||||
`LlmAdapter` 是提供方 seam:继承它、实现 `stream()`、通过 `ctx.llm.registerAdapter(models, adapter)` 注册。`block-start`/`block-end` 的 `index` 关联加上 assembler,意味着适配器只需发出格式正确的分片,块的重新组装不是各适配器自己的问题。消费方接口(`ctx.llm.stream()`)与 `llm/stream` waterfall(瀑布式事件)在 [architecture.md § Content blocks and streaming](../architecture.md#content-blocks-and-streaming-dsh-llm) 中描述。
|
||||
`LlmAdapter` 是提供方 seam:继承它、实现 `stream()`、通过 `ctx.llm.registerAdapter(models, adapter)` 注册。`block-start`/`block-end` 的 `index` 关联加上 assembler 意味着适配器只需发出格式正确的分片,块重组不是各适配器需要操心的事。消费方接口(`ctx.llm.stream()`)与 `llm/stream` waterfall(瀑布式事件)在 [architecture.md § Content blocks and streaming](../architecture.md#content-blocks-and-streaming-dsh-llm) 中描述。
|
||||
|
||||
`ContentBlockType`(`index` 关联的块所携带的键集合)派生自 `ContentBlockMap`:
|
||||
`ContentBlockType`(`index` 关联块所携带的键集合)派生自 `ContentBlockMap`:
|
||||
|
||||
```ts type-equiv
|
||||
interface ContentBlockMap {
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
persistence.md: ce7a21a5613da903a9bddd339e122bf9f899d2bd
|
||||
persistence.zh.md: 07d54895ef59b99dca47142e3fde16e6d7d0d1b3
|
||||
persistence.zh.md: 33ddda66aacc21f847dfd45702b11b5381711cce
|
||||
@@ -2,21 +2,21 @@
|
||||
|
||||
[English](persistence.md) | 中文
|
||||
|
||||
事件日志的**持久性 seam**。[session.md](session.md) 描述了内存中的 `Session`:仅追加的 `SessionEvent` 日志即为真源。本页描述该日志如何被持久化:抽象的 `SessionPersistence` 服务、它的后端、flush 检查点、崩溃恢复,以及随日志一起存储的元数据头。日志所承载的事件词汇在生成的[持久化日志事件目录](../persistence-catalog.md)中逐一列出。
|
||||
事件日志的**持久性 seam**。[session.md](session.md) 描述了内存中的 `Session`:仅追加的 `SessionEvent` 日志即为真源。本页描述如何使该日志持久化:抽象的 `SessionPersistence` 服务、它的后端、flush 检查点、崩溃恢复,以及随日志一同存储的元数据头。日志承载的事件词汇在生成的[持久化日志事件目录](../persistence-catalog.md)中逐项列举。
|
||||
|
||||
该 seam 是教科书式的[能力 seam](../rfc/implemented/architecture/2026-06-13-capability-seams.md):一个抽象服务([dsh-session-persistence](../../packages/session-persistence/session-persistence),`ctx.sessionPersistence`)在既有的 `SessionEvent` 之上定义 create/append/load/list——**没有平行的持久化类型**——以及两个可互换的后端,它们通过同一套 `runPersistenceContract` 测试。见 [session-persistence RFC](../rfc/implemented/architecture/2026-06-14-session-persistence.md)。
|
||||
该 seam 是典型的[能力 seam](../rfc/implemented/architecture/2026-06-13-capability-seams.md):一个抽象服务([dsh-session-persistence](../../packages/session-persistence/session-persistence),`ctx.sessionPersistence`)在已有的 `SessionEvent` 之上定义 create/append/load/list 操作,**没有并行的持久化类型**,以及两个可互换的后端,它们通过同一套 `runPersistenceContract` 测试。详见 [session-persistence RFC](../rfc/implemented/architecture/2026-06-14-session-persistence.md)。
|
||||
|
||||
## flush 检查点
|
||||
|
||||
`session/event` 是一个*同步*通知;持久化插件对其进行缓冲(write-behind),并在 agent loop 于每个轮次结束时触发的 `session/flush` 检查点处排空缓冲区。flush 使用 `ctx.parallel`(被 await):一个轮次的事件在下一个轮次开始前被持久提交,轮次边界即提交边界。flush 失败时通过 `agent/error` 和 logger 报告,而非作为会话事件(那样会落在提交边界之后),因此后端保留其缓冲事件等待下一次 flush。
|
||||
`session/event` 是一个*同步*通知;持久化插件对其进行缓冲(write-behind),并在 agent loop(智能体循环)于每个轮次结束时触发的 `session/flush` 检查点处排空缓冲区。flush 使用 `ctx.parallel`(被 await):一个轮次的事件在下一个轮次开始前已被持久提交,轮次边界即提交边界。flush 拒绝时通过 `agent/error` 和 logger 报告,而非作为会话事件(那样会落在提交边界之后),因此后端保留其缓冲事件等待下一次 flush。
|
||||
|
||||
## 崩溃恢复保留被中断的轮次
|
||||
|
||||
后端重新加载一个在轮次中途崩溃的日志时,会发现一个已打开的 `turn/start` 而没有对应的 `turn/end`。它**不会**截断日志:在长周期任务中,单个轮次可能非常大(许多步骤、大量工具输出),而这些事件在崩溃前已被持久追加。后端改为用一个合成的 `turn/end { reason: { kind: 'interrupted' } }` 关闭这个遗留轮次,保持日志平衡与轮次封闭不变式完好。`interrupted` 是唯一一个 agent loop 不会自行发出的 `TurnEndReason`(见 [session.md](session.md#why-a-turn-ended-turnendreasonmap))。
|
||||
后端重新加载一个在轮次中途崩溃的日志时,会发现一个已打开的 `turn/start` 却没有 `turn/end`。它**不会**截断日志:在长周期任务中,单个轮次可能非常庞大(许多步骤、大量工具输出),而这些事件在崩溃前已被持久追加。后端改为用一个合成的 `turn/end { reason: { kind: 'interrupted' } }` 关闭这个遗留轮次,保持日志平衡与轮次闭合不变式。`interrupted` 是唯一一个不由循环发出的 `TurnEndReason`(见 [session.md](session.md#why-a-turn-ended-turnendreasonmap))。
|
||||
|
||||
## `SessionHeader`:日志旁的元数据
|
||||
|
||||
每个会话的元数据与事件日志**分开**存储:格式版本、cwd、血缘关系和 seed 边界属于存储关注点而非对话事件,因此它们不在 `SessionEventMap` 中,也不会进入 `deriveMessages()`。header 通过 `session.header` 附加到 `Session` 上。
|
||||
每个会话的元数据与事件日志**分开**存储:格式版本、cwd、血统与 seed 边界是存储层关注点而非对话事件,因此不进入 `SessionEventMap`,也不会到达 `deriveMessages()`。header 通过 `session.header` 附加到 `Session` 上。
|
||||
|
||||
源码:[`packages/core/session/src/types.ts`](../../packages/core/session/src/types.ts)
|
||||
|
||||
@@ -51,7 +51,7 @@ interface SessionHeader {
|
||||
|
||||
## `CreateSessionOptions`:seed 与元数据
|
||||
|
||||
通过 store 创建 `Session` 时接受 `seed`(回放/fork 一个已有事件日志)和 `meta`(store 折叠进 `SessionHeader` 的存储级字段)。store 填充 `version`/`id` 并为 `createdAt` 设默认值;调用方提供经过校验的绝对路径 `cwd`、`parentSession` 血缘、`seedLength` seed 边界,以及仅在重建持久化会话时提供的原始 `createdAt` 以保留它。
|
||||
通过 store 创建 `Session` 时接受 `seed`(回放/fork 已有事件日志)和 `meta`(store 折叠进 `SessionHeader` 的存储层字段)。store 填充 `version`/`id` 并默认 `createdAt`;调用方提供经过校验的绝对路径 `cwd`、`parentSession` 血统、`seedLength` seed 边界,以及仅在重建持久化会话时提供的原始 `createdAt` 以保留其值。
|
||||
|
||||
```ts type-equiv
|
||||
interface CreateSessionOptions {
|
||||
@@ -78,13 +78,13 @@ interface CreateSessionOptions {
|
||||
}
|
||||
```
|
||||
|
||||
因此,回放/fork 是 `ctx.sessions.create(id, { seed: seedEvents })`;将一个*持久化*会话恢复为活跃 agent 是 `ctx.agents.resume({ resumeSessionId })`。
|
||||
因此,回放/fork 的调用方式为 `ctx.sessions.create(id, { seed: seedEvents })`;将一个*持久化*会话恢复为活跃 agent 的调用方式为 `ctx.agents.resume({ resumeSessionId })`。
|
||||
|
||||
## 后端
|
||||
|
||||
两个后端实现同一个抽象 `SessionPersistence`(在 `SessionEvent` 之上的 create/append/load/list),并通过 `runPersistenceContract`,证明该 seam 真正与后端无关:
|
||||
两者实现相同的抽象 `SessionPersistence`(在 `SessionEvent` 之上提供 create/append/load/list),并通过 `runPersistenceContract`,证明该 seam 真正与后端无关:
|
||||
|
||||
- **[dsh-session-persistence-jsonl](../../packages/session-persistence/session-persistence-jsonl)**:每个会话一个仅追加的 JSONL 日志,具备崩溃安全的原子写入、上述中断轮次崩溃恢复,以及读取/回放路径。
|
||||
- **[dsh-session-persistence-sqlite](../../packages/session-persistence/session-persistence-sqlite)**:基于 `node:sqlite`,每个 `SessionEvent` 一行。行结构 `(session_id, seq, type, time, data, source_event_seqs, surface_op)` 与事件 1:1 映射(包括可选的 surface 元数据),因此没有需要保持同步的平行持久化 schema。
|
||||
- **[dsh-session-persistence-sqlite](../../packages/session-persistence/session-persistence-sqlite)**:基于 `node:sqlite`,每个 `SessionEvent` 一行。行结构 `(session_id, seq, type, time, data, source_event_seqs, surface_op)` 与事件 1:1 映射(包含可选的 surface 元数据),因此没有需要保持同步的并行持久化 schema。
|
||||
|
||||
多个后端共享同一个磁盘会话时,通过[共享持久化写协调器](../rfc/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md)协调写入。
|
||||
多个后端共享同一磁盘会话时,通过[共享持久化写协调器](../rfc/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md)协调写入。
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
sandbox.md: be8e3cd60681077ff5036915fd99520fe9685140
|
||||
sandbox.zh.md: 2e9e5aa9a65e636167e45900f169ed6da5219c24
|
||||
sandbox.zh.md: ca21c09e7f3d0756f78d7bd1581b9b4b03a43815
|
||||
@@ -2,25 +2,25 @@
|
||||
|
||||
[English](sandbox.md) | 中文
|
||||
|
||||
[dsh-sandbox](../../packages/sandbox/sandbox) 的进程沙箱 seam 将同世界子进程 argv 包装在文件效果策略中,而不将消费方耦合到特定平台运行器。[dsh-sandbox-local](../../packages/sandbox/sandbox-local) 提供 Linux bwrap/Landlock 与 macOS Seatbelt 后端;[dsh-bash-sandbox](../../packages/bash/bash-sandbox) 是第一个消费方。容器、microVM 与远程执行是整体能力 seam 的兄弟实现,而非 `ctx.sandbox` 的提供方。
|
||||
[dsh-sandbox](../../packages/sandbox/sandbox) 的进程沙箱 seam 将同世界子进程的 argv 包装在文件效果策略中,而不将消费方耦合到特定平台运行器。[dsh-sandbox-local](../../packages/sandbox/sandbox-local) 提供 Linux bwrap/Landlock 与 macOS Seatbelt 后端;[dsh-bash-sandbox](../../packages/bash/bash-sandbox) 是第一个消费方。容器、microVM 和远程执行是完整能力 seam 的兄弟实现,而非 `ctx.sandbox` 的提供方。
|
||||
|
||||
源码:[`packages/sandbox/sandbox/src/index.ts`](../../packages/sandbox/sandbox/src/index.ts)
|
||||
|
||||
## 模式与强制
|
||||
## 模式与强制执行
|
||||
|
||||
`SandboxMode` 仅管控文件系统效果。`read-only` 拒绝写入(必需的 `/dev/null` sink 除外);`workspace-write` 允许在工作区根目录与后端承诺的临时区域下写入;`danger-full-access` 绕过隔离。网络与进程可见性不在此词汇范围内。
|
||||
`SandboxMode` 仅管控文件系统效果。`read-only` 拒绝所有写入(必需的 `/dev/null` 接收器除外);`workspace-write` 允许在工作区根目录及后端承诺的临时区域下写入;`danger-full-access` 绕过隔离。网络与进程可见性不在此处的定义范围内。
|
||||
|
||||
```ts type-equiv
|
||||
type SandboxMode = 'read-only' | 'workspace-write' | 'danger-full-access'
|
||||
```
|
||||
|
||||
只有前两种模式可以发送给提供方。`danger-full-access` 消费方直接 spawn 原始 argv,不调用 `ctx.sandbox`。
|
||||
只有前两种模式可以发送给提供方。`danger-full-access` 的消费方直接 spawn 原始 argv,不调用 `ctx.sandbox`。
|
||||
|
||||
```ts type-equiv
|
||||
type ConfinedSandboxMode = Exclude<SandboxMode, 'danger-full-access'>
|
||||
```
|
||||
|
||||
强制级别是一个报告事实。`full` 表示后端管控了该模式承诺的所有文件效果;`partial` 表示活跃后端或较旧的内核 ABI 仅管控了一个子集,因此要求绝对承诺的消费方必须拒绝或向上暴露这一区别。
|
||||
强制执行程度是一个报告事实。`full` 表示后端管控了该模式承诺的所有文件效果;`partial` 表示活跃后端或较旧的内核 ABI 仅管控其中一个子集,因此要求绝对保证的消费方必须拒绝或向上暴露这一区别。
|
||||
|
||||
```ts type-equiv
|
||||
type SandboxEnforcement = 'full' | 'partial'
|
||||
@@ -28,7 +28,7 @@ type SandboxEnforcement = 'full' | 'partial'
|
||||
|
||||
## 逐调用策略
|
||||
|
||||
策略在每次调用时完全解析并随调用携带。这使得并发消费方和一次性升级重试可以向同一个提供方请求不同的边界,而无需修改提供方状态。
|
||||
策略在每次调用时完全解析并随调用携带。这使得并发消费方和一次性提权重试能够向同一个提供方请求不同的边界,而无需修改提供方状态。
|
||||
|
||||
```ts type-equiv
|
||||
interface SandboxPolicy {
|
||||
@@ -41,7 +41,7 @@ interface SandboxPolicy {
|
||||
|
||||
## 包装后的 argv 与分类方言
|
||||
|
||||
`ConfinedArgv` 是消费方实际 spawn 的内容。除了替换后的 argv,它还携带后端的强制事实和两组正交的 stderr 方言。`denialSignatures` 标识沙箱正常工作时被隔离命令被阻止的情况。`runnerFailureSignatures` 标识沙箱运行器在执行命令之前拒绝或失败的情况;消费方应先检查后者,将其作为沙箱基础设施故障暴露,而非普通任务失败。
|
||||
`ConfinedArgv` 是消费方实际 spawn 的内容。除了替换后的 argv,它还携带后端的强制执行事实和两种正交的 stderr 方言。`denialSignatures` 用于识别沙箱正常工作时被隔离命令被阻止的情况。`runnerFailureSignatures` 用于识别沙箱运行器在执行命令之前拒绝或失败的情况;消费方应先检查后者,将其作为沙箱基础设施故障上报,而非普通任务失败。
|
||||
|
||||
```ts type-equiv
|
||||
interface ConfinedArgv {
|
||||
@@ -75,10 +75,10 @@ interface ConfinedArgv {
|
||||
}
|
||||
```
|
||||
|
||||
运维人员配置的本地运行器必须为自身的 pre-exec 拒绝方言提供至少一条 `runnerFailureSignatures` 条目;提供方会自动添加外层 shell 的 missing 和 unexecutable 形式。这使得可执行的自定义运行器拒绝其 profile 的情况与被包装命令以相同状态码退出的情况可以区分开来。
|
||||
运维人员配置的本地运行器必须为自身的 pre-exec 拒绝方言提供至少一条 `runnerFailureSignatures` 条目;提供方会自动添加外层 shell 的 missing 和 unexecutable 形式。这使得可执行的自定义运行器拒绝其 profile 的情况能够与被包装命令以相同状态码退出的情况区分开来。
|
||||
|
||||
## 提供方与 fail-closed 错误
|
||||
|
||||
`ctx.sandbox.confine(argv, policy)` 返回一个 `ConfinedArgv`,或在没有可用后端时抛出 `SandboxUnavailableError`(错误码 `SANDBOX_UNAVAILABLE`)。已选定的运行器也可能在执行时 fail-closed,此时其失败签名承载相同的基础设施含义。对于受限策略,静默的无隔离透传永远不合法。
|
||||
|
||||
提供方探测在多个候选后端之间仲裁,结果在提供方生命周期内缓存。只有一个候选的平台可以直接选定它;执行时拒绝仍保留安全属性。本地提供方将 bwrap 和 Seatbelt 报告为 full,并保留 Landlock 启动器的 full/partial 内核裁定。
|
||||
提供方探测在多个候选后端之间仲裁,结果在提供方生命周期内缓存。只有一个候选后端的平台可以直接选定它;执行时拒绝仍保留安全属性。本地提供方将 bwrap 和 Seatbelt 报告为 full,并保留 Landlock 启动器的 full/partial 内核裁定。
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
scope.md: f95594329ee9ac83da2efcc31df377c53e64331a
|
||||
scope.zh.md: 80b2a7355e0499fcccf0e218c57f395252416cbd
|
||||
scope.zh.md: 277fa4ec5c365e5ff3ee6d9baeae525d3dfc9f96
|
||||
@@ -2,19 +2,19 @@
|
||||
|
||||
[English](scope.md) | 中文
|
||||
|
||||
[scope 包](../../packages/core/scope)提供身份标识与载体词汇,使一个注册上下文同时表达「按 agent 可见」和「共享生命周期所有权」两层含义。它是一个库级原语,而非 Cordis 服务;[agent-scope 运行时设计 RFC](../rfc/implemented/architecture/2026-07-12-agent-scope-runtime-design.md#scope-routing-one-opaque-key-selects-one-layer) 拥有实现动机,包的 [README](../../packages/core/scope/README.md) 拥有可调用 API 与过滤语义。
|
||||
[scope 包(package)](../../packages/core/scope)提供身份标识与载体词汇,使一个注册上下文同时表达逐 agent(智能体)的可见性与共享的生命周期归属。它是一个库级原语,而非 Cordis 服务;[agent-scope 运行时设计 RFC](../rfc/implemented/architecture/2026-07-12-agent-scope-runtime-design.md#scope-routing-one-opaque-key-selects-one-layer) 阐述了实现原理,包的 [README](../../packages/core/scope/README.md) 说明了可调用 API 与过滤语义。
|
||||
|
||||
源码:[`packages/core/scope/src/index.ts`](../../packages/core/scope/src/index.ts)。
|
||||
|
||||
## 身份标识与分发载体
|
||||
|
||||
`ScopeKey` 是一个不透明的对象标识。已交付的 agent loop 使用活跃的 `Agent` 对象作为自身的 key,但该原语从不检视该对象。
|
||||
`ScopeKey` 是一个不透明的对象身份标识。已交付的 agent loop(智能体循环)使用活跃的 `Agent` 对象作为自身的 key,但该原语从不检视该对象。
|
||||
|
||||
```ts type-equiv
|
||||
type ScopeKey = object
|
||||
```
|
||||
|
||||
`Scoped<T>` 是 `scopeTarget(base, key)` 返回的不透明路由接收者上的编译期品牌类型。经作用域过滤的事件声明要求以此载体作为其 `this` 类型,而真正的事件主体仍作为显式参数传递。
|
||||
`Scoped<T>` 是编译期品牌标记,标注在 `scopeTarget(base, key)` 返回的不透明路由接收器上。作用域过滤的事件声明要求以此载体作为 `this` 类型,而真正的事件主体仍作为显式参数传入。
|
||||
|
||||
```ts type-equiv
|
||||
type Scoped<T extends object> = object & { readonly [ScopedBrand]: T }
|
||||
@@ -22,7 +22,7 @@ type Scoped<T extends object> = object & { readonly [ScopedBrand]: T }
|
||||
|
||||
## 拥有所有权的注册上下文
|
||||
|
||||
`Scope` 将带标签的注册上下文与两个拆卸面配对。`rawDispose` 保留有序组合副作用所需的精确 Cordis disposer 标识;`dispose()` 是面向直接调用方和竞争调用方的公共共享静默边界。
|
||||
`Scope` 将带标签的注册上下文与两个拆卸接口配对。`rawDispose` 保留有序复合 effect 所需的精确 Cordis disposer 身份;`dispose()` 是面向直接调用方和竞争调用方的公共静默边界,用于 dispose(资源释放)。
|
||||
|
||||
```ts type-equiv
|
||||
interface Scope {
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
session-query.md: 444f2bb2256a43df7bd8521dfe234f771eec7181
|
||||
session-query.zh.md: 4d00e5fa310d82c6099ab4f5255f09f9e253c150
|
||||
session-query.zh.md: 70f9737a702f84074962d9d1c7ac49a7c119f8cf
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
[English](session-query.md) | 中文
|
||||
|
||||
对实时优先的逻辑会话语料库进行精确读取。[包(package)契约](../../packages/session-query/session-query)定义了源优先级、动态可选持久化、克隆、surface 分类、有界窗口与类型化错误。全文搜索是一个独立提议的 SQLite 阶段。
|
||||
对实时优先的逻辑会话语料库进行精确读取。[包(package)契约](../../packages/session-query/session-query)定义了源优先级、动态可选持久化、克隆、surface 分类、有界窗口与类型化错误。全文搜索是另一个拟议的 SQLite 阶段。
|
||||
|
||||
源码:[`packages/session-query/session-query/src/types.ts`](../../packages/session-query/session-query/src/types.ts)
|
||||
|
||||
## 逻辑记录
|
||||
|
||||
`SessionRecord` 由跨语料库列表返回。它独立于克隆的实时优先 header 暴露源可用性。`SessionEventRecord` 是一个轻量的原始日志投影;分类使用与 model-history 推导相同的 `foldSurface()` 状态转换。
|
||||
`SessionRecord` 由跨语料库列表返回。它独立于克隆后的实时优先 header 暴露源可用性。`SessionEventRecord` 是轻量的原始日志投影;分类使用与 model-history 推导相同的 `foldSurface()` 状态转换。
|
||||
|
||||
```ts type-equiv
|
||||
export type SessionEventSurface = 'current' | 'shadowed' | 'log-only'
|
||||
@@ -34,7 +34,7 @@ export interface SessionEventRecord {
|
||||
|
||||
## 有界事件读取
|
||||
|
||||
请求指定一个原始 seq 以及可选的前后邻近数量。结果携带 `SessionHeader` 而非可用性标志,使已知的实时目标可以独立于持久化健康状态。
|
||||
请求指定一个原始 seq 及可选的邻近数量。结果携带 `SessionHeader` 而非可用性标志,使已知的实时目标可以独立于持久化健康状态。
|
||||
|
||||
```ts type-equiv
|
||||
export interface SessionEventReadRequest {
|
||||
@@ -57,7 +57,7 @@ export interface SessionEventWindow {
|
||||
|
||||
## 错误
|
||||
|
||||
封闭的 code 联合类型区分请求校验、目标缺失、surface 日志格式错误、可选后端失败与源元数据矛盾。
|
||||
封闭的 code 联合类型区分请求校验、目标缺失、surface 日志格式错误、可选后端故障与矛盾的源元数据。
|
||||
|
||||
```ts type-equiv
|
||||
export type SessionQueryErrorCode =
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
session.md: 796abebbc31a54c7c341028cf0a09031ae59cd78
|
||||
session.zh.md: a2ff9319af1425792702502e12bd287d7f3ca805
|
||||
session.zh.md: b55ff3b7fe9c3a5f65dc46addf266dc1e5430116
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
[English](session.md) | 中文
|
||||
|
||||
[dsh-session](../../packages/core/session) 的内存事件溯源模型。`Session` 是一份由类型化 `SessionEvent` 组成的**仅追加日志**,是 agent(智能体)整个交互历史的唯一真源。LLM(大语言模型)消息历史从日志*派生*而来,从不单独存储;回放即从同一组事件重新派生。日志如何实现**持久化**(持久化 seam、后端、崩溃恢复)是兄弟文档 [persistence.md](persistence.md) 的关注点。
|
||||
[dsh-session](../../packages/core/session) 的内存事件溯源模型。`Session` 是一份由类型化 `SessionEvent` 组成的**仅追加日志**,是 agent(智能体)完整交互历史的唯一真源。LLM(大语言模型)消息历史从日志*派生*而来,从不单独存储;回放即从同一组事件重新派生。日志如何实现**持久化**(持久化 seam、后端、崩溃恢复)是兄弟文档 [persistence.md](persistence.md) 的关注点。
|
||||
|
||||
源码:[`packages/core/session/src/types.ts`](../../packages/core/session/src/types.ts)
|
||||
|
||||
## `SessionEventMap`:事件词汇
|
||||
|
||||
仅追加的事件类型。可通过声明合并扩展:插件通过 declaration merging 声明额外的事件类型。例如[压缩(compaction) seam](compaction.md) 添加了 `compact/start` / `compact/summary` / `compact/end`,`@deepseek-ai/dsh-hook-protocol` 添加了仅记录日志的 `hook/invoked` / `hook/result` 溯源事件用于钩子桥接。与 `compact/*` 一样,这些都不是 `SurfaceEventType`(没有 `surfaceOp`)。生成的[持久化日志事件目录](../persistence-catalog.md)列举了所有成员(核心与合并的),包括其 payload、surface 标记和声明位置。
|
||||
仅追加的事件类型。可通过声明合并扩展:插件通过 declaration merging 声明额外的事件类型。例如[上下文压缩(context compaction) seam](compaction.md) 添加了 `compact/start` / `compact/summary` / `compact/end`,`@deepseek-ai/dsh-hook-protocol` 添加了仅记录日志的 `hook/invoked` / `hook/result` 溯源事件,用于钩子桥接。与 `compact/*` 一样,这些都不是 `SurfaceEventType`(没有 `surfaceOp`)。生成的[持久化日志事件目录](../persistence-catalog.md)列举了所有成员(核心与合并扩展的),包含其 payload、surface 标记与声明位置。
|
||||
|
||||
```ts type-equiv
|
||||
interface SessionEventMap {
|
||||
@@ -90,7 +90,7 @@ interface SessionEventMap {
|
||||
|
||||
### `TodoItem`:一条待办项
|
||||
|
||||
`todo/write` 事件全量快照的单元。刻意保持最小化:一行 `content` 加一个三态 `status`(无 id、无优先级、无 `activeForm`)。列表在每次写入时整体替换,因此条目不需要稳定标识;三态 status 恰好对应 ACP 的 `PlanEntryStatus`,UI 桥接层可以将 todo 列表 1:1 映射到 ACP `plan`(ACP 额外要求的 priority 由桥接层合成)。见 [todo_write RFC](../rfc/implemented/feature/2026-06-29-todo-write-tool.md)。
|
||||
`todo/write` 事件全量快照的单元。刻意保持精简:一行 `content` 加一个三态 `status`(无 id、无优先级、无 `activeForm`)。列表在每次写入时整体替换,因此条目不需要稳定标识;三态 status 恰好是 ACP(Agent Client Protocol)的 `PlanEntryStatus`,UI 桥接层可以将待办列表 1:1 映射到 ACP `plan`(再合成 ACP 额外要求的优先级)。见 [todo_write RFC](../rfc/implemented/feature/2026-06-29-todo-write-tool.md)。
|
||||
|
||||
```ts type-equiv
|
||||
export interface TodoItem {
|
||||
@@ -101,7 +101,7 @@ export interface TodoItem {
|
||||
|
||||
### 请求头事件:`request/header` 与 `request/header-delta`
|
||||
|
||||
请求信封(`EpochHeader`:调用配置 + 渲染后的系统提示词 + 组装好的工具 schema + 会话前缀)是被记录到日志中的会话状态,因此每次对话请求都是日志的纯函数(可重建性 RFC)。`request/header` 快照(reason 为 `'initial' | 'resume' | 'fallback'`)在对话创建、进程边界和 delta 编码回退时锚定折叠点;`request/header-delta` 事件在运行中修正它。`foldRequestHeader(events)` 可重建任何请求构建时所用的 header;写入器在记录每个 delta 前都会做往返验证,因此格式良好的日志总能折叠。两者都不是 `SurfaceEventType`,不产生 LLM 消息。
|
||||
请求信封(`EpochHeader`:调用配置 + 渲染后的系统提示词 + 组装好的工具 schema + 会话前缀)是被记录到日志中的会话状态,使得每次对话请求都是日志的纯函数(可重建性 RFC)。`request/header` 快照(reason 为 `'initial' | 'resume' | 'fallback'`)在对话诞生、进程边界和 delta 编码回退时锚定折叠点;`request/header-delta` 事件在运行中修正它。`foldRequestHeader(events)` 可重建任一请求构建时所用的 header;写入器在记录每个 delta 前都会做往返验证,因此格式正确的日志总能折叠。两者都不是 `SurfaceEventType`,不产生 LLM 消息。
|
||||
|
||||
```ts type-equiv
|
||||
export interface EpochHeader {
|
||||
@@ -122,11 +122,11 @@ export interface EpochHeader {
|
||||
}
|
||||
```
|
||||
|
||||
规范形式:空的系统提示词、空的工具列表和空的会话前缀表示为字段缺失,与请求构建方式一致。`messagePrefix` 是 `agent/session-prefix` waterfall(瀑布式事件)产物的持久记录(请求 = `messagePrefix` + 派生历史);每个 agent loop(智能体循环)实例组装一次,由该实例的快照锚定,因此循环实际上不会产生 prefix delta。delta 分支(数组整体替换,空数组编码回到缺失状态的转换)为编解码完备性而存在。其他 delta payload(`SystemDelta`:公共前缀/后缀行裁剪;`ToolsDelta`:按名称键控的增/删/改)与事件一起定义在 [`packages/core/session/src/types.ts`](../../packages/core/session/src/types.ts) 中。
|
||||
规范形式:空的系统提示词、空的工具列表和空的会话前缀均为 ABSENT 字段,与请求构建方式一致。`messagePrefix` 是 `agent/session-prefix` waterfall(瀑布式事件)产物的持久记录(请求 = `messagePrefix` + 派生历史);每个 agent loop(智能体循环)实例组合一次,由该实例的快照锚定,因此实际上 loop 不会产生前缀 delta。delta 分支(整数组替换,空数组编码「回到无前缀」的转换)存在是为了编解码的完备性。其他 delta payload(`SystemDelta`:公共前缀/后缀行裁剪;`ToolsDelta`:按名称键控的增/删/改)与事件一起定义在 [`packages/core/session/src/types.ts`](../../packages/core/session/src/types.ts)。
|
||||
|
||||
## `SessionEvent<T>`:一条日志条目
|
||||
|
||||
基于 `type` 的正规可辨识联合(而非独立的 `type`/`data` 联合),因此 `switch (event.type)` 可以收窄 `event.data` 而无需类型断言。`seq` 是日志中的单调递增位置(`seq = log.length`);`time` 为 epoch 毫秒。
|
||||
基于 `type` 的真正可辨识联合(而非独立的 `type`/`data` 联合),因此 `switch (event.type)` 能直接收窄 `event.data`,无需类型断言。`seq` 是日志中的单调递增位置(`seq = log.length`);`time` 为 epoch 毫秒。
|
||||
|
||||
```ts type-equiv
|
||||
type SessionEvent<T extends SessionEventType = SessionEventType> = {
|
||||
@@ -150,13 +150,13 @@ type SessionEvent<T extends SessionEventType = SessionEventType> = {
|
||||
}[T]
|
||||
```
|
||||
|
||||
`SessionEventType = keyof SessionEventMap`。由于 `SessionEventMap` 可通过合并扩展,对 `SessionEvent` 的 switch 禁止使用 `assertNever`:插件添加的变体是合法的未知值;处理已知 case 后在 `default` 中放行。
|
||||
`SessionEventType = keyof SessionEventMap`。由于 `SessionEventMap` 可通过合并扩展,对 `SessionEvent` 的 switch 语句禁止使用 `assertNever`:插件添加的变体是合法的未知值;处理已知 case 后在 `default` 中放行。
|
||||
|
||||
## Surface 类型
|
||||
|
||||
五种产生消息的类型(`SurfaceEventType`:`user/message`、`assistant/message`、`tool/result`、`context/message`、`steering/message`)携带 surface 元数据,声明它们如何加入派生的 surface 链表。见[会话 surface RFC](../rfc/implemented/architecture/2026-06-18-session-surface.md)。
|
||||
五种产生消息的类型(`SurfaceEventType`:`user/message`、`assistant/message`、`tool/result`、`context/message`、`steering/message`)携带 surface 元数据,声明它们如何加入派生的 surface 链表。见 [session surface RFC](../rfc/implemented/architecture/2026-06-18-session-surface.md)。
|
||||
|
||||
### `SurfaceEventType`:产生消息的事件类型子集
|
||||
### `SurfaceEventType`:事件类型中产生消息的子集
|
||||
|
||||
```ts type-equiv
|
||||
export type SurfaceEventType =
|
||||
@@ -175,7 +175,7 @@ export type SurfaceOp =
|
||||
| { op: 'replace'; start: number; end: number }
|
||||
```
|
||||
|
||||
`'append'` 是正常的尾部追加路径。`replace` 遮蔽从 `start` 到 `end`(含两端,两者必须是有效的 surface 节点 seq;`start === end` 替换单个节点)的 surface 节点,并在其位置插入新节点。
|
||||
`'append'` 是正常的尾部追加路径。`replace` 遮蔽从 `start` 到 `end`(含两端)的 surface 节点(两者都必须是有效的 surface 节点 seq),并在其位置插入新节点。
|
||||
|
||||
### `SurfaceIntent`:`session.append()` 的参数
|
||||
|
||||
@@ -186,7 +186,7 @@ export interface SurfaceIntent {
|
||||
}
|
||||
```
|
||||
|
||||
`SurfaceEventType` 事件必须提供此参数:每个产生消息的事件都必须声明它如何加入 surface(派生历史的唯一来源)。非 surface 类型在编译期拒绝此参数。
|
||||
对 `SurfaceEventType` 事件必填:每个产生消息的事件都必须声明它如何加入 surface(派生历史的唯一来源)。非 surface 类型在编译期拒绝此参数。
|
||||
|
||||
### `SurfaceNode`:surface 链表中的一个节点
|
||||
|
||||
@@ -220,22 +220,22 @@ export interface SurfaceFoldResult {
|
||||
|
||||
## 派生历史:`deriveMessages()` 与 `deriveEventMessage()`
|
||||
|
||||
`Session.deriveMessages()` 将事件日志投影为模型看到的 `Message[]`。它是缓存的(每个 surface 节点在首次出现时投影一次;surface 重写触发重建)且冻结的(每次调用返回一个新数组,其中的消息是共享的深度冻结对象,因此无法通过投影来修改已记录的历史)。`deriveEventMessage(event)` 是折叠所应用的逐节点纯函数,公开暴露以便外部重建器和开发不变式检查能以完全相同的规则投影日志前缀,不会与缓存产生分歧。投影规则:
|
||||
`Session.deriveMessages()` 将事件日志投影为模型看到的 `Message[]`。它是缓存的(每个 surface 节点在首次出现时投影一次;surface 重写触发重建)且冻结的(每次调用返回一个新数组,引用共享的深冻结消息,因此通过投影修改已记录的历史在类型上不可表达)。`deriveEventMessage(event)` 是折叠所应用的逐节点纯函数,公开暴露以便外部重建器和开发不变式检查能以完全相同的规则投影日志前缀,不会与缓存产生分歧。投影规则:
|
||||
|
||||
- `user/message` → 一条 user 消息。
|
||||
- `assistant/message` → 一条 assistant 消息。原始 `assistant/chunk` 事件是回放/UI 数据,在派生中被**跳过**(组装后的消息才是权威的)。**空内容**的 `assistant/message` 也被跳过:max-tokens 截断且无内容的步骤仍会记录 `assistant/message` 以承载其 `usage`,但无内容的 assistant 轮次不得进入提供方的 transcript(文本记录)。
|
||||
- `assistant/message` → 一条 assistant 消息。原始 `assistant/chunk` 事件是回放/UI 数据,在派生中被**跳过**(组装后的消息才是权威的)。**空内容**的 `assistant/message` 也被跳过:一个因 max-tokens 截断且无内容的步骤仍会记录 `assistant/message` 以承载其 `usage`,但无内容的 assistant 轮次不得进入提供方的 transcript(文本记录)。
|
||||
- `tool/result` → 一条携带 `tool-result` 块的 user 消息。
|
||||
- `context/message`、`steering/message` → 按时间顺序插入的 user 角色消息,包裹在标签信封中(`<context source="…">…</context>`)。这是"系统提醒"模式;模型通过信封将它们与真实提示词区分开来。
|
||||
- `context/message`、`steering/message` → 以 user 角色、按时间顺序插入的消息,包裹在标记信封中(`<context source="…">…</context>`),即「系统提醒」模式;模型通过信封区分它们与真实提示词。
|
||||
|
||||
其他一切(`turn/*`、`step/*`)是结构性的,不投影为消息。token 用量在 `assistant/message.usage` 上观察(即产生它的那个步骤);操作错误的步骤编号在 `turn/end.reason` 中(`kind: 'error'` 时)。
|
||||
其他一切(`turn/*`、`step/*`)是结构性事件,不投影为消息。token 用量通过 `assistant/message.usage` 观察(产生该用量的步骤);操作错误的步骤号在 `turn/end.reason` 中(`kind: 'error'` 时)。
|
||||
|
||||
## 活跃会话 fork API
|
||||
|
||||
`ctx.sessions.create(id, { seed, meta })` 是底层的回放/fork 原语。对于普通的活跃会话 fork,`SessionStore` 暴露一个策略 API:
|
||||
|
||||
- `fork(source, boundary?, childSessionId?)` 接受一个活跃的 `Session` 对象或活跃的 `SessionId`,选取源事件直到(含)`boundary` seq(默认:当前最后一个事件),要求 boundary 事件为 `turn/end`,然后创建一个活跃的子会话,包含深克隆的种子事件和子元数据(`parentSession`、`seedLength` 以及继承的 `cwd`)。
|
||||
- `fork(source, boundary?, childSessionId?)` 接受一个活跃的 `Session` 对象或活跃的 `SessionId`,选取到 `boundary` seq(含)为止的源事件(默认为当前最后一个事件),要求 boundary 事件必须是 `turn/end`,然后创建一个活跃的子会话,包含深克隆的种子事件和子会话元数据(`parentSession`、`seedLength` 及继承的 `cwd`)。
|
||||
|
||||
显式 `boundary` 允许调用方从之前完成的轮次 fork,即使源有更新的事件或一个未关闭的当前轮次。API 拒绝非 `turn/end` 的 boundary,而不是静默裁剪。更广泛的轮次封闭性检查保留在既有的 `dsh-invariants` 插件和持久化修复路径中,而非在 `fork()` 中重复。`dsh-subagent-fork` 保留其已完成前缀裁剪逻辑,因为工具时委托通常在父轮次打开时启动;普通的会话分支应显式指定所请求的 boundary。
|
||||
显式 `boundary` 允许调用者从之前完成的轮次 fork,即使源会话有更新的事件或正在进行的轮次。API 拒绝非 `turn/end` 的 boundary,而不是静默截断。更广泛的轮次封闭性检查留在既有的 `dsh-invariants` 插件和持久化修复路径中,不在 `fork()` 中重复。`dsh-subagent-fork` 保留其已完成前缀截断逻辑,因为工具时委托通常在父轮次仍然打开时启动;普通的会话分支应显式指定请求的 boundary。
|
||||
|
||||
## 轮次的触发原因:`TurnTriggerMap`
|
||||
|
||||
@@ -293,20 +293,20 @@ interface TurnEndReasonMap {
|
||||
}
|
||||
```
|
||||
|
||||
`max-tokens` 对应同名的模型调用 `FinishReason`:轮次中任何一个步骤出现 `max-tokens`,整个轮次就以 `max-tokens` 结束而非 `completed`(截断事实优先于后续续写),消费方可以区分正常停止与被截断的情况。但这仅相对于 `completed` 而言:`disposed`/`aborted`/`error` 结果优先级更高。`rejected` 是一个零步骤轮次,其整个提示词批次被 `agent/prompt-submit` 钩子阻止(ACP 桥接层将其映射为 `cancelled`)。`interrupted` 是唯一不由循环发出的 reason,由崩溃恢复合成(见 [persistence.md](persistence.md))。两个 map 均可通过合并扩展。
|
||||
`max-tokens` 对应同名的模型调用 `FinishReason`:轮次中任何一个步骤出现 `max-tokens`,整个轮次就以 `max-tokens` 结束而非 `completed`(截断事实优先于后续的继续),消费方据此区分正常停止与被截断的情况。但这仅相对于 `completed` 而言:`disposed`/`aborted`/`error` 结果优先级更高。`rejected` 是一个零步骤轮次,其整批提示词被 `agent/prompt-submit` 钩子阻止(ACP 桥接层将其映射为 `cancelled`)。`interrupted` 是唯一不由 loop 发出的原因,由崩溃恢复合成(见 [persistence.md](persistence.md))。两个 map 均可通过合并扩展。
|
||||
|
||||
## 轮次封闭不变式
|
||||
|
||||
每个会话事件都位于一个轮次**内部**(在 `turn/start` 与其对应的 `turn/end` 之间)。循环在 `turn/start` *之后*追加排队的 `user/message` 事件;空闲时的 `agent.inject()` 将其 `context/message` 包裹在一个一次性的 `injection` 轮次中。这使得轮次成为唯一的持久性/回放边界:后端可以将最后一个 `turn/end` 之后的任何内容视为中断崩溃的尾部,而不会误丢合法记录的轮次间上下文。`dsh-invariants` 插件在开发环境中强制执行此不变式(在未打开的轮次中追加消息事件会抛出异常)。见[轮次封闭不变式 RFC](../rfc/implemented/architecture/2026-06-15-turn-enclosure-invariant.md)。
|
||||
每个会话事件都存在于一个轮次**内部**(位于 `turn/start` 与其对应的 `turn/end` 之间)。loop 在 `turn/start` *之后*追加排队的 `user/message` 事件;空闲时的 `agent.inject()` 将其 `context/message` 包裹在一个一次性的 `injection` 轮次中。这使得轮次成为唯一的持久性/回放边界:后端可以将最后一个 `turn/end` 之后的任何内容视为中断崩溃的尾部,而不会误丢合法记录的轮次间上下文。`dsh-invariants` 插件在开发环境中强制执行此不变式(在无打开轮次时追加消息事件会抛出异常)。见[轮次封闭不变式 RFC](../rfc/implemented/architecture/2026-06-15-turn-enclosure-invariant.md)。
|
||||
|
||||
## 插件贡献的仅日志事件
|
||||
|
||||
插件可以通过 declaration merging 向 `SessionEventMap` 添加额外类型。这些是**仅日志**事件:不是 `SurfaceEventType`(不携带 `surfaceOp`,不参与派生历史),但与所有事件一样,必须位于一个已打开的轮次内。完整的逐事件枚举(核心与插件贡献的,含 payload 和溯源信息)见生成的[持久化日志事件目录](../persistence-catalog.md);压缩 seam 的 `compact/*` 语义在 [compaction.md](compaction.md) 中讨论。
|
||||
插件可以通过 declaration merging 添加额外的 `SessionEventMap` 类型。这些是**仅日志**事件:不是 `SurfaceEventType`(不携带 `surfaceOp`,不参与派生历史),但与所有事件一样,必须位于一个打开的轮次内。完整的逐事件枚举(核心与插件贡献的,含 payload 与溯源信息)见生成的[持久化日志事件目录](../persistence-catalog.md);压缩 seam 的 `compact/*` 语义在 [compaction.md](compaction.md) 中讨论。
|
||||
|
||||
钩子桥接的 `hook/invoked` / `hook/result` 溯源对(来自 `@deepseek-ai/dsh-hook-protocol`)通过 `handlerId` 关联。轮次中的钩子点(`PreToolUse`/`PostToolUse`/`UserPromptSubmit`/`Stop`)在循环的已打开轮次内触发,因此其 `hook/*` 记录天然满足轮次封闭。`SessionStart` 没有 `hook/*` 记录(其注入的 `context/message` 就是持久证据),因为它没有可以容纳记录的已打开轮次(见[钩子桥接 RFC](../rfc/implemented/feature/2026-06-30-hook-bridges.md))。
|
||||
钩子桥接的 `hook/invoked` / `hook/result` 溯源对(来自 `@deepseek-ai/dsh-hook-protocol`)通过 `handlerId` 关联。轮次中的钩子点(`PreToolUse`/`PostToolUse`/`UserPromptSubmit`/`Stop`)在 loop 打开的轮次内触发,因此其 `hook/*` 记录天然满足轮次封闭。`SessionStart` 不产生 `hook/*` 记录(其注入的 `context/message` 就是持久证据),因为它没有打开的轮次来容纳记录(见[钩子桥接 RFC](../rfc/implemented/feature/2026-06-30-hook-bridges.md))。
|
||||
|
||||
## 持久性契约
|
||||
|
||||
持久化后端所依赖的契约:持久日志逐字保存每个事件,**包括** `assistant/chunk`。`seq` 必须保持连续,因此不能从规范日志中过滤掉 chunk。所有 `event.data` 必须是 JSON 可序列化的;`Session.append` 在源头强制执行此约束(对不可序列化的数据抛出异常),因此坏事件永远不会进入日志,`session.events` 始终等于后端可以持久化的内容。添加一个携带不可序列化数据的事件类型,或破坏不变式插件所检查的轮次/步骤嵌套,都是对磁盘格式的破坏性变更。
|
||||
持久化后端所依赖的约定:持久日志逐字保存每个事件,**包括** `assistant/chunk`。`seq` 必须保持连续,因此不能从规范日志中过滤掉 chunk。所有 `event.data` 必须可 JSON 序列化;`Session.append` 在源头强制执行此约束(对不可序列化的数据抛出异常),因此坏事件永远不会进入日志,`session.events` 始终等于后端能持久化的内容。添加一个携带不可序列化数据的事件类型,或破坏不变式插件所检查的 turn/step 嵌套结构,都是对磁盘格式的破坏性变更。
|
||||
|
||||
消费此契约的后端见 [persistence.md](persistence.md)。
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
skills.md: b0a847cec05651b63e63de96423170a0fd7ca2a9
|
||||
skills.zh.md: db196d2058cd1ede2ef7c9fda7668720ea2824b9
|
||||
skills.zh.md: 201bf53e95b5cbf5f8873217acca3c478cf30860
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
[English](skills.md) | 中文
|
||||
|
||||
[skill(技能)能力族](../../packages/skill)拆分为三个包(package):注册表([dsh-skill](../../packages/skill/skill),`ctx.skills`)合并各提供方的目录;本地提供方([dsh-skill-local](../../packages/skill/skill-local))扫描项目/自定义/用户目录;消费方([dsh-tool-skill](../../packages/skill/tool-skill))拥有会话前缀目录和面向模型的 `skill` 工具。Skill 是可选指令而非会话事件,因此其词汇定义在此处而非 [core.md](core.md)。
|
||||
[skill 能力族](../../packages/skill)拆分为三个包(package):注册表([dsh-skill](../../packages/skill/skill),`ctx.skills`)合并各提供方的目录;本地提供方([dsh-skill-local](../../packages/skill/skill-local))扫描项目/自定义/用户目录;消费方([dsh-tool-skill](../../packages/skill/tool-skill))拥有会话前缀目录和面向模型的 `skill` 工具。skill(技能)是可选的指令而非会话事件,因此其词汇定义在此处而非 [core.md](core.md)。
|
||||
|
||||
源码:[`packages/skill/skill/src/index.ts`](../../packages/skill/skill/src/index.ts)、[`packages/skill/skill-local/src/index.ts`](../../packages/skill/skill-local/src/index.ts) 与 [`packages/skill/tool-skill/src/index.ts`](../../packages/skill/tool-skill/src/index.ts)。
|
||||
|
||||
## 提供方注册表
|
||||
|
||||
`ctx.skills` 组合本地、内嵌、远程或其他提供方。注册是同步的;远程初始化和发现属于 await 的 `list()`。提供方对象、选项和候选项以只读方式借用,语义字段会被校验。
|
||||
`ctx.skills` 组合本地、内嵌、远程或其他提供方。注册是同步的;远程初始化与发现属于 `list()` 的 await 阶段。提供方对象、选项与候选项以只读方式借用,语义字段会被校验。
|
||||
|
||||
重名按 rank、提供方顺序、本地顺序依次解决;摘要按名称排序。`list()` 拒绝时记录日志并跳过,不缓存降级后的目录;格式错误的候选项快速失败。
|
||||
|
||||
@@ -22,7 +22,7 @@ interface SkillProvider {
|
||||
|
||||
## 本地发现优先级
|
||||
|
||||
内置的本地提供方按 rank 顺序扫描根目录:
|
||||
内置的本地提供方按 rank 顺序扫描各根目录:
|
||||
|
||||
| Rank | Source | Root |
|
||||
|---|---|---|
|
||||
@@ -32,11 +32,11 @@ interface SkillProvider {
|
||||
| 400 | `user-dsh` | `<dshHome>/skills` |
|
||||
| 500 | `user-agents` | `<agentsHome>/skills` |
|
||||
|
||||
项目根目录是最近的包含 `.git` 的祖先目录;找不到时使用当前 cwd。当 `ctx.fs` 可用时,git-root 遍历通过文件系统服务探测 `.git`,使远程或沙箱化的工作区不会回退到宿主文件系统边界。用户 DSH 根目录会跳过其 `.system` 子目录。本地提供方不附带内置系统 skill;部署方通过另一个提供方提供内置 skill。
|
||||
项目根目录为包含 `.git` 的最近祖先目录;找不到时使用当前 cwd。当 `ctx.fs` 可用时,git-root 向上查找通过文件系统服务探测 `.git`,使远程或沙箱工作区不会回退到宿主文件系统边界。用户 DSH 根目录会跳过其 `.system` 子目录。本地提供方不附带内置系统 skill;部署方通过另一个提供方提供内置 skill。
|
||||
|
||||
## Skill 标识
|
||||
## Skill 身份
|
||||
|
||||
Skill 名称为 kebab-case(`^[a-z0-9]+(?:-[a-z0-9]+)*$`)。本地提供方接受目录包(`<name>/SKILL.md`)和扁平 Markdown 文件(`<name>.md`)。嵌套递归的 `**/SKILL.md` 发现有意不在 v1 范围内。
|
||||
skill 名称为 kebab-case(`^[a-z0-9]+(?:-[a-z0-9]+)*$`)。本地提供方接受目录包(`<name>/SKILL.md`)和扁平 Markdown 文件(`<name>.md`)。嵌套递归的 `**/SKILL.md` 发现有意不在 v1 范围内。
|
||||
|
||||
```ts type-equiv
|
||||
type SkillSource = 'project-dsh' | 'project-agents' | 'runtime' | 'user-dsh' | 'user-agents' | 'custom' | (string & {})
|
||||
@@ -44,7 +44,7 @@ type SkillSource = 'project-dsh' | 'project-agents' | 'runtime' | 'user-dsh' | '
|
||||
|
||||
## 摘要、候选项与完整定义
|
||||
|
||||
`SkillSummary` 是注册表面向模型可调用的摘要形状。消费方自行选择渲染哪些字段;会话目录仅使用 `name` 和 `description`,从不使用正文或绝对文件路径。`disableModelInvocation` 将 skill 从模型列表中隐藏,但允许受信代码按名称加载。
|
||||
`SkillSummary` 是注册表中可供模型调用的摘要形状。消费方自行选择渲染哪些字段;会话目录仅使用 `name` 和 `description`,从不使用 body 或绝对文件路径。`disableModelInvocation` 将 skill 从模型列表中隐藏,但允许受信代码按名称加载。
|
||||
|
||||
```ts type-equiv
|
||||
interface SkillSummary {
|
||||
@@ -58,7 +58,7 @@ interface SkillSummary {
|
||||
}
|
||||
```
|
||||
|
||||
`SkillCandidate` 是提供方到注册表的形状。`locator` 是提供方的不透明状态;注册表只存储它并在调用获胜提供方的 `get()` 时回传。
|
||||
`SkillCandidate` 是提供方到注册表的形状。`locator` 是提供方的不透明状态;注册表只存储它并在调用获胜提供方的 `get()` 时传回。
|
||||
|
||||
```ts type-equiv
|
||||
interface SkillCandidate extends SkillSummary {
|
||||
@@ -69,7 +69,7 @@ interface SkillCandidate extends SkillSummary {
|
||||
}
|
||||
```
|
||||
|
||||
`SkillDefinition` 是 `ctx.skills.get()` 返回的完整解析结果,供 `skill` 工具使用。`resourceBase` 告诉工具如何为本地、URL 或提供方管理的 skill 渲染相对资源指引。
|
||||
`SkillDefinition` 是 `ctx.skills.get()` 返回的完整解析结果,供 `skill` 工具使用。`resourceBase` 告知工具如何为本地、URL 或提供方管理的 skill 渲染相对资源引导。
|
||||
|
||||
```ts type-equiv
|
||||
type SkillResourceBase =
|
||||
@@ -96,7 +96,7 @@ type SkillRegistration = Omit<SkillDefinition, 'provider'> & {
|
||||
|
||||
## 查找与配置
|
||||
|
||||
Skill 查找对 cwd 敏感,因为提供方可能暴露工作区本地的 skill;可选的 signal 为调用方取消提供方工作。提供方接收同一个只读选项对象,用于缓存标识和加载。取消在目录选择前后(包括缓存命中)都会检查,并同时竞争发现和完整定义加载。如果找不到 git 根目录,本地提供方将提供的 cwd 本身视为项目根目录。
|
||||
skill 查找对 cwd 敏感,因为提供方可能暴露工作区本地的 skill;可选的 signal 为调用方取消提供方的工作。提供方接收与缓存标识和加载相同的只读选项对象。取消在目录选择前后(包括缓存命中时)都会检查,并与发现和完整定义加载竞争。如果找不到 git root,本地提供方将所提供的 cwd 本身视为项目根目录。
|
||||
|
||||
```ts type-equiv
|
||||
interface SkillLookupOptions {
|
||||
@@ -105,7 +105,7 @@ interface SkillLookupOptions {
|
||||
}
|
||||
```
|
||||
|
||||
注册表只拥有其发现缓存上限。本地提供方拥有文件系统根目录(`dshHome`、`agentsHome` 和 `customSkillDirs`)。消费方拥有其目录描述上限。
|
||||
注册表只拥有其发现缓存上限。本地提供方拥有文件系统根目录(`dshHome`、`agentsHome` 与 `customSkillDirs`)。消费方拥有其目录描述上限。
|
||||
|
||||
```ts type-equiv
|
||||
interface Config {
|
||||
@@ -115,6 +115,6 @@ interface Config {
|
||||
|
||||
## 会话目录与工具契约
|
||||
|
||||
`dsh-tool-skill` 通过 `agent/session-prefix` 贡献一个 user-role 的 `<system-reminder>`。目录包含按名称排序的 skill `name` 和经过规范化、XML 转义的 `description`;不包含正文、路径、来源、提供方和路由提示。前缀发现通过 `SkillLookupOptions` 转发调用方的 abort signal。`catalogDescriptionMaxLength` 是消费方配置的描述上限,默认 `500`,整数最小值 `3`。其仅限请求、记录于 header 的生命周期由 [session-prefix RFC](../rfc/implemented/feature/2026-07-07-session-prefix.md) 定义。
|
||||
`dsh-tool-skill` 通过 `agent/session-prefix` 贡献一条 user-role 的 `<system-reminder>`。目录包含排序后的 skill `name` 和经过规范化、XML 转义的 `description`;不包含 body、路径、来源、提供方和路由提示。前缀发现通过 `SkillLookupOptions` 转发调用方的 abort signal。`catalogDescriptionMaxLength` 是消费方配置的描述上限,默认值 `500`,整数最小值 `3`。其仅请求级别、记录于 header 的生命周期由 [session-prefix RFC](../rfc/implemented/feature/2026-07-07-session-prefix.md) 定义。
|
||||
|
||||
面向模型的 `skill({ name })` 工具校验 kebab-case 名称,为调用 agent 的 cwd 加载完整定义,将未解决的 skill 报告为未知或不再可用,拒绝 `disableModelInvocation` 的 skill,并返回包含 `<skill_content name="...">`、`<skill_resources>` 和 `<skill_instructions>` 的工具结果。`resourceBase` 仅按需解析显式引用的脚本、参考资料和资产;加载结果不枚举 skill 目录。工具结果是模型获取完整指令的可见路径。
|
||||
面向模型的 `skill({ name })` 工具校验 kebab-case 名称,为调用方 agent 的 cwd 加载完整定义,将未解析的 skill 报告为 unknown 或 no longer available,拒绝 `disableModelInvocation` 的 skill,并返回包含 `<skill_content name="...">`、`<skill_resources>` 和 `<skill_instructions>` 的工具结果。`resourceBase` 仅按需解析显式引用的脚本、参考资料和资产;加载结果不枚举 skill 目录。工具结果是模型获取完整指令的可见路径。
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
subagent.md: eb9160abaee26969aecdc533fb9fd56fae18b7fa
|
||||
subagent.zh.md: f29ebcd50b5d3a6d961583e381b81eb88ab95823
|
||||
subagent.zh.md: c8f680217a9f62f245a4de81cbc546deba87bbed
|
||||
@@ -2,15 +2,15 @@
|
||||
|
||||
[English](subagent.md) | 中文
|
||||
|
||||
subagent seam:一个 agent(智能体)将工作委派给子 agent。与 [bash](bash.md) 类似,它是**一项可选能力**,不属于 agent loop(智能体循环)的主干,因此其词汇定义在这里而非 [core.md](core.md)。但它在一个维度上与其他所有 seam 不同:**多个提供方实现在同一个上下文中共存**,按名称注册(`ctx.subagents`),而 bash 只允许一个执行器。注册表的形状参照 [LLM 适配器注册表](llm-streaming.md),而非单服务的 bash 执行器。
|
||||
subagent seam:一个 agent(智能体)将工作委派给子 agent。与 [bash](bash.md) 一样,它是**一项可选能力**,不属于 agent loop(智能体循环)主干,因此其词汇定义在此而非 [core.md](core.md) 中。但它在一个维度上与其他所有 seam 不同:**同一上下文中可共存多个提供方实现**,按名称注册(`ctx.subagents`),而 bash 只允许一个执行器。注册表的形状参照 [LLM 适配器注册表](llm-streaming.md),而非单服务的 bash 执行器。
|
||||
|
||||
接口:[dsh-subagent](../../packages/subagent/subagent)(`ctx.subagents` + 下文词汇)。实现是兄弟包(`dsh-subagent-spawn`、`-fork`、`-acp`);面向模型的消费方是 [dsh-tool-subagent](../../packages/subagent/tool-subagent)。提案与设计动机见 [subagent RFC](../rfc/implemented/feature/2026-06-21-subagent-capability-seam.md)。
|
||||
接口:[dsh-subagent](../../packages/subagent/subagent)(`ctx.subagents` + 下文词汇)。实现为兄弟包(`dsh-subagent-spawn`、`-fork`、`-acp`);面向模型的消费方是 [dsh-tool-subagent](../../packages/subagent/tool-subagent)。提案与设计动机见 [subagent RFC](../rfc/implemented/feature/2026-06-21-subagent-capability-seam.md)。
|
||||
|
||||
源码:[`packages/subagent/subagent/src/types.ts`](../../packages/subagent/subagent/src/types.ts)
|
||||
|
||||
## 两类能力,两种发现方式
|
||||
|
||||
提供方通过一个静态描述符公布其**启动时**特性,服务在运行实例存在之前就会检查它;如果请求需要提供方不具备的特性,会被大声拒绝(`SubagentError('UNSUPPORTED_CAPABILITY')`),绝不会接受后静默忽略。**运行时**特性(steering(中途引导)、resume)则是 [`SubagentRun`](#a-live-run-subagentrun) 上的可选方法:方法的存在本身即为能力,TypeScript 的类型收窄就是发现机制。
|
||||
提供方通过一个静态描述符公布其**启动时**特性,服务在 run 存在之前即行检查;如果请求依赖提供方不具备的特性,会被大声拒绝(`SubagentError('UNSUPPORTED_CAPABILITY')`),绝不会被接受后静默忽略。**运行时**特性(steering(中途引导)、resume)则是 [`SubagentRun`](#a-live-run-subagentrun) 上的可选方法——方法的存在即为能力,TypeScript 的类型收窄即为发现机制。
|
||||
|
||||
```ts type-equiv
|
||||
interface SubagentCapabilities {
|
||||
@@ -23,7 +23,7 @@ interface SubagentCapabilities {
|
||||
|
||||
## 启动请求
|
||||
|
||||
工具层根据模型输入和自身配置构建此请求;服务在 `start` 之前对照指定提供方进行校验。必填的 `parent` 提供会话 cwd、血统链和委派深度。可选的 output schema、depth、tool filter 和 persona 需要对应的能力标志位。不支持的 schema 在启动时即失败;进程内后端将 filter 和 persona 限定在子 agent 创建阶段,并通过一个强制捕获工具实现所支持的 object-rooted schema。
|
||||
工具层根据模型输入和自身配置构建此请求;服务在 `start` 之前针对指定提供方进行校验。必填的 `parent` 提供会话 cwd、谱系与委派深度。可选的 output schema、depth、tool filter 和 persona 需要对应的能力 flag 匹配。不支持的 schema 在启动时即失败;进程内后端将 filter 和 persona 的作用域限定在子 agent 创建阶段,并通过强制 capture tool 实现所支持的 object-rooted schema。
|
||||
|
||||
```ts type-equiv
|
||||
interface SubagentStartRequest {
|
||||
@@ -38,11 +38,11 @@ interface SubagentStartRequest {
|
||||
}
|
||||
```
|
||||
|
||||
`signal` 是就绪前后唯一的取消通道。[subagent 组合控制 RFC](../rfc/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md) 拥有 persona、实时全局工具过滤、绝对深度以及「可见性而非权限」的设计理由。
|
||||
`signal` 是就绪前后唯一的取消通道。[subagent 组合控制 RFC](../rfc/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md) 负责 persona、运行时全局工具过滤、绝对深度以及「可见性而非权限」的设计理由。
|
||||
|
||||
## 终态结果:`SubagentResult`
|
||||
|
||||
一次运行的结果,由 `SubagentRun.result` resolve。`structured` 仅在请求了 `outputSchema` 且成功满足时才存在;请求 schema 不保证一定能得到,提供方在子 agent 失败或结束时未产出有效捕获时可能返回 `stopReason: 'error'`。非 `completed` 的 `stopReason` 意味着 `output` 可能不完整:消费方将其映射为 `isError` 的工具结果,而非把不完整的输出当作成功上报。
|
||||
一次 run 的最终产出,由 `SubagentRun.result` resolve。`structured` 仅在请求了 `outputSchema` 且成功满足时才存在;请求 schema 不保证一定能得到它,当子 agent 失败或结束时未产出有效 capture 时,提供方可能返回 `stopReason: 'error'`。非 `completed` 的 `stopReason` 意味着 `output` 可能不完整——消费方将其映射为 `isError` 的工具结果,而非将部分输出报告为成功。
|
||||
|
||||
```ts type-equiv
|
||||
interface SubagentResult {
|
||||
@@ -52,7 +52,7 @@ interface SubagentResult {
|
||||
}
|
||||
```
|
||||
|
||||
`SubagentStopReason` 是一个[可合并扩展的派生联合类型](core.md#the-map--derived-union-pattern):后端可以添加变体,因此消费方应对已知 case 分支处理,并将未知的终态原因视为失败:
|
||||
`SubagentStopReason` 是一个[可合并扩展的派生联合类型](core.md#the-map--derived-union-pattern)——后端可以添加变体,因此消费方应对已知 case 分支处理,将未知的终态原因视为失败:
|
||||
|
||||
```ts type-equiv
|
||||
interface SubagentStopReasonMap {
|
||||
@@ -64,9 +64,9 @@ interface SubagentStopReasonMap {
|
||||
}
|
||||
```
|
||||
|
||||
## 活跃运行:`SubagentRun`
|
||||
## 活跃 run:`SubagentRun`
|
||||
|
||||
`SubagentRun` 是消费方持有的、指向一个就绪子 agent 的句柄。消费方 await `result` 并始终 dispose 该运行以达到静止态。子 agent 失败以非 completed 的 stop reason resolve;只有无法表示的基础设施故障才会 reject。可选的 `sendMessage` 和 `resume` 方法通过其存在性公布运行时能力。
|
||||
`SubagentRun` 是消费方持有的、指向一个就绪子 agent 的句柄。消费方 await `result` 并始终 dispose(资源释放)该 run 以达到静止状态。子 agent 失败时以非 completed 的 stop reason resolve;只有不可表示的基础设施故障才会 reject。可选的 `sendMessage` 和 `resume` 方法通过自身的存在来公布运行时能力。
|
||||
|
||||
```ts type-equiv
|
||||
interface SubagentRun {
|
||||
@@ -80,7 +80,7 @@ interface SubagentRun {
|
||||
|
||||
## 提供方 seam:`SubagentProvider`
|
||||
|
||||
每个提供方是一个具名的子 agent 传输层,多个提供方可以共存。服务在 `start()` 之前校验请求的启动时能力。`inheritsParentContext` 仅描述对话种子行为(`fork`:true;`spawn` 和 `acp`:false),使消费方能生成准确的面向模型的措辞,而不暗示继承了工具、服务或权限。
|
||||
每个提供方是一个具名的子 agent 传输层,多个提供方可以共存。服务在 `start()` 之前校验请求的启动时能力。`inheritsParentContext` 仅描述对话种子注入(`fork`:true;`spawn` 和 `acp`:false),使消费方能生成准确的面向模型的措辞,而不暗示继承了工具、服务或权限。
|
||||
|
||||
```ts type-equiv
|
||||
interface SubagentProvider {
|
||||
@@ -91,11 +91,11 @@ interface SubagentProvider {
|
||||
}
|
||||
```
|
||||
|
||||
`start()` 仅在运行就绪时才 fulfill。服务观察其 result、发出 `subagent/start`,并返回同一个 run;rejection 意味着提供方已自行清理,且不发出生命周期事件对。进程内子 agent 可通过 `ctx.agents` 发现,远程子 agent 则不必如此。`subagent/end` 报告最终输出或基础设施故障。两个事件均为仅观察事件,包含监听器异常。
|
||||
`start()` 仅在 run 就绪时 fulfill。服务观察其 result、发出 `subagent/start`,并返回同一个 run;rejection 意味着提供方已自行清理,不发出生命周期配对事件。进程内子 agent 可通过 `ctx.agents` 发现,远程子 agent 则不必如此。`subagent/end` 报告最终输出或基础设施故障。两个事件均为仅观察事件,包含监听器异常。
|
||||
|
||||
## 进程内后端:深度与种子
|
||||
|
||||
spawn 和 fork 后端通过 `parent.ctx` 创建一个普通 agent,将取消信号传入核心创建过程,并通过 `AgentHandle` 进行 dispose。提供方被移除时会阻止新的 start,但不会撤销已接受的运行。每个子 agent 获得一个新的扁平作用域,而非继承父级的注册。深度和 fork 种子复用既有的 agent 与会话词汇:
|
||||
spawn 和 fork 后端通过 `parent.ctx` 创建一个普通 agent,将取消信号传入核心创建流程,并通过 `AgentHandle` 进行 dispose。移除提供方会阻止新的 start,但不会撤销已接受的 run。每个子 agent 获得一个新的扁平作用域,而非继承父级注册。深度与 fork 种子注入复用既有的 agent 和会话词汇:
|
||||
|
||||
- **委派深度**是一个可合并扩展的 `AgentOptions.subagentDepth` 字段(顶层 agent 为 `0`,子 agent 为 parent + 1)。只有 `undefined` 表示顶层;每个已存储的 present 值必须是非负安全整数。该 seam 拥有此字段:循环既不设置也不读取它。嵌套 spawn 校验其父级的已存储深度,拒绝超出安全整数范围的派生子深度,并将已定义的绝对 `request.maxDepth` 上限应用于该子 agent。
|
||||
- **Fork 种子**使用 `CreateAgentOptions.seed`(一个 `SessionEvent[]` 前缀,经由 `AgentLoop.createAgent` → `ctx.sessions.prepare({ seed })` 传递,与 resume 使用的是同一原语)。fork 后端传入父级日志的一段*平衡的已完成轮次前缀*:父级事件直到并包含其最后一个 `turn/end`。因此种子从 0 开始连续,[invariants](../../packages/support/invariants) 的回放能接受它(进行中的、未平衡的轮次被排除在外)。
|
||||
- **委派深度**是一个可合并扩展的 `AgentOptions.subagentDepth` 字段(顶层 agent 为 `0`,子 agent 为 parent + 1)。只有 `undefined` 表示顶层;所有已存储的值必须是非负安全整数。该字段归 seam 所有——循环既不设置也不读取它——因此嵌套 spawn 会校验父级的已存储深度,拒绝超出安全整数域的派生子深度,并在定义了绝对 `request.maxDepth` 上限时将其施加于子 agent。
|
||||
- **Fork 种子注入**使用 `CreateAgentOptions.seed`(一个 `SessionEvent[]` 前缀,经由 `AgentLoop.createAgent` → `ctx.sessions.prepare({ seed })` 传递,与 `resume` 使用的原语相同)。fork 后端传入父级日志的一段*平衡的已完成轮次前缀*——父级事件直到并包括其最后一个 `turn/end`——因此种子从 0 连续,[invariants](../../packages/support/invariants) 回放可以接受它(进行中的、未平衡的轮次被排除在外)。
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
system-prompt.md: 175f2af407e5c39a24f0f8f8e4e664063b897ead
|
||||
system-prompt.zh.md: ea1f48c56f18c97203e1052d6d0eca72710e942d
|
||||
system-prompt.zh.md: 8827ac0b915915f716643e0a9e04edaacedbc868
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
[English](system-prompt.md) | 中文
|
||||
|
||||
[system-prompt 包](../../packages/core/system-prompt)定义了提示词贡献方与单次组装调用之间交换的数据。包的 [README](../../packages/core/system-prompt/README.md) 文档记录了注册、排序、作用域与渲染行为;本页固定各插件实现或传递的跨包字面形状。
|
||||
[system-prompt 包](../../packages/core/system-prompt)负责管理 prompt 贡献者与一次组装调用之间交换的数据。该包的 [README](../../packages/core/system-prompt/README.md) 记录了注册、排序、作用域与渲染行为;本页固定各插件实现或传递的跨包字面形状。
|
||||
|
||||
源码:[`packages/core/system-prompt/src/index.ts`](../../packages/core/system-prompt/src/index.ts)。
|
||||
|
||||
## 组装上下文
|
||||
|
||||
`AssembleContext` 标识单次组装所解析的作用域层。它可通过合并扩展:`dsh-agent` 添加了可选的运行时 `agent` 字段,`assembleContextFor(agent)` 同时设置该字段与 `scope`。
|
||||
`AssembleContext` 标识一次组装所解析的作用域层。它可通过合并扩展:`dsh-agent` 添加可选的活跃 `agent` 字段,`assembleContextFor(agent)` 同时设置该字段与 `scope`。
|
||||
|
||||
```ts type-equiv
|
||||
interface AssembleContext {
|
||||
@@ -18,7 +18,7 @@ interface AssembleContext {
|
||||
|
||||
## 工具提供方结果
|
||||
|
||||
`ToolProviderResult.schemas` 是当前组装中模型可见的工具集。`knownNames` 是提供方在限制前的完整名称集合,用于区分「配置名拼写错误」与「已知工具在此作用域下被有意隐藏」。
|
||||
`ToolProviderResult.schemas` 是当前组装中对模型可见的工具集合。`knownNames` 是提供方在限制前的名称全集,用于区分「配置名拼写错误」与「已知工具在此作用域中被有意隐藏」。
|
||||
|
||||
```ts type-equiv
|
||||
interface ToolProviderResult {
|
||||
@@ -27,9 +27,9 @@ interface ToolProviderResult {
|
||||
}
|
||||
```
|
||||
|
||||
## 提示词段
|
||||
## Prompt 段落
|
||||
|
||||
`PromptSection` 是一个只读的同进程注册契约。其文本可以是静态的,也可以从当前组装上下文动态解析。
|
||||
`PromptSection` 是一份只读的同进程注册契约。其文本可以是静态的,也可以从当前组装上下文动态解析。
|
||||
|
||||
```ts type-equiv
|
||||
interface PromptSection {
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
tools.md: f8be67054cd81027d4b751329948a784fa4f0ed9
|
||||
tools.zh.md: 080d0d99decb8630525e434a8079e29591e63ce9
|
||||
tools.zh.md: 8b77259b37b0e4c2118cc91af0e6dc3ca27c0479
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
[English](tools.md) | 中文
|
||||
|
||||
[dsh-tools](../../packages/core/tools) 的工具流水线。[core.md](core.md) 介绍了 `ToolDefinition` 作为唯一被提升到主干的流水线编写类型,以及 `ToolSchema` 作为面向模型的协议格式(wire format)。本页拥有完整的 `ToolDefinition`、构建它的类型化 schema DSL、带守卫的执行形状,以及 UI 展示词汇。
|
||||
[dsh-tools](../../packages/core/tools) 的工具流水线。[core.md](core.md) 介绍了 `ToolDefinition`(唯一被提升到主干的流水线编写类型)和 `ToolSchema`(面向模型的协议格式(wire format)形状)。本页拥有完整的 `ToolDefinition`、用于构建它的类型化 schema DSL、受保护的执行形状,以及 UI 展示词汇。
|
||||
|
||||
源码:[`packages/core/tools/src/index.ts`](../../packages/core/tools/src/index.ts) · [`packages/core/tools/src/schema.ts`](../../packages/core/tools/src/schema.ts) · [`packages/core/tools/src/presentation.ts`](../../packages/core/tools/src/presentation.ts)
|
||||
|
||||
## `ToolDefinition`:一个已注册的工具
|
||||
## `ToolDefinition` — 一个已注册的工具
|
||||
|
||||
一个 `ToolSchema`(面向模型的字段)加上 `execute` 函数与可选的 UI 展示器。注册表持有这些定义;agent loop(智能体循环)通过它们分发调用。注册表的 `schemas()` 通过显式白名单构建面向模型的 `ToolSchema[]`:`execute`/`presentCall`/`presentResult` 绝不能泄漏到模型请求中。
|
||||
一个 `ToolSchema`(面向模型的字段)加上 `execute` 函数和可选的 UI 展示器。注册表持有这些定义;agent loop(智能体循环)通过它们分派调用。注册表的 `schemas()` 通过显式白名单构建面向模型的 `ToolSchema[]`——`execute`/`presentCall`/`presentResult` 绝不能泄漏到模型请求中。
|
||||
|
||||
```ts type-equiv
|
||||
interface ToolDefinition extends ToolSchema {
|
||||
@@ -42,11 +42,11 @@ interface ToolDefinition extends ToolSchema {
|
||||
}
|
||||
```
|
||||
|
||||
`execute` 接收 `args: unknown`:原始的 `ToolDefinition` 自行校验输入。第一方工具不需要手写校验;它们使用 `defineTool`,由后者代为校验和收窄类型。
|
||||
`execute` 接收 `args: unknown`——原始的 `ToolDefinition` 自行校验输入。第一方工具不需要手写校验;它们使用 `defineTool`,由后者代为校验并收窄类型。
|
||||
|
||||
## 类型化 schema DSL
|
||||
|
||||
插件作者为每个属性编写带有布尔值 `required: true` 的规格,类型层面的辅助工具将规格映射为 `execute` 的参数类型——零类型断言。该 DSL 是为 `ToolDefinition` *提供类型*的机制;它有意作为子页面细节,不属于核心。
|
||||
插件作者为每个属性编写带有布尔值 `required: true` 的规格,类型层面的辅助工具将规格映射为 `execute` 的参数类型——零类型断言。该 DSL 是为 `ToolDefinition` 提供类型的*机制*;它有意作为子页面细节,而非核心内容。
|
||||
|
||||
源码:[`packages/core/tools/src/schema.ts`](../../packages/core/tools/src/schema.ts)
|
||||
|
||||
@@ -72,7 +72,7 @@ interface SchemaProp {
|
||||
type SchemaSpec = Record<string, SchemaProp>
|
||||
```
|
||||
|
||||
`SchemaType` 是原始联合类型 `'string' | 'number' | 'boolean' | 'object' | 'array'`。`InferArgs<S>` 将一个 `SchemaSpec` 映射为 TS 参数类型:`required: true` 的属性成为必选键,其余为真正的可选:
|
||||
`SchemaType` 是原始联合类型 `'string' | 'number' | 'boolean' | 'object' | 'array'`。`InferArgs<S>` 将一个 `SchemaSpec` 映射为 TS 参数类型——`required: true` 的属性成为必选键,其余为真正的可选:
|
||||
|
||||
```ts type-equiv
|
||||
type InferArgs<S extends SchemaSpec> = Simplify<
|
||||
@@ -81,11 +81,11 @@ type InferArgs<S extends SchemaSpec> = Simplify<
|
||||
>
|
||||
```
|
||||
|
||||
`defineTool({ name, description, parameters, execute, … })` 将各部分串联:`parameters` 是一个 `SchemaSpec`,`execute(args, exec)` 得到 `args: InferArgs<typeof parameters>`,辅助函数将规格转换为 JSON Schema(`schemaSpecToJsonSchema`)用于协议传输,并在类型化函数体运行前校验模型生成的参数(`validateArgs`)。不匹配时抛出 `ToolArgsError`(`code: 'INVALID_ARGS'`),注册表将其转为 `isError` 结果以便模型自我修正。为什么用自定义 DSL 而非 schemastery:工具参数需要的是 JSON Schema(LLM(大语言模型)协议格式),不是校验/转换——轻量 DSL 以最小表面积提供最佳编写体验。
|
||||
`defineTool({ name, description, parameters, execute, … })` 将各部分串联:`parameters` 是一个 `SchemaSpec`,`execute(args, exec)` 获得 `args: InferArgs<typeof parameters>`,辅助函数将规格转换为 JSON Schema(`schemaSpecToJsonSchema`)用于协议传输,并在类型化函数体运行前校验模型生成的参数(`validateArgs`)。校验不通过时抛出 `ToolArgsError`(`code: 'INVALID_ARGS'`),注册表将其转为 `isError` 结果以便模型自行修正。为何用自定义 DSL 而非 schemastery:工具参数需要 JSON Schema(LLM(大语言模型)的协议格式),而非校验/转换——轻量 DSL 以最小的接口面积提供最佳的编写体验。
|
||||
|
||||
注册是受信的同进程契约。注册表以 readonly 方式借用类型化定义作为输入,仅校验语义要求(如 `timeoutMs` 必须为正有限值);`schemas()` 在模型边界处具象化显式的面向模型投影,使执行与展示共享同一份已解析定义,而不会将回调泄漏到协议上。
|
||||
注册是一个受信任的同进程契约。注册表以 readonly 输入借用类型化定义,仅校验语义要求(如 `timeoutMs` 必须为正有限值);`schemas()` 在模型边界处物化显式的面向模型投影,使执行和展示共享同一份已解析定义,而不会将回调泄漏到协议上。
|
||||
|
||||
## `ToolRestriction`:单个作用域的实时全局过滤器
|
||||
## `ToolRestriction` — 单个作用域的实时全局过滤器
|
||||
|
||||
`ToolRestriction` 仅作用于实时的部署全局工具层。注册表将 readonly 名称编译为私有集合,对多个限制取交集,再叠加作用域本地工具。仅 deny 的过滤器允许后续未列出的全局工具通过,而 allow 列表则排除它们。
|
||||
|
||||
@@ -98,7 +98,7 @@ interface ToolRestriction {
|
||||
|
||||
## 执行:可扩展的 waterfall(瀑布式事件)加单调策略
|
||||
|
||||
`ctx.tools.execute()` 接受调用方拥有的 `ToolExecutionInput`,将其解析后的 JSON 参数一次性具象化为流水线拥有的 `ToolExecution`,然后将该调用依次通过 `tools/pre-execute`(可重排的 allow/deny/ask waterfall)→ 已注册的单调守卫 → `tools/execute`(around-dispatch 包装层)→ `tools/post-execute`(检查/替换结果)→ `tools/result`(不可变的权威结果)。最终结果是一个 `ToolExecutionResult`。
|
||||
`ctx.tools.execute()` 接收调用方拥有的 `ToolExecutionInput`,将其解析后的 JSON 参数一次性物化为流水线拥有的 `ToolExecution`,然后依次通过 `tools/pre-execute`(可重排的 allow/deny/ask waterfall)→ 已注册的单调 guard → `tools/execute`(around-dispatch 包装层)→ `tools/post-execute`(检查/替换结果)→ `tools/result`(不可变的权威结果)。最终产出为 `ToolExecutionResult`。
|
||||
|
||||
```ts type-equiv
|
||||
type ToolExecutionToken = symbol & { readonly [toolExecutionTokenBrand]: true }
|
||||
@@ -129,9 +129,9 @@ interface ToolExecution extends ToolExecutionInput {
|
||||
}
|
||||
```
|
||||
|
||||
`ToolExecutionToken` 是一个不透明的运行时 `Symbol`,仅用于身份比较。在策略执行之前,`execute()` 具象化并冻结参数、拒绝非 JSON 输入、分配 token。身份字段和可选的 parent token 保持 readonly;只有 `signal` 可在 dispatch 前后变化。最终观察者接收到的是冻结的执行身份。
|
||||
`ToolExecutionToken` 是一个不透明的运行时 `Symbol`,仅用于身份比较。在策略执行之前,`execute()` 物化并冻结参数、拒绝非 JSON 输入、分配 token。身份字段和可选的 parent token 保持 readonly;只有 `signal` 可以在分派前后变化。最终观察者接收到的是冻结的执行身份。
|
||||
|
||||
`ToolGuard` 是感知作用域的最终 pre-dispatch 策略。其形状有意不包含 allow 结果:`undefined` 保留 waterfall 的决策,而返回的 reason 只能缩减权限,因此后续监听器无法撤销它。
|
||||
`ToolGuard` 是感知作用域的最终预分派策略。其形状有意不包含 allow 结果:`undefined` 保留 waterfall 的决策,而返回的 reason 只能缩减权限,因此后续监听器无法撤销它。
|
||||
|
||||
```ts type-equiv
|
||||
type ToolGuard = (execution: Readonly<ToolExecution>) => string | undefined
|
||||
@@ -168,9 +168,9 @@ interface ToolExecutionResult {
|
||||
}
|
||||
```
|
||||
|
||||
结果仅承载结果本身。调用身份保留在不可变的 `ToolExecution` 上,后者伴随结果通过每个钩子,也保留在持久化的 `tool/call` / `tool/result` 会话事件上,因此包装层无法创建第二个相互矛盾的身份。
|
||||
结果仅承载产出。调用身份保留在不可变的 `ToolExecution` 上,后者伴随结果经过每个钩子,并出现在持久化的 `tool/call` / `tool/result` 会话事件上,因此包装层无法创建第二个相互矛盾的身份。
|
||||
|
||||
注册表在 `tools/result` 之前立即具象化并冻结最终接受的结果。其 content、结构化错误、附加上下文和展示元数据必须通过 JSON 无损往返;无效结果会被转为 JSON 安全的 `isError` 结果,确保被观察到的实时结果对后续持久化的 `tool/result` 追加是安全的。
|
||||
注册表在 `tools/result` 之前立即物化并冻结最终接受的结果。其内容、结构化错误、附加上下文和展示元数据必须通过 JSON 无损往返;无效的产出会被转为 JSON 安全的 `isError` 结果,从而保证被观察到的实时产出对后续持久化的 `tool/result` 追加是安全的。
|
||||
|
||||
每个拦截 waterfall 返回一个类型化的 **Decision**(与 `agent/*` seam 共享的惯用模式)。`tools/pre-execute` 监听器接收 `(exec, next)` 并返回 `PreToolDecision`;`tools/execute` 包装层返回 `ToolExecutionResult`;`tools/post-execute` 监听器接收 `(exec, result, next)` 并返回 `PostToolDecision`:
|
||||
|
||||
@@ -187,13 +187,13 @@ type PostToolDecision =
|
||||
| { kind: 'block'; feedback: ContentBlock[]; additionalContext?: HookContext }
|
||||
```
|
||||
|
||||
调用 `next()` 走默认路径,或返回 decision 以短路。Pre-policy 可以 deny 或 ask;只有 `allowed-once` 才继续执行,而 non-grant、缺少审批通道或服务、或无 agent 的请求都会变为 denial。守卫仍可施加最终 denial。参数不可被改写,因为历史记录、审计、UI 和执行必须一致。
|
||||
调用 `next()` 获取默认决策,或直接返回一个决策以短路。前置策略可以 deny 或 ask;只有 `allowed-once` 才继续执行,而未授权、缺少审批通道或服务、或无 agent 的请求都会变为拒绝。Guard 仍可施加最终拒绝。参数不可被改写,因为历史记录、审计、UI 和执行必须保持一致。
|
||||
|
||||
Post-policy 可以替换 content;block 会变为包含其纠正反馈的 `isError` 结果。`tools/result` 在归一化后接收冻结的执行和结果;观察者无法转换它们,观察者的失败被隔离。未知工具和抛出异常的工具都变为结构化错误(`ToolNotFoundError` 映射为 `UNKNOWN_TOOL`),调用失败但不终止当前轮次。
|
||||
后置策略可以替换内容;block 会变为包含纠正反馈的 `isError` 结果。`tools/result` 在归一化后接收冻结的执行和结果;观察者无法对其进行变换,观察者的失败也会被隔离。未知工具和抛出异常的工具都会变为结构化错误(`ToolNotFoundError` 映射为 `UNKNOWN_TOOL`),调用失败但不终止当前轮次。
|
||||
|
||||
## 结构化输出 schema 子集
|
||||
|
||||
调用方用来向 subagent 要求机器可读结果的词汇(`SubagentStartRequest.outputSchema`,见 [subagent.md](subagent.md#the-start-request)),或工作流 `agent()` 调用使用的词汇。它有意**不是**完整的 JSON Schema:schema 原样传递给模型作为强制工具的 `parameters`,产出的值由客户端的 `validateStructuredValue` 校验——因此每个被接受的关键字都必须是校验器实际执行的,`assertSupportedOutputSchema` 会大声拒绝其他任何内容(`OutputSchemaError`,列出所有违规)。两个遍历器都只处理自有可枚举属性(JSON 不携带其他东西),并拒绝会有损序列化的非普通对象(`Date`、`Map`)。
|
||||
调用方用来向 subagent 要求机器可读结果的词汇(`SubagentStartRequest.outputSchema`,见 [subagent.md](subagent.md#the-start-request)),或工作流 `agent()` 调用使用的词汇。它有意**不是**完整的 JSON Schema:schema 原样传给模型作为强制工具的 `parameters`,产出的值由 `validateStructuredValue` 在客户端校验——因此每个被接受的关键字都必须是校验器实际执行的,`assertSupportedOutputSchema` 会大声拒绝其他任何内容(`OutputSchemaError`,列出所有违规项)。两个遍历器仅推理自有可枚举属性(JSON 不携带其他内容),并拒绝会有损序列化的非纯对象(`Date`、`Map`)。
|
||||
|
||||
```ts type-equiv
|
||||
type StructuredScalar = string | number | boolean | null
|
||||
@@ -219,7 +219,7 @@ interface StructuredSchemaNode {
|
||||
}
|
||||
```
|
||||
|
||||
schema 是一个以 object 为根的节点(`enum`/`const` 仅限标量;`description`/`title`/`default`/`examples` 是注解,允许但忽略,仍要求为 JSON 数据——它们随协议传输):
|
||||
schema 是一个以 object 为根的节点(`enum`/`const` 仅限标量;`description`/`title`/`default`/`examples` 是注解,允许但忽略,但仍要求为 JSON 数据——它们随协议传输):
|
||||
|
||||
```ts type-equiv
|
||||
type StructuredOutputSchema = StructuredSchemaNode & { type: 'object' }
|
||||
@@ -227,11 +227,11 @@ type StructuredOutputSchema = StructuredSchemaNode & { type: 'object' }
|
||||
|
||||
## 工具展示 UI 词汇
|
||||
|
||||
工具希望其调用在 UI 中如何呈现(编辑器工具调用卡片、CLI 日志行),提供方无关,使工具无需依赖任何客户端协议即可描述自身。`presentCall`/`presentResult` 返回一个 **`card` 标签的渲染意图**——一个可辨识联合类型,UI 桥接层据此分发:
|
||||
工具希望其调用在 UI 中如何呈现(编辑器工具调用卡片、CLI 日志行),提供方无关,使工具在不依赖任何客户端协议的情况下描述自身。`presentCall`/`presentResult` 返回一个 **`card` 标签的渲染意图**——一个可辨识联合类型,UI 桥接层据此分发:
|
||||
|
||||
- `ToolCallView`(pending 状态):`{ card: 'generic', title, kind?, rawInput?, content?, locations? }`(默认卡片;`locations` 是 `{ path, line? }[]`,表示该调用读取/修改的文件,供编辑器跟随定位)、`{ card: 'terminal', title, description?, cwd? }`(shell 命令 → 终端卡片)、或 `{ card: 'diff', title, diffs, locations? }`(文件创建/修改 → 内联 diff 卡片;`diffs` 是 `{ path, oldText, newText }[]`,`oldText: null` 表示新文件)。
|
||||
- `ToolResultView`(completed 状态):`{ card: 'generic', title?, content? }`、`{ card: 'terminal', title?, output?, exitCode?, signal? }`(捕获的运行输出 + 退出状态;有能力的 UI 显示退出状态标签,无能力的 UI 获得桥接层从 `output` 派生的围栏 ` ```console ` 回退)、或 `{ card: 'diff', title?, diffs }`(已完成的文件变更 → 要展示的变更,通常是从 before/after 内容计算出带上下文行的已应用 hunk,或在没有 before-image 时的整文件 diff——如文件创建。`tool_call_update` 的 content 会**替换**调用的 content,因此变更工具即使与调用时的片段重复也要返回此值,以防结果文本覆盖 diff)。
|
||||
- `ToolCallView`(待执行):`{ card: 'generic', title, kind?, rawInput?, content?, locations? }`(默认卡片;`locations` 是 `{ path, line? }[]`,表示调用读取/修改的文件,供编辑器跟随)、`{ card: 'terminal', title, description?, cwd? }`(shell 命令→终端卡片)、或 `{ card: 'diff', title, diffs, locations? }`(文件创建/修改→行内 diff 卡片;`diffs` 是 `{ path, oldText, newText }[]`,新文件时 `oldText: null`)。
|
||||
- `ToolResultView`(已完成):`{ card: 'generic', title?, content? }`、`{ card: 'terminal', title?, output?, exitCode?, signal? }`(捕获的运行输出 + 退出状态;有能力的 UI 显示退出状态标签,无能力的 UI 获得桥接层从 `output` 派生的围栏 ` ```console ` 回退)、或 `{ card: 'diff', title?, diffs }`(已完成的文件变更→要展示的变更,通常是从变更前后内容计算出带上下文行的已应用 hunk,或在没有前像时的整文件 diff——例如文件创建。`tool_call_update` 的内容会**替换**调用的内容,因此变更工具即使与调用时的片段重复也要返回此卡片,以防结果文本覆盖 diff)。
|
||||
|
||||
`ToolCallKind`(`'read' | 'edit' | 'delete' | 'move' | 'search' | 'execute' | 'fetch' | 'other'`)为 generic 卡片选择图标。`FileLocation`(`{ path, line? }`)和 `FileDiff`(`{ path, oldText, newText }`)是共享的文件卡片词汇。该设计固定于[渲染意图联合类型 RFC](../rfc/implemented/architecture/2026-07-02-tool-render-intent-union.md);ACP(Agent Client Protocol)桥接层将 `diff` 卡片映射为 `{ type: 'diff' }` 内容块,将 `terminal` 卡片映射为 `_meta` 终端约定,并将文件卡片的标题相对于会话 cwd 做相对化处理。
|
||||
`ToolCallKind`(`'read' | 'edit' | 'delete' | 'move' | 'search' | 'execute' | 'fetch' | 'other'`)为 generic 卡片选择图标。`FileLocation`(`{ path, line? }`)和 `FileDiff`(`{ path, oldText, newText }`)是共享的文件卡片词汇。该设计固定在[渲染意图联合类型 RFC](../rfc/implemented/architecture/2026-07-02-tool-render-intent-union.md) 中;ACP 桥接层将 `diff` 卡片映射为 `{ type: 'diff' }` 内容块,将 `terminal` 卡片映射为 `_meta` 终端约定,并将文件卡片的标题相对于会话 cwd 做相对化处理。
|
||||
|
||||
完整的展示字段文档见 [`packages/core/tools/src/presentation.ts`](../../packages/core/tools/src/presentation.ts)。bash 工具自身的 schema(`bash`/`bash_output`/`bash_kill`)及其驱动的执行器见 [bash.md](bash.md)。
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
user-interaction.md: 47e0e26cd0a5201185dd252a882496456a9c3edd
|
||||
user-interaction.zh.md: bddcc991fd48d475f06912b9134b331d623b80d2
|
||||
user-interaction.zh.md: e7d2f27b8729a7694008d1ae7e21bdccae6058dd
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
[English](user-interaction.md) | 中文
|
||||
|
||||
[dsh-user-interaction](../../packages/ui/user-interaction) 的用户交互 seam。它是工具或权限插件在需要人类回答后 agent 才能继续时所使用的提供方无关词汇。UI 表面提供活跃的 `UserInteractionProvider`:`dsh-stdio-demo` 在 readline 中渲染问题,`dsh-acp` 将其映射为 ACP 表单引出。
|
||||
[dsh-user-interaction](../../packages/ui/user-interaction) 的用户交互 seam。它是提供方无关的词汇,工具或权限插件在需要人类回答后 agent(智能体)才能继续时使用这套词汇。UI 表面提供活跃的 `UserInteractionProvider`:`dsh-stdio-demo` 在 readline 中渲染问题,`dsh-acp` 将其映射为 ACP(Agent Client Protocol)表单征询。
|
||||
|
||||
源码:[`packages/ui/user-interaction/src/index.ts`](../../packages/ui/user-interaction/src/index.ts)
|
||||
|
||||
## 问题选项
|
||||
|
||||
`AskUserQuestionOption` 是可选择项的形状。`label` 是面向用户的选项文字,同时也是模型侧选中后的值;`description` 是可选的 UI 辅助文字。
|
||||
`AskUserQuestionOption` 是可选择项的形状。`label` 是面向用户的选项文字,同时也是面向模型的选中值;`description` 是可选的 UI 帮助文本。
|
||||
|
||||
```ts type-equiv
|
||||
interface AskUserQuestionOption {
|
||||
@@ -40,7 +40,7 @@ interface AskUserQuestionItem {
|
||||
|
||||
## 提问请求
|
||||
|
||||
`AskUserQuestionRequest` 是跨包请求。`questions` 是数组,这样 UI 可以在一次流程中展示相关问题,同时为每个回答保留稳定的 id。
|
||||
`AskUserQuestionRequest` 是跨包(package)的请求。`questions` 是数组,这样 UI 可以在一个流程中呈现相关提示,同时保持每个回答有稳定的 id。
|
||||
|
||||
```ts type-equiv
|
||||
interface AskUserQuestionRequest {
|
||||
@@ -55,7 +55,7 @@ interface AskUserQuestionRequest {
|
||||
|
||||
## 回答
|
||||
|
||||
提供方为每个已回答的问题 id 返回一条回答。`selected` 包含选中的选项 label,`custom` 在用户输入了自由文本"其他"答案时携带该内容。当 `custom` 存在时,`selected` 为空;自定义文本是对选中项的覆盖,而非补充。
|
||||
提供方为每个已回答的问题 id 返回一条回答。`selected` 包含选中的选项标签,`custom` 在用户输入自由文本时携带「其他」回答。当 `custom` 存在时,`selected` 为空;自定义文本是对选中项的覆盖,而非补充。
|
||||
|
||||
```ts type-equiv
|
||||
interface AskUserQuestionAnswerItem {
|
||||
@@ -77,7 +77,7 @@ interface AskUserQuestionAnswer {
|
||||
|
||||
## 提供方
|
||||
|
||||
同一上下文中只能有一个活跃的提供方。提供方注册与 effect 绑定,因此 HMR(热模块替换)或 dispose(资源释放)会移除活跃的 UI。
|
||||
同一上下文中只能有一个活跃的提供方。提供方注册绑定到 effect,因此 HMR(热模块替换)或 dispose(资源释放)会移除当前活跃的 UI。
|
||||
|
||||
```ts type-equiv
|
||||
interface UserInteractionProvider {
|
||||
@@ -87,7 +87,7 @@ interface UserInteractionProvider {
|
||||
|
||||
## 错误
|
||||
|
||||
`UserInteractionError` 继承 `HarnessError`,因此 `ctx.tools.execute()` 会为面向模型的工具失败保留 `{ name, code }`,例如 `EMPTY_QUESTIONS`、`NO_PROVIDER`、`ASK_ABORTED` 或 ACP 侧的取消。
|
||||
`UserInteractionError` 继承 `HarnessError`,因此 `ctx.tools.execute()` 会保留 `{ name, code }`,用于面向模型的工具失败,如 `EMPTY_QUESTIONS`、`NO_PROVIDER`、`ASK_ABORTED` 或 ACP 侧取消。
|
||||
|
||||
```ts type-equiv
|
||||
class UserInteractionError extends HarnessError {
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
web.md: 74db18835df02ef233f78ad7fbfec5d9b26d58e6
|
||||
web.zh.md: da8dad9dc06309b6f3108148dc39bc2b66bb5d22
|
||||
web.zh.md: 7b94aa457985075c79052aeef66b2e46eac5b49d
|
||||
@@ -2,17 +2,17 @@
|
||||
|
||||
[English](web.md) | 中文
|
||||
|
||||
Web 访问 seam 是一个[能力 seam](../rfc/implemented/architecture/2026-06-24-web-capability-seam.md),在单一 `ctx.web` 服务上横跨**两种能力**(搜索与抓取),拆分到多个包(package)中:接口([dsh-web](../../packages/web/web),`ctx.web` + 提供方注册表)、实现([dsh-web-search-exa](../../packages/web/web-search-exa)、[dsh-web-search-perplexity](../../packages/web/web-search-perplexity)、[dsh-web-search-deepseek](../../packages/web/web-search-deepseek)、[dsh-web-fetch-local](../../packages/web/web-fetch-local)),以及消费方([dsh-tool-web](../../packages/web/tool-web),`web_search`/`web_fetch` 工具 schema)。Web 是**一项可选能力**,不属于 agent loop 主干,因此其词汇定义在此,而非 [core.md](core.md)。更换搜索提供方不会改变模型发起查询的方式,更换抓取实现也不会改变模型请求 URL 的方式。
|
||||
Web 访问 seam 是一个[能力 seam](../rfc/implemented/architecture/2026-06-24-web-capability-seam.md),在一个 `ctx.web` 服务上横跨**两项能力**(搜索与抓取),分布在多个包(package)中:接口([dsh-web](../../packages/web/web),`ctx.web` + 提供方注册表)、实现([dsh-web-search-exa](../../packages/web/web-search-exa)、[dsh-web-search-perplexity](../../packages/web/web-search-perplexity)、[dsh-web-search-deepseek](../../packages/web/web-search-deepseek)、[dsh-web-fetch-local](../../packages/web/web-fetch-local)),以及消费方([dsh-tool-web](../../packages/web/tool-web),`web_search`/`web_fetch` 工具 schema)。Web 是**一项可选能力**,不属于 agent loop(智能体循环)主干,因此其词汇定义在此,而非 [core.md](core.md)。更换搜索提供方不会改变模型发起查询的方式,更换抓取实现也不会改变模型请求 URL 的方式。
|
||||
|
||||
源码:[`packages/web/web/src/types.ts`](../../packages/web/web/src/types.ts)
|
||||
|
||||
## 为何两种能力共用一个 seam
|
||||
## 为什么两项能力合为一个 seam
|
||||
|
||||
搜索与抓取既不共享请求 schema,也不共享业务逻辑,但它们被有意设计为同一个 `ctx.web` 中间层:一个提供方选择策略的归属者、一套 abort/error 词汇、一个面向产品的「此 harness 如何访问 Web」配置界面。代价是服务上出现了并行的 `searchX`/`fetchX` 方法对;这种并行是有意为之,而非遗漏的提取。提供方注册的是**能力**(`WebSearchProvider` 或 `WebFetchProvider`),而非工具;面向模型的名称、schema、prompt 引导与展示全部集中在唯一的消费方 `dsh-tool-web` 中。
|
||||
搜索与抓取既不共享请求 schema,也不共享业务逻辑,但它们被有意设计为同一个 `ctx.web` 中间层:一个提供方选择策略的所有者、一套 abort/error 词汇、一个面向产品的「此 harness 如何访问 Web」配置界面。代价是服务上并行的 `searchX`/`fetchX` 方法对;这种并行是有意为之,而非遗漏的提取。提供方注册的是**能力**(`WebSearchProvider` 或 `WebFetchProvider`),而非工具;面向模型的名称、schema、prompt 引导与展示全部集中在唯一的消费方 `dsh-tool-web` 中。
|
||||
|
||||
## 搜索请求与结果
|
||||
|
||||
面向模型的工具参数仅为一个 `query`;`maxResults` 是消费方持有的上限(`dsh-tool-web` 的 `searchMaxResults` 配置,默认 `8`),通过 seam 传递并在返回时强制执行:如果提供方返回的结果超量,seam 会截断 `sources[]` 并设置 `truncated`。
|
||||
面向模型的工具参数仅为一个 `query`;`maxResults` 是消费方自有的上限(`dsh-tool-web` 的 `searchMaxResults` 配置,默认 `8`),通过 seam 传递并在返回时强制执行——如果提供方返回超量,seam 截断 `sources[]` 并设置 `truncated`。
|
||||
|
||||
```ts type-equiv
|
||||
interface WebSearchRequest {
|
||||
@@ -33,7 +33,7 @@ interface WebSearchResult {
|
||||
}
|
||||
```
|
||||
|
||||
`content` 是提供方可选生成的回答文本(Exa 和 DeepSeek 不返回;Perplexity 返回生成式回答)。`sources[]` 是可移植的引用界面。每条 source 必有 `url`;`title`/`snippet`/`publishedAt` 可选,因为并非所有提供方都返回它们:Perplexity 的引用可能只有 URL,强迫适配器编造其余字段会让 seam 说谎。`dsh-tool-web` 渲染时使用 `title ?? hostname(url)`。
|
||||
`content` 是提供方可选生成的回答文本(Exa 和 DeepSeek 不返回;Perplexity 返回生成式回答)。`sources[]` 是可移植的引用表面。一个 source 必有 `url`;`title`/`snippet`/`publishedAt` 可选,因为并非每个提供方都返回它们——Perplexity 的引用可能只有 URL,强迫适配器编造其余字段会让 seam 说谎。`dsh-tool-web` 渲染时使用 `title ?? hostname(url)`。
|
||||
|
||||
```ts type-equiv
|
||||
interface WebSearchSource {
|
||||
@@ -52,7 +52,7 @@ interface WebFetchRequest {
|
||||
}
|
||||
```
|
||||
|
||||
HTTP 状态码是被抓取资源状态的一部分,不自动视为失败:成功的网络抓取返回 `404`/`500` 时,结果仍是一个带状态码和有界解码 body 的 `WebFetchResult`。`url` 是经过允许的重定向后的最终 URL。`WebError` 保留给无法安全获取或表示资源的失败情形。
|
||||
HTTP 状态码是被抓取资源状态的一部分,不自动视为失败:成功的网络抓取返回 `404`/`500` 时,仍产出一个带状态码和有界解码 body 的 `WebFetchResult`。`url` 是经过允许的重定向后的最终 URL。`WebError` 仅用于无法安全获取或表示资源的情况。
|
||||
|
||||
```ts type-equiv
|
||||
interface WebFetchResult {
|
||||
@@ -63,7 +63,7 @@ interface WebFetchResult {
|
||||
}
|
||||
```
|
||||
|
||||
`WebFetchBody` 是 `dsh-web` 持有的**封闭**可辨识联合类型(不是可合并扩展的 map):提供方解码 kind,`dsh-tool-web` 渲染它,因此新增一个 kind 是跨已知包的协调变更,而非插件扩展。消费方对 `kind` 做 `switch` 并以 `default: assertNever(...)` 结尾,因此新增 kind 会在每个消费方处破坏编译直到被处理。即使当前各分支字段相同,每个分支仍保持独立的对象字面量,为将来的分支特有字段留出空间(例如未来 `pdf` body 的 `pageCount`)。
|
||||
`WebFetchBody` 是 `dsh-web` 拥有的**封闭**可辨识联合类型(不是可合并扩展的 map):提供方解码 kind,`dsh-tool-web` 渲染它,因此新增一个 kind 是已知包之间的协调变更,而非插件扩展。消费方对 `kind` 做 `switch` 并以 `default: assertNever(...)` 结尾,所以新增 kind 会在每个消费方处编译失败,直到被处理。即使各分支当前字段一致,每个分支仍保持独立的对象字面量,为将来分支特有字段留出空间(例如未来 `pdf` body 的 `pageCount`)。
|
||||
|
||||
```ts type-equiv
|
||||
type WebFetchBody =
|
||||
@@ -73,14 +73,14 @@ type WebFetchBody =
|
||||
|
||||
## 提供方可用性
|
||||
|
||||
提供方的 `available(): boolean` 是一个廉价的**本地**检查(凭证是否存在、配置是否可解析),**禁止发起网络调用**。它是执行时选择的输入,而非健康检查系统:`search()`/`fetch()` 读取它来选出可用的提供方,选择失败以结构化的 `WebError` 呈现给调用方路由,其 code 和 message 携带可分支的细节(缺失的 id 或歧义的候选集)。
|
||||
提供方的 `available(): boolean` 是一个廉价的**本地**检查(凭证是否存在、配置是否可解析),**禁止发起网络调用**。它是执行时选择的输入,而非健康检查系统:`search()`/`fetch()` 读取它以选出可用的提供方,选择失败以结构化的 `WebError` 呈现给调用方路由——其 code 和 message 携带可分支的细节(缺失的 id 或有歧义的候选集)。
|
||||
|
||||
选择从不依赖注册顺序、配置顺序或 HMR 顺序:一项能力要么有显式的提供方 id(配置 `searchProvider`/`fetchProvider`,或喂入同一字段的对应环境变量),要么在恰好只有一个可用提供方注册时自动选择;多个可用提供方且未配置 id 时为 `WEB_PROVIDER_AMBIGUOUS`,而非先注册先赢。
|
||||
选择从不依赖注册顺序、配置顺序或 HMR(热模块替换)顺序:一项能力要么有显式的提供方 id(配置 `searchProvider`/`fetchProvider`,或填充同一字段的对应环境变量),要么在恰好只有一个可用提供方注册时自动选择;多个可用提供方且未配置 id 时为 `WEB_PROVIDER_AMBIGUOUS`,而非先注册先赢。
|
||||
|
||||
## 错误
|
||||
|
||||
`WebError extends HarnessError`([core.md](core.md) 错误分类体系),带有 `code: string`(开放式,与其他 seam 的错误一致:`LlmError`、`SubagentError`),而非封闭联合类型:提供方可以在不修改 `dsh-web` 的情况下抛出自己的 code,消费方必须容忍未知 code。code 按归属者划分。seam 中性的 code 由 `WebService` 选择逻辑和共享契约抛出:`WEB_PROVIDER_UNAVAILABLE`、`WEB_PROVIDER_CONFIGURED_MISSING`、`WEB_PROVIDER_CONFIGURED_UNAVAILABLE`、`WEB_PROVIDER_AMBIGUOUS`、`WEB_DUPLICATE_PROVIDER`(注册时的编程错误,类似 `LlmService` 的 `DUPLICATE_ADAPTER`)、`WEB_ABORTED`,以及 `WEB_PROVIDER_ERROR`(提供方自身失败通过 seam 暴露的兜底 code,包括网络/传输失败:DNS、连接被拒、TLS)。抓取传输层 code 由 `dsh-web-fetch-local` 实现持有,不同的抓取后端不必抛出它们:`WEB_INVALID_URL`、`WEB_BLOCKED_URL`、`WEB_REDIRECT_BLOCKED`、`WEB_FETCH_TOO_LARGE`、`WEB_FETCH_TIMEOUT`、`WEB_UNSUPPORTED_CONTENT_TYPE`。
|
||||
`WebError extends HarnessError`([core.md](core.md) 错误分类体系),带有 `code: string`(开放式,与其他 seam 的错误一致——`LlmError`、`SubagentError`),而非封闭联合类型:提供方可以在不修改 `dsh-web` 的情况下抛出自己的 code,消费方必须容忍未知 code。code 按所有者划分。seam 中立的 code 由 `WebService` 选择逻辑和共享契约抛出:`WEB_PROVIDER_UNAVAILABLE`、`WEB_PROVIDER_CONFIGURED_MISSING`、`WEB_PROVIDER_CONFIGURED_UNAVAILABLE`、`WEB_PROVIDER_AMBIGUOUS`、`WEB_DUPLICATE_PROVIDER`(注册时的编程错误,类似 `LlmService` 的 `DUPLICATE_ADAPTER`)、`WEB_ABORTED`,以及 `WEB_PROVIDER_ERROR`(提供方自身故障通过 seam 暴露的兜底 code,包括网络/传输失败——DNS、连接被拒、TLS)。抓取传输层 code 由 `dsh-web-fetch-local` 实现拥有,不同的抓取后端无需抛出它们:`WEB_INVALID_URL`、`WEB_BLOCKED_URL`、`WEB_REDIRECT_BLOCKED`、`WEB_FETCH_TOO_LARGE`、`WEB_FETCH_TIMEOUT`、`WEB_UNSUPPORTED_CONTENT_TYPE`。
|
||||
|
||||
## 服务
|
||||
|
||||
`WebService` 注册搜索与抓取提供方,以 `WEB_DUPLICATE_PROVIDER` 拒绝重复 id,并在执行时以结构化的选择错误解析提供方。本地抓取后端仅接受 HTTP(S)、拒绝凭证、限制重定向次数、字节数、字符数与时间、对每一跳同源重定向重新校验,并解码 body;展示由工具负责。私有网络阻断尚未实现,因此不要在能触及敏感内部目标的环境中启用 `web_fetch`。
|
||||
`WebService` 注册搜索与抓取提供方,以 `WEB_DUPLICATE_PROVIDER` 拒绝重复 id,并在执行时以结构化的选择错误解析提供方。本地抓取后端仅接受 HTTP(S)、拒绝凭证、限制重定向次数、字节数、字符数和时间、对每一跳同源重定向重新校验,并解码 body;展示由工具负责。私有网络阻断尚未实现,因此请勿在可触及敏感内部目标的环境中启用 `web_fetch`。
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
workflow.md: 1571723c172fe851e89550e4ed8588ddb14088a0
|
||||
workflow.zh.md: 9fa3b4eea4efdcdb8e594c3558e30846aea0af46
|
||||
workflow.zh.md: 68a5ff9b6ddf43145966e60706a42a758ea260ba
|
||||
@@ -2,15 +2,15 @@
|
||||
|
||||
[English](workflow.md) | 中文
|
||||
|
||||
工作流 seam:由 agent(智能体)运行一段模型编写的编排脚本(SCRIPT),向外扇出 subagent。与 [subagent](subagent.md) 一样,它是**一项可选能力**,不属于 agent loop(智能体循环)主干,因此其词汇定义在此而非 [core.md](core.md) 中。与 subagent 注册表不同,它采用 bash 形态:每个上下文只有一个引擎实现提供 `ctx.workflows`;没有命名提供方注册表(第二个引擎是插件替换,而非共存)。
|
||||
工作流 seam:一个 agent(智能体)运行由模型编写的编排脚本(SCRIPT),扇出 subagent。与 [subagent](subagent.md) 一样,它是**一项可选能力**,不属于 agent loop(智能体循环)主干,因此其词汇定义在此处而非 [core.md](core.md)。与 subagent 注册表不同,它采用 bash 形态:每个上下文只有一个引擎实现提供 `ctx.workflows`;没有命名提供方注册表(第二个引擎是插件替换,而非共存)。
|
||||
|
||||
接口:[dsh-workflow](../../packages/workflow/workflow)(`ctx.workflows` + 下文词汇)。实现为 [dsh-workflow-workerthread](../../packages/workflow/workflow-workerthread)(基于 `node:worker_threads` 的引擎:每次运行一个 worker,脚本的 vm 上下文在其中执行);面向模型的消费方是 [dsh-tool-workflow](../../packages/workflow/tool-workflow)。提案与设计理由见[动态工作流 RFC](../rfc/implemented/feature/2026-07-05-dynamic-workflows.md)。
|
||||
接口:[dsh-workflow](../../packages/workflow/workflow)(`ctx.workflows` + 下文词汇)。实现是 [dsh-workflow-workerthread](../../packages/workflow/workflow-workerthread)(一个 `node:worker_threads` 引擎:每次运行一个 worker,脚本的 vm 上下文在其中执行);面向模型的消费方是 [dsh-tool-workflow](../../packages/workflow/tool-workflow)。提案与设计动机见[动态工作流 RFC](../rfc/implemented/feature/2026-07-05-dynamic-workflows.md)。
|
||||
|
||||
源码:[`packages/workflow/workflow/src/types.ts`](../../packages/workflow/workflow/src/types.ts)
|
||||
|
||||
## 启动请求
|
||||
|
||||
调用方启动一次运行时发出的请求。工具层根据模型的 `{ script, meta, args }` 调用加上发起调用的 agent 构建此请求;`meta` 和 `args` 是纯 JSON 数据(引擎在任何代码运行之前对 `meta` 做形状校验,不通过则大声拒绝——永远不会为了获取 meta 而执行脚本文本)。`parent` 是必需的:脚本 spawn 的每个子 agent 都归属于它(cwd、血统和深度通过 [subagent seam](subagent.md) 流转)。
|
||||
调用方启动一次运行时提交的内容。工具层从模型的 `{ script, meta, args }` 调用加上发起调用的 agent 构建此请求;`meta` 和 `args` 是纯 JSON 数据(引擎在任何代码执行之前对 `meta` 做形状校验,不通过则立即报错:永远不会为了获取 meta 而执行脚本文本)。`parent` 是必填项:脚本 spawn 的每个子 agent 都归属于它(cwd、血统与深度通过 [subagent seam](subagent.md) 传递)。
|
||||
|
||||
```ts type-equiv
|
||||
interface WorkflowStartRequest {
|
||||
@@ -24,7 +24,7 @@ interface WorkflowStartRequest {
|
||||
|
||||
## 工作流的身份标识:`WorkflowMeta`
|
||||
|
||||
作为数据附在启动请求上的身份块(工具的 `meta` 参数;字段词汇与 Claude Code 动态工作流的 meta 块一致)。`phases` 仅为进度词汇:`phase()` 调用与标题匹配供观察者使用;不暗示任何执行结构。
|
||||
作为数据附在启动请求上的身份块(工具的 `meta` 参数;字段词汇与 Claude Code 动态工作流的 meta 块一致)。`phases` 仅用于进度展示:`phase()` 调用与标题匹配,供观察者使用;不暗示任何执行结构。
|
||||
|
||||
```ts type-equiv
|
||||
interface WorkflowMeta {
|
||||
@@ -37,7 +37,7 @@ interface WorkflowMeta {
|
||||
|
||||
## 终态结果:`WorkflowResult`
|
||||
|
||||
一次运行的结果,由 `WorkflowRun.result` resolve。`value` 是脚本的物化返回值——纯宿主域 JSON 数据(脚本无返回值时为 `null`)——仅在 `completed` 时有意义。`stopReason` 是一个封闭联合类型(引擎拥有;消费方可穷举):`completed` | `cancelled` | `error`。非 `completed` 的原因在 `error` 中携带失败信息,消费方将其映射为 `isError` 工具结果,而非把部分输出当作成功上报。
|
||||
一次运行的结果,由 `WorkflowRun.result` resolve。`value` 是脚本的物化返回值——纯宿主域 JSON 数据(脚本无返回值时为 `null`)——仅在 `completed` 时有意义。`stopReason` 是封闭联合类型(引擎所有;消费方可穷举):`completed` | `cancelled` | `error`。非 `completed` 的原因在 `error` 中携带失败信息,消费方将其映射为 `isError` 工具结果,而非把部分输出当作成功上报。
|
||||
|
||||
```ts type-equiv
|
||||
interface WorkflowResult {
|
||||
@@ -50,7 +50,7 @@ interface WorkflowResult {
|
||||
|
||||
## 活跃运行:`WorkflowRun`
|
||||
|
||||
脚本执行期间消费方持有的句柄。消费方 await `result`,可在运行中途 `cancel`,且必须在每条路径上调用 `dispose`。`result` 不会 reject:脚本失败以 `stopReason: 'error'` resolve;一旦运行被取消,即使脚本本身永不 settle,它也会在引擎的有界宽限期内 settle(引擎强制以 `cancelled` settle;worker-thread 引擎随后终止脚本的 worker),因此消费方 await `result` 不会在取消后永远卡住。`dispose()` = cancel + 有界 settle + 子 agent 静默;它不会因脚本卡死而挂起。
|
||||
脚本执行期间消费方持有的句柄。消费方 await `result`,可中途 `cancel`,且*必须*在每条路径上 `dispose`。`result` 不会 reject:脚本失败以 `stopReason: 'error'` resolve;一旦运行被取消,即使脚本本身永不 settle,它也会在引擎的有界宽限期内 settle(引擎强制以 `cancelled` settle;worker-thread 引擎随后终止脚本的 worker),因此消费方 await `result` 不会在取消后卡死。`dispose()` = cancel + 有界 settle + 子 agent 静默;它不会因脚本卡死而挂起。
|
||||
|
||||
```ts type-equiv
|
||||
interface WorkflowRun {
|
||||
@@ -64,8 +64,8 @@ interface WorkflowRun {
|
||||
|
||||
## 失败纪律:`WorkflowError.fatal`
|
||||
|
||||
脚本内部的钩子误用——错误参数、未知或延迟的 `agent()` 选项、超出[结构化输出子集](../../packages/core/tools/README.md)的 schema、触发的上限、seam 启动失败、取消——会抛出 `fatal: true` 的 `WorkflowError`。`parallel()`/`pipeline()` 组合器对 fatal 错误执行重新抛出,而非将该项映射为 `null`:一个拼写错误的选项必须大声杀死脚本,绝不能消融为看似普通子 agent 失败的东西。逐项的 `null` 保留给子运行失败(非 `completed` 的 stop reason)和阶段内的普通脚本错误。
|
||||
脚本内部的钩子误用:错误参数、未知或延迟的 `agent()` 选项、超出[结构化输出子集](../../packages/core/tools/README.md)的 schema、触发的上限、seam 启动失败、取消,都会抛出 `fatal: true` 的 `WorkflowError`。`parallel()`/`pipeline()` 组合器对 fatal 错误直接重新抛出,而非将该项映射为 `null`:一个拼写错误的选项必须让脚本大声失败,绝不能消融为看似普通子 agent 失败的结果。逐项的 `null` 保留给子运行失败(非 `completed` 的 stop reason)和阶段内的普通脚本错误。
|
||||
|
||||
## 事件
|
||||
|
||||
`workflow/*` 事件(`workflow/start`、`workflow/phase`、`workflow/log`、`workflow/agent-start`、`workflow/agent-end`、`workflow/end`——见[事件目录](../cordis-catalog/events.md))是**仅供观察**的 emit,携带数据快照:每个 payload 以 `WorkflowRunInfo`(id + meta)开头,从不暴露活跃的 `WorkflowRun`,因此订阅者无法获得 `cancel`/`dispose`;`workflow/end` 刻意省略 result value(观察结果的监听器不得收到调用方 result 的可变别名)。每次 emit 对每个监听器隔离:抛异常的订阅者被记录但不传播,不会饿死其后注册的监听器;每个监听器收到自己的 payload 克隆,因此修改它既不会损坏引擎也不会影响其他监听器。这种隔离与 `subagent/start`/`subagent/end` 一致。
|
||||
`workflow/*` 事件(`workflow/start`、`workflow/phase`、`workflow/log`、`workflow/agent-start`、`workflow/agent-end`、`workflow/end`,见[事件目录](../cordis-catalog/events.md))是**仅供观察**的 emit,携带数据快照:每个 payload 以 `WorkflowRunInfo`(id + meta)开头,而非活跃的 `WorkflowRun`,因此订阅者无法获得 `cancel`/`dispose`;`workflow/end` 刻意省略 result value(观察结果的监听器不得收到调用方 result 的可变别名)。每次 emit 对每个监听器隔离:抛出异常的订阅者被记录日志但不传播,不会饿死在它之后注册的监听器;每个监听器收到自己的 payload 克隆,因此修改它既不会损坏引擎也不会影响其他监听器。这种隔离方式与 `subagent/start`/`subagent/end` 一致。
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
0001-acp-default-export-drops-inject.md: 6a71d8d7ef72e3110a99774b180f3de7115ef622
|
||||
0001-acp-default-export-drops-inject.zh.md: 12bb3501a56c3cfef8f7a1b0d773be62db8e09ca
|
||||
0001-acp-default-export-drops-inject.zh.md: 1d97f1140a9595ac7e304b5a1600c3cbc47292f1
|
||||
@@ -6,23 +6,23 @@ Status: resolved (fix in PR #41 `feat/acp-2-bridge`)
|
||||
|
||||
## 摘要
|
||||
|
||||
两个集成错误在单元测试全绿的情况下击溃了 ACP:一个 default export 导致 Loader 丢弃 `inject`,一个经过 traceable 代理的可选服务查找在 shadow 边界上失败。手动挂载的测试绕过了这两条路径。修复后新增了无需 API key 的真实 Loader 覆盖,以及关于插件导出和可选服务访问的包(package)规则。
|
||||
两个集成错误在单元测试全覆盖的情况下仍然导致 ACP(Agent Client Protocol)崩溃:一个 default export 使 Loader 丢弃了 `inject`,一个经 traceable 代理的可选服务查找在 shadow 边界上失败。手动挂载的测试绕过了这两条路径。修复方案增加了无需 API key 的真实 Loader 覆盖率,并为插件导出和可选服务访问制定了包(package)级规则。
|
||||
|
||||
## 概述
|
||||
|
||||
ACP 服务器(`examples/acp-agent`、`@deepseek-ai/dsh-acp`)在真实编辑器(Zed)连接的瞬间崩溃:第一个 `session/new` 请求返回 `Internal error: cannot get property "agents" without inject`,`session/load` 对 `sessionPersistence` 返回相同错误。尽管有 178 个绿色单元测试和 100% 行覆盖率,bridge 在生产环境中完全无法工作。两个独立的 bug 隐藏在同一个错误字符串背后,测试套件因同一个原因漏掉了二者:每个测试都通过一条不会触及插件实际加载方式或服务实际解析方式的路径来挂载插件。
|
||||
ACP 服务器(`examples/acp-agent`、`@deepseek-ai/dsh-acp`)在真实编辑器(Zed)连接的瞬间崩溃:第一个 `session/new` 请求返回 `Internal error: cannot get property "agents" without inject`,`session/load` 对 `sessionPersistence` 返回同样的错误。尽管有 178 个绿色单元测试和 100% 行覆盖率,bridge 在生产环境中完全无法工作。两个独立的 bug 隐藏在同一个错误字符串背后,测试套件之所以两个都没捕获,原因也相同:所有测试都通过一条不会触及插件真实加载方式和服务真实解析方式的路径来挂载插件。
|
||||
|
||||
## 影响
|
||||
|
||||
ACP 服务器无法创建或加载任何一个会话——这正是编辑器最先调用的两个 RPC。任何将 agent 接入 Zed 的人都会立即遇到硬性失败。无数据丢失(崩溃前没有持久化任何内容);代价完全是「功能不可用」加上两次定位原因的调试时间。
|
||||
ACP 服务器无法创建或加载任何一个会话——而这正是编辑器最先调用的两个 RPC。任何将 agent(智能体)接入 Zed 的人都会立即遭遇硬性失败。无数据丢失(崩溃前没有任何内容被持久化);代价完全是「功能不可用」加上两次定位原因的调试时间。
|
||||
|
||||
## 时间线
|
||||
|
||||
- Bridge(RFC 010)带着完整的单元测试套件(编解码、内存传输、基于属性的协议形状测试、失败路径、HMR(热模块替换))、一个需要 key 的真实 API e2e 测试,以及一个无需 key 的 stdout 纯净性 e2e 测试一起落地。全部绿色,100% 覆盖率。
|
||||
- 一次真实的 Zed 会话立即在 `session/new` 上失败,报错 `cannot get property "agents" without inject`。
|
||||
- 调查最初追踪的是 Cordis「traceable/shadow」理论(合理,且机制确实存在——见 Bug #2),随后在 vendor 的 `reflect.ts` 中对实际 fiber 遍历做了插桩,并运行了真实子进程。trace 显示 throw 发生在 `apply()` 第 179 行、**插件加载时**,位于 ROOT fiber 且没有 shadow——推翻了 shadow 理论对 `session/new` 的解释。
|
||||
- 找到根因 #1:一行多余的 `export default apply`。移除后 `session/new` 修复。
|
||||
- 移除后暴露了 Bug #2:`session/load` 仍然在 `sessionPersistence` 上抛出——这是一个真正不同的机制(shadow 遍历),通过隔离修复并重新运行真实子进程得到确认。
|
||||
- bridge(RFC 010)落地时附带完整的单元测试套件(codec、内存传输、基于属性的协议形状测试、失败路径、HMR(热模块替换))、一个需要 key 的真实 API e2e 测试,以及一个无需 key 的 stdout 纯净性 e2e 测试。全部绿色,100% 覆盖率。
|
||||
- 真实 Zed 会话在 `session/new` 上立即失败,报错 `cannot get property "agents" without inject`。
|
||||
- 调查最初追踪了一个 Cordis「traceable/shadow」理论(看似合理,且该机制确实存在——见 Bug #2),随后在 vendor 的 `reflect.ts` 中对实际 fiber 遍历做了插桩,并运行了真实子进程。trace 显示 throw 发生在 `apply()` 第 179 行、**插件加载时**,位于 ROOT fiber 且没有 shadow——推翻了 shadow 理论对 `session/new` 的解释。
|
||||
- 找到根因 #1:一行多余的 `export default apply`。删除后 `session/new` 修复。
|
||||
- 删除后暴露了 Bug #2:`session/load` 仍然在 `sessionPersistence` 上抛错——这是一个真正不同的机制(shadow 遍历),通过隔离修复并重新运行真实子进程得到确认。
|
||||
|
||||
## 根因 #1——`export default apply` 丢弃了插件的 `inject`(导致 `session/new` 崩溃)
|
||||
|
||||
@@ -36,7 +36,7 @@ export function apply(ctx: Context, config: AcpConfig): void { /* … */ }
|
||||
export default apply // ← the bug
|
||||
```
|
||||
|
||||
当插件从 `cordis.yml` 加载时,Cordis Loader 通过 `Loader.unwrapExports`(`vendor/loader/src/index.ts`)对导入的模块做规范化处理:
|
||||
当插件从 `cordis.yml` 加载时,Cordis Loader 通过 `Loader.unwrapExports`(`vendor/loader/src/index.ts`)对导入的模块进行规范化:
|
||||
|
||||
```ts ignore-check
|
||||
unwrapExports(exports: any) {
|
||||
@@ -47,19 +47,19 @@ unwrapExports(exports: any) {
|
||||
}
|
||||
```
|
||||
|
||||
存在 default export 时,`exports.default ?? exports` 解析为**裸 `apply` 函数**。裸函数没有 `inject`、没有 `name`、没有 `Config` 属性——这些作为*兄弟*命名导出存在于模块命名空间上,而 unwrap 到 `.default` 把命名空间整个丢弃了。Loader 随后基于一个空的 `inject` 构建了插件的 fiber。
|
||||
存在 default export 时,`exports.default ?? exports` 解析为**裸 `apply` 函数**。裸函数没有 `inject`、没有 `name`、没有 `Config` 属性——这些作为*兄弟*命名导出存在于模块命名空间上,而 unwrap 到 `.default` 把整个命名空间丢弃了。Loader 随后基于空的 `inject` 构建了插件的 fiber。
|
||||
|
||||
因此 `apply` 在一个**没有注入任何服务**的 fiber 中运行。第一行 `const agents = ctx.agents` 遍历 fiber 树(ROOT → Include → Loader → ROOT),在所有 fiber 的 store 中都找不到 `agents`,到达根 fiber(`runtime === null`)后抛出 `cannot get property "agents" without inject`。崩溃发生在*加载时*,而非后续的请求处理器中——请求只是恰好触发了加载。
|
||||
|
||||
**修复:**删除 `export default apply`。Loader 随后使用模块命名空间,正确识别 `inject`/`name`/`Config`,`apply` 在一个真正授予了声明服务的 fiber 中运行。
|
||||
**修复:** 删除 `export default apply`。Loader 随后使用模块命名空间,正确识别 `inject`/`name`/`Config`,`apply` 在一个真正授予了声明服务的 fiber 中运行。
|
||||
|
||||
## 根因 #2——可选服务的属性读取在 traceable shadow 中触发 inject 守卫(导致 `session/load` 崩溃)
|
||||
## 根因 #2——可选服务读取通过 traceable shadow 触发 inject 守卫(导致 `session/load` 崩溃)
|
||||
|
||||
修复 #1 后,`session/new` 正常工作,但 `session/load` 仍然抛出 `cannot get property "sessionPersistence" without inject`。这次*确实*是 Cordis 的 traceable/shadow 机制,值得精确理解。
|
||||
修复 #1 后,`session/new` 正常工作,但 `session/load` 仍然抛出 `cannot get property "sessionPersistence" without inject`。这个问题*确实*是 Cordis 的 traceable/shadow 机制,值得精确理解。
|
||||
|
||||
`session/load` 调用 `agents.resume(...)`,后者委托给 `AgentLoop.resume()`,其中读取了 `this.ctx.sessionPersistence`。`AgentLoop` 的 `static inject` 故意**不**包含 `sessionPersistence`——注入它会导致非持久化的演示永远挂起,等待一个永远不会加载的后端。该服务由一个独立的兄弟插件/fiber 提供,按需读取。
|
||||
`session/load` 调用 `agents.resume(...)`,后者委托给 `AgentLoop.resume()`,其中读取了 `this.ctx.sessionPersistence`。`AgentLoop` 的 `static inject` 故意**不**包含 `sessionPersistence`——注入它会导致非持久化的演示永远挂起,等待一个永远不会加载的后端。该服务由一个独立的兄弟插件/fiber 提供,以机会性方式读取。
|
||||
|
||||
Cordis 中的服务访问通过上下文代理(`vendor/cordis/src/reflect.ts`)进行。当通过从外部 fiber 获取的 *traceable 代理*调用服务方法时(此处:bridge fiber 调用 `ctx.agents.resume`,注册表返回 `this.factory`——即 `AgentLoop`——被重新包装为绑定到调用方的新 traceable 代理),`createShadowMethod`(`vendor/cordis/src/utils.ts`)将 `this` 重新绑定到一个 *shadow* 对象,其 `ctx` 携带 `[symbols.shadow]` 指向 `AgentLoop` 自身的构造上下文。在 `resume` 内部,`this.ctx.sessionPersistence` 的解析从 shadow 的 fiber 开始遍历:
|
||||
Cordis 中的服务访问通过上下文代理(`vendor/cordis/src/reflect.ts`)进行。当通过从外部 fiber 获取的 *traceable 代理*调用服务方法时(此处:bridge fiber 调用 `ctx.agents.resume`,注册表返回 `this.factory`——即 `AgentLoop`——重新包装为绑定到调用方的新 traceable 代理),`createShadowMethod`(`vendor/cordis/src/utils.ts`)将 `this` 重新绑定到一个 *shadow* 对象,其 `ctx` 携带 `[symbols.shadow]` 指向 `AgentLoop` 自身的构造上下文。在 `resume` 内部,`this.ctx.sessionPersistence` 的解析从 shadow 的 fiber 开始遍历:
|
||||
|
||||
```ts ignore-check
|
||||
// reflect.ts get handler
|
||||
@@ -74,40 +74,40 @@ while (true) {
|
||||
}
|
||||
```
|
||||
|
||||
遍历**只走祖先方向**。`sessionPersistence` 既不在 `AgentLoop` 的 fiber store 中(不在其 `static inject` 里),也不在通往根的任何祖先上(它在一个*兄弟*分支上),因此遍历到达根 fiber 后抛出。
|
||||
遍历**仅向祖先方向**进行。`sessionPersistence` 既不在 `AgentLoop` 的 fiber store 中(不在其 `static inject` 中),也不在通往 root 的任何祖先上(它位于一个*兄弟*分支),因此遍历到达根 fiber 后抛错。
|
||||
|
||||
为什么内存中的 `AgentLoop` resume 测试没有捕获到这个问题?因为它们从测试代码中直接调用 `ctx.agents.resume(...)`——*不在任何插件 fiber 内*。此时 `ctx.fiber.runtime` 为 `null`,代理处理器走了一条提前退出的路径:
|
||||
为什么内存中的 `AgentLoop` resume 测试没有捕获这个问题?因为它们从测试代码直接调用 `ctx.agents.resume(...)`——*在任何插件 fiber 之外*。此时 `ctx.fiber.runtime` 为 `null`,代理处理器走了一条提前绕过的路径:
|
||||
|
||||
```ts ignore-check
|
||||
if (!ctx.fiber.runtime) return ctx.reflect.get(prop, false) // ← direct global-store lookup, no fiber walk
|
||||
```
|
||||
|
||||
`ctx.reflect.get(name, false)` 是基于 isolate symbol 的全局服务 store 直接查找——完全忽略 fiber 拓扑,能找到服务。因此从顶层测试读取正常;从真实插件 fiber 内部、经由 shadow 到达时则抛出。bridge 恰好是后者。
|
||||
`ctx.reflect.get(name, false)` 是基于 isolate symbol 的全局服务 store 直接查找——完全忽略 fiber 拓扑,能找到服务。因此从顶层测试读取可以成功;而从真实插件 fiber 内部、经由 shadow 到达时则抛错。bridge 恰好是后者。
|
||||
|
||||
**修复:**使用 `ctx.get('sessionPersistence')` 读取可选服务,该方法使用全局 isolate-keyed store,同时保留活跃状态检查。对于插件声明注入集中的服务,直接属性读取仍然适用。
|
||||
**修复:** 使用 `ctx.get('sessionPersistence')` 读取可选服务,该方法使用全局 isolate-keyed store 同时保留活跃状态检查。对于插件声明注入集中的服务,直接属性读取仍然适用。
|
||||
|
||||
## 为什么所有测试都漏掉了(真正的失败)
|
||||
## 为什么所有测试都没有捕获(真正的失败)
|
||||
|
||||
两个 bug 共享同一个流程缺口:**没有任何测试通过插件的真实加载路径或真实调用拓扑来运行它。**
|
||||
两个 bug 共享同一个流程缺口:**没有任何测试通过插件的真实加载路径或真实调用拓扑来驱动它。**
|
||||
|
||||
- 内存 harness 通过手动构建插件对象来挂载 bridge:`ctx.plugin({ name, inject, apply })`。这手动提供了 `inject`,因此永远无法复现 Bug #1——`unwrapExports` 只被 *Loader* 调用,`ctx.plugin` 从不调用它。即使 `ctx.plugin(NamespaceImport)` 也无法捕获此问题。
|
||||
- 同一个 harness 把所有东西平铺挂载在一个根上下文上,因此从中触达的 `AgentLoop` resume 要么在顶层运行(`!runtime` 旁路),要么通过一个 origin 仍在根上解析的 shadow——掩盖了 Bug #2 的祖先遍历失败。
|
||||
- 唯一的无 key e2e 发送 `initialize` 并检查 stdout 纯净性。`initialize` 从不触达 factory,因此安然通过两个 bug。
|
||||
- 唯一驱动 `session/new`/`session/load` 的测试需要 key 才能运行,CI(无 key)跳过了它——而本地它之所以「通过」,只是因为一个陈旧的已构建 `lib/`(包含旧代码)恰好满足了模块解析。
|
||||
- 内存 harness 通过手动构建插件对象来挂载 bridge:`ctx.plugin({ name, inject, apply })`。这手动提供了 `inject`,因此永远无法复现 Bug #1——`unwrapExports` 只被 *Loader* 调用,`ctx.plugin` 从不调用它。即使 `ctx.plugin(NamespaceImport)` 也无法捕获。
|
||||
- 同一个 harness 将所有内容平铺挂载在一个根上下文上,因此从中触达的 `AgentLoop` resume 要么运行在顶层(`!runtime` 绕过),要么通过一个 origin 仍然解析在 root 上的 shadow——掩盖了 Bug #2 的祖先遍历失败。
|
||||
- 唯一的无 key e2e 发送 `initialize` 并检查 stdout 纯净性。`initialize` 从不触达 factory,因此两个 bug 都安然通过。
|
||||
- 唯一驱动 `session/new`/`session/load` 的测试需要 key 才能运行,因此 CI(无 key)跳过了它——而本地它之所以「通过」,只是因为一个陈旧的已构建 `lib/`(包含旧代码)恰好满足了模块解析。
|
||||
|
||||
100% 行覆盖率自始至终满足。覆盖率证明代码行*被执行过*;它不能说明功能是否*以交付的方式*工作。
|
||||
100% 行覆盖率始终满足。覆盖率证明代码行*被执行过*;它不能说明功能是否*按交付方式正常工作*。
|
||||
|
||||
## 新增的防护措施
|
||||
|
||||
- **移除 `export default apply`**(`packages/ui/acp/src/index.ts`)——Bug #1 的修复。
|
||||
- **删除 `export default apply`**(`packages/ui/acp/src/index.ts`)——Bug #1 的修复。
|
||||
- **`AgentLoop.resume` 使用 `this.ctx.get('sessionPersistence')`**(`packages/core/agent-loop/src/index.ts`)——Bug #2 的修复,附注释说明 shadow 遍历陷阱。
|
||||
- **无需 key 的 `session/new` e2e,通过真实 stdio 运行**(`examples/acp-agent/tests/acp.e2e.ts`):以子进程方式通过真实 Loader 启动示例,并断言 `session/new` 正常返回。无需 API key 即可在 Bug #1 上大声失败。已验证恢复 `export default apply` 时测试失败。
|
||||
- **e2e spawn 中设置 `TSX_TSCONFIG_PATH`**:子进程从临时 cwd 运行,tsx 无法通过向上搜索找到仓库根的 tsconfig `paths` 映射——因此 dsh-* 的导入静默回退到已构建的 `lib/`。将 tsx 指向仓库 tsconfig 使解析不依赖 cwd,确保测试运行的是*源码*而非可能陈旧的构建产物。
|
||||
- **[docs/testing.md](../testing.md) 规则**:「测试真实入口路径」,行覆盖率不等于行为覆盖率——将此教训编纂为所有未来插件的规则。
|
||||
- **e2e spawn 中设置 `TSX_TSCONFIG_PATH`**:子进程从临时 cwd 运行,tsx 无法通过向上搜索找到仓库根的 tsconfig `paths` 映射——因此 dsh-* 的 import 静默回退到已构建的 `lib/`。将 tsx 指向仓库 tsconfig 使解析不依赖 cwd,确保测试运行的是*源码*而非可能陈旧的构建产物。
|
||||
- **[docs/testing.md](../testing.md) 规则**:「测试真实入口路径」,行覆盖率不等于行为覆盖率——将这一教训编纂为所有未来插件的规则。
|
||||
|
||||
## 教训
|
||||
## 经验教训
|
||||
|
||||
- 命名空间插件与 default export 在 Cordis Loader 下互斥。选择命名空间形式(`name`/`inject`/`Config`/`apply`),不要添加 `export default`——`unwrapExports` 会丢弃命名空间。
|
||||
- 对于插件按需读取但**不**声明在 `static inject` 中的服务,使用 `ctx.get(name)`,绝不使用 `ctx.<name>`。属性代理通过只走祖先方向的 fiber 遍历解析,经由外部 shadow 时会失败;`ctx.get(name)` 是拓扑无关的查找(且默认严格——后端未激活时返回 `undefined`,而非在 teardown 过程中把半拆除的实例交出去)。
|
||||
- 手动构造插件的测试无法验证插件的加载方式。至少一个测试必须端到端地驱动真实的 Loader/export 路径。当核心操作不调用模型时,该测试无需 API key——因此它属于 CI,而非 key 门控之后。
|
||||
- 相信 trace,不要相信理论。优雅的 shadow 解释是真实的,但它是*第二个* bug;*第一个*是一行导出错误,在数小时合理但错误的推理之后,一条 fiber 遍历的 `console.error` 几分钟就找到了它。
|
||||
- 对于插件机会性读取但**未**在 `static inject` 中声明的服务,使用 `ctx.get(name)`,绝不使用 `ctx.<name>`。属性代理通过仅向祖先方向的 fiber 遍历解析,经由外部 shadow 时会失败;`ctx.get(name)` 是拓扑无关的查找(且默认严格——非活跃后端读取为 `undefined`,而非在 teardown 过程中被交出)。
|
||||
- 手动构建插件的测试无法验证插件的加载方式。至少一个测试必须端到端地驱动真实的 Loader/export 路径。当核心操作不调用模型时,该测试无需 API key——因此它属于 CI,而非 key 门控之后。
|
||||
- 相信 trace,不要相信理论。优雅的 shadow 解释是真实的,但它是*第二个* bug;*第一个*是一行导出错误,在数小时看似合理但实际错误的推理之后,一个 fiber 遍历的 `console.error` 在几分钟内就找到了它。
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
0002-js-expression-disabled-filesystem-tools.md: 43e57a6bd1b68f38c47eeda3c3abb8455024b350
|
||||
0002-js-expression-disabled-filesystem-tools.zh.md: e54431b7f4061bb4bdc22a37f0651697c6247dda
|
||||
0002-js-expression-disabled-filesystem-tools.zh.md: 35bd54bb6f1551b5927c00184306141417803eaf
|
||||
@@ -4,44 +4,44 @@
|
||||
|
||||
Status: resolved
|
||||
|
||||
## 概要
|
||||
|
||||
ACP(Agent Client Protocol)示例试图通过 `disabled: !!js ...` 有条件地启用文件系统插件,但 Cordis 仅在插件 `config` 内部对 JavaScript 表达式求值。原始的表达式对象为 truthy,因此文件系统栈始终处于禁用状态。快照刷新随后将 `UNKNOWN_TOOL` 结果接受为新的 golden 基准。修复方案改用显式的文件系统 overlay,并增加了静态配置守卫和快照结果守卫。
|
||||
|
||||
## 摘要
|
||||
|
||||
ACP 示例试图通过 `disabled: !!js ...` 有条件地启用文件系统插件,但 Cordis 仅在插件 `config` 内部求值 JavaScript 表达式。原始的表达式对象为 truthy,因此文件系统栈始终处于禁用状态。快照刷新随后将 `UNKNOWN_TOOL` 结果作为新的 golden 接受。修复方案使用显式的文件系统 overlay,并增加了静态配置守卫和快照结果守卫。
|
||||
默认的 ACP 组合有意只启用 bash,因为其沙箱无法约束进程内的文件系统提供方。文件系统快照场景仍然需要 `read`、`write` 和 `edit`,因此这些插件被放在默认的 `cordis.yml` 中,并附带一个 `disabled` 表达式,意图仅在全权限启动和快照模式下启用它们。
|
||||
|
||||
## 概述
|
||||
|
||||
默认的 ACP 组合有意仅包含 bash,因为其沙箱无法约束进程内的文件系统提供方。文件系统快照场景仍需要 `read`、`write` 和 `edit`,因此这些插件被放入默认的 `cordis.yml`,并附带一个 `disabled` 表达式,意图仅在全权限启动和快照模式下启用它们。
|
||||
|
||||
Cordis Include 将每个 `!!js` 标量解析为一个表达式对象。Loader 递归地对插件的 `config` 进行了插值,但直接消费了 `disabled` 等入口元数据。因此每个文件系统入口都看到一个 truthy 对象,在所有模式下均保持禁用。
|
||||
Cordis Include 将每个 `!!js` 标量解析为一个表达式对象。Loader 递归地对插件的 `config` 进行插值,但直接消费 `disabled` 等入口元数据。因此每个文件系统入口看到的都是一个 truthy 对象,在所有模式下均保持禁用。
|
||||
|
||||
## 影响
|
||||
|
||||
七个文件系统场景和一个混合工作区编辑场景调用了注册表中不存在的工具。它们的结构化会话日志携带 `ToolNotFoundError`(code 为 `UNKNOWN_TOOL`),stdout 则渲染了通用的失败工具卡片。快照套件通过了,因为两个表面都与刷新后的 fixture(测试前置数据)匹配;它证明的是回归的确定性回放,而非文件系统行为的正确性。
|
||||
七个文件系统场景和一个混合工作区编辑场景调用了注册表中不存在的工具。其结构化会话日志携带 `ToolNotFoundError`(code 为 `UNKNOWN_TOOL`),stdout 渲染出通用的失败工具卡片。快照套件通过了,因为两个表面都与刷新后的 fixture(测试前置数据)匹配;它证明的是回归的确定性回放,而非文件系统行为的正确性。
|
||||
|
||||
实际运行的受限默认组合并未获得意外的文件系统访问。一个朴素的插值修复反而会引入该风险:权限预设在运行时更新 bash 沙箱和审批状态,但无法挂载、卸载或约束文件系统栈。
|
||||
实际运行的受限默认模式并未获得意外的文件系统访问权限。一个简单的插值修复反而会制造该风险:权限预设在运行时更新 bash 沙箱和审批状态,但无法挂载、卸载或约束文件系统栈。
|
||||
|
||||
## 时间线
|
||||
|
||||
- PR #261 整合了 ACP 组合并刷新了文件系统快照,同时引入了条件式文件系统入口。
|
||||
- 所有单元测试、覆盖率、快照、文档、构建和 hygiene 检查均通过。
|
||||
- 对刷新后的文件系统 golden 的评审发现了通用的失败卡片和结构化的 `UNKNOWN_TOOL` 结果。
|
||||
- 一次真实的 Loader 启动确认:每个 `disabled` 值仍然是表达式对象,每个文件系统 fiber 均未注册。
|
||||
- 一次真实的 Loader 启动确认:每个 `disabled` 值仍为表达式对象,每个文件系统 fiber 均未创建。
|
||||
|
||||
## 根因
|
||||
|
||||
实现方假设 `!!js` 适用于整个 Loader 入口。其实际边界更窄:`Entry._resolveConfig()` 仅对 `entry.options.config` 进行插值;`Entry.disabled` 直接测试 `entry.options.disabled`,不做插值。YAML 标签在语法上合法,因此加载过程不产生任何诊断信息。
|
||||
实现时假设 `!!js` 适用于整个 Loader 入口。其实际边界更窄:`Entry._resolveConfig()` 仅对 `entry.options.config` 进行插值;`Entry.disabled` 直接测试 `entry.options.disabled`,不经过插值。YAML 标签在语法上合法,因此加载过程不产生任何诊断信息。
|
||||
|
||||
快照框架将任何确定性的 transcript(文本记录)视为有效行为。Header pin 验证了组合后的工具 schema,但文件系统场景共享了来自默认组合的 pin,因此未独立证明其所需工具已注册。刷新在任何语义断言拒绝缺失工具之前,就已重写了预期的 stdout 和会话日志。
|
||||
快照框架将任何确定性的 transcript(文本记录)视为有效行为。Header pin 验证了组合后的工具 schema,但文件系统场景共享来自默认组合的 pin,因此未独立证明其所需工具已注册。刷新在任何语义断言拒绝缺失工具之前,就已重写了预期的 stdout 和会话日志。
|
||||
|
||||
## 新增的防护措施
|
||||
## 已添加的防护措施
|
||||
|
||||
- 文件系统场景启动 `fs.cordis.yml`:一个显式的固定全权限 overlay,配有对应的 replay 配置和独立的 request-header 类。
|
||||
- [`AGENTS.md`](../../AGENTS.md) 和 [Cordis 入门](../cordis-primer.md#loader-configuration) 明确说明 `!!js` 仅在插件 `config` 下有效,条件式组合应使用 overlay。
|
||||
- `verify-cordis-config` 解析仓库中的 Cordis YAML,拒绝 Loader 入口元数据(包括 include patch 和插入的入口)中出现表达式节点。
|
||||
- `dsh-acp-snapshot` 在新鲜运行和已提交的会话 fixture 中拒绝结构化的 `UNKNOWN_TOOL` 结果,阻止其成为被接受的 golden。
|
||||
- [`AGENTS.md`](../../AGENTS.md) 与 [Cordis 入门](../cordis-primer.md#loader-configuration)明确说明 `!!js` 仅在插件 `config` 内有效,条件式组合应使用 overlay。
|
||||
- `verify-cordis-config` 解析仓库中的 Cordis YAML,拒绝 Loader 入口元数据中的表达式节点(包括 include patch 和插入的入口)。
|
||||
- `dsh-acp-snapshot` 在新鲜运行和已提交的会话 fixture 中拒绝结构化的 `UNKNOWN_TOOL` 结果,防止其成为被接受的 golden 基准。
|
||||
|
||||
## 教训
|
||||
|
||||
- 语法上被接受的配置值不一定在该位置被求值;应记录并验证插值边界。
|
||||
- 快照刷新是 fixture 生产,不是正确性评审。像「已注册工具缺失」这样的语义不可能性需要独立于 golden 的断言。
|
||||
- 权限控制只应描述它实际管辖的能力。组合时的文件系统访问无法安全地跟随运行时的 bash-only 预设。
|
||||
- 快照刷新是 fixture 的生产过程,不是正确性审查。诸如已注册工具缺失这类语义上不可能的结果,需要独立于 golden 的断言。
|
||||
- 权限控制只应描述其实际管辖的能力。组合时的文件系统访问无法安全地跟随运行时的 bash-only 预设。
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
README.md: 4dc59e4f5e70f51c4c0baa64fbe34b213f2a7c3d
|
||||
README.zh.md: 7e2d05d429b2521e7e772956b7644740d4642fac
|
||||
README.zh.md: e9ea00dacf3fde6f4d04c9a3b6dd5beeb7929660
|
||||
@@ -2,15 +2,15 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
事故记录:一个 bug 到达了它不该到达的地方(真实用户、已合并的 PR(Pull Request)、已发布的版本),有意义的部分是**为什么我们的流程放过了它**,而不仅仅是那行修复。
|
||||
事件复盘:一个 bug 到达了它不该到达的地方(真实用户、已合并的 PR(Pull Request)、已发布的版本),值得关注的是*为什么我们的流程放过了它*,而不仅仅是那一行修复。
|
||||
|
||||
事后分析不是 [RFC](../rfc/README.md)(RFC 记录的是经过深思熟虑的设计决策及其被否决的替代方案,或提出未来工作)。它是一份面向过去的失败记录:什么坏了、机制是什么、为什么每道安全网都没拦住、以及加了哪些具体护栏使同类 bug 下次能快速失败。
|
||||
事后分析不是 [RFC](../rfc/README.md)(RFC 记录一个经过深思熟虑的设计决策及其被否决的替代方案,或提出未来工作)。它是一份回顾性的失败记录:什么坏了、机制是什么、为什么每道安全网都没拦住、以及加了哪些具体的防护措施使同类 bug 下次能被显式暴露。
|
||||
|
||||
满足以下条件时写一篇:bug **隐蔽**(机制不显而易见,一位细心的工程师也得费力重新推导)、**系统性**(逃逸的原因是测试/工具/约定的缺口,而非一次性手误)、**重新发现的代价高**(它消耗了真实的调试时间,而且下次还会)。请链接该事后分析所推动建立的护栏(测试、AGENTS.md 规则、ADR)。
|
||||
当一个 bug 满足以下条件时,请撰写事后分析:**隐蔽**(机制不显而易见,即使是细心的工程师也得费力重新推导)、**系统性**(逃逸的原因是测试/工具/约定的缺口,而非一次性的笔误)、**重新发现的代价高**(它消耗了真实的调试时间,且下次还会如此)。请链接该事后分析所推动建立的防护措施(测试、AGENTS.md 规则、ADR)。
|
||||
|
||||
每篇事后分析以一段 **Executive summary** 开头:一段简短的文字,让忙碌的读者在三十秒内了解全貌——什么坏了、用通俗语言说的根因、为什么逃逸了、以及持久的教训——之后再展开详细的 Summary / Timeline / Root cause / Guardrails 各节。
|
||||
每篇事后分析以一段**摘要**开头:一个简短段落,让忙碌的读者在三十秒内吸收要点——什么坏了、用直白的话说根因是什么、为什么逃逸了、持久的教训是什么——然后才是后续的详细「概述 / 时间线 / 根因 / 防护措施」各节。
|
||||
|
||||
| # | 标题 |
|
||||
|---|---|
|
||||
| [0001](0001-acp-default-export-drops-inject.md) | ACP server crashed on connect: `export default` dropped the plugin's `inject` |
|
||||
| [0002](0002-js-expression-disabled-filesystem-tools.md) | Filesystem snapshot tools were permanently disabled by a literal `!!js` object |
|
||||
| [0001](0001-acp-default-export-drops-inject.md) | ACP 服务器在连接时崩溃:`export default` 丢失了插件的 `inject` |
|
||||
| [0002](0002-js-expression-disabled-filesystem-tools.md) | 文件系统快照工具被一个字面量 `!!js` 对象永久禁用 |
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
README.md: 9014579f3a98be907885332a0c815bca5c96855c
|
||||
README.zh.md: b51343b35aa04830b694f560ca9ae490995dcd43
|
||||
README.zh.md: cf258dc9ae1d73bf89d6a82c9bf246152c15e8b4
|
||||
+31
-31
@@ -2,48 +2,48 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
这里存放一类设计文档。**RFC** 记录塑造本代码库的决策或提案——代码和文档本身无法承载的*为什么*以及*放弃了什么*。完整列表见生成的 [INDEX.md](INDEX.md);本文是契约——RFC 放在哪里、何时该写,以及[文件内格式](#the-file-format)。
|
||||
这里存放一类设计文档。**RFC** 记录塑造本代码库的决策或提案:代码和文档无法承载的*为什么*以及*放弃了什么*。完整列表见生成的 [INDEX.md](INDEX.md);本文件是契约:RFC 存放在哪里、何时需要写一份,以及[文件内格式](#the-file-format)。
|
||||
|
||||
## 布局与命名
|
||||
|
||||
每篇 RFC 有两个轴,都编码在其**路径**中——`{lifecycle}/{class}/yyyy-mm-dd-topic-title.md`:
|
||||
每份 RFC 有两个维度,都编码在其**路径**中:`{lifecycle}/{class}/yyyy-mm-dd-topic-title.md`。
|
||||
|
||||
- **生命周期**(顶层文件夹)是 RFC 的状态,RFC 随状态变更在文件夹间移动:
|
||||
- **`proposed/`**——实现前评审的提案;尚未构建(或仅部分构建)。
|
||||
- **`implemented/`**——决策已交付。文件记录做了什么决定、否决了什么,并**与实际交付的内容保持同步**:当代码后来移动了文件、重命名了包(package)或更改了键/默认值时,RFC 在同一个变更中更新以匹配(仅限事实——路径、名称、结构——不涉及决策本身)。见 [implemented/AGENTS.md](implemented/AGENTS.md)。
|
||||
- **`rejected/`**——提案经考虑后被否决。保留以备查阅,避免同一问题被反复争论。
|
||||
- **分类**(嵌套文件夹)是决策的*类型*——见下方[分类](#classification)。
|
||||
- **生命周期**(顶层文件夹)是 RFC 的状态,RFC 随状态变化在文件夹之间移动:
|
||||
- **`proposed/`**:实施前评审的提案;尚未构建(或仅部分构建)。
|
||||
- **`implemented/`**:决策已交付。文件记录做了什么决定、否决了什么,并**与实际交付的内容保持同步**:当代码后续移动文件、重命名包(package)或更改键名/默认值时,RFC 在同一个变更中同步更新(仅限事实——路径、名称、结构——而非决策本身)。见 [implemented/AGENTS.md](implemented/AGENTS.md)。
|
||||
- **`rejected/`**:提案经过讨论后被否决。保留以备查阅,避免同一问题被反复争论。
|
||||
- **类别**(嵌套文件夹)是决策的*种类*——见下方[分类](#classification)。
|
||||
|
||||
文件名中的日期是该主题**首次提出**的时间(以 git 历史为准)。RFC 之间的交叉引用使用相对 Markdown 链接(`[topic](../../implemented/architecture/2026-…-….md)`),从不使用纯文字或编号,这样既可机械检查,也能在文件夹间移动时保持有效。
|
||||
|
||||
## 分类
|
||||
|
||||
每篇 RFC 归属于 `scripts/rfc-index.ts` 中封闭集合里的一个路径编码分类;分类门禁拒绝其他文件夹。[INDEX.md](INDEX.md) 由路径、标题和文件名日期生成,其新鲜度受门禁保护。新增分类需要同时更新规范集合与本节。见[分类 RFC](implemented/process/2026-06-20-rfc-classification.md) 与[索引生成 RFC](implemented/process/2026-07-04-generate-rfc-index-tables.md)。
|
||||
每份 RFC 属于 `scripts/rfc-index.ts` 中封闭集合里的一个路径编码类别;分类门禁拒绝其他文件夹。[INDEX.md](INDEX.md) 由路径、标题和文件名日期生成,其新鲜度受门禁保护。新增类别需要同时更新规范集合与本节。见[分类 RFC](implemented/process/2026-06-20-rfc-classification.md) 与[索引生成 RFC](implemented/process/2026-07-04-generate-rfc-index-tables.md)。
|
||||
|
||||
| 分类 | 涵盖内容 |
|
||||
| 类别 | 覆盖范围 |
|
||||
|---|---|
|
||||
| `feature` | 面向用户或模型的新能力。 |
|
||||
| `bug-fix` | 修正缺陷或填补事后复盘暴露的空白。 |
|
||||
| `simplification` | 在不增加能力的前提下移除代码、行为或接口面。 |
|
||||
| `architecture` | 关于**交付源码**的结构性决策——包之间的关系、运行时词汇。 |
|
||||
| `process` | 围绕代码的工具、政策或工作流——门禁、包管理器、vendor 化——而非运行时行为。 |
|
||||
| `bug-fix` | 修正缺陷或弥补事后复盘发现的缺口。 |
|
||||
| `simplification` | 在不增加能力的前提下移除代码、行为或对外表面积。 |
|
||||
| `architecture` | 关于**交付源码**的结构性决策:包之间的关系、运行时词汇。 |
|
||||
| `process` | 代码**周边**的工具、策略或工作流——门禁、包管理器、vendor 化——不涉及运行时行为。 |
|
||||
| `testing` | 测试基础设施与策略。 |
|
||||
|
||||
`architecture` 与 `process` 的分界线:**architecture** 关乎我们交付的源码;**process** 关乎围绕源码的工具与工作流。(`refactor` 被刻意省略——它与 `simplification` 重叠,后者的判别标准「可观测行为是否改变」已覆盖了它。)
|
||||
`architecture` 与 `process` 的界线:**architecture** 关乎我们交付的源码;**process** 关乎围绕源码的工具与工作流。(`refactor` 被有意排除:它与 `simplification` 重叠,而后者的判别标准「可观察行为是否改变」已经覆盖了它。)
|
||||
|
||||
## 何时该写
|
||||
## 何时需要写一份
|
||||
|
||||
当一个决策**持久**(它塑造代码库的范围超出单个函数或包)、**有争议**(存在一个合理工程师可能选择的真实替代方案)、且**令人意外**(未来读者否则会问「为什么要这样做」)时,请写一篇 RFC。对未来大量工作的提案从 `proposed/` 开始;已做出的决策从 `implemented/` 开始。选择与决策匹配的分类文件夹(见[分类](#classification))。
|
||||
当一个决策具备以下三个特征时,请写一份 RFC:**持久性**(它的影响超出单个函数或包)、**争议性**(存在一个合理工程师可能选择的真实替代方案)、**意外性**(未来读者否则会问「为什么要这样做」)。对未来重大工作的提案从 `proposed/` 开始;已经做出的决策从 `implemented/` 开始。选择与决策匹配的类别文件夹(见[分类](#classification))。
|
||||
|
||||
以下情况**不要**写 RFC:机械性或局部的选择(变量名、单文件重构);已由门禁或 AGENTS.md 中的约定强制并解释的事项;代码中标记为 `TODO(...)` 的暂定决策——将其记为 TODO,待尘埃落定后再提升为 RFC。RFC 永远不会被编辑成*另一个决策*:用新 RFC 取代旧的并互相链接。(编辑 `implemented/` RFC 以跟踪其已做出的决策现在*位于何处*——移动的文件、重命名的包——不是另一个决策,是必须做的,而非禁止的;见 [implemented/AGENTS.md](implemented/AGENTS.md)。)
|
||||
以下情况**不要**写 RFC:机械性或局部的选择(一个变量名、一次单文件重构);已由门禁或 AGENTS.md 中的约定强制执行并解释的事项;代码中标记为 `TODO(...)` 的临时决策——将其记为 TODO,待稳定后再升级为 RFC。RFC 永远不会被编辑为一个*不同的决策*:用新 RFC 取代旧的,并互相链接。(编辑 `implemented/` RFC 以跟踪其已做出的决策现在*位于*何处——移动的文件、重命名的包——不是不同的决策,这是必需的而非禁止的;见 [implemented/AGENTS.md](implemented/AGENTS.md)。)
|
||||
|
||||
## 文件格式
|
||||
|
||||
每篇 RFC 遵循统一的文件内格式,由 `pnpm run verify-rfc-format`([scripts/verify-rfc-format.ts](../../scripts/verify-rfc-format.ts),doc-sync(文档同步门禁)的一环)强制执行;该格式的设计动机及其否决的替代方案见[统一格式 RFC](implemented/process/2026-07-05-uniform-rfc-format.md)。
|
||||
每份 RFC 遵循统一的文件内格式,由 `pnpm run verify-rfc-format`([scripts/verify-rfc-format.ts](../../scripts/verify-rfc-format.ts),`doc-sync`(文档同步门禁)的一环)强制执行;该格式的设计动机及其否决的替代方案见[统一格式 RFC](implemented/process/2026-07-05-uniform-rfc-format.md)。
|
||||
|
||||
### 头部块
|
||||
|
||||
每篇 RFC 的前三行严格为:
|
||||
每份 RFC 的前三行严格为:
|
||||
|
||||
```markdown
|
||||
# RFC: <title>
|
||||
@@ -51,17 +51,17 @@
|
||||
Status: <status>
|
||||
```
|
||||
|
||||
后接一个空行。`Status:` 的值有三种形式,且必须与文件所在的生命周期文件夹一致——门禁会交叉检查:
|
||||
后跟一个空行。`Status:` 的值有三种形式,且必须与文件所在的生命周期文件夹一致——门禁会交叉检查:
|
||||
|
||||
- `Status: proposed`
|
||||
- `Status: implemented`
|
||||
- `Status: rejected — <why, in one line>`
|
||||
|
||||
状态行不带日期、不带括号补充说明:文件名承载首次提出日期,git 承载其余一切,「以修订形式接受」之类的说明属于正文内容(在陈述决策的地方说明修订)。否决原因是唯一带内容的状态行,因为读者查阅被否决 RFC 时要的就是结论。
|
||||
状态行不带日期、不带括号补充说明:文件名记录首次提出日期,git 记录其余一切;「以修订形式接受」之类的说明属于正文内容(在陈述决策的地方说明修订)。拒绝原因是唯一带内容的状态,因为读者查阅被否决的 RFC 时,结论正是他们要找的。
|
||||
|
||||
### 正文骨架
|
||||
|
||||
每篇 RFC 的正文以 `## Problem` 开头——动机,写法应独立于解决方案。后续内容取决于生命周期;重复出现的章节使用以下规范名称且仅限这些名称,而真正特有的技术章节(包拓扑、协议格式(wire format)、schema)在必需章节之间自由编排。
|
||||
每份 RFC 的正文以 `## Problem` 开头:动机,写法上不依赖解决方案即可独立成文。后续内容取决于生命周期;固定章节使用以下规范名称且仅限这些名称,而真正独特的技术章节(包拓扑、协议契约、schema 等)在必需章节之间可自由组织。
|
||||
|
||||
#### `proposed/`
|
||||
|
||||
@@ -74,7 +74,7 @@ Status: <status>
|
||||
## Risks
|
||||
```
|
||||
|
||||
`## Proposal` 是拟议的变更,可以正当地使用将来时——计划、迁移步骤和未决问题在工作尚未构建时属于此处。`## Acceptance criteria` 说明什么可观测状态意味着完成。`## Risks` 涵盖可能出错的事项以及变更有意放弃的东西。
|
||||
`## Proposal` 描述拟议的变更,可以合理地使用将来时态——计划、迁移步骤和待解决问题在工作尚未完成时属于此处。`## Acceptance criteria` 说明什么可观察状态意味着完成。`## Risks` 涵盖可能出错的事项以及该变更有意放弃的东西。
|
||||
|
||||
#### `implemented/`
|
||||
|
||||
@@ -86,26 +86,26 @@ Status: <status>
|
||||
## Consequences
|
||||
```
|
||||
|
||||
`## Decision` 以现在时描述已交付的现实,整个文件按 [implemented/AGENTS.md](implemented/AGENTS.md) 的要求与之保持同步。`## Consequences` 记录权衡的代价**与**收益。提案阶段的标题在这里属于规格用语,门禁会拒绝:`## Proposal`、`## Plan`、`## Migration plan` 和 `## Acceptance criteria` 不得出现在 implemented RFC 中([slop 检查清单](../AGENTS.md)说明了原因)。`## Testing`、`## Deferred` 或 `## Related` 章节在陈述现在时事实时是允许的。
|
||||
`## Decision` 以现在时态描述已交付的现实,整个文件按 [implemented/AGENTS.md](implemented/AGENTS.md) 的要求与之保持同步。`## Consequences` 记录权衡的代价**与**收益。提案阶段的标题在此属于规格用语,门禁会拒绝它们:`## Proposal`、`## Plan`、`## Migration plan` 和 `## Acceptance criteria` 不得出现在 implemented RFC 中(原因见 [slop 检查清单](../AGENTS.md))。`## Testing`、`## Deferred` 或 `## Related` 章节在陈述现在时态的事实时是允许的。
|
||||
|
||||
#### `rejected/`
|
||||
|
||||
被否决的 RFC 是冻结的提案:保留其提案时的所有章节(包括 `## Acceptance criteria` 或 `## Plan`),结论写在 `Status:` 行。仅头部块、`## Problem` 开头、`## Proposal` 章节,以及下方的「曾考虑的替代方案」强制要求适用。
|
||||
被否决的 RFC 是冻结的提案:保留提案时的所有章节(包括 `## Acceptance criteria` 或 `## Plan`),结论写在 `Status:` 行上。仅头部块、`## Problem` 开头、`## Proposal` 章节以及下方的「曾考虑的替代方案」强制要求适用。
|
||||
|
||||
### 曾考虑的替代方案——强制要求
|
||||
### 曾考虑的替代方案——必需
|
||||
|
||||
每篇 RFC 都必须有一个 `## Alternatives considered` 章节:每个真实的替代方案及其落选原因,每个替代方案一段(加粗引导),或对争议较大的方案使用 `### Why not <X>?` 子章节。记录决策却不记录它击败了什么,就是在邀请反复争论——正是 RFC 存在的目的所要防止的。
|
||||
每份 RFC 都必须包含 `## Alternatives considered` 章节:每个真实的替代方案及其落选原因,每个替代方案用一个加粗引导的段落,或对争议较大的替代方案用 `### Why not <X>?` 子节。记录决策时不记录它击败了什么,就是在邀请反复争论——正是 RFC 存在的意义所要防止的。
|
||||
|
||||
替代方案是记录下来的,而非凭空编造的。日期早于 2026-07-05 的 RFC,如果其替代方案无法从记录中重建,则在该章节位置放置以下精确注释,门禁仅对格式前文件接受此注释:
|
||||
替代方案是记录下来的,不是凭空编造的。日期早于 2026-07-05 且替代方案无法从记录中重建的 RFC,在该章节位置放置以下精确注释,门禁仅对格式规范之前的文件接受此注释:
|
||||
|
||||
```markdown
|
||||
<!-- rfc-format: alternatives-not-recorded (pre-format RFC) -->
|
||||
```
|
||||
|
||||
### 在生命周期间移动
|
||||
### 在生命周期之间移动
|
||||
|
||||
将文件在生命周期文件夹间移动意味着在同一个变更中更新 `Status:` 行并满足目标文件夹的骨架要求——否则门禁会失败。具体而言,`proposed/` → `implemented/` 将 `## Proposal` 改写为现在时的 `## Decision`,将 `## Acceptance criteria` 和 `## Risks` 折叠进 `## Consequences`(或一个现在时的 `## Testing`/`## Verification` 章节,用于说明现在什么在固定该行为),并用实际交付的内容替换计划——即 [implemented/AGENTS.md](implemented/AGENTS.md) 要求的改写,使之机械化。`proposed/` → `rejected/` 仅在 `Status:` 行添加原因并冻结文件。
|
||||
将文件在生命周期文件夹之间移动意味着在同一个变更中更新 `Status:` 行并满足目标文件夹的骨架要求——否则门禁会失败。具体而言,`proposed/` → `implemented/` 将 `## Proposal` 改写为现在时态的 `## Decision`,将 `## Acceptance criteria` 和 `## Risks` 折入 `## Consequences`(或折入一个现在时态的 `## Testing`/`## Verification` 章节,用于描述现在锁定该行为的内容),并用实际交付的内容替换计划——即 [implemented/AGENTS.md](implemented/AGENTS.md) 所要求的改写,使之机械化。`proposed/` → `rejected/` 仅在 `Status:` 行添加原因并冻结文件。
|
||||
|
||||
### 中文对侧文件
|
||||
|
||||
`.zh.md` 对侧文件按 [i18n 契约](../i18n/README.md)逐章节镜像其英文兄弟文件的结构;机器检查的头部标记(`# RFC: ` 和 `Status:` 行)保持英文原样不变。格式门禁跳过 `.zh.md` 文件——配对门禁负责它们的一致性。
|
||||
`.zh.md` 对侧文件按 [i18n 契约](../i18n/README.md)逐章节镜像其英文兄弟文件的结构;机器检查的头部标记(`# RFC: ` 和 `Status:` 行)保持英文原样不翻译。格式门禁跳过 `.zh.md` 文件——配对门禁负责它们的一致性。
|
||||
Reference in New Issue
Block a user