mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-11-04 21:26:52 +00:00
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
* 🧵 feat: Background Tool Calls for Agents & Model Specs Opt-in, poll-based background tool execution. The model marks an eligible tool call with `run_in_background: true`; the host executor registers a task, returns a handle immediately (so the graph turn resolves), runs the tool as a detached promise, and the model retrieves the result via a new `check_background_task` poll tool. Host-side only — no `@librechat/agents` change. - Opt-in mirrors `deferred_tools`: admin capability `run_in_background` (off by default) + per-tool `tool_options.run_in_background`. - Model specs / ephemeral agents: `TModelSpec.runInBackground` / `TEphemeralAgent.run_in_background` synthesize per-tool options; both paths converge at `initializeAgent`. - In-process task registry: scoped per user+conversation, idempotent by toolCallId (safe across resume/replay), capped, TTL-swept. - Excludes direct-path / host-special / code-session tools. Subagents and push notifications are deferred follow-ups. * 🩹 fix: Harden background tool calls (Codex review) - Reliable per-agent execution gate: thread the injected `run_in_background` tool names from `initializeAgent` through `configurable.backgroundToolNames` (`toolRegistry` only reaches the executor for PTC/tool_search), fixing the silent no-op + unstripped-arg leak for ordinary event-driven tools. - Enforce the per-tool opt-in at execution (`backgroundToolSet.has(name)`) so a non-opted-in tool can't be backgrounded via an extra arg. - Gate the `check_background_task` interception on the run actually enabling background, so a user tool sharing that name still executes. - Forward `backgroundToolsAvailable` to added-convo (multi-convo) agents. - Exclude `web_search`/`file_search` from eligibility — their results are turned into user-visible attachments/citations only by the foreground toolEndCallback. * 🩹 fix: Address Codex round 2 on background tool calls - Idempotency scoped to run+turn: provider tool-call ids repeat across turns (e.g. `call_0`), so key the dedupe map by `runId::toolCallId` and sweep orphaned mappings — a later turn no longer collides with a retained task. - Artifacts preserved: a backgrounded tool's artifact is processed through the same `toolEndCallback` as the foreground path (images/files/citations no longer silently dropped), best-effort/guarded. - Forward the `run_in_background` capability to connected-agent discovery and subagent `processAgent` init, so a child agent's own event-driven tools work the same as when it runs as primary. - Strip the injected flag on foreground calls of background-capable tools (the model may emit it as `false`) so strict MCP/action schemas don't reject. - `check_background_task` list path returns metadata only (result_available / result_chars), never full results — prevents context overflow; the full result is returned only when a specific id is requested. * 🩹 fix: Address Codex round 3 on background tool calls - Exclude background-capable tools from eager execution (run.ts): a speculative eager dispatch of a `run_in_background` call could launch the detached task with partial/stale args, and that side effect can't be canceled. - Reserve the `check_background_task` name: overwrite a colliding user/MCP tool with the host poll schema (with a warning) so the advertised schema matches the executor's interception instead of hijacking a mismatched tool. - Don't inject background schemas into pure subagents (spawn-tool child graphs) whose tools don't reach the host interceptor; keep it for primary/added/ connected agents. Subagent background is the durable follow-up. - Thread `backgroundToolsAvailable` + `backgroundToolNames` through the OpenAI-compatible and Responses agent routes (was chat-only), so the same agent/model spec behaves consistently across surfaces. - Exclude image-generation built-ins (dalle/flux/gemini_image_gen/image_gen_oai/ image_edit_oai) — artifact-first tools whose files can't reliably attach to an already-saved turn when backgrounded. * 🩹 fix: Address Codex round 4 on background tool calls - Sanitize self-spawn subagent inputs: strip `run_in_background` + the `check_background_task` def from the parent AgentInputs reused for self-spawn, so the isolated child (direct/child-graph path) doesn't advertise a background schema it can't honor. The SDK resolver keeps a provided `agentInputs` even with `self: true`. - Exclude `check_background_task` from PTC (`run_tools_with_code`) tool definitions — it's host-only and not callable from generated code. - Parse stringified JSON args before deciding background dispatch and before stripping the flag, so string-delivered `run_in_background` is honored and never leaks to strict object-schema tools. - Skip injection for tools that already declare their own `run_in_background` param (would otherwise hijack/strip it), and for non-object (string-input) schemas (would otherwise rewrite the input contract). * 🩹 fix: Address Codex round 5 on background tool calls - check_background_task now parses stringified JSON args, so providers that deliver args as a string can retrieve a specific task by id (not just list). - Include agentId in the background dedupe key (`agentId::runId::toolCallId`): two agents in the same run emitting the same provider id (e.g. `call_0`) now launch independent tasks instead of colliding. - Self-spawn sanitization also strips the background entries from the reused toolRegistry (not just toolDefinitions), so a child using tool_search/deferred loading can't rediscover the host-only run_in_background / check_background_task. * 🩹 fix: Strip run_in_background from PTC target tool schemas (Codex round 6) The PTC path already filtered out the host-only check_background_task poll tool but still exposed target tool schemas with the injected `run_in_background` param (the shared toolRegistry entries were mutated by applyBackgroundToolCalls). PTC codegen doesn't go through the host background interceptor, so it could pass the flag to an MCP/action tool (strict-schema rejection or silent foreground with no poll). Sanitize the PTC toolDefs like the self-spawn path does. * 🩹 fix: Sanitize background from explicit subagent inputs (Codex round 7) A child agent reachable as a top-level/handoff agent is initialized WITH the background capability, then reused as an explicit subagent via buildSubagentConfigs. Round 4 only sanitized the self-spawn case; this now applies the same stripBackgroundFromToolDefinitions/Registry to explicit child agentInputs when `child.backgroundToolNames` is non-empty, so an isolated child graph doesn't advertise a run_in_background / check_background_task contract it can't honor. * 🩹 fix: Reap stuck/expired background tasks (Codex round 8) - get() now sweeps before returning, so repeatedly polling a known background_task_id can't keep an expired completed task (and its retained result, up to 100k chars) alive past the one-hour completed TTL. - sweep() now reaps `running` tasks older than a 30-min running TTL, marking them errored. Previously a detached call that never settled (hung network / lost MCP connection) held a running slot forever, exhausting the per-conversation cap and rejecting every later dispatch. * 🩹 fix: Evict oldest settled tasks instead of blocking at the cap (Codex round 9) Only the running-task cap gates dispatch now. The total-tasks cap (MAX_TASKS_PER_BUCKET) bounds memory but no longer rejects new background calls: when full, it evicts the oldest settled (completed/error) tasks to make room. Previously 200 quick background calls in one conversation would block all new dispatches for up to the completed-task TTL, since polling doesn't remove settled tasks. Running is already capped, so room always frees. * 📝 docs: Frame background tool calls as within-turn (Codex P1 contract) Codex escalated the request-lifecycle findings to P1 on the grounds that the advertised "poll later" contract can't be honored for genuinely long-running calls (request-scoped MCP connections + the run abort signal are torn down at turn end). Align the model-facing contract with what the same-run implementation actually delivers: the run_in_background param, check_background_task, and the dispatch handle now instruct the model to collect the result WITHIN THE SAME TURN (backgrounded work isn't guaranteed to survive past the turn). This is within-turn parallelism; cross-turn survival of long-running calls remains the deliberate durable subagent follow-up. Copy/comment-only; no behavior change. * ♻️ refactor: Cross-turn background tool calls, leak-free Extend background tool calls from within-turn to cross-turn on a single process, since the mechanism already supports it: the run's abort signal never reaches the detached invoke (the graph forwards only configurable/ metadata to the tool-execute handler), so the floating promise keeps running past turn completion and its result stays in the in-process registry for a later turn to poll (get/list key only on user::conversation + id, never the dispatch run/turn). Guarantee no connection leak: ephemeral request-scoped MCP tools (runtime {{LIBRECHAT_BODY_*}} placeholders) capture their request-scoped store at creation and fall back to it, so config manipulation can't redirect them; their connection is torn down at request end. Tag such tools in createToolInstance and run them in the foreground instead of backgrounding them. Pooled/app-level MCP and structured tools are unaffected and survive cross-turn via their managed pools. Reword the model-facing contract (run_in_background, check_background_task, handle message, fileoverview) from within-turn to cross-turn on this server (not across restart/replica, which stays the durable follow-up). Tests: cross-turn poll retrieval; ephemeral MCP tool runs foreground. * 🐛 fix: Guard ephemeral MCP tag against a null server config createToolInstance can be reached with a null/stale capturedServerConfig (cached availableTools + getServerConfig returns null, as several MCP unit tests construct tools). The new unconditional requiresEphemeralUserConnection call then dereferenced config.source and threw during tool construction (CI: Tests api shard 2/3). Guard with the same serverConfig ? ... : false pattern the other callers use; a missing config is not request-scoped. * 🎨 fix: Deliver backgrounded tool artifacts on the poll turn A slow backgrounded MCP/action tool resolves after its dispatch turn is finalized: createToolEndCallback only appends to that turn's artifactPromises (already awaited) and writes to a closed stream, so the artifact (file/citation/ UI resource) was silently dropped — check_background_task recorded only the hasArtifact boolean. The cross-turn contract made this the common case. Hold the artifact on the task and deliver it through the LIVE poll turn's toolEndCallback the first time check_background_task collects that id (once, then cleared to free memory), attributed to the original tool. Same-turn and cross-turn now share this path since the model must poll to collect any result. Tests: registry claim-once; artifact delivered on poll not dispatch, idempotent. * ✨ feat: Agent-builder toggle for background tool calls + cap tool descriptions Add a per-MCP-tool "run in background" toggle in the agent builder, mirroring the programmatic/deferred pattern: gated on the admin `run_in_background` capability via useAgentCapabilities, read/written on tool_options[id] .run_in_background through useMCPToolOptions (per-tool + bulk mark-all), and rendered as a Zap toggle in MCPToolItem and McpSection with new locale keys. Also cap the section tool/server descriptions (McpSection, ToolSection, SkillSection) with max-h-40 overflow-y-auto so a long description scrolls instead of overflowing the dialog, matching MCPToolItem's existing cap. Tests: MCPToolItem renders/toggles the background button only when enabled. * 🧪 fix: Mock new background hook functions in McpSection spec * 🎨 fix: Restore background artifact when poll-turn delivery fails * 🛡️ fix: Harden background tool call edges from review findings - Error immediately (matching foreground) when a background-requested tool failed to load, instead of returning a success handle for a dead task - Exclude ephemeral request-scoped MCP tools at injection time so the model never sees a run_in_background param the executor would silently downgrade; flip the execute-time tag to fail closed on a missing server config - Source image-tool background exclusions from the shared imageGenTools set (adds missing stable-diffusion, an artifact-first live tool) instead of a hand-copied list - Add check_background_task to the eager-execution exclusion list: artifact collection is a one-shot claim that must not fire from a speculative snapshot the SDK may discard - Strip an imitated run_in_background arg on tools the executing agent never opted in (multi-agent history bleed), unless the tool's own schema declares the parameter - Truncate oversized stored results with an explicit marker via the shared truncateMiddle (moved to utils/text) instead of a silent slice - Document the at-most-once artifact delivery semantics honestly (the callback's downstream persistence is fire-and-forget, as in foreground) * ♻️ refactor: Deduplicate background tool-call plumbing and tighten types - Use the SDK's JsonSchemaType instead of a local duplicate; drop all as-unknown casts and type the poll-tool serializer explicitly - Drop derivable BackgroundTask state (progress, hasArtifact) and the dead `enabled` param/return on applyBackgroundToolCalls (guarded at the call site), which also skips the defs pass when nothing opted in - Fold the enable expression into synthesizeBackgroundToolOptions so the three load/added call sites can't drift - Throttle the registry's all-buckets sweep and always sweep the accessed bucket, so a hot poll loop is no longer O(total tasks server-wide); bound retained artifact memory with a size cap - Single-pass stripBackgroundFromToolDefinitions; pass metadata through to the poll-turn callback instead of a no-op reconstruction - Collapse the client's copy-pasted boolean option families into a keyed factory (also removes the shared-object mutation in the bulk toggles) and the six toggle-button copies into one OptionToggle component * 🧪 test: e2e coverage for cross-turn background tool calls Proves the full contract through the real pipeline (mock harness): an agent opts an MCP tool in via tool_options.run_in_background, the model dispatches it detached and receives the synthetic handle while the tool is still running (status=running in the rendered ack — the non-blocking guarantee without timing assertions), the tool completes after its turn finalized, and a later user turn recovers the task id from replayed history, polls check_background_task, and renders the collected result. - fake-mcp-server: slow_echo fixture tool (delayed echo) - fake-model: E2E_BACKGROUND_DISPATCH / E2E_BACKGROUND_COLLECT markers - e2e yaml: agents capabilities = defaults + run_in_background * 🔧 fix: Close two background capability gaps from review - Thread backgroundToolsAvailable through the OpenAI-compatible service (derived from app capabilities like codeEnvAvailable/statefulSessions), so agents with tool_options.run_in_background keep the feature on that route; fold the three capability derivations into one helper - Index ephemeral MCP servers by normalizeServerName when excluding tools from background injection: tool names embed the normalized server name while mcpConfig keys the original, so exotic server names previously escaped the injection-time exclusion * 🛂 fix: Fall back to configurable user identity for background task scoping The in-repo routes merge req into the tool-execute configurable, but external hosts of the exported OpenAI-compatible service inject their own loadTools and may not — tasks would then register under an empty user id, collapsing registry isolation to conversationId alone. Resolve the scoping id from req.user.id, then configurable.user_id / user, and cover the isolation with a foreign-user not_found test. * 🧹 chore: Apply repo import sorter to PR-touched files
379 lines
13 KiB
JavaScript
379 lines
13 KiB
JavaScript
const crypto = require('crypto');
|
|
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
|
|
const { batchUploadCodeEnvFiles } = require('~/server/services/Files/Code/crud');
|
|
const {
|
|
getSessionInfo,
|
|
checkIfActive,
|
|
readSandboxFile,
|
|
writeSandboxFile,
|
|
} = require('~/server/services/Files/Code/process');
|
|
const {
|
|
checkAccess,
|
|
getStorageMetadata,
|
|
resolveRequestTenantId,
|
|
enrichWithSkillConfigurable,
|
|
mergeDeploymentSkillIds,
|
|
createDeploymentSkillMethods,
|
|
isDeploymentSkillFileSource,
|
|
getDeploymentSkillDownloadStream,
|
|
} = require('@librechat/api');
|
|
const {
|
|
Permissions,
|
|
FileContext,
|
|
ResourceType,
|
|
PermissionBits,
|
|
AccessRoleIds,
|
|
PrincipalType,
|
|
PermissionTypes,
|
|
isEphemeralAgentId,
|
|
} = require('librechat-data-provider');
|
|
const { checkPermission, grantPermission } = require('~/server/services/PermissionService');
|
|
const { getFileStrategy } = require('~/server/utils/getFileStrategy');
|
|
const db = require('~/models');
|
|
|
|
const deploymentSkillMethods = createDeploymentSkillMethods({
|
|
getSkillById: db.getSkillById,
|
|
getSkillByName: db.getSkillByName,
|
|
listSkillsByAccess: db.listSkillsByAccess,
|
|
listAlwaysApplySkills: db.listAlwaysApplySkills,
|
|
listSkillFiles: db.listSkillFiles,
|
|
getSkillFileByPath: db.getSkillFileByPath,
|
|
updateSkillFileContent: db.updateSkillFileContent,
|
|
updateSkillFileCodeEnvIds: db.updateSkillFileCodeEnvIds,
|
|
});
|
|
|
|
function getSkillDbMethods() {
|
|
return deploymentSkillMethods;
|
|
}
|
|
|
|
function withDeploymentSkillIds(ids = []) {
|
|
return mergeDeploymentSkillIds(ids);
|
|
}
|
|
|
|
function getSkillStrategyFunctions(source) {
|
|
if (isDeploymentSkillFileSource(source)) {
|
|
return {
|
|
getDownloadStream: (_req, filepath) => getDeploymentSkillDownloadStream(filepath),
|
|
};
|
|
}
|
|
return getStrategyFunctions(source);
|
|
}
|
|
|
|
function resolveSkillStorage(req, { isImage = false } = {}) {
|
|
const source = getFileStrategy(req.config, { context: FileContext.skill_file, isImage });
|
|
const strategy = getStrategyFunctions(source);
|
|
if (!strategy.saveBuffer) {
|
|
throw new Error(`Storage backend "${source}" does not support file writes`);
|
|
}
|
|
return { saveBuffer: strategy.saveBuffer, source };
|
|
}
|
|
|
|
function basename(relativePath) {
|
|
const slash = relativePath.lastIndexOf('/');
|
|
return slash === -1 ? relativePath : relativePath.slice(slash + 1);
|
|
}
|
|
|
|
async function saveSkillFileContent({ req, skillId, relativePath, content, mimeType }) {
|
|
const existingFile = await db.getSkillFileByPath(skillId, relativePath);
|
|
const tenantId = resolveRequestTenantId(req);
|
|
const fileId = crypto.randomUUID();
|
|
const filename = basename(relativePath);
|
|
const storageFileName = `${fileId}__${filename}`;
|
|
const buffer = Buffer.from(content, 'utf8');
|
|
const storage = resolveSkillStorage(req, { isImage: mimeType.startsWith('image/') });
|
|
const filepath = await storage.saveBuffer({
|
|
userId: req.user.id,
|
|
buffer,
|
|
fileName: storageFileName,
|
|
basePath: 'uploads',
|
|
tenantId,
|
|
});
|
|
const storageMetadata = getStorageMetadata({ filepath, source: storage.source });
|
|
|
|
let result;
|
|
try {
|
|
result = await db.upsertSkillFile({
|
|
skillId,
|
|
relativePath,
|
|
file_id: fileId,
|
|
filename,
|
|
filepath,
|
|
...storageMetadata,
|
|
source: storage.source,
|
|
mimeType,
|
|
bytes: buffer.length,
|
|
isExecutable: false,
|
|
author: req.user._id ?? req.user.id,
|
|
tenantId,
|
|
});
|
|
if (!result) {
|
|
const error = new Error('Skill file save failed to persist metadata');
|
|
error.code = 'SKILL_FILE_UPSERT_NOT_FOUND';
|
|
throw error;
|
|
}
|
|
} catch (error) {
|
|
const { deleteFile } = getStrategyFunctions(storage.source);
|
|
if (deleteFile) {
|
|
await deleteFile(req, { filepath, user: req.user.id, tenantId }).catch(() => undefined);
|
|
}
|
|
throw error;
|
|
}
|
|
|
|
if (existingFile && existingFile.filepath !== filepath) {
|
|
const { deleteFile } = getStrategyFunctions(existingFile.source);
|
|
if (deleteFile) {
|
|
deleteFile(req, {
|
|
filepath: existingFile.filepath,
|
|
storageKey: existingFile.storageKey,
|
|
storageRegion: existingFile.storageRegion,
|
|
user: existingFile.author ?? req.user.id,
|
|
tenantId: existingFile.tenantId ?? tenantId,
|
|
}).catch(() => undefined);
|
|
}
|
|
}
|
|
|
|
return { bytes: result.bytes, relativePath: result.relativePath };
|
|
}
|
|
|
|
function canCreateSkill({ req }) {
|
|
return checkAccess({
|
|
req,
|
|
user: req.user,
|
|
permissionType: PermissionTypes.SKILLS,
|
|
permissions: [Permissions.USE, Permissions.CREATE],
|
|
getRoleByName: db.getRoleByName,
|
|
});
|
|
}
|
|
|
|
function canEditSkill({ req, skillId }) {
|
|
return checkPermission({
|
|
userId: req.user.id,
|
|
role: req.user.role,
|
|
resourceType: ResourceType.SKILL,
|
|
resourceId: skillId,
|
|
requiredPermission: PermissionBits.EDIT,
|
|
});
|
|
}
|
|
|
|
function isAgentSkillsEnabledForRun({ agent, skillsCapabilityEnabled, ephemeralSkillsToggle }) {
|
|
if (!skillsCapabilityEnabled) {
|
|
return false;
|
|
}
|
|
if (isEphemeralAgentId(agent.id)) {
|
|
if (agent.skills_enabled === false) {
|
|
return false;
|
|
}
|
|
if (agent.skills_enabled === true) {
|
|
return true;
|
|
}
|
|
return ephemeralSkillsToggle === true;
|
|
}
|
|
return agent.skills_enabled === true;
|
|
}
|
|
|
|
function canAuthorSkillFiles({
|
|
agent,
|
|
scopedEditableSkillIds = [],
|
|
skillCreateAllowed,
|
|
skillsCapabilityEnabled,
|
|
ephemeralSkillsToggle,
|
|
}) {
|
|
return (
|
|
isAgentSkillsEnabledForRun({ agent, skillsCapabilityEnabled, ephemeralSkillsToggle }) &&
|
|
(scopedEditableSkillIds.length > 0 || skillCreateAllowed === true)
|
|
);
|
|
}
|
|
|
|
function grantSkillOwner({ req, skillId }) {
|
|
return grantPermission({
|
|
principalType: PrincipalType.USER,
|
|
principalId: req.user.id,
|
|
resourceType: ResourceType.SKILL,
|
|
resourceId: skillId,
|
|
accessRoleId: AccessRoleIds.SKILL_OWNER,
|
|
grantedBy: req.user.id,
|
|
});
|
|
}
|
|
|
|
function getAuthorSkillByName({ req, name }) {
|
|
const author = req.user?._id ?? req.user?.id;
|
|
if (!author) {
|
|
return null;
|
|
}
|
|
return db.getAuthorSkillByName({
|
|
name,
|
|
author,
|
|
tenantId: resolveRequestTenantId(req),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Builds the `skillPrimedIdsByName` map threaded through
|
|
* `buildAgentToolContext`. Centralized here so every runtime route shares
|
|
* one source of truth — if `ResolvedManualSkill` ever renames `_id` or
|
|
* gains new identifying fields, only this helper changes.
|
|
*
|
|
* Combines both manual (`$`-popover) primes AND always-apply primes so
|
|
* `read_file` can:
|
|
* - Relax the `disable-model-invocation: true` gate for either source
|
|
* (the body is already in context; blocking its own files would be
|
|
* nonsensical).
|
|
* - Pin same-name collision lookups to the exact `_id` the resolver
|
|
* primed (otherwise a newer same-name duplicate could shadow the
|
|
* body/file pair within a single turn).
|
|
*
|
|
* On the rare overlap (a name appears in both arrays because upstream
|
|
* dedup was skipped), manual wins — manual invocation is explicit user
|
|
* intent and carries the authoritative `_id` for this turn.
|
|
*
|
|
* Returns `undefined` (not `{}`) when both arrays are empty, so the
|
|
* downstream `enrichWithSkillConfigurable` cleanly omits the field from
|
|
* `mergedConfigurable` rather than threading an empty object.
|
|
*
|
|
* @param {Array<{ name: string, _id: { toString(): string } }> | undefined} manualSkillPrimes
|
|
* @param {Array<{ name: string, _id: { toString(): string } }> | undefined} alwaysApplySkillPrimes
|
|
* @returns {Record<string, string> | undefined}
|
|
*/
|
|
function buildSkillPrimedIdsByName(manualSkillPrimes, alwaysApplySkillPrimes) {
|
|
const manualCount = manualSkillPrimes?.length ?? 0;
|
|
const alwaysApplyCount = alwaysApplySkillPrimes?.length ?? 0;
|
|
if (manualCount === 0 && alwaysApplyCount === 0) {
|
|
return undefined;
|
|
}
|
|
const out = {};
|
|
/* Order matters on the edge case where the same name appears in both
|
|
lists: always-apply goes in first, then manual overwrites — manual
|
|
wins because it's explicit user intent for this turn. */
|
|
if (alwaysApplyCount > 0) {
|
|
for (const p of alwaysApplySkillPrimes) {
|
|
out[p.name] = p._id.toString();
|
|
}
|
|
}
|
|
if (manualCount > 0) {
|
|
for (const p of manualSkillPrimes) {
|
|
out[p.name] = p._id.toString();
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* Builds the per-agent context consumed by ON_TOOL_EXECUTE. Keeping this
|
|
* shape in one Adapter gives every runtime path the same configurable
|
|
* fields and the same primed-skill pinning behavior.
|
|
*
|
|
* @param {object} params
|
|
* @param {object} params.agent
|
|
* @param {object} params.config
|
|
* @param {Record<string, import('@librechat/api').LCAvailableTools>} [params.config.mcpAvailableTools]
|
|
* @param {import('@librechat/api').RequestScopedMCPConnectionStore} [params.config.requestScopedConnections]
|
|
* @returns {object}
|
|
*/
|
|
function buildAgentToolContext({ agent, config }) {
|
|
return {
|
|
agent,
|
|
/** Per-agent resolved endpoint token/pricing config. Retained here because
|
|
* `agentToolContexts` is the one map that holds every agent — including
|
|
* pure subagents pruned from `agentConfigs` — so usage can be priced with
|
|
* the producing agent's config in multi-endpoint graphs. */
|
|
endpointTokenConfig: config.endpointTokenConfig,
|
|
toolRegistry: config.toolRegistry,
|
|
backgroundToolNames: config.backgroundToolNames,
|
|
mcpAvailableTools: config.mcpAvailableTools,
|
|
requestScopedConnections: config.requestScopedConnections,
|
|
userMCPAuthMap: config.userMCPAuthMap,
|
|
tool_resources: config.tool_resources,
|
|
actionsEnabled: config.actionsEnabled,
|
|
accessibleSkillIds: config.accessibleSkillIds,
|
|
activeSkillNames: config.activeSkillNames,
|
|
codeEnvAvailable: config.codeEnvAvailable,
|
|
skillAuthoringAvailable: config.skillAuthoringAvailable,
|
|
fileAuthoringToolNames: config.fileAuthoringToolNames,
|
|
skillPrimedIdsByName:
|
|
buildSkillPrimedIdsByName(config.manualSkillPrimes, config.alwaysApplySkillPrimes) ?? {},
|
|
};
|
|
}
|
|
|
|
function hasOwn(value, key) {
|
|
return Object.prototype.hasOwnProperty.call(value ?? {}, key);
|
|
}
|
|
|
|
/**
|
|
* Applies per-agent runtime context to a loadToolsForExecution result.
|
|
*
|
|
* @param {object} params
|
|
* @param {{ loadedTools: unknown[], configurable?: Record<string, unknown> }} params.result
|
|
* @param {object} params.req
|
|
* @param {object | undefined} params.ctx
|
|
* @param {object | undefined} [params.fallback]
|
|
* @returns {{ loadedTools: unknown[], configurable: Record<string, unknown> }}
|
|
*/
|
|
function enrichLoadedToolsWithAgentContext({ result, req, ctx = {}, fallback = {} }) {
|
|
const codeEnvAvailable = hasOwn(ctx, 'codeEnvAvailable')
|
|
? ctx.codeEnvAvailable === true
|
|
: fallback.codeEnvAvailable === true;
|
|
const skillAuthoringAvailable = hasOwn(ctx, 'skillAuthoringAvailable')
|
|
? ctx.skillAuthoringAvailable === true
|
|
: fallback.skillAuthoringAvailable === true;
|
|
|
|
return enrichWithSkillConfigurable({
|
|
result,
|
|
context: {
|
|
req,
|
|
codeEnvAvailable,
|
|
accessibleSkillIds: ctx.accessibleSkillIds ?? fallback.accessibleSkillIds,
|
|
skillPrimedIdsByName: ctx.skillPrimedIdsByName ?? fallback.skillPrimedIdsByName,
|
|
activeSkillNames: ctx.activeSkillNames ?? fallback.activeSkillNames,
|
|
skillAuthoringAvailable,
|
|
fileAuthoringToolNames: ctx.fileAuthoringToolNames ?? fallback.fileAuthoringToolNames,
|
|
},
|
|
});
|
|
}
|
|
|
|
/** Skill-related properties for ToolExecuteOptions (stable references, allocated once). */
|
|
const skillToolDeps = {
|
|
getSkillByName: deploymentSkillMethods.getSkillByName,
|
|
getAuthorSkillByName,
|
|
createSkill: db.createSkill,
|
|
updateSkill: db.updateSkill,
|
|
deleteSkill: db.deleteSkill,
|
|
canCreateSkill,
|
|
canEditSkill,
|
|
grantSkillOwner,
|
|
saveSkillFileContent,
|
|
listSkillFiles: deploymentSkillMethods.listSkillFiles,
|
|
getStrategyFunctions: getSkillStrategyFunctions,
|
|
batchUploadCodeEnvFiles,
|
|
getSessionInfo,
|
|
checkIfActive,
|
|
updateSkillFileCodeEnvIds: deploymentSkillMethods.updateSkillFileCodeEnvIds,
|
|
getSkillFileByPath: deploymentSkillMethods.getSkillFileByPath,
|
|
updateSkillFileContent: deploymentSkillMethods.updateSkillFileContent,
|
|
/**
|
|
* `read_file` falls back to a sandbox `cat` for `/mnt/data/...` paths
|
|
* and for `{firstSegment}/...` paths whose first segment isn't a known
|
|
* skill name. The handler routes through this when the agent has code
|
|
* execution enabled; the codeapi base URL comes from
|
|
* `LIBRECHAT_CODE_BASEURL` and the sandbox session id is forwarded by
|
|
* the agents-side `ToolNode` via `tc.codeSessionContext`.
|
|
*/
|
|
readSandboxFile,
|
|
writeSandboxFile,
|
|
};
|
|
|
|
function getSkillToolDeps() {
|
|
return skillToolDeps;
|
|
}
|
|
|
|
module.exports = {
|
|
getSkillToolDeps,
|
|
canAuthorSkillFiles,
|
|
isAgentSkillsEnabledForRun,
|
|
getSkillDbMethods,
|
|
withDeploymentSkillIds,
|
|
getSkillStrategyFunctions,
|
|
enrichWithSkillConfigurable,
|
|
buildSkillPrimedIdsByName,
|
|
buildAgentToolContext,
|
|
enrichLoadedToolsWithAgentContext,
|
|
};
|