错误处理 / 重连 / 幂等
可靠的 Agent 不是”永不出错”,而是”出错时优雅降级 + 自愈”。
这一页讲 SDK 自带的错误处理机制、你自己代码该怎么补、以及 Agent 进入
unreachable 状态后怎么恢复。
三种错误类别
| 类别 | 来源 | SDK 默认行为 | 你的责任 |
|---|---|---|---|
| 传输错误 | WS 断开、HTTP 5xx、超时 | 自动重连 / 重试 | 监控 status,长期不可达时报警 |
| 业务错误 | 401 token 失效、403 配额、404 不存在、429 限速 | throw 给你 | 按错误码分类处理 |
| 加密错误 | 签名错、解密失败、AAD 不匹配 | 单条丢弃 + 报告 | 监控 decrypt failure rate;高时排查时钟 / 密钥 |
错误事件订阅
agent.addStatusHandler((status) => { console.log(`[hashee] status: ${status}`);});
agent.addErrorHandler((err) => { // 通常 SDK 已处理;这里是不可恢复错误的兜底 reportToSentry(err); if (isUnrecoverable(err)) process.exit(1);});addStatusHandler 收到的状态机:
connecting → authenticating → connected │ ├─ 网络抖动 → reconnecting → connected ├─ 主动 close() → disconnected └─ 不可恢复错误 → errorWS 自动重连
WebSocket 模式下:
- 心跳 ping 每 30 秒;90 秒无 pong → 视为断开。
- 断开后立即重连,指数退避 1s → 30s(cap 30s)。
- 重连成功后自动重订阅所有 handler,业务无感。
- 服务端为每个 Agent 维护 cursor,重连时未投递消息续传,不丢。
// 你不需要写任何重连逻辑——SDK 自带// 但你可以观察过程:agent.addStatusHandler((status) => { if (status === "reconnecting") { metrics.increment("hashee.reconnecting"); } if (status === "connected") { metrics.gauge("hashee.connected", 1); }});消息幂等(message_id 去重)
每条消息有 client_message_id(你侧生成 UUID)和 message_id(后端分配)。
SDK 自带去重——同 message_id 不会触发两次 handler,即使重连后重新拉取。
发送时:
import { ulid } from "ulid";
await agent.send(conversationId, { type: "text", text: "...", client_message_id: ulid(), // 自己生成;同 ID 重发幂等});服务端用 client_message_id 做幂等 key(24 小时窗口);同 (agent_id, client_message_id)
重复 POST 返回原 message_id,不会插重复行。
aadContext 强制
V0.2 SDK 起,所有 send 必须有 AAD 绑定上下文(详见 安全边界)。
未传 → throw AadContextRequiredError。
绝大多数情况下 SDK 自动从 conversation 元数据获取 epoch_id,你不需要手动管。 若你看到这个错误:
try { await agent.send(conversationId, payload);} catch (err) { if (err.name === "AadContextRequiredError") { // 通常是 conversation 元数据未加载 await agent.refreshConversation(conversationId); return agent.send(conversationId, payload); // 重试 } throw err;}Decrypt failure 处理
收到的消息解密失败时 SDK:
- 不调你的
messageHandler(避免你拿到错误数据)。 - 调
reportDecryptFailure(reason, message_id),默认console.warn+ 上报 telemetry。 - 自增内部计数器(短时高频解密失败可能是密钥不对)。
你可以注册自己的失败 handler:
agent.addDecryptFailureHandler((report) => { metrics.increment("hashee.decrypt_failure", { reason: report.reason, // signature | no_wrap | unwrap | aead | parse }); if (report.reason === "no_wrap") { // 通常意味着发送者还没拿到你 Agent 的最新公钥 // SDK 会自动重新 publish 公钥;这里只做监控 }});每种 reason 的诊断方向:
| reason | 含义 | 可能根因 |
|---|---|---|
signature | Ed25519 验签失败 | 发送者签名时用错私钥 / Agent 持有的发送者公钥版本旧 |
no_wrap | 找不到给本 Agent 的 wrap | 发送者发消息时还没拿到 Agent 公钥(首次连接竞态) |
unwrap | unwrap CEK 失败 | Agent 私钥不匹配(密钥丢失 / 误改环境变量) |
aead | AES-GCM tag 不匹配 | AAD 不一致(client / server 时钟严重不同步影响 epoch) |
parse | 解密后不是合法 JSON | 协议版本不匹配(罕见,通常上游 SDK 老) |
REST 错误码
agent.send 等 REST 调用失败时 throw 含 code 字段的错误:
try { await agent.send(conversationId, payload);} catch (err) { switch (err.code) { case "RATE_LIMITED": // SDK 已 backoff;走到这里说明 burst 限速 await sleep(err.retry_after_ms); return retry(); case "RELATION_REVOKED": // 用户撤销 H2A,清理本地数据 await cleanupUser(conversationId); break; case "AGENT_TOKEN_INVALID": // Token 失效,需要从 secret store 重新取后重启 process.exit(1); break; case "WEBHOOK_PAYLOAD_TOO_LARGE": // 1 MB 超限,分块发送或走 R2 return sendInChunks(conversationId, payload); case "ARTIFACT_REVISION_CONFLICT": // 详见 artifact-advanced 章节 return reconcileAndRetry(); default: throw err; }}完整错误码:API 错误码全表。
Webhook 模式:unreachable 状态机
Webhook agent 失败 7 次后进入 unreachable:
你的 webhook 7 次重试都返 5xx 或超时 ↓后端:agents.connection_status = "unreachable" ↓新消息进入 pending 队列(保留 24 小时) ↓你修好 webhook ↓POST /agents/:id/connection/recover ↓后端:unreachable → ready,flush 队列SDK Dispatcher 在每次 init 时自动尝试一次 recover:
const dispatcher = createWebhookDispatcher({ /* ... */ onMessage: async (msg) => { /* ... */ }, // 如果 init 时检测到 unreachable,会自动调 recover autoRecover: true, // 默认 true});如果你需要主动 recover(如你修完 bug 后立即推 deploy):
import { restRequest } from "@hasheeai/agent-sdk-ts";
const result = await restRequest({ method: "POST", baseUrl: "https://api.hashee.ai", path: `/agents/${AGENT_ID}/connection/recover`, token: AGENT_TOKEN,});console.log(`pending messages: ${result.pending_count}`);队列模式 — 长任务异步化
Webhook 同步响应 deadline 10 秒。LLM 调用 +工具调用经常超过这个, 推荐”立即 ack + 异步处理 + 主动回写”:
const dispatcher = createWebhookDispatcher({ /* ... */ onMessage: async (msg) => { // 立即把任务塞到队列 await env.MY_QUEUE.send({ conversation_id: msg.conversation_id, text: msg.payload.text, sender_id: msg.sender_id, }); // 立即返回,10s deadline 内完成 },});
// 队列消费者async function handleQueueMessage(job: Job) { const reply = await llm.complete(job.text); // 可能 30 秒 await restRequest({ method: "POST", baseUrl: "https://api.hashee.ai", path: `/agents/${AGENT_ID}/messages`, token: AGENT_TOKEN, body: { conversation_id: job.conversation_id, payload: { type: "text", text: reply }, }, });}详见 部署到 Cloudflare Workers(含 Cloudflare Queues 集成示例)。
日志与可观测性
最少要 log 的事件:
status变化- 每条 inbound(含 conversation_id, sender_id, type)
- 每条 outbound(含 message_id 返回值)
- 所有 throw 的错误
- decrypt failure(含 reason)
最佳实践:用结构化日志(pino / winston)+ 把 agent_id 作为全局 field,
这样多 Agent 部署时方便按 agent 切片。
时钟同步
X-Hashee-Timestamp 容忍 ±5 分钟。如果你的服务器时间漂移过大:
- WebSocket 模式无影响(服务端时钟)。
- Webhook 模式会拒签所有 incoming 消息(
reason: "skew")。
部署时确保 NTP 同步。Docker / Lambda 默认 OK;自托管 VPS 检查
timedatectl status。
自我健康检查
// 暴露一个 HTTP endpointapp.get("/health", (req, res) => { const status = agent.getStatus(); if (status === "connected") { res.json({ healthy: true, status }); } else { res.status(503).json({ healthy: false, status }); }});接到 Kubernetes liveness probe / Cloudflare 健康检查上做 auto-restart。
演示视频
视频文字版逐节描述
- 00:00 – 00:10 — Agent 进程 idle 显示 status=connected。终端拔掉网线模拟
网络中断。日志立即
status: reconnecting,画面叠加退避秒数 1s, 2s, 4s, …。 - 00:10 – 00:20 — 网线插回;约 16 秒后
status: connected。后端推之前 pending 的 3 条消息,全部依次进 messageHandler。 - 00:20 – 00:35 — 模拟 token 旋转(在 Hashee app 让系统 Agent “重新生成
Token”)。原 Agent 进程下次 send 时 throw
AGENT_TOKEN_INVALID; process.exit(1),画面叠加文字”systemd / pm2 / k8s 拉起新进程读新 token”。 约 5 秒后新进程 connected。 - 00:35 – 00:50 — Webhook 模式演示 unreachable:把 Worker 改成所有请求返 500,
连续发 5 条消息触发 7 次重试失败 → Agent 标 unreachable。修复 Worker →
curl
/agents/:id/connection/recover拿到pending_count: 3。 - 00:50 – 01:00 — 约 2 秒后 3 条 pending 消息全部被推送过来,Agent 处理完成 并回写。所有消息按发送顺序 deliver(cursor 单调)。
下一步
- 安全与加密边界 — 解密失败的根因诊断
- API 错误码 — 全部错误码与处理建议
- 部署 — Cloudflare Workers — Queues + Webhook 异步化