Files
mnote/design/07-ai/process/7-15-page-ai-acp-agent-runtime-unified-layer-v1.md
T

44 KiB
Raw Blame History

7-15 [process] 页面 AI ACP Agent Runtime 统一抽象层 v1

创建时间:2026-05-17

当前状态:PROCESS

本稿目的:

  1. 在 mnote-web 中引入 ACPAgent Client Protocol)作为统一 agent runtime 抽象层
  2. 使 Hermes(当前)与 Reasonix(缓存优先)可互换,前端下拉切换
  3. 褪去当前 hermes_client.rs 中的 Hermes-HTTPS-proxy 硬编码,改为 ACP JSON-RPC 通用连接器
  4. 复用现有参考代码,最小化重复实现工作

关联文档:

  • /mnt/Data1T/mnote/design/07-ai/done/7-5-hermes-client-proxy-contract-v1.md
  • /mnt/Data1T/mnote/design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md
  • /mnt/Data1T/mnote/design/07-ai/process/7-14-online-local-ai-markdown-editing-convergence-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 (fast-path 块编辑)
       └─ local_rule planner, 不经过 Hermes

问题:

  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 (不变)
       └─ fast-path 块编辑

ACP 是整个架构的支点——它是一个开放协议,不是某个产品的私有接口。


2. ACP 协议标准

ACPAgent 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"
tool_call_update 工具状态变更 toolCallId, `status: "in_progress"
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] 逻辑移植到 Rusttokio::process::Command spawn + BufReader 按行读取 + 请求 ID 映射表
src/sessionManager.ts 会话生命周期管理:session/new → session/prompt → session/cancel,去重,事件派发 [sessionManager.ts:37-294] 逻辑移植到 RustHashMap<String, Session> 管理活跃会话
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] 前端渲染参考

核心设计复用:

// acpClient.ts 的精髓:请求-响应匹配
class AcpClient {
  private pending = new Map<number, PendingRequest>();
  private nextId = 1;

  async sendRequest(method: string, params: unknown): Promise<unknown> {
    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] 前端事件架构参考ModelDeltaEventToolPreparingEventToolResultEvent 等 25+ 事件类型的设计

3.3 复用策略

不要「移植代码」——要「移植逻辑和接口形状」

Rust 端参考 acpClient.ts 的架构,但用 tokio async 重写。核心接口设计:

// Rust ACP client — 接口形状参考 acpClient.ts
pub struct AcpClient {
    child: tokio::process::Child,
    stdin: tokio::io::BufWriter<tokio::process::ChildStdin>,
    stdout: tokio::io::BufReader<tokio::process::ChildStdout>,
    pending: HashMap<u64, PendingRequest>,
    next_id: u64,
}

impl AcpClient {
    pub async fn spawn(bin: &str, args: &[&str]) -> Result<Self>;
    pub async fn send_request<P, R>(&mut self, method: &str, params: P) -> Result<R>;
    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 运行时选择器

// mnote-web 配置
struct AcpRuntimeConfig {
    name: String,          // "hermes" | "reasonix"
    bin: String,           // "hermes" | "node"
    args: Vec<String>,     // ["acp"] | ["reasonix-acp.mjs"]
    default_model: Option<String>,
    env: HashMap<String, String>,
}

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.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.fetchmnote.doc.markdown_editmnote.block.*mnote.page.*)对 ACP 来说只是一组 HTTP 端点。

对于 Reasonix 作为 runtime 的场景,需要一个 Reasonix-side 的工具注册包装脚本,将 mnote 工具注册到 ToolRegistry

// 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。

4.4 前端 SSE 扩展

当前前端 SSE 事件格式 (HermesRunEvent)

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<string, unknown> }
  | { event: "run.failed"; error?: string };

新增字段(向后兼容):

type HermesRunEvent = /* 原有 5 种 */ | {
  event: "thought.delta";        // 新增 — 思考过程
  delta: string;
} | {
  event: "run.completed";        // 扩展 — 新增缓存指标
  output?: string;
  usage?: Record<string, unknown>;
  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

struct AgentProfile {
    name: String,
    runtime_type: RuntimeType,  // Hermes | Reasonix | ACPGeneric
    runtime_config: AcpRuntimeConfig,
    api_key: Option<String>,
    models: Vec<ModelConfig>,
    default_model: Option<String>,
}

当前已存在的 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

核心接口:

use tokio::process::{Command, Child};
use tokio::io::{BufReader, BufWriter, AsyncBufReadExt, AsyncWriteExt};
use serde_json::Value;
use std::collections::HashMap;

type PendingMap = HashMap<u64, tokio::sync::oneshot::Sender<Result<Value, AcpError>>>;

pub struct AcpClient {
    child: Child,
    writer: BufWriter<ChildStdin>,
    pending: Arc<Mutex<PendingMap>>,
    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<Self> {
        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::<Value>(&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<P: Serialize, R: DeserializeOwned>(
        &self, method: &str, params: P
    ) -> Result<R> {
        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

struct AcpSession {
    id: String,
    runtime: AcpRuntimeConfig,
    client: AcpClient,
    state: SessionState,
    run_handle: Option<JoinHandle<()>>,
}

pub struct AcpSessionManager {
    runtimes: HashMap<String, AcpRuntimeConfig>,
    sessions: HashMap<String, AcpSession>,
    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

// 参考 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.tshandleUpdate() 方法

当前 stream_events 输出 SSE。改为 ACP 后:

// 在 new AcpSessionManager().on_update() 中
fn on_acp_update(update: SessionUpdateParams) -> Option<SseEvent> {
    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 增加下拉框 + 切换逻辑:

<select value={activeProfile} onChange={switchProfile}>
  <option value="default">Hermes(默认)</option>
  <option value="reasonix">Reasonix(缓存优先)</option>
</select>

switchProfilePUT /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 确立的「两层操作模型」(mnote.doc.markdown_edit 主 + mnote.block.* 辅)不受影响——工具在 Rust 侧 hermes_tools.rs 实现不变。ACP 只是换掉了 driver(从 Hermes 换成 Reasonix),不改 driver 调用的工具。

7.3 对 page_ai_workflow.rs 的影响

不影响。block_edit_workflow 作为独立 fast-path 与 ACP 无关。


8. 风险与缓解

风险 概率 缓解
Reasonix ACP server 的 tool call 调用 mnote-web HTTP 有延迟 工具调用走 localhost TCP,延迟 <1ms
Reasonix CacheFirstLoop 与 mnote 页面上下文的兼容性 ACP session/newpageContextsystem 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

Step 文件 状态 测试
3 acp_client.rs 编译通过,475 行 6 单元测试通过
4 acp_types.rs 编译通过,440 行 7 单元测试通过
5 acp_session_manager.rs 编译通过,~530 行 4 测试通过(含事件派发)
6 acp_runtime.rs 编译通过,~330 行 5 测试通过,含真实 Hermes 连接测试
7 acp_bridge.rs + hermes_client.rs 修改 编译通过 新增 acp_stream_events() SSE 端点
8 lib.rs 模块注册 编译通过
9 AppState 集成 编译通过
10 scripts/reasonix-acp-wrapper.mjs 已创建,~250 行 需安装 npm install reasonix 后测试
12 Profile 扩展 configured_runtime_for_profile() 编译通过
13 HTTP proxy 标 #[deprecated] 编译通过(6 个 warning

📋 待完成

Step 工作 前置 估算
11 前端运行时选择器(AiAgentPanel.tsx Step 7 ~半天
14 e2e 验证:Hermes ACP + Reasonix ACP Step 10 ~半天
15 压力测试:多会话、进程管理 Step 14 ~半天
16 退役旧 HTTP proxy 代码 Step 14 稳定后 ~1 天
17 基准测试:缓存收益量化 Step 10 ~半天

10. 详细执行 Checklist(顺序执行)

以下 checklist 按依赖关系排序,每个 step 标注了参考文件(可直接读的代码)、产出文件验证方法。执行时从 step-1 开始,完成后由 AI 调用 todo_write 标记进度后进入下一步。

Step 1:读取参考代码,熟悉 ACP 协议细节

内容
目的 确认 ACP JSON-RPC 协议的方法名、字段名、事件类型,确保后续实现与 Hermes/Reasonix 兼容
参考 reference-code/hermes-vscode-main/src/protocol.tsACP 事件解析)、reference-code/DeepSeek-Reasonix-main/src/acp/protocol.tsACP 类型定义)、reference-code/hermes-vscode-main/src/acpClient.tsACP 客户端完整实现)
产出 无代码产出,仅阅读确认
验证 能在脑中回答:session/update 有几种 sessionUpdate 变体?每个变体有哪些必选字段? session/prompt 的 params 结构是什么?

Step 2:确认 run_command 的 cwd 与项目根一致

内容
目的 确保后续所有文件操作路径正确,不再触发 sandbox 偏移
操作 run_command pwd 确认输出为 /mnt/Data1T/mnote
验证 输出包含 /mnt/Data1T/mnote

Step 3:创建 acp_client.rs — ACP JSON-RPC 2.0 客户端

内容
目的 实现通用的 ACP 协议传输层:spawn 子进程、读写 NDJSON、请求/响应/通知路由
参考 reference-code/hermes-vscode-main/src/acpClient.ts(完整参考,~220 行):spawn 逻辑 (L50-80)、onData 解析 (L120-180)、sendRequest (L95-110)、sendNotification (L115-118)
路径 rust/crates/mnote-web/src/acp_client.rs(新建)
结构 pub struct AcpClient { child, writer, reader, pending: HashMap<u64, OneshotSender>, next_id }
方法 spawn(bin, args) → Resultrequest(method, params) → Result<R>notification(method, params)on_notification(handler)close()
参考字段映射 JSON-RPC idpending key;响应匹配 id;通知匹配 method 字段
细节 stdin 用 BufWriter(行缓冲),stdout 用 BufReader + lines() 逐行读;后台 tokio task 处理 incoming 行;request() 返回 oneshot::Receiver;超时处理用 tokio::time::timeout(默认 5min
验证 单元测试:mock stdin/stdout 子进程,发送 session/new 请求,验证收到响应;发送 notification,验证 handler 被调用

Step 4:创建 ACP 协议类型定义 — acp_types.rs

内容
目的 ACP 协议的 Rust 类型定义,序列化/反序列化用 serde
参考 reference-code/DeepSeek-Reasonix-main/src/acp/protocol.tsACP 类型定义,~80 行)、reference-code/hermes-vscode-main/src/protocol.ts(解析逻辑,~120 行)
路径 rust/crates/mnote-web/src/acp_types.rs(新建)
结构 InitializeParams/ResultSessionNewParams/ResultSessionPromptParams/ResultSessionCancelParamsSessionUpdateParamsContentBlocktext/resource/image/audio)、SessionUpdateKindenum 6 种变体)
字段注意 Hermes 和 Reasonix 都使用 camelCase JSON 字段(sessionUpdatetoolCallId),对应 #[serde(rename_all = "camelCase")]
验证 单元测试:JSON 反序列化 acp/dispatch.ts 中的 session/update 案例、session/prompt 的请求/响应序列化-反序列化往返

Step 5:创建 acp_session_manager.rs — 会话生命周期管理

内容
目的 管理 ACP 会话生命周期:session/new → session/prompt → session/cancel,将 session/update 事件派发给 mnote-web 各模块
参考 reference-code/hermes-vscode-main/src/sessionManager.ts(完整参考,~294 行):SessionManager 类 (L37-294)、handleUpdate (L130-280)、sendPrompt (L80-128)、cancel (L280-294)
路径 rust/crates/mnote-web/src/acp_session_manager.rs(新建)
结构 pub struct AcpSessionManager { runtimes: HashMap<String, AcpRuntimeConfig>, sessions: HashMap<String, AcpSession>, active_profile: String }struct AcpSession { id, client, state, run_handle, page_context }
方法 create_session(profile, page_context) → sessionId(调 ACP session/new)、run_prompt(session_id, prompt_blocks, on_event) → JoinHandle(调 ACP session/prompt,注册 session/update handler)、cancel(session_id)(调 ACP session/cancel)、switch_runtime(profile_name)(切换 active_profile,关闭旧 sessions,创建新 runtime)、SessionUpdateKindAcpSessionEvent 的映射
事件映射 agent_message_chunkAcpSessionEvent::TextDelta { text }agent_thought_chunkAcpSessionEvent::ThoughtDelta { text }tool_callAcpSessionEvent::ToolCall { id, title, kind, status }tool_call_updateAcpSessionEvent::ToolCallUpdate { id, status, content? }usage_updateAcpSessionEvent::UsageUpdate { used, size }session_info_updateAcpSessionEvent::SessionInfo { title }
去重逻辑 参考 hermes-vscode-main/src/protocol.tsdeduplicateChunk() 函数——ACP 会重发完整文本作为可靠性 fallback,需检测并丢弃重复
验证 单元测试:构造 mock AcpClientcreate_session → 验证发送 session/newrun_prompt → 验证发送 session/prompt;模拟 session/update notification,验证 on_event 回调被正确调用

Step 6:创建 acp_runtime.rs — 运行时管理

内容
目的 管理 agent runtime 进程的 spawn、健康检查、自动重启
路径 rust/crates/mnote-web/src/acp_runtime.rs(新建)
结构 pub struct AcpRuntimeManager { runtimes: HashMap<String, AcpRuntimeConfig>, active: Mutex<Option<String>> }pub struct AcpRuntimeConfig { name, bin, args, env }
方法 register_runtime(config)spawn_runtime(name) → AcpClienthealth_check(name) → boolspawn 进程 + 发送 initialize 请求,超时 5s)、shutdown_runtime(name)switch_to(name) → Result(先 shutdown 当前 active,再 spawn 新的)
配置来源 从环境变量 / 配置文件读取(MNOTE_WEB_ACP_RUNTIMES JSON),当前固定配置:hermes{ bin: "hermes", args: ["acp"] }reasonix{ bin: "node", args: ["reasonix-acp-wrapper.mjs"] }
profile 扩展 当前 configured_upstream_for_profile() 返回 Hermes HTTP URL;改为返回 AcpRuntimeConfig。已有 profile 系统(active_profile_name, configured_upstream_for_profile) 保持接口不变,内部实现切换
验证 运行 hermes acp(需本地安装),发送 session/new 验证返回 sessionId;无 Hermes 环境时 mock 子进程验证健康检查逻辑

Step 7:集成 ACP Session Manager 到 Hermes routes

内容
目的 hermes_client.rs 的现有端点可以选择使用 ACP 而非 HTTP proxy
参考 hermes_client.rs 中现有 create_run / stream_events / abort_run
操作 hermes_client.rs 中导入 AcpRuntimeManagerAcpSessionManager;当当前 profile 的 runtime_type == "acp" 时走 ACP 路径,否则走原有 HTTP proxy 路径;create_runacp_session_manager.run_prompt()(替代 Hermes HTTP POST /api/hermes/runs);stream_events → 从 AcpSessionManageron_event 回调中发出 SSE(替代 Hermes HTTP GET /api/hermes/runs/{id}/events);abort_runacp_session_manager.cancel()(替代 Hermes HTTP POST /api/hermes/runs/{id}/abort
SSE 桥接函数 新增 fn acp_event_to_sse(event: AcpSessionEvent) → Option<SseEvent>。映射表:TextDelta("msg"){ event: "message.delta", data: { delta: "msg" } }ThoughtDelta("t"){ event: "thought.delta", data: { delta: "t" } }(新增);ToolCall("id","title","kind","pending"){ event: "tool.started", data: { tool: "title", preview: null } }ToolCallUpdate("id","completed",content){ event: "tool.completed", data: { tool: "...", duration: null } }UsageUpdate(used,size){ event: "usage.updated", data: { used, size } }(新增);SessionInfo(title) → 忽略(mnote 前端不需要)
验证 hermes acp(本地已安装)做 e2e 测试:前端发消息 → ACP session/prompt → 收到 SSE 流 → 显示工具调用 → 显示最终回复

Step 8:编辑全局 Router 添加 ACP 模块

内容
目的 让 ACP 模块被编译,各模块之间可引用
参考 rust/crates/mnote-web/src/routes/mod.rs 中当前 Hermes routes 的注册方式 (nest at hermes_base_path)
操作 mod.rs 中添加 mod acp_client;mod acp_types;mod acp_session_manager;mod acp_runtime;;初始化时创建 AcpRuntimeManager,注册 Hermes runtime 和 Reasonix runtime(如果配置存在);将 AcpRuntimeManager 放入 AppStateExtension
验证 cargo build 通过

Step 9AppState 改造 — 加入 AcpRuntimeManager

内容
目的 让路由 handler 可以访问运行时管理器
参考 rust/crates/mnote-web/src/app.rs 中的 AppState 结构
操作 AppState 中添加 acp_runtime: Arc<AcpRuntimeManager> 字段;AppState::new() 中根据配置注册 hermes 和/或 reasonix runtime
验证 cargo build 通过;health endpoint 返回 ACP runtime 状态

Step 10:创建 Reasonix ACP wrapper 脚本

内容
目的 实现 Reasonix ACP server,让 mnote-web 可以 spawn("node", ["reasonix-acp-wrapper.mjs"]) 连接
参考 reference-code/DeepSeek-Reasonix-main/src/cli/commands/acp.ts(完整参考,~339 行):acpCommand() (L195-339)、loadMcpServers() (L88-193)
路径 scripts/reasonix-acp-wrapper.mjs(新建)
结构 import AcpServer from reasonix/acp/server、import DeepSeekClient, CacheFirstLoop, ToolRegistry from reasonix;从环境变量读取 DEEPSEEK_API_KEYMNOTE_WEB_URL(默认为 http://127.0.0.1:3000);创建 ToolRegistry,注册 mnote.doc.fetchmnote.doc.markdown_edit 工具(工具实现通过 HTTP 调用 MNOTE_WEB_URL/api/hermes/tools/mnote/call);AcpServer + onRequest("session/new") → 创建 CacheFirstLoop(参考 acp.ts L220-260);onRequest("session/prompt")loop.run() + dispatchKernelEvent()(参考 acp.ts L260-330);onNotification("session/cancel")aborter.abort()(参考 acp.ts L330-339);启动后 server.done() 等待 stdin 关闭
MNOTE_WEB_URL 寻址 wrapper 脚本在本地运行,通过 http://127.0.0.1:3000 调 mnote-web 的 tool API——因为 hermes_tools.rs 的 tool 实现在 Rust 侧,wrapper 不重复实现工具逻辑
验证 手动测试:node scripts/reasonix-acp-wrapper.mjs 启动后,用标准 ACP client 发送 session/new + session/prompt,验证返回正常;工具调用可正确通过 mnote-web 读写文档

Step 11:添加运行时选择器的前端支持

内容
目的 在页面 AI 面板中增加运行时切换能力
参考 hermes-vscode-main/src/chatPanel.tsACP 事件 → UI 渲染)、hermes-vscode-main/src/webview/main.tswebview 事件处理)
前端路径 wolai-frontend/src/components/ai-agent/AiAgentPanel.tsx
操作 扩展 profile 获取接口 GET /api/hermes/client/profiles,解析 runtimeType 字段;增加 <select> 下拉框显示可用 runtime"Hermes / Reasonix");切换时调用 PUT /api/hermes/client/profiles/active;切换后自动刷新当前会话
新增 Thought Delta 渲染 在 AiAgentPanel 中处理新增的 thought.delta SSE 事件,渲染在对话气泡的独立区域(灰色小字或可折叠的 reasoning 面板,参考 hermes-vscode-main webview 对 agent_thought_chunk 的渲染)
验证 切换 runtime → 发送消息 → 确认 AI 回复流畅;Reasonix 模式下确认缓存指标显示在 header 中

Step 12profile 扩展 — 从 upstream URL 改为 runtime 配置

内容
目的 让 profile 不再持有 upstream_urlHermes HTTPS),而是持有 runtime_nameACP 通用)
参考 hermes_client.rsconfigured_upstream_for_profile()profile_gateway_status()
操作 扩展 profile 数据结构:添加 runtime_type: Option<String>"hermes_http"
health check 改造 gateway_health() 当前只 probe Hermes HTTP upstream;改为:如果 profile 是 acp 类型,则调用 acp_runtime.health_check(),否则继续 probe HTTP upstream
profile 默认值 新增环境变量 MNOTE_WEB_ACP_DEFAULT_RUNTIME:默认 hermes;设为 reasonix 则默认使用 Reasonix
验证 不改变现有 hermes_http 行为;新增 acp 类型 profile 的 health check 正常返回

Step 13Hermes HTTP proxy 代码标为 deprecated

内容
目的 标记旧代码,避免新开发继续依赖
操作 hermes_client.rs 中 HTTP proxy 相关函数(proxy_jsonproxy_streamconfigured_upstream_for_profile 等)添加 #[deprecated(note = "迁移到 ACP Session Manager")];不影响编译,只是 IDE 和 CI 提示
验证 cargo build 无 warningdeprecated 函数被自身使用时默认不 warn)

Step 14:前端运行时切换验证 e2e

内容
目的 同时验证 Hermes ACP 和 Reasonix ACP 两条路径都能正常走通
环境准备 启动 mnote-web (cargo run)、确保 hermes 命令可用、确保 nodereasonix npm 包已安装、确保 Reasonix wrapper 脚本就绪
测试路径 在浏览器页面 AI 面板中选择 "Hermes" → 发送编辑请求 → 确认工具调用和回复正常;切换到 "Reasonix" → 发送同样的编辑请求 → 确认工具调用和回复正常(且 header 显示缓存命中率);块编辑 fast-path (/api/page-ai/block-edit-workflow) 独立测试,不受 ACP 切换影响
测试账号 使用默认测试账号 mnote.e2e@example.com,见项目记忆
验证 两种 runtime 都能正常读写文档;Reasonix 模式下 message.delta 流式响应速度不慢于 Hermes

Step 15:压力测试 — 确认同时多会话稳定性

内容
目的 确保多个 ACP session 并发时不出现进程冲突、内存泄漏
操作 通过 Playwright 或手动测试:同时打开 3 个页面 AI 面板,分别发送不同的编辑请求;观察所有会话是否独立完成;检查 AcpSessionManager 的 sessions map 是否在会话结束后正确清理;检查子进程数量是否失控(每个 runtime 应有进程上限,可在 AcpRuntimeConfig 中添加 max_concurrent_sessions,默认 10
验证 所有会话都能正常完成;无僵尸子进程残留;top 确认 Reasonix Node.js 进程数可控

Step 16:退役旧的 Hermes HTTP proxy 代码

内容
前提 所有 production profile 都已迁移到 ACP;灰度观察期至少 1 周无回退
操作 删除 hermes_client.rs 中所有 #[deprecated] 的函数(proxy_jsonproxy_streamconfigured_upstream_for_profile 等);删除环境变量 MNOTE_WEB_HERMES_UPSTREAM_URL 的解析代码;统一所有 profile 为 runtime_type: "acp";不再依赖 hermes 命令的 HTTP gateway 模式
验证 cargo build、常规 e2e 测试全部通过

Step 17:基准测试 — Reasonix 缓存收益量化

内容
目的 收集 Reasonix prefix cache 的实际收益数据,作为后续切流的决策依据
操作 设计 3 轮测试:场景 A(同一文档连续编辑 5 次)——对同一文档连续发 5 次 mnote.doc.markdown_edit,记录每次的 prompt_cache_hit_tokensprompt_cache_miss_tokens场景 B(不同文档交替编辑 5 次)——交替编辑 5 个不同文档,统计 cache hit rate。场景 C(长会话 10 轮对话)——在同一 session 中连续发 10 条消息,统计 cache hit rate 变化趋势
指标 cache_hit_rate = hit_tokens / (hit + miss)Reasonix 的 CacheFirstLoop 每轮迭代后暴露 ctx.prefixHashctx.stats
对比基线 同样 3 个场景下 Hermes ACP 的 cache hit rate(理论上接近 0%
产出 记录到 benchmarks/reasonix-cache-report.md
通过标准 Reasonix 场景 A 的 cache hit rate ≥ 80%Reasonix 自述 ~90%+);场景 B ≥ 50%;场景 C ≥ 60%

附录:文件依赖关系图

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 更新进度