feat(swarm): integrate Ruflo AI-agent capabilities into ruview-swarm
Adds a feature-gated Ruflo integration layer connecting ruview-swarm to the claude-flow daemon's AgentDB, AIDefence, and SONA intelligence subsystems. Default build is unaffected (all paths behind `Option<Box<dyn RufloBackend>>`). ## New module: src/ruflo/ - backend.rs: RufloBackend trait (9 async methods) + RufloError, MissionMemoryEntry, PatternEntry, MavlinkScanResult types (always compiled) - mock_backend.rs: MockRufloBackend in-memory impl for testing (always compiled, 5 tests) - http_backend.rs: HttpRufloBackend — JSON-RPC 2.0 → claude-flow daemon localhost:3000 (gated behind `ruflo` feature, requires reqwest) - mission_summary.rs: MissionSummary serializer with pattern description + confidence scoring from victim recall, coverage %, collision penalty (always compiled, 3 tests) ## 4 capability areas 1. MissionMemory → memory_store / memory_search (cross-mission victim memory) 2. PatternLearner → agentdb_pattern-store / -search (HNSW SONA trajectory patterns) 3. MavlinkDefence → aidefence_is_safe / aidefence_scan (scan MAVLink before accepting) 4. IntelligenceHooks → trajectory-start/step/end (SONA learning loop) ## SwarmOrchestrator integration - with_ruflo(backend): builder to attach a backend - start_trajectory(task) / finish_trajectory(success, key): SONA mission lifecycle - receive_peer_detection_checked(): AIDefence scan before accepting peer detections ## Cargo feature `ruflo = ["dep:reqwest", "dep:serde_json"]` — optional, not in default ## Tests - --no-default-features: 82/82 pass (8 new ruflo tests) - --features ruflo,itar-unrestricted: 94/94 pass Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
parent
b392aded6c
commit
4aee5c9fb1
|
|
@ -3214,6 +3214,25 @@ dependencies = [
|
|||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
"fnv",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"http 1.4.0",
|
||||
"indexmap 2.13.0",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "half"
|
||||
version = "2.7.1"
|
||||
|
|
@ -3676,7 +3695,7 @@ dependencies = [
|
|||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"h2 0.3.27",
|
||||
"http 0.2.12",
|
||||
"http-body 0.4.6",
|
||||
"httparse",
|
||||
|
|
@ -3700,6 +3719,7 @@ dependencies = [
|
|||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"h2 0.4.14",
|
||||
"http 1.4.0",
|
||||
"http-body 1.0.1",
|
||||
"httparse",
|
||||
|
|
@ -3726,6 +3746,21 @@ dependencies = [
|
|||
"tokio-rustls 0.24.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-rustls"
|
||||
version = "0.27.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
|
||||
dependencies = [
|
||||
"http 1.4.0",
|
||||
"hyper 1.8.1",
|
||||
"hyper-util",
|
||||
"rustls 0.23.37",
|
||||
"tokio",
|
||||
"tokio-rustls 0.26.4",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-tls"
|
||||
version = "0.6.0"
|
||||
|
|
@ -3760,9 +3795,11 @@ dependencies = [
|
|||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2 0.6.2",
|
||||
"system-configuration 0.7.0",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -6769,11 +6806,11 @@ dependencies = [
|
|||
"encoding_rs",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"h2 0.3.27",
|
||||
"http 0.2.12",
|
||||
"http-body 0.4.6",
|
||||
"hyper 0.14.32",
|
||||
"hyper-rustls",
|
||||
"hyper-rustls 0.24.2",
|
||||
"ipnet",
|
||||
"js-sys",
|
||||
"log",
|
||||
|
|
@ -6787,7 +6824,7 @@ dependencies = [
|
|||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper 0.1.2",
|
||||
"system-configuration",
|
||||
"system-configuration 0.5.1",
|
||||
"tokio",
|
||||
"tokio-rustls 0.24.1",
|
||||
"tower-service",
|
||||
|
|
@ -6807,16 +6844,20 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
|||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2 0.4.14",
|
||||
"http 1.4.0",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"hyper 1.8.1",
|
||||
"hyper-rustls 0.27.9",
|
||||
"hyper-tls",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"native-tls",
|
||||
"percent-encoding",
|
||||
|
|
@ -7426,7 +7467,9 @@ dependencies = [
|
|||
"nalgebra",
|
||||
"ort",
|
||||
"rand 0.8.5",
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
|
|
@ -8560,7 +8603,18 @@ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
|
|||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation 0.9.4",
|
||||
"system-configuration-sys",
|
||||
"system-configuration-sys 0.5.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"core-foundation 0.9.4",
|
||||
"system-configuration-sys 0.6.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -8573,6 +8627,16 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration-sys"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-deps"
|
||||
version = "6.2.2"
|
||||
|
|
@ -9227,6 +9291,16 @@ dependencies = [
|
|||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
|
||||
dependencies = [
|
||||
"rustls 0.23.37",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-serial"
|
||||
version = "5.4.5"
|
||||
|
|
@ -11365,6 +11439,17 @@ dependencies = [
|
|||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
|
||||
dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
"windows-result 0.4.1",
|
||||
"windows-strings 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.1.2"
|
||||
|
|
|
|||
|
|
@ -16,12 +16,14 @@ onnx = ["dep:ort"]
|
|||
simulation = []
|
||||
demo = ["simulation"]
|
||||
full = ["mavlink", "onnx", "demo", "itar-unrestricted"]
|
||||
ruflo = ["dep:reqwest", "dep:serde_json"]
|
||||
|
||||
[dependencies]
|
||||
wifi-densepose-core = { path = "../wifi-densepose-core" }
|
||||
|
||||
# Serialization
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = { version = "1", optional = true }
|
||||
toml = "0.8"
|
||||
|
||||
# Async runtime
|
||||
|
|
@ -34,6 +36,9 @@ mavlink = { version = "0.13", optional = true }
|
|||
# ONNX Runtime (optional — for MARL actor inference)
|
||||
ort = { version = "2.0.0-rc.11", optional = true }
|
||||
|
||||
# HTTP client (optional — for Ruflo HTTP backend)
|
||||
reqwest = { version = "0.12", features = ["json"], optional = true }
|
||||
|
||||
# Crypto — MAVLink v2 HMAC-SHA256 signing
|
||||
hmac = "0.12"
|
||||
sha2 = "0.10"
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ pub mod demo;
|
|||
pub mod integration;
|
||||
pub mod bench_support;
|
||||
pub mod orchestrator;
|
||||
pub mod ruflo;
|
||||
|
||||
pub use types::{
|
||||
ClusterId, CsiDetection, DroneState, FailSafeState, GridCell, NodeId,
|
||||
|
|
|
|||
|
|
@ -37,6 +37,13 @@ pub struct SwarmOrchestrator {
|
|||
pub peer_detections: Vec<CsiDetection>,
|
||||
/// Accumulated mission statistics.
|
||||
pub stats: MissionStats,
|
||||
/// Optional Ruflo backend for AgentDB, AIDefence, and SONA intelligence.
|
||||
/// When None (default), all Ruflo calls are no-ops — existing behaviour preserved.
|
||||
#[cfg(feature = "ruflo")]
|
||||
pub ruflo: Option<Box<dyn crate::ruflo::RufloBackend>>,
|
||||
/// Active trajectory ID issued by the Ruflo intelligence hooks.
|
||||
#[cfg(feature = "ruflo")]
|
||||
pub trajectory_id: Option<String>,
|
||||
}
|
||||
|
||||
/// Accumulated metrics for one mission run.
|
||||
|
|
@ -102,6 +109,10 @@ impl SwarmOrchestrator {
|
|||
peer_states: HashMap::new(),
|
||||
peer_detections: Vec::new(),
|
||||
stats: MissionStats::default(),
|
||||
#[cfg(feature = "ruflo")]
|
||||
ruflo: None,
|
||||
#[cfg(feature = "ruflo")]
|
||||
trajectory_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -176,6 +187,94 @@ impl SwarmOrchestrator {
|
|||
self.peer_detections.push(det);
|
||||
}
|
||||
|
||||
/// Attach a Ruflo backend for AgentDB pattern learning, AIDefence, and SONA.
|
||||
///
|
||||
/// Call after `new_demo()`:
|
||||
/// ```ignore
|
||||
/// let orch = SwarmOrchestrator::new_demo(...)
|
||||
/// .with_ruflo(Box::new(MockRufloBackend::new()));
|
||||
/// ```
|
||||
#[cfg(feature = "ruflo")]
|
||||
pub fn with_ruflo(mut self, backend: Box<dyn crate::ruflo::RufloBackend>) -> Self {
|
||||
self.ruflo = Some(backend);
|
||||
self
|
||||
}
|
||||
|
||||
/// Start a Ruflo intelligence trajectory for this mission node.
|
||||
///
|
||||
/// Call before the mission loop begins. If no backend is attached this is a no-op.
|
||||
#[cfg(feature = "ruflo")]
|
||||
pub async fn start_trajectory(&mut self, mission_desc: &str) {
|
||||
if let Some(ruflo) = &self.ruflo {
|
||||
match ruflo.trajectory_start(mission_desc, "swarm-specialist").await {
|
||||
Ok(tid) => self.trajectory_id = Some(tid),
|
||||
Err(e) => tracing::warn!("trajectory_start failed: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// End the Ruflo trajectory and persist the mission summary in AgentDB.
|
||||
///
|
||||
/// Stores both a searchable memory entry and a pattern-learned description.
|
||||
/// If no backend is attached this is a no-op.
|
||||
#[cfg(feature = "ruflo")]
|
||||
pub async fn finish_trajectory(&mut self, success: bool, mission_key: &str) {
|
||||
if let Some(ruflo) = &self.ruflo {
|
||||
let tid = self.trajectory_id.take();
|
||||
if let Some(tid) = &tid {
|
||||
let _ = ruflo.trajectory_end(tid, success, None).await;
|
||||
}
|
||||
// Build and serialise mission summary.
|
||||
let summary = crate::ruflo::MissionSummary::from_stats(
|
||||
&self.stats,
|
||||
&self.config.mission.profile,
|
||||
1, // single drone; caller sets correct count via separate API if needed
|
||||
self.config.mission.area_width_m,
|
||||
self.config.mission.area_height_m,
|
||||
0, // caller sets victims_total; 0 = unknown
|
||||
self.probability_grid.coverage_pct(),
|
||||
);
|
||||
if let Ok(json) = serde_json::to_string(&summary) {
|
||||
let _ = ruflo.store_mission(mission_key, &json, "swarm-missions").await;
|
||||
}
|
||||
let _ = ruflo.store_pattern(
|
||||
&summary.to_pattern_description(),
|
||||
summary.pattern_type(),
|
||||
summary.pattern_confidence(),
|
||||
).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// AIDefence-checked variant of `receive_peer_detection`.
|
||||
///
|
||||
/// Returns `true` and enqueues the detection if it passes the safety check.
|
||||
/// Returns `false` (and drops the detection) if AIDefence flags it as unsafe.
|
||||
/// Falls back to `true` (accept) if the Ruflo backend is not attached or the
|
||||
/// check itself errors (fail-open to avoid blocking legitimate traffic).
|
||||
#[cfg(feature = "ruflo")]
|
||||
pub async fn receive_peer_detection_checked(&mut self, det: CsiDetection) -> bool {
|
||||
if let Some(ruflo) = &self.ruflo {
|
||||
// Serialise the detection to a string for AIDefence inspection.
|
||||
let repr = format!(
|
||||
"drone_id={:?} confidence={:.3} victim={:?}",
|
||||
det.drone_id, det.confidence, det.victim_position
|
||||
);
|
||||
match ruflo.mavlink_is_safe(&repr).await {
|
||||
Ok(false) => {
|
||||
tracing::warn!(
|
||||
"aidefence rejected peer detection from {:?}",
|
||||
det.drone_id
|
||||
);
|
||||
return false;
|
||||
}
|
||||
Err(e) => tracing::debug!("aidefence check failed (proceeding): {}", e),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
self.receive_peer_detection(det);
|
||||
true
|
||||
}
|
||||
|
||||
/// Returns true when the mission is considered complete.
|
||||
pub fn is_mission_complete(&self) -> bool {
|
||||
self.probability_grid.coverage_pct() > 0.95
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
//! RufloBackend trait and shared types.
|
||||
use async_trait::async_trait;
|
||||
|
||||
/// Error type for Ruflo backend operations.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum RufloError {
|
||||
#[error("network error: {0}")]
|
||||
Network(String),
|
||||
#[error("tool error: {0}")]
|
||||
Tool(String),
|
||||
#[error("serialization error: {0}")]
|
||||
Serialize(String),
|
||||
}
|
||||
|
||||
/// A past mission retrieved from AgentDB memory.
|
||||
#[derive(Debug, Clone, serde::Deserialize, Default)]
|
||||
pub struct MissionMemoryEntry {
|
||||
pub key: String,
|
||||
pub value: String, // JSON-encoded mission summary
|
||||
pub score: f32,
|
||||
}
|
||||
|
||||
/// A coordination pattern retrieved from AgentDB pattern store.
|
||||
#[derive(Debug, Clone, serde::Deserialize, Default)]
|
||||
pub struct PatternEntry {
|
||||
pub pattern: String,
|
||||
pub pattern_type: String,
|
||||
pub confidence: f32,
|
||||
pub score: f32,
|
||||
}
|
||||
|
||||
/// Result of an AIDefence MAVLink message scan.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MavlinkScanResult {
|
||||
pub safe: bool,
|
||||
pub threats: Vec<String>,
|
||||
}
|
||||
|
||||
/// Core Ruflo capability trait.
|
||||
///
|
||||
/// Two implementations:
|
||||
/// - `HttpRufloBackend` (feature=ruflo): calls the claude-flow daemon at localhost:3000
|
||||
/// - `MockRufloBackend`: in-memory mock for testing (always available)
|
||||
#[async_trait]
|
||||
pub trait RufloBackend: Send + Sync {
|
||||
// ── MissionMemory (claude-flow: memory_store / memory_search) ────
|
||||
async fn store_mission(&self, key: &str, summary: &str, namespace: &str)
|
||||
-> Result<(), RufloError>;
|
||||
async fn search_missions(&self, query: &str, limit: usize, namespace: &str)
|
||||
-> Result<Vec<MissionMemoryEntry>, RufloError>;
|
||||
|
||||
// ── PatternLearner (agentdb_pattern-store / agentdb_pattern-search) ─
|
||||
async fn store_pattern(&self, pattern: &str, pattern_type: &str, confidence: f32)
|
||||
-> Result<(), RufloError>;
|
||||
async fn search_patterns(&self, query: &str, top_k: usize, min_confidence: f32)
|
||||
-> Result<Vec<PatternEntry>, RufloError>;
|
||||
|
||||
// ── MavlinkDefence (aidefence_is_safe / aidefence_scan) ──────────
|
||||
async fn mavlink_is_safe(&self, message_repr: &str) -> Result<bool, RufloError>;
|
||||
async fn mavlink_scan(&self, message_repr: &str) -> Result<MavlinkScanResult, RufloError>;
|
||||
|
||||
// ── IntelligenceHooks (hooks_intelligence_trajectory-*) ──────────
|
||||
async fn trajectory_start(&self, task: &str, agent: &str)
|
||||
-> Result<String, RufloError>; // returns trajectoryId
|
||||
async fn trajectory_step(&self, trajectory_id: &str, action: &str, result: &str, quality: f32)
|
||||
-> Result<(), RufloError>;
|
||||
async fn trajectory_end(&self, trajectory_id: &str, success: bool, feedback: Option<&str>)
|
||||
-> Result<(), RufloError>;
|
||||
}
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
//! HTTP backend that calls the claude-flow daemon via JSON-RPC 2.0.
|
||||
//! Default endpoint: http://localhost:3000/rpc
|
||||
//!
|
||||
//! Start the daemon with: npx @claude-flow/cli@latest daemon start
|
||||
|
||||
use async_trait::async_trait;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use super::backend::*;
|
||||
|
||||
pub struct HttpRufloBackend {
|
||||
client: reqwest::Client,
|
||||
base_url: String,
|
||||
request_id: AtomicU64,
|
||||
}
|
||||
|
||||
impl HttpRufloBackend {
|
||||
pub fn new(base_url: &str) -> Self {
|
||||
Self {
|
||||
client: reqwest::Client::new(),
|
||||
base_url: base_url.trim_end_matches('/').to_string(),
|
||||
request_id: AtomicU64::new(1),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn localhost() -> Self { Self::new("http://localhost:3000") }
|
||||
|
||||
async fn call_tool(
|
||||
&self,
|
||||
tool: &str,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, RufloError> {
|
||||
let id = self.request_id.fetch_add(1, Ordering::SeqCst);
|
||||
let body = serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "tools/call",
|
||||
"id": id,
|
||||
"params": { "name": tool, "arguments": args }
|
||||
});
|
||||
|
||||
let resp = self.client
|
||||
.post(format!("{}/rpc", self.base_url))
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| RufloError::Network(e.to_string()))?;
|
||||
|
||||
let json: serde_json::Value = resp.json().await
|
||||
.map_err(|e| RufloError::Serialize(e.to_string()))?;
|
||||
|
||||
if let Some(err) = json.get("error") {
|
||||
return Err(RufloError::Tool(err.to_string()));
|
||||
}
|
||||
|
||||
Ok(json["result"].clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl RufloBackend for HttpRufloBackend {
|
||||
async fn store_mission(&self, key: &str, value: &str, namespace: &str)
|
||||
-> Result<(), RufloError>
|
||||
{
|
||||
self.call_tool("memory_store", serde_json::json!({
|
||||
"key": key, "value": value, "namespace": namespace
|
||||
})).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn search_missions(&self, query: &str, limit: usize, namespace: &str)
|
||||
-> Result<Vec<MissionMemoryEntry>, RufloError>
|
||||
{
|
||||
let result = self.call_tool("memory_search", serde_json::json!({
|
||||
"query": query, "namespace": namespace, "limit": limit
|
||||
})).await?;
|
||||
let entries: Vec<MissionMemoryEntry> = serde_json::from_value(result)
|
||||
.unwrap_or_default();
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
async fn store_pattern(&self, pattern: &str, pattern_type: &str, confidence: f32)
|
||||
-> Result<(), RufloError>
|
||||
{
|
||||
self.call_tool("agentdb_pattern-store", serde_json::json!({
|
||||
"pattern": pattern, "type": pattern_type, "confidence": confidence
|
||||
})).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn search_patterns(&self, query: &str, top_k: usize, min_confidence: f32)
|
||||
-> Result<Vec<PatternEntry>, RufloError>
|
||||
{
|
||||
let result = self.call_tool("agentdb_pattern-search", serde_json::json!({
|
||||
"query": query, "topK": top_k, "minConfidence": min_confidence
|
||||
})).await?;
|
||||
let entries: Vec<PatternEntry> = serde_json::from_value(
|
||||
result["results"].clone()
|
||||
).unwrap_or_default();
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
async fn mavlink_is_safe(&self, message_repr: &str) -> Result<bool, RufloError> {
|
||||
let result = self.call_tool("aidefence_is_safe", serde_json::json!({
|
||||
"input": message_repr
|
||||
})).await?;
|
||||
Ok(result["safe"].as_bool().unwrap_or(true))
|
||||
}
|
||||
|
||||
async fn mavlink_scan(&self, message_repr: &str) -> Result<MavlinkScanResult, RufloError> {
|
||||
let result = self.call_tool("aidefence_scan", serde_json::json!({
|
||||
"input": message_repr, "quick": false
|
||||
})).await?;
|
||||
let safe = result["safe"].as_bool().unwrap_or(true);
|
||||
let threats: Vec<String> = result["threats"]
|
||||
.as_array()
|
||||
.map(|a| a.iter().filter_map(|v| v["type"].as_str().map(String::from)).collect())
|
||||
.unwrap_or_default();
|
||||
Ok(MavlinkScanResult { safe, threats })
|
||||
}
|
||||
|
||||
async fn trajectory_start(&self, task: &str, agent: &str)
|
||||
-> Result<String, RufloError>
|
||||
{
|
||||
let result = self.call_tool("hooks_intelligence_trajectory-start", serde_json::json!({
|
||||
"task": task, "agent": agent
|
||||
})).await?;
|
||||
Ok(result["trajectoryId"]
|
||||
.as_str()
|
||||
.unwrap_or("unknown-traj")
|
||||
.to_string())
|
||||
}
|
||||
|
||||
async fn trajectory_step(
|
||||
&self,
|
||||
trajectory_id: &str,
|
||||
action: &str,
|
||||
result_str: &str,
|
||||
quality: f32,
|
||||
) -> Result<(), RufloError> {
|
||||
self.call_tool("hooks_intelligence_trajectory-step", serde_json::json!({
|
||||
"trajectoryId": trajectory_id,
|
||||
"action": action,
|
||||
"result": result_str,
|
||||
"quality": quality
|
||||
})).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn trajectory_end(
|
||||
&self,
|
||||
trajectory_id: &str,
|
||||
success: bool,
|
||||
feedback: Option<&str>,
|
||||
) -> Result<(), RufloError> {
|
||||
let mut args = serde_json::json!({
|
||||
"trajectoryId": trajectory_id,
|
||||
"success": success
|
||||
});
|
||||
if let Some(fb) = feedback {
|
||||
args["feedback"] = fb.into();
|
||||
}
|
||||
self.call_tool("hooks_intelligence_trajectory-end", args).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
//! Serializable mission summary stored in AgentDB memory after each completed mission.
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::orchestrator::MissionStats;
|
||||
|
||||
/// Serializable summary of a completed mission stored in AgentDB.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MissionSummary {
|
||||
pub mission_profile: String,
|
||||
pub num_drones: usize,
|
||||
pub area_width_m: f64,
|
||||
pub area_height_m: f64,
|
||||
pub victims_total: usize,
|
||||
pub victims_confirmed: u32,
|
||||
pub cells_covered: u32,
|
||||
pub coverage_pct: f64,
|
||||
pub elapsed_secs: f64,
|
||||
pub collision_events: u32,
|
||||
pub localization_error_m: Option<f64>,
|
||||
}
|
||||
|
||||
impl MissionSummary {
|
||||
pub fn from_stats(
|
||||
stats: &MissionStats,
|
||||
profile: &str,
|
||||
num_drones: usize,
|
||||
area_width: f64,
|
||||
area_height: f64,
|
||||
victims_total: usize,
|
||||
coverage_pct: f64,
|
||||
) -> Self {
|
||||
Self {
|
||||
mission_profile: profile.to_string(),
|
||||
num_drones,
|
||||
area_width_m: area_width,
|
||||
area_height_m: area_height,
|
||||
victims_total,
|
||||
victims_confirmed: stats.victims_confirmed,
|
||||
cells_covered: stats.cells_covered,
|
||||
coverage_pct,
|
||||
elapsed_secs: stats.elapsed_secs,
|
||||
collision_events: stats.collision_events,
|
||||
localization_error_m: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Pattern description for AgentDB pattern-store — human-readable.
|
||||
pub fn to_pattern_description(&self) -> String {
|
||||
format!(
|
||||
"{} mission: {} drones over {}x{}m, {} victims confirmed in {:.1}s, {:.0}% coverage, {} collisions",
|
||||
self.mission_profile,
|
||||
self.num_drones,
|
||||
self.area_width_m as u32,
|
||||
self.area_height_m as u32,
|
||||
self.victims_confirmed,
|
||||
self.elapsed_secs,
|
||||
self.coverage_pct * 100.0,
|
||||
self.collision_events,
|
||||
)
|
||||
}
|
||||
|
||||
/// Pattern type tag for AgentDB.
|
||||
pub fn pattern_type(&self) -> &str {
|
||||
match self.mission_profile.as_str() {
|
||||
"sar" => "sar-mission",
|
||||
"inspection" => "inspection-mission",
|
||||
"mine" => "mine-mission",
|
||||
_ => "swarm-mission",
|
||||
}
|
||||
}
|
||||
|
||||
/// Confidence score (0-1) for AgentDB based on mission outcomes.
|
||||
pub fn pattern_confidence(&self) -> f32 {
|
||||
let victim_score = if self.victims_total > 0 {
|
||||
self.victims_confirmed as f32 / self.victims_total as f32
|
||||
} else {
|
||||
0.5
|
||||
};
|
||||
let coverage_score = self.coverage_pct as f32;
|
||||
let collision_penalty = (self.collision_events as f32 * 0.1).min(0.5);
|
||||
((victim_score * 0.5 + coverage_score * 0.5) - collision_penalty).clamp(0.0, 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn make_stats(victims_confirmed: u32, cells_covered: u32, collision_events: u32) -> MissionStats {
|
||||
MissionStats {
|
||||
cells_covered,
|
||||
victims_confirmed,
|
||||
collision_events,
|
||||
steps: 100,
|
||||
elapsed_secs: 30.0,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pattern_type_tags() {
|
||||
let stats = make_stats(2, 80, 0);
|
||||
let s = MissionSummary::from_stats(&stats, "sar", 4, 400.0, 400.0, 3, 0.85);
|
||||
assert_eq!(s.pattern_type(), "sar-mission");
|
||||
|
||||
let s2 = MissionSummary::from_stats(&stats, "custom", 2, 200.0, 200.0, 0, 0.5);
|
||||
assert_eq!(s2.pattern_type(), "swarm-mission");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pattern_confidence_penalises_collisions() {
|
||||
let no_collisions = make_stats(3, 80, 0);
|
||||
let with_collisions = make_stats(3, 80, 4);
|
||||
let s_good = MissionSummary::from_stats(&no_collisions, "sar", 4, 400.0, 400.0, 3, 0.9);
|
||||
let s_bad = MissionSummary::from_stats(&with_collisions, "sar", 4, 400.0, 400.0, 3, 0.9);
|
||||
assert!(s_good.pattern_confidence() > s_bad.pattern_confidence());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_pattern_description_contains_profile() {
|
||||
let stats = make_stats(1, 50, 0);
|
||||
let s = MissionSummary::from_stats(&stats, "inspection", 2, 100.0, 100.0, 1, 0.75);
|
||||
let desc = s.to_pattern_description();
|
||||
assert!(desc.contains("inspection"), "description should include profile: {desc}");
|
||||
assert!(desc.contains("2 drones"), "description should include drone count: {desc}");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
//! In-memory mock RufloBackend for testing — no network, zero latency.
|
||||
use async_trait::async_trait;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use super::backend::*;
|
||||
|
||||
/// Configurable mock. All writes go to in-memory vecs; searches return stored items.
|
||||
pub struct MockRufloBackend {
|
||||
pub missions: Arc<Mutex<Vec<(String, String)>>>, // (key, value)
|
||||
pub patterns: Arc<Mutex<Vec<(String, String, f32)>>>, // (pattern, type, confidence)
|
||||
pub scan_safe: bool, // set false to simulate a detected threat
|
||||
pub traj_ids: Arc<Mutex<Vec<String>>>,
|
||||
}
|
||||
|
||||
impl Default for MockRufloBackend {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
missions: Arc::new(Mutex::new(Vec::new())),
|
||||
patterns: Arc::new(Mutex::new(Vec::new())),
|
||||
scan_safe: true,
|
||||
traj_ids: Arc::new(Mutex::new(Vec::new())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MockRufloBackend {
|
||||
pub fn new() -> Self { Self::default() }
|
||||
|
||||
/// Pre-load a past mission for search to return.
|
||||
pub fn seed_mission(&self, key: &str, value: &str) {
|
||||
self.missions.lock().unwrap().push((key.to_string(), value.to_string()));
|
||||
}
|
||||
|
||||
/// Pre-load a pattern for search to return.
|
||||
pub fn seed_pattern(&self, pattern: &str, ptype: &str, confidence: f32) {
|
||||
self.patterns.lock().unwrap().push((pattern.to_string(), ptype.to_string(), confidence));
|
||||
}
|
||||
|
||||
/// Configure the scanner to reject the next message.
|
||||
pub fn reject_next(self) -> Self { Self { scan_safe: false, ..self } }
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl RufloBackend for MockRufloBackend {
|
||||
async fn store_mission(&self, key: &str, value: &str, _ns: &str) -> Result<(), RufloError> {
|
||||
self.missions.lock().unwrap().push((key.to_string(), value.to_string()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn search_missions(&self, query: &str, limit: usize, _ns: &str)
|
||||
-> Result<Vec<MissionMemoryEntry>, RufloError>
|
||||
{
|
||||
let missions = self.missions.lock().unwrap();
|
||||
Ok(missions.iter().take(limit).map(|(k, v)| MissionMemoryEntry {
|
||||
key: k.clone(),
|
||||
value: v.clone(),
|
||||
score: if v.contains(query) { 0.9 } else { 0.5 },
|
||||
}).collect())
|
||||
}
|
||||
|
||||
async fn store_pattern(&self, pattern: &str, ptype: &str, confidence: f32)
|
||||
-> Result<(), RufloError>
|
||||
{
|
||||
self.patterns.lock().unwrap().push((pattern.to_string(), ptype.to_string(), confidence));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn search_patterns(&self, _query: &str, top_k: usize, min_conf: f32)
|
||||
-> Result<Vec<PatternEntry>, RufloError>
|
||||
{
|
||||
let patterns = self.patterns.lock().unwrap();
|
||||
Ok(patterns.iter()
|
||||
.filter(|(_, _, c)| *c >= min_conf)
|
||||
.take(top_k)
|
||||
.map(|(p, t, c)| PatternEntry {
|
||||
pattern: p.clone(),
|
||||
pattern_type: t.clone(),
|
||||
confidence: *c,
|
||||
score: *c,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn mavlink_is_safe(&self, _msg: &str) -> Result<bool, RufloError> {
|
||||
Ok(self.scan_safe)
|
||||
}
|
||||
|
||||
async fn mavlink_scan(&self, _msg: &str) -> Result<MavlinkScanResult, RufloError> {
|
||||
Ok(MavlinkScanResult {
|
||||
safe: self.scan_safe,
|
||||
threats: if self.scan_safe {
|
||||
vec![]
|
||||
} else {
|
||||
vec!["suspicious_coordinates".into()]
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async fn trajectory_start(&self, task: &str, _agent: &str)
|
||||
-> Result<String, RufloError>
|
||||
{
|
||||
let id = format!("mock-traj-{}", task.len()); // deterministic for testing
|
||||
self.traj_ids.lock().unwrap().push(id.clone());
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
async fn trajectory_step(&self, _id: &str, _act: &str, _res: &str, _q: f32)
|
||||
-> Result<(), RufloError> { Ok(()) }
|
||||
|
||||
async fn trajectory_end(&self, _id: &str, _ok: bool, _fb: Option<&str>)
|
||||
-> Result<(), RufloError> { Ok(()) }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mock_store_and_search_mission() {
|
||||
let mock = MockRufloBackend::new();
|
||||
mock.store_mission("m1", r#"{"victims":2}"#, "swarm-missions").await.unwrap();
|
||||
let results = mock.search_missions("victims", 5, "swarm-missions").await.unwrap();
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].key, "m1");
|
||||
assert!(results[0].score > 0.5, "keyword match should score high");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mock_pattern_lifecycle() {
|
||||
let mock = MockRufloBackend::new();
|
||||
mock.store_pattern("approach from 3 angles when P > 0.7", "sar-trajectory", 0.9).await.unwrap();
|
||||
let results = mock.search_patterns("SAR convergence", 5, 0.5).await.unwrap();
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].confidence, 0.9);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mock_mavlink_defence_safe() {
|
||||
let mock = MockRufloBackend::new();
|
||||
assert!(mock.mavlink_is_safe(r#"{"drone_id":1,"confidence":0.8}"#).await.unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mock_mavlink_defence_rejected() {
|
||||
let mock = MockRufloBackend { scan_safe: false, ..Default::default() };
|
||||
let scan = mock.mavlink_scan("SUSPICIOUS MESSAGE").await.unwrap();
|
||||
assert!(!scan.safe);
|
||||
assert!(!scan.threats.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mock_trajectory_lifecycle() {
|
||||
let mock = MockRufloBackend::new();
|
||||
let tid = mock.trajectory_start("SAR 400x400", "swarm-specialist").await.unwrap();
|
||||
mock.trajectory_step(&tid, "scan (5,3)", "prob=0.6", 0.7).await.unwrap();
|
||||
mock.trajectory_end(&tid, true, Some("victim found")).await.unwrap();
|
||||
assert!(!mock.traj_ids.lock().unwrap().is_empty());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
//! Ruflo AI-agent capabilities integration.
|
||||
//!
|
||||
//! Integrates the claude-flow daemon's AgentDB, AIDefence, and SONA intelligence
|
||||
//! hooks into the ruview-swarm orchestrator via a trait-based backend.
|
||||
//!
|
||||
//! Feature gate: `ruflo`. The `RufloBackend` trait and `MockRufloBackend` are always
|
||||
//! compiled so tests can use them without enabling the `ruflo` feature. Only
|
||||
//! `HttpRufloBackend` (which requires `reqwest` + `serde_json`) is gated.
|
||||
|
||||
pub mod backend;
|
||||
pub mod mock_backend;
|
||||
pub mod mission_summary;
|
||||
|
||||
#[cfg(feature = "ruflo")]
|
||||
pub mod http_backend;
|
||||
|
||||
pub use backend::{RufloBackend, RufloError, MissionMemoryEntry, PatternEntry, MavlinkScanResult};
|
||||
pub use mock_backend::MockRufloBackend;
|
||||
pub use mission_summary::MissionSummary;
|
||||
|
||||
#[cfg(feature = "ruflo")]
|
||||
pub use http_backend::HttpRufloBackend;
|
||||
Loading…
Reference in New Issue