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 diagnostic;
25pub use diagnostic::{Diagnostic, DiagnosticAction, DiagnosticSeverity};
26pub mod matrix;
27pub mod planning;
28pub mod trace;
29
30pub const EVENT_SCHEMA_VERSION: &str = "0.20.0";
32
33#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
36#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
37pub struct VersionedThreadEvent {
38 schema_version: String,
40 event: ThreadEvent,
42}
43
44impl VersionedThreadEvent {
45 pub fn new(event: ThreadEvent) -> Self {
48 Self {
49 schema_version: EVENT_SCHEMA_VERSION.to_string(),
50 event,
51 }
52 }
53
54 pub fn into_event(self) -> ThreadEvent {
56 self.event
57 }
58}
59
60impl From<ThreadEvent> for VersionedThreadEvent {
61 fn from(event: ThreadEvent) -> Self {
62 Self::new(event)
63 }
64}
65
66pub trait EventEmitter {
68 fn emit(&mut self, event: &ThreadEvent);
70}
71
72impl<F> EventEmitter for F
73where
74 F: FnMut(&ThreadEvent),
75{
76 fn emit(&mut self, event: &ThreadEvent) {
77 self(event);
78 }
79}
80
81#[cfg(feature = "serde-json")]
83pub(crate) mod json {
84 use super::{ThreadEvent, VersionedThreadEvent};
85
86 pub fn to_value(event: &ThreadEvent) -> serde_json::Result<serde_json::Value> {
88 serde_json::to_value(event)
89 }
90
91 pub(crate) fn to_string(event: &ThreadEvent) -> serde_json::Result<String> {
93 serde_json::to_string(event)
94 }
95
96 pub fn from_str(payload: &str) -> serde_json::Result<ThreadEvent> {
98 serde_json::from_str(payload)
99 }
100
101 pub(crate) fn versioned_to_string(event: &ThreadEvent) -> serde_json::Result<String> {
103 serde_json::to_string(&VersionedThreadEvent::new(event.clone()))
104 }
105
106 pub(crate) fn versioned_from_str(payload: &str) -> serde_json::Result<VersionedThreadEvent> {
108 serde_json::from_str(payload)
109 }
110}
111
112#[cfg(feature = "telemetry-log")]
113mod log_support {
114 use log::Level;
115
116 use super::{EventEmitter, ThreadEvent, json};
117
118 #[derive(Debug, Clone)]
120 pub struct LogEmitter {
121 level: Level,
122 }
123
124 impl LogEmitter {
125 pub fn new(level: Level) -> Self {
127 Self { level }
128 }
129 }
130
131 impl Default for LogEmitter {
132 fn default() -> Self {
133 Self { level: Level::Info }
134 }
135 }
136
137 impl EventEmitter for LogEmitter {
138 fn emit(&mut self, event: &ThreadEvent) {
139 if log::log_enabled!(self.level) {
140 match json::to_string(event) {
141 Ok(serialized) => log::log!(self.level, "{serialized}"),
142 Err(err) => log::log!(self.level, "failed to serialize vtcode exec event for logging: {err}"),
143 }
144 }
145 }
146 }
147
148 pub use LogEmitter as PublicLogEmitter;
149}
150
151#[cfg(feature = "telemetry-log")]
152pub use log_support::PublicLogEmitter as LogEmitter;
153
154#[cfg(feature = "telemetry-tracing")]
155mod tracing_support {
156 use tracing::Level;
157
158 use super::{EVENT_SCHEMA_VERSION, EventEmitter, ThreadEvent, VersionedThreadEvent};
159
160 #[derive(Debug, Clone)]
162 pub struct TracingEmitter {
163 level: Level,
164 }
165
166 impl TracingEmitter {
167 pub fn new(level: Level) -> Self {
169 Self { level }
170 }
171 }
172
173 impl Default for TracingEmitter {
174 fn default() -> Self {
175 Self { level: Level::INFO }
176 }
177 }
178
179 impl EventEmitter for TracingEmitter {
180 fn emit(&mut self, event: &ThreadEvent) {
181 match self.level {
182 Level::TRACE => tracing::event!(
183 target: "vtcode_exec_events",
184 Level::TRACE,
185 schema_version = EVENT_SCHEMA_VERSION,
186 event = ?VersionedThreadEvent::new(event.clone()),
187 "vtcode_exec_event"
188 ),
189 Level::DEBUG => tracing::event!(
190 target: "vtcode_exec_events",
191 Level::DEBUG,
192 schema_version = EVENT_SCHEMA_VERSION,
193 event = ?VersionedThreadEvent::new(event.clone()),
194 "vtcode_exec_event"
195 ),
196 Level::INFO => tracing::event!(
197 target: "vtcode_exec_events",
198 Level::INFO,
199 schema_version = EVENT_SCHEMA_VERSION,
200 event = ?VersionedThreadEvent::new(event.clone()),
201 "vtcode_exec_event"
202 ),
203 Level::WARN => tracing::event!(
204 target: "vtcode_exec_events",
205 Level::WARN,
206 schema_version = EVENT_SCHEMA_VERSION,
207 event = ?VersionedThreadEvent::new(event.clone()),
208 "vtcode_exec_event"
209 ),
210 Level::ERROR => tracing::event!(
211 target: "vtcode_exec_events",
212 Level::ERROR,
213 schema_version = EVENT_SCHEMA_VERSION,
214 event = ?VersionedThreadEvent::new(event.clone()),
215 "vtcode_exec_event"
216 ),
217 }
218 }
219 }
220
221 pub use TracingEmitter as PublicTracingEmitter;
222}
223
224#[cfg(feature = "telemetry-tracing")]
225pub use tracing_support::PublicTracingEmitter as TracingEmitter;
226
227#[cfg(feature = "telemetry-otel")]
228mod otel_support {
229 use opentelemetry::KeyValue;
230 use opentelemetry::trace::{Span, Status, Tracer};
231
232 use super::{EventEmitter, ThreadEvent, ThreadItemDetails};
233
234 pub struct OtelEmitter<T: Tracer> {
250 tracer: T,
251 }
252
253 impl<T: Tracer> OtelEmitter<T> {
254 pub fn new(tracer: T) -> Self {
255 Self { tracer }
256 }
257 }
258
259 impl<T: Tracer> EventEmitter for OtelEmitter<T> {
260 fn emit(&mut self, event: &ThreadEvent) {
261 let span_name = match event {
262 ThreadEvent::ThreadStarted(_) => "thread.started",
263 ThreadEvent::ThreadCompleted(_) => "thread.completed",
264 ThreadEvent::ContextReset(_) => "context.reset",
265 ThreadEvent::TurnStarted(_) => "turn.started",
266 ThreadEvent::TurnCompleted(_) => "turn.completed",
267 ThreadEvent::TurnFailed(_) => "turn.failed",
268 ThreadEvent::ItemStarted(_) => "item.started",
269 ThreadEvent::ItemUpdated(_) => "item.updated",
270 ThreadEvent::ItemCompleted(_) => "item.completed",
271 ThreadEvent::Error(_) => "error",
272 _ => "event",
273 };
274
275 let mut span = self.tracer.start(span_name);
276
277 match event {
278 ThreadEvent::ThreadStarted(e) => {
279 span.set_attribute(KeyValue::new("thread_id", e.thread_id.clone()));
280 }
281 ThreadEvent::ThreadCompleted(e) => {
282 if let Some(ref cost) = e.total_cost_usd {
283 span.set_attribute(KeyValue::new("total_cost_usd", cost.as_f64().unwrap_or(0.0)));
284 }
285 span.set_attribute(KeyValue::new(
286 "input_tokens",
287 i64::try_from(e.usage.input_tokens).unwrap_or(i64::MAX),
288 ));
289 span.set_attribute(KeyValue::new(
290 "output_tokens",
291 i64::try_from(e.usage.output_tokens).unwrap_or(i64::MAX),
292 ));
293 span.set_attribute(KeyValue::new("completion_subtype", e.subtype.as_str().to_string()));
294 }
295 ThreadEvent::ContextReset(e) => {
296 span.set_attribute(KeyValue::new("thread_id", e.thread_id.clone()));
297 span.set_attribute(KeyValue::new("turn_id", e.turn_id.clone()));
298 span.set_attribute(KeyValue::new("plan_preserved", e.plan_preserved));
299 span.set_attribute(KeyValue::new(
300 "previous_context_usage_percent",
301 e.previous_context_usage_percent as i64,
302 ));
303 span.set_attribute(KeyValue::new("tool_budget_reset", e.tool_budget_reset));
304 }
305 ThreadEvent::TurnCompleted(e) => {
306 span.set_attribute(KeyValue::new(
307 "turn_input_tokens",
308 i64::try_from(e.usage.input_tokens).unwrap_or(i64::MAX),
309 ));
310 span.set_attribute(KeyValue::new(
311 "turn_output_tokens",
312 i64::try_from(e.usage.output_tokens).unwrap_or(i64::MAX),
313 ));
314 }
315 ThreadEvent::ItemCompleted(e) => {
316 if let ThreadItemDetails::Harness(harness) = &e.item.details {
317 span.set_attribute(KeyValue::new("harness_event", format!("{:?}", harness.event)));
318 if let Some(ref msg) = harness.message {
319 span.set_attribute(KeyValue::new("harness_message", msg.clone()));
320 }
321 if let Some(ref path) = harness.path {
322 span.set_attribute(KeyValue::new("harness_path", path.clone()));
323 }
324 if let Some(dur) = harness.duration_ms {
325 span.set_attribute(KeyValue::new("duration_ms", i64::try_from(dur).unwrap_or(i64::MAX)));
326 }
327 let mut event_attrs = vec![KeyValue::new("event_kind", format!("{:?}", harness.event))];
328 if let Some(ref msg) = harness.message {
329 event_attrs.push(KeyValue::new("message", msg.clone()));
330 }
331 span.add_event("harness_event", event_attrs);
332 }
333 }
334 ThreadEvent::Error(e) => {
335 span.set_status(Status::Error { description: e.message.clone().into() });
336 span.set_attribute(KeyValue::new("error_message", e.message.clone()));
337 }
338 _ => {}
339 }
340
341 span.end();
342 }
343 }
344
345 pub use OtelEmitter as PublicOtelEmitter;
346}
347
348#[cfg(feature = "telemetry-otel")]
349pub use otel_support::PublicOtelEmitter as OtelEmitter;
350
351#[cfg(feature = "schema-export")]
352pub mod schema {
353 use schemars::{Schema, schema_for};
354
355 use super::{ThreadEvent, VersionedThreadEvent};
356
357 pub fn thread_event_schema() -> Schema {
359 schema_for!(ThreadEvent)
360 }
361
362 pub fn versioned_thread_event_schema() -> Schema {
364 schema_for!(VersionedThreadEvent)
365 }
366}
367
368#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
370#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
371#[serde(tag = "type")]
372pub enum ThreadEvent {
373 #[serde(rename = "matrix.updated")]
375 MatrixUpdated(Box<matrix::MatrixSnapshot>),
376 #[serde(rename = "thread.started")]
378 ThreadStarted(ThreadStartedEvent),
379 #[serde(rename = "thread.completed")]
381 ThreadCompleted(Box<ThreadCompletedEvent>),
382 #[serde(rename = "thread.compact_boundary")]
384 ThreadCompactBoundary(Box<ThreadCompactBoundaryEvent>),
385 #[serde(rename = "context.reset")]
387 ContextReset(ContextResetEvent),
388 #[serde(rename = "turn.started")]
390 TurnStarted(TurnStartedEvent),
391 #[serde(rename = "turn.completed")]
393 TurnCompleted(TurnCompletedEvent),
394 #[serde(rename = "turn.failed")]
396 TurnFailed(TurnFailedEvent),
397 #[serde(rename = "turn.blocked")]
401 TurnBlocked(Box<TurnBlockedEvent>),
402 #[serde(rename = "item.started")]
404 ItemStarted(ItemStartedEvent),
405 #[serde(rename = "item.updated")]
407 ItemUpdated(ItemUpdatedEvent),
408 #[serde(rename = "item.completed")]
410 ItemCompleted(ItemCompletedEvent),
411 #[serde(rename = "permission.requested")]
413 PermissionRequested(PermissionRequestedEvent),
414 #[serde(rename = "permission.resolved")]
416 PermissionResolved(PermissionResolvedEvent),
417 #[serde(rename = "interjected")]
419 Interjected(InterjectedEvent),
420 #[serde(rename = "plan.delta")]
422 PlanDelta(Box<PlanDeltaEvent>),
423 #[serde(rename = "plan.approval.requested")]
425 PlanApprovalRequested(PlanApprovalRequestedEvent),
426 #[serde(rename = "plan.approval.resolved")]
428 PlanApprovalResolved(PlanApprovalResolvedEvent),
429 #[serde(rename = "error")]
431 Error(ThreadErrorEvent),
432 #[serde(other)]
435 Unknown,
436}
437
438#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
439#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
440pub struct ThreadStartedEvent {
441 pub thread_id: String,
443}
444
445#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
446#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
447#[serde(rename_all = "snake_case")]
448pub enum ThreadCompletionSubtype {
449 Success,
450 ErrorMaxTurns,
451 ErrorMaxBudgetUsd,
452 ErrorDuringExecution,
453 Cancelled,
454 #[serde(other)]
456 Unknown,
457}
458
459impl ThreadCompletionSubtype {
460 pub const fn as_str(&self) -> &'static str {
461 match self {
462 Self::Success => "success",
463 Self::ErrorMaxTurns => "error_max_turns",
464 Self::ErrorMaxBudgetUsd => "error_max_budget_usd",
465 Self::ErrorDuringExecution => "error_during_execution",
466 Self::Cancelled => "cancelled",
467 Self::Unknown => "unknown",
468 }
469 }
470
471 pub const fn is_success(self) -> bool {
472 matches!(self, Self::Success)
473 }
474}
475
476#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
477#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
478#[serde(rename_all = "snake_case")]
479pub enum CompactionTrigger {
480 Manual,
481 Auto,
482 Recovery,
483 ModelSwitch,
486 #[serde(other)]
488 Unknown,
489}
490
491impl CompactionTrigger {
492 pub const fn as_str(self) -> &'static str {
493 match self {
494 Self::Manual => "manual",
495 Self::Auto => "auto",
496 Self::Recovery => "recovery",
497 Self::ModelSwitch => "model_switch",
498 Self::Unknown => "unknown",
499 }
500 }
501}
502
503#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
504#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
505#[serde(rename_all = "snake_case")]
506pub enum CompactionMode {
507 Provider,
508 Local,
509 #[serde(other)]
511 Unknown,
512}
513
514impl CompactionMode {
515 pub const fn as_str(self) -> &'static str {
516 match self {
517 Self::Provider => "provider",
518 Self::Local => "local",
519 Self::Unknown => "unknown",
520 }
521 }
522}
523
524#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
525#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
526pub struct ThreadCompletedEvent {
527 #[serde(default, skip_serializing_if = "Option::is_none")]
529 pub completed_at: Option<String>,
530 pub thread_id: String,
532 pub session_id: String,
534 pub subtype: ThreadCompletionSubtype,
536 pub outcome_code: String,
538 #[serde(skip_serializing_if = "Option::is_none")]
540 pub result: Option<String>,
541 #[serde(skip_serializing_if = "Option::is_none")]
543 pub stop_reason: Option<String>,
544 pub usage: Usage,
546 #[serde(skip_serializing_if = "Option::is_none")]
548 pub total_cost_usd: Option<serde_json::Number>,
549 pub num_turns: usize,
551}
552
553#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
554#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
555pub struct ThreadCompactBoundaryEvent {
556 pub thread_id: String,
558 pub trigger: CompactionTrigger,
560 pub mode: CompactionMode,
562 pub original_message_count: usize,
564 pub compacted_message_count: usize,
566 #[serde(skip_serializing_if = "Option::is_none")]
568 pub history_artifact_path: Option<String>,
569 #[serde(skip_serializing_if = "Option::is_none")]
571 pub previous_segment_id: Option<String>,
572 #[serde(skip_serializing_if = "Option::is_none")]
574 pub new_segment_id: Option<String>,
575 #[serde(skip_serializing_if = "Option::is_none")]
577 pub previous_prefix_hash: Option<String>,
578 #[serde(skip_serializing_if = "Option::is_none")]
580 pub new_prefix_hash: Option<String>,
581 #[serde(skip_serializing_if = "Option::is_none")]
583 pub previous_catalog_hash: Option<String>,
584 #[serde(skip_serializing_if = "Option::is_none")]
586 pub new_catalog_hash: Option<String>,
587}
588
589#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
590#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
591#[serde(rename_all = "snake_case")]
592pub enum ContextResetTrigger {
593 PlanApproval,
595 #[serde(other)]
597 Unknown,
598}
599
600#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
601#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
602pub struct ContextResetEvent {
603 pub thread_id: String,
605 pub turn_id: String,
607 pub trigger: ContextResetTrigger,
609 pub plan_preserved: bool,
611 pub previous_context_usage_percent: u8,
613 pub tool_budget_reset: bool,
615}
616
617#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
618#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
619pub struct TurnStartedEvent {
620 #[serde(skip_serializing_if = "Option::is_none")]
624 token_breakdown: Option<Box<TokenBreakdown>>,
625 #[serde(default, skip_serializing_if = "Option::is_none")]
627 pub context: Option<Box<ExecutionContext>>,
628}
629
630impl TurnStartedEvent {
631 pub fn token_breakdown(&self) -> Option<&TokenBreakdown> {
633 self.token_breakdown.as_deref()
634 }
635}
636
637#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
639#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
640#[serde(rename_all = "snake_case")]
641pub enum InputOrigin {
642 User,
643 Correction,
644 PlanApproval,
645 Continuation,
646 Retry,
647}
648
649#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
651#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
652pub struct ExecutionContext {
653 pub task_id: String,
654 pub turn_id: String,
655 pub actor_id: String,
656 #[serde(default, skip_serializing_if = "Option::is_none")]
657 pub parent_actor_id: Option<String>,
658 pub origin: InputOrigin,
659 pub timestamp: String,
660 #[serde(default, skip_serializing_if = "Option::is_none")]
661 pub goal: Option<String>,
662}
663
664#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
666#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
667#[serde(rename_all = "snake_case")]
668pub enum CommandActivity {
669 Inspection,
670 Verification,
671 Mutation,
672}
673
674#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
676#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
677pub struct ItemContext {
678 pub task_id: String,
679 pub turn_id: String,
680 pub actor_id: String,
681 #[serde(default, skip_serializing_if = "Option::is_none")]
682 pub parent_actor_id: Option<String>,
683 pub timestamp: String,
684 #[serde(default, skip_serializing_if = "Option::is_none")]
685 pub activity: Option<CommandActivity>,
686}
687
688#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
690#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
691pub struct TokenBreakdown {
692 system_prompt_tokens: u64,
694 tool_schema_tokens: u64,
696 instruction_file_tokens: u64,
698 message_history_tokens: u64,
700 cache_read_tokens: u64,
702 cache_write_tokens: u64,
704 cache_miss_tokens: u64,
706 #[serde(skip_serializing_if = "Option::is_none")]
708 subagent_bootstrap_tokens: Option<u64>,
709}
710
711pub const MAX_IN_PROGRESS_EXEC_SESSIONS: usize = 4;
715
716#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
717#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
718pub struct TurnCompletedEvent {
719 #[serde(default, skip_serializing_if = "Option::is_none")]
720 pub completed_at: Option<Box<String>>,
721 pub usage: Usage,
723 #[serde(
728 default,
729 skip_serializing_if = "Vec::is_empty",
730 deserialize_with = "deserialize_null_as_default"
731 )]
732 pub in_progress_exec_sessions: Vec<String>,
733}
734
735#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
736#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
737pub struct TurnFailedEvent {
738 #[serde(default, skip_serializing_if = "Option::is_none")]
739 pub completed_at: Option<Box<String>>,
740 pub message: String,
742 #[serde(skip_serializing_if = "Option::is_none")]
744 pub usage: Option<Usage>,
745}
746
747#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
748#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
749pub struct TurnBlockedEvent {
750 #[serde(default, skip_serializing_if = "Option::is_none")]
752 pub completed_at: Option<String>,
753 pub message: String,
755 #[serde(skip_serializing_if = "Option::is_none")]
757 pub last_tool: Option<String>,
758 #[serde(default)]
760 pub blocked_streak: usize,
761 #[serde(default)]
763 pub blocked_total: usize,
764 #[serde(default)]
766 pub consecutive_cap: usize,
767 #[serde(default)]
769 pub total_cap: usize,
770 #[serde(default)]
772 pub recovery_active: bool,
773 #[serde(skip_serializing_if = "Option::is_none")]
775 pub usage: Option<Usage>,
776}
777
778#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
779#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
780pub struct ThreadErrorEvent {
781 pub message: String,
783}
784
785#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
786#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
787pub struct Usage {
788 #[serde(default, deserialize_with = "deserialize_null_as_default")]
790 pub input_tokens: u64,
791 #[serde(default, deserialize_with = "deserialize_null_as_default")]
793 pub cached_input_tokens: u64,
794 #[serde(default, deserialize_with = "deserialize_null_as_default")]
796 pub cache_creation_tokens: u64,
797 #[serde(default, deserialize_with = "deserialize_null_as_default")]
799 pub output_tokens: u64,
800}
801
802pub fn deserialize_null_as_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
809where
810 D: serde::Deserializer<'de>,
811 T: Deserialize<'de> + Default,
812{
813 Ok(Option::<T>::deserialize(deserializer)?.unwrap_or_default())
814}
815
816impl Usage {
817 #[must_use]
822 fn uncached_input_tokens(&self) -> u64 {
823 self.input_tokens
824 .saturating_sub(self.cached_input_tokens)
825 .saturating_sub(self.cache_creation_tokens)
826 }
827
828 #[must_use]
831 pub fn cache_hit_rate(&self) -> Option<f64> {
832 if self.input_tokens == 0 {
833 return None;
834 }
835 Some(self.cached_input_tokens as f64 / self.input_tokens as f64)
836 }
837
838 #[must_use]
840 pub fn cache_summary(&self) -> String {
841 let total_input = self.input_tokens;
842 if total_input == 0 {
843 return "No input tokens recorded.".to_string();
844 }
845
846 let cached = self.cached_input_tokens;
847 let creation = self.cache_creation_tokens;
848 let uncached = self.uncached_input_tokens();
849 let rate = cached as f64 / total_input as f64 * 100.0;
850 format!(
851 "Cache: {cached} cached / {total_input} total input ({rate:.1}% hit rate), \
852 {creation} cache-creation, {uncached} uncached"
853 )
854 }
855
856 pub fn add(&mut self, other: &Usage) {
858 self.input_tokens = self.input_tokens.saturating_add(other.input_tokens);
859 self.cached_input_tokens = self.cached_input_tokens.saturating_add(other.cached_input_tokens);
860 self.cache_creation_tokens = self.cache_creation_tokens.saturating_add(other.cache_creation_tokens);
861 self.output_tokens = self.output_tokens.saturating_add(other.output_tokens);
862 }
863}
864
865#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
866#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
867pub struct ItemCompletedEvent {
868 pub item: ThreadItem,
870}
871
872#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
873#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
874pub struct ItemStartedEvent {
875 pub item: ThreadItem,
877}
878
879#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
880#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
881pub struct ItemUpdatedEvent {
882 pub item: ThreadItem,
884}
885
886#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
887#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
888pub struct PlanDeltaEvent {
889 pub thread_id: String,
891 pub turn_id: String,
893 pub item_id: String,
895 pub delta: String,
897 #[serde(default, skip_serializing_if = "Option::is_none")]
899 pub recovery: Option<Box<planning::PlanningRecoverySnapshot>>,
900}
901
902#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
903#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
904pub struct PlanApprovalRequestedEvent {
905 pub thread_id: String,
907 pub turn_id: String,
909 #[serde(skip_serializing_if = "Option::is_none")]
911 pub plan_file: Option<String>,
912}
913
914#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
915#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
916#[serde(rename_all = "snake_case")]
917pub enum PlanApprovalDecision {
918 Execute,
920 AutoAccept,
922 FreshContext,
924 Revise,
926 Cancel,
928 SwitchBuild,
930 SwitchAuto,
932 #[serde(other)]
934 Unknown,
935}
936
937#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
938#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
939pub struct PlanApprovalResolvedEvent {
940 pub thread_id: String,
942 pub turn_id: String,
944 pub decision: PlanApprovalDecision,
946 pub automatic: bool,
948}
949
950#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
951#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
952pub struct ThreadItem {
953 #[serde(default, skip_serializing_if = "Option::is_none")]
954 pub context: Option<Box<ItemContext>>,
955 pub id: String,
957 #[serde(flatten)]
959 pub details: ThreadItemDetails,
960}
961
962#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
963#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
964#[serde(tag = "type", rename_all = "snake_case")]
965pub enum ThreadItemDetails {
966 AgentMessage(AgentMessageItem),
968 Plan(PlanItem),
970 Reasoning(Box<ReasoningItem>),
972 Decision(Box<DecisionItem>),
974 CommandExecution(Box<CommandExecutionItem>),
976 ToolInvocation(Box<ToolInvocationItem>),
978 ToolOutput(Box<ToolOutputItem>),
980 FileChange(Box<FileChangeItem>),
982 McpToolCall(Box<McpToolCallItem>),
984 WebSearch(Box<WebSearchItem>),
986 Harness(Box<HarnessEventItem>),
988 Error(ErrorItem),
990}
991
992#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
993#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
994pub struct AgentMessageItem {
995 pub text: String,
997}
998
999#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1001#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1002pub struct DecisionItem {
1003 pub summary: String,
1004 pub rationale: String,
1005 #[serde(default)]
1006 pub alternatives: Vec<String>,
1007 #[serde(default)]
1008 pub evidence_ids: Vec<String>,
1009}
1010
1011#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1012#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1013pub struct PlanItem {
1014 pub text: String,
1016}
1017
1018#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1019#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1020pub struct ReasoningItem {
1021 #[serde(default, skip_serializing_if = "Option::is_none")]
1023 pub diagnostic: Option<Box<Diagnostic>>,
1024 pub text: String,
1026 #[serde(skip_serializing_if = "Option::is_none")]
1029 pub stage: Option<String>,
1030}
1031
1032#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
1033#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1034#[serde(rename_all = "snake_case")]
1035pub enum CommandExecutionStatus {
1036 #[default]
1038 Completed,
1039 Failed,
1041 InProgress,
1043}
1044
1045#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1046#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1047pub struct CommandExecutionItem {
1048 pub command: String,
1050 #[serde(skip_serializing_if = "Option::is_none")]
1052 pub arguments: Option<Value>,
1053 #[serde(default)]
1055 pub aggregated_output: String,
1056 #[serde(skip_serializing_if = "Option::is_none")]
1058 pub exit_code: Option<i32>,
1059 pub status: CommandExecutionStatus,
1061}
1062
1063#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
1064#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1065#[serde(rename_all = "snake_case")]
1066pub enum ToolCallStatus {
1067 #[default]
1069 Completed,
1070 Failed,
1072 InProgress,
1074}
1075
1076#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
1084#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1085#[serde(rename_all = "snake_case")]
1086pub enum ToolOutcome {
1087 #[default]
1089 Success,
1090 Error,
1092 PermissionRejected,
1094 PermissionCancelled,
1096 Followup,
1098 HookDenied,
1100 InvalidTool,
1102 Cancelled,
1104}
1105
1106impl ToolOutcome {
1107 #[must_use]
1108 pub const fn is_terminal(self) -> bool {
1109 !matches!(self, Self::Followup)
1110 }
1111}
1112
1113#[must_use]
1120#[allow(
1121 clippy::unreachable,
1122 reason = "Intentional compatibility, platform, or test-only suppression."
1123)]
1124pub fn tool_outcome_from_status(status: &ToolCallStatus) -> ToolOutcome {
1125 match status {
1126 ToolCallStatus::Completed => ToolOutcome::Success,
1127 ToolCallStatus::Failed => ToolOutcome::Error,
1128 ToolCallStatus::InProgress => unreachable!("InProgress status passed to completion event"),
1129 }
1130}
1131
1132#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1133#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1134pub struct ToolInvocationItem {
1135 pub tool_name: String,
1137 #[serde(skip_serializing_if = "Option::is_none")]
1139 pub arguments: Option<Value>,
1140 #[serde(skip_serializing_if = "Option::is_none")]
1142 pub tool_call_id: Option<String>,
1143 pub status: ToolCallStatus,
1145 #[serde(skip_serializing_if = "Option::is_none")]
1147 pub outcome: Option<ToolOutcome>,
1148}
1149
1150#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1151#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1152pub struct ToolOutputItem {
1153 pub call_id: String,
1155 #[serde(skip_serializing_if = "Option::is_none")]
1157 pub tool_call_id: Option<String>,
1158 #[serde(skip_serializing_if = "Option::is_none")]
1160 pub spool_path: Option<String>,
1161 #[serde(default)]
1163 pub output: String,
1164 #[serde(skip_serializing_if = "Option::is_none")]
1166 pub exit_code: Option<i32>,
1167 pub status: ToolCallStatus,
1169}
1170
1171#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1172#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1173pub struct FileChangeItem {
1174 #[serde(default, skip_serializing_if = "Option::is_none")]
1176 pub diff_incomplete: Option<bool>,
1177 pub changes: Vec<FileUpdateChange>,
1179 pub status: PatchApplyStatus,
1181 #[serde(default, skip_serializing_if = "Option::is_none")]
1186 pub unified_diff: Option<String>,
1187 #[serde(default, skip_serializing_if = "Option::is_none")]
1189 pub additions: Option<u64>,
1190 #[serde(default, skip_serializing_if = "Option::is_none")]
1192 pub deletions: Option<u64>,
1193}
1194
1195#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1196#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1197pub struct FileUpdateChange {
1198 pub path: String,
1200 pub kind: PatchChangeKind,
1202}
1203
1204#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1205#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1206#[serde(rename_all = "snake_case")]
1207pub enum PatchApplyStatus {
1208 Completed,
1210 Failed,
1212}
1213
1214#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1215#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1216#[serde(rename_all = "snake_case")]
1217pub enum PatchChangeKind {
1218 Add,
1220 Delete,
1222 Update,
1224}
1225
1226#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1227#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1228pub struct McpToolCallItem {
1229 pub tool_name: String,
1231 #[serde(skip_serializing_if = "Option::is_none")]
1233 pub arguments: Option<Value>,
1234 #[serde(skip_serializing_if = "Option::is_none")]
1236 pub result: Option<String>,
1237 #[serde(skip_serializing_if = "Option::is_none")]
1239 pub status: Option<McpToolCallStatus>,
1240}
1241
1242#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1243#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1244#[serde(rename_all = "snake_case")]
1245pub enum McpToolCallStatus {
1246 Started,
1248 Completed,
1250 Failed,
1252}
1253
1254#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1255#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1256pub struct WebSearchItem {
1257 pub query: String,
1259 #[serde(skip_serializing_if = "Option::is_none")]
1261 pub provider: Option<String>,
1262 #[serde(skip_serializing_if = "Option::is_none")]
1264 pub results: Option<Vec<String>>,
1265}
1266
1267#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1268#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1269#[serde(rename_all = "snake_case")]
1270pub enum HarnessEventKind {
1271 PlanningStarted,
1272 PlanningCompleted,
1273 ContinuationStarted,
1274 ContinuationSkipped,
1275 TurnBlocked,
1278 BlockedRecoveryStarted,
1280 BlockedRecoveryFinished,
1282 BlockedHandoffWritten,
1283 BlockedHandoffResolved,
1286 EvaluationStarted,
1287 EvaluationPassed,
1288 EvaluationFailed,
1289 RevisionStarted,
1290 EscalationTriggered,
1291 EscalationBypassed,
1292 VerificationStarted,
1293 VerificationPassed,
1294 VerificationFailed,
1295 ErrorRecovered,
1297 ToolRetryAttempted,
1299 ToolLatencyRecorded,
1301 SnapshotCreated,
1303 SnapshotRestored,
1305 SessionToolLimitIncreased,
1308 ToolLoopLimitIncreased,
1310 BackgroundSubprocessCompleted,
1312 DelegatedAgentStatus,
1314}
1315
1316#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1317#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1318#[serde(rename_all = "snake_case")]
1319pub enum PermissionDecision {
1320 Allow,
1321 Deny,
1322 Cancelled,
1323 Followup,
1324}
1325
1326#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1327#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1328pub struct PermissionRequestedEvent {
1329 pub tool_name: String,
1331 #[serde(default, skip_serializing_if = "Option::is_none")]
1333 pub tool_call_id: Option<String>,
1334}
1335
1336#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1337#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1338pub struct PermissionResolvedEvent {
1339 pub tool_name: String,
1341 #[serde(default, skip_serializing_if = "Option::is_none")]
1343 pub tool_call_id: Option<String>,
1344 pub decision: PermissionDecision,
1346 pub wait_ms: u64,
1348}
1349
1350#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1351#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1352#[serde(rename_all = "snake_case")]
1353pub enum InterjectionSource {
1354 Direct,
1355 Queue,
1356}
1357
1358#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1359#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1360#[serde(rename_all = "snake_case")]
1361pub enum RedirectKind {
1362 Interjection,
1363}
1364
1365#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1366#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1367pub struct InterjectedEvent {
1368 #[serde(default, skip_serializing_if = "Option::is_none")]
1370 pub text: Option<Box<String>>,
1371 pub source: InterjectionSource,
1373 pub image_count: u32,
1375 pub redirect_kind: RedirectKind,
1378}
1379
1380#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1381#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1382pub struct HarnessEventItem {
1383 pub event: HarnessEventKind,
1385 #[serde(skip_serializing_if = "Option::is_none")]
1387 pub message: Option<String>,
1388 #[serde(skip_serializing_if = "Option::is_none")]
1390 pub command: Option<String>,
1391 #[serde(skip_serializing_if = "Option::is_none")]
1393 pub path: Option<String>,
1394 #[serde(skip_serializing_if = "Option::is_none")]
1396 pub exit_code: Option<i32>,
1397 #[serde(skip_serializing_if = "Option::is_none")]
1399 pub attempt: Option<u32>,
1400 #[serde(skip_serializing_if = "Option::is_none")]
1402 pub error_category: Option<String>,
1403 #[serde(skip_serializing_if = "Option::is_none")]
1405 pub duration_ms: Option<u64>,
1406 #[serde(skip_serializing_if = "Option::is_none")]
1408 pub task_id: Option<String>,
1409 #[serde(skip_serializing_if = "Option::is_none")]
1411 pub session_id: Option<String>,
1412 #[serde(skip_serializing_if = "Option::is_none")]
1414 pub exec_session_id: Option<String>,
1415 #[serde(skip_serializing_if = "Option::is_none")]
1417 pub status: Option<String>,
1418 #[serde(skip_serializing_if = "Option::is_none")]
1420 pub transcript_path: Option<String>,
1421 #[serde(skip_serializing_if = "Option::is_none")]
1423 pub archive_path: Option<String>,
1424}
1425
1426#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1427#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1428pub struct ErrorItem {
1429 pub message: String,
1431}
1432
1433#[cfg(test)]
1434mod tests;