Skip to main content

vtcode_exec_events/
lib.rs

1#![allow(
2    missing_docs,
3    dead_code,
4    unused_imports,
5    reason = "Intentional compatibility, platform, or test-only suppression."
6)]
7//! Structured execution telemetry events shared across VT Code crates.
8//!
9//! This crate exposes the serialized schema for thread lifecycle updates,
10//! command execution results, and other timeline artifacts emitted by the
11//! automation runtime. Downstream applications can deserialize these
12//! structures to drive dashboards, logging, or auditing pipelines without
13//! depending on the full `vtcode-core` crate.
14//!
15//! # Agent Trace Support
16//!
17//! This crate implements the [Agent Trace](https://agent-trace.dev/) specification
18//! for tracking AI-generated code attribution. See the [`trace`] module for details.
19
20use 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
30/// Semantic version of the serialized event schema exported by this crate.
31pub const EVENT_SCHEMA_VERSION: &str = "0.20.0";
32
33/// Wraps a [`ThreadEvent`] with schema metadata so downstream consumers can
34/// negotiate compatibility before processing an event stream.
35#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
36#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
37pub struct VersionedThreadEvent {
38    /// Semantic version describing the schema of the nested event payload.
39    schema_version: String,
40    /// Concrete event emitted by the agent runtime.
41    event: ThreadEvent,
42}
43
44impl VersionedThreadEvent {
45    /// Creates a new [`VersionedThreadEvent`] using the current
46    /// [`EVENT_SCHEMA_VERSION`].
47    pub fn new(event: ThreadEvent) -> Self {
48        Self {
49            schema_version: EVENT_SCHEMA_VERSION.to_string(),
50            event,
51        }
52    }
53
54    /// Returns the nested [`ThreadEvent`], consuming the wrapper.
55    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
66/// Sink for processing [`ThreadEvent`] instances.
67pub trait EventEmitter {
68    /// Invoked for each event emitted by the automation runtime.
69    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/// JSON helper utilities for serializing and deserializing thread events.
82#[cfg(feature = "serde-json")]
83pub(crate) mod json {
84    use super::{ThreadEvent, VersionedThreadEvent};
85
86    /// Converts an event into a `serde_json::Value`.
87    pub fn to_value(event: &ThreadEvent) -> serde_json::Result<serde_json::Value> {
88        serde_json::to_value(event)
89    }
90
91    /// Serializes an event into a JSON string.
92    pub(crate) fn to_string(event: &ThreadEvent) -> serde_json::Result<String> {
93        serde_json::to_string(event)
94    }
95
96    /// Deserializes an event from a JSON string.
97    pub fn from_str(payload: &str) -> serde_json::Result<ThreadEvent> {
98        serde_json::from_str(payload)
99    }
100
101    /// Serializes a [`VersionedThreadEvent`] wrapper.
102    pub(crate) fn versioned_to_string(event: &ThreadEvent) -> serde_json::Result<String> {
103        serde_json::to_string(&VersionedThreadEvent::new(event.clone()))
104    }
105
106    /// Deserializes a [`VersionedThreadEvent`] wrapper.
107    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    /// Emits JSON serialized events to the `log` facade at the configured level.
119    #[derive(Debug, Clone)]
120    pub struct LogEmitter {
121        level: Level,
122    }
123
124    impl LogEmitter {
125        /// Creates a new [`LogEmitter`] that logs at the provided [`Level`].
126        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    /// Emits structured events as `tracing` events at the specified level.
161    #[derive(Debug, Clone)]
162    pub struct TracingEmitter {
163        level: Level,
164    }
165
166    impl TracingEmitter {
167        /// Creates a new [`TracingEmitter`] with the provided [`Level`].
168        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    /// Emits [`ThreadEvent`]s as OpenTelemetry spans and span events.
235    ///
236    /// Each `ThreadEvent` is recorded as an OTel span with attributes derived
237    /// from the event payload.  Harness events are attached as span events
238    /// with their own attributes (event kind, message, path, etc.).
239    ///
240    /// # Usage
241    ///
242    /// ```rust,ignore
243    /// // Requires concrete SDK type (e.g. opentelemetry_sdk::trace::SdkTracerProvider)
244    /// # use vtcode_exec_events::OtelEmitter;
245    /// # let tracer = opentelemetry_sdk::trace::SdkTracerProvider::default()
246    /// #     .tracer("vtcode");
247    /// # let mut emitter = OtelEmitter::new(tracer);
248    /// ```
249    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    /// Generates a JSON Schema describing [`ThreadEvent`].
358    pub fn thread_event_schema() -> Schema {
359        schema_for!(ThreadEvent)
360    }
361
362    /// Generates a JSON Schema describing [`VersionedThreadEvent`].
363    pub fn versioned_thread_event_schema() -> Schema {
364        schema_for!(VersionedThreadEvent)
365    }
366}
367
368/// Structured events emitted during autonomous execution.
369#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
370#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
371#[serde(tag = "type")]
372pub enum ThreadEvent {
373    /// Replayable local matrix lifecycle checkpoint.
374    #[serde(rename = "matrix.updated")]
375    MatrixUpdated(Box<matrix::MatrixSnapshot>),
376    /// Indicates that a new execution thread has started.
377    #[serde(rename = "thread.started")]
378    ThreadStarted(ThreadStartedEvent),
379    /// Indicates that an execution thread has reached a terminal outcome.
380    #[serde(rename = "thread.completed")]
381    ThreadCompleted(Box<ThreadCompletedEvent>),
382    /// Indicates that conversation compaction replaced older history with a boundary.
383    #[serde(rename = "thread.compact_boundary")]
384    ThreadCompactBoundary(Box<ThreadCompactBoundaryEvent>),
385    /// Indicates that the approved plan handoff rebuilt a fresh execution context.
386    #[serde(rename = "context.reset")]
387    ContextReset(ContextResetEvent),
388    /// Marks the beginning of an execution turn.
389    #[serde(rename = "turn.started")]
390    TurnStarted(TurnStartedEvent),
391    /// Marks the completion of an execution turn.
392    #[serde(rename = "turn.completed")]
393    TurnCompleted(TurnCompletedEvent),
394    /// Marks a turn as failed with additional context.
395    #[serde(rename = "turn.failed")]
396    TurnFailed(TurnFailedEvent),
397    /// Marks a turn as blocked before success could be confirmed. Emitted
398    /// alongside `turn.failed` so UI subscribers get a first-class signal
399    /// with the fuse counters and last tool instead of inferring it.
400    #[serde(rename = "turn.blocked")]
401    TurnBlocked(Box<TurnBlockedEvent>),
402    /// Indicates that an item has started processing.
403    #[serde(rename = "item.started")]
404    ItemStarted(ItemStartedEvent),
405    /// Indicates that an item has been updated.
406    #[serde(rename = "item.updated")]
407    ItemUpdated(ItemUpdatedEvent),
408    /// Indicates that an item reached a terminal state.
409    #[serde(rename = "item.completed")]
410    ItemCompleted(ItemCompletedEvent),
411    /// Emitted when a tool requires user permission before execution.
412    #[serde(rename = "permission.requested")]
413    PermissionRequested(PermissionRequestedEvent),
414    /// Emitted when the user resolves a permission prompt.
415    #[serde(rename = "permission.resolved")]
416    PermissionResolved(PermissionResolvedEvent),
417    /// A mid-turn user interjection was merged into the running turn.
418    #[serde(rename = "interjected")]
419    Interjected(InterjectedEvent),
420    /// Streaming delta for a plan item in Planning workflow.
421    #[serde(rename = "plan.delta")]
422    PlanDelta(Box<PlanDeltaEvent>),
423    /// Indicates that a completed plan is waiting for an implementation decision.
424    #[serde(rename = "plan.approval.requested")]
425    PlanApprovalRequested(PlanApprovalRequestedEvent),
426    /// Records the user's or policy's decision about a completed plan.
427    #[serde(rename = "plan.approval.resolved")]
428    PlanApprovalResolved(PlanApprovalResolvedEvent),
429    /// Represents a fatal error.
430    #[serde(rename = "error")]
431    Error(ThreadErrorEvent),
432    /// Catch-all for unknown event types added in newer schema versions.
433    /// Preserves forward compatibility when older binaries read newer event streams.
434    #[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    /// Unique identifier for the thread that was started.
442    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    /// Catch-all for unknown completion subtypes added in newer schema versions.
455    #[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    /// Compaction triggered by a mid-session switch of the main model or
484    /// provider, so the newly selected model starts from a clean summary.
485    ModelSwitch,
486    /// Catch-all for unknown triggers added in newer schema versions.
487    #[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    /// Catch-all for unknown modes added in newer schema versions.
510    #[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    /// Runtime completion timestamp, absent from legacy events.
528    #[serde(default, skip_serializing_if = "Option::is_none")]
529    pub completed_at: Option<String>,
530    /// Stable thread identifier for the session.
531    pub thread_id: String,
532    /// Stable session identifier for the runtime that produced the thread.
533    pub session_id: String,
534    /// Coarse result category aligned with SDK-style terminal states.
535    pub subtype: ThreadCompletionSubtype,
536    /// VT Code-specific detailed outcome code.
537    pub outcome_code: String,
538    /// Final assistant result text when the thread completed successfully.
539    #[serde(skip_serializing_if = "Option::is_none")]
540    pub result: Option<String>,
541    /// Provider stop reason or VT Code terminal reason when available.
542    #[serde(skip_serializing_if = "Option::is_none")]
543    pub stop_reason: Option<String>,
544    /// Aggregated token usage across the thread.
545    pub usage: Usage,
546    /// Optional estimated total API cost for the thread.
547    #[serde(skip_serializing_if = "Option::is_none")]
548    pub total_cost_usd: Option<serde_json::Number>,
549    /// Number of turns executed before completion.
550    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    /// Stable thread identifier for the session.
557    pub thread_id: String,
558    /// Whether compaction was triggered manually or automatically.
559    pub trigger: CompactionTrigger,
560    /// Whether the compaction boundary came from provider-native or local compaction.
561    pub mode: CompactionMode,
562    /// Number of messages before compaction.
563    pub original_message_count: usize,
564    /// Number of messages after compaction.
565    pub compacted_message_count: usize,
566    /// Optional persisted artifact containing the archived compaction summary/history.
567    #[serde(skip_serializing_if = "Option::is_none")]
568    pub history_artifact_path: Option<String>,
569    /// Segment identifier that contained the request prefix before compaction.
570    #[serde(skip_serializing_if = "Option::is_none")]
571    pub previous_segment_id: Option<String>,
572    /// Segment identifier created after compaction.
573    #[serde(skip_serializing_if = "Option::is_none")]
574    pub new_segment_id: Option<String>,
575    /// Hash of the immutable request prefix before compaction.
576    #[serde(skip_serializing_if = "Option::is_none")]
577    pub previous_prefix_hash: Option<String>,
578    /// Hash of the immutable request prefix after compaction.
579    #[serde(skip_serializing_if = "Option::is_none")]
580    pub new_prefix_hash: Option<String>,
581    /// Hash of the ordered tool catalog before compaction.
582    #[serde(skip_serializing_if = "Option::is_none")]
583    pub previous_catalog_hash: Option<String>,
584    /// Hash of the ordered tool catalog after compaction.
585    #[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    /// The user selected the fresh-context plan approval path.
594    PlanApproval,
595    /// Catch-all for triggers introduced by newer schema versions.
596    #[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    /// Stable thread identifier for the session.
604    pub thread_id: String,
605    /// Identifier of the turn that approved the plan.
606    pub turn_id: String,
607    /// What initiated the context reset.
608    pub trigger: ContextResetTrigger,
609    /// Whether the approved plan and task tracker survived the reset.
610    pub plan_preserved: bool,
611    /// Context pressure reported before the reset, expressed as a percentage.
612    pub previous_context_usage_percent: u8,
613    /// Whether the per-turn and per-session tool budgets were reset.
614    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    /// Optional decomposition of the assembled first-request prefix so
621    /// downstream consumers can attribute token overhead without inventing
622    /// parallel event types.
623    #[serde(skip_serializing_if = "Option::is_none")]
624    token_breakdown: Option<Box<TokenBreakdown>>,
625    /// Task identity and public input recorded at the request boundary.
626    #[serde(default, skip_serializing_if = "Option::is_none")]
627    pub context: Option<Box<ExecutionContext>>,
628}
629
630impl TurnStartedEvent {
631    /// Recorded prefix token breakdown, absent when the producer did not capture it.
632    pub fn token_breakdown(&self) -> Option<&TokenBreakdown> {
633        self.token_breakdown.as_deref()
634    }
635}
636
637/// Origin of a turn's input; internal turns retain their parent task.
638#[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/// Optional recorded execution ancestry. Absent fields are historical gaps.
650#[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/// Recorded shell classification, supplied by the runtime's shared classifier.
665#[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/// Item identity, timing, and command semantics for explanation consumers.
675#[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/// Per-request token-budget breakdown for the assembled first-request prefix.
689#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
690#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
691pub struct TokenBreakdown {
692    /// System prompt text tokens.
693    system_prompt_tokens: u64,
694    /// On-wire tool schema tokens.
695    tool_schema_tokens: u64,
696    /// Instruction file tokens included in the prompt.
697    instruction_file_tokens: u64,
698    /// Message history text tokens.
699    message_history_tokens: u64,
700    /// Cache read tokens (served from prior turns).
701    cache_read_tokens: u64,
702    /// Cache write tokens (new cache entries created this turn).
703    cache_write_tokens: u64,
704    /// Tokens that missed cache (neither read nor written).
705    cache_miss_tokens: u64,
706    /// Subagent bootstrap tokens, if this turn spawned a child agent.
707    #[serde(skip_serializing_if = "Option::is_none")]
708    subagent_bootstrap_tokens: Option<u64>,
709}
710
711/// Bound on exec session ids recorded in one turn's `turn.completed` event.
712/// Mirrors `SnapshotTurnDiagnostics::in_progress_exec_sessions` (cap 4,
713/// newest first) so `ThreadEvent` and checkpoint diagnostics cannot drift.
714pub 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    /// Token usage summary for the completed turn.
722    pub usage: Usage,
723    /// Exec sessions still running when the turn ended (bounded, newest
724    /// first). Empty when every command settled within the turn. Correlates
725    /// with the next turn's transient exec-session resume hint without
726    /// requiring session-id reconstruction.
727    #[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    /// Human-readable explanation describing why the turn failed.
741    pub message: String,
742    /// Optional token usage that was consumed before the failure occurred.
743    #[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    /// Runtime terminal timestamp, absent from legacy events.
751    #[serde(default, skip_serializing_if = "Option::is_none")]
752    pub completed_at: Option<String>,
753    /// Human-readable explanation describing why the turn was blocked.
754    pub message: String,
755    /// Display label of the last blocked tool call, when known.
756    #[serde(skip_serializing_if = "Option::is_none")]
757    pub last_tool: Option<String>,
758    /// Consecutive blocked tool calls observed this turn.
759    #[serde(default)]
760    pub blocked_streak: usize,
761    /// Total blocked tool calls observed this turn.
762    #[serde(default)]
763    pub blocked_total: usize,
764    /// Consecutive cap that was enforced.
765    #[serde(default)]
766    pub consecutive_cap: usize,
767    /// Total cap that was enforced.
768    #[serde(default)]
769    pub total_cap: usize,
770    /// Whether the fuse tripped while a tool-free recovery pass was active.
771    #[serde(default)]
772    pub recovery_active: bool,
773    /// Optional token usage that was consumed before the block occurred.
774    #[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    /// Fatal error message associated with the thread.
782    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    /// Number of prompt tokens processed during the turn.
789    #[serde(default, deserialize_with = "deserialize_null_as_default")]
790    pub input_tokens: u64,
791    /// Number of cached prompt tokens reused from previous turns.
792    #[serde(default, deserialize_with = "deserialize_null_as_default")]
793    pub cached_input_tokens: u64,
794    /// Number of cache-creation tokens charged during the turn.
795    #[serde(default, deserialize_with = "deserialize_null_as_default")]
796    pub cache_creation_tokens: u64,
797    /// Number of completion tokens generated by the model.
798    #[serde(default, deserialize_with = "deserialize_null_as_default")]
799    pub output_tokens: u64,
800}
801
802/// Serde helper that accepts explicit `null` as `T::default()` for
803/// backward-compatible checkpoint/diagnostics payloads. Pair with
804/// `#[serde(default, deserialize_with = "deserialize_null_as_default")]` so
805/// both missing and `null` fields degrade to the default instead of failing
806/// deserialization. Reused by downstream crates (e.g. `vtcode-core`
807/// snapshots) so the null-tolerance rule cannot drift between copies.
808pub 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    /// Number of input tokens billed at the full input rate: neither served
818    /// from cache nor written to it. `input_tokens` is the total prompt token
819    /// count (uncached + cached + cache-creation), so both cached and
820    /// cache-creation tokens are subtracted out here.
821    #[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    /// Cache hit rate as a fraction (0.0 to 1.0): cached input over total input.
829    /// Returns `None` when no input tokens were recorded.
830    #[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    /// Human-readable summary of prompt cache efficiency.
839    #[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    /// Accumulate another usage sample into this one.
857    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    /// Snapshot of the thread item that completed.
869    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    /// Snapshot of the thread item that began processing.
876    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    /// Snapshot of the thread item after it was updated.
883    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    /// Identifier of the thread emitting this plan delta.
890    pub thread_id: String,
891    /// Identifier of the current turn.
892    pub turn_id: String,
893    /// Identifier of the plan item receiving the delta.
894    pub item_id: String,
895    /// Incremental plan text chunk.
896    pub delta: String,
897    /// Runtime-owned recovery checkpoint; absent in older sessions.
898    #[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    /// Identifier of the thread emitting the approval request.
906    pub thread_id: String,
907    /// Identifier of the turn that produced the plan.
908    pub turn_id: String,
909    /// Plan file associated with the approval request, when available.
910    #[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 with normal per-edit approval prompts.
919    Execute,
920    /// Execute with automatic edit approval enabled.
921    AutoAccept,
922    /// Execute the plan after rebuilding a fresh context.
923    FreshContext,
924    /// Keep planning and revise the proposed plan.
925    Revise,
926    /// Dismiss the approval request without implementing.
927    Cancel,
928    /// Hand the plan to the build primary agent.
929    SwitchBuild,
930    /// Hand the plan to the auto primary agent.
931    SwitchAuto,
932    /// Catch-all for decisions added in newer schema versions.
933    #[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    /// Identifier of the thread emitting the approval decision.
941    pub thread_id: String,
942    /// Identifier of the turn in which the decision was made.
943    pub turn_id: String,
944    /// Decision selected by the user or active execution policy.
945    pub decision: PlanApprovalDecision,
946    /// Whether the decision came from policy rather than an interactive user action.
947    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    /// Stable identifier associated with the item.
956    pub id: String,
957    /// Embedded event details for the item type.
958    #[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    /// Message authored by the agent.
967    AgentMessage(AgentMessageItem),
968    /// Structured plan content authored by the agent in Planning workflow.
969    Plan(PlanItem),
970    /// Free-form reasoning text produced during a turn.
971    Reasoning(Box<ReasoningItem>),
972    /// Public rationale explicitly recorded by the agent; never private reasoning.
973    Decision(Box<DecisionItem>),
974    /// Command execution lifecycle update for an actual shell/PTY process.
975    CommandExecution(Box<CommandExecutionItem>),
976    /// Tool invocation lifecycle update.
977    ToolInvocation(Box<ToolInvocationItem>),
978    /// Tool output lifecycle update tied to a tool invocation.
979    ToolOutput(Box<ToolOutputItem>),
980    /// File change summary associated with the turn.
981    FileChange(Box<FileChangeItem>),
982    /// MCP tool invocation status.
983    McpToolCall(Box<McpToolCallItem>),
984    /// Web search event emitted by a registered search provider.
985    WebSearch(Box<WebSearchItem>),
986    /// Harness-managed continuation or verification lifecycle event.
987    Harness(Box<HarnessEventItem>),
988    /// General error captured for auditing.
989    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    /// Textual content of the agent message.
996    pub text: String,
997}
998
999/// A consequential choice and the agent's reported public rationale.
1000#[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    /// Plan markdown content.
1015    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    /// Operational diagnostic independent of private model reasoning.
1022    #[serde(default, skip_serializing_if = "Option::is_none")]
1023    pub diagnostic: Option<Box<Diagnostic>>,
1024    /// Free-form reasoning content captured during planning.
1025    pub text: String,
1026    /// Optional stage of reasoning (e.g., "analysis", "plan", "verification",
1027    /// or the bounded evidence-only "diagnosis" stage).
1028    #[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    /// Command finished successfully.
1037    #[default]
1038    Completed,
1039    /// Command failed (non-zero exit code or runtime error).
1040    Failed,
1041    /// Command is still running and may emit additional output.
1042    InProgress,
1043}
1044
1045#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1046#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1047pub struct CommandExecutionItem {
1048    /// Tool or command identifier executed by the runner.
1049    pub command: String,
1050    /// Arguments passed to the tool invocation, when available.
1051    #[serde(skip_serializing_if = "Option::is_none")]
1052    pub arguments: Option<Value>,
1053    /// Aggregated output emitted by the command.
1054    #[serde(default)]
1055    pub aggregated_output: String,
1056    /// Exit code reported by the process, when available.
1057    #[serde(skip_serializing_if = "Option::is_none")]
1058    pub exit_code: Option<i32>,
1059    /// Current status of the command execution.
1060    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    /// Tool finished successfully.
1068    #[default]
1069    Completed,
1070    /// Tool failed.
1071    Failed,
1072    /// Tool is still running and may emit additional output.
1073    InProgress,
1074}
1075
1076/// Fine-grained outcome of a tool invocation lifecycle.
1077///
1078/// Mirrors the outcome taxonomy used by the runtime: `status` remains the
1079/// coarse lifecycle signal (`Completed` / `Failed` / `InProgress`), while
1080/// `outcome` captures *why* the invocation terminated. Consumers that only
1081/// need success/failure can continue to read `status`; analytics and the UI
1082/// layer use `outcome` for richer classification.
1083#[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    /// Tool executed and returned a result.
1088    #[default]
1089    Success,
1090    /// Tool executed but returned an error.
1091    Error,
1092    /// User rejected the permission prompt.
1093    PermissionRejected,
1094    /// User cancelled the permission prompt (e.g. Ctrl+C / Esc).
1095    PermissionCancelled,
1096    /// User provided a followup message instead of approving.
1097    Followup,
1098    /// A user-configured hook blocked execution.
1099    HookDenied,
1100    /// Tool not found or arguments couldn't be parsed.
1101    InvalidTool,
1102    /// Tool was cancelled during execution or closed before dispatch when its turn ended.
1103    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/// Map a terminal [`ToolCallStatus`] to its corresponding [`ToolOutcome`].
1114///
1115/// # Panics
1116///
1117/// Panics if `status` is [`ToolCallStatus::InProgress`], which is a non-terminal
1118/// state and must never be passed to a completion-event emitter.
1119#[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    /// Name of the invoked tool.
1136    pub tool_name: String,
1137    /// Structured arguments passed to the tool.
1138    #[serde(skip_serializing_if = "Option::is_none")]
1139    pub arguments: Option<Value>,
1140    /// Raw model-emitted tool call identifier, when available.
1141    #[serde(skip_serializing_if = "Option::is_none")]
1142    pub tool_call_id: Option<String>,
1143    /// Current lifecycle status of the invocation.
1144    pub status: ToolCallStatus,
1145    /// Fine-grained outcome of the invocation lifecycle.
1146    #[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    /// Identifier of the related harness invocation item.
1154    pub call_id: String,
1155    /// Raw model-emitted tool call identifier, when available.
1156    #[serde(skip_serializing_if = "Option::is_none")]
1157    pub tool_call_id: Option<String>,
1158    /// Canonical spool file path when the full output was written to disk.
1159    #[serde(skip_serializing_if = "Option::is_none")]
1160    pub spool_path: Option<String>,
1161    /// Aggregated output emitted by the tool.
1162    #[serde(default)]
1163    pub output: String,
1164    /// Exit code reported by the tool, when available.
1165    #[serde(skip_serializing_if = "Option::is_none")]
1166    pub exit_code: Option<i32>,
1167    /// Current lifecycle status of the output item.
1168    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    /// Captured preview was truncated, suppressed, or unavailable.
1175    #[serde(default, skip_serializing_if = "Option::is_none")]
1176    pub diff_incomplete: Option<bool>,
1177    /// List of individual file updates included in the change set.
1178    pub changes: Vec<FileUpdateChange>,
1179    /// Whether the patch application succeeded.
1180    pub status: PatchApplyStatus,
1181    /// Optional precomputed unified diff for the change set.
1182    ///
1183    /// Populated by the turn diff tracker so consumers can render per-change
1184    /// previews without recomputation. Absent in older events.
1185    #[serde(default, skip_serializing_if = "Option::is_none")]
1186    pub unified_diff: Option<String>,
1187    /// Optional added-line count for the change set.
1188    #[serde(default, skip_serializing_if = "Option::is_none")]
1189    pub additions: Option<u64>,
1190    /// Optional deleted-line count for the change set.
1191    #[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    /// Path of the file that was updated.
1199    pub path: String,
1200    /// Type of change applied to the file.
1201    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    /// Patch successfully applied.
1209    Completed,
1210    /// Patch application failed.
1211    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    /// File addition.
1219    Add,
1220    /// File deletion.
1221    Delete,
1222    /// File update in place.
1223    Update,
1224}
1225
1226#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1227#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1228pub struct McpToolCallItem {
1229    /// Name of the MCP tool invoked by the agent.
1230    pub tool_name: String,
1231    /// Arguments passed to the tool invocation, if any.
1232    #[serde(skip_serializing_if = "Option::is_none")]
1233    pub arguments: Option<Value>,
1234    /// Result payload returned by the tool, if captured.
1235    #[serde(skip_serializing_if = "Option::is_none")]
1236    pub result: Option<String>,
1237    /// Lifecycle status for the tool call.
1238    #[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    /// Tool invocation has started.
1247    Started,
1248    /// Tool invocation completed successfully.
1249    Completed,
1250    /// Tool invocation failed.
1251    Failed,
1252}
1253
1254#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1255#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
1256pub struct WebSearchItem {
1257    /// Query that triggered the search.
1258    pub query: String,
1259    /// Search provider identifier, when known.
1260    #[serde(skip_serializing_if = "Option::is_none")]
1261    pub provider: Option<String>,
1262    /// Optional raw search results captured for auditing.
1263    #[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    /// A turn was blocked before success could be confirmed. Carries the fuse
1276    /// counters so UI layers can render without correlating multiple events.
1277    TurnBlocked,
1278    /// A bounded tool-free recovery pass was scheduled after blocked calls.
1279    BlockedRecoveryStarted,
1280    /// A bounded tool-free recovery pass finished.
1281    BlockedRecoveryFinished,
1282    BlockedHandoffWritten,
1283    /// The owning session resolved its archived blocked handoff and removed
1284    /// the live recovery pointer.
1285    BlockedHandoffResolved,
1286    EvaluationStarted,
1287    EvaluationPassed,
1288    EvaluationFailed,
1289    RevisionStarted,
1290    EscalationTriggered,
1291    EscalationBypassed,
1292    VerificationStarted,
1293    VerificationPassed,
1294    VerificationFailed,
1295    /// Agent recovered from a transient error (e.g. after retry succeeded).
1296    ErrorRecovered,
1297    /// A transient tool failure triggered an automatic retry attempt.
1298    ToolRetryAttempted,
1299    /// Latency record for a tool execution, emitted on turn completion.
1300    ToolLatencyRecorded,
1301    /// A checkpoint snapshot was created for the current turn.
1302    SnapshotCreated,
1303    /// A checkpoint snapshot was restored (rewind operation).
1304    SnapshotRestored,
1305    /// The user granted additional session tool-call capacity and the
1306    /// pending call will be retried in the same turn.
1307    SessionToolLimitIncreased,
1308    /// The user granted additional tool-loop capacity for the current turn.
1309    ToolLoopLimitIncreased,
1310    /// A background subprocess or exec session reached a terminal state.
1311    BackgroundSubprocessCompleted,
1312    /// A native delegated agent's public status and ancestry were observed.
1313    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    /// Name of the tool that requires permission.
1330    pub tool_name: String,
1331    /// Identifier of the tool invocation that requested permission, when known.
1332    #[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    /// Name of the tool that was permitted or denied.
1340    pub tool_name: String,
1341    /// Identifier of the tool invocation whose permission was resolved, when known.
1342    #[serde(default, skip_serializing_if = "Option::is_none")]
1343    pub tool_call_id: Option<String>,
1344    /// User's decision on the permission prompt.
1345    pub decision: PermissionDecision,
1346    /// Wall-clock time the prompt was visible, in milliseconds.
1347    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    /// Public correction text, when recorded by the runtime.
1369    #[serde(default, skip_serializing_if = "Option::is_none")]
1370    pub text: Option<Box<String>>,
1371    /// How the interjection reached the running turn.
1372    pub source: InterjectionSource,
1373    /// Number of image attachments that accompanied the interjection.
1374    pub image_count: u32,
1375    /// Always `Interjection` for this event; carried so the shared
1376    /// `redirect_kind` field is queryable uniformly across redirect events.
1377    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    /// Specific harness event emitted by the runtime.
1384    pub event: HarnessEventKind,
1385    /// Optional human-readable message associated with the event.
1386    #[serde(skip_serializing_if = "Option::is_none")]
1387    pub message: Option<String>,
1388    /// Optional verification command associated with the event.
1389    #[serde(skip_serializing_if = "Option::is_none")]
1390    pub command: Option<String>,
1391    /// Optional artifact path associated with the event.
1392    #[serde(skip_serializing_if = "Option::is_none")]
1393    pub path: Option<String>,
1394    /// Optional exit code associated with verification results.
1395    #[serde(skip_serializing_if = "Option::is_none")]
1396    pub exit_code: Option<i32>,
1397    /// Retry/recovery attempt number (1-indexed). Only set for retry-related events.
1398    #[serde(skip_serializing_if = "Option::is_none")]
1399    pub attempt: Option<u32>,
1400    /// Canonical error category for retry/recovery events.
1401    #[serde(skip_serializing_if = "Option::is_none")]
1402    pub error_category: Option<String>,
1403    /// Latency in milliseconds for tool-execution latency events.
1404    #[serde(skip_serializing_if = "Option::is_none")]
1405    pub duration_ms: Option<u64>,
1406    /// Stable task identifier for background completion events.
1407    #[serde(skip_serializing_if = "Option::is_none")]
1408    pub task_id: Option<String>,
1409    /// Child session identifier for background completion events.
1410    #[serde(skip_serializing_if = "Option::is_none")]
1411    pub session_id: Option<String>,
1412    /// Exec-session identifier for background completion events.
1413    #[serde(skip_serializing_if = "Option::is_none")]
1414    pub exec_session_id: Option<String>,
1415    /// Terminal background status, when the event represents a subprocess.
1416    #[serde(skip_serializing_if = "Option::is_none")]
1417    pub status: Option<String>,
1418    /// Archived transcript reference for background completion events.
1419    #[serde(skip_serializing_if = "Option::is_none")]
1420    pub transcript_path: Option<String>,
1421    /// Archived session reference for background completion events.
1422    #[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    /// Error message displayed to the user or logs.
1430    pub message: String,
1431}
1432
1433#[cfg(test)]
1434mod tests;