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}