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.
Maka already has a working local coding agent runtime:
AgentRun ledgerThe 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 JSONLSessionEventAgentRunEvent in the run ledgerRunTraceEventThat split makes the system hard to evolve. It also makes important questions harder than they should be:
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.
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.Maka has different product constraints, so the evolution should be adapted:
ToolRuntime, AgentRunStore, and RunTrace;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:
Flow.Run; the model/tool loop is implicit inside AiSdkBackend.send() plus the AI SDK.StoredMessage types, not from a canonical runtime event history.materializePriorMessages().StoredMessage, SessionEvent, RunTraceEvent, and telemetry rather than one canonical fact.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
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.
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.
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.
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.
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 / 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.
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.
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.
Add the canonical event types and adapters without changing the run path.
Deliverables:
RuntimeEvent and RuntimeEventActionsStoredMessage, SessionEvent, AgentRunEvent, and RunTraceEventReason:
This gives the team a shared language before changing control flow.
Introduce RuntimeRunner.run() and make SessionManager.sendMessage() delegate to it while still using current AgentRun.execute().
Deliverables:
RuntimeRunnerInvocationContextRuntimeGate placeholder for readiness policySessionManager facadeReason:
This moves invocation ownership out of SessionManager without changing the AI SDK or storage path all at once.
Upgrade the run ledger to carry invocation/event linkage.
Deliverables:
invocationIdReason:
The current run ledger is already useful. It should become part of the runtime spine instead of a parallel diagnostic store.
Extract/formalize the current AiSdkBackend.send() loop into AiSdkFlow.
Deliverables:
AgentFlow interfaceAiSdkFlow implementationModelHistoryProjectorAiSdkFlow emits canonical RuntimeEventsAiSdkBackend becomes configuration/factory shell rather than the runtime loop ownerReason:
The AI SDK remains the main engine, but Maka gains explicit flow input/output semantics.
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 eventsReason:
Tools are where model intent becomes local side effects. That boundary must be auditable and replayable.
Move readiness and rebind policy out of desktop main and into runtime gates.
Deliverables:
RuntimeGate owns connection readiness/rebind/session blocked/running guardsReason:
Entry points should translate protocols; they should not own runtime policy.
Build future model prompts from runtime event history projection.
Deliverables:
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.
Avoid these failure modes:
RuntimeEvent.RunTrace become a success/failure source of truth.ToolRuntime; evolve it into the event/action model.Runtime v2 is working when these statements are true:
SessionManager is mostly session CRUD plus backward-compatible facade methods.AiSdkFlow.RuntimeEvent be stored as a separate JSONL immediately, or first mirrored into AgentRunStore events?StoredMessage projection be synchronous with event append, or should projection failures be recoverable/replayable?RunTrace should be folded into RuntimeEvent refs versus left as diagnostic-only rows?These should be answered in the RFC before implementation starts.