Skip to content

Commit ebe2cb1

Browse files
SandyTao520gundermanc
authored andcommitted
feat(core): add experimental memory manager agent to replace save_memory tool (google-gemini#22726)
Co-authored-by: Christian Gunderman <gundermanc@gmail.com>
1 parent 5e536f4 commit ebe2cb1

27 files changed

Lines changed: 696 additions & 21 deletions

.gemini/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
"experimental": {
33
"plan": true,
44
"extensionReloading": true,
5-
"modelSteering": true
5+
"modelSteering": true,
6+
"memoryManager": true
67
},
78
"general": {
89
"devtools": true

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
@@ -1431,6 +1431,13 @@ their corresponding top-level category object in your `settings.json` file.
14311431
- **Default:** `"gemma3-1b-gpu-custom"`
14321432
- **Requires restart:** Yes
14331433

1434+
- **`experimental.memoryManager`** (boolean):
1435+
- **Description:** Replace the built-in save_memory tool with a memory manager
1436+
subagent that supports adding, removing, de-duplicating, and organizing
1437+
memories.
1438+
- **Default:** `false`
1439+
- **Requires restart:** Yes
1440+
14341441
- **`experimental.topicUpdateNarration`** (boolean):
14351442
- **Description:** Enable the experimental Topic & Update communication model
14361443
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
@@ -840,6 +840,7 @@ export async function loadCliConfig(
840840
skillsSupport: settings.skills?.enabled ?? true,
841841
disabledSkills: settings.skills?.disabled,
842842
experimentalJitContext: settings.experimental?.jitContext,
843+
experimentalMemoryManager: settings.experimental?.memoryManager,
843844
modelSteering: settings.experimental?.modelSteering,
844845
topicUpdateNarration: settings.experimental?.topicUpdateNarration,
845846
toolOutputMasking: settings.experimental?.toolOutputMasking,

packages/cli/src/config/policy-engine.integration.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -516,7 +516,9 @@ describe('Policy Engine Integration Tests', () => {
516516
);
517517
expect(mcpServerRule?.priority).toBe(4.1); // MCP allowed server
518518

519-
const readOnlyToolRule = rules.find((r) => r.toolName === 'glob');
519+
const readOnlyToolRule = rules.find(
520+
(r) => r.toolName === 'glob' && !r.subagent,
521+
);
520522
// Priority 70 in default tier → 1.07 (Overriding Plan Mode Deny)
521523
expect(readOnlyToolRule?.priority).toBeCloseTo(1.07, 5);
522524

@@ -673,7 +675,7 @@ describe('Policy Engine Integration Tests', () => {
673675
const server1Rule = rules.find((r) => r.toolName === 'mcp_server1_*');
674676
expect(server1Rule?.priority).toBe(4.1); // Allowed servers (user tier)
675677

676-
const globRule = rules.find((r) => r.toolName === 'glob');
678+
const globRule = rules.find((r) => r.toolName === 'glob' && !r.subagent);
677679
// Priority 70 in default tier → 1.07
678680
expect(globRule?.priority).toBeCloseTo(1.07, 5); // Auto-accept read-only
679681

packages/cli/src/config/settingsSchema.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2045,6 +2045,16 @@ const SETTINGS_SCHEMA = {
20452045
},
20462046
},
20472047
},
2048+
memoryManager: {
2049+
type: 'boolean',
2050+
label: 'Memory Manager Agent',
2051+
category: 'Experimental',
2052+
requiresRestart: true,
2053+
default: false,
2054+
description:
2055+
'Replace the built-in save_memory tool with a memory manager subagent that supports adding, removing, de-duplicating, and organizing memories.',
2056+
showInDialog: true,
2057+
},
20482058
topicUpdateNarration: {
20492059
type: 'boolean',
20502060
label: 'Topic & Update Narration',

packages/cli/src/ui/AppContainer.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1007,10 +1007,18 @@ Logging in with Google... Restarting Gemini CLI to continue.
10071007
Date.now(),
10081008
);
10091009
try {
1010-
const { memoryContent, fileCount } =
1011-
await refreshServerHierarchicalMemory(config);
1010+
let flattenedMemory: string;
1011+
let fileCount: number;
10121012

1013-
const flattenedMemory = flattenMemory(memoryContent);
1013+
if (config.isJitContextEnabled()) {
1014+
await config.getContextManager()?.refresh();
1015+
flattenedMemory = flattenMemory(config.getUserMemory());
1016+
fileCount = config.getGeminiMdFileCount();
1017+
} else {
1018+
const result = await refreshServerHierarchicalMemory(config);
1019+
flattenedMemory = flattenMemory(result.memoryContent);
1020+
fileCount = result.fileCount;
1021+
}
10141022

10151023
historyManager.addItem(
10161024
{
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
8+
import { MemoryManagerAgent } from './memory-manager-agent.js';
9+
import {
10+
ASK_USER_TOOL_NAME,
11+
EDIT_TOOL_NAME,
12+
GLOB_TOOL_NAME,
13+
GREP_TOOL_NAME,
14+
LS_TOOL_NAME,
15+
READ_FILE_TOOL_NAME,
16+
WRITE_FILE_TOOL_NAME,
17+
} from '../tools/tool-names.js';
18+
import { Storage } from '../config/storage.js';
19+
import type { Config } from '../config/config.js';
20+
import type { HierarchicalMemory } from '../config/memory.js';
21+
22+
function createMockConfig(memory: string | HierarchicalMemory = ''): Config {
23+
return {
24+
getUserMemory: vi.fn().mockReturnValue(memory),
25+
} as unknown as Config;
26+
}
27+
28+
describe('MemoryManagerAgent', () => {
29+
beforeEach(() => {
30+
vi.clearAllMocks();
31+
});
32+
33+
afterEach(() => {
34+
vi.restoreAllMocks();
35+
});
36+
37+
it('should have the correct name "save_memory"', () => {
38+
const agent = MemoryManagerAgent(createMockConfig());
39+
expect(agent.name).toBe('save_memory');
40+
});
41+
42+
it('should be a local agent', () => {
43+
const agent = MemoryManagerAgent(createMockConfig());
44+
expect(agent.kind).toBe('local');
45+
});
46+
47+
it('should have a description', () => {
48+
const agent = MemoryManagerAgent(createMockConfig());
49+
expect(agent.description).toBeTruthy();
50+
expect(agent.description).toContain('memory');
51+
});
52+
53+
it('should have a system prompt with memory management instructions', () => {
54+
const agent = MemoryManagerAgent(createMockConfig());
55+
const prompt = agent.promptConfig.systemPrompt;
56+
const globalGeminiDir = Storage.getGlobalGeminiDir();
57+
expect(prompt).toContain(`Global (${globalGeminiDir}`);
58+
expect(prompt).toContain('Project (./');
59+
expect(prompt).toContain('Memory Hierarchy');
60+
expect(prompt).toContain('De-duplicating');
61+
expect(prompt).toContain('Adding');
62+
expect(prompt).toContain('Removing stale entries');
63+
expect(prompt).toContain('Organizing');
64+
expect(prompt).toContain('Routing');
65+
});
66+
67+
it('should have efficiency guidelines in the system prompt', () => {
68+
const agent = MemoryManagerAgent(createMockConfig());
69+
const prompt = agent.promptConfig.systemPrompt;
70+
expect(prompt).toContain('Efficiency & Performance');
71+
expect(prompt).toContain('Use as few turns as possible');
72+
expect(prompt).toContain('Do not perform any exploration');
73+
expect(prompt).toContain('Be strategic with your thinking');
74+
expect(prompt).toContain('Context Awareness');
75+
});
76+
77+
it('should inject hierarchical memory into initial context', () => {
78+
const config = createMockConfig({
79+
global:
80+
'--- Context from: ../../.gemini/GEMINI.md ---\nglobal context\n--- End of Context from: ../../.gemini/GEMINI.md ---',
81+
project:
82+
'--- Context from: .gemini/GEMINI.md ---\nproject context\n--- End of Context from: .gemini/GEMINI.md ---',
83+
});
84+
85+
const agent = MemoryManagerAgent(config);
86+
const query = agent.promptConfig.query;
87+
88+
expect(query).toContain('# Initial Context');
89+
expect(query).toContain('global context');
90+
expect(query).toContain('project context');
91+
});
92+
93+
it('should inject flat string memory into initial context', () => {
94+
const config = createMockConfig('flat memory content');
95+
96+
const agent = MemoryManagerAgent(config);
97+
const query = agent.promptConfig.query;
98+
99+
expect(query).toContain('# Initial Context');
100+
expect(query).toContain('flat memory content');
101+
});
102+
103+
it('should exclude extension memory from initial context', () => {
104+
const config = createMockConfig({
105+
global: 'global context',
106+
extension: 'extension context that should be excluded',
107+
project: 'project context',
108+
});
109+
110+
const agent = MemoryManagerAgent(config);
111+
const query = agent.promptConfig.query;
112+
113+
expect(query).toContain('global context');
114+
expect(query).toContain('project context');
115+
expect(query).not.toContain('extension context');
116+
});
117+
118+
it('should not include initial context when memory is empty', () => {
119+
const agent = MemoryManagerAgent(createMockConfig());
120+
const query = agent.promptConfig.query;
121+
122+
expect(query).not.toContain('# Initial Context');
123+
});
124+
125+
it('should have file-management and search tools', () => {
126+
const agent = MemoryManagerAgent(createMockConfig());
127+
expect(agent.toolConfig).toBeDefined();
128+
expect(agent.toolConfig!.tools).toEqual(
129+
expect.arrayContaining([
130+
READ_FILE_TOOL_NAME,
131+
EDIT_TOOL_NAME,
132+
WRITE_FILE_TOOL_NAME,
133+
LS_TOOL_NAME,
134+
GLOB_TOOL_NAME,
135+
GREP_TOOL_NAME,
136+
ASK_USER_TOOL_NAME,
137+
]),
138+
);
139+
});
140+
141+
it('should require a "request" input parameter', () => {
142+
const agent = MemoryManagerAgent(createMockConfig());
143+
const schema = agent.inputConfig.inputSchema as Record<string, unknown>;
144+
expect(schema).toBeDefined();
145+
expect(schema['properties']).toHaveProperty('request');
146+
expect(schema['required']).toContain('request');
147+
});
148+
149+
it('should use a fast model', () => {
150+
const agent = MemoryManagerAgent(createMockConfig());
151+
expect(agent.modelConfig.model).toBe('flash');
152+
});
153+
});

0 commit comments

Comments
 (0)