Hermes Dashboard 协议参考

这一页列出 Hermes Dashboard 适配器(@volcengine/chatkit/adapters/hermes-gateway)在 tui_gateway 上读写的字段,以及它们怎么变成 ChatKit 的消息、片段和轮次。接入写法见 Hermes Dashboard;API server 入口见 Hermes。

传输

通道路径(相对 Dashboard 根目录)用途
WebSocketapi/ws,查询参数 tokenJSON-RPC 2.0:客户端发请求;服务端以 method: "event" 推事件(params.type、params.session_id、params.payload)
HTTP GETapi/sessions/{id}/messages,Authorization: Bearer <token>历史
HTTP GETchat取 session token:从页面里匹配 __HERMES_SESSION_TOKEN__ = "...";给了 sessionToken 选项时不请求
要点说明
地址只拼上面的相对路径,由 resolveUrl(path, kind) 解析成完整地址;kind 为 ws 时要返回 ws(s) 地址。地址前缀与外层鉴权由接入方决定
token每次建连都重新取(页面重新下发后旧 token 失效);历史请求用最近一次取到的 token
会话编号ChatKit 的 sessionId 是持久编号 stored_session_id。事件与请求用的是这条连接上的 live 编号,每次 session.resume 都会变。连接按会话记下两者的对应,事件按 live 编号换回持久编号再分发
断线后对应关系清空;下次用到这个会话时重新取 token、重新建连、重新 session.resume;有 listen 监听时自动重连,见下文「listen 断线重连」
一个连接服务多个会话;一个适配器实例共用一个连接

一轮怎么认定

Hermes 的事件不带轮次编号。prompt.submit 固定带 queued: true,适配器按返回的 status 认定哪些事件属于这一轮:

status认定
streaming发送前开始缓冲;缓冲的事件(message.start 可能先于返回到达)和之后的事件都属于这一轮
queued会话正忙,排在正在执行的那一轮后面。先跳过当前那一轮直到它的 message.complete 或 error,下一次 message.start 起才属于这一轮;排队的这一轮分发失败时后端不发 message.start、直接发 error,这一轮以失败结束
其他(redirected、steered、rejected 等)不受理,run 抛错

读到 message.complete 或 error 时这一轮结束,之后的事件不再进这一轮。

核验版本与稳定性

tui_gateway 是 Hermes 内部接口,随版本变化。本页按 Hermes 0.20.0 安装版的 tui_gateway 源码与实测核对;升级后以 wire.types.ts 为准重新核对。完整的方法与事件见 hermes-agent 的 tui_gateway/。

发出的请求

方法什么时候发字段含义来源
session.createconnection.createSession(params)调用方给的全部字段如 cwd原样透传;读结果的 session_id(live)与 stored_session_id(持久,没有时用 session_id)
session.resume会话在这条连接上还没有 live 编号时,先于下面各请求与 listensession_id持久编号ChatKit sessionId;读结果的 session_id 作为 live 编号
image.attach_bytesrun,每张图片一次,在 prompt.submit 之前session_idlive 编号连接
filename文件名body.images[].filename(附件 name)
content_base64图片 base64,不带 data: 前缀body.images[].data(内联附件的 data)
prompt.submitrunsession_idlive 编号连接,不可覆盖
text这一轮的文本body.text:正文与引用按顺序拼接
queued固定 true连接,不可覆盖
body 里的其他字段原样放进 paramstransformInput 加的字段
session.interruptabortsession_idlive 编号连接。会话级:停的是会话里正在执行的那一轮
session.steersteersession_id、textlive 编号、追加的指令body.text;结果 status 为 rejected 时抛错
approval.respondconnection.respond,requestId 以 approval: 开头session_id、choicelive 编号、回答见审批与追问
clarify.respondconnection.respond,其他 requestIdsession_id、request_id、answerlive 编号、追问编号、答案同上
任意connection.request(method, params)调用方给的—原样发出;会话参数要用 live 编号,可先 connection.liveSessionId(sessionId) 换算

transformInput 收到默认请求体 { text, images? },返回完整请求体:

能力能改什么
runtext、images,以及加进 prompt.submit 的其他字段(session_id、queued 会被连接覆盖)
steer只有 text 会发出;images 与其他字段不发

附件只接受 MIME 为 image/*、来源为内联的图片;其他附件不发请求,抛出 cause.code 为 UNSUPPORTED_ATTACHMENT 的错误。

读取的事件

事件读取的字段映射成 ChatKit 的什么
message.start—阶段 sampling;排队的轮次从这里开始认定
message.deltatext正文片段 text:{n} 增量
reasoning.deltatext思考片段 reasoning:{n} 增量
tool.starttool_id、name、context工具片段 tool:{tool_id},status: 'running',title 为 {name} {context};阶段 tool
tool.completetool_id、name、args、result同一工具片段:args、result(字符串时按 JSON 解析)、status 为 completed;result.error 是非空字符串时为 failed 并带 error。没有收到 tool.start 时补建片段。阶段回到 sampling(还有待回答的交互时不变)
approval.requestcommand、description、choices交互请求,挂到正在执行的工具上;阶段 awaiting-external
clarify.requestrequest_id、question、choices交互请求,挂到正在执行的 clarify 工具上;阶段 awaiting-external
clarify.expirerequest_id对应的交互请求变为 cancelled
message.completestatus、error、text终态,见结束与错误
errormessage失败终态

片段规则(实时与历史相同):

规则说明
分段n 从 0 开始;出现过工具之后再来正文或思考时,n 加 1(正文与思考共用这个计数)
空白每段开头的空白去掉;结束时去掉末尾空白,空的片段删掉。与 Hermes 存进历史的正文一致
消息一轮一条回答消息 assistant:{runId},每次结构变化发整条快照,正文与思考的追加发增量

message.complete 的 usage、reasoning,tool.start 的 args_text,tool.complete 的 duration_s、summary、todos、inline_diff、result_text 等字段适配器不读;这些事件属于这一轮,可在 transformOutput 里从原始事件取。tool.start / tool.complete 是否推送受 Hermes 的 display.tool_progress 配置影响(clarify 工具总会推送)。

没有内置处理的事件

连接把一个会话的事件交给两处:

去处收到什么
connection.listen(sessionId, listener)这个会话(带本连接上的 live session_id)的全部事件,以及本地的回答状态项 request-answering / request-answer-failed / request-answered。调用时若还没有 live 编号会先 session.resume
这一轮的事件序列 → transformOutput只有认定属于这一轮的项:从这一轮开始认定到它的 message.complete / error 为止,按到达顺序,包括下表里落在这段时间内的事件;这类事件没有默认内容(defaultChanges 为空)

不带 session_id(或为空)的事件,以及属于本连接没有 resume 过的会话的事件,连接不交给任何一处;需要时接入方自行连接 Dashboard。

事件含义是否属于某一轮怎么拿
thinking.delta等待时的状态文字(不是模型思考)是transformOutput、listen
tool.generating模型正在生成某个工具调用(name)是transformOutput、listen
reasoning.available思考 / 回答的预览文本是transformOutput、listen
message.interim工具调用之间的中途说明(text、already_streamed)是transformOutput、listen
tool.output_risk工具输出的风险提示是transformOutput、listen
moa.reference、moa.aggregating、moa.progress、moa.phaseMixture-of-Agents 进度是transformOutput、listen
subagent.start、subagent.thinking、subagent.tool、subagent.complete子 Agent 进度(subagent.text 不推给父会话)是transformOutput、listen
reaction好感反馈(kind)是transformOutput、listen
sudo.request、secret.request、terminal.read.request阻塞请求:sudo 密码、secret、读取终端;带 request_id是(工具执行中)transformOutput、listen;回答见下
sudo.expire、secret.expire、terminal.read.expire上面的请求超时(request_id)是transformOutput、listen
status.update状态文字(kind、text),如压缩上下文不固定listen;落在轮内时也进 transformOutput
notification.show、notification.clear通知的显示 / 清除不固定同上
review.summary后台自我复盘的摘要不固定同上
session.title生成的会话标题(title)不固定同上
session.info会话信息(模型、工具、目录、用量等)否,多在轮次结束后、resume 与配置变更时listen
background.complete、preview.restart.progress、preview.restart.complete后台任务的进度与结果(task_id)否listen
agent.terminal.output、terminal.close、browser.progress终端输出 / 关闭、浏览器管理进度否listen
voice.*、wake.detected、billing.step_up.verification语音、唤醒词、计费验证否listen
gateway.ready、sessions.changed、platforms.changed、skin.changed、session.reclaimed、pet.*.progress连接就绪与全局广播否,不带 session_id连接不转发

阻塞请求不回答时,工具会等到超时。用 listen 拿到后经 connection.request 回答(参数是 request_id 加 password / value / text):

const off = connection.listen(sessionId, item => {
  if (item.kind !== 'event' || item.event.type !== 'sudo.request') return;
  const password = window.prompt('sudo 密码') ?? '';
  void connection.request('sudo.respond', { request_id: item.event.payload?.request_id, password });
});

在这一轮的回答里显示中途说明:

const adapter = createHermesGatewayAdapter({
  connection,
  transformOutput: (item, { messageId, defaultChanges }) => {
    if (item.kind !== 'event' || item.event.type !== 'message.interim' || !messageId) return undefined;
    return [
      ...defaultChanges,
      {
        type: 'set-part',
        messageId,
        part: { id: 'hermes-interim', type: 'data', kind: 'hermes-interim', data: item.event.payload ?? {} },
      },
    ];
  },
});

刷新加载历史时,适配器从消息行合成与实时同形的事件逐条交给 transformOutput(context.source 为 history,带这一轮的 runId 与 messageId,session_id 是持久编号):思考、正文各一条 reasoning.delta、message.delta(整段内容),工具一条 tool.start(只有 tool_id、name)与一条 tool.complete(tool_id、name、result),每轮收尾一条 message.complete。message.interim 等其它事件不在历史里,上面这张卡片刷新后不会还原;要还原就从回放里有的事件生成。

审批与追问

Hermes 推送的请求(只列读取的字段):

{ "type": "approval.request", "session_id": "<live>", "payload": { "command": "rm -rf ./tmp", "description": "delete in root path", "choices": ["once", "session", "always", "deny"] } }
{ "type": "clarify.request", "session_id": "<live>", "payload": { "request_id": "<id>", "question": "选 A 还是 B?", "choices": ["A", "B"] } }
要点审批追问
前提实例的 approvals.mode 为 manual 才交给客户端(缺省 smart 自动批准)—
请求编号Hermes 不给;连接生成 approval:{n}(连接内递增,各会话共用计数)写进 payload 的 request_idHermes 的 request_id
挂在哪最后一个还在执行、没有待回答交互的工具;没有时新建工具片段 tool:{requestId}(即 tool:approval:{n})(toolName: 'terminal')最后一个还在执行的 clarify 工具;没有时新建 tool:clarify:{request_id}
choices 缺省['once', 'deny'][](自由回答)

交互请求放在工具片段的 metadata['chatkit.interaction'] 上,用 getHermesGatewayInteraction(part) 读取:

字段审批 kind: 'approval'追问 kind: 'question'
requestId回答时交给 respond;answered / cancelled 后删除同左
statuspending / answering / answered / cancelled同左
callId挂上的工具的 toolCallId;新建片段时没有同左
command、description有值时带上—
question—问题
choices可给的回答选项
choice / answer已回答时的回答已回答时的答案
状态变化触发
→ pending收到请求;或回答没送达(request-answer-failed),可重试
pending → answering本客户端调用了 connection.respond(request-answering)
→ answered后端受理了回答(request-answered);或先收到了工具的 tool.complete:审批取本客户端记下的回答作 choice,追问取 result.user_response 作 answer
→ cancelledclarify.expire;或这一轮结束时还在等待

回答:connection.respond({ sessionId, requestId, response }),审批给 choices 里的一项,追问给答案文本。

requestId发出
approval:{n}approval.respond { session_id: <live>, choice: response }
其他clarify.respond { session_id: <live>, request_id: requestId, answer: response }

历史

GET api/sessions/{id}/messages 返回 { session_id, messages, pagination },适配器只读 messages;404 当作空历史。每一行读取:

字段用途
id排序;没有对应本地轮次时作轮次编号
roleuser 开始新的一轮;assistant、tool 归入最近一轮;其他角色与第一个 user 之前的行不用
display_kind为 hidden 的行跳过
content用户行:用户消息正文;助手行:正文片段;工具行:工具结果(按 JSON 解析)
reasoning助手行的思考片段
tool_calls数组或 JSON 字符串;每项 id、function.name、function.arguments(JSON 解析成 args)
tool_call_id、tool_name工具行对应的调用与名字
timestampUnix 秒;用户行作 createdAt,这一轮最后一行作 finishedAt
规则说明
片段与实时同一套规则,编号相同
追问历史里没有追问信令:clarify 工具结果里有 question 时,补出 status: 'answered' 的交互请求(choices_offered、user_response)
轮次编号适配器按会话记下本地结束过的轮次(读到 message.complete 或 error);历史的最后几轮从末尾按顺序对应回它们,其余用 hermes-row:{用户行 id}。页面重新加载后本地记录清空,全部按行 id 编号
轮次结果一律 origin: 'remote'、status: 'completed'、phase: 'idle'
分页一次取完,hasMore: false;不读 pagination

历史不保留:轮次是否被中断或失败、被中断那一轮的思考与部分正文、审批记录、追加指令、用户消息里的图片。进行中的轮次在结束前不写进历史。同一会话有别的客户端同时写入时,按顺序对应可能错位。

编号

ChatKit 编号来源
sessionIdstored_session_id
runId实时:ChatKit 生成;历史:本地轮次或 hermes-row:{id}
用户消息user:{runId}
回答消息assistant:{runId}
正文 / 思考片段text:{n} / reasoning:{n}
工具片段tool:{tool_id};历史里 tool:{tool_calls[].id};toolCallId 为同一个值
审批 requestIdapproval:{n},连接生成
追问 requestIdclarify.request 的 request_id
eventIdlocal:{n},每一轮从 1 起

结束与错误

情况结果
message.complete,status: 'interrupted'中断
message.complete,status: 'error'失败,HERMES_TURN_FAILED,消息取 error,没有时取 text
message.complete,其他 status完成
error 事件失败,HERMES_TURN_FAILED,消息取 message
工具结果带非空 error工具片段 failed,HERMES_TOOL_ERROR;不影响这一轮

结束时:正文与思考去掉末尾空白,等待中的交互变为 cancelled;没正常完成时,还在执行的工具标为 cancelled。

错误什么时候
HermesGatewayRpcError请求返回 JSON-RPC 错误;code 为 Hermes 的错误码
HermesGatewayClosedErrorWebSocket 断开、建连失败或 connection.close() 之后
Errorprompt.submit 没受理、session.steer 返回 rejected、取不到 session token、历史返回 404 以外的非 2xx
UNSUPPORTED_ATTACHMENT输入里有内联图片以外的附件

断线时,所有等待中的请求失败,正在读的轮次收到 HermesGatewayClosedError,由 Core 标记连接中断,不认定执行结束;适配器不支持续接,执行结果以之后的历史为准。listen 的监听按下文自动重新订阅。connection.close() 结束全部正在读的轮次,之后的请求都失败。连接内部异常经 onError(kind, error) 报告(frame、listener、listen),没给时打到控制台。

listen 断线重连

连接断开(或这个会话的订阅单独结束)时,有 listen 监听的会话:

  1. 监听先收到 { kind: 'connection', status: 'disconnected' };
  2. 连接按退避自动重连并重新订阅(重新取 token、建连并 session.resume,live 编号会变):第一次等 reconnect.minDelayMs(缺省 1000),之后翻倍,最多等 reconnect.maxDelayMs(缺省 30000);
  3. 订阅恢复后收到 { kind: 'connection', status: 'reconnected' }。断线期间的事件不会补发,需要时自行补拉状态(如重新取一次 REST 历史)。

每次重新订阅失败经 onError('listen', error) 报告,之后继续重试。取消全部监听或调用 close() 后停止;连接选项 reconnect: false 关闭自动重连(仍会收到 disconnected)。正在进行的一轮不续接,仍由会话层经历史确认终态。

示例

一轮带工具调用的事件(节选,session_id 为 live 编号):

{"jsonrpc":"2.0","method":"event","params":{"type":"message.start","session_id":"<live>"}}
{"jsonrpc":"2.0","method":"event","params":{"type":"reasoning.delta","session_id":"<live>","payload":{"text":"想一想"}}}
{"jsonrpc":"2.0","method":"event","params":{"type":"tool.start","session_id":"<live>","payload":{"tool_id":"call_1","name":"terminal","context":"printf ok"}}}
{"jsonrpc":"2.0","method":"event","params":{"type":"tool.complete","session_id":"<live>","payload":{"tool_id":"call_1","name":"terminal","args":{"command":"printf ok"},"result":{"output":"ok","exit_code":0,"error":null},"duration_s":0.1}}}
{"jsonrpc":"2.0","method":"event","params":{"type":"message.delta","session_id":"<live>","payload":{"text":"\n\n查到了"}}}
{"jsonrpc":"2.0","method":"event","params":{"type":"message.complete","session_id":"<live>","payload":{"text":"查到了","status":"complete"}}}

得到 assistant:{runId},片段依次为 reasoning:0、tool:call_1(completed)、text:1(查到了),这一轮完成。

同一轮写进历史后的行:

{
  "session_id": "<stored>",
  "messages": [
    { "id": 1, "role": "user", "content": "跑一下 printf ok", "timestamp": 1700000001 },
    {
      "id": 2,
      "role": "assistant",
      "content": "",
      "reasoning": "想一想",
      "tool_calls": "[{\"id\":\"call_1\",\"type\":\"function\",\"function\":{\"name\":\"terminal\",\"arguments\":\"{\\\"command\\\":\\\"printf ok\\\"}\"}}]",
      "timestamp": 1700000002
    },
    {
      "id": 3,
      "role": "tool",
      "tool_call_id": "call_1",
      "tool_name": "terminal",
      "content": "{\"output\":\"ok\",\"exit_code\":0,\"error\":null}",
      "timestamp": 1700000003
    },
    { "id": 4, "role": "assistant", "content": "查到了", "timestamp": 1700000004 }
  ]
}

片段编号与实时相同;页面重新加载后这一轮的 runId 为 hermes-row:1。