@librechat/agents 3.1.73 → 3.1.74

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.
@@ -3,17 +3,23 @@ import fetch, { RequestInit } from 'node-fetch';
3
3
  import { HttpsProxyAgent } from 'https-proxy-agent';
4
4
  import { tool, DynamicStructuredTool } from '@langchain/core/tools';
5
5
  import type * as t from '@/types';
6
- import { imageExtRegex, getCodeBaseURL } from './CodeExecutor';
6
+ import { getCodeBaseURL, renderFileSection } from './CodeExecutor';
7
7
  import { Constants } from '@/common';
8
8
 
9
9
  config();
10
10
 
11
- const imageMessage = 'Image is already displayed to the user';
12
11
  const otherMessage = 'File is already downloaded by the user';
12
+ const inheritedFileMessage =
13
+ 'Available as an input — already known to the user';
13
14
  const accessMessage =
14
15
  'Note: Files from previous executions are automatically available and can be modified.';
15
16
  const emptyOutputMessage =
16
17
  'stdout: Empty. Ensure you\'re writing output explicitly.\n';
18
+ const inheritedFilesHeader =
19
+ 'Available files (inputs, not generated by this execution):';
20
+ const generatedFilesHeader = 'Generated files:';
21
+ const inheritedNote =
22
+ '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.';
17
23
 
18
24
  const baseEndpoint = getCodeBaseURL();
19
25
  const EXEC_ENDPOINT = `${baseEndpoint}/exec`;
@@ -198,20 +204,38 @@ function createBashExecutionTool(
198
204
  }
199
205
  if (result.stderr) formattedOutput += `stderr:\n${result.stderr}\n`;
200
206
  if (result.files && result.files.length > 0) {
201
- formattedOutput += 'Generated files:\n';
207
+ /* Split inherited (read-only / unchanged-input passthroughs from
208
+ * codeapi) from genuine generated outputs. The LLM was previously
209
+ * shown skill files under "Generated files:" with the message
210
+ * "File is already downloaded by the user", which led it to
211
+ * (a) believe it had just produced files it merely referenced
212
+ * and (b) sometimes invent paths like /mnt/user-data/uploads/
213
+ * trying to find the "originals". Labeling them as inputs makes
214
+ * the mental model accurate. */
215
+ const inheritedFiles = result.files.filter(
216
+ (f) => f.inherited === true
217
+ );
218
+ const generatedFiles = result.files.filter(
219
+ (f) => f.inherited !== true
220
+ );
202
221
 
203
- const fileCount = result.files.length;
204
- for (let i = 0; i < fileCount; i++) {
205
- const file = result.files[i];
206
- const isImage = imageExtRegex.test(file.name);
207
- formattedOutput += `- /mnt/data/${file.name} | ${isImage ? imageMessage : otherMessage}`;
222
+ formattedOutput += renderFileSection(
223
+ generatedFilesHeader,
224
+ generatedFiles,
225
+ otherMessage
226
+ );
227
+ formattedOutput += renderFileSection(
228
+ inheritedFilesHeader,
229
+ inheritedFiles,
230
+ inheritedFileMessage
231
+ );
208
232
 
209
- if (i < fileCount - 1) {
210
- formattedOutput += fileCount <= 3 ? ', ' : ',\n';
211
- }
233
+ if (generatedFiles.length > 0) {
234
+ formattedOutput += `\n\n${accessMessage}`;
235
+ }
236
+ if (inheritedFiles.length > 0) {
237
+ formattedOutput += `\n\n${inheritedNote}`;
212
238
  }
213
-
214
- formattedOutput += `\n\n${accessMessage}`;
215
239
  return [
216
240
  formattedOutput.trim(),
217
241
  {
@@ -15,10 +15,41 @@ export const getCodeBaseURL = (): string =>
15
15
 
16
16
  const imageMessage = 'Image is already displayed to the user';
17
17
  const otherMessage = 'File is already downloaded by the user';
18
+ const inheritedFileMessage =
19
+ 'Available as an input — already known to the user';
18
20
  const accessMessage =
19
21
  'Note: Files from previous executions are automatically available and can be modified.';
20
22
  const emptyOutputMessage =
21
23
  '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
+ }
22
53
 
23
54
  const SUPPORTED_LANGUAGES = [
24
55
  'py',
@@ -196,20 +227,33 @@ function createCodeExecutionTool(
196
227
  }
197
228
  if (result.stderr) formattedOutput += `stderr:\n${result.stderr}\n`;
198
229
  if (result.files && result.files.length > 0) {
199
- formattedOutput += 'Generated files:\n';
230
+ /* See BashExecutor for the rationale: split inherited (read-only
231
+ * passthrough) inputs from real generated outputs so the LLM
232
+ * doesn't conflate skill files with newly-produced artifacts. */
233
+ const inheritedFiles = result.files.filter(
234
+ (f) => f.inherited === true
235
+ );
236
+ const generatedFiles = result.files.filter(
237
+ (f) => f.inherited !== true
238
+ );
200
239
 
201
- const fileCount = result.files.length;
202
- for (let i = 0; i < fileCount; i++) {
203
- const file = result.files[i];
204
- const isImage = imageExtRegex.test(file.name);
205
- formattedOutput += `- /mnt/data/${file.name} | ${isImage ? imageMessage : otherMessage}`;
240
+ formattedOutput += renderFileSection(
241
+ generatedFilesHeader,
242
+ generatedFiles,
243
+ otherMessage
244
+ );
245
+ formattedOutput += renderFileSection(
246
+ inheritedFilesHeader,
247
+ inheritedFiles,
248
+ inheritedFileMessage
249
+ );
206
250
 
207
- if (i < fileCount - 1) {
208
- formattedOutput += fileCount <= 3 ? ', ' : ',\n';
209
- }
251
+ if (generatedFiles.length > 0) {
252
+ formattedOutput += `\n\n${accessMessage}`;
253
+ }
254
+ if (inheritedFiles.length > 0) {
255
+ formattedOutput += `\n\n${inheritedNote}`;
210
256
  }
211
-
212
- formattedOutput += `\n\n${accessMessage}`;
213
257
  return [
214
258
  formattedOutput.trim(),
215
259
  {
@@ -5,7 +5,7 @@ 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 { imageExtRegex, getCodeBaseURL } from './CodeExecutor';
8
+ import { getCodeBaseURL, renderFileSection } from './CodeExecutor';
9
9
  import { Constants } from '@/common';
10
10
 
11
11
  config();
@@ -14,8 +14,14 @@ config();
14
14
  // Constants
15
15
  // ============================================================================
16
16
 
17
- const imageMessage = 'Image is already displayed to the user';
18
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.';
19
25
  const accessMessage =
20
26
  'Note: Files from previous executions are automatically available and can be modified.';
21
27
  const emptyOutputMessage =
@@ -552,20 +558,29 @@ export function formatCompletedResponse(
552
558
  }
553
559
 
554
560
  if (response.files && response.files.length > 0) {
555
- formatted += 'Generated files:\n';
556
-
557
- const fileCount = response.files.length;
558
- for (let i = 0; i < fileCount; i++) {
559
- const file = response.files[i];
560
- const isImage = imageExtRegex.test(file.name);
561
- formatted += `- /mnt/data/${file.name} | ${isImage ? imageMessage : otherMessage}`;
561
+ /* See BashExecutor for the rationale: split inherited (read-only
562
+ * passthrough) inputs from real generated outputs so the LLM doesn't
563
+ * conflate skill files with newly-produced artifacts. */
564
+ const inheritedFiles = response.files.filter((f) => f.inherited === true);
565
+ const generatedFiles = response.files.filter((f) => f.inherited !== true);
566
+
567
+ formatted += renderFileSection(
568
+ generatedFilesHeader,
569
+ generatedFiles,
570
+ otherMessage
571
+ );
572
+ formatted += renderFileSection(
573
+ inheritedFilesHeader,
574
+ inheritedFiles,
575
+ inheritedFileMessage
576
+ );
562
577
 
563
- if (i < fileCount - 1) {
564
- formatted += fileCount <= 3 ? ', ' : ',\n';
565
- }
578
+ if (generatedFiles.length > 0) {
579
+ formatted += `\n\n${accessMessage}`;
580
+ }
581
+ if (inheritedFiles.length > 0) {
582
+ formatted += `\n\n${inheritedNote}`;
566
583
  }
567
-
568
- formatted += `\n\n${accessMessage}`;
569
584
  }
570
585
 
571
586
  return [
@@ -664,6 +664,66 @@ for member in team:
664
664
  expect(output).toContain('chart.png');
665
665
  expect(output).toContain('Image is already displayed to the user');
666
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
+ { 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
+ ],
679
+ session_id: 'sess_abc123',
680
+ };
681
+
682
+ const [output, artifact] = formatCompletedResponse(response);
683
+
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
+
722
+ 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');
726
+ });
667
727
  });
668
728
 
669
729
  describe('createProgrammaticToolCallingTool - Manual Invocation', () => {
@@ -113,6 +113,15 @@ export type FileRef = {
113
113
  path?: string;
114
114
  /** Session ID this file belongs to (for multi-session file tracking) */
115
115
  session_id?: string;
116
+ /**
117
+ * `true` when the codeapi sandbox echoed this entry as an unchanged
118
+ * passthrough of an input the caller already owns (skill files,
119
+ * downloaded inputs whose hash matched the baseline, inherited
120
+ * `.dirkeep` markers). The tool-result formatter renders these as
121
+ * "Available files" rather than "Generated files" so the LLM doesn't
122
+ * conflate infrastructure inputs with newly-produced outputs.
123
+ */
124
+ inherited?: true;
116
125
  };
117
126
 
118
127
  export type FileRefs = FileRef[];