1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
//! Memory API — memory subsystem facade (Phase C).
//!
//! Extracted from `AgentApi` to provide a focused interface for
//! memory operations. Replaces the previous `AgentApi.memory_manager()`
//! leaky accessor pattern.
//!
//! Consumers should use [`KernelHandle::memory()`] to access this API.
use crate::memory::{HnswMemoryIndex, MemoryEntry, MemoryManager, MemoryType, SemanticHit};
use std::sync::Arc;
/// Memory subsystem facade.
///
/// Provides high-level memory operations without exposing the
/// internal `MemoryManager` directly. This is the 14th typed API
/// in `KernelHandle` (alongside `A2aApi`, `AgentApi`, etc.).
pub struct MemoryApi {
/// Underlying memory manager.
pub(crate) memory_manager: Arc<MemoryManager>,
/// Optional HNSW index for fast semantic search.
pub(crate) hnsw_index: Option<Arc<HnswMemoryIndex>>,
}
impl MemoryApi {
/// Create a new MemoryApi.
///
/// The optional HNSW index, when present, is used by `search_semantic`
/// and `rebuild_hnsw_index`. Callers that have an attached index (e.g.
/// `KernelHandle::memory`) should pass it here so this facade agrees with
/// `AgentApi`'s memory operations.
pub fn new(
memory_manager: Arc<MemoryManager>,
hnsw_index: Option<Arc<HnswMemoryIndex>>,
) -> Self {
Self {
memory_manager,
hnsw_index,
}
}
/// Attach an HNSW index for fast semantic search.
pub fn set_hnsw_index(&mut self, index: Arc<HnswMemoryIndex>) {
self.hnsw_index = Some(index);
}
/// Store a memory entry. Returns the entry's ID.
pub async fn remember(&self, entry: MemoryEntry) -> anyhow::Result<String> {
self.memory_manager.remember(entry).await
}
/// Search memory by text query.
pub async fn search(
&self,
query: &str,
memory_type: Option<MemoryType>,
limit: usize,
) -> anyhow::Result<Vec<MemoryEntry>> {
self.memory_manager.search(query, memory_type, limit).await
}
/// Recall memory by query (semantic if HNSW available, else keyword).
pub async fn recall(&self, query: &str) -> anyhow::Result<Vec<MemoryEntry>> {
self.memory_manager.recall(query).await
}
/// Get a specific memory entry by ID and type.
pub async fn get(
&self,
id: &str,
memory_type: MemoryType,
) -> anyhow::Result<Option<MemoryEntry>> {
self.memory_manager.get(id, memory_type).await
}
/// Forget (delete) a memory entry.
pub async fn forget(&self, id: &str, memory_type: MemoryType) -> anyhow::Result<bool> {
self.memory_manager.forget(id, memory_type).await
}
/// List memories of a given type.
pub async fn list(
&self,
memory_type: MemoryType,
limit: usize,
) -> anyhow::Result<Vec<MemoryEntry>> {
self.memory_manager.list(memory_type, limit).await
}
/// Search memory using semantic similarity (returns `SemanticHit`s ranked
/// by cosine similarity).
///
/// Uses the attached HNSW index when available; otherwise falls back to
/// keyword search. In the fallback case `similarity` is `0.0` (unknown),
/// never a fabricated `1.0`.
pub async fn search_semantic(
&self,
query: &str,
limit: usize,
) -> anyhow::Result<Vec<SemanticHit>> {
if let Some(hnsw) = &self.hnsw_index {
self.memory_manager
.semantic_search(query, None, limit, hnsw)
.await
} else {
// Fallback: keyword search. similarity/distance are unknown.
let entries = self.memory_manager.search(query, None, limit).await?;
Ok(entries
.into_iter()
.map(|e| SemanticHit {
entry: e,
distance: 0.0,
similarity: 0.0,
})
.collect())
}
}
/// Get memory statistics: (total_entries, vector_index_size).
pub async fn stats(&self) -> (usize, usize) {
(
self.memory_manager.total_entries().await,
self.memory_manager.vector_index_size(),
)
}
/// Rebuild the HNSW index from current memory state.
///
/// Returns the number of vectors indexed. Errors if no HNSW index is
/// attached (matching `AgentApi::rebuild_hnsw_index`).
pub async fn rebuild_hnsw_index(&self) -> anyhow::Result<usize> {
if let Some(hnsw) = &self.hnsw_index {
self.memory_manager.rebuild_hnsw_index(hnsw).await
} else {
Err(anyhow::anyhow!("HNSW index not initialized"))
}
}
/// Access the underlying memory manager. For advanced operations
/// not yet exposed via this facade.
pub fn manager(&self) -> &Arc<MemoryManager> {
&self.memory_manager
}
}