LibreChat/api/server/services/Endpoints/agents/addedConvo.js
Danny Avila 6357ea10c1
🧭 feat: Scope Model Spec Skills (#13522)
* feat: scope model spec skills

* style: format skill catalog limit

* fix: serialize model spec skill resolution

* test: satisfy model spec load config typing

* fix: apply model spec skills to added conversations

* fix: support alwaysApply frontmatter alias

* fix: address model spec skills review
2026-06-05 10:22:02 -04:00

224 lines
7.8 KiB
JavaScript

const { logger } = require('@librechat/data-schemas');
const {
ADDED_AGENT_ID,
initializeAgent,
validateAgentModel,
resolveAgentScopedSkillIds,
resolveModelSpecSkillIds,
loadAddedAgent: loadAddedAgentFn,
} = require('@librechat/api');
const { isEphemeralAgentId } = require('librechat-data-provider');
const { filterFilesByAgentAccess } = require('~/server/services/Files/permissions');
const { getMCPServerTools } = require('~/server/services/Config');
const { canAuthorSkillFiles } = require('./skillDeps');
const db = require('~/models');
const loadAddedAgent = (params) =>
loadAddedAgentFn(params, { getAgent: db.getAgent, getMCPServerTools });
/**
* Process addedConvo for parallel agent execution.
* Creates a parallel agent config from an added conversation.
*
* When an added agent has no incoming edges, it becomes a start node
* and runs in parallel with the primary agent automatically.
*
* Edge cases handled:
* - Primary agent has edges (handoffs): Added agent runs in parallel with primary,
* but doesn't participate in the primary's handoff graph
* - Primary agent has agent_ids (legacy chain): Added agent runs in parallel with primary,
* but doesn't participate in the chain
* - Primary agent has both: Added agent is independent, runs parallel from start
*
* @param {Object} params
* @param {import('express').Request} params.req
* @param {import('express').Response} params.res
* @param {Object} params.endpointOption - The endpoint option containing addedConvo
* @param {Object} params.modelsConfig - The models configuration
* @param {Function} params.logViolation - Function to log violations
* @param {Function} params.loadTools - Function to load agent tools
* @param {Array} params.requestFiles - Request files
* @param {string} params.conversationId - The conversation ID
* @param {string} [params.parentMessageId] - The parent message ID for thread filtering
* @param {Set} params.allowedProviders - Set of allowed providers
* @param {Map} params.agentConfigs - Map of agent configs to add to
* @param {string} params.primaryAgentId - The primary agent ID
* @param {Object|undefined} params.userMCPAuthMap - User MCP auth map to merge into
* @param {Array} [params.accessibleSkillIds] - Full VIEW-accessible skill IDs for the user
* @param {Array} [params.editableSkillIds] - Full EDIT-accessible skill IDs for the user
* @param {boolean} [params.skillsCapabilityEnabled] - Whether endpoint Skills are enabled
* @param {boolean} [params.ephemeralSkillsToggle] - Per-request ephemeral Skills badge state
* @param {boolean} [params.skillCreateAllowed] - Whether the user can create Skills
* @param {Record<string, boolean>} [params.skillStates] - Per-user Skill active overrides
* @param {boolean} [params.defaultActiveOnShare] - Default active state for shared Skills
* @param {boolean} [params.codeEnvAvailable] - `execute_code` capability flag;
* forwarded verbatim to the added agent's `initializeAgent`. @see
* InitializeAgentParams.codeEnvAvailable for full semantics.
* @returns {Promise<{userMCPAuthMap: Object|undefined}>} The updated userMCPAuthMap
*/
const processAddedConvo = async ({
req,
res,
endpointOption,
modelsConfig,
logViolation,
loadTools,
requestFiles,
conversationId,
parentMessageId,
allowedProviders,
agentConfigs,
primaryAgentId,
primaryAgent,
userMCPAuthMap,
accessibleSkillIds = [],
editableSkillIds = [],
skillsCapabilityEnabled = false,
ephemeralSkillsToggle = false,
skillCreateAllowed = false,
skillStates,
defaultActiveOnShare,
codeEnvAvailable,
}) => {
const addedConvo = endpointOption.addedConvo;
if (addedConvo == null) {
return { userMCPAuthMap };
}
logger.debug('[processAddedConvo] Processing added conversation', {
model: addedConvo.model,
agentId: addedConvo.agent_id,
endpoint: addedConvo.endpoint,
});
try {
const addedAgent = await loadAddedAgent({ req, conversation: addedConvo, primaryAgent });
if (!addedAgent) {
return { userMCPAuthMap };
}
const addedValidation = await validateAgentModel({
req,
res,
modelsConfig,
logViolation,
agent: addedAgent,
});
if (!addedValidation.isValid) {
logger.warn(
`[processAddedConvo] Added agent validation failed: ${addedValidation.error?.message}`,
);
return { userMCPAuthMap };
}
const selectedModelSpec =
addedConvo.spec && Array.isArray(req.config?.modelSpecs?.list)
? req.config.modelSpecs.list.find((modelSpec) => modelSpec.name === addedConvo.spec)
: null;
if (
addedAgent &&
isEphemeralAgentId(addedAgent.id) &&
selectedModelSpec &&
Object.hasOwn(selectedModelSpec, 'skills')
) {
if (selectedModelSpec.skills === true) {
addedAgent.skills_enabled = true;
delete addedAgent.skills;
} else if (selectedModelSpec.skills === false) {
addedAgent.skills_enabled = false;
addedAgent.skills = [];
} else if (Array.isArray(selectedModelSpec.skills)) {
const resolvedSkillIds = await resolveModelSpecSkillIds({
names: selectedModelSpec.skills,
accessibleSkillIds,
getSkillByName: db.getSkillByName,
});
addedAgent.skills_enabled = true;
addedAgent.skills = resolvedSkillIds.map((id) => id.toString());
}
}
const scopedSkillIds = resolveAgentScopedSkillIds({
agent: addedAgent,
accessibleSkillIds,
skillsCapabilityEnabled,
ephemeralSkillsToggle,
});
const scopedEditableSkillIds = resolveAgentScopedSkillIds({
agent: addedAgent,
accessibleSkillIds: editableSkillIds,
skillsCapabilityEnabled,
ephemeralSkillsToggle,
});
const addedConfig = await initializeAgent(
{
req,
res,
loadTools,
requestFiles,
conversationId,
parentMessageId,
agent: addedAgent,
endpointOption,
allowedProviders,
accessibleSkillIds: scopedSkillIds,
skillAuthoringAvailable: canAuthorSkillFiles({
agent: addedAgent,
scopedEditableSkillIds,
skillCreateAllowed,
skillsCapabilityEnabled,
ephemeralSkillsToggle,
}),
codeEnvAvailable,
skillStates,
defaultActiveOnShare,
},
{
getFiles: db.getFiles,
getUserKey: db.getUserKey,
getMessages: db.getMessages,
getConvoFiles: db.getConvoFiles,
updateFilesUsage: db.updateFilesUsage,
getUserCodeFiles: db.getUserCodeFiles,
getUserKeyValues: db.getUserKeyValues,
getToolFilesByIds: db.getToolFilesByIds,
getCodeGeneratedFiles: db.getCodeGeneratedFiles,
filterFilesByAgentAccess,
listSkillsByAccess: db.listSkillsByAccess,
listAlwaysApplySkills: db.listAlwaysApplySkills,
getSkillByName: db.getSkillByName,
},
);
if (userMCPAuthMap != null) {
Object.assign(userMCPAuthMap, addedConfig.userMCPAuthMap ?? {});
} else {
userMCPAuthMap = addedConfig.userMCPAuthMap;
}
const addedAgentId = addedConfig.id || ADDED_AGENT_ID;
agentConfigs.set(addedAgentId, addedConfig);
// No edges needed - agent without incoming edges becomes a start node
// and runs in parallel with the primary agent automatically.
// This is independent of any edges/agent_ids the primary agent has.
logger.debug(
`[processAddedConvo] Added parallel agent: ${addedAgentId} (primary: ${primaryAgentId}, ` +
`primary has edges: ${!!endpointOption.edges}, primary has agent_ids: ${!!endpointOption.agent_ids})`,
);
return { userMCPAuthMap };
} catch (err) {
logger.error('[processAddedConvo] Error processing addedConvo for parallel agent', err);
return { userMCPAuthMap };
}
};
module.exports = {
processAddedConvo,
ADDED_AGENT_ID,
};