-
-
Notifications
You must be signed in to change notification settings - Fork 7.5k
🎬 feat: Prime Manually-Invoked Skills via $ Popover #12709
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 7 commits
a77f56a
480e1fa
df865e2
1b62e2f
fac0973
ae0ed41
a83fa9d
6825378
cff9188
f659243
6939030
1f81a43
2f5347b
067a98a
3bbcb94
94f7299
9de3a4b
cfba20a
eb2826c
0d2c944
101d7b0
49f84fb
af8265f
7dd6bcf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,6 +28,9 @@ const { | |
| filterMalformedContentParts, | ||
| countFormattedMessageTokens, | ||
| hydrateMissingIndexTokenCounts, | ||
| injectManualSkillPrimes, | ||
| isSkillPrimeMessage, | ||
| buildSkillPrimeContentParts, | ||
| } = require('@librechat/api'); | ||
| const { | ||
| Callback, | ||
|
|
@@ -603,18 +606,27 @@ class AgentClient extends BaseClient { | |
| const memoryConfig = appConfig.memory; | ||
| const messageWindowSize = memoryConfig?.messageWindowSize ?? 5; | ||
|
|
||
| let messagesToProcess = [...messages]; | ||
| if (messages.length > messageWindowSize) { | ||
| for (let i = messages.length - messageWindowSize; i >= 0; i--) { | ||
| const potentialWindow = messages.slice(i, i + messageWindowSize); | ||
| /** | ||
| * Strip skill-primed meta messages before memory extraction. The primes | ||
| * sit next to the latest user message and carry large SKILL.md bodies, | ||
| * so letting them into the window would crowd out real chat turns and | ||
| * pollute extracted memories with synthetic instruction content the | ||
| * user never typed. | ||
| */ | ||
| const chatMessages = messages.filter((m) => !isSkillPrimeMessage(m)); | ||
|
|
||
| let messagesToProcess = [...chatMessages]; | ||
| if (chatMessages.length > messageWindowSize) { | ||
| for (let i = chatMessages.length - messageWindowSize; i >= 0; i--) { | ||
| const potentialWindow = chatMessages.slice(i, i + messageWindowSize); | ||
| if (potentialWindow[0]?.role === 'user') { | ||
| messagesToProcess = [...potentialWindow]; | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| if (messagesToProcess.length === messages.length) { | ||
| messagesToProcess = [...messages.slice(-messageWindowSize)]; | ||
| if (messagesToProcess.length === chatMessages.length) { | ||
| messagesToProcess = [...chatMessages.slice(-messageWindowSize)]; | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -759,6 +771,32 @@ class AgentClient extends BaseClient { | |
| `[AgentClient] Boundary token adjustment: ${boundaryTokenAdjustment.original} → ${boundaryTokenAdjustment.adjusted} (${boundaryTokenAdjustment.remainingChars}/${boundaryTokenAdjustment.totalChars} chars)`, | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Phase 3 manual skill priming — injected by user via `$` popover. | ||
| * | ||
| * Splice + index-shift logic lives in `injectManualSkillPrimes` | ||
| * (packages/api/src/agents/skills.ts) so the delicate position math | ||
| * can be unit-tested in TS without standing up AgentClient. Runs for | ||
| * both single-agent and multi-agent runs; how primes interact with | ||
| * handoff / added-convo agents' per-agent state is an agents-SDK | ||
| * concern, not this layer's to gate. | ||
| */ | ||
| const manualSkillPrimes = this.options.agent?.manualSkillPrimes; | ||
| if (manualSkillPrimes && manualSkillPrimes.length > 0) { | ||
| const primeResult = injectManualSkillPrimes({ | ||
| initialMessages, | ||
| indexTokenCountMap, | ||
| manualSkillPrimes, | ||
| }); | ||
|
Comment on lines
+787
to
+791
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Injecting Useful? React with 👍 / 👎. |
||
| indexTokenCountMap = primeResult.indexTokenCountMap; | ||
| if (primeResult.inserted > 0) { | ||
| logger.debug( | ||
| `[AgentClient] Primed ${primeResult.inserted} manual skill(s) at message index ${primeResult.insertIdx}: ${manualSkillPrimes.map((p) => p.name).join(', ')}`, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| if (indexTokenCountMap && isEnabled(process.env.AGENT_DEBUG_LOGGING)) { | ||
| const entries = Object.entries(indexTokenCountMap); | ||
| const perMsg = entries.map(([idx, count]) => { | ||
|
|
@@ -875,6 +913,28 @@ class AgentClient extends BaseClient { | |
| const hideSequentialOutputs = config.configurable.hide_sequential_outputs; | ||
| await runAgents(initialMessages); | ||
|
|
||
| /** | ||
| * Surface a completed `skill` tool_call content part per manually- | ||
| * invoked skill so the existing `SkillCall` frontend renderer shows | ||
| * a "Skill X loaded" card on the assistant response. Applied after | ||
| * the graph finishes to avoid clashing with the aggregator's own | ||
| * per-step content indexing. Prepended (not appended) so the cards | ||
| * render ahead of the model's output — matching the turn semantics: | ||
| * priming ran first, the model's reply followed. | ||
| * | ||
| * Persistence and final-event reconcile piggyback on the existing | ||
| * pipeline: `sendCompletion` reads `this.contentParts` verbatim, so | ||
| * the cards land in the saved response message and the frontend | ||
| * picks them up via the final SSE event. | ||
| */ | ||
| const primedSkills = this.options.agent?.manualSkillPrimes; | ||
| if (primedSkills && primedSkills.length > 0) { | ||
| const primeParts = buildSkillPrimeContentParts(primedSkills, { | ||
| runId: this.responseMessageId ?? 'manual-skill', | ||
| }); | ||
| this.contentParts.unshift(...primeParts); | ||
|
Comment on lines
+933
to
+938
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Prepending these synthetic Useful? React with 👍 / 👎. |
||
| } | ||
|
|
||
| /** @deprecated Agent Chain */ | ||
| if (hideSequentialOutputs) { | ||
| this.contentParts = this.contentParts.filter((part, index) => { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,6 +19,7 @@ const { | |
| validateRequest, | ||
| initializeAgent, | ||
| getBalanceConfig, | ||
| extractManualSkills, | ||
| createErrorResponse, | ||
| recordCollectedUsage, | ||
| getTransactionsConfig, | ||
|
|
@@ -237,6 +238,8 @@ const OpenAIChatCompletionController = async (req, res) => { | |
| accessibleSkillIds, | ||
| }); | ||
|
|
||
| const manualSkills = extractManualSkills(req.body); | ||
|
|
||
| const primaryConfig = await initializeAgent( | ||
| { | ||
| req, | ||
|
|
@@ -256,6 +259,7 @@ const OpenAIChatCompletionController = async (req, res) => { | |
| codeEnvAvailable: enabledCapabilities.has(AgentCapabilities.execute_code), | ||
| skillStates, | ||
| defaultActiveOnShare, | ||
| manualSkills, | ||
| }, | ||
|
Comment on lines
260
to
264
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This path now resolves Useful? React with 👍 / 👎. |
||
| { | ||
| getConvoFiles: db.getConvoFiles, | ||
|
|
@@ -268,6 +272,7 @@ const OpenAIChatCompletionController = async (req, res) => { | |
| getToolFilesByIds: db.getToolFilesByIds, | ||
| getCodeGeneratedFiles: db.getCodeGeneratedFiles, | ||
| listSkillsByAccess: db.listSkillsByAccess, | ||
| getSkillByName: db.getSkillByName, | ||
| }, | ||
| ); | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This injects
manualSkillPrimesinto the sharedinitialMessagesarray beforerunAgentsfans out to[primary, ...agentConfigs], so added/handoff agents receive the primary agent’s SKILL.md bodies too. In multi-agent or added-convo runs, that leaks/bleeds skill instructions across agent boundaries even when those agents were never scoped to or selected for the skill.Useful? React with 👍 / 👎.