1use serde::{Deserialize, Serialize};
15use serde_json::Value;
16
17pub mod atif;
18pub mod trace;
19
20pub const EVENT_SCHEMA_VERSION: &str = "0.4.0";
22
23#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
26#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
27pub struct VersionedThreadEvent {
28 pub schema_version: String,
30 pub event: ThreadEvent,
32}
33
34impl VersionedThreadEvent {
35 pub fn new(event: ThreadEvent) -> Self {
38 Self {
39 schema_version: EVENT_SCHEMA_VERSION.to_string(),
40 event,
41 }
42 }
43
44 pub fn into_event(self) -> ThreadEvent {
46 self.event
47 }
48}
49
50impl From<ThreadEvent> for VersionedThreadEvent {
51 fn from(event: ThreadEvent) -> Self {
52 Self::new(event)
53 }
54}
55
56pub trait EventEmitter {
58 fn emit(&mut self, event: &ThreadEvent);
60}
61
62impl<F> EventEmitter for F
63where
64 F: FnMut(&ThreadEvent),
65{
66 fn emit(&mut self, event: &ThreadEvent) {
67 self(event);
68 }
69}
70
71#[cfg(feature = "serde-json")]
73pub mod json {
74 use super::{ThreadEvent, VersionedThreadEvent};
75
76 pub fn to_value(event: &ThreadEvent) -> serde_json::Result<serde_json::Value> {
78 serde_json::to_value(event)
79 }
80
81 pub fn to_string(event: &ThreadEvent) -> serde_json::Result<String> {
83 serde_json::to_string(event)
84 }
85
86 pub fn from_str(payload: &str) -> serde_json::Result<ThreadEvent> {
88 serde_json::from_str(payload)
89 }
90
91 pub fn versioned_to_string(event: &ThreadEvent) -> serde_json::Result<String> {
93 serde_json::to_string(&VersionedThreadEvent::new(event.clone()))
94 }
95
96 pub fn versioned_from_str(payload: &str) -> serde_json::Result<VersionedThreadEvent> {
98 serde_json::from_str(payload)
99 }
100}
101
102#[cfg(feature = "telemetry-log")]
103mod log_support {
104 use log::Level;
105
106 use super::{EventEmitter, ThreadEvent, json};
107
108 #[derive(Debug, Clone)]
110 pub struct LogEmitter {
111 level: Level,
112 }
113
114 impl LogEmitter {
115 pub fn new(level: Level) -> Self {
117 Self { level }
118 }
119 }
120
121 impl Default for LogEmitter {
122 fn default() -> Self {
123 Self { level: Level::Info }
124 }
125 }
126
127 impl EventEmitter for LogEmitter {
128 fn emit(&mut self, event: &ThreadEvent) {
129 if log::log_enabled!(self.level) {
130 match json::to_string(event) {
131 Ok(serialized) => log::log!(self.level, "{}", serialized),
132 Err(err) => log::log!(
133 self.level,
134 "failed to serialize vtcode exec event for logging: {err}"
135 ),
136 }
137 }
138 }
139 }
140
141 pub use LogEmitter as PublicLogEmitter;
142}
143
144#[cfg(feature = "telemetry-log")]
145pub use log_support::PublicLogEmitter as LogEmitter;
146
147#[cfg(feature = "telemetry-tracing")]
148mod tracing_support {
149 use tracing::Level;
150
151 use super::{EVENT_SCHEMA_VERSION, EventEmitter, ThreadEvent, VersionedThreadEvent};
152
153 #[derive(Debug, Clone)]
155 pub struct TracingEmitter {
156 level: Level,
157 }
158
159 impl TracingEmitter {
160 pub fn new(level: Level) -> Self {
162 Self { level }
163 }
164 }
165
166 impl Default for TracingEmitter {
167 fn default() -> Self {
168 Self { level: Level::INFO }
169 }
170 }
171
172 impl EventEmitter for TracingEmitter {
173 fn emit(&mut self, event: &ThreadEvent) {
174 match self.level {
175 Level::TRACE => tracing::event!(
176 target: "vtcode_exec_events",
177 Level::TRACE,
178 schema_version = EVENT_SCHEMA_VERSION,
179 event = ?VersionedThreadEvent::new(event.clone()),
180 "vtcode_exec_event"
181 ),
182 Level::DEBUG => tracing::event!(
183 target: "vtcode_exec_events",
184 Level::DEBUG,
185 schema_version = EVENT_SCHEMA_VERSION,
186 event = ?VersionedThreadEvent::new(event.clone()),
187 "vtcode_exec_event"
188 ),
189 Level::INFO => tracing::event!(
190 target: "vtcode_exec_events",
191 Level::INFO,
192 schema_version = EVENT_SCHEMA_VERSION,
193 event = ?VersionedThreadEvent::new(event.clone()),
194 "vtcode_exec_event"
195 ),
196 Level::WARN => tracing::event!(
197 target: "vtcode_exec_events",
198 Level::WARN,
199 schema_version = EVENT_SCHEMA_VERSION,
200 event = ?VersionedThreadEvent::new(event.clone()),
201 "vtcode_exec_event"
202 ),
203 Level::ERROR => tracing::event!(
204 target: "vtcode_exec_events",
205 Level::ERROR,
206 schema_version = EVENT_SCHEMA_VERSION,
207 event = ?VersionedThreadEvent::new(event.clone()),
208 "vtcode_exec_event"
209 ),
210 }
211 }
212 }
213
214 pub use TracingEmitter as PublicTracingEmitter;
215}
216
217#[cfg(feature = "telemetry-tracing")]
218pub use tracing_support::PublicTracingEmitter as TracingEmitter;
219
220#[cfg(feature = "schema-export")]
221pub mod schema {
222 use schemars::{Schema, schema_for};
223
224 use super::{ThreadEvent, VersionedThreadEvent};
225
226 pub fn thread_event_schema() -> Schema {
228 schema_for!(ThreadEvent)
229 }
230
231 pub fn versioned_thread_event_schema() -> Schema {
233 schema_for!(VersionedThreadEvent)
234 }
235}
236
237#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
239#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
240#[serde(tag = "type")]
241pub enum ThreadEvent {
242 #[serde(rename = "thread.started")]
244 ThreadStarted(ThreadStartedEvent),
245 #[serde(rename = "thread.completed")]
247 ThreadCompleted(ThreadCompletedEvent),
248 #[serde(rename = "thread.compact_boundary")]
250 ThreadCompactBoundary(ThreadCompactBoundaryEvent),
251 #[serde(rename = "turn.started")]
253 TurnStarted(TurnStartedEvent),
254 #[serde(rename = "turn.completed")]
256 TurnCompleted(TurnCompletedEvent),
257 #[serde(rename = "turn.failed")]
259 TurnFailed(TurnFailedEvent),
260 #[serde(rename = "item.started")]
262 ItemStarted(ItemStartedEvent),
263 #[serde(rename = "item.updated")]
265 ItemUpdated(ItemUpdatedEvent),
266 #[serde(rename = "item.completed")]
268 ItemCompleted(ItemCompletedEvent),
269 #[serde(rename = "plan.delta")]
271 PlanDelta(PlanDeltaEvent),
272 #[serde(rename = "error")]
274 Error(ThreadErrorEvent),
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
278#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
279pub struct ThreadStartedEvent {
280 pub thread_id: String,
282}
283
284#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
285#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
286#[serde(rename_all = "snake_case")]
287pub enum ThreadCompletionSubtype {
288 Success,
289 ErrorMaxTurns,
290 ErrorMaxBudgetUsd,
291 ErrorDuringExecution,
292 Cancelled,
293}
294
295impl ThreadCompletionSubtype {
296 pub const fn as_str(&self) -> &'static str {
297 match self {
298 Self::Success => "success",
299 Self::ErrorMaxTurns => "error_max_turns",
300 Self::ErrorMaxBudgetUsd => "error_max_budget_usd",
301 Self::ErrorDuringExecution => "error_during_execution",
302 Self::Cancelled => "cancelled",
303 }
304 }
305
306 pub const fn is_success(self) -> bool {
307 matches!(self, Self::Success)
308 }
309}
310
311#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
312#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
313#[serde(rename_all = "snake_case")]
314pub enum CompactionTrigger {
315 Manual,
316 Auto,
317 Recovery,
318}
319
320impl CompactionTrigger {
321 pub const fn as_str(self) -> &'static str {
322 match self {
323 Self::Manual => "manual",
324 Self::Auto => "auto",
325 Self::Recovery => "recovery",
326 }
327 }
328}
329
330#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
331#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
332#[serde(rename_all = "snake_case")]
333pub enum CompactionMode {
334 Provider,
335 Local,
336}
337
338impl CompactionMode {
339 pub const fn as_str(self) -> &'static str {
340 match self {
341 Self::Provider => "provider",
342 Self::Local => "local",
343 }
344 }
345}
346
347#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
348#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
349pub struct ThreadCompletedEvent {
350 pub thread_id: String,
352 pub session_id: String,
354 pub subtype: ThreadCompletionSubtype,
356 pub outcome_code: String,
358 #[serde(skip_serializing_if = "Option::is_none")]
360 pub result: Option<String>,
361 #[serde(skip_serializing_if = "Option::is_none")]
363 pub stop_reason: Option<String>,
364 pub usage: Usage,
366 #[serde(skip_serializing_if = "Option::is_none")]
368 pub total_cost_usd: Option<serde_json::Number>,
369 pub num_turns: usize,
371}
372
373#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
374#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
375pub struct ThreadCompactBoundaryEvent {
376 pub thread_id: String,
378 pub trigger: CompactionTrigger,
380 pub mode: CompactionMode,
382 pub original_message_count: usize,
384 pub compacted_message_count: usize,
386 #[serde(skip_serializing_if = "Option::is_none")]
388 pub history_artifact_path: Option<String>,
389}
390
391#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
392#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
393pub struct TurnStartedEvent {}
394
395#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
396#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
397pub struct TurnCompletedEvent {
398 pub usage: Usage,
400}
401
402#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
403#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
404pub struct TurnFailedEvent {
405 pub message: String,
407 #[serde(skip_serializing_if = "Option::is_none")]
409 pub usage: Option<Usage>,
410}
411
412#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
413#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
414pub struct ThreadErrorEvent {
415 pub message: String,
417}
418
419#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
420#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
421pub struct Usage {
422 pub input_tokens: u64,
424 pub cached_input_tokens: u64,
426 pub cache_creation_tokens: u64,
428 pub output_tokens: u64,
430}
431
432#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
433#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
434pub struct ItemCompletedEvent {
435 pub item: ThreadItem,
437}
438
439#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
440#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
441pub struct ItemStartedEvent {
442 pub item: ThreadItem,
444}
445
446#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
447#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
448pub struct ItemUpdatedEvent {
449 pub item: ThreadItem,
451}
452
453#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
454#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
455pub struct PlanDeltaEvent {
456 pub thread_id: String,
458 pub turn_id: String,
460 pub item_id: String,
462 pub delta: String,
464}
465
466#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
467#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
468pub struct ThreadItem {
469 pub id: String,
471 #[serde(flatten)]
473 pub details: ThreadItemDetails,
474}
475
476#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
477#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
478#[serde(tag = "type", rename_all = "snake_case")]
479pub enum ThreadItemDetails {
480 AgentMessage(AgentMessageItem),
482 Plan(PlanItem),
484 Reasoning(ReasoningItem),
486 CommandExecution(Box<CommandExecutionItem>),
488 ToolInvocation(ToolInvocationItem),
490 ToolOutput(ToolOutputItem),
492 FileChange(Box<FileChangeItem>),
494 McpToolCall(McpToolCallItem),
496 WebSearch(WebSearchItem),
498 Harness(HarnessEventItem),
500 Error(ErrorItem),
502}
503
504#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
505#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
506pub struct AgentMessageItem {
507 pub text: String,
509}
510
511#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
512#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
513pub struct PlanItem {
514 pub text: String,
516}
517
518#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
519#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
520pub struct ReasoningItem {
521 pub text: String,
523 #[serde(skip_serializing_if = "Option::is_none")]
525 pub stage: Option<String>,
526}
527
528#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
529#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
530#[serde(rename_all = "snake_case")]
531pub enum CommandExecutionStatus {
532 #[default]
534 Completed,
535 Failed,
537 InProgress,
539}
540
541#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
542#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
543pub struct CommandExecutionItem {
544 pub command: String,
546 #[serde(skip_serializing_if = "Option::is_none")]
548 pub arguments: Option<Value>,
549 #[serde(default)]
551 pub aggregated_output: String,
552 #[serde(skip_serializing_if = "Option::is_none")]
554 pub exit_code: Option<i32>,
555 pub status: CommandExecutionStatus,
557}
558
559#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
560#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
561#[serde(rename_all = "snake_case")]
562pub enum ToolCallStatus {
563 #[default]
565 Completed,
566 Failed,
568 InProgress,
570}
571
572#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
573#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
574pub struct ToolInvocationItem {
575 pub tool_name: String,
577 #[serde(skip_serializing_if = "Option::is_none")]
579 pub arguments: Option<Value>,
580 #[serde(skip_serializing_if = "Option::is_none")]
582 pub tool_call_id: Option<String>,
583 pub status: ToolCallStatus,
585}
586
587#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
588#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
589pub struct ToolOutputItem {
590 pub call_id: String,
592 #[serde(skip_serializing_if = "Option::is_none")]
594 pub tool_call_id: Option<String>,
595 #[serde(skip_serializing_if = "Option::is_none")]
597 pub spool_path: Option<String>,
598 #[serde(default)]
600 pub output: String,
601 #[serde(skip_serializing_if = "Option::is_none")]
603 pub exit_code: Option<i32>,
604 pub status: ToolCallStatus,
606}
607
608#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
609#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
610pub struct FileChangeItem {
611 pub changes: Vec<FileUpdateChange>,
613 pub status: PatchApplyStatus,
615}
616
617#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
618#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
619pub struct FileUpdateChange {
620 pub path: String,
622 pub kind: PatchChangeKind,
624}
625
626#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
627#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
628#[serde(rename_all = "snake_case")]
629pub enum PatchApplyStatus {
630 Completed,
632 Failed,
634}
635
636#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
637#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
638#[serde(rename_all = "snake_case")]
639pub enum PatchChangeKind {
640 Add,
642 Delete,
644 Update,
646}
647
648#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
649#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
650pub struct McpToolCallItem {
651 pub tool_name: String,
653 #[serde(skip_serializing_if = "Option::is_none")]
655 pub arguments: Option<Value>,
656 #[serde(skip_serializing_if = "Option::is_none")]
658 pub result: Option<String>,
659 #[serde(skip_serializing_if = "Option::is_none")]
661 pub status: Option<McpToolCallStatus>,
662}
663
664#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
665#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
666#[serde(rename_all = "snake_case")]
667pub enum McpToolCallStatus {
668 Started,
670 Completed,
672 Failed,
674}
675
676#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
677#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
678pub struct WebSearchItem {
679 pub query: String,
681 #[serde(skip_serializing_if = "Option::is_none")]
683 pub provider: Option<String>,
684 #[serde(skip_serializing_if = "Option::is_none")]
686 pub results: Option<Vec<String>>,
687}
688
689#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
690#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
691#[serde(rename_all = "snake_case")]
692pub enum HarnessEventKind {
693 PlanningStarted,
694 PlanningCompleted,
695 ContinuationStarted,
696 ContinuationSkipped,
697 BlockedHandoffWritten,
698 EvaluationStarted,
699 EvaluationPassed,
700 EvaluationFailed,
701 RevisionStarted,
702 VerificationStarted,
703 VerificationPassed,
704 VerificationFailed,
705}
706
707#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
708#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
709pub struct HarnessEventItem {
710 pub event: HarnessEventKind,
712 #[serde(skip_serializing_if = "Option::is_none")]
714 pub message: Option<String>,
715 #[serde(skip_serializing_if = "Option::is_none")]
717 pub command: Option<String>,
718 #[serde(skip_serializing_if = "Option::is_none")]
720 pub path: Option<String>,
721 #[serde(skip_serializing_if = "Option::is_none")]
723 pub exit_code: Option<i32>,
724}
725
726#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
727#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
728pub struct ErrorItem {
729 pub message: String,
731}
732
733#[cfg(test)]
734mod tests {
735 use super::*;
736 use std::error::Error;
737
738 #[test]
739 fn thread_event_round_trip() -> Result<(), Box<dyn Error>> {
740 let event = ThreadEvent::TurnCompleted(TurnCompletedEvent {
741 usage: Usage {
742 input_tokens: 1,
743 cached_input_tokens: 2,
744 cache_creation_tokens: 0,
745 output_tokens: 3,
746 },
747 });
748
749 let json = serde_json::to_string(&event)?;
750 let restored: ThreadEvent = serde_json::from_str(&json)?;
751
752 assert_eq!(restored, event);
753 Ok(())
754 }
755
756 #[test]
757 fn versioned_event_wraps_schema_version() {
758 let event = ThreadEvent::ThreadStarted(ThreadStartedEvent {
759 thread_id: "abc".to_string(),
760 });
761
762 let versioned = VersionedThreadEvent::new(event.clone());
763
764 assert_eq!(versioned.schema_version, EVENT_SCHEMA_VERSION);
765 assert_eq!(versioned.event, event);
766 assert_eq!(versioned.into_event(), event);
767 }
768
769 #[cfg(feature = "serde-json")]
770 #[test]
771 fn versioned_json_round_trip() -> Result<(), Box<dyn Error>> {
772 let event = ThreadEvent::ItemCompleted(ItemCompletedEvent {
773 item: ThreadItem {
774 id: "item-1".to_string(),
775 details: ThreadItemDetails::AgentMessage(AgentMessageItem {
776 text: "hello".to_string(),
777 }),
778 },
779 });
780
781 let payload = json::versioned_to_string(&event)?;
782 let restored = json::versioned_from_str(&payload)?;
783
784 assert_eq!(restored.schema_version, EVENT_SCHEMA_VERSION);
785 assert_eq!(restored.event, event);
786 Ok(())
787 }
788
789 #[test]
790 fn tool_invocation_round_trip() -> Result<(), Box<dyn Error>> {
791 let event = ThreadEvent::ItemCompleted(ItemCompletedEvent {
792 item: ThreadItem {
793 id: "tool_1".to_string(),
794 details: ThreadItemDetails::ToolInvocation(ToolInvocationItem {
795 tool_name: "read_file".to_string(),
796 arguments: Some(serde_json::json!({ "path": "README.md" })),
797 tool_call_id: Some("tool_call_0".to_string()),
798 status: ToolCallStatus::Completed,
799 }),
800 },
801 });
802
803 let json = serde_json::to_string(&event)?;
804 let restored: ThreadEvent = serde_json::from_str(&json)?;
805
806 assert_eq!(restored, event);
807 Ok(())
808 }
809
810 #[test]
811 fn tool_output_round_trip_preserves_raw_tool_call_id() -> Result<(), Box<dyn Error>> {
812 let event = ThreadEvent::ItemCompleted(ItemCompletedEvent {
813 item: ThreadItem {
814 id: "tool_1:output".to_string(),
815 details: ThreadItemDetails::ToolOutput(ToolOutputItem {
816 call_id: "tool_1".to_string(),
817 tool_call_id: Some("tool_call_0".to_string()),
818 spool_path: None,
819 output: "done".to_string(),
820 exit_code: Some(0),
821 status: ToolCallStatus::Completed,
822 }),
823 },
824 });
825
826 let json = serde_json::to_string(&event)?;
827 let restored: ThreadEvent = serde_json::from_str(&json)?;
828
829 assert_eq!(restored, event);
830 Ok(())
831 }
832
833 #[test]
834 fn harness_item_round_trip() -> Result<(), Box<dyn Error>> {
835 let event = ThreadEvent::ItemCompleted(ItemCompletedEvent {
836 item: ThreadItem {
837 id: "harness_1".to_string(),
838 details: ThreadItemDetails::Harness(HarnessEventItem {
839 event: HarnessEventKind::VerificationFailed,
840 message: Some("cargo check failed".to_string()),
841 command: Some("cargo check".to_string()),
842 path: None,
843 exit_code: Some(101),
844 }),
845 },
846 });
847
848 let json = serde_json::to_string(&event)?;
849 let restored: ThreadEvent = serde_json::from_str(&json)?;
850
851 assert_eq!(restored, event);
852 Ok(())
853 }
854
855 #[test]
856 fn thread_completed_round_trip() -> Result<(), Box<dyn Error>> {
857 let event = ThreadEvent::ThreadCompleted(ThreadCompletedEvent {
858 thread_id: "thread-1".to_string(),
859 session_id: "session-1".to_string(),
860 subtype: ThreadCompletionSubtype::ErrorMaxBudgetUsd,
861 outcome_code: "budget_limit_reached".to_string(),
862 result: None,
863 stop_reason: Some("max_tokens".to_string()),
864 usage: Usage {
865 input_tokens: 10,
866 cached_input_tokens: 4,
867 cache_creation_tokens: 2,
868 output_tokens: 5,
869 },
870 total_cost_usd: serde_json::Number::from_f64(1.25),
871 num_turns: 3,
872 });
873
874 let json = serde_json::to_string(&event)?;
875 let restored: ThreadEvent = serde_json::from_str(&json)?;
876
877 assert_eq!(restored, event);
878 Ok(())
879 }
880
881 #[test]
882 fn compact_boundary_round_trip() -> Result<(), Box<dyn Error>> {
883 let event = ThreadEvent::ThreadCompactBoundary(ThreadCompactBoundaryEvent {
884 thread_id: "thread-1".to_string(),
885 trigger: CompactionTrigger::Recovery,
886 mode: CompactionMode::Provider,
887 original_message_count: 12,
888 compacted_message_count: 5,
889 history_artifact_path: Some("/tmp/history.jsonl".to_string()),
890 });
891
892 let json = serde_json::to_string(&event)?;
893 let restored: ThreadEvent = serde_json::from_str(&json)?;
894
895 assert_eq!(restored, event);
896 Ok(())
897 }
898}