🗂️ fix: Scope Handoff Agent Context Docs (#13167)

* fix: Scope agent context docs to handoff agents

* fix: Deduplicate scoped request context

* refactor: Extract agent attachment helpers
This commit is contained in:
Danny Avila 2026-05-18 15:36:22 -04:00 committed by GitHub
parent 394839a76b
commit 68eac104ad
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 529 additions and 17 deletions

View file

@ -12,6 +12,7 @@ const {
resolveHeaders,
createSafeUser,
initializeAgent,
countTokens,
getBalanceConfig,
omitTitleOptions,
getProviderConfig,
@ -31,6 +32,8 @@ const {
hydrateMissingIndexTokenCounts,
injectSkillPrimes,
isSkillPrimeMessage,
collectFileIds,
buildAgentScopedContext,
buildSkillPrimeContentParts,
buildInitialToolSessions,
} = require('@librechat/api');
@ -270,10 +273,15 @@ class AgentClient extends BaseClient {
}))
: []),
];
const sharedRunAttachmentIds = new Set();
if (this.options.attachments) {
const attachments = await this.options.attachments;
const latestMessage = orderedMessages[orderedMessages.length - 1];
for (const fileId of collectFileIds(attachments)) {
sharedRunAttachmentIds.add(fileId);
}
if (this.message_file_map) {
this.message_file_map[latestMessage.messageId] = attachments;
} else {
@ -402,6 +410,14 @@ class AgentClient extends BaseClient {
const sharedRunContext = sharedRunContextParts.join('\n\n');
const memoryAgentEnabled = isMemoryAgentEnabled(this.options.req.config?.memory);
const agentScopedContext = await buildAgentScopedContext({
agentIds: allAgents.map(({ agentId }) => agentId),
attachmentsByAgentId: this.options.agentContextAttachmentsByAgentId,
sharedRunAttachmentIds,
req: this.options.req,
tokenCountFn: (text) => countTokens(text),
});
/** Preserve canonical pre-format token counts for all history entering graph formatting */
this.indexTokenCountMap = canonicalTokenCountMap;
@ -439,10 +455,14 @@ class AgentClient extends BaseClient {
await Promise.all(
allAgents.map(({ agent, agentId }) => {
const agentRunContext =
memoryContext && (agentId === this.options.agent.id || memoryAgentEnabled)
? [sharedRunContext, memoryContext].filter(Boolean).join('\n\n')
: sharedRunContext;
const agentRunContextParts = [sharedRunContext];
if (memoryContext && (agentId === this.options.agent.id || memoryAgentEnabled)) {
agentRunContextParts.push(memoryContext);
}
const scopedContext = agentScopedContext.get(agentId);
if (scopedContext) {
agentRunContextParts.push(scopedContext);
}
return applyContextToAgent({
agent,
@ -450,7 +470,7 @@ class AgentClient extends BaseClient {
logger,
mcpManager,
configServers,
sharedRunContext: agentRunContext,
sharedRunContext: agentRunContextParts.filter(Boolean).join('\n\n'),
ephemeralAgent: agentId === this.options.agent.id ? ephemeralAgent : undefined,
});
}),

View file

@ -13,6 +13,8 @@ jest.mock('@librechat/agents', () => ({
jest.mock('@librechat/api', () => ({
...jest.requireActual('@librechat/api'),
checkAccess: jest.fn(),
countFormattedMessageTokens: jest.fn(() => 42),
countTokens: jest.fn((text) => Math.ceil(String(text ?? '').length / 4)),
initializeAgent: jest.fn(),
createMemoryProcessor: jest.fn(),
isMemoryAgentEnabled: jest.fn((config) => {
@ -1429,6 +1431,183 @@ describe('AgentClient - titleConvo', () => {
});
});
describe('buildMessages with request and agent-scoped context attachments', () => {
let client;
let mockReq;
let mockRes;
let mockAgent;
const makeTextFile = (file_id, filename, text) => ({
user: 'user-123',
file_id,
filename,
filepath: `/uploads/${filename}`,
object: 'file',
type: 'text/plain',
bytes: text.length,
embedded: false,
usage: 0,
source: 'text',
text,
});
beforeEach(() => {
jest.clearAllMocks();
mockFormatInstructions.mockResolvedValue('');
mockAgent = {
id: 'primary-agent',
endpoint: EModelEndpoint.openAI,
provider: EModelEndpoint.openAI,
instructions: 'Primary instructions',
model_parameters: {
model: 'gpt-4',
},
tools: [],
};
mockReq = {
user: {
id: 'user-123',
personalization: {
memories: true,
},
},
body: {
endpoint: EModelEndpoint.openAI,
fileTokenLimit: 1000,
},
config: {
memory: {
disabled: true,
},
},
};
mockRes = {};
client = new AgentClient({
req: mockReq,
res: mockRes,
agent: mockAgent,
endpoint: EModelEndpoint.agents,
});
client.conversationId = 'convo-123';
client.responseMessageId = 'response-123';
client.shouldSummarize = false;
client.maxContextTokens = 4096;
client.useMemory = jest.fn().mockResolvedValue(undefined);
});
it("applies shared request context plus each agent's own context docs only", async () => {
const requestFile = makeTextFile('request-file', 'request.txt', 'Shared request context');
const primaryContext = makeTextFile(
'primary-context',
'primary.txt',
'Primary private context',
);
const handoffContext = makeTextFile(
'handoff-context',
'handoff.txt',
'Handoff private context',
);
const handoffAgent = {
id: 'handoff-agent',
endpoint: EModelEndpoint.openAI,
provider: EModelEndpoint.openAI,
instructions: 'Handoff instructions',
model_parameters: {
model: 'gpt-4',
},
tools: [],
};
client.options.attachments = [requestFile];
client.options.agentContextAttachmentsByAgentId = new Map([
['primary-agent', [primaryContext]],
['handoff-agent', [handoffContext]],
]);
client.agentConfigs = new Map([['handoff-agent', handoffAgent]]);
await client.buildMessages(
[
{
messageId: 'msg-1',
parentMessageId: null,
sender: 'User',
text: 'Use the available context.',
isCreatedByUser: true,
},
],
'msg-1',
{},
);
expect(mockAgent.additional_instructions).toContain('Shared request context');
expect(mockAgent.additional_instructions).toContain('Primary private context');
expect(mockAgent.additional_instructions).not.toContain('Handoff private context');
expect(handoffAgent.additional_instructions).toContain('Shared request context');
expect(handoffAgent.additional_instructions).toContain('Handoff private context');
expect(handoffAgent.additional_instructions).not.toContain('Primary private context');
});
it('does not duplicate a file that is both request context and scoped context', async () => {
const sharedFile = makeTextFile('shared-file', 'shared.txt', 'Shared duplicate context');
client.options.attachments = [sharedFile];
client.options.agentContextAttachmentsByAgentId = new Map([['primary-agent', [sharedFile]]]);
client.agentConfigs = new Map();
await client.buildMessages(
[
{
messageId: 'msg-1',
parentMessageId: null,
sender: 'User',
text: 'Use the available context.',
isCreatedByUser: true,
},
],
'msg-1',
{},
);
const occurrences = (
mockAgent.additional_instructions.match(/Shared duplicate context/g) ?? []
).length;
expect(occurrences).toBe(1);
});
it('keeps direct chats with context-doc agents working without request attachments', async () => {
const primaryContext = makeTextFile(
'primary-context',
'primary.txt',
'Direct primary context',
);
client.options.agentContextAttachmentsByAgentId = new Map([
['primary-agent', [primaryContext]],
]);
client.agentConfigs = new Map();
await client.buildMessages(
[
{
messageId: 'msg-1',
parentMessageId: null,
sender: 'User',
text: 'Answer from your context.',
isCreatedByUser: true,
},
],
'msg-1',
{},
);
expect(mockAgent.additional_instructions).toContain('Direct primary context');
});
});
describe('runMemory method', () => {
let client;
let mockReq;

View file

@ -10,6 +10,7 @@ const {
getCustomEndpointConfig,
discoverConnectedAgents,
resolveAgentScopedSkillIds,
buildAgentContextAttachmentsByAgentId,
} = require('@librechat/api');
const {
ResourceType,
@ -796,6 +797,11 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => {
}
}
const agentContextAttachmentsByAgentId = buildAgentContextAttachmentsByAgentId([
primaryConfig,
...agentConfigs.values(),
]);
let endpointConfig = appConfig.endpoints?.[primaryConfig.endpoint];
if (!isAgentsEndpoint(primaryConfig.endpoint) && !endpointConfig) {
try {
@ -851,7 +857,8 @@ const initializeClient = async ({ req, res, signal, endpointOption }) => {
agent: primaryConfig,
spec: endpointOption.spec,
iconURL: endpointOption.iconURL,
attachments: primaryConfig.attachments,
attachments: primaryConfig.requestAttachments ?? primaryConfig.attachments,
agentContextAttachmentsByAgentId,
endpointType: endpointOption.endpointType,
resendFiles: primaryConfig.resendFiles ?? true,
maxContextTokens: primaryConfig.maxContextTokens,

View file

@ -183,6 +183,15 @@ describe('initializeClient — processAgent ACL gate', () => {
});
const edges = [{ from: PRIMARY_ID, to: AUTHORIZED_ID, edgeType: 'handoff' }];
const requestAttachment = { file_id: 'request_file', filename: 'request.txt' };
const primaryContextAttachment = { file_id: 'primary_context', filename: 'primary.txt' };
const handoffContextAttachment = { file_id: 'handoff_context', filename: 'handoff.txt' };
const primaryConfig = {
...makePrimaryConfig(edges),
attachments: [primaryContextAttachment, requestAttachment],
requestAttachments: [requestAttachment],
agentContextAttachments: [primaryContextAttachment],
};
const handoffConfig = {
id: AUTHORIZED_ID,
edges: [],
@ -190,14 +199,13 @@ describe('initializeClient — processAgent ACL gate', () => {
toolRegistry: new Map(),
userMCPAuthMap: null,
tool_resources: {},
agentContextAttachments: [handoffContextAttachment],
};
let callCount = 0;
mockInitializeAgent.mockImplementation(() => {
callCount++;
return callCount === 1
? Promise.resolve(makePrimaryConfig(edges))
: Promise.resolve(handoffConfig);
return callCount === 1 ? Promise.resolve(primaryConfig) : Promise.resolve(handoffConfig);
});
await initializeClient({
@ -210,6 +218,13 @@ describe('initializeClient — processAgent ACL gate', () => {
expect(mockInitializeAgent).toHaveBeenCalledTimes(2);
expect(agentClientArgs.agent.edges).toHaveLength(1);
expect(agentClientArgs.agent.edges[0].to).toBe(AUTHORIZED_ID);
expect(agentClientArgs.attachments).toEqual([requestAttachment]);
expect(agentClientArgs.agentContextAttachmentsByAgentId.get(PRIMARY_ID)).toEqual([
primaryContextAttachment,
]);
expect(agentClientArgs.agentContextAttachmentsByAgentId.get(AUTHORIZED_ID)).toEqual([
handoffContextAttachment,
]);
});
});

View file

@ -502,6 +502,45 @@ describe('initializeAgent — stable and dynamic instruction fields', () => {
});
});
describe('initializeAgent — attachment scoping', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('keeps request attachments separate from agent context attachments', async () => {
const { primeResources } = jest.requireMock('../resources') as {
primeResources: jest.Mock;
};
const requestFile = { file_id: 'request-file', filename: 'request.txt' };
const agentContextFile = { file_id: 'agent-context-file', filename: 'agent-context.txt' };
primeResources.mockResolvedValueOnce({
attachments: [agentContextFile, requestFile],
requestAttachments: [requestFile],
agentContextAttachments: [agentContextFile],
tool_resources: undefined,
});
const { agent, req, res, loadTools, db } = createMocks();
const result = await initializeAgent(
{
req,
res,
agent,
loadTools,
endpointOption: { endpoint: EModelEndpoint.agents },
allowedProviders: new Set([Providers.OPENAI]),
isInitialAgent: true,
},
db,
);
expect(result.attachments).toEqual([agentContextFile, requestFile]);
expect(result.requestAttachments).toEqual([requestFile]);
expect(result.agentContextAttachments).toEqual([agentContextFile]);
});
});
describe('initializeAgent — maxContextTokens', () => {
beforeEach(() => {
jest.clearAllMocks();

View file

@ -0,0 +1,81 @@
import { FileSources } from 'librechat-data-provider';
import type { IMongoFile } from '@librechat/data-schemas';
import type { ServerRequest } from '~/types';
import {
collectFileIds,
buildAgentScopedContext,
getAgentContextAttachments,
buildAgentContextAttachmentsByAgentId,
} from './attachments';
const makeTextFile = (file_id: string, filename: string, text: string): IMongoFile =>
({
file_id,
filename,
text,
source: FileSources.text,
}) as IMongoFile;
describe('agent attachment helpers', () => {
it('collects file ids from attachment-like files', () => {
const fileIds = collectFileIds([
{ file_id: 'file-1' },
null,
{ file_id: '' },
{ file_id: 'file-2' },
{ file_id: 'file-1' },
]);
expect(Array.from(fileIds)).toEqual(['file-1', 'file-2']);
});
it('builds an agent context attachment map from initialized configs', () => {
const file = makeTextFile('context-file', 'context.txt', 'context');
const attachmentsByAgentId = buildAgentContextAttachmentsByAgentId([
{ id: 'agent-a', agentContextAttachments: [file] },
{ id: 'agent-b', agentContextAttachments: [] },
{ id: null, agentContextAttachments: [file] },
undefined,
]);
expect(attachmentsByAgentId.size).toBe(1);
expect(attachmentsByAgentId.get('agent-a')).toEqual([file]);
});
it('filters shared request files out of scoped context attachments', () => {
const shared = makeTextFile('shared-file', 'shared.txt', 'shared');
const scoped = makeTextFile('scoped-file', 'scoped.txt', 'scoped');
const attachments = getAgentContextAttachments({
agentId: 'agent-a',
attachmentsByAgentId: new Map([['agent-a', [shared, scoped]]]),
excludeFileIds: new Set(['shared-file']),
});
expect(attachments).toEqual([scoped]);
});
it('builds scoped context only from non-shared context documents', async () => {
const shared = makeTextFile('shared-file', 'shared.txt', 'Shared duplicate context');
const scoped = makeTextFile('scoped-file', 'scoped.txt', 'Scoped private context');
const req = {
body: { fileTokenLimit: 1000 },
config: {},
} as ServerRequest;
const scopedContext = await buildAgentScopedContext({
agentIds: ['agent-a', 'agent-b'],
attachmentsByAgentId: new Map([
['agent-a', [shared, scoped]],
['agent-b', [shared]],
]),
sharedRunAttachmentIds: new Set(['shared-file']),
req,
tokenCountFn: (text) => text.length,
});
expect(scopedContext.get('agent-a')).toContain('Scoped private context');
expect(scopedContext.get('agent-a')).not.toContain('Shared duplicate context');
expect(scopedContext.has('agent-b')).toBe(false);
});
});

View file

@ -0,0 +1,112 @@
import type { IMongoFile } from '@librechat/data-schemas';
import type { ServerRequest } from '~/types';
import type { TokenCountFn } from '~/utils/text';
import { countTokens } from '~/utils/tokenizer';
import { extractFileContext } from '~/files';
type FileWithId = {
file_id?: string | null;
};
export type AgentContextAttachmentCarrier<TFile extends FileWithId = IMongoFile> = {
id?: string | null;
agentContextAttachments?: TFile[] | null;
};
export type AgentContextAttachmentsByAgentId<TFile extends FileWithId = IMongoFile> =
| Map<string, TFile[]>
| Record<string, TFile[] | undefined>
| null
| undefined;
export function collectFileIds<TFile extends FileWithId>(
files?: Array<TFile | null | undefined> | null,
): Set<string> {
const fileIds = new Set<string>();
for (const file of files ?? []) {
if (file?.file_id) {
fileIds.add(file.file_id);
}
}
return fileIds;
}
export function buildAgentContextAttachmentsByAgentId<TFile extends FileWithId>(
configs: Iterable<AgentContextAttachmentCarrier<TFile> | null | undefined>,
): Map<string, TFile[]> {
const attachmentsByAgentId = new Map<string, TFile[]>();
for (const config of configs) {
if (!config?.id || !Array.isArray(config.agentContextAttachments)) {
continue;
}
if (config.agentContextAttachments.length === 0) {
continue;
}
attachmentsByAgentId.set(config.id, config.agentContextAttachments);
}
return attachmentsByAgentId;
}
export function getAgentContextAttachments<TFile extends FileWithId>({
agentId,
attachmentsByAgentId,
excludeFileIds,
}: {
agentId: string;
attachmentsByAgentId: AgentContextAttachmentsByAgentId<TFile>;
excludeFileIds?: Set<string>;
}): TFile[] {
if (!attachmentsByAgentId) {
return [];
}
const attachments: TFile[] =
attachmentsByAgentId instanceof Map
? (attachmentsByAgentId.get(agentId) ?? [])
: (attachmentsByAgentId[agentId] ?? []);
if (!excludeFileIds || excludeFileIds.size === 0) {
return attachments;
}
return attachments.filter((file) => !file?.file_id || !excludeFileIds.has(file.file_id));
}
export async function buildAgentScopedContext({
agentIds,
attachmentsByAgentId,
sharedRunAttachmentIds,
req,
tokenCountFn = countTokens,
}: {
agentIds: string[];
attachmentsByAgentId: AgentContextAttachmentsByAgentId<IMongoFile>;
sharedRunAttachmentIds?: Set<string>;
req?: ServerRequest;
tokenCountFn?: TokenCountFn;
}): Promise<Map<string, string>> {
const uniqueAgentIds = Array.from(new Set(agentIds.filter(Boolean)));
const entries = await Promise.all(
uniqueAgentIds.map(async (agentId) => {
const attachments = getAgentContextAttachments({
agentId,
attachmentsByAgentId,
excludeFileIds: sharedRunAttachmentIds,
});
if (attachments.length === 0) {
return [agentId, ''] as const;
}
const context = await extractFileContext({
attachments,
req,
tokenCountFn,
});
return [agentId, context ?? ''] as const;
}),
);
return new Map(entries.filter(([, context]) => Boolean(context)));
}

View file

@ -1,4 +1,5 @@
export * from './avatars';
export * from './attachments';
export * from './chain';
export * from './client';
export * from './config';

View file

@ -117,7 +117,12 @@ function resolveAnthropicToolConflicts({
*/
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<string, unknown>;
dynamicToolContextMap?: Record<string, unknown>;
maxContextTokens: number;
@ -535,7 +540,12 @@ export async function initializeAgent(
});
}
const { attachments: primedAttachments, tool_resources } = await primeResources({
const {
attachments: primedAttachments,
requestAttachments: primedRequestAttachments,
agentContextAttachments: primedAgentContextAttachments,
tool_resources,
} = await primeResources({
req: req as never,
getFiles: db.getFiles as never,
filterFiles: db.filterFilesByAgentAccess,
@ -959,9 +969,17 @@ export async function initializeAgent(
const maxOutputTokensNum = Number(maxOutputTokens) || 0;
const baseContextTokens = Math.max(0, agentMaxContextNum - maxOutputTokensNum);
const finalAttachments: IMongoFile[] = (primedAttachments ?? [])
.filter((a): a is TFile => a != null)
.map((a) => a as unknown as IMongoFile);
const toMongoFiles = (files: Array<TFile | undefined> | 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 =
@ -992,7 +1010,9 @@ export async function initializeAgent(
activeSkillNames,
manualSkillPrimes,
alwaysApplySkillPrimes,
attachments: finalAttachments,
attachments: compatibilityAttachments,
requestAttachments,
agentContextAttachments,
toolContextMap: toolContextMap ?? {},
dynamicToolContextMap: dynamicToolContextMap ?? {},
useLegacyContent: !!options.useLegacyContent,

View file

@ -84,6 +84,8 @@ describe('primeResources', () => {
agentId: 'agent_test',
});
expect(result.attachments).toEqual(mockOcrFiles);
expect(result.agentContextAttachments).toEqual(mockOcrFiles);
expect(result.requestAttachments).toBeUndefined();
expect(result.tool_resources).toEqual({});
});
});
@ -423,6 +425,8 @@ describe('primeResources', () => {
expect(result.attachments).toHaveLength(2);
expect(result.attachments?.[0]?.file_id).toBe('ocr-file-1');
expect(result.attachments?.[1]?.file_id).toBe('file1');
expect(result.agentContextAttachments).toEqual(mockOcrFiles);
expect(result.requestAttachments).toEqual(mockAttachmentFiles);
});
it('should include both context (as `ocr` resource) files and attachment files', async () => {
@ -475,6 +479,8 @@ describe('primeResources', () => {
expect(result.attachments).toHaveLength(2);
expect(result.attachments?.[0]?.file_id).toBe('ocr-file-1');
expect(result.attachments?.[1]?.file_id).toBe('file1');
expect(result.agentContextAttachments).toEqual(mockOcrFiles);
expect(result.requestAttachments).toEqual(mockAttachmentFiles);
});
it('should prevent duplicate files when same file exists in context tool_resource and attachments', async () => {
@ -528,6 +534,8 @@ describe('primeResources', () => {
expect(result.attachments).toHaveLength(2);
expect(result.attachments?.filter((f) => f?.file_id === 'shared-file-id')).toHaveLength(1);
expect(result.attachments?.find((f) => f?.file_id === 'unique-file')).toBeDefined();
expect(result.agentContextAttachments).toEqual(mockOcrFiles);
expect(result.requestAttachments).toEqual(mockAttachmentFiles);
});
it('should still categorize duplicate files for tool_resources', async () => {

View file

@ -174,8 +174,12 @@ export const primeResources = async ({
agentId?: string;
}): Promise<{
attachments: Array<TFile | undefined> | undefined;
requestAttachments: Array<TFile | undefined> | undefined;
agentContextAttachments: Array<TFile | undefined> | undefined;
tool_resources: AgentToolResources | undefined;
}> => {
const requestAttachments: Array<TFile> = [];
const agentContextAttachments: Array<TFile> = [];
try {
/**
* Array to collect all unique files that will be returned as attachments
@ -269,6 +273,7 @@ export const primeResources = async ({
// Add to attachments
attachments.push(file);
agentContextAttachments.push(file);
attachmentFileIds.add(file.file_id);
// Categorize for tool resources
@ -282,10 +287,17 @@ export const primeResources = async ({
}
if (!_attachments) {
return { attachments: attachments.length > 0 ? attachments : undefined, tool_resources };
return {
attachments: attachments.length > 0 ? attachments : undefined,
requestAttachments: undefined,
agentContextAttachments:
agentContextAttachments.length > 0 ? agentContextAttachments : undefined,
tool_resources,
};
}
const files = await _attachments;
const requestAttachmentFileIds = new Set<string>();
for (const file of files) {
if (!file) {
@ -300,16 +312,30 @@ export const primeResources = async ({
});
if (file.file_id && attachmentFileIds.has(file.file_id)) {
if (!requestAttachmentFileIds.has(file.file_id)) {
requestAttachments.push(file);
requestAttachmentFileIds.add(file.file_id);
}
continue;
}
attachments.push(file);
if (!file.file_id || !requestAttachmentFileIds.has(file.file_id)) {
requestAttachments.push(file);
}
if (file.file_id) {
attachmentFileIds.add(file.file_id);
requestAttachmentFileIds.add(file.file_id);
}
}
return { attachments: attachments.length > 0 ? attachments : [], tool_resources };
return {
attachments: attachments.length > 0 ? attachments : [],
requestAttachments,
agentContextAttachments:
agentContextAttachments.length > 0 ? agentContextAttachments : undefined,
tool_resources,
};
} catch (error) {
logger.error('Error priming resources', error);
@ -328,6 +354,9 @@ export const primeResources = async ({
return {
attachments: safeAttachments,
requestAttachments: safeAttachments,
agentContextAttachments:
agentContextAttachments.length > 0 ? agentContextAttachments : undefined,
tool_resources: _tool_resources,
};
}

View file

@ -3,6 +3,7 @@ import { FileSources, mergeFileConfig } from 'librechat-data-provider';
import type { IMongoFile } from '@librechat/data-schemas';
import type { ServerRequest } from '~/types';
import { processTextWithTokenLimit } from '~/utils/text';
import type { TokenCountFn } from '~/utils/text';
/**
* Extracts text context from attachments and returns formatted text.
@ -20,7 +21,7 @@ export async function extractFileContext({
}: {
attachments: IMongoFile[];
req?: ServerRequest;
tokenCountFn: (text: string) => number;
tokenCountFn: TokenCountFn;
}): Promise<string | undefined> {
if (!attachments || attachments.length === 0) {
return undefined;