DSH / Atlas
2026-07-23implementedarchitecture

Toolview dissolution — tool rows are per-view keyed slots

toolview 溶解——工具行即 per-view keyed slot

After the view ring dissolved into the slot system, the client kept exactly one parallel registration model: the tool ring — a named registry (`ctx.toolviews`) with its own register grammar, its own resolve semantics (scoped-beats-global predicate dispatch), its own subscribe/version pair, its own inject cache, and its own render outlet with a private error boundary. Every one of those was a second implementation of

English

Problem

After the view ring dissolved into the slot system, the client kept exactly one parallel registration model: the tool ring — a named registry (ctx.toolviews) with its own register grammar, its own resolve semantics (scoped-beats-global predicate dispatch), its own subscribe/version pair, its own inject cache, and its own render outlet with a private error boundary. Every one of those was a second implementation of something the slot machinery already owned, and every future capability (a store seat for row drafts, i18n injection, cross-bundle identity) would have had to be built twice or drift. The ring's one honest justification was that tool names are a runtime-open set while SlotMap is a closed declaration table — a registry keyed by arbitrary strings seemed structurally necessary.

Decision

The tool ring is gone as independent infrastructure: a tool row is a keyed child slot each view declares for itself, and the client has exactly one registration model. The justification above was hollow — a keyed slot's key space is already runtime-open (SlotMap declares slots, never keys; the ask-user composer's key: 'question' was the precedent), so the open tool-name set fits entryKey dispatch natively.

This decision originally placed 'conversation.chat.toolview' under the chat entry and made the chat render site dispatch each row. The follow-up Tool presentation ownership moves that placement into a whole-Tool seat and gives ui-tool one keyed 'tool.call.toolview' child slot. That follow-up changes the presentation owner, not this decision's core constraint: Tool registration continues to use ordinary keyed-slot machinery, with framework-owned activation, replacement, caching, error isolation, versioning, and fallback behavior.

Accepted semantic changes

Four behavioral deltas were accepted deliberately, not overlooked. Cross-view appearance was initially per-view registration; the follow-up note records why root/subcall composition later justified one Tool-wide presentation owner. Same-key double registration is a loud throw where the registry let later-wins silently override — a discipline correction, not a loss. Session-dimension dispatch, when a row needs it, belongs inside the component (the standard kit already carries useSessions), not in registry predicates — there is no shipped session-variant exemplar today. Registry-level shape override by third parties (a scoped registration shadowing a global one) has no equivalent; a real future need routes through key-naming conventions or a small in-component resolver, never a revived parallel registry.

Alternatives considered

Keep the standalone registry (the original shape). Rejected: each of its multi-dimensional dispatch axes has a more correct home — presentation ownership belongs to an explicitly declared child slot, and the session dimension belongs inside the component, which already holds the standard kit. What remains is a second copy of slot machinery with no distinguishing capability.

Promote renderToolView into the standard kit and move the registry into the runtime package. Rejected: Tool presentation is Client UI vocabulary; hoisting it into runtime would leak presentation into the data object layer and still leave two registration models.

Derive slot declarations from subscription refCounts (declare the slot implicitly when the first registrant subscribes). Rejected for implicit coupling and debounce complexity; noted as a possible revisit only if a genuinely multi-viewer UI appears.

A thin registerToolView facade over slots.register. Deferred, not rejected: after dissolution the facade would carry only compile-time sugar (slot-name literal narrowing, tool→key vocabulary, props pre-composition) with zero runtime. Per "enforce at the operation boundary" (a facade is not an enforcement point), it stays unbuilt; the useful type composition ships as the exported Tool view props alias. A later facade can be added without disturbing direct registration if repeated registration ceremony justifies it.

Consequences

The client has one registration model; auditing who renders Tool calls means reading slot register calls, the same audit as every other slot. Registrants get the framework's error isolation, inject caching, and store seat for free — no capability ships twice. The costs are the accepted semantic changes above, chiefly loud duplicate-key failure and no third-party registry-level override. Independent registrants name the typed slot in ctx.slots.inject, so the dependency is explicit and follows declaration replacement without a service-order convention.

中文

Problem

视图环溶解进 slot 体系之后,client 侧恰好还剩一套平行注册模型:工具环——一个具名注册表(ctx.toolviews),带自己的 register 文法、自己的 resolve 语义(scoped 压 global 的谓词分发)、自己的 subscribe/version 对、自己的 inject 缓存、自己带私有错误边界的渲染出口。其中每一件都是 slot 机器已经拥有之物的第二份实现,而每一项未来能力(行草稿的 store 席位、i18n 注入、跨 bundle 身份)都将不得不建两遍或漂移。这条环唯一像样的存在理由是:tool 名是运行时开放集,而 SlotMap 是封闭声明表——以任意字符串为键的注册表看似结构上必需。

Decision

工具环作为独立基础设施已消失:工具行是各视图为自己声明的 keyed 子槽,client 全域只剩一种注册模型。上述理由是空的——keyed slot 的 key 空间本就运行时开放(SlotMap 声明槽、从不声明 key;ask-user composer 的 key: 'question' 即先例),开放的 tool 名集合天然适配 entryKey 分发。

本决策最初把 'conversation.chat.toolview' 放在 chat 条目下,由 chat 渲染点逐行分发。后续的 Tool 展示所有权 将该放置方式移入整体 Tool 席位,并让 ui-tool 拥有一个 keyed 'tool.call.toolview' 子 slot。后续决策改变的是展示所有者,而非本决策的核心约束:Tool 注册继续使用普通 keyed-slot 机制,激活、替换、缓存、错误隔离、版本与 fallback 行为仍归框架所有。

接受的语义变化

四项行为增量是刻意接受而非疏漏。跨视图出场最初采用逐视图注册;后续 Note 记录了为何 root/subcall 编排后来证明由一个 Tool 级展示所有者统一负责是合理的。同 key 重复注册从注册表的 later-wins 静默覆盖变为 loud throw——纪律修正而非损失。会话维分发若行需要,归组件内部(标配 kit 已带 useSessions),不走注册表谓词——今天没有已落地的会话变体样例。第三方在 registry 级覆盖形态(scoped 注册压过 global)不复存在;真出现的未来需求走 key 命名空间约定或组件内小 resolver,永不复活平行注册表。

Alternatives considered

保留独立注册表(原形态)。 拒绝:其多维分发的每一维都有更正确的家——展示所有权归显式声明的子 slot,会话维归已持有标配 kit 的组件内部。剩下的只是一份没有任何独有能力的 slot 机器副本。

renderToolView 提进标配 kit、注册表迁入 runtime 包。 拒绝:Tool 展示是 Client UI 词汇;上提进 runtime 会把展示概念泄漏进数据对象层,且依然留着两套注册模型。

以订阅 refCount 推导槽声明(首个注册方订阅时隐式声明槽)。拒绝:隐式耦合加去抖复杂度;记为将来真出现多视图 UI 时的备选。

slots.register 之上的薄 registerToolView 门面。 缓建而非拒绝:溶解后该门面只剩编译期语法糖(slot 名字面量收窄、tool→key 词汇翻译、props 预组合),运行时为零。按「enforce at the operation boundary」(门面不是强制点)保持不建;有用的类型组合以导出的 Tool view props 别名兑现。若重复注册仪式今后足以证明其价值,可在不扰动直接注册的前提下补充门面。

Consequences

client 只有一种注册模型;审计谁渲染 Tool 调用就是读 slot register 调用,与其他所有 slot 同一套审计。注册方免费获得框架的错误隔离、inject 缓存与 store 席位——没有能力要建两遍。代价即上文接受的语义变化,主要是重复 key 会 loud failure,且第三方无 registry 级覆盖。独立注册方在 ctx.slots.inject 中点名有类型约束的 slot,因此依赖关系既显式,又能跟随声明替换,无需服务顺序约定。