Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'

This commit is contained in:
ruv
2026-02-28 14:39:40 -05:00
7854 changed files with 3522914 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
[package]
name = "rvf-adapter-agentic-flow"
version = "0.1.0"
edition = "2021"
description = "Agentic-flow swarm adapter for RuVector Format -- maps inter-agent memory, coordination state, and learning patterns to RVF segments"
license = "MIT OR Apache-2.0"
repository = "https://github.com/ruvnet/ruvector"
rust-version = "1.87"
[features]
default = ["std"]
std = []
[dependencies]
rvf-runtime = { path = "../../rvf-runtime", features = ["std"] }
rvf-types = { path = "../../rvf-types", features = ["std"] }
rvf-crypto = { path = "../../rvf-crypto", features = ["std"] }
[dev-dependencies]
tempfile = "3"

View File

@@ -0,0 +1,148 @@
//! Configuration for the agentic-flow swarm adapter.
use std::path::PathBuf;
/// Configuration for the RVF-backed agentic-flow swarm store.
#[derive(Clone, Debug)]
pub struct AgenticFlowConfig {
/// Directory where RVF data files are stored.
pub data_dir: PathBuf,
/// Vector embedding dimension (must match embeddings used by agents).
pub dimension: u16,
/// Unique identifier for this agent.
pub agent_id: String,
/// Whether to log consensus events in a WITNESS_SEG audit trail.
pub enable_witness: bool,
/// Optional swarm group identifier for multi-swarm deployments.
pub swarm_id: Option<String>,
}
impl AgenticFlowConfig {
/// Create a new configuration with required parameters.
///
/// Uses sensible defaults: dimension=384, witness enabled, no swarm group.
pub fn new(data_dir: impl Into<PathBuf>, agent_id: impl Into<String>) -> Self {
Self {
data_dir: data_dir.into(),
dimension: 384,
agent_id: agent_id.into(),
enable_witness: true,
swarm_id: None,
}
}
/// Set the embedding dimension.
pub fn with_dimension(mut self, dimension: u16) -> Self {
self.dimension = dimension;
self
}
/// Enable or disable witness audit trails.
pub fn with_witness(mut self, enable: bool) -> Self {
self.enable_witness = enable;
self
}
/// Set the swarm group identifier.
pub fn with_swarm_id(mut self, swarm_id: impl Into<String>) -> Self {
self.swarm_id = Some(swarm_id.into());
self
}
/// Return the path to the main vector store RVF file.
pub fn store_path(&self) -> PathBuf {
self.data_dir.join("swarm.rvf")
}
/// Return the path to the witness chain file.
pub fn witness_path(&self) -> PathBuf {
self.data_dir.join("witness.bin")
}
/// Ensure the data directory exists.
pub fn ensure_dirs(&self) -> std::io::Result<()> {
std::fs::create_dir_all(&self.data_dir)
}
/// Validate the configuration.
pub fn validate(&self) -> Result<(), ConfigError> {
if self.dimension == 0 {
return Err(ConfigError::InvalidDimension);
}
if self.agent_id.is_empty() {
return Err(ConfigError::EmptyAgentId);
}
Ok(())
}
}
/// Errors specific to adapter configuration.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ConfigError {
/// Dimension must be > 0.
InvalidDimension,
/// Agent ID must not be empty.
EmptyAgentId,
}
impl std::fmt::Display for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidDimension => write!(f, "vector dimension must be > 0"),
Self::EmptyAgentId => write!(f, "agent_id must not be empty"),
}
}
}
impl std::error::Error for ConfigError {}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn config_defaults() {
let cfg = AgenticFlowConfig::new("/tmp/test", "agent-1");
assert_eq!(cfg.dimension, 384);
assert!(cfg.enable_witness);
assert!(cfg.swarm_id.is_none());
assert_eq!(cfg.agent_id, "agent-1");
}
#[test]
fn config_paths() {
let cfg = AgenticFlowConfig::new("/data/swarm", "a1");
assert_eq!(cfg.store_path(), Path::new("/data/swarm/swarm.rvf"));
assert_eq!(cfg.witness_path(), Path::new("/data/swarm/witness.bin"));
}
#[test]
fn validate_zero_dimension() {
let cfg = AgenticFlowConfig::new("/tmp", "a1").with_dimension(0);
assert_eq!(cfg.validate(), Err(ConfigError::InvalidDimension));
}
#[test]
fn validate_empty_agent_id() {
let cfg = AgenticFlowConfig::new("/tmp", "");
assert_eq!(cfg.validate(), Err(ConfigError::EmptyAgentId));
}
#[test]
fn validate_ok() {
let cfg = AgenticFlowConfig::new("/tmp", "agent-1").with_dimension(64);
assert!(cfg.validate().is_ok());
}
#[test]
fn builder_methods() {
let cfg = AgenticFlowConfig::new("/tmp", "a1")
.with_dimension(128)
.with_witness(false)
.with_swarm_id("swarm-alpha");
assert_eq!(cfg.dimension, 128);
assert!(!cfg.enable_witness);
assert_eq!(cfg.swarm_id.as_deref(), Some("swarm-alpha"));
}
}

View File

@@ -0,0 +1,283 @@
//! Swarm coordination state management.
//!
//! Tracks agent state changes and consensus votes in-memory, with the
//! coordination state serialized alongside the RVF store. State entries
//! and votes are appended chronologically for audit and replay.
/// A recorded agent state change.
#[derive(Clone, Debug, PartialEq)]
pub struct StateEntry {
/// The agent that produced this state change.
pub agent_id: String,
/// State key (e.g., "status", "role", "topology").
pub key: String,
/// State value (e.g., "active", "coordinator", "mesh").
pub value: String,
/// Timestamp in nanoseconds since the Unix epoch.
pub timestamp: u64,
}
/// A consensus vote cast by an agent.
#[derive(Clone, Debug, PartialEq)]
pub struct ConsensusVote {
/// The topic being voted on (e.g., "leader-election-42").
pub topic: String,
/// The agent casting the vote.
pub agent_id: String,
/// The vote (true = approve, false = reject).
pub vote: bool,
/// Timestamp in nanoseconds since the Unix epoch.
pub timestamp: u64,
}
/// Swarm coordination state tracker.
///
/// Maintains an in-memory log of agent state changes and consensus votes.
/// This state lives alongside the RVF store and is used for coordination
/// protocol decisions (leader election, topology changes, etc.).
pub struct SwarmCoordination {
states: Vec<StateEntry>,
votes: Vec<ConsensusVote>,
}
impl SwarmCoordination {
/// Create a new, empty coordination tracker.
pub fn new() -> Self {
Self {
states: Vec::new(),
votes: Vec::new(),
}
}
/// Record an agent state change.
pub fn record_state(
&mut self,
agent_id: &str,
state_key: &str,
state_value: &str,
) -> Result<(), CoordinationError> {
if agent_id.is_empty() {
return Err(CoordinationError::EmptyAgentId);
}
if state_key.is_empty() {
return Err(CoordinationError::EmptyKey);
}
self.states.push(StateEntry {
agent_id: agent_id.to_string(),
key: state_key.to_string(),
value: state_value.to_string(),
timestamp: now_ns(),
});
Ok(())
}
/// Get the state history for a specific agent.
pub fn get_agent_states(&self, agent_id: &str) -> Vec<StateEntry> {
self.states
.iter()
.filter(|s| s.agent_id == agent_id)
.cloned()
.collect()
}
/// Get all coordination state entries.
pub fn get_all_states(&self) -> Vec<StateEntry> {
self.states.clone()
}
/// Record a consensus vote for a topic.
pub fn record_consensus_vote(
&mut self,
topic: &str,
agent_id: &str,
vote: bool,
) -> Result<(), CoordinationError> {
if topic.is_empty() {
return Err(CoordinationError::EmptyTopic);
}
if agent_id.is_empty() {
return Err(CoordinationError::EmptyAgentId);
}
self.votes.push(ConsensusVote {
topic: topic.to_string(),
agent_id: agent_id.to_string(),
vote,
timestamp: now_ns(),
});
Ok(())
}
/// Get all votes for a specific topic.
pub fn get_votes(&self, topic: &str) -> Vec<ConsensusVote> {
self.votes
.iter()
.filter(|v| v.topic == topic)
.cloned()
.collect()
}
/// Get the total number of state entries.
pub fn state_count(&self) -> usize {
self.states.len()
}
/// Get the total number of votes.
pub fn vote_count(&self) -> usize {
self.votes.len()
}
}
impl Default for SwarmCoordination {
fn default() -> Self {
Self::new()
}
}
/// Errors from coordination operations.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CoordinationError {
/// Agent ID must not be empty.
EmptyAgentId,
/// State key must not be empty.
EmptyKey,
/// Topic must not be empty.
EmptyTopic,
}
impl std::fmt::Display for CoordinationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::EmptyAgentId => write!(f, "agent_id must not be empty"),
Self::EmptyKey => write!(f, "state key must not be empty"),
Self::EmptyTopic => write!(f, "topic must not be empty"),
}
}
}
impl std::error::Error for CoordinationError {}
fn now_ns() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn record_and_get_states() {
let mut coord = SwarmCoordination::new();
coord.record_state("a1", "status", "active").unwrap();
coord.record_state("a2", "status", "idle").unwrap();
coord.record_state("a1", "role", "coordinator").unwrap();
let a1_states = coord.get_agent_states("a1");
assert_eq!(a1_states.len(), 2);
assert_eq!(a1_states[0].key, "status");
assert_eq!(a1_states[1].key, "role");
let a2_states = coord.get_agent_states("a2");
assert_eq!(a2_states.len(), 1);
}
#[test]
fn get_all_states() {
let mut coord = SwarmCoordination::new();
coord.record_state("a1", "k1", "v1").unwrap();
coord.record_state("a2", "k2", "v2").unwrap();
let all = coord.get_all_states();
assert_eq!(all.len(), 2);
}
#[test]
fn record_and_get_votes() {
let mut coord = SwarmCoordination::new();
coord
.record_consensus_vote("leader-election", "a1", true)
.unwrap();
coord
.record_consensus_vote("leader-election", "a2", false)
.unwrap();
coord
.record_consensus_vote("other-topic", "a1", true)
.unwrap();
let votes = coord.get_votes("leader-election");
assert_eq!(votes.len(), 2);
assert!(votes[0].vote);
assert!(!votes[1].vote);
let other = coord.get_votes("other-topic");
assert_eq!(other.len(), 1);
}
#[test]
fn empty_agent_id_rejected() {
let mut coord = SwarmCoordination::new();
assert_eq!(
coord.record_state("", "k", "v"),
Err(CoordinationError::EmptyAgentId)
);
assert_eq!(
coord.record_consensus_vote("topic", "", true),
Err(CoordinationError::EmptyAgentId)
);
}
#[test]
fn empty_key_rejected() {
let mut coord = SwarmCoordination::new();
assert_eq!(
coord.record_state("a1", "", "v"),
Err(CoordinationError::EmptyKey)
);
}
#[test]
fn empty_topic_rejected() {
let mut coord = SwarmCoordination::new();
assert_eq!(
coord.record_consensus_vote("", "a1", true),
Err(CoordinationError::EmptyTopic)
);
}
#[test]
fn counts() {
let mut coord = SwarmCoordination::new();
assert_eq!(coord.state_count(), 0);
assert_eq!(coord.vote_count(), 0);
coord.record_state("a1", "k", "v").unwrap();
coord.record_consensus_vote("t", "a1", true).unwrap();
assert_eq!(coord.state_count(), 1);
assert_eq!(coord.vote_count(), 1);
}
#[test]
fn no_states_for_unknown_agent() {
let coord = SwarmCoordination::new();
assert!(coord.get_agent_states("ghost").is_empty());
}
#[test]
fn no_votes_for_unknown_topic() {
let coord = SwarmCoordination::new();
assert!(coord.get_votes("nonexistent").is_empty());
}
#[test]
fn timestamps_are_monotonic() {
let mut coord = SwarmCoordination::new();
coord.record_state("a1", "k1", "v1").unwrap();
coord.record_state("a1", "k2", "v2").unwrap();
let states = coord.get_agent_states("a1");
assert!(states[0].timestamp <= states[1].timestamp);
}
}

View File

@@ -0,0 +1,301 @@
//! Agent learning pattern management.
//!
//! Stores learned patterns as vectors with metadata (pattern type, description,
//! effectiveness score) in the RVF store. Patterns can be searched by embedding
//! similarity and ranked by their effectiveness scores.
use std::collections::HashMap;
/// A learning pattern search result.
#[derive(Clone, Debug)]
pub struct PatternResult {
/// Unique pattern identifier.
pub id: u64,
/// The cognitive pattern type (e.g., "convergent", "divergent", "lateral").
pub pattern_type: String,
/// Human-readable description of the pattern.
pub description: String,
/// Effectiveness score (0.0 - 1.0).
pub score: f32,
/// Distance from query embedding (only meaningful in search results).
pub distance: f32,
}
/// In-memory metadata for a stored pattern.
#[derive(Clone, Debug)]
struct PatternMeta {
pattern_type: String,
description: String,
score: f32,
}
/// Agent learning pattern store.
///
/// Wraps a vector store to provide pattern-specific operations: store, search,
/// update scores, and retrieve top patterns. Each pattern has a type, description,
/// effectiveness score, and an embedding vector for similarity search.
pub struct LearningPatternStore {
patterns: HashMap<u64, PatternMeta>,
/// Ordered list of (score, id) for efficient top-k retrieval.
score_index: Vec<(f32, u64)>,
next_id: u64,
}
impl LearningPatternStore {
/// Create a new, empty learning pattern store.
pub fn new() -> Self {
Self {
patterns: HashMap::new(),
score_index: Vec::new(),
next_id: 1,
}
}
/// Store a learned pattern.
///
/// The `embedding` is stored in the parent `RvfSwarmStore` via metadata;
/// this struct tracks the pattern metadata for filtering and ranking.
///
/// Returns the assigned pattern ID.
pub fn store_pattern(
&mut self,
pattern_type: &str,
description: &str,
score: f32,
) -> Result<u64, LearningError> {
if pattern_type.is_empty() {
return Err(LearningError::EmptyPatternType);
}
let clamped_score = score.clamp(0.0, 1.0);
let id = self.next_id;
self.next_id += 1;
self.patterns.insert(
id,
PatternMeta {
pattern_type: pattern_type.to_string(),
description: description.to_string(),
score: clamped_score,
},
);
self.score_index.push((clamped_score, id));
Ok(id)
}
/// Search patterns by returning those whose IDs are in the given candidate
/// set (from a vector similarity search), enriched with metadata.
pub fn enrich_results(
&self,
candidates: &[(u64, f32)],
k: usize,
) -> Vec<PatternResult> {
let mut results: Vec<PatternResult> = candidates
.iter()
.filter_map(|&(id, distance)| {
let meta = self.patterns.get(&id)?;
Some(PatternResult {
id,
pattern_type: meta.pattern_type.clone(),
description: meta.description.clone(),
score: meta.score,
distance,
})
})
.collect();
results.truncate(k);
results
}
/// Update the effectiveness score for a pattern.
pub fn update_score(&mut self, id: u64, new_score: f32) -> Result<(), LearningError> {
let meta = self
.patterns
.get_mut(&id)
.ok_or(LearningError::PatternNotFound(id))?;
let clamped = new_score.clamp(0.0, 1.0);
meta.score = clamped;
// Update the score index entry.
if let Some(entry) = self.score_index.iter_mut().find(|(_, eid)| *eid == id) {
entry.0 = clamped;
}
Ok(())
}
/// Get the top-k patterns by effectiveness score (highest first).
pub fn get_top_patterns(&self, k: usize) -> Vec<PatternResult> {
let mut sorted = self.score_index.clone();
sorted.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
sorted.truncate(k);
sorted
.into_iter()
.filter_map(|(_, id)| {
let meta = self.patterns.get(&id)?;
Some(PatternResult {
id,
pattern_type: meta.pattern_type.clone(),
description: meta.description.clone(),
score: meta.score,
distance: 0.0,
})
})
.collect()
}
/// Get a pattern by ID.
pub fn get_pattern(&self, id: u64) -> Option<PatternResult> {
let meta = self.patterns.get(&id)?;
Some(PatternResult {
id,
pattern_type: meta.pattern_type.clone(),
description: meta.description.clone(),
score: meta.score,
distance: 0.0,
})
}
/// Get the total number of stored patterns.
pub fn len(&self) -> usize {
self.patterns.len()
}
/// Returns true if no patterns are stored.
pub fn is_empty(&self) -> bool {
self.patterns.is_empty()
}
}
impl Default for LearningPatternStore {
fn default() -> Self {
Self::new()
}
}
/// Errors from learning pattern operations.
#[derive(Clone, Debug, PartialEq)]
pub enum LearningError {
/// Pattern type must not be empty.
EmptyPatternType,
/// Pattern with the given ID was not found.
PatternNotFound(u64),
}
impl std::fmt::Display for LearningError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::EmptyPatternType => write!(f, "pattern_type must not be empty"),
Self::PatternNotFound(id) => write!(f, "pattern not found: {id}"),
}
}
}
impl std::error::Error for LearningError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn store_and_retrieve() {
let mut store = LearningPatternStore::new();
let id = store.store_pattern("convergent", "Use batched writes", 0.85).unwrap();
let p = store.get_pattern(id).unwrap();
assert_eq!(p.pattern_type, "convergent");
assert_eq!(p.description, "Use batched writes");
assert!((p.score - 0.85).abs() < f32::EPSILON);
}
#[test]
fn update_score() {
let mut store = LearningPatternStore::new();
let id = store.store_pattern("lateral", "Try alternative approach", 0.5).unwrap();
store.update_score(id, 0.95).unwrap();
let p = store.get_pattern(id).unwrap();
assert!((p.score - 0.95).abs() < f32::EPSILON);
}
#[test]
fn update_nonexistent_pattern() {
let mut store = LearningPatternStore::new();
assert_eq!(
store.update_score(999, 0.5),
Err(LearningError::PatternNotFound(999))
);
}
#[test]
fn top_patterns() {
let mut store = LearningPatternStore::new();
store.store_pattern("a", "low", 0.2).unwrap();
store.store_pattern("b", "mid", 0.5).unwrap();
store.store_pattern("c", "high", 0.9).unwrap();
store.store_pattern("d", "highest", 1.0).unwrap();
let top = store.get_top_patterns(2);
assert_eq!(top.len(), 2);
assert!((top[0].score - 1.0).abs() < f32::EPSILON);
assert!((top[1].score - 0.9).abs() < f32::EPSILON);
}
#[test]
fn score_clamping() {
let mut store = LearningPatternStore::new();
let id1 = store.store_pattern("a", "over", 1.5).unwrap();
let id2 = store.store_pattern("b", "under", -0.3).unwrap();
assert!((store.get_pattern(id1).unwrap().score - 1.0).abs() < f32::EPSILON);
assert!(store.get_pattern(id2).unwrap().score.abs() < f32::EPSILON);
}
#[test]
fn empty_pattern_type_rejected() {
let mut store = LearningPatternStore::new();
assert_eq!(
store.store_pattern("", "desc", 0.5),
Err(LearningError::EmptyPatternType)
);
}
#[test]
fn enrich_results() {
let mut store = LearningPatternStore::new();
let id1 = store.store_pattern("convergent", "desc1", 0.8).unwrap();
let id2 = store.store_pattern("divergent", "desc2", 0.6).unwrap();
let _id3 = store.store_pattern("lateral", "desc3", 0.4).unwrap();
let candidates = vec![(id1, 0.1), (id2, 0.3), (999, 0.5)];
let results = store.enrich_results(&candidates, 10);
// id 999 is filtered out (not in patterns map)
assert_eq!(results.len(), 2);
assert_eq!(results[0].id, id1);
assert_eq!(results[1].id, id2);
}
#[test]
fn len_and_is_empty() {
let mut store = LearningPatternStore::new();
assert!(store.is_empty());
assert_eq!(store.len(), 0);
store.store_pattern("a", "desc", 0.5).unwrap();
assert!(!store.is_empty());
assert_eq!(store.len(), 1);
}
#[test]
fn get_nonexistent_pattern() {
let store = LearningPatternStore::new();
assert!(store.get_pattern(42).is_none());
}
#[test]
fn top_patterns_empty_store() {
let store = LearningPatternStore::new();
assert!(store.get_top_patterns(5).is_empty());
}
}

View File

@@ -0,0 +1,53 @@
//! RVF adapter for agentic-flow swarm coordination.
//!
//! This crate bridges agentic-flow's swarm coordination primitives with the
//! RuVector Format (RVF) segment store, per ADR-029. It provides persistent
//! storage for inter-agent memory sharing, swarm coordination state, and
//! agent learning patterns.
//!
//! # Segment mapping
//!
//! - **VEC_SEG + META_SEG**: Shared memory entries (embeddings + key/value
//! metadata) for inter-agent memory sharing via the RVF streaming protocol.
//! - **META_SEG**: Swarm coordination state (agent states, topology changes).
//! - **SKETCH_SEG**: Agent learning patterns with effectiveness scores.
//! - **WITNESS_SEG**: Distributed consensus votes with signatures for
//! tamper-evident audit trails.
//!
//! # Usage
//!
//! ```rust,no_run
//! use rvf_adapter_agentic_flow::{AgenticFlowConfig, RvfSwarmStore};
//!
//! let config = AgenticFlowConfig::new("/tmp/swarm-data", "agent-001");
//! let mut store = RvfSwarmStore::create(config).unwrap();
//!
//! // Share a memory entry with other agents
//! let embedding = vec![0.1f32; 384];
//! store.share_memory("auth-pattern", "JWT with refresh tokens",
//! "patterns", &embedding).unwrap();
//!
//! // Search shared memories by embedding similarity
//! let results = store.search_shared(&embedding, 5);
//!
//! // Record coordination state
//! store.coordination().record_state("agent-001", "status", "active").unwrap();
//!
//! // Store a learning pattern
//! store.learning().store_pattern("convergent", "Use batched writes",
//! 0.92).unwrap();
//!
//! store.close().unwrap();
//! ```
pub mod config;
pub mod coordination;
pub mod learning;
pub mod swarm_store;
pub use config::{AgenticFlowConfig, ConfigError};
pub use coordination::{ConsensusVote, StateEntry, SwarmCoordination};
pub use learning::{LearningPatternStore, PatternResult};
pub use swarm_store::{
RvfSwarmStore, SharedMemoryEntry, SharedMemoryResult, SwarmStoreError,
};

View File

@@ -0,0 +1,587 @@
//! `RvfSwarmStore` -- main API wrapping `RvfStore` for swarm operations.
//!
//! Maps agentic-flow's inter-agent memory sharing model onto the RVF
//! segment model:
//! - Embeddings are stored as vectors via `ingest_batch`
//! - Agent ID, key, value, and namespace are encoded as metadata fields
//! - Searches use `query` with optional namespace filtering
//! - Coordination state and learning patterns are managed by sub-stores
use std::collections::HashMap;
use rvf_runtime::options::{
DistanceMetric, MetadataEntry, MetadataValue, QueryOptions, RvfOptions,
};
use rvf_runtime::RvfStore;
use rvf_types::RvfError;
use crate::config::{AgenticFlowConfig, ConfigError};
use crate::coordination::SwarmCoordination;
use crate::learning::LearningPatternStore;
/// Metadata field IDs for shared memory entries.
const FIELD_AGENT_ID: u16 = 0;
const FIELD_KEY: u16 = 1;
const FIELD_VALUE: u16 = 2;
const FIELD_NAMESPACE: u16 = 3;
/// A search result from shared memory, enriched with agent metadata.
#[derive(Clone, Debug)]
pub struct SharedMemoryResult {
/// Vector ID in the underlying store.
pub id: u64,
/// Distance from the query embedding (lower = more similar).
pub distance: f32,
/// The agent that shared this memory.
pub agent_id: String,
/// The memory key.
pub key: String,
}
/// A full shared memory entry retrieved by ID.
#[derive(Clone, Debug)]
pub struct SharedMemoryEntry {
/// Vector ID in the underlying store.
pub id: u64,
/// The agent that shared this memory.
pub agent_id: String,
/// The memory key.
pub key: String,
/// The memory value.
pub value: String,
/// The namespace this entry belongs to.
pub namespace: String,
}
/// The RVF-backed swarm store for agentic-flow.
pub struct RvfSwarmStore {
store: RvfStore,
config: AgenticFlowConfig,
coordination: SwarmCoordination,
learning: LearningPatternStore,
/// Maps "agent_id/namespace/key" -> vector_id for fast lookup.
key_index: HashMap<String, u64>,
/// Maps vector_id -> SharedMemoryEntry for retrieval by ID.
entry_index: HashMap<u64, SharedMemoryEntry>,
/// Next vector ID to assign.
next_id: u64,
}
impl RvfSwarmStore {
/// Create a new swarm store, initializing the data directory and RVF file.
pub fn create(config: AgenticFlowConfig) -> Result<Self, SwarmStoreError> {
config.validate().map_err(SwarmStoreError::Config)?;
config
.ensure_dirs()
.map_err(|e| SwarmStoreError::Io(e.to_string()))?;
let rvf_options = RvfOptions {
dimension: config.dimension,
metric: DistanceMetric::Cosine,
..Default::default()
};
let store = RvfStore::create(&config.store_path(), rvf_options)
.map_err(SwarmStoreError::Rvf)?;
Ok(Self {
store,
config,
coordination: SwarmCoordination::new(),
learning: LearningPatternStore::new(),
key_index: HashMap::new(),
entry_index: HashMap::new(),
next_id: 1,
})
}
/// Open an existing swarm store.
pub fn open(config: AgenticFlowConfig) -> Result<Self, SwarmStoreError> {
config.validate().map_err(SwarmStoreError::Config)?;
let store =
RvfStore::open(&config.store_path()).map_err(SwarmStoreError::Rvf)?;
// Rebuild next_id from the store status so new IDs don't collide.
let status = store.status();
let next_id = status.total_vectors + status.current_epoch as u64 + 1;
Ok(Self {
store,
config,
coordination: SwarmCoordination::new(),
learning: LearningPatternStore::new(),
key_index: HashMap::new(),
entry_index: HashMap::new(),
next_id,
})
}
/// Share a memory entry with other agents.
///
/// Stores the embedding vector with agent_id/key/value/namespace as
/// metadata fields. If an entry with the same agent_id/namespace/key
/// already exists, the old one is soft-deleted and replaced.
///
/// Returns the assigned vector ID.
pub fn share_memory(
&mut self,
key: &str,
value: &str,
namespace: &str,
embedding: &[f32],
) -> Result<u64, SwarmStoreError> {
if embedding.len() != self.config.dimension as usize {
return Err(SwarmStoreError::DimensionMismatch {
expected: self.config.dimension as usize,
got: embedding.len(),
});
}
let compound_key = format!(
"{}/{}/{}",
self.config.agent_id, namespace, key
);
// Soft-delete existing entry with the same compound key.
if let Some(&old_id) = self.key_index.get(&compound_key) {
self.store.delete(&[old_id]).map_err(SwarmStoreError::Rvf)?;
self.entry_index.remove(&old_id);
}
let vector_id = self.next_id;
self.next_id += 1;
let metadata = vec![
MetadataEntry {
field_id: FIELD_AGENT_ID,
value: MetadataValue::String(self.config.agent_id.clone()),
},
MetadataEntry {
field_id: FIELD_KEY,
value: MetadataValue::String(key.to_string()),
},
MetadataEntry {
field_id: FIELD_VALUE,
value: MetadataValue::String(value.to_string()),
},
MetadataEntry {
field_id: FIELD_NAMESPACE,
value: MetadataValue::String(namespace.to_string()),
},
];
self.store
.ingest_batch(&[embedding], &[vector_id], Some(&metadata))
.map_err(SwarmStoreError::Rvf)?;
self.key_index.insert(compound_key, vector_id);
self.entry_index.insert(
vector_id,
SharedMemoryEntry {
id: vector_id,
agent_id: self.config.agent_id.clone(),
key: key.to_string(),
value: value.to_string(),
namespace: namespace.to_string(),
},
);
Ok(vector_id)
}
/// Search for shared memories similar to the given embedding.
///
/// Returns up to `k` results sorted by distance (closest first),
/// enriched with agent metadata from the in-memory index.
pub fn search_shared(
&self,
embedding: &[f32],
k: usize,
) -> Vec<SharedMemoryResult> {
let options = QueryOptions::default();
let results = match self.store.query(embedding, k, &options) {
Ok(r) => r,
Err(_) => return Vec::new(),
};
results
.into_iter()
.filter_map(|r| {
let entry = self.entry_index.get(&r.id)?;
Some(SharedMemoryResult {
id: r.id,
distance: r.distance,
agent_id: entry.agent_id.clone(),
key: entry.key.clone(),
})
})
.collect()
}
/// Retrieve a shared memory entry by its vector ID.
pub fn get_shared(&self, id: u64) -> Option<SharedMemoryEntry> {
self.entry_index.get(&id).cloned()
}
/// Delete shared memory entries by their vector IDs.
///
/// Returns the number of entries actually deleted.
pub fn delete_shared(&mut self, ids: &[u64]) -> Result<usize, SwarmStoreError> {
let existing: Vec<u64> = ids
.iter()
.filter(|id| self.entry_index.contains_key(id))
.copied()
.collect();
if existing.is_empty() {
return Ok(0);
}
self.store
.delete(&existing)
.map_err(SwarmStoreError::Rvf)?;
let mut removed = 0;
for &id in &existing {
if let Some(entry) = self.entry_index.remove(&id) {
let compound_key = format!(
"{}/{}/{}",
entry.agent_id, entry.namespace, entry.key
);
self.key_index.remove(&compound_key);
removed += 1;
}
}
Ok(removed)
}
/// Get a mutable reference to the coordination state tracker.
pub fn coordination(&mut self) -> &mut SwarmCoordination {
&mut self.coordination
}
/// Get an immutable reference to the coordination state tracker.
pub fn coordination_ref(&self) -> &SwarmCoordination {
&self.coordination
}
/// Get a mutable reference to the learning pattern store.
pub fn learning(&mut self) -> &mut LearningPatternStore {
&mut self.learning
}
/// Get an immutable reference to the learning pattern store.
pub fn learning_ref(&self) -> &LearningPatternStore {
&self.learning
}
/// Get the current store status.
pub fn status(&self) -> rvf_runtime::StoreStatus {
self.store.status()
}
/// Get the agent ID for this store.
pub fn agent_id(&self) -> &str {
&self.config.agent_id
}
/// Close the swarm store, releasing locks.
pub fn close(self) -> Result<(), SwarmStoreError> {
self.store.close().map_err(SwarmStoreError::Rvf)
}
}
/// Errors from swarm store operations.
#[derive(Debug)]
pub enum SwarmStoreError {
/// Underlying RVF store error.
Rvf(RvfError),
/// Configuration error.
Config(ConfigError),
/// I/O error.
Io(String),
/// Embedding dimension mismatch.
DimensionMismatch { expected: usize, got: usize },
}
impl std::fmt::Display for SwarmStoreError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Rvf(e) => write!(f, "RVF store error: {e}"),
Self::Config(e) => write!(f, "config error: {e}"),
Self::Io(msg) => write!(f, "I/O error: {msg}"),
Self::DimensionMismatch { expected, got } => {
write!(f, "dimension mismatch: expected {expected}, got {got}")
}
}
}
}
impl std::error::Error for SwarmStoreError {}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn test_config(dir: &std::path::Path) -> AgenticFlowConfig {
AgenticFlowConfig::new(dir, "test-agent").with_dimension(4)
}
fn make_embedding(seed: f32) -> Vec<f32> {
vec![seed, seed * 0.5, seed * 0.25, seed * 0.125]
}
#[test]
fn create_and_share() {
let dir = TempDir::new().unwrap();
let config = test_config(dir.path());
let mut store = RvfSwarmStore::create(config).unwrap();
let id = store
.share_memory("key1", "value1", "default", &make_embedding(1.0))
.unwrap();
assert!(id > 0);
let status = store.status();
assert_eq!(status.total_vectors, 1);
store.close().unwrap();
}
#[test]
fn share_and_search() {
let dir = TempDir::new().unwrap();
let config = test_config(dir.path());
let mut store = RvfSwarmStore::create(config).unwrap();
store
.share_memory("a", "val_a", "ns1", &[1.0, 0.0, 0.0, 0.0])
.unwrap();
store
.share_memory("b", "val_b", "ns1", &[0.0, 1.0, 0.0, 0.0])
.unwrap();
store
.share_memory("c", "val_c", "ns2", &[0.0, 0.0, 1.0, 0.0])
.unwrap();
let results = store.search_shared(&[1.0, 0.0, 0.0, 0.0], 3);
assert_eq!(results.len(), 3);
// Closest should be "a"
assert_eq!(results[0].key, "a");
store.close().unwrap();
}
#[test]
fn get_shared_by_id() {
let dir = TempDir::new().unwrap();
let config = test_config(dir.path());
let mut store = RvfSwarmStore::create(config).unwrap();
let id = store
.share_memory("mykey", "myval", "ns", &make_embedding(2.0))
.unwrap();
let entry = store.get_shared(id).unwrap();
assert_eq!(entry.key, "mykey");
assert_eq!(entry.value, "myval");
assert_eq!(entry.namespace, "ns");
assert_eq!(entry.agent_id, "test-agent");
assert!(store.get_shared(9999).is_none());
store.close().unwrap();
}
#[test]
fn delete_shared_entries() {
let dir = TempDir::new().unwrap();
let config = test_config(dir.path());
let mut store = RvfSwarmStore::create(config).unwrap();
let id1 = store
.share_memory("k1", "v1", "ns", &make_embedding(1.0))
.unwrap();
let id2 = store
.share_memory("k2", "v2", "ns", &make_embedding(2.0))
.unwrap();
let removed = store.delete_shared(&[id1]).unwrap();
assert_eq!(removed, 1);
assert!(store.get_shared(id1).is_none());
assert!(store.get_shared(id2).is_some());
store.close().unwrap();
}
#[test]
fn delete_nonexistent_ids() {
let dir = TempDir::new().unwrap();
let config = test_config(dir.path());
let mut store = RvfSwarmStore::create(config).unwrap();
let removed = store.delete_shared(&[999, 1000]).unwrap();
assert_eq!(removed, 0);
store.close().unwrap();
}
#[test]
fn replace_existing_key() {
let dir = TempDir::new().unwrap();
let config = test_config(dir.path());
let mut store = RvfSwarmStore::create(config).unwrap();
let id1 = store
.share_memory("k", "v1", "ns", &make_embedding(1.0))
.unwrap();
let id2 = store
.share_memory("k", "v2", "ns", &make_embedding(2.0))
.unwrap();
assert_ne!(id1, id2);
assert!(store.get_shared(id1).is_none());
let entry = store.get_shared(id2).unwrap();
assert_eq!(entry.value, "v2");
let status = store.status();
assert_eq!(status.total_vectors, 1);
store.close().unwrap();
}
#[test]
fn dimension_mismatch() {
let dir = TempDir::new().unwrap();
let config = test_config(dir.path());
let mut store = RvfSwarmStore::create(config).unwrap();
let result = store.share_memory("k", "v", "ns", &[1.0, 2.0]);
assert!(result.is_err());
}
#[test]
fn coordination_state() {
let dir = TempDir::new().unwrap();
let config = test_config(dir.path());
let mut store = RvfSwarmStore::create(config).unwrap();
store
.coordination()
.record_state("agent-1", "status", "active")
.unwrap();
store
.coordination()
.record_state("agent-2", "status", "idle")
.unwrap();
let states = store.coordination_ref().get_all_states();
assert_eq!(states.len(), 2);
store.close().unwrap();
}
#[test]
fn learning_patterns() {
let dir = TempDir::new().unwrap();
let config = test_config(dir.path());
let mut store = RvfSwarmStore::create(config).unwrap();
let id = store
.learning()
.store_pattern("convergent", "Use batched writes", 0.85)
.unwrap();
let pattern = store.learning_ref().get_pattern(id).unwrap();
assert_eq!(pattern.pattern_type, "convergent");
assert!((pattern.score - 0.85).abs() < f32::EPSILON);
store.close().unwrap();
}
#[test]
fn open_existing_store() {
let dir = TempDir::new().unwrap();
let config = test_config(dir.path());
{
let mut store = RvfSwarmStore::create(config.clone()).unwrap();
store
.share_memory("k", "v", "ns", &make_embedding(1.0))
.unwrap();
store.close().unwrap();
}
{
let store = RvfSwarmStore::open(config).unwrap();
let status = store.status();
assert_eq!(status.total_vectors, 1);
store.close().unwrap();
}
}
#[test]
fn agent_id_accessor() {
let dir = TempDir::new().unwrap();
let config = AgenticFlowConfig::new(dir.path(), "special-agent")
.with_dimension(4);
let store = RvfSwarmStore::create(config).unwrap();
assert_eq!(store.agent_id(), "special-agent");
store.close().unwrap();
}
#[test]
fn empty_store_search() {
let dir = TempDir::new().unwrap();
let config = test_config(dir.path());
let store = RvfSwarmStore::create(config).unwrap();
let results = store.search_shared(&[1.0, 0.0, 0.0, 0.0], 5);
assert!(results.is_empty());
store.close().unwrap();
}
#[test]
fn consensus_votes() {
let dir = TempDir::new().unwrap();
let config = test_config(dir.path());
let mut store = RvfSwarmStore::create(config).unwrap();
store
.coordination()
.record_consensus_vote("leader-election", "a1", true)
.unwrap();
store
.coordination()
.record_consensus_vote("leader-election", "a2", false)
.unwrap();
let votes = store.coordination_ref().get_votes("leader-election");
assert_eq!(votes.len(), 2);
assert!(votes[0].vote);
assert!(!votes[1].vote);
store.close().unwrap();
}
#[test]
fn invalid_config_rejected() {
let dir = TempDir::new().unwrap();
// Zero dimension
let config = AgenticFlowConfig::new(dir.path(), "a1").with_dimension(0);
assert!(RvfSwarmStore::create(config).is_err());
// Empty agent_id
let config = AgenticFlowConfig::new(dir.path(), "").with_dimension(4);
assert!(RvfSwarmStore::create(config).is_err());
}
}