Files
hermes-web-ui/docs/cli-chat-sessions.md
T
ekko 634a622934 [codex] fix media skill profile auth and run events (#965)
* fix media skill profile auth and run events

* test bridge run profile context
2026-05-24 12:52:14 +08:00

16 KiB
Raw Blame History

CLI/Bridge Chat Sessions 实现文档

状态:本文档描述当前 main 中 Web UI 聊天会话的 API Server / Bridge(beta) 双路径实现。

概述

当前实现把原来的聊天通道统一到 Socket.IO namespace /chat-run。前端仍使用同一套 ChatPanel + MessageList + ChatInput,通过会话的 source 字段区分运行方式:

source 运行路径 说明
api_server Web UI Server → Hermes Gateway /v1/responses 默认聊天路径
cli Web UI Server → Python agent bridge → AIAgent Bridge(beta),在 Web UI 服务端子进程里直接运行 Hermes Agent

Bridge 会话不是一个独立 UI 面板,而是普通会话的一种来源。用户通过“新建聊天”下拉菜单选择 APIBridge (beta)

Bridge 模式支持:

  • 流式文本输出
  • reasoning/thinking 增量
  • tool started/completed 事件
  • 工具审批请求与响应
  • abort 中断
  • per-session 队列
  • profile 隔离
  • 从 DB resume 会话
  • 与 API Server 路径共用上下文压缩逻辑

当前不再支持旧文档里的独立 /cli-chat-run namespace、CliChatPanel.vuecli-chat.ts 和独立 command / steer socket 事件。CLI/Bridge 会话中的 slash command 现在通过统一 /chat-runrun payload 进入后端解析;当前支持 /usage/status/abort/queue/clear/title/compress/steer/destroy


整体架构

ChatPanel.vue
  ├─ MessageList.vue
  └─ ChatInput.vue
        │
        │ Socket.IO /chat-run
        ▼
ChatRunSocket (Node.js)
  ├─ source=api_server → Hermes Gateway /v1/responses
  └─ source=cli        → AgentBridgeClient
                              │ TCP/Unix socket, newline JSON
                              ▼
                         hermes_bridge.py
                              │ in-process import
                              ▼
                         AIAgent (hermes-agent)

分流规则

ChatRunSocket.resolveRunSource() 决定本轮运行走哪个后端:

  1. run payload 中 source === 'cli' 时走 bridge。
  2. source === 'api_server' 时走 gateway。
  3. 未显式传 source 时,如果 DB 中已有 session 的 sourcecli,继续走 bridge。
  4. 其他情况默认走 api_server

主要文件

前端

文件 说明
packages/client/src/components/hermes/chat/ChatPanel.vue 统一聊天面板;新建菜单包含 APIBridge (beta);渲染审批条
packages/client/src/components/hermes/chat/MessageList.vue 统一消息列表;展示文本、reasoning、tool 消息等
packages/client/src/components/hermes/chat/ChatInput.vue 统一输入框;发送、停止、附件上传入口
packages/client/src/api/hermes/chat.ts /chat-run Socket.IO 客户端;注册 session 事件处理器;发送 run/abort/approval
packages/client/src/stores/hermes/chat.ts 会话状态、发送流程、resume、队列、审批、消息映射

后端

文件 说明
packages/server/src/services/hermes/run-chat/index.ts /chat-run Socket.IO 入口;按 source 分流 API Server 与 Bridge 运行
packages/server/src/services/hermes/run-chat/handle-api-run.ts API Server 路径;调用 Hermes Gateway /v1/responses 并消费流式响应
packages/server/src/services/hermes/run-chat/handle-bridge-run.ts Bridge 路径;调用 Agent Bridge 并写入本地会话库
packages/server/src/services/hermes/run-chat/session-command.ts CLI/Bridge slash command 解析与处理
packages/server/src/services/hermes/agent-bridge/client.ts Node 端 bridge 客户端;通过 socket 请求 Python bridge
packages/server/src/services/hermes/agent-bridge/manager.ts Python bridge 子进程生命周期管理
packages/server/src/services/hermes/agent-bridge/hermes_bridge.py Python bridge 服务;创建并复用 AIAgent 实例
packages/server/src/services/hermes/agent-bridge/index.ts bridge 模块导出
packages/server/src/index.ts 启动 AgentBridgeManagerChatRunSocket
packages/server/src/services/shutdown.ts 关闭时停止 chat socket 和 bridge 子进程
packages/server/src/controllers/hermes/sessions.ts 会话列表和详情读取,包含 source 信息
packages/server/src/controllers/hermes/profiles.ts profile 管理接口;按 URL/body 中的 profile 做权限校验

已移除的旧文件

文件 状态
packages/client/src/api/hermes/cli-chat.ts 已删除
packages/client/src/components/hermes/chat/CliChatPanel.vue 已删除
packages/server/src/services/hermes/cli-chat-run-socket.ts 已删除

前端流程

新建会话

ChatPanel.vue 中的新建按钮使用下拉菜单:

  • API:调用 chatStore.newChat(),创建默认 api_server 会话。
  • Bridge (beta):调用 chatStore.newCliSession(),创建 source: 'cli' 会话。

Bridge 会话 ID 使用类似 YYYYMMDD_HHMMSS_xxxxxx 的格式,便于与 Hermes CLI 风格的 session ID 对齐。

发送消息

  1. ChatInput.vue 触发 store 的发送逻辑。
  2. chat.ts 根据 active session 组装输入内容,附件会被转为 ContentBlock[]
  3. 调用 startRunViaSocket()
  4. 前端向 /chat-run emit
socket.emit('run', {
  session_id,
  input,
  instructions,
  model,
  queue_id,
  source, // api_server 或 cli
})
  1. 前端注册本 session 的事件 handler,通过 session_id 隔离多会话并发事件。

Resume

切换会话、页面恢复可见、或刷新后,前端通过:

socket.emit('resume', { session_id })

服务端返回:

{
  session_id,
  messages,
  isWorking,
  isAborting,
  events,
  inputTokens,
  outputTokens,
  queueLength,
}

如果服务端发现该 session 仍在运行,前端会重新注册 handler,并允许继续 abort。

审批

Bridge 工具需要人工确认时,服务端会发 approval.requested,前端 store 记录为 activePendingApprovalChatPanel.vue 在输入框上方显示审批条。

前端响应审批:

socket.emit('approval.respond', {
  session_id,
  approval_id,
  choice, // once | session | always | deny
})

/chat-run Socket.IO 协议

客户端 → 服务端

事件 数据 说明
run { session_id, input, model?, instructions?, queue_id?, source? } 启动一轮运行;source 决定 API Server 或 Bridge
resume { session_id } 加入 session room 并恢复状态
abort { session_id } 中断当前运行
cancel_queued_run { session_id, queue_id } 取消等待队列中的一条 run
approval.respond { session_id, approval_id, choice } 响应 Bridge 工具审批

客户端不再发送独立 commandsteer Socket.IO 事件;slash command 作为普通 run.input 进入 /chat-run,由服务端在 source=cli 时解析。

服务端 → 客户端

事件 说明
resumed 返回 DB 消息、运行状态、队列长度和最近事件
run.started 运行开始
run.queued 当前 session 已有运行,新请求进入队列
message.delta 文本增量
reasoning.delta reasoning 增量
thinking.delta thinking 增量
reasoning.available reasoning 内容可用
tool.started 工具调用开始
tool.completed 工具调用结束
approval.requested Bridge 工具请求人工审批
approval.resolved 审批完成或超时
compression.started 上下文压缩开始
compression.completed 上下文压缩结束
usage.updated token 用量更新
abort.started 中断开始
abort.completed 中断结束
session.command slash command 的执行结果或错误反馈
run.completed 运行完成
run.failed 运行失败

认证

/chat-run 使用 Socket.IO auth token

io(`${baseUrl}/chat-run`, {
  auth: { token },
  query: { profile },
})

如果未设置 AUTH_DISABLED=1,服务端会与 Web UI token 比对。


ChatRunSocket 后端行为

API Server 路径

source=api_server 时:

  1. 写入用户消息到 Web UI 本地 session DB。
  2. 通过 buildCompressedHistory() 构建上下文。
  3. 请求当前 profile 的 Hermes Gateway
POST <upstream>/v1/responses
  1. 读取 SSE frame,映射为统一的 /chat-run 事件。
  2. 完成后写入 assistant/tool 消息,更新 usage。

Bridge 路径

source=cli 时:

  1. 写入用户消息到 Web UI 本地 session DB。
  2. 复用同一套 buildCompressedHistory() 构建压缩上下文。
  3. 调用:
this.bridge.chat(session_id, input, history, instructions, profile)
  1. 轮询 AgentBridgeClient.streamOutput(run_id)
  2. 将 Python bridge 的 delta 和 events 映射成统一事件。
  3. 将 assistant 文本、reasoning、tool 调用结果 flush 回 DB。

队列

同一个 session_id 同时只能有一个 active run。新的 run 到达时:

  • 如果当前 session 正在运行,则放入 state.queue
  • 发送 run.queued 更新队列长度。
  • 当前 run 结束或 abort 完成后,自动执行下一条 queued run。

Python Agent Bridge

通信协议

Node 和 Python bridge 之间使用本地 socket 的单行 JSON 协议:

{ "action": "chat", "session_id": "xxx", "message": "hello" }

响应也是单行 JSON

{ "ok": true, "run_id": "xxx", "session_id": "xxx", "status": "running" }

Endpoint

默认 endpoint 按平台选择:

平台 默认 endpoint
Windows tcp://127.0.0.1:18765
macOS/Linux ipc:///tmp/hermes-agent-bridge.sock

Windows 使用 TCP 是因为部分 Python/Windows 环境没有 Unix domain socket 支持。

当前实际使用的 action

Action 说明
chat 启动一轮 AIAgent.run_conversation()
get_output 通过 cursorevent_cursor 获取增量文本与事件
interrupt 调用 agent 中断当前运行
approval_respond 响应工具审批
destroy_all 维护动作;仅用于明确的全量清理/进程关闭场景,普通 profile 切换不会调用

bridge 代码里还保留了一些调试/维护 action,例如 pingget_resultget_historydestroylistshutdownsteer。当前 /chat-run 前端路径不会直接暴露这些 action;需要的能力由 Node /chat-run 层封装,例如 /steer slash command 会调用 steer action。

旧的 command action 已移除,Python bridge 不再直接解析 /new/undo/retry/branch 等旧斜杠命令;当前 CLI/Bridge slash command 支持范围以 Node /chat-runsession-command.ts 为准。

会话和 profile

AgentPool 维护 session_id -> AgentSession

  • 每个 session 持有独立 AIAgent 实例。
  • session 按请求中的 profile 创建和复用;前端切换 Hermes Profile 只改变后续请求使用的 profile,不会影响其他 bridge 内存 session。
  • HERMES_HOME 会在创建 agent 时临时切到 profile home。
  • SessionDB 按 profile 的 state.db 路径缓存。
  • 空闲 session 会被 bridge GC,默认 30 分钟无运行后销毁内存态。

工具和审批事件

bridge 从 AIAgent 回调中收集事件:

  • stream.delta
  • reasoning.delta
  • thinking.delta
  • tool.started
  • tool.completed
  • tool.progress
  • approval.requested
  • approval.resolved
  • turn.boundary
  • status

ChatRunSocket 会把这些事件转换为前端统一事件,并负责 DB 落盘。

审批默认等待 60 秒,超时自动 deny


AgentBridgeClient

AgentBridgeClient 是 Node 端本地 socket 客户端。

行为:

  • 支持 ipc://tcp:// endpoint。
  • 每次请求新建 socket,发送一行 JSON,读取一行 JSON。
  • 请求通过内部 lock 串行化。
  • 默认请求响应超时为 120000ms
  • streamOutput() 每 100ms 轮询一次 get_output

示例:

const started = await bridge.chat(sessionId, input, history, instructions, profile)

for await (const chunk of bridge.streamOutput(started.run_id)) {
  // chunk.delta
  // chunk.events
  // chunk.done
}

注意:目前 socket connect 阶段没有独立 connect timeout,主要依赖系统连接错误和请求响应 timeout。


AgentBridgeManager

AgentBridgeManager 负责启动和停止 Python bridge。

启动流程:

  1. 定位 hermes_bridge.py
  2. 发现 hermes-agent 根目录。
  3. 选择 Python 解释器。
  4. 以子进程启动:
python hermes_bridge.py --endpoint <endpoint> --agent-root <root> --hermes-home <home>
  1. 监听 stdout,等待:
{ "event": "ready", "endpoint": "..." }
  1. 默认 ready 超时为 120000ms

Python 选择优先级:

  1. HERMES_AGENT_BRIDGE_PYTHON
  2. agentRoot/venvagentRoot/.venv
  3. installed hermes 命令 shebang
  4. uv run --project <agentRoot> python
  5. 系统 python3 / python

关闭时先发 SIGTERM1.5 秒后仍未退出则 SIGKILL


启动与关闭

启动

bootstrap() 中会先尝试启动 bridge

agentBridgeManager = await startAgentBridgeManager()

bridge 启动失败不会阻止 Web UI 启动,但 Bridge(beta) 会话后续运行会失败。

随后创建统一的 chat socket

chatRunServer = new ChatRunSocket(groupChatServer.getIO())
chatRunServer.init()

关闭

服务关闭时会清理:

  • /chat-run Socket.IO 状态
  • Python agent bridge 子进程
  • 其他 WebSocket/Socket.IO 服务

环境变量

变量 说明
HERMES_AGENT_BRIDGE_ENDPOINT Bridge endpointWindows 默认 tcp://127.0.0.1:18765macOS/Linux 默认 ipc:///tmp/hermes-agent-bridge.sock
HERMES_AGENT_BRIDGE_TIMEOUT_MS Node 等待 bridge 请求响应的超时,默认 120000 ms
HERMES_AGENT_BRIDGE_CONNECT_RETRY_MS Node 连接 bridge socket 失败时的短重试窗口,默认 5000 ms
HERMES_AGENT_BRIDGE_STARTUP_TIMEOUT_MS Node 等待 Python bridge ready 的超时,默认 120000 ms
HERMES_AGENT_BRIDGE_AUTO_RESTART bridge broker 意外退出后是否自动重启;设为 0/false/no/off 可关闭,默认开启
HERMES_AGENT_BRIDGE_RESTART_DELAY_MS bridge broker 自动重启基础延迟,默认 1000 ms,连续失败时最多退避到 30000 ms
HERMES_AGENT_BRIDGE_PYTHON 指定 Python 解释器路径
HERMES_AGENT_ROOT hermes-agent 安装目录
HERMES_AGENT_BRIDGE_UV 指定 uv 可执行文件路径
HERMES_AGENT_BRIDGE_PLATFORM bridge 传给 Hermes Agent 的平台标识,默认 cli
HERMES_BRIDGE_PROVIDER 覆盖 bridge 使用的 provider
HERMES_BRIDGE_MAX_TURNS 覆盖 bridge 最大轮数
UV uv 可执行文件路径 fallback

正常使用不需要配置这些变量。Bridge 支持多个用户/多个 profile 的运行并存;Web UI 的 Hermes Profile 切换不会重启 bridge 或销毁其他正在运行的任务。Windows 下如果默认 TCP 端口被旧 bridge/broker/worker 占用,新 bridge 会先按端口杀掉旧进程树,再用同一个 endpoint 重建。

Windows 首次启动慢时可以临时放大:

$env:HERMES_AGENT_BRIDGE_STARTUP_TIMEOUT_MS = "300000"
$env:HERMES_AGENT_BRIDGE_TIMEOUT_MS = "300000"

当前限制

  • Bridge(beta) 仍依赖 Python bridge 成功启动;启动失败时 Web UI 可用,但 bridge 会话不可用。
  • bridge socket connect 阶段还没有单独 connect timeout。
  • 旧 CLI 独立面板和独立 /cli-chat-run namespace 已移除。
  • 旧 bridge command/steer socket 控制层已移除;CLI/Bridge slash command 现在通过统一 /chat-runrun.input 解析并以 session.command 反馈。