实时订阅
消费统一 SessionMutation 流,渲染流式文本、工具状态、Turn 与 Session 变化
实时订阅
session.subscribe() 是 Session 唯一的实时入口。它只广播订阅建立之后的变化,不缓存、不回放,也不提供背压控制。
const unsubscribe = session.subscribe((mutation) => {
apply_mutation(mutation);
});
// 不再需要时必须取消,避免重复渲染和内存泄漏。
unsubscribe();订阅回调可以是异步函数。单个订阅者抛错或 reject 不会回滚已持久化状态,也不会阻塞其他订阅者。
六类 Mutation
所有 Mutation 都有 mutation_id、session_id 与 created_at。
variant | type | 用途 | 是否带 Message revision |
|---|---|---|---|
message | `user | assistant | action |
part | `text | reasoning | tool |
delta | `text | reasoning | tool_input` |
turn | `start | finish` | Turn 执行生命周期 |
compact | `start | finish` | 显式 Compact Command 生命周期 |
session | title | Session 自身属性变化 | 否 |
message、part 和 delta 都在对应完整 Message 或 Assistant 草稿持久化成功后才发布。Mutation envelope 本身不写入 active.jsonl 或 Segment。
用 Delta 流式显示文本
session.subscribe((mutation) => {
if (mutation.variant !== "delta" || mutation.type !== "text") return;
append_text({
message_id: mutation.message_id,
part_id: mutation.part_id,
delta: mutation.delta,
});
});Delta 是新增片段,不是累计全文。不要把 delta 当成完整 Part 覆盖 UI;也不要根据文本内容推断顺序,使用 message_id、part_id 与 revision。tool_input Delta 还会携带对应的 tool_call_id。
用完整快照纠正状态
网络、渲染批处理或 UI 切换可能让客户端错过自己维护的中间状态。message 与 part 是纠正状态的完整快照:
function apply_mutation(mutation: SessionMutation): void {
if (mutation.variant === "message") {
upsert_message(mutation.message, mutation.revision);
return;
}
if (mutation.variant === "part") {
upsert_part(mutation.message_id, mutation.part, mutation.revision);
return;
}
if (mutation.variant === "delta") {
if (mutation.type === "tool_input") {
append_tool_input_delta(
mutation.message_id,
mutation.part_id,
mutation.tool_call_id,
mutation.delta,
);
} else {
append_text_delta(mutation.message_id, mutation.part_id, mutation.delta);
}
}
}客户端应保存每个 Message 已应用的最大 revision。低于或等于当前 revision 的 message / part 快照可以忽略,避免旧事件覆盖新状态。
Turn、Compact 与标题
session.subscribe((mutation) => {
if (mutation.variant === "turn" && mutation.type === "start") {
set_turn_running(mutation.turn_id);
}
if (mutation.variant === "turn" && mutation.type === "finish") {
set_turn_finished(mutation.turn_id, mutation.status, mutation.error);
}
if (mutation.variant === "compact" && mutation.type === "finish") {
set_compact_finished(mutation.compact_id, mutation.status, mutation.reason);
}
if (mutation.variant === "session" && mutation.type === "title") {
set_session_title(mutation.title);
}
});turn.finish 的状态为 completed、failed 或 stopped。它和 turn.finished 共同描述同一最终结果:前者适合订阅 UI,后者适合控制流。
compact.finish 与 compact_handle.finished 使用同一最终结果语义:Mutation 适合更新 UI,Handle 适合业务控制流。
工具 Part 的状态机
同一个 Tool Part 以同一个 part_id 更新:
input-streaming -> ready -> waiting-user -> running -> completed
\-> failed
ready ----------------------------------------------> runninginput-streaming 表示模型仍在输出工具参数。此阶段参数原文通过 tool_input Delta 追加到 Tool Part 的 input_text;ready Part 是输入完整后的校准快照,并携带解析完成的 input。UI 应以 tool_call_id 或 part_id 做稳定 key,不要为每个 Delta 新增一项。
审批的处理方式见 工具审批。
生命周期边界
unsubscribe() 只停止未来事件,不会停止 Session、Turn 或工具。路由切换、组件卸载和 Session 切换时都应调用它;切换回该 Session 后重新加载快照并重新订阅。
断线恢复不能只靠重新订阅,见 重连与同步。