Skip to main content

InMemoryStore

Struct InMemoryStore 

Source
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

Source

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());
Source

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

Source§

fn default() -> Self

Returns the “default value” for a type. Read more
Source§

impl LoopMemory for InMemoryStore

Source§

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>>

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 relevance field.
  • 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 + '_>>

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);
Source§

fn len(&self) -> usize

Number of entries currently stored.

Used by is_empty and reported by the engine’s consolidate hook after each run.

Source§

fn is_empty(&self) -> bool

Whether the memory is empty. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more