use std::sync::Mutex;
use tokio::sync::mpsc;
use crate::types::{AgentResult, Notice, NoticeKind, RuntimeEvent, SessionId, UserEvent};
use super::plan_runner::RuntimeCore;
#[derive(Clone)]
pub struct NoticeHandle {
tx: mpsc::UnboundedSender<Notice>,
}
impl NoticeHandle {
pub(crate) fn new(tx: mpsc::UnboundedSender<Notice>) -> Self {
Self { tx }
}
pub fn new_channel() -> (Self, mpsc::UnboundedReceiver<Notice>) {
let (tx, rx) = mpsc::unbounded_channel();
(Self { tx }, rx)
}
pub fn send(&self, kind: NoticeKind, source: impl Into<String>, text: impl Into<String>) {
let _ = self.tx.send(Notice {
kind,
source: source.into(),
text: text.into(),
});
}
}
impl RuntimeCore {
pub(crate) fn drain_notices<F>(
&self,
session_id: &SessionId,
on_event: &Mutex<F>,
) -> AgentResult<()>
where
F: FnMut(RuntimeEvent) -> AgentResult<()>,
{
let mut rx = self.notice_rx.lock().unwrap();
while let Ok(notice) = rx.try_recv() {
let event = RuntimeEvent::UserEvent {
session_id: session_id.clone(),
event: UserEvent::Notice {
kind: notice.kind,
source: notice.source,
text: notice.text,
},
agent_id: None,
trace_id: None,
};
if let Ok(mut cb) = on_event.lock()
&& let Err(e) = cb(event.clone())
{
tracing::warn!(error = %e, "notice consumer callback failed — notice still published to the event bus");
}
self.event_bus.emit(event);
}
Ok(())
}
pub(crate) fn discard_pending_notices(&self) {
let mut rx = self.notice_rx.lock().unwrap();
while rx.try_recv().is_ok() {}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn send_after_receiver_closed_is_silent_no_panic() {
let (tx, rx) = mpsc::unbounded_channel::<Notice>();
let handle = NoticeHandle::new(tx);
drop(rx);
handle.send(NoticeKind::Warning, "guard", "nobody listening");
handle.send(NoticeKind::Info, "judge", "also dropped");
}
#[test]
fn send_stores_kind_source_text() {
let (tx, mut rx) = mpsc::unbounded_channel::<Notice>();
let handle = NoticeHandle::new(tx);
handle.send(
NoticeKind::Warning,
"guard",
"judge unparsed — treating as complete",
);
let notice = rx.try_recv().unwrap();
assert_eq!(notice.kind, NoticeKind::Warning);
assert_eq!(notice.source, "guard");
assert_eq!(notice.text, "judge unparsed — treating as complete");
}
}