✂️ fix: Deduplicate Skill Bodies Across Fresh Primes and History (#13610)

When a skill is primed fresh this turn (manual $-popover or always-apply) AND
also appears in history as a `skill` tool_call, its SKILL.md body was injected
twice — once by injectSkillPrimes and once reconstructed by formatAgentMessages.

- add `collectFreshSkillPrimeNames` helper (packages/api) — union of manual +
  always-apply prime names
- client.js: pass the set as `skipSkillBodyNames` to formatAgentMessages for
  both the initialMessages and memoryMessages paths so the body reconstructs
  once. Names not primed this turn still reconstruct (sticky manual re-prime).

Requires `@librechat/agents` with `skipSkillBodyNames` support; the published
dist silently ignores the unknown option until upgraded.
This commit is contained in:
Danny Avila 2026-06-09 17:16:24 -04:00 committed by GitHub
parent d7fc4a73a3
commit 793cbd49f0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 98 additions and 3 deletions

View file

@ -45,6 +45,7 @@ import {
resolveAlwaysApplySkills,
injectManualSkillPrimes,
injectSkillPrimes,
collectFreshSkillPrimeNames,
extractManualSkills,
isSkillPrimeMessage,
buildSkillPrimeContentParts,
@ -2247,3 +2248,45 @@ describe('injectSkillPrimes', () => {
expect(result.inserted).toBe(2);
});
});
describe('collectFreshSkillPrimeNames', () => {
it('returns the union of manual + always-apply prime names', () => {
const names = collectFreshSkillPrimeNames({
manualSkillPrimes: [{ name: 'pdf-analyzer' }, { name: 'code-review' }],
alwaysApplySkillPrimes: [{ name: 'clickhouse-best-practices' }],
});
expect(names).toEqual(new Set(['pdf-analyzer', 'code-review', 'clickhouse-best-practices']));
});
it('dedupes a skill that is both manual and always-apply', () => {
const names = collectFreshSkillPrimeNames({
manualSkillPrimes: [{ name: 'clickhouse-best-practices' }],
alwaysApplySkillPrimes: [{ name: 'clickhouse-best-practices' }],
});
expect(names.size).toBe(1);
expect(names.has('clickhouse-best-practices')).toBe(true);
});
it('handles undefined / empty inputs', () => {
expect(collectFreshSkillPrimeNames({}).size).toBe(0);
expect(
collectFreshSkillPrimeNames({
manualSkillPrimes: [],
alwaysApplySkillPrimes: [],
}).size,
).toBe(0);
});
it('collects names from only one side when the other is absent', () => {
expect(
collectFreshSkillPrimeNames({
alwaysApplySkillPrimes: [{ name: 'only-always' }],
}),
).toEqual(new Set(['only-always']));
expect(
collectFreshSkillPrimeNames({
manualSkillPrimes: [{ name: 'only-manual' }],
}),
).toEqual(new Set(['only-manual']));
});
});

View file

@ -1087,6 +1087,32 @@ export function injectManualSkillPrimes(
return { initialMessages, indexTokenCountMap, inserted: numPrimes, insertIdx };
}
/**
* Collects the set of skill names primed fresh this turn the union of manual
* ($-popover) and always-apply (frontmatter) primes. Passed to
* `formatAgentMessages` as `skipSkillBodyNames` so a skill that is BOTH primed
* this turn AND present in history (as a `skill` tool_call) has its SKILL.md
* body injected exactly once by the fresh prime via `injectSkillPrimes` and
* is NOT also reconstructed from history. Names absent from this set still
* reconstruct from history, preserving sticky manual re-priming across turns.
*/
export function collectFreshSkillPrimeNames({
manualSkillPrimes,
alwaysApplySkillPrimes,
}: {
manualSkillPrimes?: Pick<ResolvedManualSkill, 'name'>[];
alwaysApplySkillPrimes?: Pick<ResolvedAlwaysApplySkill, 'name'>[];
}): Set<string> {
const names = new Set<string>();
for (const prime of manualSkillPrimes ?? []) {
names.add(prime.name);
}
for (const prime of alwaysApplySkillPrimes ?? []) {
names.add(prime.name);
}
return names;
}
export interface InjectSkillPrimesParams {
/** Formatted LangChain messages produced by `formatAgentMessages`. Mutated in place. */
initialMessages: BaseMessage[];