Agent: clarify embedded transport errors

This commit is contained in:
scoootscooob
2026-03-20 21:20:25 -07:00
parent 6b4c24c2e5
commit ceaea0dfbe
5 changed files with 98 additions and 5 deletions

View File

@@ -125,6 +125,27 @@ describe("formatAssistantErrorText", () => {
const msg = makeAssistantError("request ended without sending any chunks");
expect(formatAssistantErrorText(msg)).toBe("LLM request timed out.");
});
it("returns a connection-refused message for ECONNREFUSED failures", () => {
const msg = makeAssistantError("connect ECONNREFUSED 127.0.0.1:443 during upstream call");
expect(formatAssistantErrorText(msg)).toBe(
"LLM request failed: connection refused by the provider endpoint.",
);
});
it("returns a DNS-specific message for provider lookup failures", () => {
const msg = makeAssistantError("dial tcp: lookup api.example.com: no such host (ENOTFOUND)");
expect(formatAssistantErrorText(msg)).toBe(
"LLM request failed: DNS lookup for the provider endpoint failed.",
);
});
it("returns an interrupted-connection message for socket hang ups", () => {
const msg = makeAssistantError("socket hang up");
expect(formatAssistantErrorText(msg)).toBe(
"LLM request failed: network connection was interrupted.",
);
});
});
describe("formatRawAssistantErrorForUi", () => {

View File

@@ -88,6 +88,14 @@ describe("sanitizeUserFacingText", () => {
);
});
it("returns a transport-specific message for prefixed ECONNREFUSED errors", () => {
expect(
sanitizeUserFacingText("Error: connect ECONNREFUSED 127.0.0.1:443", {
errorContext: true,
}),
).toBe("LLM request failed: connection refused by the provider endpoint.");
});
it.each([
{
input: "Hello there!\n\nHello there!",

View File

@@ -65,6 +65,57 @@ function formatRateLimitOrOverloadedErrorCopy(raw: string): string | undefined {
return undefined;
}
function formatTransportErrorCopy(raw: string): string | undefined {
if (!raw) {
return undefined;
}
const lower = raw.toLowerCase();
if (
/\beconnrefused\b/i.test(raw) ||
lower.includes("connection refused") ||
lower.includes("actively refused")
) {
return "LLM request failed: connection refused by the provider endpoint.";
}
if (
/\beconnreset\b|\beconnaborted\b|\benetreset\b|\bepipe\b/i.test(raw) ||
lower.includes("socket hang up") ||
lower.includes("connection reset") ||
lower.includes("connection aborted")
) {
return "LLM request failed: network connection was interrupted.";
}
if (
/\benotfound\b|\beai_again\b/i.test(raw) ||
lower.includes("getaddrinfo") ||
lower.includes("no such host") ||
lower.includes("dns")
) {
return "LLM request failed: DNS lookup for the provider endpoint failed.";
}
if (
/\benetunreach\b|\behostunreach\b|\behostdown\b/i.test(raw) ||
lower.includes("network is unreachable") ||
lower.includes("host is unreachable")
) {
return "LLM request failed: the provider endpoint is unreachable from this host.";
}
if (
lower.includes("fetch failed") ||
lower.includes("connection error") ||
lower.includes("network request failed")
) {
return "LLM request failed: network connection error.";
}
return undefined;
}
function isReasoningConstraintErrorMessage(raw: string): boolean {
if (!raw) {
return false;
@@ -566,6 +617,11 @@ export function formatAssistantErrorText(
return transientCopy;
}
const transportCopy = formatTransportErrorCopy(raw);
if (transportCopy) {
return transportCopy;
}
if (isTimeoutErrorMessage(raw)) {
return "LLM request timed out.";
}
@@ -626,6 +682,10 @@ export function sanitizeUserFacingText(text: string, opts?: { errorContext?: boo
if (prefixedCopy) {
return prefixedCopy;
}
const transportCopy = formatTransportErrorCopy(trimmed);
if (transportCopy) {
return transportCopy;
}
if (isTimeoutErrorMessage(trimmed)) {
return "LLM request timed out.";
}

View File

@@ -58,14 +58,16 @@ describe("handleAgentEnd", () => {
expect(warn.mock.calls[0]?.[1]).toMatchObject({
event: "embedded_run_agent_end",
runId: "run-1",
error: "connection refused",
error: "LLM request failed: connection refused by the provider endpoint.",
rawErrorPreview: "connection refused",
consoleMessage:
"embedded run agent end: runId=run-1 isError=true model=unknown provider=unknown error=LLM request failed: connection refused by the provider endpoint. rawError=connection refused",
});
expect(onAgentEvent).toHaveBeenCalledWith({
stream: "lifecycle",
data: {
phase: "error",
error: "connection refused",
error: "LLM request failed: connection refused by the provider endpoint.",
},
});
});
@@ -92,7 +94,7 @@ describe("handleAgentEnd", () => {
failoverReason: "overloaded",
providerErrorType: "overloaded_error",
consoleMessage:
"embedded run agent end: runId=run-1 isError=true model=claude-test provider=anthropic error=The AI service is temporarily overloaded. Please try again in a moment.",
'embedded run agent end: runId=run-1 isError=true model=claude-test provider=anthropic error=The AI service is temporarily overloaded. Please try again in a moment. rawError={"type":"error","error":{"type":"overloaded_error","message":"Overloaded"}}',
});
});
@@ -112,7 +114,7 @@ describe("handleAgentEnd", () => {
const meta = warn.mock.calls[0]?.[1];
expect(meta).toMatchObject({
consoleMessage:
"embedded run agent end: runId=run-1 isError=true model=claude sonnet 4 provider=anthropic]8;;https://evil.test error=connection refused",
"embedded run agent end: runId=run-1 isError=true model=claude sonnet 4 provider=anthropic]8;;https://evil.test error=LLM request failed: connection refused by the provider endpoint. rawError=connection refused",
});
expect(meta?.consoleMessage).not.toContain("\n");
expect(meta?.consoleMessage).not.toContain("\r");

View File

@@ -50,6 +50,8 @@ export function handleAgentEnd(ctx: EmbeddedPiSubscribeContext) {
const safeRunId = sanitizeForConsole(ctx.params.runId) ?? "-";
const safeModel = sanitizeForConsole(lastAssistant.model) ?? "unknown";
const safeProvider = sanitizeForConsole(lastAssistant.provider) ?? "unknown";
const safeRawErrorPreview = sanitizeForConsole(observedError.rawErrorPreview);
const rawErrorConsoleSuffix = safeRawErrorPreview ? ` rawError=${safeRawErrorPreview}` : "";
ctx.log.warn("embedded run agent end", {
event: "embedded_run_agent_end",
tags: ["error_handling", "lifecycle", "agent_end", "assistant_error"],
@@ -60,7 +62,7 @@ export function handleAgentEnd(ctx: EmbeddedPiSubscribeContext) {
model: lastAssistant.model,
provider: lastAssistant.provider,
...observedError,
consoleMessage: `embedded run agent end: runId=${safeRunId} isError=true model=${safeModel} provider=${safeProvider} error=${safeErrorText}`,
consoleMessage: `embedded run agent end: runId=${safeRunId} isError=true model=${safeModel} provider=${safeProvider} error=${safeErrorText}${rawErrorConsoleSuffix}`,
});
emitAgentEvent({
runId: ctx.params.runId,