🔧 feat: Lazy file provisioning — defer uploads to tool invocation time

Move file provisioning from eager (at chat-request start) to lazy
(at tool invocation time via ON_TOOL_EXECUTE). Files are now only
uploaded to code env / vector DB when the LLM actually calls the
respective tool.

- resources.ts: primeResources no longer provisions; computes
  provisionState (which files need code env / vector DB uploads)
  with staleness check and single credential load
- handlers.ts: add provisionFiles callback to ToolExecuteOptions,
  called once per tool-call batch before execution
- initialize.ts: pass provisionState through InitializedAgent
- initialize.js: implement provisionFiles closure that provisions
  files in parallel, batches DB updates, clears state after use;
  store provisionState in agentToolContexts for all agent types
This commit is contained in:
Danny Avila 2026-03-22 13:50:59 -04:00
parent 455d377600
commit 8684da106d
4 changed files with 144 additions and 122 deletions

View file

@ -1,5 +1,5 @@
const { logger } = require('@librechat/data-schemas');
const { createContentAggregator } = require('@librechat/agents');
const { Constants, createContentAggregator } = require('@librechat/agents');
const {
initializeAgent,
validateAgentModel,
@ -148,6 +148,70 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => {
return result;
},
toolEndCallback,
provisionFiles: async (toolNames, agentId) => {
const ctx = agentToolContexts.get(agentId);
if (!ctx?.provisionState) {
return;
}
const { provisionState, tool_resources } = ctx;
const needsCode =
toolNames.includes(Constants.EXECUTE_CODE) ||
toolNames.includes(Constants.PROGRAMMATIC_TOOL_CALLING);
const needsSearch = toolNames.includes('file_search');
if (!needsCode && !needsSearch) {
return;
}
/** @type {import('@librechat/api').TFileUpdate[]} */
const pendingUpdates = [];
if (needsCode && provisionState.codeEnvFiles.length > 0 && provisionState.codeApiKey) {
const results = await Promise.allSettled(
provisionState.codeEnvFiles.map(async (file) => {
const { fileIdentifier, fileUpdate } = await provisionToCodeEnv({
req,
file,
entity_id: agentId,
apiKey: provisionState.codeApiKey,
});
file.metadata = { ...file.metadata, fileIdentifier };
pendingUpdates.push(fileUpdate);
}),
);
for (const result of results) {
if (result.status === 'rejected') {
logger.error('[provisionFiles] Code env provisioning failed', result.reason);
}
}
provisionState.codeEnvFiles = [];
}
if (needsSearch && provisionState.vectorDBFiles.length > 0) {
const results = await Promise.allSettled(
provisionState.vectorDBFiles.map(async (file) => {
const result = await provisionToVectorDB({ req, file, entity_id: agentId });
if (result.embedded) {
file.embedded = true;
if (result.fileUpdate) {
pendingUpdates.push(result.fileUpdate);
}
}
}),
);
for (const result of results) {
if (result.status === 'rejected') {
logger.error('[provisionFiles] Vector DB provisioning failed', result.reason);
}
}
provisionState.vectorDBFiles = [];
}
if (pendingUpdates.length > 0) {
await Promise.allSettled(pendingUpdates.map((update) => db.updateFile(update)));
}
},
};
const summarizationOptions =
@ -239,6 +303,7 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => {
userMCPAuthMap: primaryConfig.userMCPAuthMap,
tool_resources: primaryConfig.tool_resources,
actionsEnabled: primaryConfig.actionsEnabled,
provisionState: primaryConfig.provisionState,
});
const agent_ids = primaryConfig.agent_ids;
@ -329,6 +394,7 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => {
userMCPAuthMap: config.userMCPAuthMap,
tool_resources: config.tool_resources,
actionsEnabled: config.actionsEnabled,
provisionState: config.provisionState,
});
agentConfigs.set(agentId, config);
@ -412,6 +478,7 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => {
userMCPAuthMap: config.userMCPAuthMap,
tool_resources: config.tool_resources,
actionsEnabled: config.actionsEnabled,
provisionState: config.provisionState,
});
}