#![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 diagnostic;
pub use diagnostic::{Diagnostic, DiagnosticAction, DiagnosticSeverity};
pub mod items;
pub use items::{
AgentMessageItem, AppPreviewItem, AppUiPosture, CommandExecutionItem, CommandExecutionStatus, DecisionItem,
FileChangeItem, FileUpdateChange, McpToolCallItem, McpToolCallStatus, PatchApplyStatus, PatchChangeKind, PlanItem,
ReasoningItem, ThreadItem, ThreadItemDetails, ToolCallStatus, ToolInvocationItem, ToolOutcome, ToolOutputItem,
WebSearchItem, tool_outcome_from_status,
};
pub mod matrix;
pub mod planning;
pub mod trace;
pub const EVENT_SCHEMA_VERSION: &str = "0.23.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 event(&self) -> &ThreadEvent {
&self.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;
#[cfg(feature = "telemetry-log")]
mod log_support;
#[cfg(feature = "telemetry-log")]
pub use log_support::PublicLogEmitter as LogEmitter;
#[cfg(feature = "telemetry-tracing")]
mod tracing_support;
#[cfg(feature = "telemetry-tracing")]
pub use tracing_support::PublicTracingEmitter as TracingEmitter;
#[cfg(feature = "telemetry-otel")]
mod otel_support;
#[cfg(feature = "telemetry-otel")]
pub use otel_support::PublicOtelEmitter as OtelEmitter;
#[cfg(feature = "schema-export")]
pub mod schema;
#[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 = "app.sandbox_denied")]
AppSandboxDenied(Box<AppSandboxDeniedEvent>),
#[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)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct AppSandboxDeniedEvent {
pub tool: String,
pub tier: String,
pub uris: Vec<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,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub recovery: Option<Box<planning::PlanningRecoverySnapshot>>,
}
#[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))]
#[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,
SessionMessageSent,
SessionMessageReceived,
SessionMessageExpired,
}
#[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,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
pub struct PermissionResolvedEvent {
pub tool_name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<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 correlation_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>,
}
impl HarnessEventItem {
#[must_use]
pub fn new(event: HarnessEventKind) -> Self {
Self {
event,
message: None,
command: None,
path: None,
exit_code: None,
attempt: None,
error_category: None,
duration_ms: None,
task_id: None,
session_id: None,
correlation_id: None,
exec_session_id: None,
status: None,
transcript_path: None,
archive_path: None,
}
}
}
#[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;