From 8ea5cdd894d9db4680f83644b214faebd9df535d Mon Sep 17 00:00:00 2001 From: ZiyaZhang <199893125+ZiyaZhang@users.noreply.github.com> Date: Wed, 22 Jul 2026 03:07:36 -0700 Subject: [PATCH] docs(i18n): re-translate RFC batch with the prompt-v4 pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 146 篇 RFC 译文按 v4 基线(#348)重出:v4 模板+术语表、金标 few-shot、三段协议、切换行后处理;全量机械核对零异常(一处 task id 术语违规已修)。三篇超长 RFC(code-mode 已入,web-seam/ agent-scope/sandbox/cds-core 仍在长文档通道产出)随后补。 --- ...6-06-11-content-block-vocabulary.i18n.yaml | 2 +- .../2026-06-11-content-block-vocabulary.zh.md | 18 +-- .../2026-06-11-custom-schema-dsl.i18n.yaml | 2 +- .../2026-06-11-custom-schema-dsl.zh.md | 12 +- ...ev-invariants-over-deep-readonly.i18n.yaml | 2 +- ...11-dev-invariants-over-deep-readonly.zh.md | 34 +++--- ...026-06-11-event-sourced-sessions.i18n.yaml | 2 +- .../2026-06-11-event-sourced-sessions.zh.md | 18 +-- ...06-11-microkernel-event-taxonomy.i18n.yaml | 2 +- ...026-06-11-microkernel-event-taxonomy.zh.md | 24 ++-- ...026-06-11-runtime-arg-validation.i18n.yaml | 2 +- .../2026-06-11-runtime-arg-validation.zh.md | 12 +- ...-06-11-structured-error-taxonomy.i18n.yaml | 2 +- ...2026-06-11-structured-error-taxonomy.zh.md | 16 +-- ...-tool-schemas-in-prompt-assembly.i18n.yaml | 2 +- ...6-11-tool-schemas-in-prompt-assembly.zh.md | 12 +- .../2026-06-13-capability-seams.i18n.yaml | 2 +- .../2026-06-13-capability-seams.zh.md | 28 ++--- .../2026-06-13-twin-llm-adapters.i18n.yaml | 2 +- .../2026-06-13-twin-llm-adapters.zh.md | 18 +-- .../2026-06-14-session-persistence.i18n.yaml | 2 +- .../2026-06-14-session-persistence.zh.md | 28 ++--- ...6-06-15-turn-enclosure-invariant.i18n.yaml | 2 +- .../2026-06-15-turn-enclosure-invariant.zh.md | 36 +++--- ...06-17-filesystem-capability-seam.i18n.yaml | 2 +- ...026-06-17-filesystem-capability-seam.zh.md | 106 ++++++++-------- ...nt-lifecycle-and-ownership-seams.i18n.yaml | 2 +- ...-agent-lifecycle-and-ownership-seams.zh.md | 26 ++-- .../2026-06-18-session-surface.i18n.yaml | 2 +- .../2026-06-18-session-surface.zh.md | 46 +++---- ...ed-persistence-write-coordinator.i18n.yaml | 2 +- ...shared-persistence-write-coordinator.zh.md | 38 +++--- .../2026-06-20-branded-ids.i18n.yaml | 2 +- .../architecture/2026-06-20-branded-ids.zh.md | 46 +++---- ...-20-extract-example-app-packages.i18n.yaml | 2 +- ...6-06-20-extract-example-app-packages.zh.md | 52 ++++---- .../2026-06-20-package-hierarchy.i18n.yaml | 2 +- .../2026-06-20-package-hierarchy.zh.md | 38 +++--- ...andatory-app-attribution-headers.i18n.yaml | 2 +- ...21-mandatory-app-attribution-headers.zh.md | 74 ++++++------ ...06-26-file-context-as-event-gate.i18n.yaml | 2 +- ...026-06-26-file-context-as-event-gate.zh.md | 92 +++++++------- ...stdin-env-trusted-plugin-surface.i18n.yaml | 2 +- ...ash-stdin-env-trusted-plugin-surface.zh.md | 22 ++-- ...026-06-30-event-domain-semantics.i18n.yaml | 2 +- .../2026-06-30-event-domain-semantics.zh.md | 28 ++--- .../2026-07-02-fs-per-session-cwd.i18n.yaml | 2 +- .../2026-07-02-fs-per-session-cwd.zh.md | 28 ++--- ...2-result-time-applied-hunk-diffs.i18n.yaml | 2 +- ...07-02-result-time-applied-hunk-diffs.zh.md | 46 +++---- ...6-07-02-tool-render-intent-union.i18n.yaml | 2 +- .../2026-07-02-tool-render-intent-union.zh.md | 48 ++++---- ...ilesystem-directory-listing-seam.i18n.yaml | 2 +- ...03-filesystem-directory-listing-seam.zh.md | 44 +++---- ...bles-and-tool-guidance-ownership.i18n.yaml | 2 +- ...ariables-and-tool-guidance-ownership.zh.md | 64 +++++----- ...6-07-05-reconstructable-requests.i18n.yaml | 2 +- .../2026-07-05-reconstructable-requests.zh.md | 46 +++---- ...bagent-provider-lifecycle-events.i18n.yaml | 2 +- ...5-subagent-provider-lifecycle-events.zh.md | 30 ++--- ...6-07-06-timeout-deadline-library.i18n.yaml | 2 +- .../2026-07-06-timeout-deadline-library.zh.md | 52 ++++---- ...6-07-07-tool-call-timeout-policy.i18n.yaml | 2 +- .../2026-07-07-tool-call-timeout-policy.zh.md | 66 +++++----- .../2026-07-08-agent-scope-contexts.i18n.yaml | 2 +- .../2026-07-08-agent-scope-contexts.zh.md | 64 +++++----- ...-06-14-acp-agent-client-protocol.i18n.yaml | 2 +- ...2026-06-14-acp-agent-client-protocol.zh.md | 46 +++---- .../2026-06-14-acp-multi-session.i18n.yaml | 2 +- .../2026-06-14-acp-multi-session.zh.md | 30 ++--- .../feature/2026-06-15-code-mode.i18n.yaml | 2 +- .../feature/2026-06-15-code-mode.zh.md | 114 +++++++++--------- ...26-06-17-filesystem-tool-schemas.i18n.yaml | 2 +- .../2026-06-17-filesystem-tool-schemas.zh.md | 74 ++++++------ ...-acp-terminal-and-tool-rendering.i18n.yaml | 2 +- ...6-18-acp-terminal-and-tool-rendering.zh.md | 40 +++--- ...06-18-compaction-capability-seam.i18n.yaml | 2 +- ...026-06-18-compaction-capability-seam.zh.md | 90 +++++++------- ...6-06-21-subagent-capability-seam.i18n.yaml | 2 +- .../2026-06-21-subagent-capability-seam.zh.md | 54 ++++----- .../2026-06-22-acp-subagent-backend.i18n.yaml | 2 +- .../2026-06-22-acp-subagent-backend.zh.md | 36 +++--- .../2026-06-25-ask-user-question.i18n.yaml | 2 +- .../2026-06-25-ask-user-question.zh.md | 34 +++--- .../2026-06-29-todo-write-tool.i18n.yaml | 2 +- .../feature/2026-06-29-todo-write-tool.zh.md | 48 ++++---- .../feature/2026-06-30-hook-bridges.i18n.yaml | 2 +- .../feature/2026-06-30-hook-bridges.zh.md | 50 ++++---- .../2026-06-30-hook-protocol-lib.i18n.yaml | 2 +- .../2026-06-30-hook-protocol-lib.zh.md | 24 ++-- .../2026-06-30-interception-seams.i18n.yaml | 2 +- .../2026-06-30-interception-seams.zh.md | 44 +++---- ...026-06-30-session-store-fork-api.i18n.yaml | 2 +- .../2026-06-30-session-store-fork-api.zh.md | 26 ++-- ...26-06-30-subagent-observe-enrich.i18n.yaml | 2 +- .../2026-06-30-subagent-observe-enrich.zh.md | 20 +-- .../2026-07-05-dynamic-workflows.i18n.yaml | 2 +- .../2026-07-05-dynamic-workflows.zh.md | 70 +++++------ .../feature/2026-07-05-skill-system.i18n.yaml | 2 +- .../feature/2026-07-05-skill-system.zh.md | 40 +++--- .../2026-07-06-approval-seam.i18n.yaml | 2 +- .../feature/2026-07-06-approval-seam.zh.md | 92 +++++++------- .../2026-07-06-explicit-tool-order.i18n.yaml | 2 +- .../2026-07-06-explicit-tool-order.zh.md | 52 ++++---- .../2026-07-07-mcp-client-plugin.i18n.yaml | 2 +- .../2026-07-07-mcp-client-plugin.zh.md | 106 ++++++++-------- .../2026-07-07-session-prefix.i18n.yaml | 2 +- .../feature/2026-07-07-session-prefix.zh.md | 40 +++--- .../2026-07-08-repeat-tool-guard.i18n.yaml | 2 +- .../2026-07-08-repeat-tool-guard.zh.md | 56 ++++----- ...-self-referential-cordis-toolset.i18n.yaml | 2 +- ...7-08-self-referential-cordis-toolset.zh.md | 66 +++++----- ...2026-07-10-session-query-service.i18n.yaml | 2 +- .../2026-07-10-session-query-service.zh.md | 32 ++--- ...nt-persona-tool-filter-and-depth.i18n.yaml | 2 +- ...bagent-persona-tool-filter-and-depth.zh.md | 78 ++++++------ .../2026-06-11-doc-sync-enforcement.i18n.yaml | 2 +- .../2026-06-11-doc-sync-enforcement.zh.md | 26 ++-- .../2026-06-11-quality-gates.i18n.yaml | 2 +- .../process/2026-06-11-quality-gates.zh.md | 24 ++-- .../2026-06-11-tsdown-over-dumble.i18n.yaml | 2 +- .../2026-06-11-tsdown-over-dumble.zh.md | 26 ++-- ...26-06-11-vendor-cordis-as-source.i18n.yaml | 2 +- .../2026-06-11-vendor-cordis-as-source.zh.md | 24 ++-- .../2026-06-16-pnpm-over-yarn.i18n.yaml | 2 +- .../process/2026-06-16-pnpm-over-yarn.zh.md | 40 +++--- .../2026-06-17-ts-build-config.i18n.yaml | 2 +- .../process/2026-06-17-ts-build-config.zh.md | 65 +++++----- ...6-06-18-markdown-cross-link-lint.i18n.yaml | 2 +- .../2026-06-18-markdown-cross-link-lint.zh.md | 28 ++--- ...-20-core-data-structures-catalog.i18n.yaml | 2 +- ...6-06-20-core-data-structures-catalog.zh.md | 48 ++++---- ...6-06-20-generated-cordis-catalog.i18n.yaml | 2 +- .../2026-06-20-generated-cordis-catalog.zh.md | 36 +++--- .../2026-06-20-rfc-classification.i18n.yaml | 2 +- .../2026-06-20-rfc-classification.zh.md | 42 +++---- .../2026-07-02-tool-schema-catalog.i18n.yaml | 2 +- .../2026-07-02-tool-schema-catalog.zh.md | 46 +++---- ...-07-03-documentation-graph-atlas.i18n.yaml | 2 +- ...2026-07-03-documentation-graph-atlas.zh.md | 70 +++++------ ...4-cordis-jsdoc-completeness-gate.i18n.yaml | 2 +- ...07-04-cordis-jsdoc-completeness-gate.zh.md | 40 +++--- ...2026-07-04-doc-tiers-and-budgets.i18n.yaml | 2 +- .../2026-07-04-doc-tiers-and-budgets.zh.md | 26 ++-- ...-07-04-generate-rfc-index-tables.i18n.yaml | 2 +- ...2026-07-04-generate-rfc-index-tables.zh.md | 22 ++-- ...26-07-04-persistence-log-catalog.i18n.yaml | 2 +- .../2026-07-04-persistence-log-catalog.zh.md | 26 ++-- .../2026-07-05-uniform-rfc-format.i18n.yaml | 2 +- .../2026-07-05-uniform-rfc-format.zh.md | 30 ++--- ...-07-06-export-surface-jsdoc-gate.i18n.yaml | 2 +- ...2026-07-06-export-surface-jsdoc-gate.zh.md | 48 ++++---- ...6-07-06-generated-config-catalog.i18n.yaml | 2 +- .../2026-07-06-generated-config-catalog.zh.md | 34 +++--- .../2026-07-06-node-engine-floor.i18n.yaml | 2 +- .../2026-07-06-node-engine-floor.zh.md | 36 +++--- ...6-07-06-parallel-github-ci-gates.i18n.yaml | 2 +- .../2026-07-06-parallel-github-ci-gates.zh.md | 34 +++--- ...26-07-06-parallel-pre-push-gates.i18n.yaml | 2 +- .../2026-07-06-parallel-pre-push-gates.zh.md | 36 +++--- ...10-readme-known-limitations-gate.i18n.yaml | 2 +- ...-07-10-readme-known-limitations-gate.zh.md | 26 ++-- ...ackage-model-experience-contract.i18n.yaml | 2 +- ...12-package-model-experience-contract.zh.md | 32 ++--- ...-19-drop-mutable-session-summary.i18n.yaml | 2 +- ...6-06-19-drop-mutable-session-summary.zh.md | 22 ++-- ...llapse-trace-only-session-events.i18n.yaml | 2 +- ...0-collapse-trace-only-session-events.zh.md | 28 ++--- ...onsumed-llm-adapter-change-event.i18n.yaml | 2 +- ...-unconsumed-llm-adapter-change-event.zh.md | 22 ++-- ...nconsumed-llm-assembled-surfaces.i18n.yaml | 2 +- ...op-unconsumed-llm-assembled-surfaces.zh.md | 26 ++-- ...26-06-20-prune-dead-seam-methods.i18n.yaml | 2 +- .../2026-06-20-prune-dead-seam-methods.zh.md | 24 ++-- ...-06-20-public-agent-stop-surface.i18n.yaml | 2 +- ...2026-06-20-public-agent-stop-surface.zh.md | 24 ++-- ...ove-agent-boundary-mirror-events.i18n.yaml | 2 +- ...-remove-agent-boundary-mirror-events.zh.md | 22 ++-- .../2026-06-26-fsspec-style-fs-seam.i18n.yaml | 2 +- .../2026-06-26-fsspec-style-fs-seam.zh.md | 78 ++++++------ ...07-02-remove-stream-chunk-mirror.i18n.yaml | 2 +- ...026-07-02-remove-stream-chunk-mirror.zh.md | 22 ++-- ...6-07-04-drop-image-content-block.i18n.yaml | 2 +- .../2026-07-04-drop-image-content-block.zh.md | 16 +-- ...6-07-04-drop-inert-request-knobs.i18n.yaml | 2 +- .../2026-07-04-drop-inert-request-knobs.zh.md | 22 ++-- ...consumed-web-observation-surface.i18n.yaml | 2 +- ...p-unconsumed-web-observation-surface.zh.md | 20 +-- .../2026-07-04-fold-stdio-ui-helper.i18n.yaml | 2 +- .../2026-07-04-fold-stdio-ui-helper.zh.md | 18 +-- ...producerless-vocabulary-variants.i18n.yaml | 2 +- ...une-producerless-vocabulary-variants.zh.md | 24 ++-- ...7-04-prune-write-only-fs-surface.i18n.yaml | 2 +- ...26-07-04-prune-write-only-fs-surface.zh.md | 24 ++-- ...-04-remove-agent-steering-mirror.i18n.yaml | 2 +- ...6-07-04-remove-agent-steering-mirror.zh.md | 18 +-- ...26-07-04-share-app-bin-boot-glue.i18n.yaml | 2 +- .../2026-07-04-share-app-bin-boot-glue.zh.md | 22 ++-- ...4-tighten-hook-protocol-contract.i18n.yaml | 2 +- ...07-04-tighten-hook-protocol-contract.zh.md | 20 +-- ...m-acp-bridge-unreachable-surface.i18n.yaml | 2 +- ...-trim-acp-bridge-unreachable-surface.zh.md | 18 +-- ...unconsumed-skill-provider-events.i18n.yaml | 2 +- ...rop-unconsumed-skill-provider-events.zh.md | 20 +-- ...-12-prune-unused-web-seam-fields.i18n.yaml | 2 +- ...6-07-12-prune-unused-web-seam-fields.zh.md | 18 +-- ...026-06-11-property-based-testing.i18n.yaml | 2 +- .../2026-06-11-property-based-testing.zh.md | 22 ++-- .../2026-06-19-acp-snapshot-tests.i18n.yaml | 2 +- .../2026-06-19-acp-snapshot-tests.zh.md | 42 +++---- .../2026-06-19-real-api-e2e-ci.i18n.yaml | 2 +- .../testing/2026-06-19-real-api-e2e-ci.zh.md | 82 ++++++------- ...e-redundant-snapshot-log-goldens.i18n.yaml | 2 +- ...emove-redundant-snapshot-log-goldens.zh.md | 22 ++-- ...-fork-child-replay-seed-boundary.i18n.yaml | 2 +- ...6-22-fork-child-replay-seed-boundary.zh.md | 36 +++--- ...26-06-22-fork-snapshot-scenarios.i18n.yaml | 2 +- .../2026-06-22-fork-snapshot-scenarios.zh.md | 24 ++-- ...6-06-22-subagent-snapshot-replay.i18n.yaml | 2 +- .../2026-06-22-subagent-snapshot-replay.zh.md | 46 +++---- .../2026-07-04-hook-snapshot-matrix.i18n.yaml | 2 +- .../2026-07-04-hook-snapshot-matrix.zh.md | 38 +++--- ...-single-source-acp-replay-config.i18n.yaml | 2 +- ...7-04-single-source-acp-replay-config.zh.md | 22 ++-- ...t-header-content-in-one-scenario.i18n.yaml | 2 +- ...quest-header-content-in-one-scenario.zh.md | 28 ++--- ...7-08-shared-acp-snapshot-package.i18n.yaml | 2 +- ...26-07-08-shared-acp-snapshot-package.zh.md | 34 +++--- .../2026-06-16-typed-event-schemas.i18n.yaml | 2 +- .../2026-06-16-typed-event-schemas.zh.md | 72 +++++------ ...eneric-long-running-tool-runtime.i18n.yaml | 2 +- ...20-generic-long-running-tool-runtime.zh.md | 34 +++--- ...026-06-30-pre-tool-input-rewrite.i18n.yaml | 2 +- .../2026-06-30-pre-tool-input-rewrite.zh.md | 46 +++---- ...code-and-codex-subagent-backends.i18n.yaml | 2 +- ...ude-code-and-codex-subagent-backends.zh.md | 68 +++++------ ...-07-08-interactive-side-sessions.i18n.yaml | 2 +- ...2026-07-08-interactive-side-sessions.zh.md | 38 +++--- ...10-sqlite-session-query-provider.i18n.yaml | 2 +- ...-07-10-sqlite-session-query-provider.zh.md | 38 +++--- ...flow-progress-through-tool-calls.i18n.yaml | 2 +- ...workflow-progress-through-tool-calls.zh.md | 28 ++--- ...2026-06-11-api-extractor-reports.i18n.yaml | 2 +- .../2026-06-11-api-extractor-reports.zh.md | 14 +-- ...-06-11-architectural-conformance.i18n.yaml | 2 +- ...2026-06-11-architectural-conformance.zh.md | 16 +-- ...11-supply-chain-and-vendor-drift.i18n.yaml | 2 +- ...-06-11-supply-chain-and-vendor-drift.zh.md | 26 ++-- ...06-20-discover-package-inventory.i18n.yaml | 2 +- ...026-06-20-discover-package-inventory.zh.md | 26 ++-- ...06-20-unify-agent-and-session-id.i18n.yaml | 2 +- ...026-06-20-unify-agent-and-session-id.zh.md | 28 ++--- ...04-prune-dead-core-spine-surface.i18n.yaml | 2 +- ...-07-04-prune-dead-core-spine-surface.zh.md | 70 +++++------ ...plify-session-log-representation.i18n.yaml | 2 +- ...-simplify-session-log-representation.zh.md | 24 ++-- ...deterministic-and-stress-testing.i18n.yaml | 2 +- ...-11-deterministic-and-stress-testing.zh.md | 18 +-- .../2026-06-11-mutation-testing.i18n.yaml | 2 +- .../testing/2026-06-11-mutation-testing.zh.md | 22 ++-- ...-06-11-immutable-public-surfaces.i18n.yaml | 2 +- ...2026-06-11-immutable-public-surfaces.zh.md | 18 +-- ...-06-20-providerless-example-base.i18n.yaml | 2 +- ...2026-06-20-providerless-example-base.zh.md | 18 +-- ...ssembled-assistant-messages-only.i18n.yaml | 2 +- ...20-assembled-assistant-messages-only.zh.md | 24 ++-- ...2026-06-20-drop-acp-session-load.i18n.yaml | 2 +- .../2026-06-20-drop-acp-session-load.zh.md | 24 ++-- ...026-06-20-drop-acp-terminal-meta.i18n.yaml | 2 +- .../2026-06-20-drop-acp-terminal-meta.zh.md | 20 +-- ...-20-drop-bash-output-spill-files.i18n.yaml | 2 +- ...6-06-20-drop-bash-output-spill-files.zh.md | 16 +-- ...-20-drop-durable-step-boundaries.i18n.yaml | 2 +- ...6-06-20-drop-durable-step-boundaries.zh.md | 20 +-- ...6-20-drop-unused-session-lineage.i18n.yaml | 2 +- ...26-06-20-drop-unused-session-lineage.zh.md | 16 +-- ...ld-session-persistence-interface.i18n.yaml | 2 +- ...0-fold-session-persistence-interface.zh.md | 14 +-- ...026-06-20-generic-tool-rendering.i18n.yaml | 2 +- .../2026-06-20-generic-tool-rendering.zh.md | 18 +-- ...6-06-20-retire-mid-turn-steering.i18n.yaml | 2 +- .../2026-06-20-retire-mid-turn-steering.zh.md | 28 ++--- ...-06-20-single-session-acp-bridge.i18n.yaml | 2 +- ...2026-06-20-single-session-acp-bridge.zh.md | 14 +-- ...06-20-truncate-interrupted-turns.i18n.yaml | 2 +- ...026-06-20-truncate-interrupted-turns.zh.md | 26 ++-- ...nimplemented-subagent-vocabulary.i18n.yaml | 2 +- ...ne-unimplemented-subagent-vocabulary.zh.md | 30 ++--- ...apse-workflow-to-foreground-core.i18n.yaml | 2 +- ...collapse-workflow-to-foreground-core.zh.md | 28 ++--- ...ne-unused-skill-registry-surface.i18n.yaml | 2 +- ...-prune-unused-skill-registry-surface.zh.md | 22 ++-- 292 files changed, 2819 insertions(+), 2820 deletions(-) diff --git a/docs/rfc/implemented/architecture/2026-06-11-content-block-vocabulary.i18n.yaml b/docs/rfc/implemented/architecture/2026-06-11-content-block-vocabulary.i18n.yaml index 67537fda94..7ddbc75586 100644 --- a/docs/rfc/implemented/architecture/2026-06-11-content-block-vocabulary.i18n.yaml +++ b/docs/rfc/implemented/architecture/2026-06-11-content-block-vocabulary.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-11-content-block-vocabulary.md: 9414bda624fa6e5fc7e9b11b7a738d32b269af6b -2026-06-11-content-block-vocabulary.zh.md: 32308a5fe58f3a2e3c402982b7067811b9698218 +2026-06-11-content-block-vocabulary.zh.md: 764791cbef65c2031b8337af4f4cb6835e9d312a diff --git a/docs/rfc/implemented/architecture/2026-06-11-content-block-vocabulary.zh.md b/docs/rfc/implemented/architecture/2026-06-11-content-block-vocabulary.zh.md index 32308a5fe5..764791cbef 100644 --- a/docs/rfc/implemented/architecture/2026-06-11-content-block-vocabulary.zh.md +++ b/docs/rfc/implemented/architecture/2026-06-11-content-block-vocabulary.zh.md @@ -1,4 +1,4 @@ -# RFC:由 dsh-llm 持有的提供方无关内容块词汇 +# RFC:由 dsh-llm 拥有的提供方无关内容块词汇 Status: implemented @@ -10,19 +10,19 @@ harness 需要一套统一的内部消息语言,供 agent loop(智能体循 ## 决策 -自行持有词汇:消息是类型化内容块(`text`、`reasoning`、`tool-call`、`tool-result`)的数组,其联合类型派生自可合并扩展的 `ContentBlockMap`,插件通过声明合并添加新的块类型。同一套可合并扩展映射模式也用于所有「字符串化」字段的类型定义(`MessageSource`、`FinishReason`、`TurnTrigger`、`TurnEndReason`)。流式输出是原始分片协议;`BlockAssembler` 是唯一的共享组装实现。适配器负责转换为各提供方的协议格式(wire format):映射成本留在适配器中,这正是它该待的地方。 +自主拥有词汇:消息是类型化内容块的数组(`text`、`reasoning`、`tool-call`、`tool-result`),其联合类型派生自可合并扩展的 `ContentBlockMap`,插件通过声明合并添加新的块类型。同一可合并扩展映射模式为所有「字符串化」字段提供类型(`MessageSource`、`FinishReason`、`TurnTrigger`、`TurnEndReason`)。流式输出采用原始分片协议;`BlockAssembler` 是唯一的共享组装实现。适配器负责转换为提供方的协议格式(wire format)——映射成本留在适配器中,正是它该在的地方。 -会话内上下文注入(`context/message`、`steering/message`)渲染为带标签的 user-role 信封(system-reminder 模式),而非引入新 role,因此适配器零负担。真实适配器验证已确认该渲染方式在当前 DeepSeek 行为下有效;如果未来某个提供方出现不匹配,应在该适配器内处理,而非引入新的规范 role。 +会话内上下文注入(`context/message`、`steering/message`)渲染为带标签的 user-role 信封(system-reminder 模式),而非引入新角色,因此适配器无需承担额外负担。实际适配器验证已确认此渲染方式符合当前 DeepSeek 的行为;如果未来某提供方出现不兼容,应在该适配器内处理,而非引入新的规范角色。 ## 曾考虑的替代方案 -- **镜像 DeepSeek/OpenAI chat-completions 的结构**:对第一个提供方零映射成本,但对富内容(推理(reasoning)、作为结构化块的工具结果)处理起来别扭。 -- **原样采用 Anthropic Messages 的块结构**:经过实战检验,但规范类型将镜像一个 harness 并非首要对接的第三方 API。 +- **镜像 DeepSeek/OpenAI chat-completions 结构**:对第一个提供方零映射成本,但对富内容(推理、结构化块形式的工具结果)处理不便。 +- **原样采用 Anthropic Messages 块结构**:经过实战检验,但规范类型将镜像一个 harness 并非首要对接的第三方 API。 ## 后果 - 推理(reasoning)在核心层有了归属,无需依赖提供方特有的结构。 -- 多模态块只有在适配器、UI 与上下文压缩(context compaction)三方协同支持时才会回归;见[移除 image 内容块 RFC](../simplification/2026-07-04-drop-image-content-block.md)。 -- 缓存提示与 assistant prefill 在有实际适配器能兑现之前保持缺席;见[无生产者的变体](../simplification/2026-07-04-prune-producerless-vocabulary-variants.md)与[惰性请求旋钮](../simplification/2026-07-04-drop-inert-request-knobs.md) RFC。 -- 每个适配器都要承担翻译成本;首批真实适配器已验证了流式输出协议,后续新适配器应继续在适配器本地测试中证明其提供方特有的映射。 -- 跨包边界的 ID 使用品牌类型(`CallId`、`SessionId`、`AgentId`):零运行时成本的名义类型。 +- 多模态块只有在适配器、UI 和上下文压缩(context compaction)三方协同支持后才会回归;见 [drop-image RFC](../simplification/2026-07-04-drop-image-content-block.md)。 +- 缓存提示与 assistant prefill 在有实际适配器能兑现之前保持缺席;见 [producer-less variants](../simplification/2026-07-04-prune-producerless-vocabulary-variants.md) 与 [inert request knobs](../simplification/2026-07-04-drop-inert-request-knobs.md) RFC。 +- 每个适配器都需承担翻译成本;首批真实适配器已验证了流式输出协议,新适配器应继续在适配器本地测试中验证其提供方特有的映射。 +- 跨包(package)边界的 ID 使用品牌类型(`CallId`、`SessionId`、`AgentId`)——零运行时开销的名义类型。 diff --git a/docs/rfc/implemented/architecture/2026-06-11-custom-schema-dsl.i18n.yaml b/docs/rfc/implemented/architecture/2026-06-11-custom-schema-dsl.i18n.yaml index 5046fe3a38..6e7b3a2125 100644 --- a/docs/rfc/implemented/architecture/2026-06-11-custom-schema-dsl.i18n.yaml +++ b/docs/rfc/implemented/architecture/2026-06-11-custom-schema-dsl.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-11-custom-schema-dsl.md: 4c4d572b15d5e474e00e99fc5e7dd89240251c63 -2026-06-11-custom-schema-dsl.zh.md: 31af594846b6b1bc8ce983b7b9d4ddd956a560ff +2026-06-11-custom-schema-dsl.zh.md: eca0428d46ba3b3a4beac0cf9e8f02a9fa198388 diff --git a/docs/rfc/implemented/architecture/2026-06-11-custom-schema-dsl.zh.md b/docs/rfc/implemented/architecture/2026-06-11-custom-schema-dsl.zh.md index 31af594846..eca0428d46 100644 --- a/docs/rfc/implemented/architecture/2026-06-11-custom-schema-dsl.zh.md +++ b/docs/rfc/implemented/architecture/2026-06-11-custom-schema-dsl.zh.md @@ -1,4 +1,4 @@ -# RFC:使用自定义类型化工具 schema DSL 替代 schemastery +# RFC:使用自定义类型化 tool-schema DSL 替代 schemastery Status: implemented @@ -6,18 +6,18 @@ Status: implemented ## 问题 -工具参数必须以标准 JSON Schema 的形式传递给模型,同时让工具作者在 `execute(args)` 中获得类型推导而无需类型断言。schemastery 已用于插件配置,但工具作者 API 需要的是逐属性的 `required: true` 布尔值,而非 JSON Schema 的独立 `required` 数组。 +工具参数必须以标准 JSON Schema 形式到达模型,同时让工具作者在 `execute(args)` 中获得类型化的参数而无需类型断言。Schemastery 已用于插件配置,但工具作者 API 需要逐属性的 `required: true` 布尔值,而非 JSON Schema 的独立 `required` 数组。 ## 决策 -在 dsh-tools 中实现一个小型自定义 DSL:`SchemaSpec`(逐属性规格,带 `required: true` 布尔值);类型层面的 `InferArgs` 将规格映射为参数类型(required 键为必选,其余通过 `?` 真正可选);运行时的 `schemaSpecToJsonSchema()` 转换器;以及将它们串联起来的 `defineTool()`。`ToolRegistry.register()` 仍接受原始 JSON Schema 的 `ToolDefinition`——MCP 来源的工具就是这样注册的。 +在 dsh-tools 中实现一个小型自定义 DSL:`SchemaSpec`(逐属性规格,带 `required: true` 布尔值);类型层面的 `InferArgs` 将规格映射为参数类型(required 键为必选,其余通过 `?` 标记为真正可选);运行时的 `schemaSpecToJsonSchema()` 转换器;以及将三者串联的 `defineTool()`。`ToolRegistry.register()` 仍然接受原始 JSON Schema 的 `ToolDefinition`——MCP 来源的工具正是以此方式注册。 ## 曾考虑的替代方案 -**schemastery**(已 vendor、用于插件 Config)经评估后被否决:它面向的是基于 StandardSchema 的校验/转换,而非 JSON Schema *生成*,因此会增加间接层却无法干净地产出协议格式(wire format)。 +**Schemastery**(已作为 vendor 引入,用于插件 Config)经评估后被否决:它面向的是基于 StandardSchema 的校验/转换,而非 JSON Schema **生成**,因此会增加间接层却无法干净地产出协议格式(wire format)。 ## 后果 - 第一方工具作者获得零类型断言的类型化参数;类型体操的成本留在核心包内部(符合 AGENTS.md 的类型安全策略)。 -- DSL 刻意保持小巧(string/number/boolean/object/array、enum、default、嵌套 properties/items)。相对完整 JSON Schema 的缺口(union、format、约束)在真实工具提出需求之前暂不填补。 -- `InferArgs` 映射在一次早期可选性 bug 之后已有类型层面的回归测试。 +- DSL 有意保持小巧(string/number/boolean/object/array、enum、default、嵌套 properties/items)。相对完整 JSON Schema 的缺口(union、format、constraint)在真实工具提出需求之前暂不补齐。 +- `InferArgs` 映射在类型层面有回归测试,源于早期一个可选性 bug。 diff --git a/docs/rfc/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.i18n.yaml b/docs/rfc/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.i18n.yaml index ef2f73d949..0758761505 100644 --- a/docs/rfc/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.i18n.yaml +++ b/docs/rfc/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-11-dev-invariants-over-deep-readonly.md: dc89b5b66d02bde2f4fe2794e04b76ecdeac62ae -2026-06-11-dev-invariants-over-deep-readonly.zh.md: 02beb323df84cd442611f0c52b3c56734d6d0554 +2026-06-11-dev-invariants-over-deep-readonly.zh.md: 2c3c9e221b42f4b45216d09e825a41b1be3bcee9 diff --git a/docs/rfc/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.zh.md b/docs/rfc/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.zh.md index 02beb323df..2c3c9e221b 100644 --- a/docs/rfc/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.zh.md +++ b/docs/rfc/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.zh.md @@ -1,4 +1,4 @@ -# RFC:源头拥有的会话不可变性与开发模式不变式 +# RFC:源端拥有的会话不可变性与开发模式不变式 Status: implemented @@ -6,55 +6,55 @@ Status: implemented ## 问题 -会话日志需要两种不同的保护:对每条已存储事实的不可变所有权,以及对跨时间和服务 seam 的事实间关系的检查。如果将二者混为一体放进一个可选的开发插件,生产环境的历史记录将失去保护;如果试图通过 TypeScript readonly 类型同时表达两者,既无法建立运行时边界,也无法描述关系规则。 +会话日志需要两种不同的保护:对每条已存储事实的不可变所有权,以及对跨时间和服务 seam 的事实之间关系的检查。如果将二者混为一个可选的开发插件,生产环境的历史记录将失去保护;如果试图通过 TypeScript readonly 类型同时表达两者,既无法建立运行时边界,也无法描述关系规则。 -会话日志是回放、请求重建、持久化和用户可见历史的持久真源。会话包以外的代码必须能够检视该历史,但不能保留一个可以事后改写它的引用;从调用方接收的输入也不能继续连接到调用方拥有的可变对象上。 +会话日志是回放、请求重建、持久化与用户可见历史的持久真源。会话包(package)外部的代码必须能检视历史,但不能保留一个可在之后改写历史的引用;从调用方接受的输入也不能继续连接到调用方拥有的可变对象。 单个值的不可变性只是契约的一半。一份日志可以包含完全不可变的记录,但其序列、轮次/步骤嵌套、工具调用配对、作用域分发或重建的模型请求是错误的。这些规则涉及多条记录或多个服务,无法通过冻结单个对象来建立。 -TypeScript readonly 类型不构成充分的运行时边界。它们在程序运行时消失,一次类型转换即可绕过,而递归的 `DeepReadonly` 会扩散到每个日志和消息消费方,尽管某些下游请求处理 API 有意使用可变值。 +TypeScript readonly 类型不是充分的运行时边界。它们在程序运行时消失,类型转换可以绕过它们,而递归的 `DeepReadonly` 会扩散到每个日志和消息消费方,尽管某些下游请求处理 API 有意使用可变值。 ## 决策 -职责在一个始终开启的存储边界与可选的开发断言之间分离。 +职责在始终启用的存储边界与可选的开发断言之间分离。 ### Session 拥有不可变历史 -`Session` 仅在一次递归遍历完成无损 JSON 快照后才接受事件。该遍历拒绝不支持的值,并产出进入日志的确切脱离记录,因此校验和存储不可能从有状态的 getter 观察到不同的值,也不会保留调用方拥有的嵌套引用。 +`Session` 仅在一次递归遍历完成无损 JSON 快照的物化之后才接受事件。该遍历拒绝不支持的值,并产出进入日志的确切分离记录,因此验证与存储不会从有状态的 getter 观察到不同的值,也不会保留调用方拥有的嵌套引用。 -被接受的事件及其所有后代在发布前被深度冻结。`append()` 返回该拥有的冻结事件,`session/event` 观察者收到同一条记录,`session.events` 返回一份冻结的数组快照。先前返回的数组不会因后续 append 而增长。种子记录在构造成功前经过相同的校验、快照与冻结边界。 +被接受的事件及其所有后代在发布前被深度冻结。`append()` 返回该拥有的冻结事件,`session/event` 观察者接收同一记录,`session.events` 返回冻结的数组快照。先前返回的数组不会因后续 append 而增长。种子记录在构造成功前经过相同的验证、快照与冻结边界。 -这一保证属于 `Session` 而非可选的监听器,因为每种组合都依赖可信的历史。无论是否注册了开发支持插件,生产部署、聚焦测试或自定义嵌入都获得相同的存储语义。 +此保证属于 `Session` 而非可选监听器,因为每种组合都依赖可信的历史。无论是否注册了开发支持插件,生产部署、聚焦测试或自定义嵌入都获得相同的存储语义。 -### 派生请求保持脱离 +### 派生请求保持分离 -`deriveMessages()` 将已记录的表面事件投影为脱离的、深度冻结的 `Message` 对象,并返回一份新的数组快照。请求组装因此可以将派生历史与其他输入组合,而不会暴露一条回到日志的路径。缓存复用安全的不可变投影,而非为每次模型调用重新克隆完整历史。 +`deriveMessages()` 将已记录的表面事件投影为分离的、深度冻结的 `Message` 对象,并返回一份新的数组快照。因此请求组装可以将派生历史与其他输入组合,而不会暴露一条回到日志的路径。缓存复用安全的不可变投影,而非为每次模型调用重新克隆完整历史。 ### 不变式插件检查关系 -`dsh-invariants` 是一个纯监听的开发插件。它不冻结记录,没有配置;dispose 仅移除其断言。它检查需要追踪状态或观察另一个 seam 的规则,包括单调递增的序列号、轮次与步骤嵌套、工具调用/结果配对、合法的 agent 状态转换、主体正确的作用域分发,以及 agent loop 构建的请求与从其会话日志前缀重建的请求之间的等价性。 +`dsh-invariants` 是一个纯监听器的开发插件。它不冻结记录,没有配置;dispose(资源释放)仅移除其断言。它检查需要跟踪状态或观察另一个 seam 的规则,包括单调递增的序列号、轮次与步骤嵌套、工具调用/结果配对、合法的 agent(智能体)状态转换、主体正确的作用域分发,以及循环构建的请求与从其会话日志前缀重建的请求之间的等价性。 -当插件附加到已有或已播种的会话时,它回放不可变日志以重建追踪状态。这使得在轮次中间进行热重载是安全的,同时不赋予插件对会话存储的所有权。 +当插件附加到已有或已播种的会话时,它回放不可变日志以重建跟踪状态。这使得在轮次中途热重载是安全的,同时不赋予插件对会话存储的所有权。 ## 曾考虑的替代方案 ### 全面的 deep-readonly 类型 -[被否决的不可变公开表面提案](../../rejected/architecture/2026-06-11-immutable-public-surfaces.md)会在公开的日志和消息表面全面应用递归 readonly 类型。这能提供编辑器反馈,但不能提供运行时保证:TypeScript 类型在运行时被擦除,插件代码可以通过类型转换绕过。它还会将 readonly 类型推入有意进行修改的消费方。在 `Session` 边界处的运行时所有权保护所有调用方,无需这种类型传播。 +[被否决的不可变公共表面提案](../../rejected/architecture/2026-06-11-immutable-public-surfaces.md)会在公共日志和消息表面上应用递归 readonly 类型。这能提供编辑器反馈,但无法提供运行时保证:TypeScript 类型在运行时被擦除,插件代码可以通过类型转换绕过。它还会将 readonly 类型推入有意进行修改的消费方。在 `Session` 边界处的运行时所有权保护所有调用方,无需这种类型传播。 ### 仅在开发模式冻结 -仅在安装了不变式插件时才冻结历史,会使核心保证依赖于组合方式。代码可能通过开发测试,却在生产环境或省略了该插件的聚焦组合中破坏历史。因此存储不可变性始终开启,而更昂贵的关系检查保持为可选的开发支持。 +仅当不变式插件安装时才冻结历史,会使核心保证依赖于组合方式。代码可能通过开发测试,却在生产环境或省略了该插件的聚焦组合中破坏历史。因此存储不可变性始终启用,而开销更大的关系检查则保持为可选的开发支持。 ### 仅在派生消息时克隆 -脱离 `deriveMessages()` 会保护最常见的请求路径,但 `session.events` 的其他读者、append 返回值和会话事件观察者仍能修改持久历史。日志必须保护自身的边界;派生投影是额外的隔离边界,不是替代品。 +分离 `deriveMessages()` 能保护最常见的请求路径,但 `session.events` 的其他读取者、append 返回值和会话事件观察者仍能修改持久历史。日志必须保护自身的边界;派生投影是额外的隔离边界,而非替代品。 ## 后果 -- 每条被接受的实时或种子会话事件在任何观察者收到之前,都已从调用方拥有的输入中脱离并深度不可变。 +- 每个被接受的实时或种子会话事件在任何观察者接收之前,都已从调用方拥有的输入中分离并深度不可变。 - `session.events` 暴露稳定的不可变快照,而非私有的增长数组。 - 请求侧的修改无法通过派生消息触及已存储的历史。 - 开发构建可以启用关系断言而不改变存储行为;dispose 或省略该插件不会削弱日志不可变性。 - `dsh-invariants` 没有 `Config` 表面,因为它没有可调节的行为。 -- 运行时边界在每条被接受的事件上承担一次递归快照与冻结的开销;后续读者和缓存投影复用已拥有的不可变记录。 +- 运行时边界对每个被接受的事件产生一次递归快照与冻结的开销;后续读取者和缓存投影复用已拥有的不可变记录。 diff --git a/docs/rfc/implemented/architecture/2026-06-11-event-sourced-sessions.i18n.yaml b/docs/rfc/implemented/architecture/2026-06-11-event-sourced-sessions.i18n.yaml index 00e6dc9181..e7645f5f8d 100644 --- a/docs/rfc/implemented/architecture/2026-06-11-event-sourced-sessions.i18n.yaml +++ b/docs/rfc/implemented/architecture/2026-06-11-event-sourced-sessions.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-11-event-sourced-sessions.md: 04ff974826ffbc9052c7eb9f5794bc16557241f9 -2026-06-11-event-sourced-sessions.zh.md: a3fec18445673bc2368333413f9802f9822b9c89 +2026-06-11-event-sourced-sessions.zh.md: 12d6aedcc32b4d93f0dbe010854bcd3a0721703f diff --git a/docs/rfc/implemented/architecture/2026-06-11-event-sourced-sessions.zh.md b/docs/rfc/implemented/architecture/2026-06-11-event-sourced-sessions.zh.md index a3fec18445..12d6aedcc3 100644 --- a/docs/rfc/implemented/architecture/2026-06-11-event-sourced-sessions.zh.md +++ b/docs/rfc/implemented/architecture/2026-06-11-event-sourced-sessions.zh.md @@ -1,28 +1,28 @@ # RFC:事件溯源的会话与派生消息历史 -Status: implemented - [English](2026-06-11-event-sourced-sessions.md) | 中文 +Status: implemented + ## 问题 -MVP 要求严格的基于事件的 trace、logging 系统,session 完全可回放。 +MVP 要求严格的基于事件的追踪,以及完全可回放的会话(严格的基于事件的 trace、logging 系统,session 完全可回放)。 ## 决策 -`Session` 是一份仅追加的、类型化的 `SessionEvent` 日志,是唯一的真源。LLM(大语言模型)消息历史从日志*派生*(`deriveMessages()`);原始流分片被记入日志以保证 token 级别的回放保真度,而组装后的 `assistant/message` 事件才是派生的权威来源。回放/fork = 用已有日志初始化一个新会话。 +`Session` 是一份仅追加的、类型化的 `SessionEvent` 日志,是唯一的真源。LLM(大语言模型)消息历史从日志*派生*(`deriveMessages()`);原始流分片被记录以保证 token 级别的回放保真度,而组装后的 `assistant/message` 事件才是派生的权威依据。回放/fork = 用已有日志初始化一个新会话。 -追加操作是同步的(热路径从不阻塞在 I/O 上);`session/event` 是同步通知;持久化插件在后台缓冲写入,并在每个轮次结束时触发的 `session/flush` 检查点处等待排空。 +追加操作是同步的(热路径从不阻塞于 I/O);`session/event` 是同步通知;持久化插件在后台缓冲写入,并在每个轮次结束时触发的 `session/flush` 检查点处等待排空。 顺序契约:agent loop(智能体循环)先追加到会话,再发出对应的 Cordis 事件;`agent/step-result` waterfall(瀑布式事件)在 `assistant/message` 追加之前运行,因此日志记录的是工具调度实际使用的消息。回归测试固定了这一顺序。 ## 曾考虑的替代方案 -**可变消息数组 + 事件作为通知发出**:更简单,但状态与日志可能分歧;采用事件溯源后,日志本身就是状态,分歧在结构上不可能发生。 +**可变消息数组 + 事件仅作通知发出**:更简单,但状态与日志可能分歧;采用事件溯源后,日志本身即是状态,分歧在结构上不可能发生。 ## 后果 -- 回放、trace 与遥测在结构上得到保证,而非事后附加。 -- 持久化仍是插件关注点;内存存储随 dsh-session 一起发布。 +- 回放、追踪与遥测在结构上得到保证,而非事后附加。 +- 持久化仍是插件关注点;内存存储随 dsh-session 一起提供。 - 事件词汇可通过合并扩展(插件可添加如压缩(compaction)事件);[会话持久化](2026-06-14-session-persistence.md)在日志变为持久后冻结了其形状。 -- 派生成本随日志长度增长——压缩(未来插件)是预期的缓解手段,而非日志变更。 +- 派生成本随日志长度增长,压缩(未来插件)是预期的缓解手段,而非日志变更。 diff --git a/docs/rfc/implemented/architecture/2026-06-11-microkernel-event-taxonomy.i18n.yaml b/docs/rfc/implemented/architecture/2026-06-11-microkernel-event-taxonomy.i18n.yaml index 00320a6991..8634a86f36 100644 --- a/docs/rfc/implemented/architecture/2026-06-11-microkernel-event-taxonomy.i18n.yaml +++ b/docs/rfc/implemented/architecture/2026-06-11-microkernel-event-taxonomy.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-11-microkernel-event-taxonomy.md: c66968257a5a6304f187ccb5b9a162aa143e608d -2026-06-11-microkernel-event-taxonomy.zh.md: 86750f6ece488e735f2c14f759db7c9eef360828 +2026-06-11-microkernel-event-taxonomy.zh.md: 7becf5872aee16fe21b4acbbc61920ed91c40f44 diff --git a/docs/rfc/implemented/architecture/2026-06-11-microkernel-event-taxonomy.zh.md b/docs/rfc/implemented/architecture/2026-06-11-microkernel-event-taxonomy.zh.md index 86750f6ece..7becf5872a 100644 --- a/docs/rfc/implemented/architecture/2026-06-11-microkernel-event-taxonomy.zh.md +++ b/docs/rfc/implemented/architecture/2026-06-11-microkernel-event-taxonomy.zh.md @@ -6,26 +6,26 @@ Status: implemented ## 问题 -产品原则是「一切皆插件」:钩子、/goal、/loop、动态工作流、上下文压缩(context compaction)、沙箱、权限、UI、持久化、MCP、skill(技能)都必须能以插件形式编写,而无需修改核心。 +产品原则是「一切皆插件」:钩子、/goal、/loop、动态工作流、上下文压缩(context compaction)、沙箱、权限、UI、持久化、MCP、skill(技能)都必须能以插件形式编写,无需修改核心。 ## 决策 -纯 Cordis 事件分类体系(taxonomy)。循环的扩展 seam 是带有明确分发模式的类型化事件: +纯 Cordis 事件分类体系。agent loop(智能体循环)的扩展 seam 是带类型的事件,具有明确的分发模式: -- **waterfall(瀑布式事件)**(around-middleware):插件可以变换、否决或包装:`agent/prompt-submit`、`agent/request`、`agent/step-result`、`agent/turn-continuation`、`tools/pre-execute`、`tools/execute`、`tools/post-execute`、`llm/stream`、`system-prompt/assemble`。 -- **serial**(按监听器顺序依次 await;bail 值会阻止后续监听器):用于有序检查点。所有 `agent/pre-step` 监听器在全部弃权时都会运行,而 `agent/turn-stop` 返回的第一个 stop 值即为最终的终止决策。 -- **parallel**(await 扇出):每个监听器都必须获得独立执行机会:`session/flush` 持久性检查点。 -- **emit**(同步 fire-and-forget):用于通知:轮次/步骤边界、流式分片、生命周期、错误,以及包含不可变 `tools/result` 观测值的事件。 +- **waterfall(瀑布式事件)**(around-middleware):插件可变换、否决或包装:`agent/prompt-submit`、`agent/request`、`agent/step-result`、`agent/turn-continuation`、`tools/pre-execute`、`tools/execute`、`tools/post-execute`、`llm/stream`、`system-prompt/assemble`。 +- **serial**(按监听器顺序依次 await;bail 值会阻止后续监听器执行):用于有序检查点。所有 `agent/pre-step` 监听器在全部弃权时才继续运行,而 `agent/turn-stop` 返回的第一个 stop 值即为最终的终止决策。 +- **parallel**(await 扇出):每个监听器都必须获得独立执行的机会:`session/flush` 持久性检查点。 +- **emit**(同步 fire-and-forget):用于通知:轮次/步骤边界、流分片、生命周期、错误,以及包含不可变 `tools/result` 观测的事件。 -事件词汇定义在接口包中(dsh-agent 声明 agent/* 事件);`@deepseek-ai/dsh-agent-loop` 是唯一的具体循环插件,且本身可替换——它之外的任何代码都不得依赖它。 +事件词汇定义在接口包中(dsh-agent 声明 agent/* 事件);`@deepseek-ai/dsh-agent-loop` 是唯一的具体循环插件,且自身可替换——外部不得依赖它。 ## 曾考虑的替代方案 -**专用中间件栈(koa-compose 风格)** 与 **插件插入其中的显式阶段状态机**:两者都需要重新实现分发、dispose(资源释放)和重载语义,而 Cordis 原生事件系统已经提供了这些;作为 Cordis effect,监听器天然获得 HMR(热模块替换)和 dispose 能力。 +**专用中间件栈(koa-compose 风格)** 与**显式阶段状态机(插件向其中插入阶段)**:两者都需要重新实现 Cordis 原生事件系统已提供的分发、dispose(资源释放)与重载语义;作为 Cordis effect,监听器天然获得 HMR(热模块替换)与 dispose 能力。 ## 后果 -- 每个 MVP 功能都映射到一个监听器([功能→机制映射](../../../cookbook/extension-cookbook.md#the-feature--mechanism-map)是证明义务,保持最新)。 -- HMR 和 dispose 免费获得:监听器和注册都是 Cordis effect。 -- waterfall 语义(调用 `next()` 或短路)不直观,需要教学——已在 AGENTS.md 中记录,并由组合测试覆盖。 -- 循环必须具备防御性:插件异常在轮次级别被隔离,来自任何 seam 的 steering(中途引导)绝不会被搁置(有回归测试保障)。 +- 每个 MVP 功能都映射到一个监听器([功能→机制映射](../../../cookbook/extension-cookbook.md#the-feature--mechanism-map)是证明义务,保持更新)。 +- HMR 与 dispose 无需额外工作:监听器和注册均为 Cordis effect。 +- waterfall 语义(调用 `next()` 或短路)不直观,需要教学——在 AGENTS.md 中记录,并由组合测试覆盖。 +- 循环必须具备防御性:插件异常在轮次级别被隔离,任何 seam 发出的 steering(中途引导)永远不会被搁置(有回归测试保障)。 diff --git a/docs/rfc/implemented/architecture/2026-06-11-runtime-arg-validation.i18n.yaml b/docs/rfc/implemented/architecture/2026-06-11-runtime-arg-validation.i18n.yaml index 8e7b36b58d..9ae6b5d602 100644 --- a/docs/rfc/implemented/architecture/2026-06-11-runtime-arg-validation.i18n.yaml +++ b/docs/rfc/implemented/architecture/2026-06-11-runtime-arg-validation.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-11-runtime-arg-validation.md: 6da117643166d304bee1d368a314cc1602cac828 -2026-06-11-runtime-arg-validation.zh.md: fb67568da3bfc7f1f8fcc0429b6c68193e04693a +2026-06-11-runtime-arg-validation.zh.md: 5f41ff99a2d56f39ff8d61191d92dc782f163c82 diff --git a/docs/rfc/implemented/architecture/2026-06-11-runtime-arg-validation.zh.md b/docs/rfc/implemented/architecture/2026-06-11-runtime-arg-validation.zh.md index fb67568da3..5f41ff99a2 100644 --- a/docs/rfc/implemented/architecture/2026-06-11-runtime-arg-validation.zh.md +++ b/docs/rfc/implemented/architecture/2026-06-11-runtime-arg-validation.zh.md @@ -6,19 +6,19 @@ Status: implemented ## 问题 -`defineTool`([自定义 schema DSL](2026-06-11-custom-schema-dsl.md))通过 `InferArgs` 映射为工具作者提供了类型化的 `execute(args)`。但该类型只是编译期对一个运行时值的声明:这个值以模型生成的 JSON 形式到达,没有任何机制强制模型遵守 schema。因此,一次格式错误的调用(缺少必填键、声明为数字的位置传入字符串、枚举值超出集合)会以「仅有类型之名」的状态抵达 `execute`。工具体要么在错误形状上崩溃(产生一条模型无法据以行动的通用堆栈跟踪),要么更糟:静默地行为异常。与此同时,转换器已经编码了校验器遍历所需的完整结构。 +`defineTool`([自定义 schema DSL](2026-06-11-custom-schema-dsl.md))通过 `InferArgs` 映射为工具作者提供了类型化的 `execute(args)`。但该类型只是对运行时值的编译期声明,而这个值实际上是模型生成的 JSON:没有任何机制强制模型遵守 schema,因此畸形调用(缺少必需键、声明为数字的位置传入字符串、枚举值超出集合)会以「仅名义类型化」的状态到达 `execute`。工具函数体要么在错误形状上崩溃(产生模型无法据以自我修正的通用堆栈跟踪),要么更糟——静默地行为异常。与此同时,转换器已经编码了校验器遍历所需的完整结构。 ## 决策 -`validateArgs(spec, args): string[]` 对一个运行时值解释 `SchemaSpec`,返回人类可读的违规列表(空 = 合法),且是全函数(从不抛出异常)。`defineTool` 在调用类型化的工具体之前运行它;如果存在违规,则抛出 `ToolArgsError`(`code: 'INVALID_ARGS'`,消息列出违规项),注册表既有的 execute-waterfall catch 将其转为模型可读取并据以自我修正的 `isError` 结果。 +`validateArgs(spec, args): string[]` 对运行时值解释一个 `SchemaSpec`,返回可读的违规列表(空数组 = 合法),且是全函数(永不抛出异常)。`defineTool` 在调用类型化函数体之前运行它;存在违规时抛出 `ToolArgsError`(`code: 'INVALID_ARGS'`,消息中列出违规项),注册表既有的 execute-waterfall(瀑布式事件)catch 将其转为模型可读取并据以自我修正的 `isError` 结果。 -校验器严格镜像 `schemaSpecToJsonSchema` 的语义:遍历相同的结构、执行相同的规则:顶层必须是非数组对象;必填键仅来自 `required: true`;允许额外键(不设 `additionalProperties: false`);不应用 `default`;没有 `properties`/`items` 的 `object`/`array` 属性仅做类型检查;`enum` 是成员判定。原始注册的(MCP)工具不受影响:它们自行校验输入。 +校验器严格镜像 `schemaSpecToJsonSchema` 的语义——遍历相同结构、执行相同规则:顶层必须是非数组对象;必需键仅来自 `required: true`;允许额外键(不设 `additionalProperties: false`);不应用 `default`;没有 `properties`/`items` 的 `object`/`array` 属性仅做类型检查;`enum` 是成员资格检查。原始注册的(MCP)工具不受影响——它们自行校验输入。 ## 后果 -- 模型在自身格式错误的调用上获得可操作的反馈,而非不透明的崩溃,弥合了 `InferArgs` 的承诺与运行时现实之间的鸿沟。 -- 校验器与 `InferArgs` 必须保持一致;一组[属性测试](../testing/2026-06-11-property-based-testing.md)会生成满足 spec 的参数并断言它们通过 `validateArgs`(同时断言定向破坏的参数被拒绝),以机械方式封堵漂移风险。 -- `ToolArgsError` 目前是一个带 `code` 字段的普通 `Error`;如果日后引入 harness 级别的错误分类体系,它将变为子类,而不影响读取 `.message` 的调用方。 +- 模型在自身畸形调用上获得可操作的反馈,而非不透明的崩溃,弥合了 `InferArgs` 的承诺与运行时现实之间的鸿沟。 +- 校验器与 `InferArgs` 必须保持一致;一项[属性测试](../testing/2026-06-11-property-based-testing.md)生成满足 spec 的参数并断言它们通过 `validateArgs`(同时断言定向破坏的参数被拒绝),以机械方式封堵漂移风险。 +- `ToolArgsError` 目前是带 `code` 字段的普通 `Error`;如果日后引入 harness 级别的错误分类体系,它将变为子类,但不影响读取 `.message` 的调用方。 - 校验开销相对于一次模型调用可忽略不计。 diff --git a/docs/rfc/implemented/architecture/2026-06-11-structured-error-taxonomy.i18n.yaml b/docs/rfc/implemented/architecture/2026-06-11-structured-error-taxonomy.i18n.yaml index 84528dccf1..aa6cdb26ff 100644 --- a/docs/rfc/implemented/architecture/2026-06-11-structured-error-taxonomy.i18n.yaml +++ b/docs/rfc/implemented/architecture/2026-06-11-structured-error-taxonomy.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-11-structured-error-taxonomy.md: 2baf88a1f942215e79561e565f455276c80178c4 -2026-06-11-structured-error-taxonomy.zh.md: f4762c4e92fb94c5bdbccc5f9a61e1496dc4c66d +2026-06-11-structured-error-taxonomy.zh.md: 90b0fdd7f6c8b4f0565c6538c2ddd4cb31680c38 diff --git a/docs/rfc/implemented/architecture/2026-06-11-structured-error-taxonomy.zh.md b/docs/rfc/implemented/architecture/2026-06-11-structured-error-taxonomy.zh.md index f4762c4e92..90b0fdd7f6 100644 --- a/docs/rfc/implemented/architecture/2026-06-11-structured-error-taxonomy.zh.md +++ b/docs/rfc/implemented/architecture/2026-06-11-structured-error-taxonomy.zh.md @@ -6,21 +6,21 @@ Status: implemented ## 问题 -错误跨越服务边界时只是裸字符串。工具错误被扁平化为一个文本块——name、code 和 stack 全部丢失——导致未来的沙箱/重试插件无法区分 ENOENT 和 EACCES,模型得到的反馈也不如本可以获得的那样可操作。非 Error 的 throw 退化得更严重:agent loop(智能体循环)将其包装为 `new Error(String(x))`,丢弃了所有 code。而 `LlmError` 是系统中唯一的类型化错误,没有共享基类,消费方无法对一个通用基类做 `instanceof`。 +故障跨越 seam 时只是裸字符串。工具错误被扁平化为一个文本块(name、code 和 stack 全部丢失),导致未来的沙箱/重试插件无法区分 ENOENT 和 EACCES,模型得到的反馈也不如本可以那样具有可操作性。非 Error 的 throw 退化更严重:agent loop(智能体循环)将其包装为 `new Error(String(x))`,丢弃了所有 code。而 `LlmError` 是系统中唯一的类型化错误,没有共享基类,消费方无法对其进行通用的 `instanceof` 判断。 ## 决策 -在 `dsh-llm`(叶子包(package),所有其他包都已依赖它——不引入新的依赖边)中建立一个 `HarnessError extends Error` 基类:稳定的 `code`(与 `message` 分离)、通过 `ErrorOptions` 的 `cause` 链式传递、`name` 默认为子类名。`isHarnessError` 在服务边界处做类型收窄。 +在 `dsh-llm`(叶子包,所有其他包都已依赖它,不引入新的依赖边)中引入一个 `HarnessError extends Error` 基类:稳定的 `code`(与 `message` 分离)、通过 `ErrorOptions` 进行 `cause` 链接、`name` 默认为子类名。`isHarnessError` 在 seam 处做类型收窄。 - `LlmError`、`ToolArgsError`(dsh-tools)和 `InvariantError`(dsh-invariants)现在继承该基类,保留各自既有的 code。 -- `ToolExecutionResult` 新增可选字段 `error: { name, code }`,在注册表的 catch 中当抛出值为 `HarnessError` 时填充。agent loop 将其转发到 `tool/result` 会话事件(该事件也新增了同一可选字段),使结构化的失败信息存入日志,供重试/沙箱插件和回放使用。面向模型的文本块不变。 -- agent loop 的 `toError` 将非 Error 的 throw 包装为 `HarnessError`(`code: 'UNKNOWN'`,原始值通过 `cause` 链接),而非裸 `Error`;这样即使是不规范的 throw 也能携带可路由的 code 进入会话的 `error` 事件(该事件已暴露 `code`)。 +- `ToolExecutionResult` 新增可选字段 `error: { name, code }`,在注册表的 catch 中当抛出值为 `HarnessError` 时填充。agent loop 将其转发到 `tool/result` 会话事件(该事件也新增了同一可选字段),使结构化的失败信息存活到日志中,供重试/沙箱插件和回放使用。面向模型的文本块保持不变。 +- agent loop 的 `toError` 将非 Error 的 throw 包装为 `HarnessError`(`code: 'UNKNOWN'`,原始值作为 `cause` 链接),而非裸 `Error`;这样即使是不规范的 throw 也能携带可路由的 code 进入会话的 `error` 事件(该事件此前已暴露 `code`)。 ## 后果 -- 错误在端到端链路上可被机器路由:插件可以按 `error.code` 分支,而非对 message 做子串匹配。 -- 一个基类被广泛导入,但它位于所有包本已依赖的包中,代价只是一条 import 语句,而非一条新的依赖边。 -- `deriveMessages` 不会将 `error` 字段呈现到模型历史中——模型仍然看到文本块;结构化字段服务于代码逻辑和回放。 -- 参数校验与开发不变式保留各自既有的 code 和行为;共享基类添加了跨服务边界的路由元数据,不改变面向模型的文本。 +- 错误端到端可机器路由:插件可以基于 `error.code` 分支,而无需对 message 做子串匹配。 +- 一个基类被广泛导入,但它位于所有包已经依赖的包中,代价仅是一条 import 语句,而非新的依赖边。 +- `deriveMessages` 不会将 `error` 暴露到模型历史中——模型仍然看到文本块;结构化字段服务于代码和回放。 +- 参数校验与开发不变式保留各自既有的 code 和行为;共享基类增加了跨 seam 的路由元数据,不改变面向模型的文本。 diff --git a/docs/rfc/implemented/architecture/2026-06-11-tool-schemas-in-prompt-assembly.i18n.yaml b/docs/rfc/implemented/architecture/2026-06-11-tool-schemas-in-prompt-assembly.i18n.yaml index c0625866a3..5f5b2ef266 100644 --- a/docs/rfc/implemented/architecture/2026-06-11-tool-schemas-in-prompt-assembly.i18n.yaml +++ b/docs/rfc/implemented/architecture/2026-06-11-tool-schemas-in-prompt-assembly.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-11-tool-schemas-in-prompt-assembly.md: 443e6f20115e5a76001b4466c2d756675adbd886 -2026-06-11-tool-schemas-in-prompt-assembly.zh.md: 2d23d6fd0ead17fbeed3feaf3cec864813ef47e4 +2026-06-11-tool-schemas-in-prompt-assembly.zh.md: 03624b98fabdedf691f3fb448cc5f37ba5a649ef diff --git a/docs/rfc/implemented/architecture/2026-06-11-tool-schemas-in-prompt-assembly.zh.md b/docs/rfc/implemented/architecture/2026-06-11-tool-schemas-in-prompt-assembly.zh.md index 2d23d6fd0e..03624b98fa 100644 --- a/docs/rfc/implemented/architecture/2026-06-11-tool-schemas-in-prompt-assembly.zh.md +++ b/docs/rfc/implemented/architecture/2026-06-11-tool-schemas-in-prompt-assembly.zh.md @@ -1,4 +1,4 @@ -# RFC:工具 schema 属于系统提示词组装的一部分 +# RFC:工具 schema 是系统提示词组装的一部分 Status: implemented @@ -6,18 +6,18 @@ Status: implemented ## 问题 -在协议格式(wire format)层面,工具 schema 通过模型请求中专用的 `tools` 字段传输,而非嵌入提示词文本。但从架构角度看,「模型被告知它能做什么」是一个内聚的关注点:提示词段落和工具列表由同一批插件贡献组装而成,并在同一时刻被消费。 +在协议格式(wire format)层面,工具 schema 通过模型请求中专用的 `tools` 字段传输,而非嵌入提示词文本。然而从架构角度看,「模型被告知它能做什么」是一个统一的关注点:提示词段落与工具列表由相同的插件贡献组装,并在同一时刻被消费。 ## 决策 -`PromptAssembly { sections, tools }`:系统提示词服务同时收集有序的文本段落和工具 schema(工具注册表自动贡献一个提供方)。agent loop(智能体循环)每步消费一个 assembly;适配器将 `sections` 映射到提供方的 system 槽位,将 `tools` 映射到协议格式的 `tools` 字段。因此 `system-prompt/assemble` waterfall(瀑布式事件)是模型前置信息的唯一拦截点——工具过滤(ToolSearch / 渐进式披露)是一次 assembly 改写,与提示词编辑无异。 +`PromptAssembly { sections, tools }`:系统提示词服务同时收集有序的文本段落和工具 schema(工具注册表自动贡献一个提供方)。agent loop(智能体循环)每个步骤消费一份 assembly;适配器将 `sections` 映射到提供方的 system 槽位,将 `tools` 映射到协议格式的 `tools` 字段。因此 `system-prompt/assemble` waterfall(瀑布式事件)是模型预先获知的所有信息的唯一拦截点:工具过滤(ToolSearch / 渐进式披露)是一次 assembly 重写,与提示词编辑无异。 ## 曾考虑的替代方案 -**循环分别向工具注册表和提示词服务查询**——将一个内聚的关注点拆到两个 seam 上;任何想塑造「模型被告知什么」的拦截(工具过滤、plan 模式)都需要在两个接口上各挂一个监听器,而非一次 assembly 改写。 +**循环从工具注册表和提示词服务分别查询**:将一个统一的关注点拆到两个 seam 上;任何想影响「模型被告知什么」的拦截(工具过滤、plan 模式)都需要在两个接口上各挂一个监听器,而非一次 assembly 重写即可完成。 ## 后果 - 一条 waterfall 统管模型的常驻上下文;plan 模式等插件可以在一个监听器中同时替换提示词文本和可见工具。 -- assembly 接口通过声明合并实现可扩展(无需无类型的 `extras` 包——扩展即声明合并)。 -- 「schema 出现在提示词服务中」有轻微的概念意外感,本文与 package README 对此做了说明。 +- assembly 接口通过声明合并实现可扩展(没有无类型的 `extras` 包——扩展即声明合并),为未来的槽位预留空间。 +- 将 schema 放在「提示词」服务中略有概念上的意外感,已在本文及 package README 中加以说明。 diff --git a/docs/rfc/implemented/architecture/2026-06-13-capability-seams.i18n.yaml b/docs/rfc/implemented/architecture/2026-06-13-capability-seams.i18n.yaml index 25e7f01d0f..d0eac81287 100644 --- a/docs/rfc/implemented/architecture/2026-06-13-capability-seams.i18n.yaml +++ b/docs/rfc/implemented/architecture/2026-06-13-capability-seams.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-13-capability-seams.md: e9d417dbd2bafcaece39601b12dbb310feb1e19b -2026-06-13-capability-seams.zh.md: e5d9be803f71ef3b85f79ed483dab64612a27fc7 +2026-06-13-capability-seams.zh.md: c569f3df083ec48bd05e6be2d3a1e5875fde362f diff --git a/docs/rfc/implemented/architecture/2026-06-13-capability-seams.zh.md b/docs/rfc/implemented/architecture/2026-06-13-capability-seams.zh.md index e5d9be803f..c569f3df08 100644 --- a/docs/rfc/implemented/architecture/2026-06-13-capability-seams.zh.md +++ b/docs/rfc/implemented/architecture/2026-06-13-capability-seams.zh.md @@ -1,32 +1,32 @@ -# RFC:能力 seam——接口/实现/消费方拆分 - -Status: implemented +# RFC:能力 seam——接口/实现/消费方三分 [English](2026-06-13-capability-seams.md) | 中文 +Status: implemented + ## 问题 -harness 具有可替换的能力:目前是 bash 执行,未来会有沙箱/远程执行器和替代模型提供方。一项能力有三个关注点,它们以不同的速率、出于不同的原因变化:*契约*(这项能力是什么)、*实现*(它如何运行)、*消费方接口*(模型和其他插件面对什么来编程)。将三者打包在一个 package 中会耦合这些变化速率:把本地执行器换成沙箱执行器时,模型看到的工具 schema 也会被搅动,尽管面向模型的契约从未改变。 +harness 具有可替换的能力:当前是 bash 执行,未来会有沙箱化/远程执行器和替代模型提供方。一项能力涉及三个关注点,它们以不同速率、因不同原因变化:*契约*(这项能力是什么)、*实现*(它如何运行)、*消费方接口*(模型和其他插件面向什么编程)。将三者捆绑在一个包(package)中会耦合这些变化速率——把本地执行器换成沙箱化执行器时,模型看到的工具 schema 也会被搅动,尽管面向模型的契约从未改变。 -这与「运行时谁提供、谁需要一项能力」是不同的问题,后者 Cordis 已经用 service + `inject` 回答了(提供方注册 `ctx.bash`;消费方声明 `inject: ['bash']`,其 fiber 挂起直到该服务存在)。那套机制是必要的,但它不决定 package 边界;本 RFC 决定。 +这与「谁在运行时提供、谁需要一项能力」是不同的问题,后者 Cordis 已通过 service + `inject` 解决(提供方注册 `ctx.bash`;消费方声明 `inject: ['bash']`,其 fiber 挂起直到服务存在)。该机制是必要的,但不决定包的边界;本 RFC 决定的是包的边界。 ## 决策 -一项可替换的能力拆为**三个 package**: +一项可替换的能力由**三个包**构成: -1. **接口**:一个抽象 service 加词汇类型,拥有 `ctx.`,仅依赖 cordis(例如 `dsh-bash`:`BashExecutor`、`BashRunResult`、`BashTask`)。 -2. **实现**:一个具体子类,以插件形式加载(例如 `dsh-bash-local`:子进程、进程组 kill、spill-file 截断)。沙箱/远程后端是实现同一接口的兄弟 package。 -3. **消费方**:模型和插件看到的东西(例如 `dsh-tool-bash`:`bash`/`bash_output`/`bash_kill` 工具 schema)。消费方 `inject` 接口 key,从不导入实现类型。 +1. **接口**——一个抽象服务加词汇类型,拥有 `ctx.`,仅依赖 cordis(例如 `dsh-bash`:`BashExecutor`、`BashRunResult`、`BashTask`)。 +2. **实现**——一个具体子类,以插件形式加载(例如 `dsh-bash-local`:子进程、进程组 kill、溢出文件截断)。沙箱化/远程后端是实现同一接口的兄弟包。 +3. **消费方**——模型和插件看到的内容(例如 `dsh-tool-bash`:`bash`/`bash_output`/`bash_kill` 工具 schema)。消费方 `inject` 接口键,从不导入实现类型。 -实现与消费方随后独立演进:沙箱执行器替换 `dsh-bash-local` 时无需触碰任何工具 schema。 +实现与消费方由此独立演进:沙箱化执行器替换 `dsh-bash-local` 时无需触碰任何工具 schema。 -当各部分确实属于同一关注点时,拆分并非强制:LLM seam 将接口 + 消费方合并为 `dsh-llm`(消费方是 agent loop(智能体循环)本身,而非可替换的 schema 表面),适配器作为实现 package。不要预防性拆分:只有一种可设想的实现和一个消费方的能力保持为一个 package,直到第二个出现。 +当各部分确实属于同一个关注点时,三分并非强制:LLM(大语言模型) seam 将接口 + 消费方合并为 `dsh-llm`(消费方是 agent loop(智能体循环)本身,而非可替换的 schema 表面),适配器作为实现包。不要预防性地拆分——如果一项能力只有一种可设想的实现和一个消费方,就保持为一个包,直到第二种出现。 ## 曾考虑的替代方案 -- **合并为一个 package**:否决,因为它重新耦合了拆分所要分离的三种变化速率(这正是拆分的全部意义)。 -- **`@cordisjs/plugin-capability`**:完全不同的维度。它是一个权限/能力*安全*服务(带继承的命名权限,通过 `ctx.capability.test` 对会话进行检测),是延后的权限/沙箱工作(`tools/pre-execute` deny/ask seam)的候选方案,而**不是**替换实现的机制。混淆这两个「能力」正是本 RFC 所指出的陷阱。 +- **单一合并包**:否决。因为它重新耦合了三分设计本要分离的三种变化速率(这正是拆分的意义所在)。 +- **`@cordisjs/plugin-capability`**:这是完全不同的维度。它是一个权限/能力*安全*服务(具名权限加继承,通过 `ctx.capability.test` 对会话进行检测),是延后的权限/沙箱工作(`tools/pre-execute` deny/ask seam)的候选方案,**不是**替换实现的机制。混淆这两个「能力」概念正是本 RFC 所指出的陷阱。 ## 后果 -每项能力多出更多 package 和更多样板代码(一套 `package.json`/`tsconfig`/README,加上 inject 接线)。换来的是:实现与消费方独立发布和版本化,新后端永远不会波及面向模型的契约。该规则记录在 [AGENTS.md](../../../../AGENTS.md) § Conventions("Capability seams are three packages")和 [architecture.md](../../../architecture.md) § "Capability seams" 中;bash 三件套是参考模板。何时合并、何时拆分是一个判断性决策,架构文档已做说明——本 RFC 记录的是*为什么*默认选择拆分。 +每项能力需要更多包和更多样板代码(一组 `package.json`/`tsconfig`/README,加上 inject 接线)。换来的是:实现与消费方独立发布和版本管理,新后端永远不会波及面向模型的契约。该规则记录在 [AGENTS.md](../../../../AGENTS.md) § Conventions("Capability seams are three packages")和 [architecture.md](../../../architecture.md) § "Capability seams" 中;bash 三件套是参考模板。何时合并、何时拆分是一个判断问题,架构文档对此有详细说明——本 RFC 记录的是*为什么*默认选择拆分。 diff --git a/docs/rfc/implemented/architecture/2026-06-13-twin-llm-adapters.i18n.yaml b/docs/rfc/implemented/architecture/2026-06-13-twin-llm-adapters.i18n.yaml index a46193a7cd..df04a14d6a 100644 --- a/docs/rfc/implemented/architecture/2026-06-13-twin-llm-adapters.i18n.yaml +++ b/docs/rfc/implemented/architecture/2026-06-13-twin-llm-adapters.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-13-twin-llm-adapters.md: 4efefcf4e2f6b1d60567ba3bfed7ae1ea53a7a4e -2026-06-13-twin-llm-adapters.zh.md: 806eea84ff3e6c4de988348964429ccaea27f4ba +2026-06-13-twin-llm-adapters.zh.md: 6cabd95c5361afdea5b33ffdee34a5acb6026f7f diff --git a/docs/rfc/implemented/architecture/2026-06-13-twin-llm-adapters.zh.md b/docs/rfc/implemented/architecture/2026-06-13-twin-llm-adapters.zh.md index 806eea84ff..6cabd95c53 100644 --- a/docs/rfc/implemented/architecture/2026-06-13-twin-llm-adapters.zh.md +++ b/docs/rfc/implemented/architecture/2026-06-13-twin-llm-adapters.zh.md @@ -1,4 +1,4 @@ -# RFC:以两个 LLM 适配器作为设计验证孪生 +# RFC:以两个 LLM 适配器作为设计验证孪生体 Status: implemented @@ -6,22 +6,22 @@ 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` + SSE 解析,直连 DeepSeek API。 -- `dsh-llm-pi-ai`:通过 `@earendil-works/pi-ai` 库(有自己的事件词汇)访问同一端点。 +- `dsh-llm-deepseek`:手写 `fetch` + SSE(Server-Sent Events)解析,直接对接 DeepSeek API。 +- `dsh-llm-pi-ai`:通过 `@earendil-works/pi-ai` 库访问同一端点(该库有自己的事件词汇)。 -它们强制执行的规则是:**凡是 StreamChunk 词汇无法同时为两个实现表达的东西,都是核心词汇的 bug**——立即暴露,而非等到下一个提供方才发现。这对孪生确定了现已记录在 `dsh-llm/src/types.ts` 中 `StreamChunk` 上的约定:usage 在 finish 之前发出、finish 之后不再有任何事件、工具调用的 `arguments` 全程为原始 JSON 字符串,以及消费方必须在两侧都处理的两条合法错误路径(从 `stream()` 抛异常,*或*以 `finish {kind:'error'|'aborted'}` 结束)。后一项分歧正是由库封装的适配器暴露出来的,单一手写适配器会将其掩盖。 +二者共同执行的规则是:**凡 StreamChunk 词汇无法为两个实现同时表达的内容,都是核心词汇的缺陷**——立即暴露,而非等到下一个提供方接入时才发现。这对孪生体确定了现已记录在 `dsh-llm/src/types.ts` 中 `StreamChunk` 上的约定:usage 在 finish 之前发出、finish 之后不再有任何事件、工具调用的 `arguments` 全程以原始 JSON 字符串传递,以及消费方必须在两侧都处理的两条合法错误路径(`stream()` 抛异常,*或者*以 `finish {kind:'error'|'aborted'}` 结束)。后一项分歧正是由基于库的适配器暴露出来的,单一手写适配器会将其隐藏。 ## 曾考虑的替代方案 -- **单一适配器**:代码更少、e2e 成本减半,但「提供方无关」的声明无法验证;词汇会默默编码 DeepSeek-via-fetch 的假设。 -- **mock 第二适配器**:更便宜,但不会触及真实提供方的协议格式(wire format)怪癖,因此证明力有限。孪生是真实对真实。 +- **单一适配器**:代码更少、e2e 成本减半,但「提供方无关」的声明无从验证;词汇会默默编码 DeepSeek-via-fetch 的假设。 +- **mock 第二适配器**:更便宜,但不会触及真实提供方的协议格式(wire format)怪癖,因此证明力有限。孪生体是真实对真实的验证。 ## 后果 -孪生使适配器和需要密钥的 e2e 维护量翻倍——两者都覆盖 V4 Flash 和 Pro 在各代表性推理模式下的表现——换来的是对 seam 中立性的持续验证和第二份实现示例。两者都使用 `apiKey`、`baseURL` 和 `models`;手写适配器暴露 `thinking`/`reasoningEffort`,pi-ai 适配器暴露一个 `reasoning` 级别。未来的一致性测试套件可以通过一份取代性 RFC 来论证退役其中一个适配器。 +孪生体使适配器和需要密钥的 e2e 维护量翻倍——两者都覆盖 V4 Flash 和 Pro 在各代表性推理(reasoning)模式下的行为——换来的是持续的 seam 中立性验证和第二份实现示例。两个适配器均使用 `apiKey`、`baseURL` 和 `models`;手写适配器暴露 `thinking`/`reasoningEffort`,pi-ai 适配器暴露一个 `reasoning` 级别。未来如果有一致性测试套件,可以通过后续 RFC 论证退役其中一个适配器。 diff --git a/docs/rfc/implemented/architecture/2026-06-14-session-persistence.i18n.yaml b/docs/rfc/implemented/architecture/2026-06-14-session-persistence.i18n.yaml index 3858139410..c6b9eaf967 100644 --- a/docs/rfc/implemented/architecture/2026-06-14-session-persistence.i18n.yaml +++ b/docs/rfc/implemented/architecture/2026-06-14-session-persistence.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-14-session-persistence.md: 4078487b71862791dd25cf2afcfc03255cfeb0be -2026-06-14-session-persistence.zh.md: 801632b56d2056b36dbf7869f5114d74599bd5d5 +2026-06-14-session-persistence.zh.md: a44ef9647ca4003bc81023a54b7e9b5945003b02 diff --git a/docs/rfc/implemented/architecture/2026-06-14-session-persistence.zh.md b/docs/rfc/implemented/architecture/2026-06-14-session-persistence.zh.md index 801632b56d..a44ef9647c 100644 --- a/docs/rfc/implemented/architecture/2026-06-14-session-persistence.zh.md +++ b/docs/rfc/implemented/architecture/2026-06-14-session-persistence.zh.md @@ -1,4 +1,4 @@ -# RFC:会话持久化——基于既有 `SessionEvent` 的抽象服务 +# RFC:会话持久化作为基于现有 `SessionEvent` 的抽象服务 Status: implemented @@ -6,31 +6,31 @@ Status: implemented ## 问题 -会话此前只存在于内存中。示例插件 `session-jsonl.ts`(在两个 examples 目录中逐字节重复)是只写的遥测:它缓冲 `session/event` 并追加 JSON 行,没有读取/回放路径,没有崩溃安全性(无 fsync、无原子写入、dispose 时 fire-and-forget 地排空缓冲区),没有列表功能,也没有格式版本控制。没有任何东西能把磁盘上的历史会话重新注入一个活跃的 agent,因此持久恢复(「继续昨天的任务」)、持久 fork,以及 ACP 的 `session/load` 方法([ACP 支持](../../implemented/feature/2026-06-14-acp-agent-client-protocol.md))都不可能实现。 +会话此前仅存在于内存中。示例插件 `session-jsonl.ts`(在两个示例中逐字节重复)是只写的遥测:它缓冲 `session/event` 并追加 JSON 行,没有读取/回放路径,没有崩溃安全性(无 fsync、无原子写入、fire-and-forget 的 dispose 排空),没有列表功能,也没有格式版本控制。没有任何机制能将磁盘上的历史会话重新注入到活跃的 agent(智能体)中,因此持久恢复("继续昨天的任务")、持久 fork 以及 ACP(Agent Client Protocol)的 `session/load` 方法([ACP 支持](../../implemented/feature/2026-06-14-acp-agent-client-protocol.md))都无法实现。 -[事件溯源模型](2026-06-11-event-sourced-sessions.md)将仅追加日志作为唯一真源,并从中派生 LLM 历史。持久化必须忠于这一点:直接持久化既有的 `SessionEvent`,不引入需要来回转换的并行「持久化消息」类型。后端也必须可替换——当前是文件存储,将来是数据库存储——统一在一个接口之后。 +[事件溯源模型](2026-06-11-event-sourced-sessions.md)将仅追加日志作为唯一真源,并从中派生 LLM(大语言模型)历史。持久化必须忠实于这一设计:直接持久化现有的 `SessionEvent`,不引入需要来回转换的并行"持久化消息"类型。后端也必须可替换——当前用文件存储,以后用数据库存储——统一在一个接口之后。 ## 决策 持久化是一个抽象的**能力 seam**([能力 seam](2026-06-13-capability-seams.md),`dsh-bash` 模板),而非循环或核心逻辑: -1. **接口**(`dsh-session-persistence`,`ctx.sessionPersistence`):一个抽象的 `SessionPersistence` 服务,提供 `create`/`append`/`load`/`list`。其持久化单元就是既有的 `SessionEvent`(`{ type, seq, time, data }`),逐字复用,无转换类型。 -2. **实现**(`dsh-session-persistence-jsonl`):每个会话一个仅追加的 JSONL 日志(一行 `SessionHeader`,之后每行一个 `SessionEvent`,逐字保留**包括 `assistant/chunk`**)。 +1. **接口**(`dsh-session-persistence`,`ctx.sessionPersistence`):一个抽象的 `SessionPersistence` 服务,提供 `create`/`append`/`load`/`list`。其持久化单元就是现有的 `SessionEvent`(`{ type, seq, time, data }`),原样复用,无转换类型。 +2. **实现**(`dsh-session-persistence-jsonl`):每个会话一个仅追加的 JSONL 日志(一行 `SessionHeader`,之后每行一个 `SessionEvent`,逐字节保留,**包括 `assistant/chunk`**)。 -以下关键选择记录于此,因为它们是持久的、有争议的、且出人意料的: +以下关键选择记录于此,因为它们是持久性的、有争议的、且出人意料的: -- **规范持久日志逐字保留每个 `SessionEvent`,包括 `assistant/chunk`。** `deriveMessages()` 跳过 chunk,过滤 chunk 的方案(Codex 的 `policy.rs`)很有吸引力,但 `seq = log.length` 以及加载校验 `events[i].seq === i` 要求日志*连续*;过滤掉 chunk 会留下空洞,同时破坏契约和恢复功能。未来可以将过滤 chunk 的投影作为带独立重编号的派生视图,但它不是规范日志。 -- **仅追加;崩溃的轮次被关闭,而非截断。** 已刷写的 `turn/end` 之前的事件永不重写,且循环只在轮次结束时刷写。由于一个被中断的轮次可能包含大量有效工作,`load` 会保留其中连续且可解析的事件,并为未应答的工具调用追加错误结果、补一个缺失的 `step/end`,以及带 `{ kind: 'interrupted' }` 的 `turn/end`。这些合成结果使恢复后的 provider transcript 保持有效。只有不完整的最后一条记录会被丢弃;如果在最后一个真实 `turn/end` 或之前出现解析错误或序号间隙,则视为损坏,该会话不可加载。 -- **文件后端为规范实现,数据库后端为已验证的可替换方案。** `SessionEvent` 1:1 映射为一行 `(session_id, seq, type, time, data)`:`append` 是 INSERT(在一个断言连续 seq 契约的事务中),`load` 是 SELECT … ORDER BY seq。`dsh-session-persistence-sqlite` 正是如此:一个 `SessionPersistence` 子类,接口不变(opencode 在 SQLite/WAL 上运行的正是这个形状),且它通过与 JSONL 后端相同的 `runPersistenceContract` 套件——因此契约以相同的语义(惰性物化、加载时关闭中断轮次、连续 seq)约束两个后端,一次表达在文件字节上,一次表达在行上。 -- **元数据在日志之外。** 格式版本、cwd 和谱系是存储关注点,不是可回放的对话状态,因此它们存放在 `dsh-session` 拥有的 `SessionHeader` 中,通过新的只读属性 `session.header` 附加到 `Session`——永远不在 `SessionEventMap` 中,永远不会到达 `deriveMessages()`。替代方案(一个可合并扩展的 `session/meta` 事件作为日志第 0 行)被否决:日志内事件在 seed/fork 会话时可以免费携带,但元数据不是可回放状态,因此显式的日志外 header seam 是更干净的代价。(header 最初被拆分为不可变的 `SessionHeader` 加可变的 `SessionSummary`,二者的联合类型为 `SessionMeta`;可变 summary 后来因为是死状态而被移除——见 [移除可变会话摘要](../simplification/2026-06-19-drop-mutable-session-summary.md)。) -- **`ctx.agents.create()` 与 `ctx.agents.resume()` 是异步工厂;resume 还额外跨越持久化边界。** `ctx.agents.resume({ resumeSessionId })` 等待 `ctx.sessionPersistence.load`,用加载的事件重建活跃会话(使 `lastTurnNumber`/`deriveMessages` 继续),并在恢复的 id 上启动一个新 agent(不是 `${agentId}-session`)。agent loop 不会硬注入 `sessionPersistence`(那会让非持久化的演示永远挂起);当 `sessionPersistence` 不存在时,`resume` 以明确的错误拒绝。 +- **规范的持久日志逐字节保留每个 `SessionEvent`,包括 `assistant/chunk`。** `deriveMessages()` 跳过 chunk,而过滤 chunk 的方案(Codex 的 `policy.rs`)很有吸引力,但 `seq = log.length` 以及加载验证 `events[i].seq === i` 要求日志是*连续*的;过滤掉 chunk 会留下空洞,同时破坏契约和恢复功能。基于 chunk 过滤的投影可以作为派生视图在后续实现(带有自己的重新编号),但它不是规范日志。 +- **仅追加;崩溃的轮次被关闭,而非截断。** 已刷写到 `turn/end` 的事件永不被重写,且循环仅在轮次结束时刷写。由于一个被中断的轮次可能包含大量有效工作,`load` 保留其连续、可解析的事件,并为未应答的工具调用追加错误结果、补一个缺失的 `step/end`,以及带 `{ kind: 'interrupted' }` 的 `turn/end`。合成的结果保证恢复后的 provider transcript(文本记录)仍然有效。只有不完整的最后一条记录会被丢弃;在最后一个真实 `turn/end` 处或之前出现解析错误或序号间隙,属于数据损坏,会使该会话不可加载。 +- **文件后端为规范实现,数据库后端为经过验证的直接替换。** `SessionEvent` 1:1 映射到一行 `(session_id, seq, type, time, data)`:`append` 是 INSERT(在一个断言连续 seq 契约的事务中),`load` 是 SELECT … ORDER BY seq。`dsh-session-persistence-sqlite` 正是如此:一个 `SessionPersistence` 子类,接口无变化(opencode 在 SQLite/WAL 上运行的正是这个形状),且通过与 JSONL 后端相同的 `runPersistenceContract` 测试套件。该契约以相同的语义约束两个后端(惰性物化、加载时关闭中断轮次、连续 seq),一次表达在文件字节上,一次表达在数据库行上。 +- **元数据在日志之外。** 格式版本、cwd 和谱系是存储关注点,不是可回放的对话状态,因此它们存放在 `dsh-session` 拥有的 `SessionHeader` 中,并通过新的只读属性 `session.header` 附加到 `Session` 上——永远不进入 `SessionEventMap`,永远不到达 `deriveMessages()`。替代方案(一个可合并扩展的 `session/meta` 事件作为日志第 0 行)被否决:日志内事件会随 seed/fork 的会话免费携带,但元数据不是可回放状态,因此显式的日志外 header seam 是更干净的代价。(header 最初被拆分为不可变的 `SessionHeader` 加可变的 `SessionSummary`,二者的联合类型为 `SessionMeta`;可变 summary 后来因属于死状态而被移除——见 [移除可变会话摘要](../simplification/2026-06-19-drop-mutable-session-summary.md)。) +- **`ctx.agents.create()` 和 `ctx.agents.resume()` 是异步工厂;resume 还跨越持久化边界。** `ctx.agents.resume({ resumeSessionId })` 等待 `ctx.sessionPersistence.load`,用加载的事件重建活跃会话(使 `lastTurnNumber`/`deriveMessages` 得以延续),并在恢复的 id 上启动一个新 agent(不是 `${agentId}-session`)。agent loop(智能体循环)不会硬注入 `sessionPersistence`(那样会让非持久化的演示永远挂起);当 `sessionPersistence` 不存在时,`resume` 以明确的错误拒绝。 ## 曾考虑的替代方案 -上述每个关键选择在陈述时已记录了其被否决的替代方案:**过滤 chunk 的规范日志**(Codex 的 `policy.rs` 形状)——破坏连续 seq 契约;**截断崩溃的轮次**——静默销毁长时间自主运行的真实工作;**日志内 `session/meta` 事件作为第 0 行**——元数据不是可回放状态;**将 `sessionPersistence` 硬注入循环**——会让非持久化的演示永远挂起。 +上述每个关键选择都在陈述处记录了被否决的替代方案:**过滤 chunk 的规范日志**(Codex 的 `policy.rs` 形式)破坏连续 seq 契约;**截断崩溃的轮次**会静默销毁长时间自主运行中的真实工作;**日志内 `session/meta` 事件作为第 0 行**——元数据不是可回放状态;**将 `sessionPersistence` 硬注入循环**会让非持久化的演示永远挂起。 -格式版本控制:header 携带一个 `version`;`load` 拒绝任何非当前版本(不做迁移——预发布的会话格式固定在 `SESSION_FORMAT_VERSION = 0`,按 AGENTS.md 的预发布立场吸收形状变动)。坦率地说:仅追加 + 刷写对部分尾部写入(加载时容忍)是健壮的,但对行写入中途的无 fsync 断电不健壮;数据库/WAL 后端是将来更强的选项。 +格式版本控制:header 携带一个 `version`;`load` 拒绝任何非当前版本(不做迁移——预发布阶段的会话格式固定为 `SESSION_FORMAT_VERSION = 0` 并吸收形状变动,遵循 AGENTS.md 的预发布立场)。坦率地说:仅追加 + 刷写对部分尾部写入是健壮的(加载时容忍),但对行写入中途的无 fsync 断电不健壮;数据库/WAL 后端是后续更强的选项。 ## 后果 -新增两个包(package),以及 `dsh-session` 中的元数据 seam(`session.header`、`create(id?, options?)` 签名)。收获:持久恢复/fork、读取/回放路径、崩溃容忍,以及 ACP `session/load`([ACP 支持](../../implemented/feature/2026-06-14-acp-agent-client-protocol.md))所需的基础——全部建立在既有的事件溯源日志之上,后端可在一个接口后替换。可复用的 `runPersistenceContract` 套件以相同的仅追加、连续 seq、惰性物化与可序列化语义约束每个后端。持久化完整日志还确定了事件保真度:`assistant/chunk` 保持逐字保留。 +新增两个包(package),以及 `dsh-session` 中的元数据 seam(`session.header`,`create(id?, options?)` 签名)。收益:持久恢复/fork、读取/回放路径、崩溃容忍,以及 ACP `session/load`([ACP 支持](../../implemented/feature/2026-06-14-acp-agent-client-protocol.md))所需的基础——全部基于现有的事件溯源日志,后端在一个接口之后可替换。可复用的 `runPersistenceContract` 测试套件以相同的仅追加、连续 seq、惰性物化与可序列化语义约束每个后端。持久化完整日志还确定了事件保真度:`assistant/chunk` 保持逐字节不变。 diff --git a/docs/rfc/implemented/architecture/2026-06-15-turn-enclosure-invariant.i18n.yaml b/docs/rfc/implemented/architecture/2026-06-15-turn-enclosure-invariant.i18n.yaml index 545fdae4ff..72eb9e3d7a 100644 --- a/docs/rfc/implemented/architecture/2026-06-15-turn-enclosure-invariant.i18n.yaml +++ b/docs/rfc/implemented/architecture/2026-06-15-turn-enclosure-invariant.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-15-turn-enclosure-invariant.md: bf0789f21ba7bd928e023bb5fd844d9ec78c4bc1 -2026-06-15-turn-enclosure-invariant.zh.md: 9f6f69b923ed7feff51122c4e4040bff3c9db8ea +2026-06-15-turn-enclosure-invariant.zh.md: 644e7b975e024286d5307f586f509c2b4a9f21ea diff --git a/docs/rfc/implemented/architecture/2026-06-15-turn-enclosure-invariant.zh.md b/docs/rfc/implemented/architecture/2026-06-15-turn-enclosure-invariant.zh.md index 9f6f69b923..644e7b975e 100644 --- a/docs/rfc/implemented/architecture/2026-06-15-turn-enclosure-invariant.zh.md +++ b/docs/rfc/implemented/architecture/2026-06-15-turn-enclosure-invariant.zh.md @@ -1,4 +1,4 @@ -# RFC:每个会话事件必须包含在一个轮次内 +# RFC:每个会话事件都封闭在一个轮次内 Status: implemented @@ -6,37 +6,37 @@ Status: implemented ## 问题 -持久化的会话持久化后端(在一个配套变更中引入)以**轮次**作为崩溃恢复边界:崩溃可能留下一个未关闭的最终轮次,`load` 会用一个合成的 `turn/end {kind:'interrupted'}` 将其关闭,同时保留该轮次的真实事件(见[会话持久化](2026-06-14-session-persistence.md))。这种恢复只有在没有任何*合法的*持久化事件位于轮次之外(即上一个 `turn/end` 与下一个 `turn/start` 之间的间隙)时才是良定义的,否则这类事件会被裹入下一个轮次的中断关闭中。 +持久化的会话持久化后端(在配套变更中引入)以**轮次**作为崩溃恢复边界:崩溃可能留下一个未关闭的最终轮次,`load` 会用一个合成的 `turn/end {kind:'interrupted'}` 将其关闭,同时保留该轮次的真实事件(见[会话持久化](2026-06-14-session-persistence.md))。这种恢复只有在没有任何*合法的*持久事件位于轮次之外(即上一个 `turn/end` 与下一个 `turn/start` 之间的间隙)时才是良定义的,否则这类事件会被卷入下一个轮次的中断关闭中。 -该假设并不成立。有两条路径在轮次之外记录了事件: +这一假设并不成立。有两条路径在任何轮次之外记录了事件: -1. **排队的用户消息。** agent loop(智能体循环)排空排队消息并在 `turn/start` **之前**追加 `user/message`,导致一个轮次自身的提示词落在前一个 `turn/end` 与下一个 `turn/start` 之间的间隙中。 -2. **空闲时的上下文注入。** `agent.inject()` 直接追加一条 `context/message`。它在生产环境中的实际调用方是 `dsh-tool-bash`,后者从 `ctx.bash.onTaskDone` 注入后台任务完成通知——该回调在后台 bash 任务完成时触发,经常发生在 agent **空闲**(两个轮次之间)时。 +1. **排队的用户消息。** agent loop(智能体循环)排空排队消息并在 `turn/start` **之前**追加 `user/message`——于是一个轮次自身的提示词落在了前一个 `turn/end` 与下一个 `turn/start` 之间的间隙中。 +2. **空闲时的上下文注入。** `agent.inject()` 直接追加一条 `context/message`。它在生产环境中的真实调用方是 `dsh-tool-bash`,后者从 `ctx.bash.onTaskDone` 注入后台任务完成通知——该回调在后台 bash 任务完成时触发,而这经常发生在 agent **空闲**(轮次之间)时。 -对于情况 2,如果注入的 `context/message` 是 flush/dispose 之前的最后一个事件(之后没有轮次追加 `turn/end`),`scanLog` 会将其视为崩溃残留并**在恢复时丢弃**——注入的上下文虽然已持久化到磁盘,但在重新加载时被静默丢失。情况 1 单独来看是无害的(`user/message` 之后总是紧跟它触发的轮次),但使得「什么可以出现在轮次之外」这条规则变得模糊。 +在情况 2 中,如果注入的 `context/message` 是 flush/dispose 之前的最后一个事件(之后没有轮次追加 `turn/end`),`scanLog` 会将其视为崩溃残留并在**恢复时丢弃**——注入的上下文已持久写入磁盘,但重新加载后被静默丢失。情况 1 本身无害(`user/message` 之后总会跟着它触发的轮次),但使「什么可以出现在轮次之外」这条规则变得模糊。 ## 决策 -**每个会话事件都位于一个轮次内部**——在一个 `turn/start` 与其匹配的 `turn/end` 之间。具体而言: +**每个会话事件都位于一个轮次内部**:在 `turn/start` 与其匹配的 `turn/end` 之间。具体而言: -- agent loop 在 `turn/start` **之后**(轮次内部)追加排队的 `user/message` 事件,而非在其之前。因此,这些消息一经记录,`turn/end` 就已被承诺,而现有的 finalizer 保证了这一点。 -- 在 agent **运行中**调用 `agent.inject()` 时,其 `context/message` 追加到已打开的轮次中(行为不变)。 -- 在 agent **空闲时**调用 `agent.inject()`,系统将 `context/message` 包裹在一个一次性轮次中:`turn/start{trigger:{kind:'injection'}}` → `context/message` → `turn/end{completed}`。一个新的 `injection` 变体加入可合并扩展的 `TurnTriggerMap`。 -- agent loop 每次迭代从日志推导下一个轮次编号(`lastTurnNumber(session) + 1`),而非维护一个私有计数器,因此空闲注入的一次性轮次不会与下一个真实轮次的编号冲突。 -- `dsh-invariants` 插件在开发模式下**强制执行**该不变式:在没有打开的轮次时追加 `user/message`/`context/message`/`steering/message` 会抛出 `InvariantError`。 +- agent loop 在 `turn/start` **之后**(轮次内部)追加排队的 `user/message` 事件,而非之前。因此,一旦这些消息被记录,就欠下一个 `turn/end`,既有的 finalizer 保证它被写入。 +- agent **运行中**调用 `agent.inject()` 时,`context/message` 追加到已打开的轮次中(行为不变)。 +- agent **空闲时**调用 `agent.inject()`,则将 `context/message` 包裹在一个一次性轮次中:`turn/start{trigger:{kind:'injection'}}` → `context/message` → `turn/end{completed}`。一个新的 `injection` 变体加入可合并扩展的 `TurnTriggerMap`。 +- agent loop 每次迭代从日志推导下一个轮次编号(`lastTurnNumber(session) + 1`),而不是维护一个私有计数器,这样空闲注入的一次性轮次不会与下一个真实轮次的编号冲突。 +- `dsh-invariants` 插件在开发环境中**强制执行**该不变式:在没有打开轮次的情况下追加 `user/message` / `context/message` / `steering/message` 会抛出 `InvariantError`。 -可序列化性不变式在同一个源码边界强制执行(`Session.append` 对不可 JSON 序列化的数据抛出异常),因此「什么可以进入日志」现在由一处统一管控,而非由下游恰好在监听的某个后端去发现。 +可序列化性不变式在同一源码边界处强制执行(`Session.append` 对不可 JSON 序列化的数据抛出异常),因此「什么可以进入日志」现在由一个位置统一管控,而非由下游碰巧在监听的某个后端各自发现。 ## 曾考虑的替代方案 -**放宽读取端而非约束生产端**——让 `scanLog` 提交位于已打开轮次之外的事件。否决:一条可检查的生产端规则优于一条更宽松的边界扫描逻辑,后者需要同时推理部分轮次*和*轮次间的散落事件。 +**放宽读取端而非约束生产端**——让 `scanLog` 提交位于已打开轮次之外的事件。否决:一条单一、可检查的生产端规则优于一个更宽松的边界扫描(后者需要同时推理部分轮次*和*轮次间的散落事件)。 ## 后果 -轮次现在是*唯一的*持久化/回放边界,因此[会话持久化](2026-06-14-session-persistence.md)的崩溃恢复规则是完备的,而不仅仅是充分的:一个被中断的最终轮次会被关闭(用合成的 `turn/end {interrupted}`),其真实事件被保留,且完全不存在将轮次间上下文混入其中的风险,因为不再有轮次间上下文。`scanLog` 保持简单(至多一个可能未关闭的最终轮次,永远没有散落的轮次间事件),空闲时的后台任务通知在持久化 + 恢复后得以存活。 +轮次现在是*唯一的*持久性/回放边界,因此[会话持久化](2026-06-14-session-persistence.md)的崩溃恢复规则是完备的,而不仅仅是充分的:被中断的最终轮次被关闭(用合成的 `turn/end {interrupted}`),其真实事件得以保留,且零风险将轮次间上下文混入其中,因为不存在轮次间上下文。`scanLog` 保持简洁(最多一个可能未关闭的最终轮次,绝无散落的轮次间事件),空闲时的后台任务通知在持久化 + 恢复后依然存活。 -代价:空闲时调用 `agent.inject()` 现在写入三行日志而非一行,且推导出的历史中多出一个仅包含注入上下文(无 assistant 输出)的轮次——`deriveMessages()` 本就纯粹按事件类型推导,因此渲染结果不变。`injection` 触发器是一个新的磁盘词汇值;与每一个 `SessionEventMap`/`TurnTriggerMap` 的新增项一样,它属于冻结格式的一部分。轮次内的事件顺序发生了变化(`turn/start` 现在先于 `user/message`),这对任何断言旧顺序的代码是可观测的——agent loop 自身的测试是唯一的此类消费方。 +代价:空闲时调用 `agent.inject()` 现在写入三行日志而非一行;派生的历史中多出一个仅包含注入上下文(无 assistant 输出)的轮次——`deriveMessages()` 已经纯粹按事件类型派生,因此渲染结果完全相同。`injection` 触发器是一个新的磁盘词汇值;与每次 `SessionEventMap`/`TurnTriggerMap` 的新增一样,它属于冻结格式的一部分。轮次内的事件顺序发生了变化(`turn/start` 现在先于 `user/message`),这对任何断言旧顺序的代码可观测——agent loop 自身的测试是唯一的此类消费方。 -该规则有意采用生产端强制执行 + 开发模式检查的方式,而非读取端容忍:未来的后端(SQLite/WAL)可以免费继承同样干净的边界,而在轮次外记录事件的插件会在开发模式下大声失败,而非在下次重新加载时静默丢失数据。 +该规则有意采用生产端强制、开发环境检查的方式,而非读取端容忍的方式:未来的后端(SQLite/WAL)无需额外工作即可继承同样干净的边界,而在轮次外记录事件的插件会在开发环境中大声失败,而非在下次重新加载时静默丢失数据。 -轮次内检测到的失败在 `turn/end` 之前记录。之后的 flush 失败没有合法的轮次内位置,因此通过 `agent/error` 和日志报告,而非作为会话事件追加。这保持了回放日志的平衡;持久化的运维诊断需要一个独立的遥测通道。 +轮次内检测到的失败在 `turn/end` 之前记录。后续的 flush 失败没有有效的轮次内位置,因此通过 `agent/error` 和日志报告,而非作为会话事件追加。这保持了回放日志的平衡;持久化的运维诊断需要一个独立的遥测通道。 diff --git a/docs/rfc/implemented/architecture/2026-06-17-filesystem-capability-seam.i18n.yaml b/docs/rfc/implemented/architecture/2026-06-17-filesystem-capability-seam.i18n.yaml index 5d92edb79a..11faf45d26 100644 --- a/docs/rfc/implemented/architecture/2026-06-17-filesystem-capability-seam.i18n.yaml +++ b/docs/rfc/implemented/architecture/2026-06-17-filesystem-capability-seam.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-17-filesystem-capability-seam.md: c502ae712de22e97661192057d4410c7c55ea044 -2026-06-17-filesystem-capability-seam.zh.md: 01a4318237ebbd29fe3effa3f8528b6e4a5132d6 +2026-06-17-filesystem-capability-seam.zh.md: 4e544e24c548a52bde254886a65ff2fdb559a986 diff --git a/docs/rfc/implemented/architecture/2026-06-17-filesystem-capability-seam.zh.md b/docs/rfc/implemented/architecture/2026-06-17-filesystem-capability-seam.zh.md index 01a4318237..4e544e24c5 100644 --- a/docs/rfc/implemented/architecture/2026-06-17-filesystem-capability-seam.zh.md +++ b/docs/rfc/implemented/architecture/2026-06-17-filesystem-capability-seam.zh.md @@ -1,42 +1,42 @@ # RFC:文件系统能力 seam——ctx.fs、本地后端与面向模型的文件系统工具 -Status: implemented - [English](2026-06-17-filesystem-capability-seam.md) | 中文 +Status: implemented + ## 问题 -harness 已有一个具体的 `bash` 能力 seam(`dsh-bash` / `dsh-bash-local` / `dsh-tool-bash`),但文件系统操作即将作为面向模型的工具加入,却没有等价的 seam。如果 `read`、`write` 和 `edit` 直接使用 `node:fs`,面向模型的工具包将同时拥有文件系统执行策略、本地路径解析、原子写入行为、文本解码、符号链接行为和编辑语义。 +harness 已有一个具体的 `bash` 能力 seam(`dsh-bash` / `dsh-bash-local` / `dsh-tool-bash`),但文件系统操作即将作为面向模型的工具加入,却没有等价的 seam。如果 `read`、`write` 和 `edit` 直接使用 `node:fs`,面向模型的工具包将同时承担文件系统执行策略、本地路径解析、原子写入行为、文本解码、符号链接行为和编辑语义。 这把三个独立变化的关注点耦合在了一起: 1. 文件系统契约:插件可以请求哪些操作。 -2. 后端:当前是本地磁盘,未来可能是沙箱/远程/项目范围的文件系统。 +2. 后端:当前是本地磁盘,未来可能是沙箱/远程/项目作用域的文件系统。 3. 消费方接口:面向模型的 `read` / `write` / `edit` schema 与结果格式化。 -没有 `ctx.fs` 接口,将本地文件系统访问替换为沙箱或远程后端时,即使面向模型的契约应当保持稳定,也会搅动工具 schema、演示和提示词引导。这还使权限/沙箱边界更难推理:一个 `cwd` 选项看起来像沙箱,但除非有显式后端或 `tools/execute` 策略强制隔离,否则它只是一个基础路径。 +如果没有 `ctx.fs` 接口,将本地文件系统访问替换为沙箱或远程后端时,即使面向模型的契约应当保持稳定,工具 schema、演示和提示词引导也会被迫变动。这还使权限/沙箱边界更难推理:一个 `cwd` 选项看起来像沙箱,但除非有显式的后端或 `tools/execute` 策略强制隔离,否则它只是一个基础路径。 -我们需要让文件系统工具在成为公开包接口之前,以与 bash 相同的能力 seam 形态落地。 +我们需要文件系统工具在成为公开包(package)接口之前,以与 bash 相同的能力 seam 形态落地。 ## 决策 -文件系统访问是一个一等能力 seam,遵循[能力 seam RFC](../../implemented/architecture/2026-06-13-capability-seams.md): +文件系统访问是一个一等的能力 seam,遵循[能力 seam RFC](../../implemented/architecture/2026-06-13-capability-seams.md): 1. `@deepseek-ai/dsh-fs`(`packages/fs/fs`)拥有抽象的 `ctx.fs` 服务、文件系统词汇类型,以及 `fs/*` 策略事件词汇。 2. `@deepseek-ai/dsh-fs-local`(`packages/fs/fs-local`)提供第一个实现,以本地文件系统为后端。 -3. `@deepseek-ai/dsh-tool-fs`(`packages/fs/tool-fs`)通过 `ctx.fs` 提供面向模型的 `read`、`write` 和 `edit` 工具,并作为执行器分发 `fs/*` 事件。 +3. `@deepseek-ai/dsh-tool-fs`(`packages/fs/tool-fs`)通过 `ctx.fs` 提供面向模型的 `read`、`write` 和 `edit` 工具,是分发 `fs/*` 事件的执行器。 消费方包仅依赖接口包,从不依赖 `dsh-fs-local`。需要不同后端的部署只需为 `ctx.fs` 加载不同的提供方,无需改动工具 schema 或面向模型的提示词引导。 -先读后写/编辑与已观察状态策略是第四个包 `@deepseek-ai/dsh-fs-policy`(`packages/fs/fs-policy`),通过 `fs/*` 事件门而非 `ctx.fs` 方法贡献;加载 `dsh-tool-fs` 的部署同时加载 `dsh-fs-policy` 以获得先读后写/编辑能力。本 RFC 确立了三包 seam;策略从提供方基类拆出的决策见 [split-fs-seam RFC](../simplification/2026-06-26-fsspec-style-fs-seam.md),其作为事件门插件(而非方法服务)的实现见 [event-gate RFC](2026-06-26-file-context-as-event-gate.md)。本文已更新为描述最终落地的四包形态。 +读后写/编辑与观测状态策略是第四个包 `@deepseek-ai/dsh-fs-policy`(`packages/fs/fs-policy`),通过 `fs/*` 事件门控贡献,而非挂在 `ctx.fs` 上;加载 `dsh-tool-fs` 的部署同时加载 `dsh-fs-policy` 以获得读后写/编辑能力。本 RFC 确立了由三个包构成的 seam;策略从提供方基类拆出的决策由 [split-fs-seam RFC](../simplification/2026-06-26-fsspec-style-fs-seam.md) 做出,其以事件门控插件(而非方法服务)实现的方式由 [event-gate RFC](2026-06-26-file-context-as-event-gate.md) 做出。本文已更新为描述最终落地的四包形态。 -第一个后端刻意仅限本地:`dsh-fs-local` 针对宿主文件系统实现 `ctx.fs`。未来的兄弟后端可以在同一接口后面提供沙箱、远程、虚拟或项目范围的文件系统。 +第一个后端有意仅限本地:`dsh-fs-local` 基于宿主文件系统实现 `ctx.fs`。未来的兄弟后端可在同一接口之后提供沙箱、远程、虚拟或项目作用域的文件系统。 -第一个消费方刻意仅限文本文件:`dsh-tool-fs` 暴露面向模型的 `read`、`write` 和 `edit` 工具,处理 UTF-8 文本文件。未来的消费方可以添加目录列表、搜索/glob、二进制安全操作、文件监听或更高层的项目操作,只要所需能力存在于 `ctx.fs` 上,就无需改动本地后端包。直接目录列表后来由 [Add direct directory listing to the filesystem seam](2026-07-03-filesystem-directory-listing-seam.md) 添加。 +第一个消费方有意仅限文本文件:`dsh-tool-fs` 暴露面向模型的 `read`、`write` 和 `edit` 工具,处理 UTF-8 文本文件。未来的消费方可以添加目录列表、搜索/glob、二进制安全操作、文件监视或更高层的项目操作,只要 `ctx.fs` 上存在所需能力,就无需改动本地后端包。直接目录列表后来由 [Add direct directory listing to the filesystem seam](2026-07-03-filesystem-directory-listing-seam.md) 添加。 -文件系统权限和沙箱并非此拆分所隐含。本地后端从其配置的基础目录解析相对路径,但隔离策略是独立决策:要么由更严格的 `ctx.fs` 实现强制执行,要么由权限/沙箱插件包装 `tools/execute` 并在调用到达消费方之前否决。 +文件系统权限和沙箱并非此拆分所隐含。本地后端从其配置的基目录解析相对路径,但隔离策略是独立的决策:要么由更严格的 `ctx.fs` 实现强制执行,要么由权限/沙箱插件包装 `tools/execute` 并在调用到达消费方之前否决。 -先读后写/编辑与已观察状态属于 `dsh-fs-policy`,而非 `ctx.fs`。通过 `fs/*` 事件门,策略按不透明 actor 记录版本,并提供可选的变更期望;提供方原子性地强制新鲜度。`dsh-tool-fs` 发出事件但不依赖策略。详见 [split-seam](../simplification/2026-06-26-fsspec-style-fs-seam.md) 与 [event-gate](2026-06-26-file-context-as-event-gate.md) RFC。 +读后写/编辑与观测状态属于 `dsh-fs-policy`,而非 `ctx.fs`。通过 `fs/*` 事件门控,策略按不透明 actor 记录版本,并提供可选的变更期望;提供方原子性地强制新鲜度。`dsh-tool-fs` 发出事件但不依赖策略。见 [split-seam](../simplification/2026-06-26-fsspec-style-fs-seam.md) 和 [event-gate](2026-06-26-file-context-as-event-gate.md) RFC。 ## 包拓扑 @@ -47,11 +47,11 @@ harness 已有一个具体的 `bash` 能力 seam(`dsh-bash` / `dsh-bash-local` consumer interface implementation ``` -`@deepseek-ai/dsh-fs` 仅依赖 `cordis` 和来自 `@deepseek-ai/dsh-llm` 的仓库级 `HarnessError` 基类。它声明 `ctx.fs` 键、抽象 `FileSystem` 服务、后端与消费方共享的词汇类型、文件系统错误词汇,以及 `fs/*` 策略事件词汇。它不持有已观察状态存储,也不持有 owner 推导形态;事件传递一个不透明的 `object` actor,提供方从不读取它,`dsh-fs-policy` 插件在这些事件之上拥有 owner 推导形态和已观察状态存储。 +`@deepseek-ai/dsh-fs` 仅依赖 `cordis` 加上来自 `@deepseek-ai/dsh-llm` 的仓库级 `HarnessError` 基类。它声明 `ctx.fs` 键、抽象 `FileSystem` 服务、后端和消费方共享的词汇类型、文件系统错误词汇,以及 `fs/*` 策略事件词汇。它不持有观测状态存储,也不持有 owner 推导形态;事件传递一个不透明的 `object` actor,提供方从不读取它,`dsh-fs-policy` 插件在这些事件之上拥有 owner 推导形态和观测状态存储。 -`@deepseek-ai/dsh-fs-local` 依赖 `@deepseek-ai/dsh-fs` 和 `cordis`。它继承 `FileSystem`,将自身注册为 `ctx.fs`,拥有本地后端配置(如基础目录),并包含所有直接的 `node:fs` / `node:path` 访问。它不持有已观察状态存储:新鲜度是后端铸造、策略插件记录的版本令牌。 +`@deepseek-ai/dsh-fs-local` 依赖 `@deepseek-ai/dsh-fs` 和 `cordis`。它继承 `FileSystem`,将自身注册为 `ctx.fs`,拥有本地后端配置(如基目录),并包含所有直接的 `node:fs` / `node:path` 访问。它不持有观测状态存储——新鲜度是后端铸造、策略插件记录的版本令牌。 -`@deepseek-ai/dsh-tool-fs` 依赖 `@deepseek-ai/dsh-fs`、`@deepseek-ai/dsh-tools`、`@deepseek-ai/dsh-system-prompt` 和 `cordis`。它注册面向模型的工具和提示词段落。它禁止导入 `node:fs`、`node:path` 或 `@deepseek-ai/dsh-fs-local`;文件系统执行始终通过 `ctx.fs`。如果实现需要具体的 agent 或会话辅助类型,这些依赖属于 `tool-fs`;它们禁止回漏到 `dsh-fs`。 +`@deepseek-ai/dsh-tool-fs` 依赖 `@deepseek-ai/dsh-fs`、`@deepseek-ai/dsh-tools`、`@deepseek-ai/dsh-system-prompt` 和 `cordis`。它注册面向模型的工具和提示词段落。它禁止导入 `node:fs`、`node:path` 或 `@deepseek-ai/dsh-fs-local`;文件系统执行始终通过 `ctx.fs`。如果实现需要具体的 agent 或会话辅助类型,这些依赖属于 `tool-fs`;它们禁止回漏到 `dsh-fs` 中。 根 `tool-fs` 插件通过组合各工具的注册辅助函数来注册完整的文件系统工具套件(`read`、`write` 和 `edit`)。它注入 `fs`,从不导入实现包。 @@ -59,23 +59,23 @@ harness 已有一个具体的 `bash` 能力 seam(`dsh-bash` / `dsh-bash-local` `@deepseek-ai/dsh-fs` 拥有一个语义文件系统服务。它比 `readFile` / `writeFile` 更高层,这样 `tool-fs` 就不必重新实现路径解析、版本管理、文本解码、二进制拒绝、分页、原子替换、符号链接行为或字面编辑语义。 -该接口覆盖以下语义操作: +该接口涵盖以下语义操作: - 将模型/插件提供的路径解析为后端定义的目标。 -- 在不读取文件内容的情况下获取目标元数据。 +- 获取目标元数据而不读取文件内容。 - 从目标读取有界的 UTF-8 文本页。 - 创建或替换一个 UTF-8 文本文件。 -- 通过字面替换编辑一个已存在的 UTF-8 文本文件。 +- 通过字面替换编辑一个已有的 UTF-8 文本文件。 -提供方 seam 还承载策略所依赖的新鲜度钩子,但已观察状态存储和 owner 推导位于 `dsh-fs-policy` 插件中,而非 `ctx.fs` 上: +提供方 seam 还携带策略所依赖的新鲜度钩子——但观测状态存储和 owner 推导位于 `dsh-fs-policy` 插件中,而非 `ctx.fs` 上: -- 后端为每个目标铸造一个不透明的 `version` 令牌(在 `stat` 和每次读取/变更结果中)。 -- `writeText`/`editText` 接受一个可选的版本期望:省略它则执行无条件的裸提供方变更,提供它则在后端的原子临界区内守护变更。 -- `dsh-fs-policy` 插件在 `fs/write-intent`/`fs/edit-intent` 上决定该期望,并在 `fs/observed` 上记录已观察版本,以从不透明事件 actor 推导出的 owner 为键(通常是 `exec.agent.session`)。 +- 后端为每个目标铸造一个不透明的 `version` 令牌(在 `stat` 以及每次读取/变更结果中)。 +- `writeText`/`editText` 接受一个可选的版本期望:省略它表示无条件的裸提供方变更;提供它则在后端的原子临界区内守护变更。 +- `dsh-fs-policy` 插件在 `fs/write-intent`/`fs/edit-intent` 上决定该期望,并在 `fs/observed` 上记录观测版本,以它从不透明事件 actor 推导出的 owner 为键(通常是 `exec.agent.session`)。 -授权基于版本新鲜度,而非完整/部分视图的区分:任何读取都记录目标的版本,后续的写入/编辑只要文件仍处于该版本即被授权——因此对第 100-150 行的窗口读取可以授权对第 120 行的编辑。已观察状态存储是 `dsh-fs-policy` 内部的 `WeakMap>`;`dsh-fs` 不持有任何此类数据,并将 actor 视为不透明。(本 RFC 最初建模了一个带 `full`/`partial` 视图的 `FileState` 缓存放在 `ctx.fs` 上;split-fs-seam 和 event-gate RFC 将其替换为此处描述的基于新鲜度的策略插件。) +授权基于版本新鲜度,而非完整/部分视图的区分:任何读取都会记录目标的版本,后续的写入/编辑只要文件仍处于该版本就被授权——因此对第 100-150 行的窗口化读取可以授权对第 120 行的编辑。观测状态存储是 `dsh-fs-policy` 内部的 `WeakMap>`;`dsh-fs` 不持有任何此类数据,并将 actor 视为不透明。(本 RFC 最初建模了一个带 `full`/`partial` 视图的 `FileState` 缓存放在 `ctx.fs` 上;split-fs-seam 和 event-gate RFC 将其替换为此处描述的基于新鲜度的策略插件。) -路径解析是显式的,允许异步。本地解析可能只做路径规范化,但沙箱/远程/项目范围的后端可能需要 I/O 才能将用户提供的路径解析为稳定的目标标识。 +路径解析是显式的,允许异步。本地解析可能只做路径规范化,但沙箱/远程/项目作用域的后端可能需要 I/O 才能将用户提供的路径解析为稳定的目标标识。 解析后的目标必须至少暴露三个概念: @@ -83,19 +83,19 @@ harness 已有一个具体的 `bash` 能力 seam(`dsh-bash` / `dsh-bash-local` - 不透明的 `targetKey`,用于过期守护和文件状态查找。本地后端可能使用类似 realpath 的键;远程后端可能使用工作区 URI 或文件 id。消费方禁止解析或假设它是本地绝对路径。 - `displayPath`,用于面向模型/UI 的输出。根据后端不同,它可能是本地绝对路径、工作区相对路径或远程 URI。 -读取和变更结果必须包含一个不透明的文件 `version`。本地后端可以使用 mtime/size 或类 hash 令牌;远程后端可以使用修订 id。`dsh-fs-policy` 插件记录版本用于过期检查;消费方可以展示相关元数据但禁止解释版本令牌。 +读取和变更结果必须包含不透明的文件 `version`。本地后端可以使用 mtime/size 或类似 hash 的令牌;远程后端可以使用 revision id。`dsh-fs-policy` 插件记录版本用于过期检查;消费方可以展示相关元数据但禁止解释版本令牌。 -提供方返回已解码的文本:`readText` 返回整个常规文本文件,`streamText` 以流式传输相同的文本语义用于大文件。二者都负责常规文件检查;有界行/输出处理不是它们的职责——行窗口、带行号渲染和总行数统计位于执行器(`dsh-tool-fs`)中,执行器通过 `ctx.fs` 读取并渲染面向模型的窗口。提供方负责 UTF-8 解码和二进制/NUL 拒绝;它不知道行窗口或视图。 +提供方返回已解码的文本:`readText` 返回整个常规文本文件,`streamText` 为大文件流式传输相同的文本语义。两者负责常规文件检查;有界的行/输出处理不是它们的职责——行窗口化、带行号渲染和总行数统计位于执行器(`dsh-tool-fs`)中,执行器通过 `ctx.fs` 读取并渲染面向模型的窗口。提供方负责 UTF-8 解码和二进制/NUL 拒绝;它不知道行窗口或视图。 -已观察状态记录不在 `ctx.fs` 上:成功读取后执行器发出 `fs/observed`,`dsh-fs-policy` 插件为推导出的 owner 记录 `{ version }`。没有 `full`/`partial` 视图——任何窗口的读取都记录版本,新鲜度(而非视图完整性)授权后续的写入/编辑。 +观测状态记录不在 `ctx.fs` 上:成功读取后,执行器发出 `fs/observed`,`dsh-fs-policy` 插件为推导出的 owner 记录 `{ version }`。没有 `full`/`partial` 视图——任何窗口的读取都记录版本,新鲜度(而非视图完整性)授权后续的写入/编辑。 -全文件写入创建或替换 UTF-8 文本文件。后端在支持且有文档说明时可以创建父目录。已存在的非常规目标被拒绝。`writeText` 接受一个可选期望:`createIfAbsent` 创建缺失的目标并在已存在时以 `FS_NOT_OBSERVED` 拒绝(这是策略为未观察 owner 使用的路径);`replaceIfVersion` 仅在目标处于已观察版本时替换,否则 `FS_STALE_VERSION`;省略期望则为无条件的裸提供方创建或覆盖。策略插件根据 owner 的已观察状态选择提供哪个期望。 +全文件写入创建或替换 UTF-8 文本文件。后端在支持且有文档说明时可以创建父目录。已有的非常规目标被拒绝。`writeText` 接受一个可选期望:`createIfAbsent` 创建缺失的目标并拒绝已存在的(报 `FS_NOT_OBSERVED`,这是策略为未观测 owner 使用的路径);`replaceIfVersion` 仅在目标处于观测版本时替换,否则报 `FS_STALE_VERSION`;省略期望则为无条件的裸提供方创建或覆盖。策略插件根据 owner 的观测状态选择提供哪个期望。 -字面编辑是提供方原语(`editText`),而非在 `tool-fs` 中由读取加写入组合而成。字面匹配、重复匹配拒绝、CRLF 保留、二进制拒绝、可选的过期版本检查和原子读-改-写必须一起留在后端的变更临界区内。`editText` 接受相同的可选版本期望;过期检查在字面匹配之前运行,因此针对旧读取的编辑会报告 `FS_STALE_VERSION`。远程后端可以将编辑实现为原生的 compare-and-edit 操作;消费方不强制本地式组合。 +字面编辑是提供方原语(`editText`),而非在 `tool-fs` 中由读取加写入组合而成。字面匹配、重复匹配拒绝、CRLF 保留、二进制拒绝、可选的过期版本检查和原子读-改-写必须一起留在后端的变更临界区内。`editText` 接受相同的可选版本期望;过期检查在字面匹配之前运行,因此基于旧读取的编辑会报 `FS_STALE_VERSION`。远程后端可以将编辑实现为原生的 compare-and-edit 操作;消费方不强制本地风格的组合。 -策略插件(而非 `ctx.fs`)对先前观察进行门控:`edit` 要求 owner 有先前观察(否则 `FS_NOT_OBSERVED`),记录的版本作为 CAS 基础传递给 `editText`。在策略插件缺席时,`ctx.fs` 单独是一个完整的无约束 seam(无条件写入/编辑);工具从不与策略方法耦合。 +策略插件(而非 `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 区分,因此已移除。目录列表相关的错误码后来由 [Add direct directory listing to the filesystem seam](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 区分,因此已删除。目录列表相关的错误码后来由 [Add direct directory listing to the filesystem seam](2026-07-03-filesystem-directory-listing-seam.md) 添加。) ## 工具消费方行为 @@ -105,7 +105,7 @@ harness 已有一个具体的 `bash` 能力 seam(`dsh-bash` / `dsh-bash-local` - `read`:检查一个 UTF-8 文本文件并返回带行号的内容与分页引导。 - `write`:创建或完全替换一个 UTF-8 文本文件。 -- `edit`:通过替换字面文本更新一个已存在的 UTF-8 文本文件,默认要求唯一匹配,并允许显式的全部替换模式。 +- `edit`:通过替换字面文本更新一个已有的 UTF-8 文本文件,默认要求唯一匹配,并允许显式的全部替换模式。 每个工具遵循相同的执行形态: @@ -114,47 +114,47 @@ harness 已有一个具体的 `bash` 能力 seam(`dsh-bash` / `dsh-bash-local` 3. 将结果格式化为面向模型的 `ContentBlock[]`。 4. 让抛出的后端/工具错误流经 `ToolRegistry.execute()`,由其转换为 `isError` 工具结果。 -该包通过 `ctx.systemPrompt.section(...)` 注册提示词引导,通过 `ctx.tools.register(...)` 注册 schema。工具 schema 仍通过 `SystemPrompt.assemble()` 和 `ToolRegistry.schemas()` 流入正常的提示词组装路径;无需修改 agent loop。 +该包通过 `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` 创建新文件不要求先前观察。 +默认部署要求在用 `write` 或 `edit` 更新已有文件之前先 `read`。`tool-fs` 不通过检查是否运行过名为 `read` 的工具来实现这一点:它分发 `fs/write-intent`/`fs/edit-intent` 事件(将执行上下文作为不透明 actor 传递),`dsh-fs-policy` 插件推导 owner、对先前观测进行门控并提供版本期望。任何窗口化读取都能授权后续的写入/编辑,只要文件未变。用 `write` 创建新文件不要求先前观测。 根插件通过组合各工具的注册辅助函数来注册完整套件。它注入 `fs`、`tools` 和 `systemPrompt`。 ## 测试 -测试遵循包边界,而非仅覆盖用户可见的工具:`dsh-fs` 中的服务 seam;`dsh-fs-local` 中通过 `ctx.fs` 接口的真实文件系统行为(解析、符号链接、流式传输、二进制/UTF-8 拒绝、无条件与版本守护写入、字面编辑语义、行尾保留、结构化 `FsError` 错误码);`dsh-tool-fs` 中针对真实本地提供方的消费方接口(仅 mock 模型/时钟,从不 mock 协作者);以及通过 `ctx.tools.execute()` 在有无 `dsh-fs-policy` 两种情况下的集成测试,通过从磁盘回读文件来验证世界状态,而非信任返回的 `ContentBlock[]`。已观察状态/owner 推导策略在 `dsh-fs-policy` 中测试,不在此处。 +测试遵循包边界,而不仅是用户可见的工具:`dsh-fs` 中的服务 seam;`dsh-fs-local` 中通过 `ctx.fs` 接口测试的真实文件系统行为(解析、符号链接、流式传输、二进制/UTF-8 拒绝、无条件和版本守护的写入、字面编辑语义、行尾保留、结构化 `FsError` 错误码);`dsh-tool-fs` 中基于真实本地提供方的消费方接口(只 mock 模型/时钟,从不 mock 协作者);以及通过 `ctx.tools.execute()` 在有和没有 `dsh-fs-policy` 的情况下进行集成测试,通过从磁盘回读文件来验证世界状态,而非信任返回的 `ContentBlock[]`。观测状态/owner 推导策略在 `dsh-fs-policy` 中测试,不在此处。 本仓库曾踩过的防御性模式类别被直接固定: -- **原子写入临时文件安全。** 写入/编辑通过目标旁边一个私有随机 `0700` 目录中的独占 owner-only(`'wx'`、`0o600`)临时文件暂存,失败时清理,最后原子 rename。这与 bash spill-file 规则一致,因为可预测的 world-readable 临时路径招致符号链接竞争和信息泄露。测试断言权限以及已存在的临时路径不会被覆盖;此原语是 seam 的常设要求。 -- **通过符号链接的 `targetKey` 同一性。** 两个输入路径解析到同一 realpath 时共享一个已观察状态条目:通过路径 A 的 `read` 满足通过符号链接路径 B 的 `edit` 的先读守护,通过一个路径的过期写入可通过另一个路径检测到。 -- **并发/过期竞争。** 两个并发的写入/编辑操作针对同一目标确定性地结算:一个成功,另一个以 `FS_STALE_VERSION` 被拒绝;成功的编辑刷新记录状态,使同一 owner 的下一次编辑可以继续。 -- **HMR 安全与 dispose。** 释放后端的 fiber 会撤回 `ctx.fs` 提供方;后续提供方启动时没有继承的状态。 +- **原子写入临时文件安全。** 写入/编辑通过目标旁边一个私有随机 `0700` 目录中的独占 owner-only(`'wx'`、`0o600`)临时文件暂存,失败时清理,最后原子 rename——与 bash 溢出文件规则一致,因为可预测的 world-readable 临时路径招致符号链接竞争和信息泄露。测试断言权限,并断言已存在的临时路径不会被覆盖;此原语是 seam 的常设要求。 +- **通过符号链接的 `targetKey` 同一性。** 两个输入路径解析到同一 realpath 时共享一个观测状态条目:通过路径 A 的 `read` 满足通过符号链接路径 B 的 `edit` 的读后编辑守护,通过一个路径的过期写入可通过另一个路径检测到。 +- **并发/过期竞争。** 对同一目标的两个并发写入/编辑操作确定性地收敛——一个成功,另一个被 `FS_STALE_VERSION` 拒绝——成功的编辑刷新记录状态,使同一 owner 的下一次编辑可以继续。 +- **HMR(热模块替换)安全与 dispose(资源释放)。** dispose 后端的 fiber 会撤回 `ctx.fs` 提供方;后续的提供方以无继承状态启动。 ## 曾考虑的替代方案 -- **面向模型的工具直接使用 `node:fs`**:工具包将同时拥有执行策略、路径解析、原子写入、文本解码和编辑语义,耦合了「问题」一节所列的三个独立变化的关注点,且任何后端替换都会搅动 schema。 -- **单一合并包 `dsh-fs-tools`**:seam 之前的形态;出于与 bash 相同的接口/实现/消费方拆分理由被否决,且合并名称从未成为公开接口。 -- **已观察状态放在 `ctx.fs` 上**:本 RFC 最初落地的形态;被 [split-fs-seam RFC](../simplification/2026-06-26-fsspec-style-fs-seam.md) 和 [event-gate RFC](2026-06-26-file-context-as-event-gate.md) 取代:沙箱/远程后端不应继承面向模型的观察策略,因此提供方仅保留版本令牌和可选的版本守护变更。 +- **面向模型的工具直接基于 `node:fs`**:工具包将同时承担执行策略、路径解析、原子写入、文本解码和编辑语义,耦合问题部分所列的三个独立变化的关注点,且任何后端替换都会搅动 schema。 +- **单一合并包 `dsh-fs-tools`**:seam 之前的形态;以与 bash 相同的接口/实现/消费方拆分理由否决,且合并名称从未成为公开接口。 +- **观测状态放在 `ctx.fs` 上**:本 RFC 最初落地的形态;被 [split-fs-seam RFC](../simplification/2026-06-26-fsspec-style-fs-seam.md) 和 [event-gate RFC](2026-06-26-file-context-as-event-gate.md) 取代:沙箱/远程后端不应继承面向模型的观测策略,因此提供方只保留版本令牌和可选的版本守护变更。 ## 后果 -**`cwd` 可能被误认为沙箱。** 本地后端的基础目录是解析默认值,而非自动的隔离边界。如果需要隔离,必须由后端契约或 `tools/execute` 上的权限/沙箱插件强制执行。 +**`cwd` 可能被误认为沙箱。** 本地后端的基目录是解析默认值,而非自动的隔离边界。如果需要隔离,必须由后端契约或 `tools/execute` 上的权限/沙箱插件强制执行。 -**接口可能变得过于本地化。** 如果 `ctx.fs` 返回 `absolutePath` 之类的字段,远程、沙箱或虚拟后端会变得尴尬。契约应暴露展示元数据,而不要求消费方理解宿主路径。 +**接口可能变得过于本地化。** 如果 `ctx.fs` 返回 `absolutePath` 之类的字段,远程、沙箱或虚拟后端会变得尴尬。契约应暴露显示元数据,而不要求消费方理解宿主路径。 -**接口可能变得过于薄。** 如果 `ctx.fs` 只镜像 `node:fs` 原语,`tool-fs` 将重新实现二进制检测、分页、原子写入和编辑语义。这会重新制造本 RFC 试图避免的耦合。 +**接口可能变得过于薄。** 如果 `ctx.fs` 只镜像 `node:fs` 原语,`tool-fs` 将重新实现二进制检测、分页、原子写入和编辑语义,重新制造本 RFC 试图避免的耦合。 -**编辑语义天然易受竞争影响。** 字面编辑是读-改-写操作;守护是后端的原子变更临界区加上可选的版本期望,因此并发编辑确定性地结算:一个赢,另一个得到 `FS_STALE_VERSION`。 +**编辑语义天然易受竞争影响。** 字面编辑是读-改-写操作;守护手段是后端的原子变更临界区加上可选的版本期望,因此并发编辑确定性地收敛——一个赢,另一个得到 `FS_STALE_VERSION`。 -**已观察状态不属于 `ctx.fs`。** 记录执行上下文看到了什么是工作流策略,而非原始文件系统 I/O。本 RFC 最初将其放在文件系统 seam 内;split-fs-seam RFC 随后确立:沙箱/远程后端不应继承面向模型的观察策略,并将其移入 `dsh-fs-policy` 插件。提供方 seam 仅保留写入/编辑安全在存储层真正需要的东西——后端铸造的版本令牌和可选的版本守护变更——而策略插件拥有 owner 推导、已观察状态和先读后编辑门控,通过 `fs/*` 事件实现。 +**观测状态不属于 `ctx.fs`。** 记录执行上下文看到了什么是工作流策略,而非原始文件系统 I/O。本 RFC 最初将其放在文件系统 seam 内部;split-fs-seam RFC 随后确立了沙箱/远程后端不应继承面向模型的观测策略,并将其移入 `dsh-fs-policy` 插件。提供方 seam 只保留写入/编辑安全在存储层真正需要的东西——后端铸造的版本令牌和可选的版本守护变更——而策略插件拥有 owner 推导、观测状态和基于 `fs/*` 事件的读后编辑门控。 -**`resolve` 后操作的形态每次调用多一次往返。** 每个工具可能先将路径解析为 `FsTarget`,再作为单独的 `ctx.fs` 调用发起读取/写入/编辑。对本地后端而言这可以忽略(解析是内存中的路径规范化),但远程/沙箱后端可能将每一步变为独立请求,使单次 `read` 变成两次网络往返。往返开销重要的后端可以在内部缓存或折叠解析,同时保持可观察契约不变。 +**`resolve` 然后操作的形态每次调用多一次往返。** 每个工具可能先将路径解析为 `FsTarget`,再以单独的 `ctx.fs` 调用发起读取/写入/编辑。对本地后端来说这可以忽略(解析是内存中的路径规范化),但远程/沙箱后端可能将每步变成独立请求,使单次 `read` 变为两次网络往返。往返开销重要的后端可以在内部缓存或折叠解析,同时保持可观测契约不变。 -**已观察状态持久化被推迟。** 已观察状态存在于内存中(`dsh-fs-policy` 内部的 `WeakMap`),因此恢复的会话保守地要求文件在写入/编辑前重新读取,直到未来的会话事件或持久化机制使观察可回放。 +**观测状态持久化被推迟。** 观测状态存在于内存中(`dsh-fs-policy` 内部的 `WeakMap`),因此恢复的会话保守地要求文件在写入/编辑前重新读取,直到未来的会话事件或持久化机制使观测可回放。 -**错误码成为 seam 的一部分。** `FsError` 错误码使过期版本和观察失败可通过既有的结构化错误分类体系进行机器路由。代价是 `dsh-fs` 从 `dsh-llm` 导入共享的 `HarnessError` 基类;该依赖是有意为之且仅限于错误词汇。 +**错误码成为 seam 的一部分。** `FsError` 错误码使过期版本和观测失败可通过既有的结构化错误分类体系进行机器路由。代价是 `dsh-fs` 从 `dsh-llm` 导入共享的 `HarnessError` 基类;该依赖是有意为之且限于错误词汇。 -**包拆分的代价前置。** 三包拆分在只有一个后端时就增加了样板代码。这是有意为之:文件系统访问是可能的沙箱/远程边界,在面向模型的工具发布后再改变包接口代价更高。 +**包拆分的成本前置。** 三包拆分在只有一个后端时就增加了样板代码。这是有意为之:文件系统访问是可能的沙箱/远程边界,在面向模型的工具发布后再改包接口代价更高。 diff --git a/docs/rfc/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-seams.i18n.yaml b/docs/rfc/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-seams.i18n.yaml index 85783f6ea4..27f4b260ae 100644 --- a/docs/rfc/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-seams.i18n.yaml +++ b/docs/rfc/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-seams.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-18-agent-lifecycle-and-ownership-seams.md: a70e7db8d809efd68ae770995795fc7b3d1b83d2 -2026-06-18-agent-lifecycle-and-ownership-seams.zh.md: ac42a09c70e9570d3def0f0bd056bd571b923315 +2026-06-18-agent-lifecycle-and-ownership-seams.zh.md: 3fb68336c56b5296f18b3587ea42399a05362733 diff --git a/docs/rfc/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-seams.zh.md b/docs/rfc/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-seams.zh.md index ac42a09c70..3fb68336c5 100644 --- a/docs/rfc/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-seams.zh.md +++ b/docs/rfc/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-seams.zh.md @@ -1,12 +1,12 @@ # RFC:Agent 生命周期与所有权 seam -Status: implemented - [English](2026-06-18-agent-lifecycle-and-ownership-seams.md) | 中文 +Status: implemented + ## 问题 -ACP(Agent Client Protocol)与 tool-bash 的若干限制是同一个缺失 seam 的不同症状:插件可以通过 `ctx.agents` 创建或恢复 agent,但无法独立拥有并 dispose(资源释放)单个 agent;长时间运行的 bash 任务在执行器内部也没有稳定的所有者。ACP 在断开连接时中止并等待 agent,却无法只注销该会话的 agent;`session/cancel` 无法取消已排队但尚未开始的工作;`tool-bash` 将任务所有权保存在插件本地的 `Map` 中,因此一次 HMR(热模块替换)重载就可能让旧任务看起来无主。 +ACP(Agent Client Protocol)与 tool-bash 的若干限制是同一个缺失 seam 的症状:插件可以通过 `ctx.agents` 创建或恢复 agent(智能体),但无法独立拥有和 dispose(资源释放)单个 agent,而长时间运行的 bash 任务在执行器中也没有稳定的所有者。ACP 在断连时中止并等待 agent,却无法仅注销该会话的 agent;`session/cancel` 无法取消已入队但尚未开始的工作;`tool-bash` 将任务所有权保存在插件本地的 `Map` 中,因此一次 HMR(热模块替换)重载就可能让旧任务看起来无主。 ## 决策 @@ -14,33 +14,33 @@ ACP(Agent Client Protocol)与 tool-bash 的若干限制是同一个缺失 se ### 1. 队列感知的 `Agent.cancel(reason?)` -`cancel()` 是唯一的公开停止原语。它清除已排队的输入和 steering(中途引导)输入,中止正在执行的步骤,并设置一个在每个轮次边界检查的轮次作用域标记。因此,已排队的提示词在取消后无法启动,也无法吸收后续输入。`whenIdle()` 等待取消后的静默状态,ACP 的 `session/cancel` 映射到此方法。对空闲 agent 的 cancel 不设置标记。 +`cancel()` 是唯一的公开停止原语。它清除已入队和 steering(中途引导)输入、中止正在进行的步骤,并设置一个在每个轮次边界检查的轮次作用域标记。因此,已入队的 prompt 在取消后无法启动,也无法吸收后续输入。`whenIdle()` 等待取消后的静默状态,ACP 的 `session/cancel` 映射到此方法。对空闲状态的 cancel 不设置标记。 ### 2. `AgentHandle` 异步释放器 -`ctx.agents.create`/`resume` 与 `AgentFactory` 返回 `AgentHandle = { agent, dispose() }`。释放是消费方的能力;仅持有 `Agent` 的观察者无法拆除它。调用方 fiber 和 factory 提供方也拥有该实例,所有路径共享同一个 memoized 的拆除流程:停止循环、等待静默与 flush 完成、分离 agent 和会话,然后回收其 scope。注册表条目分离后 ID 即可复用。由配置创建的 agent 归 loop fiber 所有;ACP 存储并 dispose 每个会话的 handle。 +`ctx.agents.create`/`resume` 和 `AgentFactory` 返回 `AgentHandle = { agent, dispose() }`。释放是消费方的能力;仅持有 `Agent` 的观察者无法将其拆除。调用方 fiber 和 factory 提供方也拥有该实例,所有路径共享一个 memoize 的拆除过程:停止循环、等待静默与刷写完成、分离 agent 和会话,然后解除其 scope。ID 在注册表条目分离后变为可复用。由配置创建的 agent 归 loop fiber 所有;ACP 存储并 dispose 每个会话的 handle。 -拆除顺序对持久性至关重要。会话生命周期与循环共享一个复合 Cordis effect,因此 LIFO 释放先停止循环并等待 `agent.done`,再分离会话。如果使用兄弟 effect,它们会并发释放,可能在关闭 flush 之前移除 append 钩子。释放通知被隔离,不会中断拆除链。 +拆除顺序对持久性至关重要。会话生命周期与循环共享一个复合 Cordis effect,因此 LIFO 释放会先停止循环并等待 `agent.done`,然后再分离会话。若使用兄弟 effect,则会并发释放,可能在关闭刷写之前就移除 append 钩子。释放通知被隔离,不会中断拆除链。 -### 3. Bash 所有者令牌置于 seam 中 +### 3. Bash seam 中的所有者令牌 -后台任务的所有权归执行器持有。`BashExecSpec.owner` 携带一个可选的不透明令牌,`ownerOf(id)` 读取它,`dsh-tool-bash` 在启动时盖上调用方的会话令牌。`bash_output` 与 `bash_kill` 拒绝不匹配的调用方;完成通知通过注册表按会话令牌定位存活的 agent。将所有权保留在任务上,使得这道围栏在工具插件重载后依然有效。完成监听器仍然是 effect 作用域的,因此在重载间隙到达的通知仍可能被丢弃。 +后台任务的所有权属于执行器。`BashExecSpec.owner` 携带一个可选的不透明令牌,`ownerOf(id)` 读取它,`dsh-tool-bash` 在启动时盖上调用方的会话令牌。`bash_output` 和 `bash_kill` 拒绝不匹配的调用方;完成通知通过注册表按会话令牌定位存活的 agent。将所有权保存在任务上,使得这道隔离在工具插件重载后依然有效。完成监听器仍然是 effect 作用域的,因此在重载间隙到达的通知仍可能被丢弃。 ## 验证 -- ACP 断开连接或会话关闭后,不留下任何已注册的 agent 或 session-store 条目,包括 `session/load` 与拆除竞争的情况。 -- 在已排队的提示词启动前取消,能阻止该提示词运行或吸收下一条提示词。 +- ACP 断连或会话关闭后,不留下任何已注册的 agent 或 session-store 条目,包括 `session/load` 与拆除竞争的情况。 +- 在已入队的 prompt 启动前取消,能阻止该 prompt 运行或吸收下一条 prompt。 - 重载 `dsh-tool-bash` 不会让另一个会话读取或终止已有的后台任务,因为所有权保留在执行器上。 -- 由配置创建的 agent 仍归 loop fiber 所有,因此非 ACP 的演示无需显式管理 handle。 +- 由配置创建的 agent 仍归 loop fiber 所有,因此非 ACP 演示无需显式管理 handle。 ## 会话所有者令牌在存活 agent 中唯一 -bash 所有者令牌依赖 `session.header.id` 在存活 agent 中的唯一性。并发的同 ID 操作可以私下准备,但 `SessionStore.enter()` 拒绝重复发布,失败的事务会回滚。`tool-bash` 拥有比较策略;bash seam 存储一个不透明的 `owner` 字符串,不对其做解释。 +bash 所有者令牌依赖 `session.header.id` 在存活 agent 中的唯一性。并发的同 ID 操作可以私下准备,但 `SessionStore.enter()` 拒绝重复发布,失败的事务回滚。`tool-bash` 拥有比较策略;bash seam 存储一个不透明的 `owner` 字符串,不对其做解释。 ## 曾考虑的替代方案 - **公开的 `BashTask.owner` 字段**而非 `BashExecutor.ownerOf(id)` seam:否决。一条读取路径即可,无需冗余 API。 -- **为 agent 的会话生命周期使用兄弟 Cordis effect**:否决。fiber 卸载时兄弟 effect 并发释放(`Promise.all`),store 持有的 append 发布钩子的移除与循环的关闭 `session/flush` 产生竞争;单一复合 effect 的有序 LIFO 链才能在两条释放路径上都捕获关闭的 `turn/end`。 +- **为 agent 的会话生命周期使用兄弟 Cordis effect**:否决。fiber 卸载时并发释放兄弟 effect(`Promise.all`),store 拥有的 append 发布钩子的移除与循环的关闭 `session/flush` 产生竞争;单一复合 effect 的有序 LIFO 链才能在两条释放路径上都捕获关闭的 `turn/end`。 - **在 `cancel()` 之外另设一个仅中止步骤的 `abort()`**:最初发布过,后因无人使用而移除;`cancel()` 是唯一的公开停止原语(见[公开停止接口 RFC](../simplification/2026-06-20-public-agent-stop-surface.md))。 ## 后果 diff --git a/docs/rfc/implemented/architecture/2026-06-18-session-surface.i18n.yaml b/docs/rfc/implemented/architecture/2026-06-18-session-surface.i18n.yaml index 0e2a4891d9..98b4ee0e59 100644 --- a/docs/rfc/implemented/architecture/2026-06-18-session-surface.i18n.yaml +++ b/docs/rfc/implemented/architecture/2026-06-18-session-surface.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-18-session-surface.md: 31297166b735468147850a81d7fd43a8fa30a1e8 -2026-06-18-session-surface.zh.md: 9e2933a1e564b3dbb7719d55b5264f670c3b833f +2026-06-18-session-surface.zh.md: 159aefc10261ac5380701f46c3d4a367674940b1 diff --git a/docs/rfc/implemented/architecture/2026-06-18-session-surface.zh.md b/docs/rfc/implemented/architecture/2026-06-18-session-surface.zh.md index 9e2933a1e5..159aefc102 100644 --- a/docs/rfc/implemented/architecture/2026-06-18-session-surface.zh.md +++ b/docs/rfc/implemented/architecture/2026-06-18-session-surface.zh.md @@ -1,22 +1,22 @@ -# RFC:Session surface——基于事件日志的链表,用于 LLM 消息推导 - -Status: implemented +# RFC:会话 surface——基于事件日志的链表,用于 LLM 消息派生 [English](2026-06-18-session-surface.md) | 中文 +Status: implemented + ## 问题 -事件日志是权威数据源,但历史操作此前没有持久化的共享机制。如果没有这样的机制,上下文压缩(context compaction)等插件只能通过顺序敏感的监听器改写派生请求,不留溯源记录,且每次新增操作都要修改 `deriveMessages()`。 +事件日志是权威数据源,但历史操纵此前没有持久化的共享机制。如果没有这样的机制,上下文压缩(context compaction)等插件只能通过顺序敏感的监听器改写派生请求,不留溯源信息,且每次新增操纵都要反复修改 `deriveMessages()`。 ## 决策 -新增一个 **surface**:一条从事件日志派生、带缓存的链表,由「surface 节点」(即产出 LLM 消息的那部分事件)组成,通过事件日志中的 `surfaceOp` 标记维护。 +新增一个 **surface**:一条派生的、缓存的链表,由「surface 节点」(事件中产出 LLM(大语言模型)消息的子集)组成,通过事件日志中的 `surfaceOp` 标记维护。 -### `SessionEvent` 上的两个新顶层字段 +### `SessionEvent` 新增两个顶层字段 -每个 `SessionEvent` 新增两个可选字段(与 `seq`/`time` 同属结构元数据): +每个 `SessionEvent` 获得两个可选字段(结构性元数据,与 `seq`/`time` 同级): -- **`sourceEventSeqs?: number[]`**:作为溯源来源的事件 seq 编号(例如:构成 `assistant/message` 的各 `assistant/chunk` 的 seq,或被压缩标记遮蔽的 surface 节点)。溯源是核心设计原则;没有它,replace-range 操作在回放时无法被验证。 +- **`sourceEventSeqs?: number[]`**:作为溯源来源的事件 seq 编号(例如构成 `assistant/message` 的各 `assistant/chunk` 的 seq,或被压缩标记遮蔽的 surface 节点)。溯源是核心设计原则;没有它,replace-range 操作在回放时无法被验证。 - **`surfaceOp?: SurfaceOp`**:该事件如何进入 surface。非 surface 事件不携带此字段。 ### SurfaceOp:两种操作 @@ -27,45 +27,45 @@ export type SurfaceOp = | { op: 'replace'; start: number; end: number } // shadow [start, end] inclusive ``` -1. **Append**:在尾部追加一个新节点。`user/message`、`assistant/message`、`tool/result`、`context/message`、`steering/message` 使用此操作。agent loop 在所有此类追加上传入 `surfaceOp: 'append'`,并在适用时附带 `sourceEventSeqs`(例如 `assistant/message` 记录其 `assistant/chunk` 来源;`tool/result` 记录其 `tool/call` 来源)。 +1. **Append**:在尾部追加一个新节点。`user/message`、`assistant/message`、`tool/result`、`context/message`、`steering/message` 使用此操作。agent loop(智能体循环)在所有此类追加上传入 `surfaceOp: 'append'`,并在适用时附带 `sourceEventSeqs`(例如 `assistant/message` 记录其 `assistant/chunk` 来源;`tool/result` 记录其 `tool/call` 来源)。 -2. **Replace**:移除从 `start` 到 `end`(两端含)的节点,并在其位置插入一个新节点。`start` 和 `end` 都必须是当前 surface 上有效的 surface 节点 seq;`start === end` 表示替换单个节点。该节点的 `sourceEventSeqs` 必须包含所有被遮蔽的 surface 节点。被遮蔽的事件仍保留在日志中,但不再出现在 surface 上。 +2. **Replace**:移除从 `start` 到 `end`(两端包含)的节点,并在其位置插入一个新节点。`start` 和 `end` 都必须是当前 surface 上有效的 surface 节点 seq;`start === end` 表示替换单个节点。该节点的 `sourceEventSeqs` 必须包含所有被遮蔽的 surface 节点。被遮蔽的事件仍留在日志中,但不再出现在 surface 上。 ### SurfaceManager:基于增量,而非全量重建 -`SurfaceManager` 类(`Session` 的私有实现)维护缓存的链表。它跟踪 `_lastProcessedSeq`,仅处理**增量**(上次访问以来的新事件),而非重新扫描整个日志。由于日志是仅追加的,先前事件不会改变;种子日志只是在首次访问时折叠的初始增量。 +`SurfaceManager` 类(`Session` 私有)维护缓存的链表。它跟踪 `_lastProcessedSeq`,仅处理**增量**(自上次访问以来的新事件),而非重新扫描整个日志。由于日志是仅追加的,先前的事件不会改变;种子日志只是在首次访问时折叠的初始增量。 无新事件时增量处理为 O(1),有新事件到达时为 O(新事件数)。 -`deriveMessages()` 在存在 surface 标记时使用 surface,否则回退到既有的线性扫描(向后兼容)。 +`deriveMessages()` 在存在 surface 标记时使用 surface,对没有标记的会话回退到既有的线性扫描(向后兼容)。 ### 持久化 -新字段作为顶层 JSON 属性序列化。JSONL 后端无需任何修改:`JSON.stringify`/`JSON.parse` 透明地保留一切。SQLite 后端的 `events` 表新增两个可空 TEXT 列(`source_event_seqs`、`surface_op`)。磁盘上的 `SCHEMA_VERSION` 递增以反映列集变化,并且按照预发布的 bump-and-reject 策略,由其他构建写入的数据库在打开时被拒绝,而非迁移(没有需要升级的持久化用户数据)。会话格式 `version` 固定为 `SESSION_FORMAT_VERSION = 0`(「不稳定/预发布」立场):可选的 surface 字段被吸收而不递增版本号。 +新字段作为顶层 JSON 属性序列化。JSONL 后端无需任何改动:`JSON.stringify`/`JSON.parse` 透明地保留一切。SQLite 后端的 `events` 表新增两个可空 TEXT 列(`source_event_seqs`、`surface_op`)。磁盘上的 `SCHEMA_VERSION` 递增以反映列集变化,并且按照预发布的 bump-and-reject 策略,由其他构建写入的数据库在打开时被拒绝而非迁移(没有需要升级的持久化用户数据)。会话格式 `version` 固定为 `SESSION_FORMAT_VERSION = 0`(「不稳定/预发布」立场):可选的 surface 字段被吸收而不递增版本号。 ### 崩溃恢复 -`repair.ts` 模块在崩溃后为孤立的工具调用合成 `tool/result` 关闭事件。这些关闭事件携带 `surfaceOp: 'append'` 和指向孤立 `tool/call` 事件的 `sourceEventSeqs`,确保重建后的 surface 有效。 +`repair.ts` 模块在崩溃后为孤立的工具调用合成 `tool/result` 闭合事件。这些闭合事件携带 `surfaceOp: 'append'` 和指向孤立 `tool/call` 事件的 `sourceEventSeqs`,确保重建的 surface 有效。 ### 不变式 -开发模式不变式插件验证:`sourceEventSeqs` 引用(非空、无重复、引用更早的事件、引用已知 seq)以及 `surfaceOp`(replace 的 `start ≤ end`、两个端点都在被跟踪的 surface 上、范围在 surface 位置上不反转、`sourceEventSeqs` 包含该范围遮蔽的每个节点)。 +开发模式下的不变式插件验证:`sourceEventSeqs` 引用(非空、无重复、引用更早的事件、引用已知 seq)以及 `surfaceOp`(replace 的 `start ≤ end`、两个端点都在被跟踪的 surface 上、范围在 surface 位置上不反转、`sourceEventSeqs` 包含该范围遮蔽的每个节点)。 -每个 surface 可达事件都必须携带 `surfaceOp`,否则它会从派生历史中消失。类型化的 `append` 重载对字面事件类型强制执行此要求;`append` 和种子构造函数中的运行时检查覆盖了宽化联合类型和加载的日志。无效种子在预发布格式策略下被拒绝而非升级。 +每个 surface 可达事件都必须携带 `surfaceOp`,否则它将从派生历史中消失。类型化的 `append` 重载对字面事件类型强制执行此规则;`append` 和种子构造函数中的运行时检查覆盖宽化联合类型和加载的日志。按照预发布格式策略,无效的种子被拒绝而非升级。 ## 曾考虑的替代方案 -- **逐插件的 `agent/request` 包装**(surface 之前的历史操作模式):监听器排序脆弱,不留持久化的变更记录,且每次新增操作都要修改核心 `deriveMessages()`。 +- **逐插件的 `agent/request` 包装**(surface 之前的历史操纵模式):监听器排序脆弱、无法持久记录改动内容,且每种新操纵都迫使核心 `deriveMessages()` 再次修改。 - **半开区间 `[start, endExclusive)` 的 replace 范围**:否决。surface 是双向链表,端点自然以节点 seq 命名,单节点替换(`start === end`)在闭区间语义下读起来更自然。 -- **脏标记触发全量重建**而非增量处理:在会话生命周期内为 O(N²)——每次单事件追加都要重新扫描所有先前事件。 +- **脏标记后全量重建**替代增量处理:在会话生命周期内为 O(N²),每次单事件追加都要重新扫描所有先前事件。 ## 后果 -- **`packages/core/session`**:新增 `surface.ts`(`SurfaceManager`)、新类型(`SurfaceOp`、`SurfaceIntent`)、`SessionEvent` 上的新字段、修改 `append()`(第三个必需参数 `SurfaceIntent`)、重构 `deriveMessages()`(以 surface 遍历作为唯一推导路径)、surface 感知的 `repair.ts`。种子构造函数拒绝缺少 `surfaceOp` 标记的 surface 可达种子事件(见「不变式」一节)。 -- **`packages/core/agent-loop`**:所有 surface 可达的追加传入 surface 选项。收集 chunk seq 用于 `assistant/message` 溯源;捕获 `tool/call` seq 用于 `tool/result` 溯源。 +- **`packages/core/session`**:新增 `surface.ts`(`SurfaceManager`)、新类型(`SurfaceOp`、`SurfaceIntent`)、`SessionEvent` 新字段、修改 `append()`(第三个必选参数 `SurfaceIntent`)、重构 `deriveMessages()`(以 surface 遍历作为唯一派生路径)、surface 感知的 `repair.ts`。种子构造函数拒绝缺少 `surfaceOp` 标记的 surface 可达种子事件(见「不变式」一节)。 +- **`packages/core/agent-loop`**:所有 surface 可达的追加操作传入 surface 选项。收集 chunk seq 用于 `assistant/message` 溯源;捕获 `tool/call` seq 用于 `tool/result` 溯源。 - **`packages/session-persistence/session-persistence-sqlite`**:`events` 表新增两个可空 TEXT 列(`source_event_seqs`、`surface_op`);`SCHEMA_VERSION` 递增(bump-and-reject,无迁移)。 -- **`packages/support/invariants`**:surface 相关的验证规则。 -- **`packages/session-persistence/session-persistence-jsonl`**:无需修改。 +- **`packages/support/invariants`**:surface 相关验证规则。 +- **`packages/session-persistence/session-persistence-jsonl`**:无需改动。 - **`packages/session-persistence/session-persistence`**:抽象接口不变。 -Surface 是未来历史操作的基础。压缩或 tool-result-prune 插件追加一个既有的消息产出事件类型(例如一条携带摘要的 `user/message`),附带 `surfaceOp: { op: 'replace', start, end }` 和覆盖被遮蔽节点的 `sourceEventSeqs`——新节点取代该范围在 surface 上的位置,而插件自身的跟踪事件(如 `compaction/start`、`compaction/end`)则不进入 surface。回放确定性地保留这一决策。 +Surface 是未来历史操纵的基础。压缩或 tool-result-prune 插件追加一个既有的消息产出事件类型(例如一条携带摘要的 `user/message`),附带 `surfaceOp: { op: 'replace', start, end }` 和覆盖被遮蔽节点的 `sourceEventSeqs`——新节点在 surface 上取代该范围的位置,而插件自身的 trace 事件(如 `compaction/start`、`compaction/end`)不进入 surface。回放以确定性方式保留该决策。 diff --git a/docs/rfc/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.i18n.yaml b/docs/rfc/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.i18n.yaml index 566ce4ae59..f693539b91 100644 --- a/docs/rfc/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.i18n.yaml +++ b/docs/rfc/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-18-shared-persistence-write-coordinator.md: 3fc30dc2e1382fd983d050433123f46a2cd0ed19 -2026-06-18-shared-persistence-write-coordinator.zh.md: 6c8ccef8dd5603d837dd0a9884adf9c1cd8a17d4 +2026-06-18-shared-persistence-write-coordinator.zh.md: 38e900d38cc48317836717ddeda5323cf97df993 diff --git a/docs/rfc/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.zh.md b/docs/rfc/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.zh.md index 6c8ccef8dd..38e900d38c 100644 --- a/docs/rfc/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.zh.md +++ b/docs/rfc/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.zh.md @@ -1,44 +1,44 @@ # RFC:共享持久化写入协调器 -Status: implemented - [English](2026-06-18-shared-persistence-write-coordinator.md) | 中文 +Status: implemented + ## 问题 -`dsh-session-persistence-jsonl` 与 `dsh-session-persistence-sqlite` 有意在不同存储介质上证明同一份 `SessionPersistence` 契约,但二者的写入路径编排是重复的:per-session 状态、`session/created` 接管、后端特定的前缀读取、write-behind 缓冲区、串行化 flush 链、HMR(热模块替换)种子注入,以及 dispose(资源释放)排空。纯粹的种子前缀冲突与可串行化守卫已经迁入 seam 包;剩余的编排仍然是正确性密集的,并且相同的修复被应用了两次。代码级 diff 表明两个后端在**所有**这些逻辑上是逐字节一致或同算法的:四个 map(`states`/`buffers`/`chains`/`inits`)、`installWritePath`、`initFor`、`onCreated` 的四种分支、`flush`、`drain`、`serialize`、`adopt`、`adoptLivePrefix`、`assertVersion`,以及 `create`/`append`/`load` 骨架。唯一不同的只有存储原语(写字节 vs. INSERT 行)。 +`dsh-session-persistence-jsonl` 与 `dsh-session-persistence-sqlite` 有意在不同存储介质上证明同一份 `SessionPersistence` 契约,但它们的写入路径编排是重复的:per-session 状态、`session/created` 接管、后端特定的前缀读取、write-behind 缓冲区、序列化的 flush 链、HMR(热模块替换)种子注入与 dispose(资源释放)排空。纯粹的种子前缀碰撞检查与可序列化守卫已迁入 seam 包;剩余的编排仍然对正确性要求很高,且同样的修复被应用了两次。代码级 diff 表明两个后端在**全部**这些逻辑上要么字节相同、要么算法相同:四个 map(`states`/`buffers`/`chains`/`inits`)、`installWritePath`、`initFor`、`onCreated` 的四种分支、`flush`、`drain`、`serialize`、`adopt`、`adoptLivePrefix`、`assertVersion`,以及 `create`/`append`/`load` 的骨架。唯一的差异在于存储原语(写字节 vs. INSERT 行)。 ## 决策 -将一个后端无关的 `PersistenceCoordinator` 提取到 `dsh-session-persistence` 中。协调器统一拥有编排逻辑;每个第一方后端组合一个实例(`new PersistenceCoordinator(ctx, this)`),实现一个小型 `PersistenceBackend` 钩子接口,并将其四个公开服务方法(`create`/`append`/`load`/`list`)委托给协调器。 +将一个后端无关的 `PersistenceCoordinator` 提取到 `dsh-session-persistence` 中。协调器统一拥有编排逻辑;每个第一方后端组合一个协调器实例(`new PersistenceCoordinator(ctx, this)`),实现一个小型 `PersistenceBackend` 钩子接口,并将其四个公开服务方法(`create`/`append`/`load`/`list`)委托给协调器。 -组合,而非继承。协调器是后端持有的具体类,不是后端继承的基类。本 RFC 的风险点——「协调器不得迫使非常规后端与继承层级搏斗」——由此规避:后端只暴露钩子;它无法触及协调器的私有编排状态,且公开的 `SessionPersistence` 服务形状不变,因此第三方后端仍然可以完全不使用协调器、直接实现抽象服务。 +组合,而非继承。协调器是后端持有的具体类,不是后端继承的基类。本 RFC 的风险——「协调器不得让非常规后端与继承层级作斗争」——由此规避:后端只暴露钩子;它无法触及协调器的私有编排状态,且公开的 `SessionPersistence` 服务形状不变,因此第三方后端仍然可以完全不使用协调器、直接实现抽象服务。 ### 钩子接口(`PersistenceBackend`) -六个方法(五个必需 + 一个可选生命周期钩子)——协调器与存储之间唯一的 seam: +六个方法(五个必需 + 一个可选的生命周期钩子)——协调器与存储之间唯一的 seam: -- `name`:后端标签,用于 dispose 失败时的 `AggregateError`。 -- `loadStored(id)`:按 id 读取已存储的前缀,扫描**任何**存储范围(JSONL 的每个 cwd bucket;SQLite 的 id 全局唯一)。用于恢复/加载,以及通过 `!== undefined` 实现创建冲突探测。 -- `loadLive(id, cwd)`:读取**限定于 `cwd`** 的已存储前缀。**刻意区别于 `loadStored`**:HMR live-adoption 只能接管与活跃会话**相同 cwd** 下的持久化日志;同 id 但不同 cwd 的日志是冲突而非恢复。合并这两个方法会重新引入跨 cwd 接管 bug。SQLite 忽略 `cwd`。 -- `appendBatch(meta, events, isMaterialized)`:持久地追加一个连续批次,在尚未物化时**原子地**惰性物化会话(物化写入与第一个事件批次必须一起提交——崩溃发生在二者之间时不得留下一个已物化但为空的会话;这就是为什么没有单独的 `materialize` 钩子)。 -- `commitRepair(meta, tornMarker, closers)`:使崩溃修复持久化:截断撕裂尾部(当且仅当 `tornMarker !== undefined`)并追加 `closers`。**不要求原子性**——JSONL 合理地分两步 fsync(先截断再追加),SQLite 在一个事务中完成 DELETE+INSERT。用于 `load`(截断 + 合成 closers)和 live-adoption(仅截断,`closers = []`)。 -- `list()`:列出所有已存储的元数据。 -- `close?()`:可选的生命周期清理(SQLite 关闭 db 句柄;JSONL 省略),在 dispose effect 中于静默排空**之后**被 await,确保 close 失败不会掩盖排空错误。 +- `name`——后端标签,用于 dispose 失败时的 `AggregateError`。 +- `loadStored(id)`——按 id 读取已存储的前缀,扫描**任何**存储范围(JSONL 的每个 cwd bucket;SQLite 的 id 全局唯一)。用于恢复/加载,以及通过 `!== undefined` 进行创建碰撞探测。 +- `loadLive(id, cwd)`——读取**限定于 `cwd`** 的已存储前缀。**与 `loadStored` 有意区分**:HMR live-adoption 只能接管与存活会话处于**同一 cwd** 的持久化日志;同 id 但不同 cwd 的日志是碰撞而非恢复。合并二者会重新引入跨 cwd 接管 bug。SQLite 忽略 `cwd`。 +- `appendBatch(meta, events, isMaterialized)`——持久追加一个连续批次,在尚未物化时**原子地**惰性物化会话(物化写入与首批事件必须一起提交——崩溃不得留下一个已物化但为空的会话;这就是为什么没有单独的 `materialize` 钩子)。 +- `commitRepair(meta, tornMarker, closers)`——使崩溃修复持久化:截断损坏的尾部(当且仅当 `tornMarker !== undefined`)并追加 `closers`。**不要求原子性**——JSONL 合理地分两步 fsync(先截断再追加),SQLite 在一个事务中完成 DELETE+INSERT。用于 `load`(截断 + 合成 closers)和 live-adoption(仅截断,`closers = []`)。 +- `list()`——列出所有已存储的元数据。 +- `close?()`——可选的生命周期清理(SQLite 关闭 db 句柄;JSONL 省略),在 dispose effect 中于静默排空**之后**被 await,因此 close 失败不会掩盖排空错误。 -### 不透明的撕裂标记 +### 不透明的 torn marker -保持 seam 干净的唯一设计选择:崩溃修复中的「撕裂尾部在哪里」token 对协调器是**不透明的**。协调器计算合成 closers(它拥有来自 `dsh-session` 的 `interruptedTurnClosers`),但它只测试 `tornMarker !== undefined` 并将值原样传回 `commitRepair`——从不检视其内容。每个后端选择自己的标记类型:JSONL 使用要截断到的字节偏移量,SQLite 使用要从其开始删除的 seq(两者碰巧都是 `number`)。JSONL 后端将其 `committedBytes < buffer.byteLength` 比较**折叠在钩子内部**,因此返回的标记已经是 `number | undefined`;如果不做这个折叠,协调器就必须了解字节长度。 +保持 seam 整洁的唯一设计选择:崩溃修复中「损坏尾部在哪里」的 token 对协调器是**不透明的**。协调器计算合成 closers(它拥有来自 `dsh-session` 的 `interruptedTurnClosers`),但它只测试 `tornMarker !== undefined` 并将值原样传回 `commitRepair`——从不检视其内容。每个后端选择自己的 marker 类型:JSONL 使用要截断到的字节偏移,SQLite 使用要从其开始删除的 seq(两者恰好都是 `number`)。JSONL 后端将其 `committedBytes < buffer.byteLength` 比较折叠**在钩子内部**,因此返回的 marker 已经是 `number | undefined`;如果不做这层折叠,协调器就必须了解字节长度。 ## 测试 -共享的 `runPersistenceContract`(公开 API 契约)继续为每个后端运行。新增的 `runCoordinatorContract`(`tests/coordinator-contract.ts`)覆盖写入路径编排——接管、HMR、冲突、dispose 排空、崩溃尾部修复——通过 `CoordinatorFixture`(内存参考实现 + jsonl + sqlite)为每个后端运行一次。各后端自身的测试缩减为仅覆盖存储机制(JSONL:路径安全、fsync 回滚、bucket 列举;SQLite:schema 版本、`scanRows`、事务回滚)。每个真实后端有一个 through-coordinator 的 torn-tail→load→`commitRepair` 测试(通过 `corruptTail` fixture 钩子),确保协调器的撕裂标记修复分支在 100% per-file 门禁下被覆盖——契约崩溃测试只产生合成 closers 而不产生撕裂标记,因此无法触达该分支。 +共享的 `runPersistenceContract`(公开 API 契约)继续为每个后端运行。新增的 `runCoordinatorContract`(`tests/coordinator-contract.ts`)覆盖写入路径编排——接管、HMR、碰撞、dispose 排空、崩溃尾部修复——通过 `CoordinatorFixture`(内存参考实现 + jsonl + sqlite)为每个后端运行一次。各后端自身的测试规格缩减为仅覆盖存储机制(JSONL:路径安全、fsync 回滚、bucket 列举;SQLite:schema 版本、`scanRows`、事务回滚)。每个真实后端有一个经由协调器的 torn-tail→load→`commitRepair` 测试(通过 `corruptTail` fixture(测试前置数据)钩子),确保协调器的 torn-marker 修复分支在 100% per-file 门禁下被覆盖——契约崩溃测试只产生合成 closers 而不产生 torn marker,因此无法触达该分支。 ## 曾考虑的替代方案 -- **后端继承的基类**:否决,改用组合。后端只暴露钩子,无法触及协调器的私有编排状态,且第三方后端仍然可以完全不使用协调器、直接实现抽象服务。 -- **更宽的钩子面**:每个候选钩子都被折叠掉了:没有单独的 `materialize` 钩子(物化写入必须在 `appendBatch` 内与第一个事件批次原子提交);没有单独的创建冲突探测(它就是 `loadStored(id) !== undefined`);`list()` 也不经过协调器透传(列举不需要任何编排)。 +- **后端继承的基类**——否决,改用组合:后端只暴露钩子,无法触及协调器的私有编排状态,且第三方后端仍可完全不使用协调器、直接实现抽象服务。 +- **更宽的钩子面**——每个候选钩子都被折叠掉:没有单独的 `materialize` 钩子(物化写入必须在 `appendBatch` 内与首批事件原子提交);没有单独的创建碰撞探测(即 `loadStored(id) !== undefined`);`list()` 也不经由协调器透传(列举不需要任何编排)。 ## 后果 -协调器增加了一层间接和一个不透明的撕裂标记,但将此前每个后端重复的正确性密集编排集中到一处。其钩子面保持窄小:冲突检查复用 `loadStored`,物化保持在 `appendBatch` 内原子完成,列举绕过协调器。新后端只需实现存储原语,无需复制事件-缓冲区-flush 生命周期。 +协调器增加了一层间接和一个不透明的 torn marker,但将此前每个后端重复的、对正确性要求很高的编排逻辑集中到一处。其钩子面保持窄小:碰撞检查复用 `loadStored`,物化保持在 `appendBatch` 内原子完成,列举绕过协调器。新后端只需实现存储原语,而无需复制事件-缓冲区-flush 生命周期。 diff --git a/docs/rfc/implemented/architecture/2026-06-20-branded-ids.i18n.yaml b/docs/rfc/implemented/architecture/2026-06-20-branded-ids.i18n.yaml index 52a3ca3b01..9fc5b9299f 100644 --- a/docs/rfc/implemented/architecture/2026-06-20-branded-ids.i18n.yaml +++ b/docs/rfc/implemented/architecture/2026-06-20-branded-ids.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-20-branded-ids.md: f6d066857d8904ae5343f12310266663806a0ae2 -2026-06-20-branded-ids.zh.md: 14f82c395cdf43e2f5df5b2317dda3d45c595a64 +2026-06-20-branded-ids.zh.md: 80c158e598f3007416d31a89a6704a759798e44e diff --git a/docs/rfc/implemented/architecture/2026-06-20-branded-ids.zh.md b/docs/rfc/implemented/architecture/2026-06-20-branded-ids.zh.md index 14f82c395c..80c158e598 100644 --- a/docs/rfc/implemented/architecture/2026-06-20-branded-ids.zh.md +++ b/docs/rfc/implemented/architecture/2026-06-20-branded-ids.zh.md @@ -1,30 +1,30 @@ -# RFC:在所有应当使用品牌类型的位置推行 Branded ID - -Status: implemented +# RFC:在所有应有之处使用 branded ID [English](2026-06-20-branded-ids.md) | 中文 +Status: implemented + ## 问题 -harness 已经为三个标识符打上了品牌类型:`CallId`(`packages/llm/llm/src/brand.ts`)、`SessionId`(`packages/core/session/src/types.ts`)和 `AgentId`(`packages/core/agent/src/types.ts`),使用 `Branded = string & { readonly [BRAND]: B }` 机制(由纯类型包 `@deepseek-ai/dsh-brand` 拥有,位于 `packages/util/brand/`,见其 [README](../../../../packages/util/brand/README.md)),并为每个类型提供零成本的 cast 工厂函数。`dsh-brand` 还声明了治理策略:*"品牌类型用于跨包边界且可能被混淆的 id;并非每个 string 都需要品牌类型。"* 这条策略是正确的;问题在于它只落实了一半。两个缺口使得「结构相同但语义不同」的 string 今天仍能通过类型检查。 +harness 已经为三个标识符做了 brand 处理:`CallId`(`packages/llm/llm/src/brand.ts`)、`SessionId`(`packages/core/session/src/types.ts`)和 `AgentId`(`packages/core/agent/src/types.ts`),使用 `Branded = string & { readonly [BRAND]: B }` 机制(由纯类型包(package) `@deepseek-ai/dsh-brand` 拥有,位于 `packages/util/brand/`,见其 [README](../../../../packages/util/brand/README.md)),并为每个类型提供零开销的 cast 工厂。`dsh-brand` 还声明了治理策略:*"Branding 用于跨包边界且可能被混淆的 id;不是每个 string 都需要 brand。"* 这条策略是正确的;问题在于它只落实了一半。两处缺口使得结构相同但语义错误的 string 今天仍能通过类型检查器。 -**缺口 1:bash seam 中未打品牌的 ID。** `BashTask.id` 以及所有 executor/tool 边界使用裸 `string`,尽管生成的值与默认 session id 具有相同的 `name-N` 形状。模型也通过 `task_id` 返回该值,因此混淆 task id 和 session id 既是类型正确的,也是可达的。 +**缺口 1:bash seam 中未 brand 的 ID。** `BashTask.id` 以及所有执行器/工具边界使用裸 `string`,尽管生成的值与默认 session id 具有相同的 `name-N` 形状。模型还通过 `task_id` 返回该值,因此混淆 task id 和 session id 既类型正确又可达。 -bash **owner token** 是相关的子情形:`BashExecRequest.owner?: string` 和 `BashExecSpec.owner: string | undefined`(`packages/bash/bash/src/types.ts`)被文档描述为刻意*不透明*的隔离键,但在所有实际调用方中,该值就是拥有者 agent 的 `session.header.id`(`callerToken = (exec) => exec.agent?.session.header.id`,见 `packages/bash/tool-bash/src/index.ts`)——即一个穿着 `string` 外衣的 `SessionId`。它被用于访问控制比较(`owner !== callerToken(exec)`),因此一个「不匹配但类型正确」的 string 在此处就是一个跨会话隔离 bug,而当前类型系统无法捕获。这正是 [unify-the-agent-id-and-the-session-id](../../proposed/simplification/2026-06-20-unify-agent-and-session-id.md) 提案所称的「bash owner-token 别名漏洞」。 +bash **owner token** 是相关的子情形:`BashExecRequest.owner?: string` 和 `BashExecSpec.owner: string | undefined`(`packages/bash/bash/src/types.ts`)被文档描述为刻意*不透明*的隔离键,但在所有实际调用方中,该值就是所属 agent(智能体)的 `session.header.id`(`callerToken = (exec) => exec.agent?.session.header.id`,位于 `packages/bash/tool-bash/src/index.ts`),即一个穿着 `string` 外衣的 `SessionId`。它被用于访问控制比较(`owner !== callerToken(exec)`),因此一个不匹配但类型正确的 string 在此处就是一个跨会话隔离 bug,而当前类型系统无法捕获。这正是 [unify-the-agent-id-and-the-session-id](../../proposed/simplification/2026-06-20-unify-agent-and-session-id.md) 提案所称的"bash owner-token alias hole"。 -**缺口 2:既有品牌类型的侵蚀。** `CallId`、`SessionId` 和 `AgentId` 在注册表 map、公开查找参数、ACP 会话追踪和持久化协调器中退化为裸 string。在查找边界丢弃品牌类型,等于废掉了它的核心保护。 +**缺口 2:既有 brand 的侵蚀。** `CallId`、`SessionId` 和 `AgentId` 在注册表 map、公开查找参数、ACP 会话跟踪和持久化协调器中退化为裸 string。在查找边界丢弃 brand 会使其主要保护失效。 ## 决策 -纯类型变更。品牌类型是零成本 cast;运行时行为、序列化、比较和协议格式(wire format)均不变。工作分三部分,全部遵守既有的「并非每个 string 都需要」策略。 +纯类型变更。Brand 是零开销 cast;运行时行为、序列化、比较和协议格式(wire format)均不变。工作分三部分,全部遵循既有的"不是每个 string 都需要"策略。 -- **为 bash task id 打品牌。** 在 `packages/bash/bash/src/types.ts`(*拥有*该 id 的包)中添加 `BashTaskId = Branded<'BashTaskId'>` 及其同名工厂函数,从 `@deepseek-ai/dsh-brand` 导入 `Branded`,方式与 `SessionId`/`AgentId` 完全一致。品牌原语放在无依赖的 `dsh-brand` 工具包中,正是为了让 `dsh-bash` 只依赖它就能为自己的 id 打品牌——永远不需要为了获取 `Branded` 而引入 `dsh-llm`(或 `dsh-session`)。将品牌贯穿 `BashTask.id`、`BashExecutor` seam 方法(`get`/`ownerOf`/`readOutput`/`kill`)、`dsh-bash-local` 中的生成点(在创建时一次性为计数器输出打品牌),以及 `dsh-tool-bash` 的校验/访问控制面(`validateTaskId` 返回 `BashTaskId`;`task_id` 在模型 string 到达的 tool 边界处打品牌)。 +- **为 bash task id 加 brand。** 在 `packages/bash/bash/src/types.ts`(拥有该 id 的包)中添加 `BashTaskId = Branded<'BashTaskId'>` 及其同名工厂,从 `@deepseek-ai/dsh-brand` 导入 `Branded`,方式与 `SessionId`/`AgentId` 完全一致。brand 原语位于无依赖的 `dsh-brand` 工具包中,正是为了让 `dsh-bash` 仅依赖它就能为自己的 id 加 brand,而无需引入 `dsh-llm`(或 `dsh-session`)来获取 `Branded`。将其贯穿 `BashTask.id`、`BashExecutor` seam 方法(`get`/`ownerOf`/`readOutput`/`kill`)、`dsh-bash-local` 中的生成点(在创建时对计数器输出做一次 brand),以及 `dsh-tool-bash` 的校验/访问面(`validateTaskId` 返回 `BashTaskId`;`task_id` 在模型 string 到达的工具边界处被 brand)。 -- **铸造独立的 `OwnerToken` 品牌。** 在 `packages/bash/bash/src/types.ts` 中添加 `OwnerToken = Branded<'OwnerToken'>`;将 `BashExecRequest.owner` / `BashExecSpec.owner` / `BashExecutor.ownerOf` 的类型标注为 `OwnerToken | undefined`。`dsh-tool-bash` 消费方在边界处将 agent 的 `session.header.id`(一个 `SessionId`)cast 为 `OwnerToken`——这是两套词汇交汇的唯一位置。bash seam 永远不导入 `dsh-session`。(理由见下一节。) +- **铸造独立的 `OwnerToken` brand。** 在 `packages/bash/bash/src/types.ts` 中添加 `OwnerToken = Branded<'OwnerToken'>`;将 `BashExecRequest.owner` / `BashExecSpec.owner` / `BashExecutor.ownerOf` 的类型标注为 `OwnerToken | undefined`。`dsh-tool-bash` 消费方在边界处将 agent 的 `session.header.id`(一个 `SessionId`)cast 为 `OwnerToken`——这是两套词汇唯一交汇的地方。bash seam 从不导入 `dsh-session`。(理由见下一节。) -- **阻止品牌侵蚀。** 将既有品牌传播到缺口 2 列出的 `Map` 键类型和公开方法参数:`Map`、`get(id: SessionId)`、`Map`、`Map`、ACP 的 `SessionRecord.sessionId: SessionId` 接口、协调器的 `Map`。这是 diff 中机械性最大的部分,也是让*既有*品牌在查找处真正发挥作用(而非仅在结构体字段上标注)的关键。 +- **阻止 brand 侵蚀。** 将既有 brand 传播到缺口 2 列出的 `Map` 键类型和公开方法参数中:`Map`、`get(id: SessionId)`、`Map`、`Map`、ACP 的 `SessionRecord.sessionId: SessionId` 接口、协调器的 `Map`。这是 diff 中机械量最大的部分,也是让*既有* brand 在查找处真正发挥作用(而不仅仅标注在结构体字段上)的关键。 -示意形状(工厂模式与现有三个品牌完全一致): +示意形状(工厂模式与已有的三个 brand 完全一致): ```ts ignore-check import type { Branded } from '@deepseek-ai/dsh-brand' @@ -46,24 +46,24 @@ export function OwnerToken(id: string): OwnerToken { ### 为什么不把 `owner` 类型标注为 `SessionId`? -executor 将 ownership 视为不透明的,不应依赖 session 模型。独立的 `OwnerToken` 保持了这一边界,同时防止裸 string 或 task id 被当作 owner 传入。`dsh-tool-bash` 拥有访问策略,由它执行从 `SessionId` 到 `OwnerToken` 的唯一转换。 +执行器将 ownership 视为不透明的,不应依赖 session 模型。独立的 `OwnerToken` 保留了这一边界,同时防止裸 string 或 task id 被当作 owner 传入。`dsh-tool-bash` 拥有访问策略,由它执行从 `SessionId` 到 `OwnerToken` 的唯一转换。 ## 不在范围内 / 可能的扩展 -遵循「并非每个 string 都需要品牌类型」策略,刻意保持窄范围。以下每项都是合理的未来品牌候选,附有推迟理由而非承诺: +遵循"不是每个 string 都需要 brand"的策略,刻意保持窄范围。以下每项都是合理的未来 brand 候选,附带推迟理由而非承诺: -- **`ModelId`**(`GenerateOptions.model`,`LlmService` 适配器注册表键)——一个真正的跨包查找键(config → agent → llm → adapter);合理的下一个品牌,仅为控制本 RFC 的影响范围而暂不纳入。 -- **`ToolName`**(`ToolRegistry` 键)——由作者定义、人类可读,且很少与其他 id 混淆;候选强度最弱,可能不值得打品牌。 -- **`ErrorCode`**(`HarnessError.code`)——封闭词汇(`ABORTED`、`NO_ADAPTER`……),不是逐实例的 id;如果要加强类型,用 string 字面量联合类型比品牌更合适。 -- **数值序号**——轮次号、步骤号和事件 `seq` 是 `number` 而非 `string`,`Branded` 不适用;可以用并行的 `number & { readonly [BRAND]: B }` 变体为它们打品牌,但它们是位置序号、很少跨边界传递,收益低。 -- **带校验的构造**——品牌工厂是纯 cast,无运行时检查,且每个边界(ACP `sessionId`、提供方发放的 `call.id`、`dsh-llm-deepseek` 中的空字符串回退)今天都信任裸 string。一个在边界对畸形输入抛异常的 `SessionId.parse()` / `isValid()` 伴生函数确实是缺口,但它是一项*运行时行为*变更,有自己的设计问题(什么算「畸形」?失败时怎么办?),应在独立 RFC 中处理,不应捆绑进这次纯类型改动。 +- **`ModelId`**(`GenerateOptions.model`,`LlmService` 适配器注册表的键):一个真正的跨包查找键(config → agent → llm → adapter);合理的下一个 brand,仅为控制本 RFC 的影响范围而暂不纳入。 +- **`ToolName`**(`ToolRegistry` 的键):由作者定义、人类可读,且很少与其他 id 混淆;最弱的候选,可能不值得加 brand。 +- **`ErrorCode`**(`HarnessError.code`):一个封闭词汇(`ABORTED`、`NO_ADAPTER`……),不是逐实例的 id;如果要做,string 字面量联合类型比 brand 更合适。 +- **数值序号**:轮次号、步骤号和事件 `seq` 是 `number` 而非 `string`,`Branded` 不适用;可以用并行的 `number & { readonly [BRAND]: B }` 变体来 brand 它们,但它们是位置序号、很少跨边界传递,收益较低。 +- **带校验的构造**:brand 工厂是纯 cast,无运行时检查,且每个边界(ACP `sessionId`、提供方签发的 `call.id`、`dsh-llm-deepseek` 中的空字符串回退)今天都信任裸 string。一个在边界处对格式错误的输入抛异常的 `SessionId.parse()` / `isValid()` 配套工具确实是缺口,但它是*运行时行为*变更,有自己的设计问题(什么算"格式错误"?失败时怎么办?),应在独立 RFC 中处理,不应捆绑进这次纯类型变更。 ## 验证 -`BashTaskId` 和 `OwnerToken` 定义在 `dsh-bash` 中,贯穿 executor、本地实现和面向模型的 tool,且未引入 `dsh-session` 依赖。集合、公开参数和导出签名对 `CallId`、`SessionId`、`AgentId` 或 `BashTaskId` 使用对应的品牌类型而非裸 `string`;来自提供方、ACP 和模型的原始输入通过品牌工厂进入,而非散落的 cast。 +`BashTaskId` 和 `OwnerToken` 定义在 `dsh-bash` 中,贯穿执行器、本地实现和面向模型的工具,且未添加 `dsh-session` 依赖。集合、公开参数和导出签名对 `CallId`、`SessionId`、`AgentId` 或 `BashTaskId` 使用相应的 brand 而非裸 `string`;来自提供方、ACP 和模型的原始输入通过 brand 工厂进入,而非散落的 cast。 ## 后果 -- **两个面上的机械性改动。** 传播品牌类型涉及 bash seam(接口 + 实现 + 消费方)以及 ACP session-id 接口和持久化协调器。改动面广但严重度低:遗漏的位置是编译错误,而非静默 bug。变更可观测地是纯类型的——无快照或 e2e 行为差异。它与 [unify-the-agent-id-and-the-session-id](../../proposed/simplification/2026-06-20-unify-agent-and-session-id.md) 提案相邻(两者都触及 session-id / owner-token 边界);即使该提案落地,`OwnerToken` 出于上述解耦理由仍与统一后的 id 保持独立。 -- **品牌类型不做校验。** 品牌类型是混淆防护,不是正确性证明:一个*错误的* session id 只要仍是格式良好的 string,就和以前一样能通过类型检查。本 RFC 不关闭这个缺口(见「不在范围内」)——它只阻止传入错误*类别*的 id 这一类错误。 -- **「在哪里停下」仍是判断题。** 为 `BashTaskId` 打品牌而不为 `ToolName`,为 `OwnerToken` 打品牌而不为 `ModelId`,是对哪些 string「可能被混淆」的品味判断。合理的评审者可能想要更多或更少;`brand.ts` 中的策略是裁决依据,本 RFC 倾向于面向模型或用于访问控制的 id。 +- **两个接口面的机械性改动。** 传播 brand 涉及 bash seam(接口 + 实现 + 消费方)以及 ACP session-id 接口和持久化协调器。改动面广但严重度低:遗漏的位置是编译错误而非静默 bug。变更可观察地为纯类型变更——无快照或 e2e 行为差异。它与 [unify-the-agent-id-and-the-session-id](../../proposed/simplification/2026-06-20-unify-agent-and-session-id.md) 提案相邻(两者都触及 session-id / owner-token 边界);如果该提案落地,`OwnerToken` 出于上述解耦理由仍与统一后的 id 保持独立。 +- **Brand 不做校验。** Brand 是混淆防护,不是正确性证明:一个*错误的* session id 只要仍是合法的 string,就和以前一样能通过类型检查器。本 RFC 不关闭这个缺口(见"不在范围内")——它只阻止传入错误*类别*的 id 这种错误。 +- **"在哪里停下"仍是判断题。** 为 `BashTaskId` 加 brand 但不为 `ToolName` 加,为 `OwnerToken` 加但不为 `ModelId` 加,是对哪些 string"可能被混淆"的品味判断。合理的评审者可能想要更多或更少;`brand.ts` 中的策略是裁决依据,本 RFC 倾向于面向模型或用于访问控制的 id。 diff --git a/docs/rfc/implemented/architecture/2026-06-20-extract-example-app-packages.i18n.yaml b/docs/rfc/implemented/architecture/2026-06-20-extract-example-app-packages.i18n.yaml index 992793d609..d10d531a66 100644 --- a/docs/rfc/implemented/architecture/2026-06-20-extract-example-app-packages.i18n.yaml +++ b/docs/rfc/implemented/architecture/2026-06-20-extract-example-app-packages.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-20-extract-example-app-packages.md: 3a0a0f5d4b329afed72bd3c00bf880989e24fe54 -2026-06-20-extract-example-app-packages.zh.md: 9de9f79369ebad387778a0418b75dfde96b285a7 +2026-06-20-extract-example-app-packages.zh.md: 8945f0c97727479c9e847711a96fc5d18118e09e diff --git a/docs/rfc/implemented/architecture/2026-06-20-extract-example-app-packages.zh.md b/docs/rfc/implemented/architecture/2026-06-20-extract-example-app-packages.zh.md index 9de9f79369..8945f0c977 100644 --- a/docs/rfc/implemented/architecture/2026-06-20-extract-example-app-packages.zh.md +++ b/docs/rfc/implemented/architecture/2026-06-20-extract-example-app-packages.zh.md @@ -1,57 +1,57 @@ -# RFC:将示例应用提取为 package - -Status: implemented +# RFC:将示例应用提取为独立包 [English](2026-06-20-extract-example-app-packages.md) | 中文 +Status: implemented + ## 问题 -示例目录本应是*薄*的:只包含演示的可变接线,而非演示的机制本身。在本次变更之前它是厚的。每个示例都携带一份手写的 `start.ts` 启动引导、一段基础设施前导(`timer`,以及 stdio 演示还需要的 `logger` + `hmr`)、三个共享 YAML 片段的嵌套引入(`base.yml` / `base-core.yml` / `acp-agent/acp-tail.yml`),以及每个示例各自的 `agent-loop`/持久化/系统提示词配置。真正的应用——每个 agent 都需要的服务主干——分散在叶子配置和那些 include 中。 +示例目录本应是*精简的*——只包含演示的可变接线,而非演示的基础设施。在此次变更之前,它是臃肿的。每个示例都携带一份手写的 `start.ts` 启动引导、一段基础设施前导(`timer`,以及 stdio 演示所需的 `logger` + `hmr`(热模块替换))、三个共享 YAML 片段的嵌套引用(`base.yml` / `base-core.yml` / `acp-agent/acp-tail.yml`),还有各示例自身的 `agent-loop`/persistence/system-prompt 配置。真正的应用——每个 agent(智能体)都需要的服务主干——散落在叶子配置和那些 include 中。 -叶子配置还拥有一个耦合的前门。ACP 要求 stdout 纯净,通过 `session/new` 创建 agent;stdio 需要控制台 logger 和一个预创建的 `main`。防止错误组合的唯一手段是行文中的警告,而三个 `start.ts` 文件重复了 Loader 引导和生命周期代码。 +叶子配置还拥有一个耦合的前门。ACP(Agent Client Protocol)要求 stdout 纯净,并通过 `session/new` 创建 agent;stdio 则需要一个控制台 logger 和一个预创建的 `main`。防止错误组合的唯一屏障是文档中的文字警告,而三个 `start.ts` 文件重复着 Loader 引导和生命周期代码。 ## 决策 -每个示例现在**基本上是对一个 app package 的调用**,沿着既有的[接口 / 实现 / 消费方 seam](2026-06-13-capability-seams.md) 拆分接线:**app 包拥有组合**,叶子 `cordis.yml` 只拥有**可替换的选择**(哪个 LLM 适配器、哪个 bash 执行器、模型、提示词、持久化根目录)。 +每个示例现在**主要是对一个应用包(package)的调用**,沿着既有的[接口 / 实现 / 消费方 seam](2026-06-13-capability-seams.md) 拆分接线:**应用包拥有组合**,叶子 `cordis.yml` 只拥有**可替换的选择**(哪个 LLM(大语言模型)适配器、哪个 bash 执行器、模型、提示词、持久化根目录)。 -- **`@deepseek-ai/dsh-agent-spine-demo`**([packages/examples/agent-spine-demo](../../../../packages/examples/agent-spine-demo))组合无提供方、无执行器、无 UI 的主干,并转发 loop 的 agent 列表配置。它对具体 loop 的依赖是有意为之,因为这个包组合的是主干而非扩展它;替换 loop 意味着提供另一个 bundle。 -- **`@deepseek-ai/dsh-stdio-demo`**([packages/examples/stdio-demo](../../../../packages/examples/stdio-demo))和 **`@deepseek-ai/dsh-acp-demo`**([packages/examples/acp-demo](../../../../packages/examples/acp-demo))各自内置了前门。Stdio 包含 `ui-stdio`、控制台 logger 和 `main`;ACP 包含 bridge 和 JSONL 持久化,但不含 stdout logger 或预创建的 agent。叶子可以追加插件,但安全的组合现在是默认产物。 -- **`start.ts` 已移除。** 每个 app 包暴露一个 `bin`(`dsh-stdio-demo` / `dsh-acp-demo`);`demo:*` 脚本调用它(如 `dsh-stdio-demo ./cordis.yml`)。Loader 引导尾部、`.env` 加载和 fail-loud 守卫位于共享的 [`@deepseek-ai/dsh-app-boot`](../../../../packages/ui/app-boot) 包(在逐文件覆盖率门禁下有单元测试——见[共享 app bin 的启动胶水](../simplification/2026-07-04-share-app-bin-boot-glue.md));每个 bin 是一个薄的自执行组合,基于这些辅助函数加上自身特有的生命周期逻辑(ACP bin:快照模式选择与 stdin-dispose)。`bin.ts` 文件本身仍排除在覆盖率之外(自执行 CLI 入口,与旧的 `start.ts` 类似),由 keyless Loader 路径测试驱动。 -- **每个叶子 `cordis.yml` 精简**为后端 + 配置:LLM 适配器(带 apiKey/models 的 `llm-deepseek`,或 `llm-replay`)、bash 执行器(`bash-local`)、stdio 演示的 `hmr`(见下方修正),以及一个 app 条目承载 app 的配置(模型、系统提示词、持久化根目录——作为 app 包自身的 `Config` 暴露,由 app 将每个值路由到其接线的目标位置:stdio 路由到预创建的 agent,acp 路由到 bridge 插件)。 -- **echo-agent 折叠到 `dsh-stdio-demo`**,将 LLM 后端替换为本地的 `mock-llm`,并在叶子层添加本地的 `echo-tool`(加上 `bash-local`,由主干的 `tool-bash` 注入)——这是「替换后端、保留应用」的干净示范。`mock-llm.ts` / `echo-tool.ts` 作为示例本地的教学插件保留。 -- **`base.yml`、`base-core.yml` 和 `acp-agent/acp-tail.yml` 退役**——它们共享的主干现在位于 `dsh-agent-spine-demo`。 +- **`@deepseek-ai/dsh-agent-spine-demo`**([packages/examples/agent-spine-demo](../../../../packages/examples/agent-spine-demo))组合了不含 provider、不含执行器、不含 UI 的主干,并转发 agent loop(智能体循环)的 agent 列表配置。它对具体 loop 的依赖是有意为之,因为该包组合的是主干而非扩展主干;替换 loop 意味着提供另一个 bundle。 +- **`@deepseek-ai/dsh-stdio-demo`**([packages/examples/stdio-demo](../../../../packages/examples/stdio-demo))和 **`@deepseek-ai/dsh-acp-demo`**([packages/examples/acp-demo](../../../../packages/examples/acp-demo))各自内置了前门。Stdio 包含 `ui-stdio`、控制台 logger 和 `main`;ACP 包含 bridge 和 JSONL 持久化,但不含 stdout logger 或预创建的 agent。叶子可以添加插件,但安全的组合现在是默认产物。 +- **`start.ts` 已移除。** 每个应用包暴露一个 `bin`(`dsh-stdio-demo` / `dsh-acp-demo`);`demo:*` 脚本调用它(例如 `dsh-stdio-demo ./cordis.yml`)。Loader 引导尾部、`.env` 加载和快速失败守卫位于共享的 [`@deepseek-ai/dsh-app-boot`](../../../../packages/ui/app-boot) 包(在逐文件覆盖率门禁下有单元测试——见[共享应用 bin 的启动胶水](../simplification/2026-07-04-share-app-bin-boot-glue.md));每个 bin 是一个精简的自执行组合,基于这些辅助函数加上其应用特有的生命周期逻辑(ACP bin:快照模式选择与 stdin-dispose)。`bin.ts` 文件本身仍被排除在覆盖率之外(自执行 CLI(命令行界面)入口,与旧的 `start.ts` 性质相同),由 keyless 的 Loader 路径测试驱动。 +- **每个叶子 `cordis.yml` 精简为**后端 + 配置:LLM 适配器(带 apiKey/models 的 `llm-deepseek`,或 `llm-replay`)、bash 执行器(`bash-local`)、stdio 演示的 `hmr`(见下方修正),以及一个承载应用配置的 app 条目(模型、系统提示词、持久化根目录——以应用包自身的 `Config` 形式暴露,由它将各值路由到应用接线的目标位置:stdio 路由到预创建的 agent,acp 路由到 bridge 插件)。 +- **echo-agent 折叠到 `dsh-stdio-demo` 上**,将 LLM 后端替换为本地的 `mock-llm`,并在叶子层添加本地的 `echo-tool`(加上 `bash-local`,由主干的 `tool-bash` 注入)——这是「替换后端、保留应用」的干净示范。`mock-llm.ts` / `echo-tool.ts` 作为示例本地的教学插件保留。 +- **`base.yml`、`base-core.yml` 和 `acp-agent/acp-tail.yml` 已退役**——它们共享的主干现在位于 `dsh-agent-spine-demo` 中。 -`bash-local` 和 LLM 适配器保持为**叶子选择**:bundle 提供 `tool-bash`(消费方 schema),叶子选择执行器实现,因此沙箱执行器或回放适配器可以在不触碰 app 的情况下替换进来。 +`bash-local` 和 LLM 适配器仍然是**叶子选择**:bundle 提供 `tool-bash`(消费方 schema),叶子选择执行器实现,因此沙箱执行器或回放适配器无需触碰应用即可替换。 ### 实现修正:`hmr` 保留为叶子条目 -提案将 `hmr` 列入 stdio app 内置的前门集群。对照代码验证后发现,将 `hmr` 内置到 `dsh-stdio-demo` 包在两方面与 Cordis 冲突,因此改为作为**叶子 `cordis.yml` 条目**交付: +提案最初将 `hmr` 列入 stdio 应用内置的前门集群。对照代码验证后发现,将 `hmr` 内置到 `dsh-stdio-demo` 包中会在两个方面与 Cordis 冲突,因此改为作为**叶子 `cordis.yml` 条目**交付: -1. `@cordisjs/plugin-hmr` 是一个仅限 Loader、仅限子进程的开发插件——其构造函数在没有 `node --expose-internals` 和活跃 `loader` 服务的情况下会抛出异常,因此只能在真实的 `demo:*`/bin 子进程中运行,无法在进程内的单元/覆盖率测试层运行。 +1. `@cordisjs/plugin-hmr` 是一个仅限 Loader、仅限子进程的开发插件——其构造函数在没有 `node --expose-internals` 和活跃的 `loader` 服务时会抛出异常,因此只能在真实的 `demo:*`/bin 子进程中运行,不能在进程内的单元/覆盖率测试层运行。 2. 进程内测试层(vitest)甚至无法*导入* vendor 的 `hmr` 模块(其 class-decorator `@Inject` 形式在 Vite 的 transform 下会失败),因此一个 `apply` 静态导入了它的包永远无法满足其主函数的逐文件 100% 覆盖率门禁。 -关键在于,`hmr` **不是**像控制台 logger 那样的 stdout 纯净隐患——在 ACP 配置中误加 `hmr` 不会破坏 JSON-RPC 帧——因此将它留在叶子不会损失耦合论证所关注的安全性。**logger**(真正的耦合)保持内置:stdio app 包含它,ACP app 省略它。 +关键在于,`hmr` **不是**像控制台 logger 那样的 stdout 纯净隐患:ACP 配置中误加 `hmr` 不会破坏 JSON-RPC 帧,因此将它留在叶子层不会损失耦合论证所关注的安全性。**logger**(真正的耦合点)保持内置:stdio 应用包含它,ACP 应用省略它。 ## 曾考虑的替代方案 -### 为什么不继续用共享 YAML include 来接线? +### 为什么不继续用共享 YAML include 来管理接线? -旧的 `base*.yml`/`acp-tail.yml` include 已经去重了*配置*,但 YAML include 无法**封装**前门耦合——它只能在注释中描述,并信任每个叶子遵守。它也无法拥有 `bin`,因此启动胶水只能在三个 `start.ts` 文件中复制。包将「ACP app 绝不向 stdout 输出日志」从行文警告变成产物的属性:叶子中没有可以写错的 logger 条目。 +旧的 `base*.yml`/`acp-tail.yml` include 已经去重了*配置*,但 YAML include 无法**封装**前门耦合——它只能在注释中描述,并信任每个叶子遵守。它也无法拥有 `bin`,因此启动胶水一直在三个 `start.ts` 文件中重复。包将「ACP 应用绝不向 stdout 输出日志」从文字警告变成了产物的属性:叶子中不存在可以写错的 logger 条目。 ## 验证 - 示例目录只包含配置、README 和测试:`start.ts`、基础设施前导和共享 YAML include 已移除。 -- `demo:echo`、`demo:repl` 和 `demo:acp` 调用 app 包的 bin。 -- 每个新包有 README 和逐文件 100% 覆盖率;每个 app 包还有一个 keyless 的真实 Loader 路径 bin 冒烟测试,用于捕获 [postmortem 0001](../../../postmortem/0001-acp-default-export-drops-inject.md) 中描述的导出形状失败。 -- ACP 回放 transcript 保持不变,因为插件集和加载顺序未改变。 +- `demo:echo`、`demo:repl` 和 `demo:acp` 调用应用包的 bin。 +- 每个新包都有 README 和逐文件 100% 覆盖率;每个应用包还有一个 keyless 的真实 Loader 路径 bin 冒烟测试,用于捕获[事后分析 0001](../../../postmortem/0001-acp-default-export-drops-inject.md) 中描述的导出形状故障。 +- ACP 回放 transcript(文本记录)保持不变,因为插件集合和加载顺序未改变。 ## 后果 -- **裸插件树教学法。** echo-agent 的内联 `cordis.yml` 曾一次展示所有插件;主干现在藏在 bundle 后面,因此查看完整树意味着打开 `dsh-agent-spine-demo`。app 包的 README 承担了这部分教学职责。 -- **多了一层间接。** 「这个演示加载了什么?」变成了读一个 package,而非扫一份 YAML。 +- **裸插件树的教学性。** echo-agent 内联的 `cordis.yml` 曾一次展示所有插件;主干现在隐藏在 bundle 之后,查看完整树意味着打开 `dsh-agent-spine-demo`。应用包的 README 承担了这份教学职责。 +- **多了一层间接。**「这个演示加载了什么?」从扫描单个 YAML 变成了阅读一个包。 ## 相关 -- 取代 [Make the shared example base providerless](../../rejected/architecture/2026-06-20-providerless-example-base.md):一旦主干移入 `dsh-agent-spine-demo` 且 `base*.yml` 文件被删除,将 `base.yml` 重命名为无提供方核心便不再有意义。 -- 建立在[能力 seam](2026-06-13-capability-seams.md) 的接口/实现/消费方拆分之上——后端和展示层保持为叶子选择;主干是共享 bundle。 -- 与 [Reorganize packages into a modular hierarchy](2026-06-20-package-hierarchy.md) 互补:新的 app/core 包按该层级结构归入既有分组(`core` 放可复用的主干 bundle,`ui` 放 app 特有的前门)。 +- 取代 [Make the shared example base providerless](../../rejected/architecture/2026-06-20-providerless-example-base.md):一旦主干移入 `dsh-agent-spine-demo` 且 `base*.yml` 文件被删除,将 `base.yml` 重命名为无 provider 核心便不再有意义。 +- 基于 [capability-seams](2026-06-13-capability-seams.md) 的接口/实现/消费方拆分——后端和展示层保持为叶子选择;主干是共享 bundle。 +- 与 [Reorganize packages into a modular hierarchy](2026-06-20-package-hierarchy.md) 互补:新的 app/core 包按该层级结构归入既有分组(`core` 放可复用的主干 bundle,`ui` 放应用特有的前门)。 diff --git a/docs/rfc/implemented/architecture/2026-06-20-package-hierarchy.i18n.yaml b/docs/rfc/implemented/architecture/2026-06-20-package-hierarchy.i18n.yaml index 9f89c454fc..08ab9fc7f0 100644 --- a/docs/rfc/implemented/architecture/2026-06-20-package-hierarchy.i18n.yaml +++ b/docs/rfc/implemented/architecture/2026-06-20-package-hierarchy.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-20-package-hierarchy.md: faf5815222b20699a32f1af489e625ba3e891230 -2026-06-20-package-hierarchy.zh.md: 4b71cd41826e727eea19d305635c6485e18393c2 +2026-06-20-package-hierarchy.zh.md: 118367f2655bffbd270f259df4ade37a70dcb2d7 diff --git a/docs/rfc/implemented/architecture/2026-06-20-package-hierarchy.zh.md b/docs/rfc/implemented/architecture/2026-06-20-package-hierarchy.zh.md index 4b71cd4182..118367f265 100644 --- a/docs/rfc/implemented/architecture/2026-06-20-package-hierarchy.zh.md +++ b/docs/rfc/implemented/architecture/2026-06-20-package-hierarchy.zh.md @@ -1,4 +1,4 @@ -# RFC:将包(package)重组为模块化层级结构 +# RFC:将包重组为模块化层级结构 [English](2026-06-20-package-hierarchy.md) | 中文 @@ -6,13 +6,13 @@ Status: implemented ## 问题 -`packages/` 原先是扁平的:18 个包全部位于 `packages//`,一个包的位置无法体现它是核心产品 API、可替换的能力 seam、提供方适配器、产品集成,还是示例/测试支撑。package README 带着 `FIXME(package-hierarchy)`,`scripts/publint-all.ts` 带着 `TODO(package-inventory)`,标记的正是这个问题。核心包、提供方集成、能力 seam、示例 UI 支撑和仅用于快照的回放支撑,看起来都同等基础。 +`packages/` 原先是扁平的:18 个包(package)全部位于 `packages//`,从路径上完全看不出一个包属于核心产品 API、可替换的能力 seam、提供方适配器、产品集成,还是示例/测试支撑。包的 README 带着 `FIXME(package-hierarchy)`,`scripts/publint-all.ts` 带着 `TODO(package-inventory)`,标记的正是这个问题。核心包、提供方集成、能力 seam、示例 UI 支撑和仅用于快照的回放支撑看起来同样基础。 -这不仅是外观问题。因为每个顶层包看起来都属于同一个公开接口面,未来移除更难;发布/lint/文档脚本不得不通过注释或手工维护的静态列表来编码意图,而非从布局直接读取。 +这不仅仅是外观问题。由于每个顶层包看起来都属于同一个公开接口,未来移除更加困难,而 publish/lint/doc 脚本不得不通过注释或手工维护的静态列表来编码意图,而不是从布局中直接读取。 ## 决策 -按模块角色分组,统一为 `packages///` 两层深度。分组目录是纯容器(没有 `package.json`);每个包保留其 `@deepseek-ai/dsh-` 名称——这是仓库结构与维护策略,不是包重命名。 +按模块角色将包分组,统一放在 `packages///` 深度。分组目录是纯容器(没有 `package.json`);每个包保留其 `@deepseek-ai/dsh-` 名称——这是仓库结构与维护策略的调整,不是包的重命名。 ```text packages/ @@ -44,32 +44,32 @@ packages/ ### 放置决策 -- **能力族使用同名嵌套。** 一个族的接口包位于 `packages///`(`llm/llm`、`bash/bash`、`session-persistence/session-persistence`),实现和消费方作为扁平兄弟。不设额外的 `adapters/`/`impls/` 子层——每个包恰好在深度 2,workspace glob 保持简洁的 `packages/*/*`,一条 `@deepseek-ai/dsh-*` tsconfig 通配符即可解析所有包(目录名唯一,使 first-on-disk-wins 无歧义)。 -- **`session` 留在 `core/`;持久化自成一族。** 会话日志是核心产品 API。其存储后端构成一个平行的能力族(`session-persistence/`),与 `llm/` 和 `bash/` 对称,而非嵌套在 `core/session/` 下。 -- **`agent-loop` 在 `core/` 中。** 它是 `agent` seam 唯一的具体实现,但作为 harness 的默认产品循环随产品发布,因此与核心主干同住。插件仍然依赖 `agent` 的词汇,从不依赖 `agent-loop`,因此循环仍可替换。 -- **`invariants` 和 `ui-stdio` 属于 `support/`,不是产品。** `invariants` 是开发模式的契约检查。`ui-stdio` 从示例中提取以便复用和满足覆盖率门禁——它与示例耦合,因此与 `llm-replay`(快照测试回放适配器)一起放在 `support/` 中。`acp` 是 `ui/` 的唯一成员,因为它是真正的产品接口面(编辑器驱动的 ACP 桥接),在结构上不同于 readline 演示辅助工具。 +- **能力族使用同名嵌套。** 一个族的接口包位于 `packages///`(`llm/llm`、`bash/bash`、`session-persistence/session-persistence`),实现和消费方作为扁平兄弟并列。不设额外的 `adapters/`/`impls/` 子层——每个包恰好在深度 2,这使 workspace glob 保持简洁的 `packages/*/*`,并让一条 `@deepseek-ai/dsh-*` tsconfig 通配符即可解析所有包(唯一的目录名使 first-on-disk-wins 无歧义)。 +- **`session` 留在 `core/`;持久化独立成族。** 会话日志是核心产品 API。其存储后端构成一个平行的能力族(`session-persistence/`),与 `llm/` 和 `bash/` 对称,而非嵌套在 `core/session/` 下。 +- **`agent-loop` 在 `core/` 中。** 它是 `agent` seam 唯一的具体实现,但作为 harness 的默认产品循环交付,因此与核心主干同处。插件仍然依赖 `agent` 的词汇,从不依赖 `agent-loop`,所以循环仍可替换。 +- **`invariants` 和 `ui-stdio` 属于 `support/`,不是产品。** `invariants` 是开发模式的契约检查。`ui-stdio` 从示例中提取出来以便复用和满足覆盖率门禁——它与示例耦合,因此与 `llm-replay`(快照测试的回放适配器)一起放在 `support/` 中。`acp` 是 `ui/` 的唯一成员,因为它是真正的产品接口(编辑器驱动的 ACP 桥接),与 readline 演示辅助工具在结构上截然不同。 -### 去重包清单 +### 去重包列表 -包清单此前在五处重复枚举。统一的深度 2 布局使大部分可以被推导出来: +包列表此前在五个地方重复枚举。统一的深度 2 布局使大部分可以被推导: -- `tsconfig.base.json` 通过一条 `@deepseek-ai/dsh-*` `paths` 通配符(每个分组列一个候选路径)映射所有包,取代逐包条目。根 `tsconfig.json` 复用该源码映射,并携带显式的 project references 以保持 package/vendor 类型检查边界完整。(这里引入了一个细节:路径候选包含 `/*/`,朴素的正则注释剥离器会误认为块注释——`scripts/doc-typecheck.ts` 正是因此通过 TypeScript 解析器读取 JSONC 配置,而非手工剥离注释。) -- `scripts/publint-all.ts` 通过读取层级结构(`packages//`)推导出列表,解决了 `TODO(package-inventory)`。 -- `tsconfig.build.json` 的 project `references` 仍为显式列表——TypeScript project references 没有通配符形式。从 manifest 生成这些引用留作后续工作(见 [discover package inventories](../../proposed/process/2026-06-20-discover-package-inventory.md))。 +- `tsconfig.base.json` 通过一条 `@deepseek-ai/dsh-*` `paths` 通配符(每个分组列一个候选)映射所有包,取代了逐包条目。根 `tsconfig.json` 复用该源映射,并携带显式 project references 以保持 package/vendor 类型检查边界完整。(这里引入了一个细节:路径候选中包含 `/*/`,朴素的正则注释剥离器会将其误认为块注释——`scripts/doc-typecheck.ts` 正是因此通过 TypeScript 解析器读取 JSONC 配置,而非手动剥离注释。) +- `scripts/publint-all.ts` 通过读取层级结构(`packages//`)推导列表,解决了 `TODO(package-inventory)`。 +- `tsconfig.build.json` 的 project `references` 仍为显式列表——TypeScript project references 没有通配符形式。从 manifest(元数据清单)生成这些引用留作后续工作(见 [discover package inventories](../../proposed/process/2026-06-20-discover-package-inventory.md))。 ### 新增的护栏 -两道 doc-sync/hygiene 门禁保证结构及其引用的正确性,使本次重组所需的人工检查不必再次手动重复: +两道 doc-sync/hygiene 门禁确保结构及其引用保持正确,使本次重组所需的手动检查无需日后重复: -- `scripts/verify-package-paths.ts` 标记 Markdown 或 `.ts` 注释/字符串中的 `packages/` 引用:如果该路径无法解析**且**某段命名了一个真实存在的包,则视为指向已移动包的陈旧路径。如果路径命名的包在任何地方都不存在(前瞻性提案),则不报错;因此该门禁对 proposed/implemented/rejected 统一适用。 -- `scripts/check-workspace-constraints.ts` 断言 `packages//` 形状:分组目录不含 `package.json`,没有包扁平地位于根层级或嵌套更深。分组名称保持开放——新增分组无需修改门禁;只有深度 2 的形状是固定的。 +- `scripts/verify-package-paths.ts` 标记 Markdown 或 `.ts` 注释/字符串中的 `packages/` 引用,如果该引用无法解析**且**某个路径段命名了一个真实存在的包,即指向已移动包的陈旧路径。如果路径命名的包在任何地方都不存在(前瞻性提案),则不予标记,因此该门禁在 proposed/implemented/rejected 中统一适用。 +- `scripts/check-workspace-constraints.ts` 断言 `packages//` 形状:分组目录不带 `package.json`,且没有包扁平地位于根层或嵌套更深。分组名称保持开放——添加新分组无需修改门禁;只有深度 2 的形状是固定的。 ## 曾考虑的替代方案 -- **第三层(每个族下设 `adapters/`/`impls/`)**:否决。统一深度 2 使 workspace glob 保持简洁的 `packages/*/*`,一条 `@deepseek-ai/dsh-*` tsconfig 通配符即可解析所有包。 +- **第三层(每个族下设 `adapters/`/`impls/`)**:否决。统一深度 2 使 workspace glob 保持简洁的 `packages/*/*`,并让一条 `@deepseek-ai/dsh-*` tsconfig 通配符即可解析所有包。 - **将持久化嵌套在 `core/session/` 下**:否决。存储后端构成一个平行的能力族,与 `llm/` 和 `bash/` 对称,而会话日志本身属于核心产品 API。 -- **`ui-stdio` 放在 `ui/` 下**:否决。它是与示例耦合的开发支撑,不是产品接口面;`acp` 是 `ui/` 的唯一成员,因为编辑器确实在驱动它。 +- **`ui-stdio` 放在 `ui/` 下**:否决。它是与示例耦合的开发支撑,不是产品接口;`acp` 是 `ui/` 的唯一成员,因为编辑器实际驱动它。 ## 后果 -本次重组在一次协调的变更中搅动了 import、workspace glob、文档链接、构建引用和包路径。这种搅动在发布前是可接受的(遵循 AGENTS.md 中「基础优先于爆炸半径」的立场),因为它阻止了扁平布局将支撑包固化为产品契约;而且这是一次性成本:通配符 `paths`、glob 推导的 publint 列表和形状门禁意味着新增一个包无需再做额外的结构编辑。 +本次重组在一次协调的变更中搅动了 import、workspace glob、文档链接、构建引用和包路径。这种变动在发布前是可接受的(依据 AGENTS.md 中「基础优先于爆炸半径」的立场),因为它阻止了扁平布局将支撑包固化为产品契约,且这是一次性成本:通配符 `paths`、glob 推导的 publint 列表和形状门禁意味着新增一个包无需额外的结构性编辑。 diff --git a/docs/rfc/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.i18n.yaml b/docs/rfc/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.i18n.yaml index c69c49d2c7..4a0b9d6ebc 100644 --- a/docs/rfc/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.i18n.yaml +++ b/docs/rfc/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-21-mandatory-app-attribution-headers.md: 4fa773e089b3a4b682e42269a66d85aeaf5c18f6 -2026-06-21-mandatory-app-attribution-headers.zh.md: 3660b8e7de977c01a19f9ed9ac9e73409f69706a +2026-06-21-mandatory-app-attribution-headers.zh.md: 42cc396b5719adb2a2d71e0e9cf0d3554c5533a5 diff --git a/docs/rfc/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.zh.md b/docs/rfc/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.zh.md index 3660b8e7de..42cc396b57 100644 --- a/docs/rfc/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.zh.md +++ b/docs/rfc/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.zh.md @@ -1,84 +1,84 @@ # RFC:对提供方请求强制携带 `User-Agent` 归属标识 -Status: implemented - [English](2026-06-21-mandatory-app-attribution-headers.md) | 中文 +Status: implemented + ## 问题 -LLM(大语言模型)提供方请求应当标识发出请求的产品。这对提供方侧的技术支持、滥用调查、兼容性调试和流量分析都有价值。在本 RFC 之前,harness 只部分做到了这一点:手写的 DeepSeek 适配器发送一个手动复制的 `User-Agent` 常量(`packages/llm/llm-deepseek/src/adapter.ts`),而基于 pi-ai 的孪生适配器完全不发送 harness 自有的头部(`packages/llm/llm-pi-ai/src/adapter.ts`)。因此新适配器可以静默地遗漏归属标识,而库封装的适配器也可能与手写适配器产生偏差——尽管[孪生适配器 RFC](2026-06-13-twin-llm-adapters.md) 的存在正是为了让两种实现在提供方 seam 上保持诚实。 +LLM(大语言模型)提供方请求应当标识发出请求的产品。这对提供方侧的技术支持、滥用调查、兼容性调试和流量分析都有价值。在本 RFC 之前,harness 只做了部分工作:手写的 DeepSeek 适配器发送了一个手动复制的 `User-Agent` 常量(`packages/llm/llm-deepseek/src/adapter.ts`),而基于 pi-ai 的孪生适配器则完全不发送 harness 自有的头部(`packages/llm/llm-pi-ai/src/adapter.ts`)。因此新适配器可以悄无声息地省略归属标识,而基于库的适配器也可能与手写适配器产生偏差——尽管[孪生适配器 RFC](2026-06-13-twin-llm-adapters.md) 的存在正是为了让两种实现在提供方 seam 上保持诚实。 -直接触发点来自 OpenRouter 的 [App Attribution](https://openrouter.ai/docs/app-attribution) 文档。OpenRouter 通过 `HTTP-Referer` 加展示名/分类头部来创建应用页面和排名。这有价值,但它不是 HTTP 标准中的应用身份机制。风险在于:把 OpenRouter 的确切头部集合当作通用标准采纳,然后将提供方特定的头部泄漏到直连 DeepSeek 的请求、未来的 OpenAI/Anthropic/Vertex 适配器、测试服务器或无限期记录未知字段的代理中。 +直接触发因素来自 OpenRouter 的 [App Attribution](https://openrouter.ai/docs/app-attribution) 文档。OpenRouter 根据 `HTTP-Referer` 加上 display/category 头部来创建应用页面和排名。这有价值,但它不是 HTTP 标准中的应用身份机制。风险在于:把 OpenRouter 的精确头部集当作通用标准来采纳,然后将提供方特有的头部泄漏到直连 DeepSeek 的请求、未来的 OpenAI/Anthropic/Vertex 适配器、测试服务器或无限期记录未知字段的代理中。 ## 调研 -- **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 的框架通常需要一种方式来透传它。但 ACP(Agent Client Protocol)等 agent 协议在自己的 initialize 消息中协商名称、版本和能力,而模型提供方请求仍需 HTTP 层面的身份标识。因此「在 agent 世界被接受」意味着「被 OpenRouter 集成所识别」,而非「可跨 agent 运行时或提供方移植」。 -- **编程 agent 在 `User-Agent` 中标识产品和版本。** 公开实现在环境细节和提供方特定附加头部上各有不同,但产品身份是共同契约;不存在通用的精确格式。 -- **标准化的通用客户端身份头部是 `User-Agent`。** RFC 9110 第 10.1.5 节将 `User-Agent` 定义为用户代理软件的身份标识,说明它用于互操作性报告和分析,并说用户代理应当(SHOULD)在每个请求中发送它,除非被配置为不发送。这是唯一直接匹配「哪个产品在发出这个 HTTP 请求」的标准头部。 +- **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 的框架通常需要一种方式来透传它。但 ACP(Agent 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` 定义为负责用户代理的人类的电子邮件地址。机器人代理应当(SHOULD)发送它以便服务器联系运营者,但非机器人代理不应在没有用户显式配置的情况下发送它,因为存在隐私和安全策略顾虑。harness 可以后续支持运营者联系方式,但不得凭空编造或全局强制要求。 -- **请求体中的 `user` 或 `metadata` 字段不是应用归属。** 某些模型 API 暴露稳定的终端用户标识符、请求元数据、标签或项目/账户头部。这些对滥用监控、内部计费、仪表盘或链路追踪有用,但它们要么标识的是终端用户而非产品,要么是提供方特定的 body schema,要么不保证能通过 OpenAI 兼容网关转发。它们不能替代静态的应用身份头部。 -- **SDK 遥测头部标识的是 SDK,而非应用。** 官方和第三方 SDK 经常发送库/版本头部。这些帮助 SDK 维护者调试客户端,但除非应用显式提供产品归属层,否则它们不会将 harness 标识为应用。 -- **pi-ai 有一流的头部钩子。** `@earendil-works/pi-ai` 的 `StreamOptions.headers` 将调用方头部最后合并(覆盖提供方默认值),因此库封装的适配器无需包装或上游改动即可满足与手写适配器相同的协议格式(wire format)契约。mock 服务器测试套件对两个适配器都断言头部到达了线路。 +- **`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 服务器测试套件对两个适配器都断言头部到达了线路。 ## 决策 -在 LLM 适配器边界,提供方请求归属是强制的,且仅使用标准 `User-Agent` 头部。规则是:每个产品 LLM 适配器在每个提供方 HTTP 请求上发送一个静态、非机密的应用身份,且每个适配器都有测试证明 `User-Agent` 到达了线路(mock 服务器断言收到的头部;对于库封装的适配器,库的头部钩子喂入同一个 mock 服务器断言)。 +在 LLM 适配器边界,提供方请求归属是强制的,且仅使用标准 `User-Agent` 头部。规则:每个生产 LLM 适配器在每个提供方 HTTP 请求上发送一个静态、非机密的应用身份,且每个适配器都有测试证明 `User-Agent` 到达了线路(mock 服务器断言收到的头部;对于基于库的适配器,通过库的头部钩子馈入同一个 mock 服务器断言)。 -本 RFC **不**实现 OpenRouter 应用归属。`HTTP-Referer`、`X-OpenRouter-Title`、`X-Title` 和 `X-OpenRouter-Categories` 是 OpenRouter 特定的产品展示头部,不是提供方无关的模型请求归属。它们可以后续由 OpenRouter 适配器或显式 OpenRouter 模式提出,带有自己的隐私/产品决策、测试和文档。在那之前,即使请求指向 OpenRouter,也只发送本 RFC 的共享 `User-Agent` 归属。 +本 RFC **不**实现 OpenRouter 应用归属。`HTTP-Referer`、`X-OpenRouter-Title`、`X-Title` 和 `X-OpenRouter-Categories` 是 OpenRouter 特有的产品展示头部,不是提供方无关的模型请求归属。它们可以后续由 OpenRouter 适配器或显式 OpenRouter 模式提出,附带自己的隐私/产品决策、测试和文档。在此之前,即使请求指向 OpenRouter,也只发送本 RFC 定义的共享 `User-Agent` 归属。 -提供方无关的身份由 `dsh-llm`(`packages/llm/llm/src/attribution.ts`)拥有,而非各个适配器。`AppIdentity` 仅包含构建 `User-Agent` 所需的公开产品事实,默认的 `APP_IDENTITY` 确定了提案中留待决定的值: +提供方无关的身份由 `dsh-llm`(`packages/llm/llm/src/attribution.ts`)拥有,而非各适配器。`AppIdentity` 仅包含构建 `User-Agent` 所需的公开产品事实,默认的 `APP_IDENTITY` 确定了提案中留待决定的值: -- `User-Agent` 的产品令牌:`deepseek-harness`(与 RFC 之前的线路值以及仓库/组织身份保持连续性) -- 版本:通过 `createRequire` 从所属包的 manifest(元数据清单)读取,绝不手动复制常量 -- 应用 URL:`https://github.com/deepseek-ai/deepseek-harness-sdk`——计划中的公开主页;`attribution.ts` 中的 `FIXME` 阻塞发布,直到该仓库实际存在 +- `User-Agent` 的产品 token:`deepseek-harness`(与 RFC 之前的线路值及仓库/组织身份保持连续性) +- 版本:通过 `createRequire` 从所属包的 manifest 读取,绝不手动复制常量 +- 应用 URL:`https://github.com/deepseek-ai/deepseek-harness-sdk`——计划中的公开主页;`attribution.ts` 中的 `FIXME` 标记在该仓库实际存在之前阻塞发布 -默认值是强制的且非空。白标部署向 `attributionHeaders(identity)` 传入自己的 `AppIdentity`——覆盖 seam 就是函数参数,在有消费方需要之前不做部署配置管道——省略时回退到 harness 默认值而非抑制归属。没有逐请求 API 让模型、用户提示词、会话 id、cwd、用户邮箱、API key 所有者或本地机器身份影响这些字段。 +默认值是强制的且非空。白标部署通过向 `attributionHeaders(identity)` 传入自己的 `AppIdentity` 来覆盖——覆盖 seam 就是函数参数,在有消费方需要之前不做部署配置管道——省略时回退到 harness 默认值而非抑制归属。没有逐请求 API 允许模型、用户提示词、会话 id、cwd、用户邮箱、API key 所有者或本地机器身份影响这些字段。 -线路映射(`attributionHeaders`;代码中头部名称为小写——HTTP 字段名在线路上不区分大小写): +线路映射(`attributionHeaders`;代码中头部名称小写——HTTP 字段名在线路上不区分大小写): | 目标 | 映射 | |---|---| | 所有基于 HTTP 的适配器 | `User-Agent: {product}/{version} (+{url})`——括号中的 `+url` 注释符合 RFC 9110 保守的 product/comment 语法。 | -| 直连 DeepSeek 端点 | `User-Agent`;除非 DeepSeek 文档记录了等效契约,否则不发送 OpenRouter 专用头部。 | +| 直连 DeepSeek 端点 | `User-Agent`;除非 DeepSeek 文档化了等效契约,否则不发送 OpenRouter 特有头部。 | | OpenRouter 端点 | 目前仅 `User-Agent`。本 RFC 下不发送 `HTTP-Referer`、`X-OpenRouter-Title`、`X-Title` 或 `X-OpenRouter-Categories`。 | -| 未来提供方 | 仅 `User-Agent`,除非后续提供方特定 RFC 接受额外头部。不以类推方式复用 `HTTP-Referer`。 | +| 未来提供方 | 仅 `User-Agent`,除非后续提供方特有的 RFC 接受额外头部。不要类比复用 `HTTP-Referer`。 | -端点检测不属于本 RFC,因为此处不接受任何端点特定映射。如果后续落地 OpenRouter 支持,检测必须是显式的:要么是专用的 OpenRouter 提供方包,要么是显式的 `provider: 'openrouter'` / `attributionTarget: 'openrouter'` 配置,而非任意路径片段或模型名。 +端点检测不在本 RFC 范围内,因为此处不接受任何端点特有的映射。如果后续支持 OpenRouter,检测必须是显式的:要么是专门的 OpenRouter 提供方包,要么是显式的 `provider: 'openrouter'` / `attributionTarget: 'openrouter'` 配置,而非任意路径片段或模型名称。 ## 验证 已落地的契约: -- `dsh-llm` 为 `LlmAdapter` 作者记录了强制的 `User-Agent` 归属契约(`LlmAdapter` JSDoc、包 README,以及 `docs/core-data-structures/llm-streaming.md` 的适配器契约章节)。 +- `dsh-llm` 为 `LlmAdapter` 作者文档化了强制的 `User-Agent` 归属契约(`LlmAdapter` JSDoc、包 README,以及 `docs/core-data-structures/llm-streaming.md` 的适配器契约章节)。 - 共享辅助函数(`attributionHeaders` / `userAgent`)从包元数据构建应用身份和标准 `User-Agent` 值,适配器无需手动复制版本常量。 - `dsh-llm-deepseek` 在每个请求上发送共享的 `User-Agent`,其 mock 服务器套件断言精确值。 - `dsh-llm-pi-ai` 通过 pi-ai 的 `StreamOptions.headers` 钩子发送相同的 `User-Agent`,其 mock 服务器套件断言精确值。 -- 本 RFC 下没有适配器发送 OpenRouter 特定的归属头部(`HTTP-Referer`、`X-OpenRouter-Title`、`X-Title`、`X-OpenRouter-Categories`)。 -- 没有应用归属字段携带机密、本地路径、会话 id、提示词文本、模型输出、用户邮箱或逐用户稳定标识符。 -- 适配器 README 声明了 `User-Agent` 归属策略,并明确避免将 OpenRouter 应用归属记录为已实现行为。 +- 本 RFC 下没有适配器发送 OpenRouter 特有的归属头部(`HTTP-Referer`、`X-OpenRouter-Title`、`X-Title`、`X-OpenRouter-Categories`)。 +- 没有应用归属字段携带机密、本地路径、会话 id、提示词文本、模型输出、用户邮箱或逐用户的稳定标识符。 +- 适配器 README 声明了 `User-Agent` 归属策略,并明确避免将 OpenRouter 应用归属记录为已实现的行为。 ## 曾考虑的替代方案 -**现在就实现 OpenRouter 应用归属。** 本 RFC 否决。发送 `HTTP-Referer` 加 `X-OpenRouter-Title` 可以满足 OpenRouter 排名,但这些头部是提供方特定的产品功能,不是本 RFC 试图标准化的提供方无关模型请求归属。支持它们应当是后续显式的 OpenRouter 适配器/模式决策,而非隐藏在第一个共享归属辅助函数中。 +**现在就实现 OpenRouter 应用归属。** 本 RFC 否决。发送 `HTTP-Referer` 加 `X-OpenRouter-Title` 可以满足 OpenRouter 排名,但这些头部是提供方特有的产品功能,不是本 RFC 试图标准化的提供方无关的模型请求归属。支持它们应当是后续显式的 OpenRouter 适配器/模式决策,而非隐藏在首个共享归属辅助函数中。 -**所有地方都发 OpenRouter 头部。** 否决。这会把一份自定义 OpenRouter 契约当作通用标准,并向未要求这些字段的提供方发送语义误导的字段。还有风险把 `HTTP-Referer` 当作通用应用 URL 字段使用,尽管标准 HTTP 已有 `User-Agent` 用于产品身份、`Referer` 用于不同的浏览上下文概念。 +**向所有提供方发送 OpenRouter 头部。** 否决。这会把一份自定义的 OpenRouter 契约当作通用标准,并向未要求这些字段的提供方发送语义误导的头部。还有风险将 `HTTP-Referer` 当作通用应用 URL 字段使用,尽管标准 HTTP 已有 `User-Agent` 用于产品身份、`Referer` 用于不同的浏览上下文概念。 -**仅使用提供方账户/项目身份。** 否决。组织/项目头部、API key、云账户和计费项目标识的是谁付费或谁拥有请求,而非哪个应用在发送流量。它们也不暴露公开的应用标题/分类,不帮助 OpenRouter 等网关构建应用排名。 +**仅使用提供方账户/项目身份。** 否决。组织/项目头部、API key、云账户和计费项目标识的是谁付费或谁拥有请求,而非哪个应用在发送流量。它们也不暴露公开的应用标题/类别,无法帮助 OpenRouter 等网关构建应用排名。 -**终端用户 `user`/`metadata` 字段。** 本 RFC 否决。这些对滥用监控和客户支持有价值,但描述的是请求背后的人或租户。应用归属必须是静态产品身份,且可安全地在每个请求上发送。 +**终端用户 `user`/`metadata` 字段。** 本 RFC 否决。这些对滥用监控和客户支持有价值,但描述的是请求背后的人或租户。应用归属必须是静态的产品身份,且可安全地在每个请求上发送。 -**仅配置 opt-in 的归属。** 否决。默认关闭的设置正是适配器持续漂移的原因。策略是强制默认归属加可覆盖的公开值,而非可选归属。 +**仅配置启用的归属。** 否决。默认关闭的设置正是适配器不断漂移的原因。策略是强制默认归属加可覆盖的公开值,而非可选归属。 -**以产品命名的令牌(`deepseek-harness-sdk`)。** 曾考虑用于 `User-Agent` 令牌,因为产品名是 DeepSeek Harness SDK。`deepseek-harness` 以连续性胜出:它是提供方已经从本代码库看到的身份,与组织/仓库身份和包作用域一致,且在展示文案承载产品名的同时保持线路归属稳定。 +**以产品命名的 token(`deepseek-harness-sdk`)。** 曾考虑用于 `User-Agent` token,因为产品名是 DeepSeek Harness SDK。`deepseek-harness` 因连续性胜出:它是提供方从本代码库已经看到的身份,与组织/仓库身份和包 scope 一致,且在展示文案承载产品名的同时保持线路归属稳定。 ## 后果 -**提供方看到流量来自 harness。** 这正是目的,但意味着此前混入通用 SDK 流量的部署变得可识别。缓解措施:仅发送静态公开产品数据,并允许 fork/白标部署传入自己的 `AppIdentity`。 +**提供方看到流量来自 harness。** 这正是目的,但意味着此前混在通用 SDK 流量中的部署变得可识别。缓解措施:仅发送静态公开产品数据,并允许 fork/白标部署传入自己的 `AppIdentity`。 -**应用 URL 指向一个尚不存在的仓库。** `deepseek-ai/deepseek-harness-sdk` 是计划中的公开主页;在创建之前该 URL 是一个悬空承诺。常量上的 `FIXME` 标记阻塞发布,使其不会在未解决的情况下发版(见 `docs/development.md` 标记语义)。 +**应用 URL 指向一个尚不存在的仓库。** `deepseek-ai/deepseek-harness-sdk` 是计划中的公开主页;在它创建之前,该 URL 是一个悬空承诺。常量上的 `FIXME` 标记阻塞发布,不允许带着未解决的问题出门(见 `docs/development.md` 标记语义)。 -**不同客户端库的头部支持有差异。** 手写适配器直接设置头部;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` 才能实现该产品功能。这是有意为之:公开应用市场参与是一个独立的产品决策,不是强制请求归属的前提。 +**OpenRouter 排名尚未受益。** `User-Agent` 是提供方无关的 HTTP 身份的正确基线,但它不会创建 OpenRouter 应用页面或排名,因为 OpenRouter 要求 `HTTP-Referer` 来实现该产品功能。这是有意为之:公开应用市场参与是一个独立的产品决策,不是强制请求归属的前提。 diff --git a/docs/rfc/implemented/architecture/2026-06-26-file-context-as-event-gate.i18n.yaml b/docs/rfc/implemented/architecture/2026-06-26-file-context-as-event-gate.i18n.yaml index 7a69d33245..06f5fb57d8 100644 --- a/docs/rfc/implemented/architecture/2026-06-26-file-context-as-event-gate.i18n.yaml +++ b/docs/rfc/implemented/architecture/2026-06-26-file-context-as-event-gate.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-26-file-context-as-event-gate.md: 6e78e2df5f7969b5ed9b74c0b597e2fcacbe8e82 -2026-06-26-file-context-as-event-gate.zh.md: b0806fd3e1d61a9bdaf20a728dbfcfc945013b78 +2026-06-26-file-context-as-event-gate.zh.md: d69ccdbcea4b14dbd0291cf69af0bf7d5f3fadfc diff --git a/docs/rfc/implemented/architecture/2026-06-26-file-context-as-event-gate.zh.md b/docs/rfc/implemented/architecture/2026-06-26-file-context-as-event-gate.zh.md index b0806fd3e1..d69ccdbcea 100644 --- a/docs/rfc/implemented/architecture/2026-06-26-file-context-as-event-gate.zh.md +++ b/docs/rfc/implemented/architecture/2026-06-26-file-context-as-event-gate.zh.md @@ -1,4 +1,4 @@ -# RFC:将 `dsh-fs-policy` 改为事件门禁插件,而非方法接口 +# RFC:将 `dsh-fs-policy` 改为事件门控插件,而非方法接口 Status: implemented @@ -6,7 +6,7 @@ Status: implemented ## 问题 -[split-fs-seam RFC](../simplification/2026-06-26-fsspec-style-fs-seam.md) 在面向模型的工具与 `ctx.fs` 提供方之间放置了 `ctx.fileContext`:`dsh-tool-fs` 注入 `fileContext`,并将每次 `read`/`write`/`edit` 都路由到它的方法。这使得 `fileContext` **处于调用路径上且不可省略**。工具不经过它就无法触及 `ctx.fs`,策略层拥有 fs I/O 和读取窗口化,而一个不需要观测状态策略的部署无法简单地移除该包——否则 `dsh-tool-fs` 将无法解析 `ctx.fileContext`。 +[split-fs-seam RFC](../simplification/2026-06-26-fsspec-style-fs-seam.md) 在面向模型的工具与 `ctx.fs` 提供方之间放置了 `ctx.fileContext`:`dsh-tool-fs` 注入 `fileContext`,并将每次 `read`/`write`/`edit` 路由到它的方法。这使得 `fileContext` **位于关键路径上且不可省略**。工具不经过它就无法访问 `ctx.fs`,策略层掌控着 fs I/O 和读取窗口,而一个不需要观测状态策略的部署也无法简单地移除该包——`dsh-tool-fs` 会因无法解析 `ctx.fileContext` 而失败。 这把三件本应可分离的事情耦合在了一起: @@ -14,11 +14,11 @@ Status: implemented 2. **新鲜度/观测策略**——"编辑前必须先读"、"写入/编辑必须基于你读到的版本"。这是 `dsh-fs-policy` 插件的职责。 3. **观测状态的记录**——一个副作用,永远不应阻止工具正常运行。 -因为工具调用 `fileContext` 的方法,移除策略层是一个破坏性变更,而非优雅地失去一个*附加功能*。策略对于工具的运行是承重的,而非可选的收紧。 +由于工具调用的是 `fileContext` 方法,移除策略层就是一个破坏性变更,而非优雅地失去一个*附加*能力。策略层对工具的运行是承重性的,而非可选的收紧。 ## 决策 -反转控制流。**`dsh-tool-fs` 成为执行器,直接调用 `ctx.fs`**;**`dsh-fs-policy` 成为门禁 + 记录器插件**,通过事件参与,既不通过工具调用的方法,也不注册 `ctx.fileContext` 服务。 +反转控制流。**`dsh-tool-fs` 成为执行器,直接调用 `ctx.fs`**;**`dsh-fs-policy` 成为门控 + 记录插件**,通过事件参与,从不通过工具调用的方法,也不注册 `ctx.fileContext` 服务。 ```text tool dsh-tool-fs executor: resolves, reads windows, writes/edits via ctx.fs; @@ -31,22 +31,22 @@ provider seam dsh-fs ctx.fs: text IO + ATOMIC mutation primitives who provider dsh-fs-local local implementation of ctx.fs ``` -该模型是叠加式的:裸 `ctx.fs` 执行原子的、无约束的文本 I/O,而 `dsh-fs-policy` 在其上叠加观测状态、读后才能编辑、以及版本守卫。因此移除策略后工具仍可用,只是不受约束。正式发布的 agent 配置会加载策略;裸模式的存在是为了在服务边界保持策略可选,而非作为正常部署姿态。 +该模型是叠加式的:裸 `ctx.fs` 执行原子化、无约束的文本 I/O,而 `dsh-fs-policy` 叠加观测状态、先读后编辑和版本守卫。因此移除策略层后工具仍可用,只是不受约束。正式发布的 agent 配置会加载策略;裸模式的存在是为了让策略在服务边界保持可选,而非作为正常部署姿态。 -`dsh-tool-fs` 不再注入 `fileContext`。它注入 `fs` 以及 `tools`/`systemPrompt`。 +`dsh-tool-fs` 不再注入 `fileContext`。它注入 `fs` 和 `tools`/`systemPrompt`。 -## 策略由提供方 CAS 强制执行,而非由 `dsh-fs-policy` stat +## 策略由提供方 CAS 强制执行,而非 `dsh-fs-policy` 的 stat -`dsh-fs-policy` 强制执行"你必须基于你读到的版本来写入/编辑",**自身从不调用 `stat` 或比较版本**。它将观测到的版本作为 CAS 基准提供,让提供方的变更临界区检测陈旧: +`dsh-fs-policy` 强制执行"你必须基于你读到的版本来写入/编辑",**自身从不调用 `stat` 或比较版本**。它将观测到的版本作为 CAS 基准提供,让提供方的 mutation 临界区检测陈旧性: - "你读过这个文件吗?"是 `dsh-fs-policy` 在本地决定的唯一事项——一次 `WeakMap` 查找,无 I/O。无记录 ⇒ `FS_NOT_OBSERVED`。 -- "你读到的版本还是最新的吗?"由 **`ctx.fs.editText`/`writeText` 内部**决定,在执行 read-match-rename 的同一把原子锁中。`dsh-fs-policy` 将 `vObserved` 作为期望值传入;如果文件已变更,提供方抛出 `FS_STALE_VERSION`。 +- "你读到的版本是否仍为最新?"由 **`ctx.fs.editText`/`writeText` 内部**决定,在执行 read-match-rename 的同一个原子锁中完成。`dsh-fs-policy` 将 `vObserved` 作为期望值传入;如果文件已变更,提供方抛出 `FS_STALE_VERSION`。 -这是刻意的设计。如果 `dsh-fs-policy` 在其 waterfall(瀑布式事件)处理器中 stat 并比较版本,那么该检查与工具实际写入之间会存在 TOCTOU 间隙——文件可能在两者之间变化,因此该检查只是一个虚假保证,提供方的锁无论如何都要兜底。将版本检查放在提供方的临界区内既无竞态又零额外 `stat`。所以 `dsh-fs-policy` **不做**任何文件系统 I/O;"必须基于最新读取"的保证由 CAS *实现*,`dsh-fs-policy` 只负责选择基准(`vObserved`)并对先前观测进行门控。 +这是有意为之的。如果 `dsh-fs-policy` 在其 waterfall(瀑布式事件)处理器中 stat 并比较版本,该检查与工具实际写入之间会存在 TOCTOU 间隙——文件可能在此期间变化,因此该检查只是一个虚假保证,提供方的锁无论如何都要兜底。将版本检查放在提供方的临界区中既无竞态又无额外 `stat`。所以 `dsh-fs-policy` **不做**任何文件系统 I/O;"必须基于最近一次读取"的保证由 CAS *实现*,`dsh-fs-policy` 只负责选择基准(`vObserved`)并对先前观测进行门控。 ## 提供方契约变更:版本守卫变为可选 -为使裸提供方不受约束,其两个变更操作上的版本守卫变为**可选**——有则守卫,无则无条件: +为使裸提供方不受约束,其两个 mutation 上的版本守卫变为**可选**——传入则守卫,省略则无条件执行: ```ts ignore-check // writeText: expected is now optional. The FsWriteIntent union is UNCHANGED. @@ -62,17 +62,17 @@ editText(target: FsTarget, edit: FsEditRequest, expected?: { version: FsVersion // { version } → edit only at that version, else FS_STALE_VERSION (the current behavior) ``` -`FsWriteIntent` 联合类型本身不变——第三种"无条件"状态通过*省略* `expected` 来表达,因此两个变更操作共享一个对称的形状(`expected?`:省略 = 无守卫,提供 = 有守卫)。这对 `dsh-fs-policy` 使用的有守卫路径保持完全向后兼容;只有之前不可能的"无守卫"情况是新增的,且它是裸提供方的默认行为。无论哪种情况,变更操作仍在后端的 per-target 锁内运行,因此无条件的写入/编辑仍然是原子的(不会出现文件撕裂);"无条件"去掉的是*版本*前置条件,而非原子性。`editText` 在有守卫和无守卫路径上都将缺失的目标报告为 `FS_STALE_VERSION`,为"此刻无法编辑该目标"保留一个统一的编辑失败码。 +`FsWriteIntent` 联合类型本身不变——第三种"无条件"状态通过*省略* `expected` 来表达,因此两个 mutation 共享同一种对称形状(`expected?`:省略 = 无守卫,传入 = 有守卫)。这对 `dsh-fs-policy` 使用的有守卫路径保持完全向后兼容;只有之前不可能出现的"无守卫"情况是新增的,且它是裸提供方的默认行为。无论哪种情况,mutation 仍在后端的 per-target 锁内运行,因此无条件写入/编辑仍是原子的(不会产生撕裂文件);"无条件"去掉的是*版本*前置条件,而非原子性。`editText` 在有守卫和无守卫路径上都将缺失目标报告为 `FS_STALE_VERSION`,保持一个统一的编辑失败码表示"此刻无法编辑该目标"。 -## 事件词汇(归属 `dsh-fs`) +## 事件词汇(由 `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——而非面向模型的概念(行窗口、行号、渲染页脚均不会泄漏到此层)。 +这些事件携带既有的 `dsh-fs` 词汇(`FsTarget`、`FsVersion`、`FsWriteIntent`)加一个不透明的 actor——不携带面向模型的概念(行窗口、行号或渲染后的页脚不会泄漏到此层)。 -**两个 `fs/*` 决策事件是单槽位、先到先得的 waterfall。** `dsh-fs-policy` 不调用 `next()` 即返回,因此在默认部署中它占据该槽位;一个注册更早或使用 `prepend` 的监听器会取代该策略。权限、审计和沙箱关注点仍在可组合的 `tools/execute` waterfall 上。 +**两个 `fs/*` 决策事件是单槽、先到先得的 waterfall。** `dsh-fs-policy` 不调用 `next()` 直接返回,因此在默认部署中它占据该槽位;更早注册或使用 `prepend` 的监听器会替代该策略。权限、审计和沙箱关注点仍留在可组合的 `tools/execute` waterfall 上。 -actor 在 `dsh-fs` 中类型为 `object`——一个纯粹的不透明载体,提供方 seam 从不读取或窄化它。owner 的推导(`actor.agent?.session`)和 `{ agent?: { session? } }` 结构形状完全留在 `dsh-fs-policy` 内部,由其监听器将 `object` actor 窄化为该形状。`dsh-fs` 拥有事件名和 fs 词汇;它**不**拥有策略层的运行时 owner 结构。 +actor 在 `dsh-fs` 中类型为 `object`——一个纯粹的不透明载体,提供方 seam 从不读取或收窄它。owner 的推导(`actor.agent?.session`)和 `{ agent?: { session? } }` 结构形状完全留在 `dsh-fs-policy` 内部,由其在监听器中将 `object` actor 收窄为该形状。`dsh-fs` 拥有事件名和 fs 词汇;它**不**拥有策略层的运行时 owner 结构。 ```ts import type { FsTarget, FsVersion, FsWriteIntent } from '@deepseek-ai/dsh-fs' @@ -106,66 +106,66 @@ interface Events { } ``` -`fs/*` 决策事件是**由工具分发的无绑定 waterfall**(类似 `agent/request`,由 loop 分发且无 `this`),而非服务绑定的 waterfall(如 `llm/stream`)。分发方是 `dsh-tool-fs` 插件,它不是一个服务。 +`fs/*` 决策事件是**由工具分发的无绑定 waterfall**(类似 `agent/request`,由循环分发且无 `this`),而非服务绑定的 waterfall(如 `llm/stream`)。分发者是 `dsh-tool-fs` 插件,它不是一个服务。 ## 工具契约(`dsh-tool-fs`) -工具保留其面向模型的 schema(`read`/`write`/`edit`,逐字节不变)和 prompt 段落。prompt 引导仍以策略为先,因为加载 fs 工具的部署预期也会加载 `dsh-fs-policy`:模型仍被告知在覆写或编辑前先读取,任何说"后端"要求如此的措辞应改为说 fs-policy 插件要求如此。裸提供方的回退不改变 prompt 立场。 +工具保留其面向模型的 schema(`read`/`write`/`edit`,逐字节不变)和 prompt 段落。prompt 引导仍以策略优先,因为加载 fs 工具的部署预期也会加载 `dsh-fs-policy`:模型仍被告知在覆写或编辑前先读取,任何声称"后端"要求如此的措辞应修正为 fs-policy 插件要求如此。裸提供方回退不改变 prompt 立场。 -`dsh-tool-fs` 获得了从旧 `fileContext` 方法服务迁移来的执行器职责,包括**读取渲染**(`read-render.ts`:`buildWindow` + `formatReadOutput`、`READ_MAX_BYTES`、`READ_MAX_LINE_LENGTH`、`FileReadOutcome`/`FileTextLine`,以及 `read.ts` 中的 `STREAM_MIN_SIZE`),这些现在是工具的渲染细节,因为工具拥有了读取操作。这些读取渲染类型和辅助函数迁入 `dsh-tool-fs`;策略插件不得继续作为工具的类型依赖。 +`dsh-tool-fs` 获得从旧 `fileContext` 方法服务迁移来的执行器职责,包括**读取渲染**(`read-render.ts`:`buildWindow` + `formatReadOutput`、`READ_MAX_BYTES`、`READ_MAX_LINE_LENGTH`、`FileReadOutcome`/`FileTextLine`,以及 `read.ts` 中的 `STREAM_MIN_SIZE`),这些现在是工具的渲染细节,因为读取已由工具拥有。这些读取渲染类型和辅助函数移入 `dsh-tool-fs`;策略插件不得继续作为工具的类型依赖。 -`dsh-tool-fs` 是一个注册全部三个工具(`read`/`write`/`edit`)的单根插件,与 `dsh-tool-bash` 对齐。它注入 `fs`(加 `tools`/`systemPrompt`),从不注入 `fileContext`。(最初的提案还将每个工具作为 `/read`/`/write`/`/edit` 子路径插件暴露,以支持聚焦部署;实现时已放弃——没有消费方需要单工具部署,且子路径发布迫使引入定制的 `tsdown`/`tsconfig`/`files`/workspace-constraint 处理,而同级的工具包都不需要这些。每工具的注册辅助函数(`applyReadTool`/`applyWriteTool`/`applyEditTool`)保留为根插件组合的内部模块。) +`dsh-tool-fs` 是一个注册全部三个工具(`read`/`write`/`edit`)的单一根插件,与 `dsh-tool-bash` 相同。它注入 `fs`(加 `tools`/`systemPrompt`),从不注入 `fileContext`。(最初的提案还将每个工具作为 `/read`/`/write`/`/edit` 子路径插件暴露,供聚焦部署使用;实现时被放弃——没有消费方需要单工具部署,且子路径发布迫使引入兄弟工具包都不需要的定制 `tsdown`/`tsconfig`/`files`/workspace-constraint 处理。每工具的注册辅助函数(`applyReadTool`/`applyWriteTool`/`applyEditTool`)仍作为根插件组合的内部模块保留。) -`stat` 预算通过让 waterfall 惰性产出期望值来最小化——裸默认返回 `undefined`(无守卫),从不 stat: +通过让 waterfall 惰性产出期望值来最小化 `stat` 预算——裸默认返回 `undefined`(无守卫),从不 stat: -- **read**——一次 `stat`(类型 + 大小路由 + 版本),然后 `readText`/`streamText`,然后 `buildWindow`,然后 `emit('fs/observed', target, info.version, exec)`。旧 `fileContext.read` 中读取后的确认 `stat` 被移除;在路由 stat 和读取之间竞争的写入者最多只能使*后续*有守卫的编辑虚假地 `FS_STALE_VERSION`(快速失败:模型重新读取,从不基于错误版本写入,因为 `editText` 在其锁内重新检查)。 -- **write**——`expectation = await ctx.waterfall('fs/write-intent', target, exec, () => undefined)`,然后 `ctx.fs.writeText(target, content, expectation)`,然后 `emit('fs/observed', target, outcome.version, exec)`。**工具内零 stat**,无论是否有 `dsh-fs-policy`。 -- **edit**——`expectation = await ctx.waterfall('fs/edit-intent', target, exec, () => undefined)`,然后 `ctx.fs.editText(target, edit, expectation)`,然后 `emit('fs/observed', target, outcome.version, exec)`。**两种情况下工具内均零 stat**:裸默认为 `undefined`(无条件编辑),因此工具从不 stat 来制造基准。如果目标不存在,提供方即使在无守卫路径上也报告 `FS_STALE_VERSION`。 +- **read**——一次 `stat`(类型 + 大小路由 + 版本),然后 `readText`/`streamText`,然后 `buildWindow`,然后 `emit('fs/observed', target, info.version, exec)`。旧 `fileContext.read` 中读后确认的 `stat` 被移除;在路由 stat 和读取之间竞争的写入者最多只能使*后续*有守卫的编辑误报 `FS_STALE_VERSION`(快速失败:模型重新读取,从不基于错误版本写入,因为 `editText` 在其锁内重新检查)。 +- **write**——`expectation = await ctx.waterfall('fs/write-intent', target, exec, () => undefined)`,然后 `ctx.fs.writeText(target, content, expectation)`,然后 `emit('fs/observed', target, outcome.version, exec)`。无论是否有 `dsh-fs-policy`,**工具内零 stat**。 +- **edit**——`expectation = await ctx.waterfall('fs/edit-intent', target, exec, () => undefined)`,然后 `ctx.fs.editText(target, edit, expectation)`,然后 `emit('fs/observed', target, outcome.version, exec)`。两种情况下**工具内零 stat**:裸默认为 `undefined`(无条件编辑),因此工具从不 stat 来制造基准。如果目标不存在,提供方即使在无守卫路径上也报告 `FS_STALE_VERSION`。 -工具在每次分发时将 `exec`(工具执行上下文)作为 `actor` 参数传入,这样 `dsh-fs-policy` 就能推导其观测状态的 owner。工具不知道策略插件是否存在:它总是在 `next` thunk 中提供裸默认行为,而 `dsh-fs-policy` 在默认部署中会在 thunk 运行前短路它。 +工具在每次分发时将 `exec`(工具执行上下文)作为 `actor` 参数传入,以便 `dsh-fs-policy` 推导其观测状态的 owner。工具不知道策略插件是否存在:它始终在 `next` thunk 中提供裸默认行为,而 `dsh-fs-policy` 在默认部署中会在 thunk 运行前短路它。 -**`fs/observed` 在操作成功后触发。** 其监听器必须是同步的、不抛异常的记录器;工具不对 plain emit 做守卫,因此抛异常的监听器会在变更已成功后报告失败。异步或可失败的观测需要另一个事件契约。 +**`fs/observed` 在操作成功后触发。** 其监听器必须是同步、不抛异常的记录器;工具不对 plain emit 做保护,因此抛异常的监听器会在 mutation 已成功后报告失败。异步或可失败的观测需要另一份事件契约。 ## 策略插件契约(`dsh-fs-policy`) -`dsh-fs-policy` 是一个插件,不是服务。它不注册 `ctx.fileContext`,没有公开方法面,也不暴露 `read`/`write`/`edit`/`resolve` 方法。它通过 `ctx.on()` 注册三个监听器(每个返回一个用于 HMR(热模块替换)的 disposer(资源释放))。它维护观测状态的 `WeakMap>` 和结构化的 owner 推导(将事件中不透明的 `object` actor 窄化为自己的 `{ agent?: { session? } }` 形状),但不注入 `fs`——每个处理器只操作自己的 `WeakMap`,从不操作 `ctx.fs`。 +`dsh-fs-policy` 是插件,不是服务。它不注册 `ctx.fileContext`,没有公开方法面,不暴露 `read`/`write`/`edit`/`resolve` 方法。它通过 `ctx.on()` 注册三个监听器(每个返回一个 disposer 用于 HMR)。它维护观测状态 `WeakMap>`,以及结构化的 owner 推导(将事件中不透明的 `object` actor 收窄为自己的 `{ agent?: { session? } }` 形状),但不注入 `fs`——每个处理器只操作自己的 `WeakMap`,从不操作 `ctx.fs`。 - `fs/write-intent` 监听器:`prior = getObserved(owner, key)`;返回 `prior ? { kind: 'replaceIfVersion', version: prior.version } : { kind: 'createIfAbsent' }`。它不调用 `next()`:完全占据单一决策槽位。 - `fs/edit-intent` 监听器:`prior = getObserved(owner, key)`;如果无 `owner` 或无 `prior`,抛出 `FS_NOT_OBSERVED`;否则返回 `{ version: prior.version }`。同样不调用 `next()`。 - `fs/observed` 监听器:`record(owner, key, version)`。 -一条观测状态条目是**先前观测记录**:成功的 `read`、`write` 或 `edit` 都会 emit `fs/observed` 并记录 `{ version }`,因此条目的存在意味着"该 owner 在此版本观测过该目标",而非狭义的"已读取过"。这使得 create-then-edit 或 edit-then-edit 序列无需中间重新读取即可工作:变更操作将记录的版本刷新为自身的结果,因此下一次编辑的基准就是它刚产出的版本。`FS_NOT_OBSERVED` 只拒绝完全没有任何先前观测的编辑。owner 从 `{ agent?: { session? } }` 结构化推导;dispose(资源释放)时丢弃所有状态(HMR 安全)。 +一条观测状态条目是**先前观测记录**:成功的 `read`、`write` 或 `edit` 都会 emit `fs/observed` 并记录 `{ version }`,因此条目的存在意味着"此 owner 在此版本观测过此目标",而非狭义的"已读取过"。这使得 create-then-edit 或 edit-then-edit 序列无需中间重新读取即可工作:mutation 将记录的版本刷新为自身的结果,因此下一次编辑的基准就是它刚产出的版本。`FS_NOT_OBSERVED` 只拒绝完全没有任何先前观测的编辑。owner 从 `{ agent?: { session? } }` 结构化推导;dispose 时丢弃所有状态(HMR 安全)。 -`dsh-fs-policy` 现在是一个纯策略/记录插件,没有服务面——它只通过事件 seam 影响外部世界。这正是从 `dsh-tool-fs` 移除方法耦合的关键。 +`dsh-fs-policy` 现在是一个纯策略/记录插件,没有服务面——它只通过事件 seam 影响外界。这正是移除 `dsh-tool-fs` 方法耦合的关键。 ## 裸提供方行为(无 `dsh-fs-policy`) -这不是预期的部署姿态——加载 fs 工具的配置预期也会加载 `dsh-fs-policy`。这是工具不再耦合于策略方法服务后存在的无约束提供方下限。在 `dsh-fs-policy` 缺席时,每个 `fs/*` waterfall 都落入其 `undefined` 默认值,`fs/observed` 无监听器: +这不是预期的部署姿态——加载 fs 工具的配置预期也会加载 `dsh-fs-policy`。它是工具不再耦合于策略方法服务后所存在的无约束提供方下限。当 `dsh-fs-policy` 不存在时,每个 `fs/*` waterfall 落入其 `undefined` 默认值,`fs/observed` 无监听器: -- **read** 不变(它从不需要策略;只是 emit 了一个现在无人听取的 `fs/observed`)。 -- **write** 无条件 create-or-overwrite:`expected` 为 `undefined`,因此 `writeText` 无论文件是否存在、无论当前版本如何都直接写入。无读取前置要求,无版本检查。 -- **edit** 无条件替换文件当前内容中的字面文本:`expected` 为 `undefined`,因此 `editText` 不带版本守卫或读取前置要求即进行匹配和重写(`FS_EDIT_NOT_FOUND`/`FS_AMBIGUOUS_EDIT` 仍然适用——它们关乎字面匹配,而非新鲜度)。缺失的目标仍报告 `FS_STALE_VERSION`,与有守卫编辑路径的"此刻无法编辑该目标"错误码一致。 +- **read** 行为不变(它从不需要策略;只是 emit 了一个现在无人监听的 `fs/observed`)。 +- **write** 无条件 create-or-overwrite:`expected` 为 `undefined`,因此 `writeText` 无论文件是否存在、无论当前版本如何都直接写入。无先读要求,无版本检查。 +- **edit** 无条件替换文件当前内容中的字面文本:`expected` 为 `undefined`,因此 `editText` 无版本守卫、无先读要求地匹配并重写(`FS_EDIT_NOT_FOUND`/`FS_AMBIGUOUS_EDIT` 仍适用——它们关乎字面匹配,而非新鲜度)。缺失目标仍报告 `FS_STALE_VERSION`,与有守卫编辑路径的"此刻无法编辑该目标"错误码一致。 -两个变更操作仍然是原子的(后端的 per-target 锁是无条件的)。简单地*不存在*(而非丢失)的是 `dsh-fs-policy` 本会叠加的策略:观测状态、读后才能编辑、以及版本守卫的写入/编辑。加载 `dsh-fs-policy` 后,其监听器返回有守卫的 `expected` 值而非 `undefined`,从而叠加这些约束;裸提供方本身不变。 +两个 mutation 仍是原子的(后端的 per-target 锁是无条件的)。仅仅是*不存在*(而非丢失)的是 `dsh-fs-policy` 本会叠加的策略:观测状态、先读后编辑和版本守卫的写入/编辑。加载 `dsh-fs-policy` 后,其监听器返回有守卫的 `expected` 值而非 `undefined`,从而叠加这些约束;裸提供方本身无需任何变更。 -## 取代 +## 取代关系 -本 RFC 修正——而非撤销——[split-fs-seam RFC](../simplification/2026-06-26-fsspec-style-fs-seam.md)。四层拆分、提供方契约和新鲜度*策略*均保留。改变的是**工具与策略层之间的耦合方式**:一个强制方法服务变成了插件拥有的事件门禁,fs I/O + 读取窗口化从 `fileContext` 上移到了 `dsh-tool-fs`。split-fs-seam RFC 中关于 `dsh-tool-fs` 注入 `fileContext` 以及 `fileContext` 拥有 `read`/`write`/`edit` 的描述已在同一变更中更新。 +本 RFC 修正——而非推翻——[split-fs-seam RFC](../simplification/2026-06-26-fsspec-style-fs-seam.md)。四层拆分、提供方契约和新鲜度*策略*均保留。变更的是**工具与策略层之间的耦合方式**:强制性方法服务变为插件拥有的事件门控,fs I/O + 读取窗口从 `fileContext` 上移至 `dsh-tool-fs`。split-fs-seam RFC 中关于 `dsh-tool-fs` 注入 `fileContext` 以及 `fileContext` 拥有 `read`/`write`/`edit` 的描述已在同一变更中更新。 ## 验证 -测试固定了两条路径:无 `dsh-fs-policy` 时,根工具插件对 `dsh-fs-local` 启动,read、create、overwrite 和未读取的 edit 均成功;有策略时,未读取的 edit 返回 `FS_NOT_OBSERVED`,未读取的 overwrite 被 `createIfAbsent` 门控。策略做出决策后,后注册的 intent 监听器不会被触达。陈旧编辑通过提供方 CAS 失败,而策略不执行 `stat`;工具的预算在两条路径上均为 read 一次 `stat`、write 或 edit 零次 `stat`。面向模型的 schema 逐字节不变,因此快照不变。 +测试固定了两条路径:无 `dsh-fs-policy` 时,根工具插件对 `dsh-fs-local` 启动,read、create、overwrite 和未读 edit 均成功;有策略时,未读 edit 返回 `FS_NOT_OBSERVED`,未读 overwrite 被 `createIfAbsent` 门控。策略决定后,后注册的 intent 监听器不会被触达。陈旧编辑通过提供方 CAS 失败,而策略不执行 `stat`;工具预算在两条路径上保持 read 一次 `stat`、write 或 edit 零次 `stat`。面向模型的 schema 逐字节不变,因此快照不变。 ## 曾考虑的替代方案 -- **保留 `ctx.fileContext` 作为路径内方法服务**——[split-fs-seam RFC](../simplification/2026-06-26-fsspec-style-fs-seam.md) 最初落地的形态;否决,因为工具不加载策略层就无法运行,使策略对基本操作是承重的,而非可选的收紧。 -- **策略侧版本检查**(`dsh-fs-policy` 在其 waterfall 处理器中 stat 并比较)——否决,因为该检查与工具实际写入之间存在 TOCTOU 间隙;提供方的变更临界区是唯一无竞态的位置,因此策略只选择 CAS 基准并对先前观测进行门控。 -- **每工具 `/read`/`/write`/`/edit` 子路径插件**——实现时放弃。没有消费方需要单工具部署,且子路径发布迫使引入定制的 `tsdown`/`tsconfig`/`files`/workspace-constraint 处理,而同级的工具包都不需要这些;每工具的注册辅助函数保留为根插件组合的内部模块。 +- **保留 `ctx.fileContext` 作为关键路径上的方法服务**——[split-fs-seam RFC](../simplification/2026-06-26-fsspec-style-fs-seam.md) 最初落地的形态;否决,因为工具无法在没有策略层的情况下运行,使策略对基本操作是承重性的,而非可选的收紧。 +- **策略侧版本检查**(`dsh-fs-policy` 在其 waterfall 处理器中 stat 并比较版本)——否决,因为该检查与工具实际写入之间存在 TOCTOU 间隙;提供方的 mutation 临界区是唯一无竞态的位置,因此策略只选择 CAS 基准并对先前观测进行门控。 +- **每工具 `/read`/`/write`/`/edit` 子路径插件**——实现时放弃:没有消费方需要单工具部署,且子路径发布迫使引入兄弟工具包都不需要的定制 `tsdown`/`tsconfig`/`files`/workspace-constraint 处理;每工具的注册辅助函数仍作为根插件组合的内部模块保留。 ## 后果 -- **事件间接层取代方法调用。** 一次 waterfall + emit 不如 `await ctx.fileContext.edit(...)` 直接。收益是移除了工具对策略的方法依赖,同时保留默认策略插件;代价是多了一套事件词汇需要学习。通过将三个事件保持窄小并在每个事件上记录 default-thunk 语义来缓解。 -- **策略事件放在存储 seam 中。** `dsh-fs` 获得了两个版本决策事件加一个记录事件,尽管它"只是存储"。这是解耦的代价(发射方不能依赖策略插件)。这些事件只携带 `dsh-fs` 词汇加一个不透明的 `object` actor,不含面向模型的概念,因此 seam 不会沾染行窗口/观测策略类型和 agent/session owner 结构。 -- **单策略占位,按约定先到先得。** `fs/write-intent`/`fs/edit-intent` 槽位恰好容纳一个决策者;先注册(或 `prepend` 的)监听器获胜,其余被短路。`dsh-fs-policy` 占据该槽位是部署约定,而非事件强制的不变式——一个先注册的第二决策者会绕过它。这是可接受的,因为第二个 fs 版本策略决策者是配置错误,而非功能特性。如果未来出现*分层* fs 版本策略的需求,那是一个新 RFC(可组合的值传递 seam),而非在这些事件上静默添加第二个监听器。分层的权限/审计/沙箱拦截已有其归属:`tools/execute`。 -- **移除读取后的确认 stat** 使后续*有守卫*的编辑在读写竞争下偶尔快速失败(`FS_STALE_VERSION` → 重新读取)。这是丢失的 UX 便利,从不是正确性漏洞;提供方锁仍然阻止基于错误版本的写入。 -- **裸提供方不做读后写入/编辑检查,也不做版本检查。** 不加载 `dsh-fs-policy` 的部署允许模型无条件覆写或编辑任何现有文件。这正是保持工具独立于策略服务的刻意含义:安全纪律存在于 `dsh-fs-policy` 插件中。省略它的部署是有意选择无约束的文件系统;这不是发布 fs 工具的配置的预期姿态。 +- **事件间接层取代方法调用。** 一次 waterfall + emit 不如 `await ctx.fileContext.edit(...)` 直接。收益是移除了工具到策略的方法依赖,同时保留默认策略插件;代价是多一套事件词汇需要学习。通过保持三个事件的窄小范围并在每个事件上记录 default-thunk 语义来缓解。 +- **策略事件位于存储 seam 中。** `dsh-fs` 增加了两个版本决策事件和一个记录事件,尽管它"只是存储"。这是解耦的代价(发射方不能依赖策略插件)。这些事件只携带 `dsh-fs` 词汇加一个不透明的 `object` actor,不携带面向模型的概念,因此 seam 不沾染行窗口/观测策略类型,也不沾染 agent/session owner 结构。 +- **单一策略占位者,按约定先到先得。** `fs/write-intent`/`fs/edit-intent` 槽位恰好容纳一个决策者;先注册(或 `prepend`)的监听器获胜,其余被短路。`dsh-fs-policy` 占据该槽位是部署约定,而非事件系统强制的不变式——一个先注册的第二决策者会绕过它。这是可接受的,因为第二个 fs 版本策略决策者是配置错误,而非功能。如果未来出现*分层* fs 版本策略的需求,那是一个新 RFC(可组合的值传递 seam),而非在这些事件上静默添加第二个监听器。分层的权限/审计/沙箱拦截已有其归属:`tools/execute`。 +- **移除读后确认 stat** 使后续*有守卫*的编辑在 read/write 竞争下偶尔快速失败(`FS_STALE_VERSION` → 重新读取)。这是丢失的 UX 便利,绝非正确性漏洞;提供方锁仍阻止基于错误版本的写入。 +- **裸提供方不做先读后写/编辑,也不做版本检查。** 没有 `dsh-fs-policy` 的部署允许模型无条件覆写或编辑任何已有文件。这正是保持工具独立于策略服务的有意含义:安全纪律存在于 `dsh-fs-policy` 插件中。省略它的部署是有意选择无约束的文件系统;对于发布 fs 工具的配置而言,这不是预期的姿态。 diff --git a/docs/rfc/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.i18n.yaml b/docs/rfc/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.i18n.yaml index 041271d286..3bb8d3e607 100644 --- a/docs/rfc/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.i18n.yaml +++ b/docs/rfc/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-30-bash-stdin-env-trusted-plugin-surface.md: 72aae03361cbc088cf64f3548a43ac6253eb21eb -2026-06-30-bash-stdin-env-trusted-plugin-surface.zh.md: 5c708cb6bfed28b2164cbd1d0b1c7368bf3e1d07 +2026-06-30-bash-stdin-env-trusted-plugin-surface.zh.md: f661199048b7eaa359f792e96ac52baf8cd61fdf diff --git a/docs/rfc/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.zh.md b/docs/rfc/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.zh.md index 5c708cb6bf..f661199048 100644 --- a/docs/rfc/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.zh.md +++ b/docs/rfc/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.zh.md @@ -1,4 +1,4 @@ -# RFC:bash seam 上的 stdin 与额外 env +# RFC:在 bash seam 上支持 stdin 与额外 env Status: implemented @@ -6,28 +6,28 @@ Status: implemented ## 问题 -钩子子系统运行外部钩子命令的方式与 Claude Code 和 Codex 相同:一个钩子就是一条 shell 命令,通过 **stdin 上的 JSON** 接收事件载荷,并从若干**环境变量**(`CLAUDE_PROJECT_DIR`、`CLAUDE_PLUGIN_ROOT`、`PLUGIN_ROOT`……)读取上下文。harness 在 `ctx.bash` 能力 seam 背后已经有一个完善的命令运行器([dsh-bash](../../../../packages/bash/bash) → [dsh-bash-local](../../../../packages/bash/bash-local)),具备进程组 kill、输出截断/溢出处理和凭证擦除。将它复用于钩子执行,意味着钩子桥接层无需重新实现子进程管道——但该 seam 此前没有写入 stdin 或设置额外 env 的能力。本 RFC 添加这两项输入。 +钩子子系统以 Claude Code 和 Codex 的方式运行外部钩子命令:钩子是一条 shell 命令,通过 **stdin 上的 JSON** 接收事件载荷,并从若干**环境变量**(`CLAUDE_PROJECT_DIR`、`CLAUDE_PLUGIN_ROOT`、`PLUGIN_ROOT`……)读取上下文。harness 已经在 `ctx.bash` 能力 seam 后面有一个完善的命令执行器([dsh-bash](../../../../packages/bash/bash) → [dsh-bash-local](../../../../packages/bash/bash-local)),具备进程组终止、输出截断/溢出处理和凭证擦除功能。复用它来执行钩子意味着钩子桥接层无需重新实现子进程管道——但该 seam 此前无法写入 stdin 或设置额外 env。本 RFC 添加这两个输入。 -`stdin` 和 `env` 不构成新的模型能力,因为普通 shell 语法已经能提供这两者。环境中的凭证由 `dsh-bash-local` 的子进程环境擦除机制保护,而非靠隐藏这些 seam 字段;模型工具参数是静态 JSON,不会展开 shell 变量。因此这些字段服务于受信的进程内调用方(如钩子桥接层),它们需要传递结构化输入和 `CLAUDE_*` 变量,而不必将其嵌入模型可见的 shell 文本。环境变量规则见 [defensive-patterns.md](../../../defensive-patterns.md)。 +`stdin` 和 `env` 不构成新的模型能力,因为普通 shell 语法已经能提供两者。环境凭证由 `dsh-bash-local` 的子环境擦除机制保护,而非靠隐藏这些 seam 字段;模型工具参数是静态 JSON,不会展开 shell 变量。因此这些字段服务于受信的进程内调用方(如钩子桥接层),它们需要传递结构化输入和 `CLAUDE_*` 变量,而不必将其嵌入模型可见的 shell 文本。环境变量规则见 [defensive-patterns.md](../../../defensive-patterns.md)。 ## 决策 -在 `BashExecRequest`(面向模型/插件的请求)和 `BashExecSpec`(`run`/`start` 实际执行的解析后规格)上**同时**添加 `stdin?: string` 与 `env?: Record`,并在 `dsh-bash-local` 中贯穿:`resolve()` 原样传递,`run()`/`start()` 将它们传给 `runBash`,后者把字节写入子进程的 stdin 并合并额外 env。 +在 `BashExecRequest`(模型/插件侧请求)和 `BashExecSpec`(`run`/`start` 所作用的已解析 spec)上**同时**添加 `stdin?: string` 与 `env?: Record`,并在 `dsh-bash-local` 中贯穿它们:`resolve()` 原样传递,`run()`/`start()` 将其传给 `runBash`,后者把字节写入子进程的 stdin 并合并额外 env。 -三个刻意的选择: +三个有意为之的选择: -1. **面向模型的工具不暴露 `stdin` 和 `env`。** Shell 语法已经覆盖这些需求,重复的参数只会增加接口面而不带来权限隔离。工具仅从声明的模型参数、signal 和 owner 构建请求;受信的进程内调用方可以直接设置 seam 字段。 +1. **模型侧工具不暴露 `stdin` 和 `env`。** Shell 语法已覆盖这些需求,重复参数只会增加接口面而不带来权限隔离。工具仅从声明的模型参数、signal 和 owner 构建请求;受信的进程内调用方可以直接设置 seam 字段。 -2. **`env` 在凭证擦除之后合并,因此调用方显式设置的条目总是胜出**——即使名称看起来像凭证。这是正确的,因为擦除的职责很窄:阻止 harness 自身 *ambient* `process.env` 中的凭证泄漏到子命令中。调用方显式设置一个变量时,它命名的是自己已持有的值(而非 ambient 密钥),因此擦除不是对它的约束。`childEnv(extra?)` 的分层为 `scrub(process.env)` → `ENV_OVERRIDES`(面向模型的 `TERM=dumb` 等)→ `extra`,后者优先。 +2. **`env` 在凭证擦除之后合并,因此调用方显式设置的条目总是胜出**——即使键名与凭证同形。这是正确的,因为擦除的职责很窄:阻止 harness 的*环境* `process.env` 凭证泄漏到被 spawn 的命令中。调用方显式设置一个变量时,它命名的是自己已持有的值(而非环境中的秘密),因此擦除不构成对它的约束。`childEnv(extra?)` 按 `scrub(process.env)` → `ENV_OVERRIDES`(对模型友好的 `TERM=dumb` 等)→ `extra` 的顺序分层,后者优先。 -3. **`stdin`/`env` 在解析后规格上是 required-absent-OK(普通 optional),而非像 `owner` 那样 required-but-nullable。** `owner` 之所以是 required-but-nullable,是因为*静默*缺失的 owner 会产生一个无主的、跨会话可读的任务——这是一个安全隐患,显式的 `undefined` 可以防范。`stdin`/`env` 没有这种风险:缺失意味着「无 stdin / 无额外 env」,这是安全的常规情况(所有模型驱动的调用都如此)。因此它们保持普通 optional,与 `signal` 一致。 +3. **`stdin`/`env` 在已解析 spec 上是 required-absent-OK(普通 optional),而非像 `owner` 那样 required-but-nullable。** `owner` 之所以是 required-but-nullable,是因为*静默*缺失的 owner 会产生一个无主、跨会话可读的任务——一个安全隐患,显式的 `undefined` 可以防范。`stdin`/`env` 没有这种风险:缺失意味着「无 stdin / 无额外 env」,这是安全的常规情况(所有模型驱动的调用都如此)。因此它们保持普通 optional,与 `signal` 一致。 -`dsh-bash-local` 仅在提供了字节时才创建 stdin 管道;否则 fd 0 保持 `/dev/null`,维持原有行为。它写入字节后关闭管道。如果子进程未读取就退出导致 `EPIPE`,则忽略该错误,因为命令退出状态和输出决定结果。 +`dsh-bash-local` 仅在有字节需要写入时才创建 stdin 管道;否则 fd 0 仍为 `/dev/null`,保持先前行为。它写入字节后关闭管道。子进程未读取即退出时产生的 `EPIPE` 被忽略,因为命令退出码和输出决定结果。 ## 曾考虑的替代方案 -**可配置的 ambient 密钥擦除。** 否决,属于推测性需求。受信调用方可以在擦除之后显式提供所需值,无需削弱默认的 ambient 保护。 +**可配置的环境秘密擦除。** 否决,属于推测性需求。受信调用方可以在擦除之后显式提供所需值,无需削弱默认的环境保护。 ## 后果 -钩子桥接层通过既有的 bash seam 传递 JSON 载荷和钩子专属变量,保留其进程组管理、截断和溢出行为。模型接口面不变,bash 工具仍是模型调用请求构建的唯一入口。相关词汇定义见 [bash 数据结构参考](../../../core-data-structures/bash.md)。 +钩子桥接层通过既有的 bash seam 传递 JSON 载荷和钩子特定变量,保留其进程组终止、截断和溢出行为。模型接口面不变,bash 工具仍是模型调用请求构建的唯一所有者。相关词汇定义见 [bash 数据结构参考](../../../core-data-structures/bash.md)。 diff --git a/docs/rfc/implemented/architecture/2026-06-30-event-domain-semantics.i18n.yaml b/docs/rfc/implemented/architecture/2026-06-30-event-domain-semantics.i18n.yaml index 557ac2bab4..2416919ab7 100644 --- a/docs/rfc/implemented/architecture/2026-06-30-event-domain-semantics.i18n.yaml +++ b/docs/rfc/implemented/architecture/2026-06-30-event-domain-semantics.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-30-event-domain-semantics.md: e05c238c52052454d3e01e82767cddd9af316a9d -2026-06-30-event-domain-semantics.zh.md: a5453824183aa3f71486b5dbd24ed8c056d9c854 +2026-06-30-event-domain-semantics.zh.md: 9048679ec7a7992852cce76bf43269f5499da792 diff --git a/docs/rfc/implemented/architecture/2026-06-30-event-domain-semantics.zh.md b/docs/rfc/implemented/architecture/2026-06-30-event-domain-semantics.zh.md index a545382418..9048679ec7 100644 --- a/docs/rfc/implemented/architecture/2026-06-30-event-domain-semantics.zh.md +++ b/docs/rfc/implemented/architecture/2026-06-30-event-domain-semantics.zh.md @@ -1,4 +1,4 @@ -# RFC:事件域语义——session 是事实日志,agent 是实时表面 +# RFC:事件域语义——session 是事实日志,agent 是运行时表面 Status: implemented @@ -6,34 +6,34 @@ Status: implemented ## 问题 -harness 通过 Cordis 事件分类体系扩展 agent loop(智能体循环)(见[微内核事件分类体系 RFC](2026-06-11-microkernel-event-taxonomy.md))。随着分类体系的增长,三个事件域之间的界限变得模糊: +harness 通过 Cordis 事件分类体系扩展 agent loop(智能体循环)(见[微内核事件分类体系 RFC](2026-06-11-microkernel-event-taxonomy.md))。随着该分类体系的增长,三个事件域之间的界限变得模糊: - `session/*` 承载持久的、事件溯源的日志(`SessionEventMap`)。 -- `agent/*` 承载实时运行时信号,向插件传递 `Agent` 句柄。 +- `agent/*` 承载运行时实时信号,向插件传递 `Agent` 句柄。 - `tools/*` 承载工具注册表与执行 seam。 -两个问题促使我们明确固定这些语义。第一,若干轮次/步骤边界同时以持久的 `SessionEvent`(`turn/start`、`turn/end`、`step/start`、`step/end`)和镜像的 `agent/*` emit(`agent/turn-start`、`agent/turn-end`、`agent/step-start`、`agent/step-end`)两种形式存在。消费方对同一事实有两个真源,每次生命周期变更都必须同时更新两处。第二,即将到来的 Hooks 子系统需要一个统一、有文档的订阅表面:插件作者(以及基于其上构建的 Claude Code / Codex 钩子桥接)必须无需阅读循环代码就能判断应该监听会话事件还是 agent 事件,以及为什么。 +两个问题促使我们固定语义。第一,若干轮次/步骤边界同时作为持久的 `SessionEvent`(`turn/start`、`turn/end`、`step/start`、`step/end`)**和**镜像的 `agent/*` emit(`agent/turn-start`、`agent/turn-end`、`agent/step-start`、`agent/step-end`)存在。消费方对同一事实有两个真源,每次生命周期变更都必须同时更新两处。第二,即将到来的 Hooks 子系统需要**一个**连贯且有文档的订阅表面——插件作者(以及基于其上构建的 Claude Code / Codex 钩子桥接)必须在不阅读循环代码的情况下知道应该监听 session 事件还是 agent 事件,以及原因。 -这套词汇是拦截决策、持久的 `hook/*` 日志,以及 Claude Code 与 Codex 桥接的基础。 +这套词汇是拦截决策、持久的 `hook/*` 日志,以及 Claude Code 和 Codex 桥接的基础。 ## 决策 -**三个域,各司其职,一条边界规则。** +**三个域,各司其职,以一条边界规则统一。** -- **`session/*`——持久的、可回放的事实日志。** 拥有 `SessionEventMap`;每条记录仅含 JSON(无活对象)。每次追加触发一次 `session/event` emit,加上 `session/flush` 并行持久性检查点。它同时也是实时 transcript(文本记录)流:想要渲染或响应已发生事件的消费方在此订阅,因此实时渲染与 `session/load` 回放共享同一路径。 -- **`agent/*`——实时运行时表面。** 始终携带活的 `Agent`。两种形态:拦截型 waterfall(瀑布式事件)(`agent/request`、`agent/step-result`、`agent/turn-continuation`)可修改或否决,以及瞬态 emit(`agent/status`、`agent/error`、`agent/created`/`agent/disposed`、`agent/queued`)在持有 `Agent` 的情况下通知。轮次和步骤的**边界**不在此域——它们是持久的会话事件,从 `session/event` 读取;token 流(`assistant/chunk`)和中途引导(`steering/message`)同理。 +- **`session/*`——持久的、可回放的事实日志。** 拥有 `SessionEventMap`;每条记录仅含 JSON(无活对象)。每次追加触发一次 `session/event` emit,加上 `session/flush` 并行持久性检查点。它同时也是实时 transcript(文本记录)源:想渲染或响应已发生事件的消费方在此订阅,因此实时渲染与 `session/load` 回放共享同一路径。 +- **`agent/*`——运行时实时表面。** 始终携带活的 `Agent`。两种形态:拦截 waterfall(瀑布式事件)(`agent/request`、`agent/step-result`、`agent/turn-continuation`)可变更或否决;瞬态 emit(`agent/status`、`agent/error`、`agent/created`/`agent/disposed`、`agent/queued`)在持有 `Agent` 的情况下通知。轮次和步骤**边界**不在此处——它们是持久的 session 事件,从 `session/event` 读取;token 流(`assistant/chunk`)和中途 steering(中途引导)(`steering/message`)同理。 - **`tools/*`——工具注册表与执行 seam。** -**边界规则:** 持久的、可回放的事实是 `SessionEvent`;实时拦截或瞬态/活对象信号是 `agent`/`tools` Cordis 事件。轮次或步骤边界是持久事实,因此存在于会话日志中、从 `session/event` 流读取——不会被镜像为 `agent/*` emit。 +**边界规则:** 持久的、可回放的事实是 `SessionEvent`;实时拦截或瞬态/活对象信号是 `agent`/`tools` Cordis 事件。轮次或步骤边界是持久事实,因此存在于 session 日志中并从 `session/event` 源读取——不会被镜像为 `agent/*` emit。 -**将规则应用于边界镜像:** 全部四个边界镜像——`agent/turn-start`、`agent/turn-end`、`agent/step-start`、`agent/step-end`——被**移除**。没有生产消费方需要在边界处持有活的 `Agent`:ACP 桥接从 `session/event` 的 `turn/end` 加 `agent/status` 结算;唯一的 turn 镜像消费方(`dsh-ui-stdio`,一个一次性测试 REPL)已迁移为从 `session/event` 渲染边界,通过 `agent/created`→id 映射恢复简短的 agent 标签。step 镜像先被移除(它们根本没有消费方);turn 镜像在 ui-stdio 迁移后随之移除——见[移除边界镜像事件 RFC](../simplification/2026-06-20-remove-agent-boundary-mirror-events.md),该决策由它拥有。移除这些 emit 也简化了循环的 `closeStep`/`closeTurn`(各只需一次 append,无需配对 emit)。 +**将规则应用于边界镜像:** 全部四个边界镜像——`agent/turn-start`、`agent/turn-end`、`agent/step-start`、`agent/step-end`——被**移除**。没有生产消费方需要在边界处获取活的 `Agent`:ACP 桥接从 `session/event` 的 `turn/end` 加 `agent/status` 结算;唯一的 turn 镜像消费方(`dsh-ui-stdio`,一个一次性测试 REPL)已迁移为从 `session/event` 渲染边界,通过 `agent/created`→id 映射恢复简短的 agent 标签。step 镜像先被移除(它们完全没有消费方);turn 镜像在 ui-stdio 迁移后随之移除,见[移除边界镜像事件 RFC](../simplification/2026-06-20-remove-agent-boundary-mirror-events.md),该决策由它负责。移除 emit 也简化了循环的 `closeStep`/`closeTurn`(各只需一次 append,无需配对 emit)。 ## 后果 -- 循环不再 emit 任何边界镜像;`closeStep` 仅追加 `step/end`,`closeTurn` 仅追加 `turn/end`。`Session.append` 负责 post-commit observer 的隔离,因此抛出异常的边界 observer 无法改变轮次结果或饿死后续消费方;acceptance 或内部校验失败仍会在边界进入日志之前逃逸。 -- 之前通过已移除 emit 观察边界的测试现在观察持久的 `turn/start`/`turn/end`/`step/start`/`step/end` 会话事件——它们所固定的行为(边界排序、步骤计数)不变;只是读取的流切换到了权威的那一个。那些测试「抛出异常的 turn 边界 emit 监听器」的用例被删除,因为该代码路径已不存在(没有 emit 可供抛出)。按照 [AGENTS.md "tests document behavior, not golden truth"](../../../../AGENTS.md),行为与其测试一起迁移(或一起消亡)。 -- 循环仅在 `append('step/start')` 返回后才标记步骤为已打开(`stepOpen = true`)。内部 dispatch 校验在日志推送前运行,可能在不打开步骤的情况下拒绝;post-commit `session/event` observer 的失败被隔离在 `Session.append` 内部。因此该标记精确代表已提交的边界,该边界欠一个后续的 `step/end`。 -- 本 RFC 的完整实现是[简化 RFC「停止将持久边界镜像为 agent 事件」](../simplification/2026-06-20-remove-agent-boundary-mirror-events.md):全部四个边界镜像被移除,所有消费方从 `session/event` 读取边界。`agent/steering`(不是边界镜像)不在该 RFC 范围内,由其后续 RFC [移除 `agent/steering` 镜像 emit](../simplification/2026-07-04-remove-agent-steering-mirror.md) 单独移除——它镜像的是持久的 `steering/message`。 +- 循环不再 emit 任何边界镜像;`closeStep` 仅追加 `step/end`,`closeTurn` 仅追加 `turn/end`。`Session.append` 负责 post-commit observer 隔离,因此抛出异常的边界 observer 无法改变轮次结果或饿死后续消费方;接受或内部校验失败仍会在边界进入日志之前逃逸。 +- 之前通过已移除 emit 观察边界的测试,现在观察持久的 `turn/start`/`turn/end`/`step/start`/`step/end` session 事件——它们固定的行为(边界顺序、步骤计数)不变;只是读取的源移到了规范源。那些测试「抛出异常的 turn 边界 emit 监听器」的用例被删除,因为该代码路径不再存在(没有 emit 可供抛出)。按照 [AGENTS.md「测试记录行为,而非黄金真相」](../../../../AGENTS.md),行为与其测试一同迁移(或一同消亡)。 +- 循环仅在 `append('step/start')` 返回后才标记步骤已打开(`stepOpen = true`)。内部分发校验在日志推入之前运行,可能在不打开步骤的情况下拒绝;post-commit `session/event` observer 的失败被隔离在 `Session.append` 内部。因此该标记精确表示已提交的、欠一个后续 `step/end` 的边界。 +- 完整实现见[简化 RFC「停止将持久边界镜像为 agent 事件」](../simplification/2026-06-20-remove-agent-boundary-mirror-events.md):全部四个边界镜像被移除,所有消费方从 `session/event` 读取边界。`agent/steering`(不是边界镜像)不在该 RFC 范围内,由其后续 RFC [移除 `agent/steering` 镜像 emit](../simplification/2026-07-04-remove-agent-steering-mirror.md) 单独移除——它镜像的是持久的 `steering/message`。 - Cordis 事件目录(`docs/cordis-catalog/events.md`)重新生成以移除镜像事件。 diff --git a/docs/rfc/implemented/architecture/2026-07-02-fs-per-session-cwd.i18n.yaml b/docs/rfc/implemented/architecture/2026-07-02-fs-per-session-cwd.i18n.yaml index fc9d825dc0..3cff8c7aca 100644 --- a/docs/rfc/implemented/architecture/2026-07-02-fs-per-session-cwd.i18n.yaml +++ b/docs/rfc/implemented/architecture/2026-07-02-fs-per-session-cwd.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-02-fs-per-session-cwd.md: 00643955d918dff87241f4b240bb7e6774d21a0b -2026-07-02-fs-per-session-cwd.zh.md: ca37e1f41af53c43151ac82e1be1b76eaafdb97e +2026-07-02-fs-per-session-cwd.zh.md: 73176cde3747a2eb8c03aadbf3f419bf27173d70 diff --git a/docs/rfc/implemented/architecture/2026-07-02-fs-per-session-cwd.zh.md b/docs/rfc/implemented/architecture/2026-07-02-fs-per-session-cwd.zh.md index ca37e1f41a..73176cde37 100644 --- a/docs/rfc/implemented/architecture/2026-07-02-fs-per-session-cwd.zh.md +++ b/docs/rfc/implemented/architecture/2026-07-02-fs-per-session-cwd.zh.md @@ -1,34 +1,34 @@ -# RFC:将文件系统路径解析基于调用方的会话 cwd - -Status: implemented +# RFC:相对文件系统路径按调用方的会话 cwd 解析 [English](2026-07-02-fs-per-session-cwd.md) | 中文 +Status: implemented + ## 问题 -ACP 桥接层为每个会话提供独立的工作区:`session/new` 将编辑器的项目目录记录为 `SessionHeader.cwd`,`dsh-tool-bash` 将每次 bash 调用的 `workdir` 默认设为调用方 agent 的 `session.header.cwd`(见 [`packages/ui/acp`](../../../../packages/ui/acp) 中的 per-session cwd RFC 相关工作,以及 `dsh-tool-bash` 中的 `resolveWorkdir`)。因此会话 A 中的 bash 命令在 A 的项目目录运行,会话 B 中的在 B 的项目目录运行——一个服务器进程,N 个工作区。 +ACP(Agent Client Protocol)桥接层为每个会话提供独立的工作区:`session/new` 将编辑器的项目目录记录为 `SessionHeader.cwd`,`dsh-tool-bash` 将每次 bash 调用的 `workdir` 默认设为调用方 agent(智能体)的 `session.header.cwd`(见 [`packages/ui/acp`](../../../../packages/ui/acp) 中的 per-session cwd RFC 工作与 `dsh-tool-bash` 中的 `resolveWorkdir`)。因此会话 A 中的 bash 命令在 A 的项目目录执行,会话 B 中的在 B 的项目目录执行——一个服务器进程,N 个工作区。 -文件系统路径解析使用的是插件加载时的单一 cwd,而 bash 使用的是会话的项目目录。因此,当编辑器项目目录与服务器启动目录不同时,相对路径的解析结果就会不一致;快照测试因为让这两个路径相同而掩盖了这个 bug。 +文件系统解析使用的是插件加载时的 cwd,而 bash 使用的是会话的项目目录。因此,当编辑器项目目录与服务器启动目录不同时,相对路径的解析结果就会不一致;快照测试因为让这两个路径相同而掩盖了这个 bug。 ## 决策 -将调用方的会话 cwd 透传到路径解析中,与 `dsh-tool-bash` 对 `workdir` 的处理方式完全一致。**调用方**(即工具)提供 cwd;提供方不读取会话或 agent。 +将调用方的会话 cwd 传入路径解析,与 `dsh-tool-bash` 对 `workdir` 的处理方式完全一致。**调用方**(即工具)提供 cwd;提供方不读取会话或 agent。 -- `FileSystem.resolve` 扩展为 `resolve(path: string, opts?: { cwd?: string }): Promise`。`opts.cwd` 是相对 `path` 的解析基准;绝对 `path` 忽略它;省略 `opts.cwd` 时使用后端自身的默认值。使用 options 对象(而非位置参数 `cwd?`)为将来的解析提示留出空间,无需再次变更签名。 -- `dsh-fs-local.resolve` 使用 `resolveLocalTarget(opts?.cwd ?? this.config.cwd, path)`。`config.cwd` 仍是调用方未提供 cwd 时的默认值(非 ACP/无会话场景,以及 `process.cwd()` 本身就是工作区的单会话 stdio 演示)。 -- `dsh-tool-fs` 的 `read`/`write`/`edit` 通过共享的 `sessionCwd(exec)` 辅助函数获取会话 cwd(`exec.agent?.session.header.cwd`,与 bash 的 `resolveWorkdir` 一致),并传给 `resolve`。非 agent/无 header 的调用方返回 `undefined`,后端则应用其默认值。 +- `FileSystem.resolve` 扩展为 `resolve(path: string, opts?: { cwd?: string }): Promise`。`opts.cwd` 是相对 `path` 解析时的基准目录;绝对 `path` 忽略它;省略 `opts.cwd` 则使用后端自身的默认值。采用 options 对象(而非位置参数 `cwd?`)为将来的解析提示留出空间,无需再次变更签名。 +- `dsh-fs-local.resolve` 使用 `resolveLocalTarget(opts?.cwd ?? this.config.cwd, path)`。`config.cwd` 仍作为调用方未提供 cwd 时的默认值(非 ACP/无会话场景,以及 `process.cwd()` 本身就是工作区的单会话 stdio 演示)。 +- `dsh-tool-fs` 的 `read`/`write`/`edit` 通过共享的 `sessionCwd(exec)` 辅助函数(`exec.agent?.session.header.cwd`,与 bash 的 `resolveWorkdir` 对应)获取会话 cwd,并传给 `resolve`。非 agent/无 header 的调用方得到 `undefined`,后端因此应用其默认值。 ## 曾考虑的替代方案 -### 为什么由调用方提供 cwd(而非提供方) +### 为何由调用方(而非提供方)提供 cwd -提供方 seam 不应依赖 `dsh-agent`/`dsh-session`:它是一个文本存储后端,沙箱或远程实现同样满足该接口,而它们没有「agent 会话」的概念。工具已经接收到 `ToolExecution`(`exec`),其中携带了 agent,因此工具是将 `exec → cwd` 投影并向提供方传递一个纯字符串的正确位置。这遵循「包边界处显式优于隐式」的约定:基目录作为显式参数到达提供方并由其执行,而非让提供方越界去读取它不应知道的会话。这也与 `dsh-tool-bash` 一一对应,使两个面向模型的文件操作接口以相同方式解析路径。 +提供方 seam 不得依赖 `dsh-agent`/`dsh-session`——它是一个文本存储后端,沙箱或远程实现同样满足该接口,而这些实现没有「agent 会话」的概念。工具已经接收了 `ToolExecution`(`exec`),其中携带 agent,因此工具是将 `exec → cwd` 投影并向提供方传递一个纯字符串的正确位置。这遵循「包(package)边界处显式优于隐式」的约定:基准目录作为显式参数传入,提供方据此行动,而非让提供方越界去读取它不应知晓的会话。这也与 `dsh-tool-bash` 一一对应,使两个面向模型的文件操作接口以相同方式解析路径。 -默认值只存在于**一个**地方:提供方的 `config.cwd`。`sessionCwd` 在没有会话时返回 `undefined` 而非 `process.cwd()`,因此工具永远不会制造一个提供方本来会自行选择的基目录。 +默认值只存在于**一个**地方——提供方的 `config.cwd`。`sessionCwd` 在没有会话时返回 `undefined` 而非 `process.cwd()`,因此工具永远不会自行制造一个提供方本应自行选择的基准目录。 ## 后果 -- 在 ACP 演示中,fs 工具和 bash 现在对每个会话的工作区达成一致;编辑器可以打开任意项目文件夹,两类工具都在该目录下工作。 -- `FsTarget` 的标识不变:`targetKey` 仍然是解析后绝对路径的 realpath,因此 observed-state 键控和符号链接标识不受影响——正确的 per-session cwd 产生的 key 与 bash 目标一致。 +- 在 ACP 演示中,fs 工具与 bash 现在对每个会话的工作区达成一致;编辑器可以打开任意项目目录,两类工具都在该目录下操作。 +- `FsTarget` 的标识不变:`targetKey` 仍为解析后绝对路径的 realpath,因此 observed-state 键控与符号链接标识不受影响——正确的 per-session cwd 产生与 bash 目标相同的 key。 - 向后兼容:所有现有的 `resolve(path)` 调用(均在测试中)继续正常工作;新参数是可选的。 - 单会话 stdio 演示不受影响:它不提供会话 cwd(其 agent 的会话没有 `cwd`),因此解析回退到 `config.cwd = process.cwd()`,即工作区本身。 diff --git a/docs/rfc/implemented/architecture/2026-07-02-result-time-applied-hunk-diffs.i18n.yaml b/docs/rfc/implemented/architecture/2026-07-02-result-time-applied-hunk-diffs.i18n.yaml index b5dcdf42f8..0401c48e08 100644 --- a/docs/rfc/implemented/architecture/2026-07-02-result-time-applied-hunk-diffs.i18n.yaml +++ b/docs/rfc/implemented/architecture/2026-07-02-result-time-applied-hunk-diffs.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-02-result-time-applied-hunk-diffs.md: 81ab8b9827ddaec39d63e2a3f8fbb864085a9ac9 -2026-07-02-result-time-applied-hunk-diffs.zh.md: 3914bb872025d7116a047dfaf3455f527e35879a +2026-07-02-result-time-applied-hunk-diffs.zh.md: 2914c4242c4246ede8588967ed36b3f6c725c607 diff --git a/docs/rfc/implemented/architecture/2026-07-02-result-time-applied-hunk-diffs.zh.md b/docs/rfc/implemented/architecture/2026-07-02-result-time-applied-hunk-diffs.zh.md index 3914bb8720..2914c4242c 100644 --- a/docs/rfc/implemented/architecture/2026-07-02-result-time-applied-hunk-diffs.zh.md +++ b/docs/rfc/implemented/architecture/2026-07-02-result-time-applied-hunk-diffs.zh.md @@ -1,20 +1,20 @@ # RFC:结果时刻的 applied-hunk diff 用于文件变更 -Status: implemented - [English](2026-07-02-result-time-applied-hunk-diffs.md) | 中文 +Status: implemented + ## 问题 -[带标签的渲染意图联合类型](2026-07-02-tool-render-intent-union.md)为 `dsh-tool-fs` 的 write/edit 在调用时(CALL time)提供了 `card:'diff'`,纯粹从工具参数推导:write ⇒ `{oldText:null, newText:content}`(整个新文件),edit ⇒ `{oldText:old_string, newText:new_string}`(裸替换片段)。编辑器将其渲染为行内 diff,但这是一个**无上下文**的 diff:裸的 `old_string`→`new_string` 没有周围行,而一次 `replace_all` 如果触及五个分散位置,仍然只渲染为一对片段。 +[tagged render-intent union](2026-07-02-tool-render-intent-union.md) 为 `dsh-tool-fs` 的 write/edit 在调用时刻提供了 `card:'diff'`,纯粹从工具参数推导:write ⇒ `{oldText:null, newText:content}`(整个新文件),edit ⇒ `{oldText:old_string, newText:new_string}`(裸替换片段)。编辑器将其渲染为行内 diff,但这是一个**无上下文**的 diff:裸的 `old_string`→`new_string` 没有周围行,而一次触及五个分散位置的 `replace_all` 仍然渲染为一对片段。 -驱动 `claude-agent-acp` 自身的 ACP 桥接层可以看到完整编辑器 diff 的样子:变更应用后,它发出第二个 `tool_call_update`,其 diff 是**带 ±3 行上下文的 applied hunk**(`replace_all` 的每个变更位置各一个 hunk),由工具的 `structuredPatch` 重建。这个结果时刻的 hunk 正是让 Zed 在文件中*原地*展示变更(而非浮动片段)的关键。我们的工具止步于调用时片段;完成后的结果只携带纯文本 "updated successfully",没有 diff。 +在对接 `claude-agent-acp` 自身的 ACP(Agent Client Protocol) bridge 时可以看到完整编辑器 diff 的样子:变更应用后,它发出第二个 `tool_call_update`,其 diff 是**带 ±3 行上下文的 applied hunk**(`replace_all` 的每个变更位置各一个 hunk),由工具的 `structuredPatch` 重建。这个结果时刻的 hunk 正是让 Zed 在文件中**原位**显示变更(而非浮动片段)的关键。我们的工具止步于调用时刻的片段;完成后的结果只携带纯文本 "updated successfully",没有 diff。 -障碍在于一个 seam 边界:`presentResult(args, result)` 是 **`args` + 面向模型的 `result`(`{content, isError}`)的纯函数**——它在实时流式输出和会话日志回放时都会运行,因此必须具有回放确定性且不能做 I/O。它看不到文件的变更前/后内容,而 `FsEditOutcome`/`FsWriteOutcome` 只携带替换计数 + 版本,没有文本。因此既无法计算、也无法传递 applied hunk 给 presenter。 +障碍在于一个 seam 边界:`presentResult(args, result)` 是 **`args` + 面向模型的 `result`(`{content, isError}`)的纯函数**——它在实时流式输出和会话日志回放中都会运行,因此必须具备回放确定性且不能做 I/O。它看不到文件的前后内容,而 `FsEditOutcome`/`FsWriteOutcome` 只携带替换计数和版本号,没有文本。因此无法计算——甚至无法携带——applied hunk 给 presenter。 ## 决策 -新增一个**持久化的、工具私有的展示通道**,使工具的 `execute` 能附加一个结果时刻的渲染载荷并在回放中存活,并用它来承载 applied-hunk diff。 +添加一个**持久化的、工具私有的展示通道**,使工具的 `execute` 能附加一个结果时刻的渲染载荷并在回放中存活,并用它来携带 applied-hunk diff。 ### 1. 工具结果上的 `meta` 通道(core) @@ -24,38 +24,38 @@ Status: implemented type ToolExecuteReturn = ContentBlock[] | { content: ContentBlock[]; meta?: unknown } ``` -`meta` 是工具自有的 `unknown`,core 持久化它但不解释。`Session.append` 拒绝非 JSON 值,回放时将存储的载荷传回 `presentResult`;因此展示无需 I/O 或重新计算即可复现。运行时校验避免了向 tools core 添加共享的 serializable-value 依赖。 +`meta` 是工具自有的 `unknown`,core 持久化但不解释。`Session.append` 拒绝非 JSON 值,回放时将存储的载荷回传给 `presentResult`;因此展示无需 I/O 或重新计算即可复现。运行时校验避免了向 tools core 添加共享的 serializable-value 依赖。 -这是通用形态("工具附加持久化的结果展示"),而非 fs 专用——任何工具都可以使用。 +这是通用形态("工具附加持久化的结果展示"),而非 fs 特有的——任何工具都可以使用。 -### 2. 工具计算 hunk;后端返回变更前/后文本(fs) +### 2. 工具计算 hunk;后端返回 before/after(fs) -按照[能力-seam 拆分](2026-06-13-capability-seams.md),存储后端只返回**存储事实**,面向模型的工具拥有**展示**: +按照 [capability-seam 拆分](2026-06-13-capability-seams.md),存储后端只返回**存储事实**,面向模型的工具拥有**展示**: -- `dsh-fs` 扩展 `FsEditOutcome`,增加 `{ before: string; after: string }`;扩展 `FsWriteOutcome`,增加 `{ before: string | null; after: string }`(`before: null` ⇒ 新建文件,或已存在但不可 diff 的二进制/非 UTF-8 文件)。本地后端在写入时已持有两份文本;它以原始 LF 规范化文本返回,**不让任何 diff/UI 概念进入 seam**。 -- `dsh-tool-fs` 将带上下文的 hunk 存入 `meta: { diffs: FileDiff[] }`。成功的变更始终以 diff 卡片完成,因为 ACP 结果内容会替换 pending 卡片:新建或无变化的覆写回退为参数推导的全文件 diff,而编辑使用 applied hunk。失败的变更不携带 diff 元数据,正常渲染错误信息。 +- `dsh-fs` 将 `FsEditOutcome` 扩展为包含 `{ before: string; after: string }`,将 `FsWriteOutcome` 扩展为包含 `{ before: string | null; after: string }`(`before: null` 表示创建,或已存在但不可 diff 的二进制/非 UTF-8 文件)。本地后端在写入时已持有两份文本;它以原始 LF 规范化文本返回,**不让任何 diff/UI 概念进入 seam**。 +- `dsh-tool-fs` 将上下文 hunk 存入 `meta: { diffs: FileDiff[] }`。成功的变更始终以 diff 卡片完成,因为 ACP 结果内容会替换待定卡片:创建或无变化的覆写回退到由参数推导的整文件 diff,而编辑使用 applied hunk。失败的变更不携带 diff 元数据,正常渲染其错误信息。 -### 3. 桥接层渲染 `diff` 结果卡片 +### 3. Bridge 渲染 `diff` 结果卡片 -`ToolResultView` 新增 `DiffResultView { card:'diff'; title?; diffs: FileDiff[] }`;桥接层结果侧的 `switch (view.card)` 增加 `diff` 分支,发出 `{type:'diff'}` 的 `ToolCallContent` 块(与调用侧分支对称)。ACP 的 `tool_call_update.content` 在编辑器中**替换**调用时的内容,因此结果 diff **取代**调用时片段(并防止面向模型的结果文本覆盖它)——两次更新的序列(先调用片段,后结果 diff)与 `claude-agent-acp` 完全一致。 +`ToolResultView` 新增 `DiffResultView { card:'diff'; title?; diffs: FileDiff[] }`;bridge 结果侧的 `switch (view.card)` 增加 `diff` 分支,发出 `{type:'diff'}` 的 `ToolCallContent` 块(与调用侧分支对称)。ACP 的 `tool_call_update.content` 在编辑器中**替换**调用时的内容,因此结果 diff **取代**调用时刻的片段(并防止面向模型的结果文本覆盖它)——两次更新序列(先调用片段,再结果 diff)与 `claude-agent-acp` 完全一致。 ## 曾考虑的替代方案 -**手写或 vendor diff 算法。** 带上下文的 hunk 有已知的边界情况,因此 `dsh-tool-fs` 使用带类型的 [`diff`](https://www.npmjs.com/package/diff) 包,并在一个模块中规范化 `structuredPatch` 输出。本仓库的 vendor 策略适用于其框架源码,而非每个叶子工具。 +**手写或 vendor diff 算法。** 上下文 hunk 有已知的边界情况,因此 `dsh-tool-fs` 使用带类型的 [`diff`](https://www.npmjs.com/package/diff) 包,并在一个模块中规范化 `structuredPatch` 输出。仓库的 vendor 策略适用于框架源码,而非每个叶子工具库。 ## 后果 -`tool/result` 事件现在可以携带工具私有的 `meta` 载荷——属于磁盘词汇的一部分,由 `Session.append` 在运行时限制为 JSON——任何工具都可以附加持久化的结果展示而无需再改 core。diff 卡片在会话重载和快照回放时免费复现:从日志读回,从不重新计算。代价:覆写操作在内存中同时持有变更前和新文本以计算仅用于 UI 的 hunk(`TODO(overwrite-diff-bound)`),且 `dsh-tool-fs` 引入了一个小型、知名的运行时依赖。 +`tool/result` 事件现在可以携带工具私有的 `meta` 载荷——属于磁盘格式词汇的一部分,由 `Session.append` 在运行时限制为 JSON——任何工具都可以附加持久化的结果展示而无需再改 core。diff 卡片在会话重载和快照回放时免费复现:它从日志中读回,从不重新计算。代价:覆写操作在内存中同时持有旧文本和新文本以计算仅用于 UI 的 hunk(`TODO(overwrite-diff-bound)`),且 `dsh-tool-fs` 引入了一个小型、知名的运行时依赖。 ## 非目标 -- **实时增量 diff 流式输出。** hunk 在变更完成后一次性计算;没有逐按键 diff。 -- **对二进制/非 UTF-8 覆写做 diff。** 此类文件的 `before` 为 `null`(没有文本 diff 基础);写入仍然成功,结果渲染全文件 diff(`oldText: null`)而非带上下文的 hunk。 -- **重命名/移动 diff。** 仅对单个已解析路径做内容 diff。 -- **限制覆写 diff 基础的大小。** 覆写操作将整个旧文件读入内存以计算带上下文的 hunk(在已持有的新内容之上),因此非常大的文本覆写会为仅 UI 用途的 diff 分配两份文本。后续优化可以设定预读上限,超过阈值时回退到全文件/无上下文 diff;以 `TODO(overwrite-diff-bound)` 标记在读取位置。 +- **实时增量 diff 流式输出。** hunk 在变更完成后一次性计算;没有逐键 diff。 +- **对二进制/非 UTF-8 覆写做 diff。** 此类文件的 `before` 为 `null`(没有文本 diff 基础);写入仍然成功,结果渲染整文件 diff(`oldText: null`)而非上下文 hunk。 +- **重命名/移动 diff。** 仅限单个已解析路径的内容 diff。 +- **限制覆写 diff 基础的大小。** 覆写操作将整个旧文件读入内存以计算上下文 hunk(加上已持有的新内容),因此非常大的文本覆写会为仅 UI 用途的 diff 分配两份文本。未来的改进可以设定预读上限,超过阈值时回退到整文件/无上下文 diff;在读取位置以 `TODO(overwrite-diff-bound)` 跟踪。 ## 相关 -- 补齐了[带标签的渲染意图联合类型](2026-07-02-tool-render-intent-union.md)中作为非目标列出的最后一项表示差异——该 RFC 的「非目标」一节已更新,记录 applied-hunk diff 在此处交付。 -- 建立在[文件系统能力 seam](2026-06-17-filesystem-capability-seam.md)(变更前/后文本是后端返回的存储事实)和[事件溯源会话](2026-06-11-event-sourced-sessions.md)(`meta` 载荷持久化在 `tool/result` 事件上,因此回放可复现卡片)之上。 -- `meta` 通道有意设计为通用的:未来的工具(结构化搜索、数据表结果等)可以附加自己的持久化结果展示而无需再改 core。 +- 补全了 [Tagged render-intent union](2026-07-02-tool-render-intent-union.md) 中作为非目标列出的最后一项表示差异——该 RFC 的「非目标」一节已更新,记录 applied-hunk diff 在此处交付。 +- 基于[文件系统 capability seam](2026-06-17-filesystem-capability-seam.md)(before/after 是后端返回的存储事实)和[事件溯源会话](2026-06-11-event-sourced-sessions.md)(`meta` 载荷持久化在 `tool/result` 事件上,因此回放可复现卡片)。 +- `meta` 通道有意设计为通用的:未来的工具(结构化搜索、数据表结果)可以附加自己的持久化结果展示而无需再改 core。 diff --git a/docs/rfc/implemented/architecture/2026-07-02-tool-render-intent-union.i18n.yaml b/docs/rfc/implemented/architecture/2026-07-02-tool-render-intent-union.i18n.yaml index 4c8a1f1b63..8c2f7faf6d 100644 --- a/docs/rfc/implemented/architecture/2026-07-02-tool-render-intent-union.i18n.yaml +++ b/docs/rfc/implemented/architecture/2026-07-02-tool-render-intent-union.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-02-tool-render-intent-union.md: 8256f09f9c297658627d0c3d9e99ee1c5424b254 -2026-07-02-tool-render-intent-union.zh.md: ed46bf0a8bea1cbfbce4287e5dc48be21c8d8fb9 +2026-07-02-tool-render-intent-union.zh.md: 35bd775545c9131a20da9e7f7424506e4554eb4b diff --git a/docs/rfc/implemented/architecture/2026-07-02-tool-render-intent-union.zh.md b/docs/rfc/implemented/architecture/2026-07-02-tool-render-intent-union.zh.md index ed46bf0a8b..35bd775545 100644 --- a/docs/rfc/implemented/architecture/2026-07-02-tool-render-intent-union.zh.md +++ b/docs/rfc/implemented/architecture/2026-07-02-tool-render-intent-union.zh.md @@ -1,4 +1,4 @@ -# RFC:用于工具调用展示的标签化 render-intent 联合类型 +# RFC:用于工具调用展示的带标签 render-intent 联合类型 Status: implemented @@ -6,17 +6,17 @@ Status: implemented ## 问题 -工具通过 `ToolDefinition` 上的两个回调 `presentCall`/`presentResult` 声明其调用在 UI(编辑器的工具调用卡片)中的渲染方式,返回 `ToolCallPresentation` / `ToolResultPresentation`,并带有可选的 `ToolTerminal` 子结构。这些类型在增量演进中变成了一个**可选字段的大杂烩**:调用侧有 `title`、`kind`、`rawInput`、`content`、`locations`、`terminal`;结果侧有 `title`、`content`、`terminal`;`ToolTerminal` 上有 `cwd`/`output`/`exitCode`/`signal`。职责划分含混不清: +工具通过 `ToolDefinition` 上的两个回调 `presentCall`/`presentResult` 声明其调用在 UI(编辑器的工具调用卡片)中如何渲染,返回 `ToolCallPresentation` / `ToolResultPresentation`,并带有一个可选的 `ToolTerminal` 子结构。这些类型在增量演进中变成了一个**可选字段的集合**:调用侧有 `title`、`kind`、`rawInput`、`content`、`locations`、`terminal`;结果侧有 `title`、`content`、`terminal`;`ToolTerminal` 上有 `cwd`/`output`/`exitCode`/`signal`。职责划分模糊不清: -- 调用侧和结果侧的 `terminal` 字段重叠,bridge 需要将一个 `content` 块、一个 `terminal` 块和 `rawInput` 按调用拼接在一起,靠临时条件逻辑缝合。 -- 哪些组合是*合法的*没有文档:一个设置了 `terminal` 的调用如果同时设置了 `content`,含义是「卡片上方的描述」;一个 generic 调用如果设置了 `terminal`,毫无意义但类型允许。类型允许无意义的状态。 -- 无法表达编辑器最需要的文件工具能力:**diff 卡片**(`{path, oldText, newText}`,Zed 将其渲染为内联 diff / 新文件预览)。`ToolCallPresentation.content` 是 *LLM* 的 `ContentBlock[]` 词汇(text/image),工具字面上无法请求一个 diff。 +- 调用侧和结果侧的 `terminal` 字段重叠,bridge 需要将每次调用的 `content` 块、`terminal` 块和 `rawInput` 用临时条件逻辑拼接在一起。 +- 哪些组合是*合法的*没有文档说明:一个设置了 `content` 的 `terminal` 调用意味着「卡片上方的描述」;一个设置了 `terminal` 的 generic 调用毫无意义但类型上可表达。类型允许无意义的状态存在。 +- 无法表达编辑器最需要的文件工具能力:**diff 卡片**(`{path, oldText, newText}`,Zed 将其渲染为内联 diff / 新文件预览)。`ToolCallPresentation.content` 使用的是 *LLM(大语言模型)* 的 `ContentBlock[]` 词汇(text/image),工具根本无法请求 diff 展示。 -`packages/core/tools/src/index.ts` 中现有的 `FIXME(tool-presentation)` 指明了修复方向:「重新设计类型,让工具一次性声明其渲染意图(例如按卡片种类的标签联合类型),而不是一堆可选字段由 bridge 拼接。」被否决的 RFC [Collapse tool-owned UI presentation](../../rejected/simplification/2026-06-20-generic-tool-rendering.md) 明确推迟了此事:富渲染「应当在至少有两个真实工具和两个真实消费方来验证词汇之后,以标签化 render-intent 联合类型的形式回归」。这个门槛现已达到:两个生产方族(`dsh-tool-bash`、`dsh-tool-fs`)和两个消费方(ACP bridge 实时路径 + snapshot-golden 回放路径)。 +`packages/core/tools/src/index.ts` 中已有的 `FIXME(tool-presentation)` 指出了修复方向:「重新设计类型,让工具一次性声明其渲染意图(例如按卡片种类的带标签联合类型),而非一堆由 bridge 拼接的可选字段。」被否决的 RFC [Collapse tool-owned UI presentation](../../rejected/simplification/2026-06-20-generic-tool-rendering.md) 明确推迟了此事:富渲染「应当在至少有两个真实工具和两个真实消费方验证词汇之后,以带标签 render-intent 联合类型的形式回归。」该条件现已满足:两个生产者族(`dsh-tool-bash`、`dsh-tool-fs`)和两个消费方(ACP bridge 实时路径 + snapshot-golden 回放路径)。 ## 决策 -用一个**以 `card` 为标签的可辨识联合类型**替代可选字段大杂烩。工具为每次调用/结果声明一个渲染意图;bridge 按标签分发。 +用一个**以 `card` 为标签的可辨识联合类型**替代可选字段集合。工具为每次调用/结果声明一个渲染意图;bridge 根据标签分发。 ```ts ignore-check type FileLocation = { path: string; line?: number } @@ -34,41 +34,41 @@ interface GenericResultView { card: 'generic'; title?: string; content?: Content interface TerminalResultView { card: 'terminal'; title?: string; output?: string; exitCode?: number; signal?: string } ``` -`card` 在每个变体上都是**必填**的:一个真正的判别字段,而非可选默认值。bridge 执行 `switch (view.card) { case 'generic': … case 'terminal': … case 'diff': … default: assertNever(view) }`。该联合类型是**封闭的**(遵循 [switch 穷举约定](../../../../AGENTS.md)):第四种渲染意图(表格、图表)无论如何都需要新的 bridge 代码来渲染,因此一个插件添加的变体如果被 bridge 静默丢弃,比编译错误更糟。添加变体会在 bridge 的 switch 处中断编译——这正是我们想要的信号。 +`card` 在每个变体上都是**必填**的——真正的判别式,而非可选默认值。bridge 执行 `switch (view.card) { case 'generic': … case 'terminal': … case 'diff': … default: assertNever(view) }`。该联合类型是**封闭的**(遵循 [switch 穷举约定](../../../../AGENTS.md)):第四种渲染意图(表格、图表)无论如何需要新的 bridge 代码来渲染,因此一个由插件添加但被 bridge 静默丢弃的变体,比编译错误更糟糕。新增变体会在 bridge 的 switch 处中断编译——这正是我们想要的信号。 -### 为什么标签联合类型优于字段大杂烩 +### 为什么带标签联合类型优于字段集合 -- **无效状态变得不可表示。** generic 卡片不能携带终端输出;terminal 卡片不能携带 diff。旧的大杂烩允许所有这些组合。 -- **bridge 按分支分发,而非拼接。** 每种卡片一个分支,各自精确产出该卡片所需的协议格式(wire format),而非协调五个交互关系未文档化的可选字段。 -- **`diff` 成为一等意图。** `dsh-tool-fs` 的 write/edit 声明 `card:'diff'`;bridge 发出 ACP `{type:'diff', path, oldText, newText}` `ToolCallContent`(已存在于 SDK 的 `ToolCallContent` 联合类型中,此前 bridge 未使用)。这是本次重设计解锁的能力。 +- **无效状态变得不可表达。** generic 卡片不能携带终端输出;terminal 卡片不能携带 diff。旧的字段集合允许所有这些组合。 +- **bridge 分发而非拼接。** 每种卡片一个分支,各自精确产出该卡片所需的协议格式(wire format),而非调和五个交互关系未文档化的可选字段。 +- **`diff` 成为一等意图。** `dsh-tool-fs` 的 write/edit 声明 `card:'diff'`;bridge 输出 ACP `{type:'diff', path, oldText, newText}` 的 `ToolCallContent`(已存在于 SDK 的 `ToolCallContent` 联合类型中,此前 bridge 未使用)。这正是本次重设计解锁的能力。 -### 生产方映射 +### 生产者映射 -- `dsh-tool-fs` read → `generic`(`kind:'read'`,附带一个 follow-along `location`);write → `diff`(`oldText:null`);edit → `diff`(`oldText:old_string || null`,`newText:new_string ?? ''`)。这与 `claude-agent-acp` 的 `toolInfoFromToolUse` Read/Write/Edit 分支逐字段对应。 +- `dsh-tool-fs` read → `generic`(`kind:'read'`,附带一个 follow-along `location`);write → `diff`(`oldText:null`);edit → `diff`(`oldText:old_string || null`,`newText:new_string ?? ''`)。这与 `claude-agent-acp` 的 `toolInfoFromToolUse` 中 Read/Write/Edit 各分支逐字段对应。 - `dsh-tool-bash` foreground → `terminal` 调用 + `terminal` 结果;`run_in_background` 和 `bash_output`/`bash_kill` → `generic`。 - `dsh-tool-todo` → `generic`。 ### 终端回退的归属 -`TerminalResultView` 只携带 `output`/`exitCode`/`signal`。不具备终端能力的 UI 需要一个围栏 ` ```console ` 文本回退;该推导移至 **bridge**(bridge 在无能力路径上将 `output` 包裹为围栏代码块),而非由工具双重编码。这使 bash 工具的结果保持单一结构化形状,并逐字节保留既有的 capability 门控行为。 +`TerminalResultView` 只携带 `output`/`exitCode`/`signal`。不具备终端能力的 UI 需要一个围栏 ` ```console ` 文本回退;该推导移至 **bridge**(在无能力路径上将 `output` 包裹在围栏代码块中),而非由工具双重编码。这使 bash 工具的结果保持单一结构化形状,并逐字节保留既有的能力门控行为。 ### 纯函数性保持不变 -`presentCall`/`presentResult` 仍然是 `args`(以及 `presentResult` 的 result)的纯函数——它们在实时流式输出和会话日志回放中都会运行,因此必须具备回放确定性。每个 view 仅从 args 推导:write 的 diff 是新文件样式(`oldText:null`),因为工具在调用时没有旧内容;edit 的 diff 是 `old_string`→`new_string`。 +`presentCall`/`presentResult` 仍然是 `args`(`presentResult` 还有 result)的纯函数——它们在实时流式输出和会话日志回放中都会运行,因此必须具备回放确定性。每个 view 仅从 args 推导:write 的 diff 是新文件风格(`oldText:null`),因为工具在调用时没有旧内容;edit 的 diff 是 `old_string`→`new_string`。 ## 相对路径显示标题 -`claude-agent-acp` 将文件卡片的标题路径相对于会话 cwd 做相对化处理(`toDisplayPath`):显示 `Read src/foo.ts` 而非 `/abs/proj/src/foo.ts`,同时保持 `locations[]`/`diff.path` **原始**(编辑器打开真实路径)。我们的 `presentCall` 是纯函数/仅依赖 args,无法看到会话 cwd,因此相对化发生在 **bridge**——bridge 已经将会话 cwd 传入工具调用渲染(与它用于解析 terminal 卡片标题的 cwd 相同)。bridge 仅对标题做相对化,通过对已知 `locations[0].path`/`diffs[0].path` 子串的精确结构化替换实现——对文件卡片类型通用,从不特判工具名。 +`claude-agent-acp` 将文件卡片标题中的路径相对于会话 cwd 做缩短处理(`toDisplayPath`)——显示 `Read src/foo.ts` 而非 `/abs/proj/src/foo.ts`——同时保持 `locations[]`/`diff.path` 为**原始路径**(编辑器打开真实路径)。我们的 `presentCall` 是纯函数/仅依赖 args,无法访问会话 cwd,因此这一相对化处理发生在 **bridge**,bridge 已经将会话 cwd 传入工具调用渲染逻辑(与它用于解析 terminal 卡片标题的 cwd 相同)。bridge 仅对标题做相对化,方式是对已知的 `locations[0].path`/`diffs[0].path` 子串做精确的结构化替换——对所有文件卡片类型通用,从不针对工具名做特殊处理。 ## 曾考虑的替代方案 -- **完全删除工具自有的展示**:即[被否决的 collapse 提案](../../rejected/simplification/2026-06-20-generic-tool-rendering.md);其结论明确推迟到两个真实工具和两个真实消费方存在后再做这个联合类型,而该门槛现已达到。 -- **可合并扩展的联合类型**(`ContentBlockMap` 模式):否决。新的渲染意图无论如何都需要新的 bridge 代码来渲染,因此一个插件添加的变体如果被 bridge 静默丢弃,比封闭联合类型在 bridge 的 `assertNever` switch 处引发的编译错误更糟。 -- **保留可选字段大杂烩**:即「问题」一节所剖析的现状:无效状态可表示、字段交互未文档化、且完全无法请求 diff 卡片。 +- **完全删除工具自有的展示**:即[被否决的 collapse 提案](../../rejected/simplification/2026-06-20-generic-tool-rendering.md);其自身的结论正是推迟到两个真实工具和两个真实消费方存在后再做此联合类型,该条件现已满足。 +- **可合并扩展的联合类型**(`ContentBlockMap` 模式):否决。新的渲染意图无论如何需要新的 bridge 代码来渲染,因此一个被 bridge 静默丢弃的插件添加变体,比封闭联合类型在 bridge 的 `assertNever` switch 处引发的编译错误更糟糕。 +- **保留可选字段集合**:即「问题」一节所剖析的现状:无效状态可表达、字段交互无文档、且完全无法请求 diff 卡片。 ## 后果 -新的渲染意图是 bridge switch 处的编译中断变更——这是有意为之:渲染代码必须在卡片种类存在之前就位。无效的卡片/字段组合现已不可表示,bash 回退推导归 bridge 所有,工具只返回一个结构化形状。第四种卡片(表格、图表)的门槛是在同一个变更中编写其 bridge 分支。 +新的渲染意图会在 bridge 的 switch 处引发编译中断——这是有意为之:渲染代码必须先于卡片种类存在。无效的卡片/字段组合现已不可表达,bash 回退推导归 bridge 所有,工具只返回一个结构化形状。第四种卡片(表格、图表)的门槛是在同一个变更中编写其 bridge 分支。 ## 非目标 @@ -76,7 +76,7 @@ interface TerminalResultView { card: 'terminal'; title?: string; output?: string ## 相关 -- 取代 [Collapse tool-owned UI presentation](../../rejected/simplification/2026-06-20-generic-tool-rendering.md)(已否决——「等两个真实工具和两个真实消费方,然后做标签化 render-intent 联合类型」)中的推迟决定。该门槛现已达到;本 RFC 即是那个联合类型。 -- 由 [Result-time applied-hunk diffs](2026-07-02-result-time-applied-hunk-diffs.md) 扩展:该 RFC 增加了一个持久化的 `meta` 通道,使 write/edit 在结果时发出 `DiffResultView`(应用后的变更:带上下文行的 contextual hunk / 每个 `replace_all` 站点一个,或新建文件的整文件 diff),叠加在本联合类型的调用时 diff 卡片之上。 -- 将 `ToolTerminal` 折入 [ACP terminal and tool-call rendering](../feature/2026-06-18-acp-terminal-and-tool-rendering.md) 所描述的 `terminal` view(`_meta` terminal 卡片约定和 capability 门控不变;仅 harness 侧的展示类型改变)。 +- 取代 [Collapse tool-owned UI presentation](../../rejected/simplification/2026-06-20-generic-tool-rendering.md)(已否决——「等两个真实工具和两个真实消费方,然后做带标签 render-intent 联合类型」)中的推迟决定。该条件现已满足;本 RFC 即为那个联合类型。 +- 被 [Result-time applied-hunk diffs](2026-07-02-result-time-applied-hunk-diffs.md) 扩展:后者添加了一个持久化的 `meta` 通道,使 write/edit 在结果时输出 `DiffResultView`(应用后的变更:带上下文行的 contextual hunk / 每个 `replace_all` 位点一个,或创建时的整文件 diff),叠加在本联合类型的调用时 diff 卡片之上。 +- 将 `ToolTerminal` 折入 [ACP terminal and tool-call rendering](../feature/2026-06-18-acp-terminal-and-tool-rendering.md) 所描述的 `terminal` view(`_meta` terminal 卡片约定和能力门控不变;仅 harness 侧的展示类型改变)。 - ACP SDK 的 `Diff` / `ToolCallContent` 类型支撑新的 `diff` 卡片。 diff --git a/docs/rfc/implemented/architecture/2026-07-03-filesystem-directory-listing-seam.i18n.yaml b/docs/rfc/implemented/architecture/2026-07-03-filesystem-directory-listing-seam.i18n.yaml index 25679116f6..f589fd9ddf 100644 --- a/docs/rfc/implemented/architecture/2026-07-03-filesystem-directory-listing-seam.i18n.yaml +++ b/docs/rfc/implemented/architecture/2026-07-03-filesystem-directory-listing-seam.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-03-filesystem-directory-listing-seam.md: bb8d9c4deda18b320b85d548bbd5bcb32f1c1d72 -2026-07-03-filesystem-directory-listing-seam.zh.md: a0332fe6cec576ad1c5b4722e2decb87344aeee1 +2026-07-03-filesystem-directory-listing-seam.zh.md: ccc5ca67f58537134da5c5484b3d527ba84fe8d3 diff --git a/docs/rfc/implemented/architecture/2026-07-03-filesystem-directory-listing-seam.zh.md b/docs/rfc/implemented/architecture/2026-07-03-filesystem-directory-listing-seam.zh.md index a0332fe6ce..ccc5ca67f5 100644 --- a/docs/rfc/implemented/architecture/2026-07-03-filesystem-directory-listing-seam.zh.md +++ b/docs/rfc/implemented/architecture/2026-07-03-filesystem-directory-listing-seam.zh.md @@ -1,53 +1,53 @@ # RFC:为文件系统 seam 添加直接目录列举能力 -Status: implemented - [English](2026-07-03-filesystem-directory-listing-seam.md) | 中文 +Status: implemented + ## 问题 -`@deepseek-ai/dsh-fs` 是文件系统访问的提供方 seam,本地后端与未来的非本地后端共享同一个 `ctx.fs` 契约。在本次变更之前,它能解析路径、stat 目标、读取文本、流式读取文本、写入文本和编辑文本。这对面向模型的文件工具已经够用,但对于需要枚举目录而又不想直接导入 `node:fs` 的非模型侧消费方来说还不够。 +`@deepseek-ai/dsh-fs` 是文件系统访问的提供方 seam,本地后端与未来的非本地后端共享同一个 `ctx.fs` 契约。在本次变更之前,它能解析路径、stat 目标、读取文本、流式读取文本、写入文本和编辑文本。这对面向模型的文件工具已经足够,但对于需要枚举目录而又不想直接导入 `node:fs` 的非模型侧消费方来说还不够。 -直接的压力来自 skill 加载:读取单个 `SKILL.md` 已经可以走 `ctx.get('fs')`,但发现哪些 skill 根目录下包含 `/SKILL.md` 或 `.md` 仍需要目录枚举。如果只在 `dsh-skill` 中添加目录列举,要么保留一个直接的 Node 依赖,要么在文件系统提供方栈之外发明一个一次性的本地辅助函数。 +直接的压力来自 skill(技能)加载:读取单个 `SKILL.md` 已经可以走 `ctx.get('fs')`,但发现哪些 skill 根目录包含 `/SKILL.md` 或 `.md` 仍需要目录枚举。如果仅在 `dsh-skill` 中添加目录列举,要么保留对 Node 的直接依赖,要么在文件系统提供方栈之外发明一个一次性的本地辅助函数。 -本决策只添加提供方能力,不引入面向模型的 `ls`/`list` 工具,也不改变 skill 发现逻辑。那些消费方需要独立的 UX、提示词和策略决策。 +本决策只添加提供方能力,不涉及面向模型的 `ls`/`list` 工具或 skill 发现机制的变更。那些消费方需要独立的 UX、prompt 与策略决策。 ## 决策 在 `@deepseek-ai/dsh-fs` 中添加 `FileSystem.listDir(target, signal?)`。 -`listDir` 仅列举一级目录。它以稳定的名称顺序返回直接子项,包含: +`listDir` 仅列举一层目录。它以稳定的名称顺序返回直接子项,包含以下字段: -- `name`:子项的 basename。 -- `type`:`file`、`directory` 或 `other`。 -- `target`:已解析的子项 `FsTarget`。 -- `version`:可用时提供的轻量元数据。 -- `size`:可用时提供的常规文件大小。 +- `name`:子项的 basename; +- `type`:`file`、`directory` 或 `other`; +- `target`:已解析的子项 `FsTarget`; +- `version`:可用时返回的轻量元数据; +- `size`:可用时返回的常规文件大小。 它从不读取文件内容。递归遍历、glob 匹配、分页、搜索、文件监听和面向模型的渲染均有意不在范围内。 -本地后端通过 `readdir({ withFileTypes: true })`、`resolveLocalTarget` 以及元数据 `stat`/`realpath` 探测来实现。结果顺序是确定性的(`name.localeCompare`),以保持未来消费方的提示词/列表输出稳定,并提升前缀缓存复用率。 +本地后端通过 `readdir({ withFileTypes: true })`、`resolveLocalTarget` 以及元数据 `stat`/`realpath` 探测来实现。结果顺序是确定性的(`name.localeCompare`),以保持未来消费方的 prompt/列表输出稳定,并提高前缀缓存复用率。 -损坏或已消失的子项可以表示为 `type: 'other'`(不带 `version`/`size`);它们不会中止整个列举。列举目录或解析/探测子项元数据时遇到的权限或后端 I/O 故障会以结构化的 `FsError` 代码使整个列举失败: +损坏或已消失的子项可以表示为 `type: 'other'`(不带 `version`/`size`);它们不会中止整个列举。在列举目录或解析/探测子项元数据时遇到权限或后端 I/O 故障,则以结构化的 `FsError` 错误码使整个列举失败: -- `FS_NOT_FOUND`:目标不存在。 -- `FS_NOT_DIRECTORY`:目标存在但不是目录。 -- `FS_PERMISSION_DENIED`:权限不足。 -- `FS_IO_ERROR`:其他后端 I/O 故障。 +- `FS_NOT_FOUND`:目标不存在; +- `FS_NOT_DIRECTORY`:目标存在但不是目录; +- `FS_PERMISSION_DENIED`:权限不足; +- `FS_IO_ERROR`:其他后端 I/O 故障; - `FS_ABORTED`:调用被中止。 ## 曾考虑的替代方案 -**在添加 seam 的同时添加面向模型的 list 工具。** 否决。其提示词、schema 和渲染契约与提供方原语无关。 +**在添加 seam 的同时添加面向模型的 list 工具。** 否决。其 prompt、schema 和渲染契约与提供方原语相互独立。 -**让每个消费方自行枚举目录。** 否决。这会把 `dsh-skill` 等产品包绑定到 Node/本地文件系统行为上,绕过策略/远程/沙箱后端。 +**让每个消费方自行枚举目录。** 否决。这会将 `dsh-skill` 等产品包绑定到 Node/本地文件系统行为上,绕过策略/远程/沙箱后端。 -**让 `listDir` 支持递归或 glob 形式。** 暂时否决。skill 根目录发现只需要直接子项,简单的单级列举是未来消费方可以安全组合的最小后端契约。 +**让 `listDir` 支持递归或 glob 形式。** 暂时否决。skill 根发现只需要直接子项,而简单的单层列举是未来消费方可以安全组合的最小后端契约。 -**跳过元数据解析失败的子项。** 否决。API 承诺返回已解析的子项 target,因此解析子项时遇到的权限/IO 故障属于契约失败。损坏或已消失的子项是例外,因为它们仍可在不声称拥有一个活跃已解析文件的前提下被表示。 +**跳过元数据解析失败的子项。** 否决。API 承诺返回已解析的子项 target,因此解析子项时的权限/IO 故障属于契约失败。损坏或已消失的子项是例外,因为它们仍可在不声称拥有一个活跃已解析文件的前提下被表示。 ## 后果 每个文件系统后端现在必须多实现一个提供方原语。这是 harness 尚未发布时有意为之的基础工作,但也意味着未来的沙箱/远程后端需要定义等价的直接子项列举行为。 -该能力仍然面向提供方。在消费方落地之前,ACP/模型会话仍需使用 `bash` 等既有工具来列举目录。没有面向模型的 `listdir` 工具是预期行为,而非接线遗漏。 +该能力仍停留在提供方层面。在消费方落地之前,ACP(Agent Client Protocol)/模型会话仍需使用 `bash` 等既有工具来列举目录。缺少面向模型的 `listdir` 工具是预期行为,而非接线错误。 diff --git a/docs/rfc/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.i18n.yaml b/docs/rfc/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.i18n.yaml index 8ae2221209..5c821e12fa 100644 --- a/docs/rfc/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.i18n.yaml +++ b/docs/rfc/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-05-prompt-variables-and-tool-guidance-ownership.md: fce9d555c8843b99fdbfa7b652b46d0b88053935 -2026-07-05-prompt-variables-and-tool-guidance-ownership.zh.md: 5af4fca19649d4ee458eaa6a23ae7374abdd89e4 +2026-07-05-prompt-variables-and-tool-guidance-ownership.zh.md: 93a639a2ddac33cb1ceac57101cb6185fe6034ad diff --git a/docs/rfc/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.zh.md b/docs/rfc/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.zh.md index 5af4fca196..93a639a2dd 100644 --- a/docs/rfc/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.zh.md +++ b/docs/rfc/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.zh.md @@ -1,72 +1,72 @@ -# RFC:提示词变量与工具指导归属 - -Status: implemented +# RFC:Prompt 变量与工具指导归属 [English](2026-07-05-prompt-variables-and-tool-guidance-ownership.md) | 中文 +Status: implemented + ## 问题 -组装后的系统提示词有四个缺陷,同属一类:harness 已经掌握的事实在别处被手工重述,然后漂移。 +组装后的系统提示词存在四个缺陷,同属一类:harness 已知的事实在别处被手工重述,然后漂移。 -**模型无法知道自己的名字。** `AgentOptions.model` 驱动每次请求,但没有任何提示词文本携带它——也不可能携带:`dsh-system-prompt` 中的 section 是上下文全局的,而模型名称是 per-agent 的,且 `assemble()` 根本不接受任何 per-agent 输入。 +**模型无法知道自己的名字。** `AgentOptions.model` 驱动每个请求,但没有任何 prompt 文本携带它——也不可能携带:`dsh-system-prompt` 中的 section 是上下文全局的,而模型名称是 per-agent 的,`assemble()` 根本不接受任何 per-agent 输入。 -**工具指导是叶子 YAML 中的手写行文。** bash/subagent/todo_write 的使用指导存放在 `examples/coding-agent/cordis.yml` 和 `examples/acp-agent/cordis.yml` 的 `systemPrompt` 字符串中——两份漂移的副本(ACP 那份已经被删减)——而 `dsh-tool-fs` 和 `dsh-tool-web` 则以 `ctx.systemPrompt.section()` 贡献的方式持有各自的指导。加载或卸载一个工具插件意味着手动编辑每个部署的 persona;两份 YAML 都带着一条 `FIXME(config-comments)` 为这种割裂的症状道歉,stdio 的欢迎横幅也手动枚举了工具集。 +**工具指导是 leaf YAML 中的手写行文。** bash/subagent/todo_write 的使用指导存放在 `examples/coding-agent/cordis.yml` 和 `examples/acp-agent/cordis.yml` 的 `systemPrompt` 字符串里——两份漂移的副本(ACP 那份已经被删减)——而 `dsh-tool-fs` 和 `dsh-tool-web` 则通过 `ctx.systemPrompt.section()` 贡献各自的指导。加载或卸载一个工具插件意味着手动编辑每个部署的 persona;两份 YAML 都带着一条 `FIXME(config-comments)` 为这种分裂的症状道歉,stdio 的欢迎横幅也手动枚举了工具集。 -**Persona 渲染在工具指导之后。** agent loop(智能体循环)将 `agent.options.systemPrompt` 字符串拼接在已组装的 section 之后,于是模型先读到「使用 read 工具……」再读到「你是 coding-agent」——与身份优先的惯例(Claude Code、Codex)相反,且在 section 流水线之外形成了第二条组合路径。 +**Persona 渲染在工具指导之后。** agent loop(智能体循环)将 `agent.options.systemPrompt` 字符串拼接在已组装的 section 之后,于是模型先读到「Use the read tool…」再读到「You are coding-agent」——与 identity-first 约定(Claude Code、Codex)相反,且是 section 流水线之外的第二条组合路径。 -**Fork 工具的描述是假的。** `dsh-tool-subagent` 硬编码了一段为 spawn 语义撰写的描述——"a separate agent that works in its own context … it does not see this conversation"——而 `subagent_fork` 实例(其子 agent 继承父级已完成的轮次)拿到了同样的措辞;YAML 行文在带外纠正了这个谎言。小问题同族:`PromptSection.name` 文档写着"(diagnostics / dedup)",但重复项被静默接受。 +**Fork 工具的描述是假的。** `dsh-tool-subagent` 硬编码了一段为 spawn 语义编写的描述——"a separate agent that works in its own context … it does not see this conversation"——而 `subagent_fork` 实例(其子 agent 继承父级已完成的轮次)拿到了同样的措辞;YAML 行文在带外纠正了这个谎言。小问题:`PromptSection.name` 文档标注为 "(diagnostics / dedup)",但重复项被静默接受。 ## 决策 -**一条原则:提示词中的每个事实恰好有一个归属方。** 模型名称和工作区是配置/会话事实 → harness 将它们暴露为变量,persona 引用它们。每个工具的语义和何时使用 → 工具的 `description`。description 无法承载的跨调用习惯 → 工具包的 prompt section。harness 出处 → 静态的 `harness:identity` section。部署角色和行为 → 部署的 persona。 +**一条原则:prompt 中的每个事实恰好有一个归属方。** 模型名称和工作区是配置/会话事实 → harness 将它们暴露为变量,persona 引用它们。每个工具的语义和何时使用 → 工具的 `description`。description 无法承载的跨调用习惯 → 工具包(package)的 prompt section。harness 来源标识 → 静态的 `harness:identity` section。部署角色与行为 → 部署的 persona。 ### 组装上下文 -`SystemPrompt.assemble(context)` 接受一个可 merge 扩展的 `AssembleContext`。`dsh-system-prompt` 声明用于 scoped routing 的可选 `scope` 选择器,而 `dsh-agent` 通过 declaration-merge 将可选的类型化 `agent` 字段附加到其上(类型层面的 `agent → system-prompt` 边,无运行时依赖环)。循环在每一步调用 `assembleContextFor(agent)`,使两个字段标识同一个 agent;section 文本提供方可以读取该上下文,`system-prompt/assemble` waterfall(瀑布式事件)也会收到它,监听方可据此按 agent 过滤或扩展。 +`SystemPrompt.assemble(context)` 接受一个可合并扩展的 `AssembleContext`。`dsh-system-prompt` 声明可选的 `scope` 选择器用于 scoped 路由,而 `dsh-agent` 通过声明合并将可选的类型化 `agent` 字段附加到其上(类型层面的 `agent → system-prompt` 边,无运行时依赖循环)。循环在每个步骤调用 `assembleContextFor(agent)`,使两个字段标识同一个 agent;section 文本提供方可以读取该上下文,`system-prompt/assemble` waterfall(瀑布式事件)也接收它,监听器可据此按 agent 过滤或扩展。 -### 提示词变量 +### Prompt 变量 -插件通过 `ctx.systemPrompt.variable(name, provider)` 注册 `{{name}}` 值。组装时将它们解析到 waterfall 可见的变量映射中。渲染阶段拒绝:未知的 own-property 引用、注册的 provider 返回 `undefined`、格式错误的完整引用、以及仍包含闭合 `}}` 的不平衡引用;孤立的未匹配 `{{` 保留为行文,替换后的值不会被再次扫描。注册阶段拒绝无效或重复的变量名,section 名称也必须唯一。 +插件通过 `ctx.systemPrompt.variable(name, provider)` 注册 `{{name}}` 值。组装过程将它们解析到 waterfall 可见的变量映射中。渲染阶段拒绝以下情况:引用了未知的 own-property、已注册的 provider 返回 `undefined`、格式错误的完整引用、以及仍包含闭合 `}}` 的不平衡引用;孤立的未匹配 `{{` 保留为行文,替换后的值不会被重新扫描。注册阶段拒绝无效或重复的变量名,section 名称也必须唯一。 -`dsh-agent-loop` 注册两个内置变量,均为上下文 agent 的纯投影:`model`(= `options.model`)和 `cwd`(= `session.header.cwd`)。示例 persona 写 `powered by the {{model}} model`——模型名称只在 `model:` 配置键中声明一次。`{{cwd}}` 仅在 ACP 示例中演示:每个 ACP 会话携带客户端的 cwd,而配置预创建的 stdio agent 没有 cwd(在那里声称 `{{cwd}}` 的 persona 会导致该轮次失败——这是有意为之)。变量留在 loop 插件上(不同于下文的 section):它们是本循环所驱动的 agent 的运行时事实,替换循环自行提供自己的变量。 +`dsh-agent-loop` 注册两个内置变量,均为上下文 agent 的纯投影:`model`(= `options.model`)和 `cwd`(= `session.header.cwd`)。示例 persona 写 `powered by the {{model}} model`——模型名称只在 `model:` 配置键中声明一次。`{{cwd}}` 仅在 ACP 示例中演示:每个 ACP 会话携带客户端的 cwd,而配置预创建的 stdio agent 没有 cwd(在那里声称 `{{cwd}}` 的 persona 会导致该轮次失败——这是有意为之)。变量留在 loop 插件上(不同于下面的 section):它们是本循环驱动的 agent 的运行时事实,替换循环自行提供自己的变量。 ### Persona 作为 order-0 section -`dsh-system-prompt` 持有 order 为 `-100` 的 `harness:identity` 和 order 为 `0` 的已配置 `deployment:persona`,因此两者在替换循环时仍然存活。提示词渲染只有一条路径 `renderPrompt(assembly)`,`agent/pre-step` 因此能测量用于压缩(compaction)的确切提示词。agent 作用域的 `deployment:persona` 遮蔽全局默认值,允许 subagent 提供方在发布前安装 persona。约定的 order 分段为:identity `-100`、persona `0`、工具指导 `100–199`。 +`dsh-system-prompt` 拥有 order 为 `-100` 的 `harness:identity` 和 order 为 `0` 的配置 `deployment:persona`,因此两者在循环被替换时仍然存活。prompt 渲染只有一条路径 `renderPrompt(assembly)`,`agent/pre-step` 因此测量的正是用于压缩(compaction)的确切 prompt。agent 作用域的 `deployment:persona` 遮蔽全局默认值,允许 subagent provider 在发布前安装 persona。约定的 order 区间为:identity `-100`、persona `0`、工具指导 `100–199`。 ### 工具指导归属 -每个工具的语义和选择指导存放在工具描述中。Prompt section 仅承载跨调用习惯,例如检查 bash 退出标记或优先使用文件系统工具而非 shell 命令。`todo_write` 和 subagent 工具不需要 section,因为它们的描述已包含完整契约。部署 persona 只包含角色和行为。 +每个工具的语义和选择指导放在工具 description 中。prompt section 只承载跨调用习惯,例如检查 bash 退出标记或优先使用文件系统工具而非 shell 命令。`todo_write` 和 subagent 工具不需要 section,因为它们的 description 包含完整契约。部署 persona 只包含角色和行为。 ### Subagent 对话历史描述符 -`SubagentProvider.inheritsParentContext` 描述的是对话种子,而非作用域、服务、工具或权限。spawn 和 ACP 将其设为 `false`;fork 设为 `true`。`dsh-tool-subagent` 根据该标志派生工具描述和 prompt 参数描述,包括 fork 继承已完成轮次但不继承进行中轮次这一事实。提供方生命周期事件使该措辞与响应式的 provider 注册保持同步;其设计动机见 [provider-lifecycle-events RFC](2026-07-05-subagent-provider-lifecycle-events.md)。 +`SubagentProvider.inheritsParentContext` 描述的是对话种子,而非作用域、服务、工具或权限。spawn 和 ACP 将其设为 `false`;fork 设为 `true`。`dsh-tool-subagent` 根据该标志派生工具和 prompt 参数的描述,包括 fork 继承已完成轮次但不继承进行中轮次这一点。provider 生命周期事件使该措辞与响应式 provider 注册保持同步;其设计动机见 [provider-lifecycle-events RFC](2026-07-05-subagent-provider-lifecycle-events.md)。 ## 曾考虑的替代方案 -- **循环自行组合一行身份文本**——在必须保持精简的那个包里硬编码面向模型的行文("plugins, not loop changes"),且在 section 流水线之外形成第二条组合路径。(身份确实以代码字面量交付——但作为 `dsh-system-prompt` 注册的普通 section,其 `system-prompt/assemble` waterfall 仍是部署方需要移除它时的逃生阀。) -- **通过 `agent/request` waterfall 注入模型名称**——提示词文本在两处组合,且 `agent/pre-step` 的 `fullSystemPrompt` 会遗漏它,导致压缩(compaction)测量的提示词与模型实际看到的不一致。 -- **在每个 persona 中手写模型名称**——与上方一行的 `model:` 键重复,配置修改后默默失实——正是本 RFC 要治的病。 -- **宽松插值(未知引用保留原样或替换为空)**——一个拼写错误 `{{modle}}`(或一个空洞)会被送到模型,直到 transcript(文本记录)审查才有人注意到。 -- **在配置中逐实例手写 subagent 措辞**——面向模型的行文重新回到每个部署 × 每个实例,又是同一个病。**按 provider 名称匹配措辞**——`providerName` 本身是配置,重命名 provider 后会静默拿到错误的措辞。 -- **在 `apply` 时解析 provider(加载顺序要求)** 和 **仅用 section 承载 subagent 措辞(在 assemble 时惰性解析)**——provider 生命周期事件的替代方案;均在 [provider-lifecycle-events RFC](2026-07-05-subagent-provider-lifecycle-events.md) 中被否决。 +- **循环自行组合一行 identity 文本**:在必须保持精简的那个包("用插件,不改循环")中硬编码面向模型的行文,且在 section 流水线之外构成第二条组合路径。(identity 确实以代码字面量交付——但作为 `dsh-system-prompt` 注册的普通 section,其 `system-prompt/assemble` waterfall 仍是部署需要移除它时的逃生阀。) +- **通过 `agent/request` waterfall 注入模型名称**:prompt 文本在两处组合,且 `agent/pre-step` 的 `fullSystemPrompt` 会遗漏它,导致 compaction 测量的 prompt 与模型实际看到的不一致。 +- **在每个 persona 中手写模型名称**:与上方一行的 `model:` 键重复,配置修改后静默失实;正是本 RFC 要治愈的病症。 +- **宽松插值(未知引用保留原样或替换为空)**:一个拼写错误 `{{modle}}`(或一个空洞)会被发送给模型,直到 transcript(文本记录)审查时才会被发现。 +- **在配置中为每个 subagent 实例编写措辞**:面向模型的行文回到每个部署 × 实例中,重蹈 P2 病症。**根据 provider 名称选择措辞**:`providerName` 本身是配置,重命名 provider 后会静默获得错误的措辞。 +- **在 `apply` 时解析 provider(加载顺序要求)** 与 **仅用 section 承载 subagent 措辞(在 assemble 时惰性解析)**:provider 生命周期事件的替代方案;两者均在 [provider-lifecycle-events RFC](2026-07-05-subagent-provider-lifecycle-events.md) 中被否决。 ## 不在范围内 -- 更多变量(`date`、平台、git 状态)——注册表使每个变量成为拥有该事实的插件的一行贡献;本 RFC 不认领任何一个。 -- 为预创建的 stdio agent 提供配置 `cwd`(可让 stdio persona 使用 `{{cwd}}` 并按真实路径分区持久化)——推迟到 session-cwd 方案重新讨论时。 +- 更多变量(`date`、platform、git 状态):注册表使每个变量成为拥有该事实的插件的一行贡献;本 RFC 不认领任何一个。 +- 为预创建的 stdio agent 提供配置 `cwd`(可让 stdio persona 使用 `{{cwd}}` 并按真实路径分区持久化):推迟到 session-cwd 方案重新讨论时。 ## 交付的不变式 -- coding-agent 提示词通过一条组装路径渲染:identity、带插值模型名的 persona,然后是 fs/bash/web 指导。 +- coding-agent 的 prompt 通过一条组装路径依次渲染 identity、带插值模型名的 persona,然后是 fs/bash/web 指导。 - fork 和 fresh subagent 的描述反映 provider 是否继承已完成的对话轮次;工具随 provider 生命周期变化而出现、消失和重新措辞。 -- 未知、无值、格式错误或不平衡的变量引用会指名 section 并抛出异常;重复的 section、变量和工具注册也会抛出异常。 -- 快照回放与提示词无关:它按轮次和步骤索引已录制的 chunk 流,不比较发出的请求。 +- 未知、无值、格式错误或不平衡的变量引用会指明 section 名称并抛出异常;重复的 section、变量和工具注册同样抛出异常。 +- 快照回放与 prompt 无关:它按轮次和步骤索引已记录的 chunk 流,不比较发出的请求。 ## 后果 -- 组装后的提示词中每个事实现在恰好有一个归属方,叶子 YAML 中手写的工具行文已消除:加载或卸载一个工具插件不再需要编辑任何部署的 persona。 -- `{{model}}` 在组装时反映 `AgentOptions.model`。如果一个插件在 `agent/request` waterfall 中切换模型,提示词中的声明在该步骤就会过时;如果一个插件在那里**提供**模型(options.model 未设置——循环文档记载的回退路径),变量在渲染时无值,含 `{{model}}` 的 persona 会在 waterfall 运行前失败。两者的补救方式相同,且正是归属规则本身:拥有该延迟绑定模型事实的插件在 `system-prompt/assemble` waterfall 上提前声明它(`assembly.variables['model'] = …`)——一个归属方,两处声明;一个循环测试端到端固定了 supply 路径。已接受。 -- 当一个已绑定的 provider 不在位(尚未激活、已卸载、HMR(热模块替换)重载中)时,subagent 工具不存在,该窗口内的模型请求只是缺少它。这是诚实的状态——替代方案是一个描述或执行都不可信的已注册工具。 -- 严格性意味着 persona 可能在渲染时导致轮次失败(例如在无 cwd 的会话上使用 `{{cwd}}`)。失败是受控的——该轮次以 `error` 结束,循环存活——而且这是一个我们希望大声暴露的撰写错误。 -- 目前没有在 prompt 行文中转义字面 `{{name}}` 的语法;如果真实 prompt 确实需要,届时再添加。 +- 组装后的 prompt 中每个事实现在恰好有一个归属方,leaf YAML 中手工维护的工具行文已消除:加载或卸载一个工具插件不再需要编辑任何部署的 persona。 +- `{{model}}` 在组装时反映 `AgentOptions.model`。如果一个插件在 `agent/request` waterfall 中切换模型,prompt 对该步骤的声明就会过时;如果一个插件在那里**提供**模型(options.model 未设置——循环文档中记载的回退路径),变量在渲染时无值,包含 `{{model}}` 的 persona 会在 waterfall 运行前失败。两者的补救方式相同,就是归属规则本身:拥有延迟绑定模型事实的插件在 `system-prompt/assemble` waterfall 上提前声明它(`assembly.variables['model'] = …`)——一个归属方,两处声明;一个循环测试端到端固定了 supply 路径。已接受。 +- 当一个已绑定的 provider 不存在时(尚未激活、已卸载、HMR(热模块替换)重载中),subagent 工具不存在,该窗口内的模型请求中不会包含它。这是诚实的状态——替代方案是注册一个 description 或执行都不可信的工具。 +- 严格性意味着 persona 可能在渲染时导致轮次失败(例如在无 cwd 的会话上使用 `{{cwd}}`)。失败是受控的——该轮次以 `error` 结束,循环存活——且这是一个我们**希望**大声暴露的撰写错误。 +- 目前没有在 prompt 行文中转义字面 `{{name}}` 的语法;如果真实 prompt 确实需要,再行添加。 diff --git a/docs/rfc/implemented/architecture/2026-07-05-reconstructable-requests.i18n.yaml b/docs/rfc/implemented/architecture/2026-07-05-reconstructable-requests.i18n.yaml index 7357dfe0f8..2ef938def7 100644 --- a/docs/rfc/implemented/architecture/2026-07-05-reconstructable-requests.i18n.yaml +++ b/docs/rfc/implemented/architecture/2026-07-05-reconstructable-requests.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-05-reconstructable-requests.md: 0978cd8760c6a0420be1bf0a3baf6b50c1a04a13 -2026-07-05-reconstructable-requests.zh.md: 82f9cf085db3d6cd408e54a8e7cf99082d848176 +2026-07-05-reconstructable-requests.zh.md: a4864c9e795ccc0da2cbbf0ca4d17a90c029858c diff --git a/docs/rfc/implemented/architecture/2026-07-05-reconstructable-requests.zh.md b/docs/rfc/implemented/architecture/2026-07-05-reconstructable-requests.zh.md index 82f9cf085d..a4864c9e79 100644 --- a/docs/rfc/implemented/architecture/2026-07-05-reconstructable-requests.zh.md +++ b/docs/rfc/implemented/architecture/2026-07-05-reconstructable-requests.zh.md @@ -1,4 +1,4 @@ -# RFC:每个 LLM 请求都可从会话日志重建 +# RFC:每个 LLM(大语言模型)请求都可从会话日志重建 Status: implemented @@ -6,50 +6,50 @@ Status: implemented ## 问题 -请求流水线此前不保证前缀稳定性以利用提供方缓存,会话日志也无法重建模型实际看到的内容。日志遗漏了 model、系统提示词和工具 schema,同时允许逐次调用的请求改写。因此缓存行为和回放等价性取决于碰巧加载了哪些插件。 +请求流水线未能保证前缀稳定性以利用提供方缓存,会话日志也无法重建模型实际看到的内容。日志遗漏了 model、系统提示词和工具 schema,同时允许逐次调用的请求改写。因此缓存行为和回放等价性取决于碰巧加载了哪些插件。 -快乐路径的参考形态是 MiniCode 的 `LLMClient`:一个有状态的对话客户端,随对话推进只追加、从不重建,仅在系统提示词、工具集或压缩(compaction)真正改变了模型必须看到的内容时才重置。本 RFC 回答的设计问题是:如何在不放弃事件溯源的前提下获得这种纪律。 +快乐路径的参考形态是 MiniCode 的 `LLMClient`:一个有状态的对话客户端,随对话推进只做追加而不重建,仅在系统提示词、工具集或压缩(compaction)真正改变了模型需要看到的内容时才重置。本 RFC 回答的设计问题是:如何在不放弃事件溯源的前提下获得这种纪律。 ## 决策 ### 原则 -**模型可见 ⟺ 已记录。** 凡到达模型请求的内容,都必须记录在会话日志中。可检查的推论:**循环发出的每个对话请求都是会话日志的纯函数**——任何持有日志的人都能逐字节重建它。精确的范围说明:保证覆盖循环构建的 `GenerateOptions`;提供方协议格式(wire format)字节由它推导而来,因为两个适配器的序列化在固定代码版本下都是逐消息的纯函数;直接的一次性调用(压缩的 summarize 调用)记录其信封标量(`compact/summary.{model, maxTokens}`),其输入是对已记录区域的确定性代码运算——可从日志加代码重建,通过 unfrozen-request 标记排除在不变式之外。 +**模型可见 ⟺ 已记录。** 凡到达模型请求的内容都必须记录在会话日志中。可检查的推论:**循环发出的每个对话请求都是会话日志的纯函数**——任何人持有日志即可逐字节重建请求。精确的范围声明:该保证覆盖循环构建的 `GenerateOptions`;提供方协议格式(wire format)字节由此推导而来,因为两个适配器的序列化在固定代码版本下都是逐消息的纯函数;直接的一次性调用(压缩的 summarize 调用)记录其信封标量(`compact/summary.{model, maxTokens}`),其输入是对日志区域的确定性代码运算——可从日志加代码重建,通过 unfrozen-request 标记排除在不变式之外。 -前缀缓存稳定性是推论 #1,而非标题:一个仅追加的日志经逐节点纯函数投影,在 header 不变时自然产出前一请求的追加扩展——稳定性是涌现的,不是管理出来的。逐字节精确的审计/回放是推论 #2;带*可归因*漂移的恢复与 fork 是推论 #3。 +前缀缓存稳定性是推论 #1,而非标题:一个仅追加的日志经逐节点纯函数投影,在 header 不变时自然产出前一请求的追加扩展——稳定性是涌现的,不是管理出来的。字节精确的审计/回放是推论 #2;带*可归因*漂移的恢复与 fork 是推论 #3。 ### 机制 -**消息。** `Session.deriveMessages()` 带缓存:每个 surface 节点在首次出现时通过公开的逐节点函数 `deriveEventMessage(event)` 精确投影一次;surface 改写(压缩的 `replace`——`SurfaceManager.replaceGeneration`)触发重建。调用方每次获得一个新数组,其中的消息是共享的、深度冻结的:通过投影修改已记录的历史是不可表达的(会抛异常),取代了旧的每次调用克隆隔离。外部重建器对日志前缀折叠同一个公开函数,因此不可能有两条路径产生分歧。 +**消息。** `Session.deriveMessages()` 带缓存:每个 surface 节点在首次出现时通过公开的逐节点函数 `deriveEventMessage(event)` 精确投影一次;surface 重写(压缩的 `replace`,即 `SurfaceManager.replaceGeneration`)触发重建。调用方每次获得一个新数组,底层是共享的深度冻结消息:通过投影变异已记录的历史是不可表达的(会抛异常),取代了旧的逐次调用克隆隔离。外部重建器对日志前缀折叠同一个公开函数,因此不可能有两条路径产生分歧。 -`EpochHeader` 记录请求的非历史状态:调用配置、渲染后的系统提示词、工具 schema 和会话前缀,空值规范化为缺失。`request/header` 写入完整的初始、恢复或回退快照。`request/header-delta` 通过公共前缀/后缀行裁剪编码系统提示词变更,通过按名称键控的增/删/改编码工具变更,通过完整替换编码配置或前缀变更。`foldRequestHeader`、`diffHeader` 和 `applyHeaderDelta` 是纯编解码器。每个循环实例在其首次请求时写入一个快照,以锚定进程边界。Delta 仅是优化:写入方验证往返等价性,对不可表达的变更(如纯工具重排序)回退到完整快照。 +`EpochHeader` 记录请求的非历史状态:调用配置、渲染后的系统提示词、工具 schema 和会话前缀,空值规范化为缺失。`request/header` 写入完整的初始、恢复或回退快照。`request/header-delta` 通过公共前缀/后缀行裁剪编码系统变更,通过按名称键控的增/删/改编码工具变更,通过完整替换编码配置或前缀变更。`foldRequestHeader`、`diffHeader` 和 `applyHeaderDelta` 是纯编解码器。每个循环实例在首次请求时写入一个快照以锚定进程边界。delta 只是优化:写入方验证往返等价性,对无法表达的变更(如纯工具重排序)回退到完整快照。 -每一步重建 prompt 组装。实例的第一步中,`agent/session-prefix` 用仅限请求的开场消息扩展一个冻结的空种子;结果被冻结并缓存于该循环实例。`agent/pre-step` 随后在消息快照紧接 `step/start` 之前接收组合后的前缀。首次调用配置从显式的 `AgentOptions` 出发,保留 fork 覆盖和恢复重配置;后续调用从折叠后的 header 出发。`agent/request` 只能替换那个冻结的配置种子,而模型可见的内容通过已记录的通道进入。循环记录欠写的 header 事件(前缀唯一的持久化归属),从前缀、快照和 header 构建 `GenerateOptions`,并深度冻结它,同时保持 `AbortSignal` 活跃。每实例状态仅有缓存的前缀和其锚定快照是否已写入。 +每个步骤重建 prompt 组装。在实例的首个步骤中,`agent/session-prefix` 以一个冻结的空种子为基础,用仅限请求的开场消息进行扩展;结果被冻结并缓存于该循环实例。`agent/pre-step` 随后接收组合后的前缀,消息在 `step/start` 之前立即被快照。首次调用配置从显式的 `AgentOptions` 出发,保留 fork 覆盖和恢复重配置;后续调用从折叠后的 header 出发。`agent/request` 只能替换那个冻结的配置种子,模型可见内容通过已记录的通道进入。循环记录欠下的 header 事件(前缀唯一的持久归宿),从前缀、快照和 header 构建 `GenerateOptions`,对其深度冻结但保持 `AbortSignal` 活跃。每实例状态仅有缓存的前缀和锚定快照是否已写入。 -**`step/start` 是重建边界。** 一步从该序列之前的事件派生消息。快照之后的注入加入下一次请求,事件发布期间的重入追加被拒绝。`agent/pre-step` 是当前请求所需内容的 seam。Header 重建折叠该步骤自身的 `request/header*` 事件,或在无新 header 写入时沿用前一次折叠结果。 +**`step/start` 是重建边界。** 一个步骤从该序列之前的事件推导消息。快照之后的注入加入下一次请求,事件发布期间的重入追加被拒绝。`agent/pre-step` 是当前请求所需内容的 seam。header 重建通过该步骤自身的 `request/header*` 事件折叠,或在无新 header 写入时沿用前一次折叠结果。 -**强制执行。** 在开发环境中,`dsh-invariants` 通过一个全新的 `Session` 独立重建每个循环请求,使活跃缓存无法为自身背书,然后在 `llm/stream` 处比较消息和折叠后的 header 字段。循环请求通过其冻结形态和 session id 识别;直接的一次性调用被排除。正确性依赖于序列有界的重建而非监听器顺序。带密钥的 e2e 要求首次请求之后出现正数的 cache-read token;逐步 usage 是生产信号,header 变更或压缩表现为下一步 cache-read 的下降。 +**强制执行。** 在开发环境中,`dsh-invariants` 通过一个全新的 `Session` 独立重建每个循环请求,使活跃缓存无法为自身背书,然后在 `llm/stream` 处比较消息和折叠后的 header 字段。循环请求通过其冻结形状和 session id 识别;直接的一次性调用被排除。正确性依赖于序列有界的重建,而非监听器顺序。带密钥的 e2e 要求首次请求之后有正值的 cache-read token;逐步骤用量是生产信号,header 变更或压缩表现为下一步骤的 cache-read 下降。 ### MiniCode 形态:采纳,但溯源箭头反转 -与 MiniCode 一样,对话仅追加推进,仅在模型可见状态变更时重置。与 MiniCode 不同的是,事件日志仍是真源,因为它还拥有持久化、恢复、边界、工具配对和溯源。`Session` 缓存从日志派生的消息和 header 折叠结果,使每个请求都可独立检查。 +与 MiniCode 相同,对话仅追加推进,仅在模型可见状态变更时重置。与 MiniCode 不同,事件日志仍是真源,因为它同时拥有持久化、恢复、边界、工具配对和溯源。`Session` 缓存从日志推导的消息和 header 折叠结果,使每个请求都可独立检查。 ## 曾考虑的替代方案 -- **客户端作为真源**(照搬 MiniCode):在日志之外出现第二个生效的真相——两者漂移而无人察觉;见上节。 +- **客户端作为真源**(照搬 MiniCode):在日志之外多出一个运行时真相——两者漂移而无人察觉;见上节。 - **镜像日志的有状态传输客户端**:重复对话状态,需要围绕监听器做回滚,留下未记录的编辑面,且仍无法重建请求 header。Session 拥有的缓存加已记录的 header 避免了这些分裂的真相。 -- **逐次调用的请求标量**(每次 `agent/request` 分发时传入一个可自由修改的配置):监听器可以零记账地逐次切换 model,悄然放弃本设计旨在保护的提供方缓存。配置是逐对话的已记录状态;waterfall(瀑布式事件)提议,日志记录。 -- **检测并报告**(比较连续请求,发现分歧时警告):事后捕获违规;违规请求仍可构造并发出。因接口层面的不可表达性而否决。 -- **事件驱动组装**(仅在变更信号时重新渲染):存在信号遗漏的 bug 类别——会话中途注册的工具发出 `tools/change` 而非 `system-prompt/change`,第三方提供方可能什么都不发。逐步渲染加值比较在零信号纪律下仍然健壮。 -- **Header 事件上的叙事字段**(delta 上的 `reason`/`changed` 列表):可通过 diff 连续事件派生——每个事实只有一个归属;快照携带 reason 是因为锚点的成因无法从数据本身派生。 +- **逐次调用的请求标量**(一个可自由变异的配置传给每次 `agent/request` 分发):监听器可以零记账地逐次切换 model,悄然放弃本设计旨在保护的提供方缓存。配置是逐对话的已记录状态;waterfall(瀑布式事件)提议,日志记录。 +- **检测并报告**(比较连续请求,发散时告警):事后捕获违规;违规请求仍可构造并发出。因接口层面的不可表达性而否决。 +- **事件驱动组装**(仅在变更信号时重新渲染):存在漏信号的 bug 类别——会话中途注册的工具发出 `tools/change` 而非 `system-prompt/change`,第三方提供方可能什么都不发。逐步骤渲染加值比较在零信号纪律下即可稳健工作。 +- **Header 事件上的叙事字段**(delta 上的 `reason`/`changed` 列表):可通过 diff 连续事件推导——每个事实只有一个归宿;快照携带 reason 是因为锚点的成因无法从数据推导。 ## 后果 -- 一个无法由日志解释的请求不可能被意外构造——无论是循环还是监听器;修改已构建的请求会抛异常;每次 header 变更都是一个持久的、可 diff 的日志事件。 -- 在建议通道之间做选择是变更频率决策,而本设计让稳定的那个成为结构性的:`agent/session-prefix` 的贡献在每个循环实例中只组合一次并逐字复用,因此它以零边际成本扩展可缓存前缀,且**不可能**在会话中途击穿提供方缓存;会话中途变化的内容通过仅追加的历史通道流入——`agent.inject()`、`tools/post-execute` 决策的 `additionalContext`、prompt-submit 的 `additionalContext`——每个都是持久的 `context/message`,付出一次代价后即享受前缀缓存,代价是在历史和日志中累积。将会话冻结的开场内容路由到前缀,将变更通知路由到历史通道;逐步的仅限请求尾部槽位被有意放弃(无消费方,且持久追加覆盖了所有当前更新模式)。 -- 在提供方处仍需全价的内容是固有的且已记录的:压缩(其 `compact/*` 事件和 replace 节点)、真正的 prompt/工具变更(`request/header-delta`)、配置切换(同上)、带漂移的进程边界(`'resume'` 快照与前一个不同)。提供方自身的 reasoning-content 排除由服务端管理。 +- 一个日志无法解释的请求不可能被意外构造——无论是循环还是监听器;变异已构建的请求会抛异常;每个 header 变更都是持久的、可 diff 的日志事件。 +- 在建议性通道之间做选择是变更频率的决策,而本设计使稳定的那个在结构上成为默认:`agent/session-prefix` 的贡献在每个循环实例中只组合一次并逐字复用,因此以零边际成本扩展可缓存前缀,且**不可能**在会话中途击穿提供方缓存;会话中途变化的内容通过仅追加的历史通道流入——`agent.inject()`、`tools/post-execute` 决策的 `additionalContext`、prompt-submit 的 `additionalContext`——每条都是持久的 `context/message`,付出一次代价后即被前缀缓存,代价是在历史和日志中累积。将会话冻结的开场内容路由到前缀,将变更通知路由到历史通道;逐步骤的仅限请求尾部槽位被有意放弃(无消费方,且持久追加覆盖了当前所有更新模式)。 +- 在提供方处仍需全价计算的内容是固有的且已记录的:压缩(其 `compact/*` 事件和 replace 节点)、真正的 prompt/工具变更(`request/header-delta`)、配置切换(同上)、带漂移的进程边界(`'resume'` 快照与前一快照不同)。提供方自身的 reasoning-content 排除由服务端管理。 - `step/start` 监听器行为变更(见上文)是对插件唯一可观察的语义变更;`agent/pre-step` 是当前请求的 seam。 -- 工具结果裁剪(计划中)无需新机制:一个已记录的单节点 surface replace(`start === end`),携带同一 `callId` 下裁剪后的 `tool/result`——属于压缩家族,回放正确,缓存击穿由相同的压力逻辑批量处理。 -- 会话日志每个对话增长一个 `request/header` 快照(系统提示词 + 工具 schema:主导项),加上真正变更时的 delta——相对于 `assistant/chunk` 的体量很小;`SESSION_FORMAT_VERSION` 保持 `0`(预发布期间的变动被吸收,后端拒绝而非迁移)。 -- 快照 golden 文件变更一次(每份 transcript 增加其 header 事件);写文件系统的 fixture 以规范化的撰写形式存储,工具参数使用 cwd 相对路径,因为回放只往返 cwd 无关的参数路径。 -- FIXME(call-config-shape):重新审视 `LlmCallConfig` 的确切字段集——哪些字段对缓存而言真正属于 epoch 级别(`model` 毫无疑问;采样标量出于谨慎放在那里),以及当适配器需要时,提供方特有的额外项(reasoning 选项、额外 body 参数)应归属何处。 +- 工具结果裁剪(计划中)无需新机制:一个已记录的单节点 surface replace(`start === end`),携带同一 `callId` 下裁剪后的 `tool/result`——属压缩家族,回放正确,缓存击穿由相同的压力逻辑批量处理。 +- 会话日志每个对话增长一个 `request/header` 快照(系统提示词 + 工具 schema:主导项),加上真正变更时的 delta——相对 `assistant/chunk` 的体量很小;`SESSION_FORMAT_VERSION` 保持 `0`(预发布期间的变动被吸收,后端拒绝而非迁移)。 +- 快照 golden 文件变更一次(每个 transcript(文本记录)增加其 header 事件);写入文件系统的 fixture(测试前置数据)以规范化的撰写形式存储,工具参数使用 cwd 相对路径,因为回放只对 cwd 无关的参数路径做往返。 +- FIXME(call-config-shape):重新审视 `LlmCallConfig` 的确切字段集——哪些字段对缓存而言真正属于 epoch 级别(`model` 毫无疑问;采样标量出于谨慎放在那里),以及当适配器需要时,提供方特定的额外项(reasoning 选项、额外 body 参数)应归属何处。 diff --git a/docs/rfc/implemented/architecture/2026-07-05-subagent-provider-lifecycle-events.i18n.yaml b/docs/rfc/implemented/architecture/2026-07-05-subagent-provider-lifecycle-events.i18n.yaml index 1bf06ce567..74fbfda121 100644 --- a/docs/rfc/implemented/architecture/2026-07-05-subagent-provider-lifecycle-events.i18n.yaml +++ b/docs/rfc/implemented/architecture/2026-07-05-subagent-provider-lifecycle-events.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-05-subagent-provider-lifecycle-events.md: 6d711f2a6d8496a8a229ec63d86dd89816efb6f8 -2026-07-05-subagent-provider-lifecycle-events.zh.md: 45eedcfdfa834722000c9f2c15e3955ced791c2f +2026-07-05-subagent-provider-lifecycle-events.zh.md: f412b031644c14ca70caae3efc72eefa9ce2c2ac diff --git a/docs/rfc/implemented/architecture/2026-07-05-subagent-provider-lifecycle-events.zh.md b/docs/rfc/implemented/architecture/2026-07-05-subagent-provider-lifecycle-events.zh.md index 45eedcfdfa..f412b03164 100644 --- a/docs/rfc/implemented/architecture/2026-07-05-subagent-provider-lifecycle-events.zh.md +++ b/docs/rfc/implemented/architecture/2026-07-05-subagent-provider-lifecycle-events.zh.md @@ -1,36 +1,36 @@ # RFC:Subagent 提供方生命周期事件——`subagent/provider-added` / `subagent/provider-removed` -Status: implemented - [English](2026-07-05-subagent-provider-lifecycle-events.md) | 中文 +Status: implemented + ## 问题 -[prompt-variables RFC](2026-07-05-prompt-variables-and-tool-guidance-ownership.md) 使 `dsh-tool-subagent` 从其提供方**派生**面向模型的措辞:`SubagentProvider.inheritsParentContext`(spawn/ACP 为 `false`,fork 为 `true`)同时驱动工具描述和 `prompt` 参数描述(`providerWording`),从而让 fork 工具不再在上下文继承问题上对模型撒谎。这一修复引入了跨 fiber 的数据依赖:工具描述在**工具注册时**就已固定(这是有意为之——描述是 tool-choice 引导所在之处),但提供方在自己的插件 fiber 上到达,时机不确定。 +[prompt-variables RFC](2026-07-05-prompt-variables-and-tool-guidance-ownership.md) 让 `dsh-tool-subagent` 从其提供方**派生**面向模型的措辞:`SubagentProvider.inheritsParentContext`(spawn/ACP 为 `false`,fork 为 `true`)同时驱动工具描述和 `prompt` 参数描述(`providerWording`),使 fork 工具不再在上下文继承问题上对模型撒谎。这一修复引入了跨 fiber 的数据依赖:工具描述在**工具注册时**固定(这是有意为之——描述是 tool-choice 引导所在之处),但提供方在自己的插件 fiber 上到达,时机不确定。 -如果在工具插件的 `apply` 时刻解析提供方,就会产生隐式的加载顺序要求("在 cordis.yml 中把后端列在工具前面")。这一要求行不通,因为 Cordis Loader 并发启动同级条目,且 `Entry.init()` 不等待激活完成:一个延迟到达的后端可能导致工具 fiber 失败,即使它在配置中列在前面也是如此。Loader 不提供同级顺序保证——"异步状态不是同步状态"(见[防御性模式](../../../defensive-patterns.md))。 +如果在工具插件的 `apply` 时刻解析提供方,就会产生一个隐式的加载顺序要求("在 cordis.yml 中把后端列在工具前面")。这个要求不成立,因为 Cordis Loader 并发启动同级条目,且 `Entry.init()` 不会等待激活完成:延迟到达的后端即使列在前面,也可能让工具 fiber 失败。Loader 不提供同级顺序保证——"异步状态不是同步状态"(见[防御性模式](../../../defensive-patterns.md))。 ## 决策 -注册表将提供方的成员变动作为类型化事件广播,消费方镜像这些事件而非假设顺序: +注册表将提供方的成员变化作为类型化事件广播,消费方镜像这些事件而非假设顺序: - **`subagent/provider-added(provider)`**:一个提供方在 `ctx.subagents` 注册表中变为可解析。在注册时发出。 -- **`subagent/provider-removed(name)`**:一个提供方离开了注册表(其插件 fiber 被 dispose——卸载或 HMR 重载)。从注册的 disposer 中发出。 +- **`subagent/provider-removed(name)`**:一个提供方离开注册表(其插件 fiber 被 dispose(资源释放)——卸载或 HMR(热模块替换)重载)。从注册的 disposer 中发出。 -`dsh-tool-subagent` 镜像其命名提供方的生命周期:当提供方可用(或变为可用)时注册工具——在那一刻从该提供方派生措辞;当提供方离开时注销工具;在重新注册时(HMR 重载)重新派生。提供方不在时工具不存在,因此不可能对模型撒谎。这里**刻意不留**任何需要文档化的加载顺序要求:事件使顺序问题消失,而非将其钉死。 +`dsh-tool-subagent` 镜像其命名提供方的生命周期:当提供方可用(或变为可用)时注册工具——在那一刻从该提供方派生措辞——当提供方离开时注销工具,并在重新注册时(HMR 重载)重新派生。提供方不在时工具不存在,因此不会对模型撒谎。这里有意**不留下**任何需要文档化的加载顺序要求:事件让顺序问题消失,而非将其钉死。 -这些事件还补全了该 seam 的词汇:`ctx.subagents` 是一个命名注册表,多个委派后端(`spawn`、`fork`、`acp`)在其上共存;一个内容会被其他插件用来派生状态的注册表,应当以类型化事件广播成员变动,而非要求轮询或依赖加载顺序。 +这些事件还完善了 seam 的词汇:`ctx.subagents` 是一个命名注册表,多个委派后端(`spawn`、`fork`、`acp`)在其上共存;一个其他插件从中派生状态的注册表,应当以类型化事件广播成员变化,而非要求轮询或依赖加载顺序。 ## 曾考虑的替代方案 -- **在 `apply` 时解析提供方,不存在则抛异常**:否决。"先列后端"会声称一个 Loader 并不提供的顺序保证。 -- **重试查找(轮询直到提供方出现)**:最终会收敛,但在框架已有的机制(effect 注册 + disposal)之外自行发明了一套私有就绪协议;而且它无法感知提供方**离开**,因此 HMR 会让一个措辞描述着已 dispose 后端的工具滞留。 -- **仅在 section 中放置 subagent 措辞,在组装时延迟解析**:同样能容忍任意加载顺序,但把 tool-choice 引导移出了描述,与 prompt-variables RFC 确立的归属规则相矛盾(每个工具的语义和使用时机属于描述)。响应式注册既保持描述的权威性,又不依赖顺序。 -- **根据提供方名称而非提供方对象来确定措辞**:`providerName` 本身是配置,重命名提供方后会静默获得错误的措辞;从已解析提供方自身的 `inheritsParentContext` 派生则不会漂移。 +- **在 `apply` 时解析提供方,不存在则抛异常**:否决。"先列后端"这一要求声称了 Loader 并不存在的顺序保证。 +- **重试查找(轮询直到提供方出现)**:最终能收敛,但在框架已有的机制(effect 注册 + disposal)之外发明了一套私有就绪协议;它也无法感知提供方**离开**,因此 HMR 会遗留一个措辞描述已 dispose 后端的工具。 +- **仅在 section 中放置 subagent 措辞,在组装时惰性解析**:同样能容忍任意加载顺序,但将 tool-choice 引导移出了**描述**,与 prompt-variables RFC 建立的所有权规则相矛盾(每个工具的语义和何时使用属于描述)。响应式注册既保持描述的权威性,又不依赖顺序。 +- **根据提供方名称而非提供方对象确定措辞**:`providerName` 本身是配置,重命名后的提供方会静默获得错误的措辞;从已解析提供方自身的 `inheritsParentContext` 派生则不会漂移。 ## 后果 - 从命名提供方派生状态的消费方响应 `subagent/provider-added`/`-removed` 事件,而非在 `apply` 时读取注册表;`dsh-tool-subagent` 是参考实现。 -- **添加时大声失败;移除时按监听器隔离。** 添加监听器可以回滚注册。移除在 disposal 期间运行,因此单个监听器抛异常只会被记录,不会饿死后续镜像或扰乱拆卸流程。`start()` 仍然在每次运行时按名称解析提供方,防止陈旧工具调用已移除的后端。见[事件目录](../../../cordis-catalog/events.md)和[生产者/消费者映射](../../../event-producer-consumer.md)。 -- **工具不存在的窗口期。** 在后端 disposal 与重新注册之间(HMR 重载),模型看不到 subagent 工具。这是诚实的状态——替代方案是一个向空处派发的工具——工具注册表的 `tools/change` 事件确保 prompt 组装保持最新。 -- **两个等待中的 fiber 共享同一 `toolName` 是无效配置,且被延迟捕获。** 如果两个 `dsh-tool-subagent` 实例命名了不同的提供方但相同的 `toolName`,二者都会等待,先到达的提供方触发注册;第二个注册仅在**其**提供方到达时才抛异常。插件中的 `TODO(subagent-dup-toolname)` 记录了这一爆炸半径;工具注册表的重名拒绝机制仍是最终兜底。 +- **添加时大声失败;移除时按监听器隔离。** 添加监听器可以回滚注册。移除在 disposal 期间运行,因此单个监听器抛异常只会被记录日志,不会饿死后续镜像或干扰拆解流程。`start()` 仍在每次运行时按名称解析提供方,防止陈旧工具调用已移除的后端。见[事件目录](../../../cordis-catalog/events.md)与[生产者/消费者映射](../../../event-producer-consumer.md)。 +- **工具不存在的窗口期。** 在后端 disposal 与重新注册之间(HMR 重载期间),模型看不到 subagent 工具。这是诚实的状态——替代方案是一个向空处分发的工具——工具注册表的 `tools/change` 事件发出会保持 prompt 组装的时效性。 +- **两个等待中的 fiber 共享同一 `toolName` 是无效配置,被延迟捕获。** 如果两个 `dsh-tool-subagent` 加载实例命名了不同的提供方但相同的 `toolName`,两者都会等待,先到达的提供方先注册;第二次注册仅在**其**提供方到达时才抛异常。插件中的 `TODO(subagent-dup-toolname)` 记录了这一影响范围;工具注册表的重名拒绝机制仍是最终防线。 diff --git a/docs/rfc/implemented/architecture/2026-07-06-timeout-deadline-library.i18n.yaml b/docs/rfc/implemented/architecture/2026-07-06-timeout-deadline-library.i18n.yaml index cc697c3e30..a051d20478 100644 --- a/docs/rfc/implemented/architecture/2026-07-06-timeout-deadline-library.i18n.yaml +++ b/docs/rfc/implemented/architecture/2026-07-06-timeout-deadline-library.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-06-timeout-deadline-library.md: 9906aa7cce40ffd7b5f7082199d05edb8d0a54b2 -2026-07-06-timeout-deadline-library.zh.md: ef99a1a051f3cedbe5a2770e5bbeeebe716745c4 +2026-07-06-timeout-deadline-library.zh.md: dd1a60f9d7b91575b32e1cc85b3800cf823b6a6d diff --git a/docs/rfc/implemented/architecture/2026-07-06-timeout-deadline-library.zh.md b/docs/rfc/implemented/architecture/2026-07-06-timeout-deadline-library.zh.md index ef99a1a051..dd1a60f9d7 100644 --- a/docs/rfc/implemented/architecture/2026-07-06-timeout-deadline-library.zh.md +++ b/docs/rfc/implemented/architecture/2026-07-06-timeout-deadline-library.zh.md @@ -1,22 +1,22 @@ -# RFC:共享的超时/截止时间原语,hard-kill 留给各能力自行实现 - -[English](2026-07-06-timeout-deadline-library.md) | 中文 +# RFC:共享的超时/截止时间原语,硬终止留给各能力自行实现 Status: implemented +[English](2026-07-06-timeout-deadline-library.md) | 中文 + ## 问题 -超时处理在各个承载工具的能力之间逐渐分化,而这种分化并非表面的——同一套逻辑被三种方式各自重新实现,每种都带着自己微妙的正确性负担。 +超时处理在各个承载工具的能力之间逐渐分化,而且这种分化并非表面的:同一套逻辑被以三种方式重新实现,各自带有微妙的正确性负担。 -- **bash**([packages/bash/bash-local/src/run.ts](../../../../packages/bash/bash-local/src/run.ts))在进程管道内部有一套完整、正确的超时实现:一个经配置钳位的 `timeoutMs`,两个独立触发器——用于超时的 `killTimer` 和用于上游取消的 `onAbort` 监听器——各自调用同一个 `kill()` 闭包,该闭包对进程组执行 SIGTERM→宽限期→SIGKILL 升级,以及两个正交的结果布尔值(`timedOut`、`aborted`)各自独立锁存。 -- **web_fetch**([packages/web/web-fetch-local/src/provider.ts](../../../../packages/web/web-fetch-local/src/provider.ts))有一套正确但*手工搭建*的超时:它构造一个 `AbortController`,接入 `setTimeout(() => controller.abort(new WebError(…, 'WEB_FETCH_TIMEOUT')))`,手动添加和移除上游信号监听器,在 `finally` 中清除定时器,并在 `translateAbortOrNetwork` 辅助函数中从 `signal.reason` 恢复超时原因——因为 reader 只抛出裸 `AbortError`。 -- **web_search**([packages/web/tool-web/src/search.ts](../../../../packages/web/tool-web/src/search.ts))**完全没有超时**:`WebSearchRequest`([packages/web/web/src/types.ts](../../../../packages/web/web/src/types.ts))不携带 `timeoutMs` 字段,各提供方的 `search()` 只转发 `exec.signal`。(web_search 在本 RFC 中保持无超时——见「后果」。) +- **bash**([packages/bash/bash-local/src/run.ts](../../../../packages/bash/bash-local/src/run.ts))在进程管道内部有一套完整、正确的超时实现:一个经配置钳位的 `timeoutMs`,两个独立触发器(用于超时的 `killTimer` 和用于上游取消的 `onAbort` 监听器),各自调用同一个 `kill()` 闭包对进程组执行 SIGTERM→宽限期→SIGKILL 升级,以及两个正交的结果布尔值(`timedOut`、`aborted`)独立锁存。 +- **web_fetch**([packages/web/web-fetch-local/src/provider.ts](../../../../packages/web/web-fetch-local/src/provider.ts))有一套正确但*手写*的超时:构造一个 `AbortController`,连接 `setTimeout(() => controller.abort(new WebError(…, 'WEB_FETCH_TIMEOUT')))`,手动添加和移除上游信号监听器,在 `finally` 中清除定时器,并在 `translateAbortOrNetwork` 辅助函数中从 `signal.reason` 恢复超时原因(因为 reader 只抛出裸 `AbortError`)。 +- **web_search**([packages/web/tool-web/src/search.ts](../../../../packages/web/tool-web/src/search.ts))**完全没有超时**:`WebSearchRequest`([packages/web/web/src/types.ts](../../../../packages/web/web/src/types.ts))不携带 `timeoutMs` 字段,各提供方的 `search()` 只转发 `exec.signal`。(web_search 在本次设计中保持无超时——见「后果」。) -每个新的外部进程或网络工具都要重新推导同样四件事:钳位请求值、启动定时器、将超时与上游取消融合、在出口处区分「超时」与「被取消」——而融合和原因恢复恰恰是最容易出微妙错误的部分(web_fetch 的 `signal.reason` 舞步就是证据)。与此同时,各能力执行的*终止*动作不可归约地不同:bash 杀的是 OS 进程组(工作运行在子进程中,在本运行时之外,只能通过信号触达),而 web 中止的是进程内的 `fetch`(undici 拆掉 socket)。不存在一种单一机制能停止所有这些工作。 +每个新的外部进程或网络工具都要重新推导同样四件事:钳位请求值、启动定时器、将超时与上游取消融合、在出口处区分「超时」与「已取消」。而融合与原因恢复恰恰是最容易出微妙错误的部分(web_fetch 的 `signal.reason` 处理就是证据)。与此同时,各能力执行的*终止*操作不可归约地不同:bash 杀死一个 OS 进程组(工作运行在子进程中,在本运行时之外,只能通过信号触达),而 web 中止一个进程内的 `fetch`(undici 拆除 socket)。不存在一个能停止所有能力工作的单一机制。 ## 决策 -`@deepseek-ai/dsh-timeout` 位于 `packages/util/`(与 `dsh-brand` 同级),拥有超时的*计时与分类*这一半;*终止*那一半——hard kill——留在各能力的实现中。它是一个纯函数库,**不是** Cordis 服务或插件:不接收 `ctx`、不注册任何东西、不持有跨调用状态、不发射事件。刻意不设中央「超时服务」——那样的服务必须知道如何停止每个能力的工作,而这正是微内核要排除在共享层之外的知识,也是 Codex 的 `ExecExpiration` 作用域仅限于 exec 家族所示范的。 +`@deepseek-ai/dsh-timeout` 位于 `packages/util/`(与 `dsh-brand` 同级),负责超时的*计时与分类*这一半;*终止*那一半——硬终止——留在各能力的实现中。它是一个纯函数库,**不是** Cordis 服务或插件:不接收 `ctx`、不注册任何东西、不持有跨调用状态、不发射事件。这里刻意不设中央「超时服务」,因为那样的服务必须知道如何停止每个能力的工作——而这正是微内核要排除在共享层之外的知识,也是 Codex 将 `ExecExpiration` 限定于 exec 族所示范的原则。 ### 库的对外接口 @@ -57,42 +57,42 @@ export function deadline( export function timeoutOf(x: AbortSignal | { reason?: unknown }, code?: string): TimeoutReason | undefined ``` -`deadline` 通过 `AbortSignal.any` 将上游信号与定时器融合,附加一个类型化的 `TimeoutReason`,并暴露可 dispose(资源释放)的定时器清理。非正数超时是内部的无超时哨兵,用于后端自有的后台任务;外部提示经 `clampTimeout` 后必须为正有限值。既无定时器也无上游信号时,函数返回一个永不中止的信号,但具有相同的 disposal 形状。提供方将超时原因翻译为 seam 特定的结果。`timeoutOf(signal, code)` 通过 code 限定分类范围,使外层嵌套的 deadline 被视为上游取消而非内层能力的超时。 +`deadline` 通过 `AbortSignal.any` 将上游信号与定时器融合,附加一个类型化的 `TimeoutReason`,并暴露可 dispose(资源释放)的定时器清理。非正数超时是内部的「无超时」哨兵,用于后端拥有的后台任务;外部提示经过 `clampTimeout`,必须为正有限值。既无定时器也无上游信号时,函数返回一个永不中止的信号,具有相同的 disposal 形状。提供方将超时原因转译为 seam 特定的结果。`timeoutOf(signal, code)` 限定分类范围,使外层嵌套的 deadline 被视为上游取消而非内层能力自身的超时。 -### 分工 +### 职责划分 | 关注点 | 负责方 | |---|---| -| 校验请求提示并钳位 default/max | `dsh-timeout`(`clampTimeout`)——纯算术加共享的正有限请求契约 | +| 校验请求提示并钳位默认值/最大值 | `dsh-timeout`(`clampTimeout`):纯算术加共享的正有限请求契约 | | 启动定时器、到期中止、携带 reason、与上游取消融合 | `dsh-timeout`(`deadline`) | | 清除定时器 | `dsh-timeout`(`[Symbol.dispose]`) | -| 中止后分类首个 abort reason | `dsh-timeout`(`timeoutOf`) | +| 中止后对首个 abort reason 进行分类 | `dsh-timeout`(`timeoutOf`) | | **实际终止工作** | 各能力的实现 | -| default/max *值* | 各能力的配置 | +| 默认值/最大值*数值* | 各能力的配置 | | 超时 `code` 字符串 | 各能力(`WEB_FETCH_TIMEOUT` ≠ `BASH_TIMEOUT`) | -信号只*通知*;终止始终是监听者的职责,而监听者因能力而异。bash 自己写 `addEventListener('abort', kill)`,因为 OS 进程活在本运行时之外,没有别的东西会杀它;web 把 `d.signal` 交给 `fetch`,undici 拆掉 socket。这也是文件 read/write/edit 不接受 **`timeoutMs`** 的原因:本地系统调用至多只能尽力中止,超时无法强制 `fsync`/`rename` 停下,加一个超时等于引入一个违反「显式优于隐式」的隐式默认值。两个参考 agent 出于同样的理由都不给文件 I/O 设超时。 +信号只*通知*;终止始终是监听方的职责,而监听方因能力而异。bash 自行编写 `addEventListener('abort', kill)`,因为 OS 进程存在于本运行时之外,没有别的东西会杀死它;web 将 `d.signal` 交给 `fetch`,由 undici 拆除 socket。这也是文件读/写/编辑**不接受** `timeoutMs` 的原因:本地系统调用最多只能尽力中止,超时无法强制 `fsync`/`rename` 停止,添加超时将是一个违反「显式优于隐式」的隐式默认值。两个参考 agent 出于同样的原因对文件 I/O 不设超时。 -### 各能力如何消费 +### 各能力如何消费该库 -- **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 分类为超时或取消。后台启动保持无超时,仅转发上游取消。 +- **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 分类为超时或取消。后台启动保持无超时,仅转发上游取消。 ## 后果 - `runBash` 的结果不再独立锁存 `timedOut` 和 `aborted`;超时与用户中止在进程关闭前竞争时,现在报告单一的首个 abort 原因,而非两者同时为 true。统一的 SIGTERM→宽限期→SIGKILL 终止路径不变,seam 类型 `BashRunResult` 保留两个布尔值(现在互斥),因此 `dsh-tool-bash` 的结果渲染不受影响。 -- `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 基线,已满足)。 +- `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 基线,已满足)。 -不在本 RFC 范围内,列出以标明边界:`web_search` 可以在其 tool-schema/快照覆盖率规划完成后获得可选的面向模型的 `timeout_ms`;未来基于 ripgrep 的文件系统发现工具可以在存在后消费同样的提供方自有 deadline 形状;`tools/execute` waterfall(瀑布式事件)中间件可以通过驱动 `exec.signal` 为每次工具调用设置默认 deadline——那将是一个*消费*本库的插件,仍然只做通知,hard kill 仍是各能力自己的事。 +以下内容不在本次范围内,列出以标明边界:`web_search` 可以在其 tool-schema/snapshot 覆盖率规划就绪后获得可选的面向模型的 `timeout_ms`;未来基于 ripgrep 的文件系统发现工具可以在存在后消费同样的提供方自有 deadline 形状;`tools/execute` waterfall(瀑布式事件)中间件可以通过驱动 `exec.signal` 为每次工具调用设置默认 deadline——那将是一个*消费*本库的插件,仍然只做通知,硬终止仍是各能力自己的事。 ## 曾考虑的替代方案 -**统一的超时*插件* / `ctx.timeout` 服务。** 基于微内核理由否决。一个能停止任何工具工作的服务必须理解每个能力的终止机制(进程组 SIGKILL、socket 拆除、系统调用边界检查)——这正是架构所禁止的「内核知道太多」。Codex 的 `ExecExpiration` 作用域仅限于 exec 家族,正是因为它驱动的 kill(`killpg`)是进程家族特有的;MCP 和 model-stream 各自保有自己的。不存在一个连贯的中间层能为所有东西拥有终止权,因此共享部分只能是纯计时/分类那一半——一个库,而非服务。 +**统一的超时*插件* / `ctx.timeout` 服务。** 基于微内核原则否决。一个能停止任何工具工作的服务必须理解每个能力的终止机制(进程组 SIGKILL、socket 拆除、系统调用边界检查),这正是架构所禁止的「内核知道太多」。Codex 的 `ExecExpiration` 被限定于 exec 族,正是因为它驱动的 kill(`killpg`)是进程族特有的;MCP 和 model-stream 各自保有自己的。不存在一个连贯的中间层能为所有东西拥有终止权,因此共享部分只能是纯计时/分类那一半——一个库,而非服务。 -**每个工具各自实现超时,不共享代码(之前的现状,也是 Claude Code 的选择)。** 否决,因为它已经在产生分化和重复的正确性负担:web_fetch 手工搭建的 controller/reason 逻辑正是未来每个网络/进程工具都要重新推导的,而融合 + `signal.reason` 恢复是容易出错的部分。Claude Code 容忍完全重复;本仓库有一条统一的共享中止通道(每次 `execute` 上的 `exec.signal`),使一个小型共享原语严格更干净,因此成本/收益不同。 +**每个工具各自实现超时,不共享代码(先前的现状,也是 Claude Code 的选择)。** 否决,因为它已经在产生分化和重复的正确性负担:web_fetch 手写了与未来网络/进程类工具各自需要重新推导的完全相同的 controller/reason 逻辑,而融合 + `signal.reason` 恢复正是容易出错的部分。Claude Code 容忍完全重复;本仓库有一个统一的共享 abort 通道(每次 `execute` 上的 `exec.signal`),使得一个小型共享原语严格更优,因此成本/收益不同。 -**用 `withTimeout(promise, ms)` 包装器代替信号工厂。** 否决,因为让 promise 与定时器竞争只是在 deadline 时 resolve *工具调用*的 promise,而不停止底层工作——子进程或 fetch socket 会泄漏。发出信号并要求能力去监听,才能强制一条真正的终止路径存在。这与「dispose 必须达到静止态,而非仅仅请求它」的防御性规则一致。 +**用 `withTimeout(promise, ms)` 包装器代替信号工厂。** 否决,因为让 promise 与定时器竞争只是在截止时间到达时 resolve *工具调用*的 promise,而不会停止底层工作——子进程或 fetch socket 会泄漏。分发信号并要求能力监听,才能强制一条真实的终止路径存在。这与「dispose 必须达到静止状态,而非仅仅请求它」的防御性规则一致。 -**保留 bash 独立的超时和取消触发器。** 否决,因为一个 deadline 信号移除了自建定时器并标准化了分类。竞争的原因报告先到达的那个 abort,而既有的 SIGTERM→SIGKILL 终止路径不变。 +**保留 bash 独立的超时和取消触发器。** 否决,因为一个 deadline 信号移除了定制定时器并标准化了分类。竞争的原因报告先到达的那个 abort,而既有的 SIGTERM→SIGKILL 终止路径保持不变。 diff --git a/docs/rfc/implemented/architecture/2026-07-07-tool-call-timeout-policy.i18n.yaml b/docs/rfc/implemented/architecture/2026-07-07-tool-call-timeout-policy.i18n.yaml index 36a4652f26..31025407be 100644 --- a/docs/rfc/implemented/architecture/2026-07-07-tool-call-timeout-policy.i18n.yaml +++ b/docs/rfc/implemented/architecture/2026-07-07-tool-call-timeout-policy.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-07-tool-call-timeout-policy.md: 0e69c8504dd34dfc4427bf1f3865a80be93eff9d -2026-07-07-tool-call-timeout-policy.zh.md: d842d0a3c811e7502f4d45baba49010a06c5f1a5 +2026-07-07-tool-call-timeout-policy.zh.md: 2f4eca6eff9d135aad3fe538b887402a42ac6f84 diff --git a/docs/rfc/implemented/architecture/2026-07-07-tool-call-timeout-policy.zh.md b/docs/rfc/implemented/architecture/2026-07-07-tool-call-timeout-policy.zh.md index d842d0a3c8..2f4eca6eff 100644 --- a/docs/rfc/implemented/architecture/2026-07-07-tool-call-timeout-policy.zh.md +++ b/docs/rfc/implemented/architecture/2026-07-07-tool-call-timeout-policy.zh.md @@ -1,24 +1,24 @@ # RFC:工具调用超时策略作为插件 -Status: implemented - [English](2026-07-07-tool-call-timeout-policy.md) | 中文 +Status: implemented + ## 问题 -[超时/截止时间 RFC](2026-07-06-timeout-deadline-library.md) 将计时与分类原语提取到了 `@deepseek-ai/dsh-timeout`,但超时策略仍然附着在各个能力和面向模型的 schema 上。`bash` 暴露了 `timeoutMs`;`web_fetch` 暴露了 `timeout_ms`;`web_search` 没有面向模型的超时参数,尽管提供方已经遵守 `exec.signal`;未来的 grep/glob 工具要么直接导入超时库,要么自行发明超时策略。对于一个插件 SDK 来说,这是错误的编写形态:工具作者通常只需将 `exec.signal` 转发给所调用的实现,而部署策略来决定预算。 +[超时/截止时间 RFC](2026-07-06-timeout-deadline-library.md) 将计时与分类原语提取到了 `@deepseek-ai/dsh-timeout`,但超时策略仍然附着在各个能力和面向模型的 schema 上。`bash` 暴露了 `timeoutMs`;`web_fetch` 暴露了 `timeout_ms`;`web_search` 没有面向模型的超时参数,尽管提供方已经遵循 `exec.signal`;未来的 grep/glob 工具要么直接导入超时库,要么自行发明超时策略。对于一个插件 SDK 来说,这是错误的编写范式:工具作者通常只需将 `exec.signal` 转发给其调用的实现,而部署策略来决定预算。 -与此同时,仓库中并非所有超时都是面向模型的工具调用预算。钩子通过直接调用 `ctx.bash` 来执行命令钩子,而非通过 `ctx.tools.execute()`;`bash` 模型工具通过同一后端复用了前台执行、后台启动、后台轮询和钩子调用。一步到位地把所有超时都移入工具插件会混淆这些路径,并有破坏钩子超时语义的风险。 +与此同时,仓库中并非所有超时都是面向模型的工具调用预算。钩子通过直接调用 `ctx.bash` 执行命令钩子,而非通过 `ctx.tools.execute()`;`bash` 模型工具通过同一个后端复用前台执行、后台启动、后台轮询和钩子调用。一步到位地将所有超时移入工具插件会混淆这些路径,并有破坏钩子超时语义的风险。 ## 决策 -工具调用超时是一项仅适用于面向模型的工具执行的策略,由三部分组成: +工具调用超时是仅适用于面向模型的工具执行的策略,由三部分组成: -- `@deepseek-ai/dsh-timeout` 仍然是拥有 `deadline()` 和 `timeoutOf()` 的共享库。 +- `@deepseek-ai/dsh-timeout` 仍是拥有 `deadline()` 和 `timeoutOf()` 的共享库。 - `@deepseek-ai/dsh-tools` 在 `tools/pre-execute` 和 `tools/post-execute` 之间有一个环绕分发的 waterfall(瀑布式事件)`tools/execute`。 - `@deepseek-ai/dsh-timeout-policy` 从注册表读取每个工具声明的 `timeoutMs`,并通过派生新的 `exec.signal` 来包装有此声明的调用。 -执行流水线为: +执行流水线如下: ```text ctx.tools.execute(exec) @@ -30,17 +30,17 @@ ctx.tools.execute(exec) -> tools/post-execute ``` -默认行为是保守的:未声明 `timeoutMs` 的工具不会从该插件收到 `TOOL_TIMEOUT` 截止时间。 +默认行为是保守的:未声明 `timeoutMs` 的工具不会从该插件收到 `TOOL_TIMEOUT` 截止信号。 ### `tools/execute` 环绕 seam -`@deepseek-ai/dsh-tools` 声明了一个 `tools/execute` waterfall,其基础 `next()` 是「分发并规范化」的 thunk:即同一个内部 `try`/`catch`,它将抛出的工具错误(或未知工具错误)转换为 `isError` 的 `ToolExecutionResult`。监听器接收 `(exec, next)`:调用 `next()` 委托给分发(返回其结果,可选地包装),或返回替代结果以短路分发。整条流水线仍处于 `execute` 的外层 try/catch 之内,因此抛出异常的监听器会变成 `isError` 结果,永远不会导致轮次失败。 +`@deepseek-ai/dsh-tools` 声明了一个 `tools/execute` waterfall,其基础 `next()` 是带规范化的分发 thunk——即同一个内部 `try`/`catch`,将抛出的工具错误(或未知工具错误)转换为 `isError` 的 `ToolExecutionResult`。监听器接收 `(exec, next)`:调用 `next()` 委托给分发(返回其结果,可选地包装),或返回替代结果以短路分发。整个流水线仍位于 `execute` 的外层 try/catch 内,因此抛出异常的监听器会变成 `isError` 结果,而非轮次失败。 -catch 是基础 `next()` 而非 waterfall 之外的东西,这一点是关键:当提供方看到超时信号并抛出自己的上游中止错误时,注册表分发首先将其转换为正常的错误结果,然后 `timeout-policy` 才能将最终结果替换为 `TOOL_TIMEOUT`。 +catch 是基础 `next()`(而非 waterfall 之外的东西)这一点至关重要:当提供方看到超时信号并抛出自己的上游中止错误时,注册表分发首先将其转换为普通错误结果,然后 `timeout-policy` 才能将最终结果替换为 `TOOL_TIMEOUT`。 ### `timeout-policy` 插件 -该插件是 `@deepseek-ai/dsh-timeout-policy`,位于 `packages/timeout/` 分组中,是一个零配置的函数/命名空间插件(`name` / `inject` / `apply`)。每个工具的预算声明在工具自身上,而非此插件上:`ToolDefinition` 携带可选的 `timeoutMs`,由拥有该工具的插件从自身配置中设置。例如 `dsh-tool-web` 将 `fetchTimeoutMs` / `searchTimeoutMs`(默认 30000)解析到 `web_fetch` / `web_search` 的定义上: +该插件是 `@deepseek-ai/dsh-timeout-policy`,一个零配置的函数/命名空间插件(`name` / `inject` / `apply`),位于 `packages/timeout/` 组。每个工具的预算声明在工具自身,而非本插件:`ToolDefinition` 携带一个可选的 `timeoutMs`,由拥有该工具的插件从自身配置中设置。例如 `dsh-tool-web` 将 `fetchTimeoutMs` / `searchTimeoutMs`(默认 30000)解析到 `web_fetch` / `web_search` 的定义上: ```yaml - id: timeout-policy @@ -52,11 +52,11 @@ catch 是基础 `next()` 而非 waterfall 之外的东西,这一点是关键 searchTimeoutMs: 30000 ``` -超时声明在工具定义上而非自由文本的名称映射中,消除了拼错名称导致策略不生效的问题。`defineTool` 会校验预算为正有限数。分发期间,执行器派生截止时间信号,之后恢复调用方信号,并将自身的超时转换为 `TOOL_TIMEOUT`;没有预算的工具原样通过。 +超时放在工具定义上而非自由文本名称映射中,消除了拼错名称导致策略不生效的问题。`defineTool` 校验预算为正有限数。分发期间,执行器派生截止信号,之后恢复调用方信号,并将自身的超时转换为 `TOOL_TIMEOUT`;没有预算的工具原样通过。 -信号替换采用**就地修改 `exec.signal`** 的方式,而非向 `next()` 传递新对象。Cordis 的 waterfall `next()` 忽略传入的参数,使用共享的 payload 数组重新调用下游监听器(`vendor/cordis/src/events.ts`),因此 Cordis 的文档惯用法——修改共享对象再委托——是唯一能到达分发的机制。插件在 `finally` 中将 `exec.signal` 恢复为调用方的原始信号,使 `tools/post-execute` 永远不会看到此插件的(可能已中止的)截止时间信号。 +信号替换采用**就地修改 `exec.signal`** 的方式,而非向 `next()` 传递新对象。Cordis 的 waterfall `next()` 忽略传入的任何参数,并以共享的 payload 数组重新调用下游监听器(`vendor/cordis/src/events.ts`),因此 Cordis 的惯用方式——修改共享对象再委托——是唯一能到达分发的机制。插件在 `finally` 中将 `exec.signal` 恢复为调用方的原始值,使 `tools/post-execute` 永远不会看到本插件的(可能已中止的)截止信号。 -`timeout-policy` 拥有 `TOOL_TIMEOUT` 代码的两种用途:传递给 `deadline()`/`timeoutOf()` 的内部截止时间代码(作用域化,使嵌套的外层截止时间读取为普通取消),以及结构化工具结果的错误代码。其替换结果为: +`timeout-policy` 拥有 `TOOL_TIMEOUT` 代码的两种用途:传递给 `deadline()`/`timeoutOf()` 的内部截止代码(有作用域,使嵌套的外层截止读为普通取消)和结构化工具结果错误代码。其替换结果为: ```ts ignore-check function toolTimeoutResult(timeoutMs: number): ToolExecutionResult { @@ -68,44 +68,44 @@ 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 }` 错误。 +无需新的会话事件来保证可重建性:`TOOL_TIMEOUT` 是该调用的最终面向模型的 `tool/result`,因此现有会话日志已经记录了下一次模型请求所见的内容和结构化 `{ name, code }` 错误。 ### 现有工具适配 -`web_fetch` 和 `web_search` 已迁移。`dsh-tool-web` 保留对其面向模型 schema 的所有权,这些 schema 不暴露超时旋钮:`web_fetch` 移除了 `timeout_ms` 参数以匹配参考 agent 的形态,`web_search` 保持仅查询。工具体不导入 `@deepseek-ai/dsh-timeout`;它们将 `exec.signal` 转发给 `ctx.web`。 +`web_fetch` 和 `web_search` 已迁移。`dsh-tool-web` 保留对其面向模型 schema 的所有权,这些 schema 不暴露超时旋钮:`web_fetch` 移除了 `timeout_ms` 参数以匹配参考 agent 的形状,`web_search` 保持仅查询。工具体不导入 `@deepseek-ai/dsh-timeout`;它们将 `exec.signal` 转发给 `ctx.web`。 -`dsh-web-fetch-local` 保留一个配置的提供方级 `timeoutMs`,作为直接调用 `ctx.web.fetch()` 的调用方和配置错误部署的大资源兜底;它不拥有面向模型的超时。当 `TOOL_TIMEOUT` 信号先到达 fetch 提供方时,提供方作用域的分类将其视为上游 `WEB_ABORTED`,外层 `tools/execute` 包装器将最终工具结果替换为 `TOOL_TIMEOUT`。已发布的 web 工具部署将提供方兜底配置为高于 `timeout-policy` 预算,使工具调用策略在模型调用中通常获胜。 +`dsh-web-fetch-local` 保留一个配置级别的 `timeoutMs` 作为大型资源兜底,服务于直接调用 `ctx.web.fetch()` 的调用方和配置错误的部署;它不拥有面向模型的超时。当 `TOOL_TIMEOUT` 信号先到达 fetch 提供方时,提供方作用域的分类将其视为上游 `WEB_ABORTED`,而外层 `tools/execute` 包装器将最终工具结果替换为 `TOOL_TIMEOUT`。一个已发布的 web 工具部署将提供方兜底配置为高于 `timeout-policy` 预算,使工具调用策略在模型调用中通常胜出。 `bash` 保持当前的后端超时路径。`dsh-tool-bash` 继续暴露 `timeoutMs` 和 `run_in_background`;`dsh-bash-local` 继续使用 `@deepseek-ai/dsh-timeout` 处理 `BASH_TIMEOUT`;钩子桥接继续调用 `runHook()` 并通过 `ctx.bash` 传递 `timeoutMs`。这保持了前台/后台/钩子行为的稳定。 -`read`、`write`、`edit`、`todo_write`、`bash_output` 和 `bash_kill` 不加入工具调用超时:它们是本地文件系统或短暂的注册表/会话操作,截止时间对它们要么只能尽力而为,要么没有必要。 +`read`、`write`、`edit`、`todo_write`、`bash_output` 和 `bash_kill` 不加入工具调用超时:它们是本地文件系统或短暂的注册表/会话操作,截止时间对它们而言要么只能尽力而为,要么没有必要。 -未来面向模型的 grep/glob 工具可以基于 `ctx.bash` 实现,无需导入 `@deepseek-ai/dsh-timeout`:它将 `exec.signal` 转发给 `ctx.bash`,并声明自己的 `timeoutMs`(来自其插件配置)供执行器应用。如果 bash-local 的后端超时对此类工具造成问题,bash seam 可以后续添加调用方拥有截止时间的模式;那不在本次范围内。 +未来面向模型的 grep/glob 工具可以基于 `ctx.bash` 实现而无需导入 `@deepseek-ai/dsh-timeout`:它将 `exec.signal` 转发给 `ctx.bash`,并声明自己的 `timeoutMs`(来自其插件配置)供执行器应用。如果 bash-local 的后端超时对这类工具造成问题,bash seam 可以后续添加调用方自有截止模式;这不在本次范围内。 ## 曾考虑的替代方案 -**将插件命名为 `tool-timeout`。** 字面的 RFC 名称匹配了 `gen-tool-catalog` 完整性守卫的 `packages/*/tool-*` glob,该守卫要求每个匹配项注册一个面向模型的工具。此插件不注册任何工具——它是 `tools/execute` 的包装器——因此 `tool-*` 名称要么导致 `verify-tool-catalog` 失败,要么强制一个误导性的启动条目。包名为 `@deepseek-ai/dsh-timeout-policy`,位于新的 `packages/timeout/` 分组;cordis.yml 的 `id` 仍可为 `timeout-policy`。 +**将插件命名为 `tool-timeout`。** 字面的 RFC 名称匹配了 `gen-tool-catalog` 完整性守卫的 `packages/*/tool-*` glob,该 glob 要求每个匹配项注册一个面向模型的工具。本插件不注册任何工具——它是一个 `tools/execute` 包装器——因此 `tool-*` 名称要么导致 `verify-tool-catalog` 失败,要么强制产生一个误导性的启动条目。包(package)为 `@deepseek-ai/dsh-timeout-policy`,位于新的 `packages/timeout/` 组;cordis.yml 的 `id` 仍可为 `timeout-policy`。 -**仅保留逐工具的超时处理。** 这是 `bash` 和 `web_fetch` 的原有形态,也与 Claude Code 和 Codex 对 shell 命令的做法一致。对 web 类工具而言它不够好,因为每个新的支持超时的工具都必须自行选择校验、上限语义、文档、快照和分类。插件集中了策略和分类,同时让每个工具的 schema 专注于业务输入。 +**仅保留逐工具的超时处理。** 这是 `bash` 和 `web_fetch` 的既有形态,也与 Claude Code 和 Codex 对 shell 命令的做法一致。它对 web 类工具不利,因为每个新的支持超时的工具都必须自行选择校验方式、上限语义、文档、快照和分类。插件集中了策略和分类,让每个工具的 schema 专注于业务输入。 -**立即将所有超时策略移出 bash-local。** 长期更干净:bash-local 将变为纯子进程执行器,所有调用方拥有自己的截止时间。作为第一步它不合适,因为钩子直接调用 `ctx.bash`,而 bash 模型工具有前台/后台语义,这与工具调用的生命周期不同。保留 `BASH_TIMEOUT` 维持了这些路径的稳定,同时工具调用超时在更简单的工具上验证自身。 +**立即将所有超时策略移出 bash-local。** 长期来看更干净——bash-local 将成为纯子进程执行器,所有调用方自行管理截止时间。但作为第一步不合适,因为钩子直接调用 `ctx.bash`,且 bash 模型工具的前台/后台语义与工具调用生命周期不同。保留 `BASH_TIMEOUT` 维持了这些路径的稳定,同时让工具调用超时在更简单的工具上先行验证。 -**为所有工具使用全局默认预算。** 方便,但会让工具作者意外:任何偶然运行超过全局预算的工具在插件加载后就会开始失败。逐工具声明的预算使采纳成为有意识的行为。 +**为所有工具使用全局默认预算。** 方便,但会让工具作者意外:任何偶然运行超过全局预算的工具在插件加载后就会开始失败。逐工具声明预算使采纳成为有意的行为。 -**暴露面向模型的 `timeout_ms` 覆盖参数。** Claude Code 的 `WebFetch`/`WebSearch` 和 Codex 的 web 工具将超时排除在模型调用形态之外。模型覆盖会使超时成为提示词语义的一部分,并迫使 `timeout-policy` 引入 schema/参数剥离规则。Web 超时仅作为部署策略。 +**暴露面向模型的 `timeout_ms` 覆盖参数。** Claude Code 的 `WebFetch`/`WebSearch` 和 Codex 的 web 工具将超时排除在模型调用形状之外。模型覆盖会使超时成为提示词语义的一部分,并迫使 `timeout-policy` 引入 schema/参数剥离规则。Web 超时仅作为部署策略。 -**让 `timeout-policy` 自行匹配工具参数。** 类似「当 `bash.run_in_background` 为 true 时禁用超时」的规则引擎会使策略插件了解工具特定的参数语义。通过不将 bash 迁移到工具调用超时来避免此问题。 +**让 `timeout-policy` 自行匹配工具参数。** 诸如「当 `bash.run_in_background` 为 true 时禁用超时」之类的规则引擎会让策略插件了解工具特定的参数语义。通过不将 bash 迁移到工具调用超时来规避此问题。 -**使用 `tools/pre-execute` 加 `tools/post-execute` 代替新的环绕 seam。** pre 监听器可以启动截止时间并修改 `exec.signal`;post 监听器可以分类并替换。这不可行,因为截止时间的生命周期将跨越两个独立的 waterfall:需要 call-id 映射、在每个 pre-deny/tool-throw/post-throw/dispose 路径上清理,以及与其他监听器的排序规则。`tools/pre-execute` 也是允许/拒绝门禁,而非执行包装器。`tools/execute` 给超时一个词法作用域:启动、委托、分类、释放。 +**使用 `tools/pre-execute` 加 `tools/post-execute` 代替新的环绕 seam。** pre 监听器可以启动截止时间并修改 `exec.signal`;post 监听器可以分类并替换。这样做的问题是截止时间的生命周期会跨越两个独立的 waterfall:需要 call-id 映射、在每条 pre-deny/tool-throw/post-throw/dispose 路径上清理,以及与其他监听器的排序规则。`tools/pre-execute` 也是允许/拒绝门禁,而非执行包装器。`tools/execute` 给超时一个词法作用域:启动、委托、分类、释放。 -**使用 `Promise.race` 为非协作式工具强制超时。** 否决,原因与超时库 RFC 相同:它在底层进程、fetch 或提供方操作可能仍在运行时就将控制权返回给调用方。插件只发送信号;终止仍是实现方的责任。 +**使用 `Promise.race` 对非协作工具强制超时。** 与超时库 RFC 相同的理由否决:它在底层进程、fetch 或提供方操作可能仍在运行时就将控制权返回给调用方。插件只发送信号;终止仍是实现方的责任。 ## 后果 -- `@deepseek-ai/dsh-tools` 在有意拆分 pre/post 工具钩子的拦截 seam 之后,获得了一个环绕分发的表面。其契约是窄的:包装注册表分发,而非替代 pre 门禁或 post 结果策略;基础 `next()` 是「分发并规范化」,因此包装器永远不会看到原始的工具抛出。 -- 多个 `tools/execute` 监听器通过普通的 Cordis waterfall 顺序组合:调用 `next()` 的监听器包装下游监听器加分发;不调用 `next()` 直接返回的监听器短路它们。组合超时与未来的重试/沙箱/指标包装器的部署通过注册顺序选择语义(「超时覆盖整个重试」vs「超时覆盖每次尝试」)。 -- 按声明加入是一个有意的配置错误风险:工具可以声明 `timeoutMs` 但不遵守 `exec.signal`,这样的工具在超时时不会停止。插件契约声明:声明预算意味着协作式;web 工具在已经转发信号的工具上证明了这一模式。 -- 过渡期间 `bash` 和已迁移的 web 工具有意使用不同的超时路径:`TOOL_TIMEOUT` 是面向模型的工具调用预算,而 `BASH_TIMEOUT` 仍然是 bash 和钩子使用的 bash 后端超时。 -- 与字面提案的偏差,按已实现 RFC 规则记录:插件包名为 `@deepseek-ai/dsh-timeout-policy`(而非 `tool-timeout`),信号替换是在 `next()` 之前就地修改 `exec.signal`(而非 `next({ ...exec, signal })`,Cordis 会忽略后者),逐工具预算声明在 `ToolDefinition` 上(`timeoutMs`,由拥有该工具的插件从其配置中设置)而非在此插件的配置中按工具名映射——因此执行器是零配置的,拼错工具名不可能发生。以上三点均在「## 决策」中描述。 +- `@deepseek-ai/dsh-tools` 在有意拆分 pre/post 工具钩子的拦截 seam 之后,获得了一个环绕分发的表面。其契约是狭窄的——包装注册表分发,而非替代 pre 门禁或 post 结果策略——且基础 `next()` 是带规范化的分发,因此包装器永远不会看到原始的工具抛出。 +- 多个 `tools/execute` 监听器按普通 Cordis waterfall 顺序组合:调用 `next()` 的监听器包装下游监听器加分发;不调用 `next()` 直接返回的监听器短路它们。一个同时组合超时与未来重试/沙箱/指标包装器的部署通过注册顺序选择语义(「超时覆盖整个重试」vs「超时覆盖每次尝试」)。 +- 按声明加入是一个有意的误配置风险:工具可以声明 `timeoutMs` 但不遵循 `exec.signal`,这样的工具在超时时不会停止。插件契约声明:声明预算意味着协作;web 工具在已转发信号的工具上验证了这一模式。 +- 过渡期间 `bash` 和已迁移的 web 工具有意使用不同的超时路径:`TOOL_TIMEOUT` 是面向模型的工具调用预算,而 `BASH_TIMEOUT` 仍是 bash 和钩子使用的 bash 后端超时。 +- 与字面提案的偏差,按 implemented-RFC 规则记录:插件包为 `@deepseek-ai/dsh-timeout-policy`(而非 `tool-timeout`);信号替换是在 `next()` 之前就地修改 `exec.signal`(而非 `next({ ...exec, signal })`,Cordis 会忽略后者);逐工具预算声明在 `ToolDefinition` 上(`timeoutMs`,由拥有该工具的插件从其配置中设置),而非在本插件配置中按工具名映射——因此执行器是零配置的,拼错工具名不可能发生。以上三点均在上文「决策」一节中描述。 diff --git a/docs/rfc/implemented/architecture/2026-07-08-agent-scope-contexts.i18n.yaml b/docs/rfc/implemented/architecture/2026-07-08-agent-scope-contexts.i18n.yaml index 7f9d0625f3..3253751754 100644 --- a/docs/rfc/implemented/architecture/2026-07-08-agent-scope-contexts.i18n.yaml +++ b/docs/rfc/implemented/architecture/2026-07-08-agent-scope-contexts.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-08-agent-scope-contexts.md: f238d58d90413d36e81b34c1a2c94e1291e889de -2026-07-08-agent-scope-contexts.zh.md: dfade19709ccbf206054f414882ecff114ac1e44 +2026-07-08-agent-scope-contexts.zh.md: 0f4de12e782fc72d3761b6d46cd953ab4650654d diff --git a/docs/rfc/implemented/architecture/2026-07-08-agent-scope-contexts.zh.md b/docs/rfc/implemented/architecture/2026-07-08-agent-scope-contexts.zh.md index dfade19709..0f4de12e78 100644 --- a/docs/rfc/implemented/architecture/2026-07-08-agent-scope-contexts.zh.md +++ b/docs/rfc/implemented/architecture/2026-07-08-agent-scope-contexts.zh.md @@ -6,25 +6,25 @@ Status: implemented ## 问题 -一个应用需要在多个 agent(智能体)之间共享基础设施,同时让每个 agent 拥有自己的工具、prompt 贡献、策略和监听器。共享的适配器、持久化和用户界面属于部署层面;而一个人设、工具变体或监听器往往只属于某一个 agent。 +一个应用需要在多个 agent(智能体)之间共享基础设施,同时让每个 agent 拥有自己的工具、提示词贡献、策略和监听器。共享的适配器、持久化和用户界面属于部署层面;而 persona、工具变体或监听器往往只属于某一个 agent。 -为每个 agent 建立独立的服务图会重复共享基础设施。一个全局注册图则有相反的问题:某个 agent 的专属贡献可能泄漏到无关的 agent 中。贡献者需要一种普通的注册机制,既能决定谁能看到一项贡献,又能决定何时清理它。 +为每个 agent 建立独立的服务图会重复共享基础设施。使用一个全局注册图则有相反的问题:某个 agent 特有的贡献可能泄漏到无关的 agent 中。贡献者需要一种普通的注册机制,既能决定谁可以看到某项贡献,又能决定何时清理它。 -该机制还需要一个发布边界。agent 在其本地世界完整之前不得变为可见,而拆除过程必须保留该世界直到最终工作停止。 +该机制还需要一个发布边界。agent 在其本地世界构建完成之前不得变为可见,拆除时也必须保留该本地世界直到最终工作停止。 ## 决策 -每个存活的 agent 拥有一个扁平的注册层,通过 `agent.ctx` 暴露。代码通过拥有该贡献的上下文进行注册;感知作用域的服务将部署全局注册与恰好一个匹配的 agent 层组合;操作从其真实 agent 选择该层;该层在 agent 完整的已发布生命周期内存在。 +每个存活的 agent 拥有一个扁平的注册层,通过 `agent.ctx` 暴露。代码通过拥有某项贡献的 context 进行注册;具备作用域感知的服务将部署全局注册与恰好一个匹配的 agent 层合并;操作从其真实 agent 选择该层;该层在 agent 的完整发布生命周期内存在。 -Cordis 是 SDK 底层的插件框架。Cordis **上下文(context)** 是插件用来访问服务和注册效果的对象,效果的清理跟随该上下文。[Cordis 入门](../../../cordis-primer.md)对框架有更详细的说明。 +Cordis 是 SDK 底层的插件框架。Cordis **context** 是插件用来访问服务和注册效果的对象,效果的清理跟随该 context。[Cordis 入门](../../../cordis-primer.md)对该框架有更详细的说明。 -对大多数贡献者而言,完整的契约是四条规则: +对大多数贡献者而言,完整契约是四条规则: | 问题 | 规则 | |---|---| -| 在哪里为某个 agent 注册行为? | 通过 `agent.ctx` 调用普通的注册 API | -| 某个 agent 的操作能看到什么? | 部署全局加上该 agent 的层,使用所属服务的合并规则 | -| 哪些作用域监听器会运行? | 无作用域监听器加上为该操作的 agent 注册的监听器 | +| 在哪里为某个 agent 注册行为? | 通过 `agent.ctx` 调用普通注册 API | +| 某个 agent 的操作能看到什么? | 部署全局加上该 agent 的层,按所属服务的合并规则 | +| 哪些作用域监听器会运行? | 无作用域监听器加上为该操作所属 agent 注册的监听器 | | 该层存在多久? | setup 在发布前完成;dispose 保留该层直到工作达到静止 | 作用域是扁平的。解析永远不会遍历父级或兄弟作用域,生命周期所有权也不意味着注册继承。 @@ -43,20 +43,20 @@ flowchart LR agentBLayer --> agentBView ``` -缺失的交叉边就是隔离规则:Agent A 的本地注册不会进入 Agent B 的视图,父级的注册也不会仅因为父级拥有子级的生命周期就进入子级。 +缺失的交叉边即隔离规则:Agent A 的本地注册不会进入 Agent B 的视图,父级的注册也不会仅因父级拥有子级的生命周期就进入子级。 -配套的[运行时设计 RFC](2026-07-12-agent-scope-runtime-design.md) 解释了实现与正确性推理。[subagent 组合控制 RFC](../feature/2026-07-12-subagent-persona-tool-filter-and-depth.md) 拥有独立的 `persona`、`toolFilter` 和 `maxDepth` 功能。 +配套的[运行时设计 RFC](2026-07-12-agent-scope-runtime-design.md) 阐述了实现与正确性推理。[subagent 组合控制 RFC](../feature/2026-07-12-subagent-persona-tool-filter-and-depth.md) 负责独立的 `persona`、`toolFilter` 和 `maxDepth` 功能。 ### 注册来源决定可见性与清理 -通过普通插件上下文进行的注册是部署全局的,随该插件 dispose。同一方法通过 `agent.ctx` 调用则贡献给一个 agent,随该 agent 的作用域 dispose。 +通过普通插件 context 进行的注册是部署全局的,随该插件一起 dispose(资源释放)。同一方法通过 `agent.ctx` 调用则贡献给一个 agent,随该 agent 的作用域一起 dispose。 | 注册来源 | 默认可见性 | 随谁 dispose | |---|---|---| -| 普通插件上下文 | 每个符合条件的 agent 视图 | 注册插件 | +| 普通插件 context | 每个符合条件的 agent 视图 | 注册插件 | | `agent.ctx` | 仅该 agent 的视图 | agent 作用域 | -工具、prompt 段落与变量、工具限制、守卫和作用域事件监听器都采用此契约。同名的本地值通常对该 agent 遮蔽同名的全局值;每个所属服务自行记录例外与合并行为。 +工具、提示词段落与变量、工具限制、守卫以及作用域事件监听器都遵循此契约。命名的本地值通常对该 agent 遮蔽同名全局值;各所属服务文档会说明例外与合并行为。 普通贡献者的模式是在 agent setup 期间注册完整的本地世界: @@ -89,35 +89,35 @@ await handle.dispose() ctx.tools.get('review_summary', handle.agent) // undefined: scope is gone ``` -setup 接收一个完整的受信 Cordis 上下文,因此可以组合普通的插件和服务。其契约仅限组合:通过强制转换或内部注册表调用来驱动或发布正在构建的 agent 是不受支持的。 +setup 接收一个完整的受信 Cordis context,因此可以组合普通插件和服务。其契约仅限组合:不支持通过 cast 或内部注册表调用来驱动或发布正在构建中的 agent。 ### 操作选择视图 注册来源与操作主体是两个独立的事实。通过 `agent.ctx` 调用服务决定的是新注册归属何处,并不将后续读取绑定到该 agent。 -工具查找与执行接收其服务的 agent。prompt 组装接收正在构建请求的 agent 的组装上下文。事件分发接收其领域主体。这使共享服务实例可在多个 agent 间复用,同时让每个操作的视图保持显式。 +工具查找与执行接收其所服务的 agent。提示词组装接收正在构建请求的 agent 的组装上下文。事件分发接收其领域主体。这使共享服务实例可在多个 agent 间复用,同时让每个操作的视图保持显式。 只有采纳了作用域契约的服务才会解析 agent 层。`agent.ctx` 不会自动改变任意 Cordis 服务调用的行为。 ### 作用域事件将路由与事件数据分离 -关于 Agent A 的事件通常到达无作用域监听器和 A 作用域监听器,而不到达 B 作用域监听器。没有 agent 主体的事件只到达无作用域监听器。 +关于 Agent A 的事件通常到达无作用域监听器和 A 作用域监听器,而不到达 B 作用域监听器。没有 agent 主体的事件仅到达无作用域监听器。 在 Cordis 层面,`Scoped` 是一个不透明的路由接收器。它携带用于选择监听器的过滤器,但本身不是领域对象。因此事件签名将真实的 `Agent`、工具执行、审批请求或其他主体作为显式参数保留,供监听器检查。 -以 `{ global: true }` 注册的监听器有意绕过上下文受众过滤,但其清理仍跟随注册上下文。注册表成员变更通知保持不过滤,因为它们描述的是共享注册表状态而非某个 agent 的操作。生成的[事件目录](../../../cordis-catalog/events.md)是详尽的事件参考。 +以 `{ global: true }` 注册的监听器有意绕过上下文受众过滤,但其清理仍跟随注册 context。注册表成员变更通知保持不过滤,因为它们描述的是共享注册表状态而非某个 agent 的操作。生成的[事件目录](../../../cordis-catalog/events.md)是详尽的事件参考。 ### 创建最后发布,dispose 最后撤销 `ctx.agents.create()` 和 `resume()` 构建未发布的会话、作用域、agent 和驱动器。它们等待 `setup`,准入最终的会话和 agent 条目,按序公告,启动循环,然后才返回 handle。 -可选的创建信号仅在 create 或 resume 挂起期间取消工作。promise resolve 后,返回的 `AgentHandle` 拥有显式的 dispose 权。 +可选的创建信号仅在 create 或 resume 挂起期间取消工作。promise resolve 后,返回的 `AgentHandle` 拥有显式 dispose 权。 -如果加载、setup、准入或发布失败,私有事务回滚其准备的一切。使用同一调用方提供的存活 ID 的并发操作可能都到达 setup,但最终注册表条目只准入一个;所有失败者拒绝并清理其私有资源。在等待 dispose 完成后的顺序复用仍然有效。 +如果加载、setup、准入或发布失败,私有事务回滚其准备的一切。使用同一个调用方提供的存活 ID 的并发操作可能都到达 setup,但最终注册表条目只准入一个;每个失败者拒绝并清理其私有资源。在等待 dispose 完成后的顺序复用仍然有效。 -`AgentHandle.dispose()` 反转边界。它停用创建或驱动,等待同步发布解除,停止并排空驱动器和最终会话刷新,分离 agent 和会话,最后 dispose 作用域。重复或竞争的 dispose 请求合并为一个完成 promise。 +`AgentHandle.dispose()` 反转边界。它停用创建或驱动,等待同步发布解除,停止并排空驱动器和最终会话刷写,分离 agent 和会话,最后 dispose 作用域。重复或竞争的 dispose 请求合并为一个完成 promise。 -调用方的 Cordis 上下文和具体的 AgentLoop 工厂是结构性共同所有者。卸载任一方都会 dispose 事务或存活 agent。 +调用方的 Cordis context 和具体的 AgentLoop 工厂是结构性共同所有者。卸载任一方都会 dispose 事务或存活 agent。 ```mermaid flowchart TB @@ -139,25 +139,25 @@ flowchart TB ## 安全与权限是非目标 -agent 作用域组合的是受信的同进程注册。它不沙箱化插件,不定义父到子的权限格,不在创建时冻结授权,也不保证子级不能做超出父级的事。 +agent 作用域组合的是受信的同进程注册。它不沙箱化插件、不定义父到子的权限格、不在创建时冻结授权、也不保证子级不能做超出父级的事。 -父级可以拥有一个可见工具比自身更宽的子级,因为生命周期所有权不捐赠也不封顶注册。持有 Cordis 上下文的插件同样运行在同一进程中,可以直接调用可用服务。 +父级可以拥有一个可见工具比自身更广的子级,因为生命周期所有权不赠予也不限制注册。持有 Cordis context 的插件同样运行在同一进程中,可以直接调用可用服务。 -需要非升级保证的部署需要独立的权限表示、传播规则和执行检查。父集合授权、创建时授权快照、显式的未来授权 API、以及通用的能力/输出/终止标签均不在本决策范围内。 +需要非升权保证的部署需要独立的权限表示、传播规则和执行检查。父集合授权、创建时授权快照、显式未来授权 API,以及通用的能力/输出/终止标签均不在本决策范围内。 ## 曾考虑的替代方案 -被否决的设计要么将可见性与清理分离,要么只覆盖一个注册族,要么重复共享基础设施,要么将生命周期所有权与继承混为一谈。 +被否决的设计要么将可见性与清理分离,要么只覆盖一类注册,要么重复共享基础设施,要么将生命周期所有权与继承混为一谈。 -### 向每次注册传递 agent 选项 +### 向每个注册传递 agent 选项 -类似 `tools.register(definition, { agent })` 的 API 在每个注册表中重复作用域管道,并允许可见性所有权与清理所有权漂移。通过 `agent.ctx` 注册使两个事实跟随同一个 Cordis 效果所有者。 +类似 `tools.register(definition, { agent })` 的 API 在每个注册表中重复作用域管道,且允许可见性所有权与清理所有权漂移。通过 `agent.ctx` 注册使两个事实跟随同一个 Cordis effect owner。 ### 过滤事件但保持注册表全局 -监听器过滤能阻止错误的钩子运行,但无法限定工具 schema、可执行查找、prompt 段落、变量或其他已注册数据的作用域。agent 本地组合仍需临时的全局变更。 +监听器过滤可以阻止错误的钩子运行,但无法限定工具 schema、可执行查找、提示词段落、变量或其他已注册数据的作用域。agent 本地组合仍需临时的全局变更。 -### 为每个 agent 创建一个服务图 +### 为每个 agent 创建独立的服务图 所需的视图是共享部署服务加上一个本地注册层。每 agent 一个图会重复适配器,并使共享持久化、提供方注册表和应用启动复杂化。 @@ -167,6 +167,6 @@ agent 作用域组合的是受信的同进程注册。它不沙箱化插件, ## 后果 -贡献者使用一种熟悉的模式:通过插件上下文注册共享行为,通过 `agent.ctx` 注册本地行为,在操作上选择真实 agent,dispose 返回的 handle。从观察者角度看 setup 是原子的,teardown 保留本地行为直到工作停止。 +贡献者使用一种熟悉的模式:通过插件 context 注册共享行为,通过 `agent.ctx` 注册本地行为,在操作中选择真实 agent,dispose 返回的 handle。从观察者角度看 setup 是原子的,拆除则保留本地行为直到工作停止。 -代价是显式的主体选择、异步的编程式创建,以及服务需要逐个采纳作用域。扁平注册作用域有意不等于权限,subagent 组合控制作为独立功能存在,而非隐藏在作用域语义中。 +代价是显式的主体选择、异步的编程式创建,以及服务需要逐个采纳作用域。扁平注册作用域有意不等同于权限,subagent 组合控制作为独立功能存在,而非隐藏的作用域语义。 diff --git a/docs/rfc/implemented/feature/2026-06-14-acp-agent-client-protocol.i18n.yaml b/docs/rfc/implemented/feature/2026-06-14-acp-agent-client-protocol.i18n.yaml index c48d3db510..b0b2ec8727 100644 --- a/docs/rfc/implemented/feature/2026-06-14-acp-agent-client-protocol.i18n.yaml +++ b/docs/rfc/implemented/feature/2026-06-14-acp-agent-client-protocol.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-14-acp-agent-client-protocol.md: 0bb2a2f2e307b8f23a3a9ca98edb2a2d3b5df0a8 -2026-06-14-acp-agent-client-protocol.zh.md: 19744cb9f4d4b675586d4327a1da423fdea21b77 +2026-06-14-acp-agent-client-protocol.zh.md: 7bb0e066572150c9c8fc0b94de1d5bf2d69527ee diff --git a/docs/rfc/implemented/feature/2026-06-14-acp-agent-client-protocol.zh.md b/docs/rfc/implemented/feature/2026-06-14-acp-agent-client-protocol.zh.md index 19744cb9f4..7bb0e06657 100644 --- a/docs/rfc/implemented/feature/2026-06-14-acp-agent-client-protocol.zh.md +++ b/docs/rfc/implemented/feature/2026-06-14-acp-agent-client-protocol.zh.md @@ -1,4 +1,4 @@ -# RFC:ACP(Agent Client Protocol)支持——从外部编辑器驱动编码 agent +# RFC:Agent Client Protocol(ACP)支持——从外部编辑器驱动编码 agent [English](2026-06-14-acp-agent-client-protocol.md) | 中文 @@ -6,54 +6,54 @@ Status: implemented ## 问题 -harness 最初只通过 readline 循环暴露 agent(智能体)。该接口能传输文本,但编辑器无法以结构化方式创建或恢复会话、关联 prompt 完成状态、流式输出推理(reasoning)与工具活动、渲染工具专属 UI、请求权限,或在不干扰其他对话的情况下取消某个对话。ACP 将这些交互定义为基于 stdio 的 JSON-RPC,Zed 是用于做出具体兼容性决策的目标客户端。 +harness 最初仅通过 readline 循环暴露 agent。该接口能传输文本,但编辑器无法以结构化方式创建或恢复会话、关联 prompt 完成、流式输出推理(reasoning)与工具活动、渲染工具专属 UI、请求权限,或在不干扰其他对话的前提下取消某个对话。ACP(Agent Client Protocol)将这些交互定义为基于 stdio 的 JSON-RPC,Zed 是用于做出具体兼容性决策的目标客户端。 -桥接层必须保持 harness 既有的职责边界。它不能依赖具体的 agent loop(智能体循环)、绕过工具注册表、在编辑器中执行 shell 命令,或发明第二个会话真源。stdout 同时也是协议传输通道,因此任何意外的日志输出都会破坏连接。 +桥接层必须保持 harness 既有的所有权边界。它不能依赖具体的 agent loop(智能体循环),不能绕过工具注册表,不能在编辑器中执行 shell 命令,也不能发明第二个会话真源。stdout 同时也是协议传输通道,因此任何意外的日志输出都会破坏连接。 ## 决策 -`@deepseek-ai/dsh-acp` 是位于 `packages/ui/acp` 的 UI/客户端驱动插件。它使用 `@agentclientprotocol/sdk` 的 `AgentSideConnection`(基于 stdin/stdout),仅编程接口级服务:agent 创建/恢复工厂、会话持久化、工具注册表、用户交互,以及可选的审批/bash 能力。它不改变 agent loop,也不是能力 seam 的实现。 +`@deepseek-ai/dsh-acp` 是位于 `packages/ui/acp` 的 UI/客户端驱动插件。它使用 `@agentclientprotocol/sdk` 的 `AgentSideConnection`(基于 stdin/stdout),仅编排接口服务:agent 创建/恢复工厂、会话持久化、工具注册表、用户交互,以及可选的审批/bash 能力。它不修改 agent loop,也不是能力 seam 的实现。 桥接层实现以下稳定的会话路径: -- `initialize` 协商协议版本,声明支持 text 与 `resource_link` prompt,并声明 `loadSession`。 -- `session/new` 校验绝对路径 `cwd`,将其存入 `SessionHeader`,通过 `ctx.agents` 创建 agent,并返回组合支持的配置选项。 -- `session/load` 在构造 agent 之前,先用持久化元数据校验请求的 cwd;在异步恢复期间预留 id;将 user/assistant/tool 事件作为 ACP update 回放;并报告恢复后的 config-option fold。 -- `session/prompt` 接受 text 和 resource link,拒绝不支持或空的内容,每个会话只允许一个 in-flight prompt,并在该 prompt 所属的 `turn/end` 时结算。错误 turn 拒绝 RPC;其他关闭 turn 的原因通过一个全覆盖的 ACP stop-reason codec 映射。 +- `initialize` 协商协议版本,声明支持 text 与 `resource_link` 类型的 prompt,并声明 `loadSession` 能力。 +- `session/new` 校验绝对路径 `cwd`,将其存入 `SessionHeader`,通过 `ctx.agents` 创建 agent,并返回由组合层支持的配置选项。 +- `session/load` 在构造 agent 之前校验请求的 cwd 与持久化元数据是否一致,在异步恢复期间保留 id,将用户/助手/工具事件作为 ACP update 回放,并报告恢复后的 config-option 折叠结果。 +- `session/prompt` 接受文本和 resource link,拒绝不支持的或空的内容,每个会话同时只允许一个 in-flight prompt,并在该 prompt 所属的 `turn/end` 时结算。错误轮次拒绝 RPC;其他关闭轮次的原因通过一个全覆盖的 ACP stop-reason 编解码器映射。 - `session/cancel` 调用队列感知的 agent 取消路径,仅结算被寻址会话的 prompt。 -工具调用的呈现仍由工具自身负责。工具的 `presentCall` 和 `presentResult` 返回 `generic`、`terminal` 或 `diff` 渲染意图变体;桥接层对该联合类型做 switch 并映射到 ACP。没有 presenter 的工具获得通用回退。Bash 终端卡片使用 Zed 的能力门控 `_meta.terminal_info`、`_meta.terminal_output` 和 `_meta.terminal_exit` 约定;harness 仍通过 `ctx.bash` 执行命令,保留沙箱、环境变量清理、任务归属和 cwd。不支持该扩展的客户端收到普通文本内容。文件系统工具提供 diff 卡片和文件位置,桥接层中没有硬编码的工具名分支。 +工具调用的展示仍由工具自身负责。工具的 `presentCall` 和 `presentResult` 返回 `generic`、`terminal` 或 `diff` 渲染意图变体;桥接层对该联合类型做 switch 并映射到 ACP。没有 presenter 的工具获得通用回退。Bash 终端卡片使用 Zed 的能力门控约定 `_meta.terminal_info`、`_meta.terminal_output` 和 `_meta.terminal_exit`;harness 仍通过 `ctx.bash` 执行命令,保留沙箱、环境清洗、所有权和 cwd。不支持该扩展的客户端收到普通文本内容。文件系统工具提供 diff 卡片和文件位置,桥接层中无需硬编码工具名分支。 -权限处理是[用户审批 seam](2026-07-06-approval-seam.md) 上的一个 answerer,而非 ACP 中「每次工具调用都询问」的策略。一个带有 call id 的、针对桥接层所属 agent 的 `approval/request`,会变成该 agent 编辑器会话上的 `session/request_permission`,提供一次性允许/拒绝选项。非本桥接层的请求或无 call id 的请求走委托路径;answerer 缺失或失败时保持 fail-closed。决定是否询问的插件(如预执行策略或 bash 升级)拥有「是否询问」的决策权。 +权限处理是 [user-approval seam](2026-07-06-approval-seam.md) 上的一个 answerer,而非 ACP 中的「每次工具调用都询问」策略。对桥接层所属 agent 且带有 call id 的 `approval/request`,会变为该 agent 编辑器会话上的 `session/request_permission`,提供一次性允许/拒绝选项。外部请求或无 call id 的请求委托给下游;缺失或失败的 answerer 保持 fail-closed。发起询问的插件(如预执行策略或 bash 升级)拥有「是否询问」的决策权。 -当 `ctx.permission` 被组合时,桥接层从部署的预设表中暴露一个 `permission` select。出厂的 `workspace-write` 和 `danger-full-access` 预设各自捆绑一个沙箱模式与一个审批策略;无法匹配的有效旋钮组合产生只能切走的 `custom` 状态。`session/set_config_option` 通过 `PermissionService.set()` 校验,并写入两个所属旋钮事件。在 open turn 期间的切换立即追加;idle 状态下的切换在响应中叠加,并在下一次 `agent/prompt-submit` 时锚定,位于请求组装之前。在此之前它仅存于内存,因此崩溃后恢复的是持久化的 fold。ACP session mode 不被建模,因为 config option 是面向未来的协议表面;`AcpConfig.model` 仍为连接级。 +当 `ctx.permission` 被组合时,桥接层从部署的预设表中暴露一个 `permission` select。已发布的 `workspace-write` 和 `danger-full-access` 预设各自捆绑一个沙箱模式与一条审批策略;无法匹配的有效旋钮组合产生只能切走的 `custom` 状态。`session/set_config_option` 通过 `PermissionService.set()` 校验并写入两个所属旋钮事件。在开放轮次中的切换立即追加;空闲时的切换叠加在响应中,并在下一次 `agent/prompt-submit` 时锚定到开放轮次之前的请求组装阶段。在此之前它仅存于内存,因此崩溃后恢复的是持久化的折叠结果。ACP session mode 不被建模,因为 config option 是面向未来的协议表面;`AcpConfig.model` 保持连接级别。 -桥接层还提供基于 ACP 的 `UserInteractionProvider`:`ask_user_question` 请求变为所属会话上的表单引导。select、multi-select、选项描述和自定义回答覆盖语义均被保留。 +桥接层还提供基于 ACP 的 `UserInteractionProvider`:`ask_user_question` 请求变为所属会话上的表单引导。select、multi-select、选项描述与自定义回答覆盖语义均被保留。 -生命周期归属是显式的。桥接层为每个活跃会话持有一个 `AgentHandle`。断连和 Cordis dispose(资源释放)会取消待处理的 prompt、并行 dispose 每个 handle、等待循环静默和持久化刷盘,然后移除记录。流式通知失败被隔离,已消失的客户端无法破坏 agent turn。ACP 应用组合不加载 stdout logger;一个测试守卫 stdout 仅包含帧化的 JSON-RPC。 +生命周期所有权是显式的。桥接层为每个活跃会话持有一个 `AgentHandle`。断连和 Cordis dispose(资源释放)会取消待处理的 prompt,并行 dispose 所有 handle,等待循环静默与持久化刷写,然后移除记录。流通知失败被隔离,因此消失的客户端不会破坏 agent 轮次。ACP 应用组合不加载 stdout logger;一个测试守卫 stdout 仅包含帧化的 JSON-RPC。 -精确的已支持与已推迟的协议行列表见 [`packages/ui/acp/acp-feature-support.md`](../../../../packages/ui/acp/acp-feature-support.md);package README 是运维契约。 +精确的已支持与已推迟的协议行列表见 [`packages/ui/acp/acp-feature-support.md`](../../../../packages/ui/acp/acp-feature-support.md);package README 是操作契约。 ## 曾考虑的替代方案 -**在 `tools/execute` 前置一个监听器,对每个 ACP 所属调用都询问权限**:否决。这会把权限策略硬编码进 UI 桥接层,即使没有策略要求也会询问,且无法服务执行开始后才产生的审批请求。共享的用户审批 seam 将机制、询问策略和 UI answerer 分离。 +**在 `tools/execute` 监听器前置一层,对每个 ACP 所属调用都询问权限**:否决。这会将权限策略硬编码到 UI 桥接层,即使没有策略要求也会询问,且无法服务于执行开始后才产生的审批请求。共享的 user-approval seam 将机制、询问策略和 UI answerer 分离。 -**注入具体的 `agentLoop`**:否决。agent 的创建、恢复、idle 观察和 dispose 是 `dsh-agent` 上的接口级归属操作;UI 插件不需要依赖规则的例外。 +**注入具体的 `agentLoop`**:否决。agent 的创建、恢复、空闲观察与释放是 `dsh-agent` 上的接口级所有权操作;UI 插件不需要依赖规则例外。 -**通过 ACP `terminal/*` 执行 bash**:否决。那会把执行移到 harness 之外,绕过其沙箱、凭证清理、任务归属、cwd 解析和会话日志。终端元数据仅用于呈现。 +**通过 ACP `terminal/*` 执行 bash**:否决。这会将执行移到 harness 之外,绕过其沙箱、凭证清洗、任务所有权、cwd 解析与会话日志。终端元数据仅用于展示。 -**将权限预设表示为 ACP session mode**:否决。部署定义的预设已经是一个 config-option select,而 session mode 是 ACP v2 计划移除的旧接口。 +**将权限预设表示为 ACP session mode**:否决。部署定义的预设已经是一个 config-option select,而 session mode 是 ACP v2 计划移除的遗留接口。 -**防御性劫持 stdout**:否决。进程级 monkey-patching 超出 Cordis 副作用归属范围,且与协议传输竞争。应用组合拥有 stdout 纯净性。 +**防御性劫持 stdout**:否决。进程级 monkey-patching 超出 Cordis 副作用所有权范围,且与协议传输存在竞争。应用组合拥有 stdout 纯净性。 ## 后果 -编辑器可以通过一条 ACP 连接创建、加载、prompt、取消、渲染、询问和重新配置多个 harness 会话,无需依赖特定的循环实现。会话事件日志仍是回放、prompt 结算、cwd 和每会话配置的持久真源。工具呈现与人工回答通道仍是可扩展的插件契约,而非 ACP 特有行为。 +编辑器可以通过一条 ACP 连接创建、加载、提交 prompt、取消、渲染、询问和重新配置多个 harness 会话,无需依赖特定的循环实现。会话事件日志仍是回放、prompt 结算、cwd 与每会话配置的持久真源。工具展示与人工回答通道仍是可扩展的插件契约,而非 ACP 专属行为。 -桥接层有意不实现会话列表/删除/恢复/关闭能力、MCP 透传、附加目录、图片/音频/嵌入资源 prompt、运行时模型选择、plan、斜杠命令、用量更新、编辑器文件系统委托,以及 ACP 终端执行子协议。功能清单将这些记录为不支持,而非静默接受。 +桥接层有意不实现会话列表/删除/恢复/关闭能力、MCP 透传、附加目录、图片/音频/嵌入资源 prompt、运行时模型选择、plan、斜杠命令、用量更新、编辑器文件系统委托或 ACP 终端执行子协议。功能清单将这些记录为不支持,而非静默接受。 -idle 状态下的 config 选择在实时响应中是真实的,但在下一次 `agent/prompt-submit` 将其锚定到 open turn 之前不具有持久性。在该边界之前崩溃会丢失待定选择;这是保持会话事件 turn 封闭且回放安全的代价。 +空闲时的配置选择在实时响应中是真实的,但在下一次 `agent/prompt-submit` 将其锚定到开放轮次之前不具持久性。在该边界之前崩溃会丢失待定选择;这是保持会话事件封闭于轮次内且回放安全的代价。 ## 验证 -ACP 测试套件覆盖内存协议编解码、创建/加载回放、精确的 prompt 结算、取消竞态、不支持的内容、工具呈现、终端能力回退、权限结果映射、config-option 校验与持久化、多会话隔离、断连/dispose 静默,以及 HMR(热模块替换)清理。快照和 built-bin 测试检验应用组合,真实 API 的 e2e 在无 key 时自动跳过。 +ACP 测试套件覆盖内存协议编解码器、创建/加载回放、精确的 prompt 结算、取消竞争、不支持的内容、工具展示、终端能力回退、权限结果映射、config-option 校验与持久化、多会话隔离、断连/释放静默,以及 HMR(热模块替换)清理。快照测试与 built-bin 测试验证应用组合,真实 API 的 e2e 测试在无 key 时自动跳过。 diff --git a/docs/rfc/implemented/feature/2026-06-14-acp-multi-session.i18n.yaml b/docs/rfc/implemented/feature/2026-06-14-acp-multi-session.i18n.yaml index 312106acaf..825e4c2ed8 100644 --- a/docs/rfc/implemented/feature/2026-06-14-acp-multi-session.i18n.yaml +++ b/docs/rfc/implemented/feature/2026-06-14-acp-multi-session.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-14-acp-multi-session.md: b96557d2d94711adb2183aa4f5dd8debf39c1de8 -2026-06-14-acp-multi-session.zh.md: 263e292e7161b789b8e06772deb0d2bc896f8de6 +2026-06-14-acp-multi-session.zh.md: 6a9f5e8162d46ed8719164247e22b8b9c5d26c61 diff --git a/docs/rfc/implemented/feature/2026-06-14-acp-multi-session.zh.md b/docs/rfc/implemented/feature/2026-06-14-acp-multi-session.zh.md index 263e292e71..6a9f5e8162 100644 --- a/docs/rfc/implemented/feature/2026-06-14-acp-multi-session.zh.md +++ b/docs/rfc/implemented/feature/2026-06-14-acp-multi-session.zh.md @@ -1,39 +1,39 @@ -# RFC:在单连接上多路复用并发 ACP 会话 - -Status: implemented +# RFC:在单个连接上多路复用并发 ACP 会话 [English](2026-06-14-acp-multi-session.md) | 中文 +Status: implemented + ## 问题 -一个 ACP(Agent Client Protocol)编辑器可以在同一个 agent(智能体)子进程上维持多个活跃对话。如果桥接层只允许单活跃会话,就不得不额外启动进程,也无法匹配 Zed 的客户端模型——该模型跟踪多个 session id 和并发加载。多路复用引入了隔离风险:事件、prompt 完成、取消、权限提示、配置选择以及可预测的后台任务 id 都绝不能跨越会话边界。 +一个 ACP(Agent Client Protocol)编辑器可以在同一个 agent(智能体)子进程上保持多个对话。如果桥接层只支持单活跃会话,就不得不启动额外进程,也无法匹配 Zed 的客户端模型——该模型跟踪多个 session id 和并发加载。多路复用引入了隔离风险:事件、prompt 完成、取消、权限提示、配置选择以及可预测的后台 task id 绝不能跨越会话边界。 ## 决策 -ACP 桥接层将活跃会话存储在 `Map` 中,并维护一个 `WeakMap` 反向索引,供 agent 作用域的回调使用。一条记录拥有其 agent 句柄、进行中的 prompt、活跃的工具调用展示状态、待生效的空闲配置切换、会话 cwd 以及客户端能力快照。一个独立的 loading-id 集合在异步恢复之前预留每个 id,使两个流水线化的加载请求无法构造重复的 agent;不同 id 可以并发加载。 +ACP 桥接层将活跃会话存储在 `Map` 中,并维护一个 `WeakMap` 反向索引,用于 agent 作用域的回调。一条记录拥有其 agent 句柄、进行中的 prompt、活跃的工具调用展示状态、待处理的空闲配置切换、会话 cwd 以及客户端能力快照。一个独立的 loading-id 集合在异步恢复之前预留每个 id,使两个流水线化的加载请求无法构造出重复的 agent;不同 id 可以并发加载。 -每个 `session/event` 和 `agent/status` 回调在发送或结算任何内容之前,先解析出所属记录。每个会话独立允许一个进行中的 prompt。prompt 记录一个日志水位线,捕获自己的 `turn/start`,并仅在匹配的 `turn/end` 到来时结算;来自已取消的先前轮次的迟到 end 不能 resolve 更新的 prompt。`session/cancel` 定位到单条记录,只调用该 agent 的队列感知取消路径。 +每个 `session/event` 和 `agent/status` 回调在发送或结算任何内容之前,先解析出所属记录。每个会话独立允许一个进行中的 prompt。prompt 记录一个日志水位线,捕获自己的 `turn/start`,并仅在匹配的 `turn/end` 到达时结算;来自已取消的前一轮次的迟到 end 不能 resolve 更新的 prompt。`session/cancel` 定位到一条记录,只调用该 agent 的队列感知取消路径。 -权限归属使用同一个反向索引。ACP `approval/request` 应答器仅向拥有发起请求的 agent 的编辑器会话发起提示,并将外部请求委托出去。用户交互引出同样按 agent 归属路由。每会话的沙箱和审批配置值仅折叠该会话自身的事件,待生效的空闲切换存储在该记录上,直到下一个轮次将其锚定。 +权限归属使用同一个反向索引。ACP `approval/request` 应答器只向拥有发起请求的 agent 的编辑器会话发起提示,并将外部请求委托出去。用户交互引出同样按 agent 归属路由。每会话的沙箱和审批配置值只折叠该会话自身的事件,待处理的空闲切换存储在该记录上,直到下一轮次将其锚定。 -后台 bash 任务携带一个不透明的 owner token,其值等于所属会话的 session id。`bash_output` 和 `bash_kill` 在读取或终止之前,会将调用方的 token 与执行器的任务归属进行比较;仅凭可预测的 task id 不授予访问权限。归属信息存储在执行器任务上,因此工具插件重载不会擦除它。 +后台 bash 任务携带一个不透明的 owner token,其值等于所属会话 id。`bash_output` 和 `bash_kill` 在读取或终止之前,将调用方的 token 与执行器的任务归属进行比较;仅凭可预测的 task id 不能获得访问权。归属信息与执行器任务一起存储,因此工具插件重载不会擦除它。 -连接拆除时清空活跃 map,将每个待结算的 prompt 以取消状态结算,并并行 dispose 所有 `AgentHandle`。每个句柄停止并等待其循环结束,在仍挂载时刷新会话,注销 agent,然后移除会话。拆除操作被 memoize 并在客户端断开与插件 dispose 之间共享。 +连接拆除时清空活跃 map,将每个待处理的 prompt 以取消状态结算,并并行 dispose(资源释放)所有 `AgentHandle`。每个句柄停止并等待其循环完成、在仍然附着时刷新会话、注销 agent 并移除会话。拆除操作被 memoize 化,由客户端断连和插件 dispose 共享。 ## 曾考虑的替代方案 -**每连接单活跃会话**:否决。它增加进程开销,与目标客户端的多会话形态相矛盾,且并未消除编辑器端的多路复用需求。 +**每连接单活跃会话**:否决。增加进程开销,与目标客户端的多会话形态相矛盾,且并未消除编辑器端的多路复用需求。 -**每会话一个 `ctx.extend()`**:否决。子上下文本身并不创建子插件 fiber,因此监听器仍属于桥接层 fiber。实际实现的桥接层使用全局监听器加显式 O(1) 解复用,以及每会话的归属记录;agent 生命周期由 `AgentHandle` 拥有。 +**每会话 `ctx.extend()`**:否决。子上下文本身不会创建子插件 fiber,因此监听器仍属于桥接层 fiber。实际实现的桥接层使用全局监听器加显式 O(1) 解复用,以及每会话拥有的记录;agent 生命周期由 `AgentHandle` 管理。 -**以 agent 对象标识作为 bash 任务归属**:否决。恢复或替换后的 agent 对象可能合法地代表同一个持久会话。不透明的 session token 才是应当在插件重载后存活的跨边界标识。 +**以 Agent 对象标识作为 bash 任务归属**:否决。恢复或替换后的 agent 对象可能合法地代表同一个持久会话。不透明的 session token 才是跨边界的标识,应当在插件重载后仍然存活。 ## 后果 -N 个会话可以并发地进行流式输出、prompt、权限请求、配置切换和后台任务运行,而不会交错或跨会话结算。一个会话中的取消或 dispose 不影响相邻会话。桥接层为此付出了显式 map 和隔离测试的代价,但它不为每个会话添加一套监听器,因此在长连接期间避免了监听器扇出。 +N 个会话可以并发地进行流式输出、prompt、权限请求、配置切换和后台任务运行,而不会交错或跨会话结算。一个会话中的取消或 dispose 不影响相邻会话。桥接层为此付出了显式 map 和隔离测试的代价,但它不会为每个会话添加一组监听器,从而避免了长连接期间的监听器扇出。 -桥接层目前仍未暴露独立关闭单个活跃会话的协议方法。当前所有记录在连接拆除时一起离开;会话关闭/恢复的生命周期能力在 ACP 功能清单中仍处于推迟状态。 +桥接层目前仍未暴露独立关闭单个活跃会话的协议方法。当前所有记录在连接拆除时一起离开;会话关闭/恢复的生命周期能力在 ACP 功能清单中仍处于延期状态。 ## 验证 -多会话测试套件通过交错更新、独立的进行中 prompt、定向取消、相同 id 与不同 id 的加载竞争、权限路由、配置隔离和拆除来驱动并发会话。工具 bash 测试证明一个会话无法读取或终止另一个会话的后台任务。 +多会话测试套件通过交错更新、独立的进行中 prompt、定向取消、相同 id 与不同 id 的加载竞争、权限路由、配置隔离以及拆除来驱动并发会话。工具 bash 测试证明一个会话无法读取或终止另一个会话的后台任务。 diff --git a/docs/rfc/implemented/feature/2026-06-15-code-mode.i18n.yaml b/docs/rfc/implemented/feature/2026-06-15-code-mode.i18n.yaml index b7e263a61a..5aeee85382 100644 --- a/docs/rfc/implemented/feature/2026-06-15-code-mode.i18n.yaml +++ b/docs/rfc/implemented/feature/2026-06-15-code-mode.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-15-code-mode.md: c64b6d6d8442e60240fa6c849833385ed50d71ff -2026-06-15-code-mode.zh.md: f94ef61dae180bad5ec604b5c7f9552eeecff593 +2026-06-15-code-mode.zh.md: e9ae74f6629f3e34a0e97f0fa532764c70095bba diff --git a/docs/rfc/implemented/feature/2026-06-15-code-mode.zh.md b/docs/rfc/implemented/feature/2026-06-15-code-mode.zh.md index f94ef61dae..e9ae74f662 100644 --- a/docs/rfc/implemented/feature/2026-06-15-code-mode.zh.md +++ b/docs/rfc/implemented/feature/2026-06-15-code-mode.zh.md @@ -1,132 +1,132 @@ # RFC:Code Mode——模型针对工具注册表编写 TypeScript -Status: implemented - [English](2026-06-15-code-mode.md) | 中文 +Status: implemented + ## 问题 -在注册表的原生呈现方式中,agent loop(智能体循环)将每个可见能力作为 JSON Schema 函数定义广播。`ToolRegistry` 将其 schema 贡献给系统提示词组装,组装结果中的 `tools` 落到协议格式(wire format)上(也记录在请求头日志中),模型每步调用一个 `tool-call` 块,循环通过 `ctx.tools.execute()` **逐个**分发每次调用(并行工具执行是 `dsh-tools` 和 [docs/architecture.md](../../../architecture.md) 中明确标注的 open TODO),且**每个**中间 `tool-result` 都在下一次请求时重新进入模型上下文。 +在注册表的原生呈现方式下,agent loop(智能体循环)将每个可见能力以 JSON Schema 函数定义的形式通告给模型。`ToolRegistry` 将其 schema 贡献给系统提示词组装,组装结果中的 `tools` 落到协议格式(wire format)上(也记录在请求头日志中),模型每步调用一个 `tool-call` 块,循环通过 `ctx.tools.execute()` **逐个**分发每次调用(并行工具执行是 `dsh-tools` 和 [docs/architecture.md](../../../architecture.md) 中明确标注的 open TODO),且**每一个**中间 `tool-result` 都会在下一次请求时重新进入模型上下文。 -对于多步工具操作,这种方式 token 开销大且串行。模型无法组合工具——遍历结果集、根据中间值分支、扇出、后处理——每次调用都需要一次完整的模型往返,而每次往返都把整个中间结果拖回上下文,无论模型是否需要。 +对于多步工具操作,这种方式 token 开销大且串行。模型无法组合工具——遍历结果集、根据中间值分支、扇出、后处理——每次调用都需要一次完整的模型往返,而每次往返都会把完整的中间结果拖回上下文,不管模型是否需要。 -Cloudflare 的 [Code Mode](https://blog.cloudflare.com/code-mode/) 提出了一种替代方案,基于一个简单观察:LLM(大语言模型)写代码比发出工具调用更擅长,因为它们见过数百万行真实代码,而见过的人造工具调用 trace 相对很少。模型不再每步发出一个工具调用,而是针对工具生成的 API 编写一段 TypeScript 程序,程序在沙箱运行时中执行,模型只取回它打印或返回的内容——而非所有中间结果。 +Cloudflare 的 [Code Mode](https://blog.cloudflare.com/code-mode/) 提出了一种替代方案,基于一个简单的观察:LLM(大语言模型)编写代码的能力优于发出工具调用,因为它们见过数百万行真实代码,而人为构造的工具调用 trace 相对很少。模型不再每步发出一次工具调用,而是针对工具生成的 API 编写一段 TypeScript 程序,程序在沙箱运行时中执行,模型只策展返回的内容——仅限它 print 或 return 的部分——而非所有中间结果。 -工具呈现属于拥有工具可见性的注册表:如果把第二种呈现方式实现为事后的 waterfall(瀑布式事件)变换,正确性将依赖监听器顺序,并与[可重建请求](../../implemented/architecture/2026-07-05-reconstructable-requests.md)冲突。执行基底同样属于基础设施而非占位符:Node `worker_threads` 提供独立隔离区、空环境、堆上限以及对热同步循环的终止能力,同时契合 harness 现有的信任模型(见§信任姿态)。 +工具呈现属于掌管工具可见性的注册表:如果把第二种呈现方式实现为事后的 waterfall(瀑布式事件)变换,正确性将依赖监听器顺序,并与[可重建请求](../../implemented/architecture/2026-07-05-reconstructable-requests.md)冲突。执行基底同样属于基础设施而非占位实现:Node `worker_threads` 提供独立 isolate、空环境、堆上限以及对热同步循环的终止能力,同时契合 harness 既有的信任模型(§信任姿态)。 ## 决策 三项决策,各自在下方独立小节中展开: -1. **Code Mode 是 `ToolRegistry`(`dsh-tools`)的一等呈现模式**,通过经过校验的 `mode` 配置选择:`'native'`(默认,贡献可见能力 schema)、`'code'`(注册表仅贡献其保留的 `run_code` 传输通道加一份生成的 SDK `.d.ts` 放入系统提示词)、或 `'both'`(原生 schema 加传输通道 + SDK)。注册表在源头塑造其规范贡献;协作式 prompt 组装的结果仍具权威性,请求头日志记录的正是该返回的呈现。 +1. **Code Mode 是 `ToolRegistry`(`dsh-tools`)的一等呈现模式**,通过经校验的 `mode` 配置选择:`'native'`(默认,贡献可见能力 schema)、`'code'`(注册表仅贡献其保留的 `run_code` 传输通道加一份生成的 SDK `.d.ts` 到系统提示词中)或 `'both'`(原生 schema 加传输通道 + SDK)。注册表在源头塑造其权威贡献;协作式 prompt 组装的结果仍具权威性,记录在日志中的请求头精确反映该返回的呈现。 2. **代码执行是一个能力 seam**——`packages/code-runtime/` 包含接口包 `@deepseek-ai/dsh-code-runtime`,拥有 `ctx.codeRuntime`([能力 seam](../../implemented/architecture/2026-06-13-capability-seams.md);消费方 = `dsh-tools`,core 消费 seam 的先例见 `agent-loop` → `dsh-llm`)。运行时对工具一无所知:它接收一段程序和命名的异步绑定,执行程序,报告 `{ value, logs, error? }`。语言和基底是后端属性,因此未来的 Python 或容器后端只是另一个实现包,而非重新设计。 -3. **交付的实现是 `@deepseek-ai/dsh-code-runtime-worker`**:每次运行启动一个全新的 Node worker 线程,对模型的 TypeScript 进行 type-strip 后执行,绑定通过消息端口桥接,环境为空,堆/输出/时间上限可配置,并支持硬终止。其信任姿态在设计上等同于 bash——不需要 unsafe-acknowledgement 标志——因为 harness 已经提供了 `dsh-bash-local`,后者以严格**更大**的环境权限执行模型编写的任意 shell 命令。 +3. **交付的实现是 `@deepseek-ai/dsh-code-runtime-worker`**:每次运行 spawn 一个全新的 Node worker 线程,对模型的 TypeScript 进行 type-strip 后执行,绑定通过 message port 桥接,环境为空,堆/输出/时间上限可配置,并支持硬终止。其信任姿态在设计上等同于 bash——无需 unsafe-acknowledgement flag——因为 harness 已经交付了 `dsh-bash-local`,后者以严格**更高**的环境权限执行模型编写的任意 shell 命令。 ### 注册表拥有模式 -`ToolRegistry` 获得一个 schemastery 校验的配置(`static Config`),这是它的第一个配置:`mode: 'native' | 'code' | 'both'`,默认 `'native'`。部署通过 `cordis.yml` 切换(`tools: { mode: code }`)——无需改代码,遵循 no-hardcoded-tunables 约定。 +`ToolRegistry` 获得一个经 schemastery 校验的配置(`static Config`),这是它的第一个配置:`mode: 'native' | 'code' | 'both'`,默认 `'native'`。部署通过 `cordis.yml` 翻转模式(`tools: { mode: code }`),无需改代码,遵循 no-hardcoded-tunables 约定。 **协议工具列表。** 注册表在 `'native'` 下贡献可见能力,在 `'code'` 下仅贡献 `run_code`,在 `'both'` 下两者都贡献。最终的 `PromptAssembly.tools` 列表记录在请求头中。`run_code` 是一个保留的呈现传输通道,位于注册和限制层之外;直接 prompt 提供方和组装 waterfall 仍各自负责自己的贡献。 -**与 `toolOrder` 的交互,预先声明:** 如果配置的 `systemPrompt.toolOrder` 命名了原生能力,则在 `mode: 'code'` 下会拒绝所有组装,因为这些名称不在该模式的协议校验范围内。这是正确行为,不是 bug:使用 Code Mode 的部署需要更新其 order 配置或移除它。 +**与 `toolOrder` 的交互,预先说明:** 如果配置的 `systemPrompt.toolOrder` 引用了原生能力名称,在 `mode: 'code'` 下会拒绝所有组装,因为那些名称不在该模式的协议校验范围内。这是正确行为而非 bug:使用 Code Mode 的部署需要更新其 order 配置或移除它。 -**SDK prompt 段落。** 在 `'code'` 和 `'both'` 下,tool-guidance order band 中的惰性 `tools:sdk` 段落为作用域内可见能力渲染 TypeScript 声明加固定的使用说明。它共享查找和执行可见性,排除 `run_code`,并按字典序排列工具以获得字节稳定的输出。 +**SDK prompt 段。** 在 `'code'` 和 `'both'` 下,tool-guidance order band 中的惰性 `tools:sdk` 段为当前 scope 的可见能力渲染 TypeScript 声明加固定的使用说明。它共享查找和执行可见性,排除 `run_code`,并按字典序排列工具以获得字节稳定的输出。 -**组装所有权。** `run_code` 和 `tools:sdk` 作为正常的组装输入进入受信任的 `system-prompt/assemble` waterfall。作用域内的 `tools:sdk` 段落可以在分发前遮蔽全局默认值,监听器可以移除或替换任一贡献。waterfall 返回的组装结果是最终的,因此修改这些输入的人有责任在部署期望 Code Mode 可用时保持协议可行;没有恢复 pass 会覆盖有意的组合。 +**组装所有权。** `run_code` 和 `tools:sdk` 作为正常的组装输入进入受信任的 `system-prompt/assemble` waterfall。一个 scoped 的 `tools:sdk` 段可以在分发前遮蔽全局默认值,监听器也可以移除或替换任一贡献。waterfall 返回的组装结果是最终的,因此修改这些输入的人有责任在部署期望 Code Mode 可用时保持协议面的完整性;没有恢复 pass 会覆盖有意的组合。 -**代码生成。** `jsonSchemaToTs()` 将 `defineTool` 的 JSON Schema 子集映射为 TypeScript,将 schema 描述带入 JSDoc,并将不支持的构造降级为 `unknown`。SDK 以带引号的对象键暴露工具,支持任意名称而无需别名或冲突处理。类型是建议性的,因为运行时在执行前会剥离类型。 +**代码生成。** `jsonSchemaToTs()` 将 `defineTool` 的 JSON Schema 子集映射为 TypeScript,将 schema 描述带入 JSDoc,不支持的构造降级为 `unknown`。SDK 将工具暴露为带引号的对象键,支持任意名称而无需别名或冲突处理。类型是建议性的,因为运行时在执行前会剥离类型。 -### run_code 工具与分发桥接 +### run_code 工具与分发桥 -在 `'code'` 和 `'both'` 下,注册表拥有 `run_code` 作为保留的呈现传输通道,带一个必需参数 `{ code: string }`。它由一个正常的 `ToolDefinition` 表示以便分发,但位于可过滤的能力层之外,因此限制规则不会意外移除 Code Mode 的唯一入口。调用遍历完整的工具流水线——`tools/pre-execute` → 单调守卫 → `tools/execute` 包裹分发 → `tools/post-execute` → 不可变的 `tools/result` 通知——与原生调用完全一致;权限插件可以在程序运行前检查程序文本,最终结果观察者看到的是归一化的外层结果。其 `execute(args, exec)`: +在 `'code'` 和 `'both'` 下,注册表拥有 `run_code` 作为保留的呈现传输通道,带一个必需参数 `{ code: string }`。它由一个正常的 `ToolDefinition` 表示以供分发,但位于可过滤的能力层之外,因此限制规则不会意外移除 Code Mode 的唯一入口。调用遍历完整的工具流水线——`tools/pre-execute` → 单调守卫 → `tools/execute` 包裹分发 → `tools/post-execute` → 不可变的 `tools/result` 通知——与原生调用完全一致;权限插件可以在程序运行前检查程序文本,最终结果观察者看到的是规范化的外层结果。其 `execute(args, exec)`: -1. **构建绑定。** 一个 run 作用域的 signal 跟随外层取消,并在 run 结算时被 abort。每个可见工具绑定对其参数做 JSON 归一化——在分发前拒绝有损值——等待序列化队列,以确定性的 call id 和外层 token 作为 `parent` 执行,并记录 `tool/code-dispatch`。成功的文本变为字符串,非文本块变为占位符;工具错误使绑定 promise reject。每个子调用保留自己的不可变执行身份,并遍历完整的工具流水线。 -2. **运行程序**:`ctx.codeRuntime.run({ program: args.code, bindings: [{ global: 'tools', functions }], signal: runController.signal })`。运行时接收的是 run 作用域的 signal 而非仅调用方的外层 signal,因此外层 run 以任何方式结算都会同时 abort 运行时内部的工作。 -3. **静默后结算。** 运行时结算后,桥接 abort 未完成的工作并排空分发队列后再返回。成功时返回捕获的输出和呈现元数据。运行时失败变为 `CodeRunFailedError`;后端拒绝使用注册表的正常错误边界。两者都产生结构化的错误结果,且 `run_code` 结算后没有子调用可以追加。 +1. **构建绑定。** 一个 run 级别的 signal 跟随外层取消,并在 run 结算时被 abort。每个可见工具绑定对参数做 JSON 规范化——在分发前拒绝有损值——等待序列化队列,以确定性的 call id 和外层 token 作为 `parent` 执行,并记录 `tool/code-dispatch`。成功的文本变为字符串,非文本块变为占位符;工具错误使绑定 promise reject。每个子调用保留自己不可变的执行标识,并遍历完整的工具流水线。 +2. **运行程序**:`ctx.codeRuntime.run({ program: args.code, bindings: [{ global: 'tools', functions }], signal: runController.signal })`。运行时接收的是 run 级别的 signal 而非仅调用方的外层 signal,因此外层 run 以任何方式结算都会同时 abort 运行时内部的工作。 +3. **静默后结算。** 运行时结算后,桥 abort 未完成的工作并排空分发队列后再返回。成功时返回捕获的输出和呈现元数据。运行时失败变为 `CodeRunFailedError`;后端拒绝使用注册表的正常错误边界。两者都产生结构化的错误结果,且 `run_code` 结算后不允许子调用追加。 -**子调用的 `additionalContext` 被省略。** 在 `run_code` 期间注入它会破坏父调用/结果的邻接性,而一个程序可以产生多个上下文。支持它需要一个复数通道或循环级别的子分发缓冲区。 +**子调用的 `additionalContext` 被省略。** 在 `run_code` 期间注入它会破坏父调用/结果的相邻性,而一个程序可以产生多个 context。支持它需要一个复数通道或循环级别的子分发缓冲区。 -**并发被序列化。** 每次 run 拥有一个分发队列,因此即使 `Promise.all` 也按提交顺序执行工具调用。结算时放弃尚未开始的排队调用。并行化需要逐工具的并发安全元数据。 +**并发被序列化。** 每次 run 拥有一个分发队列,因此即使 `Promise.all` 也按提交顺序执行工具调用。结算时放弃尚未开始的排队调用。并行化需要每个工具的并发安全元数据。 -**呈现。** `run_code` 的渲染意图按 [render-intent RFC](../../implemented/architecture/2026-07-02-tool-render-intent-union.md) 在此决定:`presentCall` → 一个 `generic` 卡片,`kind: 'execute'`,title = 程序文本,`rawInput` = 同一段程序文本;`presentResult` → 一个 `generic` 卡片,内容为捕获的输出(来自 `meta`)。程序作为 title 是因为 ACP execute 卡片可靠地渲染该字段,而某些客户端会省略 body 和 raw-input 内容。这不是 `terminal` 卡片:该卡片的语义是「工作目录中的 shell 命令」,程序不是。 +**呈现。** `run_code` 的 render intent 按 [render-intent RFC](../../implemented/architecture/2026-07-02-tool-render-intent-union.md) 在此决定:`presentCall` → 一个 `generic` 卡片,`kind: 'execute'`,title = 程序文本,`rawInput` = 同一程序文本;`presentResult` → 一个 `generic` 卡片,content 为捕获的输出(来自 `meta`)。程序作为 title 是因为 ACP execute 卡片可靠地渲染该字段,而某些客户端会省略 body 和 raw-input 内容。这不是 `terminal` 卡片:该卡片的语义是「工作目录中的 shell 命令」,程序不是。 ### 可观测性:`tool/code-dispatch` -每次子分发追加一个仅日志的 `tool/code-dispatch` 事件,包含父子 call id、工具身份、归一化参数和结果摘要。它不进入模型历史,但可供持久化和 UI 使用。追加发生在打开的 `run_code` 轮次内。没有 agent 的直接执行仍然运行,但无法记录该事件。 +每次子分发追加一个仅日志的 `tool/code-dispatch` 事件,包含父子 call id、工具标识、规范化参数和结果摘要。它不进入模型历史,但可供持久化和 UI 使用。追加发生在开放的 `run_code` 轮次内。没有 agent 的直接执行仍然运行,但无法记录该事件。 ### code-runtime seam -`packages/code-runtime/code-runtime/`——`@deepseek-ai/dsh-code-runtime`,仅依赖 `cordis`。一个抽象的 `CodeRuntime extends Service`(`super(ctx, 'codeRuntime')`)加词汇: +`packages/code-runtime/code-runtime/`——`@deepseek-ai/dsh-code-runtime`,仅依赖 `cordis`。一个抽象的 `CodeRuntime extends Service`(`super(ctx, 'codeRuntime')`)加上词汇: - `CodeRunRequest = { program: string; bindings: CodeBindingNamespace[]; signal?: AbortSignal }` - `CodeBindingNamespace = { global: string; functions: Record Promise> }`——运行时将每个命名空间作为程序内部的全局异步函数对象暴露;绑定参数和解析值必须是 structured-cloneable 的(运行时可能跨越序列化边界;我们的实现确实如此)。 -- `CodeRunResult = { value?: unknown; logs: CodeLogEntry[]; error?: CodeRunFailure }`——程序执行结果,包括异常、超时、abort 和 worker 退出,以 `error` 字段解析。`run()` 仅在调用方/seam 误用时才 reject(例如重复的绑定命名空间);消费方仍在自己的错误边界处理不合规的后端 rejection。 +- `CodeRunResult = { value?: unknown; logs: CodeLogEntry[]; error?: CodeRunFailure }`——程序执行结果,包括异常、超时、abort 和 worker 退出,都解析为 `error` 字段。`run()` 仅在调用方/seam 误用时才 reject(例如重复的绑定命名空间);消费方仍在自己的错误边界处理不合规的后端拒绝。 - `CodeLogEntry = { source: 'console' | 'stdout' | 'stderr'; level?: 'log' | 'info' | 'warn' | 'error' | 'debug'; text: string }` -- `CodeRunFailure = { kind: 'exception' | 'timeout' | 'abort' | 'worker-exit'; message: string }`——正交的结果按[防御性模式](../../../defensive-patterns.md)独立报告;超时的 run 不是异常,abort 不是超时。 -- 两个只读的后端描述符,仅供信息参考不用于门控:`language`(程序必须使用的语言——交付的后端为 `'typescript'`;Python 后端会声明自己,并在呈现侧配对自己的 SDK 生成器)和 `isolation`(交付的后端为 `'worker-thread'`;未来的为 `'process'`、`'container'` 等)。`dsh-tools` 在 MVP 中要求 `language === 'typescript'`——其代码生成输出 TS——否则组装会大声失败,与 `toolOrder` 违规时的配置错误惯用法相同(如 `mode` 为非 native 但根本没有加载 `ctx.codeRuntime`)。 +- `CodeRunFailure = { kind: 'exception' | 'timeout' | 'abort' | 'worker-exit'; message: string }`——按[防御性模式](../../../defensive-patterns.md)独立报告的正交结果;超时的 run 不是异常,abort 不是超时。 +- 两个只读的后端描述符,仅供信息参考而非门禁判定:`language`(程序必须使用的语言——交付的后端为 `'typescript'`;Python 后端会声明自己,并在呈现侧配对自己的 SDK 生成器)和 `isolation`(交付的后端为 `'worker-thread'`;未来可为 `'process'`、`'container'` 等)。`dsh-tools` 在 MVP 中要求 `language === 'typescript'`——其代码生成输出 TS——否则组装会大声失败,与 `toolOrder` 违规时的配置错误惯用法相同(如 `mode` 为非 native 但根本没有加载 `ctx.codeRuntime`)。 -请求包含所有运行时输入;实现方拥有经过校验的超时和上限默认值。注册表仅在组装 Code Mode 时查找可选的运行时,因此原生模式不依赖它。缺失或语言不兼容的运行时会大声失败。替代基底或语言可以在同一 seam 后面替换实现,配对相应的 SDK 生成器。 +请求包含所有运行时输入;实现方拥有经校验的超时和上限默认值。注册表仅在组装 Code Mode 时查找可选的运行时,因此 native 模式不依赖它。缺失或语言不兼容的运行时会大声失败。替代基底或语言可以在同一 seam 背后替换实现,配对相应的 SDK 生成器。 -### worker 线程运行时 +### worker-thread 运行时 -`@deepseek-ai/dsh-code-runtime-worker`,`packages/code-runtime/` 组的第二个包。每次 `run()`: +`@deepseek-ai/dsh-code-runtime-worker`,`packages/code-runtime/` 组的第二个包(package)。每次 `run()`: -1. **宿主侧 type-strip**,使用 Node 内置的 `stripTypeScriptTypes`(`node:module`;在本仓库的整个引擎范围 `^22.19.0 || >=24.0.0` 内可用,且保持位置不变,因此运行时错误行号与模型源码一致)。Strip-only 模式拒绝不可擦除的语法(`enum`、namespaces)——该拒绝以 `error.kind: 'exception'` 加 Node 的消息返回,SDK 说明写明「仅限可擦除 TypeScript」,模型像处理任何其他程序错误一样自我纠正。语法级别的失败永远不会 spawn worker。 -2. **每次 run spawn 一个全新 `Worker`**,来自包自身的 bootstrap 模块:`env: {}`(真正为空——比 spawn 命令的 scrubbed-env 规则更严格),`resourceLimits` 来自配置,`stdout`/`stderr` 捕获到 `logs` 而非继承。不做池化、不跨 run 共享状态:程序的世界随 worker 消亡,这使得 run 仅从日志即可重建,且状态泄漏不可表达。 -3. **在 bootstrap 中执行**:剥离类型后的程序成为一个 `AsyncFunction` 的函数体,其参数是绑定全局变量和一个捕获式 `console` shim,因此顶层 `await` 和 `return` 可用,程序的完成值即为 run 的 `value`(structured-cloneable 值原样跨越;其他值被替换为其 `util.inspect` 渲染,已文档化)。 -4. **通过消息端口桥接绑定**:worker 中的每个绑定函数发送 `{ id, global, name, args }` 并等待回复;宿主根据请求的绑定校验名称、调用、并回复 `{ id, ok, value }` 或 `{ id, ok: false, message }`(宿主侧绑定拒绝变为程序侧 rejection)。worker 侧的命名空间对象通过 `defineProperty` 构建为 null-prototype,因此名为 `__proto__`、`constructor` 或 `toString` 的绑定是普通的自有属性,不会产生原型链冲突。未知名称、重复 id 和结算后的消息被拒绝或忽略——端口协议假设对端是敌对的,因为对端运行的是模型代码。 -5. **强制独立预算。** `computeMs` 计量 worker 忙碌时间,允许慢速的 awaited 工具而不放过热循环。`maxWallMs` 限制总经过时间,包括未完成的等待。到期、取消和完成都会终止 worker。堆退出和截断被显式报告;compute、wall、heap、log 和返回值上限都是经过校验的配置。 -6. **Dispose 至静默**:服务自身的 disposal 终止进行中的 worker 并*等待*它们退出后再 resolve,遵循[防御性模式](../../../defensive-patterns.md)。 +1. **宿主侧 type-strip**,使用 Node 内置的 `stripTypeScriptTypes`(`node:module`;在本仓库的整个引擎范围 `^22.19.0 || >=24.0.0` 内可用,且保持位置不变,因此运行时错误行号与模型源码一致)。仅剥离模式拒绝不可擦除的语法(`enum`、namespaces)——该拒绝以 `error.kind: 'exception'` 加 Node 的消息返回,SDK 说明写明「仅限可擦除 TypeScript」,模型像处理其他程序错误一样自我修正。语法级失败不会 spawn worker。 +2. **每次 run spawn 一个全新 `Worker`**,来自包自身的 bootstrap 模块:`env: {}`(真正为空——比 spawn 命令的 scrubbed-env 规则更严格),`resourceLimits` 来自配置,`stdout`/`stderr` 捕获到 `logs` 而非继承。不做池化,不跨 run 保留状态:程序的世界随 worker 消亡,这使得 run 仅从日志即可重建,状态泄漏不可表达。 +3. **在 bootstrap 中执行**:剥离后的程序成为一个 `AsyncFunction` 的函数体,其参数是绑定全局变量和一个捕获式 `console` shim,因此顶层 `await` 和 `return` 可用,程序的完成值即为 run 的 `value`(structured-cloneable 值原样跨越;其他值被替换为其 `util.inspect` 渲染,已文档化)。 +4. **通过 message port 桥接绑定**:worker 中的每个绑定函数发送 `{ id, global, name, args }` 并等待回复;宿主根据请求的绑定校验名称、调用、并回复 `{ id, ok, value }` 或 `{ id, ok: false, message }`(宿主侧绑定拒绝变为程序侧 rejection)。worker 侧的命名空间对象通过 `defineProperty` 构建为 null-prototype,因此名为 `__proto__`、`constructor` 或 `toString` 的绑定是普通自有属性,而非原型链碰撞。未知名称、重复 id 和结算后消息被拒绝或忽略——端口协议假设对端是恶意的,因为对端运行的是模型代码。 +5. **强制独立预算。** `computeMs` 计量 worker 忙碌时间,允许慢速的 awaited 工具而不放过热循环。`maxWallMs` 约束总经过时间,包括未解析的等待。到期、取消和完成都终止 worker。堆退出和截断被显式报告;compute、wall、heap、log 和返回值上限是经校验的配置。 +6. **dispose 至静默**:服务自身的 dispose(资源释放)终止进行中的 worker 并*等待*其退出后再 resolve,遵循[防御性模式](../../../defensive-patterns.md)。 ### 信任姿态 -worker 运行时提供的是封闭隔离,而非安全边界:模型代码可以触及 Node API,权限与 bash 工具相当。`worker.terminate()` 停止线程但不停止它 spawn 的 OS 进程。Code Mode 使用与 bash 相同的 `tools/pre-execute` 策略门控,并额外提供空环境、堆限制、独立隔离区和对程序本身的硬终止。需要硬多租户边界的部署需要为代码和 bash 都使用容器级后端;运行时的 isolation 描述符让它们能区分该后端。 +worker 运行时提供的是隔离,而非安全边界:模型代码可以访问 Node API,权限与 bash 工具相当。`worker.terminate()` 停止线程但不停止它 spawn 的 OS 进程。Code Mode 使用与 bash 相同的 `tools/pre-execute` 策略门禁,并额外提供空环境、堆限制、独立 isolate 和对程序本身的硬终止。需要硬多租户边界的部署需要为代码和 bash 都使用容器级后端;运行时的 isolation 描述符让它们能区分该后端。 ### 模型看到的内容 -SDK 指示模型编写一个异步的可擦除 TypeScript 函数体,通过 `await tools.name(args)` 调用工具,在需要时 catch 被 reject 的工具调用,并仅返回或打印应重新进入上下文的输出。即使在 `Promise.all` 下调用仍保持顺序。声明前缀可以与原生 schema 一样大,尤其在 `'both'` 下,但对提供方缓存保持稳定。 +SDK 指示模型编写一个异步的可擦除 TypeScript 函数体,通过 `await tools.name(args)` 调用工具,在需要时捕获被拒绝的工具调用,并仅 return 或 log 应重新进入上下文的输出。即使在 `Promise.all` 下调用仍保持顺序。声明前缀可能与原生 schema 一样大,尤其在 `'both'` 下,但对提供方缓存保持稳定。 ## 后果 -切换到 `'code'` 的部署必须更新任何仅原生的 `toolOrder`。组装监听器负责维护任何被重写的协议表面的完整性。子分发保持序列化,桥接不会传播逐调用的 `additionalContext`,直到为 Code Mode 设计好这些契约。 +切换到 `'code'` 的部署必须更新任何仅限 native 的 `toolOrder`。组装监听器有责任维护任何被重写的协议面的完整性。子分发保持序列化,桥不传播每次调用的 `additionalContext`,直到为 Code Mode 设计好这些契约。 ## 测试 -- **Worker 运行时:** 真实 worker 测试覆盖输出和值捕获、失败类型、compute 和 wall 预算、敌对绑定流量、空环境、structured-clone 回退、输出上限和 disposal 至静默。一个 built-package 测试在纯 Node 下运行 worker 入口。 -- **注册表集成:** 测试覆盖代码生成、所有呈现模式、保留名称和限制规则、作用域可见性、权威组装重写、`toolOrder`、运行时兼容性失败、完整流水线子分发、parent-token 关联、序列化、取消和队列排空、JSON 归一化、错误传播、日志事件、省略的 `additionalContext` 和 HMR(热模块替换)清理。 -- **带密钥 e2e:** 真实模型在一个程序中组合两次 bash 调用;测试验证折叠的请求头、关联的分发事件、生成的文件和精选的回答。 -- **快照:** `code-mode-turn` 和 `both-mode-turn` fixture(测试前置数据)固定 SDK 段落、头部工具列表、分发事件和结果卡片。 +- **Worker 运行时:** 真实 worker 测试覆盖输出和值捕获、失败类型、compute 和 wall 预算、恶意绑定流量、空环境、structured-clone 回退、输出上限和 dispose 至静默。一个构建后包测试在纯 Node 下运行 worker 入口。 +- **注册表集成:** 测试覆盖代码生成、所有呈现模式、保留名称和限制规则、scoped 可见性、权威组装重写、`toolOrder`、运行时兼容性失败、完整流水线子分发、parent-token 关联、序列化、取消和队列排空、JSON 规范化、错误传播、日志事件、省略的 `additionalContext` 和 HMR(热模块替换)清理。 +- **带密钥 e2e:** 真实模型在一个程序中组合两次 bash 调用;测试验证折叠的请求头、关联的分发事件、结果文件和策展后的回答。 +- **快照:** `code-mode-turn` 和 `both-mode-turn` fixture(测试前置数据)固定 SDK 段、请求头工具列表、分发事件和结果卡片。 ## 曾考虑的替代方案 -**一个零核心改动的附加消费方插件。** 否决,因为 `agent/request` 在[可重建请求](../../implemented/architecture/2026-07-05-reconstructable-requests.md)下仅限 call-config,而变换已组装的工具列表需要在不拥有其配置的情况下撤销 `toolOrder` 规范化,且依赖监听器顺序。模型被提供哪些工具、以何种表示,是注册表的单一关注点:原生 schema 和 SDK 是同一可见存储的两种投影。 +**一个零核心改动的附加消费方插件。** 否决,因为 `agent/request` 在[可重建请求](../../implemented/architecture/2026-07-05-reconstructable-requests.md)下仅限 call-config,而变换已组装的工具列表需要在不拥有其配置的情况下撤销 `toolOrder` 规范化,并依赖监听器顺序。向模型提供哪些工具、以何种表示形式提供,是注册表的单一关注点:原生 schema 和 SDK 是同一个可见存储的两种投影。 -**`node:vm` 作为参考运行时,加固推迟。** 否决:`node:vm` 不是隔离(原型链逃逸可达宿主 realm)且无法中断热循环。worker 线程提供独立隔离区、空环境、`resourceLimits` 和可靠的 `terminate()`,信任等级等同于 bash,因此参考实现和生产实现是同一个包,无需 unsafe-acknowledgement 仪式。 +**`node:vm` 作为参考运行时,加固推迟。** 否决:`node:vm` 不是隔离(原型链逃逸可达宿主 realm)且无法中断热循环。worker 线程提供独立 isolate、空环境、`resourceLimits` 和可靠的 `terminate()`,信任等级等同于 bash,因此参考实现和生产实现是同一个包,无需 unsafe-acknowledgement 仪式。 -**对原生工具调用做结果省略/摘要。** 仅解决问题的上下文膨胀一半:裁剪旧 `tool-result` 作为可重建请求下的日志表面替换很容易添加,但仍然每次调用付出一次模型往返,且无法表达循环、分支或汇合。互补而非竞争;它可以在 Code Mode 下为残余的原生调用分层。 +**在原生工具调用上做结果省略/摘要。** 仅解决问题的上下文膨胀一半:裁剪旧 `tool-result` 作为可重建请求下的日志化表面替换成本低,但仍需每次调用一次模型往返,且无法表达循环、分支或汇合。互补而非竞争;它可以在 Code Mode 下为残余的原生调用分层。 -**循环中的并行原生分发。** 往返开销的另一个答案;仍是有效的未来工作(open TODO),仍被并发安全元数据阻塞,且仍无组合能力——它并行化的是模型在一步中已经决定的调用。Code Mode 的序列化队列决策使两者兼容:当元数据就绪时,原生并行分发和逐工具绑定并行化一起解锁。 +**循环中的并行原生分发。** 往返成本的另一个答案;仍是有效的未来工作(open TODO),仍被并发安全元数据阻塞,且仍无组合能力——它并行化的是模型在一步中已经决定的调用。Code Mode 的序列化队列决策保持两者兼容:当元数据就绪时,原生并行分发和每工具绑定并行化一起解锁。 -**始终排他(忠实于 Cloudflare,无模式)。** 否决,因为本 SDK 的主要消费方是编码 agent:其日常的单次调用(`bash`、`read`、`edit`)作为原生调用已经是理想的,强迫每次编辑都通过程序会加重常见场景的负担。mode 配置让忠实形式(`'code'`)只需一行配置即可启用而不强加。 +**始终排他(忠于 Cloudflare,无模式)。** 否决,因为本 SDK 的主要消费方是编码 agent:其日常的单次调用(`bash`、`read`、`edit`)作为原生调用已经是最优的,强制每次编辑都通过程序会给常见场景增加负担。mode 配置让忠实形式(`'code'`)只需一行配置即可启用,而不强加于人。 -**逐工具可见性层级(此工具原生,彼工具仅 code)。** 推迟:它需要逐工具元数据和 `'native' | 'code' | 'both'` 不具备的呈现拆分,且其设计依赖于模型在 `'both'` 下如何分配使用的证据。 +**每工具可见性分层(此工具 native,彼工具 code-only)。** 推迟:它需要每工具元数据和 `'native' | 'code' | 'both'` 不提供的呈现拆分,且其设计取决于模型在 `'both'` 下如何分配使用的证据。 -**SDK 中的消毒标识符别名**(`my-tool` → `my_tool`,Cloudflare 的做法)。否决:`declare const` 上的带引号键使每个名称可达,零别名冲突逻辑;模型处理 `tools["my-tool"](…)` 没有问题。 +**SDK 中的清洁化标识符别名**(`my-tool` → `my_tool`,Cloudflare 的做法)。否决:`declare const` 上的带引号键使每个名称可达,零别名碰撞逻辑;模型能正常处理 `tools["my-tool"](…)`。 -**REPL 风格的持久内核**(状态跨 `run_code` 调用存活)。MVP 否决:跨调用状态对会话日志不可见,破坏了每个请求是日志纯函数的可重建性保证;每次 run 全新保持了这一点。内核风格后端在未来仍可通过 seam 表达,配合自己的日志方案。 +**REPL 风格的持久内核**(状态跨 `run_code` 调用存活)。在 MVP 中否决:跨调用状态对会话日志不可见,破坏了「每个请求是日志的纯函数」这一可重建性保证;每次 run 全新保持了这一点。内核风格的后端在未来仍可通过同一 seam 表达,配合自己的日志方案。 ## 风险 -**Worker 不是硬安全边界。** 有意为之且已文档化(见§信任姿态):姿态等同于现有 bash 工具,封闭隔离超过它,门控使用相同的 seam。需要更多的部署需要未来的 `isolation: 'container'` 后端——作为 seam 的设计扩展跟踪,而非本设计的 TODO。 +**Worker 不是硬安全边界。** 有意为之且已文档化(§信任姿态):姿态等同于既有的 bash 工具,隔离程度超过它,门禁使用相同的 seam。需要更强隔离的部署需要未来的 `isolation: 'container'` 后端——作为 seam 设计的扩展点跟踪,而非本设计的 TODO。 -**`stripTypeScriptTypes` 标记为 experimental。** 它与 Node 自身原生 `.ts` 执行背后的引擎(amaro/swc)相同,在本仓库的整个引擎范围内作为 API 暴露。缓解措施:运行时的单元测试套件固定了所依赖的行为(位置保持、可擦除限制的拒绝消息形状宽松匹配),调用位于一个私有函数后面,且 `amaro`/`sucrase` 是 API 变动时的即插即用替代品。可擦除子集是面向模型的契约线,错误路径是一个可工作的反馈循环,而非死胡同。 +**`stripTypeScriptTypes` 标记为 experimental。** 它与 Node 自身原生 `.ts` 执行背后的引擎(amaro/swc)相同,在本仓库的整个引擎范围内作为 API 暴露。缓解措施:运行时的单元测试套件固定了所依赖的行为(位置保持、可擦除限制的拒绝消息形状宽松匹配),调用位于一个私有函数之后,且 `amaro`/`sucrase` 是 API 变化时的直接替代品。仅可擦除子集是面向模型的契约线,错误路径是一个可工作的反馈循环,而非死胡同。 -**SDK 的 prompt 开销,尤其在 `'both'` 下。** `.d.ts` 可以与它补充的原生 schema 相当大;`'both'` 携带两种表示。前缀稳定性 + 提供方缓存摊销了每会话开销;mode 是逐部署的;本 RFC 不做无条件节省的声明。何时偏好哪种模式的量化指导明确是上线后的学习。 +**SDK 的 prompt 成本,尤其在 `'both'` 下。** `.d.ts` 可能与它补充的原生 schema 体量相当;`'both'` 携带两种表示。前缀稳定性 + 提供方缓存摊销了每会话成本;mode 是每部署的;本 RFC 不做无条件节省的声明。何时优先使用哪种模式的量化指导明确属于上线后学习。 -**注册表范围增长。** `dsh-tools` 吸收了代码生成、一个工具、一个桥接和一个事件。通过包内的模块边界(`ts-types.ts`、`code-mode.ts` 与 `schema.ts`/`json-schema.ts`/`presentation.ts` 并列)和 seam 来约束:所有基底形状的东西都在 `ctx.codeRuntime` 后面。 +**注册表 scope 增长。** `dsh-tools` 吸收了代码生成、一个工具、一个桥和一个事件。通过包内的模块边界(`ts-types.ts`、`code-mode.ts` 与 `schema.ts`/`json-schema.ts`/`presentation.ts` 并列)和 seam 约束:所有基底相关的内容都在 `ctx.codeRuntime` 之后。 -**Structured-clone 值可以超出 JSON。** 因此工具绑定在分发前对参数做 JSON 归一化,确保每个执行的调用都可以被记录。底层运行时保持其更宽的端口契约,而更严格的消费方在自己的边界处校验。非文本子结果变为占位符。 +**Structured-clone 值可能超出 JSON。** 因此工具绑定在分发前对参数做 JSON 规范化,确保每次执行的调用都可记录。底层运行时保持其更宽的端口契约,而更严格的消费方在自己的边界处校验。非文本子结果变为占位符。 -**仅序列化的子分发。** `Promise.all` 尚未获得挂钟并行性,仅减少了往返次数;模型可能过度期望。说明中已声明;解除此限制与原生并行分发 TODO 所需的相同并发安全元数据绑定。 +**仅序列化的子分发。** `Promise.all` 尚未获得挂钟并行性,仅减少往返次数;模型可能过度期望。说明中已声明;解除此限制与原生并行分发 TODO 所需的并发安全元数据绑定。 -**预算计量读取事件循环,而非标志。** 忙碌时间轮询(`eventLoopUtilization()`)比精确 CPU 计量更粗——预算到期最多延迟一个轮询间隔——且其正确性声明(「pending dispatch 无法暂停它」)对敌对程序是承重的。两侧都有单元测试(带 pending decoy dispatch 的热循环在 `computeMs` 时死亡;idle-on-slow-binding 存活到 `maxWallMs`),轮询间隔是内部常量而非配置——部署无法将其误调为绕过。 +**预算计量读取事件循环,而非 flag。** 忙碌时间轮询(`eventLoopUtilization()`)比精确 CPU 计量更粗糙——预算到期最多延迟一个轮询间隔——且其正确性声明(「pending 的分发不能暂停它」)对恶意程序是承重的。两侧都有单元测试(带 pending 诱饵分发的热循环在 `computeMs` 处死亡;在慢绑定上空闲的程序存活到 `maxWallMs`),轮询间隔是内部常量而非配置——部署无法将其误调为绕过手段。 diff --git a/docs/rfc/implemented/feature/2026-06-17-filesystem-tool-schemas.i18n.yaml b/docs/rfc/implemented/feature/2026-06-17-filesystem-tool-schemas.i18n.yaml index 6aac0209ec..f5eae41ba4 100644 --- a/docs/rfc/implemented/feature/2026-06-17-filesystem-tool-schemas.i18n.yaml +++ b/docs/rfc/implemented/feature/2026-06-17-filesystem-tool-schemas.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-17-filesystem-tool-schemas.md: c2d3aa679599b1129a19b9082b9254ecf3103f12 -2026-06-17-filesystem-tool-schemas.zh.md: b13274c41da244d7d2a5fe6ff2064d8d5e0a9b42 +2026-06-17-filesystem-tool-schemas.zh.md: cd504a37d5ee25f6634b651d30099afe5acd6495 diff --git a/docs/rfc/implemented/feature/2026-06-17-filesystem-tool-schemas.zh.md b/docs/rfc/implemented/feature/2026-06-17-filesystem-tool-schemas.zh.md index b13274c41d..cd504a37d5 100644 --- a/docs/rfc/implemented/feature/2026-06-17-filesystem-tool-schemas.zh.md +++ b/docs/rfc/implemented/feature/2026-06-17-filesystem-tool-schemas.zh.md @@ -1,24 +1,24 @@ -# RFC:文件系统工具 schema——面向模型的读/写/编辑形状 - -Status: implemented +# RFC:文件系统工具 schema——面向模型的读/写/编辑接口形状 [English](2026-06-17-filesystem-tool-schemas.md) | 中文 +Status: implemented + ## 问题 -[文件系统能力 seam RFC](../architecture/2026-06-17-filesystem-capability-seam.md) 定义了文件系统能力 seam(`ctx.fs`)、包拆分(`dsh-fs`、`dsh-fs-local`、`dsh-tool-fs`,加上 `dsh-fs-policy` 策略插件),以及 read-before-write/edit 检查所依赖的 observed-file/stale-version 策略——[split-fs-seam](../simplification/2026-06-26-fsspec-style-fs-seam.md) 和 [event-gate](../architecture/2026-06-26-file-context-as-event-gate.md) 两份 RFC 随后将该策略从 `ctx.fs` 移到了 `dsh-fs-policy` 插件的 `fs/*` 事件门上。第一版文件系统工具交付剩余的决策是面向模型的 schema 表面:模型在 `read`、`write` 和 `edit` 中看到哪些参数。 +[文件系统能力 seam RFC](../architecture/2026-06-17-filesystem-capability-seam.md) 定义了文件系统能力 seam(`ctx.fs`)、包(package)拆分(`dsh-fs`、`dsh-fs-local`、`dsh-tool-fs`,加上 `dsh-fs-policy` 策略插件),以及针对 read-before-write/edit 检查的 observed-file/stale-version 策略——[split-fs-seam](../simplification/2026-06-26-fsspec-style-fs-seam.md) 和 [event-gate](../architecture/2026-06-26-file-context-as-event-gate.md) RFC 后来将其从 `ctx.fs` 移至 `dsh-fs-policy` 插件的 `fs/*` 事件门上。首次文件系统工具交付剩余的决策是面向模型的 schema 接口:模型在 `read`、`write` 和 `edit` 中看到哪些参数。 -schema 应当足够小,能在 `dsh-tool-fs` 的首次实现中完成;同时又足够稳定,使未来的本地/远程/沙箱文件系统后端不会引起面向模型的接口变动。它还应避免从参考系统照搬所有选项。Claude Code 和 OpenCode 暴露了类似的核心文件工具,但在命名风格和额外 flag 上有所不同;本 RFC 为原型选择最小的共有表面。 +该 schema 应足够小,以便在 `dsh-tool-fs` 的首次实现中完成,但又足够稳定,使未来的本地/远程/沙箱文件系统后端不需要改动面向模型的接口。同时应避免从参考系统中照搬所有选项。Claude Code 和 OpenCode 暴露了类似的核心文件工具,但在命名风格和额外 flag 上有所不同;本 RFC 为原型选择最小的共有接口。 ## 决策 -`@deepseek-ai/dsh-tool-fs` 在第一版文件系统工具套件中暴露以下三个面向模型的工具: +`@deepseek-ai/dsh-tool-fs` 在首个文件系统工具套件中暴露以下三个面向模型的工具: -| Tool | 我们的 schema | Claude Code | OpenCode | 说明 | 纳入原型 | +| Tool | Our schema | Claude Code | OpenCode | Notes | Part of prototype | |---|---|---|---|---|---| -| `read` | `read(file_path, offset?, limit?)` | `Read(file_path, offset?, limit?, pages?)` | `read(filePath, offset?, limit?)` | 仅文件;`offset` 从 1 开始;首次实现不支持图片/PDF/多模态。 | 是 | -| `write` | `write(file_path, content)` | `Write(file_path, content)` | `write(content, filePath)` | 创建或覆写 UTF-8 文本。在默认 fs-policy 下,更新已有文件需要先有一次观测;新建文件则不需要。 | 是 | -| `edit` | `edit(file_path, old_string, new_string, replace_all?)` | `Edit(file_path, old_string, new_string, replace_all?)` | `edit(filePath, oldString, newString, replaceAll?)` | 字面字符串替换;默认要求唯一匹配;在默认 fs-policy 下需要先有一次观测(任何窗口化的 read 都算)。 | 是 | +| `read` | `read(file_path, offset?, limit?)` | `Read(file_path, offset?, limit?, pages?)` | `read(filePath, offset?, limit?)` | Files only; 1-indexed `offset`; no image/PDF/multimodal support in the first pass. | YES | +| `write` | `write(file_path, content)` | `Write(file_path, content)` | `write(content, filePath)` | Creates or overwrites UTF-8 text. Under the default fs-policy, updates to existing files require a prior observation; new-file creates do not. | YES | +| `edit` | `edit(file_path, old_string, new_string, replace_all?)` | `Edit(file_path, old_string, new_string, replace_all?)` | `edit(filePath, oldString, newString, replaceAll?)` | Literal string replacement; unique match required by default; under the default fs-policy requires a prior observation (any windowed read counts). | YES | schema 使用 snake_case 字段名(`file_path`、`old_string`、`new_string`、`replace_all`),与 Claude Code 及现有 DeepSeek Harness 工具 schema 示例保持一致。消费方包将这些面向模型的名称转换为 `ctx.fs` 调用和 `fs/*` 事件分发。 @@ -26,47 +26,47 @@ schema 使用 snake_case 字段名(`file_path`、`old_string`、`new_string` ### `read` -`read` 检查一个 UTF-8 文本文件并返回带行号的内容。 +`read` 检视一个 UTF-8 文本文件并返回带行号的内容。 参数: - `file_path: string`——必填。要读取的路径,由 `ctx.fs` 解析。 - `offset?: number`——可选。返回的第一行,从 1 开始。默认为第一行。 -- `limit?: number`——可选。返回的最大行数。默认值和上限是 `dsh-tool-fs` / `ctx.fs` 的实现细节。 +- `limit?: number`——可选。返回的最大行数。默认值与上限是 `dsh-tool-fs` / `ctx.fs` 的实现细节。 -首次实现的非目标: +首次实现不涉及的内容: -- 不支持 PDF `pages` 参数。 -- 不支持图片或多模态文件读取。 -- 不通过 `read` 列出目录;如有需要,目录列表将作为单独的未来工具。 +- 无 PDF `pages` 参数。 +- 无图片或多模态文件读取。 +- 不通过 `read` 列出目录;如有需要,目录列表将作为单独的后续工具。 ### `write` -`write` 创建或完全替换一个 UTF-8 文本文件。 +`write` 创建或完整替换一个 UTF-8 文本文件。 参数: - `file_path: string`——必填。要写入的路径,由 `ctx.fs` 解析。 - `content: string`——必填。要写入的完整 UTF-8 文本内容。 -在默认 fs-policy 下,用 `write` 更新已有文件需要同一执行上下文对该文件有过一次先前观测(read/write/edit);`dsh-fs-policy` 插件将观测到的版本作为 `fs/write-intent` 上的 stale guard 提供。创建新文件不需要先前观测。如果策略插件不存在,`write` 是无条件的裸提供方 create-or-overwrite。 +在默认 fs-policy 下,使用 `write` 更新已有文件需要同一执行上下文先前对该文件有过一次观测(read/write/edit);`dsh-fs-policy` 插件将观测到的版本作为 `fs/write-intent` 上的 stale guard 提供。创建新文件不需要先前观测。如果策略插件不存在,`write` 是无条件的裸提供方 create-or-overwrite。 -schema 不将 `expected_hash`、`expected_version` 或 `create_only` 暴露为面向模型的参数。stale-version 检查由后端产生的版本和策略插件的观测状态驱动,而非要求模型通过 schema 复制版本令牌。 +schema 不将 `expected_hash`、`expected_version` 或 `create_only` 作为面向模型的参数暴露。过期版本检查由后端产生的版本和策略插件的观测状态驱动,而非要求模型通过 schema 复制版本令牌。 ### `edit` -`edit` 通过替换字面文本来更新一个已有的 UTF-8 文本文件。 +`edit` 通过替换字面文本来更新已有的 UTF-8 文本文件。 参数: - `file_path: string`——必填。要编辑的路径,由 `ctx.fs` 解析。 - `old_string: string`——必填。要替换的字面文本。首次实现中空字符串无效。 -- `new_string: string`——必填。字面替换文本;空字符串表示删除匹配项。 +- `new_string: string`——必填。字面替换文本;空字符串表示删除匹配内容。 - `replace_all?: boolean`——可选。默认为 false。为 false 时,`old_string` 必须恰好匹配一处。 -`edit` 要求同一执行上下文对该文件有过一次先前观测(任何窗口化的 read 都算——授权依据是版本新鲜度,而非全文查看要求),或该上下文对该文件有过先前的 write/edit。`dsh-fs-policy` 策略插件推导所有者并将记录的版本作为 stale guard 提供;提供方的 mutation lock 强制执行。 +`edit` 要求同一执行上下文先前对该文件有过一次观测(任何窗口化的 read 都算——授权基于版本新鲜度,而非全文查看要求),或该上下文先前对该文件做过 write/edit。`dsh-fs-policy` 策略插件推导所有者并将记录的版本作为 stale guard 提供;提供方的 mutation lock 负责执行。 -首次实现拒绝 Codex 风格的 patch 语法和多模式 edit API。它使用一种严格的字面替换模式,使面向模型的契约保持简单,后端可以自行掌控精确匹配、重复匹配、行尾和 stale-version 语义。 +首次实现拒绝 Codex 风格的 patch 语法和多模式 edit API。它使用一种严格的字面替换模式,使面向模型的契约保持简单,并让后端掌控精确匹配、重复匹配、行尾和过期版本的语义。 ## 结果形状 @@ -74,17 +74,17 @@ schema 不将 `expected_hash`、`expected_version` 或 `create_only` 暴露为 默认原生投影: -| Tool | `tool-fs` 消费的结构化 `ctx.fs` 结果 | 默认模型投影 | +| Tool | Structured `ctx.fs` outcome consumed by `tool-fs` | Default model projection | |---|---|---| -| `read` | 返回的行、返回行数、总行数、目标显示路径、文件版本、部分视图标志 | 带行号的文本加分页脚注 | -| `write` | create/update 操作、目标显示路径、新文件版本 | 简洁的 create/update 成功文本 | -| `edit` | 替换次数、replace-all 标志、目标显示路径、新文件版本 | 简洁的 edit 成功文本 | +| `read` | returned lines, returned line count, total line count, target display path, file version, partial-view flag | line-numbered text plus pagination footer | +| `write` | create/update operation, target display path, new file version | concise create/update success text | +| `edit` | replacement count, replace-all flag, target display path, new file version | concise edit success text | -结构化结果不重复模型参数(如 `file_path`、`old_string` 或 `content`),除非后端已将其解析为新信息(如 `displayPath`、`targetKey` 或新版本)。token 感知的截断属于模型投影的职责,不属于后端的规范结果。 +结构化结果不会重复模型参数(如 `file_path`、`old_string` 或 `content`),除非后端已将其解析为新信息(如 `displayPath`、`targetKey` 或新版本)。面向 token 的截断属于模型投影的职责,而非后端规范结果的一部分。 -## 延后 +## 延后事项 -以下内容被明确排除在首版文件系统 schema 之外: +以下内容被明确排除在首次文件系统 schema 实现之外: - 面向模型的 `expected_hash`、`expected_version` 或 `create_only` 参数。 - 目录列表、glob、grep 和搜索工具。 @@ -95,18 +95,18 @@ schema 不将 `expected_hash`、`expected_version` 或 `create_only` 暴露为 ## 测试 -schema 测试固定每个工具的必填/可选参数集、空 `old_string` 拒绝、`replace_all` 默认值、snake_case 字段名、描述文本中对观测策略的说明,以及根插件套件注册;集成测试通过 `ctx.tools.execute()` 对真实的 `dsh-fs-local` 提供方执行全部三个工具,并验证模型参数被正确转换为预期的 `ctx.fs` 调用和 `fs/*` 分发。 +schema 测试固定每个工具的必填/可选参数集、空 `old_string` 拒绝、`replace_all` 默认值、snake_case 字段名、描述文字中对观测策略的说明,以及根插件套件注册;集成测试通过 `ctx.tools.execute()` 对真实的 `dsh-fs-local` 提供方执行全部三个工具,并验证模型参数被正确转换为预期的 `ctx.fs` 调用和 `fs/*` 分发。 ## 曾考虑的替代方案 -- **Codex 风格的 patch 语法或多模式 edit API**:否决。一种严格的字面替换模式使面向模型的契约保持简单,并让后端自行掌控精确匹配、重复匹配、行尾和 stale-version 语义。 -- **camelCase 参数名(OpenCode 风格)**:snake_case 与 Claude Code 及现有 harness 工具 schema 示例一致,且命名一旦发布即成为公开表面。 -- **面向模型的 `expected_hash` / `expected_version` / `create_only` 参数**:否决。stale 检查由后端产生的版本和策略插件的观测状态驱动,从不依赖模型复制的脆弱令牌。 +- **Codex 风格的 patch 语法或多模式 edit API**:否决。一种严格的字面替换模式使面向模型的契约保持简单,并让后端掌控精确匹配、重复匹配、行尾和过期版本的语义。 +- **camelCase 参数名(OpenCode 风格)**:snake_case 与 Claude Code 及现有 harness 工具 schema 示例一致,且命名一旦发布即成为公开接口。 +- **面向模型的 `expected_hash` / `expected_version` / `create_only` 参数**:否决。过期检查由后端产生的版本和策略插件的观测状态驱动,从不依赖模型复制的脆弱令牌。 ## 后果 -**首版 schema 有意小于 Claude Code。** 去掉 PDF pages、多模态 read、丰富的 grep/list flag 和 expected hash 字段使实现保持聚焦,但用户可能很快提出这些需求。它们将以独立 RFC 或聚焦的后续工作形式到来,而非在初始 schema 上叠加重载。 +**首版 schema 有意小于 Claude Code 的。** 去掉 PDF pages、多模态 read、丰富的 grep/list flag 和 expected hash 字段使实现保持聚焦,但用户可能很快就会提出这些需求。它们将以独立 RFC 或聚焦的后续工作形式到来,而非对初始 schema 的重载。 -**v1 没有显式的面向模型 stale guard。** schema 不要求模型提供 expected hash/version。这是有意为之:stale 检查来自后端产生的版本和 `dsh-fs-policy` 插件的观测状态,而非来自模型复制的脆弱令牌。文件系统安全失败通过 `dsh-fs` 拥有的结构化 `FsError` 代码浮现,而非通过模型提供的版本字段。 +**v1 中没有显式的面向模型的 stale guard。** schema 不要求模型提供 expected hash/version。这是有意为之:过期检查来自后端产生的版本和 `dsh-fs-policy` 插件的观测状态,而非模型复制的脆弱令牌。文件系统安全失败通过 `dsh-fs` 拥有的结构化 `FsError` 代码浮现,而非模型提供的版本字段。 -**命名成为公开表面。** 一旦发布,将 `file_path` 改为 `filePath` 或将 `old_string` 改为 `oldString` 会搅动提示词、示例和下游客户端。本 RFC 预先选定 snake_case 并将其视为稳定的面向模型契约。 +**命名成为公开接口。** 一旦发布,将 `file_path` 改为 `filePath` 或 `old_string` 改为 `oldString` 会搅动提示词、示例和下游客户端。本 RFC 预先选择 snake_case,并将其视为稳定的面向模型的契约。 diff --git a/docs/rfc/implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.i18n.yaml b/docs/rfc/implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.i18n.yaml index 7f9212b3d3..c8ce15293a 100644 --- a/docs/rfc/implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.i18n.yaml +++ b/docs/rfc/implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-18-acp-terminal-and-tool-rendering.md: cab89aa690c2068399ea5429a9c467410c744ce8 -2026-06-18-acp-terminal-and-tool-rendering.zh.md: 5a545b7a53f0814bc0e6071430c367047dc83f7f +2026-06-18-acp-terminal-and-tool-rendering.zh.md: 4047c493e63ac23f758de718616fd1f4bb29f7d4 diff --git a/docs/rfc/implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.zh.md b/docs/rfc/implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.zh.md index 5a545b7a53..4047c493e6 100644 --- a/docs/rfc/implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.zh.md +++ b/docs/rfc/implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.zh.md @@ -1,48 +1,48 @@ -# RFC:丰富的 ACP bash 渲染——通过 `_meta` 约定实现终端卡片 - -Status: implemented +# RFC:富 ACP bash 渲染——通过 `_meta` 约定实现终端卡片 [English](2026-06-18-acp-terminal-and-tool-rendering.md) | 中文 +Status: implemented + ## 问题 -ACP 桥接层允许每个工具通过 `presentCall`/`presentResult` 自行控制调用渲染(见[工具调用 UI 展示](../../implemented/feature/2026-06-14-acp-agent-client-protocol.md)与 `packages/core/tools`)。对于 `bash`,我们将确切命令作为 `tool_call` 标题呈现,模型的 `description` 作为内容文本块,`kind: 'execute'`,完成后的输出包裹在 ` ```console ` 围栏文本块中。 +ACP(Agent Client Protocol)桥接层允许每个工具通过 `presentCall`/`presentResult` 自行控制调用渲染(见 [tool-call UI presentation](../../implemented/feature/2026-06-14-acp-agent-client-protocol.md) 与 `packages/core/tools`)。对于 `bash`,我们将确切命令作为 `tool_call` 标题呈现,模型的 `description` 作为一个内容文本块,`kind: 'execute'`,完成后的输出包裹在 ` ```console ` 围栏文本块中。 -参考编辑器将终端元数据渲染为一张专用卡片,包含 cwd、命令、实时风格输出和退出状态;纯文本丢失了这些结构。命令之所以作为标题,是因为执行卡片隐藏了原始输入,而人类可读的描述保留为卡片上方的独立块。 +参考编辑器将终端元数据渲染为一张专用卡片,包含 cwd、命令、实时风格的输出和退出状态;纯文本则丢失了这些结构。命令之所以作为标题,是因为执行卡片隐藏原始输入,而人类可读的描述保留为卡片上方的独立块。 ## 关键发现:agent 执行的终端使用 `_meta` 约定,而非 `terminal/create` -ACP 规范有一个*客户端侧*终端子协议:agent 调用客户端的 `terminal/create`,传入 `{ command, args, cwd, env }`,由**编辑器**执行进程,然后 agent 读取 `terminal/output` / `wait_for_exit`。这个模型不适合我们:我们的 harness 通过 `dsh-bash` 自行执行 bash(沙箱化的环境变量清洗、后台任务所有权、按会话的 cwd)。把执行路由到编辑器会绕过所有这些机制,并将执行分裂为两个后端。 +ACP 规范有一个*客户端侧*终端子协议:agent(智能体)调用客户端的 `terminal/create`(传入 `{ command, args, cwd, env }`),由**编辑器**执行进程,然后 agent 读取 `terminal/output` / `wait_for_exit`。这个模型不适合我们:我们的 harness 通过 `dsh-bash` 自行执行 bash(沙箱化的环境清理、后台任务所有权、按会话的 cwd)。将执行路由到编辑器会绕过所有这些机制,并将执行分叉到两个后端。 研究两个参考 agent(2026-06-18)发现,二者都没有为自己的 shell 工具使用 `terminal/create`——**两者都保持 agent 侧执行,并发出一套 `_meta` 约定**,由 Zed 特殊处理: -- **`claude-agent-acp`**(`tools.ts`、`acp-agent.ts`):以 `clientCapabilities._meta.terminal_output` 为门控。`tool_call` 携带 `content: [{ type: 'terminal', terminalId }]` 和 `_meta.terminal_info.{ terminal_id, cwd }`;输出/退出通过 `tool_call_update` 的 `_meta.terminal_output.{ terminal_id, data }` 和 `_meta.terminal_exit.{ terminal_id, exit_code, signal }` 到达。 -- **`codex-acp`**(`CodexToolCallMapper.ts`、`TerminalOutputMode.ts`):调用上同样携带 `terminal_info`;输出通过 `_meta.terminal_output`(完整)或 `_meta.terminal_output_delta`(增量)发送,由同一个 `_meta.terminal_output` 能力选择。 +- **`claude-agent-acp`**(`tools.ts`、`acp-agent.ts`):以 `clientCapabilities._meta.terminal_output` 为门控。`tool_call` 携带 `content: [{ type: 'terminal', terminalId }]` 与 `_meta.terminal_info.{ terminal_id, cwd }`;输出和退出通过 `tool_call_update` 的 `_meta.terminal_output.{ terminal_id, data }` 与 `_meta.terminal_exit.{ terminal_id, exit_code, signal }` 到达。 +- **`codex-acp`**(`CodexToolCallMapper.ts`、`TerminalOutputMode.ts`):调用上同样携带 `terminal_info`;输出通过 `_meta.terminal_output`(完整)或 `_meta.terminal_output_delta`(增量),由同一个 `_meta.terminal_output` 能力选择。 -Zed 侧(`crates/agent_servers/src/acp.rs`,已验证):收到 `ToolCall` 且其 `_meta.terminal_info.terminal_id` 已设置时,注册一个**仅展示**的终端(header = `terminal_info.cwd`,label = `tool_call.title`);收到 `ToolCallUpdate` 时,`_meta.terminal_output.data` 写入该终端,`_meta.terminal_exit.{exit_code,signal}` 设置状态。它将能力声明为 `clientCapabilities._meta.terminal_output = true`。`_meta` 本身是 ACP 规范认可的扩展点(在 `ToolCall`/`ToolCallUpdate` 上类型为 `{[k]: unknown} | null`);这里的*具体键*(`terminal_info`/`terminal_output`/`terminal_exit`)是 Zed 约定,不属于 ACP 规范——但它们是 Zed 集成的事实契约,也是在保持 agent 侧执行的前提下获得终端卡片的唯一途径。 +Zed 侧(`crates/agent_servers/src/acp.rs`,已验证):收到 `ToolCall` 且其 `_meta.terminal_info.terminal_id` 已设置时,注册一个**仅展示**的终端(header = `terminal_info.cwd`,label = `tool_call.title`);收到 `ToolCallUpdate` 时,`_meta.terminal_output.data` 写入该终端,`_meta.terminal_exit.{exit_code,signal}` 设置状态。客户端通过 `clientCapabilities._meta.terminal_output = true` 声明此能力。`_meta` 本身是 ACP 规范认可的扩展点(在 `ToolCall`/`ToolCallUpdate` 上类型为 `{[k]: unknown} | null`);这里的*具体键*(`terminal_info`/`terminal_output`/`terminal_exit`)是 Zed 约定,不属于 ACP 规范,但它们是 Zed 集成的事实契约,也是在保持 agent 侧执行的前提下获得终端卡片的唯一方式。 ## 决策 保持 `dsh-bash` 的 agent 侧执行;通过 `_meta` 约定渲染终端卡片,以能力声明为门控,以 ` ```console ` 文本块作为保底回退。 1. **能力声明。** `initialize` 读取 `clientCapabilities._meta.terminal_output`,桥接层按连接记住它。 -2. **提供方无关的展示词汇。** `dsh-tools` 新增一种终端形态的展示结构,工具可以返回它——提供方无关(`cwd`、输出 `data`、`exitCode`/`signal`),不含 ACP 类型。`dsh-tool-bash` 为 `bash` 返回该结构(cwd 来自解析后的工作目录;输出 + 退出从运行结果解析)。 -3. **桥接映射。** 当客户端声明了该能力时,桥接层将展示结构映射为:在 `tool_call` 上,`content:[…, {type:'terminal', terminalId}]`(工具的任何 `content`,如描述,渲染在终端块之前)+ `_meta.terminal_info.{terminal_id,cwd}`;在 `tool_call_update` 上,`_meta.terminal_output.{terminal_id,data}`(捕获的输出)+ `_meta.terminal_exit.{terminal_id, exit_code|signal}`(解析的退出),且 update 的文本 `content` 被省略(ACP 的 `tool_call_update.content` 会**替换**调用的 content 集合,因此重发围栏块会覆盖终端内容块)。`terminalId` 由 harness 的 `callId` 派生(稳定、每次调用唯一)。当能力未声明时,桥接层在调用上发送描述内容块,在 update 上发送既有的 ` ```console ` 文本内容——行为不变。 -4. **退出标记从渲染输出中解析;无新执行路径,无实时流式传输。** 输出在完成时附加(来自 agent 自身的 `tool/result`),不逐 token 流式传输。退出状态标记(`_meta.terminal_exit.{exit_code,signal}`)会被发出:纯 `presentResult(args, result)` seam 只能看到内容块,因此 `dsh-tool-bash` 通过解析 `renderResult` 追加的状态标记(`[exit code: N]` / `[killed by signal: …]`)来恢复结构化退出——解析是标记发出的精确逆操作,二者在同一文件中共同演进,一个往返测试守护这对关系。dispose 不受影响:没有新资源需要清理,因为桥接层从未创建客户端侧终端。 +2. **提供方无关的展示词汇。** `dsh-tools` 新增一种终端形态的展示结构,工具可返回它——提供方无关(`cwd`、输出 `data`、`exitCode`/`signal`),不含 ACP 类型。`dsh-tool-bash` 为 `bash` 返回该结构(cwd 来自解析后的工作目录;输出与退出从运行结果解析)。 +3. **桥接映射。** 当客户端声明了该能力时,桥接层将展示结构映射为:在 `tool_call` 上,`content:[…, {type:'terminal', terminalId}]`(工具的任何 `content`,如描述,渲染在终端块之前)+ `_meta.terminal_info.{terminal_id,cwd}`;在 `tool_call_update` 上,`_meta.terminal_output.{terminal_id,data}`(捕获的输出)+ `_meta.terminal_exit.{terminal_id, exit_code|signal}`(解析后的退出),且 update 的文本 `content` 被省略(ACP 的 `tool_call_update.content` 会**替换**调用的 content 集合,因此重新发送围栏块会覆盖终端内容块)。`terminalId` 由 harness 的 `callId` 派生(稳定、每次调用唯一)。当能力未声明时,桥接层在调用上发送描述内容块,在 update 上发送既有的 ` ```console ` 文本内容——行为不变。 +4. **退出信息从渲染输出中解析;无新执行路径,无实时流式传输。** 输出在完成时附加(来自 agent 自身的 `tool/result`),不逐 token 流式传输。退出状态(`_meta.terminal_exit.{exit_code,signal}`)确实会发出:纯 `presentResult(args, result)` seam 只能看到内容块,因此 `dsh-tool-bash` 通过解析 `renderResult` 追加的状态标记(`[exit code: N]` / `[killed by signal: …]`)来恢复结构化退出信息——解析是标记发出的精确逆操作,二者在同一文件中共同演进,一个往返测试守护这对关系。资源释放不受影响:无需新增拆除逻辑,因为桥接层从未创建客户端侧终端。 ## 曾考虑的替代方案 -- **ACP 客户端侧终端子协议(`terminal/create`)**:明确否决。编辑器将执行进程,绕过 `dsh-bash` 的环境变量清洗、后台任务所有权和按会话的 cwd,并将执行分裂为两个后端。两个参考 agent 以同样的方式否决了它(见上述关键发现);agent 侧执行加 `_meta` 约定是在保持 harness 执行策略的同时获得终端卡片的唯一形态。 -- **通过事件 schema 透传结构化退出**:否决,改用标记往返方案。纯 `presentResult(args, result)` seam 只能看到内容块,而解析是标记发出的精确逆操作,在同一文件中共同演进并由往返测试守护。 +- **ACP 客户端侧终端子协议(`terminal/create`)**:明确否决。编辑器将执行进程,绕过 `dsh-bash` 的环境清理、后台任务所有权和按会话的 cwd,并将执行分叉到两个后端。两个参考 agent 以同样的方式否决了它(见上述关键发现);agent 侧执行加 `_meta` 约定是在保持 harness 执行策略的同时获得终端卡片的唯一形态。 +- **通过事件 schema 传递结构化退出信息**:否决,改用标记往返方案。纯 `presentResult(args, result)` seam 只能看到内容块,而解析是标记发出的精确逆操作,二者在同一文件中共同演进,由往返测试守护。 ## 后果 -- **Zed 约定的 `_meta` 键。** 终端卡片依赖 Zed 特有的键(`terminal_info`/`terminal_output`/`terminal_exit`),位于 ACP 规范认可的 `_meta` 扩展点内,而非 ACP 终端子协议。不识别这些键的客户端仍然获得文本回退(能力门控确保我们只在客户端通过 `_meta.terminal_output` 声明支持时才发出这些键),因此非 Zed 客户端永远不会变差。如果 ACP 日后标准化了 agent 执行的终端,迁移到该标准并移除约定键。 -- **能力诚实。** 仅在客户端声明了 `_meta.terminal_output` 时才发出终端元数据;文本回退是对所有其他客户端的契约,绝不能退化。由一个无能力测试覆盖,断言 ` ```console ` 路径。 +- **Zed 约定的 `_meta` 键。** 终端卡片依赖 Zed 特有的键(`terminal_info`/`terminal_output`/`terminal_exit`),位于 ACP 规范认可的 `_meta` 扩展点内,而非 ACP 终端子协议。不识别这些键的客户端仍然获得文本回退(能力门控确保我们仅在客户端通过 `_meta.terminal_output` 声明支持时才发出这些键),因此非 Zed 客户端不会变差。如果 ACP 日后标准化了 agent 执行的终端,则迁移到该标准并移除约定键。 +- **能力诚实。** 仅在客户端声明了 `_meta.terminal_output` 时才发出终端元数据;文本回退是对其他所有客户端的契约,绝不可退化。由一个无能力测试覆盖,断言 ` ```console ` 路径。 - **terminalId 冲突。** 从每次调用的 `callId` 派生,保证在会话内唯一且在 call/result 对之间稳定;绝不跨调用复用。 -- **退出从渲染文本中解析。** 退出标记通过解析 `renderResult` 的状态标记来恢复 `exit_code`/`signal`,而非通过事件 schema 透传结构化退出(纯 `presentResult` seam 看不到结构化退出)。解析是标记发出的精确逆操作,位于同一文件中;一个往返测试固定了这对关系,标记格式的变更如果破坏了解析就会使测试套件失败。如果标记将来需要与退出标记的需求分歧,改为在 result 事件上暴露结构化退出。 -- **提供方无关词汇的蔓延。** 终端展示结构扩大了 `dsh-tools` 的接口面;保持其中立性(不让 ACP 类型泄漏到 `dsh-tools`),且只提供第二个 UI 消费方也会需要的丰富度。 +- **退出信息从渲染文本解析。** 退出信息通过解析 `renderResult` 的状态标记恢复 `exit_code`/`signal`,而非通过事件 schema 传递结构化退出(纯 `presentResult` seam 看不到后者)。解析是标记发出的精确逆操作,且位于同一文件中;往返测试固定了这对关系,标记格式变更若破坏解析则测试套件失败。如果标记格式日后需要与退出信息分道扬镳,则改为在 result 事件上暴露结构化退出。 +- **提供方无关词汇的蔓延。** 终端展示结构扩大了 `dsh-tools` 的接口面;保持其中立性(不让 ACP 类型泄漏到 `dsh-tools`),且只提供第二个 UI 消费方同样需要的丰富度。 -## 不在范围内 / 非目标 +## 超出范围 / 非目标 -文本块基线仍是无能力声明时的默认行为。两个后续工作有意不在此处构建,各自需要独立 RFC:**实时增量流式传输**(`_meta.terminal_output_delta`,在分片到达时发送,需要 `dsh-bash` 上的增量输出 seam),以及**命令分类**(将 `cat`/`sed` 解析为带文件位置的 `read` 卡片、将 `grep` 解析为 `search` 等,回退到终端卡片——仅展示,绝不改变实际执行的内容)。 +文本块基线仍为无能力声明时的默认行为。以下两项后续工作有意不在此处构建,各自需要单独的 RFC:**实时增量流式传输**(在分片到达时发出 `_meta.terminal_output_delta`,需要在 `dsh-bash` 上新增增量输出 seam);**命令分类**(将 `cat`/`sed` 解析为带文件位置的 `read` 卡片,将 `grep` 解析为 `search`,回退到终端卡片——仅展示,绝不改变实际执行内容)。 diff --git a/docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.i18n.yaml b/docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.i18n.yaml index 211ad897f7..aff86591fb 100644 --- a/docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.i18n.yaml +++ b/docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-18-compaction-capability-seam.md: 31b06905924a07a7f0c2af427d8868585966f1a7 -2026-06-18-compaction-capability-seam.zh.md: ef71b3df39f02221f1bd25beb5e026cb056b95a0 +2026-06-18-compaction-capability-seam.zh.md: 1675484ed65e5cd890f420d4bdd1e16e2a95b2ef diff --git a/docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.zh.md b/docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.zh.md index ef71b3df39..1675484ed6 100644 --- a/docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.zh.md +++ b/docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.zh.md @@ -6,37 +6,37 @@ Status: implemented ## 问题 -长时间运行的 agent(智能体)对话会无限增长。随着事件日志不断累积轮次,派生出的消息历史最终逼近模型的上下文窗口——模型随即在响应中途截断(`max-tokens`)或质量退化。**压缩(compaction)**是缓解手段:用一段简洁的摘要替换一段较早的历史,保持近期上下文完整。 +长时间运行的 agent(智能体)对话会无限增长。随着事件日志不断累积轮次,派生出的消息历史最终逼近模型的上下文窗口,模型随即截断响应(`max-tokens`)或性能退化。**上下文压缩(context compaction)** 是对此的缓解手段:用一段简洁的摘要替换一批较早的历史,保持近期上下文完整。 -[会话 surface](../../implemented/architecture/2026-06-18-session-surface.md) 正是为此而建的基础设施:它是事件日志之上的链表,带有一个 `surfaceOp: { op: 'replace', start, end }` 操作,专门用于遮蔽一段节点并插入替换内容,`sourceEventSeqs` 记录来源以便决策可确定性回放。剩下的是那个*决定压缩什么、并产出摘要*的插件。 +[session surface](../../implemented/architecture/2026-06-18-session-surface.md) 正是为此而构建的基础设施:一条建立在事件日志之上的链表,带有专门设计的 `surfaceOp: { op: 'replace', start, end }` 操作,用于遮蔽一段节点并插入替换内容,`sourceEventSeqs` 记录来源以便决策可确定性地回放。剩下的是那个*决定压缩什么、并产出摘要*的插件。 -两股力量塑造了设计。第一,压缩是**可替换的**:token 计数可以是 char/4 启发式或真实 tokenizer,摘要生成可以是模型调用、模板或远程服务——这些与*何时*压缩、*压缩哪段*彼此独立变化。第二,`SurfaceEventType` 是封闭的,只有五种事件类型(`user/message`、`assistant/message`、`tool/result`、`context/message`、`steering/message`);只有它们可以携带 `surfaceOp`。因此一个专属的 `compaction/*` 事件**不能**出现在 surface 上——编译器拒绝在其上放 `surfaceOp`,invariants 插件在运行时也会拒绝。 +两股力量塑造了设计。第一,压缩是**可替换的**:token 计数可以是 char/4 启发式或真实 tokenizer,摘要生成可以是模型调用、模板或远程服务——它们独立于*何时*以及*压缩哪段范围*而变化。第二,`SurfaceEventType` 封闭为五种事件类型(`user/message`、`assistant/message`、`tool/result`、`context/message`、`steering/message`);只有这些类型可以携带 `surfaceOp`。因此一个专用的 `compaction/*` 事件**不能**出现在 surface 上——编译器拒绝在其上附加 `surfaceOp`,invariants 插件在运行时也会拒绝。 ## 决策 ### 压缩是一个能力 seam,接口与实现分离 -按照[能力 seam RFC](../../implemented/architecture/2026-06-13-capability-seams.md),压缩以独立包(package)发布,使契约、算法和(后续的)消费方 surface 各自独立演进: +遵循[能力 seam RFC](../../implemented/architecture/2026-06-13-capability-seams.md),压缩以独立包(package)发布,使契约、算法和(后续的)消费方 surface 各自独立演进: -1. **接口** — `@deepseek-ai/dsh-compact`:一个抽象的 `CompactService`,拥有 `ctx.compact` 键、`CompactionResult` 词汇以及 `compact/*` 会话事件。它将 `compactIfNeeded()` 和 `compactRegion()` 声明为**抽象方法**——契约阐述压缩*做什么*,而非*怎么做*。 -2. **实现** — `@deepseek-ai/dsh-compact-basic`:一个具体的 `BasicCompactService`,拥有完整算法——token 估算(每 token 字符数——`charsPerToken` 配置,默认 4——加逐块开销)、尾→头保留遍历、通过 `ctx.llm.stream()` 的摘要生成、surface 替换、锁,以及 `agent/pre-step` 自动压缩监听器。基于 tokenizer 或模板的后端是兄弟包(或覆写两个 protected 估算/摘要钩子的子类)。 +1. **接口** — `@deepseek-ai/dsh-compact`:抽象 `CompactService`,拥有 `ctx.compact` 键、`CompactionResult` 词汇以及 `compact/*` 会话事件。它将 `compactIfNeeded()` 和 `compactRegion()` 声明为**抽象方法**——契约说明压缩*做什么*,而非*怎么做*。 +2. **实现** — `@deepseek-ai/dsh-compact-basic`:具体的 `BasicCompactService`,拥有完整算法——token 估算(每 token 字符数,即 `charsPerToken` 配置,默认 4,加上每块开销)、尾→头保留遍历、通过 `ctx.llm.stream()` 进行摘要生成、surface 替换、锁,以及 `agent/pre-step` 自动压缩监听器。基于 tokenizer 或模板的后端是同级包(或覆盖两个 protected 估算/摘要钩子的子类)。 3. **消费方** — 推迟。一个 `/compact` 工具和斜杠命令将 `inject: ['compact']` 并调用契约;它们被有意排除在本 RFC 范围之外,以便 seam 先稳定下来。 -### 契约依赖 `dsh-session` 和 `dsh-llm`——有意的偏离 +### 契约依赖 `dsh-session` 和 `dsh-llm`——有意为之的偏离 -能力 seam RFC 规定接口包「只依赖 cordis」(对 `dsh-bash` 成立,其词汇是自包含的)。压缩**无法**遵守这一点:它的动词定义在 `Session` 之上(`compactRegion(session, start, end)`),其输出*就是*内容词汇(`CompactionResult.summary: ContentBlock[]`)。不引用 `Session`/`SessionEvent`(来自 `dsh-session`)和 `ContentBlock`(来自 `dsh-llm`),契约无法表达。 +能力 seam RFC 规定接口包"仅依赖 cordis"(对 `dsh-bash` 成立,因为其词汇是自包含的)。压缩**无法**遵守这一点:它的动词定义*在* `Session` 之上(`compactRegion(session, start, end)`),其输出*就是*内容词汇(`CompactionResult.summary: ContentBlock[]`)。不引用 `Session`/`SessionEvent`(来自 `dsh-session`)和 `ContentBlock`(来自 `dsh-llm`),契约就无法表达。 -这不是耦合异味——而是契约的领域本身。「只依赖 cordis」的指导原则本来就是「接口只依赖契约真正命名的东西,绝不依赖实现」的简写。`dsh-session` 和 `dsh-llm` 本身就是接口/词汇包,不是实现;`dsh-compact` 仍然不导入任何后端。seam 的真正不变式——*消费方和实现在抽象服务背后独立演进*——完好无损。 +这不是耦合异味,而是契约的领域所在。"仅 cordis"的指导原则一直是"接口仅依赖契约真正需要命名的东西,绝不依赖实现"的简写。`dsh-session` 和 `dsh-llm` 本身是接口/词汇包,不是实现;`dsh-compact` 仍然不导入任何后端。seam 的真正不变式——*消费方和实现在抽象服务背后独立演进*——完好无损。 -### 抽象的 `compactIfNeeded` / `compactRegion`,算法在后端 +### 抽象 `compactIfNeeded` / `compactRegion`,算法在后端 -早期草案将完整算法(保留遍历、token 求和、文本提取)作为接口上的具体方法,只有 `estimateContentTokens()` 和 `summarize()` 是抽象的。这会把契约重新耦合到一种策略:想要不同保留策略或不同事件排序的后端不得不与继承来的具体代码对抗。将两个核心方法都设为抽象,把所有*怎么做*的决策放在后端——它本该在那里——接口则保持为纯粹的*做什么*声明。后端内部仍有分层——`estimateContentTokens()` 和 `summarize()` 是 `protected` 钩子,子后端可以覆写而无需重新实现遍历——但这种分层是后端的私有关注,不是契约的。 +早期草案将完整算法(保留遍历、token 求和、文本提取)作为接口上的具体方法,仅 `estimateContentTokens()` 和 `summarize()` 为抽象。这会将契约重新耦合到一种策略:想要不同保留策略或不同事件排序的后端必须与继承来的具体代码对抗。将两个核心方法都设为抽象,把所有*怎么做*的决策放在后端——它本该在那里——并让接口保持为纯粹的*做什么*声明。后端内部仍有分层——`estimateContentTokens()` 和 `summarize()` 是 `protected` 钩子,子后端可以覆盖而无需重新实现遍历——但那是后端的私有关注点,不是契约的。 -`compactIfNeeded(agent, turn, step, fullSystemPrompt, signal)` 接受**必填**参数(而非最初的全可选形态)。自动压缩 seam(见下文)总是提供 agent、生命周期上下文、组装好的系统提示词(计入估算)以及轮次的 abort signal,因此可选性只会在 seam 处引入隐藏默认值。被压缩的会话来自 agent 上下文。`compactRegion(session, start, end, agent, turn, step, signal?)` 保留可选的 signal(手动调用方可以省略)。传递生命周期上下文而非具体模型,使路由 agent 保持诚实:后端的摘要请求可以走 `agent/request`,模型路由插件已在那里选择实际模型。 +`compactIfNeeded(agent, turn, step, fullSystemPrompt, signal)` 接收**必需**参数(而非最初的全可选形式)。自动压缩 seam(见下文)总是提供 agent、生命周期上下文、组装好的系统提示词(计入估算)和轮次的 abort signal,因此可选性只会在 seam 处引入隐藏的默认值。被压缩的会话来自 agent 上下文。`compactRegion(session, start, end, agent, turn, step, signal?)` 保留可选的 signal(手动调用方可以省略)。传递生命周期上下文而非具体模型,使路由 agent 保持诚实:后端的摘要请求可以走 `agent/request`,模型路由插件在那里已经选择了实际模型。 -### 自动压缩运行在 `agent/pre-step`,一个专用的 surface 变更 seam +### 自动压缩在 `agent/pre-step` 运行——一个专用的 surface 变更 seam -压缩会变更会话 surface,因此它在步骤开启之前、消息派生之前运行。`agent/request` 仍然是调用配置变换,永远不需要在 surface 变更后重建历史。 +压缩会变更 session surface,因此在步骤开启之前、消息派生之前运行。`agent/request` 保持为调用配置变换,无需在 surface 变更后重建历史。 解决方案是一个专用的循环 seam:**`agent/pre-step`**(`@mode serial`),由循环在系统组装*之后*、步骤开启(`step/start`)*之前*触发: @@ -48,29 +48,29 @@ messages = session.deriveMessages() ⟵ single derive, reflects the compaction request = waterfall agent/request ⟵ pure request transform (hooks, model switch) ``` -循环在 `agent/pre-step` 之后只派生一次消息。在 `step/start` 之前运行使压缩记录落在任何半开步骤之外,简化崩溃修复。该 seam 是 awaited 且 serial 的,因此 surface 变更不会交错;监听器返回 `void`,不使用 Cordis bail 值作为否决。 +循环在 `agent/pre-step` 之后派生一次消息。在 `step/start` 之前运行,使压缩记录位于任何半开步骤之外,简化崩溃修复。该 seam 是 awaited 且串行的,因此 surface 变更不会交错;监听器返回 `void`,不使用 Cordis bail 值作为否决。 ### 保留是轮次无关的;工具配对平衡是唯一的结构守卫 -自动压缩在**每个**步骤之前触发,而非每轮一次。这对**失控轮次存活至关重要**:一个工具密集的 ReAct 轮次每步追加一个 `assistant/message` + 一个 `tool/result`,surface 在*一轮之内*就会增长。单独一轮就可能超出窗口(「失控轮次」)——而在下一次模型调用溢出之前能挽救它的唯一时机,就是下一步的 `pre-step` 检查点。如果把压缩限制在轮次的第一步(或更糟,逐字保留整个进行中的轮次),就恰好重新打开了压缩存在的意义所要堵住的那个缺口:harness 会在最需要压缩的时候崩溃。 +自动压缩在**每个**步骤之前触发,而非每轮一次。这对**失控轮次存活至关重要**:工具密集型的 ReAct 轮次每步追加一个 `assistant/message` + 一个 `tool/result`,因此 surface 在*一轮之内*就会增长。单独一轮就可能超出窗口("失控轮次"),而在下一次模型调用溢出之前唯一能挽救的时机是下一步的 `pre-step` 检查点。如果将压缩限制在轮次的第一步(或者更糟,逐字保留整个进行中的轮次),恰好重新打开了压缩存在的意义所要堵住的缺口:harness 会在最需要压缩时崩溃。 -`compactIfNeeded` 保留估算大小达到 `retainTokens` 的最小尾部完整 surface 单元,压缩更早的节点。一个单元是一个完整的已关闭步骤或一条无步骤消息。如果 token 截断点落在步骤内部,保留范围会扩展直到截断处工具配对平衡。平衡按 surface 顺序检查,而非日志序列号,因为替换摘要在旧 surface 位置有新的序列号。`compactRegion` 拒绝将工具调用与其结果拆开的边界。进行中的轮次不享有特殊保留。 +`compactIfNeeded` 保留估算大小达到 `retainTokens` 的最小完整 surface 单元尾部,压缩更早的节点。一个单元是一个完整的已关闭步骤或一条无步骤消息。如果 token 截断点落在步骤内部,保留范围会扩展直到切割点满足工具配对平衡。平衡按 surface 顺序检查,而非日志序号,因为替换摘要在旧的 surface 位置拥有新的序号。`compactRegion` 拒绝将工具调用与其结果拆分的边界。进行中的轮次不享受特殊保留。 -因此失控轮次的压缩方式与任何其他历史完全相同:其早期*已关闭*步骤被摘要,近期步骤保持逐字。当唯一可压缩的内容只剩一个不可拆分的开放尾部步骤(其工具调用尚无结果)时,压缩拒绝执行(返回 `null`),待该步骤关闭后重试。 +因此失控轮次的压缩方式与其他历史完全相同:其早期*已关闭*步骤被摘要,近期步骤保持原样。当唯一可压缩的内容只剩一个不可拆分的开放尾部步骤(其工具调用尚无结果)时,压缩拒绝执行(返回 `null`)并在该步骤关闭后重试。 -**单单元溢出不在范围内,这是有意的。** 如果单个被保留的单元——一个已关闭步骤,或一个大型自由节点如粘贴的 `user/message`——*单独*超出预算,压缩无能为力,下一次模型调用可能超预算发出。限制单个单元的大小是另一个关注点(输出截断),在别处处理;压缩对此不作承诺,而没有这种机制的 harness 仍然可能在单个超大单元上崩溃。这里诚实地命名了这个边界,而非掩盖它。 +**单单元溢出不在范围内,这是有意为之。** 如果单个被保留的单元——一个已关闭步骤,或一个大型自由节点(如粘贴的 `user/message`)——*单独*超出预算,压缩无能为力,下一次模型调用可能超预算发出。限制单个单元的大小是另一个关注点(输出截断),在别处处理;压缩对此不作承诺,而没有这种机制的 harness 仍然可能在单个超大单元上崩溃。这里诚实地指出这一点,而非掩盖。 ### 头部锚定:一个自动检查点,始终在头部 -自动压缩始终从 surface 头部开始,将先前的检查点与新压缩的历史合并,使自动检查点始终只有一个。因此 `shadowedRange` 是位置性的而非数值序列区间:一个更新的摘要序列号可能占据更旧的 surface 位置。`shadowedSeqs` 记录权威的 surface 顺序。手动的中间范围压缩可能留下多个检查点。 +自动压缩始终从 surface 头部开始,将先前的检查点与新压缩的历史合并,因此只保留一个自动检查点。`shadowedRange` 因此是位置性的而非数值序号区间:一个较新的摘要序号可能占据较旧的 surface 位置。`shadowedSeqs` 记录权威的 surface 顺序。手动的中间范围压缩可能留下多个检查点。 ### 近似收敛不变式 -`resolveConfig` 校验数值参数但**不**基于假想的摘要长度不变式拒绝。收敛是动态的:提供方的输出上限可能被隐藏或外显的推理 token 消耗,模型可能输出不可预测大小的摘要。`maxTokens` 只是摘要调用的提供方侧生成上限;推理块在检查点存储前被剥离。如果压缩后的 surface 仍超阈值,`compactIfNeeded()` 最多额外重压缩头部检查点 `compactionRetries` 次,但每次提交的摘要必须小于它遮蔽的内容。唯一的残余情况是上述单单元溢出(一个向后取整的超大步骤可能把保留尾部推过预算)——这恰好是上面声明的范围外关注点,而非抖动 bug。 +`resolveConfig` 校验数值参数,但**不**基于虚构的摘要长度不变式来拒绝。收敛是动态的:提供方的输出上限可能被隐藏或显式的推理 token 消耗,模型可能生成不可预测大小的摘要。`maxTokens` 仅是摘要调用的提供方侧生成上限;推理块在检查点存储前被剥离。如果压缩后的 surface 仍超阈值,`compactIfNeeded()` 最多额外重压缩头部检查点 `compactionRetries` 次,但每次提交的摘要必须小于其遮蔽的内容。唯一的残余情况是上述单单元溢出(一个向后取整的超大步骤可能将保留尾部推过预算),这恰好是上述范围外的关注点,而非抖动 bug。 -### Surface 替换:`compact/*` 事件仅存于日志;一条 `user/message` 承载摘要 +### Surface 替换:`compact/*` 事件仅存在于日志;一条 `user/message` 承载摘要 -由于 `SurfaceEventType` 是封闭的,摘要不能搭载在 `compact/*` 事件上。后端改为追加一条**单独的 `user/message`**,带有 `surfaceOp: { op: 'replace', start, end }`,其 `content` 是(带框架的)摘要,其 `sourceEventSeqs` 覆盖被遮蔽的节点*以及*簿记事件。`compact/*` 事件是纯日志记录(锁 + 来源)。surface 变更位于锁**内部**——`compact/end` 是最后追加的事件: +由于 `SurfaceEventType` 是封闭的,摘要不能搭载在 `compact/*` 事件上。后端改为追加一条**单独的 `user/message`**,带有 `surfaceOp: { op: 'replace', start, end }`,其 `content` 是(带框架的)摘要,`sourceEventSeqs` 覆盖被遮蔽的节点*和*簿记事件。`compact/*` 事件是纯日志记录(锁 + 来源)。surface 变更位于锁**内部**——`compact/end` 是最后追加的事件: ``` compact/start → log-only. Acquires the lock. @@ -81,47 +81,47 @@ user/message → surfaceOp { op:'replace', start, end }. THE surface mutatio compact/end → log-only. Releases the lock (carries `error` on a recoverable failure). ``` -`deriveMessages()` 随后产出 `[summary_as_user_message, ...retained_nodes]`。复用 `user/message` 是诚实的而非变通:摘要确实*就是* user 角色的上下文。 +`deriveMessages()` 随后产出 `[summary_as_user_message, ...retained_nodes]`。复用 `user/message` 是诚实的而非变通:摘要确实*是* user 角色的上下文。 ### 检查点框架 + 增量合并(后端私有) -基础后端将摘要包装为已建立的检查点上下文,并标记它以便下一轮增量合并。原始摘要保留在 `compact/summary` 上。框架是后端策略;seam 只承诺一条替换 user 消息承载可能带框架的摘要。 +基础后端将摘要包装为已建立的检查点上下文,并标记以便下一轮增量合并。原始摘要保留在 `compact/summary` 上。框架是后端策略;seam 仅承诺一条替换 user 消息承载可能带框架的摘要。 -### 通过日志记录的锁实现阻塞,加上崩溃/可恢复失败分类 +### 通过日志记录的锁实现阻塞,加上崩溃/可恢复失败的分类 -`compact/start … compact/end` 括号的合理性,按实际承担的工作排序: +`compact/start … compact/end` 括号的存在理由,按当前实际承担的职责排序: -1. **可检测的崩溃孤儿 + 来源记录**(首要)。摘要生成是一次慢模型调用,在 `compact/start` *之后*持久化。摘要生成中途崩溃会留下一个没有匹配 `compact/end` 的 `compact/start`——一个可检测的孤儿。最后释放锁(而非最先释放)将崩溃窗口从*静默损坏*转化为可检测的孤儿。 -2. **防止并发压缩。** 如果当前轮次持有一个未匹配的 `compact/start`,`compactRegion` 拒绝启动。(循环在 awaited 的 `pre-step` 上是单线程的,因此这也是一个重入绊线——抛出的「already in progress」信号意味着真正的 bug。) +1. **可检测的崩溃孤儿 + 来源追溯**(首要)。摘要生成是一次慢速模型调用,持久化在 `compact/start` *之后*。摘要生成中途崩溃会留下一个没有匹配 `compact/end` 的 `compact/start`——一个可检测的孤儿。最后释放锁(而非最先)将崩溃窗口从*静默损坏*转变为可检测的孤儿。 +2. **防止并发压缩。** 如果当前轮次持有未匹配的 `compact/start`,`compactRegion` 拒绝启动。(循环在 awaited 的 `pre-step` 上是单线程的,因此这也是重入绊线——抛出"already in progress"表示真正的 bug。) 两种失败路径,均有文档记录: -- **崩溃**(循环在摘要生成中途死亡):一个悬空的 `compact/start`,没有关闭者。因为 `compact/*` 是**仅日志**事件,孤儿是**惰性的**——surface 替换从未落地,所以完整的未压缩历史正确派生。通用轮次修复(`interruptedTurnClosers`)用合成的 `turn/end` 关闭轮次;孤儿位于该 `turn/end` *之前*,因此轮次范围的进行中检查永远看不到它,崩溃不会卡住未来的压缩。压缩在下一个 `pre-step` 简单地重新尝试。 -- **可恢复**(摘要生成抛出异常但循环存活):后端追加带有 **`error`** 字段的 `compact/end`,surface 不受影响,模型调用继续使用完整历史。 +- **崩溃**(循环在摘要生成中途死亡):悬空的 `compact/start`,无关闭事件。由于 `compact/*` 是**仅日志**事件,孤儿是**惰性的**——surface 替换从未落地,因此完整的未压缩历史正确派生。通用轮次修复(`interruptedTurnClosers`)用合成的 `turn/end` 关闭轮次;孤儿位于该 `turn/end` *之前*,因此轮次范围内的进行中检查永远看不到它,崩溃不会卡住未来的压缩。压缩在下一个 `pre-step` 简单地重新尝试。 +- **可恢复**(摘要生成抛出异常但循环存活):后端追加带有 **`error`** 字段的 `compact/end`,surface 保持不变,模型调用以完整历史继续。 `compact/end` 保留其 `error?` 字段(与 `tool/result` 的自包含错误一致——一个事件即可区分成功与失败,无需关联兄弟事件)。没有单独的 `compact/error` 事件。 -**核心会话修复保持对压缩无感知——这是有意的。** `interruptedTurnClosers` 从不被教导 `compact/*`。如果教导它,每个未来的 `xxx/start … xxx/end` 插件对都必须修补核心模块——这恰好是能力 seam 架构存在的意义所要避免的耦合。因为仅日志的孤儿是惰性的,不需要特殊修复:通用轮次修复加上未落地 surface 变更的惰性就足够了。 +**核心 session 修复保持对压缩无感知——这是有意为之。** `interruptedTurnClosers` 从不被教导 `compact/*`。如果教导它,每个未来的 `xxx/start … xxx/end` 插件对都必须修补核心模块——这恰好是能力 seam 架构存在的意义所要避免的耦合。由于仅日志的孤儿是惰性的,不需要特殊修复:通用轮次修复加上未落地 surface 变更的惰性就足够了。 ## 曾考虑的替代方案 -- **完整算法作为接口上的具体方法**(只有估算/摘要是抽象的)——早期草案;否决,因为它把契约重新耦合到一种保留策略。两个核心方法都是抽象的;`protected` 的估算/摘要钩子是后端的私有分层,不是契约的。 -- **压缩运行在 `agent/request` waterfall(瀑布式事件)上**——早期方案;否决,因为它强制了双重派生,且交给监听器的上下文在结构上无法压缩。专用的 `agent/pre-step` seam 使分层在构造上正确。 +- **完整算法作为接口的具体方法**(仅估算/摘要为抽象)——早期草案;否决,因为它将契约重新耦合到一种保留策略。两个核心方法都是抽象的;`protected` 的估算/摘要钩子是后端的私有分层,不是契约的。 +- **在 `agent/request` waterfall(瀑布式事件)上执行压缩**——早期方案;否决,因为它强制双重派生,且将监听器上下文交给了结构上无法压缩的对象。专用的 `agent/pre-step` seam 从构造上使分层正确。 - **单独的 `compact/error` 事件**——否决:`compact/end` 保留 `error?` 字段,与 `tool/result` 的自包含错误一致——一个事件即可区分成功与失败,无需关联兄弟事件。 -- **教导核心轮次修复认识 `compact/*`**——否决:仅日志的孤儿是惰性的,而一个为每个未来 `xxx/start … xxx/end` 插件对打补丁的核心模块,恰好是能力 seam 架构存在的意义所要避免的耦合。 +- **教导核心轮次修复识别 `compact/*`**——否决:仅日志的孤儿是惰性的,为每个未来的 `xxx/start … xxx/end` 插件对修补核心模块恰好是能力 seam 架构存在的意义所要避免的耦合。 ## 后果 -- **新包**:`packages/compact/compact`(接口)和兄弟包 `compact-basic`(后端),位于 `packages/compact/` 下,接入根 tsconfig。消费方层推迟。 -- **新循环 seam**:`agent/pre-step`(`@mode serial`),在 `dsh-agent` 中声明,由 `dsh-agent-loop` 在系统组装之后、`step/start` 之前触发。这是循环的文档化变更——`docs/architecture.md` 记录了它,生成的 cordis catalog 携带其签名。 -- **`SessionEventMap`** 通过声明合并(merge-extensible)获得 `compact/start` / `compact/summary` / `compact/end`;`SurfaceEventType` **不受影响**。这些是会话事件而非 cordis `Events`,因此事件分类门禁无需新增条目。 -- **`dsh-session`** 获得工具配对平衡谓词(`isToolPairingBalanced`,位于 `tool-pairing.ts`,从包索引导出),`compactRegion`/`compactIfNeeded` 用它确保折叠区域不会拆开步骤的工具调用/结果对。surface 的 `replace` 操作和 surface 元数据运行时守卫已经存在,直接复用。 -- **`dsh-invariants`** 移除其 `surface replace: start must be <= end` 断言:头部锚定的压缩会将高序列号的替换节点放在更旧范围的*位置*,因此 `start > end` 在数值上是正常且有效的(范围是位置性的,由 surface 的 `indexOf` 检查验证,这些检查保持不变)。轮次包含不变式原样复用。 -- **接线**:`dsh-compact-basic` 在 `examples/coding-agent` 的 `cordis.yml` 中加载,使 seam 在真实演示中交付(此前未在任何地方加载)。 +- **新包**:`packages/compact/compact`(接口)和同级的 `compact-basic`(后端),位于 `packages/compact/` 下,接入根 tsconfig。消费方层推迟。 +- **新循环 seam**:`agent/pre-step`(`@mode serial`),在 `dsh-agent` 中声明,由 `dsh-agent-loop` 在系统组装之后、`step/start` 之前触发。这是对循环的文档化变更——`docs/architecture.md` 记录了它,生成的 cordis catalog 携带其签名。 +- **`SessionEventMap`** 通过声明合并(merge-extensible)获得 `compact/start` / `compact/summary` / `compact/end`;`SurfaceEventType` **未被**触及。这些是会话事件,不是 cordis `Events`,因此事件分类门禁无需新增条目。 +- **`dsh-session`** 获得工具配对平衡谓词(`isToolPairingBalanced`,位于 `tool-pairing.ts`,从包索引导出),`compactRegion`/`compactIfNeeded` 用它确保折叠区域不会拆分步骤的工具调用/结果对。surface 的 `replace` 操作和 surface 元数据运行时守卫已经存在并被复用。 +- **`dsh-invariants`** 移除其 `surface replace: start must be <= end` 断言:头部锚定的压缩将高序号替换节点放在较旧范围的*位置*上,因此 `start > end` 在数值上是正常且有效的(范围是位置性的,由 surface 的 `indexOf` 检查验证,这些检查保持不变)。轮次封闭不变式原样复用。 +- **接线**:`dsh-compact-basic` 在 `examples/coding-agent` 的 `cordis.yml` 中加载,使 seam 在真实演示中生效(此前它未被任何地方加载)。 ## 测试 -- **单元测试:** 真实 Loader 和 invariant 插件覆盖整单元保留、收敛失败、`compact/end` 的两种结果、头部锚定、开放尾部拒绝、惰性崩溃孤儿,以及在一个超大开放轮次内压缩已关闭步骤。 -- **循环测试:** 测试固定每步在 `turn/start` 和 `step/start` 之间有一次 awaited 的 `agent/pre-step`;在那里的 surface 变更落在步骤之外,并出现在单次派生的请求中。 -- **带密钥 e2e:** 真实模型和 bash 会话在降低限制下触发压缩,记录完整的 `compact/start…end` 对,缩小 surface,并完成任务。 -- **快照缺口:** 失控轮次压缩尚无法回放,因为摘要调用未记录 `assistant/chunk` 事件或 `sessionId`;交错的摘要调用回放仍是后续工作。 +- **单元测试:** 使用真实 Loader 和 invariant 插件覆盖完整单元保留、收敛失败、`compact/end` 的两种结果、头部锚定、开放尾部拒绝、惰性崩溃孤儿,以及在一个超大开放轮次内压缩已关闭步骤。 +- **循环测试:** 测试固定每步在 `turn/start` 与 `step/start` 之间有一次 awaited 的 `agent/pre-step`;在该处的 surface 变更落在步骤之外,并出现在单次派生的请求中。 +- **带密钥 e2e:** 真实模型和 bash 会话在降低的限制下触发压缩,记录完整的 `compact/start…end` 对,缩小 surface,并完成任务。 +- **快照缺口:** 失控轮次压缩尚无法回放,因为摘要调用未记录 `assistant/chunk` 事件或 `sessionId`;交错摘要调用的回放仍是后续工作。 diff --git a/docs/rfc/implemented/feature/2026-06-21-subagent-capability-seam.i18n.yaml b/docs/rfc/implemented/feature/2026-06-21-subagent-capability-seam.i18n.yaml index ec702a6101..1da7113f51 100644 --- a/docs/rfc/implemented/feature/2026-06-21-subagent-capability-seam.i18n.yaml +++ b/docs/rfc/implemented/feature/2026-06-21-subagent-capability-seam.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-21-subagent-capability-seam.md: 2bed84cd9166e8aa1ad5fa65b3afa44b8a842045 -2026-06-21-subagent-capability-seam.zh.md: a99d0fe894dca485452dd266752785a26815bb8a +2026-06-21-subagent-capability-seam.zh.md: a5917c14141dd06c14b4f45c5f6e4703f0eb661f diff --git a/docs/rfc/implemented/feature/2026-06-21-subagent-capability-seam.zh.md b/docs/rfc/implemented/feature/2026-06-21-subagent-capability-seam.zh.md index a99d0fe894..a5917c1414 100644 --- a/docs/rfc/implemented/feature/2026-06-21-subagent-capability-seam.zh.md +++ b/docs/rfc/implemented/feature/2026-06-21-subagent-capability-seam.zh.md @@ -1,74 +1,74 @@ # RFC:Subagent 能力 seam -Status: implemented - [English](2026-06-21-subagent-capability-seam.md) | 中文 -> 完整 seam 已交付:`dsh-subagent` 接口、`dsh-subagent-mock` 测试后端与 `dsh-tool-subagent` 消费方;两个进程内后端(`dsh-subagent-spawn`、`dsh-subagent-fork`);嵌套 agent 快照基础设施([按会话快照回放](../testing/2026-06-22-subagent-snapshot-replay.md));以及进程外 `dsh-subagent-acp` 后端([其 RFC](2026-06-22-acp-subagent-backend.md))。 +Status: implemented + +> 完整 seam 已交付:`dsh-subagent` 接口、`dsh-subagent-mock` 测试后端与 `dsh-tool-subagent` 消费方;两个进程内后端(`dsh-subagent-spawn`、`dsh-subagent-fork`);嵌套 agent 快照基础设施([逐会话快照回放](../testing/2026-06-22-subagent-snapshot-replay.md));以及进程外后端 `dsh-subagent-acp`([其 RFC](2026-06-22-acp-subagent-backend.md))。 ## 问题 -harness 有一个长期搁置的 subagent seam:一个 agent 将工作委派给另一个 agent。意图已在 `Agent`/`AgentLoop` 接口中勾勒([packages/core/agent/src/types.ts](../../../../packages/core/agent/src/types.ts)、[packages/core/agent-loop/src/index.ts](../../../../packages/core/agent-loop/src/index.ts)):创建选项引用父 agent(fork = 用父会话的事件日志为子会话播种;spawn = 全新会话),子 agent 以 `Agent` 句柄返回,使 steering(中途引导)和事件订阅统一工作。本 RFC 实现该 seam;上方横幅列出了已交付的内容。 +harness 有一个长期搁置的 seam 用于 **subagent**:一个 agent(智能体)将工作委派给另一个 agent。这一意图在 `Agent`/`AgentLoop` 接口中已有草案([packages/core/agent/src/types.ts](../../../../packages/core/agent/src/types.ts)、[packages/core/agent-loop/src/index.ts](../../../../packages/core/agent-loop/src/index.ts)):一个创建选项引用父 agent(fork = 用父会话的事件日志初始化子会话;spawn = 全新会话),子 agent 以 `Agent` 句柄返回,使 steering(中途引导)和事件订阅可以统一工作。本 RFC 实现了这个 seam;上方横幅列出了已交付的内容。 -决定整体设计走向的核心需求是:**多种 subagent 实现必须在运行时共存**。一个父 agent 可能在同一个会话中既需要一个廉价的进程内子 agent 处理有限范围的子任务,又需要一个隔离的进程外子 agent(通过 ACP)。我们预见的传输方式: +决定整体设计走向的核心需求是:**多种 subagent 实现必须在运行时共存**。一个父 agent 可能在同一个会话中既需要一个廉价的进程内子 agent 处理有限范围的子任务,又需要一个隔离的进程外子 agent(通过 ACP(Agent Client Protocol))。我们预见的传输方式: -- **进程内**:在同一个 `Context` 上创建子 `ReactLoopAgent`(最廉价,且鉴于已有的 agent 工厂几乎零成本); +- **进程内**:在同一个 `Context` 上创建子 `ReactLoopAgent`(最廉价,且鉴于现有 agent 工厂几乎零成本); - **ACP**:作为 ACP *客户端*驱动另一个 agent 进程(可以是自身的另一个实例); -- 后续:**A2A**、**Codex app-server** 与 **Claude Code Agent SDK**——每种都与 ACP 后端相同的进程外「启动子 agent、发送提示词、流式更新、取消」形态。 +- 后续:**A2A**、**Codex app-server** 与 **Claude Code Agent SDK**——每种都与 ACP 后端相同的进程外形状:「启动子 agent、发送提示词、流式接收更新、取消」。 ## 曾考虑的替代方案 -### 为什么不用 bash seam 的形态 +### 为何不采用 bash seam 的形状 -bash seam([能力 seam](../../implemented/architecture/2026-06-13-capability-seams.md))在每个 context 中只注册一个 `BashExecutor`;加载第二个会抛异常。这对 bash 是正确的(一台机器、一种执行命令的方式),但对这里是错的:共存才是需求。因此 subagent 服务是一个**命名提供方注册表**:每个实现以唯一名称注册,调用方按名称选取。这与 **LLM 适配器注册表**(`LlmService.registerAdapter`)同构,而非单服务的 bash 执行器。seam 仍然是三包结构(接口 / 实现 / 消费方);唯一不同的轴是「单实现 vs. 多实现」。 +bash seam([能力 seam](../../implemented/architecture/2026-06-13-capability-seams.md))在每个 context 中只注册恰好一个 `BashExecutor`;加载第二个会抛异常。这对 bash 是正确的(一台机器、一种执行命令的方式),但对这里是错误的:共存才是需求。因此 subagent 服务是一个**命名提供方注册表**——每个实现以唯一名称注册,调用方按名称选择——镜像 **LLM(大语言模型)适配器注册表**(`LlmService.registerAdapter`),而非单服务的 bash 执行器。seam 仍然是由三个包构成的结构(接口 / 实现 / 消费方);只是「一个 vs. 多个实现」这个维度不同。 ## 决策 -### 三包 seam +### 由三个包构成的 seam -新增包组 `packages/subagent/`: +新建包(package)组 `packages/subagent/`: | 包 | 角色 | |---|---| -| `@deepseek-ai/dsh-subagent` | 接口:`SubagentService`(`ctx.subagents`)、`SubagentProvider`、`SubagentRun`、请求/结果/能力词汇表、`subagent/*` 事件 | +| `@deepseek-ai/dsh-subagent` | 接口:`SubagentService`(`ctx.subagents`)、`SubagentProvider`、`SubagentRun`、请求/结果/能力词汇、`subagent/*` 事件 | | `@deepseek-ai/dsh-subagent-spawn` | 实现:通过 `ctx.agents.create` 创建全新的进程内子 agent | -| `@deepseek-ai/dsh-subagent-fork` | 实现:以父会话日志快照为种子的进程内子 agent | +| `@deepseek-ai/dsh-subagent-fork` | 实现:用父 agent 日志快照初始化的进程内子 agent | | `@deepseek-ai/dsh-subagent-acp` | 实现:作为 ACP 客户端驱动已配置的子进程 | -| `@deepseek-ai/dsh-subagent-mock` | 支撑:脚本化的提供方,用于通过真实加载路径测试 seam | +| `@deepseek-ai/dsh-subagent-mock` | 辅助:用于通过真实加载路径测试 seam 的脚本化提供方 | | `@deepseek-ai/dsh-tool-subagent` | 消费方:基于 `ctx.subagents` 的面向模型的 `subagent` 工具 | -### 基本原语:异步 `start → SubagentRun` +### 原语:异步 `start → SubagentRun` -提供方暴露 `start(request) → Promise`。完成后发布一个就绪的子 agent 并将其运行句柄转交给调用方。一个信号覆盖就绪前后的取消;`dispose()` 取消剩余工作并等待静默。启动失败时清理部分资源,不发出生命周期事件。`start` 是传输无关的;`spawn` 仅命名全新进程内后端。 +提供方暴露 `start(request) → Promise`。完成时发布一个就绪的子 agent 并将其运行句柄转交给调用方。一个信号覆盖就绪前后的取消;`dispose()`(资源释放)取消剩余工作并等待静止。启动失败时清理部分资源,不发出生命周期事件。`start` 与传输方式无关;`spawn` 仅指代全新的进程内后端。 ### 两类可选能力,两种发现方式 -- **启动时特性**(`outputSchema`、`depthLimit`、`toolFilter`、`persona`)挂在静态 `provider.capabilities` 描述符上。服务在委派之前检查每一项请求的特性,若提供方不支持则**大声拒绝**(`SubagentError('UNSUPPORTED_CAPABILITY')`),绝不「接受后静默忽略」。它们必须在 run 存在之前被检查,这就是为什么不能做成运行时方法。 -- **运行时特性**(通过 `sendMessage` 进行 steering、通过 `resume` 进行后续交互)是 `SubagentRun` 上的**可选方法**。方法的存在即是能力,TypeScript 窄化即是发现机制:消费方不经窄化就无法调用不存在的方法,因此不存在静默降级路径,也不需要一个单独的 flags 对象来保持同步。 +- **启动时特性**(`outputSchema`、`depthLimit`、`toolFilter`、`persona`)挂在静态的 `provider.capabilities` 描述符上。服务在委派**之前**检查每个被请求的特性,如果提供方不支持则**大声拒绝**(`SubagentError('UNSUPPORTED_CAPABILITY')`),绝不接受后静默忽略。这些特性必须在 run 存在之前检查,因此不能是运行时方法。 +- **运行时特性**(通过 `sendMessage` 进行 steering、通过 `resume` 进行后续对话)是 `SubagentRun` 上的**可选方法**。方法的存在本身即为能力,TypeScript 类型收窄即为发现机制:消费方不经收窄就无法调用不存在的方法,因此不存在静默降级路径,也不需要额外的 flags 对象来保持同步。 ### Fork 与 fresh 是独立后端,而非一个 flag -全新子 agent 和 fork 子 agent 是独立的提供方,而非请求上的 flag。`dsh-subagent-spawn` 启动隔离的子 agent;`dsh-subagent-fork` 以仅包含已完成父轮次的平衡前缀为种子。进行中的轮次被排除,因为其 subagent 调用尚无结果,无法构成有效的回放历史。 +全新子 agent 与 fork 子 agent 是独立的提供方,而非请求中的一个 flag。`dsh-subagent-spawn` 启动隔离的子 agent;`dsh-subagent-fork` 用一个平衡前缀初始化子 agent,该前缀仅包含已完成的父轮次。进行中的轮次被排除,因为其 subagent 调用尚无结果,无法构成有效的回放历史。 ### 子 agent 隔离与父日志 -每个 subagent 运行在自己的 **`Session`** 中(独立 id、`parentSession` 谱系),独立持久化。父日志仅记录 spawn 的 `tool/call` 及其 `tool/result`(子 agent 的最终输出);子 agent 的内部步骤和工具调用留在子 agent 自己的会话中,从不注入父日志。这是唯一在所有传输方式下行为一致的设计:ACP 子 agent 的内部事件物理上无法注入我们的父日志,因此让进程内行为保持一致,使 seam 保持传输无关。 +每个 subagent 运行在**自己的 `Session`** 中(独立 id、`parentSession` 谱系),独立持久化。父日志仅记录 spawn `tool/call` 及其 `tool/result`(子 agent 的最终输出)——子 agent 的内部步骤和工具调用留在子 agent 自己的会话中,绝不注入父日志。这是唯一在所有传输方式下行为一致的设计:ACP 子 agent 的内部事件在物理上无法注入我们的父日志,因此让进程内行为保持一致,使 seam 真正与传输方式无关。 -### 同步收集(第一版) +### 同步收集(首版) `dsh-tool-subagent` 将其执行信号传给 `start()`,等待子 agent 结果,并在 `finally` 中 dispose 该 run。非完成态的结果变为错误结果,而非成功的部分输出。这个前台消费方不使用 run 的可选 steering 方法。 ### 提供方选择是配置,不面向模型 -`dsh-tool-subagent` 绑定到恰好一个提供方名称(`Config.provider`);模型只看到 `{ description, prompt }`。若要暴露多种传输方式,多次加载该工具插件,每次绑定不同的提供方和不同的 `toolName`(工具注册表拒绝重名)。*服务*持有多提供方注册表;*工具*选取其中一个。本版 schema 中没有 provider/type 参数。 +`dsh-tool-subagent` 绑定到恰好一个提供方名称(`Config.provider`);模型只看到 `{ description, prompt }`。若要暴露多种传输方式,请多次加载该工具插件,每次绑定不同的提供方和不同的 `toolName`(工具注册表拒绝重名)。*服务*持有多提供方注册表;*工具*选择其中一个——本版 schema 中没有 provider/type 参数。 ## 测试 -seam 通过真实的 Cordis Loader/export 路径测试,这能捕获 [postmortem 0001](../../../postmortem/0001-acp-default-export-drops-inject.md) 中描述的 export 形状失败。注册表测试覆盖重载安全性、重名和启动时能力拒绝;嵌套 agent 场景通过[按会话快照回放](../testing/2026-06-22-subagent-snapshot-replay.md)进行无密钥回放;进程内后端还有真实循环的单元测试和带密钥的 e2e。 +seam 通过真实的 Cordis Loader/export 路径测试,这能捕获[事后分析 0001](../../../postmortem/0001-acp-default-export-drops-inject.md) 中描述的 export 形状错误。注册表测试覆盖重载安全性、重名和启动时能力拒绝;嵌套 agent 场景通过[逐会话快照回放](../testing/2026-06-22-subagent-snapshot-replay.md)进行无密钥回放;进程内后端还有真实循环的单元测试和带密钥的 e2e 测试。 ## 后果 -- **递归。** 若无限制,进程内子 agent 能看到委派工具并递归。进程内后端实现了可选的绝对深度限制和有作用域的实时全局 `toolFilter`;ACP 声明这两项能力为关闭并拒绝此类请求。[subagent 组合控制 RFC](2026-07-12-subagent-persona-tool-filter-and-depth.md) 拥有它们的确切语义和安全限制。 -- **阻塞父轮次。** 同步收集在子 agent 的整个持续期间保持父 agent 的 `runStep` 打开。这对第一版是可接受的;**后台 / 轮询 / 溢出语义推迟到未来的重新设计,该重新设计将统一 subagent 与 bash 的长时运行工具处理**(一个 sub-agent 和一个长时间运行的 `bash` 后台任务面临相同的「模型启动了一个慢操作,之后如何收集结果」问题,应共享一套机制而非各自发明)。 -- **实时进度。** 本版仅暴露生命周期事件和最终结果;逐分片的子→父更新流推迟到后台重新设计。 -- **ACP 客户端接口。** 将 ACP 子 agent 的 `fs`/`terminal` 代理回父 agent(共享工作区模式)是后续工作;第一版不声明这两项能力,子 agent 在自己的进程中自给自足。 +- **递归。** 如果不设限制,进程内子 agent 能看到委派工具并递归调用。进程内后端实现了可选的绝对深度限制和有作用域的实时全局 `toolFilter`;ACP 声明这两项能力为关闭状态,并拒绝此类请求。[subagent 组合控制 RFC](2026-07-12-subagent-persona-tool-filter-and-depth.md) 负责定义它们的确切语义和安全边界。 +- **阻塞父轮次。** 同步收集在子 agent 的整个持续时间内保持父 agent 的 `runStep` 打开。这对首版是可接受的;**后台 / 轮询 / 溢出语义推迟到未来的重新设计,该设计将统一 subagent 和 bash 的长时间运行工具处理**(一个 subagent 和一个长时间运行的 `bash` 后台任务面临相同的问题——「模型启动了一个慢操作,之后如何收集结果」——应共享一套机制,而非各自发明)。 +- **实时进度。** 本版仅暴露生命周期事件与最终结果;逐分片的子→父更新流推迟到后台重新设计时一并处理。 +- **ACP 客户端接口。** 将 ACP 子 agent 的 `fs`/`terminal` 代理回父 agent(共享工作区模式)是后续工作;首版不声明这两项能力,子 agent 在自己的进程中自行服务。 diff --git a/docs/rfc/implemented/feature/2026-06-22-acp-subagent-backend.i18n.yaml b/docs/rfc/implemented/feature/2026-06-22-acp-subagent-backend.i18n.yaml index a34398b188..2939af3939 100644 --- a/docs/rfc/implemented/feature/2026-06-22-acp-subagent-backend.i18n.yaml +++ b/docs/rfc/implemented/feature/2026-06-22-acp-subagent-backend.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-22-acp-subagent-backend.md: 7eb03ddf68f54c29524944e7b8bc801eb1724fe6 -2026-06-22-acp-subagent-backend.zh.md: 6f7b95318a2c714fea43a584ba49da00a7c12040 +2026-06-22-acp-subagent-backend.zh.md: 249f5a5ebf18d42f3d83d2159f6c2bcb52a245c3 diff --git a/docs/rfc/implemented/feature/2026-06-22-acp-subagent-backend.zh.md b/docs/rfc/implemented/feature/2026-06-22-acp-subagent-backend.zh.md index 6f7b95318a..249f5a5ebf 100644 --- a/docs/rfc/implemented/feature/2026-06-22-acp-subagent-backend.zh.md +++ b/docs/rfc/implemented/feature/2026-06-22-acp-subagent-backend.zh.md @@ -1,57 +1,57 @@ # RFC:ACP subagent 后端(进程外委派) -Status: implemented - [English](2026-06-22-acp-subagent-backend.md) | 中文 +Status: implemented + ## 问题 -subagent seam(见 [seam RFC](2026-06-21-subagent-capability-seam.md))的设计使得多个后端可以按名称共存于 `ctx.subagents` 上。进程内后端(`-spawn`/`-fork`)将子 agent 作为同一个 Cordis 上下文上的第二个 `Agent` 运行——开销低,但子 agent 与父 agent 共享进程、模型客户端和工具。seam 的核心意义正是还要支持通过协议到达的进程外子 agent,以证明这层抽象能跨越进程边界泛化。本 RFC 添加第一个此类后端:一个 ACP(Agent Client Protocol)客户端。 +subagent seam([seam RFC](2026-06-21-subagent-capability-seam.md))的设计使多个后端可以按名称共存于 `ctx.subagents`。进程内后端(`-spawn`/`-fork`)将子 agent(智能体)作为第二个 `Agent` 运行在**同一个** Cordis 上下文上:开销低,但子 agent 与父 agent 共享进程、模型客户端和工具。seam 的核心意义在于同时支持通过协议到达的**进程外**子 agent,以证明该抽象能跨越进程边界泛化。本 RFC 添加第一个此类后端:一个 ACP(Agent Client Protocol)客户端。 ## 决策 -`@deepseek-ai/dsh-subagent-acp` 注册一个 `SubagentProvider`,将每个子 agent 运行在一个**派生的子进程**中,以 ACP *客户端*身份驱动。它是现有服务端桥接 `@deepseek-ai/dsh-acp`(ACP *agent*)的方向反转孪生体:桥接**应答** `initialize`/`newSession`/`prompt`;本后端**调用**它们并**实现** `Client` 回调(`sessionUpdate`、`requestPermission`)。将配置的 spawn 命令指向 `acp-agent` 示例,即可让 harness 与自身进程对话。 +`@deepseek-ai/dsh-subagent-acp` 注册一个 `SubagentProvider`,将每个子 agent 运行在一个**派生的子进程**中,并以 ACP *客户端*身份驱动它。它是现有服务端桥接 `@deepseek-ai/dsh-acp`(ACP *agent*)的方向反转孪生体:桥接**应答** `initialize`/`newSession`/`prompt`;本后端**调用**它们并**实现** `Client` 回调(`sessionUpdate`、`requestPermission`)。将配置的 spawn 命令指向 `acp-agent` 示例,即可让 harness 与自身进程通信。 -### 每次运行启动新进程 +### 每次运行启动全新进程 -每次 `start` 都 spawn 一个新子进程,运行恰好一个 ACP 会话(`initialize` → `newSession` → `prompt`),`dispose` 杀死子进程并等待其退出。这是最简单的生命周期,与进程内「每次运行一个子 agent」的形态一致。 +每次 `start` 都 spawn 一个新的子进程,运行恰好一个 ACP 会话(`initialize` → `newSession` → `prompt`),`dispose` 杀死子进程并等待其退出。这是最简单的生命周期,与进程内「每次运行一个子 agent」的形态一致。 -### 最小客户端桩 +### 最小化客户端桩 -客户端不声明任何可选能力(无 `fs`、无 `terminal`):子 agent 在自己的进程中自行处理文件/终端访问。`session/update` 通知被消费——后端累积 `agent_message_chunk` 文本作为结果输出,在本次实现中忽略其余内容(思考、工具调用卡片),仅呈现子 agent 的最终回答。`session/request_permission` 由配置的策略自动应答(`reject` 拒绝每个提示,`allow` 通过第一个 allow 形态的选项批准)——本次实现不将任何提示呈现给人类。将 `fs`/`terminal` 代理回父进程(共享工作区模式)仍是未来工作,如 seam RFC 所述。 +客户端不声明任何可选能力(无 `fs`、无 `terminal`):子 agent 在自己的进程中自行处理文件/终端访问。`session/update` 通知被消费:后端将 `agent_message_chunk` 文本累积为结果输出,在本阶段忽略其余内容(思考、工具调用卡片),仅暴露子 agent 的最终回答。`session/request_permission` 由配置的策略自动应答(`reject` 拒绝所有提示,`allow` 通过第一个允许形态的选项批准)——本阶段不向人类暴露任何权限提示。将 `fs`/`terminal` 代理回父进程(共享工作区模式)仍为后续工作,如 seam RFC 所述。 ### 无启动时能力 -提供方的 `capabilities` 全部为 `false`。进程外子 agent 无法遵守父 agent 的 `maxDepth`(它无法访问 `parent.options.subagentDepth`)或 `toolFilter`(它拥有自己的工具注册表),且本次实现未实现 `outputSchema`。服务在 `start` 运行之前就会拒绝需要上述任何能力的请求。后端仅注入 `subagents`(而非 `ctx.agents`),并忽略 `request.parent`。 +提供方的 `capabilities` 全部为 `false`。进程外子 agent 无法遵守父 agent 的 `maxDepth`(它无权访问 `parent.options.subagentDepth`)或 `toolFilter`(它拥有自己的工具注册表),本阶段也未实现 `outputSchema`。如果请求需要其中任何一项,服务在 `start` 运行前即拒绝。后端仅注入 `subagents`(而非 `ctx.agents`),并忽略 `request.parent`。 ### StopReason 映射 -ACP `StopReason` → harness `SubagentStopReason`:`end_turn`→`completed`、`max_tokens`→`max-tokens`、`refusal`→`refusal`、`cancelled`→`aborted`、`max_turn_requests`→`error`(无对等语义——任务未完成)、未知→`error`。spawn/传输/RPC 失败解析为 `error`(如果已请求取消则为 `aborted`);按 seam 契约,`result` 永远不会因子 agent 级别的失败而 reject。 +ACP `StopReason` → harness `SubagentStopReason`:`end_turn`→`completed`、`max_tokens`→`max-tokens`、`refusal`→`refusal`、`cancelled`→`aborted`、`max_turn_requests`→`error`(无对等语义,任务未完成)、未知→`error`。spawn/传输/RPC 失败解析为 `error`(如果已请求取消则为 `aborted`);按 seam 契约,`result` 在子 agent 级别失败时从不 reject。 ### 安全:清洗子进程环境 -子 agent 是独立进程,因此会继承环境变量。凭证形态的环境变量(`/KEY|SECRET|TOKEN/i`)默认**不**转发——父 harness 自身的密钥不得隐式泄漏到派生进程中(与 bash 执行器采用的策略相同)。子 agent **自身**的凭证(它需要模型密钥)通过 `config.env` **显式**提供,在清洗之后叠加,因此有意传入的 `DEEPSEEK_API_KEY` 得以保留,而偶然存在的 `AWS_SECRET_ACCESS_KEY` 不会。子进程 stderr 继承到父进程的 stderr(诊断信息自然浮现);spawn 级别的 `error` 事件(如命令不存在时的 ENOENT)被捕获并与 ACP 驱动竞争,使错误命令解析为 `error` 而非以未处理错误崩溃父进程。 +子 agent 是独立进程,因此会继承环境变量。形如凭证的环境变量(`/KEY|SECRET|TOKEN/i`)默认**不**转发——父 harness 自身的密钥不得隐式泄露到派生进程中(与 bash 执行器采用的策略相同)。子 agent **自己**的凭证(它需要模型密钥)通过 `config.env` **显式**提供,在清洗之后叠加,因此有意传入的 `DEEPSEEK_API_KEY` 得以保留,而偶然存在的 `AWS_SECRET_ACCESS_KEY` 则不会。子进程的 stderr 继承到父进程的 stderr(诊断信息自然浮现);spawn 级别的 `error` 事件(如命令不存在时的 ENOENT)被捕获并与 ACP 驱动竞速,因此错误命令解析为 `error` 而非以未处理错误崩溃父进程。 ## 测试 -- **无需密钥的单元/集成测试:** 一个脚本化的 ACP 子进程通过真实 stdio 测试 prompt/output 流、所有 stop-reason 映射、信号与 dispose 取消(包括 pre-abort、pre-session 竞态和管道断裂场景)、两种权限策略、被忽略的非消息更新、命令缺失时的清理、提供方重载,以及命名空间导出。 -- **需要密钥的 e2e 测试:** 后端 spawn 真实的 ACP 示例;其模型回答 `PONG`、写入 `proof.txt`,父进程验证该文件。 -- **快照缺口:** 每个 ACP 子 agent 是独立进程、拥有自己的回放会话,不同于进程内的按会话回放。确定性 mock-server 覆盖已有;`TODO(acp-subagent-replay)` 跟踪父 agent 对回放中子 agent 的回放支持。 +- **无需密钥的单元/集成测试:** 一个脚本化的 ACP 子进程通过真实 stdio 测试 prompt/output 流、所有 stop-reason 映射、信号与 dispose 取消(包括 pre-abort、pre-session 竞态和管道断裂场景)、两种权限策略、被忽略的非消息更新、命令缺失时的清理、提供方重载以及命名空间导出。 +- **需要密钥的 e2e 测试:** 后端 spawn 真实的 ACP 示例;其模型回答 `PONG`,写入 `proof.txt`,父进程验证该文件。 +- **快照缺口:** 每个 ACP 子 agent 是独立进程,拥有自己的回放会话,不同于进程内的按会话回放。确定性 mock 服务器覆盖率已具备;`TODO(acp-subagent-replay)` 跟踪父进程对回放中子 agent 的回放支持。 ## 曾考虑的替代方案 ### 为何继续使用 SDK 0.25.1? -后端仅需 `ClientSideConnection`、`ndJsonStream`、`PROTOCOL_VERSION` 和客户端协议类型,0.25.1 均已支持。0.28 的 fluent API 需要在 ACP 层同时迁移客户端和服务端连接类,但不会改善本后端,因此升级作为独立变更保留。 +后端只需要 `ClientSideConnection`、`ndJsonStream`、`PROTOCOL_VERSION` 和客户端协议类型,0.25.1 全部支持。0.28 的 fluent API 需要在 ACP 层同时迁移客户端和服务端连接类,却不会改善本后端,因此升级作为独立变更保留。 ### 为何不使用持久子进程? -持久进程池(跨运行复用热子进程)是一项性能优化,推迟到未来工作——它引入会话生命周期和崩溃恢复的复杂性,本次实现不需要;每次 `start` spawn 新子进程与进程内「每次运行一个子 agent」的形态一致。 +持久进程池(跨运行复用热子进程)是一项性能优化,推迟到后续工作。它增加了会话生命周期和崩溃恢复的复杂度,本阶段不需要;每次 `start` spawn 全新子进程与进程内「每次运行一个子 agent」的形态一致。 ## 后果 -每次运行都要付出一个新子进程的开销(spawn + `initialize` + `newSession`)。父 agent 仅呈现子 agent 的最终回答:`session/update` 中的思考和工具调用卡片被消费后丢弃,权限提示永远不会到达人类——由配置的策略应答。子进程环境默认经过凭证清洗,因此其自身的模型密钥须通过 `config.env` 显式提供。 +每次运行都要付出一个全新子进程的代价(spawn + `initialize` + `newSession`)。父进程仅暴露子 agent 的最终回答:`session/update` 中的思考和工具调用卡片被消费后丢弃,权限提示从不到达人类——由配置的策略应答。子进程环境默认经过凭证清洗,因此其自身的模型密钥需通过 `config.env` 显式提供。 -## 未来提供方 +## 后续提供方 同样的进程外 spawn/prompt/stream/cancel 形态可泛化到 seam RFC 中列出的其他传输方式——A2A、Codex app-server 和 Claude Code Agent SDK——每个都是按名称注册的兄弟提供方。ACP 后端证明了 seam 支持跨进程边界;其余在机制上类似。 diff --git a/docs/rfc/implemented/feature/2026-06-25-ask-user-question.i18n.yaml b/docs/rfc/implemented/feature/2026-06-25-ask-user-question.i18n.yaml index c46a38e46f..30f57427e2 100644 --- a/docs/rfc/implemented/feature/2026-06-25-ask-user-question.i18n.yaml +++ b/docs/rfc/implemented/feature/2026-06-25-ask-user-question.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-25-ask-user-question.md: 06673233038d10214f8de3d1f29766d43b575442 -2026-06-25-ask-user-question.zh.md: 01d1284dba3622984d5403f94e3edd0ba02583b6 +2026-06-25-ask-user-question.zh.md: a036220fd54e3f634ab4be80a45964b076d3fd2d diff --git a/docs/rfc/implemented/feature/2026-06-25-ask-user-question.zh.md b/docs/rfc/implemented/feature/2026-06-25-ask-user-question.zh.md index 01d1284dba..a036220fd5 100644 --- a/docs/rfc/implemented/feature/2026-06-25-ask-user-question.zh.md +++ b/docs/rfc/implemented/feature/2026-06-25-ask-user-question.zh.md @@ -1,51 +1,51 @@ # RFC:ask-user 提问能力 -Status: implemented - [English](2026-06-25-ask-user-question.md) | 中文 +Status: implemented + ## 问题 -agent(智能体)有时仅凭模型推理(inference)无法安全地继续:它需要人类选择路径、确认有风险或默认的操作,或提供缺失的信息。在此变更之前,获取答案的唯一方式是模型在 assistant 文本中提问然后停止,这会打断正常的工具调用循环:agent 没有结构化的暂停手段,没有供 UI 使用的选项元数据,没有中止/错误分类体系,也没有让非 stdio 前端一致地呈现问题的方式。 +agent(智能体)有时仅凭模型推理(inference)无法安全地继续执行:它需要人类选择路径、确认有风险的或默认的操作,或者提供缺失的信息。在此变更之前,获取答案的唯一方式是模型在 assistant 文本中提问然后停止,这打断了正常的工具调用循环:agent 没有结构化的暂停方式,没有供 UI 使用的选项元数据,没有中止/错误分类体系,也没有让非 stdio 前端一致地呈现问题的途径。 -这是一个面向用户的能力,但它也跨越了包(package)边界。模型侧的工具需要一套提供方无关的请求词汇;每个 UI 表面需要决定如何展示和收集答案;agent loop(智能体循环)应保持不变,因为工具调用本身已具备正确的异步形态。 +这是一个面向用户的能力,但它也跨越了包(package)边界。面向模型的工具需要一套提供方无关的请求词汇;每个 UI 界面需要决定如何展示和收集答案;agent loop(智能体循环)应保持不变,因为工具调用本身已具备正确的异步形状。 ## 决策 -引入 `dsh-user-interaction` 作为 `ctx.userInteraction` 的提供方无关接口包,与模型侧消费方 `dsh-tool-ask-user` 一同放在 `packages/ui` 下。这一分组是有意为之:向人类提问是一种由 UI 支撑的产品能力,不属于无提供方的核心主干。seam 仍然拥有稳定的请求/应答/错误词汇,而 UI 产品表面提供收集答案的具体 provider。工具注册 `ask_user_question`,转发 `{ questions, agent, signal }`,并将 provider 计算出的结构化答案作为工具结果返回。 +引入 `dsh-user-interaction` 作为 `ctx.userInteraction` 的提供方无关接口包,与面向模型的消费方 `dsh-tool-ask-user` 一同放在 `packages/ui` 下。这一分组是有意为之的:向人类提问是一种由 UI 支撑的产品功能,不属于无提供方的核心主干。seam 仍然拥有稳定的请求/应答/错误词汇,而 UI 产品界面提供收集答案的具体 provider。该工具注册 `ask_user_question`,转发 `{ questions, agent, signal }`,并将 provider 计算出的结构化答案作为工具结果返回。 -模型侧的请求词汇有意与产品研究 schema 对齐:`ask_user_question({ questions: [{ id, question, header?, options?: [{ label, description? }], multi_select? }] })`。`id` 按问题提供并在结果中回传,使批量请求可以路由而不依赖问题文本。`label` 既是面向用户的显示文本,也是返回给模型的选中值;没有单独的 `value`,没有 `recommended`,没有 `allow_custom`,也没有 `desc` 别名。 +面向模型的请求词汇有意与产品调研 schema 对齐:`ask_user_question({ questions: [{ id, question, header?, options?: [{ label, description? }], multi_select? }] })`。`id` 按问题提供并在结果中回传,使批量请求无需依赖问题文本即可路由。`label` 既是面向用户的显示文本,也是返回给模型的选中值;没有单独的 `value`,没有 `recommended`,没有 `allow_custom`,也没有 `desc` 别名。 -provider 返回 `{ answers: [{ id, selected, custom? }] }`。`selected` 始终是选中选项 label 的数组,因此单选和 `multi_select` 的答案共享同一种结果形态。`custom` 承载自由文本的「其他」答案;无选项的问题直接收集 `custom`。当 `custom` 存在时,它覆盖所有已选选项,`selected` 为空。 +Provider 返回 `{ answers: [{ id, selected, custom? }] }`。`selected` 始终是选中选项 label 的数组,因此单选和 `multi_select` 的答案共享同一种结果形状。`custom` 承载自由文本的「其他」答案;无选项的问题直接收集 `custom`。当 `custom` 存在时,它覆盖任何已选择的选项,`selected` 为空。 -`UserInteractionError` 继承 `HarnessError`,因此 `NO_PROVIDER`、`ASK_ABORTED`、ACP 取消或会话路由缺失等失败会以可机器路由的 `{ name, code }` 工具错误形式通过 `ctx.tools.execute()` 传出。这与结构化错误分类体系一致,使模型或包装插件能区分「用户取消」与通用抛出异常。 +`UserInteractionError` 继承 `HarnessError`,因此 `NO_PROVIDER`、`ASK_ABORTED`、ACP(Agent Client Protocol)取消或会话路由缺失等失败会以机器可路由的 `{ name, code }` 工具错误形式通过 `ctx.tools.execute()` 传出。这与结构化错误分类体系一致,使模型或包装插件能够区分「用户取消」与一般的抛出异常。 ## UI 映射 -`dsh-stdio-demo` 的包内 readline 模块逐题渲染每个问题,在下一行展示每个选项的 `description`,支持以逗号/空格分隔的数字选择 `multi_select`,接受自由格式的自定义答案,并在中止、provider dispose(资源释放)或 stdin EOF 时拒绝待处理的问题。批量请求按顺序逐题询问,合并为一个答案对象返回。stdio provider 通过内部队列序列化并发请求,确保同一时刻只有一个 prompt 占用 stdin。 +`dsh-stdio-demo` 的包内 readline 模块渲染每个问题,在下一行显示每个选项的 `description`,支持以逗号/空格分隔的数字选择 `multi_select`,接受自由格式的自定义答案,并在中止、provider dispose(资源释放)或 stdin EOF 时拒绝待处理的问题。批量请求按顺序询问,作为一个答案对象整体解析。stdio provider 通过内部队列序列化并发请求,确保同一时刻只有一个提示占用 stdin。 -`dsh-acp` 为 ACP(Agent Client Protocol)会话提供同一 seam。它通过 bridge 的 `agent→sessionId` 反向映射将调用方 `Agent` 的 ask 请求路由到对应会话,并为每个问题调用 ACP `unstable_createElicitation`(携带会话作用域的表单)。单选选项变为 `choice` 字符串枚举;`multi_select` 选项变为 `choice` 数组枚举;无选项问题使用必填的 `custom` 文本字段。如果客户端同时返回 `choice` 和非空 `custom`,以 custom 答案为准。ACP `decline`/`cancel`、缺失答案、缺失会话以及客户端不支持 elicitation 的情况都会变为结构化的 `UserInteractionError`。 +`dsh-acp` 为 ACP 会话提供同一 seam。它通过 bridge 的 `agent→sessionId` 反向映射将调用方 `Agent` 的 ask 请求路由出去,并为每个问题调用 ACP `unstable_createElicitation`(附带会话范围的表单)。单选选项变为 `choice` 字符串枚举;`multi_select` 选项变为 `choice` 数组枚举;无选项的问题使用必填的 `custom` 文本字段。如果客户端同时返回 `choice` 和非空 `custom`,以 custom 答案为准。ACP `decline`/`cancel`、缺失答案、缺失会话以及客户端不支持 elicitation,都会转为结构化的 `UserInteractionError`。 ACP 映射有意使用 elicitation 而非 `session/request_permission`。`request_permission` 仍保留给独立的权限门禁:它是围绕工具执行的 yes/no 或策略式授权协议。`ask_user_question` 是一个通用的信息收集工具,支持可选的自由格式答案,因此 ACP 表单 elicitation 是更贴合的协议。bridge 的会话路由与未来的权限门禁共享,但用户意图不同。 ## 曾考虑的替代方案 -**Assistant 文本后跟一个停止的轮次。** 模型可以在纯 assistant 文本中向用户提问然后停止。这会丢失结构化的选项元数据,UI 没有提供方无关的方式来渲染选择,且下一条人类回答只能作为新的 user prompt 到达,而非作为需要答案的那次操作的结果。 +**Assistant 文本后跟一个停止的轮次。** 模型可以在纯 assistant 文本中向用户提问然后停止。这会丢失结构化选项元数据,UI 没有提供方无关的方式来渲染选择,且下一条人类回答只能作为新的 user prompt 到达,而非作为需要答案的那次操作的结果。 -**核心包拥有 ask-user 相关包。** 最初实现将 seam 和模型侧工具分别放在 `packages/core` 和 `packages/ui`,但两者描述的是同一个由 UI 支撑的人机交互能力。seam 仍然是提供方无关的,但它不是像会话、工具或 agent 注册表那样的无提供方核心基础设施。将 `dsh-user-interaction` 和 `dsh-tool-ask-user` 一起放在 `packages/ui` 下,使包结构与产品边界一致:应用和 bridge 提供人类答案的 provider,stdio 应用选择性加载模型侧工具。 +**核心拥有的 ask-user 包。** 最初实现将 seam 和面向模型的工具分别放在 `packages/core` 和 `packages/ui`,但两者描述的是同一个由 UI 支撑的人机交互功能。seam 仍然是提供方无关的,但它不是像会话、工具或 agent 注册表那样的无提供方核心基础设施。将 `dsh-user-interaction` 和 `dsh-tool-ask-user` 一起放在 `packages/ui` 下,使包的划分与产品边界一致:应用和 bridge 提供人类答案的 provider,stdio 应用选择性加载面向模型的工具。 **ACP `session/request_permission`。** 权限请求是围绕工具执行的授权;`ask_user_question` 是带可选自由格式答案的信息收集。将权限用于通用提问会混淆两个不同的产品概念,并使未来的权限门禁更难推理。 -**循环级别的暂停原语。** agent loop 已经知道如何等待工具调用并从工具结果恢复。新增一个循环特例会重复这一异步形态,并迫使每个循环实现都了解一个 UI 关注点。 +**循环级别的暂停原语。** agent loop 已经知道如何等待工具调用并从工具结果恢复。添加新的循环特殊分支会重复这一异步形状,并迫使每个循环实现都了解一个 UI 关注点。 ## 后果 -ACP elicitation 目前在 SDK 中标记为 unstable。回退仍然是结构化的:如果客户端未实现它,工具返回 `ASK_FAILED` 而非挂起。后续 ACP 稳定化可能重命名或重塑该方法;该迁移应留在 `dsh-acp` 内部,因为核心 `ctx.userInteraction` 词汇是提供方无关的。 +ACP elicitation 目前在 SDK 中标记为 unstable。回退仍然是结构化的:如果客户端未实现它,工具返回 `ASK_FAILED` 而非挂起。后续 ACP 稳定化可能重命名或重塑该方法;该迁移应限制在 `dsh-acp` 内部,因为核心 `ctx.userInteraction` 词汇是提供方无关的。 -该特性赋予模型一个强大的暂停原语,因此提示词引导很重要。工具描述告诉模型提问要简洁、尽可能使用选项。产品策略后续可以包装 `tools/execute` 来限制工具何时可用,但循环不应对其做特殊处理。 +该功能赋予模型一个强大的暂停原语,因此 prompt 引导很重要。工具描述告诉模型:提问要简洁,尽可能使用选项。产品策略后续可以包装 `tools/execute` 来限制工具何时可用,但循环不应对其做特殊处理。 -`dsh-user-interaction` 和 `dsh-tool-ask-user` 都位于 `packages/ui`,因为它们共同构成一个面向产品的人机交互能力。`agent-core` 不加载工具或 provider。`stdio-agent` 选择性加载 seam、其 readline provider 和模型侧工具。`acp-agent` 默认只保留 `userInteraction` seam/provider:ACP elicitation 支持仍取决于客户端,因此 ACP 叶子节点必须在其客户端能够完成 elicitation 请求后才有意加载模型侧工具。 +`dsh-user-interaction` 和 `dsh-tool-ask-user` 都位于 `packages/ui`,因为它们共同构成一个面向产品的人机交互能力。`agent-core` 不加载工具或 provider。`stdio-agent` 选择性加载 seam、其 readline provider 和面向模型的工具。`acp-agent` 默认只保留 `userInteraction` seam/provider:ACP elicitation 支持仍取决于客户端,因此 ACP 叶节点必须在其客户端能完成 elicitation 请求后才有意加载面向模型的工具。 ## 测试 -单元覆盖率固定了以下场景:provider 注册/释放、重复 provider 拒绝、provider 就绪前中止、空问题拒绝、通过 `ctx.tools.execute()` 的结构化工具错误、批量答案、多选答案、自定义答案,以及模型 schema(包括移除 `value`、`recommended`、`allow_custom` 和 `desc` 的验证)。`dsh-stdio-demo` 测试覆盖选项描述、排队请求、EOF/中止清理、无选项自由格式输入、无效选项重新提示、重复多选编号和批量问题流程。ACP bridge 测试驱动一个真实的内存 ACP 连接(使用真实的 `ask_user_question` 工具),验证选中选项、custom 覆盖 choice、多选和无选项自由格式 elicitation 路径能继续 agent loop。 +单元覆盖率固定了以下场景:provider 注册/释放、重复 provider 拒绝、provider 就绪前中止、空问题拒绝、通过 `ctx.tools.execute()` 传出的结构化工具错误、批量答案、多选答案、自定义答案,以及模型 schema(包括移除 `value`、`recommended`、`allow_custom` 和 `desc`)。`dsh-stdio-demo` 测试覆盖选项描述、排队请求、EOF/中止清理、无选项自由格式输入、无效选项重新提示、重复多选编号和批量问题流。ACP bridge 测试驱动一个真实的内存 ACP 连接(使用真实的 `ask_user_question` 工具),验证选中选项、custom 覆盖 choice、多选和无选项自由格式 elicitation 路径能继续 agent loop。 diff --git a/docs/rfc/implemented/feature/2026-06-29-todo-write-tool.i18n.yaml b/docs/rfc/implemented/feature/2026-06-29-todo-write-tool.i18n.yaml index d2c28a3946..677df03ea6 100644 --- a/docs/rfc/implemented/feature/2026-06-29-todo-write-tool.i18n.yaml +++ b/docs/rfc/implemented/feature/2026-06-29-todo-write-tool.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-29-todo-write-tool.md: 69f81cf6fd93df63ce53bb82c97dbac16dbbd486 -2026-06-29-todo-write-tool.zh.md: a687f5ab4bc5b9fcd5583ca4aac2857ab4c3f513 +2026-06-29-todo-write-tool.zh.md: eb3c6fb8a9ddc7d26e4a620761c96a8d35ddf469 diff --git a/docs/rfc/implemented/feature/2026-06-29-todo-write-tool.zh.md b/docs/rfc/implemented/feature/2026-06-29-todo-write-tool.zh.md index a687f5ab4b..eb3c6fb8a9 100644 --- a/docs/rfc/implemented/feature/2026-06-29-todo-write-tool.zh.md +++ b/docs/rfc/implemented/feature/2026-06-29-todo-write-tool.zh.md @@ -1,4 +1,4 @@ -# RFC:`todo_write` 工具——将模型任务列表建模为事件溯源的会话状态 +# RFC:`todo_write` 工具——将模型任务列表作为事件溯源的会话状态 Status: implemented @@ -6,59 +6,59 @@ Status: implemented ## 问题 -harness 为模型提供了 bash 和 subagent 工具,但没有任何方式记录结构化的任务列表。todo 列表服务于两个同等重要的目的:引导模型规划多步骤工作并保持当前任务明确(最多一个 in_progress,有未完成工作时恰好一个),以及为人类提供实时进度清单。ACP(Agent Client Protocol)协议有原生的 `plan` sessionUpdate,编辑器(Zed)已经在渲染它,但 bridge 从未发出过。调研的每个参考编码 agent(智能体)实现(claude-code、opencode、codex、oh-my-pi、pi)都提供了某种形式的此功能;而 harness 什么都没有。 +harness 为模型提供了 bash 和 subagent 工具,却没有办法记录结构化的任务列表。todo 列表有两个同等重要的用途:引导模型规划多步骤工作并保持当前活跃任务明确(最多一个活跃,有剩余工作时恰好一个);同时为人类提供实时进度清单。ACP(Agent Client Protocol)协议原生支持 `plan` sessionUpdate,编辑器(Zed)已能渲染它,但 bridge 从未发出过。调研的所有参考编码 agent(智能体)(claude-code、opencode、codex、oh-my-pi、pi)都提供了某种形式的此功能;本 harness 此前没有。 ## 决策 -新增一个面向模型的 `todo_write(todos: [{ content, status }])` 工具,其全量列表状态以新的 `todo/write` `SessionEventMap` 变体存在于事件溯源的会话日志上。stdio UI 和 ACP bridge 都从既有的 `session/event` 渲染——ACP bridge 将列表映射为 `plan` sessionUpdate。 +新增一个面向模型的 `todo_write(todos: [{ content, status }])` 工具,其整列表状态作为新的 `todo/write` `SessionEventMap` 变体存储在事件溯源的会话日志上。stdio UI 和 ACP bridge 均从现有的 `session/event` 渲染;ACP bridge 将列表映射为 `plan` sessionUpdate。 -### 全量替换,三态 status +### 整列表替换,三态 status -模型每次调用发送**完整**列表;新列表替换旧列表(回放时 last-write-wins)。这是 claude-code V1、opencode 和 codex `update_plan` 共同使用的形态,也是模型训练最多的形态——没有逐项 id,没有 delta 协议。`status` 恰好是 `pending | in_progress | completed`:与 codex `update_plan` 相同的三元组,且关键的是**与 ACP `PlanEntryStatus` 完全一致**,因此 bridge 做 1:1 映射,无损失转换。 +模型每次调用发送**完整**列表;新列表替换旧列表(回放时 last-write-wins)。这是 claude-code V1、opencode 和 codex `update_plan` 共同采用的形状,也是模型训练最多的形状——没有逐项 id,没有 delta 协议。`status` 恰好是 `pending | in_progress | completed`:与 codex `update_plan` 相同的三元组,且关键的是**与 ACP `PlanEntryStatus` 完全一致**,bridge 因此可以 1:1 映射,无需有损转换。 ### 状态在会话日志上,而非服务 -列表以 `todo/write` 事件追加,携带完整的 `{ todos }` 快照。harness 是事件溯源的——LLM(大语言模型)历史、工具调用和轮次结构都在日志上——所以 todo 列表也在那里。这免费获得了持久性、回放和 `session/load` 重建:重新打开的会话从最后一条 `todo/write` 重新推导当前列表,ACP bridge 在加载时重新发出 `plan`,无需独立的持久化后端、无需重新注水的内存服务、无需额外接线。一个内存中的 `ctx.todos` 服务需要重新发明所有这些。 +列表作为 `todo/write` 事件追加到日志,携带完整的 `{ todos }` 快照。harness 是事件溯源的——LLM(大语言模型)历史、工具调用和轮次结构都在日志上——所以 todo 列表也在那里。这免费获得了持久性、回放和 `session/load` 重建:重新打开的会话从最后一条 `todo/write` 重新推导当前列表,ACP bridge 在加载时重新发出 `plan`,无需独立的持久化后端、无需重新注水的内存服务、无需额外接线。一个内存中的 `ctx.todos` 服务需要重新发明以上所有。 ### 不是 surface 事件 -`todo/write` 被刻意排除在 `SurfaceEventType` 之外。surface 是产出 LLM 消息历史(`deriveMessages()`)的投影;一次 todo write 不产生对话消息。因此它不携带 `surfaceOp`,不加入 surface 链表,不进入 `deriveMessages()`——它是持久的、可回放的 *UI* 状态,伴随对话传播但不属于对话的一部分。(开发模式的不变式仍要求它位于一个打开的轮次内,事实也确实如此:它在工具调用的 mid-step 阶段追加。) +`todo/write` 被有意排除在 `SurfaceEventType` 之外。surface 是产出 LLM 消息历史(`deriveMessages()`)的投影;todo write 不产生对话消息。因此它不携带 `surfaceOp`,不加入 surface 链表,不进入 `deriveMessages()`——它是持久、可回放的 *UI* 状态,与对话并行传输但不属于对话的一部分。(dev-mode 不变式仍要求它位于一个打开的轮次内,而它始终如此:它在工具调用的步骤中途追加。) -### priority 仅在 ACP 边界合成 +### Priority 仅在 ACP 边界合成 -ACP 的 `PlanEntry` 要求 `content` + `priority` + `status`,但 `TodoItem` 没有 priority——模型从不推理它。与其在 schema 中增加一个模型每次都必须提供的字段,不如让 bridge 在构建 `plan` 时为每个条目合成一个常量 `priority: 'medium'`。priority 是 ACP 协议格式(wire format)的要求,不是 harness 的概念,因此它恰好存在于需要它的边界处。 +ACP 的 `PlanEntry` 要求 `content` + `priority` + `status`,但 `TodoItem` 没有 priority——模型从不推理它。与其在 schema 中增加一个模型每次都必须提供的字段,bridge 在构建 `plan` 时为每条条目合成常量 `priority: 'medium'`。Priority 是 ACP 协议格式(wire format)的要求,不是 harness 概念,因此它恰好存在于需要它的边界上。 -### 相比 claude-code V1 去掉的字段:`activeForm`、id、priority +### 相比 claude-code V1 舍弃的字段:`activeForm`、id、priority -claude-code V1 的 item 是 `{ content, status, activeForm }`;后来(V2)增加了 id、依赖和所有权——但那只是为了支持 agent *集群*(磁盘持久化、锁保护、逐项变更)。本工具将 item 保持在最小集:`{ content, status }`。没有 `activeForm`(现在进行时标签)——UI 直接展示 `content`;没有 id——全量替换不需要稳定标识;没有 priority——见上文。每去掉一个字段,模型每次调用就少产出一项。 +claude-code V1 的条目是 `{ content, status, activeForm }`;后来(V2)增加了 id、依赖和所有权——但仅为支持 agent *集群*(磁盘持久、锁保护、逐项变更)。本工具将条目保持在最小集:`{ content, status }`。不要 `activeForm`(现在进行时标签)——UI 直接展示 `content`;不要 id——整列表替换不需要稳定标识;不要 priority——见上文。每舍弃一个字段,模型每次调用就少产出一项。 ### 单一所有者——无集群机制(YAGNI) -每个列表属于调用方 agent 会话,非 agent 调用会被拒绝。没有共享作用域、resolver 或 delta 协议。跨 agent 列表需要逐项日志 delta 和显式作用域选择,因此留作未来独立设计。 +每个列表属于调用它的 agent 会话,非 agent 调用被拒绝。没有共享作用域、resolver 或 delta 协议。跨 agent 列表需要逐项日志 delta 和显式作用域选择,因此留作未来独立设计。 ### 校验:低成本的中间路线 -schema 强制 type/required/enum。在此之上,`execute` 拒绝空 `content`、重复 `content` 以及多于一个 `in_progress` 任务。claude-code 将 single-in-progress 留给 prompt;oh-my-pi 在代码中强制。我们取中间路线:强制那些使计划*连贯*的低成本不变式(无空白任务、无重复、最多一个活跃),但将排序和保持列表最新的纪律通过工具描述留给模型。被拒绝的写入返回 `isError` 结果,模型可自行修正。 +schema 强制 type/required/enum。在此之上,`execute` 拒绝空 `content`、重复 `content`,以及超过一个 `in_progress` 任务。claude-code 将单一 in_progress 交给 prompt 约束;oh-my-pi 在代码中强制。我们取中间路线:强制执行使计划*连贯*的低成本不变式(无空任务、无重复、最多一个活跃),但将排序和保持列表最新的纪律通过工具描述交给模型。被拒绝的写入返回 `isError` 结果,使模型自行修正。 -## 为什么没有 cordis-catalog 条目 / 没有 `@mode` +## 为何没有 cordis-catalog 条目 / 没有 `@mode` -`todo/write` 是 `SessionEventMap` 的成员,不是一等的 cordis `interface Events` 事件。catalog 生成器(`scripts/gen-cordis-catalog.ts`)扫描 `interface Events` 声明;`SessionEventMap` 变体搭载既有的 `session/event` emit,不产生新的 catalog 行。因此它不携带 `@mode` 标签(生成器仅对 `interface Events` 成员要求此标签)——加上它也没有意义。 +`todo/write` 是 `SessionEventMap` 的成员,不是一等的 cordis `interface Events` 事件。catalog 生成器(`scripts/gen-cordis-catalog.ts`)扫描 `interface Events` 声明;`SessionEventMap` 变体搭载现有的 `session/event` emit,不产生新的 catalog 行。因此它不携带 `@mode` 标签(生成器仅对 `interface Events` 成员要求该标签)——添加一个毫无意义。 ## 测试 -四层,预先设计: -- **单元测试**——会话事件(append/snapshot-clone/last-write-wins/not-on-surface);工具(schema 形状、通过真实 `ctx.tools.execute` 的参数校验、值校验、事件追加与替换、非 agent 拒绝、`presentCall`、HMR 安全性);ACP `todosToPlan` 映射;stdio 渲染分支。 -- **真实 Loader 路径**——插件通过 `Loader.unwrapExports` 运行,断言命名空间导出形状存活(它有 `inject`,因此一个意外的 default 导出会在加载时崩溃——postmortem/0001)。 -- **全链路集成**——一个脚本化的 mock 模型通过真实 agent loop(智能体循环)调用 `todo_write`;`todo/write` 事件落地,第二次调用替换它。 -- **`session/load` 回放**——一条持久化的 `todo/write` 在新的 ACP bridge 加载会话时重新发出 `plan` 更新。 -- **带 key 的 e2e + 快照**——一个真实 prompt 诱导 `todo_write`;快照 golden 新增 `plan` 通知和日志事件。 +四个层级,预先设计: +- **单元测试**——会话事件(append/snapshot-clone/last-write-wins/not-on-surface);工具(schema 形状、通过真实 `ctx.tools.execute` 的参数校验、值校验、事件追加与替换、非 agent 拒绝、`presentCall`、HMR(热模块替换)安全性);ACP `todosToPlan` 映射;stdio 渲染分支。 +- **真实 Loader 路径**——插件通过 `Loader.unwrapExports` 运行,断言命名空间导出形状存活(它**有** `inject`,因此一个意外的 default 导出会在加载时崩溃——postmortem/0001)。 +- **全循环集成**——一个脚本化的 mock 模型通过真实 agent loop(智能体循环)调用 `todo_write`;`todo/write` 事件落地,第二次调用替换它。 +- **`session/load` 回放**——持久化的 `todo/write` 在新的 ACP bridge 加载会话时重新发出 `plan` 更新。 +- **带密钥 e2e + 快照**——真实 prompt 诱导一次 `todo_write`;快照 golden 获得 `plan` 通知和日志事件。 ## 曾考虑的替代方案 - **内存中的 `ctx.todos` 服务**——需要重新发明日志免费提供的持久性、回放和 `session/load` 重建。 -- **逐项 delta 协议**——仅在共享多所有者列表时需要,不在本次范围内;全量替换更简单且与参考实现一致。 -- **工具放在 `core/`**——`todo_write` 是注册在 `ctx.tools` 上的扩展工具,不属于主干;它与其他工具族一样放在自己的 `packages/todo/` 分组中。 +- **逐项 delta 协议**——仅在共享多所有者列表时需要,超出当前范围;整列表替换更简单,且与参考实现一致。 +- **工具放在 `core/` 中**——`todo_write` 是注册在 `ctx.tools` 上的扩展工具,不属于主干;它像其他工具族一样位于自己的 `packages/todo/` 分组中。 ## 后果 -todo 列表是持久的、可回放的会话状态:一条持久化的 `todo/write` 在 `session/load` 时重新向编辑器发出 `plan` 更新,日志(而非插件内存)是唯一真源。全量替换意味着每次更新一次工具调用、last-write-wins;没有需要协调的 delta 协议。事件不进入 surface,因此 todo 更新永远不会扰动推导出的模型历史——模型只看到自己的工具调用和结果。 +todo 列表是持久、可回放的会话状态:持久化的 `todo/write` 在 `session/load` 时重新发出编辑器的 `plan` 更新,日志(而非插件内存)是唯一真源。整列表替换意味着每次更新一次工具调用,last-write-wins;没有需要协调的 delta 协议。事件不进入 surface,因此 todo 更新永远不会扰动推导出的模型历史——模型只看到自己的工具调用和结果。 diff --git a/docs/rfc/implemented/feature/2026-06-30-hook-bridges.i18n.yaml b/docs/rfc/implemented/feature/2026-06-30-hook-bridges.i18n.yaml index 5402e85e81..03e276e446 100644 --- a/docs/rfc/implemented/feature/2026-06-30-hook-bridges.i18n.yaml +++ b/docs/rfc/implemented/feature/2026-06-30-hook-bridges.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-30-hook-bridges.md: 17ff57307c34121c845592efa93c723e66c98886 -2026-06-30-hook-bridges.zh.md: 2a94d5cca2f490e4aac493fe357a825ad3b4d271 +2026-06-30-hook-bridges.zh.md: a4b8c12593cdac35deb882ba15a58876650c1653 diff --git a/docs/rfc/implemented/feature/2026-06-30-hook-bridges.zh.md b/docs/rfc/implemented/feature/2026-06-30-hook-bridges.zh.md index 2a94d5cca2..a4b8c12593 100644 --- a/docs/rfc/implemented/feature/2026-06-30-hook-bridges.zh.md +++ b/docs/rfc/implemented/feature/2026-06-30-hook-bridges.zh.md @@ -1,4 +1,4 @@ -# RFC:dsh-hooks-claude + dsh-hooks-codex——Claude Code / Codex 钩子桥接插件 +# RFC:dsh-hooks-claude + dsh-hooks-codex —— Claude Code / Codex 钩子桥接插件 Status: implemented @@ -6,65 +6,65 @@ Status: implemented ## 问题 -harness 的扩展面是其类型化的拦截 seam(见[拦截 seam RFC](2026-06-30-interception-seams.md)):所谓「原生钩子」不过是一个普通的 Cordis 插件,订阅 `agent/session-start`、`agent/prompt-submit`、`tools/pre-execute`、`tools/post-execute`、`agent/turn-continuation`、`subagent/start`、`subagent/end`。但用户带着**已有的** Claude Code(CC)和 Codex 钩子配置到来——一个 `hooks.json`(或设置文件中的 `hooks` 键)里满是 shell 命令钩子——并且希望它们原样运行。本 RFC 引入两个**桥接插件**,将外部 shell 钩子协议翻译到类型化 seam 上,基于共享的协议格式(wire format)库(见 [hook-protocol-lib RFC](2026-06-30-hook-protocol-lib.md))构建。 +harness 的扩展面是其类型化的拦截 seam(见[拦截 seam RFC](2026-06-30-interception-seams.md)):所谓「原生钩子」不过是一个普通的 Cordis 插件,订阅 `agent/session-start`、`agent/prompt-submit`、`tools/pre-execute`、`tools/post-execute`、`agent/turn-continuation`、`subagent/start`、`subagent/end`。但用户带着**既有的** Claude Code(CC)和 Codex 钩子配置到来,一个 `hooks.json`(或 settings 文件中的 `hooks` 键)里满是 shell 命令钩子,并希望它们原样运行。本 RFC 引入两个**桥接插件**,将外部 shell 钩子协议翻译到类型化 seam 上,构建于共享的协议格式(wire format)库之上(见 [hook-protocol-lib RFC](2026-06-30-hook-protocol-lib.md))。 -贯穿整个设计的定位是:**桥接是兼容性适配器,不是高级工具。**桥接能做的事(阻止工具、注入上下文、强制继续、观察 subagent),原生 Cordis 插件都能更强力地完成——有类型化返回值、完整的 `ctx`、无序列化边界。桥接存在的理由是运行外部 CC/Codex 命令钩子中被明确支持的子集。这使每个桥接保持精简:解析配置、选择匹配模式、构建每事件的 payload、调用共享库的 `runHook` + `mergeHookOutputs`,再将中性结果映射到 seam 的 Decision。各 package 的 README 记录了当前相对官方协议的不支持事件与部分字段清单。 +贯穿整个设计的定位:**桥接是兼容性适配器,不是高级工具。** 桥接能做的事(阻止工具、注入上下文、强制继续、观察 subagent),原生 Cordis 插件都能做得更强——类型化返回值、完整 `ctx`、无序列化边界。桥接存在的理由是运行外部 CC/Codex 命令钩子中被明确支持的子集。这使每个桥接保持精简:解析配置、选择匹配模式、构建每事件的 payload、调用共享库的 `runHook` + `mergeHookOutputs`,再将中性结果映射为 seam Decision。各 package 的 README 维护着当前不支持的事件和部分字段的完整清单,以官方协议为参照。 ## 决策 -`packages/hooks/` 分组下两个独立插件,各自为函数/命名空间插件(`name`/`inject`/`Config`/`apply`,无 default export——见 [postmortem 0001](../../../postmortem/0001-acp-default-export-drops-inject.md)),仅注入 `bash`: +`packages/hooks/` 组下两个独立插件,各为 function/namespace 插件(`name`/`inject`/`Config`/`apply`,无 default export——见 [postmortem 0001](../../../postmortem/0001-acp-default-export-drops-inject.md)),仅注入 `bash`: -- **`dsh-hooks-claude`**——CC 方言。Claude Code 当前钩子点中的七个:`SessionStart`、`UserPromptSubmit`、`PreToolUse`、`PostToolUse`、`Stop`、`SubagentStart` 和 `SubagentStop`。拥有 CC 形状的每事件 stdin payload(基础字段为 `session_id`/`cwd`/`hook_event_name`,加上每事件特有字段)、`CLAUDE_PROJECT_DIR` 环境变量加 `${CLAUDE_PLUGIN_ROOT}`/`${CLAUDE_PROJECT_DIR}` 替换,以及字面量或正则匹配模式。CC 钩子的 stdin 带有**尾随换行**。 -- **`dsh-hooks-codex`**——Codex 当前钩子点中的五个:`PreToolUse`、`PostToolUse`、`SessionStart`、`UserPromptSubmit` 和 `Stop`。使用始终为正则的匹配模式、Codex 形状的 snake_case payload(带 `turn_id`/`model`/`permission_mode` 额外字段),写入时**不带**尾随换行,不注入 Codex 插件环境变量,不做配置时占位符替换,也没有 pre-tool 审批或重写路径。工具调用的 payload 在桥接的精简 `tool_input: { command }` 形状中携带真实的 `tool_name`。 +- **`dsh-hooks-claude`**——CC 方言。Claude Code 当前七个钩子点中的七个:`SessionStart`、`UserPromptSubmit`、`PreToolUse`、`PostToolUse`、`Stop`、`SubagentStart` 和 `SubagentStop`。拥有 CC 形态的每事件 stdin payload(基础字段 `session_id`/`cwd`/`hook_event_name` 加每事件字段)、`CLAUDE_PROJECT_DIR` 环境变量加 `${CLAUDE_PLUGIN_ROOT}`/`${CLAUDE_PROJECT_DIR}` 替换,以及字面量或正则的匹配模式。CC 钩子的 stdin 带有**尾部换行**。 +- **`dsh-hooks-codex`**——Codex 当前五个钩子点中的五个:`PreToolUse`、`PostToolUse`、`SessionStart`、`UserPromptSubmit` 和 `Stop`。使用始终为正则的匹配模式、Codex 形态的 snake_case payload(含 `turn_id`/`model`/`permission_mode` 额外字段),写入时**不带**尾部换行,不注入 Codex 插件环境变量,不做配置时占位符替换,也没有 pre-tool 审批或重写路径。工具调用的 payload 在桥接精简后的 `tool_input: { command }` 形态中携带真实的 `tool_name`。 -### 结果 → Decision 映射 +### Outcome → Decision 映射 每个桥接将共享库返回的中性 `MergedHookOutcome` 映射到 seam 的类型化 Decision: | Seam | CC | Codex | |---|---|---| -| `agent/session-start`(emit) | additionalContext → `agent.inject()` | plain-stdout 输出 → additionalContext → `agent.inject()` | +| `agent/session-start`(emit) | additionalContext → `agent.inject()` | plain-stdout output → additionalContext → `agent.inject()` | | `agent/prompt-submit` | `deny`→`block`;仅上下文→delegate+fold | `block`→`block`;仅上下文→delegate+fold | | `tools/pre-execute` | `deny`→`deny`;`ask`→`ask` | `block`→`deny`(无 allow/ask) | | `tools/post-execute` | `deny`→`block`+feedback;仅上下文→delegate+fold | 同上 | -| `agent/turn-continuation` | 阻塞式 Stop → `continue`(reason = 下一步 steering(中途引导)) | 同上 | -| `subagent/start`(emit) | additionalContext → 注入进程内活跃子 agent;远程子 agent 没有本地注入目标 | 本桥接不支持 | +| `agent/turn-continuation` | 阻塞的 Stop → `continue`(reason = next-step steering(中途引导)) | 同上 | +| `subagent/start`(emit) | additionalContext → 注入到存活的进程内 subagent;远程 subagent 无本地注入目标 | 本桥接不支持 | | `subagent/end`(emit) | 仅观察 | 本桥接不支持 | -CC 桥接的 `ask` 结果是一条真正的权限路径,而非桥接的终态决策:`dsh-tools` 通过可选的[审批 seam](2026-07-06-approval-seam.md) 解析它。组合式 ACP 应答器会向拥有者编辑器会话发起提示,`allowed-once` 后继续执行;如果没有 ApprovalService 或应答器,调用以 `deny` 关闭。 +CC 桥接的 `ask` 结果是一条真正的权限路径,而非终态桥接决策:`dsh-tools` 通过可选的[审批 seam](2026-07-06-approval-seam.md) 来解析它。组合式 ACP 应答器向拥有该会话的编辑器会话发起提示,`allowed-once` 后继续执行;如果没有 ApprovalService 或应答器,调用以 `deny` 安全关闭。 -### 上下文来源始终是插件(错标防护) +### 上下文来源始终是插件(误标签防护) -`agent.inject()` 在缺少 `MessageSource` 时默认为 `{ kind: 'user' }`,因此每个桥接的 `inject()` 和 `HookContext` 都传入 `{ kind: 'plugin', plugin: 'hooks-claude' | 'hooks-codex' }`。单元测试覆盖率固定了最终 `context/message.source` 为插件而非用户。 +`agent.inject()` 在缺少 `MessageSource` 时默认为 `{ kind: 'user' }`,因此每个桥接的 `inject()` 和 `HookContext` 都传入 `{ kind: 'plugin', plugin: 'hooks-claude' | 'hooks-codex' }`。单元测试覆盖率固定验证结果中的 `context/message.source` 为插件而非用户。 ### 添加上下文不是否决——先 delegate,再 fold -仅含上下文的钩子必须调用 `next()` 然后将其 `additionalContext` 折入下游决策;直接返回 allow 或 accept 会绕过后续策略监听器。Post-tool 的 block 和 accept 决策都保留已添加的上下文。Prompt allow 保留上下文,而 prompt block 丢弃上下文,因为提示词从未到达模型。只有显式的钩子 denial 或 block 才会短路 waterfall(瀑布式事件)。 +仅含上下文的钩子必须调用 `next()` 然后将其 `additionalContext` 折叠进下游决策;直接返回 allow 或 accept 会绕过后续策略监听器。Post-tool 的 block 和 accept 决策都保留已添加的上下文。Prompt allow 保留上下文,而 prompt block 丢弃上下文,因为提示词从未到达模型。只有显式的钩子 denial 或 block 才会短路 waterfall(瀑布式事件)。 ### CLAUDE_PROJECT_DIR 默认为会话工作区 -Claude Code 始终导出 `CLAUDE_PROJECT_DIR`,常见的未修改钩子引用 `$CLAUDE_PROJECT_DIR` 来构造项目相对路径。显式的 `config.projectDir` 优先;当它被省略时(默认的 ACP 接线只配置 `configPath`),桥接将该环境变量按每次运行默认为 agent 的会话工作区——即钩子已经运行其中的 `session.header.cwd`——而不是留空。因此一个标准的项目相对钩子在默认配置下即可工作。 +Claude Code 始终导出 `CLAUDE_PROJECT_DIR`,常见的未修改钩子引用 `$CLAUDE_PROJECT_DIR` 来构造项目相对路径。显式的 `config.projectDir` 优先;当它被省略时(默认 ACP 接线只配置 `configPath`),桥接将该环境变量按每次运行默认为 agent(智能体)的会话工作区——即钩子已经在其中运行的 `session.header.cwd`——而非留空。这样,一个标准的项目相对路径钩子在默认配置下即可正常工作。 ### 隔离 -配置在加载时一次性解析;读取/解析失败时记录日志并不注册任何内容,而非崩溃启动(一个拼错的路径不得拖垮 agent)。CC 只运行 shell 形式的 `type: 'command'` 钩子;`http`、`mcp_tool`、`prompt` 和 `agent` 处理器被解析后跳过。Codex 只运行同步命令处理器,跳过 `async: true` 或非命令条目。emit 监听路径(`session-start`、`subagent/start`)以 detached 方式运行,其 `inject` 包裹在 `.catch` 中记录日志(抛异常的 inject 不得中断会话启动或循环)。 +配置在加载时一次性解析;读取/解析失败时记录日志并不注册任何内容,而非崩溃启动(一个拼错的路径不应拖垮 agent)。CC 桥接只运行 shell 形式的 `type: 'command'` 钩子;`http`、`mcp_tool`、`prompt` 和 `agent` 处理器被解析后跳过。Codex 桥接只运行同步命令处理器,跳过 `async: true` 或非命令条目。emit 监听路径(`session-start`、`subagent/start`)以 detached 方式运行,其 `inject` 包裹在 `.catch` 中记录日志(抛异常的 inject 不得中断会话启动或循环)。 -### 钩子的运行位置与配置来源 +### 钩子在哪里运行,配置从哪里来 -钩子在 agent 的会话工作区中运行,因此相对路径指向用户的项目。`configPath` 相对于进程启动 cwd 解析一次,适用于所有会话。按会话的项目本地发现仍推迟在 `TODO(per-session-hook-config)` 下。 +钩子在 agent 的会话工作区中运行,因此相对路径指向用户的项目。`configPath` 相对于进程启动时的 cwd 解析一次,适用于所有会话。按会话的项目本地发现仍推迟在 `TODO(per-session-hook-config)` 下。 ## 推迟的兼容性缺口 -- **工具输入重写。** CC/Codex 的 `updatedInput` 被记录日志并发出警告,但不生效——输入重写是一个推迟的一致性设计问题(见 [pre-tool-input-rewrite RFC](../../proposed/feature/2026-06-30-pre-tool-input-rewrite.md)),因为预执行参数被 `tool/call` 审计、`assistant/message` 历史和 ACP/tool-bash 展示共同读取,诚实的重写是一个设计单元,而非一个字段。 -- **Stop 循环防护**(`TODO(stop-loop-guard)`)。Claude Code 提供 `stop_hook_active` 并在连续八次阻塞后覆盖钩子;Codex 提供 `stop_hook_active` 但文档中没有等效上限。两个桥接始终报告 `false`,因此一个无条件阻塞的 Stop 钩子会在每一步强制继续——钩子作者必须自行限制,直到状态追踪落地。 -- **钩子 `continue:false`(硬停止)。** 钩子可以请求终止整个运行(CC/Codex `continue:false`);共享 merge 将其折入 `MergedHookOutcome.stop`/`stopReason`,但没有桥接对其采取行动(`TODO(hook-continue-false)`)——拦截 seam 尚无「硬停止 agent」原语(Decision 阻塞/引导的是单个点,而非整个运行)。与循环防护工作一起推迟;停止请求记录在 `hook/result` 日志中,钩子在此期间保留其逐点效果(decision/上下文)。 -- **配置发现。** 路径在 `cordis.yml` 中显式指定且为进程级(见上文);完整的多层 CC/Codex 优先级遍历、按会话的项目本地发现以及信任/hash 模型均未重新实现(`TODO(per-session-hook-config)`)。 -- **Session-start / subagent-start 上下文为尽力而为(`TODO(session-start-gating)`)。** 两个钩子以 detached 方式运行于启动之外,因此其上下文在就绪时注入,但可能错过第一个请求或短命子 agent。保证首请求送达需要一个 awaited 的启动 seam。 +- **工具输入重写。** CC/Codex 的 `updatedInput` 被记录日志并发出警告,但不予执行——输入重写是一个推迟的一致性设计问题(见 [pre-tool-input-rewrite RFC](../../proposed/feature/2026-06-30-pre-tool-input-rewrite.md)),因为 pre-execution 参数被 `tool/call` 审计、`assistant/message` 历史和 ACP/tool-bash 展示共同读取,诚实的重写是一个设计单元,而非一个字段。 +- **Stop 循环防护**(`TODO(stop-loop-guard)`)。Claude Code 提供 `stop_hook_active` 并在连续八次阻塞后覆盖钩子;Codex 提供 `stop_hook_active` 但未记录等效上限。两个桥接始终报告 `false`,因此一个无条件阻塞的 Stop 钩子会在每一步强制继续——在状态追踪落地之前,钩子作者必须自行限制。 +- **钩子 `continue:false`(硬停止)。** 钩子可以请求终止整个运行(CC/Codex `continue:false`);共享合并将其折叠为 `MergedHookOutcome.stop`/`stopReason`,但没有桥接对其采取行动(`TODO(hook-continue-false)`)——拦截 seam 尚无「硬停止 agent」原语(Decision 阻塞/引导的是单个点,而非整个运行)。与循环防护工作一同推迟;停止请求记录在 `hook/result` 日志中,钩子在此期间保留其逐点效果(决策/上下文)。 +- **配置发现。** 路径在 `cordis.yml` 中显式指定且为进程级(见上文);完整的多层 CC/Codex 优先级遍历、按会话的项目本地发现以及信任/hash 模型未被重新实现(`TODO(per-session-hook-config)`)。 +- **Session-start / subagent-start 上下文为尽力而为(`TODO(session-start-gating)`)。** 两个钩子以 detached 方式运行于启动过程之外,因此其上下文在就绪时注入,但可能错过首个请求或短命的 subagent。要保证首请求送达,需要一个 awaited 的启动 seam。 ## 曾考虑的替代方案 -**同一点的钩子并发执行。** 参考引擎对同一点匹配到的钩子并发运行并折叠结果。本桥接**串行**运行它们(匹配循环内逐钩子 `await`),并以相同的最严格合并策略折叠。串行是刻意的:它使每个钩子的 `hook/invoked`/`hook/result` 对在会话日志中相邻且顺序确定,而折叠对决策是顺序无关的(`deny > ask > allow`),因此结果一致。代价是延迟(钩子 *N* 等待钩子 *N−1*)且逐钩子超时不重叠——对真实配置使用的钩子数量而言可接受;如果某天配置扇出到足以影响挂钟时间,再重新审视。 +**每点钩子并发执行。** 参考引擎对一个点匹配到的钩子并发运行并折叠结果。本桥接**串行**运行(匹配循环内每个钩子 `await`),并以相同的最严格合并策略折叠。串行是刻意的:它使每个钩子的 `hook/invoked`/`hook/result` 对在会话日志中相邻且顺序确定,而折叠对决策是顺序无关的(`deny > ask > allow`),因此结果一致。代价是延迟(钩子 *N* 等待钩子 *N−1*)以及每钩子超时不重叠——对真实配置中的钩子数量可以接受;如果某配置的扇出大到影响总耗时,再重新评估。 ## 后果 -匹配语义、退出码处理与合并优先级位于 `dsh-hook-protocol`;每个桥接只负责解析配置、构建方言 payload 和映射结果。逐文件覆盖率包含配置分支加上通过真实循环、`dsh-bash-local` 和 shell 脚本的端到端映射,同时一个真实 Loader 冒烟测试守护 package 的导出形状。原生插件绕过协议格式,直接返回类型化决策。 +匹配语义、退出码处理和合并优先级位于 `dsh-hook-protocol`;每个桥接只负责解析配置、构建方言 payload 和映射结果。逐文件覆盖率包含配置分支以及通过真实循环、`dsh-bash-local` 和 shell 脚本的端到端映射,同时一个真实 Loader 冒烟测试守护 package 的导出形态。原生插件绕过协议格式,直接返回类型化决策。 diff --git a/docs/rfc/implemented/feature/2026-06-30-hook-protocol-lib.i18n.yaml b/docs/rfc/implemented/feature/2026-06-30-hook-protocol-lib.i18n.yaml index 22f7710469..5979186c01 100644 --- a/docs/rfc/implemented/feature/2026-06-30-hook-protocol-lib.i18n.yaml +++ b/docs/rfc/implemented/feature/2026-06-30-hook-protocol-lib.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-30-hook-protocol-lib.md: 924c320f7ef9fdb55b20ff06f492addbf42d1720 -2026-06-30-hook-protocol-lib.zh.md: 2f1cf0f1e4c99eaf1172642475e3b9bc8c8aed39 +2026-06-30-hook-protocol-lib.zh.md: 81315cbe8767e9a3cc07cdef92359734e4e20f31 diff --git a/docs/rfc/implemented/feature/2026-06-30-hook-protocol-lib.zh.md b/docs/rfc/implemented/feature/2026-06-30-hook-protocol-lib.zh.md index 2f1cf0f1e4..81315cbe87 100644 --- a/docs/rfc/implemented/feature/2026-06-30-hook-protocol-lib.zh.md +++ b/docs/rfc/implemented/feature/2026-06-30-hook-protocol-lib.zh.md @@ -1,4 +1,4 @@ -# RFC:dsh-hook-protocol——Claude Code / Codex 钩子协议格式的共享核心库 +# RFC:dsh-hook-protocol——Claude Code / Codex 钩子协议格式共享核心库 [English](2026-06-30-hook-protocol-lib.md) | 中文 @@ -6,27 +6,27 @@ Status: implemented ## 问题 -钩子子系统提供两个桥接插件:一个运行用户已有的 Claude Code(CC)钩子,一个运行 Codex 钩子。研究参考实现(`~/repos/refs/claude-code`、`~/repos/refs/codex`)后发现一个决定性事实:**Codex 有意重新实现了 CC 钩子协议的一个子集。**它的引擎读取相同的 `hooks.json`,使用相同的 matcher-group 形状、相同的 exit-code/structured-stdout 输出契约,以及相同的 command-hook 执行模型——Codex 的源码甚至以 Claude 的引擎命名自己的引擎,并在注释中标注了「有意偏离」之处。因此两个桥接插件如果各自实现,将重复协议的大部分内容。 +hooks 子系统提供两个桥接插件:一个运行用户既有的 Claude Code(CC)钩子,另一个运行 Codex 钩子。研究参考实现(`~/repos/refs/claude-code`、`~/repos/refs/codex`)后发现一个决定性事实:**Codex 有意重新实现了 CC 钩子协议的一个子集。** 它的引擎读取相同的 `hooks.json`,使用相同的 matcher-group 形状、相同的 exit-code/structured-stdout 输出契约,以及相同的 command-hook 执行模型。Codex 的源码甚至以 Claude 的引擎命名,并在注释中标注了"有意偏离"之处。因此,如果不做抽取,两个桥接插件将大量重复协议逻辑。 -本 RFC 引入 `@deepseek-ai/dsh-hook-protocol`,一个**库**(不是插件——它不注册也不注入任何东西),持有两个桥接插件共同依赖的、真正相同的原语。共享与方言各自持有的部分之间的切分,是本设计的重心所在。 +本 RFC 引入 `@deepseek-ai/dsh-hook-protocol`,一个**库**(不是插件——它不注册也不注入任何东西),持有两个桥接插件共同依赖的真正相同的原语。共享与方言专属之间的分界是本设计的重心。 ## 决策 -在 `packages/hooks/` 下新建一个组,`hook-protocol` 作为纯库存在。它拥有四个原语族以及 `hook/*` 会话事件;每个桥接插件(`dsh-hooks-claude`、`dsh-hooks-codex`)拥有真正不同的部分。 +在 `packages/hooks/` 分组下新建 `hook-protocol` 作为纯库。它拥有四个原语族和 `hook/*` 会话事件;每个桥接插件(`dsh-hooks-claude`、`dsh-hooks-codex`)拥有真正不同的部分。 **共享(本库):** -- **Matcher**——`matchesMatcher(pattern, query, mode)`。两种方言唯一不同的轴被收敛到 `mode` 参数:`claude` 将纯 `[A-Za-z0-9_|]+` 模式视为字面量(管道符 = 精确匹配的交替),其他视为正则;`codex` 始终为无锚定正则。缺失/`''`/`'*'` 时匹配全部;无效正则匹配空集(绝不向循环抛出异常)。 -- **Execution**——`runHook(bash, hook, options)`。通过 `ctx.bash` seam 而非自建 spawn 运行 command hook:执行器已经提供了经过清理但可覆盖的 env、进程组 kill 和超时——正是协议所需的能力,而 `dsh-bash` 的 `stdin`/`env` 字段(正是为此添加的)是进程内桥接插件被允许使用的受信插件接口。它将桥接插件构建的 payload 序列化到 stdin(CC 时追加尾部换行),尊重钩子的 `timeoutSec`(否则使用 `DEFAULT_HOOK_TIMEOUT_MS`,即两种方言共享的 10 分钟参考默认值),且从不抛出异常(执行器的 rejection 变为 non-blocking-error 的 `HookOutput`)。 -- **Decode**——`parseHookOutput(exit, stdout, stderr)`,exit-code + structured-stdout 编解码器,产出方言无关的 `HookOutput`。Exit `0` → 宽松 JSON 解析 stdout;exit `2` → blocking error,`stderr` 作为原因(以 `decision: 'block'` 呈现,调用方无需单独的 exit-code 分支);其他 → non-blocking error。解析 CC structured-stdout 中在某条路径上有消费方的字段(`continue`/`stopReason`/`decision`/`hookSpecificOutput.{permissionDecision,additionalContext,updatedInput}`/`systemMessage`);桥接插件只尊重对其方言有意义的子集。在任何路径上都没有消费方的字段不予解析(CC 的 `suppressOutput`——钩子 stdout 在此从不进入 transcript,因此没有什么可抑制的;见 [tighten-hook-protocol-contract RFC](../simplification/2026-07-04-tighten-hook-protocol-contract.md))。 -- **Merge**——`mergeHookOutputs(outputs)`,将多个匹配钩子的输出折叠为一个最严格的 `MergedHookOutcome`:权限优先级 **deny > ask > allow**,halt 在首个 `continue:false` 时粘滞,block 原因以 `\n\n` 拼接,context/system-messages 按序累积。 -- **`hook/*` 会话事件**——`hook/invoked` / `hook/result`,通过 declaration-merge 加入 `SessionEventMap`(仅记录日志,类似 `compact/*`——不是 `SurfaceEventType`),附带 `appendHookInvoked`/`appendHookResult` 辅助函数,确保 invoked/result 配对和轮次包含关系在各桥接插件间保持一致。`appendHookResult` 还拥有持久化记录的语义——决策字符串(钩子解析出的 decision,否则在 `continue:false` 时为 `'stop'`,否则为 `'pass'`)和 500 字符的 `stderrSummary` 截断均从此处的 `HookOutput` 导出,而非在各桥接插件中分别实现。 +- **Matcher** — `matchesMatcher(pattern, query, mode)`。两种方言唯一不同的轴被收敛为 `mode` 参数:`claude` 将纯 `[A-Za-z0-9_|]+` 模式视为字面量(管道符 = 精确匹配的多选),其余视为正则;`codex` 始终是无锚定正则。缺省/`''`/`'*'` 匹配一切;无效正则匹配空集(绝不向 agent loop(智能体循环)抛异常)。 +- **Execution** — `runHook(bash, hook, options)`。通过 `ctx.bash` seam 而非自建 spawn 运行 command hook:执行器已提供清洗但可覆盖的 env、进程组 kill 和超时,正是协议所需的能力;`dsh-bash` 的 `stdin`/`env` 字段(正是为此添加的)是进程内桥接插件被允许使用的受信插件接口。它将桥接插件构建的 payload 序列化到 stdin(CC 时追加尾部换行),遵守钩子的 `timeoutSec`(否则使用 `DEFAULT_HOOK_TIMEOUT_MS`,即两种方言共享的 10 分钟参考默认值),且从不抛异常(执行器拒绝变为 non-blocking-error 的 `HookOutput`)。 +- **Decode** — `parseHookOutput(exit, stdout, stderr)`,exit-code + structured-stdout 编解码器,产出方言无关的 `HookOutput`。Exit `0` → 宽松 JSON 解析 stdout;exit `2` → blocking error,`stderr` 为原因(以 `decision: 'block'` 呈现,调用方无需单独处理 exit-code 分支);其他 → non-blocking error。解析 CC structured-stdout 中在某条路径上有消费方的字段(`continue`/`stopReason`/`decision`/`hookSpecificOutput.{permissionDecision,additionalContext,updatedInput}`/`systemMessage`);桥接插件只采纳对其方言有意义的子集。在任何路径上都没有消费方的字段不予解析(CC 的 `suppressOutput`——钩子 stdout 在此处从不进入 transcript(文本记录),因此无需抑制;见 [tighten-hook-protocol-contract RFC](../simplification/2026-07-04-tighten-hook-protocol-contract.md))。 +- **Merge** — `mergeHookOutputs(outputs)`,将多个匹配钩子的输出折叠为一个最严格的 `MergedHookOutcome`:权限优先级 **deny > ask > allow**,halt 在首个 `continue:false` 时粘滞,block reason 以 `\n\n` 拼接,context/system-messages 按序累积。 +- **`hook/*` 会话事件** — `hook/invoked` / `hook/result`,declaration-merge 进 `SessionEventMap`(仅日志,如 `compact/*`——不是 `SurfaceEventType`),配有 `appendHookInvoked`/`appendHookResult` 辅助函数,确保 invoked/result 配对与 turn 包含关系在各桥接插件间保持一致。`appendHookResult` 还拥有持久化记录的语义:decision 字符串(钩子解析出的 decision,否则 `continue:false` 时为 `'stop'`,否则为 `'pass'`)和 500 字符的 `stderrSummary` 截断均从本库的 `HookOutput` 派生,而非各桥接插件各自实现。 -**方言各自持有(桥接插件):**构建每个事件的 stdin payload(CC 的 base + per-event 字段集 vs Codex 的 snake_case 加 `turn_id`/`model` 额外字段)、方言的 env 与 `${CLAUDE_PLUGIN_ROOT}` 替换(CC)vs 无替换(Codex),以及将方言无关的 `HookOutput`/`MergedHookOutcome` 映射到 harness 的 seam 特定类型化 Decision(`PreToolDecision`、`PromptDecision`、`ContinuationDecision`、`PostToolDecision`)。 +**方言专属(桥接插件):** 构建每个事件的 stdin payload(CC 的 base+per-event 字段集 vs Codex 的 snake_case 加 `turn_id`/`model` 额外字段)、方言的 env 与 `${CLAUDE_PLUGIN_ROOT}` 替换(CC)vs 无替换(Codex),以及将方言无关的 `HookOutput`/`MergedHookOutcome` 映射为 harness seam 专属的类型化 Decision(`PreToolDecision`、`PromptDecision`、`ContinuationDecision`、`PostToolDecision`)。 ## 曾考虑的替代方案 -**一个参数化引擎。** 否决,因为 payload 构建和决策映射在方言间确实不同。Matcher、编解码器、执行、合并规则和事件保持共享;各桥接插件保留自己的 payload 和映射,使其协议格式行为在代码中可就地阅读。 +**单一参数化引擎。** 否决,因为 payload 构建与 decision 映射在方言间确实不同。Matcher、编解码器、执行、合并规则和事件保持共享;每个桥接插件保留自己的 payload 和映射,使其协议格式行为在代码中可就地阅读。 ## 后果 -每个桥接插件解析配置、构建方言 payload、调用共享的 runner 和 merge 逻辑、映射决策、追加 `hook/*` 事件。协议测试覆盖每种 matcher 模式、exit-code 与编解码器字段、runner 管道、merge 优先级和审计辅助函数,逐文件 100% 覆盖率;桥接插件测试验证库的真实加载路径。`updatedInput` 已被解析,但在 [input-rewrite 提案](../../proposed/feature/2026-06-30-pre-tool-input-rewrite.md)落地之前仅记录日志并发出警告。 +每个桥接插件解析配置、构建方言 payload、调用共享的 runner 与 merge 逻辑、映射 decision、追加 `hook/*`。协议测试覆盖每种 matcher 模式、exit-code 与编解码器字段、runner 管道、merge 优先级和审计辅助函数,逐文件 100% 覆盖率;桥接插件测试验证库的真实加载路径。`updatedInput` 已解析但仅记录日志并发出警告,直到 [input-rewrite 提案](../../proposed/feature/2026-06-30-pre-tool-input-rewrite.md)落地。 diff --git a/docs/rfc/implemented/feature/2026-06-30-interception-seams.i18n.yaml b/docs/rfc/implemented/feature/2026-06-30-interception-seams.i18n.yaml index b40109802a..2a1efb261c 100644 --- a/docs/rfc/implemented/feature/2026-06-30-interception-seams.i18n.yaml +++ b/docs/rfc/implemented/feature/2026-06-30-interception-seams.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-30-interception-seams.md: fb8efe1e1c2057db13b440881f110ca7f579a81e -2026-06-30-interception-seams.zh.md: b22b3d61bd14b6708e5a063f02537e981fead0fc +2026-06-30-interception-seams.zh.md: 668b96dba282ecdcbe85cc0b1dc56c2de293b3b3 diff --git a/docs/rfc/implemented/feature/2026-06-30-interception-seams.zh.md b/docs/rfc/implemented/feature/2026-06-30-interception-seams.zh.md index b22b3d61bd..668b96dba2 100644 --- a/docs/rfc/implemented/feature/2026-06-30-interception-seams.zh.md +++ b/docs/rfc/implemented/feature/2026-06-30-interception-seams.zh.md @@ -6,55 +6,55 @@ Status: implemented ## 问题 -harness 需要一套钩子子系统:用户在生命周期节点扩展或拦截 agent(智能体),方式类似 Claude Code(CC)和 Codex。驱动本设计的关键重构是:**"原生钩子"不是一个 package**——原生钩子只是一个普通的 Cordis 插件,订阅规范的生命周期事件。因此真正的产品是一个*强大、类型完备的规范事件表面*;CC/Codex 桥接(`dsh-hooks-claude` / `dsh-hooks-codex` 包)只是把外部 shell-hook 协议映射到同一表面的翻译层。桥接能做的事,普通插件都能直接做——而且更强大(没有序列化边界、完整的 `ctx`、类型化的返回值)。 +harness 需要一套钩子子系统:用户像 Claude Code(CC)和 Codex 那样在生命周期节点扩展或管控 agent(智能体)。驱动本设计的关键视角转换是:**"原生钩子"不是一个包**——原生钩子只是一个普通的 Cordis 插件,订阅规范的生命周期事件。因此真正的产品是一个*强大、类型完备的规范事件表面*;CC/Codex 桥接(`dsh-hooks-claude` / `dsh-hooks-codex` 包)只是将外部 shell-hook 协议映射到同一表面的翻译层。桥接能做的事,普通插件可以直接做——而且更强大(无序列化边界、完整 `ctx`、类型化返回值)。 -这个表面需要为以下各阶段提供不同的契约:逐 prompt 策略(CC 的 `UserPromptSubmit`)、会话启动观测(CC 的 `SessionStart`)、工具执行前策略、环绕调度控制、工具执行后变换、最终结果观测,以及附带面向模型原因的继续。如果把这些阶段混为一谈,插件就会获得不需要的修改通道,终态也会依赖监听器顺序。[事件域语义 RFC](../architecture/2026-06-30-event-domain-semantics.md) 提供了三域规则和类型化 Decision 惯用法;本 RFC 将它们应用到生命周期 seam 上。 +该表面需要为以下场景提供各自独立的契约:逐 prompt 策略(CC 的 `UserPromptSubmit`)、会话启动观测(CC 的 `SessionStart`)、工具执行前策略、环绕调度控制、工具执行后变换、最终结果观测,以及携带面向模型的原因的继续执行。如果把这些阶段混为一谈,插件就会获得不需要的 mutation 通道,而终结性将依赖监听器的注册顺序。[事件域语义 RFC](../architecture/2026-06-30-event-domain-semantics.md) 提供了三域规则与类型化 Decision 惯用法;本 RFC 将其应用于生命周期 seam。 ## 决策 -规范表面将可变换策略、环绕调度控制与仅观测通知分离。策略 waterfall(瀑布式事件)返回小型的、seam 专属的**类型化 Decision 联合类型**;包装层返回归一化结果;通知接收不可变快照,不能影响结果。覆盖范围包括本次纳入的钩子点(`session-start`、`prompt-submit`、`pre-tool`、`post-tool`、通过 continuation 实现的 `stop`),同时将非钩子的执行策略留给独立组合。 +规范表面将可变换策略、环绕调度控制与仅观测通知分离。策略 waterfall(瀑布式事件)返回小型的、seam 专属的**类型化 Decision 联合类型**;包装层返回规范化结果;通知接收不可变快照,无法影响结果。覆盖的钩子点包括 `session-start`、`prompt-submit`、`pre-tool`、`post-tool`、通过 continuation 实现的 `stop`,同时将非钩子的执行策略留作独立可组合。 **Agent 事件**(`dsh-agent`): -- `agent/session-start(agent, source)`——emit,在第 1 轮次之前触发一次,携带 `SessionStartSource`(`startup` 表示全新/fork 创建,`resume` 表示重新加载的持久化会话;`clear`/`compact` 保留)。纯通知——它**不能**阻塞启动(这是有意的缺口:桥接用于记录/注入,不用于拦截启动)。监听器通过 `agent.inject()` 注入上下文。 -- `agent/prompt-submit(agent, content, source, next) → PromptDecision`——waterfall,在已开启的轮次内、`user/message` 追加之前,对每条出队的排队消息触发。`allow`(可选地重写 prompt `content` 或附加 `additionalContext`)或 `block`(丢弃该 prompt;循环在其位置追加一条持久的 `prompt/blocked`——见下方调度说明)。 +- `agent/session-start(agent, source)` ——emit,在第 1 轮次之前触发一次,携带 `SessionStartSource`(`startup` 表示全新/fork 创建,`resume` 表示重新加载的持久化会话;`clear`/`compact` 保留)。纯通知,**不能**阻塞启动(这是有意的空白:桥接可以记录/注入,但不管控启动)。监听器通过 `agent.inject()` 注入上下文。 +- `agent/prompt-submit(agent, content, source, next) → PromptDecision` ——waterfall,在已开启的轮次内、`user/message` 追加之前,对每条出队的排队消息触发。`allow`(可选地重写 prompt `content` 或附加 `additionalContext`)或 `block`(丢弃该 prompt;循环在其位置追加一条持久的 `prompt/blocked`——见下方调度说明)。 -**`agent/turn-continuation`** 接收并返回一个 `ContinuationDecision`。`{action:'continue', reason?}` 可携带面向模型的上下文,记录为同一轮次内的下一步 steering(中途引导)——与 `/goal` step-end-steer 模式互为类型化的孪生。 +**`agent/turn-continuation`** 接收并返回一个 `ContinuationDecision`。`{action:'continue', reason?}` 可携带面向模型的上下文,记录为同一轮次内的下一步 steering(中途引导)——与 `/goal` step-end-steer 模式互为类型化孪生。 ### 工具流水线为每个阶段赋予一种权限 -每次调用遵循 `tools/pre-execute` → guards → `tools/execute` → dispatch → `tools/post-execute` → `tools/result`。注册表快照调用方输入、物化并冻结参数、分配不透明 token。嵌套调用只携带父 token。身份始终不可变;只有 `signal` 可在环绕调度时改变。日志、UI 和工具体因此对「运行了什么」达成一致。 +每次调用遵循 `tools/pre-execute` → guards → `tools/execute` → dispatch → `tools/post-execute` → `tools/result`。注册表快照调用方输入、实体化并冻结参数、分配一个不透明 token。嵌套调用仅携带父 token。身份始终不可变;只有 `signal` 可在环绕调度时改变。日志、UI 和工具体因此对「执行了什么」达成一致。 -- **`tools/pre-execute`** 是可扩展的 waterfall 门禁。其 `PreToolDecision` 允许、拒绝或询问。拒绝跳过 `tools/execute` 和核心调度。询问通过可选的审批 seam 解析:只有 `allowed-once` 继续通过 guards 和调度;拒绝、取消、通道不可用、审批服务缺失或无 agent 调用均归一化为拒绝。每种结果仍会到达后策略和最终观测者。 -- **`ctx.tools.guard()`** 在整个 pre-execute waterfall 之后安装同步的作用域感知策略。guard 可以拒绝或弃权,永远不能强制允许,因此监听器顺序无法复活一个被最终不变式禁止的操作。 -- **`tools/execute`** 是用于超时、重试和指标插件的环绕调度 waterfall。包装层通过 `next()` 委托给核心调度,在此之前只能添加、替换或移除 `exec.signal`,并接收已归一化的抛出或未知工具结果;返回自己的有效结果可短路调度。 -- **`tools/post-execute`** 是检查/变换 waterfall。其 `PostToolDecision` 接受、以反馈阻止、可选地替换内容,或附加 `additionalContext`;对结果的就地修改不是变换通道,因为注册表从受保护的快照加上返回的 decision 重建结果。 -- **`tools/result`** 是每次变换、无损 JSON 物化和外层错误边界之后的同步受限通知。它接收相同的冻结执行身份和权威结果的不可变快照;观测者失败按监听器隔离,不能改变或拒绝 `ToolRegistry.execute()` 返回的结果。 +- **`tools/pre-execute`** 是可扩展的 waterfall 门禁。其 `PreToolDecision` 允许、拒绝或询问。拒绝跳过 `tools/execute` 与核心调度。询问通过可选的审批 seam 解析:只有 `allowed-once` 继续通过 guards 和调度;拒绝、取消、通道不可用、审批服务缺失或无 agent 调用均规范化为拒绝。每种结果仍会到达后策略与最终观测者。 +- **`ctx.tools.guard()`** 在整个 pre-execute waterfall 之后安装同步的、作用域感知的策略。guard 可以拒绝或弃权,永远不能强制允许,因此监听器顺序无法复活一个被最终不变式禁止的操作。 +- **`tools/execute`** 是用于超时、重试和指标插件的环绕调度 waterfall。包装层通过 `next()` 委托给核心调度,在此之前只能添加、替换或移除 `exec.signal`,并接收已规范化的抛出或未知工具结果;返回自己的有效结果则短路调度。 +- **`tools/post-execute`** 是检查/变换 waterfall。其 `PostToolDecision` 接受、以反馈阻止、可选地替换内容,或附加 `additionalContext`;对结果的原地 mutation 不是变换通道,因为注册表从受保护的快照加上返回的 decision 重建结果。 +- **`tools/result`** 是在所有变换、无损 JSON 实体化和外层错误边界之后的同步封闭通知。它接收相同的冻结执行身份和权威结果的不可变快照;观测者的失败按监听器隔离,无法改变或拒绝 `ToolRegistry.execute()` 返回的结果。 -核心调度和工具体位于归一化边界内,因此工具、监听器、格式错误的结果、非 JSON 结果和身份形状失败都解析为 JSON 安全的 `isError` 结果,而非逃逸出轮次。post-execute 监听器因此可以检查抛出异常的工具,最终观测者看到的恰好是调用方收到的、会话日志可持久化的内容。 +核心调度与工具体位于规范化边界内部,因此工具、监听器、格式错误的结果、非 JSON 结果和身份形状错误均解析为 JSON 安全的 `isError` 结果,而非逃逸出轮次。post-execute 监听器因此可以检查一个抛出异常的工具,最终观测者看到的正是调用方收到的、会话日志可以持久化的内容。 -**`TurnEndReason.rejected`**(`dsh-session`):整个 prompt 批次被 `prompt-submit` 阻止的轮次。 +**`TurnEndReason.rejected`**(`dsh-session`):整批 prompt 均被 `prompt-submit` 阻止的轮次。 ### 三个承重的循环决策 -1. **在 prompt 策略之前开启轮次。** 被完全阻止的批次成为零步骤的 `rejected` 轮次,保持封闭性并为 ACP 提供持久的终止事件。每次否决还记录 `prompt/blocked`(含原始 prompt 和原因),因此混合批次保留了被阻止的输入。允许的 `additionalContext` 注入到已开启的轮次中。 +1. **在 prompt 策略之前开启轮次。** 全部被阻止的批次成为零步骤的 `rejected` 轮次,保持封闭性并为 ACP(Agent Client Protocol)提供持久的终结事件。每次否决还记录 `prompt/blocked`(含原始 prompt 和原因),因此混合批次保留被阻止的输入。允许的 `additionalContext` 注入到已开启的轮次中。 -2. **Post-tool `additionalContext` 被缓冲,在所有 `tool/result` 之后追加。** `content`/`feedback` 塑造 `execute()` 返回的结果,但 `additionalContext` 是一条**独立的** `context/message`,而单个步骤可携带多个工具调用。如果在每个结果之后立即追加上下文,会产生 `result(c1) → context → result(c2)` 的交错,破坏工具调用/结果的邻接性。因此 `execute()` 将 `additionalContext` 暴露在其 `ToolExecutionResult` 上,循环为该步骤缓冲每次调用的上下文,仅在所有 `tool/result` 追加完毕后才以 `context/message` 形式追加。 +2. **Post-tool `additionalContext` 被缓冲,在所有 `tool/result` 之后追加。** `content`/`feedback` 塑造 `execute()` 返回的结果,但 `additionalContext` 是一条**独立的** `context/message`,而单个步骤可以携带多个工具调用。如果在每个结果之后立即追加上下文,会产生 `result(c1) → context → result(c2)` 的交错,破坏工具调用/结果的邻接性。因此 `execute()` 将 `additionalContext` 暴露在其 `ToolExecutionResult` 上,循环为该步骤的每次调用缓冲上下文,仅在所有 `tool/result` 追加完毕后才以 `context/message` 形式追加。 -3. **强制 `continue` 的 `reason` 通过 steering 通道入队**,使下一步骤的循环顶部 drain 将其记录为继续轮次的 steering——同一轮次内的下一*步骤* steering,而非下一*轮次*的 prompt(与既有的 `hasSteering` force-continue 覆盖一致)。 +3. **强制 `continue` 的 `reason` 通过 steering 通道入队**,使得下一步骤在循环顶部排空时将其记录为当前轮次的 steering——同一轮次内的下一*步骤* steering,而非下一*轮次*的 prompt(与现有的 `hasSteering` 强制继续覆盖一致)。 ### Pre-tool 输入重写是一个独立的一致性决策 -`PreToolDecision` 不能重写参数。历史和审计调用在执行前记录,ACP 展示读取相同的输入,因此注册表在策略之前封存参数。有效的重写必须在身份创建之前更新历史、审计、展示和执行;该契约属于[输入重写提案](../../proposed/feature/2026-06-30-pre-tool-input-rewrite.md)。 +`PreToolDecision` 不能重写参数。历史和审计调用在执行前记录,ACP 展示读取相同的输入,因此注册表在策略之前封存参数。有效的重写必须在身份创建之前同时更新历史、审计、展示和执行;该契约属于[输入重写提案](../../proposed/feature/2026-06-30-pre-tool-input-rewrite.md)。 ### 边界 -seam 包**不**声明 `hook/*` 会话事件(持久的钩子调用日志);那些属于 `dsh-hook-protocol`,因为原生插件使用类型化 decision 而无需外部钩子日志。原生插件集成测试(`packages/core/agent-loop/tests/interception.spec.ts`)通过真实循环组合这些 seam,不涉及 `hook/*` 协议。压缩(`PreCompact`/`PostCompact`)、Notification 和 Codex `PermissionRequest` 不在本决策范围内。[审批 seam](2026-07-06-approval-seam.md) 通过 `ctx.approval` 解析 `ask` decision,而终止的单调停止由 `agent/turn-stop` 独立负责。 +seam 包**不**声明 `hook/*` 会话事件(持久的钩子调用日志);那些属于 `dsh-hook-protocol`,因为原生插件使用类型化 decision 而无需外部钩子日志。原生插件集成测试(`packages/core/agent-loop/tests/interception.spec.ts`)通过真实循环组合这些 seam,不涉及 `hook/*` 协议。压缩(compaction)(`PreCompact`/`PostCompact`)、Notification 和 Codex `PermissionRequest` 不在本决策范围内。[审批 seam](2026-07-06-approval-seam.md) 通过 `ctx.approval` 解析 `ask` decision,而终结性的单调停止由 `agent/turn-stop` 独立负责。 ## 曾考虑的替代方案 -- **将 pre-tool 输入重写作为本 seam 集的一部分交付**——推迟,视为过度扩展信号;上文已阐述一致性问题(审计、历史和展示都读取执行前记录的 `tool/call.arguments`),[pre-tool 输入重写提案](../../proposed/feature/2026-06-30-pre-tool-input-rewrite.md)负责该设计。 -- **将持久的 `hook/*` SessionEvent 与 seam 一起声明**——否决:原生插件使用类型化 Decision 而完全不需要钩子日志(工作示例已证明),因此持久日志属于[钩子协议库](2026-06-30-hook-protocol-lib.md),而非 seam 表面。 +- **将 pre-tool 输入重写作为本 seam 集的一部分发布**:推迟,视为越界信号;上文已阐述一致性问题(审计、历史和展示都读取执行前记录的 `tool/call.arguments`),[pre-tool 输入重写提案](../../proposed/feature/2026-06-30-pre-tool-input-rewrite.md)负责该设计。 +- **将持久的 `hook/*` SessionEvents 与 seam 一起声明**:否决。原生插件使用类型化 Decision 而完全不需要钩子日志(实际示例已证明),因此持久日志属于[钩子协议库](2026-06-30-hook-protocol-lib.md),而非 seam 表面。 ## 后果 -规范的拦截表面实现了统一类型化,同时不给每个扩展相同的权力:钩子返回 decision,执行包装层做包装,终止 guard 只能拒绝,最终观测者只能观测。循环负责 session-start、prompt-submit、post-tool 上下文缓冲和 continuation;`dsh-tools` 负责身份封存和五阶段执行流水线。它们的契约记录在 [architecture.md](../../../architecture.md)、package README、[核心拦截 decision](../../../core-data-structures/core.md#interception-decisions) 和[工具结构](../../../core-data-structures/tools.md)中。ACP 桥接将 `rejected` 轮次映射为其 `cancelled` 编解码值,而钩子驱动的快照端到端验证可观测的桥接行为。 +规范拦截表面具有统一的类型化,同时不给每个扩展相同的权力:钩子返回 decision,执行包装层做包装,终结 guard 只能拒绝,最终观测者只能观测。循环负责 session-start、prompt-submit、post-tool 上下文缓冲和 continuation;`dsh-tools` 负责身份封存与五阶段执行流水线。它们的契约记录在 [architecture.md](../../../architecture.md)、各 package README、[核心拦截 decision](../../../core-data-structures/core.md#interception-decisions) 与[工具结构](../../../core-data-structures/tools.md)中。ACP 桥接将 `rejected` 轮次映射为其 `cancelled` 编解码值,而钩子驱动的快照端到端验证可观测的桥接行为。 diff --git a/docs/rfc/implemented/feature/2026-06-30-session-store-fork-api.i18n.yaml b/docs/rfc/implemented/feature/2026-06-30-session-store-fork-api.i18n.yaml index 03da25497d..c05a025ca6 100644 --- a/docs/rfc/implemented/feature/2026-06-30-session-store-fork-api.i18n.yaml +++ b/docs/rfc/implemented/feature/2026-06-30-session-store-fork-api.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-30-session-store-fork-api.md: 4bf5c3fe43821570fd947034358e54d0a0a602f9 -2026-06-30-session-store-fork-api.zh.md: 3dd15f5beb095fecb7fdaa7d80abcf4b99c07920 +2026-06-30-session-store-fork-api.zh.md: a3ffb881a446647861fa5fbaf57dde291838a090 diff --git a/docs/rfc/implemented/feature/2026-06-30-session-store-fork-api.zh.md b/docs/rfc/implemented/feature/2026-06-30-session-store-fork-api.zh.md index 3dd15f5beb..a3ffb881a4 100644 --- a/docs/rfc/implemented/feature/2026-06-30-session-store-fork-api.zh.md +++ b/docs/rfc/implemented/feature/2026-06-30-session-store-fork-api.zh.md @@ -1,20 +1,20 @@ # RFC:SessionStore fork API -Status: implemented - [English](2026-06-30-session-store-fork-api.md) | 中文 +Status: implemented + ## 问题 -事件溯源的会话日志已经具备 fork 所需的原语:创建一个新会话并带上种子事件前缀,然后像回放一样从该种子日志推导模型历史。这个原语有意保持底层:`ctx.sessions.create(id, { seed, meta })` 接受任何合法的种子,但普通的活跃会话分支需要围绕以下问题制定策略:哪些前缀可以复制、子会话打上什么元数据、错误如何分类。 +事件溯源的会话日志已经具备 fork 所需的原语:创建一个带有种子事件前缀的新会话,然后像回放一样从该种子日志推导模型历史。这个原语有意保持底层:`ctx.sessions.create(id, { seed, meta })` 接受任何合法种子,但常规的活跃会话分支需要围绕以下问题制定策略:哪些前缀可以被复制、子会话应打上哪些元数据、以及错误如何分类。 -语义风险在于 fork 边界。一个合法的用户可见 fork 种子必须是连续的且被轮次封闭。如果在一个活跃轮次内部 fork,会复制一个未关闭的 `turn/start`,可能还有未关闭的 `step/start`,以及悬空的工具调用。这违反了轮次封闭性与 provider-transcript 不变式,并且会创建一段误导性的子会话历史——看起来像是参与了父会话中一个未完成的轮次。现有的 [subagent seam](../../implemented/feature/2026-06-21-subagent-capability-seam.md) 有意解决的是另一个问题:工具触发的 subagent fork 通常发生在父轮次尚未关闭时,因此 `dsh-subagent-fork` 会将种子裁剪到父会话最后一个已完成轮次的前缀。通用的会话 fork 不应静默裁剪;它应当要么在请求的边界处 fork,要么拒绝。 +语义上的风险在于 fork 边界。一个合法的用户可见 fork 种子必须是连续的且封闭在轮次内。如果在一个活跃轮次内部 fork,会复制一个未关闭的 `turn/start`、可能还有一个未关闭的 `step/start`,以及可能悬空的工具调用。这违反了轮次封闭性与 provider-transcript 不变式,并且会创建一段误导性的子历史——看起来子会话参与了父会话中一个尚未完成的轮次。现有的 [subagent seam](../../implemented/feature/2026-06-21-subagent-capability-seam.md) 有意解决的是另一个问题:工具触发的 subagent fork 通常发生在父轮次仍然打开时,因此 `dsh-subagent-fork` 会将种子裁剪到父会话最后一个已完成轮次的前缀。通用的会话 fork 不应静默裁剪;它应当要么在请求的边界处 fork,要么拒绝请求。 ## 决策 -`dsh-session` 直接在 `ctx.sessions` 上拥有普通活跃会话的 fork 能力。没有独立的 `dsh-session-fork` 包(package),也没有 `ctx.sessionFork` 服务:该 API 没有独立的后端、事件词汇、生命周期或持久化行为,所有持久性工作都委托给现有的会话存储与持久化后端。 +`dsh-session` 直接在 `ctx.sessions` 上拥有常规活跃会话 fork 的能力。不设独立的 `dsh-session-fork` 包(package),也不设 `ctx.sessionFork` 服务:该 API 没有独立的后端、事件词汇、生命周期或持久化行为,所有持久化工作都委托给现有的 session store 和持久化后端。 -存储暴露一个操作: +store 暴露一个操作: ```ts ignore-check type SessionForkSource = Session | SessionId @@ -24,20 +24,20 @@ class SessionStore extends Service { } ``` -`boundary` 是要复制到的源事件 `seq`(含该序号)。省略时默认为源会话当前的最后一个事件;对空源会话省略 `boundary` 会创建一个空的子会话。fork 专有的校验只检查请求的边界是否存在且为 `turn/end`。选定的前缀随后被深拷贝到子会话的种子中。子会话继承源会话的 `cwd`,将 `parentSession` 标记为源会话 id,并将 `seedLength` 设为复制的前缀长度。省略 `childSessionId` 时,`SessionStore` 使用其现有的 id 策略生成一个。 +`boundary` 是要复制到的源事件 `seq`(含该序号)。省略时默认为源会话当前的最后一个事件;对空源会话省略 `boundary` 则创建一个空的子会话。fork 特有的校验仅检查请求的边界是否存在且为 `turn/end`。选定的前缀随后被深拷贝到子会话的种子中。子会话继承源会话的 `cwd`,将 `parentSession` 设为源会话 id,并将 `seedLength` 设为已复制前缀的长度。省略 `childSessionId` 时,`SessionStore` 使用其现有的 id 策略生成一个。 -空前缀可以 fork;任何非空边界必须是一个安全的、已存在的、位于 `turn/end` 处的序号,无论结束原因是什么。类型化的错误区分源不存在、对象陈旧、子会话 id 重复和边界无效。更广泛的日志校验与崩溃恢复仍由其现有的负责方处理。 +空前缀可以被 fork;任何非空边界都必须是一个安全的、已存在的、位于 `turn/end` 的序号,无论结束原因为何。类型化的错误区分源缺失、对象陈旧、子 id 重复和边界无效等情况。更广泛的日志校验与崩溃恢复仍由其现有的负责方处理。 ## 曾考虑的替代方案 -**独立的 `ctx.sessionFork` 服务。** 这是第一版实现,但评审表明它过度套用了能力 seam 模式。代码没有可替换的后端、没有额外的事件面、没有独立的所有权生命周期,也没有超出 `ctx.sessions.create({ seed, meta })` 的持久化行为。保留独立包会迫使调用方发现并安装第二个服务,仅仅为了在会话存储原语之上执行策略。 +**独立的 `ctx.sessionFork` 服务。** 这是最初的实现,但评审表明它过度套用了 capability-seam 模式。代码没有可替换的后端、没有额外的事件面、没有独立的所有权生命周期,也没有超出 `ctx.sessions.create({ seed, meta })` 的持久化行为。保留独立包会迫使调用方为了在 session store 原语之上执行一层策略而去发现并安装第二个服务。 -**两个函数:`snapshot()` 加 `fork()`。** 这保留了可复用的种子/元数据计算,但唯一支持的消费方会立即创建会话。它还让接口感觉比用户实际需要的具体操作更抽象。单一的 `fork()` 加显式 `boundary` 保持了 API 的直接性,同时仍支持对先前时间点的 fork。 +**两个函数:`snapshot()` 加 `fork()`。** 这保留了一个可复用的种子/元数据计算,但唯一支持的消费方会立即创建会话。它还使接口看起来比用户实际需要的具体操作更抽象。单一的 `fork()` 加显式 `boundary` 使 API 保持直接,同时仍支持对先前时间点的 fork。 -**静默裁剪未关闭的轮次到最后一个已完成边界。** 这对 `dsh-subagent-fork` 是正确的,因为委托通常在父轮次尚未关闭时开始,子会话应只继承已完成的前缀。但对普通的用户/会话分支来说是错误的,因为它隐藏了请求的 fork 点实际上不是合法边界这一事实,并静默丢弃了父轮次的尾部。 +**静默裁剪未关闭轮次到最后一个已完成边界。** 这对 `dsh-subagent-fork` 是正确的——委托通常在父轮次仍然打开时开始,子会话应只继承已完成的前缀。但对常规的用户/会话分支而言是错误的,因为它隐藏了请求的 fork 点实际上不是合法边界这一事实,并且静默丢弃了父轮次的尾部。 ## 后果 -公开接口保持小巧且易于发现:活跃会话分支是 `ctx.sessions` 的一部分,紧邻 `create({ seed })`,而非一个独立服务或两步辅助函数对。持久化继续通过现有的 `session/created` 和 `session/flush` 行为工作:fork 出的子会话以种子事件开始生命,因此现有后端只需持久化一次该种子,并在头部保留 `parentSession` / `seedLength`。 +公开接口保持精简且易于发现:活跃会话分支是 `ctx.sessions` 的一部分,紧邻 `create({ seed })`,而非一个独立服务或一对两步辅助函数。持久化继续通过现有的 `session/created` 和 `session/flush` 行为运作:fork 出的子会话以种子事件开始生命,因此现有后端只需持久化该种子一次,并在 header 中保存 `parentSession`/`seedLength`。 -v1 范围仍排除 ACP `session/fork`、对未加载的已持久化会话的 fork、面向模型的工具,以及 subagent 重构。如果未来添加 ACP 方法,应在具备 transcript(文本记录)/快照覆盖后才广播该能力;本 RFC 不添加面向编辑器的更新,因此当前不需要 ACP 快照。fork 子会话的回放仍由现有的[种子边界测试 RFC](../../implemented/testing/2026-06-22-fork-child-replay-seed-boundary.md) 覆盖,而本 API 获得专注的 `dsh-session` 单元测试加 JSONL 持久化覆盖。 +v1 范围仍然排除 ACP(Agent Client Protocol) `session/fork`、对未加载的已持久化会话的 fork、面向模型的工具,以及 subagent 重构。如果未来添加 ACP 方法,应在具备 transcript(文本记录)/快照覆盖后才广播该能力;本 RFC 不添加面向编辑器的更新,因此当前不需要 ACP 快照。fork 子会话的回放仍由现有的[种子边界测试 RFC](../../implemented/testing/2026-06-22-fork-child-replay-seed-boundary.md) 覆盖,而本 API 则获得专门的 `dsh-session` 单元测试加 JSONL 持久化覆盖。 diff --git a/docs/rfc/implemented/feature/2026-06-30-subagent-observe-enrich.i18n.yaml b/docs/rfc/implemented/feature/2026-06-30-subagent-observe-enrich.i18n.yaml index 085f5d2c5b..90b6939da2 100644 --- a/docs/rfc/implemented/feature/2026-06-30-subagent-observe-enrich.i18n.yaml +++ b/docs/rfc/implemented/feature/2026-06-30-subagent-observe-enrich.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-30-subagent-observe-enrich.md: b48a1fff32130345e669a3b3b905c4fda987e41e -2026-06-30-subagent-observe-enrich.zh.md: 59dc555ce4acff30f4ba0b5929b605dc5252fe38 +2026-06-30-subagent-observe-enrich.zh.md: 8ce070001e219572658fd4e94c660de1094ddee5 diff --git a/docs/rfc/implemented/feature/2026-06-30-subagent-observe-enrich.zh.md b/docs/rfc/implemented/feature/2026-06-30-subagent-observe-enrich.zh.md index 59dc555ce4..8ce070001e 100644 --- a/docs/rfc/implemented/feature/2026-06-30-subagent-observe-enrich.zh.md +++ b/docs/rfc/implemented/feature/2026-06-30-subagent-observe-enrich.zh.md @@ -1,4 +1,4 @@ -# RFC:Subagent 生命周期充实——lastAssistantMessage(仅观测) +# RFC:Subagent 生命周期丰富化——lastAssistantMessage(仅观察) Status: implemented @@ -6,26 +6,26 @@ Status: implemented ## 问题 -钩子子系统([拦截 seam RFC](2026-06-30-interception-seams.md))允许插件在生命周期节点观测和门控 agent(智能体)。Claude Code 和 Codex 都暴露了 **SubagentStart / SubagentStop** 钩子,且 CC 的钩子携带 subagent 的最终消息。harness 已经发出 `subagent/start` 和 `subagent/end` 生命周期事件([subagent 能力 seam](2026-06-21-subagent-capability-seam.md)),但其载荷极为精简(`provider`、`id`,以及 end 时的 `stopReason`)——不足以让钩子桥接层在不另行访问活跃运行的情况下报告 subagent 产出了什么。 +钩子子系统([拦截 seam RFC](2026-06-30-interception-seams.md))允许插件在生命周期节点观察和拦截 agent(智能体)。Claude Code 和 Codex 都暴露了 **SubagentStart / SubagentStop** 钩子,且 CC 的钩子携带 subagent 的最终消息。harness 已经发出 `subagent/start` 和 `subagent/end` 生命周期事件([subagent 能力 seam](2026-06-21-subagent-capability-seam.md)),但其载荷极为精简(`provider`、`id`,以及 end 时的 `stopReason`),不足以让钩子桥接层在不单独访问活跃 run 的情况下报告 subagent 产出了什么。 -本 RFC 充实 end 载荷。它刻意限定为**仅观测**:不改变控制流,不引入 waterfall(瀑布式事件)。影响运行的 subagent-stop 决策(续行、注入改变运行的内容)属于另一项更大的重新设计,不在本 RFC 范围内。 +本 RFC 丰富 end 载荷。它刻意限定为**仅观察**:不改变控制流,不引入 waterfall(瀑布式事件)。影响 run 的 subagent-stop 决策(续行、改变 run 的注入)属于另一个更大的重设计,不在本 RFC 范围内。 ## 决策 -**在 `SubagentRunEndInfo` 中添加 `lastAssistantMessage`——子 agent 的最终输出。** 在正常结算路径上,它是只读的类型化 `SubagentResult.output`,观测者无需持有运行即可看到子 agent 的产出。在基础设施拒绝、不存在 `SubagentResult` 的情况下,该字段缺失,事件报告 `stopReason: 'error'`。提供方与监听者是受信任的同进程协作者,遵守借用不可变载荷的契约。 +**在 `SubagentRunEndInfo` 中添加 `lastAssistantMessage`——子 agent 的最终输出。** 在正常结束路径上,它是只读的类型化 `SubagentResult.output`,观察者无需持有 run 即可看到子 agent 产出了什么。在基础设施拒绝(不存在 `SubagentResult`)的情况下,该字段缺失,事件报告 `stopReason: 'error'`。提供方与监听方是受信任的同进程协作者,遵守借用不可变载荷的契约。 -两个事件仍为普通 **`emit`**。异步的 `SubagentService.start()` 将结果观测附加到就绪的提供方运行上,发出 `subagent/start`,然后返回该运行;因此进程内监听者可以通过 `ctx.agents.get(info.id)` 访问已发布的子 agent,而远程提供方无需在本地注册表中有条目。提供方启动被拒绝时不发出任何事件。回调保持仅观测,逐监听者隔离确保一个坏订阅者不会阻塞活跃运行或饿死后续监听者。 +两个事件仍为普通 **`emit`**。异步的 `SubagentService.start()` 将结果观察附加到就绪的 provider run 上,发出 `subagent/start`,然后返回该 run;进程内监听方因此可以通过 `ctx.agents.get(info.id)` 访问已发布的子 agent,而远程 provider 无需在本地注册表中有对应条目。provider 启动被拒绝时不发出任何事件。回调保持仅观察,且逐监听方隔离确保一个异常订阅者不会阻塞活跃 run 或饿死后续监听方。 ## 曾考虑的替代方案 -**`agentType` subagent 类别标签**(CC 的 `subagent_type` 在 harness 中的对应物)放在请求和两个生命周期载荷上——早期草案曾包含它;评审中移除,因为它是 Claude Code 的概念,不适合我们自己的 seam(这里没有任何代码解释它,唯一的消费方是 CC 方言桥接层)。CC 桥接层改为向 Claude Code 自身的 SubagentStart/Stop `agent_type` 匹配器喂入其默认值 `"general-purpose"`,因此本 RFC 只交付一项充实:`lastAssistantMessage`。 +**`agentType` subagent 类别标签**(CC 的 `subagent_type` 在 harness 中的对应物),放在请求与两个生命周期载荷上。早期草案曾包含它;评审中移除,因为它是 Claude Code 的概念,不适合我们自己的 seam(此处没有任何逻辑解释它,唯一消费方是 CC 方言桥接层)。CC 桥接层改为直接为其 SubagentStart/Stop 的 `agent_type` matcher 填入 Claude Code 自身的默认值 `"general-purpose"`,因此本 RFC 只交付一项丰富化:`lastAssistantMessage`。 -**控制流式 `subagent/end`**——推迟;见下文。 +**控制流式 `subagent/end`**:推迟;见下文。 -## 为何仅观测,以及推迟了什么 +## 为何仅观察,以及推迟了什么 -控制流式 `subagent/end`(一个被 await 的 waterfall,返回停止/继续决策,与其他拦截 seam 一致)需要:将 `subagent/end` 从 emit 改为 waterfall、重构 `SubagentService.start` 使其在结算前 await 监听者、在进程内提供方中实现 `resume` 能力以便「继续」能真正重新运行子 agent。这属于[能力 seam RFC](2026-06-21-subagent-capability-seam.md) 已推迟的后台/steering(中途引导)subagent 重新设计(同一项重新设计还将统一 subagent 与 bash 之间的长时间运行工具处理)。本 RFC 交付钩子桥接层当前所需的仅观测充实;`FIXME(subagent-continuation)` / `TODO` 锚点标记了控制流版本在该重新设计发生时将落地的位置。 +控制流式 `subagent/end`(一个被 await 的 waterfall,返回停止/继续决策,与其他拦截 seam 一致)需要:将 `subagent/end` 从 emit 改为 waterfall、重构 `SubagentService.start` 使其在结算前 await 监听方、在进程内 provider 中实现 `resume` 能力以便「继续」能真正重新运行子 agent。这属于[能力 seam RFC](2026-06-21-subagent-capability-seam.md) 已推迟的后台/steering(中途引导)subagent 重设计(同一个重设计还将统一 subagent 与 bash 之间的长时间运行工具处理)。本 RFC 交付钩子桥接层当前所需的仅观察丰富化;`FIXME(subagent-continuation)` / `TODO` 锚点标记了控制流版本在重设计发生时的落点。 ## 后果 -钩子桥接层(或原生插件)现在可以通过订阅既有 emit 将子 agent 的 `lastAssistantMessage` 转发给 SubagentStop 处理器——无需新的控制流接口。词汇新增记录在 [docs/core-data-structures/subagent.md](../../../core-data-structures/subagent.md)(事件行文部分)和两个 subagent README 中;catalog 已重新生成。生产行为无变化——事件的触发方式与之前完全相同,end 载荷多了一个(可选的)字段——因此不需要快照或 e2e 测试变更。 +钩子桥接层(或原生插件)现在可以通过订阅既有 emit 将子 agent 的 `lastAssistantMessage` 转发给 SubagentStop 处理器,无需新的控制流接口。词汇新增记录在 [docs/core-data-structures/subagent.md](../../../core-data-structures/subagent.md)(事件行文部分)与两个 subagent README 中;catalog 已重新生成。生产行为无变化——事件触发方式与之前完全一致,end 载荷上多了一个可选字段——因此无需更新快照或 e2e 测试。 diff --git a/docs/rfc/implemented/feature/2026-07-05-dynamic-workflows.i18n.yaml b/docs/rfc/implemented/feature/2026-07-05-dynamic-workflows.i18n.yaml index 04a6289216..25c723bde1 100644 --- a/docs/rfc/implemented/feature/2026-07-05-dynamic-workflows.i18n.yaml +++ b/docs/rfc/implemented/feature/2026-07-05-dynamic-workflows.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-05-dynamic-workflows.md: 67ceebf7017f197bd800fd339b575390b3b936c1 -2026-07-05-dynamic-workflows.zh.md: 0ea8e88bfa750a9bb253c7dd3061766fe15d3630 +2026-07-05-dynamic-workflows.zh.md: 54ba0d903de228e14a53e6f64ead0f5156e61289 diff --git a/docs/rfc/implemented/feature/2026-07-05-dynamic-workflows.zh.md b/docs/rfc/implemented/feature/2026-07-05-dynamic-workflows.zh.md index 0ea8e88bfa..54ba0d903d 100644 --- a/docs/rfc/implemented/feature/2026-07-05-dynamic-workflows.zh.md +++ b/docs/rfc/implemented/feature/2026-07-05-dynamic-workflows.zh.md @@ -1,80 +1,80 @@ # RFC:动态工作流——脚本驱动的多 agent 编排 seam -Status: implemented - [English](2026-07-05-dynamic-workflows.md) | 中文 +Status: implemented + ## 问题 -harness 可以将一个任务委派给一个子 agent(`dsh-tool-subagent`),但需要扇出到多个独立片段的工作——跨多文件审计、迁移、多角度调研、对抗式验证——迫使模型逐轮次编排:每个中间结果都落入父上下文,计划没有持久存放处,每一步的协调都要消耗一次模型往返。Claude Code 以[动态工作流](https://code.claude.com/docs/en/workflows)的形式提供这一能力:模型编写一段 JavaScript 编排脚本,运行时执行它,由脚本(而非对话)持有循环、分支和中间结果。 +harness 可以将一个任务委派给一个子 agent(`dsh-tool-subagent`),但需要扇出到多个独立部分的工作——跨多文件审计、迁移、多角度调研、对抗式验证——迫使模型逐轮次编排:每个中间结果都落入父上下文,计划无处持久存储,每一步的协调都要消耗一次模型往返。Claude Code 以 [dynamic workflows](https://code.claude.com/docs/en/workflows) 的形式提供了这一能力:模型编写一段 JavaScript 编排脚本,运行时执行它,由脚本(而非对话)持有循环、分支和中间结果。 ## 决策 -在 `packages/workflow/` 下以 bash seam 的形态(接口/实现/消费方)提供一组工作流能力,加上 subagent seam 上它所需的结构化输出基础。 +在 `packages/workflow/` 下以 bash seam 的形态(接口/实现/消费方)提供一组工作流能力,以及它在 subagent seam 上所需的结构化输出基础。 ### 脚本契约(兼容 Claude Code) -一次工作流调用包含 JSON `meta`(`name`、`description`,以及可选的 `whenToUse`/`phases`)和一段支持顶层 `await` 并返回 JSON 值的 JavaScript `script` 正文。元数据作为数据校验,从不被求值。正文接收 `agent(prompt, options)`、`parallel(thunks)`、`pipeline(items, ...stages)`、`phase(title)`、`log(message)` 和 `args`。pipeline 各阶段接收 `(prev, item, index)`,阶段间无屏障;失败的子 agent 和普通阶段错误将受影响的 item 解析为 `null` 并跳过其剩余阶段。Claude Code 的确定性限制通过 journaling 延后处理,因此兼容的脚本正文在将 meta 头移入参数后,可以使用时钟和随机数。 +一次工作流调用包含 JSON `meta`(`name`、`description`,以及可选的 `whenToUse`/`phases`)和一段支持顶层 `await` 并返回 JSON 值的 JavaScript `script` 正文。元数据作为数据校验,从不被执行。正文接收 `agent(prompt, options)`、`parallel(thunks)`、`pipeline(items, ...stages)`、`phase(title)`、`log(message)` 和 `args`。pipeline 各阶段接收 `(prev, item, index)`,阶段之间无屏障;失败的子 agent 和普通阶段错误将受影响的 item 解析为 `null` 并跳过其剩余阶段。Claude Code 的确定性限制通过日志化延迟处理,因此兼容的脚本正文在将 meta 头移入参数后可以使用时钟和随机数。 -与 Claude Code 的一处刻意**偏离**:钩子误用——未知或延后的选项(`effort`/`isolation`/`agentType`)、格式错误的参数、超出支持子集的 schema、触发上限、seam 启动失败——抛出 `fatal: true` 的 `WorkflowError`,组合器对 fatal 错误**重新抛出**而非将 item 置为 null。如果不这样做,一个拼错的选项会溶解为与子 agent 失败无法区分的 `null`——正是本仓库禁止的「接受后静默忽略」失败模式。一处**新增**:工具的 `args` 参数是 JSON 对象(裸列表会被包装为一个字段),以保持协议格式(wire format)的诚实。 +与 CC 有一处刻意的严格性**差异**:钩子误用——未知或延迟的选项(`effort`/`isolation`/`agentType`)、格式错误的参数、超出支持子集的 schema、触发上限、seam 启动失败——会抛出带 `fatal: true` 的 `WorkflowError`,组合器会**重新抛出** fatal 错误而非将 item 置为 null。如果不这样做,一个拼错的选项会悄然变成一个与子 agent 失败无法区分的 `null`——这正是本仓库禁止的「被接受后被忽略」的失败模式。另有一处新增:工具的 `args` 参数是一个 JSON **对象**(裸列表被包装为一个字段),使协议格式(wire format)保持诚实。 ### seam(dsh-workflow) -`ctx.workflows` 是 bash 形态的抽象 `WorkflowService`:每个上下文一个引擎,无命名提供方注册表(引擎是部署级替换,不是共存者)。`start(request)` 对无法启动的脚本同步抛出异常;返回的 `WorkflowRun` 的 `result` 永不 reject(失败解析为 `stopReason: 'error' | 'cancelled'`)。`workflow/*` 事件是仅供观察的 emit,携带数据快照(id + meta;`workflow/end` 不含 result 值),按监听器隔离,与 `subagent/start`/`subagent/end` 对称——控制权留在 run 的持有者手中。词汇细节见 [core-data-structures/workflow.md](../../../core-data-structures/workflow.md)。 +`ctx.workflows` 是 bash 形态的抽象 `WorkflowService`——每个上下文一个引擎,无命名提供方注册表(引擎是部署级替换,不是共存者)。`start(request)` 对无法启动的脚本同步抛出;返回的 `WorkflowRun` 的 `result` **永不** reject(失败解析为 `stopReason: 'error' | 'cancelled'`)。`workflow/*` 事件是仅观察的 emit,携带**数据快照**(id + meta;`workflow/end` 省略 result 值),按监听器隔离,与 `subagent/start`/`subagent/end` 对称——控制权留在 run 的持有者手中。词汇详情见 [core-data-structures/workflow.md](../../../core-data-structures/workflow.md)。 ### 引擎(dsh-workflow-workerthread):每次运行一个 worker 线程 -**信任前提**:工作流脚本与模型的 bash 访问享有相同信任级别。引擎约束有 bug 的脚本,保证 result 必定 settle、值 JSON 安全、取消后静默;它不防御恶意代码。vm 上下文和 worker 线程不是安全边界:脚本可以逃逸到具有进程级权限的 Node API。沙箱化需要在此 seam 之后放置一个独立进程或 isolated-vm 引擎。 +**信任前提**:工作流脚本与模型的 bash 访问具有相同的信任级别。引擎容纳有缺陷的脚本,并保证结果已 settled、值为 JSON 安全、取消后静默;它不防御恶意代码。vm 上下文和 worker 线程不是安全边界:脚本可以逃逸到具有进程级权限的 Node API。沙箱化需要在此 seam 背后使用独立进程或 isolated-vm 引擎。 -**为何选择 `node:worker_threads`**:每次运行获得一个非池化 worker。vm 上下文限制了文档化的脚本表面,而 message-port RPC 将 `agent()` 桥接到宿主侧的子循环。worker 防止脚本的同步工作阻塞宿主,提供序列化边界,并允许取消后强制终止。`isolated-vm` 因其维护状态和部署要求被否决。 +**为何选择 `node:worker_threads`**:每次运行获得一个非池化的 worker。vm 上下文限制了文档化的脚本表面,而 message-port RPC 将 `agent()` 桥接到宿主侧的子循环。worker 防止脚本的同步工作阻塞宿主,提供序列化边界,并允许取消后强制终止。`isolated-vm` 因其维护状态和部署要求被否决。 -宿主在发布前校验元数据并解析正文。私有枚举键的 payload map 定义协议格式;待启动记录、已发布的子记录、单一取消信号、worker 死亡回收、result 优先级和 dispose 静默在协议两侧维持 subagent run 契约。[agent-scope runtime-design RFC](../architecture/2026-07-12-agent-scope-runtime-design.md#workflow-children-are-pending-starts-or-published-records) 拥有这些竞态算法。 +宿主在发布前校验元数据并解析正文。私有枚举键 payload 映射定义协议格式;待启动记录、已发布子记录、单一取消信号、worker 死亡回收、结果优先级与 dispose 静默,在此协议上保持 subagent run 契约。这些竞态算法归 [agent-scope runtime-design RFC](../architecture/2026-07-12-agent-scope-runtime-design.md#workflow-children-are-pending-starts-or-published-records) 所有。 引擎暴露一条进程内 `MessageChannel` 测试路径,因为主进程 V8 覆盖率无法观测 worker 执行。 -**Meta 是数据**:经 schema 校验的 `meta` 字段以 JSON 形式到达 seam,仅做形状校验。宿主从不对元数据字面量求值——否则脚本控制的访问器会在 worker 隔离之外运行。 +**Meta 是数据**:经 schema 校验的 `meta` 字段以 JSON 形式到达 seam,仅做形状校验。宿主从不执行元数据字面量,否则脚本控制的访问器可以在 worker 隔离之外运行。 -**值边界**:`materializeFromRealm` 复制出站值,拒绝函数、symbol、嵌套 `undefined`、异域原型、循环引用、稀疏数组和非有限数。数据属性复制使 `"__proto__"` 安全;getter 正常读取,抛出异常的 getter 会大声失败。`args` 通过 `workerData` 传入,暴露前再次克隆。realm 函数被调用而非复制,抛出的值使用全量渲染器以确保 `result` 不会 reject。钩子错误是宿主 realm 的 `WorkflowError`,因此脚本按 `name` 或 `code` 分支而非 `instanceof Error`,如引擎 README 所述。并发、total-agent、item、超时和 grace 限制均为经校验的配置。 +**值边界**:`materializeFromRealm` 复制出站值,并拒绝函数、symbol、嵌套 `undefined`、异域原型、循环引用、稀疏数组和非有限数字。数据属性复制使 `"__proto__"` 安全;getter 正常读取,抛出异常的 getter 会大声失败。`args` 通过 `workerData` 传入,暴露前再次克隆。realm 函数被调用而非复制,抛出的值使用全量渲染器,因此 `result` 不会 reject。钩子错误是宿主 realm 的 `WorkflowError`,脚本应基于 `name` 或 `code` 分支而非 `instanceof Error`,如引擎 README 所述。并发、total-agent、item、超时和宽限限制均为经校验的配置。 ### 消费方(dsh-tool-workflow) -一个 `workflow` 工具,镜像 `dsh-tool-subagent` 的同步形态:启动、等待、`try/finally` dispose、abort 桥接 `exec.signal`、非 `completed` → `isError`。渲染意图:一张以调用的 `meta.name` 参数为标题的 `generic` 卡片(展示是参数的纯函数)。工具描述即面向模型的编写规范。使用策略作为工具自身的 `tool:` prompt 段随工具一起交付(显式请求才使用的指导——工具指导存在于工具插件中,从不放在部署 persona 里);harness 没有 ultracode 风格的 effort 门控。 +一个 `workflow` 工具,镜像 `dsh-tool-subagent` 的同步形态:启动、await、`try/finally` dispose、abort 桥接 `exec.signal`、非 `completed` → `isError`。渲染意图:一张以调用的 `meta.name` 参数为标题的 `generic` 卡片(展示是参数的纯函数)。工具描述**即**面向模型的编写规范。使用策略以工具自身的 `tool:` prompt 段落随工具发布(显式请求才使用的引导——工具引导存在于工具插件中,从不在部署 persona 中);harness 没有 ultracode 风格的 effort 门控。 ### 基础:subagent seam 上的结构化输出 -`SubagentStartRequest.outputSchema` 由 `dsh-subagent-inprocess` 为两个进程内后端实现。每个结构化子 agent 在 `child.ctx` 上获得自己的作用域捕获工具、指令和强制注册;并发子 agent 可以使用不同 schema 而不共享可变策略,dispose 子 agent 时整个附件被移除。 +`SubagentStartRequest.outputSchema` 由 `dsh-subagent-inprocess` 为两个进程内后端实现。每个结构化子 agent 在 `child.ctx` 上获得自己的作用域捕获工具、指令和强制注册;并发子 agent 可以使用不同的 schema 而不共享可变策略,dispose 子 agent 时移除整个附件。 -输出 schema 使一次 schema 有效的已提交捕获成为子 agent 成功完成的必要条件。作用域运行时呈现捕获工具和指令,仅提交成功的最终结果(包括 SDK 调用的外层 `run_code` 结果),在捕获进入 pending 状态后拒绝后续副作用,并在提交后不再请求模型步骤即停止子 agent。校验失败仍为可重试的工具错误;干净完成但没有已提交捕获的情况 settle 为错误。 +输出 schema 使一次 schema 有效的已提交捕获成为子 agent 成功完成的必要条件。作用域运行时呈现捕获工具和指令,仅提交成功的最终结果(包括 SDK 调用时外层 `run_code` 的结果),在捕获变为 pending 后拒绝后续副作用,并在提交后不再进行模型步骤即停止子 agent。校验失败仍是可重试的工具错误;没有已提交捕获的正常完成以错误结算。 -`StructuredOutputSchema` 是 `dsh-tools` 中可强制执行的原始 JSON-Schema 子集(单字符串 `type`、`properties`/`required`/`additionalProperties`、`items`、标量 `enum`/`const`),不支持的关键字会大声失败,因为该协议数据会逐字成为捕获工具的 parameters。[agent-scope runtime-design RFC](../architecture/2026-07-12-agent-scope-runtime-design.md#structured-output-commits-only-authoritative-outcomes) 拥有组装、提交、守卫和终止停止的正确性算法。 +`StructuredOutputSchema` 是 `dsh-tools` 中可强制执行的原始 JSON-Schema 子集(单字符串 `type`、`properties`/`required`/`additionalProperties`、`items`、标量 `enum`/`const`),不支持的关键字会大声失败,因为该协议数据会逐字成为捕获工具的 parameters。组装、提交、守卫和终止停止的正确性算法归 [agent-scope runtime-design RFC](../architecture/2026-07-12-agent-scope-runtime-design.md#structured-output-commits-only-authoritative-outcomes) 所有。 ## 测试 -worker 侧逻辑通过进程内 `MessageChannel` 运行,以便 V8 覆盖率能度量它。单元测试覆盖脚本辅助函数、fatal 与 nullable 失败、JSON 边界、上限、取消、子 agent 所有权和通过真实循环的结构化输出。built-bin 冒烟测试在纯 Node 下运行单独打包的 `lib/worker.cjs`,带 key 的 e2e 驱动真实子 agent,面向模型的工作流行为通过其所属示例进行快照覆盖。 +worker 侧逻辑通过进程内 `MessageChannel` 运行,使 V8 覆盖率能够度量它。单元测试覆盖脚本辅助函数、fatal 与 nullable 失败、JSON 边界、上限、取消、子 agent 所有权和通过真实循环的结构化输出。built-bin 冒烟测试在纯 Node 下运行单独打包的 `lib/worker.cjs`,带密钥的 e2e 驱动真实子 agent,面向模型的工作流行为通过其所属示例进行快照覆盖。 -## 延后(本轮明确的非目标) +## 延迟(本轮明确的非目标) - **后台收集**(启动工具 → run id → 完成通知 → 收集),与 bash/subagent 后台统一一起设计。 -- **Journaling + 恢复**(`resumeFromRunId`、缓存的 agent() 前缀):实现它会将 Claude Code 的确定性禁令作为脚本契约收紧重新引入(脚本今天可以读取时钟)。 -- **保存/打包的工作流**(`.deepseek/workflows/` 注册表、斜杠命令界面)和**脚本持久化到 run 目录**(tool-call 事件已经持久记录了脚本)。 -- **嵌套 `workflow()`**、**token `budget`**,以及 `effort`/`isolation`/`agentType` agent 选项(每个都以命名延后项的消息大声拒绝)。 -- **整体运行的挂钟超时**:取消总能释放调用方(result 在 grace 内 settle),因此总运行时间上限是后台重设计的策略旋钮,不是此处的正确性需求。 -- **超越 worker 线程的引擎加固**:在同一 seam 之后放置 isolated-vm 或独立进程引擎(真正的沙箱化;内存限制)。 -- **ACP 进度 UI**:基于 `workflow/*` 事件(`/workflows` 风格的视图);事件已为此存在。 -- **ACP 后端结构化输出**和 **`toolFilter`**(两者仍为能力门控 `false`)。 +- **日志化 + 恢复**(`resumeFromRunId`、缓存的 agent() 前缀):实现它会以脚本契约收紧的形式重新引入 CC 的确定性禁令(脚本目前可以读取时钟)。 +- **保存/打包的工作流**(`.deepseek/workflows/` 注册表、斜杠命令界面)和**脚本持久化到运行目录**(tool-call 事件已经持久记录了脚本)。 +- **嵌套 `workflow()`**、**token `budget`**,以及 `effort`/`isolation`/`agentType` agent 选项(每个都以命名延迟的消息大声拒绝)。 +- **整体运行的挂钟超时**:取消总能释放调用方(result 在宽限期内 settle),因此总运行时间上限是后台重设计的策略旋钮,不是此处的正确性需求。 +- **超越 worker 线程的引擎加固**:在同一 seam 背后使用 isolated-vm 或独立进程引擎(真正的沙箱化;内存限制)。 +- **ACP 进度 UI**(基于 `workflow/*` 事件的 `/workflows` 风格视图);事件已为此而存在。 +- **ACP 后端结构化输出**和 **`toolFilter`**(两者仍以能力标志 `false` 门控)。 ## 曾考虑的替代方案 -- **宿主侧的恶意值防御**(无 trap 代理拒绝、从不调用访问器的描述符遍历、realm 侧预渲染抛出值、realm 构建的 promise/array/error 克隆并带结构化 fatal 识别):否决。每项防御针对的都是信任前提所接受的作者,而线程的序列化边界已经从构造上使跨 realm 值全量化。 -- **进程内 `node:vm` 执行**:机制最简——无 RPC、无线程——但 `start()` 会在脚本首段同步切片期间阻塞调用方,首个 await 之后的同步自旋无法在进程内被杀死(vm `timeout` 仅覆盖首段切片),`dispose()` 只能在宿主循环上放弃一个未 settle 的脚本。worker 线程引擎保持相同的 vm 上下文脚本表面,同时解除宿主阻塞并使终止成为现实。 -- **后台执行作为默认**(Claude Code 的形态):延后。前台同步与 `dsh-tool-subagent` 的当前形态一致,后台语义应在 bash/subagent/workflow 之间统一设计一次,而非逐工具各做一套。 -- **工作流层为 `agent({schema})` 做 JSON 解析**:在一个消费方重复 seam 的关注点,而 seam 的能力标志仍不诚实地为 `false`。 -- **Meta 嵌入脚本内作为 `export const meta = {...}`**(Claude Code 的精确格式):保持脚本自包含且 Claude Code 脚本可直接使用,但获取 meta 需要在宿主上对模型编写的文本求值。即使是空的限时 vm 上下文,在宿主读取结果对象时也无法约束脚本控制的 getter。JSON 参数消除了扫描器、求值和宿主自旋漏洞;代价是 Claude Code 脚本的 meta 头必须移入参数(正文保持可直接使用)。 -- **`SchemaSpec` 作为 outputSchema 类型**:面向作者的 DSL 无法表达以数据形式到达的内容,且无法在不丢失转换精度的情况下对其校验。 -- **schema 对象库(zod 或仓库的 schemastery)用于结构化输出子集**:schema 是协议数据——纯 JSON,跨越 `agent({schema})` 中的 vm realm 边界,逐字落入强制工具的 parameters——正是活 schema 对象无法存在的位置;在运行时消费原始 JSON Schema 需要在其上叠加第三方转换器(zod core 只输出 JSON Schema,不做反向),且会在 schemastery 的配置角色之外引入第二种 schema 语言。 -- **ajv 做值校验**:它校验完整 JSON Schema,因此子集门控——模块的真正要点,因为每个被接受的关键字都必须是 harness 所强制执行的——无论如何仍需手写;它通过 `new Function` 编译校验器;且它将成为 dsh-tools 的首个运行时依赖,所有这些只为替换约 70 行的值遍历器,而路径限定的、报告每一处违规的错误输出无论如何都是自定义的。 -- **提供方 JSON 模式代替捕获工具**:它保证有效 JSON,不保证 schema 一致性,且它与工具调用的交互尚不明确。捕获工具保留了轮次内的校验重试。提供方侧的严格工具 schema 可以在不改变本设计的前提下进一步收窄接受的子集。 +- **宿主侧的恶意值防护**(无 trap 代理拒绝、从不调用访问器的描述符遍历、realm 侧预渲染抛出值、realm 构建的 promise/array/error 克隆加结构化 fatal 识别):否决。每项防御针对的都是信任前提所接受的作者,而线程的序列化边界已经从构造上使跨 realm 值全量化。 +- **进程内 `node:vm` 执行**:机械上最简——无 RPC、无线程——但 `start()` 会在脚本的初始同步切片期间阻塞调用方,第一个 await 之后的同步自旋无法在进程内终止(vm `timeout` 仅覆盖第一个切片),且 `dispose()` 只能在宿主循环上放弃一个未 settle 的脚本。worker 线程引擎保持相同的 vm 上下文脚本表面,同时解除宿主阻塞并使终止成为现实。 +- **后台执行作为默认**(CC 的形态):延迟。前台同步与 `dsh-tool-subagent` 的当前形态一致,后台语义应在 bash/subagent/workflow 之间统一设计一次,而非逐工具设计。 +- **工作流层为 `agent({schema})` 做 JSON 解析**:在一个消费方重复 seam 关注点,而 seam 的能力标志仍不诚实地为 `false`。 +- **Meta 嵌入脚本中作为 `export const meta = {...}`**(CC 的确切格式):保持脚本自包含且 CC 脚本可直接使用,但获取 meta 需要在宿主上执行模型编写的文本。即使一个空的限时 vm 上下文也无法约束脚本控制的 getter(当宿主读取结果对象时)。JSON 参数消除了扫描器、执行和宿主自旋漏洞;代价是 CC 脚本的 meta 头必须移入参数(正文保持可直接使用)。 +- **`SchemaSpec` 作为 outputSchema 类型**:面向作者的 DSL 无法表达以数据形式到达的内容,也无法在不丢失转换精度的情况下对其进行校验。 +- **schema 对象库(zod 或本仓库的 schemastery)用于结构化输出子集**:schema 是协议数据——纯 JSON,跨越 `agent({schema})` 中的 vm realm 边界并逐字落入强制工具的 parameters——正是活 schema 对象无法存在的位置;在运行时消费原始 JSON Schema 需要在其上加一个第三方转换器(zod core 只输出 JSON Schema,不能反向),且会在 schemastery 的配置角色旁边放置第二种 schema 语言。 +- **ajv 用于值校验**:它校验完整 JSON Schema,因此子集门控——模块的真正要点,因为每个被接受的关键字都必须是 harness 强制执行的——无论如何仍需手写;它通过 `new Function` 编译校验器;且它将成为 dsh-tools 的第一个运行时依赖,仅为替换约 70 行的值遍历器,而路径限定的、报告每一处违规的错误报告无论如何都是自定义的。 +- **提供方 JSON 模式代替捕获工具**:它保证有效 JSON,不保证 schema 一致性,且它与工具调用的交互不明确。捕获工具保留了轮次内的校验重试。提供方侧的严格工具 schema 后续可以在不改变本设计的情况下收窄接受的子集。 ## 后果 -扇出计划现在存在于可重新运行的脚本中,`outputSchema` 提供权威的结构化子 agent 结果。每次运行付出 worker 启动和 message-port RPC 的开销,但宿主启动保持非阻塞,取消可以终止 worker,序列化强制执行值边界。Worker 线程不是安全边界。无效选项会失败而非退化为 Claude Code 的 `null`;消费方通过 run 句柄保持控制,观察者仅接收快照。 +扇出计划现在存在于可重运行的脚本中,`outputSchema` 提供权威的结构化子 agent 结果。每次运行付出 worker 启动和 message-port RPC 成本,但宿主启动保持非阻塞,取消可以终止 worker,序列化强制执行值边界。worker 线程不是安全边界。无效选项快速失败而非退化为 Claude Code 的 `null`;消费方通过 run handle 保持控制权,观察者仅接收快照。 diff --git a/docs/rfc/implemented/feature/2026-07-05-skill-system.i18n.yaml b/docs/rfc/implemented/feature/2026-07-05-skill-system.i18n.yaml index 0db4b2ac5a..3b3aed7c50 100644 --- a/docs/rfc/implemented/feature/2026-07-05-skill-system.i18n.yaml +++ b/docs/rfc/implemented/feature/2026-07-05-skill-system.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-05-skill-system.md: 6cfd1f977ae5a1e1ad646a707a4201e57d46bc38 -2026-07-05-skill-system.zh.md: d491899e03854140c93f67d11e4079a5b6525185 +2026-07-05-skill-system.zh.md: f59fd5d850a38d7324b391a116c72c4e71ef7401 diff --git a/docs/rfc/implemented/feature/2026-07-05-skill-system.zh.md b/docs/rfc/implemented/feature/2026-07-05-skill-system.zh.md index d491899e03..f59fd5d850 100644 --- a/docs/rfc/implemented/feature/2026-07-05-skill-system.zh.md +++ b/docs/rfc/implemented/feature/2026-07-05-skill-system.zh.md @@ -1,55 +1,55 @@ # RFC:Skill 系统——面向 agent 的渐进式指令披露 -Status: implemented - [English](2026-07-05-skill-system.md) | 中文 +Status: implemented + ## 问题 -各 agent 产品已趋同于一种 skill 模式:保持请求提示词精简,仅列出可用的指令包,待模型判定任务匹配时再加载完整正文。Codex、Claude Code、OpenCode 和 Kimi Code 在细节上各有不同,但都将发现元数据与完整指令分离,使工作区能承载可复用行为而无需在每个轮次支付全量提示词成本。 +Agent(智能体)产品已趋同于一种 skill(技能)模式:保持请求提示词精简,仅列出可用的指令包,当模型判定某任务匹配时再加载完整正文。Codex、Claude Code、OpenCode 与 Kimi Code 在细节上各有不同,但都将发现元数据与完整指令分离,使工作区能承载可复用的行为而无需在每个轮次支付全量提示词开销。 -DeepSeek Harness 使用同一原语,让项目级的评审指导、插件编写指导和工具使用指导存放在工作区或用户的 agent 配置旁,而非硬编码进 agent loop(智能体循环)。 +DeepSeek Harness 使用同一原语,使项目特定的评审、插件编写和工具使用指南存放在工作区或用户的 agent 配置旁,而非硬编码到 agent loop(智能体循环)中。 ## 决策 -`@deepseek-ai/dsh-skill` 是纯提供方注册表(`ctx.skills`),`@deepseek-ai/dsh-skill-local` 是随附的本地文件系统提供方,`@deepseek-ai/dsh-tool-skill` 负责会话前缀目录和面向模型的 loader 工具。`dsh-agent-spine-demo` 默认加载注册表、本地提供方和消费方,使 stdio 与 ACP 应用获得相同行为,同时嵌入式或远程提供方可在不改动注册表或消费方的前提下贡献 skill。其 `skills` 配置将 `registry`、`local` 和 `tool` 分支分别转发给对应的负责方。 +`@deepseek-ai/dsh-skill` 是纯提供方注册表(`ctx.skills`),`@deepseek-ai/dsh-skill-local` 是随附的本地文件系统提供方,`@deepseek-ai/dsh-tool-skill` 负责会话前缀目录与面向模型的 loader 工具。`dsh-agent-spine-demo` 默认加载注册表、本地提供方和消费方,使 stdio 与 ACP(Agent Client Protocol)应用获得相同行为,同时嵌入式或远程提供方可在不修改注册表或消费方的前提下贡献 skill。其 `skills` 配置将 `registry`、`local` 和 `tool` 分支分别转发给对应的所有者。 -提供方插件在 `apply()` 期间同步注册。提供方成员关系是直接由 effect 持有的状态:注册与 dispose(资源释放)同步地使已完成的目录失效,发现操作按需读取当前提供方映射,而非监听注册表变更事件。提供方目录从 awaited `list()` 调用返回排序后的候选项,远程提供方在此期间执行初始化、认证和发现,同时遵守查找的 abort signal。注册表校验每个候选项,对同名 skill 按 rank、提供方注册顺序和提供方内部顺序执行 first-wins 解析,然后按 skill 名称排序摘要以保证消费方获得确定性结果。注册表仅缓存已完成的目录快照,当提供方/运行时修订版本在发现过程中发生变化时重试,因此 unload 不会将一个陈旧、不可解析的 skill 冻结进会话前缀。运行时 `ctx.skills.register(...)` 仍作为嵌入式进程内 skill 的便捷方式保留,使用 project-over-user 优先级;`runtime` 作为注册表持有的提供方名称被保留。 +提供方插件在 `apply()` 期间同步注册。提供方成员资格是由直接 effect 持有的状态:注册与 dispose(资源释放)同步地使已完成的目录失效,发现操作按需读取当前提供方映射而非监听注册表变更事件。提供方目录从等待的 `list()` 调用返回排序后的候选项,远程提供方在此过程中执行初始化、认证和发现,同时遵守查找的 abort 信号。注册表校验每个候选项,按排名、提供方注册顺序和提供方内部顺序以先到先得方式解决同名 skill 冲突,然后按 skill 名称排序摘要以保证消费方获得确定性结果。它仅缓存已完成的目录快照,并在发现过程中提供方/运行时修订版本发生变化时重试,因此卸载操作不会将一个陈旧且不可解析的 skill 冻结到会话前缀中。运行时 `ctx.skills.register(...)` 仍作为嵌入式进程内 skill 的便捷方式保留,使用 project 优先于 user 的优先级;`runtime` 保留为注册表拥有的提供方名称。 -本地提供方按 first-wins 的 rank 顺序扫描对 cwd 敏感的项目根目录、自定义根目录和用户根目录:项目 `.dsh`、项目 `.agents`、`customSkillDirs`、用户 `.dsh`,然后是用户 `.agents`。用户 `.dsh/skills` 扫描跳过 `.system`,使系统持有的目录不被当作普通用户内容。DeepSeek Harness 不随附内置系统 skill;嵌入式或远程提供方在配置后提供额外 skill。 +本地提供方按先到先得的排名顺序扫描 cwd 敏感的项目根目录、自定义根目录和用户根目录:项目 `.dsh`、项目 `.agents`、`customSkillDirs`、用户 `.dsh`,然后是用户 `.agents`。用户 `.dsh/skills` 扫描跳过 `.system`,以免系统拥有的目录被当作普通用户内容处理。DeepSeek Harness 不随附内置系统 skill;嵌入式或远程提供方在配置后提供额外 skill。 -每个 skill 是 `/SKILL.md` 或带 YAML frontmatter 的 `.md`。`name` 和 `description` 为必填;`whenToUse`、`disableModelInvocation` 和 `metadata` 为可选。名称使用 kebab-case。YAML frontmatter 使用 `yaml` 包解析,而非 `js-yaml` 或手写解析器:`yaml` 是本包有限 frontmatter 需求所声明的现代解析器,手写窄解析器要么拒绝用户期望能正常工作的合法 YAML,要么膨胀为一个未经评审的 YAML 子集。 +每个 skill 是 `/SKILL.md` 或带 YAML frontmatter 的 `.md`。`name` 和 `description` 为必填;`whenToUse`、`disableModelInvocation` 和 `metadata` 为可选。名称采用 kebab-case。YAML frontmatter 使用 `yaml` 包(package)解析,而非 `js-yaml` 或手写解析器:`yaml` 是本包有限 frontmatter 需求已声明的现代解析器,窄解析器要么拒绝用户预期可用的合法 YAML,要么膨胀为一个未经评审的 YAML 子集。 -本地 skill 的文件系统 I/O 在加载了文件系统服务时通过 `ctx.fs` 进行:项目根目录查找使用 `resolve` 和 `stat` 探测 `.git`,根目录发现使用 `listDir`,skill 读取使用 `readText`。对于未挂载 fs seam 的最小上下文,Node 文件系统仍作为回退。缺失的根目录、不可读或格式错误的 skill 文件,以及提供方 `list()` 的瞬态失败均降级为 warn-and-skip,使单个坏源不会导致每个 agent 请求失败;格式错误的候选项仍然快速失败,因为它们违反了提供方契约。 +本地 skill 的文件系统 I/O 在加载了文件系统服务时通过 `ctx.fs` 进行:项目根目录查找使用 `resolve` 和 `stat` 探测 `.git`,根目录发现使用 `listDir`,skill 读取使用 `readText`。Node 文件系统作为后备,供在不挂载 fs seam 的最小上下文中加载 `dsh-skill-local` 时使用。缺失的根目录、不可读或格式错误的 skill 文件、以及提供方 `list()` 的瞬态失败均降级为警告并跳过,使一个坏源不会导致所有 agent 请求失败;格式错误的候选项仍然快速失败,因为它们违反了提供方契约。 -`dsh-tool-skill` 通过 [`agent/session-prefix`](2026-07-07-session-prefix.md) 贡献一条 user-role `` 目录。目录仅包含排序后的 skill 名称和描述;不包含正文、路径、来源、提供方和路由提示。描述经过空白规范化、XML 转义,并受 `catalogDescriptionMaxLength` 限制,其默认值为 `500`,最小值为 `3`。会话前缀 seam 将仅用于请求的目录按 loop 实例冻结,并记录在请求头中,在不将其加入持久化历史的前提下保持可重建性。完整 skill 正文从不包含在目录中。 +`dsh-tool-skill` 通过 [`agent/session-prefix`](2026-07-07-session-prefix.md) 贡献一个 user-role `` 目录。该目录仅包含排序后的 skill 名称与描述;不包含正文、路径、来源、提供方和路由提示。描述经过空白规范化、XML 转义,并受 `catalogDescriptionMaxLength` 上限约束,其默认值为 `500`,最小值为 `3`。session-prefix seam 将仅用于请求的目录按 loop 实例冻结,并记录在请求头中,在不将其加入持久化历史的前提下保持可重建性。完整的 skill 正文从不包含在目录中。 -`skill({ name })` 工具为当前 agent cwd 加载一个完整 skill,返回包含 ``、`` 和 `` 的工具结果。`resourceBase` 提供一个目录、URL 或不透明的提供方管理的基路径,用于显式引用的脚本、参考资料和资产;资源仅按需加载,不进行目录枚举。无法解析的名称报告该 skill 未知或不再可用;无效名称和标记了 `disableModelInvocation` 的 skill 保留不同的工具错误。工具结果是面向模型的披露路径。 +`skill({ name })` 工具为当前 agent cwd 加载一个完整 skill,返回包含 ``、`` 和 `` 的工具结果。`resourceBase` 提供一个目录、URL 或不透明的提供方管理的基路径,用于显式引用的脚本、参考资料和资产;资源仅按需加载,不进行目录枚举。无法解析的名称报告该 skill 未知或不再可用;无效名称和标记了 `disableModelInvocation` 的 skill 保留不同的工具错误。工具结果是面向模型的可见披露路径。 -数据结构与目录/工具契约记录在 [skills.md](../../../core-data-structures/skills.md),服务签名见生成的[服务目录](../../../cordis-catalog/services.md)。 +数据结构与目录/工具契约记录在 [skills.md](../../../core-data-structures/skills.md) 中,服务签名见生成的[服务目录](../../../cordis-catalog/services.md)。 ## 曾考虑的替代方案 **将完整 skill 正文注入每条系统提示词。** 否决,因为这破坏了渐进式披露,使每个请求都为可能不适用的指令付出代价。 -**仅将 skill 暴露为斜杠命令。** 否决,因为模型主动加载才是核心能力;斜杠/ACP 命令广播不改变发现机制。 +**仅以斜杠命令暴露 skill。** 否决,因为模型主动加载是核心能力;斜杠/ACP 命令广播不改变发现机制。 -**将本地文件系统扫描直接放在 `ctx.skills` 内。** 否决,因为编码 agent、Web agent 和未来的插件生态需要不同的 skill 来源。提供方注册表与 subagent seam 同构:注册表负责冲突解析和消费方,实现负责加载。 +**将本地文件系统扫描直接放入 `ctx.skills`。** 否决,因为编码 agent、Web agent 和未来的插件生态需要不同的 skill 来源。提供方注册表与 subagent seam 镜像:注册表拥有冲突解决和消费方,实现拥有加载。 **使用系统提示词段落。** 否决,因为渲染后的系统提示词是单一字符串,而目录是一条具有仅请求生命周期要求的 user-role `` 消息。[`agent/session-prefix`](2026-07-07-session-prefix.md) 是选定的机制:它将目录置于派生历史之前,并将组合后的消息记录在请求头中。 -**将内置 DSH 编写 skill 物化到 `~/.dsh/skills/.system`。** 否决,因为打包的 skill 不应在启动时写入用户主目录,嵌入式或远程提供方在配置后提供 skill。 +**在 `~/.dsh/skills/.system` 下物化内置 DSH 编写 skill。** 否决,因为打包的 skill 不应在启动时写入用户主目录,嵌入式或远程提供方在配置后提供 skill。 -**递归发现嵌套的 `**/SKILL.md`。** 否决。扁平文件和一级目录包已覆盖配置的根目录,同时保持重复处理和目录顺序易于推理。 +**递归发现嵌套的 `**/SKILL.md`。** 否决。扁平文件和一级目录包覆盖了配置的根目录,同时使重复处理和目录顺序易于推理。 -**手写 frontmatter 解析器。** 否决,因为已接受的 schema 包含一个开放的 `metadata` 对象。窄解析器要么拒绝用户期望能正常工作的合法 YAML,要么膨胀为一个未经评审的 YAML 子集。 +**手写 frontmatter 解析器。** 否决,因为已接受的 schema 包含一个开放的 `metadata` 对象。窄解析器要么拒绝用户预期可用的合法 YAML,要么膨胀为一个未经评审的 YAML 子集。 ## 后果 -agent-core 主干包含一个会话前缀贡献者、一个本地提供方和一个面向模型的工具。skill 发现对 cwd 敏感,因此以不同会话 cwd 值创建 agent 的调用方可以按设计观察到不同的项目 skill 覆盖。 +agent-core 主干包含一个 session-prefix 贡献者、一个本地提供方和一个面向模型的工具。Skill 发现是 cwd 敏感的,因此以不同会话 cwd 值创建 agent 的调用方可以按设计观察到不同的项目 skill 覆盖。 -目录在固定的根目录集和运行时注册修订版本下是确定性的,但不监听磁盘变化;发现结果被缓存,直到运行时注册使缓存失效或进程重启。 +目录对于固定的根目录集合和运行时注册修订版本是确定性的,但不监视磁盘变化;发现结果被缓存,直到运行时注册使缓存失效或进程重启。 ## 延后 -fork 式 skill 上下文(`context: fork`)、直接用户/斜杠调用(`user-invocable`)、参数声明与提示(`arguments` 和 `argument-hint`),以及逐 skill 的工具约束(`allowed-tools` 和 `disallowed-tools`)不在已交付的契约范围内。注册表、本地提供方和面向模型的工具不解析、不广播、不强制执行这些字段。 +Fork 的 skill 上下文(`context: fork`)、直接用户/斜杠调用(`user-invocable`)、参数声明与提示(`arguments` 和 `argument-hint`)、以及逐 skill 的工具约束(`allowed-tools` 和 `disallowed-tools`)不在已交付的契约范围内。注册表、本地提供方和面向模型的工具不解析、不广播、也不执行这些字段。 diff --git a/docs/rfc/implemented/feature/2026-07-06-approval-seam.i18n.yaml b/docs/rfc/implemented/feature/2026-07-06-approval-seam.i18n.yaml index 2e89e359db..8d7fa05dfc 100644 --- a/docs/rfc/implemented/feature/2026-07-06-approval-seam.i18n.yaml +++ b/docs/rfc/implemented/feature/2026-07-06-approval-seam.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-06-approval-seam.md: 3ef51c31216bf9f0c5d945748ab3f901ec82147c -2026-07-06-approval-seam.zh.md: cec1692d509a7c9a0680773fbdbbb18e1c90ab8c +2026-07-06-approval-seam.zh.md: 1bf679426a434c36f5363c3b70f13a8f24534df3 diff --git a/docs/rfc/implemented/feature/2026-07-06-approval-seam.zh.md b/docs/rfc/implemented/feature/2026-07-06-approval-seam.zh.md index cec1692d50..1bf679426a 100644 --- a/docs/rfc/implemented/feature/2026-07-06-approval-seam.zh.md +++ b/docs/rfc/implemented/feature/2026-07-06-approval-seam.zh.md @@ -1,4 +1,4 @@ -# RFC:审批 seam——通过应答者瀑布式事件实现一次性权限决策 +# RFC:审批 seam——基于 waterfall(瀑布式事件)应答者的一次性权限决策 Status: implemented @@ -6,17 +6,17 @@ Status: implemented ## 问题 -两个调用方需要向人类提出同一个问题——「这个具体操作可以继续吗?」:`tools/pre-execute` 的 `ask` 决策(包括 Claude-Code 钩子桥的 `permissionDecision: ask`)以及[沙箱 RFC](2026-07-06-sandbox.md) 中拒绝后的一次性升级重试。一个共享的 seam 使它们不必各自发明结果词汇、UI 路由、取消机制和审计追踪,同时保证没有 UI 的部署永远不会批准一个无法应答的请求。 +两个调用方需要向人类提出同一个问题——「这个具体操作可以继续吗?」:`tools/pre-execute` 的 `ask` 决策(包括 Claude-Code 钩子桥的 `permissionDecision: ask`)以及[沙箱 RFC](2026-07-06-sandbox.md) 中拒绝后的一次性升级重试。一个共享的 seam 使它们无需各自发明独立的结果词汇、UI 路由、取消机制和审计轨迹,同时保证没有 UI 的部署永远不会批准一个无法应答的请求。 -路由问题的本质是归属:审批提示必须到达拥有发起请求的 agent 的那个编辑器会话(ACP 桥在一条连接上复用 N 个会话),对无人拥有的 agent(进程内 subagent、测试)默认拒绝(fail-closed),并且不介入没有组合 UI 的部署(无头模式、CI)。 +路由问题的核心是归属:审批提示必须到达拥有发起请求的 agent(智能体)的编辑器会话(ACP(Agent Client Protocol)桥在一条连接上多路复用 N 个会话),对无人拥有的 agent(进程内 subagent、测试)失败关闭,并且不侵入没有组合 UI 的部署(headless、CI)。 ## 决策 -一个包 `dsh-user-approval`(`packages/ui/user-approval`),拥有词汇表和 `ctx.approval` 服务——即机制(MECHANISM)。策略(POLICY)——谁来应答、以及某个会话是否被询问——位于其外部:应答者是 `approval/request` waterfall(瀑布式事件)监听器,由拥有通道的插件注册(ACP 桥、未来的终端 UI、测试脚本),而每会话的策略层可以在任何人类介入之前做出决定。消费方(`dsh-tools` 的 ask 路由、沙箱升级门禁)将问题解析为一个封闭的结果,并从中派生各自的工具结果。刻意只用一个包,而非能力 seam 的三包拆分(见「曾考虑的替代方案」)。 +一个包 `dsh-user-approval`(`packages/ui/user-approval`),拥有词汇表和 `ctx.approval` 服务——即**机制**。**策略**——谁来应答、某个会话是否需要被询问——不在其中:应答者是 `approval/request` waterfall 监听器,由拥有通道的插件注册(ACP 桥、未来的终端 UI、测试脚本),而每会话的策略层可以在任何人类介入之前做出决定。消费方(`dsh-tools` 的 ask 路由、沙箱升级门禁)将问题解析为一个封闭结果,并从中派生各自的工具结果。刻意设计为**一个**包,而非能力 seam 的三包拆分(见「替代方案」)。 ### 部署如何使用它 -一条 `cordis.yml` 条目挂载该 seam。不加载它即为 fail-closed 退出方式:消费方在没有注册任何审批代码的情况下拒绝无法应答的请求。 +一条 `cordis.yml` 条目挂载该 seam。不加载它就是失败关闭的退出方式:消费方在没有注册任何审批代码的情况下拒绝无法应答的请求。 ```yaml - id: approval @@ -25,11 +25,11 @@ Status: implemented # policy: never # deployment default for sessions without an override; 'ask' when omitted ``` -仅有这条条目提供的是机制而非通道:没有组合应答者时,每次 ask 解析为 `unavailable`,发起 ask 的工具调用被拒绝——默认拒绝无需配置。组合 ACP 应用(`@deepseek-ai/dsh-acp-demo`,如 [acp-agent 示例的默认树](../../../../examples/acp-agent/README.md))即可闭合回路:其桥注册一个应答者,通过 `session/request_permission` 向拥有该会话的编辑器发出提示,于是钩子的 `ask` 或升级请求会以一次性 Allow/Reject 提示的形式出现在已流式输出的工具调用上。`policy: never` 是无人值守姿态——每次 ask 确定性地自动拒绝,在系统提示词中声明,无人类参与。`policy` 在插件加载时针对封闭列表做校验;其他值直接抛异常。 +仅有这条条目只提供机制,不提供通道:没有组合应答者时,每次 ask 都解析为 `unavailable`,发起请求的工具调用被拒绝——失败关闭无需配置。组合 ACP 应用(`@deepseek-ai/dsh-acp-demo`,如 [acp-agent 示例的默认树](../../../../examples/acp-agent/README.md))即可闭环:其桥注册一个应答者,通过 `session/request_permission` 向拥有该会话的编辑器发出提示,于是钩子的 `ask` 或升级请求会以一次性 Allow/Reject 提示的形式呈现,附着在已流式输出的工具调用上。`policy: never` 是无人值守姿态:每次 ask 确定性地自动拒绝,在系统提示词中声明,无人类参与。`policy` 在插件加载时对照封闭列表校验;非法值直接抛异常。 -组合后的部署观察到的行为:`allowed-once` 仅允许该次调用继续;拒绝、关闭和通道缺失以三种不同的原因拒绝,模型可以区分它们;每次 ask 都在发起请求的 agent 的会话日志上落一对持久的 `approval/asked`/`approval/decided`;授权不会在发起请求的那次调用之后持续存在。 +组合部署的可观测行为:`allowed-once` 仅允许该次调用继续;拒绝、关闭和通道缺失以三种不同原因拒绝,模型可以区分;每次 ask 在发起请求的 agent 的会话日志上落一对持久的 `approval/asked`/`approval/decided` 事件;授权不会在发起请求的调用结束后继续存在。 -以下是在此组合下的一次 ask,逐字取自沙箱示例录制的 `escalation-approved` 场景——模型请求沙箱升级,门禁发起 ask,桥向拥有该会话的编辑器发出提示,用户点击 Allow once: +以下是该组合下的一次 ask,逐字取自沙箱示例录制的 `escalation-approved` 场景——模型请求沙箱升级,门禁发起 ask,桥向拥有该会话的编辑器发出提示,用户点击 Allow once: ``` tool/call bash {"command": "printf 'escalated\n' > escalated.txt && cat escalated.txt", @@ -45,94 +45,94 @@ approval/decided {"outcome": "allowed-once"} tool/result "escalated" — this one call ran under the wider mode; the grant died with it ``` -`escalation-rejected` 的孪生场景以 `{"outcome": "rejected"}` 结束:什么都不执行,模型的结果携带发起方逐字的 fail-closed 文本(`the user rejected escalating this command to "workspace-write"`)。钩子的 `permissionDecision: ask` 走完全相同的协议;只有发起方和拒绝文本不同(§ dsh-tools 中的 Ask 路由)。无头模式下,同一请求完全跳过提示并以 `unavailable` 结算。 +`escalation-rejected` 孪生场景以 `{"outcome": "rejected"}` 结束:不执行任何操作,模型的结果携带发起方的逐字失败关闭文本(`the user rejected escalating this command to "workspace-write"`)。钩子的 `permissionDecision: ask` 走完全相同的协议;只有发起方和拒绝文本不同(§ dsh-tools 中的 Ask 路由)。在 headless 环境下,同一请求完全跳过提示,直接结算为 `unavailable`。 ### 设计细节 #### seam:机制与策略分离 -经过校验并追加 `approval/asked` 后,`request()` 解析为 `allowed-once`、`rejected`、`cancelled` 或 `unavailable`。服务借用只读请求、运行应答者 waterfall、与取消竞争,并将抛出异常或无效应答归一化为 `unavailable`。随后追加匹配的 `approval/decided`,通过 `ApprovalRequestId` 配对。 +经过校验并追加 `approval/asked` 后,`request()` 解析为 `allowed-once`、`rejected`、`cancelled` 或 `unavailable`。服务借用只读请求,运行应答者 waterfall,与取消竞速,并将抛出异常或无效应答规范化为 `unavailable`。然后追加匹配的 `approval/decided`,以 `ApprovalRequestId` 配对。 -两个审计事件都必须在一个打开的轮次内;接受或 pre-commit 追加失败会拒绝该请求。Post-commit 观察者被会话所包含。`allowed-once` 仅授权所请求的操作,服务不保留任何授权状态。 +两个审计事件都必须在一个打开的轮次内;接受或预提交追加失败会拒绝该请求。提交后的观察者由会话容纳。`allowed-once` 仅授权所请求的操作,服务不保留任何授权状态。 -应答者是 `approval/request` waterfall 监听器。监听器为其拥有的 agent 返回结果,否则调用 `next()`。没有应答者时默认为 `unavailable`;因此卸载 UI 即默认拒绝,不会留下通道。由于兄弟插件的注册顺序不确定,部署应组合一个终端应答者,仅对「决定或委托」门禁使用 `prepend`。 +应答者是 `approval/request` waterfall 监听器。监听器为它拥有的 agent 返回结果,否则调用 `next()`。没有应答者时默认为 `unavailable`;因此卸载 UI 即失败关闭,不会留下悬空通道。由于兄弟插件的注册顺序不确定,部署应组合一个终端应答者,仅对「先决策或委派」门禁使用 `prepend`。 `ApprovalRequest` 携带 agent、工具名、可选的 `callId`、原因和 signal。agent 同时路由提示和审计事件。请求使用 `dsh-llm` 的 `CallId` 而不导入 `dsh-tools`,避免包循环。工具参数被省略,因为 UI 应答者附着在已渲染的调用上。 #### dsh-tools 中的 Ask 路由 -`ToolRegistry.execute()` 在拒绝路径之前将 `ask` 发送到审批 seam。只有 `allowed-once` 才继续执行;拒绝、取消和通道不可用产生三种模型可见的不同原因。注册表按调用查找可选服务,因此缺失或未加载的服务默认拒绝,不会阻塞注册表 fiber。无 agent 的执行同样默认拒绝,因为无法路由或审计。 +`ToolRegistry.execute()` 在进入拒绝路径之前,将 `ask` 发送到审批 seam。只有 `allowed-once` 才继续执行;拒绝、取消和通道不可用产生三种模型可见的不同原因。注册表按调用查找可选服务,因此服务缺失或未加载时失败关闭,不会阻塞注册表 fiber。无 agent 的执行同样失败关闭,因为无法路由或审计。 #### 每会话策略层 -seam 拥有会话策略 `'ask' | 'never'`,遵循[沙箱 RFC](2026-07-06-sandbox.md) 中的切换契约。生效的会话或配置策略在应答者之前应用:`'never'` 在 `request()` 内部拒绝,而 `'ask'` 派发请求,无人应答时降级为 `unavailable`。系统提示词仅声明确定性的 `'never'`;叙述者报告切换,每个请求仍然收到其审计对。 +seam 拥有会话策略 `'ask' | 'never'`,遵循[沙箱 RFC](2026-07-06-sandbox.md) 中的切换契约。生效的会话或配置策略在应答者之前应用:`'never'` 在 `request()` 内部直接拒绝,`'ask'` 则派发请求,无人应答时降级为 `unavailable`。提示词仅声明确定性的 `'never'`;叙述者报告切换,每个请求仍收到其审计对。 #### ACP 应答者 -ACP 桥找到拥有该会话的编辑器,为该 `callId` 发送 `session/request_permission`,并将一次性 allow、reject 和 cancel 响应映射到 seam 词汇。未知选项永远不授权。外部 agent 和没有 `callId` 的请求通过 `next()` 委托;RPC 失败变为 `unavailable`。桥应答请求但不决定哪些调用需要审批。 +ACP 桥找到拥有该会话的编辑器,为该 `callId` 发送 `session/request_permission`,并将一次性 allow、reject、cancel 响应映射到 seam 词汇。未知选项永远不授权。外部 agent 和没有 `callId` 的请求通过 `next()` 委派;RPC 失败变为 `unavailable`。桥应答请求,但不决定哪些调用需要审批。 -应答者通过 [ACP 支持 RFC](../../implemented/feature/2026-06-14-acp-agent-client-protocol.md) 描述的桥反向映射归属 seam 进行路由,实现了[多会话 RFC](../../implemented/feature/2026-06-14-acp-multi-session.md) 所要求的每会话权限归属。 +应答者通过 [ACP 支持 RFC](../../implemented/feature/2026-06-14-acp-agent-client-protocol.md) 描述的桥反向映射归属 seam 进行路由,实现了[多会话 RFC](../../implemented/feature/2026-06-14-acp-multi-session.md) 要求的每会话权限归属。 #### 审计,以及模型看到什么 -`approval/asked` 和 `approval/decided` 是持久的仅日志事件。模型只看到发起方记录的 `tool/result`。每个被接受的请求追加一条匹配的决策,包括取消和被包含的应答者失败。 +`approval/asked` 和 `approval/decided` 是持久的仅日志事件。模型只看到发起方派生的已记录 `tool/result`。每个被接受的请求追加一条匹配的决策,包括取消和被容纳的应答者失败。 #### 实体与依赖 -`dsh-user-approval` 拥有固定的派发与审计机制;`dsh-tools` 发起请求,`dsh-acp` 应答。可替换的应答者作为监听器留在其通道拥有者插件中,因此三包能力拆分只会增加一个空的实现层。沙箱执行器仍然只负责传输,静态能力授权与交互式审批保持分离。 +`dsh-user-approval` 拥有固定的派发与审计机制;`dsh-tools` 发起请求,`dsh-acp` 应答。可替换的应答者作为监听器留在其通道拥有者插件中,因此三包能力拆分只会多出一个空的实现层。沙箱执行器仍然只负责传输,静态能力授权与交互式审批保持分离。 ### 测试 -- **单元/集成测试:** 覆盖先到先得的委托、fail-closed 默认值、格式错误和抛异常的应答者、取消竞争与迟到应答丢弃、观察者失败下的审计配对、不可绕过的 `'never'`、不同的工具拒绝原因,以及 ACP 每会话路由/结果映射。 -- **快照测试:** 通过沙箱升级的两个分支编排权限应答并固定 `'never'` 提示词加策略切换通知。没有组合应答者时钩子产生的 ask 仍作为 fail-closed 拒绝被覆盖。 +- **单元/集成测试:** 覆盖先到先得的委派、失败关闭默认值、畸形和抛异常的应答者、取消竞速与迟到应答丢弃、观察者失败时的审计配对、不可绕过的 `'never'`、不同的工具拒绝原因,以及 ACP 每会话路由/结果映射。 +- **快照测试:** 对沙箱升级的两个分支编排权限应答并固定 `'never'` 提示词加策略切换通知。无组合应答者时钩子产生的 ask 仍作为失败关闭拒绝被覆盖。 ## 延后 -- **`allow_always` 授权存储**——兑现持久授权意味着设计存储、范围标识(调用?路径?前缀?会话?时间窗口?)和撤销;在设计完成之前,只宣告一次性选项([沙箱 RFC](2026-07-06-sandbox.md) § 升级记录了开放的范围问题)。 -- **有组合应答者时录制的钩子产生的 ask**——升级录制了人类提示的协议格式(wire format),而当前钩子 fixture(测试前置数据)固定的是无服务拒绝;它们组合的生产者/应答者路径仍由单元测试覆盖。 -- **将子 agent 的审批路由到父会话**——`subagent-acp` 的子端自动应答自己的 `permission` 请求;将它们呈现给父端编辑器是独立的设计。 +- **`allow_always` 授权存储**:兑现持久授权意味着设计存储、作用域标识(调用?路径?前缀?会话?时间窗口?)和撤销;在设计完成之前,只展示一次性选项([沙箱 RFC](2026-07-06-sandbox.md) § Escalation 记录了开放的作用域问题)。 +- **有组合应答者时录制的钩子产生的 ask**:升级场景录制了人类提示的协议格式(wire format),而当前钩子 fixture(测试前置数据)固定的是无服务拒绝;二者组合的生产者/应答者路径仍由单元测试覆盖。 +- **将子 agent 的审批路由到父会话**:`subagent-acp` 的子侧自动应答自己的 `permission` 请求;将其呈现给父会话的编辑器是独立的设计。 ## 曾考虑的替代方案 -- **单个注册提供方而非 waterfall 监听器**:否决。`registerProvider()` 接口迫使所有组合问题——白名单预过滤、外部钩子决策者、脚本化测试应答、人类前面的策略门禁——都塞进一个提供方实现。waterfall 从运行时已有的机制中获得组合能力、缺失时默认拒绝和 HMR(热模块替换) dispose(资源释放);seam 的 JSDoc 用约定固定单决策槽语义,而非发明一个提供方注册表。 -- **在 ACP 桥中内联 `tools/pre-execute` 权限门禁**:否决。对桥拥有的每次调用都弹出提示,会把发起 ask 的策略硬编码到 UI 插件中,无法服务第二个发起方(沙箱升级发生在执行开始之后,没有 pre-execute 时机),且让钩子产生的 `ask` 决策没有共享机制。 -- **通用用户交互 seam(`ctx.userInteraction`)**:否决作为审批机制。两者共享骨架(按 agent 路由、阻塞等待人类、处理缺失),但审批的契约在每个关键维度上都更窄:封闭的结果词汇而非自由文本、附着在工具调用上的协议原生提示而非通用表单、强制的缺失时默认拒绝、以及审计事件。因此审批不走已发布的 `packages/ui/user-interaction` / `ask_user_question` 引出路径——引出表单不是权限提示,自由文本应答不是封闭结果;如果两者未来趋同,共享提供方管道仍然开放。 -- **在 `dsh-tools` 中静态可选注入**:否决。vendor 的 cordis `Inject` 类型没有可选标志——对象形式将服务名映射到拦截配置,声明的 inject 会阻塞 fiber。`ctx.get('approval')` 是文档化的机会性消费模式(`tool-bash` 的 owner-token 查找、loop 的持久化探测),按调用读取存在性,在 HMR 下无需额外机制即可正确降级。 -- **能力 seam 的三包拆分**:否决。接口/实现/消费方适合实现可替换的 seam(bash-local vs bash-sandbox)。这里服务体是固定机制,可变部分是留在各自通道拥有者中的监听器——拆分只会制造一个空的实现包(「不要预防性拆分」)。 -- **现在就提供 `allow_always`**:否决。协议可以表达它,但兑现它意味着设计授权存储、范围标识和撤销(§ 延后)。宣告一个 harness 无法兑现的选项只会制造注定失败的授权。 +- **单一注册提供方而非 waterfall 监听器**:否决。`registerProvider()` 接口迫使所有组合问题——允许列表预过滤、外部钩子决策者、脚本化测试应答、人类前面的策略门禁——都塞进一个提供方实现。waterfall 从运行时已有的机制中获得组合能力、缺失时失败关闭和 HMR(热模块替换) dispose(资源释放);seam 的 JSDoc 以约定固定单决策槽语义,而非发明一个提供方注册表。 +- **在 ACP 桥中内联 `tools/pre-execute` 权限门禁**:否决。对桥拥有的每次调用都弹出提示,会将请求**策略**硬编码进 UI 插件,无法服务第二个发起方(沙箱升级发生在执行开始之后,没有 pre-execute 时刻),且钩子产生的 `ask` 决策没有共享机制。 +- **通用用户交互 seam(`ctx.userInteraction`)**:否决作为审批机制。二者骨架相似(按 agent 路由、阻塞等待人类、处理缺失),但审批的契约在每个关键维度上都更窄:封闭的结果词汇而非自由文本、附着在工具调用上的协议原生提示而非通用表单、强制的缺失时失败关闭、以及审计事件。因此审批不走已交付的 `packages/ui/user-interaction` / `ask_user_question` 引出路径——引出表单不是权限提示,自由文本应答不是封闭结果;如果二者将来趋同,共享提供方管道仍然开放。 +- **`dsh-tools` 中的静态可选注入**:否决。vendor 的 Cordis `Inject` 类型没有 optional 标志——对象形式将服务名映射到拦截配置,声明的 inject 会阻塞 fiber。`ctx.get('approval')` 是文档化的机会性消费模式(`tool-bash` 的 owner-token 查找、loop 的持久化探测),按调用读取存在性,跨 HMR 正确降级,无需额外机制。 +- **能力 seam 的三包拆分**:否决。接口/实现/消费方适合实现可替换的 seam(bash-local vs bash-sandbox)。此处服务体是固定机制,可变部分是留在各自通道拥有者插件中的监听器——拆分只会制造一个空的实现包(「不要预防性拆分」)。 +- **现在就提供 `allow_always`**:否决。协议能表达它,但兑现它意味着设计授权存储、作用域标识和撤销(§ 延后)。展示 harness 无法兑现的选项只会制造注定失败的授权。 ## 后果 -- 只有 `allowed-once` 才会派发被询问的操作;缺失、拒绝、取消或应答失败的路径均拒绝。 +- 只有 `allowed-once` 才会派发被询问的操作;缺失、拒绝、取消或应答失败的路径一律拒绝。 - 会话归属路由提示、策略和审计事件,不跨越编辑器会话。 - 被接受的请求追加一对持久审计事件;模型只看到最终的工具结果。 -- 没有加载该服务的部署不会发出审批提示或审计事件,并在工具边界拒绝每个 `ask`。 +- 没有该服务的部署不产生审批提示或审计事件,在工具边界拒绝每一个 `ask`。 代价与已接受的局限: -- **两个急于决策的应答者争抢同一个槽位。** 兄弟插件的监听器顺序不确定,seam 无法仲裁竞争的终端应答者——通过约定缓解(每个部署一个终端应答者;仅对「决定或委托」门禁使用 `prepend`),而非事件总线不具备的优先级机制。 -- **生产环境的验证依赖单一组合。** `ask` 有两个生产者家族——钩子桥通过 `tools/pre-execute`,以及沙箱升级通过其自身门禁——协议格式录制在沙箱示例的快照套件中,因此 seam 的真实覆盖率就是这一种组合,直到更多部署组合它。 -- **归属以 `Agent` 对象同一性为键。** 应答者通过桥现有的 WeakMap 解析会话;当前所有路径在 loop 和各 seam 之间传递同一个对象,但未来如果某个边界克隆或代理了 agent,桥会委托并默认拒绝——安全但静默无 UI——届时需要改用 session-id 匹配。 +- **两个急于决策的应答者竞争同一槽位。** 兄弟插件的监听器顺序不确定,seam 无法仲裁竞争的终端应答者。通过约定缓解(每个部署一个终端应答者;仅对「先决策或委派」门禁使用 `prepend`),而非事件总线不具备的优先级机制。 +- **生产环境验证依赖单一组合。** `ask` 有两个生产者家族——钩子桥通过 `tools/pre-execute`,沙箱升级通过自己的门禁——协议格式录制在沙箱示例的快照套件中;因此在更多部署组合它之前,seam 的真实覆盖面就是这一种组合。 +- **归属以 `Agent` 对象标识为键。** 应答者通过桥已有的 WeakMap 解析会话;当前所有路径在 loop 和各 seam 之间传递同一对象,但未来如果某个边界克隆或代理了 agent,桥会委派并失败关闭——安全,但静默无 UI——届时需要改用 session-id 匹配。 ## FAQ -- **在完全没有应答者的部署中(无头模式、CI)会发生什么?** 每次 ask 穿过空的 waterfall 降级为 `unavailable`,工具调用以「no approval channel is available」原因被拒绝。默认拒绝是零监听器的默认行为,不是配置。 -- **授权能持久化吗——「始终允许」?** 不能。`allowed-once` 仅授权单次被询问的操作,服务在请求之间不存储任何东西;`allow_always` 在授权存储设计完成之前刻意不宣告(§ 延后)。 -- **模型看到审批的什么?** 只看到发起方从结果派生的工具结果——审计对永远不进入 transcript(文本记录)。三种非授权原因各不相同,模型可以区分人类说「不」、提示被关闭、以及通道缺失。 -- **谁决定一次调用是否首先发起 ask?** 策略生产者:返回 `permissionDecision: ask` 的钩子、任何 `tools/pre-execute` 监听器、或沙箱升级门禁。seam 和桥只负责路由和应答;两者都不注入自己对「什么值得弹出提示」的判断。 -- **用户关闭提示或轮次在 ask 进行中中止时会发生什么?** 关闭映射为 `cancelled`,有自己的拒绝文本。已中止的 signal 以 `cancelled` 结算而不派发;ask 进行中的中止丢弃迟到的应答——无论如何只有一对审计事件,绝不会有两对。 -- **如果客户端以 harness 从未提供的选项应答会怎样?** 除已提供的 `allow_once` 之外的任何选项都映射为 `rejected`——来自不合规客户端的未知 optionId 永远不能授权。 -- **subagent 的审批如何路由?** 没有应答者拥有的 agent 穿过整个 waterfall 委托并默认拒绝——进程内 subagent 被刻意设计为不可应答。`subagent-acp` 子端的自动应答是独立的;将子端的 ask 路由到父端编辑器已延后(§ 延后)。 -- **`policy: 'never'` 在运行时实际改变了什么?** 服务在派发任何应答者之前将该会话的每次 ask 解析为 `rejected`(在服务内部,因此没有注册顺序能绕过它);系统提示词声明该策略;切换在边界处被叙述;每次自动拒绝仍然落一对审计事件。 +- **在完全没有应答者的部署中(headless、CI)会发生什么?** 每次 ask 穿过空的 waterfall 降级为 `unavailable`,工具调用以「no approval channel is available」原因被拒绝。失败关闭是零监听器的默认行为,不是配置。 +- **授权能持久化吗——「始终允许」?** 不能。`allowed-once` 仅授权单次被询问的操作,服务在请求之间不存储任何内容;`allow_always` 在授权存储设计完成之前刻意不展示(§ 延后)。 +- **模型看到审批的什么?** 只看到发起方从结果派生的工具结果——审计对永远不进入 transcript(文本记录)。三种非授权原因各不相同,模型可以区分人类说「不」、提示被关闭、通道缺失。 +- **谁决定一次调用是否需要 ask?** 策略生产者:返回 `permissionDecision: ask` 的钩子、任何 `tools/pre-execute` 监听器、或沙箱升级门禁。seam 和桥只负责路由和应答;二者都不注入自己对「什么值得弹出提示」的判断。 +- **用户关闭提示或轮次在 ask 进行中中止时会发生什么?** 关闭映射为 `cancelled` 并携带自己的拒绝文本。已中止的 signal 直接结算为 `cancelled` 而不派发;ask 进行中的中止丢弃迟到的应答——无论哪种情况都恰好一对审计事件,绝不会两对。 +- **如果客户端以 harness 从未提供的选项应答呢?** 除已提供的 `allow_once` 之外的任何选项都映射为 `rejected`——来自不合规客户端的未知 optionId 永远不能授权。 +- **subagent 的审批如何路由?** 没有应答者拥有的 agent 穿过整个 waterfall 委派并失败关闭——进程内 subagent 被刻意设计为不可应答。`subagent-acp` 的子侧自动应答是独立的;将子 agent 的 ask 路由到父会话的编辑器已延后(§ 延后)。 +- **`policy: 'never'` 在运行时实际改变了什么?** 服务在派发任何应答者之前,将该会话的每次 ask 解析为 `rejected`(在服务内部,因此没有注册顺序能绕过它);系统提示词声明该策略;切换在边界处被叙述;每次自动拒绝仍落一对审计事件。 - **热重载或 UI 插件在会话中途卸载时会发生什么?** 应答者随其拥有的 fiber 一起 dispose,因此下一次 ask 降级为 `unavailable` 而非挂在死通道上;重新挂载会重新注册应答者,无需追赶状态。 -- **用户在哪里看到自己在批准什么?** 在工具调用本身上:提示通过 `callId` 附着在已流式输出的调用上(包含参数),并添加发起方的人类可读 `reason`;请求本身不携带参数副本。 +- **用户在哪里看到自己在批准什么?** 在工具调用本身:提示通过 `callId` 附着在已流式输出的调用上(包含参数),并添加发起方的人类可读 `reason`;请求本身不携带参数副本。 ## 先例 -本设计复用或对比的仓库内先例: +本设计复用或对照的仓库内先例: -- `fs/write-intent` 门禁(`packages/fs/fs/`)——文档化的单占位决策槽 waterfall 语义(先到先得、通过 `next()` 委托),应答者契约复用了它。 -- `hook/invoked`/`hook/result`——仅日志审计对先例,`approval/asked`/`approval/decided` 沿用了它;[钩子桥 RFC](2026-06-30-hook-bridges.md) 发布了 `permissionDecision: ask`,即第一个生产者。 +- `fs/write-intent` 门禁(`packages/fs/fs/`)——文档化的单占用决策槽 waterfall 语义(先到先得,通过 `next()` 委派),应答者契约复用了它。 +- `hook/invoked`/`hook/result`——仅日志审计对先例,`approval/asked`/`approval/decided` 沿用了它;[钩子桥 RFC](2026-06-30-hook-bridges.md) 交付了 `permissionDecision: ask`,即第一个生产者。 - [拦截 seam RFC](2026-06-30-interception-seams.md)——`tools/pre-execute` 的 `allow`/`deny`/`ask` 词汇,本 seam 服务其中的 `ask`。 - [ACP 支持 RFC](../../implemented/feature/2026-06-14-acp-agent-client-protocol.md)——应答者路由所经过的 `WeakMap` 归属 seam;[多会话 RFC](../../implemented/feature/2026-06-14-acp-multi-session.md)——本设计实现的每会话权限归属阻塞项。 - 机会性 `ctx.get()` 消费模式(`tool-bash` 的 owner-token 查找、loop 的持久化探测)——`dsh-tools` 消费该 seam 而不阻塞其 fiber 的方式。 diff --git a/docs/rfc/implemented/feature/2026-07-06-explicit-tool-order.i18n.yaml b/docs/rfc/implemented/feature/2026-07-06-explicit-tool-order.i18n.yaml index 352331b13d..0e418c8e57 100644 --- a/docs/rfc/implemented/feature/2026-07-06-explicit-tool-order.i18n.yaml +++ b/docs/rfc/implemented/feature/2026-07-06-explicit-tool-order.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-06-explicit-tool-order.md: 9d94496ffdcbc4c7df820581b02e3e075ec1c0be -2026-07-06-explicit-tool-order.zh.md: fa3a5bbf83115c25f87471b8a3847b9347d42934 +2026-07-06-explicit-tool-order.zh.md: 0b020f969299799289e18ed93c81db08cabc0b2d diff --git a/docs/rfc/implemented/feature/2026-07-06-explicit-tool-order.zh.md b/docs/rfc/implemented/feature/2026-07-06-explicit-tool-order.zh.md index fa3a5bbf83..0b020f9692 100644 --- a/docs/rfc/implemented/feature/2026-07-06-explicit-tool-order.zh.md +++ b/docs/rfc/implemented/feature/2026-07-06-explicit-tool-order.zh.md @@ -1,50 +1,50 @@ # RFC:显式的模型侧工具顺序 -Status: implemented - [English](2026-07-06-explicit-tool-order.md) | 中文 +Status: implemented + ## 问题 -模型侧的工具顺序此前跟随插件注册顺序,而注册顺序取决于彼此独立的插件在并发模块加载时的竞态。这一竞态导致 CI 和快照录制中产生不同的请求头。由于顺序影响请求字节、缓存和持久化的头部,需要一个显式的确定性策略。 +模型侧的工具顺序此前跟随插件注册顺序,而注册顺序取决于相互独立的插件的并发模块加载。这种竞态在 CI 和快照录制中产生了不同的请求头。由于顺序影响请求字节、缓存和持久化的 header,因此需要一个显式的确定性策略。 ## 决策 -系统提示词组装拥有模型侧工具的权威顺序,正如它已经拥有 section 顺序一样。`dsh-system-prompt` 上的 `toolOrder?: string[]` 是可选的显式策略: +系统提示词的组装逻辑拥有模型侧工具顺序的权威定义,正如它已经拥有 section 顺序的权威定义一样。`dsh-system-prompt` 上的 `toolOrder?: string[]` 是可选的显式策略: -- 列表中已注册的工具取其列出的位置。 -- 列表中的名称没有对应的已注册工具,属于配置错误。形状错误(缺少 rest 条目或名称重复)在服务构造器中快速失败;未注册的名称在每次 `assemble()` 时拒绝——这是已注册工具集存在可供检查的最早时刻(工具插件在服务构造之后注册),也是唯一的通用时刻(注册随时可能变化;Cordis 没有「所有插件已加载」事件)。在已交付的 agent loop 下,第一个轮次在任何模型请求之前就会失败——确切的影响范围见下文「后果」。 -- 已注册但不在列表中的工具,插入到 `''` rest 条目(`TOOL_ORDER_REST`)处,在其他未列出的工具之间按名称字典序排列。 -- 任何已收集的工具不得使用 `TOOL_ORDER_REST` 作为其 `ToolSchema.name`;组装在排序之前就会拒绝该保留名称。 -- 列表必须恰好包含一个 rest 条目,且名称不得重复。 +- 列表中已注册的工具按列表位置排列。 +- 列表中的名称没有对应的已注册工具,属于配置错误。形状错误(缺少 rest 条目或名称重复)在服务构造器中快速失败;未注册的名称则在每次 `assemble()` 时拒绝——这是已注册工具集存在并可供检查的最早时刻(工具插件在服务构造之后才注册),也是唯一的通用时刻(注册随时可能变化;Cordis 没有「所有插件已加载」事件)。在已交付的 agent loop(智能体循环)下,第一个轮次在发出任何模型请求之前就会失败——确切的影响范围见下文「后果」。 +- 已注册但不在列表中的工具,插入到 `''` rest 条目(`TOOL_ORDER_REST`)的位置,与其他未列出的工具按名称字典序排列。 +- 任何已收集的工具不得使用 `TOOL_ORDER_REST` 作为其 `ToolSchema.name`;组装逻辑在排序之前就会拒绝这个保留名称。 +- 列表必须恰好包含一个 rest 条目,且不得有重复名称。 - 当 `toolOrder` 未设置时,权威顺序为纯字典序(code-unit 比较,与 locale 无关),因此无需配置即可保证确定性。 -`assemble()` 在 `system-prompt/assemble` waterfall(瀑布式事件)之前规范化提供方工具,从源头消除注册顺序差异。waterfall 从这个确定性列表出发;未被改变的顺序随后流入请求头、冻结请求和重建检查,无需循环特有的排序逻辑。 +`assemble()` 在 `system-prompt/assemble` waterfall(瀑布式事件)之前对提供方工具进行规范化排序,从源头消除注册顺序的差异。waterfall 从这个确定性列表开始;不变的顺序随后流入请求头、冻结的请求和重建检查,无需 loop 特有的排序逻辑。 -范围刻意收窄:本 RFC 修复的是注册顺序竞态,而非插件行为。`system-prompt/assemble` 的监听器仍可添加、移除或重排工具——正如它可以在 section 排序之后编辑 section——并对自身输出的确定性负责;waterfall 契约已要求监听器具有确定性(可重建性不变式会捕获在构建与回放之间表现不一致的监听器)。 +范围刻意收窄:本 RFC 修复的是注册顺序竞态,而非插件行为。`system-prompt/assemble` 的监听器仍然可以添加、移除或重排工具——正如它可以在 section 排序之后编辑 section——并对自身输出的确定性负责;waterfall 契约已经要求监听器是确定性的(可重建性不变式会捕获在构建与回放之间行为不一致的监听器)。 -配置传递沿用 `persona` 的先例,`toolOrder` 与它并列:应用配置(`dsh-stdio-demo`、`dsh-acp-demo`)接受该键,并通过 `dsh-agent-spine-demo`(其 schema 是各所有者 schema 的交集)转发给 `SystemPrompt` 子服务。有一个 schemastery 细节是关键的:schemastery 数组默认为 `[]`,但省略的 `toolOrder` 必须保持 ABSENT(= 字典序),而不是变成一个显式配置的空列表(无效——缺少 rest 条目),因此链上的每个 schema 都将默认值强制为 `undefined`。 +配置传递沿用 `persona` 的先例,`toolOrder` 与之并列:应用配置(`dsh-stdio-demo`、`dsh-acp-demo`)接受该键,并通过 `dsh-agent-spine-demo`(其 schema 是各所有者 schema 的交集)转发给 `SystemPrompt` 子服务。有一个 schemastery 细节至关重要:schemastery 数组默认为 `[]`,但省略的 `toolOrder` 必须保持 ABSENT(= 字典序),而不是变成一个显式配置的空列表(无效——缺少 rest 条目),因此链路上每个 schema 都将默认值强制为 `undefined`。 ## 曾考虑的替代方案 -- **注册顺序(现状)**:并发导入竞态,依赖宿主环境(上述 CI 不稳定),评审中不可见。 -- **插件依赖图的线性化**:该关系是偏序的,独立的工具插件之间不可比较;上述不稳定发生时偏序已完全满足。 -- **每个插件在工具贡献上设 `weight`**:将顺序分散到各插件中,仍需一个无人拥有的全局编号约定(section 的 `order` 分段已经展示了这种协调成本需要手工承担)。 -- **在 `ToolRegistry.schemas()` 中排序(注册表层)**:同样确定,但注册表是一个被组装之外的更多消费方使用的成员存储;排序是 prompt 组合的关注点,而组装已经拥有 section 的组合策略。 -- **`LlmService` 配置 + 循环在记录头部前调用的 `orderTools()` 方法**:可行,但仅为在远处应用策略就增加了一个公开服务方法和一处循环改动;每个未来的请求组合者都必须记得调用。在列表诞生处规范化使无序列表不可表示,且零新增接口。 -- **在 `llm.stream()` 内部规范化**:在头部事件记录之后才运行(不稳定仍存在),且需要重建深度冻结的信封,静默地解除了重建不变式。 +- **注册顺序(现状)**:并发导入竞态,依赖宿主环境(上述 CI 抖动),评审中不可见。 +- **插件依赖图的线性化**:该关系是偏序的,独立的工具插件不可比较;抖动发生时偏序已完全满足。 +- **每个插件在其工具贡献上标注 `weight`**:将顺序分散到各插件中,仍需一个无人拥有的全局编号约定(section 的 `order` 分段已经展示了这种协调成本需要手工承担)。 +- **在 `ToolRegistry.schemas()` 中排序(注册表层)**:同样确定,但注册表是一个成员存储,被组装之外的多方消费;排序是 prompt 组合的关注点,而组装逻辑已经拥有 section 的组合策略。 +- **在 `LlmService` 上加配置 + `orderTools()` 方法,由 loop 在记录 header 前调用**:可行,但仅为在远处应用一个策略就增加了一个公开服务方法和一处 loop 改动;每个未来的请求组合者都必须记得调用。在列表诞生处进行规范化使得无序列表不可表示,且零新增接口。 +- **在 `llm.stream()` 内部规范化**:在 header 事件已记录之后才运行(抖动仍然存在),且需要重建深度冻结的信封,静默地解除了重建不变式。 - **穷举列表(无 rest 条目)**:每个新加载的工具插件都会导致启动失败;强制的 rest 条目使未列出的工具保持确定性,且其位置是显式的。 -- **启动时校验(`dsh-app-boot` 在 `loader.await()` 之后调用 `SystemPrompt.assertToolOrderSatisfied()`)**:能将配置错误变为启动死亡而非首轮失败,但需要一个公开服务方法加上通用启动胶水对单一服务的结构耦合,且无论如何不能替代组装时检查(嵌入式调用者从不运行 app boot;注册在 boot 之后仍会变化)。也没有现成事件可以承载该检查:Cordis v4 没有 ready 类事件,`loader/entry-init`/`internal/status` 在加载中途触发(与工具注册竞态——正是本 RFC 要消除的熵源),而 agent 生命周期事件不会早于组装。在 `assemble()` 设一个执行点被判定值得接受较晚的失败时刻。 +- **启动时校验(由 `dsh-app-boot` 在 `loader.await()` 之后调用 `SystemPrompt.assertToolOrderSatisfied()`)**:能将错误配置变为启动时死亡而非首轮次失败,但代价是一个公开服务方法加上通用启动胶水对单个服务的结构耦合,且无法替代组装时检查(嵌入式调用者从不运行 app boot;注册在 boot 之后仍会变化)。也没有现成事件可以承载该检查:Cordis v4 没有 ready 类事件,`loader/entry-init`/`internal/status` 在加载中途触发(与工具注册存在竞态——正是本 RFC 要消除的熵源),而 agent 生命周期事件不会早于组装。在 `assemble()` 设置单一执行点被判定值得接受较晚的失败时刻。 ## 后果 -- 每个由注册表构建的组装在任何宿主上都以确定性工具顺序开始;在没有专家监听器刻意改变的情况下,每个 `request/header` 事件和模型请求都继承该顺序。CI 与本地之间的注册顺序翻转在结构上被消除,默认为字典序。 -- 初始 `PromptAssembly.tools` 是权威的,因此 waterfall 监听器从模型侧顺序出发;提供方注册顺序在该协作 seam 之前的任何地方都不可观测。 -- 步骤之间的纯工具重排只能表示为 `request/header` 的 `'fallback'` 快照(基于名称键的 `ToolsDelta` 无法表达它);在稳定的权威顺序下,这种重排在实践中不再发生,因此 fallback 路径仅作为安全阀保留。 -- `toolOrder` 键沿 app → `agent-core` → `SystemPrompt` 转发链传递,因此部署时在 app 配置中与 `persona` 并列设置;`dsh-llm` 和 agent loop 不受影响。 -- `toolOrder` 中拼写错误或未加载的工具名称在 prompt 组装时使轮次失败,而非启动时:循环在轮次内组装(`turn/start` 之后、`step/start` 之前),因此拒绝到达轮次的外层 catch——轮次以 `error` 原因平衡关闭并携带消息,`agent/error` 镜像它,不开启步骤,不记录 `request/header`,不向适配器发出请求,agent 回到空闲。每个轮次都以相同方式失败,直到配置被修正;进程本身保持运行(与仓库规则一致:显式配置引用不得被静默忽略——执行点在组装处,因为不存在更早的通用时刻)。 -- 工具提供方返回保留的 rest 条目名称时,其 prompt 组装失败形态与未知的列出名称相同。这防止哨兵值变成歧义的真实工具,并保持「从不丢弃工具」的排序契约。 +- 每个由注册表构建的组装在任何宿主上都以确定性工具顺序开始;在没有专家监听器刻意改变的情况下,每个 `request/header` 事件和模型请求都继承该顺序。CI 与本地之间的注册顺序翻转从结构上被消除,默认为字典序。 +- 初始 `PromptAssembly.tools` 是权威的,因此 waterfall 监听器从模型侧顺序开始;提供方注册顺序在该协作 seam 之前无处可观测。 +- 步骤之间的纯工具重排只能表示为 `request/header` 的 `'fallback'` 快照(按名称索引的 `ToolsDelta` 无法表达它);在稳定的权威顺序下,这种重排在实践中不再发生,因此 fallback 路径仅作为安全阀存在。 +- `toolOrder` 键沿 app → `agent-core` → `SystemPrompt` 的转发链传递,因此部署时将其放在 app 配置中 `persona` 旁边即可;`dsh-llm` 和 agent loop 无需改动。 +- `toolOrder` 中拼错或未加载的工具名称在 prompt 组装时使轮次失败,而非启动时:loop 在轮次内部组装(`turn/start` 之后、`step/start` 之前),因此拒绝到达轮次的外层 catch——轮次以 `error` 原因平衡关闭并携带错误消息,`agent/error` 镜像该消息,不打开步骤,不记录 `request/header`,不向适配器发出请求,agent 回到空闲状态。每个轮次都以相同方式失败,直到配置被修正;进程本身保持运行(符合仓库规则:显式配置引用不得被静默忽略——执行点是组装,因为不存在更早的通用时刻)。 +- 工具提供方返回保留的 rest 条目名称时,其 prompt 组装失败形态与未知的已列名称相同。这防止哨兵值变成一个歧义的真实工具,并保持「从不丢弃工具」的排序契约。 ## 测试 -系统提示词测试覆盖字典序默认顺序、列出/rest 位置、提供方顺序无关性、共享名称、无效列表、未知或保留名称、waterfall 前的权威列表,以及监听器添加的工具不被重新排序的规则。循环测试固定跨注册排列的已记录和已分发顺序一致、通过 agent-core 和两个 app 的转发、深度冻结请求,以及在未知配置名称下的平衡轮次失败(无步骤、无头部、无适配器调用)。快照回放仅在固定的 `text-turn` 头部中保留完整的权威列表;其他 fixture(测试前置数据)继续使用 `{{tools}}`。 +系统提示词测试覆盖:字典序默认顺序、列表/rest 位置、提供方顺序无关性、共享名称、无效列表、未知或保留名称、waterfall 前的权威列表,以及监听器添加的工具不被重新排序的规则。Loop 测试固定:跨注册排列的已记录与已分发顺序一致、通过 agent-core 和两个 app 的转发、深度冻结的请求,以及在配置了未知名称时的平衡轮次失败(无步骤、无 header、无适配器调用)。快照回放仅在固定的 `text-turn` header 中保留完整的权威列表;其他 fixture(测试前置数据)继续使用 `{{tools}}`。 diff --git a/docs/rfc/implemented/feature/2026-07-07-mcp-client-plugin.i18n.yaml b/docs/rfc/implemented/feature/2026-07-07-mcp-client-plugin.i18n.yaml index c36f317c2f..03a68c9166 100644 --- a/docs/rfc/implemented/feature/2026-07-07-mcp-client-plugin.i18n.yaml +++ b/docs/rfc/implemented/feature/2026-07-07-mcp-client-plugin.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-07-mcp-client-plugin.md: 7706190257b54730532e4aa46cc9c47453c59871 -2026-07-07-mcp-client-plugin.zh.md: 4d2ea8532afbf6160a98020b8cc480e1bf683981 +2026-07-07-mcp-client-plugin.zh.md: b5fee7eff7f12de5658f0a10c32cfeed71482fdf diff --git a/docs/rfc/implemented/feature/2026-07-07-mcp-client-plugin.zh.md b/docs/rfc/implemented/feature/2026-07-07-mcp-client-plugin.zh.md index 4d2ea8532a..b5fee7eff7 100644 --- a/docs/rfc/implemented/feature/2026-07-07-mcp-client-plugin.zh.md +++ b/docs/rfc/implemented/feature/2026-07-07-mcp-client-plugin.zh.md @@ -6,15 +6,15 @@ Status: implemented ## 问题 -harness 此前无法消费 MCP(Model Context Protocol)生态的工具。MCP 是工具服务器的新兴标准:GitHub、文件系统、数据库、代码搜索以及数百个社区服务器都通过 MCP 暴露工具。用户希望将 harness 指向一个或多个 MCP 服务器,让它们的工具以原生的模型可见工具形式出现,而无需为每个服务器编写胶水代码。 +harness 此前无法消费 MCP(Model Context Protocol)生态中的工具。MCP 是工具服务器的新兴标准——GitHub、文件系统、数据库、代码搜索以及数百个社区服务器都通过 MCP 暴露工具。用户希望将 harness 指向一个或多个 MCP 服务器,让其工具以原生的模型可见工具形式出现,而无需为每个服务器编写胶水代码。 -`ToolRegistry` 已经接受原始 JSON Schema 工具定义(见 `dsh-tools` README:"Raw JSON-Schema tool definitions (from MCP servers) are still accepted by `ToolRegistry.register()` directly"),扩展实操手册(cookbook)也勾勒了预期模式("MCP | one plugin per server: discover tools → `ctx.tools.register()`")。基础设施已就绪,缺的是桥接插件。 +`ToolRegistry` 已经接受原始 JSON Schema 工具定义(`dsh-tools` README 中有记录:"Raw JSON-Schema tool definitions (from MCP servers) are still accepted by `ToolRegistry.register()` directly"),扩展实操手册(cookbook)也勾勒了预期模式("MCP | one plugin per server: discover tools → `ctx.tools.register()`")。基础设施已就绪,缺的是桥接插件。 ## 决策 ### 包 -单个包 `@deepseek-ai/dsh-mcp-client`,位于 `packages/mcp/mcp-client/`。不做能力 seam 三包拆分:可预见范围内不会有第二种 MCP 客户端实现,且约定是「不要预防性拆分」(见[能力 seam RFC](../../implemented/architecture/2026-06-13-capability-seams.md))。 +单个包(package) `@deepseek-ai/dsh-mcp-client`,位于 `packages/mcp/mcp-client/`。不做能力 seam 的三包拆分——可预见范围内不会有第二种 MCP 客户端实现,且约定是"不要预防性拆分"([能力 seam RFC](../../implemented/architecture/2026-06-13-capability-seams.md))。 ### SDK @@ -22,11 +22,11 @@ harness 此前无法消费 MCP(Model Context Protocol)生态的工具。MCP ### 范围 -仅 MCP 客户端(不含服务器端——ACP 已覆盖「将 harness 暴露为 agent」的角色)。仅桥接 **Tools**:Resources 和 Prompts 推迟(它们需要 harness 侧尚不存在的消费机制,且设计空间很大)。 +仅 MCP Client(不含 server 端——ACP 已承担"将 harness 暴露为 agent"的角色)。仅桥接 **Tools**——Resources 和 Prompts 延后处理(它们需要 harness 侧尚不存在的消费机制,且设计空间较大)。 ### 插件形态 -命名空间插件(具名导出 `name`/`inject`/`Config`/`apply`,无 `export default`)。`inject: ['tools']`。每个 MCP 服务器在 `cordis.yml` 中是一个插件实例:同一个包以不同配置加载 N 次,与 `dsh-tool-subagent` 相同。 +命名空间插件(具名导出 `name`/`inject`/`Config`/`apply`,无 `export default`)。`inject: ['tools']`。每个 MCP 服务器对应 `cordis.yml` 中的一个插件实例——同一个包以不同配置加载 N 次,与 `dsh-tool-subagent` 相同。 ### 配置 @@ -54,7 +54,7 @@ interface StreamableHttpConfig { type Config = StdioConfig | StreamableHttpConfig ``` -`serverName` 是稳定的本地标识,用于在模型可见名称(见下文)中为该服务器的工具划定命名空间。它有意设计为用户配置,**不是**远端的 `serverInfo.name`:远端名称是不可信输入,跨部署不唯一(同一服务器的 prod 和 staging 实例报告相同名称),且可能在服务器升级时变化——这些都不得静默地重命名模型可见工具。多个活跃实例使用相同 `serverName` 属于配置错误:后加载的实例在启动时以可操作的错误消息失败,绝不静默覆盖或跳过。短 `serverName`(如 `gh`)同时也是缩短公开名称的旋钮。 +`serverName` 是稳定的本地标识,用于在模型可见名称(见下文)中为该服务器的工具提供命名空间。它有意设计为用户配置,而**非**远端的 `serverInfo.name`:远端名称是不可信输入、跨部署不唯一(同一服务器的生产和预发布实例报告相同名称)、且可能在服务器升级时变化——这些都不得静默重命名模型可见工具。多个活跃实例使用重复的 `serverName` 属于配置错误:后加载的实例在启动时以可操作的错误消息失败,绝不静默覆盖或跳过。短 `serverName`(如 `gh`)也是缩短公开名称的调节手段。 `cordis.yml` 用法示例: @@ -83,28 +83,28 @@ type Config = StdioConfig | StreamableHttpConfig ### 生命周期 -启动时从 `cordis.yml` 加载。HMR(`@cordisjs/plugin-hmr`)提供热替换:编辑 yml 条目会触发旧实例的 dispose(断开连接、注销工具),并创建新实例(连接、发现、注册)。目前不提供运行时动态 API。公开名称是 `(serverName, rawName)` 的纯函数,因此保持 `serverName` 不变的 HMR 替换会重建完全相同的模型可见名称——会话历史和权限规则保持有效——且添加或移除一个无关服务器绝不会重命名已有工具。 +启动时从 `cordis.yml` 加载。HMR(热模块替换)(`@cordisjs/plugin-hmr`)提供热替换:编辑 yml 条目触发旧实例的 dispose(资源释放)(断开连接、注销工具),并创建新实例(连接、发现、注册)。目前不提供运行时动态 API。公开名称是 `(serverName, rawName)` 的纯函数,因此保持 `serverName` 不变的 HMR 替换会重建完全相同的模型可见名称——会话历史和权限规则保持有效——而添加或移除不相关的服务器永远不会重命名已有工具。 ### 工具发现与注册 每个 MCP 工具有两个名称: -- `rawName`:MCP `Tool.name` 的原始值,仅在协议层(`tools/call`)使用。 -- `publicName`:在 `ToolRegistry` 中注册的全局唯一模型可见名称: +- `rawName`——MCP `Tool.name` 的原始值,仅用于协议通信(`tools/call`)。 +- `publicName`——在 `ToolRegistry` 中注册的全局唯一模型可见名称: mcp____ -这种按服务器限定的形式是多服务器 agent 客户端的事实标准:所有被调研的终端用户产品都按服务器限定 MCP 工具([Claude Code](https://code.claude.com/docs/en/agent-sdk/mcp#tool-naming-convention) `mcp__github__list_issues`、[Codex](https://openai.com/index/unrolling-the-codex-agent-loop/) `mcp__weather__get-forecast`、[Gemini CLI](https://geminicli.com/docs/tools/mcp-server/#3-tool-naming-and-namespaces)、[VS Code](https://github.com/microsoft/vscode/blob/ab9ec62c6a61e429a9abd612ff220c3f4834c9ea/src/vs/workbench/contrib/mcp/common/mcpServer.ts#L217-L260)、[Cline](https://github.com/cline/cline/blob/52fdbb1d72f7324a28142a7ba7678d4b53c902f4/sdk/packages/core/src/extensions/mcp/name-transform.ts#L20-L35)、[Roo Code](https://github.com/RooCodeInc/Roo-Code/blob/b867ec9145750d0ae1ff7f02d35406e9bf2a0b16/src/utils/mcp-name.ts#L117-L140)、[Goose](https://github.com/block/goose/blob/b3a012cbdde854b0fe14f95b1c48543bf6517c0a/crates/goose/src/agents/extension_manager.rs#L1391-L1441)、[OpenCode](https://github.com/anomalyco/opencode/blob/d199b1bff90282a4f9cd6251b5fc7b16875a52f6/packages/opencode/src/mcp/catalog.ts#L117-L120));`mcp____` 的确切拼写沿用 Claude Code 和 Codex。`mcp__` 前缀将 MCP 注册隔离在原生工具命名空间之外,并为权限/遥测规则提供稳定的匹配形状(`mcp__*`、`mcp__github__*`)。 +这种按服务器限定的形式是多服务器 agent 客户端的事实标准——所有被调研的终端用户产品都按服务器限定 MCP 工具名([Claude Code](https://code.claude.com/docs/en/agent-sdk/mcp#tool-naming-convention) `mcp__github__list_issues`、[Codex](https://openai.com/index/unrolling-the-codex-agent-loop/) `mcp__weather__get-forecast`、[Gemini CLI](https://geminicli.com/docs/tools/mcp-server/#3-tool-naming-and-namespaces)、[VS Code](https://github.com/microsoft/vscode/blob/ab9ec62c6a61e429a9abd612ff220c3f4834c9ea/src/vs/workbench/contrib/mcp/common/mcpServer.ts#L217-L260)、[Cline](https://github.com/cline/cline/blob/52fdbb1d72f7324a28142a7ba7678d4b53c902f4/sdk/packages/core/src/extensions/mcp/name-transform.ts#L20-L35)、[Roo Code](https://github.com/RooCodeInc/Roo-Code/blob/b867ec9145750d0ae1ff7f02d35406e9bf2a0b16/src/utils/mcp-name.ts#L117-L140)、[Goose](https://github.com/block/goose/blob/b3a012cbdde854b0fe14f95b1c48543bf6517c0a/crates/goose/src/agents/extension_manager.rs#L1391-L1441)、[OpenCode](https://github.com/anomalyco/opencode/blob/d199b1bff90282a4f9cd6251b5fc7b16875a52f6/packages/opencode/src/mcp/catalog.ts#L117-L120));`mcp____` 的拼写方式与 Claude Code 和 Codex 一致。`mcp__` 前缀将 MCP 注册与原生工具的命名空间隔离,并为权限/遥测规则提供稳定的匹配模式(`mcp__*`、`mcp__github__*`)。 -1. 连接时:遍历 `client.listTools()` 的分页,推导每个工具的 `publicName`,然后通过 `ctx.tools.register()` 将其注册为原始 `ToolDefinition`。MCP 的 JSON Schema 和 description 原样透传(不做 `defineTool` DSL 转换);仅替换模型可见的 `name`。 -2. 监听 `notifications/tools/list_changed` → 重新执行同步(dispose 上一代、注册新一代)。确定性的名称意味着未变化的工具在重新同步后保持原名。 -3. 执行器闭包持有 `rawName`;公开名称从不发送给服务器,也从不被解析以恢复原始名称。 -4. 不提供 `presentCall`/`presentResult`:ACP 桥接的通用卡片回退负责渲染。 -5. 工具在系统提示词中是透明的:除名称本身外不添加 "[via MCP]" 之类的标注。 +1. 连接时:遍历 `client.listTools()` 的分页结果,推导每个工具的 `publicName`,然后通过 `ctx.tools.register()` 将其注册为原始 `ToolDefinition`。MCP 的 JSON Schema 和描述原样透传(不做 `defineTool` DSL 转换);仅替换模型可见的 `name`。 +2. 监听 `notifications/tools/list_changed` → 重新执行同步(dispose 上一代、注册新一代)。确定性命名意味着未变化的工具在重新同步后保持原名。 +3. 执行器闭包持有 `rawName`;公开名称永远不发送给服务器,也永远不被解析以还原原始名称。 +4. 无 `presentCall`/`presentResult`——ACP 桥接的通用卡片兜底负责渲染。 +5. 工具在系统提示词中是透明的——除名称本身外不附加 "[via MCP]" 标注。 ### 公开名称规范化 -MCP 允许工具名最长 128 字符且可包含 `.`;DeepSeek 的函数名契约允许 `[A-Za-z0-9_-]` 且最长 64 字符。公开名称按确定性规则规范化:非法字符替换为 `_`,当替换或截断改变了名称时,追加 `(serverName, rawName)` 标识的 12 位十六进制 SHA-256 hash,确保不同的 MCP 标识永远不会折叠为同一个公开名称: +MCP 允许工具名最长 128 字符且可包含 `.`;DeepSeek 的函数名契约允许 `[A-Za-z0-9_-]` 且最多 64 字符。公开名称按确定性规则规范化:非法字符替换为 `_`,当替换或截断改变了名称时,追加 `(serverName, rawName)` 标识的 12 位十六进制 SHA-256 hash,确保不同的 MCP 标识永远不会坍缩为同一个公开名称: ```typescript function publicToolName(serverName: string, rawName: string): string { @@ -118,97 +118,97 @@ function publicToolName(serverName: string, rawName: string): string { ### 名称冲突处理 -MCP 仅保证工具名在[单个服务器内](https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool-names)唯一;跨服务器冲突是常态而非例外(一项[微软研究院调研](https://www.microsoft.com/en-us/research/blog/tool-space-interference-in-the-mcp-era-designing-for-agent-compatibility-at-scale/#namespacing-issues-and-naming-ambiguity)覆盖 1,470 个服务器,发现 775 个冲突工具名;仅 `search` 就出现在 32 个服务器中,官方 GitHub 服务器发布的是裸 `create_issue`)。始终启用的命名空间从结构上杜绝冲突,而非在冲突发生时再处理: +MCP 仅保证工具名在[单个服务器内](https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool-names)唯一;跨服务器冲突是常态而非例外(一项[微软研究院调查](https://www.microsoft.com/en-us/research/blog/tool-space-interference-in-the-mcp-era-designing-for-agent-compatibility-at-scale/#namespacing-issues-and-naming-ambiguity)覆盖 1,470 个服务器,发现 775 个冲突的工具名;仅 `search` 就出现在 32 个服务器中,官方 GitHub 服务器发布的是裸名 `create_issue`)。始终启用的命名空间从结构上杜绝冲突,而非在冲突发生时再处理: - 两个服务器都发布 `search` → 共存为 `mcp__github__search` 和 `mcp__web__search`。 - 名为 `search` 的原生 harness 工具不受影响。 -- 重复的 `serverName` 配置导致后加载的实例在启动时失败(见「配置」一节)。 -- 同一服务器列出重复的工具名属于无效工具列表:同步抛出异常,上一代注册保持不变。 -- 替换期间的注册表冲突只可能意味着外部工具占用了本服务器的 `mcp____` 命名空间:部分生成被回滚(该服务器零工具注册),错误被醒目地记录。 +- 重复的 `serverName` 配置使后加载的实例在启动时失败(见配置一节)。 +- 服务器列出重复的工具名属于无效工具列表:同步抛出异常,上一代注册保持不变。 +- 替换期间的注册表冲突只可能意味着外部工具占据了该服务器的 `mcp____` 命名空间:部分代注册被回滚(该服务器零工具),并以醒目日志记录错误。 工具永远不会被静默跳过;哪些工具可用永远不取决于插件加载顺序。 ### 命名不变式 -1. 每个 MCP 工具有稳定标识 `(serverName, rawName)`;每个活跃标识恰好对应一个公开名称。 +1. 每个 MCP 工具拥有稳定标识 `(serverName, rawName)`;每个活跃标识恰好对应一个公开名称。 2. 公开名称是确定性的、全局唯一的,且满足 DeepSeek 64 字符 `[A-Za-z0-9_-]` 契约。 3. MCP `tools/call` 始终接收原始的 raw name。 -4. 连接、断开或重新同步一个无关服务器,绝不会重命名已有工具。 -5. 注册顺序绝不决定哪个工具可用。 +4. 连接、断开或重新同步不相关的服务器永远不会重命名已有工具。 +5. 注册顺序永远不决定哪个工具可用。 ### 工具执行 -为来自同一 MCP 服务器的所有工具提供统一的 `execute` 处理器: +为来自同一个 MCP 服务器的所有工具提供统一的 `execute` 处理器: -1. 解析 `rawName`(执行器闭包持有),以配置的超时调用 `client.callTool({ name: rawName, arguments }, { signal: exec.signal })`——公开名称从不发送给服务器。 +1. 解析 `rawName`(执行器闭包持有它),以配置的超时时间调用 `client.callTool({ name: rawName, arguments }, { signal: exec.signal })`——公开名称永远不发送给服务器。 2. 映射结果: - - 多个 `text` 内容块 → 以 `'\n'` 连接为单个 `TextBlock`(必要原因:`flattenText` 使用 `join('')` 不带分隔符,多个块会丢失块间边界)。 - - `image` 内容块 → 丢弃并记录 `ctx.logger.warn`(harness 没有图片内容块类型;见 [drop-image RFC](../../implemented/simplification/2026-07-04-drop-image-content-block.md))。 + - 多个 `text` 内容块 → 以 `'\n'` 连接为单个 `TextBlock`(必要原因:`flattenText` 使用 `join('')` 无分隔符,多块会丢失块间边界)。 + - `image` 内容块 → 丢弃并 `ctx.logger.warn`(harness 没有图片内容块类型;[drop-image RFC](../../implemented/simplification/2026-07-04-drop-image-content-block.md))。 - `isError: true` → 映射到 harness 的 `isError` 结果路径(`{ content: [...], isError: true }`)。 -3. 取消:`exec.signal`(来自 agent loop 的 cancel)透传给 MCP SDK 的 `callTool`,后者向服务器发送 `$/cancelRequest`。 +3. 取消:`exec.signal`(来自 agent loop(智能体循环)的取消)透传给 MCP SDK 的 `callTool`,后者向服务器发送 `$/cancelRequest`。 ### 子进程环境(stdio 传输) -复用 `dsh-subagent-acp` 的 `buildChildEnv` + `SENSITIVE_ENV_PATTERN` 清洗逻辑:过滤环境变量(剥离匹配 `/KEY|SECRET|TOKEN/i` 的凭证形变量),然后将 `config.env` 覆盖在上面。显式配置的 env 不受清洗影响。 +复用 `dsh-subagent-acp` 的 `buildChildEnv` + `SENSITIVE_ENV_PATTERN` 清洗逻辑:过滤环境变量(剥离匹配 `/KEY|SECRET|TOKEN/i` 的凭证形变量),然后将 `config.env` 覆盖合并到顶层。显式配置的 env 不受清洗影响。 -### 断开连接 / 崩溃 +### 断连 / 崩溃 不自动重连。如果 MCP 服务器进程退出或传输层关闭: 1. effect dispose → 所有已注册工具被注销(fiber 作用域的 disposer)。 2. 后续模型对这些工具的调用 → `ToolNotFoundError` → `isError: true`。 -3. 恢复方式:用户编辑 `cordis.yml`(触发 HMR 重载)或重启 harness。 +3. 恢复:用户编辑 `cordis.yml`(触发 HMR 重载)或重启 harness。 -这与 ACP subagent 的模式一致:「崩溃即终态,报告错误,清理资源,不重试。」 +这与 ACP subagent 模式一致:"崩溃即终态,报告错误,清理资源,不重试。" ## 曾考虑的替代方案 -### MCP 服务器端(向外部 MCP 客户端暴露 harness 工具) +### MCP Server 端(将 harness 工具暴露给外部 MCP 客户端) -推迟。ACP 桥接已将 harness 暴露为 agent 服务器。再加一层 MCP 服务器会用不同协议重复这一功能,而用户的首要需求是消费外部工具,而非暴露自身工具。 +延后。ACP 桥接已将 harness 暴露为 agent 服务器。再加一层 MCP server 会以不同协议重复这一功能,而用户的首要需求是消费外部工具,而非暴露自身工具。 -### 能力 seam 三包拆分(接口 / 实现 / 消费方) +### 能力 seam 三包拆分(interface / impl / consumer) -否决。可预见范围内不会有替代的 MCP 客户端实现:MCP 只有一个协议、一个 SDK。约定是「在第二种实现出现之前不要预防性拆分」。 +否决。可预见范围内不会有替代的 MCP 客户端实现——MCP 只有一个协议、一个 SDK。约定是"不要预防性拆分",直到出现第二种实现。 ### 指数退避自动重连 -v1 否决。引入复杂性(工具已注册但暂时不可用的部分可用状态),且 stdio 进程崩溃通常表明配置问题,重试无法修复。HMR 已提供手动恢复路径。如有需要,未来可作为 `reconnect: boolean` 配置项加入。 +v1 否决。引入复杂性(工具已注册但暂时不可用的部分可用状态),且 stdio 进程崩溃通常表明配置问题,重试无法修复。HMR 已提供手动恢复路径。如有需要,可在未来作为 `reconnect: boolean` 配置项添加。 ### 桥接 Resources 和 Prompts -推迟。Resources 需要 harness 侧的机制来决定何时注入内容(系统提示词?按需?模型触发?)。Prompts 需要 harness 目前缺少的「prompt 模板」概念。两者都需要独立设计;Tools 是高价值、低风险的起点。 +延后。Resources 需要 harness 侧的机制来决定何时注入内容(系统提示词?按需?模型触发?)。Prompts 需要 harness 尚不具备的"提示词模板"概念。两者都需要独立设计;Tools 是高价值、低风险的起点。 ### 原始模型可见工具名加可选 `toolPrefix` -否决。这是最初的提案,建立在「大多数 MCP 服务器已在工具名中使用语义前缀(如 `github_create_issue`)」的前提上。该前提不成立:官方 GitHub 服务器发布的是 `create_issue`,参考文件系统服务器是 `read_file`,Sentry 是 `search_issues`——且上述微软调研表明冲突在生态规模下很常见。冲突时再加前缀(或 warn-and-skip)还会使可用工具集取决于插件加载顺序,且添加一个无关服务器可能静默重命名已有工具——在对话中途使会话历史和权限规则失效。所有被调研的多服务器 agent 产品都不使用裸名称。 +否决。这是最初的提案,基于"大多数 MCP 服务器已在工具名中使用语义前缀(如 `github_create_issue`)"这一前提。该前提不成立:官方 GitHub 服务器发布的是 `create_issue`,参考文件系统服务器发布 `read_file`,Sentry 发布 `search_issues`——且上述微软调查表明冲突在生态规模下很常见。冲突时再加前缀(或 warn-and-skip)还会使可用工具集取决于插件加载顺序,且添加不相关服务器时工具可能被静默重命名——在对话中途使会话历史和权限规则失效。所有被调研的多服务器 agent 产品都不使用裸名。 ### 仅服务器命名空间(`github__create_issue`,无 `mcp__` 前缀) -v1 否决。它能防止跨服务器冲突,但无法将 MCP 注册与原生 harness 工具隔离,也放弃了 MCP 全局策略匹配形状(`mcp__*`)。前缀仅消耗 5 个字符;`mcp____` 的拼写与 Claude Code 和 Codex 一致,最大化模型的熟悉度。如果 ToolRegistry 将来增加源感知的命名空间,届时可作为命名策略变更重新考虑去掉字面前缀。 +v1 否决。它能防止跨服务器冲突,但无法将 MCP 注册与原生 harness 工具分离,也丧失了 MCP 全局策略匹配模式(`mcp__*`)。前缀仅多花 5 个字符;`mcp____` 拼写与 Claude Code 和 Codex 一致,最大化模型的熟悉度。如果 ToolRegistry 未来引入源感知命名空间,届时可作为命名策略变更重新考虑去掉字面前缀。 -### 从服务器公告的 `serverInfo.name` 推导命名空间 +### 从服务器公告的 `serverInfo.name` 派生命名空间 否决。远端名称不可信、跨部署不唯一、升级时可变;工具标识和权限规则不得静默跟随它。命名空间是本地配置。 ### 在工具结果中保留多个 TextBlock -否决。DeepSeek 序列化器中的 `flattenText()` 在将 `ContentBlock[]` 展平为协议格式(wire format)时使用 `join('')`(无分隔符)。多个 text 块会静默丢失块间边界——这是正确性 bug。所有现有工具返回单个 TextBlock;MCP 桥接遵循同样做法。 +否决。DeepSeek 序列化器中的 `flattenText()` 在将 `ContentBlock[]` 扁平化为协议格式(wire format)时使用 `join('')`(无分隔符)。多个 text 块会静默丢失块间边界——这是正确性缺陷。所有现有工具返回单个 TextBlock;MCP 桥接遵循同一做法。 ## 测试 -覆盖按层级命名;每个行为放在能表达它的最低成本层级。 +覆盖率按层级命名;每个行为放在能表达它的最低成本层级。 -- **单元测试**(`tests/mcp-client.spec.ts`、`tests/apply.spec.ts`,mock MCP SDK):`publicToolName` 算法(干净路径、规范化、截断加 hash、确定性、不同标识的分离)、raw 与 public 的协议纪律、跨服务器与原生工具共存、重复 `serverName` 加载失败与预留释放、无效工具列表拒绝、代际替换/回滚、重新同步失败时的保留、结果映射、取消、配置 schema 校验。100% 逐文件覆盖率门禁约束该包。 -- **E2E**(`tests/mcp-client.e2e.ts`,无需密钥):使用仓库内 fixture 服务器、`@modelcontextprotocol/server-everything` 和 `@modelcontextprotocol/server-filesystem` 通过 stdio 运行真实 MCP 协议,以及通过进程内 `StreamableHTTPServerTransport` 服务器运行 Streamable HTTP——命名空间下的发现、带点号名称的端到端规范化、执行往返、重复 `serverName` 拒绝、dispose(资源释放)。 -- **快照**:刻意不做。MCP 工具不引入新的 transcript 渲染面——它们注册为原始 `ToolDefinition`,通过 ACP 桥接的通用卡片回退渲染,而桥接的单元测试套件已固定了该行为(`packages/ui/acp/tests/stream-update.spec.ts`)。将 MCP 服务器加入快照示例的 `cordis.yml` 会改变已固定的 `text-turn` 系统提示词 fixture(迫使每条录制的 golden 都需要带密钥重新录制),并使每次回放依赖于 spawn 一个外部 MCP 服务器进程——而新增的渲染行为为零。如果后续变更为 MCP 工具引入专属的渲染意图,该变更届时自行命名其快照覆盖。 +- **单元测试**(`tests/mcp-client.spec.ts`、`tests/apply.spec.ts`,mock MCP SDK):`publicToolName` 算法(干净名称、规范化、截断加 hash、确定性、不同标识的分离)、raw 与 public 的协议纪律、跨服务器与原生工具共存、重复 `serverName` 加载失败与预留释放、无效工具列表拒绝、代切换/回滚、重新同步失败时的保留、结果映射、取消、配置 schema 校验。100% 逐文件覆盖率门禁约束该包。 +- **E2E**(`tests/mcp-client.e2e.ts`,无需密钥):使用真实 MCP 协议对接仓库内的 fixture(测试前置数据)服务器、`@modelcontextprotocol/server-everything` 和 `@modelcontextprotocol/server-filesystem`(stdio 传输),以及进程内 `StreamableHTTPServerTransport` 服务器(Streamable HTTP 传输)——命名空间下的发现、带点号名称的端到端规范化、执行往返、重复 `serverName` 拒绝、dispose。 +- **快照**:刻意不做。MCP 工具不引入新的 transcript(文本记录)呈现面——它们以原始 `ToolDefinition` 注册,通过 ACP 桥接的通用卡片兜底渲染,该兜底已由桥接的单元测试套件固定(`packages/ui/acp/tests/stream-update.spec.ts`)。将 MCP 服务器添加到快照示例的 `cordis.yml` 会改变已固定的 `text-turn` 系统提示词 fixture(迫使每条录制的 golden 都需要带密钥重新录制),且使每次回放依赖于 spawn 外部 MCP 服务器进程——而新增渲染行为为零。如果后续变更为 MCP 工具引入专属渲染意图,该变更届时自行声明快照覆盖。 ## 后果 -- 每个 MCP 服务器只需一条 `cordis.yml` 条目即完成集成:`serverName: filesystem` 加一条 stdio 命令(或一个 Streamable HTTP URL),就能把 `mcp__filesystem__read_file` 放入模型的工具列表,可调用,协议层使用原始的 `read_file`。 -- 公开名称是会话历史与权限/配置界面的一部分;命名算法是由测试固定的 v1 契约,发布后修改它是破坏性变更。 -- `mcp____` 限定符在每个名称上消耗 token。已接受:description 和 JSON Schema 在工具定义 token 中占主导,而限定符换来了稳定标识、冲突隔离和 MCP 全局策略匹配形状(`mcp__*`、`mcp__github__*`)。 -- **MCP SDK 稳定性**:`@modelcontextprotocol/sdk` 仍在演进;破坏性变更需要更新桥接。版本已固定,且该 SDK 被广泛采用(Claude Desktop、Cursor、VS Code),因此破坏性变更不太可能悄然发生。 -- **工具 schema 质量**:MCP 服务器可能暴露描述不佳的工具(模糊的 description、不完整的 JSON Schema)。harness 原样透传——垃圾进垃圾出;这是服务器作者的责任,不是桥接的责任。 -- **Stdio 进程管理**:行为异常的 MCP 服务器如果忽略信号可能卡住 dispose。Cordis fiber 的 dispose 有有界静默期;卡住的传输层最终会在框架层面超时。 -- 崩溃恢复是手动的(HMR 编辑或重启)——v1 已接受;`reconnect` 配置项作为未来工作保持开放。 +- 每个 MCP 服务器只需 `cordis.yml` 中的一条配置即完成集成:`serverName: filesystem` 加一条 stdio 命令(或一个 Streamable HTTP URL),就能将 `mcp__filesystem__read_file` 放入模型的工具列表,可调用,协议上使用原始的 `read_file`。 +- 公开名称是会话历史和权限/配置表面的一部分;命名算法是由测试固定的 v1 契约,发布后变更即为破坏性变更。 +- `mcp____` 限定符在每个名称上消耗 token。已接受:描述和 JSON Schema 在工具定义 token 中占主导,而限定符换来了稳定标识、冲突隔离和 MCP 全局策略匹配模式(`mcp__*`、`mcp__github__*`)。 +- **MCP SDK 稳定性**:`@modelcontextprotocol/sdk` 仍在演进中;破坏性变更需要更新桥接。版本已固定,且该 SDK 被广泛采用(Claude Desktop、Cursor、VS Code),因此破坏性变更不太可能悄然发生。 +- **工具 schema 质量**:MCP 服务器可能暴露描述不佳的工具(模糊的描述、不完整的 JSON Schema)。harness 原样透传——垃圾进垃圾出;这是服务器作者的责任,不是桥接的。 +- **Stdio 进程管理**:行为异常的 MCP 服务器如果忽略信号,可能卡住 dispose。Cordis fiber 的 dispose 有有界静默期;卡住的传输层最终在框架层面超时。 +- 崩溃恢复是手动的(HMR 编辑或重启)——v1 已接受;`reconnect` 配置作为未来工作保持开放。 diff --git a/docs/rfc/implemented/feature/2026-07-07-session-prefix.i18n.yaml b/docs/rfc/implemented/feature/2026-07-07-session-prefix.i18n.yaml index fddf23cccb..038349fd78 100644 --- a/docs/rfc/implemented/feature/2026-07-07-session-prefix.i18n.yaml +++ b/docs/rfc/implemented/feature/2026-07-07-session-prefix.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-07-session-prefix.md: ffa0fecb86e84ed64d45b24e1b6943d421757fb2 -2026-07-07-session-prefix.zh.md: 292c74fac75d8f2c29628fc5e90c72dadcaf4fdb +2026-07-07-session-prefix.zh.md: ca38ebf337e16f4f2368ca74e394fcc1031fbf1f diff --git a/docs/rfc/implemented/feature/2026-07-07-session-prefix.zh.md b/docs/rfc/implemented/feature/2026-07-07-session-prefix.zh.md index 292c74fac7..ca38ebf337 100644 --- a/docs/rfc/implemented/feature/2026-07-07-session-prefix.zh.md +++ b/docs/rfc/implemented/feature/2026-07-07-session-prefix.zh.md @@ -1,4 +1,4 @@ -# RFC:会话前缀——置于派生历史之前的仅请求消息 +# RFC:会话前缀——派生历史之前的仅请求消息 Status: implemented @@ -6,39 +6,39 @@ Status: implemented ## 问题 -插件经常拥有一段会话级别稳定的开场内容,模型必须始终看到它:技能目录、AGENTS.md 摘要、工作区基线。在这个 seam 出现之前,harness 只提供两个归属位置,但对这类内容来说两个都不对。系统提示词是一个渲染后的单字符串:消息形态的内容(user 角色的 `` 信封、多消息引导序列)放不进去,而且提供方对对话消息与系统文本的权重处理不同。持久化历史(`agent.inject()`、会话开始时的 `context/message`)会让开场内容变成永久记录:每个 `deriveMessages()` 消费方都会回放它,压缩(compaction)的保留遍历拥有它,fork 会把它以陈旧状态烘焙进去,resume 无法刷新它——一份在会话诞生时捕获的目录会比它所描述的世界活得更久。 +插件经常拥有一段会话级别稳定的开场内容,模型必须始终看到它:技能目录、AGENTS.md 摘要、工作区基线。在引入本 seam 之前,harness 为这类内容提供了两个归属位置,但两者都不合适。系统提示词是一个渲染后的单一字符串:消息形态的内容(user 角色的 `` 信封、多消息引导序列)放不进去,而且提供方对会话消息和系统文本的权重处理不同。持久化历史(`agent.inject()`、会话启动时的 `context/message`)使开场内容变为永久:每个 `deriveMessages()` 消费方都会回放它,压缩(compaction)的保留遍历拥有它,fork 会将其以陈旧状态固化,resume 也无法刷新它——会话诞生时捕获的目录会比它所描述的世界活得更久。 -显而易见的第三个选项——让插件在请求发出时编辑 `messages`——被[可重建请求 RFC](../architecture/2026-07-05-reconstructable-requests.md) 禁止:每个由循环构建的请求都是会话日志的纯函数,因此承载开场内容的通道必须精确记录它所发送的内容。缺失的是一个带持久记录的仅请求消息通道。 +显而易见的第三种选项——让插件在请求发出途中编辑 `messages`——被[可重建请求 RFC](../architecture/2026-07-05-reconstructable-requests.md) 禁止:每个由循环构建的请求都是会话日志的纯函数,因此无论哪个通道承载开场内容,都必须精确记录它所发送的内容。缺失的是一个带有持久记录的仅请求消息通道。 ## 决策 -`agent/session-prefix` 是 agent 事件映射上的一个 waterfall(瀑布式事件)([`packages/core/agent/src/types.ts`](../../../../packages/core/agent/src/types.ts)):监听器接收一个冻结的空种子并返回一个扩展(规范的贡献方式是前置,`[mine, ...await next()]`,在协议格式上产生注册顺序)。循环([`packages/core/agent-loop/src/loop.ts`](../../../../packages/core/agent-loop/src/loop.ts))在每个循环实例中触发一次,延迟到该实例首次 `agent/pre-step` 之前;组合后的列表被深拷贝、深冻结、缓存在实例上,并在该实例发送的每个请求中置于**整个**派生历史之前——紧接在提供方的 system 槽位之后([协议格式顺序](../../../core-data-structures/core.md#the-request-envelope-llmcallconfig-and-the-logged-header))。 +`agent/session-prefix` 是 agent 事件映射上的一个 waterfall(瀑布式事件)([`packages/core/agent/src/types.ts`](../../../../packages/core/agent/src/types.ts)):监听器接收一个冻结的空种子并返回扩展(规范的贡献方式是前置插入 `[mine, ...await next()]`,在协议格式上产生注册顺序)。agent loop(智能体循环)([`packages/core/agent-loop/src/loop.ts`](../../../../packages/core/agent-loop/src/loop.ts))在每个循环实例中触发一次,惰性地在实例首次 `agent/pre-step` 之前执行;组合后的列表被深拷贝、深冻结、缓存在实例上,并在该实例发出的每个请求中置于**整个**派生历史之前——紧接在提供方的 system 槽位之后([协议格式顺序](../../../core-data-structures/core.md#the-request-envelope-llmcallconfig-and-the-logged-header))。 三个属性承载了这一设计: -- **仅请求,记录在 header 中。** `deriveMessages()` 从不返回前缀;它唯一的持久记录是实例锚定的 `request/header` 快照上的 `EpochHeader.messagePrefix`——可重建请求 RFC 已经为请求的非历史部分拥有的通道,因此不引入新的会话事件。开发不变式([dsh-invariants](../../../../packages/support/invariants/src/index.ts))对每个循环构建的请求重新计算 `messagePrefix + 边界派生`;未记录的前缀无法到达协议格式。 -- **按实例冻结。** 复用是结构性的,而非靠纪律保证:缓存的产物在会话中途不可变,因此提供方的 prompt 缓存在构造上成立,前缀以每步零边际成本扩展了可缓存区域。进程重启或 `ctx.agents.resume()` 是一个新实例:它重新组合,任何漂移都可归因地落在 `'resume'` header 快照上。这就是该 seam 创建的路由规则:会话冻结的开场内容走前缀;会话中途变化的内容走仅追加历史通道(`agent.inject()`、`tools/post-execute` 决策的 `additionalContext`、prompt-submit 的 `additionalContext`——[拦截 seam RFC](2026-06-30-interception-seams.md)),每条都是一次性持久化的 `context/message`,之后被前缀缓存覆盖。 -- **在压力门禁之前组合。** 组合先于实例的首次 `agent/pre-step`,且 seam 将组合值传递下去:`agent/pre-step` 携带 `sessionPrefix` 参数,`CompactService.compactIfNeeded(agent, fullSystemPrompt, sessionPrefix, signal)` 将其计入 token 压力估算——如果门禁读取的是上一个实例折叠后的前缀,那么在一个贡献者增长了的 resume 或 fork 实例的首步上会低估压力,跳过压缩并发出超窗口的首请求。组合过程中如果 cancel/dispose 落入 waterfall 内部,组合结果被丢弃、永不缓存:一个感知中止的监听器的降级回退不会泄漏到后续请求中,下一轮次在活跃 signal 下重新组合。 +- **仅请求,记录在 header 中。** `deriveMessages()` 从不返回前缀;它唯一的持久记录是实例锚定的 `request/header` 快照上的 `EpochHeader.messagePrefix`——可重建请求 RFC 已为请求的非历史部分拥有的通道,因此不引入新的会话事件。开发不变式([dsh-invariants](../../../../packages/support/invariants/src/index.ts))对每个循环构建的请求重新计算 `messagePrefix + 边界派生`;未记录的前缀无法到达协议格式。 +- **按实例冻结。** 复用是结构性的,而非靠纪律保证:缓存的产物在会话中途不可变,因此提供方的 prompt 缓存从构造上成立,前缀以每步零边际成本扩展了可缓存区域。进程重启或 `ctx.agents.resume()` 产生新实例:它重新组合,任何漂移都可追溯地落在 `'resume'` header 快照上。这就是本 seam 创建的路由规则:会话冻结的开场内容走前缀;会话中途变化的内容走仅追加历史通道(`agent.inject()`、`tools/post-execute` 决策的 `additionalContext`、prompt-submit 的 `additionalContext`——[拦截 seam RFC](2026-06-30-interception-seams.md)),每条都是一次性支付的持久 `context/message`,之后被前缀缓存覆盖。 +- **在压力门禁之前组合。** 组合先于实例的首次 `agent/pre-step`,且 seam 将组合值透传:`agent/pre-step` 携带 `sessionPrefix` 参数,`CompactService.compactIfNeeded(agent, fullSystemPrompt, sessionPrefix, signal)` 将其计入 token 压力估算。如果改为让门禁读取上一个实例折叠后的前缀,则在 resume 或 fork 后的实例中(贡献者可能已增长),门禁会低估压力、跳过压缩,发出超窗口的首个请求。在首次 pre-step 之前组合并将活值透传给 seam,使估算在每一步都精确。被 cancel/dispose 中断的组合(中断落在 waterfall 内部)会被丢弃,永不缓存:感知中止的监听器的降级回退不会泄漏到后续请求中,下一轮次在活信号下重新组合。 -由于组合在边界快照之前运行,组合监听器的会话追加会加入**当前**请求的派生历史。压缩在结构上无法触及前缀(或系统提示词):它重写的是表面节点,而 header 状态从不进入表面。 +由于组合在边界快照之前运行,组合监听器的会话追加会加入**当前**请求的派生历史。压缩在结构上不可能触及前缀(或系统提示词):它重写的是表面节点,而 header 状态从不进入表面。 ## 测试 -[拦截测试](../../../../packages/core/agent-loop/tests/interception.spec.ts)固定了无 header 增量时的组合一次复用、前置顺序、空前缀省略、不可变性,以及组合先于 pre-step;[取消测试](../../../../packages/core/agent-loop/tests/cancel.spec.ts)固定了丢弃与重新组合。会话编解码器、不变式和压缩测试覆盖 header 往返、请求重建和前缀感知的压力计算。快照规范化保留前缀计数,而[固定 header 场景](../testing/2026-07-06-pin-request-header-content-in-one-scenario.md)拥有内容,默认示例保持无前缀。不需要前缀专属的 e2e 测试,因为该 seam 是确定性的且与提供方无关;带密钥的[请求缓存 e2e](../../../../packages/core/agent-loop/tests/request-cache.e2e.ts) 覆盖了其缓存经济性。 +[拦截测试](../../../../packages/core/agent-loop/tests/interception.spec.ts)固定了以下行为:无 header delta 时的组合一次复用、前置插入顺序、空前缀省略、不可变性,以及组合先于 pre-step;[取消测试](../../../../packages/core/agent-loop/tests/cancel.spec.ts)固定了丢弃与重新组合。会话编解码器、不变式和压缩测试覆盖 header 往返、请求重建与前缀感知的压力核算。快照归一化保留前缀计数,[固定 header 场景](../testing/2026-07-06-pin-request-header-content-in-one-scenario.md)拥有内容,默认示例保持无前缀。无需前缀专属的 e2e 测试,因为该 seam 是确定性的且与提供方无关;带密钥的 [request-cache e2e](../../../../packages/core/agent-loop/tests/request-cache.e2e.ts) 覆盖了其缓存经济性。 ## 曾考虑的替代方案 -- **每请求 `before`/`after` 槽位,每步重新计算**(最初提出的形态:每个请求触发一次 waterfall,贡献冻结的 `before` 消息置于历史之前、新鲜的 `after` 消息置于历史之后):否决。每步重新组合 `before` 会引入静默漂移——除非每步记录一个 header 增量,否则没有东西将其锚定到日志——而 `after` 槽位位于不断增长的历史之后,其 token 在每个请求中重新支付,且其后的所有内容不可缓存。与各替代方案对比衡量,当前每种更新模式都能由持久追加更廉价地服务(支付一次,此后缓存读取),唯一没有归属的内容是会话稳定的开场——它需要的是冻结,而非重新计算。 -- **系统提示词分区**(`system-prompt/assemble`):对此类内容否决。组装渲染为单一 `system` 字符串,消息形态的开场放不进去;且系统提示词被设计为每步重新组装(变化时带 header 增量),而开场内容需要的是按实例冻结的语义。 -- **持久化历史开场**(会话开始时 `inject()`):否决。永久历史正是问题陈述中的失败模式——到处回放、可被压缩、跨 resume 陈旧。 -- **按轮次而非按实例组合**:否决。轮次边界的重新组合要么与日志静默失同步,要么强制每次变化产生一个 header 增量,且它每次触发都会破坏提供方缓存;合理的刷新点是实例边界,`'resume'` 快照已经在那里可归因地记录漂移。 -- **在首请求时延迟组合,让压缩读取折叠后的 header**(首次合入时的形态):评审中被取代。折叠值只从实例的第二个请求起才与活跃前缀匹配,因此在 resume/fork 实例的首步上,压力门禁读取的是**上一个**实例的前缀,可能低估压力。在首次 pre-step 之前组合并通过 seam 传递活跃值,使估算在每一步都精确。 -- **承载前缀的专用会话事件**:否决。header 事件在设计上就是请求的非历史记录;第二个事件会成为同一事实的第二个归属,以及又一个需要保持完整的编解码器。 +- **每请求 `before`/`after` 槽位,每步重新计算**(最初提出的形态:一个每请求触发的 waterfall,贡献冻结的 `before` 消息置于历史之前、新鲜的 `after` 消息置于历史之后):否决。每步重新组合 `before` 会引入静默漂移——除非每步记录一个 header delta,否则没有东西将其锚定到日志;`after` 槽位位于不断增长的历史之后,其 token 在每个请求中重复支付,且其后的所有内容不可缓存。对照各替代方案衡量,当前所有更新模式都能通过持久追加更廉价地满足(支付一次,此后缓存读取),而唯一没有归属的内容是会话稳定的开场——它需要的是冻结,而非重新计算。 +- **系统提示词分段**(`system-prompt/assemble`):对此类内容否决。assembly 渲染为单一 `system` 字符串,消息形态的开场放不进去;且系统提示词被设计为每步重新组装(变化时带 header delta),而开场内容需要按实例冻结的语义。 +- **持久化历史开场**(会话启动时 `inject()`):否决。永久历史正是问题陈述中的失败模式——到处被回放、可被压缩、跨 resume 陈旧。 +- **按轮次组合而非按实例组合**:否决。轮次边界的重新组合要么与日志静默失同步,要么强制每次变化都产生 header delta;且它每次触发都会破坏提供方缓存。合理的刷新点是实例边界,`'resume'` 快照已在那里可追溯地记录漂移。 +- **在首次请求时惰性组合,让压缩读取折叠后的 header**(最初合并时的形态):评审中被取代。折叠值仅从实例的第二个请求起才与活前缀匹配,因此在 resume/fork 后的实例首步,压力门禁读取的是**上一个**实例的前缀,可能低估压力。在首次 pre-step 之前组合并将活值透传给 seam,使估算在每一步都精确。 +- **专用会话事件承载前缀**:否决。header 事件按设计就是请求的非历史记录;第二个事件会为同一事实提供第二个归属,并多出一个需要保持完整的编解码器。 ## 后果 -- `agent/pre-step` 和 `CompactService.compactIfNeeded` 携带 `sessionPrefix` 参数:每个 pre-step 监听器和压缩后端都能看到真实的每实例值(所有仓库内实现在同一个变更中更新,遵循预发布立场)。 -- 内容在会话中途变化的贡献者不会被重新读取,直到下一个实例——这是设计意图。需要会话中途目录更新的部署应将变更通知路由到仅追加历史通道,支付一条持久化 `context/message`。 -- 被放弃的 `after` 槽位使请求尾部没有仅请求通道;仓库中没有任何东西需要它,且加回它会重新引入该设计旨在避免的每步重复支付成本。 -- `request/header-delta` 的 `messagePrefix` 分支(整数组替换,空数组编码向缺失的过渡)为编解码器完备性而存在;循环从不行使它,因为缓存的前缀在实例内不可变。 -- 空组合是规范的缺失状态:无贡献者的部署不记录额外 header 字节,其请求就是裸派生。 +- `agent/pre-step` 与 `CompactService.compactIfNeeded` 携带 `sessionPrefix` 参数:每个 pre-step 监听器和压缩后端都能看到真实的按实例值(所有仓库内实现在同一个变更中更新,遵循预发布立场)。 +- 贡献者的内容在会话中途变化时,直到下一个实例才会被重新读取——这是设计意图。需要会话中途目录更新的部署,应将变更通知路由到仅追加历史通道,支付一条持久 `context/message`。 +- 被放弃的 `after` 槽位意味着请求尾部附近没有仅请求通道;仓库中没有任何功能需要它,且恢复它会重新引入本设计旨在避免的每步重复支付成本。 +- `request/header-delta` 的 `messagePrefix` 分支(整数组替换,空数组编码向缺失的过渡)为编解码器完整性而存在;循环从不触发它,因为缓存的前缀在实例内不可变。 +- 空组合即为规范缺失:无贡献者的部署不记录额外的 header 字节,其请求就是裸派生。 diff --git a/docs/rfc/implemented/feature/2026-07-08-repeat-tool-guard.i18n.yaml b/docs/rfc/implemented/feature/2026-07-08-repeat-tool-guard.i18n.yaml index 03817c1073..5c62772145 100644 --- a/docs/rfc/implemented/feature/2026-07-08-repeat-tool-guard.i18n.yaml +++ b/docs/rfc/implemented/feature/2026-07-08-repeat-tool-guard.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-08-repeat-tool-guard.md: 04d5d077a42b54ca7dc04a1efc9ea2f4034b642b -2026-07-08-repeat-tool-guard.zh.md: e20bd06a6902f9fadb77a90e719aaf703d7067cc +2026-07-08-repeat-tool-guard.zh.md: 917d958ca1019eb464b72b0201219de9dde7f658 diff --git a/docs/rfc/implemented/feature/2026-07-08-repeat-tool-guard.zh.md b/docs/rfc/implemented/feature/2026-07-08-repeat-tool-guard.zh.md index e20bd06a69..917d958ca1 100644 --- a/docs/rfc/implemented/feature/2026-07-08-repeat-tool-guard.zh.md +++ b/docs/rfc/implemented/feature/2026-07-08-repeat-tool-guard.zh.md @@ -6,32 +6,32 @@ Status: implemented ## 问题 -模型陷入循环时会反复发出参数逐字节相同的工具调用——重新运行一个失败的 grep、重新读取一个未变化的文件、轮询一个已经给出答案的命令——每一轮往返都消耗 token、挂钟时间和(对付费 API 而言)金钱,却不带来新信息。harness 目前没有任何机制能察觉这一点:循环没有步骤预算,没有插件追踪调用重复,模型只有在碰巧自行改变行为时才能脱困。这种失败模式真实存在且易于检测——[pi-repeat-tool-guard](https://github.com/Kingwl/pi-repeat-tool-guard) 正是将此作为 pi coding-agent 扩展发布的:统计连续相同调用次数,超过阈值后追加一条 `` 告知模型停止重复、改变策略。 +模型陷入循环时,会以字节级相同的参数反复发起同一个工具调用——重新运行一条失败的 grep、重新读取一个未变化的文件、轮询一条已经给出答案的命令——每一轮往返都消耗 token、挂钟时间以及(对付费 API 而言)金钱,却不带来新信息。harness 目前没有任何机制能察觉这一点:循环没有步骤预算,没有插件追踪调用重复,模型只有在碰巧改变自身行为时才能跳出。这种失败模式真实存在且检测成本极低——[pi-repeat-tool-guard](https://github.com/Kingwl/pi-repeat-tool-guard) 正是以 pi coding-agent 扩展的形式提供了这一功能:统计连续相同调用次数,超过阈值后追加一条 `` 告诉模型停止重复并换个方向。 -harness 已经具备 pi 扩展所用的全部 seam,且更好:[拦截 seam RFC](2026-06-30-interception-seams.md) 赋予 `tools/post-execute` 一种正式途径,可以在已完成的调用上附加面向模型的上下文;循环缓冲并注入该上下文,保持调用/结果的邻接关系;注入的上下文是一条已记录的 `context/message`——因此原生守卫无需新增会话事件即可满足「模型可见 ⟺ 已记录」规则。缺的只是插件本身。 +harness 已经具备 pi 扩展所使用的全部 seam,而且更好:[拦截 seam RFC](2026-06-30-interception-seams.md) 赋予 `tools/post-execute` 一种经过认可的方式,将面向模型的上下文附加到已完成的调用上;循环缓冲并注入该上下文,同时保持调用/结果的邻接关系;注入的上下文是一条已记录的 `context/message`——因此原生守卫无需新增会话事件即可满足「模型可见 ⟺ 已记录」规则。缺少的只是插件本身。 ## 决策 -守卫是一个循环卫生插件,而非面向模型的工具。它统计对同一工具以相同规范化参数发起的连续调用次数,并在配置的阈值处注入建议性提醒。它从不延迟、阻塞或改写调用;模型自行决定是否换一种方式重试或结束。 +该守卫是一个循环卫生插件,而非面向模型的工具。它统计对同一工具以相同规范化参数发起的连续调用次数,并在配置的阈值处注入建议性提醒。它从不延迟、阻止或改写调用;模型自行决定是换种方式重试还是结束。 -该插件为 `@deepseek-ai/dsh-repeat-tool-guard`,位于 `packages/guard/repeat-tool-guard/`,开辟 `guard/` 分组用于循环卫生插件(单包分组有先例:[todo-write RFC](2026-06-29-todo-write-tool.md) 发布了 `todo/tool-todo`)。它注册三个监听器,所有状态保存在以 `AgentId` 为键的插件局部 map 中——工具注册表是 context 级别的单例,其 waterfall(瀑布式事件)交错所有 agent 的调用(subagent 运行在同一 context 上),因此按 agent 分键是正确性要求,而非锦上添花。 +插件为 `@deepseek-ai/dsh-repeat-tool-guard`,位于 `packages/guard/repeat-tool-guard/`,开辟 `guard/` 分组用于循环卫生插件(单包(package)分组有先例:[todo-write RFC](2026-06-29-todo-write-tool.md) 发布了 `todo/tool-todo`)。它注册三个监听器,所有状态保存在以 `AgentId` 为键的插件局部 map 中——工具注册表是 context 级别的单例,其 waterfall(瀑布式事件)交错所有 agent(智能体)的调用(subagent 运行在同一个 context 上),因此按 agent 分键是正确性要求,而非锦上添花。 -- **`tools/post-execute`(waterfall)**——唯一的检测点。监听器同时接收 `(exec, result)`,因此计数和提醒投递无需跨事件的 pending map(pi 扩展需要 pending map 仅因其 `tool_call`/`tool_result` 钩子是独立事件)。它始终通过 `next()` 委托,当命中阈值时,将提醒折叠到下游决策的 `additionalContext` 上——这正是[钩子桥接](2026-06-30-hook-bridges.md)已在使用的「观察并丰富」姿态,遵守 waterfall 契约。计数放在此处而非 `tools/pre-execute`,是因为 post-execute 也会为被拒绝的调用触发(`ToolRegistry.execute` 将 deny 路由到同一流水线),而模型反复锤击一个被拒绝的调用恰恰是值得打破的循环。 +- **`tools/post-execute`(waterfall)**——唯一的检测点。监听器同时接收 `(exec, result)`,因此计数和提醒投递无需跨事件的 pending map(pi 扩展需要它,仅因为其 `tool_call`/`tool_result` 钩子是分开的事件)。它始终通过 `next()` 委托,当命中阈值时,将提醒折叠到下游决策的 `additionalContext` 上——这正是[钩子桥接](2026-06-30-hook-bridges.md)已采用的「观察并丰富」姿态,遵守 waterfall 契约。计数放在此处而非 `tools/pre-execute`,因为 post-execute 也会为被拒绝的调用触发(`ToolRegistry.execute` 将 deny 路由到同一条流水线),而模型反复敲击一个被拒绝的调用恰恰是值得打破的循环。 - **`agent/prompt-submit`(waterfall)**——纯重置钩子:通过 `next()` 委托,清除提交 agent 的链。用户介入改变了上下文;跨越介入的重复不是循环。 -- **`agent/status`(emit)**——在 `disposed` 时丢弃该 agent 的状态,限制 map 在 harness 生命周期内的增长。 +- **`agent/status`(emit)**——在 `disposed` 时丢弃该 agent 的状态,使 map 在 harness 生命周期内有界。 ### 检测语义 -链的键为 `(tool name, canonical arguments)`;与前一次被追踪的调用相同则递增该 agent 的连续计数器,不同则重置为 1。规范化方式为深度键排序加 `JSON.stringify`:`ToolExecution.arguments` 按构造即为循环中 `JSON.parse` 的输出(或参数 JSON 格式错误时的原始字符串回退,其本身也是可比较的值),因此 pi 原版对 bigint/循环引用/`undefined` 的处理在此没有输入,被有意去除。 +链的键是 `(tool name, canonical arguments)`;与前一个被追踪调用相同的调用递增该 agent 的连续计数器,不同的被追踪调用将其重置为 1。规范化方式为深度键排序加 `JSON.stringify`:`ToolExecution.arguments` 按构造就是循环中 `JSON.parse` 的输出(或格式错误的参数 JSON 的原始字符串回退,其本身也是可比较的值),因此 pi 原版对 bigint/循环引用/`undefined` 的处理在此没有输入,被有意去除。 -两条刻意的规则,均记录在[包 README](../../../../packages/guard/repeat-tool-guard/README.md) 中,因为它们是读者不看文档会猜测的行为: +两条刻意的规则,均记录在[包 README](../../../../packages/guard/repeat-tool-guard/README.md) 中,因为它们是读者否则只能猜测的行为: -- **未追踪的调用对链透明。** 被 `include`/`exclude` 排除的调用既不递增也不重置计数器,因此 `grep X → todo_write → grep X` 在 `todo_write` 被排除时仍计为两次连续的 `grep X`。这正是排除有用的原因——夹在循环中的记账工具不得洗白循环——也是 pi 扩展的(未文档化的)语义,有意保留并写明。 +- **未追踪的调用对链透明。** 被 `include`/`exclude` 排除的调用既不递增也不重置计数器,因此 `grep X → todo_write → grep X` 在 `todo_write` 被排除时仍计为两次连续的 `grep X`。这正是排除功能有用的原因——穿插在循环中的簿记工具不得为循环洗白——也是 pi 扩展的(未文档化的)语义,有意保留并明确写下。 - **没有 agent 的调用被忽略。** 直接调用 `ctx.tools.execute()` 的调用方(测试、非循环消费方)没有可提醒的模型,也没有可作键的 `AgentId`。 ### 提醒投递 -提醒使用 `additionalContext` 并标注插件来源,保留原始 `tool/result`。首次阈值发出简短提示;后续阈值包含工具名、计数和有长度上限的参数预览,而比较仍使用完整的规范化字符串。已有的下游上下文在守卫的 source 下拼接,因为 `HookContext` 支持单一 source。 +提醒使用带插件 source 的 `additionalContext`,保留原始 `tool/result`。第一个阈值发出简短提示;后续阈值包含工具名、计数和有界的参数预览,而比较仍使用完整的规范化字符串。已有的下游上下文在守卫的 source 下拼接,因为 `HookContext` 只支持一个 source。 ### 配置 @@ -45,32 +45,32 @@ harness 已经具备 pi 扩展所用的全部 seam,且更好:[拦截 seam RF argumentsPreviewChars: 500 # default; cap on arguments quoted in the detailed reminder ``` -`thresholds` 在加载时校验,空列表、非整数、小于 2 的值或重复值都会抛出异常——配置错误大声失败,取代 pi 原版的静默回退到默认值。`include`/`exclude` 条目支持 `*` 通配符。模式是对调用时实际存在的工具名的谓词,而非对注册表条目的引用,因此匹配不到任何当前已注册工具的条目不是错误——与 `toolOrder` 的引用检查不同,`exclude: [mcp_*]` 在未加载 MCP 工具的部署中必须保持有效。 +`thresholds` 在加载时校验,遇到空列表、非整数、小于 2 的值或重复项时抛出异常——配置错误快速失败,取代 pi 原版的静默回退到默认值。`include`/`exclude` 条目支持 `*` 通配符。模式是对调用时实际存在的工具的谓词,而非对注册表条目的引用,因此匹配不到当前已注册工具的条目不是错误——与 `toolOrder` 的引用检查不同,`exclude: [mcp_*]` 在未加载 MCP 工具的部署中也必须保持有效。 ## 测试 -- **单元测试:** 使用脚本化适配器的真实循环覆盖计数与重置规则、未追踪透明性、dispose 清理、按 agent 隔离、规范化参数键序、升级、被拒绝的调用、无 agent 执行、通配符转义、无效配置,以及下游阻塞或替换决策,达到逐文件 100% 覆盖率。 -- **快照测试:** keyless 的 `repeat-tool-guard` 场景发出五次相同的 `todo_write` 调用,将第三次的温和提醒和第五次的详细提醒固定在 ACP 输出和会话日志中。该插件在实时示例中加载,但在其他场景中保持静默。 -- **E2e:** 无;该插件是确定性的且与提供方无关,其 seam 契约由各自的所有者覆盖。 +- **单元测试:** 使用脚本化适配器的真实循环,覆盖计数与重置规则、未追踪透明性、dispose(资源释放)清理、按 agent 隔离、规范化参数键序、升级、被拒绝的调用、无 agent 执行、通配符转义、无效配置,以及下游 block 或 replacement 决策,达到逐文件 100% 覆盖率。 +- **快照测试:** keyless 的 `repeat-tool-guard` 场景发起五次相同的 `todo_write` 调用,在 ACP 输出和会话日志中固定第三次调用的温和提醒与第五次调用的详细提醒。该插件在实时示例中加载,但在其他场景中保持静默。 +- **E2e 测试:** 无。该插件是确定性的且与提供方无关,其 seam 契约由各自的所有者覆盖。 ## 曾考虑的替代方案 -- **将提醒追加到工具结果中**(`accept` 并替换 `content`——pi 扩展的机制,它修改结果内容是因为那是其 API 提供的唯一通道):否决。这会让已记录的 `tool/result` 对工具实际返回的内容撒谎,而 `additionalContext` 正是为 post-execute 评注设计的独立正式通道,循环级缓冲保持了调用/结果的邻接关系。 -- **在 `tools/pre-execute` 中计数并使用 pending-reminder map**(pi 的两阶段形态):否决。post-execute 单独就能同时看到 `(exec, result)` 且也会为被拒绝的调用触发,因此一个监听器、无跨事件状态,以更少的机制覆盖严格更多的尝试。 -- **在最高阈值升级为 `block`**:在初始范围内否决。阻塞调用会惩罚合理的相同重复(轮询长时间运行的终端、重新检查 agent 预期会变化的文件),而建议性提醒让模型保持控制权。待有证据后重新审视;决策形状(`PostToolDecision`)已支持此选项。 -- **通过 CC/Codex 桥接的按部署外部钩子**(`PostToolUse` 脚本):否决作为最终答案。它对单个部署有效,但一个已发布、有单元测试、可通过 `cordis.yml` 配置的插件才是 harness 原生形式,且无逐调用的子进程开销。 -- **在 `agent-loop` 中设置循环级步骤或重复预算**:否决。「用插件,不改循环」;硬性步骤预算是更粗粒度的正交控制,需要单独的提案。 -- **模糊/近似相同检测**(路径归一化、相似但不完全相同的参数):否决。规范化后的精确匹配廉价、确定性强且可向模型解释;相似度阈值会引入误报,在复杂度得到证据支撑之前不应引入。 -- **将包放在 `core/`**:否决。core 是产品主干;行为守卫是可选的叶子插件,`todo/` 先例表明每个插件家族用一个小型专属分组。 +- **将提醒追加到工具结果中**(以替换 `content` 的方式 `accept`——pi 扩展的机制,它修补结果内容是因为那是其 API 提供的唯一通道):否决。这会让已记录的 `tool/result` 对工具实际返回的内容撒谎,而 `additionalContext` 的存在正是作为 post-execute 评注的独立认可通道,循环级缓冲保持了调用/结果的邻接关系。 +- **在 `tools/pre-execute` 中计数并使用 pending-reminder map**(pi 的两阶段形态):否决。post-execute 单独就能同时看到 `(exec, result)` 且也为被拒绝的调用触发,因此一个监听器、无跨事件状态即可以更少的机制覆盖严格更多的尝试。 +- **在最高阈值升级为 `block`**:在初始范围内否决。阻止调用会惩罚合法的相同重复(轮询长时间运行的终端、重新检查 agent 预期会变化的文件),而建议性提醒让模型保持控制权。待有证据后重新审视;决策形状(`PostToolDecision`)已支持此选项。 +- **通过 CC/Codex 桥接的逐部署外部钩子**(一个 `PostToolUse` 脚本):否决作为最终答案。它对单个部署有效,但一个已发布、有单元测试、可通过 `cordis.yml` 配置的插件才是 harness 原生的形式,且没有逐调用的子进程开销。 +- **在 `agent-loop` 中设置循环级步骤或重复预算**:否决。「用插件,不改循环」;硬性步骤预算是一种更粗粒度的正交控制,需要自己的提案。 +- **模糊/近似相同检测**(路径归一化、相似但不完全相同的参数):否决。规范化后的精确匹配成本低、确定性强、且可向模型解释;相似度阈值引入误报风险,需要证据才能换取复杂度。 +- **将包放在 `core/`**:否决。core 是产品主干;行为守卫是可选的叶子插件,`todo/` 的先例是每个插件族一个小型专属分组。 ## 后果 -- 提醒在设计上是建议性的:有意重复相同调用的幂等轮询模式在超过阈值后仍会收到提示,减压阀是配置(`thresholds`、`exclude`)加上提醒文本中明确允许「在已收集足够证据时结束」的措辞。每次触发在下一次请求中增加提醒 token 开销;阈值限制了触发频率。 -- 链状态仅存于内存:从持久化恢复的会话以全新的链开始,因此跨越恢复的循环比实时循环更晚收到提醒——可接受,守卫是启发式提示而非已记录的不变式,持久化计数器状态带来的收益不值得其复杂度。 -- 当多个 post-execute 生产者在同一次调用上附加上下文时,折叠在守卫的 `source` 下拼接;插件间的顺序遵循监听器注册顺序。该 seam 无法表示混合来源——这是继承自 `HookContext` 的限制,不属于本插件。 +- 提醒在设计上是建议性的:有意重复相同调用的幂等轮询模式仍会在超过阈值后收到提示,减压阀是配置(`thresholds`、`exclude`)加上明确允许「在已收集足够证据时结束」的提醒文本。每次触发在下一次请求中增加提醒 token 的开销;阈值限制了触发频率。 +- 链状态仅存于内存:从持久化恢复的会话以全新的链开始,因此跨越恢复的循环比实时循环更晚收到提醒——可以接受,守卫是启发式提示而非已记录的不变式,持久化计数器状态带来的收益不值得其复杂度。 +- 当多个 post-execute 生产者在同一次调用上附加上下文时,折叠在守卫的 `source` 下拼接;插件间的顺序遵循监听器注册顺序。该 seam 无法表示混合来源——这是继承自 `HookContext` 的限制,不归本插件所有。 -## 延后 +## 延后事项 -- 上下文压缩(compaction)不重置链:压缩后的历史改变了模型所见,但重复风险通常在压缩后仍然存在。 -- 在高阈值升级为 `block` 未实现;`PostToolDecision` 已支持此选项,待证据出现后可启用。 -- subagent 的链按 agent 隔离;在出现具体需求之前不引入共享机制。 +- 压缩(compaction)不重置链:压缩后的历史改变了模型所见的内容,但重复风险通常在压缩后仍然存在。 +- 在高阈值升级为 `block` 未实现;`PostToolDecision` 已支持此选项,待证据到来时启用。 +- subagent 的链按 agent 隔离;在出现具体用例之前不提供共享机制。 diff --git a/docs/rfc/implemented/feature/2026-07-08-self-referential-cordis-toolset.i18n.yaml b/docs/rfc/implemented/feature/2026-07-08-self-referential-cordis-toolset.i18n.yaml index e667e7a029..ba1e1f60be 100644 --- a/docs/rfc/implemented/feature/2026-07-08-self-referential-cordis-toolset.i18n.yaml +++ b/docs/rfc/implemented/feature/2026-07-08-self-referential-cordis-toolset.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-08-self-referential-cordis-toolset.md: 62b97dc4bdbd0e0b5b1f67f77c763065c79964ed -2026-07-08-self-referential-cordis-toolset.zh.md: ce220d256dbb3d43514702e57e71728fdc82a788 +2026-07-08-self-referential-cordis-toolset.zh.md: 44648d7a3f195f2dc84121c0d014e5193484b106 diff --git a/docs/rfc/implemented/feature/2026-07-08-self-referential-cordis-toolset.zh.md b/docs/rfc/implemented/feature/2026-07-08-self-referential-cordis-toolset.zh.md index ce220d256d..44648d7a3f 100644 --- a/docs/rfc/implemented/feature/2026-07-08-self-referential-cordis-toolset.zh.md +++ b/docs/rfc/implemented/feature/2026-07-08-self-referential-cordis-toolset.zh.md @@ -1,82 +1,82 @@ -# RFC:自引用 Cordis 工具集 - -Status: implemented +# RFC:自引用 cordis 工具集 [English](2026-07-08-self-referential-cordis-toolset.md) | 中文 +Status: implemented + ## 问题 -本 harness 中的一切都是 Cordis 插件,但运行在该插件运行时内部的 agent(智能体)既看不到也碰不到它:它无法枚举周围的服务和事件,无法在会话中途为自己添加新工具,也无法组合自己发明的能力。把这种能力交给模型值得探索——一个能审视并修改自身运行时的自引用 agent——但它同时引出三个正确性问题,而本设计的核心正是回答这些问题,而非单纯的「让模型执行代码」机制。 +本 harness 中的一切都是 cordis 插件,但运行在该插件运行时内部的 agent(智能体)既看不到也碰不到它:它无法枚举周围的服务和事件,无法在会话中途为自己添加新工具,也无法组合自己发明的能力。赋予模型这种能力值得探索——一个能审视并修改自身运行时的自引用 agent——但这同时引发三个正确性问题,本设计的核心正是回答这些问题,而非单纯的「让模型执行代码」机制。 -第一,模型编写的注册必须在注册发生时就被校验:格式错误的工具 schema 必须在注册时失败,而非等到后续请求尝试将其组装进提示词时才暴露。第二,模型编写的代码需要调用它从未见过源码的服务 API——猜测方法签名,更糟的是猜测返回值形状,会耗费大量盲目试探步骤。第三,模型挂载的一切都必须完全可 dispose(资源释放):模型可以按需释放,宿主插件重载时普通的插件生命周期也能释放,否则长会话会积累遗留的监听器和工具。 +第一,模型编写的注册必须在注册发生时就完成校验:格式错误的工具 schema 必须在注册时失败,而不是等到后续请求尝试将其组装进提示词时才报错。第二,模型编写的代码需要调用它从未见过源码的服务 API——靠猜测方法签名、更糟糕的是猜测返回值结构,会消耗大量盲目试探的步骤。第三,模型挂载的一切都必须完全可释放:模型可以按需释放,普通的插件生命周期在宿主插件重载时也会释放,否则长会话会积累遗留的监听器和工具。 ## 决策 -该工具集以 [`@deepseek-ai/dsh-tool-cordis`](../../../../packages/cordis/tool-cordis/README.md) 发布——一个新的顶层 `packages/cordis/` 分组——并由 [`examples/cordis-agent`](../../../../examples/cordis-agent/README.md) 演示。它为模型提供三个工具,操作模型自身运行其中的活跃 Cordis 运行时:审视它、向其中挂载模型编写的插件、再将它们 dispose。 +该工具集以 [`@deepseek-ai/dsh-tool-cordis`](../../../../packages/cordis/tool-cordis/README.md) 发布——一个新的顶层 `packages/cordis/` 分组——并由 [`examples/cordis-agent`](../../../../examples/cordis-agent/README.md) 演示。它为模型提供三个工具,操作模型自身运行其中的活跃 cordis 运行时:审视它、将模型编写的插件挂载进去、再将其释放。 -vm 隔离了意外的全局污染,上下文门面隐藏了框架内部实现。二者都不限制已暴露服务的权限:一个挂载可以调用 `ctx.bash` 以宿主执行器的权限运行命令,也能触及真实文件系统和网络服务。这是一个需要主动启用的开发工具,信任等级与 bash 等同,既不是安全边界,也不是产品默认配置。 +vm 隔离了意外的全局污染,上下文门面隐藏了框架内部细节。但二者都不限制已暴露服务的权限:一个挂载可以调用 `ctx.bash` 以宿主执行器的权限运行命令,也能访问真实的文件系统和网络服务。这是一个需要显式启用的开发工具,信任等级与 bash 相当,不是安全边界,也不是产品默认配置。 ### 三个工具 | 工具 | 契约 | |---|---| -| `cordis_inspect` | 对活跃运行时的只读报告,每个 `what` 值对应一个 Markdown 段落(省略 `what` 则返回全部段落)。从不修改状态。 | -| `cordis_mount` | 在 `node:vm` 沙箱中执行 `code`(一个异步 JavaScript 函数体);代码必须 `return` 一个 Cordis 插件,该插件作为 `cordis-dynamic` 分组 fiber 的子节点挂载,并以一个新生成的 id(`dyn-1`、`dyn-2`、……)追踪。 | -| `cordis_unmount` | 按 id dispose 一个动态挂载,并等待 disposal 达到静止——该插件所做的每一项注册都被撤销,而不仅仅是请求停止。 | +| `cordis_inspect` | 对活跃运行时的只读报告,每个 `what` 值对应一个 Markdown 段落(省略 `what` 则输出全部段落)。从不产生变更。 | +| `cordis_mount` | 在 `node:vm` 沙箱中执行 `code`(一个异步 JavaScript 函数的函数体);代码必须 `return` 一个 cordis 插件,该插件作为 `cordis-dynamic` 分组 fiber 的子节点挂载,并以一个新 id(`dyn-1`、`dyn-2`……)跟踪。 | +| `cordis_unmount` | 按 id 释放一个动态挂载,并等到释放达到静止状态后才返回——该插件所做的每一项注册都被撤销,而不仅仅是请求停止。 | -`cordis_inspect` 的段落:`services`(每个已提供的 ctx 服务及其所属 fiber,非活跃的 owner 会被标记)、`plugins`(来自 `ctx.registry` 的所有已加载插件的扁平列表及其生命周期状态——展示加载了哪些能力,刻意不展示树形结构)、`tools`(模型可调用的工具)、`dynamic`(挂载表:id、名称、状态、提供的服务、等待的服务)、`api`(来自生成目录的活跃服务签名及其引用的类型形状)、`events`(harness 事件及其分发模式和签名)。面向模型的工具描述携带模型在调用时所需的操作规则;[生成的工具目录](../../../tool-catalog.md)是其完整渲染。 +`cordis_inspect` 的段落:`services`(每个已提供的 ctx 服务及其所属 fiber,非活跃的所有者会被标记)、`plugins`(来自 `ctx.registry` 的所有已加载插件的扁平列表及其生命周期状态——展示加载了哪些能力,刻意不展示树形结构)、`tools`(模型可调用的工具)、`dynamic`(挂载表:id、名称、状态、提供的服务、等待的服务)、`api`(来自生成目录的活跃服务签名及其引用的类型形状)和 `events`(harness 事件及其分发模式和签名)。面向模型的工具描述携带了模型在调用时所需的操作规则;[生成的工具目录](../../../tool-catalog.md)是其完整呈现。 ### 沙箱语义 -挂载代码作为异步函数体在一个新的 vm realm 中运行。其文档化的接口面将文件、网络、进程和定时器访问引导至 Cordis 服务,使挂载保持可审视和可 dispose。宿主 realm 的辅助手段仍使 Node 逃逸成为可能,与信任姿态一致。`vmTimeoutMs` 仅约束同步执行部分。 +挂载代码以异步函数体的形式在一个新的 vm realm 中运行。其文档化的接口面将文件、网络、进程和定时器访问引导至 Cordis 服务,使挂载保持可审视和可释放。宿主 realm 的辅助手段仍然使 Node 逃逸成为可能,这与信任姿态一致。`vmTimeoutMs` 仅约束同步执行部分。 -沙箱全局变量刻意精简:一个带标签的直通 `console`(在宿主 stdout/stderr 上输出 `[cordis:] …`,使得挂载调用结束很久后触发的监听器仍能输出到用户可见之处)、`harness.defineTool` / `harness.registerTool` 注册对、新 vm 上下文缺少的编码原语(`btoa`/`atob` 作为宿主闭包封装 `Buffer`——这是一个经过批准的例外,`Buffer` 本身从不暴露——加上 `TextEncoder`/`TextDecoder`),以及对被扣留的 Node API 的可调用陷阱(`require`、`setTimeout`/`setInterval`/`setImmediate`/`clearTimeout`/`clearInterval`、`fetch`),调用时抛出错误并指名 Cordis 替代方案。只有函数形状的全局变量被陷阱拦截;`process` 和 `Buffer` 保持 `undefined`,使 `typeof` 特性探测保持惰性而非触发抛出异常的访问器。 +沙箱全局变量刻意精简:一个带标签的直写 `console`(在宿主 stdout/stderr 上输出 `[cordis:] …`,这样在挂载调用之后很久才触发的监听器输出仍能落到用户可见的地方)、`harness.defineTool` / `harness.registerTool` 注册对、新 vm 上下文缺少的编码原语(`btoa`/`atob` 作为基于 `Buffer` 的宿主闭包——这是一个经过审批的例外,`Buffer` 本身从不暴露——加上 `TextEncoder`/`TextDecoder`),以及对被扣留的 Node API 的可调用陷阱(`require`、`setTimeout`/`setInterval`/`setImmediate`/`clearTimeout`/`clearInterval`、`fetch`),这些陷阱会抛出一条重定向消息指明 cordis 替代方案。只有函数形态的全局变量才设陷阱;`process` 和 `Buffer` 保持 `undefined`,这样 `typeof` 特性探测保持惰性而不会引爆一个抛异常的访问器。 -挂载代码通过三道控制跨越 vm 边界。双 realm `instanceof` 同时识别宿主和 vm 对象。`harness.defineTool` 将结果规范化为宿主 realm 的 JSON,并在记录日志前校验 `ToolExecuteReturn` 形状。挂载的插件接收一个白名单上下文门面,而非原始或直通的 `Context`;框架管道和以 context 为值的返回会被拒绝。服务读取要求声明 `inject`,保持 Cordis 的激活和卸载语义。`ctx.tools.get` 仅暴露 schema 视图,使挂载代码无法绕过 `ToolRegistry.execute` 直接调用定义。 +挂载代码通过三道控制跨越 vm 边界。双 realm `instanceof` 同时识别宿主和 vm 对象。`harness.defineTool` 将结果规范化为宿主 realm 的 JSON,并在记录日志前校验 `ToolExecuteReturn` 形状。挂载的插件接收的是一个白名单上下文门面,而非原始或透传的 `Context`;框架管道和以 context 为值的返回会被拒绝。服务读取需要声明 `inject`,保留 Cordis 的激活与卸载语义。`ctx.tools.get` 仅暴露 schema 视图,因此挂载代码无法绕过 `ToolRegistry.execute` 直接调用定义。 -边界将无歧义的 JSON-Schema 形式规范化为 `SchemaSpec`,包括对象包装、`integer` 和可选字段。无效词汇会失败并给出可接受的替代方案。解析错误、TypeScript 错误、缺少 return、Node API 错误和重复工具错误会包含相关源代码行或纠正性契约,但不叙述实现内部细节。 +边界将无歧义的 JSON-Schema 形式规范化为 `SchemaSpec`,包括对象包装器、`integer` 和可选字段。无效词汇会报错并给出可接受的替代方案。解析错误、TypeScript 错误、缺少 return、Node API 误用和重复工具名等错误信息包含相关源码行或纠正性契约,不叙述实现内部细节。 ### 动态分组与挂载生命周期 -所有动态挂载都是工具插件下方一个 `cordis-dynamic` 分组的子节点,因此普通的 fiber disposal 即可处理重载和卸载。挂载会等待 settlement;启动失败会在返回错误前 dispose 该 fiber。已 settle 但处于 pending 状态的挂载仍然可见,并列出其缺失的注入。`cordis_unmount` 等待挂载 fiber 的 disposal。 +所有动态挂载都是工具插件下方 `cordis-dynamic` 分组的子节点,因此普通的 fiber 释放即可处理重载和卸载。挂载会等待 settlement;启动失败时在返回错误前释放 fiber。已 settle 但处于 pending 状态的挂载仍然可见,并列出其缺失的注入。`cordis_unmount` 等待挂载 fiber 的释放完成。 ### 通过 provide/inject 实现跨挂载组合 -挂载之间通过普通的 Cordis 服务语义相互关联,以各自的 id 作为生命周期句柄:挂载 A 调用 `ctx.provide('foo', value)`,挂载 B 声明 `inject: ['foo']` 并在 `foo` 存在的瞬间激活;如果 B 先挂载,它会保持 pending 状态并列出缺失的服务;卸载 A 会使 B 回到 pending(其注册被撤销),之后重新 provide 会通过一个新的沙箱门面重新运行 B 的 `apply`;重复 provide 会大声失败并指名拥有该服务的 fiber。一个 realm 注意事项:挂载提供的服务值是 vm realm 对象——从任何地方调用其方法都能工作,但消费方不得假设其上有宿主原型。 +挂载之间通过普通的 cordis 服务语义相互关联,以各自的 id 作为生命周期句柄:挂载 A 调用 `ctx.provide('foo', value)`,挂载 B 声明 `inject: ['foo']` 并在 `foo` 存在的瞬间激活;如果 B 先挂载,它保持 pending 状态并列出缺失的服务;卸载 A 使 B 回到 pending(其注册被撤销),之后重新 provide 会通过一个新的沙箱门面重新运行 B 的 `apply`;重复 provide 会明确报错并指出拥有该服务的 fiber。一个 realm 注意事项:由挂载 provide 的服务值是 vm realm 对象——从任何地方调用其方法都能工作,但消费方不得假设它具有宿主原型。 ### 生成的 API 目录 -`cordis_inspect` 从生成的目录而非重复的表格提供 API 和事件数据。生成器复用 Cordis 目录的 AST 扫描,输出服务摘要、签名、事件模式、引用的类型声明和继承的上下文接口面。有歧义的类型名被省略,过大的声明被标记为截断。 +`cordis_inspect` 从生成的目录提供 API 和事件数据,而非维护一份重复的表格。生成器复用 Cordis 目录的 AST 扫描,输出服务摘要、签名、事件模式、引用的类型声明以及继承的 context 接口面。有歧义的类型名被省略,过大的声明被标记为截断。 -新鲜度像所有生成产物一样受门禁保护:`pnpm run verify-cordis-api`(在 `doc-sync` 中)在内存中重新生成并在有任何 diff 时失败,因此修改了公开签名的 JSDoc 变更在不重新生成模型所读目录的情况下无法发布。运行时,inspect 工具将目录与活跃运行时取交集而非直接转储:有目录条目的活跃服务渲染摘要 + 签名,没有目录条目的活跃服务(挂载提供的)渲染名称 + 所属 fiber,有目录条目但没有活跃提供方的服务简要列出,引用的类型形状随后附上。 +新鲜度像所有生成产物一样受门禁约束:`pnpm run verify-cordis-api`(在 `doc-sync` 中)在内存中重新生成并在有任何 diff 时失败,因此修改了公开签名的 JSDoc 变更如果不重新生成模型读取的目录就无法合入。运行时 inspect 工具将目录与活跃运行时取交集而非直接转储:有目录条目的活跃服务渲染摘要 + 签名,没有目录条目的活跃服务(挂载提供的)渲染名称 + 所属 fiber,有目录条目但无活跃提供方的服务简要列出,引用的类型形状随后附上。 ### 配置、渲染与可观测性 -该插件暴露一个配置字段,由 schemastery 校验并记录在[配置目录](../../../config-catalog.md)中:`vmTimeoutMs`(默认 5000),挂载代码同步执行部分的毫秒上限。工具名称、`cordis-dynamic` 分组名和 `dyn-` id 前缀是结构性词汇,保持固定。三个工具均按[工具实操手册](../../../cookbook/adding-a-tool.md)渲染为 `generic` 卡片(`cordis_inspect` 为 `read`,`cordis_mount` 为 `execute` 并将代码作为 `rawInput` 携带,`cordis_unmount` 为 `delete`),不覆盖 `presentResult`。 +该插件暴露一个配置字段,由 schemastery 校验并记录在[配置目录](../../../config-catalog.md)中:`vmTimeoutMs`(默认 5000),挂载代码同步执行部分的毫秒上限。工具名、`cordis-dynamic` 分组名和 `dyn-` id 前缀是结构性词汇,保持固定。三个工具均按[工具实操手册](../../../cookbook/adding-a-tool.md)渲染为 `generic` 卡片(`cordis_inspect` 为 `read`,`cordis_mount` 为 `execute` 并将代码作为 `rawInput` 携带,`cordis_unmount` 为 `delete`),不覆盖 `presentResult`。 -「模型可见 ⟺ 已记录」成立,且不引入新的会话事件类型:挂载或卸载仅通过其自身的 `tool/call` / `tool/result` 对可见(循环会记录它),而挂载引起的工具集变化则由循环在 schema 在步骤间变化时已有的请求头 delta 日志记录。刻意不设 `cordis/mount` 溯源事件——它只会重复工具调用对已记录的内容。动态挂载是进程生命周期的,不是会话状态:恢复持久化的会话会重建对话但不会重新挂载插件。 +「模型可见 ⟺ 已记录」成立,且无需新的会话事件类型:挂载或卸载仅通过其自身的 `tool/call` / `tool/result` 对可见(循环会记录它们),而挂载引起的工具集变化由循环在 schema 在步骤间发生变化时已有的 request-header delta 记录。刻意不设 `cordis/mount` 溯源事件——它只会重复工具调用对已记录的内容。动态挂载是进程生命周期的,不是会话状态:恢复一个持久化的会话会重建对话,但不会重新挂载插件。 ## 曾考虑的替代方案 -**用结构化的逐能力注册工具替代 `cordis_mount`。** 最诱人的替代方案是一个带有显式 `name` / `description` / `parameters` / `code` 字段的 `cordis_register_tool`(以及兄弟工具 `cordis_register_listener`、`cordis_register_service`、……),而非单一的「挂载一个插件」原语。否决原因:它唯一的真正优势——对最常见的单一场景省去插件样板——不足以抵偿其代价,而单一的挂载原语能一次性覆盖所有能力。 +**用结构化的逐能力注册工具替代 `cordis_mount`。** 最具吸引力的替代方案是一个带有显式 `name` / `description` / `parameters` / `code` 字段的 `cordis_register_tool`(以及兄弟工具 `cordis_register_listener`、`cordis_register_service`……),而非单一的「挂载一个插件」原语。否决原因:它唯一的真正优势——对最常见的单一场景免去插件样板代码——不足以抵偿其代价,而单一的 mount 原语能一次性覆盖所有能力。 | 维度 | 结构化逐能力工具 | 单一 `cordis_mount` | |---|---|---| -| Schema 正确性 | `parameters` 仍是模型编写的 JSON 对象,需要 SchemaSpec 校验,只是提前了一步 | 同样的校验在沙箱边界运行,同样的指导性错误 | -| 代码字段 | `execute` 体仍是 vm 中模型编写的 JS;realm 和服务调用正确性问题不变 | 一个沙箱、一条规范化路径、一道受守护的注册 | -| 能力覆盖面 | 仅限工具;监听器、服务、`inject` 关系各需另一个结构化工具——接口面无限增长 | 一套词汇(一个 Cordis 插件)覆盖当前和未来的所有效果 | -| 跨挂载组合 | 在工具注册载荷中无法表达 | 原生 `provide`/`inject`,普通 Cordis 语义 | -| 可审视性 | 注册的东西在插件列表中无法作为插件展示 | 模型挂载的东西正是 `cordis_inspect` 渲染的东西 | -| 模型易用性 | 对最常见的单一场景有优势(无插件样板) | 通过挂载描述中的规范示例加上教导正确做法的边界错误来缓解 | +| Schema 正确性 | `parameters` 仍然是模型编写的 JSON 对象,需要 SchemaSpec 校验,只是提前了一步 | 同样的校验在沙箱边界运行,同样的指导性错误信息 | +| 代码字段 | `execute` 函数体仍然是 vm 中模型编写的 JS;realm 和服务调用的正确性问题不变 | 一个沙箱、一条规范化路径、一处受保护的注册 | +| 能力覆盖面 | 仅限工具;监听器、服务、`inject` 关系各需另一个结构化工具——接口面无限增长 | 一套词汇(cordis 插件)覆盖当前和未来的所有效果 | +| 跨挂载组合 | 在工具注册载荷中无法表达 | 原生 `provide`/`inject`,普通的 cordis 语义 | +| 可审视性 | 注册的东西无法在插件列表中显示为插件 | 模型挂载的正是 `cordis_inspect` 渲染的 | +| 模型人机工程学 | 对最常见的单一场景有优势(无插件样板) | 通过 mount 描述中的规范示例加边界错误信息教会正确调用来缓解 | -因此,正确性投入放在能一次性覆盖所有能力的地方:通过 `cordis_inspect` 暴露的生成 API 目录,以及沙箱边界校验——其错误消息教导正确的调用方式。结构化注册工具日后仍可作为语法糖添加,合成挂载代码即可;本设计不排斥它。 +因此正确性投入放在能一次性为所有能力带来回报的地方:通过 `cordis_inspect` 呈现的生成 API 目录,以及沙箱边界校验(其错误信息教会正确的调用方式)。结构化注册工具日后仍可作为语法糖添加,由它合成 mount 代码;本设计不排斥这一可能。 -**在工具中手工维护服务/事件参考。** inspect 工具的第一版携带了一张手写的服务方法签名表。它被生成的 `api-catalog.ts` 取代,因为手写表在签名变化的瞬间就会与 JSDoc 脱节,且没有门禁检测这种漂移;而生成产物的新鲜度由与文档使用同一 AST 的检查来保证。 +**在工具中手工维护服务/事件参考。** inspect 工具的第一版携带了一份手写的服务方法签名表。它被生成的 `api-catalog.ts` 取代,因为手写表在签名变化的瞬间就会与 JSDoc 脱节且没有门禁约束这种漂移,而生成产物的新鲜度由文档使用的同一套 AST 检查。 -**新增 `cordis/mount` 会话事件。** 记录每次挂载(源码、名称)的持久溯源事件有明确先例(`hook/invoked`、`compact/start`)。v1 中否决:挂载和卸载已经作为 `tool/call` / `tool/result` 对可见,工具集变化已经作为请求头 delta 被记录,因此专用事件只会重复记录。如果审计用例需要将挂载溯源与工具调用分离,日后仍可添加。 +**新增 `cordis/mount` 会话事件。** 一个持久的溯源事件记录每次挂载(源码、名称)有明确先例(`hook/invoked`、`compact/start`)。v1 中予以否决:挂载和卸载已经作为 `tool/call` / `tool/result` 对可见,工具集变化已经作为 request-header delta 被记录,因此专用事件只会重复记录。如果审计用例需要将挂载溯源从工具调用中分离出来,日后仍可添加。 -**加固的 / 能力受限的沙箱。** 拦截 Node 内置模块并向挂载代码提供白名单门面而非原始 context,可能暗示意图是为安全而沙箱化。明确声明并非如此:陷阱和门面收窄的是挂载代码所见的*接口面*——将其引导至 Cordis 服务、远离易泄漏的 Node 内置模块和框架内部——目的是正确性和封堵未守护的 context 逃逸,但门面暴露的能力(`ctx.bash`、`ctx.fs`、`ctx.web`)触及真实运行时,因此它不是安全边界。真正的安全边界(独立进程、权限提示)对一个开发/主动启用的工具集来说超出范围,且与其核心目标——将活跃运行时交给模型——相悖。 +**加固的/能力受限的沙箱。** 对 Node 内置模块设陷阱并向挂载代码提供白名单门面而非原始 context,可能暗示意图是为安全而沙箱化。这里明确不是:陷阱和门面收窄的是挂载代码所见的*接口面*——将其引导至 cordis 服务、远离易泄漏的 Node 内置模块和框架内部——目的是正确性和封堵未受保护的 context 逃逸,但门面暴露的能力(`ctx.bash`、`ctx.fs`、`ctx.web`)触及真实运行时,因此它不是安全边界。真正的安全边界(独立进程、权限提示)超出了一个开发/显式启用工具集的范围,且会与其核心目的——将活跃运行时交给模型——相冲突。 ## 后果 -该工具集是刻意需要主动启用的,具有完全权限的 `ctx`,因此部署方采用它的意识程度与采用 bash 工具相同。以下事实由工具描述直接告知模型:waterfall(瀑布式事件)监听器(如 `tools/pre-execute`)如果不调用 `next()` 就返回,会否决整条链,因此挂载的监听器可以瘫痪 agent 自身的工具分发([waterfall 语义](../../../cordis-primer.md#cordis-waterfall-semantics));挂载代码在当前轮次的工具调用内运行,因此 await 任何只在该轮次结束后才 resolve 的东西会死锁;`vmTimeoutMs` 仅约束同步执行;挂载不会在会话恢复后存活。 +该工具集是刻意的显式启用设计,具有完全特权的 `ctx`,因此部署方采用它的意识程度应与 bash 工具相当。以下几个事实由工具描述直接告知模型:一个 waterfall(瀑布式事件)监听器(如 `tools/pre-execute`)如果不调用 `next()` 就返回,会否决整条链,因此一个挂载的监听器可以瘫痪 agent 自身的工具分发([waterfall 语义](../../../cordis-primer.md#cordis-waterfall-semantics));挂载代码在当前轮次的工具调用内运行,因此 await 任何只在该轮次结束后才 resolve 的东西会导致死锁;`vmTimeoutMs` 仅约束同步执行;挂载不会在会话恢复后存活。 diff --git a/docs/rfc/implemented/feature/2026-07-10-session-query-service.i18n.yaml b/docs/rfc/implemented/feature/2026-07-10-session-query-service.i18n.yaml index eef8b3d86c..c824942f12 100644 --- a/docs/rfc/implemented/feature/2026-07-10-session-query-service.i18n.yaml +++ b/docs/rfc/implemented/feature/2026-07-10-session-query-service.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-10-session-query-service.md: 8b742ac19fea21d8404f5f44aa64f8c3cb3efccc -2026-07-10-session-query-service.zh.md: 43175b05d82ad758a16e516f3fd8b7b650f9d762 +2026-07-10-session-query-service.zh.md: 73f47d0cc0306b3dcb6552c686f8f1a71ffbdc87 diff --git a/docs/rfc/implemented/feature/2026-07-10-session-query-service.zh.md b/docs/rfc/implemented/feature/2026-07-10-session-query-service.zh.md index 43175b05d8..73f47d0cc0 100644 --- a/docs/rfc/implemented/feature/2026-07-10-session-query-service.zh.md +++ b/docs/rfc/implemented/feature/2026-07-10-session-query-service.zh.md @@ -1,43 +1,43 @@ # RFC:精确会话查询服务 -Status: implemented - [English](2026-07-10-session-query-service.md) | 中文 +Status: implemented + ## 问题 -会话历史存在于两处:当前的 `SessionStore` 对象和可选的持久化后端。需要精确检查的消费方如果不借助统一服务,就得各自重复实现活跃/持久化优先级、持久化生命周期处理、原始事件 surface 分类和防御性克隆。检查点之间持久状态可能落后于活跃日志,因此单靠持久化并不是可信的当前数据源。 +会话历史存在于两处:当前的 `SessionStore` 对象与可选的持久化后端。需要精确检查的消费方若无统一服务,就不得不各自重复实现活跃/持久化优先级判定、持久化生命周期处理、原始事件的 surface 分类以及防御性克隆。在检查点之间,持久化状态可能落后于活跃日志,因此仅靠持久化并非当前状态的可靠来源。 -全文搜索与此相关但规模大得多。在真正的后端出现之前就设计提供方注册、抽取、同步、失效、排序和游标契约,会产生两个投机性的状态机:一个在接口服务中,另一个在最终的数据库包中。 +全文搜索与此相关,但规模大得多。在真实后端尚不存在时就设计提供方注册、提取、同步、失效、排序和游标契约,会产生两个投机性的状态机:一个在接口服务中,另一个在最终的数据库包(package)中。 ## 决策 -`@deepseek-ai/dsh-session-query` 拥有 `ctx.sessionQuery`:一个面向单一逻辑语料库的小型可信精确读取服务。它暴露 `listSessions()`、`listEvents(sessionId)` 和有界的 `readEvent(request)`。它不暴露过滤器、血缘/溯源遍历、文本抽取器、搜索请求、提供方注册或派生索引同步。 +`@deepseek-ai/dsh-session-query` 拥有 `ctx.sessionQuery`,这是一个小型的、受信任的精确读取服务,面向单一逻辑语料库。它暴露 `listSessions()`、`listEvents(sessionId)` 和有界的 `readEvent(request)`。它不暴露过滤器、血缘或溯源遍历、文本提取器、搜索请求、提供方注册或派生索引同步。 -该服务动态观察可选的 `ctx.sessionPersistence` 绑定,但不保留持久化缓存或失效监听器。每次跨语料库列举都向活跃后端请求权威元数据,然后叠加一份新鲜的活跃 store 列表。id 匹配的条目合并为一条 `SessionRecord`:活跃 header 优先,`live`/`persisted` 独立报告来源可用性。不可变 header 不一致时报 `SESSION_QUERY_SOURCE_CONFLICT`。 +该服务动态观察可选的 `ctx.sessionPersistence` 绑定,但不保留持久化缓存或失效监听器。每次跨语料库列表操作向活跃后端请求权威元数据,然后叠加一份新鲜的活跃 store 列表。id 匹配的条目合并为一条 `SessionRecord`:活跃 header 优先,`live`/`persisted` 各自独立报告来源可用性。不可变 header 不一致时产生 `SESSION_QUERY_SOURCE_CONFLICT`。 -精确目标读取首先检查活跃 store,快照活跃 header 和事件日志。此路径从不查询持久化,因此持久化后端故障不会使已知的活跃历史变得不可读。当活跃 store 中无目标时,服务列举当前持久化元数据、证明该 id 存在、加载它,并在列举/加载的 header 不一致时拒绝。所有返回的 header 和事件都经过一次 structured-clone 边界。 +精确目标读取首先检查活跃 store,快照活跃 header 与事件日志。此路径从不查询持久化,因此持久化后端故障不会导致已知的活跃历史不可读。若活跃 store 中无目标,服务列出当前持久化元数据、证明该 id 存在、加载它,并在列表/加载 header 不一致时拒绝。所有返回的 header 与事件都经过一次 structured-clone 边界。 ## Surface 语义 -`dsh-session` 导出 `foldSurface(events)`,`SurfaceManager` 对其增量缓存使用相同的转换函数。fold 返回分离的当前节点以及每次替换实际移除的 seq。`listEvents()` 利用该结果将每个原始事件分类为 `current`、`shadowed` 或 `log-only`,使检查结果不会在位置替换语义上与 model-history 推导产生分歧。 +`dsh-session` 导出 `foldSurface(events)`,`SurfaceManager` 使用相同的转换函数维护其增量缓存。fold 返回分离的当前节点以及每次替换实际移除的 seq。`listEvents()` 利用该结果将每个原始事件分类为 `current`、`shadowed` 或 `log-only`,使检查结果不会在位置替换语义上与 model-history 推导产生分歧。 -`readEvent()` 返回完整的目标事件以及按连续 seq 排列的原始邻居。`before` 和 `after` 默认为零,各自受 `readWindowMax`(默认 50)约束。结果携带克隆的 `SessionHeader` 而非来源可用性记录,因为判断活跃目标的 persisted 标志会违反「活跃精确读取不依赖持久化健康状态」这一保证。 +`readEvent()` 返回完整的目标加上按连续 seq 排列的原始相邻事件。`before` 和 `after` 默认为零,各自受 `readWindowMax`(默认 50)约束。结果携带克隆的 `SessionHeader` 而非来源可用性记录,因为判断活跃目标的 persisted 标志会违反「活跃精确读取不依赖持久化健康状态」这一保证。 ## 安全边界 -该服务是上下文范围内的可信基础设施,而非授权层。未来面向模型的历史工具或人类 UI 将施加显式的调用方/会话作用域。本阶段不添加面向模型的工具,也不改变 transcript(文本记录)或快照 surface。 +该服务是上下文级别的受信任基础设施,而非授权层。未来面向模型的历史工具或人类 UI 将施加显式的调用方/会话范围。本阶段不添加面向模型的工具,也不改变 transcript(文本记录)或快照的 surface。 ## 曾考虑的替代方案 -- **让每个消费方自行实现逻辑语料库解析**:否决。来源优先级、冲突处理、可选服务生命周期、克隆和 surface 分类是共享的正确性规则。 -- **只查询持久化**:否决。检查点之间持久化可能落后于当前活跃日志。 -- **缓存持久化元数据并监听写入/删除**:否决。精确读取可以直接询问权威来源,而缓存失效在规模尚未要求之前就引入了生命周期和并发状态。 +- **将逻辑语料库解析直接放在每个消费方中**:否决。来源优先级、冲突处理、可选服务生命周期、克隆与 surface 分类是共享的正确性规则。 +- **仅查询持久化**:否决。检查点可能落后于当前活跃日志。 +- **缓存持久化元数据并监听写入/删除**:否决。精确读取可以直接询问权威来源,而缓存失效在规模尚未要求时就引入了生命周期与并发状态。 - **现在就定义提供方无关的搜索协议**:否决。目前没有提供方消费它。第一个 SQLite FTS 包应自行拥有一个协调/事务状态机;只有当第二个实现证明了边界时,才提取更小的共享 seam。 -- **在第一阶段就包含血缘、溯源和通用过滤器**:否决。当前没有消费方需要它们,且规范日志足以在有证据时再行添加。 +- **在第一阶段就包含血缘、溯源和通用过滤器**:否决。当前没有消费方需要它们,且规范日志足以在日后有证据时再行添加。 ## 后果 -第一阶段只有一个来源解析状态变量:当前挂载的持久化服务。没有提供方队列、指纹、抽取器注册表、观察代次或派生索引更新。精确读取在纯活跃部署中仍然可用,在持久化存在时具有确定性。 +第一阶段只有一个来源解析状态变量:当前挂载的持久化服务。没有提供方队列、指纹、提取器注册表、观察代次或派生索引更新。精确读取在纯活跃部署中仍然可用,在持久化存在时具有确定性。 -跨语料库列举和持久化精确读取每次调用都执行后端 I/O。这是有意为之:正确性来自当前权威状态,面向规模的搜索属于第二阶段的数据库。全文搜索在该包定义并实现其完整契约之前不可用。 +跨语料库列表与持久化精确读取在每次调用时执行后端 I/O。这是有意为之:正确性来自当前权威状态,面向规模的搜索属于第二阶段的数据库。在该包定义并实现其完整契约之前,全文搜索不可用。 diff --git a/docs/rfc/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.i18n.yaml b/docs/rfc/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.i18n.yaml index ad328eaaaa..6675490bb9 100644 --- a/docs/rfc/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.i18n.yaml +++ b/docs/rfc/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-12-subagent-persona-tool-filter-and-depth.md: 368f3a3592c5e241bb9357d4d4ce32e175c3de45 -2026-07-12-subagent-persona-tool-filter-and-depth.zh.md: 6c1ce8ac08fe3d37c400d489808e592570ebd6c7 +2026-07-12-subagent-persona-tool-filter-and-depth.zh.md: cc78a472df014ce1eb9114277e0520c7e9c051bb diff --git a/docs/rfc/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.zh.md b/docs/rfc/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.zh.md index 6c1ce8ac08..cc78a472df 100644 --- a/docs/rfc/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.zh.md +++ b/docs/rfc/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.zh.md @@ -1,94 +1,94 @@ -# RFC:配置 subagent 的 persona、工具可见性与深度 - -Status: implemented +# RFC:配置 subagent 的人设、工具可见性与深度 [English](2026-07-12-subagent-persona-tool-filter-and-depth.md) | 中文 +Status: implemented + ## 问题 -一个可复用的 subagent 提供方解决的是「如何运行子 agent」的问题,但不同的委派工具需要不同的子 agent 行为。某个部署可能需要一个评审者 persona、一组仅限研究的工具集,或一个硬性递归上限,而不必为每种组合都创建新的提供方。 +一个可复用的 subagent 提供方解决的是「如何运行子 agent(智能体)」的问题,但不同的委派工具需要不同的子 agent 行为。某个部署可能需要评审者人设、仅限研究的工具集,或硬性递归上限,而不必为每种组合创建新的提供方。 -这些控制影响子 agent 的第一次模型请求,因此不能在子 agent 可见之后才安装。它们还需要提供方诚实地声明支持:ACP 后端不能静默接受一个仅适用于进程内的工具过滤器,而过滤器也不应在所有插件运行于同一可信进程时被描述为安全边界。 +这些控制影响子 agent 的第一次模型请求,因此不能在子 agent 可见之后再安装。它们还需要提供方的诚实支持:ACP(Agent Client Protocol)后端不能默默接受一个仅限进程内的工具过滤器,而过滤器在所有插件运行于同一可信进程的情况下也不应被描述为安全边界。 ## 决策 -subagent 启动有三个独立的组合控制:`persona`、`toolFilter` 和 `maxDepth`。提供方声明对每个控制的支持,服务在启动运行前拒绝不支持的请求,而进程内提供方在子 agent 尚未发布时安装所请求的组合。 +subagent 启动有三个独立的组合控制:`persona`、`toolFilter` 和 `maxDepth`。提供方声明对每个控制的支持情况,服务在启动运行之前拒绝不受支持的请求,进程内提供方在子 agent 尚未发布时安装所请求的组合。 这些控制回答不同的问题: | 控制 | 问题 | 结果 | |---|---|---| -| `persona` | 哪些角色指令替换该子 agent 的部署 persona? | 一个子 agent 局部的 prompt 段落遮蔽 `deployment:persona` | -| `toolFilter` | 哪些部署全局工具进入该子 agent 的可见工具视图? | 一个有作用域的限制在添加子 agent 局部工具之前过滤全局工具 | -| `maxDepth` | 这棵委派树最深可以长到多少层? | 当子 agent 深度超过绝对上限时,启动请求被拒绝 | +| `persona` | 什么角色指令替换该子 agent 的部署人设? | 一个子 agent 局部的 prompt 段落遮蔽 `deployment:persona` | +| `toolFilter` | 部署全局工具中哪些进入该子 agent 的可见工具视图? | 一个有作用域的限制在添加子 agent 局部工具之前过滤全局工具 | +| `maxDepth` | 这棵委派树最深可以长到多少层? | 子 agent 深度超过绝对上限时,启动请求被拒绝 | -`dsh-tool-subagent` 将这些控制作为插件配置暴露,并将它们复制到每个创建的请求中。直接调用 `SubagentService` 的调用方可以按请求选择。提供方能力描述符仍然是后端能否兑现各字段的真源。 +`dsh-tool-subagent` 将这些控制作为插件配置暴露,并复制到它创建的每个请求中。直接调用 `SubagentService` 的调用方可以按请求选择这些控制。提供方的能力描述符仍然是后端能否兑现各字段的真源。 -### Persona 是有作用域的遮蔽 +### 人设是有作用域的遮蔽 -persona 控制改变一个子 agent 而不改变部署级别的 prompt 组装。在未发布的设置阶段,进程内提供方在子 agent 作用域中注册一个名为 `deployment:persona` 的段落;普通的最具体者胜出解析规则仅在该子 agent 的组装中替换全局段落。 +人设控制改变一个子 agent 的行为,而不改变部署级的 prompt 组装。在未发布的设置阶段,进程内提供方在子 agent 作用域中注册一个名为 `deployment:persona` 的段落;普通的最具体者优先解析规则仅在该子 agent 的组装中替换全局段落。 -其值具有与部署 persona 相同的严格模板语义。省略时通过全局层继承部署段落;显式空字符串则以空段落遮蔽全局 persona。父 agent 和兄弟 agent 的 persona 永远不会进入子 agent 的扁平作用域。 +其值与部署人设具有相同的严格模板语义。省略时通过全局层继承部署段落;显式空字符串则以空段落遮蔽全局人设。父级和兄弟级的人设永远不会进入子 agent 的扁平作用域。 -这使用的是正常的系统提示词注册机制,而非第二条 persona 通道。因此第一次 prompt 看到的命名贡献与后续 prompt 和 prompt 检查工具看到的相同。 +这使用的是常规的系统提示词注册机制,而非第二条人设通道。因此第一次 prompt 看到的命名贡献与后续 prompt 和 prompt 检查工具看到的一致。 -### 工具过滤是一条实时的全局视图规则 +### 工具过滤是一条作用于全局视图的活规则 -工具过滤器同时控制能力可见性与可执行查找。进程内提供方在发布前于子 agent 作用域中安装 `ToolRegistry.restrict()`,注册表的单一解析器将相同结果应用于协议格式(wire format)的工具 schema、查找、执行和 Code Mode SDK 生成。独立注册的系统提示词段落不在 `ToolRegistry` 内,因此过滤一个工具不会移除该插件的独立指导文本。 +工具过滤同时控制能力可见性和可执行查找。进程内提供方在发布前于子 agent 作用域中安装 `ToolRegistry.restrict()`,注册表的单一解析器对协议格式(wire format)的工具 schema、查找、执行和 Code Mode SDK 生成施加相同的结果。独立注册的系统提示词段落不在 `ToolRegistry` 内,因此过滤一个工具不会移除该插件的独立指导文本。 解析遵循以下规则: -1. 每个限制对实时的部署全局工具注册表先应用 `allow` 再应用 `deny`。 -2. 多个限制取交集,因此每个已安装的限制都必须放行一个全局工具。 +1. 每条限制对活跃的部署全局工具注册表先应用 `allow` 再应用 `deny`。 +2. 多条限制取交集,因此每条已安装的限制都必须放行一个全局工具。 3. 子 agent 作用域的工具在全局过滤之后添加,可以遮蔽一个已放行的全局工具。 -4. 保留的 `run_code` 呈现和其他作用域局部的协议贡献不在全局过滤器范围内。 +4. 保留的 `run_code` 呈现和其他作用域局部的协议贡献不受全局过滤器影响。 -当过滤器既不提供 `allow` 也不提供 `deny`,或命名了当前全局可限制集合之外的内容(包括仅作用域局部或保留的名称)时,配置会大声失败。`allow: []` 是合法的,它有意隐藏所有全局工具。这些检查能捕获拼写错误,并防止配置在无法影响所命名条目时看起来有效。 +当过滤器既未提供 `allow` 也未提供 `deny`,或命名了当前全局可限制集合之外的内容(包括仅作用域局部或保留名称)时,配置会显式失败。`allow: []` 合法,且有意隐藏所有全局工具。这些检查能捕获拼写错误,并防止配置在无法影响所命名条目时看起来仍然有效。 -全局注册表保持实时。仅 deny 的过滤器会放行后续注册的全局名称(除非显式 deny 该名称);allow 列表会排除后续注册的全局名称(除非显式 allow 该名称)。移除一个全局工具会将其从所有解析视图中移除。这些语义在保持热注册的同时,使 allow 与 deny 的区别显式化。 +全局注册表保持活跃。仅 deny 的过滤器会放行后来注册的全局名称(除非显式 deny 该名称);allow 列表会排除后来注册的全局名称(除非显式 allow 该名称)。移除一个全局工具会将其从所有已解析视图中移除。这些语义在保持热注册的同时,使 allow 与 deny 的区别显式化。 ### 深度是绝对的树上限 -深度限制独立于工具可见性来约束递归委派。顶层 agent 的深度为零;进程内子 agent 的深度为其父 agent 经验证的深度加一。`maxDepth` 是一个绝对的非负安全整数,当推导出的子 agent 深度大于上限时,启动在子 agent 所有权开始之前即被拒绝。 +深度限制独立于工具可见性来约束递归委派。顶层 agent 深度为零;进程内子 agent 的深度为其父级已验证深度加一。`maxDepth` 是一个绝对的非负安全整数,当推导出的子 agent 深度大于上限时,启动在子 agent 所有权开始之前即被拒绝。 -每个公开入口都验证值域,而不依赖单一的面向模型的配置路径。负值、小数、负零、非有限值、不安全整数、格式错误的已存储父深度以及推导溢出都会被拒绝。省略上限则该机制不约束深度。 +每个公开入口都自行验证值域,而非依赖单一的面向模型配置路径。负值、小数、负零、非有限值、不安全整数、格式错误的存储父级深度以及推导溢出均被拒绝。省略上限时,此机制不约束深度。 -部署可以组合深度与过滤。例如,可以在深度一时保留委派工具可见但设置 `maxDepth: 1`,或在子 agent 中完全 deny 委派工具。两种选择都不改变提供方的对话历史行为。 +部署可以组合深度与过滤。例如,可以在深度一时保持委派工具可见但设置 `maxDepth: 1`,或在子 agent 中完全 deny 委派工具。两种选择都不改变提供方的对话历史行为。 ### 能力门控保持提供方诚实 -能力将请求的特性与提供方实现分离。`SubagentCapabilities` 声明 `persona`、`toolFilter` 和 `depthLimit`;`SubagentService.start()` 在调用提供方之前,对照这些标志检查请求中的每个字段。 +能力将请求的特性与提供方实现分离。`SubagentCapabilities` 声明 `persona`、`toolFilter` 和 `depthLimit`;`SubagentService.start()` 在调用提供方之前,对照这些标志检查请求中每个存在的字段。 -这使得 spawn 和 fork 提供方可以共享进程内实现,而外部提供方只声明自己能强制执行的部分。请求永远不会静默降级:选择一个不支持的控制会产生 `UNSUPPORTED_CAPABILITY`,不会有运行或生命周期事件存在。 +这使 spawn 和 fork 提供方可以共享进程内实现,而外部提供方只声明自己能强制执行的部分。请求永远不会静默降级:选择不受支持的控制会产生 `UNSUPPORTED_CAPABILITY`,不会有运行或生命周期事件存在。 -### 未发布的设置使第一次请求正确 +### 未发布设置使第一次请求正确 -所有子 agent 局部的组合在子 agent 变得可观察之前完成。进程内提供方向 agent 创建提供一个设置回调;该回调在子 agent 作用域中安装 persona、工具限制和结构化输出贡献。只有设置成功后,创建才会发布会话和 agent 并允许驱动器启动。 +所有子 agent 局部的组合在子 agent 变得可观察之前完成。进程内提供方向 agent 创建提供一个设置回调;该回调在子 agent 作用域中安装人设、工具限制和结构化输出贡献。只有设置成功后,创建才发布会话和 agent 并允许驱动器启动。 -设置失败会回滚私有的子 agent。没有观察者能获取到一个「第一次 prompt 使用了部署 persona 或未过滤工具集、后续 prompt 才使用请求配置」的子 agent。 +设置失败会回滚私有子 agent。没有观察者能获取到一个「第一次 prompt 使用了部署人设或未过滤工具集、后续 prompt 才使用所请求配置」的子 agent。 ## 可见性不是授权 -这些控制组合的是可信的同进程行为;它们不授权行为。`toolFilter` 改变工具注册表解析出的子 agent 视图,但它不创建父到子的授权格,不要求子 agent 是父 agent 的子集,不沙箱化插件,也不阻止持有另一个 Cordis 上下文的代码直接调用服务。 +这些控制组合的是同一可信进程内的行为,而非授权行为。`toolFilter` 改变工具注册表解析出的子 agent 视图,但它不创建父到子的授权格,不要求子 agent 是其父级的子集,不沙箱化插件,也不阻止持有另一个 Cordis 上下文的代码直接调用服务。 -特别地,子 agent 局部工具在全局过滤之后添加,可能不在父 agent 的视图中。仅 deny 的子 agent 也能看到 deny 列表未命名的后续全局工具。这些是有意的实时组合语义,而非不可提权保证。 +具体而言,子 agent 局部工具在全局过滤之后添加,可能不在父级视图中。仅 deny 的子 agent 也能看到 deny 列表未命名的后来全局工具。这些是有意的活组合语义,而非不可升级保证。 -安全设计需要独立的授权表示、传播规则和执行时强制点。创建时的授权快照、父集合子集授权、显式的未来授权 API,以及通用的能力/输出/终止标签都不在本特性范围内。 +安全设计需要独立的授权表示、传播规则和执行时强制点。创建时的授权快照、父级子集授权、显式的未来授权 API,以及通用的能力/输出/终止标签均不在本特性范围内。 ## 曾考虑的替代方案 -**为每个 persona 或工具集创建一个提供方。** 这会使共享相同传输和生命周期实现的提供方成倍增加,使动态部署配置变得笨拙,且仍然需要递归机制。提供方的职责仍然是执行传输;请求承载每个子 agent 的组合。 +**为每种人设或工具集创建一个提供方。** 这会使共享相同传输和生命周期实现的提供方成倍增加,使动态部署配置变得笨拙,且仍需要递归机制。提供方的职责是执行传输;请求承载每个子 agent 的组合。 -**复制父 agent 的完整工具视图。** 注册作用域设计上是扁平的,生命周期所有权不意味着可见性继承。复制已解析的视图还会冻结动态全局注册,并在未完整定义任一契约的情况下混淆组合与授权。 +**复制父级的完整工具视图。** 注册作用域设计上是扁平的,生命周期所有权不意味着可见性继承。复制已解析视图还会冻结动态全局注册,并在未完整定义任一契约的情况下混淆组合与授权。 -**在子 agent 创建时快照允许的全局工具。** 冻结的 allow 集合使未来注册一律不可用,但它改变了热注册语义并开启了授权设计。已实现的过滤器保持为实时注册表谓词,并直接记录 allow 与 deny 的行为。 +**在子 agent 创建时快照允许的全局工具。** 冻结的 allow 集合使未来注册统一不可用,但它改变了热注册语义并开启了授权设计。已实现的过滤器保持为活跃的注册表谓词,并直接记录 allow 与 deny 的行为。 -**仅隐藏工具 schema。** 仅呈现层的过滤让模型可以通过 Code Mode 或伪造调用执行一个 prompt 声称不存在的工具。改为由一个解析器同时管控呈现与执行。 +**仅隐藏工具 schema。** 仅呈现层的过滤让模型可以通过 Code Mode 或伪造调用执行一个 prompt 声称不存在的工具。改为由一个解析器同时管控呈现和执行。 -**仅用工具过滤来阻止递归。** 移除委派工具有用但依赖特定提供方,且无法保护直接的服务调用方或替代委派工具。绝对深度是一个独立的结构性约束。 +**仅用工具过滤来阻止递归。** 移除委派工具有用但依赖特定提供方,且不保护直接服务调用方或替代委派工具。绝对深度是独立的结构性约束。 ## 后果 -贡献者可以配置子 agent 的角色、可见全局工具和递归深度,而无需定义新的提供方。能力检查在所有权开始前失败,未发布的设置使第一次请求一致,单一工具解析器防止呈现/执行漂移。 +贡献者可以配置子 agent 的角色、可见全局工具和递归深度,而无需定义新的提供方。能力检查在所有权开始之前失败,未发布设置使第一次请求一致,单一工具解析器防止呈现/执行漂移。 -代价是部署方必须理解实时 allow/deny 行为以及可见性与授权的区别。提供方作者必须准确声明每个支持的控制,进程内提供方必须在发布前安装所有请求的贡献。这些控制有意不解决安全隔离或父到子的不可提权问题。 +代价是部署方必须理解活跃的 allow/deny 行为以及可见性与授权的区别。提供方作者必须准确声明每个受支持的控制,进程内提供方必须在发布前安装所有请求的贡献。这些控制有意不解决安全隔离或父到子的不可升级问题。 diff --git a/docs/rfc/implemented/process/2026-06-11-doc-sync-enforcement.i18n.yaml b/docs/rfc/implemented/process/2026-06-11-doc-sync-enforcement.i18n.yaml index 31c5a1b2e0..120abae517 100644 --- a/docs/rfc/implemented/process/2026-06-11-doc-sync-enforcement.i18n.yaml +++ b/docs/rfc/implemented/process/2026-06-11-doc-sync-enforcement.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-11-doc-sync-enforcement.md: 44ea84daddcae73ce07b0a8240f83ee9945e449d -2026-06-11-doc-sync-enforcement.zh.md: e7abe2d87fd97211f653b63ddb8818077532af48 +2026-06-11-doc-sync-enforcement.zh.md: c739e9661bb926c772f1d5399529a813ac59991c diff --git a/docs/rfc/implemented/process/2026-06-11-doc-sync-enforcement.zh.md b/docs/rfc/implemented/process/2026-06-11-doc-sync-enforcement.zh.md index e7abe2d87f..c739e9661b 100644 --- a/docs/rfc/implemented/process/2026-06-11-doc-sync-enforcement.zh.md +++ b/docs/rfc/implemented/process/2026-06-11-doc-sync-enforcement.zh.md @@ -1,32 +1,32 @@ # RFC:Doc-sync 强制 -Status: implemented - [English](2026-06-11-doc-sync-enforcement.md) | 中文 +Status: implemented + ## 问题 -AGENTS.md 承诺文档与代码严格同步,但这一承诺此前只靠肉眼验证。评审曾两次发现漂移:一次是实操手册(cookbook)示例与类型策略矛盾,一次是 README 引用了错误的 `registerAdapter` 调用。失去同步的文档比没有文档更糟;而本代码库主要由 agent 构建,agent 对门禁的遵从远比对行文的遵从可靠(机械质量门禁)。有两类文档漂移可以被机械检查:不再能编译的代码块,以及重复了 `interface Events` 声明的事件分类体系表。 +AGENTS.md 承诺文档与代码严格同步,但这一承诺此前仅靠人眼核查。评审曾两次发现漂移:一次是实操手册(cookbook)示例与类型策略矛盾,一次是 README 引用了错误的 `registerAdapter` 调用。失去同步的文档比没有文档更糟;而本代码库主要由 agent(智能体)构建,agent 遵守门禁远比遵守行文约定可靠(机械质量门禁)。有两类文档漂移可以被机械检查:不再能编译的代码块,以及与 `interface Events` 声明重复的事件分类体系表。 ## 决策 两道门禁,沿用既有的 `scripts/` 风格(tsx ESM,每个脚本一项职责): -1. **`doc-typecheck`** 从 `README.md`、`docs/**` 和 `packages/*/README.md` 中提取所有 ` ```ts ` 围栏代码块,写入一个继承根 `tsconfig.json` 的临时项目,然后用 `tsc -b` 编译。临时项目复用源码的 `paths` 映射和根 project references,因此文档示例能看到源码,而 vendor 代码仍在其自身的 tsconfig 设置下被检查。刻意作为草图的代码块可以用显式的 ` ```ts ignore-check ` 信息字符串退出检查;脚本会报告退出比例,超过一半则失败,防止逃生口悄悄变成常态。 -2. **`verify-event-taxonomy`** 从 `packages/*/src` 的 `interface Events` 块中提取事件名,再从 `docs/architecture.md` 的分类体系表中提取事件名,断言两个集合完全一致。只校验、不生成:表格保留手写的 Mode/Purpose 列,只检查名称集合。(落地此门禁时发现了表格缺失的三个事件:`tools/change`、`llm/adapter-change`、`system-prompt/change`。)**已被取代**:[生成式 Cordis 目录](2026-06-20-generated-cordis-catalog.md)取代了此门禁及其 `architecture.md` 表格,改为完全生成的 `docs/cordis-catalog/events.md` + `docs/cordis-catalog/services.md` 及其 `verify-cordis-catalog` 新鲜度门禁。本文的其他门禁(`doc-typecheck` 以及下文修订的 `verify-md-wrap`)不受影响。 +1. **`doc-typecheck`** 从 `README.md`、`docs/**` 和 `packages/*/README.md` 中提取所有 ` ```ts ` 围栏代码块,写入一个继承根 `tsconfig.json` 的临时项目,然后用 `tsc -b` 编译。临时项目复用源码的 `paths` 映射和根 project references,因此文档示例能看到源码,而 vendor 代码仍在其自身的 tsconfig 设置下被检查。刻意作为草图的代码块可通过显式的 ` ```ts ignore-check ` 信息字符串来 opt-out;脚本会报告 opt-out 比例,超过一半即失败,防止该豁免机制悄然成为常态。 +2. **`verify-event-taxonomy`** 从 `packages/*/src` 中的 `interface Events` 块和 `docs/architecture.md` 中的分类体系表分别提取事件名称,断言两个集合完全一致。只校验,不生成:表格保留手写的 Mode/Purpose 列,仅检查名称集合。(落地此门禁时发现了表格遗漏的三个事件:`tools/change`、`llm/adapter-change`、`system-prompt/change`。)**已被取代**:由[生成式 Cordis 目录](2026-06-20-generated-cordis-catalog.md)取代。此门禁及其 `architecture.md` 表格已退役,取而代之的是完全生成的 `docs/cordis-catalog/events.md` + `docs/cordis-catalog/services.md` 及其 `verify-cordis-catalog` 新鲜度门禁。本 RFC 中的其他门禁(`doc-typecheck` 以及下文修订中的 `verify-md-wrap`)不受影响。 -两者通过一个共享的 `doc-sync` package.json 脚本运行,lefthook pre-push 钩子和 CI 都调用它([机械质量门禁](2026-06-11-quality-gates.md):钩子和 CI 调用相同的脚本,因此门禁在推送前就在本地触发,而不仅仅在推送后)。它们在 `pnpm run typecheck` 之后运行,后者校验 doc-typecheck 所引用的 package/vendor 构建图。 +两者通过一个共享的 doc-sync(文档同步门禁)`package.json` 脚本运行,lefthook pre-push 钩子和 CI 都调用它([机械质量门禁](2026-06-11-quality-gates.md):钩子与 CI 调用相同脚本,因此门禁在推送前就在本地触发,而非仅在推送后)。它们在 `pnpm run typecheck` 之后运行,后者校验 doc-typecheck 所引用的 package/vendor 构建图。 -**修订(2026-06-17):** 第三道门禁 **`verify-md-wrap`** 后来也被纳入 `doc-sync`。它用 `mdast-util-from-markdown` + GFM 解析范围内的每个 Markdown 文件(`README.md`、`docs/**`、`packages/*/README.md`,加上 `AGENTS.md` / `packages/AGENTS.md`),对任何跨越多行的 `paragraph` 节点报错,强制执行 docs/AGENTS.md 中「一个段落一个物理行」的写作规则。同样遵循只校验不生成的原则:它报告硬换行,从不重写,因此不会引入格式化噪音。`doc-sync` 现在包含三道门禁。 +**修订(2026-06-17):** 第三道门禁 **`verify-md-wrap`** 随后被纳入 `doc-sync`。它使用 `mdast-util-from-markdown` + GFM 解析范围内的每个 Markdown 文件(`README.md`、`docs/**`、`packages/*/README.md`,加上 `AGENTS.md` / `packages/AGENTS.md`),如果任何 `paragraph` 节点跨越多个源码行则失败,从而强制执行 docs/AGENTS.md 中「一个段落一个物理行」的写作规则。同样遵循只校验不生成的原则:它报告硬换行但从不重写,因此不会引入格式化噪音。`doc-sync` 现在包含三道门禁。 ## 曾考虑的替代方案 -- **API-extractor 黄金报告**([已推迟的提案](../../proposed/process/2026-06-11-api-extractor-reports.md)):有意推迟。对于评审者已经能看到源码 diff 的内部 monorepo 而言价值不高,且依赖笨重、配置繁琐。 -- **从源码生成分类体系表**而非校验名称:否决,机制比问题本身更重;表格保留手写的 Mode/Purpose 列,直到[生成式 Cordis 目录](2026-06-20-generated-cordis-catalog.md)完全取代了这项检查。 +- **API-extractor 金标报告**([已推迟的提案](../../proposed/process/2026-06-11-api-extractor-reports.md)):有意推迟。对于评审者已能直接看到源码 diff 的内部 monorepo 而言价值有限,且依赖重、配置繁琐。 +- **从源码生成分类体系表**而非仅校验名称:否决,机制比问题本身更重;表格保留了手写的 Mode/Purpose 列,直到[生成式 Cordis 目录](2026-06-20-generated-cordis-catalog.md)完全取代了这项检查。 ## 后果 -- 可机械检查的文档漂移现在会让 pre-push 钩子和 CI 失败,而非等待评审者发现。这是「机械门禁优于行文约定」原则的一个实例。 -- 让文档代码片段可编译需要少量 stub import 或 `declare`;`ignore-check` 比例必须保持低位,否则门禁形同虚设(比例守卫强制执行这一点)。 -- 分类体系检查仅限名称:Mode 或 Purpose 列的错误仍需人工评审。 -- 如果这些包(package)将来对外发布,API 报告仍可重新考虑。 +- 可检查类别的文档漂移现在会让 pre-push 钩子和 CI 失败,而非等待评审者发现。这是「机械门禁优于行文约定」原则的一个实例。 +- 让文档代码片段可编译需要少量 stub import/`declare`;`ignore-check` 比例必须保持低位,否则门禁形同虚设(比例守卫强制执行此约束)。 +- 分类体系检查仅限名称——Mode 或 Purpose 列的错误仍需人工评审。 +- 如果 package 未来对外发布,API 报告方案仍可重新考虑。 diff --git a/docs/rfc/implemented/process/2026-06-11-quality-gates.i18n.yaml b/docs/rfc/implemented/process/2026-06-11-quality-gates.i18n.yaml index addb813047..ba5bb828a2 100644 --- a/docs/rfc/implemented/process/2026-06-11-quality-gates.i18n.yaml +++ b/docs/rfc/implemented/process/2026-06-11-quality-gates.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-11-quality-gates.md: 9862dc019dd6ff3b4639983821256395d0ee7b77 -2026-06-11-quality-gates.zh.md: 7a9dd7cead0e7a4964a44b650664ceb7ff570c7b +2026-06-11-quality-gates.zh.md: a9c17bb700db091d21f7930942a8f3bbf55958a0 diff --git a/docs/rfc/implemented/process/2026-06-11-quality-gates.zh.md b/docs/rfc/implemented/process/2026-06-11-quality-gates.zh.md index 7a9dd7cead..a9c17bb700 100644 --- a/docs/rfc/implemented/process/2026-06-11-quality-gates.zh.md +++ b/docs/rfc/implemented/process/2026-06-11-quality-gates.zh.md @@ -1,28 +1,28 @@ # RFC:以机械质量门禁取代行文约定 -Status: implemented - [English](2026-06-11-quality-gates.md) | 中文 +Status: implemented + ## 问题 -本代码库主要由 coding agent 开发。相比行文约定,agent 遵守强制门禁的可靠性远高得多;而当劳动由 agent 完成时,「工作量大」不构成成本论据。早期证据:未通过类型检查的测试被提交了(vitest 不做类型检查),只在评审时才被发现。 +本代码库主要由 coding agent(智能体)开发。相比行文约定,agent 遵守强制门禁的可靠性远高得多;而当劳动由 agent 承担时,「工作量大」不构成成本论据。早期证据:未通过类型检查的测试被提交(vitest 不做类型检查),仅在评审中才被发现。 ## 决策 -AGENTS.md 中的每一项承诺都对应一条退出码非零的命令,同时接入 git 钩子和 CI,两者调用相同的 package.json 脚本: +AGENTS.md 中的每一条承诺都对应一个以非零退出码表示失败的命令,通过 git 钩子和 CI 调用同一套 package.json 脚本来执行: -- 最严格的 TypeScript(`noUncheckedIndexedAccess`、`exactOptionalPropertyTypes` 等);示例、测试和脚本通过根目录 no-emit `tsconfig.json` 在 CI 中进行类型检查,而 package/vendor 代码保持在各自 project-reference 边界之后。 -- ESLint strict-type-checked + @stylistic(作为强制执行的项目风格),包括文件内重复逻辑检查;vendor 代码排除在外。 -- jscpd 检测 package 生产 TypeScript 和仓库脚本中的跨文件克隆;窄范围的源码区间例外用于记录有意为之的并行实现。 -- `packages/*/*/src` 的逐文件 100% 覆盖率(v8);不可达的防御性守卫保留 `/* v8 ignore */ ` 并注明理由,而非删除。 -- knip(死代码/依赖)、publint(包正确性)、workspace 约束(workspace 规则:private、cordis peer+dev、统一版本、ESM),以及对构建出的包声明文件进行 NodeNext 消费方类型检查。 -- lefthook pre-commit(lint 暂存文件、类型检查、vendor manifest 守卫)和 pre-push(测试、hygiene);CI 在 Node 22.19/24/26 上运行完整矩阵,外加一个端到端驱动 echo-agent 的演示冒烟测试。 +- 最严格的 TypeScript 配置(`noUncheckedIndexedAccess`、`exactOptionalPropertyTypes` 等);示例、测试和脚本通过根目录的 no-emit `tsconfig.json` 在 CI 中进行类型检查,而 package/vendor 代码保持在各自 project-reference 边界之后。 +- ESLint strict-type-checked + @stylistic(作为强制执行的统一代码风格),包括文件内重复逻辑检查;vendor 代码排除在外。 +- jscpd 检测 package 生产 TypeScript 与仓库脚本中的跨文件克隆;窄范围的源码区间例外用于记录有意为之的并行实现。 +- `packages/*/*/src` 下按文件 100% 覆盖率(v8);不可达的防御性守卫使用 `/* v8 ignore */` 并注明理由,而非删除。 +- knip(死代码/依赖)、publint(包(package)正确性)、workspace 约束(workspace 规则:private、cordis peer+dev、统一版本、ESM),以及对构建出的包声明文件进行 NodeNext 消费方类型检查。 +- lefthook pre-commit(lint 暂存文件、类型检查、vendor manifest(元数据清单)守卫)和 pre-push(测试、hygiene);CI 在 Node 22.19/24/26 上运行完整矩阵,外加一个驱动 echo-agent 端到端的演示冒烟测试。 ## 后果 -- 约定在 agent 更替后仍然存续;违规在本地快速失败。 +- 约定在 agent 更替中得以存续;违规在本地快速失败。 - 门禁本身也是需要维护的代码;配置变更与其他变更一样需要评审。 -- 100% 覆盖率的压力可能催生无断言的测试——变异测试是计划中的对冲手段(见[变异测试提案](../../proposed/testing/2026-06-11-mutation-testing.md))。 +- 100% 覆盖率的压力可能催生无断言的测试——变异测试是计划中的对策(见[变异测试提案](../../proposed/testing/2026-06-11-mutation-testing.md))。 diff --git a/docs/rfc/implemented/process/2026-06-11-tsdown-over-dumble.i18n.yaml b/docs/rfc/implemented/process/2026-06-11-tsdown-over-dumble.i18n.yaml index d7062a1c20..92dd0f65b6 100644 --- a/docs/rfc/implemented/process/2026-06-11-tsdown-over-dumble.i18n.yaml +++ b/docs/rfc/implemented/process/2026-06-11-tsdown-over-dumble.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-11-tsdown-over-dumble.md: c16ac691a9452d952303cf73b40693447d25015c -2026-06-11-tsdown-over-dumble.zh.md: 637a15a49bf22a0dd006039dd1ae2d0ea8120424 +2026-06-11-tsdown-over-dumble.zh.md: 5e9dc5242225e4420e1faa6ef19c8e8b9b3fdbcd diff --git a/docs/rfc/implemented/process/2026-06-11-tsdown-over-dumble.zh.md b/docs/rfc/implemented/process/2026-06-11-tsdown-over-dumble.zh.md index 637a15a49b..5e9dc52422 100644 --- a/docs/rfc/implemented/process/2026-06-11-tsdown-over-dumble.zh.md +++ b/docs/rfc/implemented/process/2026-06-11-tsdown-over-dumble.zh.md @@ -1,30 +1,30 @@ -# RFC:用 tsdown 替代 dumble 进行 JS 打包 - -Status: implemented +# RFC:使用 tsdown 替代 dumble 进行 JS 打包 [English](2026-06-11-tsdown-over-dumble.md) | 中文 +Status: implemented + ## 问题 -初始构建使用 **dumble**——cordiverse 的零配置 esbuild 包装层,上游 Cordis 本身也用它构建——与 vendor 包的约定最大程度对齐(它读取每个 package.json,从 `exports` 字段推断入口和格式)。但 dumble 作为本仓库的承重工具是一个隐患:v0.2.x,每周约 530 次 npm 下载,实质上只有一位维护者,而且由于它没有 workspace 模式,我们不得不通过一个自定义编排脚本(`scripts/build.ts`)来调用它。 +最初的构建使用 **dumble**,即 cordiverse 的零配置 esbuild 包装层——上游 Cordis 自身也用它构建——与 vendor 包(package)的约定最大程度对齐(它读取每个 package.json 并从 `exports` 字段推断入口/格式)。但 dumble 作为本仓库的承重工具存在隐患:v0.2.x,每周约 530 次 npm 下载,实质上只有一位维护者,而且由于它没有 workspace 模式,我们不得不通过自定义编排脚本(`scripts/build.ts`)来调用它。 -目前构建产物只对 `pnpm run build` + publint 有意义(尚无包发布;开发/测试/演示通过 tsx 直接运行未打包的源码),因此切换成本现在最低,一旦包开始发布就只会更高。 +目前构建产物只在 `pnpm run build` + publint 中有意义(尚未发布任何包;开发/测试/演示通过 tsx 直接运行未打包的源码),因此切换成本现在最低,一旦包开始发布就只会更高。 ## 决策 用 **tsdown**(基于 rolldown,每周约 250 万次下载,VoidZero 支持,活跃发布)替代 dumble: -- 根目录 `tsdown.config.ts`,配置 `workspace: ['vendor/*', 'packages/*/*']`(显式 glob 将打包范围限定在 vendor Cordis 和 TypeScript 包树;`workspace: true` 还会发现示例 manifest 和不需要打包的 workspace 成员)。 -- 共享形态:入口 `lib/types/index.js`,`outDir: 'lib'`,ESM,`platform: node`,`target: es2024`,`fixedExtension: false`(对 `"type": "module"` 的包保持 `.js` 扩展名),`dts: false`(声明文件由 tsc -b 负责),`clean: false`(lib/ 同时存放 TSC 的 `lib/types` 中间产物树)。入口最初是 `src/index.ts`;[TSC 优先构建 RFC](2026-06-17-ts-build-config.md) 后来将 tsdown 改为打包 TSC 输出的 JS,使 TypeScript 转换行为来自同一个编译器。 -- vendor/ 中有两个逐包覆盖配置(属于我们的修改,与重新生成的 tsconfig 一样;记录在 vendor/README.md 中):schemastery(通过 `outExtensions` 输出双格式 `.mjs`/`.cjs`)、logger-console(两次单入口 pass,使共享基类内联到每个入口而非生成 hash 命名的 chunk,与上游发布形态一致)。 -- 删除 `scripts/build.ts`;`pnpm run build` = `tsc -b tsconfig.build.json && tsdown`。 +- 根目录 `tsdown.config.ts`,配置 `workspace: ['vendor/*', 'packages/*/*']`(显式 glob 将打包范围限定在 vendor 的 Cordis 与 TypeScript 包目录树内;`workspace: true` 还会发现示例 manifest 和不需要打包的 workspace 成员)。 +- 共享形态:入口 `lib/types/index.js`,`outDir: 'lib'`,ESM,`platform: node`,`target: es2024`,`fixedExtension: false`(为 `"type": "module"` 的包保持 `.js` 扩展名),`dts: false`(声明文件由 tsc -b 负责),`clean: false`(lib/ 同时存放 TSC 的 `lib/types` 中间产物树)。入口最初是 `src/index.ts`;[TSC 优先构建 RFC](2026-06-17-ts-build-config.md) 后来将 tsdown 改为打包 TSC 输出的 JS,使 TypeScript 转换行为统一来自一个编译器。 +- vendor/ 中有两个按包覆盖的配置(属于我们自己的修改,与重新生成的 tsconfig 类似;记录在 vendor/README.md 中):schemastery(通过 `outExtensions` 输出双格式 `.mjs`/`.cjs`)、logger-console(两次单入口 pass,使共享基类被内联到每个入口而非生成哈希命名的 chunk,与上游发布形态一致)。 +- `scripts/build.ts` 删除;`pnpm run build` = `tsc -b tsconfig.build.json && tsdown`。 ## 曾考虑的替代方案 -- **直接编写 esbuild 脚本**:最成熟的引擎,零包装层风险,但需要手动维护 tsdown workspace 模式自动提供的逐包规格表。 -- **pkgroll**:理念上最接近的直接替代品,但每周仅 78k 下载且基于 Rollup:维护前景严格弱于 tsdown。 -- **保留 dumble**:与上游完美对齐,但 bus factor 不可接受。 +- **直接编写 esbuild 脚本**:最成熟的引擎,零包装层风险,但需要手动维护 tsdown workspace 模式自动提供的按包规格表。 +- **pkgroll**:理念上最接近的直接替代品,但每周仅 78k 下载且基于 Rollup,维护前景严格弱于 tsdown。 +- **保留 dumble**:与上游完美对齐,但巴士因子不可接受。 ## 后果 -运行时打包产物仍遵循 dumble 时代的公开入口形态(`lib/index.js`,加上包特有的变体如 `schemastery` 的 `lib/index.mjs`/`lib/index.cjs` 和 `logger-console` 的 `lib/browser.js`);声明文件现在按 [TSC 优先构建 RFC](2026-06-17-ts-build-config.md) 放在 `lib/types` 下。外部依赖仍来自各包的 dependencies/peerDependencies。我们放弃了 dumble 的 exports 字段推断能力:入口形态非默认的新包需要一个逐包的 `tsdown.config.ts`,而不能仅靠 package.json 字段。未来选项:如果 `tsc -b` 成为瓶颈,tsdown 也可以接管声明文件打包(isolatedDeclarations);那将是一个新的 RFC。 +运行时打包产物仍遵循 dumble 时代的公开入口形态(`lib/index.js`,以及按包特定的变体,如 `schemastery` 的 `lib/index.mjs`/`lib/index.cjs` 和 `logger-console` 的 `lib/browser.js`);声明文件现在位于 `lib/types` 下,见 [TSC 优先构建 RFC](2026-06-17-ts-build-config.md)。外部依赖仍来自各包的 dependencies/peerDependencies。我们放弃了 dumble 的 exports 字段推断功能:新增的非默认形态的包需要编写按包的 `tsdown.config.ts`,而不能仅靠 package.json 字段。未来可选方向:如果 `tsc -b` 成为瓶颈,tsdown 还可以接管声明文件打包(isolatedDeclarations);那将是一个新的 RFC。 diff --git a/docs/rfc/implemented/process/2026-06-11-vendor-cordis-as-source.i18n.yaml b/docs/rfc/implemented/process/2026-06-11-vendor-cordis-as-source.i18n.yaml index baaef0f47c..b71b7d025d 100644 --- a/docs/rfc/implemented/process/2026-06-11-vendor-cordis-as-source.i18n.yaml +++ b/docs/rfc/implemented/process/2026-06-11-vendor-cordis-as-source.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-11-vendor-cordis-as-source.md: 39506300dec73d0c9eb1b7b2246caa23f1b10f7f -2026-06-11-vendor-cordis-as-source.zh.md: 1c942291481f50f602d6733e3c25a892885d47fe +2026-06-11-vendor-cordis-as-source.zh.md: 0e794d97c4d535b74279bab11bda519e7da2e366 diff --git a/docs/rfc/implemented/process/2026-06-11-vendor-cordis-as-source.zh.md b/docs/rfc/implemented/process/2026-06-11-vendor-cordis-as-source.zh.md index 1c94229148..0e794d97c4 100644 --- a/docs/rfc/implemented/process/2026-06-11-vendor-cordis-as-source.zh.md +++ b/docs/rfc/implemented/process/2026-06-11-vendor-cordis-as-source.zh.md @@ -1,27 +1,27 @@ -# RFC:以源码形式收录 Cordis,而非 npm 依赖 - -Status: implemented +# RFC:将 Cordis 以源码形式收录,而非作为 npm 依赖 [English](2026-06-11-vendor-cordis-as-source.md) | 中文 +Status: implemented + ## 问题 -DeepSeek Harness SDK 基于 Cordis 框架构建。本仓库启动时,Cordis core 处于 4.0.0-rc.6(一个发布候选版本);harness 依赖框架内部实现(fiber 生命周期、effect dispose(资源释放)、waterfall(瀑布式事件)分发),这些行为的精确语义直接关系到 agent loop(智能体循环)的正确性保证。 +DeepSeek Harness SDK 构建于 Cordis 框架之上。本仓库启动时,Cordis core 处于 4.0.0-rc.6(一个候选发布版本);harness 依赖框架内部实现(fiber 生命周期、dispose(资源释放)、waterfall(瀑布式事件)分发),其确切行为直接关系到 agent loop(智能体循环)的正确性保证。 ## 决策 -将所需的 Cordis 包(core、loader、include、group、timer、hmr、logger-console)及 cordiverse 基础库(cosmokit、schemastery)以源码形式扁平复制到 `vendor/`,保留其原始 npm 包名,使 workspace 解析透明。真正的第三方依赖(js-yaml、chokidar、@standard-schema/spec 等)仍留在 npm。 +将所需的 Cordis 包(core、loader、include、group、timer、hmr、logger-console)与 cordiverse 基础库(cosmokit、schemastery)以源码形式复制到 `vendor/`,扁平化放置,保留其原始 npm 包名以实现透明的 workspace 解析。真正的第三方依赖(js-yaml、chokidar、@standard-schema/spec 等)仍从 npm 获取。 -`vendor/README.md` 是 manifest(元数据清单):记录每个包的上游仓库 + commit SHA,以及一份详尽的本地修改日志。pre-commit 守卫(`scripts/check-vendor-manifest.sh`)会拒绝未在同一次提交中更新 manifest 的 vendor 源码改动。 +`vendor/README.md` 是 manifest(元数据清单):记录每个包(package)的上游仓库 + commit SHA,以及一份详尽的本地修改日志。pre-commit 守卫(`scripts/check-vendor-manifest.sh`)会拒绝未在同一次提交中更新 manifest 的 vendor 源码变更。 ## 曾考虑的替代方案 -- **依赖 npm 包**:否决。core 处于发布候选阶段,且 harness 依赖框架内部实现(fiber 生命周期、effect dispose、waterfall 分发),agent loop 的正确性保证取决于这些行为的精确语义;上游 RC 版本升级可能在没有本地修复路径的情况下破坏它们。 -- **传递性地收录所有依赖**:否决。真正的第三方依赖(js-yaml、chokidar、@standard-schema/spec 等)仍留在 npm;只有内部实现对我们有影响的框架层才被纳入自有管理。 +- **依赖 npm 包**:否决。core 处于候选发布阶段,harness 依赖框架内部实现(fiber 生命周期、dispose、waterfall 分发),agent loop 的正确性保证取决于这些行为的确切表现;上游 RC 版本升级可能在没有本地修复路径的情况下破坏它们。 +- **递归收录所有传递依赖**:否决。真正的第三方依赖(js-yaml、chokidar、@standard-schema/spec 等)仍从 npm 获取;只有内部实现对我们有影响的框架层才需要自行持有。 ## 后果 -- harness 完全拥有其框架层:可审计、可打补丁、版本锁定。上游 RC 无法破坏我们,框架 bug 可以在仓库内直接修复。 -- 上游同步是手动的(manifest 中记录了操作步骤)。修改日志使 diff 面始终可知。 -- vendor 包保留上游代码风格;lint 与严格性门禁将其排除(它们的 tsconfig 在本地放宽了我们较新的编译器 flag)。 -- 从第一天起就存在一个本地补丁:移除了 HMR 的 locale-YAML 导入(运行时 YAML 导入钩子未被收录)。 +- harness 完全持有其框架层:可审计、可打补丁、版本锁定。上游 RC 无法影响我们,框架 bug 可以在仓库内直接修复。 +- 上游同步是手动操作(流程记录在 manifest 中)。修改日志使 diff 范围始终可知。 +- 收录的包保留上游代码风格;lint 与严格性门禁将其排除(它们的 tsconfig 在本地放宽了我们较新的编译器选项)。 +- 从第一天起就有一个本地补丁:移除了 hmr 的 locale-YAML 导入(运行时 YAML 导入钩子未被收录)。 diff --git a/docs/rfc/implemented/process/2026-06-16-pnpm-over-yarn.i18n.yaml b/docs/rfc/implemented/process/2026-06-16-pnpm-over-yarn.i18n.yaml index c2b598e0db..ec328ff0e0 100644 --- a/docs/rfc/implemented/process/2026-06-16-pnpm-over-yarn.i18n.yaml +++ b/docs/rfc/implemented/process/2026-06-16-pnpm-over-yarn.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-16-pnpm-over-yarn.md: 6e7a6e1f53056e36f54f44b87b305afa593da549 -2026-06-16-pnpm-over-yarn.zh.md: 809f10dbd63d347eccb4d00641a39da51788ee9f +2026-06-16-pnpm-over-yarn.zh.md: ba63575909f2bafbbad2102c85d6bede77999997 diff --git a/docs/rfc/implemented/process/2026-06-16-pnpm-over-yarn.zh.md b/docs/rfc/implemented/process/2026-06-16-pnpm-over-yarn.zh.md index 809f10dbd6..ba63575909 100644 --- a/docs/rfc/implemented/process/2026-06-16-pnpm-over-yarn.zh.md +++ b/docs/rfc/implemented/process/2026-06-16-pnpm-over-yarn.zh.md @@ -1,43 +1,43 @@ -# RFC:以 pnpm 替代 Yarn 4 作为包管理器 - -Status: implemented +# RFC:使用 pnpm 替代 Yarn 4 作为包管理器 [English](2026-06-16-pnpm-over-yarn.md) | 中文 +Status: implemented + ## 问题 -本仓库最初使用 **Yarn 4** 搭配 `node-modules` linker 发布——这是一个刻意保守的选择:行为类似 npm 的扁平布局,同时提供 Yarn 的 workspace 和 `yarn constraints`。它能用。但 Yarn 4 的 Plug'n'Play 血统使得 `node-modules` linker 成为非主流模式,而更广泛的 JS 生态——工具默认值、CI action、Corepack 示例、贡献者熟悉度——正日益以 pnpm 为中心。对于一个主要由 agent 构建、偶尔有人类贡献者阅读的仓库来说,「大多数工具和人所预期的包管理器」具有实际价值:更少的意外、更成熟的故障路径、更多可直接复用的答案。 +本仓库最初使用 **Yarn 4** 搭配 `node-modules` 链接器启动。这是一个刻意保守的选择:行为类似 npm 的扁平布局,同时享有 Yarn 的 workspaces 和 `yarn constraints`。它能正常工作。但 Yarn 4 源自 Plug'n'Play 的血统,使得 `node-modules` 链接器成为非主流模式;而更广泛的 JS 生态——工具默认值、CI action、Corepack 示例、贡献者的熟悉度——正日益以 pnpm 为中心。对于一个主要由 agent(智能体)构建、偶尔有人类贡献者阅读的仓库而言,「大多数工具和人所期望的包管理器」具有实际价值:更少的意外、更成熟的故障路径、更多可直接复用的解答。 -切换成本目前处于最低点。本仓库尚无任何包发布(所有 package 均为 `private: true`);开发/测试/演示全部通过 tsx **未构建**运行,因此包管理器只需做到 (a) 解析并链接 `node_modules`,(b) 运行 workspace 脚本,(c) 强制执行 workspace 约束。唯一的 Yarn 专属资产是 `yarn.config.cjs`(`@yarnpkg/types` 约束引擎),体量小且可机械地重新表达。这与 [tsdown 决策](2026-06-11-tsdown-over-dumble.md)的逻辑一致:趁爆炸半径还小,把承重工具换成生态更健康的选项。 +切换成本目前处于最低点。本仓库尚无任何包(package)发布(每个包都是 `private: true`);开发/测试/演示全部通过 tsx **未构建**运行,因此包管理器只需做到:(a) 解析并链接 `node_modules`,(b) 运行 workspace 脚本,(c) 强制执行 workspace 约束。唯一的 Yarn 特有资产是 `yarn.config.cjs`(`@yarnpkg/types` 约束引擎),体量小且可机械地重新表达。这与 [tsdown 决策](2026-06-11-tsdown-over-dumble.md)的逻辑一致:在爆炸半径尚小时,将承重工具换为生态更健康的选项。 ## 决策 -采用 **pnpm 11.7.0**,通过 `packageManager` 字段固定、经 Corepack 安装(与 Yarn 使用的机制相同): +采用 **pnpm 11.7.0**,通过 `packageManager` 字段固定版本,经 Corepack 安装(与 Yarn 使用的机制相同): -- **Workspace** 从 `package.json` 的 `workspaces` 数组加 `.yarnrc.yml` 迁移到 `pnpm-workspace.yaml`(`vendor/*`、`packages/*`——相同的 glob;`examples/*` 保持非 workspace,与先前设置及 tsdown 的显式 glob 一致)。 -- **严格符号链接 linker**(pnpm 默认)取代 Yarn 的 hoisted `node-modules` linker。我们刻意**不**添加 `node-linker=hoisted` / `shamefully-hoist` 逃生口:pnpm 的非扁平 `node_modules` 会让幽灵依赖(引用未声明的传递依赖)大声失败,这对一个以机械门禁为整体质量策略的仓库而言是一个*优点*(见[机械质量门禁](2026-06-11-quality-gates.md))。门禁套件——类型检查、lint、测试、构建、knip——是安全网,证明不存在此类幽灵引用。 -- **构建脚本白名单。** pnpm 10+ 不运行依赖的生命周期脚本,除非显式列入白名单。`pnpm-workspace.yaml` 携带一份显式的 `allowBuilds` 映射(`esbuild`、`lefthook`、`@google/genai`、`protobufjs`)——与本仓库对模型/工具输出已有的供应链加固姿态一致,现在将其扩展到安装时的代码执行。`peerDependencyRules.allowedVersions.typescript: '>=5 <7'` 消除仓库内 TypeScript 的良性 peer 范围警告。 -- **约束变为包管理器无关。** `yarn.config.cjs`(导入 `@yarnpkg/types`、使用 `Yarn.workspaces()` / `workspace.set()`)被 `scripts/check-workspace-constraints.ts` 取代——一个纯 tsx 脚本,以 `pnpm run constraints` 运行。它在相同的 `vendor` + `packages` 范围上强制执行完全相同的不变式:所有 package `private: true`;`@deepseek-ai/dsh-*` 包将 `cordis` 同时声明为对等依赖(peer dependency)和 dev 依赖且范围匹配、使用根 `package.json` 的版本、设置 `type: module`;vendor 包仅检查 privacy。 -- 所有 CI、lefthook 钩子、`package.json` 脚本和文档中的 `yarn …` 动词统一改为 `pnpm …` / `pnpm run …`。`yarn.lock` → `pnpm-lock.yaml`(lockfile v9)。`.gitignore` 将 `.yarn/` 换为 `.pnpm-store/`。vendor README(如 `vendor/cordis/README.md`)按 Vendoring Policy 保留其上游的 `yarn` 示例不动。 +- **Workspaces** 从 `package.json` 的 `workspaces` 数组 + `.yarnrc.yml` 迁移到 `pnpm-workspace.yaml`(`vendor/*`、`packages/*`——同样的 glob;`examples/*` 保持非 workspace,与先前设置及 tsdown 的显式 glob 一致)。 +- **严格符号链接链接器**(pnpm 默认)取代 Yarn 的提升式 `node-modules` 链接器。我们刻意**不**添加 `node-linker=hoisted` / `shamefully-hoist` 逃生口:pnpm 的非扁平 `node_modules` 会让幻影依赖(引用未声明的传递依赖)大声失败,这对于一个以机械门禁为核心质量保障的仓库(见[机械质量门禁](2026-06-11-quality-gates.md))是一项*优势*。门禁套件(typecheck、lint、test、build、knip)是证明不存在此类幻影导入的安全网。 +- **构建脚本白名单。** pnpm 10+ 不运行依赖的生命周期脚本,除非将其加入白名单。`pnpm-workspace.yaml` 携带一份显式的 `allowBuilds` 映射(`esbuild`、`lefthook`、`@google/genai`、`protobufjs`)——与本仓库对模型/工具输出已有的供应链加固姿态一致,现在也应用于安装时的代码执行。`peerDependencyRules.allowedVersions.typescript: '>=5 <7'` 消除仓库内 TypeScript 的良性 peer 范围警告。 +- **约束变为包管理器无关。** `yarn.config.cjs`(导入 `@yarnpkg/types`,使用 `Yarn.workspaces()` / `workspace.set()`)被 `scripts/check-workspace-constraints.ts` 取代——一个纯 tsx 脚本,通过 `pnpm run constraints` 运行。它在相同的 `vendor` + `packages` 范围上强制执行完全相同的不变式:每个包 `private: true`;`@deepseek-ai/dsh-*` 包将 `cordis` 同时声明为对等依赖(peer dependency)和 dev 依赖且范围一致、使用根 `package.json` 的版本、设置 `type: module`;vendor 包仅检查 privacy。 +- 所有 CI、lefthook 钩子、`package.json` 脚本和文档中的 `yarn …` 动词变为 `pnpm …` / `pnpm run …`。`yarn.lock` → `pnpm-lock.yaml`(lockfile v9)。`.gitignore` 将 `.yarn/` 换为 `.pnpm-store/`。vendor README(如 `vendor/cordis/README.md`)按 Vendoring Policy 保持其上游 `yarn` 示例不变。 ## 曾考虑的替代方案 -- **保留 Yarn 4**:零变动,但押注于使用者更少的 linker 模式和绑定单一包管理器的约束引擎。 -- **npm workspaces**:无处不在,但没有约束机制,monorepo 人体工学也更弱。 -- **pnpm 搭配 hoisted linker**:迁移更平滑,但放弃了幽灵依赖安全性——而这正是迁移的首要正确性理由。 +- **保留 Yarn 4**——零变动,但押注于使用率较低的链接器模式和一个绑定单一包管理器的约束引擎。 +- **npm workspaces**——无处不在,但没有约束方案,monorepo 人体工学也较弱。 +- **pnpm 搭配提升式链接器**——迁移更平滑,但放弃了幻影依赖安全性,而这正是迁移的核心正确性理由。 ## 后果 -约束检查失去了 Yarn 的自动**修复**能力(`workspace.set()` 可以就地改写 manifest);tsx 脚本仅做检查,不通过时以非零退出码加消息退出。这是可接受的:CI 从未运行过 `--fix`,且需要手动改一行的情况很少。贡献者现在为 pnpm 而非 Yarn 运行 `corepack enable`;`pnpm exec lefthook install` 取代 `yarn lefthook install`(`postinstall` 钩子仍会运行 `lefthook install`)。 +约束检查失去了 Yarn 的自动**修复**能力(`workspace.set()` 能原地改写 manifest);tsx 脚本仅做检查,不通过时以非零退出码和消息退出。这是可接受的:CI 从未运行过 `--fix`,且需要手动编辑的情况很少。贡献者现在为 pnpm 而非 Yarn 运行 `corepack enable`;`pnpm exec lefthook install` 取代 `yarn lefthook install`(`postinstall` 钩子仍会运行 `lefthook install`)。 性能(迁移时在开发 NFS 文件系统上测量;单次运行样本,方差大——仅供方向性参考,非基准测试套件): | 场景 | Yarn 4 | pnpm 11 | |---|---|---| -| Cold (empty cache/store, no `node_modules`) | ~14 s | ~16 s | -| Warm relink (cache/store warm, `node_modules` removed) | ~12–14 s | ~15–22 s | -| Frozen, `node_modules` present (no-op revalidate) | ~2–8 s | ~0.5–7 s | +| 冷启动(空缓存/store,无 `node_modules`) | ~14 s | ~16 s | +| 热重链接(缓存/store 已热,`node_modules` 已删除) | ~12–14 s | ~15–22 s | +| 冻结,`node_modules` 存在(无操作重验证) | ~2–8 s | ~0.5–7 s | -在快速本地磁盘上,pnpm 的内容寻址 store 通常在冷/热安装上胜出,尤其在多次 checkout 的**磁盘占用**上优势明显(一个全局 store 通过硬链接进入每个 `node_modules`,而 Yarn 每个 worktree 复制约 279 MB——部分开发者日常保持约 10 个或更多 worktree)。这一去重优势在上述迁移时数据中**未**体现,因为测试 store 和 `node_modules` 位于不同文件系统,硬链接失效;在单文件系统的开发机或 CI 缓存上该优势成立。诚实的总结:在我们的 NFS 开发文件系统上,安装速度在噪声范围内不分伯仲;迁移的理由是生态对齐、幽灵依赖安全性和跨 checkout 磁盘去重——而非原始安装时间的胜出。 +在快速本地磁盘上,pnpm 的内容寻址 store 通常在冷/热安装中胜出,尤其在多个检出之间的**磁盘占用**方面优势明显(一个全局 store 通过硬链接接入每个 `node_modules`,而 Yarn 每个 worktree 复制约 279 MB——部分开发者经常为本仓库保持约 10 个或更多 worktree)。该去重优势在上述迁移时数据中**未能**体现,因为测试 store 和 `node_modules` 位于不同文件系统,硬链接失效;在单文件系统的开发机或 CI 缓存上则适用。诚实的总结:在我们的 NFS 开发文件系统上,安装速度在噪声范围内不分伯仲;迁移的理由是生态对齐、幻影依赖安全性和跨检出磁盘去重,而非原始安装时间的胜出。 -所有质量门禁(约束、类型检查、lint、doc-sync、100% test:coverage、构建、knip、publint、echo-agent 演示冒烟测试)在 pnpm 上原样通过,这是 linker 切换未引入幽灵依赖破坏的正确性证明。 +所有质量门禁(constraints、typecheck、lint、doc-sync、test:coverage 100%、build、knip、publint、echo-agent 演示冒烟测试)在 pnpm 上原样通过,这是链接器切换未引入幻影依赖破坏的正确性证明。 diff --git a/docs/rfc/implemented/process/2026-06-17-ts-build-config.i18n.yaml b/docs/rfc/implemented/process/2026-06-17-ts-build-config.i18n.yaml index 9858ce0f2e..92e8c81d4b 100644 --- a/docs/rfc/implemented/process/2026-06-17-ts-build-config.i18n.yaml +++ b/docs/rfc/implemented/process/2026-06-17-ts-build-config.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-17-ts-build-config.md: cf70014b5873f74da8476c21dd71feedb956f59f -2026-06-17-ts-build-config.zh.md: f70619de5a48c7040e816c54d21f81772a51a90f +2026-06-17-ts-build-config.zh.md: d3dd0fb13edd22f1ae365286cbf8144fa0bc3f69 diff --git a/docs/rfc/implemented/process/2026-06-17-ts-build-config.zh.md b/docs/rfc/implemented/process/2026-06-17-ts-build-config.zh.md index f70619de5a..d3dd0fb13e 100644 --- a/docs/rfc/implemented/process/2026-06-17-ts-build-config.zh.md +++ b/docs/rfc/implemented/process/2026-06-17-ts-build-config.zh.md @@ -1,47 +1,46 @@ -# RFC:以 TSC 为核心的构建与统一 tsconfig - -Status: implemented +# RFC:TSC 优先的构建与单一 tsconfig [English](2026-06-17-ts-build-config.md) | 中文 +Status: implemented + ## 问题 -当时的 TypeScript 构建与类型检查配置存在以下问题: +此前的 TypeScript 构建与类型检查配置存在以下问题: -- `build` 使用 `tsc` 将 `packages//` 和 `vendor/*` 下的 `.ts` 转换为 `.d.ts`,再用 `tsdown` 将 `.ts` 转换为打包后的 `.js`。这导致两个工具各自做一次 TypeScript 转换。 -- `typecheck` 倾向于通过一个根级 typecheck 配置来校验 package、vendor 源码、示例、测试和脚本。 +- `build` 使用 `tsc` 将 `packages//` 和 `vendor/*` 下的 `.ts` 转换为 `.d.ts` 文件,然后使用 `tsdown` 将 `.ts` 转换为打包后的 `.js` 文件。这导致两个工具各自执行 TypeScript 转换。 +- `typecheck` 倾向于通过一个根目录的 typecheck 配置来校验 package、vendor 源码、示例、测试和脚本。 -目标是让 build 和 typecheck 使用一致的 tsconfig 边界与 TypeScript 解析/转换行为。build 应通过同一个编译器和配置生成 `.js`、`.d.ts`、`.js.map` 和 `.d.ts.map`,使发布产物与类型校验保持一致。 +目标是让构建与类型检查使用一致的 tsconfig 边界和 TypeScript 解析/转换行为。构建应通过单一编译器和配置生成 `.js`、`.d.ts`、`.js.map` 和 `.d.ts.map`,使发布产物与类型校验保持一致。 -验证过程中发现了若干具体技术问题和可能的路径: +验证过程中发现了若干具体的技术问题和可能的路径: -- `tsdown` 使用 `oxc` 做 TypeScript 转换,其行为与 `tsc` 不同。 +- `tsdown` 使用 `oxc` 进行 TypeScript 转换,其行为与 `tsc` 不同。 - `tsdown` 输出的打包 `.d.ts` 与 Cordis 内部的相对模块增强(module augmentation)结构冲突。 - - tsc 的输出受 `allowImportingTsExtensions` 影响,因此需要确保生成的 `.js` 不会 import `.ts` 文件,且生成的 `.d.ts` 保留 NodeNext/Node16 可接受的显式相对说明符。为此,包内相对导入在 TypeScript 源码中使用显式 `.ts` 说明符,由 `rewriteRelativeImportExtensions` 在输出的 JS 中将其改写为 `.js`。 - - `tsdown` 输出的打包 `.js` 与 `tsc -b` 逐文件输出的 `.js` 行为不同,例如 decorator 转换行为。 -- `vendor/*/src`、示例、测试和脚本无法全部以 plain-include 方式放入一个根级严格程序。 - - 在根级严格配置下直接对 `vendor/*/src` 做类型检查,会触发大量不属于本项目的类型错误。 - - `packages/*/*` 对 `vendor` 的依赖解析到 `vendor/*/lib`,以适应不同的 tsconfig 严格度。 - + - `tsc` 的输出受 `allowImportingTsExtensions` 影响,因此需要确保生成的 `.js` 文件不会导入 `.ts` 文件,且生成的 `.d.ts` 文件保留 NodeNext/Node16 接受的显式相对说明符。为此,包内相对导入在 TypeScript 源码中使用显式 `.ts` 说明符,由 `rewriteRelativeImportExtensions` 在输出的 JS 中将其重写为 `.js`。 + - `tsdown` 输出的打包 `.js` 与 `tsc -b` 逐文件输出的 `.js` 行为不同,例如装饰器转换行为。 +- `vendor/*/src`、示例、测试和脚本无法全部以 plain-include 方式纳入一个根目录的严格程序。 + - 在根目录严格配置下直接对 `vendor/*/src` 做类型检查,会触发大量不属于本项目所有权范围的类型错误。 + - `packages/*/*` 对 `vendor` 的包依赖解析到 `vendor/*/lib`,以适应不同的 tsconfig 严格度。 ## 决策 包内相对导入使用显式 `.ts` 说明符。 -`pnpm run build` 分两阶段: +`pnpm run build` 是两阶段构建: -- 阶段 1:`tsc -b tsconfig.build.json` 将逐模块的 `.js`、声明文件 `.d.ts`、JS sourcemap `.js.map` 和声明 sourcemap `.d.ts.map` 输出到各包的 `lib/types`。这是权威的 TypeScript 编译结果。发布时保留 `.d.ts` / `.d.ts.map`,忽略 `.js` / `.js.map`。 - - 构建项目使用 `tsc -b` 编译的 project-reference 图。例如,根 `tsconfig.build.json` 引用包和 vendor 的 tsconfig,校验并输出包/vendor 的构建结果。 -- 阶段 2:bundler 读取 `lib/types` 下输出的 JS,将打包后的运行时入口写为 `lib/index.js` 或 `lib/index.mjs`(沿用当前行为)。此阶段仅做打包,不得读取 TypeScript 源码,也不得输出声明文件。 +- 阶段 1:`tsc -b tsconfig.build.json` 将逐模块的 `.js`、声明文件 `.d.ts`、JS sourcemap `.js.map` 和声明 sourcemap `.d.ts.map` 输出到各 package 的 `lib/types`。这是权威的 TypeScript 编译结果。发布时保留 `.d.ts` / `.d.ts.map`,忽略 `.js` / `.js.map`。 + - 构建项目使用 `tsc -b` 编译的 project-reference 图。例如,根 `tsconfig.build.json` 引用 package 和 vendor 的 tsconfig,校验并输出 package/vendor 的构建结果。 +- 阶段 2:打包器读取 `lib/types` 下输出的 JS,将打包后的运行时入口写为 `lib/index.js` 或 `lib/index.mjs`(沿用当前行为)。此阶段仅做打包,禁止读取 TypeScript 源码或输出声明文件。 `tsdown` 不再负责 TypeScript 编译或声明文件输出。 `pnpm run typecheck` 以 build 模式运行根 `tsconfig.json`。 -- 根 `tsconfig.json` 是唯一的开发/类型检查项目。它以 `noEmit` 检查示例、测试和脚本,并通过 references 校验包/vendor 源码。 -- 被引用的包/vendor 项目保持与 build 相同的输出行为,因此 typecheck 可以刷新它们的 `lib/types` 产物,而无需使用单独的 no-emit 图。项目特有的严格度设置放在各自的 `packages/*/*/tsconfig.json` 或 `vendor/*/tsconfig.json` 中。 -- 根 no-emit 项目禁用 `rewriteRelativeImportExtensions`;它不输出任何文件,且包含跨 project-reference 边界导入 helper 的测试。包/vendor 的输出项目保持该改写启用。 +- 根 `tsconfig.json` 是唯一的开发/类型检查项目。它以 `noEmit` 方式检查示例、测试和脚本,并通过 references 校验 package/vendor 源码。 +- 被引用的 package/vendor 项目保持与 build 相同的输出行为,因此 typecheck 可以刷新它们的 `lib/types` 输出,而无需使用独立的 no-emit 图。项目特定的严格度变更放在各自的 `packages/*/*/tsconfig.json` 或 `vendor/*/tsconfig.json` 中。 +- 根 no-emit 项目禁用 `rewriteRelativeImportExtensions`;它不输出任何文件,且包含跨 project-reference 边界导入 helper 的测试。package/vendor 的 emit 项目保持重写开启。 -命令编排如下: +命令编排结构如下: ```sh pnpm run build: @@ -59,20 +58,20 @@ tsc -b tsconfig.json ## 曾考虑的替代方案 -- **继续使用 `tsdown`/oxc 作为 TypeScript 转换器**:oxc 的转换行为与 `tsc` 不同(decorator 转换有差异、打包 JS 与逐文件输出不同),且其打包 `.d.ts` 与 Cordis 内部的相对模块增强结构冲突。 -- **一个根级严格程序覆盖包、vendor、示例、测试和脚本**:vendor 源码在根级严格 flag 下会触发不属于本项目的类型错误;带有各项目独立严格度的 project references 才是可行的边界。 +- **继续使用 `tsdown`/oxc 作为 TypeScript 转换器**:oxc 的转换行为与 `tsc` 不同(装饰器转换有差异、打包 JS 与逐文件输出不同),且其打包 `.d.ts` 与 Cordis 内部的相对模块增强结构冲突。 +- **用一个根目录严格程序覆盖 package、vendor、示例、测试和脚本**:vendor 源码在根目录严格标志下会触发不属于本项目所有权范围的类型错误;带有逐项目严格度的 project references 才是可行的边界。 ## 后果 构建职责更加清晰: -- `packages//` 和 `vendor/*` 下的每个模块都有一个本地 tsconfig,同时服务于 build、typecheck 以及直接运行源码的工具(如 `tsx` 和 `vitest`)。 -- `build` 命令使用 `tsconfig.build.json`。`tsc -b` 负责可发布的逐模块 `.js` 和 `.d.ts` 输出,bundler 只负责 `lib/index.*`。 - - `lib/types/*.d.ts` 和 `.d.ts.map` 是发布用的声明文件产物。 +- `packages//` 和 `vendor/*` 下的每个模块有一份本地 tsconfig,同时服务于构建、类型检查和直接运行源码的工具(如 `tsx` 和 `vitest`)。 +- `build` 命令使用 `tsconfig.build.json`。`tsc -b` 负责可发布的逐模块 `.js` 和 `.d.ts` 输出,打包器仅负责 `lib/index.*`。 + - `lib/types/*.d.ts` 和 `.d.ts.map` 是发布用的声明输出。 - `lib/types/*.d.ts` 使用显式 `.ts` 相对说明符,TypeScript 的 NodeNext/Node16 解析器会将其映射到同级的 `.d.ts` 文件。 - - `lib/types/*.js` 仅作为 bundler 输入,不得用作运行时入口或公开导入目标。 - - `lib/index.*` 是发布用的运行时产物,由 bundler(当前为 `tsdown`)生成。 -- `pnpm run verify-node-next-types` 扫描构建出的声明文件,检查是否存在缺少文件扩展名的相对说明符,然后以 `moduleResolution: "NodeNext"` 对构建出的 `types`/`exports` 表面进行临时外部 ESM 消费方的类型检查,使声明说明符的回归在发布前即被捕获。 -- `typecheck` 命令使用 `tsconfig.json`。示例、测试和脚本由根 no-emit 项目检查,包和 vendor 模块保持与 `build` 相同的输出行为。包和 vendor 源码始终处于 project-reference 边界之后。 + - `lib/types/*.js` 仅作为打包器输入,禁止用作运行时入口或公开导入目标。 + - `lib/index.*` 是发布用的运行时输出,由打包器(当前为 `tsdown`)生成。 +- `pnpm run verify-node-next-types` 扫描构建出的声明文件,检查是否存在缺少文件扩展名的相对说明符,然后以 `moduleResolution: "NodeNext"` 对构建出的 `types`/`exports` 接口进行临时外部 ESM 消费方的类型检查,确保声明说明符的回归在发布前被捕获。 +- `typecheck` 命令使用 `tsconfig.json`。示例、测试和脚本由根 no-emit 项目检查,package 和 vendor 模块保持与 `build` 相同的输出行为。package 和 vendor 源码始终处于 project-reference 边界之后。 -Cordis vendor 副本现在与上游多了一处类型结构差异。上游同步时,必须重新应用该差异或明确将其退役。 +Cordis 的 vendor 副本现在与上游多了一处类型结构差异。在上游同步时,该差异必须被重新应用或明确废弃。 diff --git a/docs/rfc/implemented/process/2026-06-18-markdown-cross-link-lint.i18n.yaml b/docs/rfc/implemented/process/2026-06-18-markdown-cross-link-lint.i18n.yaml index 3773aca2f5..2b7b62f141 100644 --- a/docs/rfc/implemented/process/2026-06-18-markdown-cross-link-lint.i18n.yaml +++ b/docs/rfc/implemented/process/2026-06-18-markdown-cross-link-lint.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-18-markdown-cross-link-lint.md: c802b4071abf652824647e4417cde3f518776353 -2026-06-18-markdown-cross-link-lint.zh.md: abbb5930acb18287effc7c47009b9e3e910f6c89 +2026-06-18-markdown-cross-link-lint.zh.md: 917580ffce71258896f3d23ef7efef4be0176949 diff --git a/docs/rfc/implemented/process/2026-06-18-markdown-cross-link-lint.zh.md b/docs/rfc/implemented/process/2026-06-18-markdown-cross-link-lint.zh.md index abbb5930ac..917580ffce 100644 --- a/docs/rfc/implemented/process/2026-06-18-markdown-cross-link-lint.zh.md +++ b/docs/rfc/implemented/process/2026-06-18-markdown-cross-link-lint.zh.md @@ -1,33 +1,33 @@ -# RFC:Markdown 交叉链接有效性 lint - -Status: implemented +# RFC:Markdown 交叉链接有效性检查 [English](2026-06-18-markdown-cross-link-lint.md) | 中文 +Status: implemented + ## 问题 -本仓库的文档通过相对路径互相链接:`[topic](../implemented/2026-…-….md)`、`[the cookbook](adding-a-tool.md)`、`[architecture.md](../../architecture.md)`。此前没有任何机制验证这些目标是否存在。一次重命名或移动会悄无声息地打断所有入站链接,直到读者点击时才会发现。[Doc-sync 强制](2026-06-11-doc-sync-enforcement.md)已经将两类文档漂移机械化了(不可编译的代码块、陈旧的事件分类体系表),[verify-md-wrap](2026-06-11-doc-sync-enforcement.md) 处理了第三类(硬换行的行文段落),但死链接是第四类同样可机械检查的问题,此前仍靠肉眼验证。 +本仓库的文档通过相对路径互相链接:`[topic](../implemented/2026-…-….md)`、`[the cookbook](adding-a-tool.md)`、`[architecture.md](../../architecture.md)`。此前没有任何机制验证这些目标是否存在。重命名或移动文件会静默破坏所有指向它的链接,且在读者点击之前不可见。[Doc-sync 强制](2026-06-11-doc-sync-enforcement.md)已经将两类文档漂移机械化(无法编译的代码块、陈旧的事件分类表),[verify-md-wrap](2026-06-11-doc-sync-enforcement.md) 覆盖了第三类(硬换行的段落),但死链是第四类同样可机械检查、却仍靠肉眼验证的问题。 -直接触发本门禁的案例是引入它的那次 RFC 目录重组:将 `docs/adr/` + `docs/rfc/` 统一为一个 `docs/rfc/`,下设 `proposed/`、`implemented/`、`rejected/` 子目录,手动改写了约四十条文档间链接。任何一条路径的手误都会让断链随代码一起合入,而没有任何东西能拦住它。 +触发本门禁的直接案例是引入它的那次 RFC 目录重组:将 `docs/adr/` + `docs/rfc/` 统一为一个 `docs/rfc/`,下设 `proposed/`/`implemented/`/`rejected/` 子目录,手动改写了约四十条文档间链接。任何一个手误路径都会让一条死链随代码入库,而没有任何东西能拦住它。 ## 决策 新增第四道 `doc-sync` 门禁 `verify-md-links`(`scripts/verify-md-links.ts`),风格与 `verify-md-wrap` 一致(tsx ESM、基于 AST、只验证不生成): -- 用 `mdast-util-from-markdown` + GFM 解析范围内的每个 Markdown 文件,遍历所有 `link`、`image` 和 `definition` 节点。 -- 仅当目标是**相对路径**时才检查。跳过带协议的 URL(`https:`、`mailto:` 等)、协议相对路径(`//host`)、根绝对路径(`/path`——在 checkout 中没有稳定基准)以及纯页内锚点(`#section`)。去除 `#fragment`/`?query`,相对于链接所在文件的目录解析路径,并断言该路径在磁盘上存在。 -- 只报告,不改写;发现第一条断链即以非零状态退出。 +- 使用 `mdast-util-from-markdown` + GFM 解析每个范围内的 Markdown 文件,遍历所有 `link`、`image` 和 `definition` 节点。 +- 仅当目标是**相对路径**时才检查。跳过带协议的 URL(`https:`、`mailto:` 等)、协议相对路径(`//host`)、根绝对路径(`/path`,在检出目录中没有稳定基准)以及纯页内锚点(`#section`)。剥除 `#fragment`/`?query`,相对于链接所在文件的目录解析路径,并断言目标在磁盘上存在。 +- 只报告、不改写;发现第一条死链即以非零状态退出。 -范围与其他门禁一致,另加 AGENTS.md 对和 `.agents/skills/` 下仓库自有的 agent skill Markdown(这些 skill 文件交叉链接到 docs 目录树,因此本次重组也改写了其中的链接):`README.md`、`docs/**/*.md`、`packages/*/README.md`、`AGENTS.md`、`packages/AGENTS.md`、`.agents/skills/**/*.md`,按真实路径去重(`CLAUDE.md` 符号链接解析到 AGENTS.md 文件)。该门禁接入 lefthook pre-push 钩子和 CI 都会运行的 `doc-sync` 脚本,因此断链在推送前就会在本地失败——与[机械质量门禁](2026-06-11-quality-gates.md)保持一致。 +范围与其他门禁一致,另外加上 AGENTS.md 对和 `.agents/skills/` 下仓库自有的 agent skill Markdown(这些 skill 文件交叉链接到 docs 目录,因此本次重组也改写了其中的链接):`README.md`、`docs/**/*.md`、`packages/*/README.md`、`AGENTS.md`、`packages/AGENTS.md`、`.agents/skills/**/*.md`,按真实路径去重(`CLAUDE.md` 符号链接解析到 AGENTS.md 文件)。它接入 lefthook pre-push 钩子和 CI 都会运行的 `doc-sync` 脚本,因此死链在推送前就会在本地失败——与[机械化质量门禁](2026-06-11-quality-gates.md)一致。 -本门禁检查的是**文件存在性**,而非锚点有效性:链接到一个真实文件但带有 `#wrong-heading` 片段的仍然通过(文件可解析;片段被剥离)。 +本门禁检查的是**文件存在性**,而非锚点有效性:指向一个真实文件但带有 `#wrong-heading` 片段的链接仍会通过(文件可解析;片段被剥除)。 ## 曾考虑的替代方案 -**锚点级有效性检查**:更重且价值更低;实际造成问题的是文件级死链接。这一范围裁剪是有意为之:作者在链接到某个锚点时自行验证 `#fragment`。 +**锚点级有效性检查**:更重且价值更低;实际造成问题的是文件级死链。这一范围裁剪是有意为之:作者在链接到某个锚点时自行验证 `#fragment`。 ## 后果 -- 重命名或移动导致交叉链接悬空时,pre-push 钩子和 CI 会立即失败,而不是等读者点击死链接才发现。这使得引入本门禁的 RFC 重组具有自验证性:同一个 PR 既改写了四十条链接,也加入了证明无一悬空的检查。 -- `doc-sync` 链中多了一个快速 tsx 脚本;无新增依赖(mdast/GFM 技术栈已在 devDependencies 中供 `verify-md-wrap` 使用)。 -- 本门禁强制的约定——通过可机械检查的相对链接交叉引用文档,而非裸文字或编号——记录在 [docs/AGENTS.md](../../../AGENTS.md) 中,让作者知道这道门禁的存在及其原因。 +- 重命名或移动文件导致交叉链接悬空时,现在会在 pre-push 钩子和 CI 中失败,而不是等读者点击死链才发现。这使得引入本门禁的 RFC 重组具备自验证能力:改写四十条链接的同一个 PR 也添加了证明无一悬空的检查。 +- `doc-sync` 链中多了一个快速 tsx 脚本;无新增依赖(mdast/GFM 技术栈已作为 `verify-md-wrap` 的 devDependencies 存在)。 +- 本门禁强制的约定——通过可机械检查的相对链接引用文档,而非裸文本或编号——记录在 [docs/AGENTS.md](../../../AGENTS.md) 中,让作者知晓门禁的存在与原因。 diff --git a/docs/rfc/implemented/process/2026-06-20-core-data-structures-catalog.i18n.yaml b/docs/rfc/implemented/process/2026-06-20-core-data-structures-catalog.i18n.yaml index 2421ac85fc..4345634014 100644 --- a/docs/rfc/implemented/process/2026-06-20-core-data-structures-catalog.i18n.yaml +++ b/docs/rfc/implemented/process/2026-06-20-core-data-structures-catalog.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-20-core-data-structures-catalog.md: 5f1232f2f0d0644d4043af217a7177451155030b -2026-06-20-core-data-structures-catalog.zh.md: d35a4d9d32eb59971bbf8b105d01dd38c0811fb0 +2026-06-20-core-data-structures-catalog.zh.md: 8ad4453890d8be5dc4e743d1f6f9aa6a9330ed17 diff --git a/docs/rfc/implemented/process/2026-06-20-core-data-structures-catalog.zh.md b/docs/rfc/implemented/process/2026-06-20-core-data-structures-catalog.zh.md index d35a4d9d32..8ad4453890 100644 --- a/docs/rfc/implemented/process/2026-06-20-core-data-structures-catalog.zh.md +++ b/docs/rfc/implemented/process/2026-06-20-core-data-structures-catalog.zh.md @@ -6,55 +6,55 @@ Status: implemented ## 问题 -想要理解 harness 的读者可以在 [architecture.md](../../../architecture.md) 中找到它的*行为*(服务映射、会话/轮次/步骤生命周期、事件分类体系),但没有一个集中的地方描述它的*词汇*——那些行为所操作的数据结构。类型定义只存在于源码中,散落在各个 `packages/*/src/types.ts` 里,因此要理解「什么是 `Message`、`SessionEvent`、`StreamChunk`」就意味着直接阅读声明。一份行文目录会有帮助,但如果目录是对类型定义的转述或粘贴复制,那么字段一改它就会腐烂——而一份失去同步的类型文档比没有更糟,因为读者会信任它。 +一位想要理解 harness 的读者,可以在 [architecture.md](../../../architecture.md) 中找到它的*行为*(服务映射、session/turn/step 生命周期、事件分类体系),但没有一个集中的地方描述它的*词汇*——即行为所操作的数据结构。类型形状只存在于源码中,分散在各个 `packages/*/src/types.ts` 里,因此要理解「什么是 `Message`、`SessionEvent`、`StreamChunk`」就得直接阅读声明。一份行文目录会有帮助,但如果目录是对类型定义的转述或粘贴复制,那么字段一改它就会腐烂——而失去同步的类型文档比没有更糟,因为读者会信任它。 -因此这项工作包含两个交织的问题:**这样一份目录应当收录什么**(范围界定问题:一个 harness 有数十个跨包(package)的类型,全部堆上去对谁都没帮助),以及**如何防止粘贴的类型定义漂移**(持久性问题)。本 RFC 记录这两项决策。它的姊妹篇 [生成式 Cordis 事件 + 服务目录](2026-06-20-generated-cordis-catalog.md) 是*接线*轴向的补充:本篇编目数据结构,那篇编目移动它们的事件与服务。 +因此这项工作包含两个交织的问题:**这样的目录应当收录什么**(范围界定问题——一个 harness 有数十个跨包类型,全部堆上去对谁都没帮助),以及**如何防止粘贴的类型定义漂移**(持久性问题)。本 RFC 记录两项决策。它的姊妹篇 [生成式 Cordis 事件 + 服务目录](2026-06-20-generated-cordis-catalog.md) 是*接线*轴的补充:本篇编目数据结构,那篇编目传递数据结构的事件与服务。 ## 决策 -新建 `docs/core-data-structures/` 目录编目词汇,并新增 `verify-type-equiv` doc-sync(文档同步门禁)门禁,确保每一处粘贴的类型定义与源码逐字节一致。 +新建 `docs/core-data-structures/` 文件夹编目词汇,并新增 `verify-type-equiv` doc-sync(文档同步门禁)门禁,确保每处粘贴的类型定义与源码逐字节一致。 -### 什么算「核心」——主干与 seam 的分界线 +### 何为"核心"——主干与 seam 的分界线 -范围界定不是自上而下拍板的,而是将候选定义逐一对照具体的边界类型反复测试,直到一条规则在所有案例中都成立。决定性的测试是 `BashExecRequest`/`BashExecSpec`/`BashRunResult`:bash 是一个能力 *seam*,不属于 agent loop 主干;如果这些算「核心」,那「核心」就等于*所有跨包词汇*,目录就是一份平铺的全量转储;如果它们不算,「核心」就意味着*中央主干*,bash 词汇属于子页面。后者胜出,由此确定了整体结构:一个**分层目录**,而非一份平铺文档。 +范围界定并非自上而下拍定,而是将候选定义逐一对照具体的边界类型反复测试,直到一条规则在所有案例中都成立。决定性的测试是 `BashExecRequest`/`BashExecSpec`/`BashRunResult`:bash 是一个能力 *seam*,不属于 agent loop(智能体循环)主干;如果这些算"核心",那么"核心"就意味着*所有跨包词汇*,目录沦为平铺罗列;如果不算,"核心"就意味着*中央主干*,bash 词汇归入子页面。后者胜出,由此确定了整体结构:一个**分层文件夹**,而非一份平铺文档。 -解决剩余案例的规则:***你编写、持有或接收的类型是核心;为其提供类型推导、渲染或持久化的机制是子页面细节。*** 逐一验证如下: +确定其余案例的规则是:***你编写、持有或接收的类型是核心;为其提供类型推导、渲染或持久化的机制是子页面细节。*** 逐一验证如下: -- 一个数据结构是**核心**的,如果它流经 agent loop 主干——无论加载了哪些插件,循环在每个轮次都持有、派生、流式输出或记录它(`Message`、`StreamChunk`、`SessionEvent`、`Agent` 句柄)——**或者**它是插件作者面向某条流水线编写的唯一标题类型(`ToolDefinition`)。 -- `ToolDefinition` 是核心(它是每个工具作者编写的东西),**即使循环从不持有它**——对于这一个标题类型,撰写重要性覆盖了严格的「流经主干」规则。但它的类型推导机制——`SchemaSpec`/`InferArgs` DSL——是子页面细节(你编写的是 `ToolDefinition`;为其提供类型推导的机制你不直接接触)。这就是主干与 seam 分界线的精确表述。 -- `ToolSchema` 是核心(它是 `GenerateOptions` 的字段,而 `GenerateOptions` 是流经每个步骤的模型请求),即使它在概念上属于工具流水线——当*流经主干*与*概念归属*冲突时,前者胜出。 +- 一个数据结构是**核心**的,如果它流经 agent loop 主干——无论加载了哪些插件,循环在每个轮次都会持有、派生、流式输出或记录它(`Message`、`StreamChunk`、`SessionEvent`、`Agent` 句柄)——**或者**它是插件作者面对某条流水线时编写的唯一标志性类型(`ToolDefinition`)。 +- `ToolDefinition` 是核心(它是每个工具作者编写的东西),**即使循环从不持有它**——对于这一个标志性类型,撰写重要性压过了严格的"流经主干"规则。但它的类型推导机制——`SchemaSpec`/`InferArgs` DSL——是子页面细节(你编写的是 `ToolDefinition`;为其提供类型推导的机制你并不直接接触)。这就是主干与 seam 分界线的精确表述。 +- `ToolSchema` 是核心(它是 `GenerateOptions` 的一个字段,而 `GenerateOptions` 是流经每个步骤的模型请求),即使它在概念上属于工具流水线——当*流经主干*与*概念归属*冲突时,前者胜出。 - 工具展示词汇(`ToolCallView`/`ToolResultView` 等)、`SessionPersistence` 持久性 seam 以及 bash 词汇是子页面。 -`core.md` 是一份**自包含的主干文档**:它给出每个主干结构的确切类型定义,配以最少的行文,并链接到各 seam 细节的子页面。子页面包括 `llm-streaming.md`、`session.md`、`persistence.md`(沿内存模型与持久性 seam 的分界从 session 中拆出)、`tools.md` 和 `bash.md`。 +`core.md` 是一份**自包含的主干文档**:它给出每个主干结构的确切类型定义,辅以最少的行文,并链接到子页面获取各 seam 的细节。子页面包括 `llm-streaming.md`、`session.md`、`persistence.md`(沿内存模型与持久性 seam 的分界线从 session 拆出)、`tools.md` 和 `bash.md`。 -### `ts type-equiv` 机制——逐字且防漂移 +### `ts type-equiv` 机制——既逐字又防漂移 -持久性要求很具体:文档应展示**逐字**的当前类型定义(让读者看到真实形状,而非转述),**并且**机械地保证与源码一致。仓库已经能编译围栏 ` ```ts ` 块(`doc-typecheck`),但一个真正通过类型检查的块需要 import 噪音,且只能证明*可赋值性*而非*逐字节相等*——一个改了名但类型相同的字段仍能通过。因此: +持久性需求很具体:文档应当展示**当前类型定义的原文**(让读者看到真实形状,而非转述),**并且**机械地保证与源码一致。仓库已经能编译围栏 ` ```ts ` 块(`doc-typecheck`),但一个真正可编译的块需要 import 噪音,且只证明*可赋值性*而非*字节相等*——一个类型相同但改了名的字段会通过。因此: -- 类型定义逐字粘贴到专用的 ` ```ts type-equiv ` 围栏中。`doc-typecheck` 识别该围栏并跳过它(裸定义不能独立编译),并**将其排除在 opt-out 比例之外**——它是一个独立检查的类别,而非未检查的草稿。 -- 新增 `scripts/verify-type-equiv.ts`,通过 TypeScript 解析器提取每个块,并对声明的符号断言**逐字源码匹配**——之所以选择这种方式而非编译式 `_Check` 可赋值性断言,正是因为逐字节相等而非可赋值性才是我们需要的属性。 -- 来源信息保存在中央 `scripts/type-equiv.manifest.json`(`{ doc, symbol, source }` 条目)中,**而非**行文中的指令注释。脚本强制执行 **1:1 对应**:每个 type-equiv 块恰好有一条 manifest 条目,反之亦然;因此不会有块被静默漏检,也不会有条目腐烂。 -- 接入 `doc-sync`,因此与其他文档门禁在同一个 lefthook pre-push 和 CI 路径中运行。 +- 类型定义逐字粘贴到专用的 ` ```ts type-equiv ` 围栏中。`doc-typecheck` 识别该围栏并跳过它(裸定义不能独立编译),且**将其排除在 opt-out 比例之外**——它是一个独立检查的类别,而非未检查的草稿。 +- 新增的 `scripts/verify-type-equiv.ts` 通过 TypeScript 解析器提取每个块,并断言其与声明的符号**逐字节匹配源码**——之所以选择这种方式而非编译式 `_Check` 可赋值性断言,正是因为我们需要的属性是字节相等,而非可赋值性。 +- 来源信息存放在集中的 `scripts/type-equiv.manifest.json`(`{ doc, symbol, source }` 条目)中,**而非**行文中的指令注释。脚本强制执行 **1:1 对应**:每个 type-equiv 块恰好有一条 manifest 条目,反之亦然;因此一个块永远不会被静默漏检,一条条目也永远不会腐烂。 +- 接入 `doc-sync`,因此与其他文档门禁在同一条 lefthook pre-push 和 CI 路径中运行。 ### 维护是作者的职责,门禁作为兜底 -`verify-type-equiv` 能捕获已记录类型的*粘贴漂移*,但无法告诉你一个全新的核心类型没有被记录。因此 AGENTS.md 和 `dsh-code-review` skill 已更新,要求在变更添加或重塑已记录类型时同步更新目录——门禁处理漂移,人处理新增表面。 +`verify-type-equiv` 能捕获已记录类型的*粘贴漂移*,但无法告诉你一个全新的核心类型没有被记录。因此 AGENTS.md 和 `dsh-code-review` skill(技能)已更新,要求在变更添加或重塑已记录类型时同步更新目录——门禁处理漂移,人处理新增表面。 ## 曾考虑的替代方案 -- **平铺转储所有跨包词汇**:`BashExecRequest` 测试案例否决了它。如果 seam 词汇算「核心」,目录对谁都没帮助;分层的主干与 seam 结构胜出。 -- **编译式 `_Check` 可赋值性断言**替代逐字源码匹配:否决,因为逐字节相等而非可赋值性才是我们需要的属性——一个改了名但类型相同的字段能通过可赋值性检查。 -- **来源信息作为行文中的指令注释**:否决,改用中央 manifest;其强制的 1:1 对应确保不会有块被静默漏检,也不会有条目腐烂。 +- **平铺罗列所有跨包词汇**:`BashExecRequest` 测试案例否决了它。如果 seam 词汇算"核心",目录对谁都没帮助;分层的主干与 seam 结构胜出。 +- **编译式 `_Check` 可赋值性断言**替代逐字节源码匹配:否决,因为我们需要的属性是字节相等而非可赋值性——一个类型相同但改了名的字段会通过可赋值性检查。 +- **来源信息作为行文中的指令注释**:否决,改用集中 manifest;其强制的 1:1 对应确保一个块永远不会被静默漏检,一条条目也永远不会腐烂。 ## 验证教训 主干与 seam 规则在采纳前经过了 `BashExecRequest`、工具 schema 与定义、schema DSL、展示类型以及 session/persistence 拆分的逐一测试。 -`verify-type-equiv` 必须扫描完整的 Markdown 范围,而不仅是 manifest 中列出的文档。否则未登记的 `type-equiv` 块会逃脱所声称的一对一检查。因此门禁将此类块报告为遗留块。本 RFC 将这条快速失败的扫描规则与主干/seam 分界和逐字匹配决策一并记录;生成式 Cordis 目录在[其 RFC](2026-06-20-generated-cordis-catalog.md) 中有对称的设计记录。 +`verify-type-equiv` 必须扫描完整的 Markdown 范围,而不仅仅是 manifest 中列出的文档。否则一个未登记的 `type-equiv` 块会逃脱所声称的一对一检查。因此门禁将此类块报告为遗留块。本 RFC 将这条快速失败的扫描规则与主干-seam 分界线和逐字节匹配决策一并记录;生成式 Cordis 目录在[其 RFC](2026-06-20-generated-cordis-catalog.md) 中有对称的设计记录。 ## 后果 -- 词汇现在有了一个**不会静默漂移**的唯一归属地:源码中的字段重命名会在 pre-push 钩子和 CI 中使 `verify-type-equiv` 失败,直到粘贴内容被刷新。 -- 主干与 seam 分界线是一个可复用的范围界定工具,而非一次性决策:同一条「你编写/持有/接收的东西是核心;为其提供类型推导/渲染/持久化的机制是细节」规则,后来也被用于界定事件/服务目录的 harness 层与继承层分层。 -- `ts type-equiv` 围栏是继 ` ```ts `(编译)和 ` ```ts ignore-check `(草稿)之后的第三种文档块类别。后续又新增了第四种 ` ```ts cordis-catalog `(生成签名),复用了相同的跳过并排除处理。 +- 词汇现在有了一个**不会静默漂移**的唯一归属:源码中的字段重命名会在 pre-push 钩子和 CI 中导致 `verify-type-equiv` 失败,直到粘贴内容被刷新。 +- 主干与 seam 分界线是一个可复用的范围界定工具,而非一次性的:同一条「你编写/持有/接收的东西是核心;为其提供类型推导/渲染/持久化的机制是细节」规则,后来也被用于界定事件/服务目录的 harness 层与继承层分层。 +- ` ```ts type-equiv ` 围栏是继 ` ```ts `(编译)和 ` ```ts ignore-check `(草稿)之后的第三种文档块类别。后续的姊妹门禁又增加了第四种 ` ```ts cordis-catalog `(生成签名),复用了相同的跳过并排除处理。 - 添加或重塑核心类型现在附带一项文档义务,作者必须履行(门禁无法检测缺失的*新*类型),由 `dsh-code-review` 检查清单兜底。 diff --git a/docs/rfc/implemented/process/2026-06-20-generated-cordis-catalog.i18n.yaml b/docs/rfc/implemented/process/2026-06-20-generated-cordis-catalog.i18n.yaml index ee4e1d24fc..9f7e982ff7 100644 --- a/docs/rfc/implemented/process/2026-06-20-generated-cordis-catalog.i18n.yaml +++ b/docs/rfc/implemented/process/2026-06-20-generated-cordis-catalog.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-20-generated-cordis-catalog.md: 6b451e31965f8f00210aa927ed236fed28699351 -2026-06-20-generated-cordis-catalog.zh.md: 9c7150ff5f12d4d9e4a13158acdf8f52e84fd95a +2026-06-20-generated-cordis-catalog.zh.md: 5550d07b5f5635d1da6496e35049c5725329e114 diff --git a/docs/rfc/implemented/process/2026-06-20-generated-cordis-catalog.zh.md b/docs/rfc/implemented/process/2026-06-20-generated-cordis-catalog.zh.md index 9c7150ff5f..5550d07b5f 100644 --- a/docs/rfc/implemented/process/2026-06-20-generated-cordis-catalog.zh.md +++ b/docs/rfc/implemented/process/2026-06-20-generated-cordis-catalog.zh.md @@ -1,4 +1,4 @@ -# RFC:生成式 Cordis 事件 + 服务目录 +# RFC:生成式 Cordis 事件与服务目录 Status: implemented @@ -6,36 +6,36 @@ Status: implemented ## 问题 -插件作者需要两个参考面,而此前没有任何单一文档能提供:他们可以监听的每一个 Cordis **事件**(含精确签名与分发模式),以及他们可以调用的每一个 `ctx.` **服务**(含精确接口)。相关信息已经存在,但散落各处:`docs/architecture.md` 中一张手工维护的事件分类*表格*(名称 + 行文描述的 Mode/Purpose,由 `verify-event-taxonomy` 做名称集合校验)、一张服务映射表(8 行角色描述),以及 `interface Events` / `interface Context` 声明本身。分类表还有一个盲区:它无法捕获全新的*未记录*事件——名称集合校验器只检查两侧已有的名称。 +插件作者需要两个参考面,而此前没有任何单一文档能提供:他们可以监听的每一个 Cordis **事件**(含精确签名与分发模式),以及他们可以调用的每一个 `ctx.` **服务**(含精确接口)。相关信息虽然存在,但散落各处:`docs/architecture.md` 中一张手工维护的事件分类*表格*(名称 + 行文描述的 Mode/Purpose,由 `verify-event-taxonomy` 做名称集合校验)、一张服务映射表(8 行角色描述),以及 `interface Events` / `interface Context` 声明本身。分类表格还有一个盲区:它无法捕获全新的*未记录*事件——名称集合校验器只检查两侧已有的名称。 -这是[核心数据结构目录](../../../core-data-structures/core.md)([对应 RFC](2026-06-20-core-data-structures-catalog.md))在连线轴上的互补件:那份目录记录 agent loop 流转的*数据结构*(经校验的手工粘贴);本目录记录流转它们的*事件与服务*。 +这是 [core-data-structures 目录](../../../core-data-structures/core.md)([其 RFC](2026-06-20-core-data-structures-catalog.md))在连线轴上的补充:后者编目的是 agent loop(智能体循环)流转的*数据结构*(经校验的手工粘贴);本 RFC 编目的是移动这些数据结构的*事件与服务*。 ## 决策 -从源码生成目录,而非手工维护表格再校验子集。 +从源码生成目录,取代手工维护表格并校验子集的方式。 -`scripts/gen-cordis-catalog.ts` 使用 TypeScript 编译器 API,从声明和源码 JSDoc 分别输出事件参考与服务参考。事件包含分发模式;服务包含公开签名。确定性的 `--write` 与 `--check` 模式使两个页面成为生成产物,新鲜度由 doc-sync 强制。 +`scripts/gen-cordis-catalog.ts` 使用 TypeScript 编译器 API,从声明和源码 JSDoc 分别输出事件参考与服务参考。事件包含分发模式;服务包含公开签名。确定性的 `--write` 和 `--check` 模式使两个页面成为生成产物,新鲜度由 `doc-sync`(文档同步门禁)强制保障。 -纯生成在这里是正确的,因为代码库足够规范,AST 即全部真相:每个事件/服务名称都是字符串字面量,能往返映射到一个静态声明——没有动态命名的事件,也没有仅运行时存在的服务。因此生成的文档不可能出错,并且从结构上消除了未记录事件的缺口(生成器枚举源码,而非检查手写子集)。 +纯生成在此处是正确的,因为代码库足够规范,AST 就是全部事实:每个事件/服务名称都是字符串字面量,可以往返映射到静态声明——不存在动态命名的事件,也不存在仅运行时的服务。因此生成的文档不可能出错,且从结构上消除了未记录事件的缺口(生成器枚举源码,而非校验手写子集)。 具体选择: -- **`@mode` 标签,交叉校验。** 每个 harness 事件的 JSDoc 携带显式的 `@mode emit|waterfall|parallel|serial` 标签;缺少标签时生成器直接报错。当签名形状具有结论性时——尾部参数为 `next: () => …` 在结构上即为 waterfall——生成器断言标签与之一致,矛盾时直接报错。emit/parallel/serial 的区分在结构上不可见(`session/flush` 返回 `Promise | void` 且无 `next`,有序的 `agent/pre-step` 检查点亦然),因此信任标签。撰写规则见 [AGENTS.md](../../../../AGENTS.md)。 -- **分层范围。** harness 层(8 个 `@deepseek-ai/dsh-*` 服务及其事件)从源码完整渲染。继承层(cordis-core 的 `ctx.on/emit/effect/provide/…` + `internal/*` 事件 + loader/HMR/timer)是插件同样可见的固定 vendor 源;它以精简形式渲染(名称 + 一行说明 + 源码指针),数据来自生成器中的一张手工策展表,而**不是**遍历 vendor AST——cordis-core 的 `Context` 混合了真正的 ctx 成员与非服务字段(`root`、`baseUrl`、`logger`),且 vendor 表面仅在有意的 vendor 同步时才变化。 -- **交叉链接到数据结构目录。** 签名中出现的类型名(`GenerateOptions`、`StreamChunk`、`ToolDefinition` 等)链接到记录该类型的核心数据结构页面。映射是生成器中一个小型手工策展的 const,而**不是** `type-equiv.manifest.json`——后者记录的是 `…Map` 符号,而签名引用的是派生联合类型名,且有少数符号出现在两个页面上。 -- **专用围栏。** 签名块使用 ` ```ts cordis-catalog ` 信息字符串,`doc-typecheck` 识别并跳过它(裸签名片段不能独立编译),不计入 opt-out 比例——与 `type-equiv` 块的处理方式相同。 +- **`@mode` 标签,交叉校验。** 每个 harness 事件的 JSDoc 携带一个显式的 `@mode emit|waterfall|parallel|serial` 标签;缺少标签时生成器直接报错。当签名形状具有决定性时——尾部参数为 `next: () => …` 在结构上即为 waterfall(瀑布式事件)——生成器断言标签与之一致,矛盾时直接报错。emit/parallel/serial 的区别在结构上不可见(`session/flush` 返回 `Promise | void` 且无 `next`,有序的 `agent/pre-step` 检查点亦然),因此信任标签。编写规则见 [AGENTS.md](../../../../AGENTS.md)。 +- **分层范围。** harness 层(8 个 `@deepseek-ai/dsh-*` 服务及其事件)从源码完整渲染。继承层(cordis-core 的 `ctx.on/emit/effect/provide/…` + `internal/*` 事件 + loader/hmr/timer)是插件同样可见的固定 vendor 源码;它从生成器中一张人工维护的表格简洁渲染(名称 + 一行描述 + 源码指针),而**非**遍历 vendor AST。原因是 cordis-core 的 `Context` 混合了真正的 ctx 成员与非服务字段(`root`、`baseUrl`、`logger`),且 vendor 接口面仅在有意的 vendor 同步时才变化。 +- **交叉链接到数据结构目录。** 签名中的类型名(`GenerateOptions`、`StreamChunk`、`ToolDefinition` 等)链接到记录该类型的 core-data-structures 页面。映射是生成器中一个小型的人工维护常量,而**非** `type-equiv.manifest.json`——后者记录的是 `…Map` 符号,而签名引用的是派生联合类型名,且有少数符号出现在两个页面上。 +- **专用围栏。** 签名块使用 ` ```ts cordis-catalog ` 信息字符串,`doc-typecheck` 识别后跳过(裸签名片段不能独立编译),并排除在 opt-out 比例之外——与 `type-equiv` 块获得相同待遇。 -本决策**取代** [doc-sync 强制](2026-06-11-doc-sync-enforcement.md)中的事件分类部分:`verify-event-taxonomy` 及其 `docs/architecture.md` 表格退役(architecture.md 的标题保留,正文改为指向目录;服务映射角色表作为策展行文保留)。doc-typecheck、verify-md-wrap、verify-md-links 与 verify-type-equiv 不受影响。 +本决策**取代** [doc-sync 强制](2026-06-11-doc-sync-enforcement.md)中事件分类的那一半:`verify-event-taxonomy` 及其 `docs/architecture.md` 表格退役(architecture.md 的标题保留,正文改为指向目录;服务映射的角色表格作为人工行文保留)。doc-typecheck、verify-md-wrap、verify-md-links 和 verify-type-equiv 不受影响。 ## 曾考虑的替代方案 -- **校验而非生成(退役的分类检查所做的事)**:*仅对此表面*反转了方向。这里的数据可以机械地完整获取,因此生成严格强于名称集合校验(完整签名、不会漂移、能捕获未记录事件)。 -- **遍历 vendor AST 以获取继承层**:否决,改用策展表。cordis-core 的 `Context` 混合了真正的 ctx 成员与非服务字段,且固定的 vendor 表面仅在有意同步时才变化。 -- **复用 `type-equiv.manifest.json` 作为签名交叉链接映射**:否决,改用小型手工策展 const。manifest 记录的是 `…Map` 符号,而签名引用的是派生联合类型名,且有少数符号出现在两个页面上。 +- **校验而非生成(退役的分类检查所做的事)**:*仅对本参考面*反转了这一策略。此处的数据可以机械地完整获取,因此生成严格强于对手工表格做名称集合校验(完整签名、不会漂移、能捕获未记录事件)。 +- **遍历 vendor AST 以获取继承层**:否决,改用人工维护表格。cordis-core 的 `Context` 混合了真正的 ctx 成员与非服务字段,且固定的 vendor 接口面仅在有意同步时才变化。 +- **复用 `type-equiv.manifest.json` 作为签名交叉链接映射**:否决,改用小型人工维护常量。manifest 记录的是 `…Map` 符号,而签名引用的是派生联合类型名,且有少数符号出现在两个页面上。 ## 后果 -- 目录不会漂移:源码变化而已提交文件未反映时,`verify-cordis-catalog` 在 pre-push 钩子和 CI 中失败。新事件缺少 `@mode` 标签、或标签与签名矛盾时,生成器直接报错。 -- 事件的行文描述现在只有一个归属地——声明处的 JSDoc。JSDoc 写得薄,目录条目就薄,这迫使作者在源头做好文档(生成器是 AGENTS.md「每个导出都有语义 JSDoc」规则的强制函数)。 -- 继承层是手工摘要的,因此 vendor 同步若增加或重命名了 cordis-core 事件或 `ctx` 成员,需要同步编辑 `gen-cordis-catalog.ts` 中的策展表。这是不遍历固定 vendor 源的有意代价;变化很少,且在生成器中有明确标注。 -- `verify-event-taxonomy.ts` 被删除,`docs/architecture.md` 的事件表格消失;之前链接到特定表格行的人现在会落到生成目录上。 +- 目录不会漂移:源码变更而已提交文件未反映时,`verify-cordis-catalog` 在 pre-push 钩子和 CI 中失败。新事件缺少 `@mode` 标签,或标签与签名矛盾,生成器直接报错。 +- 事件的行文描述现在有了唯一归属地——声明处的 JSDoc。JSDoc 写得单薄,目录条目就单薄,这迫使作者在源码处做好文档(生成器是 AGENTS.md「每个导出都有语义 JSDoc」规则的强制函数)。 +- 继承层是手工摘要,因此 vendor 同步若新增或重命名了 cordis-core 事件或 `ctx` 成员,需要同步编辑 `gen-cordis-catalog.ts` 中的人工维护表格。这是不遍历固定 vendor 源码的有意代价;它很少变化,且在生成器中有明确标注。 +- `verify-event-taxonomy.ts` 被删除,`docs/architecture.md` 的事件表格也已移除;之前链接到特定表格行的人现在会落在生成目录上。 diff --git a/docs/rfc/implemented/process/2026-06-20-rfc-classification.i18n.yaml b/docs/rfc/implemented/process/2026-06-20-rfc-classification.i18n.yaml index 97b96e7315..cc4642657c 100644 --- a/docs/rfc/implemented/process/2026-06-20-rfc-classification.i18n.yaml +++ b/docs/rfc/implemented/process/2026-06-20-rfc-classification.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-20-rfc-classification.md: 201852a209be7f40b05de45d148a36b9185767a3 -2026-06-20-rfc-classification.zh.md: 38eb920ac216e69087f0c84c95cdd9effca7b9b3 +2026-06-20-rfc-classification.zh.md: 554ac8014719c99ed447a33ff843c40fde761eed diff --git a/docs/rfc/implemented/process/2026-06-20-rfc-classification.zh.md b/docs/rfc/implemented/process/2026-06-20-rfc-classification.zh.md index 38eb920ac2..554ac80147 100644 --- a/docs/rfc/implemented/process/2026-06-20-rfc-classification.zh.md +++ b/docs/rfc/implemented/process/2026-06-20-rfc-classification.zh.md @@ -1,48 +1,48 @@ # RFC:通过路径编码的子目录对 RFC 进行分类 -Status: implemented - [English](2026-06-20-rfc-classification.md) | 中文 +Status: implemented + ## 问题 -`docs/rfc/` 此前仅按**生命周期**分组:`proposed/`/`implemented/`/`rejected/`。没有任何机制记录每篇 RFC 属于哪一*类*决策。索引是每个生命周期下的一个扁平列表,无法按需筛选「所有精简类」或「所有测试策略类」决策。同一天落地的一批精简类 RFC 让这个缺口变得具体:浏览 `proposed/` 的读者无法在不逐一打开文件的情况下区分新能力、移除和工具策略变更。 +`docs/rfc/` 过去仅按**生命周期**分组 RFC:`proposed/`/`implemented/`/`rejected/`。没有任何机制记录每个 RFC 属于哪一*类*决策。索引在每个生命周期下只是一个扁平列表,无法按需筛选「所有简化类」或「所有测试策略类」决策。一批简化类 RFC 在同一天落地后,这个缺口变得具体:浏览 `proposed/` 的读者无法在不逐一打开文件的情况下区分新能力、移除和工具策略变更。 -本仓库的一贯倾向是[机械质量门禁优先于行文指南](2026-06-11-quality-gates.md):不被机器检查的约定终将腐烂。因此这里的分类体系必须可强制执行,而非靠自觉的文件头。 +本仓库一贯的倾向是[机械质量门禁优于行文规范](2026-06-11-quality-gates.md):不被机器检查的约定终将腐烂。因此这里的分类方案必须可强制执行,而非靠自觉的文件头。 ## 决策 -增加第二个维度——RFC 的**类别**——并将其编码在路径中:`{lifecycle}/{class}/yyyy-mm-dd-topic.md`。文件夹*就是*标签。文件的位置声明其类别,封闭集合是「这些文件夹且仅限这些」,而既有的 [verify-md-links](2026-06-18-markdown-cross-link-lint.md) 门禁已经保护了移动文件所需的路径重写。 +增加第二个维度——RFC 的**类别**——并将其编码在路径中:`{lifecycle}/{class}/yyyy-mm-dd-topic.md`。文件夹本身就是标签。文件的位置声明其类别,封闭集合是「这些文件夹且仅限这些」,而既有的 [verify-md-links](2026-06-18-markdown-cross-link-lint.md) 门禁已经保护了移动文件所需的路径重写。 ### 六个类别的封闭集合 -| 类别 | 覆盖范围 | +| 类别 | 涵盖范围 | |---|---| | `feature` | 面向用户或模型的新能力。 | -| `bug-fix` | 修正缺陷或弥补事后复盘暴露的缺口。 | -| `simplification` | 移除代码、行为或接口面,不增加新能力。 | -| `architecture` | 关于**交付源码**的结构性决策:包之间的关系、运行时词汇是什么。 | -| `process` | 围绕代码的工具、策略或工作流,不涉及运行时行为。 | +| `bug-fix` | 修正缺陷或填补事后复盘暴露的空白。 | +| `simplification` | 移除代码、行为或对外表面积,不引入新能力。 | +| `architecture` | 关于**交付源码**的结构性决策——包(package)之间的关系、运行时词汇。 | +| `process` | 围绕代码的工具、策略或工作流,而非运行时行为。 | | `testing` | 测试基础设施与策略。 | -`architecture` 与 `process` 的分界线:**architecture** 关乎我们交付的源码;**process** 关乎围绕源码的工具与工作流。本 RFC 本身是一项 `process` 决策——它改变的是仓库的组织方式和门禁,而非 harness 在运行时的行为——因此它位于 `implemented/process/` 下。 +`architecture` 与 `process` 的分界线:**architecture** 关乎我们交付的源码;**process** 关乎围绕源码的工具与工作流。本 RFC 本身是一个 `process` 决策——它改变的是仓库的组织方式和门禁,而非 harness 的运行时行为——因此它位于 `implemented/process/` 下。 ### 两道门禁 -两者都是 `doc-sync` 的成员,风格与 `verify-md-wrap` 一致(tsx ESM,只校验不生成,首个违规即以非零退出码退出): +两者都是 `doc-sync`(文档同步门禁)的成员,风格与 `verify-md-wrap` 一致(tsx ESM,只校验不生成,首个违规即以非零退出码退出): -- **`scripts/verify-rfc-classification.ts`**:封闭集合与索引新鲜度(freshness)。它断言每个生命周期文件夹下的文件都位于规范集合中的某个类别文件夹内(直接放在生命周期根目录的 `.md`,或未知的类别文件夹,都会失败),并断言生成的 [INDEX.md](../../INDEX.md) 与从目录树重新渲染的结果逐字节一致(见[生成 RFC 索引表](2026-07-04-generate-rfc-index-tables.md))。规范类别集合以 `const` 形式定义在 `scripts/rfc-index.ts` 中——这是与生成器共享的机器真源——[README](../../README.md) 以行文形式记录它;类别*描述*保持手写,索引则是生成的。 -- **`scripts/verify-doc-refs.ts`**:源码注释中的文档引用。RFC 路径不仅在 Markdown 中被引用,也出现在 TypeScript 文档注释中(根相对路径,如 `docs/rfc/implemented/testing/2026-06-19-acp-snapshot-tests.md`)。`verify-md-links` 从未扫描过这些引用,因此重组可能会悄悄使它们变成悬空引用。此门禁扫描 `packages/**` 和 `examples/**` 下仓库自有的 `.ts` 文件(排除构建产物 `lib/` 和 `vendor/`),查找 `docs/….md` 形式的 token,将每个根相对路径解析并断言其存在。它要求 `.md` 扩展名,因此无扩展名的行文引用(`docs/postmortem/0001`、`docs/architecture.md § Extending The Harness`)不受影响。 +- **`scripts/verify-rfc-classification.ts`**——封闭集合与索引新鲜度。它断言生命周期文件夹下的每个文件都位于规范集合中的某个类别文件夹内(生命周期根目录下的散落 `.md` 或未知类别文件夹均判定失败),并断言生成的 [INDEX.md](../../INDEX.md) 与从目录树重新渲染的结果逐字节一致(见[生成 RFC 索引表](2026-07-04-generate-rfc-index-tables.md))。规范类别集合以 `const` 形式定义在 `scripts/rfc-index.ts` 中——这是与生成器共享的机器真源——而 [README](../../README.md) 以行文形式记录它;类别*描述*保持手写,索引由机器生成。 +- **`scripts/verify-doc-refs.ts`**——源码注释中的文档引用。RFC 路径不仅被 Markdown 引用,也被 TypeScript 文档注释引用(以仓库根为起点的路径,如 `docs/rfc/implemented/testing/2026-06-19-acp-snapshot-tests.md`)。`verify-md-links` 从未扫描过这些引用,因此重组可能使它们静默失效。此门禁扫描 `packages/**` 和 `examples/**` 下仓库自有的 `.ts` 文件(排除构建产物 `lib/` 和 `vendor/`),查找 `docs/….md` 形式的 token,将每个以仓库根为起点的路径解析并断言其存在。它要求 `.md` 扩展名,因此无扩展名的行文引用(`docs/postmortem/0001`、`docs/architecture.md § Extending The Harness`)不受影响。 ## 曾考虑的替代方案 -- **在每个文件中加一行 `Classification:` 行文**(紧挨 `Status:`),由门禁解析。可行,但它把路径已经能承载的事实重复到了文件内,而且这一行可能与所在文件夹不一致。路径编码让标签与其存储合二为一——没有需要保持同步的东西。 -- **设立 `refactor` 类别。**它与 `simplification` 几乎完全重叠;唯一有人试图用来区分的标准是「可观测行为是否改变?」,而 `simplification` 已经编码了这一点(它不改变)。一个类别,不要两个。 -- **从文件系统自动生成索引。**此处最初否决,以保持索引手写;后来被[生成 RFC 索引表](2026-07-04-generate-rfc-index-tables.md)取代——当堆叠的提案波使手写表格成为仓库中冲突最频繁的文档区域后,列表改为完全生成的 [INDEX.md](../../INDEX.md),而 README 行文保持人工策展。 +- **在每个文件中添加 `Classification:` 行文行**(紧邻 `Status:`),由门禁解析。可行,但它将路径已能承载的事实重复到文件中,且行内容可能与所在文件夹不一致。路径编码使标签与其存储合二为一,没有需要保持同步的东西。 +- **设立 `refactor` 类别。** 与 `simplification` 几乎完全重叠;唯一有人试图用来区分的标准是「可观察行为是否改变?」,而 `simplification` 已经编码了这一点(它不改变)。一个类别即可,无需两个。 +- **从文件系统自动生成索引。** 此处最初否决,以保持索引手写;后被[生成 RFC 索引表](2026-07-04-generate-rfc-index-tables.md)取代——当堆叠的提案潮使手写表格成为仓库中冲突最频繁的文档区域后,列表改为完全生成的 [INDEX.md](../../INDEX.md),而 README 行文保持人工维护。 ## 后果 -- 每篇 RFC 现在都位于一个类别文件夹下,索引在每个生命周期内按类别分组。读者扫一个标题就能看到所有精简类或所有测试类决策。 -- `doc-sync` 链中多了两个快速 tsx 脚本;无新增依赖(mdast/GFM 栈已因 `verify-md-wrap`/`verify-md-links` 而存在)。 -- 新增类别是一个刻意的动作:修改 `scripts/rfc-index.ts` 中的 `const` 以及 [Classification 章节](../../README.md#classification),而不是仅仅 `mkdir` 一个文件夹。门禁会拒绝未知文件夹,因此临时类别无法悄悄混入。 -- 源码注释中的文档引用现在也受门禁保护:一个被移动或重命名的文档如果被 `.ts` 注释引用,pre-push 钩子就会失败,从而封堵了 `verify-md-links` 在结构上无法看到的一类漂移。 +- 每个 RFC 现在都位于一个类别文件夹下,索引在每个生命周期内按类别分组。读者只需扫一个标题即可看到所有简化类或所有测试类决策。 +- `doc-sync` 链中多了两个快速 tsx 脚本;无新依赖(mdast/GFM 栈已因 `verify-md-wrap`/`verify-md-links` 而存在)。 +- 新增类别是一个刻意的动作:修改 `scripts/rfc-index.ts` 中的 `const` 和 [Classification 章节](../../README.md#classification),而非仅仅 `mkdir` 一个文件夹。门禁拒绝未知文件夹,因此临时类别无法悄悄混入。 +- 源码注释中的文档引用现在也受门禁保护——一个被移动或重命名的文档如果被 `.ts` 注释引用,pre-push 钩子就会失败,堵住了 `verify-md-links` 在结构上无法看到的一类漂移。 diff --git a/docs/rfc/implemented/process/2026-07-02-tool-schema-catalog.i18n.yaml b/docs/rfc/implemented/process/2026-07-02-tool-schema-catalog.i18n.yaml index 852f69db42..8ecca3648d 100644 --- a/docs/rfc/implemented/process/2026-07-02-tool-schema-catalog.i18n.yaml +++ b/docs/rfc/implemented/process/2026-07-02-tool-schema-catalog.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-02-tool-schema-catalog.md: 9a99fafd36b3546be4f51a7cd9e9a47fdaaf4c2d -2026-07-02-tool-schema-catalog.zh.md: 373c681fa5870696645b138f12cad7f296434184 +2026-07-02-tool-schema-catalog.zh.md: 5860d1617ce6592c8665e4cb59304b0f6d35c99a diff --git a/docs/rfc/implemented/process/2026-07-02-tool-schema-catalog.zh.md b/docs/rfc/implemented/process/2026-07-02-tool-schema-catalog.zh.md index 373c681fa5..5860d1617c 100644 --- a/docs/rfc/implemented/process/2026-07-02-tool-schema-catalog.zh.md +++ b/docs/rfc/implemented/process/2026-07-02-tool-schema-catalog.zh.md @@ -1,55 +1,55 @@ # RFC:生成式工具 schema 目录(启动并采集) -Status: implemented - [English](2026-07-02-tool-schema-catalog.md) | 中文 +Status: implemented + ## 问题 -仓库此前没有一份统一的参考,列出实际暴露给模型的工具名称、描述与 JSON Schema。源码声明分散各处且在运行时组合,而既有的 Cordis 目录和数据结构目录覆盖的是接线与词汇,而非工具本身。 +仓库此前没有一份统一的参考文档来记录实际暴露给模型的工具名称、描述与 JSON Schema。源码声明分散各处且在运行时组合,而既有的 Cordis 目录和数据结构目录覆盖的是接线与词汇,而非工具。 ## 决策 -通过**启动每个工具插件并读取其注册的 schema** 来生成目录,而非解析源码。`scripts/gen-tool-catalog.ts` 将每个已发布的工具包(package)挂载到一个全新的 Cordis `Context` 上(带 `SystemPrompt` + `ToolRegistry` 以及插件 `apply` 所读取的注入 seam),调用 `ctx.tools.schemas()`(即发送给模型的 `ToolSchema[]`),dispose 上下文,然后为每个包渲染一个 `## ` 小节,每个工具对应一个 ` ```json ` 的 `parameters` 块。它沿用 `gen-cordis-catalog` / `gen-module-graph` 的 CLI 形态:默认 `--write` 重新生成,`--check` 在已提交副本陈旧时失败,输出是确定性的(按 manifest 排序,工具按名称排序)。`verify-tool-catalog`(即 `--check`)运行在 doc-sync 内部,因此新鲜度门禁与其他文档门禁一样在 lefthook pre-push 和 CI 路径中触发。 +通过**启动每个工具插件并读取其注册的 schema** 来生成目录,而非解析源码。`scripts/gen-tool-catalog.ts` 将每个已发布的工具包(package)挂载到一个新的 Cordis `Context`(带 `SystemPrompt` + `ToolRegistry` 以及插件 `apply` 所读取的注入 seam),调用 `ctx.tools.schemas()`(即发送给模型的 `ToolSchema[]`),dispose(资源释放)该 context,然后为每个包渲染一个 `## ` 小节,每个工具一个 ` ```json ` 的 `parameters` 块。它与 `gen-cordis-catalog` / `gen-module-graph` 的 CLI(命令行界面)形态一致:默认 `--write` 重新生成,`--check` 在已提交副本陈旧时失败,输出是确定性的(按 manifest(元数据清单)排序,工具按名称排序)。`verify-tool-catalog`(即 `--check`)在 doc-sync(文档同步门禁)内运行,因此新鲜度门禁在 lefthook pre-push 和 CI 路径中与其他文档门禁一同触发。 -### 为什么启动而非解析(核心论点) +### 为何启动而非解析(核心要点) -Cordis 目录是纯 TypeScript AST 遍历,因为每个事件/服务名都是字符串字面量,能往返映射到静态声明——AST 就是全部事实。**工具 schema 不是静态可知的**,因此同样的技术会产出一份说谎的文档: +Cordis 目录是纯 TypeScript AST 遍历,因为每个事件/服务名都是字符串字面量,可以往返映射到静态声明——AST 即全部事实。**工具 schema 在静态层面不可知**,因此同样的技术会产出一份说谎的文档: - `tool-todo` 写了 `enum: [...STATUSES]`——对一个运行时 `const` 的展开。AST 看到的是展开表达式,而非 `["pending","in_progress","completed"]`。 -- 每段 description 都由字符串**拼接**构建(`'…' + '…'`)。AST 看到的是拼接节点,而非模型实际读到的最终文本。 -- `tool-subagent` 的工具名是 `config.toolName ?? 'subagent'`——加载时选定,不是字面量。 -- MCP 插件可以通过 `ctx.tools.register()` 直接注册**原始 JSON Schema**,完全不经过 `defineTool`,因此结构化枚举 `defineTool(` 调用点会漏计。 +- 每条 description 都通过字符串**拼接**构建(`'…' + '…'`)。AST 看到的是拼接节点,而非模型实际读到的最终文本。 +- `tool-subagent` 的工具名是 `config.toolName ?? 'subagent'`——加载时选定,并非字面量。 +- MCP 插件可以通过 `ctx.tools.register()` 直接注册**原始 JSON Schema**,完全不经过 `defineTool`,因此结构化枚举 `defineTool(` 调用点会遗漏。 -唯一忠实的真源是插件加载后注册表实际持有的 schema。启动即是[测试策略](../../../testing.md)中「验证世界,而非自我报告」这一原则在文档生成器上的应用:读取已发布的产物,而非对它的重新推导。 +唯一忠实的真源是插件加载后注册表实际持有的 schema。启动是[测试策略](../../../testing.md)中「验证世界,而非自我报告」这一原则在文档生成器上的应用:读取已发布的产物,而非对它的再推导。 -### 恢复「不会静默遗漏」 +### 恢复「不会静默遗漏」的保证 -启动有一项 AST 遍历不具备的代价:没有源码声明集合可供枚举,因此新增的工具包可能被遗忘。一道**完整性守卫**恢复了这一保证——`assertManifestComplete` 对 `packages/` 下所有 `tool-*` 包做 glob,若有任何一个不在生成器的启动 manifest 中则硬报错。新增工具包会导致生成器失败,进而导致 doc-sync 失败,直到该包被注册。这与 Cordis 生成器通过枚举源码免费获得的结构性保证相同,只是为启动式生成器重新实现了一遍。 +启动有一项 AST 遍历不存在的代价:没有源码声明集合可供枚举,新工具包可能被遗忘。一个**完整性守卫**恢复了这项保证——`assertManifestComplete` 对 `packages/` 下所有 `tool-*` 包进行 glob,若有任何一个不在生成器的启动 manifest 中则直接报错。新工具包在注册之前会导致生成器失败,进而导致 doc-sync 失败。这与 Cordis 生成器通过枚举源码免费获得的结构性属性相同,只是为基于启动的生成器重新实现了一遍。 -### 手工维护的启动 manifest 是不可约减的策略 +### 手动维护的启动 manifest 是不可化约的策略 -文件系统负责发现工具包清单,完整性守卫负责拒绝遗漏。`TOOL_PACKAGES` 仍然为每个包持有一份显式的启动配方,因为所需的 seam 实现和配置是**策略**,不是能从目录布局或注入名称安全推断的事实。 +文件系统负责发现工具包清单,完整性守卫负责拒绝遗漏。`TOOL_PACKAGES` 仍然为每个包持有一份显式的启动配方,因为所需的 seam 实现和配置属于策略,不是能从目录布局或注入名称安全推断的事实。 ### 范围 -`packages/*/tool-*` 下已发布的产品级工具包,各以默认配置启动:`dsh-tool-bash`(`bash`、`bash_output`、`bash_kill`)、`dsh-tool-todo`(`todo_write`)、`dsh-tool-subagent`(`subagent`)。`examples/` 下的演示工具(`echo`)被排除,与 Cordis 目录的 packages-only 范围一致——演示工具不属于读者所要查阅的产品接口。 +`packages/*/tool-*` 下已发布的产品工具包,每个以默认配置启动:`dsh-tool-bash`(`bash`、`bash_output`、`bash_kill`)、`dsh-tool-todo`(`todo_write`)、`dsh-tool-subagent`(`subagent`)。`examples/` 下的演示工具(`echo`)被排除,与 Cordis 目录仅覆盖 packages 的范围一致——演示工具不属于读者所查阅的产品接口。 -目录的单位是包,而非每个已配置的工具实例。每个包以默认配置启动一次;加载时的别名(如 `subagent_fork`)会注明,但不枚举每种部署排列。部署清单是一个独立的、无界的接口。 +目录的单位是包,而非每个配置化的工具实例。每个包以默认配置启动一次;加载时的别名(如 `subagent_fork`)会注明,但不枚举所有部署排列。部署清单是一个独立的、无界的接口。 ### 使用普通 `json` 围栏 -schema 块使用 ` ```json `,而非自定义的 `ts` 系围栏。`doc-typecheck` 只提取 `ts*` 围栏,因此 JSON 块对它不可见——无需 `BlockKind` 接线(不同于 Cordis 目录的 `ts cordis-catalog` 围栏,后者必须加入白名单以避免裸签名片段被编译)。 +schema 块使用 ` ```json `,而非自定义的 `ts` 系围栏。`doc-typecheck` 只提取 `ts*` 围栏,因此 JSON 块对它不可见——无需 `BlockKind` 接线(不同于 Cordis 目录的 `ts cordis-catalog` 围栏,后者需要加入白名单以避免裸签名片段被编译)。 ## 曾考虑的替代方案 -- **纯 TypeScript AST 遍历,如 Cordis 目录**:工具 schema 不是静态可知的(见上文核心论点):运行时展开、字符串拼接、配置选定的名称,以及原始 `ctx.tools.register()` 注册,都会让 AST 推导出的文档说谎。 -- **从各包的 inject 推断启动配方**:[发现包清单提案](../../proposed/process/2026-06-20-discover-package-inventory.md)所警告的「过于聪明」的路径;配方保持手写策略,清单由文件系统发现并受完整性守卫保护。 +- **纯 TypeScript AST 遍历,如 Cordis 目录**:工具 schema 在静态层面不可知(见上文核心要点):运行时展开、字符串拼接、配置选定的名称,以及原始 `ctx.tools.register()` 注册,都会让 AST 推导出的文档说谎。 +- **从各包的 inject 推断启动配方**:属于[发现包清单提案](../../proposed/process/2026-06-20-discover-package-inventory.md)所警告的「过度聪明」路径;配方保持为手写策略,清单由文件系统发现并由完整性守卫把关。 - **为 schema 块使用自定义 `ts` 系围栏**:不必要。普通 ` ```json ` 围栏对 `doc-typecheck` 不可见,无需 `BlockKind` 白名单。 ## 后果 -- 目录不会漂移:工具 schema 变更而已提交文件未反映时,`verify-tool-catalog` 在 pre-push 钩子和 CI 中失败。新增 `tool-*` 包未加入 manifest 时,完整性守卫直接报错。 -- 工具描述文本只有一个归属地——源码中 `defineTool` 的 `description`——生成的条目质量完全取决于它,与 Cordis 目录对事件 JSDoc 施加的推动力相同。 -- 生成器导入并执行工作区包(这是仓库中第一个这样做的脚本;其他脚本只读取文本)。它通过根 `tsconfig` 的 `paths` 映射在 `tsx` 下运行,走的是演示和测试所用的同一条未构建源码路径,因此不需要构建步骤。 -- 未来某个工具背后新增能力 seam 时,意味着 manifest 中新增一条配方条目(需要挂载哪些 seam)。这是上文明确指出的手写代价;仅在新增工具包时才需变更。 +- 目录不会漂移:工具 schema 变更而已提交文件未反映,`verify-tool-catalog` 会在 pre-push 钩子和 CI 中失败。新 `tool-*` 包未加入 manifest 则完整性守卫直接报错。 +- 工具描述文本有唯一归属——源码中 `defineTool` 的 `description`——生成的条目质量取决于它,与 Cordis 目录对事件 JSDoc 施加的强制力相同。 +- 生成器导入并执行工作区包(这是仓库中第一个这样做的脚本;其他脚本只读文本)。它通过根 `tsconfig` 的 `paths` 映射在 `tsx` 下运行,使用与演示和测试相同的未构建源码路径,因此不需要构建步骤。 +- 未来某个工具背后新增一个能力 seam,意味着 manifest 中需要新增一条配方条目(声明要挂载哪些 seam)。这正是上文指出的有意为之的手写成本;仅在新增工具包时才需变更。 diff --git a/docs/rfc/implemented/process/2026-07-03-documentation-graph-atlas.i18n.yaml b/docs/rfc/implemented/process/2026-07-03-documentation-graph-atlas.i18n.yaml index 7b522b590b..6b19ed7740 100644 --- a/docs/rfc/implemented/process/2026-07-03-documentation-graph-atlas.i18n.yaml +++ b/docs/rfc/implemented/process/2026-07-03-documentation-graph-atlas.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-03-documentation-graph-atlas.md: d10b57e5114684ab0a2caed66fa84b84959bdf12 -2026-07-03-documentation-graph-atlas.zh.md: 8473bc27994bb33eac61659196acc53b69a16449 +2026-07-03-documentation-graph-atlas.zh.md: 6edcfbdbbc3af5808e60888acad04919ce681396 diff --git a/docs/rfc/implemented/process/2026-07-03-documentation-graph-atlas.zh.md b/docs/rfc/implemented/process/2026-07-03-documentation-graph-atlas.zh.md index 8473bc2799..6edcfbdbbc 100644 --- a/docs/rfc/implemented/process/2026-07-03-documentation-graph-atlas.zh.md +++ b/docs/rfc/implemented/process/2026-07-03-documentation-graph-atlas.zh.md @@ -1,69 +1,69 @@ -# RFC:面向维护者和 SDK 用户的文档关系图索引 - -Status: implemented +# RFC:面向维护者与 SDK 用户的文档关系图索引 [English](2026-07-03-documentation-graph-atlas.md) | 中文 +Status: implemented + ## 问题 -仓库此前已有若干高可信度的文档面,各自覆盖不同维度:[module-graph.md](../../../module-graph.md) 由 package 的 `peerDependencies` 生成;生成的 [Cordis 事件目录](../../../cordis-catalog/events.md)和[服务目录](../../../cordis-catalog/services.md)由 Cordis 的 `Events` 与 `Context` 声明生成;[tool-catalog.md](../../../tool-catalog.md) 通过启动已发布的工具插件生成;[core-data-structures/](../../../core-data-structures/core.md) 使用 `ts type-equiv` 块保持粘贴的类型定义与源码同步。 +仓库已有若干高可信度的文档面,各自覆盖不同维度:[module-graph.md](../../../module-graph.md) 由包(package)的 `peerDependencies` 生成;生成的 [Cordis events](../../../cordis-catalog/events.md) 与 [services](../../../cordis-catalog/services.md) 目录由 Cordis 的 `Events` 和 `Context` 声明生成;[tool-catalog.md](../../../tool-catalog.md) 通过启动已发布的 tool 插件生成;[core-data-structures/](../../../core-data-structures/core.md) 使用 `ts type-equiv` 块保持粘贴的类型定义与源码同步。 -这些参考资料是准确的,但它们大多是目录。维护者仍需自行综合关系:哪些 package 构成一条能力 seam、哪个应用捆绑了具体的主干、哪个事件是持久的而哪个是实时的、钩子或策略插件可以在哪里拦截工作、以及哪个面向模型的工具依赖哪个服务。SDK 用户从另一个角度面临同样的问题:「我想要某种行为,该安装或加载哪个 package?该扩展哪个事件/服务/工具?」 +这些参考文档是准确的,但大多是目录式的。维护者仍需自行综合关系:哪些包构成一个能力 seam、哪个应用组装了具体的主干、哪些事件是持久的而哪些是实时的、钩子或策略插件在哪里可以拦截工作、以及哪个面向模型的工具依赖哪个服务。SDK 用户从另一个角度面临同样的问题:「我想要某种行为,应该安装或加载哪个包?应该扩展哪个事件/服务/工具?」 -钩子子系统使事件的生产者/消费者拓扑与拦截点变得更加重要;文件系统 seam 使能力 seam、策略否决、工具呈现和 SDK 组装路径变得更加重要。如果关系图仅限于一个小的 bash/todo/subagent 表面,它们会立刻陈旧。 +钩子子系统使事件的生产者/消费者拓扑与拦截点变得更加重要;文件系统 seam 使能力 seam、策略否决、工具呈现与 SDK 组装路径变得更加重要。如果关系图的范围仅限于一个小的 bash/todo/subagent 表面,它们会立即陈旧。 ## 决策 -新增生成的关系图文档,索引位于 [docs/graph-atlas.md](../../../graph-atlas.md),由专门的生成器产出,并由 `pnpm run verify-doc-graphs` 及既有的目录新鲜度检查(作为 `doc-sync` 的一环)进行验证。 +新增生成的关系图文档,索引位于 [docs/graph-atlas.md](../../../graph-atlas.md),由专用生成器产出,并通过 `pnpm run verify-doc-graphs` 及既有的目录新鲜度检查(作为 `doc-sync` 的一环)进行验证。 -该索引是既有目录之上的关系层。它不替代精确的参考资料,而是链接到它们并解释各部分如何组合在一起。 +该索引是既有目录之上的关系层。它不取代精确的参考文档,而是链接到它们并解释各部分如何组合在一起。 ### 维护模式 每个关系图页面声明一种维护模式: -- **生成(Generated)**:所有节点和边均从源码发现;如果已提交的产物陈旧,`--check` 失败。 -- **混合生成(Hybrid generated)**:源码发现清单,一份小型 manifest 对不可约的策略进行分类,完整性守卫在发现的条目未被分类时失败。 -- **人工维护(Curated)**:图表解释设计意图、时序或归属;它由生成器输出以保证关系图文档作为一个可重新生成的整体,但内容是有意撰写的。 +- **Generated(生成)**:所有节点和边均从源码发现;如果已提交的产物陈旧,`--check` 失败。 +- **Hybrid generated(混合生成)**:源码发现清单,一个小型 manifest 对不可约的策略进行分类,完整性守卫在发现的条目未被分类时失败。 +- **Curated(人工策划)**:图表解释设计意图、时序或归属;它由生成器输出以使关系图文档保持为可重新生成的整体,但内容是有意撰写的。 -### 首批交付的索引 +### 首批发布的索引 -首批索引链接十个关系面。package 拓扑与工具-package 能力映射位于既有的生成目录中(这些目录已拥有相应事实);其余的专项图表由 `scripts/gen-doc-graphs.ts` 生成。 +首批索引链接十个关系面。包拓扑与工具-包能力映射位于已有的生成目录中(这些目录已拥有相应事实);其余聚焦图表由 `scripts/gen-doc-graphs.ts` 生成。 | 关系图 | 维护模式 | 真源 | |---|---|---| -| [模块依赖图](../../../module-graph.md) | 生成 | `packages/*/*/package.json` 的 peer dependencies 加 package 分组路径 | -| [工具 schema 目录与 package 映射](../../../tool-catalog.md) | 生成 | 启动采集的工具 schema 加工具-package 的服务/副作用元数据 | -| [能力 seam 与核心服务](../../../capability-seams.md) | 混合生成 | Cordis 服务声明加 `gen-doc-graphs.ts` 中的角色 manifest | -| [echo-agent 应用组合](../../../../examples/echo-agent/composition.md) | 混合生成 | `examples/echo-agent/cordis.yml` 插件列表加人工维护的应用/bundle 展开 | -| [coding-agent 应用组合](../../../../examples/coding-agent/composition.md) | 混合生成 | `examples/coding-agent/cordis.yml` 插件列表加人工维护的应用/bundle 展开 | -| [acp-agent 应用组合](../../../../examples/acp-agent/composition.md) | 混合生成 | `examples/acp-agent/cordis.yml` 插件列表加人工维护的应用/bundle 展开 | -| [事件生产者/消费者矩阵](../../../event-producer-consumer.md) | 混合生成 | Cordis 事件声明、AST 扫描的 `ctx.on/emit/parallel/serial/waterfall` 调用点,以及显式的动态分发覆盖 | -| [agent 轮次与步骤生命周期](../../../agent-lifecycle.md) | 人工维护 | architecture.md 的循环生命周期、Cordis 目录链接与会话事件语义 | -| [工具执行流水线](../../../tool-execution-pipeline.md) | 人工维护 | 工具流水线语义与 `tools/execute` waterfall(瀑布式事件) | -| [ACP 快照回放](../../../../packages/ui/acp/snapshot-replay.md) | 人工维护 | 快照 harness 行为 | +| [模块依赖图](../../../module-graph.md) | generated | `packages/*/*/package.json` 的 peer dependencies 加包分组路径 | +| [工具 schema 目录与包映射](../../../tool-catalog.md) | generated | 启动收集的工具 schema 加工具-包的服务/副作用元数据 | +| [能力 seam 与核心服务](../../../capability-seams.md) | hybrid generated | Cordis 服务声明加 `gen-doc-graphs.ts` 中的角色 manifest | +| [echo-agent 应用组合](../../../../examples/echo-agent/composition.md) | hybrid generated | `examples/echo-agent/cordis.yml` 插件列表加人工策划的应用/bundle 展开 | +| [coding-agent 应用组合](../../../../examples/coding-agent/composition.md) | hybrid generated | `examples/coding-agent/cordis.yml` 插件列表加人工策划的应用/bundle 展开 | +| [acp-agent 应用组合](../../../../examples/acp-agent/composition.md) | hybrid generated | `examples/acp-agent/cordis.yml` 插件列表加人工策划的应用/bundle 展开 | +| [事件生产者/消费者矩阵](../../../event-producer-consumer.md) | hybrid generated | Cordis 事件声明、AST 扫描的 `ctx.on/emit/parallel/serial/waterfall` 调用点,以及显式的动态分发覆盖 | +| [agent 轮次与步骤生命周期](../../../agent-lifecycle.md) | curated | architecture.md 的循环生命周期、Cordis 目录链接与会话事件语义 | +| [工具执行流水线](../../../tool-execution-pipeline.md) | curated | 工具流水线语义与 `tools/execute` waterfall(瀑布式事件) | +| [ACP 快照回放](../../../../packages/ui/acp/snapshot-replay.md) | curated | 快照 harness 行为 | -### 为什么由生成器拥有这些文档 +### 为什么由生成器拥有文档 -package 拓扑留在 `gen-module-graph.ts`,工具-package 能力映射留在 `gen-tool-catalog.ts`,因为这些生成器已经拥有权威事实和新鲜度门禁。`gen-doc-graphs.ts` 拥有其余关系页面和索引。代价是人工维护的图表需要在 TypeScript 字符串块中编辑,而非直接编辑 Markdown。对首批交付而言这是可接受的,因为面向用户的产物仍是纯 Markdown/Mermaid;如果撰写体验比可重新生成更重要,未来可以将人工维护的页面拆分出来。 +包拓扑留在 `gen-module-graph.ts`,工具-包能力映射留在 `gen-tool-catalog.ts`,因为这些生成器已经拥有权威事实和新鲜度门禁。`gen-doc-graphs.ts` 拥有其余关系页面和索引。代价是人工策划的图表需要在 TypeScript 字符串块中编辑,而非直接编辑 Markdown。对于首版来说这是可接受的,因为面向用户的产物仍然是纯 Markdown/Mermaid;未来如果撰写体验比可重新生成更重要,可以将人工策划的页面拆分出去。 ### 完整性守卫 -混合生成的页面在其 manifest 陈旧时必须显式失败: +混合生成的页面在其 manifest 陈旧时必须显式报错: -- 模块图读取每个 package 的 `peerDependencies`,并按 `packages//` 路径对 package 分组。 -- 工具目录通过启动采集已发布的工具,并从同一份 manifest(其完整性守卫已在检查)渲染 package/服务/副作用映射。 -- 能力 seam 图导入 Cordis 服务收集器,断言每个被发现的 harness `ctx.` 都已在 `SERVICE_ROLES` 中分类,且每个已分类的 key 仍然存在。 -- 事件生产者/消费者矩阵标记为混合生成,因为 subagent 生命周期事件有意使用 `ctx.events.dispatch` 实现逐监听器隔离;这些动态边是显式覆盖而非无声遗漏。 -- `verify-mermaid` 用 Mermaid 自身的解析器解析仓库中每个 ` ```mermaid ` 围栏,因此语法错误会在本地和 CI 的 `doc-sync` 中失败,而不是在 GitHub 渲染时才显示为损坏的图表。 +- 模块图读取每个包的 `peerDependencies`,并按 `packages//` 路径对包进行分组。 +- 工具目录通过启动收集已发布的工具,并从同一份 manifest 渲染包/服务/副作用映射(其完整性守卫已在检查该 manifest)。 +- 能力 seam 图导入 Cordis 服务收集器,断言每个发现的 harness `ctx.` 都已在 `SERVICE_ROLES` 中分类,且每个已分类的 key 仍然存在。 +- 事件生产者/消费者矩阵标记为 hybrid,因为 subagent 生命周期事件有意使用 `ctx.events.dispatch` 实现逐监听器隔离;这些动态边是显式覆盖而非无声遗漏。 +- `verify-mermaid` 使用 Mermaid 自身的解析器解析仓库中每个 ` ```mermaid ` 围栏,因此语法错误在本地和 CI 的 `doc-sync` 阶段即被捕获,而非在 GitHub 渲染时才显示为损坏的图表。 ## 曾考虑的替代方案 -已提交的图表使用 Mermaid,因为 GitHub 在 Markdown 中原生渲染它,且不引入新的文档构建依赖;密集的多对多数据(如事件生产者/消费者关系)则使用 Markdown 表格。**PlantUML、托管图表服务和生成的 SVG** 曾被考虑,但在 Mermaid 成为瓶颈之前有意不采用。 +已提交的图表使用 Mermaid,因为 GitHub 在 Markdown 中原生渲染它且不引入新的文档构建依赖;密集的多对多数据(如事件生产者/消费者关系)改用 Markdown 表格。**PlantUML、托管图表服务和生成的 SVG** 曾被考虑,但在 Mermaid 成为瓶颈之前有意不采用。 ## 后果 -- 维护者获得了拓扑、seam、事件流、生命周期、应用组合和快照行为的可视化入口。 -- SDK 用户获得了从用例到 package 组合的路径,而不仅仅是自底向上的 package 参考。 -- `doc-sync` 现在包含 `verify-doc-graphs` 和 `verify-mermaid`,因此关系图漂移和 Mermaid 语法错误与其他文档新鲜度门禁一同被捕获。 +- 维护者获得了拓扑、seam、事件流、生命周期、应用组合与快照行为的可视化入口。 +- SDK 用户获得了从用例到包组合的路径,而非仅有自底向上的包参考。 +- `doc-sync` 现在包含 `verify-doc-graphs` 和 `verify-mermaid`,因此关系图漂移和 Mermaid 语法错误与其他文档新鲜度门禁一起被捕获。 - 未来的文件系统和钩子工作有了承载新复杂度的具体位置:文件系统应扩展能力文档和工具目录,钩子应扩展事件矩阵和工具执行流水线。 diff --git a/docs/rfc/implemented/process/2026-07-04-cordis-jsdoc-completeness-gate.i18n.yaml b/docs/rfc/implemented/process/2026-07-04-cordis-jsdoc-completeness-gate.i18n.yaml index 18408700a9..80b4958372 100644 --- a/docs/rfc/implemented/process/2026-07-04-cordis-jsdoc-completeness-gate.i18n.yaml +++ b/docs/rfc/implemented/process/2026-07-04-cordis-jsdoc-completeness-gate.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-04-cordis-jsdoc-completeness-gate.md: 44a0ddce9c929deac3e03bb421aec1d5145e65ba -2026-07-04-cordis-jsdoc-completeness-gate.zh.md: 6fea7f96a62295bd37e778a0aadd1e9ee6c8f2b4 +2026-07-04-cordis-jsdoc-completeness-gate.zh.md: 073054072748bf6cfed09cdcc222087bcc0ed929 diff --git a/docs/rfc/implemented/process/2026-07-04-cordis-jsdoc-completeness-gate.zh.md b/docs/rfc/implemented/process/2026-07-04-cordis-jsdoc-completeness-gate.zh.md index 6fea7f96a6..0730540727 100644 --- a/docs/rfc/implemented/process/2026-07-04-cordis-jsdoc-completeness-gate.zh.md +++ b/docs/rfc/implemented/process/2026-07-04-cordis-jsdoc-completeness-gate.zh.md @@ -1,41 +1,41 @@ -# RFC:Cordis 接口的 JSDoc 完整性门禁 - -Status: implemented +# RFC:针对 Cordis 对外服务接口的 JSDoc 完整性门禁 [English](2026-07-04-cordis-jsdoc-completeness-gate.md) | 中文 +Status: implemented + ## 问题 -生成的 Cordis 目录已强制检查事件的 dispatch 模式,但未检查服务与事件契约的完整性。方法可以缺少描述,参数或返回值可以在跨插件 API 接口上不写文档——而这恰恰是 IDE 引导最重要的地方。 +生成的 Cordis 目录此前强制了事件分发模式,但未强制要求完整的服务与事件契约。方法可以缺少描述,参数或返回值可以在跨插件 API 接口上不写文档——而这恰恰是 IDE 引导最重要的地方。 -AGENTS.md 规则("每个导出都有解释语义的 JSDoc")只能靠评审以行文方式检查;本仓库的既定偏好是将不变式编码为机械门禁。"Cordis 服务函数与事件"这一范围有一个精确的机器定义,只有目录生成器知道:事件是 `declare module 'cordis'` 内 `interface Events` 的成员,服务接口是每个 `interface Context` 键所指向的类的公开方法。ESLint 规则看不到这层映射;生成器在每次运行时都会计算它。 +AGENTS.md 中的规则(「每个导出都有解释语义的 JSDoc」)只能靠评审以行文形式检查;本仓库的既定偏好是将不变式编码为机械门禁。「Cordis 服务函数与事件」这一范围有精确的机器定义,只有目录生成器知道:事件是 `declare module 'cordis'` 内 `interface Events` 的成员,服务接口是每个 `interface Context` 键所指向的类的公开方法。ESLint 规则看不到这层映射;生成器在每次运行时计算它。 ## 决策 -扩展 `scripts/gen-cordis-catalog.ts`——同一次遍历、同一个 `@mode` 先例——对其目录化的所有内容强制 JSDoc 完整性。`verify-cordis-catalog` 运行在 `doc-sync` 内部,CI 和 lefthook pre-push 钩子都已执行 `doc-sync`,因此门禁无需新增任何接线(质量门禁原则:单一真源)。 +扩展 `scripts/gen-cordis-catalog.ts`(同一次遍历、同一个 `@mode` 先例),对其编目的所有内容强制 JSDoc 完整性。`verify-cordis-catalog` 在 `doc-sync`(文档同步门禁)内运行,CI 和 lefthook pre-push 钩子都已执行 `doc-sync`,因此门禁无需新增任何接线(质量门禁原则:单一真源)。 契约如下: -- **事件**需要描述文字,并为每个**载荷参数**提供非空 `@param`。载荷参数是签名中承载事件数据的参数;`this` 接收者注解和尾部的 waterfall `next` 免检——`next` 是 dispatch 机制,其语义已由 `@mode waterfall` 标签(及其结构交叉检查)拥有,逐事件重述只是样板。对免检参数写文档是允许的;门禁只检查缺失。 -- **服务类**需要类级 JSDoc,每个公开方法需要描述文字、每个参数一个非空 `@param`,以及一个非空 `@returns`(除非标注的返回类型是 `void`/`Promise`,此时 `@returns` 可选——解析时机可能值得记录——但从不强制要求)。 +- **事件**需要描述性文字,以及为每个**载荷参数**提供非空的 `@param`。载荷参数是携带事件数据的签名参数;`this` 接收者注解和尾部的 waterfall(瀑布式事件) `next` 免检——`next` 是分发机制,其语义已由 `@mode waterfall` 标签(及其结构交叉检查)拥有,逐事件重述只是样板代码。为免检参数写文档是允许的;只有缺失才被检查。 +- **服务类**需要类级 JSDoc,每个公开方法需要描述性文字、为每个参数提供非空的 `@param`,以及非空的 `@returns`——除非标注的返回类型是 `void`/`Promise`(此时 `@returns` 可选——resolve 时机有时值得记录——但从不强制要求)。 - **陈旧标签报错**:`@param` 命名了一个不存在的参数即为违规,与 `@mode` 与签名矛盾的检查对称。标签描述必须非空;超出此范围的语义质量由评审负责。 -- **遍历可检查的显式性**:门禁是纯 AST 遍历(不使用类型检查器),因此服务方法必须显式标注返回类型(推断的返回类型无法分类),接口参数必须是简单标识符(解构模式没有名字供 `@param` 匹配)。 -- **违规聚合**为一条错误信息,列出所有违规项——修复时一次看到全部。此前快速失败的 `@mode` 检查也移入同一份聚合报告,消息文本不变。 +- **遍历可检查的显式性**:门禁是纯 AST 遍历(不使用类型检查器),因此服务方法必须显式标注返回类型(推断的返回类型无法分类),接口参数必须是简单标识符(解构模式没有名称供 `@param` 匹配)。 +- **违规聚合**为一条错误信息,列出所有违规项——修复时一次看到完整清单。此前快速失败的 `@mode` 检查也移入同一份聚合报告,消息文本不变。 -这些标签**仅用于门禁强制**:`parseJsDoc` 现在在遇到第一个块标签时截止描述文字(标准 JSDoc 语义,同时也防止多行标签描述泄漏到目录中成为正文),因此 `@param`/`@returns` 永远不会改变渲染出的目录。 +这些标签**仅用于强制检查**:`parseJsDoc` 现在在遇到第一个块标签时截止描述性文字(标准 JSDoc 语义,同时也防止多行标签描述泄漏到目录中充当正文),因此 `@param`/`@returns` 不会改变渲染出的目录。 -`packages/core/agent/tests/gen-cordis-catalog.spec.ts` 中的负向路径测试用合成 fixture(测试前置数据)驱动 `collectEvents`/`collectServices`,证明每个守卫都能触发且免检规则成立。撰写规则写在根 [AGENTS.md](../../../../AGENTS.md) 约定条目中,与 `@mode` 规则并列。 +`packages/core/agent/tests/gen-cordis-catalog.spec.ts` 中的负路径测试对合成 fixture(测试前置数据)运行 `collectEvents`/`collectServices`,验证每条守卫都会触发且免检规则成立。撰写规则写在根 [AGENTS.md](../../../../AGENTS.md) 的约定条目中,与 `@mode` 规则并列。 ## 曾考虑的替代方案 -- **ESLint 规则**:看不到范围的机器定义(哪些 `interface Events` 成员、哪些 `ctx.` 类构成 Cordis 接口);目录生成器在每次运行时恰好计算这层映射,因此门禁放在那里。 -- **将标签渲染到目录中**:曾考虑将服务部分重构为逐方法条目,但有意推迟:方法文档的消费场景是源码 JSDoc 加 IDE 悬浮提示,目录保持索引定位。 -- **逃生标签**:不设。接口面小且经过策划(采纳时 12 个服务、57 个方法、27 个事件),重点在于检查不可豁免。 +- **ESLint 规则**:无法看到该范围的机器定义(哪些 `interface Events` 成员、哪些 `ctx.` 类构成 Cordis 对外服务接口);目录生成器在每次运行时恰好计算这层映射,因此门禁放在那里。 +- **将标签渲染到目录中**:曾考虑将服务部分重构为逐方法条目,但有意推迟:方法文档的消费场景是源码 JSDoc 加 IDE 悬停,目录保持索引定位。 +- **逃逸标签**:不设。该接口面小且经过策展(采纳时 12 个服务、57 个方法、27 个事件),要点在于检查不可豁免。 ## 后果 -- 新增事件或服务方法如果参数或返回值未写文档,就无法合入:生成器拒绝重新生成,`verify-cordis-catalog` 在 pre-push 和 CI 中失败。采纳时发现的约 139 处缺口在同一个变更中补齐,门禁以绿色状态落地。 -- 服务接口必须显式标注返回类型并使用标识符参数。两项约束在采纳时均未构成负担(所有方法已有标注;不存在解构的 seam 参数);二者现在都是承重要求,违反时会被机械发现。 -- AGENTS.md 的通用 JSDoc 规则("一行能说清就写一行")在此接口上获得一条更严格的特例:只有当方法无参数且返回 void 时,一行摘要才仍然足够。 -- 对 `next` 或 `this` 写 `@param` 合法但不检查——这是有意的不对称:门禁强制载荷契约,拒绝索要样板。 -- 标签不改变渲染出的目录(正文在第一个块标签处截止)。如果日后需要方法级渲染,那是目录设计的独立决策,不是本门禁的缺口。 +- 新增事件或服务方法时,若参数或返回值未写文档则无法落地:生成器拒绝重新生成,`verify-cordis-catalog` 在 pre-push 和 CI 中失败。采纳时发现的约 139 处缺口在同一个变更中补齐,门禁以绿色状态落地。 +- 服务接口必须显式标注返回类型并使用标识符参数。两项约束在采纳时均未构成限制(所有方法已有标注;不存在解构的 seam 参数);但二者现在是承重要求,违反时会被机械检测到。 +- AGENTS.md 中通用的 JSDoc 规则(「一行能说清就用一行」)在此接口上获得了更严格的特例:仅当方法无参数且返回 void 时,一行摘要才足够。 +- 为 `next` 或 `this` 写 `@param` 合法但不检查——这是有意的不对称:门禁强制载荷契约,拒绝要求样板代码。 +- 渲染出的目录不受这些标签影响(正文在第一个块标签处截止)。如果后续需要方法级渲染,那是一个独立的目录设计决策,而非本门禁的缺口。 diff --git a/docs/rfc/implemented/process/2026-07-04-doc-tiers-and-budgets.i18n.yaml b/docs/rfc/implemented/process/2026-07-04-doc-tiers-and-budgets.i18n.yaml index 5d1764cea6..d6465ad8c1 100644 --- a/docs/rfc/implemented/process/2026-07-04-doc-tiers-and-budgets.i18n.yaml +++ b/docs/rfc/implemented/process/2026-07-04-doc-tiers-and-budgets.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-04-doc-tiers-and-budgets.md: ca9da847849f61f2fb244932e657cca8fec69696 -2026-07-04-doc-tiers-and-budgets.zh.md: 37447d11de181a007bcea23e29084aefd97b1ab5 +2026-07-04-doc-tiers-and-budgets.zh.md: ae34e3f04d7986f78d1b3ceeaacb3bc4c7bc64f5 diff --git a/docs/rfc/implemented/process/2026-07-04-doc-tiers-and-budgets.zh.md b/docs/rfc/implemented/process/2026-07-04-doc-tiers-and-budgets.zh.md index 37447d11de..ae34e3f04d 100644 --- a/docs/rfc/implemented/process/2026-07-04-doc-tiers-and-budgets.zh.md +++ b/docs/rfc/implemented/process/2026-07-04-doc-tiers-and-budgets.zh.md @@ -1,28 +1,28 @@ # RFC:文档分层、预算与上限门禁 -Status: implemented - [English](2026-07-04-doc-tiers-and-budgets.md) | 中文 +Status: implemented + ## 问题 -尽管已有写作指导,常设文档仍然积累了重复的规则、重述的事件、重复的 package 地图和陈旧的 RFC 摘要。由于仅靠评审无法阻止这种膨胀,仓库需要在文档分类体系之外再加一道机械化的预算。 +尽管已有写作指导,常设文档仍然积累了重复的规则、重述的事故、重复的包(package)映射和陈旧的 RFC 摘要。仅靠评审无法阻止这种膨胀,因此仓库需要在文档分类体系之外增加一道机械化的预算约束。 ## 决策 -- **分层分类体系,每条事实只有一个归属。** [docs/AGENTS.md](../../../AGENTS.md) 是文档标准:它为每个 Markdown 层级指定唯一职责(常设指令、系统地图、类型目录、决策记录、事件故事、实操手册(cookbook)、package 契约、生成目录、工作流),禁止在归属层级之外重述事实(应改为链接),并附带一份在撰写或评审任何文档时使用的冗余检查清单。 -- **窄范围、硬约束的预算门禁。** [scripts/verify-doc-budgets.ts](../../../../scripts/verify-doc-budgets.ts) 加入 doc-sync:凡列入 [scripts/doc-budgets.manifest.json](../../../../scripts/doc-budgets.manifest.json) 的文档都必须低于其字数上限(`wc -w` 语义,整个文件),且已设预算的文件若缺失也会使门禁失败,防止重命名时预算被静默遗留。范围有意仅限于容易膨胀的常设文档:根目录与子树的 `AGENTS.md`、`architecture.md`、`packages/README.md`,以及它们将内容分流到的常设策略文档(`docs/testing.md`、`docs/defensive-patterns.md`)。参考文档、RFC 和 package README 不设预算:当每一行都是事实时,长度是合理的,由评审加冗余检查清单管控。 -- **上限是只进不退的执行红线。** 上限设定在文档当前大小的至少 5% 以上(留出操作余量,使日常措辞修改不会触发门禁,而真正的膨胀仍会被拦截),并随着文档被压缩到目标预算(根 `AGENTS.md` ≤ 1,500 词;`architecture.md` ≤ 1,800;子树 `AGENTS.md` ≤ 600;`packages/README.md` ≤ 600)而保持该余量向下收紧——与[翻译配对 `required` 清单](2026-07-02-bilingual-docs-and-pairing-gate.md)的推进机制相同。门禁变红时,修复方式是按分类体系迁移或精简内容;只有在 PR 描述中给出明确理由时才允许提高上限,manifest diff 本身即为可评审的动作。 -- **轻量工作流 skill,契约在文档中。** [.agents/skills/dsh-doc-standards](../../../../.agents/skills/dsh-doc-standards/SKILL.md) 承载归位/审计/红灯修复工作流,并将文档标准作为真源——与 [dsh-translate-docs](../../../../.agents/skills/dsh-translate-docs/SKILL.md) 对 i18n 契约的分工方式相同。 +- **分层分类体系,每条事实只有一个归属地。** [docs/AGENTS.md](../../../AGENTS.md) 是文档标准:它为每个 Markdown 层级指定唯一职责(常设指令、系统地图、类型目录、决策记录、事故叙事、实操手册、逐包契约、生成目录、工作流),禁止在归属层级之外重述事实(应以链接代替),并附带一份在撰写或评审任何文档时使用的冗余检查清单。 +- **窄范围、硬约束的预算门禁。** [scripts/verify-doc-budgets.ts](../../../../scripts/verify-doc-budgets.ts) 加入 `doc-sync`(文档同步门禁):[scripts/doc-budgets.manifest.json](../../../../scripts/doc-budgets.manifest.json) 中列出的每篇文档都必须低于其字数上限(`wc -w` 语义,整个文件),且受预算约束的文件如果缺失也会导致门禁失败,防止重命名后预算被静默遗留。范围刻意限定为容易膨胀的常设文档:根目录和子树的 `AGENTS.md`、`architecture.md`、`packages/README.md`,以及它们将内容分流到的常设策略文档(`docs/testing.md`、`docs/defensive-patterns.md`)。参考文档、RFC 和 package README 不设预算:当每一行都是事实时,长度是合理的,由评审加冗余检查清单来管控。 +- **上限是只进不退的执行红线。** 上限设定为文档当前字数的至少 105%(留出工作余量,使日常措辞调整能通过,而真正的膨胀仍会触发门禁),并随着文档被精简到目标预算而同步下调、保持该余量(根 `AGENTS.md` ≤ 1,500 词;`architecture.md` ≤ 1,800;子树 `AGENTS.md` ≤ 600;`packages/README.md` ≤ 600)。推进机制与[翻译配对的 `required` 清单](2026-07-02-bilingual-docs-and-pairing-gate.md)相同。门禁变红时,修复方式是按分类体系迁移或压缩内容;只有在 PR(Pull Request)描述中给出明确理由时才允许提高上限,manifest(元数据清单)的 diff 本身即为可评审的动作。 +- **轻量工作流 skill(技能),契约在文档中。** [.agents/skills/dsh-doc-standards](../../../../.agents/skills/dsh-doc-standards/SKILL.md) 承载放置/审计/红灯修复工作流,并将文档标准作为真源,与 [dsh-translate-docs](../../../../.agents/skills/dsh-translate-docs/SKILL.md) 对 i18n 契约的分工方式一致。 ## 曾考虑的替代方案 -- **仅靠 skill 与评审纪律,不设门禁**:否决。上述膨胀正是在既有的现状规则和评审者注意力下发生的;一条没有机械后盾的行文规则在这里已被证明守不住,而本仓库自身的[质量门禁立场](2026-06-11-quality-gates.md)说的是:值得保持的不变式就值得编码。 -- **对所有文档层级设置宽泛门禁**:否决。一刀切的上限恰恰惩罚了那些正当的长文档(如功能矩阵或类型目录,每一行都是事实,例如 `packages/ui/acp/acp-feature-support.md`),并产生逐文件的例外修改,训练贡献者无脑批准上调。 -- **将标准放在 skill 内部**:否决。契约放在文档中,工作流放在 skill 中;如果标准被塞进 SKILL.md,那些不调用该 skill 而直接编辑文档的 agent 就看不到它,而 `docs/AGENTS.md` 已经作为子树指令被加载给所有在 `docs/` 下工作的人。 +- **仅靠 skill 和评审纪律,不设门禁**:否决。上述膨胀正是在现行规则和评审注意力已经存在的情况下发生的;一条没有机械后盾的行文规则在此处已被证明无法维持,而本仓库自身的[质量门禁立场](2026-06-11-quality-gates.md)认为值得保持的不变式就值得编码。 +- **对所有文档层级全面设限**:否决。一刀切的上限恰好惩罚了那些正当的长文档(如特性矩阵或类型目录,每一行都是事实,例如 `packages/ui/acp/acp-feature-support.md`),并产生逐文件的例外变更,训练贡献者机械地批准提限。 +- **将标准放在 skill 内部**:否决。契约归文档,工作流归 skill;如果标准被塞进 SKILL.md,那些不调用该 skill 而直接编辑文档的 agent(智能体)就看不到它,而 `docs/AGENTS.md` 已经作为子树指令被任何在 `docs/` 下工作的人加载。 ## 后果 -- 向已设预算的文档添加内容现在需要置换:将新增内容迁移到其分类归属处并留下指针,或精简既有行文为其腾出空间。只增不减会导致 CI 失败。 -- 将文档压缩到目标预算的重写以堆叠的后续 PR 落地,每个合并时都将 manifest 中的上限向下收紧;在各自落地之前,文档的冻结上限仅阻止进一步膨胀。 -- 字数是一个粗糙的代理指标,这是有意接受的:它无法判断质量,但它恰好在内容被添加的那一刻强制触发迁移决策——而那正是作者拥有足够上下文来正确归位内容的时刻。 +- 向受预算约束的文档添加内容现在需要置换:将新增内容迁移到其分类体系归属地并留下指针,或压缩现有行文来腾出空间。只增不减会导致 CI 失败。 +- 精简到目标预算的重写以堆叠的后续 PR 落地,每次合并时同步下调 manifest 中的上限;在各自落地之前,文档冻结的上限仅阻止进一步膨胀。 +- 字数是一个粗糙的代理指标,这是有意接受的:它无法判断质量,但它在内容被添加的那一刻强制触发迁移决策,而那正是作者拥有足够上下文来正确放置内容的时刻。 diff --git a/docs/rfc/implemented/process/2026-07-04-generate-rfc-index-tables.i18n.yaml b/docs/rfc/implemented/process/2026-07-04-generate-rfc-index-tables.i18n.yaml index 814ca38546..8d410ae5f8 100644 --- a/docs/rfc/implemented/process/2026-07-04-generate-rfc-index-tables.i18n.yaml +++ b/docs/rfc/implemented/process/2026-07-04-generate-rfc-index-tables.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-04-generate-rfc-index-tables.md: 6a8888eda8b7cf7105a44802774bf49d6463952d -2026-07-04-generate-rfc-index-tables.zh.md: 42ae64306aa1d5cf9117ca2b4d698a5d8effdeec +2026-07-04-generate-rfc-index-tables.zh.md: aad1652edc9de81a70a7a391700f63be9499035c diff --git a/docs/rfc/implemented/process/2026-07-04-generate-rfc-index-tables.zh.md b/docs/rfc/implemented/process/2026-07-04-generate-rfc-index-tables.zh.md index 42ae64306a..aad1652edc 100644 --- a/docs/rfc/implemented/process/2026-07-04-generate-rfc-index-tables.zh.md +++ b/docs/rfc/implemented/process/2026-07-04-generate-rfc-index-tables.zh.md @@ -1,34 +1,34 @@ # RFC:生成 RFC 索引表 -Status: implemented - [English](2026-07-04-generate-rfc-index-tables.md) | 中文 +Status: implemented + ## 问题 -RFC 索引中按生命周期/按类别的表格所列信息完全可推导:RFC 的路径编码了生命周期与类别,文件名编码了首次提出日期,H1 标题即为标题。手工维护这些事实的副本恰恰是本仓库文档中冲突最频繁的热点:每一波提案都向同几行追加行,因此并行的 RFC 分支恰好在此处冲突,而在其他所有地方都没有分歧;每次冲突都要手动合并那些文件系统本已知晓内容的行。[分类 RFC](2026-06-20-rfc-classification.md) 最初为了策展目的保留手写索引,但 README 中真正需要策展的部分是行文,而行文从不冲突;冲突的只有机械表格。 +RFC 索引中按生命周期/按分类的表格所列信息完全可以推导:RFC 的路径编码了生命周期与分类,文件名编码了首次提出日期,H1 标题承载了标题文本。这些信息的手工维护副本也是仓库中冲突最频繁的文档热点:每一波提案都在同几行后追加新行,因此并发的 RFC 分支恰好在此处冲突,而其他地方完全一致;每次冲突都要手工合并那些文件系统本已知晓的行。[分类 RFC](2026-06-20-rfc-classification.md) 最初为了可策展性而保留手写索引,但 README 中真正需要策展的是行文,而行文从不冲突;冲突的只有机械表格。 ## 决策 -保留策展行文;生成列表。表格位于 [`docs/rfc/INDEX.md`](../../INDEX.md),是一个**完全生成的文件**;策展行文留在 README.md 中,README.md 不包含任何索引行。[`scripts/rfc-index.ts`](../../../../scripts/rfc-index.ts) 是共享的真源:树遍历器(拥有封闭的生命周期/类别集合与结构规则,包括 H1 可解析的要求)和渲染器(行来自 H1 标题并去除 `RFC: ` 前缀,加上文件名日期,按日期再按文件名排序,以 `### {Class}` 分节、按规范类别顺序分组)。两个轻量消费方共享它: +保留策展行文;生成列表。表格位于 [`docs/rfc/INDEX.md`](../../INDEX.md),是一个**完全生成的文件**——策展行文留在 README.md 中,README.md 不包含任何索引行。[`scripts/rfc-index.ts`](../../../../scripts/rfc-index.ts) 是共享的真源:树遍历器(拥有封闭的生命周期/分类集合与结构规则,包括对可解析 H1 的要求)和渲染器(行来自 H1 标题并去掉 `RFC: ` 前缀,加上文件名日期,按日期再按文件名排序,以 `### {Class}` 分节、按规范分类顺序分组)。两个轻量消费方共享它: - [`scripts/gen-rfc-index.ts`](../../../../scripts/gen-rfc-index.ts)(`pnpm run gen-rfc-index`)从目录树完整重写 INDEX.md。 -- [`scripts/verify-rfc-classification.ts`](../../../../scripts/verify-rfc-classification.ts)(doc-sync 的一个成员)检查结构,断言已提交的 INDEX.md 与新鲜渲染结果逐字节一致(与 `gen-cordis-catalog`/`verify-cordis-catalog` 模式相同),并拒绝在策展 README 中出现索引格式的行。新鲜度检查涵盖了索引完整性检查:从磁盘生成的表格在定义上就是完整且标题正确的。 +- [`scripts/verify-rfc-classification.ts`](../../../../scripts/verify-rfc-classification.ts)(doc-sync(文档同步门禁)的一个成员)检查结构,断言已提交的 INDEX.md 与新鲜渲染结果逐字节一致(`gen-cordis-catalog`/`verify-cordis-catalog` 模式),并拒绝在策展 README 中出现索引格式的行。新鲜度检查涵盖了索引完整性检查:从磁盘生成的表格在定义上就是完整的、标题正确的。 -添加、移动或删除一个 RFC 只需编辑 RFC 文件本身并运行生成器;分类 RFC 的「否决替代方案」记录中带有取代关系的交叉链接。 +添加、移动或删除一个 RFC 只需编辑 RFC 文件本身并运行生成器;分类 RFC 的「已否决替代方案」记录中带有替代关系的交叉链接。 ## 曾考虑的替代方案 ### 为什么不在 README.md 内使用标记分隔区域? -最初落地的形态:生成器在 README.md 中的 `gen-rfc-index` 标记注释之间、每个 `## {Lifecycle}` 标题下拼接表格。在 README 同时吸收了文件内格式契约([统一格式 RFC](2026-07-05-uniform-rfc-format.md))之后,被整文件 INDEX.md 方案取代:一个门面 README 承载数百行生成行,会淹没其策展行文;而拼接机制(标记对、标题检查、区域外行检测)的存在只是为了保护策展文本——专用的生成文件根本不包含策展文本。 +最初落地的形态是:生成器将表格拼接到 README.md 中 `gen-rfc-index` 标记注释之间、各 `## {Lifecycle}` 标题之下。在 README 同时吸收了文件内格式契约([统一格式 RFC](2026-07-05-uniform-rfc-format.md))之后,被整文件 INDEX.md 方案取代:一个门面 README 承载数百行生成内容会淹没其策展行文,而拼接机制(标记对、标题检查、区域外行检测)的存在仅仅是为了保护策展文本——专用的生成文件根本不包含这类文本。 -### 为什么不采用纯校验模式? +### 为什么不采用纯校验器模式? -纯校验能捕获错误,但每次提案编辑仍然要在手工维护的表格中触碰共享热点;对于一行纯机械内容,校验失败比生成器更令人烦恼:作者已经命名并放置了文件,索引副本不增加任何信息。这与 [package-inventory 提案](../../proposed/process/2026-06-20-discover-package-inventory.md) 对 tsconfig references 和 knip stanzas 所做的「手工列表 vs. 推导」判断相同——应用于这张确实会冲突的列表。 +校验器能捕获错误,但每次提案编辑仍然要在手工维护的表格中触碰共享热点;对于纯机械的行,校验器失败比生成器更令人烦恼:作者已经命名并放置了文件,索引副本不增加任何信息。这与 [package-inventory 提案](../../proposed/process/2026-06-20-discover-package-inventory.md) 对 tsconfig references 和 knip stanzas 所做的手写列表与推导之间的判断一致——应用于这张确实会冲突的列表。 ## 后果 - 生成文件是显式的:其横幅标注了生成器名称,文件内没有需要保护的策展区域,且生成器在目录树结构无效时拒绝运行。 -- 格式错误或缺失的 H1 在生成器和门禁中都是硬错误:H1 现在是承重的,它是索引标题的来源。 -- 并行的 RFC 分支通过重新运行生成器来解决索引冲突,而非手动合并行。 +- 格式错误或缺失的 H1 在生成器和门禁中都是硬错误——H1 现在是索引标题的承重来源。 +- 并发的 RFC 分支通过重新运行生成器解决索引冲突,从不手工合并行。 diff --git a/docs/rfc/implemented/process/2026-07-04-persistence-log-catalog.i18n.yaml b/docs/rfc/implemented/process/2026-07-04-persistence-log-catalog.i18n.yaml index d824b18b5d..5851ab8d1c 100644 --- a/docs/rfc/implemented/process/2026-07-04-persistence-log-catalog.i18n.yaml +++ b/docs/rfc/implemented/process/2026-07-04-persistence-log-catalog.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-04-persistence-log-catalog.md: f8f831ca0470cf3b5c7634550115e67f7deac840 -2026-07-04-persistence-log-catalog.zh.md: d8cd5f74e24968fa2ad01f128dafe8c867f3406c +2026-07-04-persistence-log-catalog.zh.md: 1daa76e2b23484ad6434f6a55482672abf456eb7 diff --git a/docs/rfc/implemented/process/2026-07-04-persistence-log-catalog.zh.md b/docs/rfc/implemented/process/2026-07-04-persistence-log-catalog.zh.md index d8cd5f74e2..1daa76e2b2 100644 --- a/docs/rfc/implemented/process/2026-07-04-persistence-log-catalog.zh.md +++ b/docs/rfc/implemented/process/2026-07-04-persistence-log-catalog.zh.md @@ -6,31 +6,31 @@ Status: implemented ## 问题 -`SessionEventMap` 是磁盘上的词汇(vocabulary),但其声明分散在所属的 session 包与声明合并中。生成式持久化目录是每个事件及其 payload 的唯一参考;手工维护的表格会漂移,已被移除。这些记录不是 Cordis 事件:观察者通过唯一的 `session/event` 总线事件接收它们,因此 Cordis 目录无法覆盖。生成器会发现所有声明,doc-sync(文档同步门禁)新鲜度门禁会拒绝遗漏或陈旧的输出。 +`SessionEventMap` 是磁盘上的词汇,但其声明分散在拥有它的 session 包(package)与声明合并中。生成式持久化目录是所有事件与 payload 的唯一参考;手工维护的表格会漂移,已被移除。这些记录不是 Cordis 事件,观察者通过单一的 `session/event` 总线事件接收它们,因此 Cordis 目录无法覆盖。生成器发现所有声明,doc-sync(文档同步门禁)的新鲜度门禁拒绝遗漏或陈旧的输出。 ## 决策 -从源码生成 `docs/persistence-catalog.md`,配以新鲜度门禁,作为第四个参考面:持久化会话日志可以包含的*记录*,与 Cordis 目录(接线)、核心数据结构(词汇)和工具目录(工具)互补。 +从源码生成 `docs/persistence-catalog.md`,配合新鲜度门禁,作为第四个参考面:持久化会话日志可以包含的*记录*,与 Cordis 目录(接线)、核心数据结构(词汇)和工具目录(工具)互补。 -`gen-persistence-catalog.ts` 使用 TypeScript AST 扫描所有所属的和声明合并的 `SessionEventMap`。它渲染源码 JSDoc、payload 类型、派生的 surface 徽章、参考链接和源码位置。doc-sync 新鲜度检查会拒绝任何词汇变更后未重新生成目录的情况。 +`gen-persistence-catalog.ts` 使用 TypeScript AST 扫描所有拥有方与声明合并的 `SessionEventMap`。它渲染源码 JSDoc、payload 类型、派生的 surface 徽章、参考链接与源码位置。doc-sync 新鲜度检查会拒绝任何词汇变更后未重新生成目录的情况。 具体选择: -- **JSDoc 完整性,强制执行。** 每个成员必须携带描述性文字:JSDoc 即为目录条目,与 Cordis 目录对总线事件施加的强制函数相同。成员上的 `@mode` 标签是硬错误:dispatch mode 属于 Cordis 总线事件,日志事件没有 mode;该标签会被误读为「此事件以 mode X 在总线上触发」。违规项聚合为一条错误,列出所有违规者。 -- **surface 徽章由派生得出,而非手工列举。** `SurfaceEventType`(产生 LLM 消息且可能携带 `surfaceOp` 的子集)从所属包中的 union 声明解析而来;union 成员如果命名了一个未声明的事件,则为硬错误(否则一个陈旧的 union 成员会静默地不标注任何事件)。其余一律渲染为 **log-only**。 -- **专用围栏。** payload 块使用 ` ```ts persistence-catalog ` 信息字符串,`doc-typecheck` 识别并跳过它,不计入 opt-out 比例——与 `ts cordis-catalog` 的处理方式相同(裸 payload 片段不能独立编译)。 -- **仓库范围。** 目录枚举本仓库中的包,与兄弟目录的 packages-only 范围一致;下游插件可以合并更多事件类型,但它们在设计上不在目录范围内。遍历过程用硬错误保护自身假设:所属的顶层 `interface SessionEventMap` 必须是 `@deepseek-ai/dsh-session` 中唯一的导出声明(一个无关的、局部的或重复的同名接口不能被当作磁盘词汇编入目录);任何声明不得携带 `extends`(继承的键会加入 `keyof SessionEventMap` 却没有对应的目录行);每个成员必须是带有显式 payload 类型的属性签名(方法形式的成员会加入 `keyof` 却被静默遍历跳过);跨声明的重复成员会失败。 +- **JSDoc 完整性,强制执行。** 每个成员必须带有描述性文字——JSDoc 即为目录条目,与 Cordis 目录对总线事件施加的强制机制相同。成员上的 `@mode` 标签是硬错误:dispatch mode 属于 Cordis 总线事件,日志事件没有 mode,该标签会被误读为「此事件以模式 X 在总线上触发」。违规项聚合为一条错误,列出所有违规者。 +- **surface 徽章由派生得出,而非手工列举。** `SurfaceEventType`(产生 LLM(大语言模型)消息且可能携带 `surfaceOp` 的子集)从拥有方包中的 union 声明解析;如果 union 成员命名了一个未声明的事件,则为硬错误(否则陈旧的 union 成员会静默地不标注任何内容)。其余一律渲染为 **log-only**。 +- **专用围栏。** payload 块使用 ` ```ts persistence-catalog ` 信息字符串,`doc-typecheck` 识别并跳过它,不计入 opt-out 比例——与 `ts cordis-catalog` 的处理方式相同(裸 payload 片段无法独立编译)。 +- **仓库范围。** 目录枚举本仓库中的包,与兄弟文档的 packages-only 范围一致;下游插件可以合并更多事件类型,它们在设计上不在目录范围内。遍历过程用硬错误保护自身假设:拥有方的顶层 `interface SessionEventMap` 必须是 `@deepseek-ai/dsh-session` 中唯一的导出声明(无关的、局部的或同名重复的接口不能被当作磁盘词汇编入目录);任何声明不得携带 `extends`(继承的键会加入 `keyof SessionEventMap` 却没有对应的目录行);每个成员必须是带有显式 payload 类型的属性签名(方法形式的成员会加入 `keyof` 却在静默遍历中被漏过);跨声明的重复成员也会失败。 -这取代了手工副本:session.md 的 `hook/*` 表格、compact README 的事件表格、hook-protocol README 的 payload 列表,以及 session README 的名称列表现在链接到目录,而非重述 payload(周围的语义行文保留原位)。hook-protocol 合并成员上两个多余的 `@mode emit` 标签已被移除——新门禁将其拒绝为它们本来就是的类别错误。 +本方案取代了手工副本:session.md 的 `hook/*` 表格、精简版 README 的事件表格、hook-protocol README 的 payload 条目列表,以及 session README 的名称列表现在链接到目录,而不再重述 payload(周围的语义说明文字保留原位)。hook-protocol 合并成员上的两个误加的 `@mode emit` 标签已被移除——新门禁将它们作为类别错误拒绝。 ## 曾考虑的替代方案 -- **基于启动的生成器(如工具目录的方式)**:日志词汇完全是静态的,AST 遍历无需启动任何东西即可读取全部真相。 -- **保留手工副本**:手工副本只能检查作者已经写下的名称;目录落地时 session README 的合并说明已经漂移。 +- **基于启动的生成器(类似工具目录)**:日志词汇完全是静态的,AST 遍历无需启动任何东西即可读取全部真相。 +- **保留手工副本**:手工副本只能检查作者已经写下的名称;目录落地时,session README 的合并说明已经漂移。 ## 后果 -- 目录不可能漂移:词汇变更而已提交文件未反映的,`verify-persistence-catalog` 在 pre-push 钩子和 CI 中会失败;新合并的事件如果没有 JSDoc,生成器直接报错——插件不能再添加未文档化的磁盘记录类型。 -- 事件描述有唯一归属地:声明处的 JSDoc。JSDoc 写得薄,目录条目就薄,这对作者形成在源头写文档的压力。 +- 目录不会漂移:词汇变更若未反映在已提交的文件中,`verify-persistence-catalog` 会在 pre-push 钩子和 CI 中失败;新合并的事件若缺少 JSDoc,生成器直接报错——插件不再能添加未文档化的磁盘记录类型。 +- 事件描述有唯一归属地,即声明处的 JSDoc;JSDoc 写得单薄,目录条目就单薄,这迫使作者在源头做好文档。 - `SurfaceEventType` union 现在对文档具有结构性承载作用:重命名事件而不更新 union(或反过来)会导致生成器失败,而不仅仅是编译器失败。 -- 徽章派生假设 union 始终是一组封闭的字符串字面量且只有一个所有者;如果重构偏离了这一形状,必须在同一个变更中更新生成器。 +- 徽章派生假设 union 始终是一组封闭的字符串字面量且只有一个拥有方;如果重构偏离了这一形状,必须在同一个变更中更新生成器。 diff --git a/docs/rfc/implemented/process/2026-07-05-uniform-rfc-format.i18n.yaml b/docs/rfc/implemented/process/2026-07-05-uniform-rfc-format.i18n.yaml index bb326da2d8..a5e22d0911 100644 --- a/docs/rfc/implemented/process/2026-07-05-uniform-rfc-format.i18n.yaml +++ b/docs/rfc/implemented/process/2026-07-05-uniform-rfc-format.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-05-uniform-rfc-format.md: f67c61c0b627950cf07e7d57672324308a9462ec -2026-07-05-uniform-rfc-format.zh.md: a11cba1c343f150a67c1af4a04a8d89480185207 +2026-07-05-uniform-rfc-format.zh.md: b175c2d5b7537e793a61c95b6524d08bc4216384 diff --git a/docs/rfc/implemented/process/2026-07-05-uniform-rfc-format.zh.md b/docs/rfc/implemented/process/2026-07-05-uniform-rfc-format.zh.md index a11cba1c34..b175c2d5b7 100644 --- a/docs/rfc/implemented/process/2026-07-05-uniform-rfc-format.zh.md +++ b/docs/rfc/implemented/process/2026-07-05-uniform-rfc-format.zh.md @@ -1,30 +1,30 @@ -# RFC:为 RFC 统一一种有门禁保障的文件内格式 - -Status: implemented +# RFC:RFC 的统一受门禁约束的文件内格式 [English](2026-07-05-uniform-rfc-format.md) | 中文 +Status: implemented + ## 问题 -RFC 的路径已编码了生命周期与分类,但文件内容仍然混杂着不同的标题风格、状态格式、ADR 模板与提案模板,以及已实施记录中残留的提案时期章节。作者复制手边找到的任何邻居文件作为模板,而生命周期迁移可以跳过必要的改写,因为没有门禁强制执行文件内契约。 +RFC 的路径已经编码了生命周期和分类,但文件内容仍然混杂着不同的标题风格、状态格式、ADR 与 proposal 模板,以及已实现记录中残留的 proposal 时期的章节。作者随手复制找到的任何邻近文件,生命周期迁移时可以跳过必要的改写,因为没有门禁强制执行文件内契约。 ## 决策 -[README.md § The file format](../../README.md#the-file-format) 即为文件内契约:头部块(`# RFC: ` 加上不含日期、与所在文件夹一致的 `Status:` 枚举,其唯一内容是否决原因);按生命周期区分的正文骨架(所有阶段都以 `Problem` 开头;`proposed/` 中使用 `Proposal`/`Acceptance criteria`/`Risks`;`implemented/` 中使用现在时的 `Decision`/`Consequences` 且禁止提案时期标题;`rejected/` 中冻结提案形态);强制的 `Alternatives considered` 章节;以及规范的章节词汇表——在这些固定章节之间,自定义的技术章节保持自由格式。`pnpm run verify-rfc-format`([scripts/verify-rfc-format.ts](../../../../scripts/verify-rfc-format.ts))作为 doc-sync(文档同步门禁)的一环强制执行每一条机械化条款,因此跳过改写的生命周期迁移现在会导致 CI 失败,而非依赖评审者的记忆。 +[README.md § The file format](../../README.md#the-file-format) 即文件内契约:头部块(`# RFC: <title>` 加上无日期、与所在文件夹一致的 `Status:` 枚举,唯一的正文内容是 rejection reason);按生命周期区分的正文骨架(所有阶段都以 `Problem` 开头;`proposed/` 中为 `Proposal`/`Acceptance criteria`/`Risks`;`implemented/` 中为现在时态的 `Decision`/`Consequences` 且禁止 proposal 时期的标题;`rejected/` 中冻结 proposal 形态);必须包含 `Alternatives considered` 章节;以及规范的章节词汇表,其间的自定义技术章节保持自由形式。`pnpm run verify-rfc-format`([scripts/verify-rfc-format.ts](../../../../scripts/verify-rfc-format.ts))作为 doc-sync(文档同步门禁)的一环强制执行每条机械化条款,因此生命周期迁移时跳过改写现在会让 CI 失败,而不是依赖评审者的记忆。 -整个语料库在定义格式的同一个变更中完成了规范化——这是预发布阶段的立场:不设过渡期,不容忍双格式并存。唯一的祖父条款针对内容而非格式:替代方案只记录已有的,不凭空编造;因此如果一篇格式定义之前的 RFC 的替代方案无法从记录中重建,它会携带 `rfc-format: alternatives-not-recorded` 注释,门禁仅对日期早于本 RFC 的文件接受该注释。 +整个语料库在定义格式的同一个变更中完成了规范化,这是预发布阶段的立场:没有过渡期,不容忍双格式并存。唯一的祖父条款针对内容而非格式:替代方案是记录下来的,不是凭空编造的;因此如果一篇格式定义前的 RFC 的替代方案无法从记录中重建,它会携带 `rfc-format: alternatives-not-recorded` 这条精确注释,门禁仅对日期早于本 RFC 的文件接受该注释。 ## 曾考虑的替代方案 -- **完全刚性模板**(每个生命周期一套固定章节顺序,所有 RFC 重构以适配):否决。大型设计 RFC 携带八到十五个自定义技术章节(包拓扑、协议格式契约、schema),这些是承重内容而非漂移;刚性顺序会迫使当下进行破坏性改写,并永远与模板对抗。 -- **仅规范化头部**(H1 与 Status,正文不动):否决。技术债标记指出的正是*正文*的体裁分裂,让 `Context`/`Decision` 与 `Problem`/`Proposal` 无限期并存什么也解决不了。 -- **不设 Status 行**(文件夹本身就是状态;三篇最新的格式定义前 RFC(及其中一篇的中文对侧文件)省略了该行):否决,保留自描述文件。当初促使去掉该行的漂移风险,已被「门禁将该行与文件夹做一致性校验」所消除。 -- **带日期的状态**(`Status: implemented (accepted YYYY-MM-DD)`):否决。接受日期属于叙述性历史,写作规则将其排除在文档之外;文件名承载首次提出日期,git 承载其余信息,门禁能检查日期格式但永远无法检查其真实性。 -- **裸 `# <title>` H1**:否决。`RFC: ` 前缀是语料库中的多数形式,且在文件脱离目录树阅读时能自描述体裁;索引生成器会剥离它,因此索引行无论哪种写法都一样。 -- **`## What we give up` 作为已实施记录的收尾章节**(README 自身用来描述 RFC 所记录内容的措辞):否决。它只命名了代价,而诚实的后果章节同时记录权衡所换来的收益。 -- **约定而无门禁**(写下契约,靠评审强制执行):否决。slop checklist 已通过约定禁止在 `implemented/` 中使用规范体措辞,而十九个文件展示了纯靠约定在这里能达到什么效果。 -- **独立的 `FORMAT.md` 契约文件**:最初落在此处;在生成索引迁出至 [INDEX.md](../../INDEX.md) 后折入 README.md:表格移走后 README 重新有了空间,一个前门同时承载布局、分类与格式,优于将契约拆分到两个文件。 +- **完全刚性的模板**(每个生命周期一个固定章节序列,所有 RFC 重构以适配):否决。大型设计 RFC 包含八到十五个自定义技术章节(包拓扑、协议格式契约、schema),这些是承重内容而非漂移;刚性序列会立即强制破坏性改写,并永远带来与模板的对抗。 +- **仅规范化头部**(H1 和 Status,正文不动):否决。债务标记指出的是*正文*的体裁分裂,让 `Context`/`Decision` 与 `Problem`/`Proposal` 无限期并存什么也解决不了。 +- **不设 Status 行**(文件夹本身就是状态;格式定义前最新的三篇 RFC(以及其中一篇的中文对侧文件)省略了该行):否决,保留自描述文件。省略 Status 行的动机是防止漂移,而将该行与文件夹做门禁校验即可消除漂移风险。 +- **带日期的 Status**(`Status: implemented (accepted YYYY-MM-DD)`):否决。接受日期属于叙述性历史,写作规则将其排除在文档之外;文件名承载首次提出日期,git 承载其余信息;门禁能检查日期格式,但永远无法检查其真实性。 +- **裸 `# <title>` H1**:否决。`RFC: ` 前缀是语料库中的多数形式,且在文件脱离目录树被阅读时能自描述体裁;索引生成器会剥离前缀,因此索引行无论哪种写法都一样。 +- **`## What we give up` 作为 implemented 的结尾章节**(README 自身对 RFC 记录内容的措辞):否决。它只命名了代价,而诚实的后果章节同样记录这笔权衡换来了什么。 +- **只有约定没有门禁**(写下契约,靠评审强制执行):否决。slop checklist 已经通过约定禁止在 `implemented/` 中使用 spec 语气,而十九个文件展示了仅靠约定在此处能达到什么效果。 +- **独立的 `FORMAT.md` 契约文件**:最初的落地位置;在生成索引迁出到 [INDEX.md](../../INDEX.md) 之后折入 README.md:表格移走后 README 重新有了空间,一个前门同时承载布局、分类和格式,优于将契约拆分到两个文件。 ## 后果 -每篇 RFC 现在多了少许结构成本,而强制的 `Alternatives considered` 章节是有意为之的摩擦:一个不记录被否决方案的决策,会招来 RFC 本应防止的反复讨论。格式定义前的 RFC 若其替代方案无法重建,则永久携带祖父条款注释——这是记录上的诚实空白,而非编造的理由。doc-sync 新增一道门禁,在生命周期文件夹之间迁移 RFC 现在是迁移时的实际工作(即迁移本就欠下的正文改写),而非无人追踪的延后清理。三十九个技术债标记已全部消除,由它们等待的模板所解决。 +每篇 RFC 现在需要略多一些结构,而必须包含 `Alternatives considered` 章节是刻意的摩擦:一个没有记录被否决方案的决策,会招致 RFC 本来就是为了防止的重新争论。格式定义前的 RFC 如果替代方案无法重建,则永久携带祖父条款注释,这是记录上的诚实缺口,而非编造的理由。`doc-sync` 增加一道门禁,将 RFC 在生命周期文件夹之间迁移现在是迁移时的实际工作(即迁移本就欠下的正文改写),而非无人追踪的延后清理。三十九个债务标记已全部消除,由它们等待的模板所解决。 diff --git a/docs/rfc/implemented/process/2026-07-06-export-surface-jsdoc-gate.i18n.yaml b/docs/rfc/implemented/process/2026-07-06-export-surface-jsdoc-gate.i18n.yaml index dd08d60974..b79ff4cf97 100644 --- a/docs/rfc/implemented/process/2026-07-06-export-surface-jsdoc-gate.i18n.yaml +++ b/docs/rfc/implemented/process/2026-07-06-export-surface-jsdoc-gate.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-06-export-surface-jsdoc-gate.md: fd255cc6212d7f6f919ad23f999fa68b01478a73 -2026-07-06-export-surface-jsdoc-gate.zh.md: 02a5c2682095c66818a51cc14fd565111b7e6e80 +2026-07-06-export-surface-jsdoc-gate.zh.md: 796b5bb8f3a2a890986c73511bb63e167c831a5f diff --git a/docs/rfc/implemented/process/2026-07-06-export-surface-jsdoc-gate.zh.md b/docs/rfc/implemented/process/2026-07-06-export-surface-jsdoc-gate.zh.md index 02a5c26820..796b5bb8f3 100644 --- a/docs/rfc/implemented/process/2026-07-06-export-surface-jsdoc-gate.zh.md +++ b/docs/rfc/implemented/process/2026-07-06-export-surface-jsdoc-gate.zh.md @@ -1,45 +1,45 @@ # RFC:导出表面 JSDoc 门禁 -Status: implemented - [English](2026-07-06-export-surface-jsdoc-gate.md) | 中文 +Status: implemented + ## 问题 -[cordis JSDoc 完整性门禁](2026-07-04-cordis-jsdoc-completeness-gate.md)使 cordis 表面上的未文档化参数和返回值不再可能——`interface Events` 成员与 `ctx.<key>` 服务类——但那只是插件作者所导入内容的一小部分。AGENTS.md 中「每个导出(及非显而易见的方法)都应有 JSDoc 说明语义」这条规则在其他地方仍然只能靠评审以行文方式检查,而且没有任何机制要求普通导出函数带 `@param`/`@returns`。采纳时的一次调查发现 34 个包中有 203 个文档不完整的模块级导出:seam 相关辅助函数(`runBash`、`readForEdit`、`htmlToMarkdown`)、格式编解码器、整个未文档化的接口和类型别名——正是 IDE 消费方悬停时看到的那些名字。 +[Cordis JSDoc 完整性门禁](2026-07-04-cordis-jsdoc-completeness-gate.md)使得 Cordis 表面上的参数和返回值不可能缺少文档——`interface Events` 成员和 `ctx.<key>` 服务类——但这只是插件作者所导入内容的一小部分。AGENTS.md 中的规则「每个导出(以及非显而易见的方法)都必须有解释语义的 JSDoc」在其他地方只能靠评审以行文方式检查,而且没有任何机制要求普通导出函数带 `@param`/`@returns`。采纳时的一次调查发现 34 个包(package)中有 203 个文档不完整的模块级导出:seam 相关辅助函数(`runBash`、`readForEdit`、`htmlToMarkdown`)、格式编解码器、完全无文档的接口和类型别名——恰恰是 IDE 消费方悬停查看的那些名称。 ## 决策 -新增门禁 `scripts/verify-export-jsdoc.ts`(`pnpm run verify-export-jsdoc`,接入 doc-sync,与 `verify-cordis-catalog` 并列),遍历每个 `packages/<group>/<pkg>/src/` 目录树下所有模块级导出名。解析与检查辅助函数从 `gen-cordis-catalog.ts` 移入共享的 `scripts/jsdoc.ts`,因此「已文档化」在两个表面上含义一致:描述文本在第一个块标签处截止,每个可检查参数需要非空 `@param`,非 void 的**已标注**返回值需要非空 `@returns`,过时的 `@param` 报错,违规汇总为一份报告。 +新增门禁 `scripts/verify-export-jsdoc.ts`(`pnpm run verify-export-jsdoc`,接入 `doc-sync`(文档同步门禁),与 `verify-cordis-catalog` 并列),遍历每个 `packages/<group>/<pkg>/src/` 目录树下的所有模块级导出名称。解析与检查辅助函数从 `gen-cordis-catalog.ts` 移入共享的 `scripts/jsdoc.ts`,使得「已文档化」在两个表面上含义一致:描述性文字在第一个块标签处截止、每个可检查参数需要非空 `@param`、非 void 且有显式标注的返回值需要非空 `@returns`、过时的 `@param` 报错,违规项汇总为一份报告。 按声明类型划分的契约: -- 每个导出名都需要带有非空描述文本的 JSDoc。 -- 函数类导出(函数声明;以函数初始化器或行内可调用标注的 const;非标识符的函数默认导出)遵循完整的函数契约,分类前会剥离包装表达式(括号、`as`/`satisfies` 转型、非空断言)。如果 const 的声明器标注了一个**命名**类型(`export const f: Handler = …`),则签名契约推迟到该类型自身的声明,`@returns` 可选;行内 `(x: T) => U` 标注或单调用签名字面量即为表面签名本身,适用完整契约;而字面量中混合了调用/构造签名与其他成员的情况则直接拒绝(没有单一签名可供标签对照——请提取命名类型)。 -- 导出类需要类级别的描述文本;公开方法(包括静态方法——可通过导出名访问)遵循函数契约;公开属性和访问器需要描述文本(get/set 对由 getter 覆盖)。重载实现免检——由签名承载文档。 -- 导出的接口、类型别名和枚举需要声明级别的描述文本;成员级别的强制有意推迟(承载关键成员契约的 seam 服务类已在 cordis 门禁下)。 -- 导出的命名空间递归检查(在 ambient `declare` 命名空间内,每个成员隐式导出);命名空间本身仅在不与同名已文档化声明合并时才需要描述文本(Config 命名空间惯用法只需文档化插件一次)。 -- `declare module` / `declare global` 体和 `export … from` 再导出语句被跳过:augmentation 不是包的导出,再导出的定义在其定义处检查。`export import X = N.member` 别名文档化**自身**——其目标可能是遍历不会访问的非导出命名空间成员——且仅支持纯描述文本的目标类型:可调用、类或命名空间目标携带别名描述文本无法承载的签名/成员契约,门禁拒绝此类情况并要求直接导出该声明。 -- 其余一切按**封闭**原则失败:`export =` 直接拒绝;基类从未命名的参数即使作为绑定模式仍保留 `@param` 义务;调度未识别的导出语句类型本身即为违规——没有任何导出形式能因遗漏而免检。 +- 每个导出名称都需要带有非空描述文字的 JSDoc。 +- 函数类导出(函数声明;初始化器为函数或带有内联可调用标注的 const;非标识符的函数默认导出)遵循完整的函数契约,分类前会剥离包装表达式(括号、`as`/`satisfies` 类型断言、非空断言)。如果 const 声明器标注了一个具名类型(`export const f: Handler = …`),签名契约推迟到该类型自身的声明处,`@returns` 保持可选;内联的 `(x: T) => U` 标注或单调用签名字面量本身就是表面签名,适用完整契约;而混合了调用/构造签名与其他成员的字面量则直接拒绝(没有单一签名可供标签对照——请提取具名类型)。 +- 导出类需要类级别的描述文字;公开方法(包括静态方法——可通过导出名称访问)遵循函数契约;公开属性和访问器需要描述文字(get/set 对由 getter 覆盖)。重载实现体免检——签名承载文档。 +- 导出接口、类型别名和枚举需要声明级别的描述文字;成员级别的强制有意推迟(承载关键成员契约的 seam 服务类已在 Cordis 门禁之下)。 +- 导出命名空间递归检查(在 ambient `declare` 命名空间内,每个成员隐式导出);命名空间本身仅在不与同名的已文档化声明合并时才需要描述文字(Config-namespace 惯用法只需文档化插件一次)。 +- `declare module`/`declare global` 体和 `export … from` 重导出语句被跳过:augmentation 不是包的导出,重导出的定义在其定义处检查。`export import X = N.member` 别名需要文档化**自身**——其目标可能是遍历不会访问的非导出命名空间成员——且门禁仅支持纯描述文字的目标类型:可调用、类或命名空间目标携带别名描述文字无法承载的签名/成员契约,门禁会拒绝并要求直接导出该声明。 +- 其余情况按封闭原则失败:`export =` 直接拒绝;基类从未命名的参数即使作为绑定模式仍需 `@param`;dispatch 不识别的导出语句类型本身就是违规——没有任何导出形式能因遗漏而免检。 -三类豁免避免门禁要求样板代码,精神与 cordis 门禁的 `this`/`next` 豁免一致(对已豁免的名字主动写文档是允许的;只有缺失才不被检查): +三类豁免避免门禁要求样板代码,精神与 Cordis 门禁的 `this`/`next` 豁免一致(为已豁免的名称编写文档是允许的;只有缺失才不被检查): -- **继承成员。**重写从基类声明继承文档。新增的公开表面仍需文档:新增参数、将 protected 成员公开重写、或在 void 基类之上给出具体返回值。继承查找与推断返回值分类是门禁唯一需要类型检查器的工作;其他检查使用 AST。 -- **插件协议槽位。**顶层 `name` / `inject` / `reusable` / `Config` const 与 `apply` 入口,以及插件类上作为静态成员的相同槽位,属于框架协议:其形状由 cordis 固定,模块文档注释加 `interface Config` 承载插件的真实语义。 -- **构造函数**,与 cordis 门禁一致:插件类由框架构造,类文档承载全部说明。 +- **继承成员。** 重写从其基类声明继承文档。新增的公开表面仍需文档:新增参数、将 protected 成员公开重写、或在 void 基类之上返回具体类型。继承查找和推断返回值分类是门禁唯一需要类型检查器的工作;其他检查使用 AST。 +- **插件协议槽位。** 顶层的 `name`/`inject`/`reusable`/`Config` 常量和 `apply` 入口,以及插件类上的同名静态成员,属于框架协议:其形状由 Cordis 固定,模块文档注释加 `interface Config` 承载插件的真实语义。 +- **构造函数**,与 Cordis 门禁一致:插件类由框架构造,类文档承载全部说明。 -`collectExportJsdocViolations()` 返回违规列表(CLI 在非空时以 exit 1 退出),因此 `packages/core/agent/tests/verify-export-jsdoc.spec.ts` 中的负路径测试直接对发现结果断言,通过 fixture 包驱动每一种拒绝和每一种豁免。 +`collectExportJsdocViolations()` 返回违规列表(CLI 在非空时以 1 退出),因此 `packages/core/agent/tests/verify-export-jsdoc.spec.ts` 中的负路径测试直接断言发现项,通过 fixture(测试前置数据)包驱动每一种拒绝和每一种豁免。 ## 曾考虑的替代方案 -- **eslint-plugin-jsdoc**(`require-jsdoc`/`require-param`/`require-returns`):覆盖了机械核心,但无法表达本仓库的契约:继承成员豁免需要跨包类型解析,协议槽位和命名空间合并惯用法是 cordis 特有的,而完整性语义(标签前描述文本、过时标签报错、聚合报告)已在 `scripts/jsdoc.ts` 中与 catalog 生成器共享一处。两套微妙不同的「已文档化」定义正是本仓库「一处为家」规则要防止的失败模式。 -- **扩展 `gen-cordis-catalog.ts`**:catalog 生成器渲染一个精选表面并门禁其新鲜度(freshness);仓库级遍历没有 catalog 可渲染。共享辅助函数但保持遍历分离,使每个门禁的职责清晰可读。 -- **强制接口/类型别名的成员文档**:推迟。这会将检查表面扩大到大量自描述字段,而承载关键成员契约的 seam 类已在门禁下。如果评审中出现成员文档漂移再重新考虑。 +- **eslint-plugin-jsdoc**(`require-jsdoc`/`require-param`/`require-returns`):覆盖了机械核心,但无法表达本仓库的契约。继承成员豁免需要跨包的类型解析,协议槽位和命名空间合并惯用法是 Cordis 特有的,而完整性语义(标签前描述文字、过时标签报错、汇总报告)已在 `scripts/jsdoc.ts` 中与 catalog 生成器共享。两套微妙不同的「已文档化」定义,正是本仓库「单一归属」规则所要防止的失败模式。 +- **扩展 `gen-cordis-catalog.ts`**:catalog 生成器渲染一个精选表面并守卫其新鲜度;仓库级遍历没有 catalog 可渲染。共享辅助函数、保持遍历独立,使每个门禁的职责清晰可读。 +- **强制接口/类型别名的成员文档**:推迟。这会使检查表面成倍增长,而这些成员大多是自描述的字段;承载关键成员契约的 seam 服务类已有门禁。如果评审中出现成员文档漂移再重新考虑。 ## 后果 -- 新导出不能在无文档的情况下落地:`verify-export-jsdoc` 使 doc-sync 失败,而 pre-push 和 CI 已经运行 doc-sync。采纳时发现的 203 处缺口在同一个变更中补齐,因此门禁以绿色状态落地。 -- 导出函数必须标注返回类型(采纳时已全面覆盖,现在成为承载性要求),且在 `@param` 需要命名的地方使用标识符参数。 -- seam 文档是权威的:实现从继承链继承文档,值得保留在实现上的行为说明是补充,而非必需。 -- 门禁构建一个 `ts.Program`(约 6 秒)——唯一需要类型解析的文档门禁;在已经编译文档片段的 doc-sync 中可以接受。 -- 协议槽位名在模块顶层按约定保留;一个碰巧名为 `apply` 或 `Config` 的非协议导出会免检——已接受,记录于此。 +- 新增导出不能在无文档的情况下合入:`verify-export-jsdoc` 使 `doc-sync` 失败,而 pre-push 和 CI 已运行 `doc-sync`。采纳时发现的 203 处缺口在同一个变更中补齐,门禁以绿色状态落地。 +- 导出函数必须标注返回类型(采纳时已全面满足,现在成为门禁依赖),并在 `@param` 需要命名参数时使用标识符参数。 +- seam 文档是权威的:实现从其继承链继承文档,值得保留在实现上的行为说明是补充,而非必需。 +- 门禁构建一个 `ts.Program`(约 6 秒)——唯一需要类型解析的文档门禁;在已编译文档片段的 `doc-sync` 内可以接受。 +- 协议槽位名称按约定保留在模块顶层;一个恰好命名为 `apply` 或 `Config` 的非协议导出将不被检查——已接受,记录于此。 diff --git a/docs/rfc/implemented/process/2026-07-06-generated-config-catalog.i18n.yaml b/docs/rfc/implemented/process/2026-07-06-generated-config-catalog.i18n.yaml index 394c2494ac..ccfbb1cc49 100644 --- a/docs/rfc/implemented/process/2026-07-06-generated-config-catalog.i18n.yaml +++ b/docs/rfc/implemented/process/2026-07-06-generated-config-catalog.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-06-generated-config-catalog.md: 50ba0dc70b0d8ea54a4f93911f3a087806774626 -2026-07-06-generated-config-catalog.zh.md: 2dd0cbe5303f08aa2f3300c6f8613a7823c2bc78 +2026-07-06-generated-config-catalog.zh.md: 87a861bab394ec268fb3c870e848db37fa4d6fcf diff --git a/docs/rfc/implemented/process/2026-07-06-generated-config-catalog.zh.md b/docs/rfc/implemented/process/2026-07-06-generated-config-catalog.zh.md index 2dd0cbe530..87a861bab3 100644 --- a/docs/rfc/implemented/process/2026-07-06-generated-config-catalog.zh.md +++ b/docs/rfc/implemented/process/2026-07-06-generated-config-catalog.zh.md @@ -6,35 +6,35 @@ Status: implemented ## 问题 -仓库此前没有以源码为后盾的插件配置参考。各 package 的 README 对字段的记录方式不一致,没有列举哪些包可被加载,也没有校验运行时 schema 是否与声明的配置类型一致。 +仓库此前没有以源码为后盾的插件配置参考。各 package 的 README 对字段的记录方式不一致,未列举哪些包可被加载,也未校验运行时 schema 与声明的配置类型是否一致。 ## 决策 -`scripts/gen-config-catalog.ts` 从每个插件声明的配置类型与 JSDoc 生成 [docs/config-catalog.md](../../../config-catalog.md),包含注入要求、引用类型链接和源码指针。package 内部类型被传递性地包含;workspace 和外部类型以链接或名称引用。确定性的 `--write` 和 `--check` 模式使提交到仓库的页面成为一个生成产物(artifact)。 +`scripts/gen-config-catalog.ts` 从每个插件声明的配置类型与 JSDoc 生成 [docs/config-catalog.md](../../../config-catalog.md),包含注入要求、引用类型链接和源码指针。包内局部类型被传递性地包含;workspace 和外部类型以链接或名称形式引用。确定性的 `--write` 和 `--check` 模式使提交到仓库的页面成为一个生成产物。 -此处采用纯 AST 生成是正确的,原因与事件/服务目录相同,也与工具目录不同:配置类型是静态声明,仓库中每个 schemastery schema 都是静态的 `z.object`/`z.intersect` 字面量,因此源码就是全部真相——配置表面没有任何部分是运行时组合的。 +此处采用纯 AST 生成是正确的,原因与 events/services catalog 相同,而与 tool catalog 不同:配置类型是静态声明,仓库中每个 schemastery schema 都是静态的 `z.object`/`z.intersect` 字面量,因此源码即全部真相——配置表面没有任何部分是运行时组合的。 具体选择: -- **配置类型取自第二参数类型。** 目录记录的是 `apply(ctx, config)` / 服务构造函数 `(ctx, config)` 的声明参数类型——即 Cordis 实际传入的值——而非按命名约定定位的 `Config` 导出。这使得遍历是全量的:无论接口名为 `AcpConfig` 还是 `BasicCompactConfig`,无论类型声明在兄弟文件中,还是插件根本没有校验 schema,都能正常工作。 -- **分类是全量的。** 每个 `packages/<group>/<pkg>` 条目都会被解析(镜像 Loader 的 `unwrapExports`(`exports.default ?? exports`)),归入以下之一:可配置插件、无配置插件、抽象 seam 类或库——各自渲染在独立章节中——无法归类的条目会硬错误。新 package 不可能被静默地遗漏。 -- **逐字段 JSDoc 强制要求。** 粘贴的声明中每个属性(包括嵌套的类型字面量)都需要非空的 JSDoc 描述,否则生成失败。粘贴本身就是文档,因此这与事件目录通过 `@mode` 施加的强制函数相同:源码文档不足时门禁失败,而非产出一份单薄的目录。 -- **Schema 键与声明类型交叉检查。** 生成器通过本地和 workspace 类型解析嵌套的对象与数组路径。确定缺失的路径会失败;无法枚举的外部或动态形状则跳过。检查有意设计为单向的,因为声明类型可能包含从 loader 配置中排除的运行时专用字段。 +- **配置类型是第二参数的类型。** catalog 记录的是 `apply(ctx, config)` / 服务构造函数 `(ctx, config)` 的声明参数类型——即 Cordis 实际传入的值——而非按命名约定定位的 `Config` 导出。这使得遍历是全量的:无论接口叫 `AcpConfig` 还是 `BasicCompactConfig`,无论类型声明在兄弟文件中,还是插件完全没有验证 schema,都能正常工作。 +- **分类是全量的。** 每个 `packages/<group>/<pkg>` 条目都会被解析(镜像 Loader 的 `unwrapExports`:`exports.default ?? exports`),归入可配置插件、无配置插件、抽象 seam 类或库之一——各自渲染在独立小节中——无法归类的条目直接报错。新 package 不可能被悄悄遗漏。 +- **逐字段 JSDoc 强制要求。** 粘贴的声明中每个属性(包括嵌套的类型字面量)都需要非空的 JSDoc 描述,否则生成失败。粘贴本身就是文档,因此这与 events catalog 通过 `@mode` 施加的强制函数相同:源码文档过于单薄时门禁报错,而非产出单薄的 catalog。 +- **Schema 键与声明类型做比对。** 生成器通过局部和 workspace 类型解析嵌套的对象与数组路径。确定缺失的路径报错;无法枚举的外部或动态形状则跳过。比对有意设计为单向的,因为声明类型可能包含被排除在 loader 配置之外的运行时专用字段。 - **专用围栏。** 粘贴的声明使用 ` ```ts config-catalog ` 信息字符串,`doc-typecheck` 会跳过它(引用了导入类型的孤立声明无法独立编译),并将其排除在 opt-out 比例之外——与 `cordis-catalog` 和 `persistence-catalog` 围栏的处理方式相同。 -- **单文件 `docs/config-catalog.md`**,而非一个单文件目录:该页面服务于单一受众(`cordis.yml` 的编写者),只有一个维度,不同于 `cordis-catalog/`(它包含两个并列页面)。 +- **单文件 `docs/config-catalog.md`**,而非一个单文件目录:该页面面向单一受众(`cordis.yml` 的编写者),只有一个维度,不同于 `cordis-catalog/`(其中包含两个并列页面)。 -各 package README 的 `## Config` 章节保留。这种重叠是有意接受的:README 是精心策划的逐 package 契约(部署上下文中的配置语义,连同限制与扩展点),目录则是穷举式的生成枚举。因为目录是生成的,二者之间的分歧说明 README 有误,修复方式是编辑 README——目录不会漂移。 +各 package README 中的 `## Config` 小节保留。重叠是有意接受的:README 是经过策划的逐包契约(在部署上下文中描述配置语义,连同限制与扩展点),catalog 则是穷举式的生成枚举。由于 catalog 是生成的,二者不一致时说明 README 有误,修复方式是编辑 README——catalog 不会漂移。 ## 曾考虑的替代方案 -- **合成式逐字段渲染**:为每个字段生成一个项目符号列表、表格或带注释的 YAML 片段,由解析后的 JSDoc 加 schema 元数据组装。否决,改用逐字粘贴:带 JSDoc 的接口本身就是以其原始形式撰写的契约,合成渲染器会重新格式化它不拥有的行文,增加一层可能歪曲原意的渲染。 -- **运行时启动 + schema 内省(如工具目录的做法)**:否决。此处没有任何内容是运行时组合的,而且 schema 本身对配置表面的文档化不足(行文记录的默认值、运行时专用字段、完全没有 schema 的插件)。启动只会增加脆弱性而不增加真相。 -- **双向 schema/接口相等性检查**:否决,改用子集检查。声明类型合理地包含 schema 拒绝从配置接受的成员(运行时专用的 seam)。 -- **在同一变更中废弃 README 的 `## Config` 章节**:否决。接受的重叠使逐 package 契约在原地可读,而一次清扫需要先把每个 README 的额外事实折入字段 JSDoc——这是可分离的工作,目录不依赖它。 +- **合成式逐字段渲染**:为每个字段生成项目符号列表、表格或带注释的 YAML 片段,从解析的 JSDoc 加 schema 元数据组装。否决,改用逐字粘贴:接口连同其 JSDoc 本身就是以原始形式撰写的契约,合成渲染器会重新格式化它不拥有的行文,增加一个可能歪曲原意的渲染层。 +- **运行时启动 + schema 内省(如 tool catalog 所做的那样)**:否决。此处没有任何内容是运行时组合的,且 schema 本身对配置表面的文档化不足(以行文记录的默认值、运行时专用字段、完全没有 schema 的插件)。启动只会增加脆弱性而不增加真相。 +- **双向 schema/接口等价检查**:否决,改用子集检查。声明类型合理地包含 schema 拒绝从配置接受的成员(运行时专用 seam)。 +- **在同一变更中废除 README `## Config` 小节**:否决。保留可接受的重叠使逐包契约在原处可读,而清理工作需要先把每个 README 的额外事实折入字段 JSDoc——这是可分离的工作,catalog 不依赖它。 ## 后果 -- 目录不会漂移:源码变化而提交的文件未反映时,`verify-config-catalog` 在 pre-push 和 CI 中失败。未记录的配置字段、无法解析的引用类型名称、或 schema 键在配置类型中缺失,都会导致生成器直接报错。 -- 配置行文现在在声明处有了强制函数:编写新的配置字段意味着编写其 JSDoc,而 JSDoc 会逐字成为目录条目。 -- 生成器对无法静态遍历的形状硬错误——别名化的 package 内部配置导入、非 `object`/`intersect` 组合构建的 schema、未列入的全局类型名。引入这样的形状就必须同时教会生成器(否则该形状不能进入仓库),这正是设计意图:目录始终是全部真相。 -- `gen-cordis-catalog.ts` 导出其 JSDoc/指针辅助函数与 `LINK_MAP` 供复用,因此两个目录以相同方式交叉链接类型,新增一条 link-map 条目同时服务于两者。 +- catalog 不会漂移:源码变更而提交的文件未反映时,`verify-config-catalog` 在 pre-push 和 CI 中报错。未文档化的配置字段、无法解析的引用类型名、或 schema 键在配置类型中缺失,都会直接导致生成器报错。 +- 配置行文现在有了声明处的强制函数:编写新配置字段意味着编写其 JSDoc,而该 JSDoc 将逐字成为 catalog 条目。 +- 生成器对无法静态遍历的形状直接报错——别名化的包内配置导入、非 `object`/`intersect` 组合构建的 schema、未列入的全局类型名。引入此类形状时必须同时教会生成器(否则该形状不能进入仓库),这正是设计意图:catalog 始终是全部真相。 +- `gen-cordis-catalog.ts` 导出其 JSDoc/指针辅助函数与 `LINK_MAP` 供复用,因此两个 catalog 以相同方式交叉链接类型,新增一条 link-map 条目同时服务于两者。 diff --git a/docs/rfc/implemented/process/2026-07-06-node-engine-floor.i18n.yaml b/docs/rfc/implemented/process/2026-07-06-node-engine-floor.i18n.yaml index d7db5cac08..815cf98e72 100644 --- a/docs/rfc/implemented/process/2026-07-06-node-engine-floor.i18n.yaml +++ b/docs/rfc/implemented/process/2026-07-06-node-engine-floor.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-06-node-engine-floor.md: 561b6b4a124b6eaa8e2ba0756a835e35519b30b8 -2026-07-06-node-engine-floor.zh.md: c6ace7a1296b5e3049ff4ef55f29b689fce7f4ff +2026-07-06-node-engine-floor.zh.md: 21af2da919754b1ae4667b46ef2f65c14a279b49 diff --git a/docs/rfc/implemented/process/2026-07-06-node-engine-floor.zh.md b/docs/rfc/implemented/process/2026-07-06-node-engine-floor.zh.md index c6ace7a129..21af2da919 100644 --- a/docs/rfc/implemented/process/2026-07-06-node-engine-floor.zh.md +++ b/docs/rfc/implemented/process/2026-07-06-node-engine-floor.zh.md @@ -1,39 +1,39 @@ # RFC:将 Node LTS 引擎下限提升至 22.19 -Status: implemented - [English](2026-07-06-node-engine-floor.md) | 中文 +Status: implemented + ## 问题 -根 `engines.node` 范围中的 Node 22 分支是对已安装工作区的契约,而不仅仅是 harness 源码直接调用的运行时 API 的契约。该分支的下限不得低于工作区在该分支上安装的依赖所声明的 package `engines.node`;否则 `pnpm install --engine-strict` 会在一个被宣传的 LTS 版本上失败,而非严格模式的安装则会在依赖所支持的运行时范围之外运行。 +根 `engines.node` 范围中的 Node 22 分支是对已安装工作区的契约,而不仅仅是 harness 源码直接调用的运行时 API 的契约。它不得低于工作区在该分支上安装的依赖所声明的 package `engines.node`;否则 `pnpm install --engine-strict` 会在一个已宣传的 LTS 版本上失败,而非严格模式的安装则会在依赖所支持的运行时范围之外运行。 ## 决策 -将 `engines.node` 设为 `^22.19.0 || >=24.0.0`,并在 keyless CI 兼容性矩阵中测试 `['22.19', 24, 26]`。每个矩阵分支都运行 TypeScript 类型检查加一次 keyless 的源码模式 worker 冒烟测试,因此引擎下限同时通过完整的源码类型检查和真实的未构建运行时路径得到验证。真实 API 的 e2e 工作流保持在 Node 24 上运行,因为它验证的是 API 集成而非运行时下限。 +将 `engines.node` 设为 `^22.19.0 || >=24.0.0`,并在 keyless CI 兼容性矩阵中测试 `['22.19', 24, 26]`。每条矩阵分支都运行 TypeScript 类型检查加一次 keyless 的源码模式 worker 冒烟测试,因此引擎下限通过完整的源码类型检查和真实的未构建运行时路径两条路径得到验证。真实 API 的 e2e 工作流保持在 Node 24 上,因为它验证的是 API 集成而非运行时下限。 -两项 Node 特性决定了源码运行时的下限: +两个 Node 特性决定了源码运行时的门槛: -- **`node:sqlite`**:`packages/session-persistence/session-persistence-sqlite` 在顶层执行 `import { DatabaseSync } from 'node:sqlite'`。该模块在 **22.13**(LTS)和 **23.4**(Current)取消了 `--experimental-sqlite` flag 要求;在此之前,导入它会在加载时抛出异常。 -- **原生 TypeScript 类型剥离**:`packages/examples/stdio-demo/tests/built-bin.e2e.ts` 冒烟测试在纯 `node`(无 tsx)下启动已发布的 `lib/bin.js`,并加载示例的 `.ts` 插件(`mock-llm.ts`、`echo-tool.ts`)。类型剥离从 **22.18**(LTS)和 **23.6**(Current)起成为默认行为;在此之前需要 `--experimental-strip-types`。 +- **`node:sqlite`**:`packages/session-persistence/session-persistence-sqlite` 在顶层执行 `import { DatabaseSync } from 'node:sqlite'`。该模块在 **22.13**(LTS)和 **23.4**(Current)取消了 `--experimental-sqlite` 标志要求;在此之前,导入它会在加载时抛出异常。 +- **原生 TypeScript 类型剥离**:`packages/examples/stdio-demo/tests/built-bin.e2e.ts` 冒烟测试在纯 `node`(不用 tsx)下启动已发布的 `lib/bin.js`,并加载示例的 `.ts` 插件(`mock-llm.ts`、`echo-tool.ts`)。类型剥离从 **22.18**(LTS)和 **23.6**(Current)起成为默认行为;在此之前需要 `--experimental-strip-types`。 -这些源码特性在 22.x 线上于 **22.18** 全部就绪,但已安装的 Pi 适配器依赖将宣传的 LTS 下限进一步抬高。`@deepseek-ai/dsh-llm-pi-ai` 依赖 `@earendil-works/pi-ai@0.79.3`,后者的 package 声明 `engines.node >=22.19.0`,因此 LTS 下限为 **22.19**。24.x 分支保持 `>=24.0.0`。该不连续范围完全排除 Node 23:Node 23.0–23.5 仍有至少一项源码特性需要 flag,而 23 线是非 LTS/已 EOL,宣传 `>=23.6` 只会增加一个已死的发布线和一个不应被任何部署使用的 CI 分支。 +这些源码特性在 22.x 线上于 **22.18** 全部就绪,但已安装的 Pi 适配器依赖将宣传的 LTS 下限进一步提高。`@deepseek-ai/dsh-llm-pi-ai` 依赖 `@earendil-works/pi-ai@0.79.3`,后者的 package 声明 `engines.node >=22.19.0`,因此 LTS 下限为 **22.19**。24.x 分支保持 `>=24.0.0`。该不相交范围完全排除了 Node 23:Node 23.0–23.5 至少还有一个源码特性需要标志,而 23 线是非 LTS/已 EOL 的,宣传 `>=23.6` 会增加一条已终止的发布线和一条 CI 分支,而没有任何部署应当使用它。 -`@types/node` 继续固定在 22.x 线(`^22.20.0`),以匹配 LTS 支持线:如果使用了 Node 23+/24+/25+ 才有的 API,`tsc` 会在所有机器和类型检查门禁中报错,而不是编译通过后存活到只有下限矩阵分支才能捕获的运行时失败。整棵树目前在 Node 22 类型表面上类型检查全部通过,因此这个固定没有代价。 +`@types/node` 继续固定在 22.x 线(`^22.20.0`),以匹配 LTS 支持线:使用 Node 23+/24+/25+ 的 API 会在所有机器和类型检查门禁中导致 `tsc` 失败,而不是编译通过、直到仅下限矩阵分支才能捕获的运行时错误才暴露。目前整个代码树在 Node 22 类型表面上类型检查全部通过,因此这一固定没有任何代价。 ## 后果 - 宣传的 LTS 分支不再低于 Pi 适配器依赖的下限。 -- CI 用 Node 22.19 直接验证 Node 22 LTS 下限,Node 24 分支保持 `node: 24`,Node 26 用于下一个偶数线;每个分支都对源码图做类型检查,并实际启动未构建的工作流 worker。 -- built-bin 冒烟测试不需要版本条件 flag:在 22.19 上类型剥离已是默认行为,因此测试保持其文档记录的纯 `node lib/bin.js` 路径。 -- 未来如有依赖或源码 API 抬高运行时下限,必须在同一个变更中同步修改 `engines.node`、兼容性矩阵与本 RFC。 +- CI 通过 Node 22.19 直接验证 Node 22 LTS 下限,Node 24 分支保持 `node: 24`,Node 26 用于下一个偶数线;每条分支都对源码图执行类型检查,并实际启动未构建的工作流 worker。 +- built-bin 冒烟测试无需版本条件标志:在 22.19 上类型剥离已是默认行为,因此测试保持其文档所述的纯 `node lib/bin.js` 路径。 +- 未来如果有依赖或源码 API 提高运行时下限,必须在同一个变更中同步修改 `engines.node`、兼容性矩阵和本 RFC。 ## 曾考虑的替代方案 - **保持 `^22.18.0 || >=24.0.0`。** 否决:它宣传的 LTS 版本低于 Pi 适配器依赖的下限。`@earendil-works/pi-ai@0.79.3` 要求 `>=22.19.0`。 -- **降级或固定 `@earendil-works/pi-ai` 以保留 22.18 的宣传范围。** 否决:当前的 Pi 适配器依赖是工作区的预期组成部分,且 22.19 仍在 Node 22 LTS 线内。 -- **下限设为 `>=22.13`(`node:sqlite` 边界),在 22.13–22.17 的 built-bin 冒烟测试中加 `--experimental-strip-types`。** 否决:为一个窄范围增加版本条件测试 flag,并将对实验性 flag 的依赖伪装成正式支持。Pi 适配器依赖已经要求更高的 LTS 下限。 -- **开放式 `>=22.19`。** 否决:它宣传支持 Node 23.0–23.5,而在这些版本上 `node:sqlite`(直到 23.4)或类型剥离(直到 23.6)仍需 flag。 -- **包含 Node 23.6+(`^22.19.0 || >=23.6.0`)。** 否决:23.6+ 确实能无 flag 运行两项源码特性,但 Node 23 已 end-of-life;宣传一个已死的发布线只会增加一个范围项和一个 CI 分支,用于一个不应被任何部署使用的运行时。 -- **矩阵用 `[22, 24, 26]` 而非固定 `22.19`。** 否决:浮动的主版本号条目会随时间上漂,悄然不再验证所声明的 LTS 下限。 -- **让 `@types/node` 超前于下限(`^25`)。** 否决:类型定义超前于运行时下限会让仅 Node 24/25 才有的 API 编译通过,仅在 22.x 上运行时才失败。将 `@types/node` 固定在 22.x 线上,会把这种情况变成所有环境下的编译错误。 +- **降级或固定 `@earendil-works/pi-ai` 以保留 22.18 的宣传范围。** 否决:当前 Pi 适配器依赖是预期工作区的一部分,且 22.19 仍在 Node 22 LTS 线内。 +- **下限 `>=22.13`(`node:sqlite` 边界)加上在 22.13–22.17 的 built-bin 冒烟测试中使用 `--experimental-strip-types`。** 否决:它为一个狭窄范围增加了版本条件测试标志,并将实验性标志依赖包装为正式支持。Pi 适配器依赖已经要求更高的 LTS 下限。 +- **开放式 `>=22.19`。** 否决:它宣传支持 Node 23.0–23.5,而在这些版本上 `node:sqlite`(直到 23.4)或类型剥离(直到 23.6)仍需标志。 +- **包含 Node 23.6+(`^22.19.0 || >=23.6.0`)。** 否决:23.6+ 确实能无标志运行两个源码特性,但 Node 23 已 end-of-life;宣传一条已终止的发布线会增加一个范围项和一条 CI 分支,而没有任何部署应当使用该运行时。 +- **矩阵 `[22, 24, 26]` 而非固定 `22.19`。** 否决:浮动的主版本号条目会随时间上漂,悄然不再验证所声明的 LTS 下限。 +- **将 `@types/node` 保持在下限之前(`^25`)。** 否决:类型定义超前于运行时下限会让仅 Node 24/25 才有的 API 编译通过,仅在 22.x 上运行时才失败。将 `@types/node` 固定在 22.x 线上可将此类问题转化为所有环境下的编译错误。 diff --git a/docs/rfc/implemented/process/2026-07-06-parallel-github-ci-gates.i18n.yaml b/docs/rfc/implemented/process/2026-07-06-parallel-github-ci-gates.i18n.yaml index 81369745d3..7abfb8a8ed 100644 --- a/docs/rfc/implemented/process/2026-07-06-parallel-github-ci-gates.i18n.yaml +++ b/docs/rfc/implemented/process/2026-07-06-parallel-github-ci-gates.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-06-parallel-github-ci-gates.md: 890adf58ac8a20a39806aa028d035cb253d5a4f1 -2026-07-06-parallel-github-ci-gates.zh.md: 02502b1005a1f8e6f9f539878c792a9f0585e926 +2026-07-06-parallel-github-ci-gates.zh.md: 47562ed6a83b650a3275b4045c39c4de6d2ecac6 diff --git a/docs/rfc/implemented/process/2026-07-06-parallel-github-ci-gates.zh.md b/docs/rfc/implemented/process/2026-07-06-parallel-github-ci-gates.zh.md index 02502b1005..47562ed6a8 100644 --- a/docs/rfc/implemented/process/2026-07-06-parallel-github-ci-gates.zh.md +++ b/docs/rfc/implemented/process/2026-07-06-parallel-github-ci-gates.zh.md @@ -1,41 +1,41 @@ # RFC:并行 GitHub CI 门禁 -Status: implemented - [English](2026-07-06-parallel-github-ci-gates.md) | 中文 +Status: implemented + ## 问题 -keyless GitHub CI 门禁大多彼此正交:类型检查、lint、文档新鲜度、覆盖率、快照回放、构建、包发布卫生检查、demo 冒烟测试和 built-bin 冒烟测试各自因不同原因失败,且不需要彼此的运行时状态。将它们串成一条有序命令链,工作流的挂钟时间等于所有门禁之和;而将每个叶子门禁拆成独立的 GitHub job,则会重复 checkout、Node 搭建、pnpm restore 和 install 工作,直到编排开销本身成为瓶颈。 +keyless GitHub CI 门禁大多相互正交:类型检查、lint、文档新鲜度、覆盖率、快照回放、构建、包发布卫生检查、demo 冒烟测试与 built-bin 冒烟测试各自因不同原因失败,彼此不需要对方的运行时状态。将它们串成一条有序命令链,工作流的挂钟时间等于所有门禁之和;而把每个叶子门禁拆成独立的 GitHub job,则会重复 checkout、Node 设置、pnpm restore 和 install 工作,直到编排开销本身成为瓶颈。 -难点在于产物边界。`publint`、`verify-node-next-types` 和 built-bin 冒烟测试需要构建出的 `lib/` 输出,而大多数门禁只需要源码和依赖。盲目扇出要么让这些产物消费方在 `pnpm run build` 输出声明文件和 bundle 之前就开始执行,要么在每个依赖产物的 job 中重复构建。 +难点在于产物边界。`publint`、`verify-node-next-types` 和 built-bin 冒烟测试需要构建出的 `lib/` 输出,而大多数门禁只需要源码和依赖。盲目扇出要么让这些产物消费方在 `pnpm run build` 输出声明文件和 bundle 之前就开始竞跑,要么在每个依赖产物的 job 中重复构建。 ## 决策 [CI](../../../../.github/workflows/ci.yml) 将 keyless 检查分组为若干宽粒度的主运行时 lane,外加一个兼容性矩阵。工作流文件拥有当前 lane 和运行时清单的定义权。 -每个 lane 委托给 [scripts/run-gates.ts](../../../../scripts/run-gates.ts),后者以有界并发调度独立门禁,并为每个门禁打印一个可归因的结果块。产物消费方在各自 lane 内依赖一次 build;兼容性 job 则将类型检查与一次真实的未构建 worker 启动相结合,以覆盖运行时特定的 loader 行为。 +每个 lane 委托给 [scripts/run-gates.ts](../../../../scripts/run-gates.ts),该脚本以有界并发调度独立门禁,并为每个门禁打印一个可归因的结果块。产物消费方依赖其所在 lane 内的一次 build,而兼容性 job 将类型检查与一次真实的未构建 worker 启动结合,以覆盖运行时特定的 loader 行为。 -生成的 `.sessions/` 日志和 `.doc-typecheck-*` 临时目录被 lint 忽略。聚合的本地 CI 模式仍在 lint 之后运行 demo 冒烟测试;而拆分后的 GitHub 静态 lane 可以直接运行 demo 冒烟测试,因为 lint 已隔离在自己的 lane 中。 +生成的 `.sessions/` 日志和 `.doc-typecheck-*` 临时目录被 lint 忽略。聚合的本地 CI 模式仍在 lint 之后运行 demo 冒烟测试,而拆分后的 GitHub static lane 可以直接运行 demo 冒烟测试,因为 lint 已隔离在自己的 lane 中。 -构建输出在 Node 24 产物 lane 中只生成一次。产物消费方(`publint`、`verify-node-next-types` 和 built-bin 冒烟测试)声明对 `build` 的依赖,因此没有 upload/download 交接,消费方也不可能抢在声明文件或 bundle 之前执行。CI 覆盖率报告仅为文本格式,本地覆盖率则保留 HTML 报告。 +构建输出在 Node 24 的产物 lane 中只生成一次。产物消费方(`publint`、`verify-node-next-types` 和 built-bin 冒烟测试)声明对 `build` 的依赖,因此没有 upload/download 交接,消费方也不可能在声明文件或 bundle 就绪之前抢跑。CI 覆盖率报告仅输出文本,本地覆盖率则保留 HTML 报告。 -两个工作流都缓存 pnpm store。真实 API 工作流使用共享的有界 Vitest 文件池,而非为每组测试单独开 job。 +两个工作流都缓存 pnpm store。真实 API 工作流使用共享的有界 Vitest 文件池,而非为每组测试单独开一个 job。 ## 曾考虑的替代方案 -- **在 Node 矩阵中保留完整串行链**:最容易理解,但会重复执行不产生 Node 版本特定信号的仓库级门禁,且让每个 PR 等待所有门禁之和。 -- **每个门禁各开一个 GitHub job**:最大化 GitHub 可见的扇出,但产生过多 check,且对运行时间短于 runner 准备时间的门禁反复支付 setup/install 开销。 -- **将构建产物上传给依赖产物的 job**:在多 job 间保持正确性,但增加了 artifact upload/download 时间,且在产物消费方可以通过主 job 内的本地依赖运行时仍保持工作流过宽。 -- **并发运行 `typecheck` 和 `build`**:向调度器暴露更多工作,但两者都调用 `tsc -b`;在它们之间共享增量构建状态是一场不必要的竞争,换来的挂钟收益很小。 -- **使用无界的真实 API e2e 并行度**:否决。该套件包含大量真实模型/工具场景;worker 池需要一个显式的 `DSH_E2E_MAX_WORKERS` 上限,这样 CI 和本地运行都能扇出而不会把配额或资源问题隐藏在不稳定的限流失败背后。 +- **在 Node 矩阵中保留完整串行链**:最容易推理,但会重复执行不产生 Node 版本特定信号的仓库级门禁,且让每个 PR 等待所有门禁的总和。 +- **每个门禁作为独立 GitHub job 运行**:最大化 GitHub 可见的扇出,但产生过多 check,且对运行时间短于 runner 准备时间的门禁而言,重复的 setup/install 开销得不偿失。 +- **将构建产物上传给依赖产物的 job**:在多 job 间保持正确性,但增加了 artifact upload/download 时间,且当产物消费方可以在主 job 内通过本地依赖排序运行时,工作流仍然过宽。 +- **并发运行 `typecheck` 与 `build`**:向调度器暴露更多工作,但两个命令都调用 `tsc -b`;在它们之间共享增量构建状态是一场不必要的竞争,换来的挂钟收益很小。 +- **使用无界的真实 API e2e 并行度**:否决。该套件包含大量真实模型/工具场景;worker 池需要一个显式的 `DSH_E2E_MAX_WORKERS` 上限,使 CI 和本地运行都能扇出,同时不会把配额或资源问题隐藏在不稳定的限流失败背后。 ## 后果 -PR 反馈以少量 GitHub check 呈现,每个宽粒度 job 内部包含结构化的逐门禁日志块。这使 runner setup 开销可控、Actions UI 紧凑,代价是失去了每个叶子门禁各自独立的 status check。 +PR 反馈以少量 GitHub check 的形式呈现,每个宽粒度 job 内部包含结构化的逐门禁日志块。这将 runner 设置开销控制在有限范围内,并保持 Actions UI 紧凑,代价是失去了每个叶子门禁独立的状态标记。 -宽粒度 lane 拆分比单一主 job 更频繁地重复 checkout、setup 和 install。这一 setup 开销是有意为之:在 GitHub 托管 runner 上,将 lint、覆盖率和快照回放放在同一个进程池中运行会严重超额占用 CPU,以至于单 job 的关键路径反而长于重复 setup 的方案。 +宽 lane 拆分比单一主 job 更频繁地重复 checkout、setup 和 install。这一设置开销是有意为之的:在 GitHub 托管 runner 上,将 lint、覆盖率和快照回放放在同一个进程池中运行会严重超额占用 CPU,以至于单 job 的关键路径比重复设置还要长。 -这种拆分引入了一项维护义务:当 `package.json` 增删属于 CI 的门禁时,[scripts/run-gates.ts](../../../../scripts/run-gates.ts) 需要相应增删叶子。这一义务是有意的,因为该 runner 是同一套门禁词汇的并行执行计划,而非独立的质量策略。 +这种拆分引入了一项维护义务:当 `package.json` 新增或移除一个应纳入 CI 的门禁时,[scripts/run-gates.ts](../../../../scripts/run-gates.ts) 需要添加或删除对应的叶子。这一义务是有意为之的,因为该 runner 是同一套门禁词汇的并行执行计划,而非独立的质量策略。 -兼容性信号窄于主 Node 24 信号。它证明源码图在每个宣称支持的运行时上能通过类型检查、且真实的未构建 workflow-worker 启动路径能正常执行,而不必重复文档、覆盖率、发布卫生、快照回放和其他不因 Node 版本而异的冒烟检查。 +兼容性信号比主 Node 24 信号更窄。它证明源码图在每个声明支持的运行时上都能通过类型检查,且真实的未构建 workflow-worker 启动路径能够执行,而不必重复文档、覆盖率、发布、快照回放以及那些不因 Node 版本而异的无关冒烟测试。 diff --git a/docs/rfc/implemented/process/2026-07-06-parallel-pre-push-gates.i18n.yaml b/docs/rfc/implemented/process/2026-07-06-parallel-pre-push-gates.i18n.yaml index 3adf939f24..6475a65971 100644 --- a/docs/rfc/implemented/process/2026-07-06-parallel-pre-push-gates.i18n.yaml +++ b/docs/rfc/implemented/process/2026-07-06-parallel-pre-push-gates.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-06-parallel-pre-push-gates.md: 8a8813f2f3f6726ab2ab028757406d366ceb8b6d -2026-07-06-parallel-pre-push-gates.zh.md: 36207e522982990c193f9fe909faf380de53bca6 +2026-07-06-parallel-pre-push-gates.zh.md: 6ee3a8003853092d75cda9908bfae13ee0d4c7a2 diff --git a/docs/rfc/implemented/process/2026-07-06-parallel-pre-push-gates.zh.md b/docs/rfc/implemented/process/2026-07-06-parallel-pre-push-gates.zh.md index 36207e5229..6ee3a80038 100644 --- a/docs/rfc/implemented/process/2026-07-06-parallel-pre-push-gates.zh.md +++ b/docs/rfc/implemented/process/2026-07-06-parallel-pre-push-gates.zh.md @@ -1,42 +1,42 @@ # RFC:并行 pre-push 门禁 -Status: implemented - [English](2026-07-06-parallel-pre-push-gates.md) | 中文 +Status: implemented + ## 问题 -pre-push 钩子是分支离开本地机器前的最后一道检查点,因此它的挂钟时间直接影响贡献者是否愿意保持启用并信任其信号。Lefthook 已经能并行运行顶层 job,但 `pnpm run hygiene` 和 `pnpm run doc-sync` 这类聚合 job 在单个 job 内部隐藏了长串的顺序执行链。因此钩子可以配置为并行,却仍在等待那些成员彼此独立的串行子命令。 +pre-push 钩子是分支离开本地机器前的最后一道检查点,因此它的挂钟时间直接影响贡献者是否愿意保持启用并信任其信号。Lefthook 已经能并行运行顶层 job,但 `pnpm run hygiene` 和 `pnpm run doc-sync` 等聚合 job 在单个 job 内部隐藏了长串的顺序执行链。钩子因此可能在配置上看似并行,实际仍在等待那些成员彼此独立却串行执行的子命令。 -把这些成员直接展平到 `lefthook.yml` 只能解决本地钩子的问题。CI 有同样的调度问题,而在 YAML 中重复一份长长的叶子列表会让未来的脚本改动有两处可能漂移。 +将这些成员直接展平到 `lefthook.yml` 只能解决本地钩子的问题。CI 面临同样的调度问题,而在 YAML 中复制一长串叶子列表会让未来的脚本改动有两处可能漂移。 -`publint` 在更低一层也有同样的形态。每个包独立地针对自身的 manifest 和构建产物做 lint,但运行器按顺序逐个遍历所有包。在本仓库中,这意味着一个包发布门禁消耗的时间与包数量成正比,尽管各检查之间并不共享可变状态。 +`publint` 在更低一层也有同样的形态。每个包(package)独立地根据自身 manifest(元数据清单)和构建产物做 lint,但 runner 按顺序遍历所有包。在本仓库中,这意味着一个包发布门禁的耗时与包的数量成正比,尽管各检查之间并不共享可变状态。 ## 决策 -[lefthook.yml](../../../../lefthook.yml) 保留一个名为 `full check` 的 pre-push job,运行 `pnpm run check:pre-push`。该包脚本委托给 [scripts/run-gates.ts](../../../../scripts/run-gates.ts),即 CI 使用的同一个有界调度器。 +[lefthook.yml](../../../../lefthook.yml) 保留一个名为 `full check` 的 pre-push job,运行 `pnpm run check:pre-push`。该 package 脚本委托给 [scripts/run-gates.ts](../../../../scripts/run-gates.ts),即 CI 使用的同一个有界调度器。 -`pre-push` 模式展开为以下叶子门禁:单元测试套件、快照测试套件、构建、`hygiene` 成员、`doc-sync` 成员,以及 module-graph 新鲜度。叶子列表保持与包脚本相同的门禁词汇(包括 RFC 分类和 RFC 格式),运行器并发调度独立检查,并为每个门禁打印一个计时/输出块。 +`pre-push` 模式展开为以下叶子门禁:单元测试套件、快照测试套件、构建、`hygiene` 成员、`doc-sync` 成员,以及 module-graph 新鲜度。叶子列表保持与 package 脚本相同的门禁词汇,包括 RFC 分类和 RFC 格式,同时 runner 并发调度独立检查,并为每个门禁打印一个计时/输出块。 -构建门禁使钩子在干净 worktree 上也能自给自足。`publint` 和 `verify-node-next-types` 等待构建产物,而仅依赖源码的门禁继续并行执行。 +构建门禁使钩子在干净的 worktree 上也能自足运行。`publint` 和 `verify-node-next-types` 等待构建产物就绪,而仅依赖源码的门禁继续并行执行。 -[scripts/publint-all.ts](../../../../scripts/publint-all.ts) 从 `packages/<group>/<pkg>` 发现包列表,并使用大小取自 `availableParallelism()` 的 worker 池运行 `publint`。`DSH_PUBLINT_CONCURRENCY` 可以为资源配置不同的本地机器和 CI runner 设置 worker 数量上限或提高上限。结果按包缓冲,并按确定性的包顺序打印,因此并行执行不会打乱每个包的日志块。 +[scripts/publint-all.ts](../../../../scripts/publint-all.ts) 从 `packages/<group>/<pkg>` 发现包列表,并使用大小取自 `availableParallelism()` 的 worker 池运行 `publint`。`DSH_PUBLINT_CONCURRENCY` 可为资源配置不同的本地机器和 CI runner 设定或提高 worker 数量上限。结果按包缓冲,并以确定性的包顺序打印,因此并行执行不会打乱各包的日志块。 -聚合包脚本仍然是临时本地运行的真源。调度器是对其成员门禁的并行执行计划,而非替代词汇。 +聚合 package 脚本仍然是临时本地运行的真源。调度器是对其成员门禁的并行执行计划,而非替代词汇。 ## 曾考虑的替代方案 -- **在钩子中保留聚合的 `hygiene` 和 `doc-sync` job**:配置更简单,但 pre-push 的大部分挂钟时间仍然花在 lefthook 看不到也无法调度的串行命令链内部。 -- **为每个叶子门禁声明一个 lefthook job**:通过 lefthook 原生的 job 模型暴露并行性,但会让钩子文件承载一份 CI 无法复用的长成员列表。 -- **要求开发者在推送前手动构建**:省去一个钩子门禁,但会导致 `publint` 在干净 worktree 上失败,并把最后的本地检查点从可运行的检查降格为一项约定。 +- **在钩子中保留聚合的 `hygiene` 和 `doc-sync` job**:配置更简单,但 pre-push 的大部分挂钟时间仍然消耗在 lefthook 看不到也无法调度的串行命令链内部。 +- **为每个叶子门禁声明一个 lefthook job**:通过 lefthook 原生 job 模型暴露并行性,但会让钩子文件承载一长串成员列表,CI 无法复用。 +- **要求开发者在推送前手动构建**:可以省去一个钩子门禁,但会导致 `publint` 在干净 worktree 上失败,并把最后的本地检查点从可运行的检查降级为一种约定。 - **在 shell 脚本中使用后台子命令**:能并行化工作,但会丢失 lefthook 的 job 名称、逐 job 计时和失败分组,且信号处理更难推理。 -- **为每个包声明一个 publint lefthook job**:暴露最大并行度,但会把钩子变成一份手工维护的包清单,恰好在新增包时漂移。 -- **以无界并发运行 publint**:仅在小型机器上以赌进程数、内存压力、包 tarball 创建和日志可读性为代价来最小化耗时。 +- **为每个包声明一个 publint lefthook job**:暴露最大并行度,但会让钩子变成一份手动维护的包清单,恰好在新增包时漂移。 +- **以无界并发运行 publint**:仅在小型机器上以赌注方式最小化耗时,代价是进程数、内存压力、包 tarball 创建和日志可读性的风险。 ## 后果 -钩子的关键路径变为最慢的那个实际门禁,而非隐藏门禁链的总和。Lefthook 报告一个 `full check` job,运行器在该 job 内部报告逐门禁计时,因此本地检查点慢时仍能指出主导耗时的那个门禁。 +钩子的关键路径变为最慢的单个真实门禁,而非隐藏门禁链的总和。Lefthook 报告一个 `full check` job,runner 在该 job 内部报告逐门禁计时,因此本地检查点偏慢时仍能指向主导耗时的那个门禁。 -钩子文件保持简短,重复的成员列表集中在 [scripts/run-gates.ts](../../../../scripts/run-gates.ts) 中,CI 和 pre-push 可以共享。代价是一个自定义调度器脚本(而非纯 lefthook 配置),外加本地 pre-push 路径中的一次构建。 +钩子文件保持简短,重复的成员列表集中在 [scripts/run-gates.ts](../../../../scripts/run-gates.ts) 中,CI 和 pre-push 可以共享。代价是引入一个自定义调度器脚本(而非纯 lefthook 配置),外加本地 pre-push 路径中的一次构建。 -`publint-all.ts` 变为异步代码,缓冲命令输出而非实时继承 stdio。收益是包级并行、稳定的输出顺序,以及一个用于资源调优的环境变量。 +`publint-all.ts` 变为异步代码,缓冲命令输出而非实时继承 stdio。收益是包级别的并行性、稳定的输出顺序,以及一个用于资源调优的环境变量。 diff --git a/docs/rfc/implemented/process/2026-07-10-readme-known-limitations-gate.i18n.yaml b/docs/rfc/implemented/process/2026-07-10-readme-known-limitations-gate.i18n.yaml index bd24f4a6c1..81355776bb 100644 --- a/docs/rfc/implemented/process/2026-07-10-readme-known-limitations-gate.i18n.yaml +++ b/docs/rfc/implemented/process/2026-07-10-readme-known-limitations-gate.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-10-readme-known-limitations-gate.md: b7f45421bf0d4d50ec1a19941934e782f52e7926 -2026-07-10-readme-known-limitations-gate.zh.md: 4dd8db1df9b0b2be73e7ae6a64e11b8dabc2add1 +2026-07-10-readme-known-limitations-gate.zh.md: dc023ac43890d8aaaefaece2e592001629e2a74e diff --git a/docs/rfc/implemented/process/2026-07-10-readme-known-limitations-gate.zh.md b/docs/rfc/implemented/process/2026-07-10-readme-known-limitations-gate.zh.md index 4dd8db1df9..dc023ac438 100644 --- a/docs/rfc/implemented/process/2026-07-10-readme-known-limitations-gate.zh.md +++ b/docs/rfc/implemented/process/2026-07-10-readme-known-limitations-gate.zh.md @@ -1,29 +1,29 @@ -# RFC:在每个 package README 中设置受门禁保护的「已知限制」章节 - -Status: implemented +# RFC:在每个 package README 中设置受门禁保护的 Known Limitations 章节 [English](2026-07-10-readme-known-limitations-gate.md) | 中文 +Status: implemented + ## 问题 -[文档标准](../../../AGENTS.md)将限制事项归属于 package README。如果没有统一的格式,缺失的章节无法区分「经过审计确认无限制」和「忘记写了」,而各式各样的标题也让仓库级搜索无从下手。 +[文档标准](../../../AGENTS.md)将限制事项指定在 package README 中记录。如果没有统一的格式,缺失的章节无法区分「经审计确认无此内容」与「忘了写文档」,而标题写法不一致也会妨碍全仓库搜索。 ## 决策 -`packages/<group>/<pkg>/package.json` 下的每个 package manifest(元数据清单)都有一个同级 README,其中包含规范的 `## Known Limitations and Deferred Work` 章节。该章节的条目记录该 package 拥有的持久性消费方缺口与非显而易见的维护约束;普通的清理工作留在源码 TODO 或所属 RFC 中。[`verify-package-readme-limitations` 门禁](../../../../scripts/verify-package-readme-limitations.ts)从 manifest 推导 package 集合,拒绝缺少 README 的情况,并要求恰好有一个规范的 h2 标题且至少包含一个顶级条目。近似标题(如 "Limitations"、"Deferred"、"What is NOT here" 或 "Non-goals")会导致失败。 +`packages/<group>/<pkg>/package.json` 下的每个包(package)manifest(元数据清单)都有一个同目录的 README,其中包含规范的 `## Known Limitations and Deferred Work` 章节。该章节的条目记录该包拥有的持久性消费方缺口与非显而易见的维护者约束;常规清理工作仍留在源码 TODO 或所属 RFC 中。[`verify-package-readme-limitations` 门禁](../../../../scripts/verify-package-readme-limitations.ts)从 manifest 推导包集合,拒绝缺少 README 的情况,并要求恰好有一个规范的 h2 标题且至少包含一个顶级条目。近似标题(如 "Limitations"、"Deferred"、"What is NOT here" 或 "Non-goals")会导致失败。 -如果一个 package 确实没有需要声明的限制,则将其列入 `NO_LIMITATIONS` 并省略该章节。新增限制时必须移除该条目;重命名或删除条目会失败,因为每个条目必须对应一个被扫描的 package。 +如果一个包确实没有需要声明的限制事项,则将其列入 `NO_LIMITATIONS` 并省略该章节。新增限制事项时须移除该条目;重命名或移除条目会失败,因为每个条目都必须对应一个被扫描的包。 -门禁检查存在性、格式和白名单。覆盖率与准确性由文档标准和[行文标准](../../../../.agents/skills/dsh-prose-standard/SKILL.md)下的评审负责。常设规则见 [packages/AGENTS.md](../../../../packages/AGENTS.md)。 +门禁检查的是存在性、格式与白名单。覆盖面和准确性由文档标准与 [prose 标准](../../../../.agents/skills/dsh-prose-standard/SKILL.md)下的评审负责。常设规则见 [packages/AGENTS.md](../../../../packages/AGENTS.md)。 ## 曾考虑的替代方案 -- **自由格式标题**:无法统一搜索,仍然需要近似标题检测。 -- **要求空章节或写 "None."**:样板文字可能在 package 新增限制后仍然残留;白名单使「确认无限制」显式且可评审。 -- **施加字数上限**:合理的限制条目数量因 package 而异,因此由评审管控这一不设预算的 README 层级。 +- **自由格式标题**:无法统一搜索,仍需近似标题检测。 +- **要求空章节或写 "None."**:样板文字可能在包新增限制事项后仍然残留;白名单使「确无限制」这一状态显式且可评审。 +- **设置字数上限**:合理的限制事项数量因包而异,因此由评审管控这一不设预算的 README 层级。 ## 后果 -- 新 package 要么声明符合条件的限制事项,要么显式加入白名单;缺失、漂移或空白的章节会在本地和 CI 的 `doc-sync` 中失败。 -- 门禁向 `doc-sync` 新增一个无外部依赖的 TypeScript 脚本。 -- 重命名被强制的标题需要同时修改脚本和所有 package README。 +- 新建的包须声明符合条件的限制事项,或显式加入白名单;缺失、漂移或空的章节会在本地和 CI 的 `doc-sync` 中失败。 +- 门禁为 `doc-sync` 新增一个无外部依赖的 TypeScript 脚本。 +- 重命名受强制的标题需要同时修改脚本和所有 package README。 diff --git a/docs/rfc/implemented/process/2026-07-12-package-model-experience-contract.i18n.yaml b/docs/rfc/implemented/process/2026-07-12-package-model-experience-contract.i18n.yaml index 7796a46dfc..9e9b379eb9 100644 --- a/docs/rfc/implemented/process/2026-07-12-package-model-experience-contract.i18n.yaml +++ b/docs/rfc/implemented/process/2026-07-12-package-model-experience-contract.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-12-package-model-experience-contract.md: 036efbc9510d6d0ae9e3c52a5ba8f39647adc4c9 -2026-07-12-package-model-experience-contract.zh.md: 6ee96f3befbb2ead7196f412dccf915f475cfc6d +2026-07-12-package-model-experience-contract.zh.md: b1efa712bc4f6fa7b23c0afe965e56eabf068d97 diff --git a/docs/rfc/implemented/process/2026-07-12-package-model-experience-contract.zh.md b/docs/rfc/implemented/process/2026-07-12-package-model-experience-contract.zh.md index 6ee96f3bef..b1efa712bc 100644 --- a/docs/rfc/implemented/process/2026-07-12-package-model-experience-contract.zh.md +++ b/docs/rfc/implemented/process/2026-07-12-package-model-experience-contract.zh.md @@ -1,33 +1,33 @@ -# RFC:包(package)模型体验契约 - -Status: implemented +# RFC:Package Model Experience 契约 [English](2026-07-12-package-model-experience-contract.md) | 中文 +Status: implemented + ## 问题 -一个包的 README 可以解释 API 和运行时机制,却不回答主导 agent harness(智能体框架)行为与成本的核心问题:这个包中有什么内容会进入模型请求、在什么条件下进入、以及这些 token 会保留多久。在插件架构中,这一缺失尤其难以审计。消费方可能把后端结果转为工具消息,策略插件可能把成功替换为错误,压缩(compaction)可能移除旧历史,agent 作用域的注册可能改变某个 agent 的提示词或 schema 而对其他 agent 毫无影响。因此只阅读名义上面向模型的包会遗漏真实的上下文影响,而逐依赖阅读源码对于日常评审又过于昂贵。 +一个 package(包)的 README 可以解释 API 和运行时机制,却不回答那个主导 agent harness(智能体框架)行为与成本的问题:本 package 中有什么内容会进入模型请求、在什么条件下进入、以及这些 token 会保留多久。在插件架构中,这一缺失尤其难以审计。消费方可能把后端结果转为工具消息,策略插件可能把成功替换为错误,上下文压缩(context compaction)可能移除旧历史,agent 作用域的注册可能改变某个 agent 的提示词或 schema 而其他 agent 不受影响。因此,只阅读名义上面向模型的 package 会遗漏真实的上下文影响,而跨所有依赖阅读源码对日常评审来说又太昂贵。 ## 决策 -每个具有面向模型或模型相邻契约的 workspace 包 README,都以规范的 [Model Experience 章节](../../../cookbook/adding-a-package.md#4-write-the-package-readme)结尾,紧接在 `## Known Limitations and Deferred Work` 之前;如果包在 no-limitations 允许列表上,则以 Model Experience 本身结尾。经审计确认为模型无关的通用包通过 `NO_MODEL_EXPERIENCE_SECTION` 省略该章节。 +每个具有面向模型或模型相邻契约的 workspace package README,在末尾、`## Known Limitations and Deferred Work` 之前放置规范的 [Model Experience 章节](../../../cookbook/adding-a-package.md#4-write-the-package-readme);位于 no-limitations 允许列表上的 package 以 Model Experience 本身作为末尾章节。经审计确认为模型无关的通用 package 通过 `NO_MODEL_EXPERIENCE_SECTION` 省略该章节。 -具有直接、条件性、有上限、生命周期性、多表面或辅助模型效应的包,每个上下文表面使用一个 H3。每个 H3 说明相关模型接收到什么内容、何时接收,并对 token 效应进行分类。包所拥有的稳定文本逐字引用:系统提示词及其他长文本使用嵌套 H4 加 `markdown` 围栏,短文本则以行内形式保留,带命名的插值占位符。工具 schema 表面链接到生成的[工具目录](../../../tool-catalog.md)中对应的锚点章节,只陈述组合或配置差异;仅在运行时定义的则说明目录为何未收录。数据依赖和提供方拥有的文本以摘要形式呈现。agent 作用域的可见性须显式标注;当作用域可以隐藏提示词而不隐藏 schema(或反之)时,提示词和 schema 表面保持分开记录。 +具有直接、条件性、有上限、生命周期性、多表面或辅助模型效应的 package,每个上下文表面使用一个 H3。每个 H3 说明相关模型接收到什么内容以及何时接收,然后对 token 效应进行分类。由 package 拥有的稳定文本逐字引用:系统提示词行文和其他长字面量使用嵌套 H4 加 `markdown` 围栏,短字面量则以行内形式呈现并使用命名插值占位符。工具 schema 表面链接到生成的[工具目录](../../../tool-catalog.md)中对应的锚定章节,仅说明组合或配置差异;仅在运行时定义的工具则解释为何目录中未收录。数据依赖和提供方拥有的文本以摘要形式描述。agent 作用域的可见性须显式说明;当作用域可以隐藏其中一个而不影响另一个时,提示词表面与 schema 表面保持分开。 -没有模型上下文效应的包,或其路径完全由另一个包渲染的包,使用验证器审计过的单句形式:`None, as ` 或 `Indirectly, through `。纯传输和无 ctx key 的测试支持包在不产生模型绑定内容时使用 none 形式。提供方后端即使会截断或过滤数据,也使用 indirect 形式;组装 bundle 在所有效应由具名子包拥有时同样使用 indirect 形式。这些句子定位贡献所在,而不重述消费方的内容。结构化章节同样只记录包自身拥有的输入、转换和差异。 +没有模型上下文效应的 package,或其路径完全由另一个 package 渲染的 package,使用验证器审计过的单句形式:`None, as ` 或 `Indirectly, through `。纯传输和无密钥的测试支持 package 在不创建模型绑定内容时使用 none 形式。提供方后端即使对数据进行上限或过滤,也使用 indirect 形式;组装 bundle 在命名子 package 拥有全部效应时同样使用 indirect 形式。这些句子定位贡献所在,而不重述消费方的内容。结构化章节同样只记录 package 自身拥有的输入、变换和差异。 -`verify-package-readme-model-experience` 发现包的 manifest 并验证三种分类、规范的末尾章节顺序、必填字段、具体的文本证据、嵌套的逐字块以及锚定的工具目录链接。它在 `doc-sync` 和并行门禁运行器中运行。覆盖面、链接相关性和事实准确性仍由评审把关。 +`verify-package-readme-model-experience` 发现 package manifest(元数据清单)并验证三种分类、规范的末尾章节顺序、必填字段、具体字面量证据、嵌套逐字块和锚定的工具目录链接。它在 doc-sync(文档同步门禁)和并行门禁运行器中执行。覆盖面、链接相关性和事实准确性仍由评审把关。 ## 曾考虑的替代方案 -- **只记录注册了提示词或工具的包**:否决。后端、策略插件、适配器、持久化、作用域和压缩都会改变 token 的内容或生命周期,却不拥有面向模型的 schema。 -- **从源码生成一份中央上下文成本目录**:否决。AST 能找到注册点,但无法推断语义条件,例如历史保留、输出截断、父子可见性或辅助模型边界。包 README 是实现本地的契约;中央副本会增加又一个漂移面。 -- **要求给出数值 token 计数**:否决。精确计数取决于所选模型的 tokenizer、适配器序列化方式、配置和运行时数据。稳定的契约是增长形态:每请求固定、每调用条件性、保留、替换、有上限或零直接。 -- **使用三列表格**:否决。精确的源文本和条件性结果形态使单元格过于密集、难以扫读。重复的子章节为每个上下文表面提供可读的纵向空间,同时保留相同的字段。 -- **允许所有零影响包省略该章节**:否决。无约束的缺失在「经审计的零影响」和「忘了写文档」之间是歧义的。省略仅限于在验证器中以理由具名的模型无关通用包;模型相邻的零影响包保留一句显式说明。 -- **要求经审计的零影响或简单间接包也使用完整结构化形式**:否决。围绕一个事实重复标签没有意义。一句受门禁约束的句子在保持显式覆盖的同时免去了仪式感。 -- **只有约定、没有门禁**:否决。仓库级契约必须覆盖未来的每个包;评审者的记忆无法可靠地检测到遗漏的 README 章节。 +- **只记录注册提示词或工具的 package**:否决。后端、策略插件、适配器、持久化、作用域和压缩都会改变 token 的内容或生命周期,却不拥有面向模型的 schema。 +- **从源码生成一份集中式上下文成本目录**:否决。AST 能找到注册点,但无法推断语义条件,如历史保留、输出截断、父子可见性或辅助模型边界。package README 是实现本地的契约;集中副本会增加又一个漂移面。 +- **要求给出精确 token 数**:否决。精确数量取决于所选模型的 tokenizer、适配器序列化方式、配置和运行时数据。稳定的契约是增长形状:每请求固定、每调用条件性、保留、替换、有上限或零直接影响。 +- **使用三列表格**:否决。精确的源文本和条件性结果形状使单元格密集且难以扫读。重复的子章节为每个上下文表面提供可读的纵向空间,同时保留相同的字段。 +- **允许所有零影响 package 省略该章节**:否决。无约束的缺失在「经审计的零影响」和「忘记写文档」之间有歧义。省略仅限于在验证器中以理由命名的模型无关通用 package;模型相邻的零影响 package 保留一句显式说明。 +- **对审计过的零影响或简单间接 package 也要求完整结构化形式**:否决。围绕一个事实重复标签没有意义。受门禁约束的单句保留了显式覆盖而无需繁文缛节。 +- **只有约定而无门禁**:否决。仓库级契约必须覆盖未来的每个 package;评审者的记忆无法可靠地检测到遗漏的 README 章节。 ## 后果 -评审者可以从任何面向模型或模型相邻的包出发,看到它对会话模型、子模型和辅助调用的贡献,而无需重建完整的插件图。token 预算工作可以区分每次请求的重复开销与数据依赖的历史,agent 作用域的变更有了显式的文档检查点。包作者在模型可见行为变化时维护一个或多个紧凑的上下文表面块,或一句经分类的句子;经审计的通用包不带无关的模型样板文字。结构化字段不承诺提供方精确的 token 计数;测量仍然是模型和负载特定的,而文档化的增长形态与可见性契约保持稳定。 +评审者可以从任何面向模型或模型相邻的 package 出发,直接看到它对会话模型、子模型和辅助调用的贡献,无需重建完整的插件图。token 预算工作可以区分每次请求的重复开销与数据依赖的历史,agent 作用域的变更有了显式的文档检查点。package 作者在模型可见行为变更时维护一个或多个紧凑的上下文表面块或一句分类说明;经审计的通用 package 不承载无关的模型样板文字。结构化字段不承诺提供方精确的 token 数;测量仍然是模型和负载特定的,而文档化的增长与可见性契约保持稳定。 diff --git a/docs/rfc/implemented/simplification/2026-06-19-drop-mutable-session-summary.i18n.yaml b/docs/rfc/implemented/simplification/2026-06-19-drop-mutable-session-summary.i18n.yaml index f88f300193..7d48003ed6 100644 --- a/docs/rfc/implemented/simplification/2026-06-19-drop-mutable-session-summary.i18n.yaml +++ b/docs/rfc/implemented/simplification/2026-06-19-drop-mutable-session-summary.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-19-drop-mutable-session-summary.md: 0d790191906a9128ad12d40526fde3b9f8fa939f -2026-06-19-drop-mutable-session-summary.zh.md: 97293c296cd74b5a33ce7e1ebe473c970ee95d57 +2026-06-19-drop-mutable-session-summary.zh.md: 6b234eb0dbdf764223e078519c810216c28603be diff --git a/docs/rfc/implemented/simplification/2026-06-19-drop-mutable-session-summary.zh.md b/docs/rfc/implemented/simplification/2026-06-19-drop-mutable-session-summary.zh.md index 97293c296c..6b234eb0db 100644 --- a/docs/rfc/implemented/simplification/2026-06-19-drop-mutable-session-summary.zh.md +++ b/docs/rfc/implemented/simplification/2026-06-19-drop-mutable-session-summary.zh.md @@ -6,30 +6,30 @@ Status: implemented ## 问题 -[会话持久化 seam](../architecture/2026-06-14-session-persistence.md) 将会话的日志外元数据拆分为 `dsh-session` 拥有的两种类型:一个不可变的 `SessionHeader`(`version`、`id`、`createdAt`、`cwd?`、`parentSession?`),在创建时一次性写入;一个可变的 `SessionSummary`(`updatedAt`、`title?`、`firstPrompt?`),「无需触碰仅追加日志即可更新」。二者的联合类型为 `SessionMeta = SessionHeader & SessionSummary`,抽象的 `SessionPersistence` 服务为此多出第七个方法 `update(id, summary)`,用于重写摘要。各后端各自实现可变存储:JSONL 在日志旁写一个独立的原子 `.summary.json` **伴随文件**(临时写入 + rename,尽力而为);SQLite 在追加事务内更新 `updated_at`/`title`/`first_prompt` **列**。 +[session-persistence seam](../architecture/2026-06-14-session-persistence.md) 将会话的日志外元数据拆分为 `dsh-session` 拥有的两种类型:一个不可变的 `SessionHeader`(`version`、`id`、`createdAt`、`cwd?`、`parentSession?`),在创建时一次性写入;一个可变的 `SessionSummary`(`updatedAt`、`title?`、`firstPrompt?`),「可在不触碰仅追加日志的情况下更新」。二者的联合类型为 `SessionMeta = SessionHeader & SessionSummary`,抽象的 `SessionPersistence` 服务为此多出第七个方法 `update(id, summary)`,用于重写摘要。各后端各自实现可变存储:JSONL 在日志旁写一个独立的原子 `.summary.json` **伴随文件**(临时写入 + rename,尽力保证);SQLite 在追加事务内更新 `updated_at`/`title`/`first_prompt` **列**。 -摘要的设计初衷是服务于未来的会话选择器(通过 `updatedAt` 排序、用 `title`/`firstPrompt` 预览)。该选择器从未实现。对整个仓库的审计表明,`SessionSummary` 的全部表面积都是**死状态**: +摘要是为未来的会话选择器设计的(通过 `updatedAt` 排序近期会话,用 `title`/`firstPrompt` 做预览)。该选择器从未实现。对整个仓库的审计表明,`SessionSummary` 的全部表面积都是**死状态**: -- `SessionPersistence.update()` 的**生产调用方为零**(所有 `.update(` 命中都是 `createHash().update()` 或测试代码)。 +- `SessionPersistence.update()` **零个生产调用方**(所有 `.update(` 匹配都是 `createHash().update()` 或测试代码)。 - `firstPrompt` 在生产代码中**从未被读取**。 -- `title` 确实在 ACP bridge 中被读取,但来源是工具调用的 **presenter**(`present.title`),而非存储的会话元数据。 -- `updatedAt` **没有消费方**:`list()` 唯一的生产调用方读取的是 `meta.cwd`(`SessionHeader` 字段),用于在 `session/load` 时校验工作区;resume 读取的是 `createdAt`/`cwd`/`parentSession`,全部是 header 字段。 -- 决定性的事实:活跃的 `Session.header` 早已被类型化为 `SessionHeader` 而非 `SessionMeta`——摘要从未存在于活跃会话对象上;它只存在于持久化层,除了自身的契约测试之外无人写入、无人读取。 +- `title` 确实在 ACP 桥接层被读取过,但读的是工具调用的 **presenter**(`present.title`),从未读取存储的会话元数据。 +- `updatedAt` **没有消费方**:`list()` 唯一的生产调用方读取的是 `meta.cwd`(`SessionHeader` 字段),用于在 `session/load` 时校验工作区;恢复会话读取的是 `createdAt`/`cwd`/`parentSession`——全是 header 字段。 +- 决定性的一点:活跃的 `Session.header` 类型本来就是 `SessionHeader` 而非 `SessionMeta`——摘要从未存在于活跃会话对象上;它只存在于持久化层,除了自身的契约测试外无人写入、无人读取。 ## 决策 -彻底删除可变的会话摘要。`SessionSummary` 与 `SessionMeta` 这个名称一并移除;后端存储和返回的元数据仅为 `SessionHeader`。`SessionPersistence.update()` 从抽象服务和所有后端中移除。JSONL 去掉整套伴随文件机制(`writeSidecar`/`readSidecar`/`touchSummary`/`removeSidecars`/`sidecarPath` 以及 load/list 的覆盖逻辑);SQLite 删除 `updated_at`/`title`/`first_prompt` 列及每次追加时的 `updated_at` 更新,其 `SCHEMA_VERSION` 从 `1 → 2`。 +彻底删除可变的会话摘要。`SessionSummary` 与 `SessionMeta` 这个名称一并移除;后端存储和返回的元数据仅为 `SessionHeader`。`SessionPersistence.update()` 从抽象服务和所有后端中移除。JSONL 去掉整套伴随文件机制(`writeSidecar`/`readSidecar`/`touchSummary`/`removeSidecars`/`sidecarPath` 以及 load/list 的覆盖逻辑);SQLite 去掉 `updated_at`/`title`/`first_prompt` 列以及每次追加时的 `updated_at` 更新,其 `SCHEMA_VERSION` 从 `1 → 2`。 -摘要原本要提供的一切,在消费方真正需要时都**可从仅追加日志中派生**(`firstPrompt` = 第一条 `user/message`;最近活跃时间 = 最后一个事件的 `time` 或文件 mtime),或者已经存在于不可变的 header 中(`createdAt`、`cwd`)。唯一*不可*派生的——用户*手动编辑*的标题——没有任何实现,纯属 YAGNI;如果未来真有功能需要,它可以作为独立的日志事件或 header 字段回归。 +摘要原本要提供的一切,在消费方真正需要时都**可从仅追加日志中派生**(`firstPrompt` = 第一条 `user/message`;近期度 = 最后一个事件的 `time` 或文件 mtime),或者已经存在于不可变 header 中(`createdAt`、`cwd`)。唯一不可派生的是用户*手动编辑*的标题,但它从未实现,纯属 YAGNI;如果未来真有功能需要,它可以作为独立的日志事件或 header 字段回归。 -将此记录为决策,是因为它**持久**(收窄了一个公开服务契约和两个后端的磁盘格式)、**有争议**(摘要是有意的前瞻性设计,不是意外产物)、**出人意料**(未来读者看到 `SessionHeader` 而原始 RFC 描述的是 `SessionMeta`,否则会疑惑摘要为何消失)。它还为[共享持久化写入协调器](../architecture/2026-06-18-shared-persistence-write-coordinator.md)扫清了障碍:没有可变摘要,协调器的钩子接口就不需要 `updateSummary` 钩子,JSONL 伴随文件与 SQLite 列之间的持久性差异也随之消失,两个后端的写入路径得以收敛。 +将此记录为决策,原因有三:**持久性**(它收窄了一个公开服务契约和跨两个后端的磁盘格式)、**争议性**(摘要是有意的前瞻性设计,而非意外产物)、**意外性**(未来读者看到 `SessionHeader` 而原始 RFC 描述的是 `SessionMeta`,否则会疑惑摘要为何消失)。它还为 [shared persistence write coordinator](../architecture/2026-06-18-shared-persistence-write-coordinator.md) 扫清了障碍:没有可变摘要后,协调器的钩子接口无需 `updateSummary` 钩子,JSONL 伴随文件与 SQLite 列之间的持久性分歧也随之消失,两个后端的写入路径得以统一。 ## 无需迁移 -这是未发布的软件(见[根 AGENTS.md](../../../../AGENTS.md)「预发布立场:地基优先于爆炸半径」一节),因此不存在需要保留的磁盘数据库或日志。SQLite 不迁移 v1 数据库:`openDatabase` 守卫现在拒绝任何非当前版本的磁盘 `user_version`(`onDisk !== 0 && onDisk !== SCHEMA_VERSION`),无论更旧还是更新,因此陈旧的 v1 数据库会被干净地拒绝,而非在新列集上半读半错。新建数据库写入当前版本号;这是唯一需要工作的路径。 +这是未发布的软件(见[根 AGENTS.md](../../../../AGENTS.md)「Pre-release stance: foundation over blast radius」一节),因此没有需要保留的磁盘数据库或日志。SQLite 不迁移 v1 数据库:`openDatabase` 守卫现在拒绝任何非当前版本的磁盘 `user_version`(`onDisk !== 0 && onDisk !== SCHEMA_VERSION`),无论更旧还是更新,因此陈旧的 v1 数据库会被干净地拒绝,而非在新列集下被半读取。新建数据库写入当前版本号;这是唯一需要正常工作的路径。 ## 后果 -未来的会话选择器现在必须从日志派生预览和排序信息(或重新引入一个类型化字段),而不能直接读取现成的摘要行。这是正确的代价:为一个不存在的功能维护缓存,是每个后端都要承担的死重,也是每个契约测试都要断言的负担。这一原则——**通过的测试固定的是当前行为,不一定是正确行为;行为可能是过去妥协的产物**——现已作为独立约定记录在[根 AGENTS.md](../../../../AGENTS.md) 中,本次变更即为其实例。 +未来的会话选择器现在必须从日志派生预览/排序信息(或重新引入一个类型化字段),而不能直接读取现成的摘要行。这是正确的代价:为一个尚不存在的功能维护缓存,是每个后端都要付出维护成本、每个契约测试都要付出断言成本的死重。这一原则——**通过的测试固定的是当前行为,不一定是正确行为;行为可能是过去妥协的产物**——现已作为独立约定记录在[根 AGENTS.md](../../../../AGENTS.md) 中,本次变更即为其实例。 <!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> diff --git a/docs/rfc/implemented/simplification/2026-06-20-collapse-trace-only-session-events.i18n.yaml b/docs/rfc/implemented/simplification/2026-06-20-collapse-trace-only-session-events.i18n.yaml index 0e73dbfe7e..807d67d2fc 100644 --- a/docs/rfc/implemented/simplification/2026-06-20-collapse-trace-only-session-events.i18n.yaml +++ b/docs/rfc/implemented/simplification/2026-06-20-collapse-trace-only-session-events.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-20-collapse-trace-only-session-events.md: 9156c2ab356b1c46758d9d2047d491cd1952cbc4 -2026-06-20-collapse-trace-only-session-events.zh.md: f2ecfbf46d8d70cf478d57eae2ed3a18ea8746fa +2026-06-20-collapse-trace-only-session-events.zh.md: c4555f3a772096fc36de968d5d3085f5a2e879f3 diff --git a/docs/rfc/implemented/simplification/2026-06-20-collapse-trace-only-session-events.zh.md b/docs/rfc/implemented/simplification/2026-06-20-collapse-trace-only-session-events.zh.md index f2ecfbf46d..c4555f3a77 100644 --- a/docs/rfc/implemented/simplification/2026-06-20-collapse-trace-only-session-events.zh.md +++ b/docs/rfc/implemented/simplification/2026-06-20-collapse-trace-only-session-events.zh.md @@ -6,39 +6,39 @@ Status: implemented ## 问题 -会话事件词汇中包含一些一等事件,它们既不属于可回放的对话历史,在生产环境中也几乎没有消费方。`usage` 在模型流式分片中已经存在,但循环又额外追加了一个独立的 `usage` 事件。`error` 与 `turn/end { kind: 'error', message, code }` 中的循环失败原因重复;ACP(Agent Client Protocol)结算读取的是 turn-end 原因,ACP 渲染忽略 `error` 事件,`deriveMessages()` 也跳过它。 +会话事件词汇中包含一些一等事件,它们不属于可回放的对话历史,在生产环境中几乎没有消费方。`usage` 已经作为模型流分片存在,之后循环又追加了一个独立的 `usage` 事件。`error` 与 `turn/end { kind: 'error', message, code }` 中的循环失败原因重复;ACP(Agent Client Protocol)结算读取 turn-end 原因,ACP 渲染忽略 `error` 事件,`deriveMessages()` 也跳过它。 -这些事件让规范的 transcript(文本记录)看起来比实际更像遥测数据。它们增加了事件变体、不变式、测试、快照和持久化用例,但作为独立记录并不承载实际负荷。它们携带的事实仍然有用:token 用量应当保留以供核算,错误的步骤编号也不应悄然消失。简化的方式是将这些事实折叠进消费方本就必须理解的邻近事件,而非减少记录的信息量。 +这些事件让规范的 transcript(文本记录)看起来比实际更像遥测数据。它们增加了事件变体、不变式、测试、快照和持久化用例,但作为独立记录并不承载实际功能。它们携带的事实仍然有用:token 用量应当保留以供计费,错误的步骤编号也不应悄然消失。简化的方式是将这些事实折叠进消费方本已必须理解的邻近事件,而非减少记录的信息量。 ## 决策 -仅在信息已被保留、无需并行记录的位置移除独立的追踪事件: +仅在信息已被保留、无需并行记录的情况下,移除独立的追踪事件: -- 成功步骤的 usage 折叠进对应的 `assistant/message`(`assistant/message { turn, step, content, usage? }`),使组装好的模型输出与其核算信息一同传递。 -- 失败或中止的步骤如果有 usage 但没有 assistant 内容,则将 usage 挂在一个空内容的 `assistant/message` 上(下方实现说明给出了无信息丢失的证明)——不会有任何已持久化的 usage 分片失去表示。 -- 独立 `error` 事件中的步骤编号折叠进 `turn/end.reason`(当 `kind: 'error'` 时:`{ kind: 'error', step, message, code? }`)——`turn/end` 是 ACP 和恢复机制已在消费的持久化轮次结果。 -- `agent/error` 和日志保留用于实时诊断;`turn/end` 之后不再有第二条会话日志错误记录。 +- 成功步骤的 usage 折叠进匹配的 `assistant/message`(`assistant/message { turn, step, content, usage? }`),使组装好的模型输出与其计费信息一同传递。 +- 失败或中止的步骤如果有 usage 但没有 assistant 内容,则将 usage 放在一个空内容的 `assistant/message` 上(下方实现说明给出了无信息丢失的证明)——不会有已持久化的 usage 分片无处安放。 +- 独立 `error` 事件中的步骤编号折叠进 `turn/end.reason`(当 `kind: 'error'` 时:`{ kind: 'error', step, message, code? }`)——`turn/end` 是 ACP 和恢复机制已经消费的持久轮次结果。 +- `agent/error` 与日志保留用于实时诊断;`turn/end` 之后不再有第二条会话日志错误记录。 -用户对话日志包含渲染、恢复、审计和核算交互所需的全部信息,消费方无需对账重复的追踪行。 +用户对话日志包含渲染、恢复、审计和计费所需的全部信息,消费方无需协调重复的追踪行。 ## 曾考虑的替代方案 -**保留独立行作为遥测**:这些事件让规范的 transcript 看起来比实际更像遥测数据,代价是增加了事件变体、不变式、测试、快照和持久化用例,却没有消费方使用。如果分析需求真正出现,正确的形态是投影辅助工具或带有独立保留策略的专用遥测存储,而非在对话日志中放置重复的追踪行。 +**保留独立行作为遥测**——这些事件让规范 transcript 看起来比实际更像遥测数据,代价是增加了事件变体、不变式、测试、快照和持久化用例,却没有任何消费方使用。如果分析需求真正出现,正确的形态是投影辅助工具或带有独立保留策略的专用遥测存储,而非对话日志中的重复追踪行。 ## 验证 -`SessionEventMap` 不再包含独立的 `usage` 或 `error`;循环不再追加独立的 usage 事件,持久化的失败通过 `turn/end { kind: 'error', step, message, code? }` 记录;ACP 快照和持久化测试断言不存在仅追踪行;录制的 fixture(测试前置数据)已采用新事件形状,会话格式版本固定为 `0`(按预发布格式策略,后端拒绝任何非 `0` 的存储日志);文档说明了 token 用量和操作错误的观测位置。 +`SessionEventMap` 不再包含独立的 `usage` 或 `error`;agent loop(智能体循环)不再追加独立的 usage 事件,持久性失败通过 `turn/end { kind: 'error', step, message, code? }` 记录;ACP 快照和持久化测试断言不存在仅追踪行;已录制的 fixture(测试前置数据)使用新事件形状,会话格式版本固定为 `0`(后端按预发布格式策略拒绝任何非 `0` 的存储日志);文档说明了 token 用量和操作错误的观测位置。 ## 后果 -消费方不能再从规范日志中筛选独立的 `usage` 或步骤级 `error` 行,必须从承载它们的 assistant/failure 事件中读取这些事实。只有当实现 PR 证明相同的事实仍然存在时,这才是合理的简化;否则独立事件应当保留。 +消费方不能再从规范日志中筛选独立的 `usage` 或步骤级 `error` 行,必须从承载它们的 assistant/failure 事件中读取这些事实。只有在实现 PR(Pull Request)证明相同事实仍然存在的前提下,这才是合理的简化;否则独立事件应予保留。 ## 实现说明 按提案交付,有一处范围细化(遵循 AGENTS.md「RFC 是提案,不是金科玉律」): -- **空内容的 `assistant/message` 承载 usage,无数据丢失。** 提案要求的证明(不会有已持久化的 usage 分片失去表示)落在 max-tokens 路径上:一个被截断的步骤有 usage 但内容为空(例如只有一个被丢弃的工具调用),此前会发出独立的 `usage`。现在它记录一条空内容的 `assistant/message { content: [], usage }`。为避免这向提供方 transcript 注入一个无内容的虚假 assistant 轮次,`deriveMessages()` 跳过空内容的 `assistant/message` 事件。一个回归测试断言 usage 仍有表示,且派生历史未被破坏。 +- **空内容 `assistant/message` 承载 usage,无数据丢失。** 提案要求的证明(不会有已持久化的 usage 分片无处安放)落在 max-tokens 路径上:一个被截断的步骤有 usage 但内容为空(例如只有一个被丢弃的工具调用),以前会发出独立的 `usage`。现在它记录一个空内容的 `assistant/message { content: [], usage }`。为防止这向 provider transcript 注入一个无内容的虚假 assistant 轮次,`deriveMessages()` 跳过空内容的 `assistant/message` 事件。回归测试断言 usage 仍被表示,且派生历史未被破坏。 -**格式版本。** 此变更改动了持久化事件,但预发布会话格式仍固定为 `0`,拒绝任何其他版本且不做迁移。`dsh-session` 拥有写入方和加载校验使用的常量。单调递增的格式版本从首次正式发布开始。 +**格式版本。** 此变更影响已持久化的事件,但预发布会话格式仍固定为 `0`,拒绝任何其他版本且不做迁移。`dsh-session` 拥有写入方和加载校验使用的常量。单调递增的格式版本从首次正式发布开始。 -Usage 现在通过 `assistant/message.usage` 观测;操作错误的步骤编号通过 `turn/end.reason`(当 `kind: 'error'` 时)观测。`agent/error` 加日志用于实时诊断,保持不变。 +Usage 现在通过 `assistant/message.usage` 观测;操作错误的步骤编号通过 `turn/end.reason`(当 `kind: 'error'` 时)观测。`agent/error` 与日志用于实时诊断,保持不变。 diff --git a/docs/rfc/implemented/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.i18n.yaml b/docs/rfc/implemented/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.i18n.yaml index 8586b4051e..dbb62e578d 100644 --- a/docs/rfc/implemented/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.i18n.yaml +++ b/docs/rfc/implemented/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-20-drop-unconsumed-llm-adapter-change-event.md: efe90c0197671ef4385ce517540b4b238962c3b5 -2026-06-20-drop-unconsumed-llm-adapter-change-event.zh.md: 13f01566472a030cc9d4f97f6e4438fe4c3ecbf8 +2026-06-20-drop-unconsumed-llm-adapter-change-event.zh.md: b26610cfe273820112113c73b9313557cd78262c diff --git a/docs/rfc/implemented/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.zh.md b/docs/rfc/implemented/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.zh.md index 13f0156647..b26610cfe2 100644 --- a/docs/rfc/implemented/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.zh.md +++ b/docs/rfc/implemented/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.zh.md @@ -1,4 +1,4 @@ -# RFC:移除无消费方的 `llm/adapter-change` 事件 +# RFC:移除未被消费的 `llm/adapter-change` 事件 Status: implemented @@ -6,31 +6,31 @@ Status: implemented ## 问题 -`LlmService.registerAdapter()` 在注册和 dispose(资源释放)时发射 `llm/adapter-change`([packages/llm/llm/src/index.ts](../../../../packages/llm/llm/src/index.ts))。在 `packages/*/src` 和 `examples/*/src` 中 grep `llm/adapter-change`,只能找到声明、发射点、文档和测试;没有任何生产代码监听它。 +`LlmService.registerAdapter()` 在注册和 dispose(资源释放)时发出 `llm/adapter-change` 事件([packages/llm/llm/src/index.ts](../../../../packages/llm/llm/src/index.ts))。在 `packages/*/src` 和 `examples/*/src` 中搜索 `llm/adapter-change`,只能找到声明、emit 站点、文档和测试;没有任何生产环境的监听器订阅它。 -这与 `tools/change` 和 `system-prompt/change` 不同。后两个事件目前同样无消费方,但它们是合理的注册表变更信号,未来的实时工具/提示词 UI 可能用到。LLM 适配器注册更像是启动时的实现细节:适配器不是用户可见的面板,真正的模型调用拦截 seam 是 `llm/stream`。保留一个没有监听者的 adapter-change 事件,是 [drop-the-dead-summary](../../implemented/simplification/2026-06-19-drop-mutable-session-summary.md) 模式在更小尺度上的重复。 +这与 `tools/change` 和 `system-prompt/change` 不同。后两个事件目前同样未被消费,但它们是合理的注册表变更信号,未来可能服务于实时工具/提示词 UI。LLM(大语言模型)适配器注册更接近启动时的实现细节:适配器不是用户可见的面板,真正的模型调用拦截 seam 是 `llm/stream`。保留一个没有监听器的 adapter-change 事件,是在更小规模上重复 [drop-the-dead-summary](../../implemented/simplification/2026-06-19-drop-mutable-session-summary.md) 的模式。 -这个事件并非零成本。`registerAdapter()` 在发射 `llm/adapter-change` 之前先 yield 回滚 disposer,这样抛异常的监听者会回退变更而不是泄漏一条适配器条目;包里还有测试覆盖这条监听者抛异常的路径。这种防御性排序所保护的失败模式,只有测试才能触发。 +这个事件并非零成本。`registerAdapter()` 在发出 `llm/adapter-change` 之前先 yield 回滚 disposer,这样抛出异常的监听器会回退变更而非泄漏适配器条目;包内还有针对该监听器抛出路径的测试。这种防御性排序保护的是一个只有测试才能触发的失败模式。 ## 决策 -只移除 `llm/adapter-change`:`dsh-llm` 的 `interface Events` 中的声明、`ctx.emit('llm/adapter-change')` 调用,以及 `LlmService.registerAdapter` JSDoc 中「在注册和 dispose 时发射 `llm/adapter-change`」的描述。`registerAdapter()` 的 effect generator 保留变更与回滚 disposer(用于 HMR(热模块替换)/dispose),但去掉仅为已移除事件而存在的监听者抛异常回滚排序。适配器 disposer 测试断言返回的 disposer 能移除适配器,不再订阅该事件;监听者抛异常的回滚测试随其主题一同移除。[docs/architecture.md](../../../architecture.md) 和 [packages/llm/llm/README.md](../../../../packages/llm/llm/README.md) 中的事件分类体系在同一个变更中更新。 +仅移除 `llm/adapter-change`:`dsh-llm` 的 `interface Events` 中的声明、`ctx.emit('llm/adapter-change')` 调用,以及 `LlmService.registerAdapter` JSDoc 中的 "Emits `llm/adapter-change` on registration and disposal" 语句。`registerAdapter()` 的 effect generator 保留变更与回滚 disposer 以支持 HMR(热模块替换)/dispose,但去掉了仅为已移除事件而存在的监听器抛出回滚排序。适配器 disposer 测试断言返回的 disposer 能移除适配器,而不再订阅该事件;监听器抛出回滚测试随其主题一同移除。[docs/architecture.md](../../../architecture.md) 和 [packages/llm/llm/README.md](../../../../packages/llm/llm/README.md) 中的事件分类体系在同一个变更中更新。 ## 曾考虑的替代方案 ### 为什么不移除所有注册表变更事件? -一个注册表主动广播变更的微内核是一种自洽的约定。`tools/change` 和 `system-prompt/change` 在 UI 能实时刷新可用工具或提示词段落时可能变得有用。本 RFC 在有合理的面向用户消费方的地方保留该约定,仅裁掉当前和可预见未来都没有明确消费方的 adapter-change 事件。 +一个注册表主动广播变更的微内核是一种自洽的约定。`tools/change` 和 `system-prompt/change` 在 UI 能实时刷新可用工具或提示词段落时可能变得有用。本 RFC 保留该约定中有合理的面向用户消费方的部分,仅裁掉当前和可预见未来消费方都不明确的 adapter-change 事件。 -如果将来需要 LLM 适配器浏览器或动态模型选择器,届时再连同消费方一起重新引入该事件,并给出比「something changed」更清晰的 payload。 +如果将来需要 LLM 适配器浏览器或动态模型选择器用到此信号,届时再连同消费方一起重新引入,并提供比「something changed」更清晰的 payload。 ## 验证 -`llm/adapter-change` 及其发射点已移除,重新生成的 cordis catalog 是最新的;HMR 安全性保持(dispose 一个贡献 fiber 会移除对应适配器);`tools/change` 和 `system-prompt/change` 仍有文档和测试;没有任何生产路径的可观测行为发生变化——ACP 快照 golden 和 echo-agent 冒烟测试逐字节不变。 +`llm/adapter-change` 及其 emit 已移除,重新生成的 cordis catalog 是最新的;HMR 安全性保持(dispose 一个贡献 fiber 会移除对应适配器);`tools/change` 和 `system-prompt/change` 仍有文档和测试;没有任何生产路径的可观察行为发生变化——ACP(Agent Client Protocol)快照 golden 和 echo-agent 冒烟测试逐字节未变。 ## 后果 -- **移除一个已文档化的发射事件属于公开接口变更。** 它出现在分类体系表中,读起来像是有意为之的 API。但「已声明并发射」不等于「有消费方」——这正是当初移除可变 summary 时所依据的同一区分。分类体系表在同一个变更中更新,因此文档不会漂移。 -- **注册表变更约定变得不均匀。** 这是可以接受的,因为 LLM 适配器注册与工具或提示词段落不是同一层面的用户可见概念。不均匀但诚实,胜过统一但空转。 +- **移除一个已文档化的 emit 事件属于公开接口变更。** 它出现在分类体系表中,读起来像有意设计的 API。但「已声明且已发出」不等于「已被消费」——这与移除可变 summary 时的判断依据相同。分类体系表在同一个变更中更新,因此文档不会漂移。 +- **注册表变更约定变得不均匀。** 这是可接受的,因为 LLM 适配器注册与工具或提示词段落不是同一层面的面向用户概念。不均匀但诚实,胜过统一但无用。 -这是一个小裁剪,但它退役了一条守护着不存在的消费方的常设正确性不变式。 +这是一个小裁剪,但它退役了一条守护着并不存在的消费方的正确性不变式。 diff --git a/docs/rfc/implemented/simplification/2026-06-20-drop-unconsumed-llm-assembled-surfaces.i18n.yaml b/docs/rfc/implemented/simplification/2026-06-20-drop-unconsumed-llm-assembled-surfaces.i18n.yaml index 46b752f26a..0d895a2b35 100644 --- a/docs/rfc/implemented/simplification/2026-06-20-drop-unconsumed-llm-assembled-surfaces.i18n.yaml +++ b/docs/rfc/implemented/simplification/2026-06-20-drop-unconsumed-llm-assembled-surfaces.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-20-drop-unconsumed-llm-assembled-surfaces.md: c8999dd0e19b2c8eaff854c8ff544bae2fc068b6 -2026-06-20-drop-unconsumed-llm-assembled-surfaces.zh.md: 090d3e779e3e7a9f1f0a65af7740ad685f723cf6 +2026-06-20-drop-unconsumed-llm-assembled-surfaces.zh.md: de297a8cb64d9002fce2c857fd3caa5f2b25f43a diff --git a/docs/rfc/implemented/simplification/2026-06-20-drop-unconsumed-llm-assembled-surfaces.zh.md b/docs/rfc/implemented/simplification/2026-06-20-drop-unconsumed-llm-assembled-surfaces.zh.md index 090d3e779e..de297a8cb6 100644 --- a/docs/rfc/implemented/simplification/2026-06-20-drop-unconsumed-llm-assembled-surfaces.zh.md +++ b/docs/rfc/implemented/simplification/2026-06-20-drop-unconsumed-llm-assembled-surfaces.zh.md @@ -1,4 +1,4 @@ -# RFC:移除未被消费的 LLM 组装便利接口 +# RFC:移除未被消费的 LLM 组装便捷接口 Status: implemented @@ -9,31 +9,31 @@ Status: implemented `LlmService`([packages/llm/llm/src/index.ts](../../../../packages/llm/llm/src/index.ts))在模型之上暴露了三个调用接口: - `stream()`:原始 `StreamChunk`,通过 `llm/stream` waterfall(瀑布式事件)分发。 -- `streamBlocks()`:一个"便利视图",将 chunk 送入 `BlockAssembler` 并按流顺序 yield 已组装完成的 `ContentBlock`([index.ts:137-144](../../../../packages/llm/llm/src/index.ts))。 -- `generate()`:一个完整组装的 `GenerateResult`,通过第二个 `llm/generate` waterfall 分发([index.ts:151-157](../../../../packages/llm/llm/src/index.ts))。 +- `streamBlocks()`:一个「便捷视图」,将分片送入 `BlockAssembler` 并按流顺序产出已组装的 `ContentBlock`([index.ts:137-144](../../../../packages/llm/llm/src/index.ts))。 +- `generate()`:一个完整组装的 `GenerateResult`,通过第二条 `llm/generate` waterfall 分发([index.ts:151-157](../../../../packages/llm/llm/src/index.ts))。 -LLM(大语言模型)服务唯一的生产消费方是 agent loop(智能体循环),它只使用 `stream()`:将原始 chunk 送入自己的 `BlockAssembler`,以便在并行组装的同时记录 chunk 用于回放保真([packages/core/agent-loop/src/loop.ts](../../../../packages/core/agent-loop/src/loop.ts) 中的 `ctx.llm.stream(req)` 步骤)。在 `packages/*/src` 和 `examples/*/src` 中搜索 `streamBlocks` 与 `ctx.llm.generate`,找不到任何生产调用方。引用它们的只有服务方法定义、文档和测试;适配器测试用 `generate()` 作为便利驱动,但它们完全可以通过同一个 assembler 辅助函数手动消费 `stream()`,无需保留一个公开的生产 API。 +LLM(大语言模型)服务唯一的生产消费方是 agent loop(智能体循环),它只使用 `stream()`:将原始分片送入自己的 `BlockAssembler`,以便在并行组装的同时记录分片,保证回放保真度([packages/core/agent-loop/src/loop.ts](../../../../packages/core/agent-loop/src/loop.ts),`ctx.llm.stream(req)` 步骤)。在 `packages/*/src` 和 `examples/*/src` 中 grep `streamBlocks` 与 `ctx.llm.generate`,找不到任何生产调用方。仅有的引用来自服务方法定义、文档和测试;适配器测试用 `generate()` 作为便捷驱动,但它们完全可以通过同一个 assembler 辅助函数手动消费 `stream()`,无需为此保留一个公开的生产 API。 -这与 [drop-mutable-session-summary](../../implemented/simplification/2026-06-19-drop-mutable-session-summary.md) 是同一模式:拥有测试契约的组装视图 API,消费方只有测试而非生产代码。它们是为"不关心 token 级增量"的消费方预先构建的,但唯一的真实消费方恰恰需要增量,以便持久化高保真的回放数据。 +这与 [drop-mutable-session-summary](../../implemented/simplification/2026-06-19-drop-mutable-session-summary.md) 是同一模式:拥有经过测试的契约的组装视图 API,消费方却只有测试而非生产代码。它们是为「不关心 token 级增量」的消费方预设的,但唯一的真实消费方恰恰需要增量,以便持久化高保真回放数据。 -`streamBlocks()` 拖带了 `BlockAssembler` 中一块专用逻辑:`flushReady()` 和 `flushRemaining()`([packages/llm/llm/src/assembler.ts:138-168](../../../../packages/llm/llm/src/assembler.ts))以及 `flushed` 游标字段,仅为支持按序增量 yield 而存在。`generate()` 拖带了 `GenerateResult`、`BlockAssembler.result()` 以及 `llm/generate` waterfall——在同一底层流之上多出的第二个拦截面。agent loop 对 assembler 的使用仅限 `push()` / `message()` / `usage` / `finish`,不涉及流式 flush 或一次性服务组装。 +`streamBlocks()` 拖带了 `BlockAssembler` 的一块专用逻辑:`flushReady()` 与 `flushRemaining()`([packages/llm/llm/src/assembler.ts:138-168](../../../../packages/llm/llm/src/assembler.ts))以及 `flushed` 游标字段,仅为支持按序增量产出而存在。`generate()` 拖带了 `GenerateResult`、`BlockAssembler.result()` 以及 `llm/generate` waterfall——在同一底层流之上的第二个拦截面。agent loop 对 assembler 的使用仅限于 `push()` / `message()` / `usage` / `finish`,不涉及流式 flush 或一次性服务组装。 ## 决策 -`stream()` 是唯一的公开 LLM 调用接口。移除 `streamBlocks`、`generate`、其事件/结果类型,以及仅被该路径使用的 assembler 辅助方法。适配器测试通过本地辅助函数对公开的 stream 进行组装,`BlockAssembler` 只保留有生产消费方的操作。 +`stream()` 是唯一的公开 LLM 调用接口。移除 `streamBlocks`、`generate`、其事件/结果类型,以及仅被该路径使用的 assembler 辅助方法。适配器测试通过本地辅助函数对公开的 stream 进行组装;`BlockAssembler` 仅保留有生产消费方的操作。 ## 曾考虑的替代方案 -**保留 `generate()` 作为仅供测试的便利方法**:否决。适配器测试通过共享 assembler 手动消费 `stream()`,走的是与生产相同的流式路径;一个唯一调用方是测试的公开方法,正是[移除可变摘要先例](2026-06-19-drop-mutable-session-summary.md)所清退的死接口形态。未来如果有消费方需要不带增量的组装块,届时再引入一个有真实消费方的专用辅助方法。 +**保留 `generate()` 作为仅供测试的便捷方法**:否决。适配器测试通过共享 assembler 手动消费 `stream()`,走的是与生产完全相同的流式路径;一个唯一调用方只有测试的公开方法,正是 [drop-mutable-summary 先例](2026-06-19-drop-mutable-session-summary.md)所淘汰的死接口形态。未来如果有消费方需要不带增量的组装块,届时再为该消费方引入一个聚焦的辅助方法。 ## 验证 -`streamBlocks`、`generate`、`llm/generate` 以及仅被它们使用的 assembler 辅助方法已全部移除,无新增死导出;两个真实适配器通过 `stream()` 加共享 assembler 得到充分测试;agent loop 行为不变(ACP 快照 golden 文件无变化);README、架构文档与模块文档中不再提及被移除的接口。 +`streamBlocks`、`generate`、`llm/generate` 及其独占的 assembler 辅助方法已移除,无新增死导出;两个真实适配器通过 `stream()` 和共享 assembler 得到验证;agent loop 行为不变(ACP 快照 golden 文件无变化);README、架构文档与模块文档中不再提及已移除的接口。 ## 后果 -- **从一个核心词汇包中移除了公开方法。** 未来如果有插件需要不带增量的组装块,它需要直接调用 `stream()` 并使用 `BlockAssembler`,或在有真实消费方时重新引入一个专用辅助方法。鉴于预发布阶段「基础优先于投机性未来」的立场([AGENTS.md](../../../../AGENTS.md)),现在正是清除仅供测试的公开形状的正确时机。 -- **适配器测试变得更显式。** 它们失去了便利的 `generate()` 包装层,但这是有益的压力:测试走的是与生产相同的流式路径。 -- **waterfall 使用方失去 `llm/generate`。** 不存在生产监听者。未来的缓存/重试/日志插件应包装 `llm/stream`,它仍是唯一的提供方调用路径。 +- **从一个核心词汇包中移除了公开方法。** 未来如果有插件需要不带增量的组装块,它需要直接调用 `stream()` 并使用 `BlockAssembler`,或在有真实消费方时重新引入一个聚焦的辅助方法。鉴于预发布阶段「基础优先于预设未来」的立场([AGENTS.md](../../../../AGENTS.md)),现在正是裁剪仅供测试的公开接口的合适时机。 +- **适配器测试变得更显式。** 它们失去了便捷的 `generate()` 包装层,但这是有益的压力:测试走的是与生产相同的流式路径。 +- **waterfall 使用者失去 `llm/generate`。** 不存在生产监听者。未来的缓存/重试/日志插件应包装 `llm/stream`,它仍然是唯一的提供方调用路径。 -变更规模不大,但它干净地从 LLM 包中移除了投机性的接口面积,为生产和测试留下唯一一份模型调用契约。 +改动规模不大,但它从 LLM 包中干净地移除了预设的接口面积,为生产和测试留下唯一一份模型调用契约。 diff --git a/docs/rfc/implemented/simplification/2026-06-20-prune-dead-seam-methods.i18n.yaml b/docs/rfc/implemented/simplification/2026-06-20-prune-dead-seam-methods.i18n.yaml index 20c7e3718a..e995e57540 100644 --- a/docs/rfc/implemented/simplification/2026-06-20-prune-dead-seam-methods.i18n.yaml +++ b/docs/rfc/implemented/simplification/2026-06-20-prune-dead-seam-methods.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-20-prune-dead-seam-methods.md: cb3eb576dbae209ddccbea7f42a80ef09f842887 -2026-06-20-prune-dead-seam-methods.zh.md: d3c658ea2ce994e640ec4fd2cf5f74d82f695e69 +2026-06-20-prune-dead-seam-methods.zh.md: 988da3c44d8860d89f090f0ea9e2af49ae9007dd diff --git a/docs/rfc/implemented/simplification/2026-06-20-prune-dead-seam-methods.zh.md b/docs/rfc/implemented/simplification/2026-06-20-prune-dead-seam-methods.zh.md index d3c658ea2c..988da3c44d 100644 --- a/docs/rfc/implemented/simplification/2026-06-20-prune-dead-seam-methods.zh.md +++ b/docs/rfc/implemented/simplification/2026-06-20-prune-dead-seam-methods.zh.md @@ -1,43 +1,43 @@ -# RFC:清理持久化 seam 中的无用方法 +# RFC:从 persistence seam 中移除无用方法 [English](2026-06-20-prune-dead-seam-methods.md) | 中文 Status: implemented -> **实现说明:** 最终只移除了 `SessionPersistence.has()` 和 `.delete()`。`BashExecutor.get()` 和 `.list()` 保留,因为移除它们的单行查找接口需要在消费方引入大量额外的完成状态跟踪机制。它们的 id 品牌化由 [branded-ids RFC](../architecture/2026-06-20-branded-ids.md) 覆盖。 +> **实现说明:** 最终只移除了 `SessionPersistence.has()` 和 `.delete()`。`BashExecutor.get()` 和 `.list()` 保留,因为移除它们的单行查询接口需要在消费方引入大量额外的完成状态追踪机制。它们的 id 品牌化由 [branded-ids RFC](../architecture/2026-06-20-branded-ids.md) 覆盖。 ## 问题 -一个能力 seam([接口/实现/消费方](../../implemented/architecture/2026-06-13-capability-seams.md))携带了没有任何消费方调用的抽象方法。seam 存在的意义是让实现与消费方独立演进,但一个没有消费方编程依赖的方法不是 seam,而是投机性的接口面——每个实现仍然必须实现并测试它。 +一个能力 seam([接口/实现/消费方](../../implemented/architecture/2026-06-13-capability-seams.md))承载着没有任何消费方调用的抽象方法。seam 的存在是为了让实现与消费方独立演进,但一个没有消费方编程依赖的方法不是 seam,而是每个实现仍须实现和测试的投机性接口面。 ### `SessionPersistence.has()` 与 `.delete()` -抽象服务在 create/append 之外声明了更多操作:`load`、`list`、`has`、`delete`。`ctx.sessionPersistence` 的生产消费方只用到两个:agent loop(智能体循环)的恢复路径调用 `load()`([packages/core/agent-loop/src/index.ts:176](../../../../packages/core/agent-loop/src/index.ts)),ACP 桥接层为 `session/list` 调用 `list()`([packages/ui/acp/src/index.ts:494](../../../../packages/ui/acp/src/index.ts))。在 `packages/*/src` 和 `examples/` 中 grep 所有 `sessionPersistence.*` / `persistence.*` 用法,找不到对该服务的 `has(` 或 `delete(` 调用。`packages/ui/acp/src/index.ts` 中的 `.has(`/`.delete(` 调用作用于内存中的 `SessionStore` 和一个本地的 loading id `Set`,而非持久化服务。`has`/`delete` 的唯一调用方是契约测试套件和各后端的 spec。 +该抽象服务在 create/append 之外声明了更多操作:`load`、`list`、`has`、`delete`。`ctx.sessionPersistence` 的生产消费方只用了两个:agent loop(智能体循环)的恢复路径调用 `load()`([packages/core/agent-loop/src/index.ts:176](../../../../packages/core/agent-loop/src/index.ts)),ACP(Agent Client Protocol)桥接层为 `session/list` 调用 `list()`([packages/ui/acp/src/index.ts:494](../../../../packages/ui/acp/src/index.ts))。在 `packages/*/src` 和 `examples/` 中 grep 所有 `sessionPersistence.*` / `persistence.*` 的使用,找不到对该服务的 `has(` 或 `delete(` 调用。`packages/ui/acp/src/index.ts` 中的 `.has(`/`.delete(` 调用作用于内存中的 `SessionStore` 和一个本地的 loading id `Set`,而非 persistence。`has`/`delete` 的唯一调用者是契约测试套件和各后端的 spec。 -`has()` 不仅仅是未使用——它还是共享协调器中最复杂的分支:一个 tracked-vs-untracked 双探测(`loadLive(id, cwd)` 用于活跃跟踪的会话,`loadStored(id)` 用于未跟踪的会话),附带多行注释说明理由。`delete()` 则拖带了 `deleteStored` 后端钩子,每个后端都必须实现它。这与 [drop-mutable-session-summary](../../implemented/simplification/2026-06-19-drop-mutable-session-summary.md) 是同一模式:契约测试覆盖了两者,但没有任何发布代码会问「这个会话是否已持久化?」或删除一个会话。 +`has()` 不仅是未使用——它还是共享协调器中最复杂的分支:一个 tracked-vs-untracked 双探测(`loadLive(id, cwd)` 用于活跃追踪的会话,`loadStored(id)` 用于未追踪的会话),附带多行注释说明理由。`delete()` 则拖带了 `deleteStored` 后端钩子,每个后端都必须实现它。这与 [drop-mutable-session-summary](../../implemented/simplification/2026-06-19-drop-mutable-session-summary.md) 是同一模式:契约测试覆盖了两者,但没有任何发布代码会问「这个会话是否已持久化?」或删除一个会话。 ## 决策 没有消费方使用的方法被移除——从抽象 seam、实现,以及仅为覆盖它们而存在的契约/spec 测试套件中移除: -- `SessionPersistence.has()` / `.delete()` 已移除:抽象声明、协调器的 `has`/`delete`/`deleteCore`,以及 `PersistenceBackend.deleteStored` 钩子(jsonl 和 sqlite 各自实现 `deleteStored` 仅仅是为了满足该钩子——那些实现也一并移除)。后端属于[双后端](../../implemented/architecture/2026-06-14-session-persistence.md)设计,本身不在本 RFC 范围内;移除它们为无消费方实现的钩子是移除钩子的一部分,而非后端重设计。 -- 所有文档和源码注释中的引用都已更新为存活的四方法、仅含 `list()` 的契约——不仅是字面的 `has(`/`delete(`/`deleteStored` 拼写,还包括 `{@link has}`/`{@link delete}` JSDoc 链接和「六个公开方法」之类的计数——涉及 seam 和后端 README、[docs/architecture.md](../../../architecture.md)、[session-persistence](../../implemented/architecture/2026-06-14-session-persistence.md) 与 [write-coordinator](../../implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md) RFC,以及协调器/后端的 JSDoc。 +- `SessionPersistence.has()` / `.delete()` 已移除:抽象声明、协调器的 `has`/`delete`/`deleteCore`,以及 `PersistenceBackend.deleteStored` 钩子(jsonl 和 sqlite 各自实现 `deleteStored` 仅为满足该钩子——这些实现也一并移除)。后端属于[双后端](../../implemented/architecture/2026-06-14-session-persistence.md)设计,本身不在本次范围内;移除它们为无消费方实现的钩子是移除钩子的一部分,而非后端重新设计。 +- 所有文档和源码注释中的引用都已更新为存留的四方法、仅含 `list()` 的契约——不仅是字面的 `has(`/`delete(`/`deleteStored` 拼写,还包括 `{@link has}`/`{@link delete}` JSDoc 链接和「六个公开方法」之类的计数——涉及 seam 和后端 README、[docs/architecture.md](../../../architecture.md)、[session-persistence](../../implemented/architecture/2026-06-14-session-persistence.md) 和 [write-coordinator](../../implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md) RFC,以及协调器/后端的 JSDoc。 ## 曾考虑的替代方案 ### 为什么不以「seam 应当完整」为由保留? -「持久化 seam 理应提供 delete」这种直觉是真实的——而它恰恰是预发布阶段所警惕的投机完整性([AGENTS.md](../../../../AGENTS.md):为正确的基础优化,而非为你并不拥有的假想调用方优化)。`delete()` 只是一个方法,等到消费方真正需要时再加回来即可:一个删除旧会话的会话管理 UI 会需要它——到那时再加,针对该 UI 的真实需求设计(软删除?级联?确认?),而非现在猜测。 +「persistence seam 理应提供 delete」这种直觉是真实的——但它恰恰是预发布阶段所警惕的投机性完整([AGENTS.md](../../../../AGENTS.md):为正确的基础优化,而非为你并不拥有的假想调用者优化)。`delete()` 是一个方法,等消费方真正需要时再加回来即可:一个删除旧会话的会话管理 UI 会需要它——到那时再加,基于该 UI 的真实需求来设计(软删除?级联?确认?),而非现在猜测。 -在有活跃消费方时重新加入一个 seam 方法,成本低且设计更优,因为消费方锁定了契约。无人使用地携带它,意味着每个实现(以及未来的每个后端)都必须实现并测试一个什么也不做的方法。 +在有活跃消费方的情况下重新添加一个 seam 方法,成本低且设计更优,因为消费方锚定了契约。在无人使用的情况下保留它,意味着每个实现(以及未来的每个后端)都必须实现和测试一个无实际作用的方法。 ## 验证 -`has`/`delete`/`deleteStored` 已从持久化 seam、实现和契约测试套件中移除,没有新增无用导出;剩余操作(`create`/`append`/`load`/`list`)未受影响,ACP `session/list` 和崩溃恢复行为完全一致;seam README 和 `docs/architecture.md` 只列出存活的方法。 +`has`/`delete`/`deleteStored` 已从 persistence seam、实现和契约测试套件中移除,没有新增无用导出;剩余操作(`create`/`append`/`load`/`list`)未受影响,ACP `session/list` 和崩溃恢复行为完全一致;seam README 和 `docs/architecture.md` 仅列出存留的方法。 ## 后果 -- **`delete()` 是产品最终会需要的那类操作。** 确实如此——但「最终」正是关键。现在删除、等有真实消费方时再加回来,严格优于发布一份猜测的契约。双后端各自去掉了一个 `deleteStored` 实现,这是在本来不在范围内的包中的有限改动。 -- **低耦合。** 移除局限于持久化 seam + 实现 + 测试;没有跨包消费方引用被移除的方法,因此文档之外没有涟漪效应。 +- **`delete()` 是产品最终会需要的操作。** 确实如此,但「最终」正是关键。现在删除、将来基于真实消费方重新添加,严格优于发布一份猜测的契约。两个后端各自减少了一个 `deleteStored` 实现,这是在本次范围之外的包中的有限改动。 +- **低耦合。** 移除局限于 persistence seam + 实现 + 测试;没有跨包消费方引用被移除的方法,因此除文档外没有涟漪效应。 规模不大,但它将 seam 从「实现必须为无人提供什么」恢复为「恰好是消费方使用的东西」。 diff --git a/docs/rfc/implemented/simplification/2026-06-20-public-agent-stop-surface.i18n.yaml b/docs/rfc/implemented/simplification/2026-06-20-public-agent-stop-surface.i18n.yaml index dd5dd8e4e1..68ba79e73a 100644 --- a/docs/rfc/implemented/simplification/2026-06-20-public-agent-stop-surface.i18n.yaml +++ b/docs/rfc/implemented/simplification/2026-06-20-public-agent-stop-surface.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-20-public-agent-stop-surface.md: 8c371911616b0a156156355b2ca15d795cfd21e5 -2026-06-20-public-agent-stop-surface.zh.md: 31deaa3649026a7579702e8e47edfdf05d2543ae +2026-06-20-public-agent-stop-surface.zh.md: bbd61fa1738fda64ec5e068dae84062163937c1a diff --git a/docs/rfc/implemented/simplification/2026-06-20-public-agent-stop-surface.zh.md b/docs/rfc/implemented/simplification/2026-06-20-public-agent-stop-surface.zh.md index 31deaa3649..bbd61fa173 100644 --- a/docs/rfc/implemented/simplification/2026-06-20-public-agent-stop-surface.zh.md +++ b/docs/rfc/implemented/simplification/2026-06-20-public-agent-stop-surface.zh.md @@ -1,39 +1,39 @@ # RFC:保留单一公开停止原语 -[English](2026-06-20-public-agent-stop-surface.md) | 中文 - Status: implemented +[English](2026-06-20-public-agent-stop-surface.md) | 中文 + > **实现说明:** 仅移除了 `abort()`。`whenIdle()` 予以保留,因为它是公开的静默信号,能安全处理等待者结算与替换轮次竞态;消费方不应从状态转换中自行重建该行为。 ## 问题 -公开的 `Agent` 句柄暴露了两种重叠的方式来停止进行中的工作:`abort(reason?)` 与 `cancel(reason?)`。`abort()` 仅终止当前正在执行的步骤,不影响队列中的工作;`cancel()` 清除队列中的工作与 steering(中途引导),终止正在运行的步骤,并处理步骤前竞态。在生产环境中,ACP(Agent Client Protocol)使用 `cancel()` 实现 `session/cancel`,而生命周期所有者通过 `AgentHandle.dispose()` 拆除 agent。没有生产调用方需要裸 `abort()`。 +公开的 `Agent` 句柄暴露了两种重叠的方式来停止进行中的工作:`abort(reason?)` 和 `cancel(reason?)`。`abort()` 仅终止当前步骤,不影响队列中的工作;`cancel()` 清除队列中的工作和 steering(中途引导)工作、中止正在运行的步骤,并处理步骤前竞态。在生产环境中,ACP(Agent Client Protocol)使用 `cancel()` 实现 `session/cancel`,而生命周期所有者通过 `AgentHandle.dispose()` 销毁 agent(智能体)。没有生产调用方需要裸 `abort()`。 -`abort()` 与 `cancel()` 的区别是真实存在的:`abort()` 保留队列中的提示词和 steering,而 `cancel()` 丢弃它们。但没有已发布的代码调用过公开的 `abort()` 动词。agent loop(智能体循环)自身的停止路径(`cancel()` 与 dispose)直接终止当前 `AbortController`,而非经由 `Agent.abort()` 路由。大多数调用 `abort()` 的测试实际上中断的是空队列,可以改用 `cancel(reason)`;那个刻意依赖队列保留的 steering 重投递测试则直接驱动进行中的 `AbortController`,因为 `cancel()` 会丢弃它试图证明在步骤终止后仍存活的队列 steering。无参 `abort()` 的默认原因(`'aborted'`)随动词一起删除,而非意外保留;`cancel()` 保留自己的默认值 `'cancelled'`。 +`abort()`/`cancel()` 的区别是真实存在的:`abort()` 保留队列中的提示词和 steering,而 `cancel()` 丢弃它们。但没有任何已上线的代码调用过公开的 `abort()` 动词。循环自身的停止路径(`cancel()` 和 disposal)直接中止当前 `AbortController`,而不经由 `Agent.abort()` 路由。大多数调用 `abort()` 的测试中断的是空队列,可以改用 `cancel(reason)`;那个刻意依赖队列保留的 steering 重投递测试则直接驱动进行中的 `AbortController`,因为 `cancel()` 会丢弃它试图证明在步骤中止后仍存活的已排队 steering。无参 `abort()` 的默认原因(`'aborted'`)随该动词一起删除,而非被意外保留;`cancel()` 保留自己的 `'cancelled'` 默认值。 -多余的公开接口面使 agent loop 不得不承载一个本质上是拆除内部机制的公开动词:`abort()` 必须被文档描述为与队列感知的取消不同,尽管 UI 取消几乎总是需要更广义的操作。 +多余的公开接口使得循环不得不承载一个本质上属于内部拆卸的公开动词:`abort()` 必须被文档描述为有别于队列感知的取消,尽管 UI 取消几乎总是需要更广泛的操作。 ## 决策 -`cancel()` 是 `Agent` 上唯一的公开*停止*原语。生命周期所有者使用 `AgentHandle.dispose()` 停止并注销 agent;非所有者使用 `cancel()` 放弃当前与队列中的工作。实现内部保留一个私有 abort controller,但它不属于面向插件的 `Agent` 契约。 +`cancel()` 是 `Agent` 上唯一的公开*停止*原语。生命周期所有者使用 `AgentHandle.dispose()` 停止并注销 agent;非所有者使用 `cancel()` 放弃当前和队列中的工作。实现内部保留一个私有的 abort controller,但它不属于面向插件的 `Agent` 契约。 -`whenIdle()` 作为公开的静默观测原语**予以保留**(agent 脱离 `running` 状态后 resolve;已处于 idle 时立即 resolve;dispose 后等待循环退出)。它不是停止动词;它是非所有者观测停止*完成*而无需 dispose agent 的方式。它的活跃消费方是 ACP 和通过此公开 seam 等待结算的 agent 测试(`packages/ui/acp/tests`、`packages/core/agent-loop/tests`);生产环境的 ACP 桥接层拥有其 agent 并通过 `AgentHandle.dispose()` 拆除它们,因此 `packages/ui/acp/src` 本身没有 `whenIdle()` 调用。 +`whenIdle()` **保留**为公开的静默观测原语(agent 从 `running` 状态稳定后 resolve,已处于 idle 时立即 resolve,dispose 后等待循环退出)。它不是停止动词;它是非所有者在不 dispose agent 的前提下观测停止*完成*的方式。它的活跃消费方是 ACP 和通过此公开 seam 等待结算的 agent 测试(`packages/ui/acp/tests`、`packages/core/agent-loop/tests`);生产环境的 ACP 桥接层拥有其 agent 并通过 `AgentHandle.dispose()` 销毁它们,因此 `packages/ui/acp/src` 本身没有 `whenIdle()` 调用。 -公开的 `abort()` 被删除,连同将其作为独立 API 测试的用例以及将步骤级终止描述为嵌入特性的文档。空队列终止测试迁移到 `cancel(reason)`,仍然验证取消行为;测试对象为 agent loop 内部 `AbortController` 的测试通过包内类型转换直接驱动该 controller 的私有字段;仅固定已移除的无参 `abort()` 默认值的测试随方法一起删除。disposer 仍为异步,仍等待循环停止。 +公开的 `abort()` 被删除,连同将其作为独立 API 测试的用例以及将步骤级中止描述为嵌入特性的文档。空队列中止测试迁移到 `cancel(reason)`,仍然验证取消行为;以循环内部 `AbortController` 为测试对象的用例通过包内类型转换直接驱动该 controller 的私有字段;仅固定已移除的无参 `abort()` 默认值的测试随方法一起删除。disposer 仍为异步,仍等待循环停止。 ## 曾考虑的替代方案 -**同时移除 `whenIdle()`**:最初提案的形态,在对照代码验证前提后被推翻(上方的实现说明记录了完整过程):它是承重的静默原语,强迫消费方手动观测 `running`→`idle` 转换正是防御性模式所警告的脆弱路径。 +**同时移除 `whenIdle()`**:最初提案的形态,在对照代码验证前提后被推翻(上方的实现说明记录了完整过程):它是承重的静默原语,迫使消费方手动观测 `running`→`idle` 转换正是防御性模式所警告的脆弱路径。 ## 验证 -`Agent` 不再暴露公开的 `abort()`,而 `cancel()`、`whenIdle()` 与 `steer()` 保留;ACP 取消调用 `cancel()`;拆除通过 handle disposal 等待静默,`whenIdle()` 为非所有者观测者在静默时 resolve;测试套件覆盖取消与 disposal 作为两条受支持的停止路径。 +`Agent` 不再暴露公开的 `abort()`,而 `cancel()`、`whenIdle()` 和 `steer()` 保留;ACP 取消调用 `cancel()`;拆卸通过 handle disposal 等待静默,`whenIdle()` 在静默时为非所有者观测者 resolve;测试套件覆盖取消和 disposal 作为两条受支持的停止路径。 ## 后果 -未来的插件无法通过公开接口仅终止当前模型/工具步骤而保留队列中的提示词。如果该用例变为现实需求,它应当带着一个具名消费方和更窄的契约重新引入。目前它只是把一个私有循环机制暴露为公开接口的潜在泛化。 +未来的插件无法通过公开接口仅中止当前模型/工具步骤而保留队列中的提示词。如果该用例变为现实需求,它应当带着一个具名消费方和更窄的契约回归。目前它是将私有循环机制保持公开的潜在泛化。 ## 相关 -本 RFC 仅移除冗余的停止动词。中途 steering 仍是有意保留的消息路径;静默观测仍通过 `whenIdle()` 提供。最终的公开接口面为 `send()`、`steer()`、`inject()`、`cancel()`、`whenIdle()`、status、options、session 与 identity。 +本 RFC 仅移除冗余的停止动词。中途 steering 仍是有意保留的消息路径;静默观测仍通过 `whenIdle()` 提供。最终的公开接口为 `send()`、`steer()`、`inject()`、`cancel()`、`whenIdle()`、status、options、session 和 identity。 diff --git a/docs/rfc/implemented/simplification/2026-06-20-remove-agent-boundary-mirror-events.i18n.yaml b/docs/rfc/implemented/simplification/2026-06-20-remove-agent-boundary-mirror-events.i18n.yaml index 4f09cbbc3b..ec11d8ea9a 100644 --- a/docs/rfc/implemented/simplification/2026-06-20-remove-agent-boundary-mirror-events.i18n.yaml +++ b/docs/rfc/implemented/simplification/2026-06-20-remove-agent-boundary-mirror-events.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-20-remove-agent-boundary-mirror-events.md: 46b5e43951885915d4c3dd3f867ced6c31d32035 -2026-06-20-remove-agent-boundary-mirror-events.zh.md: dd6952ec8923c17d703fc6850197bef09b1c0ee7 +2026-06-20-remove-agent-boundary-mirror-events.zh.md: 15be07f43997b1d899f0297d311c3ad83f088ee0 diff --git a/docs/rfc/implemented/simplification/2026-06-20-remove-agent-boundary-mirror-events.zh.md b/docs/rfc/implemented/simplification/2026-06-20-remove-agent-boundary-mirror-events.zh.md index dd6952ec89..15be07f439 100644 --- a/docs/rfc/implemented/simplification/2026-06-20-remove-agent-boundary-mirror-events.zh.md +++ b/docs/rfc/implemented/simplification/2026-06-20-remove-agent-boundary-mirror-events.zh.md @@ -1,32 +1,32 @@ # RFC:停止将持久化边界镜像为 agent 事件 -Status: implemented - [English](2026-06-20-remove-agent-boundary-mirror-events.md) | 中文 +Status: implemented + ## 问题 -agent loop(智能体循环)曾通过可回放的 `SessionEvent` 日志和实时 `agent/*` 镜像两条路径暴露持久化的轮次与步骤边界。消费方不得不在两个表达同一事实的来源之间做选择,并协调二者的时序。ACP(Agent Client Protocol)和持久化层已经使用事件日志;stdio UI 是唯一仍在消费镜像事件的组件,而它也已经从 `session/event` 渲染工具调用和工具结果。 +agent loop(智能体循环)通过可回放的 `SessionEvent` 日志和实时 `agent/*` 镜像两条路径暴露持久化的轮次与步骤边界。消费方不得不在同一事实的两个来源之间做选择,并协调二者的时序。ACP(Agent Client Protocol)和持久化层已经使用日志;stdio UI 是唯一仍在消费镜像的组件,而它已经从 `session/event` 渲染工具调用和工具结果。 -这种重复并非零成本。每次生命周期变更都要同时更新会话事件、镜像事件、文档、不变式、测试和快照预期。重复的边界事件还使失败排序变得微妙:一个轮次可能在实时 `agent/turn-end` 监听器运行之前就已被持久化关闭,因此边界之后的监听器失败在日志中已没有合法的位置可插入,只能带外报告。 +这种重复并非零成本。每次生命周期变更都需要同时更新会话事件、镜像事件、文档、不变式、测试和快照预期。重复的边界事件还使失败排序变得微妙:一个轮次可能在实时 `agent/turn-end` 监听器运行之前就已被持久化关闭,因此边界之后的监听器失败在日志中已没有合法位置可以插入,只能带外上报。 ## 决策 -让 `session/event` 成为唯一的实时边界/transcript(文本记录)流。需要渲染轮次、工具调用、工具结果、助手消息和持久化边界的消费方统一订阅 `session/event`,从持久化层使用的同一套事件词汇派生 UI。 +将 `session/event` 作为唯一的实时边界/transcript(文本记录)流。需要渲染轮次、工具调用、工具结果、助手消息和持久化边界的消费方统一订阅 `session/event`,从持久化层使用的同一套事件词汇中派生 UI。 -移除 `agent/turn-start`、`agent/turn-end`、`agent/step-start` 和 `agent/step-end`。边界消费方改为订阅 `session/event`。需要 agent 标签的 UI 通过 `agent/created` 和 `agent/disposed` 维护一份 session 到 agent 的映射,因为持久化的 `turn/start` 携带轮次编号但不携带 agent id。 +移除 `agent/turn-start`、`agent/turn-end`、`agent/step-start` 和 `agent/step-end`。边界消费方改为订阅 `session/event`。如果 UI 需要 agent 标签,则通过 `agent/created` 和 `agent/disposed` 维护一份 session 到 agent 的映射,因为持久化的 `turn/start` 携带轮次编号但不携带 agent id。 -步骤镜像没有消费方,已由 [event-domain-semantics RFC](../architecture/2026-06-30-event-domain-semantics.md) 率先移除。该决策保留了轮次镜像供 stdio UI 使用;本 RFC 在将测试 REPL 迁移到 `session/event` 加 id 映射之后,将轮次镜像也一并移除。 +步骤镜像已无消费方,由 [event-domain-semantics RFC](../architecture/2026-06-30-event-domain-semantics.md) 先行移除。该决策保留了轮次镜像供 stdio UI 使用;本 RFC 在将测试 REPL 迁移到 `session/event` 加 id 映射之后,将轮次镜像也一并移除。 ## 范围:移除什么、不移除什么 -本决策仅涉及持久化的轮次与步骤边界。`agent/steering` 镜像的是一条控制记录,`agent/stream-chunk` 镜像的是 token 流,因此各自单独处理:见 [steering](2026-07-04-remove-agent-steering-mirror.md) 和 [stream chunks](2026-07-02-remove-stream-chunk-mirror.md)。`agent/created`、`agent/disposed`、`agent/status`、`agent/error` 和 `agent/queued` 仍作为实时生命周期或控制事件保留,而非 transcript 镜像;排队的输入可能在任何持久化事件产生之前就被取消。 +本决策仅涉及持久化的轮次与步骤边界。`agent/steering` 镜像的是一条控制记录,`agent/stream-chunk` 镜像的是 token 流,因此各自单独处理:[steering](2026-07-04-remove-agent-steering-mirror.md) 与 [stream chunks](2026-07-02-remove-stream-chunk-mirror.md)。`agent/created`、`agent/disposed`、`agent/status`、`agent/error` 和 `agent/queued` 仍作为实时生命周期或控制事件保留,而非 transcript 镜像;排队中的输入可能在任何持久化事件产生之前就被取消。 ## 曾考虑的替代方案 -- **在同一个变更中移除 `agent/steering`**:否决,因为它镜像的是控制记录而非边界。 -- **为 stdio UI 保留轮次镜像**:否决,因为 UI 可以渲染 `session/event` 并从 id 映射中恢复 agent 标签。 +- **在同一个变更中一并移除 `agent/steering`**:否决,因为它是控制记录的镜像而非边界镜像。 +- **为 stdio UI 保留轮次镜像**:否决,因为 UI 可以渲染 `session/event` 并通过 id 映射恢复 agent 标签。 ## 后果 -插件不再能从便捷的 `Agent` 优先事件中观察轮次/步骤边界。它必须订阅 `session/event` 或自行维护 session 到 agent 的关联。这是可接受的取舍:边界消费方不应依赖一条可能与持久化日志产生漂移的第二事件源。 +插件不再能从便捷的 `Agent` 优先事件中观察轮次/步骤边界,必须订阅 `session/event` 或自行维护 session 到 agent 的关联。这是可接受的取舍:边界消费方不应依赖一条可能与持久化日志产生漂移的第二事件源。 diff --git a/docs/rfc/implemented/simplification/2026-06-26-fsspec-style-fs-seam.i18n.yaml b/docs/rfc/implemented/simplification/2026-06-26-fsspec-style-fs-seam.i18n.yaml index f096620016..aecab4f6b5 100644 --- a/docs/rfc/implemented/simplification/2026-06-26-fsspec-style-fs-seam.i18n.yaml +++ b/docs/rfc/implemented/simplification/2026-06-26-fsspec-style-fs-seam.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-26-fsspec-style-fs-seam.md: 493af9341177aaaed4cdca03a3c20f326c5c4dac -2026-06-26-fsspec-style-fs-seam.zh.md: ba6a366990f749c5fb84e30142965dc5a2d1d0b7 +2026-06-26-fsspec-style-fs-seam.zh.md: 4aa9b260396c22433ea9a4c9af0b4101fa6895e7 diff --git a/docs/rfc/implemented/simplification/2026-06-26-fsspec-style-fs-seam.zh.md b/docs/rfc/implemented/simplification/2026-06-26-fsspec-style-fs-seam.zh.md index ba6a366990..4aa9b26039 100644 --- a/docs/rfc/implemented/simplification/2026-06-26-fsspec-style-fs-seam.zh.md +++ b/docs/rfc/implemented/simplification/2026-06-26-fsspec-style-fs-seam.zh.md @@ -1,21 +1,21 @@ -# RFC:拆分文件系统 seam——提供方文本变更与 `dsh-fs-policy` 插件 - -Status: implemented +# RFC:拆分文件系统 seam——提供方文本变更操作与 `dsh-fs-policy` 插件 [English](2026-06-26-fsspec-style-fs-seam.md) | 中文 +Status: implemented + ## 问题 -[filesystem-capability-seam](../../implemented/architecture/2026-06-17-filesystem-capability-seam.md) 引入的文件系统能力目前让一个抽象 `FileSystem` 服务同时承担两类职责: +[filesystem-capability-seam](../../implemented/architecture/2026-06-17-filesystem-capability-seam.md) 中引入的文件系统能力目前让一个抽象的 `FileSystem` 服务承担两类不同的职责: -1. **提供方操作**——解析目标、stat/版本元数据、文本读取/流式读取、原子写入,以及带守卫的字面编辑。 -2. **面向 agent 的策略**——行窗口、字面编辑语义,以及读后写/编辑的 observed-state。 +1. **提供方操作**——解析目标、stat/版本元数据、文本读取/流式读取、原子写入,以及受保护的字面编辑。 +2. **面向 agent(智能体)的策略**——行窗口、字面编辑语义,以及读后写/编辑的观测状态。 -这导致每个未来的后端都要重新实现面向模型的读取语义和观测策略。`readPage` 返回带行号的行和视图元数据;基类服务按 owner 存储文件状态,并区分 `full` 与 `partial` 读取。这些是有用的策略,但它们不是文件系统提供方的原语。字面文本变更则不同:版本守卫、字面匹配、歧义检测与原子重写必须在提供方变更边界内保持一体,但当前的 `applyEdit` 命名及其周围的 seam 把这个提供方操作绑定到了旧的读后编辑策略形状上。 +这导致每个未来的后端都要重新实现面向模型的读取语义和观测策略。`readPage` 返回带行号的行和视图元数据;基础服务按 owner 存储文件状态,并区分 `full` 与 `partial` 读取。这些是有用的策略,但它们不是文件系统提供方的原语。字面文本变更则不同:版本守卫、字面匹配、歧义检测与原子重写必须留在提供方的变更边界内,但当前的 `applyEdit` 命名及其周围的 seam 将这一提供方操作绑定到了旧的读后编辑策略形状上。 -这还造成了一个真实的 UX 死胡同:窗口化读取记录 `view: partial`,而 partial 视图无法授权 `edit`。一个模型读取了大文件的第 100-150 行,除非先获得一次 `full` 读取,否则无法编辑第 120 行——而对于超过读取上限的文件,full 读取可能不可行。字面编辑真正需要的只是新鲜度:被匹配的字节必须仍来自模型所读的那个版本。 +这还造成了一个真实的用户体验死胡同:窗口化读取记录 `view: partial`,而 partial 视图无法授权 `edit`。一个模型读取了大文件的第 100-150 行,如果想编辑第 120 行,就必须先获取一次 `full` 读取,而对于超过读取上限的文件这可能做不到。字面编辑实际上只需要新鲜度:被匹配的字节仍然来自模型所读取的那个版本即可。 -旧 RFC 已经推迟了独立的 `@deepseek-ai/dsh-fs-policy` 包。本 RFC 构建该层,并让 `ctx.fs` 贴近 fsspec 风格的存储原语(`info`/`cat`/`open`),但不将其变成完整的 fsspec。 +旧 RFC 已经推迟了独立的 `@deepseek-ai/dsh-fs-policy` 包(package)。本 RFC 构建该层,并让 `ctx.fs` 贴近 fsspec 风格的存储原语(`info`/`cat`/`open`),但不将其变成完整的 fsspec。 ## 决策 @@ -28,13 +28,13 @@ provider seam dsh-fs ctx.fs: text IO + atomic mutation primitives (op provider dsh-fs-local local implementation of ctx.fs ``` -`dsh-tool-fs` 保持相同的面向模型的 `read`/`write`/`edit` schema。它是执行器:注入 `fs`(不是策略服务)并直接访问 `ctx.fs`,拥有读取窗口化逻辑,并派发 `fs/*` 事件以便 `dsh-fs-policy` 进行门控和记录。 +`dsh-tool-fs` 保持相同的面向模型的 `read`/`write`/`edit` schema。它是执行器:注入 `fs`(不是策略服务)并直接访问 `ctx.fs`,拥有读取窗口化逻辑,并分发 `fs/*` 事件以便 `dsh-fs-policy` 进行门控和记录。 本 RFC 决定了四层拆分、提供方契约和新鲜度策略。工具↔策略的**耦合方式**随后由[事件门控 RFC](../architecture/2026-06-26-file-context-as-event-gate.md) 细化:`dsh-fs-policy` 是一个门控**插件**,通过 `fs/*` 事件参与而非提供 `ctx.fileContext` 方法服务,因此工具不与它产生方法耦合,读取窗口化与 fs I/O 留在 `dsh-tool-fs` 中。本文描述的是最终落地的事件门控形态;提供方的版本守卫是可选的(省略 = 无条件裸提供方)。 ## 提供方契约 -`@deepseek-ai/dsh-fs` 收缩为提供方文本 IO 加带守卫的文本变更: +`@deepseek-ai/dsh-fs` 收缩为提供方文本 IO 加受保护的文本变更: ```ts ignore-check abstract resolve(path: string): Promise<FsTarget> @@ -55,76 +55,76 @@ type FsWriteIntent = | { kind: 'replaceIfVersion'; version: FsVersion } ``` -`stat` 返回元数据而非内容。`version` 是新鲜度令牌;`type` 让执行器在读取前拒绝目录/特殊文件;`size` 让 `read` 工具无需通过失败来探测即可选择 `readText` 还是 `streamText`。返回 `undefined` 表示目标不存在。 +`stat` 返回元数据而非内容。`version` 是新鲜度令牌;`type` 让执行器在读取前拒绝目录/特殊文件;`size` 让 `read` 工具无需通过失败探测即可选择 `readText` 还是 `streamText`。`undefined` 表示目标不存在。 `readText` 读取整个常规文本文件。`streamText` 以相同的文本语义流式读取大文件。两个提供方原语负责常规文件检查、UTF-8 解码、二进制/NUL 拒绝以及 `FS_NOT_TEXT`;策略层从不处理原始字节,也不重新实现跨分片解码。`readText` 是小文件/直接全文件原语,而面向模型的大文件读取使用 `streamText`。 -`writeText` 是原子性的临时文件 + rename,带有显式的写入意图。`createIfAbsent` 创建不存在的目标,对已存在的目标以 `FS_NOT_OBSERVED` 拒绝;这是 owner 没有先前读取时使用的路径。`replaceIfVersion` 仅在目标以观测到的版本存在时替换;目标不存在或版本不匹配时抛出 `FS_STALE_VERSION`。 +`writeText` 是原子的临时文件 + rename,带有显式的写入期望。`createIfAbsent` 创建不存在的目标,对已存在的目标以 `FS_NOT_OBSERVED` 拒绝;这是 owner 没有先前读取时使用的路径。`replaceIfVersion` 仅在目标以观测到的版本存在时替换;目标不存在或版本不匹配时抛出 `FS_STALE_VERSION`。 -`editText` 是提供方级别的带守卫文本变更。启用守卫时,它先验证目标仍以 `expected.version` 存在,然后读取当前文本、应用字面替换并原子写入。陈旧检查必须在字面匹配之前发生,这样基于旧读取的编辑会报告 `FS_STALE_VERSION`,而不是对更新内容做匹配后报告 `FS_EDIT_NOT_FOUND` 或 `FS_AMBIGUOUS_EDIT`。将此原语保留在提供方 seam 上,保持了后端本地锁定能力,也让未来的远程后端可以实现原生的 compare-and-edit 而无需策略层拉取整个文件。 +`editText` 是提供方级别的受保护文本变更。启用守卫时,它首先验证目标仍以 `expected.version` 存在,然后读取当前文本、应用字面替换并原子写入。过期检查必须在字面匹配之前发生,这样基于旧读取的编辑会报告 `FS_STALE_VERSION`,而不是对更新内容进行匹配后报告 `FS_EDIT_NOT_FOUND` 或 `FS_AMBIGUOUS_EDIT`。将此原语保留在提供方 seam 上,保持了后端本地锁定的能力,也让未来的远程后端能够实现原生的 compare-and-edit,而无需策略层拉取整个文件。 -这是一个*文本存储* seam,刻意比字节级 fsspec(`cat`/`open` 返回原始字节)高半层。UTF-8 解码、二进制/NUL 拒绝、带守卫的全文件写入和带守卫的字面文本编辑都在提供方内完成,使策略层从不接触原始字节、不重新实现跨分片解码、也不将陈旧检查与变更临界区分离。面向模型的概念仍然不下沉到提供方:行窗口、带行号的行、渲染的页脚、observed-state 存储都不会泄漏下去。 +这是一个*文本存储* seam,刻意比字节级 fsspec(`cat`/`open` 返回原始字节)高半个层次。UTF-8 解码、二进制/NUL 拒绝、受保护的全文件写入和受保护的字面文本编辑都在提供方内完成,因此策略层从不接触原始字节、不重新实现跨分片解码、也不将过期检查与变更临界区分离。面向模型的概念仍然不下沉到提供方:行窗口、带行号的行、渲染的页脚、观测状态存储都不会泄漏下去。 -从 `dsh-fs` 中删除:`readPage`、`FsExpectation`、`FsView`、`FsStateSource`、`FsReadRequest`、`FsTextLine`、行/窗口常量、`formatReadBody`,以及 observed-state `WeakMap`。`applyEdit` 被更窄的提供方原语 `editText` 取代,后者的契约是版本守卫的字面文本变更,而非策略层的读取授权。`FS_PARTIAL_OBSERVATION` 错误码也从 `FsErrorCode` 分类体系中移除:新鲜度授权没有 partial/full 之分,因此没有任何场景会抛出它。`FsTargetKey` 和 `FsVersion` 按照既有的 [branded-ids RFC](../../implemented/architecture/2026-06-20-branded-ids.md) 成为品牌化的不透明 id。 +从 `dsh-fs` 中删除的内容:`readPage`、`FsExpectation`、`FsView`、`FsStateSource`、`FsReadRequest`、`FsTextLine`、行/窗口常量、`formatReadBody`,以及观测状态 `WeakMap`。`applyEdit` 被更窄的提供方原语 `editText` 取代,后者的契约是版本守卫的字面文本变更,而非策略层的读取授权。`FS_PARTIAL_OBSERVATION` 错误码也从 `FsErrorCode` 分类体系中移除:新鲜度授权没有 partial/full 之分,因此没有什么能触发它。`FsTargetKey` 和 `FsVersion` 按照既有的 [branded-ids RFC](../../implemented/architecture/2026-06-20-branded-ids.md) 成为品牌化的不透明 id。 ## 策略契约 -`@deepseek-ai/dsh-fs-policy` 是一个插件而非服务:它不注册任何 `ctx.*` 键,也不注入任何东西。它拥有写入/编辑新鲜度策略和 observed-state——这些不属于 `FileSystem` 提供方基类(否则沙箱化/远程后端会继承它无需承担的面向模型的观测策略)。它通过执行器派发的 `fs/*` 事件门控来贡献这些策略。(本 RFC 最初提出了一个具体的 `ctx.fileContext` 方法服务,带 `read`/`write`/`edit` 方法;[事件门控 RFC](../architecture/2026-06-26-file-context-as-event-gate.md) 将其细化为此处描述的门控插件,使工具从不与策略产生方法耦合。) +`@deepseek-ai/dsh-fs-policy` 是一个插件,不是服务:它不注册任何 `ctx.*` 键,也不注入任何东西。它拥有写入/编辑新鲜度策略和观测状态,这些不属于 `FileSystem` 提供方基类(否则沙箱/远程后端会继承它无需承载的面向模型的观测策略)。它通过执行器分发的 `fs/*` 事件门控贡献该策略。(本 RFC 最初提出了一个具体的 `ctx.fileContext` 方法服务,带有 `read`/`write`/`edit` 方法;[事件门控 RFC](../architecture/2026-06-26-file-context-as-event-gate.md) 将其改造为此处描述的门控插件,使工具从不与策略产生方法耦合。) -Observed state 以 `WeakMap<owner, Map<targetKey, FsVersion>>` 形式存在于此。当且仅当 owner 读取、写入或编辑过该目标时条目才存在(每次成功都会发出 `fs/observed`),因此条目的存在*本身就是*先前观测记录——没有单独的 `hasRead` 标志。owner 从不透明的事件 actor(`{ agent?: { session? } }`)结构化派生,该形状定义在 `dsh-fs-policy` 中而非 `dsh-fs` 中。 +观测状态以 `WeakMap<owner, Map<targetKey, FsVersion>>` 的形式存放于此。当且仅当 owner 读取、写入或编辑过该目标时,条目才存在(每次成功都会发出 `fs/observed`),因此条目的存在*本身就是*先前观测的记录——没有单独的 `hasRead` 标志。owner 从不透明的事件 actor(`{ agent?: { session? } }`)结构化派生,该形状定义在 `dsh-fs-policy` 中而非 `dsh-fs` 中。 该插件决定三个 `fs/*` 事件: - `fs/write-intent`——无先前观测 ⇒ `{ kind: 'createIfAbsent' }`(只有新文件可以盲创建);有先前观测 ⇒ `{ kind: 'replaceIfVersion', version: vObserved }`(已有文件仅在自观测以来未变时才替换)。单槽决策;不调用 `next()`。 -- `fs/edit-intent`——要求 owner 有先前观测(否则 `FS_NOT_OBSERVED`);返回 `{ version: vObserved }` 作为 CAS 基础。它不实现字面替换——它授权并提供版本,提供方的变更临界区负责应用守卫,因此基于同一观测版本的并发编辑仍然是一个赢/一个陈旧。 -- `fs/observed`——在成功的读取/写入/编辑后为该 owner+target 记录 `{ version }`。同步、仅副作用的 `WeakMap.set`。 +- `fs/edit-intent`——要求 owner 有先前观测(否则 `FS_NOT_OBSERVED`);返回 `{ version: vObserved }` 作为 CAS 基础。它不实现字面替换——它授权并提供版本,提供方的变更临界区负责应用守卫,因此基于同一观测版本的并发编辑仍然是一赢一过期。 +- `fs/observed`——在成功的读取/写入/编辑后,为该 owner+target 记录 `{ version }`。同步、仅副作用的 `WeakMap.set`。 -该插件不做任何文件系统 I/O:「你是否观测过这个文件?」是一次 `WeakMap` 查找,而「你读到的版本是否仍然是当前版本?」在 `ctx.fs.editText`/`writeText` 内部的同一原子锁中决定(该锁同时执行变更)——插件只提供 `vObserved` 作为基础。 +该插件不做任何文件系统 I/O:「你是否观测过此文件?」是一次 `WeakMap` 查找,而「你读取的版本是否仍然是当前版本?」在 `ctx.fs.editText`/`writeText` 内部、与执行变更相同的原子锁中决定——插件只提供 `vObserved` 作为基础。 ## 工具契约 -`dsh-tool-fs` 保持相同的 schema 和提示词表面。`read` 仍暴露 `file_path`、`offset` 和 `limit`;`write` 和 `edit` 不变。它是执行器:验证模型参数,通过 `ctx.fs` 直接读取/写入/编辑,拥有行窗口化和结果渲染(`N: text`、页脚、`<path>/<content>` 信封),并派发 `fs/*` 事件。 +`dsh-tool-fs` 保持相同的 schema 和提示词表面。`read` 仍然暴露 `file_path`、`offset` 和 `limit`;`write` 和 `edit` 不变。它是执行器:验证模型参数,通过 `ctx.fs` 直接读取/写入/编辑,拥有行窗口化和结果渲染(`N: text`、页脚、`<path>/<content>` 信封),并分发 `fs/*` 事件。 -每次变更先派发其 intent waterfall(瀑布式事件)并以 `undefined` 作为裸提供方默认值,然后调用 `ctx.fs`,再发出 `fs/observed`:例如 `write` 执行 `ctx.waterfall('fs/write-intent', target, exec, () => undefined)` → `ctx.fs.writeText(target, content, intent)` → `ctx.emit('fs/observed', …)`。`read` 做一次 stat、读取/流式读取、构建窗口,然后发出 `fs/observed`。将 `exec` 作为 actor 传入,让 `dsh-fs-policy` 无需工具深入策略即可派生 owner。 +每个变更操作先分发其 intent waterfall(瀑布式事件),带有 `undefined` 裸提供方默认值,然后调用 `ctx.fs`,再发出 `fs/observed`。例如 `write` 执行 `ctx.waterfall('fs/write-intent', target, exec, () => undefined)` → `ctx.fs.writeText(target, content, intent)` → `ctx.emit('fs/observed', …)`。`read` 先 stat 一次,然后读取/流式读取,构建窗口,最后发出 `fs/observed`。将 `exec` 作为 actor 传递,让 `dsh-fs-policy` 无需工具深入策略即可派生 owner。 -由于策略通过带 `undefined` 默认值的事件贡献,`dsh-tool-fs` 不与 `dsh-fs-policy` 产生方法耦合:插件不存在时,每个 intent waterfall 落入 `undefined`(无条件裸提供方写入/编辑),`fs/observed` 无监听者。加载插件后即叠加读后写/编辑策略。 +由于策略通过带有 `undefined` 默认值的事件贡献,`dsh-tool-fs` 不与 `dsh-fs-policy` 产生方法耦合:在插件缺席时,每个 intent waterfall 都落到 `undefined`(无条件裸提供方写入/编辑),`fs/observed` 没有监听器。加载插件后即可叠加读后写/编辑策略。 ## 并发边界 -进程内更新是安全的:本地后端保持既有的按目标变更锁,因此版本检查-然后-rename 是串行化的,失败的更新看到 `FS_STALE_VERSION`。 +进程内更新是安全的:本地后端保持既有的按目标变更锁,因此版本检查-然后-rename 是串行化的,失败的更新会看到 `FS_STALE_VERSION`。 -进程内创建由同一按目标变更锁守卫:两个调用者以 `createIfAbsent` 竞争时串行化,一个创建成功,下一个看到目标已存在并收到 `FS_NOT_OBSERVED`。跨进程创建仅尽力而为;本地的 stat-then-rename 守卫无法在所有未来后端上提供可移植的排他创建保证。 +进程内创建由同一个按目标变更锁保护:两个调用者以 `createIfAbsent` 竞争时串行化,一个创建成功,另一个看到目标已存在并收到 `FS_NOT_OBSERVED`。跨进程创建仅为尽力而为;本地的 stat-then-rename 守卫无法在所有未来后端上提供可移植的排他创建保证。 -跨进程写入是尽力新鲜度加原子替换:`mtime:size` 通常能捕获编辑器保存,但同一时刻相同大小的写入可能遗漏;原子性的 temp+rename 防止文件撕裂但不能防止所有丢失更新。 +跨进程写入是尽力而为的新鲜度加原子替换:`mtime:size` 通常能捕获编辑器保存,但同一 tick 相同大小的写入可能遗漏;原子的 temp+rename 防止文件撕裂但不能防止所有丢失更新。 ## 取代 -本 RFC 逆转了 [filesystem-capability-seam](../../implemented/architecture/2026-06-17-filesystem-capability-seam.md) 的两项决策,并收窄了第三项: +本 RFC 逆转了 [filesystem-capability-seam](../../implemented/architecture/2026-06-17-filesystem-capability-seam.md) 中的两项决策,并收窄了第三项: -- 读后写/编辑策略从 `ctx.fs` 移出,进入 `dsh-fs-policy` 插件(在 `fs/*` 事件门控上)。 +- 读后写/编辑策略从 `ctx.fs` 移出,进入 `dsh-fs-policy` 插件(通过 `fs/*` 事件门控)。 - 文本读取不再返回后端编号的行记录或 `full`/`partial` 视图;授权基于版本新鲜度,因此窗口化读取在文件未变时即可授权编辑。 -- 字面编辑不再位于旧的 `applyEdit` API 之后(该 API 混合了后端变更与 seam 拥有的观测策略)。它作为 `editText` 保留为提供方原语,因为版本守卫 + 字面匹配 + 原子重写必须在提供方的变更临界区内保持一体,以确保正确的错误归因和并发行为。 +- 字面编辑不再位于旧的 `applyEdit` API 之后(该 API 混合了后端变更与 seam 拥有的观测策略)。它作为 `editText` 保留为提供方原语,因为版本守卫 + 字面匹配 + 原子重写必须留在提供方的变更临界区内。 保留的内容:接口/实现/消费方纪律、消费方不导入后端规则、后端定义的 target/version/display 元数据、原子本地写入,以及共享的 `FsError` 分类体系。 ## 验证 -`dsh-fs` 精确暴露 `resolve`/`stat`/`readText`/`streamText`/`writeText`/`editText`(`stat` 返回 `FsInfo | undefined`,`writeText` 接受 `FsWriteIntent`),已删除的类型/原语不再存在;`dsh-fs-local` 不携带行、视图或 `formatReadBody` 逻辑;面向模型的 schema 逐字节未变。测试固定了以下行为:窗口化读取可以授权对未变文件的后续编辑;基于陈旧读取的编辑在尝试字面匹配之前报告 `FS_STALE_VERSION`;版本 CAS 行为得到保持;观测契约成立(通过 `read` 工具的读取记录 observed-state;直接的 `ctx.fs` 读取不记录);`dsh-fs-policy` 具有 HMR/dispose 覆盖率。 +`dsh-fs` 精确暴露 `resolve`/`stat`/`readText`/`streamText`/`writeText`/`editText`(`stat` 返回 `FsInfo | undefined`,`writeText` 接受 `FsWriteIntent`),已删除的类型/原语不再存在;`dsh-fs-local` 不包含行、视图或 `formatReadBody` 逻辑;面向模型的 schema 保持逐字节不变。测试固定了以下行为:窗口化读取授权对未变文件的后续编辑;基于过期读取的编辑在尝试字面匹配之前报告 `FS_STALE_VERSION`;版本 CAS 行为得以保留;观测契约成立(`read` 工具的读取记录观测状态;直接 `ctx.fs` 读取不记录);`dsh-fs-policy` 具有 HMR(热模块替换)/dispose(资源释放)覆盖率。 ## 后续扩展 -该 seam 后来由 [Add direct directory listing to the filesystem seam](../architecture/2026-07-03-filesystem-directory-listing-seam.md) 扩展了直接目录列表功能。该后续工作单独跟踪,以使本 RFC 的验收标准继续描述最初交付的 fsspec 风格改造。 +该 seam 后来由 [Add direct directory listing to the filesystem seam](../architecture/2026-07-03-filesystem-directory-listing-seam.md) 扩展了直接目录列表功能。该后续工作单独跟踪,以使本 RFC 的验收标准继续描述最初交付的 fsspec 风格重构。 ## 曾考虑的替代方案 -- **字节级 fsspec(`cat`/`open` 返回原始字节)**——否决:该 seam 刻意定位为文本存储,比字节级高半层,使 UTF-8 解码、二进制/NUL 拒绝和带守卫的文本变更在提供方内只实现一次,策略层从不接触原始字节,也不将陈旧检查与变更临界区分离。 -- **具体的 `ctx.fileContext` 方法服务**——本 RFC 最初的策略形态;由[事件门控 RFC](../architecture/2026-06-26-file-context-as-event-gate.md) 改造为门控插件,使工具从不与策略产生方法耦合。 -- **将 `readPage` 和 `full`/`partial` 视图授权保留在提供方上**——改造前的形态,即「取代」一节所逆转的内容:视图完整性不是编辑安全所需的信号,版本新鲜度才是;视图规则使超过读取上限的大文件无法编辑。 +- **字节级 fsspec(`cat`/`open` 返回原始字节)**:否决。该 seam 刻意定位为文本存储,比字节级高半个层次,这样 UTF-8 解码、二进制/NUL 拒绝和受保护的文本变更只在提供方实现一次,策略层从不接触原始字节,也不将过期检查与变更临界区分离。 +- **具体的 `ctx.fileContext` 方法服务**:本 RFC 最初的策略形态;被[事件门控 RFC](../architecture/2026-06-26-file-context-as-event-gate.md) 改造为门控插件,使工具从不与策略产生方法耦合。 +- **在提供方保留 `readPage` 和 `full`/`partial` 视图授权**:「取代」一节所逆转的重构前形态。视图完整性不是编辑安全所需的,版本新鲜度才是;而视图规则使超过读取上限的大文件无法编辑。 ## 后果 - 新增第四个 fs 包和一个新的插件层。这是有意为之:它是此前推迟的策略层,而非第二个抽象后端 seam。 -- 直接使用 `ctx.fs` 会绕过策略:直接的 `ctx.fs.readText` 不发出 `fs/observed`,因此在默认策略下,后续的 `edit` 会以 `FS_NOT_OBSERVED` 拒绝,直到通过 `read` 工具读取该文件。该失败是显式且有文档记录的。 -- 大文件行窗口化从后端移至 `dsh-tool-fs` 中的 `read` 工具;文本解码和二进制拒绝留在 `ctx.fs.streamText` 中,因此这只是窗口化逻辑的迁移,不是第二套文本 IO 实现。 -- 将 `editText` 保留在提供方 seam 上意味着每个后端都必须实现字面替换契约。这是有意为之:该操作不是纯存储,但陈旧守卫 + 字面匹配 + 原子重写是必须保持一体的单元,以确保正确的错误归因和并发行为。该契约应保持窄且仅限文本,以便未来后端可以原生实现或通过全文件重写实现。 -- 新鲜度允许在窗口化读取后执行全文件 `write`。这比旧的视图检查更弱,但避免了大文件无法编辑的问题;提示词引导仍然不鼓励盲目的全文件替换。 +- 直接使用 `ctx.fs` 会绕过策略:直接 `ctx.fs.readText` 不发出 `fs/observed`,因此在默认策略下,后续 `edit` 会以 `FS_NOT_OBSERVED` 拒绝,直到通过 `read` 工具读取该文件。这一失败是显式且有文档记录的。 +- 大文件行窗口化从后端移至 `dsh-tool-fs` 中的 `read` 工具;文本解码和二进制拒绝留在 `ctx.fs.streamText` 中,因此这只是窗口化逻辑的迁移,而非第二套文本 IO 实现。 +- 将 `editText` 保留在提供方 seam 上意味着每个后端都必须实现字面替换契约。这是有意为之:该操作不是纯存储,但过期守卫 + 字面匹配 + 原子重写是必须保持在一起的单元,以确保正确的错误归因和并发行为。该契约应保持窄且仅限文本,以便未来后端可以原生实现或通过全文件重写实现。 +- 新鲜度允许在窗口化读取后进行全文件 `write`。这比旧的视图检查更弱,但避免了大文件无法编辑的问题;提示词引导仍然不鼓励盲目的全文件替换。 diff --git a/docs/rfc/implemented/simplification/2026-07-02-remove-stream-chunk-mirror.i18n.yaml b/docs/rfc/implemented/simplification/2026-07-02-remove-stream-chunk-mirror.i18n.yaml index ca87af6f9a..a9b8b58f67 100644 --- a/docs/rfc/implemented/simplification/2026-07-02-remove-stream-chunk-mirror.i18n.yaml +++ b/docs/rfc/implemented/simplification/2026-07-02-remove-stream-chunk-mirror.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-02-remove-stream-chunk-mirror.md: 1ec633b09c8e53ae7145a49061aced191a9aa765 -2026-07-02-remove-stream-chunk-mirror.zh.md: 83674658d24621b12a866262bb58dde166bedf2d +2026-07-02-remove-stream-chunk-mirror.zh.md: 7cf8a5d056c1bb4193263c58a8e4258173fe8b4e diff --git a/docs/rfc/implemented/simplification/2026-07-02-remove-stream-chunk-mirror.zh.md b/docs/rfc/implemented/simplification/2026-07-02-remove-stream-chunk-mirror.zh.md index 83674658d2..7cf8a5d056 100644 --- a/docs/rfc/implemented/simplification/2026-07-02-remove-stream-chunk-mirror.zh.md +++ b/docs/rfc/implemented/simplification/2026-07-02-remove-stream-chunk-mirror.zh.md @@ -1,12 +1,12 @@ # RFC:停止将 token 流镜像为 agent 事件 -Status: implemented - [English](2026-07-02-remove-stream-chunk-mirror.md) | 中文 +Status: implemented + ## 问题 -agent loop(智能体循环)将模型的每个 token 增量同时记录为持久的 `assistant/chunk` 会话事件,并发射一个携带相同数据的并行实时 `agent/stream-chunk` Cordis 事件。在 `packages/core/agent-loop/src/loop.ts` 中,两者仅相隔一行: +agent loop(智能体循环)将模型的每个 token delta 同时记录为持久的 `assistant/chunk` 会话事件,并发射一个携带相同数据的并行实时 `agent/stream-chunk` Cordis 事件。在 `packages/core/agent-loop/src/loop.ts` 中,二者仅相隔一行: ```ts ignore-check const chunkEvent = session.append('assistant/chunk', { turn, step, chunk }) @@ -17,17 +17,17 @@ ctx.emit('agent/stream-chunk', agent, turn, step, chunk) // ← the mirror - 持久事件:`assistant/chunk: { turn, step, chunk }`。 - 实时发射:`agent/stream-chunk(agent, turn, step, chunk)`——相同的 `StreamChunk`,相同的 `turn`/`step`。 -实时发射相比会话事件唯一多出的东西是实时的 `Agent` 句柄,而唯一的消费方丢弃了它(其处理函数签名为 `(_agent, _turn, _step, chunk)`)。 +实时发射相比会话事件唯一多出的东西是实时的 `Agent` 句柄,而唯一的消费方直接丢弃了它(其处理函数签名为 `(_agent, _turn, _step, chunk)`)。 -这与[边界镜像移除](2026-06-20-remove-agent-boundary-mirror-events.md)为轮次/步骤边界消除的重复如出一辙:消费方对同一个持久事实有两个真源,每次修改都必须同时触及两处。那份 RFC 将分片流推迟处理(「`assistant/chunk` 的持久化仍然是承重的,因此分片流后续可以作为镜像来评估,但那是一个独立的决策」),而非一并打包。本 RFC 就是那个独立的决策。 +这与[边界镜像移除](2026-06-20-remove-agent-boundary-mirror-events.md)为 turn/step 边界消除的重复如出一辙:消费方对同一个持久事实有两个真源,每次变更都要同时修改两处。那份 RFC 将 chunk 流推迟处理(「`assistant/chunk` 的持久化仍然是承重的,因此 chunk 流后续可以作为镜像来评估,但那是一个独立决策」),而非一并纳入。本 RFC 即是那个独立决策。 -推迟所依赖的前提已经尘埃落定:分片持久化是权威的,且将保留。停止持久化分片、仅保留瞬态实时流事件的提案已被[否决](../../rejected/simplification/2026-06-20-assembled-assistant-messages-only.md)——高保真回放、部分失败的流以及快照回放都依赖持久化的 `assistant/chunk` 流。因此 `session/event` 上的 `assistant/chunk` 是持久的、承重的 token 流,而 `agent/stream-chunk` 是它的纯冗余镜像。 +推迟所依赖的前提已经明确:chunk 持久化是权威的,且将保留。停止持久化 chunk、仅保留瞬态实时流事件的提案已被[否决](../../rejected/simplification/2026-06-20-assembled-assistant-messages-only.md)——高保真回放、部分失败的流以及快照回放都依赖持久化的 `assistant/chunk` 序列。因此 `session/event` 上的 `assistant/chunk` 是持久的、承重的 token 流,而 `agent/stream-chunk` 是它的纯冗余镜像。 ## 决策 -从 agent 事件分类体系中移除 `agent/stream-chunk`。token 流通过 `session/event` 以 `assistant/chunk` 的形式读取——持久化和回放已经使用的正是同一条流。`session/event` 是唯一的实时 transcript(文本记录)流(assistant 分片、轮次/步骤边界、工具活动、todo)。 +从 agent 事件分类体系中移除 `agent/stream-chunk`。token 流通过 `session/event` 以 `assistant/chunk` 的形式读取——持久化与回放已经使用的正是同一个序列。`session/event` 是唯一的实时 transcript(文本记录)流(assistant chunk、turn/step 边界、工具活动、todo)。 -**消费方。** 唯一重要的生产消费方——ACP 桥接层(`dsh-acp`,真正面向编辑器的流式输出接口)——已经从 `session/event` 渲染 `assistant/chunk`,从未使用 `agent/stream-chunk`,因此不受影响。stdio UI(`dsh-ui-stdio`,一个一次性的测试 REPL)是唯一的实时消费方;它在边界迁移时已经有了 `session/event` 监听器,因此其分片渲染被折叠进该监听器的 `assistant/chunk` 分支。合并为一个监听器还消除了一个潜在隐患:`inReasoning` dim-SGR 标志此前在两个独立的监听器(`agent/stream-chunk` 和 `session/event`)之间共享,分片与边界在该标志上竞争时没有确定的顺序;单一监听器按追加顺序处理,使交错变为确定性的。 +**消费方。** 唯一重要的生产消费方——ACP 桥接(`dsh-acp`,面向编辑器的真实流式输出接口)——已经从 `session/event` 渲染 `assistant/chunk`,从未使用 `agent/stream-chunk`,因此不受影响。stdio UI(`dsh-ui-stdio`,一个一次性的测试 REPL)是唯一的实时消费方;它在边界迁移时已经有了 `session/event` 监听器,因此其 chunk 渲染被折叠进该监听器作为 `assistant/chunk` 分支。合并为一个监听器还消除了一个潜在隐患:`inReasoning` dim-SGR 标志此前在两个独立监听器(`agent/stream-chunk` 和 `session/event`)之间共享,chunk 与边界在该标志上竞争时没有确定的顺序;单一监听器按追加顺序处理,使交错变为确定性的。 ## 范围 @@ -35,13 +35,13 @@ ctx.emit('agent/stream-chunk', agent, turn, step, chunk) // ← the mirror 未触及: - `assistant/chunk`(持久会话事件)——权威的 token 流,原样保留。本 RFC 移除的是实时镜像,而非持久化(持久化移除提案已被单独否决,见上文)。 -- `agent/steering`——本决策未触及(它是控制信号,不是 token 流)。其持久孪生事件是 `steering/message`,镜像发射由其自己的后续 RFC 移除:[移除 `agent/steering` 镜像发射](2026-07-04-remove-agent-steering-mirror.md)。 +- `agent/steering`——本决策未触及(它是控制信号,不是 token 流)。其持久孪生事件是 `steering/message`,镜像发射由其自身的后续 RFC 移除:[移除 `agent/steering` 镜像发射](2026-07-04-remove-agent-steering-mirror.md)。 - `agent/status`、`agent/error`、`agent/created`/`agent/disposed`、`agent/queued`、`agent/session-start`——生命周期/控制事件,不是 transcript 数据,也没有持久副本。 ## 曾考虑的替代方案 -**移除持久化、仅保留瞬态实时流**——反向裁剪,已被[单独否决](../../rejected/simplification/2026-06-20-assembled-assistant-messages-only.md):高保真回放、部分失败的流以及快照回放都依赖持久化的 `assistant/chunk` 流。这一点既已确定,实时发射就是配对中冗余的那一半。 +**移除持久化、仅保留瞬态实时流**——反向裁剪,已被[单独否决](../../rejected/simplification/2026-06-20-assembled-assistant-messages-only.md):高保真回放、部分失败的流以及快照回放都依赖持久化的 `assistant/chunk` 序列。在此前提确定后,实时发射才是配对中冗余的那一半。 ## 后果 -插件不再能从以 `Agent` 为首参的事件观察 token 增量。它应订阅 `session/event` 并过滤 `assistant/chunk`(如需 `Agent` 句柄,可从 `agent/created`/`agent/disposed` 构建的 session-id→agent 映射中恢复,与边界消费方的做法完全一致)。没有任何生产消费方在分片时需要实时的 `Agent`;这与边界镜像移除所做的权衡完全相同,是可接受的。 +插件不再能通过以 `Agent` 为首参的事件观察 token delta。它需要订阅 `session/event` 并过滤 `assistant/chunk`(如需 `Agent` 句柄,可通过 `agent/created`/`agent/disposed` 构建的 session-id→agent 映射恢复,与边界消费方已有的做法完全一致)。没有任何生产消费方在 chunk 时需要实时的 `Agent`;这与边界镜像移除所做的权衡相同,是可接受的。 diff --git a/docs/rfc/implemented/simplification/2026-07-04-drop-image-content-block.i18n.yaml b/docs/rfc/implemented/simplification/2026-07-04-drop-image-content-block.i18n.yaml index 776f41a858..9b195da18b 100644 --- a/docs/rfc/implemented/simplification/2026-07-04-drop-image-content-block.i18n.yaml +++ b/docs/rfc/implemented/simplification/2026-07-04-drop-image-content-block.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-04-drop-image-content-block.md: 8222880b61c225c39a1e132353c5f343fb90cb4b -2026-07-04-drop-image-content-block.zh.md: d1379d69a0c5056cdfcc744182cd9b6c52f222d5 +2026-07-04-drop-image-content-block.zh.md: cb9372e50863193cd579c0bf8de991810db95206 diff --git a/docs/rfc/implemented/simplification/2026-07-04-drop-image-content-block.zh.md b/docs/rfc/implemented/simplification/2026-07-04-drop-image-content-block.zh.md index d1379d69a0..cb9372e508 100644 --- a/docs/rfc/implemented/simplification/2026-07-04-drop-image-content-block.zh.md +++ b/docs/rfc/implemented/simplification/2026-07-04-drop-image-content-block.zh.md @@ -1,29 +1,29 @@ # RFC:移除 `image` 内容块,直到有路径能真正处理它 -Status: implemented - [English](2026-07-04-drop-image-content-block.md) | 中文 +Status: implemented + ## 问题 -`ImageBlock`(`packages/llm/llm/src/types.ts`)没有任何生产环境的生产者,而每条路径上的每个消费方都将其**丢弃**:DeepSeek 适配器的序列化器跳过 image 块(这是文档中注明的 MVP 限制);pi-ai 转换器因无法表示而跳过;ACP 编解码器既不声明 image prompt 能力、也不向外转发 image 块,并且对入站的 image prompt 内容直接**拒绝**;压缩(compaction)估算器对其收取一个固定 token 常量并渲染为 `[image]`。此时构造的 `ImageBlock` 会在协议格式(wire format)上静默消失——词汇表声明了一种没有任何路径兑现的能力,这正是 `AGENTS.md` 防御性模式所警告的静默数据丢失形态。唯一的构造点是用于固定 skip/drop/estimate 分支的测试。 +`ImageBlock`(`packages/llm/llm/src/types.ts`)没有任何生产环境的生产者,而每条路径上的每个消费方都将其**丢弃**:deepseek 适配器的序列化器跳过 image 块(这是文档中注明的 MVP 限制);pi-ai 转换器因无法表示而跳过;ACP 编解码器既不宣告 image prompt 能力、也不向外转发 image 块,并且会拒绝入站的 image prompt 内容;压缩(compaction)估算器对其收取一个固定 token 常量并渲染为 `[image]`。此时构造的 `ImageBlock` 会在协议格式(wire format)上静默消失——词汇宣告了一种没有任何路径兑现的能力,这正是 AGENTS.md 防御性模式所警告的静默数据丢失形态。唯一的构造调用出现在测试中,用于覆盖 skip/drop/estimate 分支。 ## 决策 -移除 `ImageBlock`、其 map 条目,以及适配器、ACP 渲染和压缩中的 image 专用分支。在同一个变更中更新所属词汇文档与生成的引用。未知的扩展块仍然覆盖 default 分支,ACP 继续独立于 harness 词汇拒绝入站 image prompt 内容。 +移除 `ImageBlock`、其 map 条目,以及适配器、ACP 渲染和压缩中的 image 专用分支。在同一个变更中更新所属的词汇文档与生成的引用。未知扩展块仍然覆盖默认分支,ACP 继续独立于 harness 词汇拒绝入站的 image prompt 内容。 ## 曾考虑的替代方案 ### 为什么不保留? -当适配器、ACP 与压缩全部支持 image 时,`ContentBlockMap` 可以重新引入它。保留一个唯一实现是拒绝的核心类型,等于向外声明一个不可用的接口;移除则让生产者在编译期立即失败。 +当适配器、ACP 和压缩全部支持 image 时,`ContentBlockMap` 可以重新引入。保留一个唯一实现就是拒绝的核心类型,等于宣告一个不可用的对外服务接口;移除后,生产者会立即得到编译期错误。 -记录在案的回退方案(假设评审决定保留该槽位):保留 `ImageBlock`,但将每处静默跳过替换为显式拒绝,并在词汇文档中记录该策略——静默丢弃是唯一没有辩护者的状态。评审最终决定移除;此回退方案作为文档化的替代方案保留,以备该槽位在完整功能之前回归。 +评审中记录的回退方案(假如评审决定保留该槽位):保留 `ImageBlock`,但将所有静默跳过替换为显式拒绝,并在词汇文档中记录该策略——静默丢弃是唯一没有辩护者的状态。评审最终决定移除;此回退方案作为文档化的替代方案保留,以备该槽位在完整功能就绪之前回归。 ## 验证 -RFC 记录之外没有任何地方构造 harness `ImageBlock`。ACP 独立的入站 image 拒绝仍有测试覆盖,而适配器、编解码器与压缩的 default 分支则通过插件定义的块类型覆盖。 +RFC 记录之外没有任何地方构造 harness 的 `ImageBlock`。ACP 独立的入站 image 拒绝仍有测试覆盖,适配器、编解码器和压缩的默认分支则通过插件定义的块类型来覆盖。 ## 后果 -日后重新添加核心词汇类型会同时涉及多个包——但这种协调变更正是真正的多模态功能所需的形态(适配器映射、ACP 能力声明、压缩定价),而当前并没有什么需要保留的实现。 +日后重新添加核心词汇类型需要同时改动多个包(package)——但这种协调变更本就是真正的多模态功能所需的形态(适配器映射、ACP 能力宣告、压缩定价),而当前并不存在需要保留的实现。 diff --git a/docs/rfc/implemented/simplification/2026-07-04-drop-inert-request-knobs.i18n.yaml b/docs/rfc/implemented/simplification/2026-07-04-drop-inert-request-knobs.i18n.yaml index 028162c527..3dabffc3a4 100644 --- a/docs/rfc/implemented/simplification/2026-07-04-drop-inert-request-knobs.i18n.yaml +++ b/docs/rfc/implemented/simplification/2026-07-04-drop-inert-request-knobs.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-04-drop-inert-request-knobs.md: d5484aa5ec64f89ce705ddd0a434c2c4dfbad460 -2026-07-04-drop-inert-request-knobs.zh.md: 877b073c4693f0c87b5f25003a1d043743b658fa +2026-07-04-drop-inert-request-knobs.zh.md: 9bd13cd024bc0e2ba7795a190d77063c3457fe98 diff --git a/docs/rfc/implemented/simplification/2026-07-04-drop-inert-request-knobs.zh.md b/docs/rfc/implemented/simplification/2026-07-04-drop-inert-request-knobs.zh.md index 877b073c46..9bd13cd024 100644 --- a/docs/rfc/implemented/simplification/2026-07-04-drop-inert-request-knobs.zh.md +++ b/docs/rfc/implemented/simplification/2026-07-04-drop-inert-request-knobs.zh.md @@ -1,4 +1,4 @@ -# RFC:移除 `GenerateOptions.prefill` 与 `ToolSchema.strict`——无可用端到端路径的请求旋钮 +# RFC:移除 `GenerateOptions.prefill` 与 `ToolSchema.strict`——无端到端可用路径的请求旋钮 [English](2026-07-04-drop-inert-request-knobs.md) | 中文 @@ -6,30 +6,30 @@ Status: implemented ## 问题 -两个请求契约旋钮贯穿了整条请求流水线,但都无法产生任何效果: +两个请求契约旋钮贯穿了整条请求流水线,却都无法产生任何效果: -- **`prefill`**(`packages/llm/llm/src/types.ts`)没有生产环境的赋值方:agent loop(智能体循环)组装的只有 `model`/`system`/`tools`/`messages` 加 `sessionId`/`signal`,上下文压缩(context compaction)后端只追加 `maxTokens`;而且**两个**适配器都拒绝它:`packages/llm/llm-deepseek/src/serialize.ts` 和 `packages/llm/llm-pi-ai/src/adapter.ts` 各自在 `prefill` 非 undefined 时抛出 `LlmError('UNSUPPORTED')`。该字段全部可观测行为就是两个 throw,各由一个适配器测试固定。DeepSeek 的 chat-prefix completion 是一个 Beta 功能,使用的 base URL 两个适配器都未指向。 -- **`strict`**(`ToolSchema`,同一文件)贯穿了 `DefineToolOptions`/`defineTool`(`packages/core/tools/src/schema.ts`)、注册表的 `schemas()` 白名单(`packages/core/tools/src/index.ts`)、deepseek 协议格式(wire format)映射(`packages/llm/llm-deepseek/src/serialize.ts`,其 wire-type 注释记录了 strict 模式需要适配器未使用的 `/beta` base URL)、`packages/llm/llm-pi-ai/src/adapter.ts` 中的逐工具 payload 修补,以及 tool-catalog 渲染器(`scripts/gen-tool-catalog.ts`)中的条件 `Strict:` 行。没有任何已发布的工具设置过它:在所有 `tool-*` 包 src 和 `examples/` 中 `rg` 搜索,`strict:` 的生产方为零;唯一的赋值方是 dsh-tools 单元测试。 +- **`prefill`**(`packages/llm/llm/src/types.ts`)没有生产级的 setter:agent loop(智能体循环)组装的是 `model`/`system`/`tools`/`messages` 加 `sessionId`/`signal`,上下文压缩(context compaction)后端只追加 `maxTokens`;而且**两个**适配器都拒绝它:`packages/llm/llm-deepseek/src/serialize.ts` 和 `packages/llm/llm-pi-ai/src/adapter.ts` 各自在 `prefill` 非 undefined 时抛出 `LlmError('UNSUPPORTED')`。该字段的全部可观测行为就是两个 throw,各由一条适配器测试固定。DeepSeek 的 chat-prefix completion 是一个 Beta 功能,运行在两个适配器都未指向的 base URL 上。 +- **`strict`**(`ToolSchema`,同一文件)穿过了 `DefineToolOptions`/`defineTool`(`packages/core/tools/src/schema.ts`)、注册表的 `schemas()` 允许列表(`packages/core/tools/src/index.ts`)、deepseek 协议格式(wire format)映射(`packages/llm/llm-deepseek/src/serialize.ts`,其 wire-type 注释记录了 strict 模式需要适配器未使用的 `/beta` base URL)、`packages/llm/llm-pi-ai/src/adapter.ts` 中的逐工具 payload 修补逻辑,以及 tool-catalog 渲染器(`scripts/gen-tool-catalog.ts`)中的条件 `Strict:` 行。没有任何已发布的工具设置过它——在所有 `tool-*` 包的 src 和 `examples/` 中执行 `rg` 搜索,`strict:` 的生产者为零;唯一的 setter 出现在 dsh-tools 单元测试中。 -两个旋钮在适配器间是对称的,因此移除时两个孪生适配器一并清理——[孪生适配器设计](../architecture/2026-06-13-twin-llm-adapters.md)不受影响。 +两个旋钮在适配器间是对称的,因此移除操作将它们从两个孪生适配器中一并剥离——[孪生适配器设计](../architecture/2026-06-13-twin-llm-adapters.md)不受影响。 ## 决策 -- 从 `GenerateOptions` 中移除 `prefill`,同时移除两个适配器的 UNSUPPORTED 守卫、固定这些 throw 的测试、[core.md](../../../core-data-structures/core.md) 中的粘贴行,以及适配器 README 中记录拒绝行为的行。实操手册(Cookbook)中的 UNSUPPORTED 指导([adding-an-llm-adapter.md](../../../cookbook/adding-an-llm-adapter.md))改为泛化表述——你的提供方无法兑现的 `GenerateOptions` 字段应抛出 `LlmError(..., 'UNSUPPORTED')`——而不再以 prefill 为例。[内容块词汇 RFC](../architecture/2026-06-11-content-block-vocabulary.md) 的后果部分将 prefill 记录为「受生产方门控」而非「已有归属」,遵照 [implemented/AGENTS.md](../AGENTS.md)。 -- 从 `ToolSchema`、`DefineToolOptions`、`defineTool`、`schemas()` 白名单、deepseek 序列化器分支及其 wire-type 字段、以及 tool-catalog 渲染器的 `Strict:` 行中移除 `strict`。pi-ai 的 payload 修补简化为无条件擦除 pi-ai 自身的逐工具 strict 默认值(pi-ai 在每个序列化工具上打 `strict: false`;手写的孪生适配器不发送此字段,因此擦除逻辑为保持协议格式对等而保留,由其序列化器测试固定)。赋值测试和 core.md 粘贴行已移除;`GenerateOptions` 与 `ToolSchema` 在 `scripts/type-equiv.manifest.json` 中保留各自的行,因为两个类型本身仍然存在,只是少了一个字段。 +- 从 `GenerateOptions` 中移除 `prefill`,同时移除两个适配器的 UNSUPPORTED 守卫、固定这些 throw 的测试、[core.md](../../../core-data-structures/core.md) 中的粘贴行,以及适配器 README 中记录拒绝行为的行。实操手册(cookbook)中的 UNSUPPORTED 指引([adding-an-llm-adapter.md](../../../cookbook/adding-an-llm-adapter.md))改为泛化表述——你的 provider 无法兑现的 `GenerateOptions` 字段应抛出 `LlmError(..., 'UNSUPPORTED')`——而不再以 prefill 为例。[内容块词汇 RFC](../architecture/2026-06-11-content-block-vocabulary.md) 的后果部分将 prefill 记录为「受 producer 门控」而非「已有归属」,依据 [implemented/AGENTS.md](../AGENTS.md)。 +- 从 `ToolSchema`、`DefineToolOptions`、`defineTool`、`schemas()` 允许列表、deepseek 序列化分支及其 wire-type 字段,以及 tool-catalog 渲染器的 `Strict:` 行中移除 `strict`。pi-ai 的 payload 修补逻辑简化为对 pi-ai 自身逐工具 strict 默认值的无条件清除(pi-ai 在每个序列化的工具上打 `strict: false`;手写的孪生适配器不发送此字段,因此清除逻辑为保持协议格式对等而保留,由其序列化器测试固定)。setter 测试和 core.md 粘贴行已移除;`GenerateOptions` 与 `ToolSchema` 在 `scripts/type-equiv.manifest.json` 中保留各自的行,因为两个类型只是少了一个字段,本身仍然存在。 -本 RFC 有意**不**触及 `temperature`、`stop` 或 `maxTokens`:这些字段被两个适配器端到端地兑现,是 `agent/request` 上请求变更钩子插件的自然首选目标。 +本 RFC 有意**不**触碰 `temperature`、`stop` 或 `maxTokens`:它们在两个适配器中都被端到端地兑现,是 `agent/request` 上请求变更钩子插件的自然首选目标。 ## 曾考虑的替代方案 ### 为什么不保留? -「显式的 UNSUPPORTED throw 是诚实的契约行为」——但一个旋钮在两个孪生适配器中的唯一实现都是拒绝,它什么也不承诺;删除它反而升级了失败模式:意外的赋值从运行时 throw 变为编译错误。「strict schema 遵循是官方文档记录的提供方功能,且管道完整」——但一个旋钮在有已发布工具设置它**且**有端点兑现它之前,都不是产品表面;今天两者都不成立。二者各自随其第一个真实生产方回归:`prefill` 随实现了 chat-prefix completion 的适配器(以及对不支持它的适配器的明确策略)一起回来,`strict` 随需要它的工具和 beta 端点方案一起回来。 +「显式的 UNSUPPORTED throw 是诚实的契约行为」——但一个在两个孪生适配器中唯一的实现就是拒绝的旋钮,什么也没承诺;删除它反而升级了失败模式:意外的 setter 变成编译错误而非运行时 throw。「Strict schema 遵循是官方文档记载的 provider 功能,且管道完整」——但一个旋钮在有已发布的工具设置它**并且**有端点兑现它之前,不构成产品表面;今天两者都不成立。它们各自随首个真实 producer 回归:`prefill` 随实现了 chat-prefix completion 的适配器(以及对不支持该功能的适配器的明确策略)一起回来;`strict` 随需要它的工具和 beta 端点方案一起回来。 ## 验证 -`rg prefill` 仅返回 RFC 记录(本文与[内容块词汇 RFC](../architecture/2026-06-11-content-block-vocabulary.md) 的 producer-gated 后果);在 tool-schema 范围内 `rg strict` 仅返回本 RFC、保留的 pi-ai 擦除逻辑,以及无关行文(如 `strictEqual`)。两个适配器的契约测试在移除守卫后通过,pi-ai 修补仍然擦除库的 strict 默认值——协议格式对等由其序列化器测试固定。 +`rg prefill` 仅返回 RFC 记录(本 RFC 与[内容块词汇 RFC](../architecture/2026-06-11-content-block-vocabulary.md) 中 producer-gated 的后果);在 tool-schema 范围内执行 `rg strict` 仅返回本 RFC、保留的 pi-ai 清除逻辑,以及 `strictEqual` 等无关文本。两个适配器的契约测试在移除守卫后通过,pi-ai 修补逻辑仍然清除库的 strict 默认值——协议格式对等由其序列化器测试固定。 ## 后果 -已发布的钩子桥接不设置任何请求字段,而请求变更插件(`agent/request` waterfall(瀑布式事件)监听器)使用的是 `temperature`/`stop`(保留且可用),而非适配器拒绝的字段。如果 chat-prefix completion 或 strict 模式成为产品功能,重新添加将随适配器/端点工作一起落地,届时契约能说明实际发生了什么,而非「所有人都 throw」。 +已发布的钩子桥接不设置任何请求字段,而请求变更插件(`agent/request` waterfall(瀑布式事件)监听器)使用的是 `temperature`/`stop`(保留且可用),而非适配器拒绝的字段。如果 chat-prefix completion 或 strict 模式成为产品功能,重新添加将随适配器/端点工作一起落地,届时契约能说明实际发生了什么,而不是「所有人都 throw」。 diff --git a/docs/rfc/implemented/simplification/2026-07-04-drop-unconsumed-web-observation-surface.i18n.yaml b/docs/rfc/implemented/simplification/2026-07-04-drop-unconsumed-web-observation-surface.i18n.yaml index 429f89095a..fb1161ae18 100644 --- a/docs/rfc/implemented/simplification/2026-07-04-drop-unconsumed-web-observation-surface.i18n.yaml +++ b/docs/rfc/implemented/simplification/2026-07-04-drop-unconsumed-web-observation-surface.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-04-drop-unconsumed-web-observation-surface.md: c48ff5b80c916cd6cc04d6a8339a8555d05b0d40 -2026-07-04-drop-unconsumed-web-observation-surface.zh.md: ce8a108450ed9e9308066ab45b8f000dc20fde39 +2026-07-04-drop-unconsumed-web-observation-surface.zh.md: 4988a5aa77604f528cc23409a3c2290c890a7f5a diff --git a/docs/rfc/implemented/simplification/2026-07-04-drop-unconsumed-web-observation-surface.zh.md b/docs/rfc/implemented/simplification/2026-07-04-drop-unconsumed-web-observation-surface.zh.md index ce8a108450..4988a5aa77 100644 --- a/docs/rfc/implemented/simplification/2026-07-04-drop-unconsumed-web-observation-surface.zh.md +++ b/docs/rfc/implemented/simplification/2026-07-04-drop-unconsumed-web-observation-surface.zh.md @@ -1,4 +1,4 @@ -# RFC:移除未被消费的 web 观测面——`providers-change` 事件与 status 方法 +# RFC:移除未被消费的 web 观测接口——`providers-change` 事件与 status 方法 Status: implemented @@ -6,29 +6,29 @@ Status: implemented ## 问题 -`WebService` 暴露了一组没有任何生产代码观测的观测面: +`WebService` 暴露了一组没有任何生产代码观测的观测接口: -- **`web/providers-change`**(`packages/web/web/src/index.ts`)在每次 provider 注册和 dispose(资源释放)时声明并发射。每个注册 effect 的回滚 yield 被刻意排在 emit 之前,唯一目的是让一个抛异常的 change listener 能回退注册。该事件在包自身的两个单元测试之外没有任何 listener(其中一个测试的存在就是为了固定那个回滚顺序)。 -- **`searchStatus()` / `fetchStatus()` 与 `WebCapabilityStatus` 联合类型**(同一个包)没有任何生产调用方:`dsh-tool-web` 直接通过 `ctx.web.search()`/`fetch()` 执行,并将不可用状态以 seam 在执行时抛出的结构化 `WebError` 错误码呈现(`packages/web/tool-web/src/search.ts`、`packages/web/tool-web/src/fetch.ts`);唯一的 status 调用方是 web 包自身的测试。`packages/web/tool-web/README.md` 与 [architecture.md](../../../architecture.md) 中的行文声称工具「只读取聚合的 `searchStatus()`/`fetchStatus()`」——这种漂移之所以存活,仅仅因为没有什么机制会拿行文与调用点做比对。 +- **`web/providers-change`**(`packages/web/web/src/index.ts`)在每次 provider 注册和 dispose(资源释放)时声明并发出,且每个注册 effect 的回滚 yield 被刻意排在 emit 之前,唯一目的是让抛出异常的 change listener 能回退注册。在该包自身的两个单元测试之外没有任何 listener(其中一个测试的存在仅仅是为了固定那个回滚顺序)。 +- **`searchStatus()` / `fetchStatus()` 与 `WebCapabilityStatus` 联合类型**(同一个包)没有任何生产调用方:`dsh-tool-web` 通过 `ctx.web.search()`/`fetch()` 直接执行,并将不可用性表现为 seam 在执行时抛出的结构化 `WebError` 错误码(`packages/web/tool-web/src/search.ts`、`packages/web/tool-web/src/fetch.ts`);唯一的 status 调用方是 web 包自身的测试。`packages/web/tool-web/README.md` 和 [architecture.md](../../../architecture.md) 中的行文声称该工具「只读取聚合的 `searchStatus()`/`fetchStatus()`」——这是一处漂移,仅因没有机制检查行文与调用点的一致性而幸存。 -seam 自身的设计使两个观测面都失去了消费方:工具注册跟随产品 ENABLEMENT 而非 provider 可用性(`packages/web/tool-web/src/index.ts`),provider 选择在执行时解析、从不缓存——因此没有需要失效的缓存、没有需要重算的注册集合,也没有调用方需要一个独立于「执行并路由结构化错误」的可用性探针。HMR(热模块替换)清理由 effect disposer 自身承载。 +seam 自身的设计使这两个接口天然没有消费方:工具注册跟随产品 ENABLEMENT 而非 provider 可用性(`packages/web/tool-web/src/index.ts`),provider 选择在执行时解析且从不缓存——因此没有需要失效的缓存、没有需要重算的注册集合、也没有调用方需要一个有别于「执行并路由结构化错误」的可用性探测。HMR(热模块替换)清理由 effect disposer 自身承载。 -这与[移除未被消费的 `llm/adapter-change` 事件](../../implemented/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.md)如出一辙:那次从 `LlmService` 移除了相同的通知形态、相同的回滚先于 emit 机制,以及相同的 listener-throw 测试。该 RFC 的保留/裁剪判据——保留 `tools/change`(因为它有合理的面向用户的工具列表消费方),裁剪启动期后端注册表信号——把 web provider 注册表信号明确归入裁剪一侧;status 方法则是同一判断应用于拉取面而非推送面。 +这与 [移除未被消费的 `llm/adapter-change` 事件](../../implemented/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.md) 如出一辙:那个 RFC 从 `LlmService` 中移除了相同的通知形态、相同的 rollback-before-emit 机制和相同的 listener-throw 测试。该 RFC 的保留/裁剪判据——为 `tools/change` 保留其合理的面向用户的工具列表消费方,裁剪启动时的后端注册表信号——将 web provider 注册表明确归入裁剪一侧;status 方法是同一判断应用于 pull 接口而非 push 接口。 ## 决策 -移除注册表变更事件、聚合 status 方法与类型,以及它们的专属测试。provider 私有的 status 保留用于执行时选择。面向调用方的覆盖率现在断言成功执行或结构化的选择错误,web 相关文档描述该按需调用契约。 +移除注册表变更事件、聚合 status 方法与类型,以及它们的专属测试。provider 私有的 status 保留用于执行时选择。面向调用方的覆盖率现在断言成功执行或结构化的选择错误,web 文档描述该按需调用契约。 ## 曾考虑的替代方案 ### 为什么不保留? -web seam RFC 当初有意指定了两者——事件作为最小的 HMR 可见性信号,status 方法作为工具的聚合诊断——且未来的 provider 状态面板是可以想象的。但同一 RFC 的其他选择使它们失去了消费方:按需派生的选择与基于 enablement 的注册使得没有消费方**能**需要它们;已交付的工具展示了真实模式(执行并路由结构化错误);漂移的 README 语句表明承诺的消费方从未实现。按照 AGENTS.md 的原则「RFC 是提案,不是金科玉律」,这些正是该提案中被代码证明过度延伸的部分;未来的观测者重新引入它实际消费的最小信号或查询,由该消费方塑造其形态。 +web seam RFC 有意指定了两者——事件作为最小的 HMR 可见性信号,status 方法作为工具的聚合诊断——且未来的 provider 状态面板是可以想象的。但同一 RFC 的其他设计选择使它们失去了消费方:按需派生的选择与基于 enablement 的注册使得没有消费方**能**需要这两者;已交付的工具展示了真实模式(执行并路由结构化错误);漂移的 README 语句表明承诺的消费方从未实现。按 AGENTS.md「RFC 是提案,不是金科玉律」的原则,这些是该提案中代码已证明过度设计的部分;未来的观测者按其实际消费的需求重新引入最小的信号或查询,由该消费方塑造其形态。 ## 验证 -`providers-change`、`searchStatus`、`fetchStatus` 和 `WebCapabilityStatus` 在 RFC 历史之外不再有任何拼写残留;catalog 是最新的(`verify-cordis-catalog` 绿色);注册/释放的 HMR 安全测试通过执行行为证明清理正确;tool-web README 与架构段落描述了工具实际拥有的执行时错误路由契约。 +在 RFC 历史之外不再有 `providers-change`、`searchStatus`、`fetchStatus` 或 `WebCapabilityStatus` 的拼写残留;catalog 是最新的(`verify-cordis-catalog` 绿色);注册/释放的 HMR 安全测试通过执行行为证明清理正确;tool-web README 与 architecture 段落描述了工具实际拥有的执行时错误路由契约。 ## 后果 -未来如果有 provider 选择器 UI 或诊断面板需要变更通知或 status 查询,它会重新添加自己实际消费的最小观测面;相同的判断及其反转条件已记录在 LLM 先例中。 +未来若有 provider 选择器 UI 或诊断面板需要变更通知或 status 查询,它将重新添加自身所消费的最小接口;相同的判断及其反转条件已记录在 LLM(大语言模型)先例中。 diff --git a/docs/rfc/implemented/simplification/2026-07-04-fold-stdio-ui-helper.i18n.yaml b/docs/rfc/implemented/simplification/2026-07-04-fold-stdio-ui-helper.i18n.yaml index dfa01c3e89..4ca47feeee 100644 --- a/docs/rfc/implemented/simplification/2026-07-04-fold-stdio-ui-helper.i18n.yaml +++ b/docs/rfc/implemented/simplification/2026-07-04-fold-stdio-ui-helper.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-04-fold-stdio-ui-helper.md: 8d26af190a957b424960519f77cbe132291c74de -2026-07-04-fold-stdio-ui-helper.zh.md: 879cce0d40e396b56f1a961b5ff190bab4fd70dd +2026-07-04-fold-stdio-ui-helper.zh.md: 795edf082258a56d3c11afbbb8de8cfa0e74e74e diff --git a/docs/rfc/implemented/simplification/2026-07-04-fold-stdio-ui-helper.zh.md b/docs/rfc/implemented/simplification/2026-07-04-fold-stdio-ui-helper.zh.md index 879cce0d40..795edf0822 100644 --- a/docs/rfc/implemented/simplification/2026-07-04-fold-stdio-ui-helper.zh.md +++ b/docs/rfc/implemented/simplification/2026-07-04-fold-stdio-ui-helper.zh.md @@ -1,28 +1,28 @@ # RFC:将 stdio UI 辅助模块折入 stdio 应用 -Status: implemented - [English](2026-07-04-fold-stdio-ui-helper.md) | 中文 +Status: implemented + ## 问题 -readline UI 曾是一个完整的包(`@deepseek-ai/dsh-ui-stdio`,位于 `packages/support/`),其唯一的运行时导入方是应用包 `@deepseek-ai/dsh-stdio-demo`。示例通过加载该应用来使用 readline UI,从不自行组合这个辅助模块;仓库中所有其他引用都是因为包边界存在而存在的机械性或描述性表面:manifest 与 tsconfig 条目、生成的 module-graph 行、依赖图与 README 行,以及命名该包的文档注释。ui 分组 README 记录了 support 放置的理由("主要为示例和覆盖率门禁而存在——`ui/` 保留给作为产品交付的界面"),这留下了一个持续的张力:一个已交付的产品应用依赖一个被文档标注为非产品表面的 support 包。 +readline UI 曾是一个完整的包(`packages/support/` 下的 `@deepseek-ai/dsh-ui-stdio`),其唯一的运行时导入方是应用包 `@deepseek-ai/dsh-stdio-demo`。示例通过加载应用来使用 readline UI,从不自行组合该辅助模块;仓库中所有其他引用都是因为包边界存在而存在的机械性或描述性表面:manifest(元数据清单)与 tsconfig 条目、生成的 module-graph 行、依赖图与 README 行,以及命名该包的文档注释。ui 组 README 记录了 support 放置的理由("主要为示例和覆盖率门禁而存在,`ui/` 保留给作为产品交付的界面"),这留下了一个持续的张力:一个已交付的产品应用依赖一个被明确标注为非产品表面的 support 包。 -这条边界带来的是包元数据、workspace 与 tsconfig 引用、module-graph 行、README 条目,以及 publint 表面——服务于一个并不可独立替换的辅助模块:stdio 应用的前门集群总是包含 readline UI,且没有其他东西能有意义地消费它。 +这条边界换来的是:包元数据、workspace 与 tsconfig 引用、module-graph 行、README 条目,以及 publint 表面——服务于一个并不可独立替换的辅助模块:stdio 应用的前门集群始终包含 readline UI,且没有其他消费方能有意义地使用它。 ## 决策 -该辅助模块以终端通道插件的形式存在于 `@deepseek-ai/dsh-stdio` 中(`packages/ui/stdio/src/index.ts`):`createStdioChat`、其 `StdioRuntime` 测试 seam 及单元测试(`packages/ui/stdio/tests/stdio.spec.ts`、`readline.spec.ts`)一并迁入,因此 EOF 处理、渲染、dispose(资源释放)以及 piped-vs-TTY 行为在按文件覆盖率门禁下仍有单元测试覆盖,且无需劫持进程全局对象。该模块保持具名的 `name`/`inject`/`Config`/`apply` 导出形状——即应用通过 `ctx.plugin(uiStdio, …)` 挂载时消费的契约——而 `examples/echo-agent` 与 `examples/coding-agent` 中的 keyless Loader 路径冒烟测试继续证明组合树能通过真实 Loader 启动(stdio 包的插件形状单元测试套件固定了显式的 `unwrapExports` 断言,因为缺少 `inject` 的 bundle 会跳过一个意外的 default 导出而非崩溃)。 +该辅助模块作为终端通道插件存放在 `@deepseek-ai/dsh-stdio` 中(`packages/ui/stdio/src/index.ts`):`createStdioChat`、其 `StdioRuntime` 测试 seam 及单元测试(`packages/ui/stdio/tests/stdio.spec.ts`、`readline.spec.ts`)一并迁入,因此 EOF 处理、渲染、dispose(资源释放)以及管道/TTY 行为在按文件覆盖率门禁下仍有单元测试覆盖,且无需劫持进程全局对象。该模块保留具名的 `name`/`inject`/`Config`/`apply` 导出形状——即应用的 `ctx.plugin(uiStdio, …)` 挂载所消费的契约——而 `examples/echo-agent` 与 `examples/coding-agent` 中的 keyless Loader 路径冒烟测试继续证明组合树能通过真实 Loader 启动(stdio 包的插件形状单元测试套件固定了显式的 `unwrapExports` 断言,因为缺少 `inject` 的 bundle 会跳过一个意外的 default 导出而不是崩溃)。 -`packages/support/ui-stdio` 包已删除:manifest、tsconfig 引用、module-graph 行与 README 行均已清理;原先命名该包的文档注释(示例 e2e 模块文档、`packages/README.md`、support 与 todo README、[ui 分组 README](../../../../packages/ui/README.md))现在描述的是包内模块。 +`packages/support/ui-stdio` 包已移除:manifest、tsconfig 引用、module-graph 行与 README 行均已删除;曾命名该包的文档注释(示例 e2e 模块文档、`packages/README.md`、support 与 todo README、[ui 组 README](../../../../packages/ui/README.md))现在描述的是包内模块。 ## 曾考虑的替代方案 -### 为什么不将其提升到 `ui/`? +### 为什么不将其提升到 `ui/` 而是折入? -提升可以解决 support 与产品之间的错位,同时保留包边界——但只有在 readline UI 是一个可独立替换的集成或拥有第二个组合方时才是正确选择,而消费方普查表明两者都不成立。结构化的 ACP 桥接保持独立包,因为它是产品协议表面,拥有自己的契约和快照层级;readline 辅助模块只是一个应用前门的脚手架。在正式发布前重新拆出的成本很低:如果未来有第二个产品应用需要 readline UI,届时再拆出,由那个消费方来塑造包契约。 +提升可以解决 support 与 product 之间的错位,同时保留边界——只有在 readline UI 是一个可独立替换的集成或有第二个组合方时才是正确选择,而消费方普查表明两者皆非。结构化的 ACP 桥接保留为独立包,因为它是具有自身契约和快照层级的产品协议表面;readline 辅助模块只是一个应用前门的脚手架。在发布前重新拆分成本很低:如果将来有第二个产品应用需要 readline UI,届时再拆出来,由那个消费方来塑造包契约。 ## 后果 -- stdio 应用完整拥有自己的前门;一个叶子 `cordis.yml` 仍然只加载一个应用包,演示的形状没有变化。 +- stdio 应用完整拥有自己的前门;叶子 `cordis.yml` 仍然只加载一个应用包,演示的形态没有变化。 - 未来如果有独立的终端 UI 需要将该辅助模块作为包使用,届时由那个第二消费方驱动重新引入,而非仓库为假设性的复用保留一条边界。 diff --git a/docs/rfc/implemented/simplification/2026-07-04-prune-producerless-vocabulary-variants.i18n.yaml b/docs/rfc/implemented/simplification/2026-07-04-prune-producerless-vocabulary-variants.i18n.yaml index f710b62445..ef7e3c8092 100644 --- a/docs/rfc/implemented/simplification/2026-07-04-prune-producerless-vocabulary-variants.i18n.yaml +++ b/docs/rfc/implemented/simplification/2026-07-04-prune-producerless-vocabulary-variants.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-04-prune-producerless-vocabulary-variants.md: 271b97f6217a2694f36b7fe7339eab6176dba9e5 -2026-07-04-prune-producerless-vocabulary-variants.zh.md: 5d75c0327e2faf5e6e37f8db959509161beda702 +2026-07-04-prune-producerless-vocabulary-variants.zh.md: 2fe8d41a011c37919bd01022d5be6d309b865bf7 diff --git a/docs/rfc/implemented/simplification/2026-07-04-prune-producerless-vocabulary-variants.zh.md b/docs/rfc/implemented/simplification/2026-07-04-prune-producerless-vocabulary-variants.zh.md index 5d75c0327e..2fe8d41a01 100644 --- a/docs/rfc/implemented/simplification/2026-07-04-prune-producerless-vocabulary-variants.zh.md +++ b/docs/rfc/implemented/simplification/2026-07-04-prune-producerless-vocabulary-variants.zh.md @@ -1,33 +1,33 @@ -# RFC:清理无生产者的词汇变体(块缓存提示、`agent` 消息来源、`continuation` 轮次触发器) - -Status: implemented +# RFC:裁剪无生产者的词汇变体(块缓存提示、`agent` 消息来源、`continuation` 轮次触发器) [English](2026-07-04-prune-producerless-vocabulary-variants.md) | 中文 +Status: implemented + ## 问题 -合并可扩展的词汇映射表设计上通过声明合并来增长,代码库已在 `TurnEndReasonMap`(`packages/core/session/src/types.ts`)上声明了准入策略:像 `refusal` 这样的变体「在适配器或循环首次发出它之前,有意不加入」。三个已声明的词汇项违反了这一策略——每个都既无生产者也无消费方,其中两个甚至没有测试: +可合并扩展的词汇映射表设计上通过声明合并来增长,代码库已在 `TurnEndReasonMap`(`packages/core/session/src/types.ts`)上明确了准入策略:像 `refusal` 这样的变体「在适配器或循环首次发出它之前,有意不纳入」。三个已声明的词汇项违反了该策略——每个都既无生产者也无消费方,其中两个甚至没有测试: -- **`CacheHint` 及其 `cache?: CacheHint` 块字段**,位于 `TextBlock`/`ToolResultBlock`(`packages/llm/llm/src/types.ts`;image block 上还有第三个同类字段,随 image block 一起移除——见[移除 image 的 RFC](2026-07-04-drop-image-content-block.md))。没有任何地方构造过带 `cache:` 的块——src、测试和文档粘贴全部搜索为空——两个适配器也都不读 `.cache`:DeepSeek 的 prompt 缓存是自动的,适配器只从响应中映射出 `prompt_cache_hit_tokens`,从不向请求中发送提示。这是 Anthropic 风格的 `cache_control` 接口面,却没有任何提供方能兑现它。 -- **`MessageSourceMap.agent`**(`{ kind: 'agent'; agentId: string }`,同一文件)。零个构造点,测试中也没有。它预期的生产者在上线时并未使用它:subagent 后端将父级的 prompt 发送给子级时不带 `source`,因此日志中记录为 `{ kind: 'user' }`,通用信封渲染器在插值 `source.kind` 时也从不按它路由。 -- **`TurnTriggerMap.continuation`**(`packages/core/session/src/types.ts`)。agent loop(智能体循环)在结构上不可能发出它——续写发生在一个轮次*内部*作为后续步骤,从不作为新轮次——循环只构造 `message` 和 `injection` 触发器。唯一的写入者是一个手工构建的测试 fixture(测试前置数据)(`packages/support/llm-replay/tests/llm-replay.spec.ts`),它只需要一个任意的非 message 触发器,`injection` 触发器同样满足需求;唯一的生产环境触发器读取者 ACP 桥接层只过滤 `kind === 'message'`。 +- **`CacheHint` 及其 `cache?: CacheHint` 块字段**,位于 `TextBlock`/`ToolResultBlock`(`packages/llm/llm/src/types.ts`;image block 上还有第三个同类字段,已随 image block 一起移除——见[移除 image block 的 RFC](2026-07-04-drop-image-content-block.md))。没有任何地方构造过带 `cache:` 的块——src、测试和文档粘贴全部搜索为空——两个适配器也都不读 `.cache`:DeepSeek 的 prompt 缓存是自动的,适配器只从响应中映射出 `prompt_cache_hit_tokens`,从不向请求中发送提示。这是 Anthropic 风格的 `cache_control` 接口面,却没有能兑现它的提供方。 +- **`MessageSourceMap.agent`**(`{ kind: 'agent'; agentId: string }`,同一文件)。零个构造点,包括测试在内。它预期的生产者在实现时并未使用它:subagent 后端将父级的 prompt 发送给子级时不带 `source`,因此记录为 `{ kind: 'user' }`,通用信封渲染器在插值 `source.kind` 时也从未对其做路由。 +- **`TurnTriggerMap.continuation`**(`packages/core/session/src/types.ts`)。agent loop(智能体循环)在结构上不可能发出它——continuation 发生在一个轮次*内部*作为后续步骤,而非作为新轮次——循环只构造 `message` 和 `injection` 触发器。唯一的写入者是一个手工构建的测试 fixture(测试前置数据),它只需要一个任意的非 message 触发器(`packages/support/llm-replay/tests/llm-replay.spec.ts`),`injection` 触发器同样满足需求;唯一的生产环境触发器读取方 ACP 桥接层只过滤 `kind === 'message'`。 ## 决策 -删除 `CacheHint`、其 `cache?` 块字段、`agent` 消息来源变体和 `continuation` 轮次触发器变体:已发布的词汇不再包含它们。llm-replay fixture 改用 `injection` 触发器(任何非 `message` 触发器都能满足其用途)。[core.md](../../../core-data-structures/core.md) 和 [session.md](../../../core-data-structures/session.md) 中的 type-equiv 粘贴与裁剪后的映射表一致——两个符号保留在 `scripts/type-equiv.manifest.json` 中,因为每个映射表只是少了一个成员——[内容块词汇 RFC](../architecture/2026-06-11-content-block-vocabulary.md) 的「后果」部分将缓存提示记录为「受生产者门控」而非「已有归属」,遵照 [implemented/AGENTS.md](../AGENTS.md)。 +删除 `CacheHint`、其 `cache?` 块字段、`agent` 消息来源变体与 `continuation` 轮次触发器变体:发布的词汇表不再包含它们。llm-replay fixture 改用 `injection` 触发器(任何非 `message` 触发器均满足其用途)。[core.md](../../../core-data-structures/core.md) 和 [session.md](../../../core-data-structures/session.md) 中的 type-equiv 粘贴与裁剪后的映射表一致——两个符号保留在 `scripts/type-equiv.manifest.json` 中,因为每个映射表本身仍然存在,只是少了一个成员——[内容块词汇 RFC](../architecture/2026-06-11-content-block-vocabulary.md) 的后果部分将缓存提示记录为「受生产者门控」而非「已有归属」,依照 [implemented/AGENTS.md](../AGENTS.md)。 -每个变体在获得真正的生产者之日回归,这正是映射表设计上的增长方式:缓存功能连同传输它的适配器一起重新添加 `cache`;subagent 归属连同打标的后端和路由它的消费方一起重新添加 `agent`;真正启动新轮次的自动续写功能连同发出它的插件一起重新添加 `continuation`。 +每个变体在获得真正的生产者之日回归,这正是映射表设计的增长方式:缓存功能连同传输它的适配器一起重新添加 `cache`;subagent 归属连同打标的后端和路由它的消费方一起重新添加 `agent`;真正启动新轮次的自动续行功能连同发出它的插件一起重新添加 `continuation`。 ## 曾考虑的替代方案 ### 为什么不保留它们? -[内容块词汇 RFC](../architecture/2026-06-11-content-block-vocabulary.md) 将「缓存提示……已有归属」列为设计后果,预留的槽位确实能传达意图。但一个空槽位是契约面,每个实现和消费方都必须考虑它(我的适配器是否必须兑现 `cache`?我的渲染器是否必须路由 `agent` 来源?),而兄弟映射表自身的 JSDoc 已经拒绝了「无发出者的预留」——`refusal` 和 `max_turn_requests` 被标注为*当有东西首次发出它们时*再添加的变体,而非提前声明。对已声明但无生产者的变体执行同一标准,才能让词汇表有意义:如果它在映射表里,就一定有东西在生产它。 +[内容块词汇 RFC](../architecture/2026-06-11-content-block-vocabulary.md) 将「缓存提示……已有归属」列为设计后果,预留槽位确实能表达意图。但一个空槽位是每个实现和消费方都必须考虑的契约面(我的适配器需要兑现 `cache` 吗?我的渲染器需要路由 `agent` 来源吗?),而同族映射表自身的 JSDoc 已经拒绝了「无发出者的预留」——`refusal` 和 `max_turn_requests` 被明确标注为*当某物首次发出它们时*再添加的变体,而非提前声明。对已声明但无生命的变体施加同样的标准,使词汇表具有实际意义:如果它在映射表中,就一定有东西在生产它。 ## 验证 -`rg` 搜索 `CacheHint`、`agent` 消息来源的拼写和 `continuation` 触发器的拼写,只返回 RFC 记录(本文,以及[移除 image 的 RFC](2026-07-04-drop-image-content-block.md) 中关于 image block 自身 `cache` 字段的描述);llm-replay fixture 使用 `injection` 触发器断言相同的回放行为;核心数据结构粘贴与 type-equiv manifest(元数据清单)保持同步。 +对 `CacheHint`、`agent` 消息来源拼写和 `continuation` 触发器拼写执行 `rg` 搜索,结果仅返回 RFC 记录(本文,以及[移除 image block 的 RFC](2026-07-04-drop-image-content-block.md) 中关于 image block 自身 `cache` 字段的说明);llm-replay fixture 使用 `injection` 触发器断言了相同的回放行为;core-data-structures 粘贴与 type-equiv manifest 保持同步。 ## 后果 -没有运行时行为改变——本来就没有任何东西能构造这些值。镜像事件的移除([边界镜像 RFC](2026-06-20-remove-agent-boundary-mirror-events.md)、[流式分片镜像 RFC](2026-07-02-remove-stream-chunk-mirror.md))只涉及瞬态的 `agent/*` 事件,从不触及持久化词汇,因此不存在冲突。其他地方准入策略已经生效:`rejected`、`prompt/blocked` 和 `hook/invoked`/`hook/result` 各自都有活跃的生产者——本 RFC 将同一标准延伸到缺少生产者的三个变体。image block 自身的 `cache?` 字段属于[移除 image 的 RFC](2026-07-04-drop-image-content-block.md),随该块一起移除;本 RFC 覆盖的是保留下来的块类型上的两个字段。 +没有任何运行时行为改变——本来就没有东西能构造这些值。镜像事件的移除([boundary-mirror RFC](2026-06-20-remove-agent-boundary-mirror-events.md)、[stream-chunk RFC](2026-07-02-remove-stream-chunk-mirror.md))只涉及瞬态的 `agent/*` 事件,从不涉及持久词汇,因此不存在冲突。其他地方准入策略已经成立:`rejected`、`prompt/blocked` 和 `hook/invoked`/`hook/result` 各自都有活跃的生产者——本 RFC 将同一标准延伸到缺少生产者的三个变体。image block 自身的 `cache?` 字段属于[移除 image block 的 RFC](2026-07-04-drop-image-content-block.md),已随该块一起移除;本 RFC 覆盖的是留存块类型上的两个字段。 diff --git a/docs/rfc/implemented/simplification/2026-07-04-prune-write-only-fs-surface.i18n.yaml b/docs/rfc/implemented/simplification/2026-07-04-prune-write-only-fs-surface.i18n.yaml index 3a4db54e54..428dfb9379 100644 --- a/docs/rfc/implemented/simplification/2026-07-04-prune-write-only-fs-surface.i18n.yaml +++ b/docs/rfc/implemented/simplification/2026-07-04-prune-write-only-fs-surface.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-04-prune-write-only-fs-surface.md: ac2cbcc282b848b26a3d5d327e0ab54612b5ac91 -2026-07-04-prune-write-only-fs-surface.zh.md: 799847aa77599a292c1aa48e150aae99fa130ae3 +2026-07-04-prune-write-only-fs-surface.zh.md: e854df76cae74033404aa9cc1986fdd118f19b10 diff --git a/docs/rfc/implemented/simplification/2026-07-04-prune-write-only-fs-surface.zh.md b/docs/rfc/implemented/simplification/2026-07-04-prune-write-only-fs-surface.zh.md index 799847aa77..e854df76ca 100644 --- a/docs/rfc/implemented/simplification/2026-07-04-prune-write-only-fs-surface.zh.md +++ b/docs/rfc/implemented/simplification/2026-07-04-prune-write-only-fs-surface.zh.md @@ -1,32 +1,32 @@ -# RFC:从 fs seam 中移除只写字段与一个无效路由旋钮 - -Status: implemented +# RFC:从 fs seam 中移除只写字段与一个无效的路由旋钮 [English](2026-07-04-prune-write-only-fs-surface.md) | 中文 +Status: implemented + ## 问题 -[fs seam 拆分](2026-06-26-fsspec-style-fs-seam.md)将读取路由与策略从后端移入 `dsh-tool-fs` 和 `dsh-fs-policy`。四处接口保留了拆分前的形态——每次调用都填充,却无人读取: +[fs seam 拆分](2026-06-26-fsspec-style-fs-seam.md)将读取路由与策略从后端移至 `dsh-tool-fs` 和 `dsh-fs-policy`。有四处接口保留了拆分前的形态——每次调用都填充,却无人读取: -1. **`dsh-fs-local` 中的 `STREAM_MIN_SIZE` + `FsIoInternals.streamMinSize`**——*在本次变更之前已被"无硬编码可调参数"审计移除,该审计将路由阈值改为 `dsh-tool-fs` 的 `readStreamMinSize` 配置;此处记录是为了完整呈现整个裁剪。*原始位置(`packages/fs/fs-local/src/fsio.ts`,从 `packages/fs/fs-local/src/index.ts` 再导出):包括 fs-local 自身源码和测试在内,全仓库零读取者。后端不做读取路由——`readWholeText`/`streamWholeText` 是调用方自行选择的独立原语——真正的路由常量在消费方(`packages/fs/tool-fs/src/read.ts`,与 `info.size` 比较)。10 MiB 这个事实有两份镜像;后端那份是死代码,而该旋钮的 JSDoc 声称提供一个并不存在的"读取路由"覆盖。 -2. **`FsTarget.inputPath`**(`packages/fs/fs/src/types.ts`):每个后端和每个测试 fake 都必须编造一个"仅用于诊断"的值,而生产环境零读取者——策略插件和所有错误消息使用的是 `targetKey`/`displayPath`。`listDir` 的生产者暴露了语义摇摆:目录子项拿到的是裸条目名,这不是任何人的"输入路径"。 -3. **`FsEditOutcome.replacements` + `.replaceAll`**(`packages/fs/fs/src/types.ts`):`replacements` 生产环境零读取者(单匹配策略本身保留——它由后端内部的 `FS_AMBIGUOUS_EDIT`/`FS_EDIT_NOT_FOUND` 抛出强制执行,错误消息保留了内部计数);`replaceAll` 仅被 `packages/fs/tool-fs/src/edit.ts` 中的 `formatEditOutput` 读取——作为工具已持有的 `replace_all` 参数的回声。精简后,`FsEditOutcome` 变为 `{ version, before, after }`,与 `FsWriteOutcome` 中真正由后端发现的字段对齐。 -4. **`FileReadOutcome.limit` + `.version`**(`packages/fs/tool-fs/src/read-render.ts`):由读取工具填充,但 `formatReadOutput` 只渲染 `offset`/`lines`/`totalLines`/`truncatedByBytes`,而 `fs/observed` 事件直接使用 `info.version`,不使用 outcome 的副本。 +1. **`dsh-fs-local` 中的 `STREAM_MIN_SIZE` + `FsIoInternals.streamMinSize`**——*在本次变更之前已被「禁止硬编码可调参数」审计移除,该审计将路由阈值改为 `dsh-tool-fs` 的 `readStreamMinSize` 配置;此处记录是为了完整呈现整次清理。* 原始位置(`packages/fs/fs-local/src/fsio.ts`,从 `packages/fs/fs-local/src/index.ts` 重导出):包括 fs-local 自身源码和测试在内,全仓库零读取者。后端没有读取路由——`readWholeText`/`streamWholeText` 是调用方自行选择的两个独立原语——真正的路由常量位于消费方(`packages/fs/tool-fs/src/read.ts`,与 `info.size` 比较)。同一个 10 MiB 事实的两份镜像;后端那份是死代码,且该旋钮的 JSDoc 声称提供一个实际不存在的「read routing」覆盖。 +2. **`FsTarget.inputPath`**(`packages/fs/fs/src/types.ts`):每个后端和每个测试 mock 都必须为这个「仅供诊断」的字段编造一个值,而生产环境零读取者——策略插件和所有错误消息使用的是 `targetKey`/`displayPath`。`listDir` 的生产者暴露了语义上的摇摆:目录子项得到的是裸条目名,这不是任何人的「input」。 +3. **`FsEditOutcome.replacements` + `.replaceAll`**(`packages/fs/fs/src/types.ts`):`replacements` 生产环境零读取者(单匹配策略本身保留——它由后端内部 `FS_AMBIGUOUS_EDIT`/`FS_EDIT_NOT_FOUND` 抛出来强制执行,错误消息保留了内部计数);`replaceAll` 仅被 `packages/fs/tool-fs/src/edit.ts` 中的 `formatEditOutput` 读取——作为工具本身已持有的 `replace_all` 参数的回声。精简后,`FsEditOutcome` 变为 `{ version, before, after }`,与 `FsWriteOutcome` 中真正由后端发现的字段对齐。 +4. **`FileReadOutcome.limit` + `.version`**(`packages/fs/tool-fs/src/read-render.ts`):由读取工具填充,但 `formatReadOutput` 只渲染 `offset`/`lines`/`totalLines`/`truncatedByBytes`,且 `fs/observed` 事件发射直接使用 `info.version` 而非 outcome 的副本。 ## 决策 -删除 fs-local 常量及其再导出和 `streamMinSize` 旋钮(`FsIoInternals` 中剩余的旋钮确实被原子写入测试使用);从 `FsTarget` 中移除 `inputPath`;将 `FsEditOutcome` 精简为 `{ version, before, after }`,并将 `replaceAll` 从解析后的参数传给 `formatEditOutput`;从 `FileReadOutcome` 中移除 `limit`/`version`。[filesystem.md](../../../core-data-structures/filesystem.md) 中的粘贴内容、`packages/fs/fs/README.md`,以及那些不得不编造被移除字段的测试 fake 随类型一起精简。 +删除 fs-local 的常量及其重导出,以及 `streamMinSize` 旋钮(`FsIoInternals` 中剩余的旋钮确实被原子写入测试使用);从 `FsTarget` 中移除 `inputPath`;将 `FsEditOutcome` 精简为 `{ version, before, after }`,并将 `replaceAll` 从解析后的参数传入 `formatEditOutput`;从 `FileReadOutcome` 中移除 `limit`/`version`。[filesystem.md](../../../core-data-structures/filesystem.md) 中的粘贴内容、`packages/fs/fs/README.md`,以及那些不得不为已移除字段编造值的测试 mock,都随类型一起缩减。 ## 曾考虑的替代方案 ### 为什么不保留? -未来的权限/隔离层可能需要解析前的路径来生成错误文本——但它需要的是*请求*,每个调用点仍然持有请求。"替换了 N 处"可能成为面向模型的文本——那是需要时再设计的行为变更,且后端内部的计数为其错误消息保留着。读取页脚可能展示 `limit`——页脚展示的一切已经可以从 `lines`/`totalLines` 推导。与此同时,当前和未来的每个后端(远程、原生)都必须编造无人消费的协议格式(wire format)字段,每个测试 fake 都必须满足它们。 +未来的权限/隔离层可能需要解析前的路径来生成错误文本——但它需要的是*请求*,每个调用点仍然持有请求。「替换了 N 处」可能成为面向模型的文本——这是一个需要时再设计的行为变更,且后端内部的计数为其错误消息而保留。读取页脚可能展示 `limit`——但页脚展示的一切已经可以从 `lines`/`totalLines` 推导。与此同时,每个现有和未来的后端(远程、原生)都必须编造无人消费的协议字段,每个测试 mock 都必须满足它们。 ## 验证 -被移除的接口已不存在——`dsh-fs-local` 中的 `STREAM_MIN_SIZE`/`streamMinSize`、`FsTarget.inputPath`、`FsEditOutcome.replacements`/`.replaceAll`、`FileReadOutcome.limit`/`.version`——而请求侧的 `replaceAll`(`FsEditRequest`)和其他 outcome 类型上的 version 字段未受影响;测试 fake 随类型一起精简。`formatEditOutput` 在 `replace_all` 两个分支下的输出文本不变,因此没有快照 golden 被搅动。 +被移除的接口已消失——`dsh-fs-local` 中的 `STREAM_MIN_SIZE`/`streamMinSize`、`FsTarget.inputPath`、`FsEditOutcome.replacements`/`.replaceAll`,以及 `FileReadOutcome.limit`/`.version`——而请求侧的 `replaceAll`(`FsEditRequest`)和其他 outcome 类型上的 version 字段未受影响;测试 mock 随类型一起缩减。`formatEditOutput` 在 `replace_all` 两个分支下输出的文本不变,因此没有快照黄金文件被搅动。 ## 后果 -后端不增加新义务;它们卸下了四个无人消费的字段。fs 发现工作(glob/grep 工具)触及相同的 `dsh-fs` 类型文件——这是文本层面而非设计层面的重叠,可以机械地解决。 +后端不增加新义务,反而卸下了四个无人消费的字段。fs 发现功能(glob/grep 工具)涉及相同的 `dsh-fs` 类型文件——这是文本层面而非设计层面的重叠,可以机械地合并解决。 diff --git a/docs/rfc/implemented/simplification/2026-07-04-remove-agent-steering-mirror.i18n.yaml b/docs/rfc/implemented/simplification/2026-07-04-remove-agent-steering-mirror.i18n.yaml index 1e1c22f3c1..be2b32be56 100644 --- a/docs/rfc/implemented/simplification/2026-07-04-remove-agent-steering-mirror.i18n.yaml +++ b/docs/rfc/implemented/simplification/2026-07-04-remove-agent-steering-mirror.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-04-remove-agent-steering-mirror.md: 311f0f8ffd278adf71a35617b900d4d16037055a -2026-07-04-remove-agent-steering-mirror.zh.md: 4ceb31a0263a7c386ceed071d3f0db315c7a9f23 +2026-07-04-remove-agent-steering-mirror.zh.md: 24198afb0714863867f4d7e17ae19ea8af6a88bd diff --git a/docs/rfc/implemented/simplification/2026-07-04-remove-agent-steering-mirror.zh.md b/docs/rfc/implemented/simplification/2026-07-04-remove-agent-steering-mirror.zh.md index 4ceb31a026..24198afb07 100644 --- a/docs/rfc/implemented/simplification/2026-07-04-remove-agent-steering-mirror.zh.md +++ b/docs/rfc/implemented/simplification/2026-07-04-remove-agent-steering-mirror.zh.md @@ -1,4 +1,4 @@ -# RFC:移除 `agent/steering` 镜像事件发射 +# RFC:移除 `agent/steering` 镜像 emit [English](2026-07-04-remove-agent-steering-mirror.md) | 中文 @@ -6,28 +6,28 @@ Status: implemented ## 问题 -`agent/steering` 是最后一个仍存在的、对持久会话事件的瞬态镜像。循环的 steering 排空逻辑先追加持久事件 `steering/message { turn, content, source }`,紧接着下一行就发射 `agent/steering(agent, turn, content, source)`——同一个事实以 fire-and-forget 事件的形式重复一遍(`packages/core/agent-loop/src/loop.ts`,`drainSteering`)。它在生产环境中没有任何监听者:唯一的订阅方是一个循环回归测试,断言该发射携带了 `source`——而这个事实在上一行的持久事件中已经记录。 +`agent/steering` 是最后一个仍存在的、对持久会话事件的瞬态镜像。agent loop(智能体循环)的 steering(中途引导)drain 逻辑先追加持久事件 `steering/message { turn, content, source }`,紧接着下一行就 emit `agent/steering(agent, turn, content, source)`——同一个事实以 fire-and-forget 事件的形式重复发出(`packages/core/agent-loop/src/loop.ts`,`drainSteering`)。它在生产环境中没有任何监听者:唯一的订阅方是一个 agent loop 回归测试,断言 emit 携带了 `source`——而这同一个事实已经由上一行的持久事件记录。 -`agent/steering` 以相同的 payload 复制了紧邻其前的持久事件 `steering/message`。`agent/queued` 则保留为纯 live 信号,因为它在持久化之前触发,覆盖了可能在进入日志前被取消的工作。 +`agent/steering` 以相同的 payload 重复了紧接其前的持久事件 `steering/message`。`agent/queued` 仍保留为纯瞬态信号,因为它在持久化之前触发,覆盖了可能在进入日志前被取消的工作。 -steering(中途引导)承载着真实的生产流量:钩子桥的轮次续行决策通过 `inbox.steer()` 注入原因,落地为持久的 `steering/message` 事件,钩子矩阵的 golden 文件固定了这些事件。所有这些消费方观察的都是持久事件,没有任何消费方观察镜像。 +steering 承载着真实的生产流量:hook bridge 的轮次续行决策通过 `inbox.steer()` 注入理由,落地为持久的 `steering/message` 事件,hook-matrix 的 golden 文件对此进行固定——所有这些消费方观察的都是持久事件。没有任何消费方观察镜像事件。 ## 决策 -`agent/steering` 从 agent 事件分类体系中移除:`packages/core/agent/src/types.ts` 中的声明(及其在 live-events JSDoc 列表中的提及)、`drainSteering` 中的发射(随之移除的还有当时已无用的 `ctx` 参数)、`packages/core/agent/README.md` 中的对应行,以及循环伪代码块中的发射行(`packages/core/agent-loop/src/loop.ts` 模块文档与 [architecture.md](../../../architecture.md));Cordis catalog 重新生成后不再包含它。唯一的回归测试改为在持久事件 `steering/message` 上固定 source 保持——它所固定的事实存在于日志中。 +`agent/steering` 从 agent 事件分类体系中移除:`packages/core/agent/src/types.ts` 中的声明(及其在 live-events JSDoc 列表中的提及)、`drainSteering` 中的 emit(随之移除的还有当时已无用的 `ctx` 参数)、`packages/core/agent/README.md` 中的对应行,以及 loop 伪代码块中的 emit 行(`packages/core/agent-loop/src/loop.ts` 模块文档与 [architecture.md](../../../architecture.md));Cordis catalog 重新生成后不再包含它。唯一的回归测试改为在持久事件 `steering/message` 上固定 source 保持性——它所固定的事实存在于日志中。 -三份已实施的 RFC 曾声明保留该事件,每份均按 [implemented/AGENTS.md](../AGENTS.md) 修订,指向本 RFC 作为移除记录:[boundary RFC](2026-06-20-remove-agent-boundary-mirror-events.md) 的保留列表条目、[stream-chunk RFC](2026-07-02-remove-stream-chunk-mirror.md) 的范围条款,以及 [event-domain-semantics RFC](../architecture/2026-06-30-event-domain-semantics.md) 的瞬态发射枚举。 +三份已实施的 RFC 曾声明保留该事件,每份均按 [implemented/AGENTS.md](../AGENTS.md) 的要求修订,指向本 RFC 作为移除记录:[boundary RFC](2026-06-20-remove-agent-boundary-mirror-events.md) 的保留列表条目、[stream-chunk RFC](2026-07-02-remove-stream-chunk-mirror.md) 的范围条款,以及 [event-domain-semantics RFC](../architecture/2026-06-30-event-domain-semantics.md) 的瞬态 emit 枚举。 ## 曾考虑的替代方案 ### 为什么不保留? -"它是控制信号,不是边界事件"——但分类体系的实际区分维度是「镜像 vs 纯 live」,而非「控制 vs 边界」,而这个事件属于镜像。需要入队时通知的消费方有 `agent/queued`(带 steering flag);需要排空时通知的消费方本质上是在请求 `steering/message` 被追加的那一刻,而 `session/event` 以相同 payload 加上持久性提供了这一点。被否决的 [retire-mid-turn-steering RFC](../../rejected/simplification/2026-06-20-retire-mid-turn-steering.md) 捍卫的是 steering **能力**——`steer()`、持久事件、续行强制——本次移除对这些全部不动。 +"它是控制信号,不是边界事件"——但分类体系的操作性区分是「镜像 vs. 纯瞬态」,而非「控制 vs. 边界」,而这个事件属于镜像。需要入队时通知的消费方有 `agent/queued`(带 steering flag);需要 drain 时通知的消费方,本质上是在请求 `steering/message` 被追加的那一刻,而 `session/event` 以相同 payload 加上持久性提供了这一通知。被否决的 [retire-mid-turn-steering RFC](../../rejected/simplification/2026-06-20-retire-mid-turn-steering.md) 捍卫的是 steering **能力**——`steer()`、持久事件、续行强制——本次移除对这些全部保持不变。 ## 验证 -`agent/steering` 这一拼写仅存在于 RFC 行文中(本 RFC、上述三份修订后的 RFC,以及冻结的[被否决 steering 能力 RFC](../../rejected/simplification/2026-06-20-retire-mid-turn-steering.md),其文本记录了它所拒绝的提案);catalog 已重新生成;重定向后的测试在 `steering/message` 上固定 source 保持。 +`agent/steering` 这一拼写仅存于 RFC 行文中(本 RFC、上述三份修订的 RFC,以及冻结的[被否决的 steering 能力 RFC](../../rejected/simplification/2026-06-20-retire-mid-turn-steering.md),其文本记录了它所拒绝的提案);catalog 已重新生成;重定向后的测试在 `steering/message` 上固定 source 保持性。 ## 后果 -没有需要迁移的生产监听者。两种 live 通知需求都保留了归属:入队时通知归 `agent/queued`(带 `steering` flag),排空时通知归 `session/event`(持久的 `steering/message` 落地时触发)。 +生产环境中没有需要迁移的监听者,两种瞬态通知需求各有归宿:入队时由 `agent/queued`(带 `steering` flag)承载,drain 时由 `session/event` 在持久事件 `steering/message` 落地时承载。 diff --git a/docs/rfc/implemented/simplification/2026-07-04-share-app-bin-boot-glue.i18n.yaml b/docs/rfc/implemented/simplification/2026-07-04-share-app-bin-boot-glue.i18n.yaml index 16316292cd..6a470484f3 100644 --- a/docs/rfc/implemented/simplification/2026-07-04-share-app-bin-boot-glue.i18n.yaml +++ b/docs/rfc/implemented/simplification/2026-07-04-share-app-bin-boot-glue.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-04-share-app-bin-boot-glue.md: afaa61fe909f3dbf900337518969410780020a88 -2026-07-04-share-app-bin-boot-glue.zh.md: 2a022fbb8dcdf6977ede81780a87c570715ce441 +2026-07-04-share-app-bin-boot-glue.zh.md: 33fe2f27df9c8b172e4296ece0720813f9775f86 diff --git a/docs/rfc/implemented/simplification/2026-07-04-share-app-bin-boot-glue.zh.md b/docs/rfc/implemented/simplification/2026-07-04-share-app-bin-boot-glue.zh.md index 2a022fbb8d..33fe2f27df 100644 --- a/docs/rfc/implemented/simplification/2026-07-04-share-app-bin-boot-glue.zh.md +++ b/docs/rfc/implemented/simplification/2026-07-04-share-app-bin-boot-glue.zh.md @@ -1,27 +1,27 @@ -# RFC:共享应用 bin 的启动胶水代码,不再维护两份副本 - -Status: implemented +# RFC:共享应用 bin 的启动胶水代码,而非维护两份副本 [English](2026-07-04-share-app-bin-boot-glue.md) | 中文 +Status: implemented + ## 问题 -stdio 和 ACP bin 各自重复了环境加载、fail-loud 处理、入口校验与启动逻辑,包括微妙的 Loader 失败行为。两份副本已经发生漂移,且位于自执行文件中、被排除在单元测试覆盖率之外,导致其中的辅助导出无法被复用。 +stdio 和 ACP 两个 bin 各自重复了环境加载、fail-loud 处理、入口校验与启动逻辑,包括微妙的 Loader 失败行为。两份副本已经发生漂移,且位于自执行文件中、被排除在单元测试覆盖率之外,导致其导出的辅助函数无法被复用。 ## 决策 -辅助逻辑只存在一处:[`@deepseek-ai/dsh-app-boot`](../../../../packages/ui/app-boot)(`packages/ui/app-boot`,归入 `ui` 组,因为 bin 是已发布产物,其运行时依赖本身也必须是已发布的,而非 `support/`)。包含:`resolveConfigPath`(快照感知,两个 bin 共用的唯一路径解析器)、`loadEnv`、`installFailLoud`、`assertEntriesLoaded` 和 `boot`,每个函数都按 bin 的诊断前缀参数化,并在其副作用 seam(warn sink、process 切片)处可注入,使单元测试套件能覆盖每个分支——包括 `boot()` 在进程内驱动真实 Loader、使用相对路径 specifier 的配置,涵盖已就绪树的正常路径和无 fiber 入口的拒绝路径。该包(package)启用了逐文件 100% 覆盖率门禁;Loader 失败的经验知识只有一个归属地。 +辅助函数只存在一处:[`@deepseek-ai/dsh-app-boot`](../../../../packages/ui/app-boot)(`packages/ui/app-boot`,归入 `ui` 分组,因为 bin 是已发布产物,其运行时依赖本身也必须是已发布的包,而非 `support/`)。包含:`resolveConfigPath`(快照感知,两个 bin 共用的唯一路径解析器)、`loadEnv`、`installFailLoud`、`assertEntriesLoaded` 与 `boot`,每个函数都通过 bin 的诊断前缀参数化,并在其副作用 seam(warn sink、process slice)处支持注入,使单元测试套件能覆盖每个分支——包括 `boot()` 在进程内驱动真实 Loader、使用相对路径 specifier 配置的场景,既覆盖已稳定树的正常路径,也覆盖无 fiber 入口的拒绝路径。该包启用逐文件 100% 覆盖率门禁;Loader 失败的相关知识只有一个归属地。 -每个 `bin.ts` 是一个精简的自执行组合:在共享辅助逻辑之上叠加各自应用特有的生命周期(ACP bin:replay 模式下跳过环境加载与 stdin-EOF dispose;stdio bin:无额外逻辑)。bin 文件仍然被排除在覆盖率之外且不导出任何内容;已发布产物的防护措施不变——built-bin 冒烟测试仍然在一个 node_modules 形状的临时目录下用原生 node 运行每个 bin(现在也 symlink 了 `ui/app-boot`),并仍然断言缺少配置时的非零退出码,遵循「真实入口路径意味着已发布产物」的防御模式。[extract-example-app-packages RFC](../architecture/2026-06-20-extract-example-app-packages.md) 中关于 bin 归属的事实已相应修订。 +每个 `bin.ts` 是一个精简的自执行组合,基于共享辅助函数加上各自特有的应用生命周期(ACP bin:replay 模式下跳过 env 加载与 stdin-EOF dispose;stdio bin:无额外逻辑)。bin 文件仍被排除在覆盖率之外且不导出任何内容;已发布产物的守卫不变——built-bin 冒烟测试仍在 node_modules 形状的临时目录中以原生 node 运行每个 bin(现在也符号链接了 `ui/app-boot`),并仍断言缺少配置时的非零退出码,遵循「真实入口路径即已发布产物」的防御模式。[extract-example-app-packages RFC](../architecture/2026-06-20-extract-example-app-packages.md) 中关于 bin 归属的事实已相应修订。 ## 曾考虑的替代方案 -### 为什么不保留重复? +### 为何不保留重复? -bin 被定位为独立拥有的已发布产物,而新增一个包有固定开销(manifest、README、tsconfig reference、publint 表面积),与去重的代码行数相当。但创建 bin 的那份 RFC 从未权衡过应用间共享——它把三个示例 `start.ts` 副本合并**进**了 bin 就止步了;漂移是已观察到的事实;而覆盖率缺口的论点独立于去重论点:这是仓库中唯一被豁免于逐文件 100% 门禁的非平凡运行时逻辑。记录在案的回退方案(仅将纯逻辑提取为各应用模块)可以结束豁免,但会保留两个经验知识归属地。 +bin 被定位为独立拥有的已发布产物,而新增一个包(package)带来的固定开销(manifest(元数据清单)、README、tsconfig reference、publint 表面积)与去重的代码行数相当。但创建 bin 的那份 RFC 从未权衡过应用间共享的可能——它将三份示例 `start.ts` 副本合并进 bin 后便止步了;漂移是已观察到的事实;而覆盖率缺口的论据独立于去重论据:这是仓库中唯一免于逐文件 100% 门禁的非平凡运行时逻辑。记录在案的备选方案(仅将纯逻辑提取为各应用自己的模块)虽能终结豁免,但会保留两个知识归属地。 ## 后果 -- 启动胶水代码的变更(新增守卫、修复解析)只需落地一次,两个已发布 bin 自动继承;bin 之间不会再次漂移。 -- `dsh-app-boot` 保持依赖精简(cordis + loader/include 对)——它是启动机制,不是应用接口。 -- bin 自身的文件是近乎平凡的组合;所有带分支的逻辑都在覆盖率门禁之下。 +- 启动胶水代码的变更(新增守卫、修复路径解析)只需落地一次,两个已发布 bin 自动继承;bin 之间不会再次漂移。 +- `dsh-app-boot` 保持轻量依赖(cordis + loader/include 对)——它是启动机制,不是应用表面积。 +- bin 自身的文件几乎是平凡的组合;所有含分支的逻辑都在覆盖率门禁之下。 diff --git a/docs/rfc/implemented/simplification/2026-07-04-tighten-hook-protocol-contract.i18n.yaml b/docs/rfc/implemented/simplification/2026-07-04-tighten-hook-protocol-contract.i18n.yaml index 38cd3003a8..95c2fdbf50 100644 --- a/docs/rfc/implemented/simplification/2026-07-04-tighten-hook-protocol-contract.i18n.yaml +++ b/docs/rfc/implemented/simplification/2026-07-04-tighten-hook-protocol-contract.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-04-tighten-hook-protocol-contract.md: df438516b836315902378afe7f4fd09e512c0966 -2026-07-04-tighten-hook-protocol-contract.zh.md: decc6ba86131f6d1930eb51a1267a9668d91dcb2 +2026-07-04-tighten-hook-protocol-contract.zh.md: 256da42993c8581e8bce861ecbfac22dbf5f0545 diff --git a/docs/rfc/implemented/simplification/2026-07-04-tighten-hook-protocol-contract.zh.md b/docs/rfc/implemented/simplification/2026-07-04-tighten-hook-protocol-contract.zh.md index decc6ba861..256da42993 100644 --- a/docs/rfc/implemented/simplification/2026-07-04-tighten-hook-protocol-contract.zh.md +++ b/docs/rfc/implemented/simplification/2026-07-04-tighten-hook-protocol-contract.zh.md @@ -1,4 +1,4 @@ -# RFC:收紧 hook 协议契约——dialect、废弃字段、双重默认值与 lib 拥有的 `hook/result` 语义 +# RFC:收紧 hook-protocol 契约——dialect、废弃字段、双重默认值与 lib 拥有的 `hook/result` 语义 [English](2026-07-04-tighten-hook-protocol-contract.md) | 中文 @@ -8,25 +8,25 @@ Status: implemented `dsh-hook-protocol`/bridge 契约中有四处遗漏了 [subagent-observe-enrich RFC](../feature/2026-06-30-subagent-observe-enrich.md) 所记录的纪律——该 RFC 因缺乏消费方而移除了 `agentType` 生命周期字段,以下四处未通过同样的检验: -1. **`HookDialect` 的 `'native'` 变体**(`packages/hooks/hook-protocol/src/types.ts`)没有任何生产者——bridge 只打 `'claude'` 和 `'codex'` 标记;唯一的 `'native'` 构造出现在 lib 自身的单元测试中。该字段自己的 JSDoc 将 `dialect` 定义为「执行它的 bridge」,而 native 不是 bridge:[interception-seams RFC](../feature/2026-06-30-interception-seams.md) 记录了 native 钩子不是一个 package,且「native 插件已经可以直接使用类型化的 Decisions」而无需持久化的 hook 日志;旗舰 native 插件的工作示例也正是如此断言的(完全没有 `hook/*` 事件)。 -2. **`HookOutput.suppressOutput`**(同一文件)被 codec 解析后在所有路径上都被丢弃:没有 bridge 分支、没有 merge fold、没有 warn、没有 deferred-list 行——在所有「被解析但未兑现」的同类字段中,它是唯一没有明确延期声明的(`updatedInput` → 一条 warn 日志加 [pre-tool-input-rewrite 提案](../../proposed/feature/2026-06-30-pre-tool-input-rewrite.md);`systemMessage` → 一条 warn 日志加 README deferred 行;`continue`/`stopReason` → 一个 `TODO(hook-continue-false)` 锚点加 `'stop'` decision 记录)。从结构上看根本没有什么可 suppress 的:hook 的 stdout 从不进入任何 transcript(文本记录)(上下文仅通过 `additionalContext` 流入;日志只记录 `decision`/`stderrSummary`),因此 hook 作者设置 `suppressOutput: true` 得到的是无声的空操作,连 warn 都没有。 -3. **`defaultTimeoutMs` 在两个 bridge 配置中被双重默认,使用浮动字面量**——一个 schema `.default(600_000)` 加一个 `?? 600_000` 回退(`packages/hooks/hooks-claude/src/index.ts`、`packages/hooks/hooks-codex/src/index.ts`),每个 bridge 为同一个协议级常量提供两个归属,两个 bridge 可能在共享默认值上悄然分歧。*本提案最初的补救——彻底删除该配置项——被 no-hardcoded-tunables 审计取代,后者保留了该配置项作为 bridge 拥有的显式配置(并在旁边新增了 `stderrSummaryMaxChars`);剩下需要修复的是字面量的归属。* -4. **`hook/result` 的语义存在于两个 bridge 中(各一份),而非拥有该事件的 lib。** `summarize()`——stderr 截断规则——在 `packages/hooks/hooks-claude/src/index.ts` 和 `packages/hooks/hooks-codex/src/index.ts` 中逐字节相同,decision 字符串规则 `output.decision ?? (output.continue === false ? 'stop' : 'pass')` 也是如此;然而 `dsh-hook-protocol` 声明了 `hook/result`、将 `stderrSummary` 文档化为「已截断」却不拥有截断逻辑,将 decision 值文档化却不拥有映射逻辑。如果某个 bridge 漂移(不同的上限、不同的回退),共享的持久化事件的语义就会悄然分叉。 +1. **`HookDialect` 的 `'native'` 变体**(`packages/hooks/hook-protocol/src/types.ts`)没有任何生产者——bridge 只会标记 `'claude'` 和 `'codex'`;唯一构造 `'native'` 的地方是 lib 自身的单元测试。该字段的 JSDoc 将 `dialect` 定义为「运行它的 bridge」,而 native 并非 bridge:[interception-seams RFC](../feature/2026-06-30-interception-seams.md) 记录了 native hook 不是一个 package,且「native 插件已经可以直接使用类型化的 Decisions」而无需持久化 hook 日志;旗舰 native-plugin 示例也正是如此断言的(完全没有 `hook/*` 事件)。 +2. **`HookOutput.suppressOutput`**(同一文件)被 codec 解析后在所有路径上均被丢弃:没有 bridge 分支处理它、没有 merge fold、没有 warn、没有 deferred-list 行——在所有「被解析但未兑现」的同类字段中它是唯一没有明确延期声明的(`updatedInput` → 一条 warn 日志加 [pre-tool-input-rewrite 提案](../../proposed/feature/2026-06-30-pre-tool-input-rewrite.md);`systemMessage` → 一条 warn 日志加 README deferred 行;`continue`/`stopReason` → 一个 `TODO(hook-continue-false)` 锚点加 `'stop'` decision 记录)。从结构上看根本无物可抑制:hook stdout 从不进入任何 transcript(文本记录)(上下文仅通过 `additionalContext` 流入;日志只记录 `decision`/`stderrSummary`),因此 hook 作者设置 `suppressOutput: true` 得到的是无声的空操作,且无任何警告。 +3. **`defaultTimeoutMs` 在两个 bridge 配置中以浮动字面量双重默认**——schema 的 `.default(600_000)` 加上一个 `?? 600_000` 回退(`packages/hooks/hooks-claude/src/index.ts`、`packages/hooks/hooks-codex/src/index.ts`),一个协议级常量在每个 bridge 中有两个归属地,两个 bridge 可能在共享默认值上悄然分歧。*提案最初的补救措施是彻底删除该旋钮,但被 no-hardcoded-tunables 审计所取代:审计保留了该旋钮作为 bridge 拥有的显式配置(并在旁边新增了 `stderrSummaryMaxChars`);剩下要修的是字面量的归属地。* +4. **`hook/result` 的语义存在于两个 bridge 中(各一份),而非拥有该事件的 lib。** `summarize()`——stderr 截断规则——在 `packages/hooks/hooks-claude/src/index.ts` 与 `packages/hooks/hooks-codex/src/index.ts` 中逐字节相同;decision 字符串规则 `output.decision ?? (output.continue === false ? 'stop' : 'pass')` 同样如此。然而 `dsh-hook-protocol` 声明了 `hook/result`、在文档中将 `stderrSummary` 描述为「已截断」却不拥有截断逻辑,记录了 decision 值却不拥有映射逻辑。如果某个 bridge 漂移(不同的上限、不同的回退),共享持久化事件的语义就会悄然分叉。 ## 决策 -`HookDialect` 是封闭的 bridge 集合,`'claude' | 'codex'`;`HookOutput` 移除不受支持的 `suppressOutput`。`hook/result.durationMs` 保留为持久化的审计计时,仅在快照中做归一化。参考默认值各只存在一处:`DEFAULT_HOOK_TIMEOUT_MS` 和 `DEFAULT_STDERR_SUMMARY_MAX_CHARS`。`HookResultRecord` 与 `appendHookResult` 为两个 bridge 统一拥有 stderr 摘要化和 decision 推导逻辑。`BLOCKING_EXIT_CODE` 为 codec 内部常量。 +`HookDialect` 是封闭的 bridge 集合:`'claude' | 'codex'`;`HookOutput` 移除了不受支持的 `suppressOutput`。`hook/result.durationMs` 保留为持久化的审计计时,仅在快照中做归一化。参考默认值各只存在一处:`DEFAULT_HOOK_TIMEOUT_MS` 与 `DEFAULT_STDERR_SUMMARY_MAX_CHARS`。`HookResultRecord` 与 `appendHookResult` 为两个 bridge 统一拥有 stderr 摘要化和 decision 推导逻辑。`BLOCKING_EXIT_CODE` 为 codec 内部常量。 ## 曾考虑的替代方案 -### 为什么不保留? +### 为什么不保留它们? -不受支持的词汇(vocabulary)可以在真正有消费方时回归。`durationMs` 保留,因为持久化的审计计时独立于当前是否有读取者而有价值。Bridge 特有的 payload 构造留在各自 bridge 中,而共享的持久化事件归一化属于协议库。 +不受支持的词汇可以在真正有消费方时回归。`durationMs` 保留,因为持久化的审计计时独立于当前是否有读取方而有价值。Bridge 特有的 payload 构造留在各自 bridge 中,而共享持久化事件的归一化属于协议库。 ## 验证 -`HookDialect` 只包含 Claude 和 Codex,`suppressOutput` 在源码、解析字段文档和归一化逻辑中均不存在。`durationMs` 保留在事件和 fixture(测试前置数据)中,回放时做擦除。`600_000` 和 `500` 默认值各只在协议库中出现一次,per-hook 超时覆盖仍然生效,两个 bridge 的测试套件都验证了库拥有的 stderr 截断和 decision 规则。 +`HookDialect` 仅包含 Claude 和 Codex,`suppressOutput` 在源码、已解析字段文档和归一化逻辑中均不存在。`durationMs` 保留在事件和 fixture(测试前置数据)中,回放时做清洗。`600_000` 和 `500` 两个默认值各只在协议库中出现一次;per-hook 超时覆盖仍然生效;两个 bridge 的测试套件均验证了由库拥有的 stderr 截断和 decision 规则。 ## 后果 -`dialect`、`suppressOutput`、可调参数与语义变更在协议格式(wire format)和 golden 文件上不可见。代价是 `dsh-hook-protocol` 和两个 bridge 的代码变动——在预发布阶段这很廉价,且比让持久化事件语义的两份副本各自老化要廉价得多。 +`dialect`、`suppressOutput`、可调参数与语义的变更在协议格式(wire format)和 golden 文件中均不可见。代价是 `dsh-hook-protocol` 与两个 bridge 的代码变动——在预发布阶段这很廉价,且比让持久化事件语义的两份副本各自老化要廉价得多。 diff --git a/docs/rfc/implemented/simplification/2026-07-04-trim-acp-bridge-unreachable-surface.i18n.yaml b/docs/rfc/implemented/simplification/2026-07-04-trim-acp-bridge-unreachable-surface.i18n.yaml index f91d18a48e..aa5df52a6a 100644 --- a/docs/rfc/implemented/simplification/2026-07-04-trim-acp-bridge-unreachable-surface.i18n.yaml +++ b/docs/rfc/implemented/simplification/2026-07-04-trim-acp-bridge-unreachable-surface.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-04-trim-acp-bridge-unreachable-surface.md: 6decb494dcbfd348777577002187007597a8c374 -2026-07-04-trim-acp-bridge-unreachable-surface.zh.md: 77e22f75c96704ee0379c45d2ed23167df047324 +2026-07-04-trim-acp-bridge-unreachable-surface.zh.md: 9d588e6f1132869ead60586b4df6844400307863 diff --git a/docs/rfc/implemented/simplification/2026-07-04-trim-acp-bridge-unreachable-surface.zh.md b/docs/rfc/implemented/simplification/2026-07-04-trim-acp-bridge-unreachable-surface.zh.md index 77e22f75c9..9d588e6f11 100644 --- a/docs/rfc/implemented/simplification/2026-07-04-trim-acp-bridge-unreachable-surface.zh.md +++ b/docs/rfc/implemented/simplification/2026-07-04-trim-acp-bridge-unreachable-surface.zh.md @@ -1,26 +1,26 @@ -# RFC:裁剪不可达的 ACP bridge 接口——品牌旋钮与 kind 嗅探回退 - -Status: implemented +# RFC:裁剪不可达的 ACP 桥接层表面——品牌配置项与 kind 嗅探回退 [English](2026-07-04-trim-acp-bridge-unreachable-surface.md) | 中文 +Status: implemented + ## 问题 -`dsh-acp` 有两处接口在任何已交付的配置下都不可达: +`dsh-acp` 有两处对外表面在任何已交付的配置中都不可达: -1. **`AcpConfig.agentName` / `agentVersion`**(`packages/ui/acp/src/index.ts`)。已交付的 app 包(package)只向 bridge 传入 `{ model }`(`packages/examples/acp-demo/src/index.ts`),因此唯一的生产配置面——叶子 `cordis.yml`——根本无法设置这两个旋钮;它们只能通过直接挂载 bridge 来设置,而只有单元测试这样做。所有快照 golden(包括 hook-matrix 场景)都固定了 schema 默认值(`deepseek-harness-acp` / `0.0.1`)。这对字段还带着一条活跃的 `TODO(double-default)`:字面量存在两份(schema 的 `.default(...)` 加 `??` 回退),TODO 要求选定一个归属。 -2. **`toolKindFor` 名称启发式**(同一文件)在通用回退路径中对 `bash*`/`read*`/`write`/`edit*` 工具名做了特殊处理。自 [render-intent union](../architecture/2026-07-02-tool-render-intent-union.md) 以来,这些分支匹配到的每个第一方工具都自带 `presentCall` 并携带其 kind,而没有 presenter 的生产工具(`subagent`、`subagent_fork`)本来就落入 `other`。这些分支在生产中可达的唯一情况是:某个工具拒绝自行呈现其调用——`presentCall` 抛出异常(containment 回退),或模型参数未通过工具 schema 导致 `defineTool` 的 `presentCall` 包装层返回 `undefined`(例如 `bash` 调用缺少必需的 `description`)——而 bridge 自身的模块文档明确声明了该启发式所违反的设计规则:「bridge 从不对工具名做特殊处理」。 +1. **`AcpConfig.agentName` / `agentVersion`**(`packages/ui/acp/src/index.ts`)。已交付的 app 包(`packages/examples/acp-demo/src/index.ts`)只向桥接层传递 `{ model }`,因此没有任何叶子 `cordis.yml`(唯一的生产配置表面)能设置这两个配置项;它们只有通过直接挂载桥接层才能设置,而只有单元测试这样做。所有快照 golden(包括 hook-matrix 场景)都固定了 schema 默认值(`deepseek-harness-acp` / `0.0.1`)。这对字段还带着一个活跃的 `TODO(double-default)`:字面量存在两份(schema 的 `.default(...)` 加 `??` 回退),TODO 要求选定一个归属。 +2. **`toolKindFor` 名称启发式**(同一文件)在通用回退路径中对 `bash*`/`read*`/`write`/`edit*` 工具名做了特殊处理。自 [render-intent union](../architecture/2026-07-02-tool-render-intent-union.md) 以来,这些分支匹配到的每个第一方工具都自带 `presentCall` 并携带其 kind,而没有 presenter 的生产工具(`subagent`、`subagent_fork`)本来就落入 `other`。这些分支只有在工具拒绝自行呈现调用时才在生产中可达:`presentCall` 抛出异常(容错回退),或模型参数未通过工具 schema 导致 `defineTool` 的 `presentCall` 包装层返回 `undefined`(例如 `bash` 调用缺少必需的 `description`)。而桥接层自身的模块文档明确声明了该启发式所违反的设计规则:"桥接层绝不对工具名做特殊处理"。 ## 决策 -在初始化时硬编码现有的握手标识 `{ name: 'deepseek-harness-acp', version: '0.0.1' }`,移除不可达的配置字段与重复默认值。在两处 presenter 回退中,将 `toolKindFor` 替换为中性的 `'other'`。正常的第一方呈现不受影响;格式错误或失败的呈现现在渲染一张诚实的通用卡片,而非从工具名推断 kind。初始化测试和快照固定握手标识;只有 `hook-codex-posttool-block` 中格式错误的调用改变了回退卡片的 kind。 +在初始化时硬编码现有的握手标识 `{ name: 'deepseek-harness-acp', version: '0.0.1' }`,移除不可达的配置字段与重复默认值。在两个 presenter 回退处,将 `toolKindFor` 替换为中性的 `'other'`。正常的第一方呈现不受影响;格式错误或失败的呈现现在会渲染一个诚实的通用卡片,而非从工具名推断 kind。初始化测试和快照固定握手标识;只有 `hook-codex-posttool-block` 中格式错误的调用改变了回退卡片的 kind。 ## 曾考虑的替代方案 ### 为什么不保留? -品牌旋钮可以在 app 包将其暴露给部署时回归。从未知工具名推断呈现方式违反了 render-intent 契约;中性回退卡片还能为格式错误的调用和损坏的 presenter 保留原始输入。 +品牌配置可以在 app 包将其暴露给部署环境时再回来。从未知工具名推断呈现方式违反了 render-intent 契约;中性回退卡片还能为格式错误的调用和损坏的 presenter 保留原始输入。 ## 后果 -除上述回退渲染的取舍外无其他影响——退化路径下,中性卡片比推断出的第一方卡片更易于诊断。 +除上述回退渲染的取舍外没有其他影响——退化路径下,中性卡片比推断出的第一方卡片更易于诊断。 diff --git a/docs/rfc/implemented/simplification/2026-07-12-drop-unconsumed-skill-provider-events.i18n.yaml b/docs/rfc/implemented/simplification/2026-07-12-drop-unconsumed-skill-provider-events.i18n.yaml index b423b59448..593cb8a50e 100644 --- a/docs/rfc/implemented/simplification/2026-07-12-drop-unconsumed-skill-provider-events.i18n.yaml +++ b/docs/rfc/implemented/simplification/2026-07-12-drop-unconsumed-skill-provider-events.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-12-drop-unconsumed-skill-provider-events.md: 5ec9d201939b8f58334647353f599361bd2e58a0 -2026-07-12-drop-unconsumed-skill-provider-events.zh.md: 8ab2a5f55b2a1eed676b28a1ca7804d6237f63fd +2026-07-12-drop-unconsumed-skill-provider-events.zh.md: 15dbfedb07afa36b677074c403812d0f164c8bd5 diff --git a/docs/rfc/implemented/simplification/2026-07-12-drop-unconsumed-skill-provider-events.zh.md b/docs/rfc/implemented/simplification/2026-07-12-drop-unconsumed-skill-provider-events.zh.md index 8ab2a5f55b..15dbfedb07 100644 --- a/docs/rfc/implemented/simplification/2026-07-12-drop-unconsumed-skill-provider-events.zh.md +++ b/docs/rfc/implemented/simplification/2026-07-12-drop-unconsumed-skill-provider-events.zh.md @@ -1,29 +1,29 @@ # RFC:移除无消费方的 skill 提供方事件 -Status: implemented - [English](2026-07-12-drop-unconsumed-skill-provider-events.md) | 中文 +Status: implemented + ## 问题 -skill(技能)注册表产出了两个通知事件,但在生产代码中没有任何监听方。生成的生产者/消费方矩阵以及精确的事件名搜索表明,`skill/provider-added` 和 `skill/provider-removed` 只出现在声明、发射点、测试、生成目录和行文中。 +skill(技能)注册表产出两个通知事件,但没有生产环境的监听方。生成的生产者/消费方矩阵以及对事件名的精确搜索表明,`skill/provider-added` 与 `skill/provider-removed` 仅出现在声明、emit 站点、测试、生成的 catalog 和行文中。 -skill 发现按需读取当前提供方映射表,提供方注册同步清除已完成的目录缓存,await 后的修订检查防止陈旧的发现结果进入缓存。没有兄弟插件通过这些事件等待 skill 提供方,不同于 `subagent/provider-added` 的实际消费方(它容忍兄弟并发加载)。 +skill 发现按需读取当前的提供方映射表,提供方注册时同步清除已完成的 catalog,而 await 后的版本检查阻止了陈旧的发现结果进入缓存。没有兄弟插件通过这些事件等待 skill 提供方——与之形成对比的是活跃的 `subagent/provider-added` 消费方,它容忍兄弟并发加载。 -`tools/change` 和 `system-prompt/change` 明确不在本提案范围内。既有的简化决策将它们保留为面向实时工具和提示词 UI 的有意观测点,且自引用的已挂载插件已在使用 `tools/change`。本提案同样不改动 `subagent/provider-added`/`removed`,因为 `tool-subagent` 有生产级的生命周期消费方。 +`tools/change` 与 `system-prompt/change` 明确不在本提案范围内。既有的简化决策将它们保留为面向实时工具和提示词 UI 的有意观测点,且自引用的已挂载插件已在使用 `tools/change`。本提案同样不改动 `subagent/provider-added`/`removed`,因为 `tool-subagent` 有生产环境的生命周期消费方。 ## 决策 -skill 注册表不再声明和发射提供方成员变更事件。提供方的注册与 dispose(资源释放)仍为 effect 拥有的直接状态变更,同步使已完成的目录缓存失效;查找与发现按需读取当前提供方映射表。测试通过提供方查找和收集的输出来观察清理行为,而非生命周期通知。 +skill 注册表不再声明和 emit 提供方成员变更事件。提供方的注册与 dispose(资源释放)仍为 effect 所有的直接状态变更,同步使已完成的 catalog 失效;查找与发现按需读取当前提供方映射表。测试通过提供方查找和收集到的输出来观察清理行为,而非依赖生命周期通知。 -生成的事件目录、API 目录与生产者/消费方矩阵不再包含已删除的通知。skill 系统 RFC 和包文档通过 effect 拥有的直接状态及缓存失效契约来描述注册行为。 +生成的事件 catalog、API catalog 与生产者/消费方矩阵不再包含已删除的通知。skill 系统 RFC 与包文档通过 effect 所有的直接状态及缓存失效契约来描述注册行为。 ## 曾考虑的替代方案 -**为未来插件保留 skill 提供方通知。** 第三方插件可能想观察提供方的可用性,但直接提供方注册与按需查找才是扩展契约;当前没有消费方需要推送信号。如果未来出现兄弟加载竞态,可以像 subagent 注册表那样引入一个带有该消费方实际所需的身份与就绪语义的通知。 +**为未来插件保留 skill 提供方通知。** 第三方插件可能想观察提供方的可用性,但直接提供方注册与按需查找才是扩展契约;当前没有消费方需要推送信号。如果将来出现兄弟加载竞态,可以像 subagent 注册表那样,引入一个带有该消费方实际所需的身份与就绪语义的通知。 ## 后果 -生成的事件矩阵中不再有 `skill/provider-added` 或 `skill/provider-removed` 的行。skill 发现、直接运行时注册、提供方 effect 回滚/dispose、缓存失效与注册表查找清理均保留;随事件一起消失的是监听器触发的回滚。`tools/change`、`system-prompt/change` 以及已被消费的 subagent 提供方生命周期事件不受影响。 +生成的事件矩阵中不再有 `skill/provider-added` 或 `skill/provider-removed` 的行。skill 发现、直接运行时注册、提供方 effect 回滚/dispose、缓存失效与注册表查找清理保持不变;监听方触发的回滚随事件一起消失。`tools/change`、`system-prompt/change` 以及已被消费的 subagent 提供方生命周期事件不受影响。 -预发布消费方失去 skill 提供方观测点,但仍保留贡献 skill 的两种方式:直接运行时注册与提供方注册。未来若有消费方需要实时的提供方可用性信息,须新增一个带有其实际所需的身份与就绪语义的专用通知。 +预发布消费方失去 skill 提供方观测点,但仍保留两种贡献 skill 的方式:直接运行时注册与提供方注册。未来若有消费方需要实时的提供方可用性信息,必须新增一个带有其实际所需的身份与就绪语义的专用通知。 diff --git a/docs/rfc/implemented/simplification/2026-07-12-prune-unused-web-seam-fields.i18n.yaml b/docs/rfc/implemented/simplification/2026-07-12-prune-unused-web-seam-fields.i18n.yaml index 6c0c1f9108..7413576228 100644 --- a/docs/rfc/implemented/simplification/2026-07-12-prune-unused-web-seam-fields.i18n.yaml +++ b/docs/rfc/implemented/simplification/2026-07-12-prune-unused-web-seam-fields.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-12-prune-unused-web-seam-fields.md: b4773c2706cf6d18ea4bb96720cd6c932cdf8942 -2026-07-12-prune-unused-web-seam-fields.zh.md: 1beb9e597c4b990f2c4aaaf3e34e71027c3f3fb7 +2026-07-12-prune-unused-web-seam-fields.zh.md: 2c18fbcb440ce85798c8f36cdc5dc649149d8ba9 diff --git a/docs/rfc/implemented/simplification/2026-07-12-prune-unused-web-seam-fields.zh.md b/docs/rfc/implemented/simplification/2026-07-12-prune-unused-web-seam-fields.zh.md index 1beb9e597c..2c18fbcb44 100644 --- a/docs/rfc/implemented/simplification/2026-07-12-prune-unused-web-seam-fields.zh.md +++ b/docs/rfc/implemented/simplification/2026-07-12-prune-unused-web-seam-fields.zh.md @@ -1,27 +1,27 @@ # RFC:裁剪 web seam 中未使用的字段 -Status: implemented - [English](2026-07-12-prune-unused-web-seam-fields.md) | 中文 +Status: implemented + ## 问题 -web 能力携带了一组 request/result/status 值,每个已交付的实现都填充了它们,但没有生产消费方读取。`WebSearchResult.providerId`、`query` 和 `WebFetchResult.providerId` 是结果回显;`tool-web` 只格式化 content/sources/truncation 或 final URL/status/body/truncation,其他运行时也不读取这些字段。搜索提供方返回 `WebProviderStatus.reason`,但可用性检查只看 `available`,并有意输出一条通用的不可用诊断。 +web 能力携带的 request/result/status 值,虽然每个已交付的实现都会填充,但没有任何生产环境的消费方读取它们。`WebSearchResult.providerId`、`query`与 `WebFetchResult.providerId` 是结果回显;`tool-web` 只格式化 content/sources/truncation 或最终 URL/status/body/truncation,没有其他运行时读取这些字段。搜索提供方返回 `WebProviderStatus.reason`,但可用性检查只看 `available`,并有意输出一条通用的不可用诊断信息。 -`WebFetchRequest.timeoutMs` 同样没有生产调用方设置。`tool-web` 只提供 URL,用工具定义的超时加 `exec.signal` 作为调用方截止时间,并依赖本地提供方的配置默认值作为兜底。这个未使用的按请求超时覆盖迫使 `web-fetch-local` 暴露 `maxTimeoutMs`、钳位两个超时源,并为没有产品路径能选中的优先级规则编写文档和测试。`WebExecContext` 则是另一个单字段包装层:每个调用方分配 `{ signal }`,每个提供方立即解包 `exec?.signal`;不存在第二个执行控制字段。 +`WebFetchRequest.timeoutMs` 同样从未被生产调用方设置。`tool-web` 只提供 URL,使用工具定义的 timeout 加 `exec.signal` 作为调用方截止时间,并依赖本地提供方的配置默认值作为兜底。这个未使用的逐请求覆盖迫使 `web-fetch-local` 暴露 `maxTimeoutMs`、对两个 timeout 来源做 clamp,并为没有任何产品路径能选中的优先级规则编写文档和测试。`WebExecContext` 则是另一个单字段包装层:每个调用方分配 `{ signal }`,每个提供方立即解包 `exec?.signal`;不存在第二个执行控制字段。 ## 决策 -web seam 省略搜索/抓取的 `providerId` 结果回显和搜索 `query` 回显;调用方本身已持有请求和提供方选择信息。提供方以返回布尔值的方法暴露可用性。抓取请求不再有按请求超时或 `maxTimeoutMs` 钳位;本地提供方保留其可配置的默认超时,工具保留自身的截止时间。提供方方法接收一个直接的可选 `AbortSignal`,而非单字段的 `WebExecContext` 包装层。 +web seam 移除搜索/抓取结果中的 `providerId` 回显和搜索的 `query` 回显;调用方本身已持有请求和提供方选择信息。提供方以返回布尔值的方法暴露可用性。抓取请求不再有逐请求 timeout 或 `maxTimeoutMs` clamp;本地提供方保留其可配置的默认 timeout,工具保留自身的截止时间。提供方方法直接接收一个可选的 `AbortSignal`,而非单字段的 `WebExecContext` 包装层。 -所有 web 实现和面向模型的工具使用更小的契约。接口/实现/消费方的包拆分、提供方选择、来源引用、最终 URL/状态数据、截断报告与安全限制保持不变。 +所有 web 实现与面向模型的工具使用更精简的契约。接口/实现/消费方的包(package)拆分、提供方选择、来源引用、最终 URL/状态数据、截断报告与安全限制保持不变。 ## 曾考虑的替代方案 -**保留自描述结果、按请求截止时间和可扩展的执行上下文对象。** 结果回显可以帮助通用遥测,请求超时可以帮助受信的编程调用方,包装层对象为未来的控制留出空间。但这样的消费方或第二字段并不存在;在每个提供方中携带重复的身份信息、第二套截止时间策略以及包装/解包管道,使当前契约更难实现和解释。如果遥测或按调用的预算控制到来,它应当定义哪个截止时间获胜、在哪里观测提供方身份,以及多个控制是否足以证明上下文对象的存在。 +**保留自描述结果、逐请求截止时间与可扩展的执行上下文对象。** 结果回显可以帮助通用遥测,请求级 timeout 可以帮助受信的程序化调用方,包装层则为未来的控制字段留出空间。但目前不存在这样的消费方或第二个字段;在每个提供方中携带重复的身份标识、第二套截止时间策略以及包装/解包管道,使当前契约更难实现和解释。如果遥测或逐调用预算控制到来,届时应当定义哪个截止时间优先、在哪里观测提供方身份,以及多个控制字段是否足以证明需要一个上下文对象。 ## 后果 -保留下来的每个 web request/result 字段都被生产代码消费或为执行提供方请求所必需。工具可见的搜索/抓取输出、提供方回退、中止行为、配置的超时兜底、截断与引用仍被覆盖,无需请求超时优先级分支或执行上下文包装层。 +保留下来的每个 web request/result 字段,要么被生产代码消费,要么是执行提供方请求所必需的。工具可见的搜索/抓取输出、提供方回退、中止行为、可配置的 timeout 兜底、截断与引用仍然被覆盖,无需请求级 timeout 优先级分支或执行上下文包装层。 -预发布的编程调用方失去结果来源回显和按请求的抓取截止时间。提供方仍有部署可配置的超时并尊重取消信号,因此这次精简移除的是可配置性而非安全边界。 +预发布阶段的程序化调用方失去了结果来源回显和逐请求的抓取截止时间。提供方仍具备部署级可配置 timeout 并尊重取消信号,因此这次精简移除的是可配置性,而非安全边界。 diff --git a/docs/rfc/implemented/testing/2026-06-11-property-based-testing.i18n.yaml b/docs/rfc/implemented/testing/2026-06-11-property-based-testing.i18n.yaml index 34850593d6..cc2bf1f23f 100644 --- a/docs/rfc/implemented/testing/2026-06-11-property-based-testing.i18n.yaml +++ b/docs/rfc/implemented/testing/2026-06-11-property-based-testing.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-11-property-based-testing.md: 169989746ea5114b1f35e7ebe35a02e8aeb0f782 -2026-06-11-property-based-testing.zh.md: 9e1532c02bb2c46c40577af7275d6aabbb2f9a4f +2026-06-11-property-based-testing.zh.md: 4f2303010f44279c4edd0ec5a509b71f8aad606b diff --git a/docs/rfc/implemented/testing/2026-06-11-property-based-testing.zh.md b/docs/rfc/implemented/testing/2026-06-11-property-based-testing.zh.md index 9e1532c02b..4f2303010f 100644 --- a/docs/rfc/implemented/testing/2026-06-11-property-based-testing.zh.md +++ b/docs/rfc/implemented/testing/2026-06-11-property-based-testing.zh.md @@ -4,26 +4,26 @@ Status: implemented [English](2026-06-11-property-based-testing.md) | 中文 -> 将原始提案与决策记录合并为一篇。首次运行即发现了 BlockAssembler 的重复 `block-end` 真实 bug。 +> 将原始提案与同一主题的决策记录合并为一篇。首次运行即发现了 BlockAssembler 重复 `block-end` 的真实 bug。 ## 问题 -基于示例的测试只能固定我们想到的用例。harness 的核心是协议形态的代码:分片流、事件日志、schema 转换、收件箱调度。这类代码的输入空间是组合爆炸的,有趣的 bug 藏在没人写过示例的交错序列里。佐证:一个 block 组装的排序 bug 曾在 happy path 100% 行覆盖率下存活。逐文件 100% 覆盖率只能证明每行都跑过,不能证明每种交错都正确。 +基于示例的测试只能固定我们想到的用例。harness 的核心是协议形态的代码:分片流、事件日志、schema 转换、收件箱调度。这些场景的输入空间是组合式的,有趣的 bug 藏在没人写过示例的交错序列中。佐证:一个块组装的排序 bug 曾在 happy path 100% 行覆盖率下存活。逐文件 100% 覆盖率证明每一行都跑过了,但不能证明每种交错都是正确的。 ## 决策 -引入 `fast-check`(根 devDependency),在每个协议形态的包中编写一个 `tests/properties.spec.ts`。生成器调优为*逼真但对抗性*的输入(而非均匀噪声),`numRuns` 控制在本地套件总耗时远低于约 10 秒。失败时打印可复现的 seed。(原始提案还草拟了一个夜间 CI job,以 100 倍迭代运行;该部分未交付——属性测试套件仅在常规 `push`/`pull_request` CI 中运行,定时高迭代 job 仍属可能的后续工作。) +引入 `fast-check`(作为根 devDependency),在每个协议形态的包(package)中编写一个 `tests/properties.spec.ts`。生成器调优为*逼真但对抗性*的输入(而非均匀噪声),`numRuns` 控制在本地套件总耗时远低于约 10 秒。失败时打印可复现的 seed。(原始提案还草拟了一个夜间 CI job,以 100 倍迭代运行;该部分未交付。属性测试套件仅在常规的 `push`/`pull_request` CI 中运行,定时高迭代 job 仍属可能的后续工作。) -- **dsh-llm / BlockAssembler:** 任意分片流(合法 + 畸形:重复索引、滞后分片、缺少 block-start)。不变式:`blocks()` 数量 ≤ 出现过的不同索引数;重组幂等(`blocks()` 在重复调用间稳定,且 `message().content` 与之一致);`blocks()` 从不抛异常且只产出合法的 content-block 标签;`finish` 反映最后一个 `finish` 分片,无 `finish` 分片时默认为 `{kind:'stop'}`。 -- **dsh-session:** 任意事件日志。不变式:`deriveMessages` 确定性;从 seed 回放结果一致;seq 严格单调递增;非消息事件不影响派生历史;派生内容与日志解耦。 -- **dsh-tools:** 任意 `SchemaSpec`。不变式:JSON Schema 的 `required` 等于每层 `required:true` 的键集合;转换是全函数;**并且与[运行时参数校验](../architecture/2026-06-11-runtime-arg-validation.md)组合验证**——满足 spec 的生成参数通过 `validateArgs`,定向破坏(删除 required 键、顶层非 object)被拒绝。这封堵了 validator 与 `InferArgs` 漂移的风险。 -- **dsh-agent-loop:** 任意发送调度,对接一个永不耗尽的适配器,通过 `agent/status` settle 信号驱动(无挂钟 sleep)。不变式:无消息丢失;轮次编号严格递增;状态转换始终在合法状态机上。 +- **dsh-llm / BlockAssembler:** 任意分片流(合法 + 畸形:重复索引、滞后分片、缺少 block-start)。不变式:`blocks()` 计数 ≤ 已见到的不同索引数;重组幂等(`blocks()` 在重复调用间稳定,且 `message().content` 与之一致);`blocks()` 从不抛异常且仅产出合法的 content-block 标签;`finish` 反映最后一个 `finish` 分片,无 `finish` 分片时默认为 `{kind:'stop'}`。 +- **dsh-session:** 任意事件日志。不变式:`deriveMessages` 确定性;从 seed 回放结果一致;seq 严格单调递增;非消息事件不影响推导出的历史;推导出的内容与日志解耦。 +- **dsh-tools:** 任意 `SchemaSpec`。不变式:JSON Schema 的 `required` 等于每一层 `required:true` 的键集;转换是全函数;**并且与[运行时参数校验](../architecture/2026-06-11-runtime-arg-validation.md)组合验证**——满足 spec 的生成参数通过 `validateArgs`,而定向破坏(删除必填键、顶层非对象)被拒绝。这封堵了 validator 与 `InferArgs` 漂移的风险。 +- **dsh-agent-loop:** 任意发送调度,对接一个永不耗尽的适配器,通过 `agent/status` settle 信号驱动(无挂钟 sleep)。不变式:无消息丢失;轮次编号严格递增;状态转换保持在合法状态机上。 ## 后果 -- 生成器质量是价值杠杆——生成器偏向小索引池和短字符串,使碰撞与交错频繁出现。 -- **已经产出回报:** BlockAssembler 的流测试发现了一个真实 bug——同一索引的重复 `block-end` 覆写了已刷出的块,导致流式前缀与最终 `blocks()` 不一致。已修复(首次关闭生效,与既有的滞后分片规则一致),并附带专门的回归测试。 -- 属性测试因超时而 flake 是一个发现,不应重试了事。agent loop 的属性测试在设计上是确定性的(通过 `agent/status` settle),因此挂起即为真实缺陷。 -- 属性测试是示例测试的补充而非替代;示例测试固定特定分支,服务于 100% 覆盖率门禁。 +- 生成器质量是价值杠杆——生成器偏向小索引池和短字符串,使碰撞与交错频繁发生。 +- **已经产出回报:** BlockAssembler 流测试发现了一个真实 bug——同一索引的重复 `block-end` 覆盖了已刷出的块,导致流式前缀与最终 `blocks()` 不一致。已修复(首次关闭生效,与既有的滞后分片规则一致),并附带一个专门的回归测试。 +- 属性测试因超时而 flake 是一个发现,不应通过重试消除。循环属性测试在设计上是确定性的(通过 `agent/status` settle),因此挂起即为真实缺陷。 +- 属性测试是对示例测试的补充而非替代;示例测试固定特定分支,服务于 100% 覆盖率门禁。 <!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> diff --git a/docs/rfc/implemented/testing/2026-06-19-acp-snapshot-tests.i18n.yaml b/docs/rfc/implemented/testing/2026-06-19-acp-snapshot-tests.i18n.yaml index 8014cd6b63..1dfb26a628 100644 --- a/docs/rfc/implemented/testing/2026-06-19-acp-snapshot-tests.i18n.yaml +++ b/docs/rfc/implemented/testing/2026-06-19-acp-snapshot-tests.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-19-acp-snapshot-tests.md: 0b93c99932a33bca9945dd88ce45f4a1e100ccfc -2026-06-19-acp-snapshot-tests.zh.md: dc0aeb102039d3261ad6bbbb21321ca2b6ef5ec3 +2026-06-19-acp-snapshot-tests.zh.md: 26c583ba47b8ec10ab3d0e2102a8b791549fda38 diff --git a/docs/rfc/implemented/testing/2026-06-19-acp-snapshot-tests.zh.md b/docs/rfc/implemented/testing/2026-06-19-acp-snapshot-tests.zh.md index dc0aeb1020..26c583ba47 100644 --- a/docs/rfc/implemented/testing/2026-06-19-acp-snapshot-tests.zh.md +++ b/docs/rfc/implemented/testing/2026-06-19-acp-snapshot-tests.zh.md @@ -6,11 +6,11 @@ Status: implemented ## 问题 -单元测试无法覆盖完整的 ACP 子进程 transcript(文本记录),而真实 API 测试既不确定又依赖密钥。因此,面向编辑器的 `session/update` 输出可能在单元覆盖率全绿的情况下发生回归,正如 [default-export 事后分析](../../../postmortem/0001-acp-default-export-drops-inject.md)所展示的那样。 +单元测试无法覆盖完整的 ACP(Agent Client Protocol)子进程 transcript(文本记录),而真实 API 测试既不确定又需要密钥。因此,面向编辑器的 `session/update` 输出可能在单元覆盖率全绿的情况下发生回归,正如 [default-export 事后分析](../../../postmortem/0001-acp-default-export-drops-inject.md)所揭示的那样。 -全 transcript 测试的阻塞点在于模型:agent(智能体)的输出由非确定性的 LLM(大语言模型)驱动,而每次运行都命中真实 API 的密钥门控测试既不确定也无法在 CI 中运行。我们需要真实运行的保真度,同时具备 fixture(测试前置数据)的确定性。 +全 transcript 测试的阻塞因素在于模型:agent 的输出由非确定性的 LLM(大语言模型)驱动,而每次运行都命中真实 API 的密钥门控测试既不确定也无法在 CI 中运行。我们需要真实运行的保真度与 fixture(测试前置数据)的确定性兼得。 -本 RFC 记录了添加第三层测试——**快照测试**——的决策,以及使其确定、CI 中无需密钥且维护成本低的设计选择。 +本 RFC 记录了新增第三层测试——**快照测试**——的决策,以及使其具备确定性、CI 中无需密钥、维护成本低的设计选择。 ## 决策 @@ -18,11 +18,11 @@ Status: implemented ### fixture 即持久化的会话 JSONL -每个场景的 `session.jsonl` 从一次真实运行中采集。`assistant/chunk` 事件重现模型流;工具、消息和边界事件捕获 harness 行为。一份普通的会话产物因此同时充当回放源和行为 golden。 +每个场景的 `session.jsonl` 从一次真实运行中采集。`assistant/chunk` 事件重现模型流;tool、message 和 boundary 事件捕获 harness 行为。一份普通的会话产物因此同时充当回放源和行为 golden。 ### 回放从日志推导模型脚本 -`llm-replay` 短路了提供方无关的 `llm/stream` waterfall(瀑布式事件)。`deriveReplayScript()` 按 `(turn, step)` 对已录制的 chunk 分组,每次模型调用服务一组。循环每步发起一次流调用,因此分组是精确的,且无需特殊处理即可包含 error finish chunk。 +`llm-replay` 短路了提供方无关的 `llm/stream` waterfall(瀑布式事件)。`deriveReplayScript()` 按 `(turn, step)` 对已录制的 chunk 分组,每次模型调用服务一组。agent loop(智能体循环)每个 step 发起一次流调用,因此分组精确对应,错误结束 chunk 也无需特殊处理。 ### 内存中的回放条目遵守完整的 LLM 契约 @@ -34,32 +34,32 @@ Status: implemented | { kind: 'hang' } ``` -日志推导出 chunk 条目。流开始前的抛出和挂起没有可重建的 chunk 表示,因此这些场景提供 `replay.override.json`。throw 条目可以包含前缀 chunk 以表示流中途失败。显式覆盖避免了从有损的 turn-end reason 推断适配器行为。 +日志推导出 chunk 条目。流开始前的抛出和挂起没有可重建的 chunk 表示,因此这些场景提供 `replay.override.json`。throw 条目可以包含前缀 chunk 以模拟流中途失败。显式覆盖避免了从有损的轮次结束原因推断适配器行为。 ### 位置式回放,单个在途流 -回放是位置式的,因此每个场景只允许一个在途模型流。并发会话快照需要按请求键索引的条目。调用顺序变化需要重新录制,fixture 缺失或耗尽时会大声失败。 +回放是位置式的,因此每个场景只允许一个在途模型流。并发会话快照需要按请求键索引的条目。调用顺序变更需要重新录制,fixture 缺失或耗尽时立即报错。 ### 录制采集日志;无密钥回放需要无提供方的配置 -录制使用真实的 `llm-deepseek` 适配器和 JSONL 持久化后端运行场景,然后将产出的 `.jsonl` 复制到场景目录。逐事件追加是持久的,但 harness 在采集前会优雅关闭子进程(关闭 stdin → `await ctx.dispose()`),确保最终事件已刷出。`llm-replay` 本身不做录制——它只负责回放。 +录制使用真实的 `llm-deepseek` 适配器和 JSONL 持久化后端运行场景,然后将产出的 `.jsonl` 复制到场景目录。逐事件追加是持久的,但 harness 在采集前会优雅关闭子进程(关闭 stdin → `await ctx.dispose()`),确保最终事件已刷盘。`llm-replay` 本身不做录制,它只负责回放。 -回放使用 `cordis.snapshot.yml` 覆盖层,将真实适配器替换为 `llm-replay`,同时保留活跃的组合。录制使用普通配置和 harness 提供的持久化根目录。回放模式跳过 `.env` 加载,因此一个意外存在的 API key 不会触发真实调用。见[单一源配置 RFC](2026-07-04-single-source-acp-replay-config.md)。 +回放使用 `cordis.snapshot.yml` 覆盖配置,将真实适配器替换为 `llm-replay`,同时保留活跃的组合。录制使用普通配置和 harness 提供的持久化根目录。回放模式跳过 `.env` 加载,因此一个意外存在的 API key 不会触发真实调用。见[单源配置 RFC](2026-07-04-single-source-acp-replay-config.md)。 ### 两个表面:归一化后比对 快照运行断言**两个**归一化后的表面,因为 harness 的外部表面是不同的: -1. **stdout transcript**——编辑器看到的帧化 `session/update` JSON-RPC。捕获 ACP bridge 的事件→update 转换(`streamSessionEventUpdate`)中的回归。与已提交的 `stdout.golden.jsonl` 比对。 -2. **重新持久化的会话 JSONL**,归一化后与 `session.jsonl` 比对。同一份 fixture 既是回放源也是期望日志。提示词文本被擦除;每个 header 类别一个场景固定可读的 prompt 和工具内容,见 [header-pinning RFC](2026-07-06-pin-request-header-content-in-one-scenario.md)。覆盖场景的模型行为完全来自其伴随记录。 +1. **stdout transcript**——编辑器看到的带帧 `session/update` JSON-RPC。捕获 ACP bridge 事件→update 转换(`streamSessionEventUpdate`)中的回归。与已提交的 `stdout.golden.jsonl` 比对。 +2. **重新持久化的会话 JSONL**,归一化后与 `session.jsonl` 比对。同一份 fixture 既是回放源也是预期日志。提示词文本被擦除;每个 header 类别一个场景固定可读的 prompt 和 tool 内容,见 [header-pinning RFC](2026-07-06-pin-request-header-content-in-one-scenario.md)。覆盖场景的模型行为完全来自其伴随文件。 -两个表面互补:stdout 覆盖 bridge 投影,JSONL 覆盖投影所省略的循环、工具和边界结构。 +两个表面互补:stdout 覆盖 bridge 投影,JSONL 覆盖投影所省略的 loop、tool 和 boundary 结构。 -归一化替换会话 ID、cwd、protocol-id、时间戳、路径和进程易变值,同时保留确定性序列号。场景将真实 bash 使用限制在稳定命令上。stdout golden 保持协议格式(wire format)的 JSONL,每行原始数据必须能解析为 JSON。Vitest 只更新 stdout golden;归一化后的会话相等性检查从不覆写回放 fixture。 +归一化替换 session、cwd、protocol-id、时间戳、路径和进程相关的易变值,同时保留确定性序列号。场景将真实 bash 使用限制在稳定命令范围内。stdout golden 保持协议格式(wire format)的 JSONL,每一行原始数据必须可解析为 JSON。Vitest 只更新 stdout golden;归一化后的会话相等性检查从不覆盖回放 fixture。 -### 隔离:当前靠归一化,后续可沙箱 +### 隔离:当前靠归一化,后续可加沙箱 -工具确定性来自临时 cwd、擦除的环境变量、全新的非登录 shell、受限命令和归一化。它不声称具备 OS 级隔离。如果需要更强的层级,沙箱执行器可以通过既有的[能力 seam](../architecture/2026-06-13-capability-seams.md) 替换本地后端。 +工具的确定性来自临时 cwd、擦除的环境变量、全新的非登录 shell、受限命令和归一化。它不声称具备操作系统级隔离。如果需要更强的隔离层级,可通过既有的[能力 seam](../architecture/2026-06-13-capability-seams.md) 将沙箱执行器替换本地后端。 ### 回放插件是独立的包 @@ -67,16 +67,16 @@ Status: implemented ### 两个子命令,回放在默认门禁中 -`pnpm run test:snapshot` 无密钥回放已提交的 fixture;`test:snapshot:record` 使用真实 API 并重写采集到的会话日志和 stdout golden。fixture 缺失时大声失败。每个场景携带 `input.json`、`stdout.golden.jsonl` 和 `session.jsonl`;无模型场景使用仅含 header 的日志。`replay.override.json` 仅在标记为 `overridden` 的场景中必需,因为它的存在会替换推导出的回放。fixture 守卫拒绝缺失、不匹配和遗留的文件。两个命令都接受场景过滤器。 +`pnpm run test:snapshot` 无需密钥地回放已提交的 fixture;`test:snapshot:record` 使用真实 API 并重写采集到的会话日志和 stdout golden。fixture 缺失时立即报错。每个场景携带 `input.json`、`stdout.golden.jsonl` 和 `session.jsonl`;无模型场景使用仅含 header 的日志。`replay.override.json` 仅在标记为 `overridden` 的场景中必需,因为它的存在会替换推导出的回放。fixture 守卫拒绝缺失、不匹配和遗留的文件。两个命令均接受场景过滤器。 ## 曾考虑的替代方案 -- **手写的模型 chunk `llm.json`**:早期草案;复用真实会话日志使 fixture 成为系统的真实产物而非手工构建的 mock,并兼作行为 golden。 -- **字节级 HTTP 录制库(Polly/nock/MSW)**:否决。适配器相关、与流式 SSE 配合笨拙,且层级低于被测对象。 -- **从 `turn/end {kind:'error'|'aborted'}` 合成 throw/cancel 条目**:否决。这会将 `llm-replay` 耦合到循环内部的 turn 关闭语义,且 `turn/end` reason 是有损的(无法区分抛出的 401 和 finish-error);显式的 `replay.override.json` 伴随记录是更干净的 seam。 +- **手工编写的模型 chunk `llm.json`**:早期草案的做法。复用真实会话日志使 fixture 成为系统的真实产物而非手工构建的 mock,并兼作行为 golden。 +- **字节级 HTTP 录制库(Polly/nock/MSW)**:否决。与适配器耦合,处理流式 SSE(Server-Sent Events)时笨拙,且层级低于被测对象。 +- **从 `turn/end {kind:'error'|'aborted'}` 合成 throw/cancel 条目**:否决。这会将 `llm-replay` 耦合到 loop 内部的轮次关闭语义,且 `turn/end` 原因是有损的(无法区分抛出的 401 与 finish-error);显式的 `replay.override.json` 伴随文件是更清晰的 seam。 ## 后果 -新层级为每个场景添加经评审的 input、session、stdout、可选 override 和可选 workspace fixture。workspace 种子在录制和回放时都被复制到临时 cwd。作为回报,该层级通过真实的 Loader 和工具组合提供确定性的无密钥 transcript 覆盖。子进程、input、workspace、归一化和回放 harness 可以支持 ACP 以外的示例。 +新测试层为每个场景增加了经评审的 input、session、stdout、可选 override 和可选 workspace fixture。workspace 种子在录制和回放时都会被复制到临时 cwd。作为回报,该层通过真实的 Loader 和 tool 组合提供确定性的无密钥 transcript 覆盖。子进程、input、workspace、归一化和回放 harness 可以支持 ACP 之外的示例。 -本 RFC 与[拟议的确定性 RFC](../../proposed/testing/2026-06-11-deterministic-and-stress-testing.md) 相关但不取代它:该提案的「通用回放 fixture」在每次测试后重新推导会话*消息历史*(一个内部一致性不变式),而快照测试固定的是*外部协议输出*。二者互补:一个守护事件溯源不变式,另一个守护面向编辑器的契约。 +本 RFC 与[拟议的确定性 RFC](../../proposed/testing/2026-06-11-deterministic-and-stress-testing.md) 相关但不取代它:该提案的「通用回放 fixture」在每次测试后重新推导会话的*消息历史*(一项内部一致性不变式),而快照测试固定的是*外部协议输出*。二者互补:一个守护事件溯源不变式,另一个守护面向编辑器的契约。 diff --git a/docs/rfc/implemented/testing/2026-06-19-real-api-e2e-ci.i18n.yaml b/docs/rfc/implemented/testing/2026-06-19-real-api-e2e-ci.i18n.yaml index fadd3e4a52..591f51750a 100644 --- a/docs/rfc/implemented/testing/2026-06-19-real-api-e2e-ci.i18n.yaml +++ b/docs/rfc/implemented/testing/2026-06-19-real-api-e2e-ci.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-19-real-api-e2e-ci.md: 3b5995a3e060ef9b4b1639b5c7fb17c819e73150 -2026-06-19-real-api-e2e-ci.zh.md: 4d4b38cd5989426482b773da79b3a44cec90f747 +2026-06-19-real-api-e2e-ci.zh.md: 78bab7c1e00125bfbe11b8f08eeeff3f2b7723b1 diff --git a/docs/rfc/implemented/testing/2026-06-19-real-api-e2e-ci.zh.md b/docs/rfc/implemented/testing/2026-06-19-real-api-e2e-ci.zh.md index 4d4b38cd59..78bab7c1e0 100644 --- a/docs/rfc/implemented/testing/2026-06-19-real-api-e2e-ci.zh.md +++ b/docs/rfc/implemented/testing/2026-06-19-real-api-e2e-ci.zh.md @@ -1,100 +1,100 @@ # RFC:在 CI 中对外部 DeepSeek API 运行真实 API e2e 测试 -Status: implemented - [English](2026-06-19-real-api-e2e-ci.md) | 中文 +Status: implemented + ## 问题 -按照既定策略,harness 高度依赖真实 API 测试:[docs/testing.md](../../../testing.md) 论证了无密钥测试套件只能验证管道连通性而非产品行为,[ACP inject 事后分析](../../../postmortem/0001-acp-default-export-drops-inject.md)是现成的证据——178 个无密钥测试全绿,而真实编辑器会话一启动就崩溃。真实 API e2e 套件(`pnpm run test:e2e`,即 `*.e2e.ts` 文件)正是为了弥合这一缺口:它驱动 agent 对接线上 DeepSeek API——真实模型调用、真实 bash 工具、多轮对话、恢复、ACP-over-stdio。 +按照策略,harness 高度依赖真实 API 测试:[docs/testing.md](../../../testing.md) 论证了无密钥套件只能验证管道连通性而非产品本身,[ACP inject 事后分析](../../../postmortem/0001-acp-default-export-drops-inject.md)是现成的证据——178 个无密钥测试全绿,而真实编辑器会话一启动就崩溃。真实 API e2e 套件(`pnpm run test:e2e`,即 `*.e2e.ts` 文件)正是为弥合这一差距而存在的:它驱动 agent(智能体)对接实时 DeepSeek API——真实模型调用、真实 bash 工具、多轮次对话、恢复、ACP-over-stdio。 -默认门禁([.github/workflows/ci.yml](../../../../.github/workflows/ci.yml))刻意不携带密钥:它不含 secret,可供 fork 运行。`test:e2e` 在无密钥时自动跳过(`describe.skipIf(!process.env.DEEPSEEK_API_KEY)`),因此把它加到 ci.yml 只会报绿而不会真正执行真实套件。要让真实 API 覆盖率成为合并信号,需要一个独立的、携带 secret 的工作流。 +默认门禁([.github/workflows/ci.yml](../../../../.github/workflows/ci.yml))刻意无密钥:不携带 secret,可供 fork 运行。`test:e2e` 在无密钥时自动跳过(`describe.skipIf(!process.env.DEEPSEEK_API_KEY)`),因此将其加入该工作流只会报绿而不会真正执行真实套件。要让真实 API 覆盖率成为合并信号,需要一个独立的、携带 secret 的工作流。 -本 RFC 记录的决策是:新增一个**第二个、消费 secret 的工作流**来在 CI 中运行真实 API 套件。同时,由于这是向一个未来可能公开的仓库引入首个 CI secret,属于安全/隔离决策,本文一并记录其依赖的威胁模型以及仓库公开后会发生什么变化。 +本 RFC 记录的决策是:添加一个**第二个、消费 secret 的工作流**来在 CI 中运行真实 API 套件。由于这是向一个未来可能公开的仓库引入首个 CI secret,属于安全/隔离决策,本文同时记录其依赖的威胁模型以及仓库公开后的变化。 ## 决策 -新增专用工作流 [.github/workflows/e2e.yml](../../../../.github/workflows/e2e.yml),与 ci.yml 分离。它仅在受信事件上使用仓库 secret 对外部 API 运行 `pnpm run test:e2e`,并设有预检步骤:secret 缺失时以显式失败替代假绿。无密钥工作流保持独立,使可 fork 的质量门禁与消费 secret 的真实 API 门禁各自拥有不同的触发和凭证策略。 +添加一个专用工作流 [.github/workflows/e2e.yml](../../../../.github/workflows/e2e.yml),与 ci.yml 分离。它仅使用 repo secret 对外部 API 运行 `pnpm run test:e2e`,仅在可信事件上触发,并带有一个 preflight 检查:将缺失的 secret 转化为明确的失败而非虚假的绿色。无密钥工作流保持独立,使可 fork 的质量门禁与消费 secret 的真实 API 门禁各自拥有不同的触发和凭证策略。 ### 独立工作流,而非 ci.yml 中的一个 job -ci.yml 的价值在于它无密钥、可 fork、始终绿色:任何贡献者(包括外部 fork)都能获得完整的无密钥信号,secret 不在其爆炸半径内。在那里添加消费 secret 的 job 会将这个始终绿色的门禁耦合到凭证可用性和不同的触发策略上。将携带 secret 的工作放在独立文件中,隔离了 secret、触发和并发策略,并为 fork 保留了 ci.yml 的特性。不同的生命周期 → 不同的文件。 +ci.yml 的价值在于它无密钥、可 fork、始终为绿:任何贡献者(包括外部 fork)都能获得完整的无密钥信号,secret 不在爆炸半径内。在其中添加消费 secret 的 job 会将这个始终为绿的门禁耦合到凭证可用性和不同的触发策略上。将携带 secret 的工作放在独立文件中,隔离了 secret、触发和并发策略,并为 fork 保留了 ci.yml 的特性。不同的生命周期→不同的文件。 -### 成本不是约束,可靠性才是 +### 约束不是成本,而是可靠性 -内部推理成本不是限制因素,因此工作流以覆盖率和信号为优化目标。它在多种触发条件和每个受信 PR 上运行所有匹配的 `*.e2e.ts` 文件,落实 [docs/testing.md](../../../testing.md) 的 with-key 策略。 +内部推理(inference)成本不是限制因素,因此工作流以覆盖率和信号为优化目标。它在多个触发条件和每个可信 PR(Pull Request)上运行所有匹配的 `*.e2e.ts` 文件,落实 [docs/testing.md](../../../testing.md) 的有密钥策略。 -### 触发条件:仅受信事件 +### 触发条件:仅限可信事件 -`workflow_dispatch` + `push` 到 `main`/`master` + 每日定时 `schedule`(`17 0 * * *`,即北京时间 08:17)+ `pull_request`。push 提供合并后信号;schedule 捕获外部 API 漂移;dispatch 是手动逃生口;受信 pull request 获得合并前门禁。该合并前信号有意接受 § 安全性 中描述的更大密钥暴露面。 +`workflow_dispatch` + `push` 到 `main`/`master` + 每夜 `schedule`(`17 0 * * *`,即北京时间 08:17)+ `pull_request`。push 提供合并后信号;schedule 捕捉外部 API 漂移;dispatch 是手动逃生通道;可信 pull request 获得合并前门禁。该合并前信号有意接受 § 安全性中描述的更大密钥暴露面。 -### 不受信 PR 的门禁 +### 不可信 PR 的门禁 -GitHub 对两类 PR 隐藏仓库 secret:来自 **fork** 的 PR,以及 **Dependabot** PR(同仓库分支,因此 `head.repo.fork == false`,但 secret 仍被隐藏)。job 级 `if:` 对两者都跳过整个 job: +GitHub 对两类 PR 扣留 repo secret:来自 **fork** 的 PR,以及 **Dependabot** PR(同仓库分支,`head.repo.fork == false`,但 secret 仍被扣留)。一个 job 级 `if:` 对两者都跳过整个 job: ``` github.event_name != 'pull_request' || !(github.event.pull_request.head.repo.fork || github.event.pull_request.user.login == 'dependabot[bot]') ``` -Dependabot 子句基于 PR **作者**(`pull_request.user.login`)而非 `github.actor`(运行触发者):维护者重新打开或重跑 Dependabot PR 时,`github.actor` 会变成人类,但 PR 仍然无密钥;基于作者的判断在这种情况下依然正确。被 **job 级** `if:` 跳过的 job 报告为*成功*检查(不同于工作流/触发级跳过,后者保持 pending),因此如果需要,可以安全地将此工作流标记为 required status check——fork/Dependabot PR 的跳过但绿色的检查不会阻塞合并。 +Dependabot 子句基于 PR **作者**(`pull_request.user.login`)而非 `github.actor`(运行触发者):维护者重新打开或重跑 Dependabot PR 时,`github.actor` 会变成人类,但该 PR 仍然无密钥;基于作者的判断在这种情况下依然正确。被 **job 级** `if:` 跳过的 job 报告为*成功*检查(不同于工作流/触发级跳过会保持 pending),因此如果需要将此工作流标记为 required status check 也是安全的——fork/Dependabot PR 的跳过但绿色的检查不会阻塞合并。 -该门禁是一个*干净跳过的便利措施*,而非 secret 的安全边界(见 § 安全性——边界是 GitHub 自身在 `pull_request` 下对 fork 的 secret 隐藏机制)。没有这个门禁,fork 仍然无法读取密钥;它们只会遇到一个令人困惑的预检硬失败并浪费计算资源。 +该门禁是一个*干净跳过的便利措施*,而非 secret 的安全边界(见 § 安全性——边界是 GitHub 自身在 `pull_request` 下对 fork 的 secret 扣留机制)。没有该门禁,fork 仍然无法读取密钥;只是会遇到令人困惑的 preflight 硬失败并浪费计算资源。 -### 预检:大声失败,绝不假绿 +### Preflight:大声失败,绝不虚假为绿 -由于 job 仅在 secret 预期存在的受信事件上运行,预检是无条件的存在性检查:密钥为空 → `exit 1` 并附带 `::error::` 注解指明需要配置的 secret 名称。这是让自跳过套件可以安全用作门禁的关键。没有它,被删除/重命名/配置错误的 secret 会让 `test:e2e` 跳过所有真实套件并报告全绿——整个安全网的静默退化。这个守卫将「secret 缺失」从不可见的假通过变为可见的失败。(其正确性已在实际中验证:secret 存在之前的运行恰好在此步骤失败。) +由于 job 仅在 secret 应当存在的可信事件上运行,preflight 是一个无条件的存在性检查:密钥为空→`exit 1` 并附带 `::error::` 注解指明需要配置的 secret 名称。这是让自跳过套件可以安全地作为门禁的关键。没有它,被删除/重命名/错误配置的 secret 会让 `test:e2e` 跳过所有真实套件并报告全绿——整个安全网的静默退化。该守卫将「secret 缺失」从不可见的虚假通过转化为可见的失败。(其正确性已在实际中验证:secret 存在之前的运行恰好在此步骤失败。) ### Secret 映射与卫生 -仓库 secret 命名为 `DEEPSEEK_API_KEY_EXTERNAL`;它被映射到适配器和测试读取的 `DEEPSEEK_API_KEY` 环境变量(`process.env.DEEPSEEK_API_KEY`)。独立的 secret 名称记录了意图(这是*外部*公开 API 密钥,不是内部端点密钥),并允许内部端点密钥日后无冲突地共存。以下卫生选择均为防御性设计: +repo secret 命名为 `DEEPSEEK_API_KEY_EXTERNAL`;映射到适配器和测试读取的 `DEEPSEEK_API_KEY` 环境变量(`process.env.DEEPSEEK_API_KEY`)。独立的 secret 名称记录了意图(这是*外部*公开 API 密钥,不是内部端点密钥),并允许内部端点密钥日后无冲突地共存。以下卫生选择均为防御性设计: -- **步骤级 secret。** `DEEPSEEK_API_KEY` 仅在预检和 e2e 步骤的 `env:` 中设置,绝不在 job 级设置——因此 checkout/setup-node/install 永远看不到它。依赖中被入侵的安装时生命周期脚本无法读取不在其环境中的 secret。 -- **`permissions: contents: read`。** 该 job 仅读取仓库以运行测试;不需要写权限(不写 PR 评论、不写 status),因此 `GITHUB_TOKEN` 降至最小权限。 -- **`DEEPSEEK_BASE_URL` 固定**为 e2e 步骤上的 `https://api.deepseek.com`。适配器在未设置时会默认使用此值([packages/llm/llm-deepseek/src/index.ts](../../../../packages/llm/llm-deepseek/src/index.ts) 中的 `PUBLIC_BASE_URL`),但显式固定具有自文档化和密封性——一个意外的仓库根目录 `.env`(`vitest.e2e.config.ts` 如果存在会加载它)无法静默地将运行重定向到其他端点。 -- **不回显 secret。** 预检仅打印 `DEEPSEEK_API_KEY present.`——不打印值或长度。 +- **Step 级 secret。** `DEEPSEEK_API_KEY` 仅在 preflight 和 e2e 步骤的 `env:` 中设置,从不在 job 级设置——因此 checkout/setup-node/install 永远看不到它。依赖中被入侵的安装时生命周期脚本无法读取不在其环境中的 secret。 +- **`permissions: contents: read`。** job 仅读取仓库以运行测试;不需要写权限(无 PR 评论、无 status 写入),因此 `GITHUB_TOKEN` 降至最小权限。 +- **`DEEPSEEK_BASE_URL` 固定**为 e2e 步骤上的 `https://api.deepseek.com`。适配器在未设置时会默认使用此值([packages/llm/llm-deepseek/src/index.ts](../../../../packages/llm/llm-deepseek/src/index.ts) `PUBLIC_BASE_URL`),但显式固定具有自文档性和密封性——仓库根目录的 `.env`(`vitest.e2e.config.ts` 存在时会加载)无法静默地将运行重定向到其他端点。 +- **不回显 secret。** preflight 仅打印 `DEEPSEEK_API_KEY present.`——不打印值或长度。 ### 范围与运行时形态 -该 job 仅在 Node 24 上运行 `test:e2e`;无密钥门禁和版本兼容性属于主 CI 工作流。测试通过 workspace paths 映射以未构建形式运行,使用有界可配置的 worker 池、逐测试重试和 job 超时。被取代的 PR 运行会被取消,而 push 和定时运行完整执行以提供合并后信号。 +job 仅在 Node 24 上运行 `test:e2e`;无密钥门禁和版本兼容性属于主 CI 工作流。测试通过 workspace paths 映射以未构建形式运行,使用有界的可配置 worker 池、逐测试重试和 job 超时。被取代的 PR 运行会被取消,而 push 和 schedule 运行完整执行以提供合并后信号。 ## 安全性 -仓库的首个 CI secret 需要一份记录在案的威胁模型,因为同仓库 PR、fork PR 和 Dependabot PR 之间的访问权限不同,且仓库公开后会发生变化。 +仓库的首个 CI secret 需要一份记录在案的威胁模型,因为同仓库 PR、fork PR 和 Dependabot PR 的访问权限各不相同,且仓库公开后会发生变化。 -### 今天谁能触及 secret(私有仓库) +### 当前谁能触及 secret(私有仓库) -- **无写权限(fork PR):不能。** 两个独立事实阻止了它。第一,工作流使用 `pull_request` 而**非** `pull_request_target`——GitHub 不会将仓库 secret 传递给 fork PR 的 `pull_request` 运行,因此 `secrets.DEEPSEEK_API_KEY_EXTERNAL` 在 fork runner 上解析为空。第二,`if:` 门禁完全跳过 fork PR。secret 隐藏机制是真正的边界;门禁是纵深防御和用户体验。 -- **写(push)权限:能。** 同仓库分支 PR 会收到 secret,因此有写权限的作者可以修改测试代码(或安装生命周期脚本,或其分支上的工作流 YAML)来窃取密钥。这是 **GitHub Actions 固有的,并非本文引入的**:任何对任何仓库有 push 权限的人都可以通过编写工作流来窃取该仓库的任何 Actions secret。写权限 ⇒ secret 访问权,始终如此。缓解措施在于谁被授予写权限以及分支保护,而非本文件。 +- **无写权限(fork PR):不能。** 两个独立事实阻止了它。第一,工作流使用 `pull_request` 而**非** `pull_request_target`——GitHub 不会将 repo secret 传递给 fork PR 的 `pull_request` 运行,因此 `secrets.DEEPSEEK_API_KEY_EXTERNAL` 在 fork runner 上解析为空。第二,`if:` 门禁完全跳过 fork PR。secret 扣留是真正的边界;门禁是纵深防御和用户体验。 +- **有写(push)权限:能。** 同仓库分支 PR 会收到 secret,因此有写权限的作者可以修改测试代码(或安装生命周期脚本,或其分支上的工作流 YAML)来窃取密钥。这**是 GitHub Actions 的固有特性,并非本文引入的**:任何对任何仓库有 push 权限的人都可以通过编写工作流来窃取该仓库的任何 Actions secret。写权限⇒secret 访问权,始终如此。缓解措施在于谁被授予写权限以及分支保护,而非本文件。 -因此「任何能开 PR 的人都能窃取它」是错误的:只有写权限集合内的人能,而该集合本来就能窃取仓库持有的任何 secret。 +因此「任何能开 PR 的人都能窃取它」是错误的:只有写权限集合内的人能,而这些人本来就能窃取仓库持有的任何 secret。 ### `pull_request` 触发器增加的残余暴露面 -由于启用了 PR 运行,密钥会在合并前被交给**写权限作者 PR 分支上的代码**。这比 `push` + `schedule` + `workflow_dispatch` 的暴露面更大,为了在受信写权限集合内获得合并前信号而被接受。如果这一权衡发生变化,可以去掉 `pull_request` 触发器,同时保留合并后、每夜和按需覆盖。 +由于启用了 PR 运行,密钥会在合并前被交给**写权限作者 PR 分支上的代码**。这比 `push` + `schedule` + `workflow_dispatch` 的暴露面更大,为在可信写权限集合内获得合并前信号而接受。如果这一权衡发生变化,可移除 `pull_request` 触发器,同时保留合并后、每夜和按需覆盖。 -### 仓库公开后会发生什么变化 +### 仓库公开后的变化 -**通过本工作流**,secret 对公众仍然受保护:`pull_request` 在公开仓库上行为一致——fork PR(现在任何人都能开)仍然收不到 secret,且在公开仓库上 GitHub 额外要求维护者批准 fork PR 运行,即使批准后运行也不会获得 secret(批准运行不等于交出密钥)。写权限集合不因可见性改变,因此内部人员的现实也不变。 +**通过本工作流**,secret 对公众仍然受保护:`pull_request` 在公开仓库上行为一致——fork PR(现在任何人都能开)仍然收不到 secret,且在公开仓库上 GitHub 额外要求维护者批准 fork PR 运行,即使批准后运行也不会获得 secret(批准运行不等于交出密钥)。写权限集合不因可见性改变而改变,因此内部人员的现实也不变。 变差的是*周边*模型,以下是翻转可见性之前需要处理的事项: -- **日志变为全球可读。** 今天泄露给组织成员的粗心 secret 回显,在公开后会泄露给整个互联网并在几分钟内被爬取。secret 处理纪律(不回显值/长度——已完成)的重要性大幅提升。 -- **`pull_request_target` 陷阱变为灾难性的。** 如果有人为了「修复」PR 运行而将触发器切换为 `pull_request_target`,工作流将在 base 仓库上下文中运行不受信的 fork 代码**并携带** secret——完整的密钥泄露向量。这在私有仓库上尚可容忍,在公开仓库上则是灾难。e2e.yml 中触发器上的 `SECURITY —` 注释禁止此更改并指向本文。 -- **翻转时轮换密钥。** 该密钥曾存在于私有仓库的 CI 中;将公开视为「假设已暴露」,在那一刻轮换 `DEEPSEEK_API_KEY_EXTERNAL`。 -- **将 secret 置于控制之下。** 确认 Settings → Actions → *"Send secrets to workflows from fork pull requests"* 保持**关闭**(这是唯一能真正打破 fork 边界的设置),并考虑将密钥移入带有 required reviewers 的 GitHub **Environment**,使即使已合并的代码也只在受控条件下使用它,且轮换有一个统一的归属。 +- **日志变为全球可读。** 今天泄露给组织成员的粗心 secret 回显,公开后会泄露给整个互联网并在数分钟内被爬取。secret 处理纪律(不回显值/长度——已做到)的重要性大幅提升。 +- **`pull_request_target` 陷阱变为灾难性的。** 如果有人为了「修复」PR 运行而将触发器切换为 `pull_request_target`,工作流将在 base-repo 上下文中运行不可信的 fork 代码并**携带** secret——完整的密钥泄露向量。在私有仓库中这勉强无害,在公开仓库中则是灾难。e2e.yml 中触发器上的 `SECURITY —` 注释禁止此更改并指向本文。 +- **翻转时轮换密钥。** 密钥曾存在于私有仓库的 CI 中;将公开视为「假定已暴露」,在那一刻轮换 `DEEPSEEK_API_KEY_EXTERNAL`。 +- **将 secret 置于控制之下。** 确认 Settings → Actions → *"Send secrets to workflows from fork pull requests"* 保持**关闭**(这是唯一真正会打破 fork 边界的设置),并考虑将密钥移入带有 required reviewers 的 GitHub **Environment**,使即使已合并的代码也只在受控条件下使用它,且轮换有单一归属。 -以上均不需要修改工作流即可公开仓库;它们是运维步骤加上已添加的 `pull_request_target` 守卫注释。 +以上均不需要修改工作流即可公开;它们是运维步骤加上已添加的 `pull_request_target` 守卫注释。 ## 曾考虑的替代方案 -- **在 ci.yml 中添加消费 secret 的 job**:否决。它会将无密钥、可 fork、始终绿色的门禁耦合到凭证可用性和不同的触发/并发策略上;不同的生命周期,不同的文件。 -- **省略 `pull_request` 触发器**(更小的密钥暴露面):为了合并前信号而否决;安全性一节承载了被接受的暴露分析。 +- **在 ci.yml 中添加消费 secret 的 job**:否决。会将无密钥、可 fork、始终为绿的门禁耦合到凭证可用性和不同的触发/并发策略上;不同的生命周期,不同的文件。 +- **省略 `pull_request` 触发器**(更小的密钥暴露面):为获得合并前信号而否决;安全性章节承载了已接受的暴露分析。 ## 后果 -新增一个 CI 工作流和仓库首个需要维护的 secret。真实 API 套件现在成为合并门禁(受信 PR 上的合并前门禁、main 分支上的合并后门禁)并每夜运行,因此 agent 与外部 API 交互中的真实故障会在 CI 中浮现,而非仅在开发者的本地运行中出现——代价是每个受信 PR 和合并都会产生真实(但内部免费)的 API 调用。预检使 secret 配置错误变为自我通告而非静默禁用安全网。 +新增一个 CI 工作流和仓库的首个需要维护的 secret。真实 API 套件现在作为合并门禁(可信 PR 上的合并前门禁、主分支上的合并后门禁)并每夜运行,因此 agent 与外部 API 交互中的真实故障会在 CI 中浮现,而非仅在开发者的本地运行中出现——代价是每个可信 PR 和合并都会产生真实的(但内部免费的)API 调用。preflight 使 secret 配置错误变为自我通告而非静默禁用安全网。 -本设计携带一个记录在案的约束面:`pull_request` 触发器的密钥暴露权衡(去掉它以加固)、`if:` 门禁对基于作者的 Dependabot 判断的依赖,以及对 `pull_request_target` 的硬性禁止。上述公开清单是运维伴侣——本 RFC 是未来维护者在更改触发器集合或翻转仓库可见性之前应重新阅读的地方,而非从头重新推导 fork/secret 模型。 +本设计携带一个记录在案的约束面:`pull_request` 触发器的密钥暴露权衡(移除以加固)、`if:` 门禁对基于作者的 Dependabot 判断的依赖,以及对 `pull_request_target` 的硬性禁止。上述公开清单是运维伴侣——本 RFC 是未来维护者在更改触发器集合或翻转仓库可见性之前应重读的地方,而非从头重新推导 fork/secret 模型。 -定时触发器在仓库不活跃 60 天后会自动禁用(GitHub 行为);push/PR/dispatch 是后备,活跃的 monorepo 不会触及此限制。假设 runner 可出站访问 `https://api.deepseek.com`——GitHub 托管的 `ubuntu-latest` 具备此条件;出站受限的自托管 runner 需要在依赖每夜运行之前确认连通性。 +schedule 触发器在仓库不活跃 60 天后会自动禁用(GitHub 行为);push/PR/dispatch 是后备,活跃的 monorepo 不会触及此限制。假设 runner 对 `https://api.deepseek.com` 有出站连通性——GitHub 托管的 `ubuntu-latest` 具备此条件;受出站限制的自托管 runner 需要在依赖每夜运行之前确认连通性。 diff --git a/docs/rfc/implemented/testing/2026-06-20-remove-redundant-snapshot-log-goldens.i18n.yaml b/docs/rfc/implemented/testing/2026-06-20-remove-redundant-snapshot-log-goldens.i18n.yaml index 592c10b7e0..6e63ef2e55 100644 --- a/docs/rfc/implemented/testing/2026-06-20-remove-redundant-snapshot-log-goldens.i18n.yaml +++ b/docs/rfc/implemented/testing/2026-06-20-remove-redundant-snapshot-log-goldens.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-20-remove-redundant-snapshot-log-goldens.md: badd32d4479ac6d44bb7be3cd262cba57b1b3a38 -2026-06-20-remove-redundant-snapshot-log-goldens.zh.md: 35e4a698cbd19bcceb97df714d2b6bfb371c155a +2026-06-20-remove-redundant-snapshot-log-goldens.zh.md: b791fbcc971eb1340f0d43ef6a60ebb29e4711f9 diff --git a/docs/rfc/implemented/testing/2026-06-20-remove-redundant-snapshot-log-goldens.zh.md b/docs/rfc/implemented/testing/2026-06-20-remove-redundant-snapshot-log-goldens.zh.md index 35e4a698cb..b791fbcc97 100644 --- a/docs/rfc/implemented/testing/2026-06-20-remove-redundant-snapshot-log-goldens.zh.md +++ b/docs/rfc/implemented/testing/2026-06-20-remove-redundant-snapshot-log-goldens.zh.md @@ -6,32 +6,32 @@ Status: implemented ## 问题 -模型驱动的 ACP 快照场景同时包含 `session.jsonl` 和 `session.golden.jsonl`。对于普通录制场景,`session.jsonl` 是从真实运行中采集的回放 fixture(测试前置数据),回放测试将新持久化的日志归一化后与 `session.golden.jsonl` 比较。在当前 fixture 中,普通录制场景的归一化录制日志与归一化 golden 完全相同。 +模型驱动的 ACP(Agent Client Protocol)快照场景同时包含 `session.jsonl` 和 `session.golden.jsonl`。对于普通录制场景,`session.jsonl` 是从真实运行中采集的回放 fixture(测试前置数据),回放测试对新持久化的日志做归一化后与 `session.golden.jsonl` 比较。在当前 fixture 中,普通录制场景的归一化录制日志与归一化 golden 完全一致。 -手工编写的覆盖场景(`error-finish`、`cancel`)目前使用 `replay.override.json` 驱动模型行为,并保留 `session.jsonl` 作为最小占位 fixture,而 `session.golden.jsonl` 存放预期的持久化日志。覆盖文件是一个 `ReplayEntry` 对象的 JSON 数组:`{ "kind": "chunks", "chunks": StreamChunk[] }`、`{ "kind": "throw", "chunks": StreamChunk[], "message": string, "code": string, "status"?: number }` 或 `{ "kind": "hang" }`。这种拆分同样没有必要:当覆盖 sidecar 存在时,`llm-replay` 会替换派生的脚本,不需要从 `session.jsonl` 获取模型分片,因此 `session.jsonl` 仍然可以充当该场景的预期会话日志产物。 +手工编写的覆盖场景(`error-finish`、`cancel`)目前使用 `replay.override.json` 驱动模型行为,并保留 `session.jsonl` 作为最小占位 fixture,而 `session.golden.jsonl` 存放预期的持久化日志。覆盖文件是一个 `ReplayEntry` 对象的 JSON 数组:`{ "kind": "chunks", "chunks": StreamChunk[] }`、`{ "kind": "throw", "chunks": StreamChunk[], "message": string, "code": string, "status"?: number }` 或 `{ "kind": "hang" }`。这种拆分同样是多余的:当覆盖 sidecar 存在时,`llm-replay` 会替换派生脚本,不需要从 `session.jsonl` 获取模型分片,因此 `session.jsonl` 仍可作为该场景的预期会话日志产物。 ## 决策 -彻底移除 `session.golden.jsonl` 概念。每个场景最多只有一个提交的会话日志产物 `session.jsonl`: +彻底移除 `session.golden.jsonl` 概念。每个场景最多只有一个提交到仓库的会话日志产物,即 `session.jsonl`: -- 对于录制场景,`session.jsonl` 仍是原始采集的日志。回放仍从中派生模型分片,快照测试将回放运行的归一化持久化日志与归一化后的 `session.jsonl` 比较。 -- 对于手工编写的覆盖场景,`replay.override.json` 驱动模型行为,`session.jsonl` 存放预期产出的会话日志。当覆盖文件存在时回放适配器会忽略 fixture 中的模型分片,因此同一个文件既可作为预期日志,又不影响回放行为。 -- 对于无模型场景,`session.jsonl` 可以保留为启动 `llm-replay` 所需的最小 fixture;除非该场景创建了持久化会话,否则无需进行会话日志比较。 +- 对于录制场景,`session.jsonl` 仍是原始采集的日志。回放仍从中派生模型分片,快照测试将回放运行归一化后的持久化日志与归一化后的 `session.jsonl` 进行比较。 +- 对于手工编写的覆盖场景,`replay.override.json` 驱动模型行为,`session.jsonl` 存放预期产出的会话日志。当覆盖文件存在时,回放适配器不从 fixture 获取模型分片,因此同一个文件既可作为预期日志,又不影响回放行为。 +- 对于无模型场景,`session.jsonl` 可保留为引导 `llm-replay` 所需的最小 fixture;除非场景创建了持久化会话,否则无需进行会话日志比较。 -stdout golden 保持不变;它们是面向编辑器的投影,与会话 fixture 并不冗余。 +stdout golden 保持不变;它们是面向编辑器的投影,与会话 fixture 不构成冗余。 ## 曾考虑的替代方案 -**基于共享(回放运行)上下文对两侧进行归一化**:否决。`normalizeSessionLog` 通过精确字符串匹配擦除 cwd,因此 fixture 中录制的 cwd 不会被擦除,每次比较都会失败。两侧各自基于自身 header 派生的上下文进行归一化——下方的实现说明描述了具体机制。 +**对两侧基于共享的(回放运行)上下文做归一化**:否决。`normalizeSessionLog` 通过精确字符串匹配擦除 cwd,因此 fixture 中录制的 cwd 不会被擦除,每次比较都会失败。两侧各自基于自身 header 派生的上下文做归一化——下方的实现说明描述了具体机制。 ## 验证 -`session.golden.jsonl` 不再出现在快照 harness、fixture、遗留文件守卫或文档中的任何位置;快照测试对每个模型场景都从 `session.jsonl` 派生预期会话日志;手工编写的 sidecar 场景将其预期产出的日志提交为 `session.jsonl`,并以 `replay.override.json` 作为模型行为覆盖;遗留 fixture 守卫知道每种场景类型需要哪些文件。[ACP 快照测试 RFC](../../implemented/testing/2026-06-19-acp-snapshot-tests.md) 描述了精简后的 fixture 集合。 +`session.golden.jsonl` 在快照 harness、fixture、遗留文件守卫和文档中均不再出现;快照测试对每个模型场景都从 `session.jsonl` 派生预期会话日志;手工编写的 sidecar 场景将预期产出的日志作为 `session.jsonl` 提交,并以 `replay.override.json` 作为模型行为覆盖;遗留 fixture 守卫知道每种场景类型需要哪些文件。[ACP 快照测试 RFC](../../implemented/testing/2026-06-19-acp-snapshot-tests.md) 描述了精简后的 fixture 集合。 ## 后果 -评审人失去了一个让预期持久化日志在视觉上与回放 fixture 分离的产物名称。stdout golden 仍然保护编辑器 transcript(文本记录),将回放输出与 `session.jsonl` 比较则在不重复文件的前提下保留了 agent loop(智能体循环)/持久化的回归检查。 +评审者失去了一个让预期持久化日志在视觉上与回放 fixture 分离的产物名称。stdout golden 仍保护编辑器 transcript(文本记录),将回放输出与 `session.jsonl` 比较则在不重复文件的前提下保留了循环/持久化的回归检查。 ## 实现说明 -两侧各自基于自身 header 值进行归一化,因为录制与回放具有不同的 id、路径和时间戳。`fixtureContext()` 从 fixture 的 header 派生 fixture 上下文,使已归一化的 fixture 具有幂等性。会话日志使用普通相等比较而非文件快照更新,因此比较过程永远不会改写 fixture。 +两侧各自基于自身 header 值做归一化,因为录制与回放具有不同的 id、路径和时间戳。`fixtureContext()` 从 fixture 的 header 派生上下文,使已归一化的 fixture 具有幂等性。会话日志使用普通相等比较而非文件快照更新,因此比较过程不会改写 fixture。 diff --git a/docs/rfc/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.i18n.yaml b/docs/rfc/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.i18n.yaml index 73c068be2c..f3bddc661c 100644 --- a/docs/rfc/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.i18n.yaml +++ b/docs/rfc/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-22-fork-child-replay-seed-boundary.md: a0bf064508107a23147df1a6c824c53a3906c43e -2026-06-22-fork-child-replay-seed-boundary.zh.md: 7ee6c3d373ff5e598efa82d5c8fbad6b8e162aeb +2026-06-22-fork-child-replay-seed-boundary.zh.md: 3825cce806c036c7fa21641a2f0b7cc0533d84bf diff --git a/docs/rfc/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.zh.md b/docs/rfc/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.zh.md index 7ee6c3d373..3825cce806 100644 --- a/docs/rfc/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.zh.md +++ b/docs/rfc/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.zh.md @@ -1,18 +1,18 @@ -# RFC:持久化 seed 边界以确保 fork 子会话回放路由正确 - -Status: implemented +# RFC:持久化 seed 边界以确保 fork 子会话回放正确路由 [English](2026-06-22-fork-child-replay-seed-boundary.md) | 中文 +Status: implemented + ## 问题 -[逐会话快照回放 RFC](2026-06-22-subagent-snapshot-replay.md) 让快照层表达了嵌套 agent 的结构:一个父会话加上每个进程内 subagent 各一份录制日志,每份日志以调用方会话为键独立回放为自己的脚本。该 RFC 在 §Scope 末尾提到 fork 快照是「一个简单的后续补充,不是键控方案的缺口」。这个说法对 fork 子会话而言是错的——问题不在键控,而在*脚本推导*。 +[逐会话快照回放 RFC](2026-06-22-subagent-snapshot-replay.md) 让快照层表达了嵌套 agent(智能体)的形状:一个父会话加上每个进程内 subagent 各一份已录制的日志,各自作为独立脚本回放、以调用方会话为键。该 RFC 指出(§ Scope 末尾条目)fork 快照是「一个平凡的后续补充,不是键控方案的缺口」。这对 fork 子会话而言是错的——问题不在键控,而在*脚本推导*。 -subagent 脚本由 [`deriveReplayScript`](../../../../packages/support/llm-replay) 从录制的会话日志推导而来:它按 `(turn, step)` 对日志中的 `assistant/chunk` 事件分组,每次 `stream()` 调用对应一条回放条目。对 **spawn** 子会话而言这是正确的,因为其日志只包含自己的模型调用。 +subagent 脚本由 [`deriveReplayScript`](../../../../packages/support/llm-replay) 从已录制的会话日志推导:它按 `(turn, step)` 对日志中的 `assistant/chunk` 事件分组,每次 `stream()` 调用对应一条回放条目。对 **spawn** 子会话而言这是正确的,因为其日志只包含自身的模型调用。 -**fork** 子会话不同。fork 后端用*父会话日志中一段平衡的已完成轮次前缀*([`dsh-subagent-inprocess`](../../../../packages/subagent/subagent-inprocess))来初始化子会话,而这段 seed 会成为子会话持久化的 `log`(`Session` 构造函数将 seed 复制到 `this.log`)。因此 fork 子会话的 `.jsonl` 以**父会话**的事件开头——包括父会话的 `assistant/chunk` 事件——之后才是子会话自己的轮次。 +**fork** 子会话不同。fork 后端用*父日志的一段平衡的已完成轮次前缀*([`dsh-subagent-inprocess`](../../../../packages/subagent/subagent-inprocess))来播种子会话,而该 seed 会成为子会话持久化的 `log`(`Session` 构造函数将 seed 复制进 `this.log`)。因此 fork 子会话的 `.jsonl` 以**父会话**的事件开头——包括父会话的 `assistant/chunk` 事件——之后才是子会话自身的轮次。 -如果从 fork 子会话的完整日志推导脚本,就会把**父会话**录制的响应当作**子会话**的模型调用来回放:活跃的 fork 子会话第一次调用 `stream()` 时,会收到父会话的第一段 chunk 序列而非自己的。目前录制的场景全部是 spawn,所以这个问题从未触发——但 fork 快照会静默地路由错误,而这恰恰是快照层存在的意义所要捕获的那类 bug。 +从 fork 子会话的完整日志推导脚本,会把**父会话**的已录制响应当作**子会话**的模型调用来回放:实际运行的 fork 子会话第一次调用 `stream()` 时,会收到父会话的第一段 chunk 序列而非自身的。目前已录制的场景全部是 spawn,所以这从未触发——但 fork 快照会静默地错误路由,恰好属于快照层存在的意义所要捕获的那类 bug。 ## 决策 @@ -20,30 +20,30 @@ subagent 脚本由 [`deriveReplayScript`](../../../../packages/support/llm-repla ### 1. 会话头部的 `seedLength` -`SessionHeader` 新增可选字段 `seedLength: number`:表示前导多少个事件是通过 seed 继承而来、而非本会话产生的。fork 后端在创建子会话时设置它(= seed 前缀长度);新建的 spawn 子会话不设置(等价于 0)。该字段通过 `CreateSessionOptions.meta`(以及 `CreateAgentOptions.meta`)传递,在 `SessionStore.prepare` 中设置。 +`SessionHeader` 新增可选字段 `seedLength: number`——表示有多少前导事件是通过 seed 继承而来、而非本会话产生的。fork 后端在创建子会话时设置它(= 播种前缀的长度);全新的 spawn 子会话不设置(等同于 0)。它通过 `CreateSessionOptions.meta`(及 `CreateAgentOptions.meta`)传递,在 `SessionStore.prepare` 中设置。 -`seedLength` 是**显式**的,从不从 `seed.length` 推断。重建(resume/load)时用会话的完整存储日志作为 seed,此时 `seed.length` 是全长而非原始边界——重建路径改为从加载的 header 中取回持久化的 `seedLength`。(形状与 `createdAt` 相同:重建时显式保留,而非重新默认为当前时间。) +`seedLength` 是**显式**的,绝不从 `seed.length` 推断。重建(resume/load)时用会话的完整已存储日志作为 seed,此时 `seed.length` 是全长而非原始边界——resume 路径改为从加载的 header 中取回持久化的 `seedLength`。(形状与 `createdAt` 相同:重建时显式保留,而非重新默认为当前时间。) -### 2. 两个持久化后端都完整往返 +### 2. 两个持久化后端均完整往返 - **JSONL**:header 行上的 `seedLength` 字段(`toHeaderLine`/`fromHeaderLine`)。 - **SQLite**:`sessions` 表上的 `seed_length` 列。 -包含 `seed_length`、`source_event_seqs` 和 `surface_op` 的 SQLite 布局为 schema version 4。更早的 version 3 布局存在歧义,因此按预发布政策,所有非当前 `user_version` 均直接拒绝,不做迁移。 +包含 `seed_length`、`source_event_seqs` 和 `surface_op` 的 SQLite 布局为 schema version 4。更早的 version 3 布局存在歧义,因此在预发布策略下,所有非当前 `user_version` 均直接拒绝,不做迁移。 -### 3. 回放在边界之后推导子会话脚本 +### 3. 回放从边界之后推导子会话脚本 -`dsh-llm-replay` 的 `parseSessionHeader` 现在也读取 `seedLength`(缺失 ⇒ 0),`loadSessionScripts` 从 `parseSessionLog(text).slice(seedLength)` 推导子会话的条目——即边界处及之后的事件,也就是子会话自己的模型调用。对 spawn 子会话而言 `seedLength` 为 0,这是一个空操作,因此 spawn 场景逐字节不变。 +`dsh-llm-replay` 的 `parseSessionHeader` 现在也读取 `seedLength`(缺失则为 0),`loadSessionScripts` 从 `parseSessionLog(text).slice(seedLength)` 推导子会话条目——即边界及之后的事件,也就是子会话自身的模型调用。对 spawn 子会话而言 `seedLength` 为 0,此操作是空操作,spawn 场景逐字节不变。 -这关闭了路由正确性的缺口,两个录制的 fork 场景对其进行了端到端验证——见 [Record fork and mixed spawn+fork snapshot scenarios](2026-06-22-fork-snapshot-scenarios.md)。 +这关闭了路由正确性的缺口,两个已录制的 fork 场景对其进行端到端验证——见 [Record fork and mixed spawn+fork snapshot scenarios](2026-06-22-fork-snapshot-scenarios.md)。 ## 曾考虑的替代方案 -- **在 `llm-replay` 中启发式推导边界**(seed 前缀是连续的父事件,止于子会话第一条 `user/message` 之前的最后一个 `turn/end`)。否决:在测试 harness 中用脆弱的启发式重新推导一个生产者已经知道的事实。在源头(fork 后端)持久化边界,是「包边界处显式优于隐式」规则跨持久化边界的应用——子会话 fixture 的读取方永远不需要重建继承在哪里结束。 -- **固定格式版本而不递增**(事件日志使用的 `SESSION_FORMAT_VERSION = 0`「不稳定」策略)。对 SQLite *表*布局否决:`SCHEMA_VERSION` 是单调递增并拒绝旧版的旋钮(一小组值得区分的修订),与事件词汇的 `version` 不同。新增列正是它所版本化的那种破坏性表结构变更,因此递增。 +- **在 `llm-replay` 中启发式推导边界**(播种前缀是连续的父事件,止于子会话第一条 `user/message` 之前的最后一个 `turn/end`)。否决:在测试 harness 中用脆弱的启发式重新推导一个生产者已经知道的事实。在源头(fork 后端)持久化边界,是「在包(package)seam 处显式优于隐式」这条规则跨越持久化边界的应用——子会话 fixture(测试前置数据)的读取者永远不需要重建继承在哪里结束。 +- **固定格式版本而不递增**(事件日志使用的 `SESSION_FORMAT_VERSION = 0`「不稳定」姿态)。对 SQLite *表*布局否决:`SCHEMA_VERSION` 是单调递增并拒绝旧版的旋钮(一组小的、值得区分的修订),与事件词汇表的 `version` 不同。新增列正是它所版本化的那种破坏性表变更,因此需要递增。 ## 后果 -- 在 core 与两个后端之间新增一个持久化的 header 字段;核心数据结构目录(`persistence.md`)在同一个变更中更新(其 `SessionHeader` / `CreateSessionOptions` 的 `type-equiv` 块)。 +- core 与两个后端新增一个持久化 header 字段;核心数据结构目录(`persistence.md`)在同一变更中更新(其 `SessionHeader` / `CreateSessionOptions` 的 `type-equiv` 块)。 - 既有的 schema v2 SQLite 数据库在打开时被拒绝(预发布阶段无用户数据)。 -- spawn 回放不变(`seedLength` 为 0)。fork 回放现在将子会话路由到自己的脚本;由 `llm-replay` 测试中的一个回归用例覆盖(一个子会话 fixture,其 seed 前缀包含父会话的 chunk——推导出的子会话脚本必须排除它,不做 slice 时该用例为红),以及一个持久化往返测试(两个后端,通过共享的 coordinator 契约)。 +- spawn 回放不变(`seedLength` 为 0)。fork 回放现在将子会话路由到自身的脚本;由 `llm-replay` 测试中的一个回归用例覆盖(一个子会话 fixture,其播种前缀包含父会话的 chunk——推导出的子会话脚本必须排除它,不做 slice 时该用例为红)以及一个持久化往返测试(两个后端,通过共享的 coordinator 契约)。 diff --git a/docs/rfc/implemented/testing/2026-06-22-fork-snapshot-scenarios.i18n.yaml b/docs/rfc/implemented/testing/2026-06-22-fork-snapshot-scenarios.i18n.yaml index 7312eb5cab..d262a105ce 100644 --- a/docs/rfc/implemented/testing/2026-06-22-fork-snapshot-scenarios.i18n.yaml +++ b/docs/rfc/implemented/testing/2026-06-22-fork-snapshot-scenarios.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-22-fork-snapshot-scenarios.md: baca94d6a1071ec38ee20ca841fc3472a870b1a2 -2026-06-22-fork-snapshot-scenarios.zh.md: 227d54cc2bb2e66a391dddd29a7f2593cb02f7e2 +2026-06-22-fork-snapshot-scenarios.zh.md: b6f3f6a6f318a343d5e32573d39f11f59b509ee3 diff --git a/docs/rfc/implemented/testing/2026-06-22-fork-snapshot-scenarios.zh.md b/docs/rfc/implemented/testing/2026-06-22-fork-snapshot-scenarios.zh.md index 227d54cc2b..b6f3f6a6f3 100644 --- a/docs/rfc/implemented/testing/2026-06-22-fork-snapshot-scenarios.zh.md +++ b/docs/rfc/implemented/testing/2026-06-22-fork-snapshot-scenarios.zh.md @@ -1,31 +1,31 @@ # RFC:记录 fork 与混合 spawn+fork 快照场景 -[English](2026-06-22-fork-snapshot-scenarios.md) | 中文 - Status: implemented +[English](2026-06-22-fork-snapshot-scenarios.md) | 中文 + ## 问题 -[seed-boundary RFC](2026-06-22-fork-child-replay-seed-boundary.md) 让 fork 子会话的回放路由正确工作了:`dsh-llm-replay` 从子会话持久化的 `seedLength` 边界处或之后的事件推导出子会话的脚本,因此 fork 子会话继承的父会话前缀不会被当作子会话自身的模型调用来回放。但该 RFC 交付时**没有录制 fork 场景**:切片逻辑仅由 `llm-replay` 的单元测试(一个合成的子会话 fixture(测试前置数据))和一个持久化往返测试覆盖。全 transcript(文本记录)快照层——那个启动真实 `acp-agent` 并回放端到端嵌套 transcript 的网——只有 spawn 子会话(`subagent-spawn`、`subagent-multi`)。一个让单元测试保持绿色的 fork 路由回归,仍然会逃过专为捕获 transcript 回归而建的那一层。 +[seed-boundary RFC](2026-06-22-fork-child-replay-seed-boundary.md) 使 fork 子会话的回放路由正确运作:`dsh-llm-replay` 从子会话持久化的 `seedLength` 边界处或之后的事件推导出子会话的脚本,因此 fork 子会话继承的父会话前缀不会被当作子会话自身的模型调用来回放。但该 RFC 交付时**没有记录 fork 场景**——该切片仅由 `llm-replay` 的单元测试(一个合成的子会话 fixture(测试前置数据))和一个持久化往返测试覆盖。全 transcript(文本记录)快照层(即启动真实 `acp-agent` 并回放端到端嵌套 transcript 的那张网)只有 spawn 子会话(`subagent-spawn`、`subagent-multi`)。如果一个 fork 路由回归让单元测试保持绿色,它仍然会逃过专为捕获 transcript 回归而建的那一层。 -表达 fork 场景所需的快照基础设施已经就位:两个进程内后端都通过 `cordis.yml` / `cordis.snapshot.yml` 接入为两个面向模型的工具(`subagent` → spawn、`subagent_fork` → fork),harness 收集每个子会话的日志,回放按 `seedLength` 为键转发每个子会话的 fixture。缺少的只是一个*录制好的场景*来驱动 fork 子会话走完这条路径。 +表达 fork 场景所需的快照基础设施已经就位:两个进程内后端都在 `cordis.yml` / `cordis.snapshot.yml` 中以两个面向模型的工具接入(`subagent` → spawn、`subagent_fork` → fork),harness 会收集每个子会话的日志,回放按 `seedLength` 为键转发各子会话的 fixture。缺少的是一个**已记录的场景**来驱动 fork 子会话走完这条路径。 ## 决策 -对真实 API 录制两个场景,均在默认门禁中以 keyless 方式回放: +针对真实 API 记录两个场景,均在默认门禁中以无密钥方式回放: -- **`subagent-fork`**:父会话完成一个轮次以建立一个事实,然后通过 `subagent_fork` 委派一个子任务。fork 子会话继承对话(其日志携带非零 `seedLength`),因此能从父会话的上下文中作答。这是聚焦的回归守卫:子会话 fixture 的 `seedLength` 就是回放切片所依赖的边界,来自真实 fork 的录制而非手工合成。 -- **`subagent-mixed`**:父会话完成一个轮次,然后在同一个 transcript 中分别通过 `subagent`(全新的 spawn 子会话,`seedLength` 为 0)和 `subagent_fork`(fork 子会话,非零 `seedLength`)各委派一次。这是 seed-boundary 和 per-session-replay 两份 RFC 都提到的「未来补充」的混合 spawn+fork 场景:一个 transcript 同时覆盖两种传输方式和切片的两个分支(`seedLength` 0 = 无操作,`seedLength > 0` = 裁掉继承的前缀),两个子会话按 `createdAt` 排序为先 spawn 后 fork。 +- **`subagent-fork`**:父会话完成一个轮次以建立一个事实,然后通过 `subagent_fork` 委派一个子任务。fork 子会话继承对话(其日志携带非零 `seedLength`),因此可以从父会话的上下文中作答。这是聚焦的回归守卫:子会话 fixture 的 `seedLength` 就是回放切片所依赖的边界,来自真实 fork 的记录而非手工合成。 +- **`subagent-mixed`**:父会话完成一个轮次,然后在同一个 transcript 中分别通过 `subagent`(全新的 spawn 子会话,`seedLength` 为 0)和 `subagent_fork`(fork 子会话,`seedLength` 非零)各委派一次。这是 seed-boundary 和 per-session-replay 两份 RFC 都列为后续补充的混合 spawn+fork 场景:一个 transcript 同时覆盖两种传输方式和切片的两个分支(`seedLength` 0 = 无操作,`seedLength > 0` = 裁剪继承的前缀),两个子会话按 `createdAt` 排序为先 spawn 后 fork。 ### 为什么需要一个已完成的第一轮次 -fork 后端用父会话的**已完成轮次的平衡前缀**([`completedTurnPrefix`](../../../../packages/subagent/subagent-fork))来填充子会话种子。如果父会话在第一个轮次就 fork,则没有已完成的轮次可继承,种子为空(≡ 全新 spawn,`seedLength` 为 0),这**不会**覆盖切片逻辑。因此两个场景都使用两条提示词输入:第一条提示词完成一个轮次(建立一个 codeword 供子会话稍后回忆),第二条委派 fork。子会话 transcript 中回忆出的 codeword 只是模型行为的附带结果;真正承载验证的产物是子会话 fixture 中录制的 `seedLength`,回放切片消费的正是它。 +fork 后端用父会话的**已完成轮次的平衡前缀**([`completedTurnPrefix`](../../../../packages/subagent/subagent-fork))来初始化子会话。如果父会话在第一轮次就 fork,则没有已完成的轮次可继承,seed 为空(等价于全新 spawn,`seedLength` 为 0),这**不会**覆盖切片逻辑。因此两个场景都使用双 prompt 输入:第一个 prompt 完成一个轮次(建立一个 codeword,子会话稍后被要求回忆它),第二个 prompt 委派 fork。子会话 transcript 中回忆出的 codeword 只是模型行为的附带产物;真正承载验证的产物是子会话 fixture 中记录的 `seedLength`,回放切片消费的正是它。 ## 后果 -- fork 路由切片现在由全 transcript 层守卫,而不仅仅是单元测试。移除 `slice(seedLength)`(回放整个子会话日志)会让**两个**新场景变红——fork 子会话收到的是父会话录制的 chunk 而非自己的——证明守卫确实生效(场景落地时已验证红→绿)。 -- `subagent-mixed` 是第一个在同一个 transcript 中驱动两个*不同* subagent 后端的快照场景,同时覆盖了跨 spawn 和 fork 子会话的 per-session 回放键控。 -- 进程外(ACP)subagent 回放是另一种形态(每个子会话是独立进程、有自己的回放),仍以 `TODO(acp-subagent-replay)` 跟踪——本文场景仅限进程内。 -- 重新录制(`pnpm run test:snapshot:record`)会从真实 API 重新生成全部四个 fork/spawn fixture;两个新场景在没有 key 时与所有录制场景一样自动跳过。 +- fork 路由切片现在由全 transcript 层守卫,而不仅仅是单元测试。移除 `slice(seedLength)`(回放整个子会话日志)会让**两个**新场景变红——fork 子会话收到的是父会话记录的 chunk 而非自己的——证明守卫确实生效(场景落地时已验证红→绿)。 +- `subagent-mixed` 是第一个在同一个 transcript 中驱动两种**不同** subagent 后端的快照场景,同时覆盖了跨 spawn 和 fork 子会话的 per-session 回放键控。 +- 进程外(ACP)subagent 回放形态不同(每个子会话是独立进程、有自己的回放),仍以 `TODO(acp-subagent-replay)` 跟踪——本文场景仅限进程内。 +- 重新录制(`pnpm run test:snapshot:record`)会从真实 API 重新生成全部四个 fork/spawn fixture;两个新场景在无密钥时自动跳过,与所有已录制场景一致。 <!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> diff --git a/docs/rfc/implemented/testing/2026-06-22-subagent-snapshot-replay.i18n.yaml b/docs/rfc/implemented/testing/2026-06-22-subagent-snapshot-replay.i18n.yaml index 9ce1aefb81..8bea2c8a5e 100644 --- a/docs/rfc/implemented/testing/2026-06-22-subagent-snapshot-replay.i18n.yaml +++ b/docs/rfc/implemented/testing/2026-06-22-subagent-snapshot-replay.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-22-subagent-snapshot-replay.md: fbb2e5b93cced118a24f5560f229e2dc341bf3b2 -2026-06-22-subagent-snapshot-replay.zh.md: fd4ba0c109d77fdf9b64e25de74d3da577ce5a9b +2026-06-22-subagent-snapshot-replay.zh.md: 6514a0bcb5db3948f6d8f4693b17a74e4a9ad926 diff --git a/docs/rfc/implemented/testing/2026-06-22-subagent-snapshot-replay.zh.md b/docs/rfc/implemented/testing/2026-06-22-subagent-snapshot-replay.zh.md index fd4ba0c109..6514a0bcb5 100644 --- a/docs/rfc/implemented/testing/2026-06-22-subagent-snapshot-replay.zh.md +++ b/docs/rfc/implemented/testing/2026-06-22-subagent-snapshot-replay.zh.md @@ -1,58 +1,58 @@ # RFC:嵌套 agent 的逐会话快照回放 -Status: implemented - [English](2026-06-22-subagent-snapshot-replay.md) | 中文 +Status: implemented + ## 问题 -快照测试层(`pnpm run test:snapshot`)启动真实的 `acp-agent` 子进程,通过 [`dsh-llm-replay`](../../../../packages/support/llm-replay) 回放录制的会话,并将归一化后的 stdout transcript(文本记录)与重新持久化的会话日志同提交的 golden 文件做 diff。这是唯一一个端到端验证完整编辑器侧 transcript 的测试层。 +快照测试层(`pnpm run test:snapshot`)启动真实的 `acp-agent` 子进程,通过 [`dsh-llm-replay`](../../../../packages/support/llm-replay) 回放录制的会话,并将归一化后的 stdout transcript(文本记录)与重新持久化的会话日志对已提交的金标文件做 diff。这是唯一一个端到端验证完整编辑器侧 transcript 的测试层。 -它最初为**单会话单进程**而建,这一假设硬编码在两处: +该层最初为每个进程只有一个会话而构建,这一假设硬编码在两处: -- **`dsh-llm-replay` 没有任何键控。** 它用一个全局游标,将第 N 次 `llm/stream` 调用对应到单一录制序列的第 N 条。当父 agent 和进程内 subagent 同时在一个 context 上流式输出时,调用交错,单一游标会把子 agent 的脚本交给父 agent(反之亦然)。 -- **harness 只收割一份日志。** `findSessionLog` 遍历 sessions 根目录,返回找到的**第一个** `.jsonl`。subagent 作为同一 cwd bucket 中的第二个 `Session` 运行、拥有自己的日志,因此子 agent 的 transcript 被静默丢弃。 +- **`dsh-llm-replay` 没有做任何键控。** 它用一个全局游标,将第 N 次 `llm/stream` 调用对应到单一录制序列的第 N 条。当父 agent 和一个进程内 subagent 在同一个上下文上同时流式输出时,调用交错,单一游标会把子 agent 的脚本发给父 agent(反之亦然)。 +- **harness 只收集一份日志。** `findSessionLog` 遍历 sessions 根目录,返回找到的第一个 `.jsonl`。subagent 作为第二个 `Session` 运行,在同一个 cwd bucket 下有自己的日志,因此子 agent 的 transcript 被静默丢弃。 -这正是 [subagent seam RFC](../../implemented/feature/2026-06-21-subagent-capability-seam.md) 中记录的 `TODO(subagent-snapshots)` 延期项:进程内后端(PR2)已有单元测试和 e2e 覆盖,但全 transcript 快照层在本基础设施落地之前无法表达嵌套 agent 的形态。本 RFC 即为该堆叠后续。 +这就是 [subagent seam RFC](../../implemented/feature/2026-06-21-subagent-capability-seam.md) 中记录的 `TODO(subagent-snapshots)` 延期项:进程内后端(PR2)已有单元测试和 e2e 覆盖,但全 transcript 快照层在本基础设施就绪之前无法表达嵌套 agent 的形态。本 RFC 即为该堆叠后续。 ## 决策 -回放按**调用方会话**键控,harness 收割**所有**会话日志。 +回放按**调用方会话**键控,harness 收集**所有**会话日志。 -### 1. 调用方会话 id 随模型请求传递 +### 1. 调用方会话 id 附着在模型请求上 -`GenerateOptions` 新增可选字段 `sessionId`,在请求组装时从 `agent.session.id` 打入。适配器忽略它;`llm/stream` 监听器用它按发起方会话路由。其类型为 `Branded<'SessionId'>`(来自 `dsh-brand`)而非 `dsh-session` 的 `SessionId`,因为后者所在包导入了 `dsh-llm` 的 `Message`,反向导入会形成循环。两个类型等价,会话 id 赋值无需强制转换。将 brand 移入专用 ids 包属于独立工作,因为它会触及所有 id 导入。 +`GenerateOptions` 新增可选字段 `sessionId`,在请求组装时从 `agent.session.id` 赋值。适配器忽略它;`llm/stream` 监听器用它按发起会话路由。其类型为 `Branded<'SessionId'>`(来自 `dsh-brand`)而非 `dsh-session` 的 `SessionId`,因为后者所在包(package)导入了 `dsh-llm` 的 `Message`,反向导入会形成循环。两个类型等价,因此会话 id 赋值无需类型转换。将 brand 移到一个专用 ids 包属于独立工作,因为它会影响所有 id 导入。 ### 2. 回放按首次调用顺序将活跃会话绑定到录制脚本 -嵌套场景录制不止一份日志:父会话(`session.jsonl`)加每个 subagent 子会话各一份(`session.1.jsonl`、……)。`dsh-llm-replay` 全部加载,为每个录制会话推导一份脚本,并按 header 中的 `createdAt` 排序(父会话先于子会话创建)。 +嵌套场景录制多份日志:父会话(`session.jsonl`)加每个 subagent 子会话各一份(`session.1.jsonl`……)。`dsh-llm-replay` 全部加载,为每个录制会话派生一份脚本,并按 header 中的 `createdAt` 排序(父会话先于子会话创建)。 -活跃会话 id 每次运行都是全新随机值,永远不等于录制时的 id,因此活跃会话无法通过 id 相等绑定脚本。取而代之的是**首次调用顺序**绑定:第一个发起模型调用的活跃会话认领排序第一的脚本(即父会话——`createdAt` 最早,且必然最先流式输出,因为它必须先运行一个轮次才能委派),下一个新活跃会话认领下一份脚本,依此类推。之后每个会话独立推进自己的游标。 +活跃会话 id 每次运行都是全新随机值,永远不等于录制时的 id,因此活跃会话无法通过 id 相等绑定到脚本。取而代之的是**首次调用顺序**绑定:第一个发起任何模型调用的活跃会话认领第一份有序脚本(即父会话:`createdAt` 最早,且必然最先流式输出,因为它必须先运行一个轮次才能委派),下一个新活跃会话认领下一份脚本,依此类推。此后每个会话独立推进自己的游标。 -这按**谁在调用**键控,而非按全局调用顺序——因此即使 subagent 将来并发运行或在后台运行也保持正确(全局游标会导致交错)。不携带 `sessionId` 的调用(直接在单元测试中调用 `stream()`)被视为一个匿名会话、绑定到主脚本,因此单会话路径的行为与旧版逐字节一致。活跃会话数多于录制脚本数是一个 fail-loud 错误(出现了未录制的 subagent),绝不会静默误路由。 +这种方式按**谁在调用**键控,而非按全局调用顺序。因此即使 subagent 将来并发或在后台运行(全局游标会导致交错),它仍然正确。不携带 `sessionId` 的调用(直接在单元测试中调用 `stream()`)被视为一个匿名会话、绑定到主脚本,因此单会话路径与旧行为逐字节一致。活跃会话数多于录制脚本数时会快速失败报错(出现了未录制的 subagent),绝不会静默错误路由。 -子 fixture 按 `createdAt` 排序,在兄弟会话严格顺序执行时与调用顺序一致。id 平局打破只是让退化碰撞确定化。并发或后台子会话必须引入显式的首次调用序号,而非依赖时间戳。 +子 fixture(测试前置数据)按 `createdAt` 排序,在兄弟会话严格顺序执行时与调用顺序一致。id 平局打破仅使退化碰撞具有确定性。并发或后台子会话必须引入显式的首次调用序号,而非依赖时间戳。 ## 曾考虑的替代方案 -曾考虑并否决的方案是将父子日志**按调用顺序合并**为一份全局脚本(仅在进程内 subagent 严格嵌套执行——父 agent 阻塞等待子 agent——时才正确)。对当前的同步切面更简单,但把「父阻塞于子」这一不变式烤死了;未来的后台/并发 subagent 会打破它,而逐会话键控不会。 +曾考虑但否决的方案是:**将父子日志按调用顺序合并**为一份全局脚本(仅在进程内 subagent 执行严格嵌套——父 agent 阻塞等待子 agent——时才正确)。对当前的同步裁剪而言更简单,但将「父阻塞于子」这一不变式固化了进去;未来若引入后台/并发 subagent 就会失效。逐会话键控则不会。 -### 3. harness 收割所有日志,主会话优先 +### 3. harness 收集所有日志,主会话优先 -`harvestSessionLogs` 收集 sessions 根目录下每个 cwd bucket 中的所有 `.jsonl`(JSONL 后端将父会话与同 cwd 的子会话放在同一 bucket),解析各自的 header,并按主会话优先排序:顶层会话(无 `parentSession`)在前,子会话按 `createdAt` 升序排列。`RunResult.sessionLogs` 是复数结果;spec 在录制时将每份日志写回 fixture(`session.jsonl` + `session.<n>.jsonl`),在回放时将每份收割的日志与对应 fixture 做 diff。归一化器已接受复数会话 id 并折叠任何游离 UUID,因此无需修改归一化器。 +`harvestSessionLogs` 收集 sessions 根目录下每个 cwd bucket 中的所有 `.jsonl`(JSONL 后端将父会话与同 cwd 的子会话放在同一个 bucket),解析各自的 header,并按主会话优先排序:顶层会话(无 `parentSession`)在前,各子会话按 `createdAt` 升序排列。`RunResult.sessionLogs` 是复数结果;spec 在录制时将每份日志写回对应 fixture(`session.jsonl` + `session.<n>.jsonl`),在回放时将每份收集到的日志与其 fixture 做 diff。归一化器已支持复数会话 id 并会折叠任何游离 UUID,因此无需修改归一化器。 ### 4. 场景 新增两个嵌套场景,均对真实 API 录制: -- **`subagent-spawn`**:父 agent 通过 `subagent` 工具将一个子任务委派给一个新 spawn 子会话(2 个会话)。 -- **`subagent-multi`**:父 agent 委派两个子任务,各自交给独立的 spawn 子会话(3 个会话),以三份并行脚本和同一父会话下两个子会话的 `createdAt` 排序来压测逐会话键控。 +- **`subagent-spawn`**:父 agent 通过 `subagent` 工具将一个子任务委派给一个新 spawn 的子 agent(2 个会话)。 +- **`subagent-multi`**:父 agent 委派两个子任务,各自交给自己的 spawn 子 agent(3 个会话),以三份并行脚本和同一父 agent 下两个子会话的 `createdAt` 排序来压测逐会话键控。 两者均在默认门禁中以 keyless 方式回放。 ## 后果 -- `TODO(subagent-snapshots)` 延期项已解决:嵌套 agent transcript 现在是快照的一等形态。 -- `GenerateOptions.sessionId` 是一个小而诚实的 core-seam 新增,在回放之外也有用(遥测、请求路由)。 -- `subagent` 工具绑定到单一提供方,因此 `subagent-multi` 中的两个子会话都是 spawn(全新)。键控按会话路由而非按后端路由,因此对 fork 也已正确。但脚本*推导*并非如此:fork 子会话的日志以种子化的父前缀(父会话的 `assistant/chunk` 事件)开头,从整份日志推导脚本会把父会话的响应当作子会话的来回放。这一正确性缺口通过持久化种子边界来弥合——见 [Persist the seed boundary so fork-child replay routes correctly](2026-06-22-fork-child-replay-seed-boundary.md)——录制的 fork 与混合 spawn+fork 场景现在通过一份 transcript 同时验证两种传输方式(见 [Record fork and mixed spawn+fork snapshot scenarios](2026-06-22-fork-snapshot-scenarios.md))。 -- 进程外(ACP)subagent 是完全不同的回放形态(每个子 agent 是独立进程、有自己的 replay),作为 `TODO(acp-subagent-replay)` 记录在 PR3 计划中。 +- `TODO(subagent-snapshots)` 延期项已解决:嵌套 agent 的 transcript 现在是快照层的一等形态。 +- `GenerateOptions.sessionId` 是一个小而诚实的 core-seam 新增,在回放之外同样有用(遥测、请求路由)。 +- `subagent` 工具绑定到单一提供方,因此 `subagent-multi` 中的两个子 agent 都是 spawn(全新创建)。键控按会话路由而非按后端路由,因此对 fork 同样正确。但脚本**派生**逻辑此前不正确:fork 子会话的日志以种子化的父前缀(父会话的 `assistant/chunk` 事件)开头,如果从完整日志派生脚本,就会把父 agent 的响应当作子 agent 的来回放。这一正确性缺口通过持久化种子边界来弥合——见 [Persist the seed boundary so fork-child replay routes correctly](2026-06-22-fork-child-replay-seed-boundary.md)——录制的 fork 与混合 spawn+fork 场景现在通过一份 transcript 同时验证两种传输方式(见 [Record fork and mixed spawn+fork snapshot scenarios](2026-06-22-fork-snapshot-scenarios.md))。 +- 进程外(ACP)subagent 是完全不同的回放形态(每个子 agent 是自己的进程、有自己的回放),作为 `TODO(acp-subagent-replay)` 记录在 PR3 计划中。 diff --git a/docs/rfc/implemented/testing/2026-07-04-hook-snapshot-matrix.i18n.yaml b/docs/rfc/implemented/testing/2026-07-04-hook-snapshot-matrix.i18n.yaml index 10cfb7320c..a0daca4800 100644 --- a/docs/rfc/implemented/testing/2026-07-04-hook-snapshot-matrix.i18n.yaml +++ b/docs/rfc/implemented/testing/2026-07-04-hook-snapshot-matrix.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-04-hook-snapshot-matrix.md: 8505e82fb681975c7506102a3eb858a29ccc11c8 -2026-07-04-hook-snapshot-matrix.zh.md: 6bd4b65b6ba2e16f8433fb0e67ae7ca4eb6a470e +2026-07-04-hook-snapshot-matrix.zh.md: 9a9f085400e938bc15c171f652d65f7bcdfa518f diff --git a/docs/rfc/implemented/testing/2026-07-04-hook-snapshot-matrix.zh.md b/docs/rfc/implemented/testing/2026-07-04-hook-snapshot-matrix.zh.md index 6bd4b65b6b..9a9f085400 100644 --- a/docs/rfc/implemented/testing/2026-07-04-hook-snapshot-matrix.zh.md +++ b/docs/rfc/implemented/testing/2026-07-04-hook-snapshot-matrix.zh.md @@ -1,4 +1,4 @@ -# RFC:钩子快照矩阵——覆盖两种桥接的端到端金标测试 +# RFC:Hook 快照矩阵——覆盖两种 bridge 的端到端 golden 测试 Status: implemented @@ -6,45 +6,45 @@ Status: implemented ## 问题 -钩子桥接——[`dsh-hooks-claude`](../../../../packages/hooks/hooks-claude)(7 个 Claude Code 钩子点)与 [`dsh-hooks-codex`](../../../../packages/hooks/hooks-codex)(5 个 Codex 钩子点)——将外部钩子命令映射到 harness 的拦截 seam 上。它们拥有深度的单元测试与覆盖率规格覆盖(每个决策分支、每种 payload 方言,均对 mock seam 驱动),外加一个需要密钥的 e2e 测试(`hooks.e2e.ts`,一次真实的 `PreToolUse` 拦截)。但全 transcript(文本记录)快照层——那张真正启动 `acp-agent` 子进程、无密钥回放录制会话、并将归一化的 ACP stdout 与重新持久化的日志对比已提交金标的网——只覆盖了**一个**钩子:Claude 的 `UserPromptSubmit` 拦截(`hook-cc-promptsubmit-block`)。 +hook bridge——[`dsh-hooks-claude`](../../../../packages/hooks/hooks-claude)(7 个 Claude Code hook 点)和 [`dsh-hooks-codex`](../../../../packages/hooks/hooks-codex)(5 个 Codex 点)——将外部 hook 命令映射到 harness 的拦截 seam 上。它们拥有深度的单元测试和 coverage-spec 覆盖率(每个决策分支、每种 payload 方言,均对 mock 的 seam 驱动),外加一个需要密钥的 e2e 测试(`hooks.e2e.ts`,一次真实的 `PreToolUse` 拦截)。但完整 transcript(文本记录)快照层:那张真正启动 `acp-agent` 子进程、无密钥回放录制会话、并将规范化的 ACP stdout 与重新持久化的日志与已提交 golden 做 diff 的网,只覆盖了**一个** hook:Claude 的 `UserPromptSubmit` 拦截(`hook-cc-promptsubmit-block`)。 -这正是 mock 单元测试在结构上无法替代的层级:它让真实的桥接翻译真实钩子进程的结果,送入真实的 seam 决策,再由真实的 agent loop(智能体循环)做出反应,渲染结果与编辑器所见完全一致。一个桥接翻译或循环结构的回归,即使让所有单元测试保持绿色,也会在除那一个钩子点之外的所有点上逃逸——而对于 Codex 桥接,ACP 示例甚至没有加载它,因此没有任何 Codex 钩子能端到端触发。 +这正是 mock 单元测试在结构上无法替代的层级:它验证的是真实 bridge 将真实 hook 进程的结果翻译到真实 seam 决策,再到真实 agent loop(智能体循环)的反应,渲染结果与编辑器看到的完全一致。一个 bridge 翻译或 loop 结构的回归,即使让所有单元测试保持绿色,也会在除那一个 hook 点之外的所有点上逃逸;而对于 Codex bridge,ACP 示例甚至没有加载它,因此没有任何 Codex hook 能端到端触发。 ## 决策 实现由两个耦合部分组成: -### 1. ACP 示例同时加载两种钩子桥接 +### 1. ACP 示例同时加载两种 hook bridge -`examples/acp-agent/cordis.yml` 与 `cordis.snapshot.yml` 现在在 `dsh-hooks-claude` 之外同时加载 `dsh-hooks-codex`,各自指向自己的配置文件(Claude 用 `./hooks.json`,Codex 用 `./codex-hooks.json`——两种方言无法共用一个文件)。这是一个真正的产品表面变更,而非仅测试用的接线:交付的 ACP 服务器(以及 `demo:acp` 入口)现在同时携带两种桥接。 +`examples/acp-agent/cordis.yml` 和 `cordis.snapshot.yml` 现在同时加载 `dsh-hooks-codex` 与 `dsh-hooks-claude`,各自指向自己的配置文件(Claude 用 `./hooks.json`,Codex 用 `./codex-hooks.json`——两种方言无法共用一个文件)。这是一个真正的产品接口变更,而非仅用于测试的接线:交付的 ACP 服务器(以及 `demo:acp` 入口)现在同时携带两种 bridge。 -这是安全的,因为配置文件不存在时桥接是**静默空操作**:`apply()` 捕获读取失败、通过 `ctx.logger` 记录日志、不注册任何东西——零监听器、零会话事件。`acp-agent` 应用不挂载 stdout logger,因此该警告不会到达 ACP JSON-RPC 通道。只需要 Claude 钩子的场景(或真实项目)只提供 `hooks.json`;Codex 桥接找不到 `codex-hooks.json` 便自行消失。这已通过实验验证:两种桥接同时加载时,所有既有快照(均未提供 `codex-hooks.json`)逐字节一致。 +这是安全的,因为配置文件不存在时 bridge 是**静默无操作**的:`apply()` 捕获读取失败、通过 `ctx.logger` 记录日志、不注册任何东西——零监听器、零会话事件。`acp-agent` 应用不附带 stdout logger,因此警告不会到达 ACP JSON-RPC 通道。只需要 Claude hook 的场景(或真实项目)只提供 `hooks.json`;Codex bridge 找不到 `codex-hooks.json` 便自动消失。这已通过实验验证:在两种 bridge 同时加载的情况下,所有既有快照(均不附带 `codex-hooks.json`)逐字节一致。 -同时加载是让快照层能够在产品交付的同一个真实应用上对每种方言进行测试的最低要求。录制(启动 `cordis.yml`)天然加载两者,回放以同样方式继承:`cordis.snapshot.yml` 是 `cordis.yml` 的 include-overlay,仅替换 llm 条目(见 [single-source the acp-agent replay config](2026-07-04-single-source-acp-replay-config.md)),因此添加到运行时配置树的桥接无需第二次编辑即出现在回放树中。 +同时加载是让快照层能够在产品交付的同一个真实应用上验证每种方言的最低要求。录制(启动 `cordis.yml`)天然加载两者,回放以同样方式继承:`cordis.snapshot.yml` 是 `cordis.yml` 的 include-overlay,只替换 llm 入口(见[单一来源 acp-agent 回放配置](2026-07-04-single-source-acp-replay-config.md)),因此添加到运行时树的 bridge 无需第二次编辑即出现在回放树中。 -### 2. 每个钩子点 × 其标志性结果各一个快照场景,覆盖两种方言 +### 2. 每个 hook 点 × 其主要结果各一个快照场景,覆盖两种方言 `examples/acp-agent/tests/snapshots/` 下共 13 个场景,命名为 `hook-<dialect>-<point>-<outcome>`: - **手工编写、无模型轮次**(无密钥、无 sidecar——派生的回放脚本为空;比对的是携带 `hook/*` 事件的 `rejected` 轮次):`hook-cc-promptsubmit-block`、`hook-codex-promptsubmit-block`。 -- **对真实 API 录制、录制期间钩子活跃**(模型对决策的反应是捕获的 transcript 的一部分,此后无密钥回放):`hook-{cc,codex}-promptsubmit-context`(allow + additionalContext 折叠)、`hook-cc-pretool-deny` / `hook-codex-pretool-block`(deny → `isError` 工具结果)、`hook-cc-pretool-ask`(ask → 降级为 deny 并附带 approval-required 原因)、`hook-{cc,codex}-posttool-block`(block 并附反馈)、`hook-{cc,codex}-posttool-context`(accept + additionalContext)、`hook-{cc,codex}-stop-continue`(阻塞式 Stop 钩子通过 steering(中途引导)强制多走一步)。 +- **对真实 API 录制、录制期间 hook 活跃**(模型对决策的反应是捕获的 transcript 的一部分,此后无密钥回放):`hook-{cc,codex}-promptsubmit-context`(allow + additionalContext 折叠)、`hook-cc-pretool-deny` / `hook-codex-pretool-block`(deny → `isError` 工具结果)、`hook-cc-pretool-ask`(ask → 降级为 deny 并附带 approval-required 原因)、`hook-{cc,codex}-posttool-block`(block 并附带反馈)、`hook-{cc,codex}-posttool-context`(accept + additionalContext)、`hook-{cc,codex}-stop-continue`(阻塞性 Stop hook 通过 steering(中途引导)强制多走一步)。 -每个钩子命令只输出**固定字面字符串**(无时间戳/pid/`$RANDOM`/cwd 回显);快照归一化器擦除 `hook/result` 携带的唯一易变字段(`durationMs`)。`Stop` 场景通过标记文件(`.stop_fired`)自限,使 force-continue 不会循环——`stop_hook_active` 循环守卫仍是桥接的一个 `TODO`,因此无条件的 Stop 钩子会对每一步都 force-continue。 +每个 hook 命令只输出**固定字面量字符串**(无时间戳/pid/`$RANDOM`/cwd 回显);快照规范化器擦除 `hook/result` 携带的唯一不稳定字段(`durationMs`)。`Stop` 场景通过标记文件(`.stop_fired`)自限,使 force-continue 不会循环——`stop_hook_active` 循环守卫仍是 bridge 的一个 `TODO`,因此无条件的 Stop hook 会在每一步都 force-continue。 -### 三个钩子点被有意排除在快照之外 +### 三个 hook 点被有意排除在快照之外 -在构建矩阵过程中发现,记录在此是因为这是一个决策而非疏漏: +在构建矩阵过程中发现,记录于此是因为这些遗漏是决策而非疏忽: -- **`SessionStart` 与 `SubagentStart`** 通过一个分离的、尽力而为的 `void runPoint(...).then(agent.inject())` 注入上下文,**没有轮次绑定**。产生的 `context/message` 与它所先于的工作(首次模型请求/子 agent 的首轮)存在竞争,落在日志中的位置不确定。录制的金标甚至在自身回放时都无法复现——10 次回放稳定性检查对两者均 10/10 失败。它们留在桥接的单元覆盖中,单元测试直接驱动 seam 而无时序竞争。(如果注入将来变为轮次绑定且确定性的——`TODO(session-start-gating)` 所指的方向——这些点就可以纳入快照。) -- **`SubagentStop`** 是纯观察:其 `subagent/end` 处理器不传递轮次(因此无 `hook/*` 日志事件)、不做注入。它对 transcript **什么都不写**,因此金标会与无钩子运行逐字节一致,永远无法被证明失败——一道咬不到人的守卫。它留在单元覆盖中(`bridge.spec.ts` 已断言该纯观察调用)。 +- **`SessionStart` 与 `SubagentStart`** 通过一个分离的、尽力而为的 `void runPoint(...).then(agent.inject())` 注入上下文,**没有**轮次绑定。由此产生的 `context/message` 与它所先于的工作(首次模型请求/子 agent 的首轮)存在竞争,落在日志中的位置不确定。录制的 golden 甚至无法在自身回放中复现——10 次回放稳定性检查对两者均 10/10 失败。它们留在 bridge 的单元覆盖率中,单元测试直接驱动 seam 而无时序竞争。(如果注入将来变为轮次绑定且确定性的——`TODO(session-start-gating)` 所指的方向——它们就可以纳入快照。) +- **`SubagentStop`** 是纯观察性的:其 `subagent/end` 处理器不传递轮次(因此无 `hook/*` 日志事件)、不做注入。它对 transcript **不写入任何内容**,因此 golden 与无 hook 运行逐字节一致,永远无法被证明失败——一道永远不会触发的守卫。它留在单元覆盖率中(`bridge.spec.ts` 已断言了纯观察调用)。 -因此该矩阵覆盖了所有具有**确定性、可观测 transcript 足迹**的钩子点,涵盖两种方言。 +因此,该矩阵覆盖了所有具有**确定性、可观测** transcript 足迹的 hook 点,涵盖两种方言。 ## 后果 -- 每个具有可观测 transcript 的桥接 seam 映射现在都在全 transcript 层、在真实应用中、为两种方言设有守卫——包括此前完全没有端到端覆盖的 Codex 桥接。录制的金标捕获了模型对 denied/blocked/force-continued 轮次的真实反应,这是手工编写的 transcript 只能猜测的。 -- block 场景无需密钥(无模型轮次);其余场景从录制的 fixture(测试前置数据)无密钥回放。`pnpm run test:snapshot:record` 从真实 API 重新生成录制的 fixture,无密钥时像所有录制场景一样自动跳过。 -- prove-red 纪律成立:篡改钩子配置的输出(例如修改 deny 原因)会使其场景在回放时变红——钩子进程在回放期间**真实运行**(只有模型被回放),因此金标守卫的是实际的 hook→seam→loop 路径,而非它的 mock。 -- `acp-agent` 演示现在加载了一个通常会空操作的 Codex 桥接(典型项目中没有 `codex-hooks.json`),这正是预期的 fail-soft 行为,而非代价。 +- 每个具有可观测 transcript 的 bridge seam 映射现在都在完整 transcript 层级、在真实应用中、对两种方言受到守护——包括此前完全没有端到端覆盖率的 Codex bridge。录制的 golden 捕获了模型对 deny/block/force-continue 轮次的真实反应,这是手工编写的 transcript 只能猜测的。 +- block 场景无需密钥(无模型轮次);其余场景从录制的 fixture(测试前置数据)无密钥回放。`pnpm run test:snapshot:record` 从真实 API 重新生成录制的 fixture,无密钥时自动跳过,与所有录制场景一致。 +- prove-red 纪律成立:篡改 hook 配置的输出(例如修改 deny 原因)会使其场景在回放时变红——hook 进程在回放期间**真实运行**(只有模型被回放),因此 golden 守护的是实际的 hook→seam→loop 路径,而非它的 mock。 +- `acp-agent` 演示现在加载了一个通常会无操作的 Codex bridge(典型项目中没有 `codex-hooks.json`),这正是预期的柔性失败行为,而非代价。 <!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> diff --git a/docs/rfc/implemented/testing/2026-07-04-single-source-acp-replay-config.i18n.yaml b/docs/rfc/implemented/testing/2026-07-04-single-source-acp-replay-config.i18n.yaml index 642ba7f9ad..4aa9340c22 100644 --- a/docs/rfc/implemented/testing/2026-07-04-single-source-acp-replay-config.i18n.yaml +++ b/docs/rfc/implemented/testing/2026-07-04-single-source-acp-replay-config.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-04-single-source-acp-replay-config.md: 922bdcced50f8e289449e05b51774f202228b0f8 -2026-07-04-single-source-acp-replay-config.zh.md: d27ea0d3b7227f0fb5f349478591962dd5fe8030 +2026-07-04-single-source-acp-replay-config.zh.md: b347186f362fa5454f7bd5106c2e261cb8a00b61 diff --git a/docs/rfc/implemented/testing/2026-07-04-single-source-acp-replay-config.zh.md b/docs/rfc/implemented/testing/2026-07-04-single-source-acp-replay-config.zh.md index d27ea0d3b7..b347186f36 100644 --- a/docs/rfc/implemented/testing/2026-07-04-single-source-acp-replay-config.zh.md +++ b/docs/rfc/implemented/testing/2026-07-04-single-source-acp-replay-config.zh.md @@ -1,27 +1,27 @@ -# RFC:将 acp-agent 回放配置收归单一来源 - -Status: implemented +# RFC:将 acp-agent 回放配置改为单一来源 [English](2026-07-04-single-source-acp-replay-config.md) | 中文 +Status: implemented + ## 问题 -`examples/acp-agent` 曾维护两份手写配置:`cordis.yml`(线上树)和一份 `cordis.snapshot.yml`,后者逐条镜像前者、仅替换 LLM 后端。去掉注释后,全部差异只是八行 `llm-deepseek` 段落换成两行 `llm-replay` 段落。每次应用形态变更都要改两遍,且没有门禁保证对称性:如果两份副本漂移,快照层会静默地测试一个与实际交付不同的应用——正是快照层本身要消除的[「单元全绿、产品却坏」类缺口](../../../postmortem/0001-acp-default-export-drops-inject.md),在更高一层被重新引入,唯一的防线是评审者的警觉。 +`examples/acp-agent` 曾维护两份手写配置:`cordis.yml`(正式运行树)和 `cordis.snapshot.yml`(逐条镜像前者,仅替换 LLM(大语言模型)后端)。去掉注释后,全部差异只是八行的 `llm-deepseek` 段落换成两行的 `llm-replay` 段落。每次应用结构变更都要改两遍,且没有门禁保障对称性:一旦两份副本漂移,快照层就会悄悄测试一个与实际交付不同的应用——正是快照层本要消除的["单元测试全绿、产品却坏了"这类缺口](../../../postmortem/0001-acp-default-export-drops-inject.md),在上一层被重新引入,唯一的防线是评审者的警觉。 ## 决策 -`cordis.snapshot.yml` include 线上配置,按 id 和 name 禁用指定的 DeepSeek 适配器,并插入回放适配器。因此除此之外的所有条目均来自交付树。回放时选择 overlay;录制仍然启动 `cordis.yml`,加载守卫允许被有意禁用的条目。 +`cordis.snapshot.yml` include 正式配置,通过 id 和 name 禁用指定的 DeepSeek 适配器,并插入回放适配器。其余所有条目因此来自正式运行树。回放时选择 overlay;录制仍然启动 `cordis.yml`,加载守卫允许被有意禁用的条目。 -overlay 依赖的一个 vendor 插件事实(有意为之):include 在加载文件时应用 `patches`——其 `refresh()`/`internal/update` 路径重读时不重新打补丁——这恰好满足一次性回放启动的需要(回放应用不加载 `hmr`,也没有东西在运行中改写配置)。快照套件即为证明:所有场景在 overlay 上原样通过,包括逐字节一致的 golden 文件。 +overlay 依赖一个 vendor 插件的事实,这是有意为之:include 在加载文件时应用 `patches`,其 `refresh()`/`internal/update` 路径重读时不会重新打补丁。这恰好满足一次性回放启动的需要(回放应用不加载 `hmr`,也没有东西在运行中改写配置)。快照套件即为证明:所有场景在 overlay 上原样通过,包括逐字节一致的 golden 文件。 ## 曾考虑的替代方案 -### 为什么不选这些方案? +### 为何不采用这些替代方案? -保留完整的双份配置并加一道对称性校验门禁是记录在案的兜底方案——它能消除静默漂移这一类问题,但仍保留一份 125 行的近似副本,其全部内容只是一个条目的差异,且随应用每增加一个插件而增长。在 bin 侧做替换(解析配置、替换条目、删除文件)会把 YAML 手术放进发布产物,并将回放差异移出视野;overlay 方案让差异保持声明式、可读、且紧邻基础配置——这正是双份配置的支持者真正想要的教学价值。 +保留完整的双副本并加一道对称性校验门禁是记录在案的退路——它能消除静默漂移这一类问题,但仍保留一份 125 行的近乎复制品,其全部内容只是一个条目的差异,且随应用每增加一个插件而增长。在 bin 侧做替换(解析配置、替换条目、删除文件)则会把 YAML 手术放进发布产物,并把回放差异藏到视线之外;overlay 让差异保持声明式、可读,且紧邻基础配置——这正是双副本支持者真正看重的教学价值。 ## 后果 -- 向 `cordis.yml` 添加的插件无需第二次编辑即进入回放树;漂移类问题从结构上消除,而非仅靠门禁拦截。 -- overlay 依赖条目携带稳定的 `id:`。禁用补丁上的 `name` 断言防止误定位(id 被复用时补丁跳过而非禁用错误的插件)。id **重命名**会使补丁退化为跳过,其警告需要一个回放应用有意不具备的 logger——可观察的结果是一条无用的无密钥 `llm-deepseek` 条目与 `llm-replay` 并存,回放输出仍然正确(`llm-replay` 拥有流的短路权);这属于留给评审发现的配置腐烂,而非错误的快照。顶层插入的条目若 id 与已有条目冲突,通过 loader 的 id map 以 last-wins 解析——当前配置无冲突,新增补丁行才是引入冲突的位置。 -- 如果未来回放树需要第二处分歧(另一个后端被替换),只需多加一行补丁,而非再 fork 一份文件。 +- 向 `cordis.yml` 添加插件即自动进入回放树,无需第二次编辑;漂移这一类问题从结构上消失,而非靠门禁拦截。 +- overlay 依赖条目携带稳定的 `id:`。禁用补丁上的 `name` 断言防止误定位(id 被复用时补丁跳过而非禁用错误的插件)。如果 id 被**重命名**,补丁退化为跳过,其警告需要一个回放应用有意不具备的 logger——可观测结果是一条无效的无密钥 `llm-deepseek` 条目与 `llm-replay` 并存,回放输出仍然正确(`llm-replay` 拥有流的短路权);这属于配置腐烂,留给评审发现,不会产生错误的快照。顶层插入一个 id 与既有条目冲突的新条目时,loader 的 id map 以后者为准;当前配置无冲突,新增补丁行才是引入冲突的场所。 +- 如果未来回放树需要第二处差异(另一个后端被替换),只需多加一行补丁,而非再 fork 一份文件。 diff --git a/docs/rfc/implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.i18n.yaml b/docs/rfc/implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.i18n.yaml index 3ee0a6df6d..3b2d92e28c 100644 --- a/docs/rfc/implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.i18n.yaml +++ b/docs/rfc/implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-06-pin-request-header-content-in-one-scenario.md: 5ccaa23a268114c5ba37ec153f4960b47df13bfd -2026-07-06-pin-request-header-content-in-one-scenario.zh.md: f5ef5e056bb2c05d14ed71315f31c962237db72c +2026-07-06-pin-request-header-content-in-one-scenario.zh.md: 909968430dc3e648e09eeeedd47436c35b90b870 diff --git a/docs/rfc/implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.zh.md b/docs/rfc/implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.zh.md index f5ef5e056b..909968430d 100644 --- a/docs/rfc/implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.zh.md +++ b/docs/rfc/implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.zh.md @@ -1,35 +1,35 @@ -# RFC:在单一快照场景中固定 request-header 内容 - -[English](2026-07-06-pin-request-header-content-in-one-scenario.md) | 中文 +# RFC:在单个快照场景中固定请求头内容 Status: implemented +[English](2026-07-06-pin-request-header-content-in-one-scenario.md) | 中文 + ## 问题 -ACP(Agent Client Protocol)快照测试套件需要证明每个 `request/header` 中实际发送的组合系统提示词和工具 schema 列表,但如果在每个 `session.jsonl` 中重复这些内容,一次提示词或 schema 编辑就会改写数十条巨大的单行 JSON 记录。保留一份原始 header 可以避免重复,但提示词的评审体验仍然很差:行文被 JSON 转义到一行里,与数千字符的工具 schema 混在一起。 +一个 ACP(Agent Client Protocol)快照测试套件需要证明每个 `request/header` 中实际发送的组合系统提示词与工具 schema 列表,但如果在每个 `session.jsonl` 中重复这些内容,一次提示词或 schema 编辑就会改写数十条巨大的单行 JSON 记录。保留一份原始 header 可以避免重复,但提示词的评审体验仍然很差:行文被 JSON 转义到一行中,与数千字符的工具 schema 混在一起。 ## 决策 -每个 header 组合类别恰好有一个场景被标记为 `pinsHeader`。其目录按评审格式拆分固定内容:`system-prompt.golden.md` 以普通 Markdown 存放归一化后的组合提示词,`tool-schemas.golden.json` 以结构化 JSON 存放完整的初始 schema 及后续 schema 变更,`session.jsonl` 保留 config、reason 和任何模型可见的前缀,同时将 `header.system` 和 `header.tools` 存为 `"{{system}}"` / `"{{tools}}"`。其余所有 JSONL 使用相同的提示词和工具 token,并同样对 session-prefix 内容做 token 化。固定机制实现在 [`dsh-acp-snapshot`](../../../../packages/support/acp-snapshot/README.md) 中,其套件工厂强制每个类别只有一个 pin。 +每个 header 组合类别恰好有一个场景被标记为 `pinsHeader`。其目录按评审格式拆分固定内容:`system-prompt.golden.md` 以普通 Markdown 存放归一化后的组合提示词,`tool-schemas.golden.json` 以结构化 JSON 存放完整的初始 schema 及后续 schema 变更,而 `session.jsonl` 保留 config、reason 及任何模型可见的前缀,同时将 `header.system` 和 `header.tools` 存为 `"{{system}}"` / `"{{tools}}"`。其余所有 JSONL 使用相同的提示词和工具 token,并同样对会话前缀内容做 token 化处理。固定机制实现在 [`dsh-acp-snapshot`](../../../../packages/support/acp-snapshot/README.md) 中,其套件工厂强制每个类别只有一个固定场景。 -纯净的 `scrubSystemPrompts` 和 `scrubToolSchemas` 归一化器应用于所有存储的 session fixture(测试前置数据),独立地对初始 header 内容和 header-delta 批量内容做 token 化。`scrubRequestHeaders` 还为非固定场景对 session-prefix 内容做 token 化,同时保留结构性事实:system-delta 的位置与数量、增删改的工具名称、前缀消息数量、字段存在性、config 和 reason。record 和 refresh 的回写操作在写入 JSONL 前应用相应的 scrub,并从归一化的实时 header 和 delta 重新生成两个 sidecar 文件,因此两条路径都不会将提示词/schema 批量内容重新引入 JSONL,也不会让评审产物变陈旧。 +纯粹的 `scrubSystemPrompts` 和 `scrubToolSchemas` 归一化器应用于每个存储的会话 fixture(测试前置数据),独立地对初始 header 内容和 header-delta 批量内容做 token 化。`scrubRequestHeaders` 还为非固定场景的会话前缀内容做 token 化,同时保留结构性事实:system-delta 的位置与数量、新增/移除/变更的工具名称、前缀消息数量、字段存在性、config 和 reason。record 与 refresh 的回写操作在写入 JSONL 前应用相应的 scrub,并从归一化后的实时 header 和 delta 重新生成两个 sidecar 文件,因此两条路径都不会把提示词/schema 批量内容重新引入 JSONL,也不会让评审产物变陈旧。 -守卫使这一拆分自我强制。在磁盘上:每个 `session*.jsonl` 都是提示词和 schema 两个 scrubber 的不动点;只有非固定 fixture(测试前置数据)还必须是完整 header scrub 的不动点;两个 sidecar 恰好存在于固定 fixture 旁边,采用规范的换行终止格式;每个类别有且仅有一个 pin。在运行时:由 parent、spawn 子进程、fork 子进程、初始请求或恢复产生的每个 `request/header`,在易变值归一化后必须与重建的 pin 匹配;固定运行的提示词和 schema delta 也必须与其 sidecar 匹配。如果 header 缺少字符串类型的 prompt、缺少数组类型的工具列表,或出现未声明的 `request/header-delta`,则立即报错。 +守卫机制使这一拆分自我强制。在磁盘上:每个 `session*.jsonl` 都是提示词和 schema 两个 scrubber 的不动点;只有非固定 fixture 还必须是完整 header scrub 的不动点;两个 sidecar 文件恰好存在于固定 fixture 旁边,采用规范的换行终止格式;每个类别有且仅有一个固定场景。在运行时:由 parent、spawn 子会话、fork 子会话、初始请求或 resume 产生的每个 `request/header`,在经过易变值归一化后必须与重建的固定内容匹配;固定运行的提示词和 schema delta 也必须与其 sidecar 匹配。如果 header 没有字符串类型的 prompt、没有数组类型的工具列表,或包含未声明的 `request/header-delta`,则立即失败并报错。 -一个 pin 覆盖整个套件,因为每个会话(parent、spawn 子进程、fork 子进程)组合出的工具列表完全相同、提示词除 cwd 外完全相同,而一致性守卫会在这一前提不再成立时立即使套件失败。如果 header 组合在设计上变为会话相关的(例如受限的 subagent 工具集),则分化出的形状获得自己的固定场景。 +一个固定场景覆盖整个套件,因为每个会话(parent、spawn 子会话、fork 子会话)组合出的工具列表完全相同、提示词除 cwd 外完全相同,而一致性守卫会在这一前提不再成立时立即使套件失败。如果 header 组合将来在设计上变为会话相关的(例如受限的 subagent 工具集),那么分歧的形态将获得自己的固定场景。 ## 曾考虑的替代方案 - **每次变更重新录制或手动编辑所有 fixture**:保留了精确的 header,但行为差异被重复的提示词和 schema 内容淹没。 -- **仅在比较时 scrub,fixture 保持原始状态**:比较能通过,但已提交的 fixture 保留着陈旧的重复内容,下次录制时整体改写。存储 token 诚实地表明每个 JSONL 没有固定什么。 -- **全部 scrub,不做任何固定**:丢失了组合 header 实际发送内容(提示词组装、已注册工具顺序、完整 schema)的唯一端到端记录。生成的工具目录只孤立地记录每个工具;只有真实 fixture 能固定组合后的完整集合。 -- **将完整的 pin 全部保留在 JSONL 中**:消除了套件级重复,但提示词和 schema 变更仍然表现为一行转义文本。Markdown 和结构化 JSON 为各自的内容提供了自然的评审格式,同时不削弱重建 header 的断言。 -- **精简会话日志本身(记录内容摘要,header 存到别处)**:违反可重建契约:产品日志必须逐比特重现每个请求(见[可重建请求 RFC](../architecture/2026-07-05-reconstructable-requests.md))。header 体积是测试产物的问题,在测试归一化中解决;线上日志不受影响。 +- **仅在比较时 scrub,fixture 保持原始内容**:比较能通过,但已提交的 fixture 保留着陈旧的重复内容,下次录制时会整体重写。存储 token 诚实地表明每个 JSONL 没有固定什么。 +- **全部 scrub,不做任何固定**:丢失了组合 header 实际发送内容(提示词组装、已注册工具顺序、完整 schema)的唯一端到端记录。生成的工具目录只孤立地记录每个工具;只有真实 fixture 才能固定组合后的完整集合。 +- **将完整固定内容全部保留在 JSONL 中**:消除了套件范围的重复,但提示词和 schema 变更仍然是一行转义文本。Markdown 和结构化 JSON 为每种内容提供其自然的评审格式,同时不削弱重建 header 的断言。 +- **精简会话日志本身(记录内容摘要,将 header 存放在别处)**:违反可重建性契约:产品日志必须逐位重现每个请求([可重建请求 RFC](../architecture/2026-07-05-reconstructable-requests.md))。header 体积是测试产物的问题,在测试归一化中解决;线上日志不受影响。 ## 验证 -套件针对拆分后的 pin 回放每个场景。单元覆盖率检验独立 scrubber 和完整 scrubber、两种 sidecar 格式、record/refresh 重新生成、归一化的提示词/schema 提取、不动点强制、必需文件对称性、重建 header 的一致性,以及 delta 拒绝。 +套件针对拆分后的固定内容回放每个场景。单元测试覆盖率涵盖独立 scrubber 和完整 scrubber、两种 sidecar 格式、record/refresh 重新生成、归一化提示词/schema 提取、不动点强制、必需文件对称性、重建 header 一致性以及 delta 拒绝。 ## 后果 -系统提示词的变更在每个受影响的组合类别中产生一个面向行的 Markdown diff;工具描述的变更在每个类别中产生一个结构化 JSON diff;普通的行为 fixture 不受影响。session fixture 以 token 显示被省略的内容,运行时一致性守卫使每个拆分 pin 对其类别中的所有会话具有权威性。每个固定场景携带两个生成的、换行规范化的 sidecar 文件。 +系统提示词变更在每个受影响的组合类别中产生一个面向行的 Markdown diff;工具描述变更在每个类别中产生一个结构化 JSON diff;普通行为 fixture 不受影响。会话 fixture 对省略的内容显示 token,运行时一致性守卫使每个拆分固定场景对其类别内的所有会话具有权威性。每个固定场景携带两个生成的、换行规范化的 sidecar 文件。 diff --git a/docs/rfc/implemented/testing/2026-07-08-shared-acp-snapshot-package.i18n.yaml b/docs/rfc/implemented/testing/2026-07-08-shared-acp-snapshot-package.i18n.yaml index d888da4d05..c582f34c23 100644 --- a/docs/rfc/implemented/testing/2026-07-08-shared-acp-snapshot-package.i18n.yaml +++ b/docs/rfc/implemented/testing/2026-07-08-shared-acp-snapshot-package.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-08-shared-acp-snapshot-package.md: 81714191a704af1a9ed029fb8004deac88e39427 -2026-07-08-shared-acp-snapshot-package.zh.md: 2a7c3091c731ded61bed939c8ae0a323123daac9 +2026-07-08-shared-acp-snapshot-package.zh.md: f80c8e49e80e9bf287c84c0ff4fb00377fad5175 diff --git a/docs/rfc/implemented/testing/2026-07-08-shared-acp-snapshot-package.zh.md b/docs/rfc/implemented/testing/2026-07-08-shared-acp-snapshot-package.zh.md index 2a7c3091c7..f80c8e49e8 100644 --- a/docs/rfc/implemented/testing/2026-07-08-shared-acp-snapshot-package.zh.md +++ b/docs/rfc/implemented/testing/2026-07-08-shared-acp-snapshot-package.zh.md @@ -1,38 +1,38 @@ -# RFC:将 ACP 快照测试套件提取为支持包 - -Status: implemented +# RFC:将 ACP 快照套件提取为支持包 [English](2026-07-08-shared-acp-snapshot-package.md) | 中文 +Status: implemented + ## 问题 -ACP 快照层([快照 RFC](2026-06-19-acp-snapshot-tests.md))由三个位于某个示例测试目录内的模块构成:`snapshot-harness.ts`(启动真实 bin 子进程、通过 ACP JSON-RPC 驱动它、收集持久化日志)、`snapshot-normalize.ts`(纯粹的 golden 归一化器),以及 `acp.snapshot.ts` 中约 150 行的场景主体与 fixture(测试前置数据)守卫(record/replay 模式、stdout-golden 与日志比对、pinned-header 一致性守卫、orphan/required-file/single-pin 元测试)。 +ACP 快照层([快照 RFC](2026-06-19-acp-snapshot-tests.md))由位于某个示例测试目录中的三个模块构成:`snapshot-harness.ts`(启动真实 bin 子进程,通过 ACP JSON-RPC 驱动它,收集持久化日志)、`snapshot-normalize.ts`(纯粹的 golden 规范化器),以及 `acp.snapshot.ts` 中约 150 行的场景主体加 fixture(测试前置数据)守卫(record/replay 模式、stdout-golden 与日志比对、pinned-header 一致性守卫、orphan/required-file/single-pin 元测试)。 -第二个 ACP 示例只能复制 record、归一化与收集逻辑,而这些逻辑必须保持一致。`examples/` 下的代码还处于包(package)覆盖率门禁之外,且原有 harness 只能取消权限请求。共享包使这些机制纳入度量,并允许场景脚本化地指定审批答案。 +第二个 ACP 示例只能复制 record、规范化和收集逻辑,而这些逻辑必须保持一致。`examples/` 下的代码也不在包(package)覆盖率门禁范围内,且原始 harness 只能取消权限请求。共享包使这些机制纳入度量,并允许场景脚本化地提供审批答案。 ## 决策 -机制代码位于 [`packages/support/acp-snapshot`](../../../../packages/support/acp-snapshot/README.md)(`@deepseek-ai/dsh-acp-snapshot`);示例的 `*.snapshot.ts` 只包含场景表、agent 路径和一次工厂调用,配合自己的 `snapshots/` fixture 与 `cordis.snapshot.yml` 覆盖层([单源 replay 配置](2026-07-04-single-source-acp-replay-config.md))。读取 `DSH_SNAPSHOT` 留在该边界——库接收的是已解析的 `mode`。 +这些机制位于 [`packages/support/acp-snapshot`](../../../../packages/support/acp-snapshot/README.md)(`@deepseek-ai/dsh-acp-snapshot`);示例的 `*.snapshot.ts` 只包含场景表、agent 路径和一次工厂调用,依赖自己的 `snapshots/` fixture 与 `cordis.snapshot.yml` overlay([单源 replay 配置](2026-07-04-single-source-acp-replay-config.md))。读取 `DSH_SNAPSHOT` 留在边缘层——库接收的是已解析的 `mode`。 -**`src/harness.ts`** 提供 `runScenario` 及其脚本/结果类型,以 agent 的 bin 路径和配置路径为参数。权限答案构成一个 FIFO 队列,按稳定的 option kind(而非随机的 option id)索引。缺少答案时取消该请求;不可用的 kind 取消 agent 请求并使场景失败。 +**`src/harness.ts`** 提供 `runScenario` 及其脚本/结果类型,以 agent 的 bin 和配置路径为参数。权限答案构成一个 FIFO 队列,以稳定的 option kind(而非随机的 option id)为键。缺少答案时取消该请求;不可用的 kind 取消 agent 请求并使场景失败。 -**`src/normalize.ts`**:纯归一化器,按策略不含钩子。当未来的事件携带新的易变字段(如审批耗时),共享归一化器在同一个变更中学会它,保持「归一化」的含义只有一个归属地,而非各套件各自扩展清洗逻辑。 +**`src/normalize.ts`** 是纯规范化器,按策略不含钩子:当未来某个事件携带新的易变字段(例如审批耗时),共享规范化器在同一个变更中学会它,保持「规范化」的含义只有一个归属,而非各套件各自扩展清洗逻辑。 -**`src/suite.ts`**:`Scenario` 类型与 `defineAcpSnapshotSuite(options)`,注册逐场景比对、record/refresh 的 fixture 回写、header pin 及其实时一致性守卫,以及 fixture 守卫块(无 orphan 场景目录、必需文件齐全、每个 class 恰好一个 pin、每个 JSONL 是 `scrubSystemPrompts` 的不动点、非 pinning 的 fixture 也是 `scrubRequestHeaders` 的不动点)。pinned-header 契约([pinned-header RFC](2026-07-06-pin-request-header-content-in-one-scenario.md))按套件生效:每个 header class 恰好标记一个 `pinsHeader` 场景,其 `system-prompt.golden.md` 与 JSONL 工具列表将组合后的 header 拆分为可评审的产物;一致性守卫将二者与该 class 中每个实时 header 进行比对。纯辅助函数(`childFixturePaths`、`fixtureContext`、`normalizedHeaders`、`normalizedSystemPrompts`、`formatSystemPromptSnapshot`、`headerDeltaCount`)从模块导出,以便直接进行单元覆盖。 +**`src/suite.ts`** 提供 `Scenario` 类型与 `defineAcpSnapshotSuite(options)`,注册逐场景比对、record/refresh 的 fixture 回写、header pin 及其实时一致性守卫,以及 fixture 守卫块(无 orphan 场景目录、必需文件齐全、每个 class 恰好一个 pin、每个 JSONL 是 `scrubSystemPrompts` 的不动点、非 pinning fixture 也是 `scrubRequestHeaders` 的不动点)。pinned-header 契约([pinned-header RFC](2026-07-06-pin-request-header-content-in-one-scenario.md))按套件划分:每个 header class 恰好标记一个 `pinsHeader` 场景,其 `system-prompt.golden.md` 与 JSONL 工具列表将组合后的 header 拆分为可评审的产物;一致性守卫将二者与该 class 中每个实时 header 进行比对。纯辅助函数(`childFixturePaths`、`fixtureContext`、`normalizedHeaders`、`normalizedSystemPrompts`、`formatSystemPromptSnapshot`、`headerDeltaCount`)从模块导出,以便直接进行单元覆盖。 ## 曾考虑的替代方案 -- **将模块复制到每个示例中**:正是本 RFC 要阻止的分叉。record/guard 逻辑恰恰是必须在各套件间逐字节一致的代码,而 examples 在覆盖率门禁之外,因此每份副本也无法被度量。 -- **在 `examples/` 下建共享模块目录**:代码仍在覆盖率门禁之外,且需要跨示例边界的相对导入,违背包名导入约定;`examples/` 的叶子节点按设计保持精简。 -- **在 `dsh-acp-demo` 中导出 `/testing` 子路径**:将测试基础设施耦合到产品包的公开接口与依赖集中;`packages/support/` 正是为真实但兼容性要求较低的开发/测试包而设,`dsh-llm-replay` 是先例,本包是其补全。 -- **导出原始测试体函数而非套件工厂**:每个示例将重新拥有 `describe`/`it` 骨架(每套件约 80 行注册样板),却无灵活性收益;工厂让消费方只需一张场景表加一次调用,导出的纯辅助函数在工厂设计内保留了单元可测性。 -- **可注入的 ACP `Client` 工厂取代声明式 `permissionAnswers`**:灵活性最大化,但将 SDK 客户端构造泄漏给每个消费方,并在正被统一的层面重新引入逐示例漂移;声明式队列让 `input.json` 保持为唯一的脚本化接口,且可被 golden 归一化。 -- **泛化到 ACP 之外(传输无关的快照 harness)**:不存在第二种传输;harness 端到端都是 ACP 形态(SDK 客户端、JSON-RPC 帧、`session/update` 等待器),推测性的抽象会在没有消费方之前就拆出一个 seam。 +- **将模块复制到每个示例中**:正是本 RFC 要防止的 fork。record/守卫逻辑恰恰是必须在各套件间保持逐字节一致的代码,而示例不在覆盖率门禁范围内,因此每份副本也无法被度量。 +- **在 `examples/` 下建共享模块目录**:代码仍在覆盖率门禁之外,且需要跨示例边界的相对导入,违反包名导入约定;`examples/` 的叶子节点按设计应保持轻薄。 +- **`dsh-acp-demo` 的 `/testing` 子路径导出**:将测试基础设施耦合到产品包的对外服务接口与依赖集中;`packages/support/` 的存在正是为了真实但兼容性承诺较低的开发/测试包,`dsh-llm-replay` 是先例,本包与之配套。 +- **导出原始测试体函数而非套件工厂**:每个示例将重新拥有 `describe`/`it` 骨架(每套件约 80 行注册样板),却无灵活性收益;工厂使消费方只需一张场景表加一次调用,而导出的纯辅助函数在工厂设计内保留了可单元测试性。 +- **可注入的 ACP `Client` 工厂,而非声明式 `permissionAnswers`**:灵活性最大,但将 SDK 客户端构造泄露给每个消费方,并在正被统一的层面重新引入逐示例漂移;声明式队列使 `input.json` 成为唯一的脚本化界面,且可被 golden 规范化。 +- **泛化到 ACP 之外(传输无关的快照 harness)**:不存在第二种传输方式;harness 端到端都是 ACP 形态(SDK 客户端、JSON-RPC 帧、`session/update` 等待器),推测性的抽象将是一个超前于任何消费方的 seam 拆分。 ## 测试 -提取保留了所有既有 ACP golden 的每一个字节。包的 `src/` 通过脚本化的 ACP 子进程实现逐文件 100% 覆盖:harness 测试覆盖每个步骤操作、两个预期错误分支、权限选择/回退/不可能选项、环境变量转发、工作区种子注入与收集排序/噪声/回退;suite 测试对已提交的合成 fixture 执行 replay,并对临时副本执行 record,加上纯辅助函数的测试。两个结构上不可达的守卫保留了有理由的覆盖率排除。fake agent 将 `session/new` 的 cwd 替换进日志,包括 Darwin 的 `/var` realpath 行为,与真实 bin 一致。 +提取保留了所有既有 ACP golden 字节。包的 `src/` 通过脚本化的 ACP 子进程达到逐文件 100% 覆盖率:harness 测试覆盖每个步骤操作、两条预期错误分支、权限选择/回退/不可能选项、环境变量转发、workspace 种子注入、收集排序/噪声/回退;suite 测试对已提交的合成 fixture 执行 replay,并对临时副本执行 record,同时覆盖纯辅助函数。两个结构上不可达的守卫保留了有理由的覆盖率排除。fake agent 将 `session/new` 的 cwd 替换到日志中,包括 Darwin 的 `/var` realpath 行为,与真实 bin 一致。 ## 后果 -新示例只需一张场景表加 fixture 即可获得完整的快照层——sandbox 分支从 master 合并后添加自己的套件(自己的 pin 场景、自己的覆盖层、通过 `test:snapshot:record` 生成 fixture、通过 `permissionAnswers` 指定审批答案)。代价:`suite.ts` 导入 vitest,因此该包只能在 vitest 运行中被导入——这是其他包没有的形态,已在其 README 中声明;每个套件 pin 自己的约 8 KB header fixture(真正不同的组合理应有自己的 pin;相同的组合会被该套件的一致性守卫捕获);e2e 启动器的重复仍然存在(`TODO(acp-test-harness)`)——当该迁移落地时,harness 是提取目标。 +新示例只需一张场景表加 fixture 即可获得完整快照层——sandbox 分支从 master 合入后添加自己的套件(自己的 pin 场景、自己的 overlay、通过 `test:snapshot:record` 生成 fixture、通过 `permissionAnswers` 提供审批答案)。代价:`suite.ts` 导入 vitest,因此该包只能在 vitest 运行中导入——这是其他包没有的形态,已在其 README 中声明;每个套件 pin 自己约 8 KB 的 header fixture(真正不同的组合值得拥有自己的 pin;相同的组合会被该套件的一致性守卫捕获);e2e launcher 的重复仍然存在(`TODO(acp-test-harness)`)——当该迁移落地时,harness 即为提取目标。 diff --git a/docs/rfc/proposed/architecture/2026-06-16-typed-event-schemas.i18n.yaml b/docs/rfc/proposed/architecture/2026-06-16-typed-event-schemas.i18n.yaml index 5b46f88338..59f21c938e 100644 --- a/docs/rfc/proposed/architecture/2026-06-16-typed-event-schemas.i18n.yaml +++ b/docs/rfc/proposed/architecture/2026-06-16-typed-event-schemas.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-16-typed-event-schemas.md: 8d14c3b2d90d8dcf295e122e95267c2c0d7b2a17 -2026-06-16-typed-event-schemas.zh.md: 87bd6b89a2332b7a3a609a9c36d1e9835e42a0b1 +2026-06-16-typed-event-schemas.zh.md: 34f46a87058b409ecdab38d851654db49d87e201 diff --git a/docs/rfc/proposed/architecture/2026-06-16-typed-event-schemas.zh.md b/docs/rfc/proposed/architecture/2026-06-16-typed-event-schemas.zh.md index 87bd6b89a2..34f46a8705 100644 --- a/docs/rfc/proposed/architecture/2026-06-16-typed-event-schemas.zh.md +++ b/docs/rfc/proposed/architecture/2026-06-16-typed-event-schemas.zh.md @@ -1,4 +1,4 @@ -# RFC:事件词汇的运行时 schema(Zod 与 merge-extensible-map 模式之争) +# RFC:事件词汇的运行时 schema(Zod 与 merge-extensible-map 模式之辩) [English](2026-06-16-typed-event-schemas.md) | 中文 @@ -6,72 +6,72 @@ Status: proposed ## 问题 -harness 将其核心词汇——内容块、消息来源、结束原因、轮次触发器、轮次结束原因与会话事件——建模为 **merge-extensible map**:一个 TypeScript `interface`(如 `SessionEventMap`、`ContentBlockMap`),插件通过声明合并对其扩展,公开联合类型以 `Map[keyof Map]` 派生。这是本仓库的通用扩展模式,记录在 [docs/architecture.md](../../../architecture.md) 中("The same merge-extensible-map pattern is used for `MessageSource`, `FinishReason`, `TurnTrigger`, and `TurnEndReason`"),并被 `defineTool` 的 `InferArgs` DSL 与 `assertNever` 穷尽性约定所依赖。 +harness 将其核心词汇——内容块、消息来源、结束原因、轮次触发器、轮次结束原因与会话事件——建模为 **merge-extensible map**:一个 TypeScript `interface`(如 `SessionEventMap`、`ContentBlockMap`),插件通过声明合并对其扩展,公开联合类型则以 `Map[keyof Map]` 派生。这是本仓库的通用扩展模式,记录在 [docs/architecture.md](../../../architecture.md) 中("The same merge-extensible-map pattern is used for `MessageSource`, `FinishReason`, `TurnTrigger`, and `TurnEndReason`"),`defineTool` 的 `InferArgs` DSL 和 `assertNever` 穷举约定都依赖于它。 -该模式**仅存在于编译期**。类型在运行时消失:没有 schema 对象可供校验传入值、解析不可信输入或在运行时枚举。[会话持久化契约](../../implemented/architecture/2026-06-14-session-persistence.md)暴露了两个后果: +该模式**仅存在于编译期**。类型在运行时消失:没有 schema 对象可供校验传入值、解析不可信输入或在运行时枚举变体。[会话持久化契约](../../implemented/architecture/2026-06-14-session-persistence.md)暴露了两个后果: -1. **持久化将 `event.data` 视为不透明 JSON。** JSONL/SQLite 后端对每个事件逐字 `JSON.stringify`/`JSON.parse`;唯一的运行时守卫是 `isJsonValue`(往返可序列化性——拒绝 BigInt、函数、循环引用、非有限数等),而**不是**结构校验。一个损坏但仍为合法 JSON 的事件数据(字段类型错误、字段缺失)会静默往返,只有在之后被消费方的 `switch` 处理时才可能被发现。 -2. **插件新增的变体没有运行时契约。** 一个通过声明合并添加新 `SessionEventMap` 键的插件,在自身代码中获得了编译期类型,但没有任何机制校验它产出的值是否匹配它声明的形状——无论在生产端、持久化边界还是重新加载时。 +1. **持久化将 `event.data` 视为不透明 JSON。** JSONL/SQLite 后端对每个事件逐字 `JSON.stringify`/`JSON.parse`;唯一的运行时守卫是 `isJsonValue`(往返可序列化性检查:拒绝 BigInt、函数、循环引用、非有限数等),而**非**结构校验。一个损坏但仍为合法 JSON 的事件数据(字段类型错误、字段缺失)会静默往返,只有在后续消费方的 `switch` 中才可能被捕获。 +2. **插件新增变体没有运行时契约。** 一个通过声明合并添加新 `SessionEventMap` 键的插件,在自身代码中获得了编译期类型,但没有任何机制校验它产出的值是否符合它所声明的形状——无论是在生产者处、持久化边界处还是重新加载时。 -由此引出问题:事件词汇是否应迁移到 **Zod** 或其他运行时 schema 库,使持久化边界与插件边界拥有运行时 schema 而非被擦除的类型。 +由此引出问题:事件词汇是否应迁移到 **Zod** 或其他运行时 schema 库,使持久化和插件边界拥有运行时 schema 而非被擦除的类型。 -本 RFC 界定这一问题的范围,不提出具体实现。 +本 RFC 界定该问题的范围,不提出具体实现。 -## 为什么这不是一个持久化变更 +## 为什么这不是一个持久化层的改动 -很容易把「用 Zod 做序列化」理解为对 `dsh-session-persistence-jsonl/src/format.ts` 的局部改动。但它不是,原因在于一个结构性事实:**插件无法通过声明合并扩展一个 Zod schema。** 声明合并是 TypeScript 的编译期机制;Zod schema 是运行时值。要用 Zod 校验事件,你需要一个**运行时注册表**,每个产出事件的包向其贡献自己的 schema(如 `ctx.sessionEvents.register('compaction/marker', z.object({…}))`),每个消费方从中读取。这个注册表——而非持久化后端——将成为词汇的真源,取代 merge-extensible interface。 +很容易把「用 Zod 做序列化」理解为对 `dsh-session-persistence-jsonl/src/format.ts` 的局部修改。但它不是,原因在于一个结构性事实:**插件无法对 Zod schema 进行声明合并。** 声明合并是 TypeScript 编译期机制;Zod schema 是运行时值。要用 Zod 校验事件,就需要一个**运行时注册表**,每个产出事件的包(package)向其贡献自己的 schema(如 `ctx.sessionEvents.register('compaction/marker', z.object({…}))`),每个消费方从中读取。这个注册表——而非持久化后端——将成为词汇的真源,取代 merge-extensible interface。 -因此真正的提案是:**用运行时 schema 注册表替换编译期的 merge-extensible-map 模式,覆盖全仓库。** 这是一次核心词汇的重新设计。 +因此,真正的提案是:**用运行时 schema 注册表替换编译期的 merge-extensible-map 模式,范围覆盖整个仓库。** 这是一次核心词汇的重新设计。 -## 影响范围(实测) +## 影响范围(已度量) 将事件/词汇表面迁移到运行时 schema,至少涉及: -- **六个 merge-extensible map**(约 370 行核心类型):`ContentBlockMap`、`MessageSourceMap`、`FinishReasonMap`(在 `dsh-llm` 中);`TurnTriggerMap`、`TurnEndReasonMap`、`SessionEventMap`(在 `dsh-session` 中)。 -- **约 10 个 `declare module` 扩展点**,分布在 `dsh-agent`、`dsh-agent-loop`、`dsh-bash`、`dsh-llm`、`dsh-session`、`dsh-session-persistence`、`dsh-system-prompt`、`dsh-tools` 中——每个都将从声明合并改为运行时 `register()` 调用。 -- **事件生产端**——agent loop 中 16 处 `session.append(...)` 调用点——形状不变,但现在在边界处被校验。 -- **约 7 个 switch 消费方**,按这些联合类型分支:`deriveMessages`(`dsh-session`)、`BlockAssembler`(`dsh-llm`)、`dsh-invariants` 插件、两个 LLM 适配器(`dsh-llm-deepseek`、`dsh-llm-pi-ai`)以及工具 schema 层(`dsh-tools`)。`assertNever` 对封闭联合的穷尽性 vs 对可扩展联合的 fall-through 约定(一条已文档化的 lint 规则)需要重新考量——运行时变体不具备静态穷尽性。 -- **`defineTool` 的 `InferArgs` DSL**(`dsh-tools`),它从编译期 schema 规格派生零强制转换的 `execute` 参数类型——这是当前方案的标杆用例。 -- **文档**:architecture.md(该模式被描述为基础性的)、[开发模式不变式](../../implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md),以及任何引用该模式的 RFC。 +- **六个 merge-extensible map**(约 370 行核心类型):`ContentBlockMap`、`MessageSourceMap`、`FinishReasonMap`(位于 `dsh-llm`);`TurnTriggerMap`、`TurnEndReasonMap`、`SessionEventMap`(位于 `dsh-session`)。 +- **约 10 处 `declare module` 扩展点**,分布在 `dsh-agent`、`dsh-agent-loop`、`dsh-bash`、`dsh-llm`、`dsh-session`、`dsh-session-persistence`、`dsh-system-prompt`、`dsh-tools` 各包中——每处都将从声明合并改为运行时 `register()` 调用。 +- **事件生产者**——agent loop(智能体循环)中 16 处 `session.append(...)` 调用——形状不变,但现在在边界处被校验。 +- **约 7 个 switch 消费方**,对这些联合类型进行分支:`deriveMessages`(`dsh-session`)、`BlockAssembler`(`dsh-llm`)、`dsh-invariants` 插件、两个 LLM(大语言模型)适配器(`dsh-llm-deepseek`、`dsh-llm-pi-ai`)以及工具 schema 层(`dsh-tools`)。`assertNever` 对封闭联合类型的穷举 vs 对可扩展联合类型的 fall-through 约定(一条已记录的 lint 规则)需要重新考量——运行时变体在静态层面不可穷举。 +- **`defineTool` 的 `InferArgs` DSL**(`dsh-tools`),它从编译期 schema 规范派生出零类型转换的 `execute` 参数类型——这是当前方案的标杆用例。 +- **文档**:architecture.md(该模式被描述为基础性的)、[dev-mode invariants](../../implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md),以及所有引用该模式的 RFC。 -这是一次仓库级别的词汇重新设计,不是持久化的实现细节。 +这是一次仓库级别的词汇重新设计,而非持久化的实现细节。 ## 曾考虑的替代方案 -### A. 维持现状——merge-extensible 类型 + 持久化边界的 `isJsonValue` -保留编译期模式。持久化继续使用不透明 JSON + 可序列化性守卫。插件通过声明合并扩展;事件*形状*的正确性由生产方负责,在编译期由 TypeScript 强制,在开发模式下由 `dsh-invariants` 插件的结构检查强制。 +### A. 维持现状——merge-extensible 类型 + 持久化边界处 `isJsonValue` +保留编译期模式。持久化继续使用不透明 JSON + 可序列化性守卫。插件通过声明合并扩展;事件*形状*的正确性由生产者负责,编译期由 TypeScript 保证,开发模式下由 `dsh-invariants` 插件的结构检查保证。 -- **优点**:零变更;插件扩展只需一行 `interface` 声明合并,具备完整类型推断且无运行时注册仪式;无新运行时依赖;`defineTool` DSL 与 `assertNever` 穷尽性保持正常工作。 +- **优点**:零变动;插件扩展只需一行 `interface` 增补,享有完整类型推断,无需运行时注册仪式;无新运行时依赖;`defineTool` DSL 与 `assertNever` 穷举继续工作。 - **缺点**:持久化边界和插件 seam 处无运行时结构校验;格式错误但仍为合法 JSON 的数据被延迟捕获。 -### B. 仅对 header/封闭形状做校验(schemastery),事件保持不透明 -仅对那些已有手写类型守卫的真正封闭形状加强校验——例如 JSONL 的 `HeaderLine` 守卫(`isHeaderLine`)——使用 **schemastery**(本仓库现有的 schema 库,已用于每个插件的 `static Config`)。merge-extensible 事件联合保持不变。 +### B. 仅对头部/封闭形状做校验(schemastery),事件仍为不透明 +仅对那些已有手写类型守卫的真正封闭形状加以收紧——例如 JSONL 的 `HeaderLine` 守卫(`isHeaderLine`)——使用 **schemastery**(仓库现有的 schema 库,已用于每个插件的 `static Config`)。merge-extensible 事件联合类型保持不变。 -- **优点**:改动小,契合既有约定(schemastery,非新库);用声明式 schema 替换封闭形状上的手写守卫;无核心重设计。 -- **缺点**:不解决事件数据的校验问题;仅固定的元数据记录得到改善。 +- **优点**:改动小,契合现有约定(schemastery,而非新库);用声明式 schema 替换封闭形状上的手写守卫;无核心重新设计。 +- **缺点**:不解决事件数据校验问题;仅固定的元数据记录得到改善。 ### C. 为整个词汇建立运行时 schema 注册表(Zod 或 schemastery) -用运行时注册表替换 merge-extensible map,生产方向其贡献 schema,持久化/消费方据其校验。 +用运行时注册表替换 merge-extensible map,生产者向其贡献 schema,持久化/消费路径据此校验。 -- **优点**:持久化边界与插件 seam 处有真正的运行时校验;单一真源;支持通用工具(自动生成文档、模糊测试、协议格式检查)。 -- **缺点**:上述完整影响范围;**Zod 目前不是直接依赖**(仅作为 `@earendil-works/pi-ai` 的传递依赖),本仓库选定的 schema 库是 **schemastery**——广泛引入 Zod 本身就是一个依赖决策;声明合并的人体工学(一行插件扩展、完整推断)被运行时注册 + 手动类型接线取代;`assertNever` 穷尽性保证弱化(运行时变体不具备静态穷尽性)。 +- **优点**:持久化边界和插件 seam 处获得真正的运行时校验;单一真源;可支撑通用工具(自动生成文档、模糊测试、协议格式检查)。 +- **缺点**:上述全部影响范围;**Zod 目前不是直接依赖**(仅作为 `@earendil-works/pi-ai` 的传递依赖),仓库选定的 schema 库是 **schemastery**——广泛引入 Zod 本身就是一个依赖决策;声明合并的人体工学(一行插件扩展、完整推断)被运行时注册 + 手动类型接线取代;`assertNever` 穷举保证弱化(运行时变体在静态层面不可穷举)。 ## 提案 -暂缓。如果需要在持久化边界做运行时校验,**方案 B**(用 schemastery 校验封闭的 header 与元数据形状)是既有约定内的适度步骤。**方案 C** 是一项架构决策,需要自己的实现 RFC,包括在 Zod 与 schemastery 之间做出选择。 +推迟。如果需要在持久化边界做运行时校验,**方案 B**(对封闭的头部和元数据形状使用 schemastery)是现有约定下的适度步骤。**方案 C** 是一个架构决策,需要自己的实现 RFC,其中包括 Zod 与 schemastery 之间的选择。 ## 验收标准 -- 方案 C 只能通过自己的实现 RFC 推进,绝不作为持久化的附带效果。 -- 如果采纳方案 B,封闭的 header/元数据形状(JSONL 的 `isHeaderLine` 守卫及同类)改用 schemastery 校验以替代手写守卫,merge-extensible map 保持不变。 +- 方案 C 只能通过自己的实现 RFC 推进,绝不能作为持久化的附带改动。 +- 如果采纳方案 B,封闭的头部/元数据形状(JSONL 的 `isHeaderLine` 守卫及同类)改用 schemastery 校验,替代手写守卫,merge-extensible map 保持不动。 ## 风险 -- 暂缓意味着事件 `data` 在持久化边界仍无结构校验:格式错误但仍为合法 JSON 的数据被延迟捕获,由消费方的 `switch` 处理——这是现状的代价,有意接受。 -- 如果方案 C 最终被采纳,人体工学损失是实际的:一行声明合并变为运行时注册加手动类型接线,`assertNever` 的静态穷尽性保证弱化。 +- 推迟意味着事件 `data` 在持久化边界处仍无结构校验:格式错误但仍为合法 JSON 的数据被延迟捕获,由消费方的 `switch` 兜底——这是现状的代价,有意接受。 +- 如果方案 C 最终被采纳,人体工学的损失是真实的:一行声明合并变为运行时注册加手动类型接线,`assertNever` 的静态穷举保证弱化。 ## 待解问题 -- 如果采用注册表,schema 库选 **schemastery**(已在依赖树中,已是配置 schema 库)还是 **Zod**(生态更丰富,目前仅为传递依赖)?同时维护两个 schema 库本身就是成本。 -- 能否采用混合方案:保留编译期推断(使 `defineTool` 和插件 DX 不受影响),同时为每个变体添加*可选*的运行时 schema,仅在持久化/协议边界校验而非每次进程内 append 时校验? -- `dsh-invariants` 插件在开发模式下是否已覆盖了足够多的运行时形状缺口,使得边界校验仅在面对真正不可信的输入(如重新加载被外部修改的日志)时才有必要? +- 如果采用注册表,库选 **schemastery**(已在仓库中,已作为配置 schema 库)还是 **Zod**(生态更丰富,目前仅为传递依赖)?同时维护两个 schema 库本身就是一种成本。 +- 能否采用混合方案:保留编译期推断(使 `defineTool` 和插件开发体验不受影响),同时为每个变体添加*可选*的运行时 schema,仅在持久化/协议边界校验,而非每次进程内 append 都校验? +- `dsh-invariants` 插件在开发模式下是否已覆盖了足够多的运行时形状缺口,使得边界校验仅在面对真正不可信输入(重新加载外部修改过的日志)时才有必要? diff --git a/docs/rfc/proposed/architecture/2026-06-20-generic-long-running-tool-runtime.i18n.yaml b/docs/rfc/proposed/architecture/2026-06-20-generic-long-running-tool-runtime.i18n.yaml index edc5a3604d..290a729be7 100644 --- a/docs/rfc/proposed/architecture/2026-06-20-generic-long-running-tool-runtime.i18n.yaml +++ b/docs/rfc/proposed/architecture/2026-06-20-generic-long-running-tool-runtime.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-20-generic-long-running-tool-runtime.md: ea773a651b5aeec87179aac2ed419f176486977f -2026-06-20-generic-long-running-tool-runtime.zh.md: d50eb6858c11b9c98817b24bfe40f4e2c780f4b9 +2026-06-20-generic-long-running-tool-runtime.zh.md: 25c1b282b19bb7da08b552485e348c23b11dcecf diff --git a/docs/rfc/proposed/architecture/2026-06-20-generic-long-running-tool-runtime.zh.md b/docs/rfc/proposed/architecture/2026-06-20-generic-long-running-tool-runtime.zh.md index d50eb6858c..25c1b282b1 100644 --- a/docs/rfc/proposed/architecture/2026-06-20-generic-long-running-tool-runtime.zh.md +++ b/docs/rfc/proposed/architecture/2026-06-20-generic-long-running-tool-runtime.zh.md @@ -1,43 +1,43 @@ -# RFC:提取通用的长时运行工具运行时 - -[English](2026-06-20-generic-long-running-tool-runtime.md) | 中文 +# RFC:提取通用的长时间运行工具运行时 Status: proposed +[English](2026-06-20-generic-long-running-tool-runtime.md) | 中文 + ## 问题 -bash 能力 seam 同时支持前台命令和长时运行的后台任务。后台支持体量不小:抽象执行器暴露 `start`、`get`、`ownerOf`、`list`、`readOutput`、`kill` 和 `onTaskDone`;本地执行器跟踪任务、增量读取、owner token、进程清理和完成监听器;模型侧看到三个工具(`bash`、`bash_output`、`bash_kill`);工具插件将完成通知注入回所属 agent 的会话。本地执行器用 owner token 隔离任务访问,因为可预测的全局 task id 会带来跨会话的读取/终止风险。 +bash 能力 seam 同时支持前台命令和长时间运行的后台任务。后台支持体量不小:抽象执行器暴露 `start`、`get`、`ownerOf`、`list`、`readOutput`、`kill` 和 `onTaskDone`;本地执行器负责跟踪任务、增量读取、owner token、进程清理和完成监听;模型侧看到三个工具(`bash`、`bash_output`、`bash_kill`);工具插件将完成通知注入回所属 agent(智能体)的会话。本地执行器用 owner token 隔离任务访问,因为可预测的全局 task id 会带来跨会话的读取/终止风险。 -[工具实操手册](../../../cookbook/adding-a-tool.md)已经指出了真正的设计异味:后台 bash 实质上是寄居在一个工具内部的通用长时运行工具基础设施。如果未来的工具也需要后台执行、轮询、终止、所有权和完成通知,这些语义不应藏在 `dsh-bash` 里。 +[工具实操手册](../../../cookbook/adding-a-tool.md)已经指出了真正的设计异味:后台 bash 本质上是寄居在单个工具内部的通用长时间运行工具基础设施。如果未来的工具也需要后台执行、轮询、终止、所有权和完成通知,这些语义不应藏在 `dsh-bash` 里。 ## 提案 -将长时运行任务的语义从 bash 上方抽出,放入一个与工具无关的运行时。bash 仍然能运行后台命令,但不再拥有 task id、ownership token、轮询、取消、完成通知以及模型侧「读取/终止此任务」命令等通用概念。 +将长时间运行任务的语义从 bash 上移到一个与工具无关的运行时中。bash 仍然能运行后台命令,但不再拥有 task id、ownership token、轮询、取消、完成通知以及模型侧「读取/终止此任务」命令等通用概念。 该运行时应拥有: -- 稳定的 task id 与 owner token,按调用方的会话/agent 键控。 -- 注册一个长时运行任务,附带增量输出的生产者和一个完成 promise。 +- 稳定的 task id 和 owner token,按调用方的会话/agent 做键。 +- 注册一个长时间运行任务,附带增量输出的生产者和一个完成 promise。 - 通用的 read/cancel/list 操作,对所有工具使用相同的跨会话授权规则。 - 向所属会话注入完成通知。 -- 待处理/运行中/已完成任务状态的展示钩子,bash 只提供命令特有的标签和输出格式化。 +- 针对 pending/running/completed 任务状态的展示钩子,bash 只提供命令特有的标签和输出格式化。 -`dsh-bash` 随后只保留 bash 特有的执行契约:将请求解析为命令规格、运行前台命令,或启动进程并将其流/进程句柄交给通用运行时。`dsh-tool-bash` 保留模型侧的命令工具,但后续操作变为通用的长时运行工具操作(或 bash 向其注册的共享工具层),而非定制的 `bash_output`/`bash_kill` 管道。 +`dsh-bash` 保留 bash 特有的执行契约:将请求解析为命令规格、运行前台命令,或启动进程并将其流/进程句柄交给通用运行时。`dsh-tool-bash` 保留模型侧的命令工具,但后续操作变为通用的长时间运行工具操作,或者 bash 向其注册的共享工具,而不是专属的 `bash_output`/`bash_kill` 管道。 ## 当前 seam 消费情况 -当前消费方划分清晰:`dsh-tool-bash` 使用完整的前台/后台 seam,而钩子桥接只使用前台的 `resolve` 和 `run`(带受信的 `stdin` 与 `env`)。`get` 和 `list` 仅在测试中使用;`BashTask.done` 仅在实现内部用于 dispose(资源释放),生产环境的完成通知走 `onTaskDone`。提取出的运行时应暴露单一的公开完成机制,保留钩子所需的简单前台路径,并决定后台的 `timeoutMs` 是否属于 `start`。如果运行时拥有进程 spawn,还应集中处理目前重复的凭证清洗逻辑。 +当前消费方划分清晰:`dsh-tool-bash` 使用完整的前台/后台 seam,而钩子桥接层只使用前台的 `resolve` 和 `run`(带受信的 `stdin` 和 `env`)。`get` 和 `list` 仅在测试中使用;`BashTask.done` 仅在实现内部用于 dispose(资源释放),生产环境的完成通知使用 `onTaskDone`。提取出的运行时应暴露一个公开的完成机制,保留钩子所需的简单前台路径,并决定后台的 `timeoutMs` 是否属于 `start`。如果它拥有进程 spawn 的职责,还应集中处理目前重复的凭证清洗逻辑。 ## 验收标准 -- bash 特有的包不再定义通用的任务注册表、owner-token 授权、轮询、取消或完成通知机制。 -- 一个共享的长时运行任务服务或工具层拥有这些语义,并作为未来任何具备后台能力的工具的文档化路径。 -- bash 的后台行为仍可通过共享层使用,测试证明跨会话隔离依然成立。 -- ACP 和快照 fixture(测试前置数据)通过共享的任务词汇渲染后台 bash,而非通过 bash 独有的生命周期语义。 -- [工具实操手册](../../../cookbook/adding-a-tool.md)将长时运行工具指向共享运行时,而非告诉每个工具自行发明任务协议。 +- bash 特有的包(package)不再定义通用的任务注册表、owner-token 授权、轮询、取消或完成通知机制。 +- 一个共享的长时间运行任务服务或工具层拥有这些语义,并被文档化为未来任何具备后台能力的工具的接入路径。 +- bash 后台行为仍可通过共享层使用,测试证明跨会话隔离依然成立。 +- ACP 和快照 fixture(测试前置数据)通过共享任务词汇渲染后台 bash,而非通过 bash 专属的生命周期语义。 +- [工具实操手册](../../../cookbook/adding-a-tool.md)将长时间运行工具指向共享运行时,而不是让每个工具自行发明任务协议。 ## 风险 -bash 包失去了对一个已经可用的后台任务实现的本地所有权,实施 PR 可能暂时搅动模型侧的工具名称或 transcript(文本记录)展示。如果最终结果是留下一份后台任务契约、而非让每个未来的长时运行工具克隆 bash 的私有协议,这种搅动是值得的。 +bash 包失去了对一个已经可用的后台任务实现的本地所有权,实现 PR(Pull Request)可能暂时搅动模型侧的工具名称或 transcript(文本记录)展示。如果最终结果是留下一份后台任务契约,而不是让每个未来的长时间运行工具克隆 bash 的私有协议,这种搅动是值得的。 <!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> diff --git a/docs/rfc/proposed/feature/2026-06-30-pre-tool-input-rewrite.i18n.yaml b/docs/rfc/proposed/feature/2026-06-30-pre-tool-input-rewrite.i18n.yaml index 2c12df37b1..ccab930c8f 100644 --- a/docs/rfc/proposed/feature/2026-06-30-pre-tool-input-rewrite.i18n.yaml +++ b/docs/rfc/proposed/feature/2026-06-30-pre-tool-input-rewrite.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-30-pre-tool-input-rewrite.md: add84bfc76434eb25870f09e71860279663d291e -2026-06-30-pre-tool-input-rewrite.zh.md: c636cf42d5c55f0ee1192a46283f8cdbe8c4cadc +2026-06-30-pre-tool-input-rewrite.zh.md: 13d66208be07992fd414f2984c493557ad1e87a3 diff --git a/docs/rfc/proposed/feature/2026-06-30-pre-tool-input-rewrite.zh.md b/docs/rfc/proposed/feature/2026-06-30-pre-tool-input-rewrite.zh.md index c636cf42d5..13d66208be 100644 --- a/docs/rfc/proposed/feature/2026-06-30-pre-tool-input-rewrite.zh.md +++ b/docs/rfc/proposed/feature/2026-06-30-pre-tool-input-rewrite.zh.md @@ -1,53 +1,53 @@ -# RFC:工具执行前输入改写——一致性设计 - -[English](2026-06-30-pre-tool-input-rewrite.md) | 中文 +# RFC:工具执行前输入重写——一致性设计 Status: proposed +[English](2026-06-30-pre-tool-input-rewrite.md) | 中文 + ## 问题 -[拦截 seam RFC](../../implemented/feature/2026-06-30-interception-seams.md) 将 `tools/pre-execute` 定义为一道 allow/deny/ask 门禁,作用于身份已受保护、参数已被深度冻结的执行对象。Claude Code 的 `PreToolUse` 钩子还提供了 `updatedInput`,因此忠实的桥接需要一个显式的改写机制。改写不能是对现有执行对象的可变逃逸口:它必须保持持久化历史、审计记录、展示层与实际执行值之间的一致性。 +[拦截 seam RFC](../../implemented/feature/2026-06-30-interception-seams.md) 将 `tools/pre-execute` 定义为一道针对执行的允许/拒绝/询问门禁,此时执行的身份标识已受保护、参数已被深度冻结。Claude Code 的 `PreToolUse` 钩子还提供了 `updatedInput`,因此忠实的桥接需要一个显式的重写机制。重写不能是对现有执行对象的可变逃逸口:它必须保持持久化历史、审计记录、展示层与实际执行值之间的一致性。 ## 问题本质:执行前参数的三个读取方 -在 agent loop(智能体循环)中,工具调用的参数在工具执行之前就已被提交到日志并被活跃消费方读取: +在 agent loop(智能体循环)中,工具调用的参数在工具执行**之前**就已提交到日志并被实时消费方读取: -1. **`assistant/message`** 在工具分发之前追加——它是 `deriveMessages()` 回放时的模型历史来源,因此携带的是模型自身生成的工具调用参数。 +1. **`assistant/message`** 在工具分发之前追加——它是 `deriveMessages()` 回放时的模型历史来源,因此携带模型自身输出的工具调用参数。 2. **`tool/call`** 是持久化的审计记录,在 `ctx.tools.execute()` 之前追加。 -3. **展示层实时读取 `tool/call.arguments`**:ACP 桥接会记住这些参数并传给 `presentResult`;`dsh-tool-bash` 从中派生卡片标题、rawInput、cwd 以及终端/后台的处理方式。 +3. **展示层实时读取 `tool/call.arguments`**:ACP(Agent Client Protocol)桥接记住这些参数并传给 `presentResult`;`dsh-tool-bash` 从中派生卡片标题、rawInput、cwd 以及终端/后台处理方式。 -如果只做执行层面的改写,UI 会展示一条命令而实际运行的是另一条,并且结果会对着错误的参数渲染。注册表目前阻止了这种失败模式:它对 `arguments` 做 structured-clone 并深度冻结,将执行身份属性设为不可写,且不暴露任何可替换它们的测试 shim 或监听路径。改写设计必须保持这一受保护的身份边界,而非削弱它。 +如果只做执行层面的重写,UI 会显示一条命令而实际运行的是另一条,并且结果会对着错误的参数渲染。注册表目前通过以下方式防止这种失败模式:对 `arguments` 做 structured-clone 并深度冻结,将执行身份属性设为不可写,且不暴露任何可替换它们的测试 shim 或监听路径。重写设计必须维护这一受保护的身份边界,而非削弱它。 ## 提案 -改写是一次「身份构造前的一致性事务」。当钩子提供 `updatedInput` 时,有效值必须在注册表构造不可变的 `ToolExecution` 之前确定,并原子性地反映到全部三个读取方: +重写是一个「身份标识创建前的一致性事务」。当钩子提供 `updatedInput` 时,有效值必须在注册表构造其不可变的 `ToolExecution` 之前确定,并且必须原子地反映到全部三个读取方: -- `tool/call` 审计事件记录**改写后**的参数(原始参数保留在一个 sidecar 字段中用于审计追踪——钩子改变了调用,原始参数和生效参数都是值得保留的事实)。 -- 派生历史中的 `assistant/message` 必须与实际执行一致——待评估的选项:就地改写 assistant 消息中的工具调用块(改变模型「看到自己说过的话」),或记录一条单独的修正由下一次请求携带。CC 的模型是让模型看到改写已生效。 -- 展示层(`presentCall`/`presentResult`)读取改写后的参数,UI 展示的是实际运行的内容。 +- `tool/call` 审计事件记录**重写后**的参数(原始参数保留在一个伴随字段中,作为审计线索——钩子修改了调用,原始参数与生效参数都是值得保留的事实)。 +- 派生历史中的 `assistant/message` 必须与实际执行一致。待评估的选项:就地重写 assistant 消息中的工具调用块(改变模型「看到自己说了什么」),或记录一条单独的修正让下一次请求携带。Claude Code 的模型是让模型看到重写已生效。 +- 展示层(`presentCall`/`presentResult`)读取重写后的参数,使 UI 显示实际运行的内容。 -在 `PreToolDecision` 当前的触发点上做扩展不够:此时两条持久化记录都已存在,执行身份已受保护。实现必须要么将相关决策移到日志提交之前,要么在待处理的模型调用上增加一个专门的更早期改写决策。当循环将生效参数提交到历史和审计之后,再按常规构造不可变执行对象,并照常运行现有的 allow/deny/ask 与工具流水线。 +在 `PreToolDecision` 当前的触发点上做扩展是不够的:此时两条持久化记录已经存在,执行身份已受保护。实现必须将相关决策移到日志提交之前,或者增加一个专门的、更早的重写决策点来处理待定的模型调用。agent loop 将生效参数提交到历史和审计之后,再构造普通的不可变执行对象,并照常运行现有的允许/拒绝/询问和工具流水线。 ## 曾考虑的替代方案 ### 为什么不直接修改执行对象? -允许 pre-execute 监听器赋值 `exec.arguments` 只能提供执行层面的改写,模型历史、审计和展示层不会跟着变。保持身份受保护使得这种局部行为无法被表达。在一致性事务实现之前,CC/Codex 桥接对 `updatedInput` 只做日志记录并发出警告,而非声称已兑现;循环分发处的 `TODO(pre-tool-input-rewrite)` 锚定了这个缺失的更早阶段。 +允许 pre-execute 监听器赋值 `exec.arguments` 只能提供执行层面的重写,模型历史、审计和展示层不会随之改变。保持身份标识受保护使得这种局部行为不可表达。在一致性事务实现之前,CC/Codex 桥接对 `updatedInput` 记录日志并发出警告,而非声称已兑现;循环分发点的 `TODO(pre-tool-input-rewrite)` 标记了缺失的更早阶段。 ## 验收标准 -- 请求的改写在 `ToolExecution` 身份创建之前完成解析,并原子性地反映到全部三个读取方:`tool/call` 审计记录改写后的参数(原始参数保留在 sidecar 字段)、派生历史与实际执行一致、展示层渲染改写后的参数。 -- 生效的 `ToolExecution.arguments` 在 pre-policy、guards、dispatch、post-policy 和最终观测的全过程中保持深度冻结且不可写;不引入任何可变 shim。 -- CC/Codex 桥接兑现 `updatedInput`,不再输出忠实但降级的警告。 +- 请求的重写在 `ToolExecution` 身份标识创建之前解决,并原子地反映到全部三个读取方:`tool/call` 审计记录重写后的参数(原始参数保留在伴随字段中)、派生历史与实际执行一致、展示层渲染重写后的参数。 +- 生效的 `ToolExecution.arguments` 在 pre-policy、守卫、分发、post-policy 和最终观测全程保持深度冻结且不可写;不引入任何可变 shim。 +- CC/Codex 桥接兑现 `updatedInput`,不再记录忠实但降级的警告。 ## 风险 -- 改写 `assistant/message` 中的工具调用块会改变模型「看到自己说过的话」;是否有提供方在回放时拒绝这种改写,是一个必须在决策形态冻结前通过实验验证的开放问题。 -- 更早期的改写阶段改变了 `assistant/message`、`tool/call`、钩子审计事件与执行之间的顺序关系;设计必须固定这一顺序,同时不削弱轮次封闭性或 call/result 邻接性。 +- 重写 `assistant/message` 中的工具调用块会改变模型「看到自己说了什么」;是否有提供方在回放时拒绝这种改动,是一个需要通过实验确定的开放问题,必须在决策形状冻结之前解决。 +- 更早的重写阶段改变了 `assistant/message`、`tool/call`、钩子审计事件与执行之间的顺序关系;设计必须固定这一顺序,同时不削弱轮次封闭性或调用/结果邻接性。 ## 开放问题 -- 改写 `assistant/message` 中的工具调用块是否会破坏某些提供方在回放时的预期?还是记录一条单独的修正更安全? -- 原始参数是否应保留在 `tool/call` 事件(审计)上?如果是,放在哪个字段? -- 改写决策是移到日志提交之前,还是成为一个专门的更早期 seam?现有的 pre-tool allow/deny 钩子如何避免运行两次? -- 这与未来的权限 `ask` 流程(用户批准一个被改写的调用)如何交互? +- 重写 `assistant/message` 中的工具调用块是否会破坏某些提供方在回放时的预期?还是单独的修正更安全? +- 原始参数是否应保留在 `tool/call` 事件(审计)上?如果是,放在什么字段? +- 重写决策是移到日志提交之前,还是成为一个专门的更早 seam?现有的 pre-tool 允许/拒绝钩子如何避免运行两次? +- 这与未来的权限 `ask` 流程(用户批准一个被重写的调用)如何交互? diff --git a/docs/rfc/proposed/feature/2026-07-07-claude-code-and-codex-subagent-backends.i18n.yaml b/docs/rfc/proposed/feature/2026-07-07-claude-code-and-codex-subagent-backends.i18n.yaml index d7b4dcf71f..ab362ca036 100644 --- a/docs/rfc/proposed/feature/2026-07-07-claude-code-and-codex-subagent-backends.i18n.yaml +++ b/docs/rfc/proposed/feature/2026-07-07-claude-code-and-codex-subagent-backends.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-07-claude-code-and-codex-subagent-backends.md: 1ebf01dd8df0980f6c464be8b27033bdfab942f3 -2026-07-07-claude-code-and-codex-subagent-backends.zh.md: 0ed5b42bc9d60b54ac610a8f34f8261f3aeefaed +2026-07-07-claude-code-and-codex-subagent-backends.zh.md: dd26a49962ee46a8ce0965557ff3dbd5805fdcfe diff --git a/docs/rfc/proposed/feature/2026-07-07-claude-code-and-codex-subagent-backends.zh.md b/docs/rfc/proposed/feature/2026-07-07-claude-code-and-codex-subagent-backends.zh.md index 0ed5b42bc9..dd26a49962 100644 --- a/docs/rfc/proposed/feature/2026-07-07-claude-code-and-codex-subagent-backends.zh.md +++ b/docs/rfc/proposed/feature/2026-07-07-claude-code-and-codex-subagent-backends.zh.md @@ -1,4 +1,4 @@ -# RFC:Claude Code 与 Codex subagent 后端(进程外委派至外部编码 agent) +# RFC:Claude Code 与 Codex subagent 后端(向外部编码 agent 的进程外委派) [English](2026-07-07-claude-code-and-codex-subagent-backends.md) | 中文 @@ -6,84 +6,84 @@ Status: proposed ## 问题 -为 Claude Code 和 Codex 添加隔离的 subagent 提供方。既有的[命名提供方 seam](../../implemented/feature/2026-06-21-subagent-capability-seam.md) 和 [ACP 后端](../../implemented/feature/2026-06-22-acp-subagent-backend.md)已确立了进程边界的形状。harness 的一个轮次应当能够将一个自包含的任务委派给上述任一产品,并接收其最终回答,同时不暴露父进程的密钥,也不继承来自 `~/.claude` 或 `~/.codex` 的宿主配置。 +为 Claude Code 和 Codex 添加隔离的 subagent 提供方。既有的[命名提供方 seam](../../implemented/feature/2026-06-21-subagent-capability-seam.md) 和 [ACP 后端](../../implemented/feature/2026-06-22-acp-subagent-backend.md)已确立了进程边界的形状。harness 的一个轮次应能将一个自包含任务委派给上述任一产品,并接收其最终答案,同时不暴露父进程的密钥,也不继承来自 `~/.claude` 或 `~/.codex` 的宿主配置。 -## 方案 +## 提案 两个兄弟提供方包(ACP 后端的结构变体),加一次提取: -- `@deepseek-ai/dsh-subagent-claude-code`:通过 `@anthropic-ai/claude-agent-sdk` 的 `query()` 驱动一个 Claude Code 子进程(SDK 在父进程中运行,并将其捆绑的 `claude` CLI 作为子进程 spawn)。提供方名称 `claude-code`:子进程是 Claude Code 这个**产品**,而非 Anthropic 模型适配器——"claude" 保留给未来的 `dsh-llm` 适配器。 -- `@deepseek-ai/dsh-subagent-codex`:spawn `codex app-server`,通过其 JSON-RPC-over-stdio 协议,使用包内一个手写的换行 JSON 客户端(约 200–300 行)驱动一个 thread/turn。 -- `@deepseek-ai/dsh-subagent-process`:纯库(`subagent-inprocess` 先例),提取 `dsh-subagent-acp` 已有且两个新后端都需要的内容:凭证环境清洗(`SENSITIVE_ENV_PATTERN`/`buildChildEnv`)、EOF → SIGTERM → SIGKILL 的 dispose 阶梯,以及新的隔离配置目录辅助函数(`mkdtemp` 创建、尽力删除)。ACP 后端迁移到该库上;`bash-local` 的兄弟副本保持不动以限制变更范围。 +- `@deepseek-ai/dsh-subagent-claude-code`:通过 `@anthropic-ai/claude-agent-sdk` 的 `query()` 驱动一个 Claude Code 子进程(SDK 在父进程中运行,并将其内置的 `claude` CLI 作为子进程 spawn)。提供方名称为 `claude-code`:子进程是 Claude Code 这个**产品**,而非 Anthropic 模型适配器——"claude" 保留给未来的 `dsh-llm` 适配器。 +- `@deepseek-ai/dsh-subagent-codex`:spawn `codex app-server`,通过其 JSON-RPC-over-stdio 协议驱动一个 thread/turn,使用包内一个手写的换行 JSON 客户端(约 200–300 行)。 +- `@deepseek-ai/dsh-subagent-process`:纯库(沿用 `subagent-inprocess` 的先例),提取 `dsh-subagent-acp` 已有且两个新后端都需要的内容:凭证环境清洗(`SENSITIVE_ENV_PATTERN`/`buildChildEnv`)、EOF → SIGTERM → SIGKILL 的 dispose 阶梯,以及新的隔离配置目录辅助函数(`mkdtemp` 创建、尽力删除)。ACP 后端迁移到该库上;`bash-local` 的兄弟副本保持不动以限制变更范围。 -两个提供方遵循 ACP 后端契约:每次 `start` 创建一个全新子进程、一次 prompt 往返、不继承父上下文也不声明可选能力、忽略 `request.parent` 和 `request.agentOptions`、使用随机品牌 agent id。`result` 永不 reject;子进程失败映射为 stop reason,原始错误送入 logger。每个提供方在不同的工具名下挂载 `dsh-tool-subagent`。工具结果是唯一新增的模型可见产物,因此不需要新的会话事件;工作区变更仍是 transcript 回放之外的环境副作用。 +两个提供方遵循 ACP 后端契约:每次 `start` 创建一个全新子进程、一次 prompt 往返、不继承父上下文也不声明可选能力、忽略 `request.parent` 和 `request.agentOptions`、使用随机的品牌化 agent id。`result` 从不 reject;子进程失败映射为 stop reason,原始错误送入 logger。每个提供方以不同的工具名挂载 `dsh-tool-subagent`。工具结果是唯一新增的模型可见产物,因此无需新的会话事件;工作区变更仍是 transcript(文本记录)回放之外的环境副作用。 ## 已验证的接口事实(固定版本) -两个集成面在本提案之前均已针对固定实现进行了验证——读取类型与捆绑源码、运行 keyless spike——而非仅依赖厂商文档。固定版本是验证基线,不是运行时契约:后端不执行运行时版本探测(无 `codex --version` 门控、无 SDK 版本嗅探)。兼容性在开发时强制执行——每次依赖升级都重跑 keyless 套件以验证真实加载路径——运行时则通过大声失败来保障:协议层的意外通过 `onError` 结算为 `error`,绝不静默异常。 +两个集成面在本提案之前均已针对固定版本进行了验证——阅读类型与打包源码、运行无需密钥的 spike——而非仅依赖厂商文档。固定版本是验证基线,不是运行时契约:后端不执行运行时版本探测(无 `codex --version` 门禁、无 SDK 版本嗅探)。兼容性在开发时强制执行——每次依赖升级都会针对真实加载路径重跑无密钥套件——在运行时则通过大声失败来保障:协议层面的意外通过 `onError` 结算为 `error`,绝不静默异常。 -**`@anthropic-ai/claude-agent-sdk` 0.3.202。** `options.env` 会**替换**子进程环境(不与 `process.env` 合并),这正是清洗所需的行为。`settingSources` 默认加载所有文件系统设置——隔离要求显式传入 `[]`。结果子类型为 `success` | `error_during_execution` | `error_max_turns` | `error_max_budget_usd` | `error_max_structured_output_retries`。中止时 SDK 自行升级 CLI 子进程:立即关闭 stdin,若子进程忽略则约 2 秒后发送 SIGTERM(已观察到;无残留进程)——无需自定义 kill 回退。`outputFormat: {type: 'json_schema'}` 和 `agents` 选项已存在,为 seam 的 `outputSchema` 能力和命名 subagent 类型提供了未来着陆点;二者均不在本 RFC 范围内。 +**`@anthropic-ai/claude-agent-sdk` 0.3.202。** `options.env` 会**替换**子进程环境(不与 `process.env` 合并),恰好满足清洗需求。`settingSources` 默认加载所有文件系统设置——隔离要求显式传入 `[]`。结果子类型为 `success` | `error_during_execution` | `error_max_turns` | `error_max_budget_usd` | `error_max_structured_output_retries`。中止时 SDK 自行升级 CLI 子进程:立即关闭 stdin,约 2 秒后若子进程未退出则发送 SIGTERM(已观察到;无残留进程)——无需自定义 kill 回退。`outputFormat: {type: 'json_schema'}` 和 `agents` 选项已存在,为 seam 的 `outputSchema` 能力和命名 subagent 类型提供了未来着陆点;两者均不在本 RFC 范围内。 **codex CLI 0.142.5,`codex app-server`(v2 词汇)。** LF 分隔的 JSON,JSON-RPC 2.0 形状但省略 `"jsonrpc"` 头。 - 生命周期:`initialize{clientInfo}` + `initialized` → `thread/start`(接受 `cwd`、`model`、`sandbox`、`approvalPolicy`、`ephemeral`;未认证即可成功)→ `turn/start{threadId, input:[{type:'text',text}]}` 立即返回一个 `inProgress` 的 turn;终止信号是携带 `Turn{status: completed|interrupted|failed|inProgress, error}` 的 `turn/completed` 通知。 -- 审批为服务端发起的请求——`item/commandExecution/requestApproval`、`item/fileChange/requestApproval`、`item/permissions/requestApproval`、`item/tool/requestUserInput`、`mcpServer/elicitation/request`——以 `accept`/`decline` 系列决策应答。 -- 认证:`account/login/start{type:'apiKey', apiKey}` 是一等 RPC,`account/read` 报告 `requiresOpenaiAuth`——且未认证的 `turn/start` 不会快速失败(它会挂在重试中),因此后端必须预检认证并大声结算 `error`,而非等待 turn。 -- 隔离:`CODEX_HOME` 重定向被尊重(`initialize` 响应会回显它,测试可据此断言隔离),且 `ephemeral: true` 的 thread 完全不留会话文件。 +- 审批是服务端发起的请求——`item/commandExecution/requestApproval`、`item/fileChange/requestApproval`、`item/permissions/requestApproval`、`item/tool/requestUserInput`、`mcpServer/elicitation/request`——以 `accept`/`decline` 系列决策应答。 +- 认证:`account/login/start{type:'apiKey', apiKey}` 是一等 RPC,`account/read` 报告 `requiresOpenaiAuth`——且未认证的 `turn/start` 不会快速失败(它会挂在重试中),因此后端**必须**预检认证状态,并在失败时大声结算为 `error`,而非等待 turn。 +- 隔离:`CODEX_HOME` 重定向被尊重(`initialize` 响应会回显它,测试可据此断言隔离),`ephemeral: true` 的 thread 不留任何会话文件。 ## 隔离与凭证 -认证仅使用 API key。每次运行使用一个全新的配置目录(Claude Code 用 `CLAUDE_CONFIG_DIR` 配合 `settingSources: []`,Codex 用 `CODEX_HOME`),dispose 时尽力删除;配置也可选择一个持久目录。共享的子进程环境辅助函数转发 `PATH`、`HOME`、`TMPDIR`、locale、代理设置等普通值,移除凭证形状的名称,并叠加显式的 `config.env`。Claude Code 通过该叠加接收 API key,Codex 则通过 `account/login/start` 接收,而非手写认证文件。 +认证方式仅限 API key。每次运行使用一个全新的配置目录(Claude Code 用 `CLAUDE_CONFIG_DIR` 配合 `settingSources: []`,Codex 用 `CODEX_HOME`),dispose 时尽力删除;配置也可以选择一个持久目录。共享的子进程环境辅助函数转发 `PATH`、`HOME`、`TMPDIR`、locale 和代理设置等普通值,移除凭证形态的名称,并叠加显式的 `config.env`。Claude Code 通过该叠加接收 API key,而 Codex 通过 `account/login/start` 接收,而非手写认证文件。 ## 权限与审批策略 -每个后端暴露其引擎的原生策略词汇。Claude Code 默认 `permissionMode: default` 配合 `permission: reject`;Codex 默认 `sandboxMode: read-only`、`approvalPolicy: never`,以及相同的拒绝回退。示例可选择启用 `acceptEdits` 或 `workspace-write`。已知的审批、用户输入和 elicitation 请求接收配置的应答;未知方法接收 method-not-found,未知通知被消费。没有 prompt 到达人类,子进程也不会因等待不可用的输入而无限挂起。 +每个后端暴露其引擎原生的策略词汇。Claude Code 默认 `permissionMode: default` 配合 `permission: reject`;Codex 默认 `sandboxMode: read-only`、`approvalPolicy: never`,以及相同的拒绝回退。示例可选择启用 `acceptEdits` 或 `workspace-write`。已知的审批、用户输入和 elicitation 请求接收配置的应答;未知方法接收 method-not-found,未知通知被消费。没有 prompt 到达人类,子进程也不会因等待不可用的输入而无限挂起。 ## StopReason 映射 -Claude Code:`success` → `completed`;`error_max_turns`、`error_during_execution`、`error_max_budget_usd`、`error_max_structured_output_retries` → `error`(与 ACP 对 `max_turn_requests` 的处理对齐:未完成的任务不算成功);生成器中止 → `aborted`;未知值 → `error`。Codex:`Turn.status` `completed` → `completed`;`interrupted` → `aborted`;`failed` 且 `codexErrorInfo: 'contextWindowExceeded'` → `max-tokens`,其他 `failed` → `error`;传输/spawn/认证预检失败 → `error`(若已请求取消则为 `aborted`)。两者中,`cancel()` 采用 ACP 形状:标志位 + abort/interrupt + 一个 cancel-settled 竞争分支,使不合作的子进程无法阻塞结果。 +Claude Code:`success` → `completed`;`error_max_turns`、`error_during_execution`、`error_max_budget_usd`、`error_max_structured_output_retries` → `error`(与 ACP 对 `max_turn_requests` 的处理对齐:未完成的任务不是成功);生成器中止 → `aborted`;未知值 → `error`。Codex:`Turn.status` 为 `completed` → `completed`;`interrupted` → `aborted`;`failed` 且 `codexErrorInfo: 'contextWindowExceeded'` → `max-tokens`,其他 `failed` → `error`;传输/spawn/认证预检失败 → `error`(若已请求取消则为 `aborted`)。两者中,`cancel()` 采用 ACP 形状:标志位 + abort/interrupt + 一个 cancel-settled 竞争分支,使不合作的子进程无法阻塞结果。 -活性姿态,明确声明:teardown 时序是配置项,turn 时长不是。两个后端将 dispose 阶梯的宽限期作为带默认值的已验证配置字段(ACP 后端的 `disposeEofGraceMs`/`disposeGraceMs` 形状,由提取库承载),但刻意**不设** turn 时长或启动超时——与 ACP 一致:turn 期间的活性由调用方通过 `cancel()`/abort signal 掌控,subagent turn 合理地可达数分钟,且 Codex 认证预检已消除了唯一经验证的必然挂起场景;需要墙钟上限的部署从父进程取消即可。 +活性姿态,明确声明:teardown 时序是配置项,turn 时长不是。两个后端将 dispose 阶梯的宽限期作为带默认值的已验证配置字段(ACP 后端的 `disposeEofGraceMs`/`disposeGraceMs` 形状,由提取库承载),但**刻意不设** turn 时长或启动超时——与 ACP 一致:turn 期间的活性由调用方通过 `cancel()`/abort signal 掌控,subagent turn 合理地可达数分钟,而 Codex 认证预检消除了唯一已验证的必然挂起场景;需要墙钟上限的部署从父侧取消即可。 ## 测试 每个适用层级都要求覆盖: -- **Keyless 单元/集成:** 通过真实 SDK 驱动一个假 Claude CLI,通过真实 wire 客户端驱动一个脚本化的 Codex app-server。在逐文件 100% 覆盖率下,覆盖往返、每个 stop 映射、两条取消路径及预中止、权限策略、未知消息、spawn 失败、reload 清理、导出形状、清洗后的环境、临时目录删除,以及 Codex 认证预检失败。 -- **带 key 的 e2e:** 每个真实引擎在 `acceptEdits` 或 `workspace-write` 下执行文件操作;跳过时命名缺失的二进制文件或 key,并断言无残留子进程。 -- **快照:** 以 `TODO(claude-code-subagent-replay)` 和 `TODO(codex-subagent-replay)` 延后,等待 [subagent 回放 RFC](../../implemented/testing/2026-06-22-subagent-snapshot-replay.md) 描述的进程特定回放形状。 +- **无密钥单元/集成测试:** 通过真实 SDK 驱动一个假 Claude CLI,通过真实协议客户端驱动一个脚本化的 Codex app-server。在逐文件 100% 覆盖率下,验证往返、每种 stop 映射、两条取消路径及预中止、权限策略、未知消息、spawn 失败、reload 清理、导出形状、清洗后的环境、临时目录删除,以及 Codex 认证预检失败。 +- **有密钥 e2e 测试:** 每个真实引擎在 `acceptEdits` 或 `workspace-write` 下执行文件操作;跳过时命名缺失的二进制或密钥,并断言无残留子进程。 +- **快照测试:** 标记为 `TODO(claude-code-subagent-replay)` 和 `TODO(codex-subagent-replay)` 推迟,等待 [subagent 回放 RFC](../../implemented/testing/2026-06-22-subagent-snapshot-replay.md) 描述的进程特定回放形状。 ## 曾考虑的替代方案 -### 为什么不用官方 `@openai/codex-sdk` 而是手写客户端? +### 为什么不用官方 `@openai/codex-sdk` 而手写客户端? -dispose 阶梯和环境清洗要求拥有子进程(spawn 参数、env、信号、exit 等待);SDK 隐藏了进程。协议格式极其简单(LF JSON),形状可按固定版本生成(`codex app-server generate-json-schema`),且仓库先例(`hook-protocol`)是自有精简协议核心而非包装他人运行时。SDK 能节省协议演进的维护成本,但代价是失去本后端存在的意义所在的精确控制。 +dispose 阶梯和环境清洗要求拥有子进程(spawn 参数、env、信号、exit 等待);SDK 隐藏了进程。协议格式极其简单(LF JSON),形状可按固定版本生成(`codex app-server generate-json-schema`),仓库先例(`hook-protocol`)是拥有薄协议核心而非包装他人的运行时。SDK 能节省协议演进的维护成本,但代价是失去本后端存在的意义所在的精确控制。 ### 为什么不用模型可见的 `subagent_type` 参数(单一 Task 风格工具)? -Claude Code 自己的 Task 工具将 subagent 类型放在模型可见的 schema 中,选择一个 prompt + 工具集人格。这里的选择是在**执行引擎**之间做出的,而只有部署者知道哪些引擎配置了凭证——因此选择留在部署配置中,保持 `dsh-tool-subagent` 文档化的一提供方一工具契约。人格式的类型选择器应当是针对工具的独立 RFC,而非后端。 +Claude Code 自身的 Task 工具将 subagent 类型放在模型可见的 schema 中,选择一个 prompt + 工具集人格。这里的选择是在**执行引擎**之间做出的,而只有部署者知道哪些引擎配置了凭证——因此选择留在部署配置层,保持 `dsh-tool-subagent` 文档中的「一个提供方对应一个工具」契约。人格风格的类型选择器应是针对工具的另一个 RFC,而非针对后端。 -### 为什么不用登录态凭证和用户自己的配置? +### 为什么不用登录态凭证和用户自身的配置? -继承 `~/.claude` / `~/.codex`(订阅登录、用户设置、skill、MCP 服务器)会让子进程行为依赖宿主机状态,并在 ACP 后端和 bash 执行器确立的「凭证通过 `config.env` 显式进入,绝不隐式继承」规则上打一个隐式例外。仅 API key 加强制配置目录隔离保持了运行的可复现性;需要共享状态的部署可以刻意将配置目录字段指向一个持久目录。 +继承 `~/.claude` / `~/.codex`(订阅登录、用户设置、skill、MCP 服务器)会使子进程行为依赖宿主机状态,并在 ACP 后端和 bash 执行器确立的「凭证通过 `config.env` 显式进入,绝不隐式继承」规则上打开一个隐式例外。仅 API key 加强制配置目录隔离使运行可复现;需要共享状态的部署可以有意将配置目录字段指向一个持久目录。 -### 为什么不为 Claude Code keyless 测试注入一个驱动 seam? +### 为什么不为 Claude Code 无密钥测试注入驱动层 seam? -注入一个假 `query()` 会 mock 我们自己的边界,使真实 SDK 加载路径未被测试(docs/testing.md 中的 real-over-mock 策略)。曾考虑此方案的风险——SDK↔CLI 的 stream-json 控制协议是内部的——已被 spike 消除:假 CLI harness 今天能对着真实固定版本的 SDK 工作。如果 SDK 升级破坏了 mock,keyless 套件会让升级 PR 失败,这正是门禁在发挥作用。 +注入假的 `query()` 会 mock 我们自己的边界,使真实 SDK 加载路径未被测试(docs/testing.md 中的 real-over-mock 策略)。曾考虑此方案的风险——SDK↔CLI 的 stream-json 控制协议是内部实现——已被 spike 消除:假 CLI harness 今天能对真实固定版本的 SDK 正常工作。如果 SDK 升级破坏了 mock,无密钥套件会让升级 PR 失败,这正是门禁在发挥作用。 ### 为什么不用 ACP 适配器(如 `claude-code-acp`)复用既有后端? -社区 shim 将两个引擎包装为 ACP,这会让它们在 `dsh-subagent-acp` 上变成「仅配置」。但这在 harness 与引擎之间插入了一个非官方第三方层,抹掉了本 RFC 暴露的原生控制面(permissionMode、sandboxMode/approvalPolicy、配置目录隔离、apiKey RPC),并以 shim 的发布节奏换取第一方协议的稳定性。第一方接口——Agent SDK 和 app-server——才是受支持的集成点。 +社区 shim 将两个引擎包装为 ACP,这会使它们在 `dsh-subagent-acp` 上变成「仅配置」。但这在 harness 与引擎之间插入了一个非官方的第三方层,抹去了本 RFC 暴露的原生控制面(permissionMode、sandboxMode/approvalPolicy、配置目录隔离、apiKey RPC),并以 shim 的发布节奏替换了第一方协议的稳定性。第一方接口——Agent SDK 和 app-server——才是受支持的集成点。 ## 验收标准 -在同时配置了两个引擎和 key 的机器上:一个 REPL 驱动的模型通过 `subagent_claude_code` 完成一个真实文件任务,通过 `subagent_codex` 完成另一个,工具结果为子进程的最终回答,父会话日志中仅有 `tool/call` + `tool/result`。Keyless 套件在无凭证环境中以逐文件 100% 覆盖率通过,断言隔离(清洗后的子进程 env、dispose 后无残留临时配置目录)以及 `~/.claude` / `~/.codex` 的存在与否不影响子进程行为。取消父轮次后,两个后端在有界时间内静默,无残留子进程。e2e 套件干净地自跳过,命名缺失的前置条件。 +在两个引擎和密钥均已配置的机器上:一个 REPL 驱动的模型通过 `subagent_claude_code` 完成一个真实文件任务,通过 `subagent_codex` 完成另一个,工具结果为子进程的最终答案,父会话日志中仅有 `tool/call` + `tool/result`。无密钥套件在无凭证环境下以逐文件 100% 覆盖率通过,断言隔离(清洗后的子进程环境、dispose 后无残留临时配置目录),并断言 `~/.claude` / `~/.codex` 的存在与否不影响子进程行为。取消父轮次后,两个后端在有界时间内静默,无残留子进程。e2e 套件干净地自跳过,命名缺失的前置条件。 ## 风险 -- `codex app-server` 以 CLI flag 标记为实验性,其 v1/v2 词汇共存;客户端固定 0.142.5、仅实现 v2、消费未知方法/通知而不崩溃,但未来 codex 升级仍可能迫使返工(每次升级重新生成 schema 并重跑 keyless 套件——这是上述「不做运行时版本探测」立场背后的开发时强制执行)。 -- Claude Code 假 CLI mock 依赖一个内部协议:任何 SDK 升级都必须通过 keyless 套件,控制协议的破坏性变更意味着返工 mock(回退方案:上面否决的驱动注入 seam 成为逃生口)。 -- SDK 的 optionalDependencies 每平台约 280MB——已接受,且限制在单个后端包内。 -- SDK 的 SIGKILL 分支(EOF→SIGTERM 之后)未被观察到,信任其存在;e2e 保留无残留进程断言。 -- Codex 是部署前置条件(无 npm 捆绑的二进制文件);缺失或不兼容的二进制文件表现为大声的 spawn/协议 `error`,而非版本探测。 -- 每次运行付出一个全新子进程的代价,且仅最终回答浮出——思考、工具卡片和用量被消费后丢弃;池化、中间进度浮出、`sendMessage`/`resume`、通过 SDK 的 `outputFormat` 实现 `outputSchema`、以及通过 SDK 的 `agents` 选项实现命名 subagent 类型,均为刻意延后。 +- `codex app-server` 被 CLI 标记为实验性,其 v1/v2 词汇共存;客户端固定 0.142.5、仅实现 v2、对未知方法/通知消费而不崩溃,但未来 codex 升级仍可能迫使返工(每次升级重新生成 schema 并重跑无密钥套件——这是上述「不做运行时版本探测」立场背后的开发时强制执行)。 +- Claude Code 假 CLI mock 依赖一个内部协议:任何 SDK 升级都必须通过无密钥套件,控制协议的破坏性变更意味着返工 mock(回退方案:上面否决的驱动注入 seam 成为逃生舱口)。 +- SDK 的 optionalDependencies 每平台约 280MB——已接受,限制在单个后端包内。 +- SDK 的 SIGKILL 分支(EOF→SIGTERM 之后)未被观察到,信任其实现;e2e 保留无残留进程断言。 +- Codex 是部署前置条件(无 npm 内置二进制);缺失或不兼容的二进制以大声的 spawn/协议 `error` 呈现,而非版本探测。 +- 每次运行付出一个全新子进程的代价,且仅最终答案浮出——思考、工具卡片和用量被消费后丢弃;连接池、中间进度浮出、`sendMessage`/`resume`、通过 SDK 的 `outputFormat` 实现 `outputSchema`、以及通过 SDK 的 `agents` 选项实现命名 subagent 类型,均为刻意推迟。 diff --git a/docs/rfc/proposed/feature/2026-07-08-interactive-side-sessions.i18n.yaml b/docs/rfc/proposed/feature/2026-07-08-interactive-side-sessions.i18n.yaml index d9853d91fe..a86bcca889 100644 --- a/docs/rfc/proposed/feature/2026-07-08-interactive-side-sessions.i18n.yaml +++ b/docs/rfc/proposed/feature/2026-07-08-interactive-side-sessions.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-08-interactive-side-sessions.md: 250a906d9ec339399a0e0e29e70b2b8dc189fa72 -2026-07-08-interactive-side-sessions.zh.md: 17d416e2297320e8dfa238569230ecdec91dfa32 +2026-07-08-interactive-side-sessions.zh.md: d86a2b69232bc8ccad78555911b44cf727780e0c diff --git a/docs/rfc/proposed/feature/2026-07-08-interactive-side-sessions.zh.md b/docs/rfc/proposed/feature/2026-07-08-interactive-side-sessions.zh.md index 17d416e229..d86a2b6923 100644 --- a/docs/rfc/proposed/feature/2026-07-08-interactive-side-sessions.zh.md +++ b/docs/rfc/proposed/feature/2026-07-08-interactive-side-sessions.zh.md @@ -1,41 +1,41 @@ # RFC:交互式侧会话与合并回写 -Status: proposed - [English](2026-07-08-interactive-side-sessions.md) | 中文 +Status: proposed + ## 问题 -用户可能希望在不改变当前会话主上下文的前提下探索一个问题。现有原语无法提供这种产品形态:[session-store fork](../../implemented/feature/2026-06-30-session-store-fork-api.md) 创建的是一个无关联的会话,而 [fork subagent](../../implemented/feature/2026-06-21-subagent-capability-seam.md) 是模型驱动的任务,其 transcript(文本记录)会折叠为一条工具结果。两者都不能给用户一个独立的对话,也都不能将结论带着来源信息写回父会话。 +用户可能希望在不改变当前会话主上下文的前提下,探索一个来自活跃会话的问题。现有原语无法提供这种产品形态:[session-store fork](../../implemented/feature/2026-06-30-session-store-fork-api.md) 创建的是一个无关联的会话,而 [fork subagent](../../implemented/feature/2026-06-21-subagent-capability-seam.md) 是模型驱动的任务,其 transcript(文本记录)会折叠为一条工具结果。两者都不能给用户一个独立的对话,也都不能将结论带着出处信息记录回父会话。 ## 提案 -**侧会话(side session)** 是一个普通的活跃会话,从源会话最后一个已完成轮次处 fork 而来,附属于自己的 agent,以只读顾问的角色运行,并能够**合并回写**一条精炼笔记。 +**侧会话(side session)** 是一个普通的活跃会话,从源会话的最后一个已完成轮次 fork 而来,绑定到自己的 agent,定位为只读顾问,并能**合并回写**一条精简笔记。 -- **Fork 并附属:** 以父会话的均衡已完成轮次前缀创建子会话,并在其元数据中标记 `parentSession` 与 `seedLength`。这组合了 `ctx.agents.create({ seed, meta })`;不新增核心服务或 session-store 方法。 -- **顾问框架:** 创建后注入一条插件来源的 `context/message`,告知子会话只做解释,不执行变更或继续任务。保持系统提示词逐字节一致,以保留提供方对继承历史的前缀缓存。 -- **合并回写:** 向子会话请求一条有长度上限的交还内容,然后向父会话注入一条插件来源的 `context/message`。父会话的下一次请求会在其日志位置看到它,保持回放与[请求可重建性](../../implemented/architecture/2026-07-05-reconstructable-requests.md),无需新增会话事件。 -- **呈现:** 调用方式、会话切换与交还内容的渲染属于首个客户端拥有的界面。本 RFC 仅规定与界面无关的机制。 +- **Fork 并绑定:** 以父会话的平衡已完成轮次前缀创建子会话,并在其元数据中标记 `parentSession` 与 `seedLength`。这组合了 `ctx.agents.create({ seed, meta })`;不新增核心服务或 session-store 方法。 +- **顾问定位:** 创建后注入一条插件来源的 `context/message`,告知子会话只做解释,不执行变更或继续任务。保持系统提示词逐字节一致,可在继承的历史上保留提供方的前缀缓存。 +- **合并回写:** 向子会话请求一条有长度上限的 handback,然后向父会话注入一条插件来源的 `context/message`。父会话的下一次请求在其日志位置看到该消息,保持回放与[请求可重建性](../../implemented/architecture/2026-07-05-reconstructable-requests.md),无需新增会话事件。 +- **呈现:** 调用方式、会话切换与 handback 渲染属于首个客户端拥有的界面。本 RFC 仅规定与界面无关的机制。 -回退产品化、会话树视图、面向模型的侧会话工具,以及 `forkName`/`mergedInto` 元数据不在本 RFC 范围内。一次 live-adapter 原型验证了源日志隔离、继承上下文、多轮子会话交互,以及合并回写在父会话下一轮次中的可见性。 +回退产品化、会话树视图、面向模型的侧会话工具,以及 `forkName`/`mergedInto` 元数据均不在本 RFC 范围内。一次 live-adapter spike 已验证了源日志隔离、继承上下文、多轮子会话交互,以及合并回写在父会话下一轮次中的可见性。 ## 曾考虑的替代方案 -- **使用 subagent seam:** 否决。侧会话是用户驱动的、客户端可见的,且可能比父会话的一个轮次存活更久;subagent 是模型驱动的运行,返回一条工具结果。 -- **修改子会话的系统提示词:** 默认否决,因为任何字节变化都会从第零个 token 起使前缀缓存失效。部署方仍可选择更强的隔离。 -- **新增 `sidechat/*` 事件:** 推迟。插件来源的 `context/message` 已经提供持久性、来源信息与回放能力;只有当某个界面需要区分渲染时,专用事件才有正当理由。 -- **现在就绑定协议界面:** 否决。当前 UI 由客户端拥有。实时呈现最终必须从持久化消息派生,以确保回放渲染出相同的记录。 +- **使用 subagent seam:** 否决。侧会话是用户驱动的、客户端可见的,且可能存活超过父会话的一个轮次;subagent 是模型驱动的运行,返回一条工具结果。 +- **修改子会话的系统提示词:** 默认否决,因为任何字节变化都会从第零个 token 起使前缀缓存失效。部署方仍可选择这种更强的隔离方式。 +- **新增 `sidechat/*` 事件:** 延后。插件来源的 `context/message` 已提供持久性、出处与回放能力;只有当某个界面需要差异化渲染时,专用事件才有正当理由。 +- **现在就绑定一个协议界面:** 否决。当前 UI 由客户端拥有。实时呈现最终必须从持久消息派生,以使回放渲染出相同的记录。 ## 验收标准 -- Fork 不改动源会话,并创建一个子会话,子会话具有均衡的已完成轮次前缀、`parentSession`、`seedLength`,以及逐字节一致的系统提示词。 -- 顾问框架在子会话追加历史的头部恰好添加一条插件来源的 `context/message`,而非修改其系统提示词。 +- Fork 不改变源会话,创建的子会话具有平衡的已完成轮次前缀、`parentSession`、`seedLength`,以及逐字节一致的系统提示词。 +- 顾问定位在子会话追加历史的头部恰好添加一条插件来源的 `context/message`,而非修改其系统提示词。 - 合并回写恰好添加一条有长度上限的 `context/message`,来源为 `plugin: sidechat`;父会话的下一次请求与回放在相同位置看到它。 -- 父会话与子会话并发运行,日志与流之间无串扰。 +- 父会话与子会话并发运行,日志和流之间无串扰。 - 单元测试覆盖 fork/attach 与合并回写;快照覆盖率随首个绑定界面一起落地。 ## 风险 -- 只读行为在 `tools/pre-execute` 拒绝门禁强制执行之前仅为建议性的;[拦截 seam](../../implemented/feature/2026-06-30-interception-seams.md) 可以在不改变本机制的前提下添加该门禁。 -- 经过压缩(compaction)的源会话 fork 出的是其压缩视图,因此绑定界面应当告知用户:子会话继承的是摘要而非被替换的轮次。 -- 反复的交还内容会消耗父会话上下文。每次合并的长度上限约束了单条笔记的大小;后续整合属于压缩的职责。 +- 只读行为在 `tools/pre-execute` 拒绝门禁强制执行之前仅为建议性质;[拦截 seam](../../implemented/feature/2026-06-30-interception-seams.md) 可在不改变本机制的前提下添加该门禁。 +- 经过压缩(compaction)的源会话 fork 出的是其压缩视图,因此绑定的界面应当告知用户子会话继承的是摘要而非被替换的轮次。 +- 反复的 handback 会消耗父会话上下文。每次合并的长度上限约束了单条笔记的大小;后续的合并整理属于上下文压缩的职责。 diff --git a/docs/rfc/proposed/feature/2026-07-10-sqlite-session-query-provider.i18n.yaml b/docs/rfc/proposed/feature/2026-07-10-sqlite-session-query-provider.i18n.yaml index 349767710c..ab712ad131 100644 --- a/docs/rfc/proposed/feature/2026-07-10-sqlite-session-query-provider.i18n.yaml +++ b/docs/rfc/proposed/feature/2026-07-10-sqlite-session-query-provider.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-10-sqlite-session-query-provider.md: 8b67baf420433feca9d5cd09d58852bb9b1545a9 -2026-07-10-sqlite-session-query-provider.zh.md: de97e59ac6f2f90a738ff5c8b9c2872d54ba3954 +2026-07-10-sqlite-session-query-provider.zh.md: ad6b44363ab54b66b597941adb13938f27971bd5 diff --git a/docs/rfc/proposed/feature/2026-07-10-sqlite-session-query-provider.zh.md b/docs/rfc/proposed/feature/2026-07-10-sqlite-session-query-provider.zh.md index de97e59ac6..ad6b44363a 100644 --- a/docs/rfc/proposed/feature/2026-07-10-sqlite-session-query-provider.zh.md +++ b/docs/rfc/proposed/feature/2026-07-10-sqlite-session-query-provider.zh.md @@ -6,48 +6,48 @@ Status: proposed ## 问题 -精确读取服务 `ctx.sessionQuery` 有意不维护派生索引。大规模持久化的历史记录需要全文搜索,而不能在每次查询时扫描所有事件;同时,当前活跃会话需要一个比上次持久性检查点更新的覆盖层。搜索还需要具体的排序、摘要片段、过滤、分页、取消以及重建行为。 +精确读取的 `ctx.sessionQuery` 服务有意不维护派生索引。大规模持久化的历史记录需要全文搜索,而不是每次查询都扫描全部事件;当前的活跃会话则需要一个比上一次持久性检查点更新的覆盖层。搜索还需要具体的排序、摘要片段、过滤、分页、取消以及重建行为。 -如果把这些关注点拆分到一个推测性的 provider 协调器和一个数据库实现中,会产生两个耦合的协调状态机。第一个真实实现应当将源观察、提取、SQLite 事务、generation 管理和查询作为一个完整生命周期来拥有。 +如果把这些关注点拆分到一个推测性的 provider 协调器和一个数据库实现之间,会产生两个耦合的协调状态机。第一个真实实现应当将源观察、提取、SQLite 事务、generation 管理和查询作为一个完整的生命周期来拥有。 ## 提案 -在精确读取包旁新增 `@deepseek-ai/dsh-session-query-sqlite`。该包将暴露一个搜索服务或以其实际消费方所需的最小 API 扩展现有服务族;第一阶段不预先承诺 provider 注册协议。它将依赖 `ctx.sessions` 和可选的 `ctx.sessionPersistence`,拥有一个独立的派生 SQLite 数据库,并复用规范的 `foldSurface()` 分类。 +在精确读取包(exact-read package)旁新增 `@deepseek-ai/dsh-session-query-sqlite`。该包将暴露一个搜索服务,或以其实际消费方所需的最小 API 扩展服务族;第一阶段不预先承诺 provider 注册协议。它将依赖 `ctx.sessions` 和可选的 `ctx.sessionPersistence`,拥有一个独立的派生 SQLite 数据库,并复用规范的 `foldSurface()` 分类。 -实现拥有一个串行化的协调/数据库事务状态机。一次事务观察权威的持久化元数据和活跃快照,提取语义文档,更新派生表,推进相关的游标 generation,并执行或启用相应的查询。没有第二个服务维护并行的指纹、脏标记、活跃 ID 集合或失效 generation。 +实现拥有一个串行化的协调/数据库事务状态机。一次事务观察权威的持久化元数据和活跃快照,提取语义文档,更新派生表,推进相关的游标 generation,并执行或启用对应的查询。没有第二个服务维护并行的指纹、脏标记、活跃 ID 集合或失效 generation。 -持久化文档在重启后保留。活跃覆盖层是连接局部的,为同一会话遮蔽持久化行,在活跃所有者或数据库关闭时消失。派生数据库与规范持久化分离,因此索引重置、损坏、分词器变更和 schema 变动不会危及持久的对话日志。 +持久化文档在重启后存活。活跃覆盖层是连接本地的,对同一会话的持久化行进行遮蔽,在活跃所有者或数据库关闭时消失。派生数据库与规范持久化分离,确保索引重置、损坏、分词器变更和 schema 变动不会危及持久化的对话日志。 ## 随实现确定的搜索语义 -实现必须从可执行的用例出发定义跨会话和会话内两种搜索范围。每个可搜索事件是一个文档,包含会话元数据、事件元数据、surface 分类、归一化语义文本和有界的纯文本摘要片段。会话级结果按其最强匹配事件分组;数值化的后端分数保持私有。 +实现必须从可执行的用例出发定义跨会话和会话内两种搜索范围。每个可搜索事件是一个文档,包含会话元数据、事件元数据、surface 分类、归一化的语义文本和有界的纯文本摘要片段。会话级结果按其最强匹配事件分组;数值化的后端分数保持私有。 -过滤器在排序之前编译为参数化 SQL。查询语法作为数据处理。排序包含稳定的平局字段。不透明游标绑定到归一化的请求形状和最小相关 generation;不相关的会话变更不应使会话内游标失效。取消操作必须停止调用方等待,并在运行时允许的范围内中断 SQLite 工作。 +过滤器在排序之前编译为参数化 SQL。查询语法被视为数据。排序包含稳定的平局字段。不透明游标绑定到归一化的请求形状和最小相关 generation;不相关的会话变更不应使会话内游标失效。取消操作必须停止调用方等待,并在运行时允许的范围内中断 SQLite 工作。 -分词器选择仍是一个实现实验。FTS5 trigram 支持子串召回,但会拒绝短于三字符的有用词项并增大索引体积;提案在将其纳入契约之前,必须对比默认 Unicode 分词器做基准测试。 +分词器选择仍是实现层面的实验。FTS5 trigram 支持子串召回,但会拒绝短于三个字符的有用词项并增大索引体积;提案在将其写入契约之前,必须对该权衡与默认 Unicode 分词器进行基准测试。 ## 提取与协调 -该包首先为消息、推理(reasoning)、工具调用/结果、被拦截的提示词、上下文、steering(中途引导)、待办事项和错误/状态详情提供第一方语义提取。结构性事件和流式分片不贡献文档。未知的声明合并事件/内容类型保持不可搜索,除非有真实的扩展消费方证明需要公开的提取器注册表。 +该包首先为以下内容提供第一方语义提取:消息、reasoning、工具调用/结果、被阻止的提示词、上下文、steering(中途引导)、待办事项和错误/状态详情。结构性事件和流式分片不贡献文档。未知的声明合并事件/内容类型保持不可搜索,除非有真实的扩展消费方证明需要公开的提取器注册表。 -协调可以使用稳定指纹来避免重写未变更的持久化会话,但指纹的计算和存储由数据库包拥有。当源观察或提取失败时,它绝不能报告某行为最新。provider-schema 不匹配只重置派生数据库;普通的源变更使用事务性 upsert/delete。已挂载但不可读的持久化使受影响的搜索失败,但不影响规范写入或已知的活跃精确读取。 +协调可以使用稳定指纹来避免重写未变更的持久化会话,但数据库包拥有指纹的计算和存储。当源观察或提取失败时,它绝不能报告某行为最新。provider-schema 不匹配只重置派生数据库;普通的源变更使用事务性 upsert/delete。已挂载但不可读的持久化层使受影响的搜索失败,但不影响规范写入或已知的活跃精确读取。 ## 曾考虑的替代方案 -- **在规范持久化数据库中添加 FTS 表**:否决。可重建的索引不应与权威日志共享 schema/重置/故障边界。 -- **在第一阶段重新引入 provider 协调**:否决。只有一个计划中的实现,没有证据表明存在稳定的多 provider seam。 -- **立即持久化活跃覆盖层**:否决。活跃事件在现有检查点提交之前不是规范的。 -- **返回 BM25 分数**:否决。提供方特有的数值尺度在语料变化时不稳定。 +- **将 FTS 表添加到规范持久化数据库中**:否决,因为可重建的索引不应与权威日志共享 schema/重置/故障边界。 +- **重新引入第一阶段的 provider 协调**:否决,因为只有一个计划中的实现,且没有证据表明存在稳定的多 provider seam。 +- **立即持久化活跃覆盖层**:否决,因为活跃事件在现有检查点提交之前不是规范的。 +- **返回 BM25 分数**:否决,因为 provider 特定的数值尺度在语料变化时不稳定。 ## 验收标准 -- 重启测试覆盖未变更、新增、已变更和已删除的持久化会话,且不重建整个索引。 -- 重新打开时保留持久化行并移除活跃行;活跃行先遮蔽、后显露其持久化基底。 +- 重启测试覆盖未变更、新增、变更和删除的持久化会话,且不重建整个索引。 +- 重新打开时保留持久化行并移除活跃行;活跃行先遮蔽、后显露其持久化基础。 - 测试覆盖两种搜索范围、元数据过滤、surface 默认值、摘要片段、转义、确定性平局、分页、范围内的陈旧游标、取消、动态持久化挂载/卸载,以及事务失败后的恢复。 - schema 不匹配只重置派生数据库。 -- 一个无 key 的端到端测试将真实的持久化后端与真实的 SQLite 搜索包组合使用。 -- 在移入 `implemented/` 之前,本 RFC 须修订为实际实现的分词器和公开 API。 +- 一个 keyless 的端到端测试将真实的持久化后端与真实的 SQLite 搜索包组合使用。 +- 在移至 `implemented/` 之前,本 RFC 须修订为实际实现的分词器和公开 API。 ## 风险 -单一所有者比提供方无关的 seam 更简单,但初期可复用性较低。这是有意为之:第二个真实后端能揭示应当抽取什么。SQLite 运行时差异可能影响 FTS 排序和摘要片段,因此测试只能固定契约控制的排序和呈现。独立数据库增加了配置和生命周期工作,但保全了规范存储的安全边界。 +单一所有者比提供方无关的 seam 更简单,但初期可复用性较低。这是有意为之:第二个真实后端可以揭示应当抽取什么。SQLite 运行时差异可能影响 FTS 排序和摘要片段,因此测试只能固定契约控制的排序和呈现。独立数据库增加了配置和生命周期工作,但保全了规范存储的安全边界。 diff --git a/docs/rfc/proposed/feature/2026-07-13-stream-workflow-progress-through-tool-calls.i18n.yaml b/docs/rfc/proposed/feature/2026-07-13-stream-workflow-progress-through-tool-calls.i18n.yaml index 6b40707fbf..a90fafdf8b 100644 --- a/docs/rfc/proposed/feature/2026-07-13-stream-workflow-progress-through-tool-calls.i18n.yaml +++ b/docs/rfc/proposed/feature/2026-07-13-stream-workflow-progress-through-tool-calls.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-13-stream-workflow-progress-through-tool-calls.md: 525f2793052a80d82de29d2d370cfd747d002af6 -2026-07-13-stream-workflow-progress-through-tool-calls.zh.md: b5cc86df3ca42e513bda7e56b485a8287f275613 +2026-07-13-stream-workflow-progress-through-tool-calls.zh.md: 8dcb4aea2de50cdd278c702c9f6c85ab66e6be34 diff --git a/docs/rfc/proposed/feature/2026-07-13-stream-workflow-progress-through-tool-calls.zh.md b/docs/rfc/proposed/feature/2026-07-13-stream-workflow-progress-through-tool-calls.zh.md index b5cc86df3c..8dcb4aea2d 100644 --- a/docs/rfc/proposed/feature/2026-07-13-stream-workflow-progress-through-tool-calls.zh.md +++ b/docs/rfc/proposed/feature/2026-07-13-stream-workflow-progress-through-tool-calls.zh.md @@ -6,38 +6,38 @@ Status: proposed ## 问题 -工作流引擎有意为 run、phase、narration 和子 agent 进度发出成对平衡的 `workflow/*` 观察事件,但目前没有生产消费方呈现它们。因此编辑器在最终结果到来之前只显示一张 pending 状态的工作流工具卡片,尽管引擎已经报告了当前活跃的 phase、脚本日志内容以及哪些子 agent 已启动或已完成。[dynamic-workflows 决策](../../implemented/feature/2026-07-05-dynamic-workflows.md)明确将 ACP 进度 UI 保留给这条事件流。 +工作流引擎有意为 run、phase、narration 和子 agent(智能体)进度发出成对的 `workflow/*` observation 事件,但目前没有生产消费方呈现这些事件。因此,编辑器在最终结果返回之前只显示一张 pending 状态的工作流工具卡片,尽管引擎已经报告了当前活跃的 phase、脚本日志内容以及哪些子 agent 已启动或已结束。[dynamic-workflows 决策](../../implemented/feature/2026-07-05-dynamic-workflows.md)明确将 ACP(Agent Client Protocol)进度 UI 保留给这一事件流。 -如果让 `dsh-acp` 直接监听工作流事件,会反转能力边界:通用的 UI 桥接层将依赖一个可选的工作流包(package),并对一个工具名做特殊处理。工具流水线已经拥有实时更新所需的路由信息(agent 和 call id),但只暴露了纯粹的 pending/final 展示器,因此长时间运行的工具没有提供方无关的方式在二者之间报告瞬态 UI 状态。 +如果让 `dsh-acp` 直接监听工作流事件,就会反转能力边界:通用的 UI 桥接层将依赖一个可选的工作流包(package),并对一个工具名做特殊处理。工具流水线已经拥有实时更新所需的路由信息(agent 和 call id),但只暴露了纯粹的 pending/final 展示器,因此长时间运行的工具没有提供方无关的方式在二者之间报告瞬态 UI 状态。 ## 提案 -为 `dsh-tools` 添加一条实时进度通道。注册表持有的 `ToolExecution` 新增 `reportProgress(view): boolean`,其中 `view` 是一个独立的、提供方无关的通用进度快照,包含可选的替换标题和面向 UI 的内容块。进度不能改变调用的 args 派生卡片标签、kind、原始输入、locations、terminal intent 或 diff intent;它只更新初始选定的展示形式中的实时标题/内容。执行活跃期间,该方法校验并快照 view,然后派发一个受限的、agent 作用域的 `tools/progress` 观察事件,携带权威的执行标识与快照。一旦 final-result 处理开始,方法返回 `false` 且不再派发,确保迟到的异步报告者无法覆盖终态卡片。观察者异常被记录但不会导致工具失败。 +为 `dsh-tools` 添加一条实时进度通道。注册表所有的 `ToolExecution` 新增 `reportProgress(view): boolean`,其中 `view` 是一个独立的、提供方无关的通用进度快照,包含可选的替换标题和面向 UI 的内容块。进度不能更改调用的 args 派生卡片标签、kind、原始输入、locations、terminal intent 或 diff intent;它只更新在最初选定的展示方式内的实时标题/内容。当执行处于活跃状态时,该方法校验并快照 view,然后分发一个受限的、agent 作用域的 `tools/progress` observation,携带权威的执行标识与快照。一旦 final-result 处理开始,方法返回 `false` 且不再分发,因此迟到的异步报告者无法覆盖终态卡片。观察者异常会被记录日志,不会导致工具失败。 -`dsh-acp` 以通用方式消费 `tools/progress`。它通过现有的 agent-to-session 映射解析执行所属的 agent,并为同一 call id 发出一条 in-progress 的 `tool_call_update`。由于报告仅在工具执行流水线内部可用,持久化的 `tool/call` 及其 ACP `tool_call` 始终先于第一条 update;在 `tools/result` 之前关闭报告者确保没有进度更新出现在 completed/failed 卡片之后。进度是实时 UI 状态而非模型输入或持久历史:会话回放继续从 `tool/call` 和 `tool/result` 重建 pending 与 final 卡片,无需重放瞬态更新。 +`dsh-acp` 以通用方式消费 `tools/progress`。它通过既有的 agent-to-session 映射解析执行所属的 agent,并为同一 call id 发出 in-progress 的 `tool_call_update`。由于报告仅在工具执行流水线内可用,持久化的 `tool/call` 及其 ACP `tool_call` 始终先于第一条 update;在 `tools/result` 之前关闭报告者,确保进度更新不会出现在 completed/failed 卡片之后。进度是实时 UI 状态,而非模型输入或持久历史:会话回放继续从 `tool/call` 和 `tool/result` 重建 pending 与 final 卡片,无需重放瞬态更新。 -`dsh-tool-workflow` 成为第一个生产者。每次工具执行在调用 `ctx.workflows.start()` 之前安装一个紧凑的事件捕获器,因为合法的引擎可能在 `start()` 内部同步发出进度。在调用返回之前,捕获器将观察到的事件按 `WorkflowRunInfo.id` 归约为候选状态;随后选取返回的 `WorkflowRun.id`、丢弃其他候选、报告累积的快照,并将后续匹配事件直接路由。如果 `start()` 抛出异常,捕获器被 dispose,其候选被丢弃。这在不向 `WorkflowStartRequest` 添加观察者关联、也不要求进度等到 `start()` 返回的前提下,保持了引擎的可替换性。 +`dsh-tool-workflow` 成为第一个生产者。每次工具执行在调用 `ctx.workflows.start()` 之前安装一个紧凑的事件捕获器,因为合法的引擎可能在 `start()` 内部同步发出进度。在调用返回之前,捕获器将观察到的事件按 `WorkflowRunInfo.id` 归约为候选状态;随后选取返回的 `WorkflowRun.id`,丢弃其他候选,报告累积的快照,并将后续匹配事件直接路由。如果 `start()` 抛出异常,捕获器被 dispose(资源释放),其候选状态被丢弃。这在不向 `WorkflowStartRequest` 添加观察者关联、也不要求进度等到 `start()` 返回的前提下,保持了引擎的可替换性。 -归约器消费现有的 start、phase、log、agent-start、agent-end 和 end 事件,报告一个替换快照,包含当前 phase、最新日志行、活跃子 agent 标签,以及 completed/failed/cancelled 计数。它不累积 narration transcript;已完成的子 agent 离开活跃集合、转为计数。`workflow/end`、工具结算或插件 dispose 移除归约器条目和事件捕获器。六种工作流事件、它们的元数据、成对的子 agent 生命周期、run handle、取消通道和观察者隔离保持不变;第三方观察者可继续直接消费它们。 +归约器消费既有的 start、phase、log、agent-start、agent-end 和 end 事件,报告一个替换快照,包含当前 phase、最新日志行、活跃子 agent 标签以及 completed/failed/cancelled 计数。它不累积 narration transcript(文本记录);已结束的子 agent 离开活跃集合,变为计数器。`workflow/end`、工具结算或插件 dispose 移除归约器条目和事件捕获器。六种工作流事件及其元数据、成对的子 agent 生命周期、run handle、取消通道和观察者隔离保持不变;第三方观察者可继续直接消费这些事件。 -更新工具执行/展示文档、生成的事件与 API 目录、工作流包文档以及工作流数据结构目录。ACP 集成覆盖率必须使用脚本化的模型边界对真实的工作流工具和 worker seam 进行测试;主 ACP 快照套件新增一个 workflow-progress 场景,因为此变更改变了面向编辑器的 transcript。 +更新工具执行/展示文档、生成的事件与 API 目录、工作流包文档以及工作流数据结构目录。ACP 集成覆盖率必须使用脚本化的模型边界测试真实的工作流工具和 worker seam;主 ACP 快照套件新增一个 workflow-progress 场景,因为这改变了面向编辑器的 transcript。 ## 曾考虑的替代方案 -**删除工作流观察面。** 在 [collapse-workflow 简化提案](../../rejected/simplification/2026-07-12-collapse-workflow-to-foreground-core.md)中被否决:这些事件及其平衡的生命周期是有意设计的,缺失的部分是消费方。 +**删除工作流 observation 表面。** 在 [collapse-workflow 简化提案](../../rejected/simplification/2026-07-12-collapse-workflow-to-foreground-core.md)中被否决:这些事件及其成对生命周期是有意设计的,缺少的是消费方。 -**让 ACP 直接了解工作流。** 这可以将 `WorkflowRunInfo` 映射到会话和卡片,但会使通用桥接层依赖一个可选能力,并绕过「工具拥有展示意图」的规则。工具进度通道为所有长时间运行的工具解决了同样的路由问题。 +**让 ACP 直接了解工作流。** 这可以将 `WorkflowRunInfo` 映射到会话和卡片,但会使通用桥接层依赖一个可选能力,并绕过「工具拥有展示意图」的规则。工具进度通道为每个长时间运行的工具解决了相同的路由问题。 -**将每次进度更新持久化为会话事件。** 这会使实时 narration 可回放,但会用一种权威持久结果已由 tool call/result 对表达的状态永久膨胀日志。如果可恢复的工作流进度成为产品需求,它需要一个工作流日志化设计,而非伪装成持久事实的 UI 快照。 +**将每条进度更新持久化为会话事件。** 这会使实时 narration 可回放,但会用一种状态永久膨胀日志,而该状态的权威持久结果已经是工具调用/结果对。如果可恢复的工作流进度成为产品需求,需要一个工作流日志化设计,而非伪装成持久事实的 UI 快照。 ## 验收标准 -- `ToolExecution.reportProgress()` 由注册表持有、agent 作用域、快照化、观察者隔离,且在终态处理开始后返回 `false` 而不派发。 -- ACP 将进度路由到正确实时会话中的正确调用;不同会话中的并发工作流不能串扰,且 `tool_call_update` 不会出现在其 `tool_call` 之前或终态更新之后。 -- 工作流进度显示当前 phase、最新日志行、活跃子 agent 和结果计数,同时保持所有现有 `workflow/*` 事件和 run 语义;一个在 `start()` 内部同步发出 start、phase、log、child 和 end 事件的 seam 测试引擎不会丢失任何归约器状态。 +- `ToolExecution.reportProgress()` 由注册表所有、agent 作用域、快照化、观察者隔离,且在终态处理开始后返回 `false` 而不分发。 +- ACP 将进度路由到正确的实时会话中的正确调用;不同会话中的并发工作流不能串扰,且 `tool_call_update` 不会出现在其 `tool_call` 之前或终态更新之后。 +- 工作流进度显示当前 phase、最新日志行、活跃子 agent 和结果计数,同时保留所有既有 `workflow/*` 事件和 run 语义;一个在 `start()` 内部同步发出 start、phase、log、child 和 end 事件的 seam 测试引擎不会丢失任何归约器状态。 - 取消、worker 死亡、工具失败、会话关闭和插件 dispose 释放归约器状态;回放仅发出持久的 pending/final 卡片对。 - 单元测试、工作流集成测试、ACP 集成测试、快照、类型检查、覆盖率、doc-sync、module-graph、构建和 hygiene 门禁全部通过。 ## 风险 -此变更向工具 seam 添加了一个公开的实时进度方法和事件,因此实现方必须精确维护 active/terminal 边界,并在观察者看到快照之前将其分离。pre-start 捕获器可能短暂观察到无关的工作流 run,因此它仅按 run id 持有紧凑的候选状态,并在 `start()` 返回后立即丢弃所有不匹配的候选。一个工作流可能发出大量进度变更;有界归约器避免了 transcript 增长,但在关联之后仍会为每个有意义的事件发送一次 UI 更新。如果实测客户端需要合并更新,必须通过带默认值的、经过校验的桥接配置实现,而非硬编码的节流。瞬态进度在回放时有意消失,因此最终的工具结果仍是唯一持久的工作流卡片内容。 +本提案向工具 seam 添加了一个公开的实时进度方法和事件,因此实现方必须精确维护 active/terminal 边界,并在观察者看到快照之前将其分离。pre-start 捕获器可能短暂观察到无关的工作流 run,因此它仅按 run id 持有紧凑的候选状态,并在 `start()` 返回后立即丢弃所有不匹配的候选。一个工作流可能发出大量进度变更;有界归约器避免了 transcript 增长,但在关联完成后仍会为每个有意义的事件发送一条 UI 更新。如果经测量的客户端需要合并更新,这必须是一个带默认值的、经过校验的桥接配置,而非硬编码的节流。瞬态进度在回放时有意消失,因此最终工具结果仍是唯一持久的工作流卡片内容。 diff --git a/docs/rfc/proposed/process/2026-06-11-api-extractor-reports.i18n.yaml b/docs/rfc/proposed/process/2026-06-11-api-extractor-reports.i18n.yaml index 26c31b7471..fc5effd89a 100644 --- a/docs/rfc/proposed/process/2026-06-11-api-extractor-reports.i18n.yaml +++ b/docs/rfc/proposed/process/2026-06-11-api-extractor-reports.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-11-api-extractor-reports.md: 0f3f736ba662fd6366eb8d7f26887fb319b2b563 -2026-06-11-api-extractor-reports.zh.md: 3c472d64bc96c7fffa784c091f8a7a70bb0beb56 +2026-06-11-api-extractor-reports.zh.md: cf0eb3f9edbcfb2ae862f075af0628e716693a86 diff --git a/docs/rfc/proposed/process/2026-06-11-api-extractor-reports.zh.md b/docs/rfc/proposed/process/2026-06-11-api-extractor-reports.zh.md index 3c472d64bc..cf0eb3f9ed 100644 --- a/docs/rfc/proposed/process/2026-06-11-api-extractor-reports.zh.md +++ b/docs/rfc/proposed/process/2026-06-11-api-extractor-reports.zh.md @@ -4,29 +4,29 @@ Status: proposed -> 从最初的「Doc-sync 与 API 报告」RFC(2026-06-11)中拆出。第 1、2 部分(文档块类型检查、事件分类体系校验)已交付——见 [doc-sync 强制](../../implemented/process/2026-06-11-doc-sync-enforcement.md)。本文是被推迟的第 3 部分,作为独立提案保留。 +> 从最初的「Doc-sync 与 API 报告」RFC(2026-06-11)中拆出。第 1–2 部分(文档块类型检查、事件分类体系校验)已交付,见 [doc-sync 强制](../../implemented/process/2026-06-11-doc-sync-enforcement.md)。本文是被推迟的第 3 部分,作为独立提案保留。 ## 问题 -公开 API 的变更是不可见的:没有任何机制让「这个 commit 改变了公开接口」成为一个显式、可评审的事实。评审者阅读 diff 时可能遗漏一个导出类型新增了字段或方法签名发生了变化。 +公开 API 的变更是不可见的:没有任何机制将「此次提交改变了公开接口」变为一个显式、可评审的事实。评审者阅读 diff 时可能遗漏某个导出类型新增了字段,或某个方法签名发生了变化。 ## 提案 -使用 api-extractor(或 `tsc --emitDeclarationOnly` 加一份归一化的公开接口导出)为每个包(package)生成一份签入仓库的 `etc/<pkg>.api.md`;如果重新生成的结果与签入版本不同,CI 失败。这样每一次公开 API 变更都会变成评审者(或评审 agent)必须看到的一行 diff。 +使用 api-extractor(或 `tsc --emitDeclarationOnly` 加一份规范化的公开接口导出)为每个包(package)生成一份签入仓库的 `etc/<pkg>.api.md`;CI 在重新生成结果与已签入报告不一致时失败。这样,每一次公开 API 变更都会成为评审者(或评审 agent(智能体))必须看到的一行 diff。 ## 曾考虑的替代方案 -**`tsc --emitDeclarationOnly` 加一份归一化的公开接口导出**:如果 api-extractor 被证明过重,这是更轻量的机制;两者都满足本提案所需的「签入仓库、可 diff」的报告形态。 +**`tsc --emitDeclarationOnly` 加规范化的公开接口导出**:如果 api-extractor 过于笨重,这是更轻量的机制;两者都能满足提案所需的「签入仓库、可 diff」的报告形态。 ## 验收标准 -- 每个包有一份签入仓库的 `etc/<pkg>.api.md`;重新生成结果与已提交报告不同时 CI 失败。 +- 每个包都有一份签入仓库的 `etc/<pkg>.api.md`;CI 在重新生成结果与已提交报告不一致时失败。 - 公开 API 变更(新增导出、字段放宽、签名变化)在评审中以报告 diff 行的形式可见。 ## 风险 -该依赖重且难伺候——这正是它被推迟的原因——且报告格式会随编译器升级而变动,在各包尚未发布的阶段增加了一个收益甚微的维护面。 +该依赖笨重且难以调教(这正是它被推迟的原因),且报告格式会随编译器升级而变动,增加一个维护面;在各包尚未发布的阶段,收益有限。 ## 推迟原因 -在 doc-sync 落地时被推迟:对于评审者已经能看到源码 diff 的内部 monorepo 而言价值有限,且依赖重、难伺候。如果这些包将来对外发布,届时一份稳定、可 diff 的公开接口报告才值得其维护成本。 +在 doc-sync 落地时被推迟:对于一个内部 monorepo,评审者已经能看到源码 diff,价值不高;且依赖笨重、难以调教。如果各包将来对外发布,再重新评估——届时一份稳定、可 diff 的公开接口报告才值得其维护成本。 diff --git a/docs/rfc/proposed/process/2026-06-11-architectural-conformance.i18n.yaml b/docs/rfc/proposed/process/2026-06-11-architectural-conformance.i18n.yaml index 8aa3f297cd..094c2c349b 100644 --- a/docs/rfc/proposed/process/2026-06-11-architectural-conformance.i18n.yaml +++ b/docs/rfc/proposed/process/2026-06-11-architectural-conformance.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-11-architectural-conformance.md: 40858d049af2df1928e27280238d0b198a5202f7 -2026-06-11-architectural-conformance.zh.md: d61751210ef78b26e05a05efae4d5abccbfd2e5c +2026-06-11-architectural-conformance.zh.md: b68355dc1c04a4f807efdb95f813159cb7f9f178 diff --git a/docs/rfc/proposed/process/2026-06-11-architectural-conformance.zh.md b/docs/rfc/proposed/process/2026-06-11-architectural-conformance.zh.md index d61751210e..b68355dc1c 100644 --- a/docs/rfc/proposed/process/2026-06-11-architectural-conformance.zh.md +++ b/docs/rfc/proposed/process/2026-06-11-architectural-conformance.zh.md @@ -6,31 +6,31 @@ Status: proposed ## 问题 -两项架构保证目前仅存在于行文中:(1)任何包不得依赖具体的 loop 包([微内核承诺](../../implemented/architecture/2026-06-11-microkernel-event-taxonomy.md));(2)每个 LlmAdapter 都正确地遵循 chunk 协议。两者都应当机械化([质量门禁原则](../../implemented/process/2026-06-11-quality-gates.md))。 +目前有两项架构保证仅存在于行文中:(1)没有任何东西依赖具体的 loop 包([微内核承诺](../../implemented/architecture/2026-06-11-microkernel-event-taxonomy.md));(2)每个 LlmAdapter 都正确地遵循 chunk 协议。二者都应当是机械化的([质量门禁原则](../../implemented/process/2026-06-11-quality-gates.md))。 ## 提案 **dependency-cruiser** 配合以下规则: -- `packages/*`(agent-loop 自身的测试和 examples/ 除外)禁止导入 `@deepseek-ai/dsh-agent-loop`。 +- `packages/*`(除 agent-loop 自身的 tests 和 examples/ 外)禁止导入 `@deepseek-ai/dsh-agent-loop`。 - 禁止跨包深层导入(`@deepseek-ai/dsh-*/src/...` 路径)——只允许使用公开入口点。 -- packages/ 内禁止任何导入循环。 +- packages/ 内禁止导入循环。 - `vendor/*` 禁止从 `packages/*` 导入。 -- 分层:dsh-llm 不导入其他 dsh 包;dsh-session 只导入 dsh-llm;以此类推(即 packages/README.md 中的依赖表,强制执行)。 +- 分层:dsh-llm 不导入其他 dsh 包;dsh-session 仅导入 dsh-llm;以此类推(packages/README.md 中的依赖表,强制执行)。 -**适配器一致性套件**位于 dsh-llm(`@deepseek-ai/dsh-llm/conformance`):一个可复用的 vitest 套件,以适配器工厂为参数,断言 chunk 协议契约——每个 block 的 index 单调递增、`block-end` 之后该 index 不再有 delta、恰好一个 `finish`、usage 至多出现一次、每个 `tool-call-delta` 携带 call id、abort 被及时响应。当前对 mock 运行;DeepSeek V4 适配器从第一天起继承该套件。可选地提供一个 dev 模式的 `strictAdapter()` 包装层,在 debug flag 下于运行时强制执行相同约束(与 [dev 模式不变式](../../implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md)配对)。 +**适配器一致性套件**位于 dsh-llm(`@deepseek-ai/dsh-llm/conformance`):一个可复用的 vitest 套件,以适配器工厂为参数,断言 chunk 协议契约——每个 block 内 index 单调递增、`block-end` 之后该 index 不再有 delta、恰好一个 `finish`、usage 至多出现一次、每个 `tool-call-delta` 携带 call id、abort 被及时响应。当前对 mock 运行;DeepSeek V4 适配器从第一天起继承该套件。可选地提供一个 dev 模式的 `strictAdapter()` 包装层,在 debug flag 下于运行时强制执行相同规则(与 [dev 模式不变式](../../implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md) 配对)。 ## 计划 -先落地 dependency-cruiser 配置与 CI 步骤(约一小时工作量,永久保证);一致性套件随其首个消费方测试(针对 MockAdapter)一起落地,并作为 V4 适配器阶段的前置条件。 +先落地 dependency-cruiser 配置与 CI 步骤(约一小时工作量,换来永久保证);一致性套件随其首个消费方测试(针对 MockAdapter)一起落地,并作为 V4 适配器阶段的前置条件。 ## 验收标准 - dependency-cruiser 在 CI 中运行上述规则族;违规导入导致构建失败。 -- 一致性套件对 mock 适配器和两个正式适配器运行通过;新适配器包通过调用该套件并传入自己的工厂即可继承测试。 +- 一致性套件对 mock 适配器和两个正式适配器运行,新适配器包通过调用该套件并传入自己的工厂即可继承测试。 ## 风险 -随着包的增加需要维护 dep-cruiser 规则——应保持规则基于模式(`dsh-*`)而非逐一枚举。 +随着包的增加,dep-cruiser 规则需要维护——规则应基于模式(`dsh-*`)而非逐一枚举。 <!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> diff --git a/docs/rfc/proposed/process/2026-06-11-supply-chain-and-vendor-drift.i18n.yaml b/docs/rfc/proposed/process/2026-06-11-supply-chain-and-vendor-drift.i18n.yaml index 5ac9f62fee..62e4b4aa6f 100644 --- a/docs/rfc/proposed/process/2026-06-11-supply-chain-and-vendor-drift.i18n.yaml +++ b/docs/rfc/proposed/process/2026-06-11-supply-chain-and-vendor-drift.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-11-supply-chain-and-vendor-drift.md: 306e185e9175e3e7af24455cf95167f54b3d1c17 -2026-06-11-supply-chain-and-vendor-drift.zh.md: 1aeb0a8eff3f335bc87c05742acc4502a19bf3c5 +2026-06-11-supply-chain-and-vendor-drift.zh.md: a840e766c49182d7a9ca648acbbab1a2762688f5 diff --git a/docs/rfc/proposed/process/2026-06-11-supply-chain-and-vendor-drift.zh.md b/docs/rfc/proposed/process/2026-06-11-supply-chain-and-vendor-drift.zh.md index 1aeb0a8eff..a840e766c4 100644 --- a/docs/rfc/proposed/process/2026-06-11-supply-chain-and-vendor-drift.zh.md +++ b/docs/rfc/proposed/process/2026-06-11-supply-chain-and-vendor-drift.zh.md @@ -1,4 +1,4 @@ -# RFC:供应链检查与 vendor 漂移校验 +# RFC:供应链检查与 vendor 漂移验证 [English](2026-06-11-supply-chain-and-vendor-drift.md) | 中文 @@ -6,30 +6,30 @@ Status: proposed ## 问题 -vendor manifest([vendor 化决策](../../implemented/process/2026-06-11-vendor-cordis-as-source.md))在提交时只做*正向*强制(vendor 代码变更 ⇒ manifest 更新),但没有任何机制校验 manifest 的*声明*:即 vendor/ 确实等于「上游指定 SHA 的代码 + 日志中记录的修改」。此外,少量真正的 npm 依赖也没有安全公告监控或更新节奏。 +vendor manifest(元数据清单)(见[引入 vendor 的决策](../../implemented/process/2026-06-11-vendor-cordis-as-source.md))在提交时仅在**正向**强制执行(vendor 变更 ⇒ manifest 更新),但没有任何机制验证 manifest 的**声明**:即 vendor/ 确实等于上游指定 SHA 的内容加上所记录的修改。此外,少量真正的 npm 依赖也没有安全公告监控或更新节奏。 ## 提案 -1. **Vendor 漂移检查**(夜间 CI):以 manifest 中的 SHA 浅克隆上游仓库,复制对应 package 的源码,与 `vendor/*/src` 做 diff。除非 diff 与日志中的本地修改一致(每项修改保存为一个入库的 patch 文件,使日志条目成为可校验的产物而非纯文字),否则 job 失败。 -2. **依赖安全公告**:对 lockfile 运行 osv-scanner(或 `pnpm audit`),按计划调度 + 在涉及 lockfile 的 PR 上触发。 -3. **许可证清单**:一个脚本断言每个 vendor 化的 package 都携带 LICENSE 文件,且 package.json 的 `license` 字段与 vendor/README.md 中的清单一致(我们混合了 vendor 化的 MIT 与自有的 BSD-3)。作为 CI 步骤运行。 -4. **Renovate**(或一个定时 agent 任务)以小 PR 提议 npm 依赖更新,这些 PR 走完整门禁套件;vendor 化的 package 排除在外(它们的更新遵循 manifest 同步流程,理想情况下作为半自动化的 agent 工作流:拉取上游、重新应用 patch、运行门禁、打开 PR 并更新 manifest 表格)。 +1. **Vendor 漂移检查**(夜间 CI):以 manifest 中记录的 SHA 浅克隆上游仓库,复制对应的 package 源码,与 `vendor/*/src` 做 diff。除非 diff 与已记录的本地修改一致(每项修改以签入的 patch 文件保存——日志条目从行文描述变为可验证的产物),否则任务失败。 +2. **依赖安全公告**:对 lockfile 运行 osv-scanner(或 `pnpm audit`),按计划定期执行,并在涉及 lockfile 变更的 PR 上触发。 +3. **许可证清单**:一个脚本断言每个 vendor 包都携带其 LICENSE 文件,且 package.json 的 `license` 字段与 vendor/README.md 中的清单一致(我们混合了 vendor 的 MIT 与自有的 BSD-3)——作为 CI 步骤运行。 +4. **Renovate**(或定时 agent 任务)以小 PR 的形式提议 npm 依赖更新,这些 PR 走完整门禁套件;vendor 包不在其列(它们的更新遵循 manifest 同步流程,理想情况下是半自动化的 agent 工作流:拉取上游、重新应用 patch、运行门禁、以更新后的 manifest 表格开 PR)。 ## 计划 -3 最简单,先做。1 需要 CI 能通过网络访问上游仓库(私有镜像,需要 token),并将现有两项已记录的修改转为 patch 文件。2 和 4 属于配置工作。 +第 3 项最简单,先做。第 1 项需要 CI 能通过网络访问上游仓库(私有仓库,需要 token),并将现有两项已记录的修改转换为 patch 文件。第 2 项和第 4 项是配置工作。 ## 曾考虑的替代方案 -- **用 `pnpm audit` 代替 osv-scanner**:两者都满足安全公告扫描的需求;具体选择推迟到实现阶段决定。 -- **用定时 agent 任务代替 Renovate**:在「以小 PR 提议更新并走完整门禁」这件事上效果等价;vendor 化的 package 无论哪种方案都排除在外(它们的更新遵循 manifest 同步流程)。 +- **用 `pnpm audit` 替代 osv-scanner**:两者都满足安全公告扫描的需求;具体选择推迟到实现阶段决定。 +- **用定时 agent 任务替代 Renovate**:在提议小型更新 PR 并走完整门禁套件方面效果等价;vendor 包无论哪种方案都不在其列(它们的更新遵循 manifest 同步流程)。 ## 验收标准 -- 许可证清单脚本在 CI 中运行,缺少 LICENSE 或 `license` 字段与 `vendor/README.md` 清单矛盾时失败。 -- 夜间漂移 job 从 manifest SHA 加入库 patch 文件重建 `vendor/`,出现任何无法解释的 diff 时失败。 -- 安全公告扫描按计划对 lockfile 运行,并在涉及 lockfile 的 PR 上运行。 +- 许可证清单脚本在 CI 中运行,缺少 LICENSE 或 `license` 字段与 `vendor/README.md` 中的清单矛盾时失败。 +- 夜间漂移任务从 manifest SHA 加签入的 patch 文件重建 `vendor/`,出现任何无法解释的 diff 时失败。 +- 安全公告扫描按计划定期运行,并在涉及 lockfile 变更的 PR 上运行。 ## 风险 -上游仓库是私有镜像;CI 凭证与可用性是漂移检查的主要阻力。如果受阻,改为本地定时 agent 任务而非 CI 运行。 +上游仓库是私有镜像;CI 凭证与可用性是漂移检查的主要阻力。如果受阻,可改为本地定时 agent 任务而非 CI。 diff --git a/docs/rfc/proposed/process/2026-06-20-discover-package-inventory.i18n.yaml b/docs/rfc/proposed/process/2026-06-20-discover-package-inventory.i18n.yaml index f08e3c5dff..ddaafef6da 100644 --- a/docs/rfc/proposed/process/2026-06-20-discover-package-inventory.i18n.yaml +++ b/docs/rfc/proposed/process/2026-06-20-discover-package-inventory.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-20-discover-package-inventory.md: 22b3e9acbe4dad8ef829d0dd30415c516031d66b -2026-06-20-discover-package-inventory.zh.md: 8b62d42d2d514f60016690ba55d5ce77a50b3aff +2026-06-20-discover-package-inventory.zh.md: 4eeaed9ed6b390608095281775883f8e7a52e954 diff --git a/docs/rfc/proposed/process/2026-06-20-discover-package-inventory.zh.md b/docs/rfc/proposed/process/2026-06-20-discover-package-inventory.zh.md index 8b62d42d2d..4eeaed9ed6 100644 --- a/docs/rfc/proposed/process/2026-06-20-discover-package-inventory.zh.md +++ b/docs/rfc/proposed/process/2026-06-20-discover-package-inventory.zh.md @@ -1,36 +1,36 @@ -# RFC:通过发现机制获取包清单,取代静态列表维护 - -[English](2026-06-20-discover-package-inventory.md) | 中文 +# RFC:通过发现机制获取包清单,而非维护静态列表 Status: proposed +[English](2026-06-20-discover-package-inventory.md) | 中文 + ## 问题 -包(package)与门禁的清单在 TypeScript project references、package 文档、CI 行文、Knip 覆盖项以及快照场景元数据中反复出现。其中大部分只是重述包布局、manifest 数据、聚合命令内容或 fixture(测试前置数据)文件。每新增一个包或场景,都会产生本可避免的同步点。 +包(package)与门禁清单在 TypeScript project references、包文档、CI 描述、Knip 覆盖项以及快照场景元数据中反复出现。大多数只是重述包布局、manifest 数据、聚合命令内容或 fixture(测试前置数据)文件。因此每新增一个包或场景都会产生本可避免的同步点。 -[包层级结构](../../implemented/architecture/2026-06-20-package-hierarchy.md)已经手动消除了其中若干:`scripts/publint-all.ts` 现在从 `packages/<group>/<pkg>` 布局推导清单,两份 `tsconfig` 的 `paths` 映射也合并为一个 `@deepseek-ai/dsh-*` 通配符。剩下的是无法用 glob 消除的清单——主要是 `tsconfig.build.json` 的 project `references`,TypeScript 要求它是一个显式数组(没有通配符形式)。 +[包层级结构](../../implemented/architecture/2026-06-20-package-hierarchy.md)已经手动消除了其中若干:`scripts/publint-all.ts` 现在从 `packages/<group>/<pkg>` 布局推导列表,两份 `tsconfig` 的 `paths` 映射也合并为一个 `@deepseek-ai/dsh-*` 通配符。剩下的是无法用 glob 消除的清单,主要是 `tsconfig.build.json` 的 project `references`——TypeScript 要求它是显式数组(没有通配符形式)。 -静态列表在编码策略时是合理的;当它们只是重复 `package.json`、workspace glob 或包层级结构中已有的 manifest 数据或布局事实时,就是无谓的摩擦。 +当静态列表编码的是策略时,它们是合理的;当它们只是重复 `package.json`、workspace glob 或包层级结构中已有的 manifest 数据或布局事实时,就是不必要的摩擦。 ## 提案 -让剩余的包/门禁清单可被发现。一个唯一的权威来源——`packages/<group>/<pkg>` 层级结构加上 package manifest——应当驱动 `tsconfig.build.json` 的 `references`、模块图以及任何全量包列表,并配合一个生成加校验步骤(沿用现有的 `gen-module-graph` / `gen-cordis-catalog` 模式:生成器写入产物,`--check` 模式在 `hygiene`/`doc-sync` 中检测已提交副本是否陈旧)。模块图生成器已经在读取 package manifest。`doc-sync` 应当成为定义并打印其子门禁的唯一命令,文档链接到该命令而非重述第二份清单。 +让剩余的包/门禁清单可被发现。一个唯一的权威来源——`packages/<group>/<pkg>` 层级结构加上包 manifest(元数据清单)——应当驱动 `tsconfig.build.json` 的 `references`、模块图以及任何全量包列表,并配合一个生成加校验步骤(沿用现有的 `gen-module-graph` / `gen-cordis-catalog` 模式:生成器写出产物,`hygiene`/doc-sync(文档同步门禁)中的 `--check` 模式在提交副本陈旧时报错)。模块图生成已经在读取包 manifest。`doc-sync` 应当成为定义并打印其子门禁的唯一命令,文档链接到该命令而非重述第二份列表。 -层级结构不需要编码一个包的所有信息,但应当编码宽泛的维护策略:core/product 包、集成包、能力 seam 包与 support/test/example 包不应在脚本能区分它们之前先要求一份手工维护的例外清单。 +层级结构不需要编码关于包的所有事实,但应当编码宽泛的维护策略:core/product 包、集成包、能力 seam 包与 support/test/example 包不应在脚本能区分它们之前先要求一份手工维护的例外列表。 -有两项被编目的内容根本不需要生成器:把 e2e 入口 glob 折入 knip 的默认 stanza 即可直接删除各包的重述;`childSessions` 可以从每个场景的 fixture 目录发现,让场景表只声明策略(`recorded`、`hasModelTurn`、`comparesLog`)。而即便这些策略字段,今天也在追踪可从 fixture 推导的事实(`comparesLog` ⟺ 已提交的日志在表头行之后有内容;`recorded` ⟺ `hasModelTurn` 且没有 `replay.override.json` 兄弟文件),因此每个新场景类别都在不断添加 fixture 目录已经能回答的开关。 +有两类编目项根本不需要生成器:将 e2e 入口 glob 折入 knip 的默认配置段即可直接删除逐包的重复声明;`childSessions` 可从每个场景的 fixture 目录发现,使场景表只需声明策略(`recorded`、`hasModelTurn`、`comparesLog`)。而且即便是这些策略字段,今天也在追踪可从 fixture 推导的事实(`comparesLog` ⟺ 已提交的日志在头行之后还有条目;`recorded` ⟺ `hasModelTurn` 且没有 `replay.override.json` 兄弟文件),因此每个新场景类都在不断添加 fixture 目录本身已经能回答的开关。 ## 验收标准 -- `tsconfig.build.json` 的 project `references` 由层级结构生成(生成器输出它们;`--check` 门禁在已提交副本陈旧时失败),而非手工维护。 -- 新增一个包不需要为任何门禁编辑静态包列表。 +- `tsconfig.build.json` 的 project `references` 由层级结构生成(生成器输出它们;`--check` 门禁在提交副本陈旧时报错),而非手工维护。 +- 新增一个包时,不需要为任何门禁编辑静态包列表。 - 文档描述真源,而非重复生成的清单。 - CI 调用聚合命令,由这些命令自行管理其子门禁列表。 -- `knip.json` 仅在编码真实信息(额外入口文件、被忽略的依赖)时才携带 per-package 覆盖项,绝不重述默认 stanza。 +- `knip.json` 仅在编码真实信息(额外入口文件、被忽略的依赖)时才携带逐包覆盖项,绝不重述默认配置段。 - 快照场景只声明策略,不声明可从其 fixture 目录发现的事实。 ## 风险 -发现脚本可能变得过于精巧。实现应保持朴素:读取 manifest、按显式字段过滤、打印解析后的列表、出错时大声报错。收益在于消除手工清单漂移,而非发明一套构建系统。 +发现脚本可能变得过于精巧。实现应当保持朴素:读取 manifest、按显式字段过滤、打印解析后的列表、出错时大声报错。收益在于消除手工清单的漂移,而非发明一套构建系统。 <!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> diff --git a/docs/rfc/proposed/simplification/2026-06-20-unify-agent-and-session-id.i18n.yaml b/docs/rfc/proposed/simplification/2026-06-20-unify-agent-and-session-id.i18n.yaml index db8dc2d650..dde7a0ad32 100644 --- a/docs/rfc/proposed/simplification/2026-06-20-unify-agent-and-session-id.i18n.yaml +++ b/docs/rfc/proposed/simplification/2026-06-20-unify-agent-and-session-id.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-20-unify-agent-and-session-id.md: 3a6daa411673003eb1c3017e7a717ae4bf98b735 -2026-06-20-unify-agent-and-session-id.zh.md: c931831c028e3147bfb84ed4fdeefe83037e92f4 +2026-06-20-unify-agent-and-session-id.zh.md: 6b1b996879125c6ab85aed7ba419aff15d077b42 diff --git a/docs/rfc/proposed/simplification/2026-06-20-unify-agent-and-session-id.zh.md b/docs/rfc/proposed/simplification/2026-06-20-unify-agent-and-session-id.zh.md index c931831c02..6b1b996879 100644 --- a/docs/rfc/proposed/simplification/2026-06-20-unify-agent-and-session-id.zh.md +++ b/docs/rfc/proposed/simplification/2026-06-20-unify-agent-and-session-id.zh.md @@ -6,37 +6,37 @@ Status: proposed ## 问题 -agent 工厂为每个活跃的 agent/会话对维护两个 id:`agentId`(`AgentRegistry` 的路由句柄)和 `sessionId`(事件溯源与持久化日志的身份标识)。`CreateAgentOptions` 接收两者;`ResumeAgentOptions` 接收 `agentId` 加 `resumeSessionId`;进程内 subagent 铸造两个独立的 UUID,尽管血缘关系另行记录。 +agent 工厂为每个活跃的 agent/session 对维护两个 id:`agentId`(`AgentRegistry` 的路由句柄)和 `sessionId`(事件溯源与持久化日志的标识)。`CreateAgentOptions` 接收两者;`ResumeAgentOptions` 接收 `agentId` 加 `resumeSessionId`;进程内 subagent 各自铸造两个独立的 UUID,尽管血缘关系另行记录。 -ACP(Agent Client Protocol)已经对两个身份使用同一个值。二者在配置创建的 agent、恢复的会话和进程内子 agent 中才出现分歧,但没有任何生产路径会将一个活跃 agent 重新关联到多个会话,或让一个会话经过多个 agent id。Stdio 保留 `labelBySession` 仅仅是为了从会话事件中恢复 agent 标签,而钩子同时暴露两个值让使用者自行对齐。 +ACP(Agent Client Protocol)已经对这两个标识使用同一个值。它们在配置创建的 agent(智能体)、恢复的会话和进程内子 agent 中才出现分歧,但没有任何生产路径会把一个活跃 agent 重新关联到多个会话,或让一个会话经过多个 agent id。Stdio 保留 `labelBySession` 仅仅是为了从会话事件中恢复 agent 标签,而钩子同时暴露两个值让使用者自行调和。 -[agent 作用域运行时](../../implemented/architecture/2026-07-12-agent-scope-runtime-design.md)没有与身份相关的预留状态:创建和恢复使用同一个 `AgentCreationTransaction`,两个注册表条目使用相同的 final-entry 碰撞规则。分离的 id 并未复制活跃性、回滚或静默机制。统一后删除一个调用方提供的 id、每个进程内子 agent 的一个 UUID 以及剩余的翻译路径,而不改变事务生命周期;同时使活跃 agent 注册表强制执行后台任务所有权所使用的会话身份。 +[agent-scope 运行时](../../implemented/architecture/2026-07-12-agent-scope-runtime-design.md)没有与标识相关的保留状态:创建和恢复使用同一个 `AgentCreationTransaction`,两个注册表条目都使用相同的 final-entry 碰撞规则。分离的 id 并不会使活跃性、回滚或静默机制产生重复。统一后删除一个调用方提供的 id、每个进程内子 agent 的一个 UUID 以及剩余的转换路径,而不改变事务生命周期;同时使活跃 agent 注册表强制执行后台任务所有权所使用的会话标识。 -`Session` 另外同时暴露 `Session.id` 和 `Session.header.id`,尽管构造时要求二者一致。持久化边界必须校验这一重复值,消费方必须在同一事实的两个归属位置之间做选择。 +`Session` 另外同时暴露 `Session.id` 和 `Session.header.id`,尽管构造时要求二者必须一致。持久化边界必须校验这个重复值,消费方必须在同一事实的两个归属位置之间做选择。 ## 提案 -对 agent 注册表条目和 `session.header.id` 使用同一个 id。`CreateAgentOptions` 为两个最终条目接受一个身份标识;恢复操作以被恢复的 session id 注册 agent;subagent 创建铸造一个合并后的 id;`Session` 只保留一个身份归属位置。保留当前的事务、final-entry 碰撞检查、exact-entry 摘除、回滚与静默机制;仅移除唯一职责是在两个 id 之间做翻译的 map 和字段。 +对 agent 注册表条目和 `session.header.id` 使用同一个 id。`CreateAgentOptions` 为两个最终条目接收一个标识;恢复操作以被恢复的 session id 注册 agent;subagent 创建铸造一个合并后的 id;`Session` 只保留一个标识归属位置。保留当前的事务、final-entry 碰撞检查、exact-entry 摘除、回滚与静默机制;仅移除唯一职责是在两个 id 之间做转换的 map 和字段。 -配置驱动的路径必须先确定其恢复还是创建的策略。目前它使用一个稳定的 agent 标签加一个带 UUID 后缀的新 session id,以避免在下次运行时与已有的持久化日志碰撞。统一后它必须明确选择:恢复一个固定 id、铸造一个新的合并 id,或将该策略暴露出来;实现不得默默做出选择。 +配置驱动的路径必须先确定其恢复还是创建的策略。当前它使用一个稳定的 agent 标签加一个带 UUID 后缀的新 session id,以避免在下次运行时与已有的持久化日志碰撞。统一后,它必须明确选择:恢复一个固定 id、铸造一个新的合并 id,还是将该策略暴露出来;实现不得默默做出选择。 -`agent/created` 和 `agent/disposed` 不在本提案范围内。它们是发布生命周期事件而非身份别名;移除它们需要单独的生产方-消费方审计与决策。 +`agent/created` 和 `agent/disposed` 不在本提案范围内。它们是发布生命周期事件而非标识别名;移除它们需要单独的生产方-消费方审计与决策。 ## 曾考虑的替代方案 -**保留分离的路由身份与日志身份。** 一个稳定的配置 agent 标签搭配一个新的对话,是这种区分的真实用途。如果确实需要该显示或路由身份,则应否决本提案,转而显式强制 session id 唯一性,而不是将翻译隐藏在另一个 map 中。 +**保留分离的路由标识与日志标识。** 一个稳定的配置 agent 标签配合一个新的对话,是这种区分的真实用途。如果确实需要该显示或路由标识,请否决本提案,转而显式强制 session id 唯一性,而不是把转换隐藏在另一个 map 中。 ## 验收标准 -- agent 创建/恢复与 subagent 创建只携带一个身份标识;`Session` 将其存储在一个位置。 -- 创建事务保留 final-entry 碰撞、exact-entry 摘除、回滚与静默保证,且不依赖与身份相关的生命周期状态。 -- ACP、stdio、钩子、bash 所有权、持久化与血缘关系无需 agent/session id 翻译。 -- 配置驱动的恢复还是创建策略是显式的,并在持久化重启场景中得到覆盖。 +- agent 创建/恢复与 subagent 创建只携带一个标识;`Session` 将其存储在一个位置。 +- 创建事务在不依赖标识相关生命周期状态的前提下,保留 final-entry 碰撞、exact-entry 摘除、回滚与静默保证。 +- ACP、stdio、钩子、bash 所有权、持久化与血缘关系无需进行 agent/session id 转换。 +- 配置驱动的恢复还是创建策略是显式的,并在持久化重启场景下得到覆盖。 - `agent/created` 和 `agent/disposed` 仅在单独的生产方-消费方审计之后才变更。 - 类型检查、覆盖率、快照、doc-sync、module-graph 校验、构建与 hygiene 全部通过。 ## 风险 -统一后将无法再拥有一个跨多个会话日志的稳定 actor 身份,包括未来可能的交接或 fork(保留 actor 但更换会话)。重新引入该设计需要一个新的显式 actor 身份。统一还使一个持久化的、可能由客户端选择的 session id 成为注册表句柄,并改变每个创建/恢复调用点和 fixture(测试前置数据)。 +统一后将无法再拥有一个跨多个会话日志的稳定 actor 标识,包括未来可能出现的、在保留 actor 的同时切换会话的 handoff 或 fork 场景。重新引入该设计将需要一个新的显式 actor 标识。统一还使一个持久化的、可能由客户端选定的 session id 成为注册表句柄,并改变每个创建/恢复的调用点与 fixture(测试前置数据)。 -配置重启策略是阻塞性的设计决策:固定的合并 id 可能与其已有日志碰撞,而每次运行生成新 id 则放弃了稳定的配置标签。如果确实需要独立的 actor 身份或稳定标签/新会话的配对,则应否决本提案,保留分离的 id 并加上显式的唯一性守卫。 +配置重启策略是阻塞性的设计决策:固定的合并 id 可能与已有日志碰撞,而每次运行生成新 id 则放弃了稳定的配置标签。如果确实需要独立的 actor 标识或稳定标签/新会话的配对,请否决本提案,保留分离的 id 并加上显式的唯一性守卫。 diff --git a/docs/rfc/proposed/simplification/2026-07-04-prune-dead-core-spine-surface.i18n.yaml b/docs/rfc/proposed/simplification/2026-07-04-prune-dead-core-spine-surface.i18n.yaml index 1d5522f5b3..4420b2dd27 100644 --- a/docs/rfc/proposed/simplification/2026-07-04-prune-dead-core-spine-surface.i18n.yaml +++ b/docs/rfc/proposed/simplification/2026-07-04-prune-dead-core-spine-surface.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-04-prune-dead-core-spine-surface.md: c46fe464e8627dcfc39a1d3fbb38a9cbd84269cf -2026-07-04-prune-dead-core-spine-surface.zh.md: 86830904ef0d1262d4cc132fb8d4b6a50e033e1d +2026-07-04-prune-dead-core-spine-surface.zh.md: 67e89a580b086a08aed702d9e0b87bdb6e32c944 diff --git a/docs/rfc/proposed/simplification/2026-07-04-prune-dead-core-spine-surface.zh.md b/docs/rfc/proposed/simplification/2026-07-04-prune-dead-core-spine-surface.zh.md index 86830904ef..67e89a580b 100644 --- a/docs/rfc/proposed/simplification/2026-07-04-prune-dead-core-spine-surface.zh.md +++ b/docs/rfc/proposed/simplification/2026-07-04-prune-dead-core-spine-surface.zh.md @@ -1,4 +1,4 @@ -# RFC:裁剪无用的公开接口与结果面 +# RFC:裁剪无用的公开与结果接口 Status: proposed @@ -6,57 +6,57 @@ Status: proposed ## 问题 -若干包根导出、结果字段和便利方法没有生产消费方。它们之所以存活,要么是因为测试通过公开入口导入内部实现,要么是因为某个类型预设了一个从未出现的调用者。每一项单独看都很小,但合在一起,它们扩大了 SDK 契约、生成的目录、文档和回归矩阵,却没有支撑任何已交付的路径。 +若干包根导出、结果字段和便利方法没有生产消费方。它们之所以存活,要么是因为测试通过公开入口导入了内部实现,要么是因为某个类型预期了一个从未出现的调用者。每一项单独看都很小,但合在一起,它们扩大了 SDK 契约、生成的 catalog、文档和回归矩阵,却没有支撑任何已交付的路径。 -生产语料库是 `packages/*/*/src`、示例源码/配置和运行时脚本。测试、package README 和 RFC 行文是发布的证据,但不是固定的调用者。`cordis_inspect` 使 `packages/cordis/tool-cordis/src/api-catalog.ts` 对模型可见,`cordis_mount` 可以通过受保护的真实服务代理调用注入的服务,因此被编目的服务方法和返回形状是真正的动态产品面。下表因此区分了「没有固定的仓库内调用者」与「不可达」:涉及编目词汇的行有意收缩模型编写的 mount 所能发现和调用的内容,而包根的实现辅助函数并不通过该服务门面可达。精确符号搜索得出以下清单: +生产语料库是 `packages/*/*/src`、示例源码/配置和运行时脚本。测试、包(package) README 和 RFC 行文是发布的证据,但不是固定调用者。`cordis_inspect` 使 `packages/cordis/tool-cordis/src/api-catalog.ts` 对模型可见,`cordis_mount` 可以通过受保护的真实服务代理调用注入的服务,因此 catalog 中的服务方法和返回形状是真正的动态产品接口。下表因此区分「没有固定的仓库调用者」与「不可达」:涉及 catalog 词汇的行有意收缩模型编写的 mount 能发现和调用的内容,而包根实现辅助函数并不通过该服务门面可达。精确符号搜索得出以下清单: -| 接口面 | 生产证据 | 简化方式 | +| 接口 | 生产证据 | 简化方式 | | --- | --- | --- | -| `SurfaceManager.invalidate()` | 仅其单元测试调用;seeding 在惰性创建的 manager 存在之前就已完成,且会话从不替换其日志引用。 | 删除该方法及其不可能触发的整体替换契约。 | -| `ToolExecutionResult.callId` | 每个钩子已经接收不可变的 `ToolExecution`;循环和 ACP(Agent Client Protocol)通过 call/session 事件关联。没有消费方读取这个重复的结果字段。 | 移除该字段、复制/不匹配守卫,以及证明该重复不会不一致的测试。 | -| `ReactLoopAgent` 根导出 | 包外的具名导入都是测试;生产代码面向 `Agent` 编程,通过 `ctx.agents` 创建/恢复。 | 返回/接口类型为 `Agent`,将具体循环类设为包内部;保留有意为之的同步纯配置 `AgentLoop.create()` 路径。 | -| `workflow-workerthread` 的 protocol/runtime/session 再导出与具名 `WorkerWorkflowEngine` | 所有包名消费方使用默认引擎;workflow RFC 已将 worker 协议格式定义为私有。 | 保留默认插件类/配置契约;移除重复的具名类导出,将协议模块设为源码私有。 | -| `code-runtime-worker` 的 protocol/bootstrap 再导出 | 包外的生产/e2e 消费方使用 `WorkerCodeRuntime` 和配置,而非 `BootstrapPort`、`PatchableStream` 或 worker 消息/启动类型。 | 保留运行时类/配置契约,将其协议格式/bootstrap 词汇设为源码私有。 | -| ACP 的 translation/presenter 根导出 | `agentOptions`、`streamSessionEventUpdate`、`todosToPlan`、`ToolPresenter`、`nullToolPresenter` 和 `TerminalRendering` 仅有同文件或 ACP 测试消费方;唯一的包外生产消费方挂载的是插件命名空间。 | 保留 `name`、`inject`、`Config`、`AcpConfig` 和 `apply`;将 translation/presentation 辅助函数设为源码私有,在包内测试。 | -| `providerWording` 和 `completedTurnPrefix` 根导出 | 各有一个同包生产调用者;仅 balanced-prefix 辅助函数有一个同包白盒测试。 | 设为源码私有,通过 provider 行为测试。 | -| `depthOf`、`SubagentDepthError`、`SENSITIVE_ENV_PATTERN`、`waitForExit` 和 `exitsWithin` 根导出 | 生产 subagent 后端消费的是进程内 runner 和子进程构造/释放辅助函数,而非这些强制/测试内部实现。 | 保留深度/环境/退出行为,但将辅助函数和 error/regex 设为源码私有;通过 spawn 和释放来测试。 | -| `PersistenceCoordinator.inits`、后端 `inits` 访问器、`seedCoversPrefix` 和 `assertSerializable` | 访问器为白盒测试而存在;`seedCoversPrefix` 没有包外生产导入者;`assertSerializable` 没有生产调用者,且与 coordinator append 边界的无损快照重复。 | 通过 `session/flush` 观察初始化,将 `seedCoversPrefix` 设为源码私有,删除 `assertSerializable`。保留两个后端、`SessionHeader` 和 SQLite 的版本契约。 | -| `LlmError.status` 与 replay status | 适配器/replay 填充它,但生产分支基于稳定的 error code/message,从不读取原始 status。 | 移除未读字段和 replay 管道,同时保留错误分类。 | +| `SurfaceManager.invalidate()` | 只有其单元测试调用它;seeding 在惰性创建的 manager 存在之前就已完成,且会话从不替换其日志引用。 | 删除它及其不可能触发的整体替换契约。 | +| `ToolExecutionResult.callId` | 每个钩子已经接收不可变的 `ToolExecution`;循环和 ACP(Agent Client Protocol)通过 call/session 事件关联。没有消费方读取这个重复的结果字段。 | 移除该字段、复制/不匹配守卫,以及证明该重复不可能不一致的测试。 | +| `ReactLoopAgent` 根导出 | 包外的命名导入都是测试;生产代码面向 `Agent` 编程,通过 `ctx.agents` 创建/恢复。 | 返回/接口类型为 `Agent`,将具体循环类改为包内部;保留有意设计的同步、仅配置的 `AgentLoop.create()` 路径。 | +| `workflow-workerthread` 的 protocol/runtime/session 再导出与命名的 `WorkerWorkflowEngine` | 每个包名消费方都使用默认引擎;workflow RFC 已将 worker 协议格式(wire format)定义为私有。 | 保留默认插件类/配置契约;移除重复的命名类导出,将协议模块保持为源码私有。 | +| `code-runtime-worker` 的 protocol/bootstrap 再导出 | 包外的生产/e2e 消费方使用 `WorkerCodeRuntime` 和配置,而非 `BootstrapPort`、`PatchableStream` 或 worker 消息/启动类型。 | 保留运行时类/配置契约,将其协议格式/bootstrap 词汇改为源码私有。 | +| ACP 的 translation/presenter 根导出 | `agentOptions`、`streamSessionEventUpdate`、`todosToPlan`、`ToolPresenter`、`nullToolPresenter` 和 `TerminalRendering` 只有同文件或 ACP 测试消费方;唯一的包外生产消费方挂载的是插件命名空间。 | 保留 `name`、`inject`、`Config`、`AcpConfig` 和 `apply`;将 translation/presentation 辅助函数改为源码私有,在包内测试。 | +| `providerWording` 与 `completedTurnPrefix` 根导出 | 各有一个同包生产调用者;只有 balanced-prefix 辅助函数有一个同包白盒测试。 | 改为源码私有,测试 provider 行为。 | +| `depthOf`、`SubagentDepthError`、`SENSITIVE_ENV_PATTERN`、`waitForExit` 与 `exitsWithin` 根导出 | 生产 subagent 后端消费的是进程内 runner 和子进程构造/dispose(资源释放)辅助函数,而非这些强制/测试内部实现。 | 保留深度/环境/退出行为,但将辅助函数和 error/regex 改为源码私有;通过 spawn 和 dispose 测试。 | +| `PersistenceCoordinator.inits`、后端 `inits` 访问器、`seedCoversPrefix` 与 `assertSerializable` | 访问器为白盒测试而存在;`seedCoversPrefix` 没有包外生产导入者;`assertSerializable` 没有生产调用者,且与 coordinator append 边界的无损快照重复。 | 通过 `session/flush` 观察初始化,将 `seedCoversPrefix` 改为源码私有,删除 `assertSerializable`。保留两个后端、`SessionHeader` 和 SQLite 的版本契约。 | +| `LlmError.status` 与 replay status | 适配器/replay 填充它,但生产分支基于稳定的 error code/message 判断,从不读取原始 status。 | 移除未读字段和 replay 管道,保留错误分类。 | | `BlockAssembler.push()` 返回值 | 两个生产调用者都忽略返回的已完成块。 | 返回 `void`;保留有意公开的 `blocks()`/`message()` 契约。 | -| `compactRegion` 的独立 `session` 参数 | 固定调用者传入的对象与 `agent.session` 已经是同一个;模型可见的 mount API 也能调用该方法,但接受两个身份允许挂载的插件提供不一致的配对。 | 保留手动区域 seam,同时有意将其收窄为以 `agent.session` 为唯一真源。 | -| `CompactionResult.startSeq`、`summarySeq`、`endSeq` 和 `summary` | 生产消费方只读取 shadowed range/seq/token 统计;持久日志拥有摘要和事件标识。 | 移除四个结果回显,同时保留两个共享的 transcript(文本记录)渲染器。 | -| `BasicCompactService` 的 estimation/summarization 可见性 | 没有包外生产调用者调用这五个方法;已实现的 RFC 仅将 `estimateContentTokens()` 和 `summarize()` 列为子类钩子。 | 将这两个方法设为 `protected`,将三个仅用于编排的估算器设为 private。 | -| `CodeLogEntry.source`/`level` 和 `RunCodeMeta.dispatches` | 所有生产消费方将日志映射为文本;没有 presenter/模型路径读取其他字段或持久化的 dispatch 计数。 | 将 code-runtime 日志改为字符串(或纯文本条目),移除 result-meta dispatch 管道;保留用于生成确定性 dispatch id 的本地计数器。 | -| `ToolNotFoundError.toolName`、`SystemPrompt.config` 和 `BashTask.command` | 每个存储的公开值都没有生产读取者。 | 移除未读字段,同时保留错误消息、已解析的配置行为和任务生命周期。 | -| 后端包根实现辅助函数 | 下方精确清单仅通过相对同包导入调用。生产命名空间导入挂载的是保留的插件契约,不读取这些属性;具名根消费方是测试。 | 保留每个适配器/提供方/服务及其配置/错误契约;停止在包根导出所列辅助函数/常量。 | -| 消费方包根实现辅助函数 | 下方精确清单仅有同包生产调用者。生产命名空间导入挂载插件契约,不读取辅助属性;具名根消费方是测试。 | 保留插件契约和稳定错误码;将测试移至包内模块或公开行为,停止在包根导出所列辅助函数。 | +| `compactRegion` 的独立 `session` 参数 | 固定调用者传入的对象与 `agent.session` 上已有的是同一个;模型可见的 mount API 也能调用该方法,但接受两个身份允许挂载的插件提供不一致的配对。 | 保留手动 region seam,同时有意将其收窄为以 `agent.session` 为唯一真源。 | +| `CompactionResult.startSeq`、`summarySeq`、`endSeq` 与 `summary` | 生产消费方只读取 shadowed range/seq/token 统计;持久日志拥有 summary 和事件标识。 | 移除四个结果回显,保留两个共享的 transcript(文本记录)渲染器。 | +| `BasicCompactService` 的 estimation/summarization 可见性 | 没有包外生产调用者调用这五个方法;已实现的 RFC 只将 `estimateContentTokens()` 和 `summarize()` 命名为子类钩子。 | 将这两个方法改为 `protected`,其余三个编排专用的估算器改为 private。 | +| `CodeLogEntry.source`/`level` 与 `RunCodeMeta.dispatches` | 每个生产消费方都将日志映射为文本;没有 presenter/模型路径读取其他字段或持久化的 dispatch 计数。 | 将 code-runtime 日志改为字符串(或纯文本条目),移除 result-meta 的 dispatch 管道;保留用于生成确定性 dispatch id 的本地计数器。 | +| `ToolNotFoundError.toolName`、`SystemPrompt.config` 与 `BashTask.command` | 每个存储的公开值都没有生产读取者。 | 移除未读字段,保留错误消息、已解析的配置行为和任务生命周期。 | +| 后端包根实现辅助函数 | 下方精确清单仅通过相对路径的同包导入调用。生产命名空间导入挂载的是保留的插件契约,不读取这些属性;命名根消费方都是测试。 | 保留每个适配器/provider/服务及其配置/错误契约;停止在包根导出所列辅助函数/常量。 | +| 消费方包根实现辅助函数 | 下方精确清单只有同包生产调用者。生产命名空间导入挂载的是插件契约,不读取辅助属性;命名根消费方都是测试。 | 保留插件契约和稳定的错误码;将测试迁移到包内模块或公开行为,停止在包根导出所列辅助函数。 | ### 分组辅助导出清单 -- `dsh-llm-deepseek`:`httpErrorCode`、`serializeMessages`、`serializeRequest`、`DONE`、`parseSse`、`mapFinishReason`、`mapUsage` 和 `translate`;`dsh-llm-pi-ai`:`buildModel`、`mapStopReason`、`mapUsage`、`toPiContext` 和 `toStreamChunks`。 -- `dsh-bash-local`:`DEFAULT_GRACE_MS`、`ENV_OVERRIDES`、`killGroup`、`OutputCollector` 和 `runBash`;`dsh-bash-sandbox`:`shellQuote`、`classifyDenial` 和 `classifyRunnerFailure`;`dsh-sandbox-local`:`bwrapProfileArgs`、`landlockProfileArgs` 和 `seatbeltProfileArgs`。公开的可变测试注入字段及其类型不在本提案范围内。 -- `dsh-fs-local`:`applyLiteralEdit`、`listDirectory`、`probe`、`readForEdit`、`readTextForDiff`、`readWholeText`、`resolveLocalTarget`、`restoreLineEndings`、`streamWholeText` 和 `writeFileAtomic`。 -- `dsh-web-fetch-local`:`classifyContentType`、`decoderForCharset`、`isSameOrigin`、`parseCharset` 和 `validateFetchUrl`;`dsh-web-search-exa`:`mapExaResponse` 和 `mapExaResult`;`dsh-web-search-deepseek`:`citationSnippets` 和 `mapAnthropicResponse`;`dsh-web-search-perplexity`:`mapPerplexityResponse` 和 `mapPerplexityResult`。 -- `dsh-tool-fs`:`READ_LIMIT`、`STREAM_MIN_SIZE`、`READ_MAX_BYTES`、`READ_MAX_LINE_LENGTH`、`DIFF_CONTEXT`、`applyReadTool`、`parseReadArgs`、`applyWriteTool`、`formatWriteOutput`、`parseWriteArgs`、`applyEditTool`、`formatEditOutput`、`parseEditArgs`、`buildWindow`、`formatReadOutput`、`computeHunkDiffs` 和 `diffsFromMeta`。 -- `dsh-tool-web`:`WEB_SEARCH_MAX_RESULTS`、`applyWebSearchTool`、`formatSearchOutput`、`parseSearchArgs`、`presentSearchCall`、`applyWebFetchTool`、`formatFetchOutput`、`parseFetchArgs`、`presentFetchCall`、`renderBody` 和 `htmlToMarkdown`;`dsh-timeout-policy`:`toolTimeoutResult`;`dsh-compact-basic`:`resolveConfig`;`dsh-tool-bash`:`renderResult`。 +- `dsh-llm-deepseek`:`httpErrorCode`、`serializeMessages`、`serializeRequest`、`DONE`、`parseSse`、`mapFinishReason`、`mapUsage` 与 `translate`;`dsh-llm-pi-ai`:`buildModel`、`mapStopReason`、`mapUsage`、`toPiContext` 与 `toStreamChunks`。 +- `dsh-bash-local`:`DEFAULT_GRACE_MS`、`ENV_OVERRIDES`、`killGroup`、`OutputCollector` 与 `runBash`;`dsh-bash-sandbox`:`shellQuote`、`classifyDenial` 与 `classifyRunnerFailure`;`dsh-sandbox-local`:`bwrapProfileArgs`、`landlockProfileArgs` 与 `seatbeltProfileArgs`。公开的可变测试注入字段及其类型不在本提案范围内。 +- `dsh-fs-local`:`applyLiteralEdit`、`listDirectory`、`probe`、`readForEdit`、`readTextForDiff`、`readWholeText`、`resolveLocalTarget`、`restoreLineEndings`、`streamWholeText` 与 `writeFileAtomic`。 +- `dsh-web-fetch-local`:`classifyContentType`、`decoderForCharset`、`isSameOrigin`、`parseCharset` 与 `validateFetchUrl`;`dsh-web-search-exa`:`mapExaResponse` 与 `mapExaResult`;`dsh-web-search-deepseek`:`citationSnippets` 与 `mapAnthropicResponse`;`dsh-web-search-perplexity`:`mapPerplexityResponse` 与 `mapPerplexityResult`。 +- `dsh-tool-fs`:`READ_LIMIT`、`STREAM_MIN_SIZE`、`READ_MAX_BYTES`、`READ_MAX_LINE_LENGTH`、`DIFF_CONTEXT`、`applyReadTool`、`parseReadArgs`、`applyWriteTool`、`formatWriteOutput`、`parseWriteArgs`、`applyEditTool`、`formatEditOutput`、`parseEditArgs`、`buildWindow`、`formatReadOutput`、`computeHunkDiffs` 与 `diffsFromMeta`。 +- `dsh-tool-web`:`WEB_SEARCH_MAX_RESULTS`、`applyWebSearchTool`、`formatSearchOutput`、`parseSearchArgs`、`presentSearchCall`、`applyWebFetchTool`、`formatFetchOutput`、`parseFetchArgs`、`presentFetchCall`、`renderBody` 与 `htmlToMarkdown`;`dsh-timeout-policy`:`toolTimeoutResult`;`dsh-compact-basic`:`resolveConfig`;`dsh-tool-bash`:`renderResult`。 ## 提案 -以一次有界的、协调的公开接口面清理,移除或降级上述每一行。更新 package README、JSDoc、生成的 API/事件目录、type-equiv 记录、必要时的 exports map 以及测试,使测试通过所属的公开 seam 来验证行为,而非保留仅为测试而存在的入口点。不折叠任何能力 seam、LLM(大语言模型)适配器、持久化后端或生命周期静默契约。 +以一次有界的、协调的公开接口清理,移除或降级上述每一行。同步更新包 README、JSDoc、生成的 API/事件 catalog、type-equiv 记录、必要的 exports map 以及测试,使测试通过所属的公开 seam 验证行为,而非保留仅为测试而存在的入口。不折叠任何能力 seam、LLM(大语言模型)适配器、持久化后端或生命周期静默契约。 ## 曾考虑的替代方案 -**保留测试便利函数和自包含结果字段为公开。** 公开辅助函数可以让白盒测试更方便,自包含的结果字段看起来更符合人体工学,未来的嵌入者可能需要具体循环类或枚举方法。这些好处是假设性的;今天它们让每一处实现和文档都要解释没有已交付调用者能观察到的状态。真正的消费方可以引入它所需的最小契约,其所有权和失败语义已知。 +**保留测试便利函数和自包含的结果字段为公开。** 公开辅助函数可以让白盒测试更方便,自包含的结果字段看起来更符合人体工学,未来的嵌入者可能需要具体循环类或枚举方法。这些好处是假设性的;当前它们让每处实现和文档都要解释没有已交付调用者能观察到的状态。真正的消费方可以引入它所需的最小契约,其所有权和失败语义明确。 -**为模型编写的 mount 保留所有编目成员。** 自引用工具集是一条真实的通用消费路径,而非生成文档的噪音。然而,它的价值来自准确、可组合的服务面,而非无限期保留重复字段或不一致的参数对;上述每一项编目收缩都移除了在同一次执行、agent(智能体)或结果上其他位置已可获得的事实,并在同一个变更中更新 API 参考。 +**保留所有 catalog 成员以供模型编写的 mount 使用。** 自引用工具集是一条真实的通用消费路径,而非生成文档的噪音。然而,它的价值来自准确、可组合的服务接口,而非无限期保留重复字段或不一致的参数对;上述每一项 catalog 收缩都移除了在同一 execution、agent 或 result 上其他位置已可获得的事实,并在同一变更中更新 API 参考。 ## 验收标准 -- 精确符号搜索显示被移除的接口面不出现在本 RFC 和任何已实现 RFC 修正案之外。 -- 本 RFC 列出的每一项接口面均已按指定方式移除或降级;清单之外有意保留的扩展/测试契约不受影响。 -- 工具执行、压缩(compaction)、两个 LLM 适配器、两个持久化后端、工作流隔离以及 agent 创建/恢复保持其已交付行为。 -- 类型检查、覆盖率、快照、doc-sync(文档同步门禁)、module-graph 校验、构建和 hygiene 全部通过。 +- 精确符号搜索显示:在本 RFC 及任何已实现 RFC 修正之外,没有被移除的接口。 +- 本 RFC 列出的每个接口均按指定方式缺失或降级;清单之外有意保留的扩展/测试契约不变。 +- 工具执行、上下文压缩(context compaction)、两个 LLM 适配器、两个持久化后端、workflow 隔离以及 agent 创建/恢复保持其已交付行为。 +- 类型检查、覆盖率、快照、doc-sync、module-graph 校验、构建和 hygiene 通过。 ## 风险 -大多数移除在编译时可见但运行时无影响。压缩参数清理有意禁止 session/context 不匹配,同时保留手动区域 seam。外部预发布嵌入者和现有模型编写的 mount 可能导入更少的辅助函数、传入更少的参数或接收更窄的结果形状;这是有意的产品接口面收缩,而非仅仅是生成目录的清理。仓库尚未发布,因此承载不受支持的接口面才是更大的基础成本。 +大多数移除在编译时可见但对运行时无影响。上下文压缩参数清理有意禁止 session/context 不匹配,同时保留手动 region seam。外部预发布嵌入者和现有模型编写的 mount 可能导入更少的辅助函数、传递更少的参数或接收更窄的结果形状;这是有意的产品接口收缩,而非仅仅是生成 catalog 的清理。仓库尚未发布,因此承载不受支持的接口才是更大的基础成本。 diff --git a/docs/rfc/proposed/simplification/2026-07-12-simplify-session-log-representation.i18n.yaml b/docs/rfc/proposed/simplification/2026-07-12-simplify-session-log-representation.i18n.yaml index f7a882d65c..2e91f9e9c2 100644 --- a/docs/rfc/proposed/simplification/2026-07-12-simplify-session-log-representation.i18n.yaml +++ b/docs/rfc/proposed/simplification/2026-07-12-simplify-session-log-representation.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-12-simplify-session-log-representation.md: 52720231d6e4cbe0cbb412332cd016ba63f83569 -2026-07-12-simplify-session-log-representation.zh.md: 468f9a565177089c8d49c06e8d490ab56980054a +2026-07-12-simplify-session-log-representation.zh.md: 1286a7d3c571fac66310a613c548920c3f25812d diff --git a/docs/rfc/proposed/simplification/2026-07-12-simplify-session-log-representation.zh.md b/docs/rfc/proposed/simplification/2026-07-12-simplify-session-log-representation.zh.md index 468f9a5651..1286a7d3c5 100644 --- a/docs/rfc/proposed/simplification/2026-07-12-simplify-session-log-representation.zh.md +++ b/docs/rfc/proposed/simplification/2026-07-12-simplify-session-log-representation.zh.md @@ -6,33 +6,33 @@ Status: proposed ## 问题 -会话日志维护着两种表示,其机制开销超出了消费方的实际需求:伪链表 surface 与自定义请求头增量编码。 +会话日志维护着两种表示,其机制复杂度超出了消费方的实际需求:一个伪链表 surface 和自定义的请求头增量。 -`SurfaceManager` 将同一顺序存储在数组、seq 映射和可变的 `prev`/`next` 链接三处。生产代码从不读取 `prev`;压缩(compaction)唯一一次读取 `next` 是取数组位置的后继。替换操作已经使用 `indexOf`,因此链接并未使其主要操作达到常数时间。一个 seq 数组加线性替换查找具有相同的渐近替换开销,且只有一种表示需要校验。 +`SurfaceManager` 用一个数组、一个 seq 映射和可变的 `prev`/`next` 链接存储相同的顺序。生产代码从不读取 `prev`;压缩(compaction)唯一的 `next` 读取是取数组位置的后继。替换操作已经使用 `indexOf`,因此链接并未让其主要操作达到常数时间。一个 seq 数组加线性替换查找具有相同的渐近替换开销,且只有一种表示需要验证。 -请求头子系统实现了自定义的 system/tool 增量编解码器与传输决策层,尽管其契约声明增量只是编码优化而非可重建性要求。在每个 agent loop 实例边界保留 initial/resume 完整快照,然后在该实例的组装头发生变化时写入一条规范的完整 `request/header`,即可保留回放能力,同时删除 `SystemDelta`、`ToolsDelta`、往返 fallback 以及持久化的 `request/header-delta` 变体。编解码器专用词汇随编解码器一起消失,并非因为其各分支本身无效。 +请求头子系统实现了一套自定义的 system/tool 增量编解码器和传输决策层,尽管其契约声明增量只是编码优化,而非可重建性要求。在每个 agent loop(智能体循环)实例边界保留初始/恢复的完整快照,然后在该实例的组装头发生变化时写入一条规范的完整 `request/header`,即可保留回放能力,同时删除 `SystemDelta`、`ToolsDelta`、往返回退逻辑以及持久化的 `request/header-delta` 变体。编解码器专属的词汇随编解码器一起消失,并非因为其各分支本身无效。 -本提案有意保留 append 与 replacement 的 `sourceEventSeqs`、崩溃恢复溯源,以及所有 `SessionStartSource` 变体:已实施的 RFC 赋予了这些字段审计/拦截角色,零当前读者不足以推翻这一点。 +本提案有意保留追加和替换的 `sourceEventSeqs`、崩溃恢复来源信息以及所有 `SessionStartSource` 变体:已实施的 RFC 赋予这些字段审计/拦截角色,零当前读者这一事实不足以推翻它们。 ## 提案 -将 `SurfaceManager.nodes` 改为事件序列号的 `readonly number[]`,移除公开的 `SurfaceNode` 形状。保留内部的 replace-generation 信号;更新工具配对平衡与压缩调用方,使其通过数组值/索引获取前驱、后继与替换范围,移除节点链接与 seq-to-node 映射。将锚点后的请求头增量替换为规范的完整变更头快照,移除增量编解码器/事件/测试;initial 与 resume 锚点即使折叠后的头未变也仍为完整快照。 +将 `SurfaceManager.nodes` 改为事件序列号的 `readonly number[]`,移除公开的 `SurfaceNode` 形状。保留内部的替换代信号;更新 tool 配对平衡和压缩调用方,使其通过数组值/索引获取前驱、后继和替换范围,移除节点链接和 seq-to-node 映射。用规范的完整变更头快照替代锚点后的头增量,移除增量编解码器/事件/测试;初始和恢复锚点即使折叠后的头未变也仍为完整快照。 -修订会话 surface 与可重建请求的 RFC 中描述已移除编码的部分。更新事件类型/不变式、请求日志/回放、持久化 fixture(测试前置数据)、生成的 catalog、包文档与快照。将编解码器专用的 `fallback` 原因替换为显式的 `change` 原因(用于锚点后的完整快照),以区别于保留的 `initial` 与 `resume` 锚点。 +修订 session-surface 和 reconstructable-request RFC 中描述已移除编码的部分。更新事件类型/不变式、请求日志/回放、持久化 fixture(测试前置数据)、生成的 catalog、包文档和快照。将编解码器专属的 `fallback` 原因替换为锚点后完整快照的显式 `change` 原因,使其与保留的 `initial` 和 `resume` 锚点区分开来。 -`SESSION_FORMAT_VERSION` 有意保持为 `0`,因此包含 `request/header-delta` 的旧 v0 日志在增量折叠被删除后,若不做处理将通过版本检查并静默丢失头变更。seed/load 校验必须在格式边界处拒绝该遗留事件并快速失败;不添加兼容折叠或迁移。 +`SESSION_FORMAT_VERSION` 有意保持在 `0`,因此一份包含 `request/header-delta` 的旧 v0 日志在增量折叠被删除后,本会通过版本检查并静默丢失头变更。seed/load 校验必须在格式边界处拒绝该遗留事件并显式报错;不添加兼容性折叠或迁移。 ## 曾考虑的替代方案 -**保留链表节点与紧凑增量以备未来规模。** 链接可能有助于未来的游标 API,增量在大型工具 schema 仅有少量变化时能减小日志体积。但没有已发布的游标使用这些链接,而完整快照以磁盘空间换取显著更简单的正确性。如果头部体积确实成为问题,可以基于真实 trace 设计压缩方案或经过度量的规范增量方案。 +**保留链表节点和紧凑增量以备未来扩展。** 链接可能有助于未来的游标 API,增量在大型工具 schema 仅有少量变化时可以缩减日志。但没有已发布的游标使用这些链接,而完整快照以磁盘空间换取了显著更简单的正确性。如果头部体积确实成为问题,可以基于真实 trace 设计压缩方案或经过度量的规范增量方案。 ## 验收标准 -- `SurfaceManager.nodes` 是一个有序 seq 数组,没有 `SurfaceNode`、链接字段或 seq-to-node 映射;增量追加处理与内部 replace-generation 信号保留。 +- `SurfaceManager.nodes` 是一个有序 seq 数组,没有 `SurfaceNode`、链接字段或 seq-to-node 映射;增量追加处理和内部替换代信号保留。 - 回放完整变更头快照能重建出完全相同的请求;不再存在任何 header-delta 事件/类型/编解码器。 -- 包含遗留 `request/header-delta` 的 v0 seed 或持久化日志在回放前被拒绝,JSONL 与 SQLite 加载路径均有覆盖。 -- 新形状的 v0 JSONL/SQLite 回放、溯源、崩溃恢复、压缩、快照、不变式、类型检查、覆盖率、doc-sync、构建与 hygiene 全部通过。 +- 包含遗留 `request/header-delta` 的 v0 seed 或持久化日志在回放前被拒绝,JSONL 和 SQLite 加载路径均有覆盖率。 +- 新形状的 v0 JSONL/SQLite 回放、来源信息、崩溃恢复、压缩、快照、不变式、类型检查、覆盖率、doc-sync 和 hygiene 全部通过。 ## 风险 -完整头会增加日志体积,线性替换查找在非常大的 surface 上可能更慢。替换操作目前已经是线性的,因为实现调用了 `indexOf`;只有在真实 trace 表明更简单的数组成为瓶颈时才应添加基准测试。由于格式版本保持为 `0`,如果遗漏了对遗留事件的显式拒绝,后果将是静默数据损坏而非类型错误;因此快速失败的加载测试是本提案的组成部分,而非可选的清理工作。 +完整头会增加日志体积,线性替换查找在非常大的 surface 上可能更慢。替换操作已经是线性的,因为实现调用了 `indexOf`;只有当真实 trace 表明更简单的数组成为瓶颈时才应添加基准测试。由于格式版本保持为 `0`,如果遗漏了对遗留事件的显式拒绝,后果将是静默数据损坏而非类型错误;因此显式报错的加载测试是本提案的组成部分,而非可选的清理工作。 diff --git a/docs/rfc/proposed/testing/2026-06-11-deterministic-and-stress-testing.i18n.yaml b/docs/rfc/proposed/testing/2026-06-11-deterministic-and-stress-testing.i18n.yaml index 3e3afa30d0..13dc3e72b9 100644 --- a/docs/rfc/proposed/testing/2026-06-11-deterministic-and-stress-testing.i18n.yaml +++ b/docs/rfc/proposed/testing/2026-06-11-deterministic-and-stress-testing.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-11-deterministic-and-stress-testing.md: e4ed7043d1880b55dd7d77b3e09a81dd58739a70 -2026-06-11-deterministic-and-stress-testing.zh.md: d933c1dda329b95573b7a5a3fbe3a61ef94390cb +2026-06-11-deterministic-and-stress-testing.zh.md: 4e4ee9d28025a43349b702e399096535539a91d2 diff --git a/docs/rfc/proposed/testing/2026-06-11-deterministic-and-stress-testing.zh.md b/docs/rfc/proposed/testing/2026-06-11-deterministic-and-stress-testing.zh.md index d933c1dda3..4e4ee9d280 100644 --- a/docs/rfc/proposed/testing/2026-06-11-deterministic-and-stress-testing.zh.md +++ b/docs/rfc/proposed/testing/2026-06-11-deterministic-and-stress-testing.zh.md @@ -6,28 +6,28 @@ Status: proposed ## 问题 -若干 agent loop(智能体循环)测试通过 `setTimeout(30)` 睡眠来同步——这是一笔不稳定性债务,浪费 agent 重试周期,还可能掩盖排序 bug。另一方面,我们的核心架构承诺(任何会话日志回放后都能得到完全相同的派生历史)目前只在两个测试中断言,但在*所有地方*断言的成本很低。此外,inbox 唤醒竞态只被手动验证过一次,没有任何东西持续地重新验证它。 +若干 agent loop(智能体循环)测试通过 `setTimeout(30)` 睡眠来同步——这是一笔不稳定性债务,浪费 agent 的重试周期,还可能掩盖时序 bug。另外,我们的核心架构承诺(任何会话日志回放后都能得到相同的派生历史)目前只在两个测试中断言,但在**所有**测试中断言的成本极低。此外,inbox 唤醒竞态只被手动验证过一次,没有任何机制持续复验。 ## 提案 三项措施: -1. **测试中禁止挂钟睡眠。** 将 `setTimeout(N)` 等待替换为事件驱动等待(现有的 `waitForIdle` 模式,扩展为 `waitForStatus`、`waitForEvent(n)`),或在需要测试时间本身时使用 vitest fake timers。通过 lint 规则强制:禁止在 `packages/*/tests` 中使用 `setTimeout`,白名单辅助模块除外。 -2. **通用回放 fixture(测试前置数据)。** 一个共享的测试辅助函数包装 agent loop harness,使得每个测试结束后,agent 的会话日志被回放到一个全新的 Session 中,并自动断言 `deriveMessages()` 相等。这样该不变式在每次 CI 运行中会被检查数百次(覆盖套件产生的所有场景),而非仅两次。 -3. **夜间竞态压力测试。** 一个 CI job 以 `vitest --repeat=200`(加 `--shuffle`)运行 agent-loop 和 inbox 套件,以暴露调度依赖的失败;发现的任何不稳定测试都作为 bug 修复,绝不靠重试掩盖。 +1. **测试中禁止挂钟睡眠。** 将 `setTimeout(N)` 等待替换为事件驱动等待(既有的 `waitForIdle` 模式,扩展为 `waitForStatus`、`waitForEvent(n)`),或在需要测试时间本身时使用 vitest 的 fake timer。通过 lint 规则强制执行:禁止在 `packages/*/tests` 中使用 `setTimeout`,白名单辅助模块除外。 +2. **通用回放 fixture(测试前置数据)。** 一个共享测试辅助函数包装 agent loop harness,使每个测试结束后,agent 的会话日志被回放到一个全新的 Session 中,并自动断言 `deriveMessages()` 相等。这样该不变式在每次 CI 运行中会被套件产生的所有场景检查数百次,而非仅两次。 +3. **夜间竞态压力测试。** 一个 CI job 以 `vitest --repeat=200`(加 `--shuffle`)运行 agent-loop 和 inbox 套件,以暴露调度依赖的失败;发现的任何不稳定测试都视为 bug 修复,绝不靠重试掩盖。 ## 计划 -措施 1 和 2 一起落地(它们改动相同的辅助模块);在套件消除所有睡眠之后再添加夜间 job,使重复运行足够快。 +措施 1 和 2 一起落地(它们改动相同的辅助模块);在套件消除所有睡眠后再添加夜间 job,以确保重复运行速度快。 ## 验收标准 -- `packages/*/tests` 中不再有 `setTimeout`(白名单辅助模块除外),由 lint 规则强制。 -- 共享 harness 对每个测试的会话日志进行回放,将其注入全新的 `Session` 并自动断言 `deriveMessages()` 相等,覆盖整个套件。 -- 夜间 job 以 `--repeat` 和 `--shuffle` 运行 agent-loop 和 inbox 套件;发现的不稳定测试作为 bug 分诊处理,绝不靠重试掩盖。 +- `packages/*/tests` 中不再有 `setTimeout`(白名单辅助模块除外),由 lint 规则强制执行。 +- 共享 harness 将每个测试的会话日志回放到全新的 `Session` 中,并自动断言 `deriveMessages()` 相等,覆盖整个套件。 +- 夜间 job 以 `--repeat` 和 `--shuffle` 运行 agent-loop 和 inbox 套件;发现的不稳定测试作为 bug 分诊,绝不靠重试掩盖。 ## 风险 -Fake timers 与 agent loop 中的 Promise 调度存在微妙交互——优先使用事件驱动等待;仅在测试 timer 服务行为本身时才使用 fake timers。 +Fake timer 与 agent loop 中的 Promise 调度存在微妙交互——优先使用事件驱动等待;仅在测试 timer 服务行为本身时才使用 fake timer。 <!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> diff --git a/docs/rfc/proposed/testing/2026-06-11-mutation-testing.i18n.yaml b/docs/rfc/proposed/testing/2026-06-11-mutation-testing.i18n.yaml index 3f6c6157fa..3bf5967b45 100644 --- a/docs/rfc/proposed/testing/2026-06-11-mutation-testing.i18n.yaml +++ b/docs/rfc/proposed/testing/2026-06-11-mutation-testing.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-11-mutation-testing.md: 344263d1c91a5e6c83320f367bf76ed6f7ef5a49 -2026-06-11-mutation-testing.zh.md: aa89b3335a143b6f34858bdc2f3344a6a7d758d9 +2026-06-11-mutation-testing.zh.md: 28bb7253c12827dbcddd141481f26f60b3a72b7a diff --git a/docs/rfc/proposed/testing/2026-06-11-mutation-testing.zh.md b/docs/rfc/proposed/testing/2026-06-11-mutation-testing.zh.md index aa89b3335a..28bb7253c1 100644 --- a/docs/rfc/proposed/testing/2026-06-11-mutation-testing.zh.md +++ b/docs/rfc/proposed/testing/2026-06-11-mutation-testing.zh.md @@ -1,4 +1,4 @@ -# RFC:变异测试作为覆盖率的制衡 +# RFC:变异测试作为覆盖率的制衡手段 [English](2026-06-11-mutation-testing.md) | 中文 @@ -6,31 +6,31 @@ Status: proposed ## 问题 -逐文件 100% 覆盖率门禁(见[质量门禁决策](../../implemented/process/2026-06-11-quality-gates.md))证明的是每一行都在测试中*被执行*了,而非任何断言会在该行出错时有所察觉。在 agent 编写测试的场景下,覆盖率压力可能催生「执行但无断言」的测试。变异测试衡量的正是覆盖率无法衡量的:测试套件是否能*杀死*被刻意注入的缺陷。 +逐文件 100% 覆盖率门禁([质量门禁决策](../../implemented/process/2026-06-11-quality-gates.md))证明每一行代码在测试中都被*执行*了,但不能证明如果该行出错,任何断言会注意到。在 agent(智能体)编写测试的场景下,覆盖率压力可能产出「执行但不断言」的测试。变异测试衡量的正是覆盖率无法衡量的:测试套件是否能*杀死*被刻意注入的缺陷。 ## 提案 在 `packages/*/src` 上运行 Stryker(`@stryker-mutator/vitest-runner`): -- **PR 粒度的增量运行**(仅变更文件),作为 CI job:调优后足够快,可以作为合并门禁。 -- **每夜全量运行**,跟踪变异分数;先记录基线,再将阈值设为观测到的基线值并只升不降(与覆盖率策略一致:阈值只收紧)。 -- 存活的变异体是待办工作项:agent 选取一个存活体、编写杀死它的测试、循环往复——一个形态良好的自主循环。 -- 等价变异体(可证明不改变行为的)加带理由的排除注解,与 `/* v8 ignore */` 策略对称。 +- **PR 范围的增量运行**(仅变更文件),作为一个 CI job。调优后速度足以作为合并门禁。 +- **每夜全量运行**,跟踪变异分数;先记录基线,再将阈值设为观测到的基线并只升不降(与覆盖率策略一致:阈值只收紧)。 +- 存活的变异体是待办项:agent 选取一个存活体、编写杀死它的测试、循环往复——一个形态良好的自主循环。 +- 等价变异体(可证明不改变行为的)加注释排除并附理由,与 `/* v8 ignore */` 策略一致。 ## 计划 1. 添加 Stryker 配置,范围限定在一个包(llm:最小、最具算法性),测量运行时间。 2. 扩展到所有包;在配置中记录基线分数。 -3. 接入每夜 job;当运行时间可接受后,添加 PR 粒度的增量 job。 +3. 接入每夜 job;运行时间可接受后再添加 PR 范围的增量 job。 ## 验收标准 -- Stryker 配置在 `packages/*/src` 上以 vitest runner 运行;每夜 job 记录变异分数,且当分数低于记录的基线时,运行失败(阈值只升不降)。 -- PR 粒度的增量运行在运行时间可接受后作为合并门禁;或者明确保持仅每夜运行,并将该结论记录于此。 -- 等价变异体带有附理由的排除注解,与 `/* v8 ignore */` 策略对称。 +- Stryker 配置在 `packages/*/src` 上以 vitest runner 运行;每夜 job 记录变异分数,当分数低于记录的基线时,通过只升不降的阈值使运行失败。 +- PR 范围的增量运行在运行时间可接受后作为合并门禁;或者明确保持仅每夜运行,并将该结论记录于此。 +- 等价变异体带有注释排除及理由,与 `/* v8 ignore */` 策略一致。 ## 风险 -运行时间:变异测试开销大;逐文件 100% 覆盖率有所帮助(每个变异体至少会被执行到)。如果 PR 粒度的运行始终太慢,则保持仅每夜运行,依赖分数只升不降的机制。 +运行时间:变异测试开销大;逐文件 100% 覆盖率有所帮助(每个变异体至少会被执行到)。如果 PR 范围的运行始终过慢,则保持仅每夜运行,依赖分数只升不降的机制。 <!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> diff --git a/docs/rfc/rejected/architecture/2026-06-11-immutable-public-surfaces.i18n.yaml b/docs/rfc/rejected/architecture/2026-06-11-immutable-public-surfaces.i18n.yaml index 3c2a50a714..f7c21ad005 100644 --- a/docs/rfc/rejected/architecture/2026-06-11-immutable-public-surfaces.i18n.yaml +++ b/docs/rfc/rejected/architecture/2026-06-11-immutable-public-surfaces.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-11-immutable-public-surfaces.md: 68472d9817f0de22777314927f05f90949903723 -2026-06-11-immutable-public-surfaces.zh.md: e067b48d9a936133abdf149fbb9ff626c9411851 +2026-06-11-immutable-public-surfaces.zh.md: 7dc42ef9ff07682c1bbac1ca61caba49596cb7bc diff --git a/docs/rfc/rejected/architecture/2026-06-11-immutable-public-surfaces.zh.md b/docs/rfc/rejected/architecture/2026-06-11-immutable-public-surfaces.zh.md index e067b48d9a..7dc42ef9ff 100644 --- a/docs/rfc/rejected/architecture/2026-06-11-immutable-public-surfaces.zh.md +++ b/docs/rfc/rejected/architecture/2026-06-11-immutable-public-surfaces.zh.md @@ -2,28 +2,28 @@ [English](2026-06-11-immutable-public-surfaces.md) | 中文 -Status: rejected — the pervasive `DeepReadonly<T>` type flip is replaced by source-owned runtime immutability in `Session` plus relational development assertions. See [source-owned session immutability and dev-mode invariants](../../implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md). +Status: rejected — 全面使用 `DeepReadonly<T>` 类型翻转的方案已被替换为 `Session` 中由源拥有的运行时不可变性加关系型开发断言。见[源拥有的会话不可变性与开发模式不变式](../../implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md)。 ## 问题 -被否决的提案针对的是一个所有权漏洞:仅靠 `readonly SessionEvent[]` 类型无法封堵该漏洞,因为数组元素在运行时仍然可变,一次类型断言或纯 JavaScript 就能改写嵌套的历史记录。最终实现的设计在 `Session` 中通过物化并深度冻结每个已接受的事件、返回冻结的数组快照来封堵该漏洞。进行中的 prompt waterfall(瀑布式事件)被有意保留为可变,因此不可变性是一条所有权边界,而非一条覆盖全局的类型规则。 +被否决的提案针对的是一个所有权漏洞:仅靠 `readonly SessionEvent[]` 类型无法封堵该漏洞,因为其元素在运行时仍然可变,类型强制转换或纯 JavaScript 代码可以改写嵌套的历史记录。已实现的设计在 `Session` 中封堵了这一漏洞:对每个被接受的事件进行物化并深度冻结,返回冻结的数组快照。进行中的 prompt waterfall(瀑布式事件)有意保持可变换,因此不可变性是一条所有权边界,而非一条全局类型规则。 ## 提案 -> **实际实现方式不同——见 Status 行与 [source-owned session immutability and dev-mode invariants](../../implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md)。** 下文的 `DeepReadonly<T>` 设计已被否决:它仅在编译期生效、对消费方噪音大、且可被 cast 绕过。`Session` 改为在每次组合中对已接受的事件和公开日志快照进行快照与深度冻结;`deriveMessages()` 返回分离的冻结投影;开发插件检查跨记录与跨 seam 的关系约束。 +> **实际采用了不同的实现方式——见 Status 行与[源拥有的会话不可变性与开发模式不变式](../../implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md)。** 下文的 `DeepReadonly<T>` 设计已被否决:它仅在编译期生效、对消费方噪音大、且可被强制转换绕过。`Session` 改为在每次组合中对已接受的事件和公开日志快照进行快照与深度冻结;`deriveMessages()` 返回分离的冻结投影;开发插件检查跨记录与跨 seam 的关系。 -在类型层面将不可变性施加于「变异即腐败」的位置: +在类型层面为「突变即损坏」的场景引入不可变性: -- `SessionEvent` 数据在从会话**输出**时(`events`、`session/event` 监听器)变为 `DeepReadonly`;`append()` 仍接受普通可变输入。一个 `DeepReadonly<T>` 工具类型放入 dsh-llm,与 brand/never 辅助类型并列。 -- `deriveMessages()` 返回深度只读的消息;agent loop(智能体循环)在将可变请求交给 `agent/request` waterfall 之前先克隆一份(在 waterfall 中变异是被允许的——克隆使边界显式且廉价,每步仅一次)。 -- `PromptAssembly` 在其 waterfall 流程中保持可变(被允许),但注册表的内部 section 列表在每次组装时被克隆(已有此行为)。 +- `SessionEvent` 数据在从会话**输出**时(`events`、`session/event` 监听器)变为 `DeepReadonly`;`append()` 仍接受普通可变输入。一个 `DeepReadonly<T>` 工具类型放在 dsh-llm 中,与 brand/never 辅助类型相邻。 +- `deriveMessages()` 返回深度只读的消息;agent loop(智能体循环)在将可变请求交给 `agent/request` waterfall 之前先克隆(该处的突变是被允许的——克隆使边界显式且代价低廉,每个步骤仅一次)。 +- `PromptAssembly` 在其 waterfall 流经期间保持可变(被允许),但注册表内部的 section 列表在每次组装时被克隆(已有此行为)。 ## 计划 -引入 `DeepReadonly`,翻转会话的读取路径,并修复消费方由此产生的编译错误。 +引入 `DeepReadonly`,翻转会话的读取路径,并修复消费方中由此产生的编译错误。 ## 风险 -`DeepReadonly` 类型会在 waterfall 边界处产生噪音错误——因为变异在那里正是 API 的一部分。应将可变/只读边界严格限定在「已记录 vs 进行中」,并在会话 README 中加以说明。 +`DeepReadonly` 类型在 waterfall 边界处(突变本身就是 API 的地方)可能产生噪音较大的错误。应将可变/只读边界精确地划在「已记录 vs 进行中」,并在 session README 中加以说明。 <!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> diff --git a/docs/rfc/rejected/architecture/2026-06-20-providerless-example-base.i18n.yaml b/docs/rfc/rejected/architecture/2026-06-20-providerless-example-base.i18n.yaml index ba7de6f475..11ed0efa0b 100644 --- a/docs/rfc/rejected/architecture/2026-06-20-providerless-example-base.i18n.yaml +++ b/docs/rfc/rejected/architecture/2026-06-20-providerless-example-base.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-20-providerless-example-base.md: be5122ec6665dcea15619f3cb4b3ed3a2fa03972 -2026-06-20-providerless-example-base.zh.md: 011636bbdfb782a5b6e4c03695cd0a4a428993d5 +2026-06-20-providerless-example-base.zh.md: f01451f719f0fe1bbb50806aea40086e7fe08350 diff --git a/docs/rfc/rejected/architecture/2026-06-20-providerless-example-base.zh.md b/docs/rfc/rejected/architecture/2026-06-20-providerless-example-base.zh.md index 011636bbdf..f01451f719 100644 --- a/docs/rfc/rejected/architecture/2026-06-20-providerless-example-base.zh.md +++ b/docs/rfc/rejected/architecture/2026-06-20-providerless-example-base.zh.md @@ -1,4 +1,4 @@ -# RFC:使共享示例基础配置不依赖提供方 +# RFC:使共享示例基础配置与提供方无关 [English](2026-06-20-providerless-example-base.md) | 中文 @@ -6,26 +6,26 @@ Status: rejected — superseded by [Extract example apps into packages](../../im ## 问题 -示例曾有两个共享基础文件:`examples/base-core.yml` 不依赖任何模型提供方,而 `examples/base.yml` 在此核心之上加入了真实的 `llm-deepseek` 适配器。快照回放需要搭配 `llm-replay` 使用那个提供方无关的核心,因为在没有 key 的情况下加载真实适配器会抛错。常规演示则需要真实适配器。结果是命名倒挂:名为 `base.yml` 的文件并非所有示例的可复用基础,而真正的基础反而叫 `base-core.yml`。 +示例曾有两个共享基础文件:`examples/base-core.yml` 与提供方无关,而 `examples/base.yml` 在该核心基础上加入了真实的 `llm-deepseek` 适配器。快照回放需要与提供方无关的核心配合 `llm-replay` 使用,因为在没有密钥的情况下加载真实适配器会抛出异常。常规演示则需要真实适配器。结果是命名与实际含义倒挂:名为 `base.yml` 的文件并非所有示例可复用的基础,而真正的基础反倒是 `base-core.yml`。 -这种拆分可以理解,但它让每次解释配置都变得更长。它还导致了别扭的测试搭建方式:keyless 冒烟测试需要携带一个假 API key 才能让适配器启动,尽管模型根本不会被调用。 +这种拆分可以理解,但它让每次解释配置都变得更冗长。它还导致了别扭的测试搭建方式,例如无密钥冒烟测试不得不携带一个虚拟 API key,仅仅为了让适配器能启动——尽管模型根本不会被调用。 ## 提案 -将提供方无关的核心重命名为 `examples/base.yml`,让适配器选择在每个具体示例中显式声明。编码与 ACP 真实配置添加一小段 `llm-deepseek` include 或本地块;快照配置添加 `llm-replay`。删除 `examples/base-core.yml`。 +将与提供方无关的核心重命名为 `examples/base.yml`,让适配器选择在每个具体示例中显式声明。编码和 ACP 真实配置添加一小段 `llm-deepseek` include 或本地块;快照配置添加 `llm-replay`。删除 `examples/base-core.yml`。 -共享基础应当只包含提供方无关的服务与工具:`llm`、会话、系统提示词、工具、agent、不变式、bash 执行器与 bash 工具 schema。任何选择模型提供方的内容都属于叶子配置。 +共享基础应仅包含提供方无关的服务与工具:`llm`、会话、系统提示词、工具、agent、不变式、bash 执行器和 bash 工具 schema。任何涉及模型提供方选择的内容都应放在叶子配置中。 ## 验收标准 -- `examples/base.yml` 不依赖任何提供方。 +- `examples/base.yml` 与提供方无关。 - `examples/base-core.yml` 已删除。 - 真实演示配置显式添加 DeepSeek 适配器。 -- 快照回放配置引入同一个提供方无关的基础及其回放适配器。 -- [examples README](../../../../examples/README.md)、各示例的 README 与 RFC 引用不再解释"base = base-core 加适配器"。 +- 快照回放配置 include 同一个与提供方无关的基础,并加入其回放适配器。 +- [examples README](../../../../examples/README.md)、各示例 README 及 RFC 引用不再解释「base = base-core 加适配器」。 ## 放弃了什么 -真实演示失去了一层便利:每个都必须显式引入适配器。对示例而言这是正确的默认值,因为适配器选择是可变部分,而提供方无关的接线才是共享的产品核心。 +真实演示失去了一层便利:每个演示都必须显式引入适配器。对于示例而言这是正确的默认行为,因为适配器选择是可变部分,而与提供方无关的接线才是共享的产品核心。 <!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> diff --git a/docs/rfc/rejected/simplification/2026-06-20-assembled-assistant-messages-only.i18n.yaml b/docs/rfc/rejected/simplification/2026-06-20-assembled-assistant-messages-only.i18n.yaml index 243b308c50..ec03777980 100644 --- a/docs/rfc/rejected/simplification/2026-06-20-assembled-assistant-messages-only.i18n.yaml +++ b/docs/rfc/rejected/simplification/2026-06-20-assembled-assistant-messages-only.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-20-assembled-assistant-messages-only.md: 7605f286cf5914a127f5f8e2b77490648b42cc30 -2026-06-20-assembled-assistant-messages-only.zh.md: e282c5bd74fd3b854c85112d64de44a5470007bc +2026-06-20-assembled-assistant-messages-only.zh.md: 05f15ffadba4607bc64fdf2f7eefdbcc41cf03a3 diff --git a/docs/rfc/rejected/simplification/2026-06-20-assembled-assistant-messages-only.zh.md b/docs/rfc/rejected/simplification/2026-06-20-assembled-assistant-messages-only.zh.md index e282c5bd74..05f15ffadb 100644 --- a/docs/rfc/rejected/simplification/2026-06-20-assembled-assistant-messages-only.zh.md +++ b/docs/rfc/rejected/simplification/2026-06-20-assembled-assistant-messages-only.zh.md @@ -1,36 +1,36 @@ -# RFC:只持久化已组装的 assistant 消息,不持久化流式分片 - -Status: rejected — high-fidelity chunk replay, partial failed streams, and snapshot replay currently depend on persisted `assistant/chunk` events. Dropping chunks is only viable with a no-information-loss replay/artifact replacement. +# RFC:仅持久化组装后的 assistant 消息,不存储流式分片 [English](2026-06-20-assembled-assistant-messages-only.md) | 中文 +Status: rejected — high-fidelity chunk replay, partial failed streams, and snapshot replay currently depend on persisted `assistant/chunk` events. Dropping chunks is only viable with a no-information-loss replay/artifact replacement. + ## 问题 -当前的规范会话日志会逐条持久化模型流出的每个 `assistant/chunk`。[会话持久化 RFC](../../implemented/architecture/2026-06-14-session-persistence.md) 选择这一方案是为了 token 级别的回放保真度和连续的 `seq`,但代价日益增长:JSONL fixture(测试前置数据)被大量微小的 delta 记录占据;快照场景通过分组 chunk 事件来回放模型;ACP 加载时需要从 chunk 重建先前的 assistant 输出;任何未来的日志读取方都必须区分持久的消息历史与 token 级别的追踪。 +当前的规范会话日志会持久化模型流式输出的每一个 `assistant/chunk`。[会话持久化 RFC](../../implemented/architecture/2026-06-14-session-persistence.md) 选择这一方案是为了 token 级别的回放保真度和连续的 `seq`,但其代价日益增长:JSONL fixture(测试前置数据)被大量微小的 delta 记录占据,快照场景通过分组 chunk 事件来回放模型,ACP(Agent Client Protocol)加载时从 chunk 重建先前的 assistant 输出,而任何未来的日志读取方都必须区分持久的消息历史与 token 级别的追踪。 -对于成功完成并组装出完整内容的步骤,agent loop(智能体循环)已经追加了一条 `assistant/message`。这正是 `deriveMessages()` 用来构造下一次模型请求的事件。换言之,正常的可恢复对话状态已经存在,无需 chunk;chunk 是实时渲染和确定性测试的产物,不是必需的对话历史。失败或中止的流则不同:部分 assistant 输出可能仅以 chunk 形式存在,而空的 max-token 步骤可能根本不产生 `assistant/message`。 +对于成功组装出完整内容的步骤,agent loop(智能体循环)已经追加了一条 `assistant/message`。这正是 `deriveMessages()` 用来构造下一次模型请求的事件。换言之,正常的可恢复会话状态无需 chunk 即已具备;chunk 是实时渲染和确定性测试的产物,不是必需的会话历史。失败或中止的流则不同:部分 assistant 输出可能仅以 chunk 形式存在,而空的 max-token 步骤可能根本不产生 `assistant/message`。 ## 提案 -停止在规范会话日志中存储 `assistant/chunk`。持久日志只保留 `assistant/message`、`tool/call`、`tool/result`、保留的 `usage`,以及轮次边界。实时 UI 仍可通过一个刻意设计为瞬态的流事件接收 token 增量。快照回放应将其模型脚本移入显式的 fixture 伴随文件,或从已记录的适配器产物派生,而不是把规范的用户会话当作 token 磁带。需要部分失败流输出的场景必须在回放 fixture 中记录该输出。 +停止在规范会话日志中存储 `assistant/chunk`。持久日志保留 `assistant/message`、`tool/call`、`tool/result`、`usage`(如保留)以及轮次边界。实时 UI 仍可通过一个刻意设计为瞬态的流事件接收 token 增量。快照回放应将其模型脚本移入显式的 fixture 伴随文件,或从记录的适配器产物中派生,而非将规范的用户会话当作 token 磁带。需要部分失败流输出的场景必须在回放 fixture 中记录该输出。 -ACP 的 `session/load` 可以将先前的 assistant 消息作为完整内容块回放,而不是模拟原始的 token 流。加载的 transcript(文本记录)不必重现每一个历史 delta;它必须展示相同的已完成 assistant 内容,并以有效的提供方历史恢复对话。 +ACP `session/load` 可以将先前的 assistant 消息作为完整内容块回放,而非模拟原始的 token 流。加载后的 transcript(文本记录)无需重现每一个历史 delta;它必须展示相同的已完成 assistant 内容,并以有效的 provider 历史恢复运行。 ## 验收标准 -- `SessionEventMap` 移除 `assistant/chunk`,或在需要过渡性实时事件时将其标记为不持久化。 -- [会话持久化文档](../../../../packages/session-persistence/session-persistence/README.md)不再要求逐条存储每个流式分片。 -- `llm-replay` 与 ACP 快照使用显式的回放 fixture 格式或伴随文件来承载模型 chunk。 +- `SessionEventMap` 移除 `assistant/chunk`,或在需要过渡性实时事件时将其标记为非持久化。 +- [会话持久化文档](../../../../packages/session-persistence/session-persistence/README.md)不再要求逐字存储每个流式分片。 +- `llm-replay` 和 ACP 快照使用显式的回放 fixture 格式或伴随文件来存储模型 chunk。 - `session/load` 从 `assistant/message` 渲染已完成的 assistant 消息。 - 存储的日志大幅缩小,且在没有 chunk 空洞的情况下保持 `seq` 连续。 - 会话格式版本与已记录的 fixture 一并刷新;按预发布格式策略拒绝非当前版本的存储日志。 ## 放弃了什么 -规范的用户会话不再能重建旧轮次的精确 token 流。它还会丢失失败或中止流的部分 assistant 输出,除非有其他事件或 fixture 记录了它。对于当前的恢复、加载和快照契约而言,这是过大的信息损失。需要精确确定性流的测试应当自行拥有该 fixture,前提是生产会话日志为用户可见的恢复保留了足够的保真度。 +规范的用户会话不再能重建旧轮次的精确 token 流。它也会丢失失败或中止流的部分 assistant 输出,除非另有事件或 fixture 记录。对于当前的恢复、加载和快照契约而言,这是过大的信息损失。需要精确确定性流的测试应当直接拥有该 fixture,前提是生产会话日志为用户可见的恢复保留了足够的保真度。 ## 相关 -本 RFC 取代[会话持久化](../../implemented/architecture/2026-06-14-session-persistence.md)中关于 chunk 持久化的决策,并影响 [ACP 快照测试](../../implemented/testing/2026-06-19-acp-snapshot-tests.md)——其当前的回放插件从 `assistant/chunk` 事件派生脚本。 +本 RFC 取代 [会话持久化](../../implemented/architecture/2026-06-14-session-persistence.md) 中关于 chunk 持久化的决策,并影响 [ACP 快照测试](../../implemented/testing/2026-06-19-acp-snapshot-tests.md)——其当前的回放插件从 `assistant/chunk` 事件派生脚本。 <!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> diff --git a/docs/rfc/rejected/simplification/2026-06-20-drop-acp-session-load.i18n.yaml b/docs/rfc/rejected/simplification/2026-06-20-drop-acp-session-load.i18n.yaml index 9d083a9f72..35f0ecedd9 100644 --- a/docs/rfc/rejected/simplification/2026-06-20-drop-acp-session-load.i18n.yaml +++ b/docs/rfc/rejected/simplification/2026-06-20-drop-acp-session-load.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-20-drop-acp-session-load.md: 39f24d313db6083db45ff6fbc4a84f504e7714cb -2026-06-20-drop-acp-session-load.zh.md: 003c66636a75e21199f3c3ac600867cf9b73b87c +2026-06-20-drop-acp-session-load.zh.md: b7339f492d5f6b7e2daab5820dada0fa2b5fe823 diff --git a/docs/rfc/rejected/simplification/2026-06-20-drop-acp-session-load.zh.md b/docs/rfc/rejected/simplification/2026-06-20-drop-acp-session-load.zh.md index 003c66636a..b7339f492d 100644 --- a/docs/rfc/rejected/simplification/2026-06-20-drop-acp-session-load.zh.md +++ b/docs/rfc/rejected/simplification/2026-06-20-drop-acp-session-load.zh.md @@ -1,29 +1,29 @@ -# RFC:移除 ACP session/load,待恢复功能具备产品形态后再引入 - -Status: rejected — Zed is the current target ACP client, advertises and exercises load-capable sessions, and keeps pending-load state for concurrent `session/load`. The bridge should keep `session/load` and make the resume contract solid. +# RFC:移除 ACP session/load,直到 resume 具备产品形态 [English](2026-06-20-drop-acp-session-load.md) | 中文 +Status: rejected — Zed 是当前目标 ACP 客户端,它声明并使用支持 load 的会话,且为并发 `session/load` 维护 pending-load 状态。bridge 应保留 `session/load` 并使 resume 契约更加稳固。 + ## 问题 -ACP(Agent Client Protocol)当前通告 `loadSession: true` 并实现了 `session/load`:向 bridge 注入持久化能力、校验 cwd 与存储元数据的一致性、从持久化日志重建 agent、并向客户端回放先前的 transcript(文本记录)更新。这条路径有自己的竞态处理、loading-id 守卫、回放展示逻辑和测试。它还依赖规范日志保留足够的 UI 数据来重建旧的分片和工具展示。 +ACP(Agent Client Protocol)声明 `loadSession: true` 并实现 `session/load`:向 bridge 注入持久化能力、校验 cwd 与存储元数据的一致性、从持久化日志重建 agent(智能体),并向客户端回放先前的 transcript(文本记录)更新。该路径有自己的竞态处理、loading-id 守卫、回放展示逻辑和测试。它还依赖规范日志保留足够的 UI 数据,以重建旧的分片和工具展示。 -持久化本身仍是基础能力,但编辑器可见的恢复功能尚未经过产品流程设计。目前没有会话选择器、没有标题/预览元数据,对加载失败或部分加载也没有清晰的用户体验。bridge 正在为一个仅由测试、文档和当前目标客户端的会话模型所使用的功能承担复杂度。 +持久化仍然是基础能力,但编辑器可见的 resume 尚未经过产品流程设计。目前没有会话选择器、没有标题/预览元数据,也没有明确的加载失败或部分加载的用户体验。bridge 正在为一个仅被测试、文档和当前目标客户端的会话模型所使用的功能付出复杂度代价。 ## 提案 -暂时只支持新建会话。`initialize` 通告 `loadSession: false` 或省略该能力,`session/load` 不予支持。持久化仍可供 agent loop(智能体循环)和测试使用;如果其他消费方需要,恢复功能仍可作为底层工厂存在。编辑器 bridge 应在具备真正的会话选择 UX 和稳定的加载 transcript 契约后,再重新引入 `session/load`。 +当前阶段,ACP 仅启动全新会话。`initialize` 声明 `loadSession: false` 或省略该能力,`session/load` 不予支持。持久化仍可供 agent loop(智能体循环)和测试使用;如果其他消费方需要,resume 仍可作为底层工厂存在。编辑器 bridge 应在具备真正的会话选择 UX 和稳定的 load transcript 契约后,再重新引入 `session/load`。 ## 验收标准 - ACP 不再仅为 `session/load` 注入 `sessionPersistence`。 -- `initialize` 不通告加载支持。 -- `session/load` 处理器、loading-id 追踪、已加载会话的 cwd 预检以及加载回放测试全部移除。 -- 快照 fixture(测试前置数据)不再依赖加载回放的展示逻辑。 -- [ACP 文档](../../../../packages/ui/acp/README.md)仅描述新建会话的支持。 +- `initialize` 不再声明 load 支持。 +- `session/load` handler、loading-id 追踪、已加载会话的 cwd 预检以及 load 回放测试均被移除。 +- 快照 fixture(测试前置数据)不再依赖 load 回放展示。 +- [ACP 文档](../../../../packages/ui/acp/README.md)仅描述全新会话的支持。 -## 放弃了什么 +## 放弃的能力 -编辑器无法通过 ACP 重新打开先前持久化的会话。这确实是一个有价值的产品功能,但当前实现超前于 UX 设计,且将 bridge 绑定在 token 级别的日志回放上。保留持久化但移除编辑器加载,将 bridge 收窄到它当前能干净呈现的工作流。 +编辑器无法通过 ACP 重新打开先前持久化的会话。这确实是一项产品功能,但当前实现超前于 UX 设计,且将 bridge 绑定到 token 级别的日志回放。保留持久化但移除编辑器 load,可将 bridge 收窄到它当前能干净呈现的工作流。 <!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> diff --git a/docs/rfc/rejected/simplification/2026-06-20-drop-acp-terminal-meta.i18n.yaml b/docs/rfc/rejected/simplification/2026-06-20-drop-acp-terminal-meta.i18n.yaml index 61557e984b..16827c901b 100644 --- a/docs/rfc/rejected/simplification/2026-06-20-drop-acp-terminal-meta.i18n.yaml +++ b/docs/rfc/rejected/simplification/2026-06-20-drop-acp-terminal-meta.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-20-drop-acp-terminal-meta.md: 4ae3b31824ece850da0ddbc004e97b21e3cb9aad -2026-06-20-drop-acp-terminal-meta.zh.md: 62341ccae728d981400fdc22b8ec7380bbb3faf2 +2026-06-20-drop-acp-terminal-meta.zh.md: f5b3e0a4e1f37445a0b0dcbf7426806a1de89763 diff --git a/docs/rfc/rejected/simplification/2026-06-20-drop-acp-terminal-meta.zh.md b/docs/rfc/rejected/simplification/2026-06-20-drop-acp-terminal-meta.zh.md index 62341ccae7..f5b3e0a4e1 100644 --- a/docs/rfc/rejected/simplification/2026-06-20-drop-acp-terminal-meta.zh.md +++ b/docs/rfc/rejected/simplification/2026-06-20-drop-acp-terminal-meta.zh.md @@ -1,31 +1,31 @@ # RFC:移除 ACP 终端 `_meta` 渲染 -Status: rejected — Zed is the current target client, and the terminal `_meta` convention is intentional Zed UX with a plain ACP fallback for other clients. - [English](2026-06-20-drop-acp-terminal-meta.md) | 中文 +Status: rejected — Zed 是当前目标客户端,终端 `_meta` 约定是有意为之的 Zed UX 设计,同时为其他客户端提供纯 ACP(Agent Client Protocol)回退路径。 + ## 问题 -ACP(Agent Client Protocol)桥接层通过 `_meta.terminal_info`、`_meta.terminal_output` 和 `_meta.terminal_exit` 实现了一套 Zed 专属的终端卡片约定。已实现的[富 ACP bash 渲染 RFC](../../implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md) 有意避开了 ACP 客户端侧的 `terminal/create`(因为 bash 执行属于 harness 的职责),但仍采用了参考 agent 的纯展示用 `_meta` 约定。这为 Zed 带来了更好的卡片效果,代价是桥接层状态、能力协商、终端 id、特殊的 update 映射、文本回退测试,以及 `dsh-tool-bash` 中的 exit-pill 解析。 +ACP 桥接层通过 `_meta.terminal_info`、`_meta.terminal_output` 和 `_meta.terminal_exit` 实现了一套 Zed 特有的终端卡片约定。已实现的[富 ACP bash 渲染 RFC](../../implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md) 刻意回避了 ACP 客户端侧的 `terminal/create`(因为 bash 执行属于 harness 职责),但仍采用了参考 agent(智能体)的纯展示 `_meta` 约定。这在 Zed 中带来了更好的卡片效果,代价是桥接状态、能力协商、终端 id、特殊的 update 映射、文本回退测试,以及 `dsh-tool-bash` 中的 exit-pill 解析。 -回退路径已经存在:将工具调用和完成输出渲染为普通的 ACP 内容块。非 Zed 客户端本来就依赖这条路径,但 Zed 终端卡片是当前目标客户端的功能,而非投机性的装饰。 +回退路径已经存在:将工具调用和完成输出渲染为普通 ACP 内容块。非 Zed 客户端本来就依赖这条路径,但 Zed 终端卡片是当前目标客户端的功能特性,而非推测性装饰。 ## 提案 -忽略 `clientCapabilities._meta.terminal_output`,通过普通 ACP 内容路径渲染 bash 结果。执行仍留在 agent 侧,通过 `dsh-bash` 完成;只移除与展示相关的终端元数据。如果 ACP 日后标准化了 agent 执行的终端,或产品决定 Zed 专属展示值得维护成本,终端卡片可以回归。 +忽略 `clientCapabilities._meta.terminal_output`,通过纯 ACP 内容路径渲染 bash 结果。执行仍由 agent 侧的 `dsh-bash` 完成;仅移除展示相关的终端元数据。如果 ACP 日后标准化了 agent 执行的终端,或产品决定 Zed 特有展示值得其维护成本,终端卡片可以再回来。 -本提案比[收拢工具自有 UI 展示](2026-06-20-generic-tool-rendering.md)更窄:如果通用的 `presentCall`/`presentResult` 保留,本提案不动它们,只移除终端子形态和 `_meta` 映射。 +本提案比[收拢工具自有 UI 展示](2026-06-20-generic-tool-rendering.md)更窄:如果通用的 `presentCall`/`presentResult` 保留,本提案不影响它们,只移除终端子形态与 `_meta` 映射。 ## 验收标准 - ACP 不再读取或存储 `_meta.terminal_output` 能力状态。 -- `TerminalRendering`、终端 id、终端 cwd 解析以及 `_meta.terminal_*` update 映射从 `@deepseek-ai/dsh-acp` 中消失。 -- `ToolTerminal` 从 `@deepseek-ai/dsh-tools` 中消失,或在展示清理中因无使用而删除。 +- `TerminalRendering`、终端 id、终端 cwd 解析与 `_meta.terminal_*` update 映射从 `@deepseek-ai/dsh-acp` 中消失。 +- `ToolTerminal` 从 `@deepseek-ai/dsh-tools` 中消失,或在展示清理中因未使用而删除。 - Bash 结果展示不再为终端 pill 解析退出状态。 -- 已实现的[富 ACP bash 渲染 RFC](../../implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md) 保留在 `implemented/` 中作为已交付的历史记录,如被本提案取代则互相交叉引用。 +- 已实现的[富 ACP bash 渲染 RFC](../../implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md) 作为已交付历史保留在 `implemented/` 中;如被本提案取代,则加上交叉链接。 ## 放弃的内容 -Zed 用户将失去专属终端卡片:没有 cwd 头部、终端展示或 exit pill。他们仍能以普通内容形式看到命令和输出。在 ACP 桥接层尚未发布、`_meta` 键仍是约定而非标准的阶段,这是合理的简化。 +Zed 用户将失去专用终端卡片:没有 cwd 头部、终端展示或 exit pill。他们仍能以纯内容形式看到命令和输出。在 ACP 桥接层尚未发布、`_meta` 键只是约定而非标准的阶段,这是合理的简化。 <!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> diff --git a/docs/rfc/rejected/simplification/2026-06-20-drop-bash-output-spill-files.i18n.yaml b/docs/rfc/rejected/simplification/2026-06-20-drop-bash-output-spill-files.i18n.yaml index f0ed7ad2b5..de324b8de2 100644 --- a/docs/rfc/rejected/simplification/2026-06-20-drop-bash-output-spill-files.i18n.yaml +++ b/docs/rfc/rejected/simplification/2026-06-20-drop-bash-output-spill-files.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-20-drop-bash-output-spill-files.md: bbc26c645cefb1659012ca0debda78fc6850d276 -2026-06-20-drop-bash-output-spill-files.zh.md: 1439616bf55ad388e69ba693cbc7c130f4e2fc8c +2026-06-20-drop-bash-output-spill-files.zh.md: 43b6029a68542d03027b61424a2a3024ace200d5 diff --git a/docs/rfc/rejected/simplification/2026-06-20-drop-bash-output-spill-files.zh.md b/docs/rfc/rejected/simplification/2026-06-20-drop-bash-output-spill-files.zh.md index 1439616bf5..43b6029a68 100644 --- a/docs/rfc/rejected/simplification/2026-06-20-drop-bash-output-spill-files.zh.md +++ b/docs/rfc/rejected/simplification/2026-06-20-drop-bash-output-spill-files.zh.md @@ -1,20 +1,20 @@ # RFC:移除 bash 完整输出溢出文件 -Status: rejected — full-output recovery is a real bash behavior. A future artifact/blob service may generalize it, but dropping spill files before that replacement would lose useful command output. - [English](2026-06-20-drop-bash-output-spill-files.md) | 中文 +Status: rejected — full-output recovery is a real bash behavior. A future artifact/blob service may generalize it, but dropping spill files before that replacement would lose useful command output. + ## 问题 -`dsh-bash-local` 在内存中保留有界的输出,并将大体积的 stdout/stderr 流溢出到私有临时文件。这要求维护一个私有目录、创建仅所有者可读的随机文件、处理关闭失败、按字节偏移增量读取、报告有损读取、在面向模型的文本中渲染路径,以及清理纪律。当输出被截断时,工具会告诉模型去读取一个本地溢出路径。 +`dsh-bash-local` 在内存中保留有界的输出,并将大体量的 stdout/stderr 流溢出到私有临时文件。这要求一个私有目录、仅所有者可写的随机文件创建、关闭失败处理、基于字节偏移的增量读取、有损读取报告、在面向模型的文本中渲染路径,以及清理纪律。当输出被截断时,该工具会告知模型去读取一个本地溢出路径。 -这解决了一个真实问题,但方式狭窄且有泄漏。溢出路径是一个暴露在模型输出中的进程本地文件系统产物,而非具备作用域访问、保留策略或 UI 能力的持久化 harness 产物。它还使后台任务的读取变得复杂,因为有损增量读取必须指向一到两个溢出文件。 +这解决了一个真实问题,但方式狭隘且有泄漏。溢出路径是一个暴露在模型输出中的进程级文件系统产物,而非具有作用域访问控制、保留策略或 UI 支持的持久化 harness 产物。它还使后台任务的读取变得复杂,因为有损增量读取必须指向一个或两个溢出文件。 ## 提案 -保留尾部截断,移除完整输出溢出文件。bash 结果包含有界的尾部内容加一个明确的截断标记;不输出路径。如果用户需要恢复完整输出,则添加一个通用的产物/blob 服务(具备显式的所有权、清理和 UI 渲染),再让 bash 将大体积输出附加到该服务。 +保留尾部截断,移除完整输出溢出文件。bash 结果包含有界的尾部内容加一个明确的截断标记;不输出路径。如果用户需要恢复完整输出,则添加一个通用的产物/blob 服务(具有明确的所有权、清理和 UI 渲染),然后让 bash 将大体量输出附加到该服务。 -本提案可以独立于[通用长时运行工具运行时](../../proposed/architecture/2026-06-20-generic-long-running-tool-runtime.md)落地。如果后台任务保留,`bash_output` 仍应报告输出已被丢弃,但不再公布溢出路径。 +本提案可以独立于[通用长时间运行工具运行时](../../proposed/architecture/2026-06-20-generic-long-running-tool-runtime.md)落地。如果后台任务保留,`bash_output` 仍应报告输出已被丢弃,但不再提供溢出路径。 ## 验收标准 @@ -24,8 +24,8 @@ Status: rejected — full-output recovery is a real bash behavior. A future arti - 测试覆盖尾部截断,不再断言完整输出文件的内容。 - [docs/defensive-patterns.md](../../../defensive-patterns.md) 中的安全指导不再将私有溢出文件视为面向模型的接口。 -## 放弃了什么 +## 放弃的能力 -模型或用户无法再从临时文件恢复大体积命令输出中被省略的前缀。在真正的产物服务出现之前,这是可接受的。当前的溢出路径为一个生命周期和权限都未经设计的功能引入了过多的定制机制。 +模型或用户无法再从临时文件恢复大体量命令输出中被省略的前缀。在真正的产物服务出现之前,这是可以接受的。当前的溢出路径为一个生命周期和权限均未经设计的功能引入了过多的定制机制。 <!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> diff --git a/docs/rfc/rejected/simplification/2026-06-20-drop-durable-step-boundaries.i18n.yaml b/docs/rfc/rejected/simplification/2026-06-20-drop-durable-step-boundaries.i18n.yaml index 51d6027a24..7396bcf5aa 100644 --- a/docs/rfc/rejected/simplification/2026-06-20-drop-durable-step-boundaries.i18n.yaml +++ b/docs/rfc/rejected/simplification/2026-06-20-drop-durable-step-boundaries.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-20-drop-durable-step-boundaries.md: fba8ad4211db69d3a04d253fd9544caca0f538c6 -2026-06-20-drop-durable-step-boundaries.zh.md: 9ffa12cfba697f1e3bc9059529d0fdf97f595705 +2026-06-20-drop-durable-step-boundaries.zh.md: e389c5506b03a472c74853f3b73c21681f96287f diff --git a/docs/rfc/rejected/simplification/2026-06-20-drop-durable-step-boundaries.zh.md b/docs/rfc/rejected/simplification/2026-06-20-drop-durable-step-boundaries.zh.md index 9ffa12cfba..e389c5506b 100644 --- a/docs/rfc/rejected/simplification/2026-06-20-drop-durable-step-boundaries.zh.md +++ b/docs/rfc/rejected/simplification/2026-06-20-drop-durable-step-boundaries.zh.md @@ -1,32 +1,32 @@ # RFC:移除持久化的步骤边界事件 +Status: rejected — `step/end` is the durable indication that a model step finished, and keeping the symmetric `step/start` / `step/end` pair makes crash repair, invariants, and transcript inspection clearer than inferring completion from adjacent step-scoped events. + [English](2026-06-20-drop-durable-step-boundaries.md) | 中文 -Status: rejected — `step/end` 是模型步骤已完成的持久化标识,保留对称的 `step/start` / `step/end` 对,在崩溃恢复、不变式检查和 transcript(文本记录)审查方面,都比从相邻的步骤级事件推断完成状态更清晰。 - ## 问题 -会话日志存储了 `step/start` 和 `step/end` 事件,尽管每个步骤级事件本身已携带 `{ turn, step }`:assistant 分片、assistant 消息、工具调用、工具结果、token 用量和错误。`deriveMessages()` 忽略步骤边界,ACP(Agent Client Protocol)在 UI 层也忽略它们,主要消费方是不变式检查、测试、快照 golden 文件和崩溃恢复。 +会话日志存储了 `step/start` 和 `step/end` 事件,尽管每个步骤作用域的事件本身已经携带 `{ turn, step }`:assistant 分片、assistant 消息、工具调用、工具结果、用量和错误。`deriveMessages()` 忽略步骤边界,ACP(Agent Client Protocol)在 UI 层面也忽略它们,主要消费方是不变式检查、测试、快照 golden 文件和崩溃恢复。 -被否决的论点是:边界事件让日志更像仪式而非信息。实际上,`step/end` 是具体信息:读者无需从下一个事件推导,就能判断一次模型请求是已完成、已崩溃还是正在被修复。同样,一条孤立的 `step/start` 对于「模型请求已发起但在产出任何分片之前就失败了」的场景也有用。 +被否决的论点是:边界事件使日志更像仪式而非信息。实际上,`step/end` 是具体信息:读者无需从下一个事件推导状态,就能判断一次模型请求是已完成、已崩溃还是正在修复。同样,一个孤立的 `step/start` 对于「模型请求已发起但在产生任何分片之前就失败了」的场景也有价值。 ## 提案 -以轮次作为唯一的持久化边界。从 `SessionEventMap` 中移除 `step/start` 和 `step/end`;保留步骤级事件上用于分组的数值 `step` 字段。agent loop(智能体循环)递增步骤计数器,并以该编号记录步骤级事件,但不再追加开/关边界事件。消费方通过共享 `(turn, step)` 的连续事件推断步骤分组。 +将轮次作为唯一的持久化边界。从 `SessionEventMap` 中移除 `step/start` 和 `step/end`;在需要分组的事件上保留数值型 `step` 字段。agent loop(智能体循环)递增步骤计数器并以该编号记录步骤作用域的事件,但不再追加开/关边界事件。消费方通过共享 `(turn, step)` 的连续事件推断步骤分组。 -不变式插件应强制步骤级事件在一个已打开的轮次内具有有效的正整数步骤编号,而非要求它们被独立的边界记录包围。崩溃恢复不应合成 `step/end`;如果一个被中断的轮次被保留,恢复路径仍可关闭该轮次而无需捏造步骤边界记录。 +不变式插件应当强制步骤作用域的事件在一个已打开的轮次内具有有效的正整数步骤编号,而非要求独立的边界记录包围它们。崩溃恢复不应合成 `step/end`;如果一个被中断的轮次被保留,修复路径仍然可以关闭该轮次而无需捏造步骤边界记录。 ## 验收标准 - `SessionEventMap` 不再包含 `step/start` 或 `step/end`。 -- agent loop 不再有 `closeStep()` 终结路径。 +- agent loop 中不再有 `closeStep()` 终结路径。 - ACP 快照和持久化契约 fixture(测试前置数据)不再期望步骤边界行。 -- `deriveMessages()` 和回放从步骤级事件推导出相同的消息历史。 -- [事件分类体系文档](../../../architecture.md)将轮次描述为持久化边界,将步骤描述为步骤级记录上的一个字段。 +- `deriveMessages()` 和回放从步骤作用域的事件推导出相同的消息历史。 +- [事件分类体系文档](../../../architecture.md)将轮次描述为持久化边界,将步骤描述为步骤作用域记录上的一个字段。 - 会话格式版本和已记录的 fixture 被刷新;按预发布格式策略,非当前版本的已存储日志被拒绝。 ## 放弃了什么 -日志不再将「一次模型请求已发起但进程在产出任何事件之前就终止了」记录为持久化事实,也不再有显式的「此步骤已完成」标记。在会话日志仍是持久化回放与审计表面的当下,这一损失不可接受。 +日志不再将「一次模型请求已发起但进程死亡前未产生任何事件」记录为持久化事实,也不再有显式的「此步骤已完成」标记。在会话日志仍是持久化回放与审计表面的当下,这一损失不可接受。 <!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> diff --git a/docs/rfc/rejected/simplification/2026-06-20-drop-unused-session-lineage.i18n.yaml b/docs/rfc/rejected/simplification/2026-06-20-drop-unused-session-lineage.i18n.yaml index aeb154fbb1..f2aab138ee 100644 --- a/docs/rfc/rejected/simplification/2026-06-20-drop-unused-session-lineage.i18n.yaml +++ b/docs/rfc/rejected/simplification/2026-06-20-drop-unused-session-lineage.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-20-drop-unused-session-lineage.md: 4200532726a27e257e09f927240846f20a0b30ad -2026-06-20-drop-unused-session-lineage.zh.md: c20f964317a1924c3cbc36b7f7838f3a207a65c1 +2026-06-20-drop-unused-session-lineage.zh.md: 1524987111f12a9c6e2014723bb1cb4c87bcf940 diff --git a/docs/rfc/rejected/simplification/2026-06-20-drop-unused-session-lineage.zh.md b/docs/rfc/rejected/simplification/2026-06-20-drop-unused-session-lineage.zh.md index c20f964317..1524987111 100644 --- a/docs/rfc/rejected/simplification/2026-06-20-drop-unused-session-lineage.zh.md +++ b/docs/rfc/rejected/simplification/2026-06-20-drop-unused-session-lineage.zh.md @@ -1,18 +1,18 @@ # RFC:移除未使用的会话血缘元数据 -Status: rejected — `parentSession` is part of the documented fork/sub-agent seam and is already preserved by the agent/session resume path. The field is future-facing, but it is not accidental dead state. - [English](2026-06-20-drop-unused-session-lineage.md) | 中文 +Status: rejected — `parentSession` 是已文档化的 fork/subagent seam 的一部分,且已被 agent(智能体)/session 恢复路径保留。该字段面向未来,但并非意外的死状态。 + ## 问题 -`SessionHeader.parentSession` 记录新会话从哪个会话 fork 而来。它在 `dsh-session` 中定义,被持久化后端保存,在 resume 路径中被复制,作为血缘元数据被文档记录,并有往返测试覆盖。然而仓库中没有任何已上线的 fork UI 或 subagent 流程读取它。计划中的 subagent/fork seam 仍是一个 TODO,因此该字段目前只是被存储的未来形状。 +`SessionHeader.parentSession` 记录新会话从哪个会话 fork 而来。它在 `dsh-session` 中定义,被持久化后端保留,在恢复流程中复制,作为血缘元数据被文档记录,并有往返测试覆盖。然而仓库中没有任何生产环境的 fork UI 或 subagent 流程读取它。计划中的 subagent/fork seam 仍是 TODO,因此该字段目前只是预存的未来形状。 -单文件的代价虽小,但在整个格式中分布广泛:每个后端 schema 和元数据序列化器都在保存一个尚无已完成功能读取的值。由于 header 是一份磁盘契约,即便是占位字段也会成为未来重构必须维护、迁移或有意打破的东西。 +单个文件的成本虽小,但在格式层面影响面广:每个后端 schema 和元数据序列化器都在保留一个尚无已完成功能读取的值。由于 header 是磁盘契约,即使是占位字段也会成为未来重构必须维护、迁移或有意打破的东西。 ## 提案 -从 `SessionHeader` 中移除 `parentSession`,直到真正的 fork/resume 功能需要血缘信息时再引入。如果存在相应 API,fork 仍然可以用先前事件来初始化新会话,但持久化的父指针应当与读取它的功能和解释它的 UX 一同引入。 +从 `SessionHeader` 中移除 `parentSession`,直到真正的 fork/恢复功能需要血缘信息时再引入。如果存在相应 API,fork 仍然可以用先前事件来初始化新会话,但持久化的父指针应当与读取它的功能和解释它的 UX 一同引入。 如果血缘信息回归,届时再决定它应放在不可变 header 中、会话图索引中,还是作为一等事件。当前字段不应预先锁定那个设计。 @@ -20,12 +20,12 @@ Status: rejected — `parentSession` is part of the documented fork/sub-agent se - `SessionHeader` 仅包含 version、id、createdAt 和可选的 cwd。 - JSONL 与 SQLite 元数据 schema 不再存储 parent-session id。 -- resume 和 list API 不再往返传递 `parentSession`。 +- 恢复与列表 API 不再往返传递 `parentSession`。 - 文档和测试移除没有生产消费方支撑的 fork 血缘声明。 -- 会话格式版本、后端 schema 版本和录制的 fixture(测试前置数据)按需刷新;按预发布格式策略,非当前版本的已存储数据将被拒绝,不提供迁移路径。 +- 会话格式版本、后端 schema 版本与记录的 fixture(测试前置数据)按需刷新;按预发布格式策略,非当前版本的存储数据将被拒绝,不提供迁移路径。 ## 放弃了什么 -代码库失去一个为未来 fork/subagent UX 准备好的血缘钩子。这是有意为之。该字段在功能存在时很容易重新引入,而未发布的立场允许格式变更无需迁移。 +代码库失去了一个为未来 fork/subagent UX 预备的现成血缘钩子。这是有意为之。该字段在功能存在时很容易重新引入,而未发布的立场允许格式变更无需迁移。 <!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> diff --git a/docs/rfc/rejected/simplification/2026-06-20-fold-session-persistence-interface.i18n.yaml b/docs/rfc/rejected/simplification/2026-06-20-fold-session-persistence-interface.i18n.yaml index bb1af91b9b..8442da7f7e 100644 --- a/docs/rfc/rejected/simplification/2026-06-20-fold-session-persistence-interface.i18n.yaml +++ b/docs/rfc/rejected/simplification/2026-06-20-fold-session-persistence-interface.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-20-fold-session-persistence-interface.md: 695cd679c67f3e0a9f901c671c33e9507ecbe279 -2026-06-20-fold-session-persistence-interface.zh.md: 1ef93fb64cc27eeeb465136dc7dac6e751a7ccfc +2026-06-20-fold-session-persistence-interface.zh.md: 38f79f833fb5d95e4d9f392de627ee16b17cb997 diff --git a/docs/rfc/rejected/simplification/2026-06-20-fold-session-persistence-interface.zh.md b/docs/rfc/rejected/simplification/2026-06-20-fold-session-persistence-interface.zh.md index 1ef93fb64c..38f79f833f 100644 --- a/docs/rfc/rejected/simplification/2026-06-20-fold-session-persistence-interface.zh.md +++ b/docs/rfc/rejected/simplification/2026-06-20-fold-session-persistence-interface.zh.md @@ -1,4 +1,4 @@ -# RFC:将持久化接口合入 dsh-session +# RFC:将持久化接口合并进 dsh-session Status: rejected — the separate persistence interface package is the intended modular capability seam for durable backends. Folding it into `dsh-session` would reduce package count at the cost of a cleaner backend boundary. @@ -6,26 +6,26 @@ Status: rejected — the separate persistence interface package is the intended ## 问题 -`dsh-session-persistence` 是一个接口包(package),其核心概念已由 `dsh-session` 拥有:`SessionHeader`、`SessionEvent`、`SessionId`、`session/event` 和 `session/flush`。该包额外引入了抽象的 `SessionPersistence` 服务、共享写协调器和契约辅助工具。后端包依赖它,`agent-loop` 则需要可选地发现一个兄弟服务来实现恢复。 +`dsh-session-persistence` 是一个接口包(package),其核心概念已经由 `dsh-session` 拥有:`SessionHeader`、`SessionEvent`、`SessionId`、`session/event` 与 `session/flush`。该包额外添加了抽象的 `SessionPersistence` 服务、共享写入协调器和契约辅助工具。后端包依赖它,`agent-loop`(智能体循环)也需要可选地查找一个同级服务来实现恢复。 -当持久化还是一个全新的可替换后端设计时,能力 seam 的拆分是合理的。但在可变摘要被移除之后,这个接口包基本上只是包装了会话日志自身的存储关注点。保持独立可能带来的仪式感多于清晰度。 +当持久化还是一个全新的可替换后端设计时,能力 seam 的拆分是合理的。但在可变摘要被移除之后,这个接口包基本上只是包装了会话日志自身的存储关切。继续保持独立可能带来的仪式感多于清晰度。 ## 提案 将抽象的 `SessionPersistence` 服务、协调器和持久化契约辅助工具移入 `dsh-session`。JSONL 和 SQLite 仍作为独立的后端包,注册由 session 包拥有的服务。这样既保留了后端可替换性,又删除了一个支撑包和一条跨包 seam。 -实施 PR(Pull Request)应更新[能力 seam](../../implemented/architecture/2026-06-13-capability-seams.md) 指南,补充此例外:持久化不同于 bash 或 LLM(大语言模型),因为它的词汇和生命周期事件本身就是 session 包的核心领域。 +实施 PR(Pull Request)应更新[能力 seam](../../implemented/architecture/2026-06-13-capability-seams.md) 指南,补充此例外:持久化不同于 bash 或 LLM(大语言模型),因为它的词汇和生命周期事件本就属于 session 包的核心领域。 ## 验收标准 - `@deepseek-ai/dsh-session-persistence` 作为包被移除。 - `dsh-session` 导出持久化服务类型、协调器和契约辅助工具。 - JSONL 和 SQLite 后端包直接依赖 `dsh-session`。 -- `agent-loop` 的恢复功能使用由 session 包拥有的服务键。 -- [会话持久化](../../implemented/architecture/2026-06-14-session-persistence.md)、[共享持久化写协调器](../../implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md)与[包文档](../../../../packages/session-persistence/session-persistence/README.md)说明后端实现为何仍保持独立。 +- `agent-loop` 的恢复功能使用 session 包拥有的服务键。 +- [会话持久化](../../implemented/architecture/2026-06-14-session-persistence.md)、[共享持久化写入协调器](../../implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md)与[包文档](../../../../packages/session-persistence/session-persistence/README.md)说明后端实现为何仍保持独立。 ## 放弃了什么 -`dsh-session` 变得更重:它同时拥有内存日志和持久化接口。这就是取舍。如果第三方持久化后端已经形成公开生态,独立的接口包会是更清晰的 SDK 边界;但在预发布阶段,多出的包看起来像是在有外部消费方之前的过度抽象。 +`dsh-session` 变得更重:它同时拥有内存日志和持久化接口。这就是代价。如果第三方持久化后端已经形成公开生态,独立的接口包会是更清晰的 SDK 边界;但在预发布阶段,在尚无外部消费方时,多出的包看起来更像是过早的抽象。 <!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> diff --git a/docs/rfc/rejected/simplification/2026-06-20-generic-tool-rendering.i18n.yaml b/docs/rfc/rejected/simplification/2026-06-20-generic-tool-rendering.i18n.yaml index db3ac5d979..3af32eacdd 100644 --- a/docs/rfc/rejected/simplification/2026-06-20-generic-tool-rendering.i18n.yaml +++ b/docs/rfc/rejected/simplification/2026-06-20-generic-tool-rendering.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-20-generic-tool-rendering.md: 07102ed3b12d7f587b1d12f0e240c1202a2e1cdd -2026-06-20-generic-tool-rendering.zh.md: 86ea52545a79ff975fc6b9c5d080e111a2750290 +2026-06-20-generic-tool-rendering.zh.md: d2c8c745f0ac04a01eb72b50fd1b63cb655afe36 diff --git a/docs/rfc/rejected/simplification/2026-06-20-generic-tool-rendering.zh.md b/docs/rfc/rejected/simplification/2026-06-20-generic-tool-rendering.zh.md index 86ea52545a..d2c8c745f0 100644 --- a/docs/rfc/rejected/simplification/2026-06-20-generic-tool-rendering.zh.md +++ b/docs/rfc/rejected/simplification/2026-06-20-generic-tool-rendering.zh.md @@ -6,30 +6,30 @@ Status: rejected — tool-owned presentation should wait for more real tools bef ## 问题 -工具可以定义 `presentCall()` 和 `presentResult()` 回调,返回 `ToolCallPresentation`、`ToolResultPresentation` 以及可选的 `ToolTerminal` 字段。代码本身已标记出设计的混乱:title、kind、raw input、content、terminal cwd、terminal output、exit code 和 signal 逐步堆积成一堆可选字段。ACP(Agent Client Protocol)随后维护 pending call 状态以将 result 与原始 args 配对,在 `session/load` 时创建仅用于回放的 presenter,并将 terminal 子字段映射为 Zed 特有的 `_meta`。`dsh-tool-bash` 甚至从已渲染的文本中反向解析退出状态,因为纯回放安全的 presenter 已经拿不到结构化的 `BashRunResult`。 +工具可以定义 `presentCall()` 和 `presentResult()` 回调,返回 `ToolCallPresentation`、`ToolResultPresentation` 以及可选的 `ToolTerminal` 字段。代码本身就标记了这个设计的混乱:title、kind、raw input、content、terminal cwd、terminal output、exit code 和 signal 逐步增长为一堆可选字段。ACP(Agent Client Protocol)随后维护 pending call 状态以将 result 与原始 args 配对,在 `session/load` 时创建仅用于回放的 presenter,并将 terminal 子字段映射为 Zed 特有的 `_meta`。`dsh-tool-bash` 甚至从渲染后的文本中反向解析退出状态,因为纯回放安全的 presenter 已经拿不到结构化的 `BashRunResult`。 -真正的一方使用场景是 ACP 的 bash 展示。这不足以作为冻结一个跨包 UI 展示 API 的依据。 +真正的第一方用途是为 ACP 提供 bash 展示。这不足以作为冻结一个跨包 UI 展示 API 的依据。 ## 提案 -暂时移除工具自有的 UI 展示回调。规范的工具事件已经携带工具名称、原始参数字符串、结果内容与错误状态。UI 从这些字段渲染一个通用的工具卡片。工具特有的富展示可以在至少有两个真实工具和两个真实消费方来验证词汇后,以 tagged render-intent union 的形式回归。 +暂时移除工具自有的 UI 展示回调。规范的工具事件已经携带工具名、原始参数字符串、结果内容和错误状态。UI 从这些字段渲染一个通用的工具卡片。工具特有的富展示可以在至少有两个真实工具和两个真实消费方来验证词汇之后,以带标签的 render-intent union 形式回归。 ## 曾考虑的替代方案 -一个更小的替代方案是在单个 PR(Pull Request)中将当前的可选字段包替换为一个显式 union;但如果目标是简化,更彻底的做法是删除回调、保留通用路径。 +作为更小的替代方案,可以在一个 PR(Pull Request)中将当前的可选字段集合替换为一个显式 union;但如果目标是简化,更彻底的做法是删除回调、保留通用路径。 ## 验收标准 - `ToolDefinition` 移除 `presentCall` 和 `presentResult`。 - `ToolCallPresentation`、`ToolResultPresentation`、`ToolTerminal` 和 `ToolCallKind` 消失,除非一个最小的通用 UI 类型仍需要其中之一。 -- ACP 不再维护 presenter pending 状态,也不在实时流式输出/加载回放期间调用工具回调。 -- `dsh-tool-bash` 不再解析已渲染文本来恢复退出状态以生成 UI pill。 -- 快照 golden 展示通用工具卡片和文本结果。 +- ACP 不再维护 presenter pending 状态,也不再在实时流式输出/加载回放期间调用工具回调。 +- `dsh-tool-bash` 不再解析渲染文本来恢复退出状态以供 UI pill 使用。 +- 快照 golden 文件展示通用工具卡片和文本结果。 ## 放弃了什么 -Bash 失去其自定义的终端风格卡片和模型撰写的描述位置。回退方案仍然合理:命令作为工具输入展示,输出作为文本展示。富展示应在产品拥有足够的 UI/工具多样性、足以支撑一份稳定的展示契约时再行设计。 +Bash 失去其自定义的终端风格卡片和模型生成描述的放置位置。回退方案仍然合理:命令作为工具输入展示,输出作为文本展示。富展示应当在产品拥有足够的 UI/工具多样性、足以支撑一份稳定的展示契约时再行设计。 ## 相关 -本 RFC 是[移除 ACP terminal 元数据](2026-06-20-drop-acp-terminal-meta.md)的宽泛版本。如果本 RFC 被接受,那个更窄的 RFC 就不再需要。 +这是[移除 ACP terminal 元数据](2026-06-20-drop-acp-terminal-meta.md)的宽泛版本。如果本 RFC 被接受,那个更窄的 RFC 就不再必要。 diff --git a/docs/rfc/rejected/simplification/2026-06-20-retire-mid-turn-steering.i18n.yaml b/docs/rfc/rejected/simplification/2026-06-20-retire-mid-turn-steering.i18n.yaml index 62eb41dc9a..a944072e9f 100644 --- a/docs/rfc/rejected/simplification/2026-06-20-retire-mid-turn-steering.i18n.yaml +++ b/docs/rfc/rejected/simplification/2026-06-20-retire-mid-turn-steering.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-20-retire-mid-turn-steering.md: bf78125ec175aa9152789bdccd1f8e8a16863a5b -2026-06-20-retire-mid-turn-steering.zh.md: 03af2b24916a5d1a479dc06a30d54003dfa7f156 +2026-06-20-retire-mid-turn-steering.zh.md: a56e112df37bdeb75ca808f76beabe1fec8b1b7b diff --git a/docs/rfc/rejected/simplification/2026-06-20-retire-mid-turn-steering.zh.md b/docs/rfc/rejected/simplification/2026-06-20-retire-mid-turn-steering.zh.md index 03af2b2491..a56e112df3 100644 --- a/docs/rfc/rejected/simplification/2026-06-20-retire-mid-turn-steering.zh.md +++ b/docs/rfc/rejected/simplification/2026-06-20-retire-mid-turn-steering.zh.md @@ -1,37 +1,37 @@ -# RFC:废除中途 steering(中途引导) - -Status: rejected — mid-turn steering is an intentional agent capability for between-step user/plugin input and future goal/loop workflows. It is complexity with a product direction, not an accidental duplicate of `send()`. +# RFC:移除轮次中途引导 [English](2026-06-20-retire-mid-turn-steering.md) | 中文 +Status: rejected — mid-turn steering is an intentional agent capability for between-step user/plugin input and future goal/loop workflows. It is complexity with a product direction, not an accidental duplicate of `send()`. + ## 问题 -agent(智能体)暴露了两条用户消息路径,看起来相近但生命周期语义不同:`send()` 将一条普通用户轮次排入队列,而 `steer()` 在当前运行轮次的步骤之间注入一条消息,空闲时则回退为 `send()`。这一区别渗透到整个栈:`Agent.steer()` 是公开 API,会话日志有持久化的 `steering/message` 事件,agent 事件分类体系有 `agent/steering`,循环在排队消息 FIFO 之外还维护一个 steering FIFO,取消操作需要清空两个队列,`deriveMessages()` 必须将 steering 渲染为带标签的合成用户消息而非普通提示词。 +agent(智能体)暴露了两条用户消息路径,外观相近但生命周期语义不同:`send()` 将一条普通用户轮次排入队列,而 `steer()` 在当前运行轮次的步骤之间注入一条消息,空闲时则回退为 `send()`。这一区分贯穿整个栈:`Agent.steer()` 是公开 API;会话日志有持久化的 `steering/message` 事件;agent 事件分类体系有 `agent/steering`;agent loop(智能体循环)在排队消息 FIFO 之外还维护一个 steering FIFO;取消操作需要清空两个队列;`deriveMessages()` 必须将 steering 渲染为带标签的合成用户消息,而非普通提示词。 -continuation seam 放大了这一成本。`agent/turn-continuation` 默认为 `hadToolCalls || steeringInjected`,因此同一轮次内的 steering 消息即使模型没有请求工具调用也能强制循环再次调用模型。注释中提到了未来的 `/goal`、`/loop` 和预算守卫用途,但当前仓库没有生产级监听器;只有测试注册了该 waterfall(瀑布式事件)。另外,唯一调用 `steer()` 的生产 UI 是 stdio 演示。ACP 在轮次运行期间已经通过普通队列发送提示词。 +续行 seam 进一步放大了成本。`agent/turn-continuation` 默认条件为 `hadToolCalls || steeringInjected`,因此同一轮次内的 steering(中途引导)消息即使模型未请求工具调用,也会强制循环再次调用模型。注释中提到了未来 `/goal`、`/loop` 和预算守卫的用途,但当前仓库没有生产级监听器;只有测试注册了该 waterfall(瀑布式事件)。另外,唯一调用 `steer()` 的生产 UI 是 stdio 演示。ACP(Agent Client Protocol)在轮次运行期间已经通过普通队列发送提示词。 ## 提案 -暂时删除中途用户 steering。`Agent.send()` 成为提交用户内容的唯一公开方式;当 agent 正在运行时,内容等待下一轮次。循环仅因工具调用而在轮次内继续,而非因为用户在某步骤运行期间输入了内容。想要中断当前轮次的调用方使用 `cancel()` 再 `send()`。 +暂时删除轮次中途的用户 steering。`Agent.send()` 成为提交用户内容的唯一公开方式;当 agent 正在运行时,内容等待下一个轮次。循环仅因工具调用而在轮次内继续,不因用户在某个步骤运行期间输入内容而继续。调用方若要中断当前轮次,使用 `cancel()` 后再 `send()`。 -移除 `Agent.steer()`、steering FIFO、`steering/message`、`agent/steering`、由 steering 驱动的 continuation,以及区分排队消息与 steering 消息的取消逻辑。在同一变更中移除 `agent/turn-continuation`,除非实现 PR 发现了生产级监听器;没有 steering 之后,当前仓库不再有具体的 continuation 消费方。如果将来真正的预算或 goal 插件需要强制 continuation,应以该插件为具体消费方重新引入一个更窄的 seam。 +移除 `Agent.steer()`、steering FIFO、`steering/message`、`agent/steering`、由 steering 驱动的续行逻辑,以及取消操作中区分排队消息与 steering 消息的逻辑。除非实现 PR 发现了生产级监听器,否则在同一变更中一并移除 `agent/turn-continuation`;没有 steering 后,当前仓库不再有具体的续行消费方。如果将来真正的预算或目标插件需要强制续行,应以该插件为具体消费方重新引入一个更窄的 seam。 ## 验收标准 - `Agent` 暴露唯一的用户消息入口 `send()`。 - 持久化会话事件词汇不再包含 `steering/message`。 -- `deriveMessages()` 渲染普通用户消息和上下文注入,不再有 steering 标签路径。 -- 循环只有一个排队消息 FIFO,没有同轮次用户消息 continuation 路径。 -- `agent/turn-continuation` 被移除或收窄到一个具名的生产级消费方。 -- stdio UI 和文档将运行期间的输入描述为排入下一轮次的输入。 -- 会话格式版本和录制的 fixture(测试前置数据)已刷新;按预发布格式策略拒绝非当前版本的存储日志。 +- `deriveMessages()` 渲染普通用户消息和上下文注入,不存在 steering 标签路径。 +- 循环只有一个排队消息 FIFO,没有同轮次用户消息续行路径。 +- `agent/turn-continuation` 被移除,或收窄到有具名的生产级消费方。 +- stdio UI 和文档将运行期间的输入描述为「排入下一轮次的输入」。 +- 会话格式版本和已录制的 fixture(测试前置数据)已刷新;非当前版本的存储日志按预发布格式策略被拒绝。 ## 放弃了什么 -用户无法在模型处于工具步骤之间时添加同轮次 steering 内容。这种行为在理论上对「你已经在工作了,顺便也考虑一下 X」有用,但它不是 ACP 今天暴露的行为,而且它使轮次边界变得更难推理。更简单的行为是合理的:用户输入成为下一条提示词,取消仍是替换进行中工作的显式手段。 +用户无法在模型处于工具步骤之间时添加同轮次 steering 内容。这种行为在理论上对「你已经在工作了,也考虑一下 X」的场景有用,但它不是 ACP 当前暴露的行为,且使轮次边界更难推理。更简单的行为是合理的:用户输入成为下一条提示词,取消操作仍是替换进行中工作的显式手段。 ## 相关 -本提案与[删除持久化步骤边界](2026-06-20-drop-durable-step-boundaries.md)天然配对,因为移除同轮次 steering 和 `agent/turn-continuation` 之后,工具调用成为一个轮次包含多个模型步骤的唯一原因。 +本提案与[移除持久化步骤边界](2026-06-20-drop-durable-step-boundaries.md)天然配对,因为移除同轮次 steering 和 `agent/turn-continuation` 后,工具调用成为一个轮次包含多个模型步骤的唯一原因。 <!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> diff --git a/docs/rfc/rejected/simplification/2026-06-20-single-session-acp-bridge.i18n.yaml b/docs/rfc/rejected/simplification/2026-06-20-single-session-acp-bridge.i18n.yaml index aa8c2bbf58..57044817c0 100644 --- a/docs/rfc/rejected/simplification/2026-06-20-single-session-acp-bridge.i18n.yaml +++ b/docs/rfc/rejected/simplification/2026-06-20-single-session-acp-bridge.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-20-single-session-acp-bridge.md: b7b52ca4df2d118358303745f4484e3e40a242b3 -2026-06-20-single-session-acp-bridge.zh.md: 2f00067fffcbd7a74f403c6247b290ac09a846b8 +2026-06-20-single-session-acp-bridge.zh.md: bd287f79475433e4d5e502ef30abd5d14410e633 diff --git a/docs/rfc/rejected/simplification/2026-06-20-single-session-acp-bridge.zh.md b/docs/rfc/rejected/simplification/2026-06-20-single-session-acp-bridge.zh.md index 2f00067fff..bd287f7947 100644 --- a/docs/rfc/rejected/simplification/2026-06-20-single-session-acp-bridge.zh.md +++ b/docs/rfc/rejected/simplification/2026-06-20-single-session-acp-bridge.zh.md @@ -1,4 +1,4 @@ -# RFC:将 ACP 桥接恢复为每连接单会话 +# RFC:将 ACP 桥接恢复为每连接一个活跃会话 [English](2026-06-20-single-session-acp-bridge.md) | 中文 @@ -6,21 +6,21 @@ Status: rejected — Zed 是当前目标 ACP 客户端,其 ACP 实现明确支 ## 问题 -ACP 桥接现已支持在一条 JSON-RPC 连接上承载多个活跃会话。这一能力带来了多条目会话映射、反向的会话/agent 查找、逐会话的提示词状态、加载中 id、每个事件的解复用、跨会话的销毁,以及未来权限提示与后台任务的隔离问题。早先的[多会话 ACP 提案](../../implemented/feature/2026-06-14-acp-multi-session.md)仍在跟踪未完成的权限归属部分;本 RFC 是与之竞争的简化路径。 +ACP(Agent Client Protocol)桥接现在支持在一条 JSON-RPC 连接上承载多个活跃会话。这一能力带来了多条目会话映射、反向会话/agent(智能体)查找、逐会话的 prompt 状态、加载中 id、每条事件的解复用、跨会话拆除,以及未来权限提示与后台任务的隔离问题。较早的[多会话 ACP 提案](../../implemented/feature/2026-06-14-acp-multi-session.md)仍在追踪未完成的权限归属部分;本 RFC 是与之竞争的简化路径。 -产品目标已证明它需要在一个 harness 进程上承载并发的编辑器对话:Zed 的 ACP 连接拥有多个会话和加载状态。快照回放层仍然避免并发模型流,因为其回放条目是位置敏感的;这是测试 fixture 的局限,不是移除桥接多路复用的理由。 +产品目标已经证明它需要在一个 harness 进程上承载并发的编辑器对话:Zed 的 ACP 连接拥有多个会话和加载状态。快照回放层仍然避免并发模型流,因为其回放条目是位置相关的;这是测试 fixture(测试前置数据)的局限,而非移除桥接多路复用的理由。 ## 提案 -将 ACP 的作用域收回到每连接一个活跃会话。`session/new` 或 `session/load` 创建唯一的会话记录;在现有会话被 dispose 或连接关闭之前,第二个活跃会话请求将被拒绝。如果编辑器需要多个聊天标签页,可以启动多个 agent 子进程,直到桥接具备具体的多会话 UX 和权限模型。 +将 ACP 的范围收回到每连接一个活跃会话。`session/new` 或 `session/load` 创建唯一的会话记录;在现有会话被 dispose(资源释放)或连接关闭之前,第二个活跃会话请求将被拒绝。如果编辑器需要多个聊天标签页,可以启动多个 agent 子进程,直到桥接具备具体的多会话 UX 和权限模型。 -在单个 `SessionRecord | undefined` 即可满足需求的地方,移除多会话映射和解复用逻辑。桥接仍可保留使销毁行为正确的 agent/会话生命周期 seam;简化仅针对在同一传输层上多路复用多个活跃会话这一点。 +移除多会话映射和解复用逻辑,改用单一的 `SessionRecord | undefined` 即可。桥接仍可保留使 dispose 正确的 agent/会话生命周期 seam;简化仅针对在同一传输层上多路复用多个活跃会话这一点。 ## 验收标准 -- ACP 每连接仅有一条活跃会话记录。 +- ACP 每连接只有一条活跃会话记录。 - 当该记录存在时,`session/new` 和 `session/load` 拒绝请求。 -- 事件处理器不再跨 `Map<sessionId, record>` 解复用。 +- 事件处理器不再在 `Map<sessionId, record>` 上做解复用。 - 多会话测试被移除,或移至继续支持多路复用的提案下。 - 既有的[多会话 ACP 提案](../../implemented/feature/2026-06-14-acp-multi-session.md)更新为链接本 RFC,并继续作为当前方向。 diff --git a/docs/rfc/rejected/simplification/2026-06-20-truncate-interrupted-turns.i18n.yaml b/docs/rfc/rejected/simplification/2026-06-20-truncate-interrupted-turns.i18n.yaml index 32aca9d1e8..19a66353c7 100644 --- a/docs/rfc/rejected/simplification/2026-06-20-truncate-interrupted-turns.i18n.yaml +++ b/docs/rfc/rejected/simplification/2026-06-20-truncate-interrupted-turns.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-06-20-truncate-interrupted-turns.md: e17cb20f0185fe5d47d0a5ca18b7a389951fbadd -2026-06-20-truncate-interrupted-turns.zh.md: 9ded51b29323be452bd67901dea2ba7b309a9903 +2026-06-20-truncate-interrupted-turns.zh.md: 48aeae650f867fb4629db33a448dd6cbaea60ae0 diff --git a/docs/rfc/rejected/simplification/2026-06-20-truncate-interrupted-turns.zh.md b/docs/rfc/rejected/simplification/2026-06-20-truncate-interrupted-turns.zh.md index 9ded51b293..48aeae650f 100644 --- a/docs/rfc/rejected/simplification/2026-06-20-truncate-interrupted-turns.zh.md +++ b/docs/rfc/rejected/simplification/2026-06-20-truncate-interrupted-turns.zh.md @@ -1,4 +1,4 @@ -# RFC:加载时截断被中断的末尾轮次 +# RFC:加载时截断被中断的最终轮次 Status: rejected — a single turn can contain substantial real work, including many steps and large tool output. Preserving interrupted turns is preferable to silently dropping that tail on load. @@ -6,31 +6,31 @@ Status: rejected — a single turn can contain substantial real work, including ## 问题 -当前的持久化契约会保留最后一个已持久写入但从未关闭的轮次。加载时,`interruptedTurnClosers()` 扫描尾部,为未应答的工具调用合成错误的 `tool/result` 事件,在步骤未关闭时追加 `step/end`,追加 `turn/end { kind: 'interrupted' }`,并要求后端持久提交这段修复。协调器、JSONL 后端、SQLite 后端、会话事件词汇、不变式、文档和测试都对这条合成关闭路径建了模。 +当前的持久化契约会保留已持久写入但从未关闭的最终轮次。加载时,`interruptedTurnClosers()` 扫描尾部,为未应答的工具调用合成 error `tool/result` 事件,在 step 处于打开状态时追加 `step/end`,追加 `turn/end { kind: 'interrupted' }`,并要求后端持久提交这次修复。协调器、JSONL 后端、SQLite 后端、会话事件词汇、不变式、文档和测试都对这条合成关闭路径进行了建模。 -这是为了保留上一次崩溃轮次的部分工作而引入的大量机制。它还会生造从未发生过的事件。合成的工具结果有用处(它使提供方历史保持合法),但也意味着恢复后的日志中包含了没有任何工具产出过的、模型可见的文本。当前设计在尚无已发布产品、也没有真实的恢复 UX 来证明部分轮次恢复确有价值的情况下,就优化了最大化的尾部保留。 +这是一套庞大的机制,只为保留上次崩溃轮次中的部分工作。它还会凭空创造从未发生过的事件。合成的工具结果虽然有用(因为它使 provider 历史保持合法),但也意味着恢复后的日志中包含了模型可见、却并非任何工具产出的文本。当前设计在尚无已发布产品、也没有真实恢复 UX 来证明部分轮次恢复确有价值的情况下,就优化了最大化尾部保留。 ## 提案 -加载时只保留到最后一个已完成的轮次。后端仍然容忍并截断撕裂的末尾记录,但如果解析出的持久前缀在一个已打开的 `turn/start` 之后结束,规范的修复方式是丢弃上一个 `turn/end` 之后的所有事件。不合成 `tool/result`,不合成 `step/end`,不追加 `turn/end { interrupted }`,也不需要 `interrupted` 轮次结束原因。 +加载时只保留最后一个已完成的轮次。后端仍然容忍并截断撕裂的最终记录,但如果解析出的持久前缀止于一个打开的 `turn/start` 之后,规范的修复方式是丢弃上一个 `turn/end` 之后的所有事件。不合成 `tool/result`,不合成 `step/end`,不追加 `turn/end { interrupted }`,也不引入 `interrupted` 轮次结束原因。 -这使持久化的轮次边界变得简单:一个已完成的 `turn/end` 就是检查点。最后一个检查点之后的内容都是崩溃尾部。下一次提示词从最后一个已知合法的提供方 transcript 恢复,而非从部分重建的末尾轮次恢复。 +这使持久化的轮次边界变得简单:一个已完成的 `turn/end` 就是检查点。最后一个检查点之后的内容都是崩溃尾部。下一次 prompt 从最后一个已知合法的 provider transcript(文本记录)恢复,而不是从部分重建的最终轮次恢复。 ## 验收标准 - `TurnEndReasonMap` 移除 `interrupted` 变体。 -- `interruptedTurnClosers()` 及其测试消失。 -- 持久化协调器的修复钩子截断后端特定的撕裂/未关闭尾部状态,不追加关闭事件。 -- [会话持久化文档](../../../../packages/session-persistence/session-persistence/README.md)说明加载返回到最后一个已完成轮次,不包含部分末尾轮次。 -- 快照测试与契约测试随其所固定的行为一起更新。 -- 会话格式版本与已记录的 fixture(测试前置数据)一并刷新;按预发布格式策略,非当前版本的存储日志被拒绝,不提供迁移路径。 +- `interruptedTurnClosers()` 及其测试删除。 +- 持久化协调器的修复钩子截断后端特有的撕裂/打开尾部状态,不追加关闭事件。 +- [会话持久化文档](../../../../packages/session-persistence/session-persistence/README.md)说明加载返回最后一个已完成的轮次,不包含部分最终轮次。 +- 快照与契约测试随其所固定的行为一同更新。 +- 会话格式版本与记录的 fixture(测试前置数据)刷新;按预发布格式策略,非当前版本的存储日志被拒绝,不提供迁移路径。 -## 放弃了什么 +## 放弃的内容 -一次崩溃可能丢失末尾轮次中的真实工作:上一个 `turn/end` 之后追加的助手文本、工具调用和工具输出。这是有意为之的简化。产品尚未发布,末尾轮次恢复的语义未经用户验证,而一个干净的「已完成轮次即检查点」模型在解释、测试和实现上都容易得多。未来如果需要「恢复部分崩溃工作」功能,应当设计为一个面向用户的显式恢复视图,而非静默插入规范 transcript 的合成事件。 +崩溃可能丢失最终轮次中的真实工作:上一个 `turn/end` 之后追加的助手文本、工具调用和工具输出。这是有意为之的简化。产品尚未发布,最终轮次恢复的语义未经用户验证,而一个干净的「已完成轮次即检查点」模型在解释、测试和实现上都容易得多。未来若需「恢复部分崩溃工作」功能,应设计为面向用户的显式恢复视图,而非静默插入规范 transcript 的合成事件。 ## 相关 -本 RFC 是对[会话持久化](../../implemented/architecture/2026-06-14-session-persistence.md)和[轮次封闭不变式](../../implemented/architecture/2026-06-15-turn-enclosure-invariant.md)的直接简化。它还移除了持久化步骤边界事件的大部分动机,使 [drop durable step boundary events](2026-06-20-drop-durable-step-boundaries.md) 的变更范围更小。 +本提案是对[会话持久化](../../implemented/architecture/2026-06-14-session-persistence.md)与[轮次封闭不变式](../../implemented/architecture/2026-06-15-turn-enclosure-invariant.md)的直接简化。它还移除了持久化 step 边界事件的大部分动机,使[移除持久化 step 边界事件](2026-06-20-drop-durable-step-boundaries.md)的改动更小。 <!-- rfc-format: alternatives-not-recorded (pre-format RFC) --> diff --git a/docs/rfc/rejected/simplification/2026-07-04-prune-unimplemented-subagent-vocabulary.i18n.yaml b/docs/rfc/rejected/simplification/2026-07-04-prune-unimplemented-subagent-vocabulary.i18n.yaml index c61b774371..915d56d0e8 100644 --- a/docs/rfc/rejected/simplification/2026-07-04-prune-unimplemented-subagent-vocabulary.i18n.yaml +++ b/docs/rfc/rejected/simplification/2026-07-04-prune-unimplemented-subagent-vocabulary.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-04-prune-unimplemented-subagent-vocabulary.md: 3c86f11564d85b423fe59d784c6bf69959fb3907 -2026-07-04-prune-unimplemented-subagent-vocabulary.zh.md: ad8c8541ca682be6e71b6fe4ae166c3b65d14cdf +2026-07-04-prune-unimplemented-subagent-vocabulary.zh.md: 84ff6f15ff2c9b3a13240997ab3c7b5cf2ab7263 diff --git a/docs/rfc/rejected/simplification/2026-07-04-prune-unimplemented-subagent-vocabulary.zh.md b/docs/rfc/rejected/simplification/2026-07-04-prune-unimplemented-subagent-vocabulary.zh.md index ad8c8541ca..84ff6f15ff 100644 --- a/docs/rfc/rejected/simplification/2026-07-04-prune-unimplemented-subagent-vocabulary.zh.md +++ b/docs/rfc/rejected/simplification/2026-07-04-prune-unimplemented-subagent-vocabulary.zh.md @@ -1,39 +1,39 @@ -# RFC:裁剪 subagent seam 中未实现的词汇 - -Status: rejected — the deferred capability vocabulary (`outputSchema`/`structured`, `toolFilter`, `sendMessage`/`resume`) is intentionally reserved surface: the seam advertises the full intended contract ahead of its implementations by design, so providers and consumers grow into a stable shape rather than re-negotiating it per capability. The consumer-evidence analysis below stands as the record of what is currently unimplemented. +# RFC:裁剪未实现的 subagent seam 词汇 [English](2026-07-04-prune-unimplemented-subagent-vocabulary.md) | 中文 +Status: rejected — the deferred capability vocabulary (`outputSchema`/`structured`, `toolFilter`, `sendMessage`/`resume`) is intentionally reserved surface: the seam advertises the full intended contract ahead of its implementations by design, so providers and consumers grow into a stable shape rather than re-negotiating it per capability. The consumer-evidence analysis below stands as the record of what is currently unimplemented. + ## 问题 -[subagent seam](../../implemented/feature/2026-06-21-subagent-capability-seam.md) 交付了一套两层能力设计:由服务在启动时检查的能力 flag,以及 `SubagentRun` 上的可选运行时方法。三项启动时特性和两个可选运行时方法均无实现、无调用方: +[subagent seam](../../implemented/feature/2026-06-21-subagent-capability-seam.md) 交付了一套两层能力设计:启动时由服务检查的能力 flag,以及 `SubagentRun` 上的可选运行时方法。三个启动时特性和两个可选运行时方法的实现数与调用数均为零: -- **`outputSchema`/`structured` 与 `toolFilter`**(`SubagentCapabilities`、`SubagentStartRequest`、`SubagentResult`,位于 `packages/subagent/subagent/src/types.ts`):每个真实提供方都声明 `outputSchema: false, toolFilter: false`(`packages/subagent/subagent-spawn/src/index.ts`、`packages/subagent/subagent-fork/src/index.ts`、`packages/subagent/subagent-acp/src/index.ts`);唯一的生产环境 `ctx.subagents.start` 调用方(`packages/subagent/tool-subagent/src/index.ts`)构建 `{ prompt, parent, signal?, agentOptions? }`,结构上无法设置这两项;`structured` 仅由测试 mock(`packages/support/subagent-mock`)为其自身 spec 产出。服务的能力检查包含两行 assert,唯一的执行者是拒绝测试。 -- **`SubagentRun.sendMessage` / `SubagentRun.resume`**(同一文件):没有任何提供方实现——连 mock 也没有;spawn spec 断言的是它们的*缺席*。 +- **`outputSchema`/`structured` 与 `toolFilter`**(`SubagentCapabilities`、`SubagentStartRequest`、`SubagentResult`,位于 `packages/subagent/subagent/src/types.ts`):每个真实提供方都声明 `outputSchema: false, toolFilter: false`(`packages/subagent/subagent-spawn/src/index.ts`、`packages/subagent/subagent-fork/src/index.ts`、`packages/subagent/subagent-acp/src/index.ts`);唯一的生产环境 `ctx.subagents.start` 调用方(`packages/subagent/tool-subagent/src/index.ts`)构造 `{ prompt, parent, signal?, agentOptions? }`,结构上无法设置这两个字段;`structured` 仅由测试 mock(`packages/support/subagent-mock`)为其自身 spec 产出。服务的能力检查包含两行 assert,其唯一执行者是拒绝测试。 +- **`SubagentRun.sendMessage` / `SubagentRun.resume`**(同一文件):没有任何提供方实现——包括 mock 也没有;spawn spec 断言的正是它们的*缺失*。 -`dsh-subagent` 依赖 `dsh-tools` 的唯一原因是 `outputSchema` 的 `SchemaSpec` 类型。三个后续 subagent 工作流(per-session 快照回放、fork seed 边界、ACP 后端)都围绕这块表面落地,却没有增长出哪怕一个消费方。 +`dsh-subagent` 依赖 `dsh-tools` 的唯一原因是 `outputSchema` 的 `SchemaSpec` 类型。三个后续 subagent 工作流(per-session 快照回放、fork seed 边界、ACP(Agent Client Protocol) 后端)都围绕这块接口面落地,却没有增长出哪怕一个消费方。 ## 提案 -从 seam 中移除 `outputSchema`/`structured`、`toolFilter`、`sendMessage` 和 `resume`;将 `SubagentCapabilities` 缩减为 `{ depthLimit }`;删除两行能力 assert、三个提供方上的 all-false flag、mock 的 structured 分支及其 `capabilities`/`structured` 配置旋钮,以及为固定被移除表面而存在的测试(两行拒绝测试、spawn 缺席测试、mock structured spec)。从 `packages/subagent/subagent/package.json` 中删除 `dsh-tools` 的 peer/dev 依赖。更新 [subagent.md](../../../core-data-structures/subagent.md) 中的粘贴内容与 type-equiv manifest,以及 `packages/subagent/subagent`、`packages/subagent/subagent-spawn`、`packages/subagent/subagent-fork` 和 `packages/support/subagent-mock` 的 README 相关行。实现 PR 按 [implemented/AGENTS.md](../../implemented/AGENTS.md) 修订 seam RFC 的能力目录。 +从 seam 中移除 `outputSchema`/`structured`、`toolFilter`、`sendMessage` 与 `resume`;将 `SubagentCapabilities` 缩减为 `{ depthLimit }`;删除两行能力 assert、三个提供方上的 all-false flag、mock 的 structured 分支及其 `capabilities`/`structured` 配置项,以及为固定被移除接口面而存在的测试(两行拒绝测试、spawn 缺失测试、mock structured spec)。从 `packages/subagent/subagent/package.json` 中删除 `dsh-tools` 的 peer/dev 依赖。更新 [subagent.md](../../../core-data-structures/subagent.md) 中的粘贴内容与 type-equiv manifest(元数据清单),以及 `packages/subagent/subagent`、`packages/subagent/subagent-spawn`、`packages/subagent/subagent-fork` 和 `packages/support/subagent-mock` 的 README 相关行。实现 PR(Pull Request)按照 [implemented/AGENTS.md](../../implemented/AGENTS.md) 修订 seam RFC 的能力目录。 -**保留** `depthLimit`/`maxDepth` 与能力检查。进程内后端强制执行该限制,尽管当前发布的工具尚未设置它。递归是已知的 seam 风险,因此恰当的后续工作是补上工具默认值,而非删除正在工作的强制逻辑。 +**保留** `depthLimit`/`maxDepth` 与能力检查。进程内后端已强制执行该限制,尽管当前发布的 tool 尚未设置它。递归是已知的 seam 风险,因此恰当的后续工作是提供一个 tool 默认值,而非删除正在工作的强制逻辑。 -审视过但有意不动的相邻表面:`SubagentService.getProvider()`/`list()` 只有测试 harness 消费方,但 [prune-dead-seam-methods 实现说明](../../implemented/simplification/2026-06-20-prune-dead-seam-methods.md) 记录了完全相同的形态曾从 bash executor 中移除后又被回退——测试 harness 对于一个在已跟踪 map 上的单行访问器而言就是消费方。`SubagentRunEndInfo.lastAssistantMessage` 是一个已记录的保留项([subagent-observe-enrich RFC](../../implemented/feature/2026-06-30-subagent-observe-enrich.md) 的评审删除了 `agentType` 但有意保留了它,因为它是进程外子 agent 唯一的最终消息通道);它当前未接通的桥接转发是一个待弥合的缺口或待记录的消费方,不是本 RFC 要裁剪的表面。 +审视过但有意不动的相邻接口面:`SubagentService.getProvider()`/`list()` 仅有测试 harness 消费方,但 [prune-dead-seam-methods 实现说明](../../implemented/simplification/2026-06-20-prune-dead-seam-methods.md) 恰好记录了这种形态从 bash executor 中被移除后又被回退的经过——对于一个基于已跟踪 map 的单行访问器而言,测试 harness 就是消费方。`SubagentRunEndInfo.lastAssistantMessage` 是一个已记录的保留项([subagent-observe-enrich RFC](../../implemented/feature/2026-06-30-subagent-observe-enrich.md) 的评审删除了 `agentType` 但有意保留了它,因为它是进程外子 agent(智能体)唯一的最终消息通道);它当前未接通的桥接转发是一个待补的缺口或待记录的消费方,不是本 RFC 要裁剪的接口面。 -这是 [从持久化 seam 裁剪死方法](../../implemented/simplification/2026-06-20-prune-dead-seam-methods.md) 在 seam 词汇层面的回声:每个实现都必须为无人声明的成员——甚至更弱,因为这里连一个实现都不存在。 +这是[从持久化 seam 裁剪死方法](../../implemented/simplification/2026-06-20-prune-dead-seam-methods.md)在 seam 词汇层面的回响:每个实现都必须为无人声明的成员,甚至更弱,因为这里连一个实现都没有。 ## 曾考虑的替代方案 ### 为什么不保留? -两类能力的设计是 seam RFC 的核心亮点,日后重新添加 `outputSchema` 会涉及多个文件。但该设计以 `depthLimit` 作为活跃示例、以 RFC 作为记录仍然成立;而且 seam RFC 本身承认已交付的 `toolFilter` 形态是错的(真正的强制需要在子 agent 的上下文中设置 `tools/pre-execute` deny,而非 schema 过滤)——该 deny 原语已存在于拦截 seam 上,因此面向真实实现提供方重新添加时,将固定一份比当前推测性契约更好的契约。 +两类能力的设计是 seam RFC 的核心亮点,日后重新添加 `outputSchema` 会涉及多个文件。但该设计以 `depthLimit` 作为活跃示例、以 RFC 作为记录仍然成立;而且 seam RFC 本身承认已交付的 `toolFilter` 形态是错误的(真正的强制需要在子 agent 上下文中实施 `tools/pre-execute` deny,而非 schema 过滤)——该 deny 原语已存在于拦截 seam 上,因此基于真实实现提供方重新添加时,将固定出一份比当前推测性契约更好的契约。 ## 验收标准 -- 被移除的拼写仅出现在本 RFC 和修订后的 seam RFC 中;`SubagentCapabilities` 为 `{ depthLimit: boolean }`;`dsh-tools` 依赖边已消除(`hygiene` 绿)。 -- 深度强制测试不变且绿。 +- 被移除的拼写仅出现在本 RFC 和修订后的 seam RFC 中;`SubagentCapabilities` 为 `{ depthLimit: boolean }`;`dsh-tools` 依赖边已消除(`hygiene` 绿色)。 +- 深度强制测试不变且绿色。 ## 风险 -subagent 生命周期事件在结束载荷上携带 `lastAssistantMessage`——该增强位于服务模块中,不在本 RFC 缩减的 seam 词汇范围内;observe-enrich RFC 记录了因缺乏消费方而删除 `agentType` 兄弟字段的判断:本 RFC 延续的正是这一判断。CC hooks 桥接是这些生命周期事件的第一个外部消费方,它只读取事件载荷,不触及本文移除的任何表面;observe-enrich RFC 中延期的控制流重设计将实现 `resume` 列为自身的未来工作——恰好是本 RFC 模式所预期的重新添加触发点。 +subagent 生命周期事件在结束载荷上携带 `lastAssistantMessage`——该增强位于服务模块中,不在本 RFC 缩减的 seam 词汇范围内;observe-enrich RFC 记录了因缺少消费方而删除 `agentType` 兄弟字段的判断,本 RFC 延续了这一判断。CC hooks 桥接是这些生命周期事件的第一个外部消费方,它只读取事件载荷,不涉及本文移除的任何接口面;observe-enrich RFC 推迟的控制流重设计将实现 `resume` 列为自身的未来工作——恰好是本 RFC 模式所预期的重新添加触发点。 diff --git a/docs/rfc/rejected/simplification/2026-07-12-collapse-workflow-to-foreground-core.i18n.yaml b/docs/rfc/rejected/simplification/2026-07-12-collapse-workflow-to-foreground-core.i18n.yaml index 96bb401556..98bc1e4bab 100644 --- a/docs/rfc/rejected/simplification/2026-07-12-collapse-workflow-to-foreground-core.i18n.yaml +++ b/docs/rfc/rejected/simplification/2026-07-12-collapse-workflow-to-foreground-core.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-12-collapse-workflow-to-foreground-core.md: 78b67c10ac39fddaf4ea76ca90d5cbf760fe5866 -2026-07-12-collapse-workflow-to-foreground-core.zh.md: 567da63e86b24dbedfd6fb50da0984c9866bd9cb +2026-07-12-collapse-workflow-to-foreground-core.zh.md: 28b0af0a70110d39572fafac521a555c62ebb3f5 diff --git a/docs/rfc/rejected/simplification/2026-07-12-collapse-workflow-to-foreground-core.zh.md b/docs/rfc/rejected/simplification/2026-07-12-collapse-workflow-to-foreground-core.zh.md index 567da63e86..28b0af0a70 100644 --- a/docs/rfc/rejected/simplification/2026-07-12-collapse-workflow-to-foreground-core.zh.md +++ b/docs/rfc/rejected/simplification/2026-07-12-collapse-workflow-to-foreground-core.zh.md @@ -1,39 +1,39 @@ -# RFC:将工作流收缩至实际使用的前台核心 - -Status: rejected — Workflow progress is an intentional observation surface; make it useful through a consumer instead of deleting it. +# RFC:将工作流收缩至已使用的前台核心 [English](2026-07-12-collapse-workflow-to-foreground-core.md) | 中文 +Status: rejected — Workflow progress is an intentional observation surface; make it useful through a consumer instead of deleting it. + ## 问题 -工作流能力执行前台 JavaScript 来编排 subagent,但它同时携带了一套无人消费的进度观测系统。没有任何生产环境的监听器订阅六个 `workflow/*` 事件中的任何一个;监听器仅存在于工作流测试中。尽管如此,seam 仍定义了 run/phase/agent outcome 载荷,worker 仍发送 phase/log/agent 生命周期协议消息,host 通过一个 `liveAgents` 配对账本转发它们,引擎维护 run id 的唯一目的就是关联这些通知。 +工作流能力执行前台 JavaScript 来编排 subagent,但它同时携带了一套无人消费的进度观测系统。没有任何生产环境的监听器订阅六个 `workflow/*` 事件中的任何一个;监听器仅存在于工作流测试中。尽管如此,seam 定义了 run/phase/agent outcome 载荷,worker 发送 phase/log/agent 生命周期协议消息,host 通过一个 `liveAgents` 配对账本转发它们,引擎维护 run id 仅仅是为了关联这些通知。 -这套进度词汇不仅未被使用,而且在不重新设计的情况下无法服务于它唯一的具名未来消费方。`WorkflowRunInfo` 包含 `{id, meta}` 但没有父 agent、会话或工具调用标识,而面向模型的工具从不暴露 run id。一个全局 ACP 监听器无法将事件路由到正确的客户端会话。`meta.phases` 从未被查询,`phase(title)` 不会对其做校验,phase 的 `detail`/`model` 和 agent 的 `label`/`phase` 只喂给事件,`whenToUse` 被校验和复制但从未被渲染或用于选择。`phase()` 和 `log()` 仍然跨越 worker 边界,尽管没有接收方。 +这套进度词汇不仅仅是未被使用;它在不经重新设计的情况下也无法服务于其唯一已命名的未来消费方。`WorkflowRunInfo` 包含 `{id, meta}` 但没有父 agent(智能体)、会话或工具调用标识,而面向模型的工具也从不暴露 run id。一个全局 ACP(Agent Client Protocol)监听器无法将事件路由到正确的客户端会话。`meta.phases` 从未被查询,`phase(title)` 不对其做校验,phase 的 `detail`/`model` 和 agent 的 `label`/`phase` 仅供事件消费,`whenToUse` 被校验和复制但从未被渲染或用于选择。`phase()` 和 `log()` 仍然跨越 worker 边界,尽管没有接收方。 -live handle 在观察者消失后仍重复事件时代的数据。`WorkflowRun.id` 没有非事件消费方,而工具读取 `run.meta.name` 只是为了渲染一个它已经以 `args.meta.name` 形式持有的值;两者都不属于执行/取消 handle。 +live handle 在观测者消失后仍重复事件时代的数据。`WorkflowRun.id` 没有非事件消费方,而工具读取 `run.meta.name` 只是为了渲染一个它已经以 `args.meta.name` 形式持有的值;两者都不属于执行/取消 handle。 -取消也为一个同步启动提供了两条公开通道。`WorkflowStartRequest.signal` 被传给 worker host,而唯一的生产调用方另外将同一个 signal 桥接到 `WorkflowRun.cancel()`。因为 `start()` 在控制权让出之前就返回了 run,不存在需要请求时取消的就绪窗口;重复的 signal 增加了 host 的 listener/disarm 状态却没有消除任何竞态。 +取消机制也为一个同步启动提供了两条公开通道。`WorkflowStartRequest.signal` 被传递给 worker host,而唯一的生产调用方另外将同一个 signal 桥接到 `WorkflowRun.cancel()`。因为 `start()` 在控制权让出之前就返回了 run,不存在需要请求时取消的就绪窗口;重复的 signal 增加了 host 的 listener/disarm 状态却没有封堵任何竞态。 -`WorkflowError.fatal` 是同类投机分支的微缩版:每个生产环境的构造都是 fatal 的,`fatal: false` 仅存在于测试中,组合子已经通过 `instanceof` 区分工作流失败。 +`WorkflowError.fatal` 是同一种推测性分支的微缩版:所有生产环境的构造都是 fatal 的,`fatal: false` 仅存在于测试中,组合子已经通过 `instanceof` 区分工作流失败。 ## 提案 -保留实际使用的核心:`agent(prompt, { schema, model })`、`parallel`、`pipeline`、`args`、并发/agent 上限、取消、有界 dispose、结构化结果、worker 隔离,以及前台工具收集。移除所有 `workflow/*` 事件及其仅服务于事件的 info/outcome 类型;移除 `phase()`、`log()`、agent 的 `label`/`phase`、phase 声明、`whenToUse` 及其 worker 消息/host 观察者;将工作流元数据收缩为工具实际使用的 name;移除仅服务于事件的 run id/meta 快照以及合成的 agent-end 账本。将 `WorkflowRun` 收缩为 `result`、`cancel()` 和 `dispose()`;工具渲染请求方持有的 name。移除 `WorkflowStartRequest.signal` 及 worker host 的 input-signal listener/disarm 状态,保留调用方从自身 abort signal 到 `run.cancel()` 的桥接。将 `WorkflowError` 变为单一的 fatal 错误类,不再有布尔模式或 `isFatalWorkflowError()` 辅助函数。 +保留已使用的核心:`agent(prompt, { schema, model })`、`parallel`、`pipeline`、`args`、并发/agent 上限、取消、有界 dispose(资源释放)、结构化结果、worker 隔离与前台工具收集。移除所有 `workflow/*` 事件及其仅供事件使用的 info/outcome 类型;移除 `phase()`、`log()`、agent 的 `label`/`phase`、phase 声明、`whenToUse` 及其 worker 消息/host 观测者;将工作流元数据收缩为工具实际使用的 name;移除仅供事件使用的 run id/meta 快照与合成的 agent-end 账本。将 `WorkflowRun` 收缩为 `result`、`cancel()` 和 `dispose()`;工具渲染请求方持有的 name。移除 `WorkflowStartRequest.signal` 及 worker host 的 input-signal listener/disarm 状态,保留调用方从其 abort signal 到 `run.cancel()` 的桥接。将 `WorkflowError` 变为单一的 fatal 错误类,不再有布尔模式或 `isFatalWorkflowError()` 辅助函数。 -修订已实施的动态工作流 RFC,并更新 seam/tool/worker README、工具 schema、生成的 catalog 与包依赖图、worker type-equiv 记录、单元测试,以及工作流快照/header fixture。如果未来委托进度 UI 工作,应从一份命名了父 agent/会话/工具调用的关联契约出发,而非原样复活此协议。 +修订已实施的 dynamic-workflow RFC,并更新 seam/tool/worker README、工具 schema、生成的 catalog 与 package 依赖图、worker type-equiv 记录、单元测试以及工作流快照/header fixture(测试前置数据)。如果进度 UI 工作被立项,应从一份命名了父 agent/会话/工具调用的关联契约出发,而非原样复活这套协议。 ## 曾考虑的替代方案 -**为未来 UI 保留预建的观测词汇。** 当前形状类似 Claude Code 的动态工作流元数据,host 有意地将每个转发的 agent start 与 worker 的 end 或合成的终端 end 配对。移除它意味着放弃形状兼容性,使进度 UI 成为一项全新的设计任务;但现有载荷仍然缺少可路由的归属信息,因此仅靠平衡的生命周期也无法在不重新设计的情况下让具名的 ACP 消费方可行。 +**为未来 UI 保留预建的观测词汇。** 当前形态类似 Claude Code 的 dynamic-workflow 元数据,host 有意地将每个转发的 agent start 与 worker 的 end 或一个合成的终止 end 配对。移除它意味着放弃形态兼容性,使进度 UI 成为一项全新的设计任务;但现有载荷仍缺少可路由的归属信息,因此仅靠平衡的生命周期也无法在不重新设计的情况下让已命名的 ACP 消费方可行。 ## 验收标准 - 工作流公开 seam 仅包含有生产消费方的执行、取消、结果与 dispose 契约。 -- 不再保留任何工作流事件、phase/log 协议消息、run-id 生成器、仅服务于进度的元数据、host 配对账本或 fatal 模式分支。 +- 不再保留任何工作流事件、phase/log 协议消息、run-id 生成器、仅供进度使用的元数据、host 配对账本或 fatal 模式分支。 - run handle 不再有 id/meta 回显,取消在同步 `start()` 返回后只有一条持有者拥有的通道。 -- parallel/pipeline 行为、上限、取消静默、worker 隔离、结构化输出以及面向模型的工作流场景保持覆盖率。 +- parallel/pipeline 行为、上限、取消静默、worker 隔离、结构化输出与面向模型的工作流场景保持测试覆盖。 - 类型检查、覆盖率、快照、doc-sync、module-graph 校验、构建与 hygiene 全部通过。 ## 风险 -这是对工作流 DSL、事件分类体系、handle 与 start request 的编译可见收缩。现有提供描述性元数据的工作流调用,以及使用 `phase`、`log` 或 label 的脚本,必须相应精简;程序化调用方需自行将 abort 源桥接到返回的 handle;未来的观察者必须添加一个关联性更好的 seam。使工作流真正有用的执行语义不变。 +这是对工作流 DSL、事件分类体系、handle 与 start request 的编译可见收缩。现有提供描述性元数据的工作流调用,以及使用 `phase`、`log` 或 label 的脚本,都必须相应精简;程序化调用方需自行将 abort source 桥接到返回的 handle;未来的观测者必须添加一个关联性更好的 seam。使工作流有用的执行语义不变。 diff --git a/docs/rfc/rejected/simplification/2026-07-12-prune-unused-skill-registry-surface.i18n.yaml b/docs/rfc/rejected/simplification/2026-07-12-prune-unused-skill-registry-surface.i18n.yaml index 1fb7c5343a..a9c24674bf 100644 --- a/docs/rfc/rejected/simplification/2026-07-12-prune-unused-skill-registry-surface.i18n.yaml +++ b/docs/rfc/rejected/simplification/2026-07-12-prune-unused-skill-registry-surface.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write 2026-07-12-prune-unused-skill-registry-surface.md: 3e8c009871c3d609612b4a01edc2048ddedfad0d -2026-07-12-prune-unused-skill-registry-surface.zh.md: 1412e3cfac1adbb8b563ad50edb08c53a719feb8 +2026-07-12-prune-unused-skill-registry-surface.zh.md: deaea2ca53d4ed2ac5141013f969f621d3202875 diff --git a/docs/rfc/rejected/simplification/2026-07-12-prune-unused-skill-registry-surface.zh.md b/docs/rfc/rejected/simplification/2026-07-12-prune-unused-skill-registry-surface.zh.md index 1412e3cfac..deaea2ca53 100644 --- a/docs/rfc/rejected/simplification/2026-07-12-prune-unused-skill-registry-surface.zh.md +++ b/docs/rfc/rejected/simplification/2026-07-12-prune-unused-skill-registry-surface.zh.md @@ -1,29 +1,29 @@ -# RFC:裁剪未使用的 skill 注册表接口 - -Status: rejected — Direct runtime skill registration is an intentional extension path for third-party plugins. +# RFC:裁剪 skill 注册表中未使用的接口 [English](2026-07-12-prune-unused-skill-registry-surface.md) | 中文 +Status: rejected — Direct runtime skill registration is an intentional extension path for third-party plugins. + ## 问题 -skill 服务的嵌入式运行时子系统没有任何生产调用方调用 `ctx.skills.register()`。它引入了一个保留的 `runtime` 提供方名称、一套运行时 map/rank/source、重复策略、缓存键中的第二个 revision、规范化逻辑、dispose 函数和测试,而这些都与每个已交付 skill 实际使用的提供方 seam 并行存在。`SkillSummary.whenToUse` 以及 candidate/definition 上的 `path` 被解析和复制,但没有任何生产消费方读取它们:模型目录只渲染 name/description,资源加载使用 `resourceBase`,提供方自行管理其定位符。刻意开放的 `metadata` 扩展点保留不动。 +skill(技能)服务的嵌入式运行时子系统中,`ctx.skills.register()` 没有任何生产调用方。它引入了一个保留的 `runtime` 提供方名称、一套运行时 map/rank/source、重复策略、缓存键中的第二个 revision、规范化逻辑、dispose(资源释放)器以及相应测试——而所有已交付的 skill 都只使用提供方 seam。`SkillSummary.whenToUse` 和 candidate/definition 的 `path` 被解析和复制,但没有任何生产消费方读取它们:模型目录只渲染 name/description,资源加载使用 `resourceBase`,提供方自行管理其定位器。有意开放的 `metadata` 扩展点保留不动。 ## 提案 -移除 `SkillService.register()`、`SkillRegistration`、运行时伪提供方及保留名称规则、运行时 revision/缓存分支,以及仅用于运行时的 source/rank 规范化逻辑。需要嵌入式 skill 的测试改为注册一个小型真实提供方。保留 `providerRevision` 作为进行中的发现纪元,但已完成的目录仅以 cwd 为键:每次提供方变更都同步清除缓存,await 之后的 revision 比较已能阻止插入陈旧结果。从 skill 契约和本地提供方副本中移除 `whenToUse`、`SkillCandidate.path` 和 `SkillDefinition.path`,同时保留提供方的 locator/root 路径;保留 `metadata`、`disableModelInvocation`、`source`、`provider`、`locator` 和 `resourceBase`,它们要么是刻意的扩展词汇,要么是生产中被消费的字段。 +移除 `SkillService.register()`、`SkillRegistration`、运行时伪提供方及保留名称规则、运行时 revision/缓存分支,以及仅用于运行时的 source/rank 规范化逻辑。需要嵌入式 skill 的测试改为注册一个小型真实提供方。保留 `providerRevision` 作为进行中的发现 epoch,但已完成的目录缓存仅以 cwd 为键:每次提供方变更同步清除缓存,await 之后的 revision 比较已能阻止插入陈旧结果。从 skill 契约和 local-provider 副本中移除 `whenToUse`、`SkillCandidate.path` 与 `SkillDefinition.path`,同时保留提供方的 locator/root 路径;保留 `metadata`、`disableModelInvocation`、`source`、`provider`、`locator` 和 `resourceBase`,因为它们要么是有意开放的扩展词汇,要么是生产消费的字段。 -同步修订 skill 系统 RFC、README、JSDoc、目录文件和测试。agent 作用域的系统提示词段落、工具提供方和变量明确不在本提案范围内:[agent 作用域贡献者契约](../../implemented/architecture/2026-07-08-agent-scope-contexts.md)有意允许在 `setup(agentCtx)` 期间通过 agent 拥有的上下文注册这三者,因此仓库内没有固定的作用域注册并不能证明无人消费。 +同步修订 skill 系统 RFC、README、JSDoc、目录文件与测试。agent(智能体)作用域的系统提示词段、工具提供方和变量明确不在本提案范围内:[agent 作用域贡献者契约](../../implemented/architecture/2026-07-08-agent-scope-contexts.md)有意允许在 `setup(agentCtx)` 期间通过 agent 拥有的上下文注册这三者,因此仓库内没有固定的作用域注册并不能证明它们未被使用。 ## 曾考虑的替代方案 -**为嵌入方保留运行时 skill 注册。** 这是已实现的 skill RFC 中一个刻意设计的同步直接定义便利接口。一个小型提供方包装层可以在 effect 拥有的生命周期下暴露相同的嵌入数据,但它必须实现异步 `list()`/`get()`、携带提供方身份、并接受提供方的重复语义。本提案选择保留一条统一的提供方路径,而非维护第二套排序、校验、缓存失效和查找路径。 +**保留面向嵌入方的运行时 skill 注册。** 这是已实现的 skill RFC 中有意提供的同步直接定义便利接口。一个小型提供方包装层可以在 effect 拥有的生命周期下暴露相同的嵌入数据,但它必须实现异步 `list()`/`get()`、携带提供方身份,并接受提供方的重复语义。本提案选择只保留一条统一的提供方路径,而非维护第二套排序、校验、缓存失效与查找路径。 ## 验收标准 -- skill 收集只有一条提供方驱动的路径;已完成缓存的键仅为 cwd;revision 纪元仅用于进行中的失效;保留的 skill 字段要么有生产读取方,要么有记录在案的刻意扩展契约。 -- agent 作用域的 prompt 段落、变量、工具提供方、工具守卫,以及原生模式和 Code Mode 下的结构化输出提交行为保持不变。 -- 类型检查、覆盖率、快照、doc-sync、module-graph 校验、构建和 hygiene 全部通过。 +- skill 收集只有一条提供方驱动的路径,已完成缓存仅以 cwd 为键,revision epoch 仅用于进行中的失效检测;保留的 skill 字段要么有生产读取方,要么有记录在案的有意扩展契约。 +- agent 作用域的提示词段、变量、工具提供方、工具守卫,以及原生模式和 Code Mode 下的 structured-output 提交行为保持不变。 +- 类型检查、覆盖率、快照、doc-sync(文档同步门禁)、module-graph 校验、构建与 hygiene 全部通过。 ## 风险 -这是对预发布 skill 注册表的一次编译可见的收缩。外部编程式 `list()`/`get()` 消费方将失去 `whenToUse` 路由提示和 candidate/definition 上的 `path`;已交付的模型目录从未渲染它们,资源解析保留了显式的 `resourceBase` 加上提供方自有的不透明 locator,但这些字段在可观测性上并不等价。skill 本地的 frontmatter 解析必须继续保留并校验所支持的 metadata schema,外部提供方仍可提供嵌入式、文件系统、远程或其他 skill 来源。 +这是对预发布 skill 注册表的编译可见收缩。外部编程式 `list()`/`get()` 消费方将失去 `whenToUse` 路由提示和 candidate/definition 的 `path`;已交付的模型目录从未渲染它们,资源解析保留了显式的 `resourceBase` 加上提供方自有的不透明 locator,但这些字段并非观测等价。skill 本地 frontmatter 解析必须继续保留并校验所支持的 metadata schema,外部提供方仍可提供嵌入式、文件系统、远程或其他 skill 来源。