@librechat/agents 3.1.70 → 3.1.71-dev.1

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 (66) hide show
  1. package/dist/cjs/graphs/Graph.cjs +52 -0
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/llm/invoke.cjs +13 -2
  4. package/dist/cjs/llm/invoke.cjs.map +1 -1
  5. package/dist/cjs/main.cjs +4 -0
  6. package/dist/cjs/main.cjs.map +1 -1
  7. package/dist/cjs/messages/prune.cjs +9 -2
  8. package/dist/cjs/messages/prune.cjs.map +1 -1
  9. package/dist/cjs/run.cjs +4 -0
  10. package/dist/cjs/run.cjs.map +1 -1
  11. package/dist/cjs/tools/BashExecutor.cjs +43 -0
  12. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  13. package/dist/cjs/tools/ToolNode.cjs +482 -45
  14. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  15. package/dist/cjs/tools/toolOutputReferences.cjs +657 -0
  16. package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -0
  17. package/dist/cjs/utils/truncation.cjs +28 -0
  18. package/dist/cjs/utils/truncation.cjs.map +1 -1
  19. package/dist/esm/graphs/Graph.mjs +52 -0
  20. package/dist/esm/graphs/Graph.mjs.map +1 -1
  21. package/dist/esm/llm/invoke.mjs +13 -2
  22. package/dist/esm/llm/invoke.mjs.map +1 -1
  23. package/dist/esm/main.mjs +2 -2
  24. package/dist/esm/messages/prune.mjs +9 -2
  25. package/dist/esm/messages/prune.mjs.map +1 -1
  26. package/dist/esm/run.mjs +4 -0
  27. package/dist/esm/run.mjs.map +1 -1
  28. package/dist/esm/tools/BashExecutor.mjs +42 -1
  29. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  30. package/dist/esm/tools/ToolNode.mjs +482 -45
  31. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  32. package/dist/esm/tools/toolOutputReferences.mjs +649 -0
  33. package/dist/esm/tools/toolOutputReferences.mjs.map +1 -0
  34. package/dist/esm/utils/truncation.mjs +27 -1
  35. package/dist/esm/utils/truncation.mjs.map +1 -1
  36. package/dist/types/graphs/Graph.d.ts +28 -0
  37. package/dist/types/llm/invoke.d.ts +9 -0
  38. package/dist/types/run.d.ts +1 -0
  39. package/dist/types/tools/BashExecutor.d.ts +31 -0
  40. package/dist/types/tools/ToolNode.d.ts +84 -3
  41. package/dist/types/tools/toolOutputReferences.d.ts +236 -0
  42. package/dist/types/types/index.d.ts +1 -0
  43. package/dist/types/types/messages.d.ts +26 -0
  44. package/dist/types/types/run.d.ts +9 -1
  45. package/dist/types/types/tools.d.ts +70 -0
  46. package/dist/types/utils/truncation.d.ts +21 -0
  47. package/package.json +1 -1
  48. package/src/graphs/Graph.ts +55 -0
  49. package/src/llm/invoke.test.ts +442 -0
  50. package/src/llm/invoke.ts +23 -2
  51. package/src/messages/prune.ts +9 -2
  52. package/src/run.ts +4 -0
  53. package/src/specs/prune.test.ts +413 -0
  54. package/src/tools/BashExecutor.ts +45 -0
  55. package/src/tools/ToolNode.ts +631 -55
  56. package/src/tools/__tests__/BashExecutor.test.ts +36 -0
  57. package/src/tools/__tests__/ToolNode.outputReferences.test.ts +1438 -0
  58. package/src/tools/__tests__/annotateMessagesForLLM.test.ts +419 -0
  59. package/src/tools/__tests__/toolOutputReferences.test.ts +415 -0
  60. package/src/tools/toolOutputReferences.ts +813 -0
  61. package/src/types/index.ts +1 -0
  62. package/src/types/messages.ts +27 -0
  63. package/src/types/run.ts +9 -1
  64. package/src/types/tools.ts +71 -0
  65. package/src/utils/__tests__/truncation.test.ts +66 -0
  66. package/src/utils/truncation.ts +30 -0
@@ -0,0 +1,419 @@
1
+ import { AIMessage, HumanMessage, ToolMessage } from '@langchain/core/messages';
2
+ import { describe, it, expect } from '@jest/globals';
3
+ import {
4
+ annotateMessagesForLLM,
5
+ ToolOutputReferenceRegistry,
6
+ TOOL_OUTPUT_REF_KEY,
7
+ TOOL_OUTPUT_UNRESOLVED_KEY,
8
+ } from '../toolOutputReferences';
9
+
10
+ function makeToolMessage(fields: {
11
+ content: ToolMessage['content'];
12
+ name?: string;
13
+ tool_call_id?: string;
14
+ status?: 'success' | 'error';
15
+ additional_kwargs?: Record<string, unknown>;
16
+ }): ToolMessage {
17
+ return new ToolMessage({
18
+ name: fields.name ?? 'echo',
19
+ tool_call_id: fields.tool_call_id ?? 'tc1',
20
+ status: fields.status ?? 'success',
21
+ additional_kwargs: fields.additional_kwargs,
22
+ content: fields.content,
23
+ });
24
+ }
25
+
26
+ describe('annotateMessagesForLLM', () => {
27
+ it('returns the input array reference when registry is undefined', () => {
28
+ const messages = [
29
+ new HumanMessage('hi'),
30
+ makeToolMessage({
31
+ content: 'data',
32
+ additional_kwargs: { _refKey: 'tool0turn0' },
33
+ }),
34
+ ];
35
+ const out = annotateMessagesForLLM(messages, undefined, 'r1');
36
+ expect(out).toBe(messages);
37
+ });
38
+
39
+ it('does not iterate any message when the feature is disabled (registry undefined)', () => {
40
+ /**
41
+ * Hard guarantee: when the host hasn't enabled
42
+ * `RunConfig.toolOutputReferences`, calling
43
+ * `annotateMessagesForLLM` must short-circuit at O(1) without
44
+ * touching a single ToolMessage. We assert by spying on
45
+ * `_getType` — the first per-message call inside the loop — and
46
+ * confirming it was never invoked.
47
+ */
48
+ const messages = [
49
+ makeToolMessage({ content: 'a' }),
50
+ makeToolMessage({ content: 'b' }),
51
+ makeToolMessage({
52
+ content: 'c',
53
+ additional_kwargs: { _refKey: 'tool0turn0' },
54
+ }),
55
+ ];
56
+ const spies = messages.map((m) => jest.spyOn(m, '_getType'));
57
+ const out = annotateMessagesForLLM(messages, undefined, undefined);
58
+ expect(out).toBe(messages);
59
+ for (const spy of spies) {
60
+ expect(spy).not.toHaveBeenCalled();
61
+ spy.mockRestore();
62
+ }
63
+ });
64
+
65
+ it('returns the input array reference when no ToolMessage carries metadata', () => {
66
+ const registry = new ToolOutputReferenceRegistry();
67
+ registry.set('r1', 'tool0turn0', 'stored');
68
+ const messages = [
69
+ new HumanMessage('hi'),
70
+ makeToolMessage({ content: 'data' }),
71
+ new AIMessage('answer'),
72
+ ];
73
+ const out = annotateMessagesForLLM(messages, registry, 'r1');
74
+ expect(out).toBe(messages);
75
+ });
76
+
77
+ it('annotates string content when _refKey is live in the registry', () => {
78
+ const registry = new ToolOutputReferenceRegistry();
79
+ registry.set('r1', 'tool0turn0', 'stored-raw');
80
+ const tm = makeToolMessage({
81
+ content: 'output',
82
+ additional_kwargs: { _refKey: 'tool0turn0' },
83
+ });
84
+ const out = annotateMessagesForLLM([tm], registry, 'r1');
85
+ expect(out[0].content).toBe('[ref: tool0turn0]\noutput');
86
+ expect(tm.content).toBe('output');
87
+ expect(out).not.toBe([tm]);
88
+ expect(out[0]).not.toBe(tm);
89
+ });
90
+
91
+ it('leaves content untouched but strips framework metadata when _refKey is stale', () => {
92
+ /**
93
+ * Stale `_refKey` (not in registry) doesn't trigger annotation,
94
+ * but the message still gets projected so framework keys are
95
+ * removed from `additional_kwargs` before the bytes leave for the
96
+ * provider. This protects against custom or future serializers
97
+ * that transmit `additional_kwargs`.
98
+ */
99
+ const registry = new ToolOutputReferenceRegistry();
100
+ const tm = makeToolMessage({
101
+ content: 'output',
102
+ additional_kwargs: { _refKey: 'tool0turn0', userField: 'preserved' },
103
+ });
104
+ const out = annotateMessagesForLLM([tm], registry, 'r1');
105
+ expect(out[0].content).toBe('output');
106
+ expect(out[0]).not.toBe(tm);
107
+ const projectedKwargs = (out[0] as ToolMessage).additional_kwargs;
108
+ expect(projectedKwargs._refKey).toBeUndefined();
109
+ expect(projectedKwargs.userField).toBe('preserved');
110
+ expect(tm.additional_kwargs._refKey).toBe('tool0turn0');
111
+ });
112
+
113
+ it('always applies _unresolvedRefs even when there is no registry entry', () => {
114
+ const registry = new ToolOutputReferenceRegistry();
115
+ const tm = makeToolMessage({
116
+ content: 'output',
117
+ additional_kwargs: { _unresolvedRefs: ['tool9turn9'] },
118
+ });
119
+ const out = annotateMessagesForLLM([tm], registry, 'r1');
120
+ expect(out[0].content).toBe('output\n[unresolved refs: tool9turn9]');
121
+ });
122
+
123
+ it('injects _ref into JSON-object string content', () => {
124
+ const registry = new ToolOutputReferenceRegistry();
125
+ registry.set('r1', 'tool0turn0', '{"a":1}');
126
+ const tm = makeToolMessage({
127
+ content: '{"a":1,"b":"x"}',
128
+ additional_kwargs: { _refKey: 'tool0turn0' },
129
+ });
130
+ const out = annotateMessagesForLLM([tm], registry, 'r1');
131
+ const parsed = JSON.parse(out[0].content as string);
132
+ expect(parsed[TOOL_OUTPUT_REF_KEY]).toBe('tool0turn0');
133
+ expect(parsed.a).toBe(1);
134
+ expect(parsed.b).toBe('x');
135
+ });
136
+
137
+ it('injects both _ref and _unresolved_refs into JSON-object content', () => {
138
+ /**
139
+ * Combined path: when a ToolMessage carries both a live `_refKey`
140
+ * and unresolved-ref hints, JSON-object content should receive
141
+ * both `_ref` and `_unresolved_refs` fields rather than falling
142
+ * back to the prefix/trailer form. Exercises
143
+ * `annotateToolOutputWithReference`'s collision-detection logic
144
+ * through the projection entry point.
145
+ */
146
+ const registry = new ToolOutputReferenceRegistry();
147
+ registry.set('r1', 'tool0turn0', '{"a":1}');
148
+ const tm = makeToolMessage({
149
+ content: '{"a":1,"b":"x"}',
150
+ additional_kwargs: {
151
+ _refKey: 'tool0turn0',
152
+ _unresolvedRefs: ['tool9turn9'],
153
+ },
154
+ });
155
+ const out = annotateMessagesForLLM([tm], registry, 'r1');
156
+ const parsed = JSON.parse(out[0].content as string);
157
+ expect(parsed[TOOL_OUTPUT_REF_KEY]).toBe('tool0turn0');
158
+ expect(parsed[TOOL_OUTPUT_UNRESOLVED_KEY]).toEqual(['tool9turn9']);
159
+ expect(parsed.a).toBe(1);
160
+ expect(parsed.b).toBe('x');
161
+ });
162
+
163
+ it('uses [ref: …] prefix for non-JSON string content', () => {
164
+ const registry = new ToolOutputReferenceRegistry();
165
+ registry.set('r1', 'tool0turn0', 'raw');
166
+ const tm = makeToolMessage({
167
+ content: 'plain output',
168
+ additional_kwargs: { _refKey: 'tool0turn0' },
169
+ });
170
+ const out = annotateMessagesForLLM([tm], registry, 'r1');
171
+ expect(out[0].content).toBe('[ref: tool0turn0]\nplain output');
172
+ });
173
+
174
+ it('prepends an unresolved-refs warning text block to multi-part content', () => {
175
+ const registry = new ToolOutputReferenceRegistry();
176
+ const tm = makeToolMessage({
177
+ content: [
178
+ { type: 'text', text: 'data' },
179
+ { type: 'image_url', image_url: { url: 'data:...' } },
180
+ ] as unknown as ToolMessage['content'],
181
+ additional_kwargs: { _unresolvedRefs: ['tool9turn9'] },
182
+ });
183
+ const out = annotateMessagesForLLM([tm], registry, 'r1');
184
+ const blocks = out[0].content as Array<{ type: string; text?: string }>;
185
+ expect(blocks).toHaveLength(3);
186
+ expect(blocks[0].type).toBe('text');
187
+ expect(blocks[0].text).toBe('[unresolved refs: tool9turn9]');
188
+ expect(blocks[1].type).toBe('text');
189
+ expect(blocks[1].text).toBe('data');
190
+ expect(blocks[2].type).toBe('image_url');
191
+ });
192
+
193
+ it('does not mutate the original ToolMessage instance or its content', () => {
194
+ const registry = new ToolOutputReferenceRegistry();
195
+ registry.set('r1', 'tool0turn0', 'raw');
196
+ const tm = makeToolMessage({
197
+ content: 'output',
198
+ additional_kwargs: { _refKey: 'tool0turn0' },
199
+ });
200
+ const originalContent = tm.content;
201
+ const originalKwargs = { ...tm.additional_kwargs };
202
+ annotateMessagesForLLM([tm], registry, 'r1');
203
+ expect(tm.content).toBe(originalContent);
204
+ expect(tm.additional_kwargs).toEqual(originalKwargs);
205
+ });
206
+
207
+ it('strips framework ref metadata from the projected additional_kwargs but preserves other fields', () => {
208
+ /**
209
+ * Defensive: even though LangChain's standard provider serializers
210
+ * do not transmit `additional_kwargs`, a custom adapter or future
211
+ * LangChain change could. Strip our three framework-owned keys on
212
+ * the projection so the metadata never reaches the wire under any
213
+ * serializer behavior. Non-framework fields stay put.
214
+ */
215
+ const registry = new ToolOutputReferenceRegistry();
216
+ registry.set('r1', 'tool0turn0', 'raw');
217
+ const tm = makeToolMessage({
218
+ content: 'output',
219
+ additional_kwargs: {
220
+ _refKey: 'tool0turn0',
221
+ _refScope: 'r1',
222
+ _unresolvedRefs: ['tool9turn9'],
223
+ someOtherField: 'preserved',
224
+ },
225
+ });
226
+ const out = annotateMessagesForLLM([tm], registry, 'r1');
227
+ const projected = out[0] as ToolMessage;
228
+ expect(projected.additional_kwargs._refKey).toBeUndefined();
229
+ expect(projected.additional_kwargs._refScope).toBeUndefined();
230
+ expect(projected.additional_kwargs._unresolvedRefs).toBeUndefined();
231
+ expect(projected.additional_kwargs.someOtherField).toBe('preserved');
232
+ expect(tm.additional_kwargs._refKey).toBe('tool0turn0');
233
+ expect(tm.additional_kwargs._refScope).toBe('r1');
234
+ });
235
+
236
+ it('leaves additional_kwargs empty when stripping removed every framework key', () => {
237
+ /**
238
+ * `stripFrameworkRefMetadata` returns `undefined` when no non-
239
+ * framework keys remain, and the LangChain `ToolMessage`
240
+ * constructor normalizes that to `{}` — so the projected message
241
+ * exposes an empty object, not the original kwargs.
242
+ */
243
+ const registry = new ToolOutputReferenceRegistry();
244
+ registry.set('r1', 'tool0turn0', 'raw');
245
+ const tm = makeToolMessage({
246
+ content: 'output',
247
+ additional_kwargs: {
248
+ _refKey: 'tool0turn0',
249
+ _refScope: 'r1',
250
+ },
251
+ });
252
+ const out = annotateMessagesForLLM([tm], registry, 'r1');
253
+ const projected = out[0] as ToolMessage;
254
+ expect(projected.additional_kwargs).toEqual({});
255
+ });
256
+
257
+ it('passes through non-ToolMessages unchanged in the projected array', () => {
258
+ const registry = new ToolOutputReferenceRegistry();
259
+ registry.set('r1', 'tool0turn0', 'raw');
260
+ const human = new HumanMessage('hi');
261
+ const ai = new AIMessage('answer');
262
+ const tm = makeToolMessage({
263
+ content: 'output',
264
+ additional_kwargs: { _refKey: 'tool0turn0' },
265
+ });
266
+ const out = annotateMessagesForLLM([human, ai, tm], registry, 'r1');
267
+ expect(out[0]).toBe(human);
268
+ expect(out[1]).toBe(ai);
269
+ expect(out[2]).not.toBe(tm);
270
+ });
271
+
272
+ it('projects (to strip metadata) but does not annotate when only stale _refKey is present', () => {
273
+ const registry = new ToolOutputReferenceRegistry();
274
+ registry.set('r1', 'tool1turn0', 'somethingelse');
275
+ const tm = makeToolMessage({
276
+ content: 'output',
277
+ additional_kwargs: { _refKey: 'tool0turn0' },
278
+ });
279
+ const messages = [tm];
280
+ const out = annotateMessagesForLLM(messages, registry, 'r1');
281
+ expect(out).not.toBe(messages);
282
+ expect(out[0].content).toBe('output');
283
+ expect((out[0] as ToolMessage).additional_kwargs._refKey).toBeUndefined();
284
+ });
285
+
286
+ it('returns the input array reference-equal when no ToolMessage carries any framework metadata', () => {
287
+ const registry = new ToolOutputReferenceRegistry();
288
+ registry.set('r1', 'tool0turn0', 'stored');
289
+ const messages = [
290
+ new HumanMessage('hi'),
291
+ makeToolMessage({
292
+ content: 'output',
293
+ additional_kwargs: { unrelated: 'value' },
294
+ }),
295
+ new AIMessage('answer'),
296
+ ];
297
+ const out = annotateMessagesForLLM(messages, registry, 'r1');
298
+ expect(out).toBe(messages);
299
+ });
300
+
301
+ it('annotates only the live ref when both ref and unresolved are present', () => {
302
+ const registry = new ToolOutputReferenceRegistry();
303
+ registry.set('r1', 'tool0turn0', 'raw');
304
+ const tm = makeToolMessage({
305
+ content: 'output',
306
+ additional_kwargs: {
307
+ _refKey: 'tool0turn0',
308
+ _unresolvedRefs: ['tool9turn9'],
309
+ },
310
+ });
311
+ const out = annotateMessagesForLLM([tm], registry, 'r1');
312
+ expect(out[0].content).toBe(
313
+ '[ref: tool0turn0]\noutput\n[unresolved refs: tool9turn9]'
314
+ );
315
+ });
316
+
317
+ it('uses _refScope for the registry lookup when present, ignoring runId', () => {
318
+ /**
319
+ * Anonymous ToolNode batches register under a synthetic scope
320
+ * (`\0anon-<n>`) that `config.configurable.run_id` cannot recover.
321
+ * The transform must follow the message-stamped `_refScope`
322
+ * instead of the config-derived runId.
323
+ */
324
+ const registry = new ToolOutputReferenceRegistry();
325
+ const anonScope = '\0anon-3';
326
+ registry.set(anonScope, 'tool0turn0', 'raw');
327
+
328
+ const tm = makeToolMessage({
329
+ content: 'output',
330
+ additional_kwargs: {
331
+ _refKey: 'tool0turn0',
332
+ _refScope: anonScope,
333
+ },
334
+ });
335
+ const out = annotateMessagesForLLM([tm], registry, undefined);
336
+ expect(out[0].content).toBe('[ref: tool0turn0]\noutput');
337
+ });
338
+
339
+ it('falls back to runId when _refScope is absent (legacy / pre-scope messages)', () => {
340
+ const registry = new ToolOutputReferenceRegistry();
341
+ registry.set('r1', 'tool0turn0', 'raw');
342
+ const tm = makeToolMessage({
343
+ content: 'output',
344
+ additional_kwargs: { _refKey: 'tool0turn0' },
345
+ });
346
+ const out = annotateMessagesForLLM([tm], registry, 'r1');
347
+ expect(out[0].content).toBe('[ref: tool0turn0]\noutput');
348
+ });
349
+
350
+ it('coerces a non-array _unresolvedRefs to empty without throwing', () => {
351
+ /**
352
+ * Defensive against malformed hydrated messages:
353
+ * `additional_kwargs._unresolvedRefs` is untyped at the LangChain
354
+ * layer, so a persisted message could carry a string/object/null
355
+ * by mistake. The transform must not crash the run on
356
+ * `.length` / `.join` — coerce to an empty list, strip the
357
+ * malformed key, and proceed.
358
+ */
359
+ const registry = new ToolOutputReferenceRegistry();
360
+ const tm = makeToolMessage({
361
+ content: 'output',
362
+ additional_kwargs: {
363
+ _unresolvedRefs: 'tool9turn9' as unknown as string[],
364
+ },
365
+ });
366
+ const out = annotateMessagesForLLM([tm], registry, 'r1');
367
+ expect(out[0].content).toBe('output');
368
+ expect(
369
+ (out[0] as ToolMessage).additional_kwargs._unresolvedRefs
370
+ ).toBeUndefined();
371
+ });
372
+
373
+ it('filters non-string entries out of _unresolvedRefs', () => {
374
+ const registry = new ToolOutputReferenceRegistry();
375
+ const tm = makeToolMessage({
376
+ content: 'output',
377
+ additional_kwargs: {
378
+ _unresolvedRefs: [
379
+ 'tool9turn9',
380
+ 42,
381
+ null,
382
+ { not: 'a string' },
383
+ 'tool8turn8',
384
+ ] as unknown as string[],
385
+ },
386
+ });
387
+ const out = annotateMessagesForLLM([tm], registry, 'r1');
388
+ expect(out[0].content).toBe(
389
+ 'output\n[unresolved refs: tool9turn9, tool8turn8]'
390
+ );
391
+ });
392
+
393
+ it('ignores a non-string _refKey rather than poisoning the registry lookup', () => {
394
+ const registry = new ToolOutputReferenceRegistry();
395
+ registry.set('r1', 'tool0turn0', 'raw');
396
+ const tm = makeToolMessage({
397
+ content: 'output',
398
+ additional_kwargs: {
399
+ _refKey: { malformed: true } as unknown as string,
400
+ },
401
+ });
402
+ const out = annotateMessagesForLLM([tm], registry, 'r1');
403
+ expect(out[0].content).toBe('output');
404
+ expect((out[0] as ToolMessage).additional_kwargs._refKey).toBeUndefined();
405
+ });
406
+
407
+ it('treats stale _refKey but live unresolved as unresolved-only', () => {
408
+ const registry = new ToolOutputReferenceRegistry();
409
+ const tm = makeToolMessage({
410
+ content: 'output',
411
+ additional_kwargs: {
412
+ _refKey: 'tool0turn0',
413
+ _unresolvedRefs: ['tool9turn9'],
414
+ },
415
+ });
416
+ const out = annotateMessagesForLLM([tm], registry, 'r1');
417
+ expect(out[0].content).toBe('output\n[unresolved refs: tool9turn9]');
418
+ });
419
+ });