Sessions

实时订阅

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

实时订阅

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

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

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

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

六类 Mutation

所有 Mutation 都有 mutation_idsession_idcreated_at

varianttype用途是否带 Message revision
message`userassistantaction
part`textreasoningtool
delta`textreasoningtool_input`
turn`startfinish`Turn 执行生命周期
compact`startfinish`显式 Compact Command 生命周期
sessiontitleSession 自身属性变化

messagepartdelta 都在对应完整 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_idpart_idrevisiontool_input Delta 还会携带对应的 tool_call_id

用完整快照纠正状态

网络、渲染批处理或 UI 切换可能让客户端错过自己维护的中间状态。messagepart 是纠正状态的完整快照:

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 的状态为 completedfailedstopped。它和 turn.finished 共同描述同一最终结果:前者适合订阅 UI,后者适合控制流。

compact.finishcompact_handle.finished 使用同一最终结果语义:Mutation 适合更新 UI,Handle 适合业务控制流。

工具 Part 的状态机

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

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

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

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

生命周期边界

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

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