| # 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<RuntimeEvent> │ |
| │ } │ |
| │ │ |
| │ 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<string, unknown>; |
| artifactDelta?: Record<string, string | number>; |
| 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. |