@librechat/agents 2.2.1 → 2.2.3

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 (51) hide show
  1. package/dist/cjs/graphs/Graph.cjs +56 -19
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/main.cjs +18 -8
  4. package/dist/cjs/main.cjs.map +1 -1
  5. package/dist/cjs/{messages.cjs → messages/core.cjs} +2 -2
  6. package/dist/cjs/messages/core.cjs.map +1 -0
  7. package/dist/cjs/messages/format.cjs +334 -0
  8. package/dist/cjs/messages/format.cjs.map +1 -0
  9. package/dist/cjs/messages/prune.cjs +124 -0
  10. package/dist/cjs/messages/prune.cjs.map +1 -0
  11. package/dist/cjs/run.cjs +24 -0
  12. package/dist/cjs/run.cjs.map +1 -1
  13. package/dist/cjs/utils/tokens.cjs +64 -0
  14. package/dist/cjs/utils/tokens.cjs.map +1 -0
  15. package/dist/esm/graphs/Graph.mjs +51 -14
  16. package/dist/esm/graphs/Graph.mjs.map +1 -1
  17. package/dist/esm/main.mjs +3 -1
  18. package/dist/esm/main.mjs.map +1 -1
  19. package/dist/esm/{messages.mjs → messages/core.mjs} +2 -2
  20. package/dist/esm/messages/core.mjs.map +1 -0
  21. package/dist/esm/messages/format.mjs +326 -0
  22. package/dist/esm/messages/format.mjs.map +1 -0
  23. package/dist/esm/messages/prune.mjs +122 -0
  24. package/dist/esm/messages/prune.mjs.map +1 -0
  25. package/dist/esm/run.mjs +24 -0
  26. package/dist/esm/run.mjs.map +1 -1
  27. package/dist/esm/utils/tokens.mjs +62 -0
  28. package/dist/esm/utils/tokens.mjs.map +1 -0
  29. package/dist/types/graphs/Graph.d.ts +8 -1
  30. package/dist/types/messages/format.d.ts +120 -0
  31. package/dist/types/messages/index.d.ts +3 -0
  32. package/dist/types/messages/prune.d.ts +16 -0
  33. package/dist/types/types/run.d.ts +4 -0
  34. package/dist/types/utils/tokens.d.ts +2 -0
  35. package/package.json +1 -1
  36. package/src/graphs/Graph.ts +54 -16
  37. package/src/messages/format.ts +460 -0
  38. package/src/messages/formatAgentMessages.test.ts +628 -0
  39. package/src/messages/formatMessage.test.ts +277 -0
  40. package/src/messages/index.ts +3 -0
  41. package/src/messages/prune.ts +167 -0
  42. package/src/messages/shiftIndexTokenCountMap.test.ts +81 -0
  43. package/src/run.ts +26 -0
  44. package/src/scripts/code_exec_simple.ts +21 -8
  45. package/src/specs/prune.test.ts +444 -0
  46. package/src/types/run.ts +5 -0
  47. package/src/utils/tokens.ts +70 -0
  48. package/dist/cjs/messages.cjs.map +0 -1
  49. package/dist/esm/messages.mjs.map +0 -1
  50. /package/dist/types/{messages.d.ts → messages/core.d.ts} +0 -0
  51. /package/src/{messages.ts → messages/core.ts} +0 -0
@@ -0,0 +1,628 @@
1
+ import { HumanMessage, AIMessage, SystemMessage, ToolMessage } from '@langchain/core/messages';
2
+ import { formatAgentMessages } from './format';
3
+ import { ContentTypes } from '@/common';
4
+
5
+ describe('formatAgentMessages', () => {
6
+ it('should format simple user and AI messages', () => {
7
+ const payload = [
8
+ { role: 'user', content: 'Hello' },
9
+ { role: 'assistant', content: 'Hi there!' },
10
+ ];
11
+ const result = formatAgentMessages(payload);
12
+ expect(result.messages).toHaveLength(2);
13
+ expect(result.messages[0]).toBeInstanceOf(HumanMessage);
14
+ expect(result.messages[1]).toBeInstanceOf(AIMessage);
15
+ });
16
+
17
+ it('should handle system messages', () => {
18
+ const payload = [{ role: 'system', content: 'You are a helpful assistant.' }];
19
+ const result = formatAgentMessages(payload);
20
+ expect(result.messages).toHaveLength(1);
21
+ expect(result.messages[0]).toBeInstanceOf(SystemMessage);
22
+ });
23
+
24
+ it('should format messages with content arrays', () => {
25
+ const payload = [
26
+ {
27
+ role: 'user',
28
+ content: [{ type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Hello' }],
29
+ },
30
+ ];
31
+ const result = formatAgentMessages(payload);
32
+ expect(result.messages).toHaveLength(1);
33
+ expect(result.messages[0]).toBeInstanceOf(HumanMessage);
34
+ });
35
+
36
+ it('should handle tool calls and create ToolMessages', () => {
37
+ const payload = [
38
+ {
39
+ role: 'assistant',
40
+ content: [
41
+ {
42
+ type: ContentTypes.TEXT,
43
+ [ContentTypes.TEXT]: 'Let me check that for you.',
44
+ tool_call_ids: ['123'],
45
+ },
46
+ {
47
+ type: ContentTypes.TOOL_CALL,
48
+ tool_call: {
49
+ id: '123',
50
+ name: 'search',
51
+ args: '{"query":"weather"}',
52
+ output: 'The weather is sunny.',
53
+ },
54
+ },
55
+ ],
56
+ },
57
+ ];
58
+ const result = formatAgentMessages(payload);
59
+ expect(result.messages).toHaveLength(2);
60
+ expect(result.messages[0]).toBeInstanceOf(AIMessage);
61
+ expect(result.messages[1]).toBeInstanceOf(ToolMessage);
62
+ expect((result.messages[0] as AIMessage).tool_calls).toHaveLength(1);
63
+ expect((result.messages[1] as ToolMessage).tool_call_id).toBe('123');
64
+ });
65
+
66
+ it('should handle multiple content parts in assistant messages', () => {
67
+ const payload = [
68
+ {
69
+ role: 'assistant',
70
+ content: [
71
+ { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Part 1' },
72
+ { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Part 2' },
73
+ ],
74
+ },
75
+ ];
76
+ const result = formatAgentMessages(payload);
77
+ expect(result.messages).toHaveLength(1);
78
+ expect(result.messages[0]).toBeInstanceOf(AIMessage);
79
+ expect(result.messages[0].content).toHaveLength(2);
80
+ });
81
+
82
+ it('should throw an error for invalid tool call structure', () => {
83
+ const payload = [
84
+ {
85
+ role: 'assistant',
86
+ content: [
87
+ {
88
+ type: ContentTypes.TOOL_CALL,
89
+ tool_call: {
90
+ id: '123',
91
+ name: 'search',
92
+ args: '{"query":"weather"}',
93
+ output: 'The weather is sunny.',
94
+ },
95
+ },
96
+ ],
97
+ },
98
+ ];
99
+ expect(() => formatAgentMessages(payload)).toThrow('Invalid tool call structure');
100
+ });
101
+
102
+ it('should handle tool calls with non-JSON args', () => {
103
+ const payload = [
104
+ {
105
+ role: 'assistant',
106
+ content: [
107
+ { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Checking...', tool_call_ids: ['123'] },
108
+ {
109
+ type: ContentTypes.TOOL_CALL,
110
+ tool_call: {
111
+ id: '123',
112
+ name: 'search',
113
+ args: 'non-json-string',
114
+ output: 'Result',
115
+ },
116
+ },
117
+ ],
118
+ },
119
+ ];
120
+ const result = formatAgentMessages(payload);
121
+ expect(result.messages).toHaveLength(2);
122
+ expect((result.messages[0] as AIMessage).tool_calls?.[0].args).toStrictEqual({ input: 'non-json-string' });
123
+ });
124
+
125
+ it('should handle complex tool calls with multiple steps', () => {
126
+ const payload = [
127
+ {
128
+ role: 'assistant',
129
+ content: [
130
+ {
131
+ type: ContentTypes.TEXT,
132
+ [ContentTypes.TEXT]: 'I\'ll search for that information.',
133
+ tool_call_ids: ['search_1'],
134
+ },
135
+ {
136
+ type: ContentTypes.TOOL_CALL,
137
+ tool_call: {
138
+ id: 'search_1',
139
+ name: 'search',
140
+ args: '{"query":"weather in New York"}',
141
+ output: 'The weather in New York is currently sunny with a temperature of 75°F.',
142
+ },
143
+ },
144
+ {
145
+ type: ContentTypes.TEXT,
146
+ [ContentTypes.TEXT]: 'Now, I\'ll convert the temperature.',
147
+ tool_call_ids: ['convert_1'],
148
+ },
149
+ {
150
+ type: ContentTypes.TOOL_CALL,
151
+ tool_call: {
152
+ id: 'convert_1',
153
+ name: 'convert_temperature',
154
+ args: '{"temperature": 75, "from": "F", "to": "C"}',
155
+ output: '23.89°C',
156
+ },
157
+ },
158
+ { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Here\'s your answer.' },
159
+ ],
160
+ },
161
+ ];
162
+
163
+ const result = formatAgentMessages(payload);
164
+
165
+ expect(result.messages).toHaveLength(5);
166
+ expect(result.messages[0]).toBeInstanceOf(AIMessage);
167
+ expect(result.messages[1]).toBeInstanceOf(ToolMessage);
168
+ expect(result.messages[2]).toBeInstanceOf(AIMessage);
169
+ expect(result.messages[3]).toBeInstanceOf(ToolMessage);
170
+ expect(result.messages[4]).toBeInstanceOf(AIMessage);
171
+
172
+ // Check first AIMessage
173
+ expect(result.messages[0].content).toBe('I\'ll search for that information.');
174
+ expect((result.messages[0] as AIMessage).tool_calls).toHaveLength(1);
175
+ expect((result.messages[0] as AIMessage).tool_calls?.[0]).toEqual({
176
+ id: 'search_1',
177
+ name: 'search',
178
+ args: { query: 'weather in New York' },
179
+ });
180
+
181
+ // Check first ToolMessage
182
+ expect((result.messages[1] as ToolMessage).tool_call_id).toBe('search_1');
183
+ expect(result.messages[1].name).toBe('search');
184
+ expect(result.messages[1].content).toBe(
185
+ 'The weather in New York is currently sunny with a temperature of 75°F.',
186
+ );
187
+
188
+ // Check second AIMessage
189
+ expect(result.messages[2].content).toBe('Now, I\'ll convert the temperature.');
190
+ expect((result.messages[2] as AIMessage).tool_calls).toHaveLength(1);
191
+ expect((result.messages[2] as AIMessage).tool_calls?.[0]).toEqual({
192
+ id: 'convert_1',
193
+ name: 'convert_temperature',
194
+ args: { temperature: 75, from: 'F', to: 'C' },
195
+ });
196
+
197
+ // Check second ToolMessage
198
+ expect((result.messages[3] as ToolMessage).tool_call_id).toBe('convert_1');
199
+ expect(result.messages[3].name).toBe('convert_temperature');
200
+ expect(result.messages[3].content).toBe('23.89°C');
201
+
202
+ // Check final AIMessage
203
+ expect(result.messages[4].content).toStrictEqual([
204
+ { [ContentTypes.TEXT]: 'Here\'s your answer.', type: ContentTypes.TEXT },
205
+ ]);
206
+ });
207
+
208
+ it.skip('should not produce two consecutive assistant messages and format content correctly', () => {
209
+ const payload = [
210
+ { role: 'user', content: 'Hello' },
211
+ {
212
+ role: 'assistant',
213
+ content: [{ type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Hi there!' }],
214
+ },
215
+ {
216
+ role: 'assistant',
217
+ content: [{ type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'How can I help you?' }],
218
+ },
219
+ { role: 'user', content: 'What\'s the weather?' },
220
+ {
221
+ role: 'assistant',
222
+ content: [
223
+ {
224
+ type: ContentTypes.TEXT,
225
+ [ContentTypes.TEXT]: 'Let me check that for you.',
226
+ tool_call_ids: ['weather_1'],
227
+ },
228
+ {
229
+ type: ContentTypes.TOOL_CALL,
230
+ tool_call: {
231
+ id: 'weather_1',
232
+ name: 'check_weather',
233
+ args: '{"location":"New York"}',
234
+ output: 'Sunny, 75°F',
235
+ },
236
+ },
237
+ ],
238
+ },
239
+ {
240
+ role: 'assistant',
241
+ content: [
242
+ { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Here\'s the weather information.' },
243
+ ],
244
+ },
245
+ ];
246
+
247
+ const result = formatAgentMessages(payload);
248
+
249
+ // Check correct message count and types
250
+ expect(result.messages).toHaveLength(6);
251
+ expect(result.messages[0]).toBeInstanceOf(HumanMessage);
252
+ expect(result.messages[1]).toBeInstanceOf(AIMessage);
253
+ expect(result.messages[2]).toBeInstanceOf(HumanMessage);
254
+ expect(result.messages[3]).toBeInstanceOf(AIMessage);
255
+ expect(result.messages[4]).toBeInstanceOf(ToolMessage);
256
+ expect(result.messages[5]).toBeInstanceOf(AIMessage);
257
+
258
+ // Check content of messages
259
+ expect(result.messages[0].content).toStrictEqual([
260
+ { [ContentTypes.TEXT]: 'Hello', type: ContentTypes.TEXT },
261
+ ]);
262
+ expect(result.messages[1].content).toStrictEqual([
263
+ { [ContentTypes.TEXT]: 'Hi there!', type: ContentTypes.TEXT },
264
+ { [ContentTypes.TEXT]: 'How can I help you?', type: ContentTypes.TEXT },
265
+ ]);
266
+ expect(result.messages[2].content).toStrictEqual([
267
+ { [ContentTypes.TEXT]: 'What\'s the weather?', type: ContentTypes.TEXT },
268
+ ]);
269
+ expect(result.messages[3].content).toBe('Let me check that for you.');
270
+ expect(result.messages[4].content).toBe('Sunny, 75°F');
271
+ expect(result.messages[5].content).toStrictEqual([
272
+ { [ContentTypes.TEXT]: 'Here\'s the weather information.', type: ContentTypes.TEXT },
273
+ ]);
274
+
275
+ // Check that there are no consecutive AIMessages
276
+ const messageTypes = result.messages.map((message) => message.constructor);
277
+ for (let i = 0; i < messageTypes.length - 1; i++) {
278
+ expect(messageTypes[i] === AIMessage && messageTypes[i + 1] === AIMessage).toBe(false);
279
+ }
280
+
281
+ // Additional check to ensure the consecutive assistant messages were combined
282
+ expect(result.messages[1].content).toHaveLength(2);
283
+ });
284
+
285
+ it('should skip THINK type content parts', () => {
286
+ const payload = [
287
+ {
288
+ role: 'assistant',
289
+ content: [
290
+ { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Initial response' },
291
+ { type: ContentTypes.THINK, [ContentTypes.THINK]: 'Reasoning about the problem...' },
292
+ { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Final answer' },
293
+ ],
294
+ },
295
+ ];
296
+
297
+ const result = formatAgentMessages(payload);
298
+
299
+ expect(result.messages).toHaveLength(1);
300
+ expect(result.messages[0]).toBeInstanceOf(AIMessage);
301
+ expect(result.messages[0].content).toEqual('Initial response\nFinal answer');
302
+ });
303
+
304
+ it('should join TEXT content as string when THINK content type is present', () => {
305
+ const payload = [
306
+ {
307
+ role: 'assistant',
308
+ content: [
309
+ { type: ContentTypes.THINK, [ContentTypes.THINK]: 'Analyzing the problem...' },
310
+ { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'First part of response' },
311
+ { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Second part of response' },
312
+ { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Final part of response' },
313
+ ],
314
+ },
315
+ ];
316
+
317
+ const result = formatAgentMessages(payload);
318
+
319
+ expect(result.messages).toHaveLength(1);
320
+ expect(result.messages[0]).toBeInstanceOf(AIMessage);
321
+ expect(typeof result.messages[0].content).toBe('string');
322
+ expect(result.messages[0].content).toBe(
323
+ 'First part of response\nSecond part of response\nFinal part of response',
324
+ );
325
+ expect(result.messages[0].content).not.toContain('Analyzing the problem...');
326
+ });
327
+
328
+ it('should exclude ERROR type content parts', () => {
329
+ const payload = [
330
+ {
331
+ role: 'assistant',
332
+ content: [
333
+ { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Hello there' },
334
+ {
335
+ type: ContentTypes.ERROR,
336
+ [ContentTypes.ERROR]:
337
+ 'An error occurred while processing the request: Something went wrong',
338
+ },
339
+ { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Final answer' },
340
+ ],
341
+ },
342
+ ];
343
+
344
+ const result = formatAgentMessages(payload);
345
+
346
+ expect(result.messages).toHaveLength(1);
347
+ expect(result.messages[0]).toBeInstanceOf(AIMessage);
348
+ expect(result.messages[0].content).toEqual([
349
+ { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Hello there' },
350
+ { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Final answer' },
351
+ ]);
352
+
353
+ const hasErrorContent = Array.isArray(result.messages[0].content) && result.messages[0].content.some(
354
+ (item) =>
355
+ item.type === ContentTypes.ERROR || JSON.stringify(item).includes('An error occurred'),
356
+ );
357
+ expect(hasErrorContent).toBe(false);
358
+ });
359
+ it('should handle indexTokenCountMap and return updated map', () => {
360
+ const payload = [
361
+ { role: 'user', content: 'Hello' },
362
+ { role: 'assistant', content: 'Hi there!' },
363
+ ];
364
+
365
+ const indexTokenCountMap = {
366
+ 0: 5, // 5 tokens for "Hello"
367
+ 1: 10, // 10 tokens for "Hi there!"
368
+ };
369
+
370
+ const result = formatAgentMessages(payload, indexTokenCountMap);
371
+
372
+ expect(result.messages).toHaveLength(2);
373
+ expect(result.indexTokenCountMap).toBeDefined();
374
+ expect(result.indexTokenCountMap?.[0]).toBe(5);
375
+ expect(result.indexTokenCountMap?.[1]).toBe(10);
376
+ });
377
+
378
+ it('should handle complex message transformations with indexTokenCountMap', () => {
379
+ const payload = [
380
+ { role: 'user', content: 'What\'s the weather?' },
381
+ {
382
+ role: 'assistant',
383
+ content: [
384
+ {
385
+ type: ContentTypes.TEXT,
386
+ [ContentTypes.TEXT]: 'Let me check that for you.',
387
+ tool_call_ids: ['weather_1'],
388
+ },
389
+ {
390
+ type: ContentTypes.TOOL_CALL,
391
+ tool_call: {
392
+ id: 'weather_1',
393
+ name: 'check_weather',
394
+ args: '{"location":"New York"}',
395
+ output: 'Sunny, 75°F',
396
+ },
397
+ },
398
+ ],
399
+ },
400
+ ];
401
+
402
+ const indexTokenCountMap = {
403
+ 0: 10, // 10 tokens for "What's the weather?"
404
+ 1: 50, // 50 tokens for the assistant message with tool call
405
+ };
406
+
407
+ const result = formatAgentMessages(payload, indexTokenCountMap);
408
+
409
+ // The original message at index 1 should be split into two messages
410
+ expect(result.messages).toHaveLength(3);
411
+ expect(result.indexTokenCountMap).toBeDefined();
412
+ expect(result.indexTokenCountMap?.[0]).toBe(10); // User message stays the same
413
+
414
+ // The assistant message tokens should be distributed across the resulting messages
415
+ const totalAssistantTokens = Object.values(result.indexTokenCountMap || {})
416
+ .reduce((sum, count) => sum + count, 0) - 10; // Subtract user message tokens
417
+
418
+ expect(totalAssistantTokens).toBe(50); // Should match the original token count
419
+ });
420
+
421
+ it('should handle one-to-many message expansion with tool calls', () => {
422
+ // One message with multiple tool calls expands to multiple messages
423
+ const payload = [
424
+ {
425
+ role: 'assistant',
426
+ content: [
427
+ {
428
+ type: ContentTypes.TEXT,
429
+ [ContentTypes.TEXT]: 'First tool call:',
430
+ tool_call_ids: ['tool_1'],
431
+ },
432
+ {
433
+ type: ContentTypes.TOOL_CALL,
434
+ tool_call: {
435
+ id: 'tool_1',
436
+ name: 'search',
437
+ args: '{"query":"test"}',
438
+ output: 'Search result',
439
+ },
440
+ },
441
+ {
442
+ type: ContentTypes.TEXT,
443
+ [ContentTypes.TEXT]: 'Second tool call:',
444
+ tool_call_ids: ['tool_2'],
445
+ },
446
+ {
447
+ type: ContentTypes.TOOL_CALL,
448
+ tool_call: {
449
+ id: 'tool_2',
450
+ name: 'calculate',
451
+ args: '{"expression":"1+1"}',
452
+ output: '2',
453
+ },
454
+ },
455
+ {
456
+ type: ContentTypes.TEXT,
457
+ [ContentTypes.TEXT]: 'Final response',
458
+ },
459
+ ],
460
+ },
461
+ ];
462
+
463
+ const indexTokenCountMap = {
464
+ 0: 100, // 100 tokens for the complex assistant message
465
+ };
466
+
467
+ const result = formatAgentMessages(payload, indexTokenCountMap);
468
+
469
+ // One message expands to 5 messages (2 tool calls + text before, between, and after)
470
+ expect(result.messages).toHaveLength(5);
471
+ expect(result.indexTokenCountMap).toBeDefined();
472
+
473
+ // The sum of all token counts should equal the original
474
+ const totalTokens = Object.values(result.indexTokenCountMap || {})
475
+ .reduce((sum, count) => sum + count, 0);
476
+
477
+ expect(totalTokens).toBe(100);
478
+
479
+ // Check that each resulting message has a token count
480
+ for (let i = 0; i < result.messages.length; i++) {
481
+ expect(result.indexTokenCountMap?.[i]).toBeDefined();
482
+ }
483
+ });
484
+
485
+ it('should handle content filtering that reduces message count', () => {
486
+ // Message with THINK and ERROR parts that get filtered out
487
+ const payload = [
488
+ {
489
+ role: 'assistant',
490
+ content: [
491
+ { type: ContentTypes.THINK, [ContentTypes.THINK]: 'Thinking...' },
492
+ { type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Visible response' },
493
+ { type: ContentTypes.ERROR, [ContentTypes.ERROR]: 'Error occurred' },
494
+ ],
495
+ },
496
+ ];
497
+
498
+ const indexTokenCountMap = {
499
+ 0: 60, // 60 tokens for the message with filtered content
500
+ };
501
+
502
+ const result = formatAgentMessages(payload, indexTokenCountMap);
503
+
504
+ // Only one message should remain after filtering
505
+ expect(result.messages).toHaveLength(1);
506
+ expect(result.indexTokenCountMap).toBeDefined();
507
+
508
+ // All tokens should be assigned to the remaining message
509
+ expect(result.indexTokenCountMap?.[0]).toBe(60);
510
+ });
511
+
512
+ it('should handle empty result after content filtering', () => {
513
+ // Message with only THINK and ERROR parts that all get filtered out
514
+ const payload = [
515
+ {
516
+ role: 'assistant',
517
+ content: [
518
+ { type: ContentTypes.THINK, [ContentTypes.THINK]: 'Thinking...' },
519
+ { type: ContentTypes.ERROR, [ContentTypes.ERROR]: 'Error occurred' },
520
+ { type: ContentTypes.AGENT_UPDATE, update: 'Processing...' },
521
+ ],
522
+ },
523
+ ];
524
+
525
+ const indexTokenCountMap = {
526
+ 0: 40, // 40 tokens for the message with filtered content
527
+ };
528
+
529
+ const result = formatAgentMessages(payload, indexTokenCountMap);
530
+
531
+ // No messages should remain after filtering
532
+ expect(result.messages).toHaveLength(0);
533
+ expect(result.indexTokenCountMap).toBeDefined();
534
+
535
+ // The token count map should be empty since there are no messages
536
+ expect(Object.keys(result.indexTokenCountMap || {})).toHaveLength(0);
537
+ });
538
+
539
+ it('should demonstrate how 2 input messages can become more than 2 output messages', () => {
540
+ // Two input messages where one contains tool calls
541
+ const payload = [
542
+ { role: 'user', content: 'Can you help me with something?' },
543
+ {
544
+ role: 'assistant',
545
+ content: [
546
+ {
547
+ type: ContentTypes.TEXT,
548
+ [ContentTypes.TEXT]: 'I\'ll help you with that.',
549
+ tool_call_ids: ['tool_1'],
550
+ },
551
+ {
552
+ type: ContentTypes.TOOL_CALL,
553
+ tool_call: {
554
+ id: 'tool_1',
555
+ name: 'search',
556
+ args: '{"query":"help topics"}',
557
+ output: 'Found several help topics.',
558
+ },
559
+ },
560
+ ],
561
+ },
562
+ ];
563
+
564
+ const indexTokenCountMap = {
565
+ 0: 15, // 15 tokens for the user message
566
+ 1: 45, // 45 tokens for the assistant message with tool call
567
+ };
568
+
569
+ const result = formatAgentMessages(payload, indexTokenCountMap);
570
+
571
+ // 2 input messages become 3 output messages (user + assistant + tool)
572
+ expect(payload).toHaveLength(2);
573
+ expect(result.messages).toHaveLength(3);
574
+ expect(result.indexTokenCountMap).toBeDefined();
575
+ expect(Object.keys(result.indexTokenCountMap ?? {}).length).toBe(3);
576
+
577
+ // Check message types
578
+ expect(result.messages[0]).toBeInstanceOf(HumanMessage);
579
+ expect(result.messages[1]).toBeInstanceOf(AIMessage);
580
+ expect(result.messages[2]).toBeInstanceOf(ToolMessage);
581
+
582
+ // The sum of all token counts should equal the original total
583
+ const totalTokens = Object.values(result.indexTokenCountMap || {})
584
+ .reduce((sum, count) => sum + count, 0);
585
+
586
+ expect(totalTokens).toBe(60); // 15 + 45
587
+ });
588
+
589
+ it('should demonstrate how messages can be filtered out, reducing count', () => {
590
+ // Two input messages where one gets completely filtered out
591
+ const payload = [
592
+ { role: 'user', content: 'Hello there' },
593
+ {
594
+ role: 'assistant',
595
+ content: [
596
+ { type: ContentTypes.THINK, [ContentTypes.THINK]: 'Thinking about response...' },
597
+ { type: ContentTypes.ERROR, [ContentTypes.ERROR]: 'Error in processing' },
598
+ { type: ContentTypes.AGENT_UPDATE, update: 'Working on it...' },
599
+ ],
600
+ },
601
+ ];
602
+
603
+ const indexTokenCountMap = {
604
+ 0: 10, // 10 tokens for the user message
605
+ 1: 30, // 30 tokens for the assistant message that will be filtered out
606
+ };
607
+
608
+ const result = formatAgentMessages(payload, indexTokenCountMap);
609
+
610
+ // 2 input messages become 1 output message (only the user message remains)
611
+ expect(payload).toHaveLength(2);
612
+ expect(result.messages).toHaveLength(1);
613
+ expect(result.indexTokenCountMap).toBeDefined();
614
+ expect(Object.keys(result.indexTokenCountMap ?? {}).length).toBe(1);
615
+
616
+ // Check message type
617
+ expect(result.messages[0]).toBeInstanceOf(HumanMessage);
618
+
619
+ // Only the user message tokens should remain
620
+ expect(result.indexTokenCountMap?.[0]).toBe(10);
621
+
622
+ // The total tokens should be just the user message tokens
623
+ const totalTokens = Object.values(result.indexTokenCountMap || {})
624
+ .reduce((sum, count) => sum + count, 0);
625
+
626
+ expect(totalTokens).toBe(10);
627
+ });
628
+ });