From 49083d0adfc5e230a547c8d89ad66386c042a4d1 Mon Sep 17 00:00:00 2001 From: dymux <91779374+putramkti@users.noreply.github.com> Date: Thu, 20 Aug 2026 11:04:09 +0700 Subject: [PATCH] =?UTF-8?q?=E2=9C=82=EF=B8=8F=20fix(agents):=20warn=20when?= =?UTF-8?q?=20skill=20descriptions=20are=20truncated=20in=20the=20model=20?= =?UTF-8?q?catalog=20(#14878)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../api/src/agents/__tests__/skills.test.ts | 32 +++++++++++++++++++ packages/api/src/agents/skills.ts | 19 ++++++++++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/packages/api/src/agents/__tests__/skills.test.ts b/packages/api/src/agents/__tests__/skills.test.ts index 87c849fc8f..6d48466921 100644 --- a/packages/api/src/agents/__tests__/skills.test.ts +++ b/packages/api/src/agents/__tests__/skills.test.ts @@ -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); diff --git a/packages/api/src/agents/skills.ts b/packages/api/src/agents/skills.ts index f12fcf75e0..44495f120d 100644 --- a/packages/api/src/agents/skills.ts +++ b/packages/api/src/agents/skills.ts @@ -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