@librechat/agents 3.1.80-dev.2 → 3.1.80

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 (42) hide show
  1. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +27 -7
  2. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  3. package/dist/cjs/llm/vertexai/index.cjs +52 -0
  4. package/dist/cjs/llm/vertexai/index.cjs.map +1 -1
  5. package/dist/cjs/main.cjs +1 -2
  6. package/dist/cjs/main.cjs.map +1 -1
  7. package/dist/cjs/tools/BashExecutor.cjs +20 -78
  8. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  9. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +5 -1
  10. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
  11. package/dist/cjs/tools/CodeExecutor.cjs +26 -106
  12. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  13. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +12 -31
  14. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  15. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +27 -8
  16. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  17. package/dist/esm/llm/vertexai/index.mjs +53 -2
  18. package/dist/esm/llm/vertexai/index.mjs.map +1 -1
  19. package/dist/esm/main.mjs +1 -1
  20. package/dist/esm/tools/BashExecutor.mjs +20 -78
  21. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  22. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +6 -2
  23. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
  24. package/dist/esm/tools/CodeExecutor.mjs +26 -105
  25. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  26. package/dist/esm/tools/ProgrammaticToolCalling.mjs +12 -31
  27. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  28. package/dist/types/llm/anthropic/utils/message_inputs.d.ts +15 -3
  29. package/dist/types/llm/vertexai/index.d.ts +29 -0
  30. package/dist/types/tools/CodeExecutor.d.ts +1 -7
  31. package/dist/types/tools/ProgrammaticToolCalling.d.ts +5 -0
  32. package/package.json +10 -5
  33. package/src/llm/anthropic/utils/message_inputs.ts +58 -7
  34. package/src/llm/anthropic/utils/tool-id-normalization.test.ts +178 -0
  35. package/src/llm/vertexai/index.ts +69 -2
  36. package/src/llm/vertexai/llm.spec.ts +18 -0
  37. package/src/llm/vertexai/repairUsageMetadata.test.ts +54 -0
  38. package/src/tools/BashExecutor.ts +24 -104
  39. package/src/tools/BashProgrammaticToolCalling.ts +7 -2
  40. package/src/tools/CodeExecutor.ts +30 -133
  41. package/src/tools/ProgrammaticToolCalling.ts +14 -49
  42. package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +32 -131
@@ -8,48 +8,12 @@ import { EnvVar, Constants } from '@/common';
8
8
 
9
9
  config();
10
10
 
11
- export const imageExtRegex = /\.(jpg|jpeg|png|gif|webp)$/i;
12
11
  export const getCodeBaseURL = (): string =>
13
12
  getEnvironmentVariable(EnvVar.CODE_BASEURL) ??
14
13
  Constants.OFFICIAL_CODE_BASEURL;
15
14
 
16
- const imageMessage = 'Image is already displayed to the user';
17
- const otherMessage = 'File is already downloaded by the user';
18
- const inheritedFileMessage =
19
- 'Available as an input — already known to the user';
20
- const accessMessage =
21
- 'Note: Files from previous executions are automatically available and can be modified.';
22
- const emptyOutputMessage =
15
+ export const emptyOutputMessage =
23
16
  'stdout: Empty. Ensure you\'re writing output explicitly.\n';
24
- const inheritedFilesHeader =
25
- 'Available files (inputs, not generated by this execution):';
26
- const generatedFilesHeader = 'Generated files:';
27
- const inheritedNote =
28
- 'Note: Files in "Available files" are inputs the user (or a skill) already provided to the sandbox. They were not produced by this execution and you should not present them as new outputs in your response.';
29
-
30
- /**
31
- * Renders one section of the post-execution file listing. Used by the
32
- * code/bash tool formatters to keep generated outputs and inherited
33
- * inputs visually separated. See BashExecutor for full docs.
34
- */
35
- export function renderFileSection(
36
- header: string,
37
- files: t.FileRefs,
38
- defaultMessage: string
39
- ): string {
40
- if (files.length === 0) return '';
41
- let out = `${header}\n`;
42
- for (let i = 0; i < files.length; i++) {
43
- const file = files[i];
44
- const isImage = imageExtRegex.test(file.name);
45
- out += `- /mnt/data/${file.name} | ${isImage ? imageMessage : defaultMessage}`;
46
- if (i < files.length - 1) {
47
- out += files.length <= 3 ? ', ' : ',\n';
48
- }
49
- }
50
- out += '\n';
51
- return out;
52
- }
53
17
 
54
18
  const SUPPORTED_LANGUAGES = [
55
19
  'py',
@@ -150,65 +114,24 @@ function createCodeExecutionTool(
150
114
  ...params,
151
115
  };
152
116
 
153
- /**
154
- * File injection priority:
155
- * 1. Use _injected_files from ToolNode (avoids /files endpoint race condition)
156
- * 2. Fall back to fetching from /files endpoint if session_id
157
- * provided but no injected files. The /files lookup still uses the
158
- * same id value codeapi stores output files under the exec id as
159
- * their storage prefix, so the two values coincide here.
160
- */
117
+ /* File injection: `_injected_files` from ToolNode (set when host
118
+ * primes a CodeSessionContext) or `params.files` from tool
119
+ * factory (set by hosts that pre-resolve at construction time).
120
+ * The legacy `/files/<session_id>` HTTP fallback was removed
121
+ * codeapi's `sessionAuth` middleware now requires kind/id query
122
+ * params the tool can't supply at this point, so the fetch 400'd
123
+ * silently and the catch swallowed the failure. */
161
124
  if (_injected_files && _injected_files.length > 0) {
162
125
  postData.files = _injected_files;
163
- } else if (session_id != null && session_id.length > 0) {
164
- /** Fallback: fetch from /files endpoint (may have race condition issues) */
165
- try {
166
- const filesEndpoint = `${baseEndpoint}/files/${session_id}?detail=full`;
167
- const fetchOptions: RequestInit = {
168
- method: 'GET',
169
- headers: {
170
- 'User-Agent': 'LibreChat/1.0',
171
- },
172
- };
173
-
174
- if (process.env.PROXY != null && process.env.PROXY !== '') {
175
- fetchOptions.agent = new HttpsProxyAgent(process.env.PROXY);
176
- }
177
-
178
- const response = await fetch(filesEndpoint, fetchOptions);
179
- if (!response.ok) {
180
- throw new Error(
181
- `Failed to fetch files for session: ${response.status}`
182
- );
183
- }
184
-
185
- const files = await response.json();
186
- if (Array.isArray(files) && files.length > 0) {
187
- const fileReferences: t.CodeEnvFile[] = files.map((file) => {
188
- const nameParts = file.name.split('/');
189
- const id = nameParts.length > 1 ? nameParts[1].split('.')[0] : '';
190
-
191
- return {
192
- storage_session_id: session_id,
193
- /* `/files` fallback returns code-output files belonging
194
- * to the user; tag them user-private. */
195
- kind: 'user' as const,
196
- id,
197
- /* `resource_id` is informational for `kind: 'user'`
198
- * (codeapi derives sessionKey from auth context); use
199
- * the same value as `id` since the `/files` fallback
200
- * doesn't carry separate provenance. */
201
- resource_id: id,
202
- name: file.metadata['original-filename'],
203
- };
204
- });
205
-
206
- postData.files = fileReferences;
207
- }
208
- } catch {
209
- // eslint-disable-next-line no-console
210
- console.warn(`Failed to fetch files for session: ${session_id}`);
211
- }
126
+ } else if (
127
+ session_id != null &&
128
+ session_id.length > 0 &&
129
+ !Array.isArray(postData.files)
130
+ ) {
131
+ // eslint-disable-next-line no-console
132
+ console.debug(
133
+ `[CodeExecutor] No injected files for session_id=${session_id} — exec will run without input files`
134
+ );
212
135
  }
213
136
 
214
137
  try {
@@ -230,6 +153,13 @@ function createCodeExecutionTool(
230
153
  }
231
154
 
232
155
  const result: t.ExecuteResult = await response.json();
156
+ /* Output is stdout/stderr only — file listings were removed
157
+ * because the LLM-facing summary (split inherited/generated
158
+ * with prescriptive notes) caused more confusion than help,
159
+ * especially for bash where models naturally explore
160
+ * `/mnt/data/` themselves. The artifact still carries every
161
+ * file so the host's session map stays in sync; the LLM
162
+ * doesn't see them in the tool result text. */
233
163
  let formattedOutput = '';
234
164
  if (result.stdout) {
235
165
  formattedOutput += `stdout:\n${result.stdout}\n`;
@@ -237,48 +167,15 @@ function createCodeExecutionTool(
237
167
  formattedOutput += emptyOutputMessage;
238
168
  }
239
169
  if (result.stderr) formattedOutput += `stderr:\n${result.stderr}\n`;
240
- if (result.files && result.files.length > 0) {
241
- /* See BashExecutor for the rationale: split inherited (read-only
242
- * passthrough) inputs from real generated outputs so the LLM
243
- * doesn't conflate skill files with newly-produced artifacts. */
244
- const inheritedFiles = result.files.filter(
245
- (f) => f.inherited === true
246
- );
247
- const generatedFiles = result.files.filter(
248
- (f) => f.inherited !== true
249
- );
250
-
251
- formattedOutput += renderFileSection(
252
- generatedFilesHeader,
253
- generatedFiles,
254
- otherMessage
255
- );
256
- formattedOutput += renderFileSection(
257
- inheritedFilesHeader,
258
- inheritedFiles,
259
- inheritedFileMessage
260
- );
261
-
262
- if (generatedFiles.length > 0) {
263
- formattedOutput += `\n\n${accessMessage}`;
264
- }
265
- if (inheritedFiles.length > 0) {
266
- formattedOutput += `\n\n${inheritedNote}`;
267
- }
268
- return [
269
- formattedOutput.trim(),
270
- {
271
- session_id: result.session_id,
272
- files: result.files,
273
- } satisfies t.CodeExecutionArtifact,
274
- ];
275
- }
276
170
 
171
+ const hasFiles = result.files != null && result.files.length > 0;
277
172
  return [
278
173
  formattedOutput.trim(),
279
- {
280
- session_id: result.session_id,
281
- } satisfies t.CodeExecutionArtifact,
174
+ (hasFiles
175
+ ? { session_id: result.session_id, files: result.files }
176
+ : {
177
+ session_id: result.session_id,
178
+ }) satisfies t.CodeExecutionArtifact,
282
179
  ];
283
180
  } catch (error) {
284
181
  throw new Error(
@@ -5,28 +5,11 @@ import { HttpsProxyAgent } from 'https-proxy-agent';
5
5
  import { tool, DynamicStructuredTool } from '@langchain/core/tools';
6
6
  import type { ToolCall } from '@langchain/core/messages/tool';
7
7
  import type * as t from '@/types';
8
- import { getCodeBaseURL, renderFileSection } from './CodeExecutor';
8
+ import { emptyOutputMessage, getCodeBaseURL } from './CodeExecutor';
9
9
  import { Constants } from '@/common';
10
10
 
11
11
  config();
12
12
 
13
- // ============================================================================
14
- // Constants
15
- // ============================================================================
16
-
17
- const otherMessage = 'File is already downloaded by the user';
18
- const inheritedFileMessage =
19
- 'Available as an input — already known to the user';
20
- const inheritedFilesHeader =
21
- 'Available files (inputs, not generated by this execution):';
22
- const generatedFilesHeader = 'Generated files:';
23
- const inheritedNote =
24
- 'Note: Files in "Available files" are inputs the user (or a skill) already provided to the sandbox. They were not produced by this execution and you should not present them as new outputs in your response.';
25
- const accessMessage =
26
- 'Note: Files from previous executions are automatically available and can be modified.';
27
- const emptyOutputMessage =
28
- 'stdout: Empty. Ensure you\'re writing output explicitly.\n';
29
-
30
13
  /** Default max round-trips to prevent infinite loops */
31
14
  const DEFAULT_MAX_ROUND_TRIPS = 20;
32
15
 
@@ -545,6 +528,11 @@ export async function executeTools(
545
528
 
546
529
  /**
547
530
  * Formats the completed response for the agent.
531
+ *
532
+ * Output is stdout/stderr only — see `CodeExecutor.ts`. The
533
+ * artifact still carries every file so the host's session map
534
+ * stays in sync; the LLM doesn't see them in the tool result text.
535
+ *
548
536
  * @param response - The completed API response
549
537
  * @returns Tuple of [formatted string, artifact]
550
538
  */
@@ -563,32 +551,6 @@ export function formatCompletedResponse(
563
551
  formatted += `stderr:\n${response.stderr}\n`;
564
552
  }
565
553
 
566
- if (response.files && response.files.length > 0) {
567
- /* See BashExecutor for the rationale: split inherited (read-only
568
- * passthrough) inputs from real generated outputs so the LLM doesn't
569
- * conflate skill files with newly-produced artifacts. */
570
- const inheritedFiles = response.files.filter((f) => f.inherited === true);
571
- const generatedFiles = response.files.filter((f) => f.inherited !== true);
572
-
573
- formatted += renderFileSection(
574
- generatedFilesHeader,
575
- generatedFiles,
576
- otherMessage
577
- );
578
- formatted += renderFileSection(
579
- inheritedFilesHeader,
580
- inheritedFiles,
581
- inheritedFileMessage
582
- );
583
-
584
- if (generatedFiles.length > 0) {
585
- formatted += `\n\n${accessMessage}`;
586
- }
587
- if (inheritedFiles.length > 0) {
588
- formatted += `\n\n${inheritedNote}`;
589
- }
590
- }
591
-
592
554
  return [
593
555
  formatted.trim(),
594
556
  {
@@ -675,16 +637,19 @@ export function createProgrammaticToolCallingTool(
675
637
  }
676
638
 
677
639
  /**
678
- * File injection priority:
679
- * 1. Use _injected_files from ToolNode (avoids /files endpoint race condition)
680
- * 2. Fall back to fetching from /files endpoint if session_id
681
- * provided but no injected files.
640
+ * File injection: `_injected_files` from ToolNode session
641
+ * context. The legacy `/files/<session_id>` HTTP fallback was
642
+ * removed (see `CodeExecutor.ts`) codeapi's sessionAuth now
643
+ * requires kind/id query params unavailable at this point.
682
644
  */
683
645
  let files: t.CodeEnvFile[] | undefined;
684
646
  if (_injected_files && _injected_files.length > 0) {
685
647
  files = _injected_files;
686
648
  } else if (session_id != null && session_id.length > 0) {
687
- files = await fetchSessionFiles(baseUrl, session_id, proxy);
649
+ // eslint-disable-next-line no-console
650
+ console.debug(
651
+ `[ProgrammaticToolCalling] No injected files for session_id=${session_id} — exec will run without input files`
652
+ );
688
653
  }
689
654
 
690
655
  let response = await makeRequest(
@@ -626,7 +626,13 @@ for member in team:
626
626
  expect(output).toContain('stderr:\nWarning: deprecated function');
627
627
  });
628
628
 
629
- it('formats file information correctly', () => {
629
+ it('preserves files on the artifact but omits them from the LLM-facing output', () => {
630
+ /* The post-execution file summary was removed because it
631
+ * misled the model more than it helped — especially with
632
+ * bash, where models naturally `ls /mnt/data/` to discover
633
+ * available files. The artifact still carries every file
634
+ * so the host's session-tracking layer stays in sync;
635
+ * the LLM just doesn't see the prescriptive listing. */
630
636
  const response: t.ProgrammaticExecutionResponse = {
631
637
  status: 'completed',
632
638
  stdout: 'Generated report\n',
@@ -634,95 +640,26 @@ for member in team:
634
640
  files: [
635
641
  { id: '1', name: 'report.pdf' },
636
642
  { id: '2', name: 'data.csv' },
637
- ],
638
- session_id: 'sess_abc123',
639
- };
640
-
641
- const [output, artifact] = formatCompletedResponse(response);
642
-
643
- expect(output).toContain('Generated files:');
644
- expect(output).toContain('report.pdf');
645
- expect(output).toContain('data.csv');
646
- expect(artifact.files).toHaveLength(2);
647
- expect(artifact.files).toEqual(response.files);
648
- });
649
-
650
- it('handles image files with special message', () => {
651
- const response: t.ProgrammaticExecutionResponse = {
652
- status: 'completed',
653
- stdout: '',
654
- stderr: '',
655
- files: [
656
- { id: '1', name: 'chart.png' },
657
- { id: '2', name: 'photo.jpg' },
658
- ],
659
- session_id: 'sess_abc123',
660
- };
661
-
662
- const [output] = formatCompletedResponse(response);
663
-
664
- expect(output).toContain('chart.png');
665
- expect(output).toContain('Image is already displayed to the user');
666
- });
667
-
668
- it('splits inherited inputs from generated outputs into distinct sections', () => {
669
- const response: t.ProgrammaticExecutionResponse = {
670
- status: 'completed',
671
- stdout: 'analysis done\n',
672
- stderr: '',
673
- files: [
674
- { id: 'g1', name: 'report.pdf' },
675
643
  { id: 'i1', name: 'pptx/SKILL.md', inherited: true },
676
- { id: 'i2', name: 'pptx/scripts/clean.py', inherited: true },
677
- { id: 'g2', name: 'chart.png' },
678
644
  ],
679
645
  session_id: 'sess_abc123',
680
646
  };
681
647
 
682
648
  const [output, artifact] = formatCompletedResponse(response);
683
649
 
684
- /* Generated section lists only outputs the run produced. */
685
- const generatedIdx = output.indexOf('Generated files:');
686
- const inheritedIdx = output.indexOf('Available files (inputs');
687
- expect(generatedIdx).toBeGreaterThan(-1);
688
- expect(inheritedIdx).toBeGreaterThan(generatedIdx);
689
-
690
- /* Slice each section so we can assert membership without
691
- * cross-talk between the two listings. */
692
- const generatedSection = output.slice(generatedIdx, inheritedIdx);
693
- const inheritedSection = output.slice(inheritedIdx);
694
-
695
- expect(generatedSection).toContain('report.pdf');
696
- expect(generatedSection).toContain('chart.png');
697
- expect(generatedSection).not.toContain('SKILL.md');
698
-
699
- expect(inheritedSection).toContain('pptx/SKILL.md');
700
- expect(inheritedSection).toContain('pptx/scripts/clean.py');
701
- expect(inheritedSection).toContain('Available as an input');
702
-
703
- /* The artifact still carries every file so the host can still
704
- * thread per-file ids through to subsequent calls. */
705
- expect(artifact.files).toHaveLength(4);
706
- });
707
-
708
- it('omits the Generated files header when every entry is inherited', () => {
709
- const response: t.ProgrammaticExecutionResponse = {
710
- status: 'completed',
711
- stdout: 'cat: ok\n',
712
- stderr: '',
713
- files: [
714
- { id: 'i1', name: 'pptx/SKILL.md', inherited: true },
715
- { id: 'i2', name: 'pptx/editing.md', inherited: true },
716
- ],
717
- session_id: 'sess_abc123',
718
- };
719
-
720
- const [output] = formatCompletedResponse(response);
721
-
650
+ /* Tool result text is stdout/stderr only. */
651
+ expect(output).toContain('stdout:\nGenerated report');
722
652
  expect(output).not.toContain('Generated files:');
723
- expect(output).toContain('Available files (inputs');
724
- expect(output).toContain('pptx/SKILL.md');
725
- expect(output).toContain('pptx/editing.md');
653
+ expect(output).not.toContain('Available files');
654
+ expect(output).not.toContain('report.pdf');
655
+ expect(output).not.toContain('SKILL.md');
656
+ expect(output).not.toContain('Image is already displayed');
657
+ expect(output).not.toContain('Available as an input');
658
+
659
+ /* Host-facing artifact still has every file with its
660
+ * `inherited` flag intact for session-context merging. */
661
+ expect(artifact.files).toHaveLength(3);
662
+ expect(artifact.files).toEqual(response.files);
726
663
  });
727
664
  });
728
665
 
@@ -922,7 +859,11 @@ for member in team:
922
859
  });
923
860
  });
924
861
 
925
- it('formats response with files', () => {
862
+ it('passes files through on the artifact, never on the LLM-facing output', () => {
863
+ /* Output stays stdout/stderr-only regardless of file count or
864
+ * filename shape. The artifact is the sole sink for file refs;
865
+ * hosts thread them into `_injected_files` on subsequent
866
+ * tool calls via `storeCodeSessionFromResults`. */
926
867
  const response: t.ProgrammaticExecutionResponse = {
927
868
  status: 'completed',
928
869
  stdout: 'Report generated\n',
@@ -930,61 +871,21 @@ for member in team:
930
871
  files: [
931
872
  { id: '1', name: 'report.csv' },
932
873
  { id: '2', name: 'chart.png' },
933
- ],
934
- session_id: 'sess_xyz',
935
- };
936
-
937
- const [output, artifact] = formatCompletedResponse(response);
938
-
939
- expect(output).toContain('Generated files:');
940
- expect(output).toContain('report.csv');
941
- expect(output).toContain('chart.png');
942
- expect(output).toContain('File is already downloaded');
943
- expect(output).toContain('Image is already displayed');
944
- expect(artifact.files).toHaveLength(2);
945
- });
946
-
947
- it('handles multiple files with correct separators', () => {
948
- const response: t.ProgrammaticExecutionResponse = {
949
- status: 'completed',
950
- stdout: 'Done\n',
951
- stderr: '',
952
- files: [
953
- { id: '1', name: 'file1.txt' },
954
- { id: '2', name: 'file2.txt' },
955
- ],
956
- session_id: 'sess_xyz',
957
- };
958
-
959
- const [output] = formatCompletedResponse(response);
960
-
961
- // 2 files format: "- /mnt/data/file1.txt | ..., - /mnt/data/file2.txt | ..."
962
- expect(output).toContain('file1.txt');
963
- expect(output).toContain('file2.txt');
964
- expect(output).toContain('- /mnt/data/file1.txt');
965
- expect(output).toContain('- /mnt/data/file2.txt');
966
- });
967
-
968
- it('handles many files with newline separators', () => {
969
- const response: t.ProgrammaticExecutionResponse = {
970
- status: 'completed',
971
- stdout: 'Done\n',
972
- stderr: '',
973
- files: [
974
- { id: '1', name: 'file1.txt' },
975
- { id: '2', name: 'file2.txt' },
976
874
  { id: '3', name: 'file3.txt' },
977
875
  { id: '4', name: 'file4.txt' },
978
876
  ],
979
877
  session_id: 'sess_xyz',
980
878
  };
981
879
 
982
- const [output] = formatCompletedResponse(response);
880
+ const [output, artifact] = formatCompletedResponse(response);
881
+
882
+ expect(output).toBe('stdout:\nReport generated');
883
+ expect(output).not.toContain('report.csv');
884
+ expect(output).not.toContain('chart.png');
885
+ expect(output).not.toContain('/mnt/data/');
983
886
 
984
- // More than 3 files should use newline separators
985
- expect(output).toContain('file1.txt');
986
- expect(output).toContain('file4.txt');
987
- expect(output.match(/,\n/g)?.length).toBeGreaterThanOrEqual(2);
887
+ expect(artifact.files).toHaveLength(4);
888
+ expect(artifact.files).toEqual(response.files);
988
889
  });
989
890
  });
990
891