Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
6f68558
milestone: complete comparative analysis of KA vs Hermes memory systems
Bahtya Apr 23, 2026
a499eaf
feat: replace LanceDB with tantivy-jieba full-text search
Bahtya Apr 23, 2026
99e7ebe
fix: update tantivy_store.rs for tantivy 0.26 API and remove dead code
Bahtya Apr 23, 2026
249f049
fix: JiebaTokenizer constructor, collector reference, and formatting
Bahtya Apr 23, 2026
9b0932f
fix: remove embedding from gateway/heartbeat, use TantivyStore, remov…
Bahtya Apr 23, 2026
8ff07c8
fix: format execute_learning_actions call in test
Bahtya Apr 23, 2026
137186c
fix: format execute_learning_actions on single line
Bahtya Apr 23, 2026
4609ccd
fix: add futures to dev-dependencies for concurrent test
Bahtya Apr 23, 2026
e7e40d9
fix: add LowerCaser filter to jieba tokenizer for case-insensitive se…
Bahtya Apr 23, 2026
17114c7
fix: use explicit lowercasing for case-insensitive search with jieba
Bahtya Apr 23, 2026
48e57bf
fix: separate stored content from search index to preserve original case
Bahtya Apr 23, 2026
2d663f3
style: format QueryParser::for_index on single line
Bahtya Apr 23, 2026
546c21a
style: fix rustfmt formatting for content_search fields
Bahtya Apr 23, 2026
d301230
style: match rustfmt chain formatting for content_search_field
Bahtya Apr 23, 2026
b80c3df
fix: keep tempdir alive in heartbeat memory tests
Bahtya Apr 23, 2026
41c1706
fix: skip capacity check for upserts (overwrite existing entries)
Bahtya Apr 23, 2026
f934de4
refactor: use TextAnalyzer+LowerCaser for case-insensitive search
Bahtya Apr 23, 2026
90e9fae
style: fix rustfmt formatting for chain calls
Bahtya Apr 23, 2026
87c1439
refactor: adopt CC-Adv review feedback — pure read recall + single field
Bahtya Apr 23, 2026
fc41bf0
style: single-line QueryParser call per rustfmt
Bahtya Apr 23, 2026
295fca3
fix: update test to match pure-read recall behavior
Bahtya Apr 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,10 @@ dirs = "5"
lru = "0.16"
fs4 = "0.13"

# Vector database
lancedb = "0.27"
arrow-array = "57"
arrow-schema = "57"
# Full-text search
tantivy = "0.26"
tantivy-jieba = "0.19"
jieba-rs = "0.9"

# ─── Main binary package ────────────────────────────────────

Expand Down
2 changes: 1 addition & 1 deletion crates/kestrel-agent/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ mod tests {
async fn test_unified_memory_uses_kestrel_memory_trait() {
let dir = tempfile::tempdir().unwrap();
let config = MemoryConfig::for_test(dir.path());
let store = kestrel_memory::HotStore::new(&config).await.unwrap();
let store = kestrel_memory::TantivyStore::new(&config).await.unwrap();

// Store a memory entry
let entry =
Expand Down
6 changes: 3 additions & 3 deletions crates/kestrel-agent/src/loop_mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1696,8 +1696,8 @@ mod tests {
// ── Memory integration tests ────────────────────────────────

use kestrel_memory::types::ScoredEntry;
use kestrel_memory::HotStore;
use kestrel_memory::MemoryError;
use kestrel_memory::TantivyStore;
use std::sync::atomic::{AtomicUsize, Ordering};

/// Mock memory store for deterministic testing.
Expand Down Expand Up @@ -2096,7 +2096,7 @@ mod tests {
async fn test_recall_with_real_hotstore() {
let dir = tempfile::tempdir().unwrap();
let config = kestrel_memory::MemoryConfig::for_test(dir.path());
let store = HotStore::new(&config).await.unwrap();
let store = TantivyStore::new(&config).await.unwrap();

// Pre-populate
store
Expand Down Expand Up @@ -2333,7 +2333,7 @@ mod tests {
async fn test_store_with_real_hotstore() {
let dir = tempfile::tempdir().unwrap();
let config = kestrel_memory::MemoryConfig::for_test(dir.path());
let store = HotStore::new(&config).await.unwrap();
let store = TantivyStore::new(&config).await.unwrap();

let al = make_agent_loop().with_memory_store(Arc::new(store));

Expand Down
13 changes: 7 additions & 6 deletions crates/kestrel-heartbeat/src/checks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ impl HealthCheck for ChannelHealthCheck {
mod tests {
use super::*;
use crate::types::HealthCheck;
use kestrel_memory::HotStore;
use kestrel_memory::TantivyStore;

// ─── ProviderHealthCheck tests ────────────────────────────────

Expand Down Expand Up @@ -681,15 +681,16 @@ mod tests {

// ─── MemoryStoreHealthCheck tests ─────────────────────────────

async fn make_test_hot_store() -> HotStore {
async fn make_test_tantivy_store() -> (TantivyStore, tempfile::TempDir) {
let dir = tempfile::tempdir().unwrap();
let config = kestrel_memory::MemoryConfig::for_test(dir.path());
HotStore::new(&config).await.unwrap()
let store = TantivyStore::new(&config).await.unwrap();
(store, dir)
}

#[tokio::test]
async fn test_memory_check_healthy() {
let store = make_test_hot_store().await;
let (store, _dir) = make_test_tantivy_store().await;
let check = MemoryStoreHealthCheck::new(Arc::new(store));
let result = check.report_health().await;
assert_eq!(result.status, CheckStatus::Healthy);
Expand All @@ -699,7 +700,7 @@ mod tests {

#[tokio::test]
async fn test_memory_check_custom_timeout() {
let store = make_test_hot_store().await;
let (store, _dir) = make_test_tantivy_store().await;
let check =
MemoryStoreHealthCheck::new(Arc::new(store)).with_timeout(Duration::from_secs(10));
assert_eq!(check.timeout, Duration::from_secs(10));
Expand Down Expand Up @@ -804,7 +805,7 @@ mod tests {
let bus = MessageBus::new();
svc.register_check(Arc::new(BusHealthCheck::new(bus)));

let store = make_test_hot_store().await;
let (store, _dir) = make_test_tantivy_store().await;
svc.register_check(Arc::new(MemoryStoreHealthCheck::new(Arc::new(store))));

let channel_statuses = Arc::new(parking_lot::RwLock::new(vec![(
Expand Down
10 changes: 4 additions & 6 deletions crates/kestrel-memory/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,10 @@ uuid = { workspace = true }
tracing = { workspace = true }
toml = { workspace = true }
dirs = { workspace = true }
lru = { workspace = true }
lancedb = { workspace = true }
arrow-array = { workspace = true }
arrow-schema = { workspace = true }
futures = { workspace = true }
fs4 = { workspace = true }
tantivy = { workspace = true }
tantivy-jieba = { workspace = true }
jieba-rs = { workspace = true }

[dev-dependencies]
tempfile = { workspace = true }
futures = { workspace = true }
57 changes: 12 additions & 45 deletions crates/kestrel-memory/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,13 @@ use std::path::PathBuf;
/// ```
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryConfig {
/// Maximum number of entries per store layer.
/// Maximum number of entries.
#[serde(default = "default_max_entries")]
pub max_entries: usize,

/// Path to the hot store persistence file (JSON lines format).
#[serde(default = "default_hot_store_path")]
pub hot_store_path: PathBuf,

/// Path to the warm store data directory.
#[serde(default = "default_warm_store_path")]
pub warm_store_path: PathBuf,

/// Dimension of embedding vectors for semantic search.
#[serde(default = "default_embedding_dim")]
pub embedding_dim: usize,
/// Path to the tantivy index directory.
#[serde(default = "default_tantivy_store_path")]
pub tantivy_store_path: PathBuf,

/// Character budget for recalled memory content injected into prompts.
#[serde(default = "default_memory_char_budget")]
Expand All @@ -46,24 +38,12 @@ fn default_max_entries() -> usize {
1000
}

fn default_hot_store_path() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".kestrel")
.join("memory")
.join("hot.jsonl")
}

fn default_warm_store_path() -> PathBuf {
fn default_tantivy_store_path() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".kestrel")
.join("memory")
.join("warm")
}

fn default_embedding_dim() -> usize {
1536
.join("tantivy")
}

fn default_memory_char_budget() -> usize {
Expand All @@ -78,9 +58,7 @@ impl Default for MemoryConfig {
fn default() -> Self {
Self {
max_entries: default_max_entries(),
hot_store_path: default_hot_store_path(),
warm_store_path: default_warm_store_path(),
embedding_dim: default_embedding_dim(),
tantivy_store_path: default_tantivy_store_path(),
memory_char_budget: default_memory_char_budget(),
memory_char_budget_overflow: default_memory_char_budget_overflow(),
}
Expand All @@ -92,9 +70,7 @@ impl MemoryConfig {
pub fn for_test(temp_dir: &std::path::Path) -> Self {
Self {
max_entries: 100,
hot_store_path: temp_dir.join("hot.jsonl"),
warm_store_path: temp_dir.join("warm"),
embedding_dim: 8,
tantivy_store_path: temp_dir.join("tantivy"),
memory_char_budget: default_memory_char_budget(),
memory_char_budget_overflow: default_memory_char_budget_overflow(),
}
Expand All @@ -119,12 +95,10 @@ mod tests {
fn test_default_config() {
let config = MemoryConfig::default();
assert_eq!(config.max_entries, 1000);
assert_eq!(config.embedding_dim, 1536);
assert_eq!(config.memory_char_budget, 2200);
assert_eq!(config.memory_char_budget_overflow, 1375);
assert!(config.hot_store_path.to_string_lossy().contains(".kestrel"));
assert!(config
.warm_store_path
.tantivy_store_path
.to_string_lossy()
.contains(".kestrel"));
}
Expand All @@ -134,29 +108,23 @@ mod tests {
let temp = std::env::temp_dir();
let config = MemoryConfig::for_test(&temp);
assert_eq!(config.max_entries, 100);
assert_eq!(config.embedding_dim, 8);
assert!(config.hot_store_path.starts_with(&temp));
assert!(config.warm_store_path.starts_with(&temp));
assert!(config.tantivy_store_path.starts_with(&temp));
}

#[test]
fn test_toml_roundtrip() {
let config = MemoryConfig {
max_entries: 500,
hot_store_path: PathBuf::from("/tmp/hot.jsonl"),
warm_store_path: PathBuf::from("/tmp/warm"),
embedding_dim: 768,
tantivy_store_path: PathBuf::from("/tmp/tantivy"),
memory_char_budget: 3000,
memory_char_budget_overflow: 1500,
};
let toml_str = config.to_toml().unwrap();
let parsed = MemoryConfig::from_toml(&toml_str).unwrap();
assert_eq!(parsed.max_entries, 500);
assert_eq!(parsed.embedding_dim, 768);
assert_eq!(parsed.memory_char_budget, 3000);
assert_eq!(parsed.memory_char_budget_overflow, 1500);
assert_eq!(parsed.hot_store_path, PathBuf::from("/tmp/hot.jsonl"));
assert_eq!(parsed.warm_store_path, PathBuf::from("/tmp/warm"));
assert_eq!(parsed.tantivy_store_path, PathBuf::from("/tmp/tantivy"));
}

#[test]
Expand All @@ -165,7 +133,6 @@ mod tests {
let config = MemoryConfig::from_toml(toml_str).unwrap();
assert_eq!(config.max_entries, 42);
// Other fields get defaults
assert_eq!(config.embedding_dim, 1536);
assert_eq!(config.memory_char_budget, 2200);
assert_eq!(config.memory_char_budget_overflow, 1375);
}
Expand Down
Loading
Loading