@librechat/agents 3.0.13 → 3.0.15

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.
@@ -83,8 +83,21 @@ export function addBedrockCacheControl<
83
83
  i--
84
84
  ) {
85
85
  const message = updatedMessages[i];
86
+
87
+ if (
88
+ 'getType' in message &&
89
+ typeof message.getType === 'function' &&
90
+ message.getType() === 'tool'
91
+ ) {
92
+ continue;
93
+ }
94
+
86
95
  const content = message.content;
87
96
 
97
+ if (typeof content === 'string' && content === '') {
98
+ continue;
99
+ }
100
+
88
101
  if (typeof content === 'string') {
89
102
  message.content = [
90
103
  { type: ContentTypes.TEXT, text: content },
@@ -95,11 +108,29 @@ export function addBedrockCacheControl<
95
108
  }
96
109
 
97
110
  if (Array.isArray(content)) {
111
+ let hasCacheableContent = false;
112
+ for (const block of content) {
113
+ if (block.type === ContentTypes.TEXT) {
114
+ if (typeof block.text === 'string' && block.text !== '') {
115
+ hasCacheableContent = true;
116
+ break;
117
+ }
118
+ }
119
+ }
120
+
121
+ if (!hasCacheableContent) {
122
+ continue;
123
+ }
124
+
98
125
  let inserted = false;
99
126
  for (let j = content.length - 1; j >= 0; j--) {
100
127
  const block = content[j] as MessageContentComplex;
101
128
  const type = (block as { type?: string }).type;
102
129
  if (type === ContentTypes.TEXT || type === 'text') {
130
+ const text = (block as { text?: string }).text;
131
+ if (text === '' || text === undefined) {
132
+ continue;
133
+ }
103
134
  content.splice(j + 1, 0, {
104
135
  cachePoint: { type: 'default' },
105
136
  } as MessageContentComplex);
@@ -310,18 +310,24 @@ function formatAssistantMessage(
310
310
  });
311
311
  formattedMessages.push(lastAIMessage);
312
312
  } else if (part.type === ContentTypes.TOOL_CALL) {
313
- if (!lastAIMessage) {
314
- // "Heal" the payload by creating an AIMessage to precede the tool call
315
- lastAIMessage = new AIMessage({ content: '' });
316
- formattedMessages.push(lastAIMessage);
317
- }
318
-
319
313
  // Note: `tool_calls` list is defined when constructed by `AIMessage` class, and outputs should be excluded from it
320
314
  const {
321
315
  output,
322
316
  args: _args,
323
317
  ..._tool_call
324
318
  } = part.tool_call as ToolCallPart;
319
+
320
+ // Skip invalid tool calls that have no name AND no output
321
+ if (!_tool_call.name && (output == null || output === '')) {
322
+ continue;
323
+ }
324
+
325
+ if (!lastAIMessage) {
326
+ // "Heal" the payload by creating an AIMessage to precede the tool call
327
+ lastAIMessage = new AIMessage({ content: '' });
328
+ formattedMessages.push(lastAIMessage);
329
+ }
330
+
325
331
  const tool_call: ToolCallPart = _tool_call;
326
332
  // TODO: investigate; args as dictionary may need to be providers-or-tool-specific
327
333
  let args: any = _args;
@@ -914,4 +914,171 @@ describe('formatAgentMessages', () => {
914
914
 
915
915
  expect(totalTokens).toBe(10);
916
916
  });
917
+
918
+ it('should skip invalid tool calls with no name AND no output', () => {
919
+ const payload = [
920
+ {
921
+ role: 'assistant',
922
+ content: [
923
+ {
924
+ type: ContentTypes.TEXT,
925
+ [ContentTypes.TEXT]: 'Let me help you with that.',
926
+ tool_call_ids: ['valid_tool_1'],
927
+ },
928
+ {
929
+ type: ContentTypes.TOOL_CALL,
930
+ tool_call: {
931
+ id: 'invalid_tool_1',
932
+ name: '',
933
+ args: '{"query":"test"}',
934
+ output: '',
935
+ },
936
+ },
937
+ {
938
+ type: ContentTypes.TOOL_CALL,
939
+ tool_call: {
940
+ id: 'valid_tool_1',
941
+ name: 'search',
942
+ args: '{"query":"weather"}',
943
+ output: 'The weather is sunny.',
944
+ },
945
+ },
946
+ ],
947
+ },
948
+ ];
949
+
950
+ const result = formatAgentMessages(payload);
951
+
952
+ // Should have 2 messages: AIMessage and ToolMessage (invalid tool call is skipped)
953
+ expect(result.messages).toHaveLength(2);
954
+ expect(result.messages[0]).toBeInstanceOf(AIMessage);
955
+ expect(result.messages[1]).toBeInstanceOf(ToolMessage);
956
+
957
+ // The AIMessage should only have 1 tool call (the valid one)
958
+ expect((result.messages[0] as AIMessage).tool_calls).toHaveLength(1);
959
+ expect((result.messages[0] as AIMessage).tool_calls?.[0].name).toBe(
960
+ 'search'
961
+ );
962
+ expect((result.messages[0] as AIMessage).tool_calls?.[0].id).toBe(
963
+ 'valid_tool_1'
964
+ );
965
+
966
+ // The ToolMessage should be for the valid tool call
967
+ expect((result.messages[1] as ToolMessage).tool_call_id).toBe(
968
+ 'valid_tool_1'
969
+ );
970
+ expect(result.messages[1].name).toBe('search');
971
+ expect(result.messages[1].content).toBe('The weather is sunny.');
972
+ });
973
+
974
+ it('should skip tool calls with no name AND null output', () => {
975
+ const payload = [
976
+ {
977
+ role: 'assistant',
978
+ content: [
979
+ {
980
+ type: ContentTypes.TOOL_CALL,
981
+ tool_call: {
982
+ id: 'invalid_tool_1',
983
+ name: '',
984
+ args: '{"query":"test"}',
985
+ output: null,
986
+ },
987
+ },
988
+ {
989
+ type: ContentTypes.TEXT,
990
+ [ContentTypes.TEXT]: 'Here is the information.',
991
+ },
992
+ ],
993
+ },
994
+ ];
995
+
996
+ const result = formatAgentMessages(payload);
997
+
998
+ // Should have 1 message: AIMessage (invalid tool call is skipped)
999
+ expect(result.messages).toHaveLength(1);
1000
+ expect(result.messages[0]).toBeInstanceOf(AIMessage);
1001
+
1002
+ // The AIMessage should have no tool calls or an empty array
1003
+ const toolCalls = (result.messages[0] as AIMessage).tool_calls;
1004
+ expect(toolCalls === undefined || toolCalls.length === 0).toBe(true);
1005
+ expect(result.messages[0].content).toStrictEqual([
1006
+ {
1007
+ type: ContentTypes.TEXT,
1008
+ [ContentTypes.TEXT]: 'Here is the information.',
1009
+ },
1010
+ ]);
1011
+ });
1012
+
1013
+ it('should NOT skip tool calls with no name but valid output', () => {
1014
+ const payload = [
1015
+ {
1016
+ role: 'assistant',
1017
+ content: [
1018
+ {
1019
+ type: ContentTypes.TOOL_CALL,
1020
+ tool_call: {
1021
+ id: 'tool_1',
1022
+ name: '',
1023
+ args: '{"query":"test"}',
1024
+ output: 'Valid output despite missing name',
1025
+ },
1026
+ },
1027
+ ],
1028
+ },
1029
+ ];
1030
+
1031
+ const result = formatAgentMessages(payload);
1032
+
1033
+ // Should have 2 messages: AIMessage and ToolMessage
1034
+ expect(result.messages).toHaveLength(2);
1035
+ expect(result.messages[0]).toBeInstanceOf(AIMessage);
1036
+ expect(result.messages[1]).toBeInstanceOf(ToolMessage);
1037
+
1038
+ // The AIMessage should have 1 tool call
1039
+ expect((result.messages[0] as AIMessage).tool_calls).toHaveLength(1);
1040
+
1041
+ // The ToolMessage should have the output
1042
+ expect((result.messages[1] as ToolMessage).tool_call_id).toBe('tool_1');
1043
+ expect(result.messages[1].content).toBe(
1044
+ 'Valid output despite missing name'
1045
+ );
1046
+ });
1047
+
1048
+ it('should NOT skip tool calls with valid name but no output', () => {
1049
+ const payload = [
1050
+ {
1051
+ role: 'assistant',
1052
+ content: [
1053
+ {
1054
+ type: ContentTypes.TOOL_CALL,
1055
+ tool_call: {
1056
+ id: 'tool_1',
1057
+ name: 'search',
1058
+ args: '{"query":"test"}',
1059
+ output: '',
1060
+ },
1061
+ },
1062
+ ],
1063
+ },
1064
+ ];
1065
+
1066
+ const result = formatAgentMessages(payload);
1067
+
1068
+ // Should have 2 messages: AIMessage and ToolMessage
1069
+ expect(result.messages).toHaveLength(2);
1070
+ expect(result.messages[0]).toBeInstanceOf(AIMessage);
1071
+ expect(result.messages[1]).toBeInstanceOf(ToolMessage);
1072
+
1073
+ // The AIMessage should have 1 tool call
1074
+ expect((result.messages[0] as AIMessage).tool_calls).toHaveLength(1);
1075
+ expect((result.messages[0] as AIMessage).tool_calls?.[0].name).toBe(
1076
+ 'search'
1077
+ );
1078
+
1079
+ // The ToolMessage should have empty content
1080
+ expect((result.messages[1] as ToolMessage).tool_call_id).toBe('tool_1');
1081
+ expect(result.messages[1].name).toBe('search');
1082
+ expect(result.messages[1].content).toBe('');
1083
+ });
917
1084
  });
@@ -10,7 +10,7 @@ import { createSearchTool } from '@/tools/search';
10
10
 
11
11
  import { getArgs } from '@/scripts/args';
12
12
  import { Run } from '@/run';
13
- import { GraphEvents, Callback } from '@/common';
13
+ import { GraphEvents, Callback, Providers } from '@/common';
14
14
  import { getLLMConfig } from '@/utils/llmConfig';
15
15
 
16
16
  const conversationHistory: BaseMessage[] = [];
@@ -72,6 +72,10 @@ async function testStandardStreaming(): Promise<void> {
72
72
 
73
73
  const llmConfig = getLLMConfig(provider);
74
74
 
75
+ if (llmConfig.provider === Providers.BEDROCK) {
76
+ (llmConfig as t.BedrockAnthropicInput).promptCache = true;
77
+ }
78
+
75
79
  const run = await Run.create<t.IState>({
76
80
  runId: 'test-run-id',
77
81
  graphConfig: {
@@ -2,15 +2,16 @@
2
2
  // src/scripts/cli.ts
3
3
  import { config } from 'dotenv';
4
4
  config();
5
+
5
6
  import { HumanMessage, BaseMessage } from '@langchain/core/messages';
6
7
  import type * as t from '@/types';
7
8
  import { ChatModelStreamHandler, createContentAggregator } from '@/stream';
8
9
  import { ToolEndHandler, ModelEndHandler } from '@/events';
9
-
10
+ import { GraphEvents, Providers } from '@/common';
11
+ import { getLLMConfig } from '@/utils/llmConfig';
12
+ import { Calculator } from '@/tools/Calculator';
10
13
  import { getArgs } from '@/scripts/args';
11
14
  import { Run } from '@/run';
12
- import { GraphEvents, Callback } from '@/common';
13
- import { getLLMConfig } from '@/utils/llmConfig';
14
15
 
15
16
  const conversationHistory: BaseMessage[] = [];
16
17
  async function testStandardStreaming(): Promise<void> {
@@ -89,12 +90,16 @@ async function testStandardStreaming(): Promise<void> {
89
90
 
90
91
  const llmConfig = getLLMConfig(provider);
91
92
 
93
+ if (llmConfig.provider === Providers.BEDROCK) {
94
+ (llmConfig as t.BedrockAnthropicInput).promptCache = true;
95
+ }
96
+
92
97
  const run = await Run.create<t.IState>({
93
98
  runId: 'test-run-id',
94
99
  graphConfig: {
95
100
  type: 'standard',
96
101
  llmConfig,
97
- tools: [],
102
+ tools: [new Calculator()],
98
103
  instructions:
99
104
  'You are a friendly AI assistant. Always address the user by their name.',
100
105
  additional_instructions: `The user's name is ${userName} and they are located in ${location}.`,
@@ -114,13 +119,9 @@ async function testStandardStreaming(): Promise<void> {
114
119
  version: 'v2' as const,
115
120
  };
116
121
 
117
- console.log('Test 1: Weather query (content parts test)');
122
+ console.log('Test 1: Calculation query');
118
123
 
119
- const userMessage = `
120
- Make a search for the weather in ${location} today, which is ${currentDate}.
121
- Make sure to always refer to me by name, which is ${userName}.
122
- After giving me a thorough summary, tell me a joke about the weather forecast we went over.
123
- `;
124
+ const userMessage = `What is 1123123 + 123123 / 20348?`;
124
125
 
125
126
  conversationHistory.push(new HumanMessage(userMessage));
126
127