设计记录。 当前行为以产品模型为准;本页保留原设计或实现阶段的背景。
由 Canonical Agent Event Timeline 拥有 Agent Turn 生命周期
从原仓库主干保留的设计记录。文中的日期、阶段、源码行号和实现判断属于记录当时;当前行为以 Session 模型与 Agent / Adapter 模型为准。 源码路径相对于公共仓库根目录;原
cmd/amux、cmd/amuxd、cmd/amuxlet的实现分别迁至app/cli、app/hub、app/runtime,cmd/*仅保留发行入口。
状态:已接受;持久化 Timeline、第一方消费者迁移、旧生命周期清理与直接 typed producer emission 均已实现。
日期:2026-08-27
背景
amux 支持 ACP Agent 和多个 structured Provider。它们的原生协议在事件名称、终态 信号、结果位置、权限请求、重试通知和 Provider 原生 turn ID 上并不一致。 规范化这些差异是不可避免的业务复杂度;在每个内部层反复规范化则是额外复杂度。
本决策之前,同一个逻辑 turn 可能依次表现为 provider.Event、session.Event、 broker frame、扁平 protobuf AttachEvent、agentui.Event 和 TaskEvent。 Session 还会分配 Turn 身份,并根据 event kind、Provider event type、state 与 stop reason 的组合推断终态。Task 和展示消费者随后又重复解释其中部分信息。
这不仅是重复实现。一次 structured-broker 终态在跨进程时丢失了 typed Provider 事件,Session 无法识别该帧为终态,turn gate 因而没有关闭,下一条队列输入可能 永久等待。临时修复给 state frame 加入 Provider event type,并在 Runtime 侧重建 事件。补丁虽有必要,却说明生命周期真相没有唯一所有者。
本设计只取代早期 structured-provider watchdog 设计中的终态所有权与传播机制; 当前 Provider 规则由 Agent / Adapter 产品模型 维护。原有 timeout、activity、user-wait 和 tool-wait 策略仍然有效;其到期路径仍清理 producer 自己的 watchdog/turn 状态,并通过精确的 per-turn capability 发出显式 typed 终态。Session 将其映射到准确的 canonical Turn。Watchdog 清理不属于 Session 完成屏障。
原共享 provider.Event / session.Event 链是包含可选字段的兼容信封,不拥有 canonical Turn 身份、顺序或终态幂等性。它已从 producer 到 Session 的路径删除; Claude 原生 sidecar JSON DTO 仍局限在其 Provider decoder 内。
同时,amux 有三种刻意区分的历史:
- Canonical Session Timeline 是持久化且与 Provider 无关的记录,用于结构化 Agent turn 审查和基于 cursor 的交付。
- Session 可见文本 replay 是派生的对话交接视图,不是实时事件流或生命周期权威。
TaskEvent是只追加、脱敏、持久化的 TaskRun 流,拥有自己的 sequence、cursor 和 replay 契约。
它们不是三个竞争的当前状态来源。Session 与 TaskRun 快照仍是控制面当前状态的 权威,Timeline 记录可观察的 Agent 行为。把全部状态替换为 event-sourced store 会 远超审查需求,并迫使进程、socket、授权租约和 Task 控制状态进入不自然的 replay 模型。
决策
1. 增加由 Session 拥有的 Agent Event Timeline
每个 Session 组合一个 internal/agentstream Timeline,作为 Agent turn 事件的 canonical、Provider 无关的实时真相。它拥有:
- amux Turn 身份和事件顺序;
- Session 用于接收 typed producer emission 的精确 per-turn capability;
- typed event 校验;
- 每个被接受 turn 恰好一个 canonical
TurnFinished; - 终态幂等性,以及对终态之后事件的拒绝;
- 实时订阅和有界的 Session 策略投影能力。
Provider 和 ACP 实现直接发出不含 canonical 身份的 agentstream.ProducerEmission: 一个 typed turn payload、显式终态或 Session payload,附原生来源信息。 Session 把 turn payload 和终态传给捕获的精确 Turn;只有 Timeline 分配 canonical 身份与顺序。Task 直接订阅 typed Timeline 事件;CLI/MCP/web/飞书使用 Follow/Tail, agentui 映射 canonical 事件。Session 从已经接受的 canonical 值派生元数据、权限、 活动和可见对话视图,不反向构造兼容事件,也不产生第二条实时流。
Timeline 不拥有 Session 授权、权限策略、watchdog 配置、进程控制、Task 状态、展示 策略或可见文本 replay 存储;这些职责留在原有模块。它是结构化 Agent 历史的持久化 来源,不是全部 Session 行为的来源。
外部边界是一个具体 Timeline,小接口围绕启动 turn、发布 Session 级状态和按 cursor 读取。持久化存储位于窄的内部 Journal 接口后;Session 将具体 Timeline 与 Runtime SQLite store 组合。不引入全局 event bus,也不让消费者依赖数据库 Schema。
2. amux 拥有 Turn 身份与派发关联
Queue 和 Timeline 分配 canonical TurnID 与 TurnSeq。Provider message ID、thread ID、 原生 turn ID 和原生 sequence 只作为来源信息,不能替换或覆盖 amux 身份。
启动 turn 是一个操作:Timeline 创建 Turn 并发出 TurnStarted,Session 安装绑定到 该 Turn 的 producer capability,然后用不透明 amux token 派发。 进程内 driver 通过 callback 返回 typed emission。ACP 和 Structured broker v3 都使用 版本化信封,每个关联 emission 返回不透明 token,以及 broker 分配的、turn 内单调 递增 sequence。Session 将每个 typed emission 映射到捕获 Turn 的 Emit 或 Finish。 Broker sequence 仅用于重连 replay,不是 Provider sequence,也不替代 canonical SessionID + Epoch + Seq。Provider 原生 turn ID 不足以承担此角色,因为 queue admission 与生命周期身份由 amux 拥有。支持 replay 的 broker 公告版本化 turn_replay_version,按 token 分配 sequence,并在尝试 socket 交付前把不可变 emission 放入进程本地 replay buffer。Broker frame 携带不可变 producer emission, 绝不携带 canonical SessionID、TurnID、Cursor 或 timestamp。
每个进程内 driver 都必须提供此 capability。它只在以下情况有效:协议严格单 turn 且终态是最后一帧,或者能够按原生 turn 身份路由复用事件。 共享 EventSinks 不为有歧义的流推断关联。Broker 必须公告并成功协商 correlated-turn 协议后才接受连接。仍存活的旧 broker 会被拒绝,不能通过 Session 级 Prompt frame 驱动 Provider。
3. 派发失败也是可观察终态
StartTurn 创建 canonical Turn 后,driver 派发失败必须结束同一个 Turn:
TurnFinished{outcome=error, code=submit_failed}只关闭内部 gate 而不发终态,会形成不完整 turn:队列已经推进,Task、Attach 或飞书 却永远看不到已接受提交为何结束。
如果 per-turn capability 在派发返回错误前已接受 producer 终态,以已接受终态为准。 Timeline 不产生第二个终态。
4. TurnFinished 是唯一终态
Canonical 生命周期规则:
TurnStarted 是被接受 turn 的第一个事件。
TurnFinished 是该 turn 最后一个被接受的事件。
ResultObserved 携带结果内容,绝不关闭 turn。
Diagnostic、Provider 错误通知、工具失败和 usage 本身不是终态。
Provider completion、watchdog 和 interrupt 路径必须通过准确的 correlated sink 发出显式终态,由 Session 映射为 Finish。
Session 拥有的 process-exit 路径可以直接 Finish 精确 Turn。第一个被接受的终态具有权威性。重复同一终态是幂等操作,返回原事件,不分配新 sequence。第二个结果冲突时返回 ErrTerminalConflict 并保留首个终态;当前 Timeline 不自动追加诊断。已结束 Turn 的后续 emission 被拒绝,不会重新绑定到新 Turn。
稳定的 canonical outcome 为 success、error、cancelled 和 interrupted。 Provider 特定原因在 producer 协议边界规范化为稳定的 amux code 和诊断文本。 Task 直接把 canonical 终态映射为 TerminalOutcome,不检查 Provider stop reason 或原生 DTO。
5. Session 执行同步完成屏障
Timeline 接受 TurnFinished 后,Turn 的可观察事件生命周期完成,但这本身不表示 Session 已准备好派发下一条输入。
队列推进前,Session 针对精确 Turn 身份同步执行完成屏障。在 MR1–3 中,屏障清理 canonical turn 的授权和待处理权限状态,完成后队列才可启动下一项。 Provider watchdog 在 producer 终态路径停止并清理自身状态;Session 元数据持久化 仍沿用原有路径,不属于该屏障。
屏障不是异步 Timeline 订阅者。缓慢或失败的 Attach、Task、飞书消费者不能阻止 生命周期清理,旧 Turn 的完成也不能清理新 Turn 的状态。 Interrupt 被接受后仍可在 Provider 报告终态前立即撤销授权,因为这是 Session 拥有的安全策略。
6. 先持久化 Timeline 再交付,保留派生对话 replay
每个 Session 有一个不可变 Timeline epoch,随 Session 创建并在 amuxlet 重启后保留。 SQLite 严格按 sequence 存储封闭的 canonical event union。事件必须先追加并提交, 才进入内存历史或唤醒订阅者。追加失败时 Timeline 进入故障状态,不消耗 sequence, 拒绝后续写入,关闭订阅者并要求 Session 停止 producer。amux 不能继续产生未记录的事件后缀。
Cursor 在 transport 边界是不透明值,精确标识 SessionID + Epoch + Seq。 读取分为三种显式语义:
- History:返回 cursor 之后已提交的事件,然后到达 EOF。
- Follow:返回 cursor 之后已提交的历史,跨过一个无缺口的 caught-up 边界,再等待未来提交的事件。
- Tail:忽略读取开始时已有的历史,只等待未来提交的事件。
Attach 必须选择其中一种显式模式,并暴露 caught-up watermark;旧 replay 字段已 删除,其 wire number 保留。History 是只读交付操作:重放旧 canonical 事件绝不能 重新派发 Provider 输入、回答权限、调用工具或重复其它副作用。
Runtime 恢复时,Timeline 在暴露 Session 前校验完整 journal,恢复准确的 event/turn sequence 和 sticky metadata,并将未完成 Turn 保留为活动 Turn。 若 producer 进程已退出,Session 追加一个 TurnFinished{outcome=interrupted, code=runtime_restart},不重新派发旧输入。 未知事件版本或 kind、信封不匹配、sequence 缺口或无效 turn 生命周期都拒绝恢复。
如果 broker 在 Runtime 重启后仍存活且支持有序 replay,会把断线窗口 emission 保留在按不透明 turn token 和 broker sequence 索引的进程本地 buffer 中。 交付连接断开只清除当前 owner,不清除该 buffer。恢复的 Runtime 从持久化 Timeline journal 中该活动 Turn 的已提交 canonical 事件取得最大 broker sequence,作为 after_sequence,然后调用 rebind(turn_token, after_sequence)。 Broker 按序重放 sequence > after_sequence 的 buffer emission,无缝转入实时交付, 不再次 dispatch。曾被有意忽略、因此没有 canonical journal 事件的 producer frame 可能重放后再次被忽略;replay 本身不得执行 Provider 或工具副作用。 这个进程本地 replay 接口不是第二份持久化日志,也不定义逐帧持久化 ACK 协议。 首版保留活动或最近完成 Turn 的 buffer,直到不同 Turn 的 dispatch 替代它; 不会静默淘汰重连所需的事件后缀。
仍存活的旧 broker 若未公告 replay 能力,恢复会拒绝执行,而不是容忍可能的缺口 进行 rebind,或重新派发输入。
首个持久化版本的保留策略遵循 Session 所有权:清理或删除 Session 会原子删除其 canonical Timeline 和派生对话消息。没有独立 event TTL、compaction 或跨 Session 全局顺序。Runtime 数据库仅 owner 可访问(0600)。只有稳定且经过策略过滤的 canonical union 持久化;原始 Provider frame、Tool.RawOutput、原始权限详情和 权限 ToolCall.RawInput 均被排除。
Session 元数据/权限记账、活动与可见文本 handoff 都是已接受 agentstream.Event 的有界同步投影,不构造另一个 Agent event,不暴露实时订阅者,也不成为另一份终态真相。
7. 保持 TaskEvent 独立
agentstream.Event 描述一个 Session 的 Agent turn,TaskEvent 描述一个 TaskRun 的持久化控制面历史。它们在身份、保留、脱敏和交付方面要求不同,保持为独立模型。
Task 保留只追加事件日志、TaskRun 快照、脱敏、输出合并、task deadline 和 cursor 语义,Timeline 不取代这些职责。Task 直接消费 agentstream,映射 canonical 终态, 无需 Provider stop-reason helper。Producer 值仍是 Adapter 边界,但旧 v1 生命周期 和未指定模式的 Attach 路径已删除。
初始实施顺序
接受的首轮交付范围明确限制为:
- MR1 — canonical 契约与影子一致性。 添加 typed Timeline 事件、校验与测试,同时保持旧生命周期为权威。
- MR2 — per-turn 关联。 为进程内 driver 将 Session emission capability 绑定到准确 Turn;ACP 与 Structured broker 都采用带 per-turn broker sequence 的版本化不透明 token 信封。在关联可观察且稳定前,权威仍留在旧路径。
- MR3 — 生命周期权威与完成屏障。 关联后的 Session 使用 Timeline 决定终态真相和队列推进,为已有消费者与 replay 保留 canonical-to-legacy 投影。
- 持久化 Session Timeline。 交付前将封闭 canonical union 追加到 Session SQLite journal,启动时恢复,加入不透明 cursor 的 History/Follow/Tail 读取,暂时保留未指定的旧 Attach 模式。
- 第一方消费者迁移。 Task 直接订阅 typed event;CLI/MCP/web 使用 Follow,飞书使用 Tail,
agentui消费 canonical event。未指定 Attach 模式保留为隔离的兼容边界。 - 清理旧生命周期。 强制关联的
DispatchTurn,拒绝旧 broker 和未指定模式的 Attach,然后删除 Session 级实时订阅者、structured replay cache、旧 turn gate 和终态推断。 - 直接 typed producer emission。 Provider/ACP Proc API 和 broker v3 携带不含 canonical 身份的 typed payload 与显式终态;将接受后的 canonical 事件直接投影到 Session 策略,并删除共享 producer/session 兼容 Adapter。
以下内容明确不在本次实施范围:
- 在 sidecar 协议直接围绕 canonical 语义版本化之前,删除 Claude sidecar 原生 JSON DTO;
- 删除仅用于向可见 replay 追加本地系统通知的小型非 Agent
session.Event; - 用事件溯源替换派生对话 replay/handoff;
- 用事件溯源替换 Session 或 TaskRun 控制面快照;
- 持久化原始 Provider frame 或不透明工具/权限 dump;
- 将 TaskEvent 或展示策略并入 Timeline;
- 引入 Kafka、独立事件服务或完整 Session 事件溯源。
考虑过的替代方案
只共享事件值对象
把 provider.Event 移到中性包并改成 typed union,是有用的迁移步骤,但作为最终 模块过于浅。Turn 分配、终态推断、队列 gate 和重复完成仍散落在调用方。 它不满足删除检验:删除该模块只会删掉类型,几乎没有行为需要重新散布到其它位置, 因为行为从未被收进接口后面。
完整持久化、事件溯源的 Session
先把每个事件写入 SQLite,再应用 Session、Attach、Task 和飞书 projector,可以提供 强 replay 与 cursor 语义;但从日志重建全部 Session 状态,会让进程、socket、queue admission、授权租约与 Task 控制状态依赖 replay。接受的设计只持久化 canonical Agent Timeline,继续以 Session/Task 快照为权威。完整 Session 事件溯源仍被否决。
保留 Provider/Session 终态推断并加强测试
为每次转换添加更多测试可以减少局部回归,却仍保留多个真相源。每新增 Provider 或 展示表面,都需要理解 error、state 与 stop reason 的哪些组合构成终态。 这个方案没有解决架构根因。
影响
正面影响:
- 队列推进依赖一个显式 typed 终态,而不是分散的推断约定;
- 旧 turn 的延迟 emission 不会污染下一轮;
- 派发失败、watchdog、进程退出和 interrupt 共用可观察的完成模型;
- 结果内容与生命周期完成可以独立演进;
- Provider 特定变化留在 producer 协议边界后;
- Session 完成清理在一个边界变得确定且可测试;
- 已提交历史可跨 Runtime 重启按不透明 cursor 审查或续读,不重放 Provider 副作用;
- 先追加后通知,防止消费者看到尚未记录的事件;
- 后续 Task、Attach 和展示消费者可逐步删除重复映射与终态分支。
成本与风险:
- Per-turn 关联改变 broker 和 driver 的顺序,是迁移中风险最高的部分;
- canonical 授权与待处理权限清理的完成屏障正确之前,不得把队列推进交给 Timeline;producer watchdog 清理必须在终态 emission 前完成;
- 首版为每个 Session 将 canonical 历史加载到内存;超长 Session 未来可能需要有界分页或 compaction,但不能改变 cursor/读取契约;
- 先追加后通知会为每个 canonical 事件增加一次 SQLite 写入;
- 原生 Provider/ACP decoder 仍与协议相关,但输出边界统一为 typed producer emission 契约;
- 部署后仍存活的旧 broker 被拒绝,其运行 Session 可能结束或归档,而非原地迁移。
验证不变量
首轮交付测试必须通过 Timeline/Session 边界证明:
- Canonical Turn 身份由 amux 分配,绝不由 Provider ID 决定;
- 派发失败恰好产生一个可观察的
submit_failed终态; ResultObserved位于终态之前,且不关闭 gate;- 可重试诊断和工具失败不会隐式结束 Turn;
- 重复同 outcome 的 finish 幂等,冲突 finish 被拒绝;
- 旧 Turn 的迟到事件被拒绝,绝不分配给下一轮;
- Producer watchdog/interrupt 终态与 Session 进程退出通过各自 Adapter/直接路径汇聚为一个
TurnFinished; - Turn N 的 Session 完成屏障在 Turn N+1 派发前完成;
- 缓慢实时消费者不能阻塞 Provider 进度或生命周期清理;
- 追加先于通知;追加失败使 Timeline 故障,保留最后已提交 sequence,并停止 producer 继续推进;
- History 到达 EOF,Follow 无缺口地从历史转入实时,Tail 只观察订阅后提交的事件;
- 同一 Session epoch 的持久化 cursor 跨 Runtime 重启仍有效,格式错误、指向未来或 epoch 不匹配的 cursor 被拒绝;
- 恢复校验完整生命周期,不重新派发未完成 turn;支持 replay 的存活 broker 从已提交 canonical 历史中的最大 broker sequence 续传而不 dispatch,不支持 replay 的旧 broker 拒绝恢复,已退出 producer 获取一个
runtime_restart终态; - 清理 Session 时原子移除 Timeline,持久化 JSON 中不含原始 Provider dump;
- 未指定 Attach 模式和旧 broker Prompt frame 均被拒绝;
- TaskEvent 保持独立持久化、有序、脱敏且可 replay。
结构上的完成标准是删除旧实现,而不是仅增加新包。Session 终态推断、broker 终态 重建、消费者特有的 Provider stop-reason 分支,以及 producer/session 兼容 Adapter 都已从 turn 生命周期删除。原生 wire DTO 留在各自协议 decoder 内,不是生命周期 权威或消费者事件流。