
这是什么
一个桌面工作台,把几个编码 agent 放进同一个房间里,而不是让你在它们之间搬运上下文。README 列出 Claude Code、Codex 与 Hermes 为已支持,OpenClaw 在开发中;随包发布的 provider 图标与探测代码里还出现了 Cursor 与 Gemini。它们共用同一个工作区:一次 @ 提及就能引用另一个 agent 过去的对话、文件、应用调用与任务,一个引用动作就能把本地文件或某个应用的产物加进来。Tutti 自带应用中心——出图、UI 设计、文档、演示稿——人与 agent 都能调用,产物一律留在工作区里供下一步使用。agent 仍然跑在用户已经付过钱的那份订阅上,daemon、它的 SQLite 状态和工作区都留在本机。第二个面向多人的产品 Tutti · VM 处在 Early Access:agent 仍在各自主人的机器上、跑在一个受管的本地 VM 里,而工作状态实时共享进一个云端 Room。仓库是 Apache-2.0,已积累 5,361 次提交。
谁做的一个组织账号,不是个人。仓库有 26 个账号提交过代码,5,361 次提交里有 5,166 次挂在关联账号下。榜单由 SingleMai 领跑,1,256 次;其后是 devRickyyy 的 898 次、jomeswang 的 798 次、rv4no 的 505 次、hugozhou-ai 的 488 次、chovy-ai 的 364 次与 copper6666 的 336 次;另有 195 次提交没有关联账号。1,088 条共同作者尾注里有 760 条写的是模型或 agent 工具而不是人——「Claude Opus 4.8 (1M context)」239 条、「Claude Fable 5」215 条、「Cursor」146 条、「Claude Sonnet 5」60 条——写人名最多的是 rv4no,237 条。
它是怎么搭起来的
组成 · 6一个本地 daemon、一层渲染外壳、一个生命周期核心,边界在写代码之前就先写下来。所有持久状态与业务规则都在 services/tuttid 里——一个由桌面应用监管的 Go daemon;桌面只是渲染层加 Electron 主进程,并被明确禁止成为第二个业务核心。agent 行为刻意不按 provider 组织:packages/agent/host 为所有 provider 持有 session、turn、goal 与 runtime operation 的生命周期,daemon 与其他宿主对着它写适配器,而新的生命周期语义只能在 conformance 目录里先被证明。界面是同一种直觉——Agent GUI 从 descriptor 读能力、按能力渲染,而不是按 provider 名字分支——仓库用边界检查器和一份提交进仓库的基线棘轮来兜住它。因为这个产品是工作区而不是聊天窗口,大部分工作在接缝上:一次提及到底携带什么、一个应用的产物怎样变成下一个 agent 可以引用的东西、一次会话录制怎样变成一份不依赖数据库就能重放的便携磁带,以及一次构建怎样从候选走到公开下载而没有人重新打包。
- services/tuttid/
- daemon 与主业务核心,1,463 个文件:
api/放着生成的 OpenAPI 服务端(一个 1.7 MB 的server.gen.go),service/有 890 个文件,此外还有biz/、data/、server/、app/、integration/、builtin-apps/与types/。provider 的探测与安装、agent 扩展、工作区应用、issue、终端与文件都在 service 层。 - packages/agent/
- 文件数最多的一块,2,909 个文件:
host/的 142 个文件是与 provider 无关的生命周期核心及其 conformance 场景,gui/有 1,277 个界面文件,daemon/有 807 个 provider 协议文件,周围还有claude-sdk-sidecar/、activity-core/、session-replay/、session-replay-runner/、session-replay-ui/、store-sqlite/与runtimeprep/。 - apps/desktop/
- Electron 外壳,1,375 个文件,分
main/、preload/、renderer/与shared/——其中 1,042 个在渲染层。它监管 daemon、暴露带类型的桥,并被禁止持有业务规则;旁边还有apps/cli/(19 个文件)、apps/mobile/(215 个文件,Android 与 iOS)与apps/ui-storyboard/。 - docs/
- 199 个文件,分四种性质:37 份架构文档,领头的
agent-gui-node.md有 231,006 个字符;38 份约定文档,其中 troubleshooting 目录下的agent-session-lifecycle.md就有 340,594 个字符;specs/与plans/下分别是 30 份与 24 份带日期的规格与计划;adr/下 12 个文件,是十份编号决策记录加一份术语表与一份索引。 - .github/workflows/、.changeset/ 与 tools/scripts/
- 发布与校验的机械部分:23 个工作流覆盖桌面发布、晋升、通知、商店提交、Windows alpha 与适配器、Android、npm 包、应用目录与运行时,以及外部 pull request 评审闸门;
.changeset/下 113 个文件,112 份是一行式变更说明、一份是配置;另有 186 个脚本,包括按改动选车道的 runner 和一个 120 KB 的 agent 会话重放 runner。 - .codex/skills/ 与根目录的指令文件
- 五个自带清单的 agent 技能——性能 trace 分析、应用发布、架构复审、会话重放录制、测试审计——外加一份
skills-lock.json,把另外两个技能按哈希锁定到另一个tutti-agent-skills仓库。AGENTS.md按路径也按模块名做路由,CONTEXT.md是一份条目里带「避免用词」的术语表,根目录还留着两个重构文件。
取舍,以及它替代了什么
agent 生命周期语义住在
packages/agent/host替代 留在那个逐家对接 provider 的 daemon 适配器里AGENTS.md把这条规则写成一个判定问题,并写明:消费方发现缺能力时,应当把能力加进 host 并发版,而不是在适配器里重写一遍。GetSession、UpdateSettings、UpdatePin与DeleteSession就是这样由 PR #1329 加进去的;而当适配器里出现新的编排面时,pnpm check:agent-host-boundary会让它失败。与第二个产品共用一个基于类的 Workbench host 内核 替代 两个产品各自维护自己的宿主协调逻辑
ADR 0009(2026-07-11 接受)记着:仅靠依赖注入无法决定哪些状态是渲染器单例、哪些是作用域内局部,无法决定订阅、保存队列与缓存何时销毁,也无法决定用户身份变化时怎样阻止一份缓存快照漏进另一个房间。它给出的还是一个退出条件而不是意愿:一旦这次抽取产生 React 运行时依赖,或产生 host 与 surface 之间的环,就必须停下来。
给 agent 会话重放一个共享的应用核心 替代 让每个产品各自拥有一套录制与重放
ADR 0010 把录制与磁带的契约放进
packages/agent/session-replay,把重放进程的临时身份、进度与结算留给产品适配器。磁带是可携带的,schema v7 是唯一被接受的版本,没有更老的读取器、迁移或回退;每个解码后的 provider 帧 8 MiB、磁盘上 256 MiB、整份磁带 384 MiB 的上限都写进清单,异常增长因此可归因。stable 版本在有人批准之前一直是候选 替代 让发布工作流自己把它发出去
RC 与 beta 自动晋升,而 stable 候选必须手动提交,并通过一个受保护环境的批准——该环境要求的评审者是三个具名账号。晋升会重新核对候选清单、校验和、每个受支持平台锁定的运行时版本,以及公开渠道不会往回退;因为渠道指针是共享的可变状态,晋升被串行化,而且绝不重新构建安装包。
默认把 Windows 算进每一次改动 替代 把它当成以后再补的移植工作
AGENTS.md把 Windows 写进每一次改动的兼容契约,包括根本没提到它的改动,并列出了触发评估的范围:路径、文件系统语义、可执行文件的发现与后缀、命令引用、shell 选择、环境变量、进程创建、信号、权限、符号链接、套接字、打包与原生依赖。每项改动都要带一份 Windows 影响评估,操作系统的差异留在所属适配器里,不许散落到业务逻辑中。
依据AGENTS.md、docs/architecture/project-structure.md、docs/architecture/agent-gui-refactor-plan.md、仓库根目录的中文重构交接说明与遗留问题记录、docs/adr/0009-cross-product-workbench-host-kernel.md、docs/adr/0010-agent-session-replay-boundary.md、docs/conventions/local-git-hooks.md、docs/conventions/desktop-release.md、docs/README.md、根目录 package.json 与 .changeset/config.json、pull request 模板,以及完整的 8,293 个文件树及其体积。
制作过程
6 个阶段- 01
三个月、5,361 次提交,然后是一个之后什么都没有的发布
仓库创建于 2026-06-12T12:29:05Z,而它最早的一次提交比这个时间还早约十七个小时:
feat: init project for tutti open source,2026-06-11T19:38:27Z。公开历史共 5,361 次提交,按月分布是六月 1,467 次、七月 2,862 次、八月 1,029 次、九月 3 次。最新一条是 2026-09-05T06:07:25Z 的fix(agent): require current Codex CLI (#2643),而默认分支的最后一次推送是 2026-09-05T07:16:52Z——比 releasev0.2.33出现晚九秒,所以那个发布读起来是这条可见工作线的终点,而不是线上的某一点。2026-10-01 采集时仓库有 3,791 个星、384 个 fork、114 个 watcher、192 个未解决 issue,本记录据此把它记为维持中而不是活跃:在八月 1,029 次提交、九月只有 3 次之后连着四周没有推送,这是一次停顿;仓库也没有被归档。提交停了之后队列还在动——四条日期为 2026-09-13 与 2026-09-14 的 pull request 至今开着。 - 02
代码只能住在三个地方,规则只能由一个包来定
AGENTS.md开篇先给形状——一个 local-first 的桌面 monorepo——然后做了一件比画图更有用的事:说清哪块地归谁,并把常见的逃逸口堵上。services/tuttid持有 daemon 的产品规则、本地持久状态与适配器;packages/agent/host持有与 provider 无关的 agent 生命周期;apps/desktop是 Electron 外壳,「不得成为第二个业务核心」;包名不许叫shared、common、utils或client-sdk。生命周期那条规则被写成一个问题而不是偏好——如果一项改动定义了 session、turn、goal 或 runtime operation 何时被创建、何时可以发送、何时进入终态,它就属于 host 包;如果只是传输、查询或呈现,那就是适配器的事——而新的生命周期语义必须先拿到一个落在packages/agent/host/conformance下的场景。pnpm check:agent-host-boundary会卡住适配器里新出现的编排面。文档还举了自己执行这条规则的例子:GetSession、UpdateSettings、UpdatePin与DeleteSession是下游消费方提出需求后由 PR #1329 加进 host 的,而不是在那个消费方内部重写一遍。 - 03
到底有什么在 agent 之间流动
README 把问题描述成「你成了 agent 之间的信使」,把解法描述成一个实时共享工作区:上下文、文件、正在跑的任务与应用产物彼此相连。承载它的是三套机制。第一套是提及,README 叫它「Big @」:在一个 agent 里可以引用另一个 agent 过去的对话、文件、应用调用与任务,也可以让一个 agent 去指挥另一个。第二套是在输入框里做一次引用,把本地文件或某个应用产出的东西加进来。第三套是应用中心本身——出图、UI/UX 设计、文档与演示稿——人与 agent 都能调用,跑在用户已有的 agent 订阅上而不是转售的模型能力上,产物留在工作区里供下一步使用。在这之下,provider 的差异被挡在界面之外:Agent GUI 重构交接说明里记着,Claude Code 走 SDK sidecar,Codex 走它自己的 app-server 协议,OpenCode 与 Cursor 走标准 ACP,Tutti Agent 走自己的协议族,Nexight、Hermes 与 OpenClaw 走 ACP,不可用的显式禁用——于是传输协议不被当成界面的接缝,新增 provider 也不得要求在 controller、composer 或 view 里加一个身份分支。
- 04
800 行上限,和一道只往一个方向拧的棘轮
业务文件被一条仓库 lint 规则压在 800 行以内,且不计空行与纯注释行;这条上限被当作「该拆了」的信号,而不是一个可以讨价还价的目标。它周围是一整套本身就是产品一部分的校验:
pre-commit跑暂存文件的格式化外加八项边界检查,pre-push跑pnpm check:changed -- --push-ready,按改动文件集挑车道,而不是把什么都跑一遍。每条车道会存下自己输入的指纹,所以失败后可以用--failed-only重跑,且只有输入变过的车道会再跑。完整校验是一个小编排脚本,分准备、预检与校验三个阶段,默认输出精简摘要,每个任务的完整日志落在.tmp/check-full-runs。最锋利的一条是 Agent GUI 的退化棘轮:一份提交进仓库的 17 KB 基线每次都会和熵指标比对,指标上升就失败,指标下降但没在同一次改动里锁定基线也失败。让这一切成立的那次重构记在仓库里,而且是中文写的:一份日期为 2026-07-11 的交接说明记录了 controller 从 2,034 行降到 800 行以内,分七个切片完成,只剩一个私有的持久状态读取器,要等客户端覆盖窗口结束再删。 - 05
一个不许自己发布自己的 release
桌面发布约定写了三种形态——成为 Latest 的 stable,以及保持 prerelease 的
-rc.<n>与-beta.<n>。最近二十个 release 从 2026-07-30 的v0.2.6排到 2026-09-05 的v0.2.33:三十七天里二十个,其中三个就出在 2026-07-30 当天。RC 与 beta 在暂存之后自动晋升;stable 则停在候选,需要另跑一次晋升流程,并通过一个受保护环境的批准——该环境要求的评审者是三个具名账号,允许自审。晋升会重新核对候选清单、校验和文件、每个受支持平台锁定的受管运行时版本,以及公开渠道不会往回退;然后才把资产拷到不可变路径、创建正式 tag、写渠道指针与 changelog、刷新 stable 别名,全程不重新构建安装包。发布说明在有 key 时由模型起草,没有就退回一份由提交记录确定性生成的摘要;运维只编辑一段标记出来的双语评审区,晋升时它会被转成已评审的摘要。nightly 发布、S3 运行时产物、Linux 产物与 Microsoft Store 的预发布都被有意排除;2026-09-02 有一条 pull request 移除了每日定时构建触发器,并自述这对文档没有影响。 - 06
外部贡献者找到了什么
外部 pull request 必须由
tutti-rd团队成员批准当前 head 提交才能合并,再推一次就会让这道闸门重新校验。队列里的发现都很具体。一位贡献者指出,导入的会话只按 provider 的 session id 认身份,于是 427 个转录文件被压成了 33 个会话:导入向导渲染出 769 行、却只有 375 个不同身份,React key 重复了 397 次;修法是把来源路径一起放进哈希,让每一种导入来源只留一条身份规则。另一位发现多语言检查用一份硬编码的三名字白名单去发现模块,而约定文档恰恰把那三个名字写成「非穷尽」的举例,于是四个已发布的清单一直被排除在所有翻译检查之外。第三位把 Cursor 的回答变成一行——2,662 个字符、零个换行——追到了strings.HasPrefix(text, ""):去掉首尾空白后它永远为真,于是携带段落分隔的纯空白分片被当成重复丢掉。第四位把一次 Gemini CLI 失败解释清楚:那个 CLI 会重启自己,并在父进程上装下空的SIGINT、SIGTERM与SIGHUP处理器,于是 ACP 探测能跑完初始化,却仍然报告进程没有退出。第五位修的是一次点击打开了两个浏览器标签:两个处理器都接下了同一个手势,修法是在 80 毫秒内丢掉对同一地址的第二次激活。
相关档案
全部档案 →第 070 号
OpenChatCut
一个本地优先的视频剪辑器,剪辑方式是跟它说话:内置 agent 与外部 Codex、Claude Code 会话调用的是界面自己在用的同一套剪辑工具,于是每一处改动都落在一条真实的多轨时间线上——是片段、转场、字幕、特效或音频,仍然能拖、能撤销、能导出。工程与素材留在本机,预览与最终渲染都出自 Remotion。
第 061 号
Reticle
一个 MCP 服务器加一个只在开发期生效的 SDK:让编码 agent 从应用内部去读、去操作一个正在运行的 web 或桌面应用,然后给出判词和该改的文件与行号,而不是一张截图。
第 077 号
Lody
一个让团队共用他们本来就在跑的编码 agent 的工作台:连上一台机器,把 Claude Code、Codex、Kimi 或任何其他说同一套协议的 agent 接进来,然后从桌面端、手机、网页或终端派活;会话之间可以互相派活,而代码始终留在机器主人自己连上来的那台机器上。