diff --git a/src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts b/src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts index 35fc741db58..47460c5efa7 100644 --- a/src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts +++ b/src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts @@ -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", () => { diff --git a/src/agents/pi-embedded-helpers.sanitizeuserfacingtext.test.ts b/src/agents/pi-embedded-helpers.sanitizeuserfacingtext.test.ts index 2808d320cc5..82fe67c47f4 100644 --- a/src/agents/pi-embedded-helpers.sanitizeuserfacingtext.test.ts +++ b/src/agents/pi-embedded-helpers.sanitizeuserfacingtext.test.ts @@ -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!", diff --git a/src/agents/pi-embedded-helpers/errors.ts b/src/agents/pi-embedded-helpers/errors.ts index 7719ecb41a0..bb3d6b78206 100644 --- a/src/agents/pi-embedded-helpers/errors.ts +++ b/src/agents/pi-embedded-helpers/errors.ts @@ -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."; } diff --git a/src/agents/pi-embedded-subscribe.handlers.lifecycle.test.ts b/src/agents/pi-embedded-subscribe.handlers.lifecycle.test.ts index 911b124113a..9ffd7a53a72 100644 --- a/src/agents/pi-embedded-subscribe.handlers.lifecycle.test.ts +++ b/src/agents/pi-embedded-subscribe.handlers.lifecycle.test.ts @@ -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"); diff --git a/src/agents/pi-embedded-subscribe.handlers.lifecycle.ts b/src/agents/pi-embedded-subscribe.handlers.lifecycle.ts index 973de1ebefc..7edc299460c 100644 --- a/src/agents/pi-embedded-subscribe.handlers.lifecycle.ts +++ b/src/agents/pi-embedded-subscribe.handlers.lifecycle.ts @@ -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,