pub struct InMemoryStore { /* private fields */ }Expand description
A simple in-memory store for loop memory entries.
Stores MemoryEntry values in a flat Vec and retrieves them using
a weighted scoring function that combines the entry’s base
relevance, word-overlap with the query,
and tag matching. This scoring strategy provides reasonable results
without requiring an embedding model.
Not suitable for production — entries are held in process memory and lost on crash. Use this for unit tests, integration tests, and as a reference when implementing a real backend (e.g. one backed by a vector database).
§Scoring Formula
Each candidate entry is scored during retrieve
using a weighted blend of three signals:
final_score = relevance × 0.5
+ word_overlap_ratio × 0.4
+ tag_bonus (0.3 if any tag matches)
+ 0.1 (baseline)The baseline term ensures that every entry has a non-zero score so that even entries with no word overlap can still be returned when the store is sparse.
§Thread Safety
InMemoryStore is Send + Sync. Interior mutability is handled via
an internal RwLock, so store and consolidate only require &self.
This allows the store to be shared via Arc<InMemoryStore> or
Arc<InMemoryStore> across tasks without external locking.
§Construction
use loopctl::memory::builtin::InMemoryStore;
use loopctl::memory::{MemoryEntry, MemoryCategory};
// Empty store:
let store = InMemoryStore::new();
// Pre-populated:
let store = InMemoryStore::new().with_entries(vec![
MemoryEntry::new(MemoryCategory::Fact, "The project uses Rust 1.95"),
]);§Example
use loopctl::memory::builtin::InMemoryStore;
use loopctl::memory::{LoopMemory, MemoryEntry, MemoryCategory};
let store = InMemoryStore::new();
store.store(MemoryEntry::new(MemoryCategory::Insight, "Prefer Glob over manual file search")).await.unwrap();
let results = store.retrieve("file search", 5).await.unwrap();
assert_eq!(results.len(), 1);§Unbounded Growth
InMemoryStore accumulates entries in a Vec with no automatic
eviction. The consolidate() method
prunes entries with relevance < 0.05, but it must be called
explicitly. A long-running session that never calls consolidate()
will accumulate memory indefinitely. For production use, consider
calling consolidate() periodically or implementing a custom
LoopMemory with bounded capacity.
Implementations§
Source§impl InMemoryStore
impl InMemoryStore
Sourcepub fn new() -> Self
pub fn new() -> Self
Create a new empty store.
Returns a fresh InMemoryStore whose len is zero.
§Example
use loopctl::memory::builtin::InMemoryStore;
use loopctl::memory::LoopMemory;
let store = InMemoryStore::new();
assert!(store.is_empty());Sourcepub fn with_entries(self, entries: Vec<MemoryEntry>) -> Self
pub fn with_entries(self, entries: Vec<MemoryEntry>) -> Self
Create a store pre-populated with the given entries.
Useful for setting up test fixtures or seeding an agent with initial context.
§Example
use loopctl::memory::builtin::InMemoryStore;
use loopctl::memory::{LoopMemory, MemoryEntry, MemoryCategory};
let store = InMemoryStore::new().with_entries(vec![
MemoryEntry::new(MemoryCategory::Fact, "Rust 1.75 stabilised async fn in trait"),
MemoryEntry::new(MemoryCategory::Strategy, "Start refactors with tests"),
]);
assert_eq!(store.len(), 2);Trait Implementations§
Source§impl Default for InMemoryStore
impl Default for InMemoryStore
Source§impl LoopMemory for InMemoryStore
impl LoopMemory for InMemoryStore
Source§fn store(
&self,
entry: MemoryEntry,
) -> Pin<Box<dyn Future<Output = Result<(), LoopError>> + Send + '_>>
fn store( &self, entry: MemoryEntry, ) -> Pin<Box<dyn Future<Output = Result<(), LoopError>> + Send + '_>>
Store a new memory entry by appending it to the backing list.
Called by the engine after every successful tool call to record the trajectory — what tool ran, with what input, and what it returned.
§Errors
This implementation never returns an error.
Source§fn retrieve<'a>(
&'a self,
query: &'a str,
limit: usize,
) -> Pin<Box<dyn Future<Output = Result<Vec<MemoryEntry>, LoopError>> + Send + 'a>>
fn retrieve<'a>( &'a self, query: &'a str, limit: usize, ) -> Pin<Box<dyn Future<Output = Result<Vec<MemoryEntry>, LoopError>> + Send + 'a>>
Retrieve memory entries relevant to the given query.
Called by the engine before each turn to surface context the agent
can use. Returns up to limit entries ordered by a composite score
that blends:
- Base relevance (50%) — the entry’s
relevancefield. - Word overlap (40%) — fraction of query words found in the entry memory.
- Tag match (30% flat bonus) — whether any tag contains the full query.
- Baseline (10%) — ensures every entry has a non-zero score.
The query is matched case-insensitively against both the entry
memory and tags.
§Returns
A Vec<MemoryEntry> of at most limit entries, sorted by descending
composite score. May be empty if no entries match or the store is empty.
§Example
use loopctl::memory::builtin::InMemoryStore;
use loopctl::memory::{LoopMemory, MemoryEntry, MemoryCategory};
let store = InMemoryStore::new();
store.store(MemoryEntry::new(MemoryCategory::Fact, "file search uses Glob")).await.unwrap();
let results = store.retrieve("file search", 5).await.unwrap();
for entry in &results {
println!("{:?}", entry.category);
}Source§fn consolidate(
&self,
) -> Pin<Box<dyn Future<Output = Result<ConsolidationStats, LoopError>> + Send + '_>>
fn consolidate( &self, ) -> Pin<Box<dyn Future<Output = Result<ConsolidationStats, LoopError>> + Send + '_>>
Consolidate memory by pruning low-relevance entries.
Called by the engine at the end of each successful run to keep the
memory store healthy. This implementation removes entries whose
relevance score has decayed below 0.05.
It does not perform merging — merged
and bytes_saved are always zero.
§Returns
A ConsolidationStats describing the number of entries before and
after pruning, and how many were removed.
§Example
use loopctl::memory::builtin::InMemoryStore;
use loopctl::memory::LoopMemory;
let store = InMemoryStore::new();
let stats = store.consolidate().await.unwrap();
println!("Pruned {} entries", stats.pruned);