From 419cfad98f7c6431404e0f39c27d64652f6a7913 Mon Sep 17 00:00:00 2001 From: bjorger <50590409+bjorger@users.noreply.github.com> Date: Mon, 1 Jun 2026 11:01:00 +0200 Subject: [PATCH] fix(core): Display native web search results in agent session timeline (no-changelog) (#31420) Co-authored-by: Cursor --- .../src/runtime/__tests__/stream.test.ts | 60 +++++++++++++++++++ packages/@n8n/agents/src/runtime/stream.ts | 8 ++- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/packages/@n8n/agents/src/runtime/__tests__/stream.test.ts b/packages/@n8n/agents/src/runtime/__tests__/stream.test.ts index 63a0f46f204..1e4bfd48f8b 100644 --- a/packages/@n8n/agents/src/runtime/__tests__/stream.test.ts +++ b/packages/@n8n/agents/src/runtime/__tests__/stream.test.ts @@ -3,6 +3,7 @@ import type { TextStreamPart, ToolSet } from 'ai'; import { convertChunk } from '../stream'; type ToolCallChunk = Extract, { type: 'tool-call' }>; +type ToolResultChunk = Extract, { type: 'tool-result' }>; describe('convertChunk — tool-call invalid/error handling', () => { it('returns a tool-result with isError when c.invalid is true', () => { @@ -78,3 +79,62 @@ describe('convertChunk — tool-call invalid/error handling', () => { expect(result).toMatchObject({ type: 'tool-result', toolName: '', isError: true }); }); }); + +describe('convertChunk — tool-result output passthrough', () => { + it('passes a raw array output through verbatim (e.g. native web search results)', () => { + const output = [ + { title: 'n8n', url: 'https://n8n.io' }, + { title: 'Docs', url: 'https://docs.n8n.io' }, + ]; + const chunk = { + type: 'tool-result', + toolCallId: 'tc-1', + toolName: 'anthropic.web_search_20250305', + input: { query: 'n8n' }, + output, + providerExecuted: true, + } as unknown as ToolResultChunk; + + expect(convertChunk(chunk)).toEqual({ + type: 'tool-result', + toolCallId: 'tc-1', + toolName: 'anthropic.web_search_20250305', + output, + }); + }); + + it('passes a raw object output through verbatim without unwrapping a coincidental "value" key', () => { + const output = { value: 42, unit: 'C' }; + const chunk = { + type: 'tool-result', + toolCallId: 'tc-2', + toolName: 'get_temperature', + input: {}, + output, + } as unknown as ToolResultChunk; + + expect(convertChunk(chunk)).toEqual({ + type: 'tool-result', + toolCallId: 'tc-2', + toolName: 'get_temperature', + output, + }); + }); + + it('falls back to empty strings when toolCallId and toolName are absent', () => { + const chunk = { + type: 'tool-result', + toolCallId: undefined, + toolName: undefined, + input: {}, + output: 'plain text result', + } as unknown as ToolResultChunk; + + expect(convertChunk(chunk)).toEqual({ + type: 'tool-result', + toolCallId: '', + toolName: '', + output: 'plain text result', + }); + }); +}); diff --git a/packages/@n8n/agents/src/runtime/stream.ts b/packages/@n8n/agents/src/runtime/stream.ts index a32ed0715b4..4f29ecef104 100644 --- a/packages/@n8n/agents/src/runtime/stream.ts +++ b/packages/@n8n/agents/src/runtime/stream.ts @@ -96,12 +96,16 @@ export function convertChunk(c: TextStreamPart): StreamChunk | undefine } case 'tool-result': + // The fullStream emits the raw tool output here, not the + // `{ type, value }` ToolResultOutput wrapper used on the message + // side — so pass it through verbatim. Only provider-executed tools + // (e.g. native web search) reach this branch; local tool results are + // written directly by the runtime and never pass through convertChunk. return { type: 'tool-result', toolCallId: c.toolCallId ?? '', toolName: c.toolName ?? '', - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - output: c.output && 'value' in c.output ? (c.output.value as JSONValue) : null, + output: c.output, }; case 'error':