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

This commit is contained in:
ZiyaZhang
2026-07-22 03:04:57 -07:00
10 files changed
+87 -87

No files matched your search

+1 -1
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
+34 -34
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)
+1 -1
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
+14 -14
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 中,以确保资源释放按预期顺序回
+1 -1
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
+10 -10
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`)——可预测的全局可读路径会招致符号链接竞和信息泄露。
+1 -1
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
+11 -11
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>
+1 -1
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
+13 -13
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`)。
真正昂贵或不确定的边界处 mock(LLM 适配器、网络、时钟);下游一切保持真实。手写的替身只能证明桥接层在搬运字节,不能证明交付的工具行为符合断言——两者会漂移,而测试继续绿着。例: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 的缺口是排期工作,不是构建中途的意外。