Skip to content

Commit 6949763

Browse files
committed
feat(core): add experimental memory manager agent to replace save_memory tool
Add experimental.memoryManager flag that, when enabled, replaces the built-in save_memory tool with a memory manager subagent. The subagent supports adding, removing, de-duplicating, and organizing memories across both global (~/.gemini/GEMINI.md) and project-level GEMINI.md files. Users can override the agent by placing a custom save_memory.md in ~/.gemini/agents/ or .gemini/agents/.
1 parent cd2096c commit 6949763

16 files changed

Lines changed: 297 additions & 3 deletions

docs/cli/settings.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ they appear in the UI.
152152
| Plan | `experimental.plan` | Enable Plan Mode. | `true` |
153153
| Model Steering | `experimental.modelSteering` | Enable model steering (user hints) to guide the model during tool execution. | `false` |
154154
| Direct Web Fetch | `experimental.directWebFetch` | Enable web fetch behavior that bypasses LLM summarization. | `false` |
155+
| Memory Manager Agent | `experimental.memoryManager` | Replace the built-in save_memory tool with a memory manager subagent that supports adding, removing, de-duplicating, and organizing memories. | `false` |
155156
| Topic & Update Narration | `experimental.topicUpdateNarration` | Enable the experimental Topic & Update communication model for reduced chattiness and structured progress reporting. | `false` |
156157

157158
### Skills

docs/reference/configuration.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1249,6 +1249,13 @@ their corresponding top-level category object in your `settings.json` file.
12491249
- **Default:** `"gemma3-1b-gpu-custom"`
12501250
- **Requires restart:** Yes
12511251

1252+
- **`experimental.memoryManager`** (boolean):
1253+
- **Description:** Replace the built-in save_memory tool with a memory manager
1254+
subagent that supports adding, removing, de-duplicating, and organizing
1255+
memories.
1256+
- **Default:** `false`
1257+
- **Requires restart:** Yes
1258+
12521259
- **`experimental.topicUpdateNarration`** (boolean):
12531260
- **Description:** Enable the experimental Topic & Update communication model
12541261
for reduced chattiness and structured progress reporting.

packages/cli/src/config/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -813,6 +813,7 @@ export async function loadCliConfig(
813813
skillsSupport: settings.skills?.enabled ?? true,
814814
disabledSkills: settings.skills?.disabled,
815815
experimentalJitContext: settings.experimental?.jitContext,
816+
experimentalMemoryManager: settings.experimental?.memoryManager,
816817
modelSteering: settings.experimental?.modelSteering,
817818
topicUpdateNarration: settings.experimental?.topicUpdateNarration,
818819
toolOutputMasking: settings.experimental?.toolOutputMasking,

packages/cli/src/config/settingsSchema.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2018,6 +2018,16 @@ const SETTINGS_SCHEMA = {
20182018
},
20192019
},
20202020
},
2021+
memoryManager: {
2022+
type: 'boolean',
2023+
label: 'Memory Manager Agent',
2024+
category: 'Experimental',
2025+
requiresRestart: true,
2026+
default: false,
2027+
description:
2028+
'Replace the built-in save_memory tool with a memory manager subagent that supports adding, removing, de-duplicating, and organizing memories.',
2029+
showInDialog: true,
2030+
},
20212031
topicUpdateNarration: {
20222032
type: 'boolean',
20232033
label: 'Topic & Update Narration',
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { describe, it, expect } from 'vitest';
8+
import { MemoryManagerAgent } from './memory-manager-agent.js';
9+
10+
describe('MemoryManagerAgent', () => {
11+
it('should have the correct name "save_memory"', () => {
12+
const agent = MemoryManagerAgent();
13+
expect(agent.name).toBe('save_memory');
14+
});
15+
16+
it('should be a local agent', () => {
17+
const agent = MemoryManagerAgent();
18+
expect(agent.kind).toBe('local');
19+
});
20+
21+
it('should have a description', () => {
22+
const agent = MemoryManagerAgent();
23+
expect(agent.description).toBeTruthy();
24+
expect(agent.description).toContain('memory');
25+
});
26+
27+
it('should have a system prompt with memory management instructions', () => {
28+
const agent = MemoryManagerAgent();
29+
const prompt = agent.promptConfig.systemPrompt;
30+
expect(prompt).toContain('Global (~/.gemini/)');
31+
expect(prompt).toContain('Project (.gemini/)');
32+
expect(prompt).toContain('Table of Contents');
33+
expect(prompt).toContain('De-duplicating');
34+
expect(prompt).toContain('Adding');
35+
expect(prompt).toContain('Removing stale');
36+
expect(prompt).toContain('Organizing');
37+
expect(prompt).toContain('Routing');
38+
});
39+
40+
it('should have file-management and search tools', () => {
41+
const agent = MemoryManagerAgent();
42+
expect(agent.toolConfig).toBeDefined();
43+
expect(agent.toolConfig!.tools).toEqual(
44+
expect.arrayContaining([
45+
'read_file',
46+
'replace',
47+
'write_file',
48+
'grep_search',
49+
]),
50+
);
51+
});
52+
53+
it('should require a "request" input parameter', () => {
54+
const agent = MemoryManagerAgent();
55+
const schema = agent.inputConfig.inputSchema as Record<string, unknown>;
56+
expect(schema).toBeDefined();
57+
expect(schema['properties']).toHaveProperty('request');
58+
expect(schema['required']).toContain('request');
59+
});
60+
61+
it('should inherit the model from the parent agent', () => {
62+
const agent = MemoryManagerAgent();
63+
expect(agent.modelConfig.model).toBe('inherit');
64+
});
65+
});
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { z } from 'zod';
8+
import type { LocalAgentDefinition } from './types.js';
9+
10+
const MemoryManagerSchema = z.object({
11+
response: z
12+
.string()
13+
.describe('A summary of the memory operations performed.'),
14+
});
15+
16+
const MEMORY_MANAGER_SYSTEM_PROMPT = `
17+
You are a memory management agent. You maintain the user's memories stored in
18+
GEMINI.md files.
19+
20+
# Memory Hierarchy
21+
22+
## Global (~/.gemini/)
23+
- \`~/.gemini/GEMINI.md\` — Cross-project user preferences, key personal info,
24+
and habits that apply everywhere.
25+
26+
## Project (.gemini/)
27+
- \`.gemini/GEMINI.md\` — **Table of Contents** for project-specific context:
28+
architecture decisions, conventions, key contacts, and references to
29+
subdirectory GEMINI.md files for detailed context.
30+
- Subdirectory GEMINI.md files (e.g. \`src/GEMINI.md\`, \`docs/GEMINI.md\`) —
31+
detailed, domain-specific context for that part of the project. Reference
32+
these from the root \`.gemini/GEMINI.md\`.
33+
34+
## Routing
35+
36+
When adding a memory, route it to the right store:
37+
- User preferences, personal info, tool aliases, cross-project habits → **global**
38+
- Project architecture, conventions, workflows, team info → **project root**
39+
- Detailed context about a specific module or directory → **subdirectory
40+
GEMINI.md**, with a reference added to the project root
41+
42+
# Operations
43+
44+
Always read the target file(s) before writing. When editing any memory file,
45+
use \`grep_search\` to scan related files for duplicates before finishing.
46+
47+
1. **Adding** — Route to the correct store and file. Check for duplicates first.
48+
2. **Removing stale entries** — Delete outdated or unwanted entries. Clean up
49+
dangling references.
50+
3. **De-duplicating** — Search across related memory files for semantically
51+
equivalent entries. Keep the most informative version.
52+
4. **Organizing** — Restructure for clarity. Update references between files.
53+
54+
# Guidelines
55+
56+
- Keep GEMINI.md files lean — they are loaded into context every session.
57+
- Keep entries concise.
58+
- Edit surgically — preserve existing structure and user-authored content.
59+
- Always read before write to avoid overwriting concurrent changes.
60+
`.trim();
61+
62+
/**
63+
* A memory management agent that replaces the built-in save_memory tool.
64+
* It provides richer memory operations: adding, removing, de-duplicating,
65+
* and organizing memories in the global GEMINI.md file.
66+
*
67+
* Users can override this agent by placing a custom save_memory.md
68+
* in ~/.gemini/agents/ or .gemini/agents/.
69+
*/
70+
export const MemoryManagerAgent = (): LocalAgentDefinition<
71+
typeof MemoryManagerSchema
72+
> => ({
73+
kind: 'local',
74+
name: 'save_memory',
75+
displayName: 'Memory Manager',
76+
description:
77+
'Manages the global memory file (~/.gemini/GEMINI.md). Use this agent to add, remove, de-duplicate, and organize persistent user memories. It replaces the built-in save_memory tool with structured memory management including categorization and a table of contents.',
78+
inputConfig: {
79+
inputSchema: {
80+
type: 'object',
81+
properties: {
82+
request: {
83+
type: 'string',
84+
description:
85+
'The memory operation to perform. Examples: "Remember that I prefer tabs over spaces", "Clean up stale memories", "De-duplicate my memories", "Organize my memories".',
86+
},
87+
},
88+
required: ['request'],
89+
},
90+
},
91+
outputConfig: {
92+
outputName: 'result',
93+
description: 'A summary of the memory operations performed.',
94+
schema: MemoryManagerSchema,
95+
},
96+
modelConfig: {
97+
model: 'inherit',
98+
},
99+
toolConfig: {
100+
tools: [
101+
'read_file',
102+
'replace',
103+
'write_file',
104+
'list_directory',
105+
'glob',
106+
'grep_search',
107+
],
108+
},
109+
promptConfig: {
110+
systemPrompt: MEMORY_MANAGER_SYSTEM_PROMPT,
111+
query: '${request}',
112+
},
113+
runConfig: {
114+
maxTimeMinutes: 5,
115+
maxTurns: 10,
116+
},
117+
});

packages/core/src/agents/registry.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { CodebaseInvestigatorAgent } from './codebase-investigator.js';
1313
import { CliHelpAgent } from './cli-help-agent.js';
1414
import { GeneralistAgent } from './generalist-agent.js';
1515
import { BrowserAgentDefinition } from './browser/browserAgentDefinition.js';
16+
import { MemoryManagerAgent } from './memory-manager-agent.js';
1617
import { A2AClientManager } from './a2a-client-manager.js';
1718
import { A2AAuthProviderFactory } from './auth-provider/factory.js';
1819
import type { AuthenticationHandler } from '@a2a-js/sdk/client';
@@ -250,6 +251,11 @@ export class AgentRegistry {
250251
if (browserConfig.enabled) {
251252
this.registerLocalAgent(BrowserAgentDefinition(this.config));
252253
}
254+
255+
// Register the memory manager agent as a replacement for the save_memory tool.
256+
if (this.config.isMemoryManagerEnabled()) {
257+
this.registerLocalAgent(MemoryManagerAgent());
258+
}
253259
}
254260

255261
private async refreshAgents(): Promise<void> {

packages/core/src/config/config.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3086,6 +3086,35 @@ describe('Config JIT Initialization', () => {
30863086
expect(config.getUserMemory()).toBe('Initial Memory');
30873087
});
30883088

3089+
describe('isMemoryManagerEnabled', () => {
3090+
it('should default to false', () => {
3091+
const params: ConfigParameters = {
3092+
sessionId: 'test-session',
3093+
targetDir: '/tmp/test',
3094+
debugMode: false,
3095+
model: 'test-model',
3096+
cwd: '/tmp/test',
3097+
};
3098+
3099+
config = new Config(params);
3100+
expect(config.isMemoryManagerEnabled()).toBe(false);
3101+
});
3102+
3103+
it('should return true when experimentalMemoryManager is true', () => {
3104+
const params: ConfigParameters = {
3105+
sessionId: 'test-session',
3106+
targetDir: '/tmp/test',
3107+
debugMode: false,
3108+
model: 'test-model',
3109+
cwd: '/tmp/test',
3110+
experimentalMemoryManager: true,
3111+
};
3112+
3113+
config = new Config(params);
3114+
expect(config.isMemoryManagerEnabled()).toBe(true);
3115+
});
3116+
});
3117+
30893118
describe('reloadSkills', () => {
30903119
it('should refresh disabledSkills and re-register ActivateSkillTool when skills exist', async () => {
30913120
const mockOnReload = vi.fn().mockResolvedValue({

packages/core/src/config/config.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,7 @@ export interface ConfigParameters {
623623
disabledSkills?: string[];
624624
adminSkillsEnabled?: boolean;
625625
experimentalJitContext?: boolean;
626+
experimentalMemoryManager?: boolean;
626627
topicUpdateNarration?: boolean;
627628
toolOutputMasking?: Partial<ToolOutputMaskingConfig>;
628629
disableLLMCorrection?: boolean;
@@ -845,6 +846,7 @@ export class Config implements McpContext, AgentLoopContext {
845846
private readonly adminSkillsEnabled: boolean;
846847

847848
private readonly experimentalJitContext: boolean;
849+
private readonly experimentalMemoryManager: boolean;
848850
private readonly topicUpdateNarration: boolean;
849851
private readonly disableLLMCorrection: boolean;
850852
private readonly planEnabled: boolean;
@@ -994,6 +996,7 @@ export class Config implements McpContext, AgentLoopContext {
994996
);
995997

996998
this.experimentalJitContext = params.experimentalJitContext ?? false;
999+
this.experimentalMemoryManager = params.experimentalMemoryManager ?? false;
9971000
this.topicUpdateNarration = params.topicUpdateNarration ?? false;
9981001
this.modelSteering = params.modelSteering ?? false;
9991002
this.userHintService = new UserHintService(() =>
@@ -2065,6 +2068,10 @@ export class Config implements McpContext, AgentLoopContext {
20652068
return this.experimentalJitContext;
20662069
}
20672070

2071+
isMemoryManagerEnabled(): boolean {
2072+
return this.experimentalMemoryManager;
2073+
}
2074+
20682075
isTopicUpdateNarrationEnabled(): boolean {
20692076
return this.topicUpdateNarration;
20702077
}
@@ -3088,9 +3095,11 @@ export class Config implements McpContext, AgentLoopContext {
30883095
maybeRegister(ShellTool, () =>
30893096
registry.registerTool(new ShellTool(this, this.messageBus)),
30903097
);
3091-
maybeRegister(MemoryTool, () =>
3092-
registry.registerTool(new MemoryTool(this.messageBus)),
3093-
);
3098+
if (!this.isMemoryManagerEnabled()) {
3099+
maybeRegister(MemoryTool, () =>
3100+
registry.registerTool(new MemoryTool(this.messageBus)),
3101+
);
3102+
}
30943103
maybeRegister(WebSearchTool, () =>
30953104
registry.registerTool(new WebSearchTool(this, this.messageBus)),
30963105
);

packages/core/src/core/prompts.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ describe('Core System Prompt (prompts.ts)', () => {
9696
isInteractive: vi.fn().mockReturnValue(true),
9797
isInteractiveShellEnabled: vi.fn().mockReturnValue(true),
9898
isTopicUpdateNarrationEnabled: vi.fn().mockReturnValue(false),
99+
isMemoryManagerEnabled: vi.fn().mockReturnValue(false),
99100
isAgentsEnabled: vi.fn().mockReturnValue(false),
100101
getPreviewFeatures: vi.fn().mockReturnValue(true),
101102
getModel: vi.fn().mockReturnValue(DEFAULT_GEMINI_MODEL_AUTO),
@@ -410,6 +411,7 @@ describe('Core System Prompt (prompts.ts)', () => {
410411
isInteractive: vi.fn().mockReturnValue(false),
411412
isInteractiveShellEnabled: vi.fn().mockReturnValue(false),
412413
isTopicUpdateNarrationEnabled: vi.fn().mockReturnValue(false),
414+
isMemoryManagerEnabled: vi.fn().mockReturnValue(false),
413415
isAgentsEnabled: vi.fn().mockReturnValue(false),
414416
getModel: vi.fn().mockReturnValue('auto'),
415417
getActiveModel: vi.fn().mockReturnValue(PREVIEW_GEMINI_MODEL),

0 commit comments

Comments
 (0)