🗂️ 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:
Danny Avila 2026-07-09 10:48:51 -04:00 committed by GitHub
parent 73c43ded25
commit 3945d293de
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 732 additions and 137 deletions

View file

@ -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) {