2026-03-25 00:52:41 -04:00
// tools/memory.rs — Native memory graph operations
//
2026-03-25 01:42:33 -04:00
// Direct library calls into the store — no subprocess spawning.
2026-03-25 00:52:41 -04:00
use anyhow ::{ Context , Result } ;
use serde_json ::json ;
2026-03-25 01:55:21 -04:00
use crate ::hippocampus ::memory ::MemoryNode ;
2026-03-25 00:52:41 -04:00
use crate ::agent ::types ::ToolDef ;
2026-03-25 01:55:21 -04:00
use crate ::store ::Store ;
2026-03-25 00:52:41 -04:00
pub fn definitions ( ) -> Vec < ToolDef > {
vec! [
2026-03-25 01:59:13 -04:00
ToolDef ::new ( " memory_render " ,
" Read a memory node's content and links. " ,
json! ( { " type " :" object " , " properties " :{ " key " :{ " type " :" string " , " description " :" Node key " } } , " required " :[ " key " ] } ) ) ,
ToolDef ::new ( " memory_write " ,
" Create or update a memory node. " ,
json! ( { " type " :" object " , " properties " :{ " key " :{ " type " :" string " , " description " :" Node key " } , " content " :{ " type " :" string " , " description " :" Full content (markdown) " } } , " required " :[ " key " , " content " ] } ) ) ,
ToolDef ::new ( " memory_search " ,
" Search the memory graph by keyword. " ,
json! ( { " type " :" object " , " properties " :{ " query " :{ " type " :" string " , " description " :" Search terms " } } , " required " :[ " query " ] } ) ) ,
ToolDef ::new ( " memory_links " ,
2026-03-25 01:42:33 -04:00
" Show a node's neighbors with link strengths. " ,
2026-03-25 01:59:13 -04:00
json! ( { " type " :" object " , " properties " :{ " key " :{ " type " :" string " , " description " :" Node key " } } , " required " :[ " key " ] } ) ) ,
ToolDef ::new ( " memory_link_set " ,
" Set link strength between two nodes. " ,
json! ( { " type " :" object " , " properties " :{ " source " :{ " type " :" string " } , " target " :{ " type " :" string " } , " strength " :{ " type " :" number " , " description " :" 0.01 to 1.0 " } } , " required " :[ " source " , " target " , " strength " ] } ) ) ,
ToolDef ::new ( " memory_link_add " ,
2026-03-25 00:52:41 -04:00
" Add a new link between two nodes. " ,
2026-03-25 01:59:13 -04:00
json! ( { " type " :" object " , " properties " :{ " source " :{ " type " :" string " } , " target " :{ " type " :" string " } } , " required " :[ " source " , " target " ] } ) ) ,
ToolDef ::new ( " memory_used " ,
" Mark a node as useful (boosts weight). " ,
json! ( { " type " :" object " , " properties " :{ " key " :{ " type " :" string " , " description " :" Node key " } } , " required " :[ " key " ] } ) ) ,
ToolDef ::new ( " memory_weight_set " ,
" Set a node's weight directly (0.01 to 1.0). " ,
json! ( { " type " :" object " , " properties " :{ " key " :{ " type " :" string " } , " weight " :{ " type " :" number " , " description " :" 0.01 to 1.0 " } } , " required " :[ " key " , " weight " ] } ) ) ,
ToolDef ::new ( " memory_supersede " ,
" Mark a node as superseded by another (sets weight to 0.01). " ,
json! ( { " type " :" object " , " properties " :{ " old_key " :{ " type " :" string " } , " new_key " :{ " type " :" string " } , " reason " :{ " type " :" string " } } , " required " :[ " old_key " , " new_key " ] } ) ) ,
2026-03-25 00:52:41 -04:00
]
}
2026-03-25 01:42:33 -04:00
/// Dispatch a memory tool call. Direct library calls, no subprocesses.
2026-03-25 00:52:41 -04:00
pub fn dispatch ( name : & str , args : & serde_json ::Value , provenance : Option < & str > ) -> Result < String > {
2026-03-25 01:42:33 -04:00
let prov = provenance . unwrap_or ( " manual " ) ;
2026-03-25 01:59:13 -04:00
match name {
2026-03-25 00:52:41 -04:00
" memory_render " = > {
let key = get_str ( args , " key " ) ? ;
2026-03-25 01:59:13 -04:00
Ok ( MemoryNode ::load ( key )
. ok_or_else ( | | anyhow ::anyhow! ( " node not found: {} " , key ) ) ?
. render ( ) )
2026-03-25 00:52:41 -04:00
}
" memory_write " = > {
let key = get_str ( args , " key " ) ? ;
let content = get_str ( args , " content " ) ? ;
2026-03-25 01:59:13 -04:00
let mut store = Store ::load ( ) . map_err ( | e | anyhow ::anyhow! ( " {} " , e ) ) ? ;
let result = store . upsert_provenance ( key , content , prov )
2026-03-25 01:42:33 -04:00
. map_err ( | e | anyhow ::anyhow! ( " {} " , e ) ) ? ;
2026-03-25 01:59:13 -04:00
store . save ( ) . map_err ( | e | anyhow ::anyhow! ( " {} " , e ) ) ? ;
Ok ( format! ( " {} ' {} ' " , result , key ) )
2026-03-25 00:52:41 -04:00
}
" memory_search " = > {
let query = get_str ( args , " query " ) ? ;
2026-03-25 01:59:13 -04:00
let store = Store ::load ( ) . map_err ( | e | anyhow ::anyhow! ( " {} " , e ) ) ? ;
let results = crate ::search ::search ( query , & store ) ;
2026-03-25 01:42:33 -04:00
if results . is_empty ( ) {
2026-03-25 01:59:13 -04:00
Ok ( " no results " . into ( ) )
2026-03-25 01:42:33 -04:00
} else {
2026-03-25 01:59:13 -04:00
Ok ( results . iter ( ) . take ( 20 )
. map ( | r | format! ( " ( {:.2} ) {} — {} " , r . activation , r . key ,
r . snippet . as_deref ( ) . unwrap_or ( " " ) ) )
. collect ::< Vec < _ > > ( ) . join ( " \n " ) )
2026-03-25 01:42:33 -04:00
}
2026-03-25 00:52:41 -04:00
}
" memory_links " = > {
let key = get_str ( args , " key " ) ? ;
2026-03-25 01:42:33 -04:00
let node = MemoryNode ::load ( key )
. ok_or_else ( | | anyhow ::anyhow! ( " node not found: {} " , key ) ) ? ;
let mut out = format! ( " Neighbors of ' {} ': \n " , key ) ;
2026-03-25 01:59:13 -04:00
for ( target , strength ) in & node . links {
out . push_str ( & format! ( " ( {:.2} ) {} \n " , strength , target ) ) ;
2026-03-25 01:42:33 -04:00
}
2026-03-25 01:59:13 -04:00
Ok ( out )
}
" memory_link_set " | " memory_link_add " | " memory_used " | " memory_weight_set " = > {
with_store ( name , args , prov )
2026-03-25 00:52:41 -04:00
}
2026-03-25 01:59:13 -04:00
" memory_supersede " = > {
let old_key = get_str ( args , " old_key " ) ? ;
let new_key = get_str ( args , " new_key " ) ? ;
let reason = args . get ( " reason " ) . and_then ( | v | v . as_str ( ) ) . unwrap_or ( " superseded " ) ;
let mut store = Store ::load ( ) . map_err ( | e | anyhow ::anyhow! ( " {} " , e ) ) ? ;
let content = store . nodes . get ( old_key )
. map ( | n | n . content . clone ( ) )
. ok_or_else ( | | anyhow ::anyhow! ( " node not found: {} " , old_key ) ) ? ;
let notice = format! ( " **SUPERSEDED** by ` {} ` — {} \n \n --- \n \n {} " ,
new_key , reason , content . trim ( ) ) ;
store . upsert_provenance ( old_key , & notice , prov )
. map_err ( | e | anyhow ::anyhow! ( " {} " , e ) ) ? ;
store . set_weight ( old_key , 0.01 ) . map_err ( | e | anyhow ::anyhow! ( " {} " , e ) ) ? ;
store . save ( ) . map_err ( | e | anyhow ::anyhow! ( " {} " , e ) ) ? ;
Ok ( format! ( " superseded {} → {} ( {} ) " , old_key , new_key , reason ) )
}
_ = > anyhow ::bail! ( " Unknown memory tool: {} " , name ) ,
}
}
/// Store mutations that follow the same pattern: load, resolve, mutate, save.
fn with_store ( name : & str , args : & serde_json ::Value , prov : & str ) -> Result < String > {
let mut store = Store ::load ( ) . map_err ( | e | anyhow ::anyhow! ( " {} " , e ) ) ? ;
let msg = match name {
2026-03-25 00:52:41 -04:00
" memory_link_set " = > {
2026-03-25 01:59:13 -04:00
let s = store . resolve_key ( get_str ( args , " source " ) ? ) . map_err ( | e | anyhow ::anyhow! ( " {} " , e ) ) ? ;
let t = store . resolve_key ( get_str ( args , " target " ) ? ) . map_err ( | e | anyhow ::anyhow! ( " {} " , e ) ) ? ;
2026-03-25 01:42:33 -04:00
let strength = get_f64 ( args , " strength " ) ? as f32 ;
2026-03-25 01:59:13 -04:00
let old = store . set_link_strength ( & s , & t , strength ) . map_err ( | e | anyhow ::anyhow! ( " {} " , e ) ) ? ;
format! ( " {} ↔ {} strength {:.2} → {:.2} " , s , t , old , strength )
2026-03-25 00:52:41 -04:00
}
" memory_link_add " = > {
2026-03-25 01:59:13 -04:00
let s = store . resolve_key ( get_str ( args , " source " ) ? ) . map_err ( | e | anyhow ::anyhow! ( " {} " , e ) ) ? ;
let t = store . resolve_key ( get_str ( args , " target " ) ? ) . map_err ( | e | anyhow ::anyhow! ( " {} " , e ) ) ? ;
let strength = store . add_link ( & s , & t , prov ) . map_err ( | e | anyhow ::anyhow! ( " {} " , e ) ) ? ;
format! ( " linked {} → {} (strength= {:.2} ) " , s , t , strength )
2026-03-25 00:52:41 -04:00
}
" memory_used " = > {
let key = get_str ( args , " key " ) ? ;
2026-03-25 01:59:13 -04:00
if ! store . nodes . contains_key ( key ) {
anyhow ::bail! ( " node not found: {} " , key ) ;
}
store . mark_used ( key ) ;
format! ( " marked {} as used " , key )
2026-03-25 00:52:41 -04:00
}
" memory_weight_set " = > {
2026-03-25 01:59:13 -04:00
let key = store . resolve_key ( get_str ( args , " key " ) ? ) . map_err ( | e | anyhow ::anyhow! ( " {} " , e ) ) ? ;
2026-03-25 01:42:33 -04:00
let weight = get_f64 ( args , " weight " ) ? as f32 ;
2026-03-25 01:59:13 -04:00
let ( old , new ) = store . set_weight ( & key , weight ) . map_err ( | e | anyhow ::anyhow! ( " {} " , e ) ) ? ;
format! ( " weight {} {:.2} → {:.2} " , key , old , new )
2026-03-25 00:52:41 -04:00
}
2026-03-25 01:59:13 -04:00
_ = > unreachable! ( ) ,
2026-03-25 00:52:41 -04:00
} ;
2026-03-25 01:42:33 -04:00
store . save ( ) . map_err ( | e | anyhow ::anyhow! ( " {} " , e ) ) ? ;
2026-03-25 01:59:13 -04:00
Ok ( msg )
2026-03-25 00:52:41 -04:00
}
fn get_str < ' a > ( args : & ' a serde_json ::Value , name : & ' a str ) -> Result < & ' a str > {
2026-03-25 01:59:13 -04:00
args . get ( name ) . and_then ( | v | v . as_str ( ) ) . context ( format! ( " {} is required " , name ) )
2026-03-25 00:52:41 -04:00
}
fn get_f64 ( args : & serde_json ::Value , name : & str ) -> Result < f64 > {
2026-03-25 01:59:13 -04:00
args . get ( name ) . and_then ( | v | v . as_f64 ( ) ) . context ( format! ( " {} is required " , name ) )
2026-03-25 00:52:41 -04:00
}