Stop mirroring durable boundaries as agent events
停止将持久化边界镜像为 agent 事件
The loop records the canonical transcript in `SessionEvent` and also emitted a parallel set of live `agent/*` boundary mirror events: `agent/turn-start`, `agent/turn-end`, `agent/step-start`, and `agent/step-end`. The mirrors made consumers choose between two sources of truth for the SAME durable fact. ACP already chose the session log for prompt settlement and committed output because it is the one durable, replayab
English
Problem
The loop records the canonical transcript in SessionEvent and also emitted a parallel set of live agent/* boundary mirror events: agent/turn-start, agent/turn-end, agent/step-start, and agent/step-end. The mirrors made consumers choose between two sources of truth for the SAME durable fact. ACP already chose the session log for prompt settlement and committed output because it is the one durable, replayable record; consuming a live mirror would require reconciling its timing with the boundary already stored in that log. The stdio UI was the only production consumer that still rendered turn boundaries from the mirror events; it already rendered tool calls and results from session/event.
This duplication is not free. Every lifecycle change had to update the session event, the mirror event, docs, invariants, tests, and snapshot expectations. The duplicate boundary events also made failure ordering subtle: a turn can be durably closed before a live agent/turn-end listener runs, so a post-boundary listener failure has no valid in-log position left and must be reported out of band.
Decision
Make session/event the single live boundary/transcript stream. Consumers that render turns, tool calls, tool results, assistant messages, and durable boundaries subscribe to session/event and derive their UI from the same event vocabulary persistence uses.
The four durable-boundary mirrors — agent/turn-start, agent/turn-end, agent/step-start, agent/step-end — are removed from the agent event taxonomy. A UI that wants the agent handle at a boundary retains the live target object from agent/created/agent/disposed and compares its session directly; dsh-ui-stdio uses this to label the app-owned agent's [main turn N] header while other sessions render their durable id. The canonical record remains the event-sourced session log.
The step mirrors (which had no consumer at all) were removed first, in the event-domain-semantics Agent Note; that Agent Note KEPT the turn mirrors on the stated justification that the stdio UI needed the Agent handle at the turn boundary. This decision finishes the job: dsh-ui-stdio is a disposable test REPL whose rendering can change freely, so "ui-stdio needs it" is not a reason to keep a mirror — it reads session/event and retains only its live target object.
Scope: what is and isn't removed
Removed (durable-boundary mirrors — the session log is authoritative for each): agent/turn-start, agent/turn-end, agent/step-start, agent/step-end.
RETAINED — NOT durable-boundary mirrors, so out of scope for this decision:
agent/steering— not a boundary, so out of scope for THIS decision. It mirrors the durablesteering/messagecontrol record rather than a boundary, and was removed by its own follow-up: Remove theagent/steeringmirror emit.agent/stream-chunk— the live token stream. Out of scope for THIS decision (a mirror of the durableassistant/chunk, not a boundary), it was removed by its own follow-up: Stop mirroring the token stream as an agent event.agent/created,agent/disposed,agent/status,agent/error,agent/queued— lifecycle/control events that are not transcript data.agent/queuedin particular is an inbox acknowledgement that fires before any durable event exists (cancelled queued work may never enter the log), so it is deliberately live-only.
Alternatives considered
- Bundling
agent/steeringinto the removal — the original proposal's shape; narrowed out as scope creep: it mirrors the durablesteering/messagecontrol record, not a boundary, and was removed by its own later decision (as wasagent/stream-chunk, by the stream-chunk-mirror Agent Note). - Keeping the turn mirrors for the stdio UI — the event-domain-semantics Agent Note's original stance; rejected here because
dsh-ui-stdiois a disposable test REPL, not a load-bearing consumer, and it renders boundaries fromsession/eventplus its live target object instead.
Consequences
A plugin can no longer observe turn/step boundaries from a convenient Agent-first event. It subscribes to session/event and, if it needs the live object, resolves the shared id through ctx.agents or retains the object it already owns. That is an acceptable trade: boundary consumers should not depend on a second event feed that can drift from the durable log.
中文
问题
循环在 SessionEvent 中记录规范 transcript(文本记录),同时还发出一组并行的实时 agent/* 边界镜像事件:agent/turn-start、agent/turn-end、agent/step-start 和 agent/step-end。这些镜像迫使消费方在同一持久事实的两个真源之间做选择。ACP(Agent Client Protocol)已经为提示词结算和已提交输出选择会话日志,因为它是唯一持久、可重放的记录;消费实时镜像需要把它的时序与日志中已经存储的边界进行调和。stdio UI 是唯一仍从镜像事件渲染轮次边界的生产环境消费方;它已经从 session/event 渲染工具调用和工具结果。
这种重复并非零成本。每次生命周期变更都需要同时更新会话事件、镜像事件、文档、不变式、测试和快照预期。重复的边界事件还使失败事件的先后关系变得微妙:一个轮次可能在实时 agent/turn-end 监听器运行之前就已被持久化关闭,因此边界之后的监听器失败在日志中已没有合法位置可以插入,只能带外上报。
决策
将 session/event 作为唯一的实时边界/transcript 流。需要渲染轮次、工具调用、工具结果、助手消息和持久化边界的消费方统一订阅 session/event,从持久化层使用的同一套事件词汇中派生 UI。
四个持久边界镜像——agent/turn-start、agent/turn-end、agent/step-start、agent/step-end——已从 agent(智能体)事件分类体系中移除。希望在边界处取得 agent handle 的 UI 会保留来自 agent/created/agent/disposed 的实时目标对象,并直接比较其会话;dsh-ui-stdio 据此为应用拥有的 agent 标记 [main turn N] 头部,其他会话则渲染其持久 id。规范记录仍是事件溯源会话日志。
步骤镜像(完全没有消费方)最先在事件域语义 Agent Note 中移除;该 Agent Note 当时以 stdio UI 需要在轮次边界取得 Agent handle 为由,保留了轮次镜像。本决策完成余下工作:dsh-ui-stdio 是可随时丢弃的测试 REPL,其渲染可以自由变化,因此「ui-stdio 需要它」并不是保留镜像的理由——它读取 session/event,只保留自己的实时目标对象。
范围:移除什么、不移除什么
已移除(持久边界镜像——每项都以会话日志为权威):agent/turn-start、agent/turn-end、agent/step-start、agent/step-end。
保留——不是持久边界镜像,因此不在本决策范围内:
agent/steering——不是边界,因此不在本决策范围内。它镜像持久的steering/message控制记录,而非边界,后来由自己的后续决策移除:移除agent/steering镜像 emit。agent/stream-chunk——实时 token 流。不在本决策范围内(它镜像持久的assistant/chunk,而非边界),后来由自己的后续决策移除:停止将 token 流镜像为 agent 事件。agent/created、agent/disposed、agent/status、agent/error、agent/queued——不属于 transcript 数据的生命周期/控制事件。尤其是agent/queued,它是在任何持久事件存在之前触发的收件箱确认(取消的排队工作可能永远不会进入日志),所以有意只保留为实时事件。
曾考虑的替代方案
- 将
agent/steering一并移除——原始提案的范围;因超出范围而被排除:它镜像持久的steering/message控制记录,而非边界,后来由自己的决策移除(agent/stream-chunk也由流分片镜像 Agent Note 移除)。 - 为 stdio UI 保留轮次镜像——事件域语义 Agent Note 的原始立场;在此否决,因为
dsh-ui-stdio是可随时丢弃的测试 REPL,而非承载关键约束的消费方,并且它改为根据session/event加自己的实时目标对象渲染边界。
后果
插件不能再通过便捷的、以 Agent 为首个参数的事件观察轮次/步骤边界。它需要订阅 session/event;如果需要实时对象,则通过 ctx.agents 查找共享 id 对应的对象,或保留自己已经拥有的对象。这是可以接受的取舍:边界消费方不应依赖可能与持久日志发生漂移的第二个事件源。