import { Providers } from '@librechat/agents'; import { logger } from '@librechat/data-schemas'; import { Tools, Constants, ErrorTypes, EModelEndpoint, EToolResources, paramEndpoints, isAgentsEndpoint, replaceSpecialVars, providerEndpointMap, } from 'librechat-data-provider'; import type { AgentToolResources, AgentToolOptions, TEndpointOption, TFile, Agent, TUser, } from 'librechat-data-provider'; import type { GenericTool, LCToolRegistry, ToolMap, LCTool } from '@librechat/agents'; import type { Response as ServerResponse } from 'express'; import type { IMongoFile } from '@librechat/data-schemas'; import type { InitializeResultBase, ServerRequest, EndpointDbMethods } from '~/types'; import { optionalChainWithEmptyCheck, extractLibreChatParams, getModelMaxTokens, getThreadData, } from '~/utils'; import { filterFilesByEndpointConfig } from '~/files'; import { generateArtifactsPrompt } from '~/prompts'; import { getProviderConfig } from '~/endpoints'; import { injectSkillCatalog, resolveManualSkills, resolveAlwaysApplySkills, unionPrimeAllowedTools, MAX_PRIMED_SKILLS_PER_TURN, } from './skills'; import { registerCodeExecutionTools } from './tools'; import { primeResources } from './resources'; import type { ResolvedManualSkill, ResolvedAlwaysApplySkill } from './skills'; import type { TFilterFilesByAgentAccess } from './resources'; /** * Fraction of context budget reserved as headroom when no explicit maxContextTokens is set. * Reduced from 0.10 to 0.05 alongside the introduction of summarization, which actively * manages overflow. `createRun` can further override this via `SummarizationConfig.reserveRatio`. */ const DEFAULT_RESERVE_RATIO = 0.05; const temporalSpecialVarRegex = /{{\s*(current_date|current_datetime|iso_datetime)\s*}}/i; function hasTemporalSpecialVars(text: string): boolean { return temporalSpecialVarRegex.test(text); } function appendAdditionalInstructions(agent: Agent, text?: string | null): void { if (text == null || text === '') { return; } agent.additional_instructions = [agent.additional_instructions ?? '', text] .filter(Boolean) .join('\n\n'); } function getToolName(tool: unknown): string | undefined { if (tool == null || typeof tool !== 'object') { return undefined; } const { name } = tool as { name?: unknown }; return typeof name === 'string' ? name : undefined; } function hasToolDefinition(toolDefinitions: LCTool[] | undefined, name: string): boolean { return toolDefinitions?.some((toolDefinition) => toolDefinition.name === name) === true; } function resolveAnthropicToolConflicts({ provider, tools, toolDefinitions, }: { provider?: string; tools?: unknown[]; toolDefinitions?: LCTool[]; }): unknown[] | undefined { if (provider !== Providers.ANTHROPIC || !tools?.length) { return tools; } if (!hasToolDefinition(toolDefinitions, Tools.web_search)) { return tools; } let removed = 0; const resolvedTools = tools.filter((tool) => { const shouldRemove = getToolName(tool) === Tools.web_search; if (shouldRemove) { removed += 1; } return !shouldRemove; }); if (removed > 0) { logger.debug( `[initializeAgent] Removed ${removed} Anthropic native web_search tool(s); LibreChat web_search is enabled.`, ); } return resolvedTools; } /** * Extended agent type with additional fields needed after initialization */ export type InitializedAgent = Agent & { tools: GenericTool[]; /** @deprecated use requestAttachments or agentContextAttachments based on sharing semantics. */ attachments: IMongoFile[]; /** Files attached to the current user message/run and safe to share across run agents. */ requestAttachments: IMongoFile[]; /** Files attached to this agent's permanent context via tool_resources. */ agentContextAttachments: IMongoFile[]; toolContextMap: Record; dynamicToolContextMap?: Record; maxContextTokens: number; /** Pre-ratio context budget (agentMaxContextNum - maxOutputTokensNum). Used by createRun to apply a configurable reserve ratio. */ baseContextTokens?: number; useLegacyContent: boolean; resendFiles: boolean; tool_resources?: AgentToolResources; userMCPAuthMap?: Record>; /** Tool map for ToolNode to use when executing tools (required for PTC) */ toolMap?: ToolMap; /** Tool registry for PTC and tool search (only present when MCP tools with env classification exist) */ toolRegistry?: LCToolRegistry; /** Serializable tool definitions for event-driven execution */ toolDefinitions?: LCTool[]; /** Precomputed flag indicating if any tools have defer_loading enabled (for efficient runtime checks) */ hasDeferredTools?: boolean; /** Whether the actions capability is enabled (resolved during tool loading) */ actionsEnabled?: boolean; /** Maximum characters allowed in a single tool result before truncation. */ maxToolResultChars?: number; /** * Whether the code-execution environment is available *for this agent*. * Narrower than the incoming `params.codeEnvAvailable` admin flag — this * is `admin_capability_enabled && agent.tools.includes('execute_code')`, * computed once here so downstream code (`injectSkillCatalog`, * `enrichWithSkillConfigurable`, `primeInvokedSkills`) doesn't have to * re-scan the tool list on every runtime handler invocation. * Authoritative for both persisted and ephemeral agents: the * ephemeral-agent toggle is reconciled into `agent.tools` upstream * (`packages/api/src/agents/added.ts`), so the check is uniform. */ codeEnvAvailable: boolean; /** Accessible skill IDs for ACL checking at execute time */ accessibleSkillIds?: import('mongoose').Types.ObjectId[]; /** * Names of skills the runtime can resolve, mirroring `accessibleSkillIds`. * Surfaced to the runtime so handlers like `read_file` can decide if a * `{firstSegment}/...` path refers to a real skill (vs. a code-env path * that should be routed to the bash fallback) without an extra DB lookup. */ activeSkillNames?: Set; /** Number of skills in the catalog (used to determine if SkillTool should be registered) */ skillCount?: number; /** * Skills the user manually invoked for this turn via the `$` popover, resolved * to their SKILL.md bodies. The AgentClient injects these as meta user * messages right before the latest user message in the LLM's formatted * message array — deterministic priming without a tool roundtrip. */ manualSkillPrimes?: ResolvedManualSkill[]; /** * Skills auto-primed this turn because their `always-apply` frontmatter * flag is set. Resolved against the same `accessibleSkillIds` set and * subjected to the same active-state / ACL filters as the catalog, then * handed to the AgentClient for splicing alongside manual primes. Their * `allowedTools` entries also union into the agent's effective tool set * via `unionPrimeAllowedTools` (same pipeline as manual primes). */ alwaysApplySkillPrimes?: ResolvedAlwaysApplySkill[]; /** * Pre-uploaded code-env file refs from `tool_resources.execute_code` * (carries the conversation's prior-turn generated artifacts and any * user uploads). Captured by the `loadTools` callback; the AgentClient * merges these across all run agents into `Graph.sessions[EXECUTE_CODE]` * before run start so the very first `execute_code` / `bash_tool` call * sees them as `_injected_files`. Without this seed the agents-side * `CodeExecutor` falls back to `/files/{session_id}` — but `session_id` * is itself only populated by a previous successful execution, so on * call #1 the sandbox can't see the files at all. */ primedCodeFiles?: import('@librechat/agents').CodeEnvFile[]; }; export const DEFAULT_MAX_CONTEXT_TOKENS = 32000; /** * Parameters for initializing an agent * Matches the CJS signature from api/server/services/Endpoints/agents/agent.js */ export interface InitializeAgentParams { /** Request object */ req: ServerRequest; /** Response object */ res: ServerResponse; /** Agent to initialize */ agent: Agent; /** Conversation ID (optional) */ conversationId?: string | null; /** Parent message ID for determining the current thread (optional) */ parentMessageId?: string | null; /** Request files */ requestFiles?: IMongoFile[]; /** Function to load agent tools */ loadTools?: (params: { req: ServerRequest; res: ServerResponse; provider: string; agentId: string; tools: string[]; model: string | null; tool_options: AgentToolOptions | undefined; tool_resources: AgentToolResources | undefined; }) => Promise<{ /** Full tool instances (only present when definitionsOnly=false) */ tools?: GenericTool[]; toolContextMap?: Record; dynamicToolContextMap?: Record; userMCPAuthMap?: Record>; toolRegistry?: LCToolRegistry; /** Serializable tool definitions for event-driven mode */ toolDefinitions?: LCTool[]; hasDeferredTools?: boolean; actionsEnabled?: boolean; /** * Pre-uploaded code-env file refs for the agent's * `tool_resources.execute_code`. Bubbled up so the run host can seed * `Graph.sessions[EXECUTE_CODE]` before the first tool call — * otherwise `_injected_files` is empty on call #1 and prior-turn * artifacts don't reach the sandbox. */ primedCodeFiles?: import('@librechat/agents').CodeEnvFile[]; } | null>; /** Endpoint option (contains model_parameters and endpoint info) */ endpointOption?: Partial; /** Set of allowed providers */ allowedProviders: Set; /** Whether this is the initial agent */ isInitialAgent?: boolean; /** Accessible skill IDs for this user (pre-computed by the caller via ACL query) */ accessibleSkillIds?: import('mongoose').Types.ObjectId[]; /** Whether the code execution environment is available (execute_code capability enabled) */ codeEnvAvailable?: boolean; /** Per-user skill active/inactive overrides for filtering the skill catalog. */ skillStates?: Record; /** Admin-configured default for shared skills (`true` = shared skills auto-activate). */ defaultActiveOnShare?: boolean; /** * Skill names the user invoked manually for this turn via the `$` popover. * Resolved here (ACL + active-state filtered) and attached to the returned * InitializedAgent as `manualSkillPrimes` for the AgentClient to inject as * meta user messages before the LLM call. */ manualSkills?: string[]; } /** * Database methods required for agent initialization * Most methods come from data-schemas via createMethods() * getConvoFiles not yet in data-schemas but included here for consistency */ export interface InitializeAgentDbMethods extends EndpointDbMethods { /** Update usage tracking for multiple files */ updateFilesUsage: (files: Array<{ file_id: string }>, fileIds?: string[]) => Promise; /** Get files from database */ getFiles: (filter: unknown, sort: unknown, select: unknown) => Promise; /** Filter files by agent access permissions (ownership or agent attachment) */ filterFilesByAgentAccess?: TFilterFilesByAgentAccess; /** Get tool files by IDs (user-uploaded files only, code files handled separately) */ getToolFilesByIds: (fileIds: string[], toolSet: Set) => Promise; /** Get conversation file IDs */ getConvoFiles: (conversationId: string) => Promise; /** Get code-generated files by conversation ID and the file_ids * referenced from messages in the current thread (collected via * `messages.files[].file_id` during thread walk). */ getCodeGeneratedFiles?: (conversationId: string, threadFileIds?: string[]) => Promise; /** Get user-uploaded execute_code files by file IDs (from message.files in thread) */ getUserCodeFiles?: (fileIds: string[]) => Promise; /** Get messages for a conversation (supports select for field projection) */ getMessages?: ( filter: { conversationId: string }, select?: string, ) => Promise; }> | null>; /** List skill summaries for catalog injection (paginated, omits body/frontmatter) */ listSkillsByAccess?: (params: { accessibleIds: import('mongoose').Types.ObjectId[]; limit: number; cursor?: string | null; }) => Promise<{ skills: Array<{ _id: import('mongoose').Types.ObjectId; name: string; description: string; author: import('mongoose').Types.ObjectId; /** * When `true`, the skill is excluded from the catalog injected into * the agent's additional_instructions and the model cannot invoke it * via the `skill` tool. Manual `$` invocation is unaffected. */ disableModelInvocation?: boolean; /** * When `false`, the skill is hidden from the `$` popover and rejected * by the manual-invocation resolver. Defaults to `true`. */ userInvocable?: boolean; }>; has_more?: boolean; after?: string | null; }>; /** * Load a single skill by name, constrained to an ACL-accessible ID set. * Returns the full document (including `body`) so manual invocation can * prime SKILL.md without a second DB round-trip. * * `preferUserInvocable` (manual paths): on a same-name collision, * prefer the newest doc with `userInvocable !== false`. * `preferModelInvocable` (model paths — `skill` / `read_file`): on a * same-name collision, prefer the newest doc with * `disableModelInvocation !== true`. Both fall back to the newest match * so the explicit-rejection error paths still fire when only the * non-preferred variant exists. */ getSkillByName?: ( name: string, accessibleIds: import('mongoose').Types.ObjectId[], options?: { preferUserInvocable?: boolean; preferModelInvocable?: boolean }, ) => Promise<{ _id: import('mongoose').Types.ObjectId; name: string; body: string; author: import('mongoose').Types.ObjectId; /** * Skill-declared tool allowlist, forwarded verbatim from the skill doc. * Surfaced so the resolver can carry it onto `ResolvedManualSkill` for * future runtime enforcement without a second round-trip. */ allowedTools?: string[]; /** * Set when the skill was authored with `disable-model-invocation: true`. * The skill tool handler short-circuits on this so a model that names * such a skill (e.g. via hallucination or stale catalog) gets a clear * rejection instead of silently executing. */ disableModelInvocation?: boolean; /** * Set when the skill was authored with `user-invocable: false`. The * manual-invocation resolver skips with a warn log so an API-direct * caller can't bypass the popover-side filter. */ userInvocable?: boolean; } | null>; /** * Load accessible skills with `alwaysApply: true`, eagerly including * `body` so the priming pipeline can splice at turn start without a * per-skill round-trip. Cursor-paginated so the resolver can fill its * active-state budget even when early-sorted rows are inactive for * the current user. */ listAlwaysApplySkills?: (params: { accessibleIds: import('mongoose').Types.ObjectId[]; limit: number; cursor?: string | null; }) => Promise<{ skills: Array<{ _id: import('mongoose').Types.ObjectId; name: string; body: string; author: import('mongoose').Types.ObjectId; allowedTools?: string[]; }>; has_more?: boolean; after?: string | null; }>; } /** * Initializes an agent for use in requests. * Handles file processing, tool loading, provider configuration, and context token calculations. * * This function is exported from @librechat/api and replaces the CJS version from * api/server/services/Endpoints/agents/agent.js * * @param params - Initialization parameters * @param deps - Optional dependency injection for testing * @returns Promise resolving to initialized agent with tools and configuration * @throws Error if agent provider is not allowed or if required dependencies are missing */ export async function initializeAgent( params: InitializeAgentParams, db?: InitializeAgentDbMethods, ): Promise { const { req, res, agent, loadTools, requestFiles = [], conversationId, endpointOption, parentMessageId, allowedProviders, isInitialAgent = false, } = params; if (!db) { throw new Error('initializeAgent requires db methods to be passed'); } if ( isAgentsEndpoint(endpointOption?.endpoint) && allowedProviders.size > 0 && !allowedProviders.has(agent.provider) ) { throw new Error( `{ "type": "${ErrorTypes.INVALID_AGENT_PROVIDER}", "info": "${agent.provider}" }`, ); } let currentFiles: IMongoFile[] | undefined; const _modelOptions = structuredClone( Object.assign( { model: agent.model }, agent.model_parameters ?? { model: agent.model }, isInitialAgent === true ? endpointOption?.model_parameters : {}, ), ); const { resendFiles, maxContextTokens, modelOptions } = extractLibreChatParams( _modelOptions as Record, ); const provider = agent.provider; agent.endpoint = provider; /** * Load conversation files for ALL agents, not just the initial agent. * This enables handoff agents to access files that were uploaded earlier * in the conversation. Without this, file_search and execute_code tools * on handoff agents would fail to find previously attached files. */ if (conversationId != null && resendFiles) { const fileIds = (await db.getConvoFiles(conversationId)) ?? []; const toolResourceSet = new Set(); for (const tool of agent.tools ?? []) { if (EToolResources[tool as keyof typeof EToolResources]) { toolResourceSet.add(EToolResources[tool as keyof typeof EToolResources]); } } const toolFiles = (await db.getToolFilesByIds(fileIds, toolResourceSet)) as IMongoFile[]; /** * Retrieve execute_code files filtered to the current thread. * This includes both code-generated files and user-uploaded execute_code files. */ let codeGeneratedFiles: IMongoFile[] = []; let userCodeFiles: IMongoFile[] = []; if (toolResourceSet.has(EToolResources.execute_code)) { let threadFileIds: string[] | undefined; if (parentMessageId && parentMessageId !== Constants.NO_PARENT && db.getMessages) { /** Only select fields needed for thread traversal. Both * `files` (user uploads) and `attachments` (code-execution * outputs from `processCodeOutput`) carry the `file_id` * refs the next turn must prime — selecting only `files` * silently drops every code-output ref. */ const messages = await db.getMessages( { conversationId }, 'messageId parentMessageId files attachments', ); if (messages && messages.length > 0) { /** Walk the parent chain and collect file_ids referenced by * any message in the thread (`messages.files[].file_id` + * `messages.attachments[].file_id`). Used as the primary * anchor for both `getCodeGeneratedFiles` and * `getUserCodeFiles` — message ids no longer needed at * this layer. */ threadFileIds = getThreadData(messages, parentMessageId).fileIds; } } /** Code-generated and user-uploaded execute_code files share the * same primary anchor: file_ids referenced by messages in the * current thread. The two queries differ only by `context` * (`execute_code` for generated outputs, others for uploads). * Anchoring both on `threadFileIds` reaches files regardless of * which sibling first generated them — see `getCodeGeneratedFiles` * for the branched-conversation rationale. */ if (db.getCodeGeneratedFiles) { codeGeneratedFiles = (await db.getCodeGeneratedFiles( conversationId, threadFileIds, )) as IMongoFile[]; } if (db.getUserCodeFiles && threadFileIds && threadFileIds.length > 0) { userCodeFiles = (await db.getUserCodeFiles(threadFileIds)) as IMongoFile[]; } } const allToolFiles = toolFiles.concat(codeGeneratedFiles, userCodeFiles); if (requestFiles.length || allToolFiles.length) { currentFiles = (await db.updateFilesUsage(requestFiles.concat(allToolFiles))) as IMongoFile[]; } } else if (requestFiles.length) { currentFiles = (await db.updateFilesUsage(requestFiles)) as IMongoFile[]; } if (currentFiles && currentFiles.length) { let endpointType: EModelEndpoint | undefined; if (!paramEndpoints.has(agent.endpoint ?? '')) { endpointType = EModelEndpoint.custom; } currentFiles = filterFilesByEndpointConfig(req, { files: currentFiles, endpoint: agent.endpoint ?? '', endpointType, }); } const { attachments: primedAttachments, requestAttachments: primedRequestAttachments, agentContextAttachments: primedAgentContextAttachments, tool_resources, } = await primeResources({ req: req as never, getFiles: db.getFiles as never, filterFiles: db.filterFilesByAgentAccess, appConfig: req.config, agentId: agent.id, attachments: currentFiles ? (Promise.resolve(currentFiles) as unknown as Promise) : undefined, tool_resources: agent.tool_resources, requestFileSet: new Set(requestFiles?.map((file) => file.file_id)), }); /** * Pre-resolve manually-invoked + always-apply skill primes so their * `allowed-tools` can be unioned into the agent's effective tool set * BEFORE `loadTools` runs. Single load is correctness-critical: a * second `loadTools` pass would compute its own `userMCPAuthMap` / * `toolContextMap` / OAuth flow state that the InitializedAgent never * sees, so an MCP tool added via `allowed-tools` would be visible to * the model but fail at execution time without its per-user auth * context. * * Resolution uses `params.accessibleSkillIds` (not the active-filtered * subset that `injectSkillCatalog` will produce later) — see * `resolveManualSkills` doc for why a skill outside the catalog cap can * still be authorizable for direct manual invocation. * * Manual + always-apply primes feed the same `unionPrimeAllowedTools` * call — the helper is pure / set-based, so concatenating the two * lists gives the right union with no double-counting. Manual primes * go first so their names win on dedup (primes earlier in the list * contribute before the same name gets deduped on a later prime). */ const hasSkillAccess = params.accessibleSkillIds && params.accessibleSkillIds.length > 0; let manualSkillPrimes: ResolvedManualSkill[] | undefined; let alwaysApplySkillPrimes: ResolvedAlwaysApplySkill[] | undefined; let extraAllowedToolNames: string[] = []; let perSkillExtras: Map = new Map(); if (hasSkillAccess) { const [manualPrimesResult, alwaysApplyPrimesResult] = await Promise.all([ params.manualSkills?.length && db.getSkillByName ? resolveManualSkills({ names: params.manualSkills, getSkillByName: db.getSkillByName, accessibleSkillIds: params.accessibleSkillIds!, userId: req.user?.id, skillStates: params.skillStates, defaultActiveOnShare: params.defaultActiveOnShare, }) : Promise.resolve(undefined), db.listAlwaysApplySkills ? resolveAlwaysApplySkills({ listAlwaysApplySkills: db.listAlwaysApplySkills, accessibleSkillIds: params.accessibleSkillIds!, userId: req.user?.id, skillStates: params.skillStates, defaultActiveOnShare: params.defaultActiveOnShare, }) : Promise.resolve(undefined), ]); manualSkillPrimes = manualPrimesResult; alwaysApplySkillPrimes = alwaysApplyPrimesResult; /** * Cross-list dedup: when a user `$`-invokes a skill that is also * marked `always-apply`, the always-apply copy is dropped here so * the same SKILL.md body isn't primed twice in the same turn. * Manual wins because it sits closer to the user message and * carries explicit intent. Done at the initializer (not just at * splice time in `injectSkillPrimes`) so persisted user-bubble * `alwaysAppliedSkills` pills reflect the post-dedup set and the * tool-union step below doesn't bill allowed-tools to the dropped * always-apply entry. */ if ( alwaysApplySkillPrimes && alwaysApplySkillPrimes.length > 0 && manualSkillPrimes && manualSkillPrimes.length > 0 ) { const manualNames = new Set(manualSkillPrimes.map((p) => p.name)); const deduped = alwaysApplySkillPrimes.filter((p) => !manualNames.has(p.name)); const removed = alwaysApplySkillPrimes.length - deduped.length; if (removed > 0) { logger.info( `[initializeAgent] Dropped ${removed} always-apply prime(s) already present in the manual list; same-named skills prime only once per turn.`, ); alwaysApplySkillPrimes = deduped; } } /** * Enforce the combined `MAX_PRIMED_SKILLS_PER_TURN` ceiling up-front * so persisted user-bubble `alwaysAppliedSkills` pills stay in sync * with what actually gets primed. `injectSkillPrimes` re-applies the * cap as defense-in-depth at splice time. Always-apply primes are * truncated first — manual invocation is explicit user intent and * should never be silently dropped. */ const manualCount = manualSkillPrimes?.length ?? 0; const alwaysApplyCount = alwaysApplySkillPrimes?.length ?? 0; if (alwaysApplySkillPrimes && manualCount + alwaysApplyCount > MAX_PRIMED_SKILLS_PER_TURN) { const budgetForAlwaysApply = Math.max(0, MAX_PRIMED_SKILLS_PER_TURN - manualCount); const dropped = alwaysApplyCount - budgetForAlwaysApply; logger.warn( `[initializeAgent] Combined primes (${manualCount} manual + ${alwaysApplyCount} always-apply) exceeds MAX_PRIMED_SKILLS_PER_TURN (${MAX_PRIMED_SKILLS_PER_TURN}); truncating ${dropped} always-apply prime(s) so persisted user-message pills stay in sync with what got primed.`, ); alwaysApplySkillPrimes = alwaysApplySkillPrimes.slice(0, budgetForAlwaysApply); } const primesForUnion = [...(manualSkillPrimes ?? []), ...(alwaysApplySkillPrimes ?? [])]; if (primesForUnion.length > 0) { const union = unionPrimeAllowedTools({ primes: primesForUnion, agentToolNames: agent.tools ?? [], }); extraAllowedToolNames = union.extraToolNames; perSkillExtras = union.perSkillExtras; } } const baseToolNames = agent.tools ?? []; const requestedToolNames = extraAllowedToolNames.length > 0 ? [...baseToolNames, ...extraAllowedToolNames] : baseToolNames; /** * `loadTools` failures take two forms: * 1. The wrapper throws — rare; only when something around the * try/catch in `createToolLoader` itself fails. * 2. The wrapper returns `undefined` — the typical CJS path: every * production loader (`createToolLoader` in `initialize.js`, * `openai.js`, `responses.js`) catches `loadAgentTools` errors and * returns `undefined`. Without explicit handling, the empty * fallback object below would silently drop the agent's baseline * tools for the turn (not just the skill-added extras). * * If a skill-contributed `allowed-tools` entry is the culprit, retry * with just `agent.tools` so the agent's own tools still load (the * dropped-tools debug log below picks up which extras vanished). If * the retry-without-extras also fails, propagate / fall through with * the empty fallback — the agent's own tools are the problem. */ const callLoadTools = async (tools: string[]) => loadTools?.({ req, res, provider, agentId: agent.id, tools, model: agent.model, tool_options: agent.tool_options, tool_resources, }); let loadToolsResult; const initialFailedSilently = (result: unknown) => result == null && extraAllowedToolNames.length > 0; try { loadToolsResult = await callLoadTools(requestedToolNames); } catch (err) { if (extraAllowedToolNames.length > 0) { logger.warn( `[allowedTools] loadTools threw with skill-added extras [${extraAllowedToolNames.join(', ')}]; retrying without them:`, err instanceof Error ? err.message : err, ); loadToolsResult = await callLoadTools(baseToolNames); } else { throw err; } } if (initialFailedSilently(loadToolsResult)) { /* Production loaders swallow errors and return undefined. Treat that the same as a throw when extras were requested — the agent's own tools must still load. */ logger.warn( `[allowedTools] loadTools returned no result with skill-added extras [${extraAllowedToolNames.join(', ')}]; retrying without them.`, ); loadToolsResult = await callLoadTools(baseToolNames); } const { toolRegistry, toolContextMap, dynamicToolContextMap, userMCPAuthMap, toolDefinitions: loadedToolDefinitions, hasDeferredTools, actionsEnabled, tools: structuredTools, primedCodeFiles, } = loadToolsResult ?? { tools: [], toolContextMap: {}, dynamicToolContextMap: {}, userMCPAuthMap: undefined, toolRegistry: undefined, toolDefinitions: [], hasDeferredTools: false, actionsEnabled: undefined, primedCodeFiles: undefined, }; let toolDefinitions = loadedToolDefinitions; /** * Tolerant filter: anything `loadTools` couldn't resolve (capability * disabled, plugin missing, name unknown to the registry) is silently * dropped with an attributed debug log. Cross-ecosystem skills authored * against tools LibreChat hasn't shipped yet (Claude Code's `edit_file`, * etc.) import without breaking — they light up automatically once * support lands. Skips when there were no extras to begin with. */ if (extraAllowedToolNames.length > 0) { const loadedNames = new Set((toolDefinitions ?? []).map((d) => d.name)); const dropped = extraAllowedToolNames.filter((n) => !loadedNames.has(n)); if (dropped.length > 0) { const sources: string[] = []; for (const [skillName, names] of perSkillExtras) { const droppedFromSkill = names.filter((n) => !loadedNames.has(n)); if (droppedFromSkill.length > 0) { sources.push(`"${skillName}" → [${droppedFromSkill.join(', ')}]`); } } logger.debug( `[allowedTools] Dropped unrecognized tool names: ${ sources.length > 0 ? sources.join('; ') : dropped.join(', ') }`, ); } } const { getOptions, overrideProvider, customEndpointConfig } = getProviderConfig({ provider, appConfig: req.config, }); if (overrideProvider !== agent.provider) { agent.provider = overrideProvider; } const finalModelOptions = { ...modelOptions, model: agent.model, }; const options: InitializeResultBase = await getOptions({ req, endpoint: provider, model_parameters: finalModelOptions, db, }); const llmConfig = options.llmConfig as Record; const tokensModel = agent.provider === EModelEndpoint.azureOpenAI ? agent.model : (llmConfig?.model as string); const maxOutputTokens = optionalChainWithEmptyCheck( llmConfig?.maxOutputTokens as number | undefined, llmConfig?.maxTokens as number | undefined, 0, ); const agentMaxContextTokens = optionalChainWithEmptyCheck( maxContextTokens, getModelMaxTokens( tokensModel ?? '', providerEndpointMap[overrideProvider as keyof typeof providerEndpointMap], options.endpointTokenConfig, ), DEFAULT_MAX_CONTEXT_TOKENS, ); if ( agent.endpoint === EModelEndpoint.azureOpenAI && (llmConfig?.azureOpenAIApiInstanceName as string | undefined) == null ) { agent.provider = Providers.OPENAI; } if (options.provider != null) { agent.provider = options.provider; } /** * Unify code-execution tools around `bash_tool` + `read_file` when the * agent explicitly lists `execute_code` in its tools and the admin * capability is enabled for the run. The legacy `execute_code` tool * (backed by `CodeExecutionToolDefinition` + `primeCodeFiles`) is no * longer registered; the string `execute_code` on the agent document * stays as the capability-trigger marker but expands into the * `bash_tool` + `read_file` pair here. The initial `read_file` * definition is code-only; `injectSkillCatalog` upgrades it later in * this initializer when skill files are actually available. * * `effectiveCodeEnvAvailable` is the per-agent truth: the admin-level * `params.codeEnvAvailable` AND the agent actually asking for code * execution. Computed once and reused by the expansion block below, * the `injectSkillCatalog` call, and the returned `InitializedAgent`. * Downstream handlers (runtime `configurable`, `primeInvokedSkills`) * read it from the stored per-agent value so a skills-only agent * never accidentally registers `bash_tool` or primes sandbox files * just because the admin globally enabled code execution. * * Done BEFORE the `hasAgentTools` / GOOGLE_TOOL_CONFLICT gate so * execute-code-only agents on Google/Vertex still trip the conflict * guard when provider-specific tools are also configured. Also before * `injectSkillCatalog` so the skill path's own * `registerCodeExecutionTools` call upgrades `read_file` from the * code-only description to the skill-aware description without adding a * duplicate — exactly one copy of each tool reaches the LLM. */ const agentRequestsCodeExec = (agent.tools ?? []).includes(Tools.execute_code); const effectiveCodeEnvAvailable = params.codeEnvAvailable === true && agentRequestsCodeExec; if (effectiveCodeEnvAvailable) { const codeExecResult = registerCodeExecutionTools({ toolRegistry, toolDefinitions, includeBash: true, includeSkillFileInstructions: false, enableToolOutputReferences: effectiveCodeEnvAvailable, }); toolDefinitions = codeExecResult.toolDefinitions; } else if (agentRequestsCodeExec) { /** * Agent asked for `execute_code` but the admin-level gate is off — * surface a debug log so operators tracing "why isn't code * interpreter working?" get a clear signal. The event-driven tool * loader (`loadToolDefinitionsWrapper`) doesn't log capability- * disabled warnings for the definitions-only path, so without this, * the tool silently vanishes from the LLM's definitions with no trace. */ logger.debug( `[initializeAgent] Agent "${agent.id}" requests execute_code but codeEnvAvailable=${String(params.codeEnvAvailable)}; skipping bash_tool + read_file registration.`, ); } /** Check for tool presence from either full instances or definitions (event-driven mode) */ const hasAgentTools = (structuredTools?.length ?? 0) > 0 || (toolDefinitions?.length ?? 0) > 0; const providerTools = resolveAnthropicToolConflicts({ provider: agent.provider, tools: options.tools, toolDefinitions, }); const hasProviderTools = (providerTools?.length ?? 0) > 0; let tools: GenericTool[] = hasProviderTools ? (providerTools as GenericTool[]) : (structuredTools ?? []); if ( (agent.provider === Providers.GOOGLE || agent.provider === Providers.VERTEXAI) && hasProviderTools && hasAgentTools ) { throw new Error(`{ "type": "${ErrorTypes.GOOGLE_TOOL_CONFLICT}"}`); } else if ( (agent.provider === Providers.OPENAI || agent.provider === Providers.AZURE || agent.provider === Providers.ANTHROPIC) && hasProviderTools && structuredTools?.length ) { tools = structuredTools.concat(providerTools as GenericTool[]); } agent.model_parameters = { ...options.llmConfig } as Agent['model_parameters']; if (options.configOptions) { (agent.model_parameters as Record).configuration = options.configOptions; } if (agent.instructions && agent.instructions !== '') { const resolvedInstructions = replaceSpecialVars({ text: agent.instructions, user: req.user ? (req.user as unknown as TUser) : null, now: req.conversationCreatedAt, }); if (hasTemporalSpecialVars(agent.instructions)) { agent.instructions = undefined; appendAdditionalInstructions(agent, resolvedInstructions); } else { agent.instructions = resolvedInstructions; } } if (typeof agent.artifacts === 'string' && agent.artifacts !== '') { const artifactsPromptResult = generateArtifactsPrompt({ endpoint: agent.provider, artifacts: agent.artifacts as never, }); appendAdditionalInstructions(agent, artifactsPromptResult); } let skillCount = 0; /** * IDs authorized for runtime skill execution — starts as the ACL-scoped set * and gets replaced with the active-filtered subset after catalog injection. * Ensures `getSkillByName` cannot resolve a deactivated skill even if the * LLM (or a direct-invocation path) names one. */ let executableSkillIds = params.accessibleSkillIds; let activeSkillNames: Set | undefined; const { accessibleSkillIds } = params; if (accessibleSkillIds && accessibleSkillIds.length > 0) { const skillResult = await injectSkillCatalog({ agent, toolDefinitions, toolRegistry, accessibleSkillIds, contextWindowTokens: Number(agentMaxContextTokens) || 200_000, listSkillsByAccess: db?.listSkillsByAccess, codeEnvAvailable: effectiveCodeEnvAvailable, userId: req.user?.id, skillStates: params.skillStates, defaultActiveOnShare: params.defaultActiveOnShare, }); toolDefinitions = skillResult.toolDefinitions; skillCount = skillResult.skillCount; executableSkillIds = skillResult.activeSkillIds; activeSkillNames = skillResult.activeSkillNames; } const agentMaxContextNum = Number(agentMaxContextTokens) || DEFAULT_MAX_CONTEXT_TOKENS; const maxOutputTokensNum = Number(maxOutputTokens) || 0; const baseContextTokens = Math.max(0, agentMaxContextNum - maxOutputTokensNum); const toMongoFiles = (files: Array | undefined): IMongoFile[] => (files ?? []).filter((a): a is TFile => a != null).map((a) => a as unknown as IMongoFile); const finalAttachments: IMongoFile[] = toMongoFiles(primedAttachments); const requestAttachments: IMongoFile[] = toMongoFiles(primedRequestAttachments); const agentContextAttachments: IMongoFile[] = toMongoFiles(primedAgentContextAttachments); const compatibilityAttachments = finalAttachments.length > 0 ? finalAttachments : requestAttachments.concat(agentContextAttachments); const endpointConfigs = req.config?.endpoints; const providerConfig = customEndpointConfig ?? endpointConfigs?.[agent.provider as keyof typeof endpointConfigs]; const providerMaxToolResultChars = providerConfig != null && typeof providerConfig === 'object' && !Array.isArray(providerConfig) && 'maxToolResultChars' in providerConfig ? (providerConfig.maxToolResultChars as number | undefined) : undefined; const maxToolResultCharsResolved = providerMaxToolResultChars ?? endpointConfigs?.all?.maxToolResultChars; const initializedAgent: InitializedAgent = { ...agent, resendFiles, toolRegistry, tool_resources, userMCPAuthMap, toolDefinitions, hasDeferredTools, actionsEnabled, baseContextTokens, codeEnvAvailable: effectiveCodeEnvAvailable, skillCount, accessibleSkillIds: executableSkillIds, activeSkillNames, manualSkillPrimes, alwaysApplySkillPrimes, attachments: compatibilityAttachments, requestAttachments, agentContextAttachments, toolContextMap: toolContextMap ?? {}, dynamicToolContextMap: dynamicToolContextMap ?? {}, useLegacyContent: !!options.useLegacyContent, tools: (tools ?? []) as GenericTool[] & string[], maxToolResultChars: maxToolResultCharsResolved, maxContextTokens: maxContextTokens != null && maxContextTokens > 0 ? maxContextTokens : Math.max(1024, Math.round(baseContextTokens * (1 - DEFAULT_RESERVE_RATIO))), primedCodeFiles, }; return initializedAgent; }