1use anyhow::{anyhow, Result};
16use serde::{Deserialize, Serialize};
17use serde_json::{json, Value};
18use std::collections::HashMap;
19use std::path::PathBuf;
20use std::sync::Arc;
21use tokio::sync::RwLock;
22
23#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
29#[serde(rename_all = "snake_case")]
30pub enum MemoryScope {
31 Session,
32 Project,
33 Global,
34}
35
36impl Default for MemoryScope {
37 fn default() -> Self {
38 Self::Project
39 }
40}
41
42impl std::str::FromStr for MemoryScope {
43 type Err = anyhow::Error;
44
45 fn from_str(s: &str) -> Result<Self> {
46 match s.to_lowercase().as_str() {
47 "session" => Ok(Self::Session),
48 "project" => Ok(Self::Project),
49 "global" => Ok(Self::Global),
50 _ => Ok(Self::Project),
51 }
52 }
53}
54
55impl std::fmt::Display for MemoryScope {
56 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 match self {
58 Self::Session => write!(f, "session"),
59 Self::Project => write!(f, "project"),
60 Self::Global => write!(f, "global"),
61 }
62 }
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
67#[serde(rename_all = "snake_case")]
68pub enum MemoryAction {
69 Recall,
70 Create,
71 Update,
72 Delete,
73 Manage,
74 Facts,
75 Summarize,
76 List,
77 Stats,
78 Clear,
79 Export,
80 Import,
81 Merge,
82 Tag,
83 Untag,
84 Namespaces,
85 History,
86 Kb,
87 Help,
88}
89
90impl Default for MemoryAction {
91 fn default() -> Self {
92 Self::Help
93 }
94}
95
96impl std::str::FromStr for MemoryAction {
97 type Err = anyhow::Error;
98
99 fn from_str(s: &str) -> Result<Self> {
100 match s.to_lowercase().as_str() {
101 "recall" | "search" | "query" => Ok(Self::Recall),
102 "create" | "add" | "store" => Ok(Self::Create),
103 "update" | "modify" => Ok(Self::Update),
104 "delete" | "remove" => Ok(Self::Delete),
105 "manage" => Ok(Self::Manage),
106 "facts" | "fact" => Ok(Self::Facts),
107 "summarize" | "summary" => Ok(Self::Summarize),
108 "list" => Ok(Self::List),
109 "stats" => Ok(Self::Stats),
110 "clear" => Ok(Self::Clear),
111 "export" => Ok(Self::Export),
112 "import" | "import_memories" => Ok(Self::Import),
113 "merge" => Ok(Self::Merge),
114 "tag" => Ok(Self::Tag),
115 "untag" => Ok(Self::Untag),
116 "namespaces" => Ok(Self::Namespaces),
117 "history" => Ok(Self::History),
118 "kb" => Ok(Self::Kb),
119 "help" | "" => Ok(Self::Help),
120 _ => Err(anyhow!("Unknown action: {}", s)),
121 }
122 }
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct Memory {
128 pub id: String,
129 pub content: String,
130 pub scope: MemoryScope,
131 pub created_at: String,
132 pub updated_at: String,
133 pub metadata: HashMap<String, Value>,
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct Fact {
139 pub id: String,
140 pub content: String,
141 pub kb_name: String,
142 pub scope: MemoryScope,
143 pub created_at: String,
144}
145
146#[derive(Debug, Clone, Default, Serialize, Deserialize)]
148pub struct KnowledgeBase {
149 pub name: String,
150 pub description: Option<String>,
151 pub scope: MemoryScope,
152 pub facts: Vec<Fact>,
153 pub created_at: String,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
162struct PersistEntry {
163 id: String,
164 key: String,
165 value: String,
166 tags: Vec<String>,
167 namespace: String,
168 created: String,
169 updated: String,
170 #[serde(skip_serializing_if = "Option::is_none")]
171 metadata: Option<HashMap<String, Value>>,
172 #[serde(skip_serializing_if = "Option::is_none")]
173 ttl: Option<String>,
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize)]
178struct PersistFact {
179 id: String,
180 content: String,
181 kb_name: String,
182 scope: String,
183 created: String,
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize)]
188struct PersistKb {
189 name: String,
190 #[serde(skip_serializing_if = "Option::is_none")]
191 description: Option<String>,
192 scope: String,
193 created: String,
194}
195
196#[derive(Debug, Clone, Serialize, Deserialize)]
198struct PersistStore {
199 entries: Vec<PersistEntry>,
200 #[serde(rename = "lastId")]
201 last_id: u64,
202 #[serde(default)]
203 facts: Vec<PersistFact>,
204 #[serde(rename = "lastFactId", default)]
205 last_fact_id: u64,
206 #[serde(rename = "knowledgeBases", default)]
207 knowledge_bases: Vec<PersistKb>,
208}
209
210impl Default for PersistStore {
211 fn default() -> Self {
212 Self {
213 entries: Vec::new(),
214 last_id: 0,
215 facts: Vec::new(),
216 last_fact_id: 0,
217 knowledge_bases: Vec::new(),
218 }
219 }
220}
221
222fn scope_to_namespace(scope: &MemoryScope, metadata: &HashMap<String, Value>) -> String {
227 if let Some(Value::String(ns)) = metadata.get("namespace") {
228 return ns.clone();
229 }
230 match scope {
231 MemoryScope::Session => "session".to_string(),
232 MemoryScope::Project => "default".to_string(),
233 MemoryScope::Global => "global".to_string(),
234 }
235}
236
237fn namespace_to_scope(ns: &str) -> MemoryScope {
238 match ns {
239 "session" => MemoryScope::Session,
240 "global" => MemoryScope::Global,
241 "default" => MemoryScope::Project,
242 _ => MemoryScope::Project,
243 }
244}
245
246fn memory_to_entry(m: &Memory) -> PersistEntry {
247 let tags: Vec<String> = m.metadata.keys()
248 .filter(|k| k.starts_with("tag:"))
249 .map(|k| k.strip_prefix("tag:").unwrap().to_string())
250 .collect();
251
252 let key = m.metadata.get("key")
253 .and_then(|v| v.as_str())
254 .unwrap_or(&m.id)
255 .to_string();
256
257 let ttl = m.metadata.get("ttl")
258 .and_then(|v| v.as_str())
259 .map(|s| s.to_string());
260
261 let clean_meta: HashMap<String, Value> = m.metadata.iter()
263 .filter(|(k, _)| !k.starts_with("tag:") && *k != "key" && *k != "ttl" && *k != "namespace")
264 .map(|(k, v)| (k.clone(), v.clone()))
265 .collect();
266
267 PersistEntry {
268 id: m.id.clone(),
269 key,
270 value: m.content.clone(),
271 tags,
272 namespace: scope_to_namespace(&m.scope, &m.metadata),
273 created: m.created_at.clone(),
274 updated: m.updated_at.clone(),
275 metadata: if clean_meta.is_empty() { None } else { Some(clean_meta) },
276 ttl,
277 }
278}
279
280fn entry_to_memory(e: &PersistEntry) -> Memory {
281 let mut metadata: HashMap<String, Value> = e.metadata.clone().unwrap_or_default();
282
283 for tag in &e.tags {
285 metadata.insert(format!("tag:{}", tag), json!(true));
286 }
287
288 metadata.insert("key".to_string(), json!(e.key));
290
291 if let Some(ref ttl) = e.ttl {
293 metadata.insert("ttl".to_string(), json!(ttl));
294 }
295
296 let scope = namespace_to_scope(&e.namespace);
298 if e.namespace != "session" && e.namespace != "global" && e.namespace != "default" {
299 metadata.insert("namespace".to_string(), json!(e.namespace));
300 }
301
302 Memory {
303 id: e.id.clone(),
304 content: e.value.clone(),
305 scope,
306 created_at: e.created.clone(),
307 updated_at: e.updated.clone(),
308 metadata,
309 }
310}
311
312fn fact_to_persist(f: &Fact) -> PersistFact {
313 PersistFact {
314 id: f.id.clone(),
315 content: f.content.clone(),
316 kb_name: f.kb_name.clone(),
317 scope: f.scope.to_string(),
318 created: f.created_at.clone(),
319 }
320}
321
322fn persist_to_fact(p: &PersistFact) -> Fact {
323 Fact {
324 id: p.id.clone(),
325 content: p.content.clone(),
326 kb_name: p.kb_name.clone(),
327 scope: p.scope.parse().unwrap_or_default(),
328 created_at: p.created.clone(),
329 }
330}
331
332fn is_expired(metadata: &HashMap<String, Value>) -> bool {
337 if let Some(Value::String(ttl)) = metadata.get("ttl") {
338 if let Ok(expiry) = chrono::DateTime::parse_from_rfc3339(ttl) {
339 return expiry < chrono::Utc::now();
340 }
341 if let Ok(expiry) = chrono::NaiveDateTime::parse_from_str(ttl, "%Y-%m-%dT%H:%M:%S") {
343 return expiry < chrono::Utc::now().naive_utc();
344 }
345 }
346 false
347}
348
349fn entry_is_expired(e: &PersistEntry) -> bool {
350 if let Some(ref ttl) = e.ttl {
351 if let Ok(expiry) = chrono::DateTime::parse_from_rfc3339(ttl) {
352 return expiry < chrono::Utc::now();
353 }
354 if let Ok(expiry) = chrono::NaiveDateTime::parse_from_str(ttl, "%Y-%m-%dT%H:%M:%S") {
355 return expiry < chrono::Utc::now().naive_utc();
356 }
357 }
358 false
359}
360
361#[derive(Debug, Clone, Default, Serialize, Deserialize)]
367pub struct MemoryToolArgs {
368 #[serde(default)]
369 pub action: String,
370 pub queries: Option<Vec<String>>,
372 pub query: Option<String>,
374 pub statements: Option<Vec<String>>,
376 pub statement: Option<String>,
378 pub id: Option<String>,
380 pub ids: Option<Vec<String>>,
382 pub updates: Option<Vec<Value>>,
384 pub scope: Option<String>,
386 pub namespace: Option<String>,
388 pub key: Option<String>,
390 pub ttl: Option<String>,
392 pub limit: Option<usize>,
394 pub kb_name: Option<String>,
396 pub facts: Option<Vec<String>>,
398 pub content: Option<String>,
400 pub topic: Option<String>,
402 pub metadata: Option<HashMap<String, Value>>,
404 pub creations: Option<Vec<String>>,
406 pub deletions: Option<Vec<String>>,
408 pub tag: Option<String>,
410 pub data: Option<String>,
412 pub description: Option<String>,
414 pub sub_action: Option<String>,
416}
417
418pub struct MemoryTool {
424 memories: Arc<RwLock<HashMap<String, Memory>>>,
425 knowledge_bases: Arc<RwLock<HashMap<String, KnowledgeBase>>>,
426 counter: Arc<RwLock<u64>>,
427 fact_counter: Arc<RwLock<u64>>,
428 history: Arc<RwLock<Vec<String>>>,
429 loaded: Arc<RwLock<bool>>,
430 storage_path: PathBuf,
431}
432
433impl MemoryTool {
434 pub fn new() -> Self {
435 Self::with_path(None)
436 }
437
438 pub fn with_path(path: Option<PathBuf>) -> Self {
440 let storage_path = path.unwrap_or_else(|| {
441 if let Ok(p) = std::env::var("MEMORY_PATH") {
442 PathBuf::from(p)
443 } else {
444 dirs::home_dir()
445 .unwrap_or_else(|| PathBuf::from("."))
446 .join(".hanzo")
447 .join("memory.json")
448 }
449 });
450
451 let tool = Self {
452 memories: Arc::new(RwLock::new(HashMap::new())),
453 knowledge_bases: Arc::new(RwLock::new(HashMap::new())),
454 counter: Arc::new(RwLock::new(0)),
455 fact_counter: Arc::new(RwLock::new(0)),
456 history: Arc::new(RwLock::new(Vec::new())),
457 loaded: Arc::new(RwLock::new(false)),
458 storage_path,
459 };
460
461 tool
464 }
465
466 async fn ensure_loaded(&self) -> Result<()> {
468 {
469 let loaded = self.loaded.read().await;
470 if *loaded {
471 return Ok(());
472 }
473 }
474 self.load_from_disk().await?;
475 *self.loaded.write().await = true;
476 Ok(())
477 }
478
479 async fn load_from_disk(&self) -> Result<()> {
480 let store = match tokio::fs::read_to_string(&self.storage_path).await {
481 Ok(data) => {
482 serde_json::from_str::<PersistStore>(&data).unwrap_or_default()
483 }
484 Err(_) => PersistStore::default(),
485 };
486
487 let mut memories = self.memories.write().await;
488 let mut kbs = self.knowledge_bases.write().await;
489 let mut counter = self.counter.write().await;
490 let mut fact_counter = self.fact_counter.write().await;
491
492 memories.clear();
493 kbs.clear();
494
495 *counter = store.last_id;
496 *fact_counter = store.last_fact_id;
497
498 for entry in &store.entries {
500 if entry_is_expired(entry) {
501 continue;
502 }
503 let memory = entry_to_memory(entry);
504 memories.insert(memory.id.clone(), memory);
505 }
506
507 for pkb in &store.knowledge_bases {
509 kbs.entry(pkb.name.clone()).or_insert_with(|| KnowledgeBase {
510 name: pkb.name.clone(),
511 description: pkb.description.clone(),
512 scope: pkb.scope.parse().unwrap_or_default(),
513 facts: Vec::new(),
514 created_at: pkb.created.clone(),
515 });
516 }
517
518 for pf in &store.facts {
520 let fact = persist_to_fact(pf);
521 let kb = kbs.entry(fact.kb_name.clone()).or_insert_with(|| KnowledgeBase {
522 name: fact.kb_name.clone(),
523 description: None,
524 scope: fact.scope.clone(),
525 facts: Vec::new(),
526 created_at: fact.created_at.clone(),
527 });
528 kb.facts.push(fact);
529 }
530
531 Ok(())
532 }
533
534 async fn save_to_disk(&self) -> Result<()> {
535 let memories = self.memories.read().await;
536 let kbs = self.knowledge_bases.read().await;
537 let counter = self.counter.read().await;
538 let fact_counter = self.fact_counter.read().await;
539
540 let entries: Vec<PersistEntry> = memories.values()
542 .filter(|m| !is_expired(&m.metadata))
543 .map(|m| memory_to_entry(m))
544 .collect();
545
546 let facts: Vec<PersistFact> = kbs.values()
548 .flat_map(|kb| kb.facts.iter().map(|f| fact_to_persist(f)))
549 .collect();
550
551 let knowledge_bases: Vec<PersistKb> = kbs.values()
553 .map(|kb| PersistKb {
554 name: kb.name.clone(),
555 description: kb.description.clone(),
556 scope: kb.scope.to_string(),
557 created: kb.created_at.clone(),
558 })
559 .collect();
560
561 let store = PersistStore {
562 entries,
563 last_id: *counter,
564 facts,
565 last_fact_id: *fact_counter,
566 knowledge_bases,
567 };
568
569 if let Some(parent) = self.storage_path.parent() {
571 tokio::fs::create_dir_all(parent).await?;
572 }
573
574 let json = serde_json::to_string_pretty(&store)?;
575 tokio::fs::write(&self.storage_path, json).await?;
576 Ok(())
577 }
578
579 async fn next_id(&self, prefix: &str) -> String {
580 let mut counter = self.counter.write().await;
581 *counter += 1;
582 format!("{}_{}", prefix, *counter)
583 }
584
585 async fn next_fact_id(&self) -> String {
586 let mut counter = self.fact_counter.write().await;
587 *counter += 1;
588 format!("fact_{}", *counter)
589 }
590
591 fn resolve_scope(args: &MemoryToolArgs) -> (MemoryScope, Option<String>) {
593 if let Some(ref ns) = args.namespace {
594 match ns.as_str() {
595 "default" => (MemoryScope::Project, None),
596 "session" => (MemoryScope::Session, None),
597 "global" => (MemoryScope::Global, None),
598 other => (MemoryScope::Project, Some(other.to_string())),
599 }
600 } else if let Some(ref s) = args.scope {
601 (s.parse().unwrap_or_default(), None)
602 } else {
603 (MemoryScope::Project, None)
604 }
605 }
606
607 fn build_metadata(args: &MemoryToolArgs, custom_namespace: &Option<String>) -> HashMap<String, Value> {
609 let mut metadata = args.metadata.clone().unwrap_or_default();
610 if let Some(ref key) = args.key {
611 metadata.insert("key".to_string(), json!(key));
612 }
613 if let Some(ref ttl) = args.ttl {
614 metadata.insert("ttl".to_string(), json!(ttl));
615 }
616 if let Some(ref ns) = custom_namespace {
617 metadata.insert("namespace".to_string(), json!(ns));
618 }
619 metadata
620 }
621
622 pub async fn execute(&self, args: MemoryToolArgs) -> Result<String> {
623 self.ensure_loaded().await?;
625
626 let action: MemoryAction = if args.action.is_empty() {
627 MemoryAction::Help
628 } else {
629 args.action.parse()?
630 };
631
632 let result = match action {
633 MemoryAction::Recall => self.recall(args).await?,
634 MemoryAction::Create => self.create(args).await?,
635 MemoryAction::Update => self.update(args).await?,
636 MemoryAction::Delete => self.delete(args).await?,
637 MemoryAction::Manage => self.manage(args).await?,
638 MemoryAction::Facts => self.facts(args).await?,
639 MemoryAction::Summarize => self.summarize(args).await?,
640 MemoryAction::List => self.list(args).await?,
641 MemoryAction::Stats => self.stats(args).await?,
642 MemoryAction::Clear => self.clear(args).await?,
643 MemoryAction::Export => self.export_memories().await?,
644 MemoryAction::Import => self.import_memories(args).await?,
645 MemoryAction::Merge => self.merge_memories().await?,
646 MemoryAction::Tag => self.tag_memory(args).await?,
647 MemoryAction::Untag => self.untag_memory(args).await?,
648 MemoryAction::Namespaces => self.namespaces().await?,
649 MemoryAction::History => self.history_log().await?,
650 MemoryAction::Kb => self.kb(args).await?,
651 MemoryAction::Help => self.help()?,
652 };
653
654 Ok(serde_json::to_string(&result)?)
655 }
656
657 async fn recall(&self, args: MemoryToolArgs) -> Result<Value> {
662 let queries = args.queries.clone()
663 .or_else(|| args.query.clone().map(|q| vec![q]))
664 .unwrap_or_default();
665 let (scope, custom_ns) = Self::resolve_scope(&args);
666 let limit = args.limit.unwrap_or(10);
667
668 let memories = self.memories.read().await;
669 let mut results = Vec::new();
670
671 if let Some(ref key) = args.key {
673 let matches: Vec<&Memory> = memories.values()
674 .filter(|m| {
675 !is_expired(&m.metadata)
676 && m.metadata.get("key").and_then(|v| v.as_str()) == Some(key.as_str())
677 && Self::matches_scope_and_ns(m, &scope, &custom_ns)
678 })
679 .take(limit)
680 .collect();
681
682 for m in matches {
683 results.push(memory_to_json(m));
684 }
685
686 return Ok(json!({
687 "key": key,
688 "scope": scope.to_string(),
689 "results": results,
690 "count": results.len()
691 }));
692 }
693
694 if queries.is_empty() {
696 let matches: Vec<&Memory> = memories.values()
698 .filter(|m| {
699 !is_expired(&m.metadata)
700 && Self::matches_scope_and_ns(m, &scope, &custom_ns)
701 })
702 .take(limit)
703 .collect();
704 for m in matches {
705 results.push(memory_to_json(m));
706 }
707 } else {
708 for query in &queries {
709 let query_lower = query.to_lowercase();
710 let terms: Vec<&str> = query_lower.split_whitespace().collect();
711 let matches: Vec<&Memory> = memories.values()
712 .filter(|m| {
713 if is_expired(&m.metadata) { return false; }
714 if !Self::matches_scope_and_ns(m, &scope, &custom_ns) { return false; }
715 let text = m.content.to_lowercase();
716 terms.iter().all(|t| text.contains(t))
717 })
718 .take(limit)
719 .collect();
720
721 for m in matches {
722 results.push(memory_to_json(m));
723 }
724 }
725 }
726
727 Ok(json!({
728 "queries": queries,
729 "scope": scope.to_string(),
730 "results": results,
731 "count": results.len()
732 }))
733 }
734
735 async fn create(&self, args: MemoryToolArgs) -> Result<Value> {
736 let statements = args.statements.clone()
737 .or_else(|| args.statement.clone().map(|s| vec![s]))
738 .or_else(|| args.content.clone().map(|c| vec![c]))
739 .ok_or_else(|| anyhow!("statements required"))?;
740 let (scope, custom_ns) = Self::resolve_scope(&args);
741 let metadata = Self::build_metadata(&args, &custom_ns);
742 let now = chrono::Utc::now().to_rfc3339();
743
744 let mut created_ids = Vec::new();
745 let mut memories = self.memories.write().await;
746
747 for statement in statements {
748 let id = self.next_id("mem").await;
749 let memory = Memory {
750 id: id.clone(),
751 content: statement,
752 scope: scope.clone(),
753 created_at: now.clone(),
754 updated_at: now.clone(),
755 metadata: metadata.clone(),
756 };
757 memories.insert(id.clone(), memory);
758 created_ids.push(id);
759 }
760
761 drop(memories);
762 self.save_to_disk().await?;
763 self.record_history(&format!("create: {} memories", created_ids.len())).await;
764
765 Ok(json!({
766 "created": created_ids.len(),
767 "ids": created_ids,
768 "scope": scope.to_string()
769 }))
770 }
771
772 async fn update(&self, args: MemoryToolArgs) -> Result<Value> {
773 let now = chrono::Utc::now().to_rfc3339();
774 let mut updated_ids = Vec::new();
775
776 if let Some(ref id) = args.id {
778 let new_content = args.statement.as_deref()
779 .or(args.content.as_deref());
780 if let Some(content) = new_content {
781 let mut memories = self.memories.write().await;
782 if let Some(memory) = memories.get_mut(id.as_str()) {
783 memory.content = content.to_string();
784 memory.updated_at = now.clone();
785 if let Some(ref ttl) = args.ttl {
786 memory.metadata.insert("ttl".to_string(), json!(ttl));
787 }
788 if let Some(ref key) = args.key {
789 memory.metadata.insert("key".to_string(), json!(key));
790 }
791 updated_ids.push(id.clone());
792 }
793 drop(memories);
794 if !updated_ids.is_empty() {
795 self.save_to_disk().await?;
796 }
797 return Ok(json!({ "updated": updated_ids.len(), "ids": updated_ids }));
798 }
799 }
800
801 let updates = args.updates.ok_or_else(|| anyhow!("updates or (id + statement) required"))?;
803 let mut memories = self.memories.write().await;
804
805 for update_val in updates {
806 if let Some(obj) = update_val.as_object() {
807 if let (Some(id), Some(statement)) = (
808 obj.get("id").and_then(|v| v.as_str()),
809 obj.get("statement").and_then(|v| v.as_str())
810 ) {
811 if let Some(memory) = memories.get_mut(id) {
812 memory.content = statement.to_string();
813 memory.updated_at = now.clone();
814 updated_ids.push(id.to_string());
815 }
816 }
817 }
818 }
819
820 drop(memories);
821 if !updated_ids.is_empty() {
822 self.save_to_disk().await?;
823 }
824 self.record_history(&format!("update: {} memories", updated_ids.len())).await;
825
826 Ok(json!({
827 "updated": updated_ids.len(),
828 "ids": updated_ids
829 }))
830 }
831
832 async fn delete(&self, args: MemoryToolArgs) -> Result<Value> {
833 let mut ids_to_delete = args.ids.clone()
834 .or_else(|| args.id.clone().map(|id| vec![id]))
835 .unwrap_or_default();
836
837 if ids_to_delete.is_empty() {
839 if let Some(ref key) = args.key {
840 let memories = self.memories.read().await;
841 ids_to_delete = memories.values()
842 .filter(|m| m.metadata.get("key").and_then(|v| v.as_str()) == Some(key.as_str()))
843 .map(|m| m.id.clone())
844 .collect();
845 }
846 }
847
848 if ids_to_delete.is_empty() {
849 return Err(anyhow!("ids, id, or key required"));
850 }
851
852 let mut deleted_ids = Vec::new();
853 let mut memories = self.memories.write().await;
854
855 for id in ids_to_delete {
856 if memories.remove(&id).is_some() {
857 deleted_ids.push(id);
858 }
859 }
860
861 drop(memories);
862 if !deleted_ids.is_empty() {
863 self.save_to_disk().await?;
864 }
865 self.record_history(&format!("delete: {} memories", deleted_ids.len())).await;
866
867 Ok(json!({
868 "deleted": deleted_ids.len(),
869 "ids": deleted_ids
870 }))
871 }
872
873 async fn manage(&self, args: MemoryToolArgs) -> Result<Value> {
874 let (scope, custom_ns) = Self::resolve_scope(&args);
875 let metadata = Self::build_metadata(&args, &custom_ns);
876 let now = chrono::Utc::now().to_rfc3339();
877
878 let mut created_ids = Vec::new();
879 let mut updated_ids = Vec::new();
880 let mut deleted_ids = Vec::new();
881
882 if let Some(creations) = args.creations.clone() {
884 let mut memories = self.memories.write().await;
885 for statement in creations {
886 let id = self.next_id("mem").await;
887 let memory = Memory {
888 id: id.clone(),
889 content: statement,
890 scope: scope.clone(),
891 created_at: now.clone(),
892 updated_at: now.clone(),
893 metadata: metadata.clone(),
894 };
895 memories.insert(id.clone(), memory);
896 created_ids.push(id);
897 }
898 }
899
900 if let Some(updates) = args.updates.clone() {
902 let mut memories = self.memories.write().await;
903 for update_val in updates {
904 if let Some(obj) = update_val.as_object() {
905 if let (Some(id), Some(statement)) = (
906 obj.get("id").and_then(|v| v.as_str()),
907 obj.get("statement").and_then(|v| v.as_str())
908 ) {
909 if let Some(memory) = memories.get_mut(id) {
910 memory.content = statement.to_string();
911 memory.updated_at = now.clone();
912 updated_ids.push(id.to_string());
913 }
914 }
915 }
916 }
917 }
918
919 if let Some(deletions) = args.deletions.clone() {
921 let mut memories = self.memories.write().await;
922 for id in deletions {
923 if memories.remove(&id).is_some() {
924 deleted_ids.push(id);
925 }
926 }
927 }
928
929 let mutated = !created_ids.is_empty() || !updated_ids.is_empty() || !deleted_ids.is_empty();
930 if mutated {
931 self.save_to_disk().await?;
932 }
933
934 Ok(json!({
935 "created": created_ids,
936 "updated": updated_ids,
937 "deleted": deleted_ids,
938 "scope": scope.to_string()
939 }))
940 }
941
942 async fn facts(&self, args: MemoryToolArgs) -> Result<Value> {
943 let kb_name = args.kb_name.clone().unwrap_or_else(|| "general".to_string());
944 let (scope, _custom_ns) = Self::resolve_scope(&args);
945 let now = chrono::Utc::now().to_rfc3339();
946
947 if let Some(new_facts) = args.facts.clone() {
948 let mut kbs = self.knowledge_bases.write().await;
950 let kb = kbs.entry(kb_name.clone()).or_insert_with(|| KnowledgeBase {
951 name: kb_name.clone(),
952 description: None,
953 scope: scope.clone(),
954 facts: Vec::new(),
955 created_at: now.clone(),
956 });
957
958 let mut created_ids = Vec::new();
959 for fact_content in new_facts {
960 let id = self.next_fact_id().await;
961 let fact = Fact {
962 id: id.clone(),
963 content: fact_content,
964 kb_name: kb_name.clone(),
965 scope: scope.clone(),
966 created_at: now.clone(),
967 };
968 kb.facts.push(fact);
969 created_ids.push(id);
970 }
971
972 drop(kbs);
973 self.save_to_disk().await?;
974
975 return Ok(json!({
976 "stored": created_ids.len(),
977 "ids": created_ids,
978 "kb_name": kb_name
979 }));
980 }
981
982 if let Some(queries) = args.queries.clone().or_else(|| args.query.clone().map(|q| vec![q])) {
984 let kbs = self.knowledge_bases.read().await;
985 let limit = args.limit.unwrap_or(10);
986 let mut results = Vec::new();
987
988 if let Some(kb) = kbs.get(&kb_name) {
989 for query in &queries {
990 let query_lower = query.to_lowercase();
991 let matches: Vec<&Fact> = kb.facts.iter()
992 .filter(|f| f.content.to_lowercase().contains(&query_lower))
993 .take(limit)
994 .collect();
995
996 for f in matches {
997 results.push(json!({
998 "id": f.id,
999 "content": f.content,
1000 "kb_name": f.kb_name
1001 }));
1002 }
1003 }
1004 }
1005
1006 return Ok(json!({
1007 "queries": queries,
1008 "kb_name": kb_name,
1009 "results": results,
1010 "count": results.len()
1011 }));
1012 }
1013
1014 let kbs = self.knowledge_bases.read().await;
1016 let kb_list: Vec<Value> = kbs.values()
1017 .map(|kb| json!({
1018 "name": kb.name,
1019 "description": kb.description,
1020 "fact_count": kb.facts.len(),
1021 "scope": kb.scope.to_string()
1022 }))
1023 .collect();
1024
1025 Ok(json!({
1026 "knowledge_bases": kb_list,
1027 "count": kb_list.len()
1028 }))
1029 }
1030
1031 async fn summarize(&self, args: MemoryToolArgs) -> Result<Value> {
1032 let content = args.content.clone().ok_or_else(|| anyhow!("content required"))?;
1033 let topic = args.topic.clone().ok_or_else(|| anyhow!("topic required"))?;
1034 let (scope, custom_ns) = Self::resolve_scope(&args);
1035 let now = chrono::Utc::now().to_rfc3339();
1036
1037 let id = self.next_id("mem").await;
1039 let summary = format!("[{}] {}", topic, content);
1040 let mut metadata = Self::build_metadata(&args, &custom_ns);
1041 metadata.insert("topic".to_string(), json!(topic));
1042 metadata.insert("type".to_string(), json!("summary"));
1043
1044 let memory = Memory {
1045 id: id.clone(),
1046 content: summary.clone(),
1047 scope,
1048 created_at: now.clone(),
1049 updated_at: now,
1050 metadata,
1051 };
1052
1053 self.memories.write().await.insert(id.clone(), memory);
1054 self.save_to_disk().await?;
1055
1056 let facts: Vec<&str> = content.lines()
1058 .filter(|l| !l.trim().is_empty())
1059 .take(5)
1060 .collect();
1061
1062 Ok(json!({
1063 "id": id,
1064 "topic": topic,
1065 "stored": true,
1066 "extracted_facts": facts.len(),
1067 "facts": facts
1068 }))
1069 }
1070
1071 async fn list(&self, args: MemoryToolArgs) -> Result<Value> {
1072 let (scope, custom_ns) = Self::resolve_scope(&args);
1073 let has_scope_filter = args.scope.is_some() || args.namespace.is_some();
1074 let limit = args.limit.unwrap_or(50);
1075
1076 let memories = self.memories.read().await;
1077 let results: Vec<Value> = memories.values()
1078 .filter(|m| {
1079 if is_expired(&m.metadata) { return false; }
1080 if has_scope_filter {
1081 Self::matches_scope_and_ns(m, &scope, &custom_ns)
1082 } else {
1083 true
1084 }
1085 })
1086 .take(limit)
1087 .map(|m| memory_to_json(m))
1088 .collect();
1089
1090 Ok(json!({
1091 "memories": results,
1092 "count": results.len(),
1093 "total": memories.len()
1094 }))
1095 }
1096
1097 async fn record_history(&self, entry: &str) {
1098 let mut history = self.history.write().await;
1099 history.push(format!("[{}] {}", chrono::Utc::now().to_rfc3339(), entry));
1100 if history.len() > 1000 { history.drain(0..500); }
1101 }
1102
1103 async fn stats(&self, _args: MemoryToolArgs) -> Result<Value> {
1104 let memories = self.memories.read().await;
1105 let kbs = self.knowledge_bases.read().await;
1106 let mut by_scope: HashMap<String, usize> = HashMap::new();
1107 let mut by_namespace: HashMap<String, usize> = HashMap::new();
1108 let mut total_size = 0usize;
1109 for m in memories.values() {
1110 if is_expired(&m.metadata) { continue; }
1111 let scope_key = m.scope.to_string();
1112 *by_scope.entry(scope_key).or_insert(0) += 1;
1113 let ns = scope_to_namespace(&m.scope, &m.metadata);
1114 *by_namespace.entry(ns).or_insert(0) += 1;
1115 total_size += m.content.len();
1116 }
1117 Ok(json!({
1118 "total_memories": memories.len(),
1119 "total_size_bytes": total_size,
1120 "by_scope": by_scope,
1121 "by_namespace": by_namespace,
1122 "knowledge_bases": kbs.len(),
1123 "total_facts": kbs.values().map(|kb| kb.facts.len()).sum::<usize>(),
1124 "storage_path": self.storage_path.to_string_lossy()
1125 }))
1126 }
1127
1128 async fn clear(&self, args: MemoryToolArgs) -> Result<Value> {
1129 let (scope, custom_ns) = Self::resolve_scope(&args);
1130 let has_scope_filter = args.scope.is_some() || args.namespace.is_some();
1131 let mut memories = self.memories.write().await;
1132 let before = memories.len();
1133 if has_scope_filter {
1134 memories.retain(|_, m| !Self::matches_scope_and_ns(m, &scope, &custom_ns));
1135 } else {
1136 memories.clear();
1137 }
1138 let cleared = before - memories.len();
1139 drop(memories);
1140 self.save_to_disk().await?;
1141 self.record_history(&format!("clear: removed {} memories", cleared)).await;
1142 Ok(json!({ "cleared": cleared, "remaining": before - cleared }))
1143 }
1144
1145 async fn export_memories(&self) -> Result<Value> {
1146 let memories = self.memories.read().await;
1147 let kbs = self.knowledge_bases.read().await;
1148 let mem_list: Vec<Value> = memories.values()
1149 .filter(|m| !is_expired(&m.metadata))
1150 .map(|m| memory_to_json(m))
1151 .collect();
1152 let kb_list: Vec<Value> = kbs.values().map(|kb| json!({
1153 "name": kb.name, "fact_count": kb.facts.len(),
1154 "scope": kb.scope.to_string()
1155 })).collect();
1156 Ok(json!({
1157 "memories": mem_list,
1158 "knowledge_bases": kb_list,
1159 "count": mem_list.len(),
1160 "storage_path": self.storage_path.to_string_lossy()
1161 }))
1162 }
1163
1164 async fn import_memories(&self, args: MemoryToolArgs) -> Result<Value> {
1165 let data = args.data.clone().ok_or_else(|| anyhow!("data (JSON string) required"))?;
1166 let parsed: Value = serde_json::from_str(&data)?;
1167 let now = chrono::Utc::now().to_rfc3339();
1168 let mut imported = 0;
1169 let mut memories = self.memories.write().await;
1170 if let Some(items) = parsed.get("memories").and_then(|v| v.as_array()) {
1171 for item in items {
1172 let id = self.next_id("mem").await;
1173 let content = item.get("content").and_then(|v| v.as_str())
1174 .or_else(|| item.get("value").and_then(|v| v.as_str()))
1175 .unwrap_or("").to_string();
1176 let scope: MemoryScope = item.get("scope").and_then(|v| v.as_str())
1177 .unwrap_or("project").parse()?;
1178 let mut metadata = HashMap::new();
1179 if let Some(key) = item.get("key").and_then(|v| v.as_str()) {
1180 metadata.insert("key".to_string(), json!(key));
1181 }
1182 let memory = Memory {
1183 id: id.clone(), content, scope,
1184 created_at: now.clone(), updated_at: now.clone(),
1185 metadata,
1186 };
1187 memories.insert(id, memory);
1188 imported += 1;
1189 }
1190 }
1191 if let Some(items) = parsed.get("entries").and_then(|v| v.as_array()) {
1193 for item in items {
1194 if let Ok(entry) = serde_json::from_value::<PersistEntry>(item.clone()) {
1195 if entry_is_expired(&entry) { continue; }
1196 let memory = entry_to_memory(&entry);
1197 let id = self.next_id("mem").await;
1198 let mut m = memory;
1199 m.id = id.clone();
1200 memories.insert(id, m);
1201 imported += 1;
1202 }
1203 }
1204 }
1205 drop(memories);
1206 self.save_to_disk().await?;
1207 self.record_history(&format!("import: {} memories", imported)).await;
1208 Ok(json!({ "imported": imported }))
1209 }
1210
1211 async fn merge_memories(&self) -> Result<Value> {
1212 let mut memories = self.memories.write().await;
1213 let ids: Vec<String> = memories.keys().cloned().collect();
1214 let mut merged = 0;
1215 let mut to_remove = Vec::new();
1216 for i in 0..ids.len() {
1217 for j in (i+1)..ids.len() {
1218 if to_remove.contains(&ids[j]) { continue; }
1219 let a = memories.get(&ids[i]).map(|m| m.content.clone());
1220 let b = memories.get(&ids[j]).map(|m| m.content.clone());
1221 if let (Some(a), Some(b)) = (a, b) {
1222 if a == b {
1223 to_remove.push(ids[j].clone());
1224 merged += 1;
1225 }
1226 }
1227 }
1228 }
1229 for id in &to_remove { memories.remove(id); }
1230 drop(memories);
1231 if merged > 0 {
1232 self.save_to_disk().await?;
1233 }
1234 self.record_history(&format!("merge: removed {} duplicates", merged)).await;
1235 Ok(json!({ "merged": merged, "removed_ids": to_remove }))
1236 }
1237
1238 async fn tag_memory(&self, args: MemoryToolArgs) -> Result<Value> {
1239 let id = args.id.clone().ok_or_else(|| anyhow!("id required"))?;
1240 let tag = args.tag.clone().ok_or_else(|| anyhow!("tag required"))?;
1241 let mut memories = self.memories.write().await;
1242 let memory = memories.get_mut(&id).ok_or_else(|| anyhow!("Memory not found: {}", id))?;
1243 memory.metadata.insert(format!("tag:{}", tag), json!(true));
1244 drop(memories);
1245 self.save_to_disk().await?;
1246 self.record_history(&format!("tag: {} += {}", id, tag)).await;
1247 Ok(json!({ "id": id, "tag": tag, "tagged": true }))
1248 }
1249
1250 async fn untag_memory(&self, args: MemoryToolArgs) -> Result<Value> {
1251 let id = args.id.clone().ok_or_else(|| anyhow!("id required"))?;
1252 let tag = args.tag.clone().ok_or_else(|| anyhow!("tag required"))?;
1253 let mut memories = self.memories.write().await;
1254 let memory = memories.get_mut(&id).ok_or_else(|| anyhow!("Memory not found: {}", id))?;
1255 memory.metadata.remove(&format!("tag:{}", tag));
1256 drop(memories);
1257 self.save_to_disk().await?;
1258 self.record_history(&format!("untag: {} -= {}", id, tag)).await;
1259 Ok(json!({ "id": id, "tag": tag, "untagged": true }))
1260 }
1261
1262 async fn namespaces(&self) -> Result<Value> {
1263 let memories = self.memories.read().await;
1264 let mut ns_counts: HashMap<String, usize> = HashMap::new();
1265 for m in memories.values() {
1266 if is_expired(&m.metadata) { continue; }
1267 let ns = scope_to_namespace(&m.scope, &m.metadata);
1268 *ns_counts.entry(ns).or_insert(0) += 1;
1269 }
1270 let kbs = self.knowledge_bases.read().await;
1272 let kb_names: Vec<String> = kbs.keys().cloned().collect();
1273
1274 Ok(json!({
1275 "namespaces": ns_counts,
1276 "knowledge_bases": kb_names,
1277 "count": ns_counts.len()
1278 }))
1279 }
1280
1281 async fn history_log(&self) -> Result<Value> {
1282 let history = self.history.read().await;
1283 let entries: Vec<&String> = history.iter().rev().take(50).collect();
1284 Ok(json!({ "history": entries, "count": entries.len() }))
1285 }
1286
1287 async fn kb(&self, args: MemoryToolArgs) -> Result<Value> {
1288 let sub = args.sub_action.as_deref()
1289 .or(args.query.as_deref())
1290 .unwrap_or("list");
1291
1292 match sub {
1293 "create" => {
1294 let name = args.kb_name.clone()
1295 .ok_or_else(|| anyhow!("kb_name required for kb create"))?;
1296 let (scope, _) = Self::resolve_scope(&args);
1297 let now = chrono::Utc::now().to_rfc3339();
1298 let mut kbs = self.knowledge_bases.write().await;
1299 if kbs.contains_key(&name) {
1300 return Ok(json!({ "error": format!("Knowledge base '{}' already exists", name) }));
1301 }
1302 kbs.insert(name.clone(), KnowledgeBase {
1303 name: name.clone(),
1304 description: args.description.clone(),
1305 scope,
1306 facts: Vec::new(),
1307 created_at: now,
1308 });
1309 drop(kbs);
1310 self.save_to_disk().await?;
1311 Ok(json!({ "created": name, "description": args.description }))
1312 }
1313 "delete" => {
1314 let name = args.kb_name.clone()
1315 .ok_or_else(|| anyhow!("kb_name required for kb delete"))?;
1316 let mut kbs = self.knowledge_bases.write().await;
1317 if kbs.remove(&name).is_some() {
1318 drop(kbs);
1319 self.save_to_disk().await?;
1320 Ok(json!({ "deleted": name }))
1321 } else {
1322 Ok(json!({ "error": format!("Knowledge base '{}' not found", name) }))
1323 }
1324 }
1325 "list" | _ => {
1326 let kbs = self.knowledge_bases.read().await;
1327 let list: Vec<Value> = kbs.values().map(|kb| json!({
1328 "name": kb.name,
1329 "description": kb.description,
1330 "fact_count": kb.facts.len(),
1331 "scope": kb.scope.to_string(),
1332 "created_at": kb.created_at
1333 })).collect();
1334 Ok(json!({ "knowledge_bases": list, "count": list.len() }))
1335 }
1336 }
1337 }
1338
1339 fn help(&self) -> Result<Value> {
1340 Ok(json!({
1341 "name": "memory",
1342 "version": "0.13.0",
1343 "description": "Persistent memory and knowledge management tool (HIP-0300). Data persisted to ~/.hanzo/memory.json, compatible with TypeScript runtime.",
1344 "actions": {
1345 "recall": {
1346 "description": "Search memories by query or key",
1347 "params": ["queries|query", "key", "scope|namespace", "limit"]
1348 },
1349 "create": {
1350 "description": "Store new memories",
1351 "params": ["statements|statement|content", "scope|namespace", "key", "ttl", "metadata"]
1352 },
1353 "update": {
1354 "description": "Update existing memories",
1355 "params": ["id + statement|content", "updates[]", "key", "ttl"]
1356 },
1357 "delete": {
1358 "description": "Remove memories by id, ids, or key",
1359 "params": ["id|ids|key"]
1360 },
1361 "manage": {
1362 "description": "Atomic create/update/delete in one call",
1363 "params": ["creations[]", "updates[]", "deletions[]", "scope|namespace", "key", "ttl"]
1364 },
1365 "facts": {
1366 "description": "Store/recall knowledge base facts",
1367 "params": ["kb_name", "facts[]", "queries|query", "scope", "limit"]
1368 },
1369 "summarize": {
1370 "description": "Summarize content and store as memory",
1371 "params": ["content", "topic", "scope|namespace"]
1372 },
1373 "list": {
1374 "description": "List all memories, optionally filtered by scope/namespace",
1375 "params": ["scope|namespace", "limit"]
1376 },
1377 "stats": {
1378 "description": "Memory statistics by scope and namespace",
1379 "params": []
1380 },
1381 "clear": {
1382 "description": "Clear memories (optional scope/namespace filter)",
1383 "params": ["scope|namespace"]
1384 },
1385 "export": {
1386 "description": "Export all memories as JSON",
1387 "params": []
1388 },
1389 "import": {
1390 "description": "Import memories from JSON data string",
1391 "params": ["data"]
1392 },
1393 "merge": {
1394 "description": "Remove duplicate memories",
1395 "params": []
1396 },
1397 "tag": {
1398 "description": "Add tag to a memory",
1399 "params": ["id", "tag"]
1400 },
1401 "untag": {
1402 "description": "Remove tag from a memory",
1403 "params": ["id", "tag"]
1404 },
1405 "namespaces": {
1406 "description": "List all namespaces with entry counts",
1407 "params": []
1408 },
1409 "history": {
1410 "description": "Recent operation history",
1411 "params": []
1412 },
1413 "kb": {
1414 "description": "Knowledge base management (create/list/delete)",
1415 "params": ["sub_action (create|list|delete)", "kb_name", "description"]
1416 },
1417 "help": {
1418 "description": "Show this documentation",
1419 "params": []
1420 }
1421 },
1422 "scopes": ["session", "project", "global"],
1423 "namespace_mapping": {
1424 "default": "project scope",
1425 "session": "session scope",
1426 "global": "global scope",
1427 "<custom>": "project scope with namespace stored in metadata"
1428 },
1429 "persistence": "~/.hanzo/memory.json (auto-loaded, saved after every mutation)"
1430 }))
1431 }
1432
1433 fn matches_scope_and_ns(m: &Memory, scope: &MemoryScope, custom_ns: &Option<String>) -> bool {
1438 if m.scope != *scope {
1439 return false;
1440 }
1441 if let Some(ref ns) = custom_ns {
1442 m.metadata.get("namespace")
1444 .and_then(|v| v.as_str())
1445 .map_or(false, |v| v == ns.as_str())
1446 } else {
1447 !m.metadata.contains_key("namespace")
1449 }
1450 }
1451}
1452
1453fn memory_to_json(m: &Memory) -> Value {
1454 let ns = scope_to_namespace(&m.scope, &m.metadata);
1455 let key = m.metadata.get("key").and_then(|v| v.as_str()).unwrap_or(&m.id);
1456 let tags: Vec<String> = m.metadata.keys()
1457 .filter(|k| k.starts_with("tag:"))
1458 .map(|k| k.strip_prefix("tag:").unwrap().to_string())
1459 .collect();
1460
1461 json!({
1462 "id": m.id,
1463 "key": key,
1464 "content": m.content,
1465 "scope": m.scope.to_string(),
1466 "namespace": ns,
1467 "tags": tags,
1468 "created_at": m.created_at,
1469 "updated_at": m.updated_at,
1470 "metadata": m.metadata
1471 })
1472}
1473
1474#[derive(Debug, Serialize, Deserialize)]
1479pub struct MemoryToolDefinition {
1480 pub name: String,
1481 pub description: String,
1482 pub input_schema: Value,
1483}
1484
1485impl MemoryToolDefinition {
1486 pub fn new() -> Self {
1487 Self {
1488 name: "memory".to_string(),
1489 description: r#"Persistent memory and knowledge management tool (HIP-0300).
1490Data persisted to ~/.hanzo/memory.json (cross-runtime compatible).
1491
1492Actions:
1493- recall: Search memories by query or key
1494- create: Store new memories (with optional key, ttl, namespace)
1495- update: Update existing memories
1496- delete: Remove memories by id or key
1497- manage: Atomic create/update/delete
1498- facts: Manage knowledge base facts
1499- summarize: Summarize and store information
1500- list: List all memories
1501- stats: Memory statistics
1502- kb: Knowledge base management (create/list/delete)
1503- help: Show all actions and parameters
1504
1505Scopes: session, project, global
1506Namespaces: alias for scope — "default"->project, "session"->session, "global"->global, custom->project+metadata"#.to_string(),
1507 input_schema: json!({
1508 "type": "object",
1509 "properties": {
1510 "action": {
1511 "type": "string",
1512 "enum": ["recall", "create", "update", "delete", "manage", "facts", "summarize", "list", "stats", "clear", "export", "import", "merge", "tag", "untag", "namespaces", "history", "kb", "help"]
1513 },
1514 "queries": {"type": "array", "items": {"type": "string"}},
1515 "query": {"type": "string"},
1516 "statements": {"type": "array", "items": {"type": "string"}},
1517 "statement": {"type": "string"},
1518 "id": {"type": "string"},
1519 "ids": {"type": "array", "items": {"type": "string"}},
1520 "updates": {"type": "array", "items": {"type": "object"}},
1521 "scope": {"type": "string", "enum": ["session", "project", "global"]},
1522 "namespace": {"type": "string", "description": "Namespace alias for scope. 'default'->project, 'session'->session, 'global'->global, custom->project with namespace in metadata"},
1523 "key": {"type": "string", "description": "Key for key-based storage and recall"},
1524 "ttl": {"type": "string", "description": "Expiry as ISO date string (e.g. 2026-03-25T00:00:00Z)"},
1525 "limit": {"type": "integer"},
1526 "kb_name": {"type": "string"},
1527 "facts": {"type": "array", "items": {"type": "string"}},
1528 "content": {"type": "string"},
1529 "topic": {"type": "string"},
1530 "metadata": {"type": "object"},
1531 "creations": {"type": "array", "items": {"type": "string"}},
1532 "deletions": {"type": "array", "items": {"type": "string"}},
1533 "tag": {"type": "string", "description": "Tag name for tag/untag"},
1534 "data": {"type": "string", "description": "JSON data for import"},
1535 "description": {"type": "string", "description": "Description for kb create"},
1536 "sub_action": {"type": "string", "description": "Sub-action for kb (create|list|delete)"}
1537 }
1538 }),
1539 }
1540 }
1541}
1542
1543#[cfg(test)]
1548mod tests {
1549 use super::*;
1550 use tempfile::NamedTempFile;
1551
1552 fn test_tool() -> (MemoryTool, NamedTempFile) {
1553 let tmp = NamedTempFile::new().unwrap();
1554 let tool = MemoryTool::with_path(Some(tmp.path().to_path_buf()));
1555 (tool, tmp)
1556 }
1557
1558 #[tokio::test]
1559 async fn test_create_memory() {
1560 let (tool, _tmp) = test_tool();
1561 let args = MemoryToolArgs {
1562 action: "create".to_string(),
1563 statements: Some(vec!["User prefers dark mode".to_string()]),
1564 scope: Some("project".to_string()),
1565 ..Default::default()
1566 };
1567
1568 let result = tool.execute(args).await;
1569 assert!(result.is_ok());
1570 let output = result.unwrap();
1571 assert!(output.contains("created"));
1572 }
1573
1574 #[tokio::test]
1575 async fn test_recall_memory() {
1576 let (tool, _tmp) = test_tool();
1577
1578 let args = MemoryToolArgs {
1580 action: "create".to_string(),
1581 statements: Some(vec!["User prefers Python".to_string()]),
1582 ..Default::default()
1583 };
1584 tool.execute(args).await.unwrap();
1585
1586 let args = MemoryToolArgs {
1588 action: "recall".to_string(),
1589 queries: Some(vec!["Python".to_string()]),
1590 ..Default::default()
1591 };
1592
1593 let result = tool.execute(args).await;
1594 assert!(result.is_ok());
1595 let output = result.unwrap();
1596 assert!(output.contains("Python"));
1597 }
1598
1599 #[tokio::test]
1600 async fn test_facts() {
1601 let (tool, _tmp) = test_tool();
1602 let args = MemoryToolArgs {
1603 action: "facts".to_string(),
1604 kb_name: Some("coding".to_string()),
1605 facts: Some(vec!["Use uv for Python".to_string()]),
1606 ..Default::default()
1607 };
1608
1609 let result = tool.execute(args).await;
1610 assert!(result.is_ok());
1611 let output = result.unwrap();
1612 assert!(output.contains("stored"));
1613 }
1614
1615 #[tokio::test]
1616 async fn test_summarize() {
1617 let (tool, _tmp) = test_tool();
1618 let args = MemoryToolArgs {
1619 action: "summarize".to_string(),
1620 content: Some("Discussion about API design patterns and best practices.".to_string()),
1621 topic: Some("API Design".to_string()),
1622 ..Default::default()
1623 };
1624
1625 let result = tool.execute(args).await;
1626 assert!(result.is_ok());
1627 let output = result.unwrap();
1628 assert!(output.contains("API Design"));
1629 }
1630
1631 #[tokio::test]
1632 async fn test_persistence_survives_reload() {
1633 let tmp = NamedTempFile::new().unwrap();
1634 let path = tmp.path().to_path_buf();
1635
1636 {
1638 let tool = MemoryTool::with_path(Some(path.clone()));
1639 let args = MemoryToolArgs {
1640 action: "create".to_string(),
1641 statements: Some(vec!["Persistent memory test".to_string()]),
1642 key: Some("test-key".to_string()),
1643 ..Default::default()
1644 };
1645 tool.execute(args).await.unwrap();
1646 }
1647
1648 {
1650 let tool = MemoryTool::with_path(Some(path.clone()));
1651 let args = MemoryToolArgs {
1652 action: "recall".to_string(),
1653 key: Some("test-key".to_string()),
1654 ..Default::default()
1655 };
1656 let result = tool.execute(args).await.unwrap();
1657 assert!(result.contains("Persistent memory test"), "Memory should survive restart: {}", result);
1658 }
1659 }
1660
1661 #[tokio::test]
1662 async fn test_key_based_storage_and_recall() {
1663 let (tool, _tmp) = test_tool();
1664
1665 let args = MemoryToolArgs {
1667 action: "create".to_string(),
1668 statements: Some(vec!["Blue agent report for task 42".to_string()]),
1669 key: Some("blue-report-42".to_string()),
1670 ..Default::default()
1671 };
1672 tool.execute(args).await.unwrap();
1673
1674 let args = MemoryToolArgs {
1676 action: "recall".to_string(),
1677 key: Some("blue-report-42".to_string()),
1678 ..Default::default()
1679 };
1680 let result = tool.execute(args).await.unwrap();
1681 assert!(result.contains("Blue agent report"), "Key recall should work: {}", result);
1682 }
1683
1684 #[tokio::test]
1685 async fn test_namespace_support() {
1686 let (tool, _tmp) = test_tool();
1687
1688 let args = MemoryToolArgs {
1690 action: "create".to_string(),
1691 statements: Some(vec!["Blue-red coordination data".to_string()]),
1692 namespace: Some("blue-red".to_string()),
1693 ..Default::default()
1694 };
1695 tool.execute(args).await.unwrap();
1696
1697 let args = MemoryToolArgs {
1699 action: "recall".to_string(),
1700 queries: Some(vec!["coordination".to_string()]),
1701 namespace: Some("blue-red".to_string()),
1702 ..Default::default()
1703 };
1704 let result = tool.execute(args).await.unwrap();
1705 assert!(result.contains("coordination"), "Namespace recall should work: {}", result);
1706
1707 let args = MemoryToolArgs {
1709 action: "recall".to_string(),
1710 queries: Some(vec!["coordination".to_string()]),
1711 namespace: Some("other".to_string()),
1712 ..Default::default()
1713 };
1714 let result = tool.execute(args).await.unwrap();
1715 let parsed: Value = serde_json::from_str(&result).unwrap();
1716 assert_eq!(parsed["count"], 0, "Different namespace should not find memory");
1717 }
1718
1719 #[tokio::test]
1720 async fn test_ttl_expiry() {
1721 let (tool, _tmp) = test_tool();
1722
1723 let args = MemoryToolArgs {
1725 action: "create".to_string(),
1726 statements: Some(vec!["Should be expired".to_string()]),
1727 ttl: Some("2020-01-01T00:00:00Z".to_string()),
1728 ..Default::default()
1729 };
1730 tool.execute(args).await.unwrap();
1731
1732 let args = MemoryToolArgs {
1734 action: "create".to_string(),
1735 statements: Some(vec!["Should be visible".to_string()]),
1736 ttl: Some("2099-01-01T00:00:00Z".to_string()),
1737 ..Default::default()
1738 };
1739 tool.execute(args).await.unwrap();
1740
1741 let args = MemoryToolArgs {
1743 action: "list".to_string(),
1744 ..Default::default()
1745 };
1746 let result = tool.execute(args).await.unwrap();
1747 assert!(result.contains("Should be visible"), "Non-expired should be visible");
1748 assert!(!result.contains("Should be expired"), "Expired should be filtered: {}", result);
1749 }
1750
1751 #[tokio::test]
1752 async fn test_kb_management() {
1753 let (tool, _tmp) = test_tool();
1754
1755 let args = MemoryToolArgs {
1757 action: "kb".to_string(),
1758 sub_action: Some("create".to_string()),
1759 kb_name: Some("test-kb".to_string()),
1760 description: Some("Test knowledge base".to_string()),
1761 ..Default::default()
1762 };
1763 let result = tool.execute(args).await.unwrap();
1764 assert!(result.contains("test-kb"), "KB should be created: {}", result);
1765
1766 let args = MemoryToolArgs {
1768 action: "kb".to_string(),
1769 sub_action: Some("list".to_string()),
1770 ..Default::default()
1771 };
1772 let result = tool.execute(args).await.unwrap();
1773 assert!(result.contains("test-kb"), "KB should be listed: {}", result);
1774
1775 let args = MemoryToolArgs {
1777 action: "kb".to_string(),
1778 sub_action: Some("delete".to_string()),
1779 kb_name: Some("test-kb".to_string()),
1780 ..Default::default()
1781 };
1782 let result = tool.execute(args).await.unwrap();
1783 assert!(result.contains("test-kb"), "KB should be deleted: {}", result);
1784
1785 let args = MemoryToolArgs {
1787 action: "kb".to_string(),
1788 sub_action: Some("list".to_string()),
1789 ..Default::default()
1790 };
1791 let result = tool.execute(args).await.unwrap();
1792 let parsed: Value = serde_json::from_str(&result).unwrap();
1793 assert_eq!(parsed["count"], 0, "KB list should be empty after delete");
1794 }
1795
1796 #[tokio::test]
1797 async fn test_help_action() {
1798 let (tool, _tmp) = test_tool();
1799 let args = MemoryToolArgs {
1800 action: "help".to_string(),
1801 ..Default::default()
1802 };
1803 let result = tool.execute(args).await.unwrap();
1804 assert!(result.contains("recall"));
1805 assert!(result.contains("create"));
1806 assert!(result.contains("kb"));
1807 assert!(result.contains("persistence"));
1808 }
1809
1810 #[tokio::test]
1811 async fn test_delete_by_key() {
1812 let (tool, _tmp) = test_tool();
1813
1814 let args = MemoryToolArgs {
1816 action: "create".to_string(),
1817 statements: Some(vec!["Deletable memory".to_string()]),
1818 key: Some("delete-me".to_string()),
1819 ..Default::default()
1820 };
1821 tool.execute(args).await.unwrap();
1822
1823 let args = MemoryToolArgs {
1825 action: "delete".to_string(),
1826 key: Some("delete-me".to_string()),
1827 ..Default::default()
1828 };
1829 let result = tool.execute(args).await.unwrap();
1830 assert!(result.contains("\"deleted\":1"), "Should delete by key: {}", result);
1831
1832 let args = MemoryToolArgs {
1834 action: "recall".to_string(),
1835 key: Some("delete-me".to_string()),
1836 ..Default::default()
1837 };
1838 let result = tool.execute(args).await.unwrap();
1839 let parsed: Value = serde_json::from_str(&result).unwrap();
1840 assert_eq!(parsed["count"], 0, "Deleted memory should not be found");
1841 }
1842
1843 #[tokio::test]
1844 async fn test_json_format_compatibility() {
1845 let tmp = NamedTempFile::new().unwrap();
1846 let path = tmp.path().to_path_buf();
1847
1848 {
1850 let tool = MemoryTool::with_path(Some(path.clone()));
1851 let args = MemoryToolArgs {
1852 action: "create".to_string(),
1853 statements: Some(vec!["Cross-runtime test".to_string()]),
1854 key: Some("compat-key".to_string()),
1855 namespace: Some("blue-red".to_string()),
1856 ..Default::default()
1857 };
1858 tool.execute(args).await.unwrap();
1859 }
1860
1861 let data = tokio::fs::read_to_string(&path).await.unwrap();
1863 let store: Value = serde_json::from_str(&data).unwrap();
1864
1865 assert!(store.get("entries").is_some(), "Should have entries key");
1866 assert!(store.get("lastId").is_some(), "Should have lastId key");
1867 assert!(store.get("facts").is_some(), "Should have facts key");
1868 assert!(store.get("lastFactId").is_some(), "Should have lastFactId key");
1869
1870 let entry = &store["entries"][0];
1871 assert!(entry.get("key").is_some(), "Entry should have key");
1872 assert!(entry.get("value").is_some(), "Entry should have value");
1873 assert!(entry.get("namespace").is_some(), "Entry should have namespace");
1874 assert!(entry.get("tags").is_some(), "Entry should have tags");
1875 assert!(entry.get("created").is_some(), "Entry should have created");
1876 assert!(entry.get("updated").is_some(), "Entry should have updated");
1877
1878 assert_eq!(entry["key"], "compat-key");
1879 assert_eq!(entry["value"], "Cross-runtime test");
1880 assert_eq!(entry["namespace"], "blue-red");
1881 }
1882
1883 #[tokio::test]
1884 async fn test_namespace_default_maps_to_project() {
1885 let (tool, _tmp) = test_tool();
1886
1887 let args = MemoryToolArgs {
1889 action: "create".to_string(),
1890 statements: Some(vec!["Default namespace test".to_string()]),
1891 namespace: Some("default".to_string()),
1892 ..Default::default()
1893 };
1894 tool.execute(args).await.unwrap();
1895
1896 let args = MemoryToolArgs {
1898 action: "recall".to_string(),
1899 queries: Some(vec!["Default namespace".to_string()]),
1900 scope: Some("project".to_string()),
1901 ..Default::default()
1902 };
1903 let result = tool.execute(args).await.unwrap();
1904 assert!(result.contains("Default namespace test"), "default namespace should map to project scope: {}", result);
1905 }
1906
1907 #[tokio::test]
1908 async fn test_persistence_with_facts() {
1909 let tmp = NamedTempFile::new().unwrap();
1910 let path = tmp.path().to_path_buf();
1911
1912 {
1914 let tool = MemoryTool::with_path(Some(path.clone()));
1915 let args = MemoryToolArgs {
1916 action: "facts".to_string(),
1917 kb_name: Some("coding".to_string()),
1918 facts: Some(vec!["Always use uv for Python".to_string()]),
1919 ..Default::default()
1920 };
1921 tool.execute(args).await.unwrap();
1922 }
1923
1924 {
1926 let tool = MemoryTool::with_path(Some(path.clone()));
1927 let args = MemoryToolArgs {
1928 action: "facts".to_string(),
1929 kb_name: Some("coding".to_string()),
1930 query: Some("uv".to_string()),
1931 ..Default::default()
1932 };
1933 let result = tool.execute(args).await.unwrap();
1934 assert!(result.contains("Always use uv"), "Facts should survive restart: {}", result);
1935 }
1936 }
1937}