mod consolidation;
mod context;
mod conversation_turn;
mod model_call_telemetry;
mod payloads;
mod store;
pub mod terminal_state;
pub use consolidation::{Consolidator, Pruner};
pub use context::SessionContextCompiler;
#[allow(unused_imports)]
pub use conversation_turn::{turn_from_event, ConversationTurn, ConversationTurnRole, FetchedTurn};
pub use model_call_telemetry::{
record_background_model_call_telemetry, record_model_call_telemetry, ModelCallTelemetryInput,
};
pub use payloads::*;
#[allow(unused_imports)]
pub use store::{
EventEmitter, EventStore, LlmStats, PolicyGraduationReport, SessionWriteDrift, TaskLlmSummary,
TaskWindowStats, ToolStats, WriteConsistencyGateStatus, WriteConsistencyReport,
WriteConsistencyThresholds,
};
#[allow(unused_imports)]
pub use terminal_state::TerminalState;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Event {
pub id: i64,
pub session_id: String,
pub event_type: EventType,
pub data: JsonValue,
pub created_at: DateTime<Utc>,
pub consolidated_at: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub task_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub turn_id: Option<String>,
}
impl Event {
pub fn new(session_id: impl Into<String>, event_type: EventType, data: JsonValue) -> Self {
let session_id = session_id.into();
let task_id = data
.get("task_id")
.and_then(|v| v.as_str())
.map(String::from);
let tool_name = data.get("name").and_then(|v| v.as_str()).map(String::from);
let turn_id = data
.get("turn_id")
.and_then(|v| v.as_str())
.map(String::from);
Self {
id: 0, session_id,
event_type,
data,
created_at: Utc::now(),
consolidated_at: None,
task_id,
tool_name,
turn_id,
}
}
pub fn parse_data<T: for<'de> Deserialize<'de>>(&self) -> anyhow::Result<T> {
Ok(serde_json::from_value(self.data.clone())?)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EventType {
SessionStart,
SessionEnd,
UserMessage,
AssistantResponse,
ToolCall,
ToolResult,
LlmCall,
ThinkingStart,
PolicyDecision,
DecisionPoint,
TaskStart,
TaskEnd,
Error,
SubAgentSpawn,
SubAgentComplete,
ApprovalRequested,
ApprovalGranted,
ApprovalDenied,
}
impl EventType {
pub fn as_str(&self) -> &'static str {
match self {
EventType::SessionStart => "session_start",
EventType::SessionEnd => "session_end",
EventType::UserMessage => "user_message",
EventType::AssistantResponse => "assistant_response",
EventType::ToolCall => "tool_call",
EventType::ToolResult => "tool_result",
EventType::LlmCall => "llm_call",
EventType::ThinkingStart => "thinking_start",
EventType::PolicyDecision => "policy_decision",
EventType::DecisionPoint => "decision_point",
EventType::TaskStart => "task_start",
EventType::TaskEnd => "task_end",
EventType::Error => "error",
EventType::SubAgentSpawn => "sub_agent_spawn",
EventType::SubAgentComplete => "sub_agent_complete",
EventType::ApprovalRequested => "approval_requested",
EventType::ApprovalGranted => "approval_granted",
EventType::ApprovalDenied => "approval_denied",
}
}
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> Option<Self> {
match s {
"session_start" => Some(EventType::SessionStart),
"session_end" => Some(EventType::SessionEnd),
"user_message" => Some(EventType::UserMessage),
"assistant_response" => Some(EventType::AssistantResponse),
"tool_call" => Some(EventType::ToolCall),
"tool_result" => Some(EventType::ToolResult),
"llm_call" => Some(EventType::LlmCall),
"thinking_start" => Some(EventType::ThinkingStart),
"policy_decision" => Some(EventType::PolicyDecision),
"decision_point" => Some(EventType::DecisionPoint),
"task_start" => Some(EventType::TaskStart),
"task_end" => Some(EventType::TaskEnd),
"error" => Some(EventType::Error),
"sub_agent_spawn" => Some(EventType::SubAgentSpawn),
"sub_agent_complete" => Some(EventType::SubAgentComplete),
"approval_requested" => Some(EventType::ApprovalRequested),
"approval_granted" => Some(EventType::ApprovalGranted),
"approval_denied" => Some(EventType::ApprovalDenied),
_ => None,
}
}
pub fn is_conversation_event(&self) -> bool {
matches!(
self,
EventType::UserMessage | EventType::AssistantResponse | EventType::ToolResult
)
}
pub fn is_learnable(&self) -> bool {
matches!(
self,
EventType::TaskEnd | EventType::Error | EventType::ToolResult
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TaskStatus {
Completed,
Cancelled,
Failed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TaskOutcome {
Succeeded,
Partial,
Failed,
}
impl TaskOutcome {
pub fn as_str(&self) -> &'static str {
match self {
TaskOutcome::Succeeded => "succeeded",
TaskOutcome::Partial => "partial",
TaskOutcome::Failed => "failed",
}
}
pub fn from_str(s: &str) -> Option<Self> {
match s {
"succeeded" => Some(TaskOutcome::Succeeded),
"partial" => Some(TaskOutcome::Partial),
"failed" => Some(TaskOutcome::Failed),
_ => None,
}
}
pub fn task_success(&self) -> bool {
matches!(self, TaskOutcome::Succeeded)
}
}
impl TaskStatus {
pub fn as_str(&self) -> &'static str {
match self {
TaskStatus::Completed => "completed",
TaskStatus::Cancelled => "cancelled",
TaskStatus::Failed => "failed",
}
}
pub fn from_str(s: &str) -> Option<Self> {
match s {
"completed" => Some(TaskStatus::Completed),
"cancelled" => Some(TaskStatus::Cancelled),
"failed" => Some(TaskStatus::Failed),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_event_type_roundtrip() {
for event_type in [
EventType::SessionStart,
EventType::UserMessage,
EventType::ToolCall,
EventType::LlmCall,
EventType::PolicyDecision,
EventType::DecisionPoint,
EventType::TaskEnd,
EventType::Error,
] {
let s = event_type.as_str();
let parsed = EventType::from_str(s).expect("should parse");
assert_eq!(event_type, parsed);
}
}
#[test]
fn test_event_creation() {
let event = Event::new(
"session_123",
EventType::TaskStart,
serde_json::json!({
"task_id": "task_456",
"description": "Test task"
}),
);
assert_eq!(event.session_id, "session_123");
assert_eq!(event.event_type, EventType::TaskStart);
assert_eq!(event.task_id, Some("task_456".to_string()));
}
}