From 0646bae562ffb3c9f50152703e74bcc051c90cb4 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Mon, 13 Jul 2026 13:49:25 +0800 Subject: [PATCH] docs(rfc): narrow scoped-layer abstraction --- .../2026-07-12-scoped-layers-store.i18n.yaml | 4 ++-- .../architecture/2026-07-12-scoped-layers-store.md | 9 ++++----- .../architecture/2026-07-12-scoped-layers-store.zh.md | 9 ++++----- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/docs/rfc/proposed/architecture/2026-07-12-scoped-layers-store.i18n.yaml b/docs/rfc/proposed/architecture/2026-07-12-scoped-layers-store.i18n.yaml index bf5027e5ec..c12497f016 100644 --- a/docs/rfc/proposed/architecture/2026-07-12-scoped-layers-store.i18n.yaml +++ b/docs/rfc/proposed/architecture/2026-07-12-scoped-layers-store.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -2026-07-12-scoped-layers-store.md: 510f462344b25b72bb8604d88f125cc92c6510b1 -2026-07-12-scoped-layers-store.zh.md: abc2aad140cc10325392ed8800e33e38684511a8 +2026-07-12-scoped-layers-store.md: f2056446abb3b8d0793b6ef7898c01001ccf0460 +2026-07-12-scoped-layers-store.zh.md: 9726eb3de953af1086d8bcdd779fc02b3c324596 diff --git a/docs/rfc/proposed/architecture/2026-07-12-scoped-layers-store.md b/docs/rfc/proposed/architecture/2026-07-12-scoped-layers-store.md index 510f462344..f2056446ab 100644 --- a/docs/rfc/proposed/architecture/2026-07-12-scoped-layers-store.md +++ b/docs/rfc/proposed/architecture/2026-07-12-scoped-layers-store.md @@ -21,13 +21,13 @@ Finally, one agent's contribution to one service is scattered across several map `dsh-scope` gains a key-agnostic `store.ts`, with Cordis as its only peer dependency. The module implements the smallest abstraction shared by the six current sites: **business state and validation stay in an explicit layer class; one helper owns layer selection, effect attachment, rollback, notification, and reclamation**. One helper instance belongs to one service, and one layer instance aggregates everything a scope contributes to that service. -- **`ScopedLayers`** is a concrete scheduler, not a base class. It owns the global layer plus one `Map`, constructs scoped layers on demand through an explicit factory, and reclaims a layer when `isEmpty()`. Its `effect(ctx, action, options?)` accepts one synchronous action that returns one synchronous undo because that is the complete shape of all six current sites. The single `ctx` decides both the visible layer (`scopeOf(ctx)`) and the owning Cordis fiber (`ctx.effect`), so "visible to X, disposed with Y" stays unrepresentable. The helper yields the undo before notifying listeners, returns Cordis's exact disposer, and reclaims a newly created empty layer if validation or mutation throws. Reads are `global`/`peek` plus `merge` (named entries with scoped shadowing and an optional global-admission predicate), `values` (global then scoped concatenation without shadowing), `keys` (the pre-restriction name universe), and `some` (cross-layer invariant checks). +- **`ScopedLayers`** is a concrete scheduler, not a base class. It owns the global layer plus one `Map`, constructs scoped layers on demand through an explicit factory, and reclaims a layer when `isEmpty()`. Its `effect(ctx, action, options?)` accepts one synchronous action that returns one synchronous undo because that is the complete shape of all six current sites. The single `ctx` decides both the visible layer (`scopeOf(ctx)`) and the owning Cordis fiber (`ctx.effect`), so "visible to X, disposed with Y" stays unrepresentable. The helper yields the undo before notifying listeners, returns Cordis's exact disposer, and reclaims a newly created empty layer if validation or mutation throws. Reads are `global`/`peek` plus `merge` (named entries with scoped shadowing and an optional global-admission predicate), `values` (global then scoped concatenation without shadowing), and `keys` (the pre-restriction name universe). - **Explicit `ScopeLayer` classes** make each service's state visible to readers. `ToolLayer` and `PromptLayer` declare their three table properties and their `isEmpty()` aggregation directly; a small layer factory receives only the scope, while its closure may capture real constructor dependencies. Domain methods stay ordinary class methods. This costs a few repetitive declarations but avoids a mapped-type class factory, a scheduler/layer ownership cycle, reserved property names, and generated runtime structure. - **`NamedEntries` and `AnonymousEntries`** are the two shared insertion-ordered tables. Named entries expose `insert`/lookup and retain the current global/scoped duplicate wording through domain `kind` and per-agent-alternative labels; anonymous entries expose only `append`, using process-unique symbol keys for O(1) undo removal. Keeping the classes separate makes meaningless mixed named/anonymous operations unrepresentable and keeps key types sound. Their iterators borrow membership and typed contribution values; they do not clone or freeze values. `ScopedLayers` materializes only the merged arrays/maps already required by the service read paths. -`dsh-tools` migrates its three tables into one `ToolLayer`: tools, compiled restrictions, and guards. The layer owns restriction admission and guard evaluation; the facade retains domain validation that needs service configuration, such as the reserved `run_code` name and the current known-global-name universe. Readonly allow/deny inputs are compiled once into internal sets. `dsh-system-prompt` likewise migrates sections, tool providers, and variables into one `PromptLayer`; its facade performs owner-final cross-layer checks through `layers.some`. Every registration facade performs its public argument validation and then makes one `effect` call with a label and, for guards, `silent: true`. A generic helper does not learn domain rules such as "restrictions require a scoped context." +`dsh-tools` migrates its three tables into one `ToolLayer`: tools, compiled restrictions, and guards. The layer owns restriction admission and guard evaluation; the facade retains domain validation that needs service configuration, such as the reserved `run_code` name and the current known-global-name universe. Readonly allow/deny inputs are compiled once into internal sets. `dsh-system-prompt` likewise migrates sections, tool providers, and variables into one `PromptLayer`. Every registration facade performs its public argument validation and then makes one `effect` call with a label and, for guards, `silent: true`. A generic helper does not learn domain rules such as "restrictions require a scoped context." -`assemble` stays in the `SystemPrompt` facade for three reasons: the subject scope's layer may not exist and reads must not create it; shadowing requires merge-before-evaluate so a hidden section provider is never called; and the assembly waterfall, `toolOrder`, and owner-final restoration use service-level resources. Sections and tool providers keep their current materialized derived views. Variable providers instead iterate the global and scoped `NamedEntries` directly, preserving today's live Map behavior when a provider registers another variable during assembly. Tool guards likewise iterate their `AnonymousEntries` directly. Owner-final remains metadata on section and tool contributions, not a second protection registry. +`assemble` stays in the `SystemPrompt` facade for three reasons: the subject scope's layer may not exist and reads must not create it; shadowing requires merge-before-evaluate so a hidden section provider is never called; and the assembly waterfall and `toolOrder` use service-level resources. Sections and tool providers keep their current materialized derived views. Variable providers instead iterate the global and scoped `NamedEntries` directly, preserving today's live Map behavior when a provider registers another variable during assembly. Tool guards likewise iterate their `AnonymousEntries` directly. Migration preserves public behavior and exact duplicate messages. The internal aggregate layer is reclaimed only after all three tables empty rather than when one table empties; no service API exposes layer identity. Direct live iteration retains current re-entrant variable-provider and guard behavior, while selector helpers continue to materialize the same section, tool-provider, and tool-resolution views their facades build today. @@ -47,7 +47,6 @@ export class ScopedLayers { merge(scope: ScopeKey | undefined, pick: (layer: L) => NamedEntries, admitGlobal?: (name: string) => boolean): Map values(scope: ScopeKey | undefined, pick: (layer: L) => EntryValues): T[] keys(scope: ScopeKey | undefined, pick: (layer: L) => NamedEntries): string[] - some(fn: (layer: L, scope: ScopeKey | undefined) => boolean): boolean effect(ctx: Context, action: (layer: L) => () => void, options: { label: string; silent?: boolean }): () => void } @@ -133,7 +132,7 @@ class ToolRegistry extends Service { ## Acceptance criteria -- `store.ts` ships in `dsh-scope` (peer dependencies unchanged: Cordis only; module-graph position unchanged) with per-file 100% coverage of layer selection and reclamation, synchronous action/undo ordering, throwing-action cleanup, throwing-change-listener rollback, exact disposer identity, `label`/`silent`, factory typing, cross-layer `some`, merge selectors, and separate named/anonymous entry semantics. Its five public symbols are re-exported from the package root and carry export JSDoc. +- `store.ts` ships in `dsh-scope` (peer dependencies unchanged: Cordis only; module-graph position unchanged) with per-file 100% coverage of layer selection and reclamation, synchronous action/undo ordering, throwing-action cleanup, throwing-change-listener rollback, exact disposer identity, `label`/`silent`, factory typing, merge selectors, and separate named/anonymous entry semantics. Its five public symbols are re-exported from the package root and carry export JSDoc. - `dsh-tools` and `dsh-system-prompt` each collapse to one `ScopedLayers`; every registration facade validates its domain contract and then makes one `effect` call, and all keep returning the exact Cordis effect disposer. - Existing behavior, duplicate messages, validation order, live variable-provider re-entrancy, and live guard re-entrancy remain unchanged. Tests additionally pin aggregate reclamation timing and selector materialization. - Documentation lands in the same change: `dsh-scope`/`dsh-tools`/`dsh-system-prompt` READMEs; on implementation this RFC moves to `implemented/` and the [runtime-design RFC](../../implemented/architecture/2026-07-12-agent-scope-runtime-design.md)'s registration section is updated in place. diff --git a/docs/rfc/proposed/architecture/2026-07-12-scoped-layers-store.zh.md b/docs/rfc/proposed/architecture/2026-07-12-scoped-layers-store.zh.md index abc2aad140..9726eb3de9 100644 --- a/docs/rfc/proposed/architecture/2026-07-12-scoped-layers-store.zh.md +++ b/docs/rfc/proposed/architecture/2026-07-12-scoped-layers-store.zh.md @@ -21,13 +21,13 @@ agent 作用域落地之后([agent-scope RFC](../../implemented/architecture/2 `dsh-scope` 新增与键类型无关的 `store.ts`,peer 依赖仍只有 Cordis。模块只抽取六个现有登记口已经共同证明的最小形状:**业务状态与校验留在显式层类里;一个 helper 统一负责选层、挂 effect、回滚、通知与回收**。一个 helper 实例属于一个服务;一个层实例聚合某 scope 对该服务的全部贡献。 -- **`ScopedLayers`** 是具体调度器,不作基类。它持有全局层与一张 `Map`,通过显式工厂按需构造专属层,并在层 `isEmpty()` 时回收。`effect(ctx, action, options?)` 只接受一个同步 action,action 只返回一个同步 undo,因为六个现有登记口的完整形状就是如此。单一 `ctx` 同时决定可见层(`scopeOf(ctx)`)与属主 Cordis fiber(`ctx.effect`),「对 X 可见、随 Y 销毁」因此不可表达。helper 在通知监听器前 yield undo,返回 Cordis 的原始 disposer,并在校验或变更抛错时回收刚建出的空层。读取接口是 `global`/`peek`,以及 `merge`(命名条目的专属遮蔽与可选全局放行谓词)、`values`(不遮蔽地依次拼接全局与专属条目)、`keys`(限制前名字全集)和 `some`(跨层不变量检查)。 +- **`ScopedLayers`** 是具体调度器,不作基类。它持有全局层与一张 `Map`,通过显式工厂按需构造专属层,并在层 `isEmpty()` 时回收。`effect(ctx, action, options?)` 只接受一个同步 action,action 只返回一个同步 undo,因为六个现有登记口的完整形状就是如此。单一 `ctx` 同时决定可见层(`scopeOf(ctx)`)与属主 Cordis fiber(`ctx.effect`),「对 X 可见、随 Y 销毁」因此不可表达。helper 在通知监听器前 yield undo,返回 Cordis 的原始 disposer,并在校验或变更抛错时回收刚建出的空层。读取接口是 `global`/`peek`,以及 `merge`(命名条目的专属遮蔽与可选全局放行谓词)、`values`(不遮蔽地依次拼接全局与专属条目)和 `keys`(限制前名字全集)。 - **显式 `ScopeLayer` 类**让每个服务的状态一眼可见。`ToolLayer` 与 `PromptLayer` 直接声明各自三项表属性与 `isEmpty()` 聚合;一个小工厂只向构造器传入 scope,闭包仍可捕获真实构造依赖。领域方法仍是普通类方法。代价是几行重复声明,收益是不用引入 mapped-type 类工厂、scheduler/layer 属主环、保留属性名和生成式运行时结构。 - **`NamedEntries` 与 `AnonymousEntries`** 是两种共用的保插入序条目表。命名表暴露 `insert`/查询,并通过领域 `kind` 与 per-agent alternative 标签保持现有全局/专属重名文案;匿名表只暴露 `append`,以进程内唯一 symbol 作键支持 O(1) 撤销删除。分成两类以后,无意义的命名/匿名混用不可表达,key 类型也保持健全。迭代器借用表成员与带类型的贡献值,不会 clone 或 freeze 值;`ScopedLayers` 只物化服务读路径本来就需要的合并数组或 Map。 -`dsh-tools` 把工具、已编译 restriction 与 guard 三张表合并进一个 `ToolLayer`。restriction 放行判断与 guard 求值归层所有;`run_code` 保留名、当前已知全局名集合等依赖服务配置的领域校验仍留在门面。只读 allow/deny 输入只编译一次,成为内部 Set。`dsh-system-prompt` 同样把 section、tool provider 与 variable 合并进一个 `PromptLayer`;门面通过 `layers.some` 完成 owner-final 跨层冲突检查。每个登记门面先完成公开参数校验,再以 label 做一次 `effect` 调用;guard 额外传 `silent: true`。通用 helper 不理解「restriction 必须由 scoped context 调用」之类领域规则。 +`dsh-tools` 把工具、已编译 restriction 与 guard 三张表合并进一个 `ToolLayer`。restriction 放行判断与 guard 求值归层所有;`run_code` 保留名、当前已知全局名集合等依赖服务配置的领域校验仍留在门面。只读 allow/deny 输入只编译一次,成为内部 Set。`dsh-system-prompt` 同样把 section、tool provider 与 variable 合并进一个 `PromptLayer`。每个登记门面先完成公开参数校验,再以 label 做一次 `effect` 调用;guard 额外传 `silent: true`。通用 helper 不理解「restriction 必须由 scoped context 调用」之类领域规则。 -`assemble` 留在 `SystemPrompt` 门面,三条理由:主体 scope 的层可能不存在,读路径不得创建它;遮蔽语义要求先合并再求值,被遮蔽的 section provider 绝不能被调用;组装 waterfall、`toolOrder` 与 owner-final 恢复使用服务级资源。section 与 tool provider 保持既有的派生视图物化;variable provider 则直接遍历全局与专属 `NamedEntries`,保留 provider 在组装期间登记另一 variable 时的现有活 Map 行为。tool guard 同样直接遍历其 `AnonymousEntries`。owner-final 仍是 section 与 tool 贡献上的元数据,不是第二张 protection 注册表。 +`assemble` 留在 `SystemPrompt` 门面,三条理由:主体 scope 的层可能不存在,读路径不得创建它;遮蔽语义要求先合并再求值,被遮蔽的 section provider 绝不能被调用;组装 waterfall 与 `toolOrder` 使用服务级资源。section 与 tool provider 保持既有的派生视图物化;variable provider 则直接遍历全局与专属 `NamedEntries`,保留 provider 在组装期间登记另一 variable 时的现有活 Map 行为。tool guard 同样直接遍历其 `AnonymousEntries`。 迁移保持公开行为与精确重名文案不变。内部聚合层会在三张表全部清空后才回收,而不是某一张表清空时回收;服务 API 不暴露层身份。直接活遍历保留现有 variable-provider 与 guard 重入行为,selector helper 则继续物化门面今天已经在构造的 section、tool-provider 与工具解析视图。 @@ -47,7 +47,6 @@ export class ScopedLayers { merge(scope: ScopeKey | undefined, pick: (layer: L) => NamedEntries, admitGlobal?: (name: string) => boolean): Map values(scope: ScopeKey | undefined, pick: (layer: L) => EntryValues): T[] keys(scope: ScopeKey | undefined, pick: (layer: L) => NamedEntries): string[] - some(fn: (layer: L, scope: ScopeKey | undefined) => boolean): boolean effect(ctx: Context, action: (layer: L) => () => void, options: { label: string; silent?: boolean }): () => void } @@ -133,7 +132,7 @@ class ToolRegistry extends Service { ## 验收标准 -- `store.ts` 落在 `dsh-scope`(peer 依赖不变:仅 Cordis;模块图位置不变),逐文件 100% 覆盖选层与回收、同步 action/undo 顺序、action 抛错清理、change 监听器抛错回滚、原始 disposer 身份、`label`/`silent`、工厂类型、跨层 `some`、合并 selector,以及分开的命名/匿名条目语义。五个公开符号从 package 根重导出并带 export JSDoc。 +- `store.ts` 落在 `dsh-scope`(peer 依赖不变:仅 Cordis;模块图位置不变),逐文件 100% 覆盖选层与回收、同步 action/undo 顺序、action 抛错清理、change 监听器抛错回滚、原始 disposer 身份、`label`/`silent`、工厂类型、合并 selector,以及分开的命名/匿名条目语义。五个公开符号从 package 根重导出并带 export JSDoc。 - `dsh-tools` 与 `dsh-system-prompt` 各收敛为一个 `ScopedLayers`;每个登记门面先校验领域契约再做一次 `effect` 调用,并继续返回 Cordis effect 的原始 disposer。 - 既有行为、重名文案、校验顺序、variable-provider 活重入与 guard 活重入不变。测试另行钉住聚合回收时机与 selector 物化。 - 文档随同一变更落地:`dsh-scope`/`dsh-tools`/`dsh-system-prompt` 的 README;实现后本 RFC 移入 `implemented/`,并就地更新[运行时设计 RFC](../../implemented/architecture/2026-07-12-agent-scope-runtime-design.md) 的注册章节。