1use crate::agent::budget::BudgetGate;
2use crate::agent::clock::{Clock, SystemClock};
3use crate::agent::control::ControlBook;
4use crate::agent::ctx::AgentCtx;
5use crate::agent::pause::{Hold, PauseRuntime, Replay, SourceTopic};
6use crate::agent::session::{SessionConfig, Sessions};
7use crate::capabilities::HelloOutcome;
8use crate::error::LaserError;
9use crate::laser::Laser;
10use crate::provenance::{AgentTopic, LlmUsage, Provenance};
11use crate::stream::{CommitPolicy, Consumer, ConsumerMessage, Headers};
12use crate::types::{AgentId, ConsumerGroupName, ConversationId, MessageId};
13use async_trait::async_trait;
14use futures::FutureExt;
15use iggy::prelude::*;
16use laser_wire::agent::{
17 AgentDeadLetter, AgentEnvelope, AgentErrorBody, AgentErrorCode, AgentKind, DeadLetterReason,
18 LogPosition, OPERATION_TASK, SignatureContext, TaskState, features, validate,
19};
20use laser_wire::codes::AGENT_OP_VERSION;
21use laser_wire::content::ContentType;
22use laser_wire::dispatch::{
23 Dispatch, HandledOperations, addressee_filter, classify, classify_generic,
24};
25use laser_wire::framing::{decode_named, encode_named};
26use laser_wire::headers::{AGENT_VERSION, CONTENT_TYPE, CONVERSATION_ID, FENCE};
27use laser_wire::topics::{AGENT_CONTROL, AGENT_SESSIONS};
28use std::collections::{BTreeMap, HashSet, VecDeque};
29use std::str::FromStr;
30use std::sync::{Arc, Mutex};
31use std::time::Duration;
32use tokio::sync::oneshot;
33use tokio::time::sleep;
34use tracing::{debug, error, warn};
35
36pub(crate) fn backoff_for(attempt: u32) -> Duration {
39 crate::retry::CONSUMER.delay(attempt)
40}
41
42const DEDUP_SCOPE_SEP: char = '\u{1f}';
47
48const FENCE_MAP_SOFT_CAP: usize = 16_384;
52
53#[cfg(feature = "sign")]
57const VERIFIED_RECORD_WINDOW: usize = 16_384;
58
59const FENCE_ENTRY_TTL_MICROS: u64 = 600_000_000;
64
65const FENCE_SWEEP_INTERVAL_MICROS: u64 = 1_000_000;
68
69const POLL_INTERVAL: Duration = Duration::from_millis(10);
73
74const DEFAULT_SHUTDOWN_GRACE: Duration = Duration::from_secs(30);
78const DEFAULT_MAX_QUEUED_RECORDS: usize = 4_096;
79const DEFAULT_MAX_QUEUED_BYTES: usize = 64 * 1024 * 1024;
80
81#[trait_variant::make(AgentHandler: Send)]
83pub trait LocalAgentHandler {
84 async fn handle(&self, message: &AgentMessage, ctx: &AgentCtx<'_>) -> Result<(), LaserError>;
85}
86
87#[async_trait]
95pub trait AgentMiddleware: Send + Sync {
96 async fn before_handle(&self, message: &AgentMessage) -> Result<(), LaserError> {
97 let _ = message;
98 Ok(())
99 }
100
101 async fn after_handle(
102 &self,
103 message: &AgentMessage,
104 result: &Result<(), LaserError>,
105 attempt: u32,
106 ) {
107 let _ = (message, result, attempt);
108 }
109}
110
111#[async_trait]
119pub trait DeadLetterSink: Send + Sync {
120 async fn on_dead_letter(
121 &self,
122 message: Option<&AgentMessage>,
123 capsule: &AgentDeadLetter,
124 publish_result: &Result<(), LaserError>,
125 );
126}
127
128#[derive(Debug, Clone)]
130pub struct AgentMessage {
131 pub provenance: Provenance,
135 pub payload: Vec<u8>,
138 pub id: MessageId,
140 pub envelope: Option<AgentEnvelope>,
143 pub content_type: Option<ContentType>,
148 pub verified_principal: Option<String>,
151}
152
153impl AgentMessage {
154 pub fn body(&self) -> &[u8] {
160 match &self.envelope {
161 Some(envelope) => &envelope.body,
162 None => &self.payload,
163 }
164 }
165
166 fn from_consumer(
170 received: &ConsumerMessage,
171 understood_features: u64,
172 ) -> Result<DecodedAgentMessage, (Box<LaserError>, Vec<u8>)> {
173 let id = received.position;
174 let payload = received.payload.to_vec();
175 if received.headers_malformed {
176 return Err((
177 Box::new(LaserError::Invalid(
178 "the record's header block does not decode".to_owned(),
179 )),
180 payload,
181 ));
182 }
183 let decoded = match decode_record_parts(
184 &received.headers,
185 &received.payload,
186 received.timestamp_micros,
187 understood_features,
188 ) {
189 Ok(decoded) => decoded,
190 Err(error) => return Err((Box::new(error), payload)),
191 };
192 Ok(DecodedAgentMessage {
193 message: Self {
194 provenance: decoded.provenance,
195 payload,
196 id,
197 envelope: decoded.envelope,
198 content_type: decoded.content_type,
199 verified_principal: None,
200 },
201 #[cfg(feature = "sign")]
202 signature_context: decoded.signature_context,
203 #[cfg(feature = "sign")]
204 observed_at_micros: decoded.observed_at_micros,
205 })
206 }
207}
208
209struct DecodedAgentMessage {
210 message: AgentMessage,
211 #[cfg(feature = "sign")]
212 signature_context: Option<SignatureContext>,
213 #[cfg(feature = "sign")]
214 observed_at_micros: u64,
215}
216
217pub(crate) struct DecodedAgentRecord {
218 pub(crate) provenance: Provenance,
219 pub(crate) envelope: Option<AgentEnvelope>,
220 pub(crate) content_type: Option<ContentType>,
221 pub(crate) signature_context: Option<SignatureContext>,
222 pub(crate) observed_at_micros: u64,
223}
224
225pub(crate) fn decode_agent_record(
226 message: &IggyMessage,
227 understood_features: u64,
228) -> Result<DecodedAgentRecord, LaserError> {
229 let headers = message.user_headers_map()?.unwrap_or_default();
230 decode_record_parts(
231 &headers,
232 &message.payload,
233 message.header.timestamp,
234 understood_features,
235 )
236}
237
238fn decode_record_parts(
240 headers: &Headers,
241 payload: &[u8],
242 observed_at_micros: u64,
243 understood_features: u64,
244) -> Result<DecodedAgentRecord, LaserError> {
245 let content_type_key = HeaderKey::from_str(CONTENT_TYPE)?;
246 let content_type_code = headers
247 .get(&content_type_key)
248 .map(HeaderValue::as_uint8)
249 .transpose()?;
250 let content_type = content_type_code.and_then(ContentType::from_code);
251 let version_key = HeaderKey::from_str(AGENT_VERSION)?;
252 let version = headers
253 .get(&version_key)
254 .map(HeaderValue::as_uint32)
255 .transpose()?;
256 let (provenance, envelope, signature_context) = match version {
257 Some(version) => {
258 if version != AGENT_OP_VERSION {
259 return Err(LaserError::Invalid(format!(
260 "unsupported agent envelope version {version}"
261 )));
262 }
263 let envelope: AgentEnvelope = decode_named(payload)?;
264 validate(&envelope)?;
265 let unmet = envelope.unmet_requirements(understood_features);
266 if unmet != features::NONE {
267 return Err(LaserError::Invalid(format!(
268 "agent envelope requires unsupported features 0x{unmet:016x}"
269 )));
270 }
271 let provenance = provenance_from_envelope(&envelope);
272 let context = SignatureContext {
273 content_type: content_type_code,
274 agent_version: Some(version),
275 };
276 (provenance, Some(envelope), Some(context))
277 }
278 None => (
279 crate::provenance::provenance_from_headers(headers)?,
280 None,
281 None,
282 ),
283 };
284 Ok(DecodedAgentRecord {
285 provenance,
286 envelope,
287 content_type,
288 signature_context,
289 observed_at_micros,
290 })
291}
292
293pub(crate) fn provenance_and_envelope(
300 message: &IggyMessage,
301) -> Result<(Provenance, Option<AgentEnvelope>), LaserError> {
302 let decoded = decode_agent_record(message, features::NONE)?;
303 Ok((decoded.provenance, decoded.envelope))
304}
305
306fn original_conversation(headers: &Headers) -> Option<ConversationId> {
309 let value = headers.get(&HeaderKey::from_str(CONVERSATION_ID).ok()?)?;
310 match value.kind() {
311 HeaderKind::Uint128 => {
312 Some(laser_wire::agent::ConversationId::from_u128(value.as_uint128().ok()?).into())
313 }
314 _ => value.as_str().ok()?.parse().ok(),
315 }
316}
317
318fn provenance_from_envelope(envelope: &AgentEnvelope) -> Provenance {
323 Provenance::builder()
324 .conversation_id(envelope.conversation.into())
325 .maybe_parent_conversation_id(envelope.parent.map(Into::into))
326 .maybe_root_conversation_id(envelope.root.map(Into::into))
327 .maybe_agent(AgentId::try_from(envelope.source.as_str()).ok())
328 .maybe_target_agent_id(
329 envelope
330 .target
331 .as_ref()
332 .and_then(|target| AgentId::try_from(target.as_str()).ok()),
333 )
334 .maybe_idempotency_key(
335 envelope
336 .idempotency_key
337 .as_ref()
338 .map(|key| key.as_str().to_owned()),
339 )
340 .maybe_correlation_id(
341 envelope
342 .correlation
343 .map(|correlation| correlation.to_string()),
344 )
345 .maybe_deadline(envelope.deadline_micros.map(IggyTimestamp::from))
346 .maybe_usage(envelope.usage.map(|usage| LlmUsage {
347 input_tokens: Some(usage.input_tokens),
348 output_tokens: Some(usage.output_tokens),
349 cost_usd: None,
350 }))
351 .maybe_fence_token(envelope.metadata.as_ref().and_then(
355 |metadata| match metadata.get(FENCE) {
356 Some(laser_wire::query::Value::Uint(fence)) => Some(*fence),
357 Some(laser_wire::query::Value::Int(fence)) => u64::try_from(*fence).ok(),
358 _ => None,
359 },
360 ))
361 .build()
362}
363
364#[derive(Debug, Clone, Copy)]
366pub struct RetryPolicy {
367 pub max_attempts: u32,
369 pub base_delay: Duration,
371}
372
373impl RetryPolicy {
374 pub fn backoff(max_attempts: u32, base_delay: Duration) -> Self {
376 Self {
377 max_attempts,
378 base_delay,
379 }
380 }
381
382 fn delay_for(&self, attempt: u32) -> Duration {
383 self.base_delay
384 .saturating_mul(2u32.saturating_pow(attempt.min(16)))
385 }
386}
387
388impl Default for RetryPolicy {
389 fn default() -> Self {
390 Self {
391 max_attempts: 5,
392 base_delay: Duration::from_millis(200),
393 }
394 }
395}
396
397#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
399pub enum ConcurrencyPolicy {
400 #[default]
403 Serial,
404 SerialPerPartition {
411 max_partitions: usize,
413 },
414}
415
416#[derive(bon::Builder)]
418pub struct ReliableConsumer {
419 pub group: ConsumerGroupName,
420 pub agent: Option<AgentId>,
423 #[builder(into)]
424 pub topic: String,
425 #[builder(default = 10_000)]
426 pub dedup_window: usize,
427 #[builder(default)]
428 pub retry: RetryPolicy,
429 #[builder(default = features::NONE)]
432 pub understood_features: u64,
433 #[builder(default = POLL_INTERVAL)]
435 pub poll_interval: Duration,
436 #[builder(default = DEFAULT_SHUTDOWN_GRACE)]
440 pub shutdown_grace: Duration,
441 #[builder(default)]
444 pub concurrency: ConcurrencyPolicy,
445 #[builder(default = DEFAULT_MAX_QUEUED_RECORDS)]
447 pub max_queued_records: usize,
448 #[builder(default = DEFAULT_MAX_QUEUED_BYTES)]
450 pub max_queued_bytes: usize,
451 pub respond_on: Option<AgentTopic<'static>>,
452 #[builder(default)]
454 pub inbox_route: crate::agent::router::InboxRoute,
455 #[builder(default)]
460 pub ack_on_pickup: bool,
461 pub deduplicator: Option<Box<dyn Deduplicator>>,
464 #[builder(default)]
468 pub warm_dedup: bool,
469 #[builder(default)]
472 pub middleware: Vec<std::sync::Arc<dyn AgentMiddleware>>,
473 pub on_dead_letter: Option<std::sync::Arc<dyn DeadLetterSink>>,
476 pub operations: Option<Vec<String>>,
481 pub sessions: Option<SessionConfig>,
486 #[cfg(feature = "sign")]
491 pub verifier: Option<std::sync::Arc<crate::sign::KeyRegistry>>,
492 #[cfg(feature = "sign")]
495 pub signing_key: Option<std::sync::Arc<crate::sign::SigningKey>>,
496}
497
498impl ReliableConsumer {
499 pub async fn run<H>(
511 mut self,
512 laser: &Laser,
513 handler: H,
514 ready: oneshot::Sender<()>,
515 shutdown: oneshot::Receiver<()>,
516 ) -> Result<(), LaserError>
517 where
518 H: AgentHandler + Sync + Send + 'static,
519 {
520 let stream = laser.stream_required()?.to_owned();
521 let engine = DeliveryEngine::resolve(laser).await?;
522 let me = self.agent.as_ref().map(AgentId::wire_id);
523 if let Some(me) = &me {
524 engine
525 .bind_addressee(laser, &stream, &self.topic, self.group.as_str(), me)
526 .await?;
527 }
528 let opener = Opener {
529 laser: laser.clone(),
530 stream: stream.clone(),
531 topic: self.topic.clone(),
532 group: self.group.as_str().to_owned(),
533 poll_interval: self.poll_interval,
534 native: engine.native,
535 };
536 let consumer = opener.open().await?;
537
538 let deduplicator = self
539 .deduplicator
540 .take()
541 .unwrap_or_else(|| Box::new(SlidingWindow::new(self.dedup_window)));
542 if self.warm_dedup {
543 warm_dedup_window(laser, &self, deduplicator.as_ref()).await?;
544 }
545 let stream_ident = Identifier::named(&stream)?;
552 let topic_ident = Identifier::named(&self.topic)?;
553 let stream_id = laser
554 .client()
555 .get_stream(&stream_ident)
556 .await?
557 .map(|details| details.id);
558 let topic_details = laser
559 .client()
560 .get_topic(&stream_ident, &topic_ident)
561 .await?;
562 let topic_id = topic_details.as_ref().map(|details| details.id);
563 let topic_generation = topic_details
564 .as_ref()
565 .map_or(0, |details| details.created_at.as_micros());
566 if stream_id.is_none() || topic_id.is_none() {
567 warn!(
568 topic = %self.topic,
569 "could not resolve the numeric stream/topic id, dead-letter capsules \
570 carry 0 for the unresolved locator half (partition and offset stay correct)"
571 );
572 }
573 let source = match (stream_id, topic_id) {
574 (Some(stream_id), Some(topic_id)) => Some(SourceTopic {
575 stream_id,
576 topic_id,
577 generation: topic_generation,
578 }),
579 _ => None,
580 };
581 let (stream_id, topic_id) = (stream_id.unwrap_or_default(), topic_id.unwrap_or_default());
582 let sessions = laser.sessions_with(self.sessions.clone().unwrap_or_default());
583
584 let control = Arc::new(ControlBook::default());
592 let (reconcile, reconcile_requests) = tokio::sync::mpsc::unbounded_channel();
593 let mut pause = None;
594 let mut late_control = false;
595 let mut control_follower = if self.topic == AGENT_CONTROL {
596 None
597 } else {
598 match crate::agent::pause::load_control(laser, me.as_ref(), &control).await? {
599 Some(feed) => {
600 match (&me, source) {
601 (Some(me), Some(source)) => {
602 pause = Some(Arc::new(PauseRuntime::new(
603 sessions.clone(),
604 me.clone(),
605 Arc::clone(&control),
606 &feed,
607 source,
608 reconcile.clone(),
609 )));
610 }
611 (Some(_), None) => {
612 warn!(topic = %self.topic, "the source topic ids did not resolve, sessions of this agent cannot be paused");
613 }
614 (None, _) => {}
615 }
616 control.set_live(true);
617 let (stop, stopped) = oneshot::channel();
618 let task = tokio::spawn(crate::agent::pause::follow_control(
619 laser.clone(),
620 feed,
621 me.clone(),
622 Arc::clone(&control),
623 pause.is_some().then(|| reconcile.clone()),
624 self.poll_interval,
625 stopped,
626 ));
627 Some((stop, task))
628 }
629 None => {
633 late_control = true;
634 None
635 }
636 }
637 };
638 drop(reconcile);
639
640 let probe_interval = sessions.config().heartbeat_value();
641 let reliable = Arc::new(ReliableWorker {
642 handler,
643 laser: laser.clone(),
644 retry: self.retry,
645 understood_features: self.understood_features,
646 dedup: deduplicator,
647 agent: self.agent,
648 respond_on: self.respond_on,
649 inbox_route: self.inbox_route,
650 ack_on_pickup: self.ack_on_pickup,
651 stream_id,
652 topic_id,
653 middleware: self.middleware,
654 on_dead_letter: self.on_dead_letter,
655 topic: self.topic.clone(),
656 operations: self.operations,
657 sessions,
658 control,
659 pause: Arc::new(
660 pause
661 .clone()
662 .map_or_else(std::sync::OnceLock::new, std::sync::OnceLock::from),
663 ),
664 budget: BudgetGate::default(),
665 high_water_fence: dashmap::DashMap::new(),
666 fence_last_sweep: std::sync::atomic::AtomicU64::new(0),
667 #[cfg(feature = "sign")]
668 verifier: self.verifier,
669 #[cfg(feature = "sign")]
670 signing_key: self.signing_key,
671 #[cfg(feature = "sign")]
672 verified_records: Mutex::new(DedupWindow::new(VERIFIED_RECORD_WINDOW)),
673 });
674
675 let pause_driver = match &pause {
679 Some(runtime) => {
680 if let Err(error) = runtime.recover().await {
681 stop_follower(control_follower).await;
682 return Err(error);
683 }
684 let (stop, stopped) = oneshot::channel();
685 let worker: Arc<dyn Replay> = reliable.clone();
686 let task = tokio::spawn(crate::agent::pause::drive(
687 Arc::clone(runtime),
688 worker,
689 reconcile_requests,
690 stopped,
691 ));
692 Some((stop, task))
693 }
694 None => None,
695 };
696
697 let late_driver: LateDriver = Arc::default();
700 if late_control {
701 let start_pause = match (&me, source) {
702 (Some(me), Some(source)) => {
703 let slot = Arc::clone(&reliable.pause);
704 let sessions = reliable.sessions.clone();
705 let me = me.clone();
706 let book = Arc::clone(&reliable.control);
707 let worker: Arc<dyn Replay> = reliable.clone();
708 let driver = Arc::clone(&late_driver);
709 let start: crate::agent::pause::StartPause = Box::new(move |feed| {
710 let (reconcile, requests) = tokio::sync::mpsc::unbounded_channel();
711 let runtime = Arc::new(PauseRuntime::new(
712 sessions,
713 me,
714 book,
715 feed,
716 source,
717 reconcile.clone(),
718 ));
719 Box::pin(async move {
720 start_late_pause(runtime, worker, requests, reconcile, slot, driver)
721 .await
722 })
723 });
724 Some(start)
725 }
726 _ => None,
727 };
728 let (stop, stopped) = oneshot::channel();
729 let task = tokio::spawn(crate::agent::pause::discover_then_follow_control(
730 laser.clone(),
731 me.clone(),
732 Arc::clone(&reliable.control),
733 start_pause,
734 self.poll_interval,
735 stopped,
736 ));
737 control_follower = Some((stop, task));
738 }
739
740 let _late = AbortLateOnDrop(Arc::clone(&late_driver));
742 let _background = AbortOnDrop(
743 control_follower
744 .iter()
745 .map(|(_, task)| task.abort_handle())
746 .chain(pause_driver.iter().map(|(_, task)| task.abort_handle()))
747 .collect(),
748 );
749
750 let _ = ready.send(());
753 let stop = async {
756 if shutdown.await.is_err() {
757 std::future::pending::<()>().await;
758 }
759 }
760 .fuse();
761 let result = match self.concurrency {
762 ConcurrencyPolicy::Serial => {
763 run_serial(&opener, consumer, &reliable, stop, self.shutdown_grace).await
764 }
765 ConcurrencyPolicy::SerialPerPartition { max_partitions } => {
766 let limits = LaneLimits {
767 max_partitions: max_partitions.max(1),
768 max_queued_records: self.max_queued_records.clamp(1, u32::MAX as usize),
769 max_queued_bytes: self.max_queued_bytes.clamp(1, u32::MAX as usize),
770 probe_interval,
771 };
772 run_per_partition(
773 &opener,
774 consumer,
775 Arc::clone(&reliable),
776 limits,
777 stop,
778 self.shutdown_grace,
779 )
780 .await
781 }
782 };
783 if let Some((stop, mut task)) = pause_driver {
784 let _ = stop.send(());
785 if tokio::time::timeout(self.shutdown_grace, &mut task)
786 .await
787 .is_err()
788 {
789 warn!("the pause driver did not finish within the shutdown grace, aborting it");
790 task.abort();
791 }
792 }
793 let late = late_driver
794 .lock()
795 .unwrap_or_else(std::sync::PoisonError::into_inner)
796 .take();
797 if let Some((stop, mut task)) = late {
798 let _ = stop.send(());
799 if tokio::time::timeout(self.shutdown_grace, &mut task)
800 .await
801 .is_err()
802 {
803 task.abort();
804 }
805 }
806 stop_follower(control_follower).await;
807 result
808 }
809}
810
811type LateDriver = Arc<Mutex<Option<(oneshot::Sender<()>, tokio::task::JoinHandle<()>)>>>;
813
814struct AbortLateOnDrop(LateDriver);
816
817impl Drop for AbortLateOnDrop {
818 fn drop(&mut self) {
819 if let Some((_, task)) = self
820 .0
821 .lock()
822 .unwrap_or_else(std::sync::PoisonError::into_inner)
823 .take()
824 {
825 task.abort();
826 }
827 }
828}
829
830async fn start_late_pause(
834 runtime: Arc<PauseRuntime>,
835 worker: Arc<dyn Replay>,
836 requests: tokio::sync::mpsc::UnboundedReceiver<ConversationId>,
837 reconcile: tokio::sync::mpsc::UnboundedSender<ConversationId>,
838 slot: Arc<std::sync::OnceLock<Arc<PauseRuntime>>>,
839 driver: LateDriver,
840) -> Option<tokio::sync::mpsc::UnboundedSender<ConversationId>> {
841 let mut attempt = 0;
842 loop {
843 match runtime.recover().await {
844 Ok(()) => break,
845 Err(error) => {
846 attempt += 1;
847 if attempt >= crate::retry::CONSUMER_FAILURE_BUDGET {
848 warn!(%error, "the pause runtime for the late control topic did not recover, sessions of this agent cannot be paused");
849 return None;
850 }
851 tokio::time::sleep(crate::retry::CONSUMER.delay(attempt)).await;
852 }
853 }
854 }
855 if slot.set(Arc::clone(&runtime)).is_err() {
856 return None;
857 }
858 let (stop, stopped) = oneshot::channel();
859 let task = tokio::spawn(crate::agent::pause::drive(
860 runtime, worker, requests, stopped,
861 ));
862 *driver
863 .lock()
864 .unwrap_or_else(std::sync::PoisonError::into_inner) = Some((stop, task));
865 Some(reconcile)
866}
867
868struct AbortOnDrop(Vec<tokio::task::AbortHandle>);
871
872impl Drop for AbortOnDrop {
873 fn drop(&mut self) {
874 for task in &self.0 {
875 task.abort();
876 }
877 }
878}
879
880async fn stop_follower(follower: Option<(oneshot::Sender<()>, tokio::task::JoinHandle<()>)>) {
882 if let Some((stop, task)) = follower {
883 let _ = stop.send(());
884 let _ = task.await;
885 }
886}
887
888#[derive(Clone, Copy)]
891struct DeliveryEngine {
892 native: bool,
894 filters: bool,
897}
898
899impl DeliveryEngine {
900 async fn resolve(laser: &Laser) -> Result<Self, LaserError> {
901 let mut capabilities = laser.capabilities().await;
902 if capabilities.hello == HelloOutcome::Unknown {
903 capabilities = laser.refresh_capabilities().await;
904 }
905 if !crate::stream::transport::policy_aware(&capabilities)? {
908 return Ok(Self {
909 native: true,
910 filters: false,
911 });
912 }
913 if laser.connection_string().is_none() {
918 if capabilities.managed {
919 tracing::warn!(
920 "the agent reads natively because its client was not built from a connection string, so no group filter is bound"
921 );
922 }
923 return Ok(Self {
924 native: true,
925 filters: false,
926 });
927 }
928 Ok(Self {
929 native: false,
930 filters: capabilities.filters.native && capabilities.filters.catalog,
931 })
932 }
933
934 async fn bind_addressee(
938 &self,
939 laser: &Laser,
940 stream: &str,
941 topic: &str,
942 group: &str,
943 me: &laser_wire::agent::AgentId,
944 ) -> Result<(), LaserError> {
945 if !self.filters || (topic != AGENT_SESSIONS && topic != AGENT_CONTROL) {
946 return Ok(());
947 }
948 laser
949 .stream(stream)
950 .topic(topic)
951 .consumer_group(group)
952 .create()
953 .filter(addressee_filter(me))
954 .build()
955 .await
956 .map(|_| ())
957 }
958}
959
960#[derive(Clone)]
963struct Opener {
964 laser: Laser,
965 stream: String,
966 topic: String,
967 group: String,
968 poll_interval: Duration,
969 native: bool,
970}
971
972impl Opener {
973 async fn open(&self) -> Result<Consumer, LaserError> {
974 let builder = self
975 .laser
976 .stream(&self.stream)
977 .topic(&self.topic)
978 .consumer_group(&self.group)
979 .consumer()
980 .commit_policy(CommitPolicy::Disabled)
981 .poll_interval(self.poll_interval)
982 .create_group(true);
983 let builder = if self.native {
984 builder.native()
985 } else {
986 builder
987 };
988 builder.build().await
989 }
990
991 async fn reopen(&self, consumer: &mut Consumer, failures: &mut u32) -> Result<(), LaserError> {
996 if let Err(error) = consumer.shutdown().await {
997 debug!(%error, topic = %self.topic, "the failed consumer did not leave its group cleanly");
998 }
999 loop {
1000 sleep(backoff_for(*failures)).await;
1001 match self.open().await {
1002 Ok(reopened) => {
1003 *consumer = reopened;
1004 return Ok(());
1005 }
1006 Err(error) if recoverable(&error, *failures + 1) => {
1007 *failures += 1;
1008 warn!(%error, attempt = *failures, topic = %self.topic, "reopening the agent consumer failed, retrying");
1009 }
1010 Err(error) => return Err(error),
1011 }
1012 }
1013 }
1014}
1015
1016pub(crate) const MAX_CONSECUTIVE_POLL_ERRORS: u32 = crate::retry::CONSUMER_FAILURE_BUDGET;
1019
1020fn recoverable(error: &LaserError, failures: u32) -> bool {
1026 let rejoin = matches!(
1027 error,
1028 LaserError::Iggy(IggyError::ConsumerGroupMemberNotFound(..))
1029 );
1030 (rejoin || error.is_retryable()) && failures < MAX_CONSECUTIVE_POLL_ERRORS
1031}
1032
1033async fn run_serial<H, S>(
1037 opener: &Opener,
1038 mut consumer: Consumer,
1039 worker: &ReliableWorker<H>,
1040 stop: S,
1041 shutdown_grace: Duration,
1042) -> Result<(), LaserError>
1043where
1044 H: AgentHandler + Sync + Send + 'static,
1045 S: std::future::Future<Output = ()> + futures::future::FusedFuture,
1046{
1047 tokio::pin!(stop);
1048 let mut failures = 0u32;
1049 let mut stopping = false;
1050 let result = loop {
1051 if stopping {
1052 break Ok(());
1053 }
1054 let next = tokio::select! {
1055 () = &mut stop => break Ok(()),
1056 next = consumer.next() => next,
1057 };
1058 let message = match next {
1059 None => break Ok(()),
1060 Some(Ok(message)) => message,
1061 Some(Err(error)) => {
1062 failures += 1;
1063 if !recoverable(&error, failures) {
1064 break Err(error);
1065 }
1066 warn!(%error, attempt = failures, topic = %opener.topic, "agent consumer failed, reopening it");
1067 if let Err(error) = opener.reopen(&mut consumer, &mut failures).await {
1068 break Err(error);
1069 }
1070 worker.invalidate_pause();
1071 continue;
1072 }
1073 };
1074 failures = 0;
1075 worker.observe_assignment(&consumer);
1076 let handled = worker.consume(&message);
1077 tokio::pin!(handled);
1078 let outcome = tokio::select! {
1079 outcome = &mut handled => outcome,
1080 () = &mut stop => {
1081 stopping = true;
1082 match tokio::time::timeout(shutdown_grace, &mut handled).await {
1083 Ok(outcome) => outcome,
1084 Err(_) => return Err(LaserError::Timeout("agent shutdown drain")),
1088 }
1089 }
1090 };
1091 if let Err(error) = outcome {
1092 break Err(error);
1093 }
1094 if let Err(error) = consumer.commit(&message).await {
1095 failures += 1;
1096 if !recoverable(&error, failures) {
1097 break Err(error);
1098 }
1099 warn!(%error, source = %message.position, "committing a handled record failed, reopening the consumer");
1100 if let Err(error) = opener.reopen(&mut consumer, &mut failures).await {
1101 break Err(error);
1102 }
1103 worker.invalidate_pause();
1104 }
1105 };
1106 if let Err(error) = consumer.shutdown().await {
1107 warn!(%error, "failed to leave the consumer group on shutdown");
1108 }
1109 result
1110}
1111
1112struct LaneLimits {
1114 max_partitions: usize,
1115 max_queued_records: usize,
1116 max_queued_bytes: usize,
1117 probe_interval: Duration,
1120}
1121
1122async fn run_per_partition<H, S>(
1131 opener: &Opener,
1132 mut consumer: Consumer,
1133 worker: Arc<ReliableWorker<H>>,
1134 limits: LaneLimits,
1135 stop: S,
1136 shutdown_grace: Duration,
1137) -> Result<(), LaserError>
1138where
1139 H: AgentHandler + Sync + Send + 'static,
1140 S: std::future::Future<Output = ()> + futures::future::FusedFuture,
1141{
1142 tokio::pin!(stop);
1143 let mut failures = 0u32;
1144 loop {
1145 let (outcome, drain) =
1146 drive_lanes(opener, &mut consumer, &worker, &limits, &mut stop).await;
1147 if drain.delivered {
1148 failures = 0;
1149 }
1150 let tolerate_commits = matches!(outcome, LaneOutcome::Failed(_));
1153 let drained =
1154 tokio::time::timeout(shutdown_grace, drain.finish(&consumer, tolerate_commits)).await;
1155 match outcome {
1156 LaneOutcome::Stopped => {
1157 return match drained {
1158 Ok(Ok(())) => {
1159 if let Err(error) = consumer.shutdown().await {
1164 warn!(%error, "failed to leave the consumer group on shutdown");
1165 }
1166 Ok(())
1167 }
1168 Ok(Err(error)) => Err(error),
1169 Err(_) => Err(LaserError::Timeout("agent shutdown drain")),
1170 };
1171 }
1172 LaneOutcome::Fatal(error) => {
1173 if matches!(drained, Ok(Ok(())))
1176 && let Err(error) = consumer.shutdown().await
1177 {
1178 warn!(%error, "failed to leave the consumer group on shutdown");
1179 }
1180 return Err(error);
1181 }
1182 LaneOutcome::Failed(error) => {
1183 if let Ok(Err(drain_error)) | Err(drain_error) =
1184 drained.map_err(|_| LaserError::Timeout("agent lane drain"))
1185 {
1186 return Err(drain_error);
1187 }
1188 failures += 1;
1189 if !recoverable(&error, failures) {
1190 return Err(error);
1191 }
1192 warn!(%error, attempt = failures, topic = %opener.topic, "agent consumer failed, reopening it");
1193 opener.reopen(&mut consumer, &mut failures).await?;
1194 worker.invalidate_pause();
1195 }
1196 }
1197 }
1198}
1199
1200enum LaneOutcome {
1201 Stopped,
1203 Failed(LaserError),
1205 Fatal(LaserError),
1207}
1208
1209struct Lane {
1210 sender: tokio::sync::mpsc::Sender<QueuedMessage>,
1211 task: tokio::task::JoinHandle<()>,
1212}
1213
1214struct LaneDrain {
1217 lanes: Vec<tokio::task::JoinHandle<()>>,
1218 completions: tokio::sync::mpsc::Receiver<Result<ConsumerMessage, LaserError>>,
1219 delivered: bool,
1221}
1222
1223impl LaneDrain {
1224 async fn finish(
1227 mut self,
1228 consumer: &Consumer,
1229 tolerate_commits: bool,
1230 ) -> Result<(), LaserError> {
1231 for lane in self.lanes {
1232 let _ = lane.await;
1233 }
1234 while let Some(completion) = self.completions.recv().await {
1235 let message = completion?;
1236 match consumer.commit(&message).await {
1237 Ok(()) => {}
1238 Err(error) if tolerate_commits => {
1239 debug!(%error, source = %message.position, "a handled record could not be committed, it is read again");
1240 }
1241 Err(error) => return Err(error),
1242 }
1243 }
1244 Ok(())
1245 }
1246}
1247
1248async fn drive_lanes<H, S>(
1249 opener: &Opener,
1250 consumer: &mut Consumer,
1251 worker: &Arc<ReliableWorker<H>>,
1252 limits: &LaneLimits,
1253 stop: &mut std::pin::Pin<&mut S>,
1254) -> (LaneOutcome, LaneDrain)
1255where
1256 H: AgentHandler + Sync + Send + 'static,
1257 S: std::future::Future<Output = ()> + futures::future::FusedFuture,
1258{
1259 let mut lanes: std::collections::HashMap<u32, Lane> = std::collections::HashMap::new();
1260 let mut retiring: std::collections::HashMap<u32, tokio::task::JoinHandle<()>> =
1264 std::collections::HashMap::new();
1265 let (commit_tx, mut commit_rx) = tokio::sync::mpsc::channel::<
1266 Result<ConsumerMessage, LaserError>,
1267 >(limits.max_partitions * 2);
1268 let notify = Arc::new(tokio::sync::Notify::new());
1269 let record_capacity = Arc::new(tokio::sync::Semaphore::new(limits.max_queued_records));
1270 let byte_capacity = Arc::new(tokio::sync::Semaphore::new(limits.max_queued_bytes));
1271 let mut probe = tokio::time::interval(limits.probe_interval);
1272 probe.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
1273 probe.reset();
1274 let mut delivered = false;
1275
1276 let outcome = 'polling: loop {
1277 while let Ok(completion) = commit_rx.try_recv() {
1280 match completion {
1281 Ok(message) => {
1282 if let Err(error) = consumer.commit_handled(&message).await {
1283 break 'polling LaneOutcome::Failed(error);
1284 }
1285 }
1286 Err(error) => break 'polling LaneOutcome::Fatal(error),
1287 }
1288 }
1289 tokio::select! {
1290 () = &mut *stop => break LaneOutcome::Stopped,
1291 () = notify.notified() => {}
1294 _ = probe.tick() => {
1295 drop_revoked(opener, consumer, &mut lanes, &mut retiring).await;
1296 }
1297 next = consumer.next() => match next {
1298 Some(Ok(message)) => {
1299 delivered = true;
1300 worker.observe_assignment(consumer);
1301 let partition = message.partition_id;
1302 if !lanes.contains_key(&partition) && lanes.len() >= limits.max_partitions {
1303 if let Err(error) = worker.consume(&message).await {
1308 break 'polling LaneOutcome::Fatal(error);
1309 }
1310 if let Err(error) = consumer.commit_handled(&message).await {
1311 break 'polling LaneOutcome::Failed(error);
1312 }
1313 continue;
1314 }
1315 let lane = lanes.entry(partition).or_insert_with(|| {
1316 spawn_lane(
1317 worker,
1318 retiring.remove(&partition),
1319 limits.max_queued_records,
1320 commit_tx.clone(),
1321 Arc::clone(¬ify),
1322 )
1323 });
1324 let buffered_bytes = message.payload.len()
1325 + message.user_headers.as_ref().map_or(0, bytes::Bytes::len);
1326 let Ok(record_permit) = Arc::clone(&record_capacity).acquire_owned().await
1327 else {
1328 break 'polling LaneOutcome::Fatal(LaserError::HandlerConfig(
1329 "record queue closed".to_owned(),
1330 ));
1331 };
1332 let byte_permits = buffered_bytes.clamp(1, limits.max_queued_bytes) as u32;
1333 let Ok(byte_permit) = Arc::clone(&byte_capacity)
1334 .acquire_many_owned(byte_permits)
1335 .await
1336 else {
1337 break 'polling LaneOutcome::Fatal(LaserError::HandlerConfig(
1338 "byte queue closed".to_owned(),
1339 ));
1340 };
1341 let queued = QueuedMessage {
1342 message,
1343 record_permit,
1344 byte_permit,
1345 };
1346 if lane.sender.send(queued).await.is_err() {
1347 continue;
1350 }
1351 }
1352 Some(Err(error)) => {
1353 if matches!(error, LaserError::Iggy(IggyError::ConsumerGroupMemberNotFound(..))) {
1354 drop_revoked(opener, consumer, &mut lanes, &mut retiring).await;
1355 }
1356 break LaneOutcome::Failed(error);
1357 }
1358 None => break LaneOutcome::Stopped,
1359 },
1360 }
1361 };
1362 let mut tasks: Vec<_> = lanes.into_values().map(|lane| lane.task).collect();
1364 tasks.extend(retiring.into_values());
1365 drop(commit_tx);
1366 (
1367 outcome,
1368 LaneDrain {
1369 lanes: tasks,
1370 completions: commit_rx,
1371 delivered,
1372 },
1373 )
1374}
1375
1376fn spawn_lane<H>(
1377 worker: &Arc<ReliableWorker<H>>,
1378 predecessor: Option<tokio::task::JoinHandle<()>>,
1379 capacity: usize,
1380 commits: tokio::sync::mpsc::Sender<Result<ConsumerMessage, LaserError>>,
1381 notify: Arc<tokio::sync::Notify>,
1382) -> Lane
1383where
1384 H: AgentHandler + Sync + Send + 'static,
1385{
1386 let (sender, mut queue) = tokio::sync::mpsc::channel::<QueuedMessage>(capacity);
1387 let worker = Arc::clone(worker);
1388 let task = tokio::spawn(async move {
1389 if let Some(predecessor) = predecessor {
1390 let _ = predecessor.await;
1391 }
1392 while let Some(queued) = queue.recv().await {
1393 let result = worker.consume(&queued.message).await;
1394 drop(queued.record_permit);
1395 drop(queued.byte_permit);
1396 let failed = result.is_err();
1397 let _ = commits.send(result.map(|()| queued.message)).await;
1398 notify.notify_one();
1399 if failed {
1400 break;
1401 }
1402 }
1403 });
1404 Lane { sender, task }
1405}
1406
1407async fn drop_revoked(
1412 opener: &Opener,
1413 consumer: &Consumer,
1414 lanes: &mut std::collections::HashMap<u32, Lane>,
1415 retiring: &mut std::collections::HashMap<u32, tokio::task::JoinHandle<()>>,
1416) {
1417 let assigned = match consumer.assigned_partitions() {
1418 Some(assigned) => Some(assigned),
1419 None => opener.assigned_partitions().await,
1420 };
1421 let Some(assigned) = assigned else {
1422 return;
1423 };
1424 let revoked: Vec<u32> = lanes
1425 .keys()
1426 .filter(|partition| !assigned.contains(partition))
1427 .copied()
1428 .collect();
1429 for partition in revoked {
1430 if let Some(lane) = lanes.remove(&partition) {
1431 debug!(
1432 partition,
1433 "dropping the lane of a partition no longer assigned"
1434 );
1435 drop(lane.sender);
1436 retiring.insert(partition, lane.task);
1437 }
1438 }
1439 retiring.retain(|_, task| !task.is_finished());
1440}
1441
1442impl Opener {
1443 async fn assigned_partitions(&self) -> Option<std::collections::BTreeSet<u32>> {
1446 let client = self.laser.client();
1447 let me = match client.get_me().await {
1448 Ok(me) => me.client_id,
1449 Err(error) => {
1450 debug!(%error, "the assignment probe could not identify this client");
1451 return None;
1452 }
1453 };
1454 let group = client
1455 .get_consumer_group(
1456 &Identifier::named(&self.stream).ok()?,
1457 &Identifier::named(&self.topic).ok()?,
1458 &Identifier::named(&self.group).ok()?,
1459 )
1460 .await;
1461 match group {
1462 Ok(Some(details)) => details
1463 .members
1464 .iter()
1465 .find(|member| member.id == me)
1466 .map(|member| member.partitions.iter().copied().collect()),
1467 Ok(None) => None,
1468 Err(error) => {
1469 debug!(%error, "the assignment probe could not read the group");
1470 None
1471 }
1472 }
1473 }
1474}
1475
1476struct QueuedMessage {
1477 message: ConsumerMessage,
1478 record_permit: tokio::sync::OwnedSemaphorePermit,
1479 byte_permit: tokio::sync::OwnedSemaphorePermit,
1480}
1481
1482fn dedup_key(provenance: &Provenance) -> Option<String> {
1488 let key = provenance.idempotency_key.as_ref()?;
1489 Some(match &provenance.agent {
1490 Some(agent) => format!("{}{DEDUP_SCOPE_SEP}{key}", agent.as_str()),
1491 None => key.clone(),
1492 })
1493}
1494
1495#[derive(Clone, Copy)]
1498struct FenceEntry {
1499 fence: u64,
1500 touched_micros: u64,
1501}
1502
1503fn accept_fence(
1510 high_water: &dashmap::DashMap<ConversationId, FenceEntry>,
1511 last_sweep_micros: &std::sync::atomic::AtomicU64,
1512 task: ConversationId,
1513 fence: u64,
1514 now_micros: u64,
1515) -> bool {
1516 if high_water.len() > FENCE_MAP_SOFT_CAP {
1517 use std::sync::atomic::Ordering;
1518 let previous = last_sweep_micros.load(Ordering::Relaxed);
1519 if now_micros.saturating_sub(previous) > FENCE_SWEEP_INTERVAL_MICROS
1520 && last_sweep_micros
1521 .compare_exchange(previous, now_micros, Ordering::Relaxed, Ordering::Relaxed)
1522 .is_ok()
1523 {
1524 high_water.retain(|_, entry| {
1525 now_micros.saturating_sub(entry.touched_micros) < FENCE_ENTRY_TTL_MICROS
1526 });
1527 }
1528 }
1529 let mut entry = high_water.entry(task).or_insert(FenceEntry {
1530 fence: 0,
1531 touched_micros: now_micros,
1532 });
1533 if fence < entry.fence {
1534 return false;
1535 }
1536 entry.fence = fence;
1537 entry.touched_micros = now_micros;
1538 true
1539}
1540
1541const ANONYMOUS_AGENT: &str = "anonymous";
1543
1544struct ReliableWorker<H> {
1545 handler: H,
1546 laser: Laser,
1547 retry: RetryPolicy,
1548 understood_features: u64,
1549 dedup: Box<dyn Deduplicator>,
1550 agent: Option<AgentId>,
1551 respond_on: Option<AgentTopic<'static>>,
1552 inbox_route: crate::agent::router::InboxRoute,
1553 ack_on_pickup: bool,
1554 stream_id: u32,
1555 topic_id: u32,
1556 middleware: Vec<std::sync::Arc<dyn AgentMiddleware>>,
1557 on_dead_letter: Option<std::sync::Arc<dyn DeadLetterSink>>,
1558 topic: String,
1559 operations: Option<Vec<String>>,
1560 sessions: Sessions,
1563 control: Arc<ControlBook>,
1565 pause: Arc<std::sync::OnceLock<Arc<PauseRuntime>>>,
1568 budget: BudgetGate,
1570 #[cfg(feature = "sign")]
1571 verifier: Option<std::sync::Arc<crate::sign::KeyRegistry>>,
1572 #[cfg(feature = "sign")]
1573 signing_key: Option<std::sync::Arc<crate::sign::SigningKey>>,
1574 high_water_fence: dashmap::DashMap<ConversationId, FenceEntry>,
1580 fence_last_sweep: std::sync::atomic::AtomicU64,
1583 #[cfg(feature = "sign")]
1589 verified_records: Mutex<DedupWindow>,
1590}
1591
1592impl<H> ReliableWorker<H> {
1593 fn observe_assignment(&self, consumer: &Consumer) {
1596 if let Some(pause) = self.pause.get() {
1597 pause.observe_assignment(consumer.assigned_partitions());
1598 }
1599 }
1600
1601 fn invalidate_pause(&self) {
1604 if let Some(pause) = self.pause.get() {
1605 pause.invalidate();
1606 }
1607 }
1608
1609 fn dispatch(&self, message: &AgentMessage) -> Dispatch {
1610 let me = match (&self.agent, &message.provenance.target_agent_id) {
1613 (Some(agent), _) | (None, Some(agent)) => agent.wire_id(),
1614 (None, None) => ANONYMOUS_AGENT.parse().expect("a valid agent id"),
1615 };
1616 match &message.envelope {
1617 Some(envelope) => {
1618 let operations: Vec<&str> = self
1619 .operations
1620 .iter()
1621 .flatten()
1622 .map(String::as_str)
1623 .collect();
1624 let handled = match self.operations {
1625 Some(_) => HandledOperations::Only(&operations),
1626 None => HandledOperations::Any,
1627 };
1628 classify(envelope, &self.topic, &me, handled)
1629 }
1630 None => classify_generic(
1631 message
1632 .provenance
1633 .target_agent_id
1634 .as_ref()
1635 .map(AgentId::as_str),
1636 message.provenance.causal_parent.is_some(),
1637 message.provenance.correlation_id.is_some(),
1638 &self.topic,
1639 &me,
1640 ),
1641 }
1642 }
1643
1644 async fn pick_up_submitted(
1645 &self,
1646 message: &AgentMessage,
1647 ) -> Option<crate::agent::SessionLease> {
1648 let envelope = message.envelope.as_ref()?;
1649 let submitted = envelope
1650 .metadata
1651 .as_ref()
1652 .and_then(|metadata| metadata.get(laser_wire::agent::METADATA_SUBMITTED))
1653 .is_some_and(|value| matches!(value, laser_wire::query::Value::Bool(true)));
1654 let agent = self.agent.as_ref()?;
1655 if !submitted || envelope.kind != AgentKind::Command {
1656 return None;
1657 }
1658 let session = self
1659 .sessions
1660 .open(message.provenance.conversation_id)
1661 .as_agent(agent.wire_id());
1662 match session.pick_up().await {
1663 Ok(lease) => Some(lease),
1664 Err(error) => {
1665 warn!(%error, source = %message.id, "failed to mark the submitted session working");
1666 None
1667 }
1668 }
1669 }
1670
1671 fn log_position(&self, id: MessageId) -> LogPosition {
1672 LogPosition::new(self.stream_id, self.topic_id, id.partition_id, id.offset)
1673 }
1674
1675 async fn dead_letter(
1679 &self,
1680 message: &AgentMessage,
1681 reason: DeadLetterReason,
1682 attempts: u32,
1683 detail: &str,
1684 ) -> Result<(), LaserError> {
1685 let capsule = AgentDeadLetter {
1686 source: self.log_position(message.id),
1687 reason,
1688 attempts,
1689 detail: Some(detail.to_owned()),
1690 payload: message.payload.clone(),
1691 };
1692 let mut provenance = message.provenance.clone();
1696 provenance.causal_parent = Some(message.id);
1697 provenance.deadline = None;
1698 let session = Some(message.provenance.conversation_id);
1699 self.publish_dead_letter(provenance, message.id, capsule, Some(message), session)
1700 .await
1701 }
1702
1703 async fn dead_letter_undecodable(
1709 &self,
1710 source: MessageId,
1711 conversation: Option<ConversationId>,
1712 payload: Vec<u8>,
1713 ) -> Result<(), LaserError> {
1714 let capsule = AgentDeadLetter {
1715 source: self.log_position(source),
1716 reason: DeadLetterReason::DecodeFailed,
1717 attempts: 0,
1718 detail: None,
1719 payload,
1720 };
1721 let position = capsule.source;
1722 let provenance = Provenance::builder()
1723 .conversation_id(conversation.unwrap_or_else(|| {
1724 ConversationId::derive(&format!(
1725 "dead-letter\u{1f}{}\u{1f}{}\u{1f}{}\u{1f}{}",
1726 position.stream_id, position.topic_id, position.partition_id, position.offset
1727 ))
1728 }))
1729 .causal_parent(source)
1730 .build();
1731 self.publish_dead_letter(provenance, source, capsule, None, conversation)
1732 .await
1733 }
1734
1735 async fn publish_dead_letter(
1736 &self,
1737 provenance: Provenance,
1738 source: MessageId,
1739 capsule: AgentDeadLetter,
1740 message: Option<&AgentMessage>,
1741 session: Option<ConversationId>,
1742 ) -> Result<(), LaserError> {
1743 let reason = capsule.reason;
1744 let result = self.send_dead_letter(&provenance, &capsule).await;
1745 if let Err(error) = &result {
1746 error!(%error, source = %source, ?reason, "failed to publish the dead-letter capsule, leaving the source offset uncommitted");
1747 }
1748 if let Some(sink) = &self.on_dead_letter {
1749 sink.on_dead_letter(message, &capsule, &result).await;
1750 }
1751 if result.is_ok()
1752 && let Some(session) = session
1753 {
1754 self.fail_session_on_dead_letter(session, &capsule).await;
1755 }
1756 result
1757 }
1758
1759 async fn fail_session_on_dead_letter(
1763 &self,
1764 session: ConversationId,
1765 capsule: &AgentDeadLetter,
1766 ) {
1767 if !self.sessions.config().fails_on_dead_letter() {
1768 return;
1769 }
1770 let Some(agent) = &self.agent else {
1771 warn!(%session, "a consumer without an agent id cannot fail the session of a dead letter");
1772 return;
1773 };
1774 let source = capsule.source;
1775 let body = AgentErrorBody {
1776 code: AgentErrorCode::Internal,
1777 message: Some(match capsule.detail.as_deref() {
1778 Some(detail) => format!(
1779 "a record of this session was dead-lettered ({:?}): {detail}",
1780 capsule.reason
1781 ),
1782 None => format!(
1783 "a record of this session was dead-lettered ({:?})",
1784 capsule.reason
1785 ),
1786 }),
1787 retryable: false,
1788 detail: Some(BTreeMap::from([
1789 (
1790 "dead_letter_reason".to_owned(),
1791 laser_wire::query::Value::Uint(u64::from(capsule.reason.code())),
1792 ),
1793 (
1794 "source".to_owned(),
1795 laser_wire::query::Value::Str(format!(
1796 "{}/{}/{}/{}",
1797 source.stream_id, source.topic_id, source.partition_id, source.offset
1798 )),
1799 ),
1800 ])),
1801 };
1802 if let Err(error) = self
1803 .sessions
1804 .open(session)
1805 .as_agent(agent.wire_id())
1806 .fail(body)
1807 .await
1808 {
1809 warn!(%error, %session, "failed to fail the session of a dead-lettered record");
1810 }
1811 }
1812
1813 async fn send_dead_letter(
1817 &self,
1818 provenance: &Provenance,
1819 capsule: &AgentDeadLetter,
1820 ) -> Result<(), LaserError> {
1821 let payload = encode_named(capsule)
1822 .map_err(|error| LaserError::Codec(format!("dead-letter capsule: {error}")))?;
1823 let mut headers = BTreeMap::<HeaderKey, HeaderValue>::try_from(provenance)
1824 .map_err(|error| LaserError::Codec(format!("dead-letter headers: {error}")))?;
1825 let key = HeaderKey::from_str(CONTENT_TYPE)?;
1827 headers.insert(key, HeaderValue::from(ContentType::Cbor.code()));
1828 let topic = AgentTopic::Dlq.topic_string();
1829 let partition_key = provenance.partition_key();
1830 self.laser
1831 .send_with_headers(&topic, payload, headers, Some(&partition_key))
1832 .await
1833 .map(|_| ())
1834 }
1835}
1836
1837impl<H> ReliableWorker<H>
1838where
1839 H: AgentHandler + Sync,
1840{
1841 #[tracing::instrument(target = "laser", level = "debug", skip_all, fields(conversation = tracing::field::Empty, operation = "handle"))]
1845 async fn consume(&self, received: &ConsumerMessage) -> Result<(), LaserError> {
1846 self.deliver(received, true).await
1847 }
1848
1849 async fn deliver(&self, received: &ConsumerMessage, live: bool) -> Result<(), LaserError> {
1852 let source = received.position;
1853 let decoded = match AgentMessage::from_consumer(received, self.understood_features) {
1854 Ok(decoded) => decoded,
1855 Err((error, payload)) => {
1856 warn!(%error, source = %source, "undecodable provenance, dead-lettering raw payload");
1857 let conversation = (!received.headers_malformed)
1858 .then(|| original_conversation(&received.headers))
1859 .flatten();
1860 self.dead_letter_undecodable(source, conversation, payload)
1861 .await?;
1862 return Ok(());
1863 }
1864 };
1865 #[cfg(feature = "sign")]
1866 let DecodedAgentMessage {
1867 mut message,
1868 signature_context,
1869 observed_at_micros,
1870 } = decoded;
1871 #[cfg(not(feature = "sign"))]
1872 let DecodedAgentMessage { message, .. } = decoded;
1873 tracing::Span::current().record(
1874 "conversation",
1875 tracing::field::display(&message.provenance.conversation_id),
1876 );
1877
1878 if let (Some(target), Some(agent)) = (&message.provenance.target_agent_id, &self.agent)
1894 && target != agent
1895 {
1896 debug!(target = %target, agent = %agent, source = %message.id, "skipping message targeted at another agent");
1897 return Ok(());
1898 }
1899
1900 let dispatch = self.dispatch(&message);
1905 if dispatch != Dispatch::Work {
1906 debug!(source = %message.id, dispatch = dispatch.as_str(), "skipping a record that is not work for this agent");
1907 return Ok(());
1908 }
1909
1910 #[cfg(feature = "sign")]
1915 if let Some(registry) = &self.verifier {
1916 let verified = message.envelope.as_ref().and_then(|envelope| {
1917 signature_context.as_ref().and_then(|context| {
1918 registry
1919 .verify_observed_at(envelope, context, observed_at_micros)
1920 .ok()
1921 })
1922 });
1923 let Some(verified) = verified else {
1924 warn!(source = %message.id, "unsigned or unverified message on a verified topic, dead-lettering");
1925 self.dead_letter(
1926 &message,
1927 DeadLetterReason::Rejected,
1928 0,
1929 "signature verification failed",
1930 )
1931 .await?;
1932 return Ok(());
1933 };
1934 if let Some(record) = message
1939 .envelope
1940 .as_ref()
1941 .and_then(|envelope| envelope.record)
1942 {
1943 let first_delivery = self
1944 .verified_records
1945 .lock()
1946 .unwrap_or_else(|poisoned| poisoned.into_inner())
1947 .insert(&record.to_string());
1948 if !first_delivery {
1949 warn!(source = %message.id, %record, "replayed signed record, dropping");
1950 return Ok(());
1951 }
1952 }
1953 message.verified_principal = Some(verified.principal);
1954 }
1955
1956 let _gate = match (self.pause.get(), live) {
1962 (Some(pause), true) => {
1963 let gate = pause.gate(message.provenance.conversation_id).await;
1964 if pause.hold(self, &message).await? == Hold::Commit {
1965 return Ok(());
1966 }
1967 Some(gate)
1968 }
1969 _ => None,
1970 };
1971
1972 if let Some(agent) = &self.agent {
1976 let session = message.provenance.conversation_id;
1977 let admitted = self
1978 .budget
1979 .admit(
1980 &self.sessions,
1981 session,
1982 received.partition_id,
1983 received.current_offset,
1984 || {
1985 let mut lens = self.sessions.open(session).as_agent(agent.wire_id());
1986 lens.control = Some(Arc::clone(&self.control));
1987 lens
1988 },
1989 )
1990 .await;
1991 if !admitted {
1992 debug!(source = %message.id, "skipping work for a session over its budget");
1993 return Ok(());
1994 }
1995 }
1996
1997 if let Some(fence) = message.provenance.fence_token
2005 && !accept_fence(
2006 &self.high_water_fence,
2007 &self.fence_last_sweep,
2008 message.provenance.conversation_id,
2009 fence,
2010 SystemClock.now_micros(),
2011 )
2012 {
2013 debug!(source = %message.id, fence, "stale-holder fence, dropping replay");
2014 return Ok(());
2015 }
2016
2017 if let Some(key) = dedup_key(&message.provenance) {
2018 if !self.dedup.observe(&key).await {
2024 debug!(dedup_key = %key, source = %message.id, "skipping duplicate message");
2025 return Ok(());
2026 }
2027 }
2028
2029 if let Some(deadline) = message.provenance.deadline
2030 && IggyTimestamp::now().as_micros() > deadline.as_micros()
2031 {
2032 warn!(source = %message.id, "message past its deadline, dead-lettering");
2033 self.dead_letter(
2034 &message,
2035 DeadLetterReason::DeadlineExceeded,
2036 0,
2037 "message past its deadline",
2038 )
2039 .await?;
2040 return Ok(());
2041 }
2042
2043 if self.ack_on_pickup
2048 && let (Some(agent), Some(respond_on), Some(envelope)) =
2049 (&self.agent, &self.respond_on, &message.envelope)
2050 && envelope.kind == AgentKind::Command
2051 && let Some(correlation) = envelope.correlation
2052 {
2053 let producer =
2054 self.laser
2055 .agdx(respond_on.clone(), agent.wire_id(), envelope.conversation);
2056 let ack = producer
2057 .status(OPERATION_TASK)
2058 .with_correlation(correlation)
2059 .with_task_state(TaskState::Working);
2060 #[cfg(feature = "sign")]
2061 let ack = match &self.signing_key {
2062 Some(signing_key) => ack.signed_by(signing_key),
2063 None => ack,
2064 };
2065 if let Err(error) = ack.send().await {
2066 warn!(source = %message.id, %error, "failed to emit ack-on-pickup status");
2067 }
2068 }
2069
2070 let _pickup = self.pick_up_submitted(&message).await;
2074 let ctx = AgentCtx::new(
2075 &self.laser,
2076 &message,
2077 self.agent.clone(),
2078 self.respond_on.clone(),
2079 self.inbox_route.clone(),
2080 #[cfg(feature = "sign")]
2081 self.signing_key.clone(),
2082 )
2083 .with_request_at(self.log_position(message.id))
2084 .with_sessions(self.sessions.clone(), Arc::clone(&self.control));
2085 for middleware in &self.middleware {
2089 if let Err(error) = middleware.before_handle(&message).await {
2090 warn!(%error, source = %message.id, "middleware rejected message before handling, dead-lettering");
2091 self.dead_letter(&message, DeadLetterReason::Rejected, 0, &error.to_string())
2092 .await?;
2093 return Ok(());
2094 }
2095 }
2096 let mut attempt = 0;
2097 loop {
2098 let result = std::panic::AssertUnwindSafe(self.handler.handle(&message, &ctx))
2101 .catch_unwind()
2102 .await
2103 .unwrap_or_else(|panic| {
2104 let reason = panic
2105 .downcast_ref::<&str>()
2106 .map(|message| (*message).to_owned())
2107 .or_else(|| panic.downcast_ref::<String>().cloned())
2108 .unwrap_or_else(|| "unknown panic".to_owned());
2109 Err(LaserError::HandlerConfig(format!(
2110 "handler panicked: {reason}"
2111 )))
2112 });
2113 for middleware in &self.middleware {
2116 middleware
2117 .after_handle(&message, &result, attempt + 1)
2118 .await;
2119 }
2120 match result {
2121 Ok(()) => {
2122 debug!(source = %message.id, "message handled");
2123 return Ok(());
2124 }
2125 Err(error) => {
2126 if !error.is_retryable() {
2127 warn!(%error, source = %message.id, "handler rejected message, dead-lettering without retry");
2128 self.dead_letter(
2129 &message,
2130 DeadLetterReason::Rejected,
2131 attempt + 1,
2132 &error.to_string(),
2133 )
2134 .await?;
2135 return Ok(());
2136 }
2137 if attempt + 1 >= self.retry.max_attempts {
2138 error!(%error, source = %message.id, attempts = attempt + 1, "handler exhausted retries, dead-lettering");
2139 self.dead_letter(
2140 &message,
2141 DeadLetterReason::RetryExhausted,
2142 attempt + 1,
2143 &error.to_string(),
2144 )
2145 .await?;
2146 return Ok(());
2147 }
2148 warn!(%error, source = %message.id, attempt = attempt + 1, "handler failed, retrying");
2149 sleep(self.retry.delay_for(attempt)).await;
2150 attempt += 1;
2151 }
2152 }
2153 }
2154 }
2155}
2156
2157impl<H> Replay for ReliableWorker<H>
2158where
2159 H: AgentHandler + Sync,
2160{
2161 fn replay<'a>(
2162 &'a self,
2163 received: &'a ConsumerMessage,
2164 ) -> futures::future::BoxFuture<'a, Result<(), LaserError>> {
2165 Box::pin(self.deliver(received, false))
2166 }
2167}
2168
2169async fn warm_dedup_window(
2175 laser: &Laser,
2176 settings: &ReliableConsumer,
2177 dedup: &dyn Deduplicator,
2178) -> Result<(), LaserError> {
2179 let stream = Identifier::named(laser.stream_required()?)?;
2180 let topic_id = Identifier::named(&settings.topic)?;
2181 let Some(details) = laser.client().get_topic(&stream, &topic_id).await? else {
2182 return Ok(());
2183 };
2184 let group_consumer =
2185 iggy::prelude::Consumer::group(Identifier::named(settings.group.as_str())?);
2186 let reader = iggy::prelude::Consumer::new(Identifier::named("laser-dedup-warmer")?);
2187 let depth = u64::try_from(settings.dedup_window).unwrap_or(u64::MAX);
2188 for partition in 0..crate::poll::bounded_partitions(details.partitions_count) {
2189 let Some(offset) = laser
2190 .client()
2191 .get_consumer_offset(&group_consumer, &stream, &topic_id, Some(partition))
2192 .await?
2193 else {
2194 continue;
2195 };
2196 let stored = offset.stored_offset;
2197 let start = stored.saturating_sub(depth.saturating_sub(1));
2198 let count =
2199 u32::try_from(stored.saturating_sub(start).saturating_add(1)).unwrap_or(u32::MAX);
2200 let polled = laser
2201 .client()
2202 .poll_messages(
2203 &stream,
2204 &topic_id,
2205 Some(partition),
2206 &reader,
2207 &PollingStrategy::offset(start),
2208 count,
2209 false,
2210 )
2211 .await?;
2212 for message in polled.messages {
2213 if message.header.offset > stored {
2214 continue;
2215 }
2216 if let Some(key) = warm_dedup_key(
2217 &message,
2218 settings.agent.as_ref(),
2219 settings.understood_features,
2220 #[cfg(feature = "sign")]
2221 settings.verifier.as_deref(),
2222 ) {
2223 dedup.observe(&key).await;
2224 }
2225 }
2226 }
2227 Ok(())
2228}
2229
2230fn warm_dedup_key(
2231 message: &IggyMessage,
2232 agent: Option<&AgentId>,
2233 understood_features: u64,
2234 #[cfg(feature = "sign")] verifier: Option<&crate::sign::KeyRegistry>,
2235) -> Option<String> {
2236 let decoded = decode_agent_record(message, understood_features).ok()?;
2237 if let (Some(target), Some(agent)) = (&decoded.provenance.target_agent_id, agent)
2238 && target != agent
2239 {
2240 return None;
2241 }
2242 #[cfg(feature = "sign")]
2243 if let Some(registry) = verifier {
2244 registry
2245 .verify_observed_at(
2246 decoded.envelope.as_ref()?,
2247 decoded.signature_context.as_ref()?,
2248 decoded.observed_at_micros,
2249 )
2250 .ok()?;
2251 }
2252 dedup_key(&decoded.provenance)
2253}
2254
2255#[async_trait]
2260pub trait Deduplicator: Send + Sync {
2261 async fn observe(&self, key: &str) -> bool;
2263}
2264
2265pub struct SlidingWindow {
2267 inner: Mutex<DedupWindow>,
2268}
2269
2270impl SlidingWindow {
2271 pub fn new(capacity: usize) -> Self {
2273 Self {
2274 inner: Mutex::new(DedupWindow::new(capacity)),
2275 }
2276 }
2277}
2278
2279#[async_trait]
2280impl Deduplicator for SlidingWindow {
2281 async fn observe(&self, key: &str) -> bool {
2282 self.inner
2283 .lock()
2284 .expect("the dedup mutex is not poisoned")
2285 .insert(key)
2286 }
2287}
2288
2289struct DedupWindow {
2290 capacity: usize,
2291 seen: HashSet<String>,
2292 order: VecDeque<String>,
2293}
2294
2295impl DedupWindow {
2296 fn new(capacity: usize) -> Self {
2297 Self {
2298 capacity: capacity.max(1),
2299 seen: HashSet::new(),
2300 order: VecDeque::new(),
2301 }
2302 }
2303
2304 fn insert(&mut self, key: &str) -> bool {
2305 if self.seen.contains(key) {
2306 return false;
2307 }
2308 if self.order.len() >= self.capacity
2309 && let Some(evicted) = self.order.pop_front()
2310 {
2311 self.seen.remove(&evicted);
2312 }
2313 self.seen.insert(key.to_owned());
2314 self.order.push_back(key.to_owned());
2315 true
2316 }
2317}
2318
2319#[cfg(test)]
2320mod tests {
2321 use super::*;
2322
2323 #[test]
2324 fn given_consumer_failures_when_backing_off_then_should_jitter_within_the_upper_half() {
2325 for attempt in [1, 3, 10] {
2326 let ceiling = crate::retry::CONSUMER.ceiling(attempt);
2327 for _ in 0..64 {
2328 let delay = backoff_for(attempt);
2329 assert!(
2330 delay <= ceiling && delay >= ceiling / 2,
2331 "attempt {attempt}: {delay:?}"
2332 );
2333 }
2334 }
2335 assert_eq!(crate::retry::CONSUMER.ceiling(1), Duration::from_millis(50));
2336 assert_eq!(crate::retry::CONSUMER.ceiling(40), Duration::from_secs(1));
2337 }
2338
2339 #[test]
2340 fn given_an_agdx_command_when_warming_dedup_then_should_restore_the_scoped_key() {
2341 let envelope = AgentEnvelope::command(
2342 laser_wire::agent::RecordId::from_u128(3),
2343 laser_wire::agent::ConversationId::from_u128(1),
2344 "author".parse().expect("source parses"),
2345 laser_wire::agent::CorrelationId::from_u128(2),
2346 b"work".to_vec(),
2347 )
2348 .with_operation(laser_wire::agent::OPERATION_CHAT)
2349 .with_target("worker".parse().expect("target parses"))
2350 .with_idempotency_key("attempt-1".parse().expect("key parses"));
2351 let mut headers = BTreeMap::new();
2352 headers.insert(
2353 HeaderKey::from_str(AGENT_VERSION).expect("version key"),
2354 HeaderValue::from(AGENT_OP_VERSION),
2355 );
2356 let message = IggyMessage::builder()
2357 .payload(bytes::Bytes::from(
2358 encode_named(&envelope).expect("envelope encodes"),
2359 ))
2360 .user_headers(headers)
2361 .build()
2362 .expect("message builds");
2363 let worker: AgentId = "worker".parse().expect("worker parses");
2364 let other: AgentId = "other".parse().expect("other parses");
2365
2366 assert_eq!(
2367 warm_dedup_key(
2368 &message,
2369 Some(&worker),
2370 features::NONE,
2371 #[cfg(feature = "sign")]
2372 None,
2373 ),
2374 Some(format!("author{DEDUP_SCOPE_SEP}attempt-1"))
2375 );
2376 assert!(
2377 warm_dedup_key(
2378 &message,
2379 Some(&other),
2380 features::NONE,
2381 #[cfg(feature = "sign")]
2382 None,
2383 )
2384 .is_none()
2385 );
2386 }
2387
2388 #[test]
2389 fn given_a_small_positive_fence_when_decoded_then_should_preserve_the_token() {
2390 let envelope = AgentEnvelope::command(
2391 laser_wire::agent::RecordId::from_u128(3),
2392 laser_wire::agent::ConversationId::from_u128(1),
2393 "orchestrator".parse().expect("agent id parses"),
2394 laser_wire::agent::CorrelationId::from_u128(2),
2395 Vec::new(),
2396 )
2397 .with_metadata(FENCE, laser_wire::query::Value::Int(7));
2398
2399 assert_eq!(provenance_from_envelope(&envelope).fence_token, Some(7));
2400 }
2401
2402 #[test]
2403 fn given_an_envelope_with_usage_when_decoded_then_should_keep_token_counts() {
2404 let envelope = AgentEnvelope::command(
2405 laser_wire::agent::RecordId::from_u128(3),
2406 laser_wire::agent::ConversationId::from_u128(1),
2407 "author".parse().expect("agent id parses"),
2408 laser_wire::agent::CorrelationId::from_u128(2),
2409 b"work".to_vec(),
2410 )
2411 .with_operation(laser_wire::agent::OPERATION_CHAT)
2412 .with_usage(laser_wire::agent::TokenUsage {
2413 input_tokens: 13,
2414 output_tokens: 21,
2415 ..Default::default()
2416 });
2417
2418 let usage = provenance_from_envelope(&envelope)
2419 .usage
2420 .expect("usage should be preserved");
2421 assert_eq!(usage.input_tokens, Some(13));
2422 assert_eq!(usage.output_tokens, Some(21));
2423 }
2424
2425 #[test]
2426 fn given_a_seen_key_when_inserting_again_then_should_report_a_duplicate() {
2427 let mut window = DedupWindow::new(8);
2428 assert!(window.insert("a"));
2429 assert!(!window.insert("a"));
2430 assert!(window.insert("b"));
2431 }
2432
2433 #[test]
2434 fn given_a_full_window_when_inserting_then_should_evict_the_oldest_key() {
2435 let mut window = DedupWindow::new(2);
2436 assert!(window.insert("a"));
2437 assert!(window.insert("b"));
2438 assert!(window.insert("c"));
2439 assert!(window.insert("a"));
2440 }
2441
2442 #[test]
2443 fn given_increasing_attempts_when_computing_backoff_then_should_grow_and_stay_bounded() {
2444 let policy = RetryPolicy::backoff(5, Duration::from_millis(100));
2445 assert_eq!(policy.delay_for(0), Duration::from_millis(100));
2446 assert_eq!(policy.delay_for(1), Duration::from_millis(200));
2447 assert_eq!(policy.delay_for(2), Duration::from_millis(400));
2448 assert!(policy.delay_for(60) >= policy.delay_for(2));
2449 }
2450
2451 #[test]
2452 fn given_two_agents_with_the_same_idempotency_key_when_scoped_then_should_differ() {
2453 let conversation = ConversationId::new();
2454 let with_agent = |agent: &str| {
2455 Provenance::builder()
2456 .conversation_id(conversation)
2457 .agent(agent.parse().expect("valid agent id"))
2458 .idempotency_key("attempt-1".to_owned())
2459 .build()
2460 };
2461 assert_ne!(
2464 dedup_key(&with_agent("planner")),
2465 dedup_key(&with_agent("worker"))
2466 );
2467 let anon = Provenance::builder()
2469 .conversation_id(conversation)
2470 .idempotency_key("attempt-1".to_owned())
2471 .build();
2472 assert_eq!(dedup_key(&anon).as_deref(), Some("attempt-1"));
2473 }
2474
2475 #[test]
2476 fn given_a_fence_gate_when_a_stale_token_arrives_then_should_drop_it_and_keep_per_task_scope() {
2477 let high_water = dashmap::DashMap::new();
2478 let sweep = std::sync::atomic::AtomicU64::new(0);
2479 let task_a = ConversationId::new();
2480 let task_b = ConversationId::new();
2481
2482 assert!(accept_fence(&high_water, &sweep, task_a, 1, 100));
2484 assert!(accept_fence(&high_water, &sweep, task_a, 2, 101));
2486 assert!(!accept_fence(&high_water, &sweep, task_a, 1, 102));
2488 assert!(accept_fence(&high_water, &sweep, task_a, 2, 103));
2491 assert!(accept_fence(&high_water, &sweep, task_b, 1, 104));
2493 }
2494}