2025-05-16 16:36:50 -07:00
/ * *
* @license
* Copyright 2025 Google LLC
* SPDX - License - Identifier : Apache - 2.0
* /
2025-08-26 00:04:53 +02:00
import type { ToolEditConfirmationDetails , ToolResult } from './tools.js' ;
2025-07-30 15:21:31 -07:00
import {
2025-08-13 11:57:37 -07:00
BaseDeclarativeTool ,
BaseToolInvocation ,
2025-08-13 12:58:26 -03:00
Kind ,
2025-07-30 15:21:31 -07:00
ToolConfirmationOutcome ,
} from './tools.js' ;
2025-08-25 22:11:27 +02:00
import * as fs from 'node:fs/promises' ;
import * as path from 'node:path' ;
2025-08-20 10:55:47 +09:00
import { Storage } from '../config/storage.js' ;
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' ;
2025-08-26 00:04:53 +02:00
import type {
ModifiableDeclarativeTool ,
ModifyContext ,
} from './modifiable-tool.js' ;
2025-08-21 14:40:18 -07:00
import { ToolErrorType } from './tool-error.js' ;
2025-10-19 20:53:53 -04:00
import { MEMORY_TOOL_NAME } from './tool-names.js' ;
2025-10-24 13:04:40 -07:00
import type { MessageBus } from '../confirmation-bus/message-bus.js' ;
2025-05-16 16:36:50 -07:00
const memoryToolDescription = `
2026-02-09 01:06:03 -08:00
Saves concise global user context ( preferences , facts ) for use across ALL workspaces .
2025-05-16 16:36:50 -07:00
2026-02-09 01:06:03 -08:00
# # # CRITICAL : GLOBAL CONTEXT ONLY
NEVER save workspace - specific context , local paths , or commands ( e . g . "The entry point is src/index.js" , "The test command is npm test" ) . These are local to the current workspace and must NOT be saved globally . EXCLUSIVELY for context relevant across ALL workspaces .
2025-05-16 16:36:50 -07:00
2026-02-09 01:06:03 -08:00
- Use for "Remember X" or clear personal facts .
- Do NOT use for session context . ` ;
2025-05-16 16:36:50 -07:00
2025-05-31 12:49:28 -07:00
export const DEFAULT_CONTEXT_FILENAME = 'GEMINI.md' ;
2025-05-16 16:36:50 -07:00
export const MEMORY_SECTION_HEADER = '## Gemini Added Memories' ;
2025-05-31 12:49:28 -07:00
// 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.
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-05-16 16:36:50 -07:00
}
2025-10-14 02:31:39 +09:00
export function getGlobalMemoryFilePath ( ) : string {
2025-08-20 10:55:47 +09:00
return path . join ( Storage . getGlobalGeminiDir ( ) , getCurrentGeminiMdFilename ( ) ) ;
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-08-13 11:57:37 -07:00
/ * *
* Reads the current content of the memory file
* /
async function readMemoryFileContent ( ) : Promise < string > {
try {
return await fs . readFile ( getGlobalMemoryFilePath ( ) , 'utf-8' ) ;
} catch ( err ) {
2026-02-10 00:10:15 +00:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
2025-08-13 11:57:37 -07:00
const error = err as Error & { code? : string } ;
if ( ! ( error instanceof Error ) || error . code !== 'ENOENT' ) throw err ;
return '' ;
2025-07-30 15:21:31 -07:00
}
2025-08-13 11:57:37 -07:00
}
2025-07-30 15:21:31 -07:00
2025-08-13 11:57:37 -07:00
/ * *
* Computes the new content that would result from adding a memory entry
* /
function computeNewContent ( currentContent : string , fact : string ) : string {
2026-02-05 10:07:47 -08:00
// Sanitize to prevent markdown injection by collapsing to a single line.
let processedText = fact . replace ( /[\r\n]/g , ' ' ) . trim ( ) ;
2025-08-13 11:57:37 -07:00
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
2025-07-30 15:21:31 -07:00
}
2025-08-13 11:57:37 -07:00
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'
) ;
}
}
2025-07-30 15:21:31 -07:00
2025-08-13 11:57:37 -07:00
class MemoryToolInvocation extends BaseToolInvocation <
SaveMemoryParams ,
ToolResult
> {
private static readonly allowlist : Set < string > = new Set ( ) ;
2026-02-05 10:07:47 -08:00
private proposedNewContent : string | undefined ;
2025-07-30 15:21:31 -07:00
2025-10-24 13:04:40 -07:00
constructor (
params : SaveMemoryParams ,
2026-01-04 17:11:43 -05:00
messageBus : MessageBus ,
2025-10-24 13:04:40 -07:00
toolName? : string ,
displayName? : string ,
) {
super ( params , messageBus , toolName , displayName ) ;
}
2025-08-13 11:57:37 -07:00
getDescription ( ) : string {
const memoryFilePath = getGlobalMemoryFilePath ( ) ;
return ` in ${ tildeifyPath ( memoryFilePath ) } ` ;
2025-07-30 15:21:31 -07:00
}
2025-10-24 13:04:40 -07:00
protected override async getConfirmationDetails (
2025-07-30 15:21:31 -07:00
_abortSignal : AbortSignal ,
) : Promise < ToolEditConfirmationDetails | false > {
const memoryFilePath = getGlobalMemoryFilePath ( ) ;
const allowlistKey = memoryFilePath ;
2025-08-13 11:57:37 -07:00
if ( MemoryToolInvocation . allowlist . has ( allowlistKey ) ) {
2025-07-30 15:21:31 -07:00
return false ;
}
2025-08-13 11:57:37 -07:00
const currentContent = await readMemoryFileContent ( ) ;
2026-02-05 10:07:47 -08:00
const { fact , modified_by_user , modified_content } = this . params ;
// If an attacker injects modified_content, use it for the diff
// to expose the attack to the user. Otherwise, compute from 'fact'.
const contentForDiff =
modified_by_user && modified_content !== undefined
? modified_content
: computeNewContent ( currentContent , fact ) ;
this . proposedNewContent = contentForDiff ;
2025-07-30 15:21:31 -07:00
const fileName = path . basename ( memoryFilePath ) ;
const fileDiff = Diff . createPatch (
fileName ,
currentContent ,
2026-02-05 10:07:47 -08:00
this . proposedNewContent ,
2025-07-30 15:21:31 -07:00
'Current' ,
'Proposed' ,
DEFAULT_DIFF_OPTIONS ,
) ;
const confirmationDetails : ToolEditConfirmationDetails = {
type : 'edit' ,
title : ` Confirm Memory Save: ${ tildeifyPath ( memoryFilePath ) } ` ,
fileName : memoryFilePath ,
2025-08-06 17:36:05 +00:00
filePath : memoryFilePath ,
2025-07-30 15:21:31 -07:00
fileDiff ,
originalContent : currentContent ,
2026-02-05 10:07:47 -08:00
newContent : this.proposedNewContent ,
2025-07-30 15:21:31 -07:00
onConfirm : async ( outcome : ToolConfirmationOutcome ) = > {
if ( outcome === ToolConfirmationOutcome . ProceedAlways ) {
2025-08-13 11:57:37 -07:00
MemoryToolInvocation . allowlist . add ( allowlistKey ) ;
2025-07-30 15:21:31 -07:00
}
2025-12-12 13:45:39 -08:00
await this . publishPolicyUpdate ( outcome ) ;
2025-07-30 15:21:31 -07:00
} ,
} ;
return confirmationDetails ;
}
2025-08-13 11:57:37 -07:00
async execute ( _signal : AbortSignal ) : Promise < ToolResult > {
const { fact , modified_by_user , modified_content } = this . params ;
try {
2026-02-05 10:07:47 -08:00
let contentToWrite : string ;
let successMessage : string ;
// Sanitize the fact for use in the success message, matching the sanitization
// that happened inside computeNewContent.
const sanitizedFact = fact . replace ( /[\r\n]/g , ' ' ) . trim ( ) ;
2025-08-13 11:57:37 -07:00
if ( modified_by_user && modified_content !== undefined ) {
2026-02-05 10:07:47 -08:00
// User modified the content, so that is the source of truth.
contentToWrite = modified_content ;
successMessage = ` Okay, I've updated the memory file with your modifications. ` ;
2025-08-13 11:57:37 -07:00
} else {
2026-02-05 10:07:47 -08:00
// User approved the proposed change without modification.
// The source of truth is the exact content proposed during confirmation.
if ( this . proposedNewContent === undefined ) {
// This case can be hit in flows without a confirmation step (e.g., --auto-confirm).
// As a fallback, we recompute the content now. This is safe because
// computeNewContent sanitizes the input.
const currentContent = await readMemoryFileContent ( ) ;
this . proposedNewContent = computeNewContent ( currentContent , fact ) ;
}
contentToWrite = this . proposedNewContent ;
successMessage = ` Okay, I've remembered that: " ${ sanitizedFact } " ` ;
2025-08-13 11:57:37 -07:00
}
2026-02-05 10:07:47 -08:00
await fs . mkdir ( path . dirname ( getGlobalMemoryFilePath ( ) ) , {
recursive : true ,
} ) ;
await fs . writeFile ( getGlobalMemoryFilePath ( ) , contentToWrite , 'utf-8' ) ;
return {
llmContent : JSON.stringify ( {
success : true ,
message : successMessage ,
} ) ,
returnDisplay : successMessage ,
} ;
2025-08-13 11:57:37 -07:00
} catch ( error ) {
const errorMessage =
error instanceof Error ? error.message : String ( error ) ;
return {
llmContent : JSON.stringify ( {
success : false ,
error : ` Failed to save memory. Detail: ${ errorMessage } ` ,
} ) ,
returnDisplay : ` Error saving memory: ${ errorMessage } ` ,
2025-08-21 14:40:18 -07:00
error : {
message : errorMessage ,
type : ToolErrorType . MEMORY_TOOL_EXECUTION_ERROR ,
} ,
2025-08-13 11:57:37 -07:00
} ;
}
}
}
export class MemoryTool
extends BaseDeclarativeTool < SaveMemoryParams , ToolResult >
implements ModifiableDeclarativeTool < SaveMemoryParams >
{
2025-10-20 22:35:35 -04:00
static readonly Name = MEMORY_TOOL_NAME ;
2026-01-04 17:11:43 -05:00
constructor ( messageBus : MessageBus ) {
2025-08-13 11:57:37 -07:00
super (
2025-10-20 22:35:35 -04:00
MemoryTool . Name ,
2025-11-11 20:28:13 -08:00
'SaveMemory' ,
2026-02-09 01:06:03 -08:00
memoryToolDescription +
' Examples: "Always lint after building", "Never run sudo commands", "Remember my address".' ,
2025-08-13 11:57:37 -07:00
Kind . Think ,
2026-02-09 01:06:03 -08:00
{
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' ] ,
additionalProperties : false ,
} ,
2026-01-04 17:11:43 -05:00
messageBus ,
2025-10-24 13:04:40 -07:00
true ,
false ,
2025-08-13 11:57:37 -07:00
) ;
}
2025-08-19 13:55:06 -07:00
protected override validateToolParamValues (
params : SaveMemoryParams ,
) : string | null {
2025-08-13 11:57:37 -07:00
if ( params . fact . trim ( ) === '' ) {
return 'Parameter "fact" must be a non-empty string.' ;
}
return null ;
}
2025-10-24 13:04:40 -07:00
protected createInvocation (
params : SaveMemoryParams ,
2026-01-04 17:11:43 -05:00
messageBus : MessageBus ,
2025-10-24 13:04:40 -07:00
toolName? : string ,
displayName? : string ,
) {
return new MemoryToolInvocation (
params ,
2026-01-04 17:11:43 -05:00
messageBus ,
2025-10-24 13:04:40 -07:00
toolName ? ? this . name ,
displayName ? ? this . displayName ,
) ;
2025-08-13 11:57:37 -07:00
}
2025-07-30 15:21:31 -07:00
getModifyContext ( _abortSignal : AbortSignal ) : ModifyContext < SaveMemoryParams > {
return {
getFilePath : ( _params : SaveMemoryParams ) = > getGlobalMemoryFilePath ( ) ,
getCurrentContent : async ( _params : SaveMemoryParams ) : Promise < string > = >
2025-08-13 11:57:37 -07:00
readMemoryFileContent ( ) ,
2025-07-30 15:21:31 -07:00
getProposedContent : async ( params : SaveMemoryParams ) : Promise < string > = > {
2025-08-13 11:57:37 -07:00
const currentContent = await readMemoryFileContent ( ) ;
2026-02-05 10:07:47 -08:00
const { fact , modified_by_user , modified_content } = params ;
// Ensure the editor is populated with the same content
// that the confirmation diff would show.
return modified_by_user && modified_content !== undefined
? modified_content
: computeNewContent ( currentContent , fact ) ;
2025-07-30 15:21:31 -07:00
} ,
createUpdatedParams : (
_oldContent : string ,
modifiedProposedContent : string ,
originalParams : SaveMemoryParams ,
) : SaveMemoryParams = > ( {
. . . originalParams ,
modified_by_user : true ,
modified_content : modifiedProposedContent ,
} ) ,
} ;
}
2025-05-16 16:36:50 -07:00
}