Skip to content

Commit 8c79ead

Browse files
Merge 5bb04e6 into a88e4fb
2 parents a88e4fb + 5bb04e6 commit 8c79ead

6 files changed

Lines changed: 118 additions & 18 deletions

File tree

src/agents/embedded-agent-helpers.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export {
1818
classifyFailoverReasonFromHttpStatus,
1919
formatRawAssistantErrorForUi,
2020
formatAssistantErrorText,
21+
formatUserFacingAssistantErrorText,
22+
GENERIC_ASSISTANT_ERROR_TEXT,
2123
getApiErrorPayloadFingerprint,
2224
isAuthAssistantError,
2325
isAuthErrorMessage,

src/agents/embedded-agent-helpers/errors.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export {
7171

7272
const log = createSubsystemLogger("errors");
7373
const sandboxToolPolicyAuditMessages = new WeakSet<AssistantMessage>();
74+
export const GENERIC_ASSISTANT_ERROR_TEXT = "LLM request failed.";
7475

7576
export function isReasoningConstraintErrorMessage(raw: string): boolean {
7677
if (!raw) {
@@ -1327,6 +1328,33 @@ export function formatAssistantErrorText(
13271328
return raw.length > 600 ? `${raw.slice(0, 600)}…` : raw;
13281329
}
13291330

1331+
export function isRawAssistantErrorPassthrough(params: {
1332+
friendlyError?: string;
1333+
rawError?: string;
1334+
}): boolean {
1335+
const friendlyError = params.friendlyError?.trim();
1336+
const rawError = params.rawError?.trim();
1337+
if (!friendlyError || !rawError) {
1338+
return false;
1339+
}
1340+
return (
1341+
friendlyError === rawError ||
1342+
(rawError.length > 600 && friendlyError === `${rawError.slice(0, 600)}…`)
1343+
);
1344+
}
1345+
1346+
export function formatUserFacingAssistantErrorText(
1347+
msg: AssistantMessage,
1348+
opts?: { cfg?: OpenClawConfig; sessionKey?: string; provider?: string; model?: string },
1349+
): string {
1350+
const friendlyError = formatAssistantErrorText(msg, opts);
1351+
const rawError = msg.errorMessage?.trim();
1352+
const safeFriendlyError = isRawAssistantErrorPassthrough({ friendlyError, rawError })
1353+
? undefined
1354+
: friendlyError;
1355+
return (safeFriendlyError || GENERIC_ASSISTANT_ERROR_TEXT).trim();
1356+
}
1357+
13301358
export function isRateLimitAssistantError(msg: AssistantMessage | undefined): boolean {
13311359
if (!msg || msg.stopReason !== "error") {
13321360
return false;

src/agents/embedded-agent-runner/run/payloads.errors.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,22 @@ describe("buildEmbeddedRunPayloads", () => {
156156
expectNoPayloadTextContaining(payloads, "req_synthetic_provider_request_001");
157157
});
158158

159+
it("suppresses raw assistant error messages in user-facing reply payloads", () => {
160+
const payloads = buildPayloads({
161+
lastAssistant: makeAssistant({
162+
stopReason: "error",
163+
errorMessage: "SECRET_CANARY_69737",
164+
content: [],
165+
}),
166+
});
167+
168+
expectSinglePayloadSummary(payloads, {
169+
text: "LLM request failed.",
170+
isError: true,
171+
});
172+
expectNoPayloadTextContaining(payloads, "SECRET_CANARY_69737");
173+
});
174+
159175
it("surfaces OpenAI model capacity errors instead of generic empty-response copy", () => {
160176
const payloads = buildPayloads({
161177
lastAssistant: makeAssistant({
@@ -219,6 +235,20 @@ describe("buildEmbeddedRunPayloads", () => {
219235
expectNoPayloadTextContaining(payloads, "partial answer that should not leak");
220236
});
221237

238+
it("preserves aborted-without-error behavior without adding a generic error payload", () => {
239+
const payloads = buildPayloads({
240+
runAborted: true,
241+
assistantTexts: [],
242+
lastAssistant: makeAssistant({
243+
stopReason: "aborted",
244+
errorMessage: undefined,
245+
content: [],
246+
}),
247+
});
248+
249+
expect(payloads).toHaveLength(0);
250+
});
251+
222252
it("does not replay a stale previous assistant when an aborted run has no new text", () => {
223253
const payloads = buildPayloads({
224254
runAborted: true,

src/agents/embedded-agent-runner/run/payloads.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
BILLING_ERROR_USER_MESSAGE,
2828
formatAssistantErrorText,
2929
formatRawAssistantErrorForUi,
30+
formatUserFacingAssistantErrorText,
3031
getApiErrorPayloadFingerprint,
3132
isRawApiErrorPayload,
3233
normalizeTextForComparison,
@@ -299,12 +300,19 @@ export function buildEmbeddedRunPayloads(params: {
299300
assistantForPayload && lastAssistantNeedsErrorSurface
300301
? suppressAssistantArtifacts
301302
? undefined
302-
: formatAssistantErrorText(assistantForPayload, {
303-
cfg: params.config,
304-
sessionKey: params.sessionKey,
305-
provider: params.provider,
306-
model: params.model,
307-
})
303+
: lastAssistantErrored
304+
? formatUserFacingAssistantErrorText(assistantForPayload, {
305+
cfg: params.config,
306+
sessionKey: params.sessionKey,
307+
provider: params.provider,
308+
model: params.model,
309+
})
310+
: formatAssistantErrorText(assistantForPayload, {
311+
cfg: params.config,
312+
sessionKey: params.sessionKey,
313+
provider: params.provider,
314+
model: params.model,
315+
})
308316
: undefined;
309317
const rawErrorMessage = lastAssistantNeedsErrorSurface
310318
? normalizeOptionalString(assistantForPayload?.errorMessage)

src/agents/embedded-agent-subscribe.handlers.lifecycle.test.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,35 @@ function firstWarnMeta(ctx: EmbeddedAgentSubscribeContext): Record<string, unkno
8686
}
8787

8888
describe("handleAgentEnd", () => {
89+
it("suppresses raw assistant error messages in user-facing lifecycle events", async () => {
90+
const onAgentEvent = vi.fn();
91+
const ctx = createContext(
92+
{
93+
role: "assistant",
94+
stopReason: "error",
95+
errorMessage: "SECRET_CANARY_69737",
96+
content: [],
97+
},
98+
{ onAgentEvent },
99+
);
100+
101+
await handleAgentEnd(ctx);
102+
103+
const meta = firstWarnMeta(ctx);
104+
expect(meta.error).not.toContain("SECRET_CANARY_69737");
105+
expect(meta.error).toBe("LLM request failed.");
106+
const userFacingLifecycleText = JSON.stringify(onAgentEvent.mock.calls);
107+
expect(userFacingLifecycleText).not.toContain("SECRET_CANARY_69737");
108+
expect(userFacingLifecycleText).toContain("LLM request failed.");
109+
expect(onAgentEvent).toHaveBeenCalledWith({
110+
stream: "lifecycle",
111+
data: {
112+
phase: "error",
113+
error: "LLM request failed.",
114+
},
115+
});
116+
});
117+
89118
it("logs the resolved error message when run ends with assistant error", async () => {
90119
const onAgentEvent = vi.fn();
91120
const ctx = createContext(
@@ -205,13 +234,13 @@ describe("handleAgentEnd", () => {
205234

206235
const meta = firstWarnMeta(ctx);
207236
expect(meta.event).toBe("embedded_run_agent_end");
208-
expect(meta.error).toBe("x-api-key: ***");
237+
expect(meta.error).toBe("LLM request failed.");
209238
expect(meta.rawErrorPreview).toBe("x-api-key: ***");
210239
expect(onAgentEvent).toHaveBeenCalledWith({
211240
stream: "lifecycle",
212241
data: {
213242
phase: "error",
214-
error: "x-api-key: ***",
243+
error: "LLM request failed.",
215244
},
216245
});
217246
});

src/agents/embedded-agent-subscribe.handlers.lifecycle.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ import {
77
sanitizeForConsole,
88
shouldSuppressRawErrorConsoleSuffix,
99
} from "./embedded-agent-error-observation.js";
10-
import { classifyFailoverReason, formatAssistantErrorText } from "./embedded-agent-helpers.js";
10+
import {
11+
classifyFailoverReason,
12+
formatUserFacingAssistantErrorText,
13+
GENERIC_ASSISTANT_ERROR_TEXT,
14+
} from "./embedded-agent-helpers.js";
1115
import { hasCommittedMessagingToolDeliveryEvidence } from "./embedded-agent-runner/delivery-evidence.js";
1216
import { isIncompleteTerminalAssistantTurn } from "./embedded-agent-runner/run/incomplete-turn.js";
1317
import {
@@ -72,24 +76,23 @@ export function handleAgentEnd(ctx: EmbeddedAgentSubscribeContext): void | Promi
7276
ctx.state.livenessState === "working" ? derivedWorkingTerminalState : ctx.state.livenessState;
7377

7478
if (isError && lastAssistant) {
75-
const friendlyError = formatAssistantErrorText(lastAssistant, {
79+
const rawError = lastAssistant.errorMessage?.trim();
80+
const failoverReason = classifyFailoverReason(rawError ?? "", {
81+
provider: lastAssistant.provider,
82+
});
83+
const errorText = formatUserFacingAssistantErrorText(lastAssistant, {
7684
cfg: ctx.params.config,
7785
sessionKey: ctx.params.sessionKey,
7886
provider: lastAssistant.provider,
7987
model: lastAssistant.model,
8088
});
81-
const rawError = lastAssistant.errorMessage?.trim();
82-
const failoverReason = classifyFailoverReason(rawError ?? "", {
83-
provider: lastAssistant.provider,
84-
});
85-
const errorText = (friendlyError || lastAssistant.errorMessage || "LLM request failed.").trim();
8689
const observedError = buildApiErrorObservationFields(rawError, {
8790
provider: lastAssistant.provider,
8891
});
8992
const safeErrorText =
9093
buildTextObservationFields(errorText, {
9194
provider: lastAssistant.provider,
92-
}).textPreview ?? "LLM request failed.";
95+
}).textPreview ?? GENERIC_ASSISTANT_ERROR_TEXT;
9396
lifecycleErrorText = safeErrorText;
9497
const safeRunId = sanitizeForConsole(ctx.params.runId) ?? "-";
9598
const safeModel = sanitizeForConsole(lastAssistant.model) ?? "unknown";
@@ -131,7 +134,7 @@ export function handleAgentEnd(ctx: EmbeddedAgentSubscribeContext): void | Promi
131134
stream: "lifecycle",
132135
data: {
133136
phase: "error",
134-
error: lifecycleErrorText ?? "LLM request failed.",
137+
error: lifecycleErrorText ?? GENERIC_ASSISTANT_ERROR_TEXT,
135138
...terminalMeta,
136139
...(livenessState ? { livenessState } : {}),
137140
...(replayInvalid ? { replayInvalid } : {}),
@@ -142,7 +145,7 @@ export function handleAgentEnd(ctx: EmbeddedAgentSubscribeContext): void | Promi
142145
stream: "lifecycle",
143146
data: {
144147
phase: "error",
145-
error: lifecycleErrorText ?? "LLM request failed.",
148+
error: lifecycleErrorText ?? GENERIC_ASSISTANT_ERROR_TEXT,
146149
...terminalMeta,
147150
...(livenessState ? { livenessState } : {}),
148151
...(replayInvalid ? { replayInvalid } : {}),

0 commit comments

Comments
 (0)