📜 feat: Improve Skill Authoring Guidance (#13517)
Some checks failed
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
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
Publish `@librechat/client` to NPM / pack (push) Has been cancelled
Publish `@librechat/client` to NPM / publish-npm (push) Has been cancelled

* feat: Improve skill authoring guidance

* test: Guard tool description lengths

* fix: Align skill template guidance

* fix: Satisfy advisory limit test lint

* fix: Transform LangGraph ESM in Jest
This commit is contained in:
Danny Avila 2026-06-04 18:36:16 -04:00 committed by GitHub
parent 7d0feadc90
commit 44ed7864fb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 109 additions and 11 deletions

View file

@ -1,3 +1,13 @@
const esModules = [
'openid-client',
'oauth4webapi',
'jose',
'@langchain/langgraph',
'@langchain/langgraph-checkpoint',
'@langchain/langgraph-sdk',
'uuid',
].join('|');
module.exports = {
testEnvironment: 'node',
clearMocks: true,
@ -12,5 +22,13 @@ module.exports = {
'^openid-client/passport$': '<rootDir>/test/__mocks__/openid-client-passport.js',
'^openid-client$': '<rootDir>/test/__mocks__/openid-client.js',
},
transformIgnorePatterns: ['/node_modules/(?!(openid-client|oauth4webapi|jose)/).*/'],
transform: {
'\\.[jt]sx?$': [
'babel-jest',
{
presets: [['@babel/preset-env', { targets: { node: 'current' } }]],
},
],
},
transformIgnorePatterns: [`/node_modules/(?!(${esModules})/).*/`],
};

View file

@ -131,6 +131,7 @@
"zod": "^3.22.4"
},
"devDependencies": {
"@babel/preset-env": "^7.29.5",
"@types/sanitize-html": "^2.13.0",
"jest": "^30.2.0",
"mongodb-memory-server": "^11.0.1",

1
package-lock.json generated
View file

@ -146,6 +146,7 @@
"zod": "^3.22.4"
},
"devDependencies": {
"@babel/preset-env": "^7.29.5",
"@types/sanitize-html": "^2.13.0",
"jest": "^30.2.0",
"mongodb-memory-server": "^11.0.1",

View file

@ -1,3 +1,10 @@
const esModules = [
'@langchain/langgraph',
'@langchain/langgraph-checkpoint',
'@langchain/langgraph-sdk',
'uuid',
].join('|');
export default {
collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}', '!<rootDir>/node_modules/'],
coveragePathIgnorePatterns: ['/node_modules/', '/dist/'],
@ -23,6 +30,7 @@ export default {
},
],
},
transformIgnorePatterns: [`/node_modules/(?!(${esModules})/).*/`],
moduleNameMapper: {
'^@src/(.*)$': '<rootDir>/src/$1',
'~/(.*)': '<rootDir>/src/$1',

View file

@ -5,7 +5,6 @@
* Mirrors the same pattern used in `__tests__/skills.test.ts`.
*/
jest.mock('@librechat/agents', () => ({
...jest.requireActual('@librechat/agents'),
CODE_EXECUTION_TOOLS: new Set(['execute_code', 'bash_tool']),
ReadFileToolDefinition: {
name: 'read_file',
@ -51,6 +50,9 @@ import {
isCodeSessionToolName,
} from './tools';
/** Portable ceiling for OpenAI-compatible tool description validators. */
const TOOL_DESCRIPTION_ADVISORY_MAX_LENGTH = 1024;
function filePathDescription(tool?: LCTool): string {
const parameters = tool?.parameters as
| { properties?: { file_path?: { description?: string } } }
@ -58,6 +60,13 @@ function filePathDescription(tool?: LCTool): string {
return parameters?.properties?.file_path?.description ?? '';
}
function maxToolDescriptionLength(definitions: LCTool[]): number {
return definitions.reduce((max, definition) => {
const length = definition.description?.length ?? Number.POSITIVE_INFINITY;
return Math.max(max, length);
}, 0);
}
describe('buildToolSet', () => {
describe('event-driven mode (toolDefinitions)', () => {
it('builds toolSet from toolDefinitions when available', () => {
@ -274,6 +283,30 @@ describe('registerCodeExecutionTools', () => {
const names = result.toolDefinitions.map((d) => d.name);
expect(names).toEqual(['calculator', 'read_file', 'bash_tool']);
});
it('keeps code-execution tool descriptions within provider advisory limits', () => {
const skillAwareWithRefs = registerCodeExecutionTools({
toolRegistry: makeRegistry(),
toolDefinitions: [],
includeBash: true,
includeSkillFileInstructions: true,
enableToolOutputReferences: true,
});
const codeOnlyWithoutRefs = registerCodeExecutionTools({
toolRegistry: makeRegistry(),
toolDefinitions: [],
includeBash: true,
includeSkillFileInstructions: false,
enableToolOutputReferences: false,
});
expect(
maxToolDescriptionLength([
...skillAwareWithRefs.toolDefinitions,
...codeOnlyWithoutRefs.toolDefinitions,
]),
).toBeLessThanOrEqual(TOOL_DESCRIPTION_ADVISORY_MAX_LENGTH);
});
});
describe('idempotence (second call in same run)', () => {
@ -469,7 +502,12 @@ describe('registerFileAuthoringTools', () => {
expect(result.toolDefinitions[0].responseFormat).toBe('content_and_artifact');
expect(result.toolDefinitions.map((d) => d.description).join('\n')).toContain('skills/');
expect(toolRegistry.get('create_file')?.description).toContain('frontmatter name must match');
expect(toolRegistry.get('create_file')?.description).toContain('trigger-friendly');
expect(toolRegistry.get('create_file')?.description).toContain('references/template.html');
expect(toolRegistry.get('create_file')?.description).toContain('templates/{file}');
expect(toolRegistry.get('edit_file')?.description).toContain('edit_file cannot rename skills');
expect(toolRegistry.get('edit_file')?.description).toContain('Keep SKILL.md concise');
expect(toolRegistry.get('edit_file')?.description).toContain('templates/');
expect(filePathDescription(toolRegistry.get('create_file'))).toContain(
'frontmatter name must match',
);
@ -537,6 +575,33 @@ describe('registerFileAuthoringTools', () => {
expect(toolRegistry.get('edit_file')?.description).toContain('skills/');
});
it('keeps file-authoring tool descriptions within provider advisory limits', () => {
const skillAware = registerFileAuthoringTools({
toolRegistry: makeRegistry(),
toolDefinitions: [],
includeSkillFileInstructions: true,
});
const codeOnlyRegistry = makeRegistry();
const codeOnly = registerFileAuthoringTools({
toolRegistry: codeOnlyRegistry,
toolDefinitions: [],
includeSkillFileInstructions: false,
});
const upgraded = registerFileAuthoringTools({
toolRegistry: codeOnlyRegistry,
toolDefinitions: codeOnly.toolDefinitions,
includeSkillFileInstructions: true,
});
expect(
maxToolDescriptionLength([
...skillAware.toolDefinitions,
...codeOnly.toolDefinitions,
...upgraded.toolDefinitions,
]),
).toBeLessThanOrEqual(TOOL_DESCRIPTION_ADVISORY_MAX_LENGTH);
});
it('distinguishes host file authoring definitions from user tools with matching names', () => {
const result = registerFileAuthoringTools({
toolRegistry: makeRegistry(),

View file

@ -263,17 +263,18 @@ const CODE_EDIT_FILE_PARAMETERS: LCTool['parameters'] = Object.freeze({
const SKILL_CREATE_FILE_DESCRIPTION = `Create a new file, or overwrite an existing file with explicit intent.
Use for new files and full rewrites where the change is larger than half the file. Requires overwrite: true to replace existing files. Refuses otherwise.
Use for new files and full rewrites. Requires overwrite: true to replace existing files.
Paths starting with "skills/" target the skill file system:
- skills/{skillName}/SKILL.md - the skill's main instruction file
- skills/{skillName}/references/{file} - supporting reference files
- skills/{skillName}/scripts/{file} - helper scripts
- skills/{skillName}/templates/{file} - output templates
Paths starting with "skills/" write skill files:
- skills/{skillName}/SKILL.md - main instructions; keep it lean with YAML frontmatter, trigger-friendly description, workflow steps, and short snippets.
- skills/{skillName}/references/{file} - long docs, schemas, examples, large templates, HTML/CSS/JS dashboards.
- skills/{skillName}/scripts/{file} - helper scripts.
- skills/{skillName}/assets/{file} - static assets.
- skills/{skillName}/templates/{file} - reusable output templates.
For skills/{skillName}/SKILL.md, YAML frontmatter name must match {skillName}. To use a different skill name, create a new skills/{newName}/SKILL.md instead.
For SKILL.md, frontmatter name must match {skillName}; create skills/{newName}/SKILL.md to rename. Put large runnable artifacts in bundled files such as references/template.html, and have SKILL.md tell the agent when to read or reuse them.
When code execution is enabled, non-skills paths target the code-execution sandbox. Prefer /mnt/data/{file} for files that should remain available to later sandbox calls.`;
Non-skills paths target the code-execution sandbox when enabled. Prefer /mnt/data/{file}.`;
const CODE_CREATE_FILE_DESCRIPTION = `Create a new file, or overwrite an existing file with explicit intent.
@ -285,7 +286,7 @@ const SKILL_EDIT_FILE_DESCRIPTION = `Apply targeted text replacements to an exis
Use for small, precise changes. Each old_text must match exactly one location. Tries exact match first; falls back to whitespace-tolerant matching if needed. Reports which matching strategy was used. Returns a unified diff.
For skills/{skillName}/SKILL.md, edit description, title, or body content, but keep YAML frontmatter name equal to {skillName}. edit_file cannot rename skills; create a new skills/{newName}/SKILL.md for a different skill name.
For skills/{skillName}/SKILL.md, edit description, title, or body content, but keep YAML frontmatter name equal to {skillName}. edit_file cannot rename skills; create a new skills/{newName}/SKILL.md for a different skill name. Keep SKILL.md concise; move large templates, HTML/CSS/JS dashboards, examples, schemas, and long docs into references/, scripts/, assets/, or templates/ files and point to them from SKILL.md.
Paths starting with "skills/" target the skill file system. When code execution is enabled, non-skills paths target the code-execution sandbox.`;

View file

@ -253,7 +253,9 @@ describe('skill validation helpers', () => {
'user-invocable': true,
effort: 5,
version: '1.0.0',
license: 'MIT',
hooks: { 'pre-run': 'echo hi' },
metadata: { owner: 'data-team' },
}),
).toEqual([]);
});

View file

@ -251,6 +251,7 @@ const ALLOWED_FRONTMATTER_KEYS = new Set<string>([
'shell',
'hooks',
'version',
'license',
'metadata',
]);
@ -277,6 +278,7 @@ const FRONTMATTER_KIND: Record<string, FrontmatterKind | FrontmatterKind[]> = {
paths: ['string', 'stringArray'],
shell: 'string',
version: 'string',
license: 'string',
};
function isPlainObject(value: unknown): value is Record<string, unknown> {