@librechat/agents 3.1.80-dev.1 → 3.1.80-dev.3
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/main.cjs +1 -2
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +20 -78
- package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +5 -1
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +26 -106
- package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +12 -31
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +37 -14
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/esm/main.mjs +1 -1
- package/dist/esm/tools/BashExecutor.mjs +20 -78
- package/dist/esm/tools/BashExecutor.mjs.map +1 -1
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs +6 -2
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +26 -105
- package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +12 -31
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +37 -14
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/types/tools/CodeExecutor.d.ts +1 -7
- package/dist/types/tools/ProgrammaticToolCalling.d.ts +5 -0
- package/package.json +1 -1
- package/src/tools/BashExecutor.ts +24 -104
- package/src/tools/BashProgrammaticToolCalling.ts +7 -2
- package/src/tools/CodeExecutor.ts +30 -133
- package/src/tools/ProgrammaticToolCalling.ts +14 -49
- package/src/tools/ToolNode.ts +47 -15
- package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +32 -131
- package/src/tools/__tests__/ToolNode.session.test.ts +182 -0
|
@@ -626,7 +626,13 @@ for member in team:
|
|
|
626
626
|
expect(output).toContain('stderr:\nWarning: deprecated function');
|
|
627
627
|
});
|
|
628
628
|
|
|
629
|
-
it('
|
|
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
|
-
/*
|
|
685
|
-
|
|
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
|
|
724
|
-
expect(output).toContain('
|
|
725
|
-
expect(output).toContain('
|
|
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('
|
|
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
|
-
|
|
985
|
-
expect(
|
|
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
|
|
|
@@ -512,6 +512,188 @@ describe('ToolNode code execution session management', () => {
|
|
|
512
512
|
expect(chartFile!.storage_session_id).toBe('new-sess');
|
|
513
513
|
});
|
|
514
514
|
|
|
515
|
+
it('preserves prior kind/resource_id/version when worker echoes inherited file (skill 403 regression)', () => {
|
|
516
|
+
/**
|
|
517
|
+
* Regression for the codeapi `session_key_mismatch` 403 that
|
|
518
|
+
* fires on the second `/exec` after a successful skill prime.
|
|
519
|
+
*
|
|
520
|
+
* Worker `inherited: true` echoes carry only
|
|
521
|
+
* `(id, name, storage_session_id)` — the sandbox doesn't know
|
|
522
|
+
* the resource identity (`kind`, `resource_id`, `version`),
|
|
523
|
+
* which was signed at upload time. If `updateCodeSession`
|
|
524
|
+
* replaces the prior entry verbatim from the echo, the next
|
|
525
|
+
* `_injected_files` derivation reads `kind: undefined` and
|
|
526
|
+
* `toInjectedFileRef` falls back to `kind: 'user'` +
|
|
527
|
+
* `resource_id: file.id`. Codeapi then resolves
|
|
528
|
+
* `legacy:user:<authContext.userId>`, which doesn't match the
|
|
529
|
+
* cached `legacy:skill:<skillId>:v:<v>` set at upload, and
|
|
530
|
+
* authorization 403s.
|
|
531
|
+
*
|
|
532
|
+
* The merge must overlay the echo onto the prior entry by
|
|
533
|
+
* `(storage_session_id, id)` so identity survives.
|
|
534
|
+
*/
|
|
535
|
+
const SKILL_ID = '69dcf561f37f717858d4d072';
|
|
536
|
+
const SKILL_VERSION = 59;
|
|
537
|
+
const STORAGE_SESSION = 'p28pbmz0ejTZ8MMEkObN8';
|
|
538
|
+
const FILE_ID = 'JqnzC4f6gpirzW0yq_obR';
|
|
539
|
+
|
|
540
|
+
const sessions: t.ToolSessionMap = new Map();
|
|
541
|
+
sessions.set(Constants.EXECUTE_CODE, {
|
|
542
|
+
session_id: STORAGE_SESSION,
|
|
543
|
+
files: [
|
|
544
|
+
{
|
|
545
|
+
id: FILE_ID,
|
|
546
|
+
resource_id: SKILL_ID,
|
|
547
|
+
name: 'pptx/editing.md',
|
|
548
|
+
storage_session_id: STORAGE_SESSION,
|
|
549
|
+
kind: 'skill',
|
|
550
|
+
version: SKILL_VERSION,
|
|
551
|
+
},
|
|
552
|
+
],
|
|
553
|
+
lastUpdated: Date.now(),
|
|
554
|
+
} satisfies t.CodeSessionContext);
|
|
555
|
+
|
|
556
|
+
const mockTool = createMockCodeTool({ capturedConfigs: [] });
|
|
557
|
+
const toolNode = new ToolNode({
|
|
558
|
+
tools: [mockTool],
|
|
559
|
+
sessions,
|
|
560
|
+
eventDrivenMode: true,
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
const storeMethod = (
|
|
564
|
+
toolNode as unknown as {
|
|
565
|
+
storeCodeSessionFromResults: (
|
|
566
|
+
results: t.ToolExecuteResult[],
|
|
567
|
+
requestMap: Map<string, t.ToolCallRequest>
|
|
568
|
+
) => void;
|
|
569
|
+
}
|
|
570
|
+
).storeCodeSessionFromResults.bind(toolNode);
|
|
571
|
+
|
|
572
|
+
/* Worker echo shape — exactly what codeapi sends on
|
|
573
|
+
* `inherited: true` files. No kind, no resource_id, no version. */
|
|
574
|
+
storeMethod(
|
|
575
|
+
[
|
|
576
|
+
{
|
|
577
|
+
toolCallId: 'tc-skill-rerun',
|
|
578
|
+
content: 'output',
|
|
579
|
+
artifact: {
|
|
580
|
+
session_id: 'exec-sess-2',
|
|
581
|
+
files: [
|
|
582
|
+
{
|
|
583
|
+
id: FILE_ID,
|
|
584
|
+
name: 'pptx/editing.md',
|
|
585
|
+
storage_session_id: STORAGE_SESSION,
|
|
586
|
+
inherited: true,
|
|
587
|
+
} as unknown as t.FileRefs[number],
|
|
588
|
+
],
|
|
589
|
+
},
|
|
590
|
+
status: 'success',
|
|
591
|
+
},
|
|
592
|
+
],
|
|
593
|
+
new Map([
|
|
594
|
+
[
|
|
595
|
+
'tc-skill-rerun',
|
|
596
|
+
{ id: 'tc-skill-rerun', name: Constants.EXECUTE_CODE, args: {} },
|
|
597
|
+
],
|
|
598
|
+
])
|
|
599
|
+
);
|
|
600
|
+
|
|
601
|
+
const stored = sessions.get(
|
|
602
|
+
Constants.EXECUTE_CODE
|
|
603
|
+
) as t.CodeSessionContext;
|
|
604
|
+
const merged = stored.files!.find((f) => f.id === FILE_ID);
|
|
605
|
+
expect(merged).toBeDefined();
|
|
606
|
+
/* Identity preserved from prior entry: */
|
|
607
|
+
expect(merged!.kind).toBe('skill');
|
|
608
|
+
expect(merged!.resource_id).toBe(SKILL_ID);
|
|
609
|
+
expect((merged as { version?: number }).version).toBe(SKILL_VERSION);
|
|
610
|
+
/* Echo-owned fields propagated: */
|
|
611
|
+
expect((merged as { inherited?: boolean }).inherited).toBe(true);
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
it('uses fresh kind defaults for genuinely new files (no prior entry to merge from)', () => {
|
|
615
|
+
/**
|
|
616
|
+
* The merge keys on `(storage_session_id, id)`, not `name`.
|
|
617
|
+
* A file the worker emits for the first time (typical
|
|
618
|
+
* code-output / generated artifact) has no prior to inherit
|
|
619
|
+
* identity from — it lands as `kind: 'user'` via the standard
|
|
620
|
+
* fallback in `toInjectedFileRef`. Locks the contract that
|
|
621
|
+
* the merge doesn't accidentally re-tag user output as skill.
|
|
622
|
+
*/
|
|
623
|
+
const sessions: t.ToolSessionMap = new Map();
|
|
624
|
+
sessions.set(Constants.EXECUTE_CODE, {
|
|
625
|
+
session_id: 'old-sess',
|
|
626
|
+
files: [
|
|
627
|
+
{
|
|
628
|
+
id: 'skill-f1',
|
|
629
|
+
resource_id: 'skill-id-1',
|
|
630
|
+
name: 'pptx/editing.md',
|
|
631
|
+
storage_session_id: 'skill-storage',
|
|
632
|
+
kind: 'skill',
|
|
633
|
+
version: 7,
|
|
634
|
+
},
|
|
635
|
+
],
|
|
636
|
+
lastUpdated: Date.now(),
|
|
637
|
+
} satisfies t.CodeSessionContext);
|
|
638
|
+
|
|
639
|
+
const mockTool = createMockCodeTool({ capturedConfigs: [] });
|
|
640
|
+
const toolNode = new ToolNode({
|
|
641
|
+
tools: [mockTool],
|
|
642
|
+
sessions,
|
|
643
|
+
eventDrivenMode: true,
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
const storeMethod = (
|
|
647
|
+
toolNode as unknown as {
|
|
648
|
+
storeCodeSessionFromResults: (
|
|
649
|
+
results: t.ToolExecuteResult[],
|
|
650
|
+
requestMap: Map<string, t.ToolCallRequest>
|
|
651
|
+
) => void;
|
|
652
|
+
}
|
|
653
|
+
).storeCodeSessionFromResults.bind(toolNode);
|
|
654
|
+
|
|
655
|
+
storeMethod(
|
|
656
|
+
[
|
|
657
|
+
{
|
|
658
|
+
toolCallId: 'tc-output',
|
|
659
|
+
content: 'output',
|
|
660
|
+
artifact: {
|
|
661
|
+
session_id: 'output-sess',
|
|
662
|
+
files: [
|
|
663
|
+
{
|
|
664
|
+
id: 'fresh-output-id',
|
|
665
|
+
name: 'generated-chart.png',
|
|
666
|
+
storage_session_id: 'output-sess',
|
|
667
|
+
} as unknown as t.FileRefs[number],
|
|
668
|
+
],
|
|
669
|
+
},
|
|
670
|
+
status: 'success',
|
|
671
|
+
},
|
|
672
|
+
],
|
|
673
|
+
new Map([
|
|
674
|
+
[
|
|
675
|
+
'tc-output',
|
|
676
|
+
{ id: 'tc-output', name: Constants.EXECUTE_CODE, args: {} },
|
|
677
|
+
],
|
|
678
|
+
])
|
|
679
|
+
);
|
|
680
|
+
|
|
681
|
+
const stored = sessions.get(
|
|
682
|
+
Constants.EXECUTE_CODE
|
|
683
|
+
) as t.CodeSessionContext;
|
|
684
|
+
const fresh = stored.files!.find((f) => f.id === 'fresh-output-id');
|
|
685
|
+
expect(fresh).toBeDefined();
|
|
686
|
+
/* No prior to inherit from — kind/resource_id/version absent
|
|
687
|
+
* on the entry; toInjectedFileRef will default kind to 'user'
|
|
688
|
+
* downstream. */
|
|
689
|
+
expect(fresh!.kind).toBeUndefined();
|
|
690
|
+
expect(fresh!.resource_id).toBeUndefined();
|
|
691
|
+
expect((fresh as { version?: number }).version).toBeUndefined();
|
|
692
|
+
/* Skill file untouched. */
|
|
693
|
+
const skillFile = stored.files!.find((f) => f.id === 'skill-f1');
|
|
694
|
+
expect(skillFile!.kind).toBe('skill');
|
|
695
|
+
});
|
|
696
|
+
|
|
515
697
|
it('preserves existing files when new execution has no files', () => {
|
|
516
698
|
const sessions: t.ToolSessionMap = new Map();
|
|
517
699
|
sessions.set(Constants.EXECUTE_CODE, {
|