mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 14:10:37 -07:00
feat(a2a): Introduce /memory command for a2a server (#14456)
Co-authored-by: Shreya Keshive <shreyakeshive@google.com>
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { MemoryCommand } from './memory.js';
|
||||
import { debugLogger } from '@google/gemini-cli-core';
|
||||
import { ExtensionsCommand } from './extensions.js';
|
||||
import { InitCommand } from './init.js';
|
||||
@@ -22,6 +23,7 @@ export class CommandRegistry {
|
||||
this.register(new ExtensionsCommand());
|
||||
this.register(new RestoreCommand());
|
||||
this.register(new InitCommand());
|
||||
this.register(new MemoryCommand());
|
||||
}
|
||||
|
||||
register(command: Command) {
|
||||
|
||||
208
packages/a2a-server/src/commands/memory.test.ts
Normal file
208
packages/a2a-server/src/commands/memory.test.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
addMemory,
|
||||
listMemoryFiles,
|
||||
refreshMemory,
|
||||
showMemory,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
AddMemoryCommand,
|
||||
ListMemoryCommand,
|
||||
MemoryCommand,
|
||||
RefreshMemoryCommand,
|
||||
ShowMemoryCommand,
|
||||
} from './memory.js';
|
||||
import type { CommandContext } from './types.js';
|
||||
import type {
|
||||
AnyDeclarativeTool,
|
||||
Config,
|
||||
ToolRegistry,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
// Mock the core functions
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||
return {
|
||||
...actual,
|
||||
showMemory: vi.fn(),
|
||||
refreshMemory: vi.fn(),
|
||||
listMemoryFiles: vi.fn(),
|
||||
addMemory: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const mockShowMemory = vi.mocked(showMemory);
|
||||
const mockRefreshMemory = vi.mocked(refreshMemory);
|
||||
const mockListMemoryFiles = vi.mocked(listMemoryFiles);
|
||||
const mockAddMemory = vi.mocked(addMemory);
|
||||
|
||||
describe('a2a-server memory commands', () => {
|
||||
let mockContext: CommandContext;
|
||||
let mockConfig: Config;
|
||||
let mockToolRegistry: ToolRegistry;
|
||||
let mockSaveMemoryTool: AnyDeclarativeTool;
|
||||
|
||||
beforeEach(() => {
|
||||
mockSaveMemoryTool = {
|
||||
name: 'save_memory',
|
||||
description: 'Saves memory',
|
||||
buildAndExecute: vi.fn().mockResolvedValue(undefined),
|
||||
} as unknown as AnyDeclarativeTool;
|
||||
|
||||
mockToolRegistry = {
|
||||
getTool: vi.fn(),
|
||||
} as unknown as ToolRegistry;
|
||||
|
||||
mockConfig = {
|
||||
getToolRegistry: vi.fn().mockReturnValue(mockToolRegistry),
|
||||
} as unknown as Config;
|
||||
|
||||
mockContext = {
|
||||
config: mockConfig,
|
||||
};
|
||||
|
||||
vi.mocked(mockToolRegistry.getTool).mockReturnValue(mockSaveMemoryTool);
|
||||
});
|
||||
|
||||
describe('MemoryCommand', () => {
|
||||
it('delegates to ShowMemoryCommand', async () => {
|
||||
const command = new MemoryCommand();
|
||||
mockShowMemory.mockReturnValue({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: 'showing memory',
|
||||
});
|
||||
const response = await command.execute(mockContext, []);
|
||||
expect(response.data).toBe('showing memory');
|
||||
expect(mockShowMemory).toHaveBeenCalledWith(mockContext.config);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ShowMemoryCommand', () => {
|
||||
it('executes showMemory and returns the content', async () => {
|
||||
const command = new ShowMemoryCommand();
|
||||
mockShowMemory.mockReturnValue({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: 'test memory content',
|
||||
});
|
||||
|
||||
const response = await command.execute(mockContext, []);
|
||||
|
||||
expect(mockShowMemory).toHaveBeenCalledWith(mockContext.config);
|
||||
expect(response.name).toBe('memory show');
|
||||
expect(response.data).toBe('test memory content');
|
||||
});
|
||||
});
|
||||
|
||||
describe('RefreshMemoryCommand', () => {
|
||||
it('executes refreshMemory and returns the content', async () => {
|
||||
const command = new RefreshMemoryCommand();
|
||||
mockRefreshMemory.mockResolvedValue({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: 'memory refreshed',
|
||||
});
|
||||
|
||||
const response = await command.execute(mockContext, []);
|
||||
|
||||
expect(mockRefreshMemory).toHaveBeenCalledWith(mockContext.config);
|
||||
expect(response.name).toBe('memory refresh');
|
||||
expect(response.data).toBe('memory refreshed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ListMemoryCommand', () => {
|
||||
it('executes listMemoryFiles and returns the content', async () => {
|
||||
const command = new ListMemoryCommand();
|
||||
mockListMemoryFiles.mockReturnValue({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: 'file1.md\nfile2.md',
|
||||
});
|
||||
|
||||
const response = await command.execute(mockContext, []);
|
||||
|
||||
expect(mockListMemoryFiles).toHaveBeenCalledWith(mockContext.config);
|
||||
expect(response.name).toBe('memory list');
|
||||
expect(response.data).toBe('file1.md\nfile2.md');
|
||||
});
|
||||
});
|
||||
|
||||
describe('AddMemoryCommand', () => {
|
||||
it('returns message content if addMemory returns a message', async () => {
|
||||
const command = new AddMemoryCommand();
|
||||
mockAddMemory.mockReturnValue({
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'error message',
|
||||
});
|
||||
|
||||
const response = await command.execute(mockContext, []);
|
||||
|
||||
expect(mockAddMemory).toHaveBeenCalledWith('');
|
||||
expect(response.name).toBe('memory add');
|
||||
expect(response.data).toBe('error message');
|
||||
});
|
||||
|
||||
it('executes the save_memory tool if found', async () => {
|
||||
const command = new AddMemoryCommand();
|
||||
const fact = 'this is a new fact';
|
||||
mockAddMemory.mockReturnValue({
|
||||
type: 'tool',
|
||||
toolName: 'save_memory',
|
||||
toolArgs: { fact },
|
||||
});
|
||||
|
||||
const response = await command.execute(mockContext, [
|
||||
'this',
|
||||
'is',
|
||||
'a',
|
||||
'new',
|
||||
'fact',
|
||||
]);
|
||||
|
||||
expect(mockAddMemory).toHaveBeenCalledWith(fact);
|
||||
expect(mockConfig.getToolRegistry).toHaveBeenCalled();
|
||||
expect(mockToolRegistry.getTool).toHaveBeenCalledWith('save_memory');
|
||||
expect(mockSaveMemoryTool.buildAndExecute).toHaveBeenCalledWith(
|
||||
{ fact },
|
||||
expect.any(AbortSignal),
|
||||
undefined,
|
||||
{
|
||||
sanitizationConfig: {
|
||||
allowedEnvironmentVariables: [],
|
||||
blockedEnvironmentVariables: [],
|
||||
enableEnvironmentVariableRedaction: false,
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(mockRefreshMemory).toHaveBeenCalledWith(mockContext.config);
|
||||
expect(response.name).toBe('memory add');
|
||||
expect(response.data).toBe(`Added memory: "${fact}"`);
|
||||
});
|
||||
|
||||
it('returns an error if the tool is not found', async () => {
|
||||
const command = new AddMemoryCommand();
|
||||
const fact = 'another fact';
|
||||
mockAddMemory.mockReturnValue({
|
||||
type: 'tool',
|
||||
toolName: 'save_memory',
|
||||
toolArgs: { fact },
|
||||
});
|
||||
vi.mocked(mockToolRegistry.getTool).mockReturnValue(undefined);
|
||||
|
||||
const response = await command.execute(mockContext, ['another', 'fact']);
|
||||
|
||||
expect(response.name).toBe('memory add');
|
||||
expect(response.data).toBe('Error: Tool save_memory not found.');
|
||||
});
|
||||
});
|
||||
});
|
||||
118
packages/a2a-server/src/commands/memory.ts
Normal file
118
packages/a2a-server/src/commands/memory.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {
|
||||
addMemory,
|
||||
listMemoryFiles,
|
||||
refreshMemory,
|
||||
showMemory,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type {
|
||||
Command,
|
||||
CommandContext,
|
||||
CommandExecutionResponse,
|
||||
} from './types.js';
|
||||
|
||||
const DEFAULT_SANITIZATION_CONFIG = {
|
||||
allowedEnvironmentVariables: [],
|
||||
blockedEnvironmentVariables: [],
|
||||
enableEnvironmentVariableRedaction: false,
|
||||
};
|
||||
|
||||
export class MemoryCommand implements Command {
|
||||
readonly name = 'memory';
|
||||
readonly description = 'Manage memory.';
|
||||
readonly subCommands = [
|
||||
new ShowMemoryCommand(),
|
||||
new RefreshMemoryCommand(),
|
||||
new ListMemoryCommand(),
|
||||
new AddMemoryCommand(),
|
||||
];
|
||||
readonly topLevel = true;
|
||||
readonly requiresWorkspace = true;
|
||||
|
||||
async execute(
|
||||
context: CommandContext,
|
||||
_: string[],
|
||||
): Promise<CommandExecutionResponse> {
|
||||
return new ShowMemoryCommand().execute(context, _);
|
||||
}
|
||||
}
|
||||
|
||||
export class ShowMemoryCommand implements Command {
|
||||
readonly name = 'memory show';
|
||||
readonly description = 'Shows the current memory contents.';
|
||||
|
||||
async execute(
|
||||
context: CommandContext,
|
||||
_: string[],
|
||||
): Promise<CommandExecutionResponse> {
|
||||
const result = showMemory(context.config);
|
||||
return { name: this.name, data: result.content };
|
||||
}
|
||||
}
|
||||
|
||||
export class RefreshMemoryCommand implements Command {
|
||||
readonly name = 'memory refresh';
|
||||
readonly description = 'Refreshes the memory from the source.';
|
||||
|
||||
async execute(
|
||||
context: CommandContext,
|
||||
_: string[],
|
||||
): Promise<CommandExecutionResponse> {
|
||||
const result = await refreshMemory(context.config);
|
||||
return { name: this.name, data: result.content };
|
||||
}
|
||||
}
|
||||
|
||||
export class ListMemoryCommand implements Command {
|
||||
readonly name = 'memory list';
|
||||
readonly description = 'Lists the paths of the GEMINI.md files in use.';
|
||||
|
||||
async execute(
|
||||
context: CommandContext,
|
||||
_: string[],
|
||||
): Promise<CommandExecutionResponse> {
|
||||
const result = listMemoryFiles(context.config);
|
||||
return { name: this.name, data: result.content };
|
||||
}
|
||||
}
|
||||
|
||||
export class AddMemoryCommand implements Command {
|
||||
readonly name = 'memory add';
|
||||
readonly description = 'Add content to the memory.';
|
||||
|
||||
async execute(
|
||||
context: CommandContext,
|
||||
args: string[],
|
||||
): Promise<CommandExecutionResponse> {
|
||||
const textToAdd = args.join(' ').trim();
|
||||
const result = addMemory(textToAdd);
|
||||
if (result.type === 'message') {
|
||||
return { name: this.name, data: result.content };
|
||||
}
|
||||
|
||||
const toolRegistry = context.config.getToolRegistry();
|
||||
const tool = toolRegistry.getTool(result.toolName);
|
||||
if (tool) {
|
||||
const abortController = new AbortController();
|
||||
const signal = abortController.signal;
|
||||
await tool.buildAndExecute(result.toolArgs, signal, undefined, {
|
||||
sanitizationConfig: DEFAULT_SANITIZATION_CONFIG,
|
||||
});
|
||||
await refreshMemory(context.config);
|
||||
return {
|
||||
name: this.name,
|
||||
data: `Added memory: "${textToAdd}"`,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
name: this.name,
|
||||
data: `Error: Tool ${result.toolName} not found.`,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,10 @@ export async function loadConfig(
|
||||
const workspaceDir = process.cwd();
|
||||
const adcFilePath = process.env['GOOGLE_APPLICATION_CREDENTIALS'];
|
||||
|
||||
const folderTrust =
|
||||
settings.folderTrust === true ||
|
||||
process.env['GEMINI_FOLDER_TRUST'] === 'true';
|
||||
|
||||
const configParams: ConfigParameters = {
|
||||
sessionId: taskId,
|
||||
model: settings.general?.previewFeatures
|
||||
@@ -72,7 +76,8 @@ export async function loadConfig(
|
||||
settings.fileFiltering?.enableRecursiveFileSearch,
|
||||
},
|
||||
ideMode: false,
|
||||
folderTrust: settings.folderTrust === true,
|
||||
folderTrust,
|
||||
trustedFolder: true,
|
||||
extensionLoader,
|
||||
checkpointing: process.env['CHECKPOINTING']
|
||||
? process.env['CHECKPOINTING'] === 'true'
|
||||
@@ -83,16 +88,18 @@ export async function loadConfig(
|
||||
};
|
||||
|
||||
const fileService = new FileDiscoveryService(workspaceDir);
|
||||
const { memoryContent, fileCount } = await loadServerHierarchicalMemory(
|
||||
workspaceDir,
|
||||
[workspaceDir],
|
||||
false,
|
||||
fileService,
|
||||
extensionLoader,
|
||||
settings.folderTrust === true,
|
||||
);
|
||||
const { memoryContent, fileCount, filePaths } =
|
||||
await loadServerHierarchicalMemory(
|
||||
workspaceDir,
|
||||
[workspaceDir],
|
||||
false,
|
||||
fileService,
|
||||
extensionLoader,
|
||||
folderTrust,
|
||||
);
|
||||
configParams.userMemory = memoryContent;
|
||||
configParams.geminiMdFileCount = fileCount;
|
||||
configParams.geminiMdFilePaths = filePaths;
|
||||
const config = new Config({
|
||||
...configParams,
|
||||
});
|
||||
|
||||
@@ -12,12 +12,11 @@ import { createMockCommandContext } from '../../test-utils/mockCommandContext.js
|
||||
import { MessageType } from '../types.js';
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
import {
|
||||
getErrorMessage,
|
||||
refreshMemory,
|
||||
refreshServerHierarchicalMemory,
|
||||
SimpleExtensionLoader,
|
||||
type FileDiscoveryService,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type { LoadServerHierarchicalMemoryResponse } from '@google/gemini-cli-core/index.js';
|
||||
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const original =
|
||||
@@ -28,10 +27,28 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
if (error instanceof Error) return error.message;
|
||||
return String(error);
|
||||
}),
|
||||
refreshMemory: vi.fn(async (config) => {
|
||||
if (config.isJitContextEnabled()) {
|
||||
await config.getContextManager()?.refresh();
|
||||
const memoryContent = config.getUserMemory() || '';
|
||||
const fileCount = config.getGeminiMdFileCount() || 0;
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: `Memory refreshed successfully. Loaded ${memoryContent.length} characters from ${fileCount} file(s).`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: 'Memory refreshed successfully.',
|
||||
};
|
||||
}),
|
||||
refreshServerHierarchicalMemory: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const mockRefreshMemory = refreshMemory as Mock;
|
||||
const mockRefreshServerHierarchicalMemory =
|
||||
refreshServerHierarchicalMemory as Mock;
|
||||
|
||||
@@ -208,7 +225,7 @@ describe('memoryCommand', () => {
|
||||
} as unknown as LoadedSettings,
|
||||
},
|
||||
});
|
||||
mockRefreshServerHierarchicalMemory.mockClear();
|
||||
mockRefreshMemory.mockClear();
|
||||
});
|
||||
|
||||
it('should use ContextManager.refresh when JIT is enabled', async () => {
|
||||
@@ -239,12 +256,13 @@ describe('memoryCommand', () => {
|
||||
it('should display success message when memory is refreshed with content (Legacy)', async () => {
|
||||
if (!refreshCommand.action) throw new Error('Command has no action');
|
||||
|
||||
const refreshResult: LoadServerHierarchicalMemoryResponse = {
|
||||
memoryContent: 'new memory content',
|
||||
fileCount: 2,
|
||||
filePaths: ['/path/one/GEMINI.md', '/path/two/GEMINI.md'],
|
||||
const successMessage = {
|
||||
type: 'message',
|
||||
messageType: MessageType.INFO,
|
||||
content:
|
||||
'Memory refreshed successfully. Loaded 18 characters from 2 file(s).',
|
||||
};
|
||||
mockRefreshServerHierarchicalMemory.mockResolvedValue(refreshResult);
|
||||
mockRefreshMemory.mockResolvedValue(successMessage);
|
||||
|
||||
await refreshCommand.action(mockContext, '');
|
||||
|
||||
@@ -256,7 +274,7 @@ describe('memoryCommand', () => {
|
||||
expect.any(Number),
|
||||
);
|
||||
|
||||
expect(mockRefreshServerHierarchicalMemory).toHaveBeenCalledOnce();
|
||||
expect(mockRefreshMemory).toHaveBeenCalledOnce();
|
||||
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||
{
|
||||
@@ -270,12 +288,16 @@ describe('memoryCommand', () => {
|
||||
it('should display success message when memory is refreshed with no content', async () => {
|
||||
if (!refreshCommand.action) throw new Error('Command has no action');
|
||||
|
||||
const refreshResult = { memoryContent: '', fileCount: 0, filePaths: [] };
|
||||
mockRefreshServerHierarchicalMemory.mockResolvedValue(refreshResult);
|
||||
const successMessage = {
|
||||
type: 'message',
|
||||
messageType: MessageType.INFO,
|
||||
content: 'Memory refreshed successfully. No memory content found.',
|
||||
};
|
||||
mockRefreshMemory.mockResolvedValue(successMessage);
|
||||
|
||||
await refreshCommand.action(mockContext, '');
|
||||
|
||||
expect(mockRefreshServerHierarchicalMemory).toHaveBeenCalledOnce();
|
||||
expect(mockRefreshMemory).toHaveBeenCalledOnce();
|
||||
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||
{
|
||||
@@ -290,11 +312,11 @@ describe('memoryCommand', () => {
|
||||
if (!refreshCommand.action) throw new Error('Command has no action');
|
||||
|
||||
const error = new Error('Failed to read memory files.');
|
||||
mockRefreshServerHierarchicalMemory.mockRejectedValue(error);
|
||||
mockRefreshMemory.mockRejectedValue(error);
|
||||
|
||||
await refreshCommand.action(mockContext, '');
|
||||
|
||||
expect(mockRefreshServerHierarchicalMemory).toHaveBeenCalledOnce();
|
||||
expect(mockRefreshMemory).toHaveBeenCalledOnce();
|
||||
expect(mockSetUserMemory).not.toHaveBeenCalled();
|
||||
expect(mockSetGeminiMdFileCount).not.toHaveBeenCalled();
|
||||
expect(mockSetGeminiMdFilePaths).not.toHaveBeenCalled();
|
||||
@@ -306,8 +328,6 @@ describe('memoryCommand', () => {
|
||||
},
|
||||
expect.any(Number),
|
||||
);
|
||||
|
||||
expect(getErrorMessage).toHaveBeenCalledWith(error);
|
||||
});
|
||||
|
||||
it('should not throw if config service is unavailable', async () => {
|
||||
@@ -329,7 +349,7 @@ describe('memoryCommand', () => {
|
||||
expect.any(Number),
|
||||
);
|
||||
|
||||
expect(mockRefreshServerHierarchicalMemory).not.toHaveBeenCalled();
|
||||
expect(mockRefreshMemory).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
getErrorMessage,
|
||||
refreshServerHierarchicalMemory,
|
||||
addMemory,
|
||||
listMemoryFiles,
|
||||
refreshMemory,
|
||||
showMemory,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { MessageType } from '../types.js';
|
||||
import type { SlashCommand, SlashCommandActionReturn } from './types.js';
|
||||
@@ -24,18 +26,14 @@ export const memoryCommand: SlashCommand = {
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: async (context) => {
|
||||
const memoryContent = context.services.config?.getUserMemory() || '';
|
||||
const fileCount = context.services.config?.getGeminiMdFileCount() || 0;
|
||||
|
||||
const messageContent =
|
||||
memoryContent.length > 0
|
||||
? `Current memory content from ${fileCount} file(s):\n\n---\n${memoryContent}\n---`
|
||||
: 'Memory is currently empty.';
|
||||
const config = context.services.config;
|
||||
if (!config) return;
|
||||
const result = showMemory(config);
|
||||
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: messageContent,
|
||||
text: result.content,
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
@@ -47,12 +45,10 @@ export const memoryCommand: SlashCommand = {
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: false,
|
||||
action: (context, args): SlashCommandActionReturn | void => {
|
||||
if (!args || args.trim() === '') {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Usage: /memory add <text to remember>',
|
||||
};
|
||||
const result = addMemory(args);
|
||||
|
||||
if (result.type === 'message') {
|
||||
return result;
|
||||
}
|
||||
|
||||
context.ui.addItem(
|
||||
@@ -63,11 +59,7 @@ export const memoryCommand: SlashCommand = {
|
||||
Date.now(),
|
||||
);
|
||||
|
||||
return {
|
||||
type: 'tool',
|
||||
toolName: 'save_memory',
|
||||
toolArgs: { fact: args.trim() },
|
||||
};
|
||||
return result;
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -87,40 +79,21 @@ export const memoryCommand: SlashCommand = {
|
||||
try {
|
||||
const config = context.services.config;
|
||||
if (config) {
|
||||
let memoryContent = '';
|
||||
let fileCount = 0;
|
||||
|
||||
if (config.isJitContextEnabled()) {
|
||||
await config.getContextManager()?.refresh();
|
||||
memoryContent = config.getUserMemory();
|
||||
fileCount = config.getGeminiMdFileCount();
|
||||
} else {
|
||||
const result = await refreshServerHierarchicalMemory(config);
|
||||
memoryContent = result.memoryContent;
|
||||
fileCount = result.fileCount;
|
||||
}
|
||||
|
||||
await config.updateSystemInstructionIfInitialized();
|
||||
|
||||
const successMessage =
|
||||
memoryContent.length > 0
|
||||
? `Memory refreshed successfully. Loaded ${memoryContent.length} characters from ${fileCount} file(s).`
|
||||
: 'Memory refreshed successfully. No memory content found.';
|
||||
const result = await refreshMemory(config);
|
||||
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: successMessage,
|
||||
text: result.content,
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error);
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: MessageType.ERROR,
|
||||
text: `Error refreshing memory: ${errorMessage}`,
|
||||
text: `Error refreshing memory: ${(error as Error).message}`,
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
@@ -133,18 +106,14 @@ export const memoryCommand: SlashCommand = {
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: async (context) => {
|
||||
const filePaths = context.services.config?.getGeminiMdFilePaths() || [];
|
||||
const fileCount = filePaths.length;
|
||||
|
||||
const messageContent =
|
||||
fileCount > 0
|
||||
? `There are ${fileCount} GEMINI.md file(s) in use:\n\n${filePaths.join('\n')}`
|
||||
: 'No GEMINI.md files in use.';
|
||||
const config = context.services.config;
|
||||
if (!config) return;
|
||||
const result = listMemoryFiles(config);
|
||||
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: messageContent,
|
||||
text: result.content,
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
|
||||
205
packages/core/src/commands/memory.test.ts
Normal file
205
packages/core/src/commands/memory.test.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import type { Config } from '../config/config.js';
|
||||
import {
|
||||
addMemory,
|
||||
listMemoryFiles,
|
||||
refreshMemory,
|
||||
showMemory,
|
||||
} from './memory.js';
|
||||
import * as memoryDiscovery from '../utils/memoryDiscovery.js';
|
||||
|
||||
vi.mock('../utils/memoryDiscovery.js', () => ({
|
||||
refreshServerHierarchicalMemory: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockRefresh = vi.mocked(memoryDiscovery.refreshServerHierarchicalMemory);
|
||||
|
||||
describe('memory commands', () => {
|
||||
let mockConfig: Config;
|
||||
|
||||
beforeEach(() => {
|
||||
mockConfig = {
|
||||
getUserMemory: vi.fn(),
|
||||
getGeminiMdFileCount: vi.fn(),
|
||||
getGeminiMdFilePaths: vi.fn(),
|
||||
isJitContextEnabled: vi.fn(),
|
||||
updateSystemInstructionIfInitialized: vi
|
||||
.fn()
|
||||
.mockResolvedValue(undefined),
|
||||
} as unknown as Config;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('showMemory', () => {
|
||||
it('should show memory content if it exists', () => {
|
||||
vi.mocked(mockConfig.getUserMemory).mockReturnValue(
|
||||
'some memory content',
|
||||
);
|
||||
vi.mocked(mockConfig.getGeminiMdFileCount).mockReturnValue(1);
|
||||
|
||||
const result = showMemory(mockConfig);
|
||||
|
||||
expect(result.type).toBe('message');
|
||||
if (result.type === 'message') {
|
||||
expect(result.messageType).toBe('info');
|
||||
expect(result.content).toContain(
|
||||
'Current memory content from 1 file(s)',
|
||||
);
|
||||
expect(result.content).toContain('some memory content');
|
||||
}
|
||||
});
|
||||
|
||||
it('should show a message if memory is empty', () => {
|
||||
vi.mocked(mockConfig.getUserMemory).mockReturnValue('');
|
||||
vi.mocked(mockConfig.getGeminiMdFileCount).mockReturnValue(0);
|
||||
|
||||
const result = showMemory(mockConfig);
|
||||
|
||||
expect(result.type).toBe('message');
|
||||
if (result.type === 'message') {
|
||||
expect(result.messageType).toBe('info');
|
||||
expect(result.content).toBe('Memory is currently empty.');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('addMemory', () => {
|
||||
it('should return a tool action to save memory', () => {
|
||||
const result = addMemory('new memory');
|
||||
expect(result.type).toBe('tool');
|
||||
if (result.type === 'tool') {
|
||||
expect(result.toolName).toBe('save_memory');
|
||||
expect(result.toolArgs).toEqual({ fact: 'new memory' });
|
||||
}
|
||||
});
|
||||
|
||||
it('should trim the arguments', () => {
|
||||
const result = addMemory(' new memory ');
|
||||
expect(result.type).toBe('tool');
|
||||
if (result.type === 'tool') {
|
||||
expect(result.toolArgs).toEqual({ fact: 'new memory' });
|
||||
}
|
||||
});
|
||||
|
||||
it('should return an error if args are empty', () => {
|
||||
const result = addMemory('');
|
||||
expect(result.type).toBe('message');
|
||||
if (result.type === 'message') {
|
||||
expect(result.messageType).toBe('error');
|
||||
expect(result.content).toBe('Usage: /memory add <text to remember>');
|
||||
}
|
||||
});
|
||||
|
||||
it('should return an error if args are just whitespace', () => {
|
||||
const result = addMemory(' ');
|
||||
expect(result.type).toBe('message');
|
||||
if (result.type === 'message') {
|
||||
expect(result.messageType).toBe('error');
|
||||
expect(result.content).toBe('Usage: /memory add <text to remember>');
|
||||
}
|
||||
});
|
||||
|
||||
it('should return an error if args are undefined', () => {
|
||||
const result = addMemory(undefined);
|
||||
expect(result.type).toBe('message');
|
||||
if (result.type === 'message') {
|
||||
expect(result.messageType).toBe('error');
|
||||
expect(result.content).toBe('Usage: /memory add <text to remember>');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('refreshMemory', () => {
|
||||
it('should refresh memory and show success message', async () => {
|
||||
mockRefresh.mockResolvedValue({
|
||||
memoryContent: 'refreshed content',
|
||||
fileCount: 2,
|
||||
filePaths: [],
|
||||
});
|
||||
|
||||
const result = await refreshMemory(mockConfig);
|
||||
|
||||
expect(mockRefresh).toHaveBeenCalledWith(mockConfig);
|
||||
expect(
|
||||
mockConfig.updateSystemInstructionIfInitialized,
|
||||
).toHaveBeenCalled();
|
||||
expect(result.type).toBe('message');
|
||||
if (result.type === 'message') {
|
||||
expect(result.messageType).toBe('info');
|
||||
expect(result.content).toBe(
|
||||
'Memory refreshed successfully. Loaded 17 characters from 2 file(s).',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should show a message if no memory content is found after refresh', async () => {
|
||||
mockRefresh.mockResolvedValue({
|
||||
memoryContent: '',
|
||||
fileCount: 0,
|
||||
filePaths: [],
|
||||
});
|
||||
|
||||
const result = await refreshMemory(mockConfig);
|
||||
expect(result.type).toBe('message');
|
||||
if (result.type === 'message') {
|
||||
expect(result.messageType).toBe('info');
|
||||
expect(result.content).toBe(
|
||||
'Memory refreshed successfully. No memory content found.',
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('listMemoryFiles', () => {
|
||||
it('should list the memory files in use', () => {
|
||||
const filePaths = ['/path/to/GEMINI.md', '/other/path/GEMINI.md'];
|
||||
vi.mocked(mockConfig.getGeminiMdFilePaths).mockReturnValue(filePaths);
|
||||
|
||||
const result = listMemoryFiles(mockConfig);
|
||||
|
||||
expect(result.type).toBe('message');
|
||||
if (result.type === 'message') {
|
||||
expect(result.messageType).toBe('info');
|
||||
expect(result.content).toContain(
|
||||
'There are 2 GEMINI.md file(s) in use:',
|
||||
);
|
||||
expect(result.content).toContain(filePaths.join('\n'));
|
||||
}
|
||||
});
|
||||
|
||||
it('should show a message if no memory files are in use', () => {
|
||||
vi.mocked(mockConfig.getGeminiMdFilePaths).mockReturnValue([]);
|
||||
|
||||
const result = listMemoryFiles(mockConfig);
|
||||
|
||||
expect(result.type).toBe('message');
|
||||
if (result.type === 'message') {
|
||||
expect(result.messageType).toBe('info');
|
||||
expect(result.content).toBe('No GEMINI.md files in use.');
|
||||
}
|
||||
});
|
||||
|
||||
it('should show a message if file paths are undefined', () => {
|
||||
vi.mocked(mockConfig.getGeminiMdFilePaths).mockReturnValue(
|
||||
undefined as unknown as string[],
|
||||
);
|
||||
|
||||
const result = listMemoryFiles(mockConfig);
|
||||
|
||||
expect(result.type).toBe('message');
|
||||
if (result.type === 'message') {
|
||||
expect(result.messageType).toBe('info');
|
||||
expect(result.content).toBe('No GEMINI.md files in use.');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
96
packages/core/src/commands/memory.ts
Normal file
96
packages/core/src/commands/memory.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { Config } from '../config/config.js';
|
||||
import { refreshServerHierarchicalMemory } from '../utils/memoryDiscovery.js';
|
||||
import type { MessageActionReturn, ToolActionReturn } from './types.js';
|
||||
|
||||
export function showMemory(config: Config): MessageActionReturn {
|
||||
const memoryContent = config.getUserMemory() || '';
|
||||
const fileCount = config.getGeminiMdFileCount() || 0;
|
||||
let content: string;
|
||||
|
||||
if (memoryContent.length > 0) {
|
||||
content = `Current memory content from ${fileCount} file(s):\n\n---\n${memoryContent}\n---`;
|
||||
} else {
|
||||
content = 'Memory is currently empty.';
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content,
|
||||
};
|
||||
}
|
||||
|
||||
export function addMemory(
|
||||
args?: string,
|
||||
): MessageActionReturn | ToolActionReturn {
|
||||
if (!args || args.trim() === '') {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Usage: /memory add <text to remember>',
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: 'tool',
|
||||
toolName: 'save_memory',
|
||||
toolArgs: { fact: args.trim() },
|
||||
};
|
||||
}
|
||||
|
||||
export async function refreshMemory(
|
||||
config: Config,
|
||||
): Promise<MessageActionReturn> {
|
||||
let memoryContent = '';
|
||||
let fileCount = 0;
|
||||
|
||||
if (config.isJitContextEnabled()) {
|
||||
await config.getContextManager()?.refresh();
|
||||
memoryContent = config.getUserMemory();
|
||||
fileCount = config.getGeminiMdFileCount();
|
||||
} else {
|
||||
const result = await refreshServerHierarchicalMemory(config);
|
||||
memoryContent = result.memoryContent;
|
||||
fileCount = result.fileCount;
|
||||
}
|
||||
|
||||
await config.updateSystemInstructionIfInitialized();
|
||||
let content: string;
|
||||
|
||||
if (memoryContent.length > 0) {
|
||||
content = `Memory refreshed successfully. Loaded ${memoryContent.length} characters from ${fileCount} file(s).`;
|
||||
} else {
|
||||
content = 'Memory refreshed successfully. No memory content found.';
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content,
|
||||
};
|
||||
}
|
||||
|
||||
export function listMemoryFiles(config: Config): MessageActionReturn {
|
||||
const filePaths = config.getGeminiMdFilePaths() || [];
|
||||
const fileCount = filePaths.length;
|
||||
let content: string;
|
||||
|
||||
if (fileCount > 0) {
|
||||
content = `There are ${fileCount} GEMINI.md file(s) in use:\n\n${filePaths.join(
|
||||
'\n',
|
||||
)}`;
|
||||
} else {
|
||||
content = 'No GEMINI.md files in use.';
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content,
|
||||
};
|
||||
}
|
||||
@@ -22,6 +22,7 @@ export * from './confirmation-bus/message-bus.js';
|
||||
export * from './commands/extensions.js';
|
||||
export * from './commands/restore.js';
|
||||
export * from './commands/init.js';
|
||||
export * from './commands/memory.js';
|
||||
export * from './commands/types.js';
|
||||
|
||||
// Export Core Logic
|
||||
|
||||
Reference in New Issue
Block a user