feat: Phase 5.3 - Multi-Agent Learning Infrastructure
Implement intelligent agent learning from Knowledge Graph execution history
with per-task-type expertise tracking, recency bias, and learning curves.
## Phase 5.3 Implementation
### Learning Infrastructure (✅ Complete)
- LearningProfileService with per-task-type expertise metrics
- TaskTypeExpertise model tracking success_rate, confidence, learning curves
- Recency bias weighting: recent 7 days weighted 3x higher (exponential decay)
- Confidence scoring prevents overfitting: min(1.0, executions / 20)
- Learning curves computed from daily execution windows
### Agent Scoring Service (✅ Complete)
- Unified AgentScore combining SwarmCoordinator + learning profiles
- Scoring formula: 0.3*base + 0.5*expertise + 0.2*confidence
- Rank agents by combined score for intelligent assignment
- Support for recency-biased scoring (recent_success_rate)
- Methods: rank_agents, select_best, rank_agents_with_recency
### KG Integration (✅ Complete)
- KGPersistence::get_executions_for_task_type() - query by agent + task type
- KGPersistence::get_agent_executions() - all executions for agent
- Coordinator::load_learning_profile_from_kg() - core KG→Learning integration
- Coordinator::load_all_learning_profiles() - batch load for multiple agents
- Convert PersistedExecution → ExecutionData for learning calculations
### Agent Assignment Integration (✅ Complete)
- AgentCoordinator uses learning profiles for task assignment
- extract_task_type() infers task type from title/description
- assign_task() scores candidates using AgentScoringService
- Fallback to load-based selection if no learning data available
- Learning profiles stored in coordinator.learning_profiles RwLock
### Profile Adapter Enhancements (✅ Complete)
- create_learning_profile() - initialize empty profiles
- add_task_type_expertise() - set task-type expertise
- update_profile_with_learning() - update swarm profiles from learning
## Files Modified
### vapora-knowledge-graph/src/persistence.rs (+30 lines)
- get_executions_for_task_type(agent_id, task_type, limit)
- get_agent_executions(agent_id, limit)
### vapora-agents/src/coordinator.rs (+100 lines)
- load_learning_profile_from_kg() - core KG integration method
- load_all_learning_profiles() - batch loading for agents
- assign_task() already uses learning-based scoring via AgentScoringService
### Existing Complete Implementation
- vapora-knowledge-graph/src/learning.rs - calculation functions
- vapora-agents/src/learning_profile.rs - data structures and expertise
- vapora-agents/src/scoring.rs - unified scoring service
- vapora-agents/src/profile_adapter.rs - adapter methods
## Tests Passing
- learning_profile: 7 tests ✅
- scoring: 5 tests ✅
- profile_adapter: 6 tests ✅
- coordinator: learning-specific tests ✅
## Data Flow
1. Task arrives → AgentCoordinator::assign_task()
2. Extract task_type from description
3. Query KG for task-type executions (load_learning_profile_from_kg)
4. Calculate expertise with recency bias
5. Score candidates (SwarmCoordinator + learning)
6. Assign to top-scored agent
7. Execution result → KG → Update learning profiles
## Key Design Decisions
✅ Recency bias: 7-day half-life with 3x weight for recent performance
✅ Confidence scoring: min(1.0, total_executions / 20) prevents overfitting
✅ Hierarchical scoring: 30% base load, 50% expertise, 20% confidence
✅ KG query limit: 100 recent executions per task-type for performance
✅ Async loading: load_learning_profile_from_kg supports concurrent loads
## Next: Phase 5.4 - Cost Optimization
Ready to implement budget enforcement and cost-aware provider selection.
2026-01-11 13:03:53 +00:00
|
|
|
// vapora-agents: Agent configuration module
|
|
|
|
|
// Load and parse agent definitions from TOML
|
|
|
|
|
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
use std::path::Path;
|
|
|
|
|
use thiserror::Error;
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Error)]
|
|
|
|
|
pub enum ConfigError {
|
|
|
|
|
#[error("Failed to read config file: {0}")]
|
|
|
|
|
ReadError(#[from] std::io::Error),
|
|
|
|
|
|
|
|
|
|
#[error("Failed to parse TOML: {0}")]
|
|
|
|
|
ParseError(#[from] toml::de::Error),
|
|
|
|
|
|
|
|
|
|
#[error("Invalid configuration: {0}")]
|
|
|
|
|
ValidationError(String),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct AgentConfig {
|
|
|
|
|
pub registry: RegistryConfig,
|
|
|
|
|
pub agents: Vec<AgentDefinition>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct RegistryConfig {
|
|
|
|
|
#[serde(default = "default_max_agents")]
|
|
|
|
|
pub max_agents_per_role: u32,
|
|
|
|
|
#[serde(default = "default_health_check_interval")]
|
|
|
|
|
pub health_check_interval: u64,
|
|
|
|
|
#[serde(default = "default_agent_timeout")]
|
|
|
|
|
pub agent_timeout: u64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn default_max_agents() -> u32 {
|
|
|
|
|
5
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn default_health_check_interval() -> u64 {
|
|
|
|
|
30
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn default_agent_timeout() -> u64 {
|
|
|
|
|
300
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct AgentDefinition {
|
|
|
|
|
pub role: String,
|
|
|
|
|
pub description: String,
|
|
|
|
|
pub llm_provider: String,
|
|
|
|
|
pub llm_model: String,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub parallelizable: bool,
|
|
|
|
|
#[serde(default = "default_priority")]
|
|
|
|
|
pub priority: u32,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub capabilities: Vec<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn default_priority() -> u32 {
|
|
|
|
|
50
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl AgentConfig {
|
|
|
|
|
/// Load configuration from TOML file
|
|
|
|
|
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, ConfigError> {
|
|
|
|
|
let content = std::fs::read_to_string(path)?;
|
|
|
|
|
let config: Self = toml::from_str(&content)?;
|
|
|
|
|
config.validate()?;
|
|
|
|
|
Ok(config)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Load configuration from environment or default file
|
|
|
|
|
pub fn from_env() -> Result<Self, ConfigError> {
|
|
|
|
|
let config_path = std::env::var("VAPORA_AGENT_CONFIG")
|
|
|
|
|
.unwrap_or_else(|_| "/etc/vapora/agents.toml".to_string());
|
|
|
|
|
|
|
|
|
|
if Path::new(&config_path).exists() {
|
|
|
|
|
Self::load(&config_path)
|
|
|
|
|
} else {
|
|
|
|
|
// Return default config if file doesn't exist
|
|
|
|
|
Ok(Self::default())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Validate configuration
|
|
|
|
|
fn validate(&self) -> Result<(), ConfigError> {
|
|
|
|
|
// Check that all agent roles are unique
|
|
|
|
|
let mut roles = std::collections::HashSet::new();
|
|
|
|
|
for agent in &self.agents {
|
|
|
|
|
if !roles.insert(&agent.role) {
|
|
|
|
|
return Err(ConfigError::ValidationError(format!(
|
|
|
|
|
"Duplicate agent role: {}",
|
|
|
|
|
agent.role
|
|
|
|
|
)));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check that we have at least one agent
|
|
|
|
|
if self.agents.is_empty() {
|
|
|
|
|
return Err(ConfigError::ValidationError(
|
|
|
|
|
"No agents defined in configuration".to_string(),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get agent definition by role
|
|
|
|
|
pub fn get_by_role(&self, role: &str) -> Option<&AgentDefinition> {
|
|
|
|
|
self.agents.iter().find(|a| a.role == role)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// List all agent roles
|
|
|
|
|
pub fn list_roles(&self) -> Vec<String> {
|
|
|
|
|
self.agents.iter().map(|a| a.role.clone()).collect()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for AgentConfig {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
registry: RegistryConfig {
|
|
|
|
|
max_agents_per_role: default_max_agents(),
|
|
|
|
|
health_check_interval: default_health_check_interval(),
|
|
|
|
|
agent_timeout: default_agent_timeout(),
|
|
|
|
|
},
|
2026-01-11 21:32:56 +00:00
|
|
|
agents: vec![AgentDefinition {
|
|
|
|
|
role: "developer".to_string(),
|
|
|
|
|
description: "Code developer".to_string(),
|
|
|
|
|
llm_provider: "claude".to_string(),
|
|
|
|
|
llm_model: "claude-sonnet-4".to_string(),
|
|
|
|
|
parallelizable: true,
|
|
|
|
|
priority: 80,
|
|
|
|
|
capabilities: vec!["coding".to_string()],
|
|
|
|
|
}],
|
feat: Phase 5.3 - Multi-Agent Learning Infrastructure
Implement intelligent agent learning from Knowledge Graph execution history
with per-task-type expertise tracking, recency bias, and learning curves.
## Phase 5.3 Implementation
### Learning Infrastructure (✅ Complete)
- LearningProfileService with per-task-type expertise metrics
- TaskTypeExpertise model tracking success_rate, confidence, learning curves
- Recency bias weighting: recent 7 days weighted 3x higher (exponential decay)
- Confidence scoring prevents overfitting: min(1.0, executions / 20)
- Learning curves computed from daily execution windows
### Agent Scoring Service (✅ Complete)
- Unified AgentScore combining SwarmCoordinator + learning profiles
- Scoring formula: 0.3*base + 0.5*expertise + 0.2*confidence
- Rank agents by combined score for intelligent assignment
- Support for recency-biased scoring (recent_success_rate)
- Methods: rank_agents, select_best, rank_agents_with_recency
### KG Integration (✅ Complete)
- KGPersistence::get_executions_for_task_type() - query by agent + task type
- KGPersistence::get_agent_executions() - all executions for agent
- Coordinator::load_learning_profile_from_kg() - core KG→Learning integration
- Coordinator::load_all_learning_profiles() - batch load for multiple agents
- Convert PersistedExecution → ExecutionData for learning calculations
### Agent Assignment Integration (✅ Complete)
- AgentCoordinator uses learning profiles for task assignment
- extract_task_type() infers task type from title/description
- assign_task() scores candidates using AgentScoringService
- Fallback to load-based selection if no learning data available
- Learning profiles stored in coordinator.learning_profiles RwLock
### Profile Adapter Enhancements (✅ Complete)
- create_learning_profile() - initialize empty profiles
- add_task_type_expertise() - set task-type expertise
- update_profile_with_learning() - update swarm profiles from learning
## Files Modified
### vapora-knowledge-graph/src/persistence.rs (+30 lines)
- get_executions_for_task_type(agent_id, task_type, limit)
- get_agent_executions(agent_id, limit)
### vapora-agents/src/coordinator.rs (+100 lines)
- load_learning_profile_from_kg() - core KG integration method
- load_all_learning_profiles() - batch loading for agents
- assign_task() already uses learning-based scoring via AgentScoringService
### Existing Complete Implementation
- vapora-knowledge-graph/src/learning.rs - calculation functions
- vapora-agents/src/learning_profile.rs - data structures and expertise
- vapora-agents/src/scoring.rs - unified scoring service
- vapora-agents/src/profile_adapter.rs - adapter methods
## Tests Passing
- learning_profile: 7 tests ✅
- scoring: 5 tests ✅
- profile_adapter: 6 tests ✅
- coordinator: learning-specific tests ✅
## Data Flow
1. Task arrives → AgentCoordinator::assign_task()
2. Extract task_type from description
3. Query KG for task-type executions (load_learning_profile_from_kg)
4. Calculate expertise with recency bias
5. Score candidates (SwarmCoordinator + learning)
6. Assign to top-scored agent
7. Execution result → KG → Update learning profiles
## Key Design Decisions
✅ Recency bias: 7-day half-life with 3x weight for recent performance
✅ Confidence scoring: min(1.0, total_executions / 20) prevents overfitting
✅ Hierarchical scoring: 30% base load, 50% expertise, 20% confidence
✅ KG query limit: 100 recent executions per task-type for performance
✅ Async loading: load_learning_profile_from_kg supports concurrent loads
## Next: Phase 5.4 - Cost Optimization
Ready to implement budget enforcement and cost-aware provider selection.
2026-01-11 13:03:53 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_default_values() {
|
|
|
|
|
let config = AgentConfig {
|
|
|
|
|
registry: RegistryConfig {
|
|
|
|
|
max_agents_per_role: 5,
|
|
|
|
|
health_check_interval: 30,
|
|
|
|
|
agent_timeout: 300,
|
|
|
|
|
},
|
|
|
|
|
agents: vec![AgentDefinition {
|
|
|
|
|
role: "developer".to_string(),
|
|
|
|
|
description: "Code developer".to_string(),
|
|
|
|
|
llm_provider: "claude".to_string(),
|
|
|
|
|
llm_model: "claude-sonnet-4".to_string(),
|
|
|
|
|
parallelizable: true,
|
|
|
|
|
priority: 80,
|
|
|
|
|
capabilities: vec!["coding".to_string()],
|
|
|
|
|
}],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
assert!(config.validate().is_ok());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_duplicate_roles() {
|
|
|
|
|
let config = AgentConfig {
|
|
|
|
|
registry: RegistryConfig {
|
|
|
|
|
max_agents_per_role: 5,
|
|
|
|
|
health_check_interval: 30,
|
|
|
|
|
agent_timeout: 300,
|
|
|
|
|
},
|
|
|
|
|
agents: vec![
|
|
|
|
|
AgentDefinition {
|
|
|
|
|
role: "developer".to_string(),
|
|
|
|
|
description: "Code developer 1".to_string(),
|
|
|
|
|
llm_provider: "claude".to_string(),
|
|
|
|
|
llm_model: "claude-sonnet-4".to_string(),
|
|
|
|
|
parallelizable: true,
|
|
|
|
|
priority: 80,
|
|
|
|
|
capabilities: vec![],
|
|
|
|
|
},
|
|
|
|
|
AgentDefinition {
|
|
|
|
|
role: "developer".to_string(),
|
|
|
|
|
description: "Code developer 2".to_string(),
|
|
|
|
|
llm_provider: "claude".to_string(),
|
|
|
|
|
llm_model: "claude-sonnet-4".to_string(),
|
|
|
|
|
parallelizable: true,
|
|
|
|
|
priority: 80,
|
|
|
|
|
capabilities: vec![],
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
assert!(config.validate().is_err());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_get_by_role() {
|
|
|
|
|
let config = AgentConfig {
|
|
|
|
|
registry: RegistryConfig {
|
|
|
|
|
max_agents_per_role: 5,
|
|
|
|
|
health_check_interval: 30,
|
|
|
|
|
agent_timeout: 300,
|
|
|
|
|
},
|
|
|
|
|
agents: vec![AgentDefinition {
|
|
|
|
|
role: "architect".to_string(),
|
|
|
|
|
description: "System architect".to_string(),
|
|
|
|
|
llm_provider: "claude".to_string(),
|
|
|
|
|
llm_model: "claude-opus-4".to_string(),
|
|
|
|
|
parallelizable: false,
|
|
|
|
|
priority: 100,
|
|
|
|
|
capabilities: vec!["architecture".to_string()],
|
|
|
|
|
}],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let agent = config.get_by_role("architect");
|
|
|
|
|
assert!(agent.is_some());
|
|
|
|
|
assert_eq!(agent.unwrap().description, "System architect");
|
|
|
|
|
|
|
|
|
|
assert!(config.get_by_role("nonexistent").is_none());
|
|
|
|
|
}
|
|
|
|
|
}
|