use async_trait::async_trait;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::id::{EventId, SessionId, TurnId};
use crate::message::{Message, ToolCall};
pub mod event_types {
pub const TURN_STARTED: &str = "turn.started";
pub const TURN_COMPLETED: &str = "turn.completed";
pub const TURN_FAILED: &str = "turn.failed";
pub const TURN_CANCELLED: &str = "turn.cancelled";
pub const TURN_SEALED: &str = "turn.sealed";
pub const INPUT_MESSAGE: &str = "input.message";
pub const OUTPUT_MESSAGE_STARTED: &str = "output.message.started";
pub const OUTPUT_MESSAGE_DELTA: &str = "output.message.delta";
pub const OUTPUT_MESSAGE_COMPLETED: &str = "output.message.completed";
pub const TOOL_STARTED: &str = "tool.started";
pub const TOOL_COMPLETED: &str = "tool.completed";
pub const TOOL_DENIED: &str = "tool.denied";
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum EventData {
TurnStarted,
TurnCompleted {
iterations: usize,
tool_calls: usize,
},
TurnFailed {
error: String,
},
TurnCancelled,
TurnSealed {
reason: crate::turn::SealReason,
},
InputMessage {
message: Message,
},
OutputMessageStarted {
#[serde(default, skip_serializing_if = "Option::is_none")]
model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
iteration: Option<u32>,
},
OutputMessageDelta {
delta: String,
accumulated: String,
},
OutputMessageCompleted {
message: Message,
},
ToolStarted {
call: ToolCall,
},
ToolCompleted {
call_id: String,
name: String,
output: String,
is_error: bool,
},
ToolDenied {
call_id: String,
name: String,
reason: String,
},
Custom {
event_type: String,
#[serde(default)]
payload: serde_json::Value,
},
}
impl EventData {
pub fn custom(event_type: impl Into<String>, payload: serde_json::Value) -> Self {
EventData::Custom {
event_type: event_type.into(),
payload,
}
}
pub fn event_type(&self) -> &str {
match self {
EventData::TurnStarted => event_types::TURN_STARTED,
EventData::TurnCompleted { .. } => event_types::TURN_COMPLETED,
EventData::TurnFailed { .. } => event_types::TURN_FAILED,
EventData::TurnCancelled => event_types::TURN_CANCELLED,
EventData::TurnSealed { .. } => event_types::TURN_SEALED,
EventData::InputMessage { .. } => event_types::INPUT_MESSAGE,
EventData::OutputMessageStarted { .. } => event_types::OUTPUT_MESSAGE_STARTED,
EventData::OutputMessageDelta { .. } => event_types::OUTPUT_MESSAGE_DELTA,
EventData::OutputMessageCompleted { .. } => event_types::OUTPUT_MESSAGE_COMPLETED,
EventData::ToolStarted { .. } => event_types::TOOL_STARTED,
EventData::ToolCompleted { .. } => event_types::TOOL_COMPLETED,
EventData::ToolDenied { .. } => event_types::TOOL_DENIED,
EventData::Custom { event_type, .. } => event_type,
}
}
pub fn is_ephemeral(&self) -> bool {
matches!(self, EventData::OutputMessageDelta { .. })
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Event {
pub id: EventId,
#[serde(rename = "type")]
pub event_type: String,
pub ts: DateTime<Utc>,
pub session_id: SessionId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub turn_id: Option<TurnId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sequence: Option<u64>,
pub data: EventData,
}
#[derive(Debug, Clone)]
pub struct EventRequest {
pub session_id: SessionId,
pub turn_id: Option<TurnId>,
pub data: EventData,
}
impl EventRequest {
pub fn new(session_id: SessionId, data: EventData) -> Self {
Self {
session_id,
turn_id: None,
data,
}
}
pub fn with_turn(session_id: SessionId, turn_id: TurnId, data: EventData) -> Self {
Self {
session_id,
turn_id: Some(turn_id),
data,
}
}
pub fn into_event(self, id: EventId, ts: DateTime<Utc>, sequence: u64) -> Event {
Event {
id,
event_type: self.data.event_type().to_string(),
ts,
session_id: self.session_id,
turn_id: self.turn_id,
sequence: Some(sequence),
data: self.data,
}
}
pub fn into_ephemeral_event(self, id: EventId, ts: DateTime<Utc>) -> Event {
Event {
id,
event_type: self.data.event_type().to_string(),
ts,
session_id: self.session_id,
turn_id: self.turn_id,
sequence: None,
data: self.data,
}
}
}
#[async_trait]
pub trait EventListener: Send + Sync {
async fn on_event(&self, event: &Event);
fn name(&self) -> &'static str {
"listener"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn event_serde_roundtrip() {
let request = EventRequest::new(
SessionId::new(),
EventData::InputMessage {
message: Message::user("hi"),
},
);
let event = request.into_event(EventId::new(), Utc::now(), 1);
assert_eq!(event.event_type, "input.message");
assert_eq!(event.sequence, Some(1));
let json = serde_json::to_string(&event).unwrap();
let back: Event = serde_json::from_str(&json).unwrap();
assert_eq!(event, back);
}
#[test]
fn ephemeral_event_has_no_sequence() {
let request = EventRequest::new(
SessionId::new(),
EventData::OutputMessageDelta {
delta: "hi".into(),
accumulated: "hi".into(),
},
);
assert!(request.data.is_ephemeral());
let event = request.into_ephemeral_event(EventId::new(), Utc::now());
assert_eq!(event.sequence, None);
}
#[test]
fn only_delta_is_ephemeral() {
assert!(!EventData::TurnStarted.is_ephemeral());
assert!(
!EventData::OutputMessageStarted {
model: None,
iteration: None
}
.is_ephemeral()
);
assert!(
!EventData::OutputMessageCompleted {
message: Message::assistant("hi"),
}
.is_ephemeral()
);
}
#[test]
fn custom_event_carries_its_own_type_and_is_durable() {
let data = EventData::custom("budget.warning", serde_json::json!({"remaining": 10}));
assert_eq!(data.event_type(), "budget.warning");
assert!(!data.is_ephemeral());
let event =
EventRequest::new(SessionId::new(), data).into_event(EventId::new(), Utc::now(), 1);
let json = serde_json::to_string(&event).unwrap();
let back: Event = serde_json::from_str(&json).unwrap();
assert_eq!(event, back);
assert_eq!(back.event_type, "budget.warning");
}
}