OpenPay (1 crate, 3751 LOC, 83 tests)
op-crypto-extra Extended crypto rails. Stablecoin catalog (USDC, USDT,
PYUSD, DAI, RLUSD, EURC, FDUSD — 26 deployments). L2
catalog (12 chains incl. Optimism, Arbitrum, Base,
zkSync Era, Polygon zkEVM, Linea, Scroll, Starknet).
CCTP v2 BurnMessage encode/decode + domain ids +
attestation. ERC-4337 UserOperation v0.7 +
PackedUserOperation. EIP-7702 set-EOA-code with RLP
signing preimage. Uniswap Permit2 single + batch
transfer EIP-712 hashes. EIP-2612 ERC-20 permit.
BOLT-11 bech32 invoices + LNURL decode. Bitcoin
PSBT v2 (BIP-370). Atomic-swap HTLC. Solana SPL /
Aleo / Aztec confidential-transfer sketches.
Smart Byte (1 crate, 2304 LOC, 30 tests)
smart-byte-acdc Authentic Chained Data Containers (KERI ecosystem).
Acdc + AcdcBuilder (targeted + untargeted) with SAID
derivation. JSON Schema 2020-12 validator with
oneOf partial-disclosure. TEL (RIP/VRT/ISS/REV)
with hash-chained verification. IPEX (apply/offer/
agree/grant/admit/spurn) chained by SAID. Edge
records (I2I/NI2I/DI2I) + EdgeGraph BFS traversal
with cycle detection. Selective disclosure with
full/none/subset plans + compact packing.
CredentialRegistry trait + InMemoryRegistry over
TEL. Composes with smart-byte-core + smart-byte-keri-witness.
EOC (1 crate, 1764 LOC, 16 tests)
eoc-memory Long-term memory primitives. Memory trait + MemoryKind +
EpisodeId. Episodic event log with TimeIndex range
queries. RDF-style Triple store with S/P/O indices.
Bounded attention-budget Scratchpad with
salience-weighted eviction. Rolling SummaryBuffer +
FIFO SlidingWindow. Dream-cycle consolidate()
(episodic → semantic). Ebbinghaus forgetting curve
with reinforcement. Retriever blending cosine
similarity + recency + saturating frequency.
MemGpt three-tier shape (Main/Recall/Archival).
Co-Authored-By: Claude Opus 4.7 <[email protected]>
171 lines
5.1 KiB
Rust
171 lines
5.1 KiB
Rust
//! Memory retrieval: similarity + recency + frequency triangulation.
|
|
//!
|
|
//! Following the classical SAM / ACT-R activation model (Anderson
|
|
//! 1983), an item's "activation" combines three signals:
|
|
//!
|
|
//! * **Similarity** — cosine-similarity to the query embedding.
|
|
//! * **Recency** — Ebbinghaus retention given the last access time.
|
|
//! * **Frequency** — saturating function of access count.
|
|
//!
|
|
//! The final score is a weighted linear combination. Weights are
|
|
//! configurable and default to `(0.6, 0.25, 0.15)`.
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::episodic::EpisodicLog;
|
|
use crate::error::{MemoryError, MemoryResult};
|
|
use crate::forget::EbbinghausScorer;
|
|
use crate::memory::{EpisodeId, Memory, MemoryItem};
|
|
|
|
/// Configuration for [`Retriever`].
|
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
pub struct RetrievalConfig {
|
|
/// Weight on cosine similarity in `[0.0, 1.0]`.
|
|
pub w_similarity: f32,
|
|
/// Weight on Ebbinghaus recency in `[0.0, 1.0]`.
|
|
pub w_recency: f32,
|
|
/// Weight on frequency in `[0.0, 1.0]`.
|
|
pub w_frequency: f32,
|
|
/// Number of items to return.
|
|
pub top_k: usize,
|
|
}
|
|
|
|
impl RetrievalConfig {
|
|
/// Construct + validate.
|
|
pub fn new(w_sim: f32, w_rec: f32, w_freq: f32, top_k: usize) -> MemoryResult<Self> {
|
|
for (name, w) in [("w_sim", w_sim), ("w_rec", w_rec), ("w_freq", w_freq)] {
|
|
if !(0.0..=1.0).contains(&w) {
|
|
return Err(MemoryError::Config(format!("{name} must be in [0,1]")));
|
|
}
|
|
}
|
|
if top_k == 0 {
|
|
return Err(MemoryError::Config("top_k must be > 0".into()));
|
|
}
|
|
Ok(Self {
|
|
w_similarity: w_sim,
|
|
w_recency: w_rec,
|
|
w_frequency: w_freq,
|
|
top_k,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Default for RetrievalConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
w_similarity: 0.6,
|
|
w_recency: 0.25,
|
|
w_frequency: 0.15,
|
|
top_k: 8,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// One scored retrieval candidate.
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
pub struct RetrievalScore {
|
|
/// Episode id of the retrieved record.
|
|
pub episode_id: EpisodeId,
|
|
/// Final blended score in `[0.0, 1.0]`.
|
|
pub score: f32,
|
|
/// Similarity sub-score (cosine).
|
|
pub sim: f32,
|
|
/// Recency sub-score (Ebbinghaus retention).
|
|
pub recency: f32,
|
|
/// Frequency sub-score (saturating).
|
|
pub frequency: f32,
|
|
}
|
|
|
|
/// Triangulating retriever over an [`EpisodicLog`].
|
|
pub struct Retriever<'a> {
|
|
log: &'a EpisodicLog,
|
|
cfg: RetrievalConfig,
|
|
scorer: EbbinghausScorer,
|
|
}
|
|
|
|
impl<'a> Retriever<'a> {
|
|
/// New retriever over `log` with `cfg` and `scorer`.
|
|
#[must_use]
|
|
pub fn new(log: &'a EpisodicLog, cfg: RetrievalConfig, scorer: EbbinghausScorer) -> Self {
|
|
Self { log, cfg, scorer }
|
|
}
|
|
|
|
/// Rank episodes against the supplied query embedding at `now_ms`.
|
|
pub fn rank(&self, query: &[f32], now_ms: u64) -> MemoryResult<Vec<RetrievalScore>> {
|
|
let mut scored: Vec<RetrievalScore> = Vec::with_capacity(self.log.len());
|
|
for ep in self.log.all() {
|
|
let sim = ep
|
|
.embedding
|
|
.as_deref()
|
|
.map(|v| cosine(query, v))
|
|
.unwrap_or(0.0);
|
|
let recency =
|
|
self.scorer
|
|
.retention(ep.timestamp_ms, now_ms, ep.access_count) as f32;
|
|
let frequency = freq_score(ep.access_count);
|
|
let score = self.cfg.w_similarity * sim
|
|
+ self.cfg.w_recency * recency
|
|
+ self.cfg.w_frequency * frequency;
|
|
scored.push(RetrievalScore {
|
|
episode_id: ep.id,
|
|
score,
|
|
sim,
|
|
recency,
|
|
frequency,
|
|
});
|
|
}
|
|
scored.sort_by(|a, b| {
|
|
b.score
|
|
.partial_cmp(&a.score)
|
|
.unwrap_or(std::cmp::Ordering::Equal)
|
|
.then_with(|| b.episode_id.0.cmp(&a.episode_id.0))
|
|
});
|
|
scored.truncate(self.cfg.top_k);
|
|
Ok(scored)
|
|
}
|
|
|
|
/// Materialise the top-K ranked episodes as [`MemoryItem`]s.
|
|
pub fn retrieve(&self, query: &[f32], now_ms: u64) -> MemoryResult<Vec<MemoryItem>> {
|
|
let scores = self.rank(query, now_ms)?;
|
|
let mut items = Vec::with_capacity(scores.len());
|
|
for s in scores {
|
|
if let Some(ep) = self.log.get(&s.episode_id) {
|
|
items.push(ep.as_item());
|
|
}
|
|
}
|
|
Ok(items)
|
|
}
|
|
}
|
|
|
|
fn cosine(a: &[f32], b: &[f32]) -> f32 {
|
|
if a.is_empty() || a.len() != b.len() {
|
|
return 0.0;
|
|
}
|
|
let mut dot = 0.0_f32;
|
|
let mut na = 0.0_f32;
|
|
let mut nb = 0.0_f32;
|
|
for i in 0..a.len() {
|
|
dot += a[i] * b[i];
|
|
na += a[i] * a[i];
|
|
nb += b[i] * b[i];
|
|
}
|
|
let denom = (na.sqrt()) * (nb.sqrt());
|
|
if denom == 0.0 {
|
|
0.0
|
|
} else {
|
|
let c = dot / denom;
|
|
if c < 0.0 {
|
|
0.0
|
|
} else if c > 1.0 {
|
|
1.0
|
|
} else {
|
|
c
|
|
}
|
|
}
|
|
}
|
|
|
|
fn freq_score(access_count: u32) -> f32 {
|
|
// Saturating: 1 - 1/(1+n)
|
|
let n = access_count as f32;
|
|
1.0 - 1.0 / (1.0 + n)
|
|
}
|