对话 (Chat)
对话 API 让你的程序像网页端一样和已发布的 Agent 聊天:发一条消息会创建(或延续)一个对话并路由给 Agent,回复通过 SSE 流式返回,直到本轮结束。 它和 OpenHex 网页端、小程序走的是同一套协议,所以通过 SDK 发起的对话和在应用里发起的没有区别。
SDK 在同一套协议上提供三层用法,按你需要的控制程度选:
| 层级 | 入口 | 适合 |
|---|---|---|
| 等整轮结果 | client.sendMessage() / conversation().send() | 脚本、后端任务、只关心最终文本 |
| 流式消费本轮 | client.runTurn() / conversation().stream() | 需要逐字输出、实时展示工具调用 |
| 协议原语 | client.chat.send / stream / messages / history / interrupt | 自己管理游标、断线续传、历史翻页 |
浏览器里做聊天界面,直接用 React 组件,下面这些它都替你做好了。
创建客户端
import { OpenhexClient } from '@openhex-ai/agent-sdk';
const client = new OpenhexClient({
apiKey: token, // 个人 API Key,或工作区签发的会话令牌
agentId: process.env.OPENHEX_AGENT_ID, // 新对话默认路由到的 Agent,见「获取 Agent ID」
});
令牌怎么选、Agent ID 从哪来,见鉴权与获取 Agent ID。
发一条消息,拿到整轮结果
const turn = await client.sendMessage('帮我总结一下最新的客服工单');
turn.text; // 本轮 Agent 回复的全部文本
turn.toolCalls; // 本轮 Agent 调用过的工具:[{ id, name, input }]
turn.conversationId; // 对话 id,继续聊时带上
turn.records; // 本轮收到的全部原始记录
turn.result; // 本轮结束时的 result 事件
turn.lastEventId; // 最后一条记录的游标
sendMessage 会先发消息,再从这条消息之后开始读流,读到本轮的结束事件为止——所以拿到的只有 Agent 对这条消息的回复,不会混进历史。
对话里的第一条消息会让平台为 Agent 拉起运行环境,首轮可能要十几秒甚至更久才出现第一个字。SDK 默认允许连续 180 秒没有任何事件,超过才判定超时(抛
AbortError);需要时用idleTimeoutMs调整。
多轮对话
conversation() 返回一个记住对话 id 的句柄,后续消息自动延续上下文:
const convo = client.conversation();
await convo.send('记住暗号 ORBIT-7,只回复 OK。');
const reply = await convo.send('暗号是什么?');
console.log(reply.text); // → ORBIT-7
console.log(convo.id); // 第一次 send 之后才有值
接续一段已有的对话:
const convo = client.conversation({ conversationId: '已有的对话 id' });
不用句柄也可以,把 conversationId 放进单次调用的选项里:
await client.sendMessage('继续', { conversationId: turn.conversationId });
新开对话,还是回到上一段?
用会话令牌(工作区成员)或个人账号和 Agent 聊天时,不带 conversationId 发消息,平台会续上这个用户和该 Agent 最近的那段对话——这样同一个人在网页、小程序之间切换,看到的是同一段聊天。
想明确开一段新对话(例如界面上的「新对话」按钮),在协议层发送时带上 newConversation: true:
const { conversationId } = await client.chat.send({
message: '我们重新开始吧',
targetAgentIds: ['你的-agent-id'],
newConversation: true,
});
// 之后用这个 id 继续
const turn = await client.sendMessage('第一个问题是…', { conversationId });
React 组件的「新对话」按钮就是这么做的。
流式输出
runTurn 发送消息,并逐条产出本轮的记录,读到结束事件时自动结束:
import { extractText, isTurnComplete } from '@openhex-ai/agent-sdk';
for await (const record of client.runTurn('用一句话介绍你自己')) {
if (record.sender === 'assistant' || record.sender === 'agent') {
process.stdout.write(extractText(record));
}
if (isTurnComplete(record)) console.log('\n[本轮结束]');
}
runTurn 不返回对话 id。需要在流式输出的同时拿到 id,用 conversation().stream():
const convo = client.conversation();
for await (const record of convo.stream('帮我查一下订单')) {
process.stdout.write(extractText(record));
}
console.log(convo.id);
从记录里提取信息用这组辅助函数(完整列表见 API 参考):
| 函数 | 作用 |
|---|---|
extractText(record) | 取出记录里的回复文本 |
extractToolCalls(record) | 取出记录里的工具调用 |
isTurnComplete(record) | 是否为本轮的结束事件 |
isInterrupt(record) | 是否表示本轮被打断 |
记录的结构
流里的每一条都是一个 ChatStreamRecord:
{
id: '1789367583782-0', // 游标,断线续传时作为 lastEventId 传回
seq: 12,
sender: 'assistant', // 'user' | 'assistant' | 'agent' | 'system'
event: 'message', // 'message' | 'agent_status' | 'attachment' | …
timestamp: 1789367583782,
sessionId: '…',
raw: { type: 'assistant', message: { content: [{ type: 'text', text: '你好' }] } },
}
一轮对话里常见的记录:
event / raw.type | 含义 |
|---|---|
message / user | 你发出的消息(回显) |
agent_status / agent_status | Agent 状态变化,如 phase: 'thinking' / 'responding' |
message / assistant | Agent 的回复内容 |
attachment | Agent 产出的图片或文件,见附件 |
message / result | 本轮结束 |
raw 里还可能出现卡片类事件(收款、信息收集、连接器配置),分别见支付宝收款、信息收集卡片、接入外部工具。
协议原语
需要完全掌控时,client.chat 把每个接口 1:1 暴露出来。
发送
const sent = await client.chat.send({
message: 'hello',
targetAgentIds: ['你的-agent-id'], // 新对话;延续对话则传 conversationId
});
// → { conversationId, userMessage, userEventId, results: [{ agentId, msgId, streamId }] }
send 立即返回,不等 Agent 回复。userEventId 是你这条消息在事件流里的位置,从它之后开始读,拿到的恰好就是这条消息的回复。
读流
for await (const record of client.chat.stream(sent.conversationId, {
lastEventId: sent.userEventId,
})) {
console.log(record.sender, record.event, record.raw.type);
if (record.raw.type === 'result') break;
}
| 选项 | 说明 |
|---|---|
lastEventId | 从这个游标之后开始读 |
turns | 不带游标时,先回放最近几轮历史(默认 10) |
includeThinking | 是否包含模型的思考过程 |
reconnect | 断线后自动重连(默认 true,指数退避) |
signal | 用 AbortController 结束读取 |
stream 是一个长连接:它会一直读下去,直到你 break、signal 中止或服务端结束。只想读一轮,用 client.chat.resumeTurn(conversationId, { lastEventId }),它读到本轮结束事件就停。
历史
// 整段对话
const { entries } = await client.chat.messages(conversationId);
// entries: [{ id, data: ChatStreamRecord }]
// 往前翻页:取某个游标之前的若干轮
const page = await client.chat.history(conversationId, {
before: entries[0].id,
turns: 10,
});
// → { entries, hasMore }
打断
await client.chat.interrupt(conversationId);
// 或者对句柄:await convo.interrupt();
取消与超时
所有方法都接受 signal:
const ac = new AbortController();
setTimeout(() => ac.abort(), 60_000);
await client.sendMessage('一个很长的任务', { signal: ac.signal, idleTimeoutMs: 300_000 });
signal:你主动取消。只取消本地等待;要让 Agent 也停下,另外调用interrupt。idleTimeoutMs:连续这么久没有任何事件才判定超时,默认 180000。Agent 在委派子 Agent 时可能长时间安静,按需调大。- 非流式请求(发送、历史、上传等)默认 30 秒超时,可以用客户端配置
timeoutMs修改。