Skip to main content

vtcode_exec_events/
lib.rs

1//! Structured execution telemetry events shared across VT Code crates.
2//!
3//! This crate exposes the serialized schema for thread lifecycle updates,
4//! command execution results, and other timeline artifacts emitted by the
5//! automation runtime. Downstream applications can deserialize these
6//! structures to drive dashboards, logging, or auditing pipelines without
7//! depending on the full `vtcode-core` crate.
8//!
9//! # Agent Trace Support
10//!
11//! This crate implements the [Agent Trace](https://agent-trace.dev/) specification
12//! for tracking AI-generated code attribution. See the [`trace`] module for details.
13
14use serde::{Deserialize, Serialize};
15use serde_json::Value;
16
17pub mod atif;
18pub mod trace;
19
20/// Semantic version of the serialized event schema exported by this crate.
21pub const EVENT_SCHEMA_VERSION: &str = "0.4.0";
22
23/// Wraps a [`ThreadEvent`] with schema metadata so downstream consumers can
24/// negotiate compatibility before processing an event stream.
25#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
26#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
27pub struct VersionedThreadEvent {
28    /// Semantic version describing the schema of the nested event payload.
29    pub schema_version: String,
30    /// Concrete event emitted by the agent runtime.
31    pub event: ThreadEvent,
32}
33
34impl VersionedThreadEvent {
35    /// Creates a new [`VersionedThreadEvent`] using the current
36    /// [`EVENT_SCHEMA_VERSION`].
37    pub fn new(event: ThreadEvent) -> Self {
38        Self {
39            schema_version: EVENT_SCHEMA_VERSION.to_string(),
40            event,
41        }
42    }
43
44    /// Returns the nested [`ThreadEvent`], consuming the wrapper.
45    pub fn into_event(self) -> ThreadEvent {
46        self.event
47    }
48}
49
50impl From<ThreadEvent> for VersionedThreadEvent {
51    fn from(event: ThreadEvent) -> Self {
52        Self::new(event)
53    }
54}
55
56/// Sink for processing [`ThreadEvent`] instances.
57pub trait EventEmitter {
58    /// Invoked for each event emitted by the automation runtime.
59    fn emit(&mut self, event: &ThreadEvent);
60}
61
62impl<F> EventEmitter for F
63where
64    F: FnMut(&ThreadEvent),
65{
66    fn emit(&mut self, event: &ThreadEvent) {
67        self(event);
68    }
69}
70
71/// JSON helper utilities for serializing and deserializing thread events.
72#[cfg(feature = "serde-json")]
73pub mod json {
74    use super::{ThreadEvent, VersionedThreadEvent};
75
76    /// Converts an event into a `serde_json::Value`.
77    pub fn to_value(event: &ThreadEvent) -> serde_json::Result<serde_json::Value> {
78        serde_json::to_value(event)
79    }
80
81    /// Serializes an event into a JSON string.
82    pub fn to_string(event: &ThreadEvent) -> serde_json::Result<String> {
83        serde_json::to_string(event)
84    }
85
86    /// Deserializes an event from a JSON string.
87    pub fn from_str(payload: &str) -> serde_json::Result<ThreadEvent> {
88        serde_json::from_str(payload)
89    }
90
91    /// Serializes a [`VersionedThreadEvent`] wrapper.
92    pub fn versioned_to_string(event: &ThreadEvent) -> serde_json::Result<String> {
93        serde_json::to_string(&VersionedThreadEvent::new(event.clone()))
94    }
95
96    /// Deserializes a [`VersionedThreadEvent`] wrapper.
97    pub fn versioned_from_str(payload: &str) -> serde_json::Result<VersionedThreadEvent> {
98        serde_json::from_str(payload)
99    }
100}
101
102#[cfg(feature = "telemetry-log")]
103mod log_support {
104    use log::Level;
105
106    use super::{EventEmitter, ThreadEvent, json};
107
108    /// Emits JSON serialized events to the `log` facade at the configured level.
109    #[derive(Debug, Clone)]
110    pub struct LogEmitter {
111        level: Level,
112    }
113
114    impl LogEmitter {
115        /// Creates a new [`LogEmitter`] that logs at the provided [`Level`].
116        pub fn new(level: Level) -> Self {
117            Self { level }
118        }
119    }
120
121    impl Default for LogEmitter {
122        fn default() -> Self {
123            Self { level: Level::Info }
124        }
125    }
126
127    impl EventEmitter for LogEmitter {
128        fn emit(&mut self, event: &ThreadEvent) {
129            if log::log_enabled!(self.level) {
130                match json::to_string(event) {
131                    Ok(serialized) => log::log!(self.level, "{}", serialized),
132                    Err(err) => log::log!(
133                        self.level,
134                        "failed to serialize vtcode exec event for logging: {err}"
135                    ),
136                }
137            }
138        }
139    }
140
141    pub use LogEmitter as PublicLogEmitter;
142}
143
144#[cfg(feature = "telemetry-log")]
145pub use log_support::PublicLogEmitter as LogEmitter;
146
147#[cfg(feature = "telemetry-tracing")]
148mod tracing_support {
149    use tracing::Level;
150
151    use super::{EVENT_SCHEMA_VERSION, EventEmitter, ThreadEvent, VersionedThreadEvent};
152
153    /// Emits structured events as `tracing` events at the specified level.
154    #[derive(Debug, Clone)]
155    pub struct TracingEmitter {
156        level: Level,
157    }
158
159    impl TracingEmitter {
160        /// Creates a new [`TracingEmitter`] with the provided [`Level`].
161        pub fn new(level: Level) -> Self {
162            Self { level }
163        }
164    }
165
166    impl Default for TracingEmitter {
167        fn default() -> Self {
168            Self { level: Level::INFO }
169        }
170    }
171
172    impl EventEmitter for TracingEmitter {
173        fn emit(&mut self, event: &ThreadEvent) {
174            match self.level {
175                Level::TRACE => tracing::event!(
176                    target: "vtcode_exec_events",
177                    Level::TRACE,
178                    schema_version = EVENT_SCHEMA_VERSION,
179                    event = ?VersionedThreadEvent::new(event.clone()),
180                    "vtcode_exec_event"
181                ),
182                Level::DEBUG => tracing::event!(
183                    target: "vtcode_exec_events",
184                    Level::DEBUG,
185                    schema_version = EVENT_SCHEMA_VERSION,
186                    event = ?VersionedThreadEvent::new(event.clone()),
187                    "vtcode_exec_event"
188                ),
189                Level::INFO => tracing::event!(
190                    target: "vtcode_exec_events",
191                    Level::INFO,
192                    schema_version = EVENT_SCHEMA_VERSION,
193                    event = ?VersionedThreadEvent::new(event.clone()),
194                    "vtcode_exec_event"
195                ),
196                Level::WARN => tracing::event!(
197                    target: "vtcode_exec_events",
198                    Level::WARN,
199                    schema_version = EVENT_SCHEMA_VERSION,
200                    event = ?VersionedThreadEvent::new(event.clone()),
201                    "vtcode_exec_event"
202                ),
203                Level::ERROR => tracing::event!(
204                    target: "vtcode_exec_events",
205                    Level::ERROR,
206                    schema_version = EVENT_SCHEMA_VERSION,
207                    event = ?VersionedThreadEvent::new(event.clone()),
208                    "vtcode_exec_event"
209                ),
210            }
211        }
212    }
213
214    pub use TracingEmitter as PublicTracingEmitter;
215}
216
217#[cfg(feature = "telemetry-tracing")]
218pub use tracing_support::PublicTracingEmitter as TracingEmitter;
219
220#[cfg(feature = "schema-export")]
221pub mod schema {
222    use schemars::{Schema, schema_for};
223
224    use super::{ThreadEvent, VersionedThreadEvent};
225
226    /// Generates a JSON Schema describing [`ThreadEvent`].
227    pub fn thread_event_schema() -> Schema {
228        schema_for!(ThreadEvent)
229    }
230
231    /// Generates a JSON Schema describing [`VersionedThreadEvent`].
232    pub fn versioned_thread_event_schema() -> Schema {
233        schema_for!(VersionedThreadEvent)
234    }
235}
236
237/// Structured events emitted during autonomous execution.
238#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
239#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
240#[serde(tag = "type")]
241pub enum ThreadEvent {
242    /// Indicates that a new execution thread has started.
243    #[serde(rename = "thread.started")]
244    ThreadStarted(ThreadStartedEvent),
245    /// Indicates that an execution thread has reached a terminal outcome.
246    #[serde(rename = "thread.completed")]
247    ThreadCompleted(ThreadCompletedEvent),
248    /// Indicates that conversation compaction replaced older history with a boundary.
249    #[serde(rename = "thread.compact_boundary")]
250    ThreadCompactBoundary(ThreadCompactBoundaryEvent),
251    /// Marks the beginning of an execution turn.
252    #[serde(rename = "turn.started")]
253    TurnStarted(TurnStartedEvent),
254    /// Marks the completion of an execution turn.
255    #[serde(rename = "turn.completed")]
256    TurnCompleted(TurnCompletedEvent),
257    /// Marks a turn as failed with additional context.
258    #[serde(rename = "turn.failed")]
259    TurnFailed(TurnFailedEvent),
260    /// Indicates that an item has started processing.
261    #[serde(rename = "item.started")]
262    ItemStarted(ItemStartedEvent),
263    /// Indicates that an item has been updated.
264    #[serde(rename = "item.updated")]
265    ItemUpdated(ItemUpdatedEvent),
266    /// Indicates that an item reached a terminal state.
267    #[serde(rename = "item.completed")]
268    ItemCompleted(ItemCompletedEvent),
269    /// Streaming delta for a plan item in Planning workflow.
270    #[serde(rename = "plan.delta")]
271    PlanDelta(PlanDeltaEvent),
272    /// Represents a fatal error.
273    #[serde(rename = "error")]
274    Error(ThreadErrorEvent),
275    /// Catch-all for unknown event types added in newer schema versions.
276    /// Preserves forward compatibility when older binaries read newer event streams.
277    #[serde(other)]
278    Unknown,
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
282#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
283pub struct ThreadStartedEvent {
284    /// Unique identifier for the thread that was started.
285    pub thread_id: String,
286}
287
288#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
289#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
290#[serde(rename_all = "snake_case")]
291pub enum ThreadCompletionSubtype {
292    Success,
293    ErrorMaxTurns,
294    ErrorMaxBudgetUsd,
295    ErrorDuringExecution,
296    Cancelled,
297    /// Catch-all for unknown completion subtypes added in newer schema versions.
298    #[serde(other)]
299    Unknown,
300}
301
302impl ThreadCompletionSubtype {
303    pub const fn as_str(&self) -> &'static str {
304        match self {
305            Self::Success => "success",
306            Self::ErrorMaxTurns => "error_max_turns",
307            Self::ErrorMaxBudgetUsd => "error_max_budget_usd",
308            Self::ErrorDuringExecution => "error_during_execution",
309            Self::Cancelled => "cancelled",
310            Self::Unknown => "unknown",
311        }
312    }
313
314    pub const fn is_success(self) -> bool {
315        matches!(self, Self::Success)
316    }
317}
318
319#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
320#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
321#[serde(rename_all = "snake_case")]
322pub enum CompactionTrigger {
323    Manual,
324    Auto,
325    Recovery,
326    /// Catch-all for unknown triggers added in newer schema versions.
327    #[serde(other)]
328    Unknown,
329}
330
331impl CompactionTrigger {
332    pub const fn as_str(self) -> &'static str {
333        match self {
334            Self::Manual => "manual",
335            Self::Auto => "auto",
336            Self::Recovery => "recovery",
337            Self::Unknown => "unknown",
338        }
339    }
340}
341
342#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
343#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
344#[serde(rename_all = "snake_case")]
345pub enum CompactionMode {
346    Provider,
347    Local,
348    /// Catch-all for unknown modes added in newer schema versions.
349    #[serde(other)]
350    Unknown,
351}
352
353impl CompactionMode {
354    pub const fn as_str(self) -> &'static str {
355        match self {
356            Self::Provider => "provider",
357            Self::Local => "local",
358            Self::Unknown => "unknown",
359        }
360    }
361}
362
363#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
364#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
365pub struct ThreadCompletedEvent {
366    /// Stable thread identifier for the session.
367    pub thread_id: String,
368    /// Stable session identifier for the runtime that produced the thread.
369    pub session_id: String,
370    /// Coarse result category aligned with SDK-style terminal states.
371    pub subtype: ThreadCompletionSubtype,
372    /// VT Code-specific detailed outcome code.
373    pub outcome_code: String,
374    /// Final assistant result text when the thread completed successfully.
375    #[serde(skip_serializing_if = "Option::is_none")]
376    pub result: Option<String>,
377    /// Provider stop reason or VT Code terminal reason when available.
378    #[serde(skip_serializing_if = "Option::is_none")]
379    pub stop_reason: Option<String>,
380    /// Aggregated token usage across the thread.
381    pub usage: Usage,
382    /// Optional estimated total API cost for the thread.
383    #[serde(skip_serializing_if = "Option::is_none")]
384    pub total_cost_usd: Option<serde_json::Number>,
385    /// Number of turns executed before completion.
386    pub num_turns: usize,
387}
388
389#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
390#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
391pub struct ThreadCompactBoundaryEvent {
392    /// Stable thread identifier for the session.
393    pub thread_id: String,
394    /// Whether compaction was triggered manually or automatically.
395    pub trigger: CompactionTrigger,
396    /// Whether the compaction boundary came from provider-native or local compaction.
397    pub mode: CompactionMode,
398    /// Number of messages before compaction.
399    pub original_message_count: usize,
400    /// Number of messages after compaction.
401    pub compacted_message_count: usize,
402    /// Optional persisted artifact containing the archived compaction summary/history.
403    #[serde(skip_serializing_if = "Option::is_none")]
404    pub history_artifact_path: Option<String>,
405}
406
407#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
408#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
409pub struct TurnStartedEvent {}
410
411#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
412#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
413pub struct TurnCompletedEvent {
414    /// Token usage summary for the completed turn.
415    pub usage: Usage,
416}
417
418#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
419#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
420pub struct TurnFailedEvent {
421    /// Human-readable explanation describing why the turn failed.
422    pub message: String,
423    /// Optional token usage that was consumed before the failure occurred.
424    #[serde(skip_serializing_if = "Option::is_none")]
425    pub usage: Option<Usage>,
426}
427
428#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
429#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
430pub struct ThreadErrorEvent {
431    /// Fatal error message associated with the thread.
432    pub message: String,
433}
434
435#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
436#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
437pub struct Usage {
438    /// Number of prompt tokens processed during the turn.
439    pub input_tokens: u64,
440    /// Number of cached prompt tokens reused from previous turns.
441    pub cached_input_tokens: u64,
442    /// Number of cache-creation tokens charged during the turn.
443    pub cache_creation_tokens: u64,
444    /// Number of completion tokens generated by the model.
445    pub output_tokens: u64,
446}
447
448#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
449#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
450pub struct ItemCompletedEvent {
451    /// Snapshot of the thread item that completed.
452    pub item: ThreadItem,
453}
454
455#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
456#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
457pub struct ItemStartedEvent {
458    /// Snapshot of the thread item that began processing.
459    pub item: ThreadItem,
460}
461
462#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
463#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
464pub struct ItemUpdatedEvent {
465    /// Snapshot of the thread item after it was updated.
466    pub item: ThreadItem,
467}
468
469#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
470#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
471pub struct PlanDeltaEvent {
472    /// Identifier of the thread emitting this plan delta.
473    pub thread_id: String,
474    /// Identifier of the current turn.
475    pub turn_id: String,
476    /// Identifier of the plan item receiving the delta.
477    pub item_id: String,
478    /// Incremental plan text chunk.
479    pub delta: String,
480}
481
482#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
483#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
484pub struct ThreadItem {
485    /// Stable identifier associated with the item.
486    pub id: String,
487    /// Embedded event details for the item type.
488    #[serde(flatten)]
489    pub details: ThreadItemDetails,
490}
491
492#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
493#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
494#[serde(tag = "type", rename_all = "snake_case")]
495pub enum ThreadItemDetails {
496    /// Message authored by the agent.
497    AgentMessage(AgentMessageItem),
498    /// Structured plan content authored by the agent in Planning workflow.
499    Plan(PlanItem),
500    /// Free-form reasoning text produced during a turn.
501    Reasoning(ReasoningItem),
502    /// Command execution lifecycle update for an actual shell/PTY process.
503    CommandExecution(Box<CommandExecutionItem>),
504    /// Tool invocation lifecycle update.
505    ToolInvocation(ToolInvocationItem),
506    /// Tool output lifecycle update tied to a tool invocation.
507    ToolOutput(ToolOutputItem),
508    /// File change summary associated with the turn.
509    FileChange(Box<FileChangeItem>),
510    /// MCP tool invocation status.
511    McpToolCall(McpToolCallItem),
512    /// Web search event emitted by a registered search provider.
513    WebSearch(WebSearchItem),
514    /// Harness-managed continuation or verification lifecycle event.
515    Harness(HarnessEventItem),
516    /// General error captured for auditing.
517    Error(ErrorItem),
518}
519
520#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
521#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
522pub struct AgentMessageItem {
523    /// Textual content of the agent message.
524    pub text: String,
525}
526
527#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
528#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
529pub struct PlanItem {
530    /// Plan markdown content.
531    pub text: String,
532}
533
534#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
535#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
536pub struct ReasoningItem {
537    /// Free-form reasoning content captured during planning.
538    pub text: String,
539    /// Optional stage of reasoning (e.g., "analysis", "plan", "verification").
540    #[serde(skip_serializing_if = "Option::is_none")]
541    pub stage: Option<String>,
542}
543
544#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
545#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
546#[serde(rename_all = "snake_case")]
547pub enum CommandExecutionStatus {
548    /// Command finished successfully.
549    #[default]
550    Completed,
551    /// Command failed (non-zero exit code or runtime error).
552    Failed,
553    /// Command is still running and may emit additional output.
554    InProgress,
555}
556
557#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
558#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
559pub struct CommandExecutionItem {
560    /// Tool or command identifier executed by the runner.
561    pub command: String,
562    /// Arguments passed to the tool invocation, when available.
563    #[serde(skip_serializing_if = "Option::is_none")]
564    pub arguments: Option<Value>,
565    /// Aggregated output emitted by the command.
566    #[serde(default)]
567    pub aggregated_output: String,
568    /// Exit code reported by the process, when available.
569    #[serde(skip_serializing_if = "Option::is_none")]
570    pub exit_code: Option<i32>,
571    /// Current status of the command execution.
572    pub status: CommandExecutionStatus,
573}
574
575#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
576#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
577#[serde(rename_all = "snake_case")]
578pub enum ToolCallStatus {
579    /// Tool finished successfully.
580    #[default]
581    Completed,
582    /// Tool failed.
583    Failed,
584    /// Tool is still running and may emit additional output.
585    InProgress,
586}
587
588#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
589#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
590pub struct ToolInvocationItem {
591    /// Name of the invoked tool.
592    pub tool_name: String,
593    /// Structured arguments passed to the tool.
594    #[serde(skip_serializing_if = "Option::is_none")]
595    pub arguments: Option<Value>,
596    /// Raw model-emitted tool call identifier, when available.
597    #[serde(skip_serializing_if = "Option::is_none")]
598    pub tool_call_id: Option<String>,
599    /// Current lifecycle status of the invocation.
600    pub status: ToolCallStatus,
601}
602
603#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
604#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
605pub struct ToolOutputItem {
606    /// Identifier of the related harness invocation item.
607    pub call_id: String,
608    /// Raw model-emitted tool call identifier, when available.
609    #[serde(skip_serializing_if = "Option::is_none")]
610    pub tool_call_id: Option<String>,
611    /// Canonical spool file path when the full output was written to disk.
612    #[serde(skip_serializing_if = "Option::is_none")]
613    pub spool_path: Option<String>,
614    /// Aggregated output emitted by the tool.
615    #[serde(default)]
616    pub output: String,
617    /// Exit code reported by the tool, when available.
618    #[serde(skip_serializing_if = "Option::is_none")]
619    pub exit_code: Option<i32>,
620    /// Current lifecycle status of the output item.
621    pub status: ToolCallStatus,
622}
623
624#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
625#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
626pub struct FileChangeItem {
627    /// List of individual file updates included in the change set.
628    pub changes: Vec<FileUpdateChange>,
629    /// Whether the patch application succeeded.
630    pub status: PatchApplyStatus,
631}
632
633#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
634#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
635pub struct FileUpdateChange {
636    /// Path of the file that was updated.
637    pub path: String,
638    /// Type of change applied to the file.
639    pub kind: PatchChangeKind,
640}
641
642#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
643#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
644#[serde(rename_all = "snake_case")]
645pub enum PatchApplyStatus {
646    /// Patch successfully applied.
647    Completed,
648    /// Patch application failed.
649    Failed,
650}
651
652#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
653#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
654#[serde(rename_all = "snake_case")]
655pub enum PatchChangeKind {
656    /// File addition.
657    Add,
658    /// File deletion.
659    Delete,
660    /// File update in place.
661    Update,
662}
663
664#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
665#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
666pub struct McpToolCallItem {
667    /// Name of the MCP tool invoked by the agent.
668    pub tool_name: String,
669    /// Arguments passed to the tool invocation, if any.
670    #[serde(skip_serializing_if = "Option::is_none")]
671    pub arguments: Option<Value>,
672    /// Result payload returned by the tool, if captured.
673    #[serde(skip_serializing_if = "Option::is_none")]
674    pub result: Option<String>,
675    /// Lifecycle status for the tool call.
676    #[serde(skip_serializing_if = "Option::is_none")]
677    pub status: Option<McpToolCallStatus>,
678}
679
680#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
681#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
682#[serde(rename_all = "snake_case")]
683pub enum McpToolCallStatus {
684    /// Tool invocation has started.
685    Started,
686    /// Tool invocation completed successfully.
687    Completed,
688    /// Tool invocation failed.
689    Failed,
690}
691
692#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
693#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
694pub struct WebSearchItem {
695    /// Query that triggered the search.
696    pub query: String,
697    /// Search provider identifier, when known.
698    #[serde(skip_serializing_if = "Option::is_none")]
699    pub provider: Option<String>,
700    /// Optional raw search results captured for auditing.
701    #[serde(skip_serializing_if = "Option::is_none")]
702    pub results: Option<Vec<String>>,
703}
704
705#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
706#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
707#[serde(rename_all = "snake_case")]
708pub enum HarnessEventKind {
709    PlanningStarted,
710    PlanningCompleted,
711    ContinuationStarted,
712    ContinuationSkipped,
713    BlockedHandoffWritten,
714    EvaluationStarted,
715    EvaluationPassed,
716    EvaluationFailed,
717    RevisionStarted,
718    VerificationStarted,
719    VerificationPassed,
720    VerificationFailed,
721}
722
723#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
724#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
725pub struct HarnessEventItem {
726    /// Specific harness event emitted by the runtime.
727    pub event: HarnessEventKind,
728    /// Optional human-readable message associated with the event.
729    #[serde(skip_serializing_if = "Option::is_none")]
730    pub message: Option<String>,
731    /// Optional verification command associated with the event.
732    #[serde(skip_serializing_if = "Option::is_none")]
733    pub command: Option<String>,
734    /// Optional artifact path associated with the event.
735    #[serde(skip_serializing_if = "Option::is_none")]
736    pub path: Option<String>,
737    /// Optional exit code associated with verification results.
738    #[serde(skip_serializing_if = "Option::is_none")]
739    pub exit_code: Option<i32>,
740}
741
742#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
743#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
744pub struct ErrorItem {
745    /// Error message displayed to the user or logs.
746    pub message: String,
747}
748
749#[cfg(test)]
750mod tests {
751    use super::*;
752    use std::error::Error;
753
754    #[test]
755    fn thread_event_round_trip() -> Result<(), Box<dyn Error>> {
756        let event = ThreadEvent::TurnCompleted(TurnCompletedEvent {
757            usage: Usage {
758                input_tokens: 1,
759                cached_input_tokens: 2,
760                cache_creation_tokens: 0,
761                output_tokens: 3,
762            },
763        });
764
765        let json = serde_json::to_string(&event)?;
766        let restored: ThreadEvent = serde_json::from_str(&json)?;
767
768        assert_eq!(restored, event);
769        Ok(())
770    }
771
772    #[test]
773    fn versioned_event_wraps_schema_version() {
774        let event = ThreadEvent::ThreadStarted(ThreadStartedEvent {
775            thread_id: "abc".to_string(),
776        });
777
778        let versioned = VersionedThreadEvent::new(event.clone());
779
780        assert_eq!(versioned.schema_version, EVENT_SCHEMA_VERSION);
781        assert_eq!(versioned.event, event);
782        assert_eq!(versioned.into_event(), event);
783    }
784
785    #[cfg(feature = "serde-json")]
786    #[test]
787    fn versioned_json_round_trip() -> Result<(), Box<dyn Error>> {
788        let event = ThreadEvent::ItemCompleted(ItemCompletedEvent {
789            item: ThreadItem {
790                id: "item-1".to_string(),
791                details: ThreadItemDetails::AgentMessage(AgentMessageItem {
792                    text: "hello".to_string(),
793                }),
794            },
795        });
796
797        let payload = json::versioned_to_string(&event)?;
798        let restored = json::versioned_from_str(&payload)?;
799
800        assert_eq!(restored.schema_version, EVENT_SCHEMA_VERSION);
801        assert_eq!(restored.event, event);
802        Ok(())
803    }
804
805    #[test]
806    fn tool_invocation_round_trip() -> Result<(), Box<dyn Error>> {
807        let event = ThreadEvent::ItemCompleted(ItemCompletedEvent {
808            item: ThreadItem {
809                id: "tool_1".to_string(),
810                details: ThreadItemDetails::ToolInvocation(ToolInvocationItem {
811                    tool_name: "read_file".to_string(),
812                    arguments: Some(serde_json::json!({ "path": "README.md" })),
813                    tool_call_id: Some("tool_call_0".to_string()),
814                    status: ToolCallStatus::Completed,
815                }),
816            },
817        });
818
819        let json = serde_json::to_string(&event)?;
820        let restored: ThreadEvent = serde_json::from_str(&json)?;
821
822        assert_eq!(restored, event);
823        Ok(())
824    }
825
826    #[test]
827    fn tool_output_round_trip_preserves_raw_tool_call_id() -> Result<(), Box<dyn Error>> {
828        let event = ThreadEvent::ItemCompleted(ItemCompletedEvent {
829            item: ThreadItem {
830                id: "tool_1:output".to_string(),
831                details: ThreadItemDetails::ToolOutput(ToolOutputItem {
832                    call_id: "tool_1".to_string(),
833                    tool_call_id: Some("tool_call_0".to_string()),
834                    spool_path: None,
835                    output: "done".to_string(),
836                    exit_code: Some(0),
837                    status: ToolCallStatus::Completed,
838                }),
839            },
840        });
841
842        let json = serde_json::to_string(&event)?;
843        let restored: ThreadEvent = serde_json::from_str(&json)?;
844
845        assert_eq!(restored, event);
846        Ok(())
847    }
848
849    #[test]
850    fn harness_item_round_trip() -> Result<(), Box<dyn Error>> {
851        let event = ThreadEvent::ItemCompleted(ItemCompletedEvent {
852            item: ThreadItem {
853                id: "harness_1".to_string(),
854                details: ThreadItemDetails::Harness(HarnessEventItem {
855                    event: HarnessEventKind::VerificationFailed,
856                    message: Some("cargo check failed".to_string()),
857                    command: Some("cargo check".to_string()),
858                    path: None,
859                    exit_code: Some(101),
860                }),
861            },
862        });
863
864        let json = serde_json::to_string(&event)?;
865        let restored: ThreadEvent = serde_json::from_str(&json)?;
866
867        assert_eq!(restored, event);
868        Ok(())
869    }
870
871    #[test]
872    fn thread_completed_round_trip() -> Result<(), Box<dyn Error>> {
873        let event = ThreadEvent::ThreadCompleted(ThreadCompletedEvent {
874            thread_id: "thread-1".to_string(),
875            session_id: "session-1".to_string(),
876            subtype: ThreadCompletionSubtype::ErrorMaxBudgetUsd,
877            outcome_code: "budget_limit_reached".to_string(),
878            result: None,
879            stop_reason: Some("max_tokens".to_string()),
880            usage: Usage {
881                input_tokens: 10,
882                cached_input_tokens: 4,
883                cache_creation_tokens: 2,
884                output_tokens: 5,
885            },
886            total_cost_usd: serde_json::Number::from_f64(1.25),
887            num_turns: 3,
888        });
889
890        let json = serde_json::to_string(&event)?;
891        let restored: ThreadEvent = serde_json::from_str(&json)?;
892
893        assert_eq!(restored, event);
894        Ok(())
895    }
896
897    #[test]
898    fn compact_boundary_round_trip() -> Result<(), Box<dyn Error>> {
899        let event = ThreadEvent::ThreadCompactBoundary(ThreadCompactBoundaryEvent {
900            thread_id: "thread-1".to_string(),
901            trigger: CompactionTrigger::Recovery,
902            mode: CompactionMode::Provider,
903            original_message_count: 12,
904            compacted_message_count: 5,
905            history_artifact_path: Some("/tmp/history.jsonl".to_string()),
906        });
907
908        let json = serde_json::to_string(&event)?;
909        let restored: ThreadEvent = serde_json::from_str(&json)?;
910
911        assert_eq!(restored, event);
912        Ok(())
913    }
914}