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:
mapSessionEventToRuntimeEventinpackages/runtime/src/session-event-runtime-mapper.ts— the core transformation functioncreateDirectRuntimeTurnLedgerinscripts/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:
- Validates that the event is a genuine backend event (not a host projection)
- Excludes legacy permission events that lack proper runtime representation
- Initializes a fresh
SessionEventMapMemoryfor 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
partialflag 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→ partialmodelevent withpartial: truetext_complete→ finishedmodelevent withpartial: 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→systemevent withsandboxBoundaryRequeststate delta (lines 97–104)- Sandbox decision events →
systemevent withsandboxBoundaryDecisionstate 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_resultevents - 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.tsand provides deterministic, pure transformation of backend events mapSessionEventToRuntimeEventvalidates, enriches, and dispatches events throughmapBackendSessionEventwith type-specific handlersresolveBasenormalizes cross-cutting identity and temporal fields across all event types- Tool correlation uses
SessionEventMapMemoryto linktool_startandtool_resultevents without backend support for round-tripping tool names - Per-turn orchestration via
createDirectRuntimeTurnLedgerinscripts/computer-use/direct-runtime-ledger.mjshandles 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →