# 7-15 [process] 页面 AI ACP Agent Runtime 统一抽象层 v1 > 创建时间:2026-05-17 > > 当前状态:`DONE` > > 2026-05-21 Batch J 口径补充: > - 本稿的 ACP runtime 核心实现已完成并在当前页面 AI 主链中作为默认 runtime 边界使用;Hermes HTTP proxy 默认关闭,只在显式 compat 开关下保留。 > - Step 15(多会话压力测试)、Step 16(旧 HTTP proxy cleanup)、Step 17(Reasonix cache benchmark)不再压在本稿内继续推进,已拆到 `design/07-ai/process/7-34-acp-runtime-cleanup-availability-stability-tail-v1.md`。 > - `page_ai_workflow` 仍是 debug / fast path 兼容门面,不是 local-first 普通 Markdown 的默认 AI 编辑主路径;它必须继续走共享 mnote tool executor。 > > 本稿目的: > 1. 在 mnote-web 中引入 ACP(Agent Client Protocol)作为统一 agent runtime 抽象层 > 2. 使 Hermes(当前)与 Reasonix(缓存优先)可互换,前端下拉切换 > 3. 褪去当前 `hermes_client.rs` 中的 Hermes-HTTPS-proxy 硬编码,改为 ACP JSON-RPC 通用连接器 > 4. 复用现有参考代码,最小化重复实现工作 > > 关联文档: > - `/mnt/Data1T/mnote/recycle/design/07-ai/retired-http-hermes/7-5-hermes-client-proxy-contract-v1.md`(已退役历史背景) > - `/mnt/Data1T/mnote/design/old/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md` > - `/mnt/Data1T/mnote/design/old/07-ai/process/7-14-local-first-ai-markdown-editing-convergence-v1.md` > - `/mnt/Data1T/mnote/design/07-ai/reference/7-17-acp-session-convex-sharing-contract-v1.md` > - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/hermes-vscode-main/` > - `/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/DeepSeek-Reasonix-main/` --- ## 1. 问题描述 ### 1.1 当前架构 ``` 浏览器 (页面 AI 面板) │ SSE ▼ mnote-web (Rust Axum) │ ├─ hermes_client.rs (3845 行) │ └─ HTTP proxy → Hermes HTTPS gateway API │ post /api/hermes/runs │ get /api/hermes/runs/{id}/events │ ├─ hermes_tools.rs (mnote.doc.* / mnote.block.*) │ └─ Rust 兼容工具实现,通过 HTTP/Convex 或本地代理读写文档 │ └─ page_ai_workflow.rs (兼容门面) └─ local-first 下不再是主路径 ``` **问题:** 1. `hermes_client.rs` 与 Hermes HTTPS API 的字段、鉴权、错误码硬耦合——换 agent runtime 需重写整个模块 2. Hermes HTTP 协议没有标准化,Reasonix、Claude Code、Cline 各自用不同的 HTTP 接口 3. 前端 SSE 事件格式(`tool.started` / `message.delta` / `run.completed`)也是 mnote 私有定制的 4. 没有「运行时选择器」——切换 agent 需要改环境变量重启 mnote-web 5. `hermes_client.rs` 中大量代码(3845 行)是做 Hermes 专属的 HTTP proxy、session 管理、profile 路由——这些应该在统一抽象层中解决 ### 1.2 目标架构 ``` 浏览器 (页面 AI 面板) │ SSE (前端不变) ▼ mnote-web (Rust Axum) │ ├─ ACP Session Manager (统一层,新增 ~800 行) │ ├─ 运行时选择器 (profile → agent runtime 映射) │ │ ├─ Hermes: spawn("hermes", ["acp"]) │ │ └─ Reasonix: spawn("node", ["reasonix-acp.mjs"]) │ ├─ ACP JSON-RPC 2.0 client (通用实现) │ │ ├─ session/new │ │ ├─ session/prompt │ │ ├─ session/cancel │ │ └─ session/update ≫ SSE 转发 │ └─ 代理层:向下游工具通知 │ ├─ hermes_tools.rs (兼容层) │ └─ mnote.doc.* / mnote.block.* / mnote.page.* │ └─ page_ai_workflow.rs (兼容保留) └─ local-first 普通正文编辑默认不经过它 ``` ACP 是整个架构的支点——它是一个**开放协议**,不是某个产品的私有接口。 > 2026-05-18 local-first 口径补充: > > - 目标不是把 Hermes / Reasonix 再包进一层重型 MNote 工具系统,而是让它们尽量像在 VSCode 中那样直接面对授权后的本地工作区。 > - MNote 主要负责:页面定位、白名单目录授权、ACP 会话管理、审计、文件变化同步到 tiptap / File Tree / Page Aggregate。 > - local-first 普通 Markdown 编辑默认不要求 runtime 调 `mnote.doc.markdown_edit`;兼容工具只为 cloud / remote / 复杂结构场景保留。 --- ## 2. ACP 协议标准 ACP(Agent Client Protocol)是一个基于 JSON-RPC 2.0 的、面向 AI agent runtime 的标准通信协议。同时被 Reasonix (`src/acp/`) 和 Hermes (`hermes acp` CLI) 实现。 ### 2.1 传输层 NDJSON over stdio(默认),也可用 TCP/Unix socket。每行一个完整的 JSON 对象。 ### 2.2 协议方法 | 方向 | 方法 | 用途 | |------|------|------| | Client → Server | `session/new` | 创建一个会话线程 | | Client → Server | `session/prompt` | 发送用户输入,等待 agent 完成 | | Client → Server (notification) | `session/cancel` | 中断正在运行的 prompt | | Server → Client (notification) | `session/update` | 推送实时状态变更 | | Server → Client (request) | `session/request_permission` | 请求用户审批工具调用 | ### 2.3 `session/update` 事件类型 | `sessionUpdate` 值 | 含义 | 字段 | |---|---|---| | `agent_message_chunk` | 模型生成文本增量 | `content: { type: "text", text: "…" }` | | `agent_thought_chunk` | 模型推理/思考过程 | `content: { type: "text", text: "…" }` | | `tool_call` | 工具调用开始 | `toolCallId`, `title`, `kind: "read"|"edit"|"search"|"execute"|"other"`, `status: "pending"` | | `tool_call_update` | 工具状态变更 | `toolCallId`, `status: "in_progress"|"completed"|"failed"`, `content` | | `usage_update` | 上下文用量更新 | `used: number`, `size: number` | | `session_info_update` | 会话元信息 | `title: string` | ### 2.4 对比 mnote 当前 SSE 格式 | mnote 当前事件 | ACP 对应事件 | 备注 | |---|---|---| | `message.delta` | `agent_message_chunk` | 几乎 1:1 | | `tool.started` | `tool_call` + `kind` | 前者多了 `preview` 字段 | | `tool.completed` | `tool_call_update` + `status: "completed"` | 前者多了 `duration` | | `run.completed` | `session/update` 不再发事件,prompt 返回 | 语义等价 | | `run.failed` | `tool_call_update` + `status: "failed"` | 语义等价 | | 无 | `agent_thought_chunk` | 当前页面 AI 未显示思考过程,新增能力 | | 无 | `usage_update` | 可展示 token 用量 | **结论:** 前端桥接层只需做一个事件名映射 + 字段适配(~50 行),即可对接 ACP。 --- ## 3. 参考代码分析 — 可复用部分 ### 3.1 Hermes VSCode 扩展 (`hermes-vscode-main/`) 这是**最完整的 ACP 客户端参考实现**,可以直接指导 mnote-web 的 Rust ACP 客户端设计。 | 文件 | 内容 | 可复用方式 | |---|---|---| | `src/acpClient.ts` | ACP JSON-RPC 2.0 客户端:spawn 子进程、读写 NDJSON、处理分帧、请求/响应/通知路由 [acpClient.ts:30-220] | **逻辑移植到 Rust** — `tokio::process::Command` spawn + `BufReader` 按行读取 + 请求 ID 映射表 | | `src/sessionManager.ts` | 会话生命周期管理:session/new → session/prompt → session/cancel,去重,事件派发 [sessionManager.ts:37-294] | **逻辑移植到 Rust** — `HashMap` 管理活跃会话 | | `src/protocol.ts` | ACP 事件的类型解析:文本提取、去重、tool call 解析、usage 解析 [protocol.ts:1-120] | **直接指导 Rust struct 设计** | | `src/chatPanel.ts` | VSCode WebviewView 桥接:ACP 事件 → webview HTML 渲染 [chatPanel.ts:1-524] | **UI 架构参考** — mnote 页面 AI 面板已存在,只需适配事件格式 | | `src/webview/main.ts` | webview 端事件处理:消息渲染、工具展示、todo 面板 [webview/main.ts:1-531] | **UI 交互参考** — tool call 显示方式、todo overlay | | `src/webview/renderers.ts` | 工具调用格式化、历史加载 [renderers.ts:1-170] | **前端渲染参考** | **核心设计复用:** ```typescript // acpClient.ts 的精髓:请求-响应匹配 class AcpClient { private pending = new Map(); private nextId = 1; async sendRequest(method: string, params: unknown): Promise { const id = this.nextId++; return new Promise((resolve, reject) => { this.pending.set(id, { resolve, reject }); this.output.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n"); }); } } ``` ### 3.2 Reasonix ACP server (`DeepSeek-Reasonix-main/`) | 文件 | 内容 | 可复用方式 | |---|---|---| | `src/acp/server.ts` | ACP JSON-RPC 2.0 server 类 [acp/server.ts:1-150] | **理解对端协议** — Reasonix 作为 server 时的行为 | | `src/acp/protocol.ts` | ACP 类型定义 + 工具函数 [acp/protocol.ts:1-80] | **协议规格参考** | | `src/acp/dispatch.ts` | 内核事件 → ACP `session/update` 映射 [acp/dispatch.ts:55-112] | **事件映射参考** — Reasonix 的 `kernel event → ACP` 与 mnote 的 `ACP → SSE` 是互逆过程 | | `src/acp/gates.ts` | 权限审批逻辑 [gates.ts:1-6353] | **可选参考** — 页面 AI 的 tool call 审批 | | `src/cli/commands/acp.ts` | `reasonix acp` CLI 命令,将 `CacheFirstLoop` + toolset 包装为 ACP server [acp.ts:1-339] | **Reasonix 侧入口** — 启动 Reasonix ACP 的参考实现 | | `desktop/src/protocol.ts` | 桌面客户端的 UI 事件协议 [desktop/protocol.ts:1-432] | **前端事件架构参考** — `ModelDeltaEvent`、`ToolPreparingEvent`、`ToolResultEvent` 等 25+ 事件类型的设计 | ### 3.3 复用策略 **不要「移植代码」——要「移植逻辑和接口形状」**。 Rust 端参考 `acpClient.ts` 的架构,但用 tokio async 重写。核心接口设计: ```rust // Rust ACP client — 接口形状参考 acpClient.ts pub struct AcpClient { child: tokio::process::Child, stdin: tokio::io::BufWriter, stdout: tokio::io::BufReader, pending: HashMap, next_id: u64, } impl AcpClient { pub async fn spawn(bin: &str, args: &[&str]) -> Result; pub async fn send_request(&mut self, method: &str, params: P) -> Result; pub async fn send_notification(&mut self, method: &str, params: Value); pub fn on_notification(&mut self, handler: impl Fn(String, Value)); } ``` --- ## 4. 架构设计 ### 4.1 运行时选择器 ```rust // mnote-web 配置 struct AcpRuntimeConfig { name: String, // "hermes" | "reasonix" bin: String, // "hermes" | "node" args: Vec, // ["acp"] | ["reasonix-acp.mjs"] default_model: Option, env: HashMap, } ``` mnote-web 支持多个运行时配置,用户通过 profile 选择: ``` profile "default" → runtime "hermes" (spawn hermes acp) profile "reasonix" → runtime "reasonix" (spawn node reasonix-acp-wrapper.mjs) ``` 前端获取可用运行时列表:`GET /api/hermes/client/profiles`(现有接口,扩展字段) ### 4.1.1 白名单目录即 runtime 权限边界 对 local-first 而言,真正重要的不是再做一套“文档工具能力矩阵”,而是把 workspace 白名单准确传给 runtime: ```text 登录用户 -> 解析 access-policy.json / owner / admin / grant -> 得到 allowedRoots = ["/mnt/Data1T/Mnote_data/users/.../my-space", ...] -> 启动 Hermes / Reasonix 时把 allowedRoots / cwd / read-write scope 传入 runtime -> runtime 直接在这些目录里工作 ``` 这与 VSCode / Codex 的工作模型一致: - runtime 看到的是一个受限 workspace,而不是一堆抽象页面 API。 - 页面 AI 只额外提供“当前文件是谁”以及可选选区信息。 - 对 `.md` 的普通编辑不强制走 `mnote.doc.markdown_edit`。 - 一旦文件写回磁盘,MNote 再负责把变化同步回前端显示。 ### 4.2 会话生命周期 (ACP Session Manager) ``` 用户发送消息 │ ├─ POST /api/hermes/client/sessions → 创建 session │ ├─ ACP session/new → 得到 sessionId │ └─ 返回 { sessionId, ... } │ ├─ POST /api/hermes/client/runs → 开始 run(现有接口) │ ├─ ACP session/prompt → 下发用户输入 + 页面上下文 │ └─ 返回 { runId } │ ├─ GET /api/hermes/client/events/{runId} → SSE 流 │ ├─ ACP session/update 的 6 种事件 → SSE 映射 │ ├─ agent_message_chunk → { event: "message.delta", delta: ... } │ ├─ tool_call → { event: "tool.started", tool: ..., kind: ... } │ ├─ tool_call_update → { event: "tool.completed", ... } │ ├─ agent_thought_chunk → { event: "thought.delta", delta: ... } (新增) │ ├─ usage_update → { event: "usage.updated", usage: ... } (新增) │ └─ session/update 转 ACP → prompt 返回 → SSE event "run.completed" │ └─ POST /api/hermes/client/runs/{runId}/abort → 取消 └─ ACP session/cancel ``` ### 4.3 工具桥接 当前 `hermes_tools.rs` 中注册的 mnote 工具(`mnote.doc.fetch`、`mnote.doc.markdown_edit`、`mnote.block.*`、`mnote.page.*`)对 ACP 来说只是一组 HTTP 兼容端点,而不是 local-first 普通 Markdown 编辑的唯一主路径。 对于 Reasonix 作为 runtime 的场景,仍需要一个 Reasonix-side 的工具注册包装脚本,将 mnote 兼容工具注册到 `ToolRegistry`;但 local-first 默认应优先让 runtime 直接拿到授权文件引用,在受限 cwd 中使用自身成熟的 patch/diff/文件编辑能力。 ```typescript // reasonix-acp-wrapper.mjs — ACP 包装层 // 参考: DeepSeek-Reasonix-main/src/cli/commands/acp.ts (整个文件, ~339 行) import { AcpServer } from 'reasonix/acp/server'; import { DeepSeekClient, CacheFirstLoop, ToolRegistry } from 'reasonix'; // 注册 mnote 工具 — 工具实现调用 mnote-web HTTP API const tools = new ToolRegistry(); tools.define({ name: "mnote.doc.fetch", description: "阅读当前文档的 markdown 内容", parameters: { ... }, call: async (args) => { // 调 mnote-web Rust HTTP API return fetch(`${MNOTE_WEB_URL}/api/hermes/tools/mnote/call`, { method: 'POST', body: JSON.stringify({ toolName: "mnote.doc.fetch", args }) }).then(r => r.json()); }, parallelSafe: false, }); // 启动 ACP server — 参考 acp.ts 的 acpCommand 函数 const server = new AcpServer(); const client = new DeepSeekClient({ apiKey: process.env.DEEPSEEK_API_KEY }); let sessions = new Map(); server.onRequest("session/new", async (params) => { const loop = new CacheFirstLoop({ client, tools, ... }); // ... 参考 acp.ts 第 220-330 行 }); ``` 工具的实际执行路径: ``` agent → tool call → (通过 TCP/localhost HTTP) → mnote-web Rust hermes_tools.rs → Convex / 文档系统 ``` **不需要在 Rust 侧重新注册工具到 Reasonix。** mnote-web 的工具 HTTP 端点 (`/api/hermes/tools/mnote/call`) 仍可保留,只通过 ACP 换掉了 agent runtime;但这些端点主要承担 cloud / remote agent / compat fallback,而不是把所有本地文件编辑都重新包成 mnote 工具。 ### 4.3.1 local-first 默认工作流 local-first 页面 AI 的默认工作流应是: ```text 当前页面 URL / documentId -> MNote 解析出真实 markdown 文件路径 -> MNote 校验该路径是否落在 runtime allowedRoots 白名单内 -> 把 currentFile / selection / allowedRoots 传给 runtime -> runtime 直接读写该文件 -> watcher / refresh 触发前端 page aggregate 与 tiptap 更新 ``` 只有在以下情况,runtime 才需要走 mnote 兼容工具: - runtime 本身无法直接访问本地文件 - 当前 source 是 cloud / sync replica - 当前对象不是普通 markdown,而是 mindmap / table / 资源块 / 分享受限对象 - 需要显式审计某种结构化操作 ### 4.4 前端 SSE 扩展 当前前端 SSE 事件格式 (`HermesRunEvent`): ```typescript type HermesRunEvent = | { event: "tool.started"; tool: string; preview?: string | null } | { event: "tool.completed"; tool: string; duration?: number; error?: boolean } | { event: "message.delta"; delta: string } | { event: "run.completed"; output?: string; usage?: Record } | { event: "run.failed"; error?: string }; ``` 新增字段(向后兼容): ```typescript type HermesRunEvent = /* 原有 5 种 */ | { event: "thought.delta"; // 新增 — 思考过程 delta: string; } | { event: "run.completed"; // 扩展 — 新增缓存指标 output?: string; usage?: Record; cacheHitRate?: number; // 新增:Reasonix 缓存命中率 cacheHitTokens?: number; // 新增 }; ``` 前端 AiAgentPanel 接到 `thought.delta` 后,可渲染在独立区域(参考 hermes-vscode-main 的 `agent_thought_chunk` 处理)。 ### 4.5 Profile 体系扩展 当前 `hermes_client.rs` 的 profile 机制(`configured_upstream_for_profile`)是 Hermes-HTTPS 专有的。需要扩展为通用运行时 profile: ```rust struct AgentProfile { name: String, runtime_type: RuntimeType, // Hermes | Reasonix | ACPGeneric runtime_config: AcpRuntimeConfig, api_key: Option, models: Vec, default_model: Option, } ``` 当前已存在的 `hermes_client.rs` profile 相关端点不需要大改——profile 切换逻辑不变,只是 profile 的数据结构增加了 `runtimeType` 字段。 --- ## 5. 褪去历史负担 — 模块重构路线 ### 阶段 0:现状(当前) ``` hermes_client.rs (3845 行) ├─ HTTP proxy 逻辑 (proxy_json, proxy_stream) ├─ Hermes API 硬编码 (create_run, stream_events) ├─ Session/profile/Skills/工具管理 ├─ Memory 管理 └─ 各种配置和鉴权 ``` ### 阶段 1:新增 ACP 客户端,并行运行 新增文件: - `acp_client.rs` — ACP JSON-RPC 2.0 客户端(参考 hermes-vscode-main `acpClient.ts`) - `acp_session_manager.rs` — 会话生命周期管理(参考 hermes-vscode-main `sessionManager.ts`) - `acp_runtime.rs` — 运行时管理(spawn、健康检查、切换) `hermes_client.rs` 中原有的 HTTP proxy 逻辑标志为 `#[deprecated]`,前端通过 profile 选择使用 ACP 还是旧 HTTP proxy。 ### 阶段 2:前端运行时选择器 AiAgentPanel 增加 runtime 切换下拉框,实际只是切换 mnote-web 内部使用的 profile。 ### 阶段 3:存量迁移 - `list_sessions` → ACP `session/new` + 本地记录 - `create_session` → ACP `session/new` - `create_run` + `stream_events` → ACP `session/prompt` + `session/update` 事件映射 - `abort_run` → ACP `session/cancel` - `list_profiles` → 扩展为包含 runtime_type 字段 - `gateway_health` → 改为 ACP runtime 健康检查(spawn + PING) - `list_tools` → 迁移到 `acp_session_manager.rs` ### 阶段 4:退役旧代码 当所有 profile 都已迁移到 ACP 后,`hermes_client.rs` 中原来的 HTTP proxy 代码可以删掉。Profile 的 `upstream_url` 字段不再需要——运行时由 `bin + args` 定义。 --- ## 6. 实现计划 ### 6.1 Rust ACP 客户端 (`acp_client.rs`) **参考:** `hermes-vscode-main/src/acpClient.ts` 核心接口: ```rust use tokio::process::{Command, Child}; use tokio::io::{BufReader, BufWriter, AsyncBufReadExt, AsyncWriteExt}; use serde_json::Value; use std::collections::HashMap; type PendingMap = HashMap>>; pub struct AcpClient { child: Child, writer: BufWriter, pending: Arc>, next_id: AtomicU64, } impl AcpClient { /// Spawn ACP subprocess /// 参考: acpClient.ts line 50-80 (spawn + stdio setup) pub async fn spawn(bin: &str, args: &[&str]) -> Result { let mut child = Command::new(bin) .args(args) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::inherit()) .spawn()?; let writer = BufWriter::new(child.stdin.take().unwrap()); let reader = BufReader::new(child.stdout.take().unwrap()); let pending = Arc::new(Mutex::new(HashMap::new())); // 后台读取 stdout 行 let p = pending.clone(); tokio::spawn(async move { let mut lines = reader.lines(); while let Ok(Some(line)) = lines.next_line().await { if line.trim().is_empty() { continue; } if let Ok(msg) = serde_json::from_str::(&line) { // 参考 acpClient.ts line 120-180 (onData 解析逻辑) if let Some(id) = msg.get("id").and_then(|v| v.as_u64()) { // 响应 → 匹配 pending if let Some(tx) = p.lock().unwrap().remove(&id) { let _ = tx.send(Ok(msg)); } } else if let Some(method) = msg.get("method").and_then(|v| v.as_str()) { // 通知 → 调用 onNotification } } } }); Ok(Self { child, writer, pending, next_id: AtomicU64::new(1) }) } /// Send JSON-RPC request, await response /// 参考: acpClient.ts line 95-110 (sendRequest) pub async fn request( &self, method: &str, params: P ) -> Result { let id = self.next_id.fetch_add(1, Ordering::Relaxed); let (tx, rx) = tokio::sync::oneshot::channel(); self.pending.lock().unwrap().insert(id, tx); let req = json!({ "jsonrpc": "2.0", "id": id, "method": method, "params": params }); self.writer.write_all(format!("{}\n", serde_json::to_string(&req)?).as_bytes()).await?; self.writer.flush().await?; match rx.await { Ok(Ok(val)) => serde_json::from_value(val).map_err(Into::into), _ => Err(AcpError::Timeout), } } } ``` **工作量:** ~200 行 Rust。核心逻辑直接映射自 hermes-vscode-main 的 `acpClient.ts`。 ### 6.2 ACP Session Manager (`acp_session_manager.rs`) **参考:** `hermes-vscode-main/src/sessionManager.ts` ```rust struct AcpSession { id: String, runtime: AcpRuntimeConfig, client: AcpClient, state: SessionState, run_handle: Option>, } pub struct AcpSessionManager { runtimes: HashMap, sessions: HashMap, active_profile: String, } ``` 接口: | 方法 | 对应 ACP | 参考 | |---|---|---| | `create_session(profile, page_context)` | `session/new` | sessionManager.ts `start()` / `sendPrompt()` | | `run_prompt(session_id, prompt)` | `session/prompt` | sessionManager.ts `sendPrompt()` | | `cancel(session_id)` | `session/cancel` | sessionManager.ts `cancel()` | | `on_update(handler)` | `session/update` | sessionManager.ts `handleUpdate()` | | `switch_runtime(profile)` | — | 切换 `active_profile`,刷新 runtime | **工作量:** ~300 行 Rust。 ### 6.3 Reasonix ACP wrapper (`reasonix-acp-wrapper.mjs`) **参考:** `DeepSeek-Reasonix-main/src/cli/commands/acp.ts` ```typescript // 参考 acp.ts 的 acpCommand() 函数 — ~147 行核心逻辑 // 1. 创建 AcpServer // 2. 注册 mnote 工具 (调 mnote-web HTTP) // 3. onRequest("session/new") → 创建 CacheFirstLoop // 4. onRequest("session/prompt") → loop.run() + dispatchKernelEvent // 5. onNotification("session/cancel") → aborter.abort() ``` **工作量:** ~150 行 TypeScript。直接改编自 `acp.ts` 已有代码。 ### 6.4 桥接层 — ACP → SSE **参考:** `hermes-vscode-main/src/sessionManager.ts` 的 `handleUpdate()` 方法 当前 `stream_events` 输出 SSE。改为 ACP 后: ```rust // 在 new AcpSessionManager().on_update() 中 fn on_acp_update(update: SessionUpdateParams) -> Option { match update.update.sessionUpdate { "agent_message_chunk" => Some(SseEvent { event: "message.delta", data: json!({ "delta": extract_text(&update) }), }), "tool_call" => Some(SseEvent { event: "tool.started", data: json!({ "tool": update.title, "kind": update.kind }), }), // ... 其余事件映射 } } ``` **工作量:** ~60 行 Rust。 ### 6.5 前端运行时选择器 AiAgentPanel 增加下拉框 + 切换逻辑: ```tsx ``` `switchProfile` 调 `PUT /api/hermes/client/profiles/active`(已有接口)。 **工作量:** ~50 行 TypeScript。 --- ## 7. 与现有设计的关系 ### 7.1 对 7-5 (Hermes client proxy 合同) 的影响 7-5 规定的路由路径不变: | 路由 | 当前实现 | ACP 后 | |------|----------|--------| | `GET /client/sessions` | Hermes HTTP proxy | ACP `session/new` 历史 | | `POST /client/sessions` | 本地生成 sessionId | 本地生成 + ACP `session/new` | | `POST /client/runs` | Hermes HTTP run | ACP `session/prompt` | | `GET /client/events/{runId}` | Hermes SSE 流 | ACP `session/update` → SSE | | `POST /client/runs/{runId}/abort` | Hermes HTTP abort | ACP `session/cancel` | 前端看到的 HTTP 接口不变,后端实现透明切换。 ### 7.2 对 7-14 (markdown 编辑收敛) 的影响 7-14 的最新口径是:local-first 普通 Markdown 编辑优先走“授权文件引用 + agent 原生 patch/diff”,`mnote.doc.markdown_edit` / `mnote.block.*` 退到兼容与辅助层。ACP 只是换掉 driver(从 Hermes 换成 Reasonix),不改变这个权限与执行边界。 ### 7.3 对 `page_ai_workflow.rs` 的影响 最新口径下,`block_edit_workflow` 只保留为兼容门面;local-first 普通正文编辑不应再依赖它。ACP 主要服务 agent runtime 选择、权限隔离、事件桥接和审计。 --- ## 8. 风险与缓解 | 风险 | 概率 | 缓解 | |------|------|------| | Reasonix ACP server 的 tool call 调用 mnote-web HTTP 有延迟 | 中 | 工具调用走 localhost TCP,延迟 <1ms | | Reasonix `CacheFirstLoop` 与 mnote 页面上下文的兼容性 | 低 | ACP `session/new` 传 `pageContext`,system prompt 在 Reasonix wrapper 中注入 | | 两个运行时并行维护增加心智负担 | 中 | 过渡期后退役旧 Hermes HTTP proxy,只保留 ACP | | ACP 协议字段差异(Hermes vs Reasonix camelCase/snake_case) | 低 | 在桥接层做一次字段映射即可 | | Task 7-12 说「mnote 不再建设独立 AI agent runtime」 | 不冲突 | ACP 层不是 agent runtime,是 runtime 抽象接口。mnote 仍然不建设 runtime,只是可以选接不同的 runtime | --- ## 9. 开放问题 - Reasonix 的 `DeepSeekClient` 需要的 API key 如何注入?环境变量?mnote-web 配置? - **已决定**:通过 `reasonix-acp-wrapper.mjs` 的环境变量 `DEEPSEEK_API_KEY` 注入 - 本地 `.md` 文件的 tool 实现(`mnote.doc.fetch` 的本地变体)是否也在同一套 ACP 中? - Phase C(流式 review/apply)的审批事件(`session/request_permission`)是否需要先加入 ACP 层?当前跳过,等 Phase C 再扩展。 --- ## 11. 当前实现状态 ### ✅ 已完成(2026-05-17) #### Phase A — Rust ACP 基础设施 | Step | 文件 | 状态 | 测试 | |------|------|------|------| | 3 | `acp_client.rs` | 编译通过,~487 行 | 6 单元测试通过,含 `initialize` 握手 | | 4 | `acp_types.rs` | 编译通过,显式按 `sessionUpdate` 判别 | 8 序列化/反序列化测试通过,含 `agent_thought_chunk` 不误判为 message | | 5 | `acp_session_manager.rs` | 编译通过,~530 行 | 5 测试通过(含事件派发、文本去重、thought → `ThoughtDelta`) | | 6 | `acp_runtime.rs` | 编译通过,~330 行 | 6 测试通过(含真实 Hermes CLI 连接) | | 7 | `acp_bridge.rs` | 编译通过,~260 行 | ACP→SSE 事件映射,含 `ThoughtDelta` → `thought.delta` 单测 | | 8 | `lib.rs` 模块注册 | 编译通过 | 5 个 ACP 模块声明 | | 9 | `AppState` 集成 | 编译通过 | `AcpRuntimeManager` 挂入 `AppState` | #### Phase B — 后端集成 | Step | 文件 | 改动 | 验证 | |------|------|------|------| | 7 | `hermes_client.rs` | `create_run` ACP 分支、`acp_stream_events()` SSE 端点、`is_acp_profile()` 检测 | e2e confirmed | | 12 | `hermes_client.rs` | Profile 扩展 `configured_runtime_for_profile()` | — | | 12 | `hermes_client.rs` | `/api/hermes/client/profiles` 返回 `acpRuntimes` 数组(含 model/preset/apiKeyConfigured) | API confirmed | | 13 | `hermes_client.rs` | `configured_upstream_for_profile()` 标 `#[deprecated]`(后因 caller warning 移除) | — | #### Phase C — Reasonix ACP Wrapper | Step | 文件 | 状态 | 说明 | |------|------|------|------| | 10 | `scripts/reasonix-acp-wrapper.mjs` | ~400 行 | 自包含 NDJSON JSON-RPC 2.0 服务器,无依赖 `AcpServer` | | — | API key 加载 | `loadApiKey()` 先读 `DEEPSEEK_API_KEY`,再读 `~/.reasonix/config.json` 的 `apiKey`,最后兼容 `~/.reasonix/config.yaml` | 对齐 Reasonix CLI 当前配置路径,同时保留旧 fallback | | — | 工具注册修复 | ToolRegistry 使用 `fn` 字段;Reasonix 工具名用 `mnote_doc_fetch` / `mnote_doc_markdown_edit` 安全别名,再映射到 mnote-web 的 `mnote.doc.*` HTTP 工具 | 对齐 `DeepSeek-Reasonix-main/src/tools.ts`,避免 dotted tool name 与错误 `call` 字段导致工具不可调 | | — | 事件处理修复 | `ev.role` 替代 `ev.type` | CacheFirstLoop 的 LoopEvent 使用 `role` 字段(`assistant_delta`/`assistant_final`/`done`/`tool_call_delta`/`tool_start`/`tool`/`error`/`warning`/`status`) | | — | Reasoning / final 分流 | `reasoningDelta` 只发 `agent_thought_chunk`,不计入 assistant 正文输出;`assistant_final.content` 仍可在无 delta 正文时补发 | 避免只收到 reasoning 后吞掉最终正文 | #### Phase D — 前端集成(Rust SSR) | 改动 | 文件 | 说明 | |------|------|------| | ACP 下拉选择器 | `layout.rs` | Agent 标签页新增 `` 下拉框(3 选项:默认/ACP·Hermes/ACP·Reasonix) > - 状态:`pageAiAcpRuntime` + `pageAiAcpRuntimes`(从 `/api/hermes/client/profiles` 的 `acpRuntimes` 字段加载) > - `acpRuntime` 与 `profile` 分离:ACP Hermes 保留 Hermes profile 下拉,ACP Reasonix 隐藏 Hermes profile 下拉 > - ACP Hermes 复用 Hermes HTTP 的 `SOUL.md` / `USER.md` / `MEMORY.md` 可编辑 UI;ACP Reasonix 保留 runtime info 卡片 > - 页面 AI 会话历史短期真源为按文档隔离的 localStorage,保存 session 列表、messages、active profile、active ACP runtime > - `message.delta` 进入正在输出的 assistant bubble;工具调用卡片使用 `
` 默认折叠展示参数/结果/trace > - 标题栏更新为 `ACP · Reasonix` 或 `ACP · Hermes` > - `create_run` 同时发送 `profile` 与 `acpRuntime`;后端用 `acpRuntime` 判断是否走 ACP,用 `profile` 选择 Hermes profile > > 注:Thought Delta 可视化渲染尚未实现,当前阶段允许页面不展示 thought;硬要求是 `agent_thought_chunk` 不能进入最终 assistant 正文。 | **产出** | `layout.rs` 修改 | ### [x] Step 12:profile 扩展 — 从 upstream URL 改为 runtime 配置 > ✅ 部分完成。 > - `acpRuntime` payload 字段识别 `"reasonix"` 和 `"hermes"` 两个 ACP runtime;旧的 `profile=reasonix/hermes` 仍兼容 > - `configured_runtime_for_profile(profile)` 返回对应的 runtime 名称 > - `/api/hermes/client/profiles` 响应新增 `acpRuntimes` 数组(含 model/preset/apiKeyConfigured/description) > - 向后兼容:非 ACP profile 继续使用原有的 Hermes HTTP proxy 路径 > - ACP Hermes 按本次选择的 profile 启动 `hermes -p acp`,并注入 profile provider key 环境 > > 待完成:`gateway_health` 适配 ACP runtime health check、`MNOTE_WEB_ACP_DEFAULT_RUNTIME` 环境变量支持。 | **产出** | `hermes_client.rs` 修改 | ### [x] Step 13:Hermes HTTP proxy 代码标为 deprecated > ✅ 完成。`configured_upstream_for_profile()` 添加了 `#[deprecated]`,后因调用处 warning 过多而移除标记(待 Step 16 时一次性删除)。 ### [x] Step 14:前端运行时切换验证 e2e > ✅ 已通过。后端真实 SSE 探针与浏览器 UI 严格验证均通过。 > > 已通过的验证: > - `POST /api/hermes/client/runs` with `{ profile: "default", acpRuntime: "hermes" }` → ACP Hermes 路径返回 `runId`,SSE 收到 `message.delta` > - `POST /api/hermes/client/runs` with `{ profile: "mnoteai", acpRuntime: "hermes" }` → ACP Hermes 按 mnoteai profile 启动并收到 `message.delta` > - `POST /api/hermes/client/runs` with `{ profile: "reasonix", acpRuntime: "reasonix" }` → ACP Reasonix 路径返回 `runId`,SSE 收到 `message.delta` > - `CacheFirstLoop.step("hi")` 直接调用 → LLM 返回正确中文回复 > - Wrapper `initialize` + `session/new` → 握手成功 > - Wrapper `session/prompt` → 返回 `stopReason=end_turn`,产生 `agent_message_chunk` > - 浏览器验证 skill:创建临时页面,切换 `ACP · Hermes` + `mnoteai` profile,发送 marker prompt,最终 assistant 正文严格等于 `ACP_HERMES_BROWSER_OK_mp9pfqf6` > - 浏览器验证 skill:切换 `ACP · Reasonix`,发送 marker prompt,最终 assistant 正文严格等于 `ACP_REASONIX_BROWSER_OK_mp9pfqf6` > - 2026-05-17 追加:浏览器验证 skill 已扩展,检查 ACP Hermes memory 编辑器、Hermes 回复 reload 后历史恢复,并记录流式占位证据 `sawStreaming` > - 2026-05-17 追加验证通过:ACP Hermes / ACP Reasonix 回复均严格等于 marker;两者 `sawStreaming=true`;Hermes 回复刷新后仍可见 > > 证据: > - `/mnt/Data1T/mnote/tmp/page-ai-acp-browser-UAYwyM/result.json` > - `/mnt/Data1T/mnote/tmp/page-ai-acp-browser-UAYwyM/01-acp-hermes-reply.png` > - `/mnt/Data1T/mnote/tmp/page-ai-acp-browser-UAYwyM/02-acp-reasonix-reply.png` > - `/mnt/Data1T/mnote/tmp/page-ai-acp-browser-wcJzrj/result.json` > - `/mnt/Data1T/mnote/tmp/page-ai-acp-browser-wcJzrj/00-acp-hermes-memory-ui.png` > - `/mnt/Data1T/mnote/tmp/page-ai-acp-browser-wcJzrj/01-acp-hermes-reply.png` > - `/mnt/Data1T/mnote/tmp/page-ai-acp-browser-wcJzrj/01b-acp-hermes-history-after-reload.png` > - `/mnt/Data1T/mnote/tmp/page-ai-acp-browser-wcJzrj/02-acp-reasonix-reply.png` > > 仍未纳入本阶段:`thought.delta` 可视化渲染、`usage.updated` 展示。当前页面不显示 thought 是可接受行为;关键是 thought 不能误进 `message.delta`。 ### [ ] Step 15:压力测试 — 确认同时多会话稳定性 > 待 Step 14 通过后执行。 ### [ ] Step 16:退役旧的 Hermes HTTP proxy 代码 > 2026-05-18 已开始执行第一阶段退役: > > - 页面 AI 前端默认 `acpRuntime=reasonix`,不再以空 runtime 表示“默认 Hermes HTTP”;用户仍可在下拉中切换到 `ACP · Hermes`。 > - mnote-web 服务端默认把 `/api/hermes/client/runs` 的空 `acpRuntime` 归入 ACP 默认 runtime(默认 `reasonix`),避免继续落到 `configured_upstream_for_profile()` 的 HTTP proxy 分支。 > - `GET /api/hermes/client/gateway/health` 在 ACP 默认路径下返回 ACP transport 状态,不再探测 `8642/8644` HTTP gateway。 > - 旧 Hermes HTTP proxy 合同与 `/v1/runs` 默认主链设计稿已移入 `recycle/design/07-ai/retired-http-hermes/`,避免干扰后续 ACP 主线判断。 > > 剩余工作:删除或进一步隔离 `hermes_client.rs` 内的 HTTP proxy 兼容分支;当前阶段只保留显式兼容开关,避免一次性删除影响 profile/memory/tools 管理能力。 ### [ ] Step 17:基准测试 — Reasonix 缓存收益量化 > 待 Step 14 通过后执行。测试场景已设计,指标定义明确。 --- ## 附录:文件依赖关系图 ``` step-3 acp_client.rs │ depends on: none ▼ step-4 acp_types.rs step-6 acp_runtime.rs │ depends on: none │ depends on: serde_json ▼ ▼ step-5 acp_session_manager.rs ←───────────┘ │ depends on: acp_client, acp_types, acp_runtime ▼ step-7 hermes_client.rs 改造 │ depends on: acp_session_manager ▼ step-8 mod.rs 添加模块 │ depends on: step-3,4,5,6 ▼ step-9 AppState 改造 │ depends on: acp_runtime, acp_session_manager ▼ step-10 reasonix-acp-wrapper.mjs (独立, 可并行) │ depends on: npm reasonix 包 ▼ step-11 AiAgentPanel.tsx 修改 step-12 profile 扩展 │ depends on: step-7 │ depends on: hermes_client.rs ▼ ▼ step-13 标 deprecated (与 step-10 可并行) │ ▼ step-14 e2e 验证 │ ├── step-15 压力测试 │ └── step-16 退役旧代码 │ └── step-17 基准测试 ``` ### 关键并行路径 ``` step-3 ─→ step-5 ─→ step-7 ─→ step-11 ─→ step-14 ↗ step-10 (Reasonix wrapper, 可并行) step-4 ─→ step-5 step-6 ─→ step-5, step-9 ``` ### 每次 AI 执行前必须确认 1. `run_command pwd` → `/mnt/Data1T/mnote`(确认 cwd) 2. 写文件路径用相对路径 `rust/crates/...` 而非 `/mnt/Data1T/mnote/...` 3. 先 `search_content` / `read_file` 确认目标文件最新内容,避免 edit_file SEARCH 不匹配 4. 每个 step 完成后 `todo_write` 更新进度