Merge remote-tracking branch 'origin/master' into xjt/generated-docs-zh-translation-apply

# Conflicts:
#	.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.i18n.yaml
#	.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.zh.md
#	.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.i18n.yaml
#	.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.zh.md
#	.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.i18n.yaml
#	.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.zh.md
#	.agents/notes/implemented/architecture/2026-07-27-compiler-independent-typert-model.i18n.yaml
#	.agents/notes/implemented/architecture/2026-07-27-compiler-independent-typert-model.zh.md
#	.agents/notes/implemented/architecture/2026-07-28-directory-picker-capability-seam.i18n.yaml
#	.agents/notes/implemented/architecture/2026-07-28-directory-picker-capability-seam.zh.md
#	.agents/notes/implemented/architecture/2026-07-30-static-repository-plugin-format.i18n.yaml
#	.agents/notes/implemented/architecture/2026-07-30-static-repository-plugin-format.zh.md
#	.agents/notes/implemented/architecture/2026-07-31-claimed-pre-step-inbox-lifecycle.i18n.yaml
#	.agents/notes/implemented/architecture/2026-07-31-claimed-pre-step-inbox-lifecycle.zh.md
#	.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.i18n.yaml
#	.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.zh.md
#	.agents/notes/implemented/architecture/2026-08-01-packaged-ripgrep-search.i18n.yaml
#	.agents/notes/implemented/architecture/2026-08-01-packaged-ripgrep-search.zh.md
#	.agents/notes/implemented/architecture/2026-08-04-websocket-downlink-carrier.i18n.yaml
#	.agents/notes/implemented/architecture/2026-08-04-websocket-downlink-carrier.zh.md
#	.agents/notes/implemented/architecture/2026-08-05-large-session-jsonl-restore-pipeline.i18n.yaml
#	.agents/notes/implemented/architecture/2026-08-05-large-session-jsonl-restore-pipeline.zh.md
#	.agents/notes/implemented/architecture/2026-08-06-agent-event-payload-objects.i18n.yaml
#	.agents/notes/implemented/architecture/2026-08-06-agent-event-payload-objects.zh.md
#	.agents/notes/implemented/architecture/2026-08-06-subagent-list-identity-projection.i18n.yaml
#	.agents/notes/implemented/architecture/2026-08-06-subagent-list-identity-projection.zh.md
#	.agents/notes/implemented/architecture/2026-08-06-web-markdown-incremental-ast-renderer.i18n.yaml
#	.agents/notes/implemented/architecture/2026-08-06-web-markdown-incremental-ast-renderer.zh.md
#	.agents/notes/implemented/architecture/2026-08-08-client-tool-presentation-ownership.i18n.yaml
#	.agents/notes/implemented/architecture/2026-08-08-client-tool-presentation-ownership.zh.md
#	.agents/notes/implemented/bug-fix/2026-07-27-glob-sampling.i18n.yaml
#	.agents/notes/implemented/bug-fix/2026-07-27-glob-sampling.zh.md
#	.agents/notes/implemented/bug-fix/2026-07-28-themed-scrollbars-and-reserved-gutter.i18n.yaml
#	.agents/notes/implemented/bug-fix/2026-07-28-themed-scrollbars-and-reserved-gutter.zh.md
#	.agents/notes/implemented/bug-fix/2026-07-28-web-gui-feedback-loop.i18n.yaml
#	.agents/notes/implemented/bug-fix/2026-07-28-web-gui-feedback-loop.zh.md
#	.agents/notes/implemented/bug-fix/2026-07-29-human-transcript-append-origin.i18n.yaml
#	.agents/notes/implemented/bug-fix/2026-07-29-human-transcript-append-origin.zh.md
#	.agents/notes/implemented/bug-fix/2026-07-29-web-details-session-lifecycle.i18n.yaml
#	.agents/notes/implemented/bug-fix/2026-07-29-web-details-session-lifecycle.zh.md
#	.agents/notes/implemented/bug-fix/2026-07-30-web-transcript-log-ordered-projection.i18n.yaml
#	.agents/notes/implemented/bug-fix/2026-07-30-web-transcript-log-ordered-projection.zh.md
#	.agents/notes/implemented/bug-fix/2026-07-31-fork-anchor-floors-to-event-seq.i18n.yaml
#	.agents/notes/implemented/bug-fix/2026-07-31-fork-anchor-floors-to-event-seq.zh.md
#	.agents/notes/implemented/bug-fix/2026-07-31-web-stop-preserves-queue.i18n.yaml
#	.agents/notes/implemented/bug-fix/2026-07-31-web-stop-preserves-queue.zh.md
#	.agents/notes/implemented/bug-fix/2026-08-02-goal-round-wrapup-message.i18n.yaml
#	.agents/notes/implemented/bug-fix/2026-08-02-goal-round-wrapup-message.zh.md
#	.agents/notes/implemented/bug-fix/2026-08-03-hmr-initial-scan-boot-deadlock.i18n.yaml
#	.agents/notes/implemented/bug-fix/2026-08-03-hmr-initial-scan-boot-deadlock.zh.md
#	.agents/notes/implemented/bug-fix/2026-08-06-onboarding-step-owned-takeover-chrome.i18n.yaml
#	.agents/notes/implemented/bug-fix/2026-08-06-onboarding-step-owned-takeover-chrome.zh.md
#	.agents/notes/implemented/bug-fix/2026-08-06-provider-credential-lifecycle.i18n.yaml
#	.agents/notes/implemented/bug-fix/2026-08-06-provider-credential-lifecycle.zh.md
#	.agents/notes/implemented/bug-fix/2026-08-06-token-surface-unpriced-replace-compatibility.i18n.yaml
#	.agents/notes/implemented/bug-fix/2026-08-06-token-surface-unpriced-replace-compatibility.zh.md
#	.agents/notes/implemented/feature/2026-07-05-skill-system.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-05-skill-system.zh.md
#	.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.zh.md
#	.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.zh.md
#	.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.zh.md
#	.agents/notes/implemented/feature/2026-07-22-durable-subagent-catalog-and-list-agents.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-22-durable-subagent-catalog-and-list-agents.zh.md
#	.agents/notes/implemented/feature/2026-07-23-web-assistant-markdown.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-23-web-assistant-markdown.zh.md
#	.agents/notes/implemented/feature/2026-07-23-web-permission-and-approval.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-23-web-permission-and-approval.zh.md
#	.agents/notes/implemented/feature/2026-07-25-session-list-browsing-and-manual-order.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-25-session-list-browsing-and-manual-order.zh.md
#	.agents/notes/implemented/feature/2026-07-27-skill-catalog-hot-refresh.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-27-skill-catalog-hot-refresh.zh.md
#	.agents/notes/implemented/feature/2026-07-27-web-session-fork-actions.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-27-web-session-fork-actions.zh.md
#	.agents/notes/implemented/feature/2026-07-27-web-session-search.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-27-web-session-search.zh.md
#	.agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-27-web-subagent-conversations.zh.md
#	.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.zh.md
#	.agents/notes/implemented/feature/2026-07-28-skill-invocation-policy.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-28-skill-invocation-policy.zh.md
#	.agents/notes/implemented/feature/2026-07-28-web-terminal-card.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-28-web-terminal-card.zh.md
#	.agents/notes/implemented/feature/2026-07-30-deepseek-onboarding-credential-setup.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-30-deepseek-onboarding-credential-setup.zh.md
#	.agents/notes/implemented/feature/2026-07-30-search-render-card.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-30-search-render-card.zh.md
#	.agents/notes/implemented/feature/2026-07-30-web-diff-card.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-30-web-diff-card.zh.md
#	.agents/notes/implemented/feature/2026-07-30-web-read-card.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-30-web-read-card.zh.md
#	.agents/notes/implemented/feature/2026-07-30-web-result-card-frontend.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-30-web-result-card-frontend.zh.md
#	.agents/notes/implemented/feature/2026-07-30-web-result-card.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-30-web-result-card.zh.md
#	.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.zh.md
#	.agents/notes/implemented/feature/2026-07-31-telemetry-anonymous-user-id.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-31-telemetry-anonymous-user-id.zh.md
#	.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-31-web-workspace-file-links.zh.md
#	.agents/notes/implemented/feature/2026-08-01-pwsh-tool-and-executor.i18n.yaml
#	.agents/notes/implemented/feature/2026-08-01-pwsh-tool-and-executor.zh.md
#	.agents/notes/implemented/feature/2026-08-02-pwsh-tool-bash-parity.i18n.yaml
#	.agents/notes/implemented/feature/2026-08-02-pwsh-tool-bash-parity.zh.md
#	.agents/notes/implemented/feature/2026-08-02-web-thinking-tail-scroll.i18n.yaml
#	.agents/notes/implemented/feature/2026-08-02-web-thinking-tail-scroll.zh.md
#	.agents/notes/implemented/feature/2026-08-05-pwsh-ui-bash-parity.i18n.yaml
#	.agents/notes/implemented/feature/2026-08-05-pwsh-ui-bash-parity.zh.md
#	.agents/notes/implemented/feature/2026-08-06-resolved-theme-color-metadata.i18n.yaml
#	.agents/notes/implemented/feature/2026-08-06-resolved-theme-color-metadata.zh.md
#	.agents/notes/implemented/feature/2026-08-06-web-skill-tool-row.i18n.yaml
#	.agents/notes/implemented/feature/2026-08-06-web-skill-tool-row.zh.md
#	.agents/notes/implemented/process/2026-06-17-ts-build-config.i18n.yaml
#	.agents/notes/implemented/process/2026-06-17-ts-build-config.zh.md
#	.agents/notes/implemented/process/2026-07-22-tsconfig-solution-root-two-aggregates.i18n.yaml
#	.agents/notes/implemented/process/2026-07-22-tsconfig-solution-root-two-aggregates.zh.md
#	.agents/notes/implemented/process/2026-07-31-installer-adopts-existing-checkout.i18n.yaml
#	.agents/notes/implemented/process/2026-07-31-installer-adopts-existing-checkout.zh.md
#	.agents/notes/implemented/process/2026-08-08-api-remotes-generated-contract-build.i18n.yaml
#	.agents/notes/implemented/process/2026-08-08-api-remotes-generated-contract-build.zh.md
#	.agents/notes/implemented/simplification/2026-07-20-remove-stdio-and-echo-agents.i18n.yaml
#	.agents/notes/implemented/simplification/2026-07-20-remove-stdio-and-echo-agents.zh.md
#	.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.i18n.yaml
#	.agents/notes/implemented/simplification/2026-07-26-merge-subagent-control-service.zh.md
#	.agents/notes/implemented/simplification/2026-07-31-one-route-to-add-a-workspace.i18n.yaml
#	.agents/notes/implemented/simplification/2026-07-31-one-route-to-add-a-workspace.zh.md
#	.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.i18n.yaml
#	.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.zh.md
#	.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.i18n.yaml
#	.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.zh.md
#	.agents/notes/proposed/architecture/2026-08-08-semantic-composer-chain-phases.i18n.yaml
#	.agents/notes/proposed/architecture/2026-08-08-semantic-composer-chain-phases.zh.md
#	.agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.i18n.yaml
#	.agents/notes/proposed/feature/2026-07-17-sdk-follow-up-capabilities.zh.md
#	apps/cli/reference/README.i18n.yaml
#	apps/cli/reference/README.zh.md
#	docs/api-gateway.i18n.yaml
#	docs/api-gateway.zh.md
#	docs/cookbook/adding-a-package.i18n.yaml
#	docs/cookbook/adding-a-package.zh.md
#	docs/cookbook/adding-a-tool.i18n.yaml
#	docs/cookbook/adding-a-tool.zh.md
#	docs/development.i18n.yaml
#	docs/development.zh.md
#	docs/i18n/README.i18n.yaml
#	docs/i18n/README.zh.md
#	docs/i18n/style-samples.md
#	docs/i18n/terminology.md
#	docs/testing.i18n.yaml
#	docs/testing.zh.md
#	examples/web-cordis/README.i18n.yaml
#	examples/web-cordis/README.zh.md
#	packages/api/gateway/README.i18n.yaml
#	packages/api/gateway/README.zh.md
#	packages/bash/bash-env/README.i18n.yaml
#	packages/bash/bash-env/README.zh.md
#	packages/bash/tool-bash/README.i18n.yaml
#	packages/bash/tool-bash/README.zh.md
#	packages/bash/tool-pwsh/README.i18n.yaml
#	packages/bash/tool-pwsh/README.zh.md
#	packages/client/ui-conversation/README.i18n.yaml
#	packages/client/ui-conversation/README.zh.md
#	packages/client/ui-tool/README.i18n.yaml
#	packages/client/ui-tool/README.zh.md
#	packages/feedback/command-feedback/README.i18n.yaml
#	packages/feedback/command-feedback/README.zh.md
#	packages/host/webserver/README.i18n.yaml
#	packages/host/webserver/README.zh.md
#	packages/pty/tool-bash-persistent/README.i18n.yaml
#	packages/pty/tool-bash-persistent/README.zh.md
#	packages/subagent/subagent-claude-code/README.i18n.yaml
#	packages/subagent/subagent-claude-code/README.zh.md
#	packages/subagent/subagent-codex/README.i18n.yaml
#	packages/subagent/subagent-codex/README.zh.md
#	packages/typert/type-meta/README.i18n.yaml
#	packages/typert/type-meta/README.zh.md
#	packages/util/atomic-write/README.i18n.yaml
#	packages/util/atomic-write/README.zh.md
#	scripts/snapshots/translation-prompt-v4/request-response.expected.json
This commit is contained in:
xjt
2026-08-09 13:15:27 +08:00
871 changed files with 2178 additions and 2106 deletions
+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 .agents/notes/README.md
README.md: 3cfbb5154713046846a3bfcb2ccea62c0e4cb6c0
README.zh.md: 61887320e9e54b35154c79e33e9a9525aceeef50
README.zh.md: 4b3a1ee57ea61a8e8ba4d01cf7c719bbf8440e30
+7 -7
View File
@@ -2,7 +2,7 @@
[English](README.md) | 中文
这里存放一类设计文档。**Agent Note** 记录塑造本代码库的决策或提案:代码和文档无法承载的*为什么*以及*放弃了什么*。本文件是入口和约:Agent Note 存放在哪里、何时需要写一份,以及[文件内格式](#the-file-format)。
这里存放一类设计文档。**Agent Note** 记录塑造本代码库的决策或提案:代码和文档无法承载的*为什么*以及*放弃了什么*。本文件是入口和约:Agent Note 存放在哪里、何时需要写一份,以及[文件内格式](#the-file-format)。
## 布局与命名
@@ -45,13 +45,13 @@
## 何时需要写一份
每个非平凡变更都必须在同一 PR(Pull Request)中新增或更新至少一份 Agent Note。如果变更修改了行为、架构、跨文件或跨包约、流程或工具、测试策略、磁盘存储格式、协议格式(wire format)或配置格式,或者其他维护者可能合理重新审视的决策,就属于非平凡变更。对未来重大工作的提案从 `proposed/` 开始;已经做出的决策从 `implemented/` 开始。选择与决策匹配的类别文件夹(见[分类](#classification))。
每个非平凡变更都必须在同一 PR(Pull Request)中新增或更新至少一份 Agent Note。如果变更修改了行为、架构、跨文件或跨包约、流程或工具、测试策略、磁盘存储格式、协议格式(wire format)或配置格式,或者其他维护者可能合理重新审视的决策,就属于非平凡变更。对未来重大工作的提案从 `proposed/` 开始;已经做出的决策从 `implemented/` 开始。选择与决策匹配的类别文件夹(见[分类](#classification))。
更新已经拥有该决策的 Agent Note 即可满足规则;不要创建重复记录。只有不涉及行为、约、结构、流程或理由变化的纯机械性或局部编辑才可豁免。Agent Note 永远不会被编辑为一个*不同的决策*:用新 Agent Note 取代旧记录,并让两个记录保持互相链接,除非后续依据下方规则完全合并旧记录。编辑 `implemented/` Agent Note 以跟踪其现有决策的所在位置是必需的,而非禁止的;见 [implemented/AGENTS.md](implemented/AGENTS.md)。
更新已经拥有该决策的 Agent Note 即可满足规则;不要创建重复记录。只有不涉及行为、约、结构、流程或理由变化的纯机械性或局部编辑才可豁免。Agent Note 永远不会被编辑为一个*不同的决策*:用新 Agent Note 取代旧记录,并让两个记录保持互相链接,除非后续依据下方规则完全合并旧记录。编辑 `implemented/` Agent Note 以跟踪其现有决策的所在位置是必需的,而非禁止的;见 [implemented/AGENTS.md](implemented/AGENTS.md)。
被完全取代的 implemented Agent Note 可以合并到当前持有该决策的记录中,并删除原文件。删除前,当前记录必须保存所有独有的决策依据、备选方案、影响、验证约和明确指出的覆盖缺口;修复所有入站链接;并在同一变更中删除中文对侧文件和一致性记录。仅部分被取代的记录不符合此条件:保留两个记录并让它们互相链接,同时更新所有仍然适用的事实。合并不得将旧文件改写成与其相反的决策,也不得让 git 历史成为决策依据的唯一副本。
被完全取代的 implemented Agent Note 可以合并到当前持有该决策的记录中,并删除原文件。删除前,当前记录必须保存所有独有的决策依据、备选方案、影响、验证约和明确指出的覆盖缺口;修复所有入站链接;并在同一变更中删除中文对侧文件和一致性记录。仅部分被取代的记录不符合此条件:保留两个记录并让它们互相链接,同时更新所有仍然适用的事实。合并不得将旧文件改写成与其相反的决策,也不得让 git 历史成为决策依据的唯一副本。
只有当一项功能已从生产代码、配置、schema、持久化格式或协议格式、迁移和兼容行为中完全消失,当前文档不再将其描述为可用,且没有测试把它作为受支持行为来执行时,新增该功能的 Agent Note 才可合并进后续的移除记录。移除决策的依据和验证该功能已不存在的测试可以保留。移除决策的持有记录必须保留最初动机、为什么该动机已不足以证明保留该功能的合理性、完全移除之外的备选方案、放弃的能力、重新引入的条件,以及证明已彻底移除的验证。过时的实现清单和只验证已删除行为的测试不属于当前验证约。仅移除一种传输、默认值、实现或展示属于部分取代;仍有任何持久数据或兼容处理也同样如此。
只有当一项功能已从生产代码、配置、schema、持久化格式或协议格式、迁移和兼容行为中完全消失,当前文档不再将其描述为可用,且没有测试把它作为受支持行为来执行时,新增该功能的 Agent Note 才可合并进后续的移除记录。移除决策的依据和验证该功能已不存在的测试可以保留。移除决策的持有记录必须保留最初动机、为什么该动机已不足以证明保留该功能的合理性、完全移除之外的备选方案、放弃的能力、重新引入的条件,以及证明已彻底移除的验证。过时的实现清单和只验证已删除行为的测试不属于当前验证约。仅移除一种传输、默认值、实现或展示属于部分取代;仍有任何持久数据或兼容处理也同样如此。
<a id="the-file-format"></a>
@@ -79,7 +79,7 @@ Status: <status>
### 正文骨架
每份 Agent Note 的正文以 `## Problem` 开头:动机,写法上不依赖解决方案即可独立成文。后续内容取决于生命周期;固定章节使用以下规范名称且仅限这些名称,而真正独特的技术章节(包拓扑、协议约、schema 等)在必需章节之间可自由组织。
每份 Agent Note 的正文以 `## Problem` 开头:动机,写法上不依赖解决方案即可独立成文。后续内容取决于生命周期;固定章节使用以下规范名称且仅限这些名称,而真正独特的技术章节(包拓扑、协议约、schema 等)在必需章节之间可自由组织。
#### `proposed/`
@@ -126,4 +126,4 @@ Status: <status>
### 中文对侧文件
`.zh.md` 对侧文件按 [i18n ](../../docs/i18n/README.md)逐章节镜像其英文对侧文件的结构;机器检查的头部标记(`# Agent Note: ``Status:` 行)保持英文原样不翻译。格式门禁跳过 `.zh.md` 文件——配对门禁负责它们的一致性。
`.zh.md` 对侧文件按 [i18n 约](../../docs/i18n/README.md)逐章节镜像其英文对侧文件的结构;机器检查的头部标记(`# Agent Note: ``Status:` 行)保持英文原样不翻译。格式门禁跳过 `.zh.md` 文件——配对门禁负责它们的一致性。
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.md
2026-06-11-content-block-vocabulary.md: 5228724bb9101307db9929aaf7831b477c2a6022
2026-06-11-content-block-vocabulary.zh.md: ffcbbc13dfe9176941f4838b0078d3850403a16c
2026-06-11-content-block-vocabulary.zh.md: b43de335917c6b5304a94b296fcb8253827b0600
@@ -12,7 +12,7 @@ harness 需要一套统一的内部消息语言,供 agent loop(智能体循
自主拥有词汇:消息是类型化内容块的数组(`text``reasoning``tool-call``tool-result`),其联合类型派生自可合并扩展的 `ContentBlockMap`,插件通过声明合并添加新的块类型。同一可合并扩展映射模式为所有「字符串化」字段提供类型(`MessageSource``FinishReason``TurnTrigger``TurnEndReason`)。流式输出采用原始分片协议;`BlockAssembler` 是唯一的共享组装实现。适配器负责转换为提供方的协议格式(wire format)——映射成本留在适配器中,正是它该在的地方。
会话内上下文注入(`context/message`)和轮次中途 steering 最初渲染为带标签的 user-role 信封(system-reminder 模式),而非引入新角色,因此适配器无需承担额外负担。如今两者都投影为无包装的普通用户内容;见[注入内容信封 Agent Note](../simplification/2026-07-20-unwrap-injected-content-envelopes.md)。实际适配器验证已确认此渲染方式符合当前 DeepSeek 的行为;如果未来某提供方出现不兼容,应在该适配器内处理,而非引入新的规范角色。
会话内上下文注入(`context/message`)和轮次中途 steering(中途引导)最初渲染为带标签的 user-role 信封(system-reminder 模式),而非引入新角色,因此适配器无需承担额外负担。如今两者都投影为无包装的普通用户内容;见[注入内容信封 Agent Note](../simplification/2026-07-20-unwrap-injected-content-envelopes.md)。实际适配器验证已确认此渲染方式符合当前 DeepSeek 的行为;如果未来某提供方出现不兼容,应在该适配器内处理,而非引入新的规范角色。
## 曾考虑的替代方案
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md
2026-06-11-dev-invariants-over-deep-readonly.md: f1a741927cf63b43b1aaf558efc148ff80d2d881
2026-06-11-dev-invariants-over-deep-readonly.zh.md: 9aadde4f4452a53acce8a3bcf58560ee9722ecfb
2026-06-11-dev-invariants-over-deep-readonly.zh.md: ade3e20adb8f5301adb87130d42c2bd7a37f8a47
@@ -10,7 +10,7 @@ Status: implemented
会话日志是回放、请求重建、持久化与用户可见历史的持久真源。会话包外部的代码必须能检视历史,但不能保留一个可在之后改写历史的引用;从调用方接受的输入也不能继续连接到调用方拥有的可变对象。
单个值的不可变性只是约的一半。一份日志可以包含完全不可变的记录,但其序列、轮次/步骤嵌套、工具调用配对、作用域分发或重建的模型请求是错误的。这些规则涉及多条记录或多个服务,无法通过冻结单个对象来建立。
单个值的不可变性只是约的一半。一份日志可以包含完全不可变的记录,但其序列、轮次/步骤嵌套、工具调用配对、作用域分发或重建的模型请求是错误的。这些规则涉及多条记录或多个服务,无法通过冻结单个对象来建立。
TypeScript readonly 类型不是充分的运行时边界。它们在程序运行时消失,类型转换可以绕过它们,而递归的 `DeepReadonly<T>` 会扩散到每个日志和消息消费方,尽管某些下游请求处理 API 有意使用可变值。
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.md
2026-06-11-event-sourced-sessions.md: 01f9628c1cfc000aca8654caf5edeff09411fdcc
2026-06-11-event-sourced-sessions.zh.md: ec5c3e766dfa97c5612827023c5cc66bf01a8e6c
2026-06-11-event-sourced-sessions.zh.md: 5de230de9e9f1303943b43f6fdeefaf8264aa3bf
@@ -14,7 +14,7 @@ MVP 要求严格的基于事件的追踪,以及完全可回放的会话(严
追加操作是同步的(热路径从不阻塞于 I/O);`session/event` 是同步通知;持久化插件在后台缓冲写入,并在每个轮次结束时触发的 `session/flush` 检查点处等待排空。
顺序约:agent loop(智能体循环)先领取 inbox 消息,再运行 `agent/pre-step`;只有 enter 决策才打开 `step/start`,随后在请求派生前追加返回的 `user/message` 批次。提供方输出组装并以 `assistant/message` 追加后才分派工具,因此持久日志记录工具实际遵循的确切消息。回归测试固定了这一顺序。
顺序约:agent loop(智能体循环)先领取 inbox 消息,再运行 `agent/pre-step`;只有 enter 决策才打开 `step/start`,随后在请求派生前追加返回的 `user/message` 批次。提供方输出组装并以 `assistant/message` 追加后才分派工具,因此持久日志记录工具实际遵循的确切消息。回归测试固定了这一顺序。
## 曾考虑的替代方案
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-11-microkernel-event-taxonomy.md
2026-06-11-microkernel-event-taxonomy.md: 202595fed125966a5d77920536e7f4ee88f875fe
2026-06-11-microkernel-event-taxonomy.zh.md: 899c96d86cb7e37d90df349ce5f3f932e0a72f95
2026-06-11-microkernel-event-taxonomy.zh.md: 1365910157c36699bfe1fdb3a10aa0785948e0fa
@@ -15,7 +15,7 @@ Status: implemented
- **waterfall(瀑布式事件)**around-middleware):插件可变换、否决、恢复或包装:`agent/pre-step``agent/request``agent/request-error``tools/pre-execute``tools/execute``tools/post-execute``llm/stream``system-prompt/assemble`
- **serial**(按监听器顺序依次 await):用于 `agent/turn-stopping` 等有序检查点。
- **parallel**await 扇出):每个监听器都必须获得独立执行的机会:`session/flush` 持久性检查点。
- **emit**(同步 fire-and-forget):用于 inbox 转换、生命周期、错误,以及包含不可变 `tools/result` 观测的事件。轮次与步骤边界由持久会话事件拥有。
- **emit**(同步 fire-and-forget):用于通知:inbox 转换、生命周期、错误,以及受错误隔离的 `tools/result` 观测;该观测接收不可变的最终结果。轮次与步骤边界由持久会话事件拥有。
事件词汇定义在接口包中(dsh-agent 声明 agent/* 事件);`@deepseek-ai/dsh-agent-loop` 是唯一的具体循环插件,且自身可替换——外部不得依赖它。
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-13-capability-seams.md
2026-06-13-capability-seams.md: 7c755dced7825d2831acc0901f6412b8e5afe95a
2026-06-13-capability-seams.zh.md: 3d0fe2b842766838d247aca75972509bf6c7b951
2026-06-13-capability-seams.zh.md: 31d20bb15f91533ab3c09d337a3cb11f812dbeb4
@@ -6,7 +6,7 @@ Status: implemented
## 问题
harness 具有可替换的能力:当前是 bash 执行,未来会有沙箱化/远程执行器和替代模型提供方。一项能力涉及三个关注点,它们以不同速率、因不同原因变化:*约*(这项能力是什么)、*实现*(它如何运行)、*消费方接口*(模型和其他插件面向什么编程)。将三者捆绑在一个包中会耦合这些变化速率——把本地执行器换成沙箱化执行器时,模型看到的工具 schema 也会被搅动,尽管面向模型的约从未改变。
harness 具有可替换的能力:当前是 bash 执行,未来会有沙箱化/远程执行器和替代模型提供方。一项能力涉及三个关注点,它们以不同速率、因不同原因变化:*约*(这项能力是什么)、*实现*(它如何运行)、*消费方接口*(模型和其他插件面向什么编程)。将三者捆绑在一个包中会耦合这些变化速率——把本地执行器换成沙箱化执行器时,模型看到的工具 schema 也会被搅动,尽管面向模型的约从未改变。
这与「谁在运行时提供、谁需要一项能力」是不同的问题,后者 Cordis 已通过服务 + `inject` 解决(提供方注册 `ctx.bash`;消费方声明 `inject: ['bash']`,其 fiber 挂起直到服务存在)。该机制是必要的,但不决定包的边界;本 Agent Note 决定的是包的边界。
@@ -29,4 +29,4 @@ harness 具有可替换的能力:当前是 bash 执行,未来会有沙箱化
## 后果
每项能力需要更多包和更多样板代码(一组 `package.json`/`tsconfig`/README,加上 inject 接线)。换来的是:实现与消费方独立发布和版本管理,新后端永远不会波及面向模型的约。该规则记录在 [AGENTS.md](../../../../AGENTS.md) § Conventions(「Capability seams are three packages」)和 [architecture.md](../../../../docs/architecture.md) §「Capability seams」中;bash 三件套是参考模板。何时合并、何时拆分是一个判断问题,架构文档对此有详细说明——本 Agent Note 记录的是*为什么*默认选择拆分。
每项能力需要更多包和更多样板代码(一组 `package.json`/`tsconfig`/README,加上 inject 接线)。换来的是:实现与消费方独立发布和版本管理,新后端永远不会波及面向模型的约。该规则记录在 [AGENTS.md](../../../../AGENTS.md) § Conventions(「Capability seams are three packages」)和 [architecture.md](../../../../docs/architecture.md) §「Capability seams」中;bash 三件套是参考模板。何时合并、何时拆分是一个判断问题,架构文档对此有详细说明——本 Agent Note 记录的是*为什么*默认选择拆分。
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md
2026-06-13-twin-llm-adapters.md: a4c87325a0b0d1ebe6cf8f95672e5de74ef37d57
2026-06-13-twin-llm-adapters.zh.md: 753d7900f23c0d3388be0488c291145fdccf5a95
2026-06-13-twin-llm-adapters.zh.md: 2af7e7cca4d4c40e18cece1a4dd5c9cdb11901c8
@@ -6,11 +6,11 @@ Status: implemented
## 问题
`dsh-llm` 拥有一套提供方无关的流式词汇:`StreamChunk` 协议(`block-start``text-delta``reasoning-delta``tool-call-delta``block-end``usage``finish`)以及内容块类型([内容块词汇](2026-06-11-content-block-vocabulary.md))。如果词汇仅针对单个适配器定义,就有可能将该适配器的特异行为固化到「中立」约中:唯一实现碰巧做了什么,什么就成为事实上的规范;在第二个提供方到来之前,抽象层未经验证——而届时修复这种泄漏的代价已经很高。
`dsh-llm` 拥有一套提供方无关的流式词汇:`StreamChunk` 协议(`block-start``text-delta``reasoning-delta``tool-call-delta``block-end``usage``finish`)以及内容块类型([内容块词汇](2026-06-11-content-block-vocabulary.md))。如果词汇仅针对单个适配器定义,就有可能将该适配器的特异行为固化到「中立」约中:唯一实现碰巧做了什么,什么就成为事实上的规范;在第二个提供方到来之前,抽象层未经验证——而届时修复这种泄漏的代价已经很高。
## 决策
从一开始就针对同一份约交付**两个**适配器,刻意基于不同的内部实现构建:
从一开始就针对同一份约交付**两个**适配器,刻意基于不同的内部实现构建:
- `dsh-llm-deepseek`:直接 `fetch` + 仓库内翻译逻辑对接 DeepSeek APISSEServer-Sent Events)分帧委托给 `eventsource-parser`[已归档的 SSE 解析器替换](../../archived/simplification/2026-07-26-eventsource-parser-for-deepseek-sse.md))。孪生身份在于自行持有 fetch/translate 内部实现而非委托给完整的提供方 SDK,不在于手写传输层管道。
- `dsh-llm-pi-ai`:通过 `@earendil-works/pi-ai` 库访问同一端点(该库有自己的事件词汇)。
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-14-session-persistence.md
2026-06-14-session-persistence.md: 00e129e57c7144fd62eec26f5854ee21dec4e964
2026-06-14-session-persistence.zh.md: 1c98d5771819e8776d9f5cae4a147d2016ee5978
2026-06-14-session-persistence.zh.md: df03dce6bd978f10deb64b8d75ae0685dabab7cd
@@ -19,15 +19,15 @@ Status: implemented
以下关键选择记录于此,因为它们长期有效、存在争议且出人意料:
- **规范的持久日志无损保留每个 `SessionEvent`,包括 `assistant/chunk`。** JSONL 存储可以将一段连续的增量事件编码为一条打包行,但逻辑读取方会重建精确的事件边界、序号与时间戳。`deriveMessages()` 跳过分片,而过滤分片的方案(Codex 的 `policy.rs`)很有吸引力,但 `seq = log.length` 以及 `events[i].seq === i` 验证要求*连续*的逻辑日志;过滤掉分片会留下空洞,同时破坏约和恢复功能。基于分片过滤的投影可以作为派生视图在后续实现(带有自己的重新编号),但它不是规范日志。
- **仅追加;崩溃的轮次被关闭,而非截断。** 已刷写的事件永不被重写。[语义检查点策略](../bug-fix/2026-07-21-semantic-session-checkpoints.md)会在模型分发前排空请求、在工具分发前排空已记录的顶层调用,并在步骤结束后排空完整的响应/结果批次;循环则排空最终轮次边界。由于一个被中断的轮次可能包含大量有效工作,冷检查会保留其连续、可解析的事件,并在内存逻辑视图中为未应答的 assistant 调用添加按风险分类的错误结果、补一个缺失的 `step/end`,以及带 `{ kind: 'interrupted' }``turn/end``prepare``load` 在返回可恢复视图前提交这些 closer;合成结果保证恢复后的提供方 transcript(文本记录)仍然有效。只有不完整的最后一条记录会在提交修复时被丢弃;在最后一个真实 `turn/end` 处或之前出现解析错误或序号间隙,属于数据损坏,会使该会话不可加载。
- **文件后端为规范实现,数据库后端为经过验证的直接替换。** `SessionEvent` 1:1 映射到一行 `(session_id, seq, type, time, data)``append` 是 INSERT(在一个断言连续 seq 约的事务中),读取使用 SELECT … ORDER BY seq。`dsh-session-persistence-sqlite` 正是如此:一个 `SessionPersistence` 子类,接口无变化(opencode 在 SQLite/WAL 上运行的正是这个形状),且通过与 JSONL 后端相同的 `runPersistenceContract` 测试套件。该约以相同的语义约束两个后端(惰性物化、逻辑关闭中断轮次、修复只提交一次、连续 seq),一次表达在文件字节上,一次表达在数据库行上。其数据库拥有专用的 application id 与单调递增的 schema 版本。系统会在一个事务中为全新文件创建所有表并写入这两个 header 值;未版本化文件若带有任何用户定义的 schema 对象或应用标识、当前版本文件若带有外部应用标识,以及任何非当前版本文件,都会在修改日志模式之前被拒绝。
- **规范的持久日志无损保留每个 `SessionEvent`,包括 `assistant/chunk`。** JSONL 存储可以将一段连续的增量事件编码为一条打包行,但逻辑读取方会重建精确的事件边界、序号与时间戳。`deriveMessages()` 跳过分片,而过滤分片的方案(Codex 的 `policy.rs`)很有吸引力,但 `seq = log.length` 以及 `events[i].seq === i` 验证要求*连续*的逻辑日志;过滤掉分片会留下空洞,同时破坏约和恢复功能。基于分片过滤的投影可以作为派生视图在后续实现(带有自己的重新编号),但它不是规范日志。
- **仅追加;崩溃的轮次被关闭,而非截断。** 已刷写的事件永不被重写。[语义检查点策略](../bug-fix/2026-07-21-semantic-session-checkpoints.md)会在模型分发前排空请求、在工具分发前排空已记录的顶层调用,并在步骤结束后排空完整的响应/结果批次;循环则排空最终轮次边界。由于一个被中断的轮次可能包含大量有效工作,冷检查会保留其连续、可解析的事件,并在内存逻辑视图中为未应答的 assistant 调用添加按风险分类的错误结果、补一个缺失的 `step/end`,以及带 `{ kind: 'interrupted' }``turn/end``prepare``load` 在返回可恢复视图前提交这些收尾事件;合成结果保证恢复后的提供方 transcript(文本记录)仍然有效。只有不完整的最后一条记录会在提交修复时被丢弃;在最后一个真实 `turn/end` 处或之前出现解析错误或序号间隙,属于数据损坏,会使该会话不可加载。
- **文件后端为规范实现,数据库后端为经过验证的直接替换。** `SessionEvent` 1:1 映射到一行 `(session_id, seq, type, time, data)``append` 是 INSERT(在一个断言连续 seq 约定成立的事务中),读取使用 SELECT … ORDER BY seq。`dsh-session-persistence-sqlite` 正是如此:一个 `SessionPersistence` 子类,接口无变化(opencode 在 SQLite/WAL 上运行的正是这个形状),且通过与 JSONL 后端相同的 `runPersistenceContract` 测试套件。该约以相同的语义约束两个后端(惰性物化、逻辑关闭中断轮次、修复只提交一次、连续 seq),一次表达在文件字节上,一次表达在数据库行上。其数据库拥有专用的 application id 与单调递增的 schema 版本。系统会在一个事务中为全新文件创建所有表并写入这两个 header 值;未版本化文件若带有任何用户定义的 schema 对象或应用标识、当前版本文件若带有外部应用标识,以及任何非当前版本文件,都会在修改日志模式之前被拒绝。
- **元数据在日志之外。** 格式版本、cwd 和谱系是存储关注点,不是可回放的对话状态,因此它们存放在 `dsh-session` 拥有的 `SessionHeader` 中,并通过新的只读属性 `session.header` 附加到 `Session` 上——永远不进入 `SessionEventMap`,永远不到达 `deriveMessages()``createdAt` 是以 Unix epoch 毫秒表示的非负安全整数:运行时创建和持久化注册会拒绝小数值,JSONL 会验证解码后的 headerSQLite 则将其存入严格的 `INTEGER` 列。替代方案(一个可合并扩展的 `session/meta` 事件作为日志第 0 行)被否决:日志内事件会自然随 seed/fork 的会话携带,但元数据不是可回放状态,因此显式的日志外 header seam 是更清晰的取舍。(header 最初被拆分为不可变的 `SessionHeader` 加可变的 `SessionSummary`,二者的联合类型为 `SessionMeta`;可变 summary 后来因属于死状态而被移除——见 [移除可变会话摘要](../simplification/2026-06-19-drop-mutable-session-summary.md)。)
- **`ctx.agents.create()``ctx.agents.resume()` 是异步工厂;恢复还跨越持久化边界。** `ctx.agents.resume({ resumeSessionId })` 通过 `ctx.sessionPersistence.prepare()` 取得精确的未发布 Session,以持久化 id 发布,并续其投影。[Session 准备阶段决策](2026-08-05-session-preparation.md)定义历史检查与恢复之间的复用。agent loop(智能体循环)不会硬注入 `sessionPersistence`(那样会让非持久化的演示永远挂起);当它不存在时,`resume` 会以明确的错误拒绝。
- **`ctx.agents.create()``ctx.agents.resume()` 是异步工厂;恢复还跨越持久化边界。** `ctx.agents.resume({ resumeSessionId })` 通过 `ctx.sessionPersistence.prepare()` 取得原本那个尚未发布的会话,以持久化 id 发布该会话,并续其投影。[会话准备决策](2026-08-05-session-preparation.md)负责历史检查与恢复之间的复用。agent loop(智能体循环)不会硬注入 `sessionPersistence`(那样会让非持久化的演示永远挂起);当它不存在时,`resume` 会以明确的错误拒绝。
## 曾考虑的替代方案
上述每个关键选择都在陈述处记录了被否决的替代方案:**过滤分片的规范日志**(Codex 的 `policy.rs` 形式)破坏连续 seq 约;**截断崩溃的轮次**会静默销毁长时间自主运行中的真实工作;**日志内 `session/meta` 事件作为第 0 行**——元数据不是可回放状态;**有限的非整数 `createdAt` 值**没有生产方,且与整数 Unix 毫秒存储及查询列不一致;**接受非全新的未版本化 SQLite 文件**可能覆盖无关对象或应用标识;**将 `sessionPersistence` 硬注入循环**会让非持久化的演示永远挂起。
上述每个关键选择都在陈述处记录了被否决的替代方案:**过滤分片的规范日志**(Codex 的 `policy.rs` 形式)破坏连续 seq 约;**截断崩溃的轮次**会静默销毁长时间自主运行中的真实工作;**日志内 `session/meta` 事件作为第 0 行**——元数据不是可回放状态;**有限的非整数 `createdAt` 值**没有生产方,且与整数 Unix 毫秒存储及查询列不一致;**接受非全新的未版本化 SQLite 文件**可能覆盖无关对象或应用标识;**将 `sessionPersistence` 硬注入循环**会让非持久化的演示永远挂起。
格式版本控制:header 携带一个 `version`;冷读取拒绝任何非当前版本。预发布阶段的会话格式仍固定为 `SESSION_FORMAT_VERSION = 0`,不承诺广泛兼容;当持久化用户数据确有需要时,协调器可以负责显式且范围受限的导入升级([消息标识机制引入前的消息恢复](../bug-fix/2026-07-28-load-pre-identity-session-messages.md))。坦率地说:仅追加 + 刷写对部分尾部写入是健壮的(冷准备时容忍),但对行写入中途的无 fsync 断电不健壮;数据库/WAL 后端是后续更强的选项。
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.md
2026-06-17-filesystem-capability-seam.md: 7436d2a9402c76f17c7d6571eb489a86e550ebfb
2026-06-17-filesystem-capability-seam.zh.md: fdeafba387b562352b377321327c526a67669ef6
2026-06-17-filesystem-capability-seam.zh.md: a96d513d7f4fa793ebb3427e3f7b290862d0d5e9
@@ -10,11 +10,11 @@ harness 已有一个具体的 `bash` 能力 seam`dsh-bash` / `dsh-bash-local`
这把三个独立变化的关注点耦合在了一起:
1. 文件系统约:插件可以请求哪些操作。
1. 文件系统约:插件可以请求哪些操作。
2. 后端:当前是本地磁盘,未来可能是沙箱/远程/项目作用域的文件系统。
3. 消费方接口:面向模型的 `read` / `write` / `edit` schema 与结果格式化。
如果没有 `ctx.fs` 接口,将本地文件系统访问替换为沙箱或远程后端时,即使面向模型的约应当保持稳定,工具 schema、演示和提示词引导也会被迫变动。这还使权限/沙箱边界更难推理:一个 `cwd` 选项看起来像沙箱,但除非有显式的后端或 `tools/execute` 策略强制隔离,否则它只是一个基础路径。
如果没有 `ctx.fs` 接口,将本地文件系统访问替换为沙箱或远程后端时,即使面向模型的约应当保持稳定,工具 schema、演示和提示词引导也会被迫变动。这还使权限/沙箱边界更难推理:一个 `cwd` 选项看起来像沙箱,但除非有显式的后端或 `tools/execute` 策略强制隔离,否则它只是一个基础路径。
我们需要文件系统工具在成为公开包(package)接口之前,以与 bash 相同的能力 seam 形态落地。
@@ -55,7 +55,7 @@ harness 已有一个具体的 `bash` 能力 seam`dsh-bash` / `dsh-bash-local`
`tool-fs` 插件通过组合各工具的注册辅助函数来注册完整的文件系统工具套件(`read``write``edit`)。它注入 `fs`,从不导入实现包。
## `ctx.fs`
## `ctx.fs` 约
`@deepseek-ai/dsh-fs` 拥有一个语义文件系统服务。它比 `readFile` / `writeFile` 更高层,这样 `tool-fs` 就不必重新实现路径解析、版本管理、文本解码、二进制拒绝、分页、原子替换、符号链接行为或字面编辑语义。
@@ -98,7 +98,7 @@ harness 已有一个具体的 `bash` 能力 seam`dsh-bash` / `dsh-bash-local`
策略插件(而非 `ctx.fs`)对先前观测进行门控:`edit` 要求 owner 有先前观测(否则报 `FS_NOT_OBSERVED`),记录的版本作为 CAS 基础传给 `editText`。在策略插件缺席时,`ctx.fs` 本身是一个完整的无约束 seam(无条件写入/编辑);工具从不与策略方法耦合。
文件系统约失败以 `FsError extends HarnessError` 抛出,工具注册表将其转换为带结构化 `{ name, code }` 元数据的 `isError` 工具结果。`dsh-fs` 拥有此词汇,而非由每个工具各自发明消息。错误码包括 `FS_NOT_FOUND``FS_NOT_TEXT``FS_STALE_VERSION``FS_NOT_OBSERVED``FS_NOT_REGULAR_FILE``FS_AMBIGUOUS_EDIT``FS_EDIT_NOT_FOUND``FS_ABORTED`。(早期草案包含 `FS_PARTIAL_OBSERVATION`;基于新鲜度的授权没有 partial/full 区分,因此已删除。目录列表相关的错误码后来由[为文件系统 seam 添加直接目录列举能力](../../archived/architecture/2026-07-03-filesystem-directory-listing-seam.md)添加。)
文件系统约失败以 `FsError extends HarnessError` 抛出,工具注册表将其转换为带结构化 `{ name, code }` 元数据的 `isError` 工具结果。`dsh-fs` 拥有此词汇,而非由每个工具各自发明消息。错误码包括 `FS_NOT_FOUND``FS_NOT_TEXT``FS_STALE_VERSION``FS_NOT_OBSERVED``FS_NOT_REGULAR_FILE``FS_AMBIGUOUS_EDIT``FS_EDIT_NOT_FOUND``FS_ABORTED`。(早期草案包含 `FS_PARTIAL_OBSERVATION`;基于新鲜度的授权没有 partial/full 区分,因此已删除。目录列表相关的错误码后来由[为文件系统 seam 添加直接目录列举能力](../../archived/architecture/2026-07-03-filesystem-directory-listing-seam.md)添加。)
## 工具消费方行为
@@ -119,7 +119,7 @@ harness 已有一个具体的 `bash` 能力 seam`dsh-bash` / `dsh-bash-local`
该包通过 `ctx.systemPrompt.section(...)` 注册提示词引导,通过 `ctx.tools.register(...)` 注册 schema。工具 schema 仍通过 `SystemPrompt.assemble()``ToolRegistry.schemas()` 流入正常的提示词组装路径;无需改动 agent loop(智能体循环)。
工具包在后端变化时保持面向模型的约稳定:本地后端和远程后端内部可能以不同方式解析路径,但 `read` / `write` / `edit` schema 不会仅因后端变化而改变。
工具包在后端变化时保持面向模型的约稳定:本地后端和远程后端内部可能以不同方式解析路径,但 `read` / `write` / `edit` schema 不会仅因后端变化而改变。
默认部署要求在用 `write``edit` 更新已有文件之前先 `read``tool-fs` 不通过检查是否运行过名为 `read` 的工具来实现这一点:它分发 `fs/write-intent`/`fs/edit-intent` 事件(将执行上下文作为不透明 actor 传递),`dsh-fs-policy` 插件推导 owner、对先前观测进行门控并提供版本期望。任何窗口化读取都能授权后续的写入/编辑,只要文件未变。用 `write` 创建新文件不要求先前观测。
@@ -144,9 +144,9 @@ harness 已有一个具体的 `bash` 能力 seam`dsh-bash` / `dsh-bash-local`
## 后果
**`cwd` 可能被误认为沙箱。** 本地后端的基目录是解析默认值,而非自动的隔离边界。如果需要隔离,必须由后端约或 `tools/execute` 上的权限/沙箱插件强制执行。
**`cwd` 可能被误认为沙箱。** 本地后端的基目录是解析默认值,而非自动的隔离边界。如果需要隔离,必须由后端约`tools/execute` 上的权限/沙箱插件强制执行。
**接口可能变得过于本地化。** 如果 `ctx.fs` 返回 `absolutePath` 之类的字段,远程、沙箱或虚拟后端会变得尴尬。约应暴露显示元数据,而不要求消费方理解宿主路径。
**接口可能变得过于本地化。** 如果 `ctx.fs` 返回 `absolutePath` 之类的字段,远程、沙箱或虚拟后端会变得尴尬。约应暴露显示元数据,而不要求消费方理解宿主路径。
**接口可能变得过于薄。** 如果 `ctx.fs` 只镜像 `node:fs` 原语,`tool-fs` 将重新实现二进制检测、分页、原子写入和编辑语义,重新制造本 Agent Note 试图避免的耦合。
@@ -154,7 +154,7 @@ harness 已有一个具体的 `bash` 能力 seam`dsh-bash` / `dsh-bash-local`
**观测状态不属于 `ctx.fs`。** 记录执行上下文看到了什么是工作流策略,而非原始文件系统 I/O。本 Agent Note 最初将其放在文件系统 seam 内部;拆分文件系统 seam Agent Note 随后确立了沙箱/远程后端不应继承面向模型的观测策略,并将其移入 `dsh-fs-policy` 插件。提供方 seam 只保留写入/编辑安全在存储层真正需要的东西——后端铸造的版本令牌和可选的版本守护变更——而策略插件拥有 owner 推导、观测状态和基于 `fs/*` 事件的读后编辑门控。
**`resolve` 然后操作的形态每次调用多一次往返。** 每个工具可能先将路径解析为 `FsTarget`,再以单独的 `ctx.fs` 调用发起读取/写入/编辑。对本地后端来说这可以忽略(解析是内存中的路径规范化),但远程/沙箱后端可能将每步变成独立请求,使单次 `read` 变为两次网络往返。往返开销重要的后端可以在内部缓存或折叠解析,同时保持可观测约不变。
**`resolve` 然后操作的形态每次调用多一次往返。** 每个工具可能先将路径解析为 `FsTarget`,再以单独的 `ctx.fs` 调用发起读取/写入/编辑。对本地后端来说这可以忽略(解析是内存中的路径规范化),但远程/沙箱后端可能将每步变成独立请求,使单次 `read` 变为两次网络往返。往返开销重要的后端可以在内部缓存或折叠解析,同时保持可观测约不变。
**观测状态持久化被推迟。** 观测状态存在于内存中(`dsh-fs-policy` 内部的 `WeakMap`),因此恢复的会话保守地要求文件在写入/编辑前重新读取,直到未来的会话事件或持久化机制使观测可回放。
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-seams.md
2026-06-18-agent-lifecycle-and-ownership-seams.md: 93247a6da7446a5a67db33423d2b766ce4cf3308
2026-06-18-agent-lifecycle-and-ownership-seams.zh.md: 7c0f27e1e6ddeec91a8031ef7b1d9fd965a4463a
2026-06-18-agent-lifecycle-and-ownership-seams.zh.md: e17650c7789764dfb4240132f5903795e6f86381
@@ -14,7 +14,7 @@ ACPAgent Client Protocol)与 tool-bash 的若干限制是同一个缺失 se
### 1. 队列感知的 `Agent.cancel(cause?)`
`Agent` 接口新增 `cancel()` 动词——唯一的公开停止原语。(它最初与范围更窄、仅作用于步骤的 `abort()` 一同交付;后者后来因无人使用而移除,使 `cancel()` 成为唯一公开的停止工作方式。)它清空 inbox 的 queued + steering FIFO,在存在活跃轮次时中止它,并保留一个不带 cause 的 pre-run 标记,使在被领取前被取消的提示词永不运行,而后来的提示词仍保持独立。有效调用会在清空或中止前发出 `agent/cancel-requested`,携带类型化的 `user | parent` cause;空闲取消不发出任何事件,也不会使下一条提示词搁浅。`whenIdle()` 会在取消后达到完全停稳,ACP 的 `session/cancel` 映射到 `user`。[显式轮次取消决策](2026-07-16-explicit-turn-cancellation.md)规定了当前的 cause、signal 生命周期与协作式结算约。
`Agent` 接口新增 `cancel()` 动词——唯一的公开停止原语。(它最初与范围更窄、仅作用于步骤的 `abort()` 一同交付;后者后来因无人使用而移除,使 `cancel()` 成为唯一公开的停止工作方式。)它清空 inbox 的 queued + steering FIFO,在存在活跃轮次时中止它,并保留一个不带 cause 的 pre-run 标记,使在被领取前被取消的提示词永不运行,而后来的提示词仍保持独立。有效调用会在清空或中止前发出 `agent/cancel-requested`,携带类型化的 `user | parent` cause;空闲取消不发出任何事件,也不会使下一条提示词搁浅。`whenIdle()` 会在取消后达到完全停稳,ACP 的 `session/cancel` 映射到 `user`。[显式轮次取消决策](2026-07-16-explicit-turn-cancellation.md)规定了当前的 cause、signal 生命周期与协作式结算约
### 2. `AgentHandle` 异步释放器
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-18-session-surface.md
2026-06-18-session-surface.md: 1bb3baac5e9bea3f657cbd3be2623223bda78ce2
2026-06-18-session-surface.zh.md: bfbfd097ffeca59dec163dff911315d0ed94d028
2026-06-18-session-surface.zh.md: b967ad467e58ed4f4b4009fe4746f1b8b6b22f25
@@ -33,7 +33,7 @@ export type SurfaceOp =
### SurfaceManager:基于增量,而非全量重建
一个 `Session` 拥有一个 `SurfaceManager`,后者维护事件 seq 的有序 `number[]`。管理器会在提交前校验每个种子或追加候选项而不应用它,然后只处理上次同步之后已经提交的事件,而不重新扫描整个日志。`Session.surface` 通过只读的 `SessionSurface` 约暴露同一个管理器,因此接纳、派生历史、压缩与工作区上下文共享同一份增量状态。Replace 按数组位置定位两个端点(均包含在范围内),并把替换 seq splice 到该范围;不会用第二个管理器、链接对象或 seq 到节点的 map 来重复表达顺序。
一个 `Session` 拥有一个 `SurfaceManager`,后者维护事件 seq 的有序 `number[]`。管理器会在提交前校验每个种子或追加候选项而不应用它,然后只处理上次同步之后已经提交的事件,而不重新扫描整个日志。`Session.surface` 通过只读的 `SessionSurface`暴露同一个管理器,因此接纳、派生历史、压缩与工作区上下文共享同一份增量状态。Replace 按数组位置定位两个端点(均包含在范围内),并把替换 seq splice 到该范围;不会用第二个管理器、链接对象或 seq 到节点的 map 来重复表达顺序。
无新事件时增量处理为 O(1),有新事件到达时为 O(新事件数)。
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md
2026-06-18-shared-persistence-write-coordinator.md: 12131ebf8380fb8ba816618f0cbaf72cb004623a
2026-06-18-shared-persistence-write-coordinator.zh.md: a11926b602651d8a7dc641f61371e882685b508b
2026-06-18-shared-persistence-write-coordinator.zh.md: 0a7fbf351171d305b3b6245832a0b35a283c7c1d
@@ -6,13 +6,13 @@ Status: implemented
## 问题
`dsh-session-persistence-jsonl``dsh-session-persistence-sqlite` 有意在不同存储介质上证明同一份 `SessionPersistence` 约,但它们重复实现了写入路径编排:每会话状态、`session/created` 接管、后端特定的前缀读取、write-behind 控制、按 id 串行执行操作、HMR(热模块替换)种子注入与 dispose(资源释放)排空。纯粹的种子前缀碰撞检查与可序列化守卫已迁入 seam 包;剩余的编排仍然对正确性要求很高,且同样的修复被应用了两次。唯一的差异在于存储原语(写字节 vs. INSERT 行)。
`dsh-session-persistence-jsonl``dsh-session-persistence-sqlite` 有意在不同存储介质上证明同一份 `SessionPersistence`,但它们重复实现了写入路径编排:每会话状态、`session/created` 接管、后端特定的前缀读取、write-behind 控制、按 id 串行执行操作、HMR(热模块替换)种子注入与 dispose(资源释放)排空。纯粹的种子前缀碰撞检查与可序列化守卫已迁入 seam 包;剩余的编排仍然对正确性要求很高,且同样的修复被应用了两次。唯一的差异在于存储原语(写字节 vs. INSERT 行)。
## 决策
将一个后端无关的 `PersistenceCoordinator` 提取到 `dsh-session-persistence` 中。协调器统一拥有编排逻辑;每个第一方后端组合一个协调器实例(`new PersistenceCoordinator(ctx, this)`),实现一个小型 `PersistenceBackend` 钩子接口,并将其有状态的公开方法(`create`/`append`/`prepare`/`load`/`inspect`/`readFrom`)委托给协调器。由后端拥有的元数据与修订版本列举会绕过协调器。
组合,而非继承。协调器是后端持有的具体类,不是后端继承的基类。本 Agent Note 的风险——「协调器不得让非常规后端与继承层级作斗争」——由此规避:后端只暴露钩子,无法触及协调器的私有编排状态。第三方后端仍然可以完全不使用协调器、直接实现抽象服务,包括不可变逻辑检查,以及通过 `load` 实现的默认准备回退。
组合,而非继承。协调器是后端持有的具体类,不是后端继承的基类。本 Agent Note 的风险——「协调器不得让非常规后端与继承层级作斗争」——由此规避:后端只暴露钩子,无法触及协调器的私有编排状态。第三方后端仍然可以完全不使用协调器、直接实现抽象服务,包括不修改状态的逻辑检查,以及通过 `load` 实现的默认准备回退。
协调器为每个存活的 `Session` 实例持有一个生命周期条目:初始化,加上一个包私有写入控制器,后者负责待处理事件、固定批处理截止时间、活跃写入、失败保留和共享 flush 屏障。每个 `session/event` 都进入这条有界写入路径,`session/flush` 则绕过等待以观察完全停稳。控制器归并由 [flush 控制器简化](../simplification/2026-07-23-collapse-persistence-flush-state.md)定义;调度节奏由[有界批处理决策](2026-08-08-bounded-session-persistence-write-batching.md)定义。
@@ -35,7 +35,7 @@ Status: implemented
## 测试
共享的 `runPersistenceContract`(公开 API 约)为每个后端运行,并证明 `inspect` 会配平被中断的逻辑视图但不改变存储或修订版本,随后由 `prepare``load` 提交恢复。`runCoordinatorContract``tests/coordinator-contract.ts`)通过内存参考实现、JSONL 与 SQLite 覆盖接管、HMR、碰撞、会话与后端 dispose 排空和崩溃尾部修复。`persistence.spec.ts``preparations.spec.ts``write-behind.spec.ts` 覆盖准备复用与预留、有界准备状态淘汰、固定窗口后续批次、存活控制器清理、同 id 链尾竞态、失败批次重试与关闭顺序。各后端自身的测试规格只保留存储机制。每个真实后端都有一个经由协调器的崩溃尾部修复测试,以覆盖不透明 marker 分支,因为约中的崩溃用例会产生合成 closers,却不会产生 torn marker。
共享的 `runPersistenceContract`(公开 API 约)为每个后端运行,并证明 `inspect` 会配平被中断的逻辑视图但不改变存储或修订版本,随后由 `prepare``load` 提交恢复。`runCoordinatorContract``tests/coordinator-contract.ts`)通过内存参考实现、JSONL 与 SQLite 覆盖接管、HMR、碰撞、会话与后端 dispose 排空和崩溃尾部修复。`persistence.spec.ts``preparations.spec.ts``write-behind.spec.ts` 覆盖准备复用与预留、有界准备状态淘汰、固定窗口后续批次、存活控制器清理、同 id 链尾竞态、失败批次重试与关闭顺序。各后端自身的测试规格只保留存储机制。每个真实后端都有一个经由协调器的崩溃尾部修复测试,以覆盖不透明 marker 分支,因为约中的崩溃用例会产生合成 closers,却不会产生 torn marker。
## 曾考虑的替代方案
@@ -44,4 +44,4 @@ Status: implemented
## 后果
协调器增加了一层间接、一个不透明的 torn marker、脱离会话生命周期的退役任务,以及有界的已准备 Session 状态,但将此前每个后端重复的、对正确性要求很高的编排逻辑集中到一处。会话 dispose 仍是仅观察事件,因此会话所有者不会等待持久化退役;协调器会收容失败、在存活控制器中保留待处理事件,并以后端 teardown 为完全停稳边界。其钩子面保持窄小:标识校验、接管、碰撞检查、准备与不可变检查共用 `loadStored`;物化保持在 `appendBatch` 内原子完成;列举绕过协调器。读模型使用 `inspect` 而非 `load`,因此观察已持久化但仍开放的轮次时不会提交中断 closers;复用、预留与发布由 [Session 准备阶段决策](2026-08-05-session-preparation.md)定义。新后端只需实现存储原语,而无需复制有界写入生命周期。
协调器增加了一层间接、一个不透明的 torn marker、脱离会话生命周期的退役任务,以及有界的已准备 Session 状态,但将此前每个后端重复的、对正确性要求很高的编排逻辑集中到一处。会话 dispose 仍是仅观察事件,因此会话所有者不会等待持久化退役;协调器会收容失败、在存活控制器中保留待处理事件,并以后端 teardown 为完全停稳边界。其钩子面保持窄小:标识校验、接管、碰撞检查、准备与不修改状态的检查共用 `loadStored`;物化保持在 `appendBatch` 内原子完成;列举绕过协调器。读模型使用 `inspect` 而非 `load`,因此观察已持久化但仍开放的轮次时不会提交中断 closers;复用、预留与发布由 [Session 准备阶段决策](2026-08-05-session-preparation.md)定义。新后端只需实现存储原语,而无需复制有界写入生命周期。
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md
2026-06-20-generic-long-running-tool-runtime.md: 8cf6a34c3e4fc042bd73c147d83838734dfc9585
2026-06-20-generic-long-running-tool-runtime.zh.md: 5af06b78e97980822687cdc82d9b0672a42b6fc2
2026-06-20-generic-long-running-tool-runtime.zh.md: 44241321f6e4da428fe3a6402440af344fd69f10
@@ -6,7 +6,7 @@ Status: implemented
## 问题
后台 bash 原本兼有两项职责:bash 执行器既运行进程,又管理 task id、所有权、增量读取、取消、完成监听器和面向模型的控制工具。新增后台 subagent 需要相同的生命周期与交互约。如果每种长时间运行能力都独立实现该约,就会重复隔离、清理、通知和提示词行为,还会让模型为每种生产方学习不同的收集与停止协议。
后台 bash 原本兼有两项职责:bash 执行器既运行进程,又管理 task id、所有权、增量读取、取消、完成监听器和面向模型的控制工具。新增后台 subagent 需要相同的生命周期与交互约。如果每种长时间运行能力都独立实现该约,就会重复隔离、清理、通知和提示词行为,还会让模型为每种生产方学习不同的收集与停止协议。
任务注册表、控制工具与完成通知共同构成一项 harness 功能。bash 和 subagent 只提供执行专属的钩子,不拥有通用任务行为。
@@ -21,7 +21,7 @@ Status: implemented
`TaskService``@deepseek-ai/dsh-tasks` 中的抽象 seam;进程内注册表是 `@deepseek-ai/dsh-tasks-local` 中的 `LocalTaskService`(该拆分记录在[任务注册表 seam Agent Note](2026-07-26-task-registry-seam.md)中)。
## 运行时
## 运行时约
字面类型见[任务子系统页面](../../../../docs/subsystems/tasks.md)。生产方调用 `ctx.tasks.start()`,传入 kind、label、可选的所属 `Agent`、可选的正数 `outputLimitBytes` 与一个 `run()` 函数。运行时会在调用 `run()` 前完成所有可能失败的预检工作,并且只调用一次。`run()` 返回钩子后,注册过程不会再执行可能失败的步骤而直接提交;生产方无法启动没有可收集 task id 的工作。
@@ -39,7 +39,7 @@ Status: implemented
运行时为 `done` 附加一个 continuation,记录第一个终止结果、解决等待方,并逐个调用完成监听器,同时隔离每个监听器的错误。首次结果优先的结算在资源销毁期间至关重要:如果 `cancel` 抛出,运行时会强制将记录标为失败,并警告工作可能遗留,而不是永远等待一个可能永不完成的 promise。后续生产方结果不能覆盖该诊断,也不能重复通知。`cancel` 返回后如果最终未使 `done` 完成,仍会阻塞资源销毁,因为运行时无法区分这种情况与缓慢但有效的停止。
任务注册不是生产方工具 fiber 的 effect。因此,重新加载工具或控制接口插件不会终止由 agent(智能体)和后端拥有的工作。任务服务自身释放时会取消所有实时任务,并等待遵守约的生产方。
任务注册不是生产方工具 fiber 的 effect。因此,重新加载工具或控制接口插件不会终止由 agent(智能体)和后端拥有的工作。任务服务自身释放时会取消所有实时任务,并等待遵守约的生产方。
## 授权与所有者生命周期
@@ -49,7 +49,7 @@ task id 在运行时全局可见且可预测,因此注册表会授权每次访
某个所有者的第一个任务会向 `owner.ctx` 附加一个异步 effect。agent 作用域释放时会取消该所有者的实时任务、等待其终止记录,并移除其快照。该 effect 可跨生产方重载存续,并加入 agent 现有的完全停稳边界。任务服务保留 effect disposer,使服务重载可以在全局资源销毁后,从仍然存活的 agent 作用域中分离回调。
对于遵守约的生产方,`AgentHandle.dispose()` 只在所属后台工作停止后解决。需要比 agent 存活更久的工作必须以无所有者方式启动;要跨运行时重启存续,则需另行设计持久任务。
对于遵守约的生产方,`AgentHandle.dispose()` 只在所属后台工作停止后解决。需要比 agent 存活更久的工作必须以无所有者方式启动;要跨运行时重启存续,则需另行设计持久任务。
## 服务接口
@@ -103,7 +103,7 @@ bash seam 暴露 `resolve`、`run` 和 `start`。`start(spec)` 返回一个 `Bas
### 立即抽象任务运行时后端
当前 `TaskStart.run()` 约传入进程内回调与确切的 `Agent` 对象。持久化后端会改变身份、重启、所有权与观察语义,因此在引入之时注册表保持为单一具体服务,而非固化错误的边界。[任务注册表 seam Agent Note](2026-07-26-task-registry-seam.md)后来在不改变这些进程内语义的前提下,将约与进程内实现分离。
当前 `TaskStart.run()`传入进程内回调与确切的 `Agent` 对象。持久化后端会改变身份、重启、所有权与观察语义,因此在引入之时注册表保持为单一具体服务,而非固化错误的边界。[任务注册表 seam Agent Note](2026-07-26-task-registry-seam.md)后来在不改变这些进程内语义的前提下,将约与进程内实现分离。
### 由消费方负责授权或清理事件
@@ -121,7 +121,7 @@ bash seam 暴露 `resolve`、`run` 和 `start`。`start(spec)` 返回一个 `Bas
### 随机 id、提升或生命周期会话事件
授权而非不可猜测性才是访问边界,并且 id 不用于派生文件系统路径;顺序生成的品牌化 id 可保持 transcript(文本记录)易读。将前台任务提升为后台任务需要 SDK 并未规定的用户交互约。启动、读取和通知已作为工具与上下文事件记录,因此专用任务会话事件会重复面向模型的事实。
授权而非不可猜测性才是访问边界,并且 id 不用于派生文件系统路径;顺序生成的品牌化 id 可保持 transcript(文本记录)易读。将前台任务提升为后台任务需要 SDK 并未规定的用户交互约。启动、读取和通知已作为工具与上下文事件记录,因此专用任务会话事件会重复面向模型的事实。
## 测试
@@ -129,6 +129,6 @@ bash seam 暴露 `resolve`、`run` 和 `start`。`start(spec)` 返回一个 `Bas
## 后果
bash 命令与 subagent 共享一套 id 词汇、列表、通知格式、提示词习惯和控制工具。新的长时间运行生产方只需实现执行钩子,而不必再实现一套注册表与工具族。[工具实操手册](../../../../docs/cookbook/adding-a-tool.md)将生产方指向本约。
bash 命令与 subagent 共享一套 id 词汇、列表、通知格式、提示词习惯和控制工具。新的长时间运行生产方只需实现执行钩子,而不必再实现一套注册表与工具族。[工具实操手册](../../../../docs/cookbook/adding-a-tool.md)将生产方指向本约
有所属后台 bash 会随其 agent 一起停止,不再比 agent 存活更久。后台进程没有执行器超时;调用方必须终止无关工作,或依赖所有者/服务释放。流式读取只支持一个消费方,完成通知不会唤醒空闲 agent;生产方的 `cancel` 返回后如果未使 `done` 完成,仍可能阻塞资源销毁。持久任务、独立观察游标和前台提升仍属于单独设计。
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md
2026-06-21-bounded-llm-request-recovery.md: 587f2d26eee922e91c8797018964f983890eb8ec
2026-06-21-bounded-llm-request-recovery.zh.md: be52ece63ce794cb13cdf657b73bae6e9f42cf6d
2026-06-21-bounded-llm-request-recovery.zh.md: dfc5e3ab67b62042c37f05199ed39bf47ded0c5d
@@ -4,11 +4,11 @@ Status: implemented
[English](2026-06-21-bounded-llm-request-recovery.md) | 中文
[按提供方配置的请求重试策略](../feature/2026-07-24-provider-retry-policies.md)在此基础上增加了确切提供方配置与显式无界 mode。本说明继续负责结构化失败事实、已关闭步骤的恢复边界、normal mode 的暂时性默认值、可见的单次尝试和持久重试状态。[LLM 流的终止失败](2026-07-29-terminal-llm-stream-failures.md)取代了其中关于抛出错误身份和 stream sidecar 的机制。
[按提供方配置的请求重试策略](../feature/2026-07-24-provider-retry-policies.md)在此基础上增加了确切提供方配置与显式无界 mode。本说明继续负责结构化失败事实、已关闭步骤的恢复边界、normal mode 的暂时性默认值、可见的单次尝试和持久重试状态。[LLM 流的终止失败](2026-07-29-terminal-llm-stream-failures.md)取代了其中关于抛出错误身份和流伴随状态的机制。
## 问题
提供方适配器可能在分发或迭代时抛出异常,也可能以 `finish { kind: 'error' | 'aborted' }` 结束。最终适配器边界会在 `dsh-agent-loop` 接收前把抛出值规范化为该终止 finish 协议;middleware 与结果处理缺陷仍会抛出。loop 会将终止模型请求失败交给 `agent/request-error`。未被处理的失败是终态;处理失败的监听器修复策略自有状态,返回 `{ kind: 'retry' }`,并停止 waterfall 委托。[重试动作决策](../simplification/2026-07-27-request-error-retry-action.md)规定这一返回约。
提供方适配器可能在分发或迭代时抛出异常,也可能以 `finish { kind: 'error' | 'aborted' }` 结束。最终适配器边界会在 `dsh-agent-loop` 接收前把抛出值规范化为该终止 finish 协议;middleware 与结果处理缺陷仍会抛出。loop 会将终止模型请求失败交给 `agent/request-error`。未被处理的失败是终态;处理失败的监听器修复策略自有状态,返回 `{ kind: 'retry' }`,并停止 waterfall(瀑布式事件)委托。[重试动作决策](../simplification/2026-07-27-request-error-retry-action.md)规定这一返回约
该边界已能安全地再次发起请求。原始 `assistant/chunk` 事件携带失败的 `turn``step`;除非某条成功的 `assistant/message` 引用这些事件,否则消息派生会忽略它们。只有终止性 finish 成功且组装完成后,系统才会分发工具调用;重试则会从持久日志开启新的编号轮次。因此,harness 无需引入第二套响应生命周期或暂定输出协议,即可分隔两次尝试。
@@ -40,7 +40,7 @@ interface LlmFailure {
`code` 仍是 `HarnessError` 建立的提供方无关机器路由分类体系;新字段是在提供方边界观测到的事实。`ProviderRequestId` 由 `dsh-llm` 拥有并构造,序列化后为提供方发放的字符串。该载荷有意不包含 `retryable`、`failover`、`partialOutput`、提供方、模型、阶段或路由 id 字段。是否可重试属于策略,提供方/模型已位于持久请求头中,部分输出则从失败步骤的 `assistant/chunk` 事件派生。
`LlmError` 携带 `failure: LlmFailure`,并保持 `failure.code === error.code`。`FinishReasonMap.error` 和 `FinishReasonMap.aborted` 携带同一载荷,而不是并行的失败形状。最终适配器边界会从适配器抛出值中分离这些事实,并发出相应的终止 finish;未知 SDK 异常会获得 `UNKNOWN` 载荷。精确的抛出对象身份不会跨越 LLM stream seam。
`LlmError` 携带 `failure: LlmFailure`,并保持 `failure.code === error.code`。`FinishReasonMap.error` 和 `FinishReasonMap.aborted` 携带同一载荷,而不是并行的失败形状。最终适配器边界会从适配器抛出值中分离这些事实,并发出相应的终止 finish;未知 SDK 异常会获得 `UNKNOWN` 载荷。精确的抛出对象身份不会跨越 LLM seam。
agent loop(智能体循环)会将终止 finish 的 `LlmFailure` 传给 `agent/request-error`,并在记录未恢复的 `turn/end.reason` 时使用同一载荷。
@@ -78,11 +78,11 @@ agent-spine 演示组合包加载该插件,因此共享的 stdio/TUI、一次
`@deepseek-ai/dsh-timeout` 公开一个可重新布防的空闲看门狗原语。一个稳定的局部 `AbortController` 会与调用方信号融合,并在整个适配器调用期间传给传输层;每个尚未完成的 `next()` 都会布防看门狗,该调用完成时解除布防,下一次请求数据时再重新布防。超时会使用能力自身拥有的 `TimeoutReason` 中止这个稳定控制器,`finally` 则会清除定时器。适配器将自身看门狗归类为 `TIMEOUT`,将更早发生的上游中止归类为 `ABORTED`。现有的一次性 `deadline()` 不会被描述为滑动计时器。
边界测试证明两个实际传输层都能终止。手写适配器会中止其 fetch/reader,pi-ai 适配器会把稳定信号映射到 SDK,并证明 SDK 会关闭响应。如果定时器只拒绝消费方 promise,却让请求继续运行,就不满足此约。
边界测试证明两个实际传输层都能终止。手写适配器会中止其 fetch/reader,pi-ai 适配器会把稳定信号映射到 SDK,并证明 SDK 会关闭响应。如果定时器只拒绝消费方 promise,却让请求继续运行,就不满足此约
### 在现有日志中分隔尝试
一次失败尝试可以在已关闭的步骤中留下 `assistant/chunk` 事件,但绝不会追加 `assistant/message`,也不会分发工具。重试会关闭失败轮次,开启下一个编号轮次,从持久表层重建请求,并生成自己的分片。步骤仍处于打开状态时,UI 可以渲染实时分片;当 `llm/retry` 标识失败步骤,或 `turn/end` 记录失败时,UI 再标记或清除这份暂时视图。Web 会验证完整的重试载荷约,在 `llm/retry` 到达时清除失败的部分输出,将连续重试轮次的事件投影为稳定的一行,并用最新一次尝试更新该行,再从后续轮次事实派生 scheduled、started 或 cancelled 状态。倒计时以浏览器收到事件的时刻为计划延迟的起点,而不是使用 Host 事件时钟;它按向上取整且不低于 1 秒的秒数显示,仅在重试尚未结束时显示动画,并把最近一次失败的准确详情折叠在该行之后。即使失败尝试没有 assistant 节点,重试节点也会锚定自身的轨迹轮次。消息派生仍会忽略失败分片;Web 在重建历史时也会应用同一投影,因此刷新页面不会让已丢弃的部分输出重新出现,也不会生成重复的重试行。
一次失败尝试可以在已关闭的步骤中留下 `assistant/chunk` 事件,但绝不会追加 `assistant/message`,也不会分发工具。重试会关闭失败轮次,开启下一个编号轮次,从持久表层重建请求,并生成自己的分片。步骤仍处于打开状态时,UI 可以渲染实时分片;当 `llm/retry` 标识失败步骤,或 `turn/end` 记录失败时,UI 再标记或清除这份暂时视图。Web 会验证完整的重试载荷约,在 `llm/retry` 到达时清除失败的部分输出,将连续重试轮次的事件投影为稳定的一行,并用最新一次尝试更新该行,再从后续轮次事实派生 scheduled、started 或 cancelled 状态。倒计时以浏览器收到事件的时刻为计划延迟的起点,而不是使用 Host 事件时钟;它按向上取整且不低于 1 秒的秒数显示,仅在重试尚未结束时显示动画,并把最近一次失败的准确详情折叠在该行之后。即使失败尝试没有 assistant 节点,重试节点也会锚定自身的轨迹轮次。消息派生仍会忽略失败分片;Web 在重建历史时也会应用同一投影,因此刷新页面不会让已丢弃的部分输出重新出现,也不会生成重复的重试行。
如果恢复预算耗尽,最终失败会连同结构化事实在 `turn/end.reason` 中存储一次。Web 会在该序列位置派生一个 `turn-error` 节点,并内联渲染适合展示的消息与可选错误码;AUTH 投影会把可能回显凭据片段的提供方文案替换为 `API key is invalid`,原始诊断仍保留在会话日志中。实时事件和历史回放使用同一套折叠逻辑。如果暂时性恢复继续,`llm/retry` 就是该次尝试的失败与延迟的持久归属位置,因此该失败轮次不会再获得终态错误行。本决策不增加独立的最终错误事件或响应 id 词汇。
@@ -107,7 +107,7 @@ agent-spine 演示组合包加载该插件,因此共享的 stdio/TUI、一次
## 验证
- `LlmFailure` 是适配器抛出、错误 finish 和中止 finish 使用的唯一可序列化载荷;在可用时,规范化保留稳定 code、状态、重试延迟、品牌化的提供方请求 id,以及调用方中止与适配器超时之间的分类。
- 适配器抛出值会在抵达消费方前成为终止失败 chunkmiddleware 与消费方异常仍在模型请求恢复之外抛出。
- 适配器抛出值会在抵达消费方前成为终止失败分片middleware 与消费方异常仍在模型请求恢复之外抛出。
- DeepSeek 和 pi-ai 适配器测试覆盖具有代表性的 400、401/403、429、5xx、连接、格式错误/截断流、超时、中止、Retry-After 秒数/日期、请求 id 和未知 SDK 错误路径,恢复策略无需解析消息文本。
- pi-ai 将 SDK 选项固定为零次重试,并针对可重试的提供方响应执行一次可观测的线路请求尝试;独立测试确保移除任一边界都会失败。
- `agent/request-error` 携带当前失败事实、不可变的先前已重试失败事实,以及实际服务注册所对应的不可变重试策略;成功会清除历史,暂时性失败/上下文溢出交替发生的集成测试证明两种策略只消耗各自的有限预算。
@@ -126,7 +126,7 @@ agent-spine 演示组合包加载该插件,因此共享的 stdio/TUI、一次
- 提供方 SDK 可能隐藏状态或重试标头。适配器会保留 SDK 公开的稳定事实,否则使用粗粒度 code,而不会让恢复策略解析脆弱的文本。
- 持久重试事件扩展了会话协议和 UI 状态机。事件与其消费方一同交付,可避免产生无人使用的遥测词汇;但以后更改 schema 仍需要同步完成持久化和回放工作。
- 清除失败步骤的实时分片可能会明显撤回输出。与把丢弃的文本或不完整工具 JSON 呈现为已提交历史相比,这是更好的选择;快照固定这一转换。
- 适配器局部的空闲强制机制可以终止停滞的传输,而不会计入消费方思考时间。每个传输边界的约测试会防止 SDK 漂移。
- 适配器局部的空闲强制机制可以终止停滞的传输,而不会计入消费方思考时间。每个传输边界的约测试会防止 SDK 漂移。
- 多个 normal 恢复插件会叠加各自的有限预算。always mode 会先委托,再提供无界回退;重叠的分类器仍会形成依赖注册顺序的策略,必须由引入它们的插件记录并测试。
## 相关资料
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md
2026-06-21-mandatory-app-attribution-headers.md: de9125bc891cc62798480e2eccb2c90cb633de3a
2026-06-21-mandatory-app-attribution-headers.zh.md: cdada9cb235d57ad5cac98ebe0572013cac728ef
2026-06-21-mandatory-app-attribution-headers.zh.md: 6f0d0199f859fd462fdecc24e4063cbd27311bab
@@ -12,15 +12,15 @@ LLM(大语言模型)提供方请求应当标识发出请求的产品。这
## 调研
- **OpenRouter 的机制是提供方特有的。** 其当前文档说明应用归属通过 `HTTP-Referer`(必需)、`X-OpenRouter-Title``X-OpenRouter-Categories` 来追踪;`X-Title` 仅为向后兼容而接受。其 API 参考称这些头部为可选,并说它们使应用在 OpenRouter 上可被发现。这是一具体的 OpenRouter 契约,而非 IETF 或 OpenAI 兼容 API 标准。
- **在 agent 工具生态中,`HTTP-Referer` 是一种 OpenRouter 感知的约定,而非通用 agent 约定。** 它足够常见,以至于 OpenRouter SDK 和示例直接暴露它,面向 OpenRouter 的框架通常需要一种方式来透传它。但 ACPAgent Client Protocol)等 agent 协议在自己的 initialize 消息中协商名称、版本和能力,而模型提供方请求仍需 HTTP 层面的身份标识。因此「在 agent 世界中被接受」意味着「被 OpenRouter 集成所识别」,而非「可跨 agent 运行时或提供方移植」。
- **编程 agent 在 `User-Agent` 中标识产品和版本。** 公开实现在环境细节和提供方特有的附加头部上各有不同,但产品身份是共同契约;不存在通用的精确格式。
- **OpenRouter 的机制是提供方特有的。** 其当前文档说明应用归属通过 `HTTP-Referer`(必需)、`X-OpenRouter-Title``X-OpenRouter-Categories` 来追踪;`X-Title` 仅为向后兼容而接受。其 API 参考称这些头部为可选,并说它们使应用在 OpenRouter 上可被发现。这是一具体的 OpenRouter 惯例,而非 IETF 或 OpenAI 兼容 API 标准。
- **在 agent 工具生态中,`HTTP-Referer` 是一种 OpenRouter 感知的惯例,而非通用 agent 惯例。** 它足够常见,以至于 OpenRouter SDK 和示例直接暴露它,面向 OpenRouter 的框架通常需要一种方式来透传它。但 ACPAgent Client Protocol)等 agent 协议在自己的 initialize 消息中协商名称、版本和能力,而模型提供方请求仍需 HTTP 层面的身份标识。因此「在 agent 世界中被接受」意味着「被 OpenRouter 集成所识别」,而非「可跨 agent 运行时或提供方移植」。
- **编程 agent 在 `User-Agent` 中标识产品和版本。** 公开实现在环境细节和提供方特有的附加头部上各有不同,但产品身份是共同惯例;不存在通用的精确格式。
- **标准化的通用客户端身份头部是 `User-Agent`。** RFC 9110 第 10.1.5 节将 `User-Agent` 定义为用户代理软件身份,说明它用于互操作性报告和分析,并说用户代理应当在每个请求中发送它(除非被配置为不发送)。这是唯一直接对应「哪个产品在发出此 HTTP 请求」的标准头部。
- **`Referer` 是标准的,但 OpenRouter 的 `HTTP-Referer` 不是标准字段。** RFC 9110 第 10.1.3 节将 `Referer` 定义为获取目标 URI 的来源 URI,并用大量篇幅讨论隐私限制。OpenRouter 则要求 `HTTP-Referer`,将其用作应用 URL 标识符。该名称和含义是 OpenRouter 特有的,尽管它形似标准 `Referer` 头部的 CGI 环境变量形式。
- **`From` 是标准的,但不适合作为强制默认值。** RFC 9110 第 10.1.2 节将 `From` 定义为负责用户代理的人的电子邮件地址。机器人代理应当发送它以便服务器联系运营者,但非机器人代理出于隐私和安全策略考虑不应在未经用户显式配置的情况下发送。harness 可以后续支持运营者联系方式,但不得凭空捏造或全局强制要求。
- **请求体中的 `user``metadata` 字段不是应用归属。** 部分模型 API 暴露稳定的终端用户标识符、请求元数据、标签或项目/账户头部。这些对滥用监控、内部计费、仪表盘或链路追踪有用,但它们要么标识的是终端用户而非产品,要么是提供方特有的 body schema,要么不保证能通过 OpenAI 兼容网关透传。它们不能替代静态的应用身份头部。
- **SDK 遥测头部标识的是 SDK,而非应用。** 官方和第三方 SDK 常发送库/版本头部。这些帮助 SDK 维护者调试其客户端,但除非应用显式提供产品归属层,否则它们不能标识 harness 作为应用。
- **pi-ai 有原生支持的头部钩子。** `@earendil-works/pi-ai``StreamOptions.headers` 将调用方头部最后合并(覆盖提供方默认值),因此基于库的适配器无需包装或上游改动即可满足与手写适配器相同的线路约。mock 服务器测试套件对两个适配器都断言头部到达了线路。
- **pi-ai 有原生支持的头部钩子。** `@earendil-works/pi-ai``StreamOptions.headers` 将调用方头部最后合并(覆盖提供方默认值),因此基于库的适配器无需包装或上游改动即可满足与手写适配器相同的线路约。mock 服务器测试套件对两个适配器都断言头部到达了线路。
## 决策
@@ -41,7 +41,7 @@ LLM(大语言模型)提供方请求应当标识发出请求的产品。这
| 目标 | 映射 |
|---|---|
| 所有基于 HTTP 的适配器 | `User-Agent: {product}/{version} (+{url})`——括号中的 `+url` 注释符合 RFC 9110 保守的 product/comment 语法。 |
| 直连 DeepSeek 端点 | `User-Agent`;除非 DeepSeek 文档化了等效约,否则不发送 OpenRouter 特有头部。 |
| 直连 DeepSeek 端点 | `User-Agent`;除非 DeepSeek 文档化了等效约,否则不发送 OpenRouter 特有头部。 |
| OpenRouter 端点 | 目前仅 `User-Agent`。本 Agent Note 下不发送 `HTTP-Referer``X-OpenRouter-Title``X-Title``X-OpenRouter-Categories`。 |
| 未来提供方 | 仅 `User-Agent`,除非后续提供方特有的 Agent Note 接受额外头部。不要类比复用 `HTTP-Referer`。 |
@@ -49,9 +49,9 @@ LLM(大语言模型)提供方请求应当标识发出请求的产品。这
## 验证
已落地的约:
已落地的约
- `dsh-llm``LlmAdapter` 作者文档化了强制的 `User-Agent` 归属约(`LlmAdapter` JSDoc、包 README,以及 `docs/subsystems/llm-streaming.md` 的适配器约(adapter contract)章节)。
- `dsh-llm``LlmAdapter` 作者文档化了强制的 `User-Agent` 归属约`LlmAdapter` JSDoc、包 README,以及 `docs/subsystems/llm-streaming.md` 的适配器约adapter contract)章节)。
- 共享辅助函数(`attributionHeaders` / `userAgent`)从包元数据构建应用身份和标准 `User-Agent` 值,适配器无需手动复制版本常量。
- `dsh-llm-deepseek` 在每个请求上发送共享的 `User-Agent`,其 mock 服务器套件断言精确值。
- `dsh-llm-pi-ai` 通过 pi-ai 的 `StreamOptions.headers` 钩子发送相同的 `User-Agent`,其 mock 服务器套件断言精确值。
@@ -63,7 +63,7 @@ LLM(大语言模型)提供方请求应当标识发出请求的产品。这
**现在就实现 OpenRouter 应用归属。** 本 Agent Note 否决。发送 `HTTP-Referer``X-OpenRouter-Title` 可以满足 OpenRouter 排名,但这些头部是提供方特有的产品功能,不是本 Agent Note 试图标准化的提供方无关的模型请求归属。支持它们应当是后续显式的 OpenRouter 适配器/模式决策,而非隐藏在首个共享归属辅助函数中。
**向所有提供方发送 OpenRouter 头部。** 否决。这会把一自定义的 OpenRouter 契约当作通用标准,并向未要求这些字段的提供方发送语义误导的头部。还有风险将 `HTTP-Referer` 当作通用应用 URL 字段使用,尽管标准 HTTP 已有 `User-Agent` 用于产品身份、`Referer` 用于不同的浏览上下文概念。
**向所有提供方发送 OpenRouter 头部。** 否决。这会把一自定义的 OpenRouter 惯例当作通用标准,并向未要求这些字段的提供方发送语义误导的头部。还有风险将 `HTTP-Referer` 当作通用应用 URL 字段使用,尽管标准 HTTP 已有 `User-Agent` 用于产品身份、`Referer` 用于不同的浏览上下文概念。
**仅使用提供方账户/项目身份。** 否决。组织/项目头部、API key、云账户和计费项目标识的是谁付费或谁拥有请求,而非哪个应用在发送流量。它们也不暴露公开的应用标题/类别,无法帮助 OpenRouter 等网关构建应用排名。
@@ -79,6 +79,6 @@ LLM(大语言模型)提供方请求应当标识发出请求的产品。这
**应用 URL 指向一个尚不存在的仓库。** `deepseek-ai/deepseek-harness-sdk` 是计划中的公开主页;在它创建之前,该 URL 是一个阻塞发布的悬空承诺。
**不同客户端库的头部支持有差异。** 手写适配器直接设置头部;基于 pi-ai 的适配器依赖 pi-ai 继续尊重 `StreamOptions.headers`(最后合并覆盖提供方默认值)。线路级 mock 服务器测试是守卫:如果 pi-ai 升级后不再投递该头部,套件会变红。这对抽象施加了有益的压力:一个无法设置强制头部的提供方适配器不能完整实现 harness 的 LLM 约。
**不同客户端库的头部支持有差异。** 手写适配器直接设置头部;基于 pi-ai 的适配器依赖 pi-ai 继续尊重 `StreamOptions.headers`(最后合并覆盖提供方默认值)。线路级 mock 服务器测试是守卫:如果 pi-ai 升级后不再投递该头部,套件会变红。这对抽象施加了有益的压力:一个无法设置强制头部的提供方适配器不能完整实现 harness 的 LLM 约
**OpenRouter 排名尚未受益。** `User-Agent` 是提供方无关的 HTTP 身份的正确基线,但它不会创建 OpenRouter 应用页面或排名,因为 OpenRouter 要求 `HTTP-Referer` 来实现该产品功能。这是有意为之:公开应用市场参与是一个独立的产品决策,不是强制请求归属的前提。
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md
2026-06-24-web-capability-seam.md: b705236690859961ed69b307dbb59ebefcbd65ac
2026-06-24-web-capability-seam.zh.md: 15a16b12119f69cec632e219585600d261fda54f
2026-06-24-web-capability-seam.zh.md: ea9261b4bc16cbd913c90f5ccb1aeebc1a0d8dea
@@ -6,11 +6,11 @@ Status: implemented
## 问题
harness 需要面向模型的 web 工具,但不能将模型约绑定到某一家厂商的 API 形状上。搜索是当前的压力点:从一开始就同时支持 Exa 搜索和 Perplexity 搜索——两种刻意不同的提供方形状(Exa 返回扁平的 `results[]`,每项包含 `{title, url, highlights, publishedDate}`Perplexity 返回一段生成式回答加引用列表)——正是用来证明归一化的 seam 并非只是镜像某一家厂商。Fetch 是另一项独立能力:匿名公开 HTTP(S) fetch 后端涉及传输、安全、重定向、解码和大小限制等关注点,与提供方支撑的搜索并不相同。
harness 需要面向模型的 web 工具,但不能将模型约绑定到某一家厂商的 API 形状上。搜索是当前的压力点:从一开始就同时支持 Exa 搜索和 Perplexity 搜索——两种刻意不同的提供方形状(Exa 返回扁平的 `results[]`,每项包含 `{title, url, highlights, publishedDate}`Perplexity 返回一段生成式回答加引用列表)——正是用来证明归一化的 seam 并非只是镜像某一家厂商。Fetch 是另一项独立能力:匿名公开 HTTP(S) fetch 后端涉及传输、安全、重定向、解码和大小限制等关注点,与提供方支撑的搜索并不相同。
面向模型的接口必须保持稳定,而后端可以更换。更换搜索提供方不应改变模型发起查询的方式;更换 fetch 实现不应改变模型请求 URL 的方式。反过来,提供方包也不应仅仅因为自己有额外的提供方特有旋钮就暴露自己的面向模型工具 schema。
如果把搜索和 fetch 直接放进 `dsh-tool-web`,面向模型的工具就要同时承担提供方选择、后端请求映射、传输策略、结果归一化、提示词引导、展示和 schema 注册。让每个提供方注册自己的工具则有相反的问题:工具的可用性、名称、描述和参数将取决于恰好加载了哪些提供方包,提供方特有字段会泄漏到模型约中。
如果把搜索和 fetch 直接放进 `dsh-tool-web`,面向模型的工具就要同时承担提供方选择、后端请求映射、传输策略、结果归一化、提示词引导、展示和 schema 注册。让每个提供方注册自己的工具则有相反的问题:工具的可用性、名称、描述和参数将取决于恰好加载了哪些提供方包,提供方特有字段会泄漏到模型约中。
还有一个提供方选择的问题。现有的 `tool-bash``tool-fs` 可以依赖 Cordis 的 `inject`,因为只有一个后端服务键。Web 有两项独立能力(`search``fetch`),每项能力可能有多个提供方。`inject: ['web']` 能证明 seam 存在,但不能证明存在可用的搜索或 fetch 提供方,也无法定义多个提供方注册时谁胜出。
@@ -32,7 +32,7 @@ Web 访问是一个一等能力 seam,遵循[能力 seam Agent Note](2026-06-13
- 工具绝不会仅仅因为其选定的提供方缺失、配置错误、缺少凭证、存在歧义或暂时不可用就被注销。
- 提供方在执行时解析,当选定的能力无法运行时返回结构化的 `WebError`
这使模型 schema 保持稳定,而不将插件加载顺序、凭证状态或 HMR(热模块替换)时序纳入面向模型的约。如果 web 搜索已启用但不存在可用的搜索提供方,`web_search` 仍然可见,执行时以结构化的 `WebError`(如 `WEB_PROVIDER_UNAVAILABLE``WEB_PROVIDER_CONFIGURED_UNAVAILABLE`)失败。如果某个提供方在 `dsh-tool-web` 之后出现,下一次执行即可使用它而无需更改 schema。如果某个提供方在调用过程中消失,执行以结构化的 `WebError` 失败,而不是静默选择另一个提供方或回退到 `UNKNOWN_TOOL`
这使模型 schema 保持稳定,而不将插件加载顺序、凭证状态或 HMR(热模块替换)时序纳入面向模型的约。如果 web 搜索已启用但不存在可用的搜索提供方,`web_search` 仍然可见,执行时以结构化的 `WebError`(如 `WEB_PROVIDER_UNAVAILABLE``WEB_PROVIDER_CONFIGURED_UNAVAILABLE`)失败。如果某个提供方在 `dsh-tool-web` 之后出现,下一次执行即可使用它而无需更改 schema。如果某个提供方在调用过程中消失,执行以结构化的 `WebError` 失败,而不是静默选择另一个提供方或回退到 `UNKNOWN_TOOL`
该 seam 刻意不暴露任何观察面——没有注册表变更事件,也没有聚合的能力状态查询。不可用性是调用方通过执行观察到的事实:`search()`/`fetch()` 在调用时解析提供方,并抛出命名了失败原因的结构化 `WebError`。[观察面 Agent Note](../../archived/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md) 记录了这一判断:基于调用的派生选择与基于启用的注册使得没有消费方需要变更信号或独立于执行和错误路由的可用性探测;未来的提供方状态面板会重新引入它实际消费的最小信号或查询。
@@ -66,13 +66,13 @@ flowchart LR
toolWeb -->|ctx.tools.register| webFetch["tool: web_fetch"]
```
`@deepseek-ai/dsh-web` 仅依赖 Cordis 和底层 harness 支持。它声明 `ctx.web`、提供方接口、请求/结果类型、提供方可用性约和错误码。它不导入工具、agent、会话、LLM 或提供方包。
`@deepseek-ai/dsh-web` 仅依赖 Cordis 和底层 harness 支持。它声明 `ctx.web`、提供方接口、请求/结果类型、提供方可用性约和错误码。它不导入工具、agent、会话、LLM 或提供方包。
提供方包仅依赖 `dsh-web` 和 Cordis。它们拥有凭证、端点、协议格式映射、解析和 `WebError` 转换,使用平台 `fetch`。每个提供方注入共享服务并注册后端;只有 `dsh-web` 拥有 `ctx.web` 键。提供方私有的协议形状不会产生对 `ctx.llm` 或 Cordis HTTP 服务的依赖。
`@deepseek-ai/dsh-tool-web` 依赖 `@deepseek-ai/dsh-web``@deepseek-ai/dsh-tools``@deepseek-ai/dsh-system-prompt` 和 Cordis。它从不导入具体的提供方包。
## `ctx.web`
## `ctx.web` 约
`ctx.web` 是一个提供方注册表加上一个带提供方选择的执行面。注册表部分与 `LlmService` 保持接近:每种能力类别一个 `Map<id, provider>``registerSearchProvider`/`registerFetchProvider` 方法返回 disposer,重复 id 抛出 `WebError`,执行时解析在选定提供方缺失或不可用时抛出异常。权威签名见 `packages/web/web/src/types.ts`seam 的形状:
@@ -280,7 +280,7 @@ SSRF/私有网络防护(阻断私有、回环、链路本地、多播及其他
## 测试
每一层在自己的 seam 处固定:`dsh-web` 中的注册/选择/截断/abort 约与 `WebError` 码;每个提供方基于录制的 fixture(测试前置数据)的请求/响应映射(Perplexity fixture 包含纯 URL 引用,以保持可选 source 字段的诚实性),加上每个真实提供方的自跳过带密钥冒烟测试;`web-fetch-local` 中的真实本地 HTTP 行为;`dsh-tool-web` 中通过真实工具注册表的启用驱动注册、结构化执行错误和结果格式化。一个真实 Loader 冒烟测试守护两种导出形状([事故复盘(postmortem 0001](../../../../docs/postmortem/0001-acp-default-export-drops-inject.md)):`dsh-web` 是默认导出的服务,而提供方和 `tool-web` 是命名空间插件,误加 `export default` 会丢失 `inject`
每一层在自己的 seam 处固定:`dsh-web` 中的注册/选择/截断/abort 约`WebError` 码;每个提供方基于录制的 fixture(测试前置数据)的请求/响应映射(Perplexity fixture 包含纯 URL 引用,以保持可选 source 字段的诚实性),加上每个真实提供方的自跳过带密钥冒烟测试;`web-fetch-local` 中的真实本地 HTTP 行为;`dsh-tool-web` 中通过真实工具注册表的启用驱动注册、结构化执行错误和结果格式化。一个真实 Loader 冒烟测试守护两种导出形状([事故复盘(postmortem 0001](../../../../docs/postmortem/0001-acp-default-export-drops-inject.md)):`dsh-web` 是默认导出的服务,而提供方和 `tool-web` 是命名空间插件,误加 `export default` 会丢失 `inject`
## 曾考虑的替代方案
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-26-file-context-as-event-gate.md
2026-06-26-file-context-as-event-gate.md: 4700222aa2e0f91d9f355495c228e2eb92825f55
2026-06-26-file-context-as-event-gate.zh.md: e0a3619859c2ab4eb99f7cbf69375f9fe40d10b7
2026-06-26-file-context-as-event-gate.zh.md: dff5c3e9ea99e73d3d1a86c4012da1e4232021bb
@@ -44,7 +44,7 @@ provider dsh-fs-local local implementation of ctx.fs
这是有意为之的。如果 `dsh-fs-policy` 在其 waterfall(瀑布式事件)处理器中 stat 并比较版本,该检查与工具实际写入之间会存在 TOCTOU 间隙——文件可能在此期间变化,因此该检查只是一个虚假保证,提供方的锁无论如何都要兜底。将版本检查放在提供方的临界区中既无竞态又无额外 `stat`。所以 `dsh-fs-policy` **不做**任何文件系统 I/O;「必须基于最近一次读取」的保证由 CAS *实现*`dsh-fs-policy` 只负责选择基准(`vObserved`)并对先前观测进行门控。
## 提供方约变更:版本守卫变为可选
## 提供方约变更:版本守卫变为可选
为使裸提供方不受约束,其两个 mutation 上的版本守卫变为**可选**——传入则守卫,省略则无条件执行:
@@ -66,7 +66,7 @@ editText(target: FsTarget, edit: FsEditRequest, expected?: { version: FsVersion
## 事件词汇(由 `dsh-fs` 拥有)
事件定义在 `@deepseek-ai/dsh-fs` 中,而非 `dsh-fs-policy` 中。这是解耦约所迫:`dsh-tool-fs` 是发射方,因此它必须引用事件类型,且即使 `dsh-fs-policy` 不再提供方法服务,它也必须能编译通过。`dsh-fs` 是 `dsh-tool-fs` 和 `dsh-fs-policy` 都已依赖的包,因此它是唯一能让发射方和策略监听方共享词汇而不让发射方依赖策略插件的归属地。
事件定义在 `@deepseek-ai/dsh-fs` 中,而非 `dsh-fs-policy` 中。这是解耦约所迫:`dsh-tool-fs` 是发射方,因此它必须引用事件类型,且即使 `dsh-fs-policy` 不再提供方法服务,它也必须能编译通过。`dsh-fs` 是 `dsh-tool-fs` 和 `dsh-fs-policy` 都已依赖的包,因此它是唯一能让发射方和策略监听方共享词汇而不让发射方依赖策略插件的归属地。
这些事件携带既有的 `dsh-fs` 词汇(`FsTarget`、`FsVersion`、`FsWriteIntent`)加一个不透明的 actor——不携带面向模型的概念(行窗口、行号或渲染后的页脚不会泄漏到此层)。
@@ -108,7 +108,7 @@ interface Events {
`fs/*` 决策事件是**由工具分发的无绑定 waterfall**(类似 `agent/request`,由循环分发且无 `this`),而非服务绑定的 waterfall(如 `llm/stream`)。分发者是 `dsh-tool-fs` 插件,它不是一个服务。
## 工具约(`dsh-tool-fs`
## 工具约`dsh-tool-fs`
工具保留其面向模型的 schema`read`/`write`/`edit`,逐字节不变)和提示词段落。提示词引导仍以策略优先,因为加载 fs 工具的部署预期也会加载 `dsh-fs-policy`:模型仍被告知在覆写或编辑前先读取,任何声称「后端」要求如此的措辞应修正为 fs-policy 插件要求如此。裸提供方回退不改变提示词立场。
@@ -124,9 +124,9 @@ interface Events {
工具在每次分发时将 `exec`(工具执行上下文)作为 `actor` 参数传入,以便 `dsh-fs-policy` 推导其观测状态的 owner。工具不知道策略插件是否存在:它始终在 `next` thunk 中提供裸默认行为,而 `dsh-fs-policy` 在默认部署中会在 thunk 运行前短路它。
**`fs/observed` 在操作成功后触发。** 其监听器必须是同步、不抛异常的记录器;工具不对 plain emit 做保护,因此抛异常的监听器会在 mutation 已成功后报告失败。异步或可失败的观测需要另一份事件约。
**`fs/observed` 在操作成功后触发。** 其监听器必须是同步、不抛异常的记录器;工具不对 plain emit 做保护,因此抛异常的监听器会在 mutation 已成功后报告失败。异步或可失败的观测需要另一份事件约
## 策略插件约(`dsh-fs-policy`
## 策略插件约`dsh-fs-policy`
`dsh-fs-policy` 是插件,不是服务。它不注册 `ctx.fileContext`,没有公开方法面,不暴露 `read`/`write`/`edit`/`resolve` 方法。它通过 `ctx.on()` 注册三个监听器(每个返回一个 disposer 用于 HMR)。它维护观测状态 `WeakMap<owner, Map<targetKey, { version }>>`,以及结构化的 owner 推导(将事件中不透明的 `object` actor 收窄为自己的 `{ agent?: { session? } }` 形状),但不注入 `fs`——每个处理器只操作自己的 `WeakMap`,从不操作 `ctx.fs`。
@@ -150,7 +150,7 @@ interface Events {
## 取代关系
本 Agent Note 修正——而非推翻——[拆分文件系统 seam Agent Note](../simplification/2026-06-26-fsspec-style-fs-seam.md)。四层拆分、提供方约和新鲜度*策略*均保留。变更的是**工具与策略层之间的耦合方式**:强制性方法服务变为插件拥有的事件门控,fs I/O + 读取窗口从 `fileContext` 上移至 `dsh-tool-fs`。拆分文件系统 seam Agent Note 中关于 `dsh-tool-fs` 注入 `fileContext` 以及 `fileContext` 拥有 `read`/`write`/`edit` 的描述已在同一变更中更新。
本 Agent Note 修正——而非推翻——[拆分文件系统 seam Agent Note](../simplification/2026-06-26-fsspec-style-fs-seam.md)。四层拆分、提供方约和新鲜度*策略*均保留。变更的是**工具与策略层之间的耦合方式**:强制性方法服务变为插件拥有的事件门控,fs I/O + 读取窗口从 `fileContext` 上移至 `dsh-tool-fs`。拆分文件系统 seam Agent Note 中关于 `dsh-tool-fs` 注入 `fileContext` 以及 `fileContext` 拥有 `read`/`write`/`edit` 的描述已在同一变更中更新。
## 验证
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-30-event-domain-semantics.md
2026-06-30-event-domain-semantics.md: 14102b105e7bcaa6a00ac9c406933f772fc45a6f
2026-06-30-event-domain-semantics.zh.md: 91e490b57bcdc1ed95f0b8b40cc9e16ef4d36f29
2026-06-30-event-domain-semantics.zh.md: 2607e59490d8ba139bb1289d0f2941894281f6c6
@@ -21,7 +21,7 @@ harness 通过 Cordis 事件分类体系扩展 agent loop(智能体循环)
**三个域,各司其职,以一条边界规则统一。**
- **`session/*`——持久的、可回放的事实日志。** 拥有 `SessionEventMap`;每条记录仅含 JSON(无活对象)。每次追加触发一次 `session/event` emit,加上 `session/flush` 并行持久性检查点。它同时也是实时 transcript(文本记录)源:想渲染或响应已发生事件的消费方在此订阅,因此实时渲染与回放投影共享同一路径。
- **`agent/*`——运行时实时表面。** 始终携带活的 `Agent`。拦截 waterfall(瀑布式事件)(`agent/pre-step``agent/request``agent/request-error`)负责变换、拒绝或恢复;awaited `agent/turn-stopping` 观察停止边界;瞬态 emit 报告生命周期、状态、inbox 插入/领取/丢弃与错误。轮次和步骤边界不在此处——它们是持久的会话事件,从 `session/event` 读取;token 流(`assistant/chunk`)和中途 steering(以 `user/message` 呈现)同理。
- **`agent/*`——运行时实时表面。** 始终携带活的 `Agent`。拦截 waterfall(瀑布式事件)(`agent/pre-step``agent/request``agent/request-error`)负责变换、拒绝或恢复;awaited `agent/turn-stopping` 观察停止边界;瞬态 emit 报告生命周期、状态、inbox 插入/领取/丢弃与错误。轮次和步骤边界不在此处——它们是持久的会话事件,从 `session/event` 读取;token 流(`assistant/chunk`)和作为一条 `user/message` 的轮次中途 steering(中途引导)同理。
- **`tools/*`——工具注册表与执行 seam。**
**边界规则:** 持久的、可回放的事实是 `SessionEvent`;实时拦截或瞬态/活对象信号是 `agent`/`tools` Cordis 事件。轮次或步骤边界是持久事实,因此存在于会话日志中并从 `session/event` 源读取——不会被镜像为 `agent/*` emit。
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.md
2026-07-02-tool-render-intent-union.md: 67607b2848305439513503d7e03ad5e2a2e4020a
2026-07-02-tool-render-intent-union.zh.md: 9cbab75ce87f6d313ca4b6ac8dda043733361f0d
2026-07-02-tool-render-intent-union.zh.md: f69442cc1cf226a5dd949fc3890029a69a639953
@@ -63,7 +63,7 @@ terminal 意图只用于展示。harness 仍通过自身的 bash 服务执行命
## 曾考虑的替代方案
- **完全删除工具自有的展示**:即本 Agent Note 所取代的那个被否决的 collapse 提案;其自身的结论正是推迟到两个真实工具和两个真实消费方存在后再做此联合类型,该条件现已满足。
- **让 UI 执行 terminal 意图**:否决。这样会绕过 harness 的 bash 策略与归属约,并把命令执行分裂到不同后端。terminal 卡片描述的是 harness 拥有的执行,绝不授权客户端侧执行。
- **让 UI 执行 terminal 意图**:否决。这样会绕过 harness 的 bash 策略与归属约,并把命令执行分裂到不同后端。terminal 卡片描述的是 harness 拥有的执行,绝不授权客户端侧执行。
- **可合并扩展的联合类型**`ContentBlockMap` 模式):否决。新的渲染意图无论如何需要新的 bridge 代码来渲染,因此一个被 bridge 静默丢弃的插件添加变体,比封闭联合类型在 bridge 的 `assertNever` switch 处引发的编译错误更糟糕。
- **保留可选字段集合**:即「问题」一节所剖析的现状:无效状态可表达、字段交互无文档、且完全无法请求 diff 卡片。
@@ -78,5 +78,5 @@ terminal 意图只用于展示。harness 仍通过自身的 bash 服务执行命
## 相关
- 取代早先被否决的折叠工具自有呈现提案(已否决——「等两个真实工具和两个真实消费方,然后做带标签 render-intent 联合类型」)中的推迟决定。该条件现已满足;本 Agent Note 即为那个联合类型。
- 被[结果时已应用 hunk 差异](../../archived/architecture/2026-07-02-result-time-applied-hunk-diffs.md)(已归档)扩展:后者添加了一个持久化的 `meta` 通道,使 write/edit 在结果时输出 `DiffResultView`(应用后的变更:带上下文行的 contextual hunk / 每个 `replace_all` 位点一个,或创建时的整文件 diff)——值/呈现拆分与持久化的 `presentationMeta` 通道现由[规范工具输出](2026-07-20-canonical-tool-output-contract.md)拥有。
- 被[结果时已应用 hunk 差异](../../archived/architecture/2026-07-02-result-time-applied-hunk-diffs.md)(已归档)扩展:后者添加了一个持久化的 `meta` 通道,使 write/edit 在结果时输出 `DiffResultView`(应用后的变更:带上下文行的 contextual hunk / 每个 `replace_all` 位点一个,或创建时的整文件 diff)——值/呈现拆分与持久化的 `presentationMeta` 通道现由[规范工具输出约](2026-07-20-canonical-tool-output-contract.md)拥有。
-`ToolTerminal` 折入当前 UI 传输层使用的带标签 `terminal` 视图。
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md
2026-07-05-prompt-variables-and-tool-guidance-ownership.md: a3b5021daf323971308760bde4f97651db8edbda
2026-07-05-prompt-variables-and-tool-guidance-ownership.zh.md: 490d41302ea4f4e14e11e301acbf47170f95cace
2026-07-05-prompt-variables-and-tool-guidance-ownership.zh.md: 0b84f188e6637d8b1b42ed2e6df3cd7e4061d75d
@@ -36,7 +36,7 @@ Status: implemented
### 工具指导归属
每个工具的语义和选择指导放在工具 description 中。提示词 section 只承载跨调用习惯,例如检查 bash 退出标记或优先使用文件系统工具而非 shell 命令。`todo_write` 和 subagent 工具不需要 section,因为它们的 description 包含完整约。部署 persona 只包含角色和行为。
每个工具的语义和选择指导放在工具 description 中。提示词 section 只承载跨调用习惯,例如检查 bash 退出标记或优先使用文件系统工具而非 shell 命令。`todo_write` 和 subagent 工具不需要 section,因为它们的 description 包含完整约。部署 persona 只包含角色和行为。
### Subagent 对话历史描述符
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md
2026-07-05-reconstructable-requests.md: ebca9b99cad791159302da9c2bbce9f4df147aab
2026-07-05-reconstructable-requests.zh.md: 91ef3fd04502f2c2f092da60a8bd909cf7fa25df
2026-07-05-reconstructable-requests.zh.md: e7ed83bbc553e86d53d88e442b0fd8992973c9e7
@@ -47,9 +47,9 @@ Status: implemented
## 后果
- 一个日志无法解释的请求不可能被意外构造——无论是循环还是监听器;变异已构建的请求会抛异常;每个 header 变更都是持久的、可 diff 的日志事件。
- 模型可见上下文使用已记录消息通道。`agent.inject()` 与工具 `additionalContexts` 进入 inbox,等待后续领取;必须与当前已领取批次一起结算的上下文由 `agent/pre-step` 返回。每个进入步骤的值都是带来源的持久 `user/message`,只付出一次代价并在后续成为可缓存前缀,代价是会在历史中累积直至压缩。
- 在提供方处仍需全价计算的内容是固有的且已记录的:压缩(其 `compact/*` 事件和替换条目)、真正的提示词、工具或配置变更(reason 为 `change``request/header`),或带漂移的进程边界(不同的 `resume` 快照)。提供方自身的 reasoning-content 排除由服务端管理。
- 模型可见上下文使用已记录消息通道。`agent.inject()` 与工具 `additionalContexts` 进入 inbox,等待后续领取;必须与当前已领取批次一起结算的上下文由 `agent/pre-step` 返回。每个进入步骤的值都是带来源的持久 `user/message`,只付出一次代价,此后即被前缀缓存,代价是会在历史中累积直至压缩。
- 在提供方处仍需全价计算的内容是固有的且已记录的:压缩(其 `compact/*` 事件和替换条目)、真正的提示词、工具或配置变更(reason 为 `change``request/header`),或带漂移的进程边界(不同的 `resume` 快照)。提供方自身对思考内容的排除由服务端管理。
- `agent/pre-step` 是当前请求的消息 seam;直接修改 inbox 则是最终进入后续请求的 seam。
- 工具结果裁剪(计划中)无需新机制:一个已记录的单条目 surface replace`start === end`),携带同一 `callId` 下裁剪后的 `tool/result`——属压缩家族,回放正确,缓存击穿由相同的压力逻辑批量处理。
- 工具结果裁剪(计划中)无需新机制:一个已记录的单条目 surface replace`start === end`),携带同一 `callId` 下裁剪后的 `tool/result`——属压缩家族,回放正确,缓存失效由相同的压力逻辑批量处理。
- 会话日志每个循环实例增长一个 `request/header` 快照,并在真正变更时增加快照。它比 delta 编解码器更大,但相对分片密集型日志仍然很小,并只保留一种回放表示。`SESSION_FORMAT_VERSION` 保持 `0`;旧的 delta 事件被拒绝而非迁移。
- 快照预期输出变更一次(每个 transcript(文本记录)增加其 header 事件);写入文件系统的 fixture(测试前置数据)以规范化的撰写形式存储,工具参数使用 cwd 相对路径,因为回放只对 cwd 无关的参数路径做往返。
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-05-windows-jsonl-durable-publish.md
2026-07-05-windows-jsonl-durable-publish.md: 38c4adc7a4f85d45e53e70fcac84073ab4e50775
2026-07-05-windows-jsonl-durable-publish.zh.md: 3a91b7dab0751ad1b7ac1a425371ccf62d23e89c
2026-07-05-windows-jsonl-durable-publish.zh.md: 205460bcd374bd351cfdf541eb4041c09461229d
@@ -6,9 +6,9 @@ Status: implemented
## 问题
`dsh-session-persistence-jsonl` 在首次追加时延迟发布会话日志。POSIX 协议会写入临时文件,对其执行 fsync,将其链接至最终名称,对父目录执行 fsync,然后移除临时链接。对父目录执行 fsync 是持久性约的一部分:命名空间变更后发生崩溃时,已经提交的最终名称不能丢失,否则调用方会误以为会话日志已经物化。
`dsh-session-persistence-jsonl` 在首次追加时延迟发布会话日志。POSIX 协议会写入临时文件,对其执行 fsync,将其链接至最终名称,对父目录执行 fsync,然后移除临时链接。对父目录执行 fsync 是持久性约的一部分:命名空间变更后发生崩溃时,已经提交的最终名称不能丢失,否则调用方会误以为会话日志已经物化。
Windows 具备原子命名空间操作,但 Node 没有暴露与 POSIX 等价的父目录 fsync 约。如果把 Windows 目录同步失败视为成功,就会在无提示的情况下削弱持久化后端。因此,Windows 路径需要采用不同的发布原语,而不是在 POSIX 的 `syncDir` 辅助函数中添加条件分支。
Windows 具备原子命名空间操作,但 Node 没有暴露与 POSIX 等价的父目录 fsync 约。如果把 Windows 目录同步失败视为成功,就会在无提示的情况下削弱持久化后端。因此,Windows 路径需要采用不同的发布原语,而不是在 POSIX 的 `syncDir` 辅助函数中添加条件分支。
## 决策
@@ -28,8 +28,8 @@ Windows 通过持久的暂存发布来创建缺失目录:创建一个以固定
## 影响
该后端在各平台上维持同一项外部约:首次追加要么把完整日志发布到最终名称,要么失败且不覆盖已有日志。平台分流只是实现细节;`SessionPersistence` API 和 JSONL 逻辑记录格式均不改变。后续的 [Zstandard 编码决策](2026-07-19-zstandard-jsonl-session-logs.md)会先作用于不透明字节,然后才由任一平台执行发布。
该后端在各平台上维持同一项外部约:首次追加要么把完整日志发布到最终名称,要么失败且不覆盖已有日志。平台分流只是实现细节;`SessionPersistence` API 和 JSONL 逻辑记录格式均不改变。后续的 [Zstandard 编码决策](2026-07-19-zstandard-jsonl-session-logs.md)会先作用于不透明字节,然后才由任一平台执行发布。
Windows 测试会在原生 Windows 上执行真实的 Win32 发布路径。断电行为属于 API 约属性,单元测试无法证明;可测试的不变量包括:Windows 物化不会调用目录 fsync、最终路径冲突会失败、达到最大长度的目标路径组件仍可物化、临时日志在发布前已经执行 fsync,并且生成的日志可以正常加载。
Windows 测试会在原生 Windows 上执行真实的 Win32 发布路径。断电行为属于 API 约属性,单元测试无法证明;可测试的不变量包括:Windows 物化不会调用目录 fsync、最终路径冲突会失败、达到最大长度的目标路径组件仍可物化、临时日志在发布前已经执行 fsync,并且生成的日志可以正常加载。
两个平台的追加和修复仍使用普通文件句柄 fsync。追加失败后,系统会关闭仅追加句柄,以读写模式重新打开日志,将文件截断到追加前的大小,并对回滚结果执行 fsync,因为 Windows 不允许在仅追加句柄上调用 `ftruncate`
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md
2026-07-06-timeout-deadline-library.md: 63463a76a65743436d4e78479800c19e257a42de
2026-07-06-timeout-deadline-library.zh.md: 16f1a7eba902e6d7b3e2f730cb5df702a37bd429
2026-07-06-timeout-deadline-library.zh.md: c47e6d4e6ed0bec368fca8857e5bd4d7229b6b0e
@@ -77,7 +77,7 @@ export function timeoutOf(x: AbortSignal | { reason?: unknown }, code?: string):
| 关注点 | 负责方 |
|---|---|
| 校验请求提示并钳位默认值/最大值 | `dsh-timeout``clampTimeout`):纯算术加共享的正有限请求约 |
| 校验请求提示并钳位默认值/最大值 | `dsh-timeout``clampTimeout`):纯算术加共享的正有限请求约 |
| 启动一次性定时器、到期中止、携带 reason、与上游取消融合 | `dsh-timeout``deadline` |
| 仅围绕未结算的迭代器 demand 启动和重启 | `dsh-timeout``idleWatchdog` |
| 清除定时器 | `dsh-timeout`(任一原语的 `[Symbol.dispose]` |
@@ -90,7 +90,7 @@ export function timeoutOf(x: AbortSignal | { reason?: unknown }, code?: string):
### 各能力如何消费该库
- **web_fetch**:工具层保持校验并转发;提供方手写的 controller + `setTimeout` + 手动监听器 + `finally` + `signal.reason` 恢复被替换为提供方自有的 `deadline`/`timeoutOf`。已预先中止的上游信号仍然立即抛出 `WEB_ABORTED`;否则 `fetch` 使用融合后的 `d.signal` 运行,`translateAbortOrNetwork` 根据信号分类抛出的错误(`timeoutOf` → `WEB_FETCH_TIMEOUT`,否则已中止 → `WEB_ABORTED`,否则网络错误 → `WEB_PROVIDER_ERROR`)。公开的错误码约不变,`TimeoutReason` 永远不会作为公开错误跨越 web seam。
- **web_fetch**:工具层保持校验并转发;提供方手写的 controller + `setTimeout` + 手动监听器 + `finally` + `signal.reason` 恢复被替换为提供方自有的 `deadline`/`timeoutOf`。已预先中止的上游信号仍然立即抛出 `WEB_ABORTED`;否则 `fetch` 使用融合后的 `d.signal` 运行,`translateAbortOrNetwork` 根据信号分类抛出的错误(`timeoutOf` → `WEB_FETCH_TIMEOUT`,否则已中止 → `WEB_ABORTED`,否则网络错误 → `WEB_PROVIDER_ERROR`)。公开的错误码约不变,`TimeoutReason` 永远不会作为公开错误跨越 web seam。
- **bash**`resolve()` 将请求钳位为显式规格。前台 `run()` 创建 deadline 并将其信号传给进程执行,后者既有的 abort 监听器执行进程组 kill。执行器将首个 abort 分类为超时或取消。后台启动保持无超时,仅转发上游取消。
- **LLM(大语言模型)适配器**:`dsh-llm-deepseek` 和 `dsh-llm-pi-ai` 用 `idleWatchdog` 包装实际的传输迭代。配置的五分钟间隔只覆盖尚未结算的提供方 demand,不包括下游消费方在分片之间花费的时间。稳定信号在整个调用期间传给 `fetch` 或 SDK,因此超时会关闭底层请求并映射为 `TIMEOUT`,而更早的调用方中止映射为 `ABORTED`。
@@ -100,7 +100,7 @@ export function timeoutOf(x: AbortSignal | { reason?: unknown }, code?: string):
- `SpawnSpec.timeoutMs` 和 `SpawnOutcome.timedOut`/`aborted` 被移除,而非作为始终为零/始终为 false 的残余保留:由于 `runBash` 不再拥有定时器且执行器负责分类,这些字段无处被读取。这是与字面提案形状(向 `runBash` 传入 `timeoutMs: 0`)的唯一偏差;一个始终为 0 且无处读取的字段在逐文件覆盖率门禁下属于死代码。
- web_fetch 去除了其定制的 controller/timer/listener/reason-recovery;分类器现在基于 deadline 信号(`timeoutOf` + `aborted`)而非抛出错误的形状来判断,这在请求阶段的 reject-with-reason 和读取阶段的裸 `AbortError` 两种情况下都是健壮的。
- `AbortSignal.any` 和 `using`/`Symbol.dispose` 在此首次进入本仓库(Node ≥ 24 基线,已满足)。
- 模型流现在共享一个可重启的定时器约,不会把滑动的空闲间隔变成总调用截止时间,也不会计入消费方思考时间。该原语仍然只做通知;适配器测试证明其传输观察到稳定信号并终止。
- 模型流现在共享一个可重启的定时器约,不会把滑动的空闲间隔变成总调用截止时间,也不会计入消费方思考时间。该原语仍然只做通知;适配器测试证明其传输观察到稳定信号并终止。
以下内容不在本次范围内,列出以标明边界:`web_search` 可以在其工具 schema 和快照覆盖规划完成后获得可选的面向模型的 `timeout_ms`;未来基于 ripgrep 的文件系统发现工具可以在实现后消费同样的提供方自有 deadline 形状;`tools/execute` waterfall(瀑布式事件)中间件可以通过驱动 `exec.signal` 为每次工具调用设置默认 deadline——那将是一个*消费*本库的插件,仍然只做通知,硬终止仍是各能力自己的事。
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-06-tool-result-retention-library.md
2026-07-06-tool-result-retention-library.md: 5e42660360e5a23b419c75b9c8006bec459bc322
2026-07-06-tool-result-retention-library.zh.md: 422c301ba5f1aab33a54e1deffe42f390bcb662d
2026-07-06-tool-result-retention-library.zh.md: 2a523e2eff36daf9100ac10bf5ae569eca30f830
@@ -95,7 +95,7 @@ type TextRetentionStrategy =
### 工具映射
`read` 被有意排除在 v1 保留库之外。它的 `read-render` 辅助函数拥有文件专用的分页约:`offset``limit`、行号、`totalLines`、offset 越界错误、逐行预览截断,以及能够在窗口中途停止扫描的所选输出字节上限。这是行窗口渲染器,不是通用保留原语。它未来可以共享中性的提示辅助函数,但不应把已经选定的窗口再传入 `ItemRetainer`。
`read` 被有意排除在 v1 保留库之外。它的 `read-render` 辅助函数拥有文件专用的分页约`offset``limit`、行号、`totalLines`、offset 越界错误、逐行预览截断,以及能够在窗口中途停止扫描的所选输出字节上限。这是行窗口渲染器,不是通用保留原语。它未来可以共享中性的提示辅助函数,但不应把已经选定的窗口再传入 `ItemRetainer`。
下文的 `FsGlobEntry` 与 `FlatGrepMatch` 是预期由发现工具使用的条目形态,不是现有保留库的导出。`FsGlobEntry` 是一个由后端派生的路径;`FlatGrepMatch` 是后端将保留匹配项按文件分组之前的一条未分组 grep 匹配。
@@ -138,7 +138,7 @@ const formatGrepNotice = (notice: RetentionNotice): string =>
**已交付内容。** `@deepseek-ai/dsh-retention` 导出 `ItemRetainer`、`TextRetainer`、结果类型(`RetainedItems`、`RetainedText`)、策略类型(`ItemRetentionStrategy`、`TextRetentionStrategy`)、`Omitted`、`PushDecision`、`RetentionNotice`,以及中性的提示辅助函数 `describeOmitted``formatRetentionNotice`,且不依赖 Cordis 或任何工具包。单元测试覆盖具有精确省略计数的条目头部保留、文本头部保留、文本尾部保留、首尾字节保留、零预算、UTF-8 边界处理(2、3、4 字节码位,以及每个裁切位置上的无效起始字节)和未知省略量的措辞。
**已记录但尚未迁移的内容。** `glob`、`grep`、`bash`、`web_fetch` 与 `web_search` 的映射已记录在[包 README](../../../../packages/util/retention/README.md) 中,但本次改动并未把每个工具都迁移到该库;迁移工作刻意留作独立的后续任务。`read` 被明确记录为不在范围内:其 `read-render` 行窗口约(`offset``limit`、`totalLines`、offset 范围错误、逐行预览截断,以及针对所选窗口的字节上限)不属于通用保留,而一个 `Omitted` 计数也无法同时表达行窗口两侧。
**已记录但尚未迁移的内容。** `glob`、`grep`、`bash`、`web_fetch` 与 `web_search` 的映射已记录在[包 README](../../../../packages/util/retention/README.md) 中,但本次改动并未把每个工具都迁移到该库;迁移工作刻意留作独立的后续任务。`read` 被明确记录为不在范围内:其 `read-render` 行窗口约`offset``limit`、`totalLines`、offset 范围错误、逐行预览截断,以及针对所选窗口的字节上限)不属于通用保留,而一个 `Omitted` 计数也无法同时表达行窗口两侧。
**该库维持的边界。** `truncated` 表示 retainer 因预算省略了原本可用的内容,绝不表示上游不完整。工具专用状态,包括 `incomplete`、权限失败、提供方局部失败、跳过二进制文件、bash 落盘路径恢复和无效 UTF-8,均留在工具领域字段中、位于 retainer 之外。未来改动迁移某项工具时,该包的 README 与测试必须证明,除了有意改变的提示措辞外,模型可见的结果文本没有变化。
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.md
2026-07-07-tool-call-timeout-policy.md: b85a4d2d60ef60d9f75bd47d4d405aa55c132af6
2026-07-07-tool-call-timeout-policy.zh.md: e67b9e4223f486208061f3944369cf3ec06fe72d
2026-07-07-tool-call-timeout-policy.zh.md: f6a85c13a66689c48b10f160890ce3825125aec1
@@ -52,7 +52,7 @@ catch 是基础 `next`(而非 waterfall 之外的东西)这一点至关重
searchTimeoutMs: 30000
```
超时放在工具定义上而非自由文本名称映射中,消除了拼错名称导致策略不生效的问题。`defineTool` 校验预算为正有限数。分发期间,执行器派生截止信号并将其赋给 `exec.signal`;注册表依据[工具取消](2026-07-19-cooperative-tool-cancellation.md),在执行工具体之前将该截止信号与调用方的原始信号融合。执行器随后恢复调用方信号,并将自身的超时转换为 `TOOL_TIMEOUT`;没有预算的工具原样通过。
超时放在工具定义上而非自由文本名称映射中,消除了拼错名称导致策略不生效的问题。`defineTool` 校验预算为正有限数。分发期间,执行器派生截止信号并将其赋给 `exec.signal`;注册表依据[工具取消约](2026-07-19-cooperative-tool-cancellation.md),在执行工具体之前将该截止信号与调用方的原始信号融合。执行器随后恢复调用方信号,并将自身的超时转换为 `TOOL_TIMEOUT`;没有预算的工具原样通过。
信号替换采用**就地修改 `exec.signal`** 的方式,而非向 `next()` 传递新对象。Cordis 的 waterfall `next()` 忽略传入的任何参数,并以共享的 payload 数组重新调用下游监听器(`vendor/cordis/src/events.ts`),因此修改共享对象是包装器向注册表提供截止信号的方式。注册表会在进入工具体前再次融合已捕获的调用方信号;插件则在 `finally` 中将 `exec.signal` 恢复为调用方的原始值,使 `tools/post-execute` 永远不会看到本插件的截止信号。
@@ -71,7 +71,7 @@ function toolTimeoutResult(timeoutMs: number): ToolExecutionResult {
}
```
这是一个协作式截止。它不会通过竞争工具 promise 来杀死任意工作;工具或其调用的能力必须遵循 `exec.signal` 并达到完全停稳。因此声明 `timeoutMs` 意味着「此工具与 `exec.signal` 协作」,插件 README 将此作为其约。
这是一个协作式截止。它不会通过竞争工具 promise 来杀死任意工作;工具或其调用的能力必须遵循 `exec.signal` 并达到完全停稳。因此声明 `timeoutMs` 意味着「此工具与 `exec.signal` 协作」,插件 README 将此作为其约
无需新的会话事件来保证可重建性:`TOOL_TIMEOUT` 是该调用的最终面向模型的 `tool/result`,因此现有会话日志已经记录了下一次模型请求所见的内容和结构化 `{ name, code }` 错误。
@@ -107,8 +107,8 @@ function toolTimeoutResult(timeoutMs: number): ToolExecutionResult {
## 后果
- `@deepseek-ai/dsh-tools` 在有意拆分 pre/post 工具钩子的拦截 seam 之后,获得了一个环绕分发接口。其约是狭窄的——包装注册表分发,而非替代 pre 门禁或 post 结果策略——且基础 `next()` 是带规范化的分发,因此包装器永远不会看到未经处理的工具异常。
- `@deepseek-ai/dsh-tools` 在有意拆分 pre/post 工具钩子的拦截 seam 之后,获得了一个环绕分发接口。其约是狭窄的——包装注册表分发,而非替代 pre 门禁或 post 结果策略——且基础 `next()` 是带规范化的分发,因此包装器永远不会看到未经处理的工具异常。
- 多个 `tools/execute` 监听器按普通 Cordis waterfall 顺序组合:调用 `next()` 的监听器包装下游监听器加分发;不调用 `next()` 直接返回的监听器短路它们。一个同时组合超时与未来重试/沙箱/指标包装器的部署通过注册顺序选择语义(「超时覆盖整个重试」vs「超时覆盖每次尝试」)。
- 按声明加入是一个有意的误配置风险:工具可以声明 `timeoutMs` 但不遵循 `exec.signal`,这样的工具在超时时不会停止。注册表会等待这个尚未完全停稳的工具体结束,而不是与它竞速;同时插件约声明:声明预算意味着协作;web 工具在已转发信号的工具上验证了这一模式。
- 按声明加入是一个有意的误配置风险:工具可以声明 `timeoutMs` 但不遵循 `exec.signal`,这样的工具在超时时不会停止。注册表会等待这个尚未完全停稳的工具体结束,而不是与它竞速;同时插件约声明:声明预算意味着协作;web 工具在已转发信号的工具上验证了这一模式。
- 过渡期间 `bash` 和已迁移的 web 工具有意使用不同的超时路径:`TOOL_TIMEOUT` 是面向模型的工具调用预算,而 `BASH_TIMEOUT` 仍是 bash 和钩子使用的 bash 后端超时。
- 与字面提案的偏差,按 implemented-Agent Note 规则记录:插件包为 `@deepseek-ai/dsh-timeout-policy`(而非 `tool-timeout`);信号替换是在 `next()` 之前就地修改 `exec.signal`(而非 `next({ ...exec, signal })`,Cordis 会忽略后者);逐工具预算声明在 `ToolDefinition` 上(`timeoutMs`,由拥有该工具的插件从其配置中设置),而非在本插件配置中按工具名映射——因此执行器是零配置的,拼错工具名不可能发生。以上三点均在上文 `## Decision` 中描述。
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md
2026-07-08-tool-output-spill-files.md: 7c0ca90452645d251559be25108d12883210d00e
2026-07-08-tool-output-spill-files.zh.md: a1bde1131374cd11215006b822dfc19aea707747
2026-07-08-tool-output-spill-files.zh.md: 628023a9d12fec17d32749f9d17a3b701ca3a475
@@ -10,7 +10,7 @@ Status: implemented
这项改动之前的行为并不一致。`dsh-bash-local` 已经会在内存尾部溢出时,把完整 stdout/stderr 流写入私有的临时落盘文件;普通文本工具结果则仍以内联形式返回,除非工具自行实现上限。[工具结果保留库](2026-07-06-tool-result-retention-library.md)负责预览机制,但不负责存储,也不负责把这些机制应用于最终工具结果的执行流水线策略。
其形态与超时策略设计一致:工具作者声明规范值与 Native renderer(原生渲染器),由策略插件在渲染后的内容上执行部署默认的上下文预算。工具仍可在提供方采集上限处提前落盘;由工具负责的展示落盘可以保留已完整采集的规范值,而只替换展示内容。[规范工具输出](2026-07-20-canonical-tool-output-contract.md)规定了这项区分。
其形态与超时策略设计一致:工具作者声明规范值与 Native renderer(原生渲染器),由策略插件在渲染后的内容上执行部署默认的上下文预算。工具仍可在提供方采集上限处提前落盘;由工具负责的展示落盘可以保留已完整采集的规范值,而只替换展示内容。[规范工具输出约](2026-07-20-canonical-tool-output-contract.md)规定了这项区分。
## 决策
@@ -164,7 +164,7 @@ ctx.tools.register(defineTool({
## 测试
- `dsh-spill` 单元测试锁定 seam 约:注册为 `ctx.spillStore`、每个上下文只允许一种实现,并在 dispose(资源释放)时释放。
- `dsh-spill` 单元测试锁定 seam 约:注册为 `ctx.spillStore`、每个上下文只允许一种实现,并在 dispose(资源释放)时释放。
- `dsh-spill-local` 单元测试覆盖 `saveText`、`encodeSegment` 清理(分隔符/波浪号/完整路径段的点/空值)、会话哈希目录、仅所有者权限、每次保存生成不同路径、配置根目录/私有根目录,以及存储失败时的拒绝。
- `dsh-spill-policy` 单元测试通过 `ctx.tools.execute` 驱动真实工具:禁用模式下无操作、替换超大文本、小结果/非文本结果保持不变、跳过 `read`、尽力回退(保存失败/无后端/无所有者),以及下游组合(限制已替换结果、保留 `additionalContexts`)。
- `dsh-tool-web` 集成测试驱动 `web_fetch`,其实际执行路径经过 `ctx.tools.execute`,并使用真实的 `spill-local` 后端与策略;测试证明只有刻意加入的落盘提示会改变模型可见文本,而落盘文件保存完整的格式化结果。
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md
2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md: 6fbd5e2c9d57da3f25c72c652ca50eb45b84323c
2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md: c46b3eeeb446165192ef44de938c9a5a657a02f8
2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md: 8a480999038cf892842290e3ea3902683d87e431
@@ -22,9 +22,9 @@ Compact-basic 会在每个拟议请求之前包装 `agent/pre-step`。在续步
### 请求恢复只覆盖最终模型边界
`agent/request-error` 表示来自最终适配器边界的终止失败。适配器选择、分发、iterator 构造与迭代抛出会在 agent loop 消费前成为终止 `error``aborted` finish;适配器直接发出的终止 finish 进入同一路径。提示词装配、请求 middleware、请求日志、结果处理、工具、step 监听器与清理仍属于普通失败。[LLM 流的终止失败](2026-07-29-terminal-llm-stream-failures.md)规定这一规范化边界。
`agent/request-error` 表示来自最终适配器边界的终止失败。适配器选择、分发、iterator 构造与迭代抛出会在 agent loop(智能体循环)消费前成为终止 `error``aborted` finish;适配器直接发出的终止 finish 进入同一路径。提示词装配、请求 middleware、请求日志、结果处理、工具、step 监听器与清理仍属于普通失败。[LLM(大语言模型)流的终止失败](2026-07-29-terminal-llm-stream-failures.md)规定这一规范化边界。
恢复运行前,失败 step 已经关闭。负责处理的监听器修复持久状态、返回 `{ kind: 'retry' }`,并停止 waterfall 委托。循环随后关闭失败 turn,并从持久日志开启一个重试 turn,中间不发布空闲通知。重试策略与尝试计数由插件自己拥有;compact-basic 在链路到达终态 `agent/settled` 时清除对应 agent 的溢出计数。两个 DeepSeek 适配器都把识别出的提供方上下文限制错误规范化为 `CONTEXT_WINDOW_EXCEEDED`。[重试动作决策](../simplification/2026-07-27-request-error-retry-action.md)规定这一返回边界。
恢复运行前,失败 step 已经关闭。负责处理的监听器修复持久状态、返回 `{ kind: 'retry' }`,并停止 waterfall(瀑布式事件)委托。循环随后关闭失败 turn,并从持久日志开启一个重试 turn,中间不发布空闲通知。重试策略与尝试计数由插件自己拥有;compact-basic 在链路到达终态 `agent/settled` 时清除对应 agent 的溢出计数。两个 DeepSeek 适配器都把识别出的提供方上下文限制错误规范化为 `CONTEXT_WINDOW_EXCEEDED`。[重试动作决策](../simplification/2026-07-27-request-error-retry-action.md)规定这一返回边界。
如果取消发生在 assistant 工具调用已经持久化之后、所有调用完成分发之前,循环会为每个尚未分发的调用记录一对合成的 `tool/call` 与 aborted `tool/result`,随后进入正常中止路径。因此,表层不会仅因取消赢得竞态而留下孤立的持久工具调用。
@@ -42,7 +42,7 @@ Compact-basic 会在每个拟议请求之前包装 `agent/pre-step`。在续步
## 测试
单元测试覆盖最终适配器规范化边界、已关闭 turn 的重试编号与重置、取消与销毁、step 边界顺序、已路由信封压力、压力门控剪枝、剪枝独立解除压力、从已剪枝输入生成摘要、平衡溢出缩减、后续失败前已落盘的剪枝进展、generation 证明、上限、委托与辅助调用路由。真实循环测试覆盖抛出式和带内溢出,并验证剪枝或摘要压缩后重试请求从替换表层重建
单元测试覆盖最终适配器规范化边界、已关闭 turn 的重试编号与重置、取消与销毁、step 边界顺序、已路由信封压力、压力门控剪枝、剪枝独立解除压力、从已剪枝输入生成摘要、平衡溢出缩减、后续失败前已落盘的剪枝进展、generation 证明、上限、委托与辅助调用路由。真实循环测试覆盖抛出式和带内溢出在经剪枝或摘要压缩后重建重试请求的过程
## 考虑过的替代方案
@@ -58,4 +58,4 @@ Compact-basic 会在每个拟议请求之前包装 `agent/pre-step`。在续步
代价是在共享 pre-step waterfall 中执行压力工作,并需要适配器持续维护溢出分类。提供方措辞与启发式字符密度仍是维护风险。表层压缩依然无法修复仅信封本身就超出窗口的情况,也不能拆分不可分割的非工具节点,或修复非可剪枝剩余部分仍然过大的工具单元。若可移除的文本工具结果是主要体积,可选剪枝器仍可修复原本不可分割的工具配对。
[已领取 pre-step 生命周期](2026-07-31-claimed-pre-step-inbox-lifecycle.md)取代了本记录原先的 post-step 触发方式。服务拆分、独立 token meter、平衡范围约、日志记录锁、摘要替换与唯一 `summarize()` 子类 hook 均保持不变。
[已领取 pre-step 生命周期](2026-07-31-claimed-pre-step-inbox-lifecycle.md)取代了本记录原先的 post-step 触发方式。服务拆分、独立 token meter、平衡范围约、日志记录锁、摘要替换与唯一 `summarize()` 子类 hook 均保持不变。
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md
2026-07-10-single-file-executable-sdk-runtime-distribution.md: fe09e1b83f4f0a9f867f78351f643b1bd26d7c9d
2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: ee49d7ea8fade9d3e323e8c6921479b8053809af
2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: 0279e0cf032928e96bad9da02340cae753a0927f
@@ -27,7 +27,7 @@ exe 使用 [@yao-pkg/pkg](https://github.com/yao-pkg/pkg)vercel/pkg 归档后
确定性协议实现(`server.ts` / `transport.ts`)按 `acp/acp` + `examples/acp-demo` 的既有模式落为两包——对外服务接口本身也是插件:
- [`packages/scaffold/server`](../../../../packages/scaffold/server/README.md)`@deepseek-ai/dsh-jsonrpc`):纯协议插件;执行 `apply` 时,在进程 stdio 上挂载 `HarnessSdkServer` 与按行传输的 JSON-RPC 层,资源释放走 `ctx.effect()`。是否提供服务由 `cordis.yml` 决定;未挂载该插件的配置会启动一个不提供此服务的合法进程。协议级退出归插件所有(应答并刷新 `shutdown` 响应后 dispose 根运行时以排空持久化,再调用 `exit(0)`;HMR 式卸载只停止服务,不退出进程)。
- [`packages/scaffold/server`](../../../../packages/scaffold/server/README.md)`@deepseek-ai/dsh-jsonrpc`):纯协议插件;执行 `apply` 时,在进程 stdio 上挂载 `HarnessSdkServer` 与按行传输的 JSON-RPC 层,资源释放走 `ctx.effect()`。是否提供服务由 `cordis.yml` 决定;未挂载该插件的配置会启动一个不提供此服务的合法进程。协议级退出归插件所有(应答并确保 `shutdown` 响应发送完毕后,对根运行时执行 dispose(资源释放),让待处理的持久化操作完成,再调用 `exit(0)`;HMR 式卸载只停止服务,不退出进程)。
- [`packages/examples/jsonrpc-demo`](../../../../packages/examples/jsonrpc-demo/README.md)`@deepseek-ai/dsh-jsonrpc-demo`):轻量应用入口——`installFailLoud` + `loadEnv` + 配置发现 + [`dsh-app-boot`](../../../../packages/boot/app-boot/src/index.ts) 的 `boot()``boot()` 完成后入口即完成,服务器由 `cordis.yml` 中的 `dsh-jsonrpc` 条目启动。它只依赖 `app-boot`。进程级退出归 `bin` 所有(stdin EOF/SIGTERM → dispose 后返回 0SIGINT → 130)。
配置发现有两个通道,均缺失时立即报错:优先使用 `DSH_CORDIS_CONFIG` 环境变量(SDK 客户端约定),其次使用 argv 位置参数;没有默认路径或内置回退——「实际启动的插件由外部 `cordis.yml` 决定」是硬语义。
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md
2026-07-12-agent-scope-runtime-design.md: b93c291957fab98ab9d0d04eb816bb01a15a0b51
2026-07-12-agent-scope-runtime-design.zh.md: 2dbdcab2ce3a7f92bf537d93a65dfe44374ec07c
2026-07-12-agent-scope-runtime-design.zh.md: e0c01f31eb2664608139f95dc60025a5b33e4618
@@ -1,4 +1,4 @@
# Agent Note: agent 作用域运行时设计与正确性
# Agent Note: Agent 作用域运行时设计与正确性
Status: implemented
@@ -6,7 +6,7 @@ Status: implemented
## 问题
[agent(智能体)作用域约定](2026-07-08-agent-scope-contexts.md)对贡献者而言很简单:通过 `agent.ctx` 注册,解析出一个全局加单 agent 的视图,仅在 setup 完成后发布,并保持作用域直到工作停止。运行时必须在协作式插件框架、异步创建、可重入监听器、持久化会话提交以及 worker 或进程故障等场景下维护这份约定。
[agent 作用域约定](2026-07-08-agent-scope-contexts.md)对贡献者而言很简单:通过 `agent.ctx` 注册,解析出一个全局加单 agent 的视图,仅在 setup 完成后发布,并保持作用域直到工作停止。运行时必须在协作式插件框架、异步创建、可重入监听器、持久化会话提交以及 worker 或进程故障等场景下维护这份约定。
主要的设计风险是为每个竞态条件引入第二套机制。独立的预留、就绪哨兵、取消中继、快照层和保护注册表可能镜像同一个事实,直到没有读者能分辨哪个才是权威的。这些机制还会诱使运行时把可信的类型化调用当作敌对的序列化边界来处理。
@@ -23,9 +23,9 @@ Status: implemented
| 选择全局加某个 agent 的注册 | 不透明作用域键、路由载体与共享 layer store |
| 拥有一个活跃的 agent 或会话 | 由其 disposer 捕获的单条注册表条目 |
| 协调创建/恢复 | 单个 `AgentCreationTransaction` |
| 保护持久化、队列、模型或协议格式wire format数据 | 在该边界处一次性物化 |
| 保护持久化、队列、模型或协议格式数据 | 在该边界处一次性物化 |
| 在同一进程内传递类型化值 | Readonly 借用约定 |
| 组合模型可见的提示词与工具表面 | 单个共享工具视图加 assembly waterfall(瀑布式事件)的权威结果 |
| 组合模型可见的提示词与工具表面 | 单个共享工具视图加权威的 assembly-waterfall 结果 |
| 协调 subagent、worker 和进程关闭 | 单个取消信号加该边界独立的终止态/完全停稳态事实 |
本 Agent Note 余下部分按依赖顺序展开这些选择:Cordis 机制、作用域路由、创建与会话提交、工具与提示词、subagent 与工作流,最后是可执行检查。
@@ -34,7 +34,7 @@ Status: implemented
## Cordis 模型:上下文、fiber、effect、receiver 与 waterfall
理解实现需要五个 Cordis 概念。上下文选择服务和注册所有权;fiber 是一个活跃的插件或子生命周期;effect 将清理逻辑附加到 fiber;事件接收器选择监听器;waterfall 让监听器按顺序变换或否决一个操作。
理解实现需要五个 Cordis 概念。上下文选择服务和注册所有权;fiber 是一个活跃的插件或子生命周期;effect 将清理逻辑附加到 fiber;事件接收器选择监听器;waterfall(瀑布式事件)让监听器按顺序变换或否决一个操作。
### 上下文是贯穿单个服务图的所有权路径
@@ -56,7 +56,7 @@ Cordis 使用 dispatch receiver`this`)过滤监听器,而 harness 的监
因此,产品辅助函数构造载体并单独传递领域主体。这防止监听器路由变成另一套对象模型,并使事件签名在不了解载体内部的情况下也可理解。
Cordis waterfall 是中间件风格的 dispatch。每个监听器接收 `next()`:调用它则委托给剩余监听器和基础操作,不调用则否决或替换下游结果。waterfall 驱动提示词组装和工具策略;普通 emit 事件同步通知,parallel 事件等待所有监听器但没有否决结果。
Cordis waterfall 是中间件风格的 dispatch。每个监听器接收 `next()`:调用它则委托给剩余监听器和基础操作,不调用则否决或替换下游结果。Waterfall 驱动提示词组装和工具策略;普通 emit 事件同步通知,parallel 事件等待所有监听器但没有否决结果。
## 作用域路由:一个不透明键选择一层
@@ -82,7 +82,7 @@ Receiver 是一个小型载体而非领域对象的透明代理。需要 agent
类型标记拒绝普通的裸 receiver 误用,开发环境不变式覆盖直接 JavaScript 或强制转换的 dispatch。主体保持显式,因为路由正确性和有用的事件数据是不同的关注点。
## agent 创建:一个事务拥有完整操作
## Agent 创建:一个事务拥有完整操作
创建和恢复是一个具有多个阶段的异步生命周期,而非多个生命周期。`AgentCreationTransaction` 拥有调用方和工厂的活跃性、可选取消、私有资源、发布、回滚,以及每个所有者观察到的记忆化拆除。
@@ -120,7 +120,7 @@ Setup 接收完整的子上下文,可以等待插件激活。它可以注册
6. 发射 `agent/session-start`
7. 启动 driver。
agent 在两个注册表和创建通知都达成一致之前绝不驱动。同步监听器可以否决或 dispose 一个所有者;事务记录发布进行中,并等待该回调栈展开后再继续拆除。每个已开始的创建宣告在回滚期间都有匹配的销毁宣告。
Agent 在两个注册表和创建通知都达成一致之前绝不驱动。同步监听器可以否决或 dispose 一个所有者;事务记录发布进行中,并等待该回调栈展开后再继续拆除。每个已开始的创建宣告在回滚期间都有匹配的销毁宣告。
以下序列图隔离了非显而易见的竞态:同步创建监听器可以在发布调用栈仍拥有两个注册表条目时请求 dispose。拆除必须立即停用,但要等待该栈展开后才停止和分离任何东西。
@@ -172,7 +172,7 @@ Session 头部、种子和追加的事件是无损 JSON 数据。Session 构造
追加遵循一个序列:
1. 物化持久化事件和呈现意图。
1. 物化持久化事件和表面意图。
2. 取得 SessionEntry 的独占所有权,并拒绝该注册表条目上的重入追加。
3. 解析作用域回调并运行内部不变式验证。
4. 恰好推送一次;这是提交点。
@@ -196,7 +196,7 @@ Session 头部、种子和追加的事件是无损 JSON 数据。Session 构造
| 持久化会话或持久化数据 | 在提交前物化并验证 |
| Worker、进程或协议格式消息 | 序列化、验证并拥有解码后的值 |
测试中构造恶意 getter、在交接后替换类型化回调、或强制转换伪造服务对象的做法本身不定义生产约定。运行时在数据跨越解析器、队列、模型、持久化、文件、worker、进程或协议格式边界时保留检查,并在可信进程内依赖 readonly 类型加插件纪律。
测试中构造恶意 getter、在交接后替换类型化回调、或强制转换伪造服务对象的做法本身不定义生产约定。运行时在数据跨越解析器、队列、模型、持久化、文件、worker、进程或协议格式wire format边界时保留检查,并在可信进程内依赖 readonly 类型加插件纪律。
回调隔离与数据所有权是分开的。监听器是任意扩展代码,即使其参数是可信的也可能抛出异常;发布和提交后路径仍按其事件约定隔离失败。
@@ -206,7 +206,7 @@ Session 头部、种子和追加的事件是无损 JSON 数据。Session 构造
### 一个解析器定义工具视图
私有解析器应用当前展示模式、活跃的全局限制、精确的局部叠加和局部遮蔽。schema、查找、执行、Code Mode SDK 生成和限制验证都使用该解析器或其限制前的全局名称视图。
私有解析器应用当前展示模式、活跃的全局限制、精确的局部叠加和局部遮蔽。Schema、查找、执行、Code Mode SDK 生成和限制验证都使用该解析器或其限制前的全局名称视图。
[subagent 组合控制 Agent Note](../feature/2026-07-12-subagent-persona-tool-filter-and-depth.md#tool-filtering-is-one-live-global-view-rule)拥有用户可见的 allow/deny 语义。实现要求是一致性:被过滤掉的全局工具不能通过另一条查找路径仍可执行,局部遮蔽的定义就是被展示和执行的同一个定义。
@@ -238,7 +238,7 @@ Scope 直接解决了真正的隔离问题。结构化输出贡献注册在子
对于 Code Mode SDK 调用,内层成功结果记录 `{ parentToken, value }` 而非提交。观察者等待 token 匹配 `parentToken``run_code` 执行,仅在该外层最终结果也成功时才提交。程序失败、运行时中止或外层 post-policy 拒绝会丢弃待定值。
一旦值处于待定或已提交状态,作用域单调守卫拒绝后续工具调用。成功的结构化输出执行会调用 `exec.concludeTurn()`,因此其自身不可变结果携带 `concludesTurn: true`,循环在该步骤结束工具循环。schema 验证失败仍然是普通的 `INVALID_ARGS` 工具错误,子级可以在同一轮次内重试。
一旦值处于待定或已提交状态,作用域单调守卫拒绝后续工具调用。成功的结构化输出执行会调用 `exec.concludeTurn()`,因此其自身不可变结果携带 `concludesTurn: true`,循环在该步骤结束工具循环。Schema 验证失败仍然是普通的 `INVALID_ARGS` 工具错误,子级可以在同一轮次内重试。
纯 Code Mode 的注册表贡献从原生 wire schema 中省略 `structured_output`,并通过生成的 SDK 暴露它。Assembly waterfall 可以有意改变该展示;执行仍然针对子作用域定义进行验证,监听器拥有其创建的任何替代模型可见路由的一致性。
@@ -252,13 +252,13 @@ Scope 直接解决了真正的隔离问题。结构化输出贡献注册在子
| 工具结果 | 观察不可变的已提交结果 | 结构化输出必须仅提交实际逃出流水线的结果 |
| 轮次 continuation | 通过已提交工具结果终止 | 已提交的终端输出必须结束轮次 |
`ToolGuard` 是单调策略注册表。已提交的工具观察是上述被隔离的 `tools/result` 点。终端结构化输出在自身执行上标记 `concludesTurn`,因此终止性成为权威结果上的数据,而不是独立钩子决策。
`ToolGuard` 是单调策略注册表。已提交的工具观察是上述被隔离的 `tools/result` 点。终端结构化输出在自身执行上标记 `concludesTurn`,因此终止性成为权威结果上的数据,而不是独立 hook 决策。
### skill(技能)和 approval 服务信任类型化调用方
### Skill 和 approval 服务信任类型化调用方
skill 注册表定义和 approval 策略是 readonly 的同进程约定。它们的服务不克隆回调对象,也不防御交接后的回调替换。
Skill 注册表定义和 approval 策略是 readonly 的同进程约定。它们的服务不克隆回调对象,也不防御交接后的回调替换。
skill 仍然验证外部 skill 文件和解析的提供方输出,通过调用 agent 的工具视图路由目录,并精确 dispose 注册。Approval 仍然解析策略、观察取消、按 `request.agent` 路由 `approval/request`、记录持久化审计对,并隔离应答者和提交后观察者的失败。
Skill 仍然验证外部 skill 文件和解析的提供方输出,通过调用 agent 的工具视图路由目录,并精确 dispose 注册。Approval 仍然解析策略、观察取消、按 `request.agent` 路由 `approval/request`、记录持久化审计对,并隔离应答者和提交后观察者的失败。
## Subagent:发布即 start promise
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-12-scoped-layers-store.md
2026-07-12-scoped-layers-store.md: c5186d1652bca617eed62ec02937f2d055ea727c
2026-07-12-scoped-layers-store.zh.md: fbe95ab6c7b3391a70e34b83889bf9f8adeb6ba1
2026-07-12-scoped-layers-store.zh.md: f5084426beef836d71e55c0898ec1edbdc7725f3
@@ -16,7 +16,7 @@ agent(智能体)作用域机制([决策](2026-07-08-agent-scope-contexts.m
- change 回调运行前必须收集 undo,抛错的回调才能回滚变更。
- 公开 disposer 必须就是 `ctx.effect()` 返回的那个函数;包装它会破坏 Cordis 基于身份的有序拆除。
共享的是生命周期与保持插入顺序的存储,而不是注册表策略。工具限制、保留传输处理、提示词求值时机、命令规范化、精确诊断和回调异常隔离,仍分别属于不同的领域约。
共享的是生命周期与保持插入顺序的存储,而不是注册表策略。工具限制、保留传输处理、提示词求值时机、命令规范化、精确诊断和回调异常隔离,仍分别属于不同的领域约
## 决策
@@ -70,7 +70,7 @@ export class AnonymousEntries<V> {
}
```
## 存储
## 存储约
- 构造器只创建一次 `global`,调用的是 `createLayer(undefined)`。只有 `effect()` 会创建专属层;`peek()` 和 `merge()` 从不创建专属层,而 `peek(undefined)` 返回 `undefined`,因为全局层已经显式存在。
- `merge()` 是唯一会物化结果的通用读取接口。它按插入顺序复制全局命名条目,再按专属条目的插入顺序应用这些条目;同名条目完成遮蔽,但不会移动无关名称。
@@ -111,11 +111,11 @@ export class AnonymousEntries<V> {
## 后果
- 支持作用域的注册表各自通过一个聚合层表达状态,并复用相同的构造、属主、回滚、通知和回收编排。各注册表仍各自保有领域特有的校验、诊断、过滤、求值和观察者策略。
- 公开读取接口保持狭窄:直接遍历条目表可保留显式的活语义,`merge()` 是唯一共享的物化遮蔽操作。异构的 `ScopeLayer` 不具备整层 `values()` 约。
- helper 刻意保持同步。未来的登记若需要异步 setup 或多份分别拥有属主的 undo,必须先明确属主与 settlement 边界,再拓宽这项约。
- action 必须在保留贡献前抛错,或者为自己保留的一切返回 undo;helper 无法修复超出这项约的变更。提供的条目操作是原子的,迁移后的注册表会在插入前执行可能失败的校验。
- 公开读取接口保持狭窄:直接遍历条目表可保留显式的活语义,`merge()` 是唯一共享的物化遮蔽操作。异构的 `ScopeLayer` 不具备整层 `values()` 约
- helper 刻意保持同步。未来的登记若需要异步 setup 或多份分别拥有属主的 undo,必须先明确属主与 settlement 边界,再拓宽这项约
- action 必须在保留贡献前抛错,或者为自己保留的一切返回 undo;helper 无法修复超出这项约的变更。提供的条目操作是原子的,迁移后的注册表会在插入前执行可能失败的校验。
- 专属层会一直保持已分配状态,直到其聚合内的所有表都为空。因此,销毁一个门面不会丢弃同一 scope 拥有的其他贡献。
- 四个公开符号构成一项可复用的包约。将 `EntryValues` 保持为内部接口,并把消费方策略留在 helper 之外,可以限制兼容性范围。
- 四个公开符号构成一项可复用的包约。将 `EntryValues` 保持为内部接口,并把消费方策略留在 helper 之外,可以限制兼容性范围。
- 迁移不改变任何公开注册表行为,也不改变模型、人类、协议、持久化、配置或依赖图层面的任何输出。
## 验证
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.md
2026-07-15-agent-initiator-scope.md: 2f388ae1de3dd6583686e129a03f0cd76701e18d
2026-07-15-agent-initiator-scope.zh.md: 64f70a4d1c3368d5621edb7251d0b567505affc0
2026-07-15-agent-initiator-scope.zh.md: a0e3638081d875adc2412191829ab84c8dcd697c
@@ -20,21 +20,21 @@ harness 中存在两种有用但不同的上下文概念。Cordis `Context` 负
因此,并发驱动使用彼此独立的存储。子驱动的异步延续携带子 Agent;`withInitiator()` 返回后,调用方立即恢复之前的存储,而活动运行计数仍持续跟踪返回的 Promise,直到其结束。创建、持久化加载和尚未发布的 `setup(agentCtx)` 位于子驱动边界之外:由父 Agent 发起的创建使用父身份,而 `agentCtx.agent` 显式标识子 Agent。
隐式身份不会取代显式约。`ToolExecution.agent``AssembleContext.agent``GenerateOptions.sessionId`、任务归属、父子请求、`ctx.agent``agentCtx.agent`、审批与 hook 主体、`cwd` 选择、取消、worker 和进程消息、持久化记录及协议身份都保持显式传递。远程边界会把所需身份写入类型化请求,因为 ALS 只在进程内有效。
隐式身份不会取代显式约`ToolExecution.agent``AssembleContext.agent``GenerateOptions.sessionId`、任务归属、父子请求、`ctx.agent``agentCtx.agent`、审批与 hook 主体、`cwd` 选择、取消、worker 和进程消息、持久化记录及协议身份都保持显式传递。远程边界会把所需身份写入类型化请求,因为 ALS 只在进程内有效。
`AgentRegistry` 管理一个有序的发起方生命周期。teardown 会先拒绝新边界;移除 `ctx.agents` 后,AgentLoop 等注入方开始排空,注册表随后等待活动的返回 Promise 边界,最后调用 `AsyncLocalStorage.disable()`。如果某个边界继承的异步调用链启动所属 Cordis fiber 的卸载,私有运行标记谱系会从排空范围中释放该嵌套边界链,从而避免 teardown 等待自身完成,同时继续排空无关边界。在普通排空期间,进行中代码可通过保留的服务引用继续调用 `currentInitiator()``requireInitiator()`;dispose(资源释放)后,发起方方法会抛出 `agent initiator scope is disposed`。根 Context dispose 可能并发启动同级 fiber 的 teardown,因此除 Cordis 依赖顺序外仍必须统计活动边界。
发起方作用域不负责管理脱离返回链的工作:注册表排空只跟踪 `withInitiator()``withoutInitiator()` 返回的 Promise。边界内创建的异步资源会继承其存储,直到自身结束或 ALS 被禁用;所属 seam 必须显式停止未纳入返回 Promise 的工作。Agent 所属的前台工作会把完整生命周期纳入返回值,并保留显式取消约。无关的定时器、队列和部署基础设施在 `withoutInitiator(operation)` 下启动;队列、worker、进程和协议边界必须序列化身份,不能期待 ALS 传播。
发起方作用域不负责管理脱离返回链的工作:注册表排空只跟踪 `withInitiator()``withoutInitiator()` 返回的 Promise。边界内创建的异步资源会继承其存储,直到自身结束或 ALS 被禁用;所属 seam 必须显式停止未纳入返回 Promise 的工作。Agent 所属的前台工作会把完整生命周期纳入返回值,并保留显式取消约。无关的定时器、队列和部署基础设施在 `withoutInitiator(operation)` 下启动;队列、worker、进程和协议边界必须序列化身份,不能期待 ALS 传播。
宿主感知的传输层可以从 `ctx.agents.requireInitiator().session.id` 推导由部署方拥有的 `X-Harness-Session-Id` 等请求头;模型可见 schema 和参数中不包含该请求头。本决策不让现有生产 MCP 或 Web 传输层采用此请求头。测试替身传输层用于证明可信边界,而不会把宿主路由策略分配给现有的提供方无关 seam。
本决策扩展 [Agent 注册作用域](2026-07-08-agent-scope-contexts.md)及其[运行时设计](2026-07-12-agent-scope-runtime-design.md),不会改变其中 `agent.ctx` 的静态含义。
本决策扩展 [Agent 注册作用域约](2026-07-08-agent-scope-contexts.md)及其[运行时设计](2026-07-12-agent-scope-runtime-design.md),不会改变其中 `agent.ctx` 的静态含义。
## 验证
Agent 服务测试锁定可选与必需读取、同步值及跨 realm Promise 的精确身份、内建 Promise 结束状态观察、并发、嵌套及清空边界、同步抛错或 Promise 拒绝后的恢复、普通与重入排空顺序及保留引用的错误。AgentLoop 集成测试锁定并发与嵌套驱动、无 Agent 调用、AgentRegistry 重启、根 Context 销毁,以及包内私有的循环和工具调度通过隐式查找完成。组合、模块图、构建及运行时闭包检查确保默认组合包、SDK 主干、Python 运行时闭包及直接 AgentLoop harness 通过 `ctx.agents` 完成接线,无需其他提供方。
测试替身形式的宿主感知传输层在内部推导 `X-Harness-Session-Id`,并验证工具 schema 与日志中记录的参数都不包含身份字段。服务有意不排空边界操作所返回 Promise 之外的异步工作;这类工作仍由所属方的显式停止约管理。
测试替身形式的宿主感知传输层在内部推导 `X-Harness-Session-Id`,并验证工具 schema 与日志中记录的参数都不包含身份字段。服务有意不排空边界操作所返回 Promise 之外的异步工作;这类工作仍由所属方的显式停止约管理。
## 考虑过的替代方案
@@ -46,7 +46,7 @@ Agent 服务测试锁定可选与必需读取、同步值及跨 realm Promise
**保存命名帧或完整运行时帧。** 只有一个字段的 `{ agent }` 帧只是包装该值,而 Agent、会话、inbox、取消、轮次、步骤、工具执行和持久化已经有各自的真源。增加更多字段会产生陈旧快照和另一套生命周期;直接携带 `Agent`,由方法名标识边界,无需重复保存状态。
**包含步骤级 `AbortSignal`、`cwd`、沙箱或授权。** 它们的生命周期及权限范围与驱动边界不一致,而且现有 seam 已经显式传递这些值。新增控制能力需要独立决策和嵌套生命周期约。
**包含步骤级 `AbortSignal`、`cwd`、沙箱或授权。** 它们的生命周期及权限范围与驱动边界不一致,而且现有 seam 已经显式传递这些值。新增控制能力需要独立决策和嵌套生命周期约
**使用进程级 `currentAgent`。** 并发 Agent 和 subagent 会在异步延续执行之间相互覆盖,因此可变全局值只在 harness 不具备的串行保证下才正确。
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.md
2026-07-15-lsp-capability-seam.md: 63cec0a4e349ffc5be27a84e955b15e9168f898b
2026-07-15-lsp-capability-seam.zh.md: cdb1289f6956d8a4e46ed92847ddd8a58e588afa
2026-07-15-lsp-capability-seam.zh.md: fa82e29e0252b612ae38357c363478f1cbad9ead
@@ -8,7 +8,7 @@ Status: implemented
harness 已具备文本搜索与文件读取能力,但二者都无法识别程序符号。文本匹配无法可靠地区分同名函数、跟踪导入别名、关联接口与具体实现,也无法报告推断类型。因此,agent(智能体)在修改代码前缺少人类通过编辑器语言服务器获得的语义导航能力。
语言服务器协议(Language Server ProtocolLSP)支持分属三个职责方:模型需要稳定的查询 schema,harness 需要提供方选择与规范化结果,本地实现则负责进程、JSON-RPC、工作区、同步与文件系统行为。将三者合并会使模型约绑定本地子进程,并阻碍远程或沙箱原生提供方。
语言服务器协议(Language Server ProtocolLSP)支持分属三个职责方:模型需要稳定的查询 schema,harness 需要提供方选择与规范化结果,本地实现则负责进程、JSON-RPC、工作区、同步与文件系统行为。将三者合并会使模型约绑定本地子进程,并阻碍远程或沙箱原生提供方。
许多语言服务器在查询文档已按当前文本打开时表现最佳。兼容的 agent 客户端必须限制这项状态、定义内部读取是否算作模型观察,并确保文档快照与服务器工作区索引位于同一文件系统命名空间。
@@ -32,7 +32,7 @@ harness 已具备文本搜索与文件读取能力,但二者都无法识别程
服务边界只公开 `query(request, signal?)`,因为没有字段需要实现层填充默认值:`workspaceRoot` 是必填项,`languageId` 来自注册映射,超时与结果限制由消费方负责。`query()` 执行选择与推导时不使用隐藏的 `??` 后备逻辑,因此没有需要 resolve 的可执行 spec。`dsh-tool-lsp` 校验模型参数,并只把 `exec.signal` 作为裸 `AbortSignal` 传递,与 web 一致,并使 `dsh-lsp` 不依赖 `dsh-tools`。提供方在选择前被移除时按不可用失败;之后的释放遵循已选提供方的取消生命周期,不改路由。
预期约如下:
预期约如下:
```ts
import type { Branded } from '@deepseek-ai/dsh-brand'
@@ -81,7 +81,7 @@ interface LspService {
`dsh-lsp-local` 负责服务器配置、JSON-RPC、进程与临时文档状态和协议转换。它通过 `ctx.fs` 读取,通过 `ctx.subprocess` 启动,只依赖二者的接口包而非具体提供方;[可移植执行环境决策](2026-07-28-portable-execution-world-consumers.md)负责定义这种配对。服务器表的键是提供方 id。插件在注册前解析每个服务器的本地设置;如果后续映射无效或发生冲突,插件会撤销此前的注册,并为每个提供方保留独立进程池。`dsh-tool-lsp` 在运行时只注入 `tools``lsp``systemPrompt`,通过包内的 `sessionCwd(exec)` 辅助函数从 `exec.agent?.session.header.cwd` 取得工作区,其取值方式与文件系统工具一致,也不导入提供方。
## 面向模型的
## 面向模型的约
单一 `lsp` 工具接受以下参数:
@@ -149,7 +149,7 @@ interface LspToolInput {
**照搬 Claude Code 的统一 schema。** 它的光标操作验证了核心场景,但符号与调用层级需要不同参数。照搬九种操作会固化尚未验证的接口,因此本提案只对齐四种语义查询。
**允许提供方注册工具。** 已加载服务器会控制模型 schema 和提示词,无法在本地与远程提供方之间维持统一约。
**允许提供方注册工具。** 已加载服务器会控制模型 schema 和提示词,无法在本地与远程提供方之间维持统一约
**公开任意 LSP 方法。** JSON-RPC 逃生口会泄露协议载荷,并允许未经评审的变更或命令执行;操作联合保持封闭。
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md
2026-07-16-explicit-turn-cancellation.md: 2b7cb8cc77184edf1331764d28aefe748c1614a1
2026-07-16-explicit-turn-cancellation.zh.md: 68089e2c48d239afbff4c10cba5a202b4b6ff262
2026-07-16-explicit-turn-cancellation.zh.md: 0b9a06c0ae340a41eab9d6bff6126996bfc63bcf
@@ -12,7 +12,7 @@ Status: implemented
## 决策
Agent 拥有仅用于运行时的 `AgentCancelCause` 联合类型 `{ kind: 'user' } | { kind: 'parent' }``agent.cancel()` 默认使用 `user`。TypeScript 在这个类型化的同进程 seam 中强制执行该词汇,不提供运行时校验器、后备行为,也不为无类型调用方提供特殊兼容性约。活跃的 `TurnCancellation` 会把类型化判别字段复制为一个全新且已冻结的 signal 原因;空闲状态下没有可修改的持有者,也不会让后续工作预先进入取消状态。
Agent 拥有仅用于运行时的 `AgentCancelCause` 联合类型 `{ kind: 'user' } | { kind: 'parent' }``agent.cancel()` 默认使用 `user`。TypeScript 在这个类型化的同进程 seam 中强制执行该词汇,不提供运行时校验器、后备行为,也不为无类型调用方提供特殊兼容性约。活跃的 `TurnCancellation` 会把类型化判别字段复制为一个全新且已冻结的 signal 原因;空闲状态下没有可修改的持有者,也不会让后续工作预先进入取消状态。
正在运行的轮次被中断后,以粗粒度的持久化结果 `{ kind: 'aborted' }` 结束。终态事件记录轮次发生了什么,运行时 signal 标识谁请求了取消;回放不会重复保存 `user``parent`。会话 seed/load 会拒绝携带取消原因或任何其他额外字段的旧式中止记录,因此回放无法重新引入由调用方持有的取消细节。仅限进程内的 `agent/cancel-requested` 通知不会持久化;未来若有审计需求,应使用独立的持久化控制请求事件,让请求与最终结果保持为两项事实。持久化事件不包含调用栈、signal、错误对象、自由文本取消原因或后端私有细节。
@@ -20,7 +20,7 @@ AgentLoop 为每个待启动轮次私有地持有一个 `TurnCancellation`。它
对于轮次被认领前已取消的排队工作,驱动器只保留一个不携带取消原因的运行前标记。实际生效的 `cancel()` 会先发出仅供观察的 `agent/cancel-requested` 通知并携带最终确定的类型化取消原因,然后才清除排队工作和 steering(中途引导)工作或中止持有者;通知失败不能阻止此次停止,空闲状态下调用则不发出任何通知。通知观察者同步加入队列的工作也会被这次清除,而稍后由 signal 中止观察者加入队列的工作会被锁存,并在被中止的活动收敛到空闲时执行——`disposed` 取消则将其停放([取消收敛窗口唤醒锁存](../bug-fix/2026-08-07-cancel-convergence-wake-latch.md))。若 `running` 监听器同步取消旧工作并发送替代提示词,驱动器会丢弃已中止的持有者,并为替代提示词创建全新的持有者。同一活跃持有者上的重复取消遵循首次请求优先,后续调用仍可清除新入队的待处理工作。
显式事件签名传递单个 payload 对象:agent 作用域事件在 payload 中携带 `agent``signal``next` 位于最后;其余 seam 保持 `signal` 紧邻 waterfall(瀑布式事件)的最终 `next` 之前。`PreStepContext``RequestFailureContext` 已退役,其字段并入 `agent/pre-step``agent/request-error` 的 payload[payload-object 事件](2026-08-06-agent-event-payload-objects.md))。pre-step 进入决策、请求配置、请求错误恢复、模型生成、工具执行、审批、轮次停止以及 subagent 或工作流请求都会收到当前 signal。钩子桥接器也必须提供 `RunHookOptions.signal`,使轮次取消能够到达 Bash 执行器终止进程组并等待其退出的边界。`SystemPrompt.assemble()``AssembleContext` 中携带 `signal?: AbortSignal`,因为该对象是显式请求值,也可表示轮次之外不携带 signal 的组装。监听器可以配合该 signal 取消,但不得保留它来控制其他轮次。
显式事件签名传递单个 payload 对象:agent 作用域事件在 payload 中携带 `agent``signal``next` 位于最后;其余 seam 保持 `signal` 紧邻 waterfall(瀑布式事件)的最终 `next` 之前。`PreStepContext``RequestFailureContext` 已退役,其字段并入 `agent/pre-step``agent/request-error` 的 payload[payload-object 事件](2026-08-06-agent-event-payload-objects.md))。步骤前处理入口、请求配置、请求错误恢复、模型生成、工具执行、审批、轮次停止以及 subagent 或工作流请求都会收到当前 signal。钩子桥接器也必须提供 `RunHookOptions.signal`,使轮次取消能够到达 Bash 执行器终止进程组并等待其退出的边界。`SystemPrompt.assemble()``AssembleContext` 中携带 `signal?: AbortSignal`,因为该对象是显式请求值,也可表示轮次之外不携带 signal 的组装。监听器可以配合该 signal 取消,但不得保留它来控制其他轮次。
`ctx.agents` 仍只携带发起 Agent。环境中的 Agent 并不代表存活、当前轮次或取消权限。cause 读取器是 loop 私有的,它直接陈述机器私有的 slot 不变量(只有 `cancel()` 会中止轮次控制器,且总是携带规范的冻结 cause),而不是对 reason 做结构化再校验;不存在从任意 signal 读取 cause 的公开辅助函数。并发 Agent 会同时隔离各自的发起方身份和轮次 signal;子驱动会遮蔽父发起方,而父请求 signal 仍通过 subagent seam 传递。
@@ -30,7 +30,7 @@ Agent dispose(资源释放)会在活跃持有者上请求仅用于运行时
## 验证
约测试验证类型化调用方联合类型、冻结且与调用方分离、默认行为与首次请求优先行为、粗粒度的会话 JSON 往返与旧式记录拒绝、ACP `user`、进程内 subagent `parent` 以及 dispose 优先级。AgentLoop 测试让协作式监听器在 pre-step、系统提示词组装、请求、模型流、请求错误恢复、工具执行和轮次停止处等待 signal;并断言同一轮次使用一个 signal,不同轮次使用全新的 signal,终态发布期间和持久化刷新受阻期间不存在取消权限。真实钩子桥接器测试会在报告空闲状态前取消并回收受阻的提示词钩子。
测试验证类型化调用方联合类型、冻结且与调用方分离、默认行为与首次请求优先行为、粗粒度的会话 JSON 往返与旧式记录拒绝、ACP `user`、进程内 subagent `parent` 以及 dispose 优先级。AgentLoop 测试让协作式监听器在步骤前处理、系统提示词组装、请求、模型流、请求错误恢复、工具执行和轮次停止处等待 signal;并断言同一轮次使用一个 signal,不同轮次使用全新的 signal,终态发布期间和持久化刷新受阻期间不存在取消权限。真实钩子桥接器测试会在报告空闲状态前取消并回收受阻的提示词钩子。
发起方作用域测试断言所有钩子仍观察到同一个 Agent 且没有环境中的轮次 signal,并发 Agent 保持独立的身份与 signal,嵌套子驱动只遮蔽身份。竞态测试覆盖空闲状态取消、运行前取消、从 `running` 监听器提交替代提示词、重复取消以及取消与 dispose 竞争下的完全停稳。
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-19-cooperative-tool-cancellation.md
2026-07-19-cooperative-tool-cancellation.md: 4e82452cd6883593d4693766bea803c97fa4c977
2026-07-19-cooperative-tool-cancellation.zh.md: 1d9fd3dcc8fa65e6f06050be749eec6ac9e996c2
2026-07-19-cooperative-tool-cancellation.zh.md: aa94fd9f3d32167dc7d6b906ff14a6afd36ccb32
@@ -18,7 +18,7 @@ Status: implemented
`ToolDefinition.execute(args, exec)` 保持现有签名。`defineTool()` 会把 `exec.signal` 上下文推断为必填的 `AbortSignal`,因此每个已注册的 TypeScript 工具都能在无需类型断言的情况下观察或转发取消。所有第一方直接调用方和 Code Mode 嵌套调度都会显式传入当前操作的信号。
注册表信任这份类型化同进程约。它不在运行时校验 `AbortSignal`,也不为缺失或畸形信号添加敌意输入测试。校验仍位于解析器与配置、模型与工具 JSON、持久化与文件、worker、进程和协议边界;违反 TypeScript 接口的无类型 JavaScript 不享有兼容性约。
注册表信任这份类型化同进程约。它不在运行时校验 `AbortSignal`,也不为缺失或畸形信号添加敌意输入测试。校验仍位于解析器与配置、模型与工具 JSON、持久化与文件、worker、进程和协议边界;违反 TypeScript 接口的无类型 JavaScript 不享有兼容性约
### 可变性由流水线阶段决定
@@ -54,9 +54,9 @@ Status: implemented
**保留可选信号并生成后备值。** 不予采纳,因为注册表持有的后备信号不代表任何调用方生命周期,也会保留类型系统本应阻止的缺失情况。
**在运行时校验 `AbortSignal`。** 不予采纳,因为这是类型化同进程 seam,不是序列化边界。运行时检查只会重复静态约,仍无法强制实现协作式使用信号。
**在运行时校验 `AbortSignal`。** 不予采纳,因为这是类型化同进程 seam,不是序列化边界。运行时检查只会重复静态约,仍无法强制实现协作式使用信号。
**添加 `supportsCancellation` 元数据、回调参数数量检查或信号使用 lint。** 不予采纳,因为这些方法都无法证明异步工作会观察或正确转发取消。信号可用性属于类型约;具体行为仍由工具和能力负责。
**添加 `supportsCancellation` 元数据、回调参数数量检查或信号使用 lint。** 不予采纳,因为这些方法都无法证明异步工作会观察或正确转发取消。信号可用性属于类型约;具体行为仍由工具和能力负责。
**向所有阶段公开同一个可变执行类型。** 不予采纳,因为观察者和工具实现只需要借用信号。按阶段划分类型可以把替换权限限制在流水线拥有该操作的位置。
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md
2026-07-19-gui-layering-and-rpc-protocol.md: 8e020e4fe9b60100671c0cf0e98e28532d850f94
2026-07-19-gui-layering-and-rpc-protocol.zh.md: 06b861ef61e83985b5c3640ff8eddfa5f6845c81
2026-07-19-gui-layering-and-rpc-protocol.zh.md: 49ebd642264c663377f16b8669fedc112d3bb6c7
@@ -1,4 +1,4 @@
# Agent Note: GUI 分层与 RPC 协议——host/client 按能力提供方分层、四象限消息模型与 fetch 载体
# RFC: GUI 分层与 RPC 协议——host/client 按能力支持方分层、四象限消息模型与 fetch 载体
Status: implemented
@@ -8,20 +8,20 @@ Status: implemented
## Problem
需要提供 UI 对接层,除已有 ACPAgent Client Protocol/stdio 基线外,还需要 Webserver)、Electron 等其他产品 UI 形态。我们把这些形态统一称为 Client。希望具备以下能力:
- 一个 `dsh` 进程同时支持 `dsh web`启动`dsh run`headless,一个进程两种模式(设计预留)
需要提供 UI 对接层,除已有 ACP/stdio基础版本外,还需要 Web(server) 、 Electron 等其他产品 UI 形态。我们把这些形态统一称为 Client。希望有如下能力支持:
- `dsh` 进程同时支持 `dsh web`(启动) `dsh run`(headless) ,一个进程两种模式(设计预留)
- 以与 `dsh web` 同构的 Web 技术形态,在 Electron 中启动
那么当前的工程代码需要稳定的分层职责模型,便于以后接入各类 client 形态。
同时各消费的物理通道不同(浏览器 HTTPWebSocket、进程内 fetch/SSEServer-Sent Events、将来 IPC),还需要一个通道无关的消息模型和约定的单一真源,让「加一个方法」「换一种载体」互不牵连,且 wire 上的每条消息可类型校验、可观测、可对账。
同时各消费的物理通道不同(浏览器 HTTPWebSocket、进程内 fetch/SSE、将来 IPC),还需要一个通道无关的消息模型和单一约定事实源,让「加一个方法」「换一种载体」互不牵连,且 wire 上的每条消息可类型校验、可观测、可对账。
## Decision
### 分层
目录按照如下分层
- `packages/host/*`包只提供 Host 侧能力(代表了基于现有 harness 插件系统的 Node.js 代码核心工程),除此之外,还包含
目录按照如下分层:
- `packages/host/*`: 包只提供 Host 侧能力(代表了以现在 Harness 实体插件系统为主体的 Node.js 代码核心工程),除此之外,还包含
- 统一后端协议(fetch、HTTP、流式接口等)定义和支持,见本篇「消息协议」起各节
- `packages/client/*`:包只提供 Client 侧能力,每包单边不混。这里住三类包(两条轴归 [client 插件装载 RFC](2026-07-23-client-plugin-loading-model.md) 所有):
- **纯库**`ui-slots``web-react``ui-primitives`,外加内核包 `loader`):普通根入口包,静态打包进壳;前三者播种进模块表。
@@ -49,7 +49,7 @@ harness core packages ──────────────────┘
- `runtime → apiproxy` 单向;apiproxy 仅依赖类型定义。
- client 侧包**永不 import** host 侧包的运行时(只吃 `/api``/client` 两个浏览器安全子路径)。
- `webserver` 不依赖 `runtime`:它提供一个 `{ fetch }` 形状的实现 ——「webserver ← runtime」只是运行时注入关系,不是包依赖。
- `webserver` 不依赖 `runtime`:它提供 `{ fetch }` 特定实现 ——「webserver ← runtime」只是运行时注入关系,不是包依赖。
- client 侧跨包 import 插件包一律走 `/client` 子路径,且插件包之间只限类型 import——跨插件值 import 在 tsdown 纯度门禁处即构建错误(值层面的协作走 cordis 服务;边规则归 [client 插件装载 RFC](2026-07-23-client-plugin-loading-model.md) 所有)。
TypeScript 以 solution 根引用的**两个聚合 program** 检查(`tsconfig.json` = solution`tsconfig.host.json` = host 侧 + 测试,排除 `packages/client``tsconfig.client.json` = client 各包及其测试):两侧在相同键(`sessions``loader`)下以不同服务合并 cordis `Context` 接口,单一 program 会同时看到两份声明合并而报冲突。共享叶子包(session/llm/tools/apiproxy 等)只构建一次,由两个 program 共同引用([拓扑](../process/2026-07-22-tsconfig-solution-root-two-aggregates.md))。
@@ -60,11 +60,11 @@ TypeScript 以 solution 根引用的**两个聚合 program** 检查(`tsconfig.
| 层 | 包 | 职责 | 关键纪律 |
|---|---|---|---|
| 前置层 | `dsh-host-apiproxy` | TS/zod 定义 (api/)+ fetch 抽象 (fetch/handler + 客户端基类) | 保持简单——每个消费方都需要它Node/浏览器皆可 import;协议内容见下文「消息协议」起各节;client 不得经 ctx 绕开 api |
| 前置层 | `dsh-host-apiproxy` | TS/zod 定义 (api/)+ fetch 抽象 (fetch/handler + 客户端基类) | 做简单、所有接入方都要Node/浏览器皆可 import;协议内容见下文「消息协议」起各节;client 不得经 ctx 绕开 api |
| 装配层 | `dsh-host-runtime` | 插件组合 + ApiProxy 集成 + web UI 插件挂载(覆盖八个 dshClient 包的内存 Loader 树);host 级配置归属地(defaults/persistenceRoot,将来用户 profile) | 装什么插件、给什么默认值只在这里定;壳不得改装配 |
| 承载层 | `dsh-host-webserver` | Web 形态 HTTP 与 upgrade:静态服务 + `/api/*`→handler 转发 + WebSocket upgrade route + close 语义;插件 bundle 端点 + `__DSH_BOOT__` manifest(元数据清单)注入(由 web 插件注册表供给) | Web(浏览器访问)专用;零 workspace 依赖(注册表经结构注入到达);Electron 不复用它 |
| client 库 | `dsh-client-ui-slots` / `dsh-client-web-react` / `dsh-client-ui-primitives` | slot 注册表核心 / ctx↔React 胶合 / 纯 React 原子组件 | 组件零 cordis 运行时依赖;由壳播种进 loader 模块表 |
| client 插件 | `dsh-client-connection` / `dsh-client-runtime` / `dsh-client-ui-theme` / `dsh-client-i18n` / `dsh-client-ui-layout` / `dsh-client-ui-sidebar` / `dsh-client-ui-conversation` / `dsh-client-ui-trajectory` | 浏览器侧 cordis 插件树(wire 消费、核心服务、主题、i18n、布局、侧栏、对话、轨迹)——见 Web 客户端架构 RFC | 双入口(node 半边=空 apply;实现在 `src/client/`);消费面唯一经 ApiProxy |
| client 插件 | `dsh-client-connection` / `dsh-client-runtime` / `dsh-client-ui-theme` / `dsh-client-i18n` / `dsh-client-ui-layout` / `dsh-client-ui-sidebar` / `dsh-client-ui-conversation` / `dsh-client-ui-trajectory` | 浏览器侧 cordis 插件树(wire 消费、核心服务、主题、i18n、布局、侧栏、对话、轨迹)——见 Web 客户端架构 RFC | 双入口(node 半边=空 apply;实现在 `src/client/`);消费面唯一经 ApiProxy |
| 应用态 | `@deepseek-ai/dsh`apps/cli+ `dsh-frontend`apps/webvite 应用) | bin 粗分发 + 每形态一个拼装模块(web.ts / headless.ts);vite 应用是 `dsh-client-web` 壳表面之上的薄 main | 形态间动态 import 互不加载;dist 定位等 workspace 知识留在 app |
#### 命名规则
@@ -116,7 +116,7 @@ TypeScript 以 solution 根引用的**两个聚合 program** 检查(`tsconfig.
`ClientResponse` 的 HTTP 应答体是 `RpcReceipt = { accepted: true } | { accepted: false; reason: 'not-pending' | 'bad-response' }`——载体层回执,**不是** RpcMessageresponse 不再有 response);迟到/重复应答收 `not-pending`,逻辑收敛面是 `*/resolved` 帧。
## 类型体系:函数签名即
## 类型体系:函数签名即事实
### RpcMethodMap 与派生泛型(`api/rpc-map.ts`
@@ -163,7 +163,7 @@ export type ResponseValue<K> =
|---|---|---|---|
| `session.list` | `{ cursor?: string }`cursor 留座不实现) | `{ items: SessionSummary[] }` | 已持久化 sessionupdatedAt 倒序;v1 不建索引 |
其余方法(`session.create`/`session.history`/`session.rename`/`session.prompt`/`session.cancel`/`host.describe`)的参数与返回不在此复写——签名即源,见 `api/sessions.ts`、`api/host.ts` 与 `RpcMethodMap`。
其余方法(`session.create`/`session.history`/`session.rename`/`session.prompt`/`session.cancel`/`host.describe`)的参数与返回不在此复写——签名即事实源,见 `api/sessions.ts`、`api/host.ts` 与 `RpcMethodMap`。
### 帧(server→client,具名 union
@@ -173,7 +173,7 @@ export type ResponseValue<K> =
|---|---|---|
| `session/event` | `{ sessionId; event: SessionEvent }` | 核心透传:core 事件原样过,`assistant/chunk` 即 token 流,无独立 delta 帧 |
其余帧型不在此复写,union 全集见 `api/events.ts` 的 `MuxFrame`/`HostFrame`。语义上须知三点:`session/subscribed` 的 lastSeq 供 history 补缺口竞态检测;`approval/question` 的 requested 帧可应答(rpcId 稳定)、resolved 帧是收敛面;`host/agent-error` 是无 turn 位置 live 失败的唯一出口。
其余帧型不在此复写,union 全集见 `api/events.ts` 的 `MuxFrame`/`HostFrame`。语义上须知三点:`session/subscribed` 的 lastSeq 供 history 补竞态检测;`approval/question` 的 requested 帧可应答(rpcId 稳定)、resolved 帧是收敛面;`host/agent-error` 是无 turn 位置 live 失败的唯一出口。
**透传纪律**:wire 上的事件/消息/内容块就是 core 类型(`SessionEvent`/`ContentBlock`),不造第二套 DTO;类型经 `import type` 依赖链直达浏览器。`SessionEventMap` merge-extensibleclient 对未知 type documented-default(忽略),事件 schema 留「合法信封+未知类型」分支——信封仍严格,不是字段级 passthrough。
@@ -181,11 +181,11 @@ export type ResponseValue<K> =
- **历史 = 事件重放**:一套 fold(client 侧),历史分页与 live 增量同一条代码路径;server 不做物化快照第二套。history **页边界对齐消息边界**(绝不从消息中间截断;chunk 随定稿消息归组),尾页含进行中 partial 的 chunk。
- **prompt 关联**prompt 的 rpcId 经 MessageSource`'user-rpc'`)透传进 `user/message` 事件,client 以此把乐观回显转正。
- **重连 = 重建**:不做续传 cursor`mux` 的 `since` 签名留座、传了忽略);断线重开流 + 重拉 history;`subscribed.lastSeq` 与 history 尾 seq 比对,如有缺口,再补拉一次。
- **冷会话处理遵循所有权**`session.history` 与 `session.fork` 的源端读取会在不获取 agent(智能体) 的情况下检查持久化存储,而绑定到 agent 的普通会话方法(如 `prompt`)则通过在途表去重后恢复会话。由会话支撑的 subagent 会拒绝这条通用恢复路径,且附加状态不对客户端暴露(`running` 已经覆盖)。
- **重连 = 重建**:不做续传 cursor`mux` 的 `since` 签名留座、传了忽略);断线重开流 + 重拉 history;`subscribed.lastSeq` 与 history 尾 seq 比对,有缝再补拉一次。
- **冷会话处理遵循所有权**`session.history` 与 `session.fork` 的源端读取会在不获取 Agent 的情况下检查持久化存储,而绑定到 Agent 的普通会话方法(如 `prompt`)则通过在途表去重后恢复会话。由会话支撑的 subagent 会拒绝这条通用恢复路径,且附加状态不对客户端暴露(`running` 已经覆盖)。
- **审批/问答**requested 帧受理时 mint 稳定 rpcId;先到先赢,host 内存 pending 表(keyed by rpcId)是唯一裁判;mux 重开后在 subscribed 帧后重放仍 pending 的 requested 帧(rpcId 原样复用,刷新恢复)。审计事件 `approval/asked`/`decided` 照旧走 durable 日志——帧=live 控制面,事件=durable 审计。**现状**:约定与帧类型已 shippedhost 侧 pending 表/wire answerer 未实现(`api-proxy.ts` 的 `respond` 是 stub,恒回 `not-pending`);PendingCard v1 只展示。
- **不设协议版本**client 与 host 绑定发布,`host.describe` 无 protocolVersion 字段;出现独立发布的 client 时再引入。
- **预留 seam纪律**:map 只含已实现方法,未知 method 在信封 parse 即 fail loud`bad-request`),不设 not-implemented 兜底码。预留清单(实现时把签名抄进域接口+map 加行+schema 加对即升格):`session.fork`、`prompt.mode` 加 `'inject'`、`task.list`、`host.listModels`、describe 加 `hostInstanceId`。(`session.rename` 已从本清单毕业:追加 user 来源的 `session/title` 事件。)
- **预留接缝纪律**:map 只含已实现方法,未知 method 在信封 parse 即 fail loud`bad-request`),不设 not-implemented 兜底码。预留清单(实现时把签名抄进域接口+map 加行+schema 加对即升格):`session.fork`、`prompt.mode` 加 `'inject'`、`task.list`、`host.listModels`、describe 加 `hostInstanceId`。(`session.rename` 已从本清单毕业:追加 user 来源的 `session/title` 事件。)
## 客户端载体:AbstractApiClient 类体系(`fetch/client.ts`
@@ -207,20 +207,20 @@ export type ResponseValue<K> =
### 实例级 envelope 观测切面
四象限全形均过 `onEnvelope`;基类实现是**实例持有的微任务合批缓冲**(帧风暴不逐帧惊扰消费;模块级状态会跨实例/测试泄漏,故实例持有)。观测者经 `subscribeEnvelopes(listener)` 订阅(收整批 `readonly RpcMessage[]`,返回退订函数);listener 抛异常被隔离(观测不得反噬载体)。无订阅者时零缓冲成本。当前没有任何现役消费订阅——该切面是 wire 诊断的预留位(已退役的 RPC 调试面板是它的首个消费,将来的诊断消费接入时不动载体)。
四象限全形均过 `onEnvelope`;基类实现是**实例持有的微任务合批缓冲**(帧风暴不逐帧惊扰消费;模块级状态会跨实例/测试泄漏,故实例持有)。观测者经 `subscribeEnvelopes(listener)` 订阅(收整批 `readonly RpcMessage[]`,返回退订函数);listener 抛异常被隔离(观测不得反噬载体)。无订阅者时零缓冲成本。当前没有任何现役消费订阅——该切面是 wire 诊断的预留位(已退役的 RPC 调试面板是它的首个消费,将来的诊断消费接入时不动载体)。
### 子类表(传输承载)
| 子类 | 所在包 | doFetch | 用途 |
|---|---|---|---|
| `InProcessApiClient` | apiproxy 本包 | 注入的 `{ fetch }` handler | **同构点**`new InProcessApiClient(toFetchHandler(api))` 全程不过网络但真跑 wire 序列化/zod/SSE 帧——`dsh run` headless 即协议第二个真正的消费 |
| `InProcessApiClient` | apiproxy 本包 | 注入的 `{ fetch }` handler | **同构点**`new InProcessApiClient(toFetchHandler(api))` 全程不过网络但真跑 wire 序列化/zod/SSE 帧——`dsh run` headless 即协议第二真实消费 |
| `WebApiClient` | dsh-client-connection | `globalThis.fetch` 上行 + 每逻辑流一条同源 WebSocket 下行 | 浏览器形态;物理边界见 [WebSocket 下行载体](2026-08-04-websocket-downlink-carrier.md) |
| `FixtureApiClient` | dsh-client-connection | 不用(协议层覆写) | 无 server 的 UI 开发(`?fixture`):覆写 `callUnary`/`openMux`/`openHost`/`respond` 虚方法,自己就是假 server(帧 rpcId 由它 mint,语义自洽) |
| (将来)IPC 桥子类 | apps/electron | IPC 序列化往返 | 仅换 doFetch,约定/基类零改 |
## 怎么扩展(操作清单)
**加一个 unary 方法(5 步)**:①域接口加方法签名(参数/返回内联,这是唯一源);②`RpcMethodMap` 加一行;③`<域>.schema.ts` 加 request/value schema 对(锚 `Wire<RequestPayload<'…'>>`);④handler `UNARY_ROUTES` 加一行(handler 的 Web 承载见 Web 客户端架构 RFC);⑤impl 实现(回显 `request.rpcId`)。client 侧 `IApiClient`/`AbstractApiClient` 的域方法表同步加一行透传。
**加一个 unary 方法(5 步)**:①域接口加方法签名(参数/返回内联,这是唯一事实源);②`RpcMethodMap` 加一行;③`<域>.schema.ts` 加 request/value schema 对(锚 `Wire<RequestPayload<'…'>>`);④handler `UNARY_ROUTES` 加一行(handler 的 Web 承载见 Web 客户端架构 RFC);⑤impl 实现(回显 `request.rpcId`)。client 侧 `IApiClient`/`AbstractApiClient` 的域方法表同步加一行透传。
**加一个帧型(3 步)**:①`MuxFrame`/`HostFrame` union 加一支(可应答帧须注明 rpcId 稳定语义);②帧 schema 加一支;③消费端 fold/路由的 documented-default 已兜底未知型,按需加显式分支。
@@ -228,26 +228,26 @@ export type ResponseValue<K> =
**接一种新载体**:继承 `AbstractApiClient` 只实现 `doFetch`;需要拦截协议层(如 fixture)再覆写 `callUnary`/`openMux`/`openHost` 虚方法。约定与基类零改。
**升格一个预留 seam**:把预留签名抄进域接口 → map 加行 → schema 加对 → UNARY_ROUTES 加行 → impl 实现。
**升格一个预留接缝**:把预留签名抄进域接口 → map 加行 → schema 加对 → UNARY_ROUTES 加行 → impl 实现。
## Consequences
所有 client 形态消费同一约定:加一个 unary 方法是从单一签名辐射的五步机械改动,换载体只动一个 `doFetch` 子类,wire 上每条消息可 zod 校验、可经 envelope tap 观测、可按 rpcId 对账。普通 unary 调用仍受时限约束,而 `host.pickDirectory` 与 `command.execute` 可保持挂起,直到操作完成或调用方/连接取消到来;若由用户掌控节奏的操作不自行结束,请求可能一直挂起,这是为避免把合理的操作时长视为传输失败而接受的代价。其余接受的代价:两组包需要显式 tsconfig paths 条目;预留 seamfork/inject/task.list/listModels/hostInstanceId)在真正的消费出现前保持休眠。
所有 client 形态消费同一约定:加一个 unary 方法是从单一签名辐射的五步机械改动,换载体只动一个 `doFetch` 子类,wire 上每条消息可 zod 校验、可经 envelope tap 观测、可按 rpcId 对账。普通 unary 调用仍受时限约束,而 `host.pickDirectory` 与 `command.execute` 可保持挂起,直到操作完成或调用方/连接取消到来;若由用户掌控节奏的操作不自行结束,请求可能一直挂起,这是为避免把合理的操作时长视为传输失败而接受的代价。其余接受的代价:两组包需要显式 tsconfig paths 条目;预留接缝fork/inject/task.list/listModels/hostInstanceId)在真消费出现前保持休眠。
## Alternatives considered
| 放弃项 | 一句话理由 |
|---|---|
| 按「产品形态」分包(web 一族、electron 一族) | 形态间共享的是 host/client 两侧能力而非形态本身;能力提供方分层让新形态零新包 |
| 混合体建包(如 headless 独立包) | 混合体只有一个消费(它自己的 app),建包是无主抽象;拼装写在 app 里可读可弃 |
| 作为消费方的 client 直连 ctx(省 apiproxy 一层) | 第二命令面绕开约定,wire 校验/观测/多端一致性全失;ctx 只留给前门与 headless 事件订阅两个正式用途 |
| 按「产品形态」分包(web 一族、electron 一族) | 形态间共享的是 host/client 两侧能力而非形态本身;能力支持方分层让新形态零新包 |
| 混合体建包(如 headless 独立包) | 混合体只有一个消费(它自己的 app),建包是无主抽象;拼装写在 app 里可读可弃 |
| 消费型 client 直连 ctx(省 apiproxy 一层) | 第二命令面绕开约定,wire 校验/观测/多端一致性全失;ctx 只留给前门与 headless 事件订阅两个正式用途 |
| webserver 依赖 runtime(省 handler 注入) | 结构 typing 注入让 webserver 可被 sidecar/测试复用且零 workspace 依赖;包依赖会把装配知识拖进承载层 |
| 包名不带组前缀(沿用 dsh-<尾段> | `dsh-runtime`/`dsh-web-ui` 在扁平 npm 命名空间里失去归属信息;代价只是每包一条显式 paths |
| 复用仓内 JSON-RPC 2.0dsh-jsonrpc | 数字错误码退化成单码兜底、约定双份人肉对齐、命名无 convention 自然漂移 |
| 三信封模型(Request/Response/Frame 各一信封,签名不感知方向) | rpcId 是逻辑层关联,帧与应答的方向语义靠通道推断在换载体时即失效 |
| 具名 Request/Response 类型对为源(map 登记类型对) | 平铺具名类型是同一事实的第二个名字;签名 infer 反推让加方法只改一处 |
| REST 风格路径 | 消费是自家 client,无第三方 REST 体验诉求;RPC 直映方法表更机械 |
| 具名 Request/Response 类型对为事实源(map 登记类型对) | 平铺具名类型是同一事实的第二个名字;签名 infer 反推让加方法只改一处 |
| REST 风格路径 | 消费是自家 client,无第三方 REST 体验诉求;RPC 直映方法表更机械 |
| DTO 层(wire 专用第二套结构) | core 类型 type-only 直达浏览器零成本;DTO 是永久的双向同步税 |
| cursor 续传(mux since 实装) | 重连=重建(opencode 同款)覆盖 v1 全部需求;签名留座,实装等真正的消费 |
| cursor 续传(mux since 实装) | 重连=重建(opencode 同款)覆盖 v1 全部需求;签名留座,实装等真消费 |
| createApiClient 工厂函数(原实现) | 平台差异(传输/观测)是继承切面不是参数;类体系让 fixture 在协议层替换而不是包一层假信封 |
| 对 `command.execute` 应用 30 秒传输时限 | 命令耗时属于操作本身,而非传输健康预算;该时限会终止本应继续运行的长时处理器,调用方/连接取消已提供所需的停止路径 |
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md
2026-07-19-gui-web-client-architecture.md: e25d002b92df2016faf4073108aa905b65a1b115
2026-07-19-gui-web-client-architecture.zh.md: 78a212dc18c1f553a6f782ed60822e36a622bda3
2026-07-19-gui-web-client-architecture.zh.md: 10779107a9dc6dc7335b2c5932c8c4877b531b50
@@ -1,4 +1,4 @@
# Agent Note: Web 客户端架构——client cordis 插件树、slot 体系与 React-free 对象层
# RFC: Web 客户端架构——client cordis 插件树、slot 体系与 React-free 对象层
Status: implemented
@@ -30,27 +30,27 @@ Status: implemented
## client cordis 树与装载链
装载链——两类包(普通包 vs dshClient 插件)、模块系统/插件治理器之分、host 独家撰写的带修订号 entry 图之上的双阶段 boot、热重载——归 [client 插件装载 RFC](2026-07-23-client-plugin-loading-model.md) 所有。本篇赖以立足的事实:浏览器启动与 host 相同的 vendored `@cordisjs/plugin-loader`,由 client 模块系统(`ctx.modules``packages/client/modules`)填上其 `internal` seam;凡带产品行为的单元都是 host 独家撰写的 `__DSH_BOOT__` 图里的 entry——每个生产插件包(含基础设施)都携带 `dshClient` 声明、以 fetch 到达的 `./client` tsdown 闭包 bundle 供给,`immediately` 行的差别仅在 boot 第一阶段预取,而普通包(react 家族、cordis、尚未升格的库)保持打进壳、已播种、对图不可见;bundle 执行 `window.__ModuleLoader__.load({ id, factory })`,其 `require` 由 lazy CJS 模块表应答(种子词条 + 已登记工厂,首次 require 时物化并记忆化——跨插件值 import 是构建错误,协作走 cordis 服务);插件 CSS 内联在 bundle 里、物化时注入为 `<style data-plugin="<id>">`CSS Modules 哈希 + 归属标记 = 隔离,重载时移除);热重载已在 dev 图落地——webserver 对自己供给的 bundle 做 stat 轮询并广播 `rebuilt` SSEServer-Sent Events帧,`client-hmr` 插件每帧换掉一个 fiber。settled 翻转(`loader.await()` + 一次全 ACTIVE 扫描)依旧让壳从 loading 页一次切换到真 UI——settled 意味着每个 entry 已创建、每个 fiber 都到达 ACTIVEFAILED/PENDING 的 fiber 被大声列出;不存在部分可用模式(渐进渲染为后置工作)。
装载链——两类包(普通包 vs dshClient 插件)、模块系统/插件治理器之分、host 独家撰写的带修订号 entry 图之上的双 boot、热重载——归 [client 插件装载 RFC](2026-07-23-client-plugin-loading-model.md) 所有。本篇赖以立足的事实:浏览器启动与 host 相同的 vendored `@cordisjs/plugin-loader`,由 client 模块系统(`ctx.modules``packages/client/modules`)填上其 `internal` seam;凡带产品行为的单元都是 host 独家撰写的 `__DSH_BOOT__` 图里的 entry——每个生产插件包(含基础设施)都携带 `dshClient` 声明、以 fetch 到达的 `./client` tsdown 闭包 bundle 供给,`immediately` 行的差别仅在 boot 第一预取,而普通包(react 家族、cordis、尚未升格的库)保持打进壳、已播种、对图不可见;bundle 执行 `window.__ModuleLoader__.load({ id, factory })`,其 `require` 由 lazy CJS 模块表应答(种子词条 + 已登记工厂,首次 require 时物化并记忆化——跨插件值 import 是构建错误,协作走 cordis 服务);插件 CSS 内联在 bundle 里、物化时注入为 `<style data-plugin="<id>">`CSS Modules 哈希 + 归属标记 = 隔离,重载时移除);热重载已在 dev 图落地——webserver 对自己供给的 bundle 做 stat 轮询并广播 `rebuilt` SSE 帧,`client-hmr` 插件每帧换掉一个 fiber。settled 翻转(`loader.await()` + 一次全 ACTIVE 扫描)依旧让壳从 loading 页一次切换到真 UI——settled 意味着每个 entry 已创建、每个 fiber 都到达 ACTIVEFAILED/PENDING 的 fiber 被大声列出;不存在部分可用模式(渐进渲染为后置工作)。
类型宇宙在聚合层拆分——`tsconfig.host.json` 是 host program、`tsconfig.client.json` 是 client program,二者由 solution 根 `tsconfig.json` 引用,因为两侧都在相同键(`sessions``loader`)上对 cordis `Context` 做声明合并且服务不同;client 包经纯类型子路径(`@deepseek-ai/dsh-session/types` 等)消费协议词汇,host 侧的声明合并不会搭车进入 client program。
## slot 体系:页面怎么拼
slot 体系有自己的 RFC——[slot 体系标准](2026-07-22-slot-type-chain-implementation.md)——本文整体移交给它。此处只留一段定位摘要:壳只渲染 `'root'`;插件用单独一次 `register` 调用组合 UI——占用一个 slot、声明并授权其子 slot`children` spec 对象)、声明 store、注入业务面;组件 props 以四个自动推导的份额传入`PropsRuntime<K>` / `PropsRenderSlots<S>` / `PropsStore<H>` / inject),各有唯一真源。`SlotMap` 声明合并仍是类型权威,entry 只携带 owner 份额(「谁注入的,类型归谁」);每个被渲染的注册项都在 per-entry 错误边界之内。
slot 体系有自己的 RFC——[slot 体系标准](2026-07-22-slot-type-chain-implementation.md)——本文整体移交给它。此处只留一段定位摘要:壳只渲染 `'root'`;插件用单独一次 `register` 调用组合 UI——占坑、声明并授权子坑`children` spec 对象)、声明 store、注入业务面;组件 props 分四份额自动推导到达`PropsRuntime<K>` / `PropsRenderSlots<S>` / `PropsStore<H>` / inject),各有唯一真源。`SlotMap` 声明合并仍是类型权威,entry 只携带 owner 份额(「谁注入的,类型归谁」);每个被渲染的注册项都在 per-entry 错误边界之内。
实现的家:注册表核心与 props 份额类型在 `packages/client/ui-slots`,出口组件/渲染器/uSES 桥在 `packages/client/web-react`
## 服务与 scope 寻址
服务是插件对其他插件的唯一 API 面(UI 组件与注入面都不是 API;无人调用的插件不挂服务——ui-trajectory 即最小插件样板:无 ctx 服务,只做视图 slot 注册)。名册:`ctx.connection`api client + 流句柄)、`ctx.slots`(注册表包装层,发 `slots/changed`,渲染入口,渲染器安装 seam)、`ctx.sessions`(列表 store、当前会话状态、scope 树)、`ctx.loader``ctx.theme``ctx.i18n``ctx.layout`(跨插件视图导航)、`ctx.conversation`send/cancel/startSession)。过去住在服务 store 里的观看态(面板宽、选中、草稿)现按 [slot 体系标准](2026-07-22-slot-type-chain-implementation.md) 住 entry 声明的 store。
服务是插件对其他插件的唯一 API 面(UI 组件与注入面都不是 API;无人调用的插件不挂服务——ui-trajectory 即最小插件样板:无 ctx 服务,只做视图注册)。名册:`ctx.connection`api client + 流句柄)、`ctx.slots`(注册表包装层,发 `slots/changed`,渲染入口,渲染器安装)、`ctx.sessions`(列表 store、当前会话状态、scope 树)、`ctx.loader``ctx.theme``ctx.i18n``ctx.layout`(跨插件视图导航)、`ctx.conversation`send/cancel/startSession)。过去住在服务 store 里的观看态(面板宽、选中、草稿)现按 [slot 体系标准](2026-07-22-slot-type-chain-implementation.md) 住 entry 声明的 store。
slot 之外不存在第二种注册模型——原视图环与工具环都已溶解进来。会话视图即 ui-conversation 声明的 `'conversation.view'` list slot 的 entrytab 元数据随注册 options`id`/`order`/`label`)走,per-view chrome 住视图组件自身。Tool 展示跨越一条显式包边界:运行时把 Code Dispatch 拓扑投影进每个 root 递归的 `subCalls`ui-conversation 把这个已排序 root 放进 single `'conversation.chat.tool'` seat,不解释 Tool 名称或拓扑;ui-tool 渲染传入的树,并声明 keyed/session 的 `'tool.call.toolview'` 子 slot。key 空间仍在运行时开放(SlotMap 声明 slot、从不声明 key),root 与任意深度的后代都按 `entryKey: toolName` 分发,以 `GenericToolCard` 兜底。业务包通过 `ctx.slots.inject('tool.call.toolview', () => ctx.slots.register({ name: 'tool.call.toolview', key: '<tool>', inject? }, Row))` 注册原子视图;声明本身就是加载与重载依赖([决策](2026-08-05-slot-declaration-injection.md))。ui-conversation 还通过 `'conversation.details.tool'` 委托选中调用的详情正文,使 ui-tool 的 card model 保持为唯一展示所有者,同时避免 conversation 导入 Tool 组件。
slot 之外不存在第二种注册模型——原视图环与工具环都已溶解进来。会话视图即 ui-conversation 声明的 `'conversation.view'` list 的 entrytab 元数据随注册 options`id`/`order`/`label`)走,per-view chrome 住视图组件自身。Tool 展示跨越一条显式包边界:运行时把 Code Dispatch 拓扑投影进每个 root 递归的 `subCalls`ui-conversation 把这个已排序 root 放进 single `'conversation.chat.tool'` seat,不解释 Tool 名称或拓扑;ui-tool 渲染传入的树,并声明 keyed/session 的 `'tool.call.toolview'` 子 slot。key 空间仍在运行时开放(SlotMap 声明 slot、从不声明 key),root 与任意深度的后代都按 `entryKey: toolName` 分发,以 `GenericToolCard` 兜底。业务包通过 `ctx.slots.inject('tool.call.toolview', () => ctx.slots.register({ name: 'tool.call.toolview', key: '<tool>', inject? }, Row))` 注册原子视图;声明本身就是加载与重载依赖([决策](2026-08-05-slot-declaration-injection.md))。ui-conversation 还通过 `'conversation.details.tool'` 委托选中调用的详情正文,使 ui-tool 的 card model 保持为唯一展示所有者,同时避免 conversation 导入 Tool 组件。
**scope 寻址**与 host 侧 agent(智能体) scope 惯例同构:服务是 root 单例,方法不收 sessionId——它们读调用方 ctx 上的 scope 标(`scopeOf(ctx)`)。在会话 scope 内,`ctx.conversation.send('hi', 'queue')` 自动打到该会话;跨会话调用换 ctx 定向(`ctx.sessions.scope(id)!.conversation.send(...)`);从 root ctx 直接调 scoped 方法即 throw。client 会话 scope 的铸造方式与 host agent scope 相同(no-op 插件 fiber + scope 键 extend),首次观看时惰性建,只有会话被移除且无人观看才拆——仅 host 会话死亡不拆 scope(冻结为只读视窗)。
**scope 寻址**与 host 侧 agent scope 惯例同构:服务是 root 单例,方法不收 sessionId——它们读调用方 ctx 上的 scope 标(`scopeOf(ctx)`)。在会话 scope 内,`ctx.conversation.send('hi', 'queue')` 自动打到该会话;跨会话调用换 ctx 定向(`ctx.sessions.scope(id)!.conversation.send(...)`);从 root ctx 直接调 scoped 方法即 throw。client 会话 scope 的铸造方式与 host agent scope 相同(no-op 插件 fiber + scope 键 extend),首次观看时惰性建,只有会话被移除且无人观看才拆——仅 host 会话死亡不拆 scope(冻结为只读视窗)。
## 数据对象层(`packages/client/runtime/src/client/sessions/`
帧从这里进、快照从这里出、投影坐在中间——React-free(零 React importgrep 可断言):
帧从这里进、快照从这里出、fold 坐在中间——React-free(零 React importgrep 可断言):
```
mux/host 帧(ConnectionController 泵入,sinks 注入)
@@ -69,10 +69,10 @@ Notifier 微任务合批 ──► ConversationSnapshot 缓存 ──uSES──
```
- **Session**session.ts):懒建、常驻——建成后在后台持续吃帧,切走切回秒显。操作面:`prompt`/`cancel`RPC 透传;失败落进快照的 `promptError`)、`open`(拉尾页 history,幂等)、`loadOlder`(向上翻页,防重入)、`resync`(重连 = 清窗口重跑 open)。订阅面:`subscribe`/`getSnapshot`(恒返缓存引用)——`implements ObservableSnapshot<ConversationSnapshot>`,构造时挂 `useSelector = bindSnapshotSelector(this)`Session 本身就是 uSES 源。帧分发是一个 switch`session/event` 帧按 seq 去重(唯一去重键),open 在途时缓冲,否则追加 + 增量投影;open/缝合按 seq 合并 live 缓冲并去重,`subscribed.lastSeq` 超出窗口尾则回补一次。
- **ConversationSnapshot**conversation.ts):不可变快照约定——`nodes`供用户阅读的 transcript(文本记录,日志序)、`partial``runningCalls``pending``running``removed``openState``hasMore``promptError` 等。**引用纪律**(memo 与 uSES 的前提):顶层对象每变必新;未变化的 nodes 投影保持同一数组引用,消息流变化时返回新数组并复用未变化的元素引用;未变的子结构复用上一快照的引用。
- **ConversationSnapshot**conversation.ts):不可变快照约定——`nodes`人类对话记录,日志序)、`partial``runningCalls``pending``running``removed``openState``hasMore``promptError` 等。**引用纪律**(memo 与 uSES 的前提):顶层对象每变必新;未变化的 nodes 投影保持同一数组引用,消息流变化时返回新数组并复用未变化的元素引用;未变的子结构复用上一快照的引用。
- **SessionManager**manager.ts):实例簇 + 帧总入口 + 会话列表。带 sessionId 的帧只投已存在实例(mux 广播不得把每个会话都实例化);例外是审批/问答 `requested` 帧——它们不落 history、open 无法回补,故缓冲进 `pendingBuffers`,实例化时回放。
- **Notifier**notifier.ts):两条通知通道,按变更来源取用。`markDirty()`(默认;帧驱动一律用它)按微任务合批——N 次变更、一次通知、一次重渲染;flush 先重建快照缓存再通知。`notifyNow()`(仅用户手势的直接回响)同 tick 重建并通知——受控输入的回响若延到微任务,DOM 会回滚、光标跳尾。帧驱动代码用 notifyNow 会让合批塌回逐帧渲染;禁。
- **TranscriptAdapter / PartialAccumulator**transcript 是按日志顺序投影的 append 来源 surface`@deepseek-ai/dsh-session/surface``isAppendSurfaceEvent`),外加每次落地的压缩compaction检查点一个标记——绝不用模型 surface,后者遮蔽被替换的范围,会抹掉读者已经看过的对话。节点顺序天然按 seq 单调,因此既无核心 `seq === index` 断言需要满足,也没有降级分支。分片不贡献任何节点(O(1) 跳过):累积器把 StreamChunk 折叠成 `AssistantBlock[]`,一次增量只换该块引用;定稿消息到达即在同一批内弃掉累积器(提升无闪烁)。成本模型:一个分片 = 一次字符串拼接 + 一个脏标记;帧风暴下未订阅的 Session 只花那个标记。
- **TranscriptAdapter / PartialAccumulator**对话记录是按日志顺序投影的 append 来源 surface`@deepseek-ai/dsh-session/surface``isAppendSurfaceEvent`),外加每次落地的压缩检查点一个标记——绝不用模型 surface,后者遮蔽被替换的范围,会抹掉读者已经看过的对话。节点顺序天然按 seq 单调,因此既无核心 `seq === index` 断言需要满足,也没有降级分支。分片不贡献任何节点(O(1) 跳过):累积器把 StreamChunk 折叠成 `AssistantBlock[]`,一次增量只换该块引用;定稿消息到达即在同一批内弃掉累积器(提升无闪烁)。成本模型:一个分片 = 一次字符串拼接 + 一个脏标记;帧风暴下未订阅的 Session 只花那个标记。
- **ConnectionController**(在 `packages/client/connection`):开 mux/host 双流、for-await 泵入,代际围栏之内指数退避重连(500ms 翻倍至 10s 封顶、抖动、无限重试);sinks 单向注入(Controller 不认识 Session)。重连 = 重建:`onConnected` → 列表刷新 + 各已打开会话 resync。对象层只面向 `IApiClient`Web 承载以 HTTP POST 载两个 client→server 象限、以[每逻辑流一条 WebSocket](2026-08-04-websocket-downlink-carrier.md)载两个 server→client 象限,客户端类族归分层 RFC 属地。
## React 面(`packages/client/web-react`
@@ -82,11 +82,11 @@ Notifier 微任务合批 ──► ConversationSnapshot 缓存 ──uSES──
- 快照 store 引擎**住 runtime 包**zustand vanilla + 草稿式更新,缺省 `flush: 'sync'`,帧驱动 store 可选 `'raf'` 合批,可选整值 localStorage 持久化,dev 深冻结——全部从 `runtime``./client` 主出口导出,无子路径):store 产物是裸的可观察源,不带任何 hook 成员。插件只经 [slot 体系标准](2026-07-22-slot-type-chain-implementation.md) 的 `defineStore` 声明触及引擎。web-react 在绑定处(`bindSnapshotSelector`,按源缓存)从 React 消费的唯一数据约定合成每个 hook:`ObservableSnapshot<T>``getSnapshot`/`subscribe`)——Session 对象与快照 store 同构满足它。业务插件包只依赖 runtime 与 ui-slotsweb-react 是仅壳可用的胶水。
- `bindSnapshotSelector(source)`:把一个源绑定为经 uSES-with-selector 的带类型 selector hook。uSES 约定四条按构造成立:getSnapshot 恒返缓存引用;subscribe 是绑定期闭包(引用永稳);纯 CSR 不传 server snapshot;相等性缺省 `Object.is`,按调用可选 `shallowEqual`
- `useInvoke(fn)`:把异步动作包成引用恒定的触发器加 pending 标志;pending 走 per-hook 外部 store 经 uSES 读出(渲染路径零 setState),并发调用计数,invoke 引用永不变。
- 相等性协议,全链一致:生产端结构共享;消费`Object.is``shallowEqual` 短路;`React.memo` 浅比较。深比较全链禁止。
- 相等性协议,全链一致:生产端结构共享;消费`Object.is``shallowEqual` 短路;`React.memo` 浅比较。深比较全链禁止。
## 目录形态
Client 包位于 `packages/client/*``apps/web` 是壳 boot 导出之上的薄 Vite 应用。插件包的浏览器半边在 `src/client/` 下;**一切构建产物落 `lib/`**——node 半边为 `lib/index.js`/`lib/invariant.js`,浏览器 bundle 为 `lib/client.js`(共享 tsdown client 预设两者皆出;无 `dist/` 目录,`exports["./client"]` 指向 `./lib/client.js`)。`ui-slots`、web-react 与 runtime 构成基础设施方向;功能插件通过服务与 slot 协作,不导入展示实现。
Client 包位于 `packages/client/*``apps/web` 是壳 boot 导出之上的薄 Vite 应用。插件包的浏览器半边在 `src/client/` 下;**一切构建产物落 `lib/`**——node 半边为 `lib/index.js`/`lib/invariant.js`,浏览器 bundle 为 `lib/client.js`(共享 tsdown client 预设两者皆出;无 `dist/` 目录,`exports["./client"]` 指向 `./lib/client.js`)。`ui-slots`、web-react 与 runtime 构成基础设施方向;功能插件通过 service 与 slot 协作,不导入展示实现。
多域插件包的 client 半边还按未来包边界再拆——ui-conversation 即样板:
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.md
2026-07-19-package-invariant-runtime-contracts.md: 86d86f69b606c348e85d1ae654b6b35c4326985a
2026-07-19-package-invariant-runtime-contracts.zh.md: 386a294b577561d08493a3b2925910ee5ba971b4
2026-07-19-package-invariant-runtime-contracts.zh.md: 94d332a34c1d6e681c10832ea4deb559d88b21a6
@@ -1,4 +1,4 @@
# Agent Note: 有意义的包不变量
# Agent Note: 有意义的包不变量约
Status: implemented
@@ -10,7 +10,7 @@ Status: implemented
有用的运行时不变量会关联时间上的多个观测,或关联可变数据结构中的多个部分。例如:终止事件没有对应的开始事件、LLM(大语言模型)delta 指向未打开的块,或持久化结果的身份与请求不同。仅确认声明的方法存在、插件名称符合预期,或常量示例仍返回已知值,都不属于这种关系。
有些包确实没有可持续观测的关系。纯工具、仅负责组合的包、薄适配器、可执行入口和测试支持包可能仍有重要约,但类型检查、加载检查、聚焦单元测试或集成测试更适合执行这些约。强迫这些包添加合成运行时断言,只会让实现围绕通过门禁优化,而不是检测损坏。
有些包确实没有可持续观测的关系。纯工具、仅负责组合的包、薄适配器、可执行入口和测试支持包可能仍有重要约,但类型检查、加载检查、聚焦单元测试或集成测试更适合执行这些约。强迫这些包添加合成运行时断言,只会让实现围绕通过门禁优化,而不是检测损坏。
## 决策
@@ -64,7 +64,7 @@ Vitest 为每个包测试拓扑使用 `{ enabled: true }` 挂载 `InvariantServi
## 考虑过的替代方案
- **保留生成的空 companion。** 拒绝,因为包获得有意义的运行时关系后,没有解释的占位符仍可能继续存在。
- **要求每个包都执行断言。** 拒绝,因为方法存在性、插件形状和固定示例断言会重复更强的类型、加载和单元测试约,却没有检查运行时一致性。
- **要求每个包都执行断言。** 拒绝,因为方法存在性、插件形状和固定示例断言会重复更强的类型、加载和单元测试约,却没有检查运行时一致性。
- **在服务中保留通用形状 helper。** 拒绝,因为这会混淆编译期 API 验证和运行时不变量,并鼓励在中央定义产品假设。
- **把产品检查移入服务。** 拒绝,因为产品词汇、依赖、测试和变更所有权应归属于产生这些数据的包。
- **从根入口隐式注册 companion。** 拒绝,因为组合顺序和可选服务存在性会产生隐藏 effect。
@@ -75,4 +75,4 @@ Vitest 为每个包测试拓扑使用 `{ enabled: true }` 挂载 `InvariantServi
- 空 companion 是带包专属说明、可评审的决策;删除说明后门禁会失败。
- 类型声明、Cordis 可加载性、插件 metadata、服务方法形状和纯代数继续由所属的编译、加载、单元或集成门禁覆盖。
- 运行时失败会标明所属 npm 包,并指出不一致的观测,而不是复述必要的 API 形状。
- 原有 selection、blocklist 优先级、重复所有权、回滚、dispose 和 HMR(热模块替换)服务约保持不变。
- 原有 selection、blocklist 优先级、重复所有权、回滚、dispose 和 HMR(热模块替换)服务约保持不变。
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-19-package-owned-invariant-service.md
2026-07-19-package-owned-invariant-service.md: e32efe9f6b3ce6b782c61db56d928e87c160dc9a
2026-07-19-package-owned-invariant-service.zh.md: f134541c78f849fa085bebef527b57f911af0748
2026-07-19-package-owned-invariant-service.zh.md: abb77cfc2dc4c15510baf0b205cefdbf3c890041
@@ -18,7 +18,7 @@ Status: implemented
`@deepseek-ai/dsh-invariants` 是与产品无关的 Cordis 服务插件,注册 `ctx.invariants`。它只负责配置、注册唯一性、子 fiber 生命周期和带包归属的失败;不导入 session、agent、scope 或 agent-loop 包,也不包含这些包的检查。
工作区内的每个包都发布 `./invariant` 伴随插件,注册自己完整且准确的 npm 包名。如果所有者具备有意义的事件或可变数据关系,companion 就检查该关系;否则空 installer 必须携带该所有者专属的说明。后续的[运行时约 Agent Note](2026-07-19-package-invariant-runtime-contracts.md) 禁止生成的所有权占位符和合成 API 形状断言。包的根入口不会隐式导入或注册诊断,因此加载根包不会改变运行时检查,也不要求不变式服务存在。
工作区内的每个包都发布 `./invariant` 伴随插件,注册自己完整且准确的 npm 包名。如果所有者具备有意义的事件或可变数据关系,companion 就检查该关系;否则空 installer 必须携带该所有者专属的说明。后续的[运行时约 Agent Note](2026-07-19-package-invariant-runtime-contracts.md) 禁止生成的所有权占位符和合成 API 形状断言。包的根入口不会隐式导入或注册诊断,因此加载根包不会改变运行时检查,也不要求不变式服务存在。
### 配置与选择
@@ -64,13 +64,13 @@ blocklist 匹配优先于 allowlist 匹配。每个条目都是区分大小写
| `@deepseek-ai/dsh-scope/invariant` | `@deepseek-ai/dsh-scope` | scoped event carrier 存在性与主体一致性 |
| `@deepseek-ai/dsh-agent-loop/invariant` | `@deepseek-ai/dsh-agent-loop` | 模型请求重建 |
这四个所有者提供了首批有状态检查。后续运行时约决策为另外十七个确有事件或可变数据关系的所有者增加检查,并为其余包记录有理由的空 companion。每个伴随入口都是单独打包的 `./invariant` export,具有独立声明和对 Loader 安全的命名空间插件形态;服务包自身的伴随插件导入本地服务类型,避免形成自依赖。
这四个所有者提供了首批有状态检查。后续运行时约决策为另外十七个确有事件或可变数据关系的所有者增加检查,并为其余包记录有理由的空 companion。每个伴随入口都是单独打包的 `./invariant` export,具有独立声明和对 Loader 安全的命名空间插件形态;服务包自身的伴随插件导入本地服务类型,避免形成自依赖。
`verify-package-invariants` 会发现每个工作区包,并拒绝缺失的伴随插件源码、生成标记、没有解释的空 installer、缺少或不使用失败报告器的非空 installer、外部或无法解析的注册名、缺失的 `./invariant` export 或发布文件、缺失的不变式对等依赖(peer dependency)、开发依赖及项目引用,以及遗漏伴随入口的自定义构建配置。
### Scoped event 语义映射
生成的 scoped event 主体解析表位于 `dsh-scope`,与消费它的约和不变式相邻。`gen-scoped-events` 使用根 TypeScript Program 枚举 `this: Scoped<Base>` 声明,从真实 `scopeTarget(base, key)` 调用推断路由键类型,并要求唯一、无歧义的 payload 主体或显式 unsupported 标记。提交的运行时映射不导入事件所有者包,因此语义完整性不会扩大服务包或 scope 包的运行时依赖闭包。
生成的 scoped event 主体解析表位于 `dsh-scope`,与消费它的约和不变式相邻。`gen-scoped-events` 使用根 TypeScript Program 枚举 `this: Scoped<Base>` 声明,从真实 `scopeTarget(base, key)` 调用推断路由键类型,并要求唯一、无歧义的 payload 主体或显式 unsupported 标记。提交的运行时映射不导入事件所有者包,因此语义完整性不会扩大服务包或 scope 包的运行时依赖闭包。
### 示例组合与 SDK 输出
@@ -88,9 +88,9 @@ Workspace 约束识别独立的不变式 bundle;包 exports、项目引用、
## 考虑过的替代方案
- **把所有检查保留在 `dsh-invariants`。** 不予采纳,因为注册包仍要导入所有被检查的产品领域,所有者变更仍需中央编辑,测试也继续远离被保护的约。
- **把所有检查保留在 `dsh-invariants`。** 不予采纳,因为注册包仍要导入所有被检查的产品领域,所有者变更仍需中央编辑,测试也继续远离被保护的约
- **当 `ctx.invariants` 恰好存在时,让根包入口隐式注册检查。** 不予采纳,因为根入口行为会依赖组合顺序与可选服务是否存在,诊断无法独立选择,而且包加载会隐藏一个不在显式伴随插件中的注册 effect。
- **在运行时自动发现所有 `invariant.ts` 文件。** 不予采纳,因为文件系统或包发现不是运行时所有权约,会让 bundle 发布含义不清,也无法表达显式 Cordis 加载顺序或依赖安装。构建期生成与校验以及测试 host 可以枚举源码树,因为它们验证的是仓库完整性,而不是组合已发布的部署。
- **在运行时自动发现所有 `invariant.ts` 文件。** 不予采纳,因为文件系统或包发现不是运行时所有权约,会让 bundle 发布含义不清,也无法表达显式 Cordis 加载顺序或依赖安装。构建期生成与校验以及测试 host 可以枚举源码树,因为它们验证的是仓库完整性,而不是组合已发布的部署。
- **根据当前已加载包集合验证 allow/block 条目。** 不予采纳,因为零匹配模式可能有意指向稍后加载或 HMR 加载的贡献;当前加载顺序不能决定配置有效性。
## 后果
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.md
2026-07-20-canonical-tool-output-contract.md: 2429e1c141ad8c8ee932c6d654a6ba74bc4f7618
2026-07-20-canonical-tool-output-contract.zh.md: 26653ea769b8a642b70c4d3dd2f5ab907a70bd85
2026-07-20-canonical-tool-output-contract.zh.md: 534aa2ed7ad65719b280f64f73a412bf5b17a1de
@@ -1,4 +1,4 @@
# Agent Note: 规范工具输出
# Agent Note: 规范工具输出约
Status: implemented
@@ -8,7 +8,7 @@ Status: implemented
工具主体过去直接编写面向模型的 `ContentBlock[]`,并可选择将其与不透明的 `meta` 包装在一起。因此,Native 模式的 Function Calling(函数调用)虽然拥有可供人阅读的投影,但程序化调用方没有稳定的领域值:Code Mode 会将内容块重新展平为字符串,动态工具会重复定义内容形态,策略也可以替换展示内容,却无法区分这项变更究竟是替换展示,还是替换操作结果。多个能力 seam 已经返回了信息更丰富的提供方值,却又在面向模型的工具边界丢弃这些值。
持久会话约将这份展示内容视为回放时的权威来源,但如果持久化每一个信息丰富的中间值,就会扩大日志、使实现数据进入压缩(compaction)和迁移流程,还会错误地把执行期本地 API 变成会话格式的一部分。因此,系统底层需要在执行期间保留一个类型化值,并显式将其投影为现有的持久化内容和模型可见内容。
持久会话约将这份展示内容视为回放时的权威来源,但如果持久化每一个信息丰富的中间值,就会扩大日志、使实现数据进入压缩(compaction)和迁移流程,还会错误地把执行期本地 API 变成会话格式的一部分。因此,系统底层需要在执行期间保留一个类型化值,并显式将其投影为现有的持久化内容和模型可见内容。
## 决策
@@ -74,6 +74,6 @@ MCP 桥接层通过 `McpResult<{...}> = { content: JsonValue[]; structuredConten
## 影响
Native 和回放行为仍以内容为先,并保持逐字节兼容;执行期调用方则无需解析内容,即可使用经过校验的领域值。失败结果必须包含消息,并可选择附加内部类名/代码信息;成功与失败结果由判别字段区分,失败结果绝不会承诺存在值。工具作者必须一并设计值及其 Native 投影;增加这项声明是有意为之,因为它避免从自然语言内容意外推导出程序化约。
Native 和回放行为仍以内容为先,并保持逐字节兼容;执行期调用方则无需解析内容,即可使用经过校验的领域值。失败结果必须包含消息,并可选择附加内部类名/代码信息;成功与失败结果由判别字段区分,失败结果绝不会承诺存在值。工具作者必须一并设计值及其 Native 投影;增加这项声明是有意为之,因为它避免从自然语言内容意外推导出程序化约
中间值只受产生它们的能力和进程内存限制。日志不包含这些值,因此回放无法恢复;仅处理内容的 post 策略也无法隐藏这些值。这些都是执行期本地约的明确属性,并非意外缺口。
中间值只受产生它们的能力和进程内存限制。日志不包含这些值,因此回放无法恢复;仅处理内容的 post 策略也无法隐藏这些值。这些都是执行期本地约的明确属性,并非意外缺口。
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-20-unified-json-value-schema-dsl.md
2026-07-20-unified-json-value-schema-dsl.md: 5de3523eab15a91ea32dc09e2e239146fadea6f1
2026-07-20-unified-json-value-schema-dsl.zh.md: 8521e03beb7398ebfdf5e5cddae7a99471aa38d7
2026-07-20-unified-json-value-schema-dsl.zh.md: 0dc38dc289a32269c0f9e545ae53a8098ba02189
@@ -6,7 +6,7 @@ Status: implemented
## 问题
工具参数使用一套精简的作者侧 schema DSL,subagent/工作流的结构化输出则使用另一套原始 JSON Schema 子集和校验器。两套词汇在根类型、标量约束和校验方式上并不一致;如果继续沿用这种划分,类型化的规范工具输出约要么还需重复实现两条路径,要么只能接受部分投影无法强制执行的 schema。
工具参数使用一套精简的作者侧 schema DSL,subagent/工作流的结构化输出则使用另一套原始 JSON Schema 子集和校验器。两套词汇在根类型、标量约束和校验方式上并不一致;如果继续沿用这种划分,类型化的规范工具输出约要么还需重复实现两条路径,要么只能接受部分投影无法强制执行的 schema。
## 决策
@@ -33,5 +33,5 @@ Status: implemented
- 显式的对象开放方式和类型正确的字面量约束会让格式错误的声明在编写或注册阶段快速失败,而不是拖到后续模型调用时才失败。
- 有界类型推导会为常规声明保留有用的精确类型,并将异常深的尾部结构退化为 `JsonValue`;运行时 schema 强制执行在任意深度仍保持精确。
- 原始工具仍可直接注册范围更广的 JSON Schema,但统一代码生成会把不受支持的 schema 视为未知类型,不会假装自己能够强制执行。
- 每个属性的 `required: true` 仍是工具作者约;原有推导路径暴露可选性缺陷后,类型级回归覆盖会锁定必填键不得为可选。
- 每个属性的 `required: true` 仍是工具作者约;原有推导路径暴露可选性缺陷后,类型级回归覆盖会锁定必填键不得为可选。
- 运行时测试和编译期测试覆盖所有根类型、恰好匹配一个分支时的重叠/无匹配行为、原始 schema 的默认开放语义、显式开放方式、有损 JSON 值、类型推导、核心投影和动态投影中的深层嵌套、动态注册中 JSON 不可见的键,以及非普通 schema 数组。
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.md
2026-07-22-slot-type-chain-implementation.md: 2f0ec32766100e492c68c474f8798be3df0a3d15
2026-07-22-slot-type-chain-implementation.zh.md: 75e89d3f57b96a1699981123e8361c775db2b8a7
2026-07-22-slot-type-chain-implementation.zh.md: 6ef7eaa7614b88b33f0fa2aa6089e97eb3a45c25
@@ -55,7 +55,7 @@ ctx.slots.register({
第四种 `SlotKind`——`'chain'`——把路由权相对 `keyed` 反转:keyed 的分派现场以 `entryKey` 点选占用 slot 的 entrychain 则由 entry 自荐——owner 只分派一套格式统一的 owner props,永远不知道谁来接管,新的接管包注册进来 owner 零改动。chain 注册携带一个 `select` 纯选择器(`ChainSelect<O, M>``(owner) => matched | null`)与可选的 `priority`(升序;同值保持注册序 = 装配序——部署可控的 inject 拓扑——复用 list `order` 的同一稳定排序);注册缺 `select` 即上文装载即炸情形之一。渲染时 outlet 按链序依次执行各 select:首个非 null 返回值当选,该值以 `matched` 并入组件 props(组件绝不自行重新推导匹配);返回 `null` 则轮到下一个 entry;全 null 则渲染 owner 的 fallback 体(`ChainRenderOpts`)。
「不接」的判定住在 `select` 里,绝不在挂载后的组件里自探 props:组件为了渲染 null 也得先挂载,其 hook 与 effect 全部白跑,随之而来的挂载/卸载抖动还会破坏 memo 化与 React key 语义;而选择器是纯函数——可单测、零挂载副作用——与「presentation methods are pure functions of `args`」是同一条纪律。纯,就是选择器的约:不读外部可变状态、不产副作用,路由判定因此完全是 owner props 的函数,每次分派都可安全执行。选择器只做路由,绝不创建新对象——按分派逐次构造对象会让引用每次渲染都换新;把匹配值包成更丰富的面这件事,发生在当选组件内部(以 `matched` 为依赖的 `useMemo`)。
「不接」的判定住在 `select` 里,绝不在挂载后的组件里自探 props:组件为了渲染 null 也得先挂载,其 hook 与 effect 全部白跑,随之而来的挂载/卸载抖动还会破坏 memo 化与 React key 语义;而选择器是纯函数——可单测、零挂载副作用——与「presentation methods are pure functions of `args`」是同一条纪律。纯,就是选择器的约:不读外部可变状态、不产副作用,路由判定因此完全是 owner props 的函数,每次分派都可安全执行。选择器只做路由,绝不创建新对象——按分派逐次构造对象会让引用每次渲染都换新;把匹配值包成更丰富的面这件事,发生在当选组件内部(以 `matched` 为依赖的 `useMemo`)。
类型链上,chain entry 的 SlotMap 形状是 `{ kind: 'chain'; scope; owner }``owner` 即链的货币;`M`——`matched` prop 的类型——从 select 返回值推导(选择器收窄 union 成员时,`matched` 类型自动随之收窄),且组件位不参与 `M` 的推断,与钉住 inject 份额的 NoInfer 裁定同源(见下文裁定)。owner 侧,`renderSlotChain(key, owner, { fallback })` 与 `renderSlot` 同住 `PropsRenderSlots` 份额,其键域静态收窄到本 entry children 声明中 chain kind 的键(`ChainKeysOf`);分派现场只有一行,不含任何自有的派生或路由逻辑。
@@ -117,5 +117,5 @@ register 签名里的两条硬化裁定之所以存在,是因为显然的替
| 模块级 store 句柄 | 模块级句柄是跨插件重载与跨测试用例的单例;工厂形把身份圈定在单次 apply/测试调用内 |
| 组件直收 store 实例 | 渲染代码里能用 `update`/`set`,变更面就无从审计;声明的 actions 让「什么能变」保持为 register 现场的事实 |
| 注册位用 `FC` / 从组件推断 `I` | FC 静态位产生协变噪音、拒绝合法组件;组件侧推断静默吸收 props 漂移(见上文裁定) |
| 接管 slot 用 keyed 分派 + owner 侧路由 | owner 会不断攒下逐 entry 约与硬编码路由表(每种接管一份 `find` + `entryKey`);chain 货币让新增接管注册保持 owner 零改动 |
| 接管 slot 用 keyed 分派 + owner 侧路由 | owner 会不断攒下逐 entry 约与硬编码路由表(每种接管一份 `find` + `entryKey`);chain 货币让新增接管注册保持 owner 零改动 |
| 组件靠渲染 null 表示不接 | 不接也得先挂载——hook 与 effect 白跑,挂载/卸载抖动破坏 memo 化与 key 语义;纯选择器无需组件实例即可裁决 |
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-22-unified-send-and-coalesced-user-messages.md
2026-07-22-unified-send-and-coalesced-user-messages.md: f32d6ca65d5236e1fabdd177cdf54e36929c853f
2026-07-22-unified-send-and-coalesced-user-messages.zh.md: 7647742166bb760139506f93af672f323b3b7b97
2026-07-22-unified-send-and-coalesced-user-messages.zh.md: 6148cf4e0dda9032fca0ced5b466787877b89fa0
@@ -18,13 +18,13 @@ agent 的对外驱动接口逐渐长出三个近乎平行的动词——`send`
**context/message 已移除。** 注入的上下文在 inbox 中使用同一个 `UserMessage` 值,并在获准时成为 `user/message` 事件;上下文生产方显式提供合适的非 `user` 类别 `source`,类型化 source 变体携带所有特定于领域的持久来源信息。对外接口、派生逻辑和 `SurfaceEventType` 都不再包含 `context/message`;需要判断“这是不是一条人类提示词?”的消费方改为读取 `source.kind === 'user'`,而不是事件类型。
**Goal 继续执行归属使用正数 Round。** Goal 生命周期状态通过后续 [Goal 自有持久事件决策](2026-07-31-goal-owned-durable-events.md)定义的领域自有 `goal/change` 事件提交。正数 Round 只从已准入的继续执行 `user/message` 推进;goal 持久化不使用注入或 inbox 状态。
**Goal 继续执行归属使用正数 Round。** Goal 生命周期状态通过后续[Goal 自有持久事件决策](2026-07-31-goal-owned-durable-events.md)定义的领域自有 `goal/change` 事件提交。正数 Round 只从已准入的继续执行 `user/message` 推进;goal 持久化不使用注入或 inbox 状态。
**`send` 不返回标识。** 调用方已经持有完整消息及其不透明的 `MessageId`;消息的创建与冻结由[带标识的不可变消息值决策](2026-07-28-identified-immutable-message-values.md)负责,而不是由路由负责。
**Inbox 变更只有一份持久投影和三种最小实时通知。** 每次 append、prepend、编辑、删除、取消与领取都会记录规范化的 `agent/inbox/spliced` 坐标。插入会发出 `agent/inbox/inserted { message }`;普通删除携带持久 `outcome: 'canceled'`,并发出 `agent/inbox/discarded { message }`;循环的原子 `claim()` 会记录纯删除 splice,随后发出 `agent/inbox/claimed { message, turn }``MessageId` 是唯一的单次出现标识,并在两个待处理列表间保持唯一。实时载荷刻意不携带 placement、outcome 或批次封套,因为这些事实由持久 splice 持有。
**pre-step 会领取 next-step 输入,但不会为它单独创建轮次。** steering 和注入始终进入同一个 next-step inboxsteering 会唤醒驱动器,注入则不会。在轮次边界,驱动器会原子领取待处理的 next-step 输入,再领取一条排队提示词;在步骤之间则只领取 next-step 输入。领取会记录纯删除 splice,并针对每条消息发出一次 `agent/inbox/claimed { message, turn }`。随后 `agent/pre-step` 会拒绝拟议步骤,或返回进入步骤的完整批次。reject 与监听器失败都会让已领取批次保持已删除;领取后才到达的输入会等待后续边界。
**pre-step 会领取 next-step 输入,但不会为它单独创建轮次。** steering 和注入始终进入同一个 next-step inboxsteering 会唤醒驱动器,注入则不会。在轮次边界,驱动器会原子领取待处理的 next-step 输入,再领取一条排队提示词;在步骤之间则只领取 next-step 输入。领取会记录纯删除 splice,并针对每条消息发出一次 `agent/inbox/claimed { message, turn }`。随后 `agent/pre-step` 会拒绝拟议步骤,或返回进入步骤的完整批次。拒绝与监听器失败都会让已领取批次保持已删除;领取后才到达的输入会等待后续边界。
**一条已接受消息只保留一种表示。** 持久的用户角色输入和附加的模型可见上下文都直接使用带标识且冻结的 `UserMessage`。循环把该值与私有路由状态存放在一起,不会将其标识、内容或来源复制到另一种公开形状中。steering、注入和工具产生的上下文都会在 next-step inbox 中保留各自带标识的消息。[带标识的不可变消息值决策](2026-07-28-identified-immutable-message-values.md)取代了本记录此前的 `UserMessageData`/`AgentMessage` 层级,并将这一表示扩展到 assistant 消息和工具结果消息。
@@ -41,7 +41,7 @@ agent 的对外驱动接口逐渐长出三个近乎平行的动词——`send`
## 后果
投递接口现在是一个原语加三个自解释的预设,(`target` × `wakeup`) 矩阵把此前无法表达的组合显式化。同一个带标识消息值同时服务提示词、注入的上下文和 Goal Round,因此每一处“是否人类提示词?”检查都简化为一次 `source` 判断。`Agent` 约仍是接口,因此其他实现和对象字面量形式的测试替身只需实现同一个最小结构接口。正数 Goal Round 从已准入的 `user/message` 事件折叠,而 goal 生命周期状态位于投递接口之外。空闲注入会保持待处理,不打开轮次也不运行模型;后续会唤醒的投递在 pre-step 将其放入进入步骤的批次时,它才成为 `user/message`
投递接口现在是一个原语加三个自解释的预设,(`target` × `wakeup`) 矩阵把此前无法表达的组合显式化。同一个带标识消息值同时服务提示词、注入的上下文和 Goal Round,因此每一处“是否人类提示词?”检查都简化为一次 `source` 判断。`Agent`仍是接口,因此其他实现和对象字面量形式的测试替身只需实现同一个最小结构接口。正数 Goal Round 从已准入的 `user/message` 事件折叠,而 goal 生命周期状态位于投递接口之外。空闲注入会保持待处理,不打开轮次也不运行模型;后续会唤醒的投递在 pre-step 将其放入进入步骤的批次时,它才成为 `user/message`
`wakeup` 是“模型是否应当运行”的信号,因此 inbox 会区分能唤醒的排队工作与任何可领取的项:一个孤立的 `next-turn`/no-wakeup 队列项会停泊在空闲状态,并随下一次唤醒 send 一同带出,而 `whenIdle`/`cancel` 依据唤醒信号来结算完全停稳。每次插入与退出都会发布对应的实时通知,特定于领域的持久事实则通过类型化消息 source 传递,而非通过平行的元数据通道。直接使用待处理消息的表示方式,使持久 splice 与实时事件保持可关联,既无需维护第二个 steering 包装层,也避免数据发生分歧。后续的[已领取 pre-step inbox 生命周期](2026-07-31-claimed-pre-step-inbox-lifecycle.md)决策保留通过 `MessageId` 寻址的实时队列变更,并把单消息生命周期通知与持久的整体队列 splice 投影分离。
@@ -50,4 +50,4 @@ agent 的对外驱动接口逐渐长出三个近乎平行的动词——`send`
- [one-send-one-turn](../simplification/2026-07-17-one-send-one-turn.md)——本决策所依托的“每轮次只认领一条消息”规则。
- [remove-agent-steering-mirror](../../archived/simplification/2026-07-04-remove-agent-steering-mirror.md)——折叠镜像实时事件的先例。
- [explicit-turn-cancellation](2026-07-16-explicit-turn-cancellation.md)——`keepInbox` 所扩展的取消原因信号。
- [带标识的不可变消息值](2026-07-28-identified-immutable-message-values.md)——本路由决策现在所依托的消息标识与表示约。
- [带标识的不可变消息值](2026-07-28-identified-immutable-message-values.md)——本路由决策现在所依托的消息标识与表示约
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-23-toolview-dissolution.md
2026-07-23-toolview-dissolution.md: be1bd9d161714194855988d76a632c667fae8c84
2026-07-23-toolview-dissolution.zh.md: afe5e03e8345fca2a0f097d86873974f6d417ff2
2026-07-23-toolview-dissolution.zh.md: c3f301002aac4c443dedc76fa747d8b45be6577a
@@ -4,7 +4,7 @@ Status: implemented
[English](2026-07-23-toolview-dissolution.md) | 中文
> 范围:独立工具环(ToolViewRegistry/ctx.toolviews/outlet)为何退役、被什么取代。本决策产出的落地态叙述归 [Web 客户端架构注](2026-07-19-gui-web-client-architecture.md);一切现在所运行其上的注册模型归 [slot 体系标准](2026-07-22-slot-type-chain-implementation.md)所有。后续的 [Client Tool 展示所有权](2026-08-08-client-tool-presentation-ownership.md)决策仅取代本篇的 per-view 放置方式:Tool 名称分发仍使用 keyed slot,而非平行注册表。
> 范围:独立工具环(ToolViewRegistry/ctx.toolviews/outlet)为何退役、被什么取代。本决策产出的落地态叙述归 [Web 客户端架构注](2026-07-19-gui-web-client-architecture.md);一切现在所运行其上的注册模型归 [slot 体系标准](2026-07-22-slot-type-chain-implementation.md) 所有。后续的 [Client Tool 展示所有权](2026-08-08-client-tool-presentation-ownership.md) 决策仅取代本篇的 per-view 放置方式:Tool 名称分发仍使用 keyed slot,而非平行注册表。
## Problem
@@ -14,15 +14,15 @@ Status: implemented
工具环作为独立基础设施已消失:工具行是**各视图为自己声明的 keyed 子槽**,client 全域只剩一种注册模型。上述理由是空的——keyed slot 的 *key 空间*本就运行时开放(SlotMap 声明槽、从不声明 keyask-user composer 的 `key: 'question'` 即先例),开放的 tool 名集合天然适配 `entryKey` 分发。
本决策最初把 `'conversation.chat.toolview'` 放在 chat 条目下,由 chat 渲染点逐行分发。后续的 [Tool 展示所有权](2026-08-08-client-tool-presentation-ownership.md)入整体 Tool 席位,并让 `ui-tool` 拥有唯一的 keyed `'tool.call.toolview'` 子 slot。后续决策改变的是展示所有者,而非本决策的核心约束:Tool 注册继续使用普通 keyed-slot 机制,激活、替换、缓存、错误隔离、版本与 fallback 行为仍归框架所有。
本决策最初把 `'conversation.chat.toolview'` 放在 chat 条目下,由 chat 渲染点逐行分发。后续的 [Tool 展示所有权](2026-08-08-client-tool-presentation-ownership.md) 将该放置方式移入整体 Tool 席位,并让 `ui-tool` 拥有一个 keyed `'tool.call.toolview'` 子 slot。后续决策改变的是展示所有者,而非本决策的核心约束:Tool 注册继续使用普通 keyed-slot 机制,激活、替换、缓存、错误隔离、版本与 fallback 行为仍归框架所有。
## 接受的语义变化
四项行为增量是刻意接受而非疏漏。跨视图出场最初采用逐视图注册;后续 Note 记录了 root/subcall 编排为何足以支持一个 Tool 级展示所有者。同 key 重复注册从注册表的 later-wins 静默覆盖变为 loud throw——纪律修正而非损失。会话维分发若行需要,归组件内部(标配 kit 已带 `useSessions`),不走注册表谓词——今天没有已落地的会话变体样例。第三方在 registry 级覆盖形态(scoped 注册压过 global)不复存在;真出现的未来需求走 key 命名空间约定或组件内小 resolver,永不复活平行注册表。
四项行为增量是刻意接受而非疏漏。跨视图出场最初采用逐视图注册;后续 Note 记录了为何 root/subcall 编排后来证明由一个 Tool 级展示所有者统一负责是合理的。同 key 重复注册从注册表的 later-wins 静默覆盖变为 loud throw——纪律修正而非损失。会话维分发若行需要,归组件内部(标配 kit 已带 `useSessions`),不走注册表谓词——今天没有已落地的会话变体样例。第三方在 registry 级覆盖形态(scoped 注册压过 global)不复存在;真出现的未来需求走 key 命名空间约定或组件内小 resolver,永不复活平行注册表。
## Alternatives considered
**保留独立注册表(原形态)。** 拒绝:其多维分发的每一维都有更正确的家——展示所有权归显式声明的子 slot,会话维归已持有标配 kit 的组件内部。两步移完后剩下的只是一份没有任何独有能力的 slot 机器副本。
**保留独立注册表(原形态)。** 拒绝:其多维分发的每一维都有更正确的家——展示所有权归显式声明的子 slot,会话维归已持有标配 kit 的组件内部。剩下的只是一份没有任何独有能力的 slot 机器副本。
**把 `renderToolView` 提进标配 kit、注册表迁入 runtime 包。** 拒绝:Tool 展示是 Client UI 词汇;上提进 runtime 会把展示概念泄漏进数据对象层,且依然留着两套注册模型。
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-24-separate-context-injection-from-turn-execution.md
2026-07-24-separate-context-injection-from-turn-execution.md: bb28d96cf1494d94ad7a7d4a4714e536c7072d2f
2026-07-24-separate-context-injection-from-turn-execution.zh.md: fa9b9275cae67f4d5a34dac8a0236558596ab28f
2026-07-24-separate-context-injection-from-turn-execution.zh.md: 0edf3f4e8a062fe82462e263da383e27d79034c5
@@ -68,7 +68,7 @@ enter 分支的 `PreStepDecision.messages` 是拟议步骤的完整批次。wate
## 后果
- idle 注入要到后续 pre-step 让它进入步骤后才会对模型可见,并可能被取消或 dispose 丢弃,而其持久 inbox 生命周期仍会保留记录。
- idle 注入只有在后续 pre-step 接纳它后才会对模型可见,并可能被取消或 dispose 丢弃,而其持久 inbox 生命周期仍会保留记录。
- 两条连续的 user-role 消息会取代一条烘焙后的提示词消息;提供方适配器会保留这一顺序。
- 必须影响当前请求的上下文要从 `agent/pre-step` 返回;普通注入只保证由最近的后续边界交付。
- 公共投递约和收件箱记录保持精简:没有上下文附件、上下文放置元数据、提示词封套或重复的持久事件类型。
- 必须影响当前请求的上下文要从 `agent/pre-step` 返回;普通注入只支持在最近的后续边界交付。
- 公共投递约和收件箱记录保持精简:没有上下文附件、上下文放置元数据、提示词封套或重复的持久事件类型。
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-24-single-harness-home-resolver.md
2026-07-24-single-harness-home-resolver.md: 159ba88b7b4a8d50f1be2cbe5d9162a654014e16
2026-07-24-single-harness-home-resolver.zh.md: 89069c8e98e80be387fbc6b1630219b451be7bd6
2026-07-24-single-harness-home-resolver.zh.md: ed4701bbd4e37ec5c9fba02767efabfbef8d065d
@@ -32,7 +32,7 @@ explicit configured path > $DSH_HOME > ~/.dsh
**采用 XDG(遵从 `$XDG_CONFIG_HOME`,或把 config/data/cache 拆分到各自的目录树)。** 经过考虑后放弃,转而采用一个显而易见的根目录。单一的 `$DSH_HOME || ~/.dsh` 基准事实与 `~/.claude` / `~/.aws` 一致,无需对每个 `~/.dsh` 消费方按类别重新归类,也不留下任何需要协调的解析器不对称。telemetry 对齐到同一根目录——而不是保留自己的 XDG 路径——正是本决策所要消除的那种分歧。
**保留 telemetry 自己的 config 目录。** 它的 `deepseek-harness` 命名空间和独立的 XDG 策略是唯一违背 `dsh`/`~/.dsh` 约定的例外。把它折叠到共享解析器上,才让"单一 home 事实"成真。代价是匿名 id 的作用域从机器变成了 `$DSH_HOME`:若某个项目把 `DSH_HOME` 指向仓库本地路径(或某条命令在 telemetry 之前加载了项目的 `.env`),得到的就是 home 本地的 id,因此该 id 统计的是 harness home,而非机器。这被接受为单一根目录的应有含义——重定位 `$DSH_HOME` 会移动*全部* harness 状态,telemetry 身份也在其中——模块约据此表述为 per-harness-home 而非 per-machine。一个忽略 `$DSH_HOME` 的机器级全局身份,恰恰会重新引入本 Note 所要消除的那第二套 home 策略。
**保留 telemetry 自己的 config 目录。** 它的 `deepseek-harness` 命名空间和独立的 XDG 策略是唯一违背 `dsh`/`~/.dsh` 约定的例外。把它折叠到共享解析器上,才让"单一 home 事实"成真。代价是匿名 id 的作用域从机器变成了 `$DSH_HOME`:若某个项目把 `DSH_HOME` 指向仓库本地路径(或某条命令在 telemetry 之前加载了项目的 `.env`),得到的就是 home 本地的 id,因此该 id 统计的是 harness home,而非机器。这被接受为单一根目录的应有含义——重定位 `$DSH_HOME` 会移动*全部* harness 状态,telemetry 身份也在其中——模块约据此表述为 per-harness-home 而非 per-machine。一个忽略 `$DSH_HOME` 的机器级全局身份,恰恰会重新引入本 Note 所要消除的那第二套 home 策略。
## 影响
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.md
2026-07-25-web-client-session-scope-and-provide-channel.md: 3c51f06fca23a495f0fbc0cc4f1c289edea07b3b
2026-07-25-web-client-session-scope-and-provide-channel.zh.md: 06fc1005785d9d11b52839f91c3bb4b99cad7d63
2026-07-25-web-client-session-scope-and-provide-channel.zh.md: cccc0b6d8f7309c5344e042d9cc2171587a18cb5
@@ -4,7 +4,7 @@ Status: implemented
[English](2026-07-25-web-client-session-scope-and-provide-channel.md) | 中文
> 范围:client Agent scopeactx)与定向事件、client/host 实体化对等模型、空会话 blank 位与复用(`connectWorkspace`)、per-session 供数通道(`sessions.provide`),以及承载这些能力的 host wire 小件(summary `blank` 列、`host/session-added` 帧字段、`host/commands-changed` 帧)。输入状态机与 slash 管线见[输入状态机 note](2026-07-25-web-input-machine-and-slash-pipeline.md);命令业务面见[命令业务面 note](2026-07-25-web-command-surfaces-and-assembly.md)。
> 范围:client Agent scopeactx)与定向事件、client/host 实体化对等模型、空会话 blank 位与复用(`connectWorkspace`)、逐会话供数通道(`sessions.provide`),以及承载这些能力的 host wire 小件(summary `blank` 列、`host/session-added` 帧字段、`host/commands-changed` 帧)。输入状态机与 slash 管线见[输入状态机 note](2026-07-25-web-input-machine-and-slash-pipeline.md);命令业务面见[命令业务面 note](2026-07-25-web-command-surfaces-and-assembly.md)。
## 问题
@@ -61,7 +61,7 @@ Session 实例与 scope 同生命周期,存活资格 = host listed(一个判
「实体化但无首讯」的会话经 summary 派生位 `blank` 治理(派生列而非 header 字段,SessionHeader 保持不可变):
- host 判据:`session.events.length === 0`(零日志事件 = 尚无用户消息)。live 会话 `summarize()` 内存直读;cold 会话恒 `false`——lazy-create 约保证 never-appended 会话根本不进 `persistence.list()`JSONL/SQLite 两后端均已实证真 lazy),blank 从不落盘。
- host 判据:`session.events.length === 0`(零日志事件 = 尚无用户消息)。live 会话 `summarize()` 内存直读;cold 会话恒 `false`——lazy-create 约保证 never-appended 会话根本不进 `persistence.list()`JSONL/SQLite 两后端均已实证真 lazy),blank 从不落盘。
- wire 承载两处:`SessionSummary.blank` 必填列;`host/session-added` 帧必填 `blank` 字段(创建时恒 true,供别的 tab 按同一空会话状态入镜像)。
- client 镜像只降不升(单调),三来源翻转,全部复用既有 wire 信号:
- 发送方本地:首次 `prompt()` 的**成功响应**翻 false(受理即证明 user/message 已入 host 日志——此点翻转是确证而非乐观;`onEngaged` 同步更新列表镜像,当前 `New Session` 行原地转为普通标题,不新增列表行)。首讯被拒则会话保持 blank:与 host 权威对齐、继续显示为 `New Session`、在仍为该工作区成员时保持 connectWorkspace 复用资格。
@@ -74,10 +74,10 @@ Session 实例与 scope 同生命周期,存活资格 = host listed(一个判
`workspaces.connectWorkspace(workspaceId): Promise<SessionId>`(归属 WorkspacesService——它同时持有 workspace 规范 path 与 sessions 引用):
- 复用臂:list mirror 中找 `blank && cwd == workspace.path && sessionIds.includes(id)`——host 自己的成员规则,绝不只按 cwd。没有账户槽位的 cwd 匹配(CLI/TUI 在 host cwd 创建的会话,或已删除/重建的注册)会打开一个任何分组表面都无法显示在该工作区下的会话,因此落到新建臂(见[成员复用修复](../bug-fix/2026-08-05-workspace-blank-session-reuse-membership.md));命中直接返回该 id,不新建。
- 复用臂:list mirror 中找 `blank && cwd == workspace.path && sessionIds.includes(id)`——host 自己的成员规则,绝不只按 cwd。没有账户槽位的 cwd 匹配(CLI(命令行界面)/TUI 在 host cwd 创建的会话,或已删除/重建的注册)会打开一个任何分组表面都无法显示在该工作区下的会话,因此落到新建臂(见[成员复用修复](../bug-fix/2026-08-05-workspace-blank-session-reuse-membership.md));命中直接返回该 id,不新建。
- 新建臂:未命中则 `session.create({workspaceId})`,返回新 id。
- 未知 workspaceId fail loud(不静默创建到别处)。
- 解析保证(两臂同约):promise resolve 时返回的 id 已在 list store 且 `sessions.binding(id)` 同步可解析——`SessionsService.create` 在 RPC 成功后同步投影列表再 resolve,使 draft 搬运方可以在 open 之前往新 scope 的 machine 写文本,不等 notifier flush。
- 解析保证(两臂同约):promise resolve 时返回的 id 已在 list store 且 `sessions.binding(id)` 同步可解析——`SessionsService.create` 在 RPC 成功后同步投影列表再 resolve,使 draft 搬运方可以在 open 之前往新 scope 的 machine 写文本,不等 notifier flush。
- 调用方拿 id 自行 `sessions.open`;首讯发送就是普通 `session.prompt`——会话本来就在,失败即普通 prompt 失败,draft 文本还在 machine 里,重试即再次发送。
- 全局 New Session 按钮默认取 `recentWorkspaceId`:先比较各 Workspace 内 Session 的最新 `updatedAt`,无 Session 时回退 Workspace `createdAt`,同值保持 Host 顺序;只有完全没有 Workspace 时才 `sessions.clear()` 进入无会话视图。Workspace 分组内的创建动作仍显式命中该 Workspace。
- 运行时启动时订阅首次完整基线:若已有恢复成功的 current 会话则保持不动,否则自动 `connectWorkspace(recentWorkspaceId)` 并 open 返回的 blank 会话。该策略只结算一次;之后用户主动 clear 不会再次被自动选择覆盖,连接失败则等下一次基线投影重试。
@@ -131,6 +131,6 @@ slot scope 是闭集 `root | session-maybe | session`
- 插件获得与 host 同构的会话上下文:逐会话状态挂 actx、随 scope fiber 一次拆装,泄漏结构性不可能;双会话隔离由 scope filter 结构性保证。
- client 对象层收敛为 wire 镜像:会话身份、生命周期、能力判别全部以 host 实体为准——输入体系(下一层)面对的永远是「有真 Agent 的会话」,slash/skill 等提供方一律以 sessionId 直接寻址。
- 空会话治理零专用机制:状态靠一个派生位,可见性靠统一列表投影(仅 current blank 以 `New Session` 展示),回收靠 lazy persistence 的既有约(重启蒸发),常规上限靠同 Workspace 复用。
- 空会话治理零专用机制:状态靠一个派生位,可见性靠统一列表投影(仅 current blank 以 `New Session` 展示),回收靠 lazy persistence 的既有约(重启蒸发),常规上限靠同 Workspace 复用。
- 代价:id→ctx 换乘纪律、provide 的 Concurrent 纪律都是约定而非类型强制,靠 review 与测试钉住;「未选 workspace」期间输入全禁是产品面接受的体验代价(单一状态轴换来的)。
- 已知欠账:approval/question 跨 prune 恢复(TODO);模型选择以 live-mutation 形状回归(host `selectModel` 三件套现成,等独立分支)。
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-25-web-command-surfaces-and-assembly.md
2026-07-25-web-command-surfaces-and-assembly.md: 4c4a400abab940baebc1699fc15b709419865f0c
2026-07-25-web-command-surfaces-and-assembly.zh.md: 092401751aa16c1a0ec4446d85a9f2591ee18425
2026-07-25-web-command-surfaces-and-assembly.zh.md: f0d460199dcd947776924380d51fa6ceb65c9fe2
@@ -19,7 +19,7 @@ Status: implemented
### ui-command`CommandService` + 按会话键控的 `CommandDirectory` + 逐会话 `PopupSelectController`
- 投影 `ClientSessionContext { sessionId }` 自持于 ui-slash 约(types.ts):会话恒 agent-backed,会话身份即命令能力的全部投影;wire 以 `{sessionId}` 寻址(`command.list` / `command.execute` 均是;host 从会话 header 解析 Agent)。
- 投影 `ClientSessionContext { sessionId }` 自持于 ui-slash 约types.ts):会话恒 agent-backed,会话身份即命令能力的全部投影;wire 以 `{sessionId}` 寻址(`command.list` / `command.execute` 均是;host 从会话 header 解析 Agent)。
- 目录按 `SessionId` 分区,per-key single-flight + epoch guard(旧拉取永不覆盖新态),`commands/changed` 全 key 软失效(旧快照继续服务、后台重拉)、`connection/reset` 全 key 硬失效并预热,Enter 必须等待当前 key 就绪、失败留草稿不降级。预热挂 source 的 `warm` 钩子——scope 出生时对全 roster 一次,即覆盖整个会话生命周期(会话能力自出生恒定)。
- `register(contribution)` 注册 client 命令(descriptor + `available(projection)` + popupSelect spec);候选合成 = host 目录 + contribution 可用性过滤,再过 query/positionhost/contribution 重名 fail loud。
- 命令三型按注册面派生,开发者不声明位置:host descriptor 带 `input` = **leadingInput**(回填 `/name ␣` + claim,继续打参数,仅限行首);client 注册 popupSelect spec = **popupSelect**(官方选择框壳,业务零组件);两者皆无 = **execute**(选中即执行,零 UI)。
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-25-web-input-machine-and-slash-pipeline.md
2026-07-25-web-input-machine-and-slash-pipeline.md: 39ef214a94fcd019f535fb60136d5dcc09b54e60
2026-07-25-web-input-machine-and-slash-pipeline.zh.md: 9b0ca0cadbc5e0212048b165f0d60d567a5639ad
2026-07-25-web-input-machine-and-slash-pipeline.zh.md: dbd1aebff58c12f33dfe146d5cf4895801035bce
@@ -50,7 +50,7 @@ occurrence 表与 chip 三投影:
### 跨插件输入改写:三个 scoped bail 事件
约声明在 ui-slash(依赖最底层),生产者经 `sctx.bail(sctx, ...)` 派发,唯一消费侧是 hub 建 shell 时挂在 sctx 上的三个 listener;返回 `true` ⟺ 机器过 phase + CAS 守卫并实际改写(发出事件 ≠ 修改成功,Space 是否 `preventDefault` 以返回值为准):
声明在 ui-slash(依赖最底层),生产者经 `sctx.bail(sctx, ...)` 派发,唯一消费侧是 hub 建 shell 时挂在 sctx 上的三个 listener;返回 `true` ⟺ 机器过 phase + CAS 守卫并实际改写(发出事件 ≠ 修改成功,Space 是否 `preventDefault` 以返回值为准):
- `slash/input-begin-command` `{claim, span}`——菜单 pick / Space 裁决出的命令 claim 回填(SlashController 派发)。
- `slash/input-insert-reference` `{reference, span}`——引用 chip 插入(SlashController 派发)。
@@ -74,13 +74,13 @@ occurrence 表与 chip 三投影:
- ConversationRoot 的 Hero 判据是 `sessionId === undefined || (composerPhase === 'blank' && (openState === 'open' || summaryBlank === true))`summary 已证实为空的 Session 在任何 open state 下都保持 Hero,未经证实的 Session 则在 loading 期间进入 settling。首次 submit 同步进入 engaging,失败也保留 composer 与错误上下文,不退回 blank Herosidebar 的 blank 位只在 prompt 成功受理后翻 false。
- 发送统一在 hub defaultSink:乐观清稿后只走 `session.prompt` 且固定 `mode:'queue'`Web UI 无 steer 入口;host 线缆上的 `mode:'steer'` 不经此 machine);失败且 live draft 仍为空才回填,用户已经继续输入则不覆盖。不存在 Draft materialize 或 attach 事务。
- blank Hero 改选 Workspace 时,外壳调用 `connectWorkspace`;目标 session 不同时把非空 draft 从当前 shell 搬到目标 shell,再 open 新 id,旧 blank session 留存但不再 current。
- Notifier 双位约:`dirty`(快照新鲜度,`ensureFresh` 拉取可清)与 `notifyPending`(通知欠账,只有 flush 清)各自独立——拉取不得吞推送,对象层推订阅者(watchTransaction)依赖这一保证。
- Notifier 双位约`dirty`(快照新鲜度,`ensureFresh` 拉取可清)与 `notifyPending`(通知欠账,只有 flush 清)各自独立——拉取不得吞推送,对象层推订阅者(watchTransaction)依赖这一保证。
### 纯文本引用(决策 21):text outcome 与 lexicon 装饰
skill/@subagent 引用不走占位符 + occurrence 身份链——pick 直接把 `/name ` `@name ` 原文插进 draft,chip 视觉纯派生:
- PickOutcome 增 `{text}` arm;新 scoped bail 事件 `slash/input-insert-text` `{text, span}`(与另三个同约:draftRev CAS、返回 true ⟺ 实际改写);facade.insertText 走 setDraft 拼接,机器零改动。
- PickOutcome 增 `{text}` arm;新 scoped bail 事件 `slash/input-insert-text` `{text, span}`(与另三个同约draftRev CAS、返回 true ⟺ 实际改写);facade.insertText 走 setDraft 拼接,机器零改动。
- source 可选 `lexicon?(session)` 钩子:同步热快照名录,`undefined` = 数据未热——零装饰、永不触发 fetch(渲染路径保持同步无副作用);配对的可选 `subscribeLexicon?(session, listener)` 钩子是名录在 warm 之后仍会变化(目录 settle、子代生灭)时的失效通道。controller 把各名录聚合进自己的 `lexicon` snapshot store(每次 source 通知重拉);scope 出生后才注册的 source 由 service 广播给活 controller,补 warm 并并入名录。
- `decorations.scanTextRefs`:词边界扫描 draft(行首/空白后的 `/name``@name``x/name` 永不命中)对照名录,命中即 `.textRef` markbackdrop 纯 range 高亮,同 hlToken);编辑破坏匹配形状下次扫描自然消失。
- 发送即原文(不再 `<skill>` 序列化);气泡侧 MessageItem 双形状装饰(legacy `<skill>` 标签 + 纯文本 token)。
@@ -116,7 +116,7 @@ skill/@subagent 引用不走占位符 + occurrence 身份链——pick 直接把
| ActiveCommand 中间态 / registerMode 模式注册表 / 从 draft 推导命令态 | claim 由 pick 路径显式建立——无表、无推导 |
| bindTarget/bindDraft 对象直连 | 反向耦合 + root 单例跨会话误配;scoped bail 事件保依赖倒置且路由结构性正确 |
| 统一 slash/input-apply 或全事件化 | 三个独立 payload 覆盖跨插件改写;异步链路保持 registry 显式调用 |
| contenteditable / 富文本树 | 兼容性差;textarea + U+FFFC + occurrence 表覆盖全部交互约 |
| contenteditable / 富文本树 | 兼容性差;textarea + U+FFFC + occurrence 表覆盖全部交互约 |
| draft 双持久化 {text, occurrences} | mirror 写剪贴板投影零新概念;chip 跨刷新降级可接受 |
| 原生 textarea undo 栈 | 受控 + 程序化写入下不可靠;粘贴两段 undo 语义只能自管 |
| InputBar 收 16 员 wiring 回调包 | 消费矩阵实证 11 员 InputBar 独占、1 员死成员;标准件通道让组件自取,键盘面包内私递 |
@@ -130,6 +130,6 @@ skill/@subagent 引用不走占位符 + occurrence 身份链——pick 直接把
## 后果
- 一个常驻 conversation 外壳承接 no-session/blank/active:无 session → blank 保持 ConversationRoot、Hero、root scope Workspace picker、scrollport、composer seat、InputBar 与 textarea;只有严格 session header 和 body outlet 开始承载内容。同一 blank session → engaging/active 也保持 InputBar 与 textarea。EmptyState 与受控 intent 链(`sessions.updateIntent`/`updatePendingPrompt`/`workspaces.sendSession`)随最后消费者一并删除。
- 输入面对命令零知识 + 可选依赖:无命令包时纯输入可用;`@` 引用与 skill 引用免费复用同一菜单/pick 管线。代价是空格/回车裁决是逐 source 轮询协议,其应答语义(同步/异步、undefined 含义)为冻结约。
- 输入面对命令零知识 + 可选依赖:无命令包时纯输入可用;`@` 引用与 skill 引用免费复用同一菜单/pick 管线。代价是空格/回车裁决是逐 source 轮询协议,其应答语义(同步/异步、undefined 含义)为冻结约
- 提交事务化(attempt seq + 漂移守卫)使晚到结果回灌、会话切换、concurrent 重放三类缺陷结构性不可能,由矩阵测试钉住。
- 已知欠账:chip 跨刷新保真(可复用粘贴匹配)未立项;subagent 引用的模型表示待业务立项。
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-26-packed-chunk-rows-by-default.md
2026-07-26-packed-chunk-rows-by-default.md: d6a044676604e4a4512a7a6674edb80e120b2f3c
2026-07-26-packed-chunk-rows-by-default.zh.md: 2488ab1d972b89b1248733482e8e4239b83797e2
2026-07-26-packed-chunk-rows-by-default.zh.md: 0e3e06e0d4185603cd21939589855a51797560a4
@@ -26,7 +26,7 @@ JSONL 后端会打包每个持久追加批次。原始模式 `compression: 'none
每个签入仓库的会话格式 JSONL fixture 都使用规范打包表示。`scripts/session-fixture-layout.snapshot.ts` 会在整个仓库中发现已跟踪的 `*.jsonl` 文件,以及未被忽略的新增未跟踪 JSONL 文件,选择首条记录为 `session` header 的文件,解码所有正文记录,并拒绝与 `packChunkRuns()` 输出不同的内容。因此,该清单无需维护路径列表即可覆盖 ACP、headless、TUI、`apps/web`、父会话、子会话以及未来的 fixture 名称。
ACP 和 headless 快照运行会采集默认 JSONL 后端的输出。TUI 和 web 的记录模式写入器会在写入 fixture 前,对内存事件应用 `packChunkRuns()`。人工编写的 `packed-chunks` ACP 场景在普通配置下运行,并保留全部 3 种打包行类型;其约先解码独立的源 fixture 和目标 fixture,再断言二者逐事件相等。
ACP 和 headless 快照运行会采集默认 JSONL 后端的输出。TUI 和 web 的记录模式写入器会在写入 fixture 前,对内存事件应用 `packChunkRuns()`。人工编写的 `packed-chunks` ACP 场景在普通配置下运行,并保留全部 3 种打包行类型;其约先解码独立的源 fixture 和目标 fixture,再断言二者逐事件相等。
聚焦的包测试保留非打包和混合布局输入,以验证读取器兼容性。这些测试不会让默认快照语料库豁免规范布局要求。
@@ -36,9 +36,9 @@ ACP 和 headless 快照运行会采集默认 JSONL 后端的输出。TUI 和 web
只要较旧分支仍可能携带 fixture 改动,测试政策和 ACP 快照 README 就会继续链接该命令。最新的开放 PR(Pull Request)清单确认每个受影响分支均已合并、关闭或符合规范后,[移除提案](../../proposed/process/2026-07-26-remove-packed-session-fixture-migrator.md)会删除该 CLI、包命令、本过渡章节和文档链接,并替换永久门禁中仅适用于该命令的修复指引。共享规范布局转换器与快照门禁保持永久存在。
### 验证
### 验证约
JSONL 持久化测试证明:省略选项时会写入打包行,显式传入 `false` 时会按每个事件一行的形式写入,两种形式都会加载为完全相同的事件。规范布局转换器单元测试覆盖 header 保留、非打包转换、非会话 JSONL、已打包输入的幂等性和畸形输入。无密钥快照门禁覆盖每个签入仓库的 fixture 和组装后的回放路径;文档门禁则确保配置默认值与双语约保持一致。
JSONL 持久化测试证明:省略选项时会写入打包行,显式传入 `false` 时会按每个事件一行的形式写入,两种形式都会加载为完全相同的事件。规范布局转换器单元测试覆盖 header 保留、非打包转换、非会话 JSONL、已打包输入的幂等性和畸形输入。无密钥快照门禁覆盖每个签入仓库的 fixture 和组装后的回放路径;文档门禁则确保配置默认值与双语约保持一致。
## 曾考虑的替代方案
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-26-task-registry-seam.md
2026-07-26-task-registry-seam.md: 57ac176cf6d2b0a50fcbcfacd77f6a26b462b582
2026-07-26-task-registry-seam.zh.md: d32de0cc62e1d7a2b5e5d70187e742aa0392fe29
2026-07-26-task-registry-seam.zh.md: b81940a8f778f1ed9a65bf96e3a66c047fd234c2
@@ -6,30 +6,30 @@ Status: implemented
## 问题
[后台任务运行时](2026-06-20-generic-long-running-tool-runtime.md)交付时把 `TaskService` 做成了单个具体包:`@deepseek-ai/dsh-tasks` 既拥有每个生产方和控制接口面向编程的 `ctx.tasks` 约,也拥有进程内实现(内存存储、结算簿记、所有者清理 effect、拆除)。这种捆绑重新耦合了仓库[能力 seam 规则](2026-06-13-capability-seams.md)本要分离的两种变化速率:一旦替换注册表的存储或生命周期后端,被搅动的就是同一个包,而生产方(`dsh-tool-bash``dsh-tool-pty``dsh-tool-subagent`)、控制接口(`dsh-tool-tasks`)和 `TaskKindMap` 扩展方正是从这个包导入类型与 `ctx.tasks` 接口。harness 中其余每项可替换能力——bash、pty、fs、skill(技能)、subagent、web、会话持久化——都已具备接口/实现/消费方三分;任务注册表曾是仅剩的 `core` 模式例外,仅由一条 `TODO(task-service-backend)` 注释把守。
[后台任务运行时](2026-06-20-generic-long-running-tool-runtime.md)交付时把 `TaskService` 做成了单个具体包:`@deepseek-ai/dsh-tasks` 既拥有每个生产方和控制接口面向编程的 `ctx.tasks`,也拥有进程内实现(内存存储、结算簿记、所有者清理 effect、拆除)。这种捆绑重新耦合了仓库[能力 seam 规则](2026-06-13-capability-seams.md)本要分离的两种变化速率:一旦替换注册表的存储或生命周期后端,被搅动的就是同一个包,而生产方(`dsh-tool-bash``dsh-tool-pty``dsh-tool-subagent`)、控制接口(`dsh-tool-tasks`)和 `TaskKindMap` 扩展方正是从这个包导入类型与 `ctx.tasks` 接口。harness 中其余每项可替换能力——bash、pty、fs、skill(技能)、subagent、web、会话持久化——都已具备接口/实现/消费方三分;任务注册表曾是仅剩的 `core` 模式例外,仅由一条 `TODO(task-service-backend)` 注释把守。
## 决策
`tasks/` 如今是一个 bash 三件套形态的三包能力家族:
- **`@deepseek-ai/dsh-tasks`(接口)**——抽象的 `TaskService extends Service`,拥有 `ctx.tasks`、八个方法的约(`start``list``get``read``kill``wait``onTaskDone``attachSurface`)、全部词汇类型(`TaskId``TaskKindMap``TaskStart``TaskHooks``TaskOutcome``TaskSnapshot``TaskRead``TaskDoneListener`),以及快照不变式配套插件。类级 JSDoc 陈述了每个实现都必须兑现的语义:注册的存续期长于生产方与控制接口的 fiber,有所有者的访问以会话为界,结算遵循首次结果优先且监听器错误被隔离,并且在没有附加任何控制接口时 `start` 拒绝启动工作。
- **`@deepseek-ai/dsh-tasks`(接口)**——抽象的 `TaskService extends Service`,拥有 `ctx.tasks`、八个方法的约`start``list``get``read``kill``wait``onTaskDone``attachSurface`)、全部词汇类型(`TaskId``TaskKindMap``TaskStart``TaskHooks``TaskOutcome``TaskSnapshot``TaskRead``TaskDoneListener`),以及快照不变式配套插件。类级 JSDoc 陈述了每个实现都必须兑现的语义:注册的存续期长于生产方与控制接口的 fiber,有所有者的访问以会话为界,结算遵循首次结果优先且监听器错误被隔离,并且在没有附加任何控制接口时 `start` 拒绝启动工作。
- **`@deepseek-ai/dsh-tasks-local`(实现)**——`LocalTaskService`,即原样迁移的进程内注册表:内存存储、按 kind 划分的计数器、等待方簿记、`TASK_WAIT_TIMEOUT` deadline 代码、所有者清理 effect,以及强制失败的拆除。`dsh-timeout` 依赖随之迁入此包;seam 包不含任何实现依赖。
- **`@deepseek-ai/dsh-tool-tasks`(消费方)**——保持不变;它注入 `'tasks'`,从不导入实现类型。
各组合在原先加载 `dsh-tasks` 的位置改为加载 `dsh-tasks-local`CLI(命令行界面)的 cordis.yml 配置项、`agent-spine-demo`、各测试 harness,以及工具目录生成器的启动流程。生产方的配置错误诊断信息(「background tasks unavailable: load …」)点名 `dsh-tasks`——即定义缺失的 `ctx.tasks` 服务的 seam 包;seam 自身的对外呈现(其 README 与直接挂载防线)会指向各实现,因此当另一个后端日后成为推荐默认时,生产方的消息依旧正确。生产方、`TaskKindMap` 声明合并和控制接口仍然只导入 `@deepseek-ai/dsh-tasks`
该 seam 保持进程内约语义不变:`TaskStart.run()` 仍然传入回调和确切的 `Agent` 对象,因此持久化或跨进程后端在能实现此接口之前仍有设计工作要做(身份、重启、所有权、观察)。这次拆分把该项未来工作移出了每个消费方的依赖图;它并不预先设计后端。
该 seam 保持进程内约语义不变:`TaskStart.run()` 仍然传入回调和确切的 `Agent` 对象,因此持久化或跨进程后端在能实现此接口之前仍有设计工作要做(身份、重启、所有权、观察)。这次拆分把该项未来工作移出了每个消费方的依赖图;它并不预先设计后端。
## 曾考虑的替代方案
**在第二个后端出现之前保持具体服务(维持现状)。**这正是运行时 Agent Note 当初的立场:在第二种实现出现前抽取接口,可能固化错误的边界。该方案落选,因为这条边界已不再是臆测:八个服务方法及其语义自引入以来在每一次生产方集成中都保持稳定,它们正是 `dsh-tool-tasks` 与各生产方已经面向编程的那套接口,而且仓库约定默认将可替换能力拆成三个包。剩余风险(持久化后端可能需要变更约)不因这次拆分而改变:无论拆分与否,这类变更都会落在 seam 包里;而若维持现状,它们今天还会连带搅动每个消费方的实现依赖。
**在第二个后端出现之前保持具体服务(维持现状)。**这正是运行时 Agent Note 当初的立场:在第二种实现出现前抽取接口,可能固化错误的边界。该方案落选,因为这条边界已不再是臆测:八个服务方法及其语义自引入以来在每一次生产方集成中都保持稳定,它们正是 `dsh-tool-tasks` 与各生产方已经面向编程的那套接口,而且仓库约定默认将可替换能力拆成三个包。剩余风险(持久化后端可能需要变更约)不因这次拆分而改变:无论拆分与否,这类变更都会落在 seam 包里;而若维持现状,它们今天还会连带搅动每个消费方的实现依赖。
**在单个包内仅抽取接口(在具体类旁导出一个抽象类)。**否决,因为它在运作层面并未分离任何东西:消费方依然依赖携带实现及其依赖项的那个包,而替换后端若不把本地实现纳入自身依赖图,就仍然无法发布。在这里,包边界才是独立演进的单位。
**拆出 `types.ts` 但让服务保持具体。**基于同样的理由否决:类型并不是 seam,`ctx.tasks` 及其方法约才是。生产方需要的是服务键和语义,而不只是类型形状。
**拆出 `types.ts` 但让服务保持具体。**基于同样的理由否决:类型并不是 seam,`ctx.tasks` 及其方法约才是。生产方需要的是服务键和语义,而不只是类型形状。
## 后果
换来的是:任务注册表如今与全仓库通行的 seam 形态一致;持久化、远程或带插桩的注册表将是一个实现八个抽象方法的同级包,这样的注册表落地时,任何生产方、控制接口或 `TaskKindMap` 扩展方都无需改动。seam 包的 README 陈述约;生命周期簿记方面的事实归实现包的 README 所有。注册表行为测试套件(所有者清理、结算、等待、拆除)随 `dsh-tasks-local` 存放;seam 包保留一个桩子类(stub subclass)测试,固定 `ctx.tasks` 下的注册行为与单一服务的重复注册行为,外加基于探针的不变式测试套件。
换来的是:任务注册表如今与全仓库通行的 seam 形态一致;持久化、远程或带插桩的注册表将是一个实现八个抽象方法的同级包,这样的注册表落地时,任何生产方、控制接口或 `TaskKindMap` 扩展方都无需改动。seam 包的 README 陈述约;生命周期簿记方面的事实归实现包的 README 所有。注册表行为测试套件(所有者清理、结算、等待、拆除)随 `dsh-tasks-local` 存放;seam 包保留一个桩子类(stub subclass)测试,固定 `ctx.tasks` 下的注册行为与单一服务的重复注册行为,外加基于探针的不变式测试套件。
代价是:多出一个包,即多一份 manifest(元数据清单)、tsconfig、README 与不变式配套插件;同时各组合必须点名实现包。`abstract` 在运行时会被擦除,而这个包名过去正是可挂载的具体注册表,因此直接挂载 seam 时,其构造函数会明确报错——一条陈旧的组合配置行会在加载时得到「load an implementation such as @deepseek-ai/dsh-tasks-local」,而不是一个未完整注册的 `ctx.tasks` 在远离错误配置处才失败。

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