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。

app/ · Electron + React Main 进程 · index.ts / core-client.ts spawn core(--data-dir、--adapters、--session-profiles) IPC:core:request · core:notification    permission / elicitation request + response 选目录/文件对话框 · 只开 https 外链 · 主题 PATH 取自登录 shell,agent 才找得到 git 等工具 把两个适配器的启动命令交给 core preload · contextBridge → window.openyak request · onNotification · permission / elicitation pickDirectory · pickFiles · setTheme Renderer · React 19(沙箱:无 Node) App.tsx Sidebar Thread Composer Message ModelPicker ProjectPicker Settings Menu App.tsx 持有全部状态:项目、任务、消息流、 agent 状态与选项、待回答的权限请求 api.ts 薄封装 → window.openyak.request messagePresentation · attachmentDrafts · theme 只和 core 说话,不含任何 agent 逻辑 window.openyak ipcRenderer.invoke / on core/ · openyak-core(Rust · tokio) rpc.rs · JSON-RPC dispatch project.list/create/update/delete task.list/create/rename/delete chat.history/send/edit/retry/cancel agent.list/connect/disconnect/set_config permission.respond 每个请求一个 tokio task;写库后立即返回 agents.rs · AgentPool 每个 (Task, Agent) 一个 Connection + 子进程 session/load 失败 → session/new,cwd = Project 目录 session/update → chat.update;权限请求原样转发 set_config 30s 超时;cancel;Announce 重发状态 会话选项 (model / effort / mode) 不解释,只透传 handoff.rs · build_prompt cursor 之后错过的回合 → <handoff> 块前置 store.rs · SQLite(openyak.db) projects · tasks · messages(parts JSON) agent_cursors · agent_sessions agent_config — 键都是 (task_id, agent) id 为 ULID,时间为 RFC 3339 Job(user, assistant 消息 id) 发送前组装 读 transcript 与 cursor 请求 → 应答 通知 chat.update chat.done · agent.* permission · elicitation stdin/stdout · NDJSON JSON-RPC 2.0 Agents · ACP 适配器(随 app 打包) claude · claude-agent-acp 0.73 @agentclientprotocol/claude-agent-acp 内含 Claude Agent SDK,即 Claude Code 本体 由 Electron 自带 Node 运行 ELECTRON_RUN_AS_NODE=1 复用本机已有的 Claude Code 登录 codex · codex-acp 1.8 @agentclientprotocol/codex-acp 内含 Codex 本体 同样由 Electron 的 Node 运行 登录:~/.codex/auth.json 缺失时 agent.list 返回 hint 提示 codex login Agent 自己负责,OpenYak 不做 · 模型、API key、登录态 · 工具、shell、文件访问 · 权限引擎与沙箱(提示原样转发给用户) · agent loop 本身 · 会话选项的含义(model / effort / mode) OpenYak 不持有任何凭据, 不维护 allowlist,不做安全决策 ACP · stdio session/new · load prompt · update request_permission set_config_option
图 1 · Main 进程启动 core 并把两个适配器的启动命令以 --adapters 传入;core 在需要时才为每个 (Task, Agent) 拉起一个适配器子进程。Renderer 在沙箱里,只能通过 preload 暴露的 window.openyak 说话。

2 · 领域模型

术语来自 CONTEXT.md,是仓库的规范词表。Agent 是按每条消息选的,不是按任务选的;"Agent session" 是实现细节,不出现在界面文案里。

Project 磁盘上的一个目录 agent 的工作目录 包含 0..n Task 有边界的一件工作 project_id 可为空 恰好 1 个 Chat 规范、agent 中立的记录 由 core 保存,归用户 0..n Message user / assistant 一轮 记录是哪个 Agent 答的 1..n Part text · thought · tool_call error · image · file Agent claude / codex 适配器 按每条消息选择 Agent session = (Task, Agent) session_id · last_seen cursor · 已接受的 config 值 内部概念, 界面不展示
图 2 · 一个 Task 只有一个 Chat,但可以被多个 Agent 轮流服务。core 为每个 (Task, Agent) 对记住三样东西:agent 那边的 session id、它看到 Chat 的第几条(cursor)、用户为它选过的选项。

3 · 一条消息的旅程(含 handoff)

这是"换 agent 不丢上下文"的机制所在。每个 agent 只保留自己那边的上下文,OpenYak 不试图共享;它靠 Chat 记录 + 每对 (Task, Agent) 的 cursor,在发送时把对方错过的回合补给它。

App(Renderer→Main) core · rpc.rs core · Connection Agent 适配器(ACP) chat.send {task_id, agent, text, attachments?} 写入 user Message 与 assistant 占位 (status = streaming) 立即返回 {user_message_id, assistant_message_id} Job(若 (Task, Agent) 未连接,先拉起子进程) session/load(上次 session_id) 失败则 session/new(cwd = Project),cursor 归零 build_prompt:cursor 之后的回合 渲染成 <handoff> 块 + 正文 + 附件块 cursor 推进到本条 prompt session/prompt session/update × N(text · thought · tool_call) chat.update {message_id, part_index, part} 文本整段累积,不是 delta request_permission(选项原样带上) permission.request / elicitation.request 用户点选 → permission.respond {option_id | null}(经 rpc 解锁等待中的 Connection) 允许 / 拒绝 / 取消 prompt 结束(stop reason) status = done · 写 duration_ms cursor 移过这条回复 chat.done {status: done | error | cancelled, duration_ms}
图 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.rsAgentPool、每对 (Task, Agent) 的 Connection:拉起适配器、恢复/新建 ACP 会话、流式转发、权限往返、会话选项的捕获与回放、取消与超时。核心里最重的一块。1591 行
core/src/handoff.rs纯函数:按 cursor 取错过的回合,渲染 <handoff> 块并拼出最终 prompt。232 行
core/src/store.rsSQLite 表结构、迁移、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.tsapp 与 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/ 找二进制,打包后从 resourcesPathOPENYAK_CORE_BIN 可覆盖。

v2 明确不做

  • 自研 agent loop、provider tool clone、私有 Desktop API
  • provider API key 存储、远程访问、自己的权限引擎
  • 自动在 agent 之间路由(目前按每条消息手选)

v1 完整保留在 legacy/v1 分支。

薄在哪里

  • 权限提示:agent 问什么,用户就看到什么;答案原样回传
  • 会话选项:agent 通过 ACP 广播什么,界面就显示什么;core 记住已接受的值,新会话时回放
  • 适配器与其 Node 运行时随 app 打包,用户只需保留自己的登录