1use serde::{Deserialize, Serialize};
10use serde_json::Value;
11
12pub const EVENT_SCHEMA_VERSION: &str = "0.1.0";
14
15#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
18pub struct VersionedThreadEvent {
19    pub schema_version: String,
21    pub event: ThreadEvent,
23}
24
25impl VersionedThreadEvent {
26    pub fn new(event: ThreadEvent) -> Self {
29        Self {
30            schema_version: EVENT_SCHEMA_VERSION.to_string(),
31            event,
32        }
33    }
34
35    pub fn into_event(self) -> ThreadEvent {
37        self.event
38    }
39}
40
41impl From<ThreadEvent> for VersionedThreadEvent {
42    fn from(event: ThreadEvent) -> Self {
43        Self::new(event)
44    }
45}
46
47pub trait EventEmitter {
49    fn emit(&mut self, event: &ThreadEvent);
51}
52
53impl<F> EventEmitter for F
54where
55    F: FnMut(&ThreadEvent),
56{
57    fn emit(&mut self, event: &ThreadEvent) {
58        self(event);
59    }
60}
61
62#[cfg(feature = "serde-json")]
64pub mod json {
65    use super::{ThreadEvent, VersionedThreadEvent};
66
67    pub fn to_value(event: &ThreadEvent) -> serde_json::Result<serde_json::Value> {
69        serde_json::to_value(event)
70    }
71
72    pub fn to_string(event: &ThreadEvent) -> serde_json::Result<String> {
74        serde_json::to_string(event)
75    }
76
77    pub fn from_str(payload: &str) -> serde_json::Result<ThreadEvent> {
79        serde_json::from_str(payload)
80    }
81
82    pub fn versioned_to_string(event: &ThreadEvent) -> serde_json::Result<String> {
84        serde_json::to_string(&VersionedThreadEvent::new(event.clone()))
85    }
86
87    pub fn versioned_from_str(payload: &str) -> serde_json::Result<VersionedThreadEvent> {
89        serde_json::from_str(payload)
90    }
91}
92
93#[cfg(feature = "telemetry-log")]
94mod log_support {
95    use log::Level;
96
97    use super::{EventEmitter, ThreadEvent, json};
98
99    #[derive(Debug, Clone)]
101    pub struct LogEmitter {
102        level: Level,
103    }
104
105    impl LogEmitter {
106        pub fn new(level: Level) -> Self {
108            Self { level }
109        }
110    }
111
112    impl Default for LogEmitter {
113        fn default() -> Self {
114            Self { level: Level::Info }
115        }
116    }
117
118    impl EventEmitter for LogEmitter {
119        fn emit(&mut self, event: &ThreadEvent) {
120            if log::log_enabled!(self.level) {
121                match json::to_string(event) {
122                    Ok(serialized) => log::log!(self.level, "{}", serialized),
123                    Err(err) => log::log!(
124                        self.level,
125                        "failed to serialize vtcode exec event for logging: {err}"
126                    ),
127                }
128            }
129        }
130    }
131
132    pub(crate) use LogEmitter as PublicLogEmitter;
133}
134
135#[cfg(feature = "telemetry-log")]
136pub use log_support::PublicLogEmitter as LogEmitter;
137
138#[cfg(feature = "telemetry-tracing")]
139mod tracing_support {
140    use tracing::Level;
141
142    use super::{EVENT_SCHEMA_VERSION, EventEmitter, ThreadEvent, VersionedThreadEvent};
143
144    #[derive(Debug, Clone)]
146    pub struct TracingEmitter {
147        level: Level,
148    }
149
150    impl TracingEmitter {
151        pub fn new(level: Level) -> Self {
153            Self { level }
154        }
155    }
156
157    impl Default for TracingEmitter {
158        fn default() -> Self {
159            Self { level: Level::INFO }
160        }
161    }
162
163    impl EventEmitter for TracingEmitter {
164        fn emit(&mut self, event: &ThreadEvent) {
165            match self.level {
166                Level::TRACE => tracing::event!(
167                    target: "vtcode_exec_events",
168                    Level::TRACE,
169                    schema_version = EVENT_SCHEMA_VERSION,
170                    event = ?VersionedThreadEvent::new(event.clone()),
171                    "vtcode_exec_event"
172                ),
173                Level::DEBUG => tracing::event!(
174                    target: "vtcode_exec_events",
175                    Level::DEBUG,
176                    schema_version = EVENT_SCHEMA_VERSION,
177                    event = ?VersionedThreadEvent::new(event.clone()),
178                    "vtcode_exec_event"
179                ),
180                Level::INFO => tracing::event!(
181                    target: "vtcode_exec_events",
182                    Level::INFO,
183                    schema_version = EVENT_SCHEMA_VERSION,
184                    event = ?VersionedThreadEvent::new(event.clone()),
185                    "vtcode_exec_event"
186                ),
187                Level::WARN => tracing::event!(
188                    target: "vtcode_exec_events",
189                    Level::WARN,
190                    schema_version = EVENT_SCHEMA_VERSION,
191                    event = ?VersionedThreadEvent::new(event.clone()),
192                    "vtcode_exec_event"
193                ),
194                Level::ERROR => tracing::event!(
195                    target: "vtcode_exec_events",
196                    Level::ERROR,
197                    schema_version = EVENT_SCHEMA_VERSION,
198                    event = ?VersionedThreadEvent::new(event.clone()),
199                    "vtcode_exec_event"
200                ),
201            }
202        }
203    }
204
205    pub(crate) use TracingEmitter as PublicTracingEmitter;
206}
207
208#[cfg(feature = "telemetry-tracing")]
209pub use tracing_support::PublicTracingEmitter as TracingEmitter;
210
211#[cfg(feature = "schema-export")]
212pub mod schema {
213    use schemars::{schema::RootSchema, schema_for};
214
215    use super::{ThreadEvent, VersionedThreadEvent};
216
217    pub fn thread_event_schema() -> RootSchema {
219        schema_for!(ThreadEvent)
220    }
221
222    pub fn versioned_thread_event_schema() -> RootSchema {
224        schema_for!(VersionedThreadEvent)
225    }
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
230#[serde(tag = "type")]
231pub enum ThreadEvent {
232    #[serde(rename = "thread.started")]
234    ThreadStarted(ThreadStartedEvent),
235    #[serde(rename = "turn.started")]
237    TurnStarted(TurnStartedEvent),
238    #[serde(rename = "turn.completed")]
240    TurnCompleted(TurnCompletedEvent),
241    #[serde(rename = "turn.failed")]
243    TurnFailed(TurnFailedEvent),
244    #[serde(rename = "item.started")]
246    ItemStarted(ItemStartedEvent),
247    #[serde(rename = "item.updated")]
249    ItemUpdated(ItemUpdatedEvent),
250    #[serde(rename = "item.completed")]
252    ItemCompleted(ItemCompletedEvent),
253    #[serde(rename = "error")]
255    Error(ThreadErrorEvent),
256}
257
258#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
259pub struct ThreadStartedEvent {
260    pub thread_id: String,
262}
263
264#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
265pub struct TurnStartedEvent {}
266
267#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
268pub struct TurnCompletedEvent {
269    pub usage: Usage,
271}
272
273#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
274pub struct TurnFailedEvent {
275    pub message: String,
277    #[serde(skip_serializing_if = "Option::is_none")]
279    pub usage: Option<Usage>,
280}
281
282#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
283pub struct ThreadErrorEvent {
284    pub message: String,
286}
287
288#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
289pub struct Usage {
290    pub input_tokens: u64,
292    pub cached_input_tokens: u64,
294    pub output_tokens: u64,
296}
297
298#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
299pub struct ItemCompletedEvent {
300    pub item: ThreadItem,
302}
303
304#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
305pub struct ItemStartedEvent {
306    pub item: ThreadItem,
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
311pub struct ItemUpdatedEvent {
312    pub item: ThreadItem,
314}
315
316#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
317pub struct ThreadItem {
318    pub id: String,
320    #[serde(flatten)]
322    pub details: ThreadItemDetails,
323}
324
325#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
326#[serde(tag = "type", rename_all = "snake_case")]
327pub enum ThreadItemDetails {
328    AgentMessage(AgentMessageItem),
330    Reasoning(ReasoningItem),
332    CommandExecution(CommandExecutionItem),
334    FileChange(FileChangeItem),
336    McpToolCall(McpToolCallItem),
338    WebSearch(WebSearchItem),
340    Error(ErrorItem),
342}
343
344#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
345pub struct AgentMessageItem {
346    pub text: String,
348}
349
350#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
351pub struct ReasoningItem {
352    pub text: String,
354}
355
356#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
357#[serde(rename_all = "snake_case")]
358pub enum CommandExecutionStatus {
359    #[default]
361    Completed,
362    Failed,
364    InProgress,
366}
367
368#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
369pub struct CommandExecutionItem {
370    pub command: String,
372    #[serde(default)]
374    pub aggregated_output: String,
375    #[serde(skip_serializing_if = "Option::is_none")]
377    pub exit_code: Option<i32>,
378    pub status: CommandExecutionStatus,
380}
381
382#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
383pub struct FileChangeItem {
384    pub changes: Vec<FileUpdateChange>,
386    pub status: PatchApplyStatus,
388}
389
390#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
391pub struct FileUpdateChange {
392    pub path: String,
394    pub kind: PatchChangeKind,
396}
397
398#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
399#[serde(rename_all = "snake_case")]
400pub enum PatchApplyStatus {
401    Completed,
403    Failed,
405}
406
407#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
408#[serde(rename_all = "snake_case")]
409pub enum PatchChangeKind {
410    Add,
412    Delete,
414    Update,
416}
417
418#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
419pub struct McpToolCallItem {
420    pub tool_name: String,
422    #[serde(skip_serializing_if = "Option::is_none")]
424    pub arguments: Option<Value>,
425    #[serde(skip_serializing_if = "Option::is_none")]
427    pub result: Option<String>,
428    #[serde(skip_serializing_if = "Option::is_none")]
430    pub status: Option<McpToolCallStatus>,
431}
432
433#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
434#[serde(rename_all = "snake_case")]
435pub enum McpToolCallStatus {
436    Started,
438    Completed,
440    Failed,
442}
443
444#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
445pub struct WebSearchItem {
446    pub query: String,
448    #[serde(skip_serializing_if = "Option::is_none")]
450    pub provider: Option<String>,
451    #[serde(skip_serializing_if = "Option::is_none")]
453    pub results: Option<Vec<String>>,
454}
455
456#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
457pub struct ErrorItem {
458    pub message: String,
460}
461
462#[cfg(test)]
463mod tests {
464    use super::*;
465
466    #[test]
467    fn thread_event_round_trip() {
468        let event = ThreadEvent::TurnCompleted(TurnCompletedEvent {
469            usage: Usage {
470                input_tokens: 1,
471                cached_input_tokens: 2,
472                output_tokens: 3,
473            },
474        });
475
476        let json = serde_json::to_string(&event).expect("serialize");
477        let restored: ThreadEvent = serde_json::from_str(&json).expect("deserialize");
478
479        assert_eq!(restored, event);
480    }
481
482    #[test]
483    fn versioned_event_wraps_schema_version() {
484        let event = ThreadEvent::ThreadStarted(ThreadStartedEvent {
485            thread_id: "abc".to_string(),
486        });
487
488        let versioned = VersionedThreadEvent::new(event.clone());
489
490        assert_eq!(versioned.schema_version, EVENT_SCHEMA_VERSION);
491        assert_eq!(versioned.event, event);
492        assert_eq!(versioned.into_event(), event);
493    }
494
495    #[cfg(feature = "serde-json")]
496    #[test]
497    fn versioned_json_round_trip() {
498        let event = ThreadEvent::ItemCompleted(ItemCompletedEvent {
499            item: ThreadItem {
500                id: "item-1".to_string(),
501                details: ThreadItemDetails::AgentMessage(AgentMessageItem {
502                    text: "hello".to_string(),
503                }),
504            },
505        });
506
507        let payload = crate::json::versioned_to_string(&event).expect("serialize");
508        let restored = crate::json::versioned_from_str(&payload).expect("deserialize");
509
510        assert_eq!(restored.schema_version, EVENT_SCHEMA_VERSION);
511        assert_eq!(restored.event, event);
512    }
513}