#![allow(
missing_docs,
dead_code,
unused_imports,
reason = "Intentional compatibility, platform, or test-only suppression."
)]
use serde::{Deserialize, Serialize};
use serde_json::Value;
pub mod atif;
pub mod matrix;
pub mod trace;
pub const EVENT_SCHEMA_VERSION: &str = "0.18.0";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct VersionedThreadEvent {
schema_version: String,
event: ThreadEvent,
}
impl VersionedThreadEvent {
pub fn new(event: ThreadEvent) -> Self {
Self {
schema_version: EVENT_SCHEMA_VERSION.to_string(),
event,
}
}
pub fn into_event(self) -> ThreadEvent {
self.event
}
}
impl From<ThreadEvent> for VersionedThreadEvent {
fn from(event: ThreadEvent) -> Self {
Self::new(event)
}
}
pub trait EventEmitter {
fn emit(&mut self, event: &ThreadEvent);
}
impl<F> EventEmitter for F
where
F: FnMut(&ThreadEvent),
{
fn emit(&mut self, event: &ThreadEvent) {
self(event);
}
}
#[cfg(feature = "serde-json")]
pub(crate) mod json {
use super::{ThreadEvent, VersionedThreadEvent};
pub fn to_value(event: &ThreadEvent) -> serde_json::Result<serde_json::Value> {
serde_json::to_value(event)
}
pub(crate) fn to_string(event: &ThreadEvent) -> serde_json::Result<String> {
serde_json::to_string(event)
}
pub fn from_str(payload: &str) -> serde_json::Result<ThreadEvent> {
serde_json::from_str(payload)
}
pub(crate) fn versioned_to_string(event: &ThreadEvent) -> serde_json::Result<String> {
serde_json::to_string(&VersionedThreadEvent::new(event.clone()))
}
pub(crate) fn versioned_from_str(payload: &str) -> serde_json::Result<VersionedThreadEvent> {
serde_json::from_str(payload)
}
}
#[cfg(feature = "telemetry-log")]
mod log_support {
use log::Level;
use super::{EventEmitter, ThreadEvent, json};
#[derive(Debug, Clone)]
pub struct LogEmitter {
level: Level,
}
impl LogEmitter {
pub fn new(level: Level) -> Self {
Self { level }
}
}
impl Default for LogEmitter {
fn default() -> Self {
Self { level: Level::Info }
}
}
impl EventEmitter for LogEmitter {
fn emit(&mut self, event: &ThreadEvent) {
if log::log_enabled!(self.level) {
match json::to_string(event) {
Ok(serialized) => log::log!(self.level, "{serialized}"),
Err(err) => log::log!(self.level, "failed to serialize vtcode exec event for logging: {err}"),
}
}
}
}
pub use LogEmitter as PublicLogEmitter;
}
#[cfg(feature = "telemetry-log")]
pub use log_support::PublicLogEmitter as LogEmitter;
#[cfg(feature = "telemetry-tracing")]
mod tracing_support {
use tracing::Level;
use super::{EVENT_SCHEMA_VERSION, EventEmitter, ThreadEvent, VersionedThreadEvent};
#[derive(Debug, Clone)]
pub struct TracingEmitter {
level: Level,
}
impl TracingEmitter {
pub fn new(level: Level) -> Self {
Self { level }
}
}
impl Default for TracingEmitter {
fn default() -> Self {
Self { level: Level::INFO }
}
}
impl EventEmitter for TracingEmitter {
fn emit(&mut self, event: &ThreadEvent) {
match self.level {
Level::TRACE => tracing::event!(
target: "vtcode_exec_events",
Level::TRACE,
schema_version = EVENT_SCHEMA_VERSION,
event = ?VersionedThreadEvent::new(event.clone()),
"vtcode_exec_event"
),
Level::DEBUG => tracing::event!(
target: "vtcode_exec_events",
Level::DEBUG,
schema_version = EVENT_SCHEMA_VERSION,
event = ?VersionedThreadEvent::new(event.clone()),
"vtcode_exec_event"
),
Level::INFO => tracing::event!(
target: "vtcode_exec_events",
Level::INFO,
schema_version = EVENT_SCHEMA_VERSION,
event = ?VersionedThreadEvent::new(event.clone()),
"vtcode_exec_event"
),
Level::WARN => tracing::event!(
target: "vtcode_exec_events",
Level::WARN,
schema_version = EVENT_SCHEMA_VERSION,
event = ?VersionedThreadEvent::new(event.clone()),
"vtcode_exec_event"
),
Level::ERROR => tracing::event!(
target: "vtcode_exec_events",
Level::ERROR,
schema_version = EVENT_SCHEMA_VERSION,
event = ?VersionedThreadEvent::new(event.clone()),
"vtcode_exec_event"
),
}
}
}
pub use TracingEmitter as PublicTracingEmitter;
}
#[cfg(feature = "telemetry-tracing")]
pub use tracing_support::PublicTracingEmitter as TracingEmitter;
#[cfg(feature = "telemetry-otel")]
mod otel_support {
use opentelemetry::KeyValue;
use opentelemetry::trace::{Span, Status, Tracer};
use super::{EventEmitter, ThreadEvent, ThreadItemDetails};
pub struct OtelEmitter<T: Tracer> {
tracer: T,
}
impl<T: Tracer> OtelEmitter<T> {
pub fn new(tracer: T) -> Self {
Self { tracer }
}
}
impl<T: Tracer> EventEmitter for OtelEmitter<T> {
fn emit(&mut self, event: &ThreadEvent) {
let span_name = match event {
ThreadEvent::ThreadStarted(_) => "thread.started",
ThreadEvent::ThreadCompleted(_) => "thread.completed",
ThreadEvent::ContextReset(_) => "context.reset",
ThreadEvent::TurnStarted(_) => "turn.started",
ThreadEvent::TurnCompleted(_) => "turn.completed",
ThreadEvent::TurnFailed(_) => "turn.failed",
ThreadEvent::ItemStarted(_) => "item.started",
ThreadEvent::ItemUpdated(_) => "item.updated",
ThreadEvent::ItemCompleted(_) => "item.completed",
ThreadEvent::Error(_) => "error",
_ => "event",
};
let mut span = self.tracer.start(span_name);
match event {
ThreadEvent::ThreadStarted(e) => {
span.set_attribute(KeyValue::new("thread_id", e.thread_id.clone()));
}
ThreadEvent::ThreadCompleted(e) => {
if let Some(ref cost) = e.total_cost_usd {
span.set_attribute(KeyValue::new("total_cost_usd", cost.as_f64().unwrap_or(0.0)));
}
span.set_attribute(KeyValue::new(
"input_tokens",
i64::try_from(e.usage.input_tokens).unwrap_or(i64::MAX),
));
span.set_attribute(KeyValue::new(
"output_tokens",
i64::try_from(e.usage.output_tokens).unwrap_or(i64::MAX),
));
span.set_attribute(KeyValue::new("completion_subtype", e.subtype.as_str().to_string()));
}
ThreadEvent::ContextReset(e) => {
span.set_attribute(KeyValue::new("thread_id", e.thread_id.clone()));
span.set_attribute(KeyValue::new("turn_id", e.turn_id.clone()));
span.set_attribute(KeyValue::new("plan_preserved", e.plan_preserved));
span.set_attribute(KeyValue::new(
"previous_context_usage_percent",
e.previous_context_usage_percent as i64,
));
span.set_attribute(KeyValue::new("tool_budget_reset", e.tool_budget_reset));
}
ThreadEvent::TurnCompleted(e) => {
span.set_attribute(KeyValue::new(
"turn_input_tokens",
i64::try_from(e.usage.input_tokens).unwrap_or(i64::MAX),
));
span.set_attribute(KeyValue::new(
"turn_output_tokens",
i64::try_from(e.usage.output_tokens).unwrap_or(i64::MAX),
));
}
ThreadEvent::ItemCompleted(e) => {
if let ThreadItemDetails::Harness(harness) = &e.item.details {
span.set_attribute(KeyValue::new("harness_event", format!("{:?}", harness.event)));
if let Some(ref msg) = harness.message {
span.set_attribute(KeyValue::new("harness_message", msg.clone()));
}
if let Some(ref path) = harness.path {
span.set_attribute(KeyValue::new("harness_path", path.clone()));
}
if let Some(dur) = harness.duration_ms {
span.set_attribute(KeyValue::new("duration_ms", i64::try_from(dur).unwrap_or(i64::MAX)));
}
let mut event_attrs = vec![KeyValue::new("event_kind", format!("{:?}", harness.event))];
if let Some(ref msg) = harness.message {
event_attrs.push(KeyValue::new("message", msg.clone()));
}
span.add_event("harness_event", event_attrs);
}
}
ThreadEvent::Error(e) => {
span.set_status(Status::Error { description: e.message.clone().into() });
span.set_attribute(KeyValue::new("error_message", e.message.clone()));
}
_ => {}
}
span.end();
}
}
pub use OtelEmitter as PublicOtelEmitter;
}
#[cfg(feature = "telemetry-otel")]
pub use otel_support::PublicOtelEmitter as OtelEmitter;
#[cfg(feature = "schema-export")]
pub mod schema {
use schemars::{Schema, schema_for};
use super::{ThreadEvent, VersionedThreadEvent};
pub fn thread_event_schema() -> Schema {
schema_for!(ThreadEvent)
}
pub fn versioned_thread_event_schema() -> Schema {
schema_for!(VersionedThreadEvent)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
#[serde(tag = "type")]
pub enum ThreadEvent {
#[serde(rename = "matrix.updated")]
MatrixUpdated(Box<matrix::MatrixSnapshot>),
#[serde(rename = "thread.started")]
ThreadStarted(ThreadStartedEvent),
#[serde(rename = "thread.completed")]
ThreadCompleted(Box<ThreadCompletedEvent>),
#[serde(rename = "thread.compact_boundary")]
ThreadCompactBoundary(Box<ThreadCompactBoundaryEvent>),
#[serde(rename = "context.reset")]
ContextReset(ContextResetEvent),
#[serde(rename = "turn.started")]
TurnStarted(TurnStartedEvent),
#[serde(rename = "turn.completed")]
TurnCompleted(TurnCompletedEvent),
#[serde(rename = "turn.failed")]
TurnFailed(TurnFailedEvent),
#[serde(rename = "turn.blocked")]
TurnBlocked(Box<TurnBlockedEvent>),
#[serde(rename = "item.started")]
ItemStarted(ItemStartedEvent),
#[serde(rename = "item.updated")]
ItemUpdated(ItemUpdatedEvent),
#[serde(rename = "item.completed")]
ItemCompleted(ItemCompletedEvent),
#[serde(rename = "permission.requested")]
PermissionRequested(PermissionRequestedEvent),
#[serde(rename = "permission.resolved")]
PermissionResolved(PermissionResolvedEvent),
#[serde(rename = "interjected")]
Interjected(InterjectedEvent),
#[serde(rename = "plan.delta")]
PlanDelta(Box<PlanDeltaEvent>),
#[serde(rename = "plan.approval.requested")]
PlanApprovalRequested(PlanApprovalRequestedEvent),
#[serde(rename = "plan.approval.resolved")]
PlanApprovalResolved(PlanApprovalResolvedEvent),
#[serde(rename = "error")]
Error(ThreadErrorEvent),
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct ThreadStartedEvent {
pub thread_id: String,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum ThreadCompletionSubtype {
Success,
ErrorMaxTurns,
ErrorMaxBudgetUsd,
ErrorDuringExecution,
Cancelled,
#[serde(other)]
Unknown,
}
impl ThreadCompletionSubtype {
pub const fn as_str(&self) -> &'static str {
match self {
Self::Success => "success",
Self::ErrorMaxTurns => "error_max_turns",
Self::ErrorMaxBudgetUsd => "error_max_budget_usd",
Self::ErrorDuringExecution => "error_during_execution",
Self::Cancelled => "cancelled",
Self::Unknown => "unknown",
}
}
pub const fn is_success(self) -> bool {
matches!(self, Self::Success)
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum CompactionTrigger {
Manual,
Auto,
Recovery,
ModelSwitch,
#[serde(other)]
Unknown,
}
impl CompactionTrigger {
pub const fn as_str(self) -> &'static str {
match self {
Self::Manual => "manual",
Self::Auto => "auto",
Self::Recovery => "recovery",
Self::ModelSwitch => "model_switch",
Self::Unknown => "unknown",
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum CompactionMode {
Provider,
Local,
#[serde(other)]
Unknown,
}
impl CompactionMode {
pub const fn as_str(self) -> &'static str {
match self {
Self::Provider => "provider",
Self::Local => "local",
Self::Unknown => "unknown",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct ThreadCompletedEvent {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub completed_at: Option<String>,
pub thread_id: String,
pub session_id: String,
pub subtype: ThreadCompletionSubtype,
pub outcome_code: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_reason: Option<String>,
pub usage: Usage,
#[serde(skip_serializing_if = "Option::is_none")]
pub total_cost_usd: Option<serde_json::Number>,
pub num_turns: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct ThreadCompactBoundaryEvent {
pub thread_id: String,
pub trigger: CompactionTrigger,
pub mode: CompactionMode,
pub original_message_count: usize,
pub compacted_message_count: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub history_artifact_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub previous_segment_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub new_segment_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub previous_prefix_hash: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub new_prefix_hash: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub previous_catalog_hash: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub new_catalog_hash: Option<String>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum ContextResetTrigger {
PlanApproval,
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct ContextResetEvent {
pub thread_id: String,
pub turn_id: String,
pub trigger: ContextResetTrigger,
pub plan_preserved: bool,
pub previous_context_usage_percent: u8,
pub tool_budget_reset: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct TurnStartedEvent {
#[serde(skip_serializing_if = "Option::is_none")]
token_breakdown: Option<Box<TokenBreakdown>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub context: Option<Box<ExecutionContext>>,
}
impl TurnStartedEvent {
pub fn token_breakdown(&self) -> Option<&TokenBreakdown> {
self.token_breakdown.as_deref()
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum InputOrigin {
User,
Correction,
PlanApproval,
Continuation,
Retry,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct ExecutionContext {
pub task_id: String,
pub turn_id: String,
pub actor_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent_actor_id: Option<String>,
pub origin: InputOrigin,
pub timestamp: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub goal: Option<String>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum CommandActivity {
Inspection,
Verification,
Mutation,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct ItemContext {
pub task_id: String,
pub turn_id: String,
pub actor_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent_actor_id: Option<String>,
pub timestamp: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub activity: Option<CommandActivity>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct TokenBreakdown {
system_prompt_tokens: u64,
tool_schema_tokens: u64,
instruction_file_tokens: u64,
message_history_tokens: u64,
cache_read_tokens: u64,
cache_write_tokens: u64,
cache_miss_tokens: u64,
#[serde(skip_serializing_if = "Option::is_none")]
subagent_bootstrap_tokens: Option<u64>,
}
pub const MAX_IN_PROGRESS_EXEC_SESSIONS: usize = 4;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct TurnCompletedEvent {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub completed_at: Option<Box<String>>,
pub usage: Usage,
#[serde(
default,
skip_serializing_if = "Vec::is_empty",
deserialize_with = "deserialize_null_as_default"
)]
pub in_progress_exec_sessions: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct TurnFailedEvent {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub completed_at: Option<Box<String>>,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage: Option<Usage>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct TurnBlockedEvent {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub completed_at: Option<String>,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_tool: Option<String>,
#[serde(default)]
pub blocked_streak: usize,
#[serde(default)]
pub blocked_total: usize,
#[serde(default)]
pub consecutive_cap: usize,
#[serde(default)]
pub total_cap: usize,
#[serde(default)]
pub recovery_active: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub usage: Option<Usage>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct ThreadErrorEvent {
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct Usage {
#[serde(default, deserialize_with = "deserialize_null_as_default")]
pub input_tokens: u64,
#[serde(default, deserialize_with = "deserialize_null_as_default")]
pub cached_input_tokens: u64,
#[serde(default, deserialize_with = "deserialize_null_as_default")]
pub cache_creation_tokens: u64,
#[serde(default, deserialize_with = "deserialize_null_as_default")]
pub output_tokens: u64,
}
pub fn deserialize_null_as_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
D: serde::Deserializer<'de>,
T: Deserialize<'de> + Default,
{
Ok(Option::<T>::deserialize(deserializer)?.unwrap_or_default())
}
impl Usage {
#[must_use]
fn uncached_input_tokens(&self) -> u64 {
self.input_tokens
.saturating_sub(self.cached_input_tokens)
.saturating_sub(self.cache_creation_tokens)
}
#[must_use]
pub fn cache_hit_rate(&self) -> Option<f64> {
if self.input_tokens == 0 {
return None;
}
Some(self.cached_input_tokens as f64 / self.input_tokens as f64)
}
#[must_use]
pub fn cache_summary(&self) -> String {
let total_input = self.input_tokens;
if total_input == 0 {
return "No input tokens recorded.".to_string();
}
let cached = self.cached_input_tokens;
let creation = self.cache_creation_tokens;
let uncached = self.uncached_input_tokens();
let rate = cached as f64 / total_input as f64 * 100.0;
format!(
"Cache: {cached} cached / {total_input} total input ({rate:.1}% hit rate), \
{creation} cache-creation, {uncached} uncached"
)
}
pub fn add(&mut self, other: &Usage) {
self.input_tokens = self.input_tokens.saturating_add(other.input_tokens);
self.cached_input_tokens = self.cached_input_tokens.saturating_add(other.cached_input_tokens);
self.cache_creation_tokens = self.cache_creation_tokens.saturating_add(other.cache_creation_tokens);
self.output_tokens = self.output_tokens.saturating_add(other.output_tokens);
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct ItemCompletedEvent {
pub item: ThreadItem,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct ItemStartedEvent {
pub item: ThreadItem,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct ItemUpdatedEvent {
pub item: ThreadItem,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct PlanDeltaEvent {
pub thread_id: String,
pub turn_id: String,
pub item_id: String,
pub delta: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct PlanApprovalRequestedEvent {
pub thread_id: String,
pub turn_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub plan_file: Option<String>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum PlanApprovalDecision {
Execute,
AutoAccept,
FreshContext,
Revise,
Cancel,
SwitchBuild,
SwitchAuto,
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct PlanApprovalResolvedEvent {
pub thread_id: String,
pub turn_id: String,
pub decision: PlanApprovalDecision,
pub automatic: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct ThreadItem {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub context: Option<Box<ItemContext>>,
pub id: String,
#[serde(flatten)]
pub details: ThreadItemDetails,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ThreadItemDetails {
AgentMessage(AgentMessageItem),
Plan(PlanItem),
Reasoning(Box<ReasoningItem>),
Decision(Box<DecisionItem>),
CommandExecution(Box<CommandExecutionItem>),
ToolInvocation(Box<ToolInvocationItem>),
ToolOutput(Box<ToolOutputItem>),
FileChange(Box<FileChangeItem>),
McpToolCall(Box<McpToolCallItem>),
WebSearch(Box<WebSearchItem>),
Harness(Box<HarnessEventItem>),
Error(ErrorItem),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct AgentMessageItem {
pub text: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct DecisionItem {
pub summary: String,
pub rationale: String,
#[serde(default)]
pub alternatives: Vec<String>,
#[serde(default)]
pub evidence_ids: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct PlanItem {
pub text: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct ReasoningItem {
pub text: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub stage: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum CommandExecutionStatus {
#[default]
Completed,
Failed,
InProgress,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct CommandExecutionItem {
pub command: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<Value>,
#[serde(default)]
pub aggregated_output: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub exit_code: Option<i32>,
pub status: CommandExecutionStatus,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum ToolCallStatus {
#[default]
Completed,
Failed,
InProgress,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum ToolOutcome {
#[default]
Success,
Error,
PermissionRejected,
PermissionCancelled,
Followup,
HookDenied,
InvalidTool,
Cancelled,
}
impl ToolOutcome {
#[must_use]
pub const fn is_terminal(self) -> bool {
!matches!(self, Self::Followup)
}
}
#[must_use]
#[allow(
clippy::unreachable,
reason = "Intentional compatibility, platform, or test-only suppression."
)]
pub fn tool_outcome_from_status(status: &ToolCallStatus) -> ToolOutcome {
match status {
ToolCallStatus::Completed => ToolOutcome::Success,
ToolCallStatus::Failed => ToolOutcome::Error,
ToolCallStatus::InProgress => unreachable!("InProgress status passed to completion event"),
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct ToolInvocationItem {
pub tool_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
pub status: ToolCallStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub outcome: Option<ToolOutcome>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct ToolOutputItem {
pub call_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub spool_path: Option<String>,
#[serde(default)]
pub output: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub exit_code: Option<i32>,
pub status: ToolCallStatus,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct FileChangeItem {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub diff_incomplete: Option<bool>,
pub changes: Vec<FileUpdateChange>,
pub status: PatchApplyStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub unified_diff: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub additions: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub deletions: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct FileUpdateChange {
pub path: String,
pub kind: PatchChangeKind,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum PatchApplyStatus {
Completed,
Failed,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum PatchChangeKind {
Add,
Delete,
Update,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct McpToolCallItem {
pub tool_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<McpToolCallStatus>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum McpToolCallStatus {
Started,
Completed,
Failed,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct WebSearchItem {
pub query: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub provider: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub results: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum HarnessEventKind {
PlanningStarted,
PlanningCompleted,
ContinuationStarted,
ContinuationSkipped,
TurnBlocked,
BlockedRecoveryStarted,
BlockedRecoveryFinished,
BlockedHandoffWritten,
BlockedHandoffResolved,
EvaluationStarted,
EvaluationPassed,
EvaluationFailed,
RevisionStarted,
EscalationTriggered,
EscalationBypassed,
VerificationStarted,
VerificationPassed,
VerificationFailed,
ErrorRecovered,
ToolRetryAttempted,
ToolLatencyRecorded,
SnapshotCreated,
SnapshotRestored,
SessionToolLimitIncreased,
ToolLoopLimitIncreased,
BackgroundSubprocessCompleted,
DelegatedAgentStatus,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum PermissionDecision {
Allow,
Deny,
Cancelled,
Followup,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct PermissionRequestedEvent {
pub tool_name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct PermissionResolvedEvent {
pub tool_name: String,
pub decision: PermissionDecision,
pub wait_ms: u64,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum InterjectionSource {
Direct,
Queue,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum RedirectKind {
Interjection,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct InterjectedEvent {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub text: Option<Box<String>>,
pub source: InterjectionSource,
pub image_count: u32,
pub redirect_kind: RedirectKind,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct HarnessEventItem {
pub event: HarnessEventKind,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub command: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub exit_code: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub attempt: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error_category: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub duration_ms: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub task_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub exec_session_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub transcript_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub archive_path: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct ErrorItem {
pub message: String,
}
#[cfg(test)]
mod tests;