23 KiB
7-8 [process] 页面 AI Hermes Runtime BFF 下一阶段设计 v1
更新时间:2026-05-15
当前状态:
PROCESS。本文承接7-3、7-4、7-5、7-6、7-7已完成的 Hermes 页面内客户端主线,写入对hermes-web-ui-0.5.18Chat Runtime 架构的吸收结论。上位依据:
/mnt/Data1T/mnote/ARCHITECTURE.md/mnt/Data1T/mnote/design/01-05-current-priority-overview.md/mnt/Data1T/mnote/design/03-rust-web/process/3-rust-web-long-term-architecture-v1.md/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md/mnt/Data1T/mnote/design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/hermes-web-ui-0.5.18/mnt/Data1T/mnote/design/07-ai/done/7-3-page-ai-hermes-panel-and-mnote-plugin-v1.md/mnt/Data1T/mnote/design/07-ai/done/7-4-page-ai-hermes-panel-execution-checklist-v1.md/mnt/Data1T/mnote/design/07-ai/done/7-5-hermes-client-proxy-contract-v1.md/mnt/Data1T/mnote/design/07-ai/done/7-6-mnote-hermes-plugin-tool-contract-v1.md/mnt/Data1T/mnote/design/07-ai/done/7-7-page-ai-mini-hermes-control-surface-v1.md
1. 结论
hermes-web-ui-0.5.18 对 mnote 有用,但不能原样照搬。
可吸收的是:
- Web 端应采用
Browser -> BFF -> Hermes gateway,不是浏览器直接 ACP。 - BFF 应承担 runtime 管理:active run registry、resume、queue、abort、event normalization、gateway manager、错误规整。
- 前端只展示 message、tool event、状态和输入,不持有 AI 编排真相。
不可吸收的是:
- 不把 mnote 的页面 AI 聊天真相改为 Convex、SQLite 或 mnote 自建 chat DB。
- 不把 Hermes 降级成
/v1/responses store=false的纯模型执行器。 - 不让 mnote-web 根据自有数据库长期组装完整
conversation_history。
当前冻结方向:
mnote 学
hermes-web-ui的 BFF/runtime 架构思想,但会话真相继续归 Hermes;mnote 只保存业务事实、audit、artifact、edge、page/body/title/options 结果。
2. 当前实现定位
当前页面 AI 接入是:
Leptos 页面 AI 面板
-> mnote-web /api/hermes/client/*
-> Hermes gateway /v1/runs
-> Hermes gateway /v1/runs/{run_id}/events
同时:
profiles / skills / memory等设置能力由mnote-web本地 BFF 通过 Hermes CLI 或~/.hermes文件读写补齐。mnote.*工具由 mnote Rust runtime 承接,Hermes 只通过 skill/plugin/tool 调用。- 页面 AI 不再走旧
/api/ai-agent/run。
这已经满足最小闭环,但仍只是“最小 runtime 接入”,缺少 hermes-web-ui 已经具备的完整 Chat Runtime 能力。
3. ACP、gateway、responses 的取舍
3.1 ACP
ACP 适合:
- VSCode 插件
- Tauri / Electron 桌面端
- 本地 IDE host
- 需要由宿主启动
hermes acp并通过 stdio JSON-RPC 深度交互的场景
ACP 不适合作为 mnote 页面 AI 第一主链:
- 浏览器不能直接
spawn hermes acp。 - 若 Web 使用 ACP,仍需 mnote-web 作为 ACP bridge 管理 stdio 子进程。
- 这会引入长期进程、session、backpressure、并发、恢复和崩溃清理复杂度。
- 当前 Hermes gateway 已提供 Web 更自然的
/v1/*与 stream 能力。
结论:
ACP 后续只作为桌面端或高级 host bridge 候选,不作为页面 AI Web 主链。
3.2 /v1/runs
/v1/runs 适合当前 mnote 主线:
- Hermes 持有 run / session / event 语义。
- mnote 页面 AI 只是 Hermes client。
- mnote 不需要长期组装
conversation_history。 - 与“AI 会话归 Hermes,mnote 能力归 Rust kernel”一致。
风险:
- mnote-web 需要补 runtime 状态管理,否则刷新、断线、队列、abort 体验弱。
- Hermes gateway 的非
/v1/*管理面不足,需要 BFF 补 profiles/skills/memory/gateway manager。
结论:
短期继续以
/v1/runs为默认页面 AI 主链,并在 mnote-web 上补 Runtime BFF。
3.3 /v1/responses
hermes-web-ui 的产品化 Chat Runtime 使用 /v1/responses stream=true,典型形态是:
Browser Vue UI
-> Socket.IO /chat-run
-> hermes-web-ui server
-> /v1/responses stream=true
-> server 从本地 DB 组装 conversation_history
-> server 把 response.* 事件转成 run/message/tool 事件
-> server 写入 SQLite session/messages
它适合“自建 Chat Runtime”:
- 后端自有 session/messages 数据库。
- 后端自行压缩上下文。
- 后端自行映射 tool event。
store=false时 Hermes 不再是会话真相。
这与 mnote 当前页面 AI 方向有冲突。除非后续明确改变设计,把“AI 会话真相”从 Hermes 迁到 mnote,否则不能把 /v1/responses store=false + mnote conversation_history 写成默认主链。
允许使用 /v1/responses 的条件:
- 只作为特定 runtime 实验或 fallback。
- 必须证明 Hermes session 仍能作为用户可见会话真相,或者另开设计稿明确改变会话真相。
- 不得把 mnote page aggregate / Convex 页面数据变成聊天消息数据库。
结论:
/v1/responses先作为参考实现和可评估候选,不作为当前页面 AI 默认主链。
4. 目标架构
下一阶段目标不是改成 ACP,也不是直接改成 /v1/responses,而是在当前 gateway 主线上补一个 mnote-web Runtime BFF:
Leptos 页面 AI 面板
-> mnote-web Hermes Runtime BFF
- active run registry
- session/run resume
- queue
- abort
- event normalization
- gateway/profile/skill/memory BFF
- trace/audit 串联
-> Hermes gateway /v1/runs
-> Hermes session storage
-> mnote skill/plugin/tool
-> Rust runtime / kernel
真相边界:
- Hermes:AI session、message、run、tool event、usage、model、profile。
- mnote:页面、树、正文、artifact、edge、projection、audit、权限、idempotency。
- mnote-web Runtime BFF:运行时连接和事件编排,不保存长期聊天真相。
5. Hermes Web UI 参考索引
参考根目录:
/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/hermes-web-ui-0.5.18
只参考下表,不整站搬运:
| 参考主题 | 文件 | 搜索点 | mnote 中吸收什么 | mnote 中不采用什么 |
|---|---|---|---|---|
| Socket runtime 主链 | packages/server/src/services/hermes/chat-run-socket.ts |
ChatRunSocket、onConnection、resumeSession、handleRun、dequeueNextQueuedRun、handleAbort |
active run registry、resume、queue、abort、前端刷新不丢运行态 | 不照搬 Socket.IO;mnote 可用 SSE / WebSocket / HTTP 状态轮询 |
/v1/responses 事件映射 |
packages/server/src/services/hermes/chat-run-socket.ts |
/v1/responses、applyResponseStreamEvent、response.output_text.delta、response.output_item.done、response.completed |
学习如何把上游事件标准化为 message.delta、tool.started、tool.completed、run.completed |
不把 /v1/responses store=false 写成默认主链 |
| 后端 session store | packages/server/src/db/hermes/session-store.ts |
HermesSessionRow、HermesMessageRow、createSession、addMessage、getSessionDetail |
理解 Hermes Web UI 如何建完整 chat DB,以及字段形态 | 不在 mnote 建第二套 chat DB;不把 Convex 当 AI 会话真相 |
| 前端 chat store | packages/client/src/stores/hermes/chat.ts |
resumeServerWorkingRun、queueLengths、abortState、case 'message.delta'、case 'tool.started'、case 'run.completed' |
前端状态机、队列提示、abort UI、tool event 归并 | 不复用 Vue/Pinia;不把前端 state 当真相 |
| 前端 API | packages/client/src/api/hermes/chat.ts |
startRunViaSocket、resumeSession、registerSessionHandlers、RunEvent |
客户端事件合同与 runtime event 类型 | 不让浏览器绕过 mnote-web 直连 Hermes |
| VSCode ACP tool UI | design/05-editor-mainline/reference-code/hermes-vscode-main/src/sessionManager.ts、src/protocol.ts、src/chatPanel.ts、src/webview/main.ts |
tool_call、tool_call_update、parseToolCall、parseToolCallUpdate、case 'toolCall'、data-tool-id |
学习按 toolCallId 追加工具行、再用后续 update 原地更新状态的 UI 模型 |
不采用 ACP 作为 Web 主链;只参考 tool event UI 状态机 |
| gateway 管理 | packages/server/src/services/hermes/gateway-manager.ts、packages/server/src/services/gateway-bootstrap.ts |
getUpstream、start、stop、health |
profile 对应 gateway 的发现、启动、健康检查、错误提示 | 不让页面 AI 抽屉变成完整 gateway 管理台 |
| proxy handler | packages/server/src/routes/hermes/proxy-handler.ts |
resolveUpstream、SSE_EVENTS_PATH、streamSSE、/v1/runs |
路由改写、SSE 拦截、upstream 错误规整 | 不把全量 proxy catch-all 暴露给浏览器 |
| skills 来源 | packages/server/src/controllers/hermes/skills.ts |
.bundled_manifest、.hub/lock.json、.usage.json、source、modified |
skill 来源与生成/安装/本地分类 | 不复制完整 skill 管理台 |
快速定位命令:
cd /mnt/Data1T/mnote/design/05-editor-mainline/reference-code/hermes-web-ui-0.5.18
rg -n "ChatRunSocket|resumeSession|handleRun|dequeueNextQueuedRun|handleAbort|/v1/responses|applyResponseStreamEvent|response\\.output_text\\.delta|response\\.output_item\\.done|response\\.completed|HermesSessionRow|startRunViaSocket|gateway-manager" packages/server/src packages/client/src
6. RuntimeEvent 合同
mnote-web BFF 对前端暴露的事件应稳定为 mnote.hermes_runtime_event.v1。
最小事件集:
{ "event": "run.started", "sessionId": "mnote_doc_1_trace_1", "runId": "run_1", "traceId": "trace_1" }
{ "event": "message.delta", "sessionId": "mnote_doc_1_trace_1", "runId": "run_1", "delta": "当前页", "traceId": "trace_1" }
{ "event": "tool.started", "sessionId": "mnote_doc_1_trace_1", "runId": "run_1", "toolCallId": "call_1", "toolName": "mnote.page.get", "traceId": "trace_1" }
{ "event": "tool.completed", "sessionId": "mnote_doc_1_trace_1", "runId": "run_1", "toolCallId": "call_1", "toolName": "mnote.page.get", "summary": "读取当前页面", "auditId": "audit_1", "traceId": "trace_1" }
{ "event": "run.completed", "sessionId": "mnote_doc_1_trace_1", "runId": "run_1", "output": "总结文本", "usage": { "inputTokens": 0, "outputTokens": 0 }, "traceId": "trace_1" }
{ "event": "run.failed", "sessionId": "mnote_doc_1_trace_1", "runId": "run_1", "code": "hermes_client_upstream_error", "message": "Hermes upstream 返回错误", "traceId": "trace_1" }
{ "event": "abort.started", "sessionId": "mnote_doc_1_trace_1", "runId": "run_1", "traceId": "trace_1" }
{ "event": "abort.completed", "sessionId": "mnote_doc_1_trace_1", "runId": "run_1", "synced": true, "traceId": "trace_1" }
如果上游是 /v1/runs/{run_id}/events,BFF 只做字段规整;如果后续试验 /v1/responses,BFF 负责把 response.* 映射为上述 RuntimeEvent。前端不直接依赖上游原始事件格式。
7. 下一阶段 checklist
A. Runtime 状态注册表
- 在
mnote-web建立内存级sessionId -> active run stateregistry。 - 记录
sessionId、runId、profile、documentId、traceId、status、startedAt、lastEventAt。 - registry 只保存运行态和最近事件摘要,不保存完整聊天历史。
- 服务重启后允许 registry 丢失,但页面必须能从 Hermes session detail 恢复已完成消息。
验收标准:
- 页面刷新时,若 run 仍在 registry 中,前端能看到
running/tool_calling状态。 - 关闭页面再打开,不会把 mnote local storage 当作聊天真相。
B. Resume
- 增加
POST /api/hermes/client/sessions/{session_id}/resume或等价同源接口。 - 返回 Hermes session detail 中的 messages,并附带 registry 中当前运行态。
- 若 session 不在 registry 中,只返回 Hermes 已持久化状态。
验收标准:
- run 过程中刷新页面,UI 能恢复 run 状态或明确显示“正在同步 Hermes 状态”。
- run 完成后刷新页面,消息来自 Hermes session detail / export,不来自 mnote 自建聊天表。
验证证据:
cargo test -p mnote-web hermes_client_ -- --nocapture:通过 9 个 Hermes client 单测,覆盖 registry 不保存messages、tool event 摘要和/resume返回 runtime。MNOTE_UI_BASE_URL=http://127.0.0.1:13000 node scripts/task-hermes-page-ai-smoke.js:通过。13000 是本轮临时启动的新mnote-web,用于避免打断当前 3000;验证后已关闭。- 当前 3000 运行的是本轮修改前的旧
mnote-web二进制,POST /api/hermes/client/sessions/{id}/resume返回 404;需要重启 3000 后再用同一 smoke 复验主入口。
C. Queue
- 同一 session 已有 active run 时,后续输入进入后端 queue。
- queue item 只保存待发送 input、profile、context snapshot 摘要和 trace,不保存长期聊天真相。
- 当前 run 结束后自动触发下一条。
- 前端显示 queue length 和可取消项。
验收标准:
- 连续发送两条消息时,第二条显示 queued,而不是并发打到同一 Hermes session。
- 取消 queued item 后,不触发 Hermes run。
验证证据:
cargo test -p mnote-web hermes_client_ -- --nocapture:通过 12 个 Hermes client 单测,新增覆盖 active run 时第二条入队、queue item 不返回完整messages/pageContext、后端取消 queued item、终态后用 mock Hermes upstream 自动启动下一条 queued run。MNOTE_UI_BASE_URL=http://127.0.0.1:13000 node scripts/task-hermes-page-ai-queue-smoke.js:通过。验证连续发送两条消息时第二条走queued响应,UI 显示队列长度,并能从 Runtime 面板取消 queued item。MNOTE_UI_BASE_URL=http://127.0.0.1:13000 node scripts/task-hermes-page-ai-smoke.js:通过。确认 resume / tool / 完成态基础链路未被 queue 改动破坏。- 13000 是本轮临时启动的新
mnote-web,验证后已关闭;当前 3000 仍需重启后复验。
D. Abort
- abort 由 BFF 统一执行,调用 Hermes
/v1/runs/{run_id}/stop或中断当前 upstream request。 - abort 前后发出
abort.started、abort.completed。 - abort 后清理 registry 状态,并处理队列后续项。
验收标准:
- 点击停止后,UI 不再继续追加该 run 的 assistant delta。
- 若 abort 后有队列项,后端按顺序继续处理或明确要求用户确认。
验证证据:
cargo test -p mnote-web hermes_client_ -- --nocapture:通过 13 个 Hermes client 单测,新增覆盖 abort 响应返回abort.started/abort.completed,并在 abort 后继续处理 queued run。- 临时
mnote-web以MNOTE_WEB_HERMES_UPSTREAM_URL=http://127.0.0.1:3999启动后,执行MNOTE_UI_BASE_URL=http://127.0.0.1:13000 node scripts/task-hermes-page-ai-runtime-controls-smoke.js:通过。验证停止 run 后 UI 状态进入aborted,并且后续message.delta不再追加到聊天区。 MNOTE_UI_BASE_URL=http://127.0.0.1:13000 node scripts/task-hermes-page-ai-queue-smoke.js与MNOTE_UI_BASE_URL=http://127.0.0.1:13000 node scripts/task-hermes-page-ai-smoke.js:通过。确认 queue / resume 基础链路未被 abort 改动破坏。- 13000 是本轮临时启动的新
mnote-web,验证后已关闭;当前 3000 仍需重启后复验。
E. Event normalization
- mnote-web 将 Hermes 上游事件规整为
mnote.hermes_runtime_event.v1。 - 前端只消费标准事件,不直接分支处理多套 Hermes 原始事件。
- tool event 展示独立于 assistant 文本。
- 当前 UI 已能在聊天区输出
工具消息并显示工具名,例如mnote.page.get;下一阶段必须升级为按toolCallId合并的 tool event card。 tool.started创建或更新同一个工具卡,显示工具名、运行中状态、参数摘要。tool.completed原地更新同一个工具卡,显示完成状态、结果摘要、audit/trace id。tool.failed原地更新同一个工具卡,显示失败状态、错误码和可复制详情。- Runtime 页的最近 tool call 摘要继续保留,但不能替代聊天区内联工具卡。
验收标准:
message.delta、tool.started、tool.completed、run.completed、run.failed均有 smoke 覆盖。tool.started -> tool.completed不产生两条互不关联的纯文本工具消息;同一toolCallId在 UI 中表现为同一张工具卡状态变化。- 工具卡至少展示
toolName、toolCallId、状态、参数摘要、结果摘要、traceId/auditId。 - tool result 不混入正文编辑器,不写入 page body,除非 Hermes 明确调用
mnote.page.save等写工具。
验证证据:
MNOTE_UI_BASE_URL=http://127.0.0.1:13000 node scripts/task-hermes-page-ai-smoke.js:通过。smoke 发送同一toolCallId=call_smoke_page_get的tool.started -> tool.completed,断言聊天区只有 1 张 completed 工具卡;同时发送toolCallId=call_smoke_page_save的tool.started -> tool.failed,断言 failed 工具卡显示permission_denied。MNOTE_UI_BASE_URL=http://127.0.0.1:13000 node scripts/task-hermes-page-ai-runtime-controls-smoke.js与MNOTE_UI_BASE_URL=http://127.0.0.1:13000 node scripts/task-hermes-page-ai-queue-smoke.js:通过。确认工具卡改动未破坏 abort / queue。cargo test -p mnote-web hermes_client_ -- --nocapture:通过 14 个 Hermes client 单测,新增覆盖assistant_message、tool.failed等上游事件被规整为mnote.hermes_runtime_event.v1。- mnote-web
/client/events/{run_id}输出event: <canonical>与schemaVersion: "mnote.hermes_runtime_event.v1";前端消费路径只处理message.delta、run.completed、run.failed、run.aborted与tool.*标准事件名。
F. Gateway manager
- BFF 能检查当前 Hermes gateway health。
- BFF 能识别 profile 对应 gateway / model 状态。
- 缺 model/default 或 API key 时,错误提示指向 Hermes 设置,而不是只显示 502。
验收标准:
hermes_client_upstream_error场景能显示具体修复建议,例如“当前 profile 缺 model.default”。- profile/skill/settings API 不再错误代理到 Hermes gateway 非
/v1/*路径。
验证证据:
mnote-web新增GET /api/hermes/client/gateway/health?profile=<name>,只做只读探测,不启动/停止 gateway;返回gateway.configured/upstream/status/httpStatus、profile.modelDefault/provider/apiKeyConfigured与suggestions。profile_detail_payload()与 fallback profiles 会从 Hermes profileconfig.yaml读取model.default、model.provider、providerapi_key/key_env摘要,但不返回 API key 原文。upstream_error()对hermes_client_upstream_error增加 Hermes 设置修复建议;当上游错误正文包含model.default、API key、profile/provider等线索时,错误 message 会指向 Hermes 设置。- Runtime 面板增加 gateway/profile 状态区;页面请求失败时前端显示后端
message,不再只显示错误 code。 cargo test -p mnote-web hermes_client_ -- --nocapture:通过 17 个 Hermes client 单测,新增覆盖 gateway 未配置、profile 缺 model/API key、hermes_client_upstream_error错误建议。- 临时
mnote-web以MNOTE_WEB_HERMES_UPSTREAM_URL=http://127.0.0.1:3999启动到127.0.0.1:13000后,task-hermes-page-ai-smoke.js、task-hermes-page-ai-runtime-controls-smoke.js、task-hermes-page-ai-queue-smoke.js均通过;验证后已关闭 13000。 MNOTE_UI_BASE_URL=http://127.0.0.1:13000 node scripts/task-hermes-page-ai-agent-skill-smoke.js:通过。确认新增 gateway health 请求不影响 agent/profile 选择、SOUL.md 保存、skill toggle 与 run profile 透传。
G. /v1/responses 评估,不默认切换
- 增加只读 spike 文档或测试,验证当前 Hermes gateway 的
/v1/responses stream=true事件格式。 - 明确
/v1/responses是否能在不迁移会话真相的前提下复用 Hermes session。 - 若不能保持 Hermes session 真相,则不得进入默认页面 AI 主链。
验收标准:
- 有明确结论:继续
/v1/runs,或另开设计稿说明为什么改变会话真相。 - 任何
/v1/responses实验都不得把 mnote Convex / page aggregate 变成聊天消息真相。
只读 spike 结论:
- 参考
hermes-web-ui-0.5.18/packages/server/src/services/hermes/chat-run-socket.ts,其/v1/responses主链在发送前从后端 DB 组装conversation_history,随后设置body.stream = true与body.store = false,再请求${upstream}/v1/responses。 - 同一文件的
applyResponseStreamEvent()表明 responses 事件格式主要是response.created、response.output_text.delta、response.output_item.added、response.output_item.done、response.completed、response.failed,由上层 BFF 映射为run.started、message.delta、tool.started、tool.completed、run.completed、run.failed。 response.output_item.done中function_call会被映射为tool.started;function_call_output会被映射为tool.completed。这对 mnote 的 tool card 状态机有参考价值,但不改变主链。- 本机只读探测
http://127.0.0.1:3999/health、/v1/models、OPTIONS /v1/responses时,当前没有 gateway 监听,无法取得 live/v1/responses stream=true事件样本;因此本轮不把 live event capture 写成验收依据。 - 基于参考实现,
/v1/responses store=false + conversation_history的顺滑体验依赖上层 BFF/SQLite 自建 chat session 真相。除非后续确认 Hermes gateway 支持把/v1/responses直接绑定到 Hermes session storage,否则 mnote 不应把它切成默认主链。
当前决策:
继续
/v1/runs作为页面 AI 默认主链;/v1/responses只作为事件标准化与 tool card 的参考实现。若未来要切换,必须另开设计稿证明 Hermes session 仍是 AI 会话真相,或明确批准迁移会话真相。
8. 非目标
- 不在本阶段引入 ACP 主链。
- 不在本阶段把页面 AI 主链切到
/v1/responses store=false。 - 不在 mnote 建 Hermes Web UI 那样的 SQLite chat DB。
- 不把 Convex 作为 AI 会话真相。
- 不把页面 AI 抽屉扩成完整 Hermes 管理台。
- 不让 mnote plugin 直接写 Convex;写入仍回到 Rust runtime / kernel。
9. 最终验收口径
本稿完成时,应能同时成立:
- 页面 AI 仍是 Hermes 页面内客户端。
- Hermes 仍是 AI session/message/tool event/usage/model/profile 真相。
- mnote-web 有 Runtime BFF 能力,支持 run resume、queue、abort 和事件标准化。
- 页面刷新或断线不导致 active run UI 状态丢失;完成后的消息从 Hermes 恢复。
- mnote 不保存完整聊天历史,只保存 mnote 业务写入结果和 audit。
/v1/responses是否进入主链已有单独评估结论,且没有绕过 Hermes session 真相。
一句话:
补齐 Web Chat Runtime 的工程能力,但不改变“AI 会话归 Hermes,mnote 事实归 Rust kernel”的长期边界。