跳到正文

Rome

Rome 是一个自托管的 agent 运行时,把模型外面的那层环境当成真正值得养大的东西。一个 Rome App 把专门做的界面、可执行的 action、按需加载的 skill 和一份私有数据库打包成 git 里可追踪的代码,让 agent 在之后的工作里发现并复用;底下的运行会给每一次委派出去的 subagent 开一个属于它自己的子会话,允许 fork 出一个回合却不许它改动来源,并且宁可让这一轮失败,也不偷偷换掉产出这段对话的那个模型。

Screenshot of Rome
编辑截图, 1 Oct 2026Rome ↗

这是什么

一个自托管的 agent 运行时,它把产品定义成模型外面那层环境,而不是模型本身。它是一套 MIT 许可的 pnpm monorepo:一条快速启动脚本拉下 Docker 镜像,在 http://localhost:7663 上给出一个只给守护者的仪表盘,旁边还有一个预览中的托管版 Rome Cloud。产品的单位是 Rome App——一份 app.yaml 清单,加上带类型的 action、应用自己的 agent、按需加载的 skill、生命周期 hook、应用私有的数据库以及可选的网页界面——而十三个第一方应用和社区应用走的是同一条安装路径。agent 层后面挂着两套模型 harness:Anthropic 走 Claude Agent SDK,OpenAI 走 Codex,按 agent 用档位或精确的模型 id 来选。再往下是会话运行时:持久会话与记录、每一次委派出去的 subagent 都拥有自己的子会话、fork 出来的回合分隔离与精确两种模式、一个会去 steer 正在跑的回合而不是把它替掉的输入队列,以及一条失败即拒绝的会话模型钉。记忆是文件而不是服务,通过 git 同步;而这个项目所说的复利是可执行的——action、skill 与应用都是代码,后来的工作会发现、复用并组合它们。

谁做的这是一个组织账号而不是个人:仓库是 rome-os/rome,MIT 版权挂在项目名下,而历史由一位提交者扛着。zhangfand 这个账号写了 352 次提交里的 193 次,署名 Zhangfan、邮箱 dong.zhangfan@gmail.com;紧随其后的四个账号 yunfanye、Jessie-QingYu、zoolsher 与 Asuka109 合起来又占 129 次。贡献者名单上共十八个账号,其中一个发布机器人贡献了十三次;另有 215 次提交带着共同作者尾注,写的是人、是那个机器人,或者是一个 Claude 模型。

它是怎么搭起来的

组成 · 6

一个组合根,架在一套扁平的模块系统上,产品是环境而不是模型。monorepo 分成后端运行时、只给守护者的仪表盘、一个 Electron 外壳、一个手机端宿主、两个公开的应用 SDK,以及十三个和任何人都走同一条安装路径的第一方应用——正因如此,应用这一层是一个插件系统,而不是一张功能清单。三个想法撑起整个运行时。会话是持久边界,回合是被计数和被计费的单位,于是每一个分配工作的东西——被委派的 subagent、分出去的 fork、从渠道进来的一条消息、定时任务——都会变成一个带着自己证据的会话。进程隔离只在一个地方被授予,于是嵌套变成运行时携带的数据,而不是它继承来的操作系统父子关系。环境是代码,所以记忆、技能、action 与应用全都落进用户自己拥有的 git 仓库里的文件,这也正是「复利」由什么构成这个问题的答案。文档是设计的一部分而不是它的副产物:docs/authoring/ 下的规则手册带分层标签和淘汰规则,架构文档家族不许引用文件路径,散文在持续集成里被 lint——一个这么年轻的仓库之所以能给每一个已经踩过的坑留一条成文规则,靠的就是这些。

packages/core/
后端运行时:1,005 个文件、10,885 KB。核心 agent 层里有一个 143,888 字节的 agent-session 和 27,878 字节的 agent-runner,一个 47,370 字节的 Anthropic provider 与一个 52,602 字节的 Codex app-server provider,一个 51,989 字节的 MCP 门面,以及 subagent 执行、hook 递归、输入队列和几个回合流登记表。旁边是 61,425 字节的 action 引擎连同 worker RPC 与 IPC;应用管理器与安装器,带打包、发布、remix 和 Store 客户端;26 个数据库仓库,最大的 webchat 仓库 84,856 字节;带定时与事件触发器的 routine 引擎;一层以 git 为后端的同步;十来个平台的渠道适配;以及一个 68,699 字节、负责把对象图接起来的 index.ts。
packages/web/ 与 packages/ui/
只给守护者的仪表盘,775 个文件、7,964 KB,做成一个 SPA:页面级文件里有 89,871 字节的 routine 页和 79,635 字节的聊天组件,另有 storybook、多语言、包含录制的应用包的 mock 夹具,以及针对布局不变量、触摸目标与控制尺寸词表的端到端用例。旁边 106 个文件的组件套件是仪表盘与应用界面共用的那一层。
packages/desktop/ 与 packages/mobile/
一个 Electron 外壳加本地运行时,76 个文件、9,870 KB,在 desktop/vendor 下随包带了一套 Windows 工具链;以及一个 60 文件的手机端宿主,把仪表盘装进 WebView,补上 WebView 本身没有的平台导航。两边各有自己的构建与发布工作流。
rome_apps/
十三个第一方应用,每一个都是工作区包,带 app.yaml、action、agent、skill、数据库迁移、可选的网页界面和自己的测试:assistant、briefing、browser-automation、coding、connector、dream、inbox、recap、showcases、skills、system、welcome-to-rome 与 workflow-studio。光 coding 一个就是 196 个文件,装着 Rome 用来造和验别的应用的那些技能,其中一份写作参考 53,830 字节,app_creation 技能 11,473 字节。
docs/
一个目录下 102 个 Markdown 文件,按家族组织:24 份决策记录、十四个架构文档、十二条概念条目、十七本写作规则、十八份界面文档和四个 north star,另有 32,163 字节的设计系统、15,918 字节的发布指南与一份可观测性 schema。仓库根上,VISION.md 有 23,320 字节,PRODUCT.md 9,277 字节,而 DESIGN.md 把设计 token 放在自己的 frontmatter 里。
工作流表面
十四个工作流文件,最大的一个持续集成文件 24,611 字节、一个可视化端到端任务 21,433 字节,另有桌面构建与发布、Docker 发布、SDK 发布、移动端与每夜任务。它们周围是:.claude/skills/ 下十二个开发用 skill,41 个文件的脚本(含 token 策略检查器与一份散文基线),十五个可视化测试用例加一份用例清单,十四个做浏览器自动化的站点插件,以及给 Chrome、ClickHouse、OpenTelemetry、Traefik 和一个同步服务用的基础设施。

取舍,以及它替代了什么

  • 只有主进程创建 action worker 替代 让 worker 自己 fork 出它需要的那个嵌套 worker

    被 worker fork 出来的进程只有一条 IPC 对端,而那条对端就是 fork 它的 worker,于是主进程持有的每一项服务从孙进程那里都够不到,而这个缺口在写代码时没有任何信号——调用打到一个没有处理器的对端上,调用方只会等到方法过期。改成让嵌套以 root 与 parent 执行 id 的形式传递之后,每个 worker 在主进程眼里都长得一样,主进程持有的一份预算覆盖它们全部;撞上限时调用立刻失败而不是排队,因为在等容量的嵌套调用会把每一层祖先都撑开着。

  • 子会话自己拥有记录、trace、流与成本 替代 把子事件转播进父流并把用量往上滚

    这两种被否掉的做法都把子数据放到了父所有者名下,而各自的失败都很具体:一条同时承载两种终态的流,会让子结果顶替掉在它之后失败的那个父回合;把用量滚进父会话,则在子会话自己有计费行的那一刻就把全局用量算重了;而被父会话吞掉的子会话,在会话界面上根本没法查。父会话只留一个引用和一条结构化完成结果,涵盖后代的合计则在读取时现走一遍。

  • hook 递归的守卫跟着因果链跨边界走 替代 把排队或跨进程的工作当成一段全新的执行上下文

    进程边界恰恰是循环开始看不见的地方:一个 hook 到达 action worker,worker 回过头找主进程要一个 agent 回合,那个回合又派发 hook。在那里重置计数的守卫只量到了链条的一小段,会把一次失控当成一串又短又健康的循环。改成让 root id、深度以及链上每一个已经进过的 hook 的身份跟着工作一起走之后,预算归进程所有,而不归共享同一运行时的任何单个应用。

  • 触摸设备上,方形控件把自己的盒子长到目标尺寸 替代 把控件的点击区域伸到盒子外面

    没有任何层叠顺序能把一个控件的点击区域放到另一个控件盒子下面,所以伸出去的那块会吃掉本该给邻居的点击——一个开关横向伸出十二像素的触碰范围,正是吃掉了旁边按钮的最后几个像素。现在套件让方形控件把盒子本身长大,给带文字的按钮只保留纵向的可达范围,并为间距比目标尺寸还紧的控件写明了一个受支持的退出方式。

  • 带后台任务的空闲会话继续开着 替代 像别的会话一样按空闲计时器关掉它

    清扫器会在一轮结束后十五秒关掉空闲的 webchat 会话,于是一个用后台方式起的任务,会在启动它的那条回复之后十五秒就跟着死掉。现在只要还有任务在跑,会话就继续开着,上限是从它最后一次活动起的三十分钟,这样一个长任务能跑完并汇报,而拥有它的会话不会在底下消失。

依据VISION.md(23,320 字节)、README.md(15,123 字符)、三份决策记录 docs/adrs/workers-never-fork-workers.md、docs/adrs/child-session-owns-subagent-stream-and-cost.md、docs/adrs/hook-recursion-chain-crosses-queue-boundaries.md,以及 docs/concepts/sessions.md、docs/concepts/skills.md、docs/concepts/agents.md、docs/authoring/architecture.md、docs/authoring/github-issues-task-spec.md、AGENTS.md、DESIGN.md;pull request 593、592、590、589、586、585、584、583、581、579、573、572、568、567、566、564、571 的正文与评审串,issue 591、588、580、575、565;每个目录的体积汇总;以及完整的 3,421 个文件树及其体积。

制作过程

6 个阶段
  1. 01

    五周、352 次提交,版本线已经走到 v1.1.130

    仓库建于 2026-08-23,比它自己的第一次提交只晚三秒;接下来的五周半里落下了 352 次提交——八月剩下的日子里 100 次,九月 247 次,十月最初几个小时 5 次。贡献者名单上有十八个账号,而其中一个占了大头:zhangfand 写了 352 次里的 193 次,yunfanye 49 次、Jessie-QingYu 36 次、zoolsher 34 次,一个工作流机器人 13 次,Asuka109 10 次,并且每一次提交都关联到了账号。215 条共同作者尾注里有 114 条写的是一个 Claude 模型或 Claude Code——Opus 5 占 67 条,Fable 5 十六条,Fable 5.1 十五条,Opus 4.6 与 Claude Code 各六条,另有三条落在百万上下文与 4.8 这两条线上——另外 74 条写的是人,多数时候就是维护者本人。发版是按包而不是按产品走的:报告抓到的二十个 release 落在 2026-09-10 到 2026-09-25 之间,横跨七个包名,其中有 app-runtime 从 0.6.5 到 0.6.7、ui 从 0.2.7 到 0.3.3、rome-web-components 从 0.1.14 到 0.1.19,以及首个 host-helper 0.1.0。镜像 tag 是另一条更快的线,最新的二十个从 v1.1.111 排到 v1.1.130。这一圈之外还有 661 个星、56 个 fork、七个 watcher 与 83 个未关闭的 issue。最新一次提交停在 2026-10-01 的凌晨,标题是一处带破坏性标记的改动:把 TalkRouter、Talk 与 InboundMessage 从 SDK 里移除(#579)。仓库体积 20,277 KB,文件树里躺着 3,421 个文件。

  2. 02

    这里的复利指什么,又不指什么

    这个主张是先写下来、再动手建的。VISION.md 有 23,320 字节,标题是「Rome: an OS for recursive agents」,它的论点是:所有人都在量模型规模这条轴,而真正会复利的是环境那条轴——「Models scale intelligence. Rome scales the environment that intelligence can use.」它写出了自己想走的环:create、discover、compose、adapt、preserve、evaluate、improve,也老实交代了自己站在哪:今天的 Rome 能做创建、发现与组合,把成功的组合沉淀成持久能力才是下一步。README 把同一套主张放进一张对照表,里面「accumulates」的那一栏写的是「actions, skills, and apps as git-tracked code, plus memory and app-private data」;它给 Hermes Agent 划的那条线最锋利——那边留下来的「is text that informs the next reasoning run」,而 Rome 留下的是软件。代码树跟着这个说法走:capability-discovery.ts 与挂在它上面的路由、一份扁平的 skill 目录(skill 归应用所有、按需注入)、作为可执行单位的 action(引擎 61,425 字节,每跑一次留下一条执行记录),以及作为文件的记忆(背后是一个 git 源)。有两个第一方应用只为了照料这堆东西而存在——dream,它的 skill-review action 去审查技能集;以及 skills,它负责把技能导进来。和本档案的差异也在这里:最近的那几条记录,留下的是 agent 学到的东西的文字,而 Rome 主张可持久的那一格,是能执行的能力。

  3. 03

    三条把递归围起来的成文规则

    递归由三份决策记录围起来,它们都写着 2026-08-11,而这个仓库的第一次提交是 2026-08-23。第一条管委派:subagent 跑在一个第一等的子会话里,那份记录、trace、流、状态与计费都归子会话所有,父会话只留一个引用加一条结构化的完成结果。被否掉的替代方案连同它的失败一起写明了——把子事件转播进父流之后,一个在父回合失败之前就已经成功结束的子结果会成为最后留下的结果,于是子答案变成父答案;而把用量往上滚进父会话则会算错账,因为全局用量是把每一条计费行相加,子会话一旦有自己的行就会被算两遍。第二条管进程:只有主进程能创建 action worker,worker 需要嵌套执行时就请主进程去跑,嵌套关系以 rootExecutionId 与 parentExecutionId 的形式传递,而不是靠操作系统的父子边。从 worker 里 fork 出一个孙进程被否掉,是因为那个孩子只握有一条 IPC 通道,而这条通道的那头什么主进程服务都没有,失败只会在一次不相关的调用里表现为超时。主进程持有的一份预算覆盖所有深度上的所有 worker,撞到上限时调用立刻失败而不是排队,因为一个在等容量的嵌套调用会把它的每一层祖先都撑开着。第三条管 hook:root id、深度、以及这条链上已经进过的每一个 hook 的身份,都跟着 hook 引发的那份工作一起走,穿过进程内的异步边界、worker 进程,以及那个 worker 发起的回合。超预算的 hook 只记一条带遥测的跳过记录,绝不判引发它的那一轮失败。这三条规则都住在 docs/adrs/ 下,每一条都把被否掉的替代方案逐项列出,并写明「后来的改动必须遵守什么」。

  4. 04

    这一轮算谁的:steering、fork 与模型钉

    运行时花的力气主要花在「这一轮算谁的」上。一个尚未合并的 pull request 把若干规则从 provider 里搬进一个小类:哪条 SDK 消息属于哪一轮、哪些结果才算结束一轮 Rome 回合、哪些发送 id 还在追踪——它们此前散落在一堆闭包变量里;同一位作者还报告说,为了验证某个顺序,他在 SDK 0.3.281 上跑了十三次探针,而那个顺序在真实 SDK 上从未出现过。第二个尚未合并的 pull request 让思考过程、一段很长的工具输入、以及命令的输出获得和正文一样的流式处理与块身份,此前一个实时预览只能靠事件先后顺序去配对;它的第三轮评审否掉了「再算一次已完成块的索引」,理由是指名道姓地写清了必须先变的 SDK 行为。模型解析被保守地定下来:会话记住产出它这段历史的那个具体模型,而这个模型如果跑不了——登出、额度耗尽、权益丢失——这一轮就以结构化错误失败,而不是悄悄换成另一个,救回来的办法是显式选一个模型。fork 是同一个想法的另一半:默认隔离、工具面为空;调用方需要复刻来源前缀时用精确模式;可以是一次性的,也可以续下去;只能从一个已经完成、且 provider 检查点已落盘的回合分出来;并且永远不能把模型钉写到来源上。空闲被用相反的方向同样认真地处理:一个尚未合并的 pull request 让带后台任务的空闲 webchat 会话继续开着,用三十分钟的上限取代原先十五秒的清扫。输入队列那一侧同样写得细:一个被 provider 收下的输入,在它真正进入上下文之前不算被消费;一个确定没被消费的输入可以开下一轮,而一次不确定的投递不会被自动重试——后端重启之后它会挂在那里、标明投递未确认,不会被悄悄重放。

  5. 05

    作者自己写下来的失败

    这些记录下来的缺陷,对「活了多久」交代得格外具体。一个未关闭的 issue 说:dream 跑失败时,失败只被记进 action 执行表,界面上什么都看不到,于是 dream:skill_review 每一次运行都失败、持续了两个半月、212 次之后才被人发现。另一个未关闭的 issue 是维护者写给自己的代码的:Claude 返回的结果如果 subtype 写着 success、错误标志却是置上的,它就会被当成这一轮的答案发布出去、并被投递到各个渠道,因为终态只由 subtype 决定;这条是在评审相邻改动时发现的,然后被留在那里没修。一条已关闭的后续记录写着:一个用 run_in_background 起的 pnpm app:install 会立刻返回,却被算成安装成功;底下更大的检测问题也被写清了——当前的 Codex 把命令项序列化成 exitCode,而网页那侧读的是 exit_code,于是在 Codex 上这次安装从来不算成功,而在 Claude 上它根本不可能算成功,因为 Claude 的输出里根本没有退出码。另一条已关闭的修复记录写着:迁移 0037 把 webchat 的会话表改了名,而 Showcases 还在查旧名字,于是会话选择器报 no such table。还有一条写着,一次依赖升级带来一个三百像素的表格高度默认值,而 Markdown 包装层从没去设置它,于是每张超过大约十行的表格都被裁掉。还有一个开关,它的点击区域横向伸出轨道十二像素,把旁边「运行」按钮最后几个像素吃掉了——这是在回答一次不相关的评审时发现的。同一批 pull request 的测试计划里也留着实话:一条写着「Nix is unavailable on this machine」,所以宿主上只跑得了类型检查;另一条则列出类型检查、1,786 个单元测试、构建与键盘用例四比四全过。

  6. 06

    把流程用在流程自己身上

    一个这么年轻的仓库仍然把规则写下来,然后对规则本身做检查。docs/authoring/ 是一本规则手册,里面的文档家族带着分层标签,分别对机械的、模型的、人的义务;一句话只有在「一个本来能编译、测试能过、单看也读得通的改动仍然会被它判违规」时才有资格进入,而且一次「行为不变的重构不可能让这句话变假」的检验也会把它筛掉;一句话一旦不再通过这道门槛就得离场,而不是「为了安全先留着」。架构类文档被明令禁止引用文件路径。散文本身由 .vale/styles/Rome/ 下的九条 Vale 规则检查,用 pnpm lint:prose 跑,且版本在持续集成里钉死。issue 的体例活在 docs/authoring/ 里而不是模板目录里——一份任务书要有 Situation、Scope、Acceptance 三段,带 task 标签;当一份正文不依赖产出它的那段对话、agent 自己就能照着做时,再加第二个标签;每一条验收都要写清由什么来证明,一个提交进仓库的测试,或者对完成分支跑一次。然后这个仓库把这套机器用在自己身上:.claude/skills/ 里有十二个给自己工作流用的 skill,包括 respond-to-review、file-issue、babysit-pr、loop-northstar 与 loop-reconcile,另有一个把待办清单搬成项目看板;而被抽样的那些 pull request,是一个维护者在给自己做编号的评审,一轮一轮列出改了什么、以及否掉了什么并给出理由。仓库里没有 issue 模板目录,.github 下只有 workflows:十四个工作流文件,最大的持续集成文件 24,611 字节,另有一个 21,433 字节的可视化端到端任务。

相关档案

全部档案 →