mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-28 05:55:17 -07:00
feat(a2a): Introduce /memory command for a2a server (#14456)
Co-authored-by: Shreya Keshive <shreyakeshive@google.com>
This commit is contained in:
@@ -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.');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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