use serde::{Deserialize, Serialize}; use serde_json::Value; /// Fine-grained agent lifecycle events, inspired by pi's event system. /// /// Covers the full agent execution lifecycle with enough granularity /// for UI rendering, telemetry, and extension hooks. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum AgentEvent { // === Agent lifecycle === AgentStart, AgentEnd { messages: Vec, total_input_tokens: u64, total_output_tokens: u64, }, // === Turn lifecycle === TurnStart { turn_index: usize, }, TurnEnd { turn_index: usize, assistant_text: Option, tool_call_count: usize, }, // === Message lifecycle === MessageStart { role: MessageRole, }, MessageTextDelta { index: usize, delta: String, }, MessageThinkingDelta { index: usize, delta: String, }, MessageEnd { role: MessageRole, }, // === Tool execution lifecycle === ToolExecutionStart { tool_call_id: String, tool_name: String, arguments: Value, }, ToolExecutionUpdate { tool_call_id: String, tool_name: String, partial_output: String, }, ToolExecutionEnd { tool_call_id: String, tool_name: String, output: Option, error: Option, elapsed_ms: i64, }, // === Steering / follow-up === SteeringMessagesInjected { count: usize, }, FollowUpMessagesInjected { count: usize, }, // === Context management === ContextCompacted { messages_compacted: usize, tokens_saved: i64, }, BranchSummaryCreated { entry_count: usize, summary_length: usize, }, // === Model switching === ModelSwitched { from_model: String, to_model: String, reason: String, }, // === Error and retry === ErrorClassified { category: String, message: String, will_retry: bool, retry_delay_ms: Option, }, RetryAttempt { attempt: usize, max_attempts: usize, delay_ms: u64, }, } /// Simplified message role for event display. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum MessageRole { User, Assistant, ToolResult, System, } /// A simplified message representation for `AgentEnd` events. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AgentEventMessage { pub role: MessageRole, pub content: String, pub tool_calls: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EventToolCall { pub id: String, pub name: String, pub arguments: Value, pub output: Option, pub error: Option, } /// An async-friendly event sink that collects or broadcasts events. pub struct EventSink { senders: Vec>, } impl EventSink { pub fn new() -> Self { Self { senders: Vec::new(), } } /// Subscribe to events, returns a receiver. pub fn subscribe( &mut self, ) -> tokio::sync::mpsc::UnboundedReceiver { let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); self.senders.push(tx); rx } /// Emit an event to all subscribers. Non-blocking; drops if receiver disconnected. pub fn emit(&self, event: AgentEvent) { for sender in &self.senders { let _ = sender.send(event.clone()); } } /// Check if there are any active subscribers. pub fn has_subscribers(&self) -> bool { !self.senders.is_empty() } /// Remove disconnected senders. pub fn cleanup(&mut self) { self.senders.retain(|s| !s.is_closed()); } } impl Default for EventSink { fn default() -> Self { Self::new() } } impl Clone for EventSink { fn clone(&self) -> Self { Self { senders: self.senders.clone(), } } }