Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gemini/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"experimental": {
"plan": true,
"extensionReloading": true,
"modelSteering": true
"modelSteering": true,
"memoryManager": true
},
"general": {
"devtools": true
Expand Down
1 change: 1 addition & 0 deletions docs/cli/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ they appear in the UI.
| Plan | `experimental.plan` | Enable Plan Mode. | `true` |
| Model Steering | `experimental.modelSteering` | Enable model steering (user hints) to guide the model during tool execution. | `false` |
| Direct Web Fetch | `experimental.directWebFetch` | Enable web fetch behavior that bypasses LLM summarization. | `false` |
| 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` |
| Topic & Update Narration | `experimental.topicUpdateNarration` | Enable the experimental Topic & Update communication model for reduced chattiness and structured progress reporting. | `false` |

### Skills
Expand Down
7 changes: 7 additions & 0 deletions docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -1431,6 +1431,13 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `"gemma3-1b-gpu-custom"`
- **Requires restart:** Yes

- **`experimental.memoryManager`** (boolean):
- **Description:** Replace the built-in save_memory tool with a memory manager
subagent that supports adding, removing, de-duplicating, and organizing
memories.
- **Default:** `false`
- **Requires restart:** Yes

- **`experimental.topicUpdateNarration`** (boolean):
- **Description:** Enable the experimental Topic & Update communication model
for reduced chattiness and structured progress reporting.
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -840,6 +840,7 @@ export async function loadCliConfig(
skillsSupport: settings.skills?.enabled ?? true,
disabledSkills: settings.skills?.disabled,
experimentalJitContext: settings.experimental?.jitContext,
experimentalMemoryManager: settings.experimental?.memoryManager,
modelSteering: settings.experimental?.modelSteering,
topicUpdateNarration: settings.experimental?.topicUpdateNarration,
toolOutputMasking: settings.experimental?.toolOutputMasking,
Expand Down
6 changes: 4 additions & 2 deletions packages/cli/src/config/policy-engine.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,9 @@ describe('Policy Engine Integration Tests', () => {
);
expect(mcpServerRule?.priority).toBe(4.1); // MCP allowed server

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

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

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

Expand Down
10 changes: 10 additions & 0 deletions packages/cli/src/config/settingsSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2045,6 +2045,16 @@ const SETTINGS_SCHEMA = {
},
},
},
memoryManager: {
type: 'boolean',
label: 'Memory Manager Agent',
category: 'Experimental',
requiresRestart: true,
default: false,
description:
'Replace the built-in save_memory tool with a memory manager subagent that supports adding, removing, de-duplicating, and organizing memories.',
showInDialog: true,
},
topicUpdateNarration: {
type: 'boolean',
label: 'Topic & Update Narration',
Expand Down
14 changes: 11 additions & 3 deletions packages/cli/src/ui/AppContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1008,10 +1008,18 @@ Logging in with Google... Restarting Gemini CLI to continue.
Date.now(),
);
try {
const { memoryContent, fileCount } =
await refreshServerHierarchicalMemory(config);
let flattenedMemory: string;
let fileCount: number;

const flattenedMemory = flattenMemory(memoryContent);
if (config.isJitContextEnabled()) {
await config.getContextManager()?.refresh();
flattenedMemory = flattenMemory(config.getUserMemory());
fileCount = config.getGeminiMdFileCount();
} else {
const result = await refreshServerHierarchicalMemory(config);
flattenedMemory = flattenMemory(result.memoryContent);
fileCount = result.fileCount;
}

historyManager.addItem(
{
Expand Down
153 changes: 153 additions & 0 deletions packages/core/src/agents/memory-manager-agent.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { MemoryManagerAgent } from './memory-manager-agent.js';
import {
ASK_USER_TOOL_NAME,
EDIT_TOOL_NAME,
GLOB_TOOL_NAME,
GREP_TOOL_NAME,
LS_TOOL_NAME,
READ_FILE_TOOL_NAME,
WRITE_FILE_TOOL_NAME,
} from '../tools/tool-names.js';
import { Storage } from '../config/storage.js';
import type { Config } from '../config/config.js';
import type { HierarchicalMemory } from '../config/memory.js';

function createMockConfig(memory: string | HierarchicalMemory = ''): Config {
return {
getUserMemory: vi.fn().mockReturnValue(memory),
} as unknown as Config;
}

describe('MemoryManagerAgent', () => {
beforeEach(() => {
vi.clearAllMocks();
});

afterEach(() => {
vi.restoreAllMocks();
});

it('should have the correct name "save_memory"', () => {
const agent = MemoryManagerAgent(createMockConfig());
expect(agent.name).toBe('save_memory');
});

it('should be a local agent', () => {
const agent = MemoryManagerAgent(createMockConfig());
expect(agent.kind).toBe('local');
});

it('should have a description', () => {
const agent = MemoryManagerAgent(createMockConfig());
expect(agent.description).toBeTruthy();
expect(agent.description).toContain('memory');
});

it('should have a system prompt with memory management instructions', () => {
const agent = MemoryManagerAgent(createMockConfig());
const prompt = agent.promptConfig.systemPrompt;
const globalGeminiDir = Storage.getGlobalGeminiDir();
expect(prompt).toContain(`Global (${globalGeminiDir}`);
expect(prompt).toContain('Project (./');
expect(prompt).toContain('Memory Hierarchy');
expect(prompt).toContain('De-duplicating');
expect(prompt).toContain('Adding');
expect(prompt).toContain('Removing stale entries');
expect(prompt).toContain('Organizing');
expect(prompt).toContain('Routing');
});

it('should have efficiency guidelines in the system prompt', () => {
const agent = MemoryManagerAgent(createMockConfig());
const prompt = agent.promptConfig.systemPrompt;
expect(prompt).toContain('Efficiency & Performance');
expect(prompt).toContain('Use as few turns as possible');
expect(prompt).toContain('Do not perform any exploration');
expect(prompt).toContain('Be strategic with your thinking');
expect(prompt).toContain('Context Awareness');
});

it('should inject hierarchical memory into initial context', () => {
const config = createMockConfig({
global:
'--- Context from: ../../.gemini/GEMINI.md ---\nglobal context\n--- End of Context from: ../../.gemini/GEMINI.md ---',
project:
'--- Context from: .gemini/GEMINI.md ---\nproject context\n--- End of Context from: .gemini/GEMINI.md ---',
});

const agent = MemoryManagerAgent(config);
const query = agent.promptConfig.query;

expect(query).toContain('# Initial Context');
expect(query).toContain('global context');
expect(query).toContain('project context');
});

it('should inject flat string memory into initial context', () => {
const config = createMockConfig('flat memory content');

const agent = MemoryManagerAgent(config);
const query = agent.promptConfig.query;

expect(query).toContain('# Initial Context');
expect(query).toContain('flat memory content');
});

it('should exclude extension memory from initial context', () => {
const config = createMockConfig({
global: 'global context',
extension: 'extension context that should be excluded',
project: 'project context',
});

const agent = MemoryManagerAgent(config);
const query = agent.promptConfig.query;

expect(query).toContain('global context');
expect(query).toContain('project context');
expect(query).not.toContain('extension context');
});

it('should not include initial context when memory is empty', () => {
const agent = MemoryManagerAgent(createMockConfig());
const query = agent.promptConfig.query;

expect(query).not.toContain('# Initial Context');
});

it('should have file-management and search tools', () => {
const agent = MemoryManagerAgent(createMockConfig());
expect(agent.toolConfig).toBeDefined();
expect(agent.toolConfig!.tools).toEqual(
expect.arrayContaining([
READ_FILE_TOOL_NAME,
EDIT_TOOL_NAME,
WRITE_FILE_TOOL_NAME,
LS_TOOL_NAME,
GLOB_TOOL_NAME,
GREP_TOOL_NAME,
ASK_USER_TOOL_NAME,
]),
);
});

it('should require a "request" input parameter', () => {
const agent = MemoryManagerAgent(createMockConfig());
const schema = agent.inputConfig.inputSchema as Record<string, unknown>;
expect(schema).toBeDefined();
expect(schema['properties']).toHaveProperty('request');
expect(schema['required']).toContain('request');
});

it('should use a fast model', () => {
const agent = MemoryManagerAgent(createMockConfig());
expect(agent.modelConfig.model).toBe('flash');
});
});
Loading
Loading