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,
) -> impl Future<Output = Result<(), LoopError>> + Send
fn store( &self, entry: MemoryEntry, ) -> impl Future<Output = Result<(), LoopError>> + Send
Store a new memory entry by appending it to the backing list.
Called whenever the agent encounters information worth remembering — for example after a successful tool invocation, a resolved error, or an insight drawn from conversation.
§Errors
This implementation never returns an error.
Source§fn retrieve(
&self,
query: &str,
limit: usize,
) -> impl Future<Output = Result<Vec<MemoryEntry>, LoopError>> + Send
fn retrieve( &self, query: &str, limit: usize, ) -> impl Future<Output = Result<Vec<MemoryEntry>, LoopError>> + Send
Retrieve memory entries relevant to the given query.
Called before each turn (or on demand) 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,
) -> impl Future<Output = Result<ConsolidationStats, LoopError>> + Send
fn consolidate( &self, ) -> impl Future<Output = Result<ConsolidationStats, LoopError>> + Send
Consolidate memory by pruning low-relevance entries.
Called periodically by the framework 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);