Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions api/server/controllers/agents/__tests__/callbacks.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,8 @@ jest.mock('@librechat/api', () => ({
}
: null,
),
isCodeSessionToolName: jest.fn((name) =>
['execute_code', 'bash_tool', 'read_file'].includes(name),
),
isCodeArtifactToolOutput: jest.requireActual('@librechat/api').isCodeArtifactToolOutput,
isCodeSessionToolName: jest.requireActual('@librechat/api').isCodeSessionToolName,
}));

jest.mock('@librechat/data-schemas', () => ({
Expand Down
5 changes: 1 addition & 4 deletions api/server/controllers/agents/callbacks.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const {
createBackgroundCodeResultHandler: createCodeHarvestHandler,
HOST_FILE_AUTHORING_ARTIFACT_KEY,
isCodeSessionToolName,
isCodeArtifactToolOutput,
getModelRefusalInfo,
shouldSignalSandboxStart,
getToolInputValidationDetails,
Expand All @@ -40,10 +41,6 @@ function isHostFileAuthoringArtifact(artifact) {
return artifact?.[HOST_FILE_AUTHORING_ARTIFACT_KEY] === true;
}

function isCodeArtifactToolOutput(output) {
return isCodeSessionToolName(output.name) || isHostFileAuthoringArtifact(output.artifact);
}

function getAttachmentOwnership(metadata) {
const agentId = metadata?.executingAgentId ?? metadata?.agentId ?? metadata?.agent_id;
const stepId = metadata?.stepId;
Expand Down
4 changes: 4 additions & 0 deletions api/server/controllers/agents/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -4902,6 +4902,7 @@ class AgentClient extends BaseClient {
this.buildDetachedSubagentUsageRecorder(balanceConfig, transactionsConfig),
),
subagentTasks: this.options.subagentTasks,
runFiles: this.options.runFiles,
}).then((createdRun) => {
if (!createdRun) {
throw new Error('Failed to create run');
Expand Down Expand Up @@ -5113,6 +5114,7 @@ class AgentClient extends BaseClient {
} finally {
/** An aborted/erroring run can still have completed compaction before
* the failure; retain that model-visible state for actor reconciliation. */
await this.options.runFiles?.close();
this.eventActorSummary =
getLatestEventActorSummary(this.contentParts) ?? this.eventActorSummary;
/** A run that never came to exist has no state of its own: keep the
Expand Down Expand Up @@ -5651,6 +5653,7 @@ class AgentClient extends BaseClient {
this.buildDetachedSubagentUsageRecorder(balanceConfig, transactionsConfig),
),
subagentTasks: this.options.subagentTasks,
runFiles: this.options.runFiles,
});

if (!run) {
Expand Down Expand Up @@ -5774,6 +5777,7 @@ class AgentClient extends BaseClient {
});
}
} finally {
await this.options.runFiles?.close();
this.eventActorSummary =
getLatestEventActorSummary(this.contentParts) ?? this.eventActorSummary;
/** A run that never came to exist has no state of its own: keep the
Expand Down
86 changes: 83 additions & 3 deletions api/server/services/Endpoints/agents/initialize.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ const {
backgroundCompletionWakeupsEnabled,
createLazyAgentHistoryResolver,
resolveToolRoleGrants,
createChatRunFileBindings,
createAxiosInstance,
getCodeApiAuthHeaders,
getCodeExecutionBaseUrl,
getAuthorizedRunFileSnapshot,
encodeAndFormatDocuments,
encodeAndFormatAudios,
encodeAndFormatVideos,
extractFileContext,
} = require('@librechat/api');
const {
ResourceType,
Expand All @@ -38,6 +47,7 @@ const {
AgentCapabilities,
normalizeServerName,
Tools,
VisionModes,
MAX_SUBAGENT_GRAPH_NODES,
MAX_SUBAGENT_RUN_CONFIGS,
isEphemeralAgentId,
Expand Down Expand Up @@ -86,6 +96,10 @@ const {
const { logViolation } = require('~/cache');
const db = require('~/models');
const { getAppConfig } = require('~/server/services/Config');
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
const { encodeAndFormat } = require('~/server/services/Files/images/encode');
const { processCodeOutput, runPreviewFinalize } = require('~/server/services/Files/Code/process');
const { determineFileType } = require('~/server/utils');

const SUBAGENT_GRAPH_LOAD_CONCURRENCY = 4;

Expand Down Expand Up @@ -346,6 +360,7 @@ const initializeClient = async ({
* }>}
*/
const agentToolContexts = new Map();
let runFileBindings;
const resolveMcpServerName = (toolName, agentId) => {
if (typeof toolName !== 'string' || typeof agentId !== 'string') {
return undefined;
Expand Down Expand Up @@ -400,7 +415,7 @@ const initializeClient = async ({
if (trustedContext?.codeExecutionContext) {
callbackMetadata.codeExecutionContext = trustedContext.codeExecutionContext;
}
return artifactToolEndCallback(data, callbackMetadata);
return runFileBindings.deliverToolEnd(artifactToolEndCallback, data, callbackMetadata);
};
/** @type {Map<string, import('@librechat/api').EndpointTokenConfig | undefined>} */
const endpointTokenConfigByAgentId = new Map();
Expand All @@ -413,8 +428,15 @@ const initializeClient = async ({
runSignal: signal,
foregroundRunId,
ordinaryToolCancellation: ordinaryToolCancellationEnabled,
loadTools: async (toolNames, agentId, _configurable, callerCapabilityProjection, runSignal) => {
const ctx = agentToolContexts.get(agentId) ?? {};
loadTools: async (
toolNames,
agentId,
_configurable,
callerCapabilityProjection,
runSignal,
executionContext,
) => {
const ctx = runFileBindings.getContext(agentId, executionContext) ?? {};
logger.debug(`[ON_TOOL_EXECUTE] ctx found: ${!!ctx.userMCPAuthMap}, agent: ${ctx.agent?.id}`);
logger.debug(`[ON_TOOL_EXECUTE] toolRegistry size: ${ctx.toolRegistry?.size ?? 'undefined'}`);

Expand All @@ -427,6 +449,10 @@ const initializeClient = async ({
requestBody: runtimeRequestBody,
toolNames,
agent: ctx.agent,
runFileCodeExecutionContext: runFileBindings.getCodeExecutionContext(
agentId,
executionContext,
),
toolRegistry: ctx.toolRegistry,
callerCapabilityProjection,
backgroundToolNames: ctx.backgroundToolNames,
Expand Down Expand Up @@ -1193,6 +1219,11 @@ const initializeClient = async ({
agent,
loadTools: createToolLoader(req, res, context.signal, streamId, true, jobCreatedAt),
requestFiles,
authorizedRunFiles: getAuthorizedRunFileSnapshot({
policy: appConfig.endpoints?.agents?.fileSharing,
agent: primaryConfig,
files: primaryConfig.currentRequestAttachments,
}),
conversationId,
parentMessageId,
requestBody: runtimeRequestBody,
Expand Down Expand Up @@ -1648,6 +1679,54 @@ const initializeClient = async ({
}
: null;

runFileBindings = createChatRunFileBindings({
req,
contexts: agentToolContexts,
createdAt: jobCreatedAt,
requestFiles,
audit: (event) => logger.info('[agents:run-files]', event),
getInputs: () => primaryConfig.currentRequestAttachments,
loadFiles: (fileIds) => db.getRunFileCandidates(fileIds, req.user.tenantId),
filterFiles: filterFilesByAgentAccess,
listPublications: db.listRunArtifacts,
provisioning: {
provisionToCodeEnv,
provisionToVectorDB,
updateFile: db.updateFile,
updateCodeEnvRef: db.updateFileCodeEnvRef,
addEmbeddedEntity: db.addFileEmbeddedEntity,
},
fileMethods: {
claimRunArtifactFile: db.claimRunArtifactFile,
publishRunArtifactFile: db.publishRunArtifactFile,
findRunArtifactFile: db.findRunArtifactFile,
},
processCodeOutput,
snapshotAdapter: {
request: createAxiosInstance(),
getAuthHeaders: getCodeApiAuthHeaders,
getBaseURL: getCodeExecutionBaseUrl,
determineFileType,
},
finalize: runPreviewFinalize,
getStrategyFunctions,
artifactPromises,
emitAttachment: createAttachmentEmitter({ res, streamId, jobCreatedAt }),
encoder: {
getAgent: (agentId) => agentToolContexts.get(agentId)?.fileEncodingAgent,
encodeImages: (request, files, params) =>
encodeAndFormat(request, files, params, VisionModes.agents),
encodeDocuments: encodeAndFormatDocuments,
encodeAudios: encodeAndFormatAudios,
encodeVideos: encodeAndFormatVideos,
extractText: extractFileContext,
},
});
toolExecuteOptions.runFiles = runFileBindings.session;
toolExecuteOptions.provisionFiles = runFileBindings.wrapProvision(
toolExecuteOptions.provisionFiles,
);

const eventHandlers = getDefaultHandlers({
res,
contentParts,
Expand Down Expand Up @@ -1697,6 +1776,7 @@ const initializeClient = async ({
endpoint: isEphemeralAgentId(primaryConfig.id) ? primaryConfig.endpoint : EModelEndpoint.agents,
subagentAggregatorsByToolCallId,
subagentTasks,
runFiles: runFileBindings.session,
/** Resolved endpoint token/pricing config so spending and cost reflect
* configured rates for custom-endpoint agents instead of defaults. */
endpointTokenConfig: primaryConfig.endpointTokenConfig,
Expand Down
7 changes: 7 additions & 0 deletions api/server/services/Endpoints/agents/skillDeps.js
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,13 @@ function buildSkillPrimedIdsByName(manualSkillPrimes, alwaysApplySkillPrimes) {
function buildAgentToolContext({ agent, config }) {
return {
agent,
fileEncodingAgent: {
provider: config.provider,
endpoint: config.endpoint,
model_parameters: config.model_parameters,
imageDetail: config.imageDetail,
agentContextAttachments: config.agentContextAttachments,
},
/** 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ jest.mock('@librechat/data-schemas', () => ({
}));

jest.mock('@librechat/agents', () => ({
...jest.requireActual('@librechat/agents'),
getCodeBaseURL: jest.fn(() => 'http://localhost:8000'),
}));

Expand All @@ -20,6 +21,7 @@ jest.mock('@librechat/api', () => {
const http = require('http');
const https = require('https');
return {
createCodeOutputPersistence: jest.requireActual('@librechat/api').createCodeOutputPersistence,
resolveDownloadPath: (file) => file.storageKey || file.filepath,
logAxiosError: jest.fn(),
getBasePath: jest.fn(() => ''),
Expand Down
Loading