feat(core): add experimental memory manager agent to replace save_memory tool (#22726)

Co-authored-by: Christian Gunderman <gundermanc@gmail.com>
This commit is contained in:
Sandy Tao
2026-03-19 12:57:52 -07:00
committed by GitHub
parent b3ebab308e
commit 33f630111f
27 changed files with 696 additions and 21 deletions
+29
View File
@@ -3104,6 +3104,35 @@ describe('Config JIT Initialization', () => {
expect(config.getUserMemory()).toBe('Initial Memory');
});
describe('isMemoryManagerEnabled', () => {
it('should default to false', () => {
const params: ConfigParameters = {
sessionId: 'test-session',
targetDir: '/tmp/test',
debugMode: false,
model: 'test-model',
cwd: '/tmp/test',
};
config = new Config(params);
expect(config.isMemoryManagerEnabled()).toBe(false);
});
it('should return true when experimentalMemoryManager is true', () => {
const params: ConfigParameters = {
sessionId: 'test-session',
targetDir: '/tmp/test',
debugMode: false,
model: 'test-model',
cwd: '/tmp/test',
experimentalMemoryManager: true,
};
config = new Config(params);
expect(config.isMemoryManagerEnabled()).toBe(true);
});
});
describe('reloadSkills', () => {
it('should refresh disabledSkills and re-register ActivateSkillTool when skills exist', async () => {
const mockOnReload = vi.fn().mockResolvedValue({
+12 -3
View File
@@ -629,6 +629,7 @@ export interface ConfigParameters {
disabledSkills?: string[];
adminSkillsEnabled?: boolean;
experimentalJitContext?: boolean;
experimentalMemoryManager?: boolean;
topicUpdateNarration?: boolean;
toolOutputMasking?: Partial<ToolOutputMaskingConfig>;
disableLLMCorrection?: boolean;
@@ -853,6 +854,7 @@ export class Config implements McpContext, AgentLoopContext {
private readonly adminSkillsEnabled: boolean;
private readonly experimentalJitContext: boolean;
private readonly experimentalMemoryManager: boolean;
private readonly topicUpdateNarration: boolean;
private readonly disableLLMCorrection: boolean;
private readonly planEnabled: boolean;
@@ -1013,6 +1015,7 @@ export class Config implements McpContext, AgentLoopContext {
);
this.experimentalJitContext = params.experimentalJitContext ?? true;
this.experimentalMemoryManager = params.experimentalMemoryManager ?? false;
this.topicUpdateNarration = params.topicUpdateNarration ?? false;
this.modelSteering = params.modelSteering ?? false;
this.injectionService = new InjectionService(() =>
@@ -2157,6 +2160,10 @@ export class Config implements McpContext, AgentLoopContext {
return this.experimentalJitContext;
}
isMemoryManagerEnabled(): boolean {
return this.experimentalMemoryManager;
}
isTopicUpdateNarrationEnabled(): boolean {
return this.topicUpdateNarration;
}
@@ -3184,9 +3191,11 @@ export class Config implements McpContext, AgentLoopContext {
maybeRegister(ShellTool, () =>
registry.registerTool(new ShellTool(this, this.messageBus)),
);
maybeRegister(MemoryTool, () =>
registry.registerTool(new MemoryTool(this.messageBus)),
);
if (!this.isMemoryManagerEnabled()) {
maybeRegister(MemoryTool, () =>
registry.registerTool(new MemoryTool(this.messageBus)),
);
}
maybeRegister(WebSearchTool, () =>
registry.registerTool(new WebSearchTool(this, this.messageBus)),
);
@@ -0,0 +1,68 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { Config } from './config.js';
import * as path from 'node:path';
import * as os from 'node:os';
vi.mock('node:fs', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:fs')>();
return {
...actual,
existsSync: vi.fn().mockReturnValue(true),
statSync: vi.fn().mockReturnValue({
isDirectory: vi.fn().mockReturnValue(true),
}),
realpathSync: vi.fn((p) => p),
};
});
vi.mock('../utils/paths.js', async (importOriginal) => {
const actual = await importOriginal<typeof import('../utils/paths.js')>();
return {
...actual,
resolveToRealPath: vi.fn((p) => p),
isSubpath: (parent: string, child: string) => child.startsWith(parent),
};
});
describe('Config Path Validation', () => {
let config: Config;
const targetDir = '/mock/workspace';
const globalGeminiDir = path.join(os.homedir(), '.gemini');
beforeEach(() => {
config = new Config({
targetDir,
sessionId: 'test-session',
debugMode: false,
cwd: targetDir,
model: 'test-model',
});
});
it('should allow access to ~/.gemini if it is added to the workspace', () => {
const geminiMdPath = path.join(globalGeminiDir, 'GEMINI.md');
// Before adding, it should be denied
expect(config.isPathAllowed(geminiMdPath)).toBe(false);
// Add to workspace
config.getWorkspaceContext().addDirectory(globalGeminiDir);
// Now it should be allowed
expect(config.isPathAllowed(geminiMdPath)).toBe(true);
expect(config.validatePathAccess(geminiMdPath, 'read')).toBeNull();
expect(config.validatePathAccess(geminiMdPath, 'write')).toBeNull();
});
it('should still allow project workspace paths', () => {
const workspacePath = path.join(targetDir, 'src/index.ts');
expect(config.isPathAllowed(workspacePath)).toBe(true);
expect(config.validatePathAccess(workspacePath, 'read')).toBeNull();
});
});