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() instantiates AgentTranscript only 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 (#state private 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 new AgentState with structural sharing
  • Idempotent ops — turn.upsert, step.upsert, frame.upsert produce identical results on replay
  • Non-idempotent append — may return gap when 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 AgentState as opaque — use only apply() for modifications
  • Structural sharing in applyOperation minimizes 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:

  1. Halt further append operations for that frame
  2. Request reset operation from authoritative source (server or persisted log)
  3. 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 creates AgentTranscript instances and manages agent roster subscriptions via onRosterChange()
  • AgentTranscript — immutable per-agent store with apply() for operations, snapshot() for reads, and listPendingInteractions() for async workflows
  • applyOperation — pure reducer in packages/transcript/src/ops/apply.ts that processes all state changes with copy-on-write semantics
  • append — the only non-idempotent operation; requires gap detection and resynchronization protocol
  • Pagination & Views — paginate helper and registerView extensibility 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →