use crate::agent::consumer::{
AgentHandler, AgentMiddleware, ConcurrencyPolicy, DeadLetterSink, Deduplicator,
ReliableConsumer, RetryPolicy,
};
use crate::agent::router::InboxRoute;
use crate::error::LaserError;
use crate::laser::Laser;
use crate::provenance::AgentTopic;
use crate::types::{AgentId, ConsumerGroupName};
use laser_wire::agent::{AgentCard, CapabilityDescriptor};
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::oneshot;
use tokio::task::JoinHandle;
use tracing::warn;
#[derive(bon::Builder)]
pub struct Agent<H> {
pub id: AgentId,
pub consumer_group: Option<ConsumerGroupName>,
pub listen_on: AgentTopic<'static>,
pub handler: H,
pub respond_on: Option<AgentTopic<'static>>,
#[builder(default)]
pub inbox_route: InboxRoute,
pub poll_interval: Option<Duration>,
pub shutdown_grace: Option<Duration>,
pub concurrency: Option<ConcurrencyPolicy>,
pub max_queued_records: Option<usize>,
pub max_queued_bytes: Option<usize>,
#[builder(default)]
pub warm_dedup: bool,
#[builder(default)]
pub middleware: Vec<Arc<dyn AgentMiddleware>>,
pub on_dead_letter: Option<Arc<dyn DeadLetterSink>>,
pub dedup_window: Option<usize>,
pub retry: Option<RetryPolicy>,
#[builder(default = laser_wire::agent::features::NONE)]
pub understood_features: u64,
pub deduplicator: Option<Box<dyn Deduplicator>>,
#[cfg(feature = "sign")]
pub verifier: Option<std::sync::Arc<crate::sign::KeyRegistry>>,
#[cfg(feature = "sign")]
pub signing_key: Option<std::sync::Arc<crate::sign::SigningKey>>,
#[builder(default)]
pub capabilities: Vec<CapabilityDescriptor>,
#[builder(default)]
pub ack_on_pickup: bool,
pub consolidate_every: Option<Duration>,
pub consolidator: Option<crate::memory::SharedConsolidator>,
pub governor: Option<(
Arc<dyn crate::govern::ActionGovernor>,
crate::govern::GovernorMode,
)>,
pub governor_retention: Option<crate::govern::GovernorRetention>,
}
impl<H> Agent<H>
where
H: AgentHandler + Sync + Send + 'static,
{
#[tracing::instrument(target = "laser", level = "info", skip_all, fields(agent = %self.id, operation = "spawn"))]
pub fn spawn(self, laser: Laser) -> AgentHandle {
let laser = match self.governor {
Some((governor, mode)) => match self.governor_retention {
Some(retention) => laser.with_governor_retention(governor, mode, retention),
None => laser.with_governor(governor, mode),
},
None => laser,
};
let id = self.id.clone();
let listen_on = self.listen_on.clone();
let capabilities = self.capabilities;
let group = self
.consumer_group
.unwrap_or_else(|| ConsumerGroupName::for_agent(&id));
let topic = self.listen_on.topic_string();
let handler = self.handler;
let respond_on = self.respond_on;
let inbox_route = self.inbox_route;
let ack_on_pickup = self.ack_on_pickup;
let poll_interval = self.poll_interval;
let warm_dedup = self.warm_dedup;
let deduplicator = self.deduplicator;
let shutdown_grace = self.shutdown_grace;
let concurrency = self.concurrency;
let max_queued_records = self.max_queued_records;
let max_queued_bytes = self.max_queued_bytes;
let dedup_window = self.dedup_window;
let retry = self.retry;
let understood_features = self.understood_features;
let middleware = self.middleware;
let on_dead_letter = self.on_dead_letter;
#[cfg(feature = "sign")]
let verifier = self.verifier;
#[cfg(feature = "sign")]
let signing_key = self.signing_key;
let (shutdown, shutdown_rx) = oneshot::channel();
let (ready_tx, ready_rx) = oneshot::channel();
let consolidation = match (self.consolidate_every, self.consolidator) {
(Some(every), Some(consolidator)) => Some(tokio::spawn(async move {
let mut tick = tokio::time::interval(every);
tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
loop {
tick.tick().await;
let scope = crate::memory::MemoryScope::default();
if let Err(error) =
crate::memory::Consolidator::consolidate(&consolidator, &scope).await
{
warn!(%error, "background consolidation pass failed");
}
}
})),
_ => None,
};
let task = tokio::spawn(async move {
if !capabilities.is_empty() {
advertise(&laser, &id, &listen_on, capabilities).await?;
}
let consumer = ReliableConsumer::builder()
.group(group)
.agent(id)
.topic(topic)
.maybe_respond_on(respond_on)
.inbox_route(inbox_route)
.ack_on_pickup(ack_on_pickup)
.maybe_poll_interval(poll_interval)
.maybe_shutdown_grace(shutdown_grace)
.maybe_concurrency(concurrency)
.maybe_max_queued_records(max_queued_records)
.maybe_max_queued_bytes(max_queued_bytes)
.warm_dedup(warm_dedup)
.maybe_deduplicator(deduplicator)
.maybe_dedup_window(dedup_window)
.maybe_retry(retry)
.understood_features(understood_features)
.middleware(middleware)
.maybe_on_dead_letter(on_dead_letter);
#[cfg(feature = "sign")]
let consumer = consumer
.maybe_verifier(verifier)
.maybe_signing_key(signing_key);
consumer
.build()
.run(&laser, handler, ready_tx, shutdown_rx)
.await
});
AgentHandle {
shutdown,
task,
ready: Some(ready_rx),
consolidation,
}
}
}
pub(crate) async fn advertise(
laser: &Laser,
id: &AgentId,
listen_on: &AgentTopic<'static>,
capabilities: Vec<CapabilityDescriptor>,
) -> Result<(), LaserError> {
let card = AgentCard {
name: None,
version: None,
capabilities,
ttl_micros: None,
};
if let Err(error) = laser.publish_card(id.clone(), &card).await {
warn!(%error, agent = %id, "failed to publish the capability card");
}
#[cfg(feature = "query")]
{
let presence = laser_wire::agent::AgentPresence::new(id.wire_id())
.with_inbox(listen_on.topic_string());
if let Err(error) = laser.advertise_presence(&presence).await {
if matches!(error, LaserError::PresenceConflict { .. }) {
return Err(error);
}
tracing::debug!(%error, agent = %id, "inbox presence not advertised");
}
}
#[cfg(not(feature = "query"))]
let _ = listen_on;
Ok(())
}
pub struct AgentHandle {
shutdown: oneshot::Sender<()>,
task: JoinHandle<Result<(), LaserError>>,
ready: Option<oneshot::Receiver<()>>,
consolidation: Option<JoinHandle<()>>,
}
impl AgentHandle {
pub async fn ready(&mut self) -> Result<(), LaserError> {
if let Some(ready) = self.ready.take() {
ready.await.map_err(|_| {
LaserError::HandlerConfig("agent stopped before it became ready".to_owned())
})?;
}
Ok(())
}
pub async fn shutdown(self) -> Result<(), LaserError> {
let _ = self.shutdown.send(());
if let Some(consolidation) = &self.consolidation {
consolidation.abort();
}
Self::join_task(self.task).await
}
pub async fn join(self) -> Result<(), LaserError> {
let result = Self::join_task(self.task).await;
if let Some(consolidation) = &self.consolidation {
consolidation.abort();
}
result
}
pub fn abort(&self) {
self.task.abort();
if let Some(consolidation) = &self.consolidation {
consolidation.abort();
}
}
async fn join_task(task: JoinHandle<Result<(), LaserError>>) -> Result<(), LaserError> {
match task.await {
Ok(result) => result,
Err(join) => Err(LaserError::HandlerConfig(join.to_string())),
}
}
}