@librechat/agents 2.2.5 → 2.2.7

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.
Files changed (38) hide show
  1. package/dist/cjs/events.cjs +3 -0
  2. package/dist/cjs/events.cjs.map +1 -1
  3. package/dist/cjs/graphs/Graph.cjs +40 -2
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/llm/fake.cjs +45 -3
  6. package/dist/cjs/llm/fake.cjs.map +1 -1
  7. package/dist/cjs/messages/format.cjs +161 -80
  8. package/dist/cjs/messages/format.cjs.map +1 -1
  9. package/dist/cjs/tools/ToolNode.cjs +9 -1
  10. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  11. package/dist/esm/events.mjs +3 -0
  12. package/dist/esm/events.mjs.map +1 -1
  13. package/dist/esm/graphs/Graph.mjs +40 -2
  14. package/dist/esm/graphs/Graph.mjs.map +1 -1
  15. package/dist/esm/llm/fake.mjs +45 -3
  16. package/dist/esm/llm/fake.mjs.map +1 -1
  17. package/dist/esm/messages/format.mjs +162 -81
  18. package/dist/esm/messages/format.mjs.map +1 -1
  19. package/dist/esm/tools/ToolNode.mjs +9 -1
  20. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  21. package/dist/types/graphs/Graph.d.ts +3 -1
  22. package/dist/types/llm/fake.d.ts +14 -4
  23. package/dist/types/messages/format.d.ts +6 -15
  24. package/dist/types/tools/ToolNode.d.ts +2 -1
  25. package/dist/types/types/stream.d.ts +35 -1
  26. package/dist/types/types/tools.d.ts +3 -5
  27. package/package.json +6 -6
  28. package/src/events.ts +4 -0
  29. package/src/graphs/Graph.ts +52 -2
  30. package/src/llm/fake.ts +58 -8
  31. package/src/messages/format.ts +180 -111
  32. package/src/messages/formatAgentMessages.test.ts +2 -1
  33. package/src/messages/formatAgentMessages.tools.test.ts +349 -0
  34. package/src/scripts/image.ts +2 -2
  35. package/src/specs/tool-error.test.ts +156 -0
  36. package/src/tools/ToolNode.ts +9 -0
  37. package/src/types/stream.ts +41 -2
  38. package/src/types/tools.ts +3 -6
@@ -0,0 +1,349 @@
1
+ import { HumanMessage, AIMessage, SystemMessage, ToolMessage } from '@langchain/core/messages';
2
+ import type { TPayload } from '@/types';
3
+ import { formatAgentMessages } from './format';
4
+ import { ContentTypes } from '@/common';
5
+
6
+ describe('formatAgentMessages with tools parameter', () => {
7
+ it('should process messages normally when tools is not provided', () => {
8
+ const payload: TPayload = [
9
+ { role: 'user', content: 'Hello' },
10
+ {
11
+ role: 'assistant',
12
+ content: [
13
+ {
14
+ type: ContentTypes.TEXT,
15
+ [ContentTypes.TEXT]: 'Let me check that for you.',
16
+ tool_call_ids: ['123'],
17
+ },
18
+ {
19
+ type: ContentTypes.TOOL_CALL,
20
+ tool_call: {
21
+ id: '123',
22
+ name: 'search',
23
+ args: '{"query":"weather"}',
24
+ output: 'The weather is sunny.',
25
+ },
26
+ },
27
+ ],
28
+ },
29
+ ];
30
+
31
+ const result = formatAgentMessages(payload);
32
+
33
+ expect(result.messages).toHaveLength(3);
34
+ expect(result.messages[0]).toBeInstanceOf(HumanMessage);
35
+ expect(result.messages[1]).toBeInstanceOf(AIMessage);
36
+ expect(result.messages[2]).toBeInstanceOf(ToolMessage);
37
+ expect((result.messages[1] as AIMessage).tool_calls).toHaveLength(1);
38
+ expect((result.messages[2] as ToolMessage).tool_call_id).toBe('123');
39
+ });
40
+
41
+ it('should treat an empty tools set the same as disallowing all tools', () => {
42
+ const payload: TPayload = [
43
+ { role: 'user', content: 'What\'s the weather?' },
44
+ {
45
+ role: 'assistant',
46
+ content: [
47
+ {
48
+ type: ContentTypes.TEXT,
49
+ [ContentTypes.TEXT]: 'Let me check the weather for you.',
50
+ tool_call_ids: ['weather_1'],
51
+ },
52
+ {
53
+ type: ContentTypes.TOOL_CALL,
54
+ tool_call: {
55
+ id: 'weather_1',
56
+ name: 'check_weather',
57
+ args: '{"location":"New York"}',
58
+ output: 'Sunny, 75°F',
59
+ },
60
+ },
61
+ ],
62
+ },
63
+ ];
64
+
65
+ // Provide an empty set of allowed tools
66
+ const allowedTools = new Set<string>();
67
+
68
+ const result = formatAgentMessages(payload, undefined, allowedTools);
69
+
70
+ // Should convert to a single AIMessage with string content
71
+ expect(result.messages).toHaveLength(2);
72
+ expect(result.messages[0]).toBeInstanceOf(HumanMessage);
73
+ expect(result.messages[1]).toBeInstanceOf(AIMessage);
74
+
75
+ // The content should be a string representation of both messages
76
+ expect(typeof result.messages[1].content).toBe('string');
77
+ expect(result.messages[1].content).toEqual('AI: Let me check the weather for you.\nTool: check_weather, Sunny, 75°F');
78
+ });
79
+
80
+ it('should convert tool messages to string when tool is not in the allowed set', () => {
81
+ const payload: TPayload = [
82
+ { role: 'user', content: 'What\'s the weather?' },
83
+ {
84
+ role: 'assistant',
85
+ content: [
86
+ {
87
+ type: ContentTypes.TEXT,
88
+ [ContentTypes.TEXT]: 'Let me check the weather for you.',
89
+ tool_call_ids: ['weather_1'],
90
+ },
91
+ {
92
+ type: ContentTypes.TOOL_CALL,
93
+ tool_call: {
94
+ id: 'weather_1',
95
+ name: 'check_weather',
96
+ args: '{"location":"New York"}',
97
+ output: 'Sunny, 75°F',
98
+ },
99
+ },
100
+ ],
101
+ },
102
+ ];
103
+
104
+ // Provide a set of allowed tools that doesn't include 'check_weather'
105
+ const allowedTools = new Set(['search', 'calculator']);
106
+
107
+ const result = formatAgentMessages(payload, undefined, allowedTools);
108
+
109
+ // Should convert to a single AIMessage with string content
110
+ expect(result.messages).toHaveLength(2);
111
+ expect(result.messages[0]).toBeInstanceOf(HumanMessage);
112
+ expect(result.messages[1]).toBeInstanceOf(AIMessage);
113
+
114
+ // The content should be a string representation of both messages
115
+ expect(typeof result.messages[1].content).toBe('string');
116
+ expect(result.messages[1].content).toEqual('AI: Let me check the weather for you.\nTool: check_weather, Sunny, 75°F');
117
+ });
118
+
119
+ it('should not convert tool messages when tool is in the allowed set', () => {
120
+ const payload: TPayload = [
121
+ { role: 'user', content: 'What\'s the weather?' },
122
+ {
123
+ role: 'assistant',
124
+ content: [
125
+ {
126
+ type: ContentTypes.TEXT,
127
+ [ContentTypes.TEXT]: 'Let me check the weather for you.',
128
+ tool_call_ids: ['weather_1'],
129
+ },
130
+ {
131
+ type: ContentTypes.TOOL_CALL,
132
+ tool_call: {
133
+ id: 'weather_1',
134
+ name: 'check_weather',
135
+ args: '{"location":"New York"}',
136
+ output: 'Sunny, 75°F',
137
+ },
138
+ },
139
+ ],
140
+ },
141
+ ];
142
+
143
+ // Provide a set of allowed tools that includes 'check_weather'
144
+ const allowedTools = new Set(['check_weather', 'search']);
145
+
146
+ const result = formatAgentMessages(payload, undefined, allowedTools);
147
+
148
+ // Should keep the original structure
149
+ expect(result.messages).toHaveLength(3);
150
+ expect(result.messages[0]).toBeInstanceOf(HumanMessage);
151
+ expect(result.messages[1]).toBeInstanceOf(AIMessage);
152
+ expect(result.messages[2]).toBeInstanceOf(ToolMessage);
153
+ });
154
+
155
+ it('should handle multiple tool calls with mixed allowed/disallowed tools', () => {
156
+ const payload: TPayload = [
157
+ { role: 'user', content: 'Tell me about the weather and calculate something' },
158
+ {
159
+ role: 'assistant',
160
+ content: [
161
+ {
162
+ type: ContentTypes.TEXT,
163
+ [ContentTypes.TEXT]: 'Let me check the weather first.',
164
+ tool_call_ids: ['weather_1'],
165
+ },
166
+ {
167
+ type: ContentTypes.TOOL_CALL,
168
+ tool_call: {
169
+ id: 'weather_1',
170
+ name: 'check_weather',
171
+ args: '{"location":"New York"}',
172
+ output: 'Sunny, 75°F',
173
+ },
174
+ },
175
+ {
176
+ type: ContentTypes.TEXT,
177
+ [ContentTypes.TEXT]: 'Now let me calculate something for you.',
178
+ tool_call_ids: ['calc_1'],
179
+ },
180
+ {
181
+ type: ContentTypes.TOOL_CALL,
182
+ tool_call: {
183
+ id: 'calc_1',
184
+ name: 'calculator',
185
+ args: '{"expression":"1+1"}',
186
+ output: '2',
187
+ },
188
+ },
189
+ ],
190
+ },
191
+ ];
192
+
193
+ // Allow calculator but not check_weather
194
+ const allowedTools = new Set(['calculator', 'search']);
195
+
196
+ const result = formatAgentMessages(payload, undefined, allowedTools);
197
+
198
+ // Should convert the entire sequence to a single AIMessage
199
+ expect(result.messages).toHaveLength(2);
200
+ expect(result.messages[0]).toBeInstanceOf(HumanMessage);
201
+ expect(result.messages[1]).toBeInstanceOf(AIMessage);
202
+
203
+ // The content should include all parts
204
+ expect(typeof result.messages[1].content).toBe('string');
205
+ expect(result.messages[1].content).toContain('Let me check the weather first.');
206
+ expect(result.messages[1].content).toContain('Sunny, 75°F');
207
+ expect(result.messages[1].content).toContain('Now let me calculate something for you.');
208
+ expect(result.messages[1].content).toContain('2');
209
+ });
210
+
211
+ it('should update indexTokenCountMap correctly when converting tool messages', () => {
212
+ const payload: TPayload = [
213
+ { role: 'user', content: 'What\'s the weather?' },
214
+ {
215
+ role: 'assistant',
216
+ content: [
217
+ {
218
+ type: ContentTypes.TEXT,
219
+ [ContentTypes.TEXT]: 'Let me check the weather for you.',
220
+ tool_call_ids: ['weather_1'],
221
+ },
222
+ {
223
+ type: ContentTypes.TOOL_CALL,
224
+ tool_call: {
225
+ id: 'weather_1',
226
+ name: 'check_weather',
227
+ args: '{"location":"New York"}',
228
+ output: 'Sunny, 75°F',
229
+ },
230
+ },
231
+ ],
232
+ },
233
+ ];
234
+
235
+ const indexTokenCountMap = {
236
+ 0: 10, // 10 tokens for user message
237
+ 1: 40, // 40 tokens for assistant message with tool call
238
+ };
239
+
240
+ // Provide a set of allowed tools that doesn't include 'check_weather'
241
+ const allowedTools = new Set(['search', 'calculator']);
242
+
243
+ const result = formatAgentMessages(payload, indexTokenCountMap, allowedTools);
244
+
245
+ // Should have 2 messages and 2 entries in the token count map
246
+ expect(result.messages).toHaveLength(2);
247
+ expect(Object.keys(result.indexTokenCountMap || {}).length).toBe(2);
248
+
249
+ // User message token count should be unchanged
250
+ expect(result.indexTokenCountMap?.[0]).toBe(10);
251
+
252
+ // All assistant message tokens should be assigned to the single AIMessage
253
+ expect(result.indexTokenCountMap?.[1]).toBe(40);
254
+ });
255
+
256
+ it('should handle complex sequences with multiple tool calls', () => {
257
+ const payload: TPayload = [
258
+ { role: 'user', content: 'Help me with a complex task' },
259
+ {
260
+ role: 'assistant',
261
+ content: [
262
+ {
263
+ type: ContentTypes.TEXT,
264
+ [ContentTypes.TEXT]: 'I\'ll search for information first.',
265
+ tool_call_ids: ['search_1'],
266
+ },
267
+ {
268
+ type: ContentTypes.TOOL_CALL,
269
+ tool_call: {
270
+ id: 'search_1',
271
+ name: 'search',
272
+ args: '{"query":"complex task"}',
273
+ output: 'Found information about complex tasks.',
274
+ },
275
+ },
276
+ ],
277
+ },
278
+ {
279
+ role: 'assistant',
280
+ content: [
281
+ {
282
+ type: ContentTypes.TEXT,
283
+ [ContentTypes.TEXT]: 'Now I\'ll check the weather.',
284
+ tool_call_ids: ['weather_1'],
285
+ },
286
+ {
287
+ type: ContentTypes.TOOL_CALL,
288
+ tool_call: {
289
+ id: 'weather_1',
290
+ name: 'check_weather',
291
+ args: '{"location":"New York"}',
292
+ output: 'Sunny, 75°F',
293
+ },
294
+ },
295
+ ],
296
+ },
297
+ {
298
+ role: 'assistant',
299
+ content: [
300
+ {
301
+ type: ContentTypes.TEXT,
302
+ [ContentTypes.TEXT]: 'Finally, I\'ll calculate something.',
303
+ tool_call_ids: ['calc_1'],
304
+ },
305
+ {
306
+ type: ContentTypes.TOOL_CALL,
307
+ tool_call: {
308
+ id: 'calc_1',
309
+ name: 'calculator',
310
+ args: '{"expression":"1+1"}',
311
+ output: '2',
312
+ },
313
+ },
314
+ ],
315
+ },
316
+ { role: 'assistant', content: 'Here\'s your answer based on all that information.' },
317
+ ];
318
+
319
+ // Allow search and calculator but not check_weather
320
+ const allowedTools = new Set(['search', 'calculator']);
321
+
322
+ const result = formatAgentMessages(payload, undefined, allowedTools);
323
+
324
+ // Should have the user message, search tool sequence (2 messages),
325
+ // a combined message for weather and calculator (since one has an invalid tool),
326
+ // and final message
327
+ expect(result.messages).toHaveLength(5);
328
+
329
+ // Check the types of messages
330
+ expect(result.messages[0]).toBeInstanceOf(HumanMessage);
331
+ expect(result.messages[1]).toBeInstanceOf(AIMessage); // Search message
332
+ expect(result.messages[2]).toBeInstanceOf(ToolMessage); // Search tool response
333
+ expect(result.messages[3]).toBeInstanceOf(AIMessage); // Converted weather+calculator message
334
+ expect(result.messages[4]).toBeInstanceOf(AIMessage); // Final message
335
+
336
+ // Check that the combined message was converted to a string
337
+ expect(typeof result.messages[3].content).toBe('string');
338
+
339
+ // The format might vary based on the getBufferString implementation
340
+ // but we should check that all the key information is present
341
+ const content = result.messages[3].content as string;
342
+ expect(content).toContain('Now I\'ll check the weather');
343
+ expect(content).toContain('Sunny');
344
+ expect(content).toContain('75');
345
+ expect(content).toContain('Finally');
346
+ expect(content).toContain('calculate');
347
+ expect(content).toContain('2');
348
+ });
349
+ });
@@ -65,8 +65,8 @@ async function testCodeExecution(): Promise<void> {
65
65
  graphConfig: {
66
66
  type: 'standard',
67
67
  llmConfig,
68
- // tools: [fetchRandomImageTool],
69
- tools: [fetchRandomImageURL],
68
+ tools: [fetchRandomImageTool],
69
+ // tools: [fetchRandomImageURL],
70
70
  instructions: 'You are a friendly AI assistant with internet capabilities. Always address the user by their name.',
71
71
  additional_instructions: `The user's name is ${userName} and they are located in ${location}.`,
72
72
  },
@@ -0,0 +1,156 @@
1
+ /* eslint-disable no-console */
2
+ /* eslint-disable @typescript-eslint/no-explicit-any */
3
+ import { z } from 'zod';
4
+ import { config } from 'dotenv';
5
+ config();
6
+ import { tool } from '@langchain/core/tools';
7
+ import { ToolCall } from '@langchain/core/messages/tool';
8
+ import { HumanMessage, BaseMessage } from '@langchain/core/messages';
9
+ import type { RunnableConfig } from '@langchain/core/runnables';
10
+ import type * as t from '@/types';
11
+ import { ChatModelStreamHandler, createContentAggregator } from '@/stream';
12
+ import { ToolEndHandler, ModelEndHandler } from '@/events';
13
+ import { GraphEvents, Providers } from '@/common';
14
+ import { getLLMConfig } from '@/utils/llmConfig';
15
+ import { getArgs } from '@/scripts/args';
16
+ import { StandardGraph } from '@/graphs';
17
+ import { Run } from '@/run';
18
+
19
+ const errorTool = tool(
20
+ async () => {
21
+ throw new Error('this is a test error I threw on purpose');
22
+ },
23
+ {
24
+ name: 'errorTool',
25
+ description: 'A tool that always throws an error',
26
+ schema: z.object({ input: z.string().optional() }),
27
+ }
28
+ );
29
+
30
+ describe('Tool Error Handling Tests', () => {
31
+ jest.setTimeout(30000);
32
+ let run: Run<t.IState>;
33
+ let contentParts: t.MessageContentComplex[];
34
+ let conversationHistory: BaseMessage[];
35
+ let aggregateContent: t.ContentAggregator;
36
+ let handleToolCallErrorSpy: jest.SpyInstance;
37
+
38
+ const config: Partial<RunnableConfig> & { version: 'v1' | 'v2'; run_id?: string; streamMode: string } = {
39
+ configurable: {
40
+ thread_id: 'conversation-num-1',
41
+ },
42
+ streamMode: 'values',
43
+ version: 'v2' as const,
44
+ };
45
+
46
+ beforeEach(async () => {
47
+ conversationHistory = [];
48
+ const { contentParts: parts, aggregateContent: ac } = createContentAggregator();
49
+ aggregateContent = ac;
50
+ contentParts = parts as t.MessageContentComplex[];
51
+ handleToolCallErrorSpy = jest.spyOn(StandardGraph.prototype, 'handleToolCallError');
52
+ });
53
+
54
+ afterEach(() => {
55
+ handleToolCallErrorSpy.mockRestore();
56
+ });
57
+
58
+ const onMessageDeltaSpy = jest.fn();
59
+ const onRunStepSpy = jest.fn();
60
+ const onRunStepCompletedSpy = jest.fn();
61
+
62
+ afterAll(() => {
63
+ onMessageDeltaSpy.mockReset();
64
+ onRunStepSpy.mockReset();
65
+ onRunStepCompletedSpy.mockReset();
66
+ });
67
+
68
+ const setupCustomHandlers = (): Record<string | GraphEvents, t.EventHandler> => ({
69
+ [GraphEvents.TOOL_END]: new ToolEndHandler(),
70
+ [GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(),
71
+ [GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler(),
72
+ [GraphEvents.ON_RUN_STEP_COMPLETED]: {
73
+ handle: (event: GraphEvents.ON_RUN_STEP_COMPLETED, data: t.StreamEventData): void => {
74
+ if ((data.result as t.MessageContentComplex)?.['type'] === 'tool_call') {
75
+ run.Graph?.overrideTestModel(['Looks like there was an error calling the tool.'], 5);
76
+ }
77
+ onRunStepCompletedSpy(event, data);
78
+ aggregateContent({ event, data: data as unknown as { result: t.ToolEndEvent; } });
79
+ }
80
+ },
81
+ [GraphEvents.ON_RUN_STEP]: {
82
+ handle: (event: GraphEvents.ON_RUN_STEP, data: t.StreamEventData, metadata, graph): void => {
83
+ const runStepData = data as t.RunStep;
84
+ onRunStepSpy(event, runStepData, metadata, graph);
85
+ aggregateContent({ event, data: runStepData });
86
+ }
87
+ },
88
+ [GraphEvents.ON_RUN_STEP_DELTA]: {
89
+ handle: (event: GraphEvents.ON_RUN_STEP_DELTA, data: t.StreamEventData): void => {
90
+ aggregateContent({ event, data: data as t.RunStepDeltaEvent });
91
+ }
92
+ },
93
+ [GraphEvents.ON_MESSAGE_DELTA]: {
94
+ handle: (event: GraphEvents.ON_MESSAGE_DELTA, data: t.StreamEventData, metadata, graph): void => {
95
+ onMessageDeltaSpy(event, data, metadata, graph);
96
+ aggregateContent({ event, data: data as t.MessageDeltaEvent });
97
+ }
98
+ },
99
+ });
100
+
101
+ test('should handle tool call errors correctly', async () => {
102
+ const { userName, location } = await getArgs();
103
+ const llmConfig = getLLMConfig(Providers.OPENAI);
104
+ const customHandlers = setupCustomHandlers();
105
+
106
+ // Create the run instance
107
+ run = await Run.create<t.IState>({
108
+ runId: 'test-run-id',
109
+ graphConfig: {
110
+ type: 'standard',
111
+ llmConfig,
112
+ tools: [errorTool],
113
+ instructions: 'You are a helpful AI assistant.',
114
+ additional_instructions: `The user's name is ${userName} and they are located in ${location}.`,
115
+ },
116
+ returnContent: true,
117
+ customHandlers,
118
+ });
119
+
120
+ const toolCalls: ToolCall[] = [
121
+ {
122
+ name: "errorTool",
123
+ args: {
124
+ input: "test input",
125
+ },
126
+ id: "call_test123",
127
+ type: "tool_call",
128
+ }
129
+ ];
130
+
131
+ const firstResponse = 'Let me try calling the tool';
132
+ run.Graph?.overrideTestModel([firstResponse], 5, toolCalls);
133
+
134
+ const userMessage = 'Use the error tool';
135
+ conversationHistory.push(new HumanMessage(userMessage));
136
+
137
+ const inputs = {
138
+ messages: conversationHistory,
139
+ };
140
+
141
+ await run.processStream(inputs, config);
142
+
143
+ // Verify handleToolCallError was called
144
+ expect(handleToolCallErrorSpy).toHaveBeenCalled();
145
+
146
+ // Find the tool call content part
147
+ const toolCallPart = contentParts.find(part =>
148
+ part?.type === 'tool_call'
149
+ ) as t.ToolCallContent | undefined;
150
+
151
+ // Verify the error message in contentParts
152
+ expect(toolCallPart).toBeDefined();
153
+ expect(toolCallPart?.tool_call?.output).toContain('Error processing tool');
154
+ expect(toolCallPart?.tool_call?.output).toContain('this is a test error I threw on purpose');
155
+ });
156
+ });
@@ -14,12 +14,14 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
14
14
  private loadRuntimeTools?: t.ToolRefGenerator;
15
15
  handleToolErrors = true;
16
16
  toolCallStepIds?: Map<string, string>;
17
+ errorHandler?: t.ToolNodeConstructorParams['errorHandler'];
17
18
 
18
19
  constructor({
19
20
  tools,
20
21
  toolMap,
21
22
  name,
22
23
  tags,
24
+ errorHandler,
23
25
  toolCallStepIds,
24
26
  handleToolErrors,
25
27
  loadRuntimeTools,
@@ -30,6 +32,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
30
32
  this.toolCallStepIds = toolCallStepIds;
31
33
  this.handleToolErrors = handleToolErrors ?? this.handleToolErrors;
32
34
  this.loadRuntimeTools = loadRuntimeTools;
35
+ this.errorHandler = errorHandler;
33
36
  }
34
37
 
35
38
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -83,6 +86,12 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
83
86
  if (isGraphInterrupt(e)) {
84
87
  throw e;
85
88
  }
89
+ this.errorHandler?.({
90
+ error: e,
91
+ id: call.id!,
92
+ name: call.name,
93
+ input: call.args,
94
+ }, config?.metadata);
86
95
  return new ToolMessage({
87
96
  content: `Error: ${e.message}\n Please fix your mistakes.`,
88
97
  name: call.name,
@@ -93,6 +93,11 @@ export type MessageCreationDetails = {
93
93
  };
94
94
 
95
95
  export type ToolEndData = { input: string | Record<string, unknown>, output?: ToolMessage };
96
+ export type ToolErrorData = {
97
+ id: string,
98
+ name: string,
99
+ error?: Error,
100
+ } & Pick<ToolEndData, 'input'>;
96
101
  export type ToolEndCallback = (data: ToolEndData, metadata?: Record<string, unknown>) => void;
97
102
 
98
103
  export type ProcessedToolCall = {
@@ -240,8 +245,35 @@ export type BedrockReasoningContentText = {
240
245
  reasoningText: { text?: string; signature?: string; }
241
246
  };
242
247
 
248
+ /**
249
+ * A call to a tool.
250
+ */
251
+ export type ToolCallPart = {
252
+ /** Type ("tool_call") according to Assistants Tool Call Structure */
253
+ type: ContentTypes.TOOL_CALL;
254
+ /** The name of the tool to be called */
255
+ name: string;
256
+ /** The arguments to the tool call */
257
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
258
+ args?: string | Record<string, any>;
259
+
260
+ /** If provided, an identifier associated with the tool call */
261
+ id?: string;
262
+ /** If provided, the output of the tool call */
263
+ output?: string;
264
+ /** Auth URL */
265
+ auth?: string;
266
+ /** Expiration time */
267
+ expires_at?: number;
268
+ };
269
+
270
+ export type ToolCallContent = {
271
+ type: ContentTypes.TOOL_CALL;
272
+ tool_call?: ToolCallPart;
273
+ };
274
+
243
275
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
244
- export type MessageContentComplex = (ThinkingContentText | AgentUpdate | ReasoningContentText | MessageContentText | MessageContentImageUrl | (Record<string, any> & {
276
+ export type MessageContentComplex = (ThinkingContentText | AgentUpdate | ToolCallContent | ReasoningContentText | MessageContentText | MessageContentImageUrl | (Record<string, any> & {
245
277
  type?: 'text' | 'image_url' | 'think' | 'thinking' | string;
246
278
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
247
279
  }) | (Record<string, any> & {
@@ -249,7 +281,14 @@ export type MessageContentComplex = (ThinkingContentText | AgentUpdate | Reasoni
249
281
  })) & {
250
282
  tool_call_ids?: string[];
251
283
  };
252
- // #new
284
+
285
+ export interface TMessage {
286
+ role?: string;
287
+ content?: MessageContentComplex[] | string;
288
+ [key: string]: any;
289
+ }
290
+
291
+ export type TPayload = Array<Partial<TMessage>>;
253
292
 
254
293
  export type CustomChunk = Partial<OpenAITypes.ChatCompletionChunk> & {
255
294
  choices?: Partial<Array<Partial<OpenAITypes.Chat.Completions.ChatCompletionChunk.Choice> & {
@@ -2,7 +2,8 @@
2
2
  import type { RunnableToolLike } from '@langchain/core/runnables';
3
3
  import type { StructuredToolInterface } from '@langchain/core/tools';
4
4
  import type { ToolCall } from '@langchain/core/messages/tool';
5
- import { ContentTypes, EnvVar } from '@/common';
5
+ import type { ToolErrorData } from './stream';
6
+ import { EnvVar } from '@/common';
6
7
 
7
8
  /** Replacement type for `import type { ToolCall } from '@langchain/core/messages/tool'` in order to have stringified args typed */
8
9
  export type CustomToolCall = {
@@ -29,6 +30,7 @@ export type ToolNodeOptions = {
29
30
  handleToolErrors?: boolean;
30
31
  loadRuntimeTools?: ToolRefGenerator;
31
32
  toolCallStepIds?: Map<string, string>;
33
+ errorHandler?: (data: ToolErrorData, metadata?: Record<string, unknown>) => void
32
34
  };
33
35
 
34
36
  export type ToolNodeConstructorParams = ToolRefs & ToolNodeOptions;
@@ -42,11 +44,6 @@ export type ToolEndEvent = {
42
44
  index: number;
43
45
  };
44
46
 
45
- export type ToolCallContent = {
46
- type: ContentTypes.TOOL_CALL;
47
- tool_call: ToolCall;
48
- };
49
-
50
47
  export type CodeEnvFile = {
51
48
  id: string;
52
49
  name: string;