fix(agent): timeout error

This commit is contained in:
chenanran555
2026-07-27 20:23:21 +08:00
parent 05860b3bdd
commit a03ee0c72c
2 changed files with 48 additions and 8 deletions
@@ -33,23 +33,47 @@ function parseSdkResponseBody(raw: string): ApiErrorBody {
return { message: raw.trim() || undefined };
}
/**
* The SDK's session polling deadline surfaces as a plain `UserError` (no
* dedicated timeout class as of SDK 0.3.x), so it is recognized by its stable
* message shape: "Session did not complete within the timeout (N seconds)."
* (session-runtime's assertNotTimedOut — the SDK's only timeout UserError).
* It is a client-side wait limit, not a usage mistake → per bl's error
* boundary it must exit TIMEOUT, not USAGE.
*/
function isSdkPollingTimeout(error: UserError): boolean {
return /did not complete within the timeout/i.test(error.message);
}
/**
* Run an SDK-backed operation, translating SDK error types into BailianError so
* bl's error handler produces the right exit code and hint formatting.
* SDK `UserError` → USAGE; SDK `ApiError` (server HTTP error) → GENERAL via
* `mapApiError` (server message passed through verbatim, with
* httpStatus/apiCode/requestId metadata for --output json); fetch transport
* failures (`TypeError: fetch failed`) are rethrown untouched so the runtime
* error handler maps them to NETWORK with an errno-specific hint, matching the
* native client path; any other Error → GENERAL (message passed through, per
* bl's "don't translate server errors" boundary).
* SDK `UserError` → USAGE — except the polling-deadline UserError, which is a
* client-side timeout → TIMEOUT with a wait-longer hint; SDK `ApiError`
* (server HTTP error) → GENERAL via `mapApiError` (server message passed
* through verbatim, with httpStatus/apiCode/requestId metadata for
* --output json); fetch transport failures (`TypeError: fetch failed`) are
* rethrown untouched so the runtime error handler maps them to NETWORK with an
* errno-specific hint, matching the native client path; any other Error →
* GENERAL (message passed through, per bl's "don't translate server errors"
* boundary).
*/
export async function withAgentErrors<T>(fn: () => Promise<T>): Promise<T> {
try {
return await fn();
} catch (error) {
if (error instanceof BailianError) throw error;
if (error instanceof UserError) throw new BailianError(error.message, ExitCode.USAGE);
if (error instanceof UserError) {
if (isSdkPollingTimeout(error)) {
throw new BailianError(
error.message,
ExitCode.TIMEOUT,
// `bl` prefix is safe: agent commands ship on `bl` only.
"The session may still be running — check `bl managed-agent session get --session-id <id>` or `session events`.",
);
}
throw new BailianError(error.message, ExitCode.USAGE);
}
if (error instanceof Error && isSdkApiError(error)) {
throw mapApiError(error.statusCode, parseSdkResponseBody(error.responseBody));
}
@@ -39,6 +39,22 @@ test("SDK UserError maps to USAGE", async () => {
expect(mapped.message).toBe("bad agents.yaml");
});
test("SDK polling-timeout UserError maps to TIMEOUT (5), not USAGE", async () => {
// 消息形状来自 SDK session-runtime 的 assertNotTimedOut —— 客户端等待超时,
// 按 bl 错误边界必须归 TIMEOUT,不能告诉自动化调用方“参数错误”。
const mapped = await catchMapped(
new UserError("Session did not complete within the timeout (600 seconds)."),
);
expect(mapped.exitCode).toBe(ExitCode.TIMEOUT);
expect(mapped.message).toBe("Session did not complete within the timeout (600 seconds).");
expect(mapped.hint).toMatch(/session get/);
});
test("提及 timeout 但非轮询超时句式的 UserError 仍归 USAGE", async () => {
const mapped = await catchMapped(new UserError("Invalid timeout value in agents.yaml"));
expect(mapped.exitCode).toBe(ExitCode.USAGE);
});
test("SDK ApiError with DashScope-style JSON body surfaces clean message and api metadata", async () => {
const body = JSON.stringify({
code: "InvalidParameter",