diff --git a/CHANGELOG.md b/CHANGELOG.md index c0b471071..d5b32073a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Docs: https://docs.openclaw.ai - CLI/Config: add canonical `--strict-json` parsing for `config set` and keep `--json` as a legacy alias to reduce help/behavior drift. (#21332) thanks @adhitShet. - CLI: keep `openclaw -v` as a root-only version alias so subcommand `-v, --verbose` flags (for example ACP/hooks/skills) are no longer intercepted globally. (#21303) thanks @adhitShet. - Config/Memory: restore schema help/label metadata for hybrid `mmr` and `temporalDecay` settings so configuration surfaces show correct names and guidance. (#18786) Thanks @rodrigouroz. +- Tools/web_search: handle xAI Responses API payloads that emit top-level `output_text` blocks (without a `message` wrapper) so Grok web_search no longer returns `No response` for those results. (#20508) Thanks @echoVic. - Discord/Gateway: handle close code 4014 (missing privileged gateway intents) without crashing the gateway. Thanks @thewilloftheshadow. - Security/Net: strip sensitive headers (`Authorization`, `Proxy-Authorization`, `Cookie`, `Cookie2`) on cross-origin redirects in `fetchWithSsrFGuard` to prevent credential forwarding across origin boundaries. (#20313) Thanks @afurm. diff --git a/src/agents/tools/web-search.e2e.test.ts b/src/agents/tools/web-search.e2e.test.ts index 975f92be8..6dee999b4 100644 --- a/src/agents/tools/web-search.e2e.test.ts +++ b/src/agents/tools/web-search.e2e.test.ts @@ -219,4 +219,26 @@ describe("web_search grok response parsing", () => { expect(result.text).toBeUndefined(); expect(result.annotationCitations).toEqual([]); }); + + it("extracts output_text blocks directly in output array (no message wrapper)", () => { + const result = extractGrokContent({ + output: [ + { type: "web_search_call" }, + { + type: "output_text", + text: "direct output text", + annotations: [ + { + type: "url_citation", + url: "https://example.com/direct", + start_index: 0, + end_index: 5, + }, + ], + }, + ], + } as Parameters[0]); + expect(result.text).toBe("direct output text"); + expect(result.annotationCitations).toEqual(["https://example.com/direct"]); + }); }); diff --git a/src/agents/tools/web-search.ts b/src/agents/tools/web-search.ts index 52cf9f257..3f1c585ea 100644 --- a/src/agents/tools/web-search.ts +++ b/src/agents/tools/web-search.ts @@ -106,6 +106,7 @@ type GrokSearchResponse = { output?: Array<{ type?: string; role?: string; + text?: string; // present when type === "output_text" (top-level output_text block) content?: Array<{ type?: string; text?: string; @@ -116,6 +117,12 @@ type GrokSearchResponse = { end_index?: number; }>; }>; + annotations?: Array<{ + type?: string; + url?: string; + start_index?: number; + end_index?: number; + }>; }>; output_text?: string; // deprecated field - kept for backwards compatibility citations?: string[]; @@ -143,18 +150,33 @@ function extractGrokContent(data: GrokSearchResponse): { } { // xAI Responses API format: find the message output with text content for (const output of data.output ?? []) { - if (output.type !== "message") { - continue; - } - for (const block of output.content ?? []) { - if (block.type === "output_text" && typeof block.text === "string" && block.text) { - // Extract url_citation annotations from this content block - const urls = (block.annotations ?? []) - .filter((a) => a.type === "url_citation" && typeof a.url === "string") - .map((a) => a.url as string); - return { text: block.text, annotationCitations: [...new Set(urls)] }; + if (output.type === "message") { + for (const block of output.content ?? []) { + if (block.type === "output_text" && typeof block.text === "string" && block.text) { + const urls = (block.annotations ?? []) + .filter((a) => a.type === "url_citation" && typeof a.url === "string") + .map((a) => a.url as string); + return { text: block.text, annotationCitations: [...new Set(urls)] }; + } } } + // Some xAI responses place output_text blocks directly in the output array + // without a message wrapper. + if ( + output.type === "output_text" && + "text" in output && + typeof output.text === "string" && + output.text + ) { + const rawAnnotations = + "annotations" in output && Array.isArray(output.annotations) ? output.annotations : []; + const urls = rawAnnotations + .filter( + (a: Record) => a.type === "url_citation" && typeof a.url === "string", + ) + .map((a: Record) => a.url as string); + return { text: output.text, annotationCitations: [...new Set(urls)] }; + } } // Fallback: deprecated output_text field const text = typeof data.output_text === "string" ? data.output_text : undefined;