2025-05-16 16:36:50 -07:00
/ * *
* @license
* Copyright 2025 Google LLC
* SPDX - License - Identifier : Apache - 2.0
* /
2025-07-30 15:21:31 -07:00
import {
BaseTool ,
ToolResult ,
ToolEditConfirmationDetails ,
ToolConfirmationOutcome ,
Icon ,
} from './tools.js' ;
2025-07-07 23:48:44 -07:00
import { FunctionDeclaration , Type } from '@google/genai' ;
2025-05-16 16:36:50 -07:00
import * as fs from 'fs/promises' ;
import * as path from 'path' ;
import { homedir } from 'os' ;
2025-07-30 15:21:31 -07:00
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' ;
2025-05-16 16:36:50 -07:00
2025-07-07 23:48:44 -07:00
const memoryToolSchemaData : FunctionDeclaration = {
2025-05-17 19:45:16 -07:00
name : 'save_memory' ,
2025-05-16 16:36:50 -07:00
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 : {
2025-07-07 23:48:44 -07:00
type : Type . OBJECT ,
2025-05-16 16:36:50 -07:00
properties : {
fact : {
2025-07-07 23:48:44 -07:00
type : Type . STRING ,
2025-05-16 16:36:50 -07:00
description :
'The specific fact or piece of information to remember. Should be a clear, self-contained statement.' ,
} ,
2025-08-18 22:34:08 +08:00
scope : {
type : Type . STRING ,
description :
'Where to save the memory: "global" saves to user-level ~/.qwen/QWEN.md (shared across all projects), "project" saves to current project\'s QWEN.md (project-specific). If not specified, will prompt user to choose.' ,
enum : [ 'global' , 'project' ] ,
} ,
2025-05-16 16:36:50 -07:00
} ,
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".
2025-08-18 22:34:08 +08:00
- \ ` scope \` (string, optional): Where to save the memory:
- "global" : Saves to user - level ~ / . q w e n / Q W E N . m d ( s h a r e d a c r o s s a l l p r o j e c t s )
- "project" : Saves to current project ' s QWEN . md ( project - specific )
- If not specified , the tool will ask the user where they want to save the memory .
2025-05-16 16:36:50 -07:00
` ;
2025-08-03 15:12:18 +08:00
export const GEMINI_CONFIG_DIR = '.qwen' ;
export const DEFAULT_CONTEXT_FILENAME = 'QWEN.md' ;
export const MEMORY_SECTION_HEADER = '## Qwen Added Memories' ;
2025-05-16 16:36:50 -07:00
2025-08-09 10:33:02 +08:00
// This variable will hold the currently configured filename for QWEN.md context files.
2025-05-31 12:49:28 -07:00
// It defaults to DEFAULT_CONTEXT_FILENAME but can be overridden by setGeminiMdFilename.
2025-06-13 09:19:08 -07:00
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 ( ) !== '' ) {
2025-05-31 12:49:28 -07:00
currentGeminiMdFilename = newFilename . trim ( ) ;
}
}
export function getCurrentGeminiMdFilename ( ) : string {
2025-06-13 09:19:08 -07:00
if ( Array . isArray ( currentGeminiMdFilename ) ) {
return currentGeminiMdFilename [ 0 ] ;
}
2025-05-31 12:49:28 -07:00
return currentGeminiMdFilename ;
}
2025-06-13 09:19:08 -07:00
export function getAllGeminiMdFilenames ( ) : string [ ] {
if ( Array . isArray ( currentGeminiMdFilename ) ) {
return currentGeminiMdFilename ;
}
return [ currentGeminiMdFilename ] ;
}
2025-05-16 16:36:50 -07:00
interface SaveMemoryParams {
fact : string ;
2025-07-30 15:21:31 -07:00
modified_by_user? : boolean ;
modified_content? : string ;
2025-08-18 22:34:08 +08:00
scope ? : 'global' | 'project' ;
2025-05-16 16:36:50 -07:00
}
function getGlobalMemoryFilePath ( ) : string {
2025-05-31 12:49:28 -07:00
return path . join ( homedir ( ) , GEMINI_CONFIG_DIR , getCurrentGeminiMdFilename ( ) ) ;
2025-05-16 16:36:50 -07:00
}
2025-08-18 22:34:08 +08:00
function getProjectMemoryFilePath ( ) : string {
return path . join ( process . cwd ( ) , getCurrentGeminiMdFilename ( ) ) ;
}
function getMemoryFilePath ( scope : 'global' | 'project' = 'global' ) : string {
return scope === 'project'
? getProjectMemoryFilePath ( )
: getGlobalMemoryFilePath ( ) ;
}
2025-05-16 16:36:50 -07:00
/ * *
* 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' ;
}
2025-07-30 15:21:31 -07:00
export class MemoryTool
extends BaseTool < SaveMemoryParams , ToolResult >
implements ModifiableTool < SaveMemoryParams >
{
private static readonly allowlist : Set < string > = new Set ( ) ;
2025-07-07 23:48:44 -07:00
static readonly Name : string = memoryToolSchemaData . name ! ;
2025-05-16 16:36:50 -07:00
constructor ( ) {
super (
MemoryTool . Name ,
'Save Memory' ,
memoryToolDescription ,
2025-07-17 16:25:23 -06:00
Icon . LightBulb ,
2025-05-16 16:36:50 -07:00
memoryToolSchemaData . parameters as Record < string , unknown > ,
) ;
}
2025-08-18 22:34:08 +08:00
getDescription ( params : SaveMemoryParams ) : string {
const scope = params . scope || 'global' ;
const memoryFilePath = getMemoryFilePath ( scope ) ;
return ` in ${ tildeifyPath ( memoryFilePath ) } ( ${ scope } ) ` ;
2025-07-30 15:21:31 -07:00
}
/ * *
* Reads the current content of the memory file
* /
2025-08-18 22:34:08 +08:00
private async readMemoryFileContent (
scope : 'global' | 'project' = 'global' ,
) : Promise < string > {
2025-07-30 15:21:31 -07:00
try {
2025-08-18 22:34:08 +08:00
return await fs . readFile ( getMemoryFilePath ( scope ) , 'utf-8' ) ;
2025-07-30 15:21:31 -07:00
} 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 < ToolEditConfirmationDetails | false > {
2025-08-18 22:34:08 +08:00
// If scope is not specified, prompt the user to choose
if ( ! params . scope ) {
const globalPath = tildeifyPath ( getMemoryFilePath ( 'global' ) ) ;
const projectPath = tildeifyPath ( getMemoryFilePath ( 'project' ) ) ;
const confirmationDetails : ToolEditConfirmationDetails = {
type : 'edit' ,
title : ` Choose Memory Storage Location ` ,
fileName : 'Memory Storage Options' ,
fileDiff : ` Choose where to save this memory: \ n \ n" ${ params . fact } " \ n \ nOptions: \ n- Global: ${ globalPath } (shared across all projects) \ n- Project: ${ projectPath } (current project only) \ n \ nPlease specify the scope parameter: "global" or "project" ` ,
originalContent : '' ,
newContent : ` Memory to save: ${ params . fact } \ n \ nScope options: \ n- global: ${ globalPath } \ n- project: ${ projectPath } ` ,
onConfirm : async ( _outcome : ToolConfirmationOutcome ) = > {
// This will be handled by the execution flow
} ,
} ;
return confirmationDetails ;
}
const scope = params . scope ;
const memoryFilePath = getMemoryFilePath ( scope ) ;
const allowlistKey = ` ${ memoryFilePath } _ ${ scope } ` ;
2025-07-30 15:21:31 -07:00
if ( MemoryTool . allowlist . has ( allowlistKey ) ) {
return false ;
}
// Read current content of the memory file
2025-08-18 22:34:08 +08:00
const currentContent = await this . readMemoryFileContent ( scope ) ;
2025-07-30 15:21:31 -07:00
// 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' ,
2025-08-18 22:34:08 +08:00
title : ` Confirm Memory Save: ${ tildeifyPath ( memoryFilePath ) } ( ${ scope } ) ` ,
2025-07-30 15:21:31 -07:00
fileName : memoryFilePath ,
fileDiff ,
originalContent : currentContent ,
newContent ,
onConfirm : async ( outcome : ToolConfirmationOutcome ) = > {
if ( outcome === ToolConfirmationOutcome . ProceedAlways ) {
MemoryTool . allowlist . add ( allowlistKey ) ;
}
} ,
} ;
return confirmationDetails ;
}
2025-05-16 16:36:50 -07:00
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 > {
2025-07-30 15:21:31 -07:00
const { fact , modified_by_user , modified_content } = params ;
2025-05-16 16:36:50 -07:00
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 } ` ,
} ;
}
2025-08-18 22:34:08 +08:00
// If scope is not specified, prompt the user to choose
if ( ! params . scope ) {
const errorMessage =
'Please specify where to save this memory. Use scope parameter: "global" for user-level (~/.qwen/QWEN.md) or "project" for current project (./QWEN.md).' ;
return {
llmContent : JSON.stringify ( { success : false , error : errorMessage } ) ,
returnDisplay : ` ${ errorMessage } \ n \ nGlobal: ${ tildeifyPath ( getMemoryFilePath ( 'global' ) ) } \ nProject: ${ tildeifyPath ( getMemoryFilePath ( 'project' ) ) } ` ,
} ;
}
const scope = params . scope ;
const memoryFilePath = getMemoryFilePath ( scope ) ;
2025-05-16 16:36:50 -07:00
try {
2025-07-30 15:21:31 -07:00
if ( modified_by_user && modified_content !== undefined ) {
// User modified the content in external editor, write it directly
2025-08-18 22:34:08 +08:00
await fs . mkdir ( path . dirname ( memoryFilePath ) , {
2025-07-30 15:21:31 -07:00
recursive : true ,
} ) ;
2025-08-18 22:34:08 +08:00
await fs . writeFile ( memoryFilePath , modified_content , 'utf-8' ) ;
const successMessage = ` Okay, I've updated the ${ scope } memory file with your modifications. ` ;
2025-07-30 15:21:31 -07:00
return {
llmContent : JSON.stringify ( {
success : true ,
message : successMessage ,
} ) ,
returnDisplay : successMessage ,
} ;
} else {
// Use the normal memory entry logic
2025-08-18 22:34:08 +08:00
await MemoryTool . performAddMemoryEntry ( fact , memoryFilePath , {
readFile : fs.readFile ,
writeFile : fs.writeFile ,
mkdir : fs.mkdir ,
} ) ;
const successMessage = ` Okay, I've remembered that in ${ scope } memory: " ${ fact } " ` ;
2025-07-30 15:21:31 -07:00
return {
llmContent : JSON.stringify ( {
success : true ,
message : successMessage ,
} ) ,
returnDisplay : successMessage ,
} ;
}
2025-05-16 16:36:50 -07:00
} catch ( error ) {
const errorMessage =
error instanceof Error ? error.message : String ( error ) ;
console . error (
2025-08-18 22:34:08 +08:00
` [MemoryTool] Error executing save_memory for fact " ${ fact } " in ${ scope } : ${ errorMessage } ` ,
2025-05-16 16:36:50 -07:00
) ;
return {
llmContent : JSON.stringify ( {
success : false ,
error : ` Failed to save memory. Detail: ${ errorMessage } ` ,
} ) ,
returnDisplay : ` Error saving memory: ${ errorMessage } ` ,
} ;
}
}
2025-07-30 15:21:31 -07:00
getModifyContext ( _abortSignal : AbortSignal ) : ModifyContext < SaveMemoryParams > {
return {
2025-08-18 22:34:08 +08:00
getFilePath : ( params : SaveMemoryParams ) = >
getMemoryFilePath ( params . scope || 'global' ) ,
getCurrentContent : async ( params : SaveMemoryParams ) : Promise < string > = >
this . readMemoryFileContent ( params . scope || 'global' ) ,
2025-07-30 15:21:31 -07:00
getProposedContent : async ( params : SaveMemoryParams ) : Promise < string > = > {
2025-08-18 22:34:08 +08:00
const scope = params . scope || 'global' ;
const currentContent = await this . readMemoryFileContent ( scope ) ;
2025-07-30 15:21:31 -07:00
return this . computeNewContent ( currentContent , params . fact ) ;
} ,
createUpdatedParams : (
_oldContent : string ,
modifiedProposedContent : string ,
originalParams : SaveMemoryParams ,
) : SaveMemoryParams = > ( {
. . . originalParams ,
modified_by_user : true ,
modified_content : modifiedProposedContent ,
} ) ,
} ;
}
2025-05-16 16:36:50 -07:00
}