kimi-code 深度掌握系列文章-会话记录的数据层:Transcript (十三)

1. Transcript 的定位

1.1 不是日志,是会话记录系统

很多系统把"日志"当作事后追溯的工具------打印到 stdout、写进文件,出了问题时 grep 一下。Transcript 的定位完全不是这样。它是一个​四层架构的会话记录系统​,是 kimi-code 运行时最活跃的数据管道:

  • 记录 Agent 的每一次操作:不只是文本输出,而是 Turn → Step → Frame 的完整树状结构,包含工具调用参数、LLM 耗时、token 用量等结构化数据
  • 为 UI 提供数据:CLI、Web Dashboard、VS Code 扩展等不同界面通过同一套 Transcript API 获取渲染数据
  • 支持会话重放和恢复 :重启后从 wire.jsonl 重建完整的会话状态,包括任务列表、待办事项、Goal 进度、交互审批记录等

1.2 纯 TypeScript 实现,浏览器安全

packages/transcript 是一个​纯协议/数据包 ​------它只定义数据结构、操作词汇和收敛规则,​不导入任何引擎代码 ​。这意味着同一个包可以在浏览器端(Web Dashboard)和服务器端(kap-server)安全加载,不会因为一个 import 就拖入整个 Node.js runtime。所有引擎相关的 payload(如 toolCallFrame.inputtoolCallFrame.output)都是 unknown 类型------数据对 Transcript 层是完全透明的,只在 View 层(L4)由具体 UI 框架解释。

Transcript 包不导入引擎,不导入 Node.js,不导入任何 UI 框架。它的唯一外部依赖是 zod(用于跨进程的 schema 校验)。

2. 四层架构总览

Transcript 采用严格的分层设计,每一层有明确的职责边界:

scss 复制代码
┌─────────────────────────────────────────────────────────────────────┐
│                      Transcript 四层架构                             │
│                                                                     │
│  ┌───────────────────────────────────────────────────────────────┐  │
│  │  L4  Views --- 渲染器注册表                                      │  │
│  │  ViewRegistry<C>  registerTool / registerInput / registerMarker │  │
│  │  框架无关:CLI ←→ Web ←→ VS Code 各自注册自己的组件             │  │
│  └───────────────────────────────────────────────────────────────┘  │
│                              ▲                                      │
│  ┌───────────────────────────────────────────────────────────────┐  │
│  │  L3  Subscriptions --- 按粒度订阅                                 │  │
│  │  off / turn / block / delta   Per-agent grade map               │  │
│  │  WS 实时推送 + REST 补全  → 双通道同步                           │  │
│  └───────────────────────────────────────────────────────────────┘  │
│                              ▲                                      │
│  ┌───────────────────────────────────────────────────────────────┐  │
│  │  L2  Ops --- 传输词汇表(幂等操作)                                │  │
│  │  turn.upsert / step.upsert / frame.upsert / append              │  │
│  │  task.upsert / interaction.upsert / attachment.upsert / ...     │  │
│  │  除 append 外全幂等 → replay 安全                                │  │
│  └───────────────────────────────────────────────────────────────┘  │
│                              ▲                                      │
│  ┌───────────────────────────────────────────────────────────────┐  │
│  │  L1  Store --- AgentTranscript(copy-on-write 状态机)            │  │
│  │  AgentState { items, tasks, interactions, attachments, ... }   │  │
│  │  apply() 是唯一的收敛路径,前端/后端同一份代码                    │  │
│  └───────────────────────────────────────────────────────────────┘  │
│                                                                     │
│  数据流向:Engine Events → Ops → apply() → onChange → L3 → WS/REST  │
│  客户端:  WS/REST → Ops → apply() → onChange → L4 → UI 渲染       │
└─────────────────────────────────────────────────────────────────────┘

这个四层架构的核心原则:​每一层只关心自己的抽象层级​。L1 关心"当前状态是什么";L2 关心"数据如何传输";L3 关心"谁需要什么";L4 关心"数据如何渲染"。修改一层不影响其他层。

3. L1 Store --- AgentTranscript

3.1 按智能体粒度的状态存储

在 kimi-code 中,一个 Session 可以包含多个 Agent(主 Agent + 子 Agent + Swarm 成员)。每个 Agent 有自己独立的 AgentTranscript 实例,由 TranscriptStore(会话级根节点)管理:

Typescript 复制代码
class TranscriptStore {
  #agents = new Map<AgentId, AgentTranscript>();

  ensureAgent(agentId, descriptor?): AgentTranscript;  // 懒创建
  removeAgent(agentId): boolean;                        // 移除子 Agent
  agents(): readonly AgentDescriptor[];                 // 获取全会话 Agent 清单
}

AgentTranscript 本身是一个​copy-on-write 状态机​,核心结构:

Typescript 复制代码
interface AgentState {
  items: TranscriptItem[];            // 有序时间线:Turn | Marker | TaskRef
  tasks: Map<TaskId, TranscriptTask>;           // 后台执行实体(shell/subagent/tool)
  interactions: Map<InteractionId, ...>;       // 审批/提问实体
  attachments: Map<AttachmentId, ...>;         // 附件元数据
  todos: Map<TodoId, TranscriptTodo>;          // 待办事项的最新状态
  prompts: Map<PromptId, TranscriptPrompt>;    // 提示队列
  meta: TranscriptMeta;                         // Goal/Plan/Swarm/Agent 状态
  pendingInteractions: Set<InteractionId>;     // 待处理的交互请求
  hasMoreOlder: boolean;                        // 窗口化时标记更早的 Turn 存在
}

3.2 唯一收敛路径:apply()

apply()AgentTranscript 的唯一写入入口------无论数据来自服务端引擎事件还是客户端 WebSocket 推送,都走同一条路径:

Typescript 复制代码
class AgentTranscript {
  apply(ops: readonly TranscriptOperation[]): AppliedOps {
    const accepted: TranscriptOperation[] = [];
    let gap: AppliedOps['gap'];
    let state = this.#state;
    for (const op of ops) {
      const result = applyOperation(state, op);   // 纯函数 reducer
      if (result.gap) { gap = ...; continue; }    // append offset 不连续
      if (!result.changed) continue;               // 幂等跳过
      state = result.state;
      accepted.push(op);
    }
    this.#state = state;
    if (accepted.length > 0) {
      const event = { agentId: this.agentId, ops: accepted };
      for (const listener of this.#listeners) listener(event);  // 广播
    }
    return { accepted, gap };
  }
}

关键设计决策:

  • snapshot() 是零拷贝的getItems() 返回的数组不会在后续 apply 时被修改------每次状态变化都创建新的引用
  • 服务端和客户端使用同一份 ​AgentTranscript ​代码:没有"服务端状态"和"客户端投影"的区别,双方持有的是完全相同的 state machine
  • Lazy skeleton on step/frame upsert :如果 step 的 turn 还不存在,applyStepUpsert 会自动创建一个骨架 Turn(状态为 running,origin 为 other),保证结构完整性

4. L2 Ops --- 幂等操作词汇表

4.1 操作分类

L2 定义了 14 种操作(TranscriptOperation 的联合类型),分为三类:

Typescript 复制代码
// 1. 结构操作(全幂等------重复执行结果一致)
ResetOp          { op: 'reset', snapshot: AgentTranscriptSnapshot }
TurnUpsertOp     { op: 'turn.upsert',   turn: TurnHeader }
StepUpsertOp     { op: 'step.upsert',   turnId, step: StepHeader }
FrameUpsertOp    { op: 'frame.upsert',  turnId, stepId, frame }
MarkerUpsertOp   { op: 'marker.upsert', item: TranscriptMarker }
TaskRefUpsertOp  { op: 'taskref.upsert', item: TranscriptTaskRef }
TaskUpsertOp     { op: 'task.upsert',   task }
InteractionUpsertOp  { op: 'interaction.upsert', interaction }
AttachmentUpsertOp   { op: 'attachment.upsert', attachment }
TodoUpsertOp     { op: 'todo.upsert',   todo }
PromptUpsertOp   { op: 'prompt.upsert', prompt }
MetaMergeOp      { op: 'meta.merge',    meta }
ItemsRemoveOp    { op: 'items.remove',  ids }

// 2. 追加操作(唯一的非幂等操作)
AppendOp         { op: 'append', target, offset: number, text: string }

4.2 幂等性设计

append 外,所有操作都是 ​state-style upsert/merge​------相同输入多次执行结果一致:

  • 重复发送 :如果 turn.upsert 的字段与当前状态完全相同(通过 turnEquals() 逐字段比较),返回 { changed: false },不触发 onChange
  • 乱序到达:由于每个 ops 都携带完整的实体数据(而非 delta),任意顺序 apply 最终收敛到相同状态
  • 重放安全:客户端断线重连后,服务端重放所有 ops,客户端直接 apply------已处理的自动跳过,未处理的逐个应用

4.3 append 的特殊性

append 是唯一的非幂等操作,用于流式传输文本:

Typescript 复制代码
// LLM streaming: 文本逐 chunk 到达
{ op: 'append', target: { type: 'frame', turnId: 't0', stepId: 't0.1', frameId: 't0.1.f1' },
  offset: 0,  text: '我将' }
{ op: 'append', target: { ... }, offset: 2, text: '帮您分析' }
{ op: 'append', target: { ... }, offset: 6, text: '这个问题' }

// appendAtOffset 校验 offset:
//   offset < local.length  → 检查重叠区域是否匹配(不匹配 → gap,分流恢复)
//   offset == local.length → 新 chunk,追加
//   offset > local.length  → gap:上游比本地快,需要 re-snapshot

append 的 gap 检测是双通道同步的关键------当客户端通过 WebSocket 接收流式 ops 时,如果某个 chunk 丢失(offset 跳跃),apply() 返回 gap 信号,触发 since_seq catch-up 请求补全缺失的批量操作。

4.4 操作如何保证数据一致性

L2 的一致性由三条规则保证:

  1. 单一通道序列 :每个 agent 的 ops 从服务端通过单个有序通道发出(按 batch seq 递增),客户端严格按服务端顺序 apply
  2. flush 重发完整状态 :当 frame/step/turn 完成时,服务端发送对应的 frame.upsert/step.upsert/turn.upsert(携带完整数据),即使客户端之前丢失了部分 ops,flush upsert 也会将其"拉回"正确状态
  3. 降级不丢失状态 :当订阅粒度从 delta 降到 block 时,客户端丢弃流水中的 append ops,下一个 flush upsert 将 TextFrame 的完整文本一次性送达------不会丢失内容,只是收到的时间点更晚

5. L3 Subscriptions --- 按粒度订阅

5.1 四级粒度定义

订阅粒度(TranscriptGrade)是一个字符串字面量联合类型,附在每个连接上:

Typescript 复制代码
type TranscriptGrade = 'off' | 'turn' | 'block' | 'delta';

// 粒度排序(GS 连接/客户端来决定用户需要什么粒度)
const GRADE_RANK = { off: 0, turn: 1, block: 2, delta: 3 };
粒度 包含的 ops 适用场景
off Agent 不可见,完全关闭传输
turn turn.upsert, marker.upsert, taskref.upsert, 全局实体和 meta Agent 选择器、任务面板、"Turn 完成"通知
block turn 级 + step.upsert, frame.upsert(每帧完整状态) 浏览历史会话、翻页查看已有结果
delta 全部 ops,包括 append chunks 实时流式渲染、打字机效果

5.2 粒度过滤的实现

filterOpsForGrade() 在 L3 层根据当前粒度过滤 ops:

Typescript 复制代码
function admits(grade: TranscriptGrade, op: TranscriptOperation): boolean {
  switch (op.op) {
    case 'append':        // 只有 delta 级别才通过
      return GRADE_RANK[grade] >= GRADE_RANK.delta;
    case 'step.upsert':   // block 及以上通过
    case 'frame.upsert':
      return GRADE_RANK[grade] >= GRADE_RANK.block;
    default:              // turn headers, markers, tasks, meta 等
      return true;        // turn 级别就全通过
  }
}

5.3 为什么需要不同粒度?

这是 UI 渲染的性能优化关键:

  • Agent 列表不需要步骤详情 :浏览器打开 Web Dashboard 时,左侧的 Agent 列表只需要知道每个 Agent 有几轮 Turn 及其状态------以 turn 粒度订阅足矣
  • 翻阅历史不需要流式增量 :用户翻看几轮之前的对话时,以 block 粒度获取即可------每个 frame 一次性拿到完整文本,无需处理 chunk 拼接
  • 当前活跃 Turn 需要实时渲染 :正在运行的 Agent,用户需要看到逐字输出的打字机效果------必须 delta 粒度

粒度切换也考虑了安全性:

Typescript 复制代码
// 降级:客户端升级到更高粒度时,服务端重发 reset snapshot
function needsResetOnTransition(prev: TranscriptGrade, next: TranscriptGrade): boolean {
  return GRADE_RANK[next] > GRADE_RANK[prev];
}

5.4 Per-Session + Per-Agent 订阅映射

订阅配置是一个 Record<agentId|'*', grade> 映射------'*' 作为默认值,具体 agent 覆盖:

Typescript 复制代码
// 示例:主 Agent 实时流式,子 Agent 只看标题
const spec = {
  '*':     'turn',       // 默认不看详情
  'main':  'delta',      // 主 Agent 实时流式
  'sub-1': 'block',      // 子 Agent 1 看块级内容
};

6. L4 Views --- 渲染器注册表

6.1 框架无关的渲染器注册

ViewRegistry 是一个泛型类,抽象了"工具帧如何渲染"这一跨 UI 的问题:

Typescript 复制代码
class ViewRegistry<C = unknown> {
  #toolRenderers = new Map<string, C>();     // key: view ?? name(小写)
  #inputRenderers = new Map<string, C>();    // key: origin.kind
  #markerRenderers = new Map<string, C>();   // key: marker key

  registerTool(key: string, renderer: C): this;    // 注册工具渲染器
  registerInput(originKind: string, renderer: C): this;  // 注册输入渲染器
  registerMarker(marker: string, renderer: C): this;     // 注册标记渲染器

  resolveTool(frame: ToolCallFrame): C | undefined;    // frame.view ?? frame.name
  resolveInput(origin: TurnOrigin): C | undefined;    // origin.kind
  resolveMarker(marker: string): C | undefined;       // marker.marker
}

6.2 不同 UI 如何渲染相同数据

同一份 ToolCallFrame,不同 UI 注册不同的渲染器组件:

  • CLIC 是 Ink React 组件,以终端友好的格式展示工具调用(颜色高亮、折叠/展开)
  • Web DashboardC 是 Vue/React 组件,展示富交互的卡片(展开详情、复制参数、查看输出)
  • VS Code 扩展C 是 VS Code Webview 组件,嵌入编辑器上下文中

关键是,​数据模型完全不变​------所有 UI 只是注册了不同的"解释器",读取相同的 Transcript 数据结构。

6.3 可扩展的视图插件机制

注册表的设计天然支持扩展:

Typescript 复制代码
// 注册内置工具渲染器
registry.registerTool('read', ReadRenderer);
registry.registerTool('bash', BashRenderer);
registry.registerTool('agent', AgentRenderer);

// 第三方 MCP 工具渲染器
registry.registerTool('mcp__my_service', MyServiceRenderer);

// 自定义 Turn 输入渲染器
registry.registerInput('cron', CronInputRenderer);
registry.registerInput('task', TaskInputRenderer);

如果没有找到匹配的渲染器,resolveTool() 返回 fallbackTool(构造函数中的默认渲染器),保证任何工具调用都不会"白屏"。

7. 核心数据模型

7.1 Turn --- 一轮对话

Typescript 复制代码
interface TranscriptTurn {
  kind: 'turn';
  turnId: TurnId;                     // e.g. "t0", "t1"
  ordinal: number;                    // 单调递增序号(分页游标锚点)
  state: 'queued' | 'running' | 'completed' | 'failed' | 'cancelled';
  origin: TurnOrigin;                 // 触发来源
  prompt?: string;                    // 原始输入文本
  attachmentIds?: AttachmentId[];     // 附件引用
  steps: TranscriptStep[];            // LLM 调用列表
  startedAt?: string;
  endedAt?: string;
  usage?: TranscriptUsage;            // 整轮 token 汇总
  durationMs?: number;
  error?: string;                     // 终端错误信息
}

type TurnOrigin =
  | { kind: 'user';       payload?: unknown }
  | { kind: 'cron';       taskId?: TaskId; payload?: unknown }
  | { kind: 'task';       taskId: TaskId;  payload?: unknown }
  | { kind: 'hook';       payload?: unknown }
  | { kind: 'compaction'; payload?: unknown }
  | { kind: 'side';       payload?: unknown }
  | { kind: 'other';      payload?: unknown };

TurnOrigin 记录"这轮对话是谁触发的":用户输入(user)、定时任务(cron)、后台任务通知(task)、Hook 回调(hook)、上下文压缩注入(compaction)、旁路消息(side)、未知来源(other)。每种 origin 都可以携带自己的 payload,供 L4 的渲染器解释。

7.2 Step --- 每次 LLM 调用

Typescript 复制代码
interface TranscriptStep {
  kind: 'step';
  stepId: StepId;                     // e.g. "t0.1", "t0.2"
  turnId: TurnId;
  ordinal: number;
  state: 'running' | 'completed' | 'interrupted' | 'failed';
  frames: TranscriptFrame[];          // 该步骤产出的帧
  startedAt?: string;
  endedAt?: string;
  usage?: StepUsage;                  // 该次 LLM 调用的 token 用量
  finishReason?: string;              // LLM 完成原因
  timing?: StepTiming;                // 延迟分解
  retry?: StepRetry;                  // 重试状态
  endReason?: string;                 // 中断原因
  endMessage?: string;
}

interface StepUsage {
  inputOther: number;
  output: number;
  inputCacheRead: number;
  inputCacheCreation: number;
}

interface StepTiming {
  llmFirstTokenLatencyMs?: number;
  llmStreamDurationMs?: number;
  llmRequestBuildMs?: number;
  llmServerFirstTokenMs?: number;
  llmServerDecodeMs?: number;
  llmClientConsumeMs?: number;
}

一个 Turn 可能包含多个 Step------例如 Agent 在第一轮 LLM 调用后被工具结果中断,然后进行第二轮调用(tool results → next LLM call)。每次 LLM 调用就是一个 Step。

7.3 Frame --- 工具调用/结果/进度

Typescript 复制代码
type TranscriptFrame = TextFrame | ThinkingFrame | ToolCallFrame | NoticeFrame;

interface TextFrame {
  kind: 'text';
  frameId: FrameId;
  role: 'assistant' | 'user';
  text: string;                       // L1 始终持有完整文本
  attachmentIds?: AttachmentId[];
  taskId?: TaskId;
}

interface ThinkingFrame {
  kind: 'thinking';
  frameId: FrameId;
  text: string;
}

interface ToolCallFrame {
  kind: 'tool';
  frameId: FrameId;
  toolCallId: string;
  name: string;                       // 引擎工具名:Read, Bash, Agent, ...
  view?: string;                      // 可选的视图提示
  state: 'running' | 'done' | 'error';
  input?: unknown;                    // 工具输入(不透明)
  output?: unknown;                   // 工具输出(不透明)
  display?: unknown;                  // 展示数据(不透明)
  error?: string;
  inputText?: string;                 // 原始输入文本
  progress?: ToolFrameProgress;       // 最新进度
  taskId?: TaskId;                    // 关联的后台执行实体
  approvalId?: InteractionId;         // 关联的审批交互
  todoId?: TodoId;                    // 关联的待办事项
  agentRefs?: AgentRef[];             // 生成的子 Agent 引用
}

Frame 是渲染的基本单位------UI 看到的最小的"有意义的展示单元"就是 Frame。

7.4 Interaction --- 用户交互

Typescript 复制代码
interface TranscriptInteraction {
  interactionId: InteractionId;
  interactionKind: 'approval' | 'question';
  toolCallId?: string;                // 锚定到具体工具调用
  state: 'pending' | 'approved' | 'rejected' | 'cancelled' | 'answered' | 'dismissed';
  request?: unknown;                  // 审批请求/问题内容(不透明)
  response?: unknown;                 // 审批结果/答案(不透明)
}

Interaction 是全局实体------不在 Step 的 Frame 序列中,而是在 interactions Map 中独立存储。这使得它可以跨 Step 存活:一个审批请求可能在当前 Step 完成后才被用户响应,但 Interaction 实体不随 Step 分页而丢失。

7.5 Attachment / Todo / Meta

Typescript 复制代码
// 附件(只存元数据,不传字节)
interface TranscriptAttachment {
  attachmentId: AttachmentId;
  mediaType: string;                  // e.g. 'image/png'
  name?: string;
  size?: number;
  source?: { kind: 'url', url: string }
         | { kind: 'file', fileId: string };
  placeholder?: string;
}

// 待办事项(全局最新状态,TodoList 工具调用每次更新)
interface TranscriptTodo {
  todoId: TodoId;
  items: readonly TodoItem[];         // [{ title, status }]
  updatedAt?: string;
}

// 元数据(Goal / Plan / Swarm / Agent 状态)
interface TranscriptMeta {
  goal?: GoalMeta;                    // 目标进度
  modes?: ModesMeta;                  // Plan/Swarm 模式标志
  activity?: 'idle' | 'turn' | 'disposing' | 'unknown';
  agent?: AgentStatusMeta;            // 模型/权限/阶段/token 用量
}

8. wire.jsonl 持久化

8.1 单一真相源

服务端在 <sessionDir>/agents/<agentId>/wire.jsonl 中持久化会话数据。这是一个 ​append-only JSONL 文件​:每行一个 JSON 事件,按时间顺序追加,永不修改已写入的行。

8.2 为什么选择 JSONL?

  • 追加即写:不需要重写整个文件,O(1) 写入
  • 故障恢复友好:即使进程崩溃,最后一行可能不完整,前面的行完全有效------不会损坏整个文件
  • 可读写 :调试时可以 tail -f wire.jsonljq 逐行分析(而二进制格式需要专门的工具)
  • 流式解析:从文件读取时不需要将整个文件加载到内存,可以按行流式解析

8.3 与 Transcript Store 的关系

wire.jsonl 是持久化备份,不是运行时数据源。运行时数据流为:

Typescript 复制代码
Engine Events
  → 产生 Ops
  → apply() 写入 AgentTranscript(内存)
  → 序列化 ops 写入 wire.jsonl(磁盘追加)
  → 通过 WS/REST 推送给客户端

wire.jsonl 的主要用途是​会话重启时重建 ​------服务端启动后读取 JSONL 文件,重建内存中的 AgentTranscript 状态,然后继续处理新的引擎事件。

9. 历史重建

9.1 重建流程概览

wire.jsonl 重建完整会话状态是一个​两阶段流水线​:

Typescript 复制代码
wire.jsonl
   │
   ├─ context.* 消息(LLM 上下文记录)
   │     ↓ groupMessagesIntoSnapshot()
   │     按消息分组为 Turn 树,重建 attachment 实体
   │
   ├─ 非 context.* 记录(goal/plan/swarm/task/interaction/todo)
   │     ↓ foldWireRecordFacts()
   │     将 task/interaction/todo/meta 折叠到 Turn 树的基底上
   │
   ▼
AgentTranscriptSnapshot (items + tasks + interactions + ...)

9.2 groupTurns:上下文消息分组为 Turn 树

groupMessagesIntoSnapshot() 接收引擎的扁平上下文消息列表,重建层级结构:

  • 角色识别role: 'user' 作为 Turn 边界,role: 'assistant' 作为当前 Turn 的新 Step,role: 'tool' 更新对应 tool frame 的结果
  • 隐藏起源过滤 :系统注入消息(injectionsystem_trigger)默认折叠,不产生 UI 可见的 Turn;但 goal_continuationsubagent 这类真实打开引擎 Turn 的系统触发器会保留 Turn 边界(保证 0-based ordinal 与引擎对齐)
  • 标记转换skill_activationplugin_command 的触发消息转为 timeline marker 而非独立 Turn;compaction_summary 转为 compaction marker

重建的限制(故意接受的不完整性):

  • context messages 中不携带 step usage / finishReason / timing 这些在线指标------只在会话运行时通过引擎事件获得
  • base64 图片数据被丢弃,不传输给客户端
  • turn durationMs / error 等数据只存在于在线路径

9.3 foldFacts:元数据折叠

foldWireRecordFacts() 处理非 context.* 的 wire record:

  • task.started / task.terminated → 构建 TranscriptTask 实体
  • interaction.request / interaction.resolved → 构建 TranscriptInteraction(pending 未解决的设 cancelled,崩溃安全)
  • tools.update_store → 构建 TranscriptTodo(最新状态,不保留历史版本)
  • goal.create / goal.update / goal.clear → 构建 GoalMeta
  • plan_mode.enter / plan_mode.exit / plan.revision → 构建 Plan 模式标志
  • swarm_mode.enter / swarm_mode.exit → 构建 Swarm 模式标志

9.4 Session 恢复流程

完整的恢复流程:

  1. 服务端启动,读取 wire.jsonl(逐行解析 JSON)
  2. 分离 context messages 和 fact records
  3. groupMessagesIntoSnapshot() → 基底 Turn 树
  4. foldWireRecordFacts() → 完整 AgentTranscriptSnapshot
  5. 通过 reset op apply 到 AgentTranscript
  6. 引擎从最后一个 Turn 之后继续处理新事件

10. 操作批处理序列合约

10.1 单调递增的 seq 值

每个 agent 的 op batch 有一个单调递增的 seq 值:

  • scope: per (session, agent),从 1 开始计数
  • 每个 batch(不是单个 op)分配一个 seq,因此 batch 之间的 seq 是连续的
  • transcript.reset 和 REST transcript 响应携带 watermark:seq 表示"此快照包含 seq <= N 的所有 batch"

10.2 since_seq catch-up 机制

Typescript 复制代码
// 客户端持有 watermark N,请求 seq > N 的所有 batch
GET /v1/sessions/{session_id}/transcript/ops?since_seq=N

// 服务端响应:
{
  batches: [ { seq: N+1, ops: [...] }, { seq: N+2, ops: [...] }, ... ],
  latest_seq: M,
  complete: true             // 服务端日记覆盖到了 N → 可以增量
}
// 或
{
  batches: [],
  latest_seq: M,
  complete: false            // 服务端日记已过期 → 客户端必须回退到全量刷新
}

10.3 WebSocket 实时推送 + REST 补全

双通道同步策略:

  • WebSocket 通道 :推送实时 ops(transcript.ops 事件),包含 seq 号。所有 transcript 事件标记 volatile: true------不会被 WS 的持久化日志记录,可靠性来自 Transcript 层自身的 op-batch sequence
  • REST 通道 :当客户端检测到 seq 跳跃(丢失了一些 batch)、append gap(offset 不连续),或服务端返回 complete: false(日记已轮转),客户端回退到 REST 获取完整快照

10.4 增量 vs 全量同步策略

Typescript 复制代码
新客户端连接:
  → WS subscribe(携带 transcript_since 游标)
  → 如果服务端日记覆盖 to since_seq → 增量 replay(transcript.ops batches)
  → 否则 → 全量 baseline(transcript.reset snapshot)

运行时 append gap:
  → 客户端校验 offset → gap 检测
  → since_seq catch-up → 获取缺失的 batch
  → 如果 catch-up 失败(complete: false)→ 全量刷新

重新订阅升级粒度:
  → needsResetOnTransition 判断
  → 升级时(turn→block, block→delta)→ 服务端发送 reset snapshot
  → 降级时 → 客户端只需丢弃多余的 ops,下一个 flush upsert 自动重新同步

总结

Transcript 不是简单的"日志系统",而是一个精心设计的​四层会话记录架构​。回顾其核心设计:

  • L1 Store :每个 Agent 持有独立的 copy-on-write 状态机,apply() 作为唯一写入路径保证客户端和服务端使用完全相同的收敛逻辑
  • L2 Ops :14 种操作中 13 种是幂等的 state-style upsert------重复发送、乱序到达、断线重放都不会产生不一致;唯一的 append 操作通过 offset 校验检测流中断
  • L3 Subscriptions:四级粒度(off/turn/block/delta)让不同 UI 组件按需消费,避免不必要的网络传输和内存开销
  • L4 Views:框架无关的渲染器注册表,让 CLI、Web、VS Code 各自注册自己的组件,共享同一份数据模型

数据持久化和恢复也是设计重点:

  • wire.jsonl 作为 append-only 单一真相源,崩溃时只损毁最后一行
  • groupTurns + foldFacts 两阶段恢复流程可以从 JSONL 完整重建会话状态
  • seq-based 的 catch-up 机制让 WebSocket + REST 双通道同步既实时又可靠

Transcript 的设计展示了架构分层的真正价值:每一层只解决自己层级的问题,通过清晰的接口与上下层交互。L1 不知道数据会怎么传输(那是 L2 的事),L2 不知道谁会订阅(那是 L3 的事),L3 不知道数据怎么渲染(那是 L4 的事),L4 甚至不知道"组件"是什么------它只知道有一个泛型参数 C。

在下一篇中,我们将探讨 ​Protocol & RPC​------kimi-code 的进程间通信协议层,了解 WebSocket 事件系统、REST API 设计和双向流管理。

相关推荐
hust_wangyajun4 小时前
我用 Agent Reach 生成了 Claude Code 年度生态报告:一篇实战演练
ai·agent·claude code
凡泰AI5 小时前
金融机构如何选择自己的企业级 AI桌面终端?
人工智能·agent·企业级ai·企业级agent
zandy10115 小时前
8款主流编程软件,五个维度深度解析——2026年AI编程工具技术选型
agent·ai编程
神奇霸王龙5 小时前
MCP 微软教材背书:5 国产基座 Agent 承接力实测
microsoft·ai·ai作画·agent·ai编程·ai写作·mcp
武子康6 小时前
前台语音与后台任务为什么要做双循环:从委派到结果新鲜度
人工智能·chatgpt·agent
海兰7 小时前
【记忆】openclaw之Honcho 记忆系统
人工智能·agent·openclaw
HIT_Weston7 小时前
166、【Agent】【OpenCode】TuiThreadCmd(流式传输&二进制Blob)
人工智能·agent·opencode
用户847181054197 小时前
LangChain中间件教程及DeepAgents应用
javascript·agent
修远客8 小时前
感知模块:Agent的眼睛和耳朵 — 三层降级策略让Agent永不"失明"
python·agent