From a70f7702a93a20bf97168f3b1a83d6f9790ed2b7 Mon Sep 17 00:00:00 2001 From: zengLingbiao Date: Mon, 20 Jul 2026 00:39:04 +0800 Subject: [PATCH] fix(agents): keep web search error serialization surrogate-safe (#111327) * fix(agents): keep web search error serialization surrogate-safe * test(agents): pin web search truncation prefix Preserve the pre-existing provider_error fallback while proving the exact UTF-16-safe retained prefix.\n\nCo-authored-by: zenglingbiao --------- Co-authored-by: Peter Steinberger --- src/agents/tools/web-search-output.ts | 3 ++- src/agents/tools/web-search.test.ts | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) 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",