2026-01-12 11:22:56 +00:00
import { Type } from "@sinclair/typebox" ;
2026-01-27 21:57:15 -08:00
import type { MoltbotConfig } from "../../config/config.js" ;
import type { MemoryCitationsMode } from "../../config/types.memory.js" ;
2026-02-02 20:45:58 -08:00
import type { MemorySearchResult } from "../../memory/types.js" ;
import type { AnyAgentTool } from "./common.js" ;
2026-02-02 20:48:20 +01:00
import { resolveMemoryBackendConfig } from "../../memory/backend-config.js" ;
2026-01-12 11:22:56 +00:00
import { getMemorySearchManager } from "../../memory/index.js" ;
2026-02-02 20:10:45 -08:00
import { parseAgentSessionKey } from "../../routing/session-key.js" ;
2026-01-12 11:22:56 +00:00
import { resolveSessionAgentId } from "../agent-scope.js" ;
import { resolveMemorySearchConfig } from "../memory-search.js" ;
import { jsonResult , readNumberParam , readStringParam } from "./common.js" ;
const MemorySearchSchema = Type . Object ( {
query : Type.String ( ) ,
maxResults : Type.Optional ( Type . Number ( ) ) ,
minScore : Type.Optional ( Type . Number ( ) ) ,
} ) ;
const MemoryGetSchema = Type . Object ( {
path : Type.String ( ) ,
from : Type . Optional ( Type . Number ( ) ) ,
lines : Type.Optional ( Type . Number ( ) ) ,
} ) ;
export function createMemorySearchTool ( options : {
2026-01-27 21:57:15 -08:00
config? : MoltbotConfig ;
2026-01-12 11:22:56 +00:00
agentSessionKey? : string ;
} ) : AnyAgentTool | null {
const cfg = options . config ;
2026-01-27 21:57:15 -08:00
if ( ! cfg ) return null ;
2026-01-12 11:22:56 +00:00
const agentId = resolveSessionAgentId ( {
sessionKey : options.agentSessionKey ,
config : cfg ,
} ) ;
2026-01-27 21:57:15 -08:00
if ( ! resolveMemorySearchConfig ( cfg , agentId ) ) return null ;
2026-01-12 11:22:56 +00:00
return {
label : "Memory Search" ,
name : "memory_search" ,
description :
2026-01-17 18:53:48 +00:00
"Mandatory recall step: semantically search MEMORY.md + memory/*.md (and optional session transcripts) before answering questions about prior work, decisions, dates, people, preferences, or todos; returns top snippets with path + lines." ,
2026-01-12 11:22:56 +00:00
parameters : MemorySearchSchema ,
execute : async ( _toolCallId , params ) = > {
const query = readStringParam ( params , "query" , { required : true } ) ;
const maxResults = readNumberParam ( params , "maxResults" ) ;
const minScore = readNumberParam ( params , "minScore" ) ;
const { manager , error } = await getMemorySearchManager ( {
cfg ,
agentId ,
} ) ;
if ( ! manager ) {
return jsonResult ( { results : [ ] , disabled : true , error } ) ;
}
2026-01-17 09:45:45 +00:00
try {
2026-01-27 21:57:15 -08:00
const citationsMode = resolveMemoryCitationsMode ( cfg ) ;
2026-01-28 02:05:58 -08:00
const includeCitations = shouldIncludeCitations ( {
mode : citationsMode ,
sessionKey : options.agentSessionKey ,
} ) ;
2026-01-27 21:57:15 -08:00
const rawResults = await manager . search ( query , {
2026-01-17 09:45:45 +00:00
maxResults ,
minScore ,
sessionKey : options.agentSessionKey ,
} ) ;
const status = manager . status ( ) ;
2026-02-02 20:48:20 +01:00
const decorated = decorateCitations ( rawResults , includeCitations ) ;
const resolved = resolveMemoryBackendConfig ( { cfg , agentId } ) ;
const results =
status . backend === "qmd"
? clampResultsByInjectedChars ( decorated , resolved . qmd ? . limits . maxInjectedChars )
: decorated ;
2026-01-17 09:45:45 +00:00
return jsonResult ( {
results ,
provider : status.provider ,
model : status.model ,
fallback : status.fallback ,
2026-01-27 21:57:15 -08:00
citations : citationsMode ,
2026-01-17 09:45:45 +00:00
} ) ;
} catch ( err ) {
const message = err instanceof Error ? err.message : String ( err ) ;
return jsonResult ( { results : [ ] , disabled : true , error : message } ) ;
}
2026-01-12 11:22:56 +00:00
} ,
} ;
}
export function createMemoryGetTool ( options : {
2026-01-27 21:57:15 -08:00
config? : MoltbotConfig ;
2026-01-12 11:22:56 +00:00
agentSessionKey? : string ;
} ) : AnyAgentTool | null {
const cfg = options . config ;
2026-01-27 21:57:15 -08:00
if ( ! cfg ) return null ;
2026-01-12 11:22:56 +00:00
const agentId = resolveSessionAgentId ( {
sessionKey : options.agentSessionKey ,
config : cfg ,
} ) ;
2026-01-27 21:57:15 -08:00
if ( ! resolveMemorySearchConfig ( cfg , agentId ) ) return null ;
2026-01-12 11:22:56 +00:00
return {
label : "Memory Get" ,
name : "memory_get" ,
2026-01-12 23:29:44 +00:00
description :
2026-01-27 21:57:15 -08:00
"Safe snippet read from MEMORY.md or memory/*.md with optional from/lines; use after memory_search to pull only the needed lines and keep context small." ,
2026-01-12 11:22:56 +00:00
parameters : MemoryGetSchema ,
execute : async ( _toolCallId , params ) = > {
const relPath = readStringParam ( params , "path" , { required : true } ) ;
const from = readNumberParam ( params , "from" , { integer : true } ) ;
const lines = readNumberParam ( params , "lines" , { integer : true } ) ;
const { manager , error } = await getMemorySearchManager ( {
cfg ,
agentId ,
} ) ;
if ( ! manager ) {
return jsonResult ( { path : relPath , text : "" , disabled : true , error } ) ;
}
2026-01-17 09:45:45 +00:00
try {
const result = await manager . readFile ( {
relPath ,
from : from ? ? undefined ,
lines : lines ? ? undefined ,
} ) ;
return jsonResult ( result ) ;
} catch ( err ) {
const message = err instanceof Error ? err.message : String ( err ) ;
return jsonResult ( { path : relPath , text : "" , disabled : true , error : message } ) ;
}
2026-01-12 11:22:56 +00:00
} ,
} ;
}
2026-01-27 21:57:15 -08:00
function resolveMemoryCitationsMode ( cfg : MoltbotConfig ) : MemoryCitationsMode {
const mode = cfg . memory ? . citations ;
2026-02-02 22:56:20 +01:00
if ( mode === "on" || mode === "off" || mode === "auto" ) {
return mode ;
}
2026-01-27 21:57:15 -08:00
return "auto" ;
}
function decorateCitations ( results : MemorySearchResult [ ] , include : boolean ) : MemorySearchResult [ ] {
if ( ! include ) {
return results . map ( ( entry ) = > ( { . . . entry , citation : undefined } ) ) ;
}
return results . map ( ( entry ) = > {
const citation = formatCitation ( entry ) ;
const snippet = ` ${ entry . snippet . trim ( ) } \ n \ nSource: ${ citation } ` ;
return { . . . entry , citation , snippet } ;
} ) ;
}
function formatCitation ( entry : MemorySearchResult ) : string {
const lineRange =
entry . startLine === entry . endLine
? ` #L ${ entry . startLine } `
: ` #L ${ entry . startLine } -L ${ entry . endLine } ` ;
return ` ${ entry . path } ${ lineRange } ` ;
}
2026-01-28 02:05:58 -08:00
2026-02-02 20:48:20 +01:00
function clampResultsByInjectedChars (
results : MemorySearchResult [ ] ,
budget? : number ,
) : MemorySearchResult [ ] {
2026-02-02 22:56:20 +01:00
if ( ! budget || budget <= 0 ) {
return results ;
}
2026-02-02 20:48:20 +01:00
let remaining = budget ;
const clamped : MemorySearchResult [ ] = [ ] ;
for ( const entry of results ) {
2026-02-02 22:56:20 +01:00
if ( remaining <= 0 ) {
break ;
}
2026-02-02 20:48:20 +01:00
const snippet = entry . snippet ? ? "" ;
if ( snippet . length <= remaining ) {
clamped . push ( entry ) ;
remaining -= snippet . length ;
} else {
const trimmed = snippet . slice ( 0 , Math . max ( 0 , remaining ) ) ;
clamped . push ( { . . . entry , snippet : trimmed } ) ;
break ;
}
}
return clamped ;
}
2026-01-28 02:05:58 -08:00
function shouldIncludeCitations ( params : {
mode : MemoryCitationsMode ;
sessionKey? : string ;
} ) : boolean {
2026-02-02 22:56:20 +01:00
if ( params . mode === "on" ) {
return true ;
}
if ( params . mode === "off" ) {
return false ;
}
2026-01-28 02:05:58 -08:00
// auto: show citations in direct chats; suppress in groups/channels by default.
const chatType = deriveChatTypeFromSessionKey ( params . sessionKey ) ;
return chatType === "direct" ;
}
function deriveChatTypeFromSessionKey ( sessionKey? : string ) : "direct" | "group" | "channel" {
2026-02-02 20:10:45 -08:00
const parsed = parseAgentSessionKey ( sessionKey ) ;
if ( ! parsed ? . rest ) {
2026-02-02 22:56:20 +01:00
return "direct" ;
}
2026-02-02 20:45:58 -08:00
const tokens = parsed . rest . toLowerCase ( ) . split ( ":" ) . filter ( Boolean ) ;
2026-02-02 20:10:45 -08:00
if ( tokens . includes ( "channel" ) ) {
2026-02-02 22:56:20 +01:00
return "channel" ;
}
2026-02-02 20:10:45 -08:00
if ( tokens . includes ( "group" ) ) {
return "group" ;
}
2026-01-28 02:05:58 -08:00
return "direct" ;
}