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

933 lines
34 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 的主链路是:
```text
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 至少包含:
```md
---
date: YYYY-MM-DD
pr: 1234
feature: 改动功能
impact: 行为影响
---
补充说明。
```
开 PR 前还没有编号时可以先写 `pr: pending`PR 创建后再把该 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 的中断状态。 |
### 新建会话
普通新建入口最终调用:
```ts
newChat(options)
```
它会:
1. 从当前 app model/profile 状态创建本地前端 session 对象。
2. 切换到这个 session。
3. 此时 DB 里不一定已经有 session 行;真正落库发生在第一次 run 时。
历史上的 `newCliSession()` 仍存在,但普通 Chat 当前也是 `source=cli`,不再需要单独 UI 面板区分。
### 切换会话
切换 session 时,store 会优先通过 Socket.IO `resume`
```ts
resumeSession(sessionId, onResumed, profile)
```
服务端会:
1. `socket.join("session:<id>")`
2. 如果内存 `sessionMap` 有状态,直接返回。
3. 否则从 DB 读取分页消息和 usage。
4. 如果该 session 正在运行,返回最近 transient events。
前端收到 `resumed` 后会:
- 替换/补齐本地消息。
- 恢复 `isWorking``isAborting`、queue、usage。
- 对 active run 重新注册 handlers。
- 重新显示审批、澄清、压缩、abort 等未完成状态。
## 5. 发送消息链路
### 前端发送
入口是 `chat.ts` store 的:
```ts
sendMessage(content, attachments?)
```
主要步骤:
1. 如果没有 active session,先创建一个。
2. 捕获发送时的 `sid`,后续所有回调都用这个 sid,避免用户切换 session 后事件写错地方。
3. 判断是否 slash command`content.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 形态:
```ts
{
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. 发送:
```ts
socket.emit("run", body)
```
如果同一个 session 已经有 handler,新的 run 只 emit,不重复注册,避免多 tab/多次发送造成事件重复处理。
## 6. `/chat-run` 连接和认证
前端连接:
```ts
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 分流
当前代码:
```ts
export function resolveRunSource(_source?: string, _sessionId?: string): ChatRunSource {
return "cli"
}
```
因此:
- payload 即使传 `source: "api_server"`,当前仍会被解析为 `cli`
- `handleApiRun()` 是保留实现,不是当前普通 Chat 的实际路径。
- 新会话落库时 `source` 写为 `cli`
### sessionMap
后端内存态是:
```ts
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 详细链路
实际执行在:
```ts
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 前调用:
```ts
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
服务端发起:
```ts
bridge.chat(sessionId, bridgeInput, bridgeHistory, fullInstructions, profile, {
storage_message,
model,
provider,
})
```
返回:
```ts
{
ok: true,
run_id,
session_id,
status
}
```
随后 Node 轮询:
```ts
for await (const chunk of bridge.streamOutput(run_id)) {
applyBridgeChunkAsync(...)
}
```
`streamOutput()` 实际通过 `get_output` action,携带 `cursor``event_cursor`
增量读取文本和事件。
## 9. Python Agent Bridge
### 9.1 进程结构
`AgentBridgeManager` 启动 `hermes_bridge.py` broker 子进程:
```text
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 协议。
示例请求:
```json
{"action":"chat","session_id":"abc","message":"hello","profile":"default"}
```
示例响应:
```json
{"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.delta`,Node 以事件顺序处理文本,避免再处理
聚合 `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.completed``run.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 完成后仍有队列。
队列项类型:
```ts
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_messages`UI 可见队列消息。
- `dequeued_queue_id`:某条队列开始执行,前端移除 queued 展示。
取消队列:
```ts
socket.emit("cancel_queued_run", { session_id, queue_id })
```
## 13. Abort
前端 stop 按钮最终调用:
```ts
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 侧生成:
```json
{
"event": "approval.requested",
"approval_id": "...",
"command": "...",
"description": "...",
"choices": ["once", "session", "always", "deny"],
"allow_permanent": true,
"timeout_ms": 60000
}
```
Node 映射到 `/chat-run`
```ts
approval.requested
```
前端:
- `chat.ts` store 写入 `pendingApprovals`
- `ChatPanel.vue` 显示审批 UI。
- 用户选择后调用:
```ts
respondToolApproval(sessionId, approvalId, choice)
```
再发送:
```ts
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
```json
{
"event": "clarify.requested",
"clarify_id": "...",
"question": "...",
"choices": ["..."],
"timeout_ms": 60000
}
```
前端显示澄清 UI,并发送:
```ts
socket.emit("clarify.respond", {
session_id,
clarify_id,
response
})
```
服务端转发到:
```ts
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。
主链路:
```text
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=agent``GROUP_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 配置:
`triggerTokens``maxHistoryTokens``tailMessageCount`
- context 压缩通过 `ContextEngine` 和共享 `context-compressor` 实现;压缩进度会
以 room/agent 维度同步到前端。
- 用户头像、agent 成员、在线状态、邀请链接属于 group-chat member 元数据链路;
改动这些也需要记录,因为它们会影响聊天页面展示和 room 同步。
排查 group-chat 时优先看:
- `/group-chat` socket handshake auth、`authUserId``source=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` bridge`api_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.delta``chunk.delta` 是否被重复处理,重点查 `bridge-delta.ts` / `applyBridgeChunkAsync()`。 |
| 工具消息丢失 | 查 `flushBridgePendingToDb()``recordBridgeToolStarted()``recordBridgeToolCompleted()`。 |
| 刷新后状态不对 | 查 `resume` payload、`state.events`、前端 `resumeServerWorkingRun()`。 |
| 队列不同步 | 查 `run.queued``queued_messages``dequeued_queue_id`。 |
| 审批一直弹 | 查 bridge approval callback、`approval.respond`、工具 allowlist 是否持久化且 cache 已刷新。 |
| abort 后仍运行 | 查 `bridge.interrupt()``goalPause()``abort.completed` 和 Python worker 状态。 |
| token/context 数不对 | 查 `context_estimate`、compression snapshot、`refreshFinalContextUsage()`。 |