实时订阅

消费统一 SessionMutation 流,渲染流式文本、工具状态、Turn 与 Session 变化

session.subscribe() 是 Session 唯一的实时入口。它只广播订阅建立之后的变化,不缓存、不回放,也不提供背压控制。

const unsubscribe = session.subscribe((mutation) => {
  apply_mutation(mutation);
});

// 不再需要时必须取消,避免重复渲染和内存泄漏。
unsubscribe();

订阅回调可以是异步函数。单个订阅者抛错或 reject 不会回滚已持久化状态,也不会阻塞其他订阅者。

八类 Mutation

所有 Mutation 都有 mutation_id、session_id 与 created_at。

varianttype用途是否带 Message revision
message`useragent`顶层 Message 的完整快照
part`textreasoningtool
delta`textreasoningtool_input`
turn`startfinish`Turn 执行生命周期
file_diff—当前 Turn 的文件改动实时摘要否
session`titleconfig`Session 自身属性变化
warningmodel_request模型请求失败与重试进度否

message、part 和 delta 都在对应完整 Message 持久化成功后才发布。Mutation envelope 本身不写入 canonical history。

用 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 与标题

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 === "session" && mutation.type === "title") {
    set_session_title(mutation.title);
  }
});

turn.finish 的状态为 completed、failed 或 stopped。它和 turn.finished 共同描述同一最终结果:前者适合订阅 UI,后者适合控制流。

上下文压缩不是独立 Mutation,也没有对应的 public Handle。它是 SessionComposer.advance_context() 的结果,写成 Action Part:成功时为 completed,失败时为 failed,action_type 为 context-compaction。它不会改写 canonical 历史,只推进 Composer 的派生表(见 元数据与落盘)。

模型调用失败时,warning / model_request 会携带 request_kind、code、message、retryable、attempt、max_attempts 与 will_retry。主 Turn 请求还携带 turn_id,后台标题与历史压缩请求允许不带。它是实时诊断,不写入 Message 历史;retryable 表示 Provider 的错误分类,will_retry: false 表示本次失败不会再自动重试。

工具 Part 的状态机

同一个 Tool Part 以同一个 part_id 更新:

input-streaming -> ready -> waiting-user -> running -> completed
                                                   \-> failed
ready ----------------------------------------------> running

input-streaming 表示模型仍在输出工具参数。此阶段参数原文通过 tool_input Delta 追加到 Tool Part 的 input_text;ready Part 是输入完整后的校准快照,并携带解析完成的 input。UI 应以 tool_call_id 或 part_id 做稳定 key,不要为每个 Delta 新增一项。

如果参数流结束时不是有效 JSON,同一个 Part 会直接从 input-streaming 进入 failed,error 会说明输入解析失败;Tool 实现不会执行。Tool 正常返回结构化 { success: false } 时也会进入 failed。客户端只应根据 Part 的 state 和 error 展示结果,不要根据输出文本猜测成功与否。

审批的处理方式见 工具审批。

生命周期边界

unsubscribe() 只停止未来事件,不会停止 Session、Turn 或工具。路由切换、组件卸载和 Session 切换时都应调用它;切换回该 Session 后重新加载快照并重新订阅。

断线恢复不能只靠重新订阅,见 重连与同步。