/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { BaseTool, ToolResult, ToolEditConfirmationDetails, ToolConfirmationOutcome, Icon, } from './tools.js'; import { FunctionDeclaration, Type } from '@google/genai'; import * as fs from 'fs/promises'; import * as path from 'path'; import { homedir } from 'os'; import * as Diff from 'diff'; import { DEFAULT_DIFF_OPTIONS } from './diffOptions.js'; import { tildeifyPath } from '../utils/paths.js'; import { ModifiableTool, ModifyContext } from './modifiable-tool.js'; const memoryToolSchemaData: FunctionDeclaration = { name: 'save_memory', description: 'Saves a specific piece of information or fact to your long-term memory. Use this when the user explicitly asks you to remember something, or when they state a clear, concise fact that seems important to retain for future interactions.', parameters: { type: Type.OBJECT, properties: { fact: { type: Type.STRING, description: 'The specific fact or piece of information to remember. Should be a clear, self-contained statement.', }, }, required: ['fact'], }, }; const memoryToolDescription = ` Saves a specific piece of information or fact to your long-term memory. Use this tool: - When the user explicitly asks you to remember something (e.g., "Remember that I like pineapple on pizza", "Please save this: my cat's name is Whiskers"). - When the user states a clear, concise fact about themselves, their preferences, or their environment that seems important for you to retain for future interactions to provide a more personalized and effective assistance. Do NOT use this tool: - To remember conversational context that is only relevant for the current session. - To save long, complex, or rambling pieces of text. The fact should be relatively short and to the point. - If you are unsure whether the information is a fact worth remembering long-term. If in doubt, you can ask the user, "Should I remember that for you?" ## Parameters - \`fact\` (string, required): The specific fact or piece of information to remember. This should be a clear, self-contained statement. For example, if the user says "My favorite color is blue", the fact would be "My favorite color is blue". `; export const GEMINI_CONFIG_DIR = '.gemini'; export const DEFAULT_CONTEXT_FILENAME = 'GEMINI.md'; export const MEMORY_SECTION_HEADER = '## Gemini Added Memories'; // This variable will hold the currently configured filename for GEMINI.md context files. // It defaults to DEFAULT_CONTEXT_FILENAME but can be overridden by setGeminiMdFilename. let currentGeminiMdFilename: string | string[] = DEFAULT_CONTEXT_FILENAME; export function setGeminiMdFilename(newFilename: string | string[]): void { if (Array.isArray(newFilename)) { if (newFilename.length > 0) { currentGeminiMdFilename = newFilename.map((name) => name.trim()); } } else if (newFilename && newFilename.trim() !== '') { currentGeminiMdFilename = newFilename.trim(); } } export function getCurrentGeminiMdFilename(): string { if (Array.isArray(currentGeminiMdFilename)) { return currentGeminiMdFilename[0]; } return currentGeminiMdFilename; } export function getAllGeminiMdFilenames(): string[] { if (Array.isArray(currentGeminiMdFilename)) { return currentGeminiMdFilename; } return [currentGeminiMdFilename]; } interface SaveMemoryParams { fact: string; modified_by_user?: boolean; modified_content?: string; } function getGlobalMemoryFilePath(): string { return path.join(homedir(), GEMINI_CONFIG_DIR, getCurrentGeminiMdFilename()); } /** * Ensures proper newline separation before appending content. */ function ensureNewlineSeparation(currentContent: string): string { if (currentContent.length === 0) return ''; if (currentContent.endsWith('\n\n') || currentContent.endsWith('\r\n\r\n')) return ''; if (currentContent.endsWith('\n') || currentContent.endsWith('\r\n')) return '\n'; return '\n\n'; } export class MemoryTool extends BaseTool implements ModifiableTool { private static readonly allowlist: Set = new Set(); static readonly Name: string = memoryToolSchemaData.name!; constructor() { super( MemoryTool.Name, 'Save Memory', memoryToolDescription, Icon.LightBulb, memoryToolSchemaData.parameters as Record, ); } getDescription(_params: SaveMemoryParams): string { const memoryFilePath = getGlobalMemoryFilePath(); return `in ${tildeifyPath(memoryFilePath)}`; } /** * Reads the current content of the memory file */ private async readMemoryFileContent(): Promise { try { return await fs.readFile(getGlobalMemoryFilePath(), 'utf-8'); } catch (err) { const error = err as Error & { code?: string }; if (!(error instanceof Error) || error.code !== 'ENOENT') throw err; return ''; } } /** * Computes the new content that would result from adding a memory entry */ private computeNewContent(currentContent: string, fact: string): string { let processedText = fact.trim(); processedText = processedText.replace(/^(-+\s*)+/, '').trim(); const newMemoryItem = `- ${processedText}`; const headerIndex = currentContent.indexOf(MEMORY_SECTION_HEADER); if (headerIndex === -1) { // Header not found, append header and then the entry const separator = ensureNewlineSeparation(currentContent); return ( currentContent + `${separator}${MEMORY_SECTION_HEADER}\n${newMemoryItem}\n` ); } else { // Header found, find where to insert the new memory entry const startOfSectionContent = headerIndex + MEMORY_SECTION_HEADER.length; let endOfSectionIndex = currentContent.indexOf( '\n## ', startOfSectionContent, ); if (endOfSectionIndex === -1) { endOfSectionIndex = currentContent.length; // End of file } const beforeSectionMarker = currentContent .substring(0, startOfSectionContent) .trimEnd(); let sectionContent = currentContent .substring(startOfSectionContent, endOfSectionIndex) .trimEnd(); const afterSectionMarker = currentContent.substring(endOfSectionIndex); sectionContent += `\n${newMemoryItem}`; return ( `${beforeSectionMarker}\n${sectionContent.trimStart()}\n${afterSectionMarker}`.trimEnd() + '\n' ); } } async shouldConfirmExecute( params: SaveMemoryParams, _abortSignal: AbortSignal, ): Promise { const memoryFilePath = getGlobalMemoryFilePath(); const allowlistKey = memoryFilePath; if (MemoryTool.allowlist.has(allowlistKey)) { return false; } // Read current content of the memory file const currentContent = await this.readMemoryFileContent(); // Calculate the new content that will be written to the memory file const newContent = this.computeNewContent(currentContent, params.fact); const fileName = path.basename(memoryFilePath); const fileDiff = Diff.createPatch( fileName, currentContent, newContent, 'Current', 'Proposed', DEFAULT_DIFF_OPTIONS, ); const confirmationDetails: ToolEditConfirmationDetails = { type: 'edit', title: `Confirm Memory Save: ${tildeifyPath(memoryFilePath)}`, fileName: memoryFilePath, fileDiff, originalContent: currentContent, newContent, onConfirm: async (outcome: ToolConfirmationOutcome) => { if (outcome === ToolConfirmationOutcome.ProceedAlways) { MemoryTool.allowlist.add(allowlistKey); } }, }; return confirmationDetails; } static async performAddMemoryEntry( text: string, memoryFilePath: string, fsAdapter: { readFile: (path: string, encoding: 'utf-8') => Promise; writeFile: ( path: string, data: string, encoding: 'utf-8', ) => Promise; mkdir: ( path: string, options: { recursive: boolean }, ) => Promise; }, ): Promise { let processedText = text.trim(); // Remove leading hyphens and spaces that might be misinterpreted as markdown list items processedText = processedText.replace(/^(-+\s*)+/, '').trim(); const newMemoryItem = `- ${processedText}`; try { await fsAdapter.mkdir(path.dirname(memoryFilePath), { recursive: true }); let content = ''; try { content = await fsAdapter.readFile(memoryFilePath, 'utf-8'); } catch (_e) { // File doesn't exist, will be created with header and item. } const headerIndex = content.indexOf(MEMORY_SECTION_HEADER); if (headerIndex === -1) { // Header not found, append header and then the entry const separator = ensureNewlineSeparation(content); content += `${separator}${MEMORY_SECTION_HEADER}\n${newMemoryItem}\n`; } else { // Header found, find where to insert the new memory entry const startOfSectionContent = headerIndex + MEMORY_SECTION_HEADER.length; let endOfSectionIndex = content.indexOf('\n## ', startOfSectionContent); if (endOfSectionIndex === -1) { endOfSectionIndex = content.length; // End of file } const beforeSectionMarker = content .substring(0, startOfSectionContent) .trimEnd(); let sectionContent = content .substring(startOfSectionContent, endOfSectionIndex) .trimEnd(); const afterSectionMarker = content.substring(endOfSectionIndex); sectionContent += `\n${newMemoryItem}`; content = `${beforeSectionMarker}\n${sectionContent.trimStart()}\n${afterSectionMarker}`.trimEnd() + '\n'; } await fsAdapter.writeFile(memoryFilePath, content, 'utf-8'); } catch (error) { console.error( `[MemoryTool] Error adding memory entry to ${memoryFilePath}:`, error, ); throw new Error( `[MemoryTool] Failed to add memory entry: ${error instanceof Error ? error.message : String(error)}`, ); } } async execute( params: SaveMemoryParams, _signal: AbortSignal, ): Promise { const { fact, modified_by_user, modified_content } = params; if (!fact || typeof fact !== 'string' || fact.trim() === '') { const errorMessage = 'Parameter "fact" must be a non-empty string.'; return { llmContent: JSON.stringify({ success: false, error: errorMessage }), returnDisplay: `Error: ${errorMessage}`, }; } try { if (modified_by_user && modified_content !== undefined) { // User modified the content in external editor, write it directly await fs.mkdir(path.dirname(getGlobalMemoryFilePath()), { recursive: true, }); await fs.writeFile( getGlobalMemoryFilePath(), modified_content, 'utf-8', ); const successMessage = `Okay, I've updated the memory file with your modifications.`; return { llmContent: JSON.stringify({ success: true, message: successMessage, }), returnDisplay: successMessage, }; } else { // Use the normal memory entry logic await MemoryTool.performAddMemoryEntry( fact, getGlobalMemoryFilePath(), { readFile: fs.readFile, writeFile: fs.writeFile, mkdir: fs.mkdir, }, ); const successMessage = `Okay, I've remembered that: "${fact}"`; return { llmContent: JSON.stringify({ success: true, message: successMessage, }), returnDisplay: successMessage, }; } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.error( `[MemoryTool] Error executing save_memory for fact "${fact}": ${errorMessage}`, ); return { llmContent: JSON.stringify({ success: false, error: `Failed to save memory. Detail: ${errorMessage}`, }), returnDisplay: `Error saving memory: ${errorMessage}`, }; } } getModifyContext(_abortSignal: AbortSignal): ModifyContext { return { getFilePath: (_params: SaveMemoryParams) => getGlobalMemoryFilePath(), getCurrentContent: async (_params: SaveMemoryParams): Promise => this.readMemoryFileContent(), getProposedContent: async (params: SaveMemoryParams): Promise => { const currentContent = await this.readMemoryFileContent(); return this.computeNewContent(currentContent, params.fact); }, createUpdatedParams: ( _oldContent: string, modifiedProposedContent: string, originalParams: SaveMemoryParams, ): SaveMemoryParams => ({ ...originalParams, modified_by_user: true, modified_content: modifiedProposedContent, }), }; } }