doc_id: architecture.runtime-core title: “Chapter 1: Log Is the Runtime—Replaying an Agent's State Space” language: en source_language: zh-CN counterpart: ./runtime-core-architecture-draft.zh-CN.md implementation_status: current document_status: draft translation_status: synced last_verified: 2026-07-12 owners:
This chapter answers one question: how does Maka preserve the state space an agent execution actually traversed, then recover it in the next turn, after a process restart, or through a new projection? The answer is the Runtime Event Log. The model loop produces facts; the Event Log preserves them; Sessions, Runs, the UI, model context, and recovery are consumers or projections of that log.
This chapter is for engineers entering the Maka Runtime for the first time and maintainers changing its main execution path. The first half should give you a working map of the runtime boundaries. By the end, you should be able to locate the main implementation and understand the invariants that changes to termination, tools, or persistence must preserve.
The chapter describes the implementation on the production path as verified on 2026-07-12. Phase plans in historical design documents are not treated as current behavior.
Suppose a user asks Maka:
Find the failing tests in this project, fix the problem, and run the tests again.
In a chat application, the path might be only three steps: send text to a model, wait, and display the reply. An agent execution looks more like this:
Meanwhile, the UI needs live text and tool activity. The next model turn needs trustworthy history. After a process crash, the session must not remain “running” forever. After the user presses Stop, a late provider event must not rewrite the result to “completed.”
The Runtime therefore solves a broader problem than calling an LLM API:
It records an open-ended loop containing streams, tool side effects, user intervention, and process failure as a fact history that can be interpreted and replayed again.
The central design decision in the Maka Runtime is not its choice of model SDK or the number of layers in its execution path. It is this:
The Runtime Event Log is the semantic source of truth for agent interaction. System state at a point in time is a projection over that ordered log.
The relationship can be written simply:
State(t) = Project(RuntimeEvents[0..t], policy, runtime configuration)
Different consumers can interpret the same log into different forms of state:
Read this diagram from the center. The Runtime Event Log contains stable facts; every other node is a derived view that may evolve, be rebuilt, or be replaced. The event-production path is intentionally omitted and introduced later.
This differs fundamentally from an application log. An application log usually describes what the code did after business logic ran and primarily helps humans diagnose it. A RuntimeEvent is itself part of the business semantics. User messages, model responses, thinking, function calls, function responses, permission actions, usage, and terminal status enter the log as strongly typed facts. Remove the log, and the system loses the reliable basis for reconstructing interaction state.
RuntimeEvent is more than role + text. It separates a fact into orthogonal dimensions:
| Dimension | Key fields | Meaning |
|---|---|---|
| Identity | sessionId, invocationId, runId, turnId, branch | Which conversation, call, execution attempt, and branch owns the fact |
| Ordering | id, ts, and ledger order | The fact's position in causal history |
| Source | role, author | Its lane in model history and the subsystem that produced it |
| Content | text, thinking, function call/response, error | The semantic content of the AI interaction |
| Actions | state delta, permission, artifact, usage, end invocation | The control-state change or side effect the Runtime must record |
| Correlation | tool call, provider event, step, and artifact refs | How the same operation is paired across subsystems |
| Lifecycle | partial, status | Whether it is a replaceable stream fragment, a durable fact, or a terminal fact |
This preserves the original semantics of model interaction rather than a UI-formatted transcript. In particular:
Model history therefore does not need to reverse-engineer the UI transcript. It can select non-partial, model-visible RuntimeEvents, preserve their order, and materialize text-only or provider-native messages according to provider capabilities. The UI is likewise a projection rather than the source of truth.
Replay has three levels that should be distinguished precisely.
Given a RuntimeEvent ledger, Maka can reconstruct user and model text, thinking, tool calls and results, permission actions, usage, and terminal facts. The next model history and the completed Session read model already prefer this ledger.
This lets the system answer: before a given event boundary, which interactions had the model seen? Which tools had it called? What had they returned? Which permissions had been requested or decided? Had the Invocation ended?
Providers impose different requirements on tool history and signed thinking. Maka does not blindly feed every event back to every model. It first builds a replay plan that checks partials, tool call/result pairing, step IDs, thinking signatures, and provider support, then chooses provider-native replay, text-only replay, or an explicit degradation path.
Replay does not mean “send the JSONL unchanged to any model.” It means preserving facts rich enough for a projector to produce valid history for a particular provider while reporting any semantic loss.
RuntimeEvents preserve canonical message semantics for AI interaction, but they are not a full byte-level snapshot of every provider HTTP request. The system prompt, tool schemas, provider options, model implementation version, and context-selection or compaction policy still participate in the final request. The current system records some identities, diagnostics, and hashes for these inputs, but it does not copy the entire wire request into the RuntimeEvent ledger.
“State-space replay” in this chapter therefore means reconstructing interaction semantics and Runtime state first. A future promise of bit-exact deterministic replay would also require versioning or snapshotting runtime configuration, prompts, the tool catalog, projection policy, and provider request shape. This does not weaken the Event Log; it clarifies why the log is the correct foundation. Message facts remain stable while request materialization can evolve independently.
The design has two explicit conceptual roots.
The first is Google ADK. ADK treats a Session as a fact container with a chronological sequence of Events. An Event carries content, author, invocation identity, partial state, and actions; Session state is updated from event state deltas, while working model context is selected and transformed from event history. Maka borrows the deeper principle rather than merely a similar field layout: Session history is fact; working context is a computed projection.
The second is the log-first tradition in distributed data systems. Database WALs, replicated logs, event sourcing, and Kafka share an intuition: do not let every downstream view become an independent truth. Preserve ordered, unambiguous change facts first, then let consumers rebuild their state. If order and commit boundaries are trustworthy, caches, indexes, search views, and even partially damaged state tables can be regenerated.
Maka is not implementing Kafka inside one process, nor does it claim that RuntimeEventStore is a distributed consensus log. The borrowed principle is more fundamental:
Log is the source of truth; state is a materialized view.
That principle directly explains the most important terminal invariant later in this chapter: a Run header cannot declare completion on its own; a terminal RuntimeEvent must support it.
Before following the main path, separate four concepts that are often used interchangeably in casual discussion.
| Concept | Question it answers | Identity in the current implementation |
|---|---|---|
| Session | Which long-lived interaction owns these conversations and executions? | sessionId |
| Turn | Which user-visible exchange is this? | turnId |
| Run | Which concrete execution attempt is this, and what is its state? | runId / AgentRun |
| Invocation | What is the standard start-to-terminal boundary of one Flow call? | invocationId / RuntimeRunner |
On the default production path today, AgentRun creates a runId first and passes the same value to RuntimeRunner as the invocationId. The IDs are therefore usually equal, but the concepts remain distinct. A Run is a durable execution entity. An Invocation is the call boundary standardized by the Runner. Keeping both concepts leaves room for retries, scheduling, or multiple attempts without redefining the event protocol.
The key distinction is simple: a Turn is not a Run, and chat messages are not execution state. A user-visible exchange needs a system-visible execution envelope. Without one, the system can only say that some messages appeared; it cannot reliably say whether the execution actually ended.
The interactive and generic Headless paths currently share this runtime spine:
Read the diagram from left to right. The left side is closer to product entry points and long-lived Sessions. The right side is closer to one provider request and concrete tool side effects. Persistence projections and ledgers are omitted here and introduced separately below.
These layers do more than split a large function. More precisely, they divide responsibility for producing, normalizing, committing, and consuming the Event Log. Each protects a different kind of stability.
SessionManager: the stable product entry pointSessionManager.sendMessage() is the public facade. It is now deliberately thin: public Session operations remain here, while execution is delegated to RuntimeKernel.startTurn().
Desktop, CLI, bot, and Headless callers therefore do not need to understand the Run ledger, Flow, or terminal facts. Runtime internals can evolve while callers continue to express one stable operation: send a user message to a Session.
RuntimeKernel: the control plane for active executionRuntimeKernel turns a Session request into an active Run. It is responsible for:
AgentRun;turnId → runId mapping;RuntimeRunner and AiSdkFlow;SessionEvent stream to the caller;AgentRun.finalize() runs when the Flow finishes.It is an orchestration boundary, not the model loop. A Backend should not own the set of active Runs for a Session, and a product entry point should not decide whether a terminal RuntimeEvent is durable. The Kernel centralizes this cross-layer coordination.
AgentRun: the durable execution envelopeAgentRun gives one execution a durable identity and lifecycle. At startup it:
AgentRunHeader in created state;running Turn projection for a top-level Run;RuntimeEvent;running;While execution is active, AgentRun receives both legacy SessionEvents and canonical RuntimeEvents and writes each to the projection or ledger it belongs to. At the end, it unregisters the active Run, converges Session and Turn state, and commits the final Run state.
Think of AgentRun as the durable envelope around an execution. It does not choose which tool the model calls next. It guarantees which execution this is, which facts it produced, and how it ended.
RuntimeRunner: uniform Invocation semanticsRuntimeRunner neither calls a provider nor executes a tool. It defines the protocol that every Flow must obey:
The Runner returns an InvocationResult containing the ordered events, status, final output, or failure classification. This turns backend-specific streaming behavior into a stable invocation outcome.
The Runner exposes an injectable preflight gate, but the current production path assembled by RuntimeKernel does not inject one. Preflight is therefore an implemented Runner capability, not a separate admission stage on the current desktop path.
AiSdkFlow: the bridge from legacy events to runtime factsThe current model/tool loop still emits renderer-facing SessionEvents through AgentBackend.send(). AiSdkFlow wraps the Backend and maps each event to a canonical RuntimeEvent:
tool_start and tool_result become function calls and responses;It also enforces a crucial rule: one Invocation exposes only one accepted terminal RuntimeEvent. A Backend may emit abort followed by complete(user_stop) during cancellation. The Flow accepts the first terminal fact and silently drains late events, preventing double termination.
Despite its name, AiSdkFlow depends on the AgentBackend interface. It can wrap the production AiSdkBackend or another conforming Backend. It does not reimplement the model loop; it standardizes event semantics around it.
AgentBackend: where the model/tool loop actually runsFor the default AiSdkBackend, the core loop remains inside send(). It:
streamText() through ModelAdapter;ToolRuntime when the model issues a tool call;The default step limit is 50. If the model still requests tools at the cap, the Runtime retains the completed tool results and, when no closing text exists, adds a deterministic notice that tells the user how to continue in a new Turn. The UI is not left with an unexplained final tool row.
ModelAdapter isolates provider and AI SDK differences: model construction, stream startup, chunk normalization, usage normalization, and error classification. ToolRuntime isolates the high-risk side of execution: tool-availability enforcement, permissions, timeout and abort propagation, repeated-failure gating, output, telemetry, and artifact recording.
This sequence focuses on a normal execution that includes a tool call. It intentionally omits some telemetry and compatibility projections to show how control moves between the model and tools.
sequenceDiagram participant U as User participant K as RuntimeKernel participant R as AgentRun participant RR as RuntimeRunner participant F as AiSdkFlow participant B as AiSdkBackend participant M as Model Provider participant T as ToolRuntime U->>K: sendMessage(turnId, text) K->>R: begin() R-->>K: backend + history + initial RuntimeEvent K->>RR: run(InvocationRequest) RR->>F: run(context, input) F->>B: send(BackendSendInput) B->>M: streamText(messages, tools) M-->>B: thinking / text / tool call B->>T: execute(tool, args) T-->>B: tool result B->>M: next step with tool result M-->>B: final text + finish B-->>F: SessionEvents F-->>RR: RuntimeEvents + one terminal event R->>R: commit terminal fact and finalize projections
An AI SDK step is the natural beat of this loop. Maka persists assistant text and thinking per step rather than flattening a whole Turn into one final assistant message. Tool calls carry the corresponding step ID, allowing replay to reconstruct the original ordering among thinking, text, and tool calls.
The provider is silent while a tool runs. ToolRuntime pauses the model stream's idle watchdog because that silence is expected. Individual tools may still enforce their own timeouts, while an outer Run or evaluation layer remains the final backstop.
When the model requests a tool that may cause side effects, ToolRuntime asks PermissionEngine to evaluate the call. The result is one of three kinds:
While waiting, the Session projects waiting_for_user, but the Invocation retains its execution identity. The decision is routed through RuntimeKernel.respondToPermission() to the active Backend. ToolRuntime records the permission decision and either continues execution or returns a denied tool result.
The important point is that permission is not a UI-only pause. Requests and decisions enter the runtime fact model, so replay, diagnostics, and recovery can explain why execution stopped and how control returned.
Maka currently maintains three forms of durable data. They are not three equal sources of truth, nor do they store the same chat three times. RuntimeEventStore is the canonical semantic log of AI interaction; the other stores carry product projections and operational Run state.
| Store | Main contents | Question it answers best |
|---|---|---|
SessionStore | StoredMessages for users, assistants, tools, and Turn state | What should the UI and compatibility APIs display? What is the current in-flight projection? |
AgentRunStore | run.json and operational events.jsonl | When did this Run start, what is its state, and at which model or tool stage did it fail? |
RuntimeEventStore | canonical runtime-events.jsonl plus bounded partial snapshots | Which semantic facts occurred, and how should other state be rebuilt from them? |
The file-backed implementation organizes these around this layout:
sessions/<sessionId>/ ... session projection ... runs/<runId>/ run.json events.jsonl runtime-events.jsonl runtime-partials/
AgentRunStore events act more like an operational index: model stream started, tool started, permission requested, usage recorded. They help diagnose and manage a Run but do not replace the model-interaction log. RuntimeEventStore contains the reconstructable semantic facts: user content, model content, function calls and responses, permission actions, and the terminal fact.
For completed Runs with a healthy ledger, reads and the next model replay prefer RuntimeEvents. SessionStore remains necessary for compatibility and in-flight projection, but it is no longer the only authority for completed runtime semantics.
Streaming text and thinking deltas are not appended forever to immutable JSONL. The file RuntimeEventStore keeps bounded, replaceable partial snapshots. A final non-partial event supersedes the snapshot. This preserves output that was visible before a crash without turning 10,000 deltas into 10,000 permanent ledger rows.
One of the hardest runtime failure classes is disagreement about whether an execution ended. For example:
run.json says completed, but the RuntimeEvent ledger has no terminal event;Maka protects this core invariant:
A terminal Run must have exactly one valid terminal RuntimeEvent, and a terminal Run header must be supported by that terminal fact.
AgentRun therefore requires the terminal RuntimeEvent to be durable before committing a terminal Run header. A Flow without a terminal event becomes a missing_terminal_event failure. Duplicate terminal events are coalesced. Terminal events with a mismatched status, a different Run identity, or partial: true are rejected.
If the terminal RuntimeEvent exists but an interrupted header remains running, the read model can treat the event as the stronger fact and recovery can repair the header. In the opposite direction, if a header claims termination without a trustworthy terminal fact, the system does not blindly trust the header; it conservatively repairs the Run as a missing_terminal_event failure.
This invariant means recovery does not need to guess what the model intended to do next. It only needs to determine which facts are durable and converge all projections on one explainable outcome.
RuntimeKernel.stopSession() first marks active AgentRuns as stopped, then calls Backend stop(). AiSdkBackend aborts the provider stream, ends permission waits, and emits abort/complete events. Even if a provider later produces a complete or error event, AiSdkFlow and AgentRun do not allow it to overwrite the established aborted semantics. The stop source, such as the renderer stop button, is retained in the terminal fact and Run header for diagnostics.
An error is first normalized as non-terminal error content, followed by a failed terminal event that closes the Invocation. RuntimeRunner does not allow a later completed event to mask an error it has already observed. If the Backend throws directly or exhausts without a terminal event, the Runner and Flow produce a structured failure instead of leaving a dangling Run.
Startup recovery does not re-execute model requests or tool side effects. It scans non-terminal Runs and RuntimeEvent ledgers, identifies stale model streams, tool tails, permission waits, and corrupt operational events, then conservatively commits failure or cancellation and repairs Session and Turn projections.
This is state repair, not checkpoint resume. The current Runtime can retain partial output, recover a consistent terminal state, and provide the facts needed for future mid-run recovery. It does not automatically continue from the line after an interrupted tool call when the process restarts.
SessionEvent, StoredMessage, RuntimeEvent, and operational Run events, making event mapping expensive to maintain.AiSdkBackend remains large and coordinates history, context budgets, tool availability, the step loop, usage, and telemetry.AiSdkFlow is still a legacy-to-canonical adapter rather than consuming native RuntimeEvents from the Backend.SessionStore and RuntimeEvent projection must cooperate for active and in-flight reads.These are real architecture boundaries, not details to hide. Future Backend decomposition or checkpoint work must preserve request shape, tool visibility, event order, and the terminal invariant before optimizing for smaller files.
Read the current implementation in this order:
packages/runtime/src/session-manager.ts: public and recovery entry points.packages/runtime/src/runtime-kernel.ts: active Run/Backend control and main-path assembly.packages/runtime/src/agent-run.ts: durable lifecycle, history construction, and terminal commit.packages/runtime/src/runtime-runner.ts: Invocation protocol and outcome classification.packages/runtime/src/ai-sdk-flow.ts: SessionEvent → RuntimeEvent mapping and the single-terminal guarantee.packages/runtime/src/ai-sdk-backend.ts: the AI SDK model/tool step loop.packages/runtime/src/model-adapter.ts: provider stream adaptation.packages/runtime/src/tool-runtime.ts: permissions, tool execution, and side-effect boundaries.packages/core/src/runtime-event.ts: the canonical RuntimeEvent contract.packages/core/src/agent-run.ts and packages/storage/src/agent-run-store.ts: file-backed Run and RuntimeEvent ledgers.The most relevant tests are:
packages/runtime/src/__tests__/runtime-runner.test.tspackages/runtime/src/__tests__/ai-sdk-flow.test.tspackages/runtime/src/__tests__/session-manager.test.tspackages/runtime/src/__tests__/session-manager-terminal-ledger.test.tspackages/storage/src/__tests__/agent-run-store.test.tsThe core of the Maka Runtime is not one class, nor is it merely the AI SDK's multi-step tool loop. It is a Runtime Event Log that preserves and replays the state space of agent interaction. The execution protocol exists to produce, commit, and project those facts:
model/tool stepping engine → canonical RuntimeEvents → durable semantic log → model history / UI / Run state / recovery projections
SessionManager stabilizes the entry point. RuntimeKernel controls active execution. AgentRun commits durable facts. RuntimeRunner defines Invocation semantics. AiSdkFlow translates Backend events into canonical facts. AiSdkBackend, ModelAdapter, and ToolRuntime advance the model/tool loop itself. They cooperate around the Runtime Event Log instead of each retaining a private local truth.
Together, these boundaries protect a simple promise: regardless of how many model steps, tool side effects, permission waits, and failures an agent task encounters, Maka first records what actually happened. As long as that ordered fact history remains, the system can reconstruct the interaction state, materialize new views, and let the next turn continue from trustworthy history.