Skip to main content

opendev_runtime/event_bus/
events.rs

1//! Event types for the event bus system.
2//!
3//! Contains [`EventTopic`] for topic-based filtering, [`RuntimeEvent`] for
4//! typed events, and the legacy [`Event`] struct for backward compatibility.
5
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9use crate::session_status::SessionStatus;
10
11// ---------------------------------------------------------------------------
12// Event topic -- used for subscriber interest filtering (#94)
13// ---------------------------------------------------------------------------
14
15/// Identifies the category (topic) of a [`RuntimeEvent`].
16///
17/// Subscribers declare which topics they care about; the bus only delivers
18/// matching events.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
20pub enum EventTopic {
21    /// Tool execution lifecycle events.
22    Tool,
23    /// LLM request / response events.
24    Llm,
25    /// Agent lifecycle events (start, stop, error).
26    Agent,
27    /// Session lifecycle events.
28    Session,
29    /// Cost / token usage events.
30    Cost,
31    /// System-level events (config reload, shutdown).
32    System,
33    /// Custom / user-defined events.
34    Custom,
35}
36
37// ---------------------------------------------------------------------------
38// RuntimeEvent -- typed event variants (#93)
39// ---------------------------------------------------------------------------
40
41/// A strongly-typed event published on the bus.
42///
43/// Each variant carries only the data relevant to that event kind, replacing
44/// the previous stringly-typed `Event` struct.
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub enum RuntimeEvent {
47    // -- Tool events --
48    /// A tool call is about to start.
49    ToolCallStart {
50        tool_name: String,
51        call_id: String,
52        timestamp_ms: u64,
53    },
54    /// A tool call completed.
55    ToolCallEnd {
56        tool_name: String,
57        call_id: String,
58        duration_ms: u64,
59        success: bool,
60        timestamp_ms: u64,
61    },
62
63    // -- LLM events --
64    /// An LLM request was sent.
65    LlmRequestStart {
66        model: String,
67        request_id: String,
68        timestamp_ms: u64,
69    },
70    /// An LLM response was received.
71    LlmResponseEnd {
72        model: String,
73        request_id: String,
74        input_tokens: u64,
75        output_tokens: u64,
76        duration_ms: u64,
77        timestamp_ms: u64,
78    },
79
80    // -- Agent events --
81    /// An agent started working.
82    AgentStart {
83        agent_id: String,
84        task: String,
85        timestamp_ms: u64,
86    },
87    /// An agent finished.
88    AgentEnd {
89        agent_id: String,
90        success: bool,
91        timestamp_ms: u64,
92    },
93    /// An agent encountered an error.
94    AgentError {
95        agent_id: String,
96        error: String,
97        timestamp_ms: u64,
98    },
99
100    // -- Session events --
101    /// Session started.
102    SessionStart {
103        session_id: String,
104        timestamp_ms: u64,
105    },
106    /// Session ended.
107    SessionEnd {
108        session_id: String,
109        timestamp_ms: u64,
110    },
111    /// Session status changed (idle -> busy -> retry -> idle).
112    SessionStatusChanged {
113        session_id: String,
114        status: SessionStatus,
115        timestamp_ms: u64,
116    },
117
118    // -- Cost events --
119    /// Token usage was recorded.
120    TokenUsage {
121        model: String,
122        input_tokens: u64,
123        output_tokens: u64,
124        cost_usd: f64,
125        timestamp_ms: u64,
126    },
127
128    // -- Cost events --
129    /// Session cost budget has been exhausted.
130    ///
131    /// Published when [`CostTracker::is_over_budget`] returns `true` after
132    /// recording token usage. The agent loop should pause and notify the user.
133    BudgetExhausted {
134        budget_usd: f64,
135        total_cost_usd: f64,
136        timestamp_ms: u64,
137    },
138
139    // -- System events --
140    /// Configuration was reloaded.
141    ConfigReloaded { timestamp_ms: u64 },
142    /// Graceful shutdown requested.
143    ShutdownRequested { reason: String, timestamp_ms: u64 },
144
145    // -- Custom --
146    /// Escape hatch for events not covered by the typed variants.
147    Custom {
148        event_type: String,
149        source: String,
150        data: Value,
151        timestamp_ms: u64,
152    },
153}
154
155impl RuntimeEvent {
156    /// Return the [`EventTopic`] for this event.
157    pub fn topic(&self) -> EventTopic {
158        match self {
159            Self::ToolCallStart { .. } | Self::ToolCallEnd { .. } => EventTopic::Tool,
160            Self::LlmRequestStart { .. } | Self::LlmResponseEnd { .. } => EventTopic::Llm,
161            Self::AgentStart { .. } | Self::AgentEnd { .. } | Self::AgentError { .. } => {
162                EventTopic::Agent
163            }
164            Self::SessionStart { .. }
165            | Self::SessionEnd { .. }
166            | Self::SessionStatusChanged { .. } => EventTopic::Session,
167            Self::TokenUsage { .. } | Self::BudgetExhausted { .. } => EventTopic::Cost,
168            Self::ConfigReloaded { .. } | Self::ShutdownRequested { .. } => EventTopic::System,
169            Self::Custom { .. } => EventTopic::Custom,
170        }
171    }
172
173    /// Return the timestamp in milliseconds since epoch.
174    pub fn timestamp_ms(&self) -> u64 {
175        match self {
176            Self::ToolCallStart { timestamp_ms, .. }
177            | Self::ToolCallEnd { timestamp_ms, .. }
178            | Self::LlmRequestStart { timestamp_ms, .. }
179            | Self::LlmResponseEnd { timestamp_ms, .. }
180            | Self::AgentStart { timestamp_ms, .. }
181            | Self::AgentEnd { timestamp_ms, .. }
182            | Self::AgentError { timestamp_ms, .. }
183            | Self::SessionStart { timestamp_ms, .. }
184            | Self::SessionEnd { timestamp_ms, .. }
185            | Self::SessionStatusChanged { timestamp_ms, .. }
186            | Self::TokenUsage { timestamp_ms, .. }
187            | Self::BudgetExhausted { timestamp_ms, .. }
188            | Self::ConfigReloaded { timestamp_ms, .. }
189            | Self::ShutdownRequested { timestamp_ms, .. }
190            | Self::Custom { timestamp_ms, .. } => *timestamp_ms,
191        }
192    }
193}
194
195/// Helper: current time as milliseconds since UNIX epoch.
196pub fn now_ms() -> u64 {
197    std::time::SystemTime::now()
198        .duration_since(std::time::UNIX_EPOCH)
199        .unwrap_or_default()
200        .as_millis() as u64
201}
202
203// ---------------------------------------------------------------------------
204// Legacy Event -- kept for backward compatibility
205// ---------------------------------------------------------------------------
206
207/// A legacy untyped event (kept for backward compatibility).
208///
209/// New code should prefer [`RuntimeEvent`] variants.
210#[derive(Debug, Clone)]
211pub struct Event {
212    /// Event type identifier (e.g., "tool_call_start", "llm_response").
213    pub event_type: String,
214    /// Component that published the event.
215    pub source: String,
216    /// Event payload.
217    pub data: Value,
218    /// Timestamp (milliseconds since epoch).
219    pub timestamp_ms: u64,
220}
221
222impl Event {
223    /// Create a new event.
224    pub fn new(event_type: impl Into<String>, source: impl Into<String>, data: Value) -> Self {
225        Self {
226            event_type: event_type.into(),
227            source: source.into(),
228            data,
229            timestamp_ms: now_ms(),
230        }
231    }
232
233    /// Convert a legacy `Event` into a [`RuntimeEvent::Custom`].
234    pub fn into_runtime_event(self) -> RuntimeEvent {
235        RuntimeEvent::Custom {
236            event_type: self.event_type,
237            source: self.source,
238            data: self.data,
239            timestamp_ms: self.timestamp_ms,
240        }
241    }
242}