1use serde::{Deserialize, Serialize};
15use serde_json::Value;
16
17pub mod trace;
18
19pub const EVENT_SCHEMA_VERSION: &str = "0.4.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, schema_for};
222
223 use super::{ThreadEvent, VersionedThreadEvent};
224
225 pub fn thread_event_schema() -> Schema {
227 schema_for!(ThreadEvent)
228 }
229
230 pub fn versioned_thread_event_schema() -> Schema {
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 = "thread.completed")]
246 ThreadCompleted(ThreadCompletedEvent),
247 #[serde(rename = "thread.compact_boundary")]
249 ThreadCompactBoundary(ThreadCompactBoundaryEvent),
250 #[serde(rename = "turn.started")]
252 TurnStarted(TurnStartedEvent),
253 #[serde(rename = "turn.completed")]
255 TurnCompleted(TurnCompletedEvent),
256 #[serde(rename = "turn.failed")]
258 TurnFailed(TurnFailedEvent),
259 #[serde(rename = "item.started")]
261 ItemStarted(ItemStartedEvent),
262 #[serde(rename = "item.updated")]
264 ItemUpdated(ItemUpdatedEvent),
265 #[serde(rename = "item.completed")]
267 ItemCompleted(ItemCompletedEvent),
268 #[serde(rename = "plan.delta")]
270 PlanDelta(PlanDeltaEvent),
271 #[serde(rename = "error")]
273 Error(ThreadErrorEvent),
274}
275
276#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
277#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
278pub struct ThreadStartedEvent {
279 pub thread_id: String,
281}
282
283#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
284#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
285#[serde(rename_all = "snake_case")]
286pub enum ThreadCompletionSubtype {
287 Success,
288 ErrorMaxTurns,
289 ErrorMaxBudgetUsd,
290 ErrorDuringExecution,
291 Cancelled,
292}
293
294impl ThreadCompletionSubtype {
295 pub const fn as_str(&self) -> &'static str {
296 match self {
297 Self::Success => "success",
298 Self::ErrorMaxTurns => "error_max_turns",
299 Self::ErrorMaxBudgetUsd => "error_max_budget_usd",
300 Self::ErrorDuringExecution => "error_during_execution",
301 Self::Cancelled => "cancelled",
302 }
303 }
304
305 pub const fn is_success(self) -> bool {
306 matches!(self, Self::Success)
307 }
308}
309
310#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
311#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
312#[serde(rename_all = "snake_case")]
313pub enum CompactionTrigger {
314 Manual,
315 Auto,
316 Recovery,
317}
318
319impl CompactionTrigger {
320 pub const fn as_str(self) -> &'static str {
321 match self {
322 Self::Manual => "manual",
323 Self::Auto => "auto",
324 Self::Recovery => "recovery",
325 }
326 }
327}
328
329#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
330#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
331#[serde(rename_all = "snake_case")]
332pub enum CompactionMode {
333 Provider,
334 Local,
335}
336
337impl CompactionMode {
338 pub const fn as_str(self) -> &'static str {
339 match self {
340 Self::Provider => "provider",
341 Self::Local => "local",
342 }
343 }
344}
345
346#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
347#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
348pub struct ThreadCompletedEvent {
349 pub thread_id: String,
351 pub session_id: String,
353 pub subtype: ThreadCompletionSubtype,
355 pub outcome_code: String,
357 #[serde(skip_serializing_if = "Option::is_none")]
359 pub result: Option<String>,
360 #[serde(skip_serializing_if = "Option::is_none")]
362 pub stop_reason: Option<String>,
363 pub usage: Usage,
365 #[serde(skip_serializing_if = "Option::is_none")]
367 pub total_cost_usd: Option<serde_json::Number>,
368 pub num_turns: usize,
370}
371
372#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
373#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
374pub struct ThreadCompactBoundaryEvent {
375 pub thread_id: String,
377 pub trigger: CompactionTrigger,
379 pub mode: CompactionMode,
381 pub original_message_count: usize,
383 pub compacted_message_count: usize,
385 #[serde(skip_serializing_if = "Option::is_none")]
387 pub history_artifact_path: Option<String>,
388}
389
390#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
391#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
392pub struct TurnStartedEvent {}
393
394#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
395#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
396pub struct TurnCompletedEvent {
397 pub usage: Usage,
399}
400
401#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
402#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
403pub struct TurnFailedEvent {
404 pub message: String,
406 #[serde(skip_serializing_if = "Option::is_none")]
408 pub usage: Option<Usage>,
409}
410
411#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
412#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
413pub struct ThreadErrorEvent {
414 pub message: String,
416}
417
418#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
419#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
420pub struct Usage {
421 pub input_tokens: u64,
423 pub cached_input_tokens: u64,
425 pub cache_creation_tokens: u64,
427 pub output_tokens: u64,
429}
430
431#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
432#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
433pub struct ItemCompletedEvent {
434 pub item: ThreadItem,
436}
437
438#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
439#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
440pub struct ItemStartedEvent {
441 pub item: ThreadItem,
443}
444
445#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
446#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
447pub struct ItemUpdatedEvent {
448 pub item: ThreadItem,
450}
451
452#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
453#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
454pub struct PlanDeltaEvent {
455 pub thread_id: String,
457 pub turn_id: String,
459 pub item_id: String,
461 pub delta: String,
463}
464
465#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
466#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
467pub struct ThreadItem {
468 pub id: String,
470 #[serde(flatten)]
472 pub details: ThreadItemDetails,
473}
474
475#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
476#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
477#[serde(tag = "type", rename_all = "snake_case")]
478pub enum ThreadItemDetails {
479 AgentMessage(AgentMessageItem),
481 Plan(PlanItem),
483 Reasoning(ReasoningItem),
485 CommandExecution(Box<CommandExecutionItem>),
487 ToolInvocation(ToolInvocationItem),
489 ToolOutput(ToolOutputItem),
491 FileChange(Box<FileChangeItem>),
493 McpToolCall(McpToolCallItem),
495 WebSearch(WebSearchItem),
497 Harness(HarnessEventItem),
499 Error(ErrorItem),
501}
502
503#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
504#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
505pub struct AgentMessageItem {
506 pub text: String,
508}
509
510#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
511#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
512pub struct PlanItem {
513 pub text: String,
515}
516
517#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
518#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
519pub struct ReasoningItem {
520 pub text: String,
522 #[serde(skip_serializing_if = "Option::is_none")]
524 pub stage: Option<String>,
525}
526
527#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
528#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
529#[serde(rename_all = "snake_case")]
530pub enum CommandExecutionStatus {
531 #[default]
533 Completed,
534 Failed,
536 InProgress,
538}
539
540#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
541#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
542pub struct CommandExecutionItem {
543 pub command: String,
545 #[serde(skip_serializing_if = "Option::is_none")]
547 pub arguments: Option<Value>,
548 #[serde(default)]
550 pub aggregated_output: String,
551 #[serde(skip_serializing_if = "Option::is_none")]
553 pub exit_code: Option<i32>,
554 pub status: CommandExecutionStatus,
556}
557
558#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
559#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
560#[serde(rename_all = "snake_case")]
561pub enum ToolCallStatus {
562 #[default]
564 Completed,
565 Failed,
567 InProgress,
569}
570
571#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
572#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
573pub struct ToolInvocationItem {
574 pub tool_name: String,
576 #[serde(skip_serializing_if = "Option::is_none")]
578 pub arguments: Option<Value>,
579 #[serde(skip_serializing_if = "Option::is_none")]
581 pub tool_call_id: Option<String>,
582 pub status: ToolCallStatus,
584}
585
586#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
587#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
588pub struct ToolOutputItem {
589 pub call_id: String,
591 #[serde(skip_serializing_if = "Option::is_none")]
593 pub tool_call_id: Option<String>,
594 #[serde(skip_serializing_if = "Option::is_none")]
596 pub spool_path: Option<String>,
597 #[serde(default)]
599 pub output: String,
600 #[serde(skip_serializing_if = "Option::is_none")]
602 pub exit_code: Option<i32>,
603 pub status: ToolCallStatus,
605}
606
607#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
608#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
609pub struct FileChangeItem {
610 pub changes: Vec<FileUpdateChange>,
612 pub status: PatchApplyStatus,
614}
615
616#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
617#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
618pub struct FileUpdateChange {
619 pub path: String,
621 pub kind: PatchChangeKind,
623}
624
625#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
626#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
627#[serde(rename_all = "snake_case")]
628pub enum PatchApplyStatus {
629 Completed,
631 Failed,
633}
634
635#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
636#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
637#[serde(rename_all = "snake_case")]
638pub enum PatchChangeKind {
639 Add,
641 Delete,
643 Update,
645}
646
647#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
648#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
649pub struct McpToolCallItem {
650 pub tool_name: String,
652 #[serde(skip_serializing_if = "Option::is_none")]
654 pub arguments: Option<Value>,
655 #[serde(skip_serializing_if = "Option::is_none")]
657 pub result: Option<String>,
658 #[serde(skip_serializing_if = "Option::is_none")]
660 pub status: Option<McpToolCallStatus>,
661}
662
663#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
664#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
665#[serde(rename_all = "snake_case")]
666pub enum McpToolCallStatus {
667 Started,
669 Completed,
671 Failed,
673}
674
675#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
676#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
677pub struct WebSearchItem {
678 pub query: String,
680 #[serde(skip_serializing_if = "Option::is_none")]
682 pub provider: Option<String>,
683 #[serde(skip_serializing_if = "Option::is_none")]
685 pub results: Option<Vec<String>>,
686}
687
688#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
689#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
690#[serde(rename_all = "snake_case")]
691pub enum HarnessEventKind {
692 PlanningStarted,
693 PlanningCompleted,
694 ContinuationStarted,
695 ContinuationSkipped,
696 BlockedHandoffWritten,
697 EvaluationStarted,
698 EvaluationPassed,
699 EvaluationFailed,
700 RevisionStarted,
701 VerificationStarted,
702 VerificationPassed,
703 VerificationFailed,
704}
705
706#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
707#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
708pub struct HarnessEventItem {
709 pub event: HarnessEventKind,
711 #[serde(skip_serializing_if = "Option::is_none")]
713 pub message: Option<String>,
714 #[serde(skip_serializing_if = "Option::is_none")]
716 pub command: Option<String>,
717 #[serde(skip_serializing_if = "Option::is_none")]
719 pub path: Option<String>,
720 #[serde(skip_serializing_if = "Option::is_none")]
722 pub exit_code: Option<i32>,
723}
724
725#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
726#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
727pub struct ErrorItem {
728 pub message: String,
730}
731
732#[cfg(test)]
733mod tests {
734 use super::*;
735 use std::error::Error;
736
737 #[test]
738 fn thread_event_round_trip() -> Result<(), Box<dyn Error>> {
739 let event = ThreadEvent::TurnCompleted(TurnCompletedEvent {
740 usage: Usage {
741 input_tokens: 1,
742 cached_input_tokens: 2,
743 cache_creation_tokens: 0,
744 output_tokens: 3,
745 },
746 });
747
748 let json = serde_json::to_string(&event)?;
749 let restored: ThreadEvent = serde_json::from_str(&json)?;
750
751 assert_eq!(restored, event);
752 Ok(())
753 }
754
755 #[test]
756 fn versioned_event_wraps_schema_version() {
757 let event = ThreadEvent::ThreadStarted(ThreadStartedEvent {
758 thread_id: "abc".to_string(),
759 });
760
761 let versioned = VersionedThreadEvent::new(event.clone());
762
763 assert_eq!(versioned.schema_version, EVENT_SCHEMA_VERSION);
764 assert_eq!(versioned.event, event);
765 assert_eq!(versioned.into_event(), event);
766 }
767
768 #[cfg(feature = "serde-json")]
769 #[test]
770 fn versioned_json_round_trip() -> Result<(), Box<dyn Error>> {
771 let event = ThreadEvent::ItemCompleted(ItemCompletedEvent {
772 item: ThreadItem {
773 id: "item-1".to_string(),
774 details: ThreadItemDetails::AgentMessage(AgentMessageItem {
775 text: "hello".to_string(),
776 }),
777 },
778 });
779
780 let payload = json::versioned_to_string(&event)?;
781 let restored = json::versioned_from_str(&payload)?;
782
783 assert_eq!(restored.schema_version, EVENT_SCHEMA_VERSION);
784 assert_eq!(restored.event, event);
785 Ok(())
786 }
787
788 #[test]
789 fn tool_invocation_round_trip() -> Result<(), Box<dyn Error>> {
790 let event = ThreadEvent::ItemCompleted(ItemCompletedEvent {
791 item: ThreadItem {
792 id: "tool_1".to_string(),
793 details: ThreadItemDetails::ToolInvocation(ToolInvocationItem {
794 tool_name: "read_file".to_string(),
795 arguments: Some(serde_json::json!({ "path": "README.md" })),
796 tool_call_id: Some("tool_call_0".to_string()),
797 status: ToolCallStatus::Completed,
798 }),
799 },
800 });
801
802 let json = serde_json::to_string(&event)?;
803 let restored: ThreadEvent = serde_json::from_str(&json)?;
804
805 assert_eq!(restored, event);
806 Ok(())
807 }
808
809 #[test]
810 fn tool_output_round_trip_preserves_raw_tool_call_id() -> Result<(), Box<dyn Error>> {
811 let event = ThreadEvent::ItemCompleted(ItemCompletedEvent {
812 item: ThreadItem {
813 id: "tool_1:output".to_string(),
814 details: ThreadItemDetails::ToolOutput(ToolOutputItem {
815 call_id: "tool_1".to_string(),
816 tool_call_id: Some("tool_call_0".to_string()),
817 spool_path: None,
818 output: "done".to_string(),
819 exit_code: Some(0),
820 status: ToolCallStatus::Completed,
821 }),
822 },
823 });
824
825 let json = serde_json::to_string(&event)?;
826 let restored: ThreadEvent = serde_json::from_str(&json)?;
827
828 assert_eq!(restored, event);
829 Ok(())
830 }
831
832 #[test]
833 fn harness_item_round_trip() -> Result<(), Box<dyn Error>> {
834 let event = ThreadEvent::ItemCompleted(ItemCompletedEvent {
835 item: ThreadItem {
836 id: "harness_1".to_string(),
837 details: ThreadItemDetails::Harness(HarnessEventItem {
838 event: HarnessEventKind::VerificationFailed,
839 message: Some("cargo check failed".to_string()),
840 command: Some("cargo check".to_string()),
841 path: None,
842 exit_code: Some(101),
843 }),
844 },
845 });
846
847 let json = serde_json::to_string(&event)?;
848 let restored: ThreadEvent = serde_json::from_str(&json)?;
849
850 assert_eq!(restored, event);
851 Ok(())
852 }
853
854 #[test]
855 fn thread_completed_round_trip() -> Result<(), Box<dyn Error>> {
856 let event = ThreadEvent::ThreadCompleted(ThreadCompletedEvent {
857 thread_id: "thread-1".to_string(),
858 session_id: "session-1".to_string(),
859 subtype: ThreadCompletionSubtype::ErrorMaxBudgetUsd,
860 outcome_code: "budget_limit_reached".to_string(),
861 result: None,
862 stop_reason: Some("max_tokens".to_string()),
863 usage: Usage {
864 input_tokens: 10,
865 cached_input_tokens: 4,
866 cache_creation_tokens: 2,
867 output_tokens: 5,
868 },
869 total_cost_usd: serde_json::Number::from_f64(1.25),
870 num_turns: 3,
871 });
872
873 let json = serde_json::to_string(&event)?;
874 let restored: ThreadEvent = serde_json::from_str(&json)?;
875
876 assert_eq!(restored, event);
877 Ok(())
878 }
879
880 #[test]
881 fn compact_boundary_round_trip() -> Result<(), Box<dyn Error>> {
882 let event = ThreadEvent::ThreadCompactBoundary(ThreadCompactBoundaryEvent {
883 thread_id: "thread-1".to_string(),
884 trigger: CompactionTrigger::Recovery,
885 mode: CompactionMode::Provider,
886 original_message_count: 12,
887 compacted_message_count: 5,
888 history_artifact_path: Some("/tmp/history.jsonl".to_string()),
889 });
890
891 let json = serde_json::to_string(&event)?;
892 let restored: ThreadEvent = serde_json::from_str(&json)?;
893
894 assert_eq!(restored, event);
895 Ok(())
896 }
897}