Skip to content

Commit 64ec5f1

Browse files
committed
⚙️ feat: Skill runtime integration: catalog, tools, execution, file priming (danny-avila#12649)
* feat: Skill runtime integration — catalog injection, tool registration, execute handler Wires the @librechat/agents SkillTool primitive into LibreChat's agent runtime: **Enums:** - Add `skills` to AgentCapabilities + defaultAgentCapabilities **Data layer:** - Add `getSkillByName(name, accessibleIds)` — compound query that combines name lookup + ACL check in one findOne **Agent initialization (packages/api/src/agents/initialize.ts):** - Accept `accessibleSkillIds` param and `listSkillsByAccess` db method - Query accessible skills, format catalog via `formatSkillCatalog()`, append to `additional_instructions` (appears in agent system prompt) - Register `SkillToolDefinition` + `createSkillTool()` when catalog is non-empty (tool appears in model's tool list) - Store `accessibleSkillIds` and `skillCount` on InitializedAgent **Execute handler (packages/api/src/agents/handlers.ts):** - Add `getSkillByName` to `ToolExecuteOptions` - `handleSkillToolCall()` intercepts `Constants.SKILL_TOOL`: extracts skillName, loads body from DB with ACL check, substitutes $ARGUMENTS, returns ToolExecuteResult with injectedMessages (skill body as isMeta user message) **Caller wiring:** - initialize.js: query skill IDs via findAccessibleResources, pass to initializeAgent + store on agentToolContexts, add getSkillByName to toolExecuteOptions, pass accessibleSkillIds through loadTools configurable - openai.js + responses.js: same pattern for their flows Requires @librechat/agents >= 3.1.65 (PR danny-avila#91 exports). * feat: Skills toggle in tools menu + backend capability gating Frontend: - Add skills?: boolean to TEphemeralAgent type - Add LAST_SKILLS_TOGGLE_ to LocalStorageKeys for persistence - Add skillsEnabled to useAgentCapabilities hook - Add skills useToolToggle to BadgeRowContext with localStorage init - New Skills.tsx badge component (Scroll icon, cyan theme, permission-gated via PermissionTypes.SKILLS) - Add skills entry to ToolsDropdown with toggle + pin - Render Skills badge in BadgeRow ephemeral section Backend: - Extract injectSkillCatalog() into packages/api/src/agents/skills.ts (reduces initializeAgent module size, reusable helper) - initializeAgent delegates to helper instead of inline block - Capability-gate the findAccessibleResources query: - Agents endpoint: checks AgentCapabilities.skills in admin config - OpenAI/Responses controllers: checks ephemeralAgent.skills toggle - ACL query runs once per run, result shared across all agents * refactor: remove createSkillTool() instance from injectSkillCatalog SkillTool is event-driven only. The tool definition in toolDefinitions is sufficient for the LLM to see the tool schema. No tool instance is needed since the host handler intercepts via ON_TOOL_EXECUTE before tool.invoke() is ever called. Removes tools from InjectSkillCatalogParams/Result, drops the createSkillTool import. * feat: skill file priming, bash tool, and invoked skills state Multi-file skill support: - New primeSkillFiles() helper (packages/api/src/agents/skillFiles.ts) uploads skill files + SKILL.md body to code execution environment - handleSkillToolCall primes files on invocation when skill.fileCount > 0, returns session info as artifact so ToolNode stores the session - Skill-primed files available to subsequent bash/code tool calls Bash tool auto-registration: - BashExecutionToolDefinition added alongside SkillToolDefinition when skills are enabled, giving the model a bash tool for running scripts Conversation state: - Add invokedSkillIds field to conversation schema (Mongoose + Zod) - handleSkillToolCall updates conversation with $addToSet on success - Enables re-priming skill files on subsequent runs (future) Dependency wiring: - Pass listSkillFiles, getStrategyFunctions, uploadCodeEnvFile, updateConversation through ToolExecuteOptions - Pass req and codeApiKey through mergedConfigurable - All three controller entry points wired (initialize.js, openai.js, responses.js) * fix: load bash_tool instance in loadToolsForExecution, remove file listing - Add createBashExecutionTool to loadToolsForExecution alongside PTC/ToolSearch pattern: loads CODE_API_KEY, creates bash tool instance on demand - Add BASH_TOOL and SKILL_TOOL to specialToolNames set so they don't go through the generic loadTools path (bash is created here, skill is intercepted in handler before tool.invoke) - Remove file name listing from skill content text — it's the skill author's responsibility to disclose files in SKILL.md, not the framework * feat: batch upload for skill files, replace sequential uploads - Add batchUploadCodeEnvFiles() to crud.js: single POST to /upload/batch with all files in one multipart request, returns shared session_id - Rewrite primeSkillFiles to collect all streams (SKILL.md + bundled files) then do one batch upload instead of N sequential uploads - Replace uploadCodeEnvFile with batchUploadCodeEnvFiles across all callers (handlers.ts, initialize.js, openai.js, responses.js) * refactor: remove invokedSkillIds from conversation schema Skills aren't re-loaded between runs, so conversation-level state for invoked skills doesn't help. Skill state will live on messages instead (like tool_search discoveredTools and summaries), enabling in-place re-injection on follow-up runs. Removes invokedSkillIds from: convo Mongoose schema, IConversation interface, Zod schema, ToolExecuteOptions.updateConversation, and all three caller wiring points. * feat: smart skill file re-priming with session freshness checking Schema: - Add codeEnvIdentifier field to ISkillFile (type + Mongoose schema) - Add updateSkillFileCodeEnvIds batch method (uses tenantSafeBulkWrite) - Export checkIfActive from Code/process.js Extraction: - Add extractInvokedSkillsFromHistory() to run.ts — scans message history for AIMessage tool_calls where name === 'skill', extracts skillName args. Follows same pattern as extractDiscoveredToolsFromHistory. Smart re-priming in primeSkillFiles: - Before batch uploading, checks if existing codeEnvIdentifiers are still active via getSessionInfo + checkIfActive (23h threshold) - If session is still active, returns cached references (zero uploads) - If stale or missing, batch-uploads everything and persists new identifiers on SkillFile documents (fire-and-forget) - Single session check covers all files (batch shares one session_id) Wiring: - Pass getSessionInfo, checkIfActive, updateSkillFileCodeEnvIds through ToolExecuteOptions and all three controller entry points * feat: wire skill file re-priming at run start via initialSessions Flow: 1. initialize.js creates primeInvokedSkills callback with all deps 2. client.js calls it with message history before createRun 3. extractInvokedSkillsFromHistory scans for skill tool calls 4. For each invoked skill with files, primeSkillFiles uploads/checks 5. Returns initialSessions map passed to createRun 6. createRun passes initialSessions to Run.create (via RunConfig) 7. Run constructor seeds Graph.sessions, making skill files available to subsequent bash/code tool calls via ToolNode session injection Requires @librechat/agents with initialSessions on RunConfig (PR danny-avila#94). * refactor: use CODE_EXECUTION_TOOLS set for code tool checks Import CODE_EXECUTION_TOOLS from @librechat/agents and replace inline constant checks in handlers.ts and callbacks.js. Fixes missing bash tool coverage in the session context injection (handlers.ts) and code output processing (callbacks.js). * refactor: move primeInvokedSkills to packages/api, add skill body re-injection Moves primeInvokedSkills from an inline closure in initialize.js (with dynamic requires) to a proper exported function in packages/api skillFiles.ts with explicit typed dependencies. Key changes: - primeInvokedSkills now returns both initialSessions (for file priming) AND injectedMessages (skill bodies for context continuity) - createRun accepts invokedSkillMessages and appends skill bodies to systemContent so the model retains skill instructions across runs - initialize.js calls the packaged function with all deps passed explicitly - client.js passes both initialSessions and injectedMessages to createRun * fix: move dynamic requires to top-level module imports Move primeInvokedSkills, getStrategyFunctions, batchUploadCodeEnvFiles, getSessionInfo, and checkIfActive from inline requires to top-level module requires where they belong. * refactor: skill body reconstruction via formatAgentMessages, not systemContent Replaces the lazy systemContent approach with proper message-level reconstruction: SDK (formatAgentMessages): - New invokedSkillBodies param (Map<string, string>) - Reconstructs HumanMessages after skill ToolMessages at the correct position in the message sequence, matching where ToolNode originally injected them LibreChat: - extractInvokedSkillsFromPayload replaces extractInvokedSkillsFromHistory (works with raw TPayload before formatAgentMessages, not BaseMessage[]) - primeInvokedSkills now takes payload instead of messages, returns skillBodies Map instead of injectedMessages - client.js calls primeInvokedSkills BEFORE formatAgentMessages, passes skillBodies through as the 4th param - Removed invokedSkillMessages from createRun (no more systemContent hack) - Single-pass: skill detection happens inside formatAgentMessages' existing tool_call processing loop, zero extra message iterations * refactor: rename skillBodies to skills for consistency with SDK param * refactor: move auth loading into primeInvokedSkills, pass loadAuthValues as dep The payload/accessibleSkillIds guard and CODE_API_KEY loading now live inside primeInvokedSkills (packages/api) rather than in the CJS caller. initialize.js passes loadAuthValues as a dependency and the callback is only created when skillsCapabilityEnabled. * feat: ReadFile tool + conditional bash registration + skill path namespacing ReadFile tool (read_file): - General-purpose file reader, event-driven (ON_TOOL_EXECUTE) - Schema: { file_path: string } — "{skillName}/{path}" convention - handleReadFileCall: resolves skill name from path, ACL check, reads from DB cache or storage, binary detection, size limits (256KB), lazy caching (512KB), line numbers in output - SKILL.md special case: reads skill.body directly - Dispatched alongside SKILL_TOOL in createToolExecuteHandler - Added to specialToolNames in ToolService Conditional tool registration: - ReadFile + SkillTool: always registered when skills enabled - BashTool: only registered when codeEnvAvailable === true - codeEnvAvailable passed through InitializeAgentParams from caller Skill file path namespacing: - primeSkillFiles now uploads as "{skillName}/SKILL.md" and "{skillName}/{relativePath}" instead of flat names - Prevents file collisions when multiple skills are invoked Wiring: - getSkillFileByPath + updateSkillFileContent passed through ToolExecuteOptions in all three callers * feat: return images/PDFs as artifacts from read_file, tighten caching Binary artifact support: - Images (png, jpeg, gif, webp) returned as base64 in artifact.content with type: 'image_url', processed by existing callback attachment flow - PDFs returned as base64 artifact similarly - Binary size limit: 10MB (MAX_BINARY_BYTES) - Other binary files still return metadata + bash fallback Caching: - Text cached only on first read (file.content == null check) - Binary flag cached only on first detection (file.isBinary == null) - Skill files are immutable; no redundant cache writes Registration: - ReadFileToolDefinition now includes responseFormat: 'content_and_artifact' * chore: update @librechat/agents to version 3.1.66-dev.0 and add peer dependencies in package-lock.json and package.json files * fix: resolve review findings #1,#2,#4,#5,#6,#10,danny-avila#13 Critical: - #1: primeInvokedSkills now accumulates files across all skills into one session entry instead of overwriting. Parallel processing via Promise.allSettled. - #2: codeEnvAvailable now computed and passed in openai.js and responses.js (was missing, bash tool never registered in those flows) Major: - #4: relativePath in updateSkillFileCodeEnvIds now strips the {skillName}/ prefix to match SkillFile documents. SKILL.md filter uses endsWith instead of exact match. - #5: File priming guarded on apiKey being non-empty (skip when not configured instead of failing with auth error) - #6: Skills processed in parallel via Promise.allSettled instead of sequential for-of loop Minor: - #10: Use top-level imports in initialize.js instead of inline requires - danny-avila#13: Log warning when skill catalog reaches the 100-skill limit * fix: resolve followup review findings N1,N2,N4 N1 (CRITICAL): Wire skill deps into responses.js non-streaming path. Was completely missing getSkillByName, file strategy functions, etc. N2 (MAJOR): Single batch upload for ALL skills' files. Resolves skills in parallel (Phase 1), then collects all file streams across skills and does ONE batchUploadCodeEnvFiles call (Phase 2). All files share one session_id, eliminating cross-session isolation issues. N4 (MINOR): Move inline require() to top-level in openai.js and responses.js, consistent with initialize.js. * fix: add mocks for new file strategy imports in controller tests * fix: restore session freshness check, parallelize file lookups, add warnings R1: Re-add session freshness check before batch upload. Checks any existing codeEnvIdentifier via getSessionInfo + checkIfActive. If the session is still active (23h window), returns cached file references with zero re-uploads. R2: listSkillFiles calls parallelized via Promise.all (were sequential in the for-of loop). R3: Log warning when skill record lookup fails during identifier persistence (was a silent empty-string fallback). * fix: guard freshness cache on single-session consistency * fix: multi-session freshness check (code env handles mixed sessions natively) The code execution environment fetches each file by its own {session_id, fileId} pair independently — no single-session requirement. Removed the sessionIds.size === 1 guard. Now checks ALL distinct sessions for freshness. If every session is still active (23h window), returns cached references with per-file session_ids preserved. If any session expired, falls through to re-upload everything in a single batch. * perf: parallelize session freshness checks via Promise.all * fix: add optional chaining for session info retrieval in primeInvokedSkills Updated the primeInvokedSkills function to use optional chaining for getSessionInfo and checkIfActive methods, ensuring safer access and preventing potential runtime errors when these methods are undefined. * fix: address review findings #1-#9 + Codex P1/P2 + session probe Critical: - #1/Codex P1: Add codeApiKey loading to openai.js and responses.js loadTools configurable (was missing, file priming broken in 2/3 paths) - Codex P1: Fix cached file name prefix in primeSkillFiles cache path (was sf.relativePath, now ${skill.name}/${sf.relativePath}) Major: - Codex P2: Honor ephemeral skills toggle in agents endpoint (check ephemeralAgent?.skills !== false alongside admin capability) - #4: Early size check using file.bytes from DB before streaming (prevents full-file buffer for oversized files) Minor: - #5: Replace Record<string, any> with Record<string, boolean | string> - #6: Localize Pin/Unpin aria-labels with com_ui_pin/com_ui_unpin - #8: Parallelize stream acquisition in primeSkillFiles via Promise.allSettled - #9: Log warning for partial batch upload failures with filenames Performance: - Session probe optimization: getSessionInfo now hits per-object endpoint (GET /sessions/{sid}/objects/{fid}) instead of listing entire session (GET /files/{sid}?detail=summary). O(1) stat vs O(N) list + linear scan. * refactor: extract shared skill wiring helper + add unit tests DRY (#3): - New skillDeps.js exports getSkillToolDeps() with all 9 skill-related deps (getSkillByName, listSkillFiles, getStrategyFunctions, etc.) - Replaces 5 identical copy-paste blocks across initialize.js, openai.js, responses.js (streaming + non-streaming paths) - One place to maintain when skill deps change Tests (#2): - 8 unit tests for extractInvokedSkillsFromPayload covering: string args, object args, missing skill tool_calls, non-assistant messages, malformed JSON, empty skillName, empty payload, dedup * fix: remove @jest/globals import, use global jest env * fix: resolve round 2 review findings R2-1 through R2-7 R2-1 (toggle semantics): openai.js + responses.js now check admin capability (AgentCapabilities.skills) alongside ephemeral toggle. Aligns with initialize.js. R2-2 (swallowed error): primeInvokedSkills now logs updateSkillFileCodeEnvIds failures (was .catch(() => {})) R2-4 (test cast): Record<string, string> → Record<string, unknown> R2-5 (DRY regression): Extract enrichWithSkillConfigurable() into skillDeps.js. Replaces 4 identical loadAuthValues blocks. Each loadTools callback is now a one-liner. JSDoc added (R2-6). R2-7 (sequential streams): primeInvokedSkills now uses Promise.allSettled for parallel stream acquisition. * fix: require explicit skills toggle + treat partial cache as miss - initialize.js: change ephemeralSkillsToggle !== false to === true (unset toggle no longer enables skills) - primeSkillFiles cache: require ALL files to have codeEnvIdentifier before using cache (partial persistence = cache miss = re-upload) - primeInvokedSkills cache: same check (allFilesWithIds.length must equal total file count) * fix: pass entity_id=skillId on batch upload, eliminates per-user cache thrashing primeSkillFiles now passes entity_id: skill._id.toString() to batchUploadCodeEnvFiles. This scopes the code env session to the skill, not the user. All users sharing a skill share the same uploaded files — no more cache thrashing from overwriting each other's codeEnvIdentifier. The stored codeEnvIdentifier now includes ?entity_id= suffix so freshness checks pass the entity_id through to the per-object stat endpoint. Both primeSkillFiles and primeInvokedSkills store consistent identifier formats. * fix: pass entity_id on multi-skill batch upload, consistent identifier format * Revert "fix: pass entity_id on multi-skill batch upload, consistent identifier format" This reverts commit c85ce21. * refactor: per-skill upload in primeInvokedSkills, eliminate multi-skill batch Replace the monolithic multi-skill batch upload with per-skill primeSkillFiles calls. Each skill gets its own session with entity_id=skillId, ensuring: - Correct session auth (entity_id matches on freshness checks) - Per-skill freshness caching (only expired skills re-upload) - Shared skill sessions work across users (same entity_id=skillId) - Code env handles mixed session_ids natively The big batch block (stream collection, single upload, identifier mapping) is replaced by a simple loop over primeSkillFiles, which already handles freshness caching, batch upload, and identifier persistence per-skill. * fix: resolve review findings #1,#3-5,#7,#9-11 Critical: - #1: Strip ?entity_id= query string before splitting codeEnvIdentifier into session_id/fileId (was corrupting cached file IDs in 4 locations) Major: - #4: Parallelize per-skill primeSkillFiles via Promise.allSettled - #5: Add logger.warn to all empty .catch(() => {}) on cache writes Minor: - #7: Add logger.debug to enrichWithSkillConfigurable catch block - #9: Use error instanceof Error guard in batchUploadCodeEnvFiles - #10: Move enrichWithSkillConfigurable to TypeScript in packages/api (skillConfigurable.ts), skillDeps.js wraps with loadAuthValues dep - #11: Reduce MAX_BINARY_BYTES from 10MB to 5MB (~11.5MB peak with b64) * fix: forward entity_id in session probe + always register bash tool Codex P2 (entity_id in probe): getSessionInfo now preserves and forwards query params (including entity_id) to the per-object stat endpoint. Without this, identifiers stored as ...?entity_id=... would fail auth checks because the entity_id scope was dropped. Codex P2 (bash tool availability): Remove codeEnvAvailable gate from injectSkillCatalog. Bash tool definition is now always registered when skills are enabled. Actual tool instance creation still happens at execution time in loadToolsForExecution (which loads per-user credentials). This ensures users with per-user CODE_API_KEY get bash without requiring a global env var at init time. Removes codeEnvAvailable from InjectSkillCatalogParams, InitializeAgentParams, and all three controller entry points. * fix: add debug logging to primeInvokedSkills catch, rename export alias * fix: stub bash tool when no key + remove PDF artifact path Codex P1 (bash tool): When CODE_API_KEY is unavailable, create a stub tool that returns "Code execution is not available. Use read_file instead." This prevents "tool not found" errors from the model repeatedly calling bash_tool in no-code-env deployments while still registering the definition for per-user credential users. Codex P2 (PDF artifacts): Remove PDF image_url artifact path. The host artifact pipeline processes image_url via saveBase64Image which fails for PDFs. PDFs now fall through to the generic binary handler ("Use bash to process"). TODO comment for future document artifact support. Also: isImageOrPdf → isImage in early size checks (PDFs are no longer treated as artifact candidates). * fix: remove dead PDF_MIME constant, hoist skillToolDeps, document session_id - #7: Remove unused PDF_MIME constant (dead code after PDF artifact removal) - #11: Hoist skillToolDeps to module-level constant (avoid per-call allocation) - #6: Document that CodeSessionContext.session_id is a representative value; ToolNode uses per-file session_id from the files array * fix: call toolEndCallback for skill/read_file artifacts + clear codeEnvIdentifier on re-upload Codex P1 (toolEndCallback bypass): skill and read_file handler branches returned early, bypassing the toolEndCallback that processes artifacts (image attachments). Now calls toolEndCallback when the result has an artifact, using the same metadata pattern as the normal tool.invoke path. Codex P1 (stale identifiers): upsertSkillFile now $unset's codeEnvIdentifier alongside content and isBinary when a file is re-uploaded. Prevents the freshness cache from returning references to old file content after a skill file is replaced. * fix: add session_id comment at cached path, rename skillResult to handlerResult * fix: return content_and_artifact from bash stub so result.content is populated * fix: deterministic skill lookup, dedup warning, and multi-session freshness check - getSkillByName: add sort({updatedAt:-1}) so name collisions resolve deterministically to the most recently updated skill - injectSkillCatalog: warn when multiple accessible skills share a name - primeSkillFiles: check ALL distinct sessions for freshness, not just the first file's session, preventing stale refs after partial bulkWrite * refactor: update icon import in Skills component - Replaced the Scroll icon with ScrollText in the Skills component for improved clarity and consistency in the UI. * fix: SKILL.md cache parity, gate bash_tool on code env, fix read_file too-large message - primeSkillFiles: filter SKILL.md from returned files array on fresh upload so cached and non-cached paths return identical file sets (SKILL.md is still on disk in the session for bash access) - injectSkillCatalog: only register bash_tool when codeEnvAvailable is true; thread the flag from all three CJS callers via execute_code capability check - handleReadFileCall: tell the model to invoke the skill first before suggesting /mnt/data paths for oversized files * fix: use EnvVar constant, deduplicate auth lookup, validate batch upload, stream byte limit - Replace hardcoded 'LIBRECHAT_CODE_API_KEY' with EnvVar.CODE_API_KEY in skillConfigurable.ts and skillFiles.ts - Resolve code API key once at run start in initialize.js and pass to both primeInvokedSkills and enrichWithSkillConfigurable via optional preResolvedCodeApiKey param, eliminating redundant loadAuthValues calls - Add response structure validation in batchUploadCodeEnvFiles before accessing session_id/files to surface unexpected responses early - Add streaming byte counter in handleReadFileCall that aborts and destroys the stream when accumulated bytes exceed MAX_BINARY_BYTES, preventing full file buffering when DB metadata is inaccurate * refactor: update icon import in ToolsDropdown component - Replaced the Scroll icon with ScrollText in the ToolsDropdown component for improved clarity and consistency in the UI. * fix: partial upload failure detection, EnvVar in initialize.js, declaration ordering - primeSkillFiles: return null (failure) when batch upload partially succeeds — missing bundled files would cause runtime bash/read failures with missing paths in code env - initialize.js: replace hardcoded 'LIBRECHAT_CODE_API_KEY' with EnvVar.CODE_API_KEY imported from @librechat/agents - initialize.js: move enabledCapabilities, accessibleSkillIds, and codeApiKey declarations before the toolExecuteOptions closure that references them (eliminates reliance on temporal dead zone hoisting)
1 parent f6ee2ea commit 64ec5f1

32 files changed

Lines changed: 2027 additions & 48 deletions

File tree

api/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
"@google/genai": "^1.19.0",
4545
"@keyv/redis": "^4.3.3",
4646
"@langchain/core": "^0.3.80",
47-
"@librechat/agents": "^3.1.68",
47+
"@librechat/agents": "^3.1.66-dev.0",
4848
"@librechat/api": "*",
4949
"@librechat/data-schemas": "*",
5050
"@microsoft/microsoft-graph-client": "^3.0.7",

api/server/controllers/agents/__tests__/openai.spec.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,19 @@ jest.mock('~/server/services/PermissionService', () => ({
114114
checkPermission: jest.fn().mockResolvedValue(true),
115115
}));
116116

117+
jest.mock('~/server/services/Files/strategies', () => ({
118+
getStrategyFunctions: jest.fn().mockReturnValue({}),
119+
}));
120+
121+
jest.mock('~/server/services/Files/Code/crud', () => ({
122+
batchUploadCodeEnvFiles: jest.fn().mockResolvedValue({ session_id: '', files: [] }),
123+
}));
124+
125+
jest.mock('~/server/services/Files/Code/process', () => ({
126+
getSessionInfo: jest.fn().mockResolvedValue(null),
127+
checkIfActive: jest.fn().mockReturnValue(false),
128+
}));
129+
117130
const mockUpdateBalance = jest.fn().mockResolvedValue({});
118131
const mockBulkInsertTransactions = jest.fn().mockResolvedValue(undefined);
119132

api/server/controllers/agents/__tests__/responses.unit.spec.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,19 @@ jest.mock('~/cache', () => ({
144144
logViolation: jest.fn(),
145145
}));
146146

147+
jest.mock('~/server/services/Files/strategies', () => ({
148+
getStrategyFunctions: jest.fn().mockReturnValue({}),
149+
}));
150+
151+
jest.mock('~/server/services/Files/Code/crud', () => ({
152+
batchUploadCodeEnvFiles: jest.fn().mockResolvedValue({ session_id: '', files: [] }),
153+
}));
154+
155+
jest.mock('~/server/services/Files/Code/process', () => ({
156+
getSessionInfo: jest.fn().mockResolvedValue(null),
157+
checkIfActive: jest.fn().mockReturnValue(false),
158+
}));
159+
147160
const mockUpdateBalance = jest.fn().mockResolvedValue({});
148161
const mockBulkInsertTransactions = jest.fn().mockResolvedValue(undefined);
149162

api/server/controllers/agents/callbacks.js

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const {
77
GraphEvents,
88
GraphNodeKeys,
99
ToolEndHandler,
10+
CODE_EXECUTION_TOOLS,
1011
} = require('@librechat/agents');
1112
const {
1213
sendEvent,
@@ -443,9 +444,7 @@ function createToolEndCallback({ req, res, artifactPromises, streamId = null })
443444
return;
444445
}
445446

446-
const isCodeTool =
447-
output.name === Tools.execute_code || output.name === Constants.PROGRAMMATIC_TOOL_CALLING;
448-
if (!isCodeTool) {
447+
if (!CODE_EXECUTION_TOOLS.has(output.name)) {
449448
return;
450449
}
451450

@@ -651,9 +650,7 @@ function createResponsesToolEndCallback({ req, res, tracker, artifactPromises })
651650
return;
652651
}
653652

654-
const isCodeTool =
655-
output.name === Tools.execute_code || output.name === Constants.PROGRAMMATIC_TOOL_CALLING;
656-
if (!isCodeTool) {
653+
if (!CODE_EXECUTION_TOOLS.has(output.name)) {
657654
return;
658655
}
659656

api/server/controllers/agents/client.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -742,12 +742,18 @@ class AgentClient extends BaseClient {
742742

743743
const toolSet = buildToolSet(this.options.agent);
744744
const tokenCounter = createTokenCounter(this.getEncoding());
745+
746+
/** Pre-resolve invoked skill bodies + re-prime files before formatting messages */
747+
const skillPrimeResult = this.options.primeInvokedSkills
748+
? await this.options.primeInvokedSkills(payload)
749+
: undefined;
750+
745751
let {
746752
messages: initialMessages,
747753
indexTokenCountMap,
748754
summary: initialSummary,
749755
boundaryTokenAdjustment,
750-
} = formatAgentMessages(payload, this.indexTokenCountMap, toolSet);
756+
} = formatAgentMessages(payload, this.indexTokenCountMap, toolSet, skillPrimeResult?.skills);
751757
if (boundaryTokenAdjustment) {
752758
logger.debug(
753759
`[AgentClient] Boundary token adjustment: ${boundaryTokenAdjustment.original}${boundaryTokenAdjustment.adjusted} (${boundaryTokenAdjustment.remainingChars}/${boundaryTokenAdjustment.totalChars} chars)`,
@@ -829,6 +835,7 @@ class AgentClient extends BaseClient {
829835
messages,
830836
indexTokenCountMap,
831837
initialSummary,
838+
initialSessions: skillPrimeResult?.initialSessions,
832839
calibrationRatio,
833840
runId: this.responseMessageId,
834841
signal: abortController.signal,

api/server/controllers/agents/openai.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const {
66
ResourceType,
77
PermissionBits,
88
hasPermissions,
9+
AgentCapabilities,
910
} = require('librechat-data-provider');
1011
const {
1112
writeSSE,
@@ -40,6 +41,10 @@ const {
4041
findAccessibleResources,
4142
getEffectivePermissions,
4243
} = require('~/server/services/PermissionService');
44+
const {
45+
getSkillToolDeps,
46+
enrichWithSkillConfigurable,
47+
} = require('~/server/services/Endpoints/agents/skillDeps');
4348
const { getModelsConfig } = require('~/server/controllers/ModelController');
4449
const { logViolation } = require('~/cache');
4550
const db = require('~/models');
@@ -235,8 +240,22 @@ const OpenAIChatCompletionController = async (req, res) => {
235240
getUserCodeFiles: db.getUserCodeFiles,
236241
getToolFilesByIds: db.getToolFilesByIds,
237242
getCodeGeneratedFiles: db.getCodeGeneratedFiles,
243+
listSkillsByAccess: db.listSkillsByAccess,
238244
};
239245

246+
const enabledCapabilities = new Set(agentsEConfig?.capabilities);
247+
const ephemeralAgent = req.body?.ephemeralAgent;
248+
const skillsEnabled =
249+
enabledCapabilities.has(AgentCapabilities.skills) && ephemeralAgent?.skills === true;
250+
const accessibleSkillIds = skillsEnabled
251+
? await findAccessibleResources({
252+
userId: req.user.id,
253+
role: req.user.role,
254+
resourceType: ResourceType.SKILL,
255+
requiredPermissions: PermissionBits.VIEW,
256+
})
257+
: [];
258+
240259
const primaryConfig = await initializeAgent(
241260
{
242261
req,
@@ -249,6 +268,8 @@ const OpenAIChatCompletionController = async (req, res) => {
249268
endpointOption,
250269
allowedProviders,
251270
isInitialAgent: true,
271+
accessibleSkillIds,
272+
codeEnvAvailable: enabledCapabilities.has(AgentCapabilities.execute_code),
252273
},
253274
dbMethods,
254275
);
@@ -375,7 +396,7 @@ const OpenAIChatCompletionController = async (req, res) => {
375396
const toolExecuteOptions = {
376397
loadTools: async (toolNames, agentId) => {
377398
const ctx = agentToolContexts.get(agentId) ?? agentToolContexts.get(primaryConfig.id) ?? {};
378-
return loadToolsForExecution({
399+
const result = await loadToolsForExecution({
379400
req,
380401
res,
381402
toolNames,
@@ -386,8 +407,10 @@ const OpenAIChatCompletionController = async (req, res) => {
386407
tool_resources: ctx.tool_resources,
387408
actionsEnabled: ctx.actionsEnabled,
388409
});
410+
return enrichWithSkillConfigurable(result, req, primaryConfig.accessibleSkillIds);
389411
},
390412
toolEndCallback,
413+
...getSkillToolDeps(),
391414
};
392415

393416
const summarizationConfig = appConfig?.summarization;

api/server/controllers/agents/responses.js

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const {
77
ResourceType,
88
PermissionBits,
99
hasPermissions,
10+
AgentCapabilities,
1011
} = require('librechat-data-provider');
1112
const {
1213
createRun,
@@ -49,6 +50,10 @@ const {
4950
findAccessibleResources,
5051
getEffectivePermissions,
5152
} = require('~/server/services/PermissionService');
53+
const {
54+
getSkillToolDeps,
55+
enrichWithSkillConfigurable,
56+
} = require('~/server/services/Endpoints/agents/skillDeps');
5257
const { getModelsConfig } = require('~/server/controllers/ModelController');
5358
const { logViolation } = require('~/cache');
5459
const db = require('~/models');
@@ -362,8 +367,24 @@ const createResponse = async (req, res) => {
362367
getUserCodeFiles: db.getUserCodeFiles,
363368
getToolFilesByIds: db.getToolFilesByIds,
364369
getCodeGeneratedFiles: db.getCodeGeneratedFiles,
370+
listSkillsByAccess: db.listSkillsByAccess,
365371
};
366372

373+
const enabledCapabilities = new Set(
374+
appConfig?.endpoints?.[EModelEndpoint.agents]?.capabilities,
375+
);
376+
const ephemeralAgent = req.body?.ephemeralAgent;
377+
const skillsEnabled =
378+
enabledCapabilities.has(AgentCapabilities.skills) && ephemeralAgent?.skills === true;
379+
const accessibleSkillIds = skillsEnabled
380+
? await findAccessibleResources({
381+
userId: req.user.id,
382+
role: req.user.role,
383+
resourceType: ResourceType.SKILL,
384+
requiredPermissions: PermissionBits.VIEW,
385+
})
386+
: [];
387+
367388
const primaryConfig = await initializeAgent(
368389
{
369390
req,
@@ -376,6 +397,8 @@ const createResponse = async (req, res) => {
376397
endpointOption,
377398
allowedProviders,
378399
isInitialAgent: true,
400+
accessibleSkillIds,
401+
codeEnvAvailable: enabledCapabilities.has(AgentCapabilities.execute_code),
379402
},
380403
dbMethods,
381404
);
@@ -533,7 +556,7 @@ const createResponse = async (req, res) => {
533556
loadTools: async (toolNames, agentId) => {
534557
const ctx =
535558
agentToolContexts.get(agentId) ?? agentToolContexts.get(primaryConfig.id) ?? {};
536-
return loadToolsForExecution({
559+
const result = await loadToolsForExecution({
537560
req,
538561
res,
539562
toolNames,
@@ -544,8 +567,10 @@ const createResponse = async (req, res) => {
544567
tool_resources: ctx.tool_resources,
545568
actionsEnabled: ctx.actionsEnabled,
546569
});
570+
return enrichWithSkillConfigurable(result, req, primaryConfig.accessibleSkillIds);
547571
},
548572
toolEndCallback,
573+
...getSkillToolDeps(),
549574
};
550575

551576
// Combine handlers
@@ -701,7 +726,7 @@ const createResponse = async (req, res) => {
701726
loadTools: async (toolNames, agentId) => {
702727
const ctx =
703728
agentToolContexts.get(agentId) ?? agentToolContexts.get(primaryConfig.id) ?? {};
704-
return loadToolsForExecution({
729+
const result = await loadToolsForExecution({
705730
req,
706731
res,
707732
toolNames,
@@ -712,8 +737,10 @@ const createResponse = async (req, res) => {
712737
tool_resources: ctx.tool_resources,
713738
actionsEnabled: ctx.actionsEnabled,
714739
});
740+
return enrichWithSkillConfigurable(result, req, primaryConfig.accessibleSkillIds);
715741
},
716742
toolEndCallback,
743+
...getSkillToolDeps(),
717744
};
718745

719746
const handlers = {

0 commit comments

Comments
 (0)