Maka Runtime 核心架构解析:Log Is the Runtime——用 Runtime Event Log 回放 Agent 的状态空间
2026/9/17 21:08:52 网站建设 项目流程

Maka Runtime 核心架构解析:Log Is the Runtime——用 Runtime Event Log 回放 Agent 的状态空间

【免费下载链接】makaApache Maka (Incubating) is a high-performance agent workspace that keeps a complete record of everything it did.项目地址: https://gitcode.com/GitHub_Trending/mak/maka

本文是 Apache Maka(Incubating)Runtime 架构系列的第一章。核心回答一个问题:Maka 如何保存一次 Agent 运行真正经历过的状态空间,并在下一轮、进程重启或新投影中重新得到它?答案是Runtime Event Log:模型循环负责产生事实(facts),Event Log 负责保存事实,而 Session、Run、UI、模型上下文和恢复逻辑都是这份有序日志的投影(projections)。读完本文,你将掌握 Maka Runtime 的事件模型、执行主链各层职责、终止不变量(terminal invariant),以及如何按"代码阅读地图"定位主链实现——这对第一次进入 Maka Runtime 的工程师和需要修改运行主链的维护者都适用。文中描述的是截至 2026-08-23 已在生产主链中落地的实现,历史设计文档中的阶段性计划不作为当前事实。

从一个看似简单的请求开始

假设用户对 Maka 说:

找出这个项目里失败的测试,修复问题,然后重新运行测试。

如果 Maka 只是一个聊天应用,执行路径只有三步:把文字发给模型、等待回复、把回复显示出来。但 Agent 的真实执行过程远比这复杂:

  1. 模型先阅读项目和测试输出;
  2. 模型调用文件、搜索或终端工具;
  3. 某些工具需要用户授权,运行暂时停住(parking);
  4. 工具可能持续输出(streaming),也可能失败、超时或被取消;
  5. 工具结果回到模型,模型决定下一步;
  6. "模型 → 工具 → 模型"的步骤循环重复多次;
  7. 最后,系统必须明确判断这次运行究竟完成、失败,还是被用户中止。

过程中,界面需要实时显示文本和工具活动;下一轮模型需要读到可信历史;应用崩溃重启后,系统不能永远停在"运行中";用户按下停止按钮后,迟到的 provider 事件也不能把状态重新写成"完成"。

所以,Runtime 真正解决的问题不是"怎样调用一次 LLM API",而是:

如何把一个包含流式输出、工具副作用、用户介入和进程故障的开放式循环,记录成一段可以重新解释和回放的事实历史。

先说结论:状态不是一张表,而是日志的函数

Maka Runtime 最核心的设计判断,不是选择了哪个模型 SDK,也不是把执行逻辑拆成了多少层,而是:

Runtime Event Log 才是 Agent 交互的语义事实源(semantic source of truth)。系统在某一时刻的状态,是这段有序日志经过某种投影之后的结果。

可以把它写成一个简单的关系:

State(t) = Project(RuntimeEvents[0..t], policy, runtime configuration)

同一段日志可以被不同消费者解释成不同状态:

  • Model History Projector得到下一次模型调用需要看到的 messages;
  • Runtime Read Model得到 UI 需要展示的对话、工具活动和 Turn 状态;
  • Terminal Fact Classifier得到一次 Run 的最终结果;
  • Recovery逻辑判断进程退出前哪些事实已经 durable;
  • Context Budget 与 compaction 策略得到一个更小但保留关键语义的工作上下文;
  • 未来的调试器可以把读取位置停在任意事件边界,观察当时的 Agent 状态空间。

这张图从中间开始读:Runtime Event Log是稳定事实,其余节点是可以演进、重建或替换的派生视图。事件生产路径被刻意省略,由后文解释。

这与普通 application log 有本质区别。普通日志通常是在业务执行之后描述"代码做过什么",主要供人排障;而 RuntimeEvent 本身就是业务语义的一部分。用户消息、模型回复、thinking、function call、function response、权限动作、usage 和 terminal status 都以强类型事实进入日志。删除这份日志,系统就失去了可靠重建交互状态的基础。

一条 RuntimeEvent 保存了什么

RuntimeEvent不只是role + text。它把一条事实拆成几组正交信息维度:

维度关键字段意义
IdentitysessionId,invocationId,runId,turnId,branch这条事实属于哪段会话、调用、执行尝试和分支
Orderingid,ts与 ledger 顺序这条事实在因果历史中的位置
Sourcerole,author它在模型历史中扮演什么角色,由谁产生
Contenttext, thinking, function call/response, errorAI 交互本身的语义内容
Actionsstate delta, permission, artifact, usage, end invocation它要求 Runtime 怎样改变控制状态或记录副作用
Correlationtool call、provider event、step 与 artifact refs怎样把跨系统的同一件事重新配对
Lifecyclepartial,status它是可替换的流式片段、持久事实还是终止事实

在源码层面,canonical 契约定义于 packages/core/src/runtime-event.ts。其核心接口RuntimeEvent(见该文件第 665-695 行)包含:

  • 身份字段id(事件 UUID,用于重连/重放时的去重)、invocationId(持久的调用脊线 ID,聚合一次请求的所有 run/turn)、runId(持久的操作运行身份)、sessionIdturnId(聚合一次 Agent Turn 的所有事件)、ts(Unix 毫秒时间戳)、branch(可选,为未来多 Agent 树预留的代理泳道);
  • 来源字段role(在模型历史中的泳道,取值为user/model/tool/system四种,对应RUNTIME_EVENT_ROLES,见第 115 行)、author(产生该事实的子系统)、origin(执行面,旧版账本上可能缺失)、modelVisibility(显式的 provider 历史策略,缺省时按旧版兼容视为可见);
  • 生命周期字段partial(布尔值,true标记可被后续事件取代的瞬时流式分块)、status(对 invocation/turn 的生命周期断言,普通 in-flight 内容事件上省略);
  • 语义负载content(文本/thinking/function call/function response/error 等类型化内容)、actions(state delta、权限、artifact、usage、end invocation 等控制状态变更)、refs(工具调用、provider 事件、step、artifact 的关联引用)。

一个关键的设计约束是:partial: true的瞬时分块(流式文本、进度)会被后续 non-partial 事件取代;模型历史 MUST 排除 partial 事件(源码注释见第 661-663 行)。这保证了下一轮模型请求永远不会把不完整的流式片段当作事实。

这种结构保留的不是 UI 已经格式化好的聊天文本,而是模型交互的原始语义。尤其是:

  • thinking 可以携带 provider 要求的 signature(RuntimeEventThinkingContentsignature字段);
  • tool call 与 tool result 通过稳定 ID 配对,tool call 还能指向它所属的 assistant step;
  • sandbox boundary 请求与决定是 action,而不是一段伪装成聊天的文字;
  • terminal event 明确关闭一次 Run,而不是靠"最后一条消息看起来像回答"来猜测。

因此,模型历史不需要从 UI transcript 反向解析。它可以从 RuntimeEvent 中选择 non-partial、model-visible 的事件,保持顺序,再根据 provider 能力物化成 text-only 或 provider-native messages。UI 同样不需要成为事实源,它只是另一种 projection。

"回放状态空间"究竟意味着什么

这里的 replay 有三个层次,需要精确区分。

语义回放:当前已经成立

给定 RuntimeEvent ledger,Maka 可以重建用户/模型文本、thinking、工具调用与结果、权限动作、usage 和 terminal fact。下一轮模型历史与 completed Session read model 都已经优先从这份 ledger 构造。

这意味着我们能回答:在某个事件边界之前,模型已经看到了哪些交互?它调用过什么工具?工具返回了什么?哪些权限被请求或决定?一次 Run 是否已经结束?

Provider-native 回放:有能力门控

不同 provider 对 tool history 和 signed thinking 的要求不同。Maka 不会把所有事件盲目塞回模型,而是先建立 replay plan:检查 partial、tool call/result 配对、step ID、thinking signature 和 provider 支持,再决定走 provider-native、text-only,还是明确降级路径。

可回放不等于"把 JSONL 原样发给任何模型"。它意味着保留足够丰富的事实,让投影器能够为具体 provider 生成合法的历史,同时显式报告语义损失。

Bit-exact wire replay:不能只靠 message log 宣称

RuntimeEvent 保存了 AI 交互的 canonical message semantics,但当前并不等于每次 provider HTTP 请求的原始字节级快照。System prompt、工具 schema、provider options、模型实现版本以及 context selection/compaction policy 仍参与最终 request 的生成;当前系统只记录了其中一些 identity、diagnostic 和 hash,并没有把整个 wire request 复制进 RuntimeEvent ledger。

因此,"状态空间回放"首先是交互语义与 Runtime 状态的可重建性。如果未来要承诺 bit-exact deterministic replay,还需要对运行配置、prompt、tool catalog、投影策略和 provider request shape 做版本化或快照化。这不是削弱 Event Log 的价值,反而说明它提供了正确的基础:message facts 保持稳定,request materialization 可以独立演进。

两条思想来源

这个设计有两条明确的思想脉络。

第一条来自 Google ADK。ADK 把 Session 看作带有时间顺序 Events 的事实容器;Event 同时承载 content、author、invocation identity、partial 标记与 actions;Session state 通过事件中的 state delta 更新,模型工作上下文则从事件历史选择和转换得到。Maka 借鉴的关键不是字段长得相似,而是更深层的原则:Session history 是事实,working context 是计算出来的 projection。

第二条来自分布式数据系统中的 log-first 思想。数据库 WAL、replicated log、event sourcing 和 Kafka 共享一个重要直觉:不要把每个下游视图都当作独立真相;先保存有序、不含糊的变化事实,再让消费者重建自己的状态。只要事件顺序和提交边界可信,缓存、索引、搜索视图乃至部分损坏的状态表都可以重新生成。

Maka 并不是在进程内实现了 Kafka,也没有声称 RuntimeEventStore 是一个分布式共识日志。借鉴的是更基础的设计原则:

Log is the source of truth; state is a materialized view.

这一原则直接解释了后文最重要的 terminal invariant:除了这次 Run 自己的 terminal RuntimeEvent,没有别的东西能宣布它结束。

三种生命周期身份,加一个关联字段

理解主链之前,需要先分清三个经常被口语化混用的生命周期概念。

概念它回答的问题当前实现中的身份
Session这些对话和运行属于哪段长期交互?sessionId
Turn用户界面中的这一轮问答是哪一轮?turnId
Run这一次具体执行尝试是谁?状态是什么?runId/AgentRun

RuntimeEvent 仍保留invocationId作为兼容与事件关联字段。从源码看(packages/core/src/runtime-event.ts 第 654 行),Phase 0-3 身份契约是"一个invocationId对应一个runId":Invocation 标识 provider/工具执行,Run 标识其持久的操作账本。生产主链把invocationId绑定到 Run 身份;它不再对应单独的 Invocation 生命周期对象或 Runner 层。

这里最重要的判断是:Turn 不是 Run,聊天消息也不是执行状态。一个用户可见的回合需要一个系统可追踪的执行封套;否则,系统只能知道"出现过一些消息",却无法可靠回答"这次执行是否真正结束"。

围绕 Event Log 运转的执行主链

所有 hosted execution 路径共用下面这条 Runtime 主链:

从左向右读这张图:越靠左越接近产品入口和长期 Session;越靠右越接近一次 provider 请求和具体工具副作用。存储投影和事件账本被省略,后文单独解释。

这不是为了把一个函数拆成很多类。更准确地说,这些组件分担了 Event Log 的生产、规范化、提交和消费责任;每一层都在保护一种不同的稳定性。

SessionManager:稳定的产品入口

SessionManager.sendMessage()是外部调用者看到的门面。它现在很薄:读取和管理 Session 的公共能力保留在这里,真正的执行直接委托给RuntimeKernel.startTurn()RuntimeKernelstartTurn签名见 packages/runtime/src/runtime-kernel.ts 第 167 行,实现见第 664 行)。

这个边界让桌面端、CLI、Bot 和 Eval 调用者通过 Runtime Host 工作,不必理解 Run ledger、Flow 或 terminal fact。Runtime 内部可以在 Host 协议后演进。

RuntimeKernel:活跃执行的控制面

RuntimeKernel把一条 Session 请求组织成一次可运行的 Run。它负责:

  • 创建AgentRun
  • 创建或复用绑定到 Session 的 Backend;
  • 注册活跃 Run,并维护turnId → runId映射;
  • 把停止和权限响应路由到正在运行的 Backend;
  • 驱动 Backend 事件流并映射 RuntimeEvent;
  • 统一处理 abort 路由、单终态(terminal coalescing)、终态后静默排空(silent post-terminal drain)和缺失终态失败(missing-terminal failure);
  • 让 RuntimeEvent 落盘后,再把原有SessionEvent流交还调用者;
  • 在 Backend 流收尾时确保AgentRun.finalize()被执行。

它是 orchestration boundary,而不是模型循环本身。Backend 不应该负责"这个 Session 目前有哪些活跃 Run",产品入口也不应该负责"终止 RuntimeEvent 是否已经持久化"。这些跨层协调都收敛在 Kernel。

AgentRun:一次执行的 Durable Envelope

AgentRun让一次执行在持久世界里有身份和生命周期。开始运行时,它会:

  1. 把这次 invocation 的开场事实(opening fact)作为 RuntimeEvent 提交;
  2. 对顶层 Run 写入用户消息和runningTurn 投影;
  3. 写入本轮初始用户RuntimeEvent
  4. 锁定本 Session 的连接配置;
  5. 确保 Backend 已创建并注册为活跃 Run;
  6. 从此前的 RuntimeEvent ledger 构造模型历史。

运行过程中,AgentRun同时接收旧的SessionEvent与新的RuntimeEvent,并把它们写入各自所属的投影或账本。结束时,它注销活跃 Run、收敛 Session/Turn 状态,并提交最终 Run 状态。

可以把AgentRun理解成一次执行的"耐久封套":它不决定模型下一步调用哪个工具,但它保证这次执行是谁、发生了什么、最后以什么状态结束。

RuntimeKernel:一个执行所有者,一套终态协议

Runtime 不再在AgentRunAgentBackend之间插入通用 Runner/Flow 壳。RuntimeKernel直接拥有生产主链的协议:

  • AgentRun.begin()在 Backend dispatch 前持久化初始用户 RuntimeEvent;
  • AgentBackend.send()前重新校验精确的活跃 Run;
  • abort signal 路由到绑定具体 Backend generation 的 stop 函数;
  • Backend 事件先映射并由AgentRun耐久接收,再作为SessionEvent暴露;
  • 第一个被接受的 terminal fact 获胜,随后静默排空 Backend 流;
  • Backend 抛错或流结束时缺少终态,都会收敛成结构化失败;
  • durable continuation 消费一次性 start-admission proof,并且不伪造新的 user event。

这些规则是生产生命周期不变量,所以放在生产所有者(RuntimeKernel)中,让调用图和权威边界显式化。

SessionEvent Runtime mapper:旧事件流与 Runtime 事实之间的桥

当前模型/工具循环仍然由AgentBackend.send()产生 renderer-facingSessionEvent。packages/runtime/src/session-event-runtime-mapper.ts 中的mapSessionEventToRuntimeEvent()把被接受的事件逐个映射成 canonicalRuntimeEvent。源码第 104-128 行的注释完整列出了映射规则:

  • 模型文本和 thinking →role 'model',author 'agent'
  • tool_start(function call)→role 'model',author 'agent'
  • 工具进度/输出 delta →role 'tool',author 'tool'(partial);
  • tool_result(function response)→role 'tool',author 'tool'
  • sandbox_boundary_requestrole 'system',author 'system'
  • sandbox_boundary_decision_ackrole 'system',author 'user'(人在系统泳道里的决定);
  • token_usage→ runtime action(role 'system',author 'system');
  • error、abort、complete → 明确的失败或终止事实(terminal)。

从源码注释(第 108 行)可以确认:mapper 是确定性的(给定(event, ctx, memory)结果唯一),且不携带任何 I/O——streaming、stop、dispose 或 admission 都不属于它。memory会在tool_start时记录toolName,供tool_result读取,从而在流式过程中保持一致的工具调用配对关系。生命周期决策全部属于RuntimeKernel,mapper 只负责 Backend 事件入口的词汇转换。

AgentBackend:模型与工具循环真正发生的地方

对默认的AiSdkBackend来说,核心循环仍在send()内部。它会:

  1. 解析模型并准备本轮可见的工具集合;
  2. 从 RuntimeEvent 历史构造 provider messages,并应用上下文预算(context-budget)策略;
  3. 组合 system prompt、当前用户输入和附件;
  4. 通过ModelAdapter启动 AI SDKstreamText()
  5. 读取 text、thinking、step boundary、finish reason 和 usage;
  6. 让 AI SDK 在模型发起 tool call 时进入ToolRuntime
  7. 将工具结果交回下一步模型请求;
  8. 重复模型/工具 step,直到模型结束、达到上限、发生错误或被中止。

step 上限是可选的maxStepsundefined时不设限,由模型自己决定何时收尾。若设置了上限而模型在上限处仍要求继续调用工具,Runtime 会保留已经产生的工具结果,并在没有最终文本时补充一条确定性的提示,让用户可以在新 Turn 中继续,而不是留下一个没有收尾文本的界面——UI 不会出现一条解释不清的孤立工具行。

ModelAdapter(packages/runtime/src/model-adapter.ts)隔离 provider 与 AI SDK 差异:模型创建、stream 启动、chunk 归一化、usage 归一化和错误分类。ToolRuntime(packages/runtime/src/tool-runtime.ts)则隔离工具执行的高风险部分:工具可用性防守、权限、超时/中止传递、重复失败拦截、工具输出、遥测和 artifact 记录。

模型循环是怎样向前推进的

下面这张时序图聚焦一次包含工具调用的正常运行,展示控制权如何在模型与工具之间转移:

AI SDK 的 step 是这个循环的自然节拍。Maka 会按 step 持久化 assistant text/thinking,而不是把整个 Turn 压成一条最终 assistant message。工具调用会携带对应的 step ID,使 thinking、文本和工具调用在 replay 时仍能重新组成原来的执行顺序。

工具运行期间,模型 provider 不会继续输出。ToolRuntime会暂停模型流的 idle watchdog,因为这段安静是预期行为;具体工具仍然可以有自己的超时,而更外层的 Run 或评测系统继续充当最终 backstop。

权限不是弹窗,而是 Runtime 控制流

当一次工具调用越过当前沙箱边界时,工具不会静默失败,Runtime 也不会由 UI 自行弹一个对话框把执行挂起。流程是这样的:

  1. 工具返回一个带sandbox_boundary_required与具体expansion的失败结果;
  2. 模型据此调用request_sandbox_boundary发起一次边界扩张请求
  3. 运行停在一个有身份(identity)的位置上等待答复。

等待期间,Session 投影为waiting_for_user,但 Run 仍然保留自己的执行身份——它没有结束,只是停住了(parked)。用户的决定通过RuntimeKernel.respondToSandboxBoundary()路由回当前 Backend(实现见 packages/runtime/src/runtime-kernel.ts 第 2126 行);同一条路径上还有respondToUserQuestion()(第 2155 行),用于模型主动向用户提问的场景。两类请求在未答复期间都由RuntimeKernel登记为 active interaction,因此一个错过了实时事件的界面可以重新拉取待答项,而不会把运行晾在那里。

这个设计的关键是:权限不是 UI 自己暂停了一下。请求和决定都作为带类型的事实进入 Runtime 事实模型——它们没有content,而是带actions.stateDelta的系统事件。因此重放、诊断和恢复都能解释运行为什么停住,以及控制权如何回来。决定那条事实的rolesystemauthoruser:它在模型历史里属于系统泳道,但产生它的是人,这一对正交字段把这件事表达出来,而不需要把用户决定伪装成一条聊天消息。

此外,RuntimeKernel在映射和持久化前丢弃permission_request/permission_answer_ack/permission_closure_ack/permission_decision_ack这组遗留词汇:它们已被 sandbox boundary 事件取代,不能再成为活的运行时事实。这一点在 mapper 源码中同样得到印证——session-event-runtime-mapper.ts 第 143-144 行会直接抛出"is a legacy permission event and is not backend-mappable"

一份语义事实,两类辅助状态

Maka 当前同时维护三类持久数据。它们不是三个地位相同的"真相",也不是重复保存同一份聊天。RuntimeEventStore是 AI 交互的 canonical semantic log;另外两类存储承担产品投影,以及 Runtime 做过什么的运维记录。

存储主要内容它最适合回答的问题
SessionStore用户、assistant、工具和 turn-state 等StoredMessageUI 与兼容接口要展示什么?活跃流有哪些即时投影?
AgentRunStoreoperational Run events这次 Run 在哪个模型或工具阶段做了什么、又在哪里失败?
RuntimeEventStorecanonical RuntimeEvent 与有界 partial snapshotsAgent 交互发生过哪些语义事实,其他状态应如何重建?

当前实现由SQLite承载,而不是每个 Run 一个目录:AgentRunStoreRuntimeEventStore都建立在同一份 operational state 数据库之上,RuntimeEvents 落在runtime_events表。建表语句见 packages/storage/src/sqlite-runtime-schema.ts 第 49-60 行:

CREATE TABLE runtime_events ( event_id TEXT PRIMARY KEY, session_id TEXT NOT NULL, invocation_id TEXT NOT NULL, run_id TEXT NOT NULL, turn_id TEXT NOT NULL, event_seq INTEGER NOT NULL CHECK (event_seq > 0), event_kind TEXT NOT NULL, payload_json TEXT NOT NULL, committed_at INTEGER NOT NULL, UNIQUE (invocation_id, event_seq) );

顺序由该表的event_seq承担,并以(invocation_id, event_seq)唯一约束保证一次 invocation 内序号不重复——"有序日志"在存储层就是这条约束。四个身份(session/turn/run/invocation)各占一列,因此"这个 Session 的这次 Run 的这个 Turn 发生了什么"是一次索引查询(runtime_events_by_run索引按(session_id, run_id, event_seq)建立)。存储层还有一个值得注意的约束:CREATE UNIQUE INDEX runtime_events_one_opening_per_invocation ON runtime_events(invocation_id) WHERE event_kind = 'invocation_opened'(第 505-507 行)——一次 invocation 只有一个开场事实,为恢复路径提供了可靠的锚点。

AgentRunStore的事件更像 operational index:从 packages/core/src/agent-run.ts 第 43-90 行的AGENT_RUN_EVENT_TYPES可以看到这些操作事件的完整清单——model_stream_startedmodel_stream_completedtool_startedtool_completedtool_failedpermission_requestedpermission_decidedsandbox_escalation_requestedabort_requested等。它们帮助快速诊断和管理 Run,但不替代模型交互日志。RuntimeEventStore的事件才是可重建的语义事实:用户内容、模型内容、function call/response、边界与提问动作,以及 terminal fact。

对于已经完成且 ledger 完整的 Run,读取与下一轮模型 replay 优先依赖 RuntimeEvent。SessionStore仍然承担兼容投影和 in-flight 展示,不能简单删除;但它不再是 completed runtime 语义的唯一权威。

流式 text/thinking partial不会被无限追加进账本。RuntimeEventStore 为可替换的流维护有界 partial snapshot,最终 non-partial 事件到达后覆盖其语义位置。这既保留崩溃时已经展示的部分输出,也避免 10,000 个 delta 变成 10,000 条长期账本记录。

Log-first 的关键不变量:先有终止事实,再提交终止状态

Runtime 最容易出现的一类故障,是不同存储对"是否结束"给出不同答案。例如:

  • 用户已经 stop,但迟到的 complete 又把 Session 写回 active;
  • Backend 流耗尽,却从未说明它是成功还是失败;
  • 已经结束的 Run,又有第二个写入方想再结束它一次。

Maka 当前保护的核心不变量是:

一个 Run 只结束一次,而它的 terminal RuntimeEvent 是唯一说它结束了的事实。

因为结果没有第二份记录要同步,崩溃也就不可能留下一份说"已完成"、另一份说"还在跑"的状态。具体到实现:

  • 没有终态的 Backend stream 会被合成为missing_terminal_event失败——runtime-kernel.ts 第 1867-1892 行的missingTerminalSessionEvents()在 Backend 耗尽且没有 terminal RuntimeEvent 时,先发出code: 'missing_terminal_event'的 error 事件,再发出stopReason: 'error'的 complete 事件;
  • 重复终态由 Kernel 合并(第一个被接受的 terminal fact 获胜);
  • 状态不匹配、来自其他 Run 或标记为partial: true的 terminal event 都会被拒绝。

这条不变量让恢复不必"猜模型当时准备做什么"。系统只需要判断哪些事实已经 durable,然后把各个投影收敛到同一个可解释终态。

Stop、错误与崩溃如何收敛

用户停止

RuntimeKernel.stopSession()会先把所有活跃AgentRun标记为 stopped,再调用 Backend 的stop()。从 runtime-kernel.ts 第 1894 行的实现可以看到:它先为每个 execution 设置stopIntent、调用execution.run?.stop(...),再触发abortController.abort(...),最后执行stopSessionAttempt()完成收敛。AiSdkBackend会中止 provider stream、结束正在等待的 sandbox boundary 或用户提问,并产生 abort/complete 事件。即使 provider 随后发送迟到的 complete 或 error,RuntimeKernelAgentRun也不会允许它覆盖已经确定的 aborted 语义。停止来源(例如 renderer stop button)会进入 terminal fact,供诊断使用。

Provider 或 Runtime 错误

错误首先被规范化为非终止 error content,随后由 failed terminal event 关闭 Run。RuntimeKernel不允许后续的 completed event 掩盖之前已经观察到的错误。若 Backend 直接抛出异常或流没有终态,Kernel 会产生结构化失败,而不是留下悬空运行。

应用崩溃和启动恢复

启动恢复不会重新执行模型请求或工具副作用。它扫描非终止 Run 与 RuntimeEvent ledger,识别 stale model stream、tool tail、未答复的 interaction、损坏的 operational event 等情况,然后保守地提交失败或取消状态,并修复 Session/Turn 投影。

这是"状态修复",不是 checkpoint resume:它保留已经产生的部分输出,把状态收敛到一个可解释的终态,但不会在进程重启后自动从某个工具调用的下一行继续执行。

继续执行是另一条路径。safe_boundary_continuation从一个经过校验的安全边界接着跑——边界之前的事实全部可信,边界之后的不予采信。它同样不从被打断的那一行开始,区别在于它有一个可验证的起点,而崩溃收敛只是给一次执行下结论。两者差异详见 runtime-resume-architecture.zh-CN.md。

这套设计换来了什么,又付出了什么

得到的能力

  • 产品入口不依赖具体 provider 或工具循环实现;
  • 不同 Backend 通过 Kernel 共享 Run 与 terminal 语义;
  • UI 事件与模型可重放事实被明确区分;
  • 用户停止、权限和工具副作用进入可诊断的控制流;
  • 崩溃后可以依据 durable facts 收敛状态;
  • Runtime Host client、子 Agent 和调度器复用同一执行核心。

当前代价

  • 迁移期同时存在SessionEventStoredMessageRuntimeEvent和 operational Run events,事件映射的维护成本较高;
  • AiSdkBackend仍然很重,同时组织 history、context budget、tool availability、step loop、usage 与 telemetry;
  • SessionEvent Runtime mapper仍承担 legacy-to-canonical adapter 角色,而不是 Backend 原生产 canonical events;
  • SessionStore与 RuntimeEvent projection 需要在 active/in-flight 场景中协同;
  • 启动恢复是确定性终结与修复,不是从任意位置热续跑。续跑走另一条路:safe_boundary_continuation从一个经过校验的安全边界接着跑,它在 invocation 开场事实里记着自己的续跑来源,由RuntimeKernel完成准入和 dispatch。

这些不是应该隐藏的实现细节,而是当前架构的真实边界。未来拆分 Backend 或加入 checkpoint 时,首要目标不是减少文件行数,而是保持 request shape、工具可见性、事件顺序和 terminal invariant 不变。

代码阅读地图

建议按照下面的顺序阅读当前实现(均为仓库内真实路径):

  1. packages/runtime/src/session-manager.ts:公共入口与恢复入口;
  2. packages/runtime/src/runtime-kernel.ts:Run/Backend 的活跃控制与主链组装;
  3. packages/runtime/src/agent-run.ts:Durable lifecycle、历史构造和终态提交;
  4. packages/runtime/src/session-event-runtime-mapper.ts:纯SessionEvent → RuntimeEvent映射;
  5. packages/runtime/src/ai-sdk-backend.ts:AI SDK 模型/工具 step loop;
  6. packages/runtime/src/model-adapter.ts:provider stream 适配;
  7. packages/runtime/src/tool-runtime.ts:沙箱边界、工具执行和副作用边界;
  8. packages/core/src/runtime-event.ts:canonical RuntimeEvent 契约;
  9. packages/core/src/agent-run.ts 与 packages/storage/src/agent-run-store.ts:Run 与 RuntimeEvent 的账本契约与 SQLite 实现;SQLite 建表细节见 packages/storage/src/sqlite-runtime-schema.ts。

对应的关键测试集中在(均为仓库内真实路径):

  • packages/runtime/src/tests/session-event-runtime-mapper.test.ts
  • packages/runtime/src/tests/session-manager.test.ts
  • packages/runtime/src/tests/session-manager-terminal-ledger.test.ts
  • packages/runtime/src/tests/runtime-ledger-repair.test.ts

小结

Maka Runtime 的核心不在某一个类,也不只是 AI SDK 的多步工具循环。核心是一个可以保留并回放 Agent 交互状态空间的Runtime Event Log;执行协议围绕这份日志产生事实、提交事实并构造派生状态:

model/tool stepping engine → canonical RuntimeEvents → durable semantic log → model history / UI / Run state / recovery projections

SessionManager稳住入口,RuntimeKernel管理活跃执行与终态协议,AgentRun提交耐久事实,SessionEvent Runtime mapper把 Backend 事件翻译成 canonical facts,而AiSdkBackendModelAdapterToolRuntime真正推进模型和工具循环。它们都围绕 Runtime Event Log 协作,而不是各自保存一份局部真相。

这套结构最终保护的是一件很朴素的事:无论一次 Agent 工作经历多少模型 step、工具副作用、等待用户答复和异常,Maka 都要先忠实记录发生过什么。只要这份有序事实仍在,系统就能重新构造当时的交互状态,生成新的视图,并让下一轮从可信历史继续。

【免费下载链接】makaApache Maka (Incubating) is a high-performance agent workspace that keeps a complete record of everything it did.项目地址: https://gitcode.com/GitHub_Trending/mak/maka

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询