Vapora/crates/vapora-agents/src/swarm_adapter.rs
Jesús Pérez 2f76728481
Some checks failed
Rust CI / Security Audit (push) Has been cancelled
Rust CI / Check + Test + Lint (nightly) (push) Has been cancelled
Rust CI / Check + Test + Lint (stable) (push) Has been cancelled
feat: integrate NatsBridge with real JetStream into A2A server
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
2026-02-17 22:28:51 +00:00

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,
})
}
}