🦜 refactor: Use path for Read/Write/Edit/Create File Tools (#13834)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions

* fix(agents): use `path` for read/write/edit/create file tools

Pairs with @librechat/agents renaming the read_file/write_file/edit_file tool
parameter from `file_path` to `path` (models — esp. Kimi K2 — emit `path` far
more reliably, and it matches grep/glob/list_directory which already use `path`).

- tools.ts: LibreChat's own code/skill file-tool schemas use `path`
  (the skill read_file tool inherits the SDK definition, which is already renamed)
- handlers.ts: read `args.path` for the model-facing tool arg + error messages
- the internal host `readSandboxFile`/`writeSandboxFile` contract is unchanged
- tests updated

Requires @librechat/agents with the param rename (danny-avila/agents#250). All
agents unit suites green (175).

* chore: update @librechat/agents to v3.2.41 and bump related dependencies in package-lock.json and package.json files

* fix(api): Refactor header merging in MCPConnection to use Object.assign for clarity

* test(e2e): mock emits `path` for create/edit file-authoring tools

The mock LLM still sent `file_path` for the create_file/edit_file calls, which the
renamed handlers no longer read -> the skill-file-authoring e2e failed with
'Expected skill to be persisted'. Switch the fixture to `path` to match the tools.
(The internal readSandboxFile/writeSandboxFile contract stays on `file_path`, so
api/server/services/Files/Code/process.js and its spec are unchanged.)
This commit is contained in:
Danny Avila 2026-06-18 14:44:51 -04:00 committed by GitHub
parent 2fcba914f7
commit 68d142d0e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 156 additions and 152 deletions

View file

@ -46,7 +46,7 @@
"@azure/storage-blob": "^12.30.0",
"@google/genai": "^2.8.0",
"@keyv/redis": "^4.3.3",
"@librechat/agents": "^3.2.38",
"@librechat/agents": "^3.2.41",
"@librechat/api": "*",
"@librechat/data-schemas": "*",
"@microsoft/microsoft-graph-client": "^3.0.7",

View file

@ -427,7 +427,7 @@ Created by the Playwright mock e2e suite to verify host file authoring without c
function buildCreateSkillArgs(skillName) {
return {
file_path: `skills/${skillName}/SKILL.md`,
path: `skills/${skillName}/SKILL.md`,
content: buildSkillBody(skillName),
overwrite: false,
};
@ -435,7 +435,7 @@ function buildCreateSkillArgs(skillName) {
function buildEditSkillArgs(skillName) {
return {
file_path: `skills/${skillName}/SKILL.md`,
path: `skills/${skillName}/SKILL.md`,
old_text: `description: ${SKILL_DESCRIPTION}`,
new_text: `description: ${EDITED_SKILL_DESCRIPTION}`,
};

45
package-lock.json generated
View file

@ -61,7 +61,7 @@
"@azure/storage-blob": "^12.30.0",
"@google/genai": "^2.8.0",
"@keyv/redis": "^4.3.3",
"@librechat/agents": "^3.2.38",
"@librechat/agents": "^3.2.41",
"@librechat/api": "*",
"@librechat/data-schemas": "*",
"@microsoft/microsoft-graph-client": "^3.0.7",
@ -9834,9 +9834,9 @@
}
},
"node_modules/@librechat/agents": {
"version": "3.2.38",
"resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-3.2.38.tgz",
"integrity": "sha512-jbD+H9T/Bcz2ot50X2J/Ffsq5McFHriS66AhFHMIzUfsbDVbLFSYLj/6K61JRjxegIZ/z/gV6tmVAmZCHWh5kg==",
"version": "3.2.41",
"resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-3.2.41.tgz",
"integrity": "sha512-AjzSrurK/ihORn4NIftlS5CCviBWcT2kkLvYoCuV/aRT2E4HQea6aUfC+g0FIK71pBN9RThQv7Gh3I/7/aMxIw==",
"license": "MIT",
"dependencies": {
"@anthropic-ai/sdk": "^0.92.0",
@ -11538,7 +11538,8 @@
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/base64": {
"version": "1.1.2",
@ -11557,12 +11558,12 @@
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="
},
"node_modules/@protobufjs/fetch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz",
"integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==",
"license": "BSD-3-Clause",
"dependencies": {
"@protobufjs/aspromise": "^1.1.1",
"@protobufjs/inquire": "^1.1.0"
"@protobufjs/aspromise": "^1.1.1"
}
},
"node_modules/@protobufjs/float": {
@ -11571,9 +11572,9 @@
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="
},
"node_modules/@protobufjs/inquire": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.1.tgz",
"integrity": "sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz",
"integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/path": {
@ -35755,9 +35756,9 @@
}
},
"node_modules/protobufjs": {
"version": "7.5.8",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.8.tgz",
"integrity": "sha512-dvpCIeLPbXZS/Ete7yLaO7RenOdken2NHKykBXbsaGxZT0UTltcarBciw+A78SRQs9iMAAVpsYA+l8b1hTePIA==",
"version": "7.5.9",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.9.tgz",
"integrity": "sha512-Od4muIm3HW1AouyHF5lONOf1FWo3hY1NbFDoy191X9GzhpgW1clCoaFjfVs2rKJNFYpTNJbje4cbAIDBZJ63ZA==",
"hasInstallScript": true,
"license": "BSD-3-Clause",
"dependencies": {
@ -35765,9 +35766,9 @@
"@protobufjs/base64": "^1.1.2",
"@protobufjs/codegen": "^2.0.5",
"@protobufjs/eventemitter": "^1.1.0",
"@protobufjs/fetch": "^1.1.0",
"@protobufjs/fetch": "^1.1.1",
"@protobufjs/float": "^1.0.2",
"@protobufjs/inquire": "^1.1.1",
"@protobufjs/inquire": "^1.1.2",
"@protobufjs/path": "^1.1.2",
"@protobufjs/pool": "^1.1.0",
"@protobufjs/utf8": "^1.1.1",
@ -40365,9 +40366,9 @@
"license": "MIT"
},
"node_modules/undici": {
"version": "7.24.1",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.24.1.tgz",
"integrity": "sha512-5xoBibbmnjlcR3jdqtY2Lnx7WbrD/tHlT01TmvqZUFVc9Q1w4+j5hbnapTqbcXITMH1ovjq/W7BkqBilHiVAaA==",
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz",
"integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==",
"license": "MIT",
"engines": {
"node": ">=20.18.1"
@ -42389,7 +42390,7 @@
"@azure/storage-blob": "^12.30.0",
"@google/genai": "^2.8.0",
"@keyv/redis": "^4.3.3",
"@librechat/agents": "^3.2.38",
"@librechat/agents": "^3.2.41",
"@librechat/data-schemas": "*",
"@modelcontextprotocol/sdk": "^1.29.0",
"@opentelemetry/api": "^1.9.0",

View file

@ -113,7 +113,7 @@
"@azure/storage-blob": "^12.30.0",
"@google/genai": "^2.8.0",
"@keyv/redis": "^4.3.3",
"@librechat/agents": "^3.2.38",
"@librechat/agents": "^3.2.41",
"@librechat/data-schemas": "*",
"@modelcontextprotocol/sdk": "^1.29.0",
"@opentelemetry/api": "^1.9.0",

View file

@ -11,7 +11,7 @@ jest.mock('@librechat/agents', () => ({
parameters: {
type: 'object',
properties: {
file_path: {
path: {
type: 'string',
description: 'For skill files: "{skillName}/{path}".',
},

View file

@ -673,7 +673,7 @@ describe('createToolExecuteHandler', () => {
{
id: 'call_read_5',
name: Constants.READ_FILE,
args: { file_path: 'maybe-disabled-read/SKILL.md' },
args: { path: 'maybe-disabled-read/SKILL.md' },
},
]);
@ -717,7 +717,7 @@ describe('createToolExecuteHandler', () => {
{
id: 'call_read_6',
name: Constants.READ_FILE,
args: { file_path: 'manually-primed/references/foo.md' },
args: { path: 'manually-primed/references/foo.md' },
},
]);
@ -766,7 +766,7 @@ describe('createToolExecuteHandler', () => {
{
id: 'call_read_1',
name: Constants.READ_FILE,
args: { file_path: 'pii-redactor/SKILL.md' },
args: { path: 'pii-redactor/SKILL.md' },
},
]);
@ -795,7 +795,7 @@ describe('createToolExecuteHandler', () => {
{
id: 'call_read_2',
name: Constants.READ_FILE,
args: { file_path: 'normal-skill/SKILL.md' },
args: { path: 'normal-skill/SKILL.md' },
},
]);
@ -833,7 +833,7 @@ describe('createToolExecuteHandler', () => {
{
id: 'call_read_3',
name: Constants.READ_FILE,
args: { file_path: 'manual-only-skill/SKILL.md' },
args: { path: 'manual-only-skill/SKILL.md' },
},
]);
@ -869,7 +869,7 @@ describe('createToolExecuteHandler', () => {
{
id: 'call_read_4',
name: Constants.READ_FILE,
args: { file_path: 'other-disabled-skill/SKILL.md' },
args: { path: 'other-disabled-skill/SKILL.md' },
},
]);
@ -911,7 +911,7 @@ describe('createToolExecuteHandler', () => {
{
id: 'call_read_always',
name: Constants.READ_FILE,
args: { file_path: 'always-applied-legal/SKILL.md' },
args: { path: 'always-applied-legal/SKILL.md' },
},
]);
@ -947,7 +947,7 @@ describe('createToolExecuteHandler', () => {
{
id: 'call_read_pin',
name: Constants.READ_FILE,
args: { file_path: 'collides/SKILL.md' },
args: { path: 'collides/SKILL.md' },
},
]);
@ -1093,7 +1093,7 @@ describe('createToolExecuteHandler', () => {
id: 'call_create_skill',
name: 'create_file',
args: {
file_path: 'skills/new-skill/SKILL.md',
path: 'skills/new-skill/SKILL.md',
content:
'---\nname: new-skill\ndescription: Use for tests\ndisable-model-invocation: true\nallowed-tools:\n - execute_code\n---\n# New skill\n',
},
@ -1140,7 +1140,7 @@ describe('createToolExecuteHandler', () => {
id: 'call_create_auto_frontmatter',
name: 'create_file',
args: {
file_path: 'skills/auto-skill/SKILL.md',
path: 'skills/auto-skill/SKILL.md',
content: '# Auto skill\nUse this skill when testing generated frontmatter.\n',
},
},
@ -1179,7 +1179,7 @@ describe('createToolExecuteHandler', () => {
id: 'call_create_block_description',
name: 'create_file',
args: {
file_path: 'skills/block-description-skill/SKILL.md',
path: 'skills/block-description-skill/SKILL.md',
content:
'---\nname: block-description-skill\ndescription: |-\n Use this skill for long descriptions.\n Keep both lines searchable.\n---\n# Block description skill\n',
},
@ -1233,7 +1233,7 @@ describe('createToolExecuteHandler', () => {
id: 'call_create_skill',
name: 'create_file',
args: {
file_path: 'skills/new-skill/SKILL.md',
path: 'skills/new-skill/SKILL.md',
content: '---\nname: new-skill\ndescription: Use for tests\n---\n# New skill\n',
},
},
@ -1241,7 +1241,7 @@ describe('createToolExecuteHandler', () => {
id: 'call_create_reference',
name: 'create_file',
args: {
file_path: 'skills/new-skill/references/a.md',
path: 'skills/new-skill/references/a.md',
content: 'reference text',
},
},
@ -1307,7 +1307,7 @@ describe('createToolExecuteHandler', () => {
id: 'call_create_round_skill',
name: 'create_file',
args: {
file_path: 'skills/round-skill/SKILL.md',
path: 'skills/round-skill/SKILL.md',
content: createdSkill.body,
},
},
@ -1320,7 +1320,7 @@ describe('createToolExecuteHandler', () => {
{
id: 'call_read_round_skill',
name: Constants.READ_FILE,
args: { file_path: 'skills/round-skill/SKILL.md' },
args: { path: 'skills/round-skill/SKILL.md' },
},
],
runtimeConfigurable,
@ -1357,7 +1357,7 @@ describe('createToolExecuteHandler', () => {
id: 'call_create_existing',
name: 'create_file',
args: {
file_path: 'skills/existing-skill/SKILL.md',
path: 'skills/existing-skill/SKILL.md',
content: '---\nname: existing-skill\ndescription: Use for tests\n---\n# Updated\n',
},
},
@ -1396,7 +1396,7 @@ describe('createToolExecuteHandler', () => {
id: 'call_duplicate_stale_skill',
name: 'create_file',
args: {
file_path: 'skills/stale-skill/SKILL.md',
path: 'skills/stale-skill/SKILL.md',
content: '---\nname: stale-skill\ndescription: Replacement\n---\n# Replacement\n',
},
},
@ -1437,7 +1437,7 @@ describe('createToolExecuteHandler', () => {
id: 'call_edit_excluded_skill',
name: 'edit_file',
args: {
file_path: 'skills/excluded-skill/SKILL.md',
path: 'skills/excluded-skill/SKILL.md',
old_text: '# Excluded',
new_text: '# Changed',
},
@ -1477,7 +1477,7 @@ describe('createToolExecuteHandler', () => {
id: 'call_recovered_hidden_skill',
name: 'edit_file',
args: {
file_path: 'skills/hidden-recovered-skill/SKILL.md',
path: 'skills/hidden-recovered-skill/SKILL.md',
old_text: '# Hidden',
new_text: '# Changed',
},
@ -1519,7 +1519,7 @@ describe('createToolExecuteHandler', () => {
id: 'call_edit_file',
name: 'edit_file',
args: {
file_path: 'skills/edit-skill/references/a.md',
path: 'skills/edit-skill/references/a.md',
old_text: 'hello old',
new_text: 'hello new',
},
@ -1581,7 +1581,7 @@ describe('createToolExecuteHandler', () => {
id: 'call_edit_stale_bundled_file',
name: 'edit_file',
args: {
file_path: 'skills/edit-skill/references/a.md',
path: 'skills/edit-skill/references/a.md',
old_text: 'hello old',
new_text: 'hello new',
},
@ -1624,7 +1624,7 @@ describe('createToolExecuteHandler', () => {
id: 'call_edit_skill_md_frontmatter',
name: 'edit_file',
args: {
file_path: 'skills/runtime-skill/SKILL.md',
path: 'skills/runtime-skill/SKILL.md',
old_text: 'description: Use before\naction: ignored',
new_text:
'description: Use before\nuser-invocable: false\ndisable-model-invocation: true\nallowed-tools:\n - execute_code\nalways-apply: true',
@ -1677,7 +1677,7 @@ describe('createToolExecuteHandler', () => {
id: 'call_edit_skill_md_block_description',
name: 'edit_file',
args: {
file_path: 'skills/runtime-skill/SKILL.md',
path: 'skills/runtime-skill/SKILL.md',
old_text: 'description: Use before',
new_text:
'description: |-\n Use this skill for long descriptions.\n Keep both lines searchable.',
@ -1717,7 +1717,7 @@ describe('createToolExecuteHandler', () => {
id: 'call_edit_skill_md_name',
name: 'edit_file',
args: {
file_path: 'skills/runtime-skill/SKILL.md',
path: 'skills/runtime-skill/SKILL.md',
old_text: 'name: runtime-skill',
new_text: 'name: dev-toolkit',
},
@ -1759,7 +1759,7 @@ describe('createToolExecuteHandler', () => {
id: 'call_edit_ambiguous',
name: 'edit_file',
args: {
file_path: 'skills/ambiguous-skill/references/a.md',
path: 'skills/ambiguous-skill/references/a.md',
old_text: 'same',
new_text: 'different',
},
@ -1796,7 +1796,7 @@ describe('createToolExecuteHandler', () => {
id: 'call_edit_hidden_skill',
name: 'edit_file',
args: {
file_path: 'skills/hidden-skill/SKILL.md',
path: 'skills/hidden-skill/SKILL.md',
old_text: '# Hidden',
new_text: '# Changed',
},
@ -1841,7 +1841,7 @@ describe('createToolExecuteHandler', () => {
id: 'call_edit_primed_hidden_skill',
name: 'edit_file',
args: {
file_path: 'skills/primed-hidden-skill/SKILL.md',
path: 'skills/primed-hidden-skill/SKILL.md',
old_text: '# Hidden',
new_text: '# Changed',
},
@ -1883,7 +1883,7 @@ describe('createToolExecuteHandler', () => {
id: 'call_overwrite_large',
name: 'create_file',
args: {
file_path: 'skills/large-skill/references/large.md',
path: 'skills/large-skill/references/large.md',
content: 'replacement',
overwrite: true,
},
@ -1935,7 +1935,7 @@ describe('createToolExecuteHandler', () => {
id: 'call_edit_one',
name: 'edit_file',
args: {
file_path: 'skills/serial-skill/references/a.md',
path: 'skills/serial-skill/references/a.md',
old_text: 'one',
new_text: 'two',
},
@ -1944,7 +1944,7 @@ describe('createToolExecuteHandler', () => {
id: 'call_edit_two',
name: 'edit_file',
args: {
file_path: 'skills/serial-skill/references/a.md',
path: 'skills/serial-skill/references/a.md',
old_text: 'two',
new_text: 'three',
},
@ -1999,7 +1999,7 @@ describe('createToolExecuteHandler', () => {
id: 'call_create_sandbox',
name: 'create_file',
args: {
file_path: '/mnt/data/new.txt',
path: '/mnt/data/new.txt',
content: 'hello world',
},
codeSessionContext: {
@ -2038,7 +2038,7 @@ describe('createToolExecuteHandler', () => {
id: 'call_create_existing_sandbox',
name: 'create_file',
args: {
file_path: '/mnt/data/existing.txt',
path: '/mnt/data/existing.txt',
content: 'new text\n',
},
},
@ -2065,7 +2065,7 @@ describe('createToolExecuteHandler', () => {
id: 'call_edit_sandbox',
name: 'edit_file',
args: {
file_path: '/mnt/data/edit.txt',
path: '/mnt/data/edit.txt',
old_text: 'alpha old',
new_text: 'alpha new',
},
@ -2151,7 +2151,7 @@ describe('createToolExecuteHandler', () => {
id: 'call_create_queued_sandbox',
name: 'create_file',
args: {
file_path: '/mnt/data/queued.txt',
path: '/mnt/data/queued.txt',
content: 'hello world\n',
},
},
@ -2159,7 +2159,7 @@ describe('createToolExecuteHandler', () => {
id: 'call_edit_queued_sandbox',
name: 'edit_file',
args: {
file_path: '/mnt/data/queued.txt',
path: '/mnt/data/queued.txt',
old_text: 'hello world',
new_text: 'goodbye world',
},
@ -2193,7 +2193,7 @@ describe('createToolExecuteHandler', () => {
id: 'call_no_code_env_authoring',
name: 'create_file',
args: {
file_path: '/mnt/data/nope.txt',
path: '/mnt/data/nope.txt',
content: 'nope',
},
},
@ -2218,7 +2218,7 @@ describe('createToolExecuteHandler', () => {
id: 'call_code_only_skill_path',
name: 'create_file',
args: {
file_path: 'skills/nope/SKILL.md',
path: 'skills/nope/SKILL.md',
content: '---\nname: nope\ndescription: Nope\n---\n# Nope\n',
},
},
@ -2274,7 +2274,7 @@ describe('createToolExecuteHandler', () => {
{
id: 'call_mnt_1',
name: Constants.READ_FILE,
args: { file_path: '/mnt/data/sentinel.txt' },
args: { path: '/mnt/data/sentinel.txt' },
codeSessionContext: {
session_id: 'sess-X',
files: [{ id: 'f1', name: 'sentinel.txt', session_id: 'sess-X' }],
@ -2303,7 +2303,7 @@ describe('createToolExecuteHandler', () => {
{
id: 'call_mnt_2',
name: Constants.READ_FILE,
args: { file_path: '/mnt/data/sentinel.txt' },
args: { path: '/mnt/data/sentinel.txt' },
},
]);
@ -2325,7 +2325,7 @@ describe('createToolExecuteHandler', () => {
{
id: 'call_unknown_skill',
name: Constants.READ_FILE,
args: { file_path: 'not-a-skill/foo.md' },
args: { path: 'not-a-skill/foo.md' },
codeSessionContext: { session_id: 'sess-Y' },
} as unknown as ToolCallRequest,
]);
@ -2391,7 +2391,7 @@ describe('createToolExecuteHandler', () => {
{
id: 'call_primed_outside_catalog',
name: Constants.READ_FILE,
args: { file_path: 'primed-only-skill/references/foo.md' },
args: { path: 'primed-only-skill/references/foo.md' },
},
]);
@ -2440,7 +2440,7 @@ describe('createToolExecuteHandler', () => {
{
id: 'call_stale_catalog_skill',
name: Constants.READ_FILE,
args: { file_path: 'stale-catalog-skill/SKILL.md' },
args: { path: 'stale-catalog-skill/SKILL.md' },
},
]);
@ -2475,7 +2475,7 @@ describe('createToolExecuteHandler', () => {
{
id: 'call_skills_off',
name: Constants.READ_FILE,
args: { file_path: 'whatever/path.md' },
args: { path: 'whatever/path.md' },
},
]);
@ -2495,7 +2495,7 @@ describe('createToolExecuteHandler', () => {
{
id: 'call_no_route',
name: Constants.READ_FILE,
args: { file_path: 'whatever/path.md' },
args: { path: 'whatever/path.md' },
},
]);
@ -2524,7 +2524,7 @@ describe('createToolExecuteHandler', () => {
{
id: 'call_real_skill',
name: Constants.READ_FILE,
args: { file_path: 'real-skill/SKILL.md' },
args: { path: 'real-skill/SKILL.md' },
},
]);
@ -2555,7 +2555,7 @@ describe('createToolExecuteHandler', () => {
{
id: 'call_real_skill_namespace',
name: Constants.READ_FILE,
args: { file_path: 'skills/real-skill/SKILL.md' },
args: { path: 'skills/real-skill/SKILL.md' },
},
]);
@ -2584,7 +2584,7 @@ describe('createToolExecuteHandler', () => {
{
id: 'call_explicit_skill_namespace_off',
name: Constants.READ_FILE,
args: { file_path: 'skills/whatever/SKILL.md' },
args: { path: 'skills/whatever/SKILL.md' },
},
]);
@ -2613,7 +2613,7 @@ describe('createToolExecuteHandler', () => {
{
id: 'call_trailing_slash',
name: Constants.READ_FILE,
args: { file_path: 'output/' },
args: { path: 'output/' },
},
]);
@ -2635,7 +2635,7 @@ describe('createToolExecuteHandler', () => {
{
id: 'call_trailing_slash_no_env',
name: Constants.READ_FILE,
args: { file_path: 'output/' },
args: { path: 'output/' },
},
]);
@ -2655,7 +2655,7 @@ describe('createToolExecuteHandler', () => {
{
id: 'call_no_callback',
name: Constants.READ_FILE,
args: { file_path: '/mnt/data/x.txt' },
args: { path: '/mnt/data/x.txt' },
},
]);
@ -2684,7 +2684,7 @@ describe('createToolExecuteHandler', () => {
{
id: 'call_huge_file',
name: Constants.READ_FILE,
args: { file_path: '/mnt/data/huge.log' },
args: { path: '/mnt/data/huge.log' },
},
]);
@ -2712,7 +2712,7 @@ describe('createToolExecuteHandler', () => {
{
id: 'call_small_file',
name: Constants.READ_FILE,
args: { file_path: '/mnt/data/sentinel.txt' },
args: { path: '/mnt/data/sentinel.txt' },
},
]);
@ -2733,7 +2733,7 @@ describe('createToolExecuteHandler', () => {
{
id: 'call_null_result',
name: Constants.READ_FILE,
args: { file_path: '/mnt/data/missing.txt' },
args: { path: '/mnt/data/missing.txt' },
},
]);
@ -2764,7 +2764,7 @@ describe('createToolExecuteHandler', () => {
{
id: 'call_png',
name: Constants.READ_FILE,
args: { file_path: '/mnt/data/simple_graph.png' },
args: { path: '/mnt/data/simple_graph.png' },
codeSessionContext: { session_id: 'sess-Z' },
} as unknown as ToolCallRequest,
]);
@ -2789,7 +2789,7 @@ describe('createToolExecuteHandler', () => {
{
id: 'call_zip',
name: Constants.READ_FILE,
args: { file_path: '/mnt/data/archive.zip' },
args: { path: '/mnt/data/archive.zip' },
},
]);
@ -2813,7 +2813,7 @@ describe('createToolExecuteHandler', () => {
{
id: 'call_uppercase',
name: Constants.READ_FILE,
args: { file_path: '/mnt/data/CHART.PNG' },
args: { path: '/mnt/data/CHART.PNG' },
},
]);
@ -2840,7 +2840,7 @@ describe('createToolExecuteHandler', () => {
{
id: 'call_nul_sniff',
name: Constants.READ_FILE,
args: { file_path: '/mnt/data/mystery_file' },
args: { path: '/mnt/data/mystery_file' },
},
]);
@ -2864,7 +2864,7 @@ describe('createToolExecuteHandler', () => {
{
id: 'call_text',
name: Constants.READ_FILE,
args: { file_path: '/mnt/data/notes.txt' },
args: { path: '/mnt/data/notes.txt' },
},
]);
@ -2888,7 +2888,7 @@ describe('createToolExecuteHandler', () => {
{
id: 'call_dotted_dir',
name: Constants.READ_FILE,
args: { file_path: '/mnt/data/proj.v1/notes' },
args: { path: '/mnt/data/proj.v1/notes' },
},
]);
@ -2916,7 +2916,7 @@ describe('createToolExecuteHandler', () => {
{
id: 'call_svg',
name: Constants.READ_FILE,
args: { file_path: '/mnt/data/icon.svg' },
args: { path: '/mnt/data/icon.svg' },
},
]);

View file

@ -1374,10 +1374,10 @@ function isSandboxMissingFileError(error: unknown): boolean {
function invalidSandboxAuthoringPath(filePath: string): string | null {
if (filePath.length === 0) {
return 'file_path is required';
return 'path is required';
}
if (filePath.includes('\0')) {
return 'file_path cannot contain NUL bytes';
return 'path cannot contain NUL bytes';
}
if (filePath.endsWith('/')) {
return `File path "${filePath}" points to a directory. Provide a file path.`;
@ -2270,9 +2270,9 @@ async function handleCreateFileCall(
sourceConfigurable?: Record<string, unknown>,
sandboxContext?: SandboxSessionContext,
): AuthoringResult {
const args = tc.args as { file_path?: unknown; content?: unknown; overwrite?: unknown };
if (typeof args.file_path !== 'string' || args.file_path.length === 0) {
return errorResult(tc, 'file_path is required');
const args = tc.args as { path?: unknown; content?: unknown; overwrite?: unknown };
if (typeof args.path !== 'string' || args.path.length === 0) {
return errorResult(tc, 'path is required');
}
if (typeof args.content !== 'string') {
return errorResult(tc, 'content is required');
@ -2282,25 +2282,25 @@ async function handleCreateFileCall(
}
const overwrite = args.overwrite === true;
if (!args.file_path.startsWith(SKILL_FILE_PREFIX)) {
if (!args.path.startsWith(SKILL_FILE_PREFIX)) {
if (mergedConfigurable?.codeEnvAvailable !== true) {
return errorResult(
tc,
`Path "${args.file_path}" is not a skill file, and this agent does not have code execution enabled.`,
`Path "${args.path}" is not a skill file, and this agent does not have code execution enabled.`,
);
}
return await handleSandboxCreateFileCall({
tc,
options,
req,
filePath: args.file_path,
filePath: args.path,
content: args.content,
overwrite,
sandboxContext,
});
}
const parsed = parseSkillAuthoringPath(args.file_path);
const parsed = parseSkillAuthoringPath(args.path);
if (typeof parsed === 'string') {
return errorResult(tc, parsed);
}
@ -2378,13 +2378,13 @@ async function handleEditFileCall(
sandboxContext?: SandboxSessionContext,
): AuthoringResult {
const args = tc.args as {
file_path?: unknown;
path?: unknown;
old_text?: unknown;
new_text?: unknown;
edits?: unknown;
};
if (typeof args.file_path !== 'string' || args.file_path.length === 0) {
return errorResult(tc, 'file_path is required');
if (typeof args.path !== 'string' || args.path.length === 0) {
return errorResult(tc, 'path is required');
}
const edits = normalizeEditArgs(args);
@ -2392,24 +2392,24 @@ async function handleEditFileCall(
return errorResult(tc, edits);
}
if (!args.file_path.startsWith(SKILL_FILE_PREFIX)) {
if (!args.path.startsWith(SKILL_FILE_PREFIX)) {
if (mergedConfigurable?.codeEnvAvailable !== true) {
return errorResult(
tc,
`Path "${args.file_path}" is not a skill file, and this agent does not have code execution enabled.`,
`Path "${args.path}" is not a skill file, and this agent does not have code execution enabled.`,
);
}
return await handleSandboxEditFileCall({
tc,
options,
req,
filePath: args.file_path,
filePath: args.path,
edits,
sandboxContext,
});
}
const parsed = parseSkillAuthoringPath(args.file_path);
const parsed = parseSkillAuthoringPath(args.path);
if (typeof parsed === 'string') {
return errorResult(tc, parsed);
}
@ -2511,13 +2511,13 @@ async function handleReadFileCall(
): Promise<ToolExecuteResult> {
const { getSkillByName, getSkillFileByPath, getStrategyFunctions, updateSkillFileContent } =
options;
const args = tc.args as { file_path?: string };
if (!args.file_path) {
const args = tc.args as { path?: string };
if (!args.path) {
return {
toolCallId: tc.id,
status: 'error',
content: '',
errorMessage: 'file_path is required',
errorMessage: 'path is required',
};
}
@ -2529,23 +2529,23 @@ async function handleReadFileCall(
* reference (skill paths are relative `{skillName}/...`), and consulting
* `getSkillByName` would just burn a DB round-trip on a guaranteed miss.
*/
if (args.file_path.startsWith('/mnt/data/')) {
if (args.path.startsWith('/mnt/data/')) {
if (codeEnvAvailable) {
return handleSandboxFileFallback(tc, args.file_path, options, req);
return handleSandboxFileFallback(tc, args.path, options, req);
}
return {
toolCallId: tc.id,
status: 'error',
content: '',
errorMessage: `Path "${args.file_path}" is a code-execution sandbox path, but this agent does not have code execution enabled.`,
errorMessage: `Path "${args.path}" is a code-execution sandbox path, but this agent does not have code execution enabled.`,
};
}
let skillName: string;
let relativePath: string;
const explicitSkillNamespace = args.file_path.startsWith(SKILL_FILE_PREFIX);
const explicitSkillNamespace = args.path.startsWith(SKILL_FILE_PREFIX);
if (explicitSkillNamespace) {
const parsed = parseSkillAuthoringPath(args.file_path);
const parsed = parseSkillAuthoringPath(args.path);
if (typeof parsed === 'string') {
return {
toolCallId: tc.id,
@ -2557,21 +2557,21 @@ async function handleReadFileCall(
skillName = parsed.skillName;
relativePath = parsed.relativePath;
} else {
const slashIdx = args.file_path.indexOf('/');
const slashIdx = args.path.indexOf('/');
if (slashIdx < 1) {
if (codeEnvAvailable) {
return handleSandboxFileFallback(tc, args.file_path, options, req);
return handleSandboxFileFallback(tc, args.path, options, req);
}
return {
toolCallId: tc.id,
status: 'error',
content: '',
errorMessage: `Invalid file path "${args.file_path}". Use format: {skillName}/{path}`,
errorMessage: `Invalid file path "${args.path}". Use format: {skillName}/{path}`,
};
}
skillName = args.file_path.slice(0, slashIdx);
relativePath = args.file_path.slice(slashIdx + 1);
skillName = args.path.slice(0, slashIdx);
relativePath = args.path.slice(slashIdx + 1);
if (!relativePath) {
/**
* `read_file("output/")`: a malformed-but-unambiguously-not-a-skill
@ -2580,7 +2580,7 @@ async function handleReadFileCall(
* dead-ending with a skill-centric error message.
*/
if (codeEnvAvailable) {
return handleSandboxFileFallback(tc, args.file_path, options, req);
return handleSandboxFileFallback(tc, args.path, options, req);
}
return {
toolCallId: tc.id,
@ -2642,7 +2642,7 @@ async function handleReadFileCall(
*/
if (!skillsEffectivelyEnabled) {
if (codeEnvAvailable && !explicitSkillNamespace) {
return handleSandboxFileFallback(tc, args.file_path, options, req);
return handleSandboxFileFallback(tc, args.path, options, req);
}
return {
toolCallId: tc.id,
@ -2681,7 +2681,7 @@ async function handleReadFileCall(
const recovered = await recoverAuthorSkill();
if (!recovered) {
if (codeEnvAvailable && !explicitSkillNamespace) {
return handleSandboxFileFallback(tc, args.file_path, options, req);
return handleSandboxFileFallback(tc, args.path, options, req);
}
return {
toolCallId: tc.id,
@ -2763,7 +2763,7 @@ async function handleReadFileCall(
return {
toolCallId: tc.id,
status: 'success',
content: `File: ${args.file_path}\n\n${addLineNumbers(skill.body)}`,
content: `File: ${args.path}\n\n${addLineNumbers(skill.body)}`,
};
}
@ -2794,7 +2794,7 @@ async function handleReadFileCall(
return {
toolCallId: tc.id,
status: 'success',
content: `Binary file (${file.mimeType}, ${file.bytes} bytes). Use bash to process: /mnt/data/${args.file_path}`,
content: `Binary file (${file.mimeType}, ${file.bytes} bytes). Use bash to process: /mnt/data/${args.path}`,
};
}
}
@ -2804,7 +2804,7 @@ async function handleReadFileCall(
return {
toolCallId: tc.id,
status: 'success',
content: `File: ${args.file_path} (${file.bytes} bytes)\n\n${addLineNumbers(file.content)}`,
content: `File: ${args.path} (${file.bytes} bytes)\n\n${addLineNumbers(file.content)}`,
};
}
@ -2814,14 +2814,14 @@ async function handleReadFileCall(
return {
toolCallId: tc.id,
status: 'success',
content: `File "${args.file_path}" is too large to read directly (${file.bytes} bytes, limit: ${MAX_READABLE_BYTES}). Invoke the skill first, then use bash to read it at /mnt/data/${args.file_path}.`,
content: `File "${args.path}" is too large to read directly (${file.bytes} bytes, limit: ${MAX_READABLE_BYTES}). Invoke the skill first, then use bash to read it at /mnt/data/${args.path}.`,
};
}
if (isImage && file.bytes > MAX_BINARY_BYTES) {
return {
toolCallId: tc.id,
status: 'success',
content: `File too large (${file.bytes} bytes, limit: ${MAX_BINARY_BYTES}). Use bash to process: /mnt/data/${args.file_path}`,
content: `File too large (${file.bytes} bytes, limit: ${MAX_BINARY_BYTES}). Use bash to process: /mnt/data/${args.path}`,
};
}
@ -2865,7 +2865,7 @@ async function handleReadFileCall(
return {
toolCallId: tc.id,
status: 'success',
content: `File "${args.file_path}" exceeded streaming limit (${streamLimit} bytes). Invoke the skill first, then use bash to read it at /mnt/data/${args.file_path}.`,
content: `File "${args.path}" exceeded streaming limit (${streamLimit} bytes). Invoke the skill first, then use bash to read it at /mnt/data/${args.path}.`,
};
}
chunks.push(chunk);
@ -2903,7 +2903,7 @@ async function handleReadFileCall(
return {
toolCallId: tc.id,
status: 'success',
content: `Image: ${args.file_path} (${buffer.length} bytes, ${file.mimeType})`,
content: `Image: ${args.path} (${buffer.length} bytes, ${file.mimeType})`,
artifact: {
content: [
{ type: 'image_url', image_url: { url: `data:${file.mimeType};base64,${base64}` } },
@ -2919,7 +2919,7 @@ async function handleReadFileCall(
return {
toolCallId: tc.id,
status: 'success',
content: `Binary file (${file.mimeType}, ${buffer.length} bytes). Use bash to process: /mnt/data/${args.file_path}`,
content: `Binary file (${file.mimeType}, ${buffer.length} bytes). Use bash to process: /mnt/data/${args.path}`,
};
}
@ -2941,14 +2941,14 @@ async function handleReadFileCall(
return {
toolCallId: tc.id,
status: 'success',
content: `File too large (${buffer.length} bytes, limit: ${MAX_READABLE_BYTES}). Use bash: cat /mnt/data/${args.file_path}`,
content: `File too large (${buffer.length} bytes, limit: ${MAX_READABLE_BYTES}). Use bash: cat /mnt/data/${args.path}`,
};
}
return {
toolCallId: tc.id,
status: 'success',
content: `File: ${args.file_path} (${buffer.length} bytes)\n\n${addLineNumbers(text)}`,
content: `File: ${args.path} (${buffer.length} bytes)\n\n${addLineNumbers(text)}`,
};
} catch (error) {
return {
@ -3126,16 +3126,16 @@ function getFileAuthoringQueueKey(
if (!isHostFileAuthoringToolCall(tc.name, mergedConfigurable)) {
return undefined;
}
const args = tc.args as { file_path?: unknown };
if (typeof args.file_path !== 'string' || args.file_path.length === 0) {
const args = tc.args as { path?: unknown };
if (typeof args.path !== 'string' || args.path.length === 0) {
return undefined;
}
if (!args.file_path.startsWith(SKILL_FILE_PREFIX)) {
return `sandbox:${args.file_path}`;
if (!args.path.startsWith(SKILL_FILE_PREFIX)) {
return `sandbox:${args.path}`;
}
const parsed = parseSkillAuthoringPath(args.file_path);
const parsed = parseSkillAuthoringPath(args.path);
if (typeof parsed === 'string') {
return `skill:${args.file_path}`;
return `skill:${args.path}`;
}
return `skill:${parsed.skillName}`;
}
@ -3199,8 +3199,8 @@ export function createToolExecuteHandler(options: ToolExecuteOptions): EventHand
);
const isSandboxFileAuthoringCall =
isFileAuthoringCall &&
typeof (tc.args as { file_path?: unknown }).file_path === 'string' &&
!(tc.args as { file_path: string }).file_path.startsWith(SKILL_FILE_PREFIX);
typeof (tc.args as { path?: unknown }).path === 'string' &&
!(tc.args as { path: string }).path.startsWith(SKILL_FILE_PREFIX);
if (
tc.name === Constants.SKILL_TOOL ||
tc.name === Constants.READ_FILE ||

View file

@ -12,7 +12,7 @@ jest.mock('@librechat/agents', () => ({
parameters: {
type: 'object',
properties: {
file_path: {
path: {
type: 'string',
description: 'For skill files: "{skillName}/{path}".',
},
@ -55,9 +55,9 @@ const TOOL_DESCRIPTION_ADVISORY_MAX_LENGTH = 1024;
function filePathDescription(tool?: LCTool): string {
const parameters = tool?.parameters as
| { properties?: { file_path?: { description?: string } } }
| { properties?: { path?: { description?: string } } }
| undefined;
return parameters?.properties?.file_path?.description ?? '';
return parameters?.properties?.path?.description ?? '';
}
function maxToolDescriptionLength(definitions: LCTool[]): number {

View file

@ -140,13 +140,13 @@ Use for text, CSV, JSON, Markdown, logs, and small source files at paths returne
const CODE_READ_FILE_PARAMETERS: LCTool['parameters'] = Object.freeze({
type: 'object',
properties: {
file_path: {
path: {
type: 'string',
description:
'Path to a file from code execution output, such as "/mnt/data/result.csv" or another path returned by the execution tool.',
},
},
required: ['file_path'],
required: ['path'],
}) as LCTool['parameters'];
const CODE_READ_FILE_DEF: LCTool = Object.freeze({
@ -159,7 +159,7 @@ const CODE_READ_FILE_DEF: LCTool = Object.freeze({
const SKILL_CREATE_FILE_PARAMETERS: LCTool['parameters'] = Object.freeze({
type: 'object',
properties: {
file_path: {
path: {
type: 'string',
description:
'Path to write. Use "skills/{skillName}/..." for skill files when available, or a code-execution sandbox path such as "/mnt/data/result.txt" when code execution is enabled. For SKILL.md, the YAML frontmatter name must match {skillName}.',
@ -174,13 +174,13 @@ const SKILL_CREATE_FILE_PARAMETERS: LCTool['parameters'] = Object.freeze({
default: false,
},
},
required: ['file_path', 'content'],
required: ['path', 'content'],
}) as LCTool['parameters'];
const CODE_CREATE_FILE_PARAMETERS: LCTool['parameters'] = Object.freeze({
type: 'object',
properties: {
file_path: {
path: {
type: 'string',
description:
'Path to write in the code-execution sandbox, such as "/mnt/data/result.txt". Prefer /mnt/data/{file} for files that should remain available to later sandbox calls.',
@ -195,13 +195,13 @@ const CODE_CREATE_FILE_PARAMETERS: LCTool['parameters'] = Object.freeze({
default: false,
},
},
required: ['file_path', 'content'],
required: ['path', 'content'],
}) as LCTool['parameters'];
const SKILL_EDIT_FILE_PARAMETERS: LCTool['parameters'] = Object.freeze({
type: 'object',
properties: {
file_path: {
path: {
type: 'string',
description:
'Path to edit. Use "skills/{skillName}/..." for skill files when available, or a code-execution sandbox path such as "/mnt/data/result.txt" when code execution is enabled. edit_file cannot rename skills; keep SKILL.md frontmatter name equal to {skillName}.',
@ -227,13 +227,13 @@ const SKILL_EDIT_FILE_PARAMETERS: LCTool['parameters'] = Object.freeze({
},
},
},
required: ['file_path'],
required: ['path'],
}) as LCTool['parameters'];
const CODE_EDIT_FILE_PARAMETERS: LCTool['parameters'] = Object.freeze({
type: 'object',
properties: {
file_path: {
path: {
type: 'string',
description: 'Path to edit in the code-execution sandbox, such as "/mnt/data/result.txt".',
},
@ -258,7 +258,7 @@ const CODE_EDIT_FILE_PARAMETERS: LCTool['parameters'] = Object.freeze({
},
},
},
required: ['file_path'],
required: ['path'],
}) as LCTool['parameters'];
const SKILL_CREATE_FILE_DESCRIPTION = `Create a new file, or overwrite an existing file with explicit intent.

View file

@ -1630,8 +1630,11 @@ export class MCPConnection extends EventEmitter {
this.allowedAddresses,
);
/** Merge headers: SSE defaults < init headers < user headers (user wins) */
const fetchHeaders = new Headers(
Object.assign({}, SSE_REQUEST_HEADERS, resolvedInit?.headers, headers),
const fetchHeaders = Object.assign(
{},
SSE_REQUEST_HEADERS,
resolvedInit?.headers,
headers,
);
return undiciFetch(urlString, {
...resolvedInit,