@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.
- package/dist/cjs/agents/AgentContext.cjs +62 -20
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/main.cjs +1 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +21 -11
- package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +37 -10
- package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +16 -11
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +62 -20
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/main.mjs +1 -1
- package/dist/esm/tools/BashExecutor.mjs +22 -12
- package/dist/esm/tools/BashExecutor.mjs.map +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +37 -11
- package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +17 -12
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/types/agents/AgentContext.d.ts +15 -0
- package/dist/types/tools/CodeExecutor.d.ts +6 -0
- package/dist/types/types/tools.d.ts +9 -0
- package/package.json +1 -1
- package/src/agents/AgentContext.ts +66 -27
- package/src/agents/__tests__/AgentContext.test.ts +178 -0
- package/src/tools/BashExecutor.ts +37 -13
- package/src/tools/CodeExecutor.ts +55 -11
- package/src/tools/ProgrammaticToolCalling.ts +29 -14
- package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +60 -0
- package/src/types/tools.ts +9 -0
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
222
|
+
formattedOutput += renderFileSection(
|
|
223
|
+
generatedFilesHeader,
|
|
224
|
+
generatedFiles,
|
|
225
|
+
otherMessage
|
|
226
|
+
);
|
|
227
|
+
formattedOutput += renderFileSection(
|
|
228
|
+
inheritedFilesHeader,
|
|
229
|
+
inheritedFiles,
|
|
230
|
+
inheritedFileMessage
|
|
231
|
+
);
|
|
208
232
|
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
240
|
+
formattedOutput += renderFileSection(
|
|
241
|
+
generatedFilesHeader,
|
|
242
|
+
generatedFiles,
|
|
243
|
+
otherMessage
|
|
244
|
+
);
|
|
245
|
+
formattedOutput += renderFileSection(
|
|
246
|
+
inheritedFilesHeader,
|
|
247
|
+
inheritedFiles,
|
|
248
|
+
inheritedFileMessage
|
|
249
|
+
);
|
|
206
250
|
|
|
207
|
-
|
|
208
|
-
|
|
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 {
|
|
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
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
-
|
|
564
|
-
|
|
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', () => {
|
package/src/types/tools.ts
CHANGED
|
@@ -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[];
|