use crate::agent::{AgentRegistry, RegisteredCard};
use crate::error::LaserError;
use crate::provenance::{AgentTopic, Provenance};
use crate::types::{AgentId, PrincipalId};
use laser_wire::agent::CapabilityDescriptor;
#[derive(Debug, Clone)]
pub enum Router {
To(AgentId),
ToPrincipal {
agent: AgentId,
principal: PrincipalId,
},
Broadcast,
ToCapable(CapabilitySelector),
AllCapable(CapabilitySelector),
}
#[derive(Debug, Clone)]
pub struct CapabilitySelector {
pub skill: String,
pub policy: RoutePolicy,
pub principal: Option<PrincipalId>,
}
impl CapabilitySelector {
pub fn new(skill: impl Into<String>, policy: RoutePolicy) -> Self {
Self {
skill: skill.into(),
policy,
principal: None,
}
}
pub fn principal(mut self, principal: PrincipalId) -> Self {
self.principal = Some(principal);
self
}
}
#[derive(Debug, Clone, Default)]
pub enum InboxRoute {
#[default]
Advertised,
Fixed(AgentTopic<'static>),
}
impl InboxRoute {
pub fn resolve(
&self,
agent: &AgentId,
advertised: Option<&str>,
) -> Result<iggy::prelude::Identifier, LaserError> {
let topic = match self {
Self::Fixed(topic) => topic.topic_string(),
Self::Advertised => advertised
.ok_or_else(|| LaserError::NoInbox {
agent: agent.as_str().to_owned(),
})?
.to_owned(),
};
iggy::prelude::Identifier::named(&topic).map_err(|error| {
LaserError::HandlerConfig(format!("inbox topic `{topic}` is invalid: {error}"))
})
}
}
#[derive(Clone, Default)]
pub enum RoutePolicy {
Cheapest,
Fastest,
LeastLoaded,
Sticky(AgentId),
#[default]
Any,
Custom(std::sync::Arc<dyn RouteScorer>),
}
impl std::fmt::Debug for RoutePolicy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Cheapest => write!(f, "Cheapest"),
Self::Fastest => write!(f, "Fastest"),
Self::LeastLoaded => write!(f, "LeastLoaded"),
Self::Sticky(agent) => f.debug_tuple("Sticky").field(agent).finish(),
Self::Any => write!(f, "Any"),
Self::Custom(_) => write!(f, "Custom(..)"),
}
}
}
pub trait RouteScorer: Send + Sync {
fn select(&self, skill_id: &str, candidates: &[RouteCandidate<'_>]) -> Option<usize>;
}
pub struct RouteCandidate<'a> {
pub agent: &'a AgentId,
pub card: &'a RegisteredCard,
pub capability: Option<CapabilityDescriptor>,
}
fn select(skill_id: &str, candidates: &[&RegisteredCard], policy: &RoutePolicy) -> Option<AgentId> {
let view: Vec<RouteCandidate<'_>> = candidates
.iter()
.map(|card| RouteCandidate {
agent: &card.agent,
card,
capability: card
.card
.capabilities
.iter()
.find(|capability| capability.skill_id == skill_id)
.cloned(),
})
.collect();
let chosen = match policy {
RoutePolicy::Any => view.first(),
RoutePolicy::Sticky(agent) => view
.iter()
.find(|candidate| candidate.agent == agent)
.or_else(|| view.first()),
RoutePolicy::Cheapest => view.iter().min_by_key(|candidate| {
candidate
.capability
.as_ref()
.and_then(|d| d.cost_class)
.unwrap_or(u8::MAX)
}),
RoutePolicy::Fastest => view.iter().min_by_key(|candidate| {
candidate
.capability
.as_ref()
.and_then(|d| d.latency_class)
.unwrap_or(u8::MAX)
}),
RoutePolicy::LeastLoaded => view.iter().min_by_key(|candidate| {
candidate
.capability
.as_ref()
.and_then(|d| d.load)
.unwrap_or(u16::MAX)
}),
RoutePolicy::Custom(scorer) => scorer.select(skill_id, &view).and_then(|i| view.get(i)),
};
chosen.map(|candidate| candidate.agent.clone())
}
impl Router {
pub fn to(agent: AgentId) -> Self {
Self::To(agent)
}
pub fn to_principal(agent: AgentId, principal: PrincipalId) -> Self {
Self::ToPrincipal { agent, principal }
}
pub fn broadcast() -> Self {
Self::Broadcast
}
pub fn to_capable(skill: impl Into<String>, policy: RoutePolicy) -> Self {
Self::ToCapable(CapabilitySelector::new(skill, policy))
}
pub fn all_capable(skill: impl Into<String>, policy: RoutePolicy) -> Self {
Self::AllCapable(CapabilitySelector::new(skill, policy))
}
pub fn apply(&self, provenance: &mut Provenance) {
match self {
Self::To(agent) | Self::ToPrincipal { agent, .. } => {
provenance.target_agent_id = Some(agent.clone());
}
Self::Broadcast | Self::ToCapable(_) | Self::AllCapable(_) => {
provenance.target_agent_id = None;
}
}
}
pub fn resolve_targets(
&self,
registry: &AgentRegistry,
now_micros: u64,
) -> Result<Vec<AgentId>, LaserError> {
match self {
Self::To(agent) => Ok(vec![agent.clone()]),
Self::ToPrincipal { agent, principal } => {
require_principal(registry, agent, *principal)?;
Ok(vec![agent.clone()])
}
Self::Broadcast => Ok(Vec::new()),
Self::ToCapable(selector) => {
let candidates = trusted_candidates(registry, selector, now_micros)?;
select(&selector.skill, &candidates, &selector.policy)
.map(|agent| vec![agent])
.ok_or_else(|| LaserError::NoCapableAgent {
skill: selector.skill.clone(),
})
}
Self::AllCapable(selector) => {
let agents: Vec<AgentId> = trusted_candidates(registry, selector, now_micros)?
.iter()
.map(|card| card.agent.clone())
.collect();
if agents.is_empty() {
Err(LaserError::NoCapableAgent {
skill: selector.skill.clone(),
})
} else {
Ok(agents)
}
}
}
}
#[cfg(feature = "query")]
pub(crate) fn requires_presence(&self) -> bool {
matches!(self, Self::ToPrincipal { .. })
|| matches!(self, Self::ToCapable(selector) | Self::AllCapable(selector) if selector.principal.is_some())
}
#[cfg(any(feature = "sign", test))]
pub(crate) fn required_principal(&self) -> Option<PrincipalId> {
match self {
Self::ToPrincipal { principal, .. } => Some(*principal),
Self::ToCapable(selector) | Self::AllCapable(selector) => selector.principal,
Self::To(_) | Self::Broadcast => None,
}
}
}
fn trusted_candidates<'a>(
registry: &'a AgentRegistry<'_>,
selector: &CapabilitySelector,
now_micros: u64,
) -> Result<Vec<&'a RegisteredCard>, LaserError> {
let candidates = registry.resolve(&selector.skill, now_micros);
let Some(principal) = selector.principal else {
return Ok(candidates);
};
let trusted = filter_candidates_by_principal(&candidates, principal, |agent| {
registry.principal_for(agent)
});
if trusted.is_empty()
&& let Some(card) = candidates.first()
{
return Err(principal_mismatch(registry, &card.agent, principal));
}
Ok(trusted)
}
fn filter_candidates_by_principal<'a>(
candidates: &[&'a RegisteredCard],
required: PrincipalId,
principal_for: impl Fn(&AgentId) -> Option<PrincipalId>,
) -> Vec<&'a RegisteredCard> {
candidates
.iter()
.copied()
.filter(|card| principal_for(&card.agent) == Some(required))
.collect()
}
fn require_principal(
registry: &AgentRegistry<'_>,
agent: &AgentId,
principal: PrincipalId,
) -> Result<(), LaserError> {
if registry.principal_for(agent) == Some(principal) {
Ok(())
} else {
Err(principal_mismatch(registry, agent, principal))
}
}
fn principal_mismatch(
registry: &AgentRegistry<'_>,
agent: &AgentId,
expected: PrincipalId,
) -> LaserError {
LaserError::RoutePrincipalMismatch {
agent: agent.to_string(),
expected: expected.get(),
actual: registry.principal_for(agent).map(PrincipalId::get),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::ConversationId;
#[test]
fn given_a_route_to_an_agent_when_applied_then_should_set_the_target() {
let mut provenance = Provenance::builder()
.conversation_id(ConversationId::new())
.build();
Router::to("executor".parse().expect("executor is a valid agent id"))
.apply(&mut provenance);
assert_eq!(
provenance
.target_agent_id
.expect("the target should be set")
.as_str(),
"executor"
);
}
#[test]
fn given_a_broadcast_route_when_applied_then_should_clear_the_target() {
let mut provenance = Provenance::builder()
.conversation_id(ConversationId::new())
.target_agent_id("executor".parse().expect("executor is a valid agent id"))
.build();
Router::broadcast().apply(&mut provenance);
assert!(provenance.target_agent_id.is_none());
}
fn card(
agent: &str,
cost: Option<u8>,
latency: Option<u8>,
load: Option<u16>,
) -> RegisteredCard {
use laser_wire::agent::{AgentCard, CapabilityDescriptor};
RegisteredCard {
agent: agent.parse().expect("valid agent id"),
card: AgentCard {
name: None,
version: None,
capabilities: vec![CapabilityDescriptor {
skill_id: "diagnose".to_owned(),
input: None,
output: None,
cost_class: cost,
latency_class: latency,
max_concurrency: None,
health: None,
load,
}],
ttl_micros: None,
},
observed_at_micros: 0,
}
}
#[test]
fn given_candidates_when_selected_by_policy_then_should_pick_the_best_by_that_class() {
let cheap = card("cheap", Some(1), Some(9), Some(900));
let fast = card("fast", Some(9), Some(1), Some(500));
let idle = card("idle", Some(5), Some(5), Some(10));
let candidates = [&cheap, &fast, &idle];
let pick = |policy| select("diagnose", &candidates, &policy).map(|a| a.as_str().to_owned());
assert_eq!(pick(RoutePolicy::Cheapest).as_deref(), Some("cheap"));
assert_eq!(pick(RoutePolicy::Fastest).as_deref(), Some("fast"));
assert_eq!(pick(RoutePolicy::LeastLoaded).as_deref(), Some("idle"));
assert_eq!(
pick(RoutePolicy::Sticky("fast".parse().expect("valid agent id"))).as_deref(),
Some("fast")
);
assert!(pick(RoutePolicy::Sticky("gone".parse().expect("valid agent id"))).is_some());
}
#[test]
fn given_a_principal_bound_selector_when_filtering_then_should_drop_foreign_claims() {
let trusted = card("trusted", Some(1), None, None);
let foreign = card("foreign", Some(2), None, None);
let candidates = [&trusted, &foreign];
let filtered = filter_candidates_by_principal(&candidates, PrincipalId::new(7), |agent| {
match agent.as_str() {
"trusted" => Some(PrincipalId::new(7)),
"foreign" => Some(PrincipalId::new(9)),
_ => None,
}
});
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].agent.as_str(), "trusted");
}
#[test]
fn given_a_principal_bound_route_when_reading_required_identity_then_should_return_principal() {
let route = Router::to_principal(
"metrics".parse().expect("metrics is a valid agent id"),
PrincipalId::new(42),
);
assert_eq!(route.required_principal(), Some(PrincipalId::new(42)));
}
struct HighestLoad;
impl RouteScorer for HighestLoad {
fn select(&self, _skill_id: &str, candidates: &[RouteCandidate<'_>]) -> Option<usize> {
(0..candidates.len()).max_by_key(|&i| {
candidates[i]
.capability
.as_ref()
.and_then(|d| d.load)
.unwrap_or(0)
})
}
}
struct RefuseAll;
impl RouteScorer for RefuseAll {
fn select(&self, _skill_id: &str, _candidates: &[RouteCandidate<'_>]) -> Option<usize> {
None
}
}
#[test]
fn given_a_custom_scorer_when_selected_then_should_rank_over_the_same_candidate_view() {
let cheap = card("cheap", Some(1), Some(9), Some(900));
let idle = card("idle", Some(5), Some(5), Some(10));
let candidates = [&cheap, &idle];
let busiest = select(
"diagnose",
&candidates,
&RoutePolicy::Custom(std::sync::Arc::new(HighestLoad)),
);
assert_eq!(
busiest.map(|a| a.as_str().to_owned()).as_deref(),
Some("cheap")
);
let refused = select(
"diagnose",
&candidates,
&RoutePolicy::Custom(std::sync::Arc::new(RefuseAll)),
);
assert!(refused.is_none());
assert!(select("diagnose", &[], &RoutePolicy::Any).is_none());
}
#[test]
fn given_a_fixed_inbox_route_when_resolved_then_should_use_that_topic_ignoring_presence() {
let agent: AgentId = "metrics".parse().expect("valid agent id");
let route = InboxRoute::Fixed(AgentTopic::Sessions);
let id = route
.resolve(&agent, Some("some.other.topic"))
.expect("a fixed route resolves");
assert_eq!(id.to_string(), "agent.sessions");
}
#[test]
fn given_an_advertised_route_when_an_inbox_is_present_then_should_resolve_to_it() {
let agent: AgentId = "metrics".parse().expect("valid agent id");
let id = InboxRoute::Advertised
.resolve(&agent, Some("metrics.work.2026"))
.expect("an advertised inbox resolves");
assert_eq!(id.to_string(), "metrics.work.2026");
}
#[test]
fn given_an_advertised_route_when_no_inbox_then_should_error_without_a_fallback() {
let agent: AgentId = "metrics".parse().expect("valid agent id");
let error = InboxRoute::Advertised
.resolve(&agent, None)
.expect_err("no inbox fails");
assert!(
matches!(error, LaserError::NoInbox { agent } if agent == "metrics"),
"advertised route with no inbox must fail loud, never fall back to a shared topic",
);
}
}