diff --git a/api/server/controllers/agents/__tests__/callbacks.spec.js b/api/server/controllers/agents/__tests__/callbacks.spec.js index 34bab04619..0a2a3a1bbf 100644 --- a/api/server/controllers/agents/__tests__/callbacks.spec.js +++ b/api/server/controllers/agents/__tests__/callbacks.spec.js @@ -468,6 +468,36 @@ describe('createToolEndCallback', () => { artifactPromises = []; }); + it('preserves separate owners for final search artifacts with repeated tool-call IDs', async () => { + const toolEndCallback = createToolEndCallback({ req, res, artifactPromises }); + for (const agentId of ['agent-a', 'agent-b']) { + await toolEndCallback( + { + output: { + tool_call_id: 'call_0', + artifact: { + [Tools.web_search]: { + turn: 0, + organic: [{ link: `https://example.com/${agentId}` }], + }, + }, + }, + }, + { run_id: 'run456', thread_id: 'thread789', agent_id: agentId }, + ); + } + const results = await Promise.all(artifactPromises); + expect( + results.map((attachment) => ({ + owner: attachment.agentId, + link: attachment[Tools.web_search].organic[0].link, + })), + ).toEqual([ + { owner: 'agent-a', link: 'https://example.com/agent-a' }, + { owner: 'agent-b', link: 'https://example.com/agent-b' }, + ]); + }); + describe('ui_resources artifact handling', () => { it('should process ui_resources artifact and return attachment when headers not sent', async () => { const toolEndCallback = createToolEndCallback({ req, res, artifactPromises }); diff --git a/api/server/controllers/agents/callbacks.js b/api/server/controllers/agents/callbacks.js index 7409f1c16b..505018fe71 100644 --- a/api/server/controllers/agents/callbacks.js +++ b/api/server/controllers/agents/callbacks.js @@ -1006,6 +1006,7 @@ function createToolEndCallback({ req, res, artifactPromises, streamId = null, jo type: Tools.web_search, messageId: metadata.run_id, toolCallId: output.tool_call_id, + agentId: metadata.agent_id, conversationId: metadata.thread_id, [Tools.web_search]: { ...output.artifact[Tools.web_search] }, }; diff --git a/api/server/services/Tools/search.js b/api/server/services/Tools/search.js index c6e031e9ee..12091db173 100644 --- a/api/server/services/Tools/search.js +++ b/api/server/services/Tools/search.js @@ -34,6 +34,7 @@ function createOnSearchResults(res, streamId = null, jobCreatedAt) { sourceMap: new Map(), searchResultData: undefined, toolCallId: undefined, + agentId: undefined, attachmentName: undefined, messageId: undefined, conversationId: undefined, @@ -83,6 +84,7 @@ function createOnSearchResults(res, streamId = null, jobCreatedAt) { } context.toolCallId = runnableConfig.toolCall.id; + context.agentId = runnableConfig.metadata.agent_id; context.messageId = runnableConfig.metadata.run_id; context.conversationId = runnableConfig.metadata.thread_id; context.attachmentName = `${runnableConfig.toolCall.name}_${context.toolCallId}_${nanoid()}`; @@ -132,6 +134,7 @@ function buildAttachment(context) { return { messageId: context.messageId, toolCallId: context.toolCallId, + agentId: context.agentId, conversationId: context.conversationId, name: context.attachmentName, type: Tools.web_search, diff --git a/api/server/services/Tools/search.spec.js b/api/server/services/Tools/search.spec.js index 8925d84062..beb6bfaa05 100644 --- a/api/server/services/Tools/search.spec.js +++ b/api/server/services/Tools/search.spec.js @@ -60,4 +60,35 @@ describe('createOnSearchResults', () => { ]); } }); + + it('keeps repeated search calls owned by their handoff agent through highlights', () => { + const snapshots = []; + const res = { + headersSent: true, + write: (event) => snapshots.push(JSON.parse(event.split('\ndata: ')[1])), + }; + for (const agentId of ['agent-a', 'agent-b']) { + const callbacks = createOnSearchResults(res); + callbacks.onSearchResults( + { + success: true, + data: { organic: [{ link: `https://example.com/${agentId}` }], topStories: [] }, + }, + { + metadata: { agent_id: agentId, run_id: 'response-1', thread_id: 'conversation-1' }, + toolCall: { id: 'call_0', name: 'web_search', turn: 0 }, + }, + ); + callbacks.onGetHighlights(`https://example.com/${agentId}`); + } + + expect(snapshots.map(({ agentId }) => agentId)).toEqual([ + 'agent-a', + 'agent-a', + 'agent-b', + 'agent-b', + ]); + expect(snapshots[1].web_search.organic[0].processed).toBe(true); + expect(snapshots[3].web_search.organic[0].processed).toBe(true); + }); }); diff --git a/client/src/components/Chat/Messages/Content/Parts/Reasoning.tsx b/client/src/components/Chat/Messages/Content/Parts/Reasoning.tsx index 9b46300939..a95572378e 100644 --- a/client/src/components/Chat/Messages/Content/Parts/Reasoning.tsx +++ b/client/src/components/Chat/Messages/Content/Parts/Reasoning.tsx @@ -5,7 +5,13 @@ import { Lightbulb, ChevronDown } from 'lucide-react'; import { ContentTypes } from 'librechat-data-provider'; import { Button, disclosureChevronVariants } from '@librechat/client'; import type { MouseEvent, FocusEvent } from 'react'; -import { ThinkingContent, ThinkingButton, ThinkingLabel, FloatingThinkingBar, useInViewport } from './Thinking'; +import { + ThinkingContent, + ThinkingButton, + ThinkingLabel, + FloatingThinkingBar, + useInViewport, +} from './Thinking'; import { useLocalize, useExpandCollapse, useLazyCollapseBody } from '~/hooks'; import useSmoothStreaming from '~/hooks/Messages/useSmoothStreaming'; import CopyButton from '~/components/Messages/Content/CopyButton'; diff --git a/client/src/components/Chat/Messages/Content/Parts/Thinking.tsx b/client/src/components/Chat/Messages/Content/Parts/Thinking.tsx index 68d061c3da..5b8bc23155 100644 --- a/client/src/components/Chat/Messages/Content/Parts/Thinking.tsx +++ b/client/src/components/Chat/Messages/Content/Parts/Thinking.tsx @@ -9,7 +9,7 @@ import { type MouseEvent, } from 'react'; import { useAtomValue } from 'jotai'; -import { Lightbulb, ChevronDown, ChevronUp } from 'lucide-react'; +import { Lightbulb, ChevronDown } from 'lucide-react'; import { Button, MorphIcon, TooltipAnchor } from '@librechat/client'; import { ChevronUp as ChevronUpNode, ChevronDown as ChevronDownNode } from 'lucide'; import type { FocusEvent, FC } from 'react'; @@ -255,24 +255,21 @@ export const FloatingThinkingBar = memo( - + } /> {content && ( diff --git a/client/src/components/Chat/Messages/Content/Parts/__tests__/ThinkingButton.test.tsx b/client/src/components/Chat/Messages/Content/Parts/__tests__/ThinkingButton.test.tsx deleted file mode 100644 index e352ba8a3f..0000000000 --- a/client/src/components/Chat/Messages/Content/Parts/__tests__/ThinkingButton.test.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import { ThinkingButton } from '../Thinking'; - -jest.mock('jotai', () => ({ - ...jest.requireActual('jotai'), - useAtomValue: () => 'text-base', -})); - -jest.mock('~/hooks', () => ({ - useLocalize: () => (key: string) => key, -})); - -jest.mock('~/components/Messages/Content/CopyButton', () => ({ - __esModule: true, - default: ({ className }: { className?: string }) => ( -