Tutti / Agent / ACP / 架构
不止 Claude Code:Tutti 如何接入本地 Agent 与工具
如果把 Tutti 只当成 Claude Code 的桌面壳,会漏掉它最有意思的部分:Claude Code 只是它支持的一个 provider。
它真正想解决的问题是:本地 Agent 的形态太多了。Claude Code 有自己的 SDK,Codex 有自己的 app-server,Cursor、OpenCode、OpenClaw 各自说 ACP,Hermes、Kimi Code 这类工具甚至不在 Tutti 仓库里。浏览器、桌面控制、第三方连接器又是另一类东西——它们不是 Agent,而是 Agent 要调用的能力。
这篇文章不写使用教程,而是一次源码向的拆解:Tutti 用什么分层把这些东西接进来,哪些差异被 adapter 吃掉了,哪些差异必须留给 descriptor 和 runtimeprep,以及如果今天要再接一个本地工具,应该从哪一层下手。
一、先看全景:daemon 是接线板
Tutti 的进程层次可以画得很长,但边界其实很清楚:
- 01Electron DesktopUI只负责窗口、交互和 tuttid 进程生命周期;通过 loopback HTTP + Bearer 访问 daemon。
- 02tuttiddaemonAgent Host 拥有 Session / Turn 生命周期;Runtime Controller 拥有 provider 进程、事件归一化和工具服务。
- 03Provider Adapter适配层把统一生命周期翻译成 Claude SDK、Codex app-server 或 ACP 的具体调用。
- 04Provider Process本地 CLIclaude-sdk-sidecar、codex app-server、cursor-agent acp、opencode acp……每个会话一个受控进程。
- 05Local Capability工具`tutti` CLI、connector MCP、chrome-devtools-mcp、cua-driver MCP;进程生命周期由 daemon 持有。
这张图里最重要的不是进程数量,而是两条边界。
第一条在 Desktop 和 tuttid 之间。Electron 不直接启动 Agent,也不解析 provider 事件;它只调用 daemon 的 HTTP API。这样 UI 不需要知道 Cursor 和 Codex 的区别,更不需要为每个 provider 写一套会话状态机。
第二条在 adapter 和 provider 之间。所有 provider 共享同一套 Session、Turn、Interaction、Goal 语义;协议差异被压到 adapter 里。上层代码可以判断 capability,但不能按 provider 名字分支——这条规则在仓库的架构文档里被反复强调。
二、三种运行时,三种接法
providerregistry 里目前只有三种 RuntimeKind。所有内置 provider 都落在这三类里:
| Provider | Runtime / 协议 | 启动命令 | 状态 |
|---|---|---|---|
| Claude Code | claude_sdk · NDJSON | Node sidecar → Claude Agent SDK | 内置 |
| Codex | codex_app_server · JSON-RPC | codex app-server | 内置 |
| Tutti Agent | codex_app_server · JSON-RPC | tutti-agent app-server | 内置 |
| Cursor | standard_acp · ACP | cursor-agent acp / agent acp | 内置 |
| OpenCode | standard_acp · ACP | opencode acp | 内置 |
| OpenClaw | standard_acp · ACP | openclaw acp -v | 暂禁用 |
| Nexight | standard_acp · ACP | nexight-acp | 暂禁用 |