docs(rfc): narrow scoped-layer abstraction

This commit is contained in:
Tianyi Cui
2026-07-13 13:49:25 +08:00
parent eb76065622
commit 0646bae562
3 changed files with 10 additions and 12 deletions

View File

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

View File

@@ -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<L>`** is a concrete scheduler, not a base class. It owns the global layer plus one `Map<ScopeKey, L>`, 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<L>`** is a concrete scheduler, not a base class. It owns the global layer plus one `Map<ScopeKey, L>`, 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<V>` and `AnonymousEntries<V>`** 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<L extends ScopeLayer> {
merge<T>(scope: ScopeKey | undefined, pick: (layer: L) => NamedEntries<T>, admitGlobal?: (name: string) => boolean): Map<string, T>
values<T>(scope: ScopeKey | undefined, pick: (layer: L) => EntryValues<T>): T[]
keys<T>(scope: ScopeKey | undefined, pick: (layer: L) => NamedEntries<T>): 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.

View File

@@ -21,13 +21,13 @@ agent 作用域落地之后([agent-scope RFC](../../implemented/architecture/2
`dsh-scope` 新增与键类型无关的 `store.ts`peer 依赖仍只有 Cordis。模块只抽取六个现有登记口已经共同证明的最小形状**业务状态与校验留在显式层类里;一个 helper 统一负责选层、挂 effect、回滚、通知与回收**。一个 helper 实例属于一个服务;一个层实例聚合某 scope 对该服务的全部贡献。
- **`ScopedLayers<L>`** 是具体调度器,不作基类。它持有全局层与一张 `Map<ScopeKey, L>`,通过显式工厂按需构造专属层,并在层 `isEmpty()` 时回收。`effect(ctx, action, options?)` 只接受一个同步 actionaction 只返回一个同步 undo因为六个现有登记口的完整形状就是如此。单一 `ctx` 同时决定可见层(`scopeOf(ctx)`)与属主 Cordis fiber`ctx.effect`),「对 X 可见、随 Y 销毁」因此不可表达。helper 在通知监听器前 yield undo返回 Cordis 的原始 disposer并在校验或变更抛错时回收刚建出的空层。读取接口是 `global`/`peek`,以及 `merge`(命名条目的专属遮蔽与可选全局放行谓词)、`values`(不遮蔽地依次拼接全局与专属条目)`keys`(限制前名字全集)`some`(跨层不变量检查)
- **`ScopedLayers<L>`** 是具体调度器,不作基类。它持有全局层与一张 `Map<ScopeKey, L>`,通过显式工厂按需构造专属层,并在层 `isEmpty()` 时回收。`effect(ctx, action, options?)` 只接受一个同步 actionaction 只返回一个同步 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<V>``AnonymousEntries<V>`** 是两种共用的保插入序条目表。命名表暴露 `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<L extends ScopeLayer> {
merge<T>(scope: ScopeKey | undefined, pick: (layer: L) => NamedEntries<T>, admitGlobal?: (name: string) => boolean): Map<string, T>
values<T>(scope: ScopeKey | undefined, pick: (layer: L) => EntryValues<T>): T[]
keys<T>(scope: ScopeKey | undefined, pick: (layer: L) => NamedEntries<T>): 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) 的注册章节。