mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
🗂️ feat: Per-Agent Memory Partitions (#14084)
* feat: per-agent memory partitions (memory_scope)
Adds an optional agentId partition to MemoryEntry so agents can opt into
isolated memory via a new memory_scope field ('user' | 'agent'). Partition
derives from agentId presence ({agentId: null} matches legacy docs, no
migration). Inline set_memory/delete_memory tools, the post-turn memory
agent, the request-scoped memory cache, and context injection are all
partition-aware; context is only injected into agents whose resolved
partition matches. Memory routes accept the partition param, scope
duplicate/token-limit checks per partition, and enrich entries with agent
names. Memories panel gains a partition filter and agent badges; the agent
builder gains an agent-scoped memory toggle.
* fix: address Codex review findings on memory partitions
- strip runtime ____N id suffixes in getMemoryAgentId so added-conversation
runs share the persisted agent's partition
- load each agent's own partition in multi-agent context injection instead
of skipping foreign partitions entirely
- clear memory_scope to 'user' on save when Enable Memory is unchecked
- fall back to 'all' when the selected panel partition no longer exists
- restrict GET /memories agent-name resolution to agents the requester can
VIEW
This commit is contained in:
parent
73c43ded25
commit
3945d293de
27 changed files with 732 additions and 137 deletions
|
|
@ -46,6 +46,7 @@ const {
|
|||
attachAskUserQuestionArgs,
|
||||
createContentIndexOffsetHandlers,
|
||||
getRequestMemories,
|
||||
getMemoryAgentId,
|
||||
createMemoryProcessor,
|
||||
agentHasInlineMemoryTools,
|
||||
loadAgent: loadAgentFn,
|
||||
|
|
@ -536,10 +537,34 @@ class AgentClient extends BaseClient {
|
|||
* keys + token metadata) is reserved for agents that can call
|
||||
* `delete_memory`; everyone else gets the unkeyed values only. */
|
||||
const memories = await this.useMemory();
|
||||
/** Partition the loaded memories belong to (the primary agent's). */
|
||||
const loadedMemoryAgentId = getMemoryAgentId(this.options.agent);
|
||||
const buildMemoryContext = (text) =>
|
||||
text ? `${memoryInstructions}\n\n# Existing memory about the user:\n${text}` : undefined;
|
||||
const memoryContext = buildMemoryContext(memories?.withoutKeys);
|
||||
const keyedMemoryContext = buildMemoryContext(memories?.withKeys);
|
||||
/** Resolves formatted memories for an agent's own partition. A defined
|
||||
* `memories` means the run-level gates (permission, opt-out, config)
|
||||
* passed; agents on other partitions fetch through the request-scoped
|
||||
* cache so repeated partitions share one query. */
|
||||
const getAgentPartitionMemories = async (agent) => {
|
||||
if (!memories) {
|
||||
return undefined;
|
||||
}
|
||||
const agentPartition = getMemoryAgentId(agent);
|
||||
if (agentPartition === loadedMemoryAgentId) {
|
||||
return memories;
|
||||
}
|
||||
try {
|
||||
return await getRequestMemories({
|
||||
req: this.options.req,
|
||||
userId: this.options.req.user.id + '',
|
||||
agentId: agentPartition,
|
||||
getFormattedMemories: db.getFormattedMemories,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[AgentClient] Error loading partition memories', error);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const sharedRunContext = sharedRunContextParts.join('\n\n');
|
||||
const memoryAgentEnabled = isMemoryAgentEnabled(this.options.req.config?.memory);
|
||||
|
|
@ -588,15 +613,17 @@ class AgentClient extends BaseClient {
|
|||
const configServers = await resolveConfigServers(this.options.req);
|
||||
|
||||
await Promise.all(
|
||||
allAgents.map(({ agent, agentId }) => {
|
||||
allAgents.map(async ({ agent, agentId }) => {
|
||||
const agentRunContextParts = [sharedRunContext];
|
||||
const agentHasMemory = agentHasInlineMemoryTools(agent);
|
||||
const agentMemoryContext = agentHasMemory ? keyedMemoryContext : memoryContext;
|
||||
if (
|
||||
agentMemoryContext &&
|
||||
(agentId === this.options.agent.id || memoryAgentEnabled || agentHasMemory)
|
||||
) {
|
||||
agentRunContextParts.push(agentMemoryContext);
|
||||
if (agentId === this.options.agent.id || memoryAgentEnabled || agentHasMemory) {
|
||||
const partitionMemories = await getAgentPartitionMemories(agent);
|
||||
const agentMemoryContext = buildMemoryContext(
|
||||
agentHasMemory ? partitionMemories?.withKeys : partitionMemories?.withoutKeys,
|
||||
);
|
||||
if (agentMemoryContext) {
|
||||
agentRunContextParts.push(agentMemoryContext);
|
||||
}
|
||||
}
|
||||
const scopedContext = agentScopedContext.get(agentId);
|
||||
if (scopedContext) {
|
||||
|
|
@ -674,6 +701,8 @@ class AgentClient extends BaseClient {
|
|||
}
|
||||
|
||||
const userId = this.options.req.user.id + '';
|
||||
/** Memory partition of the primary agent; undefined = shared personal pool */
|
||||
const memoryAgentId = getMemoryAgentId(this.options.agent);
|
||||
this.processMemory = undefined;
|
||||
|
||||
if (!isMemoryAgentEnabled(memoryConfig)) {
|
||||
|
|
@ -681,6 +710,7 @@ class AgentClient extends BaseClient {
|
|||
const { withKeys, withoutKeys } = await getRequestMemories({
|
||||
req: this.options.req,
|
||||
userId,
|
||||
agentId: memoryAgentId,
|
||||
getFormattedMemories: db.getFormattedMemories,
|
||||
});
|
||||
return { withKeys, withoutKeys };
|
||||
|
|
@ -786,6 +816,7 @@ class AgentClient extends BaseClient {
|
|||
const streamId = this.options.req?._resumableStreamId || null;
|
||||
const [withoutKeys, processMemory] = await createMemoryProcessor({
|
||||
userId,
|
||||
agentId: memoryAgentId,
|
||||
config,
|
||||
messageId,
|
||||
streamId,
|
||||
|
|
@ -805,6 +836,7 @@ class AgentClient extends BaseClient {
|
|||
({ withKeys } = await getRequestMemories({
|
||||
req: this.options.req,
|
||||
userId,
|
||||
agentId: memoryAgentId,
|
||||
getFormattedMemories: db.getFormattedMemories,
|
||||
}));
|
||||
} catch (error) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue