feat(a2a): Introduce /memory command for a2a server (#14456)

Co-authored-by: Shreya Keshive <shreyakeshive@google.com>
This commit is contained in:
Coco Sheng
2026-01-12 16:46:42 -05:00
committed by GitHub
parent e049d5e4e8
commit d7bff8610f
9 changed files with 703 additions and 77 deletions
+205
View 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
View 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,
};
}
+1
View File
@@ -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