@librechat/agents 3.2.67 → 3.3.0

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 (68) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +1 -1
  2. package/dist/cjs/common/enum.cjs +2 -0
  3. package/dist/cjs/common/enum.cjs.map +1 -1
  4. package/dist/cjs/graphs/Graph.cjs +14 -1
  5. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  6. package/dist/cjs/graphs/MultiAgentGraph.cjs +1 -1
  7. package/dist/cjs/langfuseToolOutputTracing.cjs +4 -0
  8. package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -1
  9. package/dist/cjs/llm/google/index.cjs +2 -0
  10. package/dist/cjs/llm/google/index.cjs.map +1 -1
  11. package/dist/cjs/llm/google/utils/common.cjs +28 -0
  12. package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
  13. package/dist/cjs/llm/openai/index.cjs +1 -1
  14. package/dist/cjs/main.cjs +1 -0
  15. package/dist/cjs/messages/format.cjs +136 -4
  16. package/dist/cjs/messages/format.cjs.map +1 -1
  17. package/dist/cjs/prompts/activityLabel.cjs +101 -0
  18. package/dist/cjs/prompts/activityLabel.cjs.map +1 -0
  19. package/dist/cjs/run.cjs +162 -1
  20. package/dist/cjs/run.cjs.map +1 -1
  21. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1 -1
  22. package/dist/esm/agents/AgentContext.mjs +1 -1
  23. package/dist/esm/common/enum.mjs +2 -0
  24. package/dist/esm/common/enum.mjs.map +1 -1
  25. package/dist/esm/graphs/Graph.mjs +15 -2
  26. package/dist/esm/graphs/Graph.mjs.map +1 -1
  27. package/dist/esm/graphs/MultiAgentGraph.mjs +1 -1
  28. package/dist/esm/langfuseToolOutputTracing.mjs +4 -1
  29. package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
  30. package/dist/esm/llm/google/index.mjs +3 -1
  31. package/dist/esm/llm/google/index.mjs.map +1 -1
  32. package/dist/esm/llm/google/utils/common.mjs +28 -1
  33. package/dist/esm/llm/google/utils/common.mjs.map +1 -1
  34. package/dist/esm/llm/openai/index.mjs +1 -1
  35. package/dist/esm/main.mjs +2 -2
  36. package/dist/esm/messages/format.mjs +136 -5
  37. package/dist/esm/messages/format.mjs.map +1 -1
  38. package/dist/esm/prompts/activityLabel.mjs +100 -0
  39. package/dist/esm/prompts/activityLabel.mjs.map +1 -0
  40. package/dist/esm/run.mjs +163 -2
  41. package/dist/esm/run.mjs.map +1 -1
  42. package/dist/esm/tools/subagent/SubagentExecutor.mjs +1 -1
  43. package/dist/types/common/enum.d.ts +3 -1
  44. package/dist/types/langfuseToolOutputTracing.d.ts +4 -0
  45. package/dist/types/llm/google/utils/common.d.ts +9 -0
  46. package/dist/types/messages/format.d.ts +22 -0
  47. package/dist/types/prompts/activityLabel.d.ts +31 -0
  48. package/dist/types/run.d.ts +14 -0
  49. package/dist/types/types/activityLabel.d.ts +53 -0
  50. package/dist/types/types/index.d.ts +1 -0
  51. package/dist/types/types/stream.d.ts +2 -0
  52. package/package.json +1 -1
  53. package/src/common/enum.ts +2 -0
  54. package/src/graphs/Graph.ts +20 -0
  55. package/src/langfuseToolOutputTracing.ts +4 -1
  56. package/src/llm/google/index.ts +3 -0
  57. package/src/llm/google/utils/common.test.ts +57 -2
  58. package/src/llm/google/utils/common.ts +44 -0
  59. package/src/messages/foldToollessToolBlocks.test.ts +438 -0
  60. package/src/messages/format.ts +233 -5
  61. package/src/prompts/activityLabel.ts +177 -0
  62. package/src/run.ts +298 -2
  63. package/src/specs/activity-label-prompt.test.ts +128 -0
  64. package/src/specs/activity-label-trace-seed.test.ts +47 -0
  65. package/src/specs/bedrock-toolless.live.test.ts +123 -0
  66. package/src/types/activityLabel.ts +55 -0
  67. package/src/types/index.ts +1 -0
  68. package/src/types/stream.ts +2 -0
@@ -0,0 +1,438 @@
1
+ import {
2
+ AIMessage,
3
+ BaseMessage,
4
+ HumanMessage,
5
+ SystemMessage,
6
+ ToolMessage,
7
+ } from '@langchain/core/messages';
8
+ import type { ExtendedMessageContent } from '@/types';
9
+ import { foldToolBlocksForToollessAgent } from './format';
10
+
11
+ /** Concatenated text across a message's content (string or structured array). */
12
+ function getTextContent(msg: {
13
+ content: string | ExtendedMessageContent[];
14
+ }): string {
15
+ if (typeof msg.content === 'string') {
16
+ return msg.content;
17
+ }
18
+ if (Array.isArray(msg.content)) {
19
+ return (msg.content as ExtendedMessageContent[])
20
+ .filter((b) => b.type === 'text')
21
+ .map((b) => String(b.text ?? ''))
22
+ .join('\n');
23
+ }
24
+ return '';
25
+ }
26
+
27
+ /** Any residual tool content that a tool-less agent cannot legally send. */
28
+ function hasResidualToolContent(messages: BaseMessage[]): boolean {
29
+ return messages.some((m) => {
30
+ if (m instanceof ToolMessage) {
31
+ return true;
32
+ }
33
+ const ai = m as AIMessage;
34
+ if (ai.tool_calls != null && ai.tool_calls.length > 0) {
35
+ return true;
36
+ }
37
+ const rawToolCalls = ai.additional_kwargs.tool_calls;
38
+ if (Array.isArray(rawToolCalls) && rawToolCalls.length > 0) {
39
+ return true;
40
+ }
41
+ if (Array.isArray(m.content)) {
42
+ return (m.content as ExtendedMessageContent[]).some(
43
+ (b) =>
44
+ typeof b === 'object' &&
45
+ (b.type === 'tool_use' ||
46
+ b.type === 'tool_call' ||
47
+ b.type === 'tool_result')
48
+ );
49
+ }
50
+ return false;
51
+ });
52
+ }
53
+
54
+ describe('foldToolBlocksForToollessAgent', () => {
55
+ test('returns the same array reference when there is no tool content', () => {
56
+ const messages = [
57
+ new SystemMessage('You are helpful.'),
58
+ new HumanMessage('Hi'),
59
+ new AIMessage('Hello!'),
60
+ ];
61
+
62
+ const result = foldToolBlocksForToollessAgent(messages);
63
+
64
+ expect(result).toBe(messages);
65
+ });
66
+
67
+ test('folds an AI tool call plus its ToolMessage into one HumanMessage', () => {
68
+ const messages = [
69
+ new HumanMessage('Search my files for "roadmap"'),
70
+ new AIMessage({
71
+ content: '',
72
+ tool_calls: [
73
+ {
74
+ id: 'call_1',
75
+ name: 'file_search',
76
+ args: { query: 'roadmap' },
77
+ type: 'tool_call',
78
+ },
79
+ ],
80
+ }),
81
+ new ToolMessage({
82
+ content: 'Found roadmap.md',
83
+ tool_call_id: 'call_1',
84
+ name: 'file_search',
85
+ }),
86
+ ];
87
+
88
+ const result = foldToolBlocksForToollessAgent(messages);
89
+
90
+ expect(hasResidualToolContent(result)).toBe(false);
91
+ // Human prompt kept, AI+Tool collapsed into a single HumanMessage.
92
+ expect(result).toHaveLength(2);
93
+ expect(result[0]).toBeInstanceOf(HumanMessage);
94
+ const folded = getTextContent(result[1]);
95
+ expect(folded).toContain('[Previous tool interaction]');
96
+ expect(folded).toContain('file_search');
97
+ expect(folded).toContain('roadmap');
98
+ expect(folded).toContain('Found roadmap.md');
99
+ });
100
+
101
+ test('folds historical tool content that precedes the last human turn (the reported bug)', () => {
102
+ const messages = [
103
+ new HumanMessage('Search my files for "roadmap"'),
104
+ new AIMessage({
105
+ content: '',
106
+ tool_calls: [
107
+ {
108
+ id: 'call_1',
109
+ name: 'file_search',
110
+ args: { query: 'roadmap' },
111
+ type: 'tool_call',
112
+ },
113
+ ],
114
+ }),
115
+ new ToolMessage({
116
+ content: 'Found roadmap.md',
117
+ tool_call_id: 'call_1',
118
+ name: 'file_search',
119
+ }),
120
+ new AIMessage('Here is what I found in roadmap.md.'),
121
+ new HumanMessage('thanks'),
122
+ ];
123
+
124
+ const result = foldToolBlocksForToollessAgent(messages);
125
+
126
+ expect(hasResidualToolContent(result)).toBe(false);
127
+ // The trailing plain-text turns survive untouched.
128
+ const last = result[result.length - 1];
129
+ expect(last).toBeInstanceOf(HumanMessage);
130
+ expect(getTextContent(last)).toBe('thanks');
131
+ expect(
132
+ result.some((m) => getTextContent(m).includes('Here is what I found'))
133
+ ).toBe(true);
134
+ });
135
+
136
+ test('folds parallel tool calls and all their results together', () => {
137
+ const messages = [
138
+ new HumanMessage('Look up A and B'),
139
+ new AIMessage({
140
+ content: '',
141
+ tool_calls: [
142
+ { id: 'a', name: 'lookup', args: { key: 'A' }, type: 'tool_call' },
143
+ { id: 'b', name: 'lookup', args: { key: 'B' }, type: 'tool_call' },
144
+ ],
145
+ }),
146
+ new ToolMessage({ content: 'A=1', tool_call_id: 'a', name: 'lookup' }),
147
+ new ToolMessage({ content: 'B=2', tool_call_id: 'b', name: 'lookup' }),
148
+ ];
149
+
150
+ const result = foldToolBlocksForToollessAgent(messages);
151
+
152
+ expect(hasResidualToolContent(result)).toBe(false);
153
+ expect(result).toHaveLength(2);
154
+ const folded = getTextContent(result[1]);
155
+ expect(folded).toContain('A=1');
156
+ expect(folded).toContain('B=2');
157
+ });
158
+
159
+ test('detects Anthropic-style tool_use content blocks', () => {
160
+ const messages = [
161
+ new HumanMessage('Search'),
162
+ new AIMessage({
163
+ content: [
164
+ { type: 'text', text: 'Let me search.' },
165
+ {
166
+ type: 'tool_use',
167
+ id: 'call_1',
168
+ name: 'file_search',
169
+ input: { query: 'roadmap' },
170
+ },
171
+ ],
172
+ }),
173
+ new ToolMessage({
174
+ content: 'Found roadmap.md',
175
+ tool_call_id: 'call_1',
176
+ name: 'file_search',
177
+ }),
178
+ ];
179
+
180
+ const result = foldToolBlocksForToollessAgent(messages);
181
+
182
+ expect(hasResidualToolContent(result)).toBe(false);
183
+ const folded = getTextContent(result[result.length - 1]);
184
+ expect(folded).toContain('Let me search.');
185
+ expect(folded).toContain('file_search');
186
+ });
187
+
188
+ test('folds an AI message whose tool call is only in additional_kwargs', () => {
189
+ const messages = [
190
+ new HumanMessage('Search my files'),
191
+ // Parsed `tool_calls` is empty; the call survives only in the raw
192
+ // additional_kwargs. The OpenAI converter still serializes it, so the
193
+ // parent AI message must fold with its ToolMessage — otherwise the fold
194
+ // would leave an orphan assistant(tool_calls) -> user(...) sequence.
195
+ new AIMessage({
196
+ content: '',
197
+ additional_kwargs: {
198
+ tool_calls: [
199
+ {
200
+ id: 'call_1',
201
+ type: 'function',
202
+ function: {
203
+ name: 'file_search',
204
+ arguments: '{"query":"roadmap"}',
205
+ },
206
+ },
207
+ ],
208
+ },
209
+ }),
210
+ new ToolMessage({
211
+ content: 'Found roadmap.md',
212
+ tool_call_id: 'call_1',
213
+ name: 'file_search',
214
+ }),
215
+ ];
216
+
217
+ const result = foldToolBlocksForToollessAgent(messages);
218
+
219
+ expect(hasResidualToolContent(result)).toBe(false);
220
+ expect(result).toHaveLength(2);
221
+ const folded = getTextContent(result[1]);
222
+ expect(folded).toContain('file_search');
223
+ expect(folded).toContain('roadmap');
224
+ expect(folded).toContain('Found roadmap.md');
225
+ });
226
+
227
+ test('folds a standard tool_result content block on a user message', () => {
228
+ const messages = [
229
+ new HumanMessage('Search'),
230
+ new AIMessage({
231
+ content: '',
232
+ tool_calls: [
233
+ {
234
+ id: 'call_1',
235
+ name: 'file_search',
236
+ args: { query: 'roadmap' },
237
+ type: 'tool_call',
238
+ },
239
+ ],
240
+ }),
241
+ // Tool result stored as a content block on a user message (the shape the
242
+ // Anthropic converter produces/accepts) rather than a ToolMessage.
243
+ new HumanMessage({
244
+ content: [
245
+ {
246
+ type: 'tool_result',
247
+ tool_use_id: 'call_1',
248
+ content: 'Found roadmap.md',
249
+ },
250
+ ],
251
+ }),
252
+ new HumanMessage('thanks'),
253
+ ];
254
+
255
+ const result = foldToolBlocksForToollessAgent(messages);
256
+
257
+ expect(hasResidualToolContent(result)).toBe(false);
258
+ expect(result.map(getTextContent).join('\n')).toContain('Found roadmap.md');
259
+ });
260
+
261
+ test('detects v1 standard-content tool_call blocks (no AIMessage.tool_calls)', () => {
262
+ const messages = [
263
+ new HumanMessage('Search'),
264
+ // LangChain v1 standard content: the tool call lives only as a
265
+ // `tool_call` content block; @langchain/aws still serializes it to a
266
+ // Converse toolUse, so a tool-less destination must fold it too.
267
+ new AIMessage({
268
+ content: [
269
+ { type: 'text', text: 'Searching.' },
270
+ {
271
+ type: 'tool_call',
272
+ id: 'call_1',
273
+ name: 'file_search',
274
+ args: { query: 'roadmap' },
275
+ },
276
+ ],
277
+ response_metadata: { output_version: 'v1' },
278
+ }),
279
+ new ToolMessage({
280
+ content: 'Found roadmap.md',
281
+ tool_call_id: 'call_1',
282
+ name: 'file_search',
283
+ }),
284
+ ];
285
+
286
+ const result = foldToolBlocksForToollessAgent(messages);
287
+
288
+ expect(hasResidualToolContent(result)).toBe(false);
289
+ const folded = getTextContent(result[result.length - 1]);
290
+ expect(folded).toContain('file_search');
291
+ expect(folded).toContain('roadmap');
292
+ expect(folded).toContain('Found roadmap.md');
293
+ });
294
+
295
+ test('preserves name/args/output of the nested ToolCallContent shape', () => {
296
+ const messages = [
297
+ new HumanMessage('Search'),
298
+ // Shape produced by convertMessagesToContent / persisted LibreChat history:
299
+ // the call (and its output) are nested under `tool_call`, not top level.
300
+ new AIMessage({
301
+ content: [
302
+ {
303
+ type: 'tool_call',
304
+ tool_call: {
305
+ type: 'tool_call',
306
+ name: 'file_search',
307
+ args: { query: 'roadmap' },
308
+ output: 'Found roadmap.md',
309
+ },
310
+ },
311
+ ],
312
+ }),
313
+ ];
314
+
315
+ const result = foldToolBlocksForToollessAgent(messages);
316
+
317
+ expect(hasResidualToolContent(result)).toBe(false);
318
+ const folded = result.map(getTextContent).join('\n');
319
+ expect(folded).toContain('file_search');
320
+ expect(folded).toContain('roadmap');
321
+ // The embedded tool output is preserved, not dropped.
322
+ expect(folded).toContain('Found roadmap.md');
323
+ });
324
+
325
+ test('folds a split AIMessage(tool_call) + tool_result user message as one turn', () => {
326
+ const messages = [
327
+ new HumanMessage('Search'),
328
+ new AIMessage({
329
+ content: [
330
+ { type: 'text', text: 'Let me search.' },
331
+ {
332
+ type: 'tool_call',
333
+ id: 'c1',
334
+ name: 'file_search',
335
+ args: { query: 'roadmap' },
336
+ },
337
+ ],
338
+ }),
339
+ new HumanMessage({
340
+ content: [
341
+ { type: 'tool_result', tool_use_id: 'c1', content: 'Found roadmap.md' },
342
+ ],
343
+ }),
344
+ new HumanMessage('thanks'),
345
+ ];
346
+
347
+ const result = foldToolBlocksForToollessAgent(messages);
348
+
349
+ expect(hasResidualToolContent(result)).toBe(false);
350
+ // Call + result collapse into ONE folded turn (not split/mislabelled).
351
+ expect(result).toHaveLength(3);
352
+ const folded = getTextContent(result[1]);
353
+ expect(folded).toContain('file_search');
354
+ expect(folded).toContain('Found roadmap.md');
355
+ expect(getTextContent(result[2])).toBe('thanks');
356
+ });
357
+
358
+ test('preserves image blocks nested inside a tool_result content block', () => {
359
+ const messages = [
360
+ new HumanMessage('Chart'),
361
+ new AIMessage({
362
+ content: [{ type: 'tool_call', id: 'c1', name: 'chart', args: {} }],
363
+ }),
364
+ new HumanMessage({
365
+ content: [
366
+ {
367
+ type: 'tool_result',
368
+ tool_use_id: 'c1',
369
+ content: [
370
+ { type: 'text', text: 'chart:' },
371
+ {
372
+ type: 'image_url',
373
+ image_url: { url: 'data:image/png;base64,AAAA' },
374
+ },
375
+ ],
376
+ },
377
+ ],
378
+ }),
379
+ ];
380
+
381
+ const result = foldToolBlocksForToollessAgent(messages);
382
+
383
+ expect(hasResidualToolContent(result)).toBe(false);
384
+ const folded = result[result.length - 1].content;
385
+ expect(Array.isArray(folded)).toBe(true);
386
+ expect(
387
+ (folded as ExtendedMessageContent[]).some((b) => b.type === 'image_url')
388
+ ).toBe(true);
389
+ });
390
+
391
+ test('preserves image blocks in a tool result instead of stringifying them', () => {
392
+ const messages = [
393
+ new HumanMessage('Render a chart'),
394
+ new AIMessage({
395
+ content: '',
396
+ tool_calls: [
397
+ { id: 'c', name: 'chart', args: {}, type: 'tool_call' },
398
+ ],
399
+ }),
400
+ new ToolMessage({
401
+ content: [
402
+ { type: 'text', text: 'chart:' },
403
+ {
404
+ type: 'image_url',
405
+ image_url: { url: 'data:image/png;base64,AAAA' },
406
+ },
407
+ ],
408
+ tool_call_id: 'c',
409
+ name: 'chart',
410
+ }),
411
+ ];
412
+
413
+ const result = foldToolBlocksForToollessAgent(messages);
414
+
415
+ expect(hasResidualToolContent(result)).toBe(false);
416
+ const foldedContent = result[result.length - 1].content;
417
+ expect(Array.isArray(foldedContent)).toBe(true);
418
+ expect(
419
+ (foldedContent as ExtendedMessageContent[]).some(
420
+ (b) => b.type === 'image_url'
421
+ )
422
+ ).toBe(true);
423
+ });
424
+
425
+ test('leaves non-tool conversations untouched', () => {
426
+ const messages = [
427
+ new SystemMessage('sys'),
428
+ new HumanMessage('hi'),
429
+ new AIMessage('hello'),
430
+ new HumanMessage('bye'),
431
+ ];
432
+
433
+ const result = foldToolBlocksForToollessAgent(messages);
434
+
435
+ expect(result).toBe(messages);
436
+ expect(hasResidualToolContent(result)).toBe(false);
437
+ });
438
+ });