Hermes
Hermes Agent 是 Nous Research 的开源 Agent,它的 API server 在服务端保存会话上下文与历史。
这一页说明怎么接 Hermes Agent 的 API server:每个函数收到什么、要返回什么,以及这个协议要注意的地方。
结论
- 本适配器读取本文列出的 Hermes 事件与历史形状;示例按 API server 的一组路径接入,实际部署是否提供这些接口须逐项确认。
- 续接、重新生成、编辑后重发、会话改名和删除采用示例扩展,需要后端按后端约定另外提供。
- 请求内容里没有模型、服务商,需要时由接入方在函数里补。
- 服务密钥的权限取决于部署;长期服务密钥应留在代理,不应打包进浏览器。
最简接入
只给 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 可调用哪些工具取决于服务授权,直连仅用于受控本地开发。
会话要先存在
适配器不替接入方建会话。两种做法:
示例仅按 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。
以下是示例能力函数使用的路径,不是适配器内置路由或所有 Hermes 版本的保证:
响应的格式和例子见后端约定 · Hermes。
会话业务
会话目录、创建、删除、改名和 fork 由应用调用业务 API,取得真实 sessionId 后传给 useChatSessions。Adapter 不提供 sessions 配置。历史仍由 history 能力函数转换。详见会话契约。
业务状态与内容扩展
聊天里的业务字段通过 transformOutput 转成内容变更。独立业务状态由应用维护,再交给插槽组件;适配器不提供业务状态通知或累积状态 getter。
输入怎么放进请求
后端认别的字段时用 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。停止后仍接收输出,远端受理取消不等于已经停止。详见断线恢复与停止确认。
错误
流里的 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 版本、权限或脱敏行为的保证。