|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import { BaseTool, ToolResult } from './tools.js'; |
|
|
import * as fs from 'fs/promises'; |
|
|
import * as path from 'path'; |
|
|
import { homedir } from 'os'; |
|
|
|
|
|
const memoryToolSchemaData = { |
|
|
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: '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 DEFAULT_CONTEXT_FILENAME = 'GEMINI.md'; |
|
|
export const MEMORY_SECTION_HEADER = '## Gemini Added Memories'; |
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
function getGlobalMemoryFilePath(): string { |
|
|
return path.join(homedir(), GEMINI_CONFIG_DIR, getCurrentGeminiMdFilename()); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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(); |
|
|
|
|
|
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) { |
|
|
|
|
|
} |
|
|
|
|
|
const headerIndex = content.indexOf(MEMORY_SECTION_HEADER); |
|
|
|
|
|
if (headerIndex === -1) { |
|
|
|
|
|
const separator = ensureNewlineSeparation(content); |
|
|
content += `${separator}${MEMORY_SECTION_HEADER}\n${newMemoryItem}\n`; |
|
|
} else { |
|
|
|
|
|
const startOfSectionContent = |
|
|
headerIndex + MEMORY_SECTION_HEADER.length; |
|
|
let endOfSectionIndex = content.indexOf('\n## ', startOfSectionContent); |
|
|
if (endOfSectionIndex === -1) { |
|
|
endOfSectionIndex = content.length; |
|
|
} |
|
|
|
|
|
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 { |
|
|
|
|
|
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}`, |
|
|
}; |
|
|
} |
|
|
} |
|
|
} |
|
|
|