Skip to main content

ai_agents_observability/
event.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5/// Completed observation record that can be aggregated, reported, or exported.
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct ObservationEvent {
8    /// Trace ID shared by all spans in the same operation.
9    pub trace_id: String,
10    /// Unique ID for this event span.
11    pub span_id: String,
12    /// Parent span ID when this event is nested under another span.
13    #[serde(default, skip_serializing_if = "Option::is_none")]
14    pub parent_span_id: Option<String>,
15    /// Chat turn ID for the agent call that produced this event.
16    pub turn_id: String,
17    /// Agent that produced this event.
18    pub agent_id: String,
19    #[serde(default, skip_serializing_if = "Option::is_none")]
20    pub actor_id: Option<String>,
21    #[serde(default, skip_serializing_if = "Option::is_none")]
22    pub session_id: Option<String>,
23    /// What kind of operation produced this event.
24    pub event_type: EventType,
25    /// Why the operation was executed.
26    pub purpose: ObservationPurpose,
27    /// Success, error, cancellation, or skipped state.
28    pub status: EventStatus,
29    /// UTC timestamp when the event was recorded.
30    pub timestamp: DateTime<Utc>,
31    /// Duration of the measured operation in milliseconds.
32    pub duration_ms: u64,
33    #[serde(default, skip_serializing_if = "Option::is_none")]
34    pub tokens: Option<ObservationTokenUsage>,
35    #[serde(default, skip_serializing_if = "Option::is_none")]
36    pub cost: Option<CostEstimate>,
37    #[serde(default, skip_serializing_if = "Option::is_none")]
38    pub error: Option<ObservationError>,
39    /// Safe grouping fields used by aggregators and exporters.
40    #[serde(default)]
41    pub dimensions: HashMap<String, String>,
42    /// Extra safe labels that survived redaction.
43    #[serde(default)]
44    pub tags: HashMap<String, String>,
45    #[serde(default, skip_serializing_if = "Option::is_none")]
46    pub payload: Option<serde_json::Value>,
47}
48
49/// Operation category for an observation event.
50#[derive(Debug, Clone, Serialize, Deserialize)]
51#[serde(rename_all = "snake_case")]
52pub enum EventType {
53    LlmCall {
54        provider: String,
55        model: String,
56        alias: Option<String>,
57        streaming: bool,
58    },
59    ToolCall {
60        tool_id: String,
61    },
62    SkillExecution {
63        skill_id: String,
64    },
65    SkillStep {
66        skill_id: String,
67        step_index: usize,
68        step_type: String,
69    },
70    StateTransition {
71        from: Option<String>,
72        to: String,
73    },
74    Orchestration {
75        pattern: String,
76    },
77    HitlApproval {
78        trigger: String,
79    },
80    MemoryOperation {
81        operation: String,
82    },
83    PersonaEvent {
84        event: String,
85    },
86    FactsEvent {
87        event: String,
88    },
89    RelationshipEvent {
90        event: String,
91    },
92    Error,
93}
94
95/// Reason an observed operation ran, used for cost and latency attribution.
96#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
97#[serde(rename_all = "snake_case")]
98pub enum ObservationPurpose {
99    MainResponse,
100    Router,
101    SkillRouting,
102    SkillPrompt,
103    ProcessDetect,
104    ProcessExtract,
105    ProcessValidate,
106    ProcessTransform,
107    DisambiguationDetection,
108    DisambiguationClarification,
109    StateTransitionEvaluation,
110    StateAction,
111    ContextExtraction,
112    Summarization,
113    ReflectionDecision,
114    ReflectionEvaluation,
115    PlanGeneration,
116    PlanStep,
117    FactsExtraction,
118    RelationshipUpdate,
119    HitlLocalization,
120    OrchestrationRouting,
121    OrchestrationAggregation,
122    OrchestrationConversation,
123    EvaluationJudge,
124    Other(String),
125}
126
127impl ObservationPurpose {
128    /// Converts the purpose to the stable snake_case label used in reports.
129    pub fn as_label(&self) -> String {
130        match self {
131            Self::MainResponse => "main_response".to_string(),
132            Self::Router => "router".to_string(),
133            Self::SkillRouting => "skill_routing".to_string(),
134            Self::SkillPrompt => "skill_prompt".to_string(),
135            Self::ProcessDetect => "process_detect".to_string(),
136            Self::ProcessExtract => "process_extract".to_string(),
137            Self::ProcessValidate => "process_validate".to_string(),
138            Self::ProcessTransform => "process_transform".to_string(),
139            Self::DisambiguationDetection => "disambiguation_detection".to_string(),
140            Self::DisambiguationClarification => "disambiguation_clarification".to_string(),
141            Self::StateTransitionEvaluation => "state_transition_evaluation".to_string(),
142            Self::StateAction => "state_action".to_string(),
143            Self::ContextExtraction => "context_extraction".to_string(),
144            Self::Summarization => "summarization".to_string(),
145            Self::ReflectionDecision => "reflection_decision".to_string(),
146            Self::ReflectionEvaluation => "reflection_evaluation".to_string(),
147            Self::PlanGeneration => "plan_generation".to_string(),
148            Self::PlanStep => "plan_step".to_string(),
149            Self::FactsExtraction => "facts_extraction".to_string(),
150            Self::RelationshipUpdate => "relationship_update".to_string(),
151            Self::HitlLocalization => "hitl_localization".to_string(),
152            Self::OrchestrationRouting => "orchestration_routing".to_string(),
153            Self::OrchestrationAggregation => "orchestration_aggregation".to_string(),
154            Self::OrchestrationConversation => "orchestration_conversation".to_string(),
155            Self::EvaluationJudge => "evaluation_judge".to_string(),
156            Self::Other(value) => value.clone(),
157        }
158    }
159}
160
161impl Default for ObservationPurpose {
162    fn default() -> Self {
163        Self::Other("other".to_string())
164    }
165}
166
167/// Final state of the observed operation.
168#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
169#[serde(rename_all = "snake_case")]
170pub enum EventStatus {
171    Success,
172    Error,
173    Cancelled,
174    Skipped,
175}
176
177/// Token usage attached to an LLM event.
178#[derive(Debug, Clone, Serialize, Deserialize)]
179pub struct ObservationTokenUsage {
180    /// Input or prompt tokens after token config filters are applied.
181    pub input_tokens: u64,
182    /// Output or completion tokens after token config filters are applied.
183    pub output_tokens: u64,
184    /// Sum of input and output tokens.
185    pub total_tokens: u64,
186    /// Where the token numbers came from.
187    pub source: TokenUsageSource,
188}
189
190impl ObservationTokenUsage {
191    /// Creates usage and calculates total_tokens.
192    pub fn new(input_tokens: u64, output_tokens: u64, source: TokenUsageSource) -> Self {
193        Self {
194            input_tokens,
195            output_tokens,
196            total_tokens: input_tokens + output_tokens,
197            source,
198        }
199    }
200}
201
202/// Source of token usage for an LLM event.
203#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
204#[serde(rename_all = "snake_case")]
205pub enum TokenUsageSource {
206    Provider,
207    StreamFinalChunk,
208    Estimated,
209    Missing,
210}
211
212/// Estimated USD cost for one observed LLM event.
213#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct CostEstimate {
215    pub input_usd: f64,
216    pub output_usd: f64,
217    pub total_usd: f64,
218    pub source: CostSource,
219}
220
221/// Source of the cost estimate.
222#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
223#[serde(rename_all = "snake_case")]
224pub enum CostSource {
225    Configured,
226    BuiltIn,
227    Unknown,
228    Estimated,
229}
230
231/// Redacted error metadata attached to failed observation events.
232#[derive(Debug, Clone, Serialize, Deserialize)]
233pub struct ObservationError {
234    /// Stable error class used for grouping.
235    pub kind: String,
236    /// Redacted or truncated error message.
237    pub message: String,
238}
239
240impl ObservationError {
241    /// Creates error metadata before manager-level redaction is applied.
242    pub fn new(kind: impl Into<String>, message: impl Into<String>) -> Self {
243        Self {
244            kind: kind.into(),
245            message: message.into(),
246        }
247    }
248}