fabro/lib/components/fabro-agent/src/event.rs
2026-07-23 17:59:34 -04:00

176 lines
5.1 KiB
Rust

use std::time::SystemTime;
use tokio::sync::broadcast;
use crate::tool_registry::AgentEventEmitter;
use crate::types::{AgentEvent, SessionEvent};
#[derive(Clone)]
pub struct Emitter {
sender: broadcast::Sender<SessionEvent>,
}
impl Emitter {
#[must_use]
pub fn new() -> Self {
let (sender, _) = broadcast::channel(1024);
Self { sender }
}
pub fn emit(&self, session_id: String, event: AgentEvent) {
self.emit_with_tool_call_id(session_id, event, None);
}
pub fn emit_with_tool_call_id(
&self,
session_id: String,
event: AgentEvent,
tool_call_id: Option<String>,
) {
event.trace(&session_id);
let wrapped = SessionEvent {
event,
timestamp: SystemTime::now(),
session_id,
parent_session_id: None,
tool_call_id,
};
// Ignore send error (no receivers)
let _ = self.sender.send(wrapped);
}
pub fn forward(&self, event: SessionEvent) {
let _ = self.sender.send(event);
}
#[must_use]
pub fn subscribe(&self) -> broadcast::Receiver<SessionEvent> {
self.sender.subscribe()
}
}
impl Default for Emitter {
fn default() -> Self {
Self::new()
}
}
/// Session-bound view of an [`Emitter`] suitable for handing to tools.
/// Captures the session identity so each emitted agent event keeps the
/// correct `session_id` on the wire. `parent_session_id` is stamped later
/// by [`Session::sub_agent_event_callback`](crate::session::Session::sub_agent_event_callback)
/// when a subagent's events are forwarded through its parent.
#[derive(Clone)]
pub struct SessionBoundEmitter {
pub emitter: Emitter,
pub session_id: String,
pub tool_call_id: Option<String>,
}
impl AgentEventEmitter for SessionBoundEmitter {
fn emit(&self, event: AgentEvent) {
self.emitter.emit_with_tool_call_id(
self.session_id.clone(),
event,
self.tool_call_id.clone(),
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::Error;
#[tokio::test]
async fn emit_and_receive_event() {
let emitter = Emitter::new();
let mut receiver = emitter.subscribe();
emitter.emit("sess-1".into(), AgentEvent::SessionStarted {
provider: Some("anthropic".into()),
model: Some("claude-opus".into()),
});
let event = receiver.recv().await.unwrap();
assert!(matches!(event.event, AgentEvent::SessionStarted {
provider: Some(_),
model: Some(_),
}));
assert_eq!(event.session_id, "sess-1");
assert_eq!(event.parent_session_id, None);
}
#[tokio::test]
async fn emit_with_data() {
let emitter = Emitter::new();
let mut receiver = emitter.subscribe();
emitter.emit("sess-2".into(), AgentEvent::Error {
error: Error::ToolExecution("something went wrong".into()),
});
let event = receiver.recv().await.unwrap();
assert!(
matches!(&event.event, AgentEvent::Error { error } if error.to_string().contains("something went wrong"))
);
assert_eq!(event.parent_session_id, None);
}
#[tokio::test]
async fn multiple_subscribers() {
let emitter = Emitter::new();
let mut rx1 = emitter.subscribe();
let mut rx2 = emitter.subscribe();
emitter.emit("sess-3".into(), AgentEvent::SessionEnded);
let e1 = rx1.recv().await.unwrap();
let e2 = rx2.recv().await.unwrap();
assert!(matches!(e1.event, AgentEvent::SessionEnded));
assert!(matches!(e2.event, AgentEvent::SessionEnded));
assert_eq!(e1.session_id, "sess-3");
assert_eq!(e2.session_id, "sess-3");
assert_eq!(e1.parent_session_id, None);
assert_eq!(e2.parent_session_id, None);
}
#[test]
fn emit_without_subscribers_does_not_panic() {
let emitter = Emitter::new();
emitter.emit("sess-4".into(), AgentEvent::Error {
error: Error::ToolExecution("test".into()),
});
}
#[test]
fn default_creates_emitter() {
let emitter = Emitter::default();
let _rx = emitter.subscribe();
}
#[tokio::test]
async fn forward_preserves_session_ids() {
let emitter = Emitter::new();
let mut receiver = emitter.subscribe();
emitter.forward(SessionEvent {
event: AgentEvent::SessionStarted {
provider: Some("anthropic".into()),
model: Some("claude-opus".into()),
},
timestamp: SystemTime::now(),
session_id: "child".into(),
parent_session_id: Some("parent".into()),
tool_call_id: None,
});
let event = receiver.recv().await.unwrap();
assert_eq!(event.session_id, "child");
assert_eq!(event.parent_session_id.as_deref(), Some("parent"));
assert!(matches!(event.event, AgentEvent::SessionStarted {
provider: Some(_),
model: Some(_),
}));
}
}