Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
148
vendor/ruvector/crates/rvf/rvf-adapters/agentic-flow/src/config.rs
vendored
Normal file
148
vendor/ruvector/crates/rvf/rvf-adapters/agentic-flow/src/config.rs
vendored
Normal 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"));
|
||||
}
|
||||
}
|
||||
283
vendor/ruvector/crates/rvf/rvf-adapters/agentic-flow/src/coordination.rs
vendored
Normal file
283
vendor/ruvector/crates/rvf/rvf-adapters/agentic-flow/src/coordination.rs
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
301
vendor/ruvector/crates/rvf/rvf-adapters/agentic-flow/src/learning.rs
vendored
Normal file
301
vendor/ruvector/crates/rvf/rvf-adapters/agentic-flow/src/learning.rs
vendored
Normal 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());
|
||||
}
|
||||
}
|
||||
53
vendor/ruvector/crates/rvf/rvf-adapters/agentic-flow/src/lib.rs
vendored
Normal file
53
vendor/ruvector/crates/rvf/rvf-adapters/agentic-flow/src/lib.rs
vendored
Normal 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,
|
||||
};
|
||||
587
vendor/ruvector/crates/rvf/rvf-adapters/agentic-flow/src/swarm_store.rs
vendored
Normal file
587
vendor/ruvector/crates/rvf/rvf-adapters/agentic-flow/src/swarm_store.rs
vendored
Normal 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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user