Injected context reached the transcript as one anonymous shape whose expanded body was the whole message serialized as inline JSON, so the escaping collapsed the only readable part — the model-facing prose — into a single line. `MessageSource` gains an optional producer-declared `form`, a small semantic vocabulary of information shapes independent of `kind`: kind says who produced the context, form says what shape it is, so several producers may share one presentation. Two values ship. `instructions` (workspace-context) lists the reconciled files above their text and keeps the `<system-reminder>` framing verbatim, because the framing is part of what the model read. `catalog` moves dsh-tool-skill off the shared plugin kind onto a `skill-catalog` source carrying the published name/description entries, and the body lists those instead of re-parsing `<available_skills>` out of the prose. Catalog identity moves with it: the republish digest now covers the durable entries, deleting the text-slicing that recovered them from a logged message. Everything else renders the opaque body — the model-facing text with its real line breaks, then the remaining provenance as fields. That is the documented default, not a leftover: a resumed, forked, or foreign log must render whether or not its producer is mounted here, which is why the classification lives in the durable source rather than a client-side table keyed by producer.
6.2 KiB
Agent Note:Web transcript 标出上下文来源、召回与 steering
Status: implemented
English | 中文
Problem
生产方向模型侧对话补充的一切内容,进入 Web transcript(文本记录)后只剩两种匿名形态。每一条已记录的非用户 user/message——skill 目录、运行时快照、经过对账的 AGENTS.md 指令、guard 提示、子 agent 汇报、跨会话快照——都塌缩成同一行 上下文注入,读者不逐行展开去读原始 JSON 就无从知道究竟注入了什么。steering(中途引导)的情况更糟:它渲染成与开轮提示完全相同的气泡,于是 transcript 无法说明哪一条消息打断了正在运行的轮次。
这些区分本来就是持久事实。user/message.source 是每个生产方都必须提供的可合并扩展来源,steering 另有自己的 steering/message 事件;把它们丢掉的只有呈现层。被这套 Web UI 取代的终端 transcript 本来会写出每张卡片的生产者,因此面对同一份日志,Web 侧是一次倒退。
Decision
transcript 为非提示消息可能承担的三种角色分别命名:注入上下文、召回会话、steering。
TranscriptAdapter 与历史折叠为每个 ContextMessageNode 附加一份 provenance 视图,由 contextProvenance() 仅依据持久来源计算得出。它返回 role(inject,跨会话快照则为 recall)与命名生产者的 label。ContextInjectionRow 以角色作为标题,并按 ToolRow 摘要的几何在标题旁展示该名称,因此折叠态就已经回答了「注入了什么、由谁注入」;141px 滚动视口与截断上限沿用展开项决策,未作改动。视口里渲染什么,则由上下文形态决策引入的、相互独立的形态轴决定。
名称从日志中读出,绝不来自客户端维护的生产者名称表。 workspace-instructions 以它对账过的去重指令文件路径命名,session-reference 以它读取的会话标题命名,插件来源以其记录的插件 id 命名,其余来源则以自身的 kind 命名——这正是可合并扩展联合类型有文档记载的默认分支。没有可读 kind 的来源降级为无名注入。于是新增或重命名的生产者无需客户端发版即可辨识,任何名称都不会相对代码变味,恢复、fork 或来自外部的日志与实时会话的投影结果完全一致。
recall 覆盖 session-reference,因为它是当前唯一会把另一个会话的材料搬进本会话的已发布来源。今天没有任何 Web 叶子挂载 dsh-session-reference——它此前只有终端宿主——因此该分支的存在是为了日志可移植性,而不是为了某个已打包的生产方,其覆盖来自单元测试而非组装后的 Web 场景。
MessageItem 为持久与待处理的 steering 气泡加上 插话 标注。这推翻了取消 steer 入口与插话装饰中的一条结论——当时移除徽章,是因为 composer 无法 steer,标签指向了用户做不到的动作。此后 composer 获得了 Steer 手势,却没有同步修订那份 note;本决策提供了它在「重新引入」条款中要求的产品决策,并订正了其中留下的过时事实。标注是这里唯一的 steering 装饰:composer 模式、Queue dock 的严格 steer 操作、待处理 steering 的生命周期仍归各自的所有者。
Alternatives considered
在客户端本地化生产者名称。 以插件 id 为键的字典读起来确实比 @deepseek-ai/dsh-system-prompt 好,但它会在每次重命名时悄悄失准,每新增一个生产者都要改客户端,而且对来自外部的日志根本无法命名。日志已经承载的来源,比客户端自己编出来的措辞更有价值。
按来源 kind 注册呈现。 展开项决策把键控的 context-view 槽位推迟到出现由来源自有的呈现需求为止。为一行命名并不构成独立呈现,而以「已挂载的生产者」为键的注册表恰恰会在最要紧的地方失效——生产者已不再挂载的恢复日志同样必须渲染出来。
在 host 侧计算角色与名称。 那需要为每份事件副本附加一个视图,重复陈述持久来源已经说明的事实,并为每条上下文消息增加一个 wire 字段。改由投影为每个节点计算一次,与 transcript 其他派生事实同处一地。
给 steering 独立的行而非带标注的气泡。 steering 是一条在轮次中途抵达的用户消息;独立行形会打断右对齐的阅读节奏,并且要为零新增信息重复气泡上的复制与分支操作。
把同一套名称扩展到 trajectory 表格。 不在本次范围内:该表格的上下文单元格有自己的文本推导,而 issue 要求的是对话面。
Testing
packages/client/runtime单元覆盖钉住每个来源分支、名称字段缺失/为空/类型不符时的回退,以及来源没有可读 kind 时的无名降级。packages/client/ui-conversation的 jsdom 覆盖钉住角色标题、标题旁的生产者名称、展开后该名称的留存、无名时的标题形态,以及持久与待处理气泡上的 steering 标注。- 无密钥的组装 Web 黄金基线携带带名称的标题栏与 steering 标注,因此证明这些标识的是组装后的 transcript,而不只是组件测试。
Consequences
- 读者一眼即可归因 transcript 中每一条非提示消息;即便面对本客户端版本从未见过其生产者的日志,标题栏依然如实。
- 只要来源仅携带插件 id,UI 中的生产者名称就呈现为包名形态(
dsh-tool-skill、@deepseek-ai/dsh-system-prompt)。这是拒绝客户端名称表的代价;想要更好名称的生产者应当记录更好的来源。 ContextMessageNode增加了一个必填字段,因此每一处构造该节点的代码——包括测试 fixture——都必须提供它。- 在某个宿主挂载
dsh-session-reference之前,recall分支在已发布的 Web 叶子中没有生产者,只能通过别处写入的日志抵达。