Rakazo 源码分析:持久 Agent、任务恢复与多端协作
原仓库:elie222/rakazo。
AI 参与说明(Agent:Codex):本文基于提交
08ee8cfb1552833de6ffca85b2e3e95bdbbf88dc做静态源码分析,未运行 Rakazo 测试;使用项目 Anatomy 制作审批恢复的教学交互,并使用 humanizer-zh 编辑中文。交互不连接真实 Agent、邮件服务或数据库。执行入口 Codex Desktop;模型、reasoning effort、提供方及版本未取得任务级运行记录。
Rakazo 把 Agent 做成长期存在的协作对象:每个 Bot 有身份、对话、记忆和工作环境,用户可以从 Web、Electron 或 Expo 客户端与它交流。阅读源码时,最值得关注的是一次请求如何变成后台工作,以及进程重启、用户审批和外部操作怎样影响后续执行。
这个项目的分工很明确:Pi 负责模型与 Tool 的循环,Rakazo 负责围绕循环建立可持久化的产品状态。两者之间的接口,是理解整个仓库的起点。
阅读前先认识几个词
| 英文术语 | 中文名称 | 简要解释 |
|---|---|---|
| Bot | 机器人 | 有独立身份、对话和配置的长期协作对象 |
| Agent Runtime | 智能体运行时 | 请求模型、执行工具并产生运行事件的软件层 |
| Task | 任务 | 保存用户想完成的工作及其原始输入 |
| Run | 运行 | 一次具体执行,保存状态、模型选择和恢复信息 |
| Attempt | 执行尝试 | 某个 Worker 持有运行权时的一段执行记录 |
| Lease | 租约 | 限时声明某个 Worker 当前拥有执行权 |
| Fence | 栅栏令牌 | 随接管递增的编号,用来拒绝旧 Worker 的写入 |
| Reconciler | 状态协调器 | 扫描数据库,为遗漏或中断的工作补发唤醒 |
| ExternalEffect | 外部副作用记录 | 保存一次外部操作的请求、审批和结果 |
| Steering | 中途引导 | 在 Agent 运行中接收用户补充的输入 |
从产品功能找到代码入口
仓库由 pnpm Workspace 和 Turborepo 组织。apps/ 放各进程与客户端,packages/ 放共享规则和外部适配,infra/ 保存部署与电脑环境。这里需要区分产品 Web 与官网:apps/web 是聊天应用,apps/www 是独立的公开网站。目录说明
| 功能 | 首先阅读 | 要核对的问题 |
|---|---|---|
| 发消息与群聊 | thread-target.ts | 新建 Run,还是引导已有 Run? |
| 后台执行 | executor.ts | 谁取得执行权,怎样恢复与结束? |
| 模型循环 | pi-runtime.ts | 模型、Tool 与事件怎样接到 Pi? |
| 持久 Bot | child-bots.ts | 是否创建独立 Bot、Thread 和 Run? |
| 临时 Subagent | executeSubagent | 子任务如何返回父轮次? |
| 记忆 | MarkdownMemoryStore | 内容、作用域和修订如何保存? |
| Routine | schedule-tools.ts | 时间与输入怎样成为持久计划? |
| 应用集成 | installed-connectors.ts | 哪些已安装工具对当前用户可见? |
| 电脑 Provider | sandbox-factory.ts | 本地与远端电脑怎样选择适配器? |
| 语音 Provider | voice-factory.ts | 凭据怎样选择实现,具体能力是否支持? |
| 服务装配 | Worker 入口 | 实际启用了哪些 Provider? |
运行架构:API 接收意图,Worker 执行工作
The API persists work before the Worker executes it. 用户请求先写入数据库,后台执行由独立 Worker 接管。模型请求不需要与用户的 HTTP 请求保持同样长的生命周期。
flowchart TB
Clients[Web / Electron / Expo] --> API[API<br/>Hono / oRPC / Better Auth]
API --> DB[(PostgreSQL<br/>业务记录与 Event)]
API --> Jobs[Graphile Worker<br/>run.continue]
Jobs --> Worker[Worker<br/>createRunExecutor]
Worker --> DB
Worker --> Runtime[PiAgentRuntime<br/>模型与 Tool 循环]
Worker --> Adapters[Provider Adapters<br/>电脑 / 集成 / 记忆]
DB --> Updates[Event 查询与实时唤醒]
Updates --> Clients
图中 Graphile Worker 使用 PostgreSQL 队列,并非另一个独立消息系统。Worker 入口会选择 Pi 或用于测试的 Scripted Runtime,再装配 Sandbox、Memory、Connector、Secret Store 和通知能力。它默认以 4 个并发执行后台 Job。装配过程、执行器与并发
依赖分层可以按“约定、规则、实现”来读。contracts 定义客户端与服务端交换的数据;core 保存状态转换等共享规则;adapter-kit 定义 Runtime、Sandbox、Memory 等边界;adapters 接入外部服务;db 保存数据库模型和事务。上层在进程入口选择实现,不要求业务代码绑定同一家模型或电脑供应商。
例如 AgentRuntime 对外暴露的是返回事件流的 run() 和按 runId 中止的 abort()。外部实现不需要把自己的 SDK 对象泄露给聊天客户端。MemoryStore 与 SemanticMemoryProvider 也有分开的接口:前者保存可编辑文档,后者承担召回和语义记忆操作。这样的边界使替换供应商有明确落点,但适配器仍需处理认证、失败格式、取消和能力差异,不能只替换一个 URL 就认定行为兼容。共享接口
这也解释了 Rakazo 与 Pi 的关系。该提交实际依赖 pi-agent-core、pi-ai 和 pi-telemetry 1.3.0;没有因为产品需要持久运行就直接使用 pi-durable。持久状态与恢复调度由 Rakazo 自己实现。依赖清单
领域模型:一条聊天消息背后保存了什么
把 Task、Run 和 Attempt 分开看,很多恢复逻辑就容易理解了。Task 表示要做什么;Run 记录这次工作怎样执行;Attempt 记录某个 Worker 在某个 Fence 下尝试过什么。同一个 Run 在等待审批后恢复,可以保留身份并产生新的 Attempt。
| 对象 | 保存的关键内容 | 与其他对象的关系 |
|---|---|---|
| Space | 成员与资源边界 | 关联 Bot、Thread、Run、连接和记忆 |
| Bot | instructions、模型、父 Bot、电脑 | 长期身份,不随单轮结束而删除 |
| Thread / Message | 顺序、内容块、引用与发送 nonce | 保存用户实际看到的对话 |
| Task / Run / Attempt | 输入、状态、租约、Fence、执行记录 | 分开表达意图、运行与接管 |
| Event | seq、type、payload、runId | 为状态同步与断线追赶提供有序记录 |
| ExternalEffect | request、审批、result、幂等键 | 追踪可能改变外部世界的操作 |
| Routine | prompt、时区、触发条件、nextRunAt | 为 Bot 安排后续工作,触发后仍进入 Run |
| Computer | Provider、工作目录、控制与执行 Lease | 与 Bot 分开建模,协调共享使用和人工接管 |
| MemoryDocument / MemoryRevision | 正文、作用域、版本与来源 | 保存可编辑记忆及其修订历史 |
以上关系可从 Space、Bot、Thread 至 Run、Attempt 与 ExternalEffect 核对。Message 的 (threadId, clientNonce) 唯一约束尤其重要:客户端超时重发时,需要找回原来的发送结果,而不是产生第二条工作。
Routine、Computer 与记忆修订的字段可继续从 相关数据模型 核对。
沿着 threads.send 走完一次请求
真实 RPC 名是 threads.send。入口先确认模型可用,再解析当前用户有权访问的 Thread,最后调用 sendThreadMessage。附件、引用和群聊目标的处理也在这里进入后端规则。RPC 入口
flowchart TB
Send[threads.send] --> Nonce{已处理过此 nonce?}
Nonce -->|有| Receipt[返回原发送结果]
Nonce -->|无| Message[事务内创建 Message]
Message --> Busy{已有可引导的 Run?}
Busy -->|有| Steering[记录 SteeringMessage]
Busy -->|无| Create[创建 Task 与 queued Run]
Steering --> Commit[追加 Event 并提交事务]
Create --> Commit
Commit --> Wake[通知客户端<br/>按需入队 run.continue]
Wake --> Execute[Worker 取得 Lease<br/>调用 PiAgentRuntime]
Execute --> Final[持久化消息与最终状态]
图中展示一般单 Bot 发送路径,省略了待回答问题和群聊成员选择的分支。发送事务把 Message、Run 与 Event 放在一起,是为了避免“用户看到了消息,后台却没有工作”,也避免清空 Thread 与新消息并发时留下半套状态。已有可引导 Run 时,补充内容进入 SteeringMessage;Pi 在下一轮处理前认领这些输入,再调用 agent.steer()。Routine、Webhook 和创建介绍等运行有自己的输入,不能随意吸收新用户消息。发送分支、Steering 接入、触发类型边界
事务提交后才发 Job。Job 只需携带 runId,Worker 重新读取权威状态;同一 Run 使用稳定的 run:<runId> Job key。入队失败会记录错误,但不撤销已经持久化的发送,后面由 Reconciler 补发。提交后处理、Job key
Executor 把历史、记忆、Bot 指令、模型与 Tool 回调交给 Runtime。Runtime 产生文本和工具事件,Executor 再把事件转换成产品消息与运行结果。模型因此不能直接决定数据库中的 Run 状态;结束、暂停、审批和审计由宿主落实。Runtime 请求、Tool 回调
Lease、Fence 与 Reconciler 分别防什么
正常状态流是 queued → leased → running,之后进入 completed、failed 或 cancelled;需要用户输入或接管电脑时,停在 waiting_input 或 waiting_takeover,恢复后重新排队。状态机
Lease 解决“谁现在可以执行”,Fence 解决“旧执行者回来后还能否写入”。continueRun 用条件更新竞争执行权,并递增 leaseFence。初始 Lease 为 5 分钟,每 60 秒续期;关键提交同时检查 Worker 与 Fence。取得执行权、续期
例如 Worker A 的 Fence 是 7,断线后 Lease 过期,Worker B 用 Fence 8 接管。即使 A 又拿到模型返回值,也不能再用 Fence 7 完成这次 Run。Fence 能拒绝过时的本地写入;已经送达外部系统的请求,则还需要 ExternalEffect 的处理。
Reconciler 默认每 30 秒扫描 queued Run 和 Lease 过期的运行,重新安排 Job;多个 Worker 通过 PostgreSQL advisory lock 选出执行扫描的一方。它还会保留有效 Job 的失败退避,避免每次扫描都把重试时间重置成“现在”。扫描条件、过期运行、补发策略
这里的恢复是重读业务状态、重建 Runtime 并核对已有操作,不能扩大成“任意一条指令都能无损续跑”。普通执行失败会成为 failed;电脑忙、初始化错误、Routine 失败和模型服务错误各有不同重试路径。模型切换还要求当前没有部分输出,以免把已经开始的一轮简单重跑。模型重试条件
审批为什么还需要 uncertain
An approved action reuses the persisted request. 用户批准的是保存下来的具体请求。恢复后,Executor 会核对工具、目标资源和请求参数,不能让模型重新拼出一个相似但含义不同的操作。获批请求重放
flowchart TB
Intended[ExternalEffect: intended] --> Approval{需要用户审批?}
Approval -->|需要| Wait[Run: waiting_input<br/>保存审批卡并释放 Lease]
Wait --> Answer{用户决定}
Answer -->|允许| Approved[ExternalEffect: approved]
Answer -->|拒绝| Denied[ExternalEffect: denied]
Approval -->|允许直接执行| Claim[条件更新取得操作执行权]
Approved --> Claim
Claim --> Executing[ExternalEffect: executing]
Executing -->|结果成功保存| Completed[ExternalEffect: completed]
Executing -->|重启后无法确认结果| Uncertain[ExternalEffect: uncertain<br/>核对目标后再决定]
暂停时,数据库事务同时修改 Run、结束 Attempt、保存审批卡与 Event。用户回答后,再把 Run 设为 queued、把对应操作设为 approved 或 denied。Worker 不必一直占着进程等待点击。暂停事务、回答事务
真正困难的是远端已经接受请求,本地却来不及记录成功。例如邮件发出后 Worker 崩溃,恢复时仍看到 executing。源码会把这种结果视为 uncertain,不自动再次发送;completed 则返回已保存的结果,denied 则返回拒绝。重复效果处理
这是一种保守的恢复选择,不是跨所有外部服务实现 exactly-once。它宁可留下需要核对的结果,也不把“不知道是否成功”直接当成“可以重试”。停止运行同样不能撤销已经完成的外部操作:停止入口持久化 cancelled,并清理待引导输入和电脑执行;Runtime 再响应取消。停止路径
Anatomy:恢复时使用哪一份请求
下面解剖 resolveDuplicateEffectGate 与 Executor 恢复获批请求的机制。假设系统已经匹配到同一个 ExternalEffect,工具路由等安全检查通过,而且该操作需要审批。保存的请求是发给 team@example.test 的邮件;模型恢复时可以提出另一个收件人,但获批执行仍须取回保存的请求。
批准放行的是存档请求
改变持久化状态和模型新提议的收件人,观察执行台最终收到什么。
批准闸门允许原子认领;认领成功后,执行 payload 的 to 仍为 team@example.test。模型新提议的收件人不能替换它。
{
"to": "team@example.test",
"subject": "Build report",
"body": "Build 42 passed"
}{
"to": "other@example.test",
"subject": "Build report",
"body": "Build 42 passed"
}{
"to": "team@example.test",
"subject": "Build report",
"body": "Build 42 passed"
}这是浏览器内的教学模型,不运行 Rakazo、Agent 或邮件工具。只展示已匹配同一 effect、路由校验通过后的普通直接工具恢复分支;approved 仍需原子认领成功,才会执行保存的参数。图中不模拟 catalog 参数封装、并发 worker 或媒体任务的专门恢复。
先保持 approved,修改模型新提议的收件人,观察执行票据仍取自保存的 request;再切换 completed,观察返回内容改为保存的 result。改为 executing 时,系统还无法确认上次调用是否成功,恢复动作是核验结果,不能自动再发一次。intended 在这里表示重复请求仍在等待审批,返回 paused;新建的 intended 操作还可能通过自动审批或直接执行路径取得执行权,不属于这个恢复实验。
理解这个模型后,还可以预测两个边界:把重试次数调大,并不能消除 uncertain;用户批准一次,也不意味着所有相似请求都共享这次批准。这里展示的是单个 ExternalEffect 的决策,省略了真实远端响应、网络延迟和其他 Tool;不能把教学状态切换当成邮件服务已执行的证据。
安全边界:有审批机制,不代表所有 Tool 都会询问
源码明确把 shell、write_file、browser_act、computer_act 等列入审批豁免集合。特定外部写入、删除、生成媒体和修改 Bot 指令等能力另有规则。因此,“每次执行危险动作前都会弹窗”并不符合实现;电脑环境与 Tool 权限本身就必须承受模型可能作出的操作。审批分类
Space 的隔离也要看具体查询。requireMembership 从成员关系构造 Actor,scoped 检查记录的 spaceId 和可选 userId。新增 RPC 如果绕开这些检查,即使客户端没有显示入口,也没有建立相同的访问边界。这里核对到的是应用层授权,不能仅凭模型字段就宣称数据库会自动替每条查询完成租户隔离。作用域检查
密钥保存有专门适配器,应用加密实现使用 AES-256-GCM,并把记录标识绑定为附加认证数据。它保护保存后的凭据,不会自动阻止拥有电脑执行权的程序访问已经注入环境的秘密。因此,不能把密文存储、日志脱敏、运行权限和网络边界混成同一项保证。密文封装
Docker 网络还存在一个容易漏看的部署条件:SANDBOX_COMPUTER_EGRESS=restricted 只标记电脑网络,真正的阻断依赖宿主防火墙规则。默认出站模式仍可访问宿主、局域网和云元数据地址;要启用文档中的限制,必须同时设置该标记和安装规则,不能只看环境变量就认定已隔离。网络标记与执行位置、限制步骤
记忆、历史和工作目录要分开理解
MarkdownMemoryStore 虽然以 Markdown 为可移植形式,当前实现仍把文档与修订保存在数据库。读取按 spaceId、userId、scope 和可选 botId 过滤;保存以事务同时更新正文和修订历史,并支持 expectedRevision 冲突检查。读取与保存
它的基础搜索是先取出作用域内文档,再用字符串包含关系过滤。因此,不能仅凭“Memory”名称推断底层使用了向量数据库。语义记忆是另一条可选 Provider 边界,当前工厂包含 Supermemory、Serenity 与 Honcho。基础搜索、Provider 工厂
注入模型的持久记忆默认有 32 KiB 上限,并明确标为可能过时的数据。历史压缩另有游标和摘要:使用摘要前,要确认后续消息连续,避免摘要之后缺了一段对话。电脑工作目录则保存文件和环境,不能代替可检索的用户记忆。记忆上下文、历史连续性
长期 Bot、临时 Subagent 与三端协作
持久 Bot 会创建数据库对象和独立 Thread,可以选择自己的电脑、模型与 Routine。临时 Subagent 则是父轮次中的辅助 Pi Agent,没有独立 Bot 对话;最多同时运行 4 个,不能继续嵌套。它向父 Bot 返回结果,由父 Bot 组织用户回复。创建 Bot、Subagent 限制、Subagent 身份
三端共享的是同一服务端事实,而不是三份独立的 Agent 后端。Web 使用 React 19、Vite 和 React Router;Electron 承载 Web UI,同时管理本地设置、窗口与 Docker Compose;Expo 使用 React Native 与原生导航、存储和权限接口。Web 依赖、移动端依赖
Electron 的“在本机运行”实际启动 Compose 后端,退出应用后该后端仍可继续运行;连接已有实例时,客户端访问同一 API。不过,选择原生本机电脑执行时,还需 Electron 保持运行,因为命令通过它的认证桥接进入当前系统账户。Docker 后端存活与原生电脑执行可用,是两个需要分别判断的条件。桌面运行方式、本机电脑条件
客户端同步依靠 Snapshot 与有序 Event。Web 的 reducer 根据 run.started、thread.cleared 等事件更新展示状态,订阅循环从已见 cursor 重连;heartbeat 只说明连接还活着,不应推进业务游标。这使“暂时漏了一次实时通知”与“持久消息丢失”成为不同问题。Web 状态更新、重连循环
移动端也订阅同一 RPC 事件流,并处理连接空闲和会话切换。未发送内容还有磁盘 Outbox,但它位于系统缓存目录,源码明确说明可能被操作系统清理。它改善中断后的重试体验,不能写成永不丢失的离线消息队列;真正的已接收承诺仍需要以后端持久记录为准。移动订阅、Outbox 边界
测试与部署:代码提供了哪些证据,仍有什么限制
仓库具备 Vitest、集成测试、端到端测试、模型模拟和电脑回放等入口。它不仅测试模型能否回答,也覆盖审批重放、租约、租户边界和 Provider 契约。测试文件存在说明项目安排了对应验证,并不等于本文已经证明它们在当前环境全部通过。测试脚本、测试范围
项目自己的验证文档区分得更细:Scripted 测试检查给定调用能否正确执行;Pi 协议测试运行真实 Pi,只替换模型端点;集成旅程再加入 API、PostgreSQL 和 Executor;真实模型质量测试才验证自然请求能否促成有用行动。没有真实凭据意味着未运行,不能计为评测通过。开发者应按修改边界选择证据,例如换模型协议需要看实际请求和 Tool result,改桌面行为还需要真实浏览器或电脑环境。验证分层
自托管依然有明确运维边界。Compose 同时运行数据库、API、Worker 和电脑 Supervisor;Supervisor 挂载 Docker socket,因此属于高信任组件。数据库、Artifact、电脑文件和密钥的备份也不能只靠“能重新拉镜像”。本地模式减少了对托管供应商的依赖,但不会消除模型、机器资源和数据恢复的管理工作。Compose 服务
还有一条部署限制直接影响选型:API 与 Worker 需要常驻进程,单纯的 Serverless 请求处理器不足以承载当前运行方式。两者共享持久 DATA_DIR;当前 Home Store 使用本地文件系统,跨主机部署需要共享文件系统,仓库文档明确说尚无对象存储适配器。若准备水平扩容,应先处理文件可见性和执行租约,再讨论增加 Worker 数量。其他部署布局
有两项结构性限制值得在二次开发前考虑。其一,PostgreSQL 同时承担业务、队列和协调职责,减少组件数量,也集中连接压力;Worker 已专门共享连接池。其二,编排集中在较大的文件:该提交的 executor.ts 为 9,900 行,router.ts 为 7,458 行,pi-runtime.ts 为 3,105 行。接口边界清楚,修改核心路径仍需要跨审批、恢复和多端状态核对回归。连接池安排
此外,MAX_TOOL_CALLS_PER_TURN 缺省、空值或 0 都表示不限制。重复工具调用保护不能代替用户预算或整体费用上限;面向多用户部署时,应明确设置执行预算,并用真实业务场景验证停止条件。预算配置
固定提交后的阅读顺序
可以在一个空目录执行下面的命令,得到本文对应的源码。它只获取和读取公开代码,不安装依赖,也不调用付费模型。
git clone --no-checkout https://github.com/elie222/rakazo.git rakazo-reading
cd rakazo-reading
git checkout --detach 08ee8cfb1552833de6ffca85b2e3e95bdbbf88dc
git rev-parse HEAD
sed -n '2260,2279p' apps/api/src/router.ts
sed -n '780,866p' apps/api/src/thread-target.ts
sed -n '3498,3550p' packages/adapters/src/executor.ts
git rev-parse HEAD 应返回上面的完整提交号。继续阅读时,先看 schema.prisma 与 run-state.ts,再沿 threads.send 走到后台 Job;随后阅读 continueRun 和 PiAgentRuntime,最后带着“崩溃发生在这里会怎样”的问题检查 ExternalEffect 与 Reconciler。
这种顺序能把一个具体输入连到状态、执行权和外部结果。理解了这条链,再去扩展一个模型 Provider、电脑供应商或移动端交互,才知道哪些行为应留在客户端,哪些必须由后端统一负责。
关联阅读:Pi Agent 框架架构:包边界、Agent Loop 与接入选择 讨论 Pi 自身的分层,该文以 1.1.0 为基线;本文分析的 Rakazo 依赖 1.3.0,具体 API 仍以本文固定提交为准。其他 Agent 工作流文章见 栏目导航。