vapora-agents:
- Add nats_bridge.rs with real async_nats JetStream (submit_task, durable
pull consumer, list_agents from live registry)
- Replace swarm_adapter.rs stubs with real SwarmCoordinator calls
(select_agent via bidding, report_completion with load update, agent_load
from fractional profile)
- Expose SwarmCoordinator::get_agent() for per-agent profile access
vapora-a2a:
- CoordinatorBridge: replace raw NatsClient with NatsBridge (JetStream
at-least-once delivery via durable pull consumer)
- Add GET /a2a/agents endpoint listing registered agents
- task_manager::create(): switch .content() to parameterized INSERT INTO
to avoid SurrealDB serializer failing on adjacently-tagged enums
- task_manager::get(): explicit field projection, exclude id (Thing),
cast datetimes with type::string() to fix serde_json::Value deserialization
- Integration tests: 4/5 pass with SurrealDB + NATS
vapora-leptos-ui:
- Set doctest = false in [lib]: Leptos components require WASM reactive
runtime, incompatible with native cargo test --doc
113 lines
3.9 KiB
Rust
113 lines
3.9 KiB
Rust
use std::sync::Arc;
|
|
|
|
use async_trait::async_trait;
|
|
use uuid::Uuid;
|
|
use vapora_swarm::coordinator::SwarmCoordinator;
|
|
|
|
use crate::coordination::{AgentAssignment, AgentLoad, AgentProfile, SwarmCoordination};
|
|
|
|
/// Adapter: SwarmCoordination → SwarmCoordinator
|
|
/// Implements the coordination abstraction using the real swarm coordinator.
|
|
pub struct SwarmCoordinationAdapter {
|
|
swarm: Arc<SwarmCoordinator>,
|
|
}
|
|
|
|
impl SwarmCoordinationAdapter {
|
|
pub fn new(swarm: Arc<SwarmCoordinator>) -> Self {
|
|
Self { swarm }
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl SwarmCoordination for SwarmCoordinationAdapter {
|
|
async fn register_profiles(&self, profiles: Vec<AgentProfile>) -> anyhow::Result<()> {
|
|
for profile in profiles {
|
|
let swarm_profile = vapora_swarm::messages::AgentProfile {
|
|
id: profile.id.clone(),
|
|
roles: vec![profile.role.clone()],
|
|
capabilities: vec![profile.role],
|
|
current_load: 0.0,
|
|
availability: true,
|
|
success_rate: profile.success_rate,
|
|
};
|
|
self.swarm.register_agent(swarm_profile)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Select best agent via swarm bidding.
|
|
///
|
|
/// Uses `submit_task_for_bidding` which applies load-balanced scoring
|
|
/// (success_rate / (1 + current_load)) across all available agents with
|
|
/// matching capabilities.
|
|
async fn select_agent(
|
|
&self,
|
|
task_type: &str,
|
|
required_expertise: Option<&str>,
|
|
) -> anyhow::Result<AgentAssignment> {
|
|
let capabilities: Vec<String> = match required_expertise {
|
|
Some(exp) => vec![task_type.to_string(), exp.to_string()],
|
|
None => vec![task_type.to_string()],
|
|
};
|
|
|
|
// Use a ephemeral task_id for selection; the caller manages actual task IDs.
|
|
let selection_id = Uuid::new_v4().to_string();
|
|
|
|
let agent_id = self
|
|
.swarm
|
|
.submit_task_for_bidding(selection_id, task_type.to_string(), capabilities)
|
|
.await
|
|
.map_err(|e| anyhow::anyhow!("Swarm bidding failed: {}", e))?
|
|
.ok_or_else(|| anyhow::anyhow!("No available agent for task_type: {}", task_type))?;
|
|
|
|
let confidence = self
|
|
.swarm
|
|
.get_agent(&agent_id)
|
|
.map(|profile| profile.success_rate)
|
|
.unwrap_or(0.5);
|
|
|
|
Ok(AgentAssignment {
|
|
// Swarm profiles use ID as display name (no separate name field)
|
|
agent_name: agent_id.clone(),
|
|
agent_id,
|
|
confidence,
|
|
})
|
|
}
|
|
|
|
/// Report task completion and update agent load in the swarm.
|
|
///
|
|
/// On success the agent is marked available with minimal load.
|
|
/// On failure the agent receives a penalty load (0.5) to deprioritize it
|
|
/// in future selections until it recovers.
|
|
async fn report_completion(
|
|
&self,
|
|
agent_id: &str,
|
|
success: bool,
|
|
_duration_ms: u64,
|
|
) -> anyhow::Result<()> {
|
|
let new_load = if success { 0.0 } else { 0.5 };
|
|
self.swarm
|
|
.update_agent_status(agent_id, new_load, true)
|
|
.map_err(|e| anyhow::anyhow!("Failed to update agent status: {}", e))
|
|
}
|
|
|
|
/// Query current agent load from swarm profile.
|
|
///
|
|
/// Infers `current_tasks` from the fractional load stored in the swarm
|
|
/// profile (each task represents ~10% of a capacity-10 agent).
|
|
async fn agent_load(&self, agent_id: &str) -> anyhow::Result<AgentLoad> {
|
|
let profile = self
|
|
.swarm
|
|
.get_agent(agent_id)
|
|
.ok_or_else(|| anyhow::anyhow!("Agent not found in swarm: {}", agent_id))?;
|
|
|
|
const CAPACITY: usize = 10;
|
|
let current_tasks = (profile.current_load * CAPACITY as f64).round() as usize;
|
|
|
|
Ok(AgentLoad {
|
|
agent_id: agent_id.to_string(),
|
|
current_tasks,
|
|
capacity: CAPACITY,
|
|
})
|
|
}
|
|
}
|