1#![allow(
2 missing_docs,
3 dead_code,
4 unused_imports,
5 reason = "Intentional compatibility, platform, or test-only suppression."
6)]
7use serde::{Deserialize, Serialize};
21use serde_json::Value;
22
23pub mod atif;
24pub mod matrix;
25pub mod trace;
26
27pub const EVENT_SCHEMA_VERSION: &str = "0.18.0";
29
30#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
33#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
34pub struct VersionedThreadEvent {
35 schema_version: String,
37 event: ThreadEvent,
39}
40
41impl VersionedThreadEvent {
42 pub fn new(event: ThreadEvent) -> Self {
45 Self {
46 schema_version: EVENT_SCHEMA_VERSION.to_string(),
47 event,
48 }
49 }
50
51 pub fn into_event(self) -> ThreadEvent {
53 self.event
54 }
55}
56
57impl From<ThreadEvent> for VersionedThreadEvent {
58 fn from(event: ThreadEvent) -> Self {
59 Self::new(event)
60 }
61}
62
63pub trait EventEmitter {
65 fn emit(&mut self, event: &ThreadEvent);
67}
68
69impl<F> EventEmitter for F
70where
71 F: FnMut(&ThreadEvent),
72{
73 fn emit(&mut self, event: &ThreadEvent) {
74 self(event);
75 }
76}
77
78#[cfg(feature = "serde-json")]
80pub(crate) mod json {
81 use super::{ThreadEvent, VersionedThreadEvent};
82
83 pub fn to_value(event: &ThreadEvent) -> serde_json::Result<serde_json::Value> {
85 serde_json::to_value(event)
86 }
87
88 pub(crate) fn to_string(event: &ThreadEvent) -> serde_json::Result<String> {
90 serde_json::to_string(event)
91 }
92
93 pub fn from_str(payload: &str) -> serde_json::Result<ThreadEvent> {
95 serde_json::from_str(payload)
96 }
97
98 pub(crate) fn versioned_to_string(event: &ThreadEvent) -> serde_json::Result<String> {
100 serde_json::to_string(&VersionedThreadEvent::new(event.clone()))
101 }
102
103 pub(crate) fn versioned_from_str(payload: &str) -> serde_json::Result<VersionedThreadEvent> {
105 serde_json::from_str(payload)
106 }
107}
108
109#[cfg(feature = "telemetry-log")]
110mod log_support {
111 use log::Level;
112
113 use super::{EventEmitter, ThreadEvent, json};
114
115 #[derive(Debug, Clone)]
117 pub struct LogEmitter {
118 level: Level,
119 }
120
121 impl LogEmitter {
122 pub fn new(level: Level) -> Self {
124 Self { level }
125 }
126 }
127
128 impl Default for LogEmitter {
129 fn default() -> Self {
130 Self { level: Level::Info }
131 }
132 }
133
134 impl EventEmitter for LogEmitter {
135 fn emit(&mut self, event: &ThreadEvent) {
136 if log::log_enabled!(self.level) {
137 match json::to_string(event) {
138 Ok(serialized) => log::log!(self.level, "{serialized}"),
139 Err(err) => log::log!(self.level, "failed to serialize vtcode exec event for logging: {err}"),
140 }
141 }
142 }
143 }
144
145 pub use LogEmitter as PublicLogEmitter;
146}
147
148#[cfg(feature = "telemetry-log")]
149pub use log_support::PublicLogEmitter as LogEmitter;
150
151#[cfg(feature = "telemetry-tracing")]
152mod tracing_support {
153 use tracing::Level;
154
155 use super::{EVENT_SCHEMA_VERSION, EventEmitter, ThreadEvent, VersionedThreadEvent};
156
157 #[derive(Debug, Clone)]
159 pub struct TracingEmitter {
160 level: Level,
161 }
162
163 impl TracingEmitter {
164 pub fn new(level: Level) -> Self {
166 Self { level }
167 }
168 }
169
170 impl Default for TracingEmitter {
171 fn default() -> Self {
172 Self { level: Level::INFO }
173 }
174 }
175
176 impl EventEmitter for TracingEmitter {
177 fn emit(&mut self, event: &ThreadEvent) {
178 match self.level {
179 Level::TRACE => tracing::event!(
180 target: "vtcode_exec_events",
181 Level::TRACE,
182 schema_version = EVENT_SCHEMA_VERSION,
183 event = ?VersionedThreadEvent::new(event.clone()),
184 "vtcode_exec_event"
185 ),
186 Level::DEBUG => tracing::event!(
187 target: "vtcode_exec_events",
188 Level::DEBUG,
189 schema_version = EVENT_SCHEMA_VERSION,
190 event = ?VersionedThreadEvent::new(event.clone()),
191 "vtcode_exec_event"
192 ),
193 Level::INFO => tracing::event!(
194 target: "vtcode_exec_events",
195 Level::INFO,
196 schema_version = EVENT_SCHEMA_VERSION,
197 event = ?VersionedThreadEvent::new(event.clone()),
198 "vtcode_exec_event"
199 ),
200 Level::WARN => tracing::event!(
201 target: "vtcode_exec_events",
202 Level::WARN,
203 schema_version = EVENT_SCHEMA_VERSION,
204 event = ?VersionedThreadEvent::new(event.clone()),
205 "vtcode_exec_event"
206 ),
207 Level::ERROR => tracing::event!(
208 target: "vtcode_exec_events",
209 Level::ERROR,
210 schema_version = EVENT_SCHEMA_VERSION,
211 event = ?VersionedThreadEvent::new(event.clone()),
212 "vtcode_exec_event"
213 ),
214 }
215 }
216 }
217
218 pub use TracingEmitter as PublicTracingEmitter;
219}
220
221#[cfg(feature = "telemetry-tracing")]
222pub use tracing_support::PublicTracingEmitter as TracingEmitter;
223
224#[cfg(feature = "telemetry-otel")]
225mod otel_support {
226 use opentelemetry::KeyValue;
227 use opentelemetry::trace::{Span, Status, Tracer};
228
229 use super::{EventEmitter, ThreadEvent, ThreadItemDetails};
230
231 pub struct OtelEmitter<T: Tracer> {
247 tracer: T,
248 }
249
250 impl<T: Tracer> OtelEmitter<T> {
251 pub fn new(tracer: T) -> Self {
252 Self { tracer }
253 }
254 }
255
256 impl<T: Tracer> EventEmitter for OtelEmitter<T> {
257 fn emit(&mut self, event: &ThreadEvent) {
258 let span_name = match event {
259 ThreadEvent::ThreadStarted(_) => "thread.started",
260 ThreadEvent::ThreadCompleted(_) => "thread.completed",
261 ThreadEvent::ContextReset(_) => "context.reset",
262 ThreadEvent::TurnStarted(_) => "turn.started",
263 ThreadEvent::TurnCompleted(_) => "turn.completed",
264 ThreadEvent::TurnFailed(_) => "turn.failed",
265 ThreadEvent::ItemStarted(_) => "item.started",
266 ThreadEvent::ItemUpdated(_) => "item.updated",
267 ThreadEvent::ItemCompleted(_) => "item.completed",
268 ThreadEvent::Error(_) => "error",
269 _ => "event",
270 };
271
272 let mut span = self.tracer.start(span_name);
273
274 match event {
275 ThreadEvent::ThreadStarted(e) => {
276 span.set_attribute(KeyValue::new("thread_id", e.thread_id.clone()));
277 }
278 ThreadEvent::ThreadCompleted(e) => {
279 if let Some(ref cost) = e.total_cost_usd {
280 span.set_attribute(KeyValue::new("total_cost_usd", cost.as_f64().unwrap_or(0.0)));
281 }
282 span.set_attribute(KeyValue::new(
283 "input_tokens",
284 i64::try_from(e.usage.input_tokens).unwrap_or(i64::MAX),
285 ));
286 span.set_attribute(KeyValue::new(
287 "output_tokens",
288 i64::try_from(e.usage.output_tokens).unwrap_or(i64::MAX),
289 ));
290 span.set_attribute(KeyValue::new("completion_subtype", e.subtype.as_str().to_string()));
291 }
292 ThreadEvent::ContextReset(e) => {
293 span.set_attribute(KeyValue::new("thread_id", e.thread_id.clone()));
294 span.set_attribute(KeyValue::new("turn_id", e.turn_id.clone()));
295 span.set_attribute(KeyValue::new("plan_preserved", e.plan_preserved));
296 span.set_attribute(KeyValue::new(
297 "previous_context_usage_percent",
298 e.previous_context_usage_percent as i64,
299 ));
300 span.set_attribute(KeyValue::new("tool_budget_reset", e.tool_budget_reset));
301 }
302 ThreadEvent::TurnCompleted(e) => {
303 span.set_attribute(KeyValue::new(
304 "turn_input_tokens",
305 i64::try_from(e.usage.input_tokens).unwrap_or(i64::MAX),
306 ));
307 span.set_attribute(KeyValue::new(
308 "turn_output_tokens",
309 i64::try_from(e.usage.output_tokens).unwrap_or(i64::MAX),
310 ));
311 }
312 ThreadEvent::ItemCompleted(e) => {
313 if let ThreadItemDetails::Harness(harness) = &e.item.details {
314 span.set_attribute(KeyValue::new("harness_event", format!("{:?}", harness.event)));
315 if let Some(ref msg) = harness.message {
316 span.set_attribute(KeyValue::new("harness_message", msg.clone()));
317 }
318 if let Some(ref path) = harness.path {
319 span.set_attribute(KeyValue::new("harness_path", path.clone()));
320 }
321 if let Some(dur) = harness.duration_ms {
322 span.set_attribute(KeyValue::new("duration_ms", i64::try_from(dur).unwrap_or(i64::MAX)));
323 }
324 let mut event_attrs = vec![KeyValue::new("event_kind", format!("{:?}", harness.event))];
325 if let Some(ref msg) = harness.message {
326 event_attrs.push(KeyValue::new("message", msg.clone()));
327 }
328 span.add_event("harness_event", event_attrs);
329 }
330 }
331 ThreadEvent::Error(e) => {
332 span.set_status(Status::Error { description: e.message.clone().into() });
333 span.set_attribute(KeyValue::new("error_message", e.message.clone()));
334 }
335 _ => {}
336 }
337
338 span.end();
339 }
340 }
341
342 pub use OtelEmitter as PublicOtelEmitter;
343}
344
345#[cfg(feature = "telemetry-otel")]
346pub use otel_support::PublicOtelEmitter as OtelEmitter;
347
348#[cfg(feature = "schema-export")]
349pub mod schema {
350 use schemars::{Schema, schema_for};
351
352 use super::{ThreadEvent, VersionedThreadEvent};
353
354 pub fn thread_event_schema() -> Schema {
356 schema_for!(ThreadEvent)
357 }
358
359 pub fn versioned_thread_event_schema() -> Schema {
361 schema_for!(VersionedThreadEvent)
362 }
363}
364
365#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
367#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
368#[serde(tag = "type")]
369pub enum ThreadEvent {
370 #[serde(rename = "matrix.updated")]
372 MatrixUpdated(Box<matrix::MatrixSnapshot>),
373 #[serde(rename = "thread.started")]
375 ThreadStarted(ThreadStartedEvent),
376 #[serde(rename = "thread.completed")]
378 ThreadCompleted(Box<ThreadCompletedEvent>),
379 #[serde(rename = "thread.compact_boundary")]
381 ThreadCompactBoundary(Box<ThreadCompactBoundaryEvent>),
382 #[serde(rename = "context.reset")]
384 ContextReset(ContextResetEvent),
385 #[serde(rename = "turn.started")]
387 TurnStarted(TurnStartedEvent),
388 #[serde(rename = "turn.completed")]
390 TurnCompleted(TurnCompletedEvent),
391 #[serde(rename = "turn.failed")]
393 TurnFailed(TurnFailedEvent),
394 #[serde(rename = "turn.blocked")]
398 TurnBlocked(Box<TurnBlockedEvent>),
399 #[serde(rename = "item.started")]
401 ItemStarted(ItemStartedEvent),
402 #[serde(rename = "item.updated")]
404 ItemUpdated(ItemUpdatedEvent),
405 #[serde(rename = "item.completed")]
407 ItemCompleted(ItemCompletedEvent),
408 #[serde(rename = "permission.requested")]
410 PermissionRequested(PermissionRequestedEvent),
411 #[serde(rename = "permission.resolved")]
413 PermissionResolved(PermissionResolvedEvent),
414 #[serde(rename = "interjected")]
416 Interjected(InterjectedEvent),
417 #[serde(rename = "plan.delta")]
419 PlanDelta(Box<PlanDeltaEvent>),
420 #[serde(rename = "plan.approval.requested")]
422 PlanApprovalRequested(PlanApprovalRequestedEvent),
423 #[serde(rename = "plan.approval.resolved")]
425 PlanApprovalResolved(PlanApprovalResolvedEvent),
426 #[serde(rename = "error")]
428 Error(ThreadErrorEvent),
429 #[serde(other)]
432 Unknown,
433}
434
435#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
436#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
437pub struct ThreadStartedEvent {
438 pub thread_id: String,
440}
441
442#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
443#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
444#[serde(rename_all = "snake_case")]
445pub enum ThreadCompletionSubtype {
446 Success,
447 ErrorMaxTurns,
448 ErrorMaxBudgetUsd,
449 ErrorDuringExecution,
450 Cancelled,
451 #[serde(other)]
453 Unknown,
454}
455
456impl ThreadCompletionSubtype {
457 pub const fn as_str(&self) -> &'static str {
458 match self {
459 Self::Success => "success",
460 Self::ErrorMaxTurns => "error_max_turns",
461 Self::ErrorMaxBudgetUsd => "error_max_budget_usd",
462 Self::ErrorDuringExecution => "error_during_execution",
463 Self::Cancelled => "cancelled",
464 Self::Unknown => "unknown",
465 }
466 }
467
468 pub const fn is_success(self) -> bool {
469 matches!(self, Self::Success)
470 }
471}
472
473#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
474#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
475#[serde(rename_all = "snake_case")]
476pub enum CompactionTrigger {
477 Manual,
478 Auto,
479 Recovery,
480 ModelSwitch,
483 #[serde(other)]
485 Unknown,
486}
487
488impl CompactionTrigger {
489 pub const fn as_str(self) -> &'static str {
490 match self {
491 Self::Manual => "manual",
492 Self::Auto => "auto",
493 Self::Recovery => "recovery",
494 Self::ModelSwitch => "model_switch",
495 Self::Unknown => "unknown",
496 }
497 }
498}
499
500#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
501#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
502#[serde(rename_all = "snake_case")]
503pub enum CompactionMode {
504 Provider,
505 Local,
506 #[serde(other)]
508 Unknown,
509}
510
511impl CompactionMode {
512 pub const fn as_str(self) -> &'static str {
513 match self {
514 Self::Provider => "provider",
515 Self::Local => "local",
516 Self::Unknown => "unknown",
517 }
518 }
519}
520
521#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
522#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
523pub struct ThreadCompletedEvent {
524 #[serde(default, skip_serializing_if = "Option::is_none")]
526 pub completed_at: Option<String>,
527 pub thread_id: String,
529 pub session_id: String,
531 pub subtype: ThreadCompletionSubtype,
533 pub outcome_code: String,
535 #[serde(skip_serializing_if = "Option::is_none")]
537 pub result: Option<String>,
538 #[serde(skip_serializing_if = "Option::is_none")]
540 pub stop_reason: Option<String>,
541 pub usage: Usage,
543 #[serde(skip_serializing_if = "Option::is_none")]
545 pub total_cost_usd: Option<serde_json::Number>,
546 pub num_turns: usize,
548}
549
550#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
551#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
552pub struct ThreadCompactBoundaryEvent {
553 pub thread_id: String,
555 pub trigger: CompactionTrigger,
557 pub mode: CompactionMode,
559 pub original_message_count: usize,
561 pub compacted_message_count: usize,
563 #[serde(skip_serializing_if = "Option::is_none")]
565 pub history_artifact_path: Option<String>,
566 #[serde(skip_serializing_if = "Option::is_none")]
568 pub previous_segment_id: Option<String>,
569 #[serde(skip_serializing_if = "Option::is_none")]
571 pub new_segment_id: Option<String>,
572 #[serde(skip_serializing_if = "Option::is_none")]
574 pub previous_prefix_hash: Option<String>,
575 #[serde(skip_serializing_if = "Option::is_none")]
577 pub new_prefix_hash: Option<String>,
578 #[serde(skip_serializing_if = "Option::is_none")]
580 pub previous_catalog_hash: Option<String>,
581 #[serde(skip_serializing_if = "Option::is_none")]
583 pub new_catalog_hash: Option<String>,
584}
585
586#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
587#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
588#[serde(rename_all = "snake_case")]
589pub enum ContextResetTrigger {
590 PlanApproval,
592 #[serde(other)]
594 Unknown,
595}
596
597#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
598#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
599pub struct ContextResetEvent {
600 pub thread_id: String,
602 pub turn_id: String,
604 pub trigger: ContextResetTrigger,
606 pub plan_preserved: bool,
608 pub previous_context_usage_percent: u8,
610 pub tool_budget_reset: bool,
612}
613
614#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
615#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
616pub struct TurnStartedEvent {
617 #[serde(skip_serializing_if = "Option::is_none")]
621 token_breakdown: Option<Box<TokenBreakdown>>,
622 #[serde(default, skip_serializing_if = "Option::is_none")]
624 pub context: Option<Box<ExecutionContext>>,
625}
626
627impl TurnStartedEvent {
628 pub fn token_breakdown(&self) -> Option<&TokenBreakdown> {
630 self.token_breakdown.as_deref()
631 }
632}
633
634#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
636#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
637#[serde(rename_all = "snake_case")]
638pub enum InputOrigin {
639 User,
640 Correction,
641 PlanApproval,
642 Continuation,
643 Retry,
644}
645
646#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
648#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
649pub struct ExecutionContext {
650 pub task_id: String,
651 pub turn_id: String,
652 pub actor_id: String,
653 #[serde(default, skip_serializing_if = "Option::is_none")]
654 pub parent_actor_id: Option<String>,
655 pub origin: InputOrigin,
656 pub timestamp: String,
657 #[serde(default, skip_serializing_if = "Option::is_none")]
658 pub goal: Option<String>,
659}
660
661#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
663#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
664#[serde(rename_all = "snake_case")]
665pub enum CommandActivity {
666 Inspection,
667 Verification,
668 Mutation,
669}
670
671#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
673#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
674pub struct ItemContext {
675 pub task_id: String,
676 pub turn_id: String,
677 pub actor_id: String,
678 #[serde(default, skip_serializing_if = "Option::is_none")]
679 pub parent_actor_id: Option<String>,
680 pub timestamp: String,
681 #[serde(default, skip_serializing_if = "Option::is_none")]
682 pub activity: Option<CommandActivity>,
683}
684
685#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
687#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
688pub struct TokenBreakdown {
689 system_prompt_tokens: u64,
691 tool_schema_tokens: u64,
693 instruction_file_tokens: u64,
695 message_history_tokens: u64,
697 cache_read_tokens: u64,
699 cache_write_tokens: u64,
701 cache_miss_tokens: u64,
703 #[serde(skip_serializing_if = "Option::is_none")]
705 subagent_bootstrap_tokens: Option<u64>,
706}
707
708pub const MAX_IN_PROGRESS_EXEC_SESSIONS: usize = 4;
712
713#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
714#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
715pub struct TurnCompletedEvent {
716 #[serde(default, skip_serializing_if = "Option::is_none")]
717 pub completed_at: Option<Box<String>>,
718 pub usage: Usage,
720 #[serde(
725 default,
726 skip_serializing_if = "Vec::is_empty",
727 deserialize_with = "deserialize_null_as_default"
728 )]
729 pub in_progress_exec_sessions: Vec<String>,
730}
731
732#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
733#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
734pub struct TurnFailedEvent {
735 #[serde(default, skip_serializing_if = "Option::is_none")]
736 pub completed_at: Option<Box<String>>,
737 pub message: String,
739 #[serde(skip_serializing_if = "Option::is_none")]
741 pub usage: Option<Usage>,
742}
743
744#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
745#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
746pub struct TurnBlockedEvent {
747 #[serde(default, skip_serializing_if = "Option::is_none")]
749 pub completed_at: Option<String>,
750 pub message: String,
752 #[serde(skip_serializing_if = "Option::is_none")]
754 pub last_tool: Option<String>,
755 #[serde(default)]
757 pub blocked_streak: usize,
758 #[serde(default)]
760 pub blocked_total: usize,
761 #[serde(default)]
763 pub consecutive_cap: usize,
764 #[serde(default)]
766 pub total_cap: usize,
767 #[serde(default)]
769 pub recovery_active: bool,
770 #[serde(skip_serializing_if = "Option::is_none")]
772 pub usage: Option<Usage>,
773}
774
775#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
776#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
777pub struct ThreadErrorEvent {
778 pub message: String,
780}
781
782#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
783#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
784pub struct Usage {
785 #[serde(default, deserialize_with = "deserialize_null_as_default")]
787 pub input_tokens: u64,
788 #[serde(default, deserialize_with = "deserialize_null_as_default")]
790 pub cached_input_tokens: u64,
791 #[serde(default, deserialize_with = "deserialize_null_as_default")]
793 pub cache_creation_tokens: u64,
794 #[serde(default, deserialize_with = "deserialize_null_as_default")]
796 pub output_tokens: u64,
797}
798
799pub fn deserialize_null_as_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
806where
807 D: serde::Deserializer<'de>,
808 T: Deserialize<'de> + Default,
809{
810 Ok(Option::<T>::deserialize(deserializer)?.unwrap_or_default())
811}
812
813impl Usage {
814 #[must_use]
819 fn uncached_input_tokens(&self) -> u64 {
820 self.input_tokens
821 .saturating_sub(self.cached_input_tokens)
822 .saturating_sub(self.cache_creation_tokens)
823 }
824
825 #[must_use]
828 pub fn cache_hit_rate(&self) -> Option<f64> {
829 if self.input_tokens == 0 {
830 return None;
831 }
832 Some(self.cached_input_tokens as f64 / self.input_tokens as f64)
833 }
834
835 #[must_use]
837 pub fn cache_summary(&self) -> String {
838 let total_input = self.input_tokens;
839 if total_input == 0 {
840 return "No input tokens recorded.".to_string();
841 }
842
843 let cached = self.cached_input_tokens;
844 let creation = self.cache_creation_tokens;
845 let uncached = self.uncached_input_tokens();
846 let rate = cached as f64 / total_input as f64 * 100.0;
847 format!(
848 "Cache: {cached} cached / {total_input} total input ({rate:.1}% hit rate), \
849 {creation} cache-creation, {uncached} uncached"
850 )
851 }
852
853 pub fn add(&mut self, other: &Usage) {
855 self.input_tokens = self.input_tokens.saturating_add(other.input_tokens);
856 self.cached_input_tokens = self.cached_input_tokens.saturating_add(other.cached_input_tokens);
857 self.cache_creation_tokens = self.cache_creation_tokens.saturating_add(other.cache_creation_tokens);
858 self.output_tokens = self.output_tokens.saturating_add(other.output_tokens);
859 }
860}
861
862#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
863#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
864pub struct ItemCompletedEvent {
865 pub item: ThreadItem,
867}
868
869#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
870#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
871pub struct ItemStartedEvent {
872 pub item: ThreadItem,
874}
875
876#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
877#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
878pub struct ItemUpdatedEvent {
879 pub item: ThreadItem,
881}
882
883#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
884#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
885pub struct PlanDeltaEvent {
886 pub thread_id: String,
888 pub turn_id: String,
890 pub item_id: String,
892 pub delta: String,
894}
895
896#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
897#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
898pub struct PlanApprovalRequestedEvent {
899 pub thread_id: String,
901 pub turn_id: String,
903 #[serde(skip_serializing_if = "Option::is_none")]
905 pub plan_file: Option<String>,
906}
907
908#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
909#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
910#[serde(rename_all = "snake_case")]
911pub enum PlanApprovalDecision {
912 Execute,
914 AutoAccept,
916 FreshContext,
918 Revise,
920 Cancel,
922 SwitchBuild,
924 SwitchAuto,
926 #[serde(other)]
928 Unknown,
929}
930
931#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
932#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
933pub struct PlanApprovalResolvedEvent {
934 pub thread_id: String,
936 pub turn_id: String,
938 pub decision: PlanApprovalDecision,
940 pub automatic: bool,
942}
943
944#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
945#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
946pub struct ThreadItem {
947 #[serde(default, skip_serializing_if = "Option::is_none")]
948 pub context: Option<Box<ItemContext>>,
949 pub id: String,
951 #[serde(flatten)]
953 pub details: ThreadItemDetails,
954}
955
956#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
957#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
958#[serde(tag = "type", rename_all = "snake_case")]
959pub enum ThreadItemDetails {
960 AgentMessage(AgentMessageItem),
962 Plan(PlanItem),
964 Reasoning(Box<ReasoningItem>),
966 Decision(Box<DecisionItem>),
968 CommandExecution(Box<CommandExecutionItem>),
970 ToolInvocation(Box<ToolInvocationItem>),
972 ToolOutput(Box<ToolOutputItem>),
974 FileChange(Box<FileChangeItem>),
976 McpToolCall(Box<McpToolCallItem>),
978 WebSearch(Box<WebSearchItem>),
980 Harness(Box<HarnessEventItem>),
982 Error(ErrorItem),
984}
985
986#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
987#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
988pub struct AgentMessageItem {
989 pub text: String,
991}
992
993#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
995#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
996pub struct DecisionItem {
997 pub summary: String,
998 pub rationale: String,
999 #[serde(default)]
1000 pub alternatives: Vec<String>,
1001 #[serde(default)]
1002 pub evidence_ids: Vec<String>,
1003}
1004
1005#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1006#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1007pub struct PlanItem {
1008 pub text: String,
1010}
1011
1012#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1013#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1014pub struct ReasoningItem {
1015 pub text: String,
1017 #[serde(skip_serializing_if = "Option::is_none")]
1020 pub stage: Option<String>,
1021}
1022
1023#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
1024#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1025#[serde(rename_all = "snake_case")]
1026pub enum CommandExecutionStatus {
1027 #[default]
1029 Completed,
1030 Failed,
1032 InProgress,
1034}
1035
1036#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1037#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1038pub struct CommandExecutionItem {
1039 pub command: String,
1041 #[serde(skip_serializing_if = "Option::is_none")]
1043 pub arguments: Option<Value>,
1044 #[serde(default)]
1046 pub aggregated_output: String,
1047 #[serde(skip_serializing_if = "Option::is_none")]
1049 pub exit_code: Option<i32>,
1050 pub status: CommandExecutionStatus,
1052}
1053
1054#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
1055#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1056#[serde(rename_all = "snake_case")]
1057pub enum ToolCallStatus {
1058 #[default]
1060 Completed,
1061 Failed,
1063 InProgress,
1065}
1066
1067#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
1075#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1076#[serde(rename_all = "snake_case")]
1077pub enum ToolOutcome {
1078 #[default]
1080 Success,
1081 Error,
1083 PermissionRejected,
1085 PermissionCancelled,
1087 Followup,
1089 HookDenied,
1091 InvalidTool,
1093 Cancelled,
1095}
1096
1097impl ToolOutcome {
1098 #[must_use]
1099 pub const fn is_terminal(self) -> bool {
1100 !matches!(self, Self::Followup)
1101 }
1102}
1103
1104#[must_use]
1111#[allow(
1112 clippy::unreachable,
1113 reason = "Intentional compatibility, platform, or test-only suppression."
1114)]
1115pub fn tool_outcome_from_status(status: &ToolCallStatus) -> ToolOutcome {
1116 match status {
1117 ToolCallStatus::Completed => ToolOutcome::Success,
1118 ToolCallStatus::Failed => ToolOutcome::Error,
1119 ToolCallStatus::InProgress => unreachable!("InProgress status passed to completion event"),
1120 }
1121}
1122
1123#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1124#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1125pub struct ToolInvocationItem {
1126 pub tool_name: String,
1128 #[serde(skip_serializing_if = "Option::is_none")]
1130 pub arguments: Option<Value>,
1131 #[serde(skip_serializing_if = "Option::is_none")]
1133 pub tool_call_id: Option<String>,
1134 pub status: ToolCallStatus,
1136 #[serde(skip_serializing_if = "Option::is_none")]
1138 pub outcome: Option<ToolOutcome>,
1139}
1140
1141#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1142#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1143pub struct ToolOutputItem {
1144 pub call_id: String,
1146 #[serde(skip_serializing_if = "Option::is_none")]
1148 pub tool_call_id: Option<String>,
1149 #[serde(skip_serializing_if = "Option::is_none")]
1151 pub spool_path: Option<String>,
1152 #[serde(default)]
1154 pub output: String,
1155 #[serde(skip_serializing_if = "Option::is_none")]
1157 pub exit_code: Option<i32>,
1158 pub status: ToolCallStatus,
1160}
1161
1162#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1163#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1164pub struct FileChangeItem {
1165 #[serde(default, skip_serializing_if = "Option::is_none")]
1167 pub diff_incomplete: Option<bool>,
1168 pub changes: Vec<FileUpdateChange>,
1170 pub status: PatchApplyStatus,
1172 #[serde(default, skip_serializing_if = "Option::is_none")]
1177 pub unified_diff: Option<String>,
1178 #[serde(default, skip_serializing_if = "Option::is_none")]
1180 pub additions: Option<u64>,
1181 #[serde(default, skip_serializing_if = "Option::is_none")]
1183 pub deletions: Option<u64>,
1184}
1185
1186#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1187#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1188pub struct FileUpdateChange {
1189 pub path: String,
1191 pub kind: PatchChangeKind,
1193}
1194
1195#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1196#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1197#[serde(rename_all = "snake_case")]
1198pub enum PatchApplyStatus {
1199 Completed,
1201 Failed,
1203}
1204
1205#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1206#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1207#[serde(rename_all = "snake_case")]
1208pub enum PatchChangeKind {
1209 Add,
1211 Delete,
1213 Update,
1215}
1216
1217#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1218#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1219pub struct McpToolCallItem {
1220 pub tool_name: String,
1222 #[serde(skip_serializing_if = "Option::is_none")]
1224 pub arguments: Option<Value>,
1225 #[serde(skip_serializing_if = "Option::is_none")]
1227 pub result: Option<String>,
1228 #[serde(skip_serializing_if = "Option::is_none")]
1230 pub status: Option<McpToolCallStatus>,
1231}
1232
1233#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1234#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1235#[serde(rename_all = "snake_case")]
1236pub enum McpToolCallStatus {
1237 Started,
1239 Completed,
1241 Failed,
1243}
1244
1245#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1246#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1247pub struct WebSearchItem {
1248 pub query: String,
1250 #[serde(skip_serializing_if = "Option::is_none")]
1252 pub provider: Option<String>,
1253 #[serde(skip_serializing_if = "Option::is_none")]
1255 pub results: Option<Vec<String>>,
1256}
1257
1258#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1259#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1260#[serde(rename_all = "snake_case")]
1261pub enum HarnessEventKind {
1262 PlanningStarted,
1263 PlanningCompleted,
1264 ContinuationStarted,
1265 ContinuationSkipped,
1266 TurnBlocked,
1269 BlockedRecoveryStarted,
1271 BlockedRecoveryFinished,
1273 BlockedHandoffWritten,
1274 BlockedHandoffResolved,
1277 EvaluationStarted,
1278 EvaluationPassed,
1279 EvaluationFailed,
1280 RevisionStarted,
1281 EscalationTriggered,
1282 EscalationBypassed,
1283 VerificationStarted,
1284 VerificationPassed,
1285 VerificationFailed,
1286 ErrorRecovered,
1288 ToolRetryAttempted,
1290 ToolLatencyRecorded,
1292 SnapshotCreated,
1294 SnapshotRestored,
1296 SessionToolLimitIncreased,
1299 ToolLoopLimitIncreased,
1301 BackgroundSubprocessCompleted,
1303 DelegatedAgentStatus,
1305}
1306
1307#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1308#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1309#[serde(rename_all = "snake_case")]
1310pub enum PermissionDecision {
1311 Allow,
1312 Deny,
1313 Cancelled,
1314 Followup,
1315}
1316
1317#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1318#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1319pub struct PermissionRequestedEvent {
1320 pub tool_name: String,
1322}
1323
1324#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1325#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1326pub struct PermissionResolvedEvent {
1327 pub tool_name: String,
1329 pub decision: PermissionDecision,
1331 pub wait_ms: u64,
1333}
1334
1335#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1336#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1337#[serde(rename_all = "snake_case")]
1338pub enum InterjectionSource {
1339 Direct,
1340 Queue,
1341}
1342
1343#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1344#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1345#[serde(rename_all = "snake_case")]
1346pub enum RedirectKind {
1347 Interjection,
1348}
1349
1350#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1351#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1352pub struct InterjectedEvent {
1353 #[serde(default, skip_serializing_if = "Option::is_none")]
1355 pub text: Option<Box<String>>,
1356 pub source: InterjectionSource,
1358 pub image_count: u32,
1360 pub redirect_kind: RedirectKind,
1363}
1364
1365#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1366#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1367pub struct HarnessEventItem {
1368 pub event: HarnessEventKind,
1370 #[serde(skip_serializing_if = "Option::is_none")]
1372 pub message: Option<String>,
1373 #[serde(skip_serializing_if = "Option::is_none")]
1375 pub command: Option<String>,
1376 #[serde(skip_serializing_if = "Option::is_none")]
1378 pub path: Option<String>,
1379 #[serde(skip_serializing_if = "Option::is_none")]
1381 pub exit_code: Option<i32>,
1382 #[serde(skip_serializing_if = "Option::is_none")]
1384 pub attempt: Option<u32>,
1385 #[serde(skip_serializing_if = "Option::is_none")]
1387 pub error_category: Option<String>,
1388 #[serde(skip_serializing_if = "Option::is_none")]
1390 pub duration_ms: Option<u64>,
1391 #[serde(skip_serializing_if = "Option::is_none")]
1393 pub task_id: Option<String>,
1394 #[serde(skip_serializing_if = "Option::is_none")]
1396 pub session_id: Option<String>,
1397 #[serde(skip_serializing_if = "Option::is_none")]
1399 pub exec_session_id: Option<String>,
1400 #[serde(skip_serializing_if = "Option::is_none")]
1402 pub status: Option<String>,
1403 #[serde(skip_serializing_if = "Option::is_none")]
1405 pub transcript_path: Option<String>,
1406 #[serde(skip_serializing_if = "Option::is_none")]
1408 pub archive_path: Option<String>,
1409}
1410
1411#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1412#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1413pub struct ErrorItem {
1414 pub message: String,
1416}
1417
1418#[cfg(test)]
1419mod tests;