From b2aa29aba10457f70549f0dd60eb534121b4aafd Mon Sep 17 00:00:00 2001 From: rUv Date: Sat, 21 Feb 2026 18:00:06 +0000 Subject: [PATCH] feat(intelligence): ADR-043 External Intelligence Providers for SONA Learning Implement trait-based IntelligenceProvider extension point for external quality signals. Addresses PR #190 proposal (renumbered from ADR-029 to avoid collision with existing ADR-029-rvf-canonical-format). - IntelligenceProvider trait with load_signals() and quality_weights() - FileSignalProvider built-in for JSON file-based signal exchange - IntelligenceLoader for multi-provider registration and aggregation - QualitySignal, QualityFactors, ProviderQualityWeights types - calibration_bias() on TaskComplexityAnalyzer for router feedback - 12 unit tests (all passing) Co-Authored-By: claude-flow --- crates/ruvllm/src/claude_flow/model_router.rs | 23 + crates/ruvllm/src/intelligence/mod.rs | 604 ++++++++++++++++++ crates/ruvllm/src/lib.rs | 7 + ...ADR-043-external-intelligence-providers.md | 148 +++++ 4 files changed, 782 insertions(+) create mode 100644 crates/ruvllm/src/intelligence/mod.rs create mode 100644 docs/adr/ADR-043-external-intelligence-providers.md diff --git a/crates/ruvllm/src/claude_flow/model_router.rs b/crates/ruvllm/src/claude_flow/model_router.rs index db2da3870..6f43b3d14 100644 --- a/crates/ruvllm/src/claude_flow/model_router.rs +++ b/crates/ruvllm/src/claude_flow/model_router.rs @@ -657,6 +657,29 @@ impl TaskComplexityAnalyzer { } } + /// Returns signed calibration error. + /// + /// Positive value = systematically over-predicting complexity, + /// negative value = systematically under-predicting. + /// Returns 0.0 if no feedback has been recorded. + pub fn calibration_bias(&self) -> f32 { + let with_feedback: Vec<_> = self + .accuracy_history + .iter() + .filter(|r| r.actual.is_some()) + .collect(); + + if with_feedback.is_empty() { + return 0.0; + } + + let sum: f32 = with_feedback + .iter() + .map(|r| r.predicted - r.actual.unwrap()) + .sum(); + sum / with_feedback.len() as f32 + } + /// Get accuracy statistics pub fn accuracy_stats(&self) -> AnalyzerStats { let with_feedback: Vec<_> = self diff --git a/crates/ruvllm/src/intelligence/mod.rs b/crates/ruvllm/src/intelligence/mod.rs new file mode 100644 index 000000000..39c99cb7c --- /dev/null +++ b/crates/ruvllm/src/intelligence/mod.rs @@ -0,0 +1,604 @@ +//! External Intelligence Providers for SONA Learning +//! +//! This module provides a trait-based extension point for external systems +//! to feed quality signals into RuvLLM's learning loops (SONA, embedding +//! classifier, model router calibration). +//! +//! ## Architecture +//! +//! ```text +//! ┌─────────────────────┐ ┌──────────────────────┐ +//! │ External System │ │ IntelligenceLoader │ +//! │ (CI/CD, workflow) │────>│ ├── providers[] │ +//! │ │ │ ├── load_all_signals()│ +//! └─────────────────────┘ │ └── ingest() │ +//! └──────────┬───────────┘ +//! │ +//! ┌─────────────────────────┼──────────────┐ +//! │ │ │ +//! v v v +//! ┌────────────────┐ ┌──────────────────┐ ┌────────────┐ +//! │ SONA Loop │ │ Embedding │ │ Model │ +//! │ (trajectories) │ │ Classifier │ │ Router │ +//! └────────────────┘ └──────────────────┘ └────────────┘ +//! ``` +//! +//! ## Usage +//! +//! ```rust,ignore +//! use ruvllm::intelligence::{IntelligenceLoader, FileSignalProvider}; +//! use std::path::PathBuf; +//! +//! // Create loader and register providers +//! let mut loader = IntelligenceLoader::new(); +//! loader.register_provider(Box::new( +//! FileSignalProvider::new(PathBuf::from(".claude/intelligence/data/signals.json")) +//! )); +//! +//! // Load all signals from registered providers +//! let signals = loader.load_all_signals(); +//! println!("Loaded {} signals from {} providers", signals.len(), loader.provider_count()); +//! ``` + +use crate::error::Result; +use serde::{Deserialize, Serialize}; +use std::path::{Path, PathBuf}; + +// --------------------------------------------------------------------------- +// Core types +// --------------------------------------------------------------------------- + +/// A quality signal from an external system. +/// +/// Represents one completed task with quality assessment data +/// that can feed into SONA trajectories, the embedding classifier, +/// and model router calibration. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct QualitySignal { + /// Unique identifier for this signal + pub id: String, + + /// Human-readable task description (used for embedding generation) + pub task_description: String, + + /// Execution outcome: `"success"`, `"partial_success"`, `"failure"` + pub outcome: String, + + /// Composite quality score (0.0 - 1.0) + pub quality_score: f32, + + /// Optional human verdict: `"approved"`, `"rejected"`, or `None` + #[serde(default)] + pub human_verdict: Option, + + /// Optional structured quality factors for detailed analysis + #[serde(default)] + pub quality_factors: Option, + + /// ISO 8601 timestamp of task completion + pub completed_at: String, +} + +/// Granular quality factor breakdown. +/// +/// Not all providers will have all factors. Fields default to `None`, +/// meaning "not assessed" (distinct from `0.0`, which means "assessed as zero"). +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct QualityFactors { + /// Whether acceptance criteria were met (0.0 - 1.0) + pub acceptance_criteria_met: Option, + /// Whether tests are passing (0.0 - 1.0) + pub tests_passing: Option, + /// Whether there are no regressions (0.0 - 1.0) + pub no_regressions: Option, + /// Whether linting is clean (0.0 - 1.0) + pub lint_clean: Option, + /// Whether type checking passes (0.0 - 1.0) + pub type_check_clean: Option, + /// Whether code follows established patterns (0.0 - 1.0) + pub follows_patterns: Option, + /// Relevance to the task context (0.0 - 1.0) + pub context_relevance: Option, + /// Coherence of reasoning chain (0.0 - 1.0) + pub reasoning_coherence: Option, + /// Efficiency of execution (0.0 - 1.0) + pub execution_efficiency: Option, +} + +/// Quality weight overrides from a provider. +/// +/// If a provider returns weights, they influence how the composite +/// quality score is computed from individual factors for that provider's +/// signals. Weights should sum to approximately 1.0. +/// +/// Note: This is distinct from `quality::QualityWeights` which covers +/// the scoring engine's internal dimensions (schema, coherence, diversity). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProviderQualityWeights { + /// Weight for task completion factors (acceptance criteria, tests) + pub task_completion: f32, + /// Weight for code quality factors (lint, types, patterns) + pub code_quality: f32, + /// Weight for process factors (reasoning, efficiency) + pub process: f32, +} + +impl Default for ProviderQualityWeights { + fn default() -> Self { + Self { + task_completion: 0.5, + code_quality: 0.3, + process: 0.2, + } + } +} + +// --------------------------------------------------------------------------- +// Provider trait +// --------------------------------------------------------------------------- + +/// Trait for external systems that supply quality signals to RuvLLM. +/// +/// Implementations are registered with [`IntelligenceLoader`] and called +/// during [`IntelligenceLoader::load_all_signals`]. The loader handles +/// mapping signals to SONA trajectories, classifier entries, and router +/// calibration data. +/// +/// # Examples +/// +/// File-based provider (built-in): +/// ```rust,ignore +/// use ruvllm::intelligence::FileSignalProvider; +/// use std::path::PathBuf; +/// +/// let provider = FileSignalProvider::new(PathBuf::from("signals.json")); +/// loader.register_provider(Box::new(provider)); +/// ``` +/// +/// Custom provider: +/// ```rust,ignore +/// use ruvllm::intelligence::{IntelligenceProvider, QualitySignal, ProviderQualityWeights}; +/// use ruvllm::error::Result; +/// +/// struct MyPipelineProvider; +/// +/// impl IntelligenceProvider for MyPipelineProvider { +/// fn name(&self) -> &str { "my-pipeline" } +/// fn load_signals(&self) -> Result> { +/// // Read from your data source +/// Ok(vec![]) +/// } +/// } +/// ``` +pub trait IntelligenceProvider: Send + Sync { + /// Human-readable name for this provider (used in logs and diagnostics). + fn name(&self) -> &str; + + /// Load quality signals from this provider's data source. + /// + /// Returns an empty vec if no signals are available (not an error). + /// Errors indicate that the data source exists but could not be read. + fn load_signals(&self) -> Result>; + + /// Optional quality weight overrides for this provider's signals. + /// + /// If `None`, default weights are used when computing composite scores + /// from `QualityFactors`. + fn quality_weights(&self) -> Option { + None + } +} + +// --------------------------------------------------------------------------- +// FileSignalProvider — built-in file-based provider +// --------------------------------------------------------------------------- + +/// Built-in file-based intelligence provider. +/// +/// Reads quality signals from a JSON file at a specified path. +/// This is the default provider for systems that write a signal file +/// to `.claude/intelligence/data/`. Non-Rust integrations (TypeScript, +/// Python, etc.) typically use this path. +/// +/// ## File Format +/// +/// The JSON file should contain an array of [`QualitySignal`] objects: +/// +/// ```json +/// [ +/// { +/// "id": "task-001", +/// "task_description": "Implement login endpoint", +/// "outcome": "success", +/// "quality_score": 0.92, +/// "human_verdict": "approved", +/// "completed_at": "2025-02-21T12:00:00Z" +/// } +/// ] +/// ``` +pub struct FileSignalProvider { + path: PathBuf, +} + +impl FileSignalProvider { + /// Create a new file-based provider reading from the given path. + pub fn new(path: PathBuf) -> Self { + Self { path } + } + + /// Returns the path this provider reads from. + pub fn path(&self) -> &Path { + &self.path + } +} + +impl IntelligenceProvider for FileSignalProvider { + fn name(&self) -> &str { + "file-signals" + } + + fn load_signals(&self) -> Result> { + if !self.path.exists() { + return Ok(vec![]); // No file = no signals, not an error + } + + let contents = std::fs::read_to_string(&self.path)?; + let signals: Vec = serde_json::from_str(&contents).map_err(|e| { + crate::error::RuvLLMError::Serialization(format!( + "Failed to parse signal file {}: {}", + self.path.display(), + e + )) + })?; + + Ok(signals) + } + + fn quality_weights(&self) -> Option { + // Check for quality-weights.json alongside the signal file + let config_path = self + .path + .parent() + .unwrap_or(Path::new(".")) + .join("quality-weights.json"); + + if !config_path.exists() { + return None; + } + + let contents = std::fs::read_to_string(&config_path).ok()?; + serde_json::from_str(&contents).ok() + } +} + +// --------------------------------------------------------------------------- +// IntelligenceLoader — provider registry and signal aggregator +// --------------------------------------------------------------------------- + +/// Aggregates quality signals from multiple registered providers. +/// +/// The loader maintains a list of [`IntelligenceProvider`] implementations +/// and calls them in registration order during [`load_all_signals`]. +/// +/// # Zero Overhead +/// +/// If no providers are registered, `load_all_signals` returns an empty vec +/// with no allocations beyond the empty `Vec`. +pub struct IntelligenceLoader { + providers: Vec>, +} + +impl IntelligenceLoader { + /// Create a new empty loader with no registered providers. + pub fn new() -> Self { + Self { + providers: Vec::new(), + } + } + + /// Register an external intelligence provider. + /// + /// Providers are called in registration order during `load_all_signals()`. + pub fn register_provider(&mut self, provider: Box) { + self.providers.push(provider); + } + + /// Returns the number of registered providers. + pub fn provider_count(&self) -> usize { + self.providers.len() + } + + /// Returns the names of all registered providers. + pub fn provider_names(&self) -> Vec<&str> { + self.providers.iter().map(|p| p.name()).collect() + } + + /// Load signals from all registered providers. + /// + /// Signals from each provider are collected into a flat list. + /// If a provider fails, its error is logged but does not prevent + /// other providers from loading — the failure is non-fatal. + /// + /// Returns `(signals, errors)` where errors contains provider names + /// and their error messages for any that failed. + pub fn load_all_signals(&self) -> (Vec, Vec) { + let mut all_signals = Vec::new(); + let mut errors = Vec::new(); + + for provider in &self.providers { + match provider.load_signals() { + Ok(signals) => { + all_signals.extend(signals); + } + Err(e) => { + errors.push(ProviderError { + provider_name: provider.name().to_string(), + message: e.to_string(), + }); + } + } + } + + (all_signals, errors) + } + + /// Load signals and their associated weight overrides from all providers. + /// + /// Returns a vec of `(signals, optional_weights)` tuples grouped by provider. + pub fn load_grouped(&self) -> Vec { + self.providers + .iter() + .map(|provider| { + let signals = provider.load_signals().unwrap_or_default(); + let weights = provider.quality_weights(); + ProviderResult { + provider_name: provider.name().to_string(), + signals, + weights, + } + }) + .collect() + } +} + +impl Default for IntelligenceLoader { + fn default() -> Self { + Self::new() + } +} + +/// Error from a single provider during batch loading. +#[derive(Debug, Clone)] +pub struct ProviderError { + /// Name of the provider that failed + pub provider_name: String, + /// Error message + pub message: String, +} + +/// Result from a single provider during grouped loading. +#[derive(Debug, Clone)] +pub struct ProviderResult { + /// Name of the provider + pub provider_name: String, + /// Signals loaded (empty if provider failed) + pub signals: Vec, + /// Optional quality weight overrides + pub weights: Option, +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + + /// Test provider that returns static signals + struct MockProvider { + signals: Vec, + } + + impl IntelligenceProvider for MockProvider { + fn name(&self) -> &str { + "mock" + } + + fn load_signals(&self) -> Result> { + Ok(self.signals.clone()) + } + + fn quality_weights(&self) -> Option { + Some(ProviderQualityWeights { + task_completion: 0.6, + code_quality: 0.3, + process: 0.1, + }) + } + } + + /// Test provider that always fails + struct FailingProvider; + + impl IntelligenceProvider for FailingProvider { + fn name(&self) -> &str { + "failing" + } + + fn load_signals(&self) -> Result> { + Err(crate::error::RuvLLMError::Serialization( + "simulated failure".into(), + )) + } + } + + fn make_signal(id: &str, score: f32) -> QualitySignal { + QualitySignal { + id: id.to_string(), + task_description: format!("Task {}", id), + outcome: "success".to_string(), + quality_score: score, + human_verdict: None, + quality_factors: None, + completed_at: "2025-02-21T00:00:00Z".to_string(), + } + } + + #[test] + fn empty_loader_returns_no_signals() { + let loader = IntelligenceLoader::new(); + let (signals, errors) = loader.load_all_signals(); + assert!(signals.is_empty()); + assert!(errors.is_empty()); + assert_eq!(loader.provider_count(), 0); + } + + #[test] + fn mock_provider_returns_signals() { + let mut loader = IntelligenceLoader::new(); + loader.register_provider(Box::new(MockProvider { + signals: vec![make_signal("t1", 0.9), make_signal("t2", 0.7)], + })); + + let (signals, errors) = loader.load_all_signals(); + assert_eq!(signals.len(), 2); + assert!(errors.is_empty()); + assert_eq!(signals[0].id, "t1"); + assert!((signals[0].quality_score - 0.9).abs() < f32::EPSILON); + } + + #[test] + fn failing_provider_non_fatal() { + let mut loader = IntelligenceLoader::new(); + loader.register_provider(Box::new(FailingProvider)); + loader.register_provider(Box::new(MockProvider { + signals: vec![make_signal("t3", 0.8)], + })); + + let (signals, errors) = loader.load_all_signals(); + assert_eq!(signals.len(), 1); // mock provider's signal + assert_eq!(errors.len(), 1); // failing provider's error + assert_eq!(errors[0].provider_name, "failing"); + } + + #[test] + fn multiple_providers_aggregate() { + let mut loader = IntelligenceLoader::new(); + loader.register_provider(Box::new(MockProvider { + signals: vec![make_signal("a1", 0.9)], + })); + loader.register_provider(Box::new(MockProvider { + signals: vec![make_signal("b1", 0.8), make_signal("b2", 0.6)], + })); + + let (signals, _) = loader.load_all_signals(); + assert_eq!(signals.len(), 3); + assert_eq!(loader.provider_count(), 2); + } + + #[test] + fn grouped_loading() { + let mut loader = IntelligenceLoader::new(); + loader.register_provider(Box::new(MockProvider { + signals: vec![make_signal("g1", 0.85)], + })); + + let results = loader.load_grouped(); + assert_eq!(results.len(), 1); + assert_eq!(results[0].provider_name, "mock"); + assert_eq!(results[0].signals.len(), 1); + assert!(results[0].weights.is_some()); + } + + #[test] + fn provider_names() { + let mut loader = IntelligenceLoader::new(); + loader.register_provider(Box::new(MockProvider { signals: vec![] })); + loader.register_provider(Box::new(FailingProvider)); + assert_eq!(loader.provider_names(), vec!["mock", "failing"]); + } + + #[test] + fn file_provider_missing_file() { + let provider = FileSignalProvider::new(PathBuf::from("/nonexistent/signals.json")); + let signals = provider.load_signals().unwrap(); + assert!(signals.is_empty()); + } + + #[test] + fn file_provider_reads_json() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("test-signals.json"); + let mut f = std::fs::File::create(&path).unwrap(); + write!( + f, + r#"[ + {{ + "id": "f1", + "task_description": "Fix login bug", + "outcome": "success", + "quality_score": 0.95, + "completed_at": "2025-02-21T10:00:00Z" + }} + ]"# + ) + .unwrap(); + + let provider = FileSignalProvider::new(path); + let signals = provider.load_signals().unwrap(); + assert_eq!(signals.len(), 1); + assert_eq!(signals[0].id, "f1"); + assert!((signals[0].quality_score - 0.95).abs() < f32::EPSILON); + } + + #[test] + fn file_provider_invalid_json() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("bad.json"); + std::fs::write(&path, "not json").unwrap(); + + let provider = FileSignalProvider::new(path); + assert!(provider.load_signals().is_err()); + } + + #[test] + fn quality_factors_default() { + let factors = QualityFactors::default(); + assert!(factors.acceptance_criteria_met.is_none()); + assert!(factors.tests_passing.is_none()); + } + + #[test] + fn provider_quality_weights_default() { + let w = ProviderQualityWeights::default(); + let sum = w.task_completion + w.code_quality + w.process; + assert!((sum - 1.0).abs() < f32::EPSILON); + } + + #[test] + fn quality_signal_serde_roundtrip() { + let signal = QualitySignal { + id: "rt1".to_string(), + task_description: "Test roundtrip".to_string(), + outcome: "success".to_string(), + quality_score: 0.88, + human_verdict: Some("approved".to_string()), + quality_factors: Some(QualityFactors { + tests_passing: Some(1.0), + lint_clean: Some(0.9), + ..Default::default() + }), + completed_at: "2025-02-21T12:00:00Z".to_string(), + }; + + let json = serde_json::to_string(&signal).unwrap(); + let parsed: QualitySignal = serde_json::from_str(&json).unwrap(); + assert_eq!(parsed.id, "rt1"); + assert!((parsed.quality_score - 0.88).abs() < f32::EPSILON); + assert!(parsed.quality_factors.is_some()); + let factors = parsed.quality_factors.unwrap(); + assert!((factors.tests_passing.unwrap() - 1.0).abs() < f32::EPSILON); + } +} diff --git a/crates/ruvllm/src/lib.rs b/crates/ruvllm/src/lib.rs index f6367f34e..58ecfc384 100644 --- a/crates/ruvllm/src/lib.rs +++ b/crates/ruvllm/src/lib.rs @@ -52,6 +52,7 @@ pub mod error; pub mod evaluation; pub mod gguf; pub mod hub; +pub mod intelligence; pub mod kernels; pub mod kv_cache; pub mod lora; @@ -416,6 +417,12 @@ pub use ruvector_integration::{ VectorMetadata, }; +// Intelligence provider exports +pub use intelligence::{ + FileSignalProvider, IntelligenceLoader, IntelligenceProvider, ProviderError, + ProviderQualityWeights, ProviderResult, QualityFactors, QualitySignal, +}; + // Quality scoring exports pub use quality::{ CoherenceConfig, diff --git a/docs/adr/ADR-043-external-intelligence-providers.md b/docs/adr/ADR-043-external-intelligence-providers.md new file mode 100644 index 000000000..f400991c4 --- /dev/null +++ b/docs/adr/ADR-043-external-intelligence-providers.md @@ -0,0 +1,148 @@ +# ADR-043: External Intelligence Providers for SONA Learning + +| Field | Value | +|-------------|------------------------------------------------| +| Status | Accepted | +| Date | 2025-02-21 | +| Authors | @grparry (proposal), ruv (implementation) | +| Supersedes | — | +| Origin | PR #190 (renumbered from ADR-029 to avoid collision with ADR-029-rvf-canonical-format) | + +## Context + +RuvLLM's learning loops — SONA trajectory recording, HNSW embedding classification, and model router calibration — depend on quality signals to distinguish good executions from bad ones. Today, those signals come from ruvllm's own inference pipeline: a request completes, a quality score is computed internally, and the score feeds back into the learning loops. + +This works when ruvllm is the entire system. But increasingly, ruvllm operates as one component within larger orchestration pipelines — workflow engines, CI/CD systems, coding assistants, multi-agent frameworks — where the *real* quality signal lives outside ruvllm. The external system knows whether the task actually met acceptance criteria, whether tests passed, whether the human reviewer approved or rejected the output. Ruvllm doesn't have access to any of that. + +### The Gap + +ADR-002 established Ruvector as the unified memory layer and defined the Witness Log schema with `quality_score: f32`. ADR-CE-021 established that multiple systems (RuvLLM, Prime-Radiant) can contribute trajectories to a shared SONA instance. But neither ADR addresses **how external systems feed quality data in**. + +### Existing Extension Precedents + +Ruvllm already has well-designed trait-based extension points: + +| Trait | Purpose | Location | +|-------|---------|----------| +| `LlmBackend` | Pluggable inference backends | `crates/ruvllm/src/backends/mod.rs:756` | +| `Tokenizer` | Pluggable tokenization | Trait object behind `Option<&dyn Tokenizer>` | + +An intelligence provider follows the same pattern — a trait that external integrations implement, registered with the intelligence loader at startup. + +## Decision + +**Option B — Trait-Based Intelligence Providers**, with a built-in file-based provider as the default implementation. + +This gives the extensibility of a trait interface while keeping the simplicity of file-based exchange for the common case. Non-Rust systems write a JSON file; a built-in `FileSignalProvider` reads it. Rust-native integrations can implement the trait directly for tighter control. + +## Architecture + +``` +IntelligenceLoader (new component in intelligence module) +├── register_provider(Box) +├── load_all_signals() -> Vec +│ ├── iterate registered providers +│ ├── call provider.load_signals() +│ └── merge with optional quality_weights() +└── Built-in: FileSignalProvider + ├── reads JSON from .claude/intelligence/data/ + └── returns Vec +``` + +### Integration Points + +| Component | How Signals Flow In | +|-----------|-------------------| +| SONA Instant Loop | `QualitySignal.quality_score` → trajectory quality | +| SONA Background Loop | Batch of signals → router training data | +| Embedding Classifier | `task_description` → embedding, `outcome` → label | +| Model Router | `calibration_bias()` on `TaskComplexityAnalyzer` | + +### Key Types + +```rust +pub struct QualitySignal { + pub id: String, + pub task_description: String, + pub outcome: String, // "success", "partial_success", "failure" + pub quality_score: f32, // 0.0 - 1.0 + pub human_verdict: Option, + pub quality_factors: Option, + pub completed_at: String, // ISO 8601 +} + +pub struct QualityFactors { + pub acceptance_criteria_met: Option, + pub tests_passing: Option, + pub no_regressions: Option, + pub lint_clean: Option, + pub type_check_clean: Option, + pub follows_patterns: Option, + pub context_relevance: Option, + pub reasoning_coherence: Option, + pub execution_efficiency: Option, +} + +pub trait IntelligenceProvider: Send + Sync { + fn name(&self) -> &str; + fn load_signals(&self) -> Result>; + fn quality_weights(&self) -> Option { None } +} +``` + +## Design Constraints + +- **Zero overhead when unused.** No providers registered = no behavior change. +- **File-based by default.** Simplest provider reads a JSON file — no network calls. +- **No automatic weight changes.** Providers supply signals; weight changes are human decisions. +- **Backward compatible.** Existing loading continues unchanged. Providers are additive. + +## Existing Code References + +| Item | Status | Location | +|------|--------|----------| +| `LlmBackend` trait | EXISTS | `crates/ruvllm/src/backends/mod.rs:756` | +| `record_feedback()` | EXISTS | `crates/ruvllm/src/claude_flow/model_router.rs:646` | +| `QualityWeights` (metrics) | EXISTS | `crates/ruvllm/src/quality/metrics.rs:262` | +| `IntelligenceProvider` trait | NEW | `crates/ruvllm/src/intelligence/mod.rs` | +| `FileSignalProvider` | NEW | `crates/ruvllm/src/intelligence/mod.rs` | +| `IntelligenceLoader` | NEW | `crates/ruvllm/src/intelligence/mod.rs` | +| `calibration_bias()` | NEW | `crates/ruvllm/src/claude_flow/model_router.rs` | + +## Implementation + +### Files Created + +| # | Path | Description | +|---|------|-------------| +| 1 | `crates/ruvllm/src/intelligence/mod.rs` | IntelligenceProvider trait, QualitySignal, FileSignalProvider, IntelligenceLoader | +| 2 | `docs/adr/ADR-043-external-intelligence-providers.md` | This ADR | + +### Files Modified + +| # | Path | Changes | +|---|------|---------| +| 1 | `crates/ruvllm/src/lib.rs` | Add `pub mod intelligence;` + re-exports | +| 2 | `crates/ruvllm/src/claude_flow/model_router.rs` | Add `calibration_bias()` to TaskComplexityAnalyzer | + +## Consequences + +### Positive + +1. **Clean integration boundary.** External systems implement one trait instead of modifying ruvllm internals. +2. **Follows established patterns.** Same approach as `LlmBackend` — familiar to anyone who has extended ruvllm. +3. **Language-agnostic in practice.** Non-Rust systems write JSON; `FileSignalProvider` reads it. +4. **Graceful when absent.** No providers = no behavior change. File missing = empty signal set. +5. **Testable.** Providers can be unit-tested independently. + +### Negative + +1. One more trait to maintain (small surface: 2 required methods, 1 optional). +2. Non-Rust systems must use the file path unless they write a Rust wrapper. + +## Related Decisions + +- **ADR-002**: RuvLLM Integration with Ruvector — Witness Log schema with `quality_score: f32` +- **ADR-029**: RVF Canonical Format — (the existing ADR-029, not to be confused with this one) +- **ADR-CE-021**: Shared SONA — multiple external systems contributing trajectories +- **ADR-004**: KV Cache Management — tiered, policy-driven approach benefiting from better calibration