LibreChat/client/src/utils/memory.ts
Danny Avila 3945d293de
🗂️ 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
2026-07-09 10:48:51 -04:00

100 lines
2.8 KiB
TypeScript

import type { MemoriesResponse, TUserMemory, MemoryArtifact } from 'librechat-data-provider';
type HandleMemoryArtifactParams = {
memoryArtifact: MemoryArtifact;
currentData: MemoriesResponse;
};
/** Usage totals track the shared personal pool only; agent-partition
* writes never affect the personal usage badge. */
const isPersonal = (agentId?: string) => agentId == null;
const samePartition = (memory: TUserMemory, agentId?: string) =>
(memory.agentId ?? undefined) === (agentId ?? undefined);
/**
* Pure function to handle memory artifact updates
* @param params - Object containing memoryArtifact and currentData
* @returns Updated MemoriesResponse or undefined if no update needed
*/
export function handleMemoryArtifact({
memoryArtifact,
currentData,
}: HandleMemoryArtifactParams): MemoriesResponse | undefined {
const { type, key, value, tokenCount = 0, agentId } = memoryArtifact;
if (type === 'update' && !value) {
return undefined;
}
const memories = currentData.memories;
const existingIndex = memories.findIndex((m) => m.key === key && samePartition(m, agentId));
if (type === 'delete') {
if (existingIndex === -1) {
return undefined;
}
const deletedMemory = memories[existingIndex];
const newMemories = [...memories];
newMemories.splice(existingIndex, 1);
const totalTokens = isPersonal(agentId)
? currentData.totalTokens - (deletedMemory.tokenCount || 0)
: currentData.totalTokens;
const usagePercentage = currentData.tokenLimit
? Math.min(100, Math.round((totalTokens / currentData.tokenLimit) * 100))
: null;
return {
...currentData,
memories: newMemories,
totalTokens,
usagePercentage,
};
}
if (type === 'update') {
const timestamp = new Date().toISOString();
let totalTokens = currentData.totalTokens;
let newMemories: TUserMemory[];
const updatedMemory: TUserMemory = {
key,
value: value!,
tokenCount,
updated_at: timestamp,
...(agentId != null ? { agentId } : {}),
};
if (existingIndex >= 0) {
const oldTokenCount = memories[existingIndex].tokenCount || 0;
if (isPersonal(agentId)) {
totalTokens = totalTokens - oldTokenCount + tokenCount;
}
newMemories = [...memories];
newMemories[existingIndex] = {
...updatedMemory,
agentName: memories[existingIndex].agentName,
};
} else {
if (isPersonal(agentId)) {
totalTokens = totalTokens + tokenCount;
}
newMemories = [...memories, updatedMemory];
}
const usagePercentage = currentData.tokenLimit
? Math.min(100, Math.round((totalTokens / currentData.tokenLimit) * 100))
: null;
return {
...currentData,
memories: newMemories,
totalTokens,
usagePercentage,
};
}
return undefined;
}