Merge branch 'docs/i18n-batch-cds-postmortem' into docs/i18n-batch-rfc

This commit is contained in:
ZiyaZhang
2026-07-22 03:06:14 -07:00
54 changed files with 386 additions and 386 deletions

View File

@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
architecture.md: 32b9700b9aece988985ff932e597b537872f12c3
architecture.zh.md: 1adb0111fb67c6e252153e2500732a320de44523
architecture.zh.md: 6a4cf414da141b9d19e15d68e6b98458822b612a

View File

@@ -2,13 +2,13 @@
[English](architecture.md) | 中文
**DeepSeek Harness SDK** 基于 Cordis 构建 agent harness智能体框架。原则很简单**一切皆插件**。内置的循环只是一个插件,不是特权内核。
**DeepSeek Harness SDK** 基于 Cordis 构建 agent harness智能体框架。原则很简单**一切皆插件**。内置的循环只是一个插件,而非特权内核。
## 概览
一个 harness 就是一个 [Cordis](cordis-primer.md) 上下文。各包package贡献服务键、类型化事件和可 dispose资源释放的注册:服务暴露稳定调用(`ctx.llm``ctx.tools``ctx.sessions`),事件提供拦截与通知(`agent/request``tools/pre-execute``session/event`),注册则安装提示词段、工具、提供方、适配器或监听器。
一个 harness 就是一个 [Cordis](cordis-primer.md) 上下文。各包package贡献服务键、类型化事件和可释放的注册服务暴露稳定调用`ctx.llm``ctx.tools``ctx.sessions`),事件提供拦截与通知(`agent/request``tools/pre-execute``session/event`),注册则安装提示词段、工具、提供方、适配器或监听器。
`packages/core/` 组织了默认的 agent 流程;周能力同样是一等的 Cordis 插件。
`packages/core/` 组织了默认的 agent 流程;周围的能力同样是一等的 Cordis 插件。
### 默认服务
@@ -27,7 +27,7 @@
|---|---|---|
| `ctx.llm` | [`llm/`](../packages/llm/README.md) | 适配器注册表与流式模型调用 |
| `ctx.bash` | [`bash/`](../packages/bash/README.md) | 前台/后台命令执行 |
| `ctx.sandbox` | [`sandbox/`](../packages/sandbox/README.md) | 同世界进程隔离argv 包装、逐调用策略) |
| `ctx.sandbox` | [`sandbox/`](../packages/sandbox/README.md) | 同世界进程隔离argv 包装、逐策略) |
| `ctx.codeRuntime` | [`code-runtime/`](../packages/code-runtime/README.md) | 模型编写的程序执行 |
| `ctx.fs` | [`fs/`](../packages/fs/README.md) | 文件系统提供方原语与策略事件 |
| `ctx.skills` | [`skill/`](../packages/skill/README.md) | skill技能提供方注册表与渐进式披露 |
@@ -36,27 +36,27 @@
| `ctx.subagents` | [`subagent/`](../packages/subagent/README.md) | 命名委托提供方 |
| `ctx.workflows` | [`workflow/`](../packages/workflow/README.md) | 脚本驱动的多 agent 编排 |
| `ctx.sessionPersistence` | [`session-persistence/`](../packages/session-persistence/README.md) | 会话日志的持久化存储 |
| `ctx.sessionQuery` | [`session-query/`](../packages/session-query/README.md) | 活跃优先的逻辑语料库与精确事件读取 |
| `ctx.sessionQuery` | [`session-query/`](../packages/session-query/README.md) | 优先活跃会话的逻辑语料库与精确事件读取 |
## 事件
事件构成服务扩展 API详见完整的[事件目录](cordis-catalog/events.md)与[生产/消费方映射](event-producer-consumer.md)。
事件构成服务扩展 API详见完整的[事件目录](cordis-catalog/events.md)与[生产/消费方映射](event-producer-consumer.md)。
### 事件域
- **会话事件**是持久的、可回放的事实。轮次与步骤边界、用户输入、助手输出、工具调用、工具结果、steering中途引导、压缩记录以及工具拥有的持久事实追加到会话日志流经 `session/event`
- **Agent 事件**携带活跃的 `Agent` 句柄,用于状态、诊断、prompt 准入、调用配置塑形、结果校验与续行策略。
- **能力事件**属于拥有该动作的 seam。`tools/*``llm/*``system-prompt/*``fs/*``subagent/*` 让策略和适配器无需导入循环即可接入。
- **会话事件**是持久的、可回放的事实。轮次与步骤边界、用户输入、助手输出、工具调用、工具结果、steering中途引导、压缩记录以及工具拥有的持久事实追加到会话日志通过 `session/event` 流出
- **Agent 事件**携带活跃的 `Agent` 句柄,用于状态、诊断、提示词准入、调用配置塑形、结果校验与续行策略。
- **能力事件**属于拥有该动作的 seam。`tools/*``llm/*``system-prompt/*``fs/*``subagent/*` 让策略和适配器无需导入循环即可接入。
### 拦截语义
waterfall瀑布式事件的行为类似 around 中间件:监听器通过调用 `next()` 委托下游;不调用 `next()` 直接返回即为否决或接管。完整规则见 [Cordis waterfall 语义](cordis-primer.md#cordis-waterfall-semantics)。
waterfall瀑布式事件的行为类似环绕中间件:监听器通过调用 `next()` 委托下游;不调用 `next()` 直接返回则表示否决或接管。完整规则见 [Cordis waterfall 语义](cordis-primer.md#cordis-waterfall-semantics)。
## 默认循环生命周期
内置循环消耗工作队列、组装请求、流式接收模型回答、执行工具、应用续行策略并持久化检查点。每个暂停点都是一个服务调用或事件,可供插件介入
内置循环排空工作队列、组装请求、流式接收模型回答、执行工具、应用续行策略并持久化状态检查点。每个暂停点都是一个对插件可用的服务调用或事件。
**会话**是一个 agent 的仅追加事件日志。**轮次turn**消耗一批排队消息,运行到模型不再请求工具且没有插件求续行为止。**步骤step**是一次模型请求加上该响应引发的工具执行。下面的流程[时序图伴侣文档](agent-lifecycle.md)),带引号的名称是持久化的会话事件,事件名称是扩展点。
**会话**是一个 agent 的仅追加事件日志。**轮次**排空一批排队消息,运行到模型不再请求工具且没有插件求续行。**步骤**是一次模型请求加上该响应引发的工具执行。下流程([时序伴随文档](agent-lifecycle.md),带引号的名称是持久会话事件,事件名称是扩展点。
### 轮次流程
@@ -96,76 +96,76 @@ forever:
checkpoint persistence and notify idle/running status
```
循环每步骤渲染一次 prompt 组装。插件贡献有序段、工具 schema `{{name}}` 变量;未知或无值的引用会使轮次失败,而非带着空洞发送。`dsh-system-prompt` 拥有 harness 身份与默认部署人agent 作用域的人可以遮蔽默认值。循环提供 `model``cwd`。见 [prompt 所有权 RFC](rfc/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md)。
循环每步骤渲染一次提示词组装。插件贡献有序段、工具 schema `{{name}}` 变量;未知或无值的引用会使轮次失败,而非带着空洞发送。`dsh-system-prompt` 拥有 harness 身份与默认部署人agent 作用域的人可以遮蔽默认值。循环提供 `model``cwd`。见[提示词归属 RFC](rfc/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md)。
Post-tool 上下文在所有工具结果之后落入,以保持 tool-call/result 的邻接稳定。steering 在步骤之间排空;轮次结束后的普通剩余 steering 作为输入重新入队。终止性的 `agent/turn-stop` 是显式例外:它在普通续行与 steering 折叠之后运行,然后在轮次关闭和刷新期间保持权威,因此这些后续监听器产生的 steering 被丢弃而非成为新的步骤或轮次;普通排队的 prompt 则被保留。
工具后上下文在所有工具结果之后追加,以保持工具调用/结果的邻接稳定。Steering 在步骤之间排空;轮次结束后的普通剩余 steering 作为输入重新入队。终止性的 `agent/turn-stop` 是显式例外:它在普通续行与 steering 折叠之后运行,然后在轮次关闭和刷新期间保持权威,使后续监听器产生的 steering 被丢弃而非变成另一个步骤或轮次;普通排队的提示词则被保留。
### 失败边界
轮次是容错边界。抛出异常的监听器、适配器错误结束或失败的步骤会以错误原因结束当前轮次,并通过 `agent/error` 报告实时诊断;它不会杀死驱动循环。`cancel()` 清除排队 steering 工作,在可能时中止活跃的模型/工具边界并记录相应的轮次结束。dispose 停止循环、等待静默、注销 agent并让服务 disposer 排空。
轮次是容错边界。抛出异常的监听器、适配器错误结束或失败的步骤会以错误原因结束当前轮次,并通过 `agent/error` 报告实时诊断;它不会终止驱动循环。`cancel()` 清除排队 steering 工作,在可能时中止活跃的模型/工具边界并记录相应的轮次结束。dispose(资源释放)停止循环、等待静默、注销 agent并让服务 disposer 排空。
每个会话事件都被轮次包围。重新加载崩溃的会话时,系统保留中断的尾部并以合成的 `interrupted` 轮次结束关闭。持久轮次已关闭的失败仅通过 `agent/error` 报告,因为已没有安全的轮次内位置。轮次以一个 `TurnEndReason` 结束(`completed``aborted``error``disposed``max-tokens``rejected``interrupted`);各变体的语义见 [session.md § TurnEndReasonMap](core-data-structures/session.md#why-a-turn-ended-turnendreasonmap)。
每个会话事件都被轮次包围。重新加载崩溃的会话时,中断的尾部被保留,并以合成的 `interrupted` 轮次结束关闭。持久轮次已关闭之后发生的失败仅通过 `agent/error` 报告,因为已没有安全的轮次内位置。轮次以一个 `TurnEndReason` 结束(`completed``aborted``error``disposed``max-tokens``rejected``interrupted`);各变体的语义见 [session.md § TurnEndReasonMap](core-data-structures/session.md#why-a-turn-ended-turnendreasonmap)。
### Agent 句柄
`ctx.agents` 拥有活跃 agent 并返回 `AgentHandle { agent, dispose() }``Agent` 是其他插件驱动的 API`send()` 入队工作,`steer()` 注入轮次中内容,`inject()` 追加上下文并在空闲时开启一次性注入轮次,`cancel()` 是公开的停止原语,`whenIdle()` 观察静默状态。调用方 fiber 与具体工厂提供方在结构上共同拥有编程式生命周期;消费方句柄是唯一的非结构性拆能力,每个所有者到达同一个被 await 的 disposer。
`ctx.agents` 拥有活跃 agent 并返回 `AgentHandle { agent, dispose() }``Agent` 是其他插件驱动的 API`send()` 入队工作,`steer()` 注入轮次中内容,`inject()` 追加上下文并在空闲时开启一次性注入轮次,`cancel()` 是公开的停止原语,`whenIdle()` 观察静默状态。调用方 fiber 与具体工厂提供方在结构上共同拥有编程式生命周期;消费方句柄是唯一的非结构性拆能力,每个所有者到达同一个被等待的 disposer。
### Agent 作用域
每个活跃 agent 拥有一个作用域化的 `agent.ctx`。其注册遮蔽同名全局注册,只接收该 agent 的发,并随 agent 一起解除`CreateAgentOptions.setup(agentCtx)` 在发布前组合作用域。[语义门禁 RFC](rfc/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.md) 定义了类型化解析器,从合并的 `Events` 签名与 `scopeTarget` 派生载体检查,消除了手写事件表。见 [agent 作用域 RFC](rfc/implemented/architecture/2026-07-08-agent-scope-contexts.md)subagent 组合控制另行记录于[](rfc/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md)。
每个活跃 agent 拥有一个作用域化的 `agent.ctx`。其注册遮蔽同名全局注册,只接收该 agent 的发,并随 agent 一起卸载`CreateAgentOptions.setup(agentCtx)` 在发布前组合作用域。[语义门禁 RFC](rfc/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.md) 定义了类型化解析器,从合并的 `Events` 签名与 `scopeTarget` 推导载体检查,消除了手写事件表。见 [agent 作用域 RFC](rfc/implemented/architecture/2026-07-08-agent-scope-contexts.md)subagent 组合控制另行[文档化](rfc/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md)。
## 状态
### 会话日志
会话日志是真源。`deriveMessages()` 将会话事件投影为发送给模型的 `Message[]`;原始 `assistant/chunk` 事件留在日志中用于回放和 UI 保真。回放、fork、恢复、transcript文本记录渲染、遥测持久化都从同一事件流派生。
会话日志是真源。`deriveMessages()` 将会话事件投影为发送给模型的 `Message[]`;原始 `assistant/chunk` 事件留在日志中用于回放和 UI 保真。回放、fork、恢复、transcript文本记录渲染、遥测持久化都从同一事件流派生。
**模型可见 ⟺ 已记录**:日志重建每请求——`step/start` 处的消息前置 header 的会话前缀header 通过折叠 `request/header` 得出——开发不变式对此断言([可重建性 RFC](rfc/implemented/architecture/2026-07-05-reconstructable-requests.md))。
**模型可见 ⟺ 已记录**:日志重建每请求`step/start` 处的消息 header 的 session prefix 为前缀header 通过折叠 `request/header` 得出开发不变式对此进行断言([可重建性 RFC](rfc/implemented/architecture/2026-07-05-reconstructable-requests.md))。
持久性是插件关注点。持久化后端缓冲同步的 `session/event` 通知,循环在轮次结束检查点完成后才继续。`SessionPersistence` seam 直接存储 `SessionEvent`,元数据在 `SessionHeader`JSONL 与 SQLite 共享同一套契约测试。
### 模型内容
消息是类型化内容块(`text``reasoning``tool-call``tool-result`的数组。联合类型派生自可合并扩展的 `ContentBlockMap`;同一模式也用于 `MessageSource``FinishReason``TurnTrigger` `TurnEndReason`。新的块类型需要跨适配器、UI 桥接、压缩计价持久化协调,因此块类型仍是仓库级契约。
消息是类型化内容块的数组`text``reasoning``tool-call``tool-result`)。联合类型派生自可合并扩展的 `ContentBlockMap`;同一模式也用于 `MessageSource``FinishReason``TurnTrigger` `TurnEndReason`。新的块类型需要跨适配器、UI 桥接、压缩计价持久化协调,因此块类型仍是仓库级契约。
流式输出是原始分片协议(从 `block-start``finish``BlockAssembler` 是共享的 chunk 到 block 组装器。循环在组装分片以供发的同时记录原始 chunk`LlmAdapter` 是提供方 seam继承、实现 `stream()`、用 `ctx.llm.registerAdapter(models, adapter)` 注册。StreamChunk 约定见 [llm-streaming.md](core-data-structures/llm-streaming.md)。
流式输出是原始分片协议(从 `block-start``finish``BlockAssembler` 是共享的分片到块组装器。循环在组装分片以供发的同时记录原始分片`LlmAdapter` 是提供方 seam继承、实现 `stream()`,然后通过 `ctx.llm.registerAdapter(models, adapter)` 注册。StreamChunk 约定见 [llm-streaming.md](core-data-structures/llm-streaming.md)。
## 扩展与组合
### 能力模式
一个可替换的能力通常拆分为**接口 / 实现 / 消费方**:接口拥有其 `ctx`事件,实现注册后端,消费方通过工具或 prompt 暴露模型行为。Bash 是参考实现;[能力图](capability-seams.md)展示了每个族。
一个可替换的能力通常拆分为**接口/实现/消费方**:接口拥有其 `ctx`事件,实现注册后端,消费方通过工具或提示词暴露模型行为。Bash 是参考实现;[能力图](capability-seams.md)展示了每个族。
部分 seam 有意偏离模板。LLM(大语言模型)将接口与消费方词汇放在一起,因为适配器就是实现。文件系统在提供方原语周围添加策略门。Web 是一个服务加搜索/抓取两个提供方注册表,因此提供方替换不会重命名模型工具。skill subagent 使用命名提供方注册表;本地 skill 扫描项目/用户根目录,其他提供方可以在不改动注册表/工具的情况下添加嵌入式或远程目录。subagent 可以全新 spawn、从父级已完成轮次的前缀 fork或使用 ACP 子进程([subagent.md](core-data-structures/subagent.md))。
部分 seam 有意偏离模板。LLM 将接口与消费方词汇放在一起,因为适配器就是实现。文件系统在提供方原语周围增加了策略门。Web 是一个服务加搜索/抓取两个提供方注册表,因此替换提供方不会重命名模型工具。Skills 和 subagents 使用命名提供方注册表;本地 skills 扫描项目/用户根目录,其他提供方可以添加嵌入式或远程目录而无需修改注册表/工具。Subagents 可以全新 spawn、从父级已完成轮次的前缀 fork或使用 ACP 子进程([subagent.md](core-data-structures/subagent.md))。
### Bundle 与应用
`dsh-agent-spine-demo` 是默认的组合 bundle一个插件加载共享主干[README](../packages/examples/agent-spine-demo/README.md))。应用包将其与前端入口和启动 `bin` 组合`dsh-stdio-demo` 用于终端 REPL`dsh-acp-demo` 用于基于 JSON-RPC stdio ACP(无 stdout logger[ui/](../packages/ui/README.md))。`dsh-jsonrpc-agent` 则启动外部 `cordis.yml`Python SDK 在未设置显式配置通道时注入包默认值,并通过行分隔的 stdio JSON-RPC 驱动 `dsh-jsonrpc`[Python SDK](../python/README.md))。一个部署就是一片薄薄的 `cordis.yml` 叶子:可替换的后端、一个应用入口可选的产品工具([examples/](../examples/AGENTS.md)、[可运行接线](cookbook/extension-cookbook.md#runnable-wirings)、[关系图索引](graph-atlas.md))。
`dsh-agent-spine-demo` 是默认的组合 bundle一个插件加载共享主干[README](../packages/examples/agent-spine-demo/README.md))。应用包在其上组合前端入口和启动 `bin``dsh-stdio-demo` 用于终端 REPL`dsh-acp-demo` 用于通过 JSON-RPC stdio 提供 ACP 且不带 stdout 日志[ui/](../packages/ui/README.md))。`dsh-jsonrpc-agent` 则启动外部 `cordis.yml`Python SDK 在未设置显式配置通道时注入包默认值,并通过行分隔的 stdio JSON-RPC 驱动 `dsh-jsonrpc`[Python SDK](../python/README.md))。一个部署就是一片薄薄的 `cordis.yml` 叶子:可替换的后端、一个应用入口,加上可选的产品工具([examples/](../examples/AGENTS.md)、[可运行接线](cookbook/extension-cookbook.md#runnable-wirings)、[关系图索引](graph-atlas.md))。
### 新行为的归属
新行为应接入已记录的扩展点;修改内置循环需要同步更新映射。
新行为应接入已文档化的扩展点;修改内置循环需要同步更新映射
| 目标 | 机制 |
|---|---|
| 添加模型提供方 | 在 `ctx.llm` 上注册适配器 |
| 添加面向模型的能力 | 在 `ctx.tools` 上注册工具schema 流入 prompt 组装 |
| 添加面向模型的能力 | 在 `ctx.tools` 上注册工具schema 流入提示词组装 |
| 添加命令执行 | 实现并注册 `ctx.bash` 后端 |
| 添加文件系统访问或策略 | 实现 `ctx.fs` 提供方或监听 `fs/*` 策略事件 |
| 隔离 spawn 的进程 | 一个 `ctx.sandbox` 后端;消费方在 spawn 前包装 argv |
| 拦截 prompt、请求、工具使用或续行 | 监听相关 `agent/*``tools/*` waterfall使用串行 `agent/turn-stop` 实现单调终止 |
| 拦截提示词、请求、工具使用或续行 | 监听相关 `agent/*``tools/*` waterfall使用串行 `agent/turn-stop` 实现单调终止停止 |
| 添加历史之外的会话稳定请求前缀 | 在 `agent/session-prefix` 上组合,每个循环实例一次;记录在请求 header 上 |
| 添加 UI 或编辑器集成 | 驱动 `ctx.agents` 并从 `session/event` 渲染 |
| 添加持久会话状态 | 添加 `SessionEventMap` 成员并从日志渲染/回放 |
| fork 活跃会话 | 使用 `ctx.sessions.fork(source, boundary?, childSessionId?)` |
| 将工具、prompt 段或监听器限定到单个 agent | 通过该 agent 的 `agent.ctx` 注册(见 Agent 作用域) |
| 添加持久会话状态 | 添加 `SessionEventMap` 成员并从日志渲染/回放 |
| Fork 活跃会话 | 使用 `ctx.sessions.fork(source, boundary?, childSessionId?)` |
| 将工具、提示词段或监听器限定到单个 agent | 通过该 agent 的 `agent.ctx` 注册(见 Agent 作用域) |
[扩展实操手册cookbook](cookbook/extension-cookbook.md)提供插件骨架功能到 seam 的映射;分步指南覆盖[](cookbook/adding-a-package.md)、[工具](cookbook/adding-a-tool.md)、[LLM 适配器](cookbook/adding-an-llm-adapter.md)与[vendor 包](cookbook/adding-a-vendored-package.md)。
[扩展实操手册](cookbook/extension-cookbook.md)提供插件骨架功能到 seam 的映射;分步指南覆盖[](cookbook/adding-a-package.md)、[工具](cookbook/adding-a-tool.md)、[LLM 适配器](cookbook/adding-an-llm-adapter.md)与 [vendor 包](cookbook/adding-a-vendored-package.md)。
## 快速参考
- 领域术语见[术语表](glossary.md)
- 类型定义见 [core-data-structures/](core-data-structures/core.md)
- 精确的事件与服务签名见[事件目录](cordis-catalog/events.md)
- [服务目录](cordis-catalog/services.md)
- 精确的事件与服务签名见[事件](cordis-catalog/events.md)
- [服务](cordis-catalog/services.md)目录
- 包契约见[包映射](../packages/README.md)
- [RFC](rfc/README.md)

View File

@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
cordis-primer.md: 39d3d97b9ac43fec50cb0c832af449fc8bc6232f
cordis-primer.zh.md: 4915f665cae51b44f89190d43d147e5cda0df146
cordis-primer.zh.md: f941c90489e1153910ed809d2cef30ece8539f8f

View File

@@ -2,19 +2,19 @@
[English](cordis-primer.md) | 中文
Cordis 是 DeepSeek Harness SDK 底层以 vendor 方式引入的插件框架。本入门文档讲解 harness 插件作者在阅读生成的[事件](cordis-catalog/events.md)与[服务](cordis-catalog/services.md)目录之前需要了解的 Cordis 核心概念。vendor 源码与同步流程见 [vendor/README.md](../vendor/README.md)。
Cordis 是 DeepSeek Harness SDK 底层以 vendor 方式引入的插件框架。本文介绍 harness 插件作者在阅读生成的[事件](cordis-catalog/events.md)与[服务](cordis-catalog/services.md)目录之前需要了解的 Cordis 核心概念。vendor 源码与同步流程见 [vendor/README.md](../vendor/README.md)。
## Cordis 五大理
## 五个核心概
- **插件是实现 Service 的对象。** 它可以是一个带有可选 `inject``apply(ctx)` 字段的函数,也可以是一个 `Service` 子类,其生命周期由 Cordis 挂载到当前上下文中。
- **上下文是服务的注册表。** 一个服务在上下文中声明一个稳定的 `ctx.<key>`(如 `ctx.tools``ctx.llm``ctx.sessions`);其他插件通过 key 查找服务,而非导入具体实现。
- **通过 `inject` 声明服务依赖。** 插件声明所需的服务后,会等待这些服务就绪;加载顺序通过服务依赖表达,而非手动编排启动序列。
- **类型化事件用于通信。** 服务通过 TypeScript 声明合并定义事件名,然后以 `emit``waterfall`(瀑布式事件)、`parallel``serial` 方式分发,分别对应监听者观察、包装、并行扇出或按序执行。
- **注册是可逆的副作用。** 提示词片段、工具 schema、适配器、提供方和监听器通过 `ctx.effect()``ctx.on()` 安装,因此重载和拆卸能可预地回退它们
- **插件是实现 Service 的对象。** 它可以是一个带有可选 `inject``apply(ctx)` 字段的函数,也可以是一个 `Service` 子类,其生命周期由 Cordis 挂载到当前上下文中。
- **上下文是服务的容器。** 一个服务占据一个稳定的 `ctx.<key>`(如 `ctx.tools``ctx.llm``ctx.sessions`);其他插件通过 key 查找服务,而非导入具体实现。
- **通过 `inject` 声明服务依赖。** 插件声明所需的服务后,会等待这些服务就绪才启动;加载顺序通过服务依赖表达,而非手动编排启动序列。
- **类型化事件用于通信。** 服务通过 TypeScript 声明合并注册事件名,然后以 `emit``waterfall`(瀑布式事件)、`parallel``serial` 方式分发,分别对应监听者观察、包装、并行扇出或按序执行。
- **注册是可逆的副作用。** 提示词片段、工具 schema、适配器、提供方和监听器通过 `ctx.effect()``ctx.on()` 安装,reload 和 teardown 时可预地回
## 分发模式
每个事件具有以下分发模式之一,且只能通过对应方法分发。
每个事件具有以下分发模式之一,且只能通过对应方法分发。
| 模式 | 是否 await | 分发顺序 | 是否有返回值? |
|---|---|---|---|
@@ -23,22 +23,22 @@ Cordis 是 DeepSeek Harness SDK 底层以 vendor 方式引入的插件框架。
| `parallel` | 是 | 所有监听器并行观察事件 | 否 |
| `serial` | 是 | 监听器按注册顺序观察 | 是 |
分发模式是事件公开契约的一部分。新的 harness 事件通过 `@mode` 标签记录,以便生成的目录将声明与分发站点进行交叉校验。
分发模式是事件公开契约的一部分。新的 harness 事件通过 `@mode` 标签记录模式,以便生成的目录可以将声明与分发调用点做交叉校验。
## Cordis Waterfall 语义
`ctx.waterfall` 是环绕中间件。监听器接收 `(...args, next)`。调用 `next()` 将可能经过包装的结果委托给下一个服务;不调用 `next()` 直接返回则短路。值通过 `next()` 的返回值向下传播。
协作式监听器通常修改一个共享的请求或决策对象,然后委托。监听器也可以选择完全替换结果,下游监听器只看到替换后的结果。仅当监听器必须在普通注册之前运行时才使用 `prepend: true`
协作式监听器通常修改一个共享的请求或决策对象,然后委托。监听器也可以选择完全替换结果,下游监听器只看到替换后的结果。仅当监听器必须在普通注册之前运行时才使用 `prepend: true`
对于单决策事件,短路是设计意图。策略监听器在拥有决策权时可以不调用 `next()` 直接返回,而仅做标注或观察的监听器必须委托。
对于单决策事件,短路是设计意图。策略监听器在拥有决策权时可以不调用 `next()` 直接返回,而仅做标注或观察的监听器必须委托。
## Loader 配置
`@cordisjs/plugin-include``!!js` 解析为表达式节点,但 Loader 仅在挂载插件前对条目的 `config` 进行插值。条目元数据(`id``name``group``disabled``inject``intercept``isolate`)保持字面值;因此 `disabled: !!js ...` 是一个真值对象,总是会禁用该条目。需要根据环境选择挂载哪些插件时,请使用显式的配置覆盖。
`@cordisjs/plugin-include``!!js` 解析为表达式节点,但 Loader 仅在挂载插件前对条目的 `config` 插值。条目元数据(`id``name``group``disabled``inject``intercept``isolate`)保持字面值;因此 `disabled: !!js ...` 是一个 truthy 对象,会始终禁用该条目。需要根据环境选择挂载哪些插件时,请使用显式的配置覆盖
## 实践规则
将行为封装插件:工具流水线事件属于 `ctx.tools`,模型流式输出属于 `ctx.llm`,实时 agent 协调属于 `ctx.agents`。拦截和策略优先使用事件;直接能力调用优先使用服务方法。
将行为封装插件:工具流水线事件属于 `ctx.tools`,模型流式输出属于 `ctx.llm`,实时 agent(智能体)协调属于 `ctx.agents`。拦截和策略优先使用事件;直接能力调用优先使用服务方法。
每个注册都应有对应的 dispose资源释放:要么从 `ctx.effect()` 返回一个,要么使用 Cordis 提供的辅助函数自动处理。如果拆卸顺序有要求,请将相关工作放在同一个 effect 中,以确保 dispose 按预期顺序回退
每个注册都应有对应的 disposerdispose资源释放函数):要么从 `ctx.effect()` 返回一个,要么使用 Cordis 提供的辅助方法自动处理。如果 teardown 顺序有要求,请将相关工作放在同一个 effect 中,以确保资源释放按预期顺序回

View File

@@ -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

View File

@@ -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 绑定到其所属插件。

View File

@@ -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

View File

@@ -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)作为终端呈现)。

View File

@@ -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

View File

@@ -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` 携带**运行时所需的全部信息**。按照「包packageseam 处显式优于隐式」的规则,默认值(时间预算、输出上限)实现的已校验配置提供,绝不是 `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 完成前被终止并等待结束

View File

@@ -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

View File

@@ -1,28 +1,28 @@
# 压缩
# 上下文压缩
[English](compaction.md) | 中文
压缩compactionseam 是一个[能力 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 compactionseam 是一个[能力 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` 负责。

View File

@@ -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

View File

@@ -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` 签名。

View File

@@ -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

View File

@@ -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/SSEServer-Sent Events与 `dsh-llm-pi-ai`(通过 `@earendil-works/pi-ai` 访问同一端点)。两套独立内部实现共享一份契约,正是将协议钉死的方式:库支持的适配器无法在流中途抛异常,因此它行使了手写适配器可能不会走到的 finish-chunk 错误路径。
这份契约正是两个适配器作为意配对存在的原因:`dsh-llm-deepseek`(手写 fetch/SSEServer-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 {

View File

@@ -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

View File

@@ -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)协调写入。

View File

@@ -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

View File

@@ -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 内核裁定。

View File

@@ -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

View File

@@ -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 {

View File

@@ -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

View File

@@ -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 =

View File

@@ -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

View File

@@ -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 恰好 ACPAgent 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)。

View File

@@ -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

View File

@@ -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 目录。工具结果是模型获取完整指令的可见路径。

View File

@@ -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

View File

@@ -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`,并返回同一个 runrejection 意味着提供方已自行清理,不发出生命周期事件。进程内子 agent 可通过 `ctx.agents` 发现,远程子 agent 则不必如此。`subagent/end` 报告最终输出或基础设施故障。两个事件均为仅观察事件,包含监听器异常。
`start()` 仅在 run 就绪时 fulfill。服务观察其 result、发出 `subagent/start`,并返回同一个 runrejection 意味着提供方已自行清理,不发出生命周期配对事件。进程内子 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) 回放可以接受它(进行中的、未平衡的轮次被排除在外)。

View File

@@ -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

View File

@@ -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 {

View File

@@ -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

View File

@@ -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 SchemaLLM大语言模型协议格式不是校验/转换——轻量 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 SchemaLLM大语言模型协议格式),而非校验/转换——轻量 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 可以替换 contentblock 会变为包含纠正反馈的 `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 Schemaschema 原样传给模型作为强制工具的 `parameters`,产出的值由客户端的 `validateStructuredValue` 校验——因此每个被接受的关键字都必须是校验器实际执行的,`assertSupportedOutputSchema` 会大声拒绝其他任何内容(`OutputSchemaError`,列出所有违规)。两个遍历器都只处理自有可枚举属性JSON 不携带其他东西),并拒绝会有损序列化的非普通对象(`Date`、`Map`)。
调用方用来向 subagent 要求机器可读结果的词汇(`SubagentStartRequest.outputSchema`,见 [subagent.md](subagent.md#the-start-request)),或工作流 `agent()` 调用使用的词汇。它有意**不是**完整的 JSON Schemaschema 原样传给模型作为强制工具的 `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)ACPAgent 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)。

View File

@@ -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

View File

@@ -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` 将其映射为 ACPAgent 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 {

View File

@@ -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

View File

@@ -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`。

View File

@@ -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

View File

@@ -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` settleworker-thread 引擎随后终止脚本的 worker因此消费方 await `result` 不会在取消后永远卡住。`dispose()` = cancel + 有界 settle + 子 agent 静默;它不会因脚本卡死而挂起。
脚本执行期间消费方持有的句柄。消费方 await `result`,可中途 `cancel`,且*必须*在每条路径上 `dispose`。`result` 不会 reject脚本失败以 `stopReason: 'error'` resolve一旦运行被取消即使脚本本身永不 settle它也会在引擎的有界宽限期内 settle引擎强制以 `cancelled` settleworker-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` 一致。

View File

@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
defensive-patterns.md: fda0be0d2d3b7fa099162123b3d219673eebd07d
defensive-patterns.zh.md: 60d7389db50c30e2b85fd88b320f04b43e22d84d
defensive-patterns.zh.md: f6e4712a4a239c954193f63f32285037eb6d4f0e

View File

@@ -2,28 +2,28 @@
[English](defensive-patterns.md) | 中文
来之不易的缺陷类别规则:下每条模式都是本项目实际发布或险些发布的一类缺陷,以防止其复发的规则形式陈述。在编写生命周期、并发、子进程或清理代码之前请先阅读本文。测试层面的对应规则真实入口路径、world 验证、资源归属)见 [testing.md](testing.md)。
来之不易的缺陷类别规则:下每条模式都是本项目实际发布或差点发布的一类缺陷以防止其复发的规则形式陈述。在编写生命周期、并发、子进程或清理代码之前请先阅读本文。测试层面的对应规则真实入口路径、world 验证、资源归属)见 [testing.md](testing.md)。
## 正交结果独立上报
一个结果可以同时具有多重性质:进程可能既超时又以 exit 0 退出,因为它捕获了信号。每个独立事实(`timedOut``signal``exitCode`)都应独立暴露;永远不要把一个 flag 的上报嵌套在另一个 flag 的分支内,否则调用方会把一次被截断的运行误读为正常成功。
一个结果可以同时具有多重性质:进程可能既超时又以 exit 0 退出,因为它捕获了信号。每个独立事实(`timedOut``signal``exitCode`)都应独立暴露;切勿将某个 flag 的上报嵌套在另一个 flag 的分支内,否则调用方会把一次被截断的运行误读为正常成功。
## 在接口两侧都遵守跨 seam 契约
## 跨 seam 契约两侧都要遵守
当接口文档记录了两种有效的信号方式时——例如适配器可以通过从 `stream()` 抛出异常来报告失败,也可以通过以 `finish {kind:'error'|'aborted'}` 分片结束流来报告——消费方必须两种都处理,而不只是第一个实现碰巧使用的那种。基于库的适配器在流中途无法抛出异常,只能依赖带内路径;如果 agent loop 只捕获 throw,就会把提供方的 401 变成一个正常完成的轮次。请在类型定义处记录契约;通过真实消费方测试每个分支。
一个接口文档记录了两种合法的信号方式时——例如适配器可以通过从 `stream()` 抛出异常来报告失败,也可以通过以 `finish {kind:'error'|'aborted'}` 分片结束流来报告——消费方必须同时处理两种路径,而不是只处理第一个实现恰好使用的那种。依赖库的适配器可能无法在流中途抛出异常,只能带内路径;如果 agent loop(智能体循环)只捕获抛出的异常,就会把提供方的 401 错误变成一个正常完成的轮次。请在类型定义处记录契约;通过真实消费方测试每个分支。
## 异步状态不是同步状态
`agent.send()` 不会在返回前翻转状态;后台任务的完成与轮次边界存在竞`reader.close()` 在 EOF 和 dispose资源释放两种情况下都会触发。永远不要基于一个刚刚请求的状态来控制流程——应当基于实际触发的事件/promise 来驱动生命周期`agent/status``task.done`),并观察状态转换(先看到 `running` 再看到 `idle`),而不是假设你发出的动作与轮次 1:1 对应(循环会批量处理排队消息。这条守则是双向的如果等待的转换永远不会发生EOF 没有提交过工作 → 永远不会进入 `running`),等待就会挂起——请显式处理「无需等待」的分支。
`agent.send()` 不会在返回前翻转状态;后台任务的完成与轮次边界存在竞`reader.close()` 在 EOF 和 dispose资源释放两种情况下都会触发。切勿基于一个刚刚请求的状态来控制流程——应实际触发的事件/promise`agent/status``task.done`驱动生命周期,并观察状态转换(先看到 `running` 再看到 `idle`),而非计数你假定与轮次一一对应的操作循环会批量处理排队消息。这条守则是双向的如果等待的转换永远不会发生EOF 没有提交过任何工作 → 永远不会进入 `running`),等待就会挂起——请显式处理「无需等待」的分支。
## dispose 必须达到静止,而仅仅请求停止
## Dispose 必须达到静止,而仅仅请求停止
一个发出 kill/abort 就返回的清理逻辑会留下孤儿进程。请让清理逻辑异步化并 await 子进程退出kill → await `done`),并在 kill 之前关闭监听器/通知注册表,使迟到的完成事件保持静默。测试应证明 dispose 确实等到了进程退出`await fiber.dispose()` 之后 pid 已不存在),而仅仅证明进程最终会死。
一个清理流程如果发出 kill/abort 就返回、而不等待工作实际停止,就会留下孤儿进程。请让清理逻辑异步化并 await 子进程退出kill → await `done`),并在 kill 之前关闭监听器/通知注册表,使迟到的完成事件保持静默。测试应证明 dispose 确实等待了`await fiber.dispose()` 之后 pid 已不存在),而仅仅进程最终会死。
## 在边界处包容回调异常
用户提供的监听器抛出异常,不得导致它所在的 promise 被 reject也不得饿死排在它后的监听器。请在分发循环中用 try/catch 包裹并记录日志;一个有问题的订阅者永远不能破坏核心生命周期。
用户提供的监听器如果抛出异常,不得导致它所在的 promise 被 reject也不得饿死排在它后的监听器。请用 try/catch 包裹分发循环并记录日志;一个行为不当的订阅者不能破坏核心生命周期。
## 永远不要把环境变量或可预测路径暴露给不可信输出
## 绝不将环境变量或可预测路径暴露给不可信输出
spawn 的命令应获得一经过清洗的 env`*KEY*`/`*SECRET*`/`*TOKEN*`确保 harness 凭证不会泄漏到输出、`env` 或溢出文件中。临时/溢出文件应使用私有0700目录、随机文件名和排他的仅所有者可打开式(`'wx'``0o600`)——可预测的全局可读路径会招致符号链接竞和信息泄露。
spawn 的命令应获得一经过清洗的 env`*KEY*`/`*SECRET*`/`*TOKEN*`使 harness 凭证无法泄漏到输出、`env` 或溢出文件中。临时/溢出文件应使用私有0700目录、随机文件名和排他的仅所有者可访问打开式(`'wx'``0o600`)——可预测的全局可读路径会招致符号链接竞和信息泄露。

View File

@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
glossary.md: 23eebc5793e8232482a796f5bde1e794f0556208
glossary.zh.md: 7163015eeed71c96743b9cae491db206585a70b2
glossary.zh.md: a233f926ba3af0a4e90de90bf21d68a2244c3ea6

View File

@@ -2,18 +2,18 @@
[English](glossary.md) | 中文
DeepSeek Harness SDK 的领域词汇对每个概念使用唯一的规范术语。各术语通过标准 Markdown 锚点互相链接;实现细节留在各 package README RFC 中。
DeepSeek Harness SDK 的领域词汇对每个概念使用唯一的规范术语。各术语通过标准 Markdown 锚点互相链接;实现细节留在各 package README RFC 中。
FIXME(glossary-completeness): Expand this glossary before the first release so it covers the SDK's other core and capability subsystems, not only agent scope.
## agent 作用域
## agent-scope
- **scope(作用域)**:按 agent智能体注册单位。一项贡献(工具、prompt 段落、变量、限制、监听器)要么是*全局*的(对所有 agent 可见),要么是*有作用域*的(归属于恰好一个 [scope key](#scope-key))。只有两层,扁平结构:有作用域的注册不会向下继承给 subagent子树行为通过[血统](#lineage)数据表达,从不通过作用域结构。
- **scope key(作用域键)**:作用域的不透明标识按对象同一性比较。harness 约定:一个活跃的 agent 就是其自身作用域的 key。<a id="scope-key"></a>
- **agent context`agent.ctx`**agent 的有作用域上下文;通过它进行的注册既是作用域可见的,也是作用域生命周期的(一个事实同时驱动两者),其上的监听器参与该 agent 的作用域过滤分发。注册表主体事件可以在其自身的事件契约下有意保持不过滤。
- **scope carrier(作用域载体)**:作用域过滤分发所携带的 `thisArg`(由 `scopeTarget` 构建);其过滤器放行无标签监听器加上主体自身的监听器。*无主体*的载体(没有 key只放行无标签监听器。
- **scoped dispatch(作用域分发)**:规则是:关于某个 agent 活动的事件以该 agent 的载体进行分发。关于注册表本身的事件(如「一个工具被添加」)属于*注册表主体*事件,保持不过滤。
- **shadowing(遮蔽)**:最具体者胜出的名称解析:一个有作用域的工具/段/变量仅在该作用域内替代其同名的全局副本。这是按 agent 定制人设和按 agent 定制工具变体的机制。
- **restriction / scope-local registration限制 / 作用域局部注册)**:限制(`tools.restrict`)为单个作用域过滤全局工具面(按交集组合);作用域局部注册在过滤之后合并。被过滤掉的全局工具既不出现在 prompt 中,也拒绝执行,与不存在的工具无法区分。
- **setup window(设置窗口)**:创建者组装 agent 有作用域世界的创建时隙(`CreateAgentOptions.setup`):在作用域和 agent 对象已存在、但 agent 或会话尚未发布、`agent/session-start` 尚未触发、首次 prompt 尚未组装之前。设置窗口只做注册,从不驱动 agent。
- **lineage(血统)**:以数据形式携带的父子关系(`parentSession``subagentDepth`);从不影响可见性。<a id="lineage"></a>
- **scope**:按 agent智能体划分的注册单位。一项贡献(工具、提示词片段、变量、限制、监听器)要么是*全局的*(对所有 agent 可见),要么是*有范围的*(归属于恰好一个 [scope key](#scope-key))。只有两层,扁平结构:有范围的注册不会向下继承给 subagent子树行为通过 [lineage](#lineage) 数据表达,从不通过 scope 结构。
- **scope key**scope 的不透明标识按对象同一性比较。harness 约定:一个活跃的 agent 就是其自身 scope 的 key。<a id="scope-key"></a>
- **agent 上下文`agent.ctx`**agent 的有范围上下文;通过它进行的注册既是 scope 可见的,也是 scope 生命周期的(同一事实决定两者),其上的监听器参与该 agent 的 scope 过滤分发。注册表主体事件可以在各自的事件契约下保持故意不过滤。
- **scope carrier**scope 过滤分发所携带的 `thisArg`(由 `scopeTarget` 构建);其过滤器放行无标签监听器加上主体自身的监听器。*无主体*的 carrier(没有 key只放行无标签监听器。
- **scoped dispatch**:规则是:关于某个 agent 活动的事件以该 agent 的 carrier 进行分发。关于注册表本身的事件(如「一个工具被添加」)属于*注册表主体*事件,保持不过滤。
- **shadowing**:最具体者胜出的名称解析:一个有范围的工具/段/变量仅在该 scope 内替换同名的全局对应项。这是按 agent 定制 persona 和按 agent 定制工具变体的机制。
- **restriction / scope-local 注册**restriction`tools.restrict`)为单个 scope 过滤全局工具面(多个 restriction 取交集组合scope-local 注册在过滤之后合并。被过滤掉的全局工具既不出现在提示词中,也拒绝执行,与不存在的工具无法区分。
- **setup window**:创建者组装 agent 有范围世界的创建时隙(`CreateAgentOptions.setup`):在 scope 和 agent 对象已存在、但 agent 或会话尚未发布、`agent/session-start` 尚未触发、首次提示词尚未组装之前。setup 只做注册,从不驱动 agent。
- **lineage**:以数据形式携带的父子关系事实`parentSession``subagentDepth`);从不影响可见性。<a id="lineage"></a>

View File

@@ -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

View File

@@ -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规则。
两个集成错误在单元测试全覆盖的情况下仍然导致 ACPAgent 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 的人都会立即遇硬性失败。无数据丢失(崩溃前没有任何内容被持久化);代价完全是「功能不可用」加上两次定位原因的调试时间。
## 时间线
- BridgeRFC 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 遍历),通过隔离修复并重新运行真实子进程得到确认。
- bridgeRFC 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` 几分钟就找到了它。

View File

@@ -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

View File

@@ -4,44 +4,44 @@
Status: resolved
## 概要
ACPAgent 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 预设。

View File

@@ -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

View File

@@ -2,15 +2,15 @@
[English](README.md) | 中文
故记录:一个 bug 到达了它不该到达的地方(真实用户、已合并的 PRPull Request、已发布的版本有意义的部分是**为什么我们的流程放过了它**,而不仅仅是那行修复。
件复盘:一个 bug 到达了它不该到达的地方(真实用户、已合并的 PRPull 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` 对象永久禁用 |

View File

@@ -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

View File

@@ -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` 文件——配对门禁负责它们的一致性。

View File

@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
testing.md: ddb9da0b38e5dc9cc75ede81ec157c4744fd11c2
testing.zh.md: 6d21a37b175c8052fbc34db0fc5a9b522c27f7ec
testing.zh.md: 8a37a9eaffbf19b205e3b98e7609464133060cf2

View File

@@ -2,34 +2,34 @@
[English](testing.md) | 中文
本文说明本仓库如何逐层测试,以及保持绿色测试套件有意义的规则。命令见根目录 [AGENTS.md](../AGENTS.md);关联 RFC 承载设计动机。
本文说明本仓库的分层测试方式,以及保持绿色测试套件有意义的规则。命令见根目录 [AGENTS.md](../AGENTS.md);关联 RFC 承载设计动机。
## 层级
- **单元测试**`pnpm run test`vitest 运行 `packages|examples/*/tests/**/*.spec.ts`,与被测代码同目录。每个注册表都有一个 HMR热模块替换安全测试dispose 贡献该注册的 fiber断言清理完成。优先覆盖边界情况、错误路径、事件顺序、并发竞态永久契约回归(见 `packages/core/agent-loop/tests/contract-regressions.spec.ts`)。
- **覆盖率门禁**`pnpm run test:coverage`):门禁级运行,对 `packages/*/*/src` 按文件 100% 覆盖。未覆盖的行往往是门禁正确标记出的死代码(应删除),而非需要补写的测试。行覆盖率是必要条件,绝非充分条件:它证明代码行被执行过,不证明功能按交付预期工作。
- **真实 API e2e**`pnpm run test:e2e`):带密钥测试,对接真实提供方 API——DeepSeek 模型各提供方独立冒烟测试(各自依赖自己的密钥:`EXA_API_KEY``PERPLEXITY_API_KEY` 等);每个套件在缺少对应密钥时自动跳过,确保无密钥 CI 保持绿色([真实 API e2e RFC](rfc/implemented/testing/2026-06-19-real-api-e2e-ci.md))。
- **快照测试**`pnpm run test:snapshot`):启动真实示例子进程,无密钥回放录制的会话,将归一化的 stdout 与重新持久化的日志已提交的 golden 文件做 diff[快照 RFC](rfc/implemented/testing/2026-06-19-acp-snapshot-tests.md))。当模型 transcript文本记录需要变更时使用 `pnpm run test:snapshot:record`;当已提交的 transcript 仍是正确的 mock LLM大语言模型输入、只需无密钥重写回放 golden 时使用 `pnpm run test:snapshot:refresh`。请审 golden diff。系统提示词/工具 schema 内容由一个场景(`text-turn`)固定,其余 fixture测试前置数据中以 token 化形式引用,因此 prompt 或 schema 的修改只变动一行已提交内容([pinned-header RFC](rfc/implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.md))。
- **单元测试**`pnpm run test`vitest 运行 `packages|examples/*/tests/**/*.spec.ts`,与被测代码同目录。每个注册表都有一个 HMR热模块替换安全测试dispose(资源释放)贡献的 fiber断言清理完成。优先覆盖边界情况、错误路径、事件顺序、并发竞态,以及永久契约回归(见 `packages/core/agent-loop/tests/contract-regressions.spec.ts`)。
- **覆盖率门禁**`pnpm run test:coverage`):门禁级运行,对 `packages/*/*/src` 按文件 100% 覆盖。未覆盖的行往往是门禁正确标记出的死代码(应删除),而非需要补写的测试。行覆盖率是必要条件,但永远不是充分条件:它证明行被执行过,不证明功能按交付预期工作。
- **真实 API e2e**`pnpm run test:e2e`):带密钥测试,调用真实提供方 API。包括 DeepSeek 模型以及各提供方特有的冒烟测试(各自依赖自己的密钥:`EXA_API_KEY``PERPLEXITY_API_KEY` 等);缺少密钥时各套件自动跳过keyless CI 保持绿色([真实 API e2e RFC](rfc/implemented/testing/2026-06-19-real-api-e2e-ci.md))。
- **快照测试**`pnpm run test:snapshot`):启动真实示例子进程,无密钥环境下回放录制的会话,将归一化的 stdout 与重新持久化的日志已提交的 golden 文件做 diff[快照 RFC](rfc/implemented/testing/2026-06-19-acp-snapshot-tests.md))。当模型 transcript文本记录需要变更时使用 `pnpm run test:snapshot:record`;当已提交的 transcript 仍是正确的 mock LLM大语言模型输入、只需无密钥重写回放 golden 时使用 `pnpm run test:snapshot:refresh`。请审 golden diff。系统提示词/工具 schema 内容由**一个**场景(`text-turn`)固定,其余 fixture测试前置数据中以 token 化形式引用,因此 prompt 或 schema 的修改只影响一行已提交内容([pinned-header RFC](rfc/implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.md))。
## 带密钥策略:推理在这里很便宜
我们是 DeepSeek不要吝惜真实 API 测试。无密钥测试证明管道通;只有带密钥运行才能证明 agent 对接真实模型能正常工作。写:文件写入 prompt、多轮对话、工具调用、流中取消。价值最高的是**冒烟测试**:启动真实示例、发送一条真实 prompt、检查外部世界的状态。它们能捕获「单元测试全绿、产品却坏了」这类 mock 在结构上无法发现的问题([事后分析 0001](postmortem/0001-acp-default-export-drops-inject.md))。自动跳过机制的存在仅仅是为了不阻塞无密钥 CI 和无密钥贡献者,它不是成本信号。每个示例都附带一个无密钥冒烟测试,以及(除非本身就不需要密钥一个带密钥冒烟测试([examples/AGENTS.md](../examples/AGENTS.md))。
我们是 DeepSeek不要吝惜真实 API 测试。无密钥测试只能证明管道通;只有带密钥运行才能证明 agent(智能体)在真实模型面前能正常工作。请大量编写:文件写入 prompt、多轮对话、工具调用、流中取消。价值最高的是**冒烟测试**:启动真实示例、发送一条真实 prompt、检查外部世界的状态。它们能捕获「单元测试全绿、产品却坏了」这类 mock 在结构上无法发现的问题([事后分析 0001](postmortem/0001-acp-default-export-drops-inject.md))。自动跳过机制的存在仅仅是为了不阻塞无密钥 CI 和无密钥贡献者,它不是成本信号。每个示例都附带一个 keyless 冒烟测试,并且——除非本身就不需要密钥——还附带一个带密钥冒烟测试([examples/AGENTS.md](../examples/AGENTS.md))。
## 优先使用真实实现而非 mock
mock 真正昂贵或不确定的边界LLM 适配器、网络、时钟);下游一切保持真实。手写的替身只能证明桥接层在搬运字节,不能证明交付的工具行为符合断言——两者会漂移,而测试仍然绿着。bridge 工具调用测试运行脚本化的 mock 模型,但使用真实的 tool + 真实的执行器(`makeBridgeHarness({ withBash: true })` 接入 `dsh-bash-local` + `dsh-tool-bash`执行真正的 `echo`)。
真正昂贵或不确定的边界处 mockLLM 适配器、网络、时钟);下游一切保持真实。手写的替身只能证明桥接层在搬运字节,不能证明交付的工具行为符合断言——两者会漂移,而测试继续绿着。例bridge 工具调用测试运行脚本化的 mock 模型,但使用真实的 tool + 真实的执行器(`makeBridgeHarness({ withBash: true })` 接入 `dsh-bash-local` + `dsh-tool-bash`执行真正的 `echo`)。
## 验证外部世界,而非自我报告
e2e 断言应重新运行命令或从外部重新读取文件;对 agent 自身输出做关键词探测会让作弊的 agent 通过。断言未改的文件字节相同。e2e 测试拥有自己的资源:在测试中创建 harness`afterEach` 中 dispose即使失败/重试/超时);共享 fixture 放在普通的 `tests/harness.ts` 中,绝不放在另一个 `*.e2e.ts` import 一个 spec 会重新注册其 `describe`,导致真实 API 调用重复)。
e2e 断言应重新运行命令或从外部重新读取文件;对 agent 自身输出做关键词探测会让作弊的 agent 通过。断言未改的文件字节一致。e2e 测试自行管理资源:在测试中创建 harness`afterEach` 中 dispose即使失败/重试/超时也要释放);共享 fixture 放在普通的 `tests/harness.ts` 中,绝不放在另一个 `*.e2e.ts` 中(导入一个 spec 会重新注册其 `describe`,导致真实 API 调用重复执行)。
## 测试真实入口路径
- 产品可见的插件需要一个非单元的真实组合测试。手工搭建的 `ctx.plugin(...)` 套件不够:通过 Loader 和 app/process 启动仅用于测试的 `cordis.yml`,只 mock 外部/不确定边界,断言模型可见的请求/日志、持久状态或用户可见输出。不要把 opt-in 混入默认交付
- 一个守卫只有在回归真让它失败时才算守卫。对于没有 `inject` 的插件bundle/组合插件Loader 冒烟测试在导出形状损坏时仍然绿——需要加一个显式的 `expect('default' in mod).toBe(false)``unwrapExports` 往返断言,并证明它有效:引入回归、观察变红、还原
- 「真实入口路径」指已发布的产物package 的 `bin` 指向构建出的 `lib/bin.js`,在原生 `node` 下运行;tsx 会掩盖竞态、模块解析问题以及静默以 0 退出的加载失败。同适用于构建后 package 在运行时解析的任何非 index 运行时入口worker-thread 运行时的兄弟文件 `lib/worker.cjs`)。保持构建产物冒烟测试绿色(`packages/ui/*/tests/built-bin.e2e.ts``packages/code-runtime/code-runtime-worker/tests/built-lib.e2e.ts`),并断言真正缺失的配置以非零退出。
- 从临时 cwd spawn 示例的 e2e 测试需要设置 `TSX_TSCONFIG_PATH` 指向仓库根 tsconfig否则会静默回退到陈旧的构建 `lib/`[examples/AGENTS.md](../examples/AGENTS.md))。
- 产品可见的插件必须有一个非单元的真实组合测试。手动构建的 `ctx.plugin(...)` 套件不够:通过 Loader 和 app/process 启动仅用于测试的 `cordis.yml`,只 mock 外部/不确定边界,断言模型可见的请求/日志、持久状态或用户可见输出。不要把 opt-in 选项混入交付默认值
- 一个守卫只有在回归真的能让它失败时才有效。对于没有 `inject` 的插件bundle/组合插件Loader 冒烟测试在导出形状损坏时仍然绿——需要加显式的 `expect('default' in mod).toBe(false)``unwrapExports` 往返断言,并证明它有效:引入回归、观察变红、回退
- 「真实入口路径」指已发布的产物package 的 `bin` 指向在普通 `node` 下运行的构建产物 `lib/bin.js`tsx 会掩盖问题(竞态、模块解析、吞掉的加载失败以 exit 0 退出)。同适用于构建后 package 在运行时解析的任何非 index 运行时入口worker-thread 运行时的兄弟文件 `lib/worker.cjs`)。保持构建产物冒烟测试绿色(`packages/ui/*/tests/built-bin.e2e.ts``packages/code-runtime/code-runtime-worker/tests/built-lib.e2e.ts`),并断言真正缺失的配置以非零退出码退出
- 从临时 cwd spawn 示例的 e2e 测试需要设置 `TSX_TSCONFIG_PATH` 仓库根目录的 tsconfig否则会静默回退到陈旧的构建产物 `lib/`[examples/AGENTS.md](../examples/AGENTS.md))。
## 何时需要快照测试
任何影响编辑器侧 transcript 或端到端 agent 用户体验的变更——ACP bridge、agent loop智能体循环的可观测输出、工具呈现——都在所属示例的快照套件中添加或更新场景(`examples/<name>/tests/snapshots/`,基于 [`dsh-acp-snapshot`](../packages/support/acp-snapshot/README.md) 套件工厂的场景表;`examples/acp-agent` 是主套件),或在 PR 中说明为何不适用。新的能力 seam、生命周期形态或 transcript 表面在计划阶段就要标明各层的覆盖方,并验证 harness 能表达它——harness 的缺口是排期工作,不是构建中途的意外。
任何影响编辑器侧 transcript 或端到端 agent UX 的变更——ACP bridge、agent loop智能体循环的可观测输出、工具呈现——都需要在所属示例的快照套件中添加或更新场景(`examples/<name>/tests/snapshots/`,基于 [`dsh-acp-snapshot`](../packages/support/acp-snapshot/README.md) 套件工厂的场景表;`examples/acp-agent` 是主套件),或在 PR 中说明为何不适用。新的能力 seam、生命周期形态或 transcript 表面在计划阶段就要列出各层的覆盖方,并验证 harness 能表达它——harness 的缺口是排期工作,不是构建中途的意外。