15 KiB
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 面板,而是普通会话的一种来源。用户通过“新建聊天”下拉菜单选择 API 或 Bridge (beta)。
Bridge 模式支持:
- 流式文本输出
- reasoning/thinking 增量
- tool started/completed 事件
- 工具审批请求与响应
- abort 中断
- per-session 队列
- profile 隔离
- 从 DB resume 会话
- 与 API Server 路径共用上下文压缩逻辑
当前不再支持旧文档里的独立 /cli-chat-run namespace、CliChatPanel.vue、cli-chat.ts 和独立 command / steer socket 事件。CLI/Bridge 会话中的 slash command 现在通过统一 /chat-run 的 run 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() 决定本轮运行走哪个后端:
runpayload 中source === 'cli'时走 bridge。source === 'api_server'时走 gateway。- 未显式传
source时,如果 DB 中已有 session 的source是cli,继续走 bridge。 - 其他情况默认走
api_server。
主要文件
前端
| 文件 | 说明 |
|---|---|
packages/client/src/components/hermes/chat/ChatPanel.vue |
统一聊天面板;新建菜单包含 API 和 Bridge (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 |
启动 AgentBridgeManager 和 ChatRunSocket |
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 切换/管理时清理 bridge 内存会话 |
已移除的旧文件
| 文件 | 状态 |
|---|---|
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 对齐。
发送消息
ChatInput.vue触发 store 的发送逻辑。chat.ts根据 active session 组装输入内容,附件会被转为ContentBlock[]。- 调用
startRunViaSocket()。 - 前端向
/chat-runemit:
socket.emit('run', {
session_id,
input,
instructions,
model,
queue_id,
source, // api_server 或 cli
})
- 前端注册本 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 记录为 activePendingApproval,ChatPanel.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 工具审批 |
客户端不再发送独立 command 或 steer 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 时:
- 写入用户消息到 Web UI 本地 session DB。
- 通过
buildCompressedHistory()构建上下文。 - 请求当前 profile 的 Hermes Gateway:
POST <upstream>/v1/responses
- 读取 SSE frame,映射为统一的
/chat-run事件。 - 完成后写入 assistant/tool 消息,更新 usage。
Bridge 路径
source=cli 时:
- 写入用户消息到 Web UI 本地 session DB。
- 复用同一套
buildCompressedHistory()构建压缩上下文。 - 调用:
this.bridge.chat(session_id, input, history, instructions, profile)
- 轮询
AgentBridgeClient.streamOutput(run_id)。 - 将 Python bridge 的 delta 和 events 映射成统一事件。
- 将 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 |
通过 cursor 和 event_cursor 获取增量文本与事件 |
interrupt |
调用 agent 中断当前运行 |
approval_respond |
响应工具审批 |
destroy_all |
profile 切换/管理时销毁全部 bridge 内存 session |
bridge 代码里还保留了一些调试/维护 action,例如 ping、get_result、get_history、destroy、list、shutdown、steer。当前 /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-run 的 session-command.ts 为准。
会话和 profile
AgentPool 维护 session_id -> AgentSession:
- 每个 session 持有独立
AIAgent实例。 - session 按 profile 创建,profile 改变时会重建对应 agent。
HERMES_HOME会在创建 agent 时临时切到 profile home。SessionDB按 profile 的state.db路径缓存。- 空闲 session 会被 bridge GC,默认 30 分钟无运行后销毁内存态。
工具和审批事件
bridge 从 AIAgent 回调中收集事件:
stream.deltareasoning.deltathinking.deltatool.startedtool.completedtool.progressapproval.requestedapproval.resolvedturn.boundarystatus
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。
启动流程:
- 定位
hermes_bridge.py。 - 发现
hermes-agent根目录。 - 选择 Python 解释器。
- 以子进程启动:
python hermes_bridge.py --endpoint <endpoint> --agent-root <root> --hermes-home <home>
- 监听 stdout,等待:
{ "event": "ready", "endpoint": "..." }
- 默认 ready 超时为
120000ms。
Python 选择优先级:
HERMES_AGENT_BRIDGE_PYTHONagentRoot/venv或agentRoot/.venv- installed
hermes命令 shebang uv run --project <agentRoot> python- 系统
python3/python
关闭时先发 SIGTERM,1.5 秒后仍未退出则 SIGKILL。
启动与关闭
启动
bootstrap() 中会先尝试启动 bridge:
agentBridgeManager = await startAgentBridgeManager()
bridge 启动失败不会阻止 Web UI 启动,但 Bridge(beta) 会话后续运行会失败。
随后创建统一的 chat socket:
chatRunServer = new ChatRunSocket(groupChatServer.getIO())
chatRunServer.init()
关闭
服务关闭时会清理:
/chat-runSocket.IO 状态- Python agent bridge 子进程
- 其他 WebSocket/Socket.IO 服务
环境变量
| 变量 | 说明 |
|---|---|
HERMES_AGENT_BRIDGE_ENDPOINT |
Bridge endpoint;Windows 默认 tcp://127.0.0.1:18765,macOS/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 |
正常使用不需要配置这些变量。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-runnamespace 已移除。 - 旧 bridge
command/steersocket 控制层已移除;CLI/Bridge slash command 现在通过统一/chat-run的run.input解析并以session.command反馈。