Files
hermes-web-ui/docs/cli-chat-sessions.md
ekko f4e4296a70 [codex] stop bridge broker on restart by default (#1784)
* stop bridge broker on restart by default

* update generated openapi spec
2026-06-25 11:05:50 +08:00

34 KiB
Raw Permalink Blame History

Chat Sessions 链路文档

状态:本文档按当前 main 重新整理普通 Chat 会话的完整实现链路。旧的独立 CLI Chat 面板、/cli-chat-run namespace、cli-chat.ts 客户端和 Python bridge 直连命令层已经不再是产品入口。

最后重建时间:2026-06-03。

维护要求:后续 PR 如果修改本文列出的普通 Chat 链路核心文件,需要新增 docs/chat-chain-changes/ 下的独立变更片段。每个 PR 一个变更片段,写清楚 修改时间、PR/commit、动到的功能和行为影响,避免多个 PR 同时改本文产生冲突。 packages/server/src/services/hermes/agent-bridge/ 是普通 Chat 的核心链路, 该目录下任何改动都算 Chat 链路改动;即使只是启动、环境变量、日志或品牌 attribution,也要记录影响范围,必要时明确“运行行为无变化”。 packages/server/src/services/hermes/group-chat//group-chat Socket.IO、 group-chat 前端 store/API/component、共享压缩器和 context-engine 也属于核心 聊天链路,改动时同样需要记录。

1. 结论先行

当前普通 Chat 的主链路是:

ChatPanel / ChatInput
  -> Pinia chat store
  -> packages/client/src/api/hermes/chat.ts
  -> Socket.IO namespace /chat-run
  -> ChatRunSocket
  -> handleBridgeRun()
  -> AgentBridgeClient
  -> hermes_bridge.py broker
  -> profile worker
  -> AIAgent / Hermes Agent tools

也就是说,当前 Web UI 普通聊天默认不是走 Hermes Gateway /v1/responses 而是走 source=cli 的 Agent Bridge 路径。代码里仍保留 api_server 类型和 handle-api-run.ts,但 resolveRunSource() 当前固定返回 cli

核心原则:

  • UI 只有一套普通 Chat 页面,不再有单独 CLI Chat 页面。
  • 所有长连接事件统一走 /chat-run
  • 所有普通会话都落 Web UI 自己的 SQLite sessions/messages 表。
  • Hermes profile、model、provider、workspace、source 都是 session 级上下文。
  • 多 tab / 页面刷新通过 resume 重新加入同一个 session:{id} room。
  • 同一个 session 同时只跑一个 active run,后续输入进入 session 队列。
  • 工具审批、用户澄清、压缩、abort、usage、slash command 都复用同一条 /chat-run 事件通道。

最近链路变更记录

变更记录按 PR 拆分保存在 docs/chat-chain-changes/。新增 Chat session chain、 Agent Bridge、compression 或 Group Chat 核心链路改动时,新增一个 fragment 文件, 不要继续修改本文维护集中表格。

每个 fragment 至少包含:

---
date: YYYY-MM-DD
pr: 1234
feature: 改动功能
impact: 行为影响
---

补充说明。

开 PR 前还没有编号时可以先写 pr: pendingPR 创建后再把该 fragment 改成实际 PR 号。

2. 主要文件

前端

文件 职责
packages/client/src/components/hermes/chat/ChatPanel.vue 普通 Chat 页面容器,组合消息列表、输入框、审批条、澄清条、抽屉面板。
packages/client/src/components/hermes/chat/ChatInput.vue 输入框、发送、附件、停止按钮入口。
packages/client/src/components/hermes/chat/MessageList.vue / VirtualMessageList.vue 消息列表渲染和虚拟滚动。
packages/client/src/components/hermes/chat/MessageItem.vue 单条消息渲染,包含 assistant、tool、reasoning、附件等 UI。
packages/client/src/stores/hermes/chat.ts Chat 核心状态机:session 列表、发送、resume、队列、流式事件、审批、澄清、abort、压缩状态。
packages/client/src/api/hermes/chat.ts /chat-run Socket.IO 客户端,负责连接、全局事件分发、run/resume/abort/approval/clarify 协议。
packages/client/src/api/hermes/sessions.ts HTTP session API:列表、详情分页、删除、重命名、模型更新。
packages/client/src/api/hermes/group-chat.ts /group-chat Socket.IO 客户端和 group-chat HTTP room/agent/config API。
packages/client/src/stores/hermes/group-chat.ts Group Chat 状态机:rooms、members、messages、agents、streaming、context/compression 状态。
packages/client/src/components/hermes/group-chat/* Group Chat 页面、输入框、消息列表、成员/agent 展示和房间创建配置。

后端

文件 职责
packages/server/src/services/hermes/run-chat/index.ts ChatRunSocket/chat-run namespace 入口、认证、profile 校验、run/resume/abort/approval/clarify/queue 分发。
packages/server/src/services/hermes/run-chat/handle-bridge-run.ts 当前主运行路径:创建/更新本地 session,构建上下文,调用 Agent Bridge,消费 bridge 事件,落库。
packages/server/src/services/hermes/run-chat/handle-api-run.ts 保留的 API Server 路径实现;当前 resolveRunSource() 固定返回 cli,正常不会走到这里。
packages/server/src/services/hermes/run-chat/session-command.ts slash command 解析和执行。
packages/server/src/services/hermes/run-chat/abort.ts active run 中断、状态落盘、队列衔接。
packages/server/src/services/hermes/run-chat/compression.ts DB history 构建、snapshot-aware history、上下文压缩。
packages/server/src/services/hermes/run-chat/bridge-message.ts Bridge assistant/tool 消息的内存态与 DB flush。
packages/server/src/services/hermes/run-chat/bridge-delta.ts 过滤 bridge 输出中的工具调用标记,避免 UI 文本重复或丢字符。
packages/server/src/services/hermes/agent-bridge/client.ts Node 到 Python bridge 的本地 socket 客户端。
packages/server/src/services/hermes/agent-bridge/manager.ts Python bridge broker 子进程生命周期管理。
packages/server/src/services/hermes/agent-bridge/python/hermes_bridge.py Python broker/worker entrypoint;实现拆分在同目录的 bridge_*.py 模块中,覆盖 AIAgent 会话池、工具审批、澄清、压缩协作、goal/plan 命令等。
packages/server/src/services/hermes/group-chat/index.ts /group-chat Socket.IO server、room/member/message 存储、agent 恢复、mention 分发、approval/interrupt 入口。
packages/server/src/services/hermes/group-chat/agent-clients.ts Group Chat agent socket client,调用 Agent Bridge 执行被 mention 的 agent,并同步 tool/reasoning/context 状态。
packages/server/src/services/hermes/context-engine/* Group Chat 上下文压缩和 summary cache。
packages/server/src/lib/context-compressor/* 普通 Chat 和 Group Chat 共用的 token 估算、摘要压缩和 context message 处理。
packages/server/src/routes/hermes/group-chat.ts Group Chat HTTP room/agent/config/compress/clear-context API。
packages/server/src/db/hermes/session-store.ts Web UI 本地 session/message SQLite 存储。
packages/server/src/controllers/hermes/sessions.ts HTTP session 列表、详情、分页、删除、导入/导出等控制器。

3. 数据模型

普通 Chat 使用 Web UI 本地 SQLite,而不是直接把 Hermes CLI 历史当作唯一状态。 核心表由 packages/server/src/db/hermes/schemas.ts 初始化。

sessions

session-store.ts 暴露的主要字段:

字段 说明
id Web UI session id。前端新建时生成,发送 run 时传入。
profile Hermes profile。新 session 使用当前 active profile,已存在 session 优先使用 DB 中的 profile。
source 当前会话来源。当前普通 Chat 实际写入 cli。历史数据可能存在 api_server
model / provider session 绑定模型。首轮发送会写入选中的模型/供应商,后续可更新。
title / preview 会话标题和预览。标题可以由 /title 改,也可由首条用户输入生成。
workspace 当前工作目录上下文,会被注入 run instructions。
message_count / tool_call_count updateSessionStats() 从 messages 表统计。
input_tokens / output_tokens usage 统计结果。
last_active / started_at / ended_at 会话时间元数据。

messages

主要 role

role 说明
user 普通用户输入。
command 用户输入的 slash command 展示消息。
assistant Agent 输出文本,可带 reasoning / reasoning_content
tool 工具执行结果。

工具调用相关字段:

  • tool_call_id
  • tool_calls
  • tool_name
  • finish_reason
  • reasoning
  • reasoning_details
  • reasoning_content

前端读取历史时通过 mapHermesMessages() 把 DB 行映射成 UI Message,包括:

  • assistant 文本消息
  • reasoning 内容
  • tool started/tool completed 的可视化行
  • 队列消息
  • command/system 消息

4. 前端状态机

useChatStore() 是普通 Chat 的中心状态。

常见状态:

状态 说明
sessions 当前加载的 session 列表和每个 session 的消息数组。
activeSessionId / activeSession 当前页面正在显示的 session。
serverWorking 前端认为服务端仍有 active run 的 session id 集合。
streamStates 当前前端已注册流式 handler 的 session。
queueLengths / queued user messages 每个 session 的服务端队列长度和 UI 可见队列消息。
pendingApprovals 工具审批请求,按 sessionId + approvalId 存。
pendingClarifies 用户澄清请求,按 sessionId + clarifyId 存。
compressionStates 上下文压缩中的临时 UI 状态。
abortState 当前 active run 的中断状态。

新建会话

普通新建入口最终调用:

newChat(options)

它会:

  1. 从当前 app model/profile 状态创建本地前端 session 对象。
  2. 切换到这个 session。
  3. 此时 DB 里不一定已经有 session 行;真正落库发生在第一次 run 时。

历史上的 newCliSession() 仍存在,但普通 Chat 当前也是 source=cli,不再需要单独 UI 面板区分。

切换会话

切换 session 时,store 会优先通过 Socket.IO resume

resumeSession(sessionId, onResumed, profile)

服务端会:

  1. socket.join("session:<id>")
  2. 如果内存 sessionMap 有状态,直接返回。
  3. 否则从 DB 读取分页消息和 usage。
  4. 如果该 session 正在运行,返回最近 transient events。

前端收到 resumed 后会:

  • 替换/补齐本地消息。
  • 恢复 isWorkingisAborting、queue、usage。
  • 对 active run 重新注册 handlers。
  • 重新显示审批、澄清、压缩、abort 等未完成状态。

5. 发送消息链路

前端发送

入口是 chat.ts store 的:

sendMessage(content, attachments?)

主要步骤:

  1. 如果没有 active session,先创建一个。
  2. 捕获发送时的 sid,后续所有回调都用这个 sid,避免用户切换 session 后事件写错地方。
  3. 判断是否 slash commandcontent.trim().startsWith("/")
  4. 判断是否需要排队:
    • 如果服务端当前 session 正在运行;
    • 且不是可立即处理的 mid-run slash command
    • 则 UI 先显示 queued 消息。
  5. 如果有附件:
    • 先走 /upload
    • 再组装 ContentBlock[]
    • image block 包含 type/name/path/media_type
    • file block 包含 type/name/path/media_type?
  6. 等待模型列表 ready,读取当前 session 或全局选择的 model/provider。
  7. 构造 run payload。

当前 payload 形态:

{
  input,
  session_id: sid,
  profile,
  model,
  provider,
  model_groups,
  queue_id: userMsg.id,
  source: "cli",
}

首轮发送会带 model/provider,已存在 session 后通常依赖 DB 中的 session 模型。

Socket.IO 发送

startRunViaSocket() 负责:

  1. connectChatRun(profile) 建立或复用 /chat-run socket。
  2. 注册当前 session_id 的事件 handlers 到 sessionEventHandlers
  3. 发送:
socket.emit("run", body)

如果同一个 session 已经有 handler,新的 run 只 emit,不重复注册,避免多 tab/多次发送造成事件重复处理。

6. /chat-run 连接和认证

前端连接:

io(`${baseUrl}/chat-run`, {
  auth: { token },
  query: { profile },
  transports: ["websocket", "polling"],
  reconnection: true,
})

后端 ChatRunSocket.authMiddleware()

  1. 如果 auth 关闭,直接放行。
  2. 如果 auth 开启,使用 authenticateUserToken() 验证 token。
  3. 如果 socket query 里带 profile,检查用户是否可访问该 profile。
  4. 把 authenticated user 写入 socket.data.user

每次 run 时还会调用 resolveRunProfile()

  • payload 显式 profile 优先。
  • 没有 session id 时使用 socket 当前 profile。
  • 有 session id 时优先用 DB 中 session.profile。
  • 非 super admin 需要通过 userCanAccessProfile()

7. 服务端 run 分发

ChatRunSocket 监听:

客户端事件 服务端行为
run 解析 profile、source、slash command、queue,然后进入 run handler。
resume 加入 session room,返回 DB/内存状态。
abort 调用 handleAbort()
cancel_queued_run 从 session queue 删除指定 queue item。
approval.respond 转发到 bridge approval_respond
clarify.respond 转发到 bridge clarify_respond

source 分流

当前代码:

export function resolveRunSource(_source?: string, _sessionId?: string): ChatRunSource {
  return "cli"
}

因此:

  • payload 即使传 source: "api_server",当前仍会被解析为 cli
  • handleApiRun() 是保留实现,不是当前普通 Chat 的实际路径。
  • 新会话落库时 source 写为 cli

sessionMap

后端内存态是:

Map<string, SessionState>

每个 SessionState 包含:

  • messages
  • isWorking
  • isAborting
  • events
  • queue
  • runId
  • activeRunMarker
  • profile
  • source
  • bridge pending text/reasoning/tool 状态
  • usage/context token 状态

DB 是持久层,sessionMap 是运行时和 resume 用的 transient 层。

8. Bridge run 详细链路

实际执行在:

handleBridgeRun(...)

8.1 初始化 run

服务端会:

  1. 根据 session DB 和 payload 解析 model/provider。
  2. 生成 runMarker = cli_run_<...>,用于把同一轮用户、assistant、tool 消息串起来。
  3. 初始化 SessionState
    • isWorking = true
    • isAborting = false
    • source = "cli"
    • 清空 pending assistant/reasoning/tool 状态
  4. 如果需要展示用户输入:
    • 内存 state.messages.push(...)
    • 如果 DB session 不存在,createSession({ source: "cli" })
    • addMessage() 写入 DB
  5. socket.join("session:<id>")
  6. 向其他同 room tab 广播 run.peer_user_message

8.2 instructions 组装

最终 instructions 包含:

  • getSystemPrompt()
  • 用户或调用方传入的 instructions
  • 当前 Hermes profile
  • session workspace
  • Web UI 工具调用提示:如果工具调用 Web UI API,需要带 X-Hermes-Profile

8.3 上下文构建

run 前调用:

buildCompressedHistory(...)

它会基于 DB history 和 compression snapshot 构建适合本次 run 的历史:

  • 读取本 session messages。
  • 排除或包含当前用户输入,视调用场景决定。
  • 使用 snapshot-aware history,避免已经压缩的旧上下文重复膨胀。
  • 必要时触发上下文压缩。
  • 对 Bridge 路径会调用 context_estimate 估算固定上下文开销:
    • system prompt tokens
    • tool tokens
    • tool count/names

8.4 调用 AgentBridgeClient

服务端发起:

bridge.chat(sessionId, bridgeInput, bridgeHistory, fullInstructions, profile, {
  storage_message,
  model,
  provider,
})

返回:

{
  ok: true,
  run_id,
  session_id,
  status
}

随后 Node 轮询:

for await (const chunk of bridge.streamOutput(run_id)) {
  applyBridgeChunkAsync(...)
}

streamOutput() 实际通过 get_output action,携带 cursorevent_cursor 增量读取文本和事件。

9. Python Agent Bridge

9.1 进程结构

AgentBridgeManager 启动 hermes_bridge.py broker 子进程:

python hermes_bridge.py --endpoint <endpoint> --agent-root <root> --hermes-home <home>

默认 endpoint

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

broker 会再按 profile 路由到 worker。worker 里维护实际 AIAgent 会话池。

9.2 Node/Python 协议

Node 和 Python 使用本地 socket 的单行 JSON 协议。

示例请求:

{"action":"chat","session_id":"abc","message":"hello","profile":"default"}

示例响应:

{"ok":true,"run_id":"...","session_id":"abc","status":"running"}

AgentBridgeClient 每次请求新建 socket,发送一行 JSON,读取一行 JSON。 请求默认串行化,默认 timeout 是 120 秒,connect 失败有短重试窗口。

9.3 常用 action

action 说明
chat 启动一轮 agent conversation。
get_output 根据 cursor/event_cursor 拉取增量输出。
context_estimate 估算 system prompt/tool 固定上下文 token。
interrupt 中断 session 当前 run。
steer 给正在运行的 session 追加 steering instruction。
command 执行 plan/goal/subgoal 等 bridge command。
approval_respond 响应工具审批。
clarify_respond 响应澄清请求。
compression_respond Node 完成本地压缩后把压缩结果交还给 bridge。
goal_evaluate / goal_pause goal continuation 的状态评估和暂停。
status 查询 bridge session 状态。
destroy 销毁指定 bridge session。
destroy_all 销毁全部 bridge session,主要用于进程关闭或维护。
mcp_* MCP 相关维护动作,例如 reload。

10. 流式事件映射

Bridge 输出 chunk 里有两类数据:

  • delta:聚合文本增量。
  • events:有序事件列表。

如果 events 中出现 stream.deltaNode 以事件顺序处理文本,避免再处理 聚合 chunk.delta,否则会重复输出。

事件映射

Bridge event /chat-run event UI 结果
stream.delta / chunk delta message.delta assistant 文本增量。
reasoning.delta reasoning.delta reasoning 增量。
thinking.delta thinking.delta thinking/reasoning 增量。
reasoning.available reasoning.available 标记 reasoning 可用。
tool.started tool.started 显示工具开始行,先 flush pending assistant 文本。
tool.completed tool.completed 显示工具结果和耗时/error。
subagent.* subagent.* 显示子代理状态、工具、进度和总结。
status agent.event 显示 agent 状态事件。
approval.requested approval.requested 显示审批条。
approval.resolved approval.resolved 清理审批条。
clarify.requested clarify.requested 显示澄清输入。
clarify.resolved clarify.resolved 清理澄清输入。
bridge.compression.requested compression.started UI 显示压缩中,Node 执行本地压缩。
bridge.compression.completed compression.completed UI 显示压缩结果,更新 context tokens。
terminal chunk done run.completed / run.failed 结束 run,更新 usage,衔接队列。

消息落库

Bridge run 中不会每个字符都立即落库。Node 会维护 pending assistant/tool 状态:

  • 文本增量累加到 bridgePendingAssistantContent
  • reasoning 累加到 bridgePendingReasoningContent
  • 工具开始前、turn boundary、run 结束、abort 前会 flushBridgePendingToDb()
  • tool started/completed 通过 recordBridgeToolStarted()recordBridgeToolCompleted() 形成 DB 可恢复的 assistant/tool 结构。

这样可以保证:

  • UI 实时流式显示。
  • DB 中历史可恢复。
  • 刷新页面后 tool/reasoning/assistant 不丢。

11. run 完成和失败

当 bridge chunk done=true

  1. flush pending 工具标记和 assistant 文本。
  2. updateSessionStats(sessionId)
  3. 延迟短时间等待 usage flush。
  4. calcAndUpdateUsage() 计算 input/output tokens。
  5. refreshFinalContextUsage() 计算 snapshot-aware context tokens。
  6. updateUsage() 写 usage store。
  7. 检测 bridgeTerminalError()
    • bridge status 为 error
    • result 中带 error/exception
    • final_response 看起来是上游错误
  8. 发送 run.completedrun.failed
  9. 如果队列中还有消息,自动 dequeue 下一条。

前端收到 terminal event 后:

  • 关闭 streaming assistant 状态。
  • 清理 serverWorking
  • 播放完成提示音(如果开启)。
  • 更新 session title/usage/context tokens。
  • 如果 queue_remaining > 0,保持 handler,不提前清理。

12. 队列

同一个 session 同时只允许一个 active run。

队列进入点:

  1. 服务端收到 run,发现 state.isWorking=true
  2. 前端 /queue <message> command。
  3. /plan/goal 生成 kickoff prompt,但当前 session 正在运行。
  4. abort 完成后仍有队列。

队列项类型:

interface QueuedRun {
  queue_id: string
  input: string | ContentBlock[]
  displayInput?: string | ContentBlock[] | null
  displayRole?: "user" | "command"
  storageMessage?: string
  model?: string
  provider?: string
  model_groups?: Array<{ provider: string; models: string[] }>
  instructions?: string
  profile: string
  source?: "cli" | "api_server"
  originSocketId?: string
  goalContinuation?: boolean
}

队列事件:

  • run.queued:队列长度变化。
  • queued_messagesUI 可见队列消息。
  • dequeued_queue_id:某条队列开始执行,前端移除 queued 展示。

取消队列:

socket.emit("cancel_queued_run", { session_id, queue_id })

13. Abort

前端 stop 按钮最终调用:

socket.emit("abort", { session_id })

服务端 handleAbort()

  1. 如果没有 active run,发送 ignored 的 abort.completed
  2. 设置 state.isAborting=true
  3. 发送 abort.started
  4. 先 flush 当前内存 assistant/tool 状态到 DB。
  5. CLI source 调用:
    • bridge.interrupt(sessionId, "Aborted by user", profile)
    • bridge.goalPause(..., "user-interrupted")
    • 移除 goal continuation 队列项
  6. 标记 abort completed,更新 usage。
  7. 如果队列不为空,启动下一条队列。

前端收到:

  • abort.started:显示中断中。
  • abort.completed:清理中断状态;如果有队列继续保持 streaming handler。

14. 工具审批

Bridge 里有两类审批来源:

  1. Hermes 工具层直接回调,例如 terminal/本地命令审批。
  2. gateway approval notify,经 tools.approval 按 session key 路由。

Python 侧生成:

{
  "event": "approval.requested",
  "approval_id": "...",
  "command": "...",
  "description": "...",
  "choices": ["once", "session", "always", "deny"],
  "allow_permanent": true,
  "timeout_ms": 60000
}

Node 映射到 /chat-run

approval.requested

前端:

  • chat.ts store 写入 pendingApprovals
  • ChatPanel.vue 显示审批 UI。
  • 用户选择后调用:
respondToolApproval(sessionId, approvalId, choice)

再发送:

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

Python bridge 会:

  • 找到对应 approval queue 或 gateway session。
  • 写入选择。
  • 发送 approval.resolved
  • 在运行开始/审批相关路径刷新 command_allowlist 的进程内缓存,确保“始终允许”的持久配置能被后续工具判断读到。

15. 用户澄清

澄清请求和审批类似,但用于 agent 主动问用户问题。

Bridge event

{
  "event": "clarify.requested",
  "clarify_id": "...",
  "question": "...",
  "choices": ["..."],
  "timeout_ms": 60000
}

前端显示澄清 UI,并发送:

socket.emit("clarify.respond", {
  session_id,
  clarify_id,
  response
})

服务端转发到:

bridge.clarifyRespond(clarifyId, response)

然后通过 clarify.resolved 清理 UI 状态。

16. Slash Commands

slash command 不是独立 socket 事件。它们作为普通 run.input 发给 /chat-run 后端在 source=cli 时由 session-command.ts 解析。

当前支持:

命令 说明
/usage 计算并显示当前 session usage。
/status 显示 session/bridge/profile/model/queue/run 状态。
/abort 请求中断当前 run。
/queue <message> 当前 run 活跃时追加一条队列消息。
/plan ... 调用 bridge plan command,可能生成 kickoff prompt 并立即/排队运行。
/goal ... 设置、查询、暂停、恢复、清理 goal。
/subgoal ... 子目标命令。
/clear 清理当前显示状态,不删 DB 历史。
/clear --history 删除当前 session DB messages,要求 session idle。
/title <title> 重命名 session。
/compress session idle 时手动触发上下文压缩。
/steer <instruction> 对正在运行的 bridge run 发送 steering instruction。
/destroy 销毁 bridge agent,清空运行态和队列。
/reload-mcp [server] session idle 时重载 MCP。

未知 command 会返回 session.command error,不进入 agent run。

17. 上下文压缩

压缩有三种触发方式:

  1. run 前 buildCompressedHistory() 根据上下文窗口自动判断。
  2. Bridge 运行中发出 bridge.compression.requested
  3. 用户执行 /compress

压缩结果会写 compression snapshot,并在后续 context 构建时使用 snapshot-aware history,避免重复发送完整长历史。

事件:

  • compression.started
  • compression.completed

前端会显示临时压缩状态,并更新 session contextTokens

18. 多 tab 和断线恢复

多 tab 使用同一个 session:{id} room。

关键机制:

  • 当前 tab 发出用户消息后,其他 tab 收到 run.peer_user_message
  • 所有服务端事件都带 session_id
  • 前端全局 Socket.IO listener 根据 session_id 分发到对应 handler。
  • transient disconnect 后,前端 reconnect 会自动发 resume
  • resume 返回:
    • DB/内存 messages
    • isWorking
    • isAborting
    • transient events
    • usage/context tokens
    • queue length/messages

因此页面刷新、切换 session、断线重连后可以恢复:

  • streaming assistant
  • pending approval
  • pending clarify
  • compression state
  • abort state
  • queue state

19. HTTP Session API 和 Chat 的关系

Socket.IO /chat-run 负责 active run。HTTP API 负责静态数据读写:

能力 路径/模块
session list controllers/hermes/sessions.ts + session-store.ts
session detail/page fetchSessionMessagesPage() 对应后端分页 detail
delete session 删除 Web UI DB session/messages,同时尝试删除对应 Hermes profile 历史(如果存在)
rename session renameSession()
set session model setSessionModel()
conversation monitor 从本地 session/conversation summary 读,不驱动 active run

Chat 运行过程中落库由 Socket.IO run handler 做;页面历史和列表刷新使用 HTTP API。

20. Group Chat 链路

Group Chat 是聊天核心链路的一部分,使用独立 namespace /group-chat,但 agent 执行、context token、压缩、tool/reasoning 展示仍会复用 Agent Bridge 和共享 context-compressor。

主链路:

GroupChatPanel / GroupChatInput
  -> group-chat Pinia store
  -> packages/client/src/api/hermes/group-chat.ts
  -> Socket.IO namespace /group-chat
  -> GroupChatServer
  -> AgentClients
  -> AgentBridgeClient
  -> hermes_bridge.py broker
  -> profile worker
  -> AIAgent / Hermes Agent tools

核心行为:

  • GroupChatServer 管理 room、member、typing、message、agent runtime 和 room runtime state。
  • 普通用户通过 /group-chat socket 加入 roomagent 也作为 socket client 加入 同一个 namespace,但使用 source=agentGROUP_CHAT_AGENT_SOCKET_SECRET
  • AgentClients 根据 mention routing 选择目标 agent,构造 group history 和 instructions,然后通过 AgentBridgeClient 调用对应 profile 的 bridge session。
  • group-chat 消息、tool call、tool result、reasoning、context status 都写入 group-chat 自己的 DB 表,并广播给 room。
  • group-chat 有独立 room compression 配置: triggerTokensmaxHistoryTokenstailMessageCount
  • context 压缩通过 ContextEngine 和共享 context-compressor 实现;压缩进度会 以 room/agent 维度同步到前端。
  • 用户头像、agent 成员、在线状态、邀请链接属于 group-chat member 元数据链路; 改动这些也需要记录,因为它们会影响聊天页面展示和 room 同步。

排查 group-chat 时优先看:

  • /group-chat socket handshake auth、authUserIdsource=agent
  • GroupChatServer.onConnection()、room join/message/approval/interrupt handlers。
  • AgentClients 的 mention routing、bridge context cache、tool/reasoning 映射。
  • ContextEngine 和 room compression 配置。
  • gc_* DB 表以及 group-chat 相关 server/client tests。

21. 启动和关闭

启动时:

  1. server bootstrap 初始化 DB schema。
  2. 启动 group chat Socket.IO server。
  3. 尝试启动 Agent Bridge manager。
  4. 创建 ChatRunSocket(groupChatServer.getIO())init()

Bridge 启动失败不会阻止 Web UI server 启动,但普通 Chat run 会在调用 bridge 时失败。

关闭时:

  • ChatRunSocket.close() abort active response stream/清理内存态。
  • AgentBridgeManager.stop() 默认关闭 Python broker,包括 restart 使用的 SIGUSR2。如果需要保留 broker 和正在运行的 bridge session,可显式设置 HERMES_AGENT_BRIDGE_STOP_ON_SHUTDOWN=0
  • 其他 WebSocket/Socket.IO 服务按 shutdown 流程停止。

22. 环境变量

变量 说明
HERMES_AGENT_BRIDGE_ENDPOINT Node 到 Python bridge broker 的 endpoint。Windows 默认 TCPmacOS/Linux 默认 IPC。
HERMES_AGENT_BRIDGE_WORKER_TRANSPORT broker 到 profile worker 的 transport,支持 tcp/ipc
HERMES_AGENT_BRIDGE_WORKER_PORT_BASE TCP worker 起始端口。
HERMES_AGENT_BRIDGE_TIMEOUT_MS Node 等待 bridge 请求响应的超时,默认 120000ms。
HERMES_AGENT_BRIDGE_CONNECT_RETRY_MS Node connect bridge 的短重试窗口,默认 5000ms。
HERMES_AGENT_BRIDGE_STARTUP_TIMEOUT_MS bridge ready 超时,默认 120000ms。
HERMES_AGENT_BRIDGE_STOP_ON_SHUTDOWN Web UI shutdown/restart 是否关闭 bridge broker,默认开启。设为 0/false/no/off 才会保留 broker。
HERMES_AGENT_BRIDGE_AUTO_RESTART broker 意外退出是否自动重启,默认开启。
HERMES_AGENT_BRIDGE_RESTART_DELAY_MS 自动重启基础延迟。
HERMES_AGENT_BRIDGE_PYTHON 指定 Python 解释器。
HERMES_AGENT_ROOT 指定 hermes-agent 根目录。
HERMES_AGENT_BRIDGE_UV / UV 指定 uv。
HERMES_AGENT_BRIDGE_PLATFORM bridge 传给 Hermes Agent 的 platform,默认 cli
HERMES_BRIDGE_PROVIDER 覆盖 bridge provider。
HERMES_BRIDGE_MAX_TURNS 覆盖 bridge 最大轮数。
HERMES_WEB_UI_DISABLE_GATEWAY_AUTOSTART 跳过启动时的 gateway 检查/自动启动;dashboard-only 部署可用。
HERMES_WEB_UI_DISABLE_SKILL_INJECTION 跳过启动时的内置 skill 注入;外部管理 skills 时可用。默认注入只更新 Web UI 管理或完全相同的内置副本,用户修改的同名 skills 会跳过。
HERMES_WEB_UI_PREVIEW_AGENT_BRIDGE_TRANSPORT Version Preview bridge transport。
HERMES_WEB_UI_PREVIEW_AGENT_BRIDGE_ENDPOINT Version Preview bridge endpoint。

23. 当前限制和注意事项

  • 当前普通 Chat 实际固定走 cli bridgeapi_server handler 是保留代码,不是主链路。
  • Bridge 启动失败时 Web UI 仍能打开,但 Chat run 会失败。
  • Bridge socket 是本地 JSON line 协议,不是浏览器直接连接 Python。
  • 同一 session 只有一个 active run;并发输入走队列。
  • sessionMap 是进程内 transient 状态,server 重启后只能从 DB 恢复已落库内容,不能恢复已丢失的 Python 内存 run。
  • 工具审批依赖 Python bridge 和 Hermes tools 的审批回调;“始终允许”需要持久 allowlist 和进程内 allowlist cache 都更新。
  • workspace、profile、model/provider 都会影响 run 上下文,排查问题时不要只看 session id。
  • group-chat 是独立 namespace /group-chat 和独立 store/service,但属于聊天核心链路,相关改动也要更新本文变更记录。

24. 排查入口

常见问题和优先检查点:

问题 检查
发送后没有输出 看 server log 中 [chat-run-socket] starting CLI bridge run 和 bridge 是否 ready。
Socket 认证失败 检查 localStorage token、/chat-run auth middleware、用户 profile 权限。
profile 不对 看 socket query profile、payload profile、DB session.profile。已存在 session 优先使用 DB profile。
输出重复 stream.deltachunk.delta 是否被重复处理,重点查 bridge-delta.ts / applyBridgeChunkAsync()
工具消息丢失 flushBridgePendingToDb()recordBridgeToolStarted()recordBridgeToolCompleted()
刷新后状态不对 resume payload、state.events、前端 resumeServerWorkingRun()
队列不同步 run.queuedqueued_messagesdequeued_queue_id
审批一直弹 查 bridge approval callback、approval.respond、工具 allowlist 是否持久化且 cache 已刷新。
abort 后仍运行 bridge.interrupt()goalPause()abort.completed 和 Python worker 状态。
token/context 数不对 context_estimate、compression snapshot、refreshFinalContextUsage()