mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-02 07:54:48 -07:00
refactor(memory): replace MemoryManagerAgent with prompt-driven memory editing across four tiers (#25716)
This commit is contained in:
@@ -73,7 +73,7 @@ describe('PromptProvider', () => {
|
||||
isInteractive: vi.fn().mockReturnValue(true),
|
||||
isInteractiveShellEnabled: vi.fn().mockReturnValue(true),
|
||||
isTopicUpdateNarrationEnabled: vi.fn().mockReturnValue(false),
|
||||
isMemoryManagerEnabled: vi.fn().mockReturnValue(false),
|
||||
isMemoryV2Enabled: vi.fn().mockReturnValue(false),
|
||||
getSkillManager: vi.fn().mockReturnValue({
|
||||
getSkills: vi.fn().mockReturnValue([]),
|
||||
}),
|
||||
|
||||
@@ -30,7 +30,11 @@ import {
|
||||
} from '../tools/tool-names.js';
|
||||
import { resolveModel, supportsModernFeatures } from '../config/models.js';
|
||||
import { DiscoveredMCPTool } from '../tools/mcp-tool.js';
|
||||
import { getAllGeminiMdFilenames } from '../tools/memoryTool.js';
|
||||
import {
|
||||
getAllGeminiMdFilenames,
|
||||
getGlobalMemoryFilePath,
|
||||
getProjectMemoryIndexFilePath,
|
||||
} from '../tools/memoryTool.js';
|
||||
import type { AgentLoopContext } from '../config/agent-loop-context.js';
|
||||
|
||||
/**
|
||||
@@ -223,7 +227,13 @@ export class PromptProvider {
|
||||
context.config.getEnableShellOutputEfficiency(),
|
||||
interactiveShellEnabled: context.config.isInteractiveShellEnabled(),
|
||||
topicUpdateNarration: isTopicUpdateNarrationEnabled,
|
||||
memoryManagerEnabled: context.config.isMemoryManagerEnabled(),
|
||||
memoryV2Enabled: context.config.isMemoryV2Enabled(),
|
||||
userProjectMemoryPath: context.config.isMemoryV2Enabled()
|
||||
? getProjectMemoryIndexFilePath(context.config.storage)
|
||||
: undefined,
|
||||
globalMemoryPath: context.config.isMemoryV2Enabled()
|
||||
? getGlobalMemoryFilePath()
|
||||
: undefined,
|
||||
}),
|
||||
),
|
||||
sandbox: this.withSection('sandbox', () => ({
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { renderOperationalGuidelines } from './snippets.js';
|
||||
|
||||
describe('renderOperationalGuidelines - memoryManagerEnabled', () => {
|
||||
const baseOptions = {
|
||||
interactive: true,
|
||||
interactiveShellEnabled: false,
|
||||
topicUpdateNarration: false,
|
||||
memoryManagerEnabled: false,
|
||||
};
|
||||
|
||||
it('should include standard memory tool guidance when memoryManagerEnabled is false', () => {
|
||||
const result = renderOperationalGuidelines(baseOptions);
|
||||
expect(result).toContain('save_memory');
|
||||
expect(result).toContain('persist facts across sessions');
|
||||
expect(result).not.toContain('subagent');
|
||||
});
|
||||
|
||||
it('should include subagent memory guidance when memoryManagerEnabled is true', () => {
|
||||
const result = renderOperationalGuidelines({
|
||||
...baseOptions,
|
||||
memoryManagerEnabled: true,
|
||||
});
|
||||
expect(result).toContain('save_memory');
|
||||
expect(result).toContain('subagent');
|
||||
expect(result).not.toContain('persistent user-related information');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { renderOperationalGuidelines } from './snippets.js';
|
||||
|
||||
describe('renderOperationalGuidelines - memoryV2Enabled', () => {
|
||||
const baseOptions = {
|
||||
interactive: true,
|
||||
interactiveShellEnabled: false,
|
||||
topicUpdateNarration: false,
|
||||
memoryV2Enabled: false,
|
||||
};
|
||||
|
||||
it('should include standard memory tool guidance when memoryV2Enabled is false', () => {
|
||||
const result = renderOperationalGuidelines(baseOptions);
|
||||
expect(result).toContain('save_memory');
|
||||
expect(result).toContain('persist facts across sessions');
|
||||
expect(result).not.toContain('Instruction and Memory Files');
|
||||
});
|
||||
|
||||
it('should distinguish shared GEMINI.md instructions from private MEMORY.md when memoryV2Enabled is true', () => {
|
||||
const result = renderOperationalGuidelines({
|
||||
...baseOptions,
|
||||
memoryV2Enabled: true,
|
||||
});
|
||||
expect(result).toContain('Instruction and Memory Files');
|
||||
expect(result).toContain('GEMINI.md');
|
||||
expect(result).toContain('./GEMINI.md');
|
||||
expect(result).toContain('MEMORY.md');
|
||||
expect(result).toContain('sibling `*.md` file');
|
||||
expect(result).toContain('There is no `save_memory` tool');
|
||||
expect(result).not.toContain('subagent');
|
||||
|
||||
// The Global Personal Memory tier is now opt-in via globalMemoryPath.
|
||||
// When it is NOT provided (this case), the bullet and the cross-project
|
||||
// routing rule must not be rendered.
|
||||
expect(result).not.toContain('**Global Personal Memory**');
|
||||
expect(result).not.toContain('across all my projects');
|
||||
|
||||
// Per-tier routing block must be present so the model has one trigger
|
||||
// per home rather than a single broad "remember -> private folder"
|
||||
// default that causes duplicate writes across tiers.
|
||||
expect(result).toContain('Routing rules — pick exactly one tier per fact');
|
||||
expect(result).toContain('team-shared convention');
|
||||
expect(result).toContain('personal-to-them local setup');
|
||||
|
||||
// Explicit mutual-exclusion rule: each fact lives in exactly one tier.
|
||||
expect(result).toContain('Never duplicate or mirror the same fact');
|
||||
|
||||
// MEMORY.md must be scoped to its sibling notes only and must never
|
||||
// point at GEMINI.md topics.
|
||||
expect(result).toContain('index for its sibling `*.md` notes');
|
||||
expect(result).toContain('never use it to point at');
|
||||
});
|
||||
|
||||
it('should NOT include the Private Project Memory bullet when userProjectMemoryPath is undefined', () => {
|
||||
const result = renderOperationalGuidelines({
|
||||
...baseOptions,
|
||||
memoryV2Enabled: true,
|
||||
});
|
||||
expect(result).not.toContain('**Private Project Memory**');
|
||||
});
|
||||
|
||||
it('should include the Private Project Memory bullet with the absolute path when provided', () => {
|
||||
const userProjectMemoryPath =
|
||||
'/Users/test/.gemini/tmp/abc123/memory/MEMORY.md';
|
||||
const result = renderOperationalGuidelines({
|
||||
...baseOptions,
|
||||
memoryV2Enabled: true,
|
||||
userProjectMemoryPath,
|
||||
});
|
||||
expect(result).toContain('**Private Project Memory**');
|
||||
expect(result).toContain(userProjectMemoryPath);
|
||||
expect(result).toContain('NOT** be committed to the repo');
|
||||
});
|
||||
|
||||
it('should NOT include the Global Personal Memory bullet or cross-project routing rule when globalMemoryPath is undefined', () => {
|
||||
const result = renderOperationalGuidelines({
|
||||
...baseOptions,
|
||||
memoryV2Enabled: true,
|
||||
});
|
||||
expect(result).not.toContain('**Global Personal Memory**');
|
||||
expect(result).not.toContain('across all my projects');
|
||||
expect(result).not.toContain('cross-project personal preference');
|
||||
});
|
||||
|
||||
it('should include the Global Personal Memory bullet, cross-project routing rule, and four-tier mutual-exclusion when globalMemoryPath is provided', () => {
|
||||
const globalMemoryPath = '/Users/test/.gemini/GEMINI.md';
|
||||
const result = renderOperationalGuidelines({
|
||||
...baseOptions,
|
||||
memoryV2Enabled: true,
|
||||
globalMemoryPath,
|
||||
});
|
||||
expect(result).toContain('**Global Personal Memory**');
|
||||
expect(result).toContain(globalMemoryPath);
|
||||
expect(result).toContain('cross-project personal preference');
|
||||
expect(result).toContain('across all my projects');
|
||||
// Mutual-exclusion rule must explicitly cover all four tiers when the
|
||||
// global tier is surfaced.
|
||||
expect(result).toContain('across all four tiers');
|
||||
});
|
||||
});
|
||||
@@ -74,7 +74,12 @@ export interface OperationalGuidelinesOptions {
|
||||
enableShellEfficiency: boolean;
|
||||
interactiveShellEnabled: boolean;
|
||||
topicUpdateNarration?: boolean;
|
||||
memoryManagerEnabled: boolean;
|
||||
memoryV2Enabled: boolean;
|
||||
/**
|
||||
* Absolute path to the user's per-project private memory index. See
|
||||
* snippets.ts for full semantics.
|
||||
*/
|
||||
userProjectMemoryPath?: string;
|
||||
}
|
||||
|
||||
export type SandboxMode = 'macos-seatbelt' | 'generic' | 'outside';
|
||||
@@ -409,7 +414,7 @@ ${trimmed}
|
||||
}
|
||||
if (memory.userProjectMemory?.trim()) {
|
||||
sections.push(
|
||||
`<user_project_memory>\n--- User's Project Memory (private, not committed to repo) ---\n${memory.userProjectMemory.trim()}\n--- End User's Project Memory ---\n</user_project_memory>`,
|
||||
`<user_project_memory>\n--- Private Project Memory Index (private, not committed to repo) ---\n${memory.userProjectMemory.trim()}\n--- End Private Project Memory Index ---\n</user_project_memory>`,
|
||||
);
|
||||
}
|
||||
if (memory.extension?.trim()) {
|
||||
@@ -697,9 +702,16 @@ function toolUsageInteractive(
|
||||
function toolUsageRememberingFacts(
|
||||
options: OperationalGuidelinesOptions,
|
||||
): string {
|
||||
if (options.memoryManagerEnabled) {
|
||||
if (options.memoryV2Enabled) {
|
||||
const userProjectBullet = options.userProjectMemoryPath
|
||||
? `
|
||||
- **Private Project Memory** (\`${options.userProjectMemoryPath}\`): Personal-to-the-user, project-specific notes that must **NOT** be committed to the repo. Keep this file concise: it is the private index for this workspace. Store richer detail in sibling \`*.md\` files in the same folder and use \`MEMORY.md\` to point to them.`
|
||||
: '';
|
||||
return `
|
||||
- **Memory Tool:** You MUST use the '${AGENT_TOOL_NAME}' tool with the 'save_memory' agent to proactively record facts, preferences, and workflows that apply across all sessions. Whenever the user explicitly tells you to "remember" something, or when they state a preference or workflow (like "always lint after editing"), you MUST immediately call the save_memory subagent. Never save transient session state. Do not use memory to store summaries of code changes, bug fixes, or findings discovered during a task; this tool is strictly for persistent general knowledge.`;
|
||||
- **Instruction and Memory Files:** You persist long-lived project context by editing markdown files directly with '${EDIT_TOOL_NAME}' or '${WRITE_FILE_TOOL_NAME}'. There is no \`save_memory\` tool. The current contents of all loaded \`GEMINI.md\` files and the private project \`MEMORY.md\` index are already in your context — do not re-read them before editing.
|
||||
- **Project Instructions** (\`./GEMINI.md\`): Team-shared architecture, conventions, workflows, and other repo guidance. **Committed to the repo and shared with the team.**
|
||||
- **Subdirectory Instructions** (e.g. \`./src/GEMINI.md\`): Scoped instructions for one part of the project. Reference them from \`./GEMINI.md\` so they remain discoverable.${userProjectBullet}
|
||||
Whenever the user tells you to "remember" something or states a durable personal workflow for this codebase, save it in the private project memory folder immediately. Put concise index entries in \`MEMORY.md\`; if more detail is useful, create or update a sibling \`*.md\` note in the same folder and keep \`MEMORY.md\` as the pointer. Only update \`GEMINI.md\` files when the memory is a shared project instruction or convention that belongs in the repo. If it could be either tier, ask the user. Never save transient session state, summaries of code changes, bug fixes, or task-specific findings — these files are loaded into every session and must stay lean.`;
|
||||
}
|
||||
const base = `
|
||||
- **Remembering Facts:** Use the '${MEMORY_TOOL_NAME}' tool to remember specific, *user-related* facts or preferences when the user explicitly asks, or when they state a clear, concise piece of information that would help personalize or streamline *your future interactions with them* (e.g., preferred coding style, common project paths they use, personal tool aliases, or a workflow like "always lint after editing"). This tool is for user-specific information that should persist across sessions. Do *not* use it for general project context or information.`;
|
||||
|
||||
@@ -83,7 +83,25 @@ export interface OperationalGuidelinesOptions {
|
||||
interactive: boolean;
|
||||
interactiveShellEnabled: boolean;
|
||||
topicUpdateNarration: boolean;
|
||||
memoryManagerEnabled: boolean;
|
||||
memoryV2Enabled: boolean;
|
||||
/**
|
||||
* Absolute path to the user's per-project private memory index
|
||||
* (e.g. ~/.gemini/tmp/<project-hash>/memory/MEMORY.md). Surfaced to the
|
||||
* model when memoryV2Enabled is true so the prompt-driven memory flow
|
||||
* can route project-specific personal notes there instead of the committed
|
||||
* project GEMINI.md.
|
||||
*/
|
||||
userProjectMemoryPath?: string;
|
||||
/**
|
||||
* Absolute path to the user's global personal memory file
|
||||
* (e.g. ~/.gemini/GEMINI.md). Surfaced to the model when memoryV2Enabled
|
||||
* is true so the prompt-driven memory flow can route cross-project personal
|
||||
* preferences (preferences that follow the user across all workspaces) there
|
||||
* instead of the project-scoped tiers. Config.isPathAllowed surgically
|
||||
* allowlists this exact file (only this file, not the rest of `~/.gemini/`)
|
||||
* so the agent can edit it directly.
|
||||
*/
|
||||
globalMemoryPath?: string;
|
||||
}
|
||||
|
||||
export type SandboxMode = 'macos-seatbelt' | 'generic' | 'outside';
|
||||
@@ -525,7 +543,7 @@ ${trimmed}
|
||||
}
|
||||
if (memory.userProjectMemory?.trim()) {
|
||||
sections.push(
|
||||
`<user_project_memory>\n--- User's Project Memory (private, not committed to repo) ---\n${memory.userProjectMemory.trim()}\n--- End User's Project Memory ---\n</user_project_memory>`,
|
||||
`<user_project_memory>\n--- Private Project Memory Index (private, not committed to repo) ---\n${memory.userProjectMemory.trim()}\n--- End Private Project Memory Index ---\n</user_project_memory>`,
|
||||
);
|
||||
}
|
||||
if (memory.extension?.trim()) {
|
||||
@@ -810,9 +828,30 @@ function toolUsageInteractive(
|
||||
function toolUsageRememberingFacts(
|
||||
options: OperationalGuidelinesOptions,
|
||||
): string {
|
||||
if (options.memoryManagerEnabled) {
|
||||
if (options.memoryV2Enabled) {
|
||||
const userProjectBullet = options.userProjectMemoryPath
|
||||
? `
|
||||
- **Private Project Memory** (\`${options.userProjectMemoryPath}\`): Personal-to-the-user, project-specific notes that must **NOT** be committed to the repo. Keep this file concise: it is the private index for this workspace. Store richer detail in sibling \`*.md\` files in the same folder and use \`MEMORY.md\` to point to them.`
|
||||
: '';
|
||||
const globalMemoryBullet = options.globalMemoryPath
|
||||
? `
|
||||
- **Global Personal Memory** (\`${options.globalMemoryPath}\`): Cross-project personal preferences and facts about the user that should follow them into every workspace (e.g. preferred testing framework across all projects, language preferences, coding-style defaults). Loaded automatically in every session. Keep entries concise and durable — never workspace-specific.`
|
||||
: '';
|
||||
const globalRoutingRule = options.globalMemoryPath
|
||||
? `
|
||||
- When the user states a **cross-project personal preference** that should follow them into every workspace ("I always prefer X", "across all my projects", "my personal coding style is Y", "in general I like Z"), update the global personal memory file. Do **not** also write it into a \`GEMINI.md\` file or the private memory folder.`
|
||||
: '';
|
||||
return `
|
||||
- **Memory Tool:** You MUST use the '${AGENT_TOOL_NAME}' tool with the 'save_memory' agent to proactively record facts, preferences, and workflows that apply across all sessions. Whenever the user explicitly tells you to "remember" something, or when they state a preference or workflow (like "always lint after editing"), you MUST immediately call the save_memory subagent. Never save transient session state. Do not use memory to store summaries of code changes, bug fixes, or findings discovered during a task; this tool is strictly for persistent general knowledge.`;
|
||||
- **Instruction and Memory Files:** You persist long-lived project context by editing markdown files directly with ${formatToolName(EDIT_TOOL_NAME)} or ${formatToolName(WRITE_FILE_TOOL_NAME)}. There is no \`save_memory\` tool. The current contents of all loaded \`GEMINI.md\` files and the private project \`MEMORY.md\` index are already in your context — do not re-read them before editing.
|
||||
- **Project Instructions** (\`./GEMINI.md\`): Team-shared architecture, conventions, workflows, and other repo guidance. **Committed to the repo and shared with the team.**
|
||||
- **Subdirectory Instructions** (e.g. \`./src/GEMINI.md\`): Scoped instructions for one part of the project. Reference them from \`./GEMINI.md\` so they remain discoverable.${userProjectBullet}${globalMemoryBullet}
|
||||
**Routing rules — pick exactly one tier per fact:**
|
||||
- When the user states a **team-shared convention, architecture rule, or repo-wide workflow** ("our project uses X", "the team always Y", "for this repo, always Z"), update the relevant \`GEMINI.md\` file. Do **not** also write it into the private memory folder or the global personal memory file.
|
||||
- When the user states a **personal-to-them local setup, machine-specific note, or private workflow** for this codebase ("on my machine", "my local setup", "do not commit this"), save it under the private project memory folder. Do **not** also write it into a \`GEMINI.md\` file or the global personal memory file.${globalRoutingRule}
|
||||
- If a fact could plausibly belong to more than one tier, **ask the user** which tier they want before writing.
|
||||
**Never duplicate or mirror the same fact across tiers** — each fact lives in exactly one file across all four tiers (project \`GEMINI.md\`, subdirectory \`GEMINI.md\`, private project memory, global personal memory). Do not add cross-references between any of them.
|
||||
**Inside the private memory folder:** \`MEMORY.md\` is the index for its sibling \`*.md\` notes **in that same folder only** — never use it to point at, summarize, or duplicate content from any \`GEMINI.md\` file. For brief facts, write the entry directly into \`MEMORY.md\`. When a note has substantial detail (multiple sections, procedures, or fields), put the detail in a sibling \`*.md\` file in the same folder and add a one-line pointer entry in \`MEMORY.md\`.
|
||||
Never save transient session state, summaries of code changes, bug fixes, or task-specific findings — these files are loaded into every session and must stay lean.`;
|
||||
}
|
||||
const base = `
|
||||
- **Memory Tool:** Use ${formatToolName(MEMORY_TOOL_NAME)} to persist facts across sessions. It supports two scopes via the \`scope\` parameter:
|
||||
|
||||
Reference in New Issue
Block a user