transcript 四层模型
L1 存储 / L2 幂等操作 / L3 订阅粒度 / L4 视图注册,序列号契约,以及从 wire.jsonl 冷重建的两级折叠。
packages/transcript(约 3696 行)是转录渲染数据层。包描述:
Isomorphic transcript rendering data layer: agent-granular L1 store, idempotent L2 operations, granularity-gated L3 subscriptions, framework-free L4 view registry.
根 AGENTS.md 对它的定位没变:纯 TypeScript、浏览器安全、不 import 引擎,并且是 transcript 契约类型(src/contract/)与 op 批次序列号契约的唯一所有者。运行时依赖只有 zod。它和 agent-core-v2、kap-server 一样属于「零注释区」,源码里的注释已全部清掉。
为什么要独立成包
同一份对话历史,服务端要投影、浏览器要渲染:
- 浏览器端不能 import 引擎(Node 依赖、体积)。
- 两边若各写一套数据模型与协议类型,必然分叉。
所以把「转录的数据模型 + 归约逻辑 + 线协议 schema」抽成纯 TS 包。仓库里的运行时消费者是 kap-server;Web UI 已移到另一个仓库,以预构建产物 apps/kimi-code/dist-web 随 CLI 发布,产物里能搜到 subscribe_v2、transcript_since、transcript.ops 这些协议字面量。apps/kimi-inspect 则已改用 kap-server 的 message protocol v3,不再依赖本包(见 kap-server)。
四层
packages/transcript/src/
├── model/ L1 数据模型
├── store/ L1 存储(agentTranscript / transcriptStore)
├── ops/ L2 幂等操作(operation / apply)
├── granularity/ L3 订阅粒度(grade / filterOps)
├── view/ L4 视图注册(registry)
├── pagination/ 分页
├── history/ 冷重建(groupTurns / foldFacts)
└── contract/ 协议契约(schema / events / mediaRef / origin)L1:存储
model/ 下的实体:ids、turn、frame、interaction、attachment、todo、item、task、meta、prompt。
agent 粒度——主代理与各子代理各有一条转录流,store/agentTranscript.ts 管单个,store/transcriptStore.ts 管全体(按需 ensureAgent,并广播 agent 名册变化)。
L2:幂等操作
export { EMPTY_AGENT_STATE, applyOperation, appendAtOffset } from './ops/apply';
export type { AgentState, ApplyResult } from './ops/apply';幂等是关键:WS 可能重连、重放、乱序,同一个 op 应用两次结果必须不变。AgentState 不可变,applyOperation 是纯归约函数——标准的 event sourcing 形态。
L3:订阅粒度
四档 TranscriptGrade:off / turn / block / delta。granularity/filterOps.ts 的判定很短:
| 档 | 放行什么 |
|---|---|
off | 什么都不发 |
turn | 除下面两档专属 op 以外的全部 op;快照里 turn 的 steps 置空 |
block | 再加 step.upsert / frame.upsert(整状态帧) |
delta | 再加逐 token 的 append |
同一份数据,不同消费者按需订阅:只关心结构的订 turn,要完整帧但不要逐 token 动画的订 block,打字机效果才需要 delta。gradeFor() 支持按 agent 指定、以 '*' 兜底;needsResetOnTransition() 规定升档必须重发基线——低档时没发的帧,增量补不回来。
L4:视图注册
view/registry.ts 的 ViewRegistry<C> 不依赖任何 UI 框架,只按键分派:工具帧按 frame.view ?? frame.name(小写),输入按 origin.kind,时间线标记按 marker 名,另有 fallbackTool 兜底。C 是宿主框架自己的组件类型。
序列号契约
contract/schema.ts 第 434 行:
export const transcriptSeqSchema = z.number().int().nonnegative();- 每个 (session, agent) 有单调递增的批次
seq。 - 订阅帧
subscribe_v2的载荷里,transcript_since是按 agent 的续传游标。 - 追赶响应
transcriptOpsCatchupResponseSchema:batches(每批seq+ops)、latest_seq、complete。
订阅游标 transcript_since 和 REST 转录响应里的 seq 都是 .optional()——不认识 seq 的老对端照样能用,只是没有增量追赶,只能全量刷新。服务端 GET /sessions/{session_id}/transcript/ops?since_seq= 做点对点追赶;complete: false 表示会话不在内存或 journal 已回溯不到,调用方必须回退全量刷新。
冷重建:两级折叠
会话不在内存里(冷会话)时,要从每个 agent 的 wire.jsonl 重建转录。kap-server 的 services/transcript/transcriptService.ts 读出记录后分两步:
wire.jsonl(唯一真相)
├── groupMessagesIntoSnapshot() history/groupTurns.ts:上下文消息 → 回合树
└── foldWireRecordFacts() history/foldFacts.ts:非上下文记录 → 任务 / 交互 / 待办 / 标记……文件名没变,两个入口的函数名比 v0.29.1 时更直白。foldFacts.ts 第 579 行起:
for (const [id, entity] of interactions) {
if (entity.state === 'pending') {
interactions.set(id, { ...entity, state: 'cancelled' });
}
}关机时仍 pending 的交互,折叠成 cancelled——否则重建出来会有永远转圈的「等待审批」。折叠也处理 context.undo 记录:被撤销的回合在冷重建里同样隐藏(v2 撤销时会顺带写一条兼容的 context.undo,见记录与回放)。
旧版根 AGENTS.md 曾注明「实时投影才有的字段冷重建不回填」,这句已从现行文档删去。对照代码:model/ 里 step 的 usage / finishReason / timing / retry、task 的 resultSummary 仍在,history/ 下却找不到它们的回填逻辑——这些字段依旧只来自实时投影。
计划的引用式存储
Plan 章节提到的机制换了落点。引擎侧 features/plan/planService.ts 的 recordRevision():
const scope = this.agentCtx.scope();
const key = `plan/${id}/v${version}.md`;
await this.blobs.put(scope, key, bytes);文档写进 agent 作用域的 blob 存储,plan.revision 记录只带 {agentId, id, version, key, sha256, bytes}。foldFacts.ts 优先用 key(可由调用方传入的 resolvePlanRevisionKey 解析成路径),老记录里的 path 仍兼容。大文档卸载,记录里只存引用 + 校验和——事件流保持轻量,同时可校验完整性。
与 kap-server 的分工
transcript 是纯数据层,kap-server 负责:
- 引擎事件 → transcript ops(
services/transcript/coreEventMap.ts、coreBinding.ts) - REST 路由:
/sessions/{session_id}/transcript及其/ops、/user-messages、/plan - 批次 seq 分配与有界 journal(
TRANSCRIPT_OPS_JOURNAL_CAPACITY = 2000) - 事件抑制:订阅了 transcript 的连接,不再重复收到已被投影的旧式
session_event(suppressedByTranscript())
下一篇:记录与回放。