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:

RuntimeRunner
  -> InvocationContext
  -> AgentFlow
      -> AiSdkFlow
          -> AI SDK streamText
          -> ToolRuntime
  -> RuntimeEvent ledger
  -> projections

The key shift is:

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:

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:

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.

┌────────────────────────────────────────────────────────────────────┐
                        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:

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:

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

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

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:

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

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

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:

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:

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 RuntimeEvents
  • 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.