本文系统分享了关于 agent 开发的思考,全文近 一万字,阅读需要 15 分钟,对于想要学习 agent 开发的同学也许大有裨益。我开源了一个极简 Agent 框架,叫 Kiso 。
最初其实来源于一个很朴素的念头:我能不能做出一个比 Pi 更简单、更稳定、更省 token 的 Agent 框架。对于一个多少有点技术理想的 Agent 研究者来说,如果最后没能在开源 Agent 这件事上留下点自己的痕迹,实在是手痒难耐。
所以即便是重复造轮子,我也便造了。但“简单、稳定、省 token”是一开始写在草稿纸上的三个愿望。
而愿望和工程现实之间,隔着巨大的鸿沟。一旦真正去拆解像 Pi 这样的参考实现,就会发现它已经把“极简”逼到了一个很难继续压缩的位置:默认工具极少,系统提示词非常克制,Loop 也没有多少多余的控制逻辑。
在这样的前提下,如果所谓创新只是再少两个工具、再少几百个 prompt token ,工程空间其实已经非常有限。做着做着,我发现自己真正想解决的问题变了。
我开始意识到,一个 Agent 真正难以被托付的原因,往往并不是它不会调用某个工具,也不是缺少 Memory 、Plan 、Multi-Agent 之类的组件。模型能力会继续上涨,这些组件也会不断变化,但只要 Agent 开始通过文件系统、Shell 、Git 、浏览器或者网络接口真正改变外部世界,就会出现一个更底层的问题: 当一个概率模型开始改变现实以后,系统凭什么知道什么真的发生过?
这个问题后来几乎决定了 Kiso 的整个方向。我最开始也试图给 Agent 下一个很“第一性原理”的定义,比如:Agent 是一个在不完全信息下,通过持续观察世界、采取行动、读取反馈,让现实状态逐渐向目标状态收敛的闭环系统。
后来我觉得,这个定义作为思考工具有用,但没必要把文章变成一篇“什么才是真正 Agent”的理论争论。 Kiso 也不需要证明一个适用于所有 Agent 的终极定义。
它只需要关心一类非常具体的系统: 一个会读取外部世界、会调用有副作用的工具、而且可能长时间运行甚至中途崩溃的 Agent 。把范围缩到这里以后,有几件事情几乎无法绕开: Observed ≠ Current Intent ≠ Effect Started ≠ Succeeded Process Completion ≠ Goal Satisfaction Memory ≠ Durable Fact 模型看见过,不代表现实现在还是那个样子;模型产生了一个工具意图,不代表外部世界已经发生变化;一个操作开始了,不代表它已经成功;整个 Loop 正常结束,也不代表真正的任务目标已经被满足;而进程此刻记得的东西,只要还没有成为 durable state ,崩溃之后就没有资格继续被当作事实。
所以做到后来,我越来越觉得 Kiso 虽然对外仍然可以叫一个 Agent 框架,但它真正想守住的是更底层的一层东西。严格一点说,它越来越像一个 Agent Runtime 。
我给它最后收敛出来的分工只有一句话: 模型负责扩大系统能够处理的问题空间,而 Runtime 负责限制什么才有资格被称为“事实”。这就是 Kiso 后面绝大部分工程决定的起点。
- 控制流闭环,不等于现实闭环 最常见的 Agent Loop 其实非常简单:模型接收上下文,生成工具调用; Runtime 执行工具,把结果重新放回上下文;模型继续推理,直到最后停止。从程序控制流来看,箭头已经漂亮地绕了一圈,又回到了模型,看上去闭环成立了。
但只要真正写过 Coding Agent ,并且让它跑过稍微长一点的任务,就会发现: 控制流闭环,不代表现实闭环。因为模型面对的并不是一个静态状态机。
它面对的是一个持续变化的外部世界。文件可能被人类同时修改,Git 分支可能发生变化,Shell 命令可能执行到一半,HTTP 请求可能已经发出去但响应还没有回来,进程更可能在任何一个 CPU 指令之间被 kill -9 。
这里面其实存在几道完全不同的语义边界。第一道是认识论边界。
模型所有输入,本质上都只是现实世界某个历史切片的投影。即使今天给它 100 万 token 的上下文,它能够保存的仍然只是“过去看见过什么”,而不是一个正在持续变化的现实本身。
几十秒前读过的文件,可能已经被 IDE 、Git 、后台进程或者另一个 Agent 改过了。第二道是因果边界。
模型流里出现一个 tool_use ,只代表它提出了一个行动意图。从这个 token 被生成出来,到磁盘真正写入一个字节,中间还隔着完整的 Runtime 、权限判断、工具执行以及操作系统。
把“模型说要做”直接等同于“现实已经做了”,在正常情况下可能看不出问题,一旦进入崩溃恢复,整个因果关系就会立刻变得混乱。第三道是持久化边界。
进程当前知道一个工具已经执行成功,并不意味着重新启动以后还能证明这件事。如果这份确定性只存在于一个 Promise 、一个对象字段或者调用栈里,断电之后,它就跟从来没有存在过一样。
最后还有评价边界。一个工具返回成功,只能说明局部系统调用完成了。
write_file 成功不代表代码正确, npm test 返回 0 也不总能证明用户真正想解决的问题已经被解决。最终目标是否完成,需要来自新的观察、测试、验收或者人类判断,而不是让负责行动的模型自己顺手宣布“任务完成”。
这几道边界看起来很普通,但它们最后几乎决定了 Kiso 的整体结构。因为接受这些区别以后,就不能再把 Agent Runtime 理解成一个负责“LLM ↔ Tool”的 while loop 。
它必须维护的是一条能够在崩溃以后仍然解释得清楚的因果链: Model Intent ↓ Turn Commit ↓ Authorization ↓ Durable STARTED ↓ Real-world Effect ↓ Durable Receipt ↓ Observation / Verification 模型输出当然可以是概率的,但从什么时候开始允许现实发生变化,到现实发生变化以后系统究竟知道了什么,这些边界必须尽量确定。 02. Fail-Stop 中断与执行账本 这套思路遇到的第一个问题,就是进程崩溃。
假设 Agent 已经修改完第一个文件,接下来开始执行一条耗时的 Shell 命令。命令刚运行到一半,用户关掉终端,机器突然掉电,OOM Killer 杀掉进程,或者我们干脆在外面直接给它一个 SIGKILL 。
重新启动以后,它应该从哪里继续?很多轻量 Agent 框架保存的是一份模型交互历史:User 、Assistant 、Tool Call 、Tool Result 。
正常使用当然没有问题,但现实世界恰好存在一个非常麻烦的窗口: Tool Call ↓ 真实副作用正在发生 ↓ Tool Result 如果进程刚好死在中间,磁盘上可能只有“我要执行这个操作”,没有最终结果。但外部世界并不是一个可以整体回滚的数据库事务。
文件可能已经写完了,Git commit 可能已经产生了,远程 HTTP 请求也可能已经被服务端接受。这个时候如果 Runtime 简单地自动重试,可能制造第二次副作用;如果直接认为它已经成功,又可能建立在一个根本没有发生过的现实之上。
所以 Kiso 没有把“当前内存里的 Session State”作为最终真相,而是把整个 Session 建立在一条只追加的 Event Log 上。对未来判断有影响的状态,都要通过事件落到磁盘。
一个真正带副作用的工具执行,大致经历这样一条链: 模型完成一轮输出 ↓ Durable Turn Commit ↓ 权限检查 ↓ durable tool_execution_started ↓ 执行真实副作用 ↓ durable succeeded / failed receipt ↓ 把结果投影给模型 这里面有两个我认为非常重要的边界。第一个是 Turn Commit 。
模型在流式生成的时候,可能先吐出半个工具调用,后面又产生非法内容、Provider 中途断流,甚至整轮 response 最终被判定为不完整。如果前半段的 Tool Call 一出现,Runtime 就立即开始改文件,那么后面即使整轮模型输出被判为无效,现实已经被改变了。
所以 Kiso 把“模型提出一个工具调用”和“这个工具调用获得执行资格”分开。只有整个 response cleanly exhausted ,并且得到了兼容的终止语义之后,这一轮才被 Durable Turn Commit 。
Commit 以前的一切都只是一份 draft 。第二个边界是 Persist Before Effect 。
真正发生副作用以前, tool_execution_started 必须先持久化;副作用结束以后,再持久化对应的 receipt 。这样一来,进程不管死在哪里,恢复时至少可以非常清楚地知道自己掌握了哪些证据:没有 commit ,说明这一轮模型输出还没有获得行动资格;有 commit 但没有 STARTED ,说明这个工具还没有真正开始; STARTED 和 receipt 都存在,结果已经确定。
真正无法从本地证明的只有一种情况: durable STARTED ↓ 真实世界可能已经发生变化 X ← crash ↓ durable Receipt 这就是最麻烦的 Unknown Effect 。 Kiso 不猜。
它会把这个 execution 标记成 uncertain ,恢复流程停在这里,让人明确决定这一次操作应该 rerun 还是 abandon 。只有这个悬空的因果关系被处理以后,原来的 trajectory 才继续往后推进。
我给它定的原则很简单: Ambiguity Never Auto-Repeats 。不确定的副作用永远不自动重试。
这里还有一个我非常喜欢的设计:Kiso 没有为了 Recovery 再维护第二份特殊状态机。磁盘上只有 Event Log ,Execution Ledger 、当前 Session State 、Recovery Plan 都只是它的 projection 。
恢复逻辑本身也是一个纯函数:给它一串 durable events ,它推导出当前唯一安全的下一步——废弃未提交的 draft 、等待权限、执行已经提交但尚未开始的调用、处理 uncertain operation ,或者继续模型调用。换句话说: The Event Log is the truth. Everything else is a projection. 我专门为这件事情写了真实的 kill -9 测试。
测试不是 mock 一个 crash flag ,而是真的启动一个 PTY 子进程,让 Agent 先完成文件修改,再开始一条慢 Shell 命令,然后对整个进程组发 SIGKILL 。重新执行 kiso resume 以后,它必须能够证明前面的文件修改已经完成,被硬杀掉的那次 execution 处于 uncertain ;等人明确选择重新执行以后,再沿着原来的轨迹继续,而不是把整段 Session 当成一段聊天记录重新讲一个看起来合理的新故事。
做到这里以后,我才开始觉得 resume 这个词在 Agent 里有了真正的工程含义。 03. Context 不是事实:1M 上下文下的缓存与压缩 Event Log 确定以后,另外三样混在一起的东西也跟着分开了:对话历史、Context ,和 Agent State 。
以前我很容易把 Conversation History 、Context 和 Agent State 混成同一件事情。上下文快满了,就删一点工具输出;再满一点,就摘要;摘要之后继续把新的消息往后堆。
但如果 Event Log 才是真实发生过的 durable history ,那么模型每次看到的 Context 其实只是一种临时 representation 。 Context 不是事实本身,而是 Runtime 从事实中为下一轮模型调用生成的一次投影。
这个区别非常重要。它意味着压缩 Context 并不等于篡改历史。
Event Log 仍然保存实际发生过的东西,Runtime 可以根据当前模型、窗口大小、任务阶段和成本约束,把同一份历史投影成不同形式。问题也就从“我要不要删历史”变成了: 如何在尽量保留推理连续性的前提下,用合理的成本,把 durable history 投影成下一轮模型最需要的 Context ?
我一开始参考了行业里很自然的一种两阶段做法:Context 到 50% 左右先做一次 microcompact ,把又旧又大的 Tool Result 剪掉一些;真正快到窗口顶部时,再进行 Summary Compaction 。这个设计从 token 数量上看非常合理:先轻度修剪,再深度总结,怎么看都应该更省钱。
但真正把 Prompt Cache 算进去以后,结论反过来了。像 DeepSeek 这类 Prompt Cache 命中与未命中价格差异非常大的模型,只要整个会话历史保持 Append-Only ,前面已经出现过的几十万 token 后续请求几乎都可以继续作为缓存前缀命中。
Context 虽然看起来越来越长,但其中很大一部分并不再按完整 Prefill 成本计费。而如果在历史中间突然删掉几个旧的 Tool Result ,虽然表面上 Context 少了几万 token ,但从第一个删除点往后,整个 token prefix 都发生了变化。
下一轮请求里,那些本来可以命中缓存的十几万甚至几十万 token ,就必须重新走 Miss 。也就是说,你可能只是为了省掉 3 万个旧 token ,却让后面仍然保留的 20 万 token 全部重新 Prefill 。
这个操作只有在未来还会继续运行足够多轮时,才可能把 cache break 的损失挣回来。如果任务再跑十轮就结束了,这种所谓“优化”反而会让账单更贵。
所以 Kiso 后来把固定比例触发的 microcompact 直接撤掉了。 Primitive 还保留,但必须先经过一个 break-even 判断:只有预估未来调用次数足够覆盖破坏缓存带来的额外成本,才值得动手。
不是 Context 一大,就习惯性“剪一刀”。真正的 Summary Compaction 则是另一套逻辑。
目前在 1M context window 下,Kiso 的 Soft 阈值封顶在 400K ,Hard 阈值封顶在 700K ,并且保留最多 100K 的 Raw Tail 。这里有一点我后来特意在 ADR 里写清楚: 400K 不是成本实验得到的最优值。
我们做过 threshold sweep ,如果只看账单,更早在 150K 、200K 或 300K 摘要,在一些工作负载下反而更便宜。 400K 是一个有意识的质量取舍:减少摘要发生次数,少做几次有损信息压缩,接受一部分额外 Context 成本。
Soft 也不是“到了 400K 就立即摘要”。到了这里只代表进入 Eligible 。
Runtime 会继续等一个自然的 phase boundary ,比如刚跑完一次测试、连续编辑第一次结束、长时间只读调研第一次开始进入修改。人类工程师也不会在脑容量刚好达到 50% 时立刻停下来写总结,我们通常是在“调查完了”“代码改完了”“测试刚跑过”这些有语义的边界上整理思路。
如果一直没有出现这样的边界,到了 Hard threshold 才强制处理。压缩时还会强制保留最近最多 100K token 的 Raw Tail 。
最近的报错、Tool Result 、刚刚发生的修改不经过二次摘要,直接原样保留。旧历史负责被压缩,最近推理链保持连续。
还有一个很关键的实现是 In-Band Summary 。很多压缩实现会把当前几十万 token 的历史重新序列化成一大段文本,再用另一套 Prompt 发起一个完全新的总结请求。
这样一来,Provider 看见的是一个新的 prefix ,之前已经积累的 Prompt Cache 基本全部作废。 Kiso 优先直接复用当前 session 已经存在的系统提示词和消息 prefix ,只在末尾追加总结指令。
对于 Provider 来说,前面几十万 token 仍然是完全相同的前缀,因此大部分都能够继续命中缓存。最后我对 Coding Agent 中 Context Engineering 的理解慢慢收敛成了四句话: Event Log = Truth Context = Projection Summary = Compressed Projection Prompt Cache = Projection 的经济属性 把这几层分开以后,很多以前纠缠在一起的问题一下子清楚了。
Event Log 负责“发生过什么”,Context 负责“下一轮模型需要看什么”,Summary 是有损投影,而 Cache 根本不是 Agent State ,它只是当前投影方式在 Provider 侧的成本属性。我觉得这个区别比“上下文到底应该 50% 还是 80% 压缩”重要得多。
- 工具设计:Shell 、Budgeted Read 与过期观察 Runtime 再严谨,最后还是要通过工具去接触现实。工具设计如果只是把几个现成函数包装成 JSON Schema ,很多问题会重新从这里漏回来。
Kiso 现在默认的 Coding 工具保持得很小: read_file 、 list_dir 、 search_text 、 write_file 、 edit_file 和 shell 。像 Subagents 、Skills 、Ask 、Task 、MCP 这些能力全部从 Extension 层往外长,而不是继续往基础工具表里堆。
这里面有几个看起来很小、但我自己反复推翻过的工程决定。第一个是为什么工具叫 shell ,不是 bash 。
这不是命名洁癖。 Kiso 不希望模型继承用户完整的交互式 Shell 环境,而是通过系统 Shell 执行命令,工作目录绑定在项目 Workspace ,并且对子进程环境变量进行清洗。
Kiso 自身使用的 API Key 、Token 不应该因为模型执行一次 env 或 printenv ,就跟着整个环境一起被吐回 Context 。更麻烦的是权限判断。
如果所有 Shell 命令都要求人确认, git status 、 ls 、 grep 每执行一次都弹一个窗口,Coding Agent 基本没法用;但反过来,如果只维护一张危险命令黑名单,同样不靠谱。 Shell 的表达能力太强,一个看起来完全无害的命令,可以通过 $() 、重定向、subshell 、环境变量甚至某些 Git 配置参数产生真实副作用。
所以 Kiso 没有试图判断“这条命令危险不危险”,而是采用反过来的标准: 只有能够证明只读,才自动放行。 read-only-shell 会对命令进行解析。
存在反引号、 $() 、subshell 、后台执行等无法安全证明的结构时直接 abstain ;由管道或分号组成的多个命令,每一段都要分别满足只读条件;访问 .env 、凭据路径时仍然进入权限链;甚至类似 git -c 这种表面还是 Git 命令、但可以借配置触发外部程序的形式,也不会被当作普通只读操作。解析器不知道,就是不知道。
Unknown 不等于 Safe 。第二个是 read_file 为什么默认只有 200 行或者 16,000 个字符。
文件读取这件事情看起来简单,但两种极端都不好。一种是直接把整个文件往 Context 里倒,一个两三千行的文件瞬间占掉大量窗口;另一种是把窗口切得太碎,模型为了理解一段完整逻辑不断往返读取,又浪费了工具调用和推理轮次。
我最后选择的是一个非常普通的预算式读取:正常代码最多读取 200 行,同时设置 16,000 字符的体积限制。为什么不是按 token ?
因为 tools-node 不应该为了读文件而绑定某个 Provider 的 tokenizer 。 Claude 、OpenAI 、DeepSeek 、Qwen 的 tokenizer 都不同,基础文件工具没必要感知模型层。
行数用于保持代码结构,字符预算防止大型单行 JSON 、压缩 bundle 、source map 之类的内容一行就把 Context 炸穿。正常情况下截断发生在完整代码行边界,同时把下一段从哪里继续读告诉模型。
我一开始其实很怀疑这个数字。早期有一次 Benchmark ,Kiso 在长任务上的成本比 Pi 高了不少。
我第一反应就是: 肯定是 200 行太保守,逼得模型反复读文件。这个解释听起来实在太合理了。
于是我把当时的真实 Trace 翻出来准备验证,结果发现那组任务涉及的文件里,最长的一份只有 132 行。 200 行限制一次都没触发。
后面继续追才发现,真正多出来的成本来自 edit tool 的失败重试,和 read_file 几乎没有关系。这个小插曲后来对我影响挺大:工程上一个解释再漂亮,只要 Trace 没有支持它,就只是一种故事。
工具层还有另一个更重要的问题,就是前面说的: Observed ≠ Current 。 Agent 读取一个文件以后,可能过几十秒甚至几分钟才真正修改它。
在这段时间里,人类可能在 IDE 里改了文件,Shell 命令可能重写了内容,另一个进程也可能修改同一个路径。如果 edit_file 最后只是机械地执行模型给出的 diff ,那么模型就会拿一个过期观察覆盖一个更新的现实。
所以 Kiso 的 read_file 不只是返回内容,同时会返回这个文件当前内容对应的 revision 。后面的 write_file 或 edit_file 如果要修改一个已经存在的文件,必须带着当时看到的 revision 回来。
大致就是: read_file ↓ content + revision ↓ 模型搜索、思考、调用其他工具 ↓ edit_file(expectedRevision) ↓ 执行前重新读取当前文件 ↓ revision 一致 → 允许修改 revision 已变化 → 拒绝,要求重新观察 这里的 revision 并不证明“模型真的认真理解了这个文件”,它只证明一个非常有限的事实: 模型做出这次修改所依据的文件状态,在真正写入的这一刻是否仍然成立。 Execution Ledger 解决的是“我到底有没有做过”,Revision Guard 解决的是“我刚才看到的现在还算不算数”。
一个防止 Runtime 对过去产生虚假的确定性,一个防止模型拿过去的观察冒充现在的现实。在我看来,它们本质上属于同一个问题。
- 权限边界:能力可以扩展,裁判权不能混在一起 随着模型能力越来越强,一个很自然的倾向就是尽量少打断它。用户当然不希望一个 Coding Agent 每改一行文件都来问“可以吗”,但如果走到另一个极端,把整个 Workspace 和 Shell 彻底交给模型,出现问题时用户又很难知道自己到底授权了什么。
Kiso 当前 CLI 对外提供五种主要运行模式: default 、 accept-edits 、 plan 、 dontAsk 和 bypass 。 default 下,可以证明只读的操作直接执行,真正产生副作用的动作进入审批; accept-edits 允许常规文件修改,但 Shell 等更开放的副作用仍然需要权限判断; plan 是纯只读模式,写入和其他副作用直接拒绝; dontAsk 面向无人值守场景,它不是“什么都允许”,恰恰相反——所有原本需要人确认的操作都会直接 deny ,模型只能在现有权限里找另一条路,不能把任务永远挂在一个无人回答的确认框上; bypass 则尽量放行正常操作。
但即便 bypass 也不是“把安全系统关闭”。像 Kiso 自身凭据、明显灾难性的路径操作仍然有更底层的保护。
放权是运行策略,灾难底线不应该成为一个可以顺手关掉的 UI 选项。我更感兴趣的其实不是这五种模式本身,而是 Kiso 后来形成的一个设计倾向: 能力可以不断往外扩展,但执行者和裁判者尽量不要是同一个东西。
比如 Ask Extension 。很多 Agent 在遇到不确定需求时,会在两种坏选择之间来回摆动:要么模型自己猜一个答案继续干,要么突然在自然语言里停下来问一句,让上层产品自己想办法处理。
Kiso 更希望“向人提问”成为一个明确的协议动作。当前台存在交互 bridge 时,模型拥有 ask_user ;没有人能回答的 headless/piped 场景,这个能力干脆不进入它的动作空间。
不知道,就是一个合法状态。再比如 Subagent 。
Kiso 可以通过 Delegate Extension 把相对独立的任务交给子 Agent ,而且子会话同样是 durable 、可以恢复。但子 Agent 并不是一个拥有完整权限的复制品。
父任务可以限制允许修改的路径;在这种受限子任务里,一些高风险能力甚至不会提供给它。更重要的是,子 Agent 做完以后,不应该因为它自己说“完成了”就算完成。
父 Session 可以持有独立的 acceptance check ,或者回来以后重新做更高层的验证。这一点和前面的 Execution Ledger 其实还是同一个思想。
模型可以提出行动,但不能顺手把行动结果写成历史;模型可以执行任务,但不应该因为自己说“完成了”,目标状态就自动成立。我并不是认为模型会故意撒谎。
问题恰恰在于它是一个概率系统,它非常擅长根据当前上下文生成一个“看起来已经完成”的连续叙事。如果提出假设、执行动作、记录事实、评价结果全部由同一个概率系统掌握,那么最后得到的很容易只是一个内部非常自洽的故事。
Runtime 要做的,不是限制模型变强,而是在模型越来越强以后,仍然保留几条独立于它的事实边界。 06. 2,200 行内核门禁与 Benchmark 到这里还有一个很现实的问题:如果这些语义真的重要,怎样避免它们随着项目不断增加 Provider 、Tool 、Extension 、TUI 、MCP 、Subagent 以后,慢慢被埋在一个越来越大的中央 Loop 里?
这几乎是所有框架最自然的退化路径。因为中央循环永远最方便。
它手里拿着最多状态,新功能遇到问题时,在这里加一个 if 最快;两个本来没有关系的模块想通信,把它们都接进核心 Loop 也最快。每一次改动单独看都很合理,几年以后就会变成一个谁也不敢碰的 Blob 。
所以 Kiso 给 packages/core/src 设了一条非常笨的硬门禁: 有效代码不能超过 2,200 行。目前是 2,192 行。
2,200 并不是什么理论推导出来的神圣数字。换一门语言、换一种 LOC 统计方式,同一套逻辑完全可能得到另一个数字。
它只是一个 Tripwire 。当核心只剩 8 行余地的时候,每次有人想往 Kernel 里塞新逻辑,就会被迫回答几个问题:这个判断为什么属于内核,而不是 Runtime 、Tool 、Extension 或产品层?
它如果做错,会不会真正改变底层执行语义?如果非加不可,现有 Kernel 里有没有东西应该先被移出去?
这个限制真正保护的不是“代码少”本身,而是 决策所有权 。 Kiso 现在大致把系统拆成几层:Product Shell 处理 CLI/TUI ,Composition 负责把 Provider 、Tool 、Extension 、Store 组装起来,Agent 负责定义和创建 Session ,Session 拥有 durable conversation 和 recovery entry ,Run 拥有一次执行的生命周期,Kernel Loop 只处理模型与工具之间最核心的驱动语义;旁边还有 Store 负责 single-writer 、CAS 、torn-tail 之类真正的持久化判决。
我也不希望把“2,200 行 Kernel”变成一种廉价营销。核心只有两千多行,不等于整个系统的可靠性只依赖这两千多行。
真正的 Trusted Computing Base 明显更大。崩溃恢复依赖 Store 、进程间锁、操作系统和文