@librechat/agents 3.0.26 → 3.0.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -363,6 +363,32 @@ function _formatContent(message: BaseMessage) {
363
363
  return content;
364
364
  } else {
365
365
  const contentBlocks = content.map((contentPart) => {
366
+ /**
367
+ * Skip malformed blocks that have server tool fields mixed with text type.
368
+ * These can occur when server_tool_use blocks get mislabeled during aggregation.
369
+ */
370
+ if (
371
+ 'id' in contentPart &&
372
+ 'name' in contentPart &&
373
+ 'input' in contentPart &&
374
+ contentPart.type === 'text'
375
+ ) {
376
+ return null;
377
+ }
378
+
379
+ /**
380
+ * Skip malformed web_search_tool_result blocks marked as text.
381
+ * These have tool_use_id and nested content arrays.
382
+ */
383
+ if (
384
+ 'tool_use_id' in contentPart &&
385
+ 'content' in contentPart &&
386
+ Array.isArray(contentPart.content) &&
387
+ contentPart.type === 'text'
388
+ ) {
389
+ return null;
390
+ }
391
+
366
392
  if (isDataContentBlock(contentPart)) {
367
393
  return convertToProviderContentBlock(
368
394
  contentPart,
@@ -459,6 +485,21 @@ function _formatContent(message: BaseMessage) {
459
485
  }
460
486
  }
461
487
 
488
+ /**
489
+ * Skip server tool blocks when formatting content for subsequent requests.
490
+ * Server tools (e.g., web_search) are executed by the provider's API, and both
491
+ * the tool use and their results are already included in the response.
492
+ * We should not send these back in subsequent requests as they're not tool_calls
493
+ * that need invocation - they're already completed inline.
494
+ */
495
+ if (
496
+ contentPartCopy.type === 'server_tool_use' ||
497
+ contentPartCopy.type === 'web_search_tool_result' ||
498
+ contentPartCopy.type === 'web_search_result'
499
+ ) {
500
+ return null;
501
+ }
502
+
462
503
  // TODO: Fix when SDK types are fixed
463
504
  return {
464
505
  ...contentPartCopy,
@@ -487,10 +528,14 @@ function _formatContent(message: BaseMessage) {
487
528
  input: contentPart.functionCall.args,
488
529
  };
489
530
  } else {
531
+ console.error(
532
+ 'Unsupported content part:',
533
+ JSON.stringify(contentPart, null, 2)
534
+ );
490
535
  throw new Error('Unsupported message content format');
491
536
  }
492
537
  });
493
- return contentBlocks;
538
+ return contentBlocks.filter((block) => block !== null);
494
539
  }
495
540
  }
496
541
 
@@ -545,14 +590,15 @@ export function _convertMessagesToAnthropicPayload(
545
590
  }
546
591
  } else {
547
592
  const { content } = message;
548
- const hasMismatchedToolCalls = !message.tool_calls.every((toolCall) =>
549
- content.find(
550
- (contentPart) =>
551
- (contentPart.type === 'tool_use' ||
552
- contentPart.type === 'input_json_delta' ||
553
- contentPart.type === 'server_tool_use') &&
554
- contentPart.id === toolCall.id
555
- )
593
+ const hasMismatchedToolCalls = !message.tool_calls.every(
594
+ (toolCall) =>
595
+ !!content.find(
596
+ (contentPart) =>
597
+ (contentPart.type === 'tool_use' ||
598
+ contentPart.type === 'input_json_delta' ||
599
+ contentPart.type === 'server_tool_use') &&
600
+ contentPart.id === toolCall.id
601
+ )
556
602
  );
557
603
  if (hasMismatchedToolCalls) {
558
604
  console.warn(
@@ -316,20 +316,6 @@ function _convertLangChainContentToPart(
316
316
  mimeType,
317
317
  },
318
318
  };
319
- } else if (
320
- content.type === 'document' ||
321
- content.type === 'audio' ||
322
- content.type === 'video'
323
- ) {
324
- if (!isMultimodalModel) {
325
- throw new Error(`This model does not support ${content.type}s`);
326
- }
327
- return {
328
- inlineData: {
329
- data: content.data,
330
- mimeType: content.mimeType,
331
- },
332
- };
333
319
  } else if (content.type === 'media') {
334
320
  return messageContentMedia(content);
335
321
  } else if (content.type === 'tool_use') {
@@ -0,0 +1,162 @@
1
+ /* eslint-disable no-console */
2
+ // src/scripts/cli.ts
3
+ import { config } from 'dotenv';
4
+ config();
5
+ import { HumanMessage, BaseMessage } from '@langchain/core/messages';
6
+ import type * as t from '@/types';
7
+ import { ChatModelStreamHandler, createContentAggregator } from '@/stream';
8
+ import { ToolEndHandler, ModelEndHandler } from '@/events';
9
+ import { Calculator } from '@/tools/Calculator';
10
+
11
+ import { getArgs } from '@/scripts/args';
12
+ import { Run } from '@/run';
13
+ import { GraphEvents, Callback, Providers } from '@/common';
14
+ import { getLLMConfig } from '@/utils/llmConfig';
15
+
16
+ const conversationHistory: BaseMessage[] = [];
17
+ let _contentParts: (t.MessageContentComplex | undefined)[] = [];
18
+ async function testStandardStreaming(): Promise<void> {
19
+ const { userName, location, currentDate } = await getArgs();
20
+ const { contentParts, aggregateContent } = createContentAggregator();
21
+ _contentParts = contentParts;
22
+ const customHandlers = {
23
+ [GraphEvents.TOOL_END]: new ToolEndHandler(),
24
+ [GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(),
25
+ [GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler(),
26
+ [GraphEvents.ON_RUN_STEP_COMPLETED]: {
27
+ handle: (
28
+ event: GraphEvents.ON_RUN_STEP_COMPLETED,
29
+ data: t.StreamEventData
30
+ ): void => {
31
+ console.log('====== ON_RUN_STEP_COMPLETED ======');
32
+ // console.dir(data, { depth: null });
33
+ aggregateContent({
34
+ event,
35
+ data: data as unknown as { result: t.ToolEndEvent },
36
+ });
37
+ },
38
+ },
39
+ [GraphEvents.ON_RUN_STEP]: {
40
+ handle: (
41
+ event: GraphEvents.ON_RUN_STEP,
42
+ data: t.StreamEventData
43
+ ): void => {
44
+ console.log('====== ON_RUN_STEP ======');
45
+ console.dir(data, { depth: null });
46
+ aggregateContent({ event, data: data as t.RunStep });
47
+ },
48
+ },
49
+ [GraphEvents.ON_RUN_STEP_DELTA]: {
50
+ handle: (
51
+ event: GraphEvents.ON_RUN_STEP_DELTA,
52
+ data: t.StreamEventData
53
+ ): void => {
54
+ console.log('====== ON_RUN_STEP_DELTA ======');
55
+ console.dir(data, { depth: null });
56
+ aggregateContent({ event, data: data as t.RunStepDeltaEvent });
57
+ },
58
+ },
59
+ [GraphEvents.ON_MESSAGE_DELTA]: {
60
+ handle: (
61
+ event: GraphEvents.ON_MESSAGE_DELTA,
62
+ data: t.StreamEventData
63
+ ): void => {
64
+ // console.log('====== ON_MESSAGE_DELTA ======');
65
+ // console.dir(data, { depth: null });
66
+ aggregateContent({ event, data: data as t.MessageDeltaEvent });
67
+ },
68
+ },
69
+ [GraphEvents.TOOL_START]: {
70
+ handle: (
71
+ _event: string,
72
+ data: t.StreamEventData,
73
+ metadata?: Record<string, unknown>
74
+ ): void => {
75
+ console.log('====== TOOL_START ======');
76
+ // console.dir(data, { depth: null });
77
+ },
78
+ },
79
+ };
80
+
81
+ const llmConfig = getLLMConfig(
82
+ Providers.ANTHROPIC
83
+ ) as t.AnthropicClientOptions & t.SharedLLMConfig;
84
+ llmConfig.model = 'claude-haiku-4-5';
85
+
86
+ const run = await Run.create<t.IState>({
87
+ runId: 'test-run-id',
88
+ graphConfig: {
89
+ type: 'standard',
90
+ llmConfig,
91
+ tools: [
92
+ {
93
+ type: 'web_search_20250305',
94
+ name: 'web_search',
95
+ max_uses: 5,
96
+ },
97
+ new Calculator(),
98
+ ],
99
+ instructions: 'You are a friendly AI assistant.',
100
+ // additional_instructions: `Always address the user by their name. The user's name is ${userName} and they are located in ${location}.`,
101
+ },
102
+ returnContent: true,
103
+ customHandlers,
104
+ });
105
+
106
+ const config = {
107
+ configurable: {
108
+ provider: Providers.ANTHROPIC,
109
+ thread_id: 'conversation-num-1',
110
+ },
111
+ streamMode: 'values',
112
+ version: 'v2' as const,
113
+ };
114
+
115
+ console.log('Test 1: Web search + calculator (simultaneous tool test)');
116
+
117
+ // const userMessage = `
118
+ // Make a search for the weather in ${location} today, which is ${currentDate}.
119
+ // Before making the search, please let me know what you're about to do, then immediately start searching without hesitation.
120
+ // Make sure to always refer to me by name, which is ${userName}.
121
+ // After giving me a thorough summary, tell me a joke about the weather forecast we went over.
122
+ // `;
123
+ // const userMessage = 'Are massage guns good?';
124
+ // const userMessage = 'What is functional programming?';
125
+ // const userMessage = "Get me today's trending news.";
126
+ // const userMessage = "search recent italy earthquake volcano activity";
127
+ // const userMessage =
128
+ // "use 'Trump' as the exact search query and tell me what you find.";
129
+ const userMessage =
130
+ 'Can you search the web for the current population of Tokyo, and also calculate what 15% of that population would be? Do both at the same time.';
131
+
132
+ conversationHistory.push(new HumanMessage(userMessage));
133
+
134
+ const inputs = {
135
+ messages: conversationHistory,
136
+ };
137
+ const finalContentParts = await run.processStream(inputs, config);
138
+ const finalMessages = run.getRunMessages();
139
+ if (finalMessages) {
140
+ conversationHistory.push(...finalMessages);
141
+ console.dir(conversationHistory, { depth: null });
142
+ }
143
+ // console.dir(finalContentParts, { depth: null });
144
+ console.log('\n\n====================\n\n');
145
+ // console.dir(contentParts, { depth: null });
146
+ }
147
+
148
+ process.on('unhandledRejection', (reason, promise) => {
149
+ console.error('Unhandled Rejection at:', promise, 'reason:', reason);
150
+ console.log('Content Parts:');
151
+ console.dir(_contentParts, { depth: null });
152
+ process.exit(1);
153
+ });
154
+
155
+ testStandardStreaming().catch((err) => {
156
+ console.error(err);
157
+ console.log('Conversation history:');
158
+ console.dir(conversationHistory, { depth: null });
159
+ console.log('Content Parts:');
160
+ console.dir(_contentParts, { depth: null });
161
+ process.exit(1);
162
+ });
package/src/stream.ts CHANGED
@@ -155,7 +155,10 @@ export class ChatModelStreamHandler implements t.EventHandler {
155
155
  chunk.tool_calls.length > 0 &&
156
156
  chunk.tool_calls.every(
157
157
  (tc) =>
158
- tc.id != null && tc.id !== '' && tc.name != null && tc.name !== ''
158
+ tc.id != null &&
159
+ tc.id !== '' &&
160
+ (tc as Partial<ToolCall>).name != null &&
161
+ tc.name !== ''
159
162
  )
160
163
  ) {
161
164
  hasToolCalls = true;
@@ -201,7 +201,18 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
201
201
 
202
202
  outputs = await Promise.all(
203
203
  aiMessage.tool_calls
204
- ?.filter((call) => call.id == null || !toolMessageIds.has(call.id))
204
+ ?.filter((call) => {
205
+ /**
206
+ * Filter out:
207
+ * 1. Already processed tool calls (present in toolMessageIds)
208
+ * 2. Server tool calls (e.g., web_search with IDs starting with 'srvtoolu_')
209
+ * which are executed by the provider's API and don't require invocation
210
+ */
211
+ return (
212
+ (call.id == null || !toolMessageIds.has(call.id)) &&
213
+ !(call.id?.startsWith('srvtoolu_') ?? false)
214
+ );
215
+ })
205
216
  .map((call) => this.runTool(call, config)) ?? []
206
217
  );
207
218
  }