How to Use the Transcript Package in Kimi-Code: A Complete Guide to Session State Management
The @moonshot-ai/transcript package provides an isomorphic, immutable data layer for storing every turn, step, and frame in a Kimi session through TranscriptStore and AgentTranscript with pure reducer-based state updates.
This guide covers the core architecture, practical usage patterns, and implementation details of the transcript package in the MoonshotAI/kimi-code repository. Whether you're building server-side agents or browser-based inspection UIs, this engine-agnostic package handles session state without runtime dependencies.
Core Architecture of the Transcript Package
The transcript package organizes session data into four composable layers. Understanding these layers is essential for effective integration.
TranscriptStore: Session-Level Container
Located in packages/transcript/src/store/transcriptStore.ts, TranscriptStore serves as the root container for a Kimi session:
import { TranscriptStore } from '@moonshot-ai/transcript';
const store = new TranscriptStore('session-123');
const mainTx = store.ensureAgent('main', { agentId: 'main', type: 'main' });
Key capabilities:
- Lazy creation —
ensureAgent()instantiatesAgentTranscriptonly when first accessed - Roster management —
agents()returns current agent descriptors;onRosterChange()streams updates to clients - Fan-out support — server implementations use roster listeners to broadcast agent lifecycle events
AgentTranscript: Per-Agent Immutable Store
The L1 store in packages/transcript/src/store/agentTranscript.ts maintains a single agent's complete history:
export class AgentTranscript {
apply(ops: readonly TranscriptOperation[]): AppliedOps;
snapshot(window?: { tailTurns: number }): AgentTranscriptSnapshot;
listPendingInteractions(): readonly InteractionId[];
getItems(): readonly TranscriptItem[];
getTurn(turnId: string): TranscriptTurn | undefined;
}
Critical properties:
- Holds one immutable
AgentState(#stateprivate field) - Copy-on-write semantics —
apply()processes operation batches atomically - Self-consistent snapshots — earlier snapshots remain valid regardless of subsequent operations
applyOperation: The Pure Reducer
All state mutations flow through packages/transcript/src/ops/apply.ts:
export function applyOperation(state: AgentState, op: TranscriptOperation): ApplyResult {
switch (op.op) {
case 'reset': return applyReset(state, op);
case 'turn.upsert': return applyTurnUpsert(state, op.turn);
case 'step.upsert': return applyStepUpsert(state, op.turnId, op.step);
case 'frame.upsert': return applyFrameUpsert(state, op);
case 'append': return applyAppend(state, op);
// … additional operations
}
}
Design characteristics:
- Stateless function — never mutates input
state; returns newAgentStatewith structural sharing - Idempotent ops —
turn.upsert,step.upsert,frame.upsertproduce identical results on replay - Non-idempotent
append— may returngapwhen offset exceeds local buffer, requiring resynchronization
Working with Transcript Operations
Creating Turn, Step, and Frame Hierarchies
The transcript package models conversation as nested entities: Turn → Step → Frame:
mainTx.apply([
// Create a turn representing user input
{
op: 'turn.upsert',
turn: {
kind: 'turn',
turnId: 't1',
ordinal: 1,
state: 'running',
origin: { kind: 'user' }
}
},
// Add a processing step within the turn
{
op: 'step.upsert',
turnId: 't1',
step: {
kind: 'step',
stepId: 't1.1',
turnId: 't1',
ordinal: 1,
state: 'running'
}
},
// Attach a text frame to capture assistant output
{
op: 'frame.upsert',
turnId: 't1',
stepId: 't1.1',
frame: {
kind: 'text',
frameId: 't1.1.f1',
role: 'assistant',
text: ''
}
},
]);
Operation types are defined in packages/transcript/src/ops/operation.ts.
Streaming Text with Append Operations
Real-time token streaming uses the append operation with offset tracking:
const result = mainTx.apply([
{
op: 'append',
target: {
type: 'frame',
turnId: 't1',
stepId: 't1.1',
frameId: 't1.1.f1'
},
offset: 0,
text: 'First chunk'
},
]);
// Later, append at the correct offset
const result2 = mainTx.apply([
{
op: 'append',
target: { type: 'frame', turnId: 't1', stepId: 't1.1', frameId: 't1.1.f1' },
offset: 11, // length of "First chunk"
text: ' second chunk'
},
]);
// Handle synchronization gaps
if (result2.gap) {
console.error('Gap detected:', result2.gap); // { expected: 11, got: <offset> }
// Request fresh snapshot via 'reset' operation
}
Using appendAtOffset for Client-Side Buffering
For web clients managing local buffers, the transcript package exports appendAtOffset:
import { appendAtOffset } from '@moonshot-ai/transcript';
// Successful append
const ok = appendAtOffset('hello', 5, ' world');
// => { text: 'hello world', changed: true }
// Gap condition (client ahead of server)
const gap = appendAtOffset('hello', 10, '!');
// => { text: 'hello', changed: false, gap: { expected: 5, got: 10 } }
This utility in packages/transcript/src/ops/apply.ts mirrors the web client's "alignDelta" semantics without requiring full store instantiation.
Reading State and Managing Snapshots
Accessing Current Data
// Full conversation history (ordered by ordinal)
const items: readonly TranscriptItem[] = mainTx.getItems();
// Direct turn lookup
const turn = mainTx.getTurn('t1');
const step = turn?.steps.find(s => s.stepId === 't1.1');
const frame = step?.frames[0];
// Auxiliary entities
const task = mainTx.getTask('task-42');
const prompt = mainTx.getPrompt('prompt-7');
const metadata = mainTx.getMeta();
Windowed Snapshots for Pagination
Large transcripts require memory-efficient access patterns:
// Full snapshot (all turns)
const full = mainTx.snapshot();
// Partial snapshot — newest 50 turns only
const recent = mainTx.snapshot({ tailTurns: 50 });
if (recent.hasMoreOlder) {
// UI should offer "load more" action
}
The paginate helper in packages/transcript/src/pagination/paginate.ts provides cursor-based iteration for streaming implementations.
Handling Pending Interactions
The transcript package tracks asynchronous user interactions (approvals, clarifications, etc.):
// Submit interaction awaiting user response
mainTx.apply([
{
op: 'interaction.upsert',
interaction: {
interactionId: 'ia-1',
interactionKind: 'approval',
toolCallId: 'call-99',
state: 'pending'
}
},
]);
// Check pending queue
console.log(mainTx.listPendingInteractions()); // ['ia-1']
// Record resolution
mainTx.apply([
{
op: 'interaction.upsert',
interaction: {
interactionId: 'ia-1',
interactionKind: 'approval',
toolCallId: 'call-99',
state: 'approved' // or 'rejected'
}
},
]);
console.log(mainTx.listPendingInteractions()); // []
Interaction state management enables reliable human-in-the-loop workflows without blocking the main event loop.
Extending with Custom Views
For custom UI implementations, register frame renderers:
import { registerView } from '@moonshot-ai/transcript';
// Register component for 'mermaid' diagram frames
registerView('mermaid', (frame) => {
return <MermaidRenderer diagram={frame.diagram} />;
});
// Register handler for custom tool output
registerView('custom-tool', (frame, context) => {
return <ToolOutputView data={frame.output} agent={context.agentId} />;
});
The registry in packages/transcript/src/view/registry.ts decouples frame type definitions from presentation logic.
Critical Implementation Details
Immutability Guarantees
- Never mutate arrays from
getItems(),getTurn(), or snapshot objects - Always treat
AgentStateas opaque — use onlyapply()for modifications - Structural sharing in
applyOperationminimizes memory overhead for large sessions
Idempotency and Causal Ordering
| Operation | Idempotent | Notes |
|---|---|---|
reset |
Yes | Replaces entire state |
turn.upsert |
Yes | Keyed by turnId |
step.upsert |
Yes | Keyed by stepId |
frame.upsert |
Yes | Keyed by frameId |
append |
No | Offset-sensitive; may produce gaps |
Replay safety enables optimistic updates and network partition recovery.
Gap Detection Protocol
When apply() returns gap: true:
- Halt further
appendoperations for that frame - Request
resetoperation from authoritative source (server or persisted log) - Re-apply pending local operations against new baseline
This protocol ensures eventual consistency without global locks.
Complete Working Example
import { TranscriptStore, appendAtOffset } from '@moonshot-ai/transcript';
async function demonstrateTranscriptPackage() {
// Initialize session store
const store = new TranscriptStore('demo-session');
// Subscribe to agent roster changes
const rosterSub = store.onRosterChange((agents) => {
console.log('Active agents:', agents.map(a => a.agentId));
});
// Create main agent transcript
const tx = store.ensureAgent('main', {
agentId: 'main',
type: 'main',
name: 'Assistant'
});
// Build conversation structure
tx.apply([
{
op: 'turn.upsert',
turn: {
kind: 'turn',
turnId: 't-001',
ordinal: 1,
state: 'completed',
origin: { kind: 'user' }
}
},
{
op: 'step.upsert',
turnId: 't-001',
step: {
kind: 'step',
stepId: 't-001.s1',
turnId: 't-001',
ordinal: 1,
state: 'completed'
}
},
{
op: 'frame.upsert',
turnId: 't-001',
stepId: 't-001.s1',
frame: {
kind: 'text',
frameId: 't-001.s1.f1',
role: 'assistant',
text: 'I understand. Let me help with that.'
}
},
]);
// Simulate streaming response
let offset = 0;
const chunks = ['Analyzing', ' your', ' request...'];
for (const chunk of chunks) {
const result = tx.apply([{
op: 'append',
target: { type: 'frame', turnId: 't-001', stepId: 't-001.s1', frameId: 't-001.s1.f1' },
offset,
text: chunk
}]);
if (result.gap) {
console.error('Synchronization gap detected');
break;
}
offset += chunk.length;
}
// Read and verify final state
const turn = tx.getTurn('t-001');
const finalText = turn?.steps[0]?.frames[0];
console.log('Final frame:', finalText);
// Cleanup
rosterSub.dispose();
}
demonstrateTranscriptPackage();
Expected output:
Active agents: ['main']
Final frame: { kind: 'text', frameId: 't-001.s1.f1', role: 'assistant', text: 'I understand. Let me help with that.Analyzing your request...' }
Summary
TranscriptStore— session root that lazily createsAgentTranscriptinstances and manages agent roster subscriptions viaonRosterChange()AgentTranscript— immutable per-agent store withapply()for operations,snapshot()for reads, andlistPendingInteractions()for async workflowsapplyOperation— pure reducer inpackages/transcript/src/ops/apply.tsthat processes all state changes with copy-on-write semanticsappend— the only non-idempotent operation; requires gap detection and resynchronization protocol- Pagination & Views —
paginatehelper andregisterViewextensibility for production UIs
Frequently Asked Questions
What makes the transcript package "isomorphic"?
The transcript package contains zero runtime dependencies on browser APIs or Node.js-specific modules. It operates purely on in-memory data structures, allowing identical code to run in Kap-Server (Node.js), browser inspection UIs, and test environments. All I/O concerns (networking, persistence) are handled by external callers through the operation-based API.
How does the transcript package handle concurrent edits?
Concurrency is managed through optimistic updates with conflict detection. The append operation includes an offset parameter — if the local state has diverged from the authoritative source, apply() returns a gap object containing { expected: <length>, got: <offset> }. The caller must then request a reset operation to obtain fresh state before continuing.
What's the difference between apply() and snapshot()?
apply() modifies state by applying one or more TranscriptOperation objects, returning AppliedOps with any gap signals. snapshot() reads state without modification, returning an immutable AgentTranscriptSnapshot suitable for UI rendering or persistence. For windowed access, pass { tailTurns: N } to snapshot only recent history.
Where are the model types (TurnHeader, StepHeader, etc.) defined?
All domain types live in packages/transcript/src/model/: turn.ts defines TurnHeader and StepHeader; frame.ts contains TranscriptFrame and variants; ids.ts provides branded identifier types. These are re-exported from packages/transcript/src/index.ts for convenient importing.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →