24 KiB
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
AgentRunledger
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:
StoredMessagein the session JSONL- renderer-facing
SessionEvent AgentRunEventin 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:
Runnerowns the invocation shell and persistence boundary.Agentowns lifecycle and routing.Flowowns the model/tool loop.Eventis the shared runtime fact language.Sessionstores 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, andRunTrace; - 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.AgentRungives a durable run lifecycle record.ToolRuntimeis a real boundary for tool permission, execution, artifacts, and tool telemetry.ModelAdapterisolates 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 insideAiSdkBackend.send()plus the AI SDK. - Cross-turn model history is built from selected
StoredMessagetypes, 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:
RuntimeEventandRuntimeEventActions- mapping tests for current
StoredMessage,SessionEvent,AgentRunEvent, andRunTraceEvent - 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:
RuntimeRunnerInvocationContextRuntimeGateplaceholder for readiness policy- backward-compatible
SessionManagerfacade - 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:
AgentFlowinterfaceAiSdkFlowimplementationModelHistoryProjectorAiSdkFlowemits canonicalRuntimeEventsAiSdkBackendbecomes 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_resultruntime 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:
RuntimeGateowns 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
RunTracebecome 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.
SessionManageris mostly session CRUD plus backward-compatible facade methods.- the AI SDK remains a supported first-class flow implementation through
AiSdkFlow.
Open Questions
- Should canonical
RuntimeEventbe stored as a separate JSONL immediately, or first mirrored intoAgentRunStoreevents? - Should
StoredMessageprojection be synchronous with event append, or should projection failures be recoverable/replayable? - How much of current
RunTraceshould be folded intoRuntimeEventrefs 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.