mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
💭 feat: Require Explicit Auto-agent Enablement for Memories (#12886)
This commit is contained in:
parent
781bfb857d
commit
74307e6dcc
11 changed files with 282 additions and 64 deletions
|
|
@ -18,6 +18,7 @@ const {
|
|||
memoryInstructions,
|
||||
createTokenCounter,
|
||||
applyContextToAgent,
|
||||
isMemoryAgentEnabled,
|
||||
recordCollectedUsage,
|
||||
GenerationJobManager,
|
||||
getTransactionsConfig,
|
||||
|
|
@ -372,7 +373,8 @@ class AgentClient extends BaseClient {
|
|||
|
||||
/**
|
||||
* Build shared run context - applies to ALL agents in the run.
|
||||
* This includes: file context (latest message), augmented prompt (RAG), memory context.
|
||||
* This includes file context from the latest message and augmented prompt (RAG).
|
||||
* Memory context is handled separately and applied per-agent based on config.
|
||||
*/
|
||||
const sharedRunContextParts = [];
|
||||
|
||||
|
|
@ -392,12 +394,12 @@ class AgentClient extends BaseClient {
|
|||
|
||||
/** Memory context (user preferences/memories) */
|
||||
const withoutKeys = await this.useMemory();
|
||||
if (withoutKeys) {
|
||||
const memoryContext = `${memoryInstructions}\n\n# Existing memory about the user:\n${withoutKeys}`;
|
||||
sharedRunContextParts.push(memoryContext);
|
||||
}
|
||||
const memoryContext = withoutKeys
|
||||
? `${memoryInstructions}\n\n# Existing memory about the user:\n${withoutKeys}`
|
||||
: undefined;
|
||||
|
||||
const sharedRunContext = sharedRunContextParts.join('\n\n');
|
||||
const memoryAgentEnabled = isMemoryAgentEnabled(this.options.req.config?.memory);
|
||||
|
||||
/** Preserve canonical pre-format token counts for all history entering graph formatting */
|
||||
this.indexTokenCountMap = canonicalTokenCountMap;
|
||||
|
|
@ -423,7 +425,7 @@ class AgentClient extends BaseClient {
|
|||
|
||||
/**
|
||||
* Apply context to all agents.
|
||||
* Each agent gets: shared run context + their own base instructions + their own MCP instructions.
|
||||
* Each agent gets: run context + their own base instructions + their own MCP instructions.
|
||||
*
|
||||
* NOTE: This intentionally mutates agent objects in place. The agentConfigs Map
|
||||
* holds references to config objects that will be passed to the graph runtime.
|
||||
|
|
@ -434,17 +436,22 @@ class AgentClient extends BaseClient {
|
|||
const configServers = await resolveConfigServers(this.options.req);
|
||||
|
||||
await Promise.all(
|
||||
allAgents.map(({ agent, agentId }) =>
|
||||
applyContextToAgent({
|
||||
allAgents.map(({ agent, agentId }) => {
|
||||
const agentRunContext =
|
||||
memoryContext && (agentId === this.options.agent.id || memoryAgentEnabled)
|
||||
? [sharedRunContext, memoryContext].filter(Boolean).join('\n\n')
|
||||
: sharedRunContext;
|
||||
|
||||
return applyContextToAgent({
|
||||
agent,
|
||||
agentId,
|
||||
logger,
|
||||
mcpManager,
|
||||
configServers,
|
||||
sharedRunContext,
|
||||
sharedRunContext: agentRunContext,
|
||||
ephemeralAgent: agentId === this.options.agent.id ? ephemeralAgent : undefined,
|
||||
}),
|
||||
),
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
return result;
|
||||
|
|
@ -505,6 +512,22 @@ class AgentClient extends BaseClient {
|
|||
return;
|
||||
}
|
||||
|
||||
const userId = this.options.req.user.id + '';
|
||||
this.processMemory = undefined;
|
||||
|
||||
if (!isMemoryAgentEnabled(memoryConfig)) {
|
||||
try {
|
||||
const { withoutKeys } = await db.getFormattedMemories({ userId });
|
||||
return withoutKeys;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'[api/server/controllers/agents/client.js #useMemory] Error loading memories',
|
||||
error,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {Agent} */
|
||||
let prelimAgent;
|
||||
const allowedProviders = new Set(
|
||||
|
|
@ -593,7 +616,6 @@ class AgentClient extends BaseClient {
|
|||
tokenLimit: memoryConfig.tokenLimit,
|
||||
};
|
||||
|
||||
const userId = this.options.req.user.id + '';
|
||||
const messageId = this.responseMessageId + '';
|
||||
const conversationId = this.conversationId + '';
|
||||
const streamId = this.options.req?._resumableStreamId || null;
|
||||
|
|
@ -936,7 +958,9 @@ class AgentClient extends BaseClient {
|
|||
// messages = addCacheControl(messages);
|
||||
// }
|
||||
|
||||
memoryPromise = this.runMemory(messages);
|
||||
if (this.processMemory) {
|
||||
memoryPromise = this.runMemory(messages);
|
||||
}
|
||||
|
||||
/** Seed calibration state from previous run if encoding matches */
|
||||
const currentEncoding = this.getEncoding();
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ jest.mock('~/server/services/MCP', () => ({
|
|||
jest.mock('~/models', () => ({
|
||||
getAgent: jest.fn(),
|
||||
getRoleByName: jest.fn(),
|
||||
getFormattedMemories: jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock getMCPManager
|
||||
|
|
@ -1923,7 +1924,7 @@ describe('AgentClient - titleConvo', () => {
|
|||
client.maxContextTokens = 4096;
|
||||
});
|
||||
|
||||
it('should pass memory context to parallel agents (addedConvo)', async () => {
|
||||
it('should only pass memory context to the primary agent by default', async () => {
|
||||
const memoryContent = 'User prefers dark mode. User is a software developer.';
|
||||
client.useMemory = jest.fn().mockResolvedValue(memoryContent);
|
||||
|
||||
|
|
@ -1963,15 +1964,51 @@ describe('AgentClient - titleConvo', () => {
|
|||
|
||||
expect(client.useMemory).toHaveBeenCalled();
|
||||
|
||||
// Verify primary agent has its configured instructions (not from buildOptions) and memory context
|
||||
expect(client.options.agent.instructions).toContain('Primary agent instructions');
|
||||
expect(client.options.agent.instructions).toContain(memoryContent);
|
||||
|
||||
expect(parallelAgent1.instructions).toContain('Parallel agent 1 instructions');
|
||||
expect(parallelAgent1.instructions).toContain(memoryContent);
|
||||
expect(parallelAgent1.instructions).not.toContain(memoryContent);
|
||||
|
||||
expect(parallelAgent2.instructions).toContain('Parallel agent 2 instructions');
|
||||
expect(parallelAgent2.instructions).toContain(memoryContent);
|
||||
expect(parallelAgent2.instructions).not.toContain(memoryContent);
|
||||
});
|
||||
|
||||
it('should pass memory context to parallel agents when automatic memory updates are enabled', async () => {
|
||||
const memoryContent = 'User prefers dark mode. User is a software developer.';
|
||||
client.useMemory = jest.fn().mockResolvedValue(memoryContent);
|
||||
mockReq.config.memory.agent = {
|
||||
enabled: true,
|
||||
id: 'memory-agent',
|
||||
};
|
||||
|
||||
const parallelAgent = {
|
||||
id: 'parallel-agent-1',
|
||||
name: 'Parallel Agent 1',
|
||||
instructions: 'Parallel agent instructions',
|
||||
provider: EModelEndpoint.openAI,
|
||||
};
|
||||
|
||||
client.agentConfigs = new Map([['parallel-agent-1', parallelAgent]]);
|
||||
|
||||
const messages = [
|
||||
{
|
||||
messageId: 'msg-1',
|
||||
parentMessageId: null,
|
||||
sender: 'User',
|
||||
text: 'Hello',
|
||||
isCreatedByUser: true,
|
||||
},
|
||||
];
|
||||
|
||||
await client.buildMessages(messages, null, {
|
||||
instructions: 'Base instructions',
|
||||
additional_instructions: null,
|
||||
});
|
||||
|
||||
expect(client.options.agent.instructions).toContain(memoryContent);
|
||||
expect(parallelAgent.instructions).toContain('Parallel agent instructions');
|
||||
expect(parallelAgent.instructions).toContain(memoryContent);
|
||||
});
|
||||
|
||||
it('should not modify parallel agents when no memory context is available', async () => {
|
||||
|
|
@ -2004,7 +2041,7 @@ describe('AgentClient - titleConvo', () => {
|
|||
expect(parallelAgent.instructions).toBe('Original parallel instructions');
|
||||
});
|
||||
|
||||
it('should handle parallel agents without existing instructions', async () => {
|
||||
it('should handle parallel agents without existing instructions when memory stays primary-only', async () => {
|
||||
const memoryContent = 'User is a data scientist.';
|
||||
client.useMemory = jest.fn().mockResolvedValue(memoryContent);
|
||||
|
||||
|
|
@ -2033,7 +2070,8 @@ describe('AgentClient - titleConvo', () => {
|
|||
additional_instructions: null,
|
||||
});
|
||||
|
||||
expect(parallelAgentNoInstructions.instructions).toContain(memoryContent);
|
||||
expect(client.options.agent.instructions).toContain(memoryContent);
|
||||
expect(parallelAgentNoInstructions.instructions).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not modify agentConfigs when none exist', async () => {
|
||||
|
|
@ -2099,6 +2137,7 @@ describe('AgentClient - titleConvo', () => {
|
|||
let mockLoadAgent;
|
||||
let mockInitializeAgent;
|
||||
let mockCreateMemoryProcessor;
|
||||
let mockGetFormattedMemories;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
|
@ -2124,6 +2163,7 @@ describe('AgentClient - titleConvo', () => {
|
|||
config: {
|
||||
memory: {
|
||||
agent: {
|
||||
enabled: true,
|
||||
id: 'agent-123',
|
||||
},
|
||||
},
|
||||
|
|
@ -2147,6 +2187,12 @@ describe('AgentClient - titleConvo', () => {
|
|||
mockLoadAgent = require('@librechat/api').loadAgent;
|
||||
mockInitializeAgent = require('@librechat/api').initializeAgent;
|
||||
mockCreateMemoryProcessor = require('@librechat/api').createMemoryProcessor;
|
||||
mockGetFormattedMemories = require('~/models').getFormattedMemories;
|
||||
mockGetFormattedMemories.mockResolvedValue({
|
||||
withKeys: '',
|
||||
withoutKeys: '',
|
||||
totalTokens: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('should use current agent when memory config agent.id matches current agent id', async () => {
|
||||
|
|
@ -2211,12 +2257,89 @@ describe('AgentClient - titleConvo', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should return early when prelimAgent is undefined (no valid memory agent config)', async () => {
|
||||
it('should return existing memories without auto-processing when memory agent is not enabled', async () => {
|
||||
mockReq.config.memory = {
|
||||
agent: {},
|
||||
personalize: true,
|
||||
};
|
||||
|
||||
mockCheckAccess.mockResolvedValue(true);
|
||||
mockGetFormattedMemories.mockResolvedValue({
|
||||
withKeys: 'food: likes pasta',
|
||||
withoutKeys: 'likes pasta',
|
||||
totalTokens: 3,
|
||||
});
|
||||
|
||||
client = new AgentClient(mockOptions);
|
||||
client.conversationId = 'convo-123';
|
||||
client.responseMessageId = 'response-123';
|
||||
|
||||
const result = await client.useMemory();
|
||||
|
||||
expect(result).toBe('likes pasta');
|
||||
expect(mockGetFormattedMemories).toHaveBeenCalledWith({ userId: 'user-123' });
|
||||
expect(mockInitializeAgent).not.toHaveBeenCalled();
|
||||
expect(mockCreateMemoryProcessor).not.toHaveBeenCalled();
|
||||
expect(client.processMemory).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not initialize auto-processing when no memories exist', async () => {
|
||||
mockReq.config.memory = {
|
||||
personalize: true,
|
||||
};
|
||||
|
||||
mockCheckAccess.mockResolvedValue(true);
|
||||
mockGetFormattedMemories.mockResolvedValue({
|
||||
withKeys: '',
|
||||
withoutKeys: '',
|
||||
totalTokens: 0,
|
||||
});
|
||||
|
||||
client = new AgentClient(mockOptions);
|
||||
client.conversationId = 'convo-123';
|
||||
client.responseMessageId = 'response-123';
|
||||
|
||||
const result = await client.useMemory();
|
||||
|
||||
expect(result).toBe('');
|
||||
expect(mockGetFormattedMemories).toHaveBeenCalledWith({ userId: 'user-123' });
|
||||
expect(mockInitializeAgent).not.toHaveBeenCalled();
|
||||
expect(mockCreateMemoryProcessor).not.toHaveBeenCalled();
|
||||
expect(client.processMemory).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return existing memories without auto-processing when memory agent config lacks explicit enablement', async () => {
|
||||
mockReq.config.memory.agent = {
|
||||
id: 'agent-123',
|
||||
};
|
||||
|
||||
mockCheckAccess.mockResolvedValue(true);
|
||||
mockGetFormattedMemories.mockResolvedValue({
|
||||
withKeys: 'tone: concise',
|
||||
withoutKeys: 'prefers concise answers',
|
||||
totalTokens: 4,
|
||||
});
|
||||
|
||||
client = new AgentClient(mockOptions);
|
||||
client.conversationId = 'convo-123';
|
||||
client.responseMessageId = 'response-123';
|
||||
|
||||
const result = await client.useMemory();
|
||||
|
||||
expect(result).toBe('prefers concise answers');
|
||||
expect(mockLoadAgent).not.toHaveBeenCalled();
|
||||
expect(mockInitializeAgent).not.toHaveBeenCalled();
|
||||
expect(mockCreateMemoryProcessor).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return undefined when loading memories fails without auto-processing', async () => {
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const errorSpy = jest.spyOn(logger, 'error').mockImplementation(() => logger);
|
||||
mockReq.config.memory = {
|
||||
personalize: true,
|
||||
};
|
||||
|
||||
mockCheckAccess.mockResolvedValue(true);
|
||||
mockGetFormattedMemories.mockRejectedValue(new Error('DB connection failed'));
|
||||
|
||||
client = new AgentClient(mockOptions);
|
||||
client.conversationId = 'convo-123';
|
||||
|
|
@ -2225,13 +2348,20 @@ describe('AgentClient - titleConvo', () => {
|
|||
const result = await client.useMemory();
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(mockGetFormattedMemories).toHaveBeenCalledWith({ userId: 'user-123' });
|
||||
expect(mockInitializeAgent).not.toHaveBeenCalled();
|
||||
expect(mockCreateMemoryProcessor).not.toHaveBeenCalled();
|
||||
expect(client.processMemory).toBeUndefined();
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
'[api/server/controllers/agents/client.js #useMemory] Error loading memories',
|
||||
expect.any(Error),
|
||||
);
|
||||
});
|
||||
|
||||
it('should create ephemeral agent when no id but model and provider are specified', async () => {
|
||||
mockReq.config.memory = {
|
||||
agent: {
|
||||
enabled: true,
|
||||
model: 'gpt-4',
|
||||
provider: EModelEndpoint.openAI,
|
||||
},
|
||||
|
|
@ -2272,7 +2402,7 @@ describe('AgentClient - finalizeSubagentContent', () => {
|
|||
* ON_SUBAGENT_UPDATE handler) have their `contentParts` harvested
|
||||
* onto the matching parent `subagent` tool_call at message-save time
|
||||
* so a page refresh shows the same activity the user saw live. */
|
||||
const { createContentAggregator, GraphEvents } = jest.requireActual('@librechat/agents');
|
||||
const { GraphEvents } = jest.requireActual('@librechat/agents');
|
||||
const { getDefaultHandlers } = require('./callbacks');
|
||||
|
||||
const makeClient = (subagentAggregatorsByToolCallId) => {
|
||||
|
|
|
|||
|
|
@ -597,8 +597,11 @@ endpoints:
|
|||
# # (optional) Enable personalization features (defaults to true if memory is configured)
|
||||
# # When false, users will not see the Personalization tab in settings
|
||||
# personalize: true
|
||||
# # Memory agent configuration - either use an existing agent by ID or define inline
|
||||
# # (optional) Memory agent configuration for automatic memory updates from chat messages.
|
||||
# # If omitted, users can still create, edit, delete, and reference memories manually.
|
||||
# agent:
|
||||
# # Explicitly enables automatic memory updates from chat messages.
|
||||
# enabled: true
|
||||
# # Option 1: Use existing agent by ID
|
||||
# id: "your-memory-agent-id"
|
||||
# # Option 2: Define agent inline
|
||||
|
|
|
|||
|
|
@ -1777,7 +1777,7 @@ describe('updateInterfacePermissions - permissions', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should re-enable memory permissions when valid memory config exists without disabled field', async () => {
|
||||
it('should re-enable memory permissions when memory config exists without disabled field', async () => {
|
||||
// Mock existing memory permissions that are disabled
|
||||
mockGetRoleByName.mockResolvedValue({
|
||||
permissions: {
|
||||
|
|
@ -1793,11 +1793,6 @@ describe('updateInterfacePermissions - permissions', () => {
|
|||
|
||||
const config = {
|
||||
memory: {
|
||||
// No disabled field, but valid config
|
||||
agent: {
|
||||
id: 'test-agent-id',
|
||||
provider: 'openai',
|
||||
},
|
||||
personalize: false,
|
||||
} as unknown as TCustomConfig['memory'],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ export async function updateInterfacePermissions({
|
|||
const memoryEnabled = isMemoryEnabled(memoryConfig);
|
||||
/** Check if memory is explicitly disabled (memory.disabled === true) */
|
||||
const isMemoryExplicitlyDisabled = memoryConfig?.disabled === true;
|
||||
/** Check if memory should be enabled (explicitly enabled or valid config) */
|
||||
/** Check if memory should be enabled (explicitly enabled or configured) */
|
||||
const shouldEnableMemory =
|
||||
memoryConfig?.disabled === false ||
|
||||
(memoryConfig && memoryEnabled && memoryConfig.disabled === undefined);
|
||||
|
|
@ -152,7 +152,7 @@ export async function updateInterfacePermissions({
|
|||
logger.debug(`Role '${roleName}': Disabling memories as memory.disabled is true`);
|
||||
} else if (isMemoryReenabling) {
|
||||
logger.debug(
|
||||
`Role '${roleName}': Re-enabling memories due to valid memory configuration`,
|
||||
`Role '${roleName}': Re-enabling memories due to memory configuration`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
81
packages/api/src/memory/config.spec.ts
Normal file
81
packages/api/src/memory/config.spec.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import { logger } from '@librechat/data-schemas';
|
||||
|
||||
import type { TCustomConfig } from 'librechat-data-provider';
|
||||
|
||||
import { isMemoryAgentEnabled, isMemoryEnabled, loadMemoryConfig } from './config';
|
||||
|
||||
describe('memory config', () => {
|
||||
it('keeps memory enabled without configuring an automatic memory agent', () => {
|
||||
const config: TCustomConfig['memory'] = {
|
||||
personalize: true,
|
||||
};
|
||||
|
||||
const loaded = loadMemoryConfig(config);
|
||||
|
||||
expect(isMemoryEnabled(loaded)).toBe(true);
|
||||
expect(isMemoryAgentEnabled(loaded)).toBe(false);
|
||||
});
|
||||
|
||||
it('requires explicit memory agent enablement before enabling the automatic agent flow', () => {
|
||||
const warnSpy = jest.spyOn(logger, 'warn').mockImplementation(() => logger);
|
||||
const config: TCustomConfig['memory'] = {
|
||||
agent: {
|
||||
id: 'memory-agent',
|
||||
},
|
||||
};
|
||||
|
||||
const loaded = loadMemoryConfig(config);
|
||||
|
||||
expect(isMemoryEnabled(loaded)).toBe(true);
|
||||
expect(isMemoryAgentEnabled(loaded)).toBe(false);
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
'[memory] Agent config detected without explicit `enabled: true`. Automatic memory extraction is now opt-in. Add `memory.agent.enabled: true` to keep automatic memory updates.',
|
||||
);
|
||||
});
|
||||
|
||||
it('does not enable the automatic memory agent flow when explicitly disabled', () => {
|
||||
const warnSpy = jest.spyOn(logger, 'warn').mockImplementation(() => logger);
|
||||
const config: TCustomConfig['memory'] = {
|
||||
agent: {
|
||||
enabled: false,
|
||||
id: 'memory-agent',
|
||||
},
|
||||
};
|
||||
|
||||
const loaded = loadMemoryConfig(config);
|
||||
|
||||
expect(isMemoryEnabled(loaded)).toBe(true);
|
||||
expect(isMemoryAgentEnabled(loaded)).toBe(false);
|
||||
expect(warnSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('enables the automatic memory agent flow when explicitly configured', () => {
|
||||
const config: TCustomConfig['memory'] = {
|
||||
agent: {
|
||||
enabled: true,
|
||||
provider: 'openai',
|
||||
model: 'gpt-4o-mini',
|
||||
},
|
||||
};
|
||||
|
||||
const loaded = loadMemoryConfig(config);
|
||||
|
||||
expect(isMemoryEnabled(loaded)).toBe(true);
|
||||
expect(isMemoryAgentEnabled(loaded)).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps disabled memory disabled even when the agent is explicitly enabled', () => {
|
||||
const config: TCustomConfig['memory'] = {
|
||||
disabled: true,
|
||||
agent: {
|
||||
enabled: true,
|
||||
id: 'memory-agent',
|
||||
},
|
||||
};
|
||||
|
||||
const loaded = loadMemoryConfig(config);
|
||||
|
||||
expect(isMemoryEnabled(loaded)).toBe(false);
|
||||
expect(isMemoryAgentEnabled(loaded)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,28 +1 @@
|
|||
import { memorySchema } from 'librechat-data-provider';
|
||||
import type { TCustomConfig, TMemoryConfig } from 'librechat-data-provider';
|
||||
|
||||
const hasValidAgent = (agent: TMemoryConfig['agent']) =>
|
||||
!!agent &&
|
||||
(('id' in agent && !!agent.id) ||
|
||||
('provider' in agent && 'model' in agent && !!agent.provider && !!agent.model));
|
||||
|
||||
const isDisabled = (config?: TMemoryConfig | TCustomConfig['memory']) =>
|
||||
!config || config.disabled === true;
|
||||
|
||||
export function loadMemoryConfig(config: TCustomConfig['memory']): TMemoryConfig | undefined {
|
||||
if (!config) return undefined;
|
||||
if (isDisabled(config)) return config as TMemoryConfig;
|
||||
|
||||
if (!hasValidAgent(config.agent)) {
|
||||
return { ...config, disabled: true } as TMemoryConfig;
|
||||
}
|
||||
|
||||
const charLimit = memorySchema.shape.charLimit.safeParse(config.charLimit).data ?? 10000;
|
||||
|
||||
return { ...config, charLimit };
|
||||
}
|
||||
|
||||
export function isMemoryEnabled(config: TMemoryConfig | undefined): boolean {
|
||||
if (isDisabled(config)) return false;
|
||||
return hasValidAgent(config!.agent);
|
||||
}
|
||||
export { isMemoryAgentEnabled, isMemoryEnabled, loadMemoryConfig } from '@librechat/data-schemas';
|
||||
|
|
|
|||
|
|
@ -1031,9 +1031,11 @@ export const memorySchema = z.object({
|
|||
agent: z
|
||||
.union([
|
||||
z.object({
|
||||
enabled: z.boolean().optional(),
|
||||
id: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
enabled: z.boolean().optional(),
|
||||
provider: z.string(),
|
||||
model: z.string(),
|
||||
instructions: z.string().optional(),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
export * from './agents';
|
||||
export * from './interface';
|
||||
export * from './memory';
|
||||
export * from './service';
|
||||
export * from './specs';
|
||||
export * from './turnstile';
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export async function loadDefaultInterface({
|
|||
|
||||
const memoryConfig = config?.memory;
|
||||
const memoryEnabled = isMemoryEnabled(memoryConfig);
|
||||
/** Only disable memories if memory config is present but disabled/invalid */
|
||||
/** Only disable memories if memory config is present and explicitly disabled */
|
||||
const shouldDisableMemories = memoryConfig && !memoryEnabled;
|
||||
|
||||
const loadedInterface: AppConfig['interfaceConfig'] = removeNullishValues({
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import { memorySchema } from 'librechat-data-provider';
|
||||
|
||||
import type { TCustomConfig, TMemoryConfig } from 'librechat-data-provider';
|
||||
|
||||
import logger from '~/config/winston';
|
||||
|
||||
const hasValidAgent = (agent: TMemoryConfig['agent']) =>
|
||||
!!agent &&
|
||||
(('id' in agent && !!agent.id) ||
|
||||
|
|
@ -13,8 +16,10 @@ export function loadMemoryConfig(config: TCustomConfig['memory']): TMemoryConfig
|
|||
if (!config) return undefined;
|
||||
if (isDisabled(config)) return config as TMemoryConfig;
|
||||
|
||||
if (!hasValidAgent(config.agent)) {
|
||||
return { ...config, disabled: true } as TMemoryConfig;
|
||||
if (hasValidAgent(config.agent) && config.agent?.enabled == null) {
|
||||
logger.warn(
|
||||
'[memory] Agent config detected without explicit `enabled: true`. Automatic memory extraction is now opt-in. Add `memory.agent.enabled: true` to keep automatic memory updates.',
|
||||
);
|
||||
}
|
||||
|
||||
const charLimit = memorySchema.shape.charLimit.safeParse(config.charLimit).data ?? 10000;
|
||||
|
|
@ -23,6 +28,10 @@ export function loadMemoryConfig(config: TCustomConfig['memory']): TMemoryConfig
|
|||
}
|
||||
|
||||
export function isMemoryEnabled(config: TMemoryConfig | undefined): boolean {
|
||||
if (isDisabled(config)) return false;
|
||||
return hasValidAgent(config!.agent);
|
||||
return !isDisabled(config);
|
||||
}
|
||||
|
||||
export function isMemoryAgentEnabled(config: TMemoryConfig | undefined): boolean {
|
||||
if (!isMemoryEnabled(config)) return false;
|
||||
return config?.agent?.enabled === true && hasValidAgent(config.agent);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue