diff --git a/src/agents/tools/web-search-output.ts b/src/agents/tools/web-search-output.ts index ef910be9c53..3c1f14e1091 100644 --- a/src/agents/tools/web-search-output.ts +++ b/src/agents/tools/web-search-output.ts @@ -8,6 +8,7 @@ * spoof the trust marker and transport-specific extras never reach the model. */ import { isRecord } from "@openclaw/normalization-core/record-coerce"; +import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice"; import type { Static } from "typebox"; import { Type } from "typebox"; import { wrapWebContent } from "../../security/external-content.js"; @@ -211,7 +212,7 @@ export function normalizeWebSearchOutput(params: { const rawError = typeof result.error === "string" ? result.error - : (JSON.stringify(result.error)?.slice(0, 2_000) ?? "provider_error"); + : truncateUtf16Safe(JSON.stringify(result.error) ?? "provider_error", 2_000); const rawMessage = typeof result.message === "string" ? result.message : rawError; const docs = typeof result.docs === "string" ? toHttpUrl(result.docs) : undefined; return { diff --git a/src/agents/tools/web-search.test.ts b/src/agents/tools/web-search.test.ts index 50d2aefdd43..76bc4218c55 100644 --- a/src/agents/tools/web-search.test.ts +++ b/src/agents/tools/web-search.test.ts @@ -721,6 +721,23 @@ describe("web_search normalized output contract", () => { expect(normalized.message).toContain("quota exceeded"); }); + it("keeps structured provider error serialization surrogate-safe at the cap", () => { + const normalized = normalizeWebSearchOutput({ + provider: "external-demo", + query: "emoji error", + // The emoji straddles the 2000-unit cut: a plain slice would keep only + // its high surrogate half. + result: { error: { message: `${"x".repeat(1_987)}🤖` } }, + }); + + if (normalized.kind !== "error") { + throw new Error("expected error branch"); + } + const expectedPrefix = `{"message":"${"x".repeat(1_987)}`; + expect(stripWrapMarkers(normalized.message)).toBe(expectedPrefix); + expect(expectedPrefix).toHaveLength(1_999); + }); + it("keeps ordinary Source attribution lines in answer content", () => { const normalized = normalizeWebSearchOutput({ provider: "external-answer",