2026-01-12 11:22:56 +00:00
import { Type } from "@sinclair/typebox" ;
2026-02-02 21:10:10 -08:00
import type { OpenClawConfig } from "../../config/config.js" ;
2026-01-27 21:57:15 -08:00
import type { MemoryCitationsMode } from "../../config/types.memory.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-18 01:34:35 +00:00
import type { MemorySearchResult } from "../../memory/types.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" ;
2026-02-18 01:34:35 +00:00
import type { AnyAgentTool } from "./common.js" ;
2026-01-12 11:22:56 +00:00
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 ( ) ) ,
} ) ;
2026-02-15 16:22:59 +00:00
function resolveMemoryToolContext ( options : { config? : OpenClawConfig ; agentSessionKey? : string } ) {
2026-01-12 11:22:56 +00:00
const cfg = options . config ;
2026-02-02 21:15:43 -08:00
if ( ! cfg ) {
return null ;
}
2026-01-12 11:22:56 +00:00
const agentId = resolveSessionAgentId ( {
sessionKey : options.agentSessionKey ,
config : cfg ,
} ) ;
2026-02-02 21:15:43 -08:00
if ( ! resolveMemorySearchConfig ( cfg , agentId ) ) {
return null ;
}
2026-02-15 16:22:59 +00:00
return { cfg , agentId } ;
}
2026-03-14 00:59:20 +00:00
async function getMemoryManagerContext ( params : { cfg : OpenClawConfig ; agentId : string } ) : Promise <
| {
manager : NonNullable < Awaited < ReturnType < typeof getMemorySearchManager > > [ "manager" ] > ;
}
| {
error : string | undefined ;
}
> {
const { manager , error } = await getMemorySearchManager ( {
cfg : params.cfg ,
agentId : params.agentId ,
} ) ;
return manager ? { manager } : { error } ;
}
function createMemoryTool ( params : {
options : {
config? : OpenClawConfig ;
agentSessionKey? : string ;
} ;
label : string ;
name : string ;
description : string ;
parameters : typeof MemorySearchSchema | typeof MemoryGetSchema ;
execute : ( ctx : { cfg : OpenClawConfig ; agentId : string } ) = > AnyAgentTool [ "execute" ] ;
2026-02-15 16:22:59 +00:00
} ) : AnyAgentTool | null {
2026-03-14 00:59:20 +00:00
const ctx = resolveMemoryToolContext ( params . options ) ;
2026-02-15 16:22:59 +00:00
if ( ! ctx ) {
return null ;
}
2026-01-12 11:22:56 +00:00
return {
2026-03-14 00:59:20 +00:00
label : params.label ,
name : params.name ,
description : params.description ,
parameters : params.parameters ,
execute : params.execute ( ctx ) ,
} ;
}
export function createMemorySearchTool ( options : {
config? : OpenClawConfig ;
agentSessionKey? : string ;
} ) : AnyAgentTool | null {
return createMemoryTool ( {
options ,
2026-01-12 11:22:56 +00:00
label : "Memory Search" ,
name : "memory_search" ,
description :
2026-02-20 20:30:52 -08: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. If response has disabled=true, memory retrieval is unavailable and should be surfaced to the user." ,
2026-01-12 11:22:56 +00:00
parameters : MemorySearchSchema ,
2026-03-14 00:59:20 +00:00
execute :
( { cfg , agentId } ) = >
async ( _toolCallId , params ) = > {
const query = readStringParam ( params , "query" , { required : true } ) ;
const maxResults = readNumberParam ( params , "maxResults" ) ;
const minScore = readNumberParam ( params , "minScore" ) ;
const memory = await getMemoryManagerContext ( { cfg , agentId } ) ;
if ( "error" in memory ) {
return jsonResult ( buildMemorySearchUnavailableResult ( memory . error ) ) ;
}
try {
const citationsMode = resolveMemoryCitationsMode ( cfg ) ;
const includeCitations = shouldIncludeCitations ( {
mode : citationsMode ,
sessionKey : options.agentSessionKey ,
} ) ;
const rawResults = await memory . manager . search ( query , {
maxResults ,
minScore ,
sessionKey : options.agentSessionKey ,
} ) ;
const status = memory . manager . status ( ) ;
const decorated = decorateCitations ( rawResults , includeCitations ) ;
const resolved = resolveMemoryBackendConfig ( { cfg , agentId } ) ;
const results =
status . backend === "qmd"
? clampResultsByInjectedChars ( decorated , resolved . qmd ? . limits . maxInjectedChars )
: decorated ;
const searchMode = ( status . custom as { searchMode? : string } | undefined ) ? . searchMode ;
return jsonResult ( {
results ,
provider : status.provider ,
model : status.model ,
fallback : status.fallback ,
citations : citationsMode ,
mode : searchMode ,
} ) ;
} catch ( err ) {
const message = err instanceof Error ? err.message : String ( err ) ;
return jsonResult ( buildMemorySearchUnavailableResult ( message ) ) ;
}
} ,
} ) ;
2026-01-12 11:22:56 +00:00
}
export function createMemoryGetTool ( options : {
2026-02-02 21:10:10 -08:00
config? : OpenClawConfig ;
2026-01-12 11:22:56 +00:00
agentSessionKey? : string ;
} ) : AnyAgentTool | null {
2026-03-14 00:59:20 +00:00
return createMemoryTool ( {
options ,
2026-01-12 11:22:56 +00:00
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 ,
2026-03-14 00:59:20 +00:00
execute :
( { cfg , agentId } ) = >
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 memory = await getMemoryManagerContext ( { cfg , agentId } ) ;
if ( "error" in memory ) {
return jsonResult ( { path : relPath , text : "" , disabled : true , error : memory.error } ) ;
}
try {
const result = await memory . 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
2026-02-02 21:10:10 -08:00
function resolveMemoryCitationsMode ( cfg : OpenClawConfig ) : MemoryCitationsMode {
2026-01-27 21:57:15 -08:00
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-02-20 20:30:52 -08:00
function buildMemorySearchUnavailableResult ( error : string | undefined ) {
const reason = ( error ? ? "memory search unavailable" ) . trim ( ) || "memory search unavailable" ;
const isQuotaError = /insufficient_quota|quota|429/ . test ( reason . toLowerCase ( ) ) ;
const warning = isQuotaError
? "Memory search is unavailable because the embedding provider quota is exhausted."
: "Memory search is unavailable due to an embedding/provider error." ;
const action = isQuotaError
? "Top up or switch embedding provider, then retry memory_search."
: "Check embedding provider configuration and retry memory_search." ;
return {
results : [ ] ,
disabled : true ,
unavailable : true ,
error : reason ,
warning ,
action ,
} ;
}
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 21:19:13 -08:00
const tokens = new Set ( parsed . rest . toLowerCase ( ) . split ( ":" ) . filter ( Boolean ) ) ;
2026-02-02 21:15:43 -08:00
if ( tokens . has ( "channel" ) ) {
2026-02-02 22:56:20 +01:00
return "channel" ;
}
2026-02-02 21:15:43 -08:00
if ( tokens . has ( "group" ) ) {
2026-02-02 20:10:45 -08:00
return "group" ;
}
2026-01-28 02:05:58 -08:00
return "direct" ;
}