blob: 22550a79280b32220ba4463d7200678c43eeabd5 [file] [view]
# Runtime Kernel Extraction
> Archived on 2026-07-13. This is a point-in-time extraction record, not current architecture authority. Start with `ARCHITECTURE.md`.
This document explains the runtime-kernel work in this change set: what changed,
why it was needed, what stayed stable, and how it was verified.
## Summary
Maka already had the pieces of a local desktop coding agent: sessions, model
streams, tool calls, permission prompts, abort handling, usage telemetry, bot and
gateway entry points, and persisted session messages. The problem was that the
core execution responsibilities were still concentrated in a few large runtime
paths, especially `AiSdkBackend` and `SessionManager.sendMessage()`.
This change set keeps the product surfaces stable but introduces clearer
internal runtime boundaries:
```text
SessionManager
-> AgentRun
-> AiSdkBackend
-> ModelAdapter
-> ToolRuntime
-> RunTrace
-> AgentRunStore
```
The intent is not to rewrite the runtime or replace the Vercel AI SDK. The goal
is to make the existing runtime easier to reason about, easier to recover after
interruption, and easier to extend with future backends or workflow integrations.
## What Changed
### ToolRuntime
`ToolRuntime` is now the internal boundary for the lifecycle of model-requested
tools. The extracted runtime owns the work that used to be interleaved inside the
AI SDK backend:
- validate tool input before execution
- evaluate permission policy
- wait for parked permission decisions with timeout behavior
- pause and resume the stream watchdog around tool execution
- propagate abort signals into tools
- classify tool failures
- record tool telemetry and artifacts
- emit tool and permission trace events
`AiSdkBackend` still bridges model stream behavior, but it no longer needs to own
every detail of tool execution.
### ModelAdapter
`ModelAdapter` is the provider-facing stream and error normalization layer. It
keeps AI SDK-specific stream chunks, provider setup, usage normalization, and
provider error mapping out of the higher-level backend orchestration shell.
This makes the boundary explicit:
```text
provider / AI SDK details
-> ModelAdapter
-> Maka runtime events and usage records
```
That separation matters because future providers should not need to duplicate
permission, tool, run, or session-state behavior.
### RunTrace
`RunTrace` is an internal best-effort trace path for runtime events. It records
milestones such as:
- turn started
- model resolved / stream started / stream completed / stream failed
- tool started / completed / failed
- permission requested / decided / failed
- usage recorded
- abort requested
Trace recorder failures are intentionally non-fatal. A failed trace write must
not alter model or tool execution.
### AgentRun Types and Store
The core package now defines internal AgentRun contracts:
- `AgentRunHeader`
- `AgentRunEvent`
- `AgentRunStatus`
- `AgentRunStore`
The storage package adds a file-backed run store:
```text
sessions/<sessionId>/runs/<runId>/run.json
sessions/<sessionId>/runs/<runId>/events.jsonl
```
The store provides:
- atomic `run.json` writes
- append-only run event JSONL
- same-run write serialization
- session run listing
- corrupt committed event-line recovery via `event_corrupt`
- malformed unterminated tail tolerance
This ledger is separate from the existing session message JSONL so runtime
diagnostics and recovery state do not pollute user-visible conversation history.
### AgentRun Execution
`SessionManager.sendMessage()` now delegates the heavy turn lifecycle to
internal `AgentRun.execute()`.
`AgentRun` owns:
- generating and recording run identity
- appending the user message
- writing initial turn state
- locking the connection snapshot for the run
- ensuring the active backend exists
- driving backend stream events
- projecting session status changes
- writing turn completion, failure, abort, or permission-wait states
- recording run started/completed/failed/cancelled status
- routing `RunTrace` events into the durable run ledger
- preserving abort source diagnostics such as `renderer.stop_button`
`SessionManager` remains the public runtime API and continues to own session
CRUD, backend registry orchestration, active run lookup, and legacy recovery
entry points.
### Active Run Registry
The old active stream counters and turn-id side maps were replaced with:
```ts
activeRuns: Map<string, AgentRun>
turnToRunId: Map<string, string>
```
This makes overlapping run behavior explicit. It also avoids masking active
stream accounting bugs with defensive counter clamping.
### Startup Recovery
`recoverInterruptedSessions()` now prefers the AgentRun ledger when run rows are
available. It scans persisted run headers and events, classifies stale
non-terminal runs, repairs them, and then converges the existing session/turn
projection.
Recovered cases include:
- stale `created` or `running` runs
- runs whose last event is `run_started` or `model_stream_started`
- stale tool tails after `tool_started`
- stale permission waits after `permission_requested`
- `model_stream_completed` without a terminal run event
- corrupt run event lines represented as `event_corrupt`
Legacy sessions without run ledger rows still use the prior message and
turn-state recovery path.
## What Stayed Stable
This work intentionally does not change:
- `window.maka.*` preload API
- Electron IPC channel names
- renderer-visible `SessionEvent` behavior
- user-visible permission modes
- session message JSONL compatibility
- builtin tool names or public tool behavior
- model provider settings UI
- bot and OpenGateway public entry points
- Rive workflow integration semantics
The runtime kernel is internal. Public behavior should remain compatible while
the internals become more explicit and recoverable.
## Why This Was Needed
Before this work, a single turn execution was spread across session management,
backend stream handling, tool wrapping, permission policy, telemetry, and abort
logic. That made several questions difficult to answer after a failure:
- Did the model stream start?
- Which backend/model/connection did the turn use?
- Was the run waiting for permission?
- Which tool was running when the app exited?
- Did the user press stop?
- Was usage recorded?
- Did a stale session status reflect a real active run or a crashed process?
The AgentRun ledger gives the runtime a durable fact record for those answers.
ToolRuntime and ModelAdapter then reduce the amount of model/tool/provider logic
that has to be understood at once.
## Recovery Model
The new recovery path is conservative. It does not replay model streams or tools.
Instead, it repairs stale state into deterministic terminal states so the app
does not reopen with sessions permanently stuck in `running` or
`waiting_for_user`.
When a stale non-terminal run is recovered, the runtime:
1. Updates the AgentRun header to terminal state.
2. Appends a durable recovery event such as `run_failed` or `run_completed`.
3. Writes the existing session `turn_state` projection.
4. Updates the session header out of active running/waiting states.
The failure class for app-restart recovery is recorded as `app_restarted`.
Diagnostics are limited to small reason-code fields such as `recovered`,
`failureClass`, `recoveryReason`, `lastEventType`, and `eventCorrupt`; raw user
text and raw event payloads are not copied into recovery diagnostics.
## Files Added
- `packages/runtime/src/tool-runtime.ts`
- `packages/runtime/src/model-adapter.ts`
- `packages/runtime/src/run-trace.ts`
- `packages/core/src/agent-run.ts`
- `packages/storage/src/agent-run-store.ts`
- `packages/runtime/src/agent-run.ts`
- `packages/runtime/src/agent-run-recovery.ts`
## Tests Added or Expanded
The runtime test suite now covers:
- ToolRuntime permission allow/block/prompt/timeout paths
- watchdog pause/resume behavior around tools
- tool abort and failure classification behavior
- tool telemetry and artifact recording behavior
- ModelAdapter stream, usage, and provider error normalization
- RunTrace recording and best-effort failure behavior
- overlapping AgentRuns
- backend build failure after user-message append
- permission handoff without stuck run state
- stop button abort source preservation
- late complete after stop not overwriting aborted state
- durable trace redaction
- AgentRunStore create/read/update/list behavior
- same-run AgentRun event append serialization
- corrupt AgentRun event JSONL recovery
- startup recovery from stale AgentRun ledger states
- legacy startup recovery fallback
- terminal AgentRun recovery idempotency
## Verification
The changes were verified with:
```sh
npm --workspace @maka/core run typecheck
npm --workspace @maka/storage run test
npm --workspace @maka/runtime run typecheck
npm --workspace @maka/runtime run test
npm --workspace @maka/desktop run build:main
git diff --check
```
The final runtime suite included 315 passing tests after the AgentRun recovery
work.
## Follow-Up Work
This PR establishes the internal runtime-kernel shape, but it does not finish
every possible cleanup. Good follow-up slices are:
- merge usage and cost accounting more tightly with the run ledger
- expose an internal run-inspection/debug read model
- reduce the remaining `SessionManager` hook surface used by `AgentRun`
- consider Gateway/Bot policy unification through the same run policy layer
- consider Rive workflow/run mapping only after there is a product need