summaryrefslogtreecommitdiff
path: root/packages/server/src/tools/memoryTool.ts
diff options
context:
space:
mode:
authorAllen Hutchison <[email protected]>2025-05-16 16:36:50 -0700
committerGitHub <[email protected]>2025-05-16 16:36:50 -0700
commit1bdec55fe1c658069a45df0aa8e4923ba1954e41 (patch)
tree32aa334cb0590f06830f52ed7c0d84e2d4ed7db3 /packages/server/src/tools/memoryTool.ts
parentd9bd2b0e144560c8a82806bfb021a028c7cd43c9 (diff)
feat: Implement CLI and model memory management (#371)
Co-authored-by: N. Taylor Mullen <[email protected]>
Diffstat (limited to 'packages/server/src/tools/memoryTool.ts')
-rw-r--r--packages/server/src/tools/memoryTool.ts194
1 files changed, 194 insertions, 0 deletions
diff --git a/packages/server/src/tools/memoryTool.ts b/packages/server/src/tools/memoryTool.ts
new file mode 100644
index 00000000..177072fe
--- /dev/null
+++ b/packages/server/src/tools/memoryTool.ts
@@ -0,0 +1,194 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { BaseTool, ToolResult } from './tools.js';
+import * as fs from 'fs/promises';
+import * as path from 'path';
+import { homedir } from 'os';
+
+const memoryToolSchemaData = {
+ name: 'saveMemory',
+ 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: 'object',
+ properties: {
+ fact: {
+ 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 GEMINI_MD_FILENAME = 'GEMINI.md';
+export const MEMORY_SECTION_HEADER = '## Gemini Added Memories';
+
+interface SaveMemoryParams {
+ fact: string;
+}
+
+function getGlobalMemoryFilePath(): string {
+ return path.join(homedir(), GEMINI_CONFIG_DIR, GEMINI_MD_FILENAME);
+}
+
+/**
+ * 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<SaveMemoryParams, ToolResult> {
+ static readonly Name: string = memoryToolSchemaData.name;
+ constructor() {
+ super(
+ MemoryTool.Name,
+ 'Save Memory',
+ memoryToolDescription,
+ memoryToolSchemaData.parameters as Record<string, unknown>,
+ );
+ }
+
+ static async performAddMemoryEntry(
+ text: string,
+ memoryFilePath: string,
+ fsAdapter: {
+ readFile: (path: string, encoding: 'utf-8') => Promise<string>;
+ writeFile: (
+ path: string,
+ data: string,
+ encoding: 'utf-8',
+ ) => Promise<void>;
+ mkdir: (
+ path: string,
+ options: { recursive: boolean },
+ ) => Promise<string | undefined>;
+ },
+ ): Promise<void> {
+ 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<ToolResult> {
+ const { fact } = 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 {
+ // Use the static method with actual fs promises
+ 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 saveMemory for fact "${fact}": ${errorMessage}`,
+ );
+ return {
+ llmContent: JSON.stringify({
+ success: false,
+ error: `Failed to save memory. Detail: ${errorMessage}`,
+ }),
+ returnDisplay: `Error saving memory: ${errorMessage}`,
+ };
+ }
+ }
+}