# Runtime v2 Architecture Evolution > Archived on 2026-07-13. This proposal records the route to the current runtime; it is not current architecture authority. Start with `ARCHITECTURE.md`. This document proposes a long-term evolution path for Maka's agent runtime. It is written for developers who need to understand why the runtime needs another architecture pass, what should stay stable, and how the codebase can move there without a big-bang rewrite. The goal is not to replace the Vercel AI SDK. The goal is to make Maka own its runtime semantics while continuing to use the AI SDK as the main model/tool stepping engine. ## Executive Summary Maka already has a working local coding agent runtime: - Electron desktop entry points - session JSONL storage - model streaming through the AI SDK - tool execution and permission prompts - abort handling - bot and OpenGateway entry points - tool artifacts and usage telemetry - an internal `AgentRun` ledger The current architecture is better than the original monolithic path, but the runtime still lacks one stable center of gravity. The same run is currently represented through several related but separate structures: - `StoredMessage` in the session JSONL - renderer-facing `SessionEvent` - `AgentRunEvent` in the run ledger - best-effort `RunTraceEvent` - telemetry records That split makes the system hard to evolve. It also makes important questions harder than they should be: - Which facts are the real runtime truth? - Which objects are only UI projections? - Which events should be replayed into the next model request? - Which events are diagnostic-only? - Where should permission, tool, recovery, and telemetry semantics live? Runtime v2 should introduce a canonical invocation/event spine: ```text RuntimeRunner -> InvocationContext -> AgentFlow -> AiSdkFlow -> AI SDK streamText -> ToolRuntime -> RuntimeEvent ledger -> projections ``` The key shift is: ```text Today: SessionManager + StoredMessage/SessionEvent are close to the runtime center. Target: Invocation + RuntimeEvent + AgentFlow are the runtime center. StoredMessage, renderer SessionEvent, AgentRunStore, RunTrace, and telemetry become projections or ledgers derived from canonical runtime facts. ``` ## Design Inputs This proposal is based on the current Maka implementation and on the runtime structure documented in the Google ADK Go reading materials. The useful lesson from ADK is not to copy type names directly. The useful lesson is the layering: ```text Runner -> Agent -> Flow -> Model/Tool -> Event -> Session ``` In that model: - `Runner` owns the invocation shell and persistence boundary. - `Agent` owns lifecycle and routing. - `Flow` owns the model/tool loop. - `Event` is the shared runtime fact language. - `Session` stores durable history and scoped state. - tool, callback, plugin, instruction, workflow, entrypoint, and telemetry layers attach around that axis without becoming the axis themselves. Maka has different product constraints, so the evolution should be adapted: - keep the AI SDK as the long-term main flow implementation; - preserve Electron and existing user-visible session behavior; - preserve existing JSONL compatibility; - reuse `ToolRuntime`, `AgentRunStore`, and `RunTrace`; - avoid a flag day where all storage and UI projections change at once. ## Current Runtime Path Today, one user turn approximately flows like this: ```text renderer / bot / gateway -> desktop main / entrypoint code -> ensureSessionCanSend(...) -> SessionManager.sendMessage(sessionId, input) -> new AgentRun(...) -> AgentRun.execute() -> append user StoredMessage -> append turn_state=running -> lock connection snapshot -> ensureActive() / build AiSdkBackend -> register active run -> AiSdkBackend.send() -> PermissionEngine.beginTurn() -> RunTrace -> ModelAdapter.resolveModel() -> build AI SDK tool map -> materialize prior StoredMessage[] into AI SDK messages -> AI SDK streamText({ tools, stopWhen: stepCountIs(maxSteps) }) -> AI SDK owns model -> tool -> model stepping -> tool.execute(...) calls ToolRuntime -> pump fullStream into SessionEvent queue -> append assistant StoredMessage -> append token_usage StoredMessage -> queue complete/error/abort -> AgentRun updates SessionHeader / turn_state / AgentRunStore -> cleanup active run ``` This split has some good properties: - `SessionManager.sendMessage()` no longer owns the whole hot path. - `AgentRun` gives a durable run lifecycle record. - `ToolRuntime` is a real boundary for tool permission, execution, artifacts, and tool telemetry. - `ModelAdapter` isolates provider/AI SDK stream and error normalization. But it also has structural limits: - Maka does not have its own explicit `Flow.Run`; the model/tool loop is implicit inside `AiSdkBackend.send()` plus the AI SDK. - Cross-turn model history is built from selected `StoredMessage` types, not from a canonical runtime event history. - tool calls and tool results are persisted for UI/history, but their role in future model context is an implicit policy inside `materializePriorMessages()`. - permission and tool events are represented across `StoredMessage`, `SessionEvent`, `RunTraceEvent`, and telemetry rather than one canonical fact. - entrypoints still own some runtime readiness semantics, such as connection readiness/rebind checks. ## Target Architecture The target architecture keeps the AI SDK as the primary model/tool stepping engine, but wraps it in Maka-owned invocation and event semantics. ```text ┌────────────────────────────────────────────────────────────────────┐ │ Entrypoint Adapters │ │ │ │ Desktop IPC Bot Gateway OpenGateway │ │ - parse input - parse input - parse HTTP │ │ - validate shape - auth/rate limit - SSE/JSON │ │ - broadcast output - send bot reply - gateway output │ │ │ │ Entrypoints are protocol adapters. They do not own runtime loop, │ │ readiness, recovery, or model/tool policy. │ └──────────────────────────────┬─────────────────────────────────────┘ │ RuntimeRequest ▼ ┌────────────────────────────────────────────────────────────────────┐ │ RuntimeRunner │ │ │ │ - RuntimeGate preflight │ │ - create invocation/run context │ │ - append user RuntimeEvent │ │ - resolve active agent/flow policy │ │ - run AgentFlow │ │ - persist canonical events │ │ - drive projections │ └──────────────────────────────┬─────────────────────────────────────┘ │ InvocationContext ▼ ┌────────────────────────────────────────────────────────────────────┐ │ AgentFlow Interface │ │ │ │ interface AgentFlow { │ │ run(ctx, input): AsyncIterable │ │ } │ │ │ │ AiSdkFlow is the default long-term implementation: │ │ │ │ - build AI SDK messages from runtime history projection │ │ - build AI SDK tool map │ │ - call streamText({ tools, stopWhen: stepCountIs(...) }) │ │ - map text/thinking/tool/usage/finish/error to RuntimeEvent │ │ - delegate tool execution to ToolRuntime │ └──────────────────────────────┬─────────────────────────────────────┘ │ tool.execute seam ▼ ┌────────────────────────────────────────────────────────────────────┐ │ ToolRuntime │ │ │ │ - declaration/schema/registry │ │ - permission evaluation and parked decisions │ │ - tool implementation execution │ │ - output deltas and artifacts │ │ - tool telemetry │ │ - RuntimeEvent(tool_call/tool_result/permission/artifact) │ │ - returns result to AI SDK so streamText can continue stepping │ └────────────────────────────────────────────────────────────────────┘ ``` Shared services sit beside this path: ```text RuntimeEventLedger canonical invocation facts ProjectionManager RuntimeEvent -> StoredMessage JSONL RuntimeEvent -> renderer SessionEvent stream RuntimeEvent -> TurnRecord / SessionHeader RuntimeEvent -> AgentRunStore / RunTrace RuntimeEvent -> TelemetryRepo SessionService event history + scoped state legacy JSONL compatibility RuntimeGate readiness / rebind / blocked/running/waiting guards Plugin / Callback / Instruction prompt injection / audit / retry-reflect / telemetry hooks ``` ## Canonical RuntimeEvent Runtime v2 needs a single internal fact model. A sketch: ```ts type RuntimeEvent = { id: string; invocationId: string; runId: string; sessionId: string; turnId: string; ts: number; author: 'user' | 'agent' | 'tool' | 'system'; role: 'user' | 'model' | 'tool' | 'system'; branch?: string; partial: boolean; content?: { text?: string; thinking?: string; functionCall?: { id: string; name: string; args: unknown; }; functionResponse?: { id: string; name: string; result: unknown; isError?: boolean; }; error?: { code?: string; reason?: string; message: string; }; }; actions?: { stateDelta?: Record; artifactDelta?: Record; permissionRequest?: PermissionRequest; permissionDecision?: PermissionDecision; transferToAgent?: string; endInvocation?: boolean; tokenUsage?: TokenUsage; }; refs?: { storedMessageId?: string; traceEventId?: string; toolCallId?: string; providerEventId?: string; }; }; ``` This event is not a UI event and not a trace event. It is the internal runtime fact. Other records should either be written from it or be explicitly linked to it. ## Consumption Paths ### Desktop User Message ```text Renderer -> preload IPC -> main IPC handler -> normalize input -> validate attachment shape -> RuntimeRunner.run({ source: 'desktop', sessionId, text, attachments }) -> RuntimeGate.preflight() -> create invocation -> append RuntimeEvent(user) -> ProjectionManager writes user StoredMessage and renderer append event -> AiSdkFlow.run(ctx) -> build AI SDK messages from runtime history projection -> streamText(...) -> partial model chunks -> RuntimeEvent(partial model text) -> final model output -> RuntimeEvent(final model text) -> tool calls go through ToolRuntime -> ProjectionManager writes: -> assistant StoredMessage -> TurnRecord -> SessionHeader -> AgentRun ledger status -> telemetry usage/tool records -> main process streams projected SessionEvents back to renderer ``` The renderer consumes a projection stream. It does not consume the runtime core directly. ### Tool Call and Permission ```text AiSdkFlow -> AI SDK asks tool.execute({ args, toolCallId }) -> ToolRuntime.executeTool() -> RuntimeEvent(tool_call) -> StoredMessage tool_call -> renderer tool_start -> PermissionEngine.evaluate() ├─ allow │ -> run tool.impl │ -> RuntimeEvent(tool_result) │ -> return result to AI SDK │ ├─ block │ -> RuntimeEvent(tool_result isError synthetic) │ -> return synthetic error result to AI SDK │ └─ prompt -> RuntimeEvent(permission_request) -> renderer permission modal -> session waiting_for_user projection -> park tool execution -> respondPermission(...) -> RuntimeEvent(permission_decision) -> allow/deny branch ``` Permission is a runtime action, not just a UI event. It should be represented in `RuntimeEvent.actions`. ### Model History Construction Today, model history is built from stored messages and skips several runtime message types. Runtime v2 should make this an explicit projection: ```text RuntimeEvent history -> ModelHistoryProjector include: user text events model final text events selected system/instruction events function call events when required by the provider protocol tool function response events exclude: partial chunks token usage trace diagnostics UI-only system notes permission ack unless deliberately exposed to the model -> AI SDK messages ``` This makes the "what does the next model call see?" policy reviewable and testable. ### Startup Recovery ```text App startup -> RuntimeRecovery.scan() -> read invocation/run ledger -> classify non-terminal invocations by latest RuntimeEvent: - model stream started with no terminal event -> failed app_restarted - tool started with no result -> failed interrupted_tool - permission_request pending -> waiting_for_user or failed by policy - final model event persisted but run header not completed -> complete it -> append recovery RuntimeEvent -> ProjectionManager repairs: - TurnRecord - SessionHeader - StoredMessage system note if needed - AgentRun terminal status ``` Recovery should reason from canonical invocation facts first and then repair projections. It should not independently guess from multiple stores unless it is handling legacy data. ### Bot and OpenGateway ```text Bot / OpenGateway -> normalize external protocol -> RuntimeRunner.run({ source: 'bot' | 'gateway', ... }) -> same RuntimeGate -> same AiSdkFlow -> same ToolRuntime -> projected output: bot: collect final response and send bot reply gateway: format JSON/SSE response desktop: optional fan-out when the same session is visible ``` The runtime should not have three subtly different send paths for desktop, bot, and gateway. ## Source of Truth Boundaries The intended boundary is: ```text RuntimeEventLedger runtime fact StoredMessage JSONL user-visible conversation projection and legacy read model renderer SessionEvent transport projection for UI streaming AgentRunStore invocation/run lifecycle ledger, eventually closely linked with RuntimeEvent RunTrace diagnostic-only projection; failures must not affect execution TelemetryRepo economic and operational projection ``` This boundary is the main reason to do the work. Without it, adding more features will keep increasing the number of places that need to agree about the same run. ## Proposed Module Shape One possible file layout: ```text packages/runtime/src/ runtime-event.ts runtime-event-projection.ts invocation-context.ts runtime-runner.ts runtime-gate.ts runtime-recovery.ts flows/ agent-flow.ts ai-sdk-flow.ts model/ model-adapter.ts model-history.ts tools/ tool-runtime.ts tool-registry.ts permission-events.ts projections/ stored-message-projection.ts session-event-projection.ts turn-record-projection.ts telemetry-projection.ts session-manager.ts ``` This is not a required final file layout. The important part is the direction: `SessionManager` becomes a facade and session CRUD owner, not the runtime brain. ## Migration Plan ### Phase 1: RuntimeEvent RFC and Adapters Add the canonical event types and adapters without changing the run path. Deliverables: - `RuntimeEvent` and `RuntimeEventActions` - mapping tests for current `StoredMessage`, `SessionEvent`, `AgentRunEvent`, and `RunTraceEvent` - documentation of which events are facts and which are projections Reason: This gives the team a shared language before changing control flow. ### Phase 2: RuntimeRunner Shell Introduce `RuntimeRunner.run()` and make `SessionManager.sendMessage()` delegate to it while still using current `AgentRun.execute()`. Deliverables: - `RuntimeRunner` - `InvocationContext` - `RuntimeGate` placeholder for readiness policy - backward-compatible `SessionManager` facade - tests proving current UI-visible behavior remains stable Reason: This moves invocation ownership out of `SessionManager` without changing the AI SDK or storage path all at once. ### Phase 3: AgentRunStore to Invocation Ledger Upgrade the run ledger to carry invocation/event linkage. Deliverables: - run headers linked to `invocationId` - run events linked to canonical runtime event ids where possible - recovery tests that reason from invocation facts and repair projections Reason: The current run ledger is already useful. It should become part of the runtime spine instead of a parallel diagnostic store. ### Phase 4: AiSdkFlow Formalization Extract/formalize the current `AiSdkBackend.send()` loop into `AiSdkFlow`. Deliverables: - `AgentFlow` interface - `AiSdkFlow` implementation - `ModelHistoryProjector` - `AiSdkFlow` emits canonical `RuntimeEvent`s - `AiSdkBackend` becomes configuration/factory shell rather than the runtime loop owner Reason: The AI SDK remains the main engine, but Maka gains explicit flow input/output semantics. ### Phase 5: ToolRuntime Event Actions Make tool calls, tool results, permission requests, permission decisions, state deltas, and artifact deltas first-class runtime event actions. Deliverables: - `tool_call` / `tool_result` runtime events - permission request/decision runtime actions - artifact delta linkage - tests for allow/block/prompt/deny/abort paths through canonical events Reason: Tools are where model intent becomes local side effects. That boundary must be auditable and replayable. ### Phase 6: Entrypoint Cleanup Move readiness and rebind policy out of desktop main and into runtime gates. Deliverables: - `RuntimeGate` owns connection readiness/rebind/session blocked/running guards - desktop/bot/gateway entrypoints call the same runner APIs - contract tests proving the same session state behaves the same across desktop, bot, and gateway paths Reason: Entry points should translate protocols; they should not own runtime policy. ### Phase 7: Model History Correctness Build future model prompts from runtime event history projection. Deliverables: - explicit policy for which runtime events enter model history - tests for tool-result replay into the next model call - tests excluding partial chunks, telemetry, trace, and UI-only notes - provider compatibility tests around AI SDK message shapes Reason: The next model call must see the right history for the right reason. This should be a tested runtime policy, not incidental stored-message filtering. ## What Not To Do Avoid these failure modes: - Do not rewrite all provider streaming logic from scratch. - Do not remove the AI SDK as the main flow engine. - Do not delete session JSONL compatibility. - Do not move UI concerns into `RuntimeEvent`. - Do not let `RunTrace` become a success/failure source of truth. - Do not let desktop main remain the owner of readiness/rebind runtime policy. - Do not introduce a second tool runtime beside the current `ToolRuntime`; evolve it into the event/action model. ## Success Criteria Runtime v2 is working when these statements are true: - A developer can answer "what happened in this turn?" by reading one invocation/event ledger. - UI messages, turn records, run records, trace rows, and telemetry can be traced back to canonical runtime event ids. - desktop, bot, and gateway entry points share the same runtime readiness and execution semantics. - model history construction is an explicit tested projection from runtime events. - tool permission and tool result behavior is visible as runtime actions, not scattered side effects. - `SessionManager` is mostly session CRUD plus backward-compatible facade methods. - the AI SDK remains a supported first-class flow implementation through `AiSdkFlow`. ## Open Questions - Should canonical `RuntimeEvent` be stored as a separate JSONL immediately, or first mirrored into `AgentRunStore` events? - Should `StoredMessage` projection be synchronous with event append, or should projection failures be recoverable/replayable? - How much of current `RunTrace` should be folded into `RuntimeEvent` refs versus left as diagnostic-only rows? - Should permission prompts be model-visible events, UI-only events, or configurable by flow policy? - How should branch/agent transfer be represented before Maka has a full multi-agent tree? - What is the compatibility strategy for old sessions that have no runtime event ledger? These should be answered in the RFC before implementation starts.