How Maka Maps SessionEvents to RuntimeEvents: A Deep Dive into the Transformation Layer

Maka converts low-level SessionEvent objects from the AI SDK backend into canonical RuntimeEvent ledgers through a deterministic mapping layer in packages/runtime/src/session-event-runtime-mapper.ts.

The Apache Maka project implements a clean separation between backend session events and the runtime event stream consumed by its kernel. This article explains the exact mechanism that transforms raw backend events into structured runtime events, including the specific functions, file paths, and architectural decisions that make this mapping reliable and replayable.

Overview of the Session-to-Runtime Event Pipeline

Maka's event translation serves a critical architectural purpose: it normalizes heterogeneous backend event formats into a single, versioned RuntimeEvent schema that the runtime kernel can process uniformly. The mapping is pure and deterministic—given the same session event and context, it always produces the same runtime event.

The pipeline has two primary components:

  • mapSessionEventToRuntimeEvent in packages/runtime/src/session-event-runtime-mapper.ts — the core transformation function
  • createDirectRuntimeTurnLedger in scripts/computer-use/direct-runtime-ledger.mjs — the per-turn ledger wrapper that orchestrates multiple mappings

Entry Point: Mapping a Single Session Event

The transformation begins with mapSessionEventToRuntimeEvent(event, ctx, memory) at lines 30–34 of session-event-runtime-mapper.ts. This function performs three essential checks before proceeding:

  1. Validates that the event is a genuine backend event (not a host projection)
  2. Excludes legacy permission events that lack proper runtime representation
  3. Initializes a fresh SessionEventMapMemory for temporary state storage
import {
  mapSessionEventToRuntimeEvent,
  createSessionEventMapMemory,
} from '@maka/runtime';

// Context shared by all events of a turn
const ctx = {
  sessionId: 'sess-1',
  invocationId: 'sess-1-invocation',
  runId: 'sess-1-run',
  turnId: 'turn-1',
  now: () => Date.now(),
};

// Memory to keep tool-name linkage across events
const memory = createSessionEventMapMemory();

// Map a backend event to runtime format
const runtimeEvent = mapSessionEventToRuntimeEvent(toolStart, ctx, memory);

The ctx parameter carries invocation-wide identifiers that ensure every RuntimeEvent can be traced back to its originating session, run, and turn.

Building the Common Event Skeleton

Before type-specific conversion, resolveBase(event, ctx) (lines 89–102) constructs the foundational structure shared by all runtime events. This function populates:

  • Identity fields: id, invocationId, runId, sessionId, turnId
  • Temporal data: timestamps via ctx.now()
  • Streaming state: the partial flag indicating whether more content follows
  • Branch metadata: optional branch information for conversation forking

This base construction ensures cross-cutting concerns are handled once, rather than duplicated across every event-type handler.

Event-Type Specific Conversion with mapBackendSessionEvent

The core logic resides in mapBackendSessionEvent, which uses a switch statement to dispatch each SessionEvent variant to its appropriate handler. Every handler produces a RuntimeEvent with four key properties:

Property Purpose Example Values
role Conversation participant type model, tool, system, user
author Source of the event agent, tool, system, user
content Typed payload text, thinking, function_call, function_response, error
refs Cross-reference identifiers toolCallId, operationId, providerEventId
actions/stateDelta Side effects and state changes sandbox requests, token usage, plan submissions

Model Text Events: text_delta and text_complete

Streaming text from the backend generates partial and complete events:

  • text_delta → partial model event with partial: true
  • text_complete → finished model event with partial: false

Both map to content.kind: 'text' with the accumulated or incremental text payload (lines 95–100).

Tool Call Lifecycle: tool_start and tool_result

Tool execution spans two events that must be correlated:

// Tool start: record name in memory, create function_call event
{
  type: 'tool_start',
  id: 'event-123',
  ts: 1_712_345_678,
  toolUseId: 'use-42',
  toolName: 'search',
  args: { query: 'Maka architecture' },
}
// → RuntimeEvent with role: 'model', author: 'agent', content.kind: 'function_call'

// Tool result: look up name from memory, create function_response event
{
  type: 'tool_result',
  id: 'event-124',
  ts: 1_712_345_679,
  toolUseId: 'use-42',  // Links back to tool_start
  content: { result: 'Maka is an AI agent runtime...' },
}
// → RuntimeEvent with role: 'tool', author: 'tool', content.kind: 'function_response'

The toolNameByUseId map in SessionEventMapMemory preserves the tool name across this asynchronous boundary since tool_result events do not carry the original tool name.

Sandbox Boundary Events

Security-sensitive sandbox operations become system-role events with specialized state deltas:

  • sandbox_boundary_request → system event with sandboxBoundaryRequest state delta (lines 97–104)
  • Sandbox decision events → system event with sandboxBoundaryDecision state delta

These events enable the runtime to track and audit all sandbox escape requests without mixing them into the conversational stream.

User Steering Messages

steering_message events transform into user-role text events marked with steering: true (lines 84–90). This marker allows the UI to distinguish organic user messages from steering interventions while maintaining a consistent event structure.

Completion Events and Final Status

The complete event triggers completeRuntimeEvent (lines 60–68 and 115–120), which determines the final status field:

// Status determination logic in completeRuntimeEvent
if (stopReason === 'completed') → status: 'completed'
if (stopReason === 'aborted') → status: 'aborted'
if (previousError) → status: 'failed'  // Overrides other statuses

This post-processing ensures the runtime has a single, authoritative signal for turn completion regardless of how the backend signaled termination.

State Management with SessionEventMapMemory

The SessionEventMapMemory interface (lines 73–78) solves a specific correlation problem:

interface SessionEventMapMemory {
  toolNameByUseId: Map<string, string>;  // toolUseId → toolName
  failureToReport?: unknown;             // Captured error for status override
}

This mutable state is scoped to a single mapping operation and discarded afterward. It enables:

  • Tool name recovery when processing tool_result events
  • Error propagation for status determination in completion handling

Per-Turn Ledger Orchestration

For practical use, createDirectRuntimeTurnLedger in scripts/computer-use/direct-runtime-ledger.mjs (lines 25–68) wraps the mapper in a Ledger interface:

import { createDirectRuntimeTurnLedger } from '../../scripts/computer-use/direct-runtime-ledger.mjs';

const ledger = createDirectRuntimeTurnLedger({
  sessionId: 'sess-1',
  turnId: 'turn-1',
  text: '',
  newId: () => crypto.randomUUID(),
  now: () => Date.now(),
});

// Record events—each triggers mapping and conditional storage
ledger.record(toolStart);
ledger.record(toolResultEvent);

// Retrieve final event list
const events = await ledger.loadTurnRuntimeEvents('turn-1');

The ledger filters transient events: partial events and error-only events are mapped but not stored, ensuring the persisted ledger contains only meaningful, complete runtime events.

Testing and Verification

The mapping logic is exercised in packages/runtime/src/__tests__/session-event-runtime-mapper.test.ts, which covers:

  • All event type variants
  • Partial vs. complete streaming states
  • Tool call correlation across start/result pairs
  • Sandbox boundary event handling
  • Error propagation and status override

These tests guarantee that backend SDK changes do not silently break the runtime contract.

Summary

  • Maka's session-to-runtime event mapping lives in packages/runtime/src/session-event-runtime-mapper.ts and provides deterministic, pure transformation of backend events
  • mapSessionEventToRuntimeEvent validates, enriches, and dispatches events through mapBackendSessionEvent with type-specific handlers
  • resolveBase normalizes cross-cutting identity and temporal fields across all event types
  • Tool correlation uses SessionEventMapMemory to link tool_start and tool_result events without backend support for round-tripping tool names
  • Per-turn orchestration via createDirectRuntimeTurnLedger in scripts/computer-use/direct-runtime-ledger.mjs handles multiple events with filtering for persistence

Frequently Asked Questions

What is the difference between SessionEvent and RuntimeEvent in Maka?

SessionEvent represents raw backend output from the AI SDK—tightly coupled to specific provider formats and streaming patterns. RuntimeEvent is Maka's canonical, versioned ledger format that the runtime kernel consumes. The mapping layer decouples the system from backend changes while providing consistent identifiers, roles, and content structures for downstream processing.

Why does Maka need a memory object for mapping events?

The SessionEventMapMemory object solves a protocol limitation: tool_result events do not include the original tool name, only a toolUseId. Maka stores the name in memory during tool_start processing so the subsequent tool_result can produce a complete function_response event with both id and name populated. This state is strictly temporary and scoped to a single turn's event sequence.

How does Maka handle incomplete or streaming events from the backend?

Streaming text generates text_delta SessionEvents that map to RuntimeEvent objects with partial: true. The runtime can render these incrementally. Final text_complete events clear the partial flag. The ledger in direct-runtime-ledger.mjs filters partial events from persistent storage, ensuring only complete, meaningful events are retained for replay or audit purposes.

Where does the mapping actually get invoked in production code?

Production code typically calls createDirectRuntimeTurnLedger from scripts/computer-use/direct-runtime-ledger.mjs, which returns a ledger object with record() and loadTurnRuntimeEvents() methods. Each record() call invokes mapSessionEventToRuntimeEvent with the appropriate context and memory, collecting results for the turn. This indirection provides a stable Ledger interface while allowing the mapper implementation to evolve independently.

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 →