-
-
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 all 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
+785
to
+790
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 injects Useful? React with 👍 / 👎. |
||
| }); | ||
|
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,31 @@ 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 cards sit | ||
| * above the model's output — priming ran before the turn, the | ||
| * reply follows. | ||
| * | ||
| * Live streaming display of cards is handled on the user side via | ||
| * `ManualSkillPills` reading the message's `manualSkills` field; | ||
| * no separate SSE emit is needed here, and trying to stream a | ||
| * mid-run tool_call at index 0 collided with the LLM's first text | ||
| * content, while emitting at a sparse offset pushed the card below | ||
| * the reply on finalize. Post-run unshift keeps the final | ||
| * responseMessage.content in the right order. | ||
| */ | ||
| const primedSkills = this.options.agent?.manualSkillPrimes; | ||
| if (primedSkills && primedSkills.length > 0) { | ||
| const primeParts = buildSkillPrimeContentParts(primedSkills, { | ||
| runId: this.responseMessageId ?? 'manual-skill', | ||
| }); | ||
| this.contentParts.unshift(...primeParts); | ||
| } | ||
|
|
||
| /** @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, | ||
|
|
@@ -27,6 +28,7 @@ const { | |
| buildNonStreamingResponse, | ||
| createOpenAIStreamTracker, | ||
| createOpenAIContentAggregator, | ||
| injectManualSkillPrimes, | ||
| isChatCompletionValidationFailure, | ||
| } = require('@librechat/api'); | ||
| const { | ||
|
|
@@ -237,6 +239,8 @@ const OpenAIChatCompletionController = async (req, res) => { | |
| accessibleSkillIds, | ||
| }); | ||
|
|
||
| const manualSkills = extractManualSkills(req.body); | ||
|
|
||
| const primaryConfig = await initializeAgent( | ||
| { | ||
| req, | ||
|
|
@@ -256,6 +260,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 +273,7 @@ const OpenAIChatCompletionController = async (req, res) => { | |
| getToolFilesByIds: db.getToolFilesByIds, | ||
| getCodeGeneratedFiles: db.getCodeGeneratedFiles, | ||
| listSkillsByAccess: db.listSkillsByAccess, | ||
| getSkillByName: db.getSkillByName, | ||
| }, | ||
| ); | ||
|
|
||
|
|
@@ -331,11 +337,26 @@ const OpenAIChatCompletionController = async (req, res) => { | |
| const openaiMessages = convertMessages(request.messages); | ||
|
|
||
| const toolSet = buildToolSet(primaryConfig); | ||
| const { | ||
| messages: formattedMessages, | ||
| indexTokenCountMap, | ||
| summary: initialSummary, | ||
| } = formatAgentMessages(openaiMessages, {}, toolSet); | ||
| const formatted = formatAgentMessages(openaiMessages, {}, toolSet); | ||
| const formattedMessages = formatted.messages; | ||
| const initialSummary = formatted.summary; | ||
| let indexTokenCountMap = formatted.indexTokenCountMap; | ||
|
|
||
| /** | ||
| * Inject manual skill primes so the model sees SKILL.md bodies for this | ||
| * turn — parity with AgentClient's chat path. OpenAI-compatible streaming | ||
| * uses its own tracker/aggregator shape, so the LibreChat-style card SSE | ||
| * events don't apply here; only the message-context part carries over. | ||
| */ | ||
| const manualSkillPrimes = primaryConfig.manualSkillPrimes; | ||
| if (manualSkillPrimes && manualSkillPrimes.length > 0) { | ||
| const primeResult = injectManualSkillPrimes({ | ||
| initialMessages: formattedMessages, | ||
| indexTokenCountMap, | ||
| manualSkillPrimes, | ||
| }); | ||
| indexTokenCountMap = primeResult.indexTokenCountMap; | ||
| } | ||
|
|
||
| /** | ||
| * Create a simple handler that processes data | ||
|
|
||
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 persistence path only filters
manualSkillsto non-empty strings, so a crafted request can still attach arbitrarily many and arbitrarily long skill names touserMessage.manualSkills. Unlike the runtime resolver path (which caps count/length), this can bloat stored message documents, inflate history payloads/UI rendering, and in extreme cases trigger Mongo document-size save failures for otherwise valid messages. Reuse the same bounded sanitizer when writingmanualSkillsto the message record.Useful? React with 👍 / 👎.