Files
socaichat/langgraph/src/agent/utils/retry.ts
T
gongzhiyongandClaude Sonnet 4.6 d86da0e48b
Deploy LangGraph Server to Azure Web App / build-and-deploy (push) Failing after 20s
Deploy LangGraph UI to Azure Static Web Apps / build-and-deploy (push) Failing after 38s
feat: abort/cancel support in tool-executor and retry
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 14:33:47 +08:00

137 lines
4.9 KiB
TypeScript

/**
* Execute a function with automatic retry on failure.
* Used to wrap external API calls in tool executors.
*/
export async function executeWithRetry<T>(
fn: () => Promise<T>,
retries = 1,
options?: { backoffMs?: number; exponential?: boolean; onAttempt?: (attempt: number) => void; abortSignal?: AbortSignal },
): Promise<T> {
const backoffMs = options?.backoffMs ?? 1000;
const exponential = options?.exponential ?? false;
let attempt = 0;
const maxAttempts = retries + 1;
while (true) {
try {
return await fn();
} catch (e) {
attempt++;
options?.onAttempt?.(attempt);
if (attempt >= maxAttempts) throw e;
const delay = exponential
? backoffMs * Math.pow(2, attempt - 1)
: backoffMs;
console.warn(
`[retry] attempt ${attempt}/${maxAttempts} failed, retrying in ${delay}ms`,
);
await new Promise<void>((resolve, reject) => {
const t = setTimeout(resolve, delay);
options?.abortSignal?.addEventListener("abort", () => {
clearTimeout(t);
reject(new Error("RunCancelled"));
}, { once: true });
});
}
}
}
/**
* Classify error type from raw error for context-aware messaging.
*/
function classifyError(error: unknown): "timeout" | "not_found" | "bad_request" | "auth" | "server" | "generic" {
const msg = error instanceof Error ? error.message : String(error);
if (msg.includes("TimeoutError") || msg.includes("abort") || msg.includes("timeout")) {
return "timeout";
}
if (msg.includes("401") || msg.includes("403")) return "auth";
if (msg.includes("404")) return "not_found";
if (msg.includes("400")) return "bad_request";
if (msg.includes("500") || msg.includes("502") || msg.includes("503")) return "server";
return "generic";
}
/**
* Map tool names + error type to user-friendly Chinese error messages.
* Hides raw HTTP errors, stack traces, and technical details from the LLM/user.
*/
/**
* Fallback hints appended to error messages for specific tools.
* These are only visible to the LLM (not rendered to user) so
* the LLM can autonomously decide whether to call a fallback tool.
*/
const TOOL_FALLBACK_HINTS: Record<string, string> = {
kb_search: "建议:可尝试使用搜索引擎查找相关信息。",
};
const TOOL_ERROR_MAP: Record<string, Partial<Record<ReturnType<typeof classifyError>, string>> & { generic: string }> = {
kb_search: {
timeout: "知识库检索服务暂时响应较慢,请稍后再试",
auth: "知识库权限验证失败,请联系管理员",
server: "知识库服务暂时不可用",
generic: "知识库检索服务暂时不可用,请稍后再试",
},
ticket_list: {
timeout: "工单系统响应较慢,请稍后再试",
auth: "工单系统权限验证失败,请联系管理员",
server: "工单系统服务暂时不可用",
generic: "工单列表查询失败,请稍后再试",
},
ticket_detail: {
timeout: "工单系统响应较慢,请稍后再试",
not_found: "未找到该工单,请确认工单编号后重试",
auth: "工单系统权限验证失败,请联系管理员",
server: "工单系统服务暂时不可用",
generic: "工单详情查询失败,请检查工单编号后重试",
},
google_search: {
auth: "搜索服务权限验证失败,请联系管理员",
server: "搜索服务暂时不可用",
generic: "搜索服务暂时不可用,请稍后再试",
},
web_search_deep: {
server: "深度搜索服务暂时不可用",
generic: "深度搜索暂时不可用,已尝试自动重试",
},
web_read: {
not_found: "该网页不存在或已被删除",
generic: "网页读取失败,该页面可能无法访问或已被删除",
},
code_execute: {
bad_request: "代码执行环境暂不可用,请稍后再试",
server: "代码执行服务暂时不可用",
generic: "代码执行环境暂不可用,请稍后再试",
},
code_install: {
generic: "依赖安装失败,请检查包名后重试",
},
sandbox_run: {
bad_request: "沙盒执行环境暂不可用,请稍后再试",
server: "沙盒服务暂时不可用",
generic: "沙盒执行环境暂不可用,请稍后再试",
},
doc_create: {
generic: "文档创建失败,请稍后再试",
},
doc_edit: {
generic: "文档编辑失败,请稍后再试",
},
doc_translate: {
generic: "文档翻译失败,请稍后再试",
},
};
export function formatToolError(toolName: string, error: unknown): string {
const kind = classifyError(error);
const map = TOOL_ERROR_MAP[toolName];
let friendly = map?.[kind] ?? map?.generic ?? "处理请求时遇到问题,请重试";
// Append fallback hint for LLM (not shown to user directly)
const hint = TOOL_FALLBACK_HINTS[toolName];
if (hint) {
friendly = `${friendly}\n${hint}`;
}
// Log raw error for debugging but return friendly message to LLM
console.error(`[tool-error] ${toolName} (${kind}):`, error);
return friendly;
}