use uuid::Uuid;
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum RunEvent {
SessionStarted {
session: Uuid,
},
TurnStarted {
session: Uuid,
turn: usize,
},
ModelStarted {
session: Uuid,
turn: usize,
model: String,
},
ModelFinished {
session: Uuid,
turn: usize,
},
ToolStarted {
session: Uuid,
turn: usize,
tool: String,
call_id: String,
},
ToolFinished {
session: Uuid,
turn: usize,
tool: String,
call_id: String,
ok: bool,
},
TurnFinished {
session: Uuid,
turn: usize,
},
RunFailed {
session: Uuid,
error: String,
},
}
pub const ERROR_SUMMARY_MAX_CHARS: usize = 200;
#[must_use]
pub fn run_failed(session: Uuid, error: impl AsRef<str>) -> RunEvent {
let error = error.as_ref();
let summary = if error.len() > ERROR_SUMMARY_MAX_CHARS {
format!("{}…(truncated)", &error[..ERROR_SUMMARY_MAX_CHARS])
} else {
error.to_string()
};
RunEvent::RunFailed {
session,
error: summary,
}
}
pub trait EventSink: Send + Sync {
fn emit(&self, event: RunEvent);
}
#[derive(Default)]
pub struct NullEventSink;
impl EventSink for NullEventSink {
fn emit(&self, _event: RunEvent) {}
}
#[derive(Default)]
pub struct RunHooks<'a> {
pub session_id: Option<Uuid>,
pub turn_sink: Option<&'a dyn crate::TurnSink>,
pub event_sink: Option<&'a dyn EventSink>,
pub policy: Option<&'a dyn crate::policy::ToolPolicy>,
}
impl<'a> RunHooks<'a> {
#[must_use]
pub fn from_turn_sink(turn_sink: &'a dyn crate::TurnSink) -> Self {
Self {
session_id: None,
turn_sink: Some(turn_sink),
event_sink: None,
policy: None,
}
}
pub fn emit_event(&self, make: impl FnOnce(Uuid) -> RunEvent) {
if let (Some(sid), Some(sink)) = (self.session_id, self.event_sink) {
sink.emit(make(sid));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn run_failed_truncates_long_errors() {
let long = "x".repeat(ERROR_SUMMARY_MAX_CHARS * 2);
let event = run_failed(Uuid::nil(), &long);
match event {
RunEvent::RunFailed { error, .. } => {
assert!(error.len() < long.len(), "error not truncated");
assert!(error.ends_with("…(truncated)"));
}
_ => panic!("expected RunFailed"),
}
}
#[test]
fn run_failed_preserves_short_errors() {
let event = run_failed(Uuid::nil(), "short error");
match event {
RunEvent::RunFailed { error, .. } => {
assert_eq!(error, "short error");
}
_ => panic!("expected RunFailed"),
}
}
#[test]
fn run_failed_accepts_string_and_str() {
let _ = run_failed(Uuid::nil(), "literal");
let _ = run_failed(Uuid::nil(), String::from("owned"));
}
}