openyak · v2.0.0-alpha.0 · 截至 2026-09-03 的 main 分支
OpenYak v2 架构
一个 Electron 壳、一个 Rust 核心、两个随应用打包的 ACP 适配器。OpenYak 自己只做三件事:聊天界面、对话记录存储、ACP 客户端。模型、工具、权限、登录全部属于 agent。
1 · 进程与边界
三类进程,两道 stdio 边界。蓝色箭头是跨进程协议;每一道边界都是"一行一个 JSON"的 JSON-RPC 2.0。
图 1 · Main 进程启动 core 并把两个适配器的启动命令以 --adapters 传入;core 在需要时才为每个 (Task, Agent) 拉起一个适配器子进程。Renderer 在沙箱里,只能通过 preload 暴露的 window.openyak 说话。
2 · 领域模型
术语来自 CONTEXT.md,是仓库的规范词表。Agent 是按每条消息选的,不是按任务选的;"Agent session" 是实现细节,不出现在界面文案里。
图 2 · 一个 Task 只有一个 Chat,但可以被多个 Agent 轮流服务。core 为每个 (Task, Agent) 对记住三样东西:agent 那边的 session id、它看到 Chat 的第几条(cursor)、用户为它选过的选项。
3 · 一条消息的旅程(含 handoff)
这是"换 agent 不丢上下文"的机制所在。每个 agent 只保留自己那边的上下文,OpenYak 不试图共享;它靠 Chat 记录 + 每对 (Task, Agent) 的 cursor,在发送时把对方错过的回合补给它。
图 3 · 新会话的第一条 prompt 会带上整段历史,之后每条只带对方错过的部分,所以来回切换 agent 保持连贯又不浪费上下文。prompt 在 Connection 发送时才组装,保证反映它真正进入的那个 session。chat.edit / chat.retry 走同一条路,但会先丢弃后续记录并强制新建 session,防止被丢弃的上下文泄漏。
4 · 代码地图
| 路径 | 职责 | 规模 |
| core/src/main.rs | 入口。解析 --data-dir / --adapters,打开 SQLite,起 stdout 写线程,逐行读 stdin 分发;持有待回答的权限请求表。启动前移除 CLAUDECODE 环境变量,避免 Claude Code 误判"嵌套会话"。 | 182 行 |
| core/src/rpc.rs | 所有 app→core 方法的参数结构与处理;chat.send/edit/retry 写库后交给 AgentPool。 | 469 行 |
| core/src/agents.rs | AgentPool、每对 (Task, Agent) 的 Connection:拉起适配器、恢复/新建 ACP 会话、流式转发、权限往返、会话选项的捕获与回放、取消与超时。核心里最重的一块。 | 1591 行 |
| core/src/handoff.rs | 纯函数:按 cursor 取错过的回合,渲染 <handoff> 块并拼出最终 prompt。 | 232 行 |
| core/src/store.rs | SQLite 表结构、迁移、Project/Task/Message/Part 类型与全部读写。 | 962 行 |
| app/src/main/ | Electron 主进程:spawn core、IPC 桥接、系统对话框、主题、外链白名单。core-client.ts 是 stdio JSON-RPC 客户端。 | 317 行 |
| app/src/preload/ | contextBridge 暴露 window.openyak;把拖入文件解析成磁盘路径。 | 68 行 |
| app/src/shared/protocol.ts | app 与 core 契约的 TypeScript 类型,与 docs/core-protocol.md 对应。 | 185 行 |
| app/src/renderer/ | React 界面。App.tsx 是状态中枢;Sidebar(项目/任务列表)、Thread(消息流)、Composer(输入与附件)、Message、ModelPicker(agent 选项)、Settings。 | 约 5.3k 行 |
| docs/ | architecture.md(进程模型与 handoff)、core-protocol.md(app⇄core 契约)、message-lifecycle.md(消息操作的产品契约与回归清单)。 | |
5 · 存储、构建与运行
SQLite · openyak.db
- projects id · name · path
- tasks id · project_id(可空)· title · updated_at
- messages task_id · role · agent · parts(JSON)· status · duration_ms
- agent_cursors (task, agent) → last_seen_message_index
- agent_sessions (task, agent) → session_id
- agent_config (task, agent, config_id) → value
位置:Electron userData 目录。无项目的聊天使用同目录下的 projectless/ 作为工作目录。
构建与检查
npm run dev # cargo build --release core → electron-vite dev
npm run build # 同上,产出 app/out
npm run check # cargo test + node --test + tsc + eslint
运行时版本由 mise.toml 钉死:Node 26、Rust 1.90。开发态 Main 进程从 core/target/release/ 找二进制,打包后从 resourcesPath;OPENYAK_CORE_BIN 可覆盖。
v2 明确不做
- 自研 agent loop、provider tool clone、私有 Desktop API
- provider API key 存储、远程访问、自己的权限引擎
- 自动在 agent 之间路由(目前按每条消息手选)
v1 完整保留在 legacy/v1 分支。
薄在哪里
- 权限提示:agent 问什么,用户就看到什么;答案原样回传
- 会话选项:agent 通过 ACP 广播什么,界面就显示什么;core 记住已接受的值,新会话时回放
- 适配器与其 Node 运行时随 app 打包,用户只需保留自己的登录