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

55 KiB
Raw Blame History

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

创建时间:2026-05-17

当前状态:PROCESS

2026-05-21 Batch J 口径补充:

  • 本稿的 ACP runtime 核心实现已完成并在当前页面 AI 主链中作为默认 runtime 边界使用;Hermes HTTP proxy 默认关闭,只在显式 compat 开关下保留。
  • Step 15(多会话压力测试)、Step 16(旧 HTTP proxy cleanup)、Step 17Reasonix 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 中引入 ACPAgent 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/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/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 协议标准

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.1.1 白名单目录即 runtime 权限边界

对 local-first 而言,真正重要的不是再做一套“文档工具能力矩阵”,而是把 workspace 白名单准确传给 runtime

登录用户
  -> 解析 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.fetchmnote.doc.markdown_editmnote.block.*mnote.page.*)对 ACP 来说只是一组 HTTP 兼容端点,而不是 local-first 普通 Markdown 编辑的唯一主路径。

对于 Reasonix 作为 runtime 的场景,仍需要一个 Reasonix-side 的工具注册包装脚本,将 mnote 兼容工具注册到 ToolRegistry;但 local-first 默认应优先让 runtime 直接拿到授权文件引用,在受限 cwd 中使用自身成熟的 patch/diff/文件编辑能力。

// 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 的默认工作流应是:

当前页面 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)

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 的最新口径是: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/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

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 事件映射,含 ThoughtDeltathought.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.jsonapiKey,最后兼容 ~/.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 标签页新增 <select data-page-ai-acp-runtime>2026-05-18 起只保留 ACP · Hermes / ACP · Reasonix,移除“默认 (Hermes HTTP)”选项
状态存储 layout.rs pageAiAcpRuntime + pageAiAcpRuntimes/api/hermes/client/profiles 加载
运行时切换 layout.rs acpRuntime 只表示运行时/传输层;Hermes ACP 继续保留当前 Hermes profileReasonix ACP 使用 profile=reasonix
UI 自适应 layout.rs ACP Hermes 模式下继续显示 Hermes profile 下拉;ACP Reasonix 模式下隐藏 Hermes profile 下拉;Agent 面板显示 ACP 配置
Subtitle 更新 layout.rs 选择 ACP 后标题栏显示 ACP · ReasonixACP · Hermes

Phase E — 浏览器验证 Skill

文件 状态 说明
/home/lix/.codex/skills/page-ai-browser-verify/SKILL.md 已创建 固化页面 AI 浏览器验证流程,后续遇到 ACP Hermes / ACP Reasonix / 回复质量问题时复用
/home/lix/.codex/skills/page-ai-browser-verify/scripts/verify_mnote_page_ai_acp.js 已创建 真实登录、创建临时页、切换 ACP Hermes/Reasonix、捕获 /api/hermes/client/runs、截图,并断言 assistant 正文去空白后严格等于 marker

验证命令:

node /home/lix/.codex/skills/page-ai-browser-verify/scripts/verify_mnote_page_ai_acp.js

最新证据(2026-05-17):

路径
结构化结果 /mnt/Data1T/mnote/tmp/page-ai-acp-browser-UAYwyM/result.json
ACP Hermes 截图 /mnt/Data1T/mnote/tmp/page-ai-acp-browser-UAYwyM/01-acp-hermes-reply.png
ACP Reasonix 截图 /mnt/Data1T/mnote/tmp/page-ai-acp-browser-UAYwyM/02-acp-reasonix-reply.png

验收标准已收紧:不能只判断 UI “包含 marker”;必须确认最终 assistant 正文是干净最终答案,不能出现推理解释、工具说明、Theuserisasking... 这类 glued reasoning 文本。

🐛 问题与解决状态(2026-05-17

问题 1ACP Reasonix SSE 流返回空(stop_reason=Error

详情
状态 已修复;后端 SSE 探针与前端真实浏览器验证均通过
原现象 ACP prompt completed: stop_reason=Error,耗时 ~300msSSE 流无任何 message.delta 事件
已排除 API key 有效(CacheFirstLoop.step("hi") 直接调用正常,LLM 返回中文回复)
已排除 Wrapper 语法正确(node --check 通过,initialize + session/new 验证通过)
已排除 SSE 转发管道 race condition(频道建立已移至 prompt 启动前)
已排除 message 字段名匹配(payload.get("message") 修正)
根因 scripts/reasonix-acp-wrapper.mjs 与 Reasonix 当前 API 不匹配:ToolRegistry.register() 需要 fn 而不是 callCacheFirstLoop.step() 产出的是 ev.role,不是旧的 ev.type;本机 Reasonix API key 存在 ~/.reasonix/config.json,旧 wrapper 只读 YAML
已修复 wrapper 改为读取 config.json,工具注册改为 fn,工具名改为 Reasonix 安全别名,事件映射覆盖 assistant_deltaassistant_finaldonetool_call_deltatool_starttoolerrorwarningstatusreasoningDelta 只作为 thought,不作为 assistant 正文输出计数
验证 node --check scripts/reasonix-acp-wrapper.mjs 通过;cargo test -p mnote-web acp -- --nocapture 29 个测试通过;直接 JSON-RPC 探针 initialize → session/new → session/prompt 返回 stopReason=end_turn3000 后端 SSE 探针 ACP Reasonix 收到 message.delta + run.completed;真实浏览器验证见 tmp/page-ai-acp-browser-UAYwyM/

问题 1bACP Hermes profile 选择丢失

详情
状态 已修复,后端真实 SSE 探针已通过
原现象 选择 ACP · Hermes 后,前端把 pageAiAcpRuntime 当成 profile 发送,导致 profile 固定为 hermes,无法沿用既有 Hermes profile 选择
根因 profileacpRuntime 两个概念混用:profile 应表示 Hermes agent/profile(如 defaultmnoteai),acpRuntime 才表示运行时传输层(hermes / reasonix
已修复 前端 create_run 发送 { profile: pageAiRunProfile(), acpRuntime }ACP Hermes 保留 profile 下拉;后端按 acpRuntime 进入 ACP 分支,并按本次 profile 启动 hermes -p <profile> acp
兼容处理 ACP Hermes subprocess 会从所选 profile 的 model.api_key / providers.<provider>.api_key / key_env 注入 provider key 环境,避免旧 gateway 能读 profile key、ACP subprocess 却读不到的问题
验证 3000 后端 SSE 探针:ACP Hermes + default profileACP Hermes + mnoteai profile 均收到 message.delta + run.completed,没有 run.failed;浏览器请求体确认 Hermes 为 { profile: "mnoteai", acpRuntime: "hermes" }

问题 1c:浏览器截图显示 reasoning / thought 被当作最终回复

详情
状态 已修复,严格浏览器验证通过
原现象 旧截图 /mnt/Data1T/mnote/tmp/page-ai-acp-browser-mp9ofvbc/01-acp-hermes-reply.png 中,AI 气泡显示 Theusersentabrowserverificationstring...Reasonix 旧 result.json 也出现 Theuserisaskingmetorespond...ACP_REASONIX...,说明上次只检查“包含 marker”的验收标准不合格
根因 Rust SessionUpdate 使用 #[serde(untagged)],但 AgentMessageChunkAgentThoughtChunk 字段形状相同(sessionUpdate: String + content),serde 会按枚举顺序先匹配 AgentMessageChunk,导致 agent_thought_chunk 被误转成 message.delta
已修复 acp_types.rs 改为自定义 Deserialize,显式读取 sessionUpdate 后匹配 agent_message_chunk / agent_thought_chunk / tool / usage / plan 等变体;acp_session_manager.rsagent_thought_chunkThoughtDelta 单测;acp_bridge.rsThoughtDeltathought.delta 单测
验证 cargo test -p mnote-web acp -- --nocapture29 passed;浏览器 skill 严格断言 Hermes / Reasonix 最新 assistant 正文分别严格等于 ACP_HERMES_BROWSER_OK_mp9pfqf6ACP_REASONIX_BROWSER_OK_mp9pfqf6;截图见 tmp/page-ai-acp-browser-UAYwyM/
后续规则 页面 AI 浏览器验证必须同时看截图与正文断言;不能只用 DOM 包含 marker 作为通过条件

问题 2Hermes HTTP 路径 502

详情
现象 Hermes upstream 连接失败: error sending request for url (http://127.0.0.1:8644/v1/runs)
判断 这是旧 Hermes HTTP proxy 路径的环境/profile 配置问题,不是 ACP Reasonix wrapper 问题
原因 configured_upstream_for_profile() 优先读取 MNOTE_WEB_HERMES_UPSTREAM_URL 或 profile 的 API_SERVER_PORT,当前解析到了 8644;实际 Hermes gateway 端口应与本机服务一致(设计稿预期为 8642)
解决 只读确认当前 mnote-web 启动环境和 Hermes profile 配置;将 MNOTE_WEB_HERMES_UPSTREAM_URL 或对应 profile API_SERVER_PORT 调整到实际 gateway 端口;不要在 ACP runtime 层硬编码端口

问题 3:ACP 模式 Skills 面板内容区分(次要项)

详情
状态 🟡 次要项;不阻塞当前首要目标(ACP Hermes / ACP Reasonix 正常回复)
纠正 Reasonix skills 不是空态;本机真实目录包含 /home/lix/.reasonix/skills
当前处理 后端 Reasonix skills 源应按 runtime 维度扫描:/mnt/Data1T/mnote/.reasonix/skills/mnt/Data1T/mnote/.agents/skills/home/lix/.reasonix/skills/home/lix/.agents/skills
边界 Skills 展示只是可见信息;当前还不能据此认为这些 skills 都已经注入 Reasonix ACP runtime 的工具系统
下一步 后续若要把 Reasonix skills 变成可执行能力,需要明确 Reasonix skill schema → ToolRegistry 注册规则;当前优先保持只读展示与不误导

问题 4ACP Hermes 未复用 soul/user/memory 可编辑 UI

详情
用户反馈 ACP Hermes 页面中看不到 Hermes HTTP 已有的 SOUL.md / USER.md / MEMORY.md 编辑卡片
根因 layout.rsisAcp 分支把 Agent 面板替换成 runtime info 卡片,并且 if (agentPanel && !isAcp) 才渲染 memory 编辑器;因此 ACP Hermes 被误归入 Reasonix 风格的 runtime 信息态
参考结论 hermes-vscode-main 本身没有 soul/user/memory 模块;这部分应复用 mnote 现有 Hermes HTTP BFF/api/hermes/client/profile-memory
已修复 ACP Hermes (pageAiAcpRuntime === "hermes") 继续显示与 Hermes HTTP 相同的三张可编辑 memory 卡片;ACP Reasonix 仍显示 runtime infoHermes profile 下拉继续保留
边界 未额外把 memory 文本硬塞入 ACP prompt,避免和 hermes -p <profile> acp 自身 profile 加载逻辑重复;当前需求以 UI 可见、可编辑、可保存为准

问题 5:页面 AI 会话历史刷新丢失

详情
用户反馈 当前会话历史刷新后没有了,不清楚真源在哪里
根因 前端运行期真源是 pageUiState.pageAiSessions/pageAiMessages;旧 localStorage 只保存 activeSessionId/activeProfileName,不保存 messagesACP run 也未落 Hermes HTTP session export
参考结论 hermes-vscode-main/src/sessionStore.tsChatSession[]messagesacpSessionId 存在 VSCode workspaceState,最多 20 个 session、每 session 300 条消息
已修复 短期真源明确为“按文档隔离的浏览器 localStorage”:hermes_page_ai_session:<documentId> 现在保存 version/activeSessionId/activeProfileName/activeAcpRuntime/sessions/messages,每 session 最多 300 条消息
验证方法 浏览器验证 skill 已增加 Hermes 回复后 reload,再打开页面 AI,确认 marker 回复仍可见
长期方案 后续应把页面 AI 会话统一落后端 session store,并让 ACP session id 支持 session/load 恢复;localStorage 只保留本地缓存与离线恢复

问题 6:ACP 工具调用信息和流式输出不完整

详情
用户反馈 hermes-vscode-main 相比,缺工具调用信息(需可折叠、默认折叠),缺流式输出
根因 ACP message.delta 已到前端,但旧 UI 只累加到局部 assistantTextstream 完成后才 push assistant message;工具事件虽然会生成 tool message,但卡片是普通 div,未折叠,且 ACP bridge 丢掉了 rawInput/content/status
参考结论 hermes-vscode-main 的关键是协议层 text dedup、UI 层 pendingText/currentAgentText 流式占位、data-tool-id 原地更新工具状态;参考实现本身不是折叠 UI,因此 mnote 在其基础上新增 <details>
已修复 前端收到首个 message.delta 时创建 streaming=true assistant 消息并持续更新;完成后标记为普通 assistant 消息并持久化;工具卡改为 <details>,默认折叠,summary 显示工具名/状态/kind/call id,展开显示参数/结果/trace
后端补齐 AcpSessionEvent::ToolCall 保留 raw_inputToolCallUpdate 保留 contentacp_bridge SSE 输出 status/input/output,让前端可展示工具参数和结果摘要
验证 cargo test -p mnote-web acp -- --nocapture 已覆盖 acp_tool_events_keep_detail_for_collapsible_ui;浏览器验证 skill 会记录 sawStreaming 并保留截图

📋 待完成

Step 工作 前置 估算
旧 Hermes HTTP proxy 502 环境配置确认 非 ACP 路径,仅影响旧 gateway ~15min
ACP 模式 Skills 可执行注入设计(Reasonix skill schema → ToolRegistry 问题3 ~半天
15 压力测试:多会话并发、进程管理稳定性 Step 14 ~半天
16 退役旧 HTTP proxy 代码 Step 14 稳定后 ~1 天
17 基准测试:Reasonix cache hit rate vs Hermes Step 10 ~半天

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

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

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

完成。已阅读 acpClient.tsspawn/request/notification 模式)、protocol.ts6 种 session/update 变体)、acp.tsCacheFirstLoop + Eventizer 集成)。确认 ACP 使用 camelCase JSON 字段、NDJSON 流式传输。

| 参考 | reference-code/hermes-vscode-main/src/acpClient.tsreference-code/DeepSeek-Reasonix-main/src/acp/protocol.tsreference-code/hermes-vscode-main/src/protocol.ts |

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

完成。cwd = /mnt/Data1T/mnote,所有路径相对此目录。

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

完成。rust/crates/mnote-web/src/acp_client.rs~487 行)。实现了 AcpClient 结构体,含 spawn()request()notification()on_notification()close()。后台 tokio task 处理 NDJSON 行读取,pending: HashMap<u64, oneshot::Sender> 路由响应。6 个单元测试通过(含 initialize 握手 mock)。

| 产出 | rust/crates/mnote-web/src/acp_client.rs |

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

完成。rust/crates/mnote-web/src/acp_types.rs~440 行)。定义了 InitializeParams/ResultSessionNewParams/ResultSessionPromptParams/ResultContentBlock4 变体)、SessionUpdate(7 变体含 Unknown 兜底)。全部 camelCase JSON。7 个序列化往返测试通过

| 产出 | rust/crates/mnote-web/src/acp_types.rs |

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

完成。rust/crates/mnote-web/src/acp_session_manager.rs~530 行)。AcpSessionManagercreate_session() → ACP session/newrun_prompt() → ACP session/prompt + callback、cancel()session/cancel。文本去重(4 种模式匹配)、事件枚举(7 种 variants)。5 个单元测试通过(含事件派发、去重逻辑、thought chunk 映射)。

| 产出 | rust/crates/mnote-web/src/acp_session_manager.rs |

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

完成。rust/crates/mnote-web/src/acp_runtime.rs~330 行)。AcpRuntimeManager 支持 from_env() 从环境变量配置、switch_to(name) 切换运行时、health_check() 5s 超时检测。Reasonix wrapper 路径通过 CARGO_MANIFEST_DIR 自动解析绝对路径。6 个单元测试通过(含真实 Hermes CLI 连接)。

| 产出 | rust/crates/mnote-web/src/acp_runtime.rs |

[x] Step 7:集成 ACP Session Manager 到 Hermes routes

完成。hermes_client.rs 中:

  • create_run:新增 ACP 分支——is_acp_profile() 检测 → register_acp_runtime() → 存储 payload → 返回本地 runId
  • stream_events:新增 acp_stream_events() 函数——AcpRuntimeManager::switch_to() 激活运行时 → AcpSessionManager::create_session() + run_prompt() → broadcast → mpsc → SSE Body
  • is_acp_profile("reasonix" | "hermes") 返回 true
  • SSE 桥接:acp_event_to_sse() 映射 7 种 ACP 事件到 SSE 格式
  • 修复:payload.get("message") 替代错误的 payload.get("input")
  • 修复:SSE 转发管道先于 prompt 建立(消除 race condition

| 产出 | acp_bridge.rs~260 行)+ hermes_client.rs 修改 |

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

完成。lib.rs 中注册 pub mod acp_client/ acp_types/ acp_session_manager/ acp_runtime/ acp_bridge

[x] Step 9AppState 改造 — 加入 AcpRuntimeManager

完成。app.rsAppState 新增 acp_runtime: Arc<AcpRuntimeManager> 字段,AppState::new() 中初始化。

[x] Step 10:创建 Reasonix ACP wrapper 脚本

完成。scripts/reasonix-acp-wrapper.mjs~400 行)。自包含 NDJSON JSON-RPC 2.0 服务器(无依赖 AcpServer)。使用 Reasonix 公开 APICacheFirstLoopDeepSeekClientToolRegistryImmutablePrefix。注册 mnote.doc.fetchmnote.doc.markdown_edit 工具(工具调用 HTTP mnote-web tool API)。

关键修复:

  • ev.role 替代错误的 ev.typeCacheFirstLoop 使用 role 字段)
  • ToolRegistry.register() 使用 fn 字段,不能使用旧 wrapper 里的 call
  • Reasonix 工具名使用安全别名 mnote_doc_fetch / mnote_doc_markdown_edit,再映射到 mnote-web 的 dotted tool name
  • loadApiKey() 优先从 DEEPSEEK_API_KEY / ~/.reasonix/config.json 读取 API key,并兼容 ~/.reasonix/config.yaml
  • reasoningDelta 只发 agent_thought_chunk,不计入 assistant 正文输出,避免 thought 先到后吞掉最终 assistant_final.content
  • 无输出检测——LLM 静默失败时返回友好错误消息

验证:node --check 通过,initialize + session/new + session/prompt ACP 探针验证通过;探针返回 stopReason=end_turn,产生 agent_message_chunk;浏览器验证中 ACP Reasonix 最终 assistant 正文严格等于 marker。

| 产出 | scripts/reasonix-acp-wrapper.mjs |

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

完成(Rust SSR 侧)。layout.rs 中:

  • Agent 标签页新增 <select data-page-ai-acp-runtime> 下拉框(3 选项:默认/ACP·Hermes/ACP·Reasonix
  • 状态:pageAiAcpRuntime + pageAiAcpRuntimes(从 /api/hermes/client/profilesacpRuntimes 字段加载)
  • acpRuntimeprofile 分离:ACP Hermes 保留 Hermes profile 下拉,ACP Reasonix 隐藏 Hermes profile 下拉
  • ACP Hermes 复用 Hermes HTTP 的 SOUL.md / USER.md / MEMORY.md 可编辑 UIACP Reasonix 保留 runtime info 卡片
  • 页面 AI 会话历史短期真源为按文档隔离的 localStorage,保存 session 列表、messages、active profile、active ACP runtime
  • message.delta 进入正在输出的 assistant bubble;工具调用卡片使用 <details> 默认折叠展示参数/结果/trace
  • 标题栏更新为 ACP · ReasonixACP · Hermes
  • create_run 同时发送 profileacpRuntime;后端用 acpRuntime 判断是否走 ACP,用 profile 选择 Hermes profile

注:Thought Delta 可视化渲染尚未实现,当前阶段允许页面不展示 thought;硬要求是 agent_thought_chunk 不能进入最终 assistant 正文。

| 产出 | layout.rs 修改 |

[x] Step 12profile 扩展 — 从 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 <profile> acp,并注入 profile provider key 环境

待完成:gateway_health 适配 ACP runtime health check、MNOTE_WEB_ACP_DEFAULT_RUNTIME 环境变量支持。

| 产出 | hermes_client.rs 修改 |

[x] Step 13Hermes 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 路径返回 runIdSSE 收到 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 路径返回 runIdSSE 收到 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=trueHermes 回复刷新后仍可见

证据:

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