use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::Arc;
use std::task::Poll;
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::id::{EventId, MessageId, SessionId, TurnId};
use crate::message::{Message, ToolCall};
pub mod event_types {
pub const SESSION_FORKED: &str = "session.forked";
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";
pub const TOOL_REWRITTEN: &str = "tool.rewritten";
pub const TOOL_PROGRESS: &str = "tool.progress";
pub const HOOK_WARNING: &str = "hook.warning";
pub const HOOK_USER_MESSAGE: &str = "hook.user_message";
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
#[non_exhaustive]
pub enum EventData {
SessionForked {
parent_session_id: SessionId,
parent_sequence: u64,
},
TurnStarted {
#[serde(default = "default_max_iterations")]
max_iterations: usize,
},
TurnCompleted {
iterations: usize,
tool_calls: usize,
},
TurnFailed {
error: String,
},
TurnCancelled,
TurnSealed {
reason: crate::turn::SealReason,
},
InputMessage {
message: Message,
},
OutputMessageStarted {
#[serde(default)]
message_id: MessageId,
#[serde(default, skip_serializing_if = "Option::is_none")]
model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
iteration: Option<u32>,
},
OutputMessageDelta {
#[serde(default)]
message_id: MessageId,
delta: String,
accumulated: String,
},
OutputMessageCompleted {
#[serde(default)]
message_id: MessageId,
message: Message,
#[serde(default)]
usage: crate::driver::Usage,
},
ToolStarted {
call: ToolCall,
},
ToolCompleted {
call_id: String,
name: String,
output: String,
is_error: bool,
#[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
metadata: serde_json::Value,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
parts: Vec<crate::message::ContentPart>,
},
ToolProgress {
call_id: String,
name: String,
progress: crate::tool::ToolProgress,
},
ToolDenied {
call_id: String,
name: String,
reason: String,
},
ToolRewritten {
call_id: String,
name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
call: Option<ToolCall>,
#[serde(default, skip_serializing_if = "Option::is_none")]
by: Option<String>,
},
Custom {
event_type: String,
#[serde(default)]
payload: serde_json::Value,
#[serde(default)]
state_bearing: bool,
},
}
fn default_max_iterations() -> usize {
16
}
impl EventData {
pub fn custom(event_type: impl Into<String>, payload: serde_json::Value) -> Self {
EventData::Custom {
event_type: event_type.into(),
payload,
state_bearing: false,
}
}
pub fn event_type(&self) -> &str {
match self {
EventData::SessionForked { .. } => event_types::SESSION_FORKED,
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::ToolRewritten { .. } => event_types::TOOL_REWRITTEN,
EventData::ToolProgress { .. } => event_types::TOOL_PROGRESS,
EventData::Custom { event_type, .. } => event_type,
}
}
pub fn is_ephemeral(&self) -> bool {
matches!(
self,
EventData::OutputMessageDelta { .. } | EventData::ToolProgress { .. }
)
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[non_exhaustive]
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>,
#[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
pub metadata: serde_json::Value,
pub data: EventData,
}
#[derive(Deserialize)]
struct EventRepr {
id: EventId,
#[serde(rename = "type")]
event_type: String,
ts: DateTime<Utc>,
session_id: SessionId,
#[serde(default)]
turn_id: Option<TurnId>,
#[serde(default)]
sequence: Option<u64>,
#[serde(default)]
metadata: serde_json::Value,
data: serde_json::Value,
}
impl<'de> Deserialize<'de> for Event {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let repr = EventRepr::deserialize(deserializer)?;
let data =
serde_json::from_value::<EventData>(repr.data.clone()).unwrap_or(EventData::Custom {
event_type: repr.event_type.clone(),
payload: repr.data,
state_bearing: true,
});
Ok(Event {
id: repr.id,
event_type: repr.event_type,
ts: repr.ts,
session_id: repr.session_id,
turn_id: repr.turn_id,
sequence: repr.sequence,
metadata: repr.metadata,
data,
})
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct EventRequest {
pub session_id: SessionId,
pub turn_id: Option<TurnId>,
pub metadata: serde_json::Value,
pub data: EventData,
}
impl EventRequest {
pub fn new(session_id: SessionId, data: EventData) -> Self {
Self {
session_id,
turn_id: None,
metadata: serde_json::Value::Null,
data,
}
}
pub fn with_turn(session_id: SessionId, turn_id: TurnId, data: EventData) -> Self {
Self {
session_id,
turn_id: Some(turn_id),
metadata: serde_json::Value::Null,
data,
}
}
pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
self.metadata = metadata;
self
}
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),
metadata: self.metadata,
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,
metadata: self.metadata,
data: self.data,
}
}
}
#[async_trait]
pub trait EventListener: Send + Sync {
async fn on_event(&self, event: &Event);
fn event_types(&self) -> Option<Vec<&'static str>> {
None
}
fn name(&self) -> &'static str {
"EventListener"
}
}
#[derive(Debug, Clone, Default)]
pub struct NoopEventListener;
#[async_trait]
impl EventListener for NoopEventListener {
async fn on_event(&self, _event: &Event) {}
fn name(&self) -> &'static str {
"NoopEventListener"
}
}
#[derive(Default)]
pub struct CompositeEventListener {
listeners: Vec<Arc<dyn EventListener>>,
}
impl CompositeEventListener {
pub fn new(listeners: Vec<Arc<dyn EventListener>>) -> Self {
Self { listeners }
}
pub fn add(&mut self, listener: Arc<dyn EventListener>) {
self.listeners.push(listener);
}
pub fn len(&self) -> usize {
self.listeners.len()
}
pub fn is_empty(&self) -> bool {
self.listeners.is_empty()
}
}
#[async_trait]
impl EventListener for CompositeEventListener {
async fn on_event(&self, event: &Event) {
notify_event_listeners(&self.listeners, event).await;
}
fn name(&self) -> &'static str {
"CompositeEventListener"
}
}
pub async fn notify_event_listeners(listeners: &[Arc<dyn EventListener>], event: &Event) {
for listener in listeners {
if let Some(event_types) = listener.event_types()
&& !event_types.contains(&event.event_type.as_str())
{
continue;
}
let Ok(mut future) = catch_unwind(AssertUnwindSafe(|| listener.on_event(event))) else {
continue;
};
std::future::poll_fn(|context| {
match catch_unwind(AssertUnwindSafe(|| future.as_mut().poll(context))) {
Ok(Poll::Ready(())) | Err(_) => Poll::Ready(()),
Ok(Poll::Pending) => Poll::Pending,
}
})
.await;
}
}
#[cfg(test)]
mod tests {
use std::pin::pin;
use std::sync::atomic::{AtomicU32, Ordering};
use std::task::{Context, Waker};
use super::*;
fn block_on<F: std::future::Future>(future: F) -> F::Output {
let mut future = pin!(future);
let waker = Waker::noop();
let mut context = Context::from_waker(waker);
loop {
if let Poll::Ready(output) = future.as_mut().poll(&mut context) {
return output;
}
}
}
struct CountingListener {
count: Arc<AtomicU32>,
event_types: Option<Vec<&'static str>>,
}
#[async_trait]
impl EventListener for CountingListener {
async fn on_event(&self, _event: &Event) {
self.count.fetch_add(1, Ordering::SeqCst);
}
fn event_types(&self) -> Option<Vec<&'static str>> {
self.event_types.clone()
}
}
#[test]
fn event_serde_roundtrip() {
let request = EventRequest::new(
SessionId::new(),
EventData::InputMessage {
message: Message::user("hi"),
},
)
.with_metadata(serde_json::json!({"participant_id": "part_123"}));
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);
assert_eq!(back.metadata["participant_id"], "part_123");
}
#[test]
fn ephemeral_event_has_no_sequence() {
let request = EventRequest::new(
SessionId::new(),
EventData::OutputMessageDelta {
message_id: MessageId::new(),
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 { max_iterations: 16 }.is_ephemeral());
assert!(
!EventData::OutputMessageStarted {
message_id: MessageId::new(),
model: None,
iteration: None
}
.is_ephemeral()
);
assert!(
!EventData::OutputMessageCompleted {
message_id: MessageId::new(),
message: Message::assistant("hi"),
usage: crate::driver::Usage::default(),
}
.is_ephemeral()
);
}
#[test]
fn legacy_turn_started_defaults_the_iteration_limit() {
let data: EventData = serde_json::from_value(serde_json::json!({
"kind": "turn_started"
}))
.unwrap();
assert_eq!(data, EventData::TurnStarted { max_iterations: 16 });
}
#[test]
fn legacy_tool_rewrite_remains_readable_but_is_marked_incomplete() {
let data: EventData = serde_json::from_value(serde_json::json!({
"kind": "tool_rewritten",
"call_id": "call_1",
"name": "save",
"by": "guard"
}))
.unwrap();
assert!(matches!(data, EventData::ToolRewritten { call: None, .. }));
}
#[test]
fn an_unknown_event_kind_degrades_to_custom_instead_of_failing() {
let line = serde_json::json!({
"id": EventId::new(),
"type": "turn.compacted",
"ts": Utc::now(),
"session_id": SessionId::new(),
"sequence": 7,
"data": {"kind": "turn_compacted", "removed": 12, "note": "from a newer agentyk"},
})
.to_string();
let event: Event = serde_json::from_str(&line).expect("unknown kinds must stay readable");
assert_eq!(event.event_type, "turn.compacted");
assert_eq!(event.sequence, Some(7));
let EventData::Custom {
event_type,
ref payload,
..
} = event.data
else {
panic!("expected an unknown kind to degrade to Custom");
};
assert_eq!(event_type, "turn.compacted");
assert_eq!(payload["removed"], 12);
assert_eq!(payload["kind"], "turn_compacted");
}
#[test]
fn known_event_kinds_still_deserialize_to_their_typed_variant() {
let request = EventRequest::new(
SessionId::new(),
EventData::ToolCompleted {
call_id: "call_0".into(),
name: "read_file".into(),
output: "hi".into(),
is_error: false,
metadata: serde_json::Value::Null,
parts: Vec::new(),
},
);
let event = request.into_event(EventId::new(), Utc::now(), 3);
let back: Event = serde_json::from_str(&serde_json::to_string(&event).unwrap()).unwrap();
assert_eq!(back, event);
assert!(matches!(back.data, EventData::ToolCompleted { .. }));
}
#[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");
}
#[test]
fn composite_filters_and_forwards() {
let matching_count = Arc::new(AtomicU32::new(0));
let skipped_count = Arc::new(AtomicU32::new(0));
let all_count = Arc::new(AtomicU32::new(0));
let composite = CompositeEventListener::new(vec![
Arc::new(CountingListener {
count: matching_count.clone(),
event_types: Some(vec![event_types::INPUT_MESSAGE]),
}),
Arc::new(CountingListener {
count: skipped_count.clone(),
event_types: Some(vec![event_types::TOOL_COMPLETED]),
}),
Arc::new(CountingListener {
count: all_count.clone(),
event_types: None,
}),
]);
let event = EventRequest::new(
SessionId::new(),
EventData::InputMessage {
message: Message::user("hi"),
},
)
.into_event(EventId::new(), Utc::now(), 1);
block_on(composite.on_event(&event));
assert_eq!(matching_count.load(Ordering::SeqCst), 1);
assert_eq!(skipped_count.load(Ordering::SeqCst), 0);
assert_eq!(all_count.load(Ordering::SeqCst), 1);
}
#[test]
fn composite_continues_after_listener_panics() {
struct PanickingListener;
#[async_trait]
impl EventListener for PanickingListener {
async fn on_event(&self, _event: &Event) {
panic!("listener failed");
}
}
let count = Arc::new(AtomicU32::new(0));
let composite = CompositeEventListener::new(vec![
Arc::new(PanickingListener),
Arc::new(CountingListener {
count: count.clone(),
event_types: None,
}),
]);
let event = EventRequest::new(
SessionId::new(),
EventData::InputMessage {
message: Message::user("hi"),
},
)
.into_event(EventId::new(), Utc::now(), 1);
block_on(composite.on_event(&event));
assert_eq!(count.load(Ordering::SeqCst), 1);
}
#[test]
fn built_in_listeners_report_everruns_names() {
assert_eq!(NoopEventListener.name(), "NoopEventListener");
let composite = CompositeEventListener::default();
assert_eq!(composite.name(), "CompositeEventListener");
assert!(composite.is_empty());
assert_eq!(composite.len(), 0);
}
}