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

View File

@@ -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) {

View 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.');
});
});
});

View 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.`,
};
}
}
}

View File

@@ -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,
});

View File

@@ -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();
});
});

View File

@@ -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(),
);

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.');
}
});
});
});

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,
};
}

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