Hermes

Hermes Agent 是 Nous Research 的开源 Agent,它的 API server 在服务端保存会话上下文与历史。

这一页说明怎么接 Hermes Agent 的 API server:每个函数收到什么、要返回什么,以及这个协议要注意的地方。

结论

  • 本适配器读取本文列出的 Hermes 事件与历史形状;示例按 API server 的一组路径接入,实际部署是否提供这些接口须逐项确认。
  • 续接、重新生成、编辑后重发、会话改名和删除采用示例扩展,需要后端按后端约定另外提供。
  • 请求内容里没有模型、服务商,需要时由接入方在函数里补。
  • 服务密钥的权限取决于部署;长期服务密钥应留在代理,不应打包进浏览器。
能力示例接入方式给函数之前要确认
run直接映射示例接口—
abort直接映射示例接口—
steer直接映射示例接口—
history直接映射示例接口—
resume接入方扩展后端存了每一轮的事件,能从指定的事件编号之后接着发
regenerate / edit接入方扩展后端能作废指定的一轮和它之后的内容

最简接入

只给 run,直连本机的 API server:

adapter.ts
import { createHermesAdapter } from '@volcengine/chatkit/adapters/hermes';

// 直连本机的 Hermes API server,只用于本地开发:
// 这个密钥等于这台 Agent 的完整权限,不要提交、不要上线
const BASE = 'http://localhost:8642';
const KEY = import.meta.env.VITE_HERMES_KEY;
const post = (path: string, body: unknown) =>
  fetch(`${BASE}${path}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${KEY}` },
    body: JSON.stringify(body),
  });

export const adapter = createHermesAdapter({
  run: async ({ sessionId, body }) => {
    const send = () => post(`/api/sessions/${encodeURIComponent(sessionId)}/chat/stream`, body);
    const response = await send();
    if (response.status !== 404) return response;
    // 会话还不存在:用这个会话编号建一个,再发一次
    await post('/api/sessions', { id: sessionId });
    return send();
  },
});
有什么没有什么
多轮对话会话列表
流式显示、思考、工具调用历史:刷新后界面清空(后端的会话还在)
停止(取消请求)重新生成、编辑后重发

服务的地址、端口、密钥放在哪个请求头里,以 Hermes 的部署为准。浏览器直连时服务端还要允许跨域。

密钥

直连的写法会把密钥放进浏览器。Agent 可调用哪些工具取决于服务授权,直连仅用于受控本地开发。

会话要先存在

适配器不替接入方建会话。两种做法:

做法怎么写
不给在 run 里自己保证:后端回 404 时用这个会话编号建一个,再发一次

示例仅按 404 尝试创建,且没有检查创建响应;生产接入须先确认错误原因、创建成功和并发行为。后端对不存在的会话返回什么,以实际部署为准,确认办法见后端约定。

走自己的代理

生产环境的请求发给自己的服务,由它带上密钥转发给 Hermes:

adapter.ts
import { createHermesAdapter } from '@volcengine/chatkit/adapters/hermes';

// 请求发给自己的服务,由它带上密钥转发给 Hermes
const send = (method: string, path: string, body?: unknown) =>
  fetch(`/api/hermes${path}`, {
    method,
    credentials: 'include', // 带上登录态,服务端据此知道用户是谁
    headers: { 'Content-Type': 'application/json' },
    ...(body === undefined ? {} : { body: JSON.stringify(body) }),
  });
const session = (sessionId: string) => `/api/sessions/${encodeURIComponent(sessionId)}`;

export const adapter = createHermesAdapter({
  run: ({ sessionId, body }) => send('POST', `${session(sessionId)}/chat/stream`, body),
  abort: ({ runId }) => send('POST', `/v1/runs/${encodeURIComponent(runId)}/stop`, {}),
  history: ({ sessionId }) => send('GET', `${session(sessionId)}/messages?order=latest&limit=500`),
});
要点说明
密钥留在服务端代理转发时加上,浏览器里没有
用户只能看到自己的会话代理按登录态过滤会话列表,检查会话是不是这个用户的
事件流原样透传不要缓冲
连接断开要传到上游用户点停止时前端会断开请求,代理要跟着断开和上游的连接

全部能力

adapter.ts
import { createHermesAdapter } from '@volcengine/chatkit/adapters/hermes';

const BASE = '/api/hermes';
const send = (method: string, path: string, body?: unknown) =>
  fetch(`${BASE}${path}`, {
    method,
    headers: { 'Content-Type': 'application/json' },
    ...(body === undefined ? {} : { body: JSON.stringify(body) }),
  });
const session = (sessionId: string) => `/api/sessions/${encodeURIComponent(sessionId)}`;
const newSessionId = () => crypto.randomUUID();

export const adapter = createHermesAdapter({
  // 以下是 Hermes API server 有的接口
  run: ({ sessionId, body }) => send('POST', `${session(sessionId)}/chat/stream`, body),
  abort: ({ runId }) => send('POST', `/v1/runs/${encodeURIComponent(runId)}/stop`, {}),
  steer: ({ runId, body }) => send('POST', `/v1/runs/${encodeURIComponent(runId)}/steer`, body),
  // 历史取多少条是请求的参数,在这里定
  history: ({ sessionId }) => send('GET', `${session(sessionId)}/messages?order=latest&limit=500`),
  resume: ({ runId, eventId }) =>
    send('GET', `/v1/runs/${encodeURIComponent(runId)}/events?after=${encodeURIComponent(eventId)}`),
  regenerate: ({ sessionId, targetRunId, body }) =>
    send('POST', `${session(sessionId)}/chat/stream`, { ...body, replace_from: targetRunId }),
  edit: ({ sessionId, targetRunId, body }) =>
    send('POST', `${session(sessionId)}/chat/stream`, { ...body, replace_from: targetRunId }),
});

注释里标了「另外提供」的函数,地址和字段只是一种排法,以和后端团队商定的为准。

每个函数收到什么、要返回什么

表中“事件流”指未配置 subscribe 时的 Response 或异步事件序列。配置 subscribe 后,run / resume / regenerate / edit 只返回受理 Response,输出全部走订阅;其余能力仍返回 Response。

函数请求上的字段请求内容 body要返回的响应
runsessionId、runId、body{ input, instructions? }事件流
regenerate / edit同 run,多一个 targetRunId同 run事件流
resumesessionId、runId、eventId没有事件流,格式同 run
steersessionId、runId、body{ input }只看状态码。409 表示这一轮已经不接受
abortsessionId、runId没有只看状态码
historysessionId没有消息列表 { data, runs? }

以下是示例能力函数使用的路径,不是适配器内置路由或所有 Hermes 版本的保证:

函数接口
runPOST /api/sessions/{sessionId}/chat/stream
steerPOST /v1/runs/{runId}/steer
abortPOST /v1/runs/{runId}/stop
historyGET /api/sessions/{sessionId}/messages

响应的格式和例子见后端约定 · Hermes。

会话业务

会话目录、创建、删除、改名和 fork 由应用调用业务 API,取得真实 sessionId 后传给 useChatSessions。Adapter 不提供 sessions 配置。历史仍由 history 能力函数转换。详见会话契约。

业务状态与内容扩展

聊天里的业务字段通过 transformOutput 转成内容变更。独立业务状态由应用维护,再交给插槽组件;适配器不提供业务状态通知或累积状态 getter。

输入怎么放进请求

输入里的放在哪
正文 textinput
附件只接受 MIME 为 image/* 的附件,url 原样放进 input 的 image_url;不下载或自动内联
引用不进请求,只挂在界面上的用户消息上。正文里已经有 @名称
metadata不进请求,只挂在界面上的用户消息上

后端认别的字段时用 transformInput,返回完整请求体,覆盖默认编码后的请求内容。写法同 OpenAI 兼容接口。

输入里带了图片以外的附件时不发请求,这一轮以 UNSUPPORTED_ATTACHMENT 失败。

注意点

历史取多少条

取多少条是请求的参数,写在 history 函数的地址里,例如 ?order=latest&limit=500。历史一次取完,不支持翻页。

消息上没有 run_id 时

如果当前部署的历史没有 run_id 或 runs,限制如下:

影响说明
显示不受影响
刷新后历史里的轮次不能重新生成,不能续接
这个页面里跑过的轮次按顺序对应历史里最后几轮。别的客户端同时往这个会话写入时,对应关系可能错位

后端按后端约定返回 run_id 和准确的 runs,可避免按顺序猜轮次;续接还需要匹配消息前缀的 last_event_id 与重放能力。

会话目录

应用自行查询业务目录,转换为 SessionInfo 数组后传给 SessionList;目录响应不再交给 Adapter 解析。

工具的参数与结果

工具开始事件优先读取 args 对象,否则读取 preview;工具结束事件只读取 preview。适配器不做截断或脱敏,不保证预览完整或不含敏感内容。相同 tool_name 的开始/结束按先进先出配对,无法准确表达同名工具乱序完成。

流中途断开

连接状态与执行结果分开:断线或缺少终态只表示结果未知。给了 history 时刷新消息与轮次结论;有已收到的游标与 resume 时续流。恢复不会重新调用 run。停止后仍接收输出,远端受理取消不等于已经停止。详见断线恢复与停止确认。

错误

情况错误码能不能重试
返回非 2xx,响应内容是 { error }error.code,没有则 error.type,例如 session_not_found408、409、429、5xx 标记 retryable;不自动重发
流里的 run.failed后端给的退出原因,例如 max_iterations_reached;没有则 RUN_FAILED—
流里的 error 事件事件顶层 code,缺省 HERMES_ERROR—
连接中断或请求抛错执行结果未知可查询或续流,不自动重发
输入里有不支持的附件UNSUPPORTED_ATTACHMENT—

流里的 run.cancelled、run.interrupted 让这一轮中断,不算失败。

会话业务

会话目录、创建、删除、改名和 fork 由应用调用业务 API,取得真实 sessionId 后传给 useChatSessions。Adapter 不提供 sessions 配置。历史仍由 history 能力函数转换。详见会话契约。

本适配器的特殊处理

  • instructions 选项写入 run 请求体;steer 默认只发送非空 text,不转发附件。
  • history 遇到 404 且错误码为 session_not_found 时返回空历史;其他错误抛出。steer 的 409 返回未受理,调用过程仍可能经 onError 报告,不代表插话已经生效。
  • run.completed 为完成,run.failed/error 为失败,run.cancelled/run.interrupted 为中断。run.queued 仅表示排队,done 只结束读取;没有明确的执行结论时结果未知,不能把排队或连接关闭当成执行完成。
  • 直接 HTTP 流在结论事件后继续读到 done 或 EOF;subscribe 则在列出的结束事件关闭该轮读取器。直接流没有按 seq/SSE id 去重,恢复需后端或传输层排除重复增量。
  • 历史和列表都不分页;取最新若干条不等于完整历史,history 函数需确保返回顺序从早到晚。

本文的路径、错误码和事件语义按本适配器的行为说明;示例中的“服务已有”注释不构成对所有 Hermes 版本、权限或脱敏行为的保证。