1use serde::{Deserialize, Serialize};
15use serde_json::Value;
16
17pub mod trace;
18
19pub const EVENT_SCHEMA_VERSION: &str = "0.1.0";
21
22#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
25#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
26pub struct VersionedThreadEvent {
27 pub schema_version: String,
29 pub event: ThreadEvent,
31}
32
33impl VersionedThreadEvent {
34 pub fn new(event: ThreadEvent) -> Self {
37 Self {
38 schema_version: EVENT_SCHEMA_VERSION.to_string(),
39 event,
40 }
41 }
42
43 pub fn into_event(self) -> ThreadEvent {
45 self.event
46 }
47}
48
49impl From<ThreadEvent> for VersionedThreadEvent {
50 fn from(event: ThreadEvent) -> Self {
51 Self::new(event)
52 }
53}
54
55pub trait EventEmitter {
57 fn emit(&mut self, event: &ThreadEvent);
59}
60
61impl<F> EventEmitter for F
62where
63 F: FnMut(&ThreadEvent),
64{
65 fn emit(&mut self, event: &ThreadEvent) {
66 self(event);
67 }
68}
69
70#[cfg(feature = "serde-json")]
72pub mod json {
73 use super::{ThreadEvent, VersionedThreadEvent};
74
75 pub fn to_value(event: &ThreadEvent) -> serde_json::Result<serde_json::Value> {
77 serde_json::to_value(event)
78 }
79
80 pub fn to_string(event: &ThreadEvent) -> serde_json::Result<String> {
82 serde_json::to_string(event)
83 }
84
85 pub fn from_str(payload: &str) -> serde_json::Result<ThreadEvent> {
87 serde_json::from_str(payload)
88 }
89
90 pub fn versioned_to_string(event: &ThreadEvent) -> serde_json::Result<String> {
92 serde_json::to_string(&VersionedThreadEvent::new(event.clone()))
93 }
94
95 pub fn versioned_from_str(payload: &str) -> serde_json::Result<VersionedThreadEvent> {
97 serde_json::from_str(payload)
98 }
99}
100
101#[cfg(feature = "telemetry-log")]
102mod log_support {
103 use log::Level;
104
105 use super::{EventEmitter, ThreadEvent, json};
106
107 #[derive(Debug, Clone)]
109 pub struct LogEmitter {
110 level: Level,
111 }
112
113 impl LogEmitter {
114 pub fn new(level: Level) -> Self {
116 Self { level }
117 }
118 }
119
120 impl Default for LogEmitter {
121 fn default() -> Self {
122 Self { level: Level::Info }
123 }
124 }
125
126 impl EventEmitter for LogEmitter {
127 fn emit(&mut self, event: &ThreadEvent) {
128 if log::log_enabled!(self.level) {
129 match json::to_string(event) {
130 Ok(serialized) => log::log!(self.level, "{}", serialized),
131 Err(err) => log::log!(
132 self.level,
133 "failed to serialize vtcode exec event for logging: {err}"
134 ),
135 }
136 }
137 }
138 }
139
140 pub use LogEmitter as PublicLogEmitter;
141}
142
143#[cfg(feature = "telemetry-log")]
144pub use log_support::PublicLogEmitter as LogEmitter;
145
146#[cfg(feature = "telemetry-tracing")]
147mod tracing_support {
148 use tracing::Level;
149
150 use super::{EVENT_SCHEMA_VERSION, EventEmitter, ThreadEvent, VersionedThreadEvent};
151
152 #[derive(Debug, Clone)]
154 pub struct TracingEmitter {
155 level: Level,
156 }
157
158 impl TracingEmitter {
159 pub fn new(level: Level) -> Self {
161 Self { level }
162 }
163 }
164
165 impl Default for TracingEmitter {
166 fn default() -> Self {
167 Self { level: Level::INFO }
168 }
169 }
170
171 impl EventEmitter for TracingEmitter {
172 fn emit(&mut self, event: &ThreadEvent) {
173 match self.level {
174 Level::TRACE => tracing::event!(
175 target: "vtcode_exec_events",
176 Level::TRACE,
177 schema_version = EVENT_SCHEMA_VERSION,
178 event = ?VersionedThreadEvent::new(event.clone()),
179 "vtcode_exec_event"
180 ),
181 Level::DEBUG => tracing::event!(
182 target: "vtcode_exec_events",
183 Level::DEBUG,
184 schema_version = EVENT_SCHEMA_VERSION,
185 event = ?VersionedThreadEvent::new(event.clone()),
186 "vtcode_exec_event"
187 ),
188 Level::INFO => tracing::event!(
189 target: "vtcode_exec_events",
190 Level::INFO,
191 schema_version = EVENT_SCHEMA_VERSION,
192 event = ?VersionedThreadEvent::new(event.clone()),
193 "vtcode_exec_event"
194 ),
195 Level::WARN => tracing::event!(
196 target: "vtcode_exec_events",
197 Level::WARN,
198 schema_version = EVENT_SCHEMA_VERSION,
199 event = ?VersionedThreadEvent::new(event.clone()),
200 "vtcode_exec_event"
201 ),
202 Level::ERROR => tracing::event!(
203 target: "vtcode_exec_events",
204 Level::ERROR,
205 schema_version = EVENT_SCHEMA_VERSION,
206 event = ?VersionedThreadEvent::new(event.clone()),
207 "vtcode_exec_event"
208 ),
209 }
210 }
211 }
212
213 pub use TracingEmitter as PublicTracingEmitter;
214}
215
216#[cfg(feature = "telemetry-tracing")]
217pub use tracing_support::PublicTracingEmitter as TracingEmitter;
218
219#[cfg(feature = "schema-export")]
220pub mod schema {
221 use schemars::{schema::RootSchema, schema_for};
222
223 use super::{ThreadEvent, VersionedThreadEvent};
224
225 pub fn thread_event_schema() -> RootSchema {
227 schema_for!(ThreadEvent)
228 }
229
230 pub fn versioned_thread_event_schema() -> RootSchema {
232 schema_for!(VersionedThreadEvent)
233 }
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
238#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
239#[serde(tag = "type")]
240pub enum ThreadEvent {
241 #[serde(rename = "thread.started")]
243 ThreadStarted(ThreadStartedEvent),
244 #[serde(rename = "turn.started")]
246 TurnStarted(TurnStartedEvent),
247 #[serde(rename = "turn.completed")]
249 TurnCompleted(TurnCompletedEvent),
250 #[serde(rename = "turn.failed")]
252 TurnFailed(TurnFailedEvent),
253 #[serde(rename = "item.started")]
255 ItemStarted(ItemStartedEvent),
256 #[serde(rename = "item.updated")]
258 ItemUpdated(ItemUpdatedEvent),
259 #[serde(rename = "item.completed")]
261 ItemCompleted(ItemCompletedEvent),
262 #[serde(rename = "error")]
264 Error(ThreadErrorEvent),
265}
266
267#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
268#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
269pub struct ThreadStartedEvent {
270 pub thread_id: String,
272}
273
274#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
275#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
276pub struct TurnStartedEvent {}
277
278#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
279#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
280pub struct TurnCompletedEvent {
281 pub usage: Usage,
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
286#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
287pub struct TurnFailedEvent {
288 pub message: String,
290 #[serde(skip_serializing_if = "Option::is_none")]
292 pub usage: Option<Usage>,
293}
294
295#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
296#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
297pub struct ThreadErrorEvent {
298 pub message: String,
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
303#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
304pub struct Usage {
305 pub input_tokens: u64,
307 pub cached_input_tokens: u64,
309 pub output_tokens: u64,
311}
312
313#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
314#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
315pub struct ItemCompletedEvent {
316 pub item: ThreadItem,
318}
319
320#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
321#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
322pub struct ItemStartedEvent {
323 pub item: ThreadItem,
325}
326
327#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
328#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
329pub struct ItemUpdatedEvent {
330 pub item: ThreadItem,
332}
333
334#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
335#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
336pub struct ThreadItem {
337 pub id: String,
339 #[serde(flatten)]
341 pub details: ThreadItemDetails,
342}
343
344#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
345#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
346#[serde(tag = "type", rename_all = "snake_case")]
347pub enum ThreadItemDetails {
348 AgentMessage(AgentMessageItem),
350 Reasoning(ReasoningItem),
352 CommandExecution(CommandExecutionItem),
354 FileChange(FileChangeItem),
356 McpToolCall(McpToolCallItem),
358 WebSearch(WebSearchItem),
360 Error(ErrorItem),
362}
363
364#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
365#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
366pub struct AgentMessageItem {
367 pub text: String,
369}
370
371#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
372#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
373pub struct ReasoningItem {
374 pub text: String,
376}
377
378#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
379#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
380#[serde(rename_all = "snake_case")]
381pub enum CommandExecutionStatus {
382 #[default]
384 Completed,
385 Failed,
387 InProgress,
389}
390
391#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
392#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
393pub struct CommandExecutionItem {
394 pub command: String,
396 #[serde(default)]
398 pub aggregated_output: String,
399 #[serde(skip_serializing_if = "Option::is_none")]
401 pub exit_code: Option<i32>,
402 pub status: CommandExecutionStatus,
404}
405
406#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
407#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
408pub struct FileChangeItem {
409 pub changes: Vec<FileUpdateChange>,
411 pub status: PatchApplyStatus,
413}
414
415#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
416#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
417pub struct FileUpdateChange {
418 pub path: String,
420 pub kind: PatchChangeKind,
422}
423
424#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
425#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
426#[serde(rename_all = "snake_case")]
427pub enum PatchApplyStatus {
428 Completed,
430 Failed,
432}
433
434#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
435#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
436#[serde(rename_all = "snake_case")]
437pub enum PatchChangeKind {
438 Add,
440 Delete,
442 Update,
444}
445
446#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
447#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
448pub struct McpToolCallItem {
449 pub tool_name: String,
451 #[serde(skip_serializing_if = "Option::is_none")]
453 pub arguments: Option<Value>,
454 #[serde(skip_serializing_if = "Option::is_none")]
456 pub result: Option<String>,
457 #[serde(skip_serializing_if = "Option::is_none")]
459 pub status: Option<McpToolCallStatus>,
460}
461
462#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
463#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
464#[serde(rename_all = "snake_case")]
465pub enum McpToolCallStatus {
466 Started,
468 Completed,
470 Failed,
472}
473
474#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
475#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
476pub struct WebSearchItem {
477 pub query: String,
479 #[serde(skip_serializing_if = "Option::is_none")]
481 pub provider: Option<String>,
482 #[serde(skip_serializing_if = "Option::is_none")]
484 pub results: Option<Vec<String>>,
485}
486
487#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
488#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
489pub struct ErrorItem {
490 pub message: String,
492}
493
494#[cfg(test)]
495mod tests {
496 use super::*;
497 use std::error::Error;
498
499 #[test]
500 fn thread_event_round_trip() -> Result<(), Box<dyn Error>> {
501 let event = ThreadEvent::TurnCompleted(TurnCompletedEvent {
502 usage: Usage {
503 input_tokens: 1,
504 cached_input_tokens: 2,
505 output_tokens: 3,
506 },
507 });
508
509 let json = serde_json::to_string(&event)?;
510 let restored: ThreadEvent = serde_json::from_str(&json)?;
511
512 assert_eq!(restored, event);
513 Ok(())
514 }
515
516 #[test]
517 fn versioned_event_wraps_schema_version() {
518 let event = ThreadEvent::ThreadStarted(ThreadStartedEvent {
519 thread_id: "abc".to_string(),
520 });
521
522 let versioned = VersionedThreadEvent::new(event.clone());
523
524 assert_eq!(versioned.schema_version, EVENT_SCHEMA_VERSION);
525 assert_eq!(versioned.event, event);
526 assert_eq!(versioned.into_event(), event);
527 }
528
529 #[cfg(feature = "serde-json")]
530 #[test]
531 fn versioned_json_round_trip() -> Result<(), Box<dyn Error>> {
532 let event = ThreadEvent::ItemCompleted(ItemCompletedEvent {
533 item: ThreadItem {
534 id: "item-1".to_string(),
535 details: ThreadItemDetails::AgentMessage(AgentMessageItem {
536 text: "hello".to_string(),
537 }),
538 },
539 });
540
541 let payload = crate::json::versioned_to_string(&event)?;
542 let restored = crate::json::versioned_from_str(&payload)?;
543
544 assert_eq!(restored.schema_version, EVENT_SCHEMA_VERSION);
545 assert_eq!(restored.event, event);
546 Ok(())
547 }
548}