✂️ fix(agents): warn when skill descriptions are truncated in the model catalog (#14878)

formatSkillCatalog caps every catalog entry at 250 characters and truncates
silently, while the import validator allows descriptions up to 1024 chars.
Skill authors therefore get no signal that the trigger phrases at the end
of a long description will never reach the model, and the skill silently
stops firing on the prompts it was written for.

Log a warning per truncated skill so operators can see which descriptions
need tightening, and pass the cap explicitly so the warning and the
catalog stay in sync if @librechat/agents changes its default.

Closes #14657

Co-authored-by: dymux <putramkti@users.noreply.github.com>
This commit is contained in:
dymux 2026-08-20 11:04:09 +07:00 committed by GitHub
parent 16e4d14191
commit 49083d0adf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 50 additions and 1 deletions

View file

@ -897,6 +897,38 @@ describe('injectSkillCatalog', () => {
expect(agent.additional_instructions).toContain('desc-my-skill');
});
it('warns when a skill description exceeds the catalog entry cap', async () => {
const { logger } = await import('@librechat/data-schemas');
const warnSpy = jest.spyOn(logger, 'warn');
const longDesc = 'x'.repeat(400);
const longSkill: PageSkill = {
...makeSkill('long-skill', userObjectId),
description: longDesc,
};
const shortSkill = makeSkill('short-skill', userObjectId);
const listSkillsByAccess = buildPager([[longSkill, shortSkill]]);
const agent = makeAgent();
await injectSkillCatalog(baseParams({ listSkillsByAccess, agent }));
const truncWarns = warnSpy.mock.calls
.map((call) => String(call[0]))
.filter((msg) => msg.includes('truncated to'));
expect(truncWarns).toHaveLength(1);
expect(truncWarns[0]).toContain('"long-skill"');
expect(truncWarns[0]).toContain('was 400');
/* Short description is not flagged. */
expect(
warnSpy.mock.calls
.map((call) => String(call[0]))
.filter((msg) => msg.includes('"short-skill"') && msg.includes('truncated')),
).toHaveLength(0);
/* The catalog still reaches the model — the warning is additive. */
expect(agent.additional_instructions).toContain('long-skill');
expect(agent.additional_instructions).toContain('short-skill');
warnSpy.mockRestore();
});
it('honors a configured maxCatalogSkills below the default hard limit', async () => {
const first = makeSkill('first-skill', userObjectId);
const second = makeSkill('second-skill', userObjectId);

View file

@ -89,6 +89,13 @@ const MIN_SKILL_CATALOG_LIMIT = 1;
const MAX_CATALOG_PAGES = 10;
/** Page size used when paginating to fill the active-skill quota. */
const CATALOG_PAGE_SIZE = 100;
/**
* Per-entry description cap applied by `formatSkillCatalog` before the
* catalog is injected into agent context. `@librechat/agents` truncates
* silently, so this mirrors the default so we can warn when authors' skill
* descriptions will not reach the model verbatim.
*/
const SKILL_CATALOG_MAX_ENTRY_CHARS = 250;
/** Hard ceiling on skill names a model spec can request by config. */
const MAX_MODEL_SPEC_SKILLS = SKILL_CATALOG_LIMIT;
/**
@ -620,9 +627,19 @@ export async function injectSkillCatalog(
* and those reads would otherwise be impossible.
*/
if (catalogVisibleSkills.length > 0) {
for (const s of catalogVisibleSkills) {
if (s.description.length > SKILL_CATALOG_MAX_ENTRY_CHARS) {
logger.warn(
`[injectSkillCatalog] skill "${s.name}" description truncated to ${SKILL_CATALOG_MAX_ENTRY_CHARS} chars for the model catalog (was ${s.description.length})`,
);
}
}
const catalog = formatSkillCatalog(
catalogVisibleSkills.map((s) => ({ name: s.name, description: s.description })),
{ contextWindowTokens: contextWindowTokens || 200_000 },
{
contextWindowTokens: contextWindowTokens || 200_000,
maxEntryChars: SKILL_CATALOG_MAX_ENTRY_CHARS,
},
);
if (catalog) {
agent.additional_instructions = agent.additional_instructions