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}