Skip to main content

vtcode_exec_events/
items.rs

1//! Timeline item payloads carried by thread item events.
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4
5use super::{Diagnostic, ErrorItem, HarnessEventItem, ItemContext};
6
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
8#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
9pub struct ThreadItem {
10    #[serde(default, skip_serializing_if = "Option::is_none")]
11    pub context: Option<Box<ItemContext>>,
12    /// Stable identifier associated with the item.
13    pub id: String,
14    /// Embedded event details for the item type.
15    #[serde(flatten)]
16    pub details: ThreadItemDetails,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
20#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
21#[serde(tag = "type", rename_all = "snake_case")]
22pub enum ThreadItemDetails {
23    /// Message authored by the agent.
24    AgentMessage(AgentMessageItem),
25    /// Structured plan content authored by the agent in Planning workflow.
26    Plan(PlanItem),
27    /// Free-form reasoning text produced during a turn.
28    Reasoning(Box<ReasoningItem>),
29    /// Public rationale explicitly recorded by the agent; never private reasoning.
30    Decision(Box<DecisionItem>),
31    /// Command execution lifecycle update for an actual shell/PTY process.
32    CommandExecution(Box<CommandExecutionItem>),
33    /// Tool invocation lifecycle update.
34    ToolInvocation(Box<ToolInvocationItem>),
35    /// Tool output lifecycle update tied to a tool invocation.
36    ToolOutput(Box<ToolOutputItem>),
37    /// File change summary associated with the turn.
38    FileChange(Box<FileChangeItem>),
39    /// MCP tool invocation status.
40    McpToolCall(Box<McpToolCallItem>),
41    /// Web search event emitted by a registered search provider.
42    WebSearch(Box<WebSearchItem>),
43    /// Harness-managed continuation or verification lifecycle event.
44    Harness(Box<HarnessEventItem>),
45    /// General error captured for auditing.
46    Error(ErrorItem),
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
50#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
51pub struct AgentMessageItem {
52    /// Textual content of the agent message.
53    pub text: String,
54}
55
56/// A consequential choice and the agent's reported public rationale.
57#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
58#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
59pub struct DecisionItem {
60    pub summary: String,
61    pub rationale: String,
62    #[serde(default)]
63    pub alternatives: Vec<String>,
64    #[serde(default)]
65    pub evidence_ids: Vec<String>,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
69#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
70pub struct PlanItem {
71    /// Plan markdown content.
72    pub text: String,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
76#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
77pub struct ReasoningItem {
78    /// Operational diagnostic independent of private model reasoning.
79    #[serde(default, skip_serializing_if = "Option::is_none")]
80    pub diagnostic: Option<Box<Diagnostic>>,
81    /// Free-form reasoning content captured during planning.
82    pub text: String,
83    /// Optional stage of reasoning (e.g., "analysis", "plan", "verification",
84    /// or the bounded evidence-only "diagnosis" stage).
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub stage: Option<String>,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
90#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
91#[serde(rename_all = "snake_case")]
92pub enum CommandExecutionStatus {
93    /// Command finished successfully.
94    #[default]
95    Completed,
96    /// Command failed (non-zero exit code or runtime error).
97    Failed,
98    /// Command is still running and may emit additional output.
99    InProgress,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
103#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
104pub struct CommandExecutionItem {
105    /// Tool or command identifier executed by the runner.
106    pub command: String,
107    /// Arguments passed to the tool invocation, when available.
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub arguments: Option<Value>,
110    /// Aggregated output emitted by the command.
111    #[serde(default)]
112    pub aggregated_output: String,
113    /// Exit code reported by the process, when available.
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub exit_code: Option<i32>,
116    /// Current status of the command execution.
117    pub status: CommandExecutionStatus,
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
121#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
122#[serde(rename_all = "snake_case")]
123pub enum ToolCallStatus {
124    /// Tool finished successfully.
125    #[default]
126    Completed,
127    /// Tool failed.
128    Failed,
129    /// Tool is still running and may emit additional output.
130    InProgress,
131}
132
133/// Fine-grained outcome of a tool invocation lifecycle.
134///
135/// Mirrors the outcome taxonomy used by the runtime: `status` remains the
136/// coarse lifecycle signal (`Completed` / `Failed` / `InProgress`), while
137/// `outcome` captures *why* the invocation terminated. Consumers that only
138/// need success/failure can continue to read `status`; analytics and the UI
139/// layer use `outcome` for richer classification.
140#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
141#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
142#[serde(rename_all = "snake_case")]
143pub enum ToolOutcome {
144    /// Tool executed and returned a result.
145    #[default]
146    Success,
147    /// Tool executed but returned an error.
148    Error,
149    /// User rejected the permission prompt.
150    PermissionRejected,
151    /// User cancelled the permission prompt (e.g. Ctrl+C / Esc).
152    PermissionCancelled,
153    /// User provided a followup message instead of approving.
154    Followup,
155    /// A user-configured hook blocked execution.
156    HookDenied,
157    /// Tool not found or arguments couldn't be parsed.
158    InvalidTool,
159    /// Tool was cancelled during execution or closed before dispatch when its turn ended.
160    Cancelled,
161}
162
163impl ToolOutcome {
164    #[must_use]
165    pub const fn is_terminal(self) -> bool {
166        !matches!(self, Self::Followup)
167    }
168}
169
170/// Map a terminal [`ToolCallStatus`] to its corresponding [`ToolOutcome`].
171///
172/// # Panics
173///
174/// Panics if `status` is [`ToolCallStatus::InProgress`], which is a non-terminal
175/// state and must never be passed to a completion-event emitter.
176#[must_use]
177#[allow(
178    clippy::unreachable,
179    reason = "Intentional compatibility, platform, or test-only suppression."
180)]
181pub fn tool_outcome_from_status(status: &ToolCallStatus) -> ToolOutcome {
182    match status {
183        ToolCallStatus::Completed => ToolOutcome::Success,
184        ToolCallStatus::Failed => ToolOutcome::Error,
185        ToolCallStatus::InProgress => unreachable!("InProgress status passed to completion event"),
186    }
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
190#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
191pub struct ToolInvocationItem {
192    /// Name of the invoked tool.
193    pub tool_name: String,
194    /// Structured arguments passed to the tool.
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub arguments: Option<Value>,
197    /// Raw model-emitted tool call identifier, when available.
198    #[serde(skip_serializing_if = "Option::is_none")]
199    pub tool_call_id: Option<String>,
200    /// Current lifecycle status of the invocation.
201    pub status: ToolCallStatus,
202    /// Fine-grained outcome of the invocation lifecycle.
203    #[serde(skip_serializing_if = "Option::is_none")]
204    pub outcome: Option<ToolOutcome>,
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
208#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
209pub struct ToolOutputItem {
210    /// Identifier of the related harness invocation item.
211    pub call_id: String,
212    /// Raw model-emitted tool call identifier, when available.
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub tool_call_id: Option<String>,
215    /// Canonical spool file path when the full output was written to disk.
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub spool_path: Option<String>,
218    /// Output snapshot, or a chunk when `is_delta` is true.
219    #[serde(default)]
220    pub output: String,
221    /// Whether an updated item appends to the prior output. Absent means a
222    /// snapshot for compatibility with older event producers.
223    #[serde(default, skip_serializing_if = "Option::is_none")]
224    pub is_delta: Option<bool>,
225    /// Exit code reported by the tool, when available.
226    #[serde(skip_serializing_if = "Option::is_none")]
227    pub exit_code: Option<i32>,
228    /// Current lifecycle status of the output item.
229    pub status: ToolCallStatus,
230    /// MCP Apps referenced by this output, when the tool result was enriched
231    /// with app disclosure. Surfaces render preview affordances from these;
232    /// empty for non-app results. Schema 0.21.0; omitted when empty.
233    #[serde(default, skip_serializing_if = "Vec::is_empty")]
234    pub app_previews: Vec<AppPreviewItem>,
235}
236
237/// Disclosure facts about one MCP Apps resource referenced by a tool output.
238///
239/// Mirrors the disclosure attached at execution time; surfaces must not
240/// re-derive these by parsing rendered output text.
241#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
242#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
243pub struct AppPreviewItem {
244    /// Canonical `ui://` resource URI.
245    pub uri: String,
246    /// MIME type of the first content block, when the server provides one.
247    #[serde(default, skip_serializing_if = "Option::is_none")]
248    pub mime_type: Option<String>,
249    /// Total body bytes across content blocks.
250    #[serde(default)]
251    pub bytes: u64,
252    /// Number of content blocks.
253    #[serde(default)]
254    pub blocks: usize,
255    /// Declared UI sandbox posture, when the server declares `_meta.ui`.
256    #[serde(default, skip_serializing_if = "Option::is_none")]
257    pub ui: Option<AppUiPosture>,
258}
259
260/// Declared sandbox posture of an MCP Apps UI resource (SEP-1865 `UIResourceMeta`).
261#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
262#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
263pub struct AppUiPosture {
264    /// Origins for network requests (`csp.connectDomains`).
265    #[serde(default, skip_serializing_if = "Vec::is_empty")]
266    pub connect_domains: Vec<String>,
267    /// Origins for static resources (`csp.resourceDomains`).
268    #[serde(default, skip_serializing_if = "Vec::is_empty")]
269    pub resource_domains: Vec<String>,
270    /// Origins for nested iframes (`csp.frameDomains`).
271    #[serde(default, skip_serializing_if = "Vec::is_empty")]
272    pub frame_domains: Vec<String>,
273    /// Allowed base URIs (`csp.baseUriDomains`).
274    #[serde(default, skip_serializing_if = "Vec::is_empty")]
275    pub base_uri_domains: Vec<String>,
276    /// Requested permission-policy features (`permissions`).
277    #[serde(default, skip_serializing_if = "Vec::is_empty")]
278    pub permissions: Vec<String>,
279    /// Dedicated sandbox origin for the view (`domain`).
280    #[serde(default, skip_serializing_if = "Option::is_none")]
281    pub domain: Option<String>,
282}
283
284#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
285#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
286pub struct FileChangeItem {
287    /// Captured preview was truncated, suppressed, or unavailable.
288    #[serde(default, skip_serializing_if = "Option::is_none")]
289    pub diff_incomplete: Option<bool>,
290    /// List of individual file updates included in the change set.
291    pub changes: Vec<FileUpdateChange>,
292    /// Whether the patch application succeeded.
293    pub status: PatchApplyStatus,
294    /// Optional precomputed unified diff for the change set.
295    ///
296    /// Populated by the turn diff tracker so consumers can render per-change
297    /// previews without recomputation. Absent in older events.
298    #[serde(default, skip_serializing_if = "Option::is_none")]
299    pub unified_diff: Option<String>,
300    /// Optional added-line count for the change set.
301    #[serde(default, skip_serializing_if = "Option::is_none")]
302    pub additions: Option<u64>,
303    /// Optional deleted-line count for the change set.
304    #[serde(default, skip_serializing_if = "Option::is_none")]
305    pub deletions: Option<u64>,
306}
307
308#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
309#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
310pub struct FileUpdateChange {
311    /// Path of the file that was updated.
312    pub path: String,
313    /// Type of change applied to the file.
314    pub kind: PatchChangeKind,
315    /// Whether the recorded preview normalized line endings (a `\r` was
316    /// present, so the recorded bytes cannot reproduce the original
317    /// exactly). `None` means unknown (legacy events predating the flag).
318    #[serde(default, skip_serializing_if = "Option::is_none")]
319    pub terminators_normalized: Option<bool>,
320}
321
322#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
323#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
324#[serde(rename_all = "snake_case")]
325pub enum PatchApplyStatus {
326    /// Patch successfully applied.
327    Completed,
328    /// Patch application failed.
329    Failed,
330}
331
332#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
333#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
334#[serde(rename_all = "snake_case")]
335pub enum PatchChangeKind {
336    /// File addition.
337    Add,
338    /// File deletion.
339    Delete,
340    /// File update in place.
341    Update,
342}
343
344#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
345#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
346pub struct McpToolCallItem {
347    /// Name of the MCP tool invoked by the agent.
348    pub tool_name: String,
349    /// Arguments passed to the tool invocation, if any.
350    #[serde(skip_serializing_if = "Option::is_none")]
351    pub arguments: Option<Value>,
352    /// Result payload returned by the tool, if captured.
353    #[serde(skip_serializing_if = "Option::is_none")]
354    pub result: Option<String>,
355    /// Lifecycle status for the tool call.
356    #[serde(skip_serializing_if = "Option::is_none")]
357    pub status: Option<McpToolCallStatus>,
358}
359
360#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
361#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
362#[serde(rename_all = "snake_case")]
363pub enum McpToolCallStatus {
364    /// Tool invocation has started.
365    Started,
366    /// Tool invocation completed successfully.
367    Completed,
368    /// Tool invocation failed.
369    Failed,
370}
371
372#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
373#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
374pub struct WebSearchItem {
375    /// Query that triggered the search.
376    pub query: String,
377    /// Search provider identifier, when known.
378    #[serde(skip_serializing_if = "Option::is_none")]
379    pub provider: Option<String>,
380    /// Optional raw search results captured for auditing.
381    #[serde(skip_serializing_if = "Option::is_none")]
382    pub results: Option<Vec<String>>,
383}