Files
maka/docs/archive/maka-core-tech-walkthrough.md
agent-radar 7be535059e
Dependency audit / audit (push) Canceled after 0s
CI / plan (push) Canceled after 0s
Gitoxide helper admission / macos-latest (push) Canceled after 0s
Gitoxide helper admission / ubuntu-latest (push) Canceled after 0s
Gitoxide helper admission / windows-latest (push) Canceled after 0s
Runtime Host peer admission / quality (push) Canceled after 0s
Windows recovery / windows_recovery (push) Canceled after 0s
CI / heavy (push) Canceled after 0s
CI / test (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 19:05:14 +08:00

15 KiB
Raw Permalink Blame History

Maka 核心技术解读

Archived on 2026-07-13. Current backend guidance lives in ARCHITECTURE.md, its architecture chapters, source, and tests.

一份面向工程师的 Maka 技术栈通读笔记。覆盖 runtime 内核、权限系统、持久化与恢复、Electron 集成。 生成日期:2026-06-25

目录


整体定位

Maka 是一个本地优先(local-first)的 Electron 桌面 AI agent 工作台。一句话概括它的技术追求:让用户在自己的电脑上跑一个可观察、可控、可恢复的 agent——所有数据本地落盘,敏感值按各自边界保存在本地,工具调用走权限策略,单次对话崩溃后能从 ledger 恢复。

仓库分层

monorepo 用 npm workspaces,分层干净:

  • packages/core:纯契约层,零运行时依赖,只定义类型和纯函数。
  • packages/storage:文件持久化层。
  • packages/runtime:内核实现,最核心的部分。
  • packages/ui:共享渲染组件。
  • apps/desktop:Electron 壳子,把 runtime 装进窗口。

核心设计原则:契约与实现分离,runtime 内核再拆成可独立理解的边界,公共 API 保持稳定的同时内部可演化。


Runtime 内核架构

这是整个项目最值钱的部分。分层关系:

SessionManager            ← 对外公共 API(桌面 / bot / gateway 都走它)
  -> AgentRun             ← 单次 turn 的生命周期 + 启动恢复
      -> AiSdkBackend     ← 流式 + 工具循环引擎
          -> ModelAdapter ← provider stream / usage / error 归一化
          -> ToolRuntime  ← 工具输入校验 / 权限 / watchdog / abort / telemetry
      -> RunTrace         ← best-effort 诊断 trace,写失败不影响对话
      -> AgentRunStore    ← durable run ledger

关键代码入口:

模块 文件
SessionManager 类 packages/runtime/src/session-manager.ts:232
AgentRun.execute() packages/runtime/src/agent-run.ts
send() 流式泵 packages/runtime/src/ai-sdk-backend.ts:435
wrapToolExecute 权限门控 packages/runtime/src/tool-runtime.ts:146
provider 适配 packages/runtime/src/model-adapter.ts

SessionManager:公共 runtime API

对外(桌面 IPC、bot adapter、open-gateway 三类入口)暴露的唯一 runtime 门面。职责:

  • session CRUD
  • backend registry 编排
  • active run 查找
  • 恢复入口

真正干活的 turn 生命周期被委派给 AgentRun。

AgentRun:单 turn 的 durable 编排

理解恢复机制的关键。一次 sendMessage 会创建一个 AgentRun,它负责:

  1. 生成 runId,写 run_created 事件到 ledger。
  2. 追加用户消息、写初始 turn 状态。
  3. 锁定连接快照(connectionLocked),防止 turn 跑到一半连接被改。
  4. 构建 prior runtime context(从历史 run 的 RuntimeEvent 投影成模型历史)。
  5. 驱动 backend 流事件,投影 session 状态。
  6. 写 turn 完成 / 失败 / abort / permission-wait 状态。
  7. finalize() 收尾:注销 active run、更新 header、写 run_completed / run_failed / run_cancelled。

execute() 是 async generator,核心结构简洁:

async *execute(): AsyncIterable<SessionEvent> {
  try {
    const begin = await this.begin();
    for await (const ev of begin.backend.send(begin.backendInput)) {
      await this.recordSessionEvent(ev);
      yield ev;
    }
  } catch (error) {
    await this.recordFailure(error);
    throw error;
  } finally {
    await this.finalize();
  }
}

每个 backend 事件都会被 recordSessionEvent 投影成 session 状态变更(running / blocked / aborted),并记录到 run ledger。即使进程崩了,重启时也能从 ledger 重建。

AiSdkBackend:流式 + 多步工具循环

agent loop 的引擎。用 Vercel AI SDK 的 streamText 配合 stopWhen: stepCountIs(N) 驱动多步工具调用循环——循环本身交给 AI SDK,但所有"自己的机器"(权限、持久化、materializer、watchdog)都保留在 Maka 这边。

send() 结构(ai-sdk-backend.ts:435):

  1. 建 AsyncEventQueue<SessionEvent>,作为后台泵和前台 yield 之间的缓冲。
  2. ModelAdapter.resolveModel() 解析 LanguageModel,失败立即推 error + complete。
  3. 构建 provider tools dict,每个工具的 execute 被权限层包了一层。
  4. 从 RuntimeEvent 历史构建模型消息。
  5. 后台泵:streamText({...}) → for await (chunk of result.fullStream) → ModelAdapter.handleStreamChunk 归一化 → 推 queue。
  6. 前台 yield* drain(queue)。

精巧点:

  • StreamWatchdog:流式连接有 connect timeout 和 idle timeout,超时会 abort controller 并推 error 事件,防止挂死。
  • step cap grace:当 finishReason === 'tool-calls'(撞了 step 上限)且没有 assistant 文本时,注入一条确定性提示,告诉用户已达本轮工具上限、可发"继续"。
  • prepareStep 动态工具加载:同 turn 内可以逐步激活更多工具(deferred load),active 工具集按 step 重算。

ToolRuntime:权限门控 seam

整个安全模型的执行点。工具的 execute 回调被 wrapToolExecute 包裹后交给 AI SDK,每次模型调用工具都经过这条链:

  1. loop-gate:同一个 tool + 同一组 args 连续失败 N 次,直接 block,防止 agent 死循环。block 本身不记 outcome,streak 停在阈值,后续相同调用继续被拦。
  2. PermissionEngine.evaluate() 返回三态:
    • allow → 跑真实 impl,写 ToolResult。
    • block → 合成 isError:true 的工具结果返回给模型,不执行真实逻辑。
    • prompt → 推 PermissionRequestEvent 给 UI,await 一个 parked Promise,等用户决定。allow 则跑,deny 则合成"用户拒绝"。
  3. 工具执行期间暂停 stream watchdog(避免长任务被判超时),finally 里恢复。
  4. abort signal 透传进工具,stop 按钮能中断。
  5. 记录 telemetry、artifact candidate、错误分类。

ModelAdapter:provider 归一化

把 provider / AI SDK 的细节挡在外面:stream chunk 类型、provider setup、usage 归一化、provider error mapping。

  • startStream 统一调 streamText。
  • handleStreamChunk 把各种 chunk 翻译成 Maka 的 SessionEvent(text delta、thinking delta、complete 等)。

这样未来加 provider 不需要重复权限 / 工具 / run / session 逻辑。

RunTrace:best-effort 诊断

记录 turn 的里程碑事件:turn started、model resolved / stream started / completed / failed、tool started / completed / failed、permission requested / decided、usage recorded、abort requested。

关键约束:trace 写失败绝不影响用户对话。它和 session 消息 JSONL 是分开的两套持久化。


权限系统

这是 Maka 区别于普通 chat demo 的核心安全设计,定义在 packages/core/src/permission.ts。

四档 PermissionMode

explore / ask / execute / bypass,从只读到全自动。

12 类 ToolCategory

read、web_read、file_write、fs_destructive、shell_safe、shell_unsafe、git_destructive、network_send、privileged、browser、custom_tool、subagent。

mode × category 策略矩阵

PERMISSION_POLICY 是纯函数决策表,决定每个组合是 allow / prompt / block。设计很克制:

模式 读 写 危险操作 网络 浏览器
explore allow block block web_read=prompt block
ask allow prompt prompt prompt prompt
execute allow allow 永远 prompt allow 永远 prompt
bypass allow allow allow allow allow

核心规则:不可逆操作(fs_destructive、git_destructive、privileged、browser)在 execute 模式下仍强制 prompt——浏览器操作可能发帖 / 下单,视为不可逆。bypass 全 allow,仅用户显式选择时进入。

BUILTIN_TOOL_CATEGORY 把 Claude SDK 风格的工具名映射到类别,Bash 默认 shell_unsafe,再由 categorizeBash() 根据命令前缀动态降级或升级(ls/pwd → shell_safe,rm → fs_destructive,git push --force → git_destructive)。

纯函数原则

preToolUse() 给定输入必然返回相同结果,不生成 UUID(requestId 由 runtime 层的 PermissionEngine 生成),便于测试。

parked Promise 机制

PermissionEngine 为每个 outstanding 权限请求维护一个 parked Promise,UI 的决定通过 respondToPermission() resolve 回等待中的 adapter。turn 结束时未回答的请求会被 reject 成 user_stop。


持久化与恢复

文件布局

工作数据放 Electron userData 下的 workspace 目录:

<Electron userData>/workspaces/default/
  llm-connections.json
  credentials.json
  settings.json
  sessions/<sessionId>/
    messages.jsonl
    runs/<runId>/
      run.json          ← run header(原子写)
      events.jsonl      ← append-only run 事件

AgentRun ledger

FileAgentRunStore(packages/storage/src/agent-run-store.ts)实现:

  • run.json 原子写(writeAtomic 先写临时文件再 rename)。
  • events.jsonl append-only。
  • 同 run 写串行化(writeQueues per sessionId+runId 的 Promise 链),避免并发写竞争。
  • ID 校验 SAFE_ID_PATTERN,防路径穿越。
  • 读事件时容忍损坏行(event_corrupt)和未终止尾部。

这套 ledger 和用户可见的 session 消息 JSONL 分开,诊断 / 恢复状态不污染对话历史。

启动恢复

recoverInterruptedSessions() 优先用 run ledger:扫描持久化的 run header 和事件,分类非终态 run(created / running / waiting_permission),修复后收敛 session / turn 投影。

覆盖的恢复场景:

  • stale 的 created / running run
  • 停在 run_started / model_stream_started 的 run
  • tool_started 后的残留
  • permission_requested 后的等待
  • 缺终态事件的 model_stream_completed
  • 损坏事件行

没有 run ledger 的老 session 走旧的 message + turn-state 恢复路径。


Electron 集成层

进程边界

进程 文件 职责
main apps/desktop/src/main/main.ts(4593 行) 装配 SessionManager、注册 IPC、管理窗口、OAuth、bot、gateway
preload apps/desktop/src/preload/preload.ts contextBridge 暴露 window.maka.* 白名单 API
renderer apps/desktop/src/renderer/ React UI + Settings

main.ts:1262 装配 runtime = new SessionManager({...}),main.ts:564 起 OpenGatewayService。IPC 用 ipcMain.handle 注册大量通道(memory、artifacts、settings、connection、session 等)。

密钥安全边界

  • Provider/API key、bot token、proxy password、gateway token、Tavily key 写入本机凭据文件,依赖 OS 账号边界和文件权限;subscription OAuth token 使用独立的系统安全存储。
  • Renderer 永远拿不到明文密钥,Settings 只显示 masked 状态和测试结果。

多入口

除了桌面 UI,runtime 还服务两类外部入口:

  • Bot adapter(packages/runtime/src/bots/):Telegram、飞书、企业微信、微信 iLink、Discord、钉钉、QQ,都继承 base-adapter.ts,统一对接 SessionManager。
  • Open Gateway(open-gateway.ts,1489 行):本地 HTTP / SSE API,用 token 保护,让外部读取会话状态 / 事件 / 能力 / 健康摘要。

数据流:一次对话的完整路径

  1. 用户在 renderer 输入消息 → window.maka.session.send → IPC → SessionManager.sendMessage。
  2. 创建 AgentRun,写 run_created 到 ledger,append 用户消息到 messages.jsonl。
  3. 从历史 run 的 RuntimeEvent 投影模型上下文。
  4. AiSdkBackend.send():构建权限包裹的工具 → streamText(stepCountIs(N)) → 后台泵 fullStream。
  5. 每个流 chunk 经 ModelAdapter.handleStreamChunk 归一化成 SessionEvent → 推 queue → 前台 yield → IPC 流到 renderer。
  6. 模型调用工具 → wrapToolExecute → loop-gate → PermissionEngine.evaluate → allow / block / prompt → 执行或合成错误。
  7. assistant 文本落 messages.jsonl,usage 记 telemetry,trace 写 run ledger。
  8. finalize() 收尾,写 run_completed,更新 session header。

技术亮点小结

  • ledger + projection 模式:run 事件是 source of truth,session 状态是投影,崩溃可重建——整个恢复能力的根基。
  • 纯函数策略 + runtime 包装:权限决策表是纯函数好测试,requestId / parking 状态由 runtime 管,职责分得清楚。
  • 权限门控作为工具 execute 的 seam:不动 AI SDK 的循环,在 execute 回调里插入 allow / block / prompt,最小侵入。
  • best-effort trace 不影响主路径:诊断信息尽力写,失败静默,对话绝不受拖累。
  • 同 run 写串行化 + 原子写:文件持久化的并发安全靠 Promise 链和 rename 保证。
  • 安全分层:模式 × 类别矩阵 + 命令分类器 + 凭据文件权限 / subscription token 安全存储 + preload 白名单,多层防御。